using depth texture with 16 or 32 bits per channel for DX11 displacement

Hello,

for a scientific application I would like to use 16 bit (per pixel) signed integer depth data to use as a displacement texture in a DX11 shader. The shaders I have work fine when using standard 8bit/channel textures, but as I understand there is no support in Unity for textures using higher bit depths per channel.
Unity’s TextureFormat does not offer formats any higher than 8 bits/channel.
RenderTextureFormat offers various formats with higher bit depths and floating point. I think it is possible somehow to write to RenderTextures using DX11, ComputeShader and ComputeBuffer, but I have not gone down that road yet because a) my knowledge of those types is very limited, b) they are scarcely documented and c) although fast, I would like to avoid any extra data processing. An approach using ComputeShader is this one: Encode/Decode floating point textures in Unity.

I am searching for a solution that is as fast as possible because I would like to update the displacement texture in realtime, ie. on every frame.

So what I tried is to encode the 16 bit signed int data (which is actually signed short, –32,768 to 32,767), in a RGBA32 texture using this piece of code:

short val;                       // something between –32,768 and 32,767
int r = (valA >> 8) + 128;       // MSB, is between -128 and 127, so add 128 to make it 0-255.
int g = (valA  0x00ff);         // LSB, is in the range 0-255 anyway.
Color col = new Color(r/255f, g/255f, 0, 0);

and I decode it in the shader using:

float4 col = tex2Dlod(_DispTex, coords);
float r = trunc(col.r * 255) - 128;
float g = col.g * 255;
float highResDisplacement = (r * 256 + g) / 32768.0;

v.vertex.xyz += v.normal * ((highResDisplacement - _DisplacementOffset) * _Displacement);

This basically works, and it would even be possible to encode 2 pixels of source data into one pixel of the RGBA32 texture, but I get some errors in the result. When decoding, the MSB part (encoded in the R channel) sometimes comes out wrong. These might be rounding errors, but I really cannot find the exact problem. It might be just a mathematical issue, or a difference in Mono’s and DX11’s float precision.

Any help, any hints or comments on this issue are greatly appreciated.

Basically, my questions are:
What is the easiest and/or fastest way to use 16 or 32 bit/channel data in a DX11 shader?
Does anyone know how to reliably encode 16 bit signed int data in a texture format provided by Unity?

Kind regards,
claus

I also tried an encoding similar to the one in UnityCG.cginc’s DecodeFloatRGBA function. The result turns out to be smoother overall, but contains similar errors where stepping occurs (see attachment). I guess the stepping comes from rounding errors, which are most obvious in the MSB.

// ENCODING
short val;                              // something between –32,768 and 32,767
float valFloat = (float) val / 65536 + 0.5f;  // converted to float between 0 and 1

// from UnityCG.cginc -> EncodeFloatRGBA
Vector4 kEncodeMul = new Vector4(1.0f, 255.0f, 65025.0f, 160581375.0f);
float kEncodeBit = 1.0f/255.0f;
Vector4 enc = kEncodeMul * valFloat;
enc = new Vector4(frac(enc.x), frac(enc.y), frac(enc.z), frac(enc.w));
enc -= new Vector4(enc.y, enc.z, enc.w, enc.w) * kEncodeBit;

Color col = new Color(enc.x, enc.y, enc.z, enc.w);
// DECODING
float4 coords = float4(v.texcoord.xy, 0, 0);
float d = DecodeFloatRGBA(tex2Dlod(_DispTex, coords));
v.vertex.xyz += v.normal * ((d - _DisplacementOffset) * _Displacement);

Might not be terribly useful to your use-case, but at my end I avoid Unity’s extra CameraDepthTexture geometry pass altogether and write out the fragment screen-space (z = 0…1) depth in the alpha channel of the fragment shader for all rendered geometry (or could be surface shader, same principle), the cam is rendering to a 16-bit HDR render texture and then the post-process gets fed this very ArgbHalf precision texture to process.

Hello metaleap,

thank you for your comment. I do not use depth data coming from the camera or rendered into a RenderTexture (then I could pipe the RenderTexture into the shader straight away, I guess, which would be simple). The data I use is stored in a special 16 bit unsigned int image file format which is read via a custom plugin. So the question is, how to get this external raw data either into a RenderTexture or encoded somehow into a 8bit/channel Texture2D.

What I might do in the end is writing the data that is read from the 16 bit depth file directly to a RenderTexture using a plugin and RenderTexture’s GetNativeTexturePtr. This would probably provide the best performance anyway. This way I could (hopefully) write the 16 bit data directly to a floating point texture without having to mangle the data through a C# script at all…

If you’re just avoiding ComputeBuffers because their documentation isn’t all that great, I recommend you do try using them. They’re not that hard to use once you’ve got them working. Especially if your alternative is to create a plugin to send the data over manually.

With DX11, I use ComputeBuffers to get buoyancy data in and out of a computer shader, and it’s what I plan to use for saving and loading ARGBFloat data when I implement saving/loading for Surface Waves.

This is more or less what it looks like on the C# side

Vector4[] inputArray;// Some kind of data.
ComputeBuffer scriptSideBuffer = new ComputeBuffer(elementCount, 16);
scriptSideBuffer.SetData(inputArray);
computeShaderReference.SetBuffer(kernelIndex, "inShaderBufferName", scriptSideBuffer);

// (...) Dispatch the Shader

scriptSideBuffer.Release();// Release when the data has been sent.

In the Compute shader you only need to define a structuredBuffer of the same size, and access it as an array:

StructuredBuffer<float4> inShaderBufferName;

//(...) function code
float4 data = inShaderBufferName[flatIndex];

I used them for floating point values, but you should be able to use them for uint too, if you use a custom struct (C# side) in stead of Vector4. (I believe the data will still be represented as uints on the hardware but I’m not sure. The performance of operations on them might depend on the hardware.)

Hello RC-1290,

many thanks a lot for your encouraging comment! I took a look into ComputeShaders and ComputeBuffers, and sucessfully managed to put my data into a RenderTexture using this approach. What I did as a test was to write random 0-1 values into a float[ ], passed it to the buffer using SetData(), dispatched the shader that writes the data into a RenderTexture’s red channel (type RFloat), and then applied the RenderTexture to the material/shader, which displays the shaders image. Almost works as expected, there is just one issue I can’t get my head around:

The shader successfully displays the random 0-1 values (noise), but the displacement only works if I a) either do not update the data/renderTexture every frame or b) only on the first frame (just flickering and then the displacement is gone. Can you give me a hint on why that does not work? The weird thing is, when I view the RenderTexture in the Inspector, it updates in the Inspector of course, and on every Inspector update, the displacement works for one frame. But if i do not view the RenderTexture in the Inspector, and in a build, the displacement (vertex function of the shader) does not work.

Do you have any hints on that?

The ComputeShader looks like this:

#pragma kernel CSMain
int textureWidth;
RWTexture2D<float> renderTexture;
StructuredBuffer<float> inShaderBufferName;

[numthreads(16,16,1)]
void CSMain (uint3 id : SV_DispatchThreadID) {
	renderTexture[id.xy] = inShaderBufferName[id.x + id.y * textureWidth];
}

and the vertex and surface functions of my shader look like that (pretty straighforward I guess):

void disp(inout appdata v) {
	float displacement = tex2Dlod(_DispTex, float4(v.texcoord.xy, 0, 0)).r;
	v.vertex.xyz += v.normal * displacement;
}
		
void surf (Input IN, inout SurfaceOutput o) {
	o.Albedo = tex2D(_MainTex, IN.uv_MainTex.xy).r;
}

As always, any hints are greatly appreciated! Thanks!

SOLVED

My original issue (stepping/banding after decoding float data of a texture in a shader) is solved.

It occurred because I had the Color Space set to Linear, and the missing gamma correction (?) messed up the encoded data. When the Color Space is set to Gamma, both techniques work as expected, without any errors.

This seems a bit weird to me, because I thought that a linear color space would leave my (linear) data untouched. But I am obviously wrong. Note to myself: figure this out.

Another question that arises here is: Is it possible to set the color space on a per material basis?

Set your texture import type from texture to advanced, then check out everything related to srgb or linear… most normal 3D-modeling / photographic / artistic textures are pre-gamma-corrected (just like basically 99.9% of jpgs on the web, so to speak), so in Linear mode Unity instructs the gpu to linearize them on read, and only have the final framebuffer gamma-correct the final screen output pixels. So in Linear rendering path, for non-photographic (or rather, already-linearized) textures (gui icons, color ramps, lookup-tables, raw data-sets or heightmaps etc.) you may need to tweak the texture import settings.

You can set it when creating the RenderTexture. For this kind of data, set the readWrite mode to Linear.

He’s using depth data, not color data, so I’m guessing it’s not using standard Gamma encoding. Also, if he’s reading the depth data manually, there’s no Texture Importer. Besides, only the importer for Texture2D has that setting, the RenderTexture asset doesn’t. It also isn’t all that convenient for realtime data, where you might want to re-create the RenderTexture every frame, or use NPOT textures.

(I ended up writing a custom Texture Factory object for automatically creating and destroying different types of RenderTextures with preset settings)

[Edit] Actually, I might’ve read clausmeister’s last post wrong. If he’s using a regular texture with encoded values again, your explanation is spot on. But I don’t know what the output of that plugin is.

Hello all,

thanks for your interest and the discussion.

to clear things up a bit, let me recap what issues I had and how I managed to resolve them:

1) When encoding unsigned short (16 bit / pixel) depth data (read from a custom file format) into the RGBA channels of a standard Texture2D and decoding it in a shader, banding occurred in the final image/displacement. This happened due to the ColorSpace being set to Linear. When set back to Gamma, it worked as expected, using either of the two techniques that I mentioned in the first two posts. metaleap is correct on why it happened:

Although I still don’t see any possibility to change this in the Texture2D I was encoding the data in. I guess if I wanted the ColorSpace set to Linear, I would have to gamma-correct the data before encoding and passing it to the shader (which would be a bit weird and might impact precision).

Even before this issue was resolved, and after RS-1290’s suggestion, I decided to do this using the a completely different approach, using DX11 ComputeShader, ComputeBuffer and RenderTexture.

2)
a) Reading data custom 16 bit depth file into byte[ ] (using FileStream).
b) Setting the byte[ ] to a ComputeBuffer.
c) Setting the buffer to a ComputeShader and dispatching it. All data decoding (from raw bytes to the depth values)
happens in the shader, so there is literally no CPU processing/looping over the data. Even min/max values found in the texture can be computed inside the ComputeShader. After all processing, the data is written to a RWTexture2D (i.e. RenderTexture), using a RenderTextureFormat.RFloat and RenderTextureReadWrite.Linear.
d) Applying the RenderTexture to a shader/material attached to a default plane, and deforming it using DX11 tesselation and real straightforward displacement in the vertex shader, without any further decoding.

Using this approach I can read, process, display and deform geometry using 2048 x 2048 pixels in less than 20ms (>4 Mio. pixels = 8 Megabyte), i.e. >50fps when completely going through all steps a)-d) every frame. This a throughput I am quite happy with for now. And it works, even independently of the ColorSpace set.

The issue I had using approach 2) was that the tesselation/displacement shader did not update the displacement map after the first rendered frame. I don’t know why, but guessing it was some kind of refreshing issue I managed to solve that problem by setting

renderTexture.wrapMode = TextureWrapMode.Repeat;
renderTexture.wrapMode = TextureWrapMode.Clamp;

on the end of my Update(), after the ComputeShader is dispatched. This might somehow mark the RenderTexture as “changed” or so, and the displacement updates every frame. Doesn’t affect the performance, so I don’t really care about this bug(?)/workaround.

Thanks again for the discussion, inspiration and hints.

You can specify that no sRGB read/write should be applied on a texture. Either on the Texture importer, like metaleap suggested (for texture assets), or when constructing the texture from script, with the ‘linear’ parameter.

As for what caused the problem with the compute shader generated texture, I’m not sure. That’s very odd. Perhaps the texture is unloaded from memory, expecting that it’s unused? But I don’t remember that ever happening to me. Usually RenderTextures only disappear when doing things like switching to full-screen.

It might have something to do with the way you apply the texture. Does the texture disappear permanently if you toggle your workaround off for one frame, and enable it again in a later frame?

Anyways, it’s good to hear you’ve worked around your problem. Thanks for keeping us up to date :).

My god, you are right, I totally overlooked the last Texture2D constructor with the bool linear parameter…

Well, as to the RenderTexture:
I use it as color and displacement texture. It works and updates correctly for the color channel (in the surface shader), whereas the displacement (vertex shader) pops in for one frame and disappears, so that the plane becomes and remains flat. It does not depend on whether I use one or two sampler2D slots for the same texture in the shader. It only resets/updates correctly if a) I create new RenderTexture object or b) I do some update on the texture like viewing it in the Inspector or changing values via Script or Inspector… :roll_eyes:
It only updates in the frames where the two lines

renderTexture.wrapMode = TextureWrapMode.Repeat;
renderTexture.wrapMode = TextureWrapMode.Clamp;

are executed as well. When disabling those lines temporarily the displacement does not work, but when called, it works again. Note that both lines are needed, because the wrapmode will not be set to what it already is. It’s so strange, it’s funny. :smile:

Here’s a snap of it all at work :wink: