How to write to a RenderTexture as a cubemap

So after a lot of digging I found RenderTexture.isCubeMap. Then I found that it was obsolete.

Then I found RenderTexture.dimension and, by extension, UnityEngine.Rendering.TextureDimension.Cube which seems to be what I want.

But I’m not sure how this is used. How are the six faces represented internally when the rendertexture is created this way? I’m currently writing a compute shader that writes to a render texture, and I’m not sure how I should be writing my output that writes to the cubemap.

So what do I do with this in the compute shader…

RWTexture2D<float4> o_result;

//...

o_result[tex.xy] = float4(outputColor.x, outputColor.y, outputColor.z, 1);

As you can see, this is for a 2d texture, is there anything special I need to do to get it working with a cubemap? My first instinct is something like:

RWTexture3D<float4> o_result;

//...
// Where tex.xyz flies on the outside of the cube? How do I address each side's pixels...
o_result[tex.xyz] = float4(outputColor.x, outputColor.y, outputColor.z, 1);

If someone has visibility on cubemap rendertextures but not compute shaders, that’s fine. I’m just very unsure as to how this all lays out in addressable memory and using RenderTexture.dimension isn’t very well documented.

Well apparently it is actually impossible to write to a cubemap in a compute shader in Unity. No matter what I did, nothing would show up on the other faces. That’s incredibly upsetting.
I ended up writing a faux cubemap implementation using a RWTexture2DArray, which is something you CAN write to from a compute shader. I’m pretty happy with it currently.

This workaround is very fast (if baked into a model) and only needs one sample instruction. If it’s being used for a reflection map it ends or something that needs to be calculated at fragtime, it’s an additional 15 math operations but still only one sample.

That said, this wouldn’t have been needed if I could just write to the cubemap in the compute shader, and that would be a much better way to do this. (I hope someone from Unity reads this)

So noone else gets burned by this niche usecase and spends the many hours to reach the same conclusion I have, I’ve included all of the code I’ve written to make this possible below. [99269-cubemapdemounitypackage.zip|99269]. If only Unity Answers had spoiler tags or a way to collapse sections…

Scripts##


CubemapTransform.cs

#if SHADER_TARGET || UNITY_VERSION // We're being included in a shader
// Convert an xyz vector to a uvw Texture2DArray sample as if it were a cubemap
float3 xyz_to_uvw(float3 xyz)
{
    // Find which dimension we're pointing at the most
    float3 absxyz = abs(xyz);
    int xMoreY = absxyz.x > absxyz.y;
    int yMoreZ = absxyz.y > absxyz.z;
    int zMoreX = absxyz.z > absxyz.x;
    int xMost = (xMoreY) && (!zMoreX);
    int yMost = (!xMoreY) && (yMoreZ);
    int zMost = (zMoreX) && (!yMoreZ);

    // Determine which index belongs to each +- dimension
    // 0: +X; 1: -X; 2: +Y; 3: -Y; 4: +Z; 5: -Z;
    float xSideIdx = 0 + (xyz.x < 0);
    float ySideIdx = 2 + (xyz.y < 0);
    float zSideIdx = 4 + (xyz.z < 0);

    // Composite it all together to get our side
    float side = xMost * xSideIdx + yMost * ySideIdx + zMost * zSideIdx;

    // Depending on side, we use different components for UV and project to square
    float3 useComponents = float3(0, 0, 0);
    if (xMost) useComponents = xyz.yzx;
    if (yMost) useComponents = xyz.xzy;
    if (zMost) useComponents = xyz.xyz;
    float2 uv = useComponents.xy / useComponents.z;

    // Transform uv from [-1,1] to [0,1]
    uv = uv * 0.5 + float2(0.5, 0.5);

    return float3(uv, side);
}        

// Convert an xyz vector to the side it would fall on for a cubemap
// Can be used in conjuction with xyz_to_uvw_force_side
float xyz_to_side(float3 xyz)
{
    // Find which dimension we're pointing at the most
    float3 absxyz = abs(xyz);
    int xMoreY = absxyz.x > absxyz.y;
    int yMoreZ = absxyz.y > absxyz.z;
    int zMoreX = absxyz.z > absxyz.x;
    int xMost = (xMoreY) && (!zMoreX);
    int yMost = (!xMoreY) && (yMoreZ);
    int zMost = (zMoreX) && (!yMoreZ);

    // Determine which index belongs to each +- dimension
    // 0: +X; 1: -X; 2: +Y; 3: -Y; 4: +Z; 5: -Z;
    float xSideIdx = 0 + (xyz.x < 0);
    float ySideIdx = 2 + (xyz.y < 0);
    float zSideIdx = 4 + (xyz.z < 0);

    // Composite it all together to get our side
    return xMost * xSideIdx + yMost * ySideIdx + zMost * zSideIdx;
}

// Convert an xyz vector to a uvw Texture2DArray sample as if it were a cubemap
// Will force it to be on a certain side
float3 xyz_to_uvw_force_side(float3 xyz, float side)
{
    // Depending on side, we use different components for UV and project to square
    float3 useComponents = float3(0, 0, 0);
    if (side < 2) useComponents = xyz.yzx;
    if (side >= 2 && side < 4) useComponents = xyz.xzy;
    if (side >= 4) useComponents = xyz.xyz;
    float2 uv = useComponents.xy / useComponents.z;

    // Transform uv from [-1,1] to [0,1]
    uv = uv * 0.5 + float2(0.5, 0.5);

    return float3(uv, side);
}

// Convert a uvw Texture2DArray coordinate to the vector that points to it on a cubemap
float3 uvw_to_xyz(float3 uvw)
{
    // Use side to decompose primary dimension and negativity
    int side = uvw.z;
    int xMost = side < 2;
    int yMost = side >= 2 && side < 4;
    int zMost = side >= 4;
    int wasNegative = side & 1;

    // Insert a constant plane value for the dominant dimension in here
    uvw.z = 1;

    // Depending on the side we swizzle components back (NOTE: uvw.z is 1)
    float3 useComponents = float3(0, 0, 0);
    if (xMost) useComponents = uvw.zxy;
    if (yMost) useComponents = uvw.xzy;
    if (zMost) useComponents = uvw.xyz;

    // Transform components from [0,1] to [-1,1]
    useComponents = useComponents * 2 - float3(1, 1, 1);
    useComponents *= 1 - 2 * wasNegative;

    return useComponents;
}
#else // We're being included in a C# workspace
using UnityEngine;

namespace CubemapTransform
{
    public static class CubemapExtensions
    {
        // Convert an xyz vector to a uvw Texture2DArray sample as if it were a cubemap
        public static Vector3 XyzToUvw(this Vector3 xyz)
        {
            return xyz.XyzToUvwForceSide(xyz.XyzToSide());
        }

        // Convert an xyz vector to the side it would fall on for a cubemap
        // Can be used in conjuction with Vector3.XyzToUvwForceSide(int)
        public static int XyzToSide(this Vector3 xyz)
        {
            // Find which dimension we're pointing at the most
            Vector3 abs = new Vector3(Mathf.Abs(xyz.x), Mathf.Abs(xyz.y), Mathf.Abs(xyz.z));
            bool xMoreY = abs.x > abs.y;
            bool yMoreZ = abs.y > abs.z;
            bool zMoreX = abs.z > abs.x;
            bool xMost = (xMoreY) && (!zMoreX);
            bool yMost = (!xMoreY) && (yMoreZ);
            bool zMost = (zMoreX) && (!yMoreZ);

            // Determine which index belongs to each +- dimension
            // 0: +X; 1: -X; 2: +Y; 3: -Y; 4: +Z; 5: -Z;
            int xSideIdx = xyz.x < 0 ? 1 : 0;
            int ySideIdx = xyz.y < 0 ? 3 : 2;
            int zSideIdx = xyz.z < 0 ? 5 : 4;

            // Composite it all together to get our side
            return (xMost ? xSideIdx : 0) + (yMost ? ySideIdx : 0) + (zMost ? zSideIdx : 0);
        }

        // Convert an xyz vector to a uvw Texture2DArray sample as if it were a cubemap
        // Will force it to be on a certain side
        public static Vector3 XyzToUvwForceSide(this Vector3 xyz, int side)
        {
            // Depending on side, we use different components for UV and project to square
            Vector2 uv = new Vector2(side < 2 ? xyz.y : xyz.x, side >= 4 ? xyz.y : xyz.z);
            uv /= xyz[side / 2];

            // Transform uv from [-1,1] to [0,1]
            uv *= 0.5f;
            return new Vector3(uv.x + 0.5f, uv.y + 0.5f, side);
        }
        
        // Convert a uvw Texture2DArray coordinate to the vector that points to it on a cubemap
        public static Vector3 UvwToXyz(this Vector3 uvw)
        {    
            // Use side to decompose primary dimension and negativity
            int side = (int)uvw.z;
            bool xMost = side < 2;
            bool yMost = side >= 2 && side < 4;
            bool zMost = side >= 4;
            int wasNegative = side & 1;

            // Restore components based on side
            Vector3 result = new Vector3(
                xMost ? 1 : uvw.x, 
                yMost ? 1 : (xMost ? uvw.x : uvw.y ), 
                zMost ? 1 : uvw.y);

            // Transform components from [0,1] to [-1,1]
            result *= 2;
            result -= new Vector3(1, 1, 1);
            result *= 1 - 2 * wasNegative;

            return result;
        }
    }
}
#endif

CubemapTest.cs

using System.Collections.Generic;
using UnityEngine;

// Includes Vector3.XyzToUvw, Vector3.XyzToSide, Vector3.XyzToUvwForceSide, Vector3.UvwToXyz
using CubemapTransform;

[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
public class CubemapTest : MonoBehaviour {

    public bool bakeIntoMesh;

    public int dimensions = 1024;
    public Shader renderTextureCubemapShader;
    public ComputeShader renderTextureWriter;

    private RenderTexture cubemapRenderTexture;

    // Use this for initialization
    void Start()
    {
        // Create Render Texture
        cubemapRenderTexture = new RenderTexture(dimensions, dimensions, 0, RenderTextureFormat.ARGB32);
        {
            cubemapRenderTexture.dimension = UnityEngine.Rendering.TextureDimension.Tex2DArray;
            cubemapRenderTexture.volumeDepth = 6;
            cubemapRenderTexture.wrapMode = TextureWrapMode.Clamp;
            cubemapRenderTexture.filterMode = FilterMode.Trilinear;
            cubemapRenderTexture.enableRandomWrite = true;
            cubemapRenderTexture.isPowerOfTwo = true;
            cubemapRenderTexture.Create();
        }

        // Create material using rendertexture as cubemap
        MeshRenderer target = GetComponent<MeshRenderer>();
        {
            target.material = new Material(renderTextureCubemapShader);
            target.material.mainTexture = cubemapRenderTexture;
        }

        // If we're baking into the mesh, we'll make our own cube
        if (bakeIntoMesh)
        {
            MeshFilter filter = GetComponent<MeshFilter>();
            filter.mesh = MakeCubemapMesh();
        }
    }

    void MakeCubemapSide(
        Vector3 sideRight, Vector3 sideUp,
        List<Vector3> outPositions, List<Vector3> outBakedCoords, List<int> outTriangleIndices)
    {
        // Reserve tris
        {
            int currentStartIndex = outPositions.Count;
            outTriangleIndices.Add(currentStartIndex + 0);
            outTriangleIndices.Add(currentStartIndex + 1);
            outTriangleIndices.Add(currentStartIndex + 2);

            outTriangleIndices.Add(currentStartIndex + 3);
            outTriangleIndices.Add(currentStartIndex + 2);
            outTriangleIndices.Add(currentStartIndex + 1);
        }

        // Make verts
        {
            Vector3 sideForward = Vector3.Cross(sideUp, sideRight);
            Vector3[] vertices = new Vector3[4];
            int idx = 0;
            vertices[idx++] = sideForward + ( sideUp) + (-sideRight); // Top left
            vertices[idx++] = sideForward + ( sideUp) + ( sideRight); // Top right
            vertices[idx++] = sideForward + (-sideUp) + (-sideRight); // Bottom left
            vertices[idx++] = sideForward + (-sideUp) + ( sideRight); // Bottom right

            int sideIndex = sideForward.XyzToSide();
            foreach (Vector3 vertex in vertices)
            {
                outPositions.Add(vertex / 2); // Divide in half to match the dimensions of unity's cube
                outBakedCoords.Add(vertex.XyzToUvwForceSide(sideIndex));
            }
        }
    }

    Mesh MakeCubemapMesh()
    {
        Mesh mesh = new Mesh();

        List<Vector3> positions     = new List<Vector3>();
        List<Vector3> bakedCoords   = new List<Vector3>();
        List<int> triangleIndices   = new List<int>();

        MakeCubemapSide(Vector3.right,   Vector3.up,      positions, bakedCoords, triangleIndices); // +X
        MakeCubemapSide(Vector3.left,    Vector3.up,      positions, bakedCoords, triangleIndices); // -X
        MakeCubemapSide(Vector3.up,      Vector3.forward, positions, bakedCoords, triangleIndices); // +Y
        MakeCubemapSide(Vector3.down,    Vector3.forward, positions, bakedCoords, triangleIndices); // -Y
        MakeCubemapSide(Vector3.forward, Vector3.right,   positions, bakedCoords, triangleIndices); // +Z
        MakeCubemapSide(Vector3.back,    Vector3.right,   positions, bakedCoords, triangleIndices); // -Z

        mesh.vertices   = positions.ToArray();
        mesh.normals    = bakedCoords.ToArray();
        mesh.triangles  = triangleIndices.ToArray();

        return mesh;
    }

    private int counter = 0;
    void Update()
    {
        // Draw to Render Texture with Compute shader every few seconds
        // So feel free to recompile the compute shader while the editor is running
        if (counter == 0)
        {
            string kernelName = "CSMain";
            int kernelIndex = renderTextureWriter.FindKernel(kernelName);

            renderTextureWriter.SetTexture(kernelIndex, "o_cubeMap", cubemapRenderTexture);
            renderTextureWriter.SetInt("i_dimensions", dimensions);
            renderTextureWriter.Dispatch(kernelIndex, dimensions, dimensions, 1);
        }
        counter = (counter + 1) % 300;
    }
}

Shaders##


CubemapTest.compute

#pragma kernel CSMain

// Inputs
int i_dimensions;

// Outputs
RWTexture2DArray<float4> o_cubeMap;

// Includes xyz_to_uvw, uvw_to_xyz, xyz_to_side, xyz_to_uvw_force_side based on a macro for shaders
#include "CubemapTransform.cs"

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
	[unroll]
	for (int i = 0; i < 6; ++i)
	{
		// Colors the texture based on the xyz of the cubemap
		float3 uvw = float3(float(id.x) / (i_dimensions-1), float(id.y) / (i_dimensions-1), i);
		float3 xyz = abs(uvw_to_xyz(uvw));

		// This just puts a unique colored dot in the middle of the texture for each side
		// (Not important)
		{
			float dist = length(xyz);
			float3 centerColor = float3( // Find the color of the dot for this side
				i / 2 == 0 || (i & 1 && i / 2 == 1),
				i / 2 == 1 || (i & 1 && i / 2 == 2),
				i / 2 == 2 || (i & 1 && i / 2 == 0));
			if (dist < 1.03) xyz = float3(0, 0, 0); // Outline
			if (dist < 1.02) xyz = centerColor; // Dot
		}

		o_cubeMap[int3(id.x, id.y, i)] = float4(xyz, 1);
	}
}

CubemapTestFrag.shader

Shader "Unlit/CubemapTestFrag"
{
	Properties
	{
		_MainTex ("Texture", 2DArray) = "white" {}
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		Cull Off
		LOD 100

		Pass
		{
			CGPROGRAM

			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"

			// Includes xyz_to_uvw, uvw_to_xyz, xyz_to_side, xyz_to_uvw_force_side based on a macro for shaders
			#include "CubemapTransform.cs"

			struct appdata
			{
				float4 vertex : POSITION;
			};

			struct v2f
			{
				float4 vertex : SV_POSITION;

				// Custom interpolators
				float4 raw_position : TEXCOORD0;
			};

			UNITY_DECLARE_TEX2DARRAY(_MainTex);
			
			v2f vert (appdata v)
			{
				v2f o;
				o.raw_position = v.vertex;
				o.vertex = UnityObjectToClipPos(v.vertex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				// You can verify that both functions correspond with each other by uncommenting the following line (it should be black = no difference)
				// return abs(UNITY_SAMPLE_TEX2DARRAY(_MainTex, xyz_to_uvw(i.raw_position.xyz)) - UNITY_SAMPLE_TEX2DARRAY(_MainTex, xyz_to_uvw(uvw_to_xyz(xyz_to_uvw(i.raw_position.xyz)))));

				return UNITY_SAMPLE_TEX2DARRAY(_MainTex, xyz_to_uvw(i.raw_position.xyz));
			}

			ENDCG
		}
	}
}

CubemapTestBaked.shader

Shader "Unlit/CubemapTestBaked"
{
	Properties
	{
		_MainTex ("Texture", 2DArray) = "white" {}
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		Cull Off
		LOD 100

		Pass
		{
			CGPROGRAM

			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"

			// Includes xyz_to_uvw, uvw_to_xyz, xyz_to_side, xyz_to_uvw_force_side based on a macro for shaders
			// But we don't need this since we've baked in the Tex2DArray sample coords in C# script
			// #include "CubemapTransform.cs"

			struct appdata
			{
				float4 vertex : POSITION;

				// Note: Not actually normal, we just stole its semantic
				float4 bakedSampleCoord : NORMAL;
			};

			struct v2f
			{
				float4 vertex : SV_POSITION;

				// Custom interpolators
				float4 bakedSampleCoord : TEXCOORD0;
			};

			UNITY_DECLARE_TEX2DARRAY(_MainTex);
			
			v2f vert (appdata v)
			{
				v2f o;
				o.bakedSampleCoord = v.bakedSampleCoord;
				o.vertex = UnityObjectToClipPos(v.vertex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				// Note, we don't have to call xyz_to_uvw() when we have it baked
				// Which makes our frag shader quite faster (saves around 15 instructions)
				return UNITY_SAMPLE_TEX2DARRAY(_MainTex, i.bakedSampleCoord);
			}

			ENDCG
		}
	}
}

I’ve never been able to render to a cubemap natively. As a workaround, you can render the faces individually, then stitch them into a cubemap pretty easily.