Skip to content

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:

Constructor ​

ts
new MeshLineGeometry(options?: MeshLineGeometryOptions)

MeshLineGeometryOptions ​

ts
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:

Without smoothingsingle hairpin vertex → ribbon spike · width collapsespike / bowtie artifactWith smoothSharpBends · α (illustrative)hairpin split into two cutoff vertices → clean beveloriginal vertexα · segment
  • 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 sitting smoothSharpBendsAlpha of the way back along each adjacent segment.
  • smoothSharpBendsAlpha (default 0.001 once 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 the dot(dir_in, dir_out) cutoff below which a vertex is considered "too sharp". Lower (more negative) values subdivide only the very sharpest corners.
js
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 polylines

Tuning 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 gpuPositionNode is 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 for gpuPositionNode and for geometry-deforming positionFn, previousFn, nextFn, widthFn, normalFn, and vertexFn, because CPU bounds cannot predict arbitrary shader deformation. If you need culling, provide conservative bounds and explicitly enable it.

Methods ​

buildLine() ​

ts
buildLine(options?: MeshLineGeometryOptions): void

Applies 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() ​

ts
setLines(
  lines: LinePoints | LinePoints[],
  closed?: boolean | boolean[]
): void

Replace 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:
    • Float32Array of flattened XYZ coordinates
    • Flat numeric XYZ array
    • Nested number array of [x,y,z] coordinates
    • Vector2, Vector3, or plain { x, y, z? } point arrays
    • THREE.BufferGeometry with a 'position' attribute
  • closed (optional) – boolean or boolean[] per line to close or open loops during the rebuild.

dispose() ​

ts
dispose(): void

Releases geometry resources. Call when the geometry is no longer needed.

setPositions() ​

ts
setPositions(
  positions: LinePoints | LinePoints[],
  updateBounding?: boolean
): void

Efficiently 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:

js
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() ​

ts
setPositionRange(
  positions: ArrayLike<number> | LinePoint[],
  startPoint: number,
  options?: { lineIndex?: number; updateBounding?: boolean }
): MeshLineGeometry

Updates 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.

js
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() ​

ts
setUsage(usage: THREE.Usage, name?: string | null): void

Changes 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:

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:

js
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' } ) or line.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.

Input polyline · N pointsCPU pointsexpandIndexed segment bodies · 4N verticesside = +1side = -1each point → incoming +/- and outgoing +/-; role pairs overlap at regular joins

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 +1 or -1 body-side data. Direct geometry enables it by default; the facade allocates it only when needsSide is enabled because the built-in material can decode side from joinRole.
  • 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 (1 is +, 2 is -). It is always present, so both properties survive toNonIndexed() without a separate side attribute; do not reuse this name for a custom attribute.
  • width — per-vertex width multiplier, allocated for widthCallback or needsWidth.
  • uv — texture coordinates, allocated for textures and UV/fragment hooks or needsUV.
  • progress — normalized arc-length progress, allocated when gradients, dashes, callbacks, GPU positions, or hooks consume it, or when needsProgress is 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.