SplineContainer manages spline curves defined by a series of knots (control points).
This component stores spline data and generates smooth curves that can be used for animation paths, camera paths, racing tracks, or any curved path in 3D space.

How It Works: The spline is defined by an array of SplineData knots. Each knot contains:

  • Position: The location of the control point
  • Rotation: Orientation at that point (useful for banking/tilting objects along the path)
  • Tangents: Handles that control the curve's smoothness and shape

The component uses Catmull-Rom interpolation to create smooth curves between knots. The curve is automatically
rebuilt when knots are added, removed, or marked dirty, and all sampling methods return positions in world space.

Key Features:

  • Smooth Catmull-Rom curve interpolation
  • Support for open and closed curves
  • Dynamic knot management (add/remove at runtime)
  • World-space sampling with getPointAt and getTangentAt
  • Automatic curve regeneration when modified
  • Built-in debug visualization
  • Integrates seamlessly with SplineWalker

Common Use Cases:

  • Camera paths and cinematics
  • Object movement along curved paths
  • Racing game tracks and racing lines
  • Character patrol routes
  • Procedural road/path generation
  • Animation curves for complex motion
  • Cable/rope visualization
const splineObj = new Object3D();
const spline = splineObj.addComponent(SplineContainer);

// Add knots to define the path
spline.addKnot({ position: new Vector3(0, 0, 0) });
spline.addKnot({ position: new Vector3(2, 1, 0) });
spline.addKnot({ position: new Vector3(4, 0, 2) });
spline.addKnot({ position: new Vector3(6, -1, 1) });

// Sample a point halfway along the spline
const midpoint = spline.getPointAt(0.5);
console.log("Midpoint:", midpoint);
const loopSpline = gameObject.addComponent(SplineContainer);
loopSpline.closed = true; // Makes the spline loop back to the start

// Add circular path knots
for (let i = 0; i < 8; i++) {
const angle = (i / 8) * Math.PI * 2;
const pos = new Vector3(Math.cos(angle) * 5, 0, Math.sin(angle) * 5);
loopSpline.addKnot({ position: pos });
}
const spline = gameObject.getComponent(SplineContainer);

// Sample 10 points along the spline
const points: Vector3[] = [];
for (let i = 0; i <= 10; i++) {
const t = i / 10; // 0 to 1
const point = spline.getPointAt(t);
points.push(point);
}

// Get tangent (direction) at 75% along the spline
const tangent = spline.getTangentAt(0.75);
console.log("Direction at 75%:", tangent);
const spline = gameObject.getComponent(SplineContainer);

// Add a new knot dynamically
const newKnot = new SplineData();
newKnot.position.set(10, 5, 0);
spline.addKnot(newKnot);

// Remove the first knot
spline.removeKnot(0);

// Modify existing knot
spline.spline[1].position.y += 2;
spline.markDirty(); // Tell the spline to rebuild
// Set up spline path
const spline = pathObject.addComponent(SplineContainer);
spline.addKnot({ position: new Vector3(0, 0, 0) });
spline.addKnot({ position: new Vector3(5, 2, 5) });
spline.addKnot({ position: new Vector3(10, 0, 0) });

// Make object follow the spline
const walker = movingObject.addComponent(SplineWalker);
walker.spline = spline;
walker.speed = 2; // Units per second
walker.loop = true;

Debug Visualization: Add ?debugsplines to your URL to enable debug visualization, which draws the spline curve as a purple line. You can also enable it programmatically:

spline.debug = true; // Show debug visualization
  • SplineWalker - Component for moving objects along a spline path
  • SplineData - The knot data structure used to define spline points
  • getPointAt - Sample positions along the spline
  • getTangentAt - Get direction vectors along the spline
  • addKnot - Add control points to the spline
  • removeKnot - Remove control points from the spline

Hierarchy (View Summary)

Properties

gameObject: GameObject

Reference to the GameObject this component is attached to This is a three.js Object3D with additional GameObject functionality

guid: string = "invalid"

Unique identifier for this component instance, used for finding and tracking components

sourceId?: string

Identifier for the source asset that created this component. For example, URL to the glTF file this component was loaded from

spline: SplineData[] = []

Array of knots (control points) that define the spline curve.

Each element is a SplineData object containing position, rotation, and tangent information. You can directly access and modify this array, but remember to call markDirty afterwards to trigger a curve rebuild.

Best practices:

  • Use addKnot and removeKnot methods for automatic dirty marking
  • If modifying knots directly, always call markDirty afterwards
  • The order of knots determines the path direction
console.log(`Spline has ${spline.spline.length} knots`);

// Access first knot
const firstKnot = spline.spline[0];
console.log("Start position:", firstKnot.position);

// Modify and mark dirty
spline.spline[2].position.y += 5;
spline.markDirty();
  • SplineData for the knot data structure
  • addKnot for adding knots (auto marks dirty)
  • removeKnot for removing knots (auto marks dirty)
  • markDirty to trigger rebuild after manual modifications

Accessors

  • get "[$componentName]"(): undefined | string

    Get the original component type name before minification (available if the component is registered in the TypeStore)

    Returns undefined | string

  • get activeAndEnabled(): boolean

    Checks if this component is currently active (enabled and part of an active GameObject hierarchy) Components that are inactive won't receive lifecycle method calls

    Returns boolean

    True if the component is enabled and all parent GameObjects are active

  • get closed(): boolean

    Returns boolean

  • set closed(value: boolean): void

    Whether the spline forms a closed loop.

    When true:

    • The spline connects the last knot back to the first knot, forming a continuous loop
    • Perfect for racing tracks, patrol routes, or any circular path
    • Parameter t=1 will smoothly connect back to t=0

    When false (default):

    • The spline is open, with distinct start and end points
    • Suitable for one-way paths, camera movements, or linear progressions

    Changing this property marks the spline as dirty and triggers a rebuild.

    Parameters

    • value: boolean

    Returns void

    const patrol = gameObject.addComponent(SplineContainer);
    patrol.closed = true; // Loop back to start

    // Add points in a circle
    for (let i = 0; i < 8; i++) {
    const angle = (i / 8) * Math.PI * 2;
    patrol.addKnot({
    position: new Vector3(Math.cos(angle) * 10, 0, Math.sin(angle) * 10)
    });
    }
    false
    
  • get context(): Context

    The context this component belongs to, providing access to the runtime environment including physics, timing utilities, camera, and scene

    Returns Context

  • set context(context: Context): void

    Parameters

    Returns void

  • get curve(): null | Curve<Vector3>

    The Three.js Curve object generated from the spline knots.

    This is the underlying curve implementation (typically a CatmullRomCurve3) that's used for all position and tangent sampling. The curve is automatically regenerated when the spline is marked dirty.

    Note: This curve is in local space relative to the SplineContainer. Use getPointAt and getTangentAt methods to get world-space results.

    Returns null | Curve<Vector3>

    The generated Three.js Curve, or null if not yet built

  • set debug(debug: boolean): void

    Enables visual debug rendering of the spline curve.

    When enabled, the spline is rendered as a purple line in the scene, making it easy to visualize the path during development. The debug line automatically updates when the spline is modified.

    Debug visualization:

    • Purple line showing the complete curve path
    • Automatically rebuilds when spline changes
    • Line resolution based on number of knots (10 segments per knot)

    Tip: You can also enable debug visualization globally for all splines by adding ?debugsplines to your URL.

    Parameters

    • debug: boolean

    Returns void

    const spline = gameObject.addComponent(SplineContainer);
    spline.debug = true; // Show purple debug line

    // Add some knots to see the visualization
    spline.addKnot({ position: new Vector3(0, 0, 0) });
    spline.addKnot({ position: new Vector3(5, 2, 0) });
    spline.addKnot({ position: new Vector3(10, 0, 5) });
  • get destroyed(): boolean

    Checks if this component has been destroyed

    Returns boolean

    True if the component or its GameObject has been destroyed

  • get enabled(): boolean

    Controls whether this component is enabled Disabled components don't receive lifecycle callbacks

    Returns boolean

  • set enabled(val: boolean): void

    Parameters

    • val: boolean

    Returns void

  • get forward(): Vector3

    Gets the forward direction vector (0,0,-1) of this component's GameObject in world space

    Returns Vector3

  • get isDirty(): boolean

    Whether the spline needs to be rebuilt due to modifications.

    The spline is marked dirty when:

    The curve is automatically rebuilt on the next update frame when dirty.

    Returns boolean

    true if the spline needs rebuilding, false otherwise

  • get layer(): number

    The layer value of the GameObject this component is attached to Used for visibility and physics filtering

    Returns number

  • get name(): string

    The name of the GameObject this component is attached to Used for debugging and finding objects

    Returns string

  • set name(str: string): void

    Parameters

    • str: string

    Returns void

  • get right(): Vector3

    Gets the right direction vector (1,0,0) of this component's GameObject in world space

    Returns Vector3

  • get scene(): Scene

    Shorthand accessor for the current scene from the context

    Returns Scene

    The scene this component belongs to

  • get static(): boolean

    Indicates whether the GameObject is marked as static Static objects typically don't move and can be optimized by the engine

    Returns boolean

  • set static(value: boolean): void

    Parameters

    • value: boolean

    Returns void

  • get tag(): string

    The tag of the GameObject this component is attached to Used for categorizing objects and efficient lookup

    Returns string

  • set tag(str: string): void

    Parameters

    • str: string

    Returns void

  • get up(): Vector3

    Gets the up direction vector (0,1,0) of this component's GameObject in world space

    Returns Vector3

  • get worldEuler(): Euler

    Gets the rotation of this component's GameObject in world space as Euler angles (in radians)

    Returns Euler

  • set worldEuler(val: Euler): void

    Sets the rotation of this component's GameObject in world space using Euler angles (in radians)

    Parameters

    • val: Euler

      The world rotation Euler angles to set

    Returns void

  • get worldPosition(): Vector3

    Gets the position of this component's GameObject in world space.
    Note: This is equivalent to calling this.gameObject.worldPosition

    Returns Vector3

  • set worldPosition(val: Vector3): void

    Sets the position of this component's GameObject in world space

    Parameters

    • val: Vector3

      The world position vector to set

    Returns void

  • get worldQuaternion(): Quaternion

    Gets the rotation of this component's GameObject in world space as a quaternion Note: This is equivalent to calling this.gameObject.worldQuaternion

    Returns Quaternion

  • set worldQuaternion(val: Quaternion): void

    Sets the rotation of this component's GameObject in world space using a quaternion

    Parameters

    • val: Quaternion

      The world rotation quaternion to set

    Returns void

  • get worldRotation(): Vector3

    Gets the rotation of this component's GameObject in world space as Euler angles (in degrees)
    Note: This is equivalent to calling this.gameObject.worldRotation

    Returns Vector3

  • set worldRotation(val: Vector3): void

    Sets the rotation of this component's GameObject in world space using Euler angles (in degrees)

    Parameters

    • val: Vector3

      The world rotation vector to set (in degrees)

    Returns void

Methods

  • Registers an event listener for the specified event type

    Type Parameters

    Parameters

    • type: string

      The event type to listen for

    • listener: (evt: T) => any

      The callback function to execute when the event occurs

    Returns void

  • Adds a knot (control point) to the end of the spline.

    You can pass either a full SplineData object or a simple object with just a position. When passing a simple object, default values are used for rotation and tangents.

    The spline curve is automatically marked dirty and will be rebuilt on the next update.

    Parameters

    • knot: SplineData | { position: Vector3 }

      Either a SplineData object or an object with at least a position property

    Returns SplineContainer

    This SplineContainer for method chaining

    spline.addKnot({ position: new Vector3(0, 0, 0) })
    .addKnot({ position: new Vector3(5, 0, 0) })
    .addKnot({ position: new Vector3(5, 0, 5) });
    const knot = new SplineData();
    knot.position.set(10, 2, 5);
    knot.rotation.setFromEuler(new Euler(0, Math.PI / 4, 0));
    spline.addKnot(knot);
  • Destroys this component and removes it from its GameObject After destruction, the component will no longer receive lifecycle callbacks

    Returns void

  • Dispatches an event to all registered listeners

    Parameters

    • evt: Event

      The event object to dispatch

    Returns boolean

    Always returns false (standard implementation of EventTarget)

  • Called at the beginning of each frame before regular updates. Use for logic that needs to run before standard update callbacks.

    Returns void

  • Samples a point on the spline at a given parametric position (in world space).

    The parameter t ranges from 0 to 1, where:

    • 0 = start of the spline
    • 0.5 = middle of the spline
    • 1 = end of the spline

    The returned position is in world space, accounting for the SplineContainer's transform. Values outside 0-1 are clamped to the valid range.

    Parameters

    • t: number

      Parametric position along the spline (0 to 1)

    • Optionaltarget: Vector3

      Optional Vector3 to store the result (avoids allocation)

    Returns Vector3

    The world-space position at parameter t

    // Sample 20 evenly-spaced points
    const points: Vector3[] = [];
    for (let i = 0; i <= 20; i++) {
    const t = i / 20;
    points.push(spline.getPointAt(t));
    }
    const reusableVector = new Vector3();
    for (let i = 0; i < 100; i++) {
    const point = spline.getPointAt(i / 100, reusableVector);
    // Use point...
    }

    getTangentAt to get the direction at a point

  • Samples the tangent (direction) vector on the spline at a given parametric position (in world space).

    The tangent represents the forward direction of the curve at point t. This is useful for:

    • Orienting objects along the spline (facing the direction of travel)
    • Calculating banking/tilting for vehicles on the path
    • Understanding the curve's direction at any point

    The parameter t ranges from 0 to 1 (same as getPointAt). The returned vector is normalized and in world space, accounting for the SplineContainer's rotation.

    Parameters

    • t: number

      Parametric position along the spline (0 to 1)

    • Optionaltarget: Vector3

      Optional Vector3 to store the result (avoids allocation)

    Returns Vector3

    The normalized tangent vector in world space at parameter t

    const position = spline.getPointAt(0.5);
    const tangent = spline.getTangentAt(0.5);

    object.position.copy(position);
    object.lookAt(position.clone().add(tangent)); // Face along the spline
    let t = 0;
    update() {
    t += this.context.time.deltaTime * 0.2; // Speed
    if (t > 1) t = 0; // Loop

    const pos = spline.getPointAt(t);
    const direction = spline.getTangentAt(t);

    movingObject.position.copy(pos);
    movingObject.quaternion.setFromUnitVectors(
    new Vector3(0, 0, 1),
    direction
    );
    }

    getPointAt to get the position at a point

  • Called after all update functions have been called. Use for calculations that depend on other components being updated first.

    Returns void

  • Marks the spline as dirty, causing it to be rebuilt on the next update frame.

    Call this method whenever you manually modify the spline data (knot positions, rotations, or tangents) to ensure the curve is regenerated. This is done automatically when using addKnot or removeKnot.

    Returns void

    // Modify existing knot positions
    spline.spline[0].position.y += 2;
    spline.spline[1].position.x -= 1;

    // Tell the spline to rebuild
    spline.markDirty();
    update() {
    const time = this.context.time.time;
    // Animate knot positions
    for (let i = 0; i < spline.spline.length; i++) {
    spline.spline[i].position.y = Math.sin(time + i) * 2;
    }
    spline.markDirty(); // Rebuild curve each frame
    }
  • Called after the scene has been rendered. Use for post-processing or UI updates that should happen after rendering

    Returns void

  • Called immediately before the scene is rendered.

    Parameters

    • frame: null | XRFrame

      Current XRFrame if in an XR session, null otherwise

    Returns void

  • Called before an XR session is requested Use to modify session initialization parameters

    Parameters

    • mode: XRSessionMode

      The XR session mode being requested

    • args: XRSessionInit

      The session initialization parameters that can be modified

    Returns void

  • Called when this component's collider begins colliding with another collider.

    Parameters

    • col: Collision

      Information about the collision that occurred

    Returns any

  • Called when this component's collider stops colliding with another collider.

    Parameters

    • col: Collision

      Information about the collision that ended

    Returns any

  • Called each frame while this component's collider is colliding with another collider

    Parameters

    • col: Collision

      Information about the ongoing collision

    Returns any

  • Called when the component is destroyed. Use for cleanup operations like removing event listeners

    Returns void

  • Called every time the component becomes disabled or inactive in the hierarchy. Invoked when the component or any parent GameObject becomes invisible

    Returns void

  • Called every time the component becomes enabled or active in the hierarchy. Invoked after awake and before start.

    Returns void

  • Called when the context's pause state changes.

    Parameters

    • isPaused: boolean

      Whether the context is currently paused

    • wasPaused: boolean

      The previous pause state

    Returns void

  • Called when this component's trigger collider is entered by another collider

    Parameters

    • col: ICollider

      The collider that entered this trigger

    Returns any

  • Called when another collider exits this component's trigger collider

    Parameters

    • col: ICollider

      The collider that exited this trigger

    Returns any

  • Called each frame while another collider is inside this component's trigger collider

    Parameters

    • col: ICollider

      The collider that is inside this trigger

    Returns any

  • Called when a field decorated with @validate() is modified.

    Parameters

    • Optionalprop: string

      The name of the field that was changed

    Returns void

  • Removes a previously registered event listener

    Type Parameters

    Parameters

    • type: string

      The event type the listener was registered for

    • listener: (arg: T) => any

      The callback function to remove

    Returns void

  • Removes a knot (control point) from the spline.

    You can remove a knot either by its numeric index in the spline array or by passing a reference to the SplineData object itself.

    The spline curve is automatically marked dirty and will be rebuilt on the next update.

    Parameters

    • index: number | SplineData

      Either the numeric index of the knot to remove, or the SplineData object reference

    Returns SplineContainer

    This SplineContainer for method chaining

    spline.removeKnot(0); // Remove first knot
    spline.removeKnot(spline.spline.length - 1); // Remove last knot
    const knotToRemove = spline.spline[2];
    spline.removeKnot(knotToRemove);
  • Called when this component needs to remap guids after an instantiate operation.

    Parameters

    • guidsMap: GuidsMap

      Mapping from old guids to newly generated guids

    Returns void

  • Sets the position of this component's GameObject in world space using individual coordinates

    Parameters

    • x: number

      X-coordinate in world space

    • y: number

      Y-coordinate in world space

    • z: number

      Z-coordinate in world space

    Returns void

  • Sets the rotation of this component's GameObject in world space using quaternion components

    Parameters

    • x: number

      X component of the quaternion

    • y: number

      Y component of the quaternion

    • z: number

      Z component of the quaternion

    • w: number

      W component of the quaternion

    Returns void

  • Sets the rotation of this component's GameObject in world space using individual Euler angles

    Parameters

    • x: number

      X-axis rotation

    • y: number

      Y-axis rotation

    • z: number

      Z-axis rotation

    • degrees: boolean = true

      Whether the values are in degrees (true) or radians (false)

    Returns void

  • Called once at the beginning of the first frame after the component is enabled. Use for initialization that requires other components to be awake.

    Returns void

  • Starts a coroutine that can yield to wait for events. Coroutines allow for time-based sequencing of operations without blocking. Coroutines are based on generator functions, a JavaScript language feature.

    Parameters

    • routine: Generator

      Generator function to start

    • evt: FrameEvent = FrameEvent.Update

      Event to register the coroutine for (default: FrameEvent.Update)

    Returns Generator

    The generator function that can be used to stop the coroutine

    Time-based sequencing of operations

    *myCoroutine() {
    yield WaitForSeconds(1); // wait for 1 second
    yield WaitForFrames(10); // wait for 10 frames
    yield new Promise(resolve => setTimeout(resolve, 1000)); // wait for a promise to resolve
    }

    Coroutine that logs a message every 5 frames

    onEnable() {
    this.startCoroutine(this.myCoroutine());
    }
    private *myCoroutine() {
    while(this.activeAndEnabled) {
    console.log("Hello World", this.context.time.frame);
    // wait for 5 frames
    for(let i = 0; i < 5; i++) yield;
    }
    }
  • Stops a coroutine that was previously started with startCoroutine

    Parameters

    • routine: Generator

      The routine to be stopped

    • evt: FrameEvent = FrameEvent.Update

      The frame event the routine was registered with

    Returns void

  • Determines if this component supports a specific XR mode

    Parameters

    • mode: XRSessionMode

      The XR session mode to check support for

    Returns boolean

    True if the component supports the specified mode