しゅみぷろ

プログラミングとか

ハイトマップから法線情報の生成

はじめに

esprog.hatenablog.com

以前の記事で、ハイトマップを参照して動的に変形するオブジェクトのサンプルを作りました。

サンプルでは、シェーディング計算が適当でしたが、今回はちゃんとシェーディングを行うために必要になる法線情報を計算してみます。

なぜ計算が必要なのか

以前の例ではハイトマップに高さの情報を書き込みそれを元にして頂点を移動させていますが、シェーダー内で変形させているだけで、頂点の持つ法線方向には書き込みを行っているわけではありません。なので、変形させているにも関わらずCPUから送られてくる法線データには変化がありません。当然ですが・・・。

法線データへの書き込みが必要なら、ハイトマップの変更のときに法線マップにも変更を加える・・・というちょっと面倒な手順を踏む必要があります。 なので今回は、ハイトマップから法線がどの方向を向いているのかを計算させることにします。

この方法であれば法線マップが必要なくなる上、計算ミスなどによるデータの齟齬も防げます。

f:id:es_program:20161024144710g:plain:w500

形状の変形と、それによる法線の変化をテストした例です(本来なら青っぽい色が表示されるのですが、目に優しくないのでちょっといじってます...ということにしておいてください...)。 元々の形状はただのPlane(板)ですが、とりあえずハイトマップだけ与えて変形させ、黒い四角形(プレイヤーの操作するキャラクターを模したもの)の歩いた場所を凹ませています。

形状が変化しても法線情報が正しく得られているかと思います。

計算方法

ハイトマップのテクスチャ座標のx,yに関する高さの偏微分を取ることで、接ベクトルを求めます。 求めた接ベクトルの外積の方向ベクトルが法線になります。

コードはこんな感じになります。

float2 shiftX = { _ParallaxMap_TexelSize.x,  0 };
float2 shiftZ = { 0, _ParallaxMap_TexelSize.y };

shiftX *= _ParallaxScale * _NormalScaleFactor;
shiftZ *= _ParallaxScale * _NormalScaleFactor;

float3 texX = 2 * tex2Dlod(_ParallaxMap, float4(uv.xy + shiftX,0,0)) - 1;
float3 texx = 2 * tex2Dlod(_ParallaxMap, float4(uv.xy - shiftX,0,0)) - 1;
float3 texZ = 2 * tex2Dlod(_ParallaxMap, float4(uv.xy + shiftZ,0,0)) - 1;
float3 texz = 2 * tex2Dlod(_ParallaxMap, float4(uv.xy - shiftZ,0,0)) - 1;

float3 du = { 1, 0, _NormalScaleFactor * (texX.x - texx.x) };
float3 dv = { 0, 1, _NormalScaleFactor * (texZ.x - texz.x)};


float3 normal = normalize(cross(du, dv)) * 0.5 + 0.5;

_ParallaxMapがハイトマップ、_ParallaxMap_TexelSizeのx、yはそれぞれハイトマップのテクスチャサイズの横、縦サイズの逆数。 _ParallaxScaleはハイトマップによる変位の係数、_NormalScaleFactorは法線方向を計算するのに使用する係数です。 今回はハイトマップのx(赤色成分)から計算していますが、たとえば以下のような感じでrgbからいい感じにモノクロ抽出してそれを高さとして取ったほうが良いかもしれません。

height = 0.299 * r + 0.587 * g + 0.114 * b;

法線さえしっかり取れていると、テッセレーションなしでも割りとそれっぽく見えます。

f:id:es_program:20161024214302g:plain:w500

ちなみに法線を取ってきて、その法線情報を法線マップに書き込む場合、Textureの形式が問題になる場合があります。たとえばDXT_5nm形式に書き込む場合

float4 PackNormal(float3 normal) {
#if defined(UNITY_NO_DXT5nm)
    return float4(normal);
#else
    return float4(normal.y, normal.y, normal.y, normal.x);
#endif
}

といった感じで、書き込むための法線情報を変換する必要がありそうです。

参考

t-pot『動的法線マップ』

UnityTexturePaintでリアルタイムにオブジェクトを変形する

はじめに

UnityTexturePaintは、リアルタイム(ゲーム実行中)にオブジェクトの持つテクスチャに対してペイントを行うアセットです。

UnityTexturePaintは以下で公開しています。

github.com

基本的には

  • メインテクスチャへのペイント
  • ノーマルマップへのペイント
  • ハイトマップへのペイント

をサポートしていますが、応用次第でとにかく色々できそうな予感がします。

ちょっとした凹凸を表現するために頂点を増やしてしまうと、ザラザラしたオブジェクトの場合頂点数がスゴイことになってしまいます これを解決できるのがバンプマップです。 バンプマップでは、面の細かな法線や高さの情報をテクスチャに格納しておき、描画の際にその法線と高さを考慮したシェーディングを行うことで、実際には無い凹凸を表現する方法です。

f:id:es_program:20161008220700g:plain:w400

この画像はノーマルマップのみを利用した凹凸表現です。 実際のモデルはただの立方体ですが、ノーマルマップのお陰で凹凸があるように見えます。

ペイントしている緑の液体も、ノーマルマップペイント機能のみ利用しています。

f:id:es_program:20161015162654g:plain:w400

一方こっちはハイトマップも併用しています。 オブジェクトを近くで見た際の表現が段違いです。

実際に浮き出ているように見えます。すごい。

しかし、バンプマップは実際に凹凸があるわけではなく、オブジェクト表面の細かい凹凸を表現するのに適した方法です。 よりダイナミックに、実際に頂点を変形させたい場合が往々にして存在します。

こうなると実際に頂点を弄っていく必要があるのですが、今回は変位マップを動的に変化させ、その変位マップを参照して頂点を変形させる方法について書いていきます。

f:id:es_program:20161017191055g:plain:w500

実際にやってみると、こんな感じの表現が出来るようになります。 キャラが動いたらその部分を凹ませる。弾丸が壁を撃ち抜いたらそこを凹ませる。等、シンプルですが使いみちが結構あるかと思います。

VTF

頂点変形の方法はいくつもあるのですが、VTF(Vertex Texture Fetch)とDisplacementMappingについて書きます。

まずVTFですが、名前の通り、頂点シェーダーでテクスチャを参照する手法です。 変形の手順ですが

  1. 変形させたい位置に対してUnityTexturePaintの機能を使ってテクスチャにペイント処理を施す
  2. ペイントされたテクスチャを頂点シェーダーで参照し頂点位置を調整する

という感じになります。

シェーダーは以下のような感じ。

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を付けて各種設定を行って実行するとこんな感じになります。

f:id:es_program:20161017195507g:plain:w500

DisplacementMapping

VTFでは、床(Plane)の頂点数が少ないため、変位させるとなんだかカクカクしてしまっています。

これを解決するために、Tessellationを行います。

Tessellationは以下の記事で。

esprog.hatenablog.com

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
            }
    }
}

ちょっと雑ですが。

f:id:es_program:20161017201835g:plain:w500

こんな感じになります。VTFのときのカクカクが解消されました。

おわりに

描画の際、テクスチャは膨大な情報の保管庫となります。必要な様々なデータをテクスチャにのっけてシェーダーで処理すれば、色々出来ることが広がっていきます。

UnityTexturePaintでは、リアルタイムにシェーダーに渡すテクスチャにデータを書き込むことが可能になる便利なツールになっています。 久しぶりに手応えのある開発が出来ているので、色々なサンプルを作ってみて少しでも多くの人に知ってもらえたらなと思っています。