GPU Positions
Demonstrates GPU-based position calculation using Three.js TSL (Three Shading Language) nodes, enabling complex animations with minimal CPU overhead. The demo below draws a tunnel of 150 rings in one call: every ring point is computed on the GPU from its progress, its instance index and time.
Open the full GPU Instancing demo
Key Concepts
Instead of providing vertex positions as a buffer, you define a TSL function that computes positions based on:
progress- Normalized position along the line (0 to 1)time- Global time uniform for animations- Any custom uniforms you define
Benefits
- Performance - Calculations happen in parallel on GPU
- Memory Efficient - No need to store position arrays
- Dynamic - Easy to create complex, animated shapes
- Parametric - Change shape by modifying shader parameters
Use Cases
- Animated visualizations
- Parametric curves and surfaces
- Music visualizers
- Data-driven graphics
- Procedural line generation
Code Implementation
import { MeshLine } from 'makio-meshline'
import { cos, Fn, mix, sin, time, uniform, vec3 } from 'three/tsl'
const SEGMENTS = 128
const TWO_PI = Math.PI * 2
// Generate a circle in the XY plane from normalized per-vertex progress.
const circlePosition = Fn( ( [progress] ) => {
const angle = progress.add( time.negate() ).mul( TWO_PI )
return vec3( cos( angle ), sin( angle ), 0 )
} )
// Generate a wave in the XY plane from the same normalized progress.
const wavePosition = Fn( ( [progress] ) => {
const y = sin( progress.mul( Math.PI * 4 ).add( time.negate().mul( 4 ) ) )
return vec3( progress.oneMinus().mul( 2 ).sub( 1 ), y.mul( 0.5 ), 0 )
} )
const useWave = uniform( 0 )
const positionNode = Fn( ( [progress] ) => {
return mix( circlePosition( progress ), wavePosition( progress ), useWave )
} )
// Create line with GPU position node
const line = new MeshLine()
.segments( SEGMENTS )
.closed( true )
.gpuPositionNode( positionNode )
.lineWidth( 0.1 )
.gradientColor( 0x0000ff )
.color( 0xffffff )
.build()At Scale: One Node, Many Lines
Combine gpuPositionNode() with instances() and read instanceIndex inside the node to give every instance its own shape. The tunnel demo places each ring at a depth derived from its index, so the CPU never uploads a position:
import { MeshLine } from 'makio-meshline'
import { cos, float, Fn, instanceIndex, mod, sin, uniform, vec3 } from 'three/tsl'
const RING_COUNT = 150
const TUNNEL_LENGTH = 90
const travel = uniform( 0 ) // advanced every frame on the CPU
const ringPosition = Fn( ( [progress] ) => {
const z = mod( float( instanceIndex ).mul( TUNNEL_LENGTH / RING_COUNT ).add( travel ), TUNNEL_LENGTH ).sub( TUNNEL_LENGTH )
const angle = progress.mul( Math.PI * 2 )
return vec3( cos( angle ).mul( 2.6 ), sin( angle ).mul( 2.6 ), z )
} )
const tunnel = new MeshLine()
.instances( RING_COUNT )
.segments( 192 )
.closed( true )
.gpuPositionNode( ringPosition )
.lineWidth( 0.05 )
.build()When a ring wraps back to the far end, the demo recolors only that ring with setInstanceValues(), uploading a few floats instead of the whole instance buffer.