介绍
相信玩过ShaderToy的伙伴都对上图印象深刻,除了上图所呈现的效果外,ShaderToy的绝大多数3D效果都使用到了RayMarching技术,该算法通常与Signed distance functions(有符号距离函数,简称
SDF
)结合来实现各种炫酷的效果。
原理
在开始RayMarching原理分析之前,我们不得不先分析下有符号距离函数的概念,因为有符号距离函数是RayMarching算法中创建3D形状最通用的方式。
有符号距离函数
为了更细致的分析有符号距离函数(简称为
SDF
),我们假设空间中有一球体,同时空间中还存在另一点A,当点A在球体内部时,则A点到球心的距离小于球的半径,即
length(A) - r < 0
;当点A在球体表面上时,则A点到球心的距离等于球的半径,即
length(A) - r = 0
; 当点A远离球体表面时,则A点到球心的距离大于球的半径,即
length(A) - r > 0
;上述的表述可用以下公式表示(假设球体的半径为1):
float
sphereSDF
(
float3
p
)
{
return
length
(
p
)
-
1.0
;
}
上式是球体的SDF公式,更多的SDF公式可查阅
SDF距离函数
。
RayMarching算法
通过上述SDF的简单分析后我们已经有了建立各种基础形状的SDF函数,那么我们如何渲染他们呢?
RayMarching翻译为中文的意思为光线行进,要介绍它就需要先介绍下RayTracing(光线追踪),从字面上看这类型的技术与光线相关,在现实世界中光线的行进方向从光源开始,在经过一系列的反射,散射或折射的过程后进入眼睛。但是在计算机世界中,如果要从光源位置开始计算光线经过的一系列路径,最后进入人眼的全过程,会变得异常复杂,因为所有行进的光线不会全部进入人眼,这往往增加了巨大的工作量,因为我们最终想要的仅仅是被光源影响后的直接或间接光照后的结果图像,那么为了避免无谓的运算,通过从眼睛或相机发出射线来反方向追踪光线就变成了最直接的处理方式。
<img src="https://u3d-connect-cdn-public-prd.cdn.unity.cn/h1/20200406/p/images/697dce1d-637c-4143-b962-46b4c00cf9b2_raytrace.png" style="text-align:center;width:300px;height:200px;display:block;margin:0 auto">
上图是来自维基百科RayTracing的图示,与RayTracing一样,我们为相机选择一个位置,在相机前放置一个网格,通过网格中的每个点发送来自相机的射线,每个网格点对应于输出图像中的一个像素。在光线行进中,整个场景是根据SDF函数定义的。为了找到视线和场景之间的交点,我们从摄影机开始,沿着视线一点一点地移动。在每个步骤中,我们会获取点与物体中心的距离,直到获取到的点与物体中心的距离小于物体表面到物体中心的距离时,代表此时的点已经在物体的内部了。反之就将沿着射线不断增加最大数量。
<img src="https://u3d-connect-cdn-public-prd.cdn.unity.cn/h1/20200406/p/images/7bd81cfc-8bc1-4a83-a361-159021aeb990_spheretrace.jpg" style="text-align:center;width:400px;height:244px;display:block;margin:0 auto">
实验
1.新建Unlit着色器后,在片元着色器中定义相机位置与行进方向。
// 定义球体SDF,半径为1
float
sdfSphere
(
float3
p
)
{
return
length
(
p
)
-
1
;
}
// 获取当前行进点与物体中心的距离
float
getDist
(
float3
p
)
{
float
sdf
=
sdfSphere
(
p
)
;
return
sdf
;
}
fixed4
startRay
(
float3
ro
,
float3
rd
,
fixed4
col
)
{
float
dStart
=
0
;
float
dSphere
;
fixed4
resultCol
=
fixed4
(
1.0
,
1.0
,
1.0
,
1.0
)
;
for
(
int
i
=
0
;
i
<=
_MaxDist
;
i
++
)
{
// 当行进距离超出了最大距离时返回背景色
if
(
dStart
>
_MaxDist
)
{
resultCol
=
col
;
break
;
}
// 从相机位置开始行进
float3
p
=
ro
+
rd
*
dStart
;
dSphere
=
getDist
(
p
)
;
dStart
+=
dSphere
;
// 当行进点与物体中心距离小于半径时,表示已经碰触到了物体
// 小于0.01表示接近于物体表面
if
(
dSphere
<
0.01
)
{
resultCol
=
fixed4
(
1
,
1
,
1
,
1
)
;
break
;
}
}
return
resultCol
;
}
float3
getRayDir
(
float2
uv
)
{
// uv坐标原点移至屏幕中心,uv范围为-0.5~0.5
float2
coord
=
uv
-
0.5
;
// 使画面的x方向不至于拉伸
coord
.
x
*=
screenSize
.
x
/
screenSize
.
y
;
return
normalize
(
float3
(
coord
.
x
,
coord
.
y
,
1
)
)
;
}
fixed4
frag
(
v2f
i
)
:
SV_Target
{
// 建立网格,确立行进方向
float3
rd
=
getRayDir
(
i
.
uv
)
;
// 相机位置
float3
rayOri
=
_WorldSpaceCameraPos
;
fixed4
col
=
tex2D
(
_MainTex
,
i
.
uv
)
;
fixed4
result
=
startRay
(
rayOri
,
rd
,
col
)
;
return
result
;
}
2.新建C#脚本,传递屏幕尺寸。
using
System
.
Collections
;
using
System
.
Collections
.
Generic
;
using
UnityEngine
;
[
ExecuteInEditMode
]
public
class
RayMarching
:
MonoBehaviour
{
public
Material
mat
;
private
void
OnRenderImage
(
RenderTexture
src
,
RenderTexture
dest
)
{
if
(
!
mat
)
{
Graphics
.
Blit
(
src
,
dest
)
;
return
;
}
mat
.
SetVector
(
"screenSize"
,
new
Vector2
(
Screen
.
currentResolution
.
width
,
Screen
.
currentResolution
.
height
)
)
;
Graphics
.
Blit
(
src
,
dest
,
_mat
)
;
}
}
3.此时在屏幕上输出的结果图像如下:
注意,上面那个白色圆形,确实是个球,为了证明它是个球,下一篇将会引入基础的光照模型,除了这些我们还发现转动相机并没有改变模型的位置,显然是一个bug,下篇实验也会对此进行分析并提出解决方案。我们可以试着在场景中增加一个Cube,发现白色球体一直在立方体前方,这些优化的点,将会在后续的RayMarching系列实验中一并解决。
本篇实验就到这里了,那么我们下次实验见吧。如果觉得有用,就用微信扫描下面的二维码,关注我们吧!更多干货一起互撩。
<img src="https://u3d-connect-cdn-public-prd.cdn.unity.cn/h1/20200406/p/images/f6f110ec-1a11-465a-bc68-12484f81b109_qrcode_for_gh_7256935cd7f7_344.jpg" style="text-align:center;width:344px;height:344px;display:block;margin:0 auto">
1603

被折叠的 条评论
为什么被折叠?



