そろそろShaderをやるパート20 ジオメトリーシェーダーでポリゴンの操作を組み合わせる

7 min read読了の目安(約6600字

そろそろShaderをやります

そろそろShaderをやります。そろそろShaderをやりたいからです。
パート100までダラダラ頑張ります。10年かかってもいいのでやります。
100記事分くらい学べば私レベルの初心者でもまあまあ理解できるかなと思っています。

という感じでやってます。

※初心者がメモレベルで記録するので
 技術記事としてはお力になれないかもしれません。

下準備

下記参考
そろそろShaderをやるパート1 Unite 2017の動画を見る(基礎知識~フラグメントシェーダーで色を変える)

デモ

過去記事のものを組み合わせます。

【過去記事リンク】
そろそろShaderをやるパート16 ジオメトリーシェーダーで法線方向にポリゴンを移動させる
そろそろShaderをやるパート17 ジオメトリーシェーダーでポリゴンの大きさを変える
そろそろShaderをやるパート18 ジオメトリーシェーダーでポリゴンごとに色を変える
そろそろShaderをやるパート19 ジオメトリーシェーダーでポリゴンを回転させる

下記デモは失敗版です。

望んだ挙動としては下記でした。
・その場で各ポリゴンが回転
・その場で各ポリゴンのスケールを変更
・頂点を動かしても色は変わらない

次にうまくいったデモです。
回転、拡縮が望み通り各ポリゴンの中心で行われており、色も動くたびに変化しなくなりました。

Shaderサンプル

Shader "Custom/GeometryTest"
{
    Properties
    {
        _ScaleFactor ("Scale Factor", Range(0,1.0)) = 0.5
        _PositionFactor("Position Factor", Range(0,1.0)) = 0.5
        _RotationFactor ("Rotation Factor", Range(0,1.0)) = 0.5
    }
    SubShader
    {
        Tags
        {
            "RenderType"="Opaque"
        }
        
        //両面描画
        Cull Off

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma geometry geom
            #pragma fragment frag

            #include "UnityCG.cginc"

            float _PositionFactor;
            float _RotationFactor;
            float _ScaleFactor;

            struct appdata
            {
                float4 vertex : POSITION;
                float3 localPos : TEXCOORD0;
            };

            //頂点シェーダー
            appdata vert(appdata v)
            {
                appdata o;
                o.localPos = v.vertex.xyz; //ジオメトリーシェーダーで頂点を動かす前に"描画しようとしているピクセル"のローカル座標を保持しておく
                return v;
            }

            struct g2f
            {
                float4 vertex : SV_POSITION;
                fixed4 color : COLOR;
            };

            //回転させる
            //pは回転させたい座標 angleは回転させる角度 axisはどの軸を元に回転させるか 
            float3 rotate(float3 p, float angle, float3 axis)
            {
                float3 a = normalize(axis);
                float s = sin(angle);
                float c = cos(angle);
                float r = 1.0 - c;
                float3x3 m = float3x3(
                    a.x * a.x * r + c, a.y * a.x * r + a.z * s, a.z * a.x * r - a.y * s,
                    a.x * a.y * r - a.z * s, a.y * a.y * r + c, a.z * a.y * r + a.x * s,
                    a.x * a.z * r + a.y * s, a.y * a.z * r - a.x * s, a.z * a.z * r + c
                );

                return mul(m, p);
            }

            //ランダムな値を返す
            float rand(float2 co)
            {
                return frac(sin(dot(co.xy, float2(12.9898, 78.233))) * 43758.5453);
            }

            // ジオメトリシェーダー
            [maxvertexcount(3)]
            void geom(triangle appdata input[3], uint pid : SV_PrimitiveID,inout TriangleStream<g2f> stream)
            {
                // 法線を計算
                float3 vec1 = input[1].vertex - input[0].vertex;
                float3 vec2 = input[2].vertex - input[0].vertex;
                float3 normal = normalize(cross(vec1, vec2));

                //1枚のポリゴンの中心
                float3 center = (input[0].vertex + input[1].vertex + input[2].vertex) / 3;
                float random = 2.0 * rand(center.xy) - 0.5;
                float3 r3 = random.xxx;
              
                [unroll]
                for (int i = 0; i < 3; i++)
                {
                    appdata v = input[i];
                    g2f o;
                    //移動に利用する位置ベクトルを保持
                    // float3 currentPos = normal * _PositionFactor * abs(r3); //負の値は法線と逆方向に移動してしまうので絶対値利用
                    // //法線ベクトルに沿って頂点を移動
                    // v.vertex.xyz += currentPos;
                    // //回転させる
                    // v.vertex.xyz = currentPos + center + rotate(v.vertex.xyz - center - currentPos, (pid + _Time.y) * _RotationFactor, r3);
                    // //中心を起点にスケールを変える
                    // v.vertex.xyz = currentPos + center + (v.vertex.xyz - center - currentPos) * (1.0 - _ScaleFactor);

                    //こっちの方がすっきり 順番を変えただけ
                    v.vertex.xyz = center + rotate(v.vertex.xyz - center, (pid + _Time.y) * _RotationFactor, r3);
                    v.vertex.xyz = center + (v.vertex.xyz - center) * (1.0 - _ScaleFactor);
                    v.vertex.xyz += normal * _PositionFactor * abs(r3);
                    
                    // NGパターン
                    // v.vertex.xyz += normal * _PositionFactor * abs(r3);
                    // v.vertex.xyz = center + rotate(v.vertex.xyz - center, (pid + _Time.y) * _RotationFactor, r3);
                    // v.vertex.xyz = center + (v.vertex.xyz - center) * (1.0 - _ScaleFactor);
                    
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    //ランダムな値
                    //シード値にワールド座標を利用すると移動するたびに色が変わってやかましいのでローカル座標を利用
                    float r = rand(v.localPos.xy);
                    float g = rand(v.localPos.xz);
                    float b = rand(v.localPos.yz);
                    
                    // NGパターン
                    // float r = rand(v.vertex.xy);
                    // float g = rand(v.vertex.xz);
                    // float b = rand(v.vertex.yz);
                    o.color = fixed4(r,g,b,1);
                    stream.Append(o);
                }
            }

            //フラグメントシェーダー
            fixed4 frag(g2f i) : SV_Target
            {
                return i.color;
            }
            ENDCG
        }
    }
}

動くたびに変化する色

色が動くたびに変化するのは、
自分で定義したランダムな値を返すrand関数にワールド座標を渡していたのが原因でした。

ワールド座標は頂点が動くたびに変化するので当然でした。
ですので、頂点シェーダーで一度ローカル座標を保持しておいて
rand関数のシード値として利用しています。

中心を原点とした動きをしないポリゴン

ポリゴンの頂点を動かしてから回転やスケールの変更を行う
中心を原点とした動きが破綻しました。

理由は単純で、その後の処理で参照しているのは頂点を動かす前のポリゴンの中心だからです。
ですので、あらかじめ移動に利用した位置ベクトルを保持しておくか、
ポリゴンの頂点の移動の処理を最後に行うかをすれば
望んだとおりポリゴンの中心を原点として動いてくれました。

参考リンク

ジオメトリシェーダ入門
HoloLens で使える Near Clip 表現について解説してみた
【Unity】Gemoetry Shaderで3Dモデルがさぁ〜って消えるソースを紹介【影あり】