The .drft container · interface

MeshData

A description of vertex data, and the check that it is coherent.

Explained in The .drft container.

interface MeshData
import type { MeshData } from '@driftengine/drft';

In depth

It lives in the format package rather than the renderer because it is the boundary object between them: the container's whole job is to carry one of these, and every import the format code took from render was this type or its validator. Putting it here is what lets the container be read and written by something that never draws.

The renderer re-exports both, so nothing that consumed them from there has to move.

Properties

NameTypeDescription
positionsFloat32Array
normalsFloat32Array
colorsFloat32Array
emissiveFloat32ArrayOne float per vertex: self-illumination, revealed only at night.
specularoptionalFloat32ArrayOne float per vertex, 0–1: how sharply this surface takes a sun highlight.
More

Optional, and that is a cost decision rather than a convenience one. Seventeen places build a MeshData, and only a handful of props shine — so rather than make every producer fill an array of zeroes, an absent array leaves attribute 4 disabled and the shader reads the constant WebGL supplies for it. A world with no shiny geometry allocates nothing and uploads nothing.

tangentsoptionalFloat32ArrayFour floats per vertex: a tangent, and the handedness of the bitangent in w.
More

Optional for the same cost reason as specular and uvs, and the reason bites harder here: four floats a vertex is the widest optional attribute in the format, and only geometry that carries a normal map has any use for it. A world with none allocates nothing, uploads nothing, and reads the constant its backend supplies.

w is ±1. The bitangent is cross(normal, tangent) * w, and the sign is what keeps a mirrored UV layout from lighting one side of a model inside out. See generateTangents.

uvsoptionalFloat32ArrayTwo floats per vertex: where this vertex sits in a surface texture.
More

Optional for the same cost reason as specular, and the reason carries further here because most geometry in this engine is coloured rather than textured. An absent array leaves attribute 5 disabled and the shader reads the constant WebGL supplies, so a world with no textured surfaces allocates nothing and uploads nothing — and none of the existing MeshData producers had to change to gain a coordinate they will never use.

Only meaningful alongside a SurfaceTexture at draw time. Geometry carrying UVs and drawn without a texture shades from its vertex colours exactly as before.

emissiveColoroptionalFloat32ArrayThree floats per vertex: the colour this vertex emits, independent of its albedo.
More

Optional, and absent means what this engine always did — the emissive term is the surface's own colour scaled by emissive, which is right for anything glowing because it is hot or lit from within. It is wrong whenever the glow is a different colour from the paint, and the approximation gets brightness right and hue wrong: a ceiling tile emitting a dull warm haze over a pale panel cannot be expressed by scaling the panel.

A negative component means "inherit the albedo", which is what an absent array supplies for every vertex — so a mesh that names a colour for some of its geometry and not the rest is one buffer rather than two meshes.

roughnessoptionalFloat32ArrayOne float per vertex, 0–1: how rough this surface is, which is the shape of its highlight rather than its strength.
More

Absent means the old constant, so nothing existing changes. It matters because a highlight's width is not something intensity can express: a polished floor seen at a grazing angle smears a lamp into a long streak down the view direction, and a tight fixed lobe can only ever make a small round dot brighter. specular says how much light comes back; this says over how wide an angle.

grainoptionalFloat32ArrayOne float per vertex, 0–1: how much visible mineral structure this surface has.
More

Absent means none, which is the one thing the previous two attempts could not say. Grain was first gated on specular > 0, then weighted by roughness, and both are proxies rather than statements: painted plaster is rough and has no grain, polished granite is smooth and has a great deal of it. Deriving either property from the other guesses, and the guess was wrong in both directions — a painted tower at roughness 0.55 took 55% grain and read as marble.

So it is its own attribute, exactly as specular and roughness are, and a surface states it while it is being built. This is the material half of the answer; how strong the pattern is at a given amount belongs to the shader's constants.

reliefoptionalFloat32ArrayHow much microscopic relief a surface has, 0 to 1. Absent means a perfectly smooth one.
More

The sibling of grain, and the difference between them is the whole point. Grain says how much light a point takes, so it varies brightness across a face that stays flat. This says which way the surface is facing, so it varies the direction light leaves it. Only the second gives a surface texture that survives a shallow angle and moves as you walk past: asphalt aggregate, cast concrete, orange peel on paint, hammered metal, plaster stipple. Neither stands in for the other, which is why it is a second attribute rather than a weighting of the first.

Absent means none, so every mesh built before this existed is unchanged. How coarse the relief is and how strong belongs to the material rather than the geometry, since that is what separates asphalt from plaster, and the pass states it: see Renderer.setSurfaceRelief.

lightmapUvsoptionalFloat32ArrayA second set of texture coordinates, two floats a vertex, for a baked lightmap: where on its page a surface's baked light is, as a material made with lightmapModel reads it. Absent means a mesh nothing has baked light for.
More

It rides the grain and relief attributes, which a lightmapped surface gives up: a renderer's vertex attributes and the stage's inter-stage values are spent to the last one, and baked stage geometry carries neither procedural grain nor relief. So a mesh with any grain or relief that is not zero is refused with these, rather than one of the two quietly lost; lanes of zeros, which a builder writes for every mesh, are what a surface without either carries.

layersoptionalFloat32ArrayWhich image of a texture array each vertex's face wears: one whole number a vertex, 0 the first layer. Absent means layer 0, which is the only layer a plain texture has.
More

What it is for is a merged mesh wearing many images in one draw. A block of forty buildings with forty facades is forty materials — forty draws, forty material changes — when each image is its own texture; with the facades as the layers of one array and each face naming its layer, it is one. Renderer.createSurfaceTextureArray builds the array, and every map of a material (albedo, normal, ORM, emissive) is read at the same layer, so an albedo and its emissive twin share an index.

It travels in the texture-coordinate attribute, as its third component, because all sixteen of WebGL2's guaranteed vertex locations are spent in the instanced variant. So it needs uvs beside it and is refused without them: a layer with no coordinates addresses nothing.

Whole numbers, checked, because the shader rounds to the nearest layer and a 1.5 would land on whichever side rounding favoured on that device.

channeloptionalFloat32ArrayFour floats per vertex, and the only attribute whose lanes mean four different things.
More

.x sway: how far the shared wind moves this vertex, along the wind's own direction. .y skyDirect: how much of the directional term this vertex receives, 0 to 1. .z alpha: multiplies the draw's own opacity and the texture's cutout coverage. .w is reserved, declared and unread.

One attribute and not three, because locations are the scarce resource here. WebGL2 guarantees sixteen vertex attribute locations, eleven are already spent, and an instanced draw spends all sixteen. A vec4 costs the same one location a float would, so the spare lane is free and the next per-vertex question does not have to re-argue the budget.

skyDirect is a separate lane rather than a factor in colors, and that is the defect it exists to fix. The shader reads albedo = vColor and then lit = albedo * (ambient + sun), so a sky factor carried in the vertex colour scales both terms and an enclosed face is darkened twice — once for having no sky, once for the ambient it should still have received. A consumer reporting this raised a floor constant to 0.45 to compensate and measured what it cost: 55% of a chunk's vertices sat between 0.10 and 0.20.

Absent means (0, 1, 1, 0): planted, fully sunlit, opaque. Every mesh built before this existed is unchanged to the bit, which is what the absent-attribute constant buys.

Sway is authored, not derived. scatter.ts squares its own falloff because it computes one from height, where a linear response slides a whole plant sideways and reads as the ground moving. Here the author writes the curve — 0 on a trunk, 1 at a leaf tip — so squaring it would overrule a shape somebody had already chosen. What that costs is that a lane filled linearly up a trunk gives a tree that slides at its base, and the fix is the curve.

jointsoptionalFloat32ArrayFour floats per vertex: which joints move this vertex, as indices into a skinning palette.
More

Optional for the same cost reason as every attribute above it, and the reason is strongest here: only a skinned character has any use for one, and a world of walls and props allocates nothing and uploads nothing. Absent means unskinned, and an unskinned mesh takes exactly the draw path it took before skinning existed.

Four here, and four more in joints2 for a mesh that has them. Four is what glTF's JOINTS_0 carries and what most exports write, and it fits one attribute; a character authored for eight — a face, a shoulder — loses the shape of its deformation when the four lightest are dropped, so 4.8.4 added a second set rather than widening this one. A mesh without it pays nothing for it, and importers sort heaviest first, so this set always holds the four that matter most.

Float32Array and not Uint8Array, which costs twelve bytes a vertex. Every attribute in this format is float32 and the absent-attribute mechanism depends on it: mesh.ts attaches with gl.FLOAT, ABSENT_ATTRIBUTE is a number array, and buffers.ts interleaves floats and writes float constants for what a mesh omits. An integer attribute needs vertexAttribIPointer on one backend, a uint8x4 entry on the other, and an integer arm through the constants buffer — four places where one question starts being answered twice. Integers to 2^24 are exact in float32, so an index is never rounded. What would make it wrong is a consumer whose payload is dominated by skinned meshes; the fix is a uint8x4 attribute and that fourth mechanism.

weightsoptionalFloat32ArrayFour floats per vertex: how much each of joints' four influences moves this vertex.
More

Normalised by the importer rather than by the shader, because normalising per vertex per frame costs a divide on every vertex to correct data that should have been fixed once. gltfSkin.ts normalises on the way in and warns when a set does not sum to one.

Absent means unskinned, and it must be absent exactly when joints is — one without the other is refused below rather than drawn.

joints2optionalFloat32ArrayFour floats per vertex: the fifth to eighth joints that move this vertex, for a rig authored with eight influences. The same encoding as joints, and the same both-or-neither with weights2.
More

Present only with joints, because the importers sort a vertex's influences heaviest first: the first set is always the four that matter most, and a second set alone would be four of the lightest with the heaviest missing. What it costs is thirty-two bytes a vertex on a mesh that carries it, and nothing on one that does not — a renderer draws such a mesh through a vertex variant that reads two more attributes, and every other mesh through the variant it always had.

weights2optionalFloat32ArrayFour floats per vertex: how much each of joints2' influences moves the vertex.
morphTargetsoptionalFloat32ArrayMorph target position deltas: morphTargetCount targets, three floats a vertex each.
More

Laid out interleaved by vertex — every target of one vertex adjacent — because the shader reads all of a vertex's targets together and nothing reads one target across many vertices. So the index of target t for vertex v is (v * count + t) * 3.

A delta rather than an absolute position, which is what makes them additive: several targets apply at once by weight, and a weight of zero contributes nothing rather than dragging the vertex toward some other shape.

Positions only. A target that also moved normals would double this array and add a second fetch per target in the vertex stage; what it gives up is shading that lags a strongly morphed surface. What would make it wrong is a face rig, where the lighting is most of the effect.

morphTargetCountoptionalnumberHow many targets morphTargets holds. Absent means none, whatever the array says.
indicesUint32Array