From 63d1c4c526d1cd55b9f512a9220f8c0ebb5ff46e Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Thu, 26 Sep 2024 23:39:12 +0200 Subject: [PATCH 1/4] update gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a3d83c9..6fd25b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules old -dist \ No newline at end of file +dist +stuff \ No newline at end of file From 9d09e4ca956d0bc567e7bf376328b8b3faa154cf Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sun, 29 Sep 2024 08:16:30 +0200 Subject: [PATCH 2/4] some cleanup --- bun.lockb | Bin 0 -> 67707 bytes features.md | 2 + src/noise.ts | 624 ------------------------------------ src/script.ts | 9 +- src/worlds/biome.ts | 22 +- src/worlds/helper/octree.ts | 21 +- src/worlds/presets.ts | 4 +- src/worlds/stars.ts | 77 +++++ src/worlds/worker.ts | 7 +- 9 files changed, 117 insertions(+), 649 deletions(-) create mode 100755 bun.lockb delete mode 100644 src/noise.ts create mode 100644 src/worlds/stars.ts diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..a74dfe8789be743f1925d561615ed4ec68ad46df GIT binary patch literal 67707 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 a4Xepy5aTpIm sum + v * v, 0)); - if (oldLength === 0) return vector; - const scale = length / oldLength; - return vector.map((v) => v * scale); -} - -// angle between two 3D vectors -function angleTo(a: number[], b: number[]): number { - const dot = a.reduce((sum, v, i) => sum + v * b[i], 0); - const length = Math.sqrt( - a.reduce((sum, v) => sum + v * v, 0) * b.reduce((sum, v) => sum + v * v, 0), - ); - return Math.acos(dot / length); -} - -type ErosionUpFunction = (derivative: number[]) => number[]; - -export type VectorObject = { - x: number; - y: number; - z?: number; - w?: number; -}; - -export type NoiseParameterInput = number | UberNoise | UberNoiseOptions; -export type NoiseParameter = number | UberNoise; - -export type UberNoiseOptions = { - seed?: string | number; - - min?: NoiseParameterInput; - max?: NoiseParameterInput; - - scale?: NoiseParameterInput; - power?: NoiseParameterInput; - - shift?: number[]; - - octaves?: number; - gain?: NoiseParameterInput; - lacunarity?: NoiseParameterInput; - - amps?: number[]; - - layers?: UberNoiseOptions[]; - - erosion?: NoiseParameterInput; - erosionUp?: ErosionUpFunction; - - sharpness?: NoiseParameterInput; - steps?: NoiseParameterInput; - warp?: NoiseParameterInput; - warpNoise?: UberNoise; - warp2?: NoiseParameterInput; - warpNoise2?: UberNoise; - - invert?: boolean; - abs?: boolean; - clamp?: boolean; - tileX?: boolean; - tileY?: boolean; - tile?: boolean; -}; - -export class UberNoise { - private noise2D: NoiseFunction2D | undefined; - private noise3D: NoiseFunction3D | undefined; - private noise4D: NoiseFunction4D | undefined; - private seed: string | number; - - private _min: NoiseParameter = -1; - private _max: NoiseParameter = 1; - - private _scale: NoiseParameter = 1; - private _power: NoiseParameter = 1; - - private shift: number[] | undefined = undefined; - - private octaves = 0; - private _gain: NoiseParameter = 0.5; - private _lacunarity: NoiseParameter = 2; - - private amps: number[] = []; - - private _erosion: NoiseParameter = 0; - private _sharpness: NoiseParameter = 0; - private _steps: NoiseParameter = 0; - - private _warp: NoiseParameter = 0; - private _warpNoise: UberNoise | undefined; - private _warp2: NoiseParameter = 0; - private _warpNoise2: UberNoise | undefined; - - private tileX = false; - private tileY = false; - - private erosionDelta = 0; - private erosionDerivative?: number[] = undefined; - private erosionUp?: ErosionUpFunction; - - private static erosionDefaultUp = [0, 0, 1]; - - private position = [0, 0]; - - private pngr; - - private layers: UberNoise[] = []; - - constructor(options: UberNoiseOptions = {}) { - this.seed = options.seed ?? Math.random(); - this.pngr = alea(this.seed); - - this.min = options.min ?? -1; - this.max = options.max ?? 1; - - this.scale = options.scale ?? 1; - this.power = options.power ?? 1; - - this.octaves = options.octaves ?? options.layers?.length ?? 0; - this.gain = options.gain ?? 0.5; - this.lacunarity = options.lacunarity ?? 2; - - this.erosion = options.erosion ?? 0; - this.sharpness = options.sharpness ?? 0; - this.steps = options.steps ?? 0; - - this.warp = options.warp ?? 0; - this.warpNoise = options.warpNoise; - this.warp2 = options.warp2 ?? 0; - this.warpNoise2 = options.warpNoise2; - - this.processLayers(options); - - this.tileX = options.tileX ?? options.tile ?? false; - this.tileY = options.tileY ?? options.tile ?? false; - } - - processLayers(options: UberNoiseOptions) { - this.amps = options.amps ?? []; - - for (let i = 0; i < this.octaves; i++) { - const layerOptions = options.layers?.[i] ?? {}; - if (layerOptions instanceof UberNoise) { - this.layers[i] = layerOptions; - } else { - if (layerOptions.seed === undefined) layerOptions.seed = this.pngr(); - this.layers[i] = new UberNoise(layerOptions); - } - } - if (this.layers.length == 0) { - this.noise2D = createNoise2D(this.pngr); - this.noise3D = createNoise3D(this.pngr); - this.noise4D = createNoise4D(this.pngr); - } - } - - // same as normalized but returns between min and max - get( - x: number | VectorObject | Array, - y: number | undefined = undefined, - z: number | undefined = undefined, - w: number | undefined = undefined, - ): number { - const norm = this.normalized(x, y, z, w); - return this.toMinMax(norm); - } - - // same as get but returns between -1 and 1 - normalized( - x: number | VectorObject | Array, - y: number | undefined = undefined, - z: number | undefined = undefined, - w: number | undefined = undefined, - ): number { - // if x is an array, treat it as a vector - if (Array.isArray(x)) { - w = x[3]; - z = x[2]; - y = x[1]; - x = x[0]; - } else if (typeof x === "object") { - w = x.w; - z = x.z; - y = x.y; - x = x.x; - } - // if y is undefined, treat it as 2D noise with y = 0 - y = y ?? 0; - - return this.getNoise(x, y, z, w); - } - - private warpPosition(warp: number, warpNoise: UberNoise | undefined) { - if (warp == undefined || warp == 0) return; - - if (warpNoise != undefined) warpNoise.position = this.position; - - let x = this.position[0], - y = this.position[1], - z = this.position[2], - w = this.position[3]; - - const noise = warpNoise ?? this; - const scl = this.scale; - - this.position = [x + 54.47 * scl, y + 34.98 * scl]; - if (z != undefined) this.position.push(z + 21.63 * scl); - if (w != undefined) this.position.push(w + 67.1 * scl); - - x += noise.getFBM() * warp; - y += noise.getFBM() * warp; - if (z != undefined) z += noise.getFBM() * warp; - if (w != undefined) w += noise.getFBM() * warp; - - this.position = [x, y, z, w]; - } - - private tilePosition() { - if (!this.tileX && !this.tileY) return; - const x = this.position[0]; - const y = this.position[1]; - let newX = 0, - newY = 0, - newZ = 0, - newW = 0; - if (this.tileX) { - newX = Math.sin(x * Math.PI * 2); - newY = Math.cos(x * Math.PI * 2); - } - if (this.tileY) { - newZ = Math.sin(y * Math.PI * 2); - newW = Math.cos(y * Math.PI * 2); - } - if (this.tileX && !this.tileY) { - this.position = [newX, newY + y]; - } else if (this.tileY && !this.tileX) { - this.position = [newZ + x, newW]; - } else if (this.tileX && this.tileY) { - this.position = [newX, newY, newZ, newW]; - } - } - - private getDerivative( - x: number, - y: number, - z: number, - n: number | undefined = undefined, - ): number[] | undefined { - // left side or central difference - // very expensive (four/six noise calls), should be changed to analytical derivatives - // see https://iquilezles.org/www/articles/morenoise/morenoise.htm - - if (z == undefined) return undefined; - - const mov = this.erosionDelta; - - const dx = (n ?? this.get(x - mov, y, z)) - this.get(x + mov, y, z); - const dy = (n ?? this.get(x, y - mov, z)) - this.get(x, y + mov, z); - const dz = (n ?? this.get(x, y, z - mov)) - this.get(x, y, z + mov); - - this.erosionDerivative = [dx, dy, dz]; - return this.erosionDerivative; - } - - private getBaseLayer( - scale: number, - x: number, - y: number, - z?: number, - w?: number, - ): number { - if (z != undefined && w != undefined && this.noise4D != undefined) { - return this.noise4D(x * scale, y * scale, z * scale, w * scale); - } - if (z != undefined && this.noise3D != undefined) { - return this.noise3D(x * scale, y * scale, z * scale); - } - if (this.noise2D != undefined) { - return this.noise2D(x * scale, y * scale); - } - return 0; - } - - private getFBM(useErosion = false): number { - const x = this.position[0], - y = this.position[1], - z = this.position[2], - w = this.position[3]; - - const scale = this.scale; - - if (this.layers.length === 0) { - return this.getBaseLayer(scale, x, y, z, w); - } - - let maxAmp = 1; - let amp = 1, - freq = scale; - - const lacunarity = this.lacunarity; - const gain = this.gain; - - // for now we'll always ignore erosion - // remove the following line later to enable erosion - useErosion = false; - const erosion = useErosion ? this.erosion : 0; - const up = - this.erosionUp != undefined - ? this.erosionUp(this.position) - : UberNoise.erosionDefaultUp; - const sum = [0, 0, 0]; - - let n = 0; - - for (let i = 0; i < this.octaves; i++) { - const layer = this.layers[i]; - - const layerAmp = this.amps[i] ?? 1; - - const value = - layer.get( - x * freq, - y * freq, - z != undefined ? z * freq : undefined, - w != undefined ? w * freq : undefined, - ) * - amp * - layerAmp; - if (erosion > 0) { - const derivative = layer.getDerivative(x * freq, y * freq, z * freq); - - if (derivative != undefined) { - setLength(derivative, amp * layerAmp); - sum[0] += derivative[0]; - sum[1] += derivative[1]; - sum[2] += derivative[2]; - - const mult = Math.abs(1 - angleTo(up, sum) / Math.PI); - - n += value * (mult * erosion + 1 - erosion); - } - } else { - n += value; - } - - amp *= gain; - freq *= lacunarity; - maxAmp += amp * layerAmp; - } - return n / maxAmp; - } - - move( - x: number, - y: number, - z: number | undefined = undefined, - w: number | undefined = undefined, - ) { - if (this.shift == undefined) this.shift = [0, 0, 0]; - - this.shift[0] += x; - this.shift[1] += y; - if (z != undefined) this.shift[2] += z; - if (w != undefined) this.shift[3] += w; - } - - private getNoise( - x: number, - y: number, - z: number | undefined = undefined, - w: number | undefined = undefined, - ): number { - // set position - this.position[0] = x; - this.position[1] = y; - if (z !== undefined) { - this.position[2] = z; - } - if (w !== undefined) { - this.position[3] = w; - } - - // apply shift - const shift = this.shift; - if (shift !== undefined) { - for (let i = 0; i < this.position.length && i < shift.length; i++) { - this.position[i] += shift[i]; - } - } - - // apply tiling - this.tilePosition(); - - // apply warp - this.warpPosition(this.warp, this.warpNoise); - this.warpPosition(this.warp2, this.warpNoise2); - - let norm = this.getFBM(true); - - // apply power - const power = this.power; - if (power != 1) { - // convert to [0 - 1], apply power and back to [-1, 1] - norm = (Math.pow((norm + 1) * 0.5, power) - 0.5) * 2; - } - - // apply sharpness - const sharpness = this.sharpness; - if (sharpness != 0) { - const billow = (Math.abs(norm) - 0.5) * 2; - const ridged = (0.5 - Math.abs(norm)) * 2; - - norm = lerp(norm, billow, Math.max(0, sharpness)); - norm = lerp(norm, ridged, Math.max(0, -sharpness)); - } - - // apply steps - const steps = this.steps; - if (steps != 0) { - // convert from -1 to 1 to 0 to 1 - norm = (norm + 1) * 0.5; - // apply steps - norm = Math.floor(norm * steps) / steps; - // convert back to -1 to 1 - norm = norm * 2 - 1; - } - - return norm; - } - - /** - * value between min and max to normalized value between -1 and 1 - * - * @param value {number} - * @returns {number} - */ - toNormalized(value: number): number { - return ((value - this.min) / (this.max - this.min)) * 2 - 1; - } - - /** - * normlized value to value between min and max - * - * @param value {number} - * @returns {number} - */ - toMinMax(value: number): number { - return (value + 1) * 0.5 * (this.max - this.min) + this.min; - } - - private checkParameterInput(value: NoiseParameterInput): UberNoise | number { - if (typeof value === "object" && !(value instanceof UberNoise)) { - if (value.seed === undefined) { - value.seed = this.pngr(); - } - value = new UberNoise(value); - } - return value; - } - - private getParameter(value: NoiseParameter): number { - if (typeof value === "number") { - return value; - } - return value.get(this.position); - } - - // getter and setter for min and max - get min(): number { - return this.getParameter(this._min); - } - set min(value: NoiseParameterInput) { - this._min = this.checkParameterInput(value); - } - get max(): number { - return this.getParameter(this._max); - } - set max(value: NoiseParameterInput) { - this._max = this.checkParameterInput(value); - } - - // getter and setter for scale and power - get scale(): number { - return this.getParameter(this._scale); - } - set scale(value: NoiseParameterInput) { - this._scale = this.checkParameterInput(value); - } - get power(): number { - return this.getParameter(this._power); - } - set power(value: NoiseParameterInput) { - this._power = this.checkParameterInput(value); - } - - // getter and setter for gain and lacunarity - get gain(): number { - return this.getParameter(this._gain); - } - set gain(value: NoiseParameterInput) { - this._gain = this.checkParameterInput(value); - } - get lacunarity(): number { - return this.getParameter(this._lacunarity); - } - set lacunarity(value: NoiseParameterInput) { - this._lacunarity = this.checkParameterInput(value); - } - - // getter and setter for erosion, sharpness and steps - get erosion(): number { - return this.getParameter(this._erosion); - } - set erosion(value: NoiseParameterInput) { - this._erosion = this.checkParameterInput(value); - } - get sharpness(): number { - return this.getParameter(this._sharpness); - } - set sharpness(value: NoiseParameterInput) { - this._sharpness = this.checkParameterInput(value); - } - get steps(): number { - return Math.round(this.getParameter(this._steps)); - } - set steps(value: NoiseParameterInput) { - this._steps = this.checkParameterInput(value); - } - - // getter and setter for warp, warpNoise, warp2 and warpNoise2 - get warp(): number { - return this.getParameter(this._warp); - } - set warp(value: NoiseParameterInput) { - this._warp = this.checkParameterInput(value); - } - get warpNoise(): UberNoise | undefined { - return this._warpNoise; - } - set warpNoise(value: UberNoise | UberNoiseOptions | undefined) { - if (value == undefined) { - this._warpNoise = undefined; - return; - } - - const processed = this.checkParameterInput(value); - if (processed instanceof UberNoise) { - this._warpNoise = processed; - } - } - get warp2(): number { - return this.getParameter(this._warp2); - } - set warp2(value: NoiseParameterInput) { - this._warp2 = this.checkParameterInput(value); - } - get warpNoise2(): UberNoise | undefined { - return this._warpNoise2; - } - set warpNoise2(value: UberNoise | UberNoiseOptions | undefined) { - if (value == undefined) { - this._warpNoise2 = undefined; - return; - } - - const processed = this.checkParameterInput(value); - if (processed instanceof UberNoise) { - this._warpNoise2 = processed; - } - } -} - -export default UberNoise; diff --git a/src/script.ts b/src/script.ts index ed11430..3b4f1bf 100644 --- a/src/script.ts +++ b/src/script.ts @@ -1,6 +1,7 @@ import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { Planet } from "./worlds/planet"; +import { Stars } from "./worlds/stars"; const presets = ["beach", "forest", "snowForest"]; @@ -13,7 +14,7 @@ if (!canvas) { throw new Error("Canvas not found"); } const scene = new THREE.Scene(); -const camera = new THREE.PerspectiveCamera(70, width / height, 0.01, 10); +const camera = new THREE.PerspectiveCamera(70, width / height, 0.01, 30); camera.position.set(0, 0, 2.5); const renderer = new THREE.WebGLRenderer({ @@ -23,7 +24,6 @@ const renderer = new THREE.WebGLRenderer({ }); renderer.toneMapping = THREE.ACESFilmicToneMapping; - renderer.shadowMap.enabled = true; const _ = new OrbitControls(camera, renderer.domElement); @@ -58,7 +58,6 @@ light.shadow.camera.left = -2; const ambientLight = new THREE.AmbientLight(0xffffff, 0.1); scene.add(ambientLight); - let total = 0; let lastDelta = 0; renderer.setAnimationLoop((delta) => { @@ -78,6 +77,10 @@ renderer.setAnimationLoop((delta) => { } }); +let stars = new Stars(); + +scene.add(stars); + // add keydown event listener document.addEventListener("keydown", (event) => { if (event.key === "1") { diff --git a/src/worlds/biome.ts b/src/worlds/biome.ts index 81def71..10b723a 100644 --- a/src/worlds/biome.ts +++ b/src/worlds/biome.ts @@ -1,5 +1,5 @@ import { UberNoise, type NoiseOptions } from "uber-noise"; -import { Color, Vector3 } from "three"; +import { Color, ColorRepresentation, Vector3 } from "three"; import { ColorGradient, @@ -10,7 +10,7 @@ import { Octree } from "./helper/octree"; export type VegetationItem = { name: string; - density: number; + density?: number; minimumHeight?: number; maximumHeight?: number; @@ -21,6 +21,14 @@ export type VegetationItem = { minimumDistance?: number; maximumDistance?: number; colors?: Record; + + ground?: { + raise?: number; + color?: ColorRepresentation; + radius?: number; + + noise?: NoiseOptions; + }; }; export type BiomeOptions = { @@ -37,9 +45,8 @@ export type BiomeOptions = { tintColor?: number; vegetation?: { - defaults?: { - density?: number; - }; + defaults?: Omit, "name">; + items: VegetationItem[]; }; }; @@ -143,7 +150,10 @@ export class Biome { this.vegetationPositions.insert(position, item); } - itemsAround(position: Vector3, radius: number): Vector3[] { + itemsAround( + position: Vector3, + radius: number, + ): (Vector3 & { data: VegetationItem })[] { return this.vegetationPositions.query(position, radius); } } diff --git a/src/worlds/helper/octree.ts b/src/worlds/helper/octree.ts index 56af33b..92e5aac 100644 --- a/src/worlds/helper/octree.ts +++ b/src/worlds/helper/octree.ts @@ -113,16 +113,20 @@ export class Octree { // returns array of points where // distance between pos and point is less than dist - query(pos: Vector3Data, dist = 1) { - const points = this.queryXYZ(pos.x, pos.y, pos.z, dist); - for (let i = points.length - 1; i >= 0; i--) { - if (points[i].distanceTo(pos) > dist) points.splice(i, 1); - } - return points; + query(pos: Vector3Data, dist = 1): Vector3Data[] { + const points = this.queryBoxXYZ(pos.x, pos.y, pos.z, dist); + + return points.filter((p) => p.distanceTo(pos) < dist); + } + + // vector3 free version, returns points around xyz + queryXYZ(x: number, y: number, z: number, dist: number) { + const point = new Vector3(x, y, z); + + return this.query(point, dist); } - // vector3 free version, returns points in box around xyz - queryXYZ(x: number, y: number, z: number, s: number) { + queryBoxXYZ(x: number, y: number, z: number, s: number) { const min = new Vector3(x - s, y - s, z - s), max = new Vector3(x + s, y + s, z + s); const box = new Box3(min, max); @@ -235,7 +239,6 @@ export class Octree { q = this.query(opts.p, opts.min); for (const point of q) { - // @ts-expect-error - close is not a property of Vector3 point.close = true; } } diff --git a/src/worlds/presets.ts b/src/worlds/presets.ts index bcf151a..2148e4e 100644 --- a/src/worlds/presets.ts +++ b/src/worlds/presets.ts @@ -185,8 +185,8 @@ const snowForest: BiomeOptions = { [-0.1, 0xaaccff], ], seaNoise: { - min: -0.005, - max: 0.005, + min: -0.0, + max: 0.001, scale: 5, }, diff --git a/src/worlds/stars.ts b/src/worlds/stars.ts new file mode 100644 index 0000000..5f62aec --- /dev/null +++ b/src/worlds/stars.ts @@ -0,0 +1,77 @@ +import * as THREE from "three"; + +export type StarsOptions = { + particleCount: number; + minimumDistance: number; + maximumDistance: number; +}; + +export class Stars extends THREE.Group { + private particleCount: number = 5000; + + private minimumDistance: number = 10; + private maximumDistance: number = 20; + + private positions: Float32Array; + private colors: Float32Array; + private geometry: THREE.BufferGeometry; + private material: THREE.PointsMaterial; + private particles: THREE.Points; + + private tempVector = new THREE.Vector3(); + + constructor(opts: Partial = {}) { + super(); + + this.particleCount = opts.particleCount ?? this.particleCount; + this.minimumDistance = opts.minimumDistance ?? this.minimumDistance; + this.maximumDistance = opts.maximumDistance ?? this.maximumDistance; + + this.positions = new Float32Array(this.particleCount * 3); + this.colors = new Float32Array(this.particleCount * 3); + + this.setupParticles(); + } + + private setupParticles() { + for (let i = 0; i < this.particleCount; i++) { + this.resetParticle(i); + } + + this.geometry = new THREE.BufferGeometry(); + this.geometry.setAttribute( + "position", + new THREE.BufferAttribute(this.positions, 3), + ); + this.geometry.setAttribute( + "color", + new THREE.BufferAttribute(this.colors, 3), + ); + + this.material = new THREE.PointsMaterial({ + size: 0.04, + vertexColors: true, + }); + + this.particles = new THREE.Points(this.geometry, this.material); + + this.add(this.particles); + } + + private resetParticle(i: number) { + const index = i * 3; + + const distance = + Math.random() * (this.maximumDistance - this.minimumDistance) + + this.minimumDistance; + this.tempVector.randomDirection().multiplyScalar(distance); + + this.positions[index] = this.tempVector.x; + this.positions[index + 1] = this.tempVector.y; + this.positions[index + 2] = this.tempVector.z; + + this.colors[index] = Math.random() < 0.2 ? 0.3 : 1.0; + this.colors[index + 1] = Math.random() < 0.2 ? 0.3 : 1.0; + this.colors[index + 2] = Math.random() < 0.2 ? 0.3 : 1.0; + } +} diff --git a/src/worlds/worker.ts b/src/worlds/worker.ts index 43c0fa6..11ebc1d 100644 --- a/src/worlds/worker.ts +++ b/src/worlds/worker.ts @@ -6,7 +6,7 @@ import { Color, } from "three"; -import { Biome, type BiomeOptions } from "./biome"; +import { Biome } from "./biome"; import { type PlanetOptions } from "./planet"; import UberNoise from "uber-noise"; @@ -261,7 +261,7 @@ function createGeometry( j++ ) { const vegetation = biome.options.vegetation.items[j]; - if (Math.random() < faceSize * vegetation.density) { + if (Math.random() < faceSize * (vegetation.density ?? 1)) { // discard if point is below or above height limits if ( vegetation.minimumHeight !== undefined && @@ -350,9 +350,6 @@ function createGeometry( oceanMorphNormals.push(temp.x, temp.y, temp.z); } - console.log("normHeightMax", normHeightMax); - console.log("normHeightMin", normHeightMin); - const maxDist = 0.14; // go through all vertices again and update height and color based on vegetation for (let i = 0; i < vertices.count; i += 3) { From 46d0b9a6e82d26f36380341e7bccc9fd5d5f02d6 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:02:24 +0200 Subject: [PATCH 3/4] some cleanup --- features.md | 3 +- index.html | 15 +- src/script.ts | 9 +- src/worlds/biome.ts | 25 ++- src/worlds/helper/helper.ts | 28 ++++ src/worlds/helper/octree.ts | 18 +-- src/worlds/materials/OceanCausticsMaterial.ts | 121 ++++++++------- src/worlds/materials/noise.ts | 1 - src/worlds/planet.ts | 144 +++++++++--------- src/worlds/presets.ts | 46 +++++- src/worlds/worker.ts | 129 +++++++++------- 11 files changed, 328 insertions(+), 211 deletions(-) create mode 100644 src/worlds/helper/helper.ts diff --git a/features.md b/features.md index c9521dd..f0a9a09 100644 --- a/features.md +++ b/features.md @@ -12,6 +12,7 @@ - [ ] different materials - [ ] make vegetation sway in the wind - [ ] make all random stuff dependent on seed +- [ ] instanced vegetation ### weather - [ ] clouds @@ -45,4 +46,4 @@ - [ ] tilt shift ## other -- [ ] \ No newline at end of file +- [ ] walking on planet \ No newline at end of file diff --git a/index.html b/index.html index d868700..537b895 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,24 @@ + - three.js scaffold + + + tiny planets + + @@ -21,6 +32,8 @@ + made by flo-bit + diff --git a/src/script.ts b/src/script.ts index 3b4f1bf..a34c36e 100644 --- a/src/script.ts +++ b/src/script.ts @@ -2,6 +2,7 @@ import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { Planet } from "./worlds/planet"; import { Stars } from "./worlds/stars"; +import { planetPresets } from "./worlds/presets"; const presets = ["beach", "forest", "snowForest"]; @@ -36,7 +37,7 @@ let sphereMaterial = new THREE.MeshStandardMaterial({ wireframe: true, wireframeLinewidth: 10, }); -let planetMesh = new THREE.Mesh(sphereGeometry, sphereMaterial); +let planetMesh: THREE.Mesh = new THREE.Mesh(sphereGeometry, sphereMaterial); scene.add(planetMesh); const light = new THREE.DirectionalLight(); @@ -110,12 +111,16 @@ async function createPlanet(preset: string | undefined = undefined) { console.time("planet"); const planet = new Planet({ detail: 50, - biome: { preset }, + ...planetPresets[preset], }); let mesh = await planet.create(); scene.remove(planetMesh); scene.add(mesh); planetMesh = mesh; + + // planetMesh.add(camera); + // planet.updatePosition(camera, new THREE.Vector3(0, 0, 1.1)); + console.timeEnd("planet"); } diff --git a/src/worlds/biome.ts b/src/worlds/biome.ts index 10b723a..a5ee007 100644 --- a/src/worlds/biome.ts +++ b/src/worlds/biome.ts @@ -60,7 +60,7 @@ export class Biome { options: BiomeOptions; - vegetationPositions: Octree = new Octree(); + vegetationPositions: Octree = new Octree(); constructor(opts: BiomeOptions = {}) { if (opts.preset) { @@ -150,10 +150,31 @@ export class Biome { this.vegetationPositions.insert(position, item); } + closestVegetationDistance( + position: Vector3, + radius: number, + ): number | undefined { + const items = this.vegetationPositions.queryBoxXYZ( + position.x, + position.y, + position.z, + radius, + ); + if (items.length === 0) return undefined; + + let closest = Infinity; + for (const item of items) { + const distance = position.distanceTo(item); + if (distance < closest) closest = distance; + } + + return closest < radius ? closest : undefined; + } + itemsAround( position: Vector3, radius: number, - ): (Vector3 & { data: VegetationItem })[] { + ): (Vector3 & { data?: VegetationItem })[] { return this.vegetationPositions.query(position, radius); } } diff --git a/src/worlds/helper/helper.ts b/src/worlds/helper/helper.ts new file mode 100644 index 0000000..c4ad9ea --- /dev/null +++ b/src/worlds/helper/helper.ts @@ -0,0 +1,28 @@ +import { BufferGeometry, Float32BufferAttribute } from "three"; + +export function createBufferGeometry( + positions: number[], + colors?: number[], + normals?: number[], +) { + const geometry = new BufferGeometry(); + geometry.setAttribute( + "position", + new Float32BufferAttribute(new Float32Array(positions), 3), + ); + + if (colors) { + geometry.setAttribute( + "color", + new Float32BufferAttribute(new Float32Array(colors), 3), + ); + } + if (normals) { + geometry.setAttribute( + "normal", + new Float32BufferAttribute(new Float32Array(normals), 3), + ); + } + + return geometry; +} diff --git a/src/worlds/helper/octree.ts b/src/worlds/helper/octree.ts index 92e5aac..542ae35 100644 --- a/src/worlds/helper/octree.ts +++ b/src/worlds/helper/octree.ts @@ -25,12 +25,10 @@ export type OctreeOptions = { capacity?: number; }; -export type Vector3Data = Vector3 & { data?: unknown }; - -export class Octree { +export class Octree { boundary: Box3; - points: Vector3[]; + points: (Vector3 & { data?: T })[]; capacity: number; @@ -113,7 +111,7 @@ export class Octree { // returns array of points where // distance between pos and point is less than dist - query(pos: Vector3Data, dist = 1): Vector3Data[] { + query(pos: Vector3 & { data?: T }, dist = 1): (Vector3 & { data?: T })[] { const points = this.queryBoxXYZ(pos.x, pos.y, pos.z, dist); return points.filter((p) => p.distanceTo(pos) < dist); @@ -134,7 +132,7 @@ export class Octree { return this.queryBox(box); } - queryBox(box: Box3, found: Vector3Data[] = []) { + queryBox(box: Box3, found: (Vector3 & { data?: T })[] = []) { found ??= []; if (!box.intersectsBox(this.boundary)) return found; @@ -156,16 +154,16 @@ export class Octree { } // insert point with optional data (sets vec.data = data) - insert(pos: Vector3Data, data: unknown = undefined) { + insert(pos: Vector3 & { data?: T }, data: T | undefined = undefined) { return this.insertPoint(pos, data); } // vector3 free version - insertXYZ(x: number, y: number, z: number, data: unknown = undefined) { + insertXYZ(x: number, y: number, z: number, data: T | undefined = undefined) { return this.insertPoint(new Vector3(x, y, z), data); } - insertPoint(p: Vector3, data: unknown = undefined) { + insertPoint(p: Vector3, data: T | undefined = undefined) { p = p.clone(); // @ts-expect-error - data is not a property of Vector3 @@ -281,7 +279,7 @@ export class Octree { return boxes; } - all(arr: Vector3Data[] = []) { + all(arr: (Vector3 & { data?: T })[] = []) { arr ??= []; for (const p of this.points) { arr.push(p); diff --git a/src/worlds/materials/OceanCausticsMaterial.ts b/src/worlds/materials/OceanCausticsMaterial.ts index c5aa7c1..7b28907 100644 --- a/src/worlds/materials/OceanCausticsMaterial.ts +++ b/src/worlds/materials/OceanCausticsMaterial.ts @@ -1,64 +1,73 @@ -import { MeshStandardMaterial } from "three"; +import { + MeshStandardMaterial, + type MeshStandardMaterialParameters, +} from "three"; import { noise } from "./noise"; -const oceansCausticMaterial = new MeshStandardMaterial({ - vertexColors: true, -}); -oceansCausticMaterial.onBeforeCompile = (shader) => { - const caustics = ` -float caustics(vec4 vPos) { - // More intricate warping for marble patterns - // float warpFactor = 2.0; - // vec4 warpedPos = vPos * warpFactor + snoise(vPos * warpFactor * 0.5); - // vec4 warpedPos2 = warpedPos * warpFactor * 0.3 + snoise(warpedPos * warpFactor * 0.5 + vec4(0, 2, 4, 8)) + vPos; +export class PlanetMaterialWithCaustics extends MeshStandardMaterial { + constructor(parameters: MeshStandardMaterialParameters) { + super(parameters); - // // Modulate the color intensity based on the noise - // float vein = snoise(warpedPos2 * warpFactor) * snoise(warpedPos); + this.onBeforeCompile = (shader) => { + const caustics = ` + float caustics(vec4 vPos) { + // More intricate warping for marble patterns + // float warpFactor = 2.0; + // vec4 warpedPos = vPos * warpFactor + snoise(vPos * warpFactor * 0.5); + // vec4 warpedPos2 = warpedPos * warpFactor * 0.3 + snoise(warpedPos * warpFactor * 0.5 + vec4(0, 2, 4, 8)) + vPos; + + // // Modulate the color intensity based on the noise + // float vein = snoise(warpedPos2 * warpFactor) * snoise(warpedPos); + + // float a = 1.0 - (sin(vein * 12.0) + 1.0) * 0.5; + // float diff = snoise(vPos * warpFactor); + // diff = diff * snoise(diff * vPos) * a; + // return vec3((diff)); + + vec4 warpedPos = vPos * 2.0 + snoise(vPos * 3.0); + vec4 warpedPos2 = warpedPos * 0.3 + snoise(warpedPos * 2.0 + vec4(0, 2, 4, 8)) + vPos; + float vein = snoise(warpedPos2) * snoise(warpedPos); + float a = 1.0 - (sin(vein * 2.0) + 1.0) * 0.5; + + return snoise(vPos + warpedPos + warpedPos2) * a * 1.5; + }`; + shader.vertexShader = + `varying vec3 vPos;\n${shader.vertexShader}`.replace( + `#include `, + `#include \nvPos = position;`, + ); - // float a = 1.0 - (sin(vein * 12.0) + 1.0) * 0.5; - // float diff = snoise(vPos * warpFactor); - // diff = diff * snoise(diff * vPos) * a; - // return vec3((diff)); + shader.fragmentShader = ` + uniform float time; + varying vec3 vPos; + ${noise} + ${caustics} + ${shader.fragmentShader}`; - vec4 warpedPos = vPos * 2.0 + snoise(vPos * 3.0); - vec4 warpedPos2 = warpedPos * 0.3 + snoise(warpedPos * 2.0 + vec4(0, 2, 4, 8)) + vPos; -float vein = snoise(warpedPos2) * snoise(warpedPos); -float a = 1.0 - (sin(vein * 2.0) + 1.0) * 0.5; + shader.fragmentShader = shader.fragmentShader.replace( + "#include ", + `#include + vec3 pos = vPos * 3.0; + float len = length(vPos); + // Fade in + float fadeIn = smoothstep(0.96, 0.985, len); + // Fade out + float fadeOut = 1.0 - smoothstep(0.994, 0.999, len); + float causticIntensity = fadeIn * fadeOut * 0.7; + diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), causticIntensity * smoothstep(0.0, 1.0, caustics(vec4(pos, time * 0.05)))); + `, + ); - return snoise(vPos + warpedPos + warpedPos2) * a * 1.5; -}`; - shader.vertexShader = `varying vec3 vPos;\n${shader.vertexShader}`.replace( - `#include `, - `#include \nvPos = position;`, - ); + shader.uniforms.time = { value: 0 }; + this.userData.shader = shader; + }; + } - shader.fragmentShader = ` - uniform float time; - varying vec3 vPos; - ${noise} - ${caustics} - ${shader.fragmentShader}`; + update() { + if (this.userData.shader?.uniforms?.time) { + this.userData.shader.uniforms.time.value = performance.now() / 1000; + } + } +} - shader.fragmentShader = shader.fragmentShader.replace( - "#include ", - `#include - vec3 pos = vPos * 3.0; - float len = length(vPos); - // Fade in - float fadeIn = smoothstep(0.96, 0.985, len); - // Fade out - float fadeOut = 1.0 - smoothstep(0.994, 0.999, len); - float causticIntensity = fadeIn * fadeOut * 0.7; - diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), causticIntensity * smoothstep(0.0, 1.0, caustics(vec4(pos, time * 0.05)))); -`, - ); - - shader.uniforms.time = { value: 0 }; - oceansCausticMaterial.userData.shader = shader; - - // console.log("FRAGMENT", shader.fragmentShader); - // console.log(); - // console.log("VERTEX", shader.vertexShader); -}; - -export default oceansCausticMaterial; +export default PlanetMaterialWithCaustics; diff --git a/src/worlds/materials/noise.ts b/src/worlds/materials/noise.ts index 9f97be5..d0ecf22 100644 --- a/src/worlds/materials/noise.ts +++ b/src/worlds/materials/noise.ts @@ -118,5 +118,4 @@ float snoise(vec4 v) { m0 = m0 * m0; m1 = m1 * m1; return 49.0 * (dot(m0 * m0, vec3(dot(p0, x0), dot(p1, x1), dot(p2, x2))) + dot(m1 * m1, vec2(dot(p3, x3), dot(p4, x4)))); - }`; diff --git a/src/worlds/planet.ts b/src/worlds/planet.ts index e637083..9505f3e 100644 --- a/src/worlds/planet.ts +++ b/src/worlds/planet.ts @@ -11,8 +11,9 @@ import { import { Biome, type BiomeOptions } from "./biome"; import { loadModels } from "./models"; -import oceansCausticMaterial from "./materials/OceanCausticsMaterial"; +import { PlanetMaterialWithCaustics } from "./materials/OceanCausticsMaterial"; import { createAtmosphereMaterial } from "./materials/AtmosphereMaterial"; +import { createBufferGeometry } from "./helper/helper"; export type PlanetOptions = { scatter?: number; @@ -22,10 +23,13 @@ export type PlanetOptions = { detail?: number; atmosphere?: { + enabled?: boolean; color?: Vector3; height?: number; }; + material?: "normal" | "caustics"; + biome?: BiomeOptions; }; @@ -73,7 +77,7 @@ export class Planet { requestId: number; }; }) { - const { type, data, requestId } = event.data; + const { data, requestId } = event.data; const callback = this.callbacks[requestId]; if (!callback) { @@ -81,50 +85,38 @@ export class Planet { return; } - if (type === "geometry") { - const geometry = new BufferGeometry(); - const oceanGeometry = new BufferGeometry(); - geometry.setAttribute( - "position", - new Float32BufferAttribute(new Float32Array(data.positions), 3), - ); - geometry.setAttribute( - "color", - new Float32BufferAttribute(new Float32Array(data.colors), 3), - ); - geometry.setAttribute( - "normal", - new Float32BufferAttribute(new Float32Array(data.normals), 3), - ); - - oceanGeometry.setAttribute( - "position", - new Float32BufferAttribute(new Float32Array(data.oceanPositions), 3), - ); - oceanGeometry.setAttribute( - "color", - new Float32BufferAttribute(new Float32Array(data.oceanColors), 3), - ); - oceanGeometry.setAttribute( - "normal", - new Float32BufferAttribute(new Float32Array(data.oceanNormals), 3), - ); - // set morph targets - oceanGeometry.morphAttributes.position = [ - new Float32BufferAttribute( - new Float32Array(data.oceanMorphPositions), - 3, - ), - ]; - oceanGeometry.morphAttributes.normal = [ - new Float32BufferAttribute(new Float32Array(data.oceanMorphNormals), 3), - ]; - - this.vegetationPositions = data.vegetation; - - const planetMesh = new Mesh(geometry, oceansCausticMaterial); - planetMesh.castShadow = true; + const geometry = createBufferGeometry( + data.positions, + data.colors, + data.normals, + ); + + const oceanGeometry = createBufferGeometry( + data.oceanPositions, + data.oceanColors, + data.oceanNormals, + ); + + oceanGeometry.morphAttributes.position = [ + new Float32BufferAttribute(data.oceanMorphPositions, 3), + ]; + oceanGeometry.morphAttributes.normal = [ + new Float32BufferAttribute(data.oceanMorphNormals, 3), + ]; + this.vegetationPositions = data.vegetation; + + const materialOptions = { vertexColors: true }; + + const material = + this.options.material === "caustics" + ? new PlanetMaterialWithCaustics(materialOptions) + : new MeshStandardMaterial(materialOptions); + + const planetMesh = new Mesh(geometry, material); + planetMesh.castShadow = true; + + if (this.options.material === "caustics") { planetMesh.onBeforeRender = ( renderer, scene, @@ -132,41 +124,41 @@ export class Planet { geometry, material, ) => { - if (material.userData.shader?.uniforms?.time) { - material.userData.shader.uniforms.time.value = - performance.now() / 1000; + if (material instanceof PlanetMaterialWithCaustics) { + material.update(); } - //material.userData.shader.uniforms.time.value = performance.now() / 1000; }; + } - const oceanMesh = new Mesh( - oceanGeometry, - new MeshStandardMaterial({ - vertexColors: true, - transparent: true, - opacity: 0.7, - metalness: 0.5, - roughness: 0.5, - }), - ); - - planetMesh.add(oceanMesh); - oceanMesh.onBeforeRender = ( - renderer, - scene, - camera, - geometry, - material, - ) => { - // update morph targets - if (oceanMesh.morphTargetInfluences) - oceanMesh.morphTargetInfluences[0] = - Math.sin(performance.now() / 1000) * 0.5 + 0.5; - }; + const oceanMesh = new Mesh( + oceanGeometry, + new MeshStandardMaterial({ + vertexColors: true, + transparent: true, + opacity: 0.7, + metalness: 0.5, + roughness: 0.5, + }), + ); + + planetMesh.add(oceanMesh); + oceanMesh.onBeforeRender = ( + renderer, + scene, + camera, + geometry, + material, + ) => { + // update morph targets + if (oceanMesh.morphTargetInfluences) + oceanMesh.morphTargetInfluences[0] = + Math.sin(performance.now() / 1000) * 0.5 + 0.5; + }; + if (this.options.atmosphere?.enabled !== false) { this.addAtmosphere(planetMesh); - callback(planetMesh); } + callback(planetMesh); delete this.callbacks[requestId]; } @@ -193,7 +185,7 @@ export class Planet { const planet = await planetPromise; for (let i = 0; i < loaded.length - 1; i++) { - const models = await loaded[i]; + const models = (await loaded[i]) as Object3D[]; const name = models[0].userData.name; const positions = this.vegetationPositions?.[name]; @@ -214,7 +206,7 @@ export class Planet { model.traverse((child) => { if (child instanceof Mesh) { let color = item?.colors?.[child.material.name]; - if (color && color.array) { + if (color?.array) { let randomColor = color.array[Math.floor(Math.random() * color.array.length)]; child.material.color.setHex(randomColor); diff --git a/src/worlds/presets.ts b/src/worlds/presets.ts index 2148e4e..b08a4b2 100644 --- a/src/worlds/presets.ts +++ b/src/worlds/presets.ts @@ -1,6 +1,7 @@ import { type BiomeOptions } from "./biome"; +import { PlanetOptions } from "./planet"; -const beach: BiomeOptions = { +const beachBiome: BiomeOptions = { noise: { min: -0.05, max: 0.05, @@ -45,6 +46,9 @@ const beach: BiomeOptions = { Green: { array: [0x22851e, 0x22a51e] }, DarkGreen: { array: [0x006400] }, }, + ground: { + color: 0x338800, + }, }, { name: "Rock", @@ -58,7 +62,7 @@ const beach: BiomeOptions = { }, }; -const forest: BiomeOptions = { +const forestBiome: BiomeOptions = { noise: { min: -0.05, max: 0.05, @@ -154,7 +158,7 @@ const forest: BiomeOptions = { }, }; -const snowForest: BiomeOptions = { +const snowForestBiome: BiomeOptions = { noise: { min: -0.05, max: 0.05, @@ -251,7 +255,37 @@ const snowForest: BiomeOptions = { }; export const biomePresets: Record = { - beach, - forest, - snowForest, + beach: beachBiome, + forest: forestBiome, + snowForest: snowForestBiome, +}; + +const beachPlanet: PlanetOptions = { + biome: { + preset: "beach", + }, + + material: "caustics", +}; + +const forestPlanet: PlanetOptions = { + biome: { + preset: "forest", + }, + + material: "normal", +}; + +const snowForestPlanet: PlanetOptions = { + biome: { + preset: "snowForest", + }, + + material: "normal", +}; + +export const planetPresets: Record = { + beach: beachPlanet, + forest: forestPlanet, + snowForest: snowForestPlanet, }; diff --git a/src/worlds/worker.ts b/src/worlds/worker.ts index 11ebc1d..087eece 100644 --- a/src/worlds/worker.ts +++ b/src/worlds/worker.ts @@ -6,7 +6,7 @@ import { Color, } from "three"; -import { Biome } from "./biome"; +import { Biome, type VegetationItem } from "./biome"; import { type PlanetOptions } from "./planet"; import UberNoise from "uber-noise"; @@ -110,7 +110,7 @@ function createGeometry( a.fromBufferAttribute(vertices, 0); b.fromBufferAttribute(vertices, 1); - // default to scatter = distance of first edge + // scatterAmount is based on side length of face (all faces are the same size) const scatterAmount = (planetOptions.scatter ?? 1) * b.distanceTo(a); const scatterScale = 100; @@ -136,9 +136,13 @@ function createGeometry( const temp = new Vector3(); - let normHeightMax = 0; - let normHeightMin = 0; - + // go through all faces + // - calculate height and scatter for vertices + // - calculate height for ocean vertices + // - calculate height for ocean morph vertices + // - calculate color for vertices and ocean vertices + // - calculate normal for vertices and ocean vertices + // - add vegetation for (let i = 0; i < vertices.count; i += 3) { a.fromBufferAttribute(vertices, i); b.fromBufferAttribute(vertices, i + 1); @@ -153,16 +157,19 @@ function createGeometry( let normalizedHeight = 0; + // go through all vertices of the face for (let j = 0; j < 3; j++) { let v = a; if (j === 1) v = b; if (j === 2) v = c; + // lets see if we already have info for this vertex const key = `${v.x.toFixed(5)},${v.y.toFixed(5)},${v.z.toFixed(5)}`; - let move = calculatedVertices.get(key); + // if not, calculate it if (!move) { + // calculate height and scatter const height = biome.getHeight(v) + 1; const scatterX = scatterNoise.get(v); const scatterY = scatterNoise.get( @@ -175,6 +182,7 @@ function createGeometry( v.x + scatterScale * 200, v.y - scatterScale * 200, ); + // calculate sea height and sea morph height const seaHeight = biome.getSeaHeight(v) + 1; const secondSeaHeight = biome.getSeaHeight(v.addScalar(100)) + 1; @@ -189,19 +197,24 @@ function createGeometry( calculatedVertices.set(key, move); } + // we store this info for later use (vegetation placement) calculatedVerticesArray[i + j] = move; + // we add height here so we can calculate the average normalized height of the face later normalizedHeight += move.height - 1; + + // move vertex based on height and scatter v.add(move.scatter).normalize().multiplyScalar(move.height); vertices.setXYZ(i + j, v.x, v.y, v.z); + // move ocean vertex based on sea height and scatter let oceanV = oceanA; if (j === 1) oceanV = oceanB; if (j === 2) oceanV = oceanC; - oceanV.add(move.scatter).normalize().multiplyScalar(move.seaMorph); oceanMorphPositions.push(oceanV.x, oceanV.y, oceanV.z); + // move ocean morph vertex based on sea height and scatter if (j === 0) { oceanD.copy(oceanV); } else if (j === 1) { @@ -209,24 +222,20 @@ function createGeometry( } else if (j === 2) { oceanF.copy(oceanV); } - oceanV.normalize().multiplyScalar(move.seaHeight); oceanVertices.setXYZ(i + j, oceanV.x, oceanV.y, oceanV.z); } + // calculate normalized height for the face (between -1 and 1, 0 is sea level) normalizedHeight /= 3; - normalizedHeight = Math.min(-normalizedHeight / biome.min, 0) + Math.max(normalizedHeight / biome.max, 0); // now normalizedHeight should be between -1 and 1 (0 is sea level) + // this will be used for color calculation and vegetation placement - normHeightMax = Math.max(normHeightMax, normalizedHeight); - normHeightMin = Math.min(normHeightMin, normalizedHeight); - - // calculate new normal + // calculate face normal temp.crossVectors(b.clone().sub(a), c.clone().sub(a)).normalize(); - // flat shading, so all normals for the face are the same normals.setXYZ(i, temp.x, temp.y, temp.z); normals.setXYZ(i + 1, temp.x, temp.y, temp.z); @@ -236,9 +245,10 @@ function createGeometry( // (up vector = old mid point on sphere) const steepness = Math.acos(Math.abs(temp.dot(mid))); // steepness is between 0 and PI/2 + // this will be used for color calculation and vegetation placement + // calculate color for face const color = biome.getColor(mid, normalizedHeight, steepness); - // flat shading, so all colors for the face are the same if (color) { colors[i * 3] = color.r; @@ -254,6 +264,39 @@ function createGeometry( colors[i * 3 + 8] = color.b; } + // calculate ocean face color + const oceanColor = biome.getSeaColor(mid, normalizedHeight); + + if (oceanColor) { + oceanColors[i * 3] = oceanColor.r; + oceanColors[i * 3 + 1] = oceanColor.g; + oceanColors[i * 3 + 2] = oceanColor.b; + + oceanColors[i * 3 + 3] = oceanColor.r; + oceanColors[i * 3 + 4] = oceanColor.g; + oceanColors[i * 3 + 5] = oceanColor.b; + + oceanColors[i * 3 + 6] = oceanColor.r; + oceanColors[i * 3 + 7] = oceanColor.g; + oceanColors[i * 3 + 8] = oceanColor.b; + } + + // calculate ocean normals + temp + .crossVectors(oceanB.clone().sub(oceanA), oceanC.clone().sub(oceanA)) + .normalize(); + oceanNormals.setXYZ(i, temp.x, temp.y, temp.z); + oceanNormals.setXYZ(i + 1, temp.x, temp.y, temp.z); + oceanNormals.setXYZ(i + 2, temp.x, temp.y, temp.z); + + // calculate ocean morph normals + temp + .crossVectors(oceanE.clone().sub(oceanD), oceanF.clone().sub(oceanD)) + .normalize(); + oceanMorphNormals.push(temp.x, temp.y, temp.z); + oceanMorphNormals.push(temp.x, temp.y, temp.z); + oceanMorphNormals.push(temp.x, temp.y, temp.z); + // place vegetation for ( let j = 0; @@ -314,40 +357,6 @@ function createGeometry( break; } } - - // calculate ocean vertices - const oceanColor = biome.getSeaColor(mid, normalizedHeight); - - if (oceanColor) { - oceanColors[i * 3] = oceanColor.r; - oceanColors[i * 3 + 1] = oceanColor.g; - oceanColors[i * 3 + 2] = oceanColor.b; - - oceanColors[i * 3 + 3] = oceanColor.r; - oceanColors[i * 3 + 4] = oceanColor.g; - oceanColors[i * 3 + 5] = oceanColor.b; - - oceanColors[i * 3 + 6] = oceanColor.r; - oceanColors[i * 3 + 7] = oceanColor.g; - oceanColors[i * 3 + 8] = oceanColor.b; - } - - // calculate ocean normals - temp - .crossVectors(oceanB.clone().sub(oceanA), oceanC.clone().sub(oceanA)) - .normalize(); - - oceanNormals.setXYZ(i, temp.x, temp.y, temp.z); - oceanNormals.setXYZ(i + 1, temp.x, temp.y, temp.z); - oceanNormals.setXYZ(i + 2, temp.x, temp.y, temp.z); - - temp - .crossVectors(oceanE.clone().sub(oceanD), oceanF.clone().sub(oceanD)) - .normalize(); - - oceanMorphNormals.push(temp.x, temp.y, temp.z); - oceanMorphNormals.push(temp.x, temp.y, temp.z); - oceanMorphNormals.push(temp.x, temp.y, temp.z); } const maxDist = 0.14; @@ -355,6 +364,8 @@ function createGeometry( for (let i = 0; i < vertices.count; i += 3) { let found = false; let closestDistAll = 1; + let closestVegetation: VegetationItem | undefined = undefined; + for (let j = 0; j < 3; j++) { a.fromBufferAttribute(vertices, i + j); a.normalize(); @@ -380,20 +391,26 @@ function createGeometry( vertices.setXYZ(i + j, a.x, a.y, a.z); + if (closestDist < closestDistAll) { + closestVegetation = closest.data; + } + closestDistAll = Math.min(closestDist, closestDistAll); found = true; } } - if (found) { - let existingColor = new Color( - colors[i * 3], - colors[i * 3 + 1], - colors[i * 3 + 2], - ); + if (!found) continue; + + let existingColor = new Color( + colors[i * 3], + colors[i * 3 + 1], + colors[i * 3 + 2], + ); + if (closestVegetation?.ground?.color) { // set color - let newColor = new Color(0.1, 0.3, 0); + let newColor = new Color(closestVegetation.ground.color); newColor.lerp(existingColor, closestDistAll / maxDist); From 2800468d1fbe0cd543a07bcc6fe9a7fdaa2520db Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:47:58 +0200 Subject: [PATCH 4/4] move and fix ground calculation --- src/worlds/biome.ts | 85 +++++++++++++++++++++++++++++- src/worlds/presets.ts | 22 ++++---- src/worlds/types.ts | 9 ++++ src/worlds/worker.ts | 117 +++++++++++++++++------------------------- 4 files changed, 151 insertions(+), 82 deletions(-) create mode 100644 src/worlds/types.ts diff --git a/src/worlds/biome.ts b/src/worlds/biome.ts index a5ee007..166810e 100644 --- a/src/worlds/biome.ts +++ b/src/worlds/biome.ts @@ -7,6 +7,7 @@ import { } from "./helper/colorgradient"; import { biomePresets } from "./presets"; import { Octree } from "./helper/octree"; +import { type VertexInfo } from "./types"; export type VegetationItem = { name: string; @@ -65,6 +66,7 @@ export class Biome { constructor(opts: BiomeOptions = {}) { if (opts.preset) { const preset = biomePresets[opts.preset]; + if (preset) { opts = { ...preset, @@ -175,6 +177,87 @@ export class Biome { position: Vector3, radius: number, ): (Vector3 & { data?: VegetationItem })[] { - return this.vegetationPositions.query(position, radius); + return this.vegetationPositions.queryBoxXYZ( + position.x, + position.y, + position.z, + radius, + ); + } + + maxVegetationRadius(): number { + let max = 0; + for (const item of this.options.vegetation?.items ?? []) { + if (item.ground?.radius) { + max = Math.max(max, item.ground.radius); + } + } + + return max; + } + + vegetationHeightAndColorForFace( + a: Vector3, + b: Vector3, + c: Vector3, + color: Color, + sideLength: number, + ): { + heightA: number; + heightB: number; + heightC: number; + color: Color; + } { + const maxDist = this.maxVegetationRadius(); + // use a to find all vegetation items, we add sideLength so that we also find vegetation from b and c + // that otherwise would be missed, because they are too far away from a + const vegetations = this.itemsAround(a, maxDist + sideLength * 2); + + // go through a, b and c and add heights for all vegetation items that are close enough (distance is closer than item.ground.radius) + let heightA = 0; + let heightB = 0; + let heightC = 0; + + let all = [a, b, c]; + for (let j = 0; j < 3; j++) { + let p = all[j]; + + for (const vegetation of vegetations) { + if (!vegetation.data?.ground?.radius) continue; + + let distance = p.distanceTo(vegetation); + + if (distance < vegetation.data.ground?.radius) { + let amount = Math.max( + 0, + 1 - distance / vegetation.data.ground.radius, + ); + + amount = Math.pow(amount, 0.5); + + let height = vegetation.data.ground?.raise ?? 0; + height *= amount; + + if (j === 0) heightA += height; + if (j === 1) heightB += height; + if (j === 2) heightC += height; + + if (!vegetation.data.ground.color) continue; + + let newColor = new Color(vegetation.data.ground.color); + + // only lerp a third of the way, because we have three vertices + // so if all vertices are close enough, we lerp 3 times + color.lerp(newColor, amount / 3); + } + } + } + + return { + heightA, + heightB, + heightC, + color, + }; } } diff --git a/src/worlds/presets.ts b/src/worlds/presets.ts index b08a4b2..321ec77 100644 --- a/src/worlds/presets.ts +++ b/src/worlds/presets.ts @@ -37,6 +37,14 @@ const beachBiome: BiomeOptions = { vegetation: { items: [ + { + name: "Rock", + density: 50, + minimumHeight: 0.1, + colors: { + Gray: { array: [0x775544] }, + }, + }, { name: "PalmTree", density: 50, @@ -47,15 +55,9 @@ const beachBiome: BiomeOptions = { DarkGreen: { array: [0x006400] }, }, ground: { - color: 0x338800, - }, - }, - { - name: "Rock", - density: 10, - minimumHeight: 0.1, - colors: { - Gray: { array: [0x775544] }, + color: 0x229900, + radius: 0.1, + raise: 0.01, }, }, ], @@ -280,8 +282,6 @@ const snowForestPlanet: PlanetOptions = { biome: { preset: "snowForest", }, - - material: "normal", }; export const planetPresets: Record = { diff --git a/src/worlds/types.ts b/src/worlds/types.ts new file mode 100644 index 0000000..766b195 --- /dev/null +++ b/src/worlds/types.ts @@ -0,0 +1,9 @@ +import { Vector3 } from "three"; + +export type VertexInfo = { + height: number; + scatter: Vector3; + + seaHeight: number; + seaMorph: number; +}; diff --git a/src/worlds/worker.ts b/src/worlds/worker.ts index 087eece..e86050b 100644 --- a/src/worlds/worker.ts +++ b/src/worlds/worker.ts @@ -9,6 +9,7 @@ import { import { Biome, type VegetationItem } from "./biome"; import { type PlanetOptions } from "./planet"; import UberNoise from "uber-noise"; +import { type VertexInfo } from "./types"; onmessage = function (e) { const { type, data, requestId } = e.data; @@ -75,24 +76,11 @@ function createGeometry( const faceSize = (Math.PI * 4) / faceCount; console.log("faces:", faceCount); - const calculatedVertices = new Map< - string, - { - height: number; - scatter: Vector3; - - seaHeight: number; - seaMorph: number; - } - >(); - - const calculatedVerticesArray: { - height: number; - scatter: Vector3; - - seaHeight: number; - seaMorph: number; - }[] = new Array(faceCount); + // store calculated vertices so we don't have to recalculate them + // once store by hashed position (so we can find vertices of different faces that have the same position) + const calculatedVertices = new Map(); + // and once by index for vegetation placement + const calculatedVerticesArray: VertexInfo[] = new Array(faceCount); const colors = new Float32Array(vertices.count * 3); const oceanColors = new Float32Array(oceanVertices.count * 3); @@ -110,8 +98,10 @@ function createGeometry( a.fromBufferAttribute(vertices, 0); b.fromBufferAttribute(vertices, 1); - // scatterAmount is based on side length of face (all faces are the same size) - const scatterAmount = (planetOptions.scatter ?? 1) * b.distanceTo(a); + const faceSideLength = a.distanceTo(b); + + // scatterAmount is based on side length of face (all faces have the same size) + const scatterAmount = (planetOptions.scatter ?? 1.2) * faceSideLength; const scatterScale = 100; const scatterNoise = new UberNoise({ @@ -360,66 +350,53 @@ function createGeometry( } const maxDist = 0.14; - // go through all vertices again and update height and color based on vegetation - for (let i = 0; i < vertices.count; i += 3) { - let found = false; - let closestDistAll = 1; - let closestVegetation: VegetationItem | undefined = undefined; - - for (let j = 0; j < 3; j++) { - a.fromBufferAttribute(vertices, i + j); - a.normalize(); - - let p = biome.itemsAround(a, maxDist); - if (p.length > 0) { - // find closest point - let closest = p[0]; - let closestDist = a.distanceTo(closest); - for (let k = 1; k < p.length; k++) { - let dist = a.distanceTo(p[k]); - if (dist < closestDist) { - closest = p[k]; - closestDist = dist; - } - } - let moveInfo = calculatedVerticesArray[i + j]; + const color = new Color(); - a.multiplyScalar( - moveInfo.height + ((maxDist - closestDist) / maxDist) * 0.015, - ); + // go through all vertices again and update height and color based on vegetation + for (let i = 0; i < vertices.count; i += 3) { + a.fromBufferAttribute(vertices, i); + a.normalize(); + b.fromBufferAttribute(vertices, i + 1); + b.normalize(); + c.fromBufferAttribute(vertices, i + 2); + c.normalize(); - vertices.setXYZ(i + j, a.x, a.y, a.z); + color.setRGB(colors[i * 3], colors[i * 3 + 1], colors[i * 3 + 2]); - if (closestDist < closestDistAll) { - closestVegetation = closest.data; - } + const output = biome.vegetationHeightAndColorForFace( + a, + b, + c, + color, + faceSideLength, + ); - closestDistAll = Math.min(closestDist, closestDistAll); - found = true; - } - } + const moveDataA = calculatedVerticesArray[i]; + const moveDataB = calculatedVerticesArray[i + 1]; + const moveDataC = calculatedVerticesArray[i + 2]; - if (!found) continue; + // update height based on vegetation + a.normalize().multiplyScalar(moveDataA.height + output.heightA); + b.normalize().multiplyScalar(moveDataB.height + output.heightB); + c.normalize().multiplyScalar(moveDataC.height + output.heightC); - let existingColor = new Color( - colors[i * 3], - colors[i * 3 + 1], - colors[i * 3 + 2], - ); + vertices.setXYZ(i, a.x, a.y, a.z); + vertices.setXYZ(i + 1, b.x, b.y, b.z); + vertices.setXYZ(i + 2, c.x, c.y, c.z); - if (closestVegetation?.ground?.color) { - // set color - let newColor = new Color(closestVegetation.ground.color); + // update color based on vegetation + colors[i * 3] = output.color.r; + colors[i * 3 + 1] = output.color.g; + colors[i * 3 + 2] = output.color.b; - newColor.lerp(existingColor, closestDistAll / maxDist); + colors[i * 3 + 3] = output.color.r; + colors[i * 3 + 4] = output.color.g; + colors[i * 3 + 5] = output.color.b; - for (let j = 0; j < 3; j++) { - colors[(i + j) * 3] = newColor.r; - colors[(i + j) * 3 + 1] = newColor.g; - colors[(i + j) * 3 + 2] = newColor.b; - } - } + colors[i * 3 + 6] = output.color.r; + colors[i * 3 + 7] = output.color.g; + colors[i * 3 + 8] = output.color.b; } oceanSphere.morphAttributes.position[0] = new Float32BufferAttribute(