質問をすることでしか得られない、回答やアドバイスがある。

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

ただいまの
回答率

87.61%

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

解決済

回答 3

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 732

score 3

前提・実現したいこと

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

試したこと

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

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

using UnityEngine;
using System;
using System.IO;
using System.Text;

public class IO_Test : MonoBehaviour 
{

    private string path;
    private string fileName = "output.csv";
    private float timeleft;

    void Start() 
    {
        path = Application.dataPath + "/" + fileName;
        Debug.Log(path);
        ReadFile();
    }


    void Update() 
    {
        //出力処理
        StreamWriter sw;
        FileInfo fi;
        DateTime dt;

        GameObject cube = GameObject.Find("Cube");

        fi = new FileInfo(path);
        //10秒ごとに処理
        timeleft -= Time.deltaTime;
            if (timeleft <= 0.0 ){
                timeleft = 10.0f;
                sw = fi.AppendText();
                sw.Write(cube.transform.eulerAngles.ToString());
                dt = DateTime.Now;
                sw.WriteLine("," + dt);
                sw.Flush();
                sw.Close();
            }
    }

    void ReadFile() {
        FileInfo fi = new FileInfo(path);
        try {
            using (StreamReader sr = new StreamReader(fi.OpenRead(), Encoding.UTF8)) {
                string readTxt = sr.ReadToEnd();
                Debug.Log(readTxt);
            }
        } catch (Exception e) {
            Debug.Log(e);
        }
    }
}


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

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

追記

「Bake」だとプレビューに影のシャドウマスクが選択でき,Assets > Scenes > SampleSceneに追加される
イメージ説明
「Realtime」だとプレビューでshadowmaskがなく影の位置がわからない
イメージ説明

追記2

状況

写真の状態で実行すると模様が入った画像が出力されてしまう
イメージ説明
イメージ説明

試したこと

空のオブジェクトを作成し,作成していただいたShadowCaptorをアタッチ
ShadowCaptorにシェーダーをセット
Cubeに定期的にCapterを実行する以下のスクリプトをアタッチ

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

public class test : MonoBehaviour
{
    private float timeleft;
    GameObject gameobject;
    Renderer cubeRenderer;
    private string path;
    private string fileName = "shadow.png";
    private Material material;

    // Start is called before the first frame update
    void Start()
    {
        gameobject = GameObject.Find("GameObject");
        path = Application.dataPath + "/" + fileName;
        cubeRenderer = GetComponent<Renderer>();
    }
    void Update()
    {
        timeleft -= Time.deltaTime;
        if (timeleft <= 0.0)
        {
            timeleft = 5.0f;

            //ここに処理
            gameobject.GetComponent<ShadowCaptor>().Capture(cubeRenderer, path);

        }

    }

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

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • Bongo

    2020/12/29 03:18

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

    キャンセル

  • masutake0

    2020/12/29 12:06

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

    キャンセル

回答 3

checkベストアンサー

+1

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

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

図1

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

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

図2

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

// Hidden/Internal-ScreenSpaceShadows :
// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)

Shader "Hidden/ShadowCaptor"
{
    Properties
    {
        _ShadowMapTexture ("", any) = "" {}
    }

    SubShader
    {
        Pass
        {
            ZWrite Off
            ZTest Always
            Cull Off

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            // ソフトシャドウ表現を使うかどうかでマルチコンパイル
            #pragma multi_compile _ SOFT_SHADOW

            // ランバート反射係数を乗算するかどうかでマルチコンパイル
            #pragma multi_compile _ MULTIPLY_LAMBERT

            #pragma target 3.0

            // シャドウマップを受け取るための変数
            UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture);
            float4 _ShadowMapTexture_TexelSize;
            #define SHADOWMAPSAMPLER_AND_TEXELSIZE_DEFINED

            // ランバート反射係数を乗算する場合、ディレクショナルライトのforwardをここにセットする
            float3 _LightDirection;

            #include "UnityCG.cginc"
            #include "UnityShadowLibrary.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 clipPos : SV_POSITION;
                float4 worldPos : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
            };

            v2f vert(appdata v)
            {
                v2f o;

                // クリップ座標としてUV座標を加工したものを使い、UVマップに沿ってレンダリングする
                o.clipPos = float4(v.texcoord * 2.0 - 1.0, 0.0, 1.0);
                #if defined(UNITY_REVERSED_Z)
                    o.clipPos.y *= -1.0;
                #endif

                o.worldPos = float4(mul(unity_ObjectToWorld, v.vertex).xyz, -UnityObjectToViewPos(v.vertex).z);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);

                return o;
            }

            inline fixed4 getCascadeWeights(float z)
            {
                fixed4 zNear = float4(z >= _LightSplitsNear);
                fixed4 zFar = float4(z < _LightSplitsFar);
                fixed4 weights = zNear * zFar;
                return weights;
            }

            inline float4 getShadowCoord(float4 wpos, fixed4 cascadeWeights)
            {
                float3 sc0 = mul(unity_WorldToShadow[0], wpos).xyz;
                float3 sc1 = mul(unity_WorldToShadow[1], wpos).xyz;
                float3 sc2 = mul(unity_WorldToShadow[2], wpos).xyz;
                float3 sc3 = mul(unity_WorldToShadow[3], wpos).xyz;
                float4 shadowMapCoordinate = float4(sc0 * cascadeWeights[0] + sc1 * cascadeWeights[1] + sc2 * cascadeWeights[2] + sc3 * cascadeWeights[3], 1.0);
                #if defined(UNITY_REVERSED_Z)
                    float noCascadeWeights = 1.0 - dot(cascadeWeights, 1.0);
                    shadowMapCoordinate.z += noCascadeWeights;
                #endif
                return shadowMapCoordinate;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                float4 wpos = float4(i.worldPos.xyz, 1.0);
                fixed4 cascadeWeights = getCascadeWeights(i.worldPos.w);
                float4 shadowCoord = getShadowCoord(wpos, cascadeWeights);

                // SOFT_SHADOWがオンの場合、近傍比率フィルタリングにより縁をなめらかにする
                #if SOFT_SHADOW
                    #if defined(SHADER_API_MOBILE)
                        half shadow = UnitySampleShadowmap_PCF5x5(shadowCoord, 0.0);
                    #else
                        half shadow = UnitySampleShadowmap_PCF7x7(shadowCoord, 0.0);
                    #endif
                #else
                    fixed shadow = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, shadowCoord);
                #endif

                shadow = lerp(_LightShadowData.r, 1.0, shadow);

                // MULTIPLY_LAMBERTがオンの場合、ランバートの余弦則に基づいて黒を乗せる
                #if MULTIPLY_LAMBERT
                    shadow *= saturate(dot(normalize(i.worldNormal), -_LightDirection));
                #endif

                return fixed4(shadow, shadow, shadow, 1.0);
            }
            ENDCG
        }
    }

    Fallback Off
}

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

using System;
using System.Collections;
using System.IO;
using UnityEngine;
using UnityEngine.Rendering;

public class ShadowCaptor : MonoBehaviour
{
    private static readonly string SoftShadowKeyword = "SOFT_SHADOW";
    private static readonly string MultiplyLambertKeyword = "MULTIPLY_LAMBERT";
    private static readonly int ShadowMapTextureTexelSizeId = Shader.PropertyToID("_ShadowMapTexture_TexelSize");
    private static readonly int LightDirectionId = Shader.PropertyToID("_LightDirection");

    [SerializeField] private Shader captorShader;
    [SerializeField] private Vector2Int size = new Vector2Int(1024, 1024);
    [SerializeField] private Transform lightTransform;
    [SerializeField] private bool softShadow = true;
    [SerializeField] private bool multiplyLambert;

    private Material captorMaterial;
    private CommandBuffer captureCommand;
    private RenderTexture resultRenderTexture;
    private Texture2D resultTexture;

    private void Awake()
    {
        if (this.captorShader == null)
        {
            Debug.LogError("Captor shader is not set.");
            Destroy(this);
            return;
        }

        this.captorMaterial = new Material(this.captorShader);
        this.captorMaterial.SetVector(
            ShadowMapTextureTexelSizeId,
            new Vector4(1.0f / this.size.x, 1.0f / this.size.y, this.size.x, size.y));
        this.resultTexture = new Texture2D(this.size.x, this.size.y);
        this.resultRenderTexture = new RenderTexture(this.size.x, this.size.y, 0);
        this.captureCommand = new CommandBuffer
        {
            name = "Capture shadows"
        };
        Shader.WarmupAllShaders();
    }

    private void OnDestroy()
    {
        Destroy(this.captorMaterial);
        Destroy(this.resultTexture);
        Destroy(this.resultRenderTexture);
    }

    public void Capture(Renderer targetRenderer, string path)
    {
        if (targetRenderer == null)
        {
            throw new ArgumentNullException(nameof(targetRenderer));
        }

        if (string.IsNullOrWhiteSpace(path))
        {
            throw new ArgumentException("Invalid path.");
        }

        this.StartCoroutine(this.CaptureAtEndOfFrame(targetRenderer, path));
    }

    private IEnumerator CaptureAtEndOfFrame(Renderer targetRenderer, string path)
    {
        yield return new WaitForEndOfFrame();

        // レンダリングコマンドを構築
        this.captureCommand.Clear();
        if (this.softShadow)
        {
            this.captureCommand.EnableShaderKeyword(SoftShadowKeyword);
        }
        else
        {
            this.captureCommand.DisableShaderKeyword(SoftShadowKeyword);
        }
        if (this.multiplyLambert && (this.lightTransform != null))
        {
            this.captureCommand.EnableShaderKeyword(MultiplyLambertKeyword);
            this.captorMaterial.SetVector(LightDirectionId, this.lightTransform.forward);
        }
        else
        {
            this.captureCommand.DisableShaderKeyword(MultiplyLambertKeyword);
        }
        this.captureCommand.SetRenderTarget(this.resultRenderTexture);
        this.captureCommand.ClearRenderTarget(false, true, Color.white);
        this.captureCommand.DrawRenderer(targetRenderer, this.captorMaterial);

        // レンダーテクスチャに対してレンダリングを実行し、結果をTexture2Dに読み取らせて
        // PNG形式にエンコードして保存する
        Graphics.ExecuteCommandBuffer(this.captureCommand);
        var activeTexture = RenderTexture.active;
        RenderTexture.active = this.resultRenderTexture;
        this.resultTexture.ReadPixels(new Rect(0, 0, this.size.x, this.size.y), 0, 0);
        RenderTexture.active = activeTexture;
        var resultData = this.resultTexture.EncodeToPNG();
        File.WriteAllBytes(path, resultData);
    }
}

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

図3

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

図4

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

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

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

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/12/31 20:59

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

    キャンセル

  • 2021/01/01 00:21

    ちょっと対策を考えてみました。回答欄の字数制限のため別回答になりますがご容赦ください。

    6面をレンダリングすることについてはUV配置の変更で対応可能かと思いますが(あるいは別案として、UVの代わりにUV2...ライトマップ用UVを使う手も考えられるでしょう)、汚いブロック模様についてはきついですね...

    シャドウマップの密度を高めるよう設定を変更してみましたが、多少はマシになったものの、キューブのように平らな面でできた物体だと光がどこかの面に対して極めて浅い角度で入射するような条件では深度判定の誤差の影響が強烈に効いてくる様子で、やっぱり見苦しい縞模様が生じてしまいました。

    これ以上クオリティを上げたい場合は、以前申し上げたようにUnityの影機能を使わない独自の影描画を行う必要があるかもしれません...

    キャンセル

  • 2021/01/02 05:28

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

    キャンセル

  • 2021/01/02 21:50

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

    キャンセル

+1

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

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

using UnityEngine;

[RequireComponent(typeof(MeshFilter))]
public class CubeMeshUnwrapper : MonoBehaviour
{
    private Mesh cubeMesh;

    private void Awake()
    {
        this.cubeMesh = this.GetComponent<MeshFilter>().mesh;
        var normals = this.cubeMesh.normals;
        var uv = this.cubeMesh.uv;
        var scale = new Vector2(0.25f, 0.5f);
        for (var vertexIndex = 0; vertexIndex < normals.Length; vertexIndex++)
        {
            var normal = normals[vertexIndex];
            var absNormalYz = new Vector2(Mathf.Abs(normal.y), Mathf.Abs(normal.z));
            var x = (int)Vector2.Dot(absNormalYz, new Vector2(1, 2));
            var y = (int)Mathf.Clamp01(-(normal.x + normal.y + normal.z));
            uv[vertexIndex] = (uv[vertexIndex] + new Vector2(x, y)) * scale;
        }
        this.cubeMesh.uv = uv;
        this.cubeMesh.UploadMeshData(true);
    }

    private void OnDestroy()
    {
        Destroy(this.cubeMesh);
    }
}

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

図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に加えて、各面への頂点変換を保持するための下記スクリプトをアタッチしました。

using System.Linq;
using UnityEngine;

[RequireComponent(typeof(MeshFilter))]
public class FaceDecomposer : MonoBehaviour
{
    public int FaceCount { get; private set; }
    public Matrix4x4[] UvMatrices { get; private set; }
    public Matrix4x4[] ObjectToUvMatrices { get; private set; }

    private void Start()
    {
        var mesh = this.GetComponent<MeshFilter>().mesh;
        var vertices = mesh.vertices;
        var uv = mesh.uv;
        var triangles = mesh.triangles;
        this.FaceCount = triangles.Length / 3;
        var matrices = Enumerable.Range(0, this.FaceCount).Select(
            faceIndex =>
            {
                var k0 = faceIndex * 3;
                var k1 = k0 + 1;
                var k2 = k0 + 2;
                var i0 = triangles[k0];
                var i1 = triangles[k1];
                var i2 = triangles[k2];
                var v0 = vertices[i0];
                var v1 = vertices[i1];
                var v2 = vertices[i2];
                var t0 = uv[i0];
                var t1 = uv[i1];
                var t2 = uv[i2];
                var uvMatrix = new Matrix4x4(
                    t2 - t0,
                    t1 - t0,
                    new Vector4(0, 0, 1, 0),
                    new Vector4(t0.x, t0.y, 0, 1));
                var objectMatrix = new Matrix4x4(
                    v2 - v0,
                    v1 - v0,
                    Vector3.Cross(v2 - v0, v1 - v0).normalized,
                    new Vector4(v0.x, v0.y, v0.z, 1));
                var objectToUvMatrix = uvMatrix * objectMatrix.inverse;
                return (uvMatrix, objectToUvMatrix);
            }).ToArray();
        this.UvMatrices = matrices.Select(m => m.uvMatrix).ToArray();
        this.ObjectToUvMatrices = matrices.Select(m => m.objectToUvMatrix).ToArray();
    }
}

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

using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

public class ShadowVolumeCaptor : MonoBehaviour
{
    private static readonly int LightDirectionId = Shader.PropertyToID("_LightDirection");
    private static readonly int WorldToUvId = Shader.PropertyToID("_WorldToUv");

    [SerializeField] private Shader faceMarkerShader;
    [SerializeField] private Shader shadowMarkerShader;
    [SerializeField] private Shader shadowDrawerShader;
    [SerializeField] private Vector2Int size = new Vector2Int(1024, 1024);
    [SerializeField] private Transform lightTransform;

    private readonly Dictionary<MeshFilter, Mesh> shadowVolumes = new Dictionary<MeshFilter, Mesh>();
    private Material faceMarkerMaterial;
    private Material shadowMarkerMaterial;
    private Material shadowDrawerMaterial;
    private Mesh face;
    private Mesh quad;
    private RenderTexture resultRenderTexture;
    private Texture2D resultTexture;

    private void Awake()
    {
        if (this.faceMarkerShader == null)
        {
            Debug.LogError("Face marker shader is not set.");
            Destroy(this);
            return;
        }
        if (this.shadowMarkerShader == null)
        {
            Debug.LogError("Shadow marker shader is not set.");
            Destroy(this);
            return;
        }
        if (this.shadowDrawerShader == null)
        {
            Debug.LogError("Shadow drawer shader is not set.");
            Destroy(this);
            return;
        }

        this.faceMarkerMaterial = new Material(this.faceMarkerShader);
        this.shadowMarkerMaterial = new Material(this.shadowMarkerShader);
        this.shadowDrawerMaterial = new Material(this.shadowDrawerShader);
        this.resultTexture = new Texture2D(this.size.x, this.size.y);
        this.resultRenderTexture = new RenderTexture(this.size.x, this.size.y, 24);
        this.face = new Mesh
        {
            name = "Face",
            vertices = new[] {Vector3.zero, Vector3.up, Vector3.right},
            triangles = new[] {0, 1, 2}
        };
        this.quad = new Mesh
        {
            name = "Quad",
            vertices = new[] {new Vector3(-1, -1, 0), new Vector3(-1, 1, 0), new Vector3(1, -1, 0), new Vector3(1, 1, 0)},
            triangles = new[] {0, 1, 2, 3, 2, 1}
        };
        Shader.WarmupAllShaders();
    }

    private void OnDestroy()
    {
        Destroy(this.faceMarkerMaterial);
        Destroy(this.shadowMarkerMaterial);
        Destroy(this.shadowDrawerMaterial);
        Destroy(this.resultTexture);
        Destroy(this.resultRenderTexture);
        Destroy(this.face);
        Destroy(this.quad);
        foreach (var shadowVolume in this.shadowVolumes.Values)
        {
            Destroy(shadowVolume);
        }
    }

    public void Capture(FaceDecomposer target, string path)
    {
        if (target == null)
        {
            throw new ArgumentNullException(nameof(target));
        }

        if (string.IsNullOrWhiteSpace(path))
        {
            throw new ArgumentException("Invalid path.");
        }

        // レンダーターゲットを設定し...
        var activeTexture = RenderTexture.active;
        RenderTexture.active = this.resultRenderTexture;
        GL.Clear(true, true, Color.white);

        // ライト方向を設定し...
        this.shadowMarkerMaterial.SetVector(LightDirectionId, this.lightTransform.forward);

        // シーン上の各MeshFilterについて...
        var worldToTargetMatrix = target.transform.worldToLocalMatrix;
        foreach (var meshFilter in FindObjectsOfType<MeshFilter>())
        {
            var shadowToWorldMatrix = meshFilter.transform.localToWorldMatrix;

            // シャドウボリュームメッシュを取得し(なければ生成し)...
            if (!this.shadowVolumes.TryGetValue(meshFilter, out var shadowVolume))
            {
                shadowVolume = ShadowMeshAssembler.AssembleShadowMesh(meshFilter.sharedMesh);
                this.shadowVolumes[meshFilter] = shadowVolume;
            }

            // ターゲットを構成する各面について...
            var faceCount = target.FaceCount;
            for (var i = 0; i < faceCount; i++)
            {
                // まず面が占めるピクセルをマークする
                this.faceMarkerMaterial.SetPass(0);
                Graphics.DrawMeshNow(this.face, target.UvMatrices[i]);

                // マークされたピクセルにシャドウボリュームをレンダリングする
                this.shadowMarkerMaterial.SetMatrix(WorldToUvId, target.ObjectToUvMatrices[i] * worldToTargetMatrix);
                this.shadowMarkerMaterial.SetPass(0);
                Graphics.DrawMeshNow(shadowVolume, shadowToWorldMatrix);

                // マーキングを解除する
                this.faceMarkerMaterial.SetPass(1);
                Graphics.DrawMeshNow(this.face, target.UvMatrices[i]);
            }
        }

        // 影の中にいると判定された部分に黒を塗る
        this.shadowDrawerMaterial.SetPass(0);
        Graphics.DrawMeshNow(this.quad, Matrix4x4.identity);

        // 結果をTexture2Dに読み取らせてPNG形式にエンコードして保存する
        this.resultTexture.ReadPixels(new Rect(0, 0, this.size.x, this.size.y), 0, 0);
        RenderTexture.active = activeTexture;
        var resultData = this.resultTexture.EncodeToPNG();
        File.WriteAllBytes(path, resultData);
    }
}

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

+1

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

ShadowVolumeCaptorfaceMarkerShaderは...

Shader "Hidden/FaceMarker"
{
    Properties
    {
    }

    SubShader
    {
        CGINCLUDE
        #include "UnityCG.cginc"

        float4 vert(float4 position : POSITION) : SV_POSITION
        {
            float4 clipPos = mul(unity_ObjectToWorld, position);
            clipPos.xy = clipPos.xy * 2.0 - 1.0;
            #if UNITY_UV_STARTS_AT_TOP
                clipPos.y *= -1.0;
            #endif
            return clipPos;
        }

        fixed4 frag() : SV_Target
        {
            return 0;
        }
        ENDCG

        Pass
        {
            ColorMask 0
            ZWrite Off
            ZTest Always
            Cull Off
            Stencil
            {
                WriteMask 16
                Ref 16
                Comp Always
                Pass Replace
            }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            ENDCG
        }

        Pass
        {
            ColorMask 0
            ZWrite Off
            ZTest Always
            Cull Off
            Stencil
            {
                WriteMask 16
                Ref 0
                Comp Always
                Pass Zero
            }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            ENDCG
        }
    }

    Fallback Off
}

shadowMarkerShaderは...

Shader "Hidden/ShadowMarker"
{
    Properties
    {
    }

    SubShader
    {
        Pass
        {
            ColorMask 0
            ZWrite Off
            ZTest Always
            Cull Off
            Stencil
            {
                ReadMask 16
                WriteMask 15
                Ref 16
                CompFront Equal
                CompBack Equal
                PassFront IncrWrap
                PassBack DecrWrap
            }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            #define FACE_EXTENSION float2(0.00390625, 128.0)
            #define Z_SCALE 0.00390625

            #if defined(UNITY_REVERSED_Z)
                #if UNITY_REVERSED_Z == 1
                    // Direct3D、Z反転あり
                    #define Z_FAR 0.0
                    #define Z_NEAR 1.0
                #else
                    // OpenGL、Z反転あり
                    #define Z_FAR -1.0
                    #define Z_NEAR 1.0
                #endif
            #elif UNITY_UV_STARTS_AT_TOP
                // Direct3D、Z反転なし
                #define Z_FAR 1.0
                #define Z_NEAR 0.0
            #else
                // OpenGL、Z反転なし
                #define Z_FAR 1.0
                #define Z_NEAR -1.0
            #endif

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            float4x4 _WorldToUv;
            float3 _LightDirection;

            float4 vert(appdata v) : SV_POSITION
            {
                float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
                float3 worldNormal = UnityObjectToWorldNormal(v.normal);

                // ライトの方に向いている頂点は-1、背を向けている頂点は1
                float faceSign = sign(dot(worldNormal, _LightDirection));

                // 頂点を光に沿って移動させる
                // ライトに背を向けている頂点を大きく移動させることで、シャドウボリュームを長く引き伸ばす
                // また、ライトの方に向いている頂点もわずかに移動させ、ライト側に向いている面が自身の影に
                // 包まれて黒くなってしまうのを防止する
                worldPos.xyz += dot(saturate(float2(-faceSign, faceSign)), FACE_EXTENSION) * _LightDirection;

                // UV空間に移し、面から手前に突き出た領域を描画する
                float4 clipPos = mul(_WorldToUv, worldPos);
                clipPos.xy = clipPos.xy * 2.0 - 1.0;
                clipPos.z = lerp(Z_NEAR, Z_FAR, max((clipPos.z * Z_SCALE) + 1.0, 0.0));
                #if UNITY_UV_STARTS_AT_TOP
                    clipPos.y *= -1.0;
                #endif
                return clipPos;
            }

            fixed4 frag() : SV_Target
            {
                return 0;
            }
            ENDCG
        }
    }

    Fallback Off
}

shadowDrawerShaderは...

Shader "Hidden/ShadowDrawer"
{
    Properties
    {
    }

    SubShader
    {
        Pass
        {
            ZWrite Off
            ZTest Always
            Cull Off
            Stencil
            {
                ReadMask 15
                Ref 0
                Comp Less
            }

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

            float4 vert(float4 position : POSITION) : SV_POSITION
            {
                return position;
            }

            fixed4 frag() : SV_Target
            {
                return fixed4(0.0, 0.0, 0.0, 1.0);
            }
            ENDCG
        }
    }

    Fallback Off
}

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

図6

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

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

  • ただいまの回答率 87.61%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る