threlte logo
@threlte/rapier

useRopeJoint

<script>
  import { Canvas } from '@threlte/core'
  import Scene from './Scene.svelte'
  import { World } from '@threlte/rapier'
  import { Checkbox, Pane, Slider } from 'svelte-tweakpane-ui'
  import { NoToneMapping } from 'three'

  let debug = $state(false)
  let damping = $state(0.8)
  let segments = $state(20)
</script>

<Pane
  position="fixed"
  title="Rope"
>
  <Checkbox
    bind:value={debug}
    label="Debug"
  />
  <Slider
    bind:value={damping}
    label="Damping"
    min={0}
    max={1}
    step={0.01}
  />
  <Slider
    bind:value={segments}
    label="Segments"
    min={2}
    max={20}
    step={1}
  />
</Pane>

<div>
  <Canvas toneMapping={NoToneMapping}>
    <World>
      <Scene
        {debug}
        {damping}
        {segments}
      />
    </World>
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { RigidBody as RapierRigidBody } from '@dimforge/rapier3d-compat'
  import { observe, T, useTask } from '@threlte/core'
  import { MeshLineGeometry, MeshLineMaterial } from '@threlte/extras'
  import { Collider, RigidBody, useRopeJoint } from '@threlte/rapier'
  import { Vector3, type Object3D, type Vector3Tuple } from 'three'

  type Props = {
    segments: number
    ropeStart: Vector3Tuple
    ropeEnd: Vector3Tuple
    length: number
    ballRadius: number
    damping: number
  }

  let { segments, ropeStart, ropeEnd, length, ballRadius, damping }: Props = $props()

  const lengthBetweenSegments = length / (segments - 1)

  let jointsInitialized = $state(false)

  // make new Array with segments - 1 elements
  const joints = Array.from({ length: segments - 1 }, () => {
    return useRopeJoint([0, 0, 0], [0, 0, 0], lengthBetweenSegments)
  })

  const rigidBodies = $state<RapierRigidBody[]>([])
  const objects = $state<Object3D[]>([])

  const start = new Vector3().fromArray(ropeStart)
  const end = new Vector3().fromArray(ropeEnd)

  const getIntialRigidBodyPosition = (index: number) => {
    const t = index / (segments - 1)
    return start.clone().lerp(end, t)
  }

  $effect(() => {
    if (rigidBodies.length !== segments || objects.length !== segments) return
    if (jointsInitialized) return

    joints.forEach((joint, index) => {
      joint.rigidBodyA.set(rigidBodies[index])
      joint.rigidBodyB.set(rigidBodies[index + 1])
    })

    jointsInitialized = true
  })

  let points = $state(
    Array.from({ length: segments }, () => {
      return new Vector3(0, 0, 0)
    })
  )

  useTask(() => {
    if (!jointsInitialized) return
    for (let i = 0; i < objects.length; i++) {
      const obj = objects[i]
      obj?.getWorldPosition(points[i]!)
    }
    points = [...points]
  })

  observe(
    () => [jointsInitialized, ropeStart, ropeEnd],
    ([jointsInitialized]) => {
      if (!jointsInitialized) return
      const firstRigidBody = rigidBodies.at(0)
      const lastRigidBody = rigidBodies.at(-1)
      firstRigidBody!.setNextKinematicTranslation({
        x: ropeStart[0],
        y: ropeStart[1],
        z: ropeStart[2]
      })
      lastRigidBody!.setNextKinematicTranslation({ x: ropeEnd[0], y: ropeEnd[1], z: ropeEnd[2] })
    }
  )
</script>

{#each { length: segments } as _, i (i)}
  <T.Group
    oncreate={(ref) => {
      ref.position.copy(getIntialRigidBodyPosition(i))
    }}
  >
    <RigidBody
      linearDamping={damping}
      angularDamping={damping}
      type={i === 0 || i === segments - 1 ? 'kinematicPosition' : 'dynamic'}
      bind:rigidBody={rigidBodies[i]}
    >
      <Collider
        shape="ball"
        args={[ballRadius]}
      >
        <T.Object3D bind:ref={objects[i]!} />
      </Collider>
    </RigidBody>
  </T.Group>
{/each}

<T.Mesh>
  <MeshLineGeometry
    {points}
    shape="none"
  />
  <MeshLineMaterial
    width={0.4}
    color="#FE3D00"
  />
</T.Mesh>
<script lang="ts">
  import { T } from '@threlte/core'
  import {
    Environment,
    Grid,
    interactivity,
    OrbitControls,
    type IntersectionEvent
  } from '@threlte/extras'
  import { AutoColliders, Debug } from '@threlte/rapier'
  import { DoubleSide, type Vector3Tuple } from 'three'
  import { DEG2RAD } from 'three/src/math/MathUtils.js'
  import Rope from './Rope.svelte'

  let { debug, damping, segments }: { debug: boolean; damping: number; segments: number } = $props()

  interactivity()

  let ropeEnd = $state<Vector3Tuple>([0, 0, 0])

  const onpointermove = (e: IntersectionEvent<PointerEvent>) => {
    e.point.x -= 0.2
    ropeEnd = e.point.toArray()
  }
</script>

<Environment url="/textures/equirectangular/hdr/mpumalanga_veld_puresky_1k.hdr" />

{#if debug}
  <Debug />
{/if}

<T.PerspectiveCamera
  makeDefault
  position={[-10, 5, 10]}
>
  <OrbitControls />
</T.PerspectiveCamera>

<Grid
  sectionColor="#122036"
  cellColor="#122036"
  position.y={-5}
/>

<T.Mesh
  {onpointermove}
  rotation.y={90 * DEG2RAD}
>
  <T.CircleGeometry args={[5]} />
  <T.MeshBasicMaterial
    color="#0A0F19"
    side={DoubleSide}
  />
</T.Mesh>

<T.Mesh position={[-5, 0, 0]}>
  <T.SphereGeometry args={[0.2]} />
  <T.MeshStandardMaterial color="#335086" />
</T.Mesh>

<T.Mesh position={ropeEnd}>
  <T.SphereGeometry args={[0.2]} />
  <T.MeshStandardMaterial color="#335086" />
</T.Mesh>

<AutoColliders shape="cuboid">
  <T.Mesh position={[-2.5, 0, -1]}>
    <T.BoxGeometry />
    <T.MeshStandardMaterial color="#335086" />
  </T.Mesh>
</AutoColliders>

<AutoColliders shape="cuboid">
  <T.Mesh position={[-2.5, 0, 1]}>
    <T.BoxGeometry />
    <T.MeshStandardMaterial color="#335086" />
  </T.Mesh>
</AutoColliders>

{#key segments}
  <Rope
    ballRadius={0.2}
    ropeStart={[-5, 0, 0]}
    {ropeEnd}
    length={7}
    {segments}
    {damping}
  />
{/key}

Use this hook to initialize a RopeImpulseJoint. A rope joint limits the max distance between two bodies.

<script>
  import { useRopeJoint, RigidBody, Collider } from '@threlte/rapier'

  const { joint, rigidBodyA, rigidBodyB } = useRopeJoint({ x: 1 }, { y: 1 }, 2)
</script>

<RigidBody bind:rigidBody={$rigidBodyA}>
  <Collider
    shape="cuboid"
    args={[1, 1, 1]}
  />
</RigidBody>

<RigidBody bind:rigidBody={$rigidBodyB}>
  <Collider
    shape="cuboid"
    args={[1, 1, 1]}
  />
</RigidBody>

Signature

const {
	joint: Writable<RopeImpulseJoint>
	rigidBodyA: Writable<RAPIER.RigidBody>
	rigidBodyB: Writable<RAPIER.RigidBody>
} = useRopeJoint(
	anchorA,  // Position
  anchorB,  // Position
	length    // Length of the rope
)