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

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

[固定長進行レイマーチングやってみた](http://yagiri000.hatenablog.com/entry/2018/09/13/190006)
本記事はこのサイトにあるコードをちょっといじったものになります

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

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

#実装
##球を表示する
![sphere.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406204/44fa8bce-5a76-33fe-5734-cb910bde49f3.png)

```glsl: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で球の色を指定できます
画像では色を空色っぽく変更しています

```glsl
// ライティング(Lambert)
float LightingObject(float3 pos)
{
    return saturate(dot(normalize(pos),_WorldSpaceLightPos0));
}
```
球の表面の法線は球の中心から見た点方向のベクトルなので、それとワールドにある平行光線の方向の内積を取ることで陰を表現できます
詳しくは「Lambert反射」を調べてください

## 関数を表現する
初めの方で言った関数です
![sinsin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406204/10da1ed0-782e-0171-6c53-2ddf319dd707.png)


```glsl: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
        }
    }
}
```

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

```glsl
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));
}
```
関数

```math
z=f(x,y)
```

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

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

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

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