Basics
Planet
Your basic world is created through interacting with the Planet
class. This will create a large cube, with each of its faces subdivided into a data structure called a QuadTree
. Don't worry, it will be a sphere in the end, as each point will be bent into shape.
import { Planet } from "@hello-worlds/planet"
import worker from "./planet.worker"
// ...some threejs scene above
const kerbin = new Planet({
radius: 100_000
minCellSize: 32,
minCellResolution: 48,
workerProps: {
numWorkers: 8,
worker,
},
data: {
seed: "My Cool Seed!",
},
})
scene.add(kerbin)
By subdividing each plane into smaller pieces, we can delegate their construction to threads (in the browser called web-workers), which frees up our interactivity and makes the app feel snappier. This technique allows us to create vast earth-size (or larger!) worlds, because the user will only load a small fraction of the world at a given time in any significant detail.
Web Workers
The webworker requirement will likely be made optional and not a hard dependency in the future. Also, like.. I want this to work with node.js ¯\_(ツ)_/¯
We will use these web worker 'threads' to calculate the height positions on our planet, as well as the colors of each vertex.
Because we will make use web workers, you'll need a way to tell your bundler to build them.
Generators
How do we add features to our planet? Generators!
Generators are simply functions that take an x,y,z
position value as an input, and returns an output. You'll have access to other inputs in your webworker generator, but only by knowing the x,y,z
in world coordinates of your vertex, you can already do many things!
Generators are higher-order functions. The first function allows you to initialize some expensive objects when the worker is first spawned. Use this as your "mount" effect to do things like create your noise objects. It's possible to pass initialData
to your mounting function, and it has access to details like the planet's radius.
The second function which is returned by the first will be ran for each chunk update.
Generators are ran on the threads, and handle one chunk at a time. They're also static, which means once you bundle your app, you won't be able to change them. You should import all the functions you intend to use, as you won't be able to say... pass in a function in the generator input data (without shinanigans!)
Height Generator
The height generator will be ran inside the chunk web worker first. Usually you'll apply some noise, but you must return a number. We'll use this height number to push the vertex away from the sphere's origin, creating altitude (or lack thereof).
import {
ChunkGenerator3Initializer,
ColorArrayWithAlpha,
createThreadedPlanetWorker,
DEFAULT_NOISE_PARAMS,
Noise,
NOISE_TYPES,
} from "@hello-worlds/planets"
import { Color, MathUtils } from "three"
export type ThreadParams = {
seed: string
}
const heightGenerator: ChunkGenerator3Initializer<ThreadParams, number> = ({
data: { seed },
}) => {
const terrainNoise = new Noise({
...DEFAULT_NOISE_PARAMS,
seed,
height: 2000,
scale: 3000,
})
return ({ input }) => {
const height = terrainNoise.get(input.x + w, input.y + w, input.z + w)
return height
}
}
// ... more below
Color Generator
The color generator will run after the height generator (and therefore will have access to the heigh details), and will return a THREE.Color
. We'll use this to paint the vertex in our THREE.Material
, and thus paint the world!
// ... continued from above
const colorGenerator: ChunkGenerator3Initializer<
ThreadParams,
Color | ColorArrayWithAlpha
> = () => {
return ({ worldPosition }) => {
const w = worldPosition.clone().normalize()
return new Color().setRGB(w.x, w.y, w.z)
}
}
// ... more below
Integrating into a worker
Simply call the createThreaded-WorldType-Worker()
function and pass in your generators.
// ... continued from above
createThreadedPlanetWorker<ThreadParams>({
heightGenerator,
colorGenerator,
})