reckfellENGINE

Decals: markings and surface details

Project markings, configure rendering layers, change ORM without recoloring and diagnose mask and emission issues.

On this page

When to use a decal

A decal projects a texture onto existing surfaces: markings, tracks, damage or a local material change. It does not add geometry, cut a hole or change collision. Silhouette relief needs a mesh; a physical pit needs separate collision geometry.

The complete example below is verified in Forward+/Vulkan on the current 1.0.0 build. Materials, lighting, shadows and antialiasing are already configured.

Run the floor marking

Download the decal gallery. Extract the project, open project.reckfell in Reckfell with the C# SDK, build C# and run Main.rscene. No external textures or models are needed: DecalGallery3D.cs creates the marking and CinematicGallery3D.cs creates the gallery.

On the root, Preview=Albedo shows colored markings; OrmOnly changes roughness and metallic through the same mask. MarkingEnabled=false disables the decal at startup. These exported values are read in _Ready; from running code, change Marking.Visible or call ShowOrmOnly() after _Ready.

Same gallery without the marking projection

Marking projected onto the original floor materials

The example assigns Layers=2 to floor meshes and CullMask=2 to the decal. This is a rendering-layer bitmask, not physics: value 2 selects the second layer. For layer n, use 1u << (n - 1). The camera must also see the receiver layer.

DecalGallery3D.cs
C#
using System.Collections.Generic;
using Reckfell;

public partial class DecalGallery3D : CinematicGallery3D
{
    public enum SurfacePreview { Albedo, OrmOnly }
    [Export] public SurfacePreview Preview { get; set; } = SurfacePreview.Albedo;
    [Export] public bool MarkingEnabled { get; set; } = true;
    public Decal Marking { get; private set; } = null!;
    private readonly List<MeshInstance3D> floorTiles = new();

    public override void _Ready()
    {
        base._Ready();
        foreach (Entity child in GetChildren())
            if (child is MeshInstance3D tile && tile.Mesh is BoxMesh box &&
                Mathf.IsEqualApprox(tile.Position.Y, -0.055f) && Mathf.IsEqualApprox(box.Size.Y, 0.12f))
                floorTiles.Add(tile);
        SetFloorLayers(2);
        Marking = new Decal
        {
            Position = new Vector3(0, 0.12f, -1.5f), Size = new Vector3(4.8f, 0.7f, 4.8f),
            TextureAlbedo = CreateMarking(), CullMask = 2, AlbedoMix = 1,
            Modulate = Colors.White, UpperFade = 0, LowerFade = 0,
            NormalFade = 0.5f, DistanceFadeEnabled = false, Visible = MarkingEnabled
        };
        AddChild(Marking);
        if (Preview == SurfacePreview.OrmOnly) ShowOrmOnly();
    }

    public void ShowOrmOnly()
    {
        using var image = Image.CreateEmpty(16, 16, false, Image.Format.Rgba8);
        image.Fill(new Color(1, 0, 1)); // R=AO, G=roughness, B=metallic.
        Marking.TextureOrm = ImageTexture.CreateFromImage(image);
        Marking.AlbedoMix = 0; // Keep TextureAlbedo for its alpha mask.
    }

    public void SetFloorLayers(uint mask)
    {
        foreach (var tile in floorTiles) tile.Layers = mask;
    }

    // Alpha is the projection mask. Transparent pixels retain the floor material.
    private static ImageTexture CreateMarking()
    {
        const int size = 1024;
        using var image = Image.CreateEmpty(size, size, false, Image.Format.Rgba8);
        for (int y = 0; y < size; y++) for (int x = 0; x < size; x++)
        {
            float u = (x + 0.5f) / size * 2 - 1, v = (y + 0.5f) / size * 2 - 1;
            float a = Mathf.Abs(u), b = Mathf.Abs(v);
            float edge = Mathf.Max(Mathf.Max(a, b), (a + b) * 0.70710678f);
            float rim = 1 - Mathf.SmoothStep(0.006f, 0.01f, Mathf.Abs(edge - 0.85f));
            float band = Mathf.SmoothStep(0.74f, 0.748f, edge) * (1 - Mathf.SmoothStep(0.80f, 0.808f, edge));
            float stripe = Mathf.PosMod(u + v, 0.14f) < 0.075f ? 1 : 0.12f;
            float bracket = a > 0.34f && a < 0.43f && b > 0.27f && b < 0.43f ? 1 : 0;
            bracket = Mathf.Max(bracket, b > 0.34f && b < 0.43f && a > 0.27f && a < 0.43f ? 1 : 0);
            float arrow = 0;
            for (int i = 0; i < 3; i++)
                if (Mathf.Abs(v + 0.15f + i * 0.18f - a * 0.7f) < 0.035f && a < 0.2f) arrow = 1;
            float alpha = Mathf.Max(rim, Mathf.Max(band * stripe, Mathf.Max(bracket, arrow)));
            Color color = band > 0.1f ? new Color(0.2f, 0.52f, 0.82f, alpha) : new Color(0.82f, 0.89f, 0.95f, alpha);
            image.SetPixel(x, y, color);
        }
        image.GenerateMipmaps();
        return ImageTexture.CreateFromImage(image);
    }
}

Position the projection

Size=(4.8, 0.7, 4.8) contains full projection-volume dimensions along local axes. Texture coverage uses X/Z and depth uses Y. At the initial rotation the example paints the floor; for a wall, rotate the Decal so its volume intersects the wall and its projection direction suits the surface.

Position=(0, 0.12, -1.5) places the floor inside the volume. Moving the decal upward beyond intersection removes the marking. Raising brightness cannot fix that: check position, orientation and Size first. Excessive depth can reach unwanted geometry.

UpperFade and LowerFade soften contribution toward opposite local-Y boundaries; both are 0 for uniform markings in the example. NormalFade attenuates projection on unsuitable surface orientations rather than controlling normal-map strength. Size clamps every axis to at least 0.001: zero size is not an off switch.

Change material without recoloring

AlbedoMix=0 retains the floor color while ORM changes properties in the marking shape

ChannelChangesMask
TextureAlbedobase colorthis texture’s alpha and Modulate.A
TextureNormallighting normalTextureAlbedo alpha and Modulate.A
TextureOrmR: AO, G: roughness, B: metallicTextureAlbedo alpha and Modulate.A
TextureEmissionadditive emission from RGBits own RGB, not albedo alpha

For an ORM-only effect, retain TextureAlbedo and set AlbedoMix=0. ShowOrmOnly() assigns ORM=(1, 0, 1): unattenuated AO, zero roughness and full metallic inside the mask. This is a strong teaching contrast; choose less extreme values for the final material.

Do not remove TextureAlbedo instead of setting AlbedoMix=0: the verified configuration loses its ORM mask too. Normal also uses the albedo mask; this example does not verify imported-map orientation or authoring-tool conventions.

Transparent TextureAlbedo regions retain the original material. For imported files, preserve alpha and check filtering and mipmaps; the example generator creates mipmaps before ImageTexture.CreateFromImage. Texture2D subtypes are not interchangeable in a decal atlas: do not treat ViewportTexture or AtlasTexture as a regular static image.

Emission and disabling

Emission is independent. Native checks confirm visibility without TextureAlbedo and with Modulate.A=0. Use black RGB for non-emitting parts of the emission texture; transparency alone is insufficient. EmissionEnergy=0 removes emission rather than the other channels.

To disable the entire instance, set Marking.Visible=false. Glow can add a halo to bright emission, but decal emission alone does not create a Light3D for neighboring meshes.

Fade distant decals

DistanceFadeEnabled enables fading by camera-to-Decal-position distance. Contribution is retained before DistanceFadeBegin, fades over the following DistanceFadeLength and is excluded beyond Begin+Length. Use a nonnegative begin distance and a positive transition length.

Verification compares Begin=0, Length=0.1 with the decal far from the camera against Begin=100, Length=1 with it before the fade start. These are not universal game defaults. Distance fade does not remove the Entity, stop its scripts or stream the world.

Diagnose an incorrect result

SymptomCheck
Marking did not appearVisible, TextureAlbedo, volume/surface intersection, CullMask and Layers, receiver visibility to the camera
Unwanted neighboring objects are affectedReduce Size.Y or separate rendering layers
ORM disappeared after removing albedoRestore the albedo mask and use AlbedoMix=0
Alpha=0 but emission remainsEmission is independent of Modulate.A; disable Visible or EmissionEnergy
Decal disappears at a distanceDistanceFadeEnabled, Begin and Length
Changing the mask in Compatibility causes a shader errorThis pass was not accepted for the complete scene: ShaderGLES3: Parameter "spec" is null was observed. Use Forward+ to reproduce this example

Frames use the current local runtime: 2560×1440, internal scale 1.5, MSAA 8× and TAA. Camera, lighting and exposure remain fixed; only the illustrated decal state changes. Native checks confirm the stated albedo, ORM, emission, mask and distance operations. They do not establish renderer parity, transparent-receiver behavior, many overlapping decals or performance on other devices.

Decal API · 3D materials · Lighting and shadows

Reckfell Engine documentationBack to top ↑
QUICK LINKSGetting startedEntity hierarchyC# & .NET 10Scenes and prefabs
Reckfell guides and full API K
reckfellENGINE
RECKFELL ACCOUNT

Sign in

Sign in to your Reckfell account.

One account for the entire Reckfell platform.