Sphere Scene in Unity URP

Jordi Caballol
13 min readJan 5, 2022

--

In this article, I’ll break down the techniques I used to render a scene contained inside a sphere, cutting the objects and showing their interior achieving a fake volumetric effect.

The result can be seen in the following video:

All the effects shown here are made in Unity with URP, using Render Features.

1. Cutting objects with the sphere

The first thing we need to do is to avoid rendering anything outside the sphere.

As I’ll need to do the same in all the shaders for this project, I’ll create a subgraph with the relevant nodes so I can reuse it.

To achieve this, we are going to use alpha clipping. The idea is simple: get the distance from the current pixel to the center of the sphere. If it’s smaller than the radius, the pixel is inside (we keep it); if it’s greater than the radius, the pixel is outside (we discard it). This is the subgraph:

Clip sphere subgraph
The nodes for clipping the mesh with the sphere

We can see it’s very simple:

  • I pass in a Vector4 encoding the sphere. RGB is the center and A is the radius.
  • I take the distance from the center to the pixel.
  • I compare them using Step. Step will output 1 if edge (the radius) is greater than in, and 0 otherwise.

Now, in order to create a material that will be cut by the sphere, I simply need to add a way of getting the data of the sphere, enable alpha clipping, and use this node to get the alpha.

In order to to get the information of the sphere in the material I add the following property to it:

The WorldSphere property
The WorldSphere property

This property isn’t exposed, so I will set it from code instead of setting it in the material. This will allow me to keep it updated when the sphere changes or moves.

Now I only need to use all of this in a material. A very simple example is:

Simple material using the ClipSphere sub-graph

Finally, we need to set the values of the sphere from code. For this, we will use a component to set it, and we will attach it to the object that we want to be the center of the sphere (in the case of my scene, the character).

The component looks like this:

The code for setting up the sphere

The code is very simple. Some key points:

  • I use [ExecuteAlways] so the sphere gets updated even outside of play mode (this way I can see the changes when I edit the component).
  • I use LateUpdate instead of Update, this way this will wait until the character has moved and use the updated position.
  • To upload the info to the shaders, simply use Shader.SetGlobalVector. I save the value of PropertyToID in WORLD_SPHERE to avoid having to calculate it every frame.

With all this we will have something that looks like the following:

Clipping objects with the sphere

2. Seeing the slice of the object

With what we have done so far, the object simply disappears when it goes out of the sphere. This is very different from the effect we see in the scene where we can see the inside of the object where the sphere cuts it.

To achieve this effect we will use the following approach:

Diagram of the technique for drawing the slices

We will render the back faces of the object, clipping them with the sphere, but instead of using the normal and the position of the backface (P1), we will calculate the intersection of the view vector with the sphere (P2) and use its normal and position.

First of all, we will duplicate the shader we created, and we will set it to render back faces instead of front faces and we will disable the shadows for this material (if it receives shadows we will see its actual shape and it will break the effect). Then, we will duplicate the materials and use the new shader instead and, finally, we will duplicate the objects and use the new materials in the new objects.

With this we get a scene like this:

The scene with back faces

Now we need to calculate the normal and the position of the sphere instead of using the ones from the backfaces. To achieve this I used the Geometric Solution from this article: https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-sphere-intersection

Translated to HLSL, the method from this page looks like this:

Code for the intersection with the sphere

This function gets the information of the sphere, the ray origin (camera pos), and the ray direction (view dir), and computes the position, normal, and a float that tells us if the ray intersects with the sphere.

With this code, we can now create a sub-graph that contains a custom node with this function, this way we can reuse the sub-graph for all of our backface shaders.

The sub-graph for the sphere intersection

In the subgraph, we simply get the necessary data (camera position and view direction), pass it to the HLSL function, and return the result.

Now, we can use this in the backface shader to get the correct normal:

Simple backface shader with the normal from the sphere

In the shader, we simply use the sphere information for both the clipping and the sphere intersection. We combine the visibility we get from both methods and return the sphere normal.

Note that the normal is in world space. In order to return it, I need to transform it to tangent space.

Now the result looks like this:

The same scene but with the sphere normals

3. Slice underground

As we can see in the last screenshot, the technique we’ve seen so far works well with objects, but it does nothing for the plane I’ve used for the ground.

This is because there is no backface visible for the ground.

To work around this, we’ll need to use a slightly different approach:

Alternative technique for the ground

Just like before, we will use the intersection with the sphere to calculate P2, but this time we will use the front face of the terrain instead of the back face.

This technique has the extra problem that it would also render above the ground (the green line in the diagram). To avoid this we will discard any pixel that is above the ground (height greater than 0).

As a shader, this approach looks like this:

Simple shader to see underground

We use the intersection to get the normal and the position of the sphere. We use the normal like in the other shaders, and we discard the pixel if the Y(G) component of the position is greater than 0.

We also discard the pixel if the intersection with the sphere failed.

With this simple shader, the scene looks like the following:

The scene showing the underground correctly

4. Hidding things underground

This scene is starting to look good now! However, one of the effects I was looking forward to was having things buried underground that would appear when we slice the ground. The best example is the roots of the trees.

Let’s see what happens when we try to do this with our example scene:

The problem when we put things underground

We can see how, despite how it looks, the slice of the cube is actually the backface, so when we start putting it underground it renders BEHIND the floor, and therefore it isn’t visible.

To achieve this, we need some way of telling the ground that it shouldn’t render where there are objects underground. We’ll do this using stencil through Renderer Features.

The first thing we’ll need to set up is the layers, as they will be key to setting up the Renderer Features. I created two of them:

  • Interior: this layer will contain the objects that use the backface shader for the normal objects (the cube in this example)
  • Terrain: this layer will contain the two ground objects (the surface of the ground and the ground with the sphere effect)

With the layers in place, we can set up the features. All of this is done in the UniversalRenderer asset.

First, we set the filters to ignore these two layers. We want to render them manually instead. With this change, we should only see the front faces of the cube now:

Setting the filters
Only the front faces of the cube are visible

Now we’ll add the RenderFeature that will render the slices of the object:

The feature is of type Render Objects, which is built-in in URP, so we don’t need to code anything.

The settings for the slices

We set it to render in the AfterRenderingOpaques event, so the front faces will be already rendered, and we tell it to render all the opaque objects in the Interiors layer.

In order to tell the ground where it shouldn’t render, we set the stencil like this:

  • We need a value of reference, in this case, I use 1.
  • The compare function is Always. I don’t want to discard any backface.
  • We set the Pass to Replace, this means that the value will be exactly 1 wherever I draw a backface.

With this pass ready we can add the pass for the ground after this one:

The settings for the terrain

This time, the layer we want to render is the Terrain layer. Aside from this, the only difference is in the stencil. Now it works like the following:

  • The value is the same one we used before.
  • The compare function is Not Equal. This means that it will only render pixels where the value of the stencil is NOT EQUAL to Value (1 in our case). As the backfaces have the value 1, it won’t render over them.

With these simple changes, now we can bury the cube underground:

The underground slice now shows the cube

5. An easier set-up

If you followed all the steps, you might have realized how cumbersome is to set everything up, with all the duplicated meshes.

The easiest improvement to this is to avoid duplicating the terrain mesh. We can use another RendererFeature that renders the Ground layer again using the underground material:

The Underground RendererFeature

For the rest of the meshes, I created a component to handle this: the CuttableRenderer. Let’s see it step by step.

Members of the CuttableRenderer

The first thing we need is the data we are going to use. We need:

  • Main Mesh: The mesh.
  • Outside Material: The material of the front faces.
  • Inside Material: The material of the back faces.

We also save the references to the created objects, but they are internal stuff, so we use HideInInspector to not show them.

With this data, we can create the objects:

The CreateObject method

This is quite a big method, so let’s go bit by bit.

First of all, we check all the conditions to know if we need an object. The conditions are that we need to have a mesh and at least one material.

If we don’t need the mesh but we have an object, we destroy it. As we can’t destroy objects inside OnValidate (we are going to call this method from there) I use delayCall to destroy it after OnValidate has finished.

If we haven’t reached this code it means that we do need an object. So first thing is to create a new one, set it to the appropriate layer, and set it as a child of this object (so it moves with it).

The next thing we do is to get the MeshFilter and the MeshRenderer, or create them if they don’t exist.

Then we create a list of materials for the object. If the user specified more materials than needed, we only keep the first ones. If they specified less than needed, we repeat the first one to cover all the slots (the mesh.subMeshCount).

And finally, we set all the data in the MeshFilter and the MeshRenderer.

The only thing left, is to use this in OnValidate to create the meshes:

OnValidate in the CuttableRenderer

I create the two objects and I save a reference to them. There are three differences between the calls to CreateObject:

  • The main reason this exists: the different materials.
  • The layer of the outside object is Default, the layer of the inside object is Interiors.
  • Only the outside object casts shadows.

Additionally, I hide the objects in the hierarchy. This means that the user can’t edit or delete the generated objects.

With this we can set up our objects way more easily:

The settings for the cube of the example

6. More detail inside

With this component ready, it’s very easy to extend it so we can add extra meshes inside the objects. I use this for the hollow pumpkins for example.

In order to do this, the first thing we will need is some extra information:

The members, including the new ones

We basically add fields for the mesh and the materials we are going to use for this, and a checkbox to decide whether we need to add an extra object with the backfaces for this other mesh. We also add two fields for the new objects.

Now we only need to create them:

The updated OnValidate method

The new objects will both go to the Interiors layer, and they’ll have the shadows disabled.

Now we can create the pumpkins we see in the scene:

The pumpkin and its renderer

7. Procedural detail: ground layers

Now that the basic tech is ready, we can work on adding some extra details.

The simplest one is adding a band to the underground. First, we are going to simply add a horizontal line:

A simple horizontal line, based on the height

We simply get the Y component of the position (of the sphere, not the pixel) and compare it with -layerDepth. We then use Lerp to choose the color according to this.

To make it a bit more interesting, we are going to add some noise:

Adding noise to the band

To calculate the noise, we use the X and Z of the position as UV. This will give us a number in the (0,1) range. We subtract -0.5 to center it around zero (the range is now (-0.5, 0.5)) and multiply it with the strength.

With the noise calculated, we now compare the Y of the position with (noise-layerDepth) to get the new band.

Finally, the last thing I want to add is some anti-aliasing. For this, I created a small sub-graph called AntialiasedStep:

The anti-aliased step

I won’t go much into detail about how it works, but this will give us some intermediate values where the value changes, instead of only 0 or 1 as Step does.

The difference is really visible when you zoom into the image:

Left: Step — Right: AntialiasedStep

8. Procedural detail: wooden patterns

I used a similar technique to show wood patterns when I slice through the wooden parts of the fence in the scene.

The fence and the wood patterns

This actually uses two different effects: one for the planks and one for the posts. I’ve implemented them inside of sub-graphs to reuse them for both the front face and the back face shaders.

Let’s start with the plank effect:

The effect for the planks

This sub-graph takes the position X and Y as input. First I stretch this multiplying one of the components by 5, and I use this as UV for the noise.

If I used Step with the noise, I would only get a clear area and a dark area like in the terrain. In order to obtain various lines, I multiply the noise by 5 and use Fraction. This will give me 5 areas that go from 0 to 1 so, when I apply the Step, it creates 5 lines.

The posts are slightly different:

The effect for the posts

In this case, instead of using noise, I get the position X and Z and calculate the length. When I use the same method as before this gets me concentrical circles.

To make it a bit more interesting, I calculate a 2D noise based on the position Y and I use it to offset the X and Z, so the circles change a bit as you move through the post.

9. Tree rings

There’s another form of wood in my scene: trees. In this case, I could have use a similar technique using a SDF of the mesh, but I settled with something way simpler.

The approach I took was creating a series of smaller meshes inside the tree. You can see them here if I disable the shpere normals effect:

The interior meshes of the tree

With this, I set the culling of this mesh to Both and added the following to choose the color:

Choosing the color of the tree

And that’s it. The tree rings appear:

The rings of the tree

10. Conclusion

This was a long article! Thank you for reading all the way here!

This is my first breakdown, but I’m planning on doing this more often, so if you liked it please follow me here or on Twitter to catch the next one!

--

--