第 16 章练习:片元细节法线与光照
对应教程:16 Noise 细节法线与片元光照
起始示例:平面上的程序化细节法线,修改 16-fragment-normals.js 和需要时的 16-fragment-normals.html。
练习 1:从斜率判断法线方向
假设某片元的局部高度斜率为 hx = 0.3、hy = -0.4。模型暂时没有旋转或缩放,光方向为局部 +z。
**目标:**写出未归一化与归一化法线,计算 Lambert 漫反射值;再解释为什么 hx > 0 时法线的 x 分量为负。
**完成标准:**结果的 z 分量为正,漫反射小于平面法线的 1.0。
练习 2:同一画面左右对照
**目标:**新增“左右对照”显示模式:平面左半使用原始法线,右半使用片元细节法线;两边共用光方向、高光指数和材质颜色。
**要求:**不新增网格,也不改变顶点位置;以局部 x 的 0 为分界。可先关闭动画,再转动光方向观察。
**完成标准:**左半受光较均匀,右半有细节高光;两半的外轮廓依然是同一个平面。
练习 3:单独显示漫反射和高光
**目标:**再增加两个调试模式:一个只显示漫反射强度,一个只显示高光强度。
**要求:**保留细节法线与光方向计算;输出灰度,结果截在 0~1。改变高光指数时,漫反射模式不应变化,高光模式的亮区宽度应改变。
**完成标准:**能区分“法线影响漫反射”与“视线参与高光”两个来源;高光指数越大,亮区通常越窄。
<details><summary>提示</summary>
将已有的 diffuse 与 specular 分别转换成 vec3 灰度即可;高光使用 lightView + viewDirection 得到的半角向量。
</details>
<a id=“reference-answers”></a>
第 16 章练习参考解答
对应练习:片元细节法线与光照。
练习 1:从斜率判断法线方向
未归一化的局部法线为 (-hx,-hy,1) = (-0.3,0.4,1),长度为 √(0.09+0.16+1) = √1.25。归一化后为 (-0.3,0.4,1)/√1.25,与 +z 光线的点积为 1/√1.25 ≈ 0.894。表面朝 +x 方向升高时,法线为保持与上坡切线垂直,需要有朝 -x 的分量。
练习 2:同一画面左右对照
在 HTML 模式列表中新增 <option value="4">左右对照</option>。在片元 shader 中,保留原来的 normalView = normalize( vNormalBasisZ ),将 if ( uMode != 0 ) { ... } 整块替换为以下代码:
bool useDetail = uMode == 1 || uMode == 2 ||
( uMode == 4 && vLocalXY.x >= 0.0 );
if ( useDetail ) {
float hx = ( heightAt( vLocalXY + vec2( uEpsilon, 0.0 ) ) -
heightAt( vLocalXY - vec2( uEpsilon, 0.0 ) ) ) /
( 2.0 * uEpsilon );
float hy = ( heightAt( vLocalXY + vec2( 0.0, uEpsilon ) ) -
heightAt( vLocalXY - vec2( 0.0, uEpsilon ) ) ) /
( 2.0 * uEpsilon );
vec3 normalLocal = normalize( vec3( -hx, -hy, 1.0 ) );
normalView = normalize(
vNormalBasisX * normalLocal.x +
vNormalBasisY * normalLocal.y +
vNormalBasisZ * normalLocal.z
);
}
后面的 if ( uMode == 2 ) 法线颜色分支保持不变,uMode == 4 会进入原有光照分支。由于左半没有计算细节法线,它使用 normalize(vNormalBasisZ);右半使用局部高度差分后的法线。vLocalXY.x 是局部坐标,旋转网格时左右分界也跟着网格走。若想看见更清晰的分界线,可按 abs(vLocalXY.x) 生成一条细的颜色辅助线,但它不属于法线计算。
练习 3:单独显示漫反射和高光
在 HTML 模式列表中增加 <option value="5">仅漫反射</option> 与 <option value="6">仅高光</option>。先把练习 2 的 useDetail 条件扩为包含模式 5 和 6:
bool useDetail = uMode == 1 || uMode == 2 || uMode == 5 || uMode == 6 ||
( uMode == 4 && vLocalXY.x >= 0.0 );
在片元 shader 现有的光照分支内,算出 diffuse 和 specular 后,把颜色赋值部分改为:
if ( uMode == 5 ) {
color = vec3( clamp( diffuse, 0.0, 1.0 ) );
} else if ( uMode == 6 ) {
color = vec3( clamp( specular, 0.0, 1.0 ) );
} else {
color = vec3( 0.06, 0.35, 0.46 ) * ( 0.18 + 0.78 * diffuse ) +
vec3( 0.58 ) * specular;
}
漫反射只涉及法线与光方向;uShininess 只进入 specular 的指数,因而不应影响模式 5。模式 6 则依赖法线、光方向、观察方向与高光指数。示例中 specular 在背光时被设为 0,因此灰度高光不会从背面冒出来。