如何在Unity中用SDF技术绘制卡比(上)
本帖最后由 彭彭头Pavel 于 2024-5-14 22:20 编辑> 本帖最后由 彭彭头Pavel 于 2024-5-7 13:03 编辑
!(data/attachment/forum/202405/07/002214rnmkimnjoa2zmpij.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "Snipaste_2024-05-07_00-22-11.png")
在本文中,我们将探索如何使用基于 Signed Distance Function (SDF) 的技术来创建和渲染 3D 融球角色如星之卡比或者史莱姆。我们将详细讨论以下关键技术点:
1.使用光线步进(Ray Marching)技术实现融球隐式表面
2.利用指数对数函数构建的平滑最小值函数实现形状间的融合
3.应用有限差分方法(Finite Difference)生成法线
4.实现深度写入
# 1. 使用光线步进(Ray Marching)技术实现融球隐式表面
![](https://hyper-bovid-388.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F3f2fbaed-deef-4470-9c55-f54a154bb4bd%2F86202411-a798-4704-97df-109194dbd1c7%2FSnipaste_2024-05-05_17-52-08.png?table=block&id=4b0d18b5-fdd8-4435-aaa6-b9ef5ec9aa90&spaceId=3f2fbaed-deef-4470-9c55-f54a154bb4bd&width=480&userId=&cache=v2)
```c
// 球体的距离场函数
float4 sphereDistanceFunction(float4 sphere, float3 pos)
{
return length(sphere.xyz - pos) - sphere.w;
}
```
$\text{sphereDistanceFunction}(\text{sphere}, \text{pos}) = \left\| \text{sphere.xyz} - \text{pos} \right\| - \text{sphere.w}$
在代码示例中,我们定义了一个球体的距离场函数,其中 `sphere.xyz`表示球体的位置,`sphere.w`是球体的半径,而 `pos` 是当前光线步进的位置。
```c
// 返回与所有球体的最短距离
float getDistance(float3 pos)
{
float dist = 100000;
for (int i = 0; i < _SphereCount; i++)
{
dist = smoothMin(dist, sphereDistanceFunction(_Spheres, pos), 15);
}
return dist;
}
```
我们使用 `getDistance`函数来计算从光线当前位置到隐式表面融球的最短距离。这通过遍历所有球体并应用 `smoothMin`函数来实现平滑的融合效果。
```c
for (int i = 0; i < 20; i++)
{
// pos与融球整体的最短距离
float dist = getDistance(pos);
// 沿射线方向前进
pos += dist * rayDir;
}
```
此循环体中,光线从世界空间像素坐标出发,沿着摄像机到当前像素的方向逐步前进,直到达到迭代次数上限(20次)。每次循环时首先计算当前位置与融球体的最短距离,然后沿该方向步进相应距离。
![微信图片_20240506011404.jpg](data/attachment/forum/202405/07/003139q5vttojzlb577tz3.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "微信图片_20240506011404.jpg")
这时候对于这个像素来说可以获得的结果举例有如上图所示:
A:dist值很大甚至大于1
B:dist很小趋近于0
当然这只是两个比较极端的结果,举出来是方便理解,输出所有的dist值可以直观的看到结果,`dist`的作用是区分出融球实体部分,为其写入深度,颜色和法线作准备。
# 2. 利用指数对数函数构建的平滑最小值函数实现形状间的融合
```c
float smoothMin(float x1, float x2, float k)
{
return -log(exp(-k * x1) + exp(-k * x2)) / k;
}
```
$\text{smoothMin}(x_1, x_2, k) = -\frac{\log(\exp(-k x_1) + \exp(-k x_2))}{k}$
功能:
对输入的x1,x2值提供一个平滑过渡的方式确定他们之间的较小值。确定最小值我们比较常用的是Min函数,但是它会有一个突变点,而smoothMin函数能提供一个平滑的曲线。
效果:
当k值接近于0时,平滑效果最明显,接近在x1和x2之间的软最小值。
当k值较大时,结果趋近于传统的min(x1,x2)函数。
![](https://file.notion.so/f/f/3f2fbaed-deef-4470-9c55-f54a154bb4bd/340318c6-91cb-4433-8103-82352fd31baf/20240506_223847.gif?id=0dcd47c0-c232-4e3e-831a-3944b0ba238f&table=block&spaceId=3f2fbaed-deef-4470-9c55-f54a154bb4bd&expirationTimestamp=1715097600000&signature=i8AW6h2UhtngMP2A-Sjxf1Z-dVV9T9PeRrwcfZZ-mmc)
# 3. 应用有限差分方法(Finite Difference)生成法线
![](https://hyper-bovid-388.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F3f2fbaed-deef-4470-9c55-f54a154bb4bd%2Ff58f9a66-b2f4-4b5a-8f9a-d8062548f966%2FiShot_2024-05-06_16.01.50.png?table=block&id=ce5d04da-4222-4851-a0dd-a19da1fd53b7&spaceId=3f2fbaed-deef-4470-9c55-f54a154bb4bd&width=920&userId=&cache=v2)
```c
// 计算法线
float3 getNormal(float3 pos)
{
float d = 0.0001;
return normalize(float3(
getDistance(pos + float3(d, 0.0, 0.0)) - getDistance(pos + float3(-d, 0.0, 0.0)),
getDistance(pos + float3(0.0, d, 0.0)) - getDistance(pos + float3(0.0, -d, 0.0)),
getDistance(pos + float3(0.0, 0.0, d)) - getDistance(pos + float3(0.0, 0.0, -d))
));
}
```
使用中心差分方法来估算形状函数在 x、y、z 三个方向上的偏导数,
- x方向的偏导数
$\frac{\partial F}{\partial x} \approx \frac{F(p+d\mathbf{i}) - F(p-d\mathbf{i})}{2d}$
- y方向的偏导数
$\frac{\partial F}{\partial y} \approx \frac{F(p+d\mathbf{j}) - F(p-d\mathbf{j})}{2d}$
- z方向的偏导数
$\frac{\partial F}{\partial z} \approx \frac{F(p+d\mathbf{k}) - F(p-d\mathbf{k})}{2d}$
将这些导数构成一个向量。即可得到未归一化的法线向量 `n`:
$n = \left( \frac{F(p+d\mathbf{i}) - F(p-d\mathbf{i})}{2d}, \frac{F(p+d\mathbf{j}) - F(p-d\mathbf{j})}{2d}, \frac{F(p+d\mathbf{k}) - F(p-d\mathbf{k})}{2d} \right)$
最后,归一化词向量得到单位法线向量
$n_{\text{unit}} = \frac{n}{\|n\|}$
由于我们只需要取得方向向量,所以在程序中可以省略掉除以 `2d`的计算。
!(data/attachment/forum/202405/07/003320egkhcaokstr4gahp.gif?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "20240506_223137.gif")
功能:
如上图所示输入形状上的点 `P`的坐标 `(AX,AY),`和最小差分的值 `d`,分别计算x和y方向上的变化率,再将两个向量相加再做归一化得到法线向量。
效果:
`d`值的效果在shader中呈现出来的现象是0.0001时这种微小的偏移会使法线计算准确,但是在ggb中需要 `d`值很大才会有比较精确的效果,所以 `d`值在函数中对结果到底起到了什么影响作用呢?
有一种可能在ggb中过小的d值导致法线不精确是精度问题。
# 4. 实现深度写入
![](https://hyper-bovid-388.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F3f2fbaed-deef-4470-9c55-f54a154bb4bd%2F548e569f-cea3-4e2f-a6d5-3a496f8815fb%2FiShot_2024-05-06_16.50.44.png?table=block&id=6cdf85c1-7405-4609-9062-508903ba75e9&spaceId=3f2fbaed-deef-4470-9c55-f54a154bb4bd&width=810&userId=&cache=v2)
```c
// 计算深度
inline float getDepth(float3 pos)
{
const float4 vpPos = mul(UNITY_MATRIX_VP, float4(pos, 1.0));
float z = vpPos.z / vpPos.w;
#if defined(SHADER_API_GLCORE) || \
defined(SHADER_API_OPENGL) || \
defined(SHADER_API_GLES) || \
defined(SHADER_API_GLES3)
return z * 0.5 + 0.5;
#else
return z;
#endif
}
```
将RayMarching击中融球体的像素位置坐标从世界空间转换到观察空间,除以的 `vpPos.w`值获得透视变化后的深度 `z`。再匹配不同图形接口的深度值范围。
```c
output frag(Varyings i)
{
output o;
......
for (int i = 0; i < 20; i++)
{
float dist = getDistance(pos);
......
if(dist < 0.01)
{
......
o.depth = getDepth(pos); // 写入深度
}
// 沿射线方向前进
pos += dist * rayDir;
}
// 如果没有发生碰撞,则设置为透明
o.col = 0;
o.depth = 0;
return o;
}
```
在循环体中,每次迭代都会检测是否发生碰撞,若光线步进的位置与融球形状的最短距离小于0.01,则写入深度值,否则深度保持为 0。这样确保了只有当光线确实击中目标物体时,才记录深度信息。
在下篇中将详细讲解各种融球材质效果的实现。
## 5\. 参考
- 这个大佬实现了融球液体
(https://)
- 由顽皮狗大佬开发,功能完整的融球插件
(https://)
666666666661
页:
[1]