World-Space Surfaces
Use the surface profile for roads, tracks, trails, and flat ribbons that must live in the scene as real geometry. It expands the supplied polyline on the CPU, writes positions and normals, adds only the optional attributes used by the active material, and shades the result with a standard PBR material. The surface builder consumes your point topology as given; curve fitting and longitudinal subdivision belong in the path generator.
import { MeshLine } from 'makio-meshline'
const road = new MeshLine()
.lines( roadPoints )
.lineWidth( 4 )
.color( 0xffffff )
.map( roadTexture )
.surface( {
frame: 'transport',
up: [0, 1, 0],
bank: progress => Math.sin( progress * Math.PI * 2 ) * 0.05,
uvMode: 'distance',
uvScale: 5,
roughness: 0.9,
metalness: 0,
} )
.join( { type: 'miter', limit: 3, segments: 8 } )
.cap( { type: 'round', segments: 8 } )
.build()
scene.add( road )lineWidth is the full width in world units. It does not change with camera distance.
See V2 API and Demo Coverage for the complete surface demo set and the shared WebGL2 verification switch.
Surface and Screen Profiles
The two profiles solve different geometry problems and intentionally remain separate.
| Screen profile (default) | Surface profile | |
|---|---|---|
| Best for | Strokes, graphs, trails, outlines | Roads, tracks, flat ribbons |
| Expansion | Camera-facing in the vertex shader | Camera-independent world geometry |
| Width | CSS pixels or projected scene width | World units |
| Shading | Line gradients, dashes, maps, TSL hooks | Lit PBR surface and texture maps |
| Geometry frame | Depends on the active camera | Transported or fixed-up frame |
Calling .surface( options ) selects the surface profile. Do not use it as a pixel-perfect HUD line: use the default screen profile for that job. Screen-only dash, gradient, and billboard hook options do not define the surface material.
Surface Options
Pass these fields to .surface( options ) or to the surface member of either low-level constructor:
| Option | Type | Default | Contract |
|---|---|---|---|
frame | 'transport' | 'fixed-up' | 'transport' | Rotation-minimizing transport for arbitrary 3D paths, or projection against a preferred up vector. |
up | Vector3, XYZ tuple, or { x, y, z } | [0, 1, 0] | Finite, non-zero initial/reference direction. |
bank | number, per-point/per-line arrays, or callback | 0 | Rotation around the tangent in radians. The callback receives (progress, pointIndex, lineIndex). |
uvMode | 'distance' | 'stretch' | 'distance' | Physical repeats or one normalized U span per path. |
uvScale | number | 1 | Positive world units represented by one U repeat in distance mode. |
normalMap | Texture | null | null | Enables tangent generation and UVs. |
roughnessMap | Texture | null | null | Standard PBR roughness texture; enables UVs. |
metalnessMap | Texture | null | null | Standard PBR metalness texture; enables UVs. |
roughness | number | 0.9 | Base PBR roughness. |
metalness | number | 0 | Base PBR metalness. |
side | Three.js side constant | FrontSide | Use DoubleSide when the underside must render. |
Surface construction also uses the facade's normal geometry controls:
| Related option | Default | Surface behavior |
|---|---|---|
lineWidth | 0.3 | Full world-space width. |
widthCallback | null | Arc-length progress multiplier for the full width. |
closed | false | Accepts one boolean or one value per batched line; transport frames correct a closed-loop seam. |
join | { type: 'miter', limit: 4, segments: 8 } | True miter, bevel, or round surface topology. 'simple' is accepted as a miter alias. |
cap | { type: 'butt', segments: 8 } | Butt, square, or round open-path endpoint. Ignored for closed paths. |
usage / dynamic | StaticDrawUsage / false | Selects generated attribute usage; dynamic: true defaults to stream usage unless usage is explicit. |
frame
'transport'(default) uses a rotation-minimizing frame. It is the stable general choice for arbitrary 3D paths and applies a distributed seam correction to closed loops.'fixed-up'projectsupagainst each path tangent. It is the right choice for terrain roads that should stay level across their width: each row depends only on its own tangent, so editing one part of the path never rolls the rest of it. When the tangent andupbecome parallel, the geometry falls back to a deterministic valid frame.
up
The initial or fixed reference direction. It accepts a Three.js vector or an array such as [0, 1, 0]. It must be finite and non-zero.
bank
Rotation around the path tangent, in radians. Use a single number, one value per point, or a callback:
.surface( {
bank: ( progress, pointIndex, lineIndex ) => progress * 0.1
} )Banking rotates both the surface normal and its side direction, so lighting and geometry stay aligned. Per-point arrays address the final supplied centerline points. If bank is authored on a smaller set of controls, interpolate it while sampling the path before passing the resulting points and bank values to MeshLine.
uvMode and uvScale
uvMode: 'distance'(default) setsu = distance / uvScale. Distance is the accumulated Euclidean length of the supplied XYZ segments, so climbs and descents contribute to U. A texture repeats consistently even when points are unevenly spaced.uvScalemust be greater than zero.uvMode: 'stretch'maps the complete path tou = 0…1.uvScaledoes not change stretched UVs.
v always runs from the left edge to the right edge.
Point Density and Supplied Topology
Surface geometry expands one cross-road row at every supplied centerline point. It does not infer a spline, insert rows along a long segment, or smooth a sparse corner. Bevel and round joins can add local fan vertices, but they do not increase the fidelity of the centreline between input points.
Sample curved or rotating sources before calling .lines() or .setPositions(). A robust road planner should bound at least:
- the maximum 3D edge length;
- the curve-to-chord deviation;
- the tangent direction change between rows; and
- the bank change between rows.
These are geometry-quality budgets, not texture-repeat budgets. Perspective-correct UV interpolation is exact on a straight triangle, while curves, elevation changes, and rotation need enough rows to approximate their shape and frame. Distance UVs then use the resulting full 3D polyline length, so adaptive or uneven point spacing does not stretch the texture.
The Road Surface example demonstrates this workflow with exact raw-planner XYZ anchors, an iterative terrain-fit/re-plan pass, adaptive cubic and circular spans, per-point elevation and bank, level fixed-up frames, and compact miter topology.
PBR options
The surface material accepts:
roughnessandmetalnessnormalMap,roughnessMap, andmetalnessMapside, when the underside must also render
Use the regular fluent methods for base color, diffuse map, alpha map, opacity, alpha test, transparency, and wireframe.
Width Along the Path
widthCallback returns a multiplier for the full lineWidth. Its progress argument is based on arc length, not point index:
const ribbon = new MeshLine()
.lines( points )
.lineWidth( 2 )
.widthCallback( progress => 0.35 + Math.sin( progress * Math.PI ) * 0.65 )
.surface()
.build()Consecutive duplicate points are compacted before the surface is built. Non-finite coordinates and paths without enough distinct points are rejected instead of producing corrupt GPU buffers.
Keep centerline coordinates near the object's local origin and place the MeshLine with its object transform. If Float32 conversion would collapse a segment or materially deform its width, surface construction throws with a rebasing message instead of emitting zero-area road triangles.
Joins, Caps, and Reversals
The surface builder has a compact fast path and a variable-topology path. Both consume the supplied centreline; neither resamples it:
- Ordinary
miter+buttroads stay on the two-vertices-per-row strip whenever every miter fits insidejoin.limit. bevelemits separate incoming/outgoing outer corners and a real fill wedge.roundemits an outer arc.join.segmentsis the subdivision budget for a 180° turn; shallower corners use proportionally fewer triangles.- An over-limit
miterbevels the outer corner instead of producing a spike; the inner edge still uses its exact offset-line intersection, independently ofjoin.limit. join.innerdefaults to'exact', which rejects intersecting inner footprints. Opt into'clip'for continuously edited roads that should keep their centerline and trim only the infeasible inner edge.- Open paths support
butt,square, and tessellatedroundcaps.cap.segmentsis the full semicircle budget. - A zero-radius reversal has no unique tangent or finite miter. It is split into two independent runs and each half follows the selected cap policy, so no triangle bridges across the cusp.
Square/round cap UVs extend by physical path distance, and closed paths duplicate their texture seam before emitting seam joins. With the default inner: 'exact', the builder validates the combined inner setbacks on every segment and rejects a road whose adjacent joins would cross. Use inner: 'clip' only when preserving a dynamic centerline is more important than exact offset edges; it proportionally bounds the inner footprint while retaining the configured outer join.
Low-Level Class API
Most projects should use MeshLine().surface(). Advanced integrations can compose the profile directly:
import {
MeshLineSurfaceGeometry,
MeshLineSurfaceNodeMaterial,
} from 'makio-meshline'
const geometry = new MeshLineSurfaceGeometry( {
lines: roadPoints,
lineWidth: 4,
surface: { frame: 'fixed-up', up: [0, 1, 0] },
join: { type: 'round', limit: 3, segments: 8, inner: 'clip' },
cap: { type: 'round', segments: 8 },
} )
const material = new MeshLineSurfaceNodeMaterial( {
color: 0x303030,
surface: { roughness: 0.9, metalness: 0 },
} )MeshLineSurfaceGeometryOptions accepts the related geometry fields above plus these buffer controls:
| Option | Default | Effect |
|---|---|---|
needsUV | false | Forces uv; maps also enable it automatically. |
needsTangents | false | Forces four-component tangents; surface.normalMap also enables them automatically. |
needsProgress | false | Adds one arc-length progress value per emitted vertex. |
needsSide | false | Adds the generated left/right side value. |
MeshLineSurfaceNodeMaterial consumes color, map, alphaMap, opacity, alphaTest, transparent, wireframe, depthWrite, and depthTest plus the PBR surface options. Color defaults to white, opacity to 1, alpha test to 0, depth testing to true, and depth writing to !transparent when it is not supplied explicitly.
Class-specific members
The tables list the surface-specific contract. Both classes retain their inherited Three.js BufferGeometry or MeshStandardNodeMaterial APIs.
| Class member | Returns | Contract |
|---|---|---|
new MeshLineSurfaceGeometry(options?) | MeshLineSurfaceGeometry | Optionally builds one or more centerlines immediately. isMeshLineSurfaceGeometry is always true. |
geometry.buildLine(options?) | void | Merges configuration and rebuilds when lines are present. |
geometry.setLines(points, closed?) | void | Replaces centerlines (and optional open/closed state) and rebuilds all derived frames, joins, attributes, indices, and bounds. |
geometry.setPositions(points, updateBounding?, closed?) | void | Surface alias for a complete setLines() rebuild; range updates are intentionally unavailable. |
geometry.setUsage(usage, name?) | void | Replaces all generated attributes, or one named attribute, with the requested usage. |
geometry.dispose() | void | Releases geometry buffers. |
new MeshLineSurfaceNodeMaterial(options?) | MeshLineSurfaceNodeMaterial | Optionally configures the PBR material immediately. isMeshLineSurfaceNodeMaterial is always true. |
material.buildLine(options?) | void | Reconfigures the supported base, alpha, depth, wireframe, and surface PBR fields. |
material.setShadow(enabled) | void | Compatibility method for the facade; surface shadows already use the generated geometry, so it performs no shader rewrite. |
material.dispose() | void | Inherited Three.js material cleanup. |
The geometry always exposes standard position and normal attributes. It adds uv for texture maps, tangent for normal mapping (or low-level needsTangents), and progress/side only when explicitly requested. This keeps the default road buffer compact while remaining compatible with ordinary mesh raycasting, lighting, and tooling.
See the complete Road Surface example for a generated texture and standalone Three.js setup.
The interactive Variable Topology gallery overlays wireframes for every join/cap policy and includes a split reversal case.