はじめに
VRChatにおいて、GPUパーティクルを描画する手法はいくつかあるが、最もシンプルな方法の1つとして描画したいパーティクル数分の長さを持ったpointメッシュにより描画する方法がある。
ワールドに仕込む場合はポリゴン数制限が関係ないが、アバターに含む場合は3頂点1ポリゴン換算でカウントされてしまう。もちろん1万ポリゴン換算で3万パーティクル描画できるので十分であるとも言える上、最近はポリゴン数の成約そのものが緩くなったため、別段工夫を行う必要は少なくなっている。
そこでVRChatで0ポリゴン扱い(3分の1ポリゴン=1vertex1indexのpointメッシュ)で、GPUパーティクル的な描画を行うシェーダーを書いてみる。
概要 (本記事で得られるテクニック)
より少ないポリゴン数のメッシュからGPUパーティクル等を描画するために、テッセレーションを用いてGeometryShaderに一意連続なidを与え自在に描画を行うためのたたき台。
アイデア
メッシュを増殖する方法はいくつかあるが、メッシュを細分化する テッセレーション(wikipedia) を利用し、細分化されたメッシュをもとに、必要数分GeometryShaderを走らせる。正方形のメッシュに対してテッセレーションを行った際に、等間隔の格子状に分割される。その格子を構成する頂点の座標より、一意連続なidを発行し、GPUパーティクルの描画を試みる。
実装
HullShaderにてquadで整数のテッセレーション係数を指定し分割を行うと、メッシュは等間隔の格子状に分割が行われる。
例えば下図はテッセレーション係数を5として分割しているため、1辺当たり5つの格子に分割される。このとき5つの格子を表現するために1辺当たり6つの頂点(赤)に分割される。
DomainShaderにて分割後の頂点の座標は、セマンティクスSV_DomainLocationよりuv座標(青)の様に取得できる。このとき、頂点0はuv0.0f、頂点5はuv1.0fに対応する。
GeometryShaderにて一意連続なidを発行するためにはuv座標をもとに頂点の添字(赤)を復元するためには、uv座標に1辺当たりの頂点数-1を掛ければ良い。例えば頂点の添字が1のとき、uv座標は0.2となるため、5を掛けて1.0となる。同様に頂点の添字が3のとき、uv座標は0.6となるため、3.0となり復元される。(紫)
xとyそれぞれにおいて0~5の6つが生成されるため、id = y * 6 + x とすれば、0~35のidを発行してGeometryShaderを走らせることができる。
コード (github: konchannyan/instanceShader)
※このサンプルはGeometryShaderのinstance機能と複合させた恐らく最も多くGeometryShaderが走るケースにて、GeometryShader1つ当たり12ポリゴンのキューブを描画し、1,622,400ポリゴン描画している。
Shader "JackyGun/instanceShader"
{
Properties
{
}
SubShader
{
Tags{ "Queue" = "Transparent" "RenderType" = "Opaque" }
Pass
{
CGPROGRAM
#pragma target 5.0
#pragma vertex mainVS
#pragma hull mainHS
#pragma domain mainDS
#pragma geometry mainGS
#pragma fragment mainFS
// Geometry Shader [TESS * TESS * GS_INSTANCE]
// input mesh topology : point
#define TESS 65
#define GS_INSTANCE 32
// Structure
struct VS_IN
{
};
struct VS_OUT
{
};
struct CONSTANT_HS_OUT
{
float e[4] : SV_TessFactor;
float i[2] : SV_InsideTessFactor;
};
struct HS_OUT
{
};
struct DS_OUT
{
uint pid : PID;
};
struct GS_OUT
{
float4 vertex : SV_POSITION;
};
// Main
VS_OUT mainVS(VS_IN In)
{
VS_OUT Out;
return Out;
}
CONSTANT_HS_OUT mainCHS()
{
CONSTANT_HS_OUT Out;
Out.e[0] = Out.e[1] = Out.e[2] = Out.e[3] = Out.i[0] = Out.i[1] = TESS - 1;
return Out;
}
[domain("quad")]
[partitioning("integer")]
[outputtopology("point")]
[outputcontrolpoints(1)]
[patchconstantfunc("mainCHS")]
HS_OUT mainHS(InputPatch<VS_OUT, 4> In)
{
}
[domain("quad")]
DS_OUT mainDS(CONSTANT_HS_OUT In, const OutputPatch<HS_OUT, 4> patch, float2 uv : SV_DomainLocation)
{
DS_OUT Out;
Out.pid = (uint)(round(uv.x * (TESS - 1))) + ((uint)(round(uv.y * (TESS - 1))) * TESS);
return Out;
}
[instance(GS_INSTANCE)]
[maxvertexcount(24)]
void mainGS(point DS_OUT input[1], inout TriangleStream<GS_OUT> outStream, uint gsid : SV_GSInstanceID)
{
GS_OUT o;
// id : [0] - [TESS * TESS * GS_INSTANCE - 1]
uint id = gsid + GS_INSTANCE * input[0].pid;
// test : draw 135,200 cube, 1,622,400 polygon
uint xid = id % 368;
uint zid = id / 368;
float xd = -100 * 0.5f + 100 * 0.5f / 368 + 100 * xid / 368;
float zd = -100 * 0.5f + 100 * 0.5f / 368 + 100 * zid / 368;
float4 wpos0 = UnityObjectToClipPos(float4(float3(-1, +1, +1) * 0.1f + float3(xd, 0, zd), 1));
float4 wpos1 = UnityObjectToClipPos(float4(float3(+1, +1, +1) * 0.1f + float3(xd, 0, zd), 1));
float4 wpos2 = UnityObjectToClipPos(float4(float3(-1, +1, -1) * 0.1f + float3(xd, 0, zd), 1));
float4 wpos3 = UnityObjectToClipPos(float4(float3(+1, +1, -1) * 0.1f + float3(xd, 0, zd), 1));
float4 wpos4 = UnityObjectToClipPos(float4(float3(-1, -1, +1) * 0.1f + float3(xd, 0, zd), 1));
float4 wpos5 = UnityObjectToClipPos(float4(float3(+1, -1, +1) * 0.1f + float3(xd, 0, zd), 1));
float4 wpos6 = UnityObjectToClipPos(float4(float3(-1, -1, -1) * 0.1f + float3(xd, 0, zd), 1));
float4 wpos7 = UnityObjectToClipPos(float4(float3(+1, -1, -1) * 0.1f + float3(xd, 0, zd), 1));
o.vertex = wpos0; outStream.Append(o); o.vertex = wpos1; outStream.Append(o); o.vertex = wpos2; outStream.Append(o); o.vertex = wpos3; outStream.Append(o); outStream.RestartStrip();
o.vertex = wpos4; outStream.Append(o); o.vertex = wpos6; outStream.Append(o); o.vertex = wpos5; outStream.Append(o); o.vertex = wpos7; outStream.Append(o); outStream.RestartStrip();
o.vertex = wpos0; outStream.Append(o); o.vertex = wpos2; outStream.Append(o); o.vertex = wpos4; outStream.Append(o); o.vertex = wpos6; outStream.Append(o); outStream.RestartStrip();
o.vertex = wpos3; outStream.Append(o); o.vertex = wpos1; outStream.Append(o); o.vertex = wpos7; outStream.Append(o); o.vertex = wpos5; outStream.Append(o); outStream.RestartStrip();
o.vertex = wpos1; outStream.Append(o); o.vertex = wpos0; outStream.Append(o); o.vertex = wpos5; outStream.Append(o); o.vertex = wpos4; outStream.Append(o); outStream.RestartStrip();
o.vertex = wpos2; outStream.Append(o); o.vertex = wpos3; outStream.Append(o); o.vertex = wpos6; outStream.Append(o); o.vertex = wpos7; outStream.Append(o); outStream.RestartStrip();
}
float4 mainFS(GS_OUT In) : SV_Target
{
return float4(0, 1, 1, 1);
}
ENDCG
}
}
}
描画 (1頂点のpointメッシュより、キューブ135,200個 = 1,622,400ポリゴン)
終わりに
本記事ではメッシュを細分化するテッセレーションを用いて大量にGeometryShaderを呼ぶ方法を記載した。
そもそも、頂点数やポリゴン数等に制限がなければもともとのメッシュの方で対応した方が計算効率は良いので、その点はこの方法のデメリットである。
テッセレーションを用いる方法としてのメリットを上げるとすれば、下記3点が上げられる。
・テッセレーション係数はマテリアル等から変更できるため、動的に描画するパーティクル数等の制御が可能
・メッシュを変更せずにポリゴン数を容易に変更できるので、たたき台用のシェーダーとして利用
・CustomRenderTexture内で同様のことを行い、より自由度の高い描画を行う等。 → CustomRenderTextureだけでボリュームレンダリングしてみる!!