2008年10月31日星期五

陰影映射

完成了紋理投影後,製作基本陰影映射就如吃生一樣容易。




// Pixel shader

varying vec3 normal, lightDir, halfVector;
varying vec2 colorCoord;
varying vec4 shadowCoord;
uniform sampler2D colorTex;
uniform sampler2DShadow shadowTex;

// Light intensity inside shadow
const float shadowIntensity = 0.5;

// Should be supplied as uniform
const float shadowMapPixelScale = 1.0 / float(2048);
const int pcfSize = 1; // The pcf filtering size, 0 -> 1x1 (no filtering), 1 -> 3x3 etc

void main(void)
{
vec4 diffuse = gl_FrontLightProduct[0].diffuse;
vec4 specular = gl_FrontLightProduct[0].specular;
vec4 ambient = gl_FrontLightProduct[0].ambient;
vec3 n = normalize(normal);
float NdotL = max(dot(n, lightDir), 0.0);

diffuse *= NdotL;
vec3 halfV = normalize(halfVector);
float NdotHV = max(dot(n, halfV), 0.0);
specular *= pow(NdotHV, gl_FrontMaterial.shininess);

// Get the shadow value, let the hardware perform perspective divide,
// depth comparison and 2x2 pcf if supported.
// float shadowValue = shadow2DProj(shadowTex, shadowCoord).r;

// Perform PCF filtering
float shadowValue = 0.0;
for(int i=-pcfSize; i<=pcfSize; ++i) for(int j=-pcfSize; j<=pcfSize; ++j)
{
vec4 offset = vec4(i * shadowMapPixelScale, j * shadowMapPixelScale, 0, 0);
shadowValue += shadow2DProj(shadowTex, shadowCoord + offset).r;
}
shadowValue /= (2 * pcfSize + 1) * (2 * pcfSize + 1);

float shadowSpecularFactor = shadowValue == 0 ? 0 : 1;
float shadowDiffuseFactor = min(1.0, shadowIntensity + shadowValue);

gl_FragData[0] = shadowSpecularFactor * specular +
(ambient + shadowDiffuseFactor * diffuse) * vec4(texture2D(colorTex, colorCoord).xyz, 1);
}


當然,還有許多的陰影技術可以嘗試;我比較臨感興趣的有:


相關文章

紋理投影

為了製作陰影映射 (Shadow mapping),先來一個紋理投影 (Projective texture)。




製作紋理投影的關鍵是紋理投影矩陣,是它把物體的世界座標轉換成投影空間的紋理座標;以下是 OpenGL fixed pipeline 的實作:


// The following code assums you have already applied the camera's view matrix
// to the model-view matrix stack
setupViewMatrix();

// Use another texture unit to avoid conflit with the color texture of the model
glActiveTexture(GL_TEXTURE1);

// You can choose between GL_MODULATE and GL_ADD
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_ADD);

// Matrix44 is just a simple matrix 4x4 class, but remember opengl use column major layout
// The bias matrix is to map from clip space [-1, 1] to texture space [0, 1]
Matrix44 biasMatrix = Matrix44(
0.5f, 0, 0, 0.5f,
0, 0.5f, 0, 0.5f,
0, 0, 0.5f, 0.5f,
0, 0, 0, 1.0f);

Matrix44 projectorProjection, projectorView;

// Setup projectorProjection and projectorView according to
// how you want the texture to be projected on the scene
// ...

Matrix44 textureMatrix = biasMatrix * projectorProjection * projectorView;

// Preform a transpose so that we get the rows of the matrix rather than columns
textureMatrix = textureMatrix.transpose();

// A post-multiply by the inverse of the CURRENT modelview matrix is applied
// by opengl automatically to the eye plane equations we provide.
// Therefor, it is important to enable these texture coordinate generation
// before appling any model-world matrix transform
glTexGenfv(GL_S, GL_EYE_PLANE, textureMatrix[0]); // Row 0
glTexGenfv(GL_T, GL_EYE_PLANE, textureMatrix[1]); // Row 1
glTexGenfv(GL_R, GL_EYE_PLANE, textureMatrix[2]); // Row 2
glTexGenfv(GL_Q, GL_EYE_PLANE, textureMatrix[3]); // Row 3

glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);

// Enable automatic texture coordinate generation
// Note that the R and Q component may not be used in simple projective texture
// but they are needed for shadow mapping
glEnable(GL_TEXTURE_GEN_S);
glEnable(GL_TEXTURE_GEN_T);
glEnable(GL_TEXTURE_GEN_R);
glEnable(GL_TEXTURE_GEN_Q);

// Bind the projector's texture
glBindTexture(GL_TEXTURE_2D, textureHandle);

// You may move the clamp setting to where you initialize the texture
// rather than setting up every frame
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP);

// Set the active texture back to the model's color texture
glActiveTexture(GL_TEXTURE0);

// For each model:
// Apply any world transform for your model
// Draw the model
// End


當我試圖把上述的碼轉移到 glsl,我遇到了一個沒有多被紋理投影項目中詳述的問題,那就是如何在 glsl 裡得到物體的世界座標。這個問題沒有在 fixed pipeline 中出現是因為早於應用物體 - 世界矩陣 (Model-world matrix) 之前,那紋理投影矩陣已計算恰當。縱然 glsl (其實是整個 OpenGL) 沒有單獨的物體 - 世界矩陣可供查詢,我們可以把攝像機的視圖矩陣乘以 gl_ModelViewMatrix 求出物體 - 世界矩陣。


glActiveTexture(GL_TEXTURE1);

Matrix44 biasMatrix = Matrix44(
0.5f, 0, 0, 0.5f,
0, 0.5f, 0, 0.5f,
0, 0, 0.5f, 0.5f,
0, 0, 0, 1.0f);

// We need the camera's view matrix inverse in order to obtain the model-world
// transform in glsl
Matrix44 projectorProjection, projectorView, cameraView;

// Setup projectorProjection, projectorView and cameraView
// ...

Matrix44 textureMatrix =
biasMatrix * projectorProjection * projectorView * cameraView.inverse();

// Set up the texture matrix
glMatrixMode(GL_TEXTURE);
glLoadMatrixf(textureMatrix.getPtr());
glMatrixMode(GL_MODELVIEW);

glBindTexture(GL_TEXTURE_2D, textureHandle);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP);

glActiveTexture(GL_TEXTURE0);

// For each model:
// Apply any world transform for your model
// Draw the model
// End


// Vertex shader:
varying vec3 normal, lightDir, halfVector;
varying vec2 colorCoord;
varying vec4 projectiveCoord;

void main(void)
{
gl_Position = ftransform();
normal = gl_NormalMatrix * gl_Normal;
lightDir = normalize(gl_LightSource[0].position.xyz);
halfVector = normalize(gl_LightSource[0].halfVector.xyz);

colorCoord = gl_MultiTexCoord0.xy;

// gl_TextureMatrix[1] should contains the inverse of the view matrix,
// resulting a model matrix when combining with gl_ModelViewMatrix
projectiveCoord = gl_TextureMatrix[1] * gl_ModelViewMatrix * gl_Vertex;
}

// Pixel shader:
varying vec3 normal, lightDir, halfVector;
varying vec2 colorCoord;
varying vec4 projectiveCoord;
uniform sampler2D colorTex;
uniform sampler2D projectiveTex;

void main(void)
{
vec4 diffuse = gl_FrontLightProduct[0].diffuse;
vec4 specular = gl_FrontLightProduct[0].specular;
vec4 ambient = gl_FrontLightProduct[0].ambient;
vec3 n = normalize(normal);
float NdotL = max(dot(n, lightDir), 0.0);

diffuse *= NdotL;
vec3 halfV = normalize(halfVector);
float NdotHV = max(dot(n, halfV), 0.0);
specular *= pow(NdotHV, gl_FrontMaterial.shininess);

gl_FragData[0] = specular + (ambient + diffuse) * vec4(texture2D(colorTex, colorCoord).xyz, 1);

// Apply the projective texture
gl_FragData[0] += texture2DProj(projectiveTex, projectiveCoord);
}

2008年10月13日星期一

免費 Model 寶庫

今天無意中發現了一個博客非常慷慨地把大量高品質 (相對其他免費) 的 3D 模型分享給全世界。
站內有不同種類的模型,但還是汽車的居多;雖然下載的方法有點煩,畢竟是免費的,好應該說聲多謝。

http://i344.photobucket.com/albums/p338/free3dart/nsx_HP_small.jpg

http://i344.photobucket.com/albums/p338/free3dart/f18.jpg

2008年9月22日星期一

SSAO 新進展

Yeah! 利用了法線緩衝所提供的資訊後, SSAO (屏幕空間環境光遮蔽) 的效果迫真了許多。
開始感受到電腦繪圖算法的迷人之處,可惜再沒有人和我分享這份喜悅sad




讓我嘗試簡單地解釋它的原理吧。
螢幕中的每一像素都會和它周圍的 N 個像素作比較,比較時有兩個因數需要考慮
  1. 兩像素於三圍空間中的位置;深度較淺的像素會遮蔽較深的像素,而遮蔽的程度就取決於距離。

  2. 兩像素的法線內積 (Dot product);面向面的像素會比面向同一方向的像素較接觸不到外來的光線。
至於怎樣對周圍的 N 個像素取樣,就是整個算法中最令人頭痛的問題。當然取樣越多效果越理想,但實際經驗告訴大家 N 只可以不大於 32 左右。隨著取樣的數量受限,而又希望有比較廣闊的取樣範圍 (位置較遙遠的像素都可互相影響),可用一些隨機取樣模式;不過暫時我只用了一個十字形的取樣模式,只要取樣範圍不太大是可以接受的。


uniform sampler2DRect texColor; // Color texture
uniform sampler2DRect texDepth; // Depth texture
uniform sampler2DRect texNormal;// Normal texture
uniform vec2 camerarange = vec2(1.0, 500);

varying vec2 texCoord;
const float aoCap = 1.0;
float aoMultiplier = 1000.0;

float pw = 1.0; // Use (1.0 / screensize.x) for GL_TEXTURE2D
float ph = 1.0;

float readDepth(in vec2 coord)
{
float nearZ = camerarange.x;
float farZ = camerarange.y;
float posZ = texture2DRect(texDepth, coord).x;

return (2.0 * nearZ) / (nearZ + farZ - posZ * (farZ - nearZ));
}

vec3 readNormal(in vec2 coord)
{
return normalize(2 * (texture2DRect(texNormal, coord).xyz - 1));
}

float compareDepths(in float depth1, in float depth2)
{
float depthDiff = depth1 - depth2;
const float aorange = 10.0; // Units in space the AO effect extends to (this gets divided by the camera far range)
float diff = clamp(1.0 - depthDiff * (camerarange.y - camerarange.x) / aorange, 0.0, 1.0);
return min(aoCap, max(0.0, depthDiff) * aoMultiplier) * diff;
}

float calAO(float depth, vec3 normal, float dw, float dh)
{
vec2 coord = vec2(texCoord.x + dw, texCoord.y + dh);
float angleFactor = 1 - dot(normal, readNormal(coord));

if(length(normal) == 0)
angleFactor = 0;

return angleFactor * compareDepths(depth, readDepth(coord));
}

void main(void)
{
float depth = readDepth(texCoord);
float ao = 0.0;

vec3 normal = readNormal(texCoord);

for(int i=0; i<8; ++i) {
ao += calAO(depth, normal, pw, ph);
ao += calAO(depth, normal, pw, -ph);
ao += calAO(depth, normal, -pw, ph);
ao += calAO(depth, normal, -pw, -ph);

pw *= 1.4;
ph *= 1.4;
aoMultiplier /= 1.5;
}

ao *= 2.0;

gl_FragColor = vec4(1.0 - ao) * texture2DRect(texColor, texCoord);
}



相關文章

2008年9月14日星期日

《星海爭霸2》引擎技術解析



CryEngine 2Finding Next Gen 之後,一向不與學術界為伍的 Blizzard 也不甘示弱;於 Siggraph 08 發表了一篇論文,當中的內容頗為深入。
期望星海爭霸2可快點推出。

2008年9月13日星期六

初嚐 Shader 編程



完成基本的 Shader 類別後,一口氣連 Multiple Render Target (MRT) 和 Screen Space Ambient Occlusion (SSAO) 都攪定了。這叫 SSAO 的技術是近一年電腦遊戲繪圖領域的新寵兒;它的原理是利用深度緩衝 (Depth Buffer) 計算出當前考慮中的像素和它周圍的像素,於三圍空間中的相互關係,再加上法線緩衝的話,就可以知道這像素有沒有被其他像素所 "遮蔽"。

暫時我的實作只用上了深度緩衝,算不上真正的 SSAO,至多是一個邊緣強調器;但出來的效果也不錯,可凸顯出物件的層次感。現有的實作會繼續改進之餘,亦會留下來給低級別的顯示卡使用。

Vertex shader code:

// Screen space ambient occlusion
// Reference:
// http://www.opengl.org/discussion_boards/ubbthreads.php?ubb=showflat&Number=236698&fpart=1
// http://www.4gamer.net/games/047/G004713/20080223007/screenshot.html?num=002
// http://rgba.scenesp.org/iq/computer/articles/ssao/ssao.htm
// http://meshula.net/wordpress/?p=145

varying vec2 texCoord;

void main(void)
{
gl_Position = ftransform();
texCoord = gl_MultiTexCoord0.xy;
gl_FrontColor = gl_Color;
}


Pixel shader code:


uniform sampler2D texColor; // Color texture
uniform sampler2D texDepth; // Depth texture

uniform vec2 camerarange = vec2(1.0, 500);
uniform vec2 screensize;

varying vec2 texCoord;

float readDepth(in vec2 coord)
{
return (2.0 * camerarange.x) /
(camerarange.y + camerarange.x - texture2D(texDepth, coord).x * (camerarange.y - camerarange.x));
}

void main(void)
{
float depth = readDepth(texCoord);
float d;

float pw = 1.0 / screensize.x;
float ph = 1.0 / screensize.y;

float aoCap = 1.0;

float ao = 0.0;

float aoMultiplier = 1000.0;

float depthTolerance = 0.0001;

for(int i=0; i<4; ++i)
{
d = readDepth(vec2(texCoord.x + pw, texCoord.y + ph));
ao += min(aoCap, max(0.0, depth - d - depthTolerance) * aoMultiplier);

d = readDepth(vec2(texCoord.x - pw, texCoord.y + ph));
ao += min(aoCap, max(0.0, depth - d - depthTolerance) * aoMultiplier);

d=readDepth(vec2(texCoord.x + pw, texCoord.y - ph));
ao += min(aoCap, max(0.0, depth - d - depthTolerance) * aoMultiplier);

d = readDepth(vec2(texCoord.x - pw, texCoord.y - ph));
ao += min(aoCap, max(0.0, depth - d - depthTolerance) * aoMultiplier);

pw *= 2.0;
ph *= 2.0;
aoMultiplier /= 2.0;
}

ao /= 16.0;

gl_FragColor = vec4(1.0 - ao) * texture2D(texColor, texCoord);
}


最後還有一些未解決的問題,是關於 MRT 的;話說有些顯示卡並未支援非二乘方大小的材質緩衝,因此有必要使用 GL_TEXTURE_RECTANGLE_ARB,可惜用了這材質格式後 Pixel Shader 又神奇地把遮蔽量計錯了。看來 Shader 的除錯方法還要好好領會。

還有,材質緩衝是不支援 Fullscreen Anti-aliasing (FSAA) 的,這可以怎樣解決哩?

3DS轉換矩陣

經過半天的勞力,終於成功載入轉換矩陣 (0x4160 trunk),全靠 lib3ds 的原碼。

// Trunk 0x4160 comes before 0x4120,
// during the loading of 0x4160 we got the information to
// change the clockwise/anti-clockwise triangle winding or not
// and apply this information during the loading of 0x4120
bool invertTriangleWinding = false;

// ...

// Loading the local coordinates chunk
// It's base from lib3ds: http://www.lib3ds.org
case 0x4160:
{
// We are using row major matrix
Mat44f matrix = Mat44f::cIdentity;
for(size_t i=0; i<4; ++i)
mStream->read(matrix.row[i], sizeof(float) * 3);

// Flip X coordinate of vertices if mesh matrix has negative determinant
if((invertWinding = (matrix.determinant() < 0)) == true) {
Mat44f inv = matrix.inverse();

matrix.m00 = -matrix.m00;
matrix.m01 = -matrix.m01;
matrix.m02 = -matrix.m02;
matrix.m03 = -matrix.m03;

matrix = (inv * matrix).transpose();

size_t vertexCount = getVertexCount();
Vec3f* vertex = getVertexPointer();

for(size_t i=0; i<vertexCount; ++i) {
// Transform tmp using matrix, where matrix[3] holds the translation
#ifdef FLIP_YZ_AXIS
Vec4f tmp(vertex[i].x, -vertex[i].z, vertex[i].y, 0);
tmp = (matrix * tmp) + matrix[3];
vertex[i] = Vec3f(tmp.x, tmp.z, -tmp.y);
#else
Vec4f tmp(vertex[i].x, vertex[i].y, vertex[i].z, 0);
tmp = (matrix * tmp) + matrix[3];
vertex[i] = Vec3f(tmp.x, tmp.y, tmp.z);
#endif
}
}
} break;

// ...

// Loading the face description (index values) trunk
case 0x4120:
{
uint16_t faceCount = 0;
uint16_t* indexArray = nullptr;

// ...

if(invertTriangleWinding) {
for(size_t i=0; i<faceCount*3; i+=3)
std::swap(indexArray[i], indexArray[i+2]);
}
} break;