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[][] = []
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 _}
<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[][] = []
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'
)
const assets = Promise.all([gltf])
</script>
{#await assets then _}
<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>
{/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[][] = []
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 _}
<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[][] = []
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 _}
<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[][] = []
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 _}
<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[][] = []
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'
)
const assets = Promise.all([gltf])
</script>
{#await assets then _}
<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>
{/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[][] = []
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 _}
<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.