六、联合、交叉与取反
1):联合(union)
联合的目的很简单:当场景中有多个物体时,这些物体都要显示
假定物体 A 和物体 B,它们的 SDF 函数为 和 ,那么 A ∪ B 就对应
//联合
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 函数为 和 ,那么 A ∪ B 就对应
//交叉
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 函数为 ,那么 −A 就对应
不过一般来讲,不会对单物体进行取反,因为这样物体就变成了“无限大的”,因此取反往往用于和交叉同时作用,取场景中物体不同的部分,也就是
//取反
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 函数为 ,那么 A 缩放 倍就对应
这也可以通过代码确认是否达到了效果
但是这真的没问题嘛?其实只要多试几个 k 值就会发现空间中的物体居然不见了,但是这个缩放很明显是不可能导致物体完全消失的
→ 稍微分析下就可以明白,尽管上述公式可以得到正确的表现效果,但是算出来的 SDF 函数值却当且仅当为 0 的时候才正确,其他时候其对应值也被同时缩放了,想想前面的分析:模型空间放大,所以我们将坐标 / 2,但事实上 SDF 函数得到的值必定是在世界空间下的,因此还需要将其缩放回去,正确的公式是:
- 假定物体 A 的 SDF 函数为 ,那么 A 缩放 倍就对应
也就是需要进行放缩补偿
//物体被缩小一半
float sceneSDF(float3 rayPoint)
{
return cubeSDF(rayPoint * 2, float3(2, 2, 2)) / 2;
}
除此之外,再考虑到光线步进(Ray Marching)的过程:
每一次校验结果 SDF > 0,都会使得下一次校验点步进一段距离,而这段距离正是上一次的 SDF 值
所以如果没有进行放缩补偿,那么就会使得每次步进时使用了错误的步进距离,从而导致检测点直接跨过物体(物体缩小时,这也是为什么屏幕上会看不到物体),又或者是消耗了两倍的步进次数(物体放大时)
4):不等比缩放
继续,还有一种情况没有考虑呢,那就是不等比缩放,这个不等比缩放操作总是能整出些新的花样:例如缩放后得出错误的法向量,从而不得不使用逆矩阵重新推算,因此这次也要小心它
由于法向量是通过表面积分的手段算的,所以这次法向量不会出问题,但是计算 SDF 函数就糟糕了
必然也需要进行放缩补偿,但是该补偿多少呢??
→ 很可惜,由于 SDF 函数输入为向量,但输出的却是单一值距离,因此结论就是并不好做到正确的补偿,只能够退而求其次。考虑到没有进行放缩补偿会使得每次步进时使用了错误的步进距离,但是宁可缩小每次步进的距离,消耗更多次的步进次数,也不可放大步进距离,使得物体被“丢失”,那么最后得到的结论就是:
假定物体 A 的 SDF 函数为 ,那么 A 缩放 倍就对应
float sceneSDF(float3 rayPoint)
{
return cubeSDF(rayPoint / float3(2, 1, 0.5), float3(2, 2, 2)) * 0.5;
}
参考文章: