前言
上一章,我们深入了几何体的渲染,这一章,我们将学习光照的计算。
看完一遍后,心情复杂,一开始是打算看看事例程序后直接跳过的,毕竟学过opengl,应用过phong光照,对经验光学还算熟悉,不成想看看了后,发现书中用另一种方法解释了之前的公式,完善了之前比较模糊的概念。
因此本章节中,先不上程序,而是比较一下和曾经的区别。
对比
曾经我学习的phong光照中,将光照模型分为三部分:环境光、漫反射、镜面光,而新学的BlinnPhong同样是这三种。
先上符号表(于299页):
对于环境光,我之前理解为:为了防止模型的背面看起来过黑,给模型加一个底色;而新的理论中表示:一个物体处于黑暗的房间中,一道光打过来,背面并非是完全黑的,因为房间墙壁存在漫反射,环境光就是为了模拟漫反射而存在的。它的公式如下: 一般环境光量都很小。
对于漫反射,公式为: 这个公式同样和之前我学会的没有变化,但理解方式不同,对于后半段,直射光亮与物体颜色的分量积理解相同,但之前的L·n不同,光向量与法向量的乘积,由于两者都是单位向量,因此求出来的实际是夹角的cos值。
曾经的理解为:根据经验,当光线直射表面时,物体会更亮,而如果平行于表面,则几乎不会受光,因此用光向量和法向量的夹角cos值来模拟这种现象。
而书中给了更科学的解释方法,在290页,一个光源每秒发出的光能量成为辐射通量,单位面积上的辐射通量密度称为辐照度,假设有一小束辐射通量P、横截面积为A1,则垂直打在表面上的辐照度为,如果斜着打到平面上,受光面积会变大为A2,此时的辐照度,根据三角学可知,A1和A2的关系为,所以 这就是朗伯余弦定理。其中E1可以看做光垂直打在物体上的颜色,符合我们上面Cd公式,相比于之前理解的经验光学,从能量角度考虑更为严谨。
镜面光照的变化是最大的,曾经我只是计算反射光与观察向量的夹角的cos值的shininess次幂来计算镜面光,同样也是经验学的理解方式,出射光越接近我们的眼睛,我们能看到的镜面光越强。
镜面光的产生是一种名为菲涅尔效应的物理现象,能根据反射角度判断光反射的比率,这在之前学光追的时候看过。
菲涅尔方程十分复杂,我们用的常是石里克近似:其中RF(0°)是介质的一种属性,294页给出常见介质的数值。
一部分原因出自对性能的考虑,BlinnPhong不再计算反射光向量,而是采用另一种方式:半(中间)向量。
但这样有一个缺点,无论粗糙与否,最亮的亮度都是一样的,因此用归一化隐私组合成新的函数:这样就符合现实中的规律了。
于是光照模型的三块拼图都得到了,组合起来,就是:
能看出blinnphong属于phong的晋升版本,它不需要计算反射,而算出来的夹角和phong计算的夹角有关系,根据上面的图推导:
程序
事到如今,程序的解读不需要像一开始那样从头开始了,我们关注点变为发生重要变化的地方。
首先就是材质(Material),在我的理解中,一个材质有几个属性:1.菲涅尔的R0值,决定反射折射比例;2.光泽程度;3.能采样出颜色,无论是固定颜色(漫反射吸收率),还是贴图,亦或者是给出采样点返回一个颜色,都可以;
而作为一个封装的DirectX类,我们将其看做和渲染项差不多的结构,因此我们还需要它在堆中的位置、更新标志等等,所以封装的Material类如下(300页):
struct Material
{
std::string Name;
int MatCBIndex = -1;//常量缓冲区中的索引
int DiffuseSrvHeapIndex = -1;//材质在SRV堆中的索引
int NormalSrvHeapIndex = -1;
int NumFramesDirty = gNumFrameResources;//更新标志
DirectX::XMFLOAT4 DiffuseAlbedo = { 1.0f, 1.0f, 1.0f, 1.0f };//漫反射吸收率(物体颜色)
DirectX::XMFLOAT3 FresnelR0 = { 0.01f, 0.01f, 0.01f };//菲涅尔因子
float Roughness = .25f;//粗糙度
DirectX::XMFLOAT4X4 MatTransform = MathHelper::Identity4x4();
};
这是我们用来抽象的,因为这些参数中的一部分需要传入常量缓冲,我们需要一个材质常量结构:
struct MaterialConstants
{
DirectX::XMFLOAT4 DiffuseAlbedo = { 1.0f, 1.0f, 1.0f, 1.0f };
DirectX::XMFLOAT3 FresnelR0 = { 0.01f, 0.01f, 0.01f };
float Roughness = 0.25f;
DirectX::XMFLOAT4X4 MatTransform = MathHelper::Identity4x4();
};
材质被新的渲染项结构所引用,同时帧资源也有材质常量的常量缓冲区,并且和帧资源的常量缓冲区一样都需要每帧更新:
void LitWavesApp::UpdateMaterialCBs(const GameTimer& gt)
{
auto currMaterialCB = mCurrFrameResource->MaterialCB.get();
//std::unordered_map<std::string, std::unique_ptr<Material>> mMaterials;
for(auto& e : mMaterials)
{
Material* mat = e.second.get();
if(mat->NumFramesDirty > 0)
{
XMMATRIX matTransform = XMLoadFloat4x4(&mat->MatTransform);
MaterialConstants matConstants;
matConstants.DiffuseAlbedo = mat->DiffuseAlbedo;
matConstants.FresnelR0 = mat->FresnelR0;
matConstants.Roughness = mat->Roughness;
currMaterialCB->CopyData(mat->MatCBIndex, matConstants);
mat->NumFramesDirty--;
}
}
}
创建材质很简单,把材质参数填好即可:
void LitWavesApp::BuildMaterials()
{
// 草材质,用于山体
auto grass = std::make_unique<Material>();
grass->Name = "grass";
grass->MatCBIndex = 0;
grass->DiffuseAlbedo = XMFLOAT4(0.2f, 0.6f, 0.2f, 1.0f);
grass->FresnelR0 = XMFLOAT3(0.01f, 0.01f, 0.01f);
grass->Roughness = 0.125f;
// 水材质,用于水
auto water = std::make_unique<Material>();
water->Name = "water";
water->MatCBIndex = 1;
water->DiffuseAlbedo = XMFLOAT4(0.0f, 0.2f, 0.6f, 1.0f);
water->FresnelR0 = XMFLOAT3(0.1f, 0.1f, 0.1f);
water->Roughness = 0.0f;
mMaterials["grass"] = std::move(grass);
mMaterials["water"] = std::move(water);
}
渲染项的创建中,多了两行:
//void LitWavesApp::BuildRenderItems()
wavesRitem->Mat = mMaterials["water"].get();//水面用水材质
gridRitem->Mat = mMaterials["grass"].get();//山体用草材质
帧资源多了一个常量缓冲区:
MaterialCB = std::make_unique<UploadBuffer<MaterialConstants>>(device, materialCount, true);
对了,现在常量缓冲区有三个,所以也别忘了根签名:
//void LitWavesApp::BuildRootSignature()
CD3DX12_ROOT_PARAMETER slotRootParameter[3];
slotRootParameter[0].InitAsConstantBufferView(0);
slotRootParameter[1].InitAsConstantBufferView(1);
slotRootParameter[2].InitAsConstantBufferView(2);
如果根签名用的是描述符表那就有意思了,有多少个帧资源,就要创建n*材质树的描述符堆(和渲染项一样),不过当前实例用的是根描述符,轻松了一些。
但渲染时找地址还是要的:
void LitWavesApp::DrawRenderItems(ID3D12GraphicsCommandList* cmdList, const std::vector<RenderItem*>& ritems)
{
UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
UINT matCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(MaterialConstants));//材质常量缓冲区大小
auto objectCB = mCurrFrameResource->ObjectCB->Resource();
auto matCB = mCurrFrameResource->MaterialCB->Resource();//材质常量缓冲区
for(size_t i = 0; i < ritems.size(); ++i)
{
auto ri = ritems[i];
cmdList->IASetVertexBuffers(0, 1, &ri->Geo->VertexBufferView());
cmdList->IASetIndexBuffer(&ri->Geo->IndexBufferView());
cmdList->IASetPrimitiveTopology(ri->PrimitiveType);
D3D12_GPU_VIRTUAL_ADDRESS objCBAddress = objectCB->GetGPUVirtualAddress() + ri->ObjCBIndex*objCBByteSize;
D3D12_GPU_VIRTUAL_ADDRESS matCBAddress = matCB->GetGPUVirtualAddress() + ri->Mat->MatCBIndex*matCBByteSize;//找到渲染项所需材质的地址
cmdList->SetGraphicsRootConstantBufferView(0, objCBAddress);
cmdList->SetGraphicsRootConstantBufferView(1, matCBAddress);//填进去
//Draw Call
cmdList->DrawIndexedInstanced(ri->IndexCount, 1, ri->StartIndexLocation, ri->BaseVertexLocation, 0);
}
}
大致OK,看看Shader。这次有两个Shader,不过看BuildPSOs方法,只需要编译Default.hlsl即可,LightingUtil.hlsl则是类似库一样的功能。
实现理论参照前面的光学,具体实现参照308页,这里就不讲了。
除此之外还有一个键盘事件,这个程序只加了一个平行光,太阳在球面坐标上移动,用键盘上下左右可控制阳光在球面上移动,程序会自动转化为笛卡尔坐标,实现可动的光照效果。
效果参照封面,对了322页练习题还有一个简单的卡通风格渲染练习,感觉不错。
网友评论