Mesh
public sealed class MeshTriangle geometry, independent of where it sits in the world.
A mesh is shared: put the same one on twenty MeshNodes and it is uploaded to the GPU once. That is why the arrays are init-only — the renderers cache GPU buffers against mesh identity, and an array that could be swapped out from under them is an array whose length could change, which is a buffer size and an index count and a draw call all silently wrong at once.
The contents of those arrays are another matter. Writing new values into them and calling Mesh.InvalidateGeometry is how geometry animates: a flag, a wake, a heightfield under a cursor, a spline the user is dragging. The length cannot change that way, which is exactly why it is safe. To change the length, build a new mesh and assign it to MeshNode.Mesh.
Properties
| Member | Description |
|---|---|
| Local-space extent, computed once on first use. |
| Per-vertex colour, multiplied into the material's base colour and its base-colour map. Optional, and white where a mesh does not carry it. This is glTF's What it is for is the case that would otherwise be several meshes. A hull banded into light, base and dark for cel shading is three materials and three draw calls when the colour lives on the material, and one of each when it lives here; so is a terrain tinted by height, or a plume fading from mouth to tip. Uploaded as four bytes a vertex rather than four floats, which is why it costs eight per cent of the vertex buffer and not a third — see Held as |
| Triangle list. Length is always a multiple of three. |
| How each target bends the surface, as a per-vertex normal displacement parallel to A moved vertex is lit by the normal it had before it moved unless something says otherwise, and on a lengthened snout or a widened jaw that reads as a shape lit for a different shape. Exporters know this: every one of the reference character's targets carries a The authored answer rather than a derived one, and the difference is not only fidelity. Deriving normals from the morphed triangles means a pass over the triangles — fifty times what blending this array costs, measured — and it averages across every edge, so the hard edges the artist authored come back soft. See |
| Shapes this mesh can be moved towards, each one a per-vertex displacement from the positions above. One array per target, each as long as A face that never changes is a mask, and this is the difference a viewer notices without being able to name it. One blink is worth more surface detail than any map: it is the cheapest signal that a thing is alive, and it is a hundred vertices moving a millimetre for a tenth of a second. Displacements rather than absolute positions, which is what glTF's morph targets are and is the representation that composes: several targets at once add up, so a blink and a jaw drop can run together without either knowing about the other. Absolute positions do not compose at all — two of them at half weight is the average of two faces, which is a third face that neither target described. The weights live on the node rather than here — see |
| Optional name, carried through from the source file for debugging. |
| Per-vertex normals. Optional — |
| Vertex positions in the mesh's own space. Required. |
| Which joints move each vertex, and how much. Null for geometry that is not skinned, which is almost all of it. One entry per vertex, four influences each, matching glTF's The joints are indices into |
| Four more influences per vertex, for the rigs that need eight. Null for almost everything, which is the point of it being separate: a mesh with one set pays nothing for the existence of a second. glTF's Parallel to |
| UV set used to derive Tangents: 0 or 1. A normal map using another set is disabled with diagnostics. |
| Per-vertex tangents in the glTF packing: xyz is the tangent direction, w is ±1 giving the handedness of the bitangent, which the shader reconstructs as Only needed by |
| Per-vertex texture coordinates. Optional; without them a material's texture is ignored. |
| Second per-vertex coordinate set, glTF TEXCOORD_1. Selected independently by each TextureMapping. |
| Number of triangles, which is the index count divided by three. |
| Number of vertices. |
Methods
| Member | Description |
|---|---|
| The mesh's edges as endpoint pairs, ready for An edge is kept when the two faces meeting along it fold by at least that angle, or when only one face has it at all. Those two rules cover the whole range:
This is the general answer to drawing lines on a model, which is why there is no Two details make it work on real files rather than only on hand-built meshes. Vertices are welded by position first, because a flat-shaded export shares none — every triangle carries its own three, so two faces meeting along an edge agree on neither of its ends. And an edge with one face is a boundary rather than a fold, and is always wanted: it is exactly the line a person would draw. Not cached, unlike
|
| Says that the contents of the arrays have changed, so the renderers upload them again and Write into Both calls, because a mesh is not in the scene graph — it is data that any number of nodes in any number of scenes may be pointing at, and it has no way to reach them. This is the same contract What this costs is one buffer upload per backend per call, which is the price of the thing and not avoidable. Nothing is allocated on the managed side — the interleaving scratch is reused — and on OpenGL the GPU buffer keeps its name and its bindings too. Metal releases and retakes its buffer instead, because a command buffer retains what it references until the GPU is done and writing into one a frame in flight is still reading is a race with no way to lose loudly. Assigning a fresh array to Worth knowing before you leave it out: the CPU fallback reads the arrays afresh every frame and has no buffer that can go stale, so forgetting this call looks like it works there and only breaks on OpenGL and Metal. That is the worst order to find out in. One caveat worth knowing rather than guarding against: the renderer reads the arrays on its own thread, so writing into them while a frame is in flight can put part of a frame one step ahead of the rest. For animation driven from a timer tick — where the data is a continuous function of time and the tear is a fraction of one frame's motion — this is not visible and not worth a copy. Where it would be, keep two arrays and hand the finished one to a new mesh, so the renderer never reads the one being written. |
| One mesh holding all of |
| One mesh holding all of Geometry costs a draw call each, and a draw call is the expensive unit — a hundred rivets on a hull that never move relative to one another are a hundred times more expensive as a hundred nodes than as one. Merging is how that gets paid down, and it is why the parts have to be in a common space first: That is also the trade being made, and it is worth saying out loud: what merging buys in draw calls it spends on flexibility. The parts can no longer move independently, be hidden independently, or be picked apart — they are one object now. Merge what is nailed down; leave what moves alone. Where the parts disagree about which attributes they carry, the result carries the union of them, and each part is brought up to it on the way in by Vertices are not welded across the seams. Two parts that meet keep both copies, because welding needs a tolerance and a rule for what happens to attributes that disagree, and both are the caller's to choose. Merging nothing gives an empty mesh; merging one gives that same instance back, unwrapped. Morph targets do not survive this either, and for a second reason on top of the renumbering: the inputs have different target counts and no shared idea of what target 0 means, so there is no answer to what the merged mesh's first target would be. A head that blinks should stay its own mesh; merging is for the static geometry around it. |
| This mesh with fewer triangles, spent where the shape needs them.
returns A simplified mesh, or this one unchanged when it already has few enough triangles. |
| This mesh with A Not Three things happen that a loop over Normals take the inverse transpose rather than the matrix. Squash a sphere along Y and its surface tilts one way while its normals tilt the other; using the matrix on both leaves the shading lit as though nothing had moved. Against the closed form for an ellipsoid's normal, a unit sphere scaled by (3, 0.4, 1) comes out 6.0e-4 radians off this way and 1.31 radians — 75 degrees — the other. Tangents, despite sitting next to the normals, do take the matrix: they run along the surface, which is what positions do. The invariant that settles it is that a tangent stays perpendicular to its own normal, and it does — after a 13:1 stretch the worst dot product between them is 3e-8 this way and 0.88 with the inverse transpose, which is 62 degrees out of true and would tilt every normal-mapped surface in the mesh. And a matrix with a negative determinant — a mirror, which is how a left wing is usually made from a right one — turns every triangle inside out. So the winding is reversed to match, and the handedness in each tangent's w is flipped, without which a normal map lights the mirrored copy from the wrong side. An identity matrix returns this same instance rather than a copy, which matters more than it looks: a renderer caches GPU buffers against mesh identity, so a copy is an upload. |
| This mesh carrying The usual way a mesh gets them, because they are rarely what a generator produces and often what a caller decides afterwards — banding a hull by shade, tinting a terrain by height, fading a plume along its length. All of those are one array over geometry that already exists. |
| This mesh with every vertex unshared and one normal per triangle: the faceted look, where each face is a flat plane of one shade and the edges between them are hard. This is what a low-polygon solid is for. An eight-sided cylinder shaded smoothly reads as a badly lit tube — the shading says "round" while the silhouette says "octagon", and the eye believes the silhouette and concludes something is wrong. Faceted, the same geometry reads as an octagonal drum, deliberate and solid. Why it lives on the mesh and not on . Flat shading as a material flag needs the fragment shader to derive a normal from screen-space derivatives, which is a third answer on the CPU fallback — it has no fragment shader and lights per vertex, so the one backend where a flag would be hardest to honour is the one most likely to be running when it matters. As a vertex operation it is arithmetic on a buffer, so all three backends draw it identically by construction and there is nothing to report in a feature list. What it costs. Vertices, and a real number of them: a shape that shared them heavily comes out with three per triangle, so a 32-segment sphere goes from 561 vertices to 3,072. That is the price of a hard edge, it is what a flat-shading exporter does too, and it is why this is a build-time call rather than something to reach for on a dense mesh. Texture coordinates and tangents follow their own corners, so a mapped surface keeps its mapping. Any triangle indexing past the end of the vertex array is dropped, which is the only thing that can be done with one. Morph targets do not survive this. Splitting invents vertices, so a displacement array indexed by the old numbering describes a mesh that no longer exists — and a target silently applied through the wrong numbering moves the wrong corners, which looks like a shattered face rather than like nothing. Flatten first, then author the targets against what came out. |
| This mesh with area-weighted smooth normals, or itself when it already has them. Area weighting rather than plain averaging: a vertex shared by one large face and several slivers should follow the large face, which is what unweighted averaging gets wrong on triangulated CAD. The three members that produce normals divide up like this, and the difference between them is which one you want rather than a matter of taste. This one fills a gap — it is what a loader calls on a file that supplied none, and it returns |
| This mesh with tangents derived from its UV layout, or itself when it already has them or lacks what the derivation needs. Normal mapping needs a per-vertex frame that agrees with how the texture was painted, and the UVs are the only record of that. The tangent is the world direction in which u increases; the standard solve is per triangle, accumulated per vertex so shared vertices get a smooth frame, then made orthogonal to the normal. Two details matter in practice. Degenerate UVs — a triangle whose three vertices share a texture coordinate, common in models that mix mapped and flat-coloured faces — give a zero-area solve, so those triangles are skipped rather than allowed to poison their vertices with NaN. And the sign in w is not decoration: mirrored UV islands, which is how nearly every character and vehicle is unwrapped, have a flipped bitangent, and without it the normal map lights the mirrored half from the wrong side. |
| Derives tangents from UV0 or UV1, preserving both coordinate arrays. Existing tangents are reused only when they belong to the requested set. Returns this mesh unchanged when it has no normals or no coordinates in that set.
|
| This mesh carrying The counterpart to the three that compute them, for the case where they are already known — a format this library does not read, a solver, a shape whose normals are a closed form and therefore exact where an averaged one would not be. Passing null is how a mesh loses them, which is worth having as much as gaining them: it is what makes |
| This mesh with area-weighted smooth normals, computed afresh whether or not it already has them. The counterpart to The weld is by rounded position and is used for nothing but deciding which corners share a normal; the vertices themselves stay where and as many as they were, so texture coordinates, tangents and the index buffer all survive. That matters on a UV seam, where two vertices sit at the same point with different coordinates: they must keep both coordinates and should share one normal, which is exactly what this does and what welding the vertices outright would get wrong. |
| This mesh carrying Clearing them is the useful direction: they are needed only by |
| This mesh carrying Texture coordinates are the one attribute nothing here can derive — unwrapping a surface is a decision rather than a calculation — so this is the only way a mesh gets them after it is built. |