How to draw stuff on stuff

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.

Demo link for the Impatient

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.

Examples


image

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:

References

Drawing Stuff On Other Stuff With Deferred Screenspace Decals
Decals (deferred rendering)
Screen Space Decals in Warhammer 40,000: Space Marine

11 Likes

:melting_face:

this is super interesting! that reminds me of something i saw on twitter recently, it was not published https://twitter.com/CantBeFaraz/status/1612032035350102019 is is a similar technique?

and will any of your clustered rendering strategies be made open source or available some day?

1 Like

Hey @drcmda , looking at the claims Faraz is making - it looks to be similar at least. Doing decals in a shader makes sense in many ways, the problem is - you don’t want to run decal-related computations for every decal in the scene for every pixel on the screen. That’s where you need some kind of a culling technique.

At the very least - you have to cull by camera frustum. But that’s generally not enough, if you have just 20-50 decals in the viewport - you’ll already start noticing performance overhead. That’s where tiled/cluster rendering comes it, it lets you cull further and process much fewer decals per pixel on average.

It’s a very similar concept to number of lights in traditional forward rendering, you can have a few, but shading many lights for each viewport pixel regardless whether it would actually affect the final color or not is quite expensive. Even if you do culling per light for each pixel in the shader - it’s still too expensive, mainly because of all of the branches you have to have as well as memory access when iterating over your list of lights.

and will any of your clustered rendering strategies be made open source or available some day?

I don’t think so. I’m not very good at growing an OS community, and without that - I don’t see the point, except for leaving it as a reference for people who will commercialize the code eventually. Perhaps I’m wrong, but that’s my view on it. I do hope that the compiled list of literature and my musings on the subject will help someone out though.

3 Likes

fair enough. thanks for the explanation!

ps if you ever do feel like it, we can help you with management and preparation and such things. there are multiple devs on pmndrs that just love math and combing through published studies, exploring things in elaborate playgrounds. we help them to get a footing in oss.

1 Like

Just recently i added decals too, also non-geometry box projected with instancing (with culling) and atlasing. They work with a G-Buffer but can be rendered forward or deferred.

One advantage i feel is necessary is having the underlaying texture geometry (normals) being available to blend with, it’s also fading by slope so it gets a lot more variation on rough surfaces.

I see geometry decals as problematic as you, they natrually require a draw call by being individual geometry and computation cost to create them can be heavy, while true projection cost is uniform and realtime.

Dynamic are also suited well for footprints, while always staying one draw call spawning at foot/surface collision without further bothering about the surface, especially on edges where only half foot is on, it won’t just project the decal down the 90° wall.

Here are some tests

Snow splat

image

Metallic liquid splat

image
:hocho:
image

Obligatory emissive pentagram

image

3 Likes

@Fyrestar Looks super-cool

One advantage i feel is necessary is having the underlaying texture geometry (normals) being available to blend with, it’s also fading by slope so it gets a lot more variation on rough surfaces

I agree with that. Doing decals purely in forward pass is hard, because you’re limited by the underlying surface shader. It’s less of an issue if you mainly use StandardMaterial though, as you have all of the relevant pieces in place, such as normal and emissive components that you mentioned.

I do think that deferred decals are pretty much the best, although there are some extra problems to solve there too.

1 Like