光照在游戏中的渲染是非常重要的。但是如何保证我们实时渲染光照的同时,还能把游戏帧率保持在30以上就很值得人思考了。
环境光,主要是用来保证环境中没有直接被光源照射的部分没有直接变黑。
我们用一个分量乘法来实现环境光
eg. LightAmbient(0.5, 0.5, 0.5) * MaterialDiffuse(0.75, 1.0, 0.5) = (0.375, 0.5, 0.25)
用环境光和材质的颜色相乘。得到被光照射后的结构。
Diffuse
漫射光,当光射到表面时,我们要保证光从表面每个角度的反射都是一样的。
这样我们就不需要考虑摄像机的位置。当我们看向一个被漫射光照射的物体,物体的颜色和我们看向表面的角度没有一毛钱关系。
首先考虑,平面是否在光照中,计算系数(这里不需要考虑人眼和平面的角度,但是还是要考虑光照和平面的角度)
inLight = max(lightDirection * surfaceNormal, 0)
然后计算漫射光颜色(假设系数为1)
1 * LightDiffuse(1.0, 1.0, 1.0) * MaterialDiffuse(0.75, 1.0, 0.5) = (0.75, 1.0, 0.5)
Specular
这是计算最昂贵的光照,如果光照直射表面,它会让我们产生一点刺眼的感觉
Emissive
这个其实不算是一种光。它不会发出光照亮其他物体,它只是会让物体自身发光。
举个例子就是灯泡,灯泡自身是很亮的,但这个和它真正发出的光没有半毛钱关系。
这里有三种不同的光源:方向光,点光源,聚光灯
Directional Lights (A.K.A. Parallel Lights)
也叫平行光,它没有具体的光源位置,最好的例子就是太阳。
Point Lights
点光源有具体的位置,和一个衰减的范围。,但没有方向而言。
Spotlights
聚光灯是计算最复杂的光源。他们有方向,位置,颜色,和两个角度,以及衰减范围。
锥形中心的位置显然应该比锥形边缘的位置要亮。而从内角到外角开始,就会有一个光照衰减。
法线用来定义一个面的朝向,我们这里用来确定一个面是否在光的照射范围内。
世界坐标的变换,让法线向量可能大于1,也可能小于0,所以我们这里用一个HLSL中的操作,使他们归一化,
Vertex Normals
当定义顶点的时候,我们还需要定义顶点的法线。然后在PS阶段,我们需要确定每个像素接收到的光照。
如果我们创建一个球体的时候,我们就需要用到一个技术叫做法线平均,这样会让球体看起来光滑。否则的话,球体看起来就是几个平面拼在一起的。
Face Normals
我们在定义顶点的时候一般不定义面法线。但是面法线是由顶点法线取平均得到的。
Normal Averaging
法线平均技术是让灯光减少块状效果的,通过平均每个相邻平面的法线,这样,光照在相邻表面移动式,会逐渐变化,而不是突然变化。
我们这里需要申请一个缓存,来保存一个每帧都需要改变的对象。
ID3D11Buffer* cbPerFrameBuffer;
现在我们还需要发送物体的世界坐标矩阵。因为我们希望光照是在世界坐标的情况下在effect file中计算的。
struct cbPerObject
{
XMMATRIX WVP;
XMMATRIX World;
};
我们现在需要创建一个光照结构体。来传入effect file中。
struct Light
{
Light()
{
ZeroMemory(this, sizeof(Light));
}
XMFLOAT3 dir;
float pad;
XMFLOAT4 ambient;
XMFLOAT4 diffuse;
};
Light light;
cbPerFrame Structure
因为HLSL是每个4D Vector向量打包一次,发送到effect file中的。如果没有pad这个变量,那么dir这个3D Vector就会和ambient中的第一个数字被打包成一个4D Vector
我们把下面这个结构发送到PS Shader中。
struct cbPerFrame
{
Light light;
};
cbPerFrame constbuffPerFrame;
The UpdateScene() Functions New Parameter
这样把Light封装到 cbPerFrame中,可能是比较好理解 constant buffer的概念。
void UpdateScene(double time)
现在我们UpdateScene接收一个时间参数。
因为我们要更新灯光,所以要新传入顶点的法线结构。
struct Vertex //Overloaded Vertex Structure
{
Vertex(){}
Vertex(float x, float y, float z,
float u, float v,
float nx, float ny, float nz)
: pos(x,y,z), texCoord(u, v), normal(nx, ny, nz){}
XMFLOAT3 pos;
XMFLOAT2 texCoord;
XMFLOAT3 normal;
};
D3D11_INPUT_ELEMENT_DESC layout[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 20, D3D11_INPUT_PER_VERTEX_DATA, 0}
};
别忘了清除指针。。
现在我们定义灯光的各个变量。
light.dir = XMFLOAT3(0.25f, 0.5f, -1.0f);
light.ambient = XMFLOAT4(0.2f, 0.2f, 0.2f, 1.0f);
light.diffuse = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
Vertex v[] =
{
// Front Face
Vertex(-1.0f, -1.0f, -1.0f, 0.0f, 1.0f,-1.0f, -1.0f, -1.0f),
Vertex(-1.0f, 1.0f, -1.0f, 0.0f, 0.0f,-1.0f, 1.0f, -1.0f),
Vertex( 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, -1.0f),
Vertex( 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f),
// Back Face
Vertex(-1.0f, -1.0f, 1.0f, 1.0f, 1.0f,-1.0f, -1.0f, 1.0f),
Vertex( 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, -1.0f, 1.0f),
Vertex( 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f),
Vertex(-1.0f, 1.0f, 1.0f, 1.0f, 0.0f,-1.0f, 1.0f, 1.0f),
// Top Face
Vertex(-1.0f, 1.0f, -1.0f, 0.0f, 1.0f,-1.0f, 1.0f, -1.0f),
Vertex(-1.0f, 1.0f, 1.0f, 0.0f, 0.0f,-1.0f, 1.0f, 1.0f),
Vertex( 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f),
Vertex( 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f),
// Bottom Face
Vertex(-1.0f, -1.0f, -1.0f, 1.0f, 1.0f,-1.0f, -1.0f, -1.0f),
Vertex( 1.0f, -1.0f, -1.0f, 0.0f, 1.0f, 1.0f, -1.0f, -1.0f),
Vertex( 1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, -1.0f, 1.0f),
Vertex(-1.0f, -1.0f, 1.0f, 1.0f, 0.0f,-1.0f, -1.0f, 1.0f),
// Left Face
Vertex(-1.0f, -1.0f, 1.0f, 0.0f, 1.0f,-1.0f, -1.0f, 1.0f),
Vertex(-1.0f, 1.0f, 1.0f, 0.0f, 0.0f,-1.0f, 1.0f, 1.0f),
Vertex(-1.0f, 1.0f, -1.0f, 1.0f, 0.0f,-1.0f, 1.0f, -1.0f),
Vertex(-1.0f, -1.0f, -1.0f, 1.0f, 1.0f,-1.0f, -1.0f, -1.0f),
// Right Face
Vertex( 1.0f, -1.0f, -1.0f, 0.0f, 1.0f, 1.0f, -1.0f, -1.0f),
Vertex( 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f, -1.0f),
Vertex( 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f),
Vertex( 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f),
};
现在因为我们的正方体是在原点的,所以每个顶点的法线就是它的位置。
现在我们要创建一个缓存来保存我们的cbPerFrame结构体。
ZeroMemory(&cbbd, sizeof(D3D11_BUFFER_DESC));
cbbd.Usage = D3D11_USAGE_DEFAULT;
cbbd.ByteWidth = sizeof(cbPerFrame);
cbbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
cbbd.CPUAccessFlags = 0;
cbbd.MiscFlags = 0;
hr = d3d11Device->CreateBuffer(&cbbd, NULL, &cbPerFrameBuffer);
确保给我们上一节中字体重新设置WVP,不然它会默认使用之前的WVP
WVP = XMMatrixIdentity();
///////////////**************new**************////////////////////
cbPerObj.World = XMMatrixTranspose(WVP);
///////////////**************new**************////////////////////
cbPerObj.WVP = XMMatrixTranspose(WVP);
d3d11DevCon->UpdateSubresource( cbPerObjectBuffer, 0, NULL, &cbPerObj, 0, 0 );
d3d11DevCon->VSSetConstantBuffers( 0, 1, &cbPerObjectBuffer );
d3d11DevCon->PSSetShaderResources( 0, 1, &d2dTexture );
d3d11DevCon->PSSetSamplers( 0, 1, &CubesTexSamplerState );
constbuffPerFrame.light = light;
d3d11DevCon->UpdateSubresource( cbPerFrameBuffer, 0, NULL, &constbuffPerFrame, 0, 0 );
d3d11DevCon->PSSetConstantBuffers(0, 1, &cbPerFrameBuffer);
现在我们给每个物体的VS阶段都传入相应的世界坐标
WVP = cube1World * camView * camProjection;
cbPerObj.World = XMMatrixTranspose(cube1World);
cbPerObj.WVP = XMMatrixTranspose(WVP);
d3d11DevCon->UpdateSubresource( cbPerObjectBuffer, 0, NULL, &cbPerObj, 0, 0 );
d3d11DevCon->VSSetConstantBuffers( 0, 1, &cbPerObjectBuffer );
d3d11DevCon->PSSetShaderResources( 0, 1, &CubesTexture );
d3d11DevCon->PSSetSamplers( 0, 1, &CubesTexSamplerState );
d3d11DevCon->RSSetState(CWcullMode);
d3d11DevCon->DrawIndexed( 36, 0, 0 );
WVP = cube2World * camView * camProjection;
cbPerObj.World = XMMatrixTranspose(cube2World);
cbPerObj.WVP = XMMatrixTranspose(WVP);
d3d11DevCon->UpdateSubresource( cbPerObjectBuffer, 0, NULL, &cbPerObj, 0, 0 );
d3d11DevCon->VSSetConstantBuffers( 0, 1, &cbPerObjectBuffer );
d3d11DevCon->PSSetShaderResources( 0, 1, &CubesTexture );
d3d11DevCon->PSSetSamplers( 0, 1, &CubesTexSamplerState );
d3d11DevCon->RSSetState(CWcullMode);
d3d11DevCon->DrawIndexed( 36, 0, 0 );
使用他们的世界坐标变换对应的法线
struct Light
{
float3 dir;
float4 ambient;
float4 diffuse;
};
cbuffer cbPerObject
{
float4x4 WVP;
float4x4 World;
};
之前我们说过最好把constant buffer 按照调用次数区分
这里我们把灯光constant buffer定义如下
cbuffer cbPerFrame
{
Light light;
};
现在在VS函数中,我们对每个顶点的法线进行计算,然后输出给PS阶段。
然后PS阶段读进来,进行灯光计算。
float4 PS(VS_OUTPUT input) : SV_TARGET
{
input.normal = normalize(input.normal);
float4 diffuse = ObjTexture.Sample( ObjSamplerState, input.TexCoord );
float3 finalColor;
finalColor = diffuse * light.ambient;
finalColor += saturate(dot(light.dir, input.normal) * light.diffuse * diffuse);
return float4(finalColor, diffuse.a);
}
灯光计算过程中我们用到了saturate 函数,用来防止灯光亮度大于1.
与之前一节相比,我们每帧计算量明显增大,我的台式显卡是GTX1060,之前一节的FPS渲染大概在3500左右,这一节只是增加了光照计算就掉到了2500.看来光照虽好,还是需要优化啊。
讲到这里,这节内容终于讲完拉~
今天看了各位拿到腾讯客户端游戏开发的offer大神的博客,确实差距还很大,理解了东西有时候和自己真正的生产代码完全是两码事。看来以后还是要把自己理解了的东西不断的加入到自己的项目中,在实践中学习。
本节内容的代码部分可以在我的github里面找到!
游戏开发路途遥远,但我相信只要坚持,总能到达彼岸!
如果我的文章对于你学习DirectX11有点帮助,欢迎评论给出建议,让我们一起学习进步!
———————— 小明 2018.12.10 18.16