回顾上一篇 ShaderToy入门教程(2) - 光照和相机
这篇涵盖以下黑体所示内容
- 符号距离函数
- Ray-marching算法
- 曲面法线和光照
- 相机变换
- 构造实体形状(CSG)
- 模型变换
- 平移和旋转
- 比例缩放
- 非均匀缩放
- 结论
- 参考
构造实体形状(CSG)
构造实体形状(简称CSG)是一种通过布尔运算从简单几何形状创建复杂几何形状的方法。 WikiPedia的这张图表显示了该技术的可能性:
CSG建立在3个原始操作上:交集,合并和差异
事实证明,当组合表示为SDF的两个表面时,这些操作都是简单的。
float intersectSDF(float distA, float distB) {
return max(distA, distB);
}
float unionSDF(float distA, float distB) {
return min(distA, distB);
}
float differenceSDF(float distA, float distB) {
return max(distA, -distB);
}
如果您设置这样的场景:
float sceneSDF(vec3 samplePoint) {
float sphereDist = sphereSDF(samplePoint / 1.2) * 1.2;
float cubeDist = cubeSDF(samplePoint) * 1.2;
return intersectSDF(cubeDist, sphereDist);
}
然后你得到这样的东西(参见下面关于缩放的部分,看看1.2的除法和乘法的缘由)。
代码在这里
在同一个Shadertoy中,如果编辑代码,也可以使用联合和差异操作。
考虑由这些二进制操作产生的SDF来尝试建立它们工作原理的直觉是很有趣的。
记住SDF为负的区域代表在面的内部,上面图中相交,sceneSDF只有在cube§和sphere§都是负的时候才是负的。就是说一个点必须都在立方体和球体内部,才会在场景面的内部,这符合CSG对交集的定义。
同样的逻辑也适合于并集,如果一点只要在两者之一的SDF为负,场景SDF则为负,那么就在面的内部
差异操作对我来说是最棘手的
SDF为负值意味着什么?
如果你再想一想SDF的负面和正面区域是什么意思,你可以看到SDF的负值是表面内外的反转。 表面内的部分被认为现在都被视为外部,反之亦然。
这意味着您可以将差异视为第一个SDF和第二个SDF的反转的交集。 因此,当第一个SDF为负且第二个SDF为正时,得到的场景SDF仅为负。
切换回几何术语,这意味着当且仅当我们在第一个表面内和第二个表面之外时,我们才在场景表面内 - 正好是CSG差异的定义!
模型变换
能够移动相机给我们一些灵活性,但能够独立地移动场景的各个部分肯定会提供更多灵活性。 让我们来探索一下如何做到这一点。
SDF的旋转和平移
建模为SDF的曲面的变换或旋转,可以在评估SDF之前对点进行逆变换实现。
正如您可以对不同的网格对象应用不同的变换一样,您可以将不同的变换应用于SDF的不同部分 - 只需将变换后的视线发送到您感兴趣的SDF部分。例如,使立方体浮出在水面上下浮动,将球体留在原地,但仍然取交集,你可以这样做:
float sceneSDF(vec3 samplePoint) {
float sphereDist = sphereSDF(samplePoint / 1.2) * 1.2;
float cubeDist = cubeSDF(samplePoint + vec3(0.0, sin(iGlobalTime), 0.0));
return intersectSDF(cubeDist, sphereDist);
}
代码在这里
如果你进行这样的变换,结果函数仍然是一个带符号的距离场吗? 对于旋转和平移,它是,因为它们是“刚体变换”,意味着它们保持点之间的距离。
更一般地说,您可以通过将采样点乘以变换矩阵的倒数来进行任何刚体变换。
例如,你可以使用旋转矩阵进行变换,可以这样做:
mat4 rotateY(float theta) {
float c = cos(theta);
float s = sin(theta);
return mat4(
vec4(c, 0, s, 0),
vec4(0, 1, 0, 0),
vec4(-s, 0, c, 0),
vec4(0, 0, 0, 1)
);
}
float sceneSDF(vec3 samplePoint) {
float sphereDist = sphereSDF(samplePoint / 1.2) * 1.2;
vec3 cubePoint = (invert(rotateY(iGlobalTime)) * vec4(samplePoint, 1.0)).xyz;
float cubeDist = cubeSDF(cubePoint);
return intersectSDF(cubeDist, sphereDist);
}
但是如果你在这里使用WebGL,那么现在GLSL中没有内置的矩阵反转例程,但你可以做相反的转换。 所以上面的场景功能改为等效:
float sceneSDF(vec3 samplePoint) {
float sphereDist = sphereSDF(samplePoint / 1.2) * 1.2;
vec3 cubePoint = (rotateY(-iGlobalTime) * vec4(samplePoint, 1.0)).xyz;
float cubeDist = cubeSDF(cubePoint);
return intersectSDF(cubeDist, sphereDist);
}
有关更多转换矩阵,请参阅图形教科书的任何介绍,或查看这些幻灯片:3D仿射变换。
比例缩放
好吧,让我们回到之前我们掩盖的这个奇怪的缩放技巧:
float sphereDist = sphereSDF(samplePoint / 1.2) * 1.2;
1.2的除法是将球体缩放1.2倍(请记住,在将其发送到SDF之前,我们将逆变换应用于该点)。 但是为什么我们之后乘以该比例因子呢? 为简单起见,让我们检查一下2倍的情况。
float sphereDist = sphereSDF(samplePoint / 2) * 2;
缩放不是刚体变换 - 它不保持点之间的距离。 如果我们通过将它们除以2来转换(0,0,1)和(0,0,2)(这导致模型的均匀放大),那么 点之间的距离从1切换到0.5。
因此,当我们在sphereSDF中对缩放点进行采样时,我们最终会从该变换球体的表面返回该点距离的一半。 最后的乘法是为了补偿这种失真。
有趣的是,如果我们在着色器中尝试这一点,并且不使用比例校正,或者使用较小的比例校正值,则会呈现完全相同的事物。 为什么?
// All of the following result in an equivalent image
float sphereDist = sphereSDF(samplePoint / 2) * 2;
float sphereDist = sphereSDF(samplePoint / 2);
float sphereDist = sphereSDF(samplePoint / 2) * 0.5;
请注意,无论我们如何缩放SDF,返回距离的符号都保持不变。 “签名距离场”的标志部分仍在工作,但距离部分现在正在撒谎。
要了解为什么这是一个问题,我们需要重新检查光线行进算法的工作原理。
回想一下,在光线行进算法的每一步,我们都想沿着视线射线移动一个距离等于到地面的最短距离。 我们使用SDF预测最短距离。 为了使算法更快,我们希望这些步骤尽可能大,但是如果我们下冲,算法仍然有效,它只需要更多的迭代。
但如果我们高估距离,我们就会遇到一个真正的问题。 如果我们尝试缩小模型而不进行更正,如下所示:
float sphereDist = sphereSDF(samplePoint / 0.5);
然后球体完全消失。 如果我们高估距离,我们的光线追踪算法可能会超越表面,从未找到它。
对于任何SDF,我们可以安全地统一它,如下所示:
float dist = someSDF(samplePoint / scalingFactor) * scalingFactor;
非均匀缩放
如果我们想要非均匀地缩放模型,我们如何安全地避免上面缩放部分中描述的距离过高估计问题? 与均匀缩放不同,我们无法准确地补偿由变换引起的距离失真。 由于所有尺寸均匀缩放,因此可以在均匀缩放中进行缩放,因此无论表面上与采样点的最近点在何处,缩放补偿都是相同的。
但是对于非均匀缩放,我们需要知道表面上最近的点在哪里知道校正距离的程度。
要了解为什么会出现这种情况,请考虑单位球体的SDF,沿x轴缩放到其大小的一半,并保留其他尺寸。
如果我们在(0,2,0)处计算SDF,我们会回到1个单位的距离。 这是正确的:球体表面上的最近点是(0,1,0)。但如果在(2,0,0)进行计算,我们会回到3个单位的距离,这是不对的。 表面上的最近点是(0.5,0,0),产生1.5个单位的世界坐标距离。
因此,正如在均匀缩放中一样,我们需要校正SDF返回的距离,以避免过高估计距离,但需要多少? 高估因子取决于点的位置和表面的位置。
由于通常可以低估距离,我们可以乘以最小的比例因子,如下所示:
float dist = someSDF(samplePoint / vec3(s_x, s_y, s_z)) * min(s_x, min(s_y, s_z));
其他非刚性变换的原理是相同的:只要符号通过变换保留,您只需要找出一些补偿因子,以确保您永远不会过高估计到曲面的距离。
结论
通过学习这篇文章中的内容,您现在可以创建一些非常有趣,复杂的场景。 将这些与使用法线向量作为材质的环境/漫反射组件的简单技巧相结合,您可以创建在帖子的开头着色器的内容。完整代码
参考
有关渲染有符号距离函数的方法还有很多。 Inigo Quilez是这个主题最富有成效的作家之一。 我通过阅读他的网站和着色器代码了解了这篇文章的大部分内容。 他也是Shadertoy的共同创作者之一。
一些有趣的SDF相关材料来自他的网站,我根本没有介绍,包括smooth blending between surfaces and soft shadows
其他参考: