RayMarching3:组合与变幻

 

接上文:RayMarching2:给球加上光照

六、联合、交叉与取反

1):联合(union)

联合的目的很简单:当场景中有多个物体时,这些物体都要显示

假定物体 A 和物体 B,它们的 SDF 函数为 f_A(x) 和 f_B(x),那么 A ∪ B 就对应 min(f_A(x), f_B(x)) 

//联合
float unionSDF(float distA, float distB)
{
    return min(distA, distB);
}

//物体的SDF
float sceneSDF(float3 rayPoint)
{
    return unionSDF(cubeSDF(rayPoint), sphereSDF(rayPoint - float3(-2, 0, 2), 1));
}

2):交叉(intersect)

联合的目的:对于场景中的物体,显示它们交叉的部分

假定物体 A 和物体 B,它们的 SDF 函数为 f_A(x) 和 f_B(x),那么 A ∪ B 就对应 max(f_A(x), f_B(x)) 

//交叉
float intersectSDF(float distA, float distB)
{
    return max(distA, distB);
}

//物体的SDF
float sceneSDF(float3 rayPoint)
{
    return intersectSDF(cubeSDF(rayPoint), sphereSDF(rayPoint - float3(0, 0, 0), 1.25));
}

3):取反(difference)

取反更好理解:物体内部和外部互换

假定物体 A 的 SDF 函数为 f_A(x) ,那么 −A 就对应 -f_A(x)

不过一般来讲,不会对单物体进行取反,因为这样物体就变成了“无限大的”,因此取反往往用于和交叉同时作用,取场景中物体不同的部分,也就是 max(f_A(x), -f_B(x))

 

//取反
float differenceSDF(float distA, float distB)
{
    return max(distA, -distB);
}

//物体的SDF
float sceneSDF(float3 rayPoint)
{
    return differenceSDF(cubeSDF(rayPoint), sphereSDF(rayPoint - float3(0, 0, 0), 1.25));
}

对于场景中的多个物体,可以进行多次的取反、联合与交叉操作,来达到你想显示的效果

 

七、物体变换

1):旋转

和上一张摄像机 lookat 矩阵的实现很像,矩阵公式参考《基础线代公式汇总》,可以直接在着色器中构建你想要的矩阵

一个旋转的例子:

//三个旋转函数
float4x4 rotateY(float theta)
{
    float c = cos(theta);
    float s = sin(theta);
    return float4x4(
        float4(c, 0, s, 0),
        float4(0, 1, 0, 0),
        float4(-s, 0, c, 0),
        float4(0, 0, 0, 1)
    );
}
float4x4 rotateZ(float theta)
{
    float c = cos(theta);
    float s = sin(theta);
    return float4x4(
        float4(c, -s, 0, 0),
        float4(s, c, 0, 0),
        float4(0, 0, 1, 0),
        float4(0, 0, 0, 1)
    );
}
float4x4 rotateX(float theta)
{
    float c = cos(theta);
    float s = sin(theta);
    return float4x4(
        float4(1, 0, s, 0),
        float4(0, c, -s, 0),
        float4(0, s, c, 0),
        float4(0, 0, 0, 1)
    );
}

//物体的SDF
float sceneSDF(float3 rayPoint)
{
    float3 cubePoint = mul(float4(rayPoint, 1.0), rotateY(_Time.y));
    return cubeSDF(cubePoint);
}

不过需要注意的是,这里的矩阵乘法要反过来,因为我们是将射线上的探测点进行的旋转操作,而非对物体。后面的平移也是同样的道理

2):平移

相对于旋转,平移可以直接通过向量加减法搞定

float sceneSDF(float3 rayPoint)
{
    float3 cubePoint = rayPoint - float3(0, _Time.y % 6 - 3, 0);
    float3 spherePoint = rayPoint;
    return differenceSDF(sphereSDF(spherePoint, 1), cubeSDF(cubePoint, float3(4, 4, 4)));
}

3):等比缩放

关于等比缩放:假设将物体的长宽高放大为原来的2倍,就相当于将物体其所在的模型空间扩张两倍。可以把这个模型空间想象成网格状,模型空间扩张后相邻网格线间距就会变大,这样对于处于模型空间的采样点而言,就是坐标直接 / 2,也就是说可以得到结论:

  • 假定物体 A 的 SDF 函数为 f_A(x) ,那么 A 缩放 k 倍就对应 f_A(\frac{x}{k})

这也可以通过代码确认是否达到了效果

但是这真的没问题嘛?其实只要多试几个 k 值就会发现空间中的物体居然不见了,但是这个缩放很明显是不可能导致物体完全消失的

→ 稍微分析下就可以明白,尽管上述公式可以得到正确的表现效果,但是算出来的 SDF 函数值却当且仅当为 0 的时候才正确,其他时候其对应值也被同时缩放了,想想前面的分析:模型空间放大,所以我们将坐标 / 2,但事实上 SDF 函数得到的值必定是在世界空间下的,因此还需要将其缩放回去,正确的公式是:

  • 假定物体 A 的 SDF 函数为 f_A(x) ,那么 A 缩放 k 倍就对应 kf_A(\frac{x}{k})

也就是需要进行放缩补偿

//物体被缩小一半
float sceneSDF(float3 rayPoint)
{
    return cubeSDF(rayPoint * 2, float3(2, 2, 2)) / 2;
}

除此之外,再考虑到光线步进(Ray Marching)的过程:

每一次校验结果 SDF > 0,都会使得下一次校验点步进一段距离,而这段距离正是上一次的 SDF 值

所以如果没有进行放缩补偿,那么就会使得每次步进时使用了错误的步进距离,从而导致检测点直接跨过物体(物体缩小时,这也是为什么屏幕上会看不到物体),又或者是消耗了两倍的步进次数(物体放大时)

4):不等比缩放

继续,还有一种情况没有考虑呢,那就是不等比缩放,这个不等比缩放操作总是能整出些新的花样:例如缩放后得出错误的法向量,从而不得不使用逆矩阵重新推算,因此这次也要小心它

由于法向量是通过表面积分的手段算的,所以这次法向量不会出问题,但是计算 SDF 函数就糟糕了

必然也需要进行放缩补偿,但是该补偿多少呢??

→ 很可惜,由于 SDF 函数输入为向量,但输出的却是单一值距离,因此结论就是并不好做到正确的补偿,只能够退而求其次。考虑到没有进行放缩补偿会使得每次步进时使用了错误的步进距离,但是宁可缩小每次步进的距离,消耗更多次的步进次数,也不可放大步进距离,使得物体被“丢失”,那么最后得到的结论就是:

假定物体 A 的 SDF 函数为 f_A(x) ,那么 A 缩放 k 倍就对应 min(k_x, k_y, k_z)f_A(\frac{x}{k})

float sceneSDF(float3 rayPoint)
{
    return cubeSDF(rayPoint / float3(2, 1, 0.5), float3(2, 2, 2)) * 0.5;
}

 

参考文章:

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值