UnityTexturePaintでリアルタイムにオブジェクトを変形する
はじめに
UnityTexturePaintは、リアルタイム(ゲーム実行中)にオブジェクトの持つテクスチャに対してペイントを行うアセットです。
UnityTexturePaintは以下で公開しています。
基本的には
- メインテクスチャへのペイント
- ノーマルマップへのペイント
- ハイトマップへのペイント
をサポートしていますが、応用次第でとにかく色々できそうな予感がします。
ちょっとした凹凸を表現するために頂点を増やしてしまうと、ザラザラしたオブジェクトの場合頂点数がスゴイことになってしまいます これを解決できるのがバンプマップです。 バンプマップでは、面の細かな法線や高さの情報をテクスチャに格納しておき、描画の際にその法線と高さを考慮したシェーディングを行うことで、実際には無い凹凸を表現する方法です。
この画像はノーマルマップのみを利用した凹凸表現です。 実際のモデルはただの立方体ですが、ノーマルマップのお陰で凹凸があるように見えます。
ペイントしている緑の液体も、ノーマルマップペイント機能のみ利用しています。
一方こっちはハイトマップも併用しています。 オブジェクトを近くで見た際の表現が段違いです。
実際に浮き出ているように見えます。すごい。
しかし、バンプマップは実際に凹凸があるわけではなく、オブジェクト表面の細かい凹凸を表現するのに適した方法です。 よりダイナミックに、実際に頂点を変形させたい場合が往々にして存在します。
こうなると実際に頂点を弄っていく必要があるのですが、今回は変位マップを動的に変化させ、その変位マップを参照して頂点を変形させる方法について書いていきます。
実際にやってみると、こんな感じの表現が出来るようになります。 キャラが動いたらその部分を凹ませる。弾丸が壁を撃ち抜いたらそこを凹ませる。等、シンプルですが使いみちが結構あるかと思います。
VTF
頂点変形の方法はいくつもあるのですが、VTF(Vertex Texture Fetch)とDisplacementMappingについて書きます。
まずVTFですが、名前の通り、頂点シェーダーでテクスチャを参照する手法です。 変形の手順ですが
- 変形させたい位置に対してUnityTexturePaintの機能を使ってテクスチャにペイント処理を施す
- ペイントされたテクスチャを頂点シェーダーで参照し頂点位置を調整する
という感じになります。
シェーダーは以下のような感じ。
Shader "Custom/VTFHeightTransform" { Properties{ _MainTex("Base (RGB)", 2D) = "white" {} _ParallaxMap("ParallaxMap", 2D) = "white" {} _ParallaxScale("ParallaxScale", RANGE(0,10)) = 1 } SubShader{ Tags{ "RenderType" = "Opaque" } LOD 200 CGPROGRAM #pragma target 3.0 #pragma vertex vert #pragma surface surf BlinnPhong sampler2D _MainTex; sampler2D _ParallaxMap; float _ParallaxScale; struct Input { float2 uv_MainTex; }; void vert(inout appdata_full v) { float4 tex = tex2Dlod(_ParallaxMap, float4(v.texcoord.xy,0,0)); v.vertex.y += tex.r * _ParallaxScale; } void surf(Input IN, inout SurfaceOutput o) { half4 tex = tex2D(_MainTex, IN.uv_MainTex); o.Albedo = tex.rgb; o.Alpha = tex.a; } ENDCG } FallBack "Diffuse" }
頂点シェーダーでtex2Dlodを使って、テクスチャの情報を読み込み、頂点のy座標を移動させているだけの単純なものです。
RigitBodyを付けた球に対してUnityTexturePaintのCollisionPainterを付け、床に上記のシェーダーとDynamicCanvasを付けて各種設定を行って実行するとこんな感じになります。
DisplacementMapping
VTFでは、床(Plane)の頂点数が少ないため、変位させるとなんだかカクカクしてしまっています。
これを解決するために、Tessellationを行います。
Tessellationは以下の記事で。
DisplacementMappingの意味合いが「変位を与えるための情報(テクスチャ)を使って頂点を変形する手法」みたいな意味合いっぽい(?)ので、先程のVTFでの変形も広義のDisplacementMappingなのかなとか色々細かいツッコミがきそうですが、そこらへんはよくわかりません。
変形の手順はVTFと変わりません。シェーダーだけがTessellationを利用するものに変わります
シェーダーは以下のような感じ。
Shader "Custom/TessellateHeightTransform" { Properties{ _TessFactor("Tess Factor",Vector) = (2,2,2,2) _LODFactor("LOD Factor",range(0,10)) = 1 _MainTex("Main Texture", 2D) = "white" {} _ParallaxMap("ParallaxMap", 2D) = "white" {} _ParallaxScale("ParallaxScale", RANGE(0,10)) = 1 } SubShader{ Pass{ Tags {"LightMode"="ForwardBase"} CGPROGRAM #include "UnityCG.cginc" #include "UnityLightingCommon.cginc" #pragma vertex VS #pragma fragment FS #pragma hull HS #pragma domain DS #define INPUT_PATCH_SIZE 3 #define OUTPUT_PATCH_SIZE 3 uniform vector _TessFactor; uniform float _LODFactor; uniform sampler2D _MainTex; uniform sampler2D _ParallaxMap; uniform float _ParallaxScale; struct appdata { float4 w_vert:POSITION; float2 texcoord:TEXCOORD0; float4 diffuse : COLOR0; float3 normal:NORMAL; }; struct v2h { float4 pos:POS; float2 texcoord:TEXCOORD0; float4 diffuse : COLOR0; float3 normal:NORMAL; }; struct h2d_main { float3 pos:POS; float2 texcoord:TEXCOORD0; float4 diffuse : COLOR0; float3 normal:NORMAL; }; struct h2d_const { float tess_factor[3] : SV_TessFactor; float InsideTessFactor : SV_InsideTessFactor; }; struct d2f { float4 pos:SV_Position; float2 texcoord:TEXCOORD0; float4 diffuse : COLOR0; float3 normal:NORMAL; }; struct f_input { float4 vertex:SV_Position; float2 texcoord:TEXCOORD0; float4 diffuse : COLOR0; float3 normal:NORMAL; }; v2h VS(appdata i) { v2h o = (v2h)0; o.pos = mul(UNITY_MATRIX_MV, float4(i.w_vert.xyz, 1.0f)); o.texcoord = i.texcoord; o.normal = mul(UNITY_MATRIX_IT_MV, i.normal); return o; } h2d_const HSConst(InputPatch<v2h, INPUT_PATCH_SIZE> i) { h2d_const o = (h2d_const)0; o.tess_factor[0] = _TessFactor.x * _LODFactor; o.tess_factor[1] = _TessFactor.y * _LODFactor; o.tess_factor[2] = _TessFactor.z * _LODFactor; o.InsideTessFactor = _TessFactor.w * _LODFactor; return o; } [domain("tri")] [partitioning("integer")] [outputtopology("triangle_cw")] [outputcontrolpoints(OUTPUT_PATCH_SIZE)] [patchconstantfunc("HSConst")] h2d_main HS(InputPatch<v2h, INPUT_PATCH_SIZE> i, uint id:SV_OutputControlPointID) { h2d_main o = (h2d_main)0; o.pos = i[id].pos; o.texcoord = i[id].texcoord; o.diffuse = i[id].diffuse; o.normal = i[id].normal; return o; } [domain("tri")] d2f DS(h2d_const hs_const_data, const OutputPatch<h2d_main, OUTPUT_PATCH_SIZE> i, float3 bary:SV_DomainLocation) { d2f o = (d2f)0; float3 pos = i[0].pos * bary.x + i[1].pos * bary.y + i[2].pos * bary.z; float2 uv = i[0].texcoord * bary.x + i[1].texcoord * bary.y + i[2].texcoord * bary.z; float3 normal = i[0].normal * bary.x + i[1].normal * bary.y + i[2].normal * bary.z; float parallax = tex2Dlod(_ParallaxMap, float4(uv.xy, 0, 0)); pos.y += parallax * _ParallaxScale; float parallax_dx = tex2Dlod(_ParallaxMap, float4(float2(uv.x + 0.01f, uv.y), 0, 0)); float parallax_dy = tex2Dlod(_ParallaxMap, float4(float2(uv.x, uv.y + 0.01f), 0, 0)); float dx = 1 - parallax - parallax_dx; float dy = 1 - parallax - parallax_dy; half3 worldNormal = UnityObjectToWorldNormal(normal); half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz)); o.diffuse = nl * _LightColor0 * dx * dy; o.diffuse.rgb += ShadeSH9(half4(worldNormal,1)); o.pos = mul(UNITY_MATRIX_P, float4(pos, 1)); o.texcoord = uv; o.normal = worldNormal; return o; } float4 FS(f_input i) : SV_Target { float4 col = tex2D(_MainTex, i.texcoord); col *= i.diffuse; return col; } ENDCG } } }
ちょっと雑ですが。
こんな感じになります。VTFのときのカクカクが解消されました。
おわりに
描画の際、テクスチャは膨大な情報の保管庫となります。必要な様々なデータをテクスチャにのっけてシェーダーで処理すれば、色々出来ることが広がっていきます。
UnityTexturePaintでは、リアルタイムにシェーダーに渡すテクスチャにデータを書き込むことが可能になる便利なツールになっています。 久しぶりに手応えのある開発が出来ているので、色々なサンプルを作ってみて少しでも多くの人に知ってもらえたらなと思っています。