概要
とあるプロジェクトで頂点カラーにライトマップの影情報を焼き込めないか、という話があって試してみました。
結局その機能自体は使いませんでしたが、ライトマップデータへのアクセスなど学びがあったのでまとめておきます。
ライトマップデータの読み取り
まずはライトマップデータの取り扱いです。
ライトマップは、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" }