# 3D graphics

The [gfx](../pkg/gfx.md) package draws 2D and 3D through one context.
This guide covers the 3D half. It works through cameras, geometry,
materials, lighting, sky, then culling, grading and profiling. The
examples are the fastest reference: `viewer` orbits a scene or a glTF
file, `lighting` puts every post-processing setting on a slider,
`materials` shows one sphere per feature, `terrain` is an outdoor scene
with billboards, levels of detail and fog, and `space` and `solar` are
vacuum scenes.

## The frame

There is no scene object and no begin call. Everything queued during
`Draw` is submitted when `Draw` returns, in a fixed order: render
textures first, then the shadow atlas, then the scene into a high dynamic
range image (sky, opaque meshes, decals, blended and transmissive meshes
back to front, debug lines), then the post pass, then 2D over the
tone-mapped result in layer and call order.

So 2D always draws over 3D. `ctx.Clear` is the colour behind everything,
though when `Light.Background` is set the sky or environment map is drawn
instead and the clear colour never shows. To draw a HUD or a name plate,
make the 2D calls after the mesh calls, and use `Project` to turn a world
point into the view coordinates 2D uses. To put a 3D character on a 2D
field, draw the 3D into a `RenderTexture` and draw that texture as a
sprite between the background and foreground layers.

```go
func (g *game) Draw(ctx *engine.Context) error {
	gr := ctx.Gfx
	ctx.Clear = gfx.RGB(10, 12, 18)
	gr.SetCamera(gfx.OrbitCamera(lin.V3(0, 1, 0), g.yaw, g.pitch, 12))
	gr.SetLight(gfx.Light{Direction: lin.V3(-0.4, -1, -0.6),
		Color:   gfx.Color{R: 2.2, G: 2.1, B: 1.9, A: 1},
		Ambient: gfx.Color{R: 0.18, G: 0.2, B: 0.25, A: 1},
		Shadows: true, ShadowDistance: 40})
	gr.DrawMeshAt(g.ship, gfx.Material{Metallic: 1, Roughness: 0.3}, g.shipAt)
	gr.DebugText(10, 10, "hull 100%")
	return nil
}
```

`SetCamera` and `SetLight` apply to the whole current output for the
frame. `SetPost` applies to every output, including render textures;
the final settings are used when the frame is submitted. Call each once
for its intended output rather than changing it between draws.

## Cameras

`Camera` looks from `Position` at `Target`. `FovY` is the vertical field
of view in radians (zero means 60 degrees), `Near` and `Far` default to
0.1 and 1000, `Up` defaults to +Y. Setting `Ortho` to half the view's
height in world units makes the camera orthographic, so distance no
longer shrinks things. That is what an isometric strategy view uses.

```go
// Third person, then the same focus seen isometrically.
gr.SetCamera(gfx.Camera{Position: player.Add(lin.V3(0, 3, 8)),
	Target: player.Add(lin.V3(0, 1.6, 0)), FovY: lin.Radians(55), Far: 500})
gr.SetCamera(gfx.Camera{Position: focus.Add(lin.V3(30, 30, 30)), Target: focus, Ortho: 20})
```

`OrbitCamera(target, yaw, pitch, distance)` builds an inspector or
strategy camera from three numbers you can drive with the mouse.
`engine.FlyCamera` is a free-flying camera for looking around a scene
while it is being built. W, A, S and D move, Q and E go down and up,
Shift goes faster, and the view turns while the right mouse button is
held, or with every movement when `AlwaysLook` is set and the cursor is
captured. `LookAt` points it at a position, `Forward` gives its
direction.

```go
g.fly = &engine.FlyCamera{Position: lin.V3(0, 5, 20), Speed: 25} // in Init
func (g *game) Update(ctx *engine.Context) error { g.fly.Update(ctx); return nil }
func (g *game) Draw(ctx *engine.Context) error   { ctx.Gfx.SetCamera(g.fly.Camera()); return nil }
```

`Project(p)` maps a world point into the 2D view and returns `ok` false
when the point is behind the camera. `ScreenRay(x, y)` turns a point in
the view (the units the mouse reports) into a world ray, and
`Mesh.Intersect` or `Model.Intersect` report where it hits. Both are on
`Graphics` for use while drawing and on `Camera` itself, taking the view
size, for picking from `Update`: `g.cam.ScreenRay(x, y, ctx.Width,
ctx.Height)`. For a scene backed by physics, `phys.Raycast3` casts
against colliders instead, which is cheaper and gives you the entity.

```go
ray := gr.ScreenRay(ctx.Input.Mouse())
for _, u := range g.units {
	if hit, ok := u.model.Intersect(u.at.Matrix(), ray); ok && hit.Distance < best {
		best, g.selected = hit.Distance, u
	}
	if x, y, ok := gr.Project(u.at.Position.Add(lin.V3(0, 2.2, 0))); ok {
		gr.DrawText(g.font, u.name, x, y, gfx.White) // name plate in 2D
	}
}
```

`Camera.Frustum(aspect)` and `Graphics.Frustum()` return the volume the
camera sees; the culling section uses them.

## Meshes

A `Mesh` is indexed triangle geometry in device memory. `NewMesh` uploads
a slice of `Vertex` (position, normal, UV, an optional second UV set and
an optional colour that multiplies the material's base colour) and a
slice of indices.

The shape functions return those two slices without touching the GPU, so
you can transform and merge them first: `CubeMesh`, `SphereMesh(rings,
segments)`, `PlaneMesh(segments)`, `QuadMesh`, `CylinderMesh(segments)`,
`ConeMesh(segments)`, `CapsuleMesh(rings, segments, halfHeight)`,
`TorusMesh(tube, rings, segments)` and `HeightfieldMesh(heights, cols,
rows, cell)`. `HeightfieldMesh` turns a grid of heights, its size in
samples and the world units between samples into one mesh; for ground a
game walks about on, the `Terrain` type below builds on the same idea and
does the chunking and the levels of detail as well.

```go
verts, idx := gfx.HeightfieldMesh(g.heights, cols, rows, 1.0)
for i := range verts { // colour by height and slope: no textures needed
	v := &verts[i]
	v.Color = gfx.RGB(86, 125, 50)
	if v.Normal.Y < 0.75 {
		v.Color = gfx.RGB(110, 105, 100) // cliff
	} else if v.Pos.Y > 6 {
		v.Color = gfx.RGB(235, 240, 245) // snow
	}
}
terrain, err := ctx.Gfx.NewMesh(verts, idx)
```

Building geometry yourself takes three helpers. `TransformVertices`
returns a copy of a shape moved by a matrix, `AppendMesh` merges two
shapes into one, and `ComputeNormals` fills in smooth normals for
vertices written by hand. `FlatShaded` splits shared vertices so every
triangle keeps its own normal, for the faceted look and for coarse
levels of detail.

```go
// A voxel chunk: one mesh for the visible blocks, one draw call.
cube, cubeIdx := gfx.CubeMesh()
var verts []gfx.Vertex
var idx []uint32
for _, b := range chunk.Visible() {
	placed := gfx.TransformVertices(cube, lin.Translate(b.Pos))
	for i := range placed {
		placed[i].Color = b.Color
	}
	verts, idx = gfx.AppendMesh(verts, idx, placed, cubeIdx)
}
chunk.mesh, err = ctx.Gfx.NewMesh(verts, idx)
```

`Mesh.Update(verts, indices)` replaces the geometry of a mesh already on
the GPU, for a voxel chunk after a block is dug, a terrain edit, or a
mesh that grows. Draws already queued this frame keep the old geometry
until the frame ends, so an update is safe at any point in `Update` or
`Draw`. Use it instead of destroying and recreating a mesh. `Mesh.Min`
and `Mesh.Max` are the bounds in mesh space, `Vertices` and `Indices`
read the geometry back, and `Destroy` frees it.

Draw with `DrawMesh(mesh, material, model)` where `model` is a
`lin.Mat4`, or `DrawMeshAt(mesh, material, transform)` when a
`gfx.Transform` is what you have. Draws sharing a mesh and a material are
collected into one instanced call, so a thousand asteroids or a forest of
identical trunks cost one draw. This happens automatically, but only
while the materials stay identical, so prefer vertex colours or one
atlas over a material per object.

`NewSkinnedMesh` takes `SkinVertex` values with four joint indices and
weights, and `DrawSkinned(mesh, material, model, joints)` draws it with
joint matrices the game computed itself, as the `lighting` example does.

## Terrain

`NewTerrain(opts)` takes a heightfield and does what a game would
otherwise write itself: it splits the field into square chunks, builds
each chunk's mesh at several resolutions, and `DrawTerrain(terrain)`
queues each chunk at the resolution its distance from the camera
deserves. Chunks are ordinary draws, so the frustum and the frame's
occluders cull them, and each carries a skirt around its edge deep
enough to hide the crack where it meets a coarser neighbour.

`TerrainOptions` takes the `Heights` row by row with `Cols` and `Rows`,
the `Cell` between samples and the `Centre` the field sits on. `Cols-1`
and `Rows-1` must be whole multiples of `ChunkSize`, which is a power of
two and 32 by default; `Levels` is how many resolutions each chunk keeps
(4) and `LODDistance` how far the finest one reaches, each level after
covering twice the distance of the one before. Every chunk at every level
is uploaded at once, so a terrain costs about a third more device memory
than its finest level alone.

The ground is shaded by a built-in terrain shader. `Splat` is an RGBA
image stretched over the whole field whose four channels weight the four
tiling `Layers` textures, each repeating every `LayerScale` world units
with its own `LayerRoughness`. Give the layers `TextureOptions.Repeat`,
since they tile. `SetSplat(img)` replaces the weights later, for a map
painted from the terrain's own height and slope or repainted as the
ground changes, and `Shader()` reaches the shader to rebind a layer.

`Height(x, z)` and `Normal(x, z)` say where the ground is and which way
it faces, for scattering trees, dropping items and refusing to build on a
slope. `Raycast(ray, reach)` finds where a ray first goes under the
ground, which is what a click that digs needs. `Heights()` is the
terrain's own sample grid: write into it and call `Update(minX, minZ,
maxX, maxZ)` to rebuild the chunks that changed. `Bounds`, `Size`,
`Chunks`, `Levels`, `ChunkLevel` and `ChunkCentre` report what it holds
and what the last frame drew.

```go
g.ground, err = ctx.Gfx.NewTerrain(gfx.TerrainOptions{
	Heights: heights, Cols: 129, Rows: 129, Cell: 1, ChunkSize: 32,
	Levels: 4, LODDistance: 45,
	Layers: [4]*gfx.Texture{sand, grass, rock, snow},
	LayerScale: [4]float32{6, 5, 4, 7},
})
g.ground.SetSplat(weightsFromHeightAndSlope(g.ground))

// Digging: edit the samples, then rebuild what they cover.
h := g.ground.Heights()
h[z*cols+x] -= 2
g.ground.Update(x, z, x, z)

gr.DrawTerrain(g.ground) // one draw per chunk, culled and refined for you
```

## Models

`gltf.Load` reads a `.gltf` or `.glb` file into a `Document` of plain Go
slices with no GPU involved, and `Graphics.LoadModel` uploads it: one
`Mesh` per primitive, one `Texture` per image, materials with the
extensions the renderer supports, skins, clips and morph targets.
`asset.Model` does both through the [asset](../pkg/asset.md) package,
from a loose directory, a pack file or an embedded FS.

Malformed buffer bounds and animation accessor shapes or key counts
return errors from the glTF loader. Translation and scale outputs are
three-component vectors, rotations are four-component vectors, and
morph weights are scalars; cubic keys include both tangent records.
The loader retains the cubic keys' values but discards their tangents,
so playback uses linear interpolation for CUBICSPLINE channels.
The loader also rejects cyclic hierarchies, repeated children, multiple
parents and invalid node or scene references before resolving resources.
Scene roots must be parentless and unique within a scene; different
scenes may share roots. Deep valid trees have no traversal depth cutoff.

```go
doc, err := gltf.Load("assets/ship.glb")
if err != nil {
	return err
}
g.ship, err = ctx.Gfx.LoadModel(doc) // or asset.Model(ctx.Gfx, g.fs, "ship.glb")
```

`DrawModel(m, world)` queues every part under a world matrix and
`DrawModelAt(m, transform)` takes a `Transform` instead. `Model.Min` and
`Model.Max` bound the whole model. Use them to set a camera's distance
when you do not know the file in advance. `Model.Parts` is the placed
primitives, each a `ModelPart` with a `Mesh`, a `Name` (the name glTF
gave its material), a `Material` and a `World` matrix.

To draw a model with a material of your own, `DrawModelWith(m, world,
override)` passes each part through the override and draws what it
returns, and `DrawModelAnimatedWith` does the same for a posed one. The
override sees the part's index and the part itself, so it can match on
the index or on the name, and returning `part.Material` leaves a part
alone. The model's own materials are never changed.

```go
gr.DrawModelWith(g.ship, world, func(i int, p gfx.ModelPart) gfx.Material {
	if p.Name != "hull" {
		return p.Material
	}
	m := p.Material
	m.BaseColor = teamColor
	return m
})
```

`NodeCount`,
`NodeName`, `NodeIndex` and `NodeParent` walk the node hierarchy by name,
so you can find a node such as a gun muzzle and spawn an effect there;
`Model.NodeMatrix` and `NodePosition` give a node's rest-pose place in
model space for a model that is not animated, and `AnimPlayer.NodeMatrix`
its current place for one that is. `Model.Destroy` frees the meshes and
textures together.

For skinning, `Model.Clips` lists the animation clips,
`Model.NewAnimPlayer` makes an `AnimPlayer` holding one instance's pose,
`Play` or `CrossFade` chooses a clip, `Advance(dt)` runs it, and
`DrawModelAnimated(model, transform, player)` draws the result. Layers,
masks, events, root motion, morph targets and node overrides for
inverse kinematics are all on the player; the
[animation guide](animation.md) covers them.

## Transforms

The [lin](../pkg/lin.md) package holds the maths types: `Vec2`,
`Vec3`, `Vec4`, `Mat4` and `Quat` in float32, column-major,
right-handed, +Y up. Values pass by value and every operation returns a
new one. `lin.Translate`, `lin.Scale`, `lin.Rotate(angle, axis)` and
`lin.TRS(t, r, s)` build matrices and `Mul` composes them left to right,
so the last applied is written last. Quaternions avoid the gimbal
problems of stacked Euler angles. Build them with `lin.AxisAngle`,
`lin.FromEuler(yaw, pitch, roll)`, `lin.QuatLookAt` and
`lin.QuatIdentity`. `lin.Radians` converts degrees.

`gfx.Transform` is a simpler form. It holds a `Position`, a `Rotation`
quaternion and a `Scale`, where a zero rotation means none and a zero
scale means 1. `gfx.At(x, y, z)` makes one, `Moved`, `Rotated` and
`Scaled` return adjusted copies, `Matrix` gives the `lin.Mat4` and
`Forward` the direction it faces. It is the component physics, animation
and the ECS use, so a body's transform is the one you draw with.

```go
gr.DrawMesh(g.rock, rockMat, lin.Translate(pos).Mul(lin.Rotate(yaw, lin.V3(0, 1, 0))))
gr.DrawMeshAt(g.rock, rockMat, gfx.At(0, 1, 0).Rotated(lin.V3(0, 1, 0), lin.Radians(30)).Scaled(0.5))
```

For hierarchies, put a `gfx.Transform` on each entity, parent them with
`ecs.SetParent(w, child, parent)`, and `ecs.WorldMatrix(w, e)` composes
the chain from the root down. A turret on a hull on a chassis is three
entities and one call at draw time.

## Materials

`Material` is metallic-roughness PBR. `Texture` is the albedo in sRGB,
`BaseColor` multiplies it (zero means white), `Metallic` runs 0 for a
dielectric to 1 for a metal, and `Roughness` runs from mirror to matte
with a zero meaning 0.6. `MetalRoughTexture` carries roughness in green
and metallic in blue as glTF does, `NormalTexture` is a tangent-space
normal map, `EmissiveTexture` and `Emissive` make something glow, and
`OcclusionTexture` with `OcclusionStrength` darkens ambient light in
creases (`OcclusionUV2` samples it on the second UV set, the lightmap
convention). Every texture is optional; the factors alone are a complete
material. Gold is `{BaseColor: gfx.RGB(240, 200, 120), Metallic: 1,
Roughness: 0.15}` and a glowing lamp is `{BaseColor: gfx.RGB(255, 180,
60), Emissive: 3}`.

Each map keeps the filtering and edge handling its `TextureOptions` gave
it, whatever else the material binds: the engine's four samplers (linear
or nearest, repeating or clamped) are shared by every material and the
map only says which one to read it through. So a nearest-filtered sprite
sheet as albedo and a linear, repeating detail map on the same mesh both
sample the way they were made.

Transparency has three modes, and they do different things.
`AlphaCutoff` discards fragments below a threshold in both the lit and
the shadow pass, giving hard edges and real shadows, which is what
leaves, fences and grass need. `Blend` draws the material after the
opaque scene, sorted back to front, for smoke and water, or without
sorting at all under `PostSettings.OrderIndependent`. The alpha of
`BaseColor`, multiplied by the texture and vertex alpha, fades the whole
shaded surface, including lighting, emissive and fog. Pass straight
colors: `BaseColor: gfx.RGB(120, 180, 255).WithAlpha(0.25)` contributes
a quarter of its shaded color before tone mapping. The engine handles
premultiplication for both transparency paths. Without `Blend` or
`Transmission`, alpha only controls `AlphaCutoff`. `Transmission` is
refractive glass, below. Alongside them, `DoubleSided` turns off
back-face culling and lights back faces with a flipped normal, which a
single-quad leaf needs; `Unlit` shows the base colour and emissive as
they are, for holograms and map markers; `NoDepthTest` draws over
everything already drawn and `NoDepthWrite` leaves the depth buffer
alone, for ghosts and additive effects; and `UVTransform`, a
`lin.Affine` on the texture coordinates, scrolls a conveyor or tiles a
floor in one field.

The layered features come from the matching glTF extensions and cost
nothing left at zero. `Clearcoat` with `ClearcoatRoughness` adds a
varnish lobe for car paint and wet surfaces, `Sheen` with
`SheenRoughness` adds soft light at grazing angles, which is the fabric
look: velvet, felt, brushed cotton, and a good starting point is a sheen
colour near the base colour with a `SheenRoughness` around 0.4.
`Subsurface`, shaped by a `ThicknessTexture`, lets light through thin
parts for leaves, wax and skin. `Transmission` makes glass and ice. The
opaque scene shows through, refracted by `IOR` (zero means 1.5) across
`Thickness` world units, blurred by the roughness and absorbed towards
`AttenuationColor` over `AttenuationDistance`, with a
`TransmissionTexture` letting one material hold an opaque frame and glass
panes. Transmissive meshes draw after the opaque ones, like blended ones.

```go
floor := gfx.Material{Texture: g.stripes, Roughness: 0.8,
	UVTransform: lin.Translate2(t*0.05, 0).Mul(lin.Scale2(6, 6))}
glass := gfx.Material{Roughness: 0.05, Transmission: 1, IOR: 1.5, Thickness: 0.8,
	AttenuationColor: gfx.RGB(120, 200, 255), AttenuationDistance: 1}
velvet := gfx.Material{BaseColor: gfx.RGB(40, 30, 90), Roughness: 0.9,
	Sheen: gfx.RGB(200, 180, 255), SheenRoughness: 0.4}
```

`Specular` and `SpecularColor` scale and tint what a dielectric reflects,
so chalk (`Specular: 0.02`) and a warm varnish are describable; zero
means the plain 1 and white, and metals are unaffected. `Iridescence`
puts a thin film over the surface whose interference turns the hue with
the angle: soap bubbles, oil, beetles, tempered steel. Its look is set by
`IridescenceThickness` in nanometres (zero means 400; 100 to 800 is the
range that shows colour) and `IridescenceIOR` (zero means 1.3), and an
`IridescenceTexture` scales the strength by its red channel and the
thickness by its green, the two maps glTF packs into one image.
`Anisotropy` stretches the highlight along the surface for brushed metal,
hair, satin and vinyl, turned by `AnisotropyRotation`; the direction
comes from the mesh's texture coordinates, so an anisotropic mesh needs
UVs but no tangents of its own.

`Shells` draws the mesh again that many times, each further out along its
normals, which is fur, grass, moss and hair. `ShellLength` is how far the
outermost shell stands off in world units (zero means 0.05) and
`FurTexture` is the strand mask: a shell keeps a fragment where the map's
red channel is above that shell's height, so a tiled noise image gives
strands of different lengths and a `UVTransform` tiles it. Shells draw
after the opaque scene, leave the depth buffer alone and cast no shadow,
and each one costs an instance of the mesh, so sixteen shells of one
sphere are one draw call.

`Stencil`, `StencilRef` and `StencilWrite` mask one draw against
another. A material with `StencilWrite: gfx.StencilReplace` and a
`StencilRef` of 1 marks the buffer where it draws, and one with
`Stencil: gfx.StencilEqual` and the same reference draws only inside that
mark: portals, cutaways, magic windows. The buffer starts each frame at
zero, materials that write it are drawn before those that do not so the
order the game queues them in does not matter, and the masked draw still
has to pass the depth test, so put it in front of its mask. `Outline`
uses the stencil buffer for itself, so a material with an outline ignores
the three.

```go
brushed := gfx.Material{BaseColor: gfx.RGB(200, 200, 210), Metallic: 1,
	Roughness: 0.35, Anisotropy: 0.9}
bubble := gfx.Material{BaseColor: gfx.RGB(140, 140, 150), Metallic: 1,
	Roughness: 0.2, Iridescence: 1, IridescenceThickness: 480}
fur := gfx.Material{BaseColor: gfx.RGB(190, 140, 70), Roughness: 0.9,
	Shells: 16, ShellLength: 0.22, FurTexture: g.noise, UVTransform: lin.Scale2(8, 4)}
mask := gfx.Material{StencilWrite: gfx.StencilReplace, StencilRef: 1}
through := gfx.Material{BaseColor: gfx.RGB(255, 120, 40), Stencil: gfx.StencilEqual, StencilRef: 1}
```

Vertex colours multiply the base colour and need no field at all; glTF
files fill them from `COLOR_0` and the terrain snippet above writes them
by hand. The second UV set comes from `TEXCOORD_1`. A `Shader` from
`NewMeshShader` replaces the surface calculation before lighting, for
water, dissolves, triplanar mapping and vertex displacement; the
[shaders guide](shaders.md) covers writing one.

A model's materials arrive from its file. `LoadModel` reads the glTF
extensions the fields above come from, including
`KHR_materials_specular`, `KHR_materials_iridescence` and
`KHR_materials_anisotropy`, and converts the older
`KHR_materials_pbrSpecularGlossiness` workflow to metallic-roughness as
it loads, exactly for the factors and through the glossiness channel for
the image.

## Lighting

`SetLight` takes one `Light` for the frame: the directional light
(`Direction` is the direction the light travels, so a sun overhead points
down), its `Color`, and an `Ambient` term that lights everything evenly
when no sky or environment is set. Light colours are linear and are not
clamped to 1; a sun is usually above 2.

`Shadows` renders cascaded shadow maps for the directional light,
reaching `ShadowDistance` world units from the camera (default 60) at
`ShadowStrength` (zero means fully dark). The cascades pack more
resolution near the camera, so `ShadowDistance` is the setting to tune.
Keep it as small as the game allows. Forty over a character scene is
crisp; two hundred over a whole valley will alias. Cutout materials cast
cutout shadows, so a leaf texture throws a leaf-shaped shadow. A caster
far above the cascades, a bridge over a street or a cloud over a field,
still casts into them: the shadow pipelines clamp depth rather than clip
at the cascade's near plane.

Each caster is recorded only into the cascades, spot maps and cube faces
its bounds reach, so a scene spread over a large map pays for the maps a
mesh can land in rather than for all thirty-one.
`FrameStats.ShadowDraws` counts the instances that went into the shadow
atlas this frame.

`AddPointLight(pos, color, range)` shines in every direction from a
point, fading to nothing at `range`, for torches, muzzle flashes and
glowing ore. `AddSpotLight(pos, dir, color, range, inner, outer)` shines
in a cone, full inside the inner angle and fading to nothing at the
outer. `AddSpot` takes a `SpotLight` value instead, and `AddPoint` a
`PointLight`; with `Shadows` set, either casts shadows from its own
depth map. A shadowed spot light renders one map; a shadowed point light
renders the six faces of a cube, so it costs six depth passes and is the
most expensive light there is. Give it to the lamp the player stands
under, not to every torch on the wall.

```go
gr.SetLight(gfx.Light{Direction: lin.V3(-0.4, -0.7, -0.35),
	Color:   gfx.Color{R: 3, G: 2.85, B: 2.5, A: 1},
	Shadows: true, ShadowDistance: 60, Background: true,
	Sky:     gfx.Sky{Zenith: gfx.RGB(50, 105, 215), Horizon: gfx.RGB(184, 199, 224)}})
for i, f := range g.fires {
	flick := 0.8 + 0.2*float32(math.Sin(float64(t)*9+float64(i)))
	gr.AddPointLight(f, gfx.Color{R: 4 * flick, G: 2.2 * flick, B: 0.8 * flick, A: 1}, 12)
}
gr.AddSpot(gfx.SpotLight{Position: towerTop, Direction: beam, Range: 60,
	Color:      gfx.Color{R: 9, G: 8.5, B: 6, A: 1},
	InnerAngle: lin.Radians(14), OuterAngle: lin.Radians(28), Shadows: true})
gr.AddPoint(gfx.PointLight{Position: lampPos, Range: 14,
	Color: gfx.Color{R: 6, G: 5.4, B: 4, A: 1}, Shadows: true})
```

Lights are clustered. The view is cut into a grid of sixteen tiles
across, nine down and twenty-four slices into the distance, spaced
exponentially, and each frame's lights are sorted into the clusters they
reach. A fragment loops over its own cluster's lights alone, so a scene
can add hundreds without every one costing every pixel, and a light with
a small range costs only the part of the view it lights. Give a light
the smallest `Range` that looks right: the range is what decides how
much of the grid it lands in.

Plan around the limits. A frame keeps its first `MaxLights` (1024) point
and spot lights, gives shadow maps to the first `MaxSpotShadows` (4)
spot lights that ask and cube maps to the first `MaxPointShadows` (4)
point lights; the rest shine without. One cluster keeps 64 lights, so a
light past that in a crowded part of the view does not light it. Sort by
distance to the camera and add the nearest first;
`FrameStats.LightsDropped` counts the lights a frame refused and
`FrameStats.Lights` the ones it kept, so a scene can tell when it went
over. The cascades, the spot maps and the cube faces all share one depth
atlas, so shadows cost one texture binding however many lights cast
them.

For image-based lighting, `NewEnvironment` turns an equirectangular
panorama into a light probe and `NewEnvironmentHDR` does the same for a
floating-point one, keeping its range. `DecodePanorama` reads the bytes
of whichever kind you have: an OpenEXR file (`DecodeEXR`), a Radiance
`.hdr` file (`DecodeHDR`), or an ordinary image, whose sRGB colours it
converts. The EXR reader takes scanline files with half or float
channels, uncompressed or compressed with RLE, ZIPS or ZIP, and refuses
tiled, deep, multi-part and PIZ, PXR24, B44 or DWA files with an error
that names what the file is.
`EnvironmentOptions.Intensity` scales it and `Size` sets the cube map's
side in texels (default 128). The prefilter for every roughness runs on
all cores, and the image types Go's decoders return are read straight
from their pixels. Set it as `Light.Environment` and it
replaces the ambient and the sky. Metals reflect it, rough surfaces take
its tint from every direction, and `Light.Background` draws it behind the
scene. Environments hold GPU memory; `Destroy` releases it early.
Graphics releases remaining environments when the engine closes.

```go
panorama, err := gfx.DecodePanorama(data) // .exr, .hdr, .png or .jpg
if err != nil {
	return err
}
g.env, err = ctx.Gfx.NewEnvironmentHDR(panorama, gfx.EnvironmentOptions{Intensity: 1.5, Size: 256})
light.Environment, light.Background = g.env, true
```

## Global illumination

One environment lights the whole scene, which is right outdoors and wrong
indoors: a ball in a red room reflects the sky. Three things give parts
of a scene their own light. A reflection probe is what a place reflects,
a light probe grid is what a place is lit by, and screen-space
reflections are what the screen already shows. They stack: probes for
reflections, a grid for the diffuse light, and the screen pass on top of
both.

`ReflectionProbe` captures the scene from a point into a cube map.
`Position` is where it is captured, `Extent` the half-size of the box it
covers (or `Radius` for a sphere probe), `Resolution` the cube face size
in texels (default 64) and `Intensity` a multiplier. `BakeProbe(probe,
scene)` renders six faces from that point and prefilters them for every
roughness; the `scene` function queues the draws and the light the bake
sees, exactly as `Draw` would. It runs once for each face, so it must
queue the same scene every time. Baking submits its own command buffer
and waits for it once for all six faces, so call it from `Init` or
`Update`, never from `Draw`,
and call it again when the room it holds has changed. `AddProbe` adds a
baked probe to a frame the way `AddPointLight` adds a light.

A draw reflects the probe whose volume holds its centre; everything
outside every probe keeps `Light.Environment` or the `Sky`. Of two probes
holding a draw, the one holding it more firmly wins, and the smaller of
two that hold it equally. Only one cube map is bound per draw, so two
probes are never blended with each other: `Margin` fades a probe's
reflection towards the frame's average environment over the last few
units inside its volume, which is what stops a ball popping as it leaves
a room. `BoxProjection` reflects a box probe's walls at the place the
walls are rather than at infinity, so a floor mirrors the wall it faces.
A frame keeps its first `MaxProbes` (8) probes and counts the rest in
`FrameStats.ProbesDropped`. A probe owns GPU memory; `Destroy` releases
it early. Graphics releases its remaining GPU resources at shutdown.

`LightProbeGrid` is the diffuse half: a lattice of `Counts` cells from
`Origin` every `Spacing` units, each holding the light arriving at it as
nine spherical harmonics. `BakeLightProbes(grid, scene)` renders a small
cube at every cell (`Resolution`, default 16, is enough because harmonics
keep only the low frequencies) and projects what it saw, four cells to a
submission and one wait. `SetLightProbes`
gives a frame a baked grid, which replaces the single ambient term where
it reaches, interpolated between the eight cells around each fragment and
faded back to the environment over the outer half cell. A grid holds its
harmonics in ordinary memory and needs no `Destroy`; it is at most 4096
cells.

`PostSettings.Reflections` turns on screen-space reflections, 0 (off, the
default) to 1. Smooth surfaces trace a ray through the depth buffer and
show what the screen already holds: the bright box standing on a polished
floor, a sign over wet asphalt. `ReflectionRoughness` is the roughness a
surface stops reflecting the screen at (default 0.35),
`ReflectionDistance` how far a ray travels in world units (default 30)
and `ReflectionSteps` how many samples it takes (default 32). Where a ray
leaves the screen or hits nothing the surface keeps its probe or
environment reflection, so the two fit together rather than adding up.
What is behind the camera or hidden behind something nearer is not on the
screen and so cannot be reflected; a probe is what fills those in.

```go
// In Init: one probe for the room, a grid across its floor.
g.probe = &gfx.ReflectionProbe{Position: lin.V3(0, 1.8, 0),
	Extent: lin.V3(9, 2.5, 9), Margin: 1, Resolution: 96, BoxProjection: true}
if err := ctx.Gfx.BakeProbe(g.probe, func() { g.drawRoom(ctx.Gfx) }); err != nil {
	return err
}
g.grid = &gfx.LightProbeGrid{Origin: lin.V3(-6, 0.9, -6),
	Spacing: lin.V3(6, 2, 6), Counts: [3]int{3, 2, 3}}
if err := ctx.Gfx.BakeLightProbes(g.grid, func() { g.drawRoom(ctx.Gfx) }); err != nil {
	return err
}

// In Draw: add them to the frame, and reflect the screen off the floor.
p := gfx.DefaultPost()
p.Reflections = 0.9
gr.SetPost(p)
gr.AddProbe(g.probe)
gr.SetLightProbes(g.grid)
```

Bakes are the expensive part: a probe is six scene renders and a
prefilter, a grid is six little renders a cell. Bake at load, or when a
level changes, and keep what you baked. Both bakes draw the scene through
the same code the frame does, so light the bake the way the frame is lit
or the probe will disagree with the screen. The `probes` example puts all
three on checkboxes.

## Sky, fog and atmosphere

`Sky.Space` adds a distant image environment behind the procedural sky. Create
it with `NewEnvironmentHDR` and keep `Light.Background` enabled. The same
environment intensity controls its background radiance, diffuse contribution
and reflections. Atmospheric optical depth attenuates distant light while the
foreground atmosphere and sun disc remain visible; the solid planet occludes
the distant sky. It works for stars, distant nebulae and other image backgrounds
without replacing the local atmosphere. A cube size of 512 or 1024 preserves
small background details better than the default 128, at greater preparation
time and GPU memory cost. `Light.Environment` still replaces the entire sky
when set and takes precedence over `Sky.Space`. The environment is borrowed
by the sky and can be released with `Destroy` when no longer needed.

`Light.Sky` is a procedural environment. It takes an `Up` axis,
`Zenith`, `Horizon` and `Ground` colours, and how much air there is. It
needs no image, costs nothing to change every frame, and lights the
scene the way an environment map does. With `Light.Background` the sun's
disc, sized by `SunSize` and coloured by `Sun`, its haze and the stars
are drawn behind the scene.

`Vacuum` sets how thin the air is, which is what a space game needs. At
0 the air is full. Raising it towards 1 fades the sky to black while
`Stars` come out, and the ground half stays, so a ship can climb from a
runway to orbit with no seam. Point `Up` away from a nearby planet and
set `Ground` to that planet's colour to light the ship's night side with
planet-shine.

`Sky.Atmosphere` computes the sky instead of describing it. Set its
`Height` to how deep the air is in the game's own units and the sky
above the horizon becomes single-scattered sunlight, Rayleigh from the
air and Mie from haze, in place of `Zenith` and `Horizon`: blue
overhead at noon, orange and then red along the horizon as the sun
drops, dark once the sun is under it, and thinner as `Altitude` climbs
out of the air, so a plane can fly from a runway to space with no seam.
`Ground` still lights the half below the horizon, and `Vacuum` still
scales the result. Every other field has an Earth-like default scaled to
`Height`, so `gfx.Atmosphere{Height: 3000}` is a whole sky;
`PlanetRadius` (a hundred times `Height` by default) sets how far the
horizon is, `Rayleigh` and `Mie` are the scattering per world unit at
the ground, `Forward` is how tight the glare around the sun is, and
`Intensity` is the sunlight the scattering divides up.

`Height` is the one number to think about, because it fixes the scale of
everything else: the air is as deep as the number says in world units,
so a scene a hundred units across under a `Height` of 60 is looking
through a hundred kilometres of air and washes out, while the same scene
under 3000 has crisp distance and a sky that still works. Pick it by how
far away things should start taking the air's colour, not by the size of
a real planet. The model is integrated per pixel, eight samples along
the view ray and four towards the sun at each; there is no table to
precompute, load or keep in step, and the same function runs on the CPU
in Go to project the ambient light onto spherical harmonics. Those
harmonics are reprojected when the sun or the altitude moves far enough
to matter, not every frame.

`Light.Fog` fades geometry into a colour with distance, the cheapest way
to give a scene depth and hide the far plane. Linear fog ramps from
`Start` to full at `End`; exponential fog thickens with `Density`; when
both are set the denser wins. `Height` and `HeightFalloff` add ground
fog, full at and below `Height` and thinning above it along the world's y
axis, which puts mist in a valley without touching the hilltops. The sky
is not fogged, so outdoors pick a fog colour close to the horizon's or
the join will show.

With an atmosphere the fog has help: distant geometry is dimmed by the
air in front of it and takes the light that air scatters, from the same
model, after the fog is applied. That is aerial perspective, and it is
what makes far hills go pale and blue and the ones at sunset go pink. It
costs nothing to ask for beyond the atmosphere itself, so a scene with
one often wants `Fog` only for the valley mist.

```go
// In orbit, the night side lit by the planet below.
sky := gfx.Sky{Up: planetUp.Mul(-1), Vacuum: 1, Stars: 1,
	Ground: gfx.Color{R: 0.2 * k, G: 0.3 * k, B: 0.45 * k, A: 1}}
gr.SetLight(gfx.Light{Direction: sunDir, Sky: sky, Background: true,
	Color: gfx.Color{R: 2.4, G: 2.2, B: 1.9, A: 1}})

// On the ground: air on the horizon, mist in the valley.
cam := gfx.OrbitCamera(lin.V3(0, 2, 0), yaw, pitch, dist)
light.Sky = gfx.Sky{Ground: gfx.RGB(76, 82, 64),
	Atmosphere: gfx.Atmosphere{Height: 3000, Altitude: cam.Position.Y}}
light.Fog = gfx.Fog{Color: mist, Start: 45, End: 200, Height: 0.8, HeightFalloff: 0.4}
```

## Billboards, labels and marks on the world

`DrawBillboard` puts a textured quad in the scene that turns to face the
camera. `Upright` turns it about the world's up axis only, so a tree
stays vertical when the camera looks down; `Offset` moves the quad in its
own plane in units of its size, so `(0, 0.5)` stands the sprite on the
ground; `Lit` shades it with the scene's lights; `Cutout` gives it hard
edges that write depth and cast shadows; `OnTop` draws it over
everything; `Region` takes a rectangle of an atlas. Billboards go through
the mesh path, so many with one texture become one instanced draw, and
five hundred trees are one call. `DrawText3D(font, text, pos, scale,
color, onTop, opts)` draws a line of text the same way, centred on
`pos`, with `scale` in world units per view unit of the font.

For the many small quads an effect needs rather than the few a scene
places by hand, `DrawParticles3D(tex, quads, opts)` draws a whole slice
of camera-facing quads as one instanced call: smoke, embers, snow,
magic. It runs over the finished scene, after decals, so the geometry in
front hides them, and `opts.Soft` fades a particle out over that many
world units as it nears the surface behind it, which hides the hard line
a quad otherwise cuts where it meets the ground. `opts.Blend` picks the
blend mode. The particles are neither lit nor depth sorted against each
other, so unlit and additive effects suit them. The `particle` package
fills the slice: `GPUSystem.Draw3D` simulates an `Emitter` and hands the
result straight to this call. See the 2D graphics guide for the emitter.

`DrawDecal(tex, box, tint)` projects a texture onto whatever geometry
lies inside a box, for bullet holes, blood, footprints and road
markings. The box matrix maps the unit cube to the world, the texture
projects along the box's y axis with x and z spanning the image, and it
fades on surfaces facing away. Two material fields mark a mesh rather
than the world. `Outline` draws a silhouette line of that many pixels in
`OutlineColor` through the stencil buffer, and `XRay` tints the parts of
a mesh hidden behind other geometry, so a selected unit shows through a
wall.

```go
gr.DrawBillboard(gfx.Billboard{Texture: g.tree, Position: p, Size: lin.V2(2, 3),
	Offset: lin.V2(0, 0.5), Upright: true, Lit: true, Cutout: true})
gr.DrawText3D(g.font, "Watchtower", top, 0.05, gfx.White, false, gfx.TextOptions{})
gr.DrawDecal(g.splat, lin.Translate(hit.Point).Mul(lin.Scale(lin.V3(2, 1, 2))), gfx.RGB(120, 20, 20))
sel := gfx.Material{BaseColor: gfx.RGB(90, 200, 120), Roughness: 0.5,
	Outline: 3, OutlineColor: gfx.White, XRay: gfx.RGBA(255, 60, 60, 160)}
```

## Culling and levels of detail

Every mesh draw is tested against the camera's frustum and skipped when
its bounds are outside; culled draws still cast shadows, which is why a
tree behind the camera can still darken the road. A static mesh is
bounded by the box its vertices fill. A skinned mesh keeps a box per
joint over the vertices weighted to it, and each frame the pose's joint
matrices move those boxes and the union bounds the draw, so a limb that
swings clear of the bind pose is still drawn.

Two cases need the game's help. A mesh whose drawn shape leaves its
geometry takes `Mesh.SetBounds(min, max)` to say the box it stays
inside, in mesh space; `Mesh.Bounds()` reads the bounds back, and
`Update` leaves bounds given by hand alone. A material shader with a
vertex program can put a vertex anywhere, so draws using it are never
culled until `Shader.VertexBounds` says how far the program moves one,
as a multiple of the mesh's bounding radius; culling then grows the
radius by 1 + `VertexBounds`.

```go
// A flag whose shader ripples it by a quarter of its own size.
g.flagShader.VertexBounds = 0.25
// A billboard grass mesh the shader bends and scatters over its cell.
grass.SetBounds(lin.V3(-2, 0, -2), lin.V3(2, 3, 2))
```

Engine culling still costs the work of building the draw, so a game with
chunks, regions or a crowd should test them itself first.
`Graphics.Frustum()` gives the current camera's frustum for the current
aspect, `Camera.Frustum(aspect)` gives one for any camera, and
`gfx.FrustumOf(viewProj)` builds one from a matrix; `ContainsPoint`,
`ContainsSphere(centre, radius)` and `ContainsBox(min, max)` are the
tests.

The frustum only knows what is outside the view. To skip what is inside
it but hidden, mark the geometry that blocks the view with
`AddOccluder3D(mesh, model)` or `AddOccluder3DAt(mesh, transform)`: a
wall, a hill, a building's shell. Each frame the engine rasterises the
occluders into a small depth buffer on the CPU and culls every draw
whose bounding sphere lies entirely behind it.
`FrameStats.Occluded` counts them, and they still cast shadows like any
culled draw. Adding an occluder does not draw it, so draw the mesh too,
or add a coarse box in place of geometry drawn in detail.

Occluders must be opaque and closed enough that nothing shows through
their triangles, so a fence whose gaps are a cutout texture is a bad
one. Keep them few and low-poly, since every triangle is rasterised on
the CPU and a mesh with more than `MaxOccluderTriangles` is ignored.
`SetOcclusionSize(width, height)` sizes the buffer, 256 by 144 by
default: a gap narrower than one of its pixels counts as covered, so
raise it when a real gap is being missed and lower it when the test
costs more than it saves. Fifty box occluders and a thousand draws cost
around 170 microseconds a frame at the default size.

```go
// The castle wall hides most of the town behind it.
gr.AddOccluder3DAt(g.wallBox, g.wallAt)   // a coarse box, not the wall's own mesh
gr.DrawMeshAt(g.wall, stone, g.wallAt)
```

Both tests are per draw, so a level of ten thousand rocks, crates and
lamp posts still costs ten thousand of them. `NewStaticBatch(items)`
takes a slice of `BatchItem` (a mesh, a material and a model matrix, as
`DrawMesh` takes) and builds a bounding volume hierarchy over them once;
`DrawBatch(batch)` then tests the hierarchy rather than the items and
queues only what survives. A subtree behind the camera or behind an
occluder is rejected at one node, so the ten thousand cost a few dozen
box tests, and `FrameStats.CullTests` counts them. The items that come
through are ordinary draws, instanced, sorted, lit and shadowed like any
others. A rejected subtree is walked again against the frame's shadow
maps, so an item the camera cannot see still casts its shadow into the
view, as a culled `DrawMesh` draw does. Ten thousand cubes along a strip
most of which is behind the camera fall from 220 microseconds of culling
a frame to under two.

A batch is for geometry that never moves: the hierarchy is built from
the models given and is not rebuilt, so anything that moves belongs in
`DrawMesh`. Rebuild it if mesh geometry or bounds change, and include
vertex-shader displacement in the mesh bounds before building it.
It does not own its meshes or textures, which are destroyed
as usual, and `Len` and `Bounds` report what it holds.

```go
var items []gfx.BatchItem
for _, p := range level.Props {
	items = append(items, gfx.BatchItem{Mesh: p.Mesh, Material: p.Mat, Model: p.At.Matrix()})
}
g.props = ctx.Gfx.NewStaticBatch(items) // once, at load

gr.DrawBatch(g.props) // every frame
```

`NewLOD(meshes, distances)` takes meshes from finest to coarsest and the
camera distances at which each hands over to the next, so three meshes
take two distances. A nil last mesh draws nothing beyond the last
distance, so distant scenery disappears instead of shimmering.
`DrawLOD(lod, material, model)` and `DrawLODAt(lod, material, transform)`
pick by the camera's distance to the model's origin; `LOD.Pick` lets you
choose the level yourself. Graphics releases the meshes at shutdown;
walk `LOD.Levels` and destroy them when unloading the level earlier.

The coarsest level of all is an impostor: the model baked into pictures
of itself. `BakeImpostor(model, opts)` renders the model from a ring of
directions around it into one atlas texture, and `DrawImpostor(impostor,
pos, yaw, tint)` draws the view nearest the camera as a cutout
billboard, so a distant tree costs one quad and no vertex work. Set
`Impostor.Distance` and call `DrawModelImpostor(model, impostor,
transform)` to draw the model up close and the impostor beyond.
Impostors of one model share an atlas, so a forest of them is one
instanced draw.

`ImpostorOptions` chooses `Views` (8 by default, at most
`MaxImpostorViews`), `Resolution` in pixels per view (128), the `Pitch`
each view looks down from (15 degrees, so match it to the camera's usual
elevation) and the `Light` to bake under. The bake fixes its lighting
into the atlas the same way for every view, so an impostor does not turn
its shading as the sun moves; keep them far enough away that this does
not read. It runs a frame of its own and reads the views back, so call
it from `Init` or `Update`, never from `Draw`.

```go
// In Init: pines beyond forty units become one quad each.
g.pineFar, err = ctx.Gfx.BakeImpostor(g.pine, gfx.ImpostorOptions{Views: 12, Resolution: 96})
g.pineFar.Distance = 40

// In Draw:
for _, t := range g.forest {
	gr.DrawModelImpostor(g.pine, g.pineFar, t)
}
```

```go
// A fine rock near, a faceted one far, nothing beyond seventy units.
fine, _ := ctx.Gfx.NewMesh(gfx.SphereMesh(16, 32))
coarse, _ := ctx.Gfx.NewMesh(gfx.FlatShaded(gfx.SphereMesh(5, 8)))
g.rocks = gfx.NewLOD([]*gfx.Mesh{fine, coarse, nil}, []float32{25, 70})

fr := gr.Frustum()
for _, c := range g.chunks {
	if fr.ContainsBox(c.Min, c.Max) {
		gr.DrawMesh(c.mesh, chunkMat, lin.Translate(c.Origin))
	}
}
```

## Post-processing

`SetPost` replaces the settings the post pass uses on the 3D scene.
`DefaultPost` returns the defaults and `Post` reads back the current
ones, so you can change one field without restating the rest.
`ConfigurePost` makes that edit in a closure, keeping other settings and
preserving intentional zero values:

```go
gr.ConfigurePost(func(p *gfx.PostSettings) {
	p.Bloom = 0.3
	p.Saturation = 0 // grayscale
})
```

It commits the edited copy on normal return; a panic does not commit it.

`Exposure` multiplies the scene before tone mapping. Use it when a scene
is too dark or blown out. `Bloom` is the strength of the glow around
bright pixels and `BloomThreshold` the luminance where it starts; zero
bloom skips the passes. `Reflections` and the three `Reflection*` fields
are screen-space reflections, which the global illumination section
covers. `AmbientOcclusion` is screen-space occlusion
darkening creases and contact points, 0 to 1 with a default of 0.6, over
`OcclusionRadius` world units, and `ShowOcclusion` displays the occlusion
buffer instead of the scene while you tune it. `Vignette`, `Saturation`
and `Contrast` are the grade, and `NoAntiAlias` skips the FXAA pass.

`Samples` multisamples the scene pass: 1 (the default), 2, 4 or 8,
clamped to what the GPU supports, which `Graphics.MaxSamples` reports.
Every triangle edge is then resolved from that many coverage samples,
which is the one anti-aliasing that does not blur the picture, at the
cost of that many times the scene's colour and depth memory and
bandwidth. Shading still runs once a pixel, so the cost is in the
attachments rather than in the fragment programs. Set `NoAntiAlias` with
it: FXAA over an already resolved image only softens it again.
`TemporalAA` is the other choice and resolves the same edges over time,
so the two are alternatives rather than a pair: leave `Samples` at 1 when
it is on.

```go
p := gfx.DefaultPost()
p.Samples, p.NoAntiAlias = 4, true
gr.SetPost(p)
```

Changing `Samples` rebuilds the scene targets and the pipelines that draw
into them at the start of the next frame, so it belongs in a settings
menu rather than in a per-frame update. Everything after the scene pass
reads the resolved single-sample images, so ambient occlusion, decals,
reflections and the transmission snapshot behave the same at every sample
count; the depth they read is sample zero of each pixel, which is exact
except on an edge, where it is one of the surfaces covering it. The
order-independent transparency pass keeps its own two images at one
sample and tests against that resolved depth, so translucent edges stay
as hard as they are at one sample while everything opaque smooths.

`PostSettings.LUT` grades the finished colours through a lookup table.
`NeutralLUT(n)` returns the identity strip of n slices (16 or 32 are
usual); paste it into a corner of a screenshot, grade that in an image
editor, crop the strip back out and load it with `NewLUT`, and every
frame gets the same grade, blended in by `LUTStrength`.

```go
p := gfx.DefaultPost()
p.Exposure, p.Vignette = 1.2, 0.25
p.Bloom, p.BloomThreshold = 0.3, 1.1
p.AmbientOcclusion, p.OcclusionRadius = 0.7, 0.8
p.LUT, p.LUTStrength = g.coldGrade, 0.8
gr.SetPost(p)
```

Post applies to the 3D scene, not to the 2D drawn over it. A HUD is not
bloomed, tone-mapped or graded. That keeps text readable, and it
explains why a sprite over the scene can look brighter than the scene
does. A frame with no 3D draws in it can go through the composite as
well; see [2D graphics](graphics-2d.md). The `lighting` example puts
all of these on sliders.

### Temporal anti-aliasing

`TemporalAA` averages each frame with the ones before it. The projection
moves by a fraction of a pixel each frame along a Halton sequence, so
successive frames sample a different point inside every pixel, and the
resolve blends the last resolved frame into this one after reprojecting
it. `TemporalBlend` is how much of the new frame goes in, 0.02 to 1;
zero means 0.1, and lower is steadier and softer. It replaces FXAA while
it is on, so `NoAntiAlias` does not apply.

Reprojection needs to know where every pixel was last frame. The camera's
part comes from the depth buffer. An object's own motion has to be told,
because immediate-mode drawing has no identity across frames to look a
previous transform up by:

```go
gr.DrawMeshMoved(ship, shipMat, at(now), at(before))
```

`DrawMeshMoved`, `DrawSkinnedMoved`, `DrawModelMoved` and
`DrawModelAnimatedMoved` each take the transform the draw had last
frame; the two model forms take a `MaterialOverride` after it, as
`DrawModelWith` does, and nil draws the file's own materials. Plain `DrawMesh` says the mesh did not move, which is what a
static scene wants and what costs nothing: a frame where nothing moved
draws nothing into the velocity buffer. A moving mesh drawn through
`DrawMesh` still resolves, because the neighbourhood clamp will not let
the history stray far from the pixels around it, but it softens while it
moves. A skinned mesh carries its model matrix's motion and not its
pose's, so a character walking across the screen reprojects and an arm
swinging in place does not.

### Depth of field, motion blur and god rays

`FocusDistance` is how far in front of the camera the image is sharp, in
world units; zero turns depth of field off. `FocusRange` is how far
either side of it stays sharp before the blur grows, and how far past
that the blur reaches its full width; zero means a quarter of the focus
distance. `BokehRadius` is that full width in pixels of a 1080-high
frame (zero means 12) and `BokehSamples` how many taps the disc gathers
(zero means 16). A wide bokeh wants more of them: the disc is the same
in every pixel, so too few taps over a large radius leave a visible
pattern on fine detail. Turning the disc per pixel would break that into
noise, but it scatters the texture fetches and costs about three times
as much, so raising `BokehSamples` is the better trade.

`MotionBlur` smears each pixel back along the way it moved since the last
frame, 0 to 1; zero is off, and `MotionSamples` is how many taps it takes
(zero means 8). It reads the same velocity buffer, so an object blurs
along its own path only when it was drawn with one of the `Moved` calls;
the camera's motion always works.

`GodRays` is the strength of the shafts the directional light throws past
an occluder. Each pixel walks towards the sun's place on screen through
the depth buffer, gathering the steps where the sky shows through:
`GodRayDecay` is how fast a shaft fades along its length (zero means
0.96), `GodRayDensity` how far towards the sun the walk goes (zero means
0.6) and `GodRaySamples` how many steps it takes (zero means 32). The
pass is skipped when the sun is beside or behind the camera, and an
orthographic camera has no sun position to walk towards, so it gets none.

### The lens

Four settings model the camera rather than the scene, and all four happen
inside the composite, so together they cost about as much as a fifth of
the bloom.

`Aberration` splits the red and blue channels apart towards the edge of
the frame; 1 is about three pixels at the edge of a 1080-wide frame and
0.5 is a subtle fringe. `Distortion` bends the image about the centre,
positive for barrel and negative for pincushion. `Ghosts` draws the
bright pass mirrored through the centre a few times over, the reflections
a lens makes of a bright light, and needs `Bloom` above zero because that
is the image it reads. `Grain` adds per-pixel noise that moves each
frame; 0.05 is subtle. Every one of them is off at zero.

```go
p := gfx.DefaultPost()
p.TemporalAA, p.TemporalBlend = true, 0.1
p.FocusDistance, p.FocusRange, p.BokehRadius = 12, 4, 16
p.MotionBlur = 0.5
p.GodRays = 0.8
p.Aberration, p.Distortion, p.Grain = 0.6, 0.15, 0.03
gr.SetPost(p)
```

Roughly, at 1280 by 720 on an RTX 4090, over a frame that costs 40
microseconds with none of them on: the lens effects together 3, bloom
and FXAA 14 each, god rays 20, motion blur 24, ambient occlusion 32,
temporal anti-aliasing 35 and depth of field 42. `BenchmarkPost` in the
`gfx` package measures them; the numbers are best of five over a scene
of two dozen instanced cubes, so they are the passes' own cost rather
than a game's, and they move by a few microseconds between runs.

## Render textures

`NewRenderTexture(w, h)` makes an offscreen surface;
`NewRenderTextureOptions` adds `Nearest` for a low-resolution scene that
should stay sharp when scaled up and `Repeat` for one that tiles.
`DrawTo(rt, clear, draw)` runs the closure with that surface as the
output. Every `Draw*`, `SetCamera` and `SetLight` call inside it lands on
the texture, with its own camera and its own lighting. It renders before
the main frame, so the result can be drawn in the same frame.
`RenderTexture.Texture()` is the texture to draw with, `SetView` sets its
2D coordinate space, and `Read` copies the pixels back.

`RenderTextureOptions` also chooses what the surface is made of.
`Format` picks the colour format: `ColorScreen` matches the window, eight
bits a channel with sRGB encoding, and is the default; `ColorHDR` is
sixteen-bit floating point RGBA, so values above 1 survive into whatever
reads the texture; `ColorMask` is one eight-bit channel, for a mask, a
height field or a coverage buffer. `NoDepth` leaves out the depth buffer
of the surface's own pass, which nothing tests against, and saves the
memory; a 3D scene drawn into it still works, because the scene has a
depth buffer of its own. `Samples` multisamples the surface itself, so
every edge drawn into it, 2D paths and triangles included, is resolved
from that many coverage samples; it is separate from
`PostSettings.Samples`, which multisamples the 3D scene behind the
composite, here as on screen.

`Read` decodes whatever format the surface was made with: a `ColorHDR`
surface comes back encoded the way the screen is, so values above 1 clip,
and a `ColorMask` surface comes back as grey. `ReadDepth` returns the
depth the last 3D scene left, one float a pixel from the top-left corner,
0 at the near plane and 1 at the far plane. Both wait for the GPU and
copy the whole image to the host, so they belong in tools, tests and
one-off queries rather than in a frame.

```go
// A mask for a fog-of-war lookup: one channel, no depth, cheap to sample.
mask, err := gr.NewRenderTextureOptions(256, 256, gfx.RenderTextureOptions{
	Format: gfx.ColorMask, NoDepth: true,
})
// A portrait with smooth edges on its own vector frame.
portrait, err := gr.NewRenderTextureOptions(512, 512, gfx.RenderTextureOptions{
	Samples: 4,
})
```

```go
// A minimap: the same world from straight above, drawn as a sprite.
gr.DrawTo(g.minimap, gfx.RGB(5, 5, 12), func() {
	gr.SetCamera(gfx.Camera{Position: lin.V3(0, 400, 0.01), Target: lin.V3(0, 0, 0), Ortho: 250})
	gr.SetLight(light)
	g.drawWorld(gr)
})
// ... the main scene ...
gr.DrawTexture(g.minimap.Texture(), ctx.Width-236, 16)
```

The same call makes a portal (a camera at the far end of the pair, the
texture as the portal surface's albedo), a mirror, or a character
portrait. To put a 3D character on a 2D field, render the character with
a transparent clear, then draw that texture as a sprite on its layer.

## Debug drawing

`DrawLine3D(a, b, c)` draws a line in the world, `DrawWireBox(min, max,
c)` outlines an axis-aligned box, `DrawWireCube(m, c)` outlines the unit
cube under a matrix (the shape of a `phys.Box3` collider),
`DrawWireSphere(centre, radius, c)` a sphere, `DrawWireFrustum(cam,
aspect, c)` another camera's view volume, and `DrawAxes(m, size)` a
transform's three axes in red, green and blue. All of them ignore depth,
so they show through geometry. `DebugText(x, y, text)` and `Debugf` print
in the engine's own font with no font to load, and `DebugText3D(p, text)`
puts that text at a world point. `gfx.FrustumCorners(viewProj)` returns a
view volume's eight world-space corners.

```go
gr.DrawWireBox(body.Min, body.Max, gfx.RGB(0, 255, 0))
gr.DrawLine3D(muzzle, muzzle.Add(dir.Mul(50)), gfx.RGB(255, 80, 0))
gr.DrawAxes(t.Matrix(), 1)
scout := gfx.Camera{Position: lin.V3(30, 10, 25), Target: lin.V3(10, 0, 5), Far: 30}
gr.DrawWireFrustum(scout, 16.0/9, gfx.RGB(255, 230, 50))
gr.DebugText3D(scout.Position, "scout")
```

## Performance

`ctx.Stats` and `Graphics.Stats()` report the last frame as a
`FrameStats`: `Draws3D` is mesh draw calls after instancing across all
passes, `Instances` is mesh instances in the main pass, `ShadowDraws` is
the instances recorded into the shadow maps, `Culled` is the draws
skipped as out of view, `Occluded` how many of those an occluder hid
rather than the frustum and `CullTests` how many bounding volumes were
tested to decide, `Lights` and `LightsDropped` are the point and spot
lights the frame kept and threw away, `ProbesDropped` is the reflection
probes added past `MaxProbes`, `Draws2D` and `Vertices2D`
cover the sprite stream, and `Waits` counts the times the frame stopped
for the GPU to go idle, which a running game keeps at zero. The F3
overlay shows them and `Config.DrawBudget` warns when a frame goes over
a number you set.

```go
s := gr.Stats()
gr.Debugf(10, 10, "draws %d  instances %d  shadow %d  culled %d",
	s.Draws3D, s.Instances, s.ShadowDraws, s.Culled)
```

`Graphics.Resources()` lists every texture, mesh, model, font, render
texture and environment the context has made and not destroyed, with
sizes and an estimate of the GPU memory each holds: what to print when a
scene is using more memory than it should, or to check that a level
teardown freed what it loaded. The [debug console](console.md) shows
the same list with a running total.

On MoltenVK devices using Vulkan's portability subset, each index buffer
gets a separate allocation of the size required by the driver, bound at
offset zero. This compatibility path prevents indexed geometry from
disappearing on affected drivers; vertex buffers and other small resources
still share memory blocks. Each live index buffer counts toward the
device's `maxMemoryAllocationCount`, including replaced geometry waiting
for the GPU to finish. Many small meshes or repeated CPU morph uploads can
therefore reach the allocation limit even when their total byte size is
modest. Reuse geometry where possible, destroy resources when finished,
and handle mesh creation and update errors. The renderer returns an error
before exceeding the reported allocation limit.

Draw calls cost more than triangles. A high `Draws3D` next to a low
`Instances` means batching is breaking. Merge static geometry with
`AppendMesh` or share a material across a crowd to collapse the calls.
After that, look at the costs in order: shadows, where halving
`ShadowDistance` doubles the effective resolution at no cost, every
shadowed spot light is another pass and every shadowed point light is
six; lights, which are per-fragment work over the part of the view their
range reaches; transmission, which copies the scene before
drawing transmissive meshes; post, where ambient occlusion and bloom are
full-screen passes a zero turns off; multisampling, which multiplies the
scene's colour and depth bandwidth by its sample count; and render
textures, which are whole extra frames.

Transparency sorts by distance to the camera per draw, not per triangle,
so two blended meshes that interpenetrate pick an order and keep it, and
a large one sorted by its origin can land on the wrong side of a small
one. `PostSettings.OrderIndependent` is the way out: it accumulates every
translucent fragment with a weight that favours the nearer one and
resolves them in one pass, so a crossing looks right on both sides
without any sorting at all. It costs two images the size of the frame and
one pass, and it is an approximation: a scene of many overlapping layers
comes out slightly flatter than compositing them in order would. Where it
is off, prefer `AlphaCutoff` where hard edges are acceptable, keep
blended geometry convex and small, and use `NoDepthWrite` for additive
effects where order does not matter.

`Transmission` keeps the sorted path either way, because refraction reads
the scene behind the surface and so has to be drawn after it. A frame can
have both: the copy of the scene that glass and screen-space reflections
read is taken from the opaque draws, the order-independent pass resolves
over it, and the transmissive draws follow in sorted order. So glass
refracts the opaque scene, not the translucent surfaces in front of it.

Meshes, models, textures, environments, render textures, shaders and
fonts all hold GPU memory. The renderer owns everything it creates and
releases it when the game closes, including when setup or drawing fails.
Use `Destroy` to release a resource earlier, such as when unloading a
level. Call it from `Init`, `Update`, `Draw` or `Shutdown` on the same
goroutine, never from another.
Destroying inside a frame costs no wait: the object goes on that frame
slot's retire list and is freed a couple of frames later, once the GPU
has finished with it, so what was already queued still draws. A model's
morph buffers and their descriptor sets follow the same lifetime, and
calling `Destroy` again is harmless. Uploads
inside a frame are the same shape: `NewMesh`, `Mesh.Update`,
`NewTexture`, `Texture.Write` and `NewEnvironment` copy through a
staging arena into the frame's own command buffer, and what a frame
uploads is what that frame draws. Outside a frame, in `Init` or between
frames, the same calls go into one batch that the renderer submits ahead
of the next frame, so loading a thousand meshes and textures waits for
the GPU no more than loading one.
