ShaderToy入门教程(3) - CSG

回顾上一篇 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轴缩放到其大小的一半,并保留其他尺寸。

s p h e r e S D F ( x , y , z ) = ( 2 x ) 2 + y 2 + z 2 1 sphereSDF(x,y,z)=\sqrt {(2x)^2+y^2+z^2}-1

如果我们在(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

其他参考:

发布了36 篇原创文章 · 获赞 42 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_28710515/article/details/89530315
CSG
今日推荐