Random Scenery
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
Section titled “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, number, number, number][] = [] let randomTrees: [number, number, number, number][] = [] let randomBirchTrees: [number, number, number, number][] = [] let randomRocks: [number, number, number, number][] = []
watch([regen, numberOfObjects], () => { generateRandomNumbers() })
generateRandomNumbers()
function generateRandomNumbers() { const exponentialSumValues = calculateExponentialSumValues( $numberOfObjects, distinctObjects, commonRatio ) const totalBushes = exponentialSumValues[0] ?? 0 const totalTrees = exponentialSumValues[1] ?? 0 const totalBirchTrees = exponentialSumValues[2] ?? 0 const totalRocks = exponentialSumValues[3] ?? 0
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]} castShadow shadow.camera.top={10} shadow.camera.left={-10} shadow.camera.right={10} shadow.camera.bottom={-10}/><T.AmbientLight />
<T.Mesh rotation.x={-Math.PI / 2} receiveShadow> <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'
interface Props { transformData?: [number, number, number, number][] }
let { transformData = [] }: Props = $props()
type GLTFResult = { nodes: { Cube004: THREE.Mesh Cube004_1: THREE.Mesh } materials: { BirchTree_Bark: THREE.MeshStandardMaterial BirchTree_Leaves: THREE.MeshStandardMaterial } }
const assets = Promise.all([ useGltf<GLTFResult>('https://fun-bit.vercel.app/Ultimate-Stylized-Nature/BirchTree_1.gltf'), useTexture('https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/BirchTree_Bark.png'), useTexture('https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/BirchTree_Leaves.png'), useTexture( 'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/BirchTree_Bark_Normal.png' ) ])</script>
{#await assets then [$gltf, $texture1, $texture2, $normalMap1]} <InstancedMesh castShadow> <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'
interface Props { transformData?: [number, number, number, number][] }
let { transformData = [] }: Props = $props()
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 castShadow receiveShadow > <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'
interface Props { transformData?: [number, number, number, number][] }
let { transformData = [] }: Props = $props()
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 castShadow receiveShadow > <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'
interface Props { transformData?: [number, number, number, number][] }
let { transformData = [] }: Props = $props()
type GLTFResult = { nodes: { Cylinder001: THREE.Mesh Cylinder001_1: THREE.Mesh } materials: { NormalTree_Bark: THREE.MeshStandardMaterial NormalTree_Leaves: THREE.MeshStandardMaterial } }
const assets = Promise.all([ useGltf<GLTFResult>('https://fun-bit.vercel.app/Ultimate-Stylized-Nature/NormalTree_1.gltf'), useTexture('https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/NormalTree_Bark.png'), useTexture( 'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/NormalTree_Leaves.png' ), useTexture( 'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/NormalTree_Bark_Normal.png' ) ])</script>
{#await assets then [$gltf, $texture1, $texture2, $normalMap1]} <InstancedMesh castShadow> <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 castShadow> <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
Section titled “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 as [number, number, number, number][]} /><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]} castShadow shadow.camera.top={10} shadow.camera.left={-10} shadow.camera.right={10} shadow.camera.bottom={-10}/><T.AmbientLight />
<T.Mesh rotation.x={-Math.PI / 2} receiveShadow> <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'
interface Props { transformData?: [number, number, number, number][] }
let { transformData = [] }: Props = $props()
type GLTFResult = { nodes: { Bush: THREE.Mesh } materials: { Bush_Leaves: THREE.MeshStandardMaterial } }
const assets = Promise.all([ useGltf<GLTFResult>('https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Bush.gltf'), useTexture('https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/Bush_Leaves.png') ])</script>
{#await assets then [$gltf, $texture1]} <InstancedMesh castShadow> <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 { random: () => number radius: number cellSize: number maxCandidates: number width: number height: number gridHeight: number gridWidth: number grid: any[] points: [number, number][] spawnPoints: [number, number][]
constructor(radius: number, region: [number, number], maxCandidates: number = 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(): number[][] { 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: [number, number] = [Math.sin(angle), Math.cos(angle)] const disp = Math.floor(this.random() * (this.radius + 1)) + this.radius const candidate: [number, number] = [ spawnCentre[0] + dir[0] * disp, spawnCentre[1] + dir[1] * 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: [number, number]) { 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 { constructor( radius: number, region: [number, number], maxCandidates: number = 30, random: () => number ) { 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 = 20export const height = 20If 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
Section titled “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: [number, number, number, number][] = points .filter((obj) => obj.desription == 'small') .map((value) => { return [value.x, value.y, Math.random(), Math.random()] }) let mediumObjects: [number, number, number, number][] = points .filter((obj) => obj.desription == 'medium') .map((value) => { return [value.x, value.y, Math.random(), Math.random()] }) let largeObjects: [number, number, number, number][] = 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={[6, 13, 10]} castShadow shadow.camera.top={12} shadow.camera.left={-12} shadow.camera.right={12} shadow.camera.bottom={-12}/><T.AmbientLight />
<T.Mesh rotation.x={-Math.PI / 2} receiveShadow> <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'
interface Props { transformData?: [number, number, number, number][] }
let { transformData = [] }: Props = $props()
type GLTFResult = { nodes: { Bush: THREE.Mesh } materials: { Bush_Leaves: THREE.MeshStandardMaterial } }
const assets = Promise.all([ useGltf<GLTFResult>('https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Bush.gltf'), useTexture('https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/Bush_Leaves.png') ])</script>
{#await assets then [$gltf, $texture1]} <InstancedMesh castShadow receiveShadow > <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'
interface Props { transformData?: [number, number, number, number][] }
let { transformData = [] }: Props = $props()
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 castShadow receiveShadow > <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'
interface Props { transformData?: [number, number, number, number][] }
let { transformData = [] }: Props = $props()
type GLTFResult = { nodes: { Cylinder001: THREE.Mesh Cylinder001_1: THREE.Mesh } materials: { NormalTree_Bark: THREE.MeshStandardMaterial NormalTree_Leaves: THREE.MeshStandardMaterial } }
const assets = Promise.all([ useGltf<GLTFResult>('https://fun-bit.vercel.app/Ultimate-Stylized-Nature/NormalTree_1.gltf'), useTexture('https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/NormalTree_Bark.png'), useTexture( 'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/NormalTree_Leaves.png' ), useTexture( 'https://fun-bit.vercel.app/Ultimate-Stylized-Nature/Textures/NormalTree_Bark_Normal.png' ) ])</script>
{#await assets then [$gltf, $texture1, $texture2, $normalMap1]} <InstancedMesh castShadow> <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 castShadow> <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: { [key: string]: number }, value) => { obj[value.desription] = value.radius return obj }, {})
this.cellSizeMatrix = this.radiiMatrix.reduce((obj: { [key: string]: number }, 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: [number, number] = [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() { const number = this.random() let value = '' for (let i = 0; i < this.customRanges.length; i++) { const { start, end, desription } = this.customRanges[i]! if (number > start && number <= end) { value = desription } } return value }
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: [number, number] = [ 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 = 20export const height = 20An 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.