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
add_subdirectory, that is automatic — see Building & CMake.