vidar.games

Mesh-projected decals

2026-11-10 · Discuss on X

A flat decal quad floats off a round pillar, sinks into steps and flickers on the floor. driven-decals cuts a new mesh from the surfaces inside the decal box instead, so the paint follows every step.

The problem

The cheapest decal is a textured quad at a hit point. On anything that isn't flat it breaks: it hangs in the air beside a curved pillar, the next step up buries it, and on flat ground it sits at the same depth as the floor, so the two fight pixel by pixel (z-fighting).

How it works

driven-decals (a Unity package) treats a decal as a box. When you place it:

  1. Pick the static meshes whose bounding sphere touches the box.
  2. Move every triangle into box space, where the box is a unit cube.
  3. Skip triangles facing away from the projector.
  4. For each triangle, keep its corners inside the cube, the points where the cube's 12 edges pierce it, and the points where its edges cross the cube's 6 faces.
  5. Sort those points by angle around their centre and fan them into new triangles, at most 5 per input.
  6. UV = each point's x and y in box space.

The shader then hides the leftovers. Fade by angle: surfaces at 83° to 85° from the projector fade out, since past that the texture smears. Fade near z bounds: soften where a surface leaves the front or back of the box. Vertices also move 0.1% of the way toward the camera, so the decal wins the depth test against the surface it copies.

What I hit

Unity is left-handed with clockwise front faces; three.js is the opposite. I kept the source's maths in box space and fixed the edges: the backface test uses a normal oriented by the mesh's own vertex normals, and new triangles are flipped to face the projector.

Faces exactly side-on to the projector are skipped as backfaces, so to show the angle fade I tilted the projector 3.4°. Without the fade, the step risers catch streaks.

The default z fade covers 5% of the box depth at each end. On a 0.42 m box that's 2 cm, invisible in a wide shot, so the last shot uses ZFadeDistance 0.5 instead of 0.1.

My build

three.js 0.170, WebGL2. Five decals: a crack across a wall corner, hazard stripes round a pillar, spray paint over steps, a floor marking, ramp arrows. Generation runs once at load, in JavaScript. For the box over the steps: 3,560 triangles tested from 7 meshes, 2,616 face the projector, 128 reach into the box, 182 triangles come out.

Trade-offs

When to use it

Decals placed in level design: cracks, graffiti, signs, stains. For bullet holes or anything on animated characters, use a screen-space or deferred decal system.

New breakdowns go out on X first: @vidar_0208. If this one helped, you can buy me a coffee.

Source and credits

Technique source: driven-decals by Anatta336, https://github.com/Anatta336/driven-decals, MIT license (Copyright 2019-2020 Sam Driver), commit 4a70ad783bd5c95d9d43ed9c90654251766db17c. Ported to three.js and GLSL from its MeshProjection class and the Triangle, Edge, Square and Vertex helpers (mesh selection by bounding sphere, move into decal box space, backface skip, the three kinds of kept vertices, angle sort and fan, at most 5 triangles per input triangle, UV from box x and y), the DecalMesh simple quad as the reference for the flat quad, and the Fade by angle, Fade near z bounds and Offset in viewspace shader subgraphs with their defaults (83 to 85 degrees, ZFadeDistance 0.1, offset 0.001). The limits stated in the post (slow generation, not for bullet holes, does not follow skinned or deforming meshes) are from the source README. No files were copied unchanged.

Render framework: three.js by mrdoob and contributors, https://github.com/mrdoob/three.js, MIT license, npm package version 0.170.0. Used for rendering, shadows and raycasting. It is not the source of the decal technique.

My own code: the concrete yard scene and its procedural concrete textures, the five procedural decal textures (crack, hazard stripes, spray paint, floor marking, ramp arrows), the flat quad baseline placed at the hit point, the right-handed winding fixes, the debug wireframes and triangle counts, the camera moves, labels and video. No external models, textures or HDRI. No IA model.