3.2 场景表示与命中点处理


3.2 场景表示与命中点处理

上一节的求交函数解决"撞得上一件几何",本节回答剩下两问:百万件几何如何组织成可查询的场景(原 3.2 场景表示),撞上之后要记录什么、怎么处理才不留隐患(原 3.3 相交栈与命中点处理,本节吸收合并)。这两件事决定了求交层与上层着色层之间的数据契约质量——契约含糊,后面每章都要还债。

建模网格与相交网格不是一回事

美术软件里存的是建模网格:带语义层级("这是一扇可开启的门")、带细分修改器、带贴图坐标编辑历史。光线追踪器要的是相交网格:扁平的三角形数组加空间索引,只关心"这一毫秒里每片三角形的绝对坐标"。两者之间隔着一道求值流水线:蒙皮、形态目标、位移贴图、细分、实例变换,最后落成相交网格。理解这道工序,你才能解释两个常见现象:为什么改了模型渲染图没变(建模层改了,相交网格没重建);为什么同样模型渲染比预览慢(相交网格的三角形数远超显示代理)。

三角形是相交网格的默认原子,理由在 3.1 已经见过:求交计算紧凑、无分支、重心坐标白送法线与纹理插值。曲线曲面(NURBS、发丝曲线)在工业渲染器里通常被细分或转换成三角形束,只有少量专用渲染器保留解析求交。新兴的隐式表示——符号距离场、神经场——则通过"转网格"或"逐步推进"(sphere tracing)两种方式接入,第 10 章的新兴应用会再遇到它们。

实例化:一份原型,千军万马

场景组织的第二个关键机制是实例化。一片森林有一百万棵树,树只有二十种原型:为每种原型存一份三角形数据(几何原型),每棵具体的树只存一个变换矩阵加原型引用。射线与实例求交的标准流程是:先把射线乘以实例变换的逆矩阵,变到原型的局部空间求交,再把命中结果(命中点、法线)用正变换带回世界空间。注意法线要用逆转置矩阵变换——不均匀缩放下普通变换矩阵会把法线压歪,这是实例化实现里最经典的一处错误。

function intersect_instance(inst, ray): local_ray = transform(inst.inverse_matrix, ray) hit = intersect_prototype(inst.prototype, local_ray) # 原型可复用加速结构 if hit: hit.pos = apply(inst.matrix, hit.pos) hit.normal = normalize(apply(inst.normal_matrix, hit.normal)) hit.inst_id = inst.id # 上层靠它找回材质与动画语义 return hit

实例化的收益远不止省内存:同原型的射线经过逆变换后局部坐标相近,缓存命中率上升;第 4 章的加速结构可以对每个原型只建一棵树,顶层再放一棵实例级的世界树——这个"两层树"思想在动态场景章节(4.3)会以 TLAS 与 BLAS 的名字正式登场,此处先埋种子。

命中记录:一份别写短的契约

求交成功后向上层交付什么?初学者常只存命中点与法线,这在玩具场景里能活,遇到透明体、实例、位移贴图立刻破产。一份合格的命中记录至少包含四组信息:

struct FullHit { float t; // 命中距离(区间语义见下文) float t_exit; // 闭合体的出射距离;表面命中时可省略 vec3 pos; // 世界空间命中点 vec3 normal_geom; // 几何法线:三角形的原始面法线 vec3 normal_shade; // 着色法线:顶点法线按重心权重插值 vec2 uv; // 纹理坐标,按重心权重插值 uint prim_id; // 基元索引:回查顶点、材质、语义 uint inst_id; // 实例索引:实例化场景必带 bool front_face; // 命中的是外表面还是内表面 };

四个最容易欠账的字段逐一说明。面向标志 front_face:射线方向与几何法线的点积符号即可判定。它的作用远超"知道正反面"——折射计算必须知道"从外往里还是从里往外"才能选对折射率比值;双面材质(树叶、布料)要按面向选不同的着色规则。着色法线与几何法线分开存:几何法线是求交层的物理事实,着色法线是顶点法线插值或法线贴图扰动后的艺术表达;两者混用会把阴影射线与折射方向也带偏,产生"视觉对但物理错"的微妙偏差。实例索引:没有它,材质查表在实例化场景里直接失灵。区间字段 t_exit:普通表面命中只有一个 t,但玻璃球、水滴这类闭合体,一条射线会得到"进入 4、穿出 6"两个根——3.1 球体算例里那对根 (4, 6) 正是为此准备的。

图:从射线出发到着色层拿到的完整信息流

图:从射线出发到着色层拿到的完整信息流

实验台:玻璃球一进一出与黑点排障

两个完整小实验。实验一,玻璃球的区间语义。取 3.1 球体算例的命中区间 (4, 6):着色层拿到进点 t 等于 4、面向为"从外往里",按折射率比值 1.0 比 1.5 算折射方向;追踪折射线穿出时命中 t 等于 6、面向"从里往外",折射率比值倒转。若实现偷懒只存一个 t,出射段的折射率方向必然搞反,玻璃球会呈现出"厚度方向颜色错层"的怪相——这是检验区间语义是否正确的一块试金石。变式:把球换成薄壳(双层表面),区间退化成两对贴近的根,恰好演示为什么实时管线常把薄玻璃简化成单层。

实验二,芝麻黑点排障。画面在物体边缘和角落出现密密麻麻的黑点。诊断链按固定顺序走:第一查命中点是否被直接当作新射线的起点——命中点在表面上下抖动,新射线立刻与自身求交,解法是新起点沿法线方向偏移一个小 epsilon,或给 tmin 起步值;第二查阴影射线是否从着色点出发却撞上了自己——同样用偏移解决;第三查法线方向是否一致——若着色用几何法线、阴影偏移用插值法线,偏移方向与表面不垂直,斜面处黑点依旧。三步走完,九成黑点消失。这套排障链值得背下来:它是每个手写渲染器都要经历的成人礼。

场景更新的一笔账:谁动过就重算谁

命中契约讲的是静态瞬间,动态帧还多一件事:知道"谁变了"。细分级别被更换,相交网格与下游加速结构都要重建;实例矩阵被动画驱动,只有实例变换一层受影响;材质参数被用户拖动,几何侧谁也不用动。给每类对象挂上脏标记,让求值流水线只重算变过的环节,是把帧时间从"全量重建"压到"增量维护"的关键。这笔账在第 4 章算 TLAS 与 BLAS 的更新预算时还要再算一遍——底层正是本节"建模、实例、相交三层分离"的思想。

另一个值得点名的细节是纹理坐标的接缝。命中记录里的 uv 若来自贴图集接缝的两侧,插值会在缝隙处把一整块贴图的颜色抹进对方——画面上多出一条诡异色带,根因是几何上共享顶点、贴图上分属两块。处理办法是命中记录额外携带基元索引,着色层按三角形所属贴图块取值,绝不跨块插值。命中契约为什么要留基元索引,这里就是答案之一:它不只是回溯用的书签,还是贴图、顶点色、逐面材质这些"按面记账"数据的钥匙。

本节要点回顾

  • 建模网格与相交网格分离,中间隔一道求值流水线;渲染异常先查相交网格是否同步。
  • 实例化 = 原型数据 + 变换矩阵,法线必须用逆转置矩阵变换;两层树的思想在此埋种。
  • 命中记录契约:距离、区间、双轨法线、纹理坐标、基元与实例索引、面向标志,一个都不能省。
  • 闭合体要记区间对,玻璃一进一出的折射率方向靠面向标志裁决。
  • 黑点排障三连:起点偏移、阴影偏移、法线一致性——每个手写渲染器的成人礼。

到此,"撞得上、撞得对、撞得明白"三件事齐活。但一亿次每秒的求交调用扫不动百万三角形——第 4 章的加速结构,正是为了让求交少干活。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U