Rendering

Turian renders 3D with a single SDL3-GPU renderer (Vulkan / Metal / D3D12) shared by the editor viewport and the shipped game — so what you see while editing is what the game draws: metallic-roughness PBR, directional shadow mapping, normal mapping, and direct support for compressed (BCn / KTX2) textures.

Two small modules

The renderer is split into engine-independent packages so it isn't welded to the editor's UI toolkit:

  • gpu — a thin window + GPU device platform layer over SDL3's GPU API. It owns the OS window, device, and swapchain, and exposes the translated SDL3 API plus a per-frame command buffer. It also offers a screenshot helper (texture readback → TGA) handy for debugging.
  • render — the scene renderer: PBR pipeline, shadow pass, mesh/texture upload, and the draw loop. It depends on gpu and the engine's scene/asset types, and takes a GPU device, a color target, the scene nodes, and asset bytes via callbacks — no UI dependency.

The editor feeds render dvui's device and an offscreen target; the game feeds it an SDL window's swapchain. Both resolve assets (meshes, textures, materials) by GUID through the same byte-source seam — from the cooked .cache in the editor, from the packaged .oap in the game.

Software fallback

When SDL3 GPU isn't available (e.g. headless CI), the game falls back to a CPU software rasterizer. The GPU path is the default for normal builds.

Lighting

Up to 8 lights (directional / point / spot) are supported per frame. The first shadow-casting directional light drives a cascaded shadow map (4 splits, each with its own PCF-filtered strip of the shadow atlas), so shadow detail stays sharp close to the camera without giving up coverage over a large scene. Metals pick up an ambient specular term so they aren't black in the absence of an environment probe. See Components for the Light and MeshRenderer components, and Assets & Materials for the PBR material model.

Image-based lighting

An Environment component (at most one active per scene) points at an equirectangular HDR map and drives both the skybox background and ambient lighting:

  • Skybox — the equirect texture is sampled directly by view-ray direction; toggle show_skybox off to keep the ambient contribution while falling back to the renderer's plain clear color, useful while iterating on geometry/materials without a busy HDRI competing for attention.
  • Diffuse irradiance — projected once per environment upload into an order-2 spherical-harmonics basis (cheap to evaluate per-pixel, and already correct for a diffuse-only term — no need for a full diffuse cubemap convolution).
  • Specular reflections — a GGX-importance-sampled prefiltered cubemap: on upload, the equirect map is converted to a base cubemap, then each mip is prefiltered at increasing roughness (the standard split-sum approach), sampled at shading time by reflection direction and a roughness-selected mip. This replaced an earlier equirect-mip approximation that box-filtered in UV space — it over-blurred near the poles and had no real GGX lobe shape at grazing angles. The environment-BRDF term uses Karis' analytic approximation rather than a baked 2D LUT.

All of this happens once per environment-texture upload, not per frame — no runtime cost beyond the initial cubemap generation.

Color management

Lighting runs in linear space: color textures (albedo, emissive) are sampled as sRGB — decoded to linear by the GPU sampler for tagged DDS/KTX2 textures, or via a small envelope the importer bakes into PNG/JPEG sources — while data maps (normal, metallic-roughness, occlusion) stay linear throughout. The main pass renders into an HDR (R16G16B16A16_FLOAT) target rather than writing tonemapped color directly; the post-process composite pass applies an ACES filmic tonemap followed by gamma-2.2 encoding at the very end, once the render targets and swapchain are UNORM (not sRGB). The software rasterizer mirrors both steps directly in its shading function for parity between the GPU and CPU paths (it has no separate post-process stage).

Post-processing

A GPU-only post-process stack runs after the lit pass and before/combined with tonemapping: vignette, per-channel RGB Lift/Gamma/Gain color grading, and bloom. Settings live on a PostProcessVolume component, not the camera — add it to any scene object and it's either global (affects the whole scene) or local (a box/sphere region around that object, with a blend_distance falloff at the edge). Each effect category has its own enabled toggle:

  • Vignetteintensity (0 = off), radius, and smoothness control a radial darkening from the center out.
  • Color gradinglift, gamma, and gain (each an RGB triple) apply the same Lift/Gamma/Gain model as Unity's grading wheels: lift shifts shadows, gamma shifts midtones, gain shifts highlights, each independently per channel so you can shift color balance (e.g. warm shadows, cool highlights) and not just brightness.
  • Bloomthreshold (linear HDR brightness above which pixels glow), intensity (0 = off), and radius (glow spread) drive a multi-mip dual-filter bloom, so emissive or specular-blown surfaces glow with a soft, wide halo rather than a small fixed-radius blur.

A PostProcessVolume with every category disabled is a hard no-op — a scene with no volumes at all renders bit-identical to having no post-processing.

Overlapping volumes

Multiple volumes can affect the camera at once — a global "look" for the whole level plus local volumes for specific rooms or moments, say. Overlap is resolved by priority (higher wins) and distance-based weight (1 inside a local volume's shape, fading to 0 across its blend_distance), blended per-category so one volume can override just vignette while grading and bloom still come from whatever else is active. The blend is evaluated against the camera's actual position each frame, so flying through a volume's boundary in the editor previews the same fade the shipped game will show.

Custom effects

Engine/plugin code can register an additional post-process pass — reading and writing the HDR buffer directly, so it can do its own bright-pass or HDR-space grading work — via postprocess.registerEffect in subsystems/render/postprocess.zig. Registered effects run in registration order, after the built-in bloom generation and before the final composite; this is a Zig-level extension point (register once at startup), not a live/editor-authored shader system.

Because bloom needs unclamped HDR brightness data, post-processing requires the HDR main pass above — it isn't available in the software renderer.


← All docs Edit this page