threlte logo

Random Placement

This example explores several simple ways to automatically position objects in your scene. This is a great starting point if you want to procedurally generate terrain or other scenes. Taking these methods as a starting point, you’ll hopefully be able to find the approach that suits your project.

Manually placing objects is also a good enough approach in many projects. A hybrid approach involves starting out with random scenery, and then saving all the object properties to create a static scene from it.

Basic Random

The simplest starting point is using Math.random as is. Every object will be independently placed, this is called a uniform distribution. Starting with a plane, a couple of svelte’s {#each ... as ...} blocks and some random numbers; you can position objects like in the simple scene below.

<script>
  import { Canvas } from '@threlte/core'
  import Scene from './Scene.svelte'
  import { Pane, Button, Slider } from 'svelte-tweakpane-ui'
  import { regen, numberOfObjects } from './stores'
</script>

<Pane
  title="Completely Random"
  position="fixed"
>
  <Button
    title="regenerate"
    on:click={() => {
      $regen = !$regen
    }}
  />
  <Slider
    bind:value={$numberOfObjects}
    label="Number of Objects"
    min={20}
    max={100}
    step={10}
  />
</Pane>

<div>
  <Canvas>
    <Scene />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { watch } from '@threlte/core'
  import { regen, numberOfObjects } from './stores'
  // The following components started as copies from https://fun-bit.vercel.app/
  import BirchTrees from './assets/birch.svelte'
  import Trees from './assets/tree.svelte'
  import Bushes from './assets/bush.svelte'
  import Rocks from './assets/rock.svelte'

  const distinctObjects = 4
  const commonRatio = 0.5

  let randomBushes: number[][] = []
  let randomTrees: number[][] = []
  let randomBirchTrees: number[][] = []
  let randomRocks: number[][] = []

  watch([regen, numberOfObjects], () => {
    generateRandomNumbers()
  })

  generateRandomNumbers()

  function generateRandomNumbers() {
    const exponentialSumValues = calculateExponentialSumValues(
      $numberOfObjects,
      distinctObjects,
      commonRatio
    )
    const totalBushes = exponentialSumValues[0]
    const totalTrees = exponentialSumValues[1]
    const totalBirchTrees = exponentialSumValues[2]
    const totalRocks = exponentialSumValues[3]

    randomBushes = []
    randomTrees = []
    randomBirchTrees = []
    randomRocks = []

    for (let i = 0; i < totalBushes; i++) {
      randomBushes.push([Math.random(), Math.random(), Math.random(), Math.random()])
      if (i < totalTrees) {
        randomTrees.push([Math.random(), Math.random(), Math.random(), Math.random()])
      }
      if (i < totalBirchTrees) {
        randomBirchTrees.push([Math.random(), Math.random(), Math.random(), Math.random()])
      }
      if (i < totalRocks) {
        randomRocks.push([Math.random(), Math.random(), Math.random(), Math.random()])
      }
    }
  }

  function calculateExponentialSumValues(
    total: number,
    numberOfValues: number,
    commonRatio: number
  ): number[] {
    let result = []
    let remainingTotal = total

    for (let i = 0; i < numberOfValues - 1; i++) {
      let term = Math.ceil(remainingTotal * (1 - commonRatio))
      result.push(term)
      remainingTotal -= term
    }

    // The last term to ensure the sum is exactly equal to the total
    result.push(remainingTotal)

    return result
  }
</script>

<Bushes transformData={randomBushes} />

<BirchTrees transformData={randomBirchTrees} />

<Trees transformData={randomTrees} />

<Rocks transformData={randomRocks} />
<script>
  import { T } from '@threlte/core'
  import { OrbitControls } from '@threlte/extras'
  import Scenery from './Random.svelte'
</script>

<T.PerspectiveCamera
  makeDefault
  position={[20, 20, 20]}
>
  <OrbitControls maxPolarAngle={1.56} />
</T.PerspectiveCamera>

<T.DirectionalLight position={[3, 10, 7]} />
<T.AmbientLight />

<T.Mesh rotation.x={-Math.PI / 2}>
  <T.PlaneGeometry args={[20, 20, 1, 1]} />
  <T.MeshStandardMaterial color="green" />
</T.Mesh>

<Scenery />
<script lang="ts">
  import * as THREE from 'three'
  import { T } from '@threlte/core'
  import { useGltf, useTexture, InstancedMesh, Instance } from '@threlte/extras'

  export let transformData: number[][] = []

  type GLTFResult = {
    nodes: {
      Cube004: THREE.Mesh
      Cube004_1: THREE.Mesh
    }
    materials: {
      BirchTree_Bark: THREE.MeshStandardMaterial
      BirchTree_Leaves: THREE.MeshStandardMaterial
    }
  }

  const gltf = useGltf<GLTFResult>(
    'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/BirchTree_1.gltf'
  )
  const texture1 = useTexture(
    'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/BirchTree_Bark.png'
  )
  const normalMap1 = useTexture(
    'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/BirchTree_Bark_Normal.png'
  )
  const texture2 = useTexture(
    'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/BirchTree_Leaves.png'
  )

  const assets = Promise.all([gltf, texture1, texture2, normalMap1])
</script>

{#await assets then _}
  <InstancedMesh>
    <T is={$gltf.nodes.Cube004.geometry} />
    <T.MeshStandardMaterial
      map={$texture1}
      map.wrapS={THREE.RepeatWrapping}
      map.wrapT={THREE.RepeatWrapping}
      normalMap={$normalMap1}
      normalMap.wrapS={THREE.RepeatWrapping}
      normalMap.wrapT={THREE.RepeatWrapping}
    />
    {#each transformData as randomValues}
      {@const x = randomValues[0] * 20 - 10}
      {@const z = randomValues[1] * 20 - 10}
      {@const rot = randomValues[2] * Math.PI * 2}
      {@const scale = randomValues[3] * 2 + 1}
      <Instance
        position.x={x}
        position.z={z}
        rotation.y={rot}
        {scale}
      />
    {/each}
  </InstancedMesh>
  <InstancedMesh>
    <T is={$gltf.nodes.Cube004_1.geometry} />
    <T.MeshStandardMaterial
      map={$texture2}
      side={THREE.DoubleSide}
      alphaTest={0.5}
    />
    {#each transformData as randomValues}
      {@const x = randomValues[0] * 20 - 10}
      {@const z = randomValues[1] * 20 - 10}
      {@const rot = randomValues[2] * Math.PI * 2}
      {@const scale = randomValues[3] * 2 + 1}
      <Instance
        position.x={x}
        position.z={z}
        rotation.y={rot}
        {scale}
      />
    {/each}
  </InstancedMesh>
{/await}
<script lang="ts">
  import * as THREE from 'three'
  import { T } from '@threlte/core'
  import { useGltf, useTexture, InstancedMesh, Instance } from '@threlte/extras'

  export let transformData: [number, number, number, number][] = []

  type GLTFResult = {
    nodes: {
      Bush: THREE.Mesh
    }
    materials: {
      Bush_Leaves: THREE.MeshStandardMaterial
    }
  }

  const gltf = useGltf<GLTFResult>('https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Bush.gltf')
  const texture1 = useTexture(
    'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/Bush_Leaves.png'
  )

  const assets = Promise.all([gltf, texture1])
</script>

{#await assets then [$gltf, $texture1]}
  <InstancedMesh>
    <T is={$gltf.nodes.Bush.geometry} />
    <T.MeshStandardMaterial
      map={$texture1}
      alphaTest={0.2}
    />
    {#each transformData as randomValues}
      {@const x = randomValues[0] * 20 - 10}
      {@const z = randomValues[1] * 20 - 10}
      {@const rot = randomValues[2] * Math.PI * 2}
      {@const scale = randomValues[3] * 2 + 0.5}
      <T.Group
        position.x={x}
        position.z={z}
        rotation.y={rot}
        {scale}
      >
        <Instance rotation={[1.96, -0.48, -0.85]} />
      </T.Group>
    {/each}
  </InstancedMesh>
{/await}
<script lang="ts">
  import * as THREE from 'three'
  import { T } from '@threlte/core'
  import { useGltf, InstancedMesh, Instance } from '@threlte/extras'

  export let transformData: [number, number, number, number][] = []

  type GLTFResult = {
    nodes: {
      Rock_2: THREE.Mesh
    }
    materials: {
      Rock: THREE.MeshStandardMaterial
    }
  }

  const gltf = useGltf<GLTFResult>(
    'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Rock_2.gltf'
  )
</script>

{#if $gltf}
  <InstancedMesh>
    <T is={$gltf.nodes.Rock_2.geometry} />
    <T.MeshStandardMaterial color="grey" />
    {#each transformData as randomValues}
      {@const x = randomValues[0] * 20 - 10}
      {@const z = randomValues[1] * 20 - 10}
      {@const rot = randomValues[2] * Math.PI * 2}
      {@const scale = randomValues[3] + 0.5}
      <Instance
        position.x={x}
        position.z={z}
        rotation.y={rot}
        {scale}
      />
    {/each}
  </InstancedMesh>
{/if}
<script lang="ts">
  import * as THREE from 'three'
  import { T } from '@threlte/core'
  import { useGltf, useTexture, InstancedMesh, Instance } from '@threlte/extras'

  export let transformData: [number, number, number, number][] = []

  type GLTFResult = {
    nodes: {
      Cylinder001: THREE.Mesh
      Cylinder001_1: THREE.Mesh
    }
    materials: {
      NormalTree_Bark: THREE.MeshStandardMaterial
      NormalTree_Leaves: THREE.MeshStandardMaterial
    }
  }

  const gltf = useGltf<GLTFResult>(
    'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/NormalTree_1.gltf'
  )
  const texture1 = useTexture(
    'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/NormalTree_Bark.png'
  )
  const normalMap1 = useTexture(
    'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/NormalTree_Bark_Normal.png'
  )
  const texture2 = useTexture(
    'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/NormalTree_Leaves.png'
  )

  const assets = Promise.all([gltf, texture1, normalMap1, texture2])
</script>

{#await assets then [$gltf, $texture1, $normalMap1, $texture2]}
  <InstancedMesh>
    <T is={$gltf.nodes.Cylinder001.geometry} />
    <T.MeshStandardMaterial
      map={$texture1}
      map.wrapS={THREE.RepeatWrapping}
      map.wrapT={THREE.RepeatWrapping}
      normalMap={$normalMap1}
      normalMap.wrapS={THREE.RepeatWrapping}
      normalMap.wrapT={THREE.RepeatWrapping}
    />
    {#each transformData as randomValues}
      {@const x = randomValues[0] * 20 - 10}
      {@const z = randomValues[1] * 20 - 10}
      {@const rot = randomValues[2] * Math.PI * 2}
      {@const scale = randomValues[3] * 2 + 1}
      <Instance
        position.x={x}
        position.z={z}
        rotation.y={rot}
        {scale}
      />
    {/each}
  </InstancedMesh>
  <InstancedMesh>
    <T is={$gltf.nodes.Cylinder001_1.geometry} />
    <T.MeshStandardMaterial
      map={$texture2}
      side={THREE.DoubleSide}
      alphaTest={0.5}
    />
    {#each transformData as randomValues}
      {@const x = randomValues[0] * 20 - 10}
      {@const z = randomValues[1] * 20 - 10}
      {@const rot = randomValues[2] * Math.PI * 2}
      {@const scale = randomValues[3] * 2 + 1}
      <Instance
        position.x={x}
        position.z={z}
        rotation.y={rot}
        {scale}
      />
    {/each}
  </InstancedMesh>
{/await}
import { writable } from 'svelte/store'

export const regen = writable(false)
export const numberOfObjects = writable(50)

Preventing Object Overlap

There is a limitation in using just Math.random: it does not prevent objects from overlapping. This means that sometimes you’ll see a tree growing from a rock, or two bushes growing into each other.

In order to prevent this you can use Poisson disk sampling. This algorithm guarantees a minimum distance between your objects.

<script>
  import { Canvas } from '@threlte/core'
  import Scene from './Scene.svelte'
  import { Pane, Button, Slider } from 'svelte-tweakpane-ui'
  import { regen, radius } from './stores'
</script>

<Pane
  title="Poisson Disc Sampling"
  position="fixed"
>
  <Button
    title="regenerate"
    on:click={() => {
      $regen = !$regen
    }}
  />
  <Slider
    bind:value={$radius}
    label="Min Distance Between Objects"
    min={1}
    max={6}
    step={0.5}
  />
</Pane>

<div>
  <Canvas>
    <Scene />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { watch } from '@threlte/core'
  import { radius, regen, width, height } from './stores'
  import { AdaptedPoissonDiscSample as Sampler } from './sampling'
  // The following component started as a copy from https://fun-bit.vercel.app/
  import Bushes from './assets/bush.svelte'

  let sampler = new Sampler($radius, [width, height], undefined, Math.random)
  let points = sampler.GeneratePoints()
  addRandomValues()

  function addRandomValues() {
    for (let i = 0; i < points.length; i++) {
      points[i].push(Math.random(), Math.random())
    }
  }

  watch([regen, radius], () => {
    sampler = new Sampler($radius, [width, height], undefined, Math.random)
    points = sampler.GeneratePoints()
    addRandomValues()
  })
</script>

<Bushes transformData={points} />
<script>
  import { T } from '@threlte/core'
  import { OrbitControls } from '@threlte/extras'
  import { width, height } from './stores'
  import Random from './Random.svelte'
</script>

<T.PerspectiveCamera
  makeDefault
  position={[20, 20, 20]}
>
  <OrbitControls maxPolarAngle={1.56} />
</T.PerspectiveCamera>

<T.DirectionalLight position={[3, 10, 7]} />
<T.AmbientLight />

<T.Mesh rotation.x={-Math.PI / 2}>
  <T.PlaneGeometry args={[width, height, 1, 1]} />
  <T.MeshStandardMaterial color="green" />
</T.Mesh>

<Random />
<script lang="ts">
  import * as THREE from 'three'
  import { T } from '@threlte/core'
  import { useGltf, useTexture, InstancedMesh, Instance } from '@threlte/extras'

  export let transformData: [number, number, number, number][] = []

  type GLTFResult = {
    nodes: {
      Bush: THREE.Mesh
    }
    materials: {
      Bush_Leaves: THREE.MeshStandardMaterial
    }
  }

  const gltf = useGltf<GLTFResult>('https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Bush.gltf')
  const texture1 = useTexture(
    'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/Bush_Leaves.png'
  )

  const assets = Promise.all([gltf, texture1])
</script>

{#await assets then [$gltf, $texture1]}
  <InstancedMesh>
    <T is={$gltf.nodes.Bush.geometry} />
    <T.MeshStandardMaterial
      map={$texture1}
      alphaTest={0.2}
    />
    {#each transformData as randomValues}
      {@const x = randomValues[0] - 10}
      {@const z = randomValues[1] - 10}
      {@const rot = randomValues[2] * Math.PI * 2}
      {@const scale = randomValues[3] * 2 + 0.5}
      <T.Group
        position.x={x}
        position.z={z}
        rotation.y={rot}
        {scale}
      >
        <Instance rotation={[1.96, -0.48, -0.85]} />
      </T.Group>
    {/each}
  </InstancedMesh>
{/await}
// Adapted from: https://github.com/SebLague/Poisson-Disc-Sampling
// https://www.cs.ubc.ca/~rbridson/docs/bridson-siggraph07-poissondisk.pdf

export class PoissonDiscSample {
  /**
   * @param {number} radius
   * @param {number[]} region even numbered width/height vector
   * @param {number} maxCandidates default 30
   */
  constructor(radius, region, maxCandidates = 30) {
    this.random = Math.random

    this.radius = radius
    this.cellSize = radius / Math.SQRT2
    this.maxCandidates = maxCandidates

    this.width = region[0]
    this.height = region[1]

    this.gridHeight = Math.ceil(this.height / this.cellSize)
    this.gridWidth = Math.ceil(this.width / this.cellSize)

    this.grid = new Array(this.gridHeight)
    for (let i = 0; i < this.gridHeight; i++) {
      this.grid[i] = [...new Array(this.gridWidth)].map((_) => 0)
    }

    this.points = []
    this.spawnPoints = []

    this.spawnPoints.push([this.width / 2, this.height / 2])
  }

  /**
   * @returns {number[][]} an array of points
   */
  GeneratePoints() {
    while (this.spawnPoints.length > 0) {
      // choose one of the spawn points at random
      const spawnIndex = Math.floor(this.random() * this.spawnPoints.length)
      const spawnCentre = this.spawnPoints[spawnIndex]
      let candidateAccepted = false

      // then generate k candidates around it
      for (let k = 0; k < this.maxCandidates; k++) {
        const angle = this.random() * Math.PI * 2
        const dir = [Math.sin(angle), Math.cos(angle)]
        const disp = Math.floor(this.random() * (this.radius + 1)) + this.radius
        const candidate = spawnCentre.map((val, i) => val + dir[i] * disp)

        // check if the candidate is valid
        if (this.IsValid(candidate)) {
          this.points.push(candidate)
          this.spawnPoints.push(candidate)
          const gridX = Math.ceil(candidate[0] / this.cellSize) - 1
          const gridY = Math.ceil(candidate[1] / this.cellSize) - 1
          this.grid[gridY][gridX] = this.points.length
          candidateAccepted = true
          break
        }
      }
      // If no candidates around it were valid
      if (!candidateAccepted) {
        // Remove it from the spawnpoints list
        this.spawnPoints.splice(spawnIndex, 1)
      }
    }
    return this.points
  }

  IsValid(candidate) {
    const cX = candidate[0]
    const cY = candidate[1]
    if (cX >= 0 && cX < this.width && cY >= 0 && cY < this.height) {
      const cellX = Math.ceil(cX / this.cellSize)
      const cellY = Math.ceil(cY / this.cellSize)
      const searchStartX = Math.max(0, cellX - 2)
      const searchEndX = Math.min(cellX + 2, this.gridWidth - 1)
      const searchStartY = Math.max(0, cellY - 2)
      const searchEndY = Math.min(cellY + 2, this.gridHeight - 1)

      for (let x = searchStartX; x <= searchEndX; x++) {
        for (let y = searchStartY; y <= searchEndY; y++) {
          const pointIndex = this.grid[y][x]
          if (pointIndex != 0) {
            const diff = candidate.map((val, i) => val - this.points[pointIndex - 1][i])
            // we're not worried about the actual distance, just the equality
            const sqrdDst = Math.pow(diff[0], 2) + Math.pow(diff[1], 2)
            if (sqrdDst < Math.pow(this.radius, 2)) {
              return false
            }
          }
        }
      }
      return true
    }
    return false
  }
}

export class AdaptedPoissonDiscSample extends PoissonDiscSample {
  /**
   * @param {number} radius
   * @param {number[]} region even numbered width/height vector
   * @param {number} maxCandidates default 30
   * @param {()=>number} random a random (or pusedo-random) number generator (0, 1)
   */
  constructor(radius, region, maxCandidates = 30, random) {
    super(radius, region, maxCandidates)
    this.random = random
    this.spawnPoints = []
    const x = Math.floor(this.random() * this.width)
    const y = Math.floor(this.random() * this.height)
    this.spawnPoints.push([x, y])
  }
}
import { writable } from 'svelte/store'

export const regen = writable(false)
export const radius = writable(4)
export const width = 20
export const height = 20

If you reduce the minimum distance to something smaller than your objects size then there will look like there’s collisions. For the bushes in this example, even a distance of 1 still looks good.

Different object sizes

In many scenes this approach works well. However, sometimes you’ll want different spacing for different objects: a large tree needs more space than a small bush. Below is a variation of poisson disc sampling, but this time it allows for some different spacing, depending on the object type.

<script>
  import { Canvas } from '@threlte/core'
  import Scene from './Scene.svelte'
  import { Pane, Button } from 'svelte-tweakpane-ui'
  import { regen, radius } from './stores'
</script>

<Pane
  title="Adjusted Sampling"
  position="fixed"
>
  <Button
    title="regenerate"
    on:click={() => {
      $regen = !$regen
    }}
  />
</Pane>

<div>
  <Canvas>
    <Scene />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { watch } from '@threlte/core'
  import { radius, regen, width, height } from './stores'
  import { PoissonDiscSample as Sampler, type Point } from './sampling'
  // The following components started as copies from https://fun-bit.vercel.app/
  import Trees from './assets/tree.svelte'
  import Bushes from './assets/bush.svelte'
  import Rocks from './assets/rock.svelte'

  const pointsMatrix = [
    { radius: 6, desription: 'large', density: 15 },
    { radius: 4, desription: 'medium', density: 35 },
    { radius: 2, desription: 'small', density: 50 }
  ]

  let sampler = new Sampler(pointsMatrix, { width, height }, undefined, Math.random)
  let points: Point[] = sampler.generatePoints()
  let smallObjects = points
    .filter((obj) => obj.desription == 'small')
    .map((value) => {
      return [value.x, value.y, Math.random(), Math.random()]
    })
  let mediumObjects = points
    .filter((obj) => obj.desription == 'medium')
    .map((value) => {
      return [value.x, value.y, Math.random(), Math.random()]
    })
  let largeObjects = points
    .filter((obj) => obj.desription == 'large')
    .map((value) => {
      return [value.x, value.y, Math.random(), Math.random()]
    })

  watch([regen, radius], () => {
    sampler = new Sampler(pointsMatrix, { width, height }, undefined, Math.random)
    points = sampler.generatePoints()
    smallObjects = points
      .filter((obj) => obj.desription == 'small')
      .map((value) => {
        return [value.x, value.y, Math.random(), Math.random()]
      })
    mediumObjects = points
      .filter((obj) => obj.desription == 'medium')
      .map((value) => {
        return [value.x, value.y, Math.random(), Math.random()]
      })
    largeObjects = points
      .filter((obj) => obj.desription == 'large')
      .map((value) => {
        return [value.x, value.y, Math.random(), Math.random()]
      })
  })
</script>

<Bushes transformData={smallObjects} />
<Trees transformData={mediumObjects} />

<Rocks transformData={largeObjects} />
<script lang="ts">
  import { T } from '@threlte/core'
  import { OrbitControls } from '@threlte/extras'
  import { width, height } from './stores'
  import Random from './Random.svelte'
</script>

<T.PerspectiveCamera
  makeDefault
  position={[20, 20, 20]}
>
  <OrbitControls maxPolarAngle={1.56} />
</T.PerspectiveCamera>

<T.DirectionalLight position={[3, 10, 7]} />
<T.AmbientLight />

<T.Mesh rotation.x={-Math.PI / 2}>
  <T.PlaneGeometry args={[width, height, 1, 1]} />
  <T.MeshStandardMaterial color="green" />
</T.Mesh>

<Random />
<script lang="ts">
  import * as THREE from 'three'
  import { T } from '@threlte/core'
  import { useGltf, useTexture, InstancedMesh, Instance } from '@threlte/extras'

  export let transformData: [number, number, number, number][] = []

  type GLTFResult = {
    nodes: {
      Bush: THREE.Mesh
    }
    materials: {
      Bush_Leaves: THREE.MeshStandardMaterial
    }
  }

  const gltf = useGltf<GLTFResult>('https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Bush.gltf')
  const texture1 = useTexture(
    'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/Bush_Leaves.png'
  )

  const assets = Promise.all([gltf, texture1])
</script>

{#await assets then [$gltf, $texture1]}
  <InstancedMesh>
    <T is={$gltf.nodes.Bush.geometry} />
    <T.MeshStandardMaterial
      map={$texture1}
      alphaTest={0.2}
    />
    {#each transformData as randomValues}
      {@const x = randomValues[0] - 10}
      {@const z = randomValues[1] - 10}
      {@const rot = randomValues[2] * Math.PI * 2}
      {@const scale = randomValues[3] * 2 + 0.5}
      <T.Group
        position.x={x}
        position.z={z}
        rotation.y={rot}
        {scale}
      >
        <Instance rotation={[1.96, -0.48, -0.85]} />
      </T.Group>
    {/each}
  </InstancedMesh>
{/await}
<script lang="ts">
  import * as THREE from 'three'
  import { T } from '@threlte/core'
  import { useGltf, InstancedMesh, Instance } from '@threlte/extras'

  export let transformData: [number, number, number, number][] = []

  type GLTFResult = {
    nodes: {
      Rock_2: THREE.Mesh
    }
    materials: {
      Rock: THREE.MeshStandardMaterial
    }
  }

  const gltf = useGltf<GLTFResult>(
    'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Rock_2.gltf'
  )
</script>

{#if $gltf}
  <InstancedMesh>
    <T is={$gltf.nodes.Rock_2.geometry} />
    <T.MeshStandardMaterial color="grey" />
    {#each transformData as randomValues}
      {@const x = randomValues[0] - 10}
      {@const z = randomValues[1] - 10}
      {@const rot = randomValues[2] * Math.PI * 2}
      {@const scale = randomValues[3] * 4 + 2}
      <Instance
        position.x={x}
        position.z={z}
        rotation.y={rot}
        {scale}
      />
    {/each}
  </InstancedMesh>
{/if}
<script lang="ts">
  import * as THREE from 'three'
  import { T } from '@threlte/core'
  import { useGltf, useTexture, InstancedMesh, Instance } from '@threlte/extras'

  export let transformData: [number, number, number, number][] = []

  type GLTFResult = {
    nodes: {
      Cylinder001: THREE.Mesh
      Cylinder001_1: THREE.Mesh
    }
    materials: {
      NormalTree_Bark: THREE.MeshStandardMaterial
      NormalTree_Leaves: THREE.MeshStandardMaterial
    }
  }

  const gltf = useGltf<GLTFResult>(
    'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/NormalTree_1.gltf'
  )
  const texture1 = useTexture(
    'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/NormalTree_Bark.png'
  )
  const normalMap1 = useTexture(
    'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/NormalTree_Bark_Normal.png'
  )
  const texture2 = useTexture(
    'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/NormalTree_Leaves.png'
  )

  const assets = Promise.all([gltf, texture1, normalMap1, texture2])
</script>

{#await assets then [$gltf, $texture1, $normalMap1, $texture2]}
  <InstancedMesh>
    <T is={$gltf.nodes.Cylinder001.geometry} />
    <T.MeshStandardMaterial
      map={$texture1}
      map.wrapS={THREE.RepeatWrapping}
      map.wrapT={THREE.RepeatWrapping}
      normalMap={$normalMap1}
      normalMap.wrapS={THREE.RepeatWrapping}
      normalMap.wrapT={THREE.RepeatWrapping}
    />
    {#each transformData as randomValues}
      {@const x = randomValues[0] - 10}
      {@const z = randomValues[1] - 10}
      {@const rot = randomValues[2] * Math.PI * 2}
      {@const scale = randomValues[3] * 2 + 1}
      <Instance
        position.x={x}
        position.z={z}
        rotation.y={rot}
        {scale}
      />
    {/each}
  </InstancedMesh>
  <InstancedMesh>
    <T is={$gltf.nodes.Cylinder001_1.geometry} />
    <T.MeshStandardMaterial
      map={$texture2}
      side={THREE.DoubleSide}
      alphaTest={0.5}
    />
    {#each transformData as randomValues}
      {@const x = randomValues[0] - 10}
      {@const z = randomValues[1] - 10}
      {@const rot = randomValues[2] * Math.PI * 2}
      {@const scale = randomValues[3] * 2 + 1}
      <Instance
        position.x={x}
        position.z={z}
        rotation.y={rot}
        {scale}
      />
    {/each}
  </InstancedMesh>
{/await}
// Adapted from: https://github.com/SebLague/Poisson-Disc-Sampling
// https://www.cs.ubc.ca/~rbridson/docs/bridson-siggraph07-poissondisk.pdf

export type Point = {
  x: number
  y: number
  desription: string
}

export class PoissonDiscSample {
  random
  radiiMatrix: { desription: string; density: number; radius: number }[]
  radiiMap: { [key: string]: number }
  maxRadius: number
  customRanges: { start: number; end: number; desription: string }[] = []
  cellSize: number
  cellSizeMatrix: { [key: string]: number }
  maxCandidates: number
  windowSize: number
  width = 1
  height = 1
  /** 2D array of indices of points */
  grid: number[][] = []
  gridWidth: number
  gridHeight: number

  points: Point[] = []
  spawnPoints: Point[] = []

  constructor(
    radiiMatrix: { desription: string; density: number; radius: number }[],
    region: { width: number; height: number },
    maxCandidates = 30,
    random = Math.random
  ) {
    this.random = random

    this.radiiMatrix = radiiMatrix
    // make sure the density sums to 1 so we can use it later
    const densityTotal = this.radiiMatrix.reduce((total, obj) => {
      return total + obj.density
    }, 0)
    if (densityTotal > 1 || densityTotal < 1) {
      this.radiiMatrix = this.radiiMatrix.map((obj) => {
        return {
          ...obj,
          density: obj.density / densityTotal
        }
      }, 0)
    }
    let currentTotal = 0
    this.customRanges = this.radiiMatrix.map((obj) => {
      let range = {
        start: currentTotal,
        end: currentTotal + obj.density,
        desription: obj.desription
      }
      currentTotal += obj.density
      return range
    })

    this.maxRadius = this.radiiMatrix.reduce((max, obj) => {
      return obj.radius > max ? obj.radius : max
    }, -Infinity)

    this.radiiMap = this.radiiMatrix.reduce((obj, value) => {
      obj[value.desription] = value.radius
      return obj
    }, {})

    this.cellSizeMatrix = this.radiiMatrix.reduce((obj, value) => {
      obj[value.desription] = value.radius / Math.SQRT2
      return obj
    }, {})
    this.cellSize = Infinity
    for (const key in this.cellSizeMatrix) {
      if (this.cellSizeMatrix[key] < this.cellSize) {
        this.cellSize = this.cellSizeMatrix[key]
      }
    }
    this.windowSize = Math.ceil(this.maxRadius / this.cellSize)

    this.maxCandidates = maxCandidates

    this.width = region.width
    this.height = region.height

    this.gridHeight = Math.ceil(this.height / this.cellSize)
    this.gridWidth = Math.ceil(this.width / this.cellSize)

    this.grid = new Array(this.gridHeight)
    for (let i = 0; i < this.gridHeight; i++) {
      this.grid[i] = [...new Array(this.gridWidth)].map((_) => 0)
    }

    this.points = []
    this.spawnPoints = []

    const x = Math.floor(this.random() * this.width)
    const y = Math.floor(this.random() * this.height)

    this.spawnPoints.push({ x, y, desription: this.createPointType() })
  }

  generatePoints(): Point[] {
    while (this.spawnPoints.length > 0) {
      // choose one of the spawn points at random
      const spawnIndex = Math.floor(this.random() * this.spawnPoints.length)
      const spawnCentre = this.spawnPoints[spawnIndex]
      let candidateAccepted = false

      // then generate k candidates around it
      for (let k = 0; k < this.maxCandidates; k++) {
        const angle = this.random() * Math.PI * 2
        const dir = [Math.sin(angle), Math.cos(angle)]
        // TODO-DefinitelyMaybe: select a point and calc it's displacement
        const candidateType = this.createPointType()

        // const disp = Math.floor(this.random() * (this.radius + 1)) + this.radius
        const dispScalar = Math.max(
          this.radiiMap[candidateType],
          this.radiiMap[spawnCentre.desription]
        )
        const disp = Math.floor(this.random() * (dispScalar + 1)) + dispScalar
        const candidate = {
          x: spawnCentre.x + dir[0] * disp,
          y: spawnCentre.y + dir[1] * disp,
          desription: candidateType
        }
        // spawnCentre.map((val, i) => val + dir[i] * disp)

        // check if the candidate is valid
        if (this.isValid(candidate)) {
          this.points.push(candidate)
          this.spawnPoints.push(candidate)
          const gridX = Math.ceil(candidate.x / this.cellSize) - 1
          const gridY = Math.ceil(candidate.y / this.cellSize) - 1
          this.grid[gridY][gridX] = this.points.length
          candidateAccepted = true
          break
        }
      }
      // If no candidates around it were valid
      if (!candidateAccepted) {
        // Remove it from the spawnpoints list
        this.spawnPoints.splice(spawnIndex, 1)
      }
    }
    return this.points
  }

  createPointType(): string {
    const number = this.random()
    for (let i = 0; i < this.customRanges.length; i++) {
      const { start, end, desription } = this.customRanges[i]
      if (number > start && number <= end) {
        return desription
      }
    }
  }

  isValid(candidate: Point) {
    const cX = candidate.x
    const cY = candidate.y
    if (cX >= 0 && cX < this.width && cY >= 0 && cY < this.height) {
      const cellX = Math.ceil(cX / this.cellSize)
      const cellY = Math.ceil(cY / this.cellSize)
      const searchStartX = Math.max(0, cellX - this.windowSize)
      const searchEndX = Math.min(cellX + this.windowSize, this.gridWidth - 1)
      const searchStartY = Math.max(0, cellY - this.windowSize)
      const searchEndY = Math.min(cellY + this.windowSize, this.gridHeight - 1)

      for (let x = searchStartX; x <= searchEndX; x++) {
        for (let y = searchStartY; y <= searchEndY; y++) {
          const pointIndex = this.grid[y][x]
          if (pointIndex != 0) {
            const diff = [
              candidate.x - this.points[pointIndex - 1]?.x,
              candidate.y - this.points[pointIndex - 1]?.y
            ]
            // we're not worried about the actual distance, just the equality
            const sqrdDst = Math.pow(diff[0], 2) + Math.pow(diff[1], 2)
            if (
              sqrdDst <
              Math.pow(
                Math.max(
                  this.radiiMap[this.points[pointIndex - 1]?.desription],
                  this.radiiMap[candidate.desription]
                ),
                2
              )
            ) {
              return false
            }
          }
        }
      }
      return true
    }
    return false
  }
}
import { writable } from 'svelte/store'

export const regen = writable(false)
export const radius = writable(4)
export const width = 20
export const height = 20

An important parameter to play with when generating scenes with this last approach is the window size. It is inferred from the difference between the largest and smallest radius given. You’ll need to play around with the details if your usecase starts running into performance issues because of this algorithm.