Documentation · 3D Guide

2 · Moving things about

A mesh has no position. It is a shape, sitting around the origin, and it can be drawn in fifty places at once — that is the whole reason it does not know where it is. What knows is the Node holding it, and this chapter is about what a node can do to it.

A transform answers three questions

Where is it, which way is it turned, and how big is it. Every node has all three, and they are ordinary properties:

var node = new MeshNode(Primitives.Box(1f, 1f, 1f), Material.FromColor(0.8f, 0.3f, 0.2f))
{
    Position = new Vector3(2f, 0f, -3f),   // two right, three away from the viewer
    Scale    = new Vector3(1f, 2f, 1f),    // twice as tall, same width and depth
    RotationDegrees = new Vector3(0f, 45f, 0f)  // turned 45° about the up axis
};

Rotation is a quaternion — four numbers rather than three angles, for reasons that are the whole of chapter 4. RotationDegrees is there so you do not have to care yet.

The three become one 4×4 matrix, and a matrix is simply the compact way to hold "scale, then rotate, then move" so that applying it to a point is one operation instead of three. You can hand one over directly if you have built it yourself:

node.LocalTransform = Matrix4x4.CreateScale(2f) *
                      Matrix4x4.CreateRotationY(MathF.PI / 4f) *
                      Matrix4x4.CreateTranslation(0f, 1f, 0f);

Setting LocalTransform takes over: Position, Rotation and Scale stop driving the node until you set one of them again. It is the escape hatch, not the usual road.

Order is not a detail. Scale, then rotate, then translate. Do it the other way — move first and rotate second — and the object swings around the origin like a moon instead of turning where it stands. In System.Numerics, A * B means "do A, then B", which is the opposite of the convention in most textbooks. Read the multiplication left to right and it says what happens.

The scene graph

Nodes hold nodes. A child's transform is relative to its parent, so moving the parent moves everything under it, and the child never needs to know.

var car = new Node { Position = new Vector3(0f, 0f, 0f) };

car.Children.Add(new MeshNode(body, paint));
car.Children.Add(new MeshNode(wheel, rubber) { Position = new Vector3(-0.8f, -0.3f, 1.2f) });
car.Children.Add(new MeshNode(wheel, rubber) { Position = new Vector3( 0.8f, -0.3f, 1.2f) });

scene.Children.Add(car);

car.Position = new Vector3(0f, 0f, -10f);   // the whole car moves; the wheels stay bolted on
A scene graph, and how a world transform is built The scene is the root. A car node hangs under it, and three nodes hang under the car: a body and two wheels. A wheel's world transform is its own local transform multiplied by the car's, and then by the scene's, so moving the car moves the wheels with it. Scene car body wheel wheel a wheel's world transform wheel.LocalTransform × car.LocalTransform × scene.LocalTransform read it left to right: where the wheel is on the car, then where the car is in the world.

Two things follow. The first is instancing: both wheels here point at the same Mesh instance, so the geometry is uploaded to the GPU once and drawn twice. The second is that a hierarchy costs almost nothing — the snapshot walks it once a frame and multiplies the matrices on the way down.

A node has exactly one parent, and adding it somewhere else moves it rather than copying it. That is what makes handover work — a crate picked up by a crane, a missile leaving its rail, a passenger stepping off a train — and it is one line, because the old parent lets go on its own:

crane.Hook.Children.Add(crate);   // leaves the deck, keeps its mesh and its material

What does not carry over is where it appeared to be. Position is relative to the parent, so a crate at (0, 0, 0) on the deck is at the hook's origin the instant it changes hands. When the handover has to be seamless, read WorldTransform before the move and set the local transform to match after it.

Beside Add and Remove, Children does Insert, RemoveAt, IndexOf, Contains and Clear. Order in the list is not draw order — that is RenderOrder and the distance sort — so inserting rather than adding matters only when your own code walks the children and cares which comes first.

A node cannot be its own ancestor. Adding a node to something underneath it would make a loop, and a loop in a graph that gets walked once a frame is a hang rather than an error. So it throws instead, at the moment the mistake is made and while the stack still says who made it.

Local space, world space

A node's Position is local: relative to its parent. Most of the time that is exactly what you want. Sometimes it is not — a wheel two levels down, a turret on a hull that is itself rolling — and then you want the answer in world space, which is what everything is finally measured in.

var turret = ship.Find("turret_port");
turret.LookAt(target.WorldPosition);   // aims in world space; the ship's roll is solved out

Bounds, and framing what you built

Every node knows the box it occupies: LocalBounds in its own space, Bounds and WorldBounds once the transforms are applied. A BoundingBox is a cheap thing to test against, which is why picking uses it before it looks at a single triangle, and it is how the control frames a scene it has never seen:

View.Camera.Fit(scene.WorldBounds);   // everything visible, with a margin

The control does that for you on the first frame unless you set AutoFit="False". After that the camera is yours.

Telling the scene something changed

Node properties are watched: set Position and the scene knows a new frame is due. Objects the graph cannot watch are not — a Material and a Light are plain mutable objects that could be shared by fifty nodes — so after changing one of those, say so:

material.BaseColor = new Vector4(1f, 0f, 0f, 1f);
scene.Invalidate();

Adding or removing a node, or a light through Scene.Lights, does it for you. The rule is simple enough to state once: if you reached past a node to change something, tell the scene.

Run it

Parenting, local versus world, and a hierarchy turning as one thing.

AVA3D_SCENE=Transforms dotnet run --project samples/Ava3D.Demo.Desktop

← 1 · A scene from nothing 3 · The camera →