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



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

14 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.

4 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

Put together a new demo.

5 Likes

does fps shooter games use decals to paint gun fire hole into objects?

That’s right, there are a lot of usecases for decals in games:

  • impacts (bullet holes, explosion marks)
  • footprint
  • blood splatter

Decals are a a very economic way to increase detail in a scene too. Instead of painting detail directly into mesh texture - you can just add it on top with decals.

Let’s say you want to add road markings, with decals that’s super easy and takes close to 0 extra VRAM

Or, adding scuff marks (grunge) onto objects to add some weathering and high-frequency detail. In fact, Doom games have been using decals incredibly aggressively for this, almost 100% of all of the detail you see in those games would be decals, with some individual meshes shipping with 1000 decals attached to them. Not the entire level - just some piece of wall or a boulder.

2 Likes

the most improtant question, when mesh update does the texture made by the decal into the mesh also update generating more draw calls or decals at this time alrerady form part of the texture itself??

Not in this implementation.

These decals generate no additional draw calls.

If you have 1 decal in the scene - the number of draw calls will be the same as if you had 0.

If you have 1000 decals - it will still be 0 draw calls. If you have 100,000 decals - still the same.

The technique is called “clustered decals”, and in meep it’s part of the forward+ rendering pipeline.


It’s important to contrast this with how decals work in three.js. Three.js offers a decal implementation using clipped geometry, which is a completely different technique. A much simpler technique. It cuts a piece of underlying geometry, applies new set of UVs to it, and marks it with vertexOffset, to draw on top of other geometry in the scene. This is why you have to re-generate the decal every time the mesh changes or moves to a new spot where a different set of meshes overlap with the decal bounds.