Development notes, Reusable code

Unity Bug workaround: ReadPixels() and RenderTexture.antiAliasing > 1

This is a fairly nefarious bug in Unity that was reported at Issue ID
681089 ([TEXTURE2D] TEXTURE2D.READPIXELS() FAILS IF RENDERTEXTURE HAS ANTI-ALIASING SET) and was causing some serious problems for my Panorama Capture plug-in, since it prevented me from enabling MSAA anti-aliasing. If I tried, it would cause my output renders to be solid black.

The problem

My code was simple enough: render a view to a render texture, then use ReadPixels() to get the result to a Texture2D for further processing:

renderTexture = new RenderTexture(width, height, /*depth*/24);
renderTexture.antiAliasing = 2;
renderTexture.Create();​
// ...
cam.targetTexture = renderTexture;
cam.Render();
RenderTexture.active = renderTexture;
tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
tex.Apply();​

The output is black. Removing the “antiAliasing” line causes the output to appear correctly in tex, as expected.

When possible: just don’t use ReadPixels

When possible, it’s better to avoid the problem entirely by avoiding ReadPixels(). For example, it’s possible to write a compute shader that directly accesses the anti-aliased RenderTexture on the GPU and manipulates the result in some manner. When possible, this is the most efficient solution, and may be more efficient than what you were already doing.

A complete workaround with a Copy Compute Shader

After stumbling across the finding that compute shaders are able to access the anti-aliased render target, this led to the idea that we can exploit this to get at the data. This will only work on platforms supporting compute shaders; check SystemInfo.supportsComputeShaders and take alternate action such as disabling anti-aliasing if it does not.

First, here is the compute shader, a trivial shader that copies from one render target to another:

#pragma kernel Copy

RWTexture2D dest;
Texture2D source;
int width, height;
SamplerState MyPointRepeatSampler;

[numthreads(32,32,1)] // Must match .cs file
void Copy (uint3 id : SV_DispatchThreadID) {
    dest[id.xy] = source.SampleLevel(MyPointRepeatSampler,
        float2(((float)id.x + 0.5) / width,
               ((float)id.y + 0.5) / height), 0);
}

We use a point sampler and calculate the indices to ensure each pixel is copied exactly.

We create a slot for the compute shader on the script (public ComputeShader copyShader) and assign it to the script by default by clicking on the .cs file in Inspector and dragging the compute shader into the slot.

Next, we will need three things:

  1. A render target with antiAliasing > 1, to render our initial scene into with anti-aliasing;
  2. A render target with antiAliasing = 1 (of the same size and format), to receive a copy of the scene from the shader;
  3. A Texture2D texture to receive the result of ReadPixels().

Finally, here’s what the complete code looks like:

// Initialize textures
renderTexture = new RenderTexture(width, height, /*depth*/24);
renderTexture.antiAliasing = 2;
renderTexture.Create();​
renderTextureCopy = new RenderTexture(width, height, /*depth*/0);
// To allow copy shader to write to it
renderTextureCopy.enableRandomWrite = true;
renderTextureCopy.antiAliasing = 1;
renderTextureCopy.Create();​
tex = new Texture2D(width, height);
copyKernelIdx = copyShader.FindKernel("Copy");

// Render to render texture
cam.targetTexture = renderTexture;
cam.Render();

// Copy from anti-aliased render target to un-antialiased target with compute shader
copyShader.SetTexture(copyKernelIdx, "dest", renderTextureCopy);
copyShader.SetTexture(copyKernelIdx, "source", renderTexture);
copyShader.SetInt("width", width);
copyShader.SetInt("height", height);
int threadsX = 32, threadsY = 32; // Must match shader
copyShader.Dispatch(copyKernelIdx,
                    (width  + threadsX - 1) / threadsX,
                    (height + threadsY - 1) / threadsY, 1);

// Read pixels out of copy
RenderTexture.active = renderTextureCopy;
tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
tex.Apply();​

Hopefully this’ll be helpful to someone else plagued by this bug. Let me know if you have any questions.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s