reckfellENGINE

3D collisions: primitive shapes and compound bodies

A complete falling-crate level with floor, wall and mouse picking; when to use convex shapes or trimeshes.

On this page

Run the 3D example

Add LabBody3D.cs, ScreenPicker3D.cs and CollisionLab3D.cs to a C# project. Create a separate level with an Entity3D root, attach CollisionLab3D, build and run with F6. Leave the root unscaled/unrotated, use 60 physics ticks/s and 3D gravity 9.8 in direction (0, -1, 0). Code creates the camera, lighting, visible meshes and collisions.

The crate falls onto the floor. Space applies one upward impulse; R requests an _IntegrateForces reset. Left-clicking the crate stores a result in Picker.LastHit; this is query data without automatic material highlighting. Ray tests the wall and exposes IsColliding/GetCollisionPoint.

The crate resting on the floor beside the wall in the running 3D example

LabBody3D.cs
C#
using System;
using Reckfell;

public partial class LabBody3D : RigidBody3D
{
    private Vector3? _resetPosition;
    public int ResetCount { get; private set; }
    public void RequestReset(Vector3 worldPosition)
    {
        if (!worldPosition.IsFinite()) throw new ArgumentOutOfRangeException(nameof(worldPosition));
        _resetPosition = worldPosition;
        Sleeping = false;
    }
    public override void _IntegrateForces(PhysicsDirectBodyState3D state)
    {
        if (_resetPosition is not Vector3 position) return;
        state.Transform = new Transform3D(Basis.Identity, position);
        state.LinearVelocity = Vector3.Zero;
        state.AngularVelocity = Vector3.Zero;
        _resetPosition = null;
        ResetCount++;
    }
}
ScreenPicker3D.cs
C#
using Reckfell;
using Hit = Reckfell.Collections.Dictionary;

public partial class ScreenPicker3D : Entity
{
    [Export] public Camera3D? Camera { get; set; }
    [Export] public uint Mask { get; set; } = 4;
    [Export] public float Distance { get; set; } = 100;
    private Vector2? _pending;
    public Hit LastHit { get; private set; } = new();
    public int QueryCount { get; private set; }
    public void RequestPick(Vector2 viewportPosition) { if (viewportPosition.IsFinite()) _pending = viewportPosition; }
    public override void _UnhandledInput(InputEvent @event)
    {
        if (@event is InputEventMouseButton { Pressed: true, ButtonIndex: MouseButton.Left } click)
            RequestPick(click.Position);
    }
    public override void _PhysicsProcess(double delta)
    {
        if (_pending is not Vector2 screen) return;
        _pending = null; LastHit = new();
        if (Camera is null || !ReckfellObject.IsInstanceValid(Camera) || !Camera.IsInsideTree() || !float.IsFinite(Distance) || Distance <= 0) return;
        Vector3 origin = Camera.ProjectRayOrigin(screen);
        Vector3 end = origin + Camera.ProjectRayNormal(screen) * Distance;
        var query = PhysicsRayQueryParameters3D.Create(origin, end, Mask);
        query.CollideWithAreas = false;
        LastHit = Camera.GetWorld3D().DirectSpaceState.IntersectRay(query);
        QueryCount++;
    }
}
CollisionLab3D.cs
C#
using Reckfell;
using Hit = Reckfell.Collections.Dictionary;

public partial class CollisionLab3D : Entity3D
{
    public LabBody3D Crate { get; private set; } = null!;
    public StaticBody3D Floor { get; private set; } = null!;
    public StaticBody3D Wall { get; private set; } = null!;
    public RayCast3D Ray { get; private set; } = null!;
    public Camera3D Camera { get; private set; } = null!;
    public ScreenPicker3D Picker { get; private set; } = null!;
    public int Contacts { get; private set; }
    public override void _Ready()
    {
        Floor = StaticBox("Floor", new Vector3(0, -.25f, 0), new Vector3(12, .5f, 8));
        Wall = StaticBox("Wall", new Vector3(3, 1.5f, 0), new Vector3(.5f, 3, 4));
        Crate = new LabBody3D { Name = "Crate", Position = new Vector3(0, 3, 0), Mass = 2,
            LockRotation = true, CollisionLayer = 4, CollisionMask = 1, ContactMonitor = true, MaxContactsReported = 8,
            PhysicsMaterialOverride = new PhysicsMaterial { Bounce = 0, Friction = 1 } };
        Crate.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = Vector3.One } });
        AddVisual(Crate, Vector3.One, new Color(.25f, .45f, 1));
        Crate.BodyEntered += body => { if (body == Floor) Contacts++; };
        AddChild(Crate);
        Ray = new RayCast3D { Position = new Vector3(-4, 1.5f, 0), TargetPosition = new Vector3(8, 0, 0),
            CollisionMask = 1, CollideWithAreas = false, Enabled = true }; AddChild(Ray);
        Camera = new Camera3D { Position = new Vector3(8, 7, 10), Current = true }; AddChild(Camera);
        Camera.LookAt(ToGlobal(new Vector3(0, 1, 0)));
        Picker = new ScreenPicker3D { Camera = Camera }; AddChild(Picker);
        var light = new DirectionalLight3D { RotationDegrees = new Vector3(-55, -30, 0), LightEnergy = 1.3f }; AddChild(light);
        var environment = new Reckfell.Environment { BackgroundMode = Reckfell.Environment.BGMode.Color,
            BackgroundColor = new Color(.035f, .035f, .045f), AmbientLightSource = Reckfell.Environment.AmbientSource.Color,
            AmbientLightColor = Colors.White, AmbientLightEnergy = .55f };
        AddChild(new WorldEnvironment { Environment = environment });
    }
    private StaticBody3D StaticBox(string name, Vector3 position, Vector3 size)
    {
        var body = new StaticBody3D { Name = name, Position = position, CollisionLayer = 1, CollisionMask = 0 };
        body.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = size } });
        AddVisual(body, size, new Color(.3f, .3f, .34f)); AddChild(body); return body;
    }
    private static void AddVisual(Entity3D parent, Vector3 size, Color color)
    {
        parent.AddChild(new MeshInstance3D { Mesh = new BoxMesh { Size = size }, MaterialOverride =
            new StandardMaterial3D { AlbedoColor = color, Roughness = .85f } });
    }
    public Hit Cast(Vector3 worldFrom, Vector3 worldTo, uint mask = 1, Rid? exclude = null, bool inside = false)
    {
        var query = PhysicsRayQueryParameters3D.Create(worldFrom, worldTo, mask);
        query.HitFromInside = inside;
        if (exclude is Rid rid) query.Exclude = new Reckfell.Collections.Array<Rid> { rid };
        return GetWorld3D().DirectSpaceState.IntersectRay(query);
    }
    public override void _UnhandledKeyInput(InputEvent @event)
    {
        if (@event is not InputEventKey { Pressed: true, Echo: false } key) return;
        if (key.PhysicalKeycode == Key.R) Crate.RequestReset(ToGlobal(new Vector3(0, 3, 0)));
        if (key.PhysicalKeycode == Key.Space) Crate.ApplyCentralImpulse(Vector3.Up * 10);
    }
}

Hierarchy and dimensions

CollisionShape3D must be a direct child of a body or Area3D. A neighboring MeshInstance3D supplies only the visible image. The floor has BoxShape3D.Size = (12, 0.5, 8), the crate (1, 1, 1). Size means full dimensions, not half-extents. Child-shape position offsets it from the body.

For a compound object, add several CollisionShape3D children to one body. This represents concave objects through simple/convex pieces without a dynamic trimesh. Layer and Mask apply to the whole body. To identify a hit part, use the query shape index and shape-owner mapping; do not assume the index equals GetChild order.

Choose a shape

GeometrySuitable use
BoxShape3D, SphereShape3DSimple crates, walls and round objects
CapsuleShape3DA character; radius and total height, avoiding sharp bottom corners
CylinderShape3DCylindrical objects; separately check contact stability with the selected backend
ConvexPolygonShape3DA convex hull; it fills cutouts and holes
Several convex shapes/primitivesA moving object with a complex shape
ConcavePolygonShape3D / trimeshStationary triangular surroundings, not a dynamic body's filled volume
HeightMapShape3DStatic terrain within heightmap representation limits
WorldBoundaryShape3DAn infinite static boundary

A concave/trimesh shape is a triangle surface; an object fully inside a closed mesh is not necessarily detected as overlapping a filled volume. Do not assign it to an ordinary dynamic RigidBody3D or CharacterBody3D. Generating convex geometry changes the model's shape; inspect holes, stairs and generated piece count. Automatic creation does not guarantee suitable gameplay geometry.

Scale, resources and cost

Retain unit scale in the physics branch and set dimensions on resources. The engine specifically warns about nonuniform CollisionShape3D scale. Parent scale is inherited too. Separate visual children from physics geometry.

Shape3D is shared. When instances use the same BoxShape3D, editing Size affects all of them; Duplicate isolates a shape. Disable CollisionShape3D through SetDeferred from a physics signal. Disabled removes the shape from queries/contacts after application; hiding a MeshInstance does not.

Primitives usually produce simple contacts, but results depend on bodies, shapes, intersections and backend. Too many convex pieces are expensive too. Do not turn every static mesh into a separate dynamic body. Profile a representative level: a single-crate example does not prove a large-world budget or tall-stack stability.

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.