Yet another decal system - Explained
Updated 2 years ago
An optimized, mesh conforming and configurable approach to a decal system
A while back, while viewing a live session on making a decal system with particles, someone asked in the chat if they were conforming to meshes. Obviously the answer was no, because they are actually just simple quads instantiated in world space.
After googling the subject, I found that Unity has a solution towards this: projectors. While liking the fact that, in order to render a decal, a frustum could be specified (perspective or orthographic), there were other things that don't make them quite a practical tool to use in an optimized way:
  • they render the entire object again in a separate draw call (so, if we had three projectors on one object, it will actually be rendered four times)
  • no culling is performed - the projections are shown both on the 'front' and on the 'back' side of the object
  • there is no main controller to manage the number of projectors being used
  • they have no built-in way to auto orient themselves as to best project on the mesh faces in range
So, I decided to build my own decal system, the principles of which I will try to explain in this article.

Minimum source code is included here, but you may see/get the whole demo from my GitHub repository.
The basic idea is that a main controller will be notified when a shot will collide with the environment, as to leave a mark in its place. It checks which objects are hit (performing bounds and sphere-mesh triangles tests) and calculates a common direction for projecting the mark on them. Each hit object will create a submesh and material for the mark. The materials receive a transformation matrix, which is based on the common direction calculated earlier. Last but not least, the controller makes sure that a specified maximum number of marks are left on the hittable environment. I will try to go in a little bit more detail below.

Hit Data

A simple structure which stores information about a hit. It is passed to the Controller, which in turn passes it to all Hittables for collision checking.
public struct HitData { public Vector3 Position { get; private set; } public Vector3 Direction { get; private set; } public Material Mark { get; private set; } public float MarkSize { get; private set; } public HitData(Vector3 position, Vector3 direction, Material mark, float markSize) { Position = position; Direction = direction; Mark = mark; MarkSize = markSize; } }

The Controller

It has three main responsibilities:
  1. checks to see which objects are hit Firstly, a bounding box is created for the shot mark space. Against it, an intersection test is performed for all Hittables, using their mesh renderer's bounds - it is a cheap and fast initial test. When a bounds intersection is detected, a more thorough test is done between the sphere inscribing the mark's bounds and all the triangles from the Hittable's mesh. A valid hit is considered only if both these tests return true.
  2. determines the direction in which the mark will be projected In order to limit the 'stretchiness' of the mark, a common direction in which it will be projected is computed. It is based on the normals of the intersecting triangles detected. So, after determining the intersecting triangles and culling them (based on a specified max angle between their normal and the hit direction), a per object resultant direction is returned. All these resultants are then combined to form the mark's projection direction.
  3. maintains performance A cache of all current hits is kept in order to remove old ones once a specified limit is reached. This helps in using the system both on low and high end devices.

The Hittable

It represent an object from the environment onto which shot marks may be projected. All it needs is a MeshFilter and a MeshRenderer - mesh colliders are strictly optional.
It contains functionality for:
  • checking its mesh data for intersections with a hit
  • culling intersecting triangles based on a specified max angle between their normal and a hit direction
  • creating sub-meshes and materials when collisions are detected
  • maintaining an internal cache of its hits in order to clear old ones

The Shader

It's a simple shader which receives a float4x4 from the C# script. It represents a Unity TRS Matrix with which the vertex data is shifted into the projector's space. Only the xy components of the new location are used, setting them as the uv for the vertices, in order to sample the mark texture.
And those were the main components of the system.

Further Possible Improvements:

  • refinements to projection direction calculation idea
  • filter all detected triangles after a collision testing that a "view direction" from the collision point to a triangle is not occluded by other detected triangles (with a ray-triangle tests). Culling by angle may not be needed when using this technique.
  • add support to shader for receiving shadows
  • pool shots
  • order hits by managing the renderQueue of the materials

Thank you

For reading this article. Hope you'll find this information useful and remember that a demo is on my GitHub repository if you wish to see it in action.
All the best!
Stefan Velnita
3D Apps/Games Indie/Freelancer - Programmer
3 months ago
Thanks! One of the best solution I've found. Great idea btw
2 years ago
Technical Artist
That’s awesome! Thanks for sharing