この記事ではカメラとの距離を使って、3Dモデルをポリゴン単位で分解する方法を紹介します。
カメラとの距離を使ってオブジェクトに変化をつける
シェーダー内の値を変化させることで描画結果に動きをつけることができます。
今回は変化する値としてカメラとオブジェクトの距離を使いたいと思います。
カメラが動くことによってオブジェクトの色が変化するといった演出を作ってみましょう。
Projectウィンドウの「Create/Shader/Unlit Shader」からシェーダーファイルを新規作成します。
作成したシェーダーファイルを以下のように書き換えてください。
Shader "Custom/Color Gradient" { Properties { _Dsitance ("Distance", float) = 3.0 _FarColor ("Far Color", Color) = (0, 0, 0, 1) _NearColor ("Near Color", Color) = (1, 1, 1, 1) } SubShader { Tags { "RenderType"="Opaque" } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" float _Dsitance; fixed4 _FarColor; fixed4 _NearColor; struct appdata { float4 vertex : POSITION; }; struct v2f { float4 vertex : SV_POSITION; float3 worldPos : TEXCOORD0; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.worldPos = mul(unity_ObjectToWorld, v.vertex); // ローカル座標系をワールド座標系に変換 return o; } fixed4 frag (v2f i) : SV_Target { // カメラとオブジェクトの距離(長さ)を取得 float dist = length(_WorldSpaceCameraPos - i.worldPos); // Lerp(線形補間)を使って色を変化 fixed4 col = fixed4(lerp(_NearColor.rgb, _FarColor.rgb, dist/_Dsitance), 1); return col; } ENDCG } } }
このシェーダーではVertexシェーダー内でローカル座標系をワールド座標系に変換しFragmentシェーダーに渡しています。
そして、Fragmentシェーダー内でカメラとオブジェクトの距離を計算し、その値によってLerp関数を使いオブジェクトの色を変化させています。
// カメラとオブジェクトの距離(長さ)を取得 float dist = length(_WorldSpaceCameraPos - i.worldPos); // Lerp(線形補間)を使って色を変化 fixed4 col = fixed4(lerp(_NearColor.rgb, _FarColor.rgb, dist/_Dsitance), 1);
これでカメラとの距離によってオブジェクトに変化をつけられるようになりました。
ポリゴン単位で動かす
Geometryシェーダーを使うことでポリゴン単位で動きをつけることができます。
シェーダー内では以下の順で処理されます。
- Vertexシェーダー
- Geometryシェーダー
- Fragmentシェーダー
注意点として、Web Player環境ではWebGLの仕様上Geometryシェーダーが機能しません。
STYLYで使用する際にはSTYLY Studioでは動作しませんが、VR上では動作しますので気を付けてください。
先ほどと同じようにシェーダーファイルを新規作成し、作成したシェーダーファイルを以下のように書き換えてください。
Shader "Custom/Simple Geometry" { Properties { _Color ("Color", Color) = (1, 1, 1, 1) } SubShader { Tags { "RenderType"="Opaque" } Pass { CGPROGRAM #pragma vertex vert #pragma geometry geom #pragma fragment frag #include "UnityCG.cginc" fixed4 _Color; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct g2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; }; appdata vert (appdata v) { return v; } // ジオメトリシェーダー [maxvertexcount(3)] void geom (triangle appdata input[3], inout TriangleStream<g2f> stream) { [unroll] for(int i = 0; i < 3; i++) { appdata v = input[i]; g2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; stream.Append(o); } stream.RestartStrip(); } fixed4 frag (g2f i) : SV_Target { fixed4 col = _Color; return col; } ENDCG } } FallBack "Unlit/Color" }
このシェーダー自体は何も動きをつけていない、「Geometryシェーダーってこうやって書くんだ~」というサンプルです。
_SinTimeとGeometryシェーダーを使って動きをつけてみましょう。
Shader "Custom/Polygon Moving" { Properties { _Color ("Color", Color) = (1, 1, 1, 1) _ScaleFactor ("Scale Factor", float) = 0.5 } SubShader { Tags { "RenderType"="Opaque" } Pass { CGPROGRAM #pragma vertex vert #pragma geometry geom #pragma fragment frag #include "UnityCG.cginc" fixed4 _Color; float _ScaleFactor; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct g2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; }; appdata vert (appdata v) { return v; } // ジオメトリシェーダー [maxvertexcount(3)] void geom (triangle appdata input[3], 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)); [unroll] for(int i = 0; i < 3; i++) { appdata v = input[i]; g2f o; // 法線ベクトルに沿って頂点を移動 v.vertex.xyz += normal * (_SinTime.w * 0.5 + 0.5) * _ScaleFactor; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; stream.Append(o); } stream.RestartStrip(); } fixed4 frag (g2f i) : SV_Target { fixed4 col = _Color; return col; } ENDCG } } FallBack "Unlit/Color" }
先ほどのシンプルなGeometryシェーダーに「法線ベクトルの計算」と「_SinTimeを使った頂点移動処理」を足しています。
それでは一つずつ説明していきます。
法線ベクトルの計算
法線ベクトルの計算には外積を使用します。外積を使うことで平行でない2つのベクトルに垂直なベクトルを求めることができます。
言い換えると、3つの頂点を持つ三角ポリゴンが存在し、1つの頂点からそれぞれ異なる2つの頂点に伸びるベクトルに垂直なベクトル = 三角ポリゴンの法線ベクトルとなるのです。
// 法線ベクトルを計算 float3 vec1 = input[1].vertex - input[0].vertex; float3 vec2 = input[2].vertex - input[0].vertex; float3 normal = normalize(cross(vec1, vec2));
_SinTimeを使ったアニメーション
次にアニメーションの処理ですが、_SinTimeは時間のサイン関数です。時間変化で-1.0から1.0の間を滑らかに行ったり来たりします。
こうしたUnityがあらかじめ用意している値(定義済みの値)を使うことで計算を省略することができます。
Unity – マニュアル: ShaderLab 定義済みの値
今回はマイナスの値を取りたくないので0.0から1.0の間を滑らかに行ったり来たりするように正規化してあげます。
最小値-1.0に0.5を乗算すると-0.5になり、さらに0.5を加算すると0.0になります。同様の計算を最大値1.0に行うと1.0になります。(結果的に変化しません)
_SinTime.w * 0.5 + 0.5
これで-1.0から1.0の間で変化する値が0.0から1.0の間で変化するようになりました。
最後に法線ベクトルに_SinTimeによる変化する値を乗算したものを三角ポリゴンの各頂点座標に加算することでポリゴン単位で法線ベクトル方向に分解するような動きをつけます。
// 法線ベクトルに沿って頂点を移動 v.vertex.xyz += normal * (_SinTime.w * 0.5 + 0.5) * _ScaleFactor;
Polygon Destructionシェーダーのプログラム
これまでに説明した「カメラとオブジェクトの距離」と「Geometryシェーダー」を組み合わせてカメラが近づくと弾けるポリゴン分解シェーダーを作ります。
シェーダーファイルを新規作成し、作成したシェーダーファイルを以下のように書き換えてください。
Shader "Custom/Polygon Destruction" { Properties { _FarColor ("Far Color", Color) = (1, 1, 1, 1) _NearColor ("Near Color", Color) = (0, 0, 0, 1) _ScaleFactor ("Scale Factor", float) = 0.5 _StartDistance ("Start Distance", float) = 3.0 } SubShader { Tags { "RenderType"="Opaque" } Pass { CGPROGRAM #pragma vertex vert #pragma geometry geom #pragma fragment frag #include "UnityCG.cginc" fixed4 _FarColor; fixed4 _NearColor; fixed _ScaleFactor; fixed _StartDistance; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct g2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; fixed4 color : COLOR; }; // ランダムな値を返す float rand(float2 seed) { return frac(sin(dot(seed.xy, float2(12.9898, 78.233))) * 43758.5453); } appdata vert (appdata v) { return v; } // ジオメトリシェーダー [maxvertexcount(3)] void geom (triangle appdata input[3], inout TriangleStream<g2f> stream) { // カメラとポリゴンの重心の距離 float3 center = (input[0].vertex + input[1].vertex + input[2].vertex) / 3; float4 worldPos = mul(unity_ObjectToWorld, float4(center, 1.0)); float3 dist = length(_WorldSpaceCameraPos - worldPos); // 法線を計算 float3 vec1 = input[1].vertex - input[0].vertex; float3 vec2 = input[2].vertex - input[0].vertex; float3 normal = normalize(cross(vec1, vec2)); // カメラとの距離によってポリゴン分解を変化 fixed destruction = clamp(_StartDistance - dist, 0.0, 1.0); // カメラとの距離によって色を変化 fixed gradient = clamp(dist - _StartDistance, 0.0, 1.0); fixed random = rand(center.xy); fixed3 random3 = random.xxx; [unroll] for(int i = 0; i < 3; i++) { appdata v = input[i]; g2f o; // 法線ベクトルに沿って頂点を移動 v.vertex.xyz += normal * destruction * _ScaleFactor * random3; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; // Lerp(線形補間)を使って色を変化 o.color = fixed4(lerp(_NearColor.rgb, _FarColor.rgb, gradient), 1); stream.Append(o); } stream.RestartStrip(); } fixed4 frag (g2f i) : SV_Target { fixed4 col = i.color; return col; } ENDCG } } FallBack "Unlit/Color" }
このシェーダー内で行っている処理は先ほど説明した「カメラとの距離」と「ポリゴン単位での処理」の組み合わせです。
Polygon Destrucionシェーダーを使った演出
今回はカメラとの距離を使ってシェーダー内でポリゴン単位で分解する動きをつけましたが、_SinTimeやAnimatorからパラメーターを変化させて動きをつけることも可能です。
Animatorから制御することで動きに強弱をつけてアニメーションさせることができます。
STYLYへアップロード
自作のシェーダーで作成したMaterialをオブジェクトにつけた状態でアップロードすることで、STYLY Scene内で使用できるようになります。
STYLYにアップロードする方法はこちらの記事で詳しく解説しています。
一見すると複雑なプログラムでもいくつかの要素の組み合わせであることが多いので、何が鍵なのか見分けられるようになると自由にシェーダーを使いこなせるようになります。