@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
)