凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

デプスセンサの値を Deferred Rendering の G-Buffer に書き込んでみた

はじめに

Deferred Rendering では G-Buffer にデプスや法線、拡散色などの情報を予め描きこんでおき、その情報を使ってシェーディングやライティングの処理が行われます。

これを利用してあげると距離関数でレイトレースした非ポリゴンの図形とポリゴンの図形を良い感じにマージしたりと面白い表現も出来ます。

そこでこの G-Buffer を描きこむパスで、デプスセンサから取得したデプスおよびそれを元に計算した法線を描きこんでみたら、メッシュを生成することなく遮蔽も再現できる画が作れるのではないかと思いやってみました。

デモ

コード

解説

概要

Kinect For Windows の Unity 向け SDK では Windows.Kinect.DepthFrameReader を通じて ushoft[] でデプス画像を取ってくることが出来ます。これを Texture2D.LoadRawTextureData() で読み込むのですが、この APIbyte[] しか受け付けないので単色では 255 段階しか保存できません( Kinect V2 ではデータが 8000 分割(~8000 mm)されている)。そこで RGB のうちの R と G を使って保存し、シェーダ側で展開します(理想的には C++ から直接 float の値を書き込むのが良いのですが面倒なので...)。

スクリプト

using UnityEngine;
using Windows.Kinect;

public class KinectDepthSourceManager : MonoBehaviour
{
    private KinectSensor sensor_;
    private DepthFrameReader depthReader_;
    private ushort[] data_;
    private byte[] rawData_;

    private Texture2D texture_;
    public Texture2D GetDepthTexture()
    {
        return texture_;
    }

    public ushort[] GetData()
    {
        return data_;
    }

    void Awake()
    {
        sensor_ = KinectSensor.GetDefault();

        if (sensor_ != null) {
            depthReader_ = sensor_.DepthFrameSource.OpenReader();
            var frameDesc = sensor_.DepthFrameSource.FrameDescription;
            data_ = new ushort[frameDesc.LengthInPixels];
            rawData_ = new byte[frameDesc.LengthInPixels * 3];
            texture_ = new Texture2D(frameDesc.Width, frameDesc.Height, TextureFormat.RGB24, false);

            if (!sensor_.IsOpen) {
                sensor_.Open();
            }
        }
    }

    void Update()
    {
        if (depthReader_ != null) {
            var frame = depthReader_.AcquireLatestFrame();
            if (frame != null) {
                frame.CopyFrameDataToArray(data_);

                for (int i = 0; i < data_.Length; ++i) {
                    rawData_[3 * i + 0] = (byte)(data_[i] / 256);
                    rawData_[3 * i + 1] = (byte)(data_[i] % 256);
                    rawData_[3 * i + 2] = 0;
                }

                texture_.LoadRawTextureData(rawData_);
                texture_.Apply();

                frame.Dispose();
                frame = null;
            }
        }
    }

    void OnApplicationQuit()
    {
        if (depthReader_ != null) {
            depthReader_.Dispose();
            depthReader_ = null;
        }

        if (sensor_ != null) {
            if (sensor_.IsOpen) sensor_.Close();
            sensor_ = null;
        }
    }
}

シェーダ

先ほど RG を使って保存した値を GetDepth() で展開してメートルに変換しています。あとは Kinect V2 の視野角の値を使って x, y を復元し、そこから Y-Z 平面および X-Z 平面の単位ベクトルを求めて外積を取ることで法線を求め、これらの値を G-Buffer に描き込みます。画面を覆う板ポリに対して書き込んでおり、Unity の世界のカメラの視野角も合わせていないため、x, y に関しては Unity の世界のそれらと一致しませんが、z で遮蔽を再現することが出来ます。後は適当にグリッドを emission に書き込むことで描画しています。

Shader "Hidden/KinectDepth"
{

Properties
{
    _MainTex ("Main Texture", 2D) = "" {}
    _LineIntensity ("Line Intensity", Range(0, 30)) = 1.0
    _LineResolution ("Line Resolution", Range(0, 100)) = 10.0
}

SubShader
{

Tags { "RenderType" = "Opaque" "DisableBatching" = "True" "Queue" = "Geometry+10" }
Cull Off

Pass
{
    Tags { "LightMode" = "Deferred" }

    Stencil 
    {
        Comp Always
        Pass Replace
        Ref 128
    }

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma target 3.0
    #pragma multi_compile ___ UNITY_HDR_ON

    #include "UnityCG.cginc"

    struct VertInput
    {
        float4 vertex : POSITION;
    };

    struct VertOutput
    {
        float4 vertex    : SV_POSITION;
        float4 screenPos : TEXCOORD0;
    };

    struct GBufferOut
    {
        half4 diffuse  : SV_Target0; // rgb: diffuse,  a: occlusion
        half4 specular : SV_Target1; // rgb: specular, a: smoothness
        half4 normal   : SV_Target2; // rgb: normal,   a: unused
        half4 emission : SV_Target3; // rgb: emission, a: unused
        float depth    : SV_Depth;
    };

    sampler2D _MainTex;
    sampler2D _KinectDepthTexture;
    float4 _KinectDepthTexture_TexelSize;
    float _LineIntensity;
    float _LineResolution;

    float GetDepth(float2 uv)
    {
        uv.y *= -1.0;
        uv.x = 1.0 - uv.x;
        float3 v = tex2D(_KinectDepthTexture, uv);
        return (v.r * 65536 + v.g * 256) * 0.001;
    }

    float3 GetPosition(float2 uv)
    {
        float z = GetDepth(uv);
        float u = 2.0 * (uv.x - 0.5);
        float v = 2.0 * (uv.y - 0.5);
        float xHalf = z * 0.70803946712; // tan(35.3 deg), fov_x = 70.6 (deg).
        float yHalf = z * 0.57735026919; // tan(30.0 deg), fov_y = 60.0 (deg).
        float x = u * xHalf; 
        float y = v * yHalf;

        return float3(x, y, z);
    }

    float3 GetNormal(float2 uv)
    {
        float2 uvX = uv - float2(_KinectDepthTexture_TexelSize.x, 0);
        float2 uvY = uv - float2(0, _KinectDepthTexture_TexelSize.y);

        float3 pos0 = GetPosition(uv);
        float3 posX = GetPosition(uvX);
        float3 posY = GetPosition(uvY);

        float3 dirX = normalize(posX - pos0);
        float3 dirY = normalize(posY - pos0);

        return 0.5 + 0.5 * cross(dirY, dirX);
    }

    float GetDepthForBuffer(float2 uv)
    {
        float4 vpPos = mul(UNITY_MATRIX_VP, float4(GetPosition(uv), 1.0));
        return vpPos.z / vpPos.w;
    }

    VertOutput vert(VertInput v)
    {
        VertOutput o;
        o.vertex = v.vertex;
        o.screenPos = ComputeScreenPos(v.vertex);
        return o;
    }
    
    GBufferOut frag(VertOutput i)
    {
        float2 uv = i.screenPos.xy / i.screenPos.w;

        float depth = GetDepthForBuffer(uv);
        float3 pos = GetPosition(uv);
        float4 normal = float4(GetNormal(uv), 1.0);

        float u = fmod(pos.x, 1.0) * _LineResolution;
        float v = fmod(pos.y, 1.0) * _LineResolution;
        float w = fmod(pos.z, 1.0) * _LineResolution;

        GBufferOut o;
        o.diffuse = normal;
        o.specular = float4(0.0, 0.0, 0.0, 0.0);
        o.emission = _LineIntensity * float4(
            tex2D(_MainTex, float2(w, 0)).r, 
            tex2D(_MainTex, float2(v, 0)).r, 
            tex2D(_MainTex, float2(u, 0)).r, 
            1.0);
        o.depth = depth;
        o.normal = normal;

#ifndef UNITY_HDR_ON
        o.emission = exp2(-o.emission);
#endif

        return o;
    }

    ENDCG
}

}

Fallback Off
}

結果

手抜きですが MotionBlur の Image Effect を追加することで良い感じに平均化され、ノイズを低減できます。結構綺麗にデプスが取れるので遮蔽も綺麗です。

f:id:hecomi:20160726011552p:plain

おわりに

まだどこか間違っているようでイマイチ Z も一致してないですが....、HMD からのビューで手を出すときに軽い処理にしたい、でも遮蔽は欲しいみたいなときに、HMD につけたデプスセンサから取得した手を描画するときなどに使えそうです。