第 14 章练习:多层 Noise 与常见效果
对应教程:14 多层 Noise 与常见效果
起始示例:多层对比、常见效果;分别修改 对比源码或效果源码。
练习 1:手算归一化的三层 fBm
三层 valueNoise 样本依次为 0.2、0.8、0.4,gain = 0.5,频率倍率为 2.0。
**目标:**算出三层的权重、未归一化的总和、权重总和与归一化后的值。再说明频率倍率为什么不直接出现在这次加权计算里。
**完成标准:**能算出最终值并说明“输入坐标频率”与“输出振幅权重”的职责不同。
练习 2:比较湍流与脊线
**目标:**对单层样本 n = 0.2、0.5、0.8 分别计算 abs(2n-1) 和 1-abs(2n-1)。然后在多层对比示例切换湍流与脊线模式,观察其亮区落在哪些等值区域。
**要求:**保持层数、尺度、振幅倍率相同;可打开阈值遮罩辅助观察。
**完成标准:**能解释两者结果为何互补,以及“每层先变换再累加”和“累加后只变换一次”为何不等价。
练习 3:让石纹方向可调
**目标:**在常见效果示例增加石纹角度滑块,范围 -180°~180°。角度为 0 时与原效果一致,改变角度时整组纹路跟着转。
**要求:**继续使用域扭曲;在生成正弦条纹的坐标中使用第 9 章的二维旋转矩阵。不要旋转 Three.js 网格,也不要把度数直接送入 sin / cos。
**完成标准:**石纹始终填满原平面;角度约为 90° 时,原本主要竖向的纹路变为主要横向;旋转中心保持平面中心。
<details><summary>提示</summary>
石纹是按“查询坐标”的 x 分量生成的。要让画面看起来转过正角度,反向旋转查询坐标。
</details>
练习 4:限制看不见的高频层
**目标:**在多层对比示例中,利用片元导数 fwidth 估计每像素跨过多少 noise 格点;当某层频率过高时,逐渐减少该层权重。
**要求:**把像素跨度计算放在循环之前;保留现有的振幅归一化,并把“可见权重”也计入归一化。比较基础格数 8、层数 6、频率倍率 3.0 时的新旧画面。
**完成标准:**远超像素采样能力的细节变少;放大画面或降低基础格数后,较细层次仍可出现。能说明这种近似过滤减少锯齿,但不会自动减少 noise 函数的调用次数。
<a id=“reference-answers”></a>
第 14 章练习参考解答
对应练习:多层 Noise 与常见效果。
练习 1:手算归一化的三层 fBm
权重依次为 1.0、0.5、0.25。未归一化总和为 1.0×0.2 + 0.5×0.8 + 0.25×0.4 = 0.7;权重总和为 1.75;结果为 0.7 / 1.75 = 0.4。频率倍率 2.0 决定三次样本分别在 p、2p、4p 处求值;题目已经给出这三个求值结果,所以无需再把频率倍率乘进输出权重。
练习 2:比较湍流与脊线
原样本 n |
湍流 abs(2n-1) |
脊线 1-abs(2n-1) |
|---|---|---|
0.2 |
0.6 |
0.4 |
0.5 |
0.0 |
1.0 |
0.8 |
0.6 |
0.4 |
两者对同一个单层样本相加为 1。湍流在原样本远离 0.5 时亮,脊线在原样本接近 0.5 时亮。若对相同的每层样本使用相同正权重并归一化,最终的湍流与脊线也互补。
“每层先折叠”与“合成后折叠”一般不同。例如两个等权样本为 0.2 和 0.8:先做湍流再平均得 0.6;先平均得 0.5 再做湍流得 0.0。后者丢掉了两层各自的强弱结构。
练习 3:让石纹方向可调
在 HTML 的石纹控件附近增加 id="angle" 的 range,min="-180" max="180" step="1" value="0"。在 JS 的 uniforms 中加入 uAngle: { value: 0.0 },片元 shader 中声明 uniform float uAngle;,并在 main() 前增加:
mat2 rotate2D( float angle ) {
float c = cos( angle );
float s = sin( angle );
return mat2( c, s, -s, c );
}
在 main() 的非水纹分支中,把原有的 vec2 warpedP = warpedPoint( p ); 改为:
vec2 patternP = uMode == 1 ? rotate2D( -uAngle ) * p : p;
vec2 warpedP = warpedPoint( patternP );
后面的 float h = fbm( warpedP ); 和石纹分支中的 bands 计算保持原样。
在 updateControls() 中读取滑块并转换为弧度:
const degrees = Number( document.querySelector( '#angle' ).value );
material.uniforms.uAngle.value = THREE.MathUtils.degToRad( degrees );
p 原本以平面中心为原点,且已做比例修正,所以不用再次平移或修正比例。这里先反向旋转查询坐标,再计算域扭曲与 fBm;条纹及其弯曲细节一起转动,而网格本身不变。
练习 4:限制看不见的高频层
在 14-layered-noise.js 的片元 shader 中,用下列版本替换 layeredNoise。fwidth 只在片元阶段使用,当前示例由 Three.js r174 的 WebGL 渲染器编译运行:
float layeredNoise( vec2 p, int kind ) {
vec2 footprint = fwidth( p );
float pixelSpan = max( footprint.x, footprint.y );
float sum = 0.0;
float weight = 0.0;
float amplitude = 1.0;
float frequency = 1.0;
for ( int i = 0; i < 6; i++ ) {
if ( i < uOctaves ) {
float n = valueNoise( p );
if ( kind == 2 ) n = abs( 2.0 * n - 1.0 );
if ( kind == 3 ) n = 1.0 - abs( 2.0 * n - 1.0 );
float visible = 1.0 - smoothstep( 0.25, 0.5,
pixelSpan * frequency );
float effectiveWeight = amplitude * visible;
sum += effectiveWeight * n;
weight += effectiveWeight;
p *= uLacunarity;
frequency *= uLacunarity;
amplitude *= uGain;
}
}
return sum / max( weight, 0.0001 );
}
每像素跨过的基础 noise 坐标约为 pixelSpan;第 i 层再乘上频率倍率。跨度接近半个格子/像素时开始压低该层权重。visible 也进入分母,避免过滤高频层时整体变暗。这个阈值是教学用近似,严格采样还取决于底层 noise 的频谱、像素覆盖与投影。代码仍调用所有层的 valueNoise,所以主要改善画面,不保证提高帧率;若要减少计算,可根据像素跨度分支跳过高频调用并实测 GPU 行为。