Rendering symmetry

23 Feb 2021

Symmetry

In real time rendering, the most common application for symmetry is probably reflection rendering. In this context, a mirror that produces the reflection is the symmetry plane, and a symmetrical version of all the things that are in front of the mirror are then also rendered behind the mirror.

In reality, mirrors don't really intersect objects, but there is no reason they can't do that when rendering a digital scene. In fact, multiple mirrors can exist that can also intersect each other. In this article, a shader algorithm is explained that can apply symmetry to a mesh using any number of symmetry planes (or mirrors), that can intersect both the geometry and other symmetry planes. This can result in very interesting and exotic geometries that are unrecognizable from the original geometry that has been used as source material.

The source code for the algorithm explained in this article can be found on GitHub under the MIT license.

Mirroring right to left
Figure 1: A cube on the right is mirrored horizontally.
Mirroring top to bottom
Figure 2: The horizontally mirrored scene from Figure 1 is mirrored for a second time, now vertically.

A simple case

Figure 1 shows a simple case of symmetry. The blue cube is mirrored along the vertical axis in the image, towards the left. A mirrored version of the cube appears on the left.

Thus, to render a cube with one symmetry plane, the cube needs to be rendered twice: once without transformation (unreflected), and once reflected through the plane.

Repeated application

Figure 2 shows another application of a symmetry plane; additionally to the horizontal mirroring, the cube is now also mirrored along the horizontal axis, creating four (partial) cubes.

Without symmetry, one cube is rendered. With one symmetry plane, two cubes are rendered, and with two planes, four cubes are rendered. The number of times the original primitive is rendered is equal to $2^p$ where $p$ is the number of symmetry planes. In other words, for every added symmetry plane, the number of times the original mesh needs to be rendered doubles: every plane requires everything to be rendered normally as well as mirrored.

Rendering real time symmetry with a shader

The algorithm that mirrors a scene through one or more symmetry planes can be defined as follows, starting at the first symmetry plane:

  1. Render the primitive normally, but cull everything behind the symmetry plane.
  2. Render the primitive mirrored behind the symmetry plane, and cull everything in front of the plane.
  3. If there are more symmetry planes, repeat the algorithm for the next symmetry plane with the current state of the scene.

For every iteration of the algorithm, the number of times the primitive is rendered doubles, since the effect stacks; every symmetry plane needs to take all previous planes into account.

The interactive WebGL 2 renderer below shows the algorithm in action. The triangle at the right top can be used to toggle the controls. One to ten symmetry planes can be created to intersect a primitive, and the planes can be moved and rotated to create different transformations.

A full screen version of this application can be opened here.

The shader runs $2^p$ times, where each run requires information on which planes the primitive should be mirrored through. For one plane, the algorithm runs twice where it is once mirrored and once not. For two planes, it contains both situations required for one plane twice; once with mirroring through the second plane and once without mirroring. For every subsequent plane, the number of runs doubles.

The shader mirrors a vertex $\vec{v}$ through a plane defined by a point $\vec{o}$ and a normal vector $\vec{n}$ by$$\vec{m}=\vec{v}-2\vec{n}(\vec{n}\cdot(\vec{v}-\vec{o})),$$where $\vec{m}$ is the mirrored vertex. There is one issue with mirroring vertices: the winding order of the triangles reverses. This means that face culling no longer works well, back facing triangles will be visible instead of front facing triangles. When mirrored geometry is mirrored, the winding order reverses again. In the example above, face culling is disabled to circumvent this problem.

If the primitive has surface normals, they need to be reflected as well. This is done by vector reflection: surface normals are simply reflected through the surface normal of the current symmetry plane.

Intersections and exponential complexity

Since the number of times the primitive needs to be drawn is $2^p$, the performance of the algorithm steeply decreases for large number of symmetry planes. For larger number of planes, it will be more performant to split and mirror the mesh whenever the configuration of planes changes, even in real time. The new mesh can then be rendered once.

In some cases, it is not required to draw $2^p$ variants of the geometry. Certain configurations of planes can for example rule out iterations: consider three symmetry planes placed in the shape of a six pointed star. The three planes will create $6$ spaces instead of $2^3=8$. In other configurations, entire spaces may become invisible when placed behind a new symmetry plane.

Conclusion

The proposed method is a viable way of applying one or more symmetry planes to geometry in real time. Since all work will be done by the shader program, the primitive or model does not need to be modified.

For higher number of symmetry operations, other methods should be used since the number of times the original primitive needs to be drawn is $2^p$ in the worst case.

Some things need to be taken into account when using this algorithm:

  • Mirrored parts of the geometry (or parts of the geometry that have been mirrored an uneven number of times) will have their triangle winding order reversed, so face culling will no longer work on those parts of the model. This can be overcome by separating the draw calls in two groups, where one group has its winding order reversed. The reversed group can dan be drawn with a different winding order configuration.
  • Shaders often don't accept loops with a variable number of iterations, and the symmetry shader loops over every plane to apply mirroring transformations. This implementation compiles a shader program for every allowed number of planes to solve this issue: every different program is meant for a fixed number of symmetry planes.
  • $2^p$ is the worst case number of required rendering iterations; a number of optimizations can be applied to determine how many iterations are really required, and how many iterations would not draw visible triangles and can be omitted.

The source code for the WebGL 2 shader using this algorithm can be found on GitHub.