Files
2026-11-02 08:25:54 -05:00

3.3 KiB

Rendering

The renderer draws ECS entities: cameras pick the view, directional lights shade it, MeshRenderer draws it. This page covers the C# surface; the GPU-driven path (culling, indirect draws) is internal.

Camera, light, mesh

Register the render components by C# full name (Ecs.ComponentId<T>() resolves the ids the native side registered idempotently), upload one mesh, spawn the three entities:

// Snippet: the camera every 3D scene needs (60-degree, main view).
// Requires: Engine.Runtime (Camera).
Engine.Runtime.Camera MainCamera()
{
    return new Engine.Runtime.Camera
    {
        FovYRad = 60.0f * System.MathF.PI / 180.0f,
        NearZ = 0.1f,
        FarZ = 100.0f,
        IsMain = 1,
    };
}

Lights carry the direction light travels (pointing down into the scene), plus an intensity. Meshes are uploaded once through Renderer.MeshCreate (interleaved pos/normal/uv, 32 bytes per vertex) and referenced by the returned id:

// Snippet: a warm key light pointing down into the scene.
// Requires: Engine.Runtime (DirectionalLight).
Engine.Runtime.DirectionalLight KeyLight()
{
    var light = new Engine.Runtime.DirectionalLight { Intensity = 1.0f };
    unsafe
    {
        var len = System.MathF.Sqrt(0.25f + 0.81f + 0.4225f);
        light.Direction[0] = -0.5f / len;
        light.Direction[1] = -0.9f / len;
        light.Direction[2] = -0.65f / len;
    }

    return light;
}

The mesh fragment shader does Lambert lighting (max(dot(n, -light_dir), 0) * 0.9 + 0.1) and sRGB-encodes in-shader (lavapipe offers no _SRGB swapchain format, so the encode cannot live in the surface).

Driving frames from C#

Renderer.Create borrows the world (destroy the renderer before the world), Frame() pumps SDL events, runs systems, renders and presents, and returns true when the window was closed:

// Snippet: fixed-step frame budget shared by samples and autoplay.
// Pure math: N frames at 60 Hz take N/60 seconds of game time.
double GameSeconds(int frames) => frames / 60.0;

After Frame(), call EngineWorldPropagate so moved LocalTransform rows flow into WorldTransform before the next render.

Screenshots and goldens

renderer.Screenshot(path) captures RGBA8 (BGRA-swizzled, always RGBA on disk) after the frame. Golden comparison allows every channel ±2 with at most 0.1% of pixels differing; on mismatch the harness writes <name>.actual.png and <name>.diff.png (red where different) next to the build dir, and ENGINE_UPDATE_GOLDENS=1 overwrites the golden:

// Snippet: golden file locations for a named capture.
// Pure path math; mirrors native/tests/golden_compare.hpp.
string GoldenPaths(string goldenDir, string buildDir, string name)
{
    var golden = System.IO.Path.Combine(goldenDir, name + ".png");
    var actual = System.IO.Path.Combine(buildDir, "golden_out", name + ".actual.png");
    return $"{golden} vs {actual}";
}

2D and UI rendering

Sprites batch by atlas (SpriteRenderer2D: atlas id, sorting layer and order, UV rect, tint) under an OrthoCamera (height, aspect, optional pixel-perfect). One draw call per atlas covers 10k sprites. Game UI renders RmlUi documents through the engine (UIDocument.Load, element lookup, text set, click events) — see the P5-04 HUD golden.