夜風のMixedReality

xRと出会って変わった人生と出会った技術を書き残すためのGeekなHoloRangerの居場所

HoloLens用のSpatialMeshShaderを開発する その① HoloLens Adventカレンダー2021 5日目

本日はShader勉強枠です。

またHoloLens Adventカレンダー5日目の記事です。

〇HoloLens Adventカレンダーとは?

HoloLens Adventカレンダーは技術関連投稿サイトのQiita上で毎年12月に開かれているイベントの一つです。

あるテーマに沿って12月1日から25日までの間毎日誰かしらが情報共有を行うというイベントになっています。

HoloLens Advent カレンダーは初代HoloLensがリリースされた2016年より毎年開かれ今年で5年目となっています。

〇SpatialMeshShaderを開発する

今回はHoloLensの[SpatialMesh]用の新しいShaderを開発します。

[SpatialMesh]とは、[HoloLensのSpatialAwareness(空間認識)]によって検知された周囲の物理情報をもとに生成されたMeshを指します。

作りたいイメージとしては衝撃波が広がっていくように地面のメッシュがはじけるようなShaderを開発します。

 今回はそのベースとなるSpatialMesh上をメッシュが浮かび上がるパルスが走るShaderを目標に開発します。

〇実機での動作

youtu.be

〇ジオメトリシェーダー

今回はメッシュを動かすためにジオメトリシェーダーを用いています。

 このジオメトリシェーダーは頂点シェーダーとフラグメントシェーダーの間に実行され、頂点シェーダーで処理をした複数の頂点情報をもとにラインやメッシュ単位での処理が可能なシェーダー処理です。

[HandTrianglesShader]や[WireframeShader]などで使用されています。

今回のシェーダーは以下のコードをベースにしています。

github.com

ジオメトリシェーダー部の処理は次のようになります。

[maxvertexcount(3)]
void geom(triangle appdata_t input[3],uint pid :SV_PrimitiveID,inout TriangleStream<FragmentInput> triStream)
{
    float3 wp0 = input[0].position.xyz;
    float3 wp1 = input[1].position.xyz;
    float3 wp2 = input[2].position.xyz;


    // Extrusion amount
    float ext = saturate(0.4 - cos(_LocalTime * UNITY_PI * 2) * 0.41);
    ext *= 1 + 0.3 * sin(pid * 832.37843 + _LocalTime * 88.76);
    ext *= 0.1;
    //Wave
    float val0 = Wave(wp0);
    float val1 = Wave(wp1);
    float val2 = Wave(wp2);
    
    // Extrusion points
    float3 offs = ConstructNormal(wp0, wp1, wp2) * ext*val1;
    float3 wp3 = wp0 + offs;
    float3 wp4 = wp1 + offs;
    float3 wp5 = wp2 + offs;
    
 
    float3 wn = ConstructNormal(wp3, wp4, wp5);
    float np = saturate(ext * 10);
    float3 wn0 = lerp(input[0].normal, wn, np);
    float3 wn1 = lerp(input[1].normal, wn, np);
    float3 wn2 = lerp(input[2].normal, wn, np);
    triStream.Append(VertexOutput(wp3, wn0,val0));
    triStream.Append(VertexOutput(wp4, wn1,val1));
    triStream.Append(VertexOutput(wp5, wn2,val2));
    triStream.RestartStrip();
}

肝となる部分は[ext]と[Wave()]です。

[ext]はベースとしたシェーダーのまま使用していますが、メッシュを動かすパラメータになっています。[_LocalTime]の係数をもとにメッシュを押し出す数値として使用されます。

[Wave()]は今回のシェーダーで作成したオリジナルの関数です。

float Wave(float3 worldPosition)
{
    float3  worldPos= mul(unity_ObjectToWorld, worldPosition);
    float radius;
    #if   _AUTO_WAVE_ON
    float speed = 1/_Speed;
    radius =_WaveSize*(_Time/speed - floor(_Time/speed+1/2));
    #else
    radius = _Radius;
    #endif
    float dist = distance(_Center, worldPos);
    float Isize =_LineSize +_InnerSize;
    float intensity =1/_MasterIntensity ;
    float val = 1 - step(dist, radius - _LineSize) * _Inner;
    val =abs(1-val) * (1 - step(dist, radius - _LineSize-1.) * _Inner);
    
   return val ; 
}

この関数は以前SpatialMesh用に作成したScanShaderの処理を一部流用しています。

redhologerbera.hatenablog.com

[ext]で隆起するメッシュをScanShaderのPulseと掛け合わせて対応させています。

〇SpatialMesh用のShaderでの注意点

 SpatialMeshのShaderを描くうえでの注意点としてuvが使用できない点です。

 通常のメッシュはuvを保持していますが、SpatialMeshの場合動的に生成されたメッシュのためuvを保持していません。

 描画回りでuvを使用することができない点が注意です。

〇コード

今回はURPプロジェクトでの環境ですが、光源処理を想定していないため[UnityCG.cginc]を用いています。

URP環境下で[UnityCG.cginc]が使用できる条件は以下のようになります。

・SurfaceShaderを使用していない。

・Lit処理を行っていない。

SpatialMesh.shader

Shader "HoloMoto/SpatialMesh"
{
    Properties
    {
      _LocalTime("TimeScale",float)=0
      _Speed("Speed",float)=0
      _WaveSize("WaveSize",float)=1
      _Radius("Radius",float)=0
      _Center("CenterPosition",vector) = (0, 0, 0)
      _Inner("InnerSize",float)=0.1
        [Toggle] _Auto_Wave("AutoWave", Float) = 0  
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

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

            #pragma shader_feature _AUTO_WAVE_ON
            #include  "SpatialShader.hlsl"
            ENDCG
        }
    }
}

SpatialPulse.hlsl

#include "UnityCG.cginc"
#include "UnityStandardUtils.cginc"

float4 _Color;
float _Radius;
float4 _Center;
float _LocalTime;
float _LineSize;
float _InnerSize;
float _MasterIntensity=1;
float _Speed;
float _Inner;
float _WaveSize;
struct appdata_t 
{
    float4 position : POSITION;
    float3 normal : NORMAL;
    float4 tangent : TANGENT;
    float2 texcoord : TEXCOORD;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct FragmentInput
{
    float4 position : SV_POSITION;

   
    float3 normal : NORMAL;
    float waveVal: TEXCOORD8;
    half3 ambient : TEXCOORD4;
    
};

appdata_t vert(appdata_t v)
{
    
    v.position = mul(unity_ObjectToWorld, v.position);
    v.normal = UnityObjectToWorldNormal(v.normal);
    v.tangent.xyz = UnityObjectToWorldDir(v.tangent.xyz);
    
    return v;
}

float3 ConstructNormal(float3 v1, float3 v2, float3 v3)
{
    return normalize(cross(v2 - v1, v3 - v1));
}

FragmentInput VertexOutput(float3 wpos, half3 wnrm, float valume)
{
    FragmentInput o;

    // GBuffer construction pass
    o.position = UnityWorldToClipPos(float4(wpos, 1));
    o.normal = wnrm;

    
    o.ambient = ShadeSHPerVertex(wnrm, 0);
    o.waveVal = valume;
    return o;
}

float Wave(float3 worldPosition)
{
    float3  worldPos= mul(unity_ObjectToWorld, worldPosition);
    float radius;
    #if   _AUTO_WAVE_ON
    float speed = 1/_Speed;
    radius =_WaveSize*(_Time/speed - floor(_Time/speed+1/2));
    #else
    radius = _Radius;
    #endif
    float dist = distance(_Center, worldPos);
    float Isize =_LineSize +_InnerSize;
    float intensity =1/_MasterIntensity ;
    float val = 1 - step(dist, radius - _LineSize) * _Inner;
    val =abs(1-val) * (1 - step(dist, radius - _LineSize-1.) * _Inner);
     
    //val = lerp(radius - Isize, dist,radius - 3) * step(dist, radius) * val/intensity;
    
   return val ; 
}
[maxvertexcount(3)]
void geom(triangle appdata_t input[3],uint pid :SV_PrimitiveID,inout TriangleStream<FragmentInput> triStream)
{
    float3 wp0 = input[0].position.xyz;
    float3 wp1 = input[1].position.xyz;
    float3 wp2 = input[2].position.xyz;


    // Extrusion amount
    float ext = saturate(0.4 - cos(_LocalTime * UNITY_PI * 2) * 0.41);
    ext *= 1 + 0.3 * sin(pid * 832.37843 + _LocalTime * 88.76);
    ext *= 0.1;
    //Wave
    float val0 = Wave(wp0);
    float val1 = Wave(wp1);
    float val2 = Wave(wp2);
    
    // Extrusion points
    float3 offs = ConstructNormal(wp0, wp1, wp2) * ext*val1;
    float3 wp3 = wp0 + offs;
    float3 wp4 = wp1 + offs;
    float3 wp5 = wp2 + offs;
    
 
    float3 wn = ConstructNormal(wp3, wp4, wp5);
    float np = saturate(ext * 10);
    float3 wn0 = lerp(input[0].normal, wn, np);
    float3 wn1 = lerp(input[1].normal, wn, np);
    float3 wn2 = lerp(input[2].normal, wn, np);
    triStream.Append(VertexOutput(wp3, wn0,val0));
    triStream.Append(VertexOutput(wp4, wn1,val1));
    triStream.Append(VertexOutput(wp5, wn2,val2));
    triStream.RestartStrip();
}

float4 frag(FragmentInput i) : SV_Target
{
    
    return float4(i.waveVal, i.waveVal, i.waveVal, 1);
}