volsample RayMarchCore.cginc 精读:结构化光线步进的起点偏移与步长计算
【免费下载链接】volsampleStructured Volume Sampling - sample placement algorithm for real-time volume rendering with low aliasing, for camera-in-volume case.项目地址: https://gitcode.com/gh_mirrors/vo/volsample
volsample 是一个开源的结构化体积采样(Structured Volume Sampling)项目,专门解决"相机位于体积内部"时实时体渲染的严重混叠问题。它的核心思想写在 RayMarchCore.cginc 里:不再让采样层跟着相机跑,而是把采样点钉在世界空间的一组平行平面上。本文带你精读这份文件,重点拆解结构化光线步进(structured raymarching)中最关键的两个参数——起点偏移 t_0 与步长 dt——是如何一步步计算出来的,读完你就能自己读懂并复用这套思路。🎯
为什么要做结构化采样?先看混叠从哪来
实时体渲染里采样次数很少(例如云场景只有 32 次,见 SceneClouds.cginc),当相机在体积内部移动时,采样点如何放置直接决定画面是否稳定。
RayMarchCore.cginc 里实现了三种方案,正好构成一条演进脉络:
| 采样方案 | 采样层如何放置 | 相机平移时 | 相机旋转时 | 对应函数 |
|---|---|---|---|---|
| 固定 Z 采样 | 垂直于视线、跟随相机 | 采样层横扫体积,混叠严重 😵 | 相对稳定 | RayMarchFixedZ |
| 固定 Z 固定采样 | 只补偿前向平移 | 前向移动稳定 | 视锥横扫侧面采样层 | RayMarchFixedZPinned |
| 结构化采样 | 世界空间固定平面族 | 采样只在平面内滑动,几乎无混叠 ✅ | 多射线混合补偿 | RaymarchStructured |
结构化采样的妙处在于:相机平移时,采样点只在自己所在的平面内"平行滑动",而平面本身在空间中是密集采样的,滑动不会引入可见的走样。这也是标题里"起点偏移与步长计算"的用武之地。
IntersectPlanes 精读:两个公式搞定采样布局
结构化采样把体积剖成一组间距固定的世界空间平面族(间距为SAMPLE_PERIOD),平面法线 n 来自十二面体五边形面的朝向。光线ro + t * rd与这些平面的交点,就是采样点。计算这些交点,只需要 IntersectPlanes 这个十几行的函数:
void IntersectPlanes( in float3 n, in float3 ro, in float3 rd, out float t_0, out float dt ) { float ndotrd = dot( rd, n ); dt = SAMPLE_PERIOD / abs( ndotrd ); // 步长 t_0 = -fmod( dot( ro, n ), SAMPLE_PERIOD ) / ndotrd; // 起点偏移 if( t_0 < 0. ) t_0 += dt; // 保证首样本在视线前方 }步长 dt:入射角的自动补偿 📐
第一个公式dt = SAMPLE_PERIOD / |dot(rd, n)|。光线垂直射向平面族时,ndotrd = ±1,步长正好等于SAMPLE_PERIOD;当视线倾斜(掠射)时,|ndotrd| < 1,dt 自动变大。
这是因为光线参数 t 是"沿光线的距离",而平面族是"世界空间等距"的,两者之间隔着夹角。除以|ndotrd|后,无论视线从哪个方向穿过体积,采样点在世界空间都严格等间距——这就是"结构化"三个字的含义:采样密度由世界空间决定,与视角无关。
起点偏移 t_0:把第一条射线对齐到分层 🎯
第二个公式t_0 = -fmod(dot(ro, n), SAMPLE_PERIOD) / ndotrd稍显晦涩,拆开看就清楚了:
dot(ro, n)是相机到"过原点参考平面"的带符号距离;fmod(..., SAMPLE_PERIOD)取余数,得到相机与上一片采样平面之间的"残余距离";- 再除以
ndotrd,把这段空间距离换算成光线参数 t; - 负号把它翻转为"到下一片平面的参数距离"。
最后一行if(t_0 < 0.) t_0 += dt处理了一个隐蔽的坑:GPU 的fmod对负数参数行为不一,这行代码统一了行为,并保证第一个采样点永远落在视点前方,不会出现"背后采样"的鬼影。
RaymarchStructured:把三条光线缝合成一帧
单个平面法线只在部分像素上成立。倒角十二面体把屏幕分成三种区域:五边形面(1 条射线)、倒角棱(2 条射线混合)、倒角顶点(3 条射线混合),分别对应 VolumeRender.shader 的三个 Pass。真正的整合在 RaymarchStructured 中完成:
- 首样本淡入淡出:
firstWt = t / dt给第一片采样加权。注释里说得很直白——不做这步,会在画面里看到恼人的五边形伪影; - 隐形墙:
zbuf = min(depth, dt * SAMPLE_COUNT)在体积尽头假设一堵不可见墙,让采样在远处平滑淡出; - 权重混合:最终结果
sum = wt.x*sum0 + wt.y*sum1 + wt.z*sum2,三个权重恰好对应十二面体网格顶点色里的 RGB,见 PlatonicSolidBlend.cs。
数据从哪来:一整套闭环的调用链
法线 n 与权重 wt 不是硬编码在着色器里的,而是由 PlatonicSolidBlend.cs 实时构造的倒角十二面体网格携带:法线编码进三组 UV,权重写进顶点色;VolumeRender.shader 的顶点着色器再用 Spheremap Transform 解码还原。整个流程由 StructuredVolumeSampling.cs 的OnRenderImage驱动,三 Pass 依次绘制网格的三个子网格。想验证每一步,可以打开 sponza.unity 或 clouds.unity 场景,在 Inspector 里调PlatonicSolidBlend的 bevel 值,或切换 VolumeRender.shader 里的DEBUG_BEVEL、Z_PLANES_ONLY宏观察分层效果。
小结
回到开头的问题:为什么结构化采样能消除体渲染混叠?答案就藏在这两个公式里——dt保证世界空间采样间距恒定,t_0保证每条光线都从正确的分层起点出发。这两行代码,加上十二面体提供的多方向平面族和权重混合,构成了 volsample 全部魔法的基础。如果你正在做实时体积渲染、体积云或体雾,不妨从 RayMarchCore.cginc 开始读起,它不到 220 行,却是理解体渲染抗锯齿的一条捷径。💡
【免费下载链接】volsampleStructured Volume Sampling - sample placement algorithm for real-time volume rendering with low aliasing, for camera-in-volume case.项目地址: https://gitcode.com/gh_mirrors/vo/volsample
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考