环境:VS2017 语言:C++
总起:
这一章主要对应红龙书的第七章。
工程地址:https://github.com/anguangzhihen/Dx11。主要以Chapter 7_1 Lighting Demo作为讲解的基础。
首先说一下光照的重要性,在学习绘画的基础课程中有两个要点,一个是形,另一个就是光影的运用。好的光照不仅能提升场景的真实性,更加在氛围上做出烘托。3D游戏失去光照,就像建筑失去最主要的支撑柱最后只能轰塌。
接下说一下我现在接触到的一些光照的知识。
标准光照模型,最简单的光照模型,早期的游戏多使用该模型或该模型的改进版。物体表面的颜色由自发光(本章不会涉及)、高光、漫反射、环境光四部分组成。在标准光照模型的基础上,Blinn改进了高光算法,被称为Blinn-Phong光照模型。
我们使用的漫反射又被称为兰伯特(Lambert)光照模型,而因为背光区会失去明暗变化,所以Valve在半条命中使用一种叫做半兰伯特光照模型。
BRDF光照模型(PBR),基于物理渲染的光照模型,BRDF的含义是双向反射分布函数,较为真实,但性能消耗大。很多书上可能说多用于3A大作啥的,不过据我了解,现在很多大型的手游都开始使用该技术了。(关于这个详细的没太研究过,不过推荐三本书:RTR、PBRT、全局光照技术)
GI(Global Illumination)全局光照,GI一般来讲由两部分组成,一个是光源直接对物体照射获得的明暗,另一个是间接的光照,例如阴影、反射等。对应GI的LI(Local Illumination)局部光照只讨论直接的光照。
本章内容主要讨论的是最简单的光照模型。本次的素材是我们之前第四章实现的山地和第五章实现的水波。
首先来看一下效果:
准备数据:
事实上光照没有用到任何新的技术,只是在之前知识的基础上编写光照模型的函数。
我们来看一下需要从C++(CPU)传到Shader(GPU)的数据:
// C++代码
// 顶点Shader的传入参数
struct Vertex
{
XMFLOAT3 Pos; // 位置信息
XMFLOAT3 Normal; // 法线
};
// 顶点着色器的常量缓存
struct ConstantBuffer
{
XMMATRIX gWorld;
XMMATRIX gWorldInvTranspose; // World逆转置矩阵,用于将法线从模型坐标系中转换到世界坐标系
XMMATRIX gWorldViewProj; // wvp
};
// 片元着色器的常量缓存
struct FrameConstantBuffer
{
DirectionalLight gDirLight; // 直射光
PointLight gPointLight; // 点光源
SpotLight gSpotLight; // 聚光灯
Material gMaterial; // 物体材质
XMFLOAT4 gEyePosW; // 当前点到相机向量
};
// Shader代码
// 顶点着色器的常量缓存
cbuffer cbPerObject : register(b0)
{
row_major matrix gWorld; // 默认列主矩阵
row_major matrix gWorldInvTranspose; // World逆转置矩阵,用于将法线从模型坐标系中转换到世界坐标系
row_major matrix gWorldViewProj; // wvp
};
// 片元着色器的常量缓存
cbuffer cbPerFrame : register(b1)
{
DirectionalLight gDirLight; // 直射光
PointLight gPointLight; // 点光源
SpotLight gSpotLight; // 聚光灯
Material gMaterial; // 物体材质
float3 gEyePosW; // 当前点到相机向量
};
// 顶点Shader的传入参数
struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
};
// 顶点Shader返回值,并传入片元Shader
struct VertexOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
};
可以看到这边传入了光源的三种形式:直射光(太阳)、点光源、聚光灯。然后顶点数据中传入的法线用于光强度的计算。
这边提一下常量从C++代码中传到Shader一些注意事项(顶点数据因为一开始就在InputLayout中申明了数据类型所以一般很少会出问题)。
原则是所有数据结构都128bit(float4)对齐。
以下是一个容易出错的例子:
struct S
{
float2 s
};
cbuffer A
{
float a;
S b;
float2 c;
};
程序在处理时遵循一个简单的规则:遇到struct、数组换行;遇到类似float3、float4等数组,如果之前的还能放下则放入,否则换行。
所以以上的结果是:
a.x empty empty empty
b.s.x b.s.y c.x c.y
一开始可能感觉有点抽象,结论是写struct时不够128bit时使用空数据将其对齐,数组尽量使用float4数组或者已经对齐的struct数组。
光照的计算:
三种光照都遵循同一种思路:光照 = 漫反射 + 高光 + 环境光。(只是点光源和聚光灯有相应的衰减)
漫反射Lambert光照模型计算公式如下:
result = max(n·l, 0) * Ld * Md(其中,n为法线;l光方向的矢量;Ld直射光颜色;Md材质颜色)
Phong高光的计算公式如下:
result = (max(r·eye, 0))^p * Lc * Mc(其中,r为光线反射方向 r = 2(n·l)n – l;eye为视角方向;p为高光衰减参数;Lc高光颜色;Md材质高光颜色)
环境只是单纯对物体颜色的叠加,不多说。
以下是计算函数:
// 计算直射光
void ComputeDirectionalLight(Material mat, DirectionalLight L,
float3 normal, float3 toEye,
out float4 ambient, out float4 diffuse, out float4 spec)
{
// 初始化输出
ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
// 环境光的计算
ambient = mat.Ambient * L.Ambient;
float3 lightVec = -L.Direction; // 获取light的向量
float diffuseFactor = dot(lightVec, normal); // 漫反射的强度
[flatten]
if (diffuseFactor > 0.0f)
{
// 漫反射光的计算
diffuse = diffuseFactor * mat.Diffuse * L.Diffuse;
// 高光的计算
float3 v = reflect(-lightVec, normal); // 计算反射角
float specFactor = pow(max(dot(v, toEye), 0.0f), mat.Specular.w); // 高光的衰减程度
spec = specFactor * mat.Specular * L.Specular;
// Blinn的方式
//float3 h = normalize(toEye + lightVec);
//specFactor = pow(max(dot(h, normal), 0.0f), mat.Specular.w);
//spec = specFactor * mat.Specular * L.Specular;
}
}
// 计算点光源
void ComputePointLight(Material mat, PointLight L,
float3 pos, float3 normal, float3 toEye,
out float4 ambient, out float4 diffuse, out float4 spec)
{
// 初始化输出
ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
// 获取light的向量
float3 lightVec = L.Position - pos;
float d = length(lightVec);
lightVec /= d;
// 超过范围直接跳出
if (d > L.Range)
return;
// 环境光的计算
ambient = mat.Ambient * L.Ambient;
float diffuseFactor = dot(lightVec, normal); // 漫反射的强度
[flatten]
if (diffuseFactor > 0.0f)
{
// 漫反射光的计算
diffuse = diffuseFactor * mat.Diffuse * L.Diffuse;
// 高光的计算
float3 v = reflect(-lightVec, normal); // 计算反射角
float specFactor = pow(max(dot(v, toEye), 0.0f), mat.Specular.w); // 高光的衰减程度
spec = specFactor * mat.Specular * L.Specular;
// 衰减
float att = 1.0f / dot(L.Att, float3(1.0f, d, d*d));
diffuse *= att;
spec *= att;
}
}
// 计算聚光灯
void ComputeSpotLight(Material mat, SpotLight L,
float3 pos, float3 normal, float3 toEye,
out float4 ambient, out float4 diffuse, out float4 spec)
{
// 初始化输出
ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
// 获取light的向量
float3 lightVec = L.Position - pos;
float d = length(lightVec);
lightVec /= d;
if (d > L.Range)
return;
// 环境光的计算
ambient = mat.Ambient * L.Ambient;
float spot = pow(max(dot(-lightVec, L.Direction), 0.0f), L.Spot);
ambient *= spot;
float diffuseFactor = dot(lightVec, normal); // 漫反射的强度
[flatten]
if (diffuseFactor > 0.0f)
{
// 漫反射光的计算
diffuse = diffuseFactor * mat.Diffuse * L.Diffuse;
// 高光的计算
float3 v = reflect(-lightVec, normal); // 计算反射角
float specFactor = pow(max(dot(v, toEye), 0.0f), mat.Specular.w); // 高光的衰减程度
spec = specFactor * mat.Specular * L.Specular;
// 衰减
float att = spot / dot(L.Att, float3(1.0f, d, d * d));
diffuse *= att;
spec *= att;
}
}
这边提醒一点,高光类似于一个椎体,相机在中线处没有衰减,越往外衰减越厉害,脱离椎体后就看不到高光了。实际上聚光灯的原理和高光是类似的。