CubeMesh requires CNA link

Easy3D / Data & Batching / CubeMesh

The CubeMesh functions turn queued CubeBatch items — and a handful of related standalone shapes — into plain CPU-side vertex/index arrays. No GPU work: no GraphicsDevice, no vertex/index buffers, no shaders — just std::vector data a caller can later upload. This was Easy3D's first Roadmap Phase 3 vertex builder, and CubeMeshRenderer is the Phase 4 adapter that actually draws its output.

#include <Easy3D/CubeMesh.hpp>

namespace Easy3D
{
    struct CubeVertex;

    void AppendCubeMesh(const CubeItem& item, vertices, indices);
    void BuildCubeMesh(const CubeBatch& batch, vertices, indices);

    enum class CubeFace : int { PosZ, NegZ, PosX, NegX, PosY, NegY };
    struct DirectionalCubeFace;
    struct DirectionalCubeItem;
    void AppendDirectionalCubeMesh(const DirectionalCubeItem& item, vertices, indices);

    enum class PlateAxis { Z, X, Y };
    struct PlateItem;
    void AppendPlateMesh(const PlateItem& item, vertices, indices);

    struct TripleCrossItem;
    void AppendTripleCrossMesh(const TripleCrossItem& item, vertices, indices);

    struct PyramidTipItem;
    void AppendPyramidTipMesh(const PyramidTipItem& item, vertices, indices);
}

(Signatures abbreviated above — every Append*/Build* function takes std::vector<CubeVertex>& vertices, std::vector<std::uint32_t>& indices as its trailing two parameters, exactly like AppendCubeMesh.)

All six shapes on this page share one non-obvious, thoroughly-relearned rule: front faces are wound clockwise as seen from outside, not the OpenGL-textbook counter-clockwise. See "Winding: the XNA convention, not the OpenGL one" below before hand-building any geometry of your own alongside these.

CubeVertex

One mesh vertex — shared by every shape on this page:

FieldTypeMeaning
PositionMicrosoft::Xna::Framework::Vector3World position.
UvMicrosoft::Xna::Framework::Vector2Normalized texture coordinate.

Winding: the XNA convention, not the OpenGL one

Every shape below is wound so its first triangle's cross(v1-v0, v2-v0) points into the solid, not out of it — the opposite of the OpenGL-textbook "counter-clockwise from outside" rule. This matches CNA's real, verified culling behavior: under the default RasterizerState::CullCounterClockwise, the triangles that survive are the ones that appear visually clockwise on screen — the same convention FNA's SpriteBatch quads and XNA's canonical tutorial triangle use.

This was root-caused the hard way (2026-07-10): an earlier version of this file used the OpenGL CCW-from-outside convention, so every face was invisible from outside, and scenes only "looked right" by showing the mirrored interiors of a cube's opposite faces — a bug that stayed hidden until a level had a face with no opposite neighbor (pillar fronts, staircases). If you ever "clean up" this winding back to the textbook CCW convention, you will reintroduce that exact bug. Standard back-face culling still works correctly either way — it's just mirrored from the OpenGL-textbook expectation.

AppendCubeMesh / BuildCubeMesh — the basic block

void AppendCubeMesh(const CubeItem& item, vertices, indices)
Appends one cube's triangle mesh — 24 vertices, 36 indices (6 faces × 4 vertices, so each face can carry its own UV corners) — to existing output arrays. Indices are offset by the vertex count already present in vertices, so results from repeated calls concatenate correctly into one combined mesh.
void BuildCubeMesh(const CubeBatch& batch, vertices, indices)
Builds a combined triangle mesh for every item in batch, in CubeBatch::Items() order. Equivalent to calling AppendCubeMesh for each item in turn against the same output arrays.

Properties of the generated geometry:

All functions on this page append — they never clear the output vectors. Clear them yourself when rebuilding from scratch, or exploit the appending to accumulate several shapes into one mesh (e.g. a cube plus a PyramidTipItem hanging beneath it).

DirectionalCube — per-face textures and holes

Identified by the mobile-eggbert-reference tile-identification work as the "DirectionalCube" render mode: a cube where each of the 6 faces independently chooses its own UV region and whether it is emitted at all. A Visible == false face is a genuine geometric hole (e.g. an open grate you can see and shoot through) — not just an untextured face.

enum class CubeFace : int { PosZ = 0, NegZ = 1, PosX = 2, NegX = 3, PosY = 4, NegY = 5 };

struct DirectionalCubeFace
{
    bool Visible = true;
    UvRect Uv{0.0f, 0.0f, 1.0f, 1.0f};
};

struct DirectionalCubeItem
{
    Vector3 Center;
    Vector3 Size;
    DirectionalCubeFace Faces[6];   // indexed by CubeFace
};

void AppendDirectionalCubeMesh(const DirectionalCubeItem& item, vertices, indices);

AppendDirectionalCubeMesh appends only the item's visible faces, using CubeFace indexing into Faces[6] — faces with Visible == false contribute no vertices or indices at all, so a fully-invisible item produces an empty mesh and a 4-side, 2-top/bottom-open item produces exactly 16 vertices / 24 indices (4 faces, not 6). Each face's own Uv is independent, unlike CubeItem's single shared region.

There is deliberately no "flat fallback color" concept — a caller wanting a plain-color face (e.g. matching an icon's own background) should pick a suitable Uv sub-rect of the existing atlas texture (a corner swatch) rather than Easy3D growing a second, color-only vertex/shader path.

Plate — a flat double-sided panel

Identified as the "InnerFlatPlate" render mode: a single flat, genuinely double-sided plate centered inside an otherwise fully transparent block — a signpost, a thin post, a screen. The block's outer 6 faces are simply never drawn by the caller; this only builds the plate itself. The horizontal axis (PlateAxis::Y) is also used standalone for surface-only effects, like a grass top sitting above an otherwise ordinary block.

enum class PlateAxis { Z, X, Y };

struct PlateItem
{
    Vector3 Center;
    float Width  = 1.0f;   // X for Z/Y axis, Z for X axis
    float Height = 1.0f;   // Y for Z/X axis, Z for Y axis
    UvRect Uv{0.0f, 0.0f, 1.0f, 1.0f};
    PlateAxis Axis = PlateAxis::Z;
};

void AppendPlateMesh(const PlateItem& item, vertices, indices);
AxisPlaneReading
Zspans X (Width) × Y (Height), facing ±Za "north-south wall"
Xspans Z (Width) × Y (Height), facing ±Xan "east-west wall"
Yspans X (Width) × Z (Height), facing ±Ya horizontal "floor/ceiling/tabletop"

AppendPlateMesh emits 8 vertices, 12 indices: two coincident quads with opposite winding, genuinely visible from both sides regardless of the renderer's cull state — unlike a single quad, which under standard culling only shows from one side.

TripleCross — a 3-plane cross billboard

Identified as the "TripleCrossBillboard" render mode: the same texture drawn on 3 vertical, double-sided planes through the block's center, each 60° apart around Y — a 3-plane generalization of the classic 2-plane "cross" billboard used for plants and foliage. Rotationally symmetric by construction, so unlike PlateItem/DirectionalCubeItem there is no per-icon facing decision to make.

struct TripleCrossItem
{
    Vector3 Center;
    float Width  = 1.0f;
    float Height = 1.0f;
    UvRect Uv{0.0f, 0.0f, 1.0f, 1.0f};
};

void AppendTripleCrossMesh(const TripleCrossItem& item, vertices, indices);

AppendTripleCrossMesh emits 24 vertices, 36 indices — 3 PlateItem-style double-sided planes (8 vertices/12 indices each), the first lying in the Z=Center.Z plane exactly like a PlateAxis::Z plate, the other two rotated 60° and 120° around the Y axis through Center.

PyramidTip — a hanging spike

An inverted square pyramid: a flat square face at Center's Y (typically flush with a block's own bottom face), tapering down to a single point Height below it — a "hanging spike / stalactite" attachment, e.g. the cone tip beneath a teleporter pillar. This is not one of the confirmed terrain render modes from the tile-identification work — it's a small, distinct "extra geometry" attachment a caller layers on top of an existing cube/DirectionalCube render, not a replacement for one.

struct PyramidTipItem
{
    Vector3 Center;      // center of the square top face
    float BaseSize = 1.0f;  // width/depth of the square top
    float Height   = 0.5f;  // vertical drop from the top face to the apex
    UvRect Uv{0.0f, 0.0f, 1.0f, 1.0f};  // shared by the top square and all 4 side triangles
};

void AppendPyramidTipMesh(const PyramidTipItem& item, vertices, indices);

AppendPyramidTipMesh emits 16 vertices, 18 indices: 1 square top face (visible from below, matching a cube's own −Y face winding — 4 fresh vertices) + 4 triangular side faces tapering to one shared apex point (3 fresh vertices each; none shared with each other or the square, since each face needs its own UVs). Each triangular face maps Uv with its two base corners at (U0,V0)/(U1,V0) and the apex at the horizontally-centered ((U0+U1)/2, V1) — a tapering-to-a-point mapping matching the tapering geometry.

An earlier version of this shape used a non-tapering box with alpha-cutout triangle faces instead of a true pyramid. It was replaced (2026-07-11) because the box's 4 flat side faces don't share a common vertex the way a real pyramid's 4 triangular faces do — adjacent faces' triangle graphics visibly failed to connect at the block's 4 vertical edges in a live screenshot. A genuine pyramid's faces meet at one shared apex by construction, so this version is structurally seamless.

Example — level terrain to one mesh

#include <Easy3D/CubeBatch.hpp>
#include <Easy3D/CubeMesh.hpp>

Easy3D::CubeBatch terrain;
terrain.Begin();
// ... Add() one cube per solid tile (see the CubeBatch page) ...
terrain.End();

std::vector<Easy3D::CubeVertex> vertices;
std::vector<std::uint32_t>      indices;
Easy3D::BuildCubeMesh(terrain, vertices, indices);

// vertices.size() == terrain.Count() * 24
// indices.size()  == terrain.Count() * 36

// A grate tile (open top/bottom): append it into the same combined mesh.
Easy3D::DirectionalCubeItem grate;
grate.Center = Vector3(3.5f, 0.5f, 2.5f);
grate.Size   = Vector3(1.0f, 1.0f, 1.0f);
for (int f = 0; f < 6; ++f) {
    const bool topOrBottom = f == static_cast<int>(Easy3D::CubeFace::PosY)
                          || f == static_cast<int>(Easy3D::CubeFace::NegY);
    grate.Faces[f].Visible = !topOrBottom;
    grate.Faces[f].Uv      = tiles.GetUv("grate_side");
}
Easy3D::AppendDirectionalCubeMesh(grate, vertices, indices);

// The game now owns plain geometry data. Upload it with CubeMeshRenderer and draw.

Scope and status

CubeMesh was Easy3D's first Phase 3 vertex builder; BillboardMesh followed it. A debug line/box vertex builder is the one remaining Phase 3 gap. For issuing real CNA draw calls from this data, see CubeMeshRenderer (Phase 4, done). See the Roadmap.

There is no visibility optimization (no hidden-face removal between adjacent cubes, no meshing/greedy merge). For the small levels Easy3D targets this is fine; anything smarter would be scope creep until a real consumer needs it.