Sphere Shader

Make a plane look like a sphere

This is how I can use a shader to make a quad look like a sphere. This is cheaper to render in many cases, and can be indistinguishable from a real sphere.

Three spheres, one is revealed to be a plane, but is indistinguishable from the real spheres
The left side is shaded, and the right side is a wireframe view of the same scene. One of the spheres is actually a plane!

The shader calculates tangent space normals on the plane which make it look similar to a real 3D sphere.

Drawbacks

I like seeing where a technique fails, which can help explain how it works as well, so let’s break it! 😁

While the plane’s normals are correct thanks to the shader, the 3D positions are not. This makes the shape of the received light look weird when the light source is close. Only the sphere on the right is fake.
While the plane’s normals are correct thanks to the shader, the 3D positions are not. This makes the shape of the received light look weird when the light source is close. Only the sphere on the right is fake.
Two spheres intersect with a cube, one of the spheres has a flat intersection with the cube.
It doesn’t handle intersections well. Real sphere on the left, fake sphere on the right.
Two spheres are partially shaded by a straight wall, one looks as expected, on the other sphere the shadow is like a straight line across it despite its curved surface.
It doesn’t receive shadows properly. Real sphere on the left, fake on the right.
A texture has been applied to the plane, which breaks the illusion that it's a sphere.
It also can’t have a regular texture on it because the texture follows the camera when it rotates around the sphere. Solutions include generating UV coordinates, or using a cubemap.

Benefits

So what are the positive aspects of the technique?

Use cases

With real-time graphics, I need to balance several factors against each other to find a good use case. To do that, I’ll list some issues and potential solutions:

A potential solution to all these issues is also to keep the object small or moving, this makes the drawbacks harder to notice. Maybe it’s useful for water droplets in a particle effect, for instance.

Another potential solution is to develop the technique further to make the 3D position of the fragments correct. I have a link to a technique that does this at the end of the post.

It could also work well for distant objects where it covers a small space on the screen, or I can control the lighting conditions and what geometry it intersects with. Maybe it could be used as the last level of detail for some object?

It could work better if the game has certain properties. For instance, if the game doesn’t use point lights, or if the game is 2D, or it doesn’t use shadow maps.

Implementation

This shader is made to work with the default quad in Godot. If I’m using something else, I may need to make some changes. For the effect to work, the quad must always face the camera. I’ll include code for this in the shader as well.

Theory

The amount that the normal faces to the right or up increases linearly with the x and y coordinates. This means I already have my x and y components in the UVs!

That leaves me two things to figure out:

  1. The Z component.
    • This can be calculated from the UV coordinates.
  2. Discarding fragments in a circle.
    • I get what I need for this when calculating the Z component.

Let’s look at an implementation.

Implementation

This implementation is written in Godot’s shading language, which is very similar to GLSL, which is fairly similar to HLSL. The shader is made to be applied to a standard Godot quad, which I get by creating a MeshInstance3D, and setting its Mesh to be a New QuadMesh.

shader_type spatial;

// We use the vertex shader to make the quad face the camera.
// This doesn't have very detailed comments because it isn't the focus.
void vertex() {
	// Prepare our directions.
	vec3 plane_to_cam = normalize(CAMERA_POSITION_WORLD - NODE_POSITION_WORLD);
	const vec3 WORLD_UP = vec3(0, 1, 0);
	vec3 plane_right = normalize(cross(WORLD_UP, plane_to_cam));
	vec3 plane_up = cross(plane_to_cam, plane_right);

	// Build the rotation matrix.
	mat4 billboard = mat4(
		vec4(plane_right, 0.0),
		vec4(plane_up, 0.0),
		vec4(plane_to_cam, 0.0),
		MODEL_MATRIX[3]
	);

	// Apply the matrix.
	MODELVIEW_MATRIX = VIEW_MATRIX * billboard;
}

// This is where the sphere illusion is created.
void fragment() {
	// Bring input UVs into the -1 to 1 range.
	vec2 uv = UV * 2.0 - 1.0;

	// Do the dot product separately so I can branch on the result
	// and reuse it later.
	float uv_dot = dot(uv, uv);
	
	// If the dot product is over 1, it's outside of the circle.
	if(uv_dot > 1.0) {
		discard;
	}
	
	// Calculate the Z value from the dot product.
	float z = sqrt(1.0 - uv_dot);
	
	// Flip UV.y because that's what Godot wants for tangent space normals.
	uv.y *= -1.0;
	
	// Set the normal.
	NORMAL.rgb = vec3(uv, z);
}

If the original plane is entirely outside the view, it will be frustum-culled even though our rotated plane is still inside the view, causing it to pop in and out of existence in some cases. We can solve this by setting GeometryInstance3D -> Geometry -> Custom AABB to these values:

These values will only work for uniformly scaled planes. If I want non-uniform scaling, more work needs to be done, but I’m not covering that in this post.

More spheres

This post showed the basics of using shaders to make a plane look like a sphere. If I want to go deeper, check out these posts: