Rendering
One cube per non-air block, built with cna-engine's MeshBuilder and drawn as a GpuMesh through CNA.
Overview
GalaxyEggbertCNA renders terrain only — there is no Blupi sprite, no object
rendering, no HUD, no sky dome. Terrain rendering works end-to-end and is verified: a real run
on worlds3d/world001.vwr uploaded 2749 blocks (pre-bugfix count) as 65976 vertices
/ 32988 triangles, and a 5×5 on-screen pixel sample found 21 of 25 sampled points showing real
terrain color across 10 distinct colors — confirming genuine texture sampling, not a placeholder.
Pipeline
GETerrainRenderer(world, tileAtlas):
staticMesh = MakeTexturedMeshBuilder() // positions + one texture coordinate
for each (x, y, z) in world.blocksPerAxis()³:
block = world.getBlock(x, y, z)
if block.isAir(): continue
worldX = x - kWorldCenterX; worldZ = z - kWorldCenterZ // Y unshifted
if BlockTypes::tileAnimBase(block.type()) is animated:
store in m_animBlocks (rebuilt on anim-phase change)
else:
AddCenteredBox(staticMesh, (worldX, y, worldZ), size 1×1×1, faces) // MeshBuilder::AddBox
// faces: one BoxFaceStyle{visible, uv} per BoxFace,
// uv = tileAtlas.GetTileUv(block.type())
staticRenderer = UploadTexturedMesh(device, staticMesh.Mesh()) // unique_ptr<GpuMesh>
animatedRenderer = UploadTexturedMesh(device, animatedMeshForCurrentPhase)
MakeTexturedMeshBuilder, AddCenteredBox and UploadTexturedMesh
are the game's small helpers in src/GalaxyEggbert/Game/TexturedMesh.hpp over
cna-engine's MeshBuilder and
GpuMesh. A full cube is 24 vertices + 36 indices — the
same vertices and indices easy-3d's BuildCubeMesh produced until 2026-10-20, bit for
bit; the GPU meshes replaced easy-3d's CubeMeshRenderer.
No face culling or greedy meshing is performed — every non-air block gets a full cube, even where faces are fully occluded by neighbors. This is a known, flagged limitation (see Known Issues) that will matter once denser hand-authored worlds exist.
Texture Mapping — GETileAtlas
GetTileUv(blockType) returns a CnaEngine::Graphics::UvRect
(u0, v0, u1, v1) into object-m.png (1301×1431 px; 64 px tiles, 20 columns,
65 px pitch: 64 px icon + 1 px inter-tile gap), computed with the game's own
BlockTypes::tileUV(). It returns an all-zero rect for Air (type ≤ 0) and for types
outside the sheet's grid.
Animated Tiles
Ported 1:1 from mobile-eggbert's Tables.cpp (via GalaxyEggbertSimple3D):
| Tile | Frames |
|---|---|
| Lava | 8 |
| Spike | 16 |
| Crusher | 10 |
| Saw | 6 |
| Water1 / Water2 | 6 each |
| Temp | 20 (including 2 invisible/blank frames) |
| Fan ×4 directions | 3 each |
| Marine | 11 |
Animated blocks are split into a second mesh, rebuilt only when the anim phase actually changes (not every frame). If the current phase yields zero visible animated cubes (e.g. Temp's blank frames), the animated renderer is reset to null rather than uploading empty data.
Draw Call
terrainEffect_->View = camera_.View(); // CnaEngine::Graphics::Camera
terrainEffect_->Projection = camera_.Projection();
terrainEffect_->World = Matrix::Identity;
staticRenderer->Draw(device, *terrainEffect_); // GpuMesh::Draw: each pass of the effect
if (animatedRenderer) animatedRenderer->Draw(device, *terrainEffect_);
Known Rendering Limitations
- No face culling between adjacent solid blocks.
- No chunk-radius streaming — the entire world is uploaded up front.
- No Blupi sprite, no object/pickup rendering, no sky dome, no HUD.