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 ongpuand 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_skyboxoff 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:
- Vignette —
intensity(0 = off),radius, andsmoothnesscontrol a radial darkening from the center out. - Color grading —
lift,gamma, andgain(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. - Bloom —
threshold(linear HDR brightness above which pixels glow),intensity(0 = off), andradius(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.