e.blog

主にUnity/UE周りのことについてまとめていきます

ライトマップデータから影の情報を抜き出し頂点カラーに焼く

概要

とあるプロジェクトで頂点カラーにライトマップの影情報を焼き込めないか、という話があって試してみました。
結局その機能自体は使いませんでしたが、ライトマップデータへのアクセスなど学びがあったのでまとめておきます。

ライトマップデータの読み取り

まずはライトマップデータの取り扱いです。
ライトマップは、Lightの「Mode」をMixedBakedにし、Lightingウィンドウの「Generate Lighting」ボタンを押すことで生成されます。

生成されたライトマップのデータはこんな感じです↓
f:id:edo_m18:20180517113239p:plain

ライトマップの扱いはLightmapDataクラスを使う

ライトマップのデータは、LightmapDataクラスを介して取得します。

ライトマップデータを取り出す

オブジェクトに紐づくデータは以下のようにして取り出します。

MeshRenderer renderer = GetComponent<MeshRenderer>();
LightmapData data = LightmapSettings.lightmaps[renderer.lightmapIndex];
Texture2D lightmap = data.lightmapColor;

RendererクラスのlightmapIndexプロパティに、該当オブジェクトのライトマップデータの配列のindexが格納されているので、それを利用してLightmapSettings.lightmaps配列からライトマップデータを取得することができます。

ライトマップデータ(テクスチャ)はLightmapData.lightmapColorに格納されています。

ライトマップデータからカラー情報を取得する

ライトマップデータを取り出したら、そこからカラー情報を取得します。
ライトマップは通常、複数オブジェクトの情報がひとつ(ないし複数)のテクスチャに、テクスチャアトラスとして書き出されます。

つまり、ライトマップデータは複数オブジェクトの情報を含んでいるため、適切にテクスチャからカラー情報を取得(フェッチ)しないとなりません。

※ 注意点として、生成されたライトマップデータは読み取りができない状態になっているので、インスペクタの設定から「Read/Write Enabled」のチェックをオンにする必要があります。

f:id:edo_m18:20180517114150p:plain

そのための処理は以下のようになります。

int width = lightmap.Width;
int height = lightmap.Height;

Vector4 scaleOffset = renderer.lightmapScaleOffset;
Color[] colors = new Color[mesh.uv.Length];

for (int i = 0; i < mesh.uv.Length; i++)
{
    Vector2 uv = mesh.uv[i];
    float uv_x = (uv.x * scaleOffset.x) + scaleOffset.z;
    float uv_y = (uv.y * scaleOffset.y) + scaleOffset.w;
    Color pixel = lightmap.GetPixelBilinear(uv_x, uv_y);
    colors[i] = pixel;
}

mesh.colors = colors;

上記のように、Renderer.lightmapScaleOffsetに、ライトマップデータのオフセット情報が入っています。
それに、メッシュが持つ元々のUV情報と組み合わせてフェッチすることで、目的のライトマップのカラー情報を取得することができます。

今回はメッシュの頂点カラーに焼き込む、というのが目的なので、最終的に取得したピクセル配列をメッシュの頂点カラーとして設定しています。

そして焼き込んだメッシュ情報を利用する場合、アセットとして新規作成しておかないとならないため、今回のサンプルでは元のメッシュを複製、アセットとして保存する処理もあります。

ということで、今回実装した内容全体を下記に記載しておきます。

Meshを生成する

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

public static class BakeLightMap2VC
{
    /// <summary>
    /// Clone mesh asset.
    /// 
    /// If folder in path doesn't exist, will create all folders in path.
    /// </summary>
    public static Mesh CloneMesh(GameObject target)
    {
        if (target == null)
        {
            Debug.LogWarning("Must select a GameObject.");
            return null;
        }

        MeshFilter filter = target.GetComponent<MeshFilter>();
        if (filter == null)
        {
            Debug.LogWarning("CloneMesh need a MeshFilter component.");
            return null;
        }

        Mesh newMesh = GameObject.Instantiate(filter.sharedMesh);

        string[] folderPaths = new[]
        {
            "ClonedMesh",
        };

        string path = "Assets";
        for (int i = 0; i < folderPaths.Length; i++)
        {
            string folderName = folderPaths[i];
            string checkPath = path + "/" + folderName;
            if (!AssetDatabase.IsValidFolder(checkPath))
            {
                AssetDatabase.CreateFolder(path, folderName);
            }
            path = checkPath;
        }

        // Create and save a mesh asset.
        string assetPath = path + "/" + target.name + ".asset";
        string[] findAssets = AssetDatabase.FindAssets(target.name, new[] { path });
        if (findAssets.Length != 0)
        {
            int result = EditorUtility.DisplayDialogComplex("重複チェック", "既存のファイルがあります。上書きしますか?", "はい", "いいえ", "別名で保存");
            if (result == 1)
            {
                return null;
            }
            else if (result == 2)
            {
                assetPath = AssetDatabase.GenerateUniqueAssetPath(assetPath);
            }
        }

        AssetDatabase.CreateAsset(newMesh, assetPath);
        AssetDatabase.SaveAssets();

        EditorGUIUtility.PingObject(newMesh);

        return newMesh;
    }

    /// <summary>
    /// Bake a lightmap shadow to vertex colors.
    /// </summary>
    [MenuItem("Assets/Tools/BakeLightMap2VC")]
    public static void Bake()
    {
        GameObject target = Selection.activeGameObject;
        if (target == null)
        {
            Debug.LogWarning("Must select a GameObject.");
            return;
        }

        // Clone selected object's mesh.
        Mesh clonedMesh = CloneMesh(target);
        if (clonedMesh == null)
        {
            Debug.LogError("Can't clone mesh data.");
            return;
        }

        MeshRenderer renderer = target.GetComponent<MeshRenderer>();
        if (renderer == null)
        {
            Debug.LogWarning("Must have a MeshRenderer.");
            return;
        }
        LightmapData data = LightmapSettings.lightmaps[renderer.lightmapIndex];
        Texture2D lightMap = data.lightmapColor;

        MeshFilter filter = target.GetComponent<MeshFilter>();
        filter.sharedMesh = clonedMesh;

        #region ### Sample LightMap data and bake it to vertex colors ###
        int width = lightMap.width;
        int height = lightMap.height;
        Vector4 scaleOffset = renderer.lightmapScaleOffset;
        Color[] colors = new Color[clonedMesh.uv.Length];
        for (int i = 0; i < clonedMesh.uv.Length; i++)
        {
            Vector2 uv = clonedMesh.uv[i];
            float uv_x = (uv.x * scaleOffset.x) + scaleOffset.z;
            float uv_y = (uv.y * scaleOffset.y) + scaleOffset.w;
            Color pixel = lightMap.GetPixelBilinear(uv_x, uv_y);
            colors[i] = pixel;
        }
        clonedMesh.colors = colors;
        #endregion ### Sampling LightMap data ###
    }
}

頂点カラーを利用するシェーダ

さて最後に。
頂点カラーに情報を書き込んでも、それを読み出さなくては意味がありません。

そして残念ながら、最近の3Dではあまり頂点カラーを使わないため、Unityのデフォルトのシェーダでは頂点カラー情報は処理されません。
なので、頂点カラーを読み出す処理は自分で書く必要があります。

といっても、頂点カラー情報を取得するにはシェーダへの入力に頂点カラーのパラメータを追加するだけです。

struct v2f
{
    fixed4 vertex : SV_POSITION;
    fixed4 color : COLOR; // <- これを追加
    fixed2 uv : TEXCOORD0;
};

あとはこれを、最終出力に乗算してやるだけでOKです。

half4 frag(v2f i) : SV_Target
{
    fixed2 uv = (i.uv * _MainTex_ST.xy) + _MainTex_ST.zw;
    fixed4 col = tex2D(_MainTex, uv);
    return col * i.color * _Color;
}

シェーダ全体も載せておきます。

Shader "VertexShadow" {
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 150
        
        Pass
        {
            Tags { "LightMode"="ForwardBase" "Queue"="Geometry-10" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0
            #include "UnityCG.cginc"

            sampler2D _MainTex;
            fixed4 _MainTex_ST;
            fixed4 _Color;

            struct v2f
            {
                fixed4 vertex : SV_POSITION;
                fixed4 color : COLOR;
                fixed2 uv : TEXCOORD0;
            };

            UNITY_INSTANCING_CBUFFER_START(Props)
            UNITY_INSTANCING_CBUFFER_END

            v2f vert(appdata_full i)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(i.vertex);
                o.color = i.color;
                o.uv = i.texcoord;
                return o;
            }

            half4 frag(v2f i) : SV_Target
            {
                fixed2 uv = (i.uv * _MainTex_ST.xy) + _MainTex_ST.zw;
                fixed4 col = tex2D(_MainTex, uv);
                return col * i.color * _Color;
            }

            ENDCG
        }
    }
    FallBack "Diffuse"
}

参考

tsubakit1.hateblo.jp

tsubakit1.hateblo.jp