API Reference · Scene and camera

MeshNode

Namespace: Ava3D

public sealed class MeshNode : Node

A node that draws geometry.

Constructors

MemberDescription
MeshNode(Mesh mesh, Material material = null)

A node with geometry and, optionally, a material. Without one it gets a plain white default.

mesh

The geometry to draw.

material

How to shade it, or null for the default.

Properties

MemberDescription
bool CastsShadow { get; set; }

Whether this node's geometry blocks the scene's shadow-casting light. True by default.

Set it false on the two things that are usually wrong to cast: a sky sphere or backdrop, which surrounds everything and would spread the shadow map over the whole sky so that a chair's shadow is one texel; and a ground plane, which casts nothing anybody can see and, if it is large, does the same thing to the map's resolution. Both still receive — this is about what goes into the depth pass, not about what is shaded by it.

It does not affect Bounds or what the camera frames on, only the shadow frustum and the depth pass.

MeshInstance[] Instances { get; set; }

Copies of this mesh, drawn in one call, each with its own transform and tint. Null for the ordinary case of one.

What it buys. Forty crates as forty nodes is forty draw calls, and a draw call costs between two and ten thousand triangles here — measured, on both the Metal and the OpenGL paths. As forty instances it is one, with the vertex data uploaded once. The demo's own scenes and EliteQuest's wreck decks both run at about eighty triangles a draw, which is the signature of a renderer that has no answer to this.

What it costs. An instance is not a node: it has no name, no children, no visibility of its own, and nothing can pick it, light it or hide it individually. That is the trade, and it is the right one for a bolt and the wrong one for a door.

The node's own transform still applies, on top of each instance's, so a shelf of crates moves when the shelf does. The node's material applies to all of them, with MeshInstance.Tint multiplied in per copy.

Write into the array and call MeshNode.InvalidateInstances for a swarm that moves; assign a new array when the count changes. Sorting for transparency is per node, not per instance — a set of blended instances takes one depth for the whole set, the same limitation that already applies to two blended surfaces that interpenetrate.

bool IsPickable { get; set; }

Set false to keep this node out of Ava3DView.Pick results — right for gizmos and ground planes, which should be visible but not selectable.

LevelOfDetailCollection LevelsOfDetail { get; }

Coarser meshes drawn in place of MeshNode.Mesh as the camera gets further away. Empty by default.

Chosen per copy. On an instanced node every copy is measured on its own, so the ferns at the camera's feet are the detailed mesh and the ferns thirty metres down the path are the light one, in the same frame, from the same node. The distance is from the camera to the centre of the copy's bounds.

Shadows are cast by alone, by every copy, at every distance — exactly as a node without levels casts. The shadow map is drawn from the light and does not know where the camera is; a coarse level casting would move a shadow the moment the camera walked past a threshold, which is the one kind of popping nobody forgives.

Levels are drawn as they are: a skinned or morphed node draws its levels unskinned and unmorphed, so they belong on static geometry.

BoundingBox LocalBounds { get; }

The mesh's own extent, before this node's transform.

Material Material { get; set; }

How the geometry is shaded. Assigning null restores a default rather than throwing.

float MaxDrawDistance { get; set; }

How far from the camera this node stops being drawn, in world units. Unlimited by default.

Measured like MeshNode.LevelsOfDetail: per copy, to the centre of its bounds. It is the right setting for grass, which is a few pixels at twenty metres, and for anything past where fog is complete — see Scene.FogEnd, beyond which a surface is the fog's colour exactly and drawing it changes no pixel.

It does not limit shadows, for the reason MeshNode.LevelsOfDetail gives; how far shadows reach is Light.ShadowCasterBounds.

Mesh Mesh { get; set; }

The geometry. Null draws nothing.

bool MorphNormals { get; set; }

Whether the normals are rebuilt from the morphed positions each time they move, for targets that did not bring their own. Off by default, and consulted less often than it used to be.

A mesh with Mesh.MorphNormalTargets never reaches this. Its normals are blended from the file's own, which is both faithful and cheap — every target the reference character carries has a NORMAL, and blending one costs a pass over the vertices.

This is the fallback for geometry that has none: hand-built targets, or an exporter that wrote positions only. It costs a pass over the triangles, accumulating a face normal into each of its three corners and normalising afterwards — fifty times what blending a target costs, measured — and it averages across every edge, so hard edges come back soft.

Off, because for the motion this is for nobody sees the difference. An eyelid closing moves a few dozen vertices a millimetre over a tenth of a second, and the normals it leaves behind are wrong by a degree or two for six frames. Turn it on for a morph with no authored normals that reshapes a surface enough to relight it — a jaw dropping, a brow furrowing, a sail filling.

float[] MorphWeights { get; set; }

How far this node is moved towards each of Mesh.MorphTargets. One weight per target, in the same order; a shorter array leaves the targets past its end at zero, and a longer one ignores the extra.

Not clamped. 0 is the mesh as authored and 1 is the target reached, and the useful range is between them — but a weight above 1 overshoots and a negative one moves the opposite way, both of which are occasionally what somebody wants and neither of which can be undone once clamped away. A blink driven from a clock is MathF.Sin through a lookup and lives entirely in 0..1 regardless.

Assigning this array is what makes the morph happen; writing into it in place is not. The setter is what marks the node changed, so a caller animating a weight from a frame callback should assign — node.MorphWeights = [t, 0f] — rather than reach into the array it handed over. An array written in place is the same array, the node has no way to know it moved, and the picture stops on whatever the last assignment left.

Skin Skin { get; set; }

The skeleton driving this mesh, or null for geometry that is not skinned.

Set together with Mesh.Skinning — one without the other does nothing, because the bindings index into this skin's joints and the skin has no vertices of its own. Two nodes may share one skin, which is what a body and the robe over it want.

This node's own transform does nothing, which is glTF's rule and not an oversight. The pose is solved back into this node's space, so whatever transform the node carries is applied by the renderer and removed again — a skinned mesh sits where its joints put it and nowhere else.

To move the character, move something above the joints. A rig is a mesh and a skeleton side by side under one parent, so moving that parent moves the joints, and the pose moves with them. Moving the mesh node alone is the one thing that has no effect, and it is worth knowing which of the two nodes to reach for before wondering why one of them does nothing.

object Tag { get; set; }

Arbitrary payload, handed back on a pick. The scene graph never looks at it.

Vector2 UvOffset { get; set; }

How far this node's texture coordinates are slid, on top of its material's. Zero by default.

float UvRotation { get; set; }

How far this node's texture coordinates are turned, in radians, on top of its material's.

The variation that actually works. A phase offset slides a pattern the eye has already learned; a quarter-turn is what makes two of a kind stop being two of a kind, and it is why the consumer that asked for this spends a hundred and seventy lines rewriting vertex data to get it.

Vector2 UvScale { get; set; }

How far this node's texture coordinates are scaled, on top of its material's. One by default.

Why this exists when already does. A material is shared by every node wearing it, so a repeat cannot be broken up without either cloning the material — which costs a draw, because draws are grouped by material — or rewriting the mesh's own coordinates, which costs a copy of the geometry. Forty crates in a room want forty phases and one material, and until this existed there was no way to say that.

It composes with the material's rather than replacing it: the two are multiplied into one affine map when the scene is flattened, so a stock's art direction and an instance's variation can both be expressed without either knowing about the other.

Methods

MemberDescription
void InvalidateInstances()

Says the contents of MeshNode.Instances have changed, so the renderers upload them again.

The counterpart to LineNode.InvalidateGeometry and it exists for the same reason: a swarm that moves every frame should write into the array it already has rather than allocate a new one, and an array cannot tell anybody it has been written to.

See also