🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
Unity3D

Unity3Dは、ゲームや対話式の3Dアプリケーション、トレーニングシュミレーション、そして医学的・建築学的な技術を可視化する、商業用の開発プラットフォームです。

ビルド

ソースコードを単体で実行可能なソフトウェアへ変換する過程をビルド(build)と呼びます

Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

Q&A

解決済

3回答

3473閲覧

Unity 生成された影の画像(もしくは座標データ)をビルド後に出力したい

masutake0

総合スコア3

Unity3D

Unity3Dは、ゲームや対話式の3Dアプリケーション、トレーニングシュミレーション、そして医学的・建築学的な技術を可視化する、商業用の開発プラットフォームです。

ビルド

ソースコードを単体で実行可能なソフトウェアへ変換する過程をビルド(build)と呼びます

Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

0グッド

0クリップ

投稿2020/12/28 12:41

編集2020/12/31 11:45

前提・実現したいこと

現在Unityでオブジェクトに表示される影の座標データをビルド後に取得し,別のプログラムで適用したいと考えています.

試したこと

影のUV画像生成はLightのMoodをBakeにしてGenerate lightingをすることでライトマップを生成し,影のマスク画像(UV座標系画像)を生成,出力することができました.
イメージ説明
ただ,この画像をビルド後に特定のフォルダに出力する方法がわかりません.

一応オジェクトの座標をビルド後にcsv出力する方法については以下のスクリプトで行うことが確認できました.

C#

1using UnityEngine; 2using System; 3using System.IO; 4using System.Text; 5 6public class IO_Test : MonoBehaviour 7{ 8 9 private string path; 10 private string fileName = "output.csv"; 11 private float timeleft; 12 13 void Start() 14 { 15 path = Application.dataPath + "/" + fileName; 16 Debug.Log(path); 17 ReadFile(); 18 } 19 20 21 void Update() 22 { 23 //出力処理 24 StreamWriter sw; 25 FileInfo fi; 26 DateTime dt; 27 28 GameObject cube = GameObject.Find("Cube"); 29 30 fi = new FileInfo(path); 31 //10秒ごとに処理 32 timeleft -= Time.deltaTime; 33 if (timeleft <= 0.0 ){ 34 timeleft = 10.0f; 35 sw = fi.AppendText(); 36 sw.Write(cube.transform.eulerAngles.ToString()); 37 dt = DateTime.Now; 38 sw.WriteLine("," + dt); 39 sw.Flush(); 40 sw.Close(); 41 } 42 } 43 44 void ReadFile() { 45 FileInfo fi = new FileInfo(path); 46 try { 47 using (StreamReader sr = new StreamReader(fi.OpenRead(), Encoding.UTF8)) { 48 string readTxt = sr.ReadToEnd(); 49 Debug.Log(readTxt); 50 } 51 } catch (Exception e) { 52 Debug.Log(e); 53 } 54 } 55}

Unityのソフト上でライトマップを生成した際,Assetsにマスク画像が出力される以上,ビルド後でもできると思うのですが,難しいものでしょうか.
特定のオブジェクトに描画される影の座標さえビルド後に出力できれば,上記のようなマスク画像でなくても構いません.
例えば,その座標の影を含めた色データをビルドに取得するという内容でも大丈夫です.

UnityおよびC#は初心者で3日間これで頭を悩ませているので教えていただけると大変助かります.
よろしくお願いします.

追記

「Bake」だとプレビューに影のシャドウマスクが選択でき,Assets > Scenes > SampleSceneに追加される
イメージ説明
「Realtime」だとプレビューでshadowmaskがなく影の位置がわからない
イメージ説明
###追記2
######状況
写真の状態で実行すると模様が入った画像が出力されてしまう
イメージ説明
イメージ説明
######試したこと
空のオブジェクトを作成し,作成していただいたShadowCaptorをアタッチ
ShadowCaptorにシェーダーをセット
Cubeに定期的にCapterを実行する以下のスクリプトをアタッチ

C#

1using System.Collections; 2using System.Collections.Generic; 3using UnityEngine; 4 5public class test : MonoBehaviour 6{ 7 private float timeleft; 8 GameObject gameobject; 9 Renderer cubeRenderer; 10 private string path; 11 private string fileName = "shadow.png"; 12 private Material material; 13 14 // Start is called before the first frame update 15 void Start() 16 { 17 gameobject = GameObject.Find("GameObject"); 18 path = Application.dataPath + "/" + fileName; 19 cubeRenderer = GetComponent<Renderer>(); 20 } 21 void Update() 22 { 23 timeleft -= Time.deltaTime; 24 if (timeleft <= 0.0) 25 { 26 timeleft = 5.0f; 27 28 //ここに処理 29 gameobject.GetComponent<ShadowCaptor>().Capture(cubeRenderer, path); 30 31 } 32 33 } 34 35}

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Bongo

2020/12/28 18:18

おうかがいしたいのですが、ライトを「Bake」にして制作時の段階でライトマップをベイクしているということは、影の形はその時点で画像として焼き付けられ、ビルド後の実行中には常に一定のまま変化しないんじゃないかと思います。ですので、わざわざプログラムをビルドした後で実行時に画像を出力しなくても、ベイク時にプロジェクト内に生成された影画像ファイルをそのまま別のプログラムへ持っていけばいいんじゃないか...という気がするのですが、おっしゃるようなことをしたいのは何のためなのでしょうか? ライトを「Realtime」とし、実行中のある時点のリアルタイムシャドウを画像ファイルとして出力したいとかでしたらまだ分かる気がします(たとえば時間とともにライトが回転し、日時計のように影が変化していく様子を撮影するとか...)。「別のプログラム」というのはどのようなものなのでしょうか?
masutake0

2020/12/29 03:06

ご返信ありがとうございます. 言葉足らずで申し訳ありません.「別のプログラム」というのは3次元の熱伝導解析になります.したがって,境界条件として影の有無が必要になるのですが,Unity上で簡単に影を表現できると知り,それを利用できないかと考えています.最終的にやりたいのは日時に応じた太陽の位置(高度角,方位角)とそれによって生じたオブジェクトに表示される影を一時間おきに取得したいです. ご指摘の通り,ライトを「Bake」にするとビルド後の実行中は常に一定のまま変化しないと思います.ですので,一定時間後に焼き直し(できるかわかりません)をして出力するか,実行時に現在時刻を参照して出力→また実行とすればできるのではないかと考えています. もちろんライトを「Realtime」としてリアルタイムの影画像を一定時間ごとに出力できたらそれに越したことはありません.ただ,自分の環境だと「Realtime」にするとshadowmaskが生成されませんでした.(その状況について追記で画像を載せて説明します.)「Realtime」で出力できる方法があれば教えていただけると助かります.
guest

回答3

0

##ステンシルシャドウによる方法(その2)

ShadowVolumeCaptorfaceMarkerShaderは...

ShaderLab

1Shader "Hidden/FaceMarker" 2{ 3 Properties 4 { 5 } 6 7 SubShader 8 { 9 CGINCLUDE 10 #include "UnityCG.cginc" 11 12 float4 vert(float4 position : POSITION) : SV_POSITION 13 { 14 float4 clipPos = mul(unity_ObjectToWorld, position); 15 clipPos.xy = clipPos.xy * 2.0 - 1.0; 16 #if UNITY_UV_STARTS_AT_TOP 17 clipPos.y *= -1.0; 18 #endif 19 return clipPos; 20 } 21 22 fixed4 frag() : SV_Target 23 { 24 return 0; 25 } 26 ENDCG 27 28 Pass 29 { 30 ColorMask 0 31 ZWrite Off 32 ZTest Always 33 Cull Off 34 Stencil 35 { 36 WriteMask 16 37 Ref 16 38 Comp Always 39 Pass Replace 40 } 41 42 CGPROGRAM 43 #pragma vertex vert 44 #pragma fragment frag 45 ENDCG 46 } 47 48 Pass 49 { 50 ColorMask 0 51 ZWrite Off 52 ZTest Always 53 Cull Off 54 Stencil 55 { 56 WriteMask 16 57 Ref 0 58 Comp Always 59 Pass Zero 60 } 61 62 CGPROGRAM 63 #pragma vertex vert 64 #pragma fragment frag 65 ENDCG 66 } 67 } 68 69 Fallback Off 70}

shadowMarkerShaderは...

ShaderLab

1Shader "Hidden/ShadowMarker" 2{ 3 Properties 4 { 5 } 6 7 SubShader 8 { 9 Pass 10 { 11 ColorMask 0 12 ZWrite Off 13 ZTest Always 14 Cull Off 15 Stencil 16 { 17 ReadMask 16 18 WriteMask 15 19 Ref 16 20 CompFront Equal 21 CompBack Equal 22 PassFront IncrWrap 23 PassBack DecrWrap 24 } 25 26 CGPROGRAM 27 #pragma vertex vert 28 #pragma fragment frag 29 30 #include "UnityCG.cginc" 31 32 #define FACE_EXTENSION float2(0.00390625, 128.0) 33 #define Z_SCALE 0.00390625 34 35 #if defined(UNITY_REVERSED_Z) 36 #if UNITY_REVERSED_Z == 1 37 // Direct3D、Z反転あり 38 #define Z_FAR 0.0 39 #define Z_NEAR 1.0 40 #else 41 // OpenGL、Z反転あり 42 #define Z_FAR -1.0 43 #define Z_NEAR 1.0 44 #endif 45 #elif UNITY_UV_STARTS_AT_TOP 46 // Direct3D、Z反転なし 47 #define Z_FAR 1.0 48 #define Z_NEAR 0.0 49 #else 50 // OpenGL、Z反転なし 51 #define Z_FAR 1.0 52 #define Z_NEAR -1.0 53 #endif 54 55 struct appdata 56 { 57 float4 vertex : POSITION; 58 float3 normal : NORMAL; 59 }; 60 61 float4x4 _WorldToUv; 62 float3 _LightDirection; 63 64 float4 vert(appdata v) : SV_POSITION 65 { 66 float4 worldPos = mul(unity_ObjectToWorld, v.vertex); 67 float3 worldNormal = UnityObjectToWorldNormal(v.normal); 68 69 // ライトの方に向いている頂点は-1、背を向けている頂点は1 70 float faceSign = sign(dot(worldNormal, _LightDirection)); 71 72 // 頂点を光に沿って移動させる 73 // ライトに背を向けている頂点を大きく移動させることで、シャドウボリュームを長く引き伸ばす 74 // また、ライトの方に向いている頂点もわずかに移動させ、ライト側に向いている面が自身の影に 75 // 包まれて黒くなってしまうのを防止する 76 worldPos.xyz += dot(saturate(float2(-faceSign, faceSign)), FACE_EXTENSION) * _LightDirection; 77 78 // UV空間に移し、面から手前に突き出た領域を描画する 79 float4 clipPos = mul(_WorldToUv, worldPos); 80 clipPos.xy = clipPos.xy * 2.0 - 1.0; 81 clipPos.z = lerp(Z_NEAR, Z_FAR, max((clipPos.z * Z_SCALE) + 1.0, 0.0)); 82 #if UNITY_UV_STARTS_AT_TOP 83 clipPos.y *= -1.0; 84 #endif 85 return clipPos; 86 } 87 88 fixed4 frag() : SV_Target 89 { 90 return 0; 91 } 92 ENDCG 93 } 94 } 95 96 Fallback Off 97}

shadowDrawerShaderは...

ShaderLab

1Shader "Hidden/ShadowDrawer" 2{ 3 Properties 4 { 5 } 6 7 SubShader 8 { 9 Pass 10 { 11 ZWrite Off 12 ZTest Always 13 Cull Off 14 Stencil 15 { 16 ReadMask 15 17 Ref 0 18 Comp Less 19 } 20 21 CGPROGRAM 22 #pragma vertex vert 23 #pragma fragment frag 24 #include "UnityCG.cginc" 25 26 float4 vert(float4 position : POSITION) : SV_POSITION 27 { 28 return position; 29 } 30 31 fixed4 frag() : SV_Target 32 { 33 return fixed4(0.0, 0.0, 0.0, 1.0); 34 } 35 ENDCG 36 } 37 } 38 39 Fallback Off 40}

をセットしました。
これで影を撮影したところ、下図のような影画像が得られました。
前回と同じくサイズは横長の1024×512とし、縮小してGIF化したものです。

図6

各面ごとに影を描画していますので、面の数に比例して多数のドローコールを発行することになってしまいました。
ポリゴン数の多いメッシュに対しては速度面で不利でしょうが、シャープな直線で構成され白黒がくっきり分かれた影になりました。

投稿2021/01/01 20:26

Bongo

総合スコア10811

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

##キューブの各面に落ちる影を描かせるための対策

まずターゲットのキューブ(ご質問者さんの場合はライトの真下にある赤いキューブでしょうか)に下記スクリプトをアタッチし、6つの面がUV空間上で重ならないようにしました。

C#

1using UnityEngine; 2 3[RequireComponent(typeof(MeshFilter))] 4public class CubeMeshUnwrapper : MonoBehaviour 5{ 6 private Mesh cubeMesh; 7 8 private void Awake() 9 { 10 this.cubeMesh = this.GetComponent<MeshFilter>().mesh; 11 var normals = this.cubeMesh.normals; 12 var uv = this.cubeMesh.uv; 13 var scale = new Vector2(0.25f, 0.5f); 14 for (var vertexIndex = 0; vertexIndex < normals.Length; vertexIndex++) 15 { 16 var normal = normals[vertexIndex]; 17 var absNormalYz = new Vector2(Mathf.Abs(normal.y), Mathf.Abs(normal.z)); 18 var x = (int)Vector2.Dot(absNormalYz, new Vector2(1, 2)); 19 var y = (int)Mathf.Clamp01(-(normal.x + normal.y + normal.z)); 20 uv[vertexIndex] = (uv[vertexIndex] + new Vector2(x, y)) * scale; 21 } 22 this.cubeMesh.uv = uv; 23 this.cubeMesh.UploadMeshData(true); 24 } 25 26 private void OnDestroy() 27 { 28 Destroy(this.cubeMesh); 29 } 30}

デフォルトのキューブだと通常は下図のようにテクスチャが貼られますが...

図1

実行時にUV配置を変更することで、テクスチャの貼られ方が下図のように変化します。

図2

また、ブロック模様の軽減のため「Edit」→「Project Settings...」の「Quality」→「Shadows」の「Shadow Resolution」を「Very High Resolution」に、「Shadow Distance」をターゲットがギリギリ収まる程度まで小さくしました。

図3

UV配置を横長にしたので出力画像のサイズも1024×512の横長にして、前回と同様にライトを回しながら撮影したところ...

図4

下図のような影画像になりました。

図5

##ステンシルシャドウによる方法(その1)

まずプロジェクト内に、以前別の方の「影のできる位置を求めてコライダーをつけたい」への回答の際に作成したShadowMeshAssemblerを入れました。
そして上述のCubeMeshUnwrapper内のthis.cubeMesh.UploadMeshData(true);this.cubeMesh.UploadMeshData(false);に変更し、メッシュ加工後もメッシュデータを読み書きできるようにしました。
ターゲットにはCubeMeshUnwrapperに加えて、各面への頂点変換を保持するための下記スクリプトをアタッチしました。

C#

1using System.Linq; 2using UnityEngine; 3 4[RequireComponent(typeof(MeshFilter))] 5public class FaceDecomposer : MonoBehaviour 6{ 7 public int FaceCount { get; private set; } 8 public Matrix4x4[] UvMatrices { get; private set; } 9 public Matrix4x4[] ObjectToUvMatrices { get; private set; } 10 11 private void Start() 12 { 13 var mesh = this.GetComponent<MeshFilter>().mesh; 14 var vertices = mesh.vertices; 15 var uv = mesh.uv; 16 var triangles = mesh.triangles; 17 this.FaceCount = triangles.Length / 3; 18 var matrices = Enumerable.Range(0, this.FaceCount).Select( 19 faceIndex => 20 { 21 var k0 = faceIndex * 3; 22 var k1 = k0 + 1; 23 var k2 = k0 + 2; 24 var i0 = triangles[k0]; 25 var i1 = triangles[k1]; 26 var i2 = triangles[k2]; 27 var v0 = vertices[i0]; 28 var v1 = vertices[i1]; 29 var v2 = vertices[i2]; 30 var t0 = uv[i0]; 31 var t1 = uv[i1]; 32 var t2 = uv[i2]; 33 var uvMatrix = new Matrix4x4( 34 t2 - t0, 35 t1 - t0, 36 new Vector4(0, 0, 1, 0), 37 new Vector4(t0.x, t0.y, 0, 1)); 38 var objectMatrix = new Matrix4x4( 39 v2 - v0, 40 v1 - v0, 41 Vector3.Cross(v2 - v0, v1 - v0).normalized, 42 new Vector4(v0.x, v0.y, v0.z, 1)); 43 var objectToUvMatrix = uvMatrix * objectMatrix.inverse; 44 return (uvMatrix, objectToUvMatrix); 45 }).ToArray(); 46 this.UvMatrices = matrices.Select(m => m.uvMatrix).ToArray(); 47 this.ObjectToUvMatrices = matrices.Select(m => m.objectToUvMatrix).ToArray(); 48 } 49}

影の撮影はこれまでのShadowCaptorに代わって下記ShadowVolumeCaptorを使いました。

C#

1using System; 2using System.Collections.Generic; 3using System.IO; 4using UnityEngine; 5 6public class ShadowVolumeCaptor : MonoBehaviour 7{ 8 private static readonly int LightDirectionId = Shader.PropertyToID("_LightDirection"); 9 private static readonly int WorldToUvId = Shader.PropertyToID("_WorldToUv"); 10 11 [SerializeField] private Shader faceMarkerShader; 12 [SerializeField] private Shader shadowMarkerShader; 13 [SerializeField] private Shader shadowDrawerShader; 14 [SerializeField] private Vector2Int size = new Vector2Int(1024, 1024); 15 [SerializeField] private Transform lightTransform; 16 17 private readonly Dictionary<MeshFilter, Mesh> shadowVolumes = new Dictionary<MeshFilter, Mesh>(); 18 private Material faceMarkerMaterial; 19 private Material shadowMarkerMaterial; 20 private Material shadowDrawerMaterial; 21 private Mesh face; 22 private Mesh quad; 23 private RenderTexture resultRenderTexture; 24 private Texture2D resultTexture; 25 26 private void Awake() 27 { 28 if (this.faceMarkerShader == null) 29 { 30 Debug.LogError("Face marker shader is not set."); 31 Destroy(this); 32 return; 33 } 34 if (this.shadowMarkerShader == null) 35 { 36 Debug.LogError("Shadow marker shader is not set."); 37 Destroy(this); 38 return; 39 } 40 if (this.shadowDrawerShader == null) 41 { 42 Debug.LogError("Shadow drawer shader is not set."); 43 Destroy(this); 44 return; 45 } 46 47 this.faceMarkerMaterial = new Material(this.faceMarkerShader); 48 this.shadowMarkerMaterial = new Material(this.shadowMarkerShader); 49 this.shadowDrawerMaterial = new Material(this.shadowDrawerShader); 50 this.resultTexture = new Texture2D(this.size.x, this.size.y); 51 this.resultRenderTexture = new RenderTexture(this.size.x, this.size.y, 24); 52 this.face = new Mesh 53 { 54 name = "Face", 55 vertices = new[] {Vector3.zero, Vector3.up, Vector3.right}, 56 triangles = new[] {0, 1, 2} 57 }; 58 this.quad = new Mesh 59 { 60 name = "Quad", 61 vertices = new[] {new Vector3(-1, -1, 0), new Vector3(-1, 1, 0), new Vector3(1, -1, 0), new Vector3(1, 1, 0)}, 62 triangles = new[] {0, 1, 2, 3, 2, 1} 63 }; 64 Shader.WarmupAllShaders(); 65 } 66 67 private void OnDestroy() 68 { 69 Destroy(this.faceMarkerMaterial); 70 Destroy(this.shadowMarkerMaterial); 71 Destroy(this.shadowDrawerMaterial); 72 Destroy(this.resultTexture); 73 Destroy(this.resultRenderTexture); 74 Destroy(this.face); 75 Destroy(this.quad); 76 foreach (var shadowVolume in this.shadowVolumes.Values) 77 { 78 Destroy(shadowVolume); 79 } 80 } 81 82 public void Capture(FaceDecomposer target, string path) 83 { 84 if (target == null) 85 { 86 throw new ArgumentNullException(nameof(target)); 87 } 88 89 if (string.IsNullOrWhiteSpace(path)) 90 { 91 throw new ArgumentException("Invalid path."); 92 } 93 94 // レンダーターゲットを設定し... 95 var activeTexture = RenderTexture.active; 96 RenderTexture.active = this.resultRenderTexture; 97 GL.Clear(true, true, Color.white); 98 99 // ライト方向を設定し... 100 this.shadowMarkerMaterial.SetVector(LightDirectionId, this.lightTransform.forward); 101 102 // シーン上の各MeshFilterについて... 103 var worldToTargetMatrix = target.transform.worldToLocalMatrix; 104 foreach (var meshFilter in FindObjectsOfType<MeshFilter>()) 105 { 106 var shadowToWorldMatrix = meshFilter.transform.localToWorldMatrix; 107 108 // シャドウボリュームメッシュを取得し(なければ生成し)... 109 if (!this.shadowVolumes.TryGetValue(meshFilter, out var shadowVolume)) 110 { 111 shadowVolume = ShadowMeshAssembler.AssembleShadowMesh(meshFilter.sharedMesh); 112 this.shadowVolumes[meshFilter] = shadowVolume; 113 } 114 115 // ターゲットを構成する各面について... 116 var faceCount = target.FaceCount; 117 for (var i = 0; i < faceCount; i++) 118 { 119 // まず面が占めるピクセルをマークする 120 this.faceMarkerMaterial.SetPass(0); 121 Graphics.DrawMeshNow(this.face, target.UvMatrices[i]); 122 123 // マークされたピクセルにシャドウボリュームをレンダリングする 124 this.shadowMarkerMaterial.SetMatrix(WorldToUvId, target.ObjectToUvMatrices[i] * worldToTargetMatrix); 125 this.shadowMarkerMaterial.SetPass(0); 126 Graphics.DrawMeshNow(shadowVolume, shadowToWorldMatrix); 127 128 // マーキングを解除する 129 this.faceMarkerMaterial.SetPass(1); 130 Graphics.DrawMeshNow(this.face, target.UvMatrices[i]); 131 } 132 } 133 134 // 影の中にいると判定された部分に黒を塗る 135 this.shadowDrawerMaterial.SetPass(0); 136 Graphics.DrawMeshNow(this.quad, Matrix4x4.identity); 137 138 // 結果をTexture2Dに読み取らせてPNG形式にエンコードして保存する 139 this.resultTexture.ReadPixels(new Rect(0, 0, this.size.x, this.size.y), 0, 0); 140 RenderTexture.active = activeTexture; 141 var resultData = this.resultTexture.EncodeToPNG(); 142 File.WriteAllBytes(path, resultData); 143 } 144}

投稿2020/12/31 15:20

編集2021/01/01 20:25
Bongo

総合スコア10811

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

ベストアンサー

追記ありがとうございます。実行中に動的に変化する影をキャプチャーしたいのでしたら、やはりライトは「Realtime」の状態で何とかする必要があるでしょうね。私の思い当たるかぎりでは、Unityの標準機能にそういったリアルタイムシャドウのベイクを行う機能はありませんでしたので、ちょっと手間を要するでしょう。
念のため実行時にライトマップ、あるいは影のベイクを行う手がないものか検索してみましたが、たとえばRuntime light baking [Solved] - Unity Forumだとかでも、のっけから「ないよ」との回答がついていました。他の回答にあるように、そういうことができるアセットを探してみるのも一案かと思います。

なにか手がかりを提示したいと思いまして、まず実験用に下図のようなシーンを用意しました。

図1

デコボコしたメッシュの上にSphere、Cube、Cylinderが浮かんでおりメッシュに影を落とします。またメッシュ自身のデコボコも自身の上に影を落とすというシチュエーションです。落とされた影をこのメッシュのUVマップの形に沿ってレンダリングし、影画像を出力することを目標としました。

通常のレンダリングの過程を「Window」→「Analysis」→「Frame Debugger」で観察してみると、下図のようにカメラから見た時の影を表すテクスチャを生成している部分があるかと思います。

図2

これと同じような図をUV空間でレンダリングさせられないかと考えました。あの描画を担当しているのはダウンロードアーカイブから入手できるビルトインシェーダーの中のInternal-ScreenSpaceShadows.shaderで、それを拝借した下記のようなシェーダーを用意しました。オリジナルのシェーダーはさまざまな条件に対応できるよう長大なコードになっていますが、とりあえずの実験ということで大部分をそぎ落としました。

ShaderLab

1// Hidden/Internal-ScreenSpaceShadows : 2// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt) 3 4Shader "Hidden/ShadowCaptor" 5{ 6 Properties 7 { 8 _ShadowMapTexture ("", any) = "" {} 9 } 10 11 SubShader 12 { 13 Pass 14 { 15 ZWrite Off 16 ZTest Always 17 Cull Off 18 19 CGPROGRAM 20 #pragma vertex vert 21 #pragma fragment frag 22 23 // ソフトシャドウ表現を使うかどうかでマルチコンパイル 24 #pragma multi_compile _ SOFT_SHADOW 25 26 // ランバート反射係数を乗算するかどうかでマルチコンパイル 27 #pragma multi_compile _ MULTIPLY_LAMBERT 28 29 #pragma target 3.0 30 31 // シャドウマップを受け取るための変数 32 UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture); 33 float4 _ShadowMapTexture_TexelSize; 34 #define SHADOWMAPSAMPLER_AND_TEXELSIZE_DEFINED 35 36 // ランバート反射係数を乗算する場合、ディレクショナルライトのforwardをここにセットする 37 float3 _LightDirection; 38 39 #include "UnityCG.cginc" 40 #include "UnityShadowLibrary.cginc" 41 42 struct appdata 43 { 44 float4 vertex : POSITION; 45 float3 normal : NORMAL; 46 float2 texcoord : TEXCOORD0; 47 }; 48 49 struct v2f 50 { 51 float4 clipPos : SV_POSITION; 52 float4 worldPos : TEXCOORD0; 53 float3 worldNormal : TEXCOORD1; 54 }; 55 56 v2f vert(appdata v) 57 { 58 v2f o; 59 60 // クリップ座標としてUV座標を加工したものを使い、UVマップに沿ってレンダリングする 61 o.clipPos = float4(v.texcoord * 2.0 - 1.0, 0.0, 1.0); 62 #if defined(UNITY_REVERSED_Z) 63 o.clipPos.y *= -1.0; 64 #endif 65 66 o.worldPos = float4(mul(unity_ObjectToWorld, v.vertex).xyz, -UnityObjectToViewPos(v.vertex).z); 67 o.worldNormal = UnityObjectToWorldNormal(v.normal); 68 69 return o; 70 } 71 72 inline fixed4 getCascadeWeights(float z) 73 { 74 fixed4 zNear = float4(z >= _LightSplitsNear); 75 fixed4 zFar = float4(z < _LightSplitsFar); 76 fixed4 weights = zNear * zFar; 77 return weights; 78 } 79 80 inline float4 getShadowCoord(float4 wpos, fixed4 cascadeWeights) 81 { 82 float3 sc0 = mul(unity_WorldToShadow[0], wpos).xyz; 83 float3 sc1 = mul(unity_WorldToShadow[1], wpos).xyz; 84 float3 sc2 = mul(unity_WorldToShadow[2], wpos).xyz; 85 float3 sc3 = mul(unity_WorldToShadow[3], wpos).xyz; 86 float4 shadowMapCoordinate = float4(sc0 * cascadeWeights[0] + sc1 * cascadeWeights[1] + sc2 * cascadeWeights[2] + sc3 * cascadeWeights[3], 1.0); 87 #if defined(UNITY_REVERSED_Z) 88 float noCascadeWeights = 1.0 - dot(cascadeWeights, 1.0); 89 shadowMapCoordinate.z += noCascadeWeights; 90 #endif 91 return shadowMapCoordinate; 92 } 93 94 fixed4 frag(v2f i) : SV_Target 95 { 96 float4 wpos = float4(i.worldPos.xyz, 1.0); 97 fixed4 cascadeWeights = getCascadeWeights(i.worldPos.w); 98 float4 shadowCoord = getShadowCoord(wpos, cascadeWeights); 99 100 // SOFT_SHADOWがオンの場合、近傍比率フィルタリングにより縁をなめらかにする 101 #if SOFT_SHADOW 102 #if defined(SHADER_API_MOBILE) 103 half shadow = UnitySampleShadowmap_PCF5x5(shadowCoord, 0.0); 104 #else 105 half shadow = UnitySampleShadowmap_PCF7x7(shadowCoord, 0.0); 106 #endif 107 #else 108 fixed shadow = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, shadowCoord); 109 #endif 110 111 shadow = lerp(_LightShadowData.r, 1.0, shadow); 112 113 // MULTIPLY_LAMBERTがオンの場合、ランバートの余弦則に基づいて黒を乗せる 114 #if MULTIPLY_LAMBERT 115 shadow *= saturate(dot(normalize(i.worldNormal), -_LightDirection)); 116 #endif 117 118 return fixed4(shadow, shadow, shadow, 1.0); 119 } 120 ENDCG 121 } 122 } 123 124 Fallback Off 125}

また、下記スクリプトを作成しました。シーン上に空のオブジェクトを作ってこれをアタッチし、インスペクターのCaptor Shaderに上記シェーダーをセットしました。

C#

1using System; 2using System.Collections; 3using System.IO; 4using UnityEngine; 5using UnityEngine.Rendering; 6 7public class ShadowCaptor : MonoBehaviour 8{ 9 private static readonly string SoftShadowKeyword = "SOFT_SHADOW"; 10 private static readonly string MultiplyLambertKeyword = "MULTIPLY_LAMBERT"; 11 private static readonly int ShadowMapTextureTexelSizeId = Shader.PropertyToID("_ShadowMapTexture_TexelSize"); 12 private static readonly int LightDirectionId = Shader.PropertyToID("_LightDirection"); 13 14 [SerializeField] private Shader captorShader; 15 [SerializeField] private Vector2Int size = new Vector2Int(1024, 1024); 16 [SerializeField] private Transform lightTransform; 17 [SerializeField] private bool softShadow = true; 18 [SerializeField] private bool multiplyLambert; 19 20 private Material captorMaterial; 21 private CommandBuffer captureCommand; 22 private RenderTexture resultRenderTexture; 23 private Texture2D resultTexture; 24 25 private void Awake() 26 { 27 if (this.captorShader == null) 28 { 29 Debug.LogError("Captor shader is not set."); 30 Destroy(this); 31 return; 32 } 33 34 this.captorMaterial = new Material(this.captorShader); 35 this.captorMaterial.SetVector( 36 ShadowMapTextureTexelSizeId, 37 new Vector4(1.0f / this.size.x, 1.0f / this.size.y, this.size.x, size.y)); 38 this.resultTexture = new Texture2D(this.size.x, this.size.y); 39 this.resultRenderTexture = new RenderTexture(this.size.x, this.size.y, 0); 40 this.captureCommand = new CommandBuffer 41 { 42 name = "Capture shadows" 43 }; 44 Shader.WarmupAllShaders(); 45 } 46 47 private void OnDestroy() 48 { 49 Destroy(this.captorMaterial); 50 Destroy(this.resultTexture); 51 Destroy(this.resultRenderTexture); 52 } 53 54 public void Capture(Renderer targetRenderer, string path) 55 { 56 if (targetRenderer == null) 57 { 58 throw new ArgumentNullException(nameof(targetRenderer)); 59 } 60 61 if (string.IsNullOrWhiteSpace(path)) 62 { 63 throw new ArgumentException("Invalid path."); 64 } 65 66 this.StartCoroutine(this.CaptureAtEndOfFrame(targetRenderer, path)); 67 } 68 69 private IEnumerator CaptureAtEndOfFrame(Renderer targetRenderer, string path) 70 { 71 yield return new WaitForEndOfFrame(); 72 73 // レンダリングコマンドを構築 74 this.captureCommand.Clear(); 75 if (this.softShadow) 76 { 77 this.captureCommand.EnableShaderKeyword(SoftShadowKeyword); 78 } 79 else 80 { 81 this.captureCommand.DisableShaderKeyword(SoftShadowKeyword); 82 } 83 if (this.multiplyLambert && (this.lightTransform != null)) 84 { 85 this.captureCommand.EnableShaderKeyword(MultiplyLambertKeyword); 86 this.captorMaterial.SetVector(LightDirectionId, this.lightTransform.forward); 87 } 88 else 89 { 90 this.captureCommand.DisableShaderKeyword(MultiplyLambertKeyword); 91 } 92 this.captureCommand.SetRenderTarget(this.resultRenderTexture); 93 this.captureCommand.ClearRenderTarget(false, true, Color.white); 94 this.captureCommand.DrawRenderer(targetRenderer, this.captorMaterial); 95 96 // レンダーテクスチャに対してレンダリングを実行し、結果をTexture2Dに読み取らせて 97 // PNG形式にエンコードして保存する 98 Graphics.ExecuteCommandBuffer(this.captureCommand); 99 var activeTexture = RenderTexture.active; 100 RenderTexture.active = this.resultRenderTexture; 101 this.resultTexture.ReadPixels(new Rect(0, 0, this.size.x, this.size.y), 0, 0); 102 RenderTexture.active = activeTexture; 103 var resultData = this.resultTexture.EncodeToPNG(); 104 File.WriteAllBytes(path, resultData); 105 } 106}

ライトを上下に揺さぶりつつ回転させながら、定期的に上記スクリプトのCaptureを実行させ、得られた画像群をつないでGIFにすると下図のようになりました。

図3

また、おまけ機能として追加した「Multiply Lambert」をオンにすると下図のような画像が得られました。CGの入門書籍などで見かけるランバート反射の係数を乗算した形になります。

図4

光が浅い角度で入射すれば単位面積当たりの光量は小さくなるでしょうから、物体表面上の各地点がライトから受けるエネルギーを計算する上ではこんな画像の方が好都合なんじゃないか...と思って追加したものです。

メッシュをライティングなしのシェーダーで描画し、得られた画像をライトの回転に合わせて切り替えながら上乗せすると下図のようになりました。

本来のシェーディングMultiply LambertオフMultiply Lambertオン
図5図6図7

シェーダーやスクリプトの作りがちょっと雑だという理由もありますが、やはり十分に時間をかけて影を生成できる静的ベイクよりも粗くて汚い感じになってしまいました。
一枚の画像を描くのにもっと時間をかけてもいいのなら、Unityが自動的に作ったシャドウマップの代わりに自前で目的に適した構図のシャドウマップを作ったり、もっと高精細にしたり、レイトレーシング法を試してみたり...といった改善が可能かもしれません。ですがそれだと、手間的にはご質問者さんのおっしゃる別プログラム上で影を計算するのと大差なくなってしまうかもしれませんね。

投稿2020/12/29 18:16

Bongo

総合スコア10811

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

masutake0

2020/12/31 11:59

とても丁寧な回答ありがとうございます. 自分の方でも画像が出力されているのが確認できました. ただ,出力画像がBongoさんのようになっていない現状です. (Lighting>Sceneをリアルタイムにしたり,Moodをリアルタイムにしたりとやってみましたが,改善されませんでした.) その出力画像につきましては追記させていただきました. もし原因がお分かりでしたら教えていただけないでしょうか. また,展開図のようにキューブ側面の影も取得したいと考えております. その場合,Planeオブジェクトを張り合わせ,キューブ状にして同じ処理をする必要があるでしょうか. 何度も質問してしまい申し訳ありません. お忙しいところ恐縮ですがよろしくお願いします.
Bongo

2020/12/31 15:21

ちょっと対策を考えてみました。回答欄の字数制限のため別回答になりますがご容赦ください。 6面をレンダリングすることについてはUV配置の変更で対応可能かと思いますが(あるいは別案として、UVの代わりにUV2...ライトマップ用UVを使う手も考えられるでしょう)、汚いブロック模様についてはきついですね... シャドウマップの密度を高めるよう設定を変更してみましたが、多少はマシになったものの、キューブのように平らな面でできた物体だと光がどこかの面に対して極めて浅い角度で入射するような条件では深度判定の誤差の影響が強烈に効いてくる様子で、やっぱり見苦しい縞模様が生じてしまいました。 これ以上クオリティを上げたい場合は、以前申し上げたようにUnityの影機能を使わない独自の影描画を行う必要があるかもしれません...
Bongo

2021/01/01 20:28

ブロック模様対策案として、もう一つ別の手を試してみたので追記いたします。 前回の回答への追記だけでいければよかったのですが、申し訳ないことにまたもや字数制限をオーバーしてしまいました。 前半は前回への追記、後半は別回答になっています。
masutake0

2021/01/02 12:50

多くの対策を練ってくださりありがとうございます. 模様がつくのは影生成時の誤差判定によるものだったのですね. 自分の方でもおおかた実行確認できました. 年末年始にもかかわらず,お時間をそいで下さりありがとうございました. とても勉強になり,かつ大変助かりました.
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.36%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問