This tutorial will teach you how to create non euclidean cubes in Unity, giving the illusion that each face is a door onto another dimension. This post is part of a series of tutorials on impossible geometries.

This effect can be seen in many game, most notoriously Antichamber which uses it extensively.

You can download the Unity package here.



Step 1. Stencil Theory

For this example, all the geometries will be contained within the cube, at the same time. This particular effect is achieved using stencil buffer. The GPU offers a special buffer which can be used to mask and discard pixels. The idea behind non-euclidean cubes is strikingly simple and it requires two steps:

Stencil mask: each face of the cube writes a specific value to the stencil buffer (yellow and green, in this example).

Stencil geometry: the geometry that has to be shown in the yellow face only draws pixels where the stencil buffer is yellow.

All the objects exists at the same time, but their pixels are only drawn in the respective faces.

Step 2. The stencil mask

The first step is to create a material that initialises the stencil buffer with a specific value. To do this, we use a simple vertex and fragment shader (learn how to use them: Vertex and Fragment Shader). Even if in front of the geometry, is essential that this mask is drawn before the content of the cube. To do this, we change the Queue value to Geometry-100.

StencilMask.shader Shader "Stencils/StencilMask" { Properties { _StencilMask("Stencil Mask", Int) = 0 } SubShader { Tags { "RenderType" = "Opaque" "Queue" = "Geometry-100" } ColorMask 0 ZWrite off Stencil { Ref[_StencilMask] Comp always Pass replace } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag struct appdata { float4 vertex : POSITION; }; struct v2f { float4 pos : SV_POSITION; }; v2f vert(appdata v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); return o; } half4 frag(v2f i) : COLOR { return half4(1,1,0,1); } ENDCG } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 Shader "Stencils/StencilMask" { Properties { _StencilMask ( "Stencil Mask" , Int ) = 0 } SubShader { Tags { "RenderType" = "Opaque" "Queue" = "Geometry-100" } ColorMask 0 ZWrite off Stencil { Ref [ _StencilMask ] Comp always Pass replace } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag struct appdata { float4 vertex : POSITION ; } ; struct v2f { float4 pos : SV_POSITION ; } ; v2f vert ( appdata v ) { v2f o ; o . pos = mul ( UNITY_MATRIX_MVP , v . vertex ) ; return o ; } half4 frag ( v2f i ) : COLOR { return half4 ( 1 , 1 , 0 , 1 ) ; } ENDCG } } }

Lines 10-16 write into the stencil buffer. The line Ref[_StencilMask] indicates that the value we want to write is provided in the property _StencilMask. Comp always indicates that the value has to be written regardless of what was on the stencil buffer already.

Step 3. The stencil geometry

What we need now is a material which draws only if the stencil buffer has been initialised accordingly to a specific value. This is done using the Comp equal condition.

StencilGeometry.shader Shader "Stencils/StencilGeometry" { Properties { _StencilMask("Stencil Mask", Int) = 0 _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 Stencil { Ref[_StencilMask] Comp equal Pass keep Fail keep } CGPROGRAM // Physically based Standard lighting model, and enable shadows on all light types #pragma surface surf Standard fullforwardshadows // Use shader model 3.0 target, to get nicer looking lighting #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; }; half _Glossiness; half _Metallic; fixed4 _Color; void surf (Input IN, inout SurfaceOutputStandard o) { // Albedo comes from a texture tinted by color fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb; // Metallic and smoothness come from slider variables o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 Shader "Stencils/StencilGeometry" { Properties { _StencilMask ( "Stencil Mask" , Int ) = 0 _Color ( "Color" , Color ) = ( 1 , 1 , 1 , 1 ) _MainTex ( "Albedo (RGB)" , 2D ) = "white" { } _Glossiness ( "Smoothness" , Range ( 0 , 1 ) ) = 0.5 _Metallic ( "Metallic" , Range ( 0 , 1 ) ) = 0.0 } SubShader { Tags { "RenderType" = "Opaque" } LOD 200 Stencil { Ref [ _StencilMask ] Comp equal Pass keep Fail keep } CGPROGRAM // Physically based Standard lighting model, and enable shadows on all light types #pragma surface surf Standard fullforwardshadows // Use shader model 3.0 target, to get nicer looking lighting #pragma target 3.0 sampler2D _MainTex ; struct Input { float2 uv_MainTex ; } ; half _Glossiness ; half _Metallic ; fixed4 _Color ; void surf ( Input IN , inout SurfaceOutputStandard o ) { // Albedo comes from a texture tinted by color fixed4 c = tex2D ( _MainTex , IN . uv_MainTex ) * _Color ; o . Albedo = c . rgb ; // Metallic and smoothness come from slider variables o . Metallic = _Metallic ; o . Smoothness = _Glossiness ; o . Alpha = c . a ; } ENDCG } FallBack "Diffuse" }

The Stencil block in lines 13-18 can be easily added to many existing shaders, allowing to use virtually any material you want.

Step 4. Putting all together

To recap:

A standard cube structure;

Each face of the cube is a quad has a different material, which uses the StencilMask shader with a unique value for _StencilMask ;

shader with a unique value for ; All the geometry pieces are inside the cube;

Each geometry piece has it’s own material, derived from StencilGeometry and initialised to match the _StencilMask property of its respective face.

You can download the full Unity package here.

Conclusion

Become a Patron!

Non-euclidean cubes are always intriguing and can add an interesting twist to your game.

Despite the name, these cubes are actually euclidean. This technique works nicely for decorative elements, but it fails to provide an efficient solution to the portal effect seen in (guess!) Portal or Parallax. Future tutorials will cover other impossible geometries.

Part 1: Non-Euclidean Cubes

Non-Euclidean Cubes Part 2: The Portal Effect

The Portal Effect Part 3: The Physics of Monument Valley

This tutorial is inspired by Noisecrime’s post called “Stencils for portal rendering“.