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:

  1. Clear colour and depth to dark navy RGB(0, 35, 98), the corner colour of the menu pictures.
  2. 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.
  3. Shadow map (effects High and up): WorldRenderer::RenderShadowMap() draws the casters from the sun into their own targets.
  4. Ultra only: a geometry pass (WorldRenderer::DrawGeometry(): normal, depth, mirror per pixel) for the screen-space effects.
  5. World: DrawWorld() calls WorldRenderer::Draw(), into a render target when a finish (PostProcess or UltraFinish) is active, otherwise straight to the screen. The editor's gizmos follow, against the world's depth.
  6. Finish: bloom, grade and FXAA, or Ultra's HDR chain, onto the screen.
  7. 2D overlay: DrawOverlay() with depth testing off: underwater tint, HUD, touch pad, menus, notices.
  8. 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.)

MemberHolds
camera (SceneCamera)View, projection, position, look-at target, vertical field of view, aspect ratio.
world, terrainWhether the 3D world, and the terrain with everything drawn with it, are drawn.
reliefTarget, reliefRadius, reliefBudgetWhere the terrain reliefs are built and how many chunks this frame (-1: all, on the first frame).
cubes, cubePiecesLifts and crates as textured cubes; crates blown apart, each in its own transform.
opaqueModels, objectModels, translucentModels, editorStartsSceneModels: a SkinnedModelEXT, clip name and time, world matrix, culling, alpha, tint, per-part textures and materials.
signDecals, electrodeSparks, dynamiteSparks, glueStrandsTextured quad sets and the glue death's strand mesh.
billboards, marineSceneBillboardBatches, 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:

  1. Fog on every world effect (effects on) or off again.
  2. The sky background.
  3. 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).
  4. Billboards, blended with NonPremultiplied (the sheets have straight alpha).
  5. 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.

MeshWhat goes inDrawn
Static opaqueEvery 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-outHard-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 staticFaint tiles such as cobwebs (TileCoverage::Translucent)Blended, depth read only
Grass overlayTops of grass icon 107, textured by textures3d/grass_top.pngIts own effect, right after the opaque terrain
AnimatedAnimated tiles (spikes, marine tiles and the like); those drawn as models are left outRebuilt by Update() when the animation phase changes; blended
WaterWater1/Water2 cells, exterior faces of each connected volume only (WaterSurface.hpp)Rebuilt with the animation; blended last
Lava, lightning, windEach frame of the cycle built once when first shown and keptLava 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).

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.

LevelAddsCode
OffThe classic look; every frame byte-equal to a build without effects—
LowDistance fog in the background's colour, soft round shadows, particles for explosions, splashes, sparks and Blupi's momentsWorldEffects.cpp, EffectParticles (stepped by ticks)
MediumAmbient occlusion baked into terrain corners, water and lava shaders, FXAASceneShaders, SurfaceShaders, PostProcess
HighSun shadow map (two 1024² cascades, 44 and 160 blocks), up to 8 point lights, Ray Dream procedural materials on models, bloom and gradeSceneShaders, PostProcess
UltraHDR 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 modelsUltraFinish, TerrainGi.hpp, ModelSubdivision.hpp
Status. Checked on desktop OpenGL 3.3 under Xvfb (11 missions at each level) and on WebGL 2 in headless Chrome; not run on Android. 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.

Related pages