Vertices, Edges, Normals, and UVs: A Working Glossary
A precise guide to mesh positions, rendered vertices, edges, indices, normals, tangents, and UVs—with byte calculations and debugging methods.
A mesh is not just a cloud of points connected into triangles. Each rendered vertex is a synchronized row of attributes: position, normal, tangent, texture coordinates, color, skinning weights, and any custom data supported by the format. Indices select those rows to assemble primitives.
That detail explains several otherwise confusing results. An exporter can report more vertices than an authoring tool. Welding positions can damage a UV seam. Recomputing normals can change highlights without moving a single point. A normal map can look inverted even when the image itself is correct because its tangent basis changed.
Position versus rendered vertex#
Artists often use “vertex” to mean a point in the editable topology. A renderer uses a stricter definition: a vertex is one complete attribute tuple referenced by an index.
Consider two triangles that meet at the same position. They can share one rendered vertex only if every exported attribute is also shared there. If the faces need different normals, UV coordinates, colors, joint weights, or tangent handedness, the exporter must create separate vertices at the same position.
This is not accidental duplication. One index cannot select position row 12, normal row 48, and UV row 7 independently in a conventional glTF primitive. The glTF mesh specification requires all attribute accessors on a primitive to have the same count, and an index selects the corresponding element from each accessor.
That gives three useful counts:
- unique positions: distinct XYZ locations within a tolerance;
- rendered vertices: complete exported attribute tuples;
- index references: corner selections used to form primitives.
For indexed triangles, index references equal triangle count × 3. Rendered vertices are normally much lower than index references because adjacent triangles share tuples, but higher than unique positions where attributes split.
Edges are topological relationships#
An edge connects two mesh positions in the authoring topology. In a triangle list, an edge is usually implicit: it appears because two vertex indices are adjacent within a triangle. glTF does not require a separate edge table for triangle meshes.
Classifying edges is still useful during processing:
- a boundary edge belongs to one face;
- an ordinary manifold interior edge belongs to two faces;
- a non-manifold edge belongs to more than two faces, or participates in an ambiguous surface configuration;
- a hard edge marks a normal discontinuity;
- a UV seam marks a texture-coordinate discontinuity;
- a material boundary separates primitives or face assignments.
One geometric edge can belong to several classifications at once. A hard edge does not need to be an open boundary. A UV seam need not be a visible crease. A simplifier must distinguish these meanings because collapsing an ordinary interior edge is different from crossing the mouth of an opening or merging two UV islands.
Faces, polygons, and triangle corners#
Authoring faces may be triangles, quads, or n-gons. Runtime geometry is normally converted to triangles. Every triangle has 3 corners; each corner references a rendered vertex.
A 10,000-triangle indexed primitive therefore has 30,000 index references. If it contains 6,500 rendered vertices, the average vertex is referenced about 4.6 times. That average says nothing about quality by itself, but unexpectedly low reuse can reveal excessive seams, duplicate vertices, disconnected triangles, or an unoptimized index order.
Do not weld every duplicate position to increase reuse. Two coincident vertices can be semantically necessary. Compare all attributes and boundary roles first.
Geometric normals and vertex normals#
A normal is a unit vector perpendicular to a surface. There are two related kinds.
The geometric face normal comes from triangle positions. For vertices A, B, and C, a direction is obtained from the cross product of B − A and C − A. Reversing the triangle winding reverses that normal.
A vertex normal is an attribute stored at each rendered vertex. During rasterization, the three vertex normals are interpolated across the triangle and normalized for lighting. If adjacent faces share similar vertex normals, the edge appears smooth. If each face has its own normals, the edge appears sharp even when positions touch.
Normals change lighting, not geometry. A perfectly smooth normal field cannot make a hexagonal silhouette circular. It can only change how the six sides respond to light.
glTF requires NORMAL values to be normalized unit vectors. Quantization extensions can use integer storage, but loaders restore usable values. A normal of [0, 0, 0] has no direction and is invalid for meaningful lighting.
When should an edge be hard?#
Use a hard normal split when a real surface has a sharp change in direction that should create a discontinuous highlight: the corner of a machined box, the rim of a cut opening, or the boundary between separate panels.
Angle-based rules are a starting point, not a physical law. A 30-degree auto-smooth threshold can make one asset look right and another look inflated. Consider:
- the intended material roughness;
- bevel geometry near the edge;
- whether a normal map was baked for a particular normal field;
- the closest camera and highlight movement;
- whether the edge is a real crease or a tessellation artifact.
Bevels and weighted normals solve different problems. A bevel changes the silhouette and creates faces that catch highlights. Weighted normals alter shading across existing faces. Weighted normals cannot replace a bevel when the edge profile is visible.
Tangents and bitangents#
A tangent-space normal map stores directions relative to the surface rather than in object or world coordinates. The renderer needs a basis at each vertex:
- the normal points away from the surface;
- the tangent points along increasing texture U;
- the bitangent points along texture V, adjusted for handedness.
glTF stores TANGENT as four components. XYZ is the tangent direction. W is +1 or −1 and lets the renderer reconstruct the bitangent from the cross product of normal and tangent. The sign is essential for mirrored UV islands.
Tangents depend on positions, normals, and UVs. Change any of those and the old tangent data may no longer be correct. A safe workflow after simplification is to preserve all related attributes coherently or regenerate tangents using the same convention expected by the baker and runtime. MikkTSpace is widely used, but “uses tangents” is not enough—generator conventions must match.
When diagnosing a normal-map seam:
- Disable the normal map. If the seam remains, inspect vertex normals or geometry.
- Display the normal texture as color and check channel orientation.
- Inspect UV seams and mirrored islands.
- Confirm tangent W changes correctly across mirrors.
- Determine whether the exporter or importer regenerated tangents.
- Re-bake against the exact triangulated delivery mesh if necessary.
UV coordinates#
UVs map mesh vertices into two-dimensional texture space. U is horizontal and V is vertical by convention, but image-origin and API conventions can differ. The glTF specification defines how texture coordinates address images so conforming loaders can handle the necessary convention consistently.
A UV seam is a place where one geometric point requires multiple UV coordinates. Imagine wrapping a label around a cylinder: the first and last points of the wrap meet in 3D, but one side needs U = 0 and the other U = 1. Those must export as separate rendered vertices even if their positions and normals match.
UV quality is measurable:
- texel density: texture pixels allocated per unit of surface area;
- occupancy: how much of the 0–1 texture square is used;
- padding: pixels separating islands so lower mip levels do not bleed;
- stretch: distortion between 3D surface proportions and UV proportions;
- overlap: intentional reuse versus accidental collisions.
A 2048 × 2048 atlas has 4,194,304 texels. If UV islands occupy only 40%, roughly 2.5 million texels carry no first-order surface detail, although mipmaps and block compression complicate the exact storage effect. Repacking can improve effective resolution without increasing dimensions.
Multiple UV sets#
glTF names sets TEXCOORD_0, TEXCOORD_1, and so on. A material texture normally uses set 0 unless its textureInfo selects another set.
Common uses include:
- TEXCOORD_0 for repeating or unique material textures;
- TEXCOORD_1 for non-overlapping baked occlusion or lightmaps;
- a separate set for decals or runtime effects.
Not every viewer supports every multi-UV workflow. The glTF specification recommends client support for at least two texture coordinate sets, but platform-specific material importers may impose their own rules. Verify the final viewer, particularly when an exporter adds KHR_texture_transform or an engine repurposes UV channels.
Other vertex attributes#
Several attributes can materially change size and processing cost.
COLOR_0 supplies RGB or RGBA color per vertex. It can tint a material, store masks, or replace a texture for broad color regions. If 8-bit normalized RGBA is sufficient, it uses 4 bytes per vertex instead of 16 bytes for four 32-bit floats.
JOINTS_0 identifies joints affecting a vertex. WEIGHTS_0 supplies their weights. Four 16-bit joint indices plus four 16-bit normalized weights require 16 bytes per rendered vertex. A second influence set doubles that portion.
Morph targets add POSITION, NORMAL, or TANGENT deltas. These are not a one-time mesh-wide value; they can add an attribute array for every target. Five float position morph targets on 80,000 vertices require 4.8 MB raw: 5 × 80,000 × 3 × 4 bytes.
Remove unused attributes only after confirming they are truly unused. Stripping tangents from a normal-mapped material can force runtime generation or break shading. Stripping joints from a skinned primitive destroys deformation.
Calculating vertex-buffer size#
For an uncompressed, tightly packed static vertex with float attributes:
- position: 3 × 4 = 12 bytes;
- normal: 3 × 4 = 12 bytes;
- tangent: 4 × 4 = 16 bytes;
- UV: 2 × 4 = 8 bytes;
- total: 48 bytes per rendered vertex.
At 120,000 vertices, that is 5,760,000 raw bytes before indices, alignment, morph targets, or compression. If tangent space is not needed, omitting tangents saves 1,920,000 bytes in this example. If quantization safely reduces normal, tangent, and UV representation, the result can be smaller again.
Do not turn the arithmetic into an instruction to delete attributes indiscriminately. Use it to rank opportunities. A 16-byte tangent is important if the asset uses a tangent-space normal map. A second 8-byte float UV set is waste if no material or engine feature references it.
Why exporters create “extra” vertices#
The Blender glTF documentation explains that discontinuous UVs and flat-shaded edges are separated during export. Other causes include:
- material primitives that cannot share one index domain;
- mirrored UVs with different tangent handedness;
- vertex colors or skin weights that differ at one position;
- modifiers that duplicate or split geometry;
- loose points or edges exported as separate primitives;
- exact duplicates left by modeling operations.
To distinguish necessary splits from waste, group vertices by position and compare all their attributes. If two rows are byte-for-byte or tolerance-equivalent and share primitive constraints, they may be mergeable. If only the UV or normal differs, inspect the seam before touching it.
A diagnostic inspection sequence#
When a mesh looks wrong after export:
- Verify POSITION bounds and node transforms; wrong scale or orientation may not be a vertex-data problem.
- Render a flat unlit material to isolate shape and winding.
- Display geometric face normals and stored vertex normals separately.
- Disable normal mapping, then restore it after normals look correct.
- Apply a UV checker to expose seams, flips, stretching, and incorrect UV-set selection.
- Inspect tangent direction and handedness on mirrored islands.
- Compare authoring control-point count with exported rendered-vertex count.
- Check the importer log for regenerated normals or tangents.
- Run the official glTF Validator for accessor and normalization errors.
- Re-test the exact exported file in both a reference viewer and the production runtime.
Mesh attribute checklist#
- POSITION data matches intended dimensions and bounds.
- Triangle winding and face normals are consistent.
- NORMAL vectors are unit length and split only where needed.
- Hard edges correspond to deliberate shading discontinuities.
- TANGENT data matches normals, UVs, and the normal-map convention.
- UV islands have adequate padding for intended texture dimensions and mips.
- Mirrored UVs preserve tangent handedness.
- Additional UV sets are used by a documented material or runtime feature.
- COLOR, JOINTS, WEIGHTS, and morph data are present only when required.
- Duplicate rendered vertices are classified before welding.
- Byte estimates agree with accessor counts and component types.
- The target importer does not silently replace validated data.
Sources and further reading#
- Khronos glTF 2.0 specification: geometry and attributes
- Khronos glTF Tutorial: buffers, buffer views, and accessors
- Blender Manual: glTF 2.0 mesh export
- Blender Manual: UV unwrapping
- MikkTSpace reference implementation
Positions define shape, but complete rendered vertices define what the GPU can shade and deform. Keeping that distinction visible makes vertex-count reports, seam debugging, attribute compression, and simplification decisions far more predictable.