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
}
}
}