一、ShaderToy作品
如果你对 Shader 有一定的了解,那么你或多或少听说过 shaderToy 这个网站,这个网站上有很多令人振奋的 shader 效果,而这些效果有可能只用了几行代码来实现。就如同画家绘画,在这里片段着色器就是画笔,屏幕就是画纸:
网站中对于任意一个作品,都提供了完整的 GLSL 片段着手器代码,它们是通过在你的浏览器中运行 WebGl 来展现这些效果的。你也可以通过修改代码,修改变量和输入来直接在网页上查看效果的变幻
只通过片段着色器来输出你的画面,那么这就基本告别光栅化了,你必须准确指定每个像素的颜色,如果你愿意,你可以复刻这世界所有的画作。说到非光栅化,第一个想起的应该都是光线追踪(Ray tracing),但好在事实没有那么难:关于上述图像的绘制有一个相对简单又经典的方案:光线步进(RayMarching)
二、有向距离函数(Signed Distance Functions, SDF)
距离函数很好理解:空间中有一个物体/曲面,该函数可以得出当前点与这个物体的最短距离
那么,有向距离函数就更好理解了:如果当前点在物体内部,那么这个距离就为负数,当前点在物体外部就为正数
一个最简单的例子:一个圆心在圆点,半径为1的球,那么它的距离函数就为:
以往直接描述物体三角形顶点信息的方式其实是一种显示表示法,而以上的方式则为隐式表示法,举个例子:把坐标点 (0, 0, 0.5) 和 (0, 3, 0) 代入 ,就可以得到 以及
对应的着色器代码:
float sphereSDF(vec3 samplePoint)
{
return length(samplePoint) - 1.0;
}
当然了,球体的 SDF 是最好推算的,对于其它的物体,可以参考:Inigo Quilez :: computer graphics, mathematics, shaders, fractals, demoscene and more
三、光线步进(RayMarching)算法
这个算法可以说是非常的暴力,要知道,从摄像机到当前片段可以得到一条射线,而这个算法的核心,就是得到这个射线什么时候会和场景中的物体相交
步进算法:从摄像机开始,按照射线的方向依次模拟每一个点,对于每一个点,都会根据有向距离函数(SDF)来判断依次当前是都到了某个曲面内,如果到了就说明该次步进结束,否则我们就继续下一个点的判断,直到走到一个临界值为止
就像图中的例子,可以将起点 当成摄像机,依次枚举 ,绿色圆圈的半径正是 SDF 的值。到达 点后结束,此时绿圆半径已小于一个阈值,可以算作到达了一个物体的表面。除此之外,相邻两次步进的距离正好就是上一次的 SDF 值,很容易证明:在长度为 SDF 范围内绝对不会有物体
搞定了理论后,就可以写个代码测试下:我们假设存在一个球体,其 SDF 就为上一节中的 SDF
Shader "ShaderToy/RedCircle"
{
Properties
{
_MinDist("MinDist", Float) = 0.0 //射线步进起点
_MaxDist("MaxDist", Float) = 100.0 //射线步进终点
_Volsteps("Volsteps", Int) = 255 //最大步进次数
_Epsilon("Volsteps", Float) = 0.001 //精度
}
CGINCLUDE
float _MaxDist;
float _MinDist;
int _Volsteps;
float _Epsilon;
//球体的SDF
float sphereSDF(float3 rayPoint)
{
return length(rayPoint) - 1;
}
//获得摄像机到当前片段的射线
float3 getRay(float viewAngle, float2 screenSize, float2 pos)
{
float2 up = pos - screenSize / 2.0;
float z = screenSize.y / tan(radians(viewAngle) / 2.0);
float3 ray = normalize(float3(up, -z));
return ray;
}
float rayMarching(float3 cameraPos, float3 ray, float start, float end)
{
float nowDepth = start;
for (int i = 0; i < _Volsteps; i++)
{
float dist = sphereSDF(cameraPos + nowDepth * ray);
if (dist < _Epsilon)
return nowDepth;
nowDepth += dist;
if (dist > end)
return end;
}
return end;
}
ENDCG
SubShader
{
PASS
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct _2v
{
float4 vertex: POSITION;
};
struct v2f
{
float4 pos: SV_POSITION;
};
v2f vert(_2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag(v2f i): SV_Target
{
float3 ray = getRay(45.0, _ScreenParams.xy, i.pos.xy);
float3 eye = float3(0.0, 0.0, 5.0); //摄像机位置
float dist = rayMarching(eye, ray, _MinDist, _MaxDist);
fixed4 color = fixed4(0.0, 0.0, 0.0, 1.0);
if (dist < _MaxDist - _Epsilon)
color = fixed4(1.0, 0.0, 0.0, 1.0);
return color;
}
ENDCG
}
}
}
其实这就是一个中心位于(0, 0, 0),半径为1的球,代码中我们的摄像机位于(0, 0, 5),朝向为 z 轴负方向,视野为 45°
但为什么看起来就是一个圆呢?因为没有光照,所有球上的每一点都是纯红色,当然看不出来
参考资料: