Your First Full App
Wire the pieces from Quick Start into a model viewer: load a glTF model, frame the camera, spin it per frame, orbit with the mouse, and click to pick and highlight a part.
Install
Model loading is not in the base package — add the glTF loader:
dotnet add package Aura3D.Avalonia
dotnet add package Aura3D.Model.GltfLoaderGrab a .glb model (call it model.glb), drop it in the app's working directory, or replace the path below with an absolute one.
XAML: one view, three callbacks
<Window
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:a="https://github.com/CeSun/Aura3D"
x:Class="MyApp.MainWindow">
<a:Aura3DView
x:Name="view"
SceneInitialized="OnSceneInitialized"
SceneUpdated="OnSceneUpdated"
PointerPressed="OnPointerPressed"/>
</Window>SceneInitialized— GL is ready andSceneexists; build the scene, load the model, frame the camera here.SceneUpdated— fires before every frame withDeltaTime(seconds) in the args; per-frame animation goes here.PointerPressed— the standard Avalonia pointer event.Aura3DViewis a control, so use it directly to get the click position for picking.
Full code
MainWindow.axaml.cs:
using System;
using System.Numerics;
using Aura3D.Avalonia;
using Aura3D.Core.Nodes;
using Aura3D.Core.Resources;
using Aura3D.Model;
using Avalonia.Controls;
using Avalonia.Input;
public partial class MainWindow : Window
{
private const string ModelPath = "model.glb"; // point this at your model
private CameraController? _controller;
private Model? _model;
// Tracks the last highlighted part so the next click restores it
private Mesh? _highlighted;
private Texture? _savedBaseColor;
public MainWindow()
{
InitializeComponent();
}
private void OnSceneInitialized(object sender, InitializedRoutedEventArgs args)
{
var view = (Aura3DView)sender;
var scene = args.Scene;
scene.Background = Texture.CreateFromColor(System.Drawing.Color.Gray);
// 1) Load the model
var model = ModelLoader.LoadGlbModel(ModelPath);
model.Name = "Hero";
// 2) Place it: at the world origin, upright facing the camera
model.Position = new Vector3(0, 0, 0);
// 3) One directional light — the default BlinnPhong pipeline is pitch black without it
var light = new DirectionalLight();
light.LightColor = System.Drawing.Color.White;
light.RotationDegrees = new Vector3(-30, -20, 0);
view.AddNode(light);
// 4) Frame it: fit to the model's bounding box, leave a little breathing room
view.MainCamera.FitToBoundingBox(model.BoundingBox, padding: 0.5f);
view.AddNode(model);
_model = model;
// 5) Mouse orbit
_controller = new CameraController(view) { MoveSpeed = 20f };
}
private void OnSceneUpdated(object sender, UpdateRoutedEventArgs e)
{
if (_model == null)
return;
// Spin a little every frame around Y: 30 deg/sec × this frame's duration
_model.RotationDegrees += new Vector3(0, 30, 0) * (float)e.DeltaTime;
}
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
var view = (Aura3DView)sender!;
var scene = view.Scene;
if (scene == null)
return;
// Pick only on left click so we don't fight the right-drag orbit
if (!e.GetCurrentPoint(view).Properties.IsLeftButtonPressed)
return;
var position = e.GetPosition(view);
var hit = scene.PickClosest((float)position.X, (float)position.Y, view.MainCamera);
if (hit == null)
return;
if (hit.Value.Node is not Mesh mesh || mesh.Material == null)
return;
// Restore the previous highlight first
if (_highlighted?.Material != null)
_highlighted.Material.SetTexture("BaseColor", _savedBaseColor);
// Remember the original color, then tint the hit part red
_highlighted = mesh;
_savedBaseColor = mesh.Material.GetTexture("BaseColor");
mesh.Material.SetTexture("BaseColor", Texture.CreateFromColor(System.Drawing.Color.Red));
view.RequestNextFrameRendering();
}
}
ModelLoadercomes from theAura3D.Model.GltfLoaderpackage, in namespaceAura3D.Model— if the compiler can't find it, you haven't installed that package yet.
LightColorandTexture.CreateFromColor(...)take aSystem.Drawing.Color, not Avalonia'sAvalonia.Media.Color. The samples writeSystem.Drawing.Color.Whiteto avoid clashing with Avalonia's same-namedColor; if your file already hasusing Avalonia.Media;, keep the fully-qualified form.
What each step does
- Load —
ModelLoader.LoadGlbModel(path)returns aModelnode tree of severalMeshparts. UseLoadGlbModelAndAnimationswhen you also want animations. - Place —
Modelderives fromNode, so it has the samePosition/RotationDegrees/Scaleas the box. - Light — one
DirectionalLightis enough to see the model;RotationDegreessets where the light comes from. - Frame —
FitToBoundingBox(model.BoundingBox, padding)pulls the camera back to just enclose the whole model, so you don't hand-tune the distance. This matters most when the model's real scale is unknown. - Spin per frame —
SceneUpdatedfires each frame; scale the angular speed bye.DeltaTimeso the rotation is frame-rate independent. - Click to pick —
Scene.PickClosest(x, y, camera)returns the nearestPickResult?at that screen point;hit.Value.Nodeis theMeshyou clicked. Swap its color withSetTexture("BaseColor", ...)to highlight it, and don't forget to restore the previous one.
Common options
- Render on demand (save power when idle): set
view.AutoRequestNextFrameRendering = false;, then callview.RequestNextFrameRendering()to push one frame after an animation step or a click. This example uses the default continuous rendering. - All hits, not just the closest:
scene.Pick(x, y, camera)returns a distance-sortedList<PickResult>, so you can reach through a translucent panel to the part behind it;PickClosestreturns only the nearest. - Orbit feel:
CameraController'sMouseSensitivity,ZoomSpeed,PanSpeed, or disable specific actions withEnableZoom/EnablePan. - Tighter/looser framing: adjust
FitToBoundingBox'spadding, or changeview.MainCamera.FieldOfView.
WARNING
The default forward pipeline supports at most 4 lights per type; extras are ignored. For more lights or physical lighting see Choosing and Configuring Pipelines.
Common pitfalls
- Black screen: nine times out of ten it's a missing light, a
RotationDegreesthat points the directional light away from the model, or an unaimed camera. Confirm a directional light exists first. - Model huge/tiny, camera far away: normal —
FitToBoundingBoxpulls the camera back to the model's real size; scroll in to inspect detail. - Picking is offset (high DPI):
e.GetPosition(view)returns logical pixels. If the hit point is offset from what you clicked, see the coordinate-scaling note in Common Pitfalls and Troubleshooting. BaseColorcan't go in an initializer: it's an extension property, somesh.Material = new Material { BaseColor = ... }won't compile. Create the material first, then assign, or useSetTexture("BaseColor", ...).- Highlight not restored: each click puts the previous part's color back with
SetTexture; otherwise every part you've clicked stays red.
More real, runnable code
- Picking demo — the
ObjectPickedevent, the full-hit list, andInstanceIndexfrom instanced picking - ModelViewer demo — the model tree, bounding boxes, and what
Clone(CopyType)shares - Camera demo — projection switching,
LookAt,FitToBoundingBox,WorldToScreen
Next steps
Go deeper by topic: The Scene Graph and Nodes · Loading and Placing Models · Cameras and View Control · Lighting and Shadows · Materials and Textures. For animation see Animation System.