reckfellENGINE

2D light and shadows: a complete example and masks

PointLight2D, an occluder and concrete checks for missing light or shadows.

On this page

Result

A light spot illuminates a 2D mesh while a vertical obstacle casts a shadow to its right. Code generates the light texture and geometry. The sample has no physics bodies: neither the visible obstacle nor the light occluder creates collision.

Use Reckfell.NET.Sdk 1.0.0, net10.0 and the Compatibility renderer. Save the three files below, run dotnet build and attach CanvasLightingExample.cs to the Entity2D root of a .rscene level. Run it. For a game window, the sample sets a logical 320×240 Viewport during execution without editing saved project settings.

CanvasMeshes.cs
C#
using System;
using Reckfell;

public static class CanvasMeshes
{
    public static ArrayMesh Quad(Vector2 size)
    {
        if (!size.IsFinite() || size.X <= 0 || size.Y <= 0)
            throw new ArgumentOutOfRangeException(nameof(size));
        Vector2 half = size * 0.5f;
        return Build(
            new[] { new Vector2(-half.X, -half.Y), new Vector2(half.X, -half.Y), half, new Vector2(-half.X, half.Y) },
            new[] { Vector2.Zero, Vector2.Right, Vector2.One, Vector2.Down },
            new[] { 0, 1, 2, 0, 2, 3 });
    }

    public static ArrayMesh Triangle() => Build(
        new[] { new Vector2(-45, -40), new Vector2(50, -17), new Vector2(-20, 45) },
        new[] { Vector2.Zero, Vector2.Right, Vector2.Down },
        new[] { 0, 1, 2 });

    private static ArrayMesh Build(Vector2[] vertices, Vector2[] uv, int[] indices)
    {
        var arrays = new Reckfell.Collections.Array();
        arrays.Resize((int)Mesh.ArrayType.Max);
        arrays[(int)Mesh.ArrayType.Vertex] = vertices;
        arrays[(int)Mesh.ArrayType.TexUV] = uv;
        arrays[(int)Mesh.ArrayType.Index] = indices;
        var mesh = new ArrayMesh();
        mesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, arrays);
        return mesh;
    }
}
CanvasLampTexture.cs
C#
using Reckfell;

public static class CanvasLampTexture
{
    public static ImageTexture Create()
    {
        using var image = Image.CreateEmpty(256, 256, false, Image.Format.Rgba8);
        for (int y = 0; y < 256; y++)
        for (int x = 0; x < 256; x++)
        {
            float radius = new Vector2(x - 127.5f, y - 127.5f).Length() / 128;
            float strength = Mathf.Max(1 - radius, 0);
            image.SetPixel(x, y, new Color(strength, strength, strength, 1));
        }
        return ImageTexture.CreateFromImage(image);
    }
}
CanvasLightingExample.cs
C#
using Reckfell;

public partial class CanvasLightingExample : Entity2D
{
    [Export] public Viewport.Msaa Antialiasing { get; set; } = Viewport.Msaa.Disabled;
    [Export] public bool FollowPointer { get; set; }
    public MeshInstance2D Ground { get; private set; } = null!;
    public MeshInstance2D Block { get; private set; } = null!;
    public PointLight2D Lamp { get; private set; } = null!;
    public LightOccluder2D Occluder { get; private set; } = null!;
    public CanvasModulate Ambient { get; private set; } = null!;

    public override void _Ready()
    {
        if (GetViewport() is Window window)
        {
            window.ContentScaleSize = new Vector2I(320, 240);
            window.ContentScaleMode = Window.ContentScaleModeEnum.Viewport;
        }
        GetViewport().Msaa2D = Antialiasing;
        Ambient = new CanvasModulate { Color = new Color(0.06f, 0.07f, 0.09f) };
        AddChild(Ambient);
        Ground = new MeshInstance2D
        {
            Mesh = CanvasMeshes.Quad(new Vector2(320, 240)),
            Position = new Vector2(160, 120),
            LightMask = 1
        };
        AddChild(Ground);
        Block = new MeshInstance2D
        {
            Mesh = CanvasMeshes.Quad(new Vector2(12, 140)),
            Position = new Vector2(170, 120),
            Modulate = new Color(0.3f, 0.35f, 0.4f),
            ZIndex = 1
        };
        AddChild(Block);
        Occluder = new LightOccluder2D
        {
            Position = Block.Position,
            OccluderLightMask = 1,
            Occluder = new OccluderPolygon2D
            {
                Closed = true,
                CullMode = OccluderPolygon2D.CullModeEnum.Disabled,
                Polygon = new[] { new Vector2(-6, -70), new Vector2(6, -70), new Vector2(6, 70), new Vector2(-6, 70) }
            }
        };
        AddChild(Occluder);
        Lamp = new PointLight2D
        {
            Position = new Vector2(100, 120),
            Texture = CanvasLampTexture.Create(),
            TextureScale = 1.5f,
            Energy = 1,
            RangeItemCullMask = 1,
            ShadowItemCullMask = 1,
            ShadowEnabled = true
        };
        AddChild(Lamp);
    }

    public override void _Process(double delta)
    {
        if (FollowPointer) Lamp.Position = GetGlobalMousePosition();
    }
}

Actual frame: PointLight2D and an occluder shadow

Distinguish each object

ObjectPurpose
Ground and Block, MeshInstance2DVisible surface and obstacle
CanvasModulateDarkens the canvas to expose the light contribution
PointLight2DTextured light with Energy and TextureScale
LightOccluder2DPositions a light occluder
OccluderPolygon2DClosed occluder contour; does not draw the obstacle itself

Enable ShadowEnabled on the light. The sample Polygon is relative to Occluder.Position=(170,120), not world coordinates. It matches the visible 12×140 obstacle. CullMode=Disabled makes it two-sided, avoiding dependence on vertex winding for this example.

CanvasModulate is not a light source. PointLight2D needs a Texture: the radial texture's black edge contributes little while its bright center contributes strongly. Energy cannot repair incompatible masks, an empty contour or an Unshaded material.

Separate light and shadow masks

Every sample mask is 1. These are bitmasks, not layer numbers: 1 enables the first bit, 2 the second and 3 both. Matching means nonzero bit intersection rather than equal integers.

PointLight2D testValues compared
Receiver accepts lightLight RangeItemCullMask and receiver CanvasItem.LightMask
Occluder casts a shadowLight ShadowItemCullMask and occluder OccluderLightMask
Receiver accepts a shadowLight ShadowItemCullMask and receiver CanvasItem.LightMask

Changing only the occluder mask may therefore be insufficient. With RangeItemCullMask=1, ShadowItemCullMask=2 and OccluderLightMask=2, a receiver needs LightMask=3 to accept light through the first bit and shadows through the second. Rendered engine frames confirm this behavior. Do not substitute the occluder entity's inherited LightMask for OccluderLightMask.

Light masks are independent of physics collision layers/masks. ZIndex, light Z/canvas-layer ranges, Visible and Viewport membership require separate checks.

Configure behavior and materials

FollowPointer=true moves the sample lamp to the canvas-space pointer position. This sample option was not tested with physical input in the automated run. Keep it false for a fixed light.

CanvasItemMaterial with LightMode=Unshaded removes the ordinary PointLight2D response. Check this mode and any custom shader if an object does not darken or respond to lighting. For normal maps, see CanvasTexture; shadow contours remain separate. DirectionalLight2D has different coverage and limitations: do not assume all PointLight2D rules apply to it.

Diagnose incorrect results

  • No light: check Enabled, Texture, Energy, material and receiver mask matching.
  • No shadow: check ShadowEnabled, a nonempty OccluderPolygon2D, both shadow mask tests and a contour between the lamp and receiver.
  • Shadow without a visible obstacle: LightOccluder2D draws no image; add a Sprite2D, Polygon2D or MeshInstance2D.
  • Actor passes through the obstacle: add a physics body and Shape; a light contour is not a physics shape.
  • Excessive brightness: check ambient modulation and the combined contribution of several lights before adjusting Energy.

2D geometry · Antialiasing and renderer limits

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.