3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity】シェーダーに対する空間的なアバターインタラクションの作成

3
Posted at

先日公開した流体シミュレーション1のアバターインタラクションに少し特殊な方法を使ったので紹介します.
今回はシェーダーを知っている人向けの記事になります.

Video Project 2 (1).gif

背景

シェーダーでシミュレーションなどを行っていると,アバターとのインタラクションが必要になる場合があります.
インタラクションの手段は,おおよそ2種類かと思われます.

  • 座標で渡す
    アバターのボーンの位置や速度をスクリプトで取得してマテリアルにセットする.
  • テクスチャで渡す
    アバターをカメラで撮影してレンダリングしたテクスチャをマテリアルにセットする.

アバターとのインタラクションを求める場合,「触る」体験を求める場合が多いと思います.そのため,アバターの境界面を正確に取得できる「テクスチャで渡す」方法をよく使います.

テクスチャで渡す方法については,下記の2つが汎用的です.

  • テクスチャを読む
  • 深度バッファを読む

水面のシミュレーションのようにインタラクションが平面的な場合は,カメラの撮影範囲を平面にそろえて撮影し,撮れたテクスチャに色がついているかで判定すれば十分です.
空間的なインタラクションが必要な場合は,カメラでの撮影結果から深度バッファを読み,ワールド座標を復元することも可能です2

また,使用場面に制限がありますが,

  • 特殊シェーダーで任意データのテクスチャを作成する

ということも可能です.便利なのでよく使います.今回紹介するのもこの方法です.

前述のとおり,深度バッファを使うことで空間的なインタラクションを作ることは可能ですが,カメラで撮影している以上,カメラから向かって正面の情報しか取れないので,アバターの側面や背中側の情報などは取得できません.空間の流体シミュレーションの場合これでは不十分なので別の手段が必要になります.

前提知識 (3Dテクスチャ)

本手法では,3Dテクスチャの概念が前提となります.実装では使わないんですけどね.

3Dテクスチャとボクセル

テクスチャといえば普通は平面(2Dテクスチャ)ですが,3Dテクスチャというものもあります.

2Dテクスチャは一般的には画像のことです.連続的なイメージがありますが,十分拡大すればピクセルが見えてくるので,実際は離散的なデータです.2Dテクスチャを作る(撮影する)ということは,物理世界の連続的な値を離散化して保存することに相当します.ドット絵を作っているイメージです.

スクリーンショット 2026-04-12 160637.png

平面に対して離散化を行うのが2Dテクスチャなので,空間に対して離散化を行えば3Dテクスチャができます.マインクラフトのイメージです.

スクリーンショット 2026-04-12 150441.png

2Dテクスチャでは離散化した1マスのことをピクセル(pixel)と呼んでいます.3Dテクスチャでは離散化するとキューブができますね.このキューブのことをボクセル(boxel)と呼んでいます.

2D <-> 3D のマッピング

空間の情報を取りたいので,欲しいのは3Dテクスチャですが,カメラの撮影結果は2Dテクスチャです.なので,2Dテクスチャと3Dテクスチャの相互変換が必要になります.
これについては,3Dテクスチャをスライスして,それを1枚ずつ2Dテクスチャに並べる方法を使います.

▼ 3Dテクスチャは離散的なので2Dテクスチャの集合とみなせます.
スクリーンショット 2026-04-12 151102.png

▼ CTとかMRIみたいな感じです.1枚1枚のことをスライスと呼んだりします.
スクリーンショット 2026-04-12 141631.png

▼ スライスをタイル状に並べ,まとめて1枚の2Dテクスチャとして扱います.
スクリーンショット 2026-04-12 144814.png

ちなみにこのアバターはRadDoll3です.かわいいですね.

原理

シミュレーションを行う空間のどのあたりにアバターのポリゴンがあるかがわかる3Dテクスチャを作る,というのが基本的なコンセプトです.
シミュレーション側はこの3Dテクスチャをサンプルしてインタラクションに必要な情報を取得します.LightVolumeと同じですね4

大まかな流れは,

  1. アバターのシェーダーを置き換える
  2. ポリゴンの座標をレンダーテクスチャに書き込む
  3. 作ったテクスチャにブラーをかける
  4. 3Dテクスチャとしてサンプルする

です.

アバターのシェーダーを置き換える

Camera.SetReplacementShader() を使ってアバターのシェーダーを置き換えます.
アバター関係のレンダラーがあるレイヤーは,

  • Player (自分以外のアバター)
  • PlayerLocal (自分のアバターの頭以外)
  • MirrorReflection (自分のアバター)

なので,Player と MirrorReflection のみを撮影します.

ポリゴンの座標をテクスチャに書き込む

撮影する空間の各ボクセルに含まれるポリゴンの重心を求めます.主要な処理はすべてジオメトリシェーダで行います.

ポリゴンの図心を求める

ジオメトリシェーダではメッシュの三角形を構成する3頂点を引数で受け取れるので,三角形の図心を計算できます.また,三角形の面積もわかります.

図心の情報を書き込むピクセルを求める

求めた図心が空間のどのボクセルに含まれるかを計算します.ボクセルがわかったら3Dテクスチャ-->2Dテクスチャの変換で書き込むピクセルを求めます.

ボクセルに含まれるポリゴンの重心を求める

求めたピクセルに三角形の図心を書き込みます.加算合成で書き込んでいきますが,この時,(図心の座標) $\times$ (三角形の面積) の値を書き込むようにします.すべて書き込んだ後で,最後に三角形の面積の和で割れば重心が求まります.(が,この時点ではまだ割りません.)
重心と書いていますが,正確には面積に基づく重み付き平均です.

ブラーをかける

前段までで目的の3Dテクスチャはいったん完成していますが,流体シミュレーションの入力としてはちょっと使いづらいです.
現状の3Dテクスチャは,ポリゴンが含まれていたボクセルに関しては近くのポリゴンの情報が入っていますが,ポリゴンが含まれていなかったボクセルに関しては情報が入っていないので,周囲のボクセルをいくつか読み取って探索する必要があります.
テクスチャを読み取るシミュレーション側で探索を行ってもよかったのですが,なんか嫌だったので,先にブラーをかけておくことにしました.

比喩ではなく本当にガウシアンブラーをかけます.SDFを作る方がいいのだと思いますが5,作りたいシミュレーションが接触(衝突)を伴わないシミュレーションだったので,このくらい適当でも大丈夫でした.

3Dテクスチャとしてサンプルする

シミュレーションで使うときは,前段まででできた2Dテクスチャを3Dテクスチャとしてサンプルします.
テクスチャが本当にTexture3Dであれば tex3Dlod() でサンプルできるのですが,今回は正体がTexture2Dなので,Trilinearでサンプルする関数を自前で用意する必要があります.

サンプルしたら,最後に面積で割れば,近接ポリゴンの推定位置が取得できます.

速度が欲しい場合は,前フレームのキャプチャを保持しておいて,同じ位置でのサンプル値の差分を取ればおおよその速度が求まります.

実装

規模は,

  • 撮影空間: (x, y, z) = (7.0, 3.5, 7.0)
  • 解像度: (x, y, z) = (128, 64, 128)

とします.
この時,2Dテクスチャの解像度は,

  • 2Dテクスチャ: (x, y) = (1024, 1024)

になります.

システム

システム全体の構成は下のスクショのようになっています.

スクリーンショット 2026-04-12 214242.png

CaptureTexture はカメラでアバターのメッシュをキャプチャする Render Texture,Blur はガウシアンブラーをかける Custom Render Texture です.
Visualizer の部分は不要ですが,結果の確認のために置いています.

Blur テクスチャのプロパティについて少し説明しておきます.
Blur テクスチャはブラー用ですが,前フレームのバッファにも使うので,幅が2倍になっています.
Update Zone 0 はバッファのシフトと,Capture Texture のコピーです.
Update Zone 1-3 は,X,Y,Z のブラーを1次元ずつかけています.テクスチャの左半分が Capture Texture の流し込み,右半分は前フレームのバッファ用なので,左半分だけブラーをかけます.
Update Zone 4 はブラーの最終状態をスワップして表に出すためのダミーです.以前いろいろ調べて,これが一番簡単そうでした6

シェーダー

シェーダーは3つですが,共通の定数などはインクルードファイルに書いておきます.

Definition.cginc
Definition.cginc
#ifndef TWS__DEFINITION_CGINC__INCLUDED
#define TWS__DEFINITION_CGINC__INCLUDED


#define SIMULATION_SCALE float3( 7.0, 3.5, 7.0 )
#define SIMULATION_POSITION float3( 0.0, 1.75, 0.0 )

#define TEXEL_SIZE_3D float3( 128, 64, 128 )
#define TEXEL_SIZE_2D float2( 1024, 1024 )


float3 Uv2dToUv3d( float2 uv2d )
{
    float2 tileCount2d = floor( TEXEL_SIZE_2D.xy / TEXEL_SIZE_3D.xy );
    float2 tiledUv = uv2d * tileCount2d;
    float2 tileIndex2d = floor( tiledUv );
    float slice = tileIndex2d.y * tileCount2d.x + tileIndex2d.x;
    return float3( frac( tiledUv.xy ), ( slice + 0.5 ) / TEXEL_SIZE_3D.z );
}

float2 Uv3dToUv2d( float3 uv3d )
{
    float2 tileCount2d = floor( TEXEL_SIZE_2D.xy / TEXEL_SIZE_3D.xy );
    float slice = floor( uv3d.z * TEXEL_SIZE_3D.z );
    float2 tileIndex2d = float2( slice % tileCount2d.x, floor( slice / tileCount2d.x ) );
    return ( tileIndex2d + uv3d.xy ) / tileCount2d;
}

float4 TrilinearSampleBuffer( Texture2D<float4> tex, float3 uv3d, uint bufferIndex )
{
    // サンプル位置の特定に分岐がいらなくなるように,スケール後に -0.5 ずらす
    // Clamp でサンプルするので,1テクセル分小さい範囲でクランプする
    float3 scaledUv = clamp( uv3d * TEXEL_SIZE_3D - 0.5, 0, TEXEL_SIZE_3D - 1 );
    
    uint3 index3d0 = uint3( floor( scaledUv ) );
    uint3 index3d1 = min( index3d0 + 1, ( uint3 )TEXEL_SIZE_3D - 1 );
    
    uint2 tileCount2d = uint2( floor( TEXEL_SIZE_2D.xy / TEXEL_SIZE_3D.xy ) );
    uint bufferOffset = bufferIndex * tileCount2d.x;
    uint2 tileIndex2d0 = uint2( index3d0.z % tileCount2d.x + bufferOffset, floor( index3d0.z / tileCount2d.x ) );
    uint2 tileIndex2d1 = uint2( index3d1.z % tileCount2d.x + bufferOffset, floor( index3d1.z / tileCount2d.x ) );
    uint2 offset0 = tileIndex2d0 * ( uint2 )( TEXEL_SIZE_3D.xy );
    uint2 offset1 = tileIndex2d1 * ( uint2 )( TEXEL_SIZE_3D.xy );
    
    float4 c000 = tex[ uint2( index3d0.x, index3d0.y ) + offset0 ];
    float4 c100 = tex[ uint2( index3d1.x, index3d0.y ) + offset0 ];
    float4 c010 = tex[ uint2( index3d0.x, index3d1.y ) + offset0 ];
    float4 c110 = tex[ uint2( index3d1.x, index3d1.y ) + offset0 ];
    float4 c001 = tex[ uint2( index3d0.x, index3d0.y ) + offset1 ];
    float4 c101 = tex[ uint2( index3d1.x, index3d0.y ) + offset1 ];
    float4 c011 = tex[ uint2( index3d0.x, index3d1.y ) + offset1 ];
    float4 c111 = tex[ uint2( index3d1.x, index3d1.y ) + offset1 ];
    
    float3 ratio = frac( scaledUv );
    float4 c00 = lerp( c000, c100, ratio.x );
    float4 c10 = lerp( c010, c110, ratio.x );
    float4 c01 = lerp( c001, c101, ratio.x );
    float4 c11 = lerp( c011, c111, ratio.x );
    float4 c0  = lerp( c00, c10, ratio.y );
    float4 c1  = lerp( c01, c11, ratio.y );
    float4 c   = lerp( c0, c1, ratio.z );

    return c;
}


#endif // TWS__DEFINITION_CGINC__INCLUDED

TrilinearSample の原型はこちらからいただきました.

というか,流体シミュレーション自体これを参考にしています.

キャプチャ用のシェーダー

アバターのシェーダーを置き換えてキャプチャするためのシェーダーです.
原理で説明した手順の通りに処理をしています.

Capture.shader
Capture.shader
Shader "TsukimiWS/Test/VolumetricPolygonCapture/Capture"
{
    Properties
    {
        /* none */
    }
    SubShader
    {
        Tags { "Queue"="Overlay" "RenderType"="Transparent" }
        
        Pass
        {
            Blend One One
            ZWrite Off
            ZTest Always
            Cull Off

            CGPROGRAM
            #pragma vertex vert
            #pragma geometry geom
            #pragma fragment frag

            #pragma target 4.0
            #pragma multi_compile_instancing

            #include "UnityCG.cginc"
            #include "./Definition.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2g
            {
                float4 pos : POSITION;
            };

            struct g2f
            {
                float4 pos : SV_POSITION;
                float4 worldPos : TEXCOORD0;
            };

            v2g vert ( appdata v )
            {
                v2g o;
                UNITY_SETUP_INSTANCE_ID( v );
                o.pos = mul( unity_ObjectToWorld, v.vertex );
                return o;
            }

            [ maxvertexcount( 1 ) ]
            void geom ( triangle v2g IN[ 3 ], inout PointStream<g2f> stream )
            {
                g2f o;

                // 三角形の面積
                float area = length( cross( IN[ 1 ].pos.xyz - IN[ 0 ].pos.xyz, IN[ 2 ].pos.xyz - IN[ 0 ].pos.xyz ) );
                // 三角形の図心
                float3 worldPos = ( IN[ 0 ].pos.xyz + IN[ 1 ].pos.xyz + IN[ 2 ].pos.xyz ) / 3.0;
                // 面積で重みづけ
                o.worldPos = float4( worldPos, 1 ) * area;

                // 描画位置を求める
                float3 uv3d = ( worldPos - SIMULATION_POSITION ) / SIMULATION_SCALE;
                if ( any( abs( uv3d ) > 0.5 ) )
                {
                    // 範囲外は描画しない
                    return;
                }
                float3 index3d = clamp( floor( ( uv3d + 0.5 ) * TEXEL_SIZE_3D ), 0, TEXEL_SIZE_3D - 1 );
                uv3d = ( index3d + 0.5 ) / TEXEL_SIZE_3D;
                float2 uv2d = Uv3dToUv2d( uv3d );

                // スクリーンは [-1, 1]
                float2 screenPos = uv2d * 2 - 1;
                #if UNITY_UV_STARTS_AT_TOP
                    screenPos.y = -screenPos.y;
                #endif

                o.pos = float4( screenPos, 0.5, 1 );
                stream.Append( o );
                stream.RestartStrip();
            }

            float4 frag ( g2f IN ) : SV_Target
            {
                return IN.worldPos;
            }
            ENDCG
        }
    }
}

ガウシアンブラー用のシェーダー

複数パスで同じ処理をするので,コンパイルスイッチできるように処理本体はインクルードファイルに分離しています.

Blur.cginc
Blur.cginc
#define CRT_FORMAT float4
#include "./UnityCustomRenderTexture-Format.cginc"

float4 frag( v2f_customrendertexture IN ) : SV_Target
{
    float2 uv2d = IN.localTexcoord.xy;

    float2 index2d = floor( uv2d * TEXEL_SIZE_2D.xy );
    float2 tileCount2d = floor( TEXEL_SIZE_2D.xy / TEXEL_SIZE_3D.xy );
    float2 tileIndex2d = floor( index2d / TEXEL_SIZE_3D.xy );
    float slice = tileIndex2d.x + tileIndex2d.y * tileCount2d.x;
    float3 index3d = float3( index2d % TEXEL_SIZE_3D.xy, slice );

    float3 index3dMin = max( index3d - BLUR_SIZE, 0 );
    float3 index3dMax = min( index3d + BLUR_SIZE + 1, TEXEL_SIZE_3D );

    float2 indexRange = float2( 0, 0 );
    float indexCenter = 0;
    #if defined( TWS_BLUR_FIRST_PASS )
        indexRange = float2( index3dMin.x, index3dMax.x ) + tileIndex2d.x * TEXEL_SIZE_3D.x;
        indexCenter = index2d.x;
    #elif defined( TWS_BLUR_SECOND_PASS )
        indexRange = float2( index3dMin.y, index3dMax.y ) + tileIndex2d.y * TEXEL_SIZE_3D.y;
        indexCenter = index2d.y;
    #elif defined( TWS_BLUR_THIRD_PASS )
        indexRange = float2( index3dMin.z, index3dMax.z );
        indexCenter = index3d.z;
    #endif
    
    float4 buf = 0;
    float totalWeight = 0;
    uint2 sampleIndex2d = uint2( index2d );
    const float k = 5.0;  // ブラーの広がり具合を調整する定数

    [ unroll( 2 * BLUR_SIZE + 1 ) ]
    for ( float index = indexRange.x; index < indexRange.y; index++ )
    {
        float dist = ( index - indexCenter ) / BLUR_SIZE;
        float weight = exp( -0.5 * dist * dist * k );
        totalWeight += weight;
        #if defined( TWS_BLUR_FIRST_PASS )
            sampleIndex2d.x = uint( index );
        #elif defined( TWS_BLUR_SECOND_PASS )
            sampleIndex2d.y = uint( index );
        #elif defined( TWS_BLUR_THIRD_PASS )
            float2 sampleTileIndex2d = float2( index % tileCount2d.x, floor( index / tileCount2d.x ) );
            sampleIndex2d = uint2( sampleTileIndex2d * TEXEL_SIZE_3D.xy + index3d.xy );
        #endif
        buf += weight * _SelfTexture2D[ sampleIndex2d ];
    }
    buf /= totalWeight;

    return buf;
}
Blur.shader
Blur.shader
Shader "TsukimiWS/Test/VolumetricPolygonCapture/Blur"
{
    Properties
    {
        [NoScaleOffset] _MainTex ("Input Texture", 2D) = "black" {}
    }
    SubShader
    {
        CGINCLUDE
            #include "./Definition.cginc"
            #define BLUR_SIZE    3
            #define BUFFER_SIZE  2
        ENDCG

        Pass
        {
            Name "Copy Pass"

            CGPROGRAM
            #pragma vertex CustomRenderTextureVertexShader
            #pragma fragment frag

            #include "UnityCustomRenderTexture.cginc"

            sampler2D _MainTex;

            float4 frag( v2f_customrendertexture IN ) : SV_Target
            {
                float2 uv2d = IN.globalTexcoord.xy;
                float bufferIndex = floor( uv2d.x * BUFFER_SIZE );
                float4 col;
                if ( bufferIndex != 0 )
                {
                    // バッファを右にシフト
                    float2 nextUv = float2( uv2d.x - 1.0 / BUFFER_SIZE, uv2d.y );
                    col = tex2Dlod( _SelfTexture2D, float4( nextUv, 0, 0 ) );
                }
                else
                {
                    // バッファの左端にコピー
                    float2 localUv = float2( frac( uv2d.x * BUFFER_SIZE ), uv2d.y );
                    col = tex2Dlod( _MainTex, float4( localUv, 0, 0 ) );
                }
                
                return col;
            }
            ENDCG
        }

        Pass
        {
            Name "Blur 1st Pass"

            CGPROGRAM
            #pragma target 3.5
            #pragma vertex CustomRenderTextureVertexShader
            #pragma fragment frag
            #define TWS_BLUR_FIRST_PASS
            #include "./Blur.cginc"
            ENDCG
        }

        Pass
        {
            Name "Blur 2nd Pass"

            CGPROGRAM
            #pragma target 3.5
            #pragma vertex CustomRenderTextureVertexShader
            #pragma fragment frag
            #define TWS_BLUR_SECOND_PASS
            #include "./Blur.cginc"
            ENDCG
        }

        Pass
        {
            Name "Blur 3rd Pass"

            CGPROGRAM
            #pragma target 3.5
            #pragma vertex CustomRenderTextureVertexShader
            #pragma fragment frag
            #define TWS_BLUR_THIRD_PASS
            #include "./Blur.cginc"
            ENDCG
        }
        
        Pass
        {
            Name "Dummy Pass"

            CGPROGRAM
            #pragma target 3.5
            #pragma vertex CustomRenderTextureVertexShader
            #pragma fragment frag

            #include "UnityCustomRenderTexture.cginc"
            float4 frag( v2f_customrendertexture IN ) : SV_Target
            {
                return tex2Dlod( _SelfTexture2D, float4( IN.globalTexcoord.xy, 0, 0 ) );
            }
            ENDCG
        }
    }
}

UnityCustomRenderTexture-Format.cginc は,以前 uint4 用に作った UnityCustomRenderTexture-Uint.cginc を CRT_FORMAT で置き換えたバージョンです.

今回は float4 なうえにCRT側で Filter Mode に Point を指定しているので tex2Dlod() でも同じですが,まあ筆者の好みです.

ガウシアンブラーの実装はこちらを参考にしています.

結果確認用のシェーダー (サンプル方法の例)

レイマーチングでメッシュがある場所を表示します.Cube のメッシュにアタッチする前提です.

Visualizer.shader
Visualizer.shader
Shader "TsukimiWS/Test/VolumetricPolygonCapture/Visualizer"
{
    Properties
    {
        [NoScaleOffset] _MainTex ("Input Texture", 2D) = "black" {}
        _NeighborDistance ("Neighbor Distance", Range(0, 1)) = 0.1
        [PowerSlider(2)] _Alpha ("Alpha", Range(0,1)) = 0.1
        _StepCount ("Step Count", Range(1, 1024)) = 128
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }

        Blend SrcAlpha OneMinusSrcAlpha
        ZWrite Off
        ZTest Always
        Cull Front

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "./Definition.cginc"

            #define STEP_COUNT_LIMIT 1024

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 vertex : TEXCOORD0;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            Texture2D<float4> _MainTex;
            float _NeighborDistance;
            float _Alpha;
            uint _StepCount;

            v2f vert( appdata v )
            {
                v2f o;
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO( o );
                o.pos = UnityObjectToClipPos( v.vertex );
                o.vertex = v.vertex;
                return o;
            }

            float4 frag( v2f IN ) : SV_Target
            {
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX( IN );

                // オブジェクトのローカルでのカメラ位置と視線方向
                float3 viewPos = mul( unity_WorldToObject, float4( _WorldSpaceCameraPos, 1 ) ).xyz;
                float3 viewDir = normalize( IN.vertex.xyz - viewPos );

                float2 range = float2( 0, 1e6 );
                {
                    float3 invDir = 1.0 / viewDir;
                    float3 t1 = ( -0.5 - viewPos ) * invDir;
                    float3 t2 = ( 0.5 - viewPos ) * invDir;
                    
                    bool isOut = any( abs( viewPos ) > 0.5 );
                    if ( true == isOut )
                    {
                        float3 tmin3 = min( t1, t2 );
                        float2 tmin2 = max( tmin3.xx, tmin3.yz );
                        range.x = max( range.x, max( tmin2.x, tmin2.y ) );
                    }

                    float3 tmax3 = max( t1, t2 );
                    float2 tmax2 = min( tmax3.xx, tmax3.yz );
                    range.y = min( tmax2.x, tmax2.y );
                }

                float value = 0;
                {
                    float stepCount = clamp( _StepCount, 2, STEP_COUNT_LIMIT );
                    float3 delta = viewDir * ( range.y - range.x ) / ( stepCount - 1 );
                    float3 rayPos = viewPos + viewDir * range.x;

                    [ loop ]
                    for ( float i = 0; i < stepCount; i++ )
                    {
                        float3 uv3d = rayPos + 0.5;
                        float4 neighbor = TrilinearSampleBuffer( _MainTex, uv3d, 0 );

                        // w に面積の和が入っている
                        if ( neighbor.w > 1e-8 )
                        {
                            // 面積で割って重心を求める
                            neighbor.xyz /= neighbor.w;
                            float3 worldPos = SIMULATION_POSITION + rayPos * SIMULATION_SCALE;
                            float dist = length( worldPos - neighbor.xyz );
                            value += smoothstep( _NeighborDistance, 0.0, dist );
                        }
                        
                        rayPos += delta;
                    }

                    value *= 100.0 * ( 1.0 / stepCount ) * ( range.y - range.x );  // 適当に補正
                }
                value = saturate( value * _Alpha );

                return float4( 1, 0, 0, value );
            }
            ENDCG
        }
    }
}

Raymarchingの実装はこちらが参考になります.

スクリプト

アバターのシェーダーを置き換えるためのスクリプトです.UdonSharpです.

VolumetricPolygonCapture
VolumetricPolygonCapture.cs
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

namespace TsukimiWS.Test.VolumetricPolygonCapture
{
    [UdonBehaviourSyncMode(BehaviourSyncMode.None)]
    public class VolumetricPolygonCapture : UdonSharpBehaviour
    {
        [SerializeField] private Material captureMaterial;

        void Start()
        {
            Camera cam = GetComponent<Camera>();
            cam.SetReplacementShader(captureMaterial.shader, null);
            cam.enabled = true;
        }
    }
}

結果

Visualizer でこんな感じに見えたら成功です.映らないなと思ったらレイヤーがあっているか確認してみてください.また,アバターが小さいとつぶれてほとんど見えないので,適度に調整してください.

image.png

雑記

PointStream て何に使うんだ(笑)とか思っていましたが,結構便利ですね.
開発当時は気づいてなかったのでワールドの方では三角形で描画しています.ははは.

この手法ですが,いろいろと問題があります.

アバターの内部か外部かわからない

メッシュの座標を取っているだけで方向の情報を持たないので,メッシュを検知できたとしてもそれが内部なのか外部なのか判定できません.よって,衝突には使えません.
メッシュの座標のほかに,メッシュの法線のテクスチャも作ればある程度何とかなりそうですが,髪の周りとかはぼんやりしそうです.なんかいい方法ないでしょうか.

アーティファクトの発生

ブラーをかけるときに平均をとったので,首回り・腕と胴の間・脚の間などにアーティファクトが生じます.今回は目的が適当だったので問題なかったために何も対策しませんでしたが,ブラーじゃなくて最近接点を取るようにすればよかったかもしれません.でも滑らかにするためにはちゃんと線分で最近接を取らないといけない気がします.めんどい.

大きい三角形をとれない

三角形の図心の情報しか残らないので,空間の解像度より大きな三角形はうまく取れません.これに対応するにはテッセレーションが必要ですが,筆者はテッセレーションがわからないので見送りました.PC向けのアバターは基本的にハイポリゴンなのでよっぽど問題ないですが,パーティクルまでちゃんと取りたい場合は対応が必要かもしれません.

透明度が反映されない

置き換え先のシェーダーでメインテクスチャを無視しているので,本来透明になるべき部分もキャプチャしてしまいます.置き換え先のシェーダーでメインテクスチャをサンプルするようにすれば,「テクスチャが透明なら重みをゼロにする」といった対応も可能ですが,カットアウトされるような部分はローポリゴンになっているのが普通だと思うので,テッセレーションとの組み合わせでないと意味ない気がします.

特殊シェーダーに対応できない

シェーダーを置き換えるので,当然元のシェーダーの効果はすべて無効になります.なので,頂点を操作しているタイプのシェーダーはうまく形をとれません.これはもう本当にどうしようもないので,通常のカメラで頑張って撮るしかありません.

アバターでは使えない

シェーダーの置き換えにスクリプトが必要なので,アバターギミックとしては使えません.アバター側にあらかじめ置き換え先のシェーダーを仕込んでおけばできることにはできますが,だいぶ限定的ですね.

処理負荷がアバターのポリゴン数に依存する

ポリゴン数依存は当たり前といえば当たり前ですが,この手法の場合オーバードローが大変なことになるので結構やばいんじゃないかと思っています.少なくとも,面積が一定未満の三角形はカットするべきでしょう.ワールドの方では対応を完全に忘れていたので,どこかのタイミングでこっそり直しておきます.ジオメトリシェーダでポリゴン数を制限しておくのもいいかもですね.

Quest (Android) 環境では使えない

ジオメトリシェーダは Quest では使えません.

 

そんな感じで問題が多いですが,まあ,きっと誰かがうまいことやってくれると思います.

  1. Neitri-Unity-Shaders / World Position.shader

  2. VR向けアバターモデル「RadDollV3」

  3. VRC Light Volumes | How To Use | How It Works

  4. 解説: Just a Pool

  5. Wrap Update Zones の代替手段

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?