API Reference · Controls

Ava3DView

Namespace: Ava3D

public sealed class Ava3DView : Controls.Control

A 3D view that works on every platform Avalonia targets, including the browser.

It renders through Avalonia's public SkiaSharp graphics lease rather than through OpenGlControlBase — which does not exist on the web platform — into its own framebuffer, so it composites like a normal control: clipping, transforms, opacity and z-order all behave. Where the platform offers OpenGL it uses it, where it offers Metal it uses that, and where it offers neither it falls back to drawing triangles through Skia rather than showing nothing.

Properties

MemberDescription
bool AutoFit { get; set; }

Frame the scene automatically the first time it has any extent. On by default.

Camera Camera { get; }

The camera. Mutable and owned by the caller — move it, then call Ava3DView.InvalidateCamera, or just let the built-in orbit input drive it.

int CaptureQueueCapacity { get; set; }

How many captures may be in flight on this view at once — queued for a frame, or on their way back from the device. Eight by default. A request past the limit throws CaptureQueueFullException from Ava3DView.CaptureAsync rather than being dropped, so a caller recording a sequence can pace itself on the tasks it already holds.

Color ClearColor { get; set; }

What the view is painted in before there is a scene, and for as long as Ava3DView.Scene is null. A scene's own Scene.Background takes over from its first frame.

The default is the dark blue-grey the empty view has always had. Set it to the colour of whatever the view sits on, and the moment before the first frame stops being a visible rectangle — and Ava3DView.FirstFrameRendered says when that moment has passed.

RenderInfo Info { get; }

What the renderer is doing. Refreshed a few times a second.

bool IsAnimationEnabled { get; set; }

Whether the control advances the animators in Scene.Animators once per rendered frame. True by default.

Turn it off for a caller that drives time itself — a recorder rendering thirty frames a second of a scene the display is showing at 120, a test pinning a pose, a slider being scrubbed. Both clocks running is not a subtle fault: the clips play at whatever the sum of the two rates is.

bool IsOrbitEnabled { get; set; }

Left-drag orbits the camera.

bool IsPanEnabled { get; set; }

Middle- or right-drag moves what the camera looks at.

bool IsPickingEnabled { get; set; }

Whether a click raises Ava3DView.Picked.

bool IsZoomEnabled { get; set; }

The wheel moves the camera closer and further.

double MaxFrameRate { get; set; }

The most frames a second this control will draw. Zero, the default, is uncapped.

A separate lever from , and all four combinations are real. Continuous with a cap is a low-rate heartbeat, for a view that must catch a change nothing announced. On demand with a cap is a game view that goes quiet when parked and holds thirty while it is playing. One enum could not express both without four members and a payload, and two scalars in a style say it in two lines.

An absolute rate, not a division of the display's. Unity's renderFrameInterval divides the refresh rate, and this control cannot see the refresh rate — Avalonia raises ScalingChanged for a move between monitors and nothing at all for a change of refresh. So a 2 would mean thirty on one screen and sixty on the next, and 30 means thirty on both. The sentinel matches Engine.max_fps in Godot and t.MaxFPS in Unreal.

Suspended for the length of a drag. A capped view that lags the pointer reads as a broken control rather than as a saving. RenderInfo.MaxFrameRate reports what is actually in force, so it reads zero while the user is orbiting.

The cap only ever removes frames. It cannot add one, so a rate above what the display offers does nothing at all, and a view already idle under RenderTrigger.OnDemand is not made idler by it. Frames arrive only when the compositor offers one, so the achievable rates are the display's divided by a whole number: a cap of 24 on a 60 Hz panel delivers 20, because the alternative is 30 and 30 is more than was asked for.

It caps rendering, not repainting. A compositor also draws a visual when the window around it changes — a panel over the viewport, a host's overlay — and nothing here can decline that. What this control can do is answer such a repaint with the frame it already has: a repaint that arrives before the next frame is due shows the last rendered frame again, and only a due one renders. So a view sharing a window with something that repaints on its own no longer keeps that thing's rate as a floor; it keeps its cap, and RenderInfo.FramesReused counts the repaints it served without rendering. The retained frame is dropped when the control is detached, resized or handed to a different renderer, and the next repaint renders whatever the cap says — a hole is never the answer.

RenderBackendKind PreferredBackend { get; set; }

Which renderer to use. RenderBackendKind.Automatic by default, which is right unless the application is deliberately offering the choice.

Switching between the GPU and RenderBackendKind.Software takes effect on the next frame, because that choice belongs to this control. Switching between GPU APIs does not: Avalonia fixes its graphics API when the application is built, so asking for OpenGL in a process that was started with Metal leaves the picture alone and reports the request through RenderInfo.AvailableBackends as needing a restart. Read that list rather than assuming a selection took — it is also where each platform says which renderers it cannot offer at all, and why.

double RenderScale { get; set; }

How many pixels to render for each pixel shown, along each axis. One by default; two renders four times as many and shows the average of them.

This is the anti-aliasing control. Every renderer here rasterises one sample per pixel, so a silhouette is a hard step and, once anything moves, that step crawls from one pixel to the next. Nothing about the geometry, the mip chain or the map's resolution touches it — the edge is where two triangles meet and there is one sample to decide it with. Rendering larger and letting the composite average it down is the oldest fix there is and the only one that works identically on all four backends, including the software one, which has no multisampling to offer.

What it costs. The square. 1.5 is 2.25× the fill and 2 is 4×, and fill is most of a frame at any real window size, so this is a knob to turn up until the frame time says stop rather than a default to raise. Measured on a consumer's interior scene at 2: pixels changing by more than a fifth between two frames of a slow pan fell from 1.91% to 1.18%, and the difference against a 3× reference fell with it.

Where it stops. The target is capped by the same two guards every other size is — a total pixel budget and a maximum side, and then whatever the backend says it can allocate. Asking for more than that is not an error and does not fail: the frame comes out at the largest size that fits, which is what RenderInfo.Size reports. Values below one render smaller than the control and are a legitimate way to buy frame time back on a weak device.

A capture's own scale — see CaptureRequest.Scale — multiplies with this rather than replacing it: a capture asking for four times the window on a view already supersampling twice gets eight, rendered for the capture alone.

RenderTrigger RenderTrigger { get; set; }

When this control draws: every compositor frame, or only after something changed. Defaults to RenderTrigger.Continuous, which is what it did before this property existed.

RenderTrigger.OnDemand is what an application wants on the screens where the scene is not moving, and the enum documents exactly what counts as a change. Orbit, pan and zoom still track the pointer under it, because moving the camera is one of them.

ResourcePolicy ResourcePolicy { get; set; }

How long this view's device keeps textures and meshes nothing is drawing, how much it may keep, and how much of each frame it spends copying new ones up. Null — the default — applies Resources.DefaultPolicy.

The policy is the device's, not this view's alone: every view on the same graphics device draws from one set of resident resources, and where their policies differ the most permissive value of each field is in force — the largest budget, the longest grace, the most upload time. Read once when assigned; assign again to change it.

int SampleCount { get; set; }

How many samples per pixel to rasterise with: 1, 2, 4 or 8. One by default.

The cheaper half of the same job does. Multisampling shades once per pixel and only tests coverage per sample, so it fixes the silhouette — which is where nearly all of the crawl is — for a fraction of what rendering the whole frame larger costs. What it does not fix is anything inside a triangle: a shimmering texture or a specular sparkle is unaffected, because there is still one shade per pixel. For those, Ava3DView.RenderScale is the lever.

Not every renderer can. Metal takes it. OpenGL takes it on ES 3 and desktop GL 3.0 and above, which is every context but GL ES 2. Vulkan and the software renderer do not, and neither does a device that will not allocate the count asked for. None of that is an error and none of it changes what is drawn — the frame comes back at one sample. RenderInfo.SampleCount reports what was granted, and the renderer's feature list says why.

Scene Scene { get; set; }

The scene to draw. Mutate it freely on the UI thread; the view keeps up.

bool SoftwareThreading { get; set; }

Whether the CPU renderer shares its work across cores. True by default; no effect on the GPU backends, which have nothing to share.

Two things share, and the second is the larger of them. Placing and lighting vertices is split by draw item; filling pixels is split into horizontal bands. Between and after them — clipping, emitting into one buffer, sorting the frame's triangles back to front, handing the result to Skia — is one list in one order and stays on one thread. Since the fill is most of a frame at any real window size, most of a frame is shared.

The reason to turn it off is to measure what it is worth on your own scene, which is a comparison worth making rather than taking on trust: the answer differs by scene and by platform, and this is a switch rather than a cleverer default because of that. A WebAssembly build compiled for threads also pays a little for shared memory on every access whether a second thread runs or not, so holding one to a single core with this is the worst of both — it keeps the cost and gives up the benefit.

SwapPolicy SwapPolicy { get; set; }

What the frame after a scene assignment shows: the new scene at once, with stand-ins for what has not arrived, or the old scene's frame until the new one is resident. SwapPolicy.Immediate by default. See SwapPolicy.

Diagnostics.FrameTrace Trace { get; set; }

Per-frame records, when somebody wants them. Null — the default — costs nothing. Assign a FrameTrace and every frame from then on is recorded into it: read FrameTrace.Frames or FrameTrace.Export it afterwards. Setting AVA3D_TRACE=<path> in the environment gives every view one automatically and writes it when the view leaves the tree and when the process exits.

Fields

MemberDescription
static StyledProperty<bool> AutoFitProperty

Backing property for Ava3DView.AutoFit.

static StyledProperty<Color> ClearColorProperty

Backing property for Ava3DView.Info. Direct, because it is written from a timer rather than by a binding.

static DirectProperty<Ava3DView, RenderInfo> InfoProperty

Backing property for Ava3DView.Info.

static StyledProperty<bool> IsAnimationEnabledProperty

Backing property for Ava3DView.IsAnimationEnabled.

static StyledProperty<bool> IsOrbitEnabledProperty

Backing property for Ava3DView.IsOrbitEnabled.

static StyledProperty<bool> IsPanEnabledProperty

Backing property for Ava3DView.IsPanEnabled.

static StyledProperty<bool> IsPickingEnabledProperty

Backing property for Ava3DView.IsPickingEnabled.

static StyledProperty<bool> IsZoomEnabledProperty

Backing property for Ava3DView.IsZoomEnabled.

static StyledProperty<double> MaxFrameRateProperty

Backing property for Ava3DView.MaxFrameRate.

static StyledProperty<RenderBackendKind> PreferredBackendProperty

Backing property for Ava3DView.PreferredBackend.

static StyledProperty<double> RenderScaleProperty

Backing property for Ava3DView.RenderScale.

static StyledProperty<RenderTrigger> RenderTriggerProperty

Backing property for Ava3DView.RenderTrigger.

static StyledProperty<int> SampleCountProperty

Backing property for Ava3DView.SampleCount.

static StyledProperty<Scene> SceneProperty

Backing property for Ava3DView.Scene.

static StyledProperty<bool> SoftwareThreadingProperty

Backing property for Ava3DView.SoftwareThreading.

static StyledProperty<SwapPolicy> SwapPolicyProperty

Backing property for Ava3DView.SwapPolicy.

Events

MemberDescription
event EventHandler CameraChanged

Raised when anything that moves a projection has moved: the camera, or the control's size.

This is the signal for keeping 2D placed against 3D — see Ava3DView.Project and Ava3DOverlay. It is deliberately the same signal the picture runs on rather than a per-frame tick: Ava3DView.InvalidateCamera is already mandatory for a hand-moved camera to reach the renderer at all, so anything listening here is in step with what is drawn by construction, and a view that is idle under RenderTrigger.OnDemand costs its listeners nothing.

Ava3DView.Info is not the signal to use for this. It is published from a timer at four times a second, which is right for a rate over an interval and far too coarse to place a marker with.

event EventHandler FirstFrameRendered

Raised on the UI thread once per renderer, after it has drawn its first frame — whatever that frame was: the empty view in Ava3DView.ClearColor, or a scene assigned before the view was shown. A host that covers the view with a placeholder until there is something to show takes it away here.

Once per renderer rather than once per view: a view taken out of the tree and put back starts again, with a new renderer and a new first frame, and so does one whose renderer was dropped by Ava3DView.ResetRenderer — the frame after a reset is a new renderer's first.

event EventHandler<PickEventArgs> Picked

Raised when a click hits geometry, or misses it — check PickEventArgs.Result.

event EventHandler SceneReady

Raised on the UI thread once per assignment of Ava3DView.Scene, after the first frame that drew the scene with everything it draws with resident on the device. Textures decode off the render thread and are copied up a few milliseconds a frame, so a scene's first frame is drawn with some of them still on their way; this is the first frame on which none are. A scene that was prepared is resident already and is announced on its first frame. A scene assigned and replaced before it was drawn is never announced, and a null scene has nothing to be ready.

Once per assignment, not per change: a scene that moves after it was announced is a ready scene that moves. Assign it again to hear about it again — or put the view back in the tree, which gives it a new renderer that has to draw the scene afresh and announces it once it has.

Methods

MemberDescription
Threading.Tasks.Task<CapturedFrame> CaptureAsync(CaptureRequest request, Threading.CancellationToken cancellationToken = default)

Reads one rendered frame back into memory.

The request is queued for the render thread, and the next frame it renders serves it — the frame on screen, when the request is for the view's own size and camera; a frame rendered for the request alone otherwise, so that a capture at another size or from another camera never changes what is shown, or the view's own target. The scene is as it is at the time of the call: a change made just before is in the frame, however the dispatcher schedules its rebuild. Several requests queued before one frame all come out of it. The readback runs without the CPU waiting for the GPU where the graphics API allows it — Metal and OpenGL — and the encoding runs on the thread pool; the task completes on the caller's synchronisation context, never inside a frame.

A view that is not in the tree waits for its attachment. Leaving the tree completes every pending capture as cancelled, as does the token. A request past Ava3DView.CaptureQueueCapacity throws CaptureQueueFullException here, synchronously.

request

What to capture. See CaptureRequest.

cancellationToken

Gives up on the capture; the task ends cancelled.

returns

The frame, which the caller disposes. A request for linear pixels from a scene that has none fails with NotSupportedException.

Threading.Tasks.Task<Imaging.Bitmap> ComposeAsync(Visual root, CaptureRequest request = null, Threading.CancellationToken cancellationToken = default)

The view and the Avalonia content around it as one picture: root rendered through a RenderTargetBitmap, with this view drawing a captured frame into it in place of the live picture — which a bitmap render never sees, because the live picture is the compositor's and not this control's. Overlays, labels, a caption, anything in the tree under root comes out over the 3D as it is on screen, and both halves are the same frame.

request says which frame, as for Ava3DView.CaptureAsync; its format is ignored and the pixels come back through the bitmap. The render of the tree happens on the UI thread once the frame is back.

root

The visual to render, which contains this view. Its own size, at the top level's scale.

request

What frame of the view to compose with. Null for the view as shown.

cancellationToken

Gives up on the capture; the task ends cancelled.

returns

The bitmap, which the caller disposes.

void FitToScene()

Frames the whole scene.

void InvalidateCamera()

Call after moving Ava3DView.Camera by hand.

void InvalidateScene()

Rebuilds the snapshot and draws, whether or not anything in the scene said it had changed.

Not usually needed: an attached Ava3DView.Scene raises Changed when a node, material or light moves, and this control is already listening. What it is for is the change that nothing announced — a mesh's arrays written in place without Mesh.InvalidateGeometry, a material shared with a scene that is no longer attached, or a texture's pixels replaced behind its back. Under RenderTrigger.Continuous the next frame picks all of those up anyway and this changes nothing; under RenderTrigger.OnDemand it is the way to say so.

PickResult Pick(Point position)

Casts a ray from a point on the control into the scene and returns the nearest triangle it hits.

Runs on the UI thread against the live scene graph, which is the only place the geometry is authoritative. Bounding boxes reject most nodes before any triangle is touched.

Threading.Tasks.Task<ScenePreparation> PrepareAsync(Scene scene, IProgress<PrepareProgress> progress = null, Threading.CancellationToken cancellationToken = default)

Decodes every texture and readies every mesh of scene on this view's device, without showing it, and completes when all of it is resident — so that assigning the scene afterwards draws it complete on its first frame, with no stand-in for anything.

The work is done as the frames go by: decoding on the thread pool, copies on the render thread within the policy's milliseconds a frame, exactly as for a scene that is on screen. A view that is not in the tree waits for its attachment first, because there is no device to prepare for until then. What a preparation makes resident stays in use until the scene is assigned to this view, or the view leaves the tree; a scene prepared and never shown holds its resources until then.

Cancelling releases what was decoded and nothing else is drawing, and the task ends cancelled.

scene

The scene, as it is now: what it holds at the time of the call is what is prepared.

progress

Where to report, once per frame that changed anything. See PrepareProgress.

cancellationToken

Stops the preparation; see above.

Vector3 Project(Vector3 worldPosition)

Where a world position lands on this control.

X and Y are in this control's own device-independent coordinates — the same ones Ava3DView.Pick takes and a pointer event reports — so the result can be handed straight to Canvas.Left and Canvas.Top, or compared against Bounds, with nothing in between. Z is the distance in world units from the near clip plane, not a clip-space depth: it is a length, it grows with distance, and it is the number to sort markers by.

Z at or below zero means the position is not in front of the near plane — behind the camera, or nearer than the frustum begins. X and Y are then meaningless and must not be placed: a point behind the camera projects mirrored, so it lands on the opposite side of the picture looking entirely plausible. Test Z before using X and Y. This is the mistake this method is shaped to make visible rather than to prevent, because a caller clamping an off-screen marker to an edge needs the direction and a nullable would take it away.

On the control rather than on Ava3DView.Camera, and the reason is Z: a camera does not know the viewport, and it cannot resolve its own near plane when Camera.NearPlane is left null, because what resolves it is the scene's radius. X and Y need only the field of view and the aspect ratio and could have gone either way; Z could not, and splitting them across two types would be worse than putting both here.

The inverse is Ava3DView.Unproject, and the ray through a point is Ava3DView.Pick.

worldPosition

A position in world space.

override void Render(DrawingContext context)

A transparent rectangle over the control, drawn for one reason: so that a pointer can land on it.

This control's picture is not drawn here. It is drawn by a CompositionCustomVisual attached as a child visual — see OnAttachedToVisualTree — which is what lets the renderer own its own surface and its own thread. The consequence is that the control's own draw list is empty, and Avalonia hit-tests a visual against the drawing operations it made: no operations, no hit, and every press, drag and wheel passes straight through the viewport to whatever is behind it.

Everything the control offers the pointer is downstream of that — Ava3DView.IsOrbitEnabled, Ava3DView.IsPanEnabled, Ava3DView.IsZoomEnabled and Ava3DView.IsPickingEnabled all begin in Ava3DView.OnPointerPressed, which was never called. It reads as four unrelated faults, and it is one missing rectangle.

Transparent rather than a Background property, because there is nothing here for an application to colour: whatever it chose would be underneath an opaque render surface and would never be seen. This is a hit-test surface and nothing else, and saying so in the code is cheaper than a property that appears to do something and cannot.

The one time it draws a picture is during Ava3DView.ComposeAsync, when the tree is being rendered into a bitmap that the compositor's child visual cannot reach: the captured frame is drawn here instead, so the bitmap has the 3D under whatever the tree draws over it.

void ResetRenderer()

Drops the renderer and builds it again on the next frame, on whatever graphics context the platform then offers. Every GPU resource — programs, buffers, textures, shadow maps — is made again, and the scene is redrawn.

The hook for a loss the control cannot see. It notices its own: a Vulkan device lost, an OpenGL context that Avalonia or the driver reports reset, a Metal command buffer that finished with a device error — each of those rebuilds without being asked. What it cannot notice is a loss only the host is told about, and the one that matters is the browser's: WebGL says webglcontextlost and webglcontextrestored to the page, never to the code drawing into it, and after the restore the same context object is usable again with every texture in it gone. Call this from the restore handler. Safe at any time and from any state; a view with no renderer yet simply builds one, as it was going to.

IDisposable Retain(Scene scene)

Keeps everything scene draws with resident, whatever the policy says, until the result is disposed. Pinned by identity — the meshes, textures and instance arrays the scene holds at the time of the call, on every device — so a scene that later swaps a texture pins the old one until the pin is disposed and never the new one. Pinning does not load anything: see Ava3DView.PrepareAsync for that, and use the two together for a scene that has to appear complete the instant it is shown.

Vector3 Unproject(Vector3 viewPosition)

The inverse of Ava3DView.Project: a point on this control, at a depth, back into world space.

X and Y are this control's device-independent coordinates and Z is the distance from the near clip plane, exactly as Ava3DView.Project returns them — so Unproject(Project(p)) is p, and a Z of zero is a point on the near plane itself.

viewPosition

X and Y on the control, Z in world units from the near plane.

See also