レイマーチングで遊ぶ (1)

2019-11-07

背景

  • 最近レイマーチングを勉強し始めて、自分でも何か作ってみるかと思ってシェーダを書いた
  • 適当に数式を書いていたら、初心者ながらちょっとカッコいい雰囲気のやつができたので記録しておく
  • Unity 上で HLSL で書いている

成果物

ソースコード

Shader "Raymarching/BlueRay" {
  Properties {
    _DiffuseColor ("Diffuse Color", Color) = (1, 1, 1, 1)
    _SpecularColor ("Specular Color", Color) = (1, 1, 1, 1)
    _Metalic ("Metalic", Float) = 20
  }
  SubShader {
    Tags {"Queue" = "Transparent" "LightMode" = "ForwardBase"}
    LOD 100
    Cull Off ZWrite On ZTest Always
    Blend SrcAlpha OneMinusSrcAlpha

    Pass {
      CGPROGRAM
      #pragma vertex vert
      #pragma fragment frag

      #include "UnityCG.cginc"
      #include "Lighting.cginc"

      #define PI 3.14159265359f

      // Properties
      uniform fixed4 _DiffuseColor;
      uniform fixed4 _SpecularColor;
      uniform half   _Metalic;

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

      // Vertex to Fragment
      struct v2f {
        float4 vertex : SV_POSITION;
        float2 uv     : TEXCOORD0;
        float4 pos    : POSITION1;
      };

      //------------------------------------------------------------------------
      // Distance function helpers
      //------------------------------------------------------------------------
      float2 mod2(float2 a, float2 b) {
        return a - b * floor(a / b);
      }

      float2 divSpace2d(float2 pos, float interval, float offset = 0) {
        return mod2(pos + offset, interval) - (0.5 * interval);
      }

      float2 rotate2d(float2 pos, float angle) {
        float s = sin(angle);
        float c = cos(angle);
        return mul(float2x2(c, s, -s, c), pos);
      }

      //------------------------------------------------------------------------
      // Distance functions
      //------------------------------------------------------------------------
      float dPrism(float3 pos, float2 h) {
        float3 q = abs(pos);
        return max(q.z - h.y, max(q.x * 0.866025 + pos.y * 0.5, -pos.y) - h.x * 0.5);
      }

      float distanceFunc(float3 pos) {
        float interval = 15 + (cos(_Time * 16) + 1.0) * 5;
        float offset = 0;
        float height = 1.0 + sin(floor(pos.z / 8) + _Time * 8) * 90;
        pos.zx = divSpace2d(pos.zx, interval, offset);
        pos.yz = rotate2d(pos.yz, PI / 2);
        return dPrism(pos, float2(1, height));
      }

      //------------------------------------------------------------------------
      // Vertex shader
      //------------------------------------------------------------------------
      v2f vert(appdata v) {
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.pos = mul(unity_ObjectToWorld, v.vertex);  // local coord to world coord
        o.uv = v.uv;
        return o;
      }

      //------------------------------------------------------------------------
      // Rendering functions
      //------------------------------------------------------------------------
      float3 calcNormal(float3 pos) {
        float d = 0.001;
        return normalize(
          float3(
            distanceFunc(pos + float3(d, 0, 0)) - distanceFunc(pos + float3(-d, 0, 0)),
            distanceFunc(pos + float3(0, d, 0)) - distanceFunc(pos + float3(0, -d, 0)),
            distanceFunc(pos + float3(0, 0, d)) - distanceFunc(pos + float3(0, 0, -d))
          )
        );
      }

      fixed4 calcColor(float3 pos, float3 rayVec, float totalDistance) {
        // Lighting
        half3 lightVec   = _WorldSpaceLightPos0.xyz;
        half3 normal     = calcNormal(pos);
        half3 reflectVec = reflect(-lightVec, normal);
        half3 halfVec    = normalize(lightVec + -rayVec);

        half NdotL = saturate(dot(normal, lightVec));
        half NdotH = saturate(dot(normal, halfVec));

        fixed3 ambient  = UNITY_LIGHTMODEL_AMBIENT.rgb * _DiffuseColor.rgb;
        fixed3 diffuse  = _LightColor0.rgb * _DiffuseColor * NdotL;
        fixed3 specular = _LightColor0.rgb * _SpecularColor * pow(NdotH, _Metalic);
        fixed4 color    = fixed4(ambient + diffuse + specular, 1.0);

        // Simple Fog
        fixed4 fogColor = fixed4(0.15, 0.1, 0.5, 1.0);
        float fogFactor = saturate(totalDistance / 150);
        return lerp(color, fogColor, fogFactor);
      }

      //------------------------------------------------------------------------
      // Fragment shader : Raymarching
      //------------------------------------------------------------------------
      fixed4 frag(v2f i) : SV_Target {
        float3 pos    = i.pos.xyz;
        float3 rayVec = normalize(pos.xyz - _WorldSpaceCameraPos);

        const int MAX_MARCH = 64;
        const float EPSILON = 0.001;

        fixed4 color = 0;
        float totalDistance = 0;
        float minDistance = 1e+32;
        for (int i = 0; i < MAX_MARCH; ++i) {
          // Advance ray until it reaches the object
          float progress = distanceFunc(pos);
          float farFactor = (totalDistance / 150);
          if (minDistance > progress + farFactor) {
            minDistance = progress + farFactor;
          }
          if (progress > EPSILON) {
            pos.xyz += progress * rayVec.xyz;
            totalDistance += progress;
            continue;
          }

          // Fill fragment with lighting
          color = calcColor(pos, rayVec, totalDistance);
        }

        // Emission
        fixed4 emColor = fixed4(0.3, 0.8, 2.0, 1.0);
        fixed4 emission = pow(minDistance + 0.8, -2.0);
        return color + (emission * emColor);
        return emission * emColor;
      }

      ENDCG
    }
  }
}

解説

  • 発光する部分は解説記事などに出会えなかったので自分で何となく書いた
    • 「カメラに近いほうのオブジェクトの近くを通ってたら色を加算してやれば光るんじゃないか」 と思ってやってみたら何となく光った
  • 色々書いてあるが、レイマーチングで個性が出るのは空間を定義する 距離関数 distanceFunc() の部分
  • プリズムの柱を並べているが、高さを計算するところで sin()pos.z を入れてみたらこんな見た目になっちゃった
      float distanceFunc(float3 pos) {
        float interval = 15 + (cos(_Time * 16) + 1.0) * 5;
        float offset = 0;
        float height = 1.0 + sin(floor(pos.z / 8) + _Time * 8) * 90;
        pos.zx = divSpace2d(pos.zx, interval, offset);
        pos.yz = rotate2d(pos.yz, PI / 2);
        return dPrism(pos, float2(1, height));
      }