Recipes

Easy3D / Examples / Recipes

Practical patterns built from the current Easy3D API. One thing to keep in mind throughout: Easy3D does not render yet — the batches queue data and CubeMesh builds CPU-side geometry. Drawing is your game's job via CNA until the Phase 4 renderer adapters exist (see the Roadmap).

Recipe 1 — Animated sprite billboard from a spritesheet

The Galaxy Eggbert plan in miniature: existing 2D animation frames, registered as an atlas grid, selected per frame, queued as a camera-facing quad pivoted at the feet.

// Setup (once): the spritesheet is a 256x256 texture, 8 walk frames of 32x48
// in one row. Load the texture itself with CNA; the atlas only maps regions.
Easy3D::TextureAtlas atlas(256, 256);
atlas.AddGrid("walk", 32, 48, 8, 1);   // registers walk_0 .. walk_7

Easy3D::BillboardBatch sprites;

// Per frame:
sprites.Begin();

const int  fps   = 10;
const int  frame = static_cast<int>(totalSeconds * fps) % 8;
const auto uv    = atlas.GetUvOrDefault("walk_" + std::to_string(frame));

Easy3D::BillboardItem item;
item.Position = character.Position;
item.Size     = {1.0f, 1.5f};       // world-unit size, matching the 32x48 aspect
item.Uv       = uv;
item.Origin   = {0.5f, 1.0f};       // pivot at the feet: Position is on the ground
sprites.Add(item);

sprites.End();
// sprites.Items() now holds everything a billboard renderer needs.
GetUvOrDefault() keeps a missing frame from throwing mid-game; a frame with UV {0,0,0,0} renders as a degenerate (invisible) quad, which is easy to spot and harmless. Use strict GetUv() in loading code where you'd rather fail fast.

Recipe 2 — Cube terrain as one mesh

Queue one cube per solid map cell, then bake the whole batch into a single vertex/index array pair. Rebuild only when the level changes, not per frame.

using Vector3 = Easy3D::CubeBatch::Vector3;

Easy3D::TextureAtlas tiles(128, 128);
tiles.AddGrid("tile", 32, 32, 4, 4);   // tile_0 .. tile_15

Easy3D::CubeBatch terrain;
terrain.Begin();
for (int z = 0; z < depth; ++z)
    for (int x = 0; x < width; ++x)
        if (const int t = level.TileAt(x, z); t >= 0)   // tile semantics live in the game
            terrain.Add(Vector3(x + 0.5f, 0.5f, z + 0.5f),
                        Vector3(1, 1, 1),
                        tiles.GetUv("tile_" + std::to_string(t)));
terrain.End();

// Bake once (on load / level change), keep the result:
std::vector<Easy3D::CubeVertex> vertices;
std::vector<std::uint32_t>      indices;
Easy3D::BuildCubeMesh(terrain, vertices, indices);

// Upload vertices/indices to CNA vertex/index buffers and draw with a
// BasicEffect using camera.GetViewMatrix() / GetProjectionMatrix().

Recipe 3 — Mouse-driven orbit camera

OrbitCamera is stateless per frame — accumulate yaw/pitch from input and reapply. Clamp pitch yourself; the helper deliberately doesn't.

Easy3D::Camera3D    camera;
Easy3D::OrbitCamera orbit;
orbit.SetDistance(12.0f);

// Per frame, from your CNA input handling:
orbit.SetYaw(orbit.GetYaw() + mouseDeltaX * 0.01f);
orbit.SetPitch(std::clamp(orbit.GetPitch() + mouseDeltaY * 0.01f,
                          -1.5f, 1.5f));       // keep away from the poles
orbit.SetDistance(std::clamp(orbit.GetDistance() - wheelDelta, 3.0f, 30.0f));
orbit.SetTarget(player.Position);
orbit.ApplyTo(camera);

Recipe 4 — Smoothed follow camera with teleport handling

Easy3D::Camera3D     camera;
Easy3D::FollowCamera follow;
follow.SetOffset({0.0f, 6.0f, 12.0f});
follow.SetSmoothing(0.15f);   // 15% of the gap per 1/60 s — frame-rate independent

// On level start / teleport: snap instead of flying across the level.
using Vector3 = Easy3D::FollowCamera::Vector3;
follow.SetPosition(Vector3(player.Position.X + follow.GetOffset().X,
                           player.Position.Y + follow.GetOffset().Y,
                           player.Position.Z + follow.GetOffset().Z));

// Per frame:
follow.Update(player.Position, deltaSeconds);
follow.ApplyTo(camera, player.Position);

Why not just lerp with t = smoothing * dt? Because that drifts with frame rate. FollowCamera re-derives an exponential decay so consecutive small updates match one big one — details on the FollowCamera page.

Recipe 5 — Debug overlay for collision boxes

using Vector3 = Easy3D::DebugDraw::Vector3;
Easy3D::DebugDraw debug;

// Per frame (only when the debug overlay is enabled):
debug.Clear();
for (const auto& entity : entities)
{
    debug.Box(entity.Position, entity.BoundsSize);
    debug.Line(entity.Position, entity.Position + entity.Velocity);
}
// Render debug.Lines()/debug.Boxes() with your line renderer;
// PrimitiveCount() is handy for an on-screen stat.

Recipe 6 — One camera, several rigs

Since both rigs write into a plain Camera3D, switching camera behavior is just choosing which rig runs this frame:

switch (cameraMode)
{
case CameraMode::Orbit:    orbit.ApplyTo(camera); break;
case CameraMode::Follow:   follow.Update(player.Position, dt);
                           follow.ApplyTo(camera, player.Position); break;
case CameraMode::Scripted: camera.SetPosition(cutscenePos);
                           camera.SetTarget(cutsceneTarget); break;
}
const auto view = camera.GetViewMatrix();   // single source of truth
All recipes on this page require linking CNA at runtime (they construct CNA vector values). In a game that pulls in CNA + Easy3D via add_subdirectory, that is automatic — see Building & CMake.