DirectX12 3D 游戏开发与实战第八章内容(下)
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 游戏开发与实战第八章内容(下)的更多相关文章
- DirectX12 3D 游戏开发与实战第八章内容(上)
8.光照 学习目标 对光照和材质的交互有基本的了解 了解局部光照和全局光照的区别 探究如何用数学来描述位于物体表面上某一点的"朝向",以此来确定入射光照射到表面的角度 学习如何正确 ...
- DirectX12 3D 游戏开发与实战第九章内容(上)
仅供个人学习使用,请勿转载. 9.纹理贴图 学习目标: 学习如何将局部纹理映射到网格三角形上 探究如何创建和启用纹理 学会如何通过纹理过滤来创建更加平滑的图像 探索如何使用寻址模式来进行多次纹理贴图 ...
- DirectX12 3D 游戏开发与实战第一章内容
DirectX12 3D 第一章内容 学习目标 1.学习向量在几何学和数学中的表示方法 2.了解向量的运算定义以及它在几何学中的应用 3.熟悉DirectXMath库中与向量有关的类和方法 1.1 向 ...
- DirectX12 3D 游戏开发与实战第二章内容
矩阵代数 学习目标 理解矩阵及其相关运算的定义 探究为何能把向量和矩阵的乘法视为一种线性组合 学习单位矩阵.转置矩阵.行列式以及矩阵的逆等概念 逐步熟悉DirectXMath库中提供的关于矩阵计算的类 ...
- DirectX12 3D 游戏开发与实战第十章内容(下)
仅供个人学习使用,请勿转载.谢谢! 10.混合 本章将研究混合技术,混合技术可以让我们将当前需要光栅化的像素(也称为源像素)和之前已经光栅化到后台缓冲区的像素(也称为目标像素)进行融合.因此,该技术可 ...
- DirectX12 3D 游戏开发与实战第十章内容(上)
仅供个人学习使用,请勿转载.谢谢! 10.混合 本章将研究混合技术,混合技术可以让我们将当前需要光栅化的像素(也称为源像素)和之前已经光栅化到后台缓冲区的像素(也称为目标像素)进行融合.因此,该技术可 ...
- DirectX12 3D 游戏开发与实战第九章内容(下)
仅供个人学习使用,请勿转载.谢谢! 9.纹理贴图 学习目标 学习如何将局部纹理映射到网格三角形中 探究如何创建和启用纹理 学会如何通过纹理过滤来创建更加平滑的图像 探索如何使用寻址模式来进行多次贴图 ...
- DirectX12 3D 游戏开发与实战第五章内容
渲染流水线 学习目标: 了解用于在2D图像中表现出场景立体感和空间深度感等真实效果的关键因素 探索如何用Direct3D表示3D对象 学习如何建立虚拟摄像机 理解渲染流水线,根据给定的3D场景的几何描 ...
- DirectX12 3D 游戏开发与实战第四章内容(上)
Direct3D的初始化(上) 学习目标 了解Direct3D在3D编程中相对于硬件所扮演的角色 理解组件对象模型COM在Direct3D中的作用 掌握基础的图像学概念,例如2D图像的存储方式,页面翻 ...
随机推荐
- dice_game攻防世界进阶区
dice_game XCTF 4th-QCTF-2018 前言,不得不说,虽然是个简单题但是还是要记录一下,来让自己记住这些东西. 考察的知识点是: 1.cdll_loadlibrary加载对应库使得 ...
- Linux多线程编程之详细分析
线程?为什么有了进程还需要线程呢,他们有什么区别?使用线程有什么优势呢?还有多线程编程的一些细节问题,如线程之间怎样同步.互斥,这些东西将在本文中介绍.我见到这样一道面试题: 是否熟悉POSIX多线程 ...
- js实现日期格式化封装-八种格式
封装一个momentTime.js文件,包含8种格式. 需要传两个参数: 时间戳:stamp 格式化的类型:type, 日期补零的方法用到es6语法中的padStart(length,'字符'): 第 ...
- Linux 文本三剑客之 grep
Linux 系统中一切皆文件. 文件是个文本.可以读.可以写,如果是二进制文件,还能执行. 在使用Linux的时候,大都是要和各式各样文件打交道.熟悉文本的读取.编辑.筛选就是linux系统管理员的必 ...
- k8s入坑之路(3)containerd容器
containerd概念: containerd主要是namebases与k8s docker不同 存放路径不一致 没有默认仓库 容器运行时: 2020年未kubernetes宣布不再支持docker ...
- JavaScript 事件循环
JavaScript 事件循环 事件循环 任务队列 async/await 又是如何处理的呢 ? 定时器问题 阻塞还是非阻塞 实际应用案例 拆分 CPU 过载任务 进度指示 在事件之后做一些事情 事件 ...
- fabric运行记录
创建第一个fabric网络 Generate Network Artifacts cd first-network 先关闭服务 ./byfn.sh -m down 然后创建 ./byfn.sh -m ...
- 『学了就忘』Linux基础命令 — 36、查看系统痕迹相关命令
目录 1.w命令 2.who命令 3.last命令 4.lastlog命令 5.lastb命令 系统中有一些重要的痕迹日志文件,如/var/log/wtmp./var/run/utmp./var/lo ...
- airflow 并发上不去
airflow.cfg parallelism配置是否合适 任务池slot是否足够
- 🏆【Alibaba中间件技术系列】「RocketMQ技术专题」小白专区之领略一下RocketMQ基础之最!
应一些小伙伴们的私信,希望可以介绍一下RocketMQ的基础,那么我们现在就从0开始,进入RocketMQ的基础学习及概念介绍,为学习和使用RocketMQ打好基础! RocketMQ的定位 Rock ...