MeshLineGeometry
The MeshLineGeometry class builds the line mesh geometry from raw point data, handling the complex vertex calculations needed for thick, smooth lines.
Quick Links:
- Common Patterns - Basic geometry usage examples
- Advanced Patterns - Dynamic updates and performance tips
- MeshLine Class - Main API reference
Constructor
new MeshLineGeometry(options?: MeshLineGeometryOptions)MeshLineGeometryOptions
type LinePoint = [x: number, y: number, z?: number] | THREE.Vector2 | THREE.Vector3 | { x: number, y: number, z?: number }
type LinePoints = Float32Array | number[] | THREE.BufferGeometry | LinePoint[]
interface MeshLineGeometryOptions {
lines?: LinePoints | LinePoints[] // One line or multiple lines
points?: LinePoints | LinePoints[] // Alias for lines
closed?: boolean | boolean[] // Close the loop(s)
lineWidth?: number // Used for world-space bounding-volume padding
sizeAttenuation?: boolean // Whether lineWidth is scene-space (pads bounds)
miterLimit?: number // Miter limit factor for bounds padding
join?: MeshLineJoinType | MeshLineJoinOptions
vertexColors?: Float32Array | number[] | null
widthCallback?: (progress: number, pointIndex?: number, lineIndex?: number) => number
usage?: THREE.Usage // StaticDrawUsage / DynamicDrawUsage / StreamDrawUsage
dynamic?: boolean // Shorthand for DynamicDrawUsage
frustumCulled?: boolean // Skip bounds computation when false
verbose?: boolean // Console logging
// Optional CPU-side corner smoothing (see "Smooth sharp bends" below)
smoothSharpBends?: boolean // default false; skipped under gpuPositionNode
smoothSharpBendsAlpha?: number // default 0.001
smoothSharpBendsThreshold?: number // default -0.5 (dot(dir_in, dir_out) cutoff)
// Flags to include / exclude generated attributes (advanced)
needsPositions?: boolean
needsPrevious?: boolean
needsNext?: boolean
needsUV?: boolean
needsSide?: boolean
needsProgress?: boolean
needsWidth?: boolean
needsVertexColor?: boolean
}MeshLineGeometry mirrors most of MeshLine's geometry-related options and can be used directly when you need fine-grained control.
Smooth sharp bends
Screen-space meshlines fundamentally can't render a ribbon cleanly through a single vertex whose two adjacent segments diverge at a near-hairpin angle — the bisector collapses and the ribbon picks up spikes or bowtie artifacts at oblique camera views. Industrial wide-line libraries (Mapbox GL, Cesium, Spite's original MeshLine) all sidestep this by subdividing sharp corners on the CPU before handing vertices to the shader.
MeshLineGeometry can do the same when smoothSharpBends is enabled:
- Default off. The authored logical points stay unchanged unless you enable
smoothSharpBends. Regular billboard geometry expands every logical point into four physical GPU vertices; when smoothing is enabled, every point whose interior bend is sharper than ~60° (i.e.dot(dir_in, dir_out) < -0.5) is first replaced by two cutoff points sittingsmoothSharpBendsAlphaof the way back along each adjacent segment. smoothSharpBendsAlpha(default0.001once smoothing is enabled) controls how much of the peak you sacrifice. The default is small enough that the cutoff is visually imperceptible while keeping the shader miter math stable; larger values flatten the tip into a bevel-like cap. The bend angles at the new vertices are fixed by the original corner angle —αonly controls how visible the cutoff is.smoothSharpBendsThreshold(default-0.5) is thedot(dir_in, dir_out)cutoff below which a vertex is considered "too sharp". Lower (more negative) values subdivide only the very sharpest corners.
new MeshLine({
lines: myZigzag,
smoothSharpBends: true, // opt in: changes topology at sharp corners
smoothSharpBendsAlpha: 0.001, // default once enabled — near-imperceptible cutoff
})
.join({ limit: 2 }) // pair with a tighter miter clamp for zigzag-style polylinesTuning for very sharp polylines: the default α is usually fine once smoothing is enabled. If you want a visibly flatter bevel at the tip, raise α to 0.05–0.1. Pair a small α with a lower miterLimit (around 2): the geometry pass handles sharp corners below the threshold and the tighter miter clamp flattens any residual spikes above it into clean bevels.
Leave smoothSharpBends off when you need a stable mapping from authored points to physical vertices — for example, when animating custom vertex attributes. The regular billboard mapping is four physical rows per logical point, not one row per point.
ℹ️ GPU-deformed lines: when
gpuPositionNodeis set, the CPU polyline is a straight-line template whose point count drives the progress grid the GPU samples against. CPU smoothing is skipped in that case — subdividing the template would shift progress values and break GPU position lookups. The facade also disables frustum culling by default forgpuPositionNodeand for geometry-deformingpositionFn,previousFn,nextFn,widthFn,normalFn, andvertexFn, because CPU bounds cannot predict arbitrary shader deformation. If you need culling, provide conservative bounds and explicitly enable it.
Methods
buildLine()
buildLine(options?: MeshLineGeometryOptions): voidApplies geometry options and rebuilds the managed attributes. Options omitted from a later call keep their previous values, and same-sized managed buffers are reused where possible. Without lines, it rebuilds the current points, including in-place setPositions() and setPositionRange() edits. Custom vertex attributes are preserved while the line/point topology is unchanged. If topology changes, they are removed because their old vertex mapping is invalid; recreate and refill them after the rebuild. Instanced attributes are not removed by this topology guard.
setLines()
setLines(
lines: LinePoints | LinePoints[],
closed?: boolean | boolean[]
): voidReplace or initialize the geometry with one or multiple line segments. A single line can be passed directly; pass an array of line inputs for multiple disconnected lines. Optionally pass closed (boolean or per-line boolean[]) to update loop closure at the same time.
When a THREE.BufferGeometry is provided, the positions are extracted from its 'position' attribute. This allows direct conversion of existing Three.js geometries into MeshLine format.
Parameters
lines– A line input, or an array of line inputs for multiple disconnected lines. Each line can be:Float32Arrayof flattened XYZ coordinates- Flat numeric XYZ array
- Nested number array of
[x,y,z]coordinates Vector2,Vector3, or plain{ x, y, z? }point arraysTHREE.BufferGeometrywith a 'position' attribute
closed(optional) –booleanorboolean[]per line to close or open loops during the rebuild.
dispose()
dispose(): voidReleases geometry resources. Call when the geometry is no longer needed.
setPositions()
setPositions(
positions: LinePoints | LinePoints[],
updateBounding?: boolean
): voidEfficiently updates vertex positions without rebuilding GPU buffers. The function supports:
• Float32Array – update a single line.
• Float32Array[] – update multiple lines (each array must keep its original length).
• Arrays of tuples, vectors, or point objects are converted under the hood (slower, avoid in hot loops).
If the line count or point count changes, the geometry falls back to a full rebuild automatically using setLines(). This ensures proper buffer allocation but is less efficient than in-place updates. It also removes custom vertex attributes whose mapping is no longer valid. For best performance, maintain consistent line counts and point counts when using setPositions() and recreate custom attributes after an intentional topology change.
• positions – Must match the original line(s) vertex count exactly. Re-use the same typed arrays each frame for best performance.
• updateBounding – Recomputes bounding volumes when true (default false). Skip when the line stays roughly inside view.
Example with multiple dynamic lines:
const lines = [ new Float32Array( NUM * 3 ), new Float32Array( NUM * 3 ) ]
const geometry = new MeshLineGeometry({ lines });
function animate() {
updateFirstLine(lines[0])
updateSecondLine(lines[1])
geometry.setPositions( lines ); // uploads changes for both lines
requestAnimationFrame( animate );
}Pass true as the second argument when dynamic edits can move the line outside its previous bounds and frustum culling should stay exact.
setPositionRange()
setPositionRange(
positions: ArrayLike<number> | LinePoint[],
startPoint: number,
options?: { lineIndex?: number; updateBounding?: boolean }
): MeshLineGeometryUpdates a fixed-size span of an existing CPU billboard line. Only the changed position components and the one-point previous/next dependency halo are marked for GPU upload. startPoint addresses canonical source points; lineIndex defaults to 0 for a single line.
const patch = new Float32Array( 128 * 3 )
updateBrushStroke( patch )
geometry.setPositionRange( patch, 2048, { updateBounding: true } )The edit must keep topology stable: it cannot add/remove points, create consecutive duplicates, or run with smoothSharpBends. If a feature uses normalized arc length, its progress/UV/endpoint/width data is recomputed for the affected line because moving one point changes downstream distances. Use setPositions() for surface geometry or any topology-changing edit.
setUsage()
setUsage(usage: THREE.Usage, name?: string | null): voidChanges the Three.js usage hint on managed buffer attributes. Omit name to update every managed attribute, or pass an attribute name such as 'position' to target one. Existing attributes are replaced with clones carrying the requested usage, so call this before retaining direct attribute references.
Usage Examples
For practical examples, see:
- Basic Line Creation in Common Patterns
- Multi-Line Segments for multiple disconnected lines
- Dynamic Updates for efficient position updates
BufferGeometryinput for converting an existing geometry through itspositionattribute
Bevel joins
The billboard profile expands a polyline into four vertices per point — incoming +/- then outgoing +/- — but by default it only wires the segment bodies, six indices per segment. While a corner's miter is within join.limit both role pairs land on the same pixels; past the limit they separate toward their own incident segments, which blunts the corner and leaves an unfilled notch on the outside of the turn.
Opting into a bevel join fills that notch:
line.join( { type: 'bevel', limit: 2 } )This adds no vertices. It emits six more indices per interior join, bridging each point's already-allocated incoming cross-section to its outgoing one. The two triangles of that bridge are antipodal pairs about the join, so they always wind oppositely and front-face culling keeps exactly the one covering the outer notch, whichever way the corner turns. While the miter is supported both triangles are exactly zero-area, so a bevel join costs nothing on corners that did not need it.
It is opt-in because it changes index.count — from (points - 1) * 6 to (points - 1) * 6 + joins * 6, where joins is points - 2 for an open line and points - 1 for a closed one — which callers, toNonIndexed() expansion order, and GPU chunk culling can all observe.
Two caveats:
- The bridge covers the outer notch and also re-covers ground the two bodies already hold on the inner side. That is free for an opaque line and double-blends for a transparent or additively blended one.
- In geometry, it is a flat bevel bridge rather than a tessellated triangle fan. On billboards,
line.join( { type: 'round' } )orline.join( { fold: 'exact' } )clips the outer corner to a round silhouette in the fragment shader; for tessellated triangle-fan round joins and caps in geometry, use the world-space surface profile or the optional GPU path.
The alternative for very sharp static polylines remains smooth sharp bends, which subdivides the corner on the CPU instead of filling it on the GPU. The two are independent and can be combined.
Internal Structure
MeshLineGeometry manages the following attributes. They are allocated only when the corresponding needs* flag or active feature requires them. Direct low-level construction enables the core strip attributes by default; the high-level MeshLine facade prunes attributes that its current configuration does not consume.
For logical point i, physical rows 4i + 0…3 are ordered [incoming+, incoming-, outgoing+, outgoing-]. Segment i connects the outgoing pair of point i to the incoming pair of point i + 1. At an ordinary projected join the shader welds both role pairs to the same bounded miter. Near a projected reversal, or when a segment points almost directly at the camera, it reconstructs each incident body from its own valid direction instead. Keeping those roles topologically separate prevents the two sides from crossing into a bowtie or unbounded wedge.
position— expanded vertex positions; enabled by default for CPU billboard geometry.previous/next— neighbours used for direction calculation; the facade omits them for a fully GPU-positioned path unless explicitly requested.side— optional explicit+1or-1body-side data. Direct geometry enables it by default; the facade allocates it only whenneedsSideis enabled because the built-in material can decode side fromjoinRole.joinRole— managed internal packed metadata in[-1, -2, +1, +2]order for[incoming+, incoming-, outgoing+, outgoing-]. Its sign owns the incoming/outgoing body and its magnitude encodes the+/-side (1is+,2is-). It is always present, so both properties survivetoNonIndexed()without a separatesideattribute; do not reuse this name for a custom attribute.width— per-vertex width multiplier, allocated forwidthCallbackorneedsWidth.uv— texture coordinates, allocated for textures and UV/fragment hooks orneedsUV.progress— normalized arc-length progress, allocated when gradients, dashes, callbacks, GPU positions, or hooks consume it, or whenneedsProgressis enabled.endpoint— internal endpoint/progress metadata allocated for GPU-position and position/neighbour hook paths.vertexColor— per-vertex RGB data allocated when vertex colors are active.
The exact attribute set is intentionally configuration-dependent; inspect geometry.attributes after buildLine() or line.ensureBuilt() instead of assuming every optional attribute exists.