第 8 章 光照
光照是计算机图形学中最重要的部分之一,它让我们能够创建逼真的三维场景。没有合适的光照,即使最精细的模型也会显得平淡无趣。本章将深入探讨光照的理论基础和实现方法,从基本概念到高级技术,全面解析如何在Direct3D应用程序中实现各种光照效果。
8.1 光照与材质的交互
在现实世界中,我们看到的物体是由光线与物体表面相互作用后反射到我们眼睛的结果。这种相互作用受到两个主要因素的影响:光源的性质和物体表面的材质特性。
8.1.1 光的基本特性
光在图形学中通常被简化为以下几个参数:
- 颜色/强度: 通常用RGB值表示光的颜色和亮度
- 方向: 对于方向光源(如太阳)很重要
- 位置: 对于点光源和聚光灯源很重要
- 衰减: 随距离减弱的速率
8.1.2 材质的基本特性
材质定义了物体表面如何响应光照:
- 漫反射系数(Diffuse): 定义表面均匀散射光线的能力
- 镜面反射系数(Specular): 定义表面的反射高光强度
- 环境反射系数(Ambient): 表示间接光照下的基础可见度
- 发光系数(Emissive): 表面自发光的强度
- 光泽度(Shininess): 控制镜面高光的集中程度
在DirectX中,这些属性通常被封装在一个材质结构中:
cpp
// 基本材质结构
struct Material {
XMFLOAT4 Diffuse; // 漫反射颜色
XMFLOAT4 Specular; // 镜面反射颜色和强度(a通道存储光泽度)
XMFLOAT4 Ambient; // 环境光反射颜色
XMFLOAT4 Emissive; // 自发光颜色
float Roughness; // 表面粗糙度
float Metalness; // 金属度(用于PBR)
float Opacity; // 不透明度
float Padding; // 填充到16字节边界
};
8.1.3 光照方程基础
最简单的光照交互可以通过以下公式表示:
最终颜色 = 环境光 + 漫反射光 + 镜面反射光 + 自发光
其中:
- 环境光 = 材质环境反射系数 × 环境光颜色
- 漫反射光 = 材质漫反射系数 × 光源颜色 × max(N·L, 0)
- 镜面反射光 = 材质镜面反射系数 × 光源颜色 × max(R·V, 0)^shininess
- 自发光 = 材质发光系数
这里:
- N 是表面法线
- L 是从表面点到光源的单位向量
- R 是反射向量
- V 是从表面点到观察者的单位向量
实现这个基本光照模型的HLSL代码可能如下:
hlsl
// 基础光照计算函数
float4 CalculateBasicLighting(
float3 normal, // 表面法线
float3 toEye, // 指向视点的向量
float3 lightDir, // 指向光源的向量
float4 lightColor, // 光源颜色
Material mat) // 材质属性
{
// 确保所有向量都已归一化
normal = normalize(normal);
toEye = normalize(toEye);
lightDir = normalize(lightDir);
// 环境光分量
float4 ambient = mat.Ambient * lightColor;
// 漫反射分量 - Lambert余弦定律
float diffuseFactor = dot(normal, lightDir);
diffuseFactor = max(diffuseFactor, 0.0f);
float4 diffuse = diffuseFactor * mat.Diffuse * lightColor;
// 镜面反射分量 - Phong模型
float4 specular = float4(0, 0, 0, 0);
if(diffuseFactor > 0.0f)
{
// 计算反射向量
float3 reflection = reflect(-lightDir, normal);
// 计算镜面反射系数
float specFactor = pow(max(dot(reflection, toEye), 0.0f), mat.Specular.w);
specular = specFactor * float4(mat.Specular.xyz, 1.0f) * lightColor;
}
// 合并所有光照分量
float4 litColor = ambient + diffuse + specular + mat.Emissive;
// 保留原有的alpha值
litColor.a = mat.Diffuse.a;
return litColor;
}
8.2 法向量
法向量(Normal Vector)是垂直于表面的单位向量,它在光照计算中至关重要,因为表面对光的反应很大程度上依赖于表面相对于光源的朝向。
8.2.1 计算法向量
平面法向量
对于平面或多边形,法向量可以通过计算表面上两个向量的叉积得到:
cpp
// 计算三角形法向量
XMVECTOR CalculateTriangleNormal(
XMVECTOR v0, // 三角形第一个顶点
XMVECTOR v1, // 三角形第二个顶点
XMVECTOR v2) // 三角形第三个顶点
{
// 计算三角形的两条边向量
XMVECTOR edge1 = XMVectorSubtract(v1, v0);
XMVECTOR edge2 = XMVectorSubtract(v2, v0);
// 计算叉积得到法向量
XMVECTOR normal = XMVector3Cross(edge1, edge2);
// 归一化法向量
return XMVector3Normalize(normal);
}
顶点法向量
通常,每个顶点的法向量是通过对顶点所属所有三角形的法向量取平均得到的:
cpp
// 计算顶点法向量
void CalculateVertexNormals(
std::vector<Vertex>& vertices, // 顶点数组
const std::vector<uint32_t>& indices) // 索引数组
{
// 重置所有法向量
for(auto& v : vertices)
v.Normal = XMFLOAT3(0, 0, 0);
// 计算每个三角形对顶点法向量的贡献
for(size_t i = 0; i < indices.size(); i += 3)
{
uint32_t i0 = indices[i];
uint32_t i1 = indices[i+1];
uint32_t i2 = indices[i+2];
XMVECTOR v0 = XMLoadFloat3(&vertices[i0].Position);
XMVECTOR v1 = XMLoadFloat3(&vertices[i1].Position);
XMVECTOR v2 = XMLoadFloat3(&vertices[i2].Position);
// 计算三角形法向量
XMVECTOR normal = CalculateTriangleNormal(v0, v1, v2);
// 将法向量添加到三角形的三个顶点
XMFLOAT3 n;
XMStoreFloat3(&n, normal);
vertices[i0].Normal.x += n.x;
vertices[i0].Normal.y += n.y;
vertices[i0].Normal.z += n.z;
vertices[i1].Normal.x += n.x;
vertices[i1].Normal.y += n.y;
vertices[i1].Normal.z += n.z;
vertices[i2].Normal.x += n.x;
vertices[i2].Normal.y += n.y;
vertices[i2].Normal.z += n.z;
}
// 归一化所有顶点法向量
for(auto& v : vertices)
{
XMVECTOR n = XMLoadFloat3(&v.Normal);
XMStoreFloat3(&v.Normal, XMVector3Normalize(n));
}
}
8.2.2 变换法向量
当应用变换(如旋转和缩放)到模型时,法向量也需要变换,但方式与位置不同。法向量必须使用原变换矩阵的逆转置矩阵来变换:
cpp
// 变换法向量
XMVECTOR TransformNormal(
XMVECTOR normal, // 原始法向量
XMMATRIX worldMatrix) // 世界变换矩阵
{
// 计算世界矩阵的逆转置
XMMATRIX normalMatrix = XMMatrixTranspose(XMMatrixInverse(nullptr, worldMatrix));
// 变换法向量(忽略平移部分)
return XMVector3TransformNormal(normal, normalMatrix);
}
这在HLSL中也是一样的:
hlsl
// 在顶点着色器中变换法向量
float3 TransformNormal(float3 normal, matrix worldMatrix)
{
// 由于在HLSL中矩阵是列主序,我们无需显式转置
matrix normalMatrix = transpose(inverse(worldMatrix));
// 变换法向量
return mul(normal, (float3x3)normalMatrix);
}
8.3 参与光照计算的一些关键向量
在光照计算中,有几个关键向量经常用到:
- 法向量(N): 表面法线,垂直于表面的单位向量
- 光线向量(L): 从表面点指向光源的单位向量
- 视线向量(V): 从表面点指向观察者/相机的单位向量
- 反射向量(R): 光线向量相对于法线的反射方向
- 半向量(H): 光线向量和视线向量的中间向量,用于Blinn-Phong模型
hlsl
// 计算常用光照向量
void CalculateLightingVectors(
float3 surfacePos, // 表面点在世界空间中的位置
float3 surfaceNormal,// 表面法向量
float3 cameraPos, // 相机在世界空间中的位置
float3 lightPos, // 光源在世界空间中的位置
out float3 N, // 输出:归一化的法向量
out float3 L, // 输出:指向光源的向量
out float3 V, // 输出:指向相机的向量
out float3 R, // 输出:反射向量
out float3 H) // 输出:半向量
{
// 确保法向量是单位向量
N = normalize(surfaceNormal);
// 计算指向光源的向量
L = normalize(lightPos - surfacePos);
// 计算指向相机的向量
V = normalize(cameraPos - surfacePos);
// 计算反射向量(入射光线相对于法线的反射)
R = reflect(-L, N);
// 计算半向量(用于Blinn-Phong高光计算)
H = normalize(L + V);
}
这些向量的计算和使用是光照模型实现的核心部分。
8.4 朗伯余弦定律
朗伯余弦定律(Lambert's Cosine Law)是最基本的光照模型之一,它描述了漫反射表面的光照强度。根据这一定律,表面接收到的光照强度与光线方向和表面法线之间夹角的余弦成正比。
8.4.1 数学表达
漫反射光强度 = max(dot(N, L), 0)
其中:
- N 是表面的单位法向量
- L 是从表面指向光源的单位向量
- max函数确保当光源在表面背面时不会产生负值光照
8.4.2 HLSL实现
hlsl
// 计算朗伯漫反射
float CalculateLambertianDiffuse(float3 normal, float3 lightDir)
{
// 确保向量已归一化
normal = normalize(normal);
lightDir = normalize(lightDir);
// 计算N·L
float NdotL = dot(normal, lightDir);
// 确保不为负值
return max(NdotL, 0.0f);
}
// 在完整光照计算中使用
float3 CalculateLambertLighting(
float3 normal,
float3 lightDir,
float3 lightColor,
float3 diffuseAlbedo)
{
float diffuseFactor = CalculateLambertianDiffuse(normal, lightDir);
return diffuseFactor * lightColor * diffuseAlbedo;
}
8.4.3 双面渲染考虑
有时我们需要考虑光线从物体背面照射的情况,特别是对于薄表面:
hlsl
// 双面朗伯模型
float3 CalculateDoubleSidedLambert(
float3 normal,
float3 lightDir,
float3 lightColor,
float3 frontDiffuse,
float3 backDiffuse)
{
float NdotL = dot(normal, lightDir);
if(NdotL >= 0)
return NdotL * lightColor * frontDiffuse;
else
return -NdotL * lightColor * backDiffuse;
}
8.5 漫反射光照
漫反射是表面朝各个方向均匀散射光线的现象,是大多数非金属表面的主要光照特性。朗伯模型是最简单的漫反射模型,但还有其他更复杂的漫反射模型可以实现更逼真的效果。
8.5.1 朗伯漫反射(回顾)
前面已经描述了朗伯漫反射模型,它简单高效,但对于某些材质可能不够真实:
漫反射光 = diffuseAlbedo * lightColor * max(dot(N, L), 0)
8.5.2 Oren-Nayar漫反射模型
Oren-Nayar模型是朗伯模型的一个扩展,适用于模拟微观粗糙表面,如皮肤、布料和粘土:
hlsl
// Oren-Nayar漫反射模型
float CalculateOrenNayar(
float3 normal, // 表面法线
float3 lightDir, // 光源方向
float3 viewDir, // 视线方向
float roughness) // 表面粗糙度
{
// 确保所有向量已归一化
normal = normalize(normal);
lightDir = normalize(lightDir);
viewDir = normalize(viewDir);
// 计算基本系数
float NdotL = dot(normal, lightDir);
float NdotV = dot(normal, viewDir);
// 避免背面光照
if(NdotL < 0 || NdotV < 0)
return 0;
// 计算光线与视线在切线平面上的投影间的夹角
float cosViewLight = dot(viewDir, lightDir);
float theta_r = acos(NdotV);
float theta_i = acos(NdotL);
float alpha = max(theta_i, theta_r);
float beta = min(theta_i, theta_r);
// 计算粗糙度相关系数
float roughnessSquared = roughness * roughness;
float A = 1.0 - 0.5 * roughnessSquared / (roughnessSquared + 0.33);
float B = 0.45 * roughnessSquared / (roughnessSquared + 0.09);
// 计算几何项
float C = sin(alpha) * tan(beta);
// 组合计算漫反射系数
float diffuseFactor = NdotL * (A + B * max(0, cosViewLight) * C);
return max(diffuseFactor, 0.0);
}
8.5.3 物理基础的漫反射
在物理基础渲染(PBR)中,漫反射计算更加复杂,考虑了能量守恒和菲涅耳效应:
hlsl
// PBR漫反射部分
float3 CalculatePBRDiffuse(
float3 normal,
float3 lightDir,
float3 viewDir,
float3 diffuseAlbedo,
float roughness,
float metallic,
float3 F0)
{
// 计算基本向量和系数
float3 N = normalize(normal);
float3 L = normalize(lightDir);
float3 V = normalize(viewDir);
float3 H = normalize(L