RayMarching1:用射线的方式画一个球

一、ShaderToy作品

如果你对 Shader 有一定的了解,那么你或多或少听说过 shaderToy 这个网站,这个网站上有很多令人振奋的 shader 效果,而这些效果有可能只用了几行代码来实现。就如同画家绘画,在这里片段着色器就是画笔,屏幕就是画纸:

网站中对于任意一个作品,都提供了完整的 GLSL 片段着手器代码,它们是通过在你的浏览器中运行 WebGl 来展现这些效果的。你也可以通过修改代码,修改变量和输入来直接在网页上查看效果的变幻

只通过片段着色器来输出你的画面,那么这就基本告别光栅化了,你必须准确指定每个像素的颜色,如果你愿意,你可以复刻这世界所有的画作。说到非光栅化,第一个想起的应该都是光线追踪(Ray tracing),但好在事实没有那么难:关于上述图像的绘制有一个相对简单又经典的方案:光线步进(RayMarching)

二、有向距离函数(Signed Distance Functions, SDF)

距离函数很好理解:空间中有一个物体/曲面,该函数可以得出当前点与这个物体的最短距离

那么,有向距离函数就更好理解了:如果当前点在物体内部,那么这个距离就为负数,当前点在物体外部就为正数

一个最简单的例子:一个圆心在圆点,半径为1的球,那么它的距离函数就为:

f(x, y, z)=\sqrt{x^{2}+y^{2}+z^{2}}-1

以往直接描述物体三角形顶点信息的方式其实是一种显示表示法,而以上的方式则为隐式表示法,举个例子:把坐标点 (0, 0, 0.5) 和 (0, 3, 0) 代入 f(x, y, z),就可以得到 f(0,0,0.5) =-0.5 以及 f(0,3,0) =2

对应的着色器代码:

float sphereSDF(vec3 samplePoint)
{
    return length(samplePoint) - 1.0;
}

当然了,球体的 SDF 是最好推算的,对于其它的物体,可以参考:Inigo Quilez :: computer graphics, mathematics, shaders, fractals, demoscene and more

三、光线步进(RayMarching)算法

这个算法可以说是非常的暴力,要知道,从摄像机到当前片段可以得到一条射线,而这个算法的核心,就是得到这个射线什么时候会和场景中的物体相交

步进算法:从摄像机开始,按照射线的方向依次模拟每一个点,对于每一个点,都会根据有向距离函数(SDF)来判断依次当前是都到了某个曲面内,如果到了就说明该次步进结束,否则我们就继续下一个点的判断,直到走到一个临界值为止

就像图中的例子,可以将起点 P_0 当成摄像机,依次枚举 P_1 \rightarrow P_4 ,绿色圆圈的半径正是 SDF 的值。到达 P_4 点后结束,此时绿圆半径已小于一个阈值,可以算作到达了一个物体的表面。除此之外,相邻两次步进的距离正好就是上一次的 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°

但为什么看起来就是一个圆呢?因为没有光照,所有球上的每一点都是纯红色,当然看不出来

参考资料:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值