LoginSignup
4
2

More than 3 years have passed since last update.

固定長レイマーチで関数表示(ShaderLab)

Posted at

目的

レイマーチを試したのでその備忘録

固定長進行レイマーチングやってみた
本記事はこのサイトにあるコードをちょっといじったものになります

やること

Unityでレイマーチングのシェーダーを書いて z = f(x,y) の形の関数を表示させる

レイマーチングとは

光の経路を逆算して描画する方法
詳しくは他の記事参照
今回は本来のレイマーチングとは違って固定長でレイを進める

実装

球を表示する

sphere.png

sphere.shader
Shader "Custom/RayMarching/Sphere"
{
    Properties
    {
        _Threshold("Threshold", Range(0.0,3.0)) = 0.6   // 閾値
        _Color("Color", Color) = (1.0,1.0,1.0,1.0)      // 表面の色
    }
    SubShader
    {
        Tags{ "Queue" = "Transparent" }
        LOD 100

        Pass
        {
            ZWrite On
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 pos : POSITION1;
                float4 vertex : SV_POSITION;
            };

            // vert関数は本題ではないので簡潔に
            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.pos = mul(unity_ObjectToWorld, v.vertex);
                o.uv = v.uv;
                return o;
            }

            float _Threshold;
            fixed4 _Color;

            // 今の位置が球の内部かどうか
            bool IsInObject(float3 pos)
            {
                return distance(pos, float3(0.0, 0.0, 0.0)) < _Threshold;
            }

            // ライティング(Lambert)
            float LightingObject(float3 pos)
            {
                return saturate(dot(normalize(pos),_WorldSpaceLightPos0));
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 col;

                // 背景は透明に
                col.xyz = 0.0;
                col.w = 0.0;

                // レイの初期位置(表面上)
                float3 pos = i.pos.xyz; 

                // レイの進行方向
                float3 forward = normalize(pos.xyz - _WorldSpaceCameraPos); 

                // 一定の割合でレイが進行
                // オブジェクト内に到達したら光の計算
                // 当たらなかったら色変更なし(透過)
                const int StepNum = 2000;
                const float MarchingDist = 0.03;
                for (int i = 0; i < StepNum; i++)
                {
                    if (IsInObject(pos))
                    {
                        col.xyz = _Color.xyz * LightingObject(pos);
                        col.w = 1.0;
                        break;
                    }
                    pos.xyz += MarchingDist * forward.xyz;
                }

                return col;
            }
            ENDCG
        }
    }
}

単純に球を表示するだけです
_Thresholdで球の半径を
_Colorで球の色を指定できます
画像では色を空色っぽく変更しています

// ライティング(Lambert)
float LightingObject(float3 pos)
{
    return saturate(dot(normalize(pos),_WorldSpaceLightPos0));
}

球の表面の法線は球の中心から見た点方向のベクトルなので、それとワールドにある平行光線の方向の内積を取ることで陰を表現できます
詳しくは「Lambert反射」を調べてください

関数を表現する

初めの方で言った関数です
sinsin.png

Function.shader
Shader "Custom/RayMarching/Function"
{
    Properties
    {
        _Threshold("Threshold", Range(0.0,3.0)) = 0.6 
        _Color("Color", Color) = (1.0,1.0,1.0,1.0)
        _Ambient("Ambient", Color) = (0.0,0.0,0.0,0.0)
    }
    SubShader
    {
        Tags{ "Queue" = "Transparent" }
        LOD 100

        Pass
        {
            ZWrite On
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
           #pragma multi_compile_fog

           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 pos : POSITION1;
                float4 vertex : SV_POSITION;
            };

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.pos = mul(unity_ObjectToWorld, v.vertex);
                o.uv = v.uv;
                return o;
            }

            float _Threshold;
            fixed4 _Color;
            fixed4 _Ambient;



            float Func(float x, float z){
                return sin(x) + sin(z);
            }

            float3 CalcNormal(float2 pos){
                const float delta = 0.00001;
                float x = pos.x;float z = pos.y;
                float dx = Func(x + delta,z) - Func(x - delta, z);
                float dz = Func(x, z + delta) - Func(x, z - delta);
                dx /= delta;
                dz /= delta;
                return normalize(-float3(dx,-1,dz));
            }

            bool isInObject(float3 pos) {
                return pos.y < Func(pos.x,pos.z);
            }

            float LightingObject(float3 pos){
                return saturate(dot(CalcNormal(pos.xz),_WorldSpaceLightPos0));
            }
            fixed3 ColorDefine(float3 pos){
                float lightRate = LightingObject(pos);
                return _Color.xyz * lightRate + _Ambient * (1 - lightRate);
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 col;

                col.xyz = 0.0;
                col.w = 0.0;

                float3 pos = i.pos.xyz; 

                float3 forward = normalize(pos.xyz - _WorldSpaceCameraPos); 

                const int StepNum = 2000;
                const float MarchingDist = 0.03;
                for (int i = 0; i < StepNum; i++) {
                    if (isInObject(pos)) {
                        col.xyz = ColorDefine(pos);
                        col.w = 1.0;
                        break;
                    }
                    pos.xyz += MarchingDist * forward.xyz;
                }

                return col;
            }
            ENDCG
        }
    }
}

関数では球ほど簡単には法線を計算できないので、それ専用の関数を作っています

float3 CalcNormal(float2 pos){
    const float delta = 0.00001;
    float x = pos.x;float z = pos.y;
    float dx = Func(x + delta,z) - Func(x - delta, z);
    float dz = Func(x, z + delta) - Func(x, z - delta);
    dx /= delta;
    dz /= delta;
    return normalize(-float3(dx,-1,dz));
}

関数

z=f(x,y)

の、点(a,b,f(a,b))での法線は

(f_{x}(a,b),f_{y}(a,b),-1)

と表わされ、fx,fyはそれぞれdx,dyに対応しているわけです
ただしここで2点注意しなければいけないことがあって
一つはUnityはy軸が上方向なので式中のyとzを入れ替えなければならないこと
二つ目は実際の法線は逆方向なので-1をかけなければならないこと
それを考慮すると上記の関数ができます

Func関数の中身をいじれば別の関数を表示することもできます

まとめ

レイマーチングだと関数を正確に表現できるのでどこかで使えそう(適当)
関数次第でどんな図形も表現できるのは素晴らしいと思います

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2