Rendering
How a frame is drawn: the simulation is read into a SceneFrame draw list, WorldRenderer draws it, the HUD goes on top (as of 2026-11-10).
There is one renderer. It is built on CNA's XNA 4.0 graphics API with cna-engine's helpers
(MeshBuilder, GpuMesh, TextureAtlas rectangles); see
Engine Integration. Everything that changes how the game looks is
presentation only: the simulation never reads anything back from the renderer. This page is the
technical view; 3D Effects & Graphics describes what a player sees and sets.
A frame, step by step
GalaxyEggbertGame::DrawFrame() in src/GalaxyEggbertCNA/GalaxyEggbertGameDraw.cpp:
- Clear colour and depth to dark navy RGB(0, 35, 98), the corner colour of the menu pictures.
- Build the draw list:
BuildSceneFrame()(src/GalaxyEggbertCNA/GalaxyEggbertGameScene.cpp). Skipped where no world is shown: the Wait and Init phases (unless the title scene is behind the menu), both Setup screens and the editor's slot browser. - Shadow map (effects High and up):
WorldRenderer::RenderShadowMap()draws the casters from the sun into their own targets. - Ultra only: a geometry pass (
WorldRenderer::DrawGeometry(): normal, depth, mirror per pixel) for the screen-space effects. - World:
DrawWorld()callsWorldRenderer::Draw(), into a render target when a finish (PostProcessorUltraFinish) is active, otherwise straight to the screen. The editor's gizmos follow, against the world's depth. - Finish: bloom, grade and FXAA, or Ultra's HDR chain, onto the screen.
- 2D overlay:
DrawOverlay()with depth testing off: underwater tint, HUD, touch pad, menus, notices. - Captures:
DrawFrameCaptures()(screenshots, editor thumbnails, autoplay shots, golden frames).
The game draws on CNA's 800 × 480 logical surface, which CNA scales to the window. Only Ultra draws the world at the window's own pixels; the HUD stays on the logical surface. The simulation steps 60 times a second; at the Setup's higher frame rates (120, 144, 240) the camera, Blupi, moving objects and particles are interpolated between steps.
The seam: SceneFrame
src/GalaxyEggbert/Game/SceneFrame.hpp is a plain draw list with no device, effect or GPU mesh in it.
The host fills it from the simulation (which object shows, by which icon or which model pose, where);
WorldRenderer draws only from it and from the loaded assets. The lists keep the order the world
has always been drawn in, so blending and depth come out the same. It was introduced on 2026-11-09 as a pure
refactor: golden frames, 1728 autoplay frames of 11 missions and 28 editor frames stayed byte-equal.
(renderers.md once proposed two interchangeable renderers; the owner chose one.)
| Member | Holds |
|---|---|
camera (SceneCamera) | View, projection, position, look-at target, vertical field of view, aspect ratio. |
world, terrain | Whether the 3D world, and the terrain with everything drawn with it, are drawn. |
reliefTarget, reliefRadius, reliefBudget | Where the terrain reliefs are built and how many chunks this frame (-1: all, on the first frame). |
cubes, cubePieces | Lifts and crates as textured cubes; crates blown apart, each in its own transform. |
opaqueModels, objectModels, translucentModels, editorStarts | SceneModels: a SkinnedModelEXT, clip name and time, world matrix, culling, alpha, tint, per-part textures and materials. |
signDecals, electrodeSparks, dynamiteSparks, glueStrands | Textured quad sets and the glue death's strand mesh. |
billboards, marine | SceneBillboardBatches, one per sprite sheet (SceneSheet: element, object-m, explo, blupi, blupi1, big decor). |
effects (SceneEffects) | The effect level and its switches, fog, soft shadows, point lights, particles, water tints. |
Inside WorldRenderer::Draw()
The order in src/GalaxyEggbert/Game/WorldRenderer.cpp, which matters for blending and depth:
- Fog on every world effect (effects on) or off again.
- The sky background.
- Terrain pass: reliefs updated near the camera's target, then the opaque terrain and lava; soft round shadows; lift and crate cubes; the grass overlay; Blupi and the vehicles; every object model; sign and spark decals; translucent models (a hidden or ghost Blupi); the editor's player starts; then the terrain's semi-transparent pass (water and animated tiles) after the models, so water covers what lies in it (OR-0052).
- Billboards, blended with
NonPremultiplied(the sheets have straight alpha). - Particles (effects on).
Sky background
Content/backgrounds/decorNNN.png is chosen by the world's sky region (28 of the 32 region ids have a
picture; a missing one leaves the clear colour). It is drawn as one camera-facing quad 900 blocks away, inside
the camera's 1000-block far plane. The 640 × 480 picture is wrapped round the horizon a whole number
of times, so turning the camera scrolls it, and the camera's height moves it at 2/3 of the world's speed, as the
original's parallax does (OR-0053).
Terrain
TerrainRenderer builds GPU meshes from the
World, one cell per block, textured from the tile sheet through
TileAtlas (a padded, mipmapped copy of object-m.png).
How each block icon is drawn comes from BlockDefinitionRegistry:
render modes UniformCube, DirectionalCube, InnerPillarBox, ThinBar,
InnerFlatPlate, TripleCrossBillboard and SawBlade, with geometry helpers
DirectionalCubeTiles, InnerPillarBoxTiles, ThinBarTiles and InnerFlatPlateTiles.
| Mesh | What goes in | Drawn |
|---|---|---|
| Static opaque | Every still tile whose texels are mostly solid (TileCoverage::Opaque) | Opaque, lit; vertex colour carries ambient occlusion (or Ultra's GI) when the effects want it |
| Cut-out | Hard-edged sprite tiles: doors, pillars, girders, bars, flat-plate decor (TileCoverage::Cutout) | AlphaTestEffect at alpha 128, writes depth; light baked into the vertex colour |
| Transparent static | Faint tiles such as cobwebs (TileCoverage::Translucent) | Blended, depth read only |
| Grass overlay | Tops of grass icon 107, textured by textures3d/grass_top.png | Its own effect, right after the opaque terrain |
| Animated | Animated tiles (spikes, marine tiles and the like); those drawn as models are left out | Rebuilt by Update() when the animation phase changes; blended |
| Water | Water1/Water2 cells, exterior faces of each connected volume only (WaterSurface.hpp) | Rebuilt with the animation; blended last |
| Lava, lightning, wind | Each frame of the cycle built once when first shown and kept | Lava opaque with depth writes; lightning and wind blended |
A terrain larger than one buffer allows is uploaded in parts (MeshParts.hpp, GpuMeshParts).
Patching instead of rebuilding
The world keeps a block revision and a bounded change journal (World::blockChangesSince()).
GalaxyEggbertGame::RefreshTerrainMeshIfChanged() asks TerrainRenderer::PatchModelCells() to apply
changes that only touch model-drawn cells (a clay bridge crumbling, saw switches); anything else, or unknown
history, rebuilds the terrain, and the new renderer adopts the unchanged chunks' reliefs
(AdoptReliefs()). Measured rebuilds dropped from 211 to 6 in world 065 and from 259 to 0 in world 094 (OR-0074).
Terrain reliefs
Tiles that have an original Ray Dream scene can show that scene's relief on their exposed faces near the camera
(TerrainRelief.hpp, avatars3d/terrain/: 145 .gtr files serving 158 tile icons). The terrain
is split into chunks of 8 × 8 × 8 cells; a chunk within 8 blocks of the camera's target is built as
reliefs on worker threads (two at a time; on the web within the frame's budget) and released 4 blocks farther
out; until then, and everywhere else, it is textured cubes. The reliefs are off by default: the Setup's
"3D terrain" (terrainRelief=) turns them on, GALAXY_TERRAIN_RELIEF=0|1 and
GALAXY_TERRAIN_RELIEF_RADIUS override (Configuration).
Brick edges and lighting
The brick wall's edge pieces 262, 263 and 283 are drawn ragged only at a wall's open end: four courses of
bricks, each ending where its bricks end (BrickEdges.hpp, AppendRaggedBrick); elsewhere a whole
cube. Collision stays the whole cell. Terrain, grass and the lift and crate cubes are lit by one directional light
plus ambient (TerrainLighting.hpp: ambient 0.63, diffuse 0.42), so walls and steps no longer dissolve into
one flat colour.
3D models
All models are CNA SkinnedModelEXT directories drawn with one SkinnedEffect (or the effects'
model shader). They are converted from the original Ray Dream scenes (Assets & Tools).
- Models in the terrain (
BlockModels.hpp): the fans, the vanishing saucer, the crusher, the slime drip and the saw are animated models; still tiles such as the stone and white columns, the golden support, the barrel stack, the hanging bar, the teleporters, posts, the coloured doors and the spring are drawn as their scene's model whenavatars3d/objects/has it. Their roles and collision do not change; without the models they stay textured cubes. - Objects:
ObjectModelTablereadsicons.txtof each directory listed inavatars3d/objects/index.txt(121 models). The game picks an object's sprite icon exactly as the original would; an icon listed there is drawn as that model's pose (clip time, mirror, yaw, offset), any other icon stays a billboard. - Blupi (
avatars3d/blupi/): 56 bones and one clip perBlupiController::AnimState(68 clips, 20 frames a second), with the face texture switched per frame fromclips.txt. The vehicles (vehicles/helico,jeep,over,skate,tank) share his clip time and transform. Drawn in third person only; withoutavatars3d/blupi/a fox placeholder (avatars3d/blupi_placeholder/) stands in.
Billboards, decals and the water
Whatever has no model is a camera-facing quad cut from its sprite sheet, rebuilt every frame. Explosions, splashes,
sparks, the charge and the shield stay sprites by decision. Signs' painted faces and the dynamite and electrode
sparks are decals generated from the sprites. Water is drawn as the outside of each water volume only, so a
swimmer is not hidden behind stacked internal walls; under water the HUD lays a tint that fades in over the last
0.35 block below the surface (WaterViewImmersion).
Effect levels, technically
The Setup's "3D effects" (effectsLevel=0..4, off by default; GALAXY_EFFECTS=0-4|off|low|medium|high|ultra)
sets SceneEffects. Shaders are GLSL 3.30 or GLSL ES 3.00 compiled at run time through CNA's
ShaderEffect; where they do not compile, that part falls back to the stock effects by itself.
| Level | Adds | Code |
|---|---|---|
| Off | The classic look; every frame byte-equal to a build without effects | — |
| Low | Distance fog in the background's colour, soft round shadows, particles for explosions, splashes, sparks and Blupi's moments | WorldEffects.cpp, EffectParticles (stepped by ticks) |
| Medium | Ambient occlusion baked into terrain corners, water and lava shaders, FXAA | SceneShaders, SurfaceShaders, PostProcess |
| High | Sun shadow map (two 1024² cascades, 44 and 160 blocks), up to 8 point lights, Ray Dream procedural materials on models, bloom and grade | SceneShaders, PostProcess |
| Ultra | HDR at window resolution (auto exposure, bloom, ACES, a grade per world, TAA), physically based light, voxel GI baked into the terrain, SSAO, contact shadows, screen-space reflections, smoother models | UltraFinish, TerrainGi.hpp, ModelSubdivision.hpp |
GALAXY_EFFECTS_DEBUG=shadow|occlusion draws the terrain as that factor alone.
Golden trace and autoplay traces are equal with the effects on.2D: HUD, menus and the editor's UI
Hud and InputPad draw real 3D quads with an orthographic
projection (QuadBatch::FlushQuads), not SpriteBatch, so their order against the scene is explicit on
every backend. They lay out in the original's 640 × 480 space, scaled by the viewport's height / 480 and centred;
menu screens fill the side bars beside their pictures. Text uses Content/icons/text.png with the original's
proportional glyph widths (TextMetrics.hpp). The editor's palette and toolbar use the same quad helpers in
viewport pixels.
Golden frames: the rendering regression check
xvfb-run -a tools/verify_golden_frames.sh <build-dir>
xvfb-run -a tools/verify_golden_trace.sh <build-dir>
The first runs the game with --golden-capture (world 999, deterministic) and byte-compares frames 60, 120 and 180
with tests/golden/ (effects off), tests/golden/effects-medium/ and tests/golden/effects-high/.
GOLDEN_UPDATE=1 rewrites only the effects' references; the classic ones are replaced by hand. Both need a display, so
they are manual checks, not CTest cases. CTest covers the pure parts: VerifyTileFaces, VerifyTileCoverage,
VerifyTerrainLighting, VerifyTileUvBounds, VerifyObjectModelTable, VerifyEffectParticles,
VerifySceneEffects and VerifyUltra. See Testing.