API Reference · Scene and camera

Scene

Namespace: Ava3D

public sealed class Scene : Node

The scene: a root transform, its nodes, its lights and a background colour.

Everything here lives on the UI thread. Scene.Version ticks on every change, and the control uses that to decide when to hand the renderer a fresh snapshot — so a static scene costs nothing per frame and an animated one costs exactly one rebuild per mutation batch.

Properties

MemberDescription
IList<Animator> Animators { get; }

The animators playing over this scene. GltfLoader.LoadModelInto adds one for a file that carried clips; anything built by hand can add its own.

Ava3DView advances everything in here once per rendered frame, so the ordinary case needs no clock of its own. A caller driving time itself — a recorder, a test, a scrubbing slider — should leave the list empty and call Animator.Advance directly, or the two clocks will both be moving the same playhead.

A plain list rather than one of the collections that invalidate on change: an animator writes to nodes, and those writes invalidate the scene by themselves.

Color Background { get; set; }

What the viewport is cleared to before anything is drawn.

float BloomIntensity { get; set; }

How much of the blurred bright pass is added back. Zero — the default — switches bloom off entirely, and is the only value at which none of the work is done.

Around 0.3 is a glow you notice without naming; 1 is a bright haze; above 1 the bloom clips to white and spreads, which is occasionally what a signage or an emergency strip wants.

float BloomRadius { get; set; }

How far the glow spreads, as a standard deviation in device pixels. 16 by default.

In pixels rather than in world units, because that is what it is: a lens effect belongs to the picture, not to the scene. A radius in world units would make a lamp's glow shrink as the camera backed away, which is the opposite of what happens.

float BloomThreshold { get; set; }

How bright a pixel has to be before it blooms, from 0 for everything to 1 for only pure white. 0.75 by default. Below Scene.BloomIntensity being above zero, nothing happens either way.

The threshold is read against the finished picture rather than against the scene's radiance, and that is the one thing worth knowing before tuning it. Bloom here is a compositing step: the frame is drawn, tone-mapped and gamma-encoded first, and what blooms is what came out bright on screen. Doing it properly means blurring the linear image before the tone curve, which needs a second render target on four backends; this needs none and is what almost every real-time bloom did for a decade.

What it costs is that the tone curve has already compressed the highlights, so a lamp a hundred times over range and one twice over range both arrive near white and bloom about the same. What it buys is a lamp that reads as lit rather than as a bright patch painted on, which is the whole point.

All of that describes the default picture. With Scene.LinearHdr on, bloom is taken from the linear image before the tone curve after all, this threshold is not read, and Scene.HdrBloomThreshold takes its place.

ContactOcclusionQuality ContactOcclusion { get; set; }

Screen-space contact shading from opaque camera-visible meshes. Off by default. Authored AO and contact visibility combine by minimum, so they do not multiply-darken cavities. Transparent and alpha-cutout surfaces do not write this depth pass. RenderInfo.ContactOcclusionStatus reports what is active or why not; RenderInfo.Features lists renderers without support. No temporal history or jitter is used.

float ContactOcclusionRadius { get; set; }

Maximum contact distance in world units. Default 0.4; choose for the model's scale.

float ContactOcclusionStrength { get; set; }

Contact shading strength from 0 (disabled, no depth pass) to 2. Default 1.

float CullingMargin { get; set; }

How far outside the camera's view a node may be and still be submitted, in world units. Zero by default.

The escape hatch for geometry that is drawn somewhere its bounds do not describe — a vertex shader that displaces, a sprite grown by a property the bounds were computed before. Nothing in this library needs it, which is why it is zero; it is here so that a consumer who finds an edge does not have to turn the whole feature off to get past it.

MaterialDebugView DebugView { get; set; }

Replaces mesh shading with a material or lighting diagnostic. Data views bypass exposure, fog and tone mapping; light views use exposure and tone mapping. Sprites and lines are unchanged. The interactive CPU fallback samples maps per vertex; use OfflineRenderer.RenderShaded for per-pixel material inspection. None restores ordinary shading.

DetailLayer Detail { get; }

The ambient hemisphere. Without it metals have nothing to reflect; see EnvironmentLight.

EnvironmentLight Environment { get; set; }

The ambient light everything sits in: a sky and ground colour, or an image to reflect.

Never null — assigning null restores the default hemisphere rather than removing it, so a scene always has something filling in the light its lamps miss.

float Exposure { get; set; }

Multiplies the lit colour before the tone map. One by default.

The one control that moves an entire scene's brightness without touching a light, a material or an environment — which is what makes it the right knob for "this is too dark" and the wrong one for "this lamp is too dim". Below one darkens, above one brightens and pushes more of the picture into the shoulder of the tone curve, where highlights compress instead of clipping.

Vector3 FogColor { get; set; }

The colour distant geometry fades into, in linear RGB like every other colour on a scene. Has no effect until Scene.FogEnd is above Scene.FogStart.

In the default pipeline fog is mixed in after the tone map, so this colour is neither exposed nor tone mapped: it appears as its own sRGB encoding whatever Scene.Exposure is. That is what lets distant geometry disappear into Scene.Background exactly — give the background's colour converted to linear, each channel (byte / 255)^2.2, and a surface at Scene.FogEnd is the background's pixel. Giving the background's bytes divided by 255 instead leaves distant geometry visibly lighter than the sky behind it.

With Scene.LinearHdr on, fog is mixed into the linear image before exposure and the tone map, like light, and comes out exposed and tone mapped with everything else.

float FogEnd { get; set; }

How far from the camera fog is complete. At or below Scene.FogStart there is no fog at all, which is the default and the way fog is switched off.

float FogStart { get; set; }

How far from the camera fog begins. Nothing nearer than this is touched.

bool FrustumCulling { get; set; }

Whether geometry the camera cannot see is skipped rather than submitted. On by default.

A draw call costs between two and ten thousand triangles on the renderers here — measured, on both the Metal and the OpenGL paths — so submitting a room that is behind the camera is the most expensive thing a scene can do. This tests each node's bounds against the camera's six planes and leaves out what cannot appear.

It changes what is submitted and never what is drawn. Shadows are unaffected: the depth pass takes its casters from the scene rather than from the camera's draw list, so something behind the camera still casts into the view. And a node is skipped only when its whole bounding box is outside one plane, which is conservative in the direction that matters — a little extra submitted costs a draw call, a little too much skipped is a hole in the picture.

Turn it off to compare, or for a scene whose geometry is drawn somewhere other than where its bounds say it is.

float HdrBloomThreshold { get; set; }

Linear luminance threshold for bloom while Scene.LinearHdr is on, before exposure. Default 1, clamped to 0..65504. Unlike the compatibility Scene.BloomThreshold, this can exceed one so ordinary white need not bloom. Scene.BloomIntensity and Scene.BloomRadius still set strength and spread; bloom stays off while either is zero.

DirectionalLight Light { get; set; }

The key light: the first DirectionalLight in Scene.Lights.

This is how the scene was lit before there could be more than one, and it still works — assigning replaces the first directional light instead of appending. Scenes wanting a second light, or a PointLight, use Scene.Lights directly.

Reading is a read: it never throws, never adds a light, and never ticks Scene.Version. That is worth saying because it briefly was not — a getter that lazily added a light bumped the version and forced a snapshot rebuild from what reads as a plain field access, and threw outright when Scene.Lights was full of point lights. The one case where nothing can be found, a scene whose directional light has been removed from Scene.Lights, returns the light the scene was created with. It is a real object with the values it last held, so nothing here can fail; putting it back in Scene.Lights is what makes it shine again.

LightCollection Lights { get; }

Every light in the scene. A new scene starts with one DirectionalLight in it, which is what Scene.Light returns.

Lights are plain mutable objects the graph cannot observe, so call Scene.Invalidate after changing one's fields — adding and removing them through this collection does it for you.

bool LinearHdr { get; set; }

Renders and blends scene colour in a linear floating-point target, applies bloom, then exposure, white balance and tone mapping. False retains the inexpensive compatibility pipeline. Material diagnostic views bypass this pipeline. RenderInfo.LinearHdr and RenderInfo.ColorPipelineStatus report whether the renderer actually applied it.

int MaxShadowedLights { get; set; }

Maximum simultaneous shadowed lights, from zero to two. One by default; zero draws no shadows. Scene.ShadowMapSize controls each map's resolution; a point light needs six faces. RenderInfo.ShadowedLights reports how many the renderer actually applied.

float ShadowBias { get; set; }

How far towards the light a surface is pushed before it is compared against the map, as a fraction of the map's own depth range. 0.0015 by default.

This number exists because a shadow map is a depth taken at one resolution being asked a question at another, and the two disagree on every surface that is not square-on to the light. Too small and a lit surface shadows itself in bands — acne, and it looks like moiré crawling as the camera moves. Too large and a shadow detaches from whatever is casting it, so a figure floats above its own shadow. There is no value that is right for every scene, because the error scales with how oblique the surface is and how much world the map covers.

The default is tuned for a room-sized scene at 2048. A scene that is kilometres across wants more; a scene of one small object wants less. Most of the slope-dependent part is already handled — the comparison also offsets along the surface normal by a texel's worth of world, which is what removes the acne a constant alone cannot.

Node ShadowCasters { get; set; }

The subtree the shadow map is built from, or null — the default — for the whole scene.

What this is for. One map has one frustum, and the frustum has to enclose everything that casts. So the map's texel size is set by the largest thing casting anywhere in the scene, however far it is from what the camera can see: a corridor thirty-eight metres long spreads a 2048 map over thirty-eight metres, and the room the viewer is standing in gets the few texels that fall in it. Naming a subtree here shrinks the frustum to that subtree, which shrinks the texels with it, and takes everything outside it out of the depth pass as well.

Why a node rather than a flag on each mesh.MeshNode.CastsShadow already says whether one mesh casts, and it stays the right answer for "this pane of glass does not". It is the wrong shape for "only this room casts, and which room that is changes as somebody walks", because that is a property of the frame rather than of the mesh: answering it through the flags means writing to hundreds of nodes every time a door opens, and every one of those writes invalidates the scene. One assignment does the same job and rebuilds the snapshot once.

The node does not have to be in this scene's graph, and nothing enforces that it is — a node that is not reached during the walk simply contributes nothing, which is the same outcome as an empty subtree and reads the same way from a picture: no shadows. Set it to null to go back to the whole scene.

It scopes the casters and not the receivers. Everything visible still receives, because the depth range is fitted to what a shadow could land on rather than to what casts — see ShadowView.For, which takes both boxes for exactly this reason.

Light ShadowCastingLight { get; }

The highest-priority selected casting light, or null when shadows are disabled.

IReadOnlyList<Light> ShadowCastingLights { get; }

Selected casting lights, ordered by priority and then scene order. A backend with fewer available shadow samplers reports its fallback through RenderInfo.

int ShadowMapSize { get; set; }

The square edge of the shadow map, in pixels. 2048 by default. Zero or less switches shadows off as surely as no light asking for them, though Scene.ShadowsEnabled is the switch to use for that, because it remembers the size this was tuned to.

Clamped to 64 at the bottom and 8192 at the top. The bottom is where a map stops describing anything — a room at 32 pixels across is four or five texels per wall — and the top is the largest square every backend here can be relied on to allocate. Nothing rounds it to a power of two; the backends allocate what is asked for.

What it costs is memory and one extra pass over the casting geometry per frame. 2048 is 16 MB of depth on a GPU and the same in managed memory on the software renderer, which is why it is a property rather than a constant: a scene showing one object on a turntable wants 1024 and a corridor wants more.

float ShadowStrength { get; set; }

How dark a shadowed surface goes, from 0 for no shadow at all to 1 for the light fully blocked. One by default.

Below one is not physical and is occasionally what a scene wants anyway: a shadow that keeps some of its light reads as a room with bounce in it, and this is much cheaper than the bounce. It scales the shadow term only, so ambient, the environment and every other light are untouched — a fully shadowed surface is still lit by everything that is not the casting light.

bool ShadowsEnabled { get; set; }

Whether shadows are drawn at all. True by default, and the switch an application puts in front of a person.

There are three ways to have no shadow and they are three different statements. Light.CastsShadows is the scene saying which lamp is the one worth a map; Scene.ShadowMapSize is the budget that lamp is given; this is somebody deciding they do not want the feature today. Keeping them apart is what lets a settings checkbox turn shadows off and back on without the scene losing which light casts or how large its map was tuned to be — setting the size to zero and back would restore a number, not the number that was there.

Off costs nothing rather than nearly nothing: Scene.ShadowCastingLight reports null, so the snapshot carries no shadow view, no backend allocates a map, and the extra pass over the casting geometry does not happen. A scene with this false renders what the same scene rendered before shadows existed, to the frame time.

int Version { get; }

Ticks on every change to the graph. The control watches it to know when to re-snapshot.

float Vignette { get; set; }

How much the corners of the picture are darkened, from 0 for none to 1 for black. Zero by default.

The cheapest of the three and the one with no physical justification at all: it is a lens artefact that photography spent a century trying to remove and cinema then put back, because a darkened border holds the eye in the middle of the frame. It costs one gradient drawn over the finished image.

Vector3 WhiteBalance { get; set; }

Linear RGB gains before tone mapping. Applies only while Scene.LinearHdr is on. (1,1,1), the default, is neutral; each channel is clamped to 0..16. For example (1.1,1,0.9) warms the image. Does not alter Avalonia controls drawn over the view.

Events

MemberDescription
event EventHandler Changed

Raised on the UI thread whenever anything in the scene changes.

Methods

MemberDescription
void Advance(double seconds)

Moves every animator in Scene.Animators on by seconds.

Called by Ava3DView from the compositor's frame callback. Calling it yourself as well runs the clips at double speed rather than doing nothing, which is the usual sign that a caller meant to drive one animator directly instead of registering it.

IEnumerable<ValueTuple<MeshNode, Matrix4x4>> EnumerateMeshes()

Every MeshNode in the graph, with its accumulated world transform.

void Invalidate()

Marks the scene changed. Call this after mutating something the graph cannot observe — the fields of a Material or a DirectionalLight, for instance.

See also