threlte logo
@threlte/rapier

useFixedJoint

Use this hook to initialize a FixedImpulseJoint.

A fixed joint welds two rigid bodies together, locking their relative position and orientation so they behave as a single rigid unit. Reach for it when you need a composite from separate bodies — parts you may want to detach at runtime, or parts with different mass, friction, or collision properties.

<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="Fixed Joint"
>
  <Button
    title="Throw hammers"
    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 { useFixedJoint } from '@threlte/rapier'
  import type { Vector3Tuple } from 'three'

  interface Props {
    bodyA: RapierRigidBody
    bodyB: RapierRigidBody
    anchorA: Vector3Tuple
    anchorB: Vector3Tuple
  }

  let { bodyA, bodyB, anchorA, anchorB }: Props = $props()

  const { rigidBodyA, rigidBodyB } = useFixedJoint(anchorA, [0, 0, 0], anchorB, [0, 0, 0])

  $effect(() => {
    rigidBodyA.set(bodyA)
    rigidBodyB.set(bodyB)
  })
</script>
<script lang="ts">
  import type { RigidBody as RapierRigidBody } from '@dimforge/rapier3d-compat'
  import { T } from '@threlte/core'
  import { Collider, CollisionGroups, RigidBody } from '@threlte/rapier'
  import type { Vector3Tuple } from 'three'
  import FixedJoint from './FixedJoint.svelte'

  interface Props {
    position?: Vector3Tuple
    rotation?: Vector3Tuple
    velocity?: Vector3Tuple
  }

  let {
    position = [0, 6, 0],
    rotation = [0, 0, Math.PI / 6],
    velocity = [0, 0, 0]
  }: Props = $props()

  let handle = $state<RapierRigidBody>()
  let head = $state<RapierRigidBody>()
</script>

<T.Group
  {position}
  {rotation}
>
  <CollisionGroups
    memberships={[1]}
    filter={[0, 2]}
  >
    <T.Group position={[-0.6, 0, 0]}>
      <RigidBody
        bind:rigidBody={handle}
        linearVelocity={velocity}
      >
        <Collider
          shape="cuboid"
          args={[1.2, 0.15, 0.15]}
          density={0.5}
        />
        <T.Mesh castShadow>
          <T.BoxGeometry args={[2.4, 0.3, 0.3]} />
          <T.MeshStandardMaterial color="#8B5A2B" />
        </T.Mesh>
      </RigidBody>
    </T.Group>

    <T.Group position={[1, 0, 0]}>
      <RigidBody
        bind:rigidBody={head}
        linearVelocity={velocity}
      >
        <Collider
          shape="cuboid"
          args={[0.4, 0.4, 0.4]}
          density={8}
        />
        <T.Mesh castShadow>
          <T.BoxGeometry args={[0.8, 0.8, 0.8]} />
          <T.MeshStandardMaterial
            color="#444"
            metalness={0.8}
            roughness={0.3}
          />
        </T.Mesh>
      </RigidBody>
    </T.Group>
  </CollisionGroups>
</T.Group>

{#if handle && head}
  <FixedJoint
    bodyA={handle}
    bodyB={head}
    anchorA={[1.2, 0, 0]}
    anchorB={[-0.4, 0, 0]}
  />
{/if}
<script lang="ts">
  import { T } from '@threlte/core'
  import { OrbitControls, SoftShadows } from '@threlte/extras'
  import { Collider, Debug, RigidBody } from '@threlte/rapier'
  import Hammer from './Hammer.svelte'
  import Tower from './Tower.svelte'

  interface Props {
    debug: boolean
    resetKey: number
  }

  let { debug, resetKey }: Props = $props()
</script>

<T.PerspectiveCamera
  makeDefault
  position={[0, 7, 18]}
  fov={60}
>
  <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}
  <Tower
    position={[-3, 0, 0]}
    color="#FE3D00"
  />
  <Tower
    position={[3, 0, 0]}
    color="#335086"
    jointed
  />
  <Hammer
    position={[-10, 3, 0]}
    rotation={[0, 0, -Math.PI / 6]}
    velocity={[15, 0, 0]}
  />
  <Hammer
    position={[10, 3, 0]}
    rotation={[0, 0, Math.PI / 6]}
    velocity={[-15, 0, 0]}
  />
{/key}

<T.Group position={[0, -0.5, 0]}>
  <RigidBody type="fixed">
    <Collider
      shape="cuboid"
      args={[12, 0.5, 5]}
    />
    <T.Mesh receiveShadow>
      <T.BoxGeometry args={[24, 1, 10]} />
      <T.MeshStandardMaterial color="#888" />
    </T.Mesh>
  </RigidBody>
</T.Group>
<script lang="ts">
  import type { RigidBody as RapierRigidBody } from '@dimforge/rapier3d-compat'
  import { T } from '@threlte/core'
  import { Collider, CollisionGroups, RigidBody } from '@threlte/rapier'
  import type { Vector3Tuple } from 'three'
  import FixedJoint from './FixedJoint.svelte'

  interface Props {
    position?: Vector3Tuple
    jointed?: boolean
    color?: string
  }

  let { position = [0, 0, 0], jointed = false, color = '#FE3D00' }: Props = $props()

  const COUNT = 5
  const bodies = $state<(RapierRigidBody | undefined)[]>(Array.from({ length: 5 }, () => undefined))
  const allReady = $derived(bodies.every(Boolean))
</script>

{#snippet bricks()}
  <T.Group {position}>
    {#each { length: 5 }, i (i)}
      <T.Group position={[0, 0.5 + i, 0]}>
        <RigidBody bind:rigidBody={bodies[i]}>
          <Collider
            shape="cuboid"
            args={[0.5, 0.5, 0.5]}
          />
          <T.Mesh
            castShadow
            receiveShadow
          >
            <T.BoxGeometry args={[1, 1, 1]} />
            <T.MeshStandardMaterial {color} />
          </T.Mesh>
        </RigidBody>
      </T.Group>
    {/each}
  </T.Group>
{/snippet}

{#if jointed}
  <CollisionGroups
    memberships={[2]}
    filter={[0, 1]}
  >
    {@render bricks()}
  </CollisionGroups>
{:else}
  {@render bricks()}
{/if}

{#if jointed && allReady}
  {#each Array(COUNT - 1) as _, i (i)}
    <FixedJoint
      bodyA={bodies[i]!}
      bodyB={bodies[i + 1]!}
      anchorA={[0.5 * (i % 2 === 1 ? -1 : 1), 0.5, 0]}
      anchorB={[0, -0.5, 0]}
    />
  {/each}
{/if}
<script>
  import { useFixedJoint, RigidBody, Collider } from '@threlte/rapier'

  const { joint, rigidBodyA, rigidBodyB } = useFixedJoint({ 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<FixedImpulseJoint>
	rigidBodyA: Writable<RAPIER.RigidBody>
	rigidBodyB: Writable<RAPIER.RigidBody>
} = useFixedJoint(
	anchorA,  // Position
  frameA,   // Rotation
  anchorB,  // Position
  frameB    // Rotation
)