This is a very naive implementation of deferred decals. Very straightforward and absolutely unoptimized. Here is an example of decals.
Execute following CMake command to build the project:
cmake -S . -B build
cmake --build build
AFAIK there is no external dependencies. All of the dependencies are stored inside thirdparty
directory and are statically linked to the executable.
We use deferred rendering to implement decals. The key figure in deferred rendering is GBuffer. Our GBuffer contains several buffers:
- World Position
- World Normal (normal that was sampled from normal map and reconstructed using TBN matrix)
- Albedo
Whole render loop consists of following logical render passes:
- Geometry Pass
- Deferred Decals
- Deferred Shading Pass
- Copy GBuffer Depth Pass
- Wireframe Pass
In this pass we draw all the geometry in the scene and fill GBuffer. We use custom framebuffer with N color attachments and one depth stencil attachment.
In this pass we draw decals. Decals can write in any buffer of GBuffer. In our implementation this pass writes only to Albedo and Normal buffers.
In this pass we evaluate shading equation for all lights in the scene per pixel.
In this pass we copy GBuffer's depth stencil texture to backbuffer's depth stencil texture. This is done to draw something with forward shading.
This is an auxiliary pass that draws bounding box of the decal volume.
To draw decal we need to draw suplimentary geometry - a box. One of the normals of the faces will be used as direction of decal projection. We use -Y axis as direction of projection.
It is very important to orient box in such a way so that -Y will face towards the mesh that you want to be covered with decal. See screenshot below. Red arrows are -Y in local space of the box.
High level algorithm looks as follows:
- We draw box
- In vertex shader we pass
gl_Position
to fragment shader and transform vertices of the box using MVP matrix. - In fragment shader we get NDC coordinates
screenPos
by dividingClipPos.xy
byClipPos.w
. - Then we convert those NDC coorditanes to
uv
coordinates by scaling so that all values lie in [0, 1], i.e. multiply by 0.5 and add 0.5 - We obtain depth (the Z coordinate) from
g_depth
texture using thoseuv
coordinates. - Then we reconstruct world position
worldPos
by callingWorldPosFromDepth
function. - After that we transform
worldPos
to local space of bounding box (localPos
) by multiplyingworldPos
byg_decalInvWorld
matrix. - Then we transform
localPos.xz
from [-1,1] interval to [0,1] interval to use it asdecalUV
UV coordinates. - After that we discard all fragments that lie outside of bounding box
- For each fragment that lies inside bounding box we retrieve albedo and normal using
decalUV
. - Then we write albedo and normal to GBuffer
Because we already have something in GBuffer's depth buffer we have to set a proper render
state to be able to draw boxs. We set depth access read only and change depth function
to GL_GREATER
.
We copy GBuffer's depth to a texture to feed it to fragment shader in Deferred Decal Pass.
As of uniforms the really special are:
g_decalInvWorld
that contains inverse matrix that transforms a point to local space of the boxg_depth
that contains copy of GBuffer's depthg_rtSize
that contains in x and y components width and height of framebuffer and inverse of width and height of framebuffer in z and w components