Unity の Custom Render Texture ではテスクチャのフォーマットに整数型を選択できます.しかし,シェーダー内で普通に tex2Dlod() でサンプルすると float4 でサンプルされてしまいます.これを,ちゃんと uint4 でサンプルできるようにする方法を説明します.
動機
たいていの場合は,tex2Dlod() の戻り値が float4 であったとしても, uint4 にキャストしてしまえば事足ります.
これに対して,テクスチャのサンプルが uint4 でなければならない場合とは,例えば,Custom Render Texture でパーティクルのインデックスを管理していて,そのインデックスが $2^{24}$ 以上の値になる場合です(float の仮数部の精度が 23 bit のため).
テクスチャの解像度が 2048x2048 の場合テクセルは全部で $2^{22}$ 個,4096x4096 なら $2^{24}$ 個,8192x8192 なら $2^{26}$ 個なので,4096x4096 より大きいテクスチャに対してインデックスの管理したい場合は uint4 などの整数型でないと破綻します.
逆に言えば,それ以下のサイズであれば float4 で事足ります.
実装
テクスチャを float4 以外でサンプルしたい場合は,テクスチャを Texture2D として宣言する必要があります.また,サンプルの際は [] や .Load() を使用します.
Texture2D<uint4> _MyUint4Texture;
float4 _MyUint4Texture_TexelSize;
uint4 HowToSampleUint4Texture(float2 uv)
{
uint2 index2d = uv * _MyUint4Texture_TexelSize.zw;
return _MyUint4Texture[index2d];
// return _MyUint4Texture.Load(int3(index2d,0));
}
変えるのはこれだけなのですが,Custom Render Texture の _SelfTexture2D は UnityCustomRenderTexture.cginc で sampler2D _SelfTexture2D として宣言されているので,Custom Render Texture で自身を uint4 でサンプルしたい場合は UnityCustomRenderTexture.cginc の改造が必要です.
まず,Unity の Built-in シェーダーをダウンロードします.筆者は VRChatter なのでいまだに Unity 2022 を使っていますが,Unity 6 でも UnityCustomRenderTexture.cginc はあまり変わりません.
UnityCustomRenderTexture.cginc は CGInclude の中にあるので,自分のプロジェクトにコピーしてリネームしておきます.
UnityCustomRenderTexture.cginc を開くと 57 行目に _SelfTexture2D の宣言があるので,これを Texture2D<uint4> に変更します.
...
// sampler2D _SelfTexture2D;
// samplerCUBE _SelfTextureCube;
// sampler3D _SelfTexture3D;
Texture2D<uint4> _SelfTexture2D;
TextureCube<uint4> _SelfTextureCube;
Texture3D<uint4> _SelfTexture3D;
...
あとはこれを通常の UnityCustomRenderTexture.cginc の代わりにインクルードすれば準備完了です.間違えて通常のものをインクルードしてしまわないように気を付けましょう.
検証
本当に uint4 でサンプルできているか確認しておきましょう.
大きな整数を書き込んでみて,読み取った時に値が一致するか確認します.
Shader "Test/CrtUint/Test"
{
Properties
{
[Toggle(_IS_UINT)] _isUint ("Format is UINT", Float) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
CGINCLUDE
#pragma shader_feature_local_fragment _IS_UINT
#if _IS_UINT
#include "./UnityCustomRenderTexture-Uint.cginc"
#else
#include "UnityCustomRenderTexture.cginc"
#endif
ENDCG
Pass
{
Name "Write"
CGPROGRAM
#pragma target 3.5
#pragma vertex CustomRenderTextureVertexShader
#pragma fragment frag
uint4 frag(v2f_customrendertexture IN) : SV_Target
{
float2 uv = IN.globalTexcoord.xy;
uint2 index2d = uv * _CustomRenderTextureInfo.xy;
uint index = index2d.y * _CustomRenderTextureWidth + index2d.x;
uint4 expectedValue = index.xxxx + uint4(0,1,2,3);
#if !_IS_UINT
// tex2Dlod() でサンプルする場合は float4 で書き込まないと正しく読めない
expectedValue = asuint(float4(expectedValue));
#endif
return expectedValue;
}
ENDCG
}
Pass
{
Name "Read"
CGPROGRAM
#pragma target 3.5
#pragma vertex CustomRenderTextureVertexShader
#pragma fragment frag
uint4 frag(v2f_customrendertexture IN) : SV_Target
{
float2 uv = IN.globalTexcoord.xy;
uint2 index2d = uv * _CustomRenderTextureInfo.xy;
uint index = index2d.y * _CustomRenderTextureWidth + index2d.x;
uint4 expectedValue = index + uint4(0,1,2,3);
#if _IS_UINT
uint4 readValue = _SelfTexture2D[index2d];
#else
uint4 readValue = tex2Dlod(_SelfTexture2D, float4(uv, 0, 0));
#endif
float result = 0.0;
if (all(readValue == expectedValue))
{
result = 1.0;
}
// Unlit/Texture のシェーダーで表示できるように float4 で結果を返す
return asuint(result).xxxx;
}
ENDCG
}
Pass
{
Name "Dummy"
CGPROGRAM
#pragma target 3.5
#pragma vertex CustomRenderTextureVertexShader
#pragma fragment frag
// バッファをスワップするためだけのパス.中身は実行されない.
uint4 frag(v2f_customrendertexture IN) : SV_Target
{
return uint4(0,0,0,0);
}
ENDCG
}
}
}
tex2Dlod() について,試してみたところ,サンプル対象のテクスチャに書き込まれている値が float4 の形式で書かれていないと正しくサンプルできないようです.そのため,(比較の意味がなくなってしまいますが,)上のサンプルでは, tex2Dlod() を使う場合は float4 の形式で書き込むようにしています.
上のシェーダーを Custom Render Texture に適用して,Quad メッシュにはりつけると,float4 の場合は $2^{24}$ 以上のインデックスが破綻することがわかります.uint4 の方は期待通りの値が取れています.
雑記
Custom Render Texture の Format に関してですが,どうやらシェーダー内での型とは関係がないようで,frag() の戻り値として指定した型がそのままテクスチャに書き込まれるようです.
そのため,テクスチャのフォーマットが R32G32B32A32_UINT であっても,float4 frag() として書き込むと,ほかのシェーダーで tex2Dlod() でサンプルすればちゃんと float4 の値として読み取れます.
逆に,テスクチャのフォーマットが R32G32B32A32_SFLOAT であっても,uint4 frag() として書き込むと,Texture<uint4>[] や Texture<uint4>.Load() でサンプルしたときにちゃんと uint4 の値として読めます.
なんかちょっと納得いきませんが,これはこれで便利ですね.


