How to write negative values to render textures

I want to output the normals of a scene, using a shader I created, to a render texture. Then I want to convert it to an EXR file so I can read it with exrdisplay and see the positive and negative values of RGB.

Right now it seems that either Unity somehow automatically transforms all the values of RGB that I get from my shader below 0 to 0 or render textures don’t support negative values. Does anybody know how to bypass this problem?

Render textures, assuming you’re using a float or half format, can have negative values written to and read from. Make sure your render texture is one of those formats, and that the Texture2D you’re copying the values to with ReadPixels is the matching format (For example RenderTextureFormat.ARGBHalf and TextureFormat.RGBAHalf).

Here’s a very simple test script I wrote up to confirm this all works. This tests to make sure Color values aren’t being clamped when set, SetPixels() on a floating point texture don’t get clamped, writing to a render texture with a shader doesn’t clamp the values, ReadPixels() from a floating point render texture to a floating point texture doesn’t clamp the values, and writing out an EXR file doesn’t clamp the values.

using UnityEngine;
using System.Collections;
using System.IO;

public class NegativeEXRTest : MonoBehaviour
{
    public int resolutionX = 16;
    public int resolutionY = 16;
    public TextureFormat tex2DFormat = TextureFormat.RGBAHalf;
    public RenderTextureFormat rtFormat = RenderTextureFormat.ARGBHalf;
    public Texture2D.EXRFlags exrFlags = Texture2D.EXRFlags.None;

    [ContextMenu("Do Test")]
    public void DoTest()
    {
        // Create an array of colors that has RGB values from -1.0 to 1.0
        Color[] colors = new Color[resolutionX * resolutionY];
        int numPixels = resolutionX * resolutionY;
        for (int i=0; i<numPixels; i++)
        {
            float a = ((float)i / (float)(numPixels - 1)) * 2f - 1f;
            colors[i] = new Color(a, a, a, 1f);
        }

        // Create Texture2D and set pixels colors
        var tex = new Texture2D(resolutionX, resolutionY, tex2DFormat, false, true);
        tex.SetPixels(colors, 0);
        tex.Apply();

        // Create RenderTexture
        RenderTexture rt = new RenderTexture(resolutionX, resolutionY, 0, rtFormat, RenderTextureReadWrite.Linear);
        rt.Create();

        // Copy Texture2D to RenderTexture
        // It would be faster to use CopyTexture, but Blit() works by rendering the source texture into the destination
        // render texture with a simple unlit shader.
        Graphics.Blit(tex, rt);

        // Read RenderTexture contents into a new Texture2D using ReadPixels
        var texReadback = new Texture2D(resolutionX, resolutionY, tex2DFormat, false, true);
        Graphics.SetRenderTarget(rt);
        texReadback.ReadPixels(new Rect(0, 0, resolutionX, resolutionY), 0, 0, false);
        Graphics.SetRenderTarget(null);
        texReadback.Apply();

        // Save out EXR file to project's root folder (outside of assets)
        byte[] bytes = texReadback.EncodeToEXR(exrFlags);
        File.WriteAllBytes(Application.dataPath + "/../Negative EXR Test.exr", bytes);
        Debug.Log("Saved texture to: " + Application.dataPath + "/../Negative EXR Test.exr");

        // Destroy texture objects
        Object.DestroyImmediate(tex);
        Object.DestroyImmediate(texReadback);
        Object.DestroyImmediate(rt);
    }
}

Add this to a game object and right click on the component, then select the Do Test option. It’ll save out an exr file with a range of RGB values from -1.0 to 1.0. I confirmed this works with 2018.1.0f2. You can play around with the format and exr compression settings to see how that breaks things if you want. However setting the Exr Flags = Compress PIZ option crashes the editor, so don’t do that.

7 Likes

I was wondering whether it is possible to do this from inside a shader. Because if I try to pass negative values from the shader then it automatically converts them to 0 and all the negative values are black in the final exr picture.

That’s what the Blit() is testing. Blit reads the initially generated texture value and outputs those values using a simple internal shader, the fragment shader function for which can basically be summed up as:

return tex2D(_MainTex, i.uv);

In my list of things I’m testing, and my comments within the script I call it out that I’m using Blit() to make sure shaders writing negative values work.

Thank you, it works. Is there any way that when for example I pass -1,0,0 as values for the color that it shows it in the final exr as red(as if it was 1,0,0)?

Minor necro, but here’s a shader to test the output of the above script.

Shader "Unlit/NegativeValueTest"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "PreviewType"="Plane" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
           
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
           
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
           
            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);

                if (any(col.rgb < 0.0))
                    col = fixed4(0.5,0,0,1) + 0.5 * abs(col);

                return col;
            }
            ENDCG
        }
    }
}

Set the texture asset created by the above script to have no compression, and assign it to a material using this shader. Any pixel values that are negative will be shown with a red tint.

1 Like