@threlte/rapier
useSphericalJoint
Use this hook to initialize a SphericalImpulseJoint.
A spherical joint is a ball-and-socket — it pins two rigid bodies together at a shared point and allows free rotation around all three axes. Reach for it for chains and ropes, ragdoll joints (hips, shoulders), tethered objects, and anywhere two bodies need to pivot freely in 3D.
<script lang="ts">
import { Canvas } from '@threlte/core'
import { HTML } from '@threlte/extras'
import { World } from '@threlte/rapier'
import { Button, Checkbox, Pane } from 'svelte-tweakpane-ui'
import Scene from './Scene.svelte'
let debug = $state(false)
let resetKey = $state(0)
</script>
<Pane
position="fixed"
title="Spherical Joint"
>
<Button
title="Reset"
on:click={() => resetKey++}
/>
<Checkbox
bind:value={debug}
label="Debug"
/>
</Pane>
<div>
<Canvas>
<World>
<Scene
{debug}
{resetKey}
/>
{#snippet fallback()}
<HTML transform>
<p class="text-xs">
It seems your browser<br />
doesn't support WASM.<br />
I'm sorry.
</p>
</HTML>
{/snippet}
</World>
</Canvas>
</div>
<style>
div {
height: 100%;
}
</style>
<script lang="ts">
import type { RigidBody as RapierRigidBody } from '@dimforge/rapier3d-compat'
import { T } from '@threlte/core'
import { Collider, CollisionGroups, RigidBody, useSphericalJoint } from '@threlte/rapier'
const segments = 10
const spacing = 0.4
const radius = 0.18
const wreckingRadius = 0.4
const beadDensity = 1
const wreckingDensity = 3
const wreckingOffset = radius + wreckingRadius + 0.05
const wreckingAnchor = wreckingOffset - spacing / 2
const bodies = $state<(RapierRigidBody | undefined)[]>(
Array.from({ length: segments + 1 }, () => undefined)
)
const allReady = $derived(bodies.every(Boolean))
// Chain extends to the -x direction, so each upper-side anchor is on its left
// surface (-x in local), each lower-side anchor on its right (+x).
const joints = Array.from({ length: segments }, (_, i) => {
if (i === 0) {
return useSphericalJoint([0, 0, 0], [spacing / 2, 0, 0])
}
if (i === segments - 1) {
return useSphericalJoint([-spacing / 2, 0, 0], [wreckingAnchor, 0, 0])
}
return useSphericalJoint([-spacing / 2, 0, 0], [spacing / 2, 0, 0])
})
$effect(() => {
if (allReady) {
joints.forEach((joint, i) => {
joint.rigidBodyA.set(bodies[i])
joint.rigidBodyB.set(bodies[i + 1])
})
}
})
</script>
<CollisionGroups
memberships={[1]}
filter={[0]}
>
<T.Group position={[0, 5, 0]}>
<RigidBody
type="fixed"
bind:rigidBody={bodies[0]}
>
<T.Mesh>
<T.BoxGeometry args={[0.3, 0.3, 0.3]} />
<T.MeshStandardMaterial color="#222" />
</T.Mesh>
</RigidBody>
</T.Group>
{#each { length: segments - 1 }, i (i)}
<T.Group position={[-spacing / 2 - i * spacing, 5, 0]}>
<RigidBody
bind:rigidBody={bodies[i + 1]}
linearDamping={0.1}
angularDamping={0.1}
>
<Collider
shape="ball"
args={[radius]}
density={beadDensity}
/>
<T.Mesh castShadow>
<T.SphereGeometry args={[radius, 16, 12]} />
<T.MeshStandardMaterial color="#8B5A2B" />
</T.Mesh>
</RigidBody>
</T.Group>
{/each}
<T.Group position={[-spacing / 2 - (segments - 2) * spacing - wreckingOffset, 5, 0]}>
<RigidBody
bind:rigidBody={bodies[segments]}
linearDamping={0.1}
angularDamping={0.1}
>
<Collider
shape="ball"
args={[wreckingRadius]}
density={wreckingDensity}
/>
<T.Mesh castShadow>
<T.SphereGeometry args={[wreckingRadius, 24, 16]} />
<T.MeshStandardMaterial
color="#222"
metalness={0.8}
roughness={0.3}
/>
</T.Mesh>
</RigidBody>
</T.Group>
</CollisionGroups>
<script lang="ts">
import { T } from '@threlte/core'
import { OrbitControls, SoftShadows } from '@threlte/extras'
import { Collider, Debug, RigidBody } from '@threlte/rapier'
import Chain from './Chain.svelte'
interface Props {
debug: boolean
resetKey: number
}
let { debug, resetKey }: Props = $props()
const tower = Array.from({ length: 4 }, (_, i) => i)
</script>
<T.PerspectiveCamera
makeDefault
position={[0, 5, 12]}
fov={55}
>
<OrbitControls
enableDamping
enableZoom={false}
target={[0, 2.5, 0]}
/>
</T.PerspectiveCamera>
<T.DirectionalLight
castShadow
intensity={2}
position={[8, 20, -3]}
shadow.camera.top={-20}
shadow.camera.bottom={20}
shadow.mapSize.width={1024}
shadow.mapSize.height={1024}
/>
<T.AmbientLight intensity={1} />
<SoftShadows />
{#if debug}
<Debug />
{/if}
{#key resetKey}
<Chain />
{#each tower as i}
<T.Group position={[3, 0.5 + i, 0]}>
<RigidBody>
<Collider
shape="cuboid"
args={[0.4, 0.4, 0.4]}
/>
<T.Mesh castShadow>
<T.BoxGeometry args={[0.8, 0.8, 0.8]} />
<T.MeshStandardMaterial color="#335086" />
</T.Mesh>
</RigidBody>
</T.Group>
{/each}
{/key}
<T.Group position={[0, -0.5, 0]}>
<RigidBody type="fixed">
<Collider
shape="cuboid"
args={[10, 0.5, 5]}
/>
<T.Mesh receiveShadow>
<T.BoxGeometry args={[20, 1, 10]} />
<T.MeshStandardMaterial color="#888" />
</T.Mesh>
</RigidBody>
</T.Group>
<script>
import { useSphericalJoint, RigidBody, Collider } from '@threlte/rapier'
const { joint, rigidBodyA, rigidBodyB } = useSphericalJoint({ x: 1 }, { x: -1 })
</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<SphericalImpulseJoint>
rigidBodyA: Writable<RAPIER.RigidBody>
rigidBodyB: Writable<RAPIER.RigidBody>
} = useSphericalJoint(
anchorA, // Position
anchorB, // Position
)