ライトマップデータから影の情報を抜き出し頂点カラーに焼く
概要
とあるプロジェクトで頂点カラーにライトマップの影情報を焼き込めないか、という話があって試してみました。
結局その機能自体は使いませんでしたが、ライトマップデータへのアクセスなど学びがあったのでまとめておきます。
ライトマップデータの読み取り
まずはライトマップデータの取り扱いです。
ライトマップは、Lightの「Mode」をMixedかBakedにし、Lightingウィンドウの「Generate Lighting」ボタンを押すことで生成されます。
生成されたライトマップのデータはこんな感じです↓

ライトマップの扱いは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」のチェックをオンにする必要があります。

そのための処理は以下のようになります。
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"
}