GPU Gems 1 · Chapter 3. Skin in the "Dawn" Demo

以 HDR 环境光、遮蔽、法线细节和轮廓透光近似 Dawn 的皮肤响应,并把计算拆到顶点与片段着色器。

学习目标

  • 能解释 Dawn 皮肤为何不能只用一张颜色贴图和一个点光源,并说出环境光、遮蔽与轮廓项各自解决的问题
  • 能从顶点阶段传递的切线到世界矩阵与视线向量,说明片段阶段如何还原法线并采样环境立方体贴图
  • 能比较真实次表面散射与 Dawn 的实时近似,识别粗糙度、遮蔽项和轮廓光对“塑料感”的不同影响
  • 能修改皮肤着色实验的参数,先预测再验证高光、边缘光和环境遮蔽的变化

为什么 Dawn 的皮肤难以看起来像皮肤

NVIDIA 制作 Dawn 是为了展示 GeForce FX 可编程硬件如何把实时角色推向电影感。人类对脸部表情、皮肤颜色和边缘明暗非常敏感;即使观众不认识“次表面散射”这个词,也能立刻察觉脸像塑料、边缘像剪纸,或阴影硬得不自然。

先预测:一张颜色贴图已经包含脸颊的红色,为什么还需要把光线送进皮肤的近似?关键在于颜色贴图描述的是“本来是什么颜色”,而光照要解释“从哪个方向来、经过哪些组织、在边缘怎样变软”。本章用一套为 Dawn 的模型、镜头距离和硬件预算定制的着色器,把这些感知线索组合起来。

皮肤的光:进入、散射、透出表皮:表面颜色与微细法线真皮:色彩变化与散射的主要近似皮下组织:更宽、更软的透光贡献入射光邻近位置透出实时近似:颜色 + 遮蔽 + 轮廓方向的环境光
皮肤不像单层塑料:光进入表面后会在多层组织中散射,再从附近位置透出。

1. 皮肤响应:从分层组织到实时线索

皮肤由半透明的表皮、真皮和皮下组织组成。光进入表面后可能在组织内部散射,再从附近位置离开;这种现象称为 。把光只看成“同一点入射、同一点反射”的模型,会漏掉手指对着太阳时边缘发红、脸颊透出柔光等线索。

完整的体积输运很昂贵,因此 Dawn 使用了几种可控近似:一张颜色贴图提供基础肤色;环境立方体贴图提供大范围光照;切线空间法线带来毛孔和细皱的微变化;视线接近轮廓时,从角色背后的环境采样一份柔和亮度,再混入基础颜色。

这里的核心不是声称“模拟了所有散射”,而是把观众最敏感的信号拆开。可以把皮肤着色理解成:环境负责整体色调,遮蔽负责哪里变暗,法线负责微表面朝向,轮廓项负责边缘的透光错觉。

2. HDR 环境光如何进入实时着色

传统实时架构常用点光、方向光和聚光灯;这会让白纸和白色光源都被压到相近的亮度,角色像被贴进场景。Dawn 的制作流程使用多张不同曝光的校准照片拼出全景,再生成 环境数据,让每个方向不只存颜色,也保留亮度差异。

从 HDR 环境可以预计算三类方向查表。按表面法线索引,近似余弦加权的半球光;按反射向量索引;另一张高光环境图按视线方向取角色背后的亮区域。

HDR 环境 → 三类方向查表HDR 全景亮度与颜色Diffuse按表面法线取余弦加权环境光Specular按反射方向取粗糙度模糊高光Hilight按视线方向取背后的亮环境优点:大范围亮度;缺点:环境贴图本身不包含 Dawn 的动态遮挡
把真实环境的亮度与颜色先编码进三类立方体贴图,运行时只需按方向查表。

立方体贴图有一个必须主动接受的边界:它对拍摄中心点最准确,角色走动时背景会像无限远。对 Dawn 这种小角色和远景,这个近似是合适的;若场景要展示近处墙面或大幅移动,就需要局部环境、阴影或更完整的空间光照。

3. 遮蔽项:让环境光知道哪里被挡住

环境图描述的是相机拍到的入射光,却不知道 Dawn 的头发、肩膀和身体会遮挡自己的半球。为此,原书用软件工具从每个顶点的可见半球随机发射射线,统计有多少方向撞上其他几何,再把比例压缩成

这个项不必每帧重新计算。Dawn 的遮蔽项保持在角色顶点上;脸部如果有 morph target,则在不同形状的遮蔽项之间做混合。这样得到的是柔和的环境暗部,而不是需要额外实时 pass 的硬阴影。注意它是“预计算的可见性近似”,不是动态阴影的替代品;角色与场景关系大幅变化时,旧项会失效。

4. 法线、轮廓与血液透射项

皮肤表面的细小毛孔和汗毛太小,不能按几何建模。Dawn 使用切线空间法线贴图,并把一些高光强度信息塞进贴图的另一个通道。让同一套细节可以沿模型表面移动,同时不会把网格顶点数量推得过高。

轮廓附近是另一个敏感区域。当视线方向与表面法线接近垂直时,N · V 变小;这时可以从角色背后的环境取光,和基础肤色混合,制造边缘透光的错觉。原书把多个相关量打包为 ,供片段阶段按模型和视觉需要调制。

数学上,若 V 是视线方向,常用的边缘因子可以写成:

rim=(1saturate(NV))2.rim = (1 - \operatorname{saturate}(N\cdot V))^2.

它不是完整的组织散射方程,却能把“越接近轮廓,背光越显眼”的视觉规律变成可调的信号。先把 rim 视作独立因素,再决定要不要叠加 bump 和高光,调参才不会互相遮蔽。

Dawn Skin Lab

先猜:高光变亮会让皮肤更真实吗?

可交互
简化的皮肤响应柔和漫反射 + 边缘透光 + 受控高光

观察:粗糙度只控制高光扩散;遮蔽项降低环境光,轮廓项负责边缘的柔和感。

5. 顶点阶段:把昂贵但可插值的量提前

本章的皮肤方案是一遍着色:不需要再做额外的渲染 pass 或 alpha 混合。顶点着色器首先接收位置、法线、切线、UV、morph target 偏移以及骨骼索引/权重。对 Dawn 的高分辨率网格,顶点阶段先做形状混合,再做索引蒙皮;这些角色变形的详细控制属于下一章,本章只关心它们如何提供最终姿态。

接着顶点阶段把法线和切线变换到世界空间,并构成一个 。它还计算世界空间视线方向、N · V 的轮廓参数和遮蔽/透射辅助量。片段阶段不必再次重做这些昂贵的矩阵变换,只需插值这些输出。

一次皮肤着色:顶点准备,片段合成1CPU 输入法线、切线、UV、骨骼权重a2vConnector2顶点着色器法线空间、眼向量、轮廓项v2fConnector3片段着色器法线贴图 + 立方体贴图nDotV / diffuse4一次输出柔和皮肤与轮廓高光RGBA + hazeWorldTanMatrix + SkinSilhouetteVec把切线空间细节与环境光照接到同一坐标系
关键不是堆更多 pass,而是把昂贵且可插值的量放到顶点阶段,把细节留给片段阶段。

一个缩减后的顶点阶段骨架如下。它强调数据契约,不是完整可编译源码:

struct Varyings {
  float4 clipPosition;
  float2 skinUv;
  float3 worldEyeDirection;
  float3 tangentToWorldX;
  float3 tangentToWorldY;
  float3 tangentToWorldZ;
  float4 skinSilhouette;
};
 
Varyings dawnVertex(Input IN, Uniforms U) {
  float3 objectPosition = applyMorphTargets(IN, U.morphWeights);
  float3 worldPosition = skinPosition(objectPosition, IN.boneIndex, IN.boneWeight, U.bones);
  float3 worldNormal = skinNormal(applyNormalMorphs(IN, U.morphWeights), IN, U.bones);
  float3 worldTangent = orthonormalize(IN.tangent, worldNormal, U.bones);
  float3 worldBinormal = normalize(cross(worldNormal, worldTangent));
  Varyings OUT;
  OUT.worldEyeDirection = normalize(U.cameraPosition - worldPosition);
  OUT.tangentToWorldX = float3(worldTangent.x, worldBinormal.x, worldNormal.x);
  OUT.tangentToWorldY = float3(worldTangent.y, worldBinormal.y, worldNormal.y);
  OUT.tangentToWorldZ = float3(worldTangent.z, worldBinormal.z, worldNormal.z);
  OUT.skinSilhouette = makeSilhouetteTerms(worldNormal, OUT.worldEyeDirection, IN.occlusion);
  OUT.clipPosition = mul(U.projection, mul(U.view, float4(worldPosition, 1)));
  OUT.skinUv = IN.skinUv;
  return OUT;
}

这里的 把数据整理成稳定接口;不要为了追求“全都逐像素”而把本可插值的矩阵和轮廓量全部推迟。

6. 片段阶段:把法线细节与环境光合成

片段阶段先从纹理读取切线空间法线,再用顶点阶段传来的矩阵转换到世界空间。可以按世界法线读取漫反射环境图,按反射向量读取镜面环境图,再用遮蔽项和 rim 调制;最后把高光、背光和基础颜色合成到一遍输出中。

float4 dawnFragment(Varyings IN, Textures T) {
  float4 normalSpec = sample2D(T.normalSideSpec, IN.skinUv);
  float3 tangentNormal = normalize(normalSpec.xyz * 2.0 - 1.0);
  float3 worldNormal = normalize(float3(
    dot(tangentNormal, IN.tangentToWorldX),
    dot(tangentNormal, IN.tangentToWorldY),
    dot(tangentNormal, IN.tangentToWorldZ)));
  float nDotV = saturate(dot(IN.worldEyeDirection, worldNormal));
  float3 skin = sample2D(T.skinColor, IN.skinUv).rgb;
  float3 diffuse = skin * sampleCube(T.diffuseCube, worldNormal);
  float3 specular = normalSpec.w * sampleCube(T.specularCube, reflect(-IN.worldEyeDirection, worldNormal));
  float3 backLight = sampleCube(T.hilightCube, IN.worldEyeDirection).rgb;
  float rim = pow(1.0 - nDotV, 2.0) * IN.skinSilhouette.y;
  float3 result = diffuse * IN.skinSilhouette.x + specular + backLight * rim;
  return float4(result, rim * normalSpec.w);
}

代码中的 normalSpec.w 代表把法线与高光强度共用一张纹理的工程取舍;它减少输入数量,却要求美术和着色器约定好每个通道的职责。则把远处光照压缩成几次方向查表。

7. 为特定角色优化,而不是宣称通用皮肤

原书特别强调 Dawn 的着色器高度针对模型与镜头范围。Dawn 网格足够细,N · V 等量在线性插值后仍能接受;如果换成低模角色、极近的脸部镜头或完全不同的骨骼,就需要重新评估误差。Dawn 的顶点着色器还由规则文件和搜索过程按角色设置生成,目标是只保留当前 fragment shader 真正请求的输入。

这是一条很实用的工程结论:实时效果可以为固定内容“量身定做”。如果产品要支持许多角色与任意视距,再把其中的环境、遮蔽、法线和轮廓思路抽成参数化材质,并为每个质量档位设定预算;不要把一套展示 Demo 的经验直接包装成万能模型。

本章回顾:让皮肤线索各司其职

  • HDR 环境提供大范围亮度与颜色,不再只依赖几个点光源。
  • 遮蔽项压低被角色自身挡住的环境光,形成柔和暗部。
  • 切线空间法线提供微细表面变化,轮廓项近似边缘透光。
  • 顶点阶段准备矩阵、视线和辅助项,片段阶段完成细节采样与合成。
  • Dawn 的方案是针对模型和镜头的实时近似,适用边界必须显式保留。

练习

问题 1 改 Demo 代码:将实验中的遮蔽项固定为 1.0,再将它固定为 0.2。分别预测脸部环境光与轮廓光会怎样变化,并说明这与实时阴影有什么不同。

问题 2 推导题:若世界空间法线 N=(0,0,1),视线方向分别为 V₁=(0,0,1)V₂=(0.98,0,0.2),用 rim=(1-saturate(N·V))² 比较两个点的轮廓因子。

问题 3 场景选型题:一个角色需要在固定的近景脸部镜头中表现柔和边缘光;另一个系统需要支持许多低模角色并允许相机贴近任意部位。分别说明 Dawn 方案哪些部分可以直接采用,哪些部分必须重新设计。

名词解释

本章出现的专业名词,用大白话再讲一遍。

次表面散射
光进入皮肤后在组织内部散开,再从附近位置透出的现象;它让边缘不至于像硬塑料。
高动态范围
同时保存很亮和很暗的环境亮度,避免所有白色区域都挤成同一个亮度。
漫反射立方体贴图
按表面法线查找预先平均好的环境光,提供柔和的整体亮度。
镜面立方体贴图
按反射方向查找环境高光,并用模糊程度近似材质粗糙度。
遮蔽项
记录周围几何挡住多少环境光的预计算比例,用来压低不该太亮的区域。
切线空间法线
相对于表面切线基记录的微小朝向,能在不增加几何的情况下做细节光照。
血液透射项
把遮蔽、视线夹角和法线朝向等辅助量打包给轮廓与背光近似使用的向量。
切线到世界矩阵
把法线贴图里的局部方向旋转到世界空间的三乘三基。
顶点着色器
逐顶点运行,负责位置变换和准备可供后续片段插值的向量。
片段着色器
逐可见片段运行,读取纹理和环境光并输出最终颜色。
环境立方体采样
用一个方向去查立方体纹理,取得该方向的远处环境颜色和亮度。

资料与写作方式声明

本章以GPU Gems 1 · Chapter 3. Skin in the "Dawn" Demo公开完整正文核定章节范围、事实坐标与时代语境,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…