threlte logo
@threlte/rapier

<Attractor>

An attractor simulates a source of gravity. Any rigid-body within range will be “pulled” toward the attractor’s center.

The force applied to rigid-bodies within range is calculated differently, depending on the gravityType.

Basic Example

<script
  lang="ts"
  context="module"
>
  const geometry = new SphereGeometry(1)
  const material = new MeshBasicMaterial({ color: 'red' })
</script>

<script lang="ts">
  import { T } from '@threlte/core'
  import { OrbitControls } from '@threlte/extras'
  import { Attractor, Collider, RigidBody } from '@threlte/rapier'
  import type { GravityType } from '@threlte/rapier'
  import { MeshBasicMaterial, SphereGeometry } from 'three'

  export let type: GravityType = 'static'
  let hide = false
  export const reset = () => {
    hide = true
    setTimeout(() => (hide = false))
  }

  const config: any = {
    static: {
      type: 'static',
      strength: 3,
      range: 100,
      gravitationalConstant: undefined
    },
    linear: {
      type: 'linear',
      strength: 1,
      range: 100,
      gravitationalConstant: undefined
    },
    newtonian: {
      type: 'newtonian',
      strength: 10,
      range: 100,
      gravitationalConstant: 10
    }
  }
</script>

<T.PerspectiveCamera
  position.y={50}
  position.z={100}
  makeDefault
  fov={70}
  far={10000}
>
  <OrbitControls
    enableZoom={true}
    target.y={20}
  />
</T.PerspectiveCamera>

<T.DirectionalLight
  castShadow
  position={[8, 20, -3]}
/>

<T.GridHelper args={[100]} />

{#if !hide}
  <T.Group position={[-50, 0, 0]}>
    <RigidBody linearVelocity={[5, -5, 0]}>
      <Collider
        shape="ball"
        args={[1]}
        mass={config[type].strength}
      />
      <T.Mesh
        {geometry}
        {material}
      />
      <Attractor
        range={config[type].range}
        gravitationalConstant={config[type].gravitationalConstant}
        strength={config[type].strength}
        gravityType={type}
      />
    </RigidBody>
  </T.Group>

  <RigidBody linearVelocity={[0, 5, 0]}>
    <Collider
      shape="ball"
      args={[1]}
      mass={config[type].strength}
    />
    <T.Mesh
      {geometry}
      {material}
    />
    <Attractor
      range={config[type].range}
      gravitationalConstant={config[type].gravitationalConstant}
      strength={config[type].strength}
      gravityType={type}
    />
  </RigidBody>

  <T.Group position={[50, 0, 0]}>
    <RigidBody linearVelocity={[-5, 0, 5]}>
      <Collider
        shape="ball"
        args={[1]}
        mass={config[type].strength}
      />
      <T.Mesh
        {geometry}
        {material}
      />
      <Attractor
        range={config[type].range}
        gravitationalConstant={config[type].gravitationalConstant}
        strength={config[type].strength}
        gravityType={type}
      />
    </RigidBody>
  </T.Group>
{/if}
<script lang="ts">
	import { Canvas } from '@threlte/core'
	import { HTML } from '@threlte/extras'
	import { World, Debug } from '@threlte/rapier'
	import BasicScene from './BasicScene.svelte'
	import type { GravityType } from '@threlte/rapier'
	import AdvancedScene from './AdvancedScene.svelte'
	import { useTweakpane } from '$lib/useTweakpane'

	let reset: (() => void) | undefined

	const { pane, action, addInput, addButton } = useTweakpane({
		title: 'Attractor'
	})

	let tabIndex = 0
	$: showAdvanced = tabIndex === 1

	addButton({
		title: 'Reset',
		label: 'Reset the scene',
		onClick: () => {
			reset?.()
		}
	})

	const showHelper = addInput({
		label: 'Show helper',
		value: false
	})

	const tab = pane.addTab({
		pages: [
			{
				title: 'Basic'
			},
			{
				title: 'Advanced'
			}
		]
	})

	tab.on('select', (e: { index: number }) => {
		tabIndex = e.index
	})

	const basicPage = tab.pages[0]

	const strengthLeft = addInput({
		label: 'Strength left',
		value: 1,
		params: {
			min: -5,
			max: 5,
			step: 0.1
		},
		parent: basicPage
	})

	const strengthCenter = addInput({
		label: 'Strength center',
		value: 1,
		params: {
			min: -5,
			max: 5,
			step: 0.1
		},
		parent: basicPage
	})

	const strengthRight = addInput({
		label: 'Strength right',
		value: 1,
		params: {
			min: -5,
			max: 5,
			step: 0.1
		},
		parent: basicPage
	})

	const advancedPage = tab.pages[1]

	const gravityTypes: GravityType[] = ['static', 'linear', 'newtonian']
	let gravityType: GravityType = gravityTypes[0]

	addButton({
		title: gravityTypes[0],
		label: 'Set Gravity Type',
		parent: advancedPage,
		onClick: () => {
			gravityType = gravityTypes[0]
		}
	})

	addButton({
		title: gravityTypes[1],
		label: ' ',
		parent: advancedPage,
		onClick: () => {
			gravityType = gravityTypes[1]
		}
	})

	addButton({
		title: gravityTypes[2],
		label: ' ',
		parent: advancedPage,
		onClick: () => {
			gravityType = gravityTypes[2]
		}
	})
</script>

<div use:action />

<Canvas>
	<World gravity={[0, showAdvanced ? 0 : -3, 0]}>
		{#if $showHelper}
			<Debug />
		{/if}
		{#if showAdvanced}
			<AdvancedScene
				type={gravityType}
				bind:reset
			/>
		{:else}
			<BasicScene
				bind:reset
				strengthLeft={$strengthLeft}
				strengthCenter={$strengthCenter}
				strengthRight={$strengthRight}
			/>
		{/if}
		<HTML
			slot="fallback"
			transform
		>
			<p>
				It seems your browser<br />
				doesn't support WASM.<br />
				I'm sorry.
			</p>
		</HTML>
	</World>
</Canvas>

<style>
	p {
		font-size: 0.75rem;
		line-height: 1rem;
	}
</style>
<script lang="ts">
  import { T } from '@threlte/core'
  import { OrbitControls } from '@threlte/extras'
  import { Attractor } from '@threlte/rapier'
  import RandomMeshes from './RandomMeshes.svelte'

  let count: number = 50
  export let strengthLeft: number
  export let strengthCenter: number
  export let strengthRight: number
  export const reset = () => {
    count = 0
    setTimeout(() => (count = 50))
  }
</script>

<T.PerspectiveCamera
  makeDefault
  position.y={50}
  position.z={100}
  fov={70}
  far={10000}
>
  <OrbitControls
    enableZoom={true}
    target.y={20}
  />
</T.PerspectiveCamera>

<T.DirectionalLight
  castShadow
  position={[8, 20, -3]}
/>

<T.GridHelper args={[100]} />

<RandomMeshes
  {count}
  rangeX={[-30, 30]}
  rangeY={[0, 75]}
  rangeZ={[-10, 10]}
/>

<Attractor
  range={20}
  strength={strengthLeft}
  position={[-25, 10, 0]}
/>
<Attractor
  range={15}
  strength={strengthCenter}
  position={[0, 20, 0]}
/>
<Attractor
  range={20}
  strength={strengthRight}
  position={[25, 10, 0]}
/>
<script
  lang="ts"
  context="module"
>
  const geometry = new SphereGeometry(1)
  const material = new MeshBasicMaterial({ color: 'red' })
</script>

<script lang="ts">
  import { T } from '@threlte/core'
  import { Collider, RigidBody } from '@threlte/rapier'
  import { MeshBasicMaterial, SphereGeometry, Vector3 } from 'three'

  export let count: number = 20
  export let rangeX: [number, number] = [-20, 20]
  export let rangeY: [number, number] = [-20, 20]
  export let rangeZ: [number, number] = [-20, 20]

  const getId = () => {
    return Math.random().toString(16).slice(2)
  }

  const randomNumber = (min: number, max: number): number => {
    return Math.random() * (max - min) + min
  }

  const getRandomPosition = (): Parameters<Vector3['set']> => {
    return new Vector3(
      randomNumber(rangeX[0], rangeX[1]),
      randomNumber(rangeY[0], rangeY[1]),
      randomNumber(rangeZ[0], rangeZ[1])
    ).toArray()
  }

  const generateBodies = (c: number) => {
    return Array(c)
      .fill('x')
      .map((_) => {
        return {
          id: getId(),
          position: getRandomPosition()
        }
      })
  }

  $: bodies = generateBodies(count)
</script>

{#each bodies as body (body.id)}
  <T.Group position={body.position}>
    <RigidBody>
      <Collider
        shape="ball"
        args={[0.75]}
        mass={Math.random() * 10}
      />
      <T.Mesh
        {geometry}
        {material}
      />
    </RigidBody>
  </T.Group>
{/each}

Gravity Types

Static (Default)

Static gravity means that the same force (strength) is applied on all rigid-bodies within range, regardless of distance.

Linear

Linear gravity means that force is calculated as strength * distance / range. That means the force applied increases as a rigid-body moves closer to the attractor’s center.

Newtonian

Newtonian gravity uses the traditional method of calculating gravitational force (F = GMm/r^2) and as such force is calculated as gravitationalConstant * mass1 * mass2 / Math.pow(distance, 2).

  • gravitationalConstant defaults to 6.673e-11 but you can provide your own
  • mass1 here is the “mass” of the Attractor, which is just the strength property
  • mass2 is the mass of the rigid-body at the time of calculation. Note that rigid-bodies with colliders will use the mass provided to the collider. This is not a value you can control from the attractor, only from wherever you’re creating rigid-body components in the scene.
  • distance is the distance between the attractor and rigid-body at the time of calculation

Debugging

The <Debug /> component will activate a wireframe helper to visualize the attractor’s range.

Component Signature

<Attractor> extends <T.Group> and supports all its props, slot props, bindings and events.

Props

name
type
required
default

gravitationalConstant
number
no
6.673e-11

gravityType
'static' | 'linear' | 'newtonian'
no

range
number
no
10

strength
number
no
1