【DirectX11】【学习笔记(13)】简单光照

光照在游戏中的渲染是非常重要的。但是如何保证我们实时渲染光照的同时,还能把游戏帧率保持在30以上就很值得人思考了。

Types of Light

Ambient

环境光,主要是用来保证环境中没有直接被光源照射的部分没有直接变黑。

我们用一个分量乘法来实现环境光

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

这个其实不算是一种光。它不会发出光照亮其他物体,它只是会让物体自身发光。

举个例子就是灯泡,灯泡自身是很亮的,但这个和它真正发出的光没有半毛钱关系。

Types of Light Sources

这里有三种不同的光源:方向光,点光源,聚光灯

Directional Lights (A.K.A. Parallel Lights)

也叫平行光,它没有具体的光源位置,最好的例子就是太阳。

Point Lights

点光源有具体的位置,和一个衰减的范围。,但没有方向而言。

Spotlights

聚光灯是计算最复杂的光源。他们有方向,位置,颜色,和两个角度,以及衰减范围。

锥形中心的位置显然应该比锥形边缘的位置要亮。而从内角到外角开始,就会有一个光照衰减。

Normals

法线用来定义一个面的朝向,我们这里用来确定一个面是否在光的照射范围内。

世界坐标的变换,让法线向量可能大于1,也可能小于0,所以我们这里用一个HLSL中的操作,使他们归一化,

Vertex Normals

当定义顶点的时候,我们还需要定义顶点的法线。然后在PS阶段,我们需要确定每个像素接收到的光照。

如果我们创建一个球体的时候,我们就需要用到一个技术叫做法线平均,这样会让球体看起来光滑。否则的话,球体看起来就是几个平面拼在一起的。

Face Normals

我们在定义顶点的时候一般不定义面法线。但是面法线是由顶点法线取平均得到的。

Normal Averaging

法线平均技术是让灯光减少块状效果的,通过平均每个相邻平面的法线,这样,光照在相邻表面移动式,会逐渐变化,而不是突然变化。

Global Declarations

我们这里需要申请一个缓存,来保存一个每帧都需要改变的对象。

ID3D11Buffer* cbPerFrameBuffer;

Updated Constant Buffer

现在我们还需要发送物体的世界坐标矩阵。因为我们希望光照是在世界坐标的情况下在effect file中计算的。

struct cbPerObject
{
    XMMATRIX  WVP;
    XMMATRIX  World;
};

The Light Structure

我们现在需要创建一个光照结构体。来传入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接收一个时间参数。

Updated Vertex Structure & Vertex Layout

因为我们要更新灯光,所以要新传入顶点的法线结构。

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}
};

Clean Up

别忘了清除指针。。

Define the Light

现在我们定义灯光的各个变量。

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);

Adding Normal Data to the Cube

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);

D2D's FPS Square's World Space Matrix

确保给我们上一节中字体重新设置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 );

Setting the Light

constbuffPerFrame.light = light;
d3d11DevCon->UpdateSubresource( cbPerFrameBuffer, 0, NULL, &constbuffPerFrame, 0, 0 );
d3d11DevCon->PSSetConstantBuffers(0, 1, &cbPerFrameBuffer);    

Setting the World Matrix

现在我们给每个物体的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 );

使用他们的世界坐标变换对应的法线

Effect File

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

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值