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