美文网首页
【DirectX12笔记】第八章 光照

【DirectX12笔记】第八章 光照

作者: crossous | 来源:发表于2019-07-26 21:01 被阅读0次
最终效果图

前言

  上一章,我们深入了几何体的渲染,这一章,我们将学习光照的计算。
  看完一遍后,心情复杂,一开始是打算看看事例程序后直接跳过的,毕竟学过opengl,应用过phong光照,对经验光学还算熟悉,不成想看看了后,发现书中用另一种方法解释了之前的公式,完善了之前比较模糊的概念。
  因此本章节中,先不上程序,而是比较一下和曾经的区别。

对比

  曾经我学习的phong光照中,将光照模型分为三部分:环境光、漫反射、镜面光,而新学的BlinnPhong同样是这三种。
  先上符号表(于299页):L:从受光点指向光源的向量\\ n:受光点的表面法向量\\ h:光向量和观察向量之间的中间向量(半向量)\\ A_L:环境光量\\ B_L:直射光量\\ m_d:表面漫反射率(可以当做物体本身颜色)\\ L·n:朗伯余弦定理\\ \alpha_h:中间向量h与光向量L之间的夹角\\ R_F(\alpha_h):根据菲涅尔效应,入射光反射的比例\\ m:表面粗糙度
  对于环境光,我之前理解为:为了防止模型的背面看起来过黑,给模型加一个底色;而新的理论中表示:一个物体处于黑暗的房间中,一道光打过来,背面并非是完全黑的,因为房间墙壁存在漫反射,环境光就是为了模拟漫反射而存在的。它的公式如下:C_a=A_L\otimes m_d  一般环境光量都很小。
  对于漫反射,公式为:C_d=max(L·n,0)·B_L\otimes m_d  这个公式同样和之前我学会的没有变化,但理解方式不同,对于后半段,直射光亮与物体颜色的分量积理解相同,但之前的L·n不同,光向量与法向量的乘积,由于两者都是单位向量,因此求出来的实际是夹角的cos值。
  曾经的理解为:根据经验,当光线直射表面时,物体会更亮,而如果平行于表面,则几乎不会受光,因此用光向量和法向量的夹角cos值来模拟这种现象。
  而书中给了更科学的解释方法,在290页,一个光源每秒发出的光能量成为辐射通量,单位面积上的辐射通量密度称为辐照度,假设有一小束辐射通量P、横截面积为A1,则垂直打在表面上的辐照度为\frac{P}{A_1},如果斜着打到平面上,受光面积会变大为A2,此时的辐照度\frac{P}{A_2},根据三角学可知,A1和A2的关系为cos\theta=\frac{A_1}{A_2},所以E_2=\frac{p}{A_2}=\frac{p}{A_1}cos\theta=E_1cos\theta=E_1(n·L)  这就是朗伯余弦定理。其中E1可以看做光垂直打在物体上的颜色,符合我们上面Cd公式,相比于之前理解的经验光学,从能量角度考虑更为严谨。
  镜面光照的变化是最大的,曾经我只是计算反射光与观察向量的夹角的cos值的shininess次幂来计算镜面光,同样也是经验学的理解方式,出射光越接近我们的眼睛,我们能看到的镜面光越强。
  镜面光的产生是一种名为菲涅尔效应的物理现象,能根据反射角度判断光反射的比率,这在之前学光追的时候看过。
  菲涅尔方程十分复杂,我们用的常是石里克近似R_F(\theta_i)=R_F(0°)+(1-R_F(0°))(1-cos\theta_i)^5其中RF(0°)是介质的一种属性,294页给出常见介质的数值。
  一部分原因出自对性能的考虑,BlinnPhong不再计算反射光向量,而是采用另一种方式:半(中间)向量。

曾经的计算方法,计算cosθv   这次我们引用了微平面理论解释粗糙度的概念(296页):为什么物体会粗糙?因为宏观上物体可能十分光滑,但围观上还是参差不齐的,这些微观细小看不见的表面,我们用无限小的平面集合去近似,可以说,当这些微观平面的法向量,和宏观平面法向量越靠近,相同的越多,表面看起来就越光滑,反之粗糙: 宏观平面(左),微观平面越来越粗糙   而决定到我们眼睛中的高光有多闪耀,是这些微表面法向量朝向我们的比例,换而言之,就是h=normalize(L+v)在这一片段中的比例:   h称为中间向量,我们定义一个分布函数,来模拟这一状况,初步定义为:  我理解为:当法向量与半向量越靠近时,我们能看到的越亮。m控制粗糙度,如下图: ,可以类比到我们生活中,越粗糙的物体,高光越暗,但光泽变大,上式中,m越小代表越粗糙,可以发现衰减速度越慢,代表着光泽程度在最高点向外衰减的速度越慢。
  但这样有一个缺点,无论粗糙与否,最亮的亮度都是一样的,因此用归一化隐私组合成新的函数:这样就符合现实中的规律了。
  于是光照模型的三块拼图都得到了,组合起来,就是:
  能看出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页练习题还有一个简单的卡通风格渲染练习,感觉不错。

相关文章

网友评论

      本文标题:【DirectX12笔记】第八章 光照

      本文链接:https://www.haomeiwen.com/subject/pxzdrctx.html