2019年2月3日

ジオメトリシェーダでワイヤーフレーム ~Diffuse 編~


ジオメトリシェーダで作成したワイヤーフレームに光源や影を反映させる方法をメモメモ…



※このページの内容の動作確認にはUnity2018.2を使用しています。
前回はジオメトリシェーダを利用してメッシュにポリゴンを追加し、厚みのあるワイヤーフレームを作成しました。ただ、ドヤ顔で作ってみたはいいものの、ライティング関連の記述が一切ない Unlit シェーダなので、まったく立体的には見えません。というわけで今回は光源を加味した Diffuse シェーダの処理を組み込んでみたいと思います。

前回作成したシェーダ

…とはいっても、ライティングの処理を自前で実装するのは、難易度的にかなり高めです。「これは厳しい道のりになるぞ…」と覚悟した矢先にこんな投稿を見つけました。

[Unity] デフォルトのDiffuseライティングをカスタムシェーダで実装する - Qiita
https://qiita.com/edo_m18/items/1b90932a284fb8e89156

もしや、この Diffuse シェーダに前回作ったジオメトリシェーダを合体させればいいのでは?という安易な考えを信じつつ、若干の試行錯誤を経ながら出来上がったのがこちらです。

今回作成したシェーダ

しっかりとライティングが反映されていますね。ワイヤーフレームの内部に置かれたキューブにも影が落ちていて特に違和感はありません。最新の Standard シェーダと併用するとちょっと変な感じがするかもしれませんが、とにかく目的は果たせました。アップにすると「厚み」がついているのがわかると思います。

ゴム製のおもちゃっぽい

動画にするとこんな感じ。




フレームの側面部分は内側を向いた面しか張っていないため、Sphere のような閉じたモデルであれば問題ありませんが、Plane のように開いたモデルだと端の見え方がおかしくなるのでそこだけ注意してください。

では、実際のコードを見ていきましょう。

まずはワイヤーフレームに関する処理だけを別ファイル(WireframeSupport.cginc)に分離します。


// WireframeSupport.cginc

/* 下の頂点計算で使う位置情報と法線情報のセット */
struct vData {
 float3 pos;
 float3 normal;
};

/* ストリームに頂点情報を追加する */
#define ADDV(v) triStream.Append(SetVertex(v))
#define ADDTRI(v1, v2, v3) ADDV(v1); ADDV(v2); ADDV(v3); triStream.RestartStrip()

/* 三角面から立体的なワイヤーフレームを作成する */
#define GENERATE_WIREFRAME \
\
 float width = lerp(0, 2.0 / 3.0, _Width); \
 float thickness = width * _Thickness; \
 float3 triNormal = normalize(IN[0].normal + IN[1].normal + IN[2].normal); \
\
 vData v[3][4]; \
\
 for (uint i = 0; i < 3; i++) \
 { \
  int inb = (i + 0) % 3; \
  int in1 = (i + 1) % 3; \
  int in2 = (i + 2) % 3; \
\
  /* もとの頂点 */ \
  v[i][0].pos = IN[inb].vertex.xyz; \
  v[i][0].normal = normalize(IN[inb].normal); \
\
  /* もとの位置から横にずらした頂点(側面の表側の頂点) */ \
  v[i][1].pos = IN[inb].vertex.xyz + ((IN[in1].vertex.xyz + IN[in2].vertex.xyz) * 0.5 - IN[inb].vertex.xyz) * width; \
  v[i][1].normal = lerp(v[i][0].normal, triNormal, _Width); \
\
  /* 横にずらした頂点の裏(側面の裏側の頂点) */ \
  v[i][2].pos = v[i][1].pos - v[i][1].normal * thickness; \
  v[i][2].normal = -v[i][1].normal; \
\
  /* もとの頂点位置の裏 */ \
  v[i][3].pos = v[i][0].pos - v[i][0].normal * thickness; \
  v[i][3].normal = -v[i][0].normal; \
 } \
\
 /* 表面 */ \
 ADDTRI(v[1][0], v[1][1], v[0][0]); \
 ADDTRI(v[0][1], v[0][0], v[1][1]); \
 ADDTRI(v[0][0], v[0][1], v[2][0]); \
 ADDTRI(v[2][1], v[2][0], v[0][1]); \
 ADDTRI(v[2][0], v[2][1], v[1][0]); \
 ADDTRI(v[1][1], v[1][0], v[2][1]); \
\
 /* 側面 */ \
 ADDTRI(v[1][1], v[0][2], v[0][1]); \
 ADDTRI(v[0][2], v[1][1], v[1][2]); \
 ADDTRI(v[0][1], v[2][2], v[2][1]); \
 ADDTRI(v[2][2], v[0][1], v[0][2]); \
 ADDTRI(v[2][1], v[1][2], v[1][1]); \
 ADDTRI(v[1][2], v[2][1], v[2][2]); \
\
 /* 裏面 */ \
 ADDTRI(v[1][2], v[1][3], v[0][2]); \
 ADDTRI(v[0][3], v[0][2], v[1][3]); \
 ADDTRI(v[0][2], v[0][3], v[2][2]); \
 ADDTRI(v[2][3], v[2][2], v[0][3]); \
 ADDTRI(v[2][2], v[2][3], v[1][2]); \
 ADDTRI(v[1][3], v[1][2], v[2][3]); \
\


普通にシェーダ内に記述してもいいのですが、コードが煩雑になるので一応分けました。ジオメトリシェーダ内の処理を丸ごと無理やりひとつのマクロ(GENERATE_WIREFRAME)にまとめています。本来こういう書き方はすべきじゃないよなーとは思いつつ、簡単だったのでつい書いちゃいました。あまり良い例ではないのでマネはしないでください…。

次は本体です。


Shader "Custom/Wireframe Diffuse"
{
 Properties
 {
  _Color("Color", Color) = (1,1,1,1)
  _Width("Width", Range(0, 1)) = 0.2
  _Thickness("Thickness", Range(0, 1)) = 0.01
 }

 SubShader
 {
  Tags{ "Queue" = "Geometry" "RenderType" = "Opaque" }

  Pass
  {
   Tags{ "LightMode" = "ForwardBase" }
   CGPROGRAM
   #pragma target 4.0
   #pragma vertex vert
   #pragma geometry geo
   #pragma fragment frag
   #pragma multi_compile_fwdbase
   
   #include "WireframeSupport.cginc"
   #include "UnityCG.cginc"
   #include "AutoLight.cginc"

   sampler2D _MainTex;
   float4 _MainTex_ST;
   fixed4 _Color;
   fixed4 _LightColor0;

   struct vertex_input {
    float4 vertex : POSITION;
    float3 normal : NORMAL;
   };

   struct vertex_output {
    float4 pos : SV_POSITION;
    float3 lightDir : TEXCOORD1;
    float3 normal : TEXCOORD2;
    LIGHTING_COORDS(3, 4)
    float3 vertexLighting : TEXCOORD5;
   };

   vertex_input vert(vertex_input v)
   {
    return v;
   }

   vertex_output SetVertex(vData data)
   {
    vertex_input v;
    vertex_output o;

    v.vertex = float4(data.pos, 1);
    v.normal = data.normal;

    o.pos = UnityObjectToClipPos(v.vertex);
    o.lightDir = ObjSpaceLightDir(v.vertex);
    o.normal = v.normal;
    TRANSFER_VERTEX_TO_FRAGMENT(o);

    o.vertexLighting = float3(0.0, 0.0, 0.0);

    #ifdef VERTEXLIGHT_ON

    float3 worldN = mul((float3x3)unity_ObjectToWorld, SCALED_NORMAL);
    float4 worldPos = mul(unity_ObjectToWorld, v.vertex);

    for (int index = 0; index < 4; index++)
    {
     float4 lightPosition = float4(unity_4LightPosX0[index], unity_4LightPosY0[index], unity_4LightPosZ0[index], 1.0);
     float3 vertexToLightSource = float3(lightPosition.xyz - worldPos.xyz);
     float3 lightDirection = normalize(vertexToLightSource);
     float squaredDistance = dot(vertexToLightSource, vertexToLightSource);
     float attenuation = 1.0 / (1.0 + unity_4LightAtten0[index] * squaredDistance);
     float3 diffuseReflection = attenuation * float3(unity_LightColor[index].rgb) * float3(_Color.rgb) * max(0.0, dot(worldN, lightDirection));
     o.vertexLighting = o.vertexLighting + diffuseReflection * 2;
    }

    #endif

    return o;
   }

   float _Width;
   float _Thickness;

   [maxvertexcount(54)]
   void geo(triangle vertex_input IN[3], inout TriangleStream<vertex_output> triStream)
   {
    GENERATE_WIREFRAME
   }

   half4 frag(vertex_output i) : COLOR
   {
    i.lightDir = normalize(i.lightDir);
    fixed atten = LIGHT_ATTENUATION(i);

    fixed4 col = _Color + fixed4(i.vertexLighting, 1.0);

    fixed diff = saturate(dot(i.normal, i.lightDir));

    fixed4 c;
    c.rgb = UNITY_LIGHTMODEL_AMBIENT.rgb * 2 * col.rgb;
    c.rgb += (col.rgb * _LightColor0.rgb * diff) * (atten * 2);
    c.a = col.a + _LightColor0.a * atten;

    return c;
   }

   ENDCG
  }

  Pass
  {
   Tags{ "LightMode" = "ForwardAdd" }
   Blend One One
   CGPROGRAM
   #pragma target 4.0
   #pragma vertex vert
   #pragma geometry geo
   #pragma fragment frag
   #pragma multi_compile_fwdadd

   #include "WireframeSupport.cginc"
   #include "UnityCG.cginc"
   #include "AutoLight.cginc"

   struct vertex_input {
    float4 vertex : POSITION;
    float3 normal : NORMAL;
   };

   struct vertex_output {
    float4 pos : SV_POSITION;
    float3 lightDir : TEXCOORD2;
    float3 normal : TEXCOORD1;
    LIGHTING_COORDS(3, 4)
   };

   vertex_input vert(vertex_input v)
   {
    return v;
   }

   vertex_output SetVertex(vData data)
   {
    vertex_input v;
    vertex_output o;

    v.vertex = float4(data.pos, 1);
    v.normal = data.normal;

    o.pos = UnityObjectToClipPos(v.vertex);

    o.lightDir = ObjSpaceLightDir(v.vertex);

    o.normal = v.normal;
    TRANSFER_VERTEX_TO_FRAGMENT(o);

    return o;
   }

   float _Width;
   float _Thickness;

   [maxvertexcount(54)]
   void geo(triangle vertex_input IN[3], inout TriangleStream<vertex_output> triStream)
   {
    GENERATE_WIREFRAME
   }

   sampler2D _MainTex;
   fixed4 _Color;
   fixed4 _LightColor0;

   fixed4 frag(vertex_output i) : COLOR
   {
    i.lightDir = normalize(i.lightDir);
    fixed atten = LIGHT_ATTENUATION(i);
    fixed4 col = _Color;
    fixed3 normal = i.normal;
    fixed diff = saturate(dot(normal, i.lightDir));

    fixed4 c;
    c.rgb = (col.rgb * _LightColor0.rgb * diff) * (atten * 2);
    c.a = col.a;

    return c;
   }

   ENDCG
  }

  Pass
  {
   Tags{ "LightMode" = "ShadowCaster" }
   CGPROGRAM
   #pragma target 4.0
   #pragma vertex vert
   #pragma geometry geo
   #pragma fragment frag
   #pragma multi_compile_shadowcaster
   #pragma multi_compile_fwdbase

   #include "WireframeSupport.cginc"
   #include "UnityCG.cginc"
   #include "AutoLight.cginc"
   #include "UnityLightingCommon.cginc"

   struct vertex_input {
    float4 vertex : POSITION;
    float3 normal : NORMAL;
   };

   struct vertex_output {
    float4 pos : SV_POSITION;
    float3 normal : TEXCOORD0;
    fixed4 color : COLOR;
    SHADOW_COORDS(1)
   };

   vertex_input vert(vertex_input v)
   {
    return v;
   }

   fixed4 _Color;
   float _Width;
   float _Thickness;

   vertex_output SetVertex(vData data)
   {
    vertex_output o;

    o.pos = UnityObjectToClipPos(float4(data.pos, 1));
    o.normal = data.normal;
    half3 worldNormal = UnityObjectToWorldNormal(data.normal);
    half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));
    o.color = nl * _LightColor0 * _Color;
    TRANSFER_SHADOW(o)
    return o;
   }
   
   [maxvertexcount(54)]
   void geo(triangle vertex_input IN[3], inout TriangleStream<vertex_output> triStream)
   {
    GENERATE_WIREFRAME
   }

   float4 frag(vertex_output i) : SV_Target
   {
    fixed4 col = i.color;
    SHADOW_CASTER_FRAGMENT(i)
   }

   ENDCG
  }
 }
 Fallback "VertexLit"
}

めちゃくちゃ長いですが、全体の構造はシンプルです。LightMode タグに ForwardBase、ForwardAdd、ShadowCaster を指定した3つの Pass を走らせています。そしてそれぞれの Pass で vert関数、geo関数、frag関数を実行しているというわけです。参考にしたページで紹介されていた Diffuse シェーダでは ForwardBase と ForwardAdd の2つだけだったのですが、ワイヤーフレーム内部の影を正しく描画するために ShadowCaster も追加しています。

頂点シェーダは飾りです。vert 関数内を見てください。データをそのまま右から左に受け流しているだけで具体的な処理は何もしていません。頂点に関する処理は、すべてジオメトリシェーダ(geo)から呼ばれる SetVertex 関数内でまとめて行うので、頂点シェーダでは特に何もする必要がないのです。

ライティングの処理はUnity側で用意されているマクロをそのまま使っています。詳しい計算内容については正直まだまだ理解しきれていません…。もしかしたらどこか間違っていたり非効率な書き方になっているかもしれないので、あくまで参考程度にご覧ください。


モデルや色によってはホラーになるので注意

あと、今回のシェーダは Forward ベースで書いてあるので、レンダリングパスの設定に関わらず一応ちゃんと描画はされると思いますが、Deferred レンダリングの諸々の恩恵を受けるには "LightMode"="Deferred" での記述が別途必要になると思います。ぶっちゃけこれ以上は知識がまったく足りないので、今回はここまでです(お手上げ)。

0 件のコメント:

コメントを投稿