这节教程是关于在光下物体如何在一个平面(plannar)产生阴影(Shadow),先看看这节教程的结构吧:
一,平面公式的推导。
可能你会疑问了,我们不是求阴影的?怎么推导平面公式?大家不要急,先慢慢往下看我如何一步一步推导出阴影的原理和公式.
先看图:
看图中的平面,已知平面的法向量N(nx,ny,nz),平面上的一点P(Xp,Yp,Zp),那么假设该平面上任意一点为A(x,y,z), 由题意知道向量PA⊥N,则PA·N=0,
PA=(x-Xp,y-Yp,z-Zp)
联立上面得 nx*x+ny*y*nz*z+(-Xp*nx-Yp*ny-Zp*nz)=0;
假设d=(-Xp*nx-Yp*ny-Zp*nz)
最终得到公式nx*x+ny*y*nz*z+d=0;
也就是说由平面的法向量N(nx,ny,nz)和一个参数d就能得到平面公式
nx*x+ny*y*nz*z+d=0; (公式1)
写成另外一个形式: N•A+d=0; ("•"为向量点乘)
二,线段(或者直线)公式的推导。
(1)情况一,单端射线
先来看看线段公式的推导,
看这幅图,为一条射线,Po为射线的起点,P1为射线经过的一个点,假设向量 U=P1-P, 假设P(t)为射线上任意一个点,则有公式
P(t)=Po+t*U, t>=0且t为实数 (公式2)
(2)情况二,双端射线
在来看一幅图:
此时,这是一条以Po为起点的双端射线,跟上面一样,推导出公式,假设P(t)为射线上任意一个点
P(t)=Po+t*U, t为实数 (公式3)
(2)情况三,线段
看图:
Po为线段的起点,P1为线段的终点,假设向量 U=P1-P, 假设P(t)为线段上任意一个点,则有公式
P(t)=Po+t*U, 0.0=<t<=1.0 (公式4)
三,求射线和平面交点的推导。
由公式1知道
N•A+d=0;
由公式2知道
P(t)=Po+t*U, t>=0且t为实数 (公式2)
将P(t)=P+t*U带入公式1,得到
N*(Po+t*U)+d=0;
N•Po+t*N•U+d=0;
t*N•U=-N•Po-d;
t=(-N•Po-d)/(N•U);
P(t)=Po+t*U=Po+((-N•Po-d)/(N•U))*U; (公式5)
这里求出了t,将t代入公式4,自然而然得到了线段和平面的交点P(t),但是得注意的是t的范围为[0.0,+∞],若计算出的t不在这个范围,则代表平面和线段没有交点.
四,投影公式的推导。
(1)平行光生成阴影(Shadow)
在3D图形中,每个3D物体一般都是由单个三角面或者多个三角面组成的,入射光线将每个三角面投影到对应的平面形成那个平面的有一定形状的三角面,
如图:
三角面A为原三角面,三角面B为投影形成的三角面,注意三角面A和三角面B平面不一定是平行的,此图仅仅刚好是看起来平行的
已知L为入射平行光向量,P为原三角面的一个顶点,投影面公式的方向量n和d都已知,则由公式5可推出入射光射线L跟投影面的交点:
点P=(Px,Py,Pz,1)
得到平行光投影(Shadow)矩阵公式
怎么知道右边那个矩阵是不是Shadow变换矩阵呢?直接从eq.10.1公式计算是很不实际的,逆过来计算,先计算出S',由S′的值来逆推eq.10.1的公式,看一不一样
这条公式的i可能是x,y或者z ,但是又有一个新问题,S' 不等于Si',其实关系是 S'/(n
•L)=Si',也就是P点乘以上面那个Shadow变换矩阵得到的投影点S为齐次坐标,其W值为
n
•L,也就是X,Y,Z,W值未经规格化的,但是不要担心,由于在3D渲染流水线中我们在齐次裁剪空间进行了裁剪之后,会进行透视除法自动进行规格化,所以在世界空间坐标点(2*x,2*y,2*z,2)
和(x,y,z,1)是同一个点,不需要我们在世界空间对S点进行手动规格化而变为普通坐标(即W=1.0)。
但是一个新的问题又来了,由于投影得到的点S的W值为
n
•L,万一n•L为负值怎么办?在D3D11渲染流水线一个点的坐标的W值为负时,在齐次裁剪空间中会被视截体当做视截体外的点裁剪掉,如图:
上面我们都是基于平行光生成的阴影(Shadow)
(2)点光源生成阴影(Shadow)
先看看点光源模型:
看平行光求交点的公式
由于点光源L下,P点受到的入射光线方向为(P-L),带入上面的公式,得到点光源下求出投影点S的公式:
下面点光源Shadow变换矩阵跟上面平行光Shadow变换矩阵类似,看下面:
(3)对平行光和点光源生成阴影(Shadow)的通用变换矩阵
上面公式 一,当Lw=0,矩阵刚好就是平行光阴影(Shadow)变换矩阵
二,当Lw=1,矩阵刚好就是点光源阴影(Shadow)变换矩阵
看看D3D11给出求ShadowMatrix的函数:
这条公式第一个参数为阴影平面,这里我在以前的
D3D11教程十六之ClipPlane(裁剪面)说过平面怎么命名的,
看第二个参数光源的位置,由于光源位置是一个向量,存在X,Y,Z,W四个值,
当W=0.0时,为平行光,但是实际的光的方向跟参数相反的。
当w=1.0f时,相当于在(x,y,z)处放一个点光源。
五,阴影重叠(Double Blend)现象的消除。
说了一大堆,也许大家也看烦了,但是为了显示最后美好的阴影效果,大家还是听我唠叨完最后一步,哈哈哈。
按照前四大点,我们知道阴影是怎么来的,怎么用函数XMMatrixShadow()求出ShadowMatrix,但是如果按照上面的方法来生成阴影有一个问题,什么问题呢?也许大家想到了,
对,就是阴影重叠问题,由于我们经常绘制的物体经常是由成百上千个三角面组成的,那么其在一个投影面上得到的阴影三角面也是数量很多的,这么多阴影面的绘制,由于我们将阴影面绘制成黑色或者暗灰色,跟投影面混合在一起显示效果(ID3D11BlendState),但是我们知道这么多阴影三角面一定有不少是重叠一部分面积的,而且可能重叠好多次,那么重叠的地方显示的阴影一定是比其它不重叠的阴影暗很多,所以我们怎么保证阴影的绘制不重叠呢?这时候我们的"StencilBuffer"出来了。
上节
D3D11教程二十八之PlannarReflection(基于stencilBuffer的实现)我们已经介绍过StencilBuffer的使用了,这次我们再次用一个StencilBuffer
我们说过初始时我们设置StencilBuffer所有的值为0,我们可以仅对StencilRef为0的位置进行阴影绘制, 并且绘制一次后对StencilBuffer 相应位置的值加1,这样下次就不会绘制对那个位置进行阴影绘制了,代码如下:
创建绘制阴影时候的DepthStencilState
ID3D11DepthStencilState* md3dDrawShadowDSS; //绘制阴影的DSS
ZeroMemory(&DSDESC, sizeof(DSDESC));
DSDESC.DepthEnable = true; //深度测试开启
DSDESC.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL; //深度写
DSDESC.DepthFunc = D3D11_COMPARISON_LESS; //小于原Z才能通过
DSDESC.StencilEnable = true; //模板测试开启
DSDESC.StencilReadMask = 0xff;
DSDESC.StencilWriteMask = 0xff;
//前面设定
DSDESC.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; //stencil失败 怎么更新stencil buffer
DSDESC.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;// stencil通过,但depth失败
DSDESC.FrontFace.StencilPassOp = D3D11_STENCIL_OP_INCR; //stencil通过 depth通过时,每次加1,并且进行clamp夹逼运算
DSDESC.FrontFace.StencilFunc = D3D11_COMPARISON_EQUAL; //总是能通过StencilTest
//背面设定 //背面设定,在光栅化状态剔除背面时这个设定没用,但是依然要设定,不然无法创建深度(模板)状态
DSDESC.BackFace.StencilFailOp = D3D11_STENCIL_OP_REPLACE;
DSDESC.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
DSDESC.BackFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
DSDESC.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
HR(md3dDevice->CreateDepthStencilState(&DSDESC, &md3dDrawShadowDSS));
Texture2D ShaderTexture:register(t0); //纹理资源
SamplerState SampleType:register(s0); //采样方式
//VertexShader
cbuffer CBMatrix:register(b0)
{
matrix World;
matrix View;
matrix Proj;
matrix WorldInvTranspose;
};
cbuffer CBLight:register(b1)
{
float4 DiffuseColor;
float3 LightDirection;
float pad;
}
struct VertexIn
{
float3 Pos:POSITION;
float2 Tex:TEXCOORD0; //多重纹理可以用其它数字
float3 Normal:NORMAL;
};
struct VertexOut
{
float4 Pos:SV_POSITION;
float2 Tex:TEXCOORD0;
float3 W_Normal:NORMAL; //世界空间的法线
};
VertexOut VS(VertexIn ina)
{
VertexOut outa;
outa.Pos = mul(float4(ina.Pos,1.0f), World);
outa.Pos = mul(outa.Pos, View);
outa.Pos = mul(outa.Pos, Proj);
outa.W_Normal = mul(ina.Normal, (float3x3)WorldInvTranspose); //此事世界逆转置矩阵的第四行本来就没啥用
outa.W_Normal = normalize(outa.W_Normal);
outa.Tex= ina.Tex;
return outa;
}
float4 PS(VertexOut outa) : SV_Target
{
float4 color = {0.1f,0.1f,0.1f,1.0f}; //最终输出的颜色
return color;
}
最终绘制阴影的函数
bool GraphicsClass::RenderShadow()
{
//World,View,Proj,以及ShadowMatrix
XMMATRIX WorldMatrix, ViewMatrix, ProjMatrix, ShadowMatrix;
bool result;
//更新)获取ViewMatrix(根据CameraClass的mPostion和mRotation来生成的)
mCamera->Render();
//获取三个变换矩阵(WorldMatrix和ProjMatrix来自mD3D类,ViewMatrix来自CameraClass)
WorldMatrix = mD3D->GetWorldMatrix();
ProjMatrix = mD3D->GetProjMatrix();
ViewMatrix = mCamera->GetViewMatrix();
//获取阴影矩阵,这里用的是平行光 ,实际的光方向与参数相反
//mLight->SetLightDirection(XMVectorSet(-1.0f, 1.0f, 0.0f, 0.0f)); <span style="font-family: Arial, Helvetica, sans-serif;">//仅取三个数 XY平面 斜向下45对角</span>
//投影面为y=-2.0f的投影面 用(0.0f,1.0f,0.0f,2.0f)表示投影面
XMVECTOR ShadowPlane=XMVectorSet(0.0f, 1.0f, 0.0f, 2.0f);
XMFLOAT4 LightDirection;
XMStoreFloat4(&LightDirection, mLight->GetLightDirection());
LightDirection.w = 0.0f; //w=0.0代表平行光,w=1.0f代表点光源 平行光阴影和点光源阴影形状也许不一样
XMVECTOR ParallelLight = XMLoadFloat4(&LightDirection);
ShadowMatrix = XMMatrixShadow(ShadowPlane,ParallelLight);
//对世界矩阵进行变换,这里乘以<span style="font-family: Arial, Helvetica, sans-serif;">XMMatrixTranslation(0.0f,0.01f,0.0f)是把阴影稍微往上平移一点距离</span>
WorldMatrix = WorldMatrix*XMMatrixRotationZ(XM_PI / 4.0f)*ShadowMatrix*XMMatrixTranslation(0.0f,0.01f,0.0f);
//开启混合颜色模式
mD3D->TurnOnBaseAlphaBlend();
//开启绘制阴影的DepthStencilState
mD3D->SetDrawShadowDepthStencilState();
//将立方体的数据放入3D渲染流水线
mCubeModel->Render(mD3D->GetDeviceContext());
//用ShadowShader进行立方体阴影的渲染
result = mShadowShader->Render(mD3D->GetDeviceContext(), mCubeModel->GetIndexCount(), WorldMatrix, ViewMatrix, ProjMatrix, mCubeModel->GetTexture(), mLight->GetDiffuseColor(), mLight->GetLightDirection());
if (!result)
{
MessageBox(NULL, L"ShadowShader Render failure", NULL, MB_OK);
return false;
}
//关闭混合模式,恢复默认的DepthStencilState
mD3D->TurnOffAlphaBlend();
mD3D->SetDefaultDepthStencilState();
return true;
}
看下面的程序运行:
1,平行光生成阴影,平行光参数(-1.0f,1.0f,0.0f,0.0f),注意这不是平行光方向,这很容易让人误导的,这么认为实际的平行光方向为 该参数的负数
2,点光源生成阴影,点光源位置(-3.0f,3.0f,0.0f,1.0f)
下面是我的源代码链接: