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.

@JonathanPearl
I come from the future! This is necro-posting yes but I ran into the same issue and found this post thanks to google. Even later now unfortunately there is no native “RWTextureCube” like object that you can work with directly, not even in the Microsoft HLSL API docs for compute shaders.

I was able to solve this problem though much more simply and not using the code you provided. Generally, it’s a very similar approach to where we have to work instead with a RWTexture2DArray.

For my cubemap rendering once I render the 6 faces of the cubemap in order, I then actually create a new render texture with the cubemap dimension set. Then using Graphics.CopyTexture, I copy the 6 elements/slices of the RWTexture2DArray directly to the cubemap render texture. Here is some example code.

//assuming we are in a C# method...

//because there is no "RWTextureCube" that exists or we can write into...
//the closest approximation is to create a RWTexture2DArray render texture that we write into.
//this is also used later to store each of the rendered slices of the cubemap with a compute shader.
RenderTexture cubemapRender2DArray = new RenderTexture(reflectionProbe.resolution, reflectionProbe.resolution, depth, RenderTextureFormat.ARGBHalf);
cubemapRender2DArray.volumeDepth = 6; //6 faces in cubemap
cubemapRender2DArray.dimension = UnityEngine.Rendering.TextureDimension.Tex2DArray; //set the dimension to a 2DArray
cubemapRender2DArray.enableRandomWrite = true; //this is used since I use a compute shader later to stitch the cubemap faces together into this array
cubemapRender2DArray.isPowerOfTwo = true; //not really required but making sure our textures are powers of two
cubemapRender2DArray.Create();

//... render 6 faces and combine into cubemapRender2DArray using compute shader, storing each rendered face into a slice of RWTexture2DArray ...

//now to create the final proper render texture with the Cube dimension, we have to create one...
//NOTE: Keep in mind here that format wise this should be identical to the prior cubemapRender2DArray so we don't have issues later!
RenderTexture finalCubemap = new RenderTexture(reflectionProbe.resolution, reflectionProbe.resolution, depth, RenderTextureFormat.ARGBHalf);
finalCubemap.dimension = UnityEngine.Rendering.TextureDimension.Cube; //set the dimension to a Cubemap!
finalCubemap.enableRandomWrite = true; 
finalCubemap.isPowerOfTwo = true; //not really required but making sure our textures are powers of two
finalCubemap.Create();

//NOTE: Make sure that both textures format-wise are as close as you can get it (aside from obvious dimension difference)
//If you don't, your probably gonna get some issues
//now we have our final cubemap, all we need to do is just copy each of the slices into a cubemap!
Graphics.CopyTexture(cubemapRender2DArray, 0, finalCubemap, 0);
Graphics.CopyTexture(cubemapRender2DArray, 1, finalCubemap, 1);
Graphics.CopyTexture(cubemapRender2DArray, 2, finalCubemap, 2);
Graphics.CopyTexture(cubemapRender2DArray, 3, finalCubemap, 3);
Graphics.CopyTexture(cubemapRender2DArray, 4, finalCubemap, 4);
Graphics.CopyTexture(cubemapRender2DArray, 5, finalCubemap, 5);

//profit! now finalCubemap is a proper Cubemap render texture that you just wrote into!

I can provide more details if needed for future users, but this should help answer the question and