RayMarching的一些实用技巧

Number of views 163

1.平面作切片。我们知道平面的SDF函数如下:

image1740715447076.png

但是该方法会行进很多次才能到达表面,所以通过加上planeHeight会减少行进次数。

float sdfPlane(vec3 p, float y) {
    return vec3(0., p.y-y, 0.)
}

image1740660402092.png如果要让平面与当前raymarching步进的位置P产生关联,如让地面倾斜或翻转一定角度,可让P与地面作点积运算,如:

float sdfPlane(vec3 p, float y) {
    // 这里需要归一化
    return dot(p, normalize(vec3(0., 1.,0.)));
}

此时的结果还是上图中显示的地面。但是如果把 normalize(vec3(0., 1.,0.)改为 normalize(vec3(0., 1.,1.),可以看到平面翻转了。并且平面所形成的表面与法线指向的方向一致

image1740710622480.png这有什么用?我们会用翻转的平面作为切面对其它SDF作切割处理。如,我们在RayMarching 不同SDF的混合 差集中介绍的那样,让平面移动到Cube的指定位置,然后做差集处理:

image1740711257978.png做差集处理后(即 max(-sdfPlane, sdfBox))得到:

image1740711305841.png

image1740711451838.png

2.模型双面开启。首先可以思考下应该怎么做能让SDF模型双面开启?回顾SDF模型内部,与当前行进距离之间的结果是 <0.001(假设等于0.001时为模型表面),以Box为例,我们可以对Box的SDF结果取绝对值,这样Box内部的计算结果也为正数了,那么之前为 -0.001的位置此时也变为了 0.001,这样Box就能开启双面渲染了。还是以Plane与Box交集的方式使得Box模型只显示公共的部分(max(sdfPlane, sdfBox)):

image1740714263927.png

此时对Box的SDF结果取绝对值(max(sdfPlane, abs(sdfBox))),会发现我们把Box的盖子掀开了,可以完全看到内部的形态:

image1740714511780.png

3.调整模型外壳厚度。上图我们已经把模型的盖板掀开了,并看到了模型内部成功开启了双面渲染,但是这盒子的外壳太薄了,一装东西必然就碎。我们需要加厚外壳,怎么做?通过(max(sdfPlane, abs(sdfBox)-.05)。这时候变成了一个质量很好的垃圾桶。可以自己代入一些数值就知道原因了。

image1740715007057.png

4.让平面以正弦波方式行进。为了让平面作为切面的状态有更丰富的形状,我们可以对平面以正弦波的形态行进。通过:

float sdfPlane(vec3 p) {
    return dot(p, vec3(0., 1., 0.))-sin(p.x);
}

image1740728646346.png有些地方会出现摩尔纹,这是因为行进过程步进过度导致:image1740728746132.png

为了让步进恢复正常,可以让进入模型内部的步进回到外部,这可以通过对步进取绝对值。如:

vec2 raymarch(vec3 ro, vec3 rd) {
    float t = 0.;
    float y = -1.;
    for(float i = 0.; i<STEP; i++) {
    	vec2 d = dist(ro + rd*t);
        // 让进入模型内部的步进回到模型外面
        if(abs(d.x) < .001) {
            y = d.y;
        	break;
        }
        if(t > STEP) {
            t = -1.;
            break;
        }
        // 当步进错误进入模型内部时,d.x必然为负,此时会加上负数,直至回到模型外部。
        t += d.x;
    }
    return vec2(t, y);
}

可以看到摩尔纹消失了:

image1740729531365.png然后我们可以继续增加正弦函数的频率,比如在原有的基础上乘以3:

float sdfPlane(vec3 p, float y) {
    return dot(p, normalize(vec3(0., 1.,0.)))-sin(p.x*3.);
}

可以看到输出的图像已不忍直视:

image1740729718608.png造成这个问题的原因是,步进相比上面还更过度,都没有在模型内部,而是跳过了整个模型,这导致了之前的矫正无法进行:

image1740729807051.png

根据上图我们已经知道了该问题的原因是由于步进过长导致,那么为了减少步进长度可以让步进长度缩短为原来的倍数,如每一步缩短为原来的0.5倍,vec2(sdfPlane(p, 1.)*0.5, 1.)问题得到有效缓解,但是该方式增加了行进的次数,会有性能损耗:

image1740730541893.png

5.任意模型的正弦波形式

p.z += sin(p.x*5.+iTime)*0.1;
float testBox = sdBox(p-vec3(0.,2., 0.), vec3(2.,1.,.1));

image1740731691637.png

6.模型的其它形态

  1. 缩放,我们可以对当前位置点p进行缩放操作,以实现SDF的结果最终是放大还是缩小的状态。当p乘以大于1的整数倍数时,实际上SDF模型最后会被缩小:
float scale = 2.;
boxPos *= scale;
float testBox = sdBox(boxPos-vec3(0.,2., 0.), vec3(1.,1.,1.));

原因是因为,相机所在的位置被拉远了。如:我们的相机的起始点最开始是 vec3 ro = vec3(0.,4.,-2.)那么乘以2后ro就变为 vec3 ro = vec3(0.,8.,-4.)。所以就呈现了相反的效果:

image1740735952923.png拉远后,SDF模型显得更小:

image1740735989730.png

2.上小下大或上大下小的模型缩放。看下面的模型,y值为-1~1之间:

image1740740642830.png我们想要让Cube下大上小,并有平滑过渡的感觉,可以这么做:

vec3 boxPos = vec3(p);
float scale = mix(1., 2., smoothstep(-1., 1., boxPos.y));
boxPos.xz *= scale;
vec2 testBox = vec2(sdBox(boxPos, vec3(1.,1.,1.)), 0.);

但是在某个视角下,发现模型会消失:

image1740740950933.png

这是上面说到的模型的SDF过度行进产生的结果。所以我们需要对sdBox的计算结果除以scale。这样即可避免:

vec3 boxPos = vec3(p);
float scale = mix(1., 2., smoothstep(-1., 1., boxPos.y));
boxPos.xz *= scale;
// 除以scale以避免过度行进
vec2 testBox = vec2(sdBox(boxPos, vec3(1.,1.,1.))/scale, 0.);

image1740742797088.png

还可以让其扭曲:

mat2 Rot(float a) {
    float s = sin(a);
    float c = cos(a);
    return mat2(c, -s, s, c);
}
vec3 boxPos = vec3(p);
float scale = mix(1., 2., smoothstep(-1., 1., boxPos.y));
boxPos.xz *= scale;
boxPos.xz *= Rot(boxPos.y);
vec2 testBox = vec2(sdBox(boxPos, vec3(1.,1.,1.))/scale, 0.);

image1740743158978.png

3.物体镜像。可以通过对于想镜像的轴取绝对值即可完成,比如 p.x = abs(p.x):

vec3 boxPos = vec3(p);
boxPos.x = abs(boxPos.x);
boxPos.x -= 2.;
float scale = mix(1., 2., smoothstep(-1., 1., boxPos.y));
boxPos.xz *= scale;
boxPos.xz *= Rot(boxPos.y);
vec2 testBox = vec2(sdBox(boxPos, vec3(1.,1.,1.))/scale, 0.);

image1740743471452.png

0 Answers