这里我用DirectX9实现一遍逐片段光照与阴影贴图,阴影贴图的算法与OpenGL的相同,各位仔细看好了.
首先声明四个着色器并获取它们的常量句柄:
void initShaders() { storeVertexShader=new Shader("../shader/store_vs.fx",SHADER_TYPE_VS); modelMatrixStoreHand=storeVertexShader->constTable->GetConstantByName(NULL,"modelMatrix"); viewMatrixStoreHand=storeVertexShader->constTable->GetConstantByName(NULL,"viewMatrix"); projMatrixStoreHand=storeVertexShader->constTable->GetConstantByName(NULL,"projMatrix"); storePixShader=new Shader("../shader/store_ps.fx",SHADER_TYPE_PS); texVertexShader=new Shader("../shader/tex_vs.fx",SHADER_TYPE_VS); modelMatrixHand=texVertexShader->constTable->GetConstantByName(NULL,"modelMatrix"); viewMatrixHand=texVertexShader->constTable->GetConstantByName(NULL,"viewMatrix"); projMatrixHand=texVertexShader->constTable->GetConstantByName(NULL,"projMatrix"); normalMatrixHand=texVertexShader->constTable->GetConstantByName(NULL,"normalMatrix"); shadowMatrixHand=texVertexShader->constTable->GetConstantByName(NULL,"shadowMatrix"); texPixShader=new Shader("../shader/tex_ps.fx",SHADER_TYPE_PS); ambHand=texPixShader->constTable->GetConstantByName(NULL,"amb"); diffHand=texPixShader->constTable->GetConstantByName(NULL,"diff"); lightDirHand=texPixShader->constTable->GetConstantByName(NULL,"lightDir"); ambMtlHand=texPixShader->constTable->GetConstantByName(NULL,"ambMtl"); diffMtlHand=texPixShader->constTable->GetConstantByName(NULL,"diffMtl"); }
store开头的顶点与片段着色器在渲染阴影贴图时使用,用于保存以光源位置为视点的场景深度,接着是取得mvp变换矩阵的句柄.
tex开头的顶点与片段着色器在渲染场景时使用,用于执行深度比较以及光照运算,然后也是获得mvp变换矩阵,法线变换矩阵,光源视点投影变换矩阵和各种光照参数的句柄.
有了这些句柄我们就可以给着色器中的常量传值了.
接着设置光照参数:
float lightd[4] = {1.0f, 1.0f, -1.0f, 0.0f}; float ambientLight[4] = {0.7f, 0.7f, 0.7f, 1.0f}; float diffuseLight[4] = {1.0f, 1.0f, 1.0f, 1.0f}; float ambientLightMtl[4] = {0.7f, 0.7f, 0.7f, 1.0f}; float diffuseLightMtl[4] = {1.0f, 1.0f, 1.0f, 1.0f}; void initLight() { D3DXVECTOR4 amb(ambientLight); D3DXVECTOR4 diff(diffuseLight); D3DXVECTOR4 lightDir(lightd); D3DXVECTOR4 ambMtl(ambientLightMtl); D3DXVECTOR4 diffMtl(diffuseLightMtl); texPixShader->constTable->SetVector(d3d,ambHand,&amb); texPixShader->constTable->SetVector(d3d,diffHand,&diff); texPixShader->constTable->SetVector(d3d,lightDirHand,&lightDir); texPixShader->constTable->SetVector(d3d,ambMtlHand,&ambMtl); texPixShader->constTable->SetVector(d3d,diffMtlHand,&diffMtl); }
这几个光照参数各种固定管线教程中已经出现n多遍了,看到这些名字就应该知道意思了吧.
进行阴影贴图渲染的时候传递一下世界变换矩阵给shader,就像这样:
D3DXMATRIX modelMatrix,scaleMat,rotXMat,rotYMat,tranMat; D3DXMatrixIdentity(&modelMatrix); D3DXMatrixScaling(&scaleMat,model->scale,model->scale,model->scale); D3DXMatrixRotationX(&rotXMat,atr(model->yr)); D3DXMatrixRotationY(&rotYMat,atr(model->xr)); D3DXMatrixTranslation(&tranMat,model->x,model->y,model->z); modelMatrix=modelMatrix*scaleMat*rotXMat*rotYMat*tranMat; storeVertexShader->constTable->SetMatrix(d3d,modelMatrixStoreHand,&modelMatrix); model->renderSimple();
进行普通场景渲染的时候还需要多传递一个法线变换矩阵,务必记住法线世界变换矩阵不是物体的世界变换矩阵,就像这样:
D3DXMATRIX modelMatrix,scaleMat,rotXMat,rotYMat,tranMat; D3DXMatrixIdentity(&modelMatrix); D3DXMatrixScaling(&scaleMat,model->scale,model->scale,model->scale); D3DXMatrixRotationX(&rotXMat,atr(model->yr)); D3DXMatrixRotationY(&rotYMat,atr(model->xr)); D3DXMatrixTranslation(&tranMat,model->x,model->y,model->z); modelMatrix=modelMatrix*scaleMat*rotXMat*rotYMat*tranMat; texVertexShader->constTable->SetMatrix(d3d,modelMatrixHand,&modelMatrix); D3DXMATRIX normalMatrix; D3DXMatrixIdentity(&normalMatrix); D3DXMatrixInverse(&normalMatrix,NULL,&modelMatrix); D3DXMatrixTranspose(&normalMatrix,&normalMatrix); texVertexShader->constTable->SetMatrix(d3d,normalMatrixHand,&normalMatrix); model->render();
接着取得默认的渲染目标与深度缓存:
d3d->GetRenderTarget(0,&defaultRenderTarget); d3d->GetDepthStencilSurface(&defaultDepthSurface);
然后创建阴影纹理,阴影纹理渲染目标与渲染阴影纹理使用的深度缓存:
d3d->CreateTexture(shadowMapSize,shadowMapSize,1,D3DUSAGE_RENDERTARGET, D3DFMT_R32F,D3DPOOL_DEFAULT,&shadowMapTexture,NULL); d3d->CreateDepthStencilSurface(shadowMapSize,shadowMapSize,D3DFMT_D24X8, D3DMULTISAMPLE_NONE,0,TRUE,&shadowDepthSurface,NULL); shadowMapTexture->GetSurfaceLevel(0,&shadowRenderTarget);
请注意,这里阴影纹理的格式是R32F,这意味着阴影贴图只有r通道可用,gba通道不可用.
然后我们要的光源是方向光,那么光投影矩阵是个正交矩阵,要这么创建:
float size=128; D3DXMatrixIdentity(&lightProjectionMatrix); D3DXMatrixOrthoOffCenterLH(&lightProjectionMatrix,-size,size,-size*2,size*2,-size*2,size*2);
这边的size可以自己定义,数值越大,阴影贴图范围越大,但是精细度下降.
随后建立光照空间视图矩阵:
D3DXVECTOR3 viewPos(lightPos.x,lightPos.y,lightPos.z); D3DXVECTOR3 toVec(cx,cy,cz); D3DXVECTOR3 upVec(0.0f,1.0f,0.0f); D3DXMatrixIdentity(&lightViewMatrix); D3DXMatrixLookAtLH(&lightViewMatrix,&viewPos,&toVec,&upVec);
最后开始渲染部分,首先渲染阴影贴图:
void generateShadowMap(CallbackPtr callback) { d3d->SetRenderTarget(0,shadowRenderTarget); d3d->SetDepthStencilSurface(shadowDepthSurface); d3d->Clear(0,NULL,D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,D3DXCOLOR(1.0f,1.0f,1.0f,1.0f),1.0f,0); d3d->BeginScene(); d3d->SetRenderState(D3DRS_CULLMODE,D3DCULL_CW); d3d->SetVertexShader(storeVertexShader->vsProg); d3d->SetPixelShader(storePixShader->psProg); storeVertexShader->constTable->SetMatrix(d3d,projMatrixStoreHand,&lightProjectionMatrix); storeVertexShader->constTable->SetMatrix(d3d,viewMatrixStoreHand,&lightViewMatrix); callback(); d3d->SetRenderState(D3DRS_CULLMODE,D3DCULL_CCW); d3d->EndScene(); }
将渲染目标与深度缓存变为阴影贴图的,然后清除颜色和深度,都重设为1.0以表示最大深度,渲染深度可以采用顺时针剔除正面,然后就能够少渲染很多东西而且结果也不会受影响,渲染完毕之后再重设回逆时针.
然后计算光空间投影变换矩阵:
void generateShadowMatrix() { shadowMatrix=lightViewMatrix*lightProjectionMatrix; }
随后进行一次普通场景渲染,把光空间投影变换矩阵传递给texVertexShader,以及摄像机使用的视图与投影变换矩阵:
void drawShadowMap(CallbackPtr callback) { d3d->SetDepthStencilSurface(defaultDepthSurface); d3d->SetRenderTarget(0,defaultRenderTarget); d3d->Clear(0,NULL,D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,D3DXCOLOR(0.5f,0.7f,0.8f,0.5f),1.0f,0); d3d->BeginScene(); d3d->SetVertexShader(texVertexShader->vsProg); d3d->SetPixelShader(texPixShader->psProg); texVertexShader->constTable->SetMatrix(d3d,viewMatrixHand,&cameraViewMatrix); texVertexShader->constTable->SetMatrix(d3d,projMatrixHand,&cameraProjectionMatrix); texVertexShader->constTable->SetMatrix(d3d,shadowMatrixHand,&shadowMatrix); d3d->SetTexture(0,shadowMapTexture); callback(); d3d->EndScene(); }
注意,在渲染之前把渲染目标和深度缓存切换到默认的,这是渲染到屏幕而不是渲染到纹理.然后清除默认颜色与深度缓存.
cpp编码到此结束,接下来看shader.
首先我们用storeVertexShader把场景顶点变换到光照空间,然后使用storePixShader保存场景深度值到阴影纹理
storeVertexShader:
float4x4 modelMatrix,viewMatrix,projMatrix; struct Input { float4 position : POSITION; float4 normal : NORMAL; float2 texCoord0 : TEXCOORD0; }; struct Output { float4 position : POSITION; float4 clipVert : TEXCOORD1; }; Output main(Input input) { float4 modelVert = input.position; float4x4 mvpMatrix = mul(modelMatrix, mul(viewMatrix, projMatrix)); float4 clipVert = mul(modelVert, mvpMatrix); Output output = (Output)0; output.position = clipVert; output.clipVert = clipVert; return output; }
注意,最终的计算结果是属于光照裁剪空间,范围-w - +w.
storePixShader:
struct Input { float4 clipVert : TEXCOORD1; }; struct Output { float4 pixColor : COLOR; }; Output main(Input input) { float4 clipVert = input.clipVert; float depth = clipVert.z / clipVert.w; Output output = (Output)0; output.pixColor = float4(depth, depth, depth, depth); return output; }
对深度值做齐次除法使其在0-1的范围.注意,输出纹理的范围在0-1,属于光照纹理空间.
接着正常渲染场景的时候使用texVertexShader与texPixShader.
texVertexShader:
float4x4 modelMatrix,viewMatrix,projMatrix,normalMatrix; float4x4 shadowMatrix; struct Input { float4 position : POSITION; float4 normal : NORMAL; float2 texCoord0 : TEXCOORD0; }; struct Output { float4 position : POSITION; float2 texCoord0 : TEXCOORD0; float3 normal : TEXCOORD1; float4 shadowVert : TEXCOORD2; }; Output main(Input input) { float4x4 modelViewProjMatrix = mul(modelMatrix, mul(viewMatrix, projMatrix)); float4 modelVert = input.position; float4 modelNormal = input.normal; float2 texCoord0 = input.texCoord0; float3 worldNormal = mul(modelNormal.xyz, (float3x3)normalMatrix); float4 clipVert = mul(modelVert, modelViewProjMatrix); float4 worldVert = mul(modelVert, modelMatrix); float4 shadowVert = mul(worldVert, shadowMatrix); Output output = (Output)0; output.position = clipVert; output.texCoord0 = texCoord0; output.normal = worldNormal; output.shadowVert = shadowVert; return output; }
shadowVert的计算结果是在光照裁剪空间,要在片段着色器中进行如下变换:
光照裁剪空间->光照标准坐标空间->光照纹理空间
切记,只有在同一空间内的向量才能进行运算.
接着是texPixShader:
float4 amb,diff,ambMtl,diffMtl,lightDir; texture TextureShadow; texture Texture1; texture Texture2; sampler texShadow = sampler_state { Texture = <Texture0>; MinFilter = LINEAR; MagFilter = LINEAR; }; sampler tex1 = sampler_state { Texture = <Texture1>; MinFilter = LINEAR; MagFilter = LINEAR; }; sampler tex2 = sampler_state { Texture = <Texture2>; MinFilter = LINEAR; MagFilter = LINEAR; }; struct Input { float2 texCoord0 : TEXCOORD0; float3 normal : TEXCOORD1; float4 shadowVert : TEXCOORD2; }; struct Output { float4 pixColor : COLOR; }; float calcShadowFactor(float4 shadowVert) { float4 shadowNDCVert = shadowVert / shadowVert.w; float2 shadowVertCoord = 0.5 * shadowNDCVert.xy + float2(0.5, 0.5); shadowVertCoord.y = 1.0 - shadowVertCoord.y; float depthNow = shadowNDCVert.z; float depthStore = tex2D(texShadow, shadowVertCoord).r; float factor = 1.0; float bias = -0.00005; if(abs(shadowNDCVert.x) <= 1.0 && abs(shadowNDCVert.y) <=1.0 && abs(shadowNDCVert.z) <=1.0) factor = (depthNow + bias > depthStore) ? 0.0 : 1.0; return factor; } float4 calcLight(float3 normal,float shadowFactor) { float4 light = float4(0.0,0.0,0.0,1.0); float nDotL = max(dot(normalize(lightDir.xyz), normalize(normal)), 0.0); light = amb * ambMtl + diff * diffMtl * nDotL * shadowFactor; return light; } Output main(Input input) { float4 texColor = (0.6 * tex2D(tex1, input.texCoord0)) + (0.4 * tex2D(tex2, input.texCoord0)); float shadowFactor = calcShadowFactor(input.shadowVert); float4 lightColor = calcLight(input.normal, shadowFactor); Output output = (Output)0; output.pixColor = lightColor * texColor; return output; }
首先进行一次齐次除法计算shadowNDCVert 光照标准坐标空间坐标,接着对它进行坐标缩放与偏移取得shadowVertCoord 光照纹理空间坐标,注意OpenGL与DirectX对于纹理坐标表示的一点区别,OpenGL纹理坐标y轴向上,DirectX纹理坐标y轴向下,最后与阴影贴图中的深度相比取得阴影参数传递个光照计算函数计算光照,把阴影参数乘以漫反射光就可以得到阴影了,光照公式与之前基于顶点的光照公式一样.
下面看看结果如何:
有好多锯齿啊,没关系我们可以在生成阴影纹理的shader里采用一些过滤算法消除掉这些难看的锯齿.
具体的阴影过滤算法可以参考:
http://fabiensanglard.net/shadowmappingPCF/index.php
http://fabiensanglard.net/shadowmappingVSM/index.php