UNITYでDepthをリニアでEXR形式で出力
概要
Depthマップ欲しい事は多いと思います Depthの取得自体は簡単ですが、Depthの計算上 Z値とリニアで変化しないため感覚的にわかりにくいので リニアにしたい DepthをPNGファイルにすると 256段階に丸められてしまうため、浮動小数点形式で保存したいので EXR形式で保存したい Depthの取得は、別パスにて画面全体から取得する。UNITYではポストプロセスで行うのが楽
UNITYちゃん
今回は題材にUNITY Chanを使う 何を使ってもいいし、UNITYの3DMeshでもいい
ポストプロセス
デプスは画面全体を取得する必要があるので、ポストプロセスで取得する ポストプロセスを使うには カメラに OnRenderImage関数の入ったスクリプトをつける
using System.IO; using UnityEngine; using UnityEngine.Rendering; [ExecuteInEditMode] [RequireComponent(typeof(Camera))] public class SceneScript : MonoBehaviour { public Material mat; [ImageEffectOpaque] public void OnRenderImage(RenderTexture source, RenderTexture destination) { Graphics.Blit(source, dest, mat); } }
これで、マテリアルに設定したシェーダーを使い ポストプロセスを実行すると
シェーダーでDepth取得
マテリアルに設定するシェーダーを作成する
まず、デプステクスチャを有効にするため、シェーダーの変数宣言のあたりに
UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
を。これでデプステクスチャに _CameraDepthTexture でアクセスできる
Shader "test/CopyDepth" { SubShader { ZTest Always Cull Off ZWrite Off Fog{ Mode Off } Tags{ "RenderType" = "Opaque" } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #pragma only_renderers d3d9 d3d11 glcore gles gles3 metal xboxone ps4 #pragma target 3.0 UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture); struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert (appdata_img v) { v2f o = (v2f)0; o.pos = UnityObjectToClipPos(v.vertex); o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord.xy); return o; } float frag(v2f i) : SV_TARGET{ return SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); } ENDCG } } }
上記シェーダーをポストプロセスに設定すると、Depthっぽいものが表示される (画面表示はこの段階では崩れているが)
ただしいDepthを取得
Depthテクスチャ用に RenderTextureを生成し、ポストエフェクト時にそこにコピーする
using System.IO; using UnityEngine; using UnityEngine.Rendering; // deplicated [ExecuteInEditMode] [RequireComponent(typeof(Camera))] public class SceneScript : MonoBehaviour { public Material mat; private RenderTexture _rtd; // Use this for initialization void Start() { GetComponent<Camera>().depthTextureMode |= DepthTextureMode.Depth; _rtd = new RenderTexture(2048, 2048, 32); } [ImageEffectOpaque] public void OnRenderImage(RenderTexture source, RenderTexture destination) { Graphics.Blit(source, _rtd, mat); } }
見てもわからないが、ポストエフェクトで画面に出力せず、 rtdへコピーされているので rtdの値を出力すればよい
EXRで保存
float形式で保存するために EXR形式を使う UNITYでは EXRへの変換は簡単にできる Aを押したらファイルに保存する
void Update () { if (Input.GetKey(KeyCode.A)){ RT2EXR(_rtd, "rtd.exr"); } } void RT2EXR(RenderTexture rt, string filename) { Texture2D tex = new Texture2D(rt.width, rt.height, TextureFormat.RGBAHalf, false); RenderTexture.active = rt; tex.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0); tex.Apply(); byte[] bytes = tex.EncodeToEXR(); Object.Destroy(tex); File.WriteAllBytes(filename, bytes); }
これで exrファイルとして出力できた
リニア化
上記のままでは、Depthの値はZとリニアではないので 直観的にわかりやすく リニアに変換する UNITYには便利な組み込み関数があるので それでリニアにできる
// Z buffer to linear 0..1 depth inline float Linear01Depth( float z ) { return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y); }
これでzバッファの値をリニアに変換できる _ZBufferParams 等はシェーダーのビルトイン変数といい、UNITYがConstantBufferに送っている
// x = 1 or -1 (-1 if projection is flipped) // y = near plane // z = far plane // w = 1/far plane float4 _ProjectionParams; // x = width // y = height // z = 1 + 1.0/width // w = 1 + 1.0/height float4 _ScreenParams; // Values used to linearize the Z buffer (http://www.humus.name/temp/Linearize%20depth.txt) // x = 1-far/near // y = far/near // z = x/far // w = y/far // or in case of a reversed depth buffer (UNITY_REVERSED_Z is 1) // x = -1+far/near // y = 1 // z = x/far // w = 1/far float4 _ZBufferParams; // x = orthographic camera's width // y = orthographic camera's height // z = unused // w = 1.0 if camera is ortho, 0.0 if perspective float4 unity_OrthoParams;
具体的には上記のような、near far等の値が入っている
シェーダーを少し変更する
float frag(v2f i) : SV_TARGET{ return Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv).x ) ; }
これでリニアになったはずである(多分)
まとめ
using System.IO; using UnityEngine; using UnityEngine.Rendering; [ExecuteInEditMode] [RequireComponent(typeof(Camera))] public class SceneScript : MonoBehaviour { public Material mat; private RenderTexture _rtd; // Use this for initialization void Start() { GetComponent<Camera>().depthTextureMode |= DepthTextureMode.Depth; _rtd = new RenderTexture(2048, 2048, 32); } [ImageEffectOpaque] public void OnRenderImage(RenderTexture source, RenderTexture destination) { Graphics.Blit(source, _rtd, mat); } // Update is called once per frame void Update () { if (Input.GetKey(KeyCode.A)){ RT2EXR(_rtd, "rtd.exr"); } } void RT2EXR(RenderTexture rt, string filename) { Texture2D tex = new Texture2D(rt.width, rt.height, TextureFormat.RGBAHalf, false); RenderTexture.active = rt; tex.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0); tex.Apply(); byte[] bytes = tex.EncodeToEXR(); Object.Destroy(tex); File.WriteAllBytes(filename, bytes); } }
Shader "test/CopyDepth" { SubShader { ZTest Always Cull Off ZWrite Off Fog{ Mode Off } Tags{ "RenderType" = "Opaque" } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #pragma only_renderers d3d9 d3d11 glcore gles gles3 metal xboxone ps4 #pragma target 3.0 UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture); struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert (appdata_img v) { v2f o = (v2f)0; o.pos = UnityObjectToClipPos(v.vertex); o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord.xy); return o; } float frag(v2f i) : SV_TARGET{ return Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv).x ) ; } ENDCG } } }