How to sample R16 texture in shader?

I’m trying to store a bitfield in a texture for use in a shader. It seems that the only integer-based non-experimental texture format is R16, which works fine for my purposes in terms of setting the data CPU-side.

However I’m not sure exactly how to retrieve the bits in the shader. My shaders won’t compile if I declare a Texture2D. It will compile with Texture2D, but since the input data is a 16-bit integer, not 32-bit, it seems like it’s not going to be sampled correctly if I do it that way. I’m getting strange results from the shader but am not sure whether this disparity is the culprit or some unrelated bug.

Any best practices when it comes to using the R16 texture format?

1 Like

The best practice is … do nothing special. The main thing is you’ll want to use Load rather than Sample.

// define texture
Texture2D _MyIntTexture;

// load texture
uint val = _MyIntTexture.Load(int3(texelU, texelV, 0)).r;

In case anyone else runs into this, try as I might I was never able to get the right data coming out of the R16 texture format. I switched to the RFloat texture format and used unit32s to store the bitfields and it worked fine (so long as you use asuint() when you load the texel in the shader).

2 Likes

How can I use that syntax in a Unity shader? I get compilation errors for Texture2D and Load.

That’s syntax for a Direct3D 11 or Vulkan shader. If you’re trying to do this for OpenGL or mobile you can’t use Texture2D in the shader. Use sampler2D like you would any other texture and set the texture to use point filtering.

Thanks for the explanation. I figured out to use point filtering with sampler2d myself but it’s good to know why the syntax didn’t work.

Could anyone provide a small code snippet how the sampling of the R16 texture finally succeeded? From what I have read so far, it seems that reading the texture via “tex2D()” will always return floating point values. How can I make sure that the value is read as uint? Thanks!

1 Like

so,How to load R16 on CPU,and sample it on GPU???

https://discussions.unity.com/t/752884/2

This answer comes in 2 parts.
Part one does appear to work, but messes up two bits. But read it, to understand part 2.
Part 1:

In c# you create it as:

// NOTICE: ARGBUInt (32 bits per channel meaning four 8-bit masks fit inside a channel).
//ARGBFloat was messing up due to denormalization:
// https://stackoverflow.com/questions/60019943/hlsl-asuint-of-a-float-seems-to-return-the-wrong-value
texture = new RenderTexture(2048, 2048, 0, UnityEngine.Experimental.Rendering.GraphicsFormat.R32G32B32A32_UInt, mipCount:0);
texture.filterMode = FilterMode.Point;
texture.useMipMap = false;
texture.antiAliasing = 1;
texture.enableRandomWrite = true;//so that our compute shaders work fine with it.
texture.Create();

Notice, you most likely did fill this integer texture via another shader, to give it some starting values.

In that case, you need to ensure the return type of that shader’s frag() function is float4 not int4 nor uint4 like I assumed originally.

But at the same time, you need to return it as uint4, ensuring you don’t cast it to float anywhere.
Again: return type is float4 but return uint4.
Otherwise if you attempt to sample it later on, you’d get zeros.

For example, I am filling this integer texture as:

//notice float4, uint4 return type doesn't work even if target texture is R32G32B32A32_UInt.
float4 frag (v2f input) : SV_Target{
    uint val = 0xFFFFFFFF;
    return uint4(val,val,val,val); //return uint4 even though return is float4
}

In C# doing
Graphics.Blit(null, texture, _myFillMaterial);

In C#, apply integer texture into shader where it will be read:
_mySamplingMaterial.SetTexture("_MyIntegerTex", texture);

Now, in some other shader you can sample it as:

sampler2D _MyIntegerTex;

float4 frag(v2f input) : SV_Target {
        //notice: storing directly into uint4:
       uint4 rgba128 =  tex2D(_MyIntegerTex, input.uv).xyzw;
}

As a final word of caution, avoid using asuint() because it makes your uint zero if float had some special bits flipped:

https://stackoverflow.com/questions/60019943/hlsl-asuint-of-a-float-seems-to-return-the-wrong-value

2 Likes

Part 2:
My answer above does work, but it still messes up the upper 2 bits due to float4 frag().
So, here is the solution to that, using compute shaders:

So, we need to find a way to fill the unsigned-integer-texture with uint4
Unity doesn’t seem to allow returning uint4 from frag() in shader. It compiles but returns zeros.

Therefore, we can do it via compute shader. Create one and put this code:

// ComputeShaderExample.compute
#pragma kernel CSMain

// Declare the RWTexture with uint4 since we're working with unsigned integers
RWTexture2D<uint4> Result;

[numthreads(8, 8, 1)]
void CSMain (uint3 id : SV_DispatchThreadID) {
    // Example unsigned integer values to write to the texture
    uint4 values = uint4(255, 255, 255, 255);

    // Writing the unsigned integer values to the texture
    Result[id.xy] = values;
}

In your C# script, to populate the unsigned-integer-texture with values do:

[SerializeField] ComputeShader _myComputeShader;

RenderTexture texture = /*make or get your render texture here*/
texture.enableRandomWrite = true;//so that our compute shaders work fine with it.

int kernelHandle = _maskFilingShader.FindKernel("CSMain");
_myComputeShader.SetTexture(kernelHandle, "Result", texture, 0);
_myComputeShader.Dispatch(kernelHandle, texture.width/8, texture.height/8, 1);

Adjust the 8 and 1 depending on your hardware, to improve performance.
If you do, change it both in [numthreads(8, 8, 1)] and also in c# .Dispatch()

However, the slightly tricky part is reading your unsigned-integer-texture in your shader.

From C#, assign your texture into material:

_myMaterial.SetTexture("_MyUIntegerTexture", texture);
_myMaterial.SetInt("_MyUIntegerTexture_Width", texture.width);
_myMaterial.SetInt("_MyUIntegerTexture_Height", texture.height);

And in shader do:

Texture2D<uint4> _MyUIntegerTexture;
int _MyUIntegerTexture_Width;
int _MyUIntegerTexture_Height;

fixed4 frag(v2f input) : SV_Target{
     uint3 sampleCoord = uint3(input.uv.x*_MyUIntegerTexture_Width, input.y*_MyUIntegerTexture_Height, 0);
     uint4 rgba128 =  _MyUIntegerTexture.Load(sampleCoord);
}

Notice that we did Texture2D and not just Texture2D nor sampler2D. Otherwise it would return zeros.
Also, notice that we assigned result to uint4 rgba128 and didn’t convert to float or other types, to avoid losing the values in the process.

There are important details about SV_DispatchThreadID, SV_GroupID, SV_GroupThreadID
You can read about them here and check unity example here.
In many cases we can just use SV_DispatchThreadID. For out-of-bounds checks see here
Or if that post ever gets removed, here is quick copy:
explanation

SV_DispatchThread ID (uint3)
Indices for which combined thread and thread group a compute shader is executing in. SV_DispatchThreadID is the sum of SV_GroupID * numthreads and GroupThreadID. It varies across the range specified in Dispatch and numthreads. For example, if Dispatch(2,2,2) is called on a compute shader with numthreads(3,3,3) SV_DispatchThreadID will have a range of 0…5 for each dimension.

SV_GroupID (uint3)
Indices for which thread group a compute shader is executing in. The indices are to the whole group and not an individual thread. Possible values vary across the range passed as parameters to Dispatch. For example calling Dispatch(2,1,1) results in possible values of 0,0,0 and 1,0,0.

Defines the group offset within a Dispatch call, per dimension of the dispatch call.

SV_GroupThreadID (uint3)
Indices for which an individual thread within a thread group a compute shader is executing in. SV_GroupThreadID varies across the range specified for the compute shader in the numthreads attribute. For example, if numthreads(3,2,1) was specified possible values for the SV_GroupThreadID input value have this range of values (0–2,0–1,0).

SV_GroupIndex (uint)
The “flattened” index of a compute shader thread within a thread group, which turns the multi-dimensional SV_GroupThreadID into a 1D value. SV_GroupIndex varies from 0 to (numthreadsX * numthreadsY * numThreadsZ) - 1.

4 Likes

WOW, thanks. GOOD Post.

1 Like