DirectX12 3D 游戏开发与实战第八章内容(下)

8.9、材质的实现

下面是材质结构体的部分代码:

// 简单的结构体来表示我们所演示的材料
struct Material
{
// 材质唯一对应的名称(便于查找)
std::string Name; // 本材质的常量缓冲区索引
int MatCBIndex = -1; // 漫反射在SRV堆中的索引(在第九章的纹理贴图中会使用)
int DiffuseSrvHeapIndex = -1; int NormalSrvHeapIndex = -1; //已更新标志(dirty flag,也称为脏标志),用于表示本材质已经发生变动
//因为每一个帧资源(Frame Resource)都有材质常量缓冲区,所以我们需要对每个
//帧资源进行更新,因此当我们修改某一个材质的时候,我们需要对每一个帧资源进
//行更新,即 NumFrameDirty = gNumFrameResource
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();
};

为了模拟出真实世界中的材质,我们需要设置DiffuseAlbedo(漫反射反照率)FresnelR0(介质的一种属性)这对和真实度相关的数值,然后添加一些细节调整,以达到更佳的视觉效果。在材质结构体中,我们把粗糙度指定在归一化的浮点值范围之间[0, 1]之间。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; 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能够在着色器中访问到这些材质数据。同时我们还要将相关的数据复制到常量缓冲区中,并将存有每个材质常量的常量缓冲区添加到每一个帧资源中:

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

在更新函数中,当材质数据发生变化之后(即存在所谓的脏标志),我们便会将其复制到常量缓冲区对应的子区域中,因此GPU中的材质常量缓冲区中的数据总是和系统内存的最新数据保持一致的:

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); // 也需要对下一个FrameResource进行更新
mat->NumFramesDirty--;
}
}
}

到现在为止,每一个渲染项都已经拥有了一个指向Material结构体的指针,多个渲染项可以指向引用相同的Material对象,每一个Material对象都存有一个索引,用于在材质常量缓冲区中指向它自己的常量数据。下面代码演示了如何用不用的材质来绘制渲染项:

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); cmdList->DrawIndexedInstanced(ri->IndexCount, 1, ri->StartIndexLocation, ri->BaseVertexLocation, 0);
}
}

重点:我们需要获取三角形网格表面上每一点处的法向量,用来确定光线照射到网格的角度(用于朗伯余弦定律),而为了获取每一个点的近似法向量,我们就需要在顶点这一层来指定法线。然后再三角形的光栅化过程中,便会利用这些顶点法线来进行插值计算。

8.10、平行光源

平行光源(parallel light)也称为方向光源(directional light),是一种距离目标物体极远的光源。因此我们可以将这种光源发出的光线视为彼此平行的光线。(计算方法略)

8.11、点光源

一个和点光源(point light)比较贴切的现实实例时灯泡,它能以球面向各个方向发出光线,特别的,对于任意一点P,由位置W处点光源发出的光线,总有一束会传播到P点。点光源和平行光源之间唯一的区别就是光向量的计算方法,点光源的光向量随着目标点的不同而改变,而对于平行光而言,光向量始终保持不变。(计算方法略)

8.12、聚光灯光源

一个和聚光灯光源(sportLight)相近的现实实例是手电筒,从本质上来讲,聚光灯是由位置W向方向d照射出范围内呈圆锥体的光(计算方法略)

8.13光照的具体实现

在本节中,我们将对上述三种光源的实现细节进行讨论

8.13.1、Light结构体

在d3dUtil文件中,我们定义了下列结构体来描述光源,此结构体可以表示方向光源、点光源和聚光灯光源。但是根据光源的具体类型,我们并不会使用到其中的所有数据。

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中的数据成员的排列顺序是不可以随便指定的,需要遵循HLSL的结构体封装规则。这条规则大意是将结构体中的元素打包成4D向量,而且不能将单个元素一分为二分到两个4D向量中。所以Light结构体经过打包之后是这样的:

	vertor4D 1:	(Strength.x, Strength.y, strength.z, FalloffStart);
vector4D 2: (Direction.x, Direction.y, Direction.z, FalloffEnd);
vector4D 3: (Position.x, Position.y, Position.z, SpotPower);

如果将上述结构体的数据成员的排列顺序进行变更:

struct Light
{
DirectX::XMFLOAT3 Strength = { 0.5f, 0.5f, 0.5f };
DirectX::XMFLOAT3 Direction = { 0.0f, -1.0f, 0.0f };
DirectX::XMFLOAT3 Position = { 0.0f, 0.0f, 0.0f };
float FalloffStart = 1.0f;
float FalloffEnd = 10.0f;
float SpotPower = 64.0f;
}; //HLSL文件:
struct Light
{
float3 Strength;
float3 Direction;
float3 Position;
float FalloffStart;
float FalloffEnd;
float SpotPower;
};

则它将被打包为:

	vertor4D 1:	(Strength.x, Strength.y, strength.z, empty);
vector4D 2: (Direction.x, Direction.y, Direction.z, empty);
vector4D 3: (Position.x, Position.y, Position.z, empty);
vector4D 4: (FalloffStart, FalloffEnd, SpotPower, empth);

显然第二种方案占用了更多的空间,而且因为c++和HLSL对应的结构体封装规则并不相同,如果不按照HLSL的规则来实现c++和HLSL的结构体,可能会导致通过memcpy函数从CPU上传到GPU常量缓冲区的数据将会导致渲染错误

8.13.2、常用的辅助函数

下面3个函数定义在LightingUtil.hlsl文件中,由于这些函数可以处理多种类型的光照,所以我们将他们定义为辅助函数

函数名称 作用
CalcAttenuation 实现了一种线性衰减因子的计算方法,可以将其应用于点光源和聚光灯光源
SchlickFresnel 代替菲涅尔方程的石里克近似,此函数基于光向量L和表面法线n之间的夹角近似的计算出以n为法线的表面所反射光的百分比
BlinnPhong 计算反射到观察者眼中的光量(该值为漫反射光量和镜面反射光量的总和)
/*
** summary:线性衰减因子的计算方法
** Parameters:
** d:距离光源的距离
** falloffStart:开始衰减的距离(未到达这个距离前保持最大强度)
** falloffEnd:大于这个距离将不会受到光照(即衰减因子为0)
** Return:衰减因子
*/
float CalcAttenuation(float d, float falloffStart, float falloffEnd)
{
// 线性衰减
return saturate((falloffEnd-d) / (falloffEnd - falloffStart));
}
//R0:介质的一种属性,会影响反射光量
float3 SchlickFresnel(float3 R0, float3 normal, float3 lightVec)
{
float cosIncidentAngle = saturate(dot(normal, lightVec)); float f0 = 1.0f - cosIncidentAngle;
float3 reflectPercent = R0 + (1.0f - R0)*(f0*f0*f0*f0*f0); return reflectPercent;
}
float3 BlinnPhong(float3 lightStrength, float3 lightVec, float3 normal, float3 toEye, Material mat)
{
const float m = mat.Shininess * 256.0f;
float3 halfVec = normalize(toEye + lightVec); float roughnessFactor = (m + 8.0f)*pow(max(dot(halfVec, normal), 0.0f), m) / 8.0f;
float3 fresnelFactor = SchlickFresnel(mat.FresnelR0, halfVec, lightVec); float3 specAlbedo = fresnelFactor*roughnessFactor; specAlbedo = specAlbedo / (specAlbedo + 1.0f); return (mat.DiffuseAlbedo.rgb + specAlbedo) * lightStrength;
}

上述代码中所用的HLSL内部函数:dot、pow和max分别表示向量点积函数、幂函数和取最大值函数。

8.13.3、实现方向光源

给定观察位置E、材质属性和以n为法线的表面上可见一点p,则下列HLSL函数将输出自某一方向光源发出,经上述表面以方向 v = normalize(E - p)反射入观察者眼中的光量。

float3 ComputeDirectionalLight(Light L, Material mat, float3 normal, float3 toEye)
{
// 光向量和光的传播方向刚好相反
float3 lightVec = -L.Direction; // 通过朗伯余弦定律按比例降低光强
float ndotl = max(dot(lightVec, normal), 0.0f);
float3 lightStrength = L.Strength * ndotl; return BlinnPhong(lightStrength, lightVec, normal, toEye, mat);
}

8.13.4、实现点光源

给出观察点E、材质属性和以n为法线的表面上可见一点p,则下面的HLSL函数将输出来自点光源放出的光线,经上述表面以 v = normalize(E - p)方向反射到观察者眼中的光量。

float3 ComputePointLight(Light L, Material mat, float3 pos, float3 normal, float3 toEye)
{
// 自物体表面指向光源的光向量
float3 lightVec = L.Position - pos; // 光源距离表面的距离
float d = length(lightVec); // 范围检测(如果超出衰减距离,则该表面无法接收到光照)
if(d > L.FalloffEnd)
return 0.0f; // 对光向量进行规格化处理
lightVec /= d; // 通过朗伯余弦定律按比例降低光强
float ndotl = max(dot(lightVec, normal), 0.0f);
float3 lightStrength = L.Strength * ndotl; // 根据距离计算衰减因子,然后计算出衰减后的光
float att = CalcAttenuation(d, L.FalloffStart, L.FalloffEnd);
lightStrength *= att; return BlinnPhong(lightStrength, lightVec, normal, toEye, mat);
}

8.13.5、实现聚光灯光源

指定观察点E、材质属性以及以n为法线的表面上的一点p,则下列HLSL函数将输出来自聚光灯光源,经过上述表面以方向v = normalize(E - p)反射到观察者眼中的光量。

float3 ComputeSpotLight(Light L, Material mat, float3 pos, float3 normal, float3 toEye)
{
// 光向量等于自表面指向光源的向量
float3 lightVec = L.Position - pos; // 计算物体表面距离光源的距离
float d = length(lightVec); // 范围检测
if(d > L.FalloffEnd)
return 0.0f; // 规格化光向量
lightVec /= d; // 根据朗伯余弦定律按比例降低光强
float ndotl = max(dot(lightVec, normal), 0.0f);
float3 lightStrength = L.Strength * ndotl; // 根据距离计算衰减因子
float att = CalcAttenuation(d, L.FalloffStart, L.FalloffEnd);
lightStrength *= att; // 根据聚光灯模型对光强进行缩放处理
float spotFactor = pow(max(dot(-lightVec, L.Direction), 0.0f), L.SpotPower);
lightStrength *= spotFactor; return BlinnPhong(lightStrength, lightVec, normal, toEye, mat);
}

8.13.6、多种光照的叠加

光照是可以叠加的,所以在拥有多个光照的场景中,我们需要遍历每一个光源,然后计算他们在我们要计算光照的点或者像素上的贡献值求和。

实例框架最多支持16个光源,即光源数量不可以超过16个。此外,代码所约定的是方向光必须存放在光照数组的开始部分,然后是点光源,最后存放聚光灯光源。下列代码用于计算某一点的光照方程:

#define MaxLights 16

// 绘制过程中所使用的杂项常量数据
cbuffer cbPass : register(b2)
{
……
//[0, NUM_DIR_LIGHTS]表示的是平行光源
//[NUM_DIR_LISGHTS, NUM_DIR_LIGHTS + NUM_POINT_LIGHTS]表示的是点光源
//[NUM_DIR_LIGHTS + NUM_POINT_LIGHTS, NUM_DIR_LIGHTS + NUM_POINT_LIGHTS + NUM_SPOT_LIGHTS]
//表示的是聚光灯光源
Light gLights[MaxLights];
}; 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 #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);
}

8.13.7、HLSl主文件

DirectX12 3D 游戏开发与实战第八章内容(下)的更多相关文章

  1. DirectX12 3D 游戏开发与实战第八章内容(上)

    8.光照 学习目标 对光照和材质的交互有基本的了解 了解局部光照和全局光照的区别 探究如何用数学来描述位于物体表面上某一点的"朝向",以此来确定入射光照射到表面的角度 学习如何正确 ...

  2. DirectX12 3D 游戏开发与实战第九章内容(上)

    仅供个人学习使用,请勿转载. 9.纹理贴图 学习目标: 学习如何将局部纹理映射到网格三角形上 探究如何创建和启用纹理 学会如何通过纹理过滤来创建更加平滑的图像 探索如何使用寻址模式来进行多次纹理贴图 ...

  3. DirectX12 3D 游戏开发与实战第一章内容

    DirectX12 3D 第一章内容 学习目标 1.学习向量在几何学和数学中的表示方法 2.了解向量的运算定义以及它在几何学中的应用 3.熟悉DirectXMath库中与向量有关的类和方法 1.1 向 ...

  4. DirectX12 3D 游戏开发与实战第二章内容

    矩阵代数 学习目标 理解矩阵及其相关运算的定义 探究为何能把向量和矩阵的乘法视为一种线性组合 学习单位矩阵.转置矩阵.行列式以及矩阵的逆等概念 逐步熟悉DirectXMath库中提供的关于矩阵计算的类 ...

  5. DirectX12 3D 游戏开发与实战第十章内容(下)

    仅供个人学习使用,请勿转载.谢谢! 10.混合 本章将研究混合技术,混合技术可以让我们将当前需要光栅化的像素(也称为源像素)和之前已经光栅化到后台缓冲区的像素(也称为目标像素)进行融合.因此,该技术可 ...

  6. DirectX12 3D 游戏开发与实战第十章内容(上)

    仅供个人学习使用,请勿转载.谢谢! 10.混合 本章将研究混合技术,混合技术可以让我们将当前需要光栅化的像素(也称为源像素)和之前已经光栅化到后台缓冲区的像素(也称为目标像素)进行融合.因此,该技术可 ...

  7. DirectX12 3D 游戏开发与实战第九章内容(下)

    仅供个人学习使用,请勿转载.谢谢! 9.纹理贴图 学习目标 学习如何将局部纹理映射到网格三角形中 探究如何创建和启用纹理 学会如何通过纹理过滤来创建更加平滑的图像 探索如何使用寻址模式来进行多次贴图 ...

  8. DirectX12 3D 游戏开发与实战第五章内容

    渲染流水线 学习目标: 了解用于在2D图像中表现出场景立体感和空间深度感等真实效果的关键因素 探索如何用Direct3D表示3D对象 学习如何建立虚拟摄像机 理解渲染流水线,根据给定的3D场景的几何描 ...

  9. DirectX12 3D 游戏开发与实战第四章内容(上)

    Direct3D的初始化(上) 学习目标 了解Direct3D在3D编程中相对于硬件所扮演的角色 理解组件对象模型COM在Direct3D中的作用 掌握基础的图像学概念,例如2D图像的存储方式,页面翻 ...

随机推荐

  1. [no code][scrum meeting] Beta 5

    $( "#cnblogs_post_body" ).catalog() 例会时间:5月18日14:30,主持者:叶开辉 下次例会时间:5月19日11:30,主持者:黎正宇 一.工作 ...

  2. 2021.9.18考试总结[NOIP模拟56]

    T1 爆零 贪心地想,肯定要先走完整个子树再走下一个,且要尽量晚地走深度大的叶子.所以对每个点的儿子以子树树高为关键字排序$DFS$即可. 也可$DP$. $code:$ T1 #include< ...

  3. 在c中使用正则表达式

    今天学习编译原理的时候,用c写一个简易的文法识别器实验遇到了一个问题:要用正则表达式去识别正则文法里面的A->ω,A->Bω, 其中ω属于T的正闭包,也就是说我们对正则文法的产生式进行抽象 ...

  4. 文件挂载swap

    根目录使用率超过79%,根目录总共45G,/home目录下有文件6G的swap,在新加的300G分区/OracleDB中建立4个G的swap替代/home下在swap文件 1.创建4个G的空文件 #  ...

  5. MySQL怎么缓解读的压力的?---buffer pool

    每当我们想要缓解读,一般会想到什么? 预读取,缓存 缓存 缓存,其实就是将高频访问的数据放到内存里面,减少读盘的次数. 为了提高内存的利用率,MySQL还建立了缓存池,也就是buffer pool,存 ...

  6. ansible模块及语法

    常用模块详解 模块说明及示例: 1.ping模块ping模块 主要用于判断远程客户端是否在线,用于ping本身服务器,返回值是changed.ping示例 ansible clu -m ping 2. ...

  7. jenkins 安装与使用

    1.jenkins下载:https://jenkins.io/zh/download/ 2.将下载好的war包放到tomcat容器下的D:\apache-tomcat-9.0.10\webapps下( ...

  8. pip 更新方法

    使用python -m pip install --upgrade pip 使用python -m pip install -U --force-reinstall pip 使用pip install ...

  9. 【Python+postman接口自动化测试】(7)Postman 的使用教程

    Postman v6的使用 Postman: 简单方便的接口调试工具,便于分享和协作.具有接口调试,接口集管理,环境配置,参数化,断言,批量执行,录制接口,Mock Server, 接口文档,接口监控 ...

  10. this.$set用法

    this.$set()的主要功能是解决改变数据时未驱动视图的改变的问题,也就是实际数据被改变了,但我们看到的页面并没有变化,这里主要讲this.$set()的用法,如果你遇到类似问题可以尝试下,vue ...