第 17 章练习:Noise 的设备差异与性能
对应教程:17 Noise 的跨设备表现与性能
起始示例:精度与高频采样对照,需要修改时使用示例源码。
练习 1:估算哪层会失去细节
**目标:**假设基础 noise 在半幅画面高度上有 8 格,半幅高度恰好是 200 个实际绘制像素;每层频率翻倍,最多八层。按示例的 visibility = 1.0 - smoothstep(0.35, 1.0, cellsPerPixel),列出第 0~6 层的每像素格数,标记完全保留、渐隐、完全跳过的层。
**要求:**再假设半幅高度变为 100 个实际像素,其余条件不变,判断最先渐隐和最先完全跳过的层怎样变化。暂时忽略横向足迹更大的情况。
**完成标准:**能够根据 8 × 2ⁱ / 像素高度 推出分类,解释为什么降低分辨率后可见的最高频层减少。
练习 2:让过滤更保守
**目标:**在示例的高频采样视图中,让右半幅更早地淡出高频层,同时保持左半幅和 noise 函数原样。改变频率、层数和渲染分辨率,观察差异。
**要求:**只调整右侧 visibility 的过渡区间;保持 sum / weight 归一化及全部层跳过时返回 0.5。对比时固定相同的参数和窗口大小,再开启动画。
**完成标准:**右半幅比原版更平滑、细节也更少;能说明这种淡出为什么仍不等于真正的像素面积平均。
练习 3:晚减原点能否找回精度
**目标:**新增第三个对照视图。左侧仍画 fract(p),右侧画 fract((p + K) - K),其中 K 使用现有的整数坐标偏移控件。比较 K = 0、65,536 和 16,777,216。
**要求:**保持 p 的计算不变,只在第三视图改变右侧取小数部分的表达式;通过颜色判断“先加上大数,再减掉大数”能否恢复已经丢失的局部变化。
**完成标准:**大偏移时右侧无法稳定恢复左侧的连续图案,并能提出一种让传入 shader 的坐标始终较小的流程。
练习 4:估算片元法线的工作量
**目标:**按第 16 章的四次邻近高度查询,每次高度用八层 value noise、每层四个 hash,算出每片元的 hash 查询上限;再比较 1920 × 1080 与 960 × 540 两种绘制缓冲区的总次数数量级。
**要求:**先不计编译器优化、缓存、遮挡和额外中心高度查询;说明这个数字为何不能直接换算为帧耗时。
**完成标准:**算出单片元及两种分辨率的结果,并指出如果画面仍闪烁,单纯降低分辨率可能让高频采样问题更严重。
<a id=“reference-answers”></a>
第 17 章练习参考解答
对应练习:Noise 的设备差异与性能。
练习 1:估算哪层会失去细节
cellsPerPixel = 8 × 2ⁱ / 200 = 0.04 × 2ⁱ:
层号 i |
每像素格数 | 原过滤区间下的状态 |
|---|---|---|
| 0 | 0.04 | 完全保留 |
| 1 | 0.08 | 完全保留 |
| 2 | 0.16 | 完全保留 |
| 3 | 0.32 | 完全保留 |
| 4 | 0.64 | 渐隐 |
| 5 | 1.28 | 完全跳过 |
| 6 | 2.56 | 完全跳过 |
半幅高度降到 100 个实际像素后,基础层的每像素格数变成 0.08。第 3 层达到 0.64,开始渐隐;第 4 层达到 1.28,已经跳过。这里把纵向足迹当作示例 max(length(dFdx(p)), length(dFdy(p))) 的主导项;若实际横向足迹更大,渐隐会更早。
练习 2:让过滤更保守
在 fbm 中只把右侧使用的淡出范围改为:
// 片元阶段;footprint 和 frequency 的单位与原示例相同。
float visibility = filtered ?
1.0 - smoothstep( 0.20, 0.65, footprint * frequency ) : 1.0;
保留 if (visibility > 0.0)、加权累加、sum / weight 和 weight == 0.0 时的中性值。左半幅的 filtered 为 false,始终得到 visibility = 1.0。第 1 题中 0.64 格/像素的那层现在几乎完全淡出;右侧通常更稳、更软,也失去一些本可辨认的细节。这个修改只减少或衰减整层采样,并没有计算一个像素覆盖范围内 noise 的平均值,所以不能承诺完全无混叠。
练习 3:晚减原点能否找回精度
在 HTML 的 #view 下拉框加 <option value="2">晚减原点诊断</option>。将片元 shader 从 if ( uView == 1 ) 开始的颜色分支替换为:
// 片元阶段;p 是局部 noise 坐标,uOffset 是大整数偏移。
if ( uView == 2 ) {
vec2 shifted = p + vec2( uOffset );
vec2 lateRebased = shifted - vec2( uOffset );
vec2 local = fract( right ? lateRebased : p );
color = vec3( local, 0.18 );
} else if ( uView == 1 ) {
vec2 local = fract( p + ( right ? vec2( uOffset ) : vec2( 0.0 ) ) );
color = vec3( local, 0.18 );
} else {
float level = fbm( p, footprint, right );
color = mix( vec3( 0.025, 0.055, 0.10 ),
vec3( 0.98, 0.72, 0.35 ),
smoothstep( 0.28, 0.72, level ) );
}
将 JavaScript 的右侧标签和说明增加 view === 2 的情况,例如:
document.querySelector( '#left-label' ).textContent = view === 0 ? '未过滤 fBm' : 'fract(p)';
document.querySelector( '#right-label' ).textContent = view === 0 ?
'按像素足迹淡出高频层' :
view === 1 ? 'fract(p + 整数偏移)' : 'fract((p + K) - K)';
document.querySelector( '#explanation' ).textContent = view === 2 ?
'先加大坐标再减去,已经舍入的小数不能恢复。' :
view === 0 ?
'左右查询相同坐标,右侧按像素足迹淡出高频层。' :
'整数偏移在数学上不应改变 fract 的结果。';
K = 0 时左右应接近一致。大 K 下,p + K 先把低位舍入掉,后面减去 K 只能得到已经量化的差值。实际应用中可在 JavaScript 的双精度数值范围内,先从世界位置减去附近的块原点,再把小的局部坐标作为 uniform 或顶点数据送给 shader;若需要跨块连续,还要定义块编号、周期或边界衔接方式。不要把已在 GPU 中量化的大坐标相减当作恢复精度的办法。
练习 4:估算片元法线的工作量
四次高度查询 × 八层 × 每层四个 hash,得到 4 × 8 × 4 = 128 次 hash/片元。若整个缓冲区都由该材质覆盖,1920 × 1080 × 128 = 265,420,800 次;960 × 540 × 128 = 66,355,200 次,恰为前者的四分之一。实际示例中的平面没有覆盖整屏,所以这两个值是按全屏覆盖假设的上限数量级。
hash 次数没有包括插值、分支、颜色、内存、其他 draw call,也没有反映 GPU 并行度或编译器优化,因此不能除以一个固定常数得出毫秒。降低分辨率确实减少片元数量,同时让每个像素覆盖更大的 noise 区域;若不减少高频层或做过滤,闪烁与混叠反而可能更明显。