dx12 龙书第八章学习笔记 -- 光照

24 篇文章 10 订阅

1.光照与材质的交互

局部光照模型(local illumination model):每个物体的光照皆独立于其他物体,我们也就可以在处理光照的过程中仅考虑光源直接发出的光,而忽略来自场景中其他物体所反弹来的

全局光照模型(global illumination model):除了要考虑由光源直接发出的光,还要顾及场景中其他物体所反弹来的间接光照。-- 全局光照模型的开销往往是实时游戏所负担不起的

2.法向量

平面法线(face normal)/曲面法线(surface normal)

对于光照计算来说,我们需要通过三角形网格曲面上每一点处的曲面法线来确定光线照到对应点上的角度。为了求出曲面法线,我们仅先指定位于网格顶点处的曲面法线(所以也称作顶点法线),接下来,为取得三角形网格曲面上每个点处的近似曲面法线,在三角形进行光栅化的过程中对这些法线进行插值计算

我们把对每个像素逐一进行法线插值并执行光照计算的方法称为逐像素光照/phong光照模型

逐顶点光照模型对每个顶点进行光照计算,并对三角形中的像素进行插值,这种开销低但是精度也低 -- 将运算作业从像素着色器转移至顶点着色器是一种常见的优化手段,在以质量为重但又允许结果存在少许偏差的情况下,这种优化方法极具吸引力

计算法向量:

\\ \Delta p_0p_1p_2 \\ u = p_1 - p_0 \ v = p_2-p_0 \\ n = \frac{u\times v}{||u\times v||}

XMVECTOR ComputeNormal(FXMVECTOR p0, FXMVECTOR p1, FXMVECTOR p2)
{
    XMVECTOR u = p1 - p0;
    XMVECTOR v = p2 - p0;

    return XMVector3Normalize(XMVector3Cross(u, v));    
}

对于可微的光滑曲面而言,我们可以利用微积分方面的知识来求出曲面点处的法线。但问题在于,三角形网格运用一种被称为求顶点法线平均值的计算方法。

 我们可以遍历所有三角形,并累加所涉及顶点的法线,最后归一化处理(简单求平均值)

切向量(tangent vector):正交于法向量

考虑这样的问题,之前,切向量u正交于法向量n,但对此应用一个非等比缩放变换A后,uA不再与nA继续保持正交性:

 u\cdot n = 0  => uA\cdot nB = 1 \therefore B = (A^{-1})^T

证明过程:

\therefore B = (A^{-1})^T 如果A是正交矩阵,那么B=A

当我们需要对经过非等比变换或剪切变换后的法向量进行变换时,则可使用逆转置矩阵

// MathHelper.h:求逆转置矩阵
static XMMATRIX InverseTranspose(CMMATRIX M)
{
    XMMATRIX A = M;
    A.r[3] = XMVectorSet(0.f, 0.f, 0.f, 1.f);
    
    XMVECTOR det = XMMatrixDeterminant(A);
    return XMMatrixTranspose(XMMatrixInverse(&det, A));
}

在这里我们将平移项清零,而只允许点类才有平移变换。若我们只把w设置为0,看似可以防止向量因平移操作而受影响。但问题在于,

 3.参与光照计算的一些关键向量

光向量(light vector) L:单位向量,方向与在点P处光线的入射方向相反

反射向量r:入射光向量L关于表面法线n的镜像

观察向量(view vector) vv=normalize(E-P) E点是人眼(观察点) P是表面点

  反射向量公式的推导证明:

 \\ r = -L + 2S 

因为S是L在n方向的投影,根据投影公式cos\theta=\frac{L\cdot n}{||L||\cdot ||n||},得到:

S=L\cdot cos\theta =L\cdot \frac{L\cdot n}{||L||\cdot ||n||}=L\cdot (L\cdot n)

所以r = -L+ 2L(L\cdot n)  

-- 照片中没有使用光向量而是用入射向量I

4.朗伯余弦定理

我们可以将光看作是光子的集合,在空间中按特定的方向传播。每个光子都载有光能量。光源每秒发出的光能量称为辐射通量。而单位面积上的辐射通量密度(辐(射)照度)是一种很重要的概念。

 (a)辐照度:E_1=P/A_1 (b)辐照度:

E_2 = P/A_2 = P\cdot cos\theta/A_1 = E_1\cdot cos\theta = E_1(n\cdot L)

换句话说,面积A2内的辐照度就相当于将受垂直方向光照的面积A1内的辐照度按比例n\cdot L = cos\theta进行缩放。注意,这里的θ角度是光向量与法线的夹角,越垂直于平面,辐照度越强。 --  这就是朗伯余弦定理(Lambert's Cosine Law)

考虑到光线照射到表面另一侧的情况(点积为负),我们使用max函数来钳制缩放因子的取值范围:

max(L\cdot n, 0)

5.漫反射光照 -- diffuse

当光线照射到物体表面上的某一点时,这些光一部分会进入物体内部,并与表面附近的物质相互作用。这些光会在物体内部四处反弹,其中一部分会被吸收,而余下部分则会向各个方向散射回表面 -- 这就是所谓的漫反射

在所用的这种光照与材质交互的近似模型之中,我们规定光线会在表面的所有方向上均匀散射 -- 因此,无需考虑观察点的具体位置,因为表面上的颜色在任何观察点上看来都是相同的

我们将漫反射的计算分为两个部分在第一个部分中,我们要指定光照颜色以及漫反射反照率(diffuse albedo)颜色。漫反射反照率表示的是根据表面的漫反射率而被反射的入射光亮(根据能量守恒定律,入射光不是被反射回表面就是被材质所吸收了)。要对它们进行处理就需用分量式颜色乘法。

例如,假设表面上一点会反射50%的入射红光、100%的绿光以及75%的蓝光,如果这时有一束强度为80%的白光袭来,那么此入射光的量值就可以表示为B_L=(0.8,0.8,0.8),又因为漫反射反照率为m_d=(0.5,1.0,0.75),所以此点的反射光亮为:C_d=B_L\otimes m_d=(0.8,0.8,0.8)\otimes (0.5,1.0,0.75)=(0.4,0.8,0.6)

在第二个部分中,我们需要将朗伯余弦定理考虑在内。

L:光向量 n:表面法线 B_L:入射光亮 m_d:漫反射反照率

c_d=max(L\cdot n, 0)\cdot B_L \otimes m_d

6.环境光照 -- ambient

其实在现实生活中我们看到的大多是间接光,但我们的光照模型中并没有考虑从其他物体反射来的间接光照。为了处理这种间接光照,我们给光照方程引进一个环境光项(ambient light)

c_a=A_L\otimes m_d

所有的环境光都是以统一的亮度将物体稍稍照亮 -- 而没有按真实的物理效果进行计算

7.镜面光照

菲涅耳效应 Fresnel effect当光线到达两种不同折射率介质之间的界面时,一部分光会被反射,而剩下的光则发生折射

折射率:介质的一种物理性质,即光在真空中传播的速度与光在给定介质内的传播速度之比

我们之前用漫反射光照来模拟漫反射的过程,而第二种光的反射过程称为镜面反射(specular reflection),把被反射的光称为镜面(反射)光(specular light) -- 镜面光只发生在特定的角度

摄入观察者眼中的反射光实则为镜面反射和漫反射的组合效果

如果折射光沿折射向量从介质的另一侧射出,并进入观察者的眼中,则该物体看起来就像是透明的。在实时的图像处理中,一般用alpha混合技术或后期处理特效来模拟透明对象的折射过程。

1️⃣菲涅耳效应:

菲涅耳方程以数学方法描述了入射光线被反射的百分比,即0\leq R_F \leq 1

根据能量守恒定律,如果R_F是反射光量,则(1-R_F)为折射光量。R_F的值是一个RGB向量,因为光的颜色反映了反射光亮。

反射的光量既依赖于介质(某些材质反射率相对更大),也与法向量n与光向量L之间的夹角\theta _i有关。由于光照过程的复杂性,我们一般不会将完整的菲涅耳方程用于实时渲染,而是采用石里克近似(Schlick approximation)法来加以代替

R_F(\theta_i)=R_F(0^o)+(1-R_F(0^o))(1-cos\theta_i)^5

 可以看出,反射光量随着θi从0°到90°递增

我们可以将菲涅耳效应简洁地概括为:反射光亮取决于材质(R_F(0^o))以及法线与光向量之间的夹角

金属会吸收透射光,这意味着它们不具有本体反射率(体漫反射)。虽然如此,但金属并不会看上去表现为纯黑色,因为它们的R_F(0^o)较高,也就是说,就算是在接近于0°这样的极小入射角度上,它们也能反射可观的镜面光亮。

2️⃣表面粗糙度:

真实世界中的反射物体往往不是理想镜面,我们认为理想镜面粗糙度为0,它的微观表面法线都与宏观表面法线的方向相同。随着粗糙度的增加,微观表面法线的方向开始纷纷偏离宏观表面法线,由此反射光逐渐扩展为一个镜面瓣

为了用数学方法对粗糙度进行建模,我们采用了微平面模型。在此模型中,我们将微观表面模拟为由多个既微小又平滑的微平面所构成的集合,而微观表面法线正式这些微平面上的法线。

我们需要了解由L向v反射的所有微平面片段的分布情况,换言之,即法线h=normalize(L+v)这种微平面片段在所有微平面中所占的比例 -- 发生由L到v反射过程的微平面越多,则观察者在此角度上看到的镜面光越明亮

半程向量(halfway vector(中间向量)):由于向量h位列于向量L向量v[向量v是人眼位置与观察点p之间的向量,不是L关于法线对称的向量]间的中间位置

h = normalize(L+E)

还要引入夹角\theta_h:半程向量h与宏观表面法线n之间的夹角

上图中,n是宏观表面法线,h是半程向量,我们找出以半程向量h为法线的微平面

我们定义归一化分布函数{\color{Red} \rho (\theta_h)\in[0,1]},用来表示微观表面法线h与宏观表面法线n之间夹角为\theta_h的微表面的分布情况。从直观上讲,我们希望函数\rho (\theta_h)\theta_h=0°时取得最大值。也就是说,我们希望微平面法线都平行于宏观表面法线,并盼望随着θh的增加,这些以向量h为法线的微平面片段逐渐减少。用来模拟以上讨论中期望模型ρ(θh)的一种较流行的可控函数为:

 \\ \rho (\theta_h)=cos^m(\theta_h)=(n\cdot h)^m

根据上图可以看到,m值越小,表面越粗糙

我们可以讲ρ(θh)与某一种归一化因子进行组合,从而得到基于粗糙度来模拟镜像反射光亮的新函数:

{\color{Violet} S(\theta_h)=\frac{m+8}{8}cos^m(\theta_h)=\frac{m+8}{8}(n\cdot h)^m }

加上该归一化因子以使光能守恒。该归一化因子能够支配图中曲线的高度,因此随着变量m的变化使镜面瓣变宽或变窄,令光能达到守恒。比如对于较小的m值来说,表面粗糙,镜面瓣变宽,因此能量被广泛传播出去了,镜面高光因此会变暗,对应的图中曲线高度会下降。

⭐总结:菲涅耳反射+表面粗糙度:<高光反射>

{\color{Brown} c_s=max(L\cdot n, 0)\cdot B_L\otimes R_F(\alpha_h)\frac{m+8}{m}(n\cdot h)^m }

注意:这里R_F(\alpha_h)中的角度是微平面法线(半程向量)h与v或者L之间的夹角 -- 之前是宏观表面法线n

8.光照模型 -- Blinn-Phong 

表面反射的光亮相当于环境反射光(c_a)、漫反射光(c_d)以及镜面反射光(c_s)的光亮总和

环境光:模拟经表面反射的间接光亮

漫反射光:对进入介质内部,又经过表面下吸收而最终散射出表面的光进行模拟。由于对表面下的散射光建模比较困难,我们便假设在表面下与介质相互作用后的光从进入表面处返回,并向各个方向均匀散射。

镜面光:模拟菲涅耳效应与表面粗糙度共同作用的表面反射光

⭐所以在shader中实现的光照方程为:

{\color[RGB]{244,96,173} \\LitColor=c_a+c_d+c_s\\ = A_L\otimes m_d+max(L\cdot n,0)\cdot B_L\otimes (m_d +R_F(\alpha_h)\frac{m+8}{8}(n\cdot h)^m)}

其中:R_F(\alpha_h)=R_F(0^o)+(1-R_F(0^o))(1-cos\alpha_h)^5

最后一项中(n·h)不能为负,因为其代表微表面的分布情况(或所占"比例")

m_d:漫反射反照率 diffuse albedo

R_F(0^o):FresnelR0

9.材质的实现

我们编写的材质结构体定义在d3dUtil.h文件中:

struct Material
{
    std::string Name;

    int MatCBIndex = -1; // 本材质的常量缓冲区索引

    int DiffuseSrvHeapIndex = -1; // 漫反射纹理在SRV堆中的索引
    
    int NumFramesDirty = gNumFrameResources;
    
    // 用于着色的材质常量缓冲区数据
    DirectX::XMFLOAT4 DiffuseAlbedo = {1.f, 1.f, 1.f, 1.f};
    DirectX::XMFLOAT3 FresnelR0 = {0.01f, 0.01f, 0.01f};
    float Roughness = 0.25f;
    DirectX::XMFLOAT4X4 MatTransform = MathHelper::Identity4x4();
};

为了模拟真实世界的材质,需要设置DiffuseAlbedo(漫反射反照率)FresnelR0(材质属性R_F(0°))对于金属而言,金属材质不会发生漫反射(DiffuseAlbedo=0),也就只有高光项。但我们往往不会按照100%物理学上的光学理论来建模,而作适当的调整。一种富有艺术性的更佳策略是为金属的DiffuseAlbedo设定一个非0的较小值,为艺术性留下一些空间,达到更佳的视觉效果。

Roughness粗糙度指定在归一化范围[0,1]内,粗糙度值越大越粗糙,我们在着色器代码中,利用粗糙度来推导所用的指数m。根据我们对粗糙度的定义,可以看出表面的光泽度(shiness)是与粗糙度相反的属性,shiness = 1 - roughness ∈[0,1]

那么我们该以什么粒度指定材质?解决方案之一是我们对每个顶点为基准来指定材质的具体数值,在光栅化阶段对材质属性进行插值处理。

创建材质,并列于一个表中(放在系统内存中):

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;

    // This is not a good water material definition, but we do not have all the rendering
    // tools we need (transparency, environment reflection), so we fake it for now.
	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);
}

为了让GPU能够访问到材质,放置到常量缓冲区中,常量缓冲区结构以及定义在帧资源中:

// 提前定义在d3dUtil.h中:
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();
};

// 更新我们定义的帧资源结构
struct FrameResource
{
public:
    
    FrameResource(ID3D12Device* device, UINT passCount, UINT objectCount, UINT materialCount, UINT waveVertCount);
    FrameResource(const FrameResource& rhs) = delete;
    FrameResource& operator=(const FrameResource& rhs) = delete;
    ~FrameResource();

    Microsoft::WRL::ComPtr<ID3D12CommandAllocator> CmdListAlloc;

    std::unique_ptr<UploadBuffer<PassConstants>> PassCB = nullptr;
    std::unique_ptr<UploadBuffer<MaterialConstants>> MaterialCB = nullptr; // 材质的常量缓冲区
    std::unique_ptr<UploadBuffer<ObjectConstants>> ObjectCB = nullptr;

    std::unique_ptr<UploadBuffer<Vertex>> WavesVB = nullptr;

    UINT64 Fence = 0;
};

更新材质的常量缓冲区:

void LitWavesApp::UpdateMaterialCBs(const GameTimer& gt)
{
	auto currMaterialCB = mCurrFrameResource->MaterialCB.get();
	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::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); // 设置材质缓冲区CBV

		cmdList->DrawIndexedInstanced(ri->IndexCount, 1, ri->StartIndexLocation, ri->BaseVertexLocation, 0);
	}
}

10.三种光源 -- 平行光源、点光源和聚光灯光源

①平行光源:parallel light 或叫 方向光源 directional light -- 太阳光☀

平行光是一种距离目标物体极远的光源,我们把这种光源发出的所有入射光看作是平行的光线,且因为距离十分遥远,忽略距离所带来的影响,仅指定照射到场景中光线的光强(light intensity)

用向量来定义平行光源,借此指定光线传播的方向

②点光源:point light -- 灯泡💡

点光源能以球面向各个方向发出光线,对于不同的照射点,光向量不同(平行光源:光向量相同)

衰减:在物理学中,光强会根据平方反比定律而随着距离函数发生衰减 I(d)=\frac{I_0}{d^2} ,其中I_0是距离光源d=1处的光强。我们通过此来设置光亮值并辅之以HDR(high dynamic range)光照与色调映射技术,那效果是极好的。

然而,我们在这里却要使用一种更为简单的公式,这便是我们所用的线性衰减(falloff)函数

att(d)=saturate(\frac{falloffEnd - d}{falloffEnd - falloffStart})

saturate函数会将它的参数限定在[0,1]范围内:saturate(x)=\left\{\begin{matrix} x,0\leq x \leq 1 \\ 0,x<0 \\ 1,x>1 \end{matrix}\right.

 还须将衰减因子与光源直射光亮B_L相乘,但衰减不会影响到环境光项。

如果一个点超过了光照的有效范围,那么就可以采取动态分支跳过此处的光照计算并提前返回。

③聚光灯光源:spotlight -- 手电筒🔦

 L与d的夹角φ要小于圆锥体的半顶角\phi_{max}时,P才位于聚光灯的圆锥体范围之内

聚光灯圆锥体范围内所有光线的强度不尽相同。位于圆锥体中心处的光线应该是最强的,随着φ角度的增大,光强逐渐趋近于0。

如何用与φ相关的函数来控制光强的衰减呢?又如何来支配圆锥体的大小呢?答案是:采用控制粗糙度时的可控函数cos^m(\theta_h),将θ换成φ,再用s代替m:

K_{spot}(\phi)=max(cos\phi , 0)^s=max(-L\cdot d, 0)^s

通过s,我们就能间接的控制\phi_{max},s=8时,φ就逼近与45°

所以聚光灯不仅要考虑衰减,还要考虑K_{spot}聚光灯因子,所以开销更高

另外:由于距离的计算涉及平方根运算,这个计算过程十分耗费资源

11.光照的具体实现

Light结构体(d3dUtil.h中):

struct Light
{
    DirectX::XMFLOAT3 Strength = { 0.5f, 0.5f, 0.5f }; // 光强或光颜色
    float FalloffStart = 1.0f; // 衰减:点光源、聚光灯                      
    DirectX::XMFLOAT3 Direction = { 0.0f, -1.0f, 0.0f }; // 方向:平行光源、聚光灯(中心方向)
    float FalloffEnd = 10.0f; // 衰减:点光源、聚光灯                           
    DirectX::XMFLOAT3 Position = { 0.0f, 0.0f, 0.0f }; // 光源位置:点光源、聚光灯
    float SpotPower = 64.0f; // 仅聚光灯光源 -- 就是聚光灯因子的幂                          
};

LightingUtil.hlsl对应结构体:

struct Light
{
    float3 Strength;
    float FalloffStart;
    float3 Direction;  
    float FalloffEnd;  
    float3 Position;   
    float SpotPower;   
};

⭐⭐结构体Light中数据成员的排列顺序并不是任意指定的,MaterialConstans也是如此,要遵循HLSL的结构体封装规则(structure packing rule):详情参考龙书附录B的常量缓冲区封装规则。大意是:以填充对齐的方式,将结构体中的元素打包成4D向量。另外,单个元素不能一分为2的方式分到两个4D向量之中。 -- 如果没有按照4D封装,那么会出现很多空隙,会占据更多的空间。占据空间还是其次的问题,更严重的:在C++应用程序代码中,应有与HLSL部分相对应的结构体,但是C++结构体与HLSL结构体的封装规则并不相同。那么两者结构体布局不匹配,会导致memcpy函数使用错误

不安4D规则打包的情况:

 会出现很多空隙

常用的辅助函数:位于LightingUtil.hlsl -- 处理多种类型的光照

①CalcAttenuation:实现线性衰减 实现saturate函数

float CalcAttenuation(float d, float falloffStart, float falloffEnd)
{
    return saturate((falloffEnd - d) / (falloffEnd - falloffStart)); // HLSL函数:saturate()
}

②SchlickFresnel:石里克近似 -- 替代菲涅耳效应 即使你计算菲尼尔效率反射率R_F(\alpha_h)

float3 SchlickFresnel(float3 R0, float3 normal, float3 lightVec)
{
    float cosIncidentAngle = saturate(dot(normal, lightVec)); // max(n·L,0) -- saturate其实也相当于max因为范围0~1 
    // IncidentAngle:入射角
    
    float f0 = 1.f - cosIncidentAngle; // 1-cosθ
    float3 reflectPercent = R0 + (1.f - R0) * (f0*f0*f0*f0*f0); // 石里克近似
    
    return reflectPercent;
}
// 其中R0 = ((n-1)/(n+1))^2 其中n是折射率 -- 参见RTR 3rd Ed. P233

③BlinnPhong:计算反射到观察者眼中的光亮,该值为漫反射光亮与镜面反射光亮的综合 -- 注意这里没有考虑环境光照(因为这个函数是计算一个光源(不管什么种类)对照射点的影响)

float3 BlinnPhong(float3 lightStrength, float3 lightVec, 
    float3 normal, float3 toEye, Material mat)
{
    const float m = mat.Shiness * 256.f; // m由光泽度推导而来 
    float3 halfVec = normalize(toEye + lightVec); 

    float roughnessFactor = (m+8.f)*pow(max(dot(halfVec, normal), 0.f), m) / 8.f;
    float3 fresnelFactor = SchlickFresnel(mat.FresnelR0, halfVec, lightVec);
       
    float3 specAlbedo = fresnelFactor*roughnessFactor;
    // 尽管我们进行的是LDR(低动态范围)渲染,但镜面反射公式得到的结果可能超出0~1,因此现将其按比例缩小一些
    specAlbedo = specAlbedo / (specAlbedo + 1.f); // 除法是分量除法?

    return (mat.DiffuseAlbedo.rgb + specAlbedo) * lightStrength; // .rgb就是指4D向量的前3个元素集合
}

HLSL语法:当使用operator*令两个向量相乘时,计算方式是分量式乘法 -- 相除是分量式除法??

另外,朗伯余弦定理没有在此处实现

④辅助结构Material:

struct Material
{
    float4 DiffuseAlbedo;
    float3 FresnelR0;
    
    float Shiness; // 光泽度: Shiness = 1 - Roughness
}

⑤实现方向光源:

float3 ComputeDirectionalLight(Light L, Material mat, float3 normal, float3 toEye)
{
    float3 lightVec = -L.Direction;
    
    float ndotl = max(dot(lightVec, normal), 0.f); 
    float3 lightStrength = L.Strength * ndotl; // 朗伯余弦定理
    
    return BlinnPhong(lightStrength, lightVec, normal, toEye, mat);
}

⑥实现点光源:

float3 ComputePointLight(Light L, Material mat, float3 pos, float3 normal, float3 toEye)
{
    float3 lightVec = normalize(L.Position - pos);
    float d = length(L.Position - pos);
    
    if(d > L.FalloffEnd)
        return 0.f;
    
    float ndotl = max(dot(lightVec, normal), 0.f);
    float lightStrength = L.Strength * ndotl;

    // 衰减
    float att = CalcAttenuation(d, L.FalloffStart, L.FalloffEnd);
    lightStrength *= att; 
    
    return BlinnPhong(lightStrength, lightVec, normal, toEye, mat);
}

⑦实现聚光灯光源:

float3 ComputeSpotLight(Light L, Material mat, float3 pos, float3 normal, float3 toEye)
{
    float3 lightVec = normalize(L.Position - pos);
    float d = length(L.Position - pos);
    
    if(d > L.FalloffEnd)
        return 0.f;
    
    float ndotl = max(dot(lightVec, normal), 0.f);
    float lightStrength = L.Strength * ndotl;

    // 衰减
    float att = CalcAttenuation(d, L.FalloffStart, L.FalloffEnd);
    lightStrength *= att; 

    // 聚光灯因子
    float spotFactor = pow(max(dot(-lightVec, L.Direction), 0.f), L.SpotPower);
    lightStrength *= spotFactor;
    
    return BlinnPhong(lightStrength, lightVec, normal, toEye, mat);
}

⑧多种光照的累加:

float4 ComputeLighting(Light gLights[MaxLights], Material mat,
                       float3 pos, float3 normal, float3 toEye,
                       float3 shadowFactor)
// 注意这里语法:Light gLights[MaxLights]传递数组~~~~
{
    float3 result = 0.0f;

    int i = 0;

#if (NUM_DIR_LIGHTS > 0)
    for(i = 0; i < NUM_DIR_LIGHTS; ++i)
    {
        result += shadowFactor[i] * ComputeDirectionalLight(gLights[i], mat, normal, toEye);
    }
#endif

#if (NUM_POINT_LIGHTS > 0)
    for(i = NUM_DIR_LIGHTS; i < NUM_DIR_LIGHTS+NUM_POINT_LIGHTS; ++i)
    {
        result += ComputePointLight(gLights[i], mat, pos, normal, toEye);
    }
#endif

#if (NUM_SPOT_LIGHTS > 0)
    for(i = NUM_DIR_LIGHTS + NUM_POINT_LIGHTS; i < NUM_DIR_LIGHTS + NUM_POINT_LIGHTS + NUM_SPOT_LIGHTS; ++i)
    {
        result += ComputeSpotLight(gLights[i], mat, pos, normal, toEye);
    }
#endif 

    return float4(result, 0.0f);
}

shadowFactor在介绍阴影时才会用到,现在暂时将其设置为(1,1,1),不会对方程产生影响

⑦HLSL主文件:

// Defaults for number of lights.
#ifndef NUM_DIR_LIGHTS
    #define NUM_DIR_LIGHTS 1
#endif

#ifndef NUM_POINT_LIGHTS
    #define NUM_POINT_LIGHTS 0
#endif

#ifndef NUM_SPOT_LIGHTS
    #define NUM_SPOT_LIGHTS 0
#endif

// Include structures and functions for lighting.
#include "LightingUtil.hlsl"

// Constant data that varies per frame.
cbuffer cbPerObject : register(b0)
{
    float4x4 gWorld;
};

cbuffer cbMaterial : register(b1)
{
	float4 gDiffuseAlbedo;
    float3 gFresnelR0;
    float  gRoughness;
	float4x4 gMatTransform;
};

// Constant data that varies per material.
cbuffer cbPass : register(b2)
{
    float4x4 gView;
    float4x4 gInvView;
    float4x4 gProj;
    float4x4 gInvProj;
    float4x4 gViewProj;
    float4x4 gInvViewProj;
    float3 gEyePosW;
    float cbPerObjectPad1;
    float2 gRenderTargetSize;
    float2 gInvRenderTargetSize;
    float gNearZ;
    float gFarZ;
    float gTotalTime;
    float gDeltaTime;
    float4 gAmbientLight;

    // Indices [0, NUM_DIR_LIGHTS) are directional lights;
    // indices [NUM_DIR_LIGHTS, NUM_DIR_LIGHTS+NUM_POINT_LIGHTS) are point lights;
    // indices [NUM_DIR_LIGHTS+NUM_POINT_LIGHTS, NUM_DIR_LIGHTS+NUM_POINT_LIGHT+NUM_SPOT_LIGHTS)
    // are spot lights for a maximum of MaxLights per object.
    Light gLights[MaxLights];
};
 
struct VertexIn
{
	float3 PosL    : POSITION;
    float3 NormalL : NORMAL;
};

struct VertexOut
{
	float4 PosH    : SV_POSITION;
    float3 PosW    : POSITION;
    float3 NormalW : NORMAL;
};

VertexOut VS(VertexIn vin)
{
	VertexOut vout = (VertexOut)0.0f;
	
    // Transform to world space.
    float4 posW = mul(float4(vin.PosL, 1.0f), gWorld);
    vout.PosW = posW.xyz;

    // Assumes nonuniform scaling; otherwise, need to use inverse-transpose of world matrix.
    vout.NormalW = mul(vin.NormalL, (float3x3)gWorld);

    // Transform to homogeneous clip space.
    vout.PosH = mul(posW, gViewProj);

    return vout;
}

float4 PS(VertexOut pin) : SV_Target
{
    // Interpolating normal can unnormalize it, so renormalize it.
    pin.NormalW = normalize(pin.NormalW);

    float3 toEyeW = normalize(gEyePosW - pin.PosW);

	// 间接光照
    float4 ambient = gAmbientLight*gDiffuseAlbedo;

    const float shininess = 1.0f - gRoughness;
    Material mat = { gDiffuseAlbedo, gFresnelR0, shininess };
    float3 shadowFactor = 1.0f;
    float4 directLight = ComputeLighting(gLights, mat, pin.PosW,
        pin.NormalW, toEyeW, shadowFactor);

    float4 litColor = ambient + directLight; // 间接光照+直接光照

    // 从漫反射材质中获取alpha值的常见手段
    litColor.a = gDiffuseAlbedo.a;

    return litColor;
}

课后习题6 -- 卡通风格光照:颜色过渡突兀

①出现的问题:方向光源只能设置少于等于3个 -- 原因是:

float4 ComputeLighting(Light gLights[MaxLights], Material mat,
                       float3 pos, float3 normal, float3 toEye,
                       float3 shadowFactor)
{
    float3 result = 0.0f;

    int i = 0;

#if (NUM_DIR_LIGHTS > 0) 
    for(i = 0; i < NUM_DIR_LIGHTS; ++i)
    {
        result += shadowFactor[i] * ComputeDirectionalLight(gLights[i], mat, normal, toEye);
    }
#endif

    // [...]
}

shadowFactor是一个float3,其下标只有0~2,所以不能超过3个。

②Light结构体的问题:

mMainPassCB.Lights[0].Direction = { 0.57735f, -0.57735f, 0.57735f };
mMainPassCB.Lights[0].Strength = { 0.6f, 0.6f, 0.6f };
mMainPassCB.Lights[1].Direction = { -0.57735f, -0.57735f, 0.57735f };
mMainPassCB.Lights[1].Strength = { 0.3f, 0.3f, 0.3f };
mMainPassCB.Lights[2].Direction = { 0.0f, -0.707f, -0.707f };
mMainPassCB.Lights[2].Strength = { 0.15f, 0.15f, 0.15f };
struct PassConstants
{
    DirectX::XMFLOAT4X4 View = MathHelper::Identity4x4();
    DirectX::XMFLOAT4X4 InvView = MathHelper::Identity4x4();
    DirectX::XMFLOAT4X4 Proj = MathHelper::Identity4x4();
    DirectX::XMFLOAT4X4 InvProj = MathHelper::Identity4x4();
    

    // ...


    Light Lights[MaxLights];
};
struct Light
{
    DirectX::XMFLOAT3 Strength = { 0.5f, 0.5f, 0.5f };
    float FalloffStart = 1.0f;                          // point/spot light only
    DirectX::XMFLOAT3 Direction = { 0.0f, -1.0f, 0.0f };// directional/spot light only
    float FalloffEnd = 10.0f;                           // point/spot light only
    DirectX::XMFLOAT3 Position = { 0.0f, 0.0f, 0.0f };  // point/spot light only
    float SpotPower = 64.0f;                            // spot light only
};

CPU端Light数据是XMFLOAT3而不是XMVECTOR,所以需要进一步转换

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值