Hello [insert name here]
, as you are my favorite member of three.js community, I thought I’d share my take on implementation of Decals.
First of all, what are decals? Decals are texture projections onto other stuff in your scene. In video game terms - think bullet holes, graffiti, footprints.
In three.js, we have a way to do this since quite a long time ago, using a technique called “geometry decals”.
How does this work? When you create a decal, under the hood it chops a cube-shaped chunk of underlying geometry, creates a new mesh, sets UVs based on that cube and adds the new mesh to the scene. Here’s a very good explainer on how that works by SimonDev.
This works very well in general and it’s an awesome piece of tech. It fits extremely well with existing materials and shaders, and works in forward-rendering, which three.js employs. However, there a few disadvantages:
- If you geometry changes in any way - you have to re-create decal.
- Generating the decal geometry can be very slow if your source geometry is large. Although this can be sped up somewhat using some spatial index.
- Decal takes spare proportional to the geometry chunk’s complexity that you have to carve out.
- Doesn’t work when there are multiple geometries intersecting.
- If you want to move the decal relative to your source geometry - you have to rebuild it completely.
- Each decal is a separate draw call
The main alternatives to this technique are:
- Deferred decals
- Forward+ decals
Both of those techniques are actually quite similar, although the implementation differs a lot, they feel very similar in usage and they have very similar performance.
A while ago, I implemented a Forward+ clustered rendering solution on top of three.js. After reading a really cool SIGGRAPH paper by id tech guys about doom 2016, my eye was caught by their use of the clustered renderer to implement decals. So I figured I’d give it a try.
What’s so good about forward+ decal tech though?
- Practically 0 CPU cost per decal, meaning you’re not bound by draw calls
- Decals can move and will project onto anything in the scene, not limited to 1 geometry at a time
- Very good GPU performance, thanks to low bandwidth requirements and little to no overdraw
The difference is between having to consider how many decals you have in the scene, and pre-baking them to avoid the construction cost of each decal to throwing 100s or even thousands of them into the scene at runtime.
Want to add 100 decals just for this 1 frame? - not a problem
Want to draw a rude shape on the wall using decals? - go for it!
Here’s a demo I put together, that spawns 100,000 decals in the scene, each using one of 181 unique textures. Admittedly it’s pretty boring, as the scene consists of a large flat plane, but you can judge the performance for yourself.
Some of the features of the solution:
- Automatic texture atlasing. All decals are placed into a single atlas, that is automatically resized as needed.
- Decals are added and removed from the atlas based on visibility queries with caching. Meaning that if you use 1,000 4k textures for your decals, but they are never seen by the user at the same time - the system just works.
- Fallback for when atlas gets full. All textures are scaled down, so the system still continues to work.
- Fully dynamic, every decal can change texture, bounds, position, rotation, scale.
- Soft angular blending. Decals will softly blend to transparency when projection angle gets too steep.
- Implemented raycating on decals, mainly for editor functionality
- Worker-based image decoding, so decals don’t stress the main thread when textures are being loaded.
If you’re curious about the implementation beyond the references section, check out my original clustered rendering article for more info as that’s the hardest part of the entire technique by far.
I read a ton of literature on the subject, but sadly there’s little I remember now as this project was actually done some months ago and I didn’t keep notes. But here are a few references I found very helpful, in no particular order: