2019年2月3日

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


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



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

前回作成したシェーダ

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

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

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

今回作成したシェーダ

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

ゴム製のおもちゃっぽい

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




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

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

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


  1. // WireframeSupport.cginc
  2.  
  3. /* 下の頂点計算で使う位置情報と法線情報のセット */
  4. struct vData {
  5.  float3 pos;
  6.  float3 normal;
  7. };
  8.  
  9. /* ストリームに頂点情報を追加する */
  10. #define ADDV(v) triStream.Append(SetVertex(v))
  11. #define ADDTRI(v1, v2, v3) ADDV(v1); ADDV(v2); ADDV(v3); triStream.RestartStrip()
  12.  
  13. /* 三角面から立体的なワイヤーフレームを作成する */
  14. #define GENERATE_WIREFRAME \
  15. \
  16. float width = lerp(0, 2.0 / 3.0, _Width); \
  17. float thickness = width * _Thickness; \
  18. float3 triNormal = normalize(IN[0].normal + IN[1].normal + IN[2].normal); \
  19. \
  20. vData v[3][4]; \
  21. \
  22. for (uint i = 0; i < 3; i++) \
  23.  { \
  24.   int inb = (i + 0) % 3; \
  25.   int in1 = (i + 1) % 3; \
  26.   int in2 = (i + 2) % 3; \
  27. \
  28.   /* もとの頂点 */ \
  29.   v[i][0].pos = IN[inb].vertex.xyz; \
  30.   v[i][0].normal = normalize(IN[inb].normal); \
  31. \
  32.   /* もとの位置から横にずらした頂点(側面の表側の頂点) */ \
  33.   v[i][1].pos = IN[inb].vertex.xyz + ((IN[in1].vertex.xyz + IN[in2].vertex.xyz) * 0.5 - IN[inb].vertex.xyz) * width; \
  34.   v[i][1].normal = lerp(v[i][0].normal, triNormal, _Width); \
  35. \
  36.   /* 横にずらした頂点の裏(側面の裏側の頂点) */ \
  37.   v[i][2].pos = v[i][1].pos - v[i][1].normal * thickness; \
  38.   v[i][2].normal = -v[i][1].normal; \
  39. \
  40.   /* もとの頂点位置の裏 */ \
  41.   v[i][3].pos = v[i][0].pos - v[i][0].normal * thickness; \
  42.   v[i][3].normal = -v[i][0].normal; \
  43.  } \
  44. \
  45. /* 表面 */ \
  46. ADDTRI(v[1][0], v[1][1], v[0][0]); \
  47. ADDTRI(v[0][1], v[0][0], v[1][1]); \
  48. ADDTRI(v[0][0], v[0][1], v[2][0]); \
  49. ADDTRI(v[2][1], v[2][0], v[0][1]); \
  50. ADDTRI(v[2][0], v[2][1], v[1][0]); \
  51. ADDTRI(v[1][1], v[1][0], v[2][1]); \
  52. \
  53. /* 側面 */ \
  54. ADDTRI(v[1][1], v[0][2], v[0][1]); \
  55. ADDTRI(v[0][2], v[1][1], v[1][2]); \
  56. ADDTRI(v[0][1], v[2][2], v[2][1]); \
  57. ADDTRI(v[2][2], v[0][1], v[0][2]); \
  58. ADDTRI(v[2][1], v[1][2], v[1][1]); \
  59. ADDTRI(v[1][2], v[2][1], v[2][2]); \
  60. \
  61. /* 裏面 */ \
  62. ADDTRI(v[1][2], v[1][3], v[0][2]); \
  63. ADDTRI(v[0][3], v[0][2], v[1][3]); \
  64. ADDTRI(v[0][2], v[0][3], v[2][2]); \
  65. ADDTRI(v[2][3], v[2][2], v[0][3]); \
  66. ADDTRI(v[2][2], v[2][3], v[1][2]); \
  67. ADDTRI(v[1][3], v[1][2], v[2][3]); \
  68. \


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

次は本体です。


  1. Shader "Custom/Wireframe Diffuse"
  2. {
  3. Properties
  4. {
  5.   _Color("Color", Color) = (1,1,1,1)
  6.   _Width("Width", Range(0, 1)) = 0.2
  7.   _Thickness("Thickness", Range(0, 1)) = 0.01
  8. }
  9.  
  10. SubShader
  11. {
  12.   Tags{ "Queue" = "Geometry" "RenderType" = "Opaque" }
  13.  
  14.   Pass
  15.   {
  16.    Tags{ "LightMode" = "ForwardBase" }
  17.    CGPROGRAM
  18.    #pragma target 4.0
  19.    #pragma vertex vert
  20.    #pragma geometry geo
  21.    #pragma fragment frag
  22.    #pragma multi_compile_fwdbase
  23.    
  24.    #include "WireframeSupport.cginc"
  25.    #include "UnityCG.cginc"
  26.    #include "AutoLight.cginc"
  27.  
  28.    sampler2D _MainTex;
  29.    float4 _MainTex_ST;
  30.    fixed4 _Color;
  31.    fixed4 _LightColor0;
  32.  
  33.    struct vertex_input {
  34.     float4 vertex : POSITION;
  35.     float3 normal : NORMAL;
  36.    };
  37.  
  38.    struct vertex_output {
  39.     float4 pos : SV_POSITION;
  40.     float3 lightDir : TEXCOORD1;
  41.     float3 normal : TEXCOORD2;
  42.     LIGHTING_COORDS(3, 4)
  43.     float3 vertexLighting : TEXCOORD5;
  44.    };
  45.  
  46.    vertex_input vert(vertex_input v)
  47.    {
  48.     return v;
  49.    }
  50.  
  51.    vertex_output SetVertex(vData data)
  52.    {
  53.     vertex_input v;
  54.     vertex_output o;
  55.  
  56.     v.vertex = float4(data.pos, 1);
  57.     v.normal = data.normal;
  58.  
  59.     o.pos = UnityObjectToClipPos(v.vertex);
  60.     o.lightDir = ObjSpaceLightDir(v.vertex);
  61.     o.normal = v.normal;
  62.     TRANSFER_VERTEX_TO_FRAGMENT(o);
  63.  
  64.     o.vertexLighting = float3(0.0, 0.0, 0.0);
  65.  
  66.     #ifdef VERTEXLIGHT_ON
  67.  
  68.     float3 worldN = mul((float3x3)unity_ObjectToWorld, SCALED_NORMAL);
  69.     float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
  70.  
  71.     for (int index = 0; index < 4; index++)
  72.     {
  73.      float4 lightPosition = float4(unity_4LightPosX0[index], unity_4LightPosY0[index], unity_4LightPosZ0[index], 1.0);
  74.      float3 vertexToLightSource = float3(lightPosition.xyz - worldPos.xyz);
  75.      float3 lightDirection = normalize(vertexToLightSource);
  76.      float squaredDistance = dot(vertexToLightSource, vertexToLightSource);
  77.      float attenuation = 1.0 / (1.0 + unity_4LightAtten0[index] * squaredDistance);
  78.      float3 diffuseReflection = attenuation * float3(unity_LightColor[index].rgb) * float3(_Color.rgb) * max(0.0, dot(worldN, lightDirection));
  79.      o.vertexLighting = o.vertexLighting + diffuseReflection * 2;
  80.     }
  81.  
  82.     #endif
  83.  
  84.     return o;
  85.    }
  86.  
  87.    float _Width;
  88.    float _Thickness;
  89.  
  90.    [maxvertexcount(54)]
  91.    void geo(triangle vertex_input IN[3], inout TriangleStream<vertex_output> triStream)
  92.    {
  93.     GENERATE_WIREFRAME
  94.    }
  95.  
  96.    half4 frag(vertex_output i) : COLOR
  97.    {
  98.     i.lightDir = normalize(i.lightDir);
  99.     fixed atten = LIGHT_ATTENUATION(i);
  100.  
  101.     fixed4 col = _Color + fixed4(i.vertexLighting, 1.0);
  102.  
  103.     fixed diff = saturate(dot(i.normal, i.lightDir));
  104.  
  105.     fixed4 c;
  106.     c.rgb = UNITY_LIGHTMODEL_AMBIENT.rgb * 2 * col.rgb;
  107.     c.rgb += (col.rgb * _LightColor0.rgb * diff) * (atten * 2);
  108.     c.a = col.a + _LightColor0.a * atten;
  109.  
  110.     return c;
  111.    }
  112.  
  113.    ENDCG
  114.   }
  115.  
  116.   Pass
  117.   {
  118.    Tags{ "LightMode" = "ForwardAdd" }
  119.    Blend One One
  120.    CGPROGRAM
  121.    #pragma target 4.0
  122.    #pragma vertex vert
  123.    #pragma geometry geo
  124.    #pragma fragment frag
  125.    #pragma multi_compile_fwdadd
  126.  
  127.    #include "WireframeSupport.cginc"
  128.    #include "UnityCG.cginc"
  129.    #include "AutoLight.cginc"
  130.  
  131.    struct vertex_input {
  132.     float4 vertex : POSITION;
  133.     float3 normal : NORMAL;
  134.    };
  135.  
  136.    struct vertex_output {
  137.     float4 pos : SV_POSITION;
  138.     float3 lightDir : TEXCOORD2;
  139.     float3 normal : TEXCOORD1;
  140.     LIGHTING_COORDS(3, 4)
  141.    };
  142.  
  143.    vertex_input vert(vertex_input v)
  144.    {
  145.     return v;
  146.    }
  147.  
  148.    vertex_output SetVertex(vData data)
  149.    {
  150.     vertex_input v;
  151.     vertex_output o;
  152.  
  153.     v.vertex = float4(data.pos, 1);
  154.     v.normal = data.normal;
  155.  
  156.     o.pos = UnityObjectToClipPos(v.vertex);
  157.  
  158.     o.lightDir = ObjSpaceLightDir(v.vertex);
  159.  
  160.     o.normal = v.normal;
  161.     TRANSFER_VERTEX_TO_FRAGMENT(o);
  162.  
  163.     return o;
  164.    }
  165.  
  166.    float _Width;
  167.    float _Thickness;
  168.  
  169.    [maxvertexcount(54)]
  170.    void geo(triangle vertex_input IN[3], inout TriangleStream<vertex_output> triStream)
  171.    {
  172.     GENERATE_WIREFRAME
  173.    }
  174.  
  175.    sampler2D _MainTex;
  176.    fixed4 _Color;
  177.    fixed4 _LightColor0;
  178.  
  179.    fixed4 frag(vertex_output i) : COLOR
  180.    {
  181.     i.lightDir = normalize(i.lightDir);
  182.     fixed atten = LIGHT_ATTENUATION(i);
  183.     fixed4 col = _Color;
  184.     fixed3 normal = i.normal;
  185.     fixed diff = saturate(dot(normal, i.lightDir));
  186.  
  187.     fixed4 c;
  188.     c.rgb = (col.rgb * _LightColor0.rgb * diff) * (atten * 2);
  189.     c.a = col.a;
  190.  
  191.     return c;
  192.    }
  193.  
  194.    ENDCG
  195.   }
  196.  
  197.   Pass
  198.   {
  199.    Tags{ "LightMode" = "ShadowCaster" }
  200.    CGPROGRAM
  201.    #pragma target 4.0
  202.    #pragma vertex vert
  203.    #pragma geometry geo
  204.    #pragma fragment frag
  205.    #pragma multi_compile_shadowcaster
  206.    #pragma multi_compile_fwdbase
  207.  
  208.    #include "WireframeSupport.cginc"
  209.    #include "UnityCG.cginc"
  210.    #include "AutoLight.cginc"
  211.    #include "UnityLightingCommon.cginc"
  212.  
  213.    struct vertex_input {
  214.     float4 vertex : POSITION;
  215.     float3 normal : NORMAL;
  216.    };
  217.  
  218.    struct vertex_output {
  219.     float4 pos : SV_POSITION;
  220.     float3 normal : TEXCOORD0;
  221.     fixed4 color : COLOR;
  222.     SHADOW_COORDS(1)
  223.    };
  224.  
  225.    vertex_input vert(vertex_input v)
  226.    {
  227.     return v;
  228.    }
  229.  
  230.    fixed4 _Color;
  231.    float _Width;
  232.    float _Thickness;
  233.  
  234.    vertex_output SetVertex(vData data)
  235.    {
  236.     vertex_output o;
  237.  
  238.     o.pos = UnityObjectToClipPos(float4(data.pos, 1));
  239.     o.normal = data.normal;
  240.     half3 worldNormal = UnityObjectToWorldNormal(data.normal);
  241.     half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));
  242.     o.color = nl * _LightColor0 * _Color;
  243.     TRANSFER_SHADOW(o)
  244.     return o;
  245.    }
  246.    
  247.    [maxvertexcount(54)]
  248.    void geo(triangle vertex_input IN[3], inout TriangleStream<vertex_output> triStream)
  249.    {
  250.     GENERATE_WIREFRAME
  251.    }
  252.  
  253.    float4 frag(vertex_output i) : SV_Target
  254.    {
  255.     fixed4 col = i.color;
  256.     SHADOW_CASTER_FRAGMENT(i)
  257.    }
  258.  
  259.    ENDCG
  260.   }
  261. }
  262. Fallback "VertexLit"
  263. }

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

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

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


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

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

0 件のコメント:

コメントを投稿