光照演示程序(第八章内容)
8.14、光照演示程序
本章演示程序基于上一章的“陆地与波浪演示程序”的基础上构建而成的,其中利用了一个方向光来表示太阳,用户可以使用方向键来控制太阳的方向。
8.14.1、顶点格式
光照的计算需要依赖于表面法线,所以我们会在顶点层级定义法线,方便在光栅化过程中进行插值计算,由此展开逐像素光照。同时我们也不需要指定顶点的颜色,而是以每一个像素应用光照方程之后所生成的像素颜色代替指定顶点颜色。下面是顶点结构体:
//c++顶点结构体 struct Vertex { DirectX::XMFLOAT3 Pos; DirectX::XMFLOAT3 Noraml; } //对应的HLSL顶点结构体 struct VertexIn { float3 PosL : POSITION; float3 NormalL : NORMAL; } //新的输入布局描述 mInputLayout = { { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 } };
8.14.2、计算法线
GeometryGenerator类中可以用于生成各种几何形状的函数,已经可以通过顶点法线去创建对应的图形数据,不过,为了使地形(terrain)拥有更加真实的表面,我们将会为地形生成法向量。
因为地形曲面的函数
\[
y = (x, z);
\]
已经给出来了,所以对于曲面上的任何一个点,我们都可以通过偏导数在+x和+z上建立两个切向量(tangent vector),因为这两个向量都位于曲面的切平面上,所以我们可以通过求这两个向量的叉乘来求取该点的法向量
\[
用于生成陆地网格的函数:f(x, z) = 0.3z·sin(0.1x) + 0.3x·cos(0.1z)
\]
则偏导数为:
\[
对x的偏导数:0.03z·cos(0.1x) + 0.3·cos(0.1z)
\]
\[ 对y的偏导数:0.3·sin(0.1x) - 0.03x·sin(0.1z) \]
此处跳过求两个切向量的叉乘步骤。
则位于曲面上一点(x, f(x, z), z)的曲面法线为:
\[
n(x, z) = (-w,1,-s);
\]
w为对x的偏导数,s为对y的偏导数。可以很清楚的看出,上述法线并不具有单位长度,所以在进行光照计算之前,我们需要对上述法线进行规格化处理。
我们要对地形的每一个顶点进行上述的法线计算,以获取他们对应的顶点法线:
XMFLOAT3 LitWavesApp::GetHillsNormal(float x, float z)const { XMFLOAT3 n( -0.03f*z*cosf(0.1f*x) - 0.3f*cosf(0.1f*z), 1.0f, -0.3f*sinf(0.1f*x) + 0.03f*x*sinf(0.1f*z)); XMVECTOR unitNormal = XMVector3Normalize(XMLoadFloat3(&n)); XMStoreFloat3(&n, unitNormal); return n; }
8.14.3、更新光照的方向
在8.13.7节中,我们将Light数组妨碍渲染过程常量缓冲区中。在演示程序中,我们使用一个方向光来表示太阳,并允许用户使用方向键来控制光源的方位。这也就是说,我们需要在每一帧更新阳光照射的方向,并且将结果设置到渲染过程常量中。
我们这里使用球坐标来追踪太阳的位置,但是由于太阳的距离使无限远的,所以对于径向距离的取值是无关紧要的。在实例程序中,我们将径向距离设置为1,使太阳始终在单位球体这一轨道上运动。下列代码用于更新方向光源方位:
float mSunTheta = 1.25f*XM_PI; float mSunPhi = XM_PIDIV4; void LitWavesApp::OnKeyboardInput(const GameTimer& gt) { const float dt = gt.DeltaTime(); if(GetAsyncKeyState(VK_LEFT) & 0x8000) mSunTheta -= 1.0f*dt; if(GetAsyncKeyState(VK_RIGHT) & 0x8000) mSunTheta += 1.0f*dt; if(GetAsyncKeyState(VK_UP) & 0x8000) mSunPhi -= 1.0f*dt; if(GetAsyncKeyState(VK_DOWN) & 0x8000) mSunPhi += 1.0f*dt; mSunPhi = MathHelper::Clamp(mSunPhi, 0.1f, XM_PIDIV2); } void LitWavesApp::UpdateMainPassCB(const GameTimer& gt) { …… XMVECTOR lightDir = -MathHelper::SphericalToCartesian(1.0f, mSunTheta, mSunPhi); XMStoreFloat3(&mMainPassCB.Lights[0].Direction, lightDir); mMainPassCB.Lights[0].Strength = { 1.0f, 1.0f, 0.9f }; auto currPassCB = mCurrFrameResource->PassCB.get(); currPassCB->CopyData(0, mMainPassCB); }
8.14.4、更新根签名
为了实现光照,我们为着色器引入了一个材质常量缓冲区,为了支持这个新引入的常量缓冲区,我们需要改写之前的根签名。
void LitWavesApp::BuildRootSignature() { // 创建根参数(根参数可以是描述符表,根描述符,根常量) CD3DX12_ROOT_PARAMETER slotRootParameter[3]; // 创建根CBV slotRootParameter[0].InitAsConstantBufferView(0); slotRootParameter[1].InitAsConstantBufferView(1); slotRootParameter[2].InitAsConstantBufferView(2); // 根签名是一系列根参数组成的 CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(3, slotRootParameter, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT); …… }
8.14.5、程序运行效果图
8.15、小节
- 运用光照之后,我们就可以不必再指定每一个顶点的颜色,取而代之的是要定义场景光源和每一个顶点的材质。我们可以将材质看作是确定光如何和物体表面进行交互的一种属性。通过对三角形表面的每个顶点处的材质进行线性插值计算,便可以获得三角形网格中每一个表面点处的顶点数据。
- 曲面法线是一种正交于曲面上某一点处切平面的单位向量,利用曲面法线可以确定曲面上某一点的“朝向”。为了获取三角形网格表面每一个点的曲面法线,我们需要指定顶点的曲面法线,然后在光栅化过程中对三角形中的这些顶点法线进行线性插值。我们一般使用一种叫做求法线平均值的计算方法来估算顶点法线。如果矩阵A可用于变换点和向量,那么A的逆转置矩阵可以用来变换经非等比变换或剪切变换之后的法线。
- 平行光源(方向光源)模拟了一种距离被照物体极远的光源,比如太阳,点光源会向四周各个方向发射光,比如电灯泡,聚光灯光源的发光范围是圆锥体,比如手电筒。
- 根据菲涅尔效应可知,当光线到达两种不同折射率介质之间的界面时,一部分光会被反射,另一部分光会折射进入介质里面,反射的光量依赖于介质和表面法向量于光向量之间的夹角。由于计算的复杂想,菲涅尔方程一般不会应用于实时渲染,而是采用石里克近似替代菲涅尔方程。
- 现实世界中的反射物体一般都不是理想镜面,都是具有一定粗糙度的。我们可以把理想镜面的粗糙度视为0,并且它的宏观表面法线和微观表面法线都指向相同的地方,随着粗糙度的增加,微观表面法线逐渐偏离宏观表面法线,并导致反射光逐渐扩展成一个镜面瓣(粗糙度令镜面反射光扩散开来,镜面反射光的范围称为镜面瓣)
- 环境光模拟了在场景中场景中进行多次散射和反弹,然后按各个方向均等的射向物体的间接光。漫反射光模拟的时进入介质内部的光,其中一部分会被吸收,剩下的部分则会散射会表面。镜面光模拟的是根据菲涅尔效应和表面粗糙度而从表面反射的光。