[READ-ONLY] Mirror of https://github.com/flo-bit/tiny-planets. procedurally generated tiny planets in the browsers flo-bit.dev/tiny-planets/
low-poly planets procedural-generation threejs
0

Configure Feed

Select the types of activity you want to include in your feed.

some cleanup

+324 -207
+2 -1
features.md
··· 12 12 - [ ] different materials 13 13 - [ ] make vegetation sway in the wind 14 14 - [ ] make all random stuff dependent on seed 15 + - [ ] instanced vegetation 15 16 16 17 ### weather 17 18 - [ ] clouds ··· 45 46 - [ ] tilt shift 46 47 47 48 ## other 48 - - [ ] 49 + - [ ] walking on planet
+14 -1
index.html
··· 1 1 <!doctype html> 2 2 <html lang="en"> 3 + 3 4 <head> 4 - <title>three.js scaffold</title> 5 + <script> 6 + !function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]); 7 + posthog.init('phc_1Q4q6SdTwvStnxFWbmdOIusLc5ve0u6Fk7WpsHPoAlD',{api_host:'https://eu.i.posthog.com', person_profiles: 'identified_only', persistence: 'memory'}) 8 + </script> 9 + 10 + <title>tiny planets</title> 5 11 <meta charset="utf-8" /> 6 12 <meta 7 13 name="viewport" 8 14 content="width=device-width, initial-scale=1.0, user-scalable=no" 9 15 /> 10 16 17 + <link 18 + rel="icon" 19 + href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🌎</text></svg>" 20 + /> 21 + 11 22 <link rel="stylesheet" href="./index.css" /> 12 23 </head> 13 24 ··· 20 31 </button> 21 32 22 33 <canvas id="root"></canvas> 34 + 35 + <a class="absolute bottom-2 left-3 text-cyan-400 text-sm font-semibold hover:text-cyan-300 transition-colors duration-100" href="https://flo-bit.dev" target="_blank">made by flo-bit</a> 23 36 24 37 <script src="src/script.ts" type="module"></script> 25 38 </body>
+7 -2
src/script.ts
··· 2 2 import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; 3 3 import { Planet } from "./worlds/planet"; 4 4 import { Stars } from "./worlds/stars"; 5 + import { planetPresets } from "./worlds/presets"; 5 6 6 7 const presets = ["beach", "forest", "snowForest"]; 7 8 ··· 36 37 wireframe: true, 37 38 wireframeLinewidth: 10, 38 39 }); 39 - let planetMesh = new THREE.Mesh(sphereGeometry, sphereMaterial); 40 + let planetMesh: THREE.Mesh = new THREE.Mesh(sphereGeometry, sphereMaterial); 40 41 scene.add(planetMesh); 41 42 42 43 const light = new THREE.DirectionalLight(); ··· 110 111 console.time("planet"); 111 112 const planet = new Planet({ 112 113 detail: 50, 113 - biome: { preset }, 114 + ...planetPresets[preset], 114 115 }); 115 116 let mesh = await planet.create(); 116 117 scene.remove(planetMesh); 117 118 scene.add(mesh); 118 119 planetMesh = mesh; 120 + 121 + // planetMesh.add(camera); 122 + // planet.updatePosition(camera, new THREE.Vector3(0, 0, 1.1)); 123 + 119 124 console.timeEnd("planet"); 120 125 } 121 126
+23 -2
src/worlds/biome.ts
··· 60 60 61 61 options: BiomeOptions; 62 62 63 - vegetationPositions: Octree = new Octree(); 63 + vegetationPositions: Octree<VegetationItem> = new Octree(); 64 64 65 65 constructor(opts: BiomeOptions = {}) { 66 66 if (opts.preset) { ··· 150 150 this.vegetationPositions.insert(position, item); 151 151 } 152 152 153 + closestVegetationDistance( 154 + position: Vector3, 155 + radius: number, 156 + ): number | undefined { 157 + const items = this.vegetationPositions.queryBoxXYZ( 158 + position.x, 159 + position.y, 160 + position.z, 161 + radius, 162 + ); 163 + if (items.length === 0) return undefined; 164 + 165 + let closest = Infinity; 166 + for (const item of items) { 167 + const distance = position.distanceTo(item); 168 + if (distance < closest) closest = distance; 169 + } 170 + 171 + return closest < radius ? closest : undefined; 172 + } 173 + 153 174 itemsAround( 154 175 position: Vector3, 155 176 radius: number, 156 - ): (Vector3 & { data: VegetationItem })[] { 177 + ): (Vector3 & { data?: VegetationItem })[] { 157 178 return this.vegetationPositions.query(position, radius); 158 179 } 159 180 }
+28
src/worlds/helper/helper.ts
··· 1 + import { BufferGeometry, Float32BufferAttribute } from "three"; 2 + 3 + export function createBufferGeometry( 4 + positions: number[], 5 + colors?: number[], 6 + normals?: number[], 7 + ) { 8 + const geometry = new BufferGeometry(); 9 + geometry.setAttribute( 10 + "position", 11 + new Float32BufferAttribute(new Float32Array(positions), 3), 12 + ); 13 + 14 + if (colors) { 15 + geometry.setAttribute( 16 + "color", 17 + new Float32BufferAttribute(new Float32Array(colors), 3), 18 + ); 19 + } 20 + if (normals) { 21 + geometry.setAttribute( 22 + "normal", 23 + new Float32BufferAttribute(new Float32Array(normals), 3), 24 + ); 25 + } 26 + 27 + return geometry; 28 + }
+8 -10
src/worlds/helper/octree.ts
··· 25 25 capacity?: number; 26 26 }; 27 27 28 - export type Vector3Data = Vector3 & { data?: unknown }; 29 - 30 - export class Octree { 28 + export class Octree<T = unknown> { 31 29 boundary: Box3; 32 30 33 - points: Vector3[]; 31 + points: (Vector3 & { data?: T })[]; 34 32 35 33 capacity: number; 36 34 ··· 113 111 114 112 // returns array of points where 115 113 // distance between pos and point is less than dist 116 - query(pos: Vector3Data, dist = 1): Vector3Data[] { 114 + query(pos: Vector3 & { data?: T }, dist = 1): (Vector3 & { data?: T })[] { 117 115 const points = this.queryBoxXYZ(pos.x, pos.y, pos.z, dist); 118 116 119 117 return points.filter((p) => p.distanceTo(pos) < dist); ··· 134 132 return this.queryBox(box); 135 133 } 136 134 137 - queryBox(box: Box3, found: Vector3Data[] = []) { 135 + queryBox(box: Box3, found: (Vector3 & { data?: T })[] = []) { 138 136 found ??= []; 139 137 140 138 if (!box.intersectsBox(this.boundary)) return found; ··· 156 154 } 157 155 158 156 // insert point with optional data (sets vec.data = data) 159 - insert(pos: Vector3Data, data: unknown = undefined) { 157 + insert(pos: Vector3 & { data?: T }, data: T | undefined = undefined) { 160 158 return this.insertPoint(pos, data); 161 159 } 162 160 163 161 // vector3 free version 164 - insertXYZ(x: number, y: number, z: number, data: unknown = undefined) { 162 + insertXYZ(x: number, y: number, z: number, data: T | undefined = undefined) { 165 163 return this.insertPoint(new Vector3(x, y, z), data); 166 164 } 167 165 168 - insertPoint(p: Vector3, data: unknown = undefined) { 166 + insertPoint(p: Vector3, data: T | undefined = undefined) { 169 167 p = p.clone(); 170 168 171 169 // @ts-expect-error - data is not a property of Vector3 ··· 281 279 return boxes; 282 280 } 283 281 284 - all(arr: Vector3Data[] = []) { 282 + all(arr: (Vector3 & { data?: T })[] = []) { 285 283 arr ??= []; 286 284 for (const p of this.points) { 287 285 arr.push(p);
+65 -56
src/worlds/materials/OceanCausticsMaterial.ts
··· 1 - import { MeshStandardMaterial } from "three"; 1 + import { 2 + MeshStandardMaterial, 3 + type MeshStandardMaterialParameters, 4 + } from "three"; 2 5 import { noise } from "./noise"; 3 6 4 - const oceansCausticMaterial = new MeshStandardMaterial({ 5 - vertexColors: true, 6 - }); 7 - oceansCausticMaterial.onBeforeCompile = (shader) => { 8 - const caustics = ` 9 - float caustics(vec4 vPos) { 10 - // More intricate warping for marble patterns 11 - // float warpFactor = 2.0; 12 - // vec4 warpedPos = vPos * warpFactor + snoise(vPos * warpFactor * 0.5); 13 - // vec4 warpedPos2 = warpedPos * warpFactor * 0.3 + snoise(warpedPos * warpFactor * 0.5 + vec4(0, 2, 4, 8)) + vPos; 7 + export class PlanetMaterialWithCaustics extends MeshStandardMaterial { 8 + constructor(parameters: MeshStandardMaterialParameters) { 9 + super(parameters); 14 10 15 - // // Modulate the color intensity based on the noise 16 - // float vein = snoise(warpedPos2 * warpFactor) * snoise(warpedPos); 11 + this.onBeforeCompile = (shader) => { 12 + const caustics = ` 13 + float caustics(vec4 vPos) { 14 + // More intricate warping for marble patterns 15 + // float warpFactor = 2.0; 16 + // vec4 warpedPos = vPos * warpFactor + snoise(vPos * warpFactor * 0.5); 17 + // vec4 warpedPos2 = warpedPos * warpFactor * 0.3 + snoise(warpedPos * warpFactor * 0.5 + vec4(0, 2, 4, 8)) + vPos; 18 + 19 + // // Modulate the color intensity based on the noise 20 + // float vein = snoise(warpedPos2 * warpFactor) * snoise(warpedPos); 21 + 22 + // float a = 1.0 - (sin(vein * 12.0) + 1.0) * 0.5; 23 + // float diff = snoise(vPos * warpFactor); 24 + // diff = diff * snoise(diff * vPos) * a; 25 + // return vec3((diff)); 26 + 27 + vec4 warpedPos = vPos * 2.0 + snoise(vPos * 3.0); 28 + vec4 warpedPos2 = warpedPos * 0.3 + snoise(warpedPos * 2.0 + vec4(0, 2, 4, 8)) + vPos; 29 + float vein = snoise(warpedPos2) * snoise(warpedPos); 30 + float a = 1.0 - (sin(vein * 2.0) + 1.0) * 0.5; 31 + 32 + return snoise(vPos + warpedPos + warpedPos2) * a * 1.5; 33 + }`; 34 + shader.vertexShader = 35 + `varying vec3 vPos;\n${shader.vertexShader}`.replace( 36 + `#include <begin_vertex>`, 37 + `#include <begin_vertex>\nvPos = position;`, 38 + ); 17 39 18 - // float a = 1.0 - (sin(vein * 12.0) + 1.0) * 0.5; 19 - // float diff = snoise(vPos * warpFactor); 20 - // diff = diff * snoise(diff * vPos) * a; 21 - // return vec3((diff)); 40 + shader.fragmentShader = ` 41 + uniform float time; 42 + varying vec3 vPos; 43 + ${noise} 44 + ${caustics} 45 + ${shader.fragmentShader}`; 22 46 23 - vec4 warpedPos = vPos * 2.0 + snoise(vPos * 3.0); 24 - vec4 warpedPos2 = warpedPos * 0.3 + snoise(warpedPos * 2.0 + vec4(0, 2, 4, 8)) + vPos; 25 - float vein = snoise(warpedPos2) * snoise(warpedPos); 26 - float a = 1.0 - (sin(vein * 2.0) + 1.0) * 0.5; 27 - 28 - return snoise(vPos + warpedPos + warpedPos2) * a * 1.5; 29 - }`; 30 - shader.vertexShader = `varying vec3 vPos;\n${shader.vertexShader}`.replace( 31 - `#include <begin_vertex>`, 32 - `#include <begin_vertex>\nvPos = position;`, 33 - ); 34 - 35 - shader.fragmentShader = ` 36 - uniform float time; 37 - varying vec3 vPos; 38 - ${noise} 39 - ${caustics} 40 - ${shader.fragmentShader}`; 41 - 42 - shader.fragmentShader = shader.fragmentShader.replace( 43 - "#include <color_fragment>", 44 - `#include <color_fragment> 45 - vec3 pos = vPos * 3.0; 46 - float len = length(vPos); 47 - // Fade in 48 - float fadeIn = smoothstep(0.96, 0.985, len); 49 - // Fade out 50 - float fadeOut = 1.0 - smoothstep(0.994, 0.999, len); 51 - float causticIntensity = fadeIn * fadeOut * 0.7; 52 - diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), causticIntensity * smoothstep(0.0, 1.0, caustics(vec4(pos, time * 0.05)))); 53 - `, 54 - ); 47 + shader.fragmentShader = shader.fragmentShader.replace( 48 + "#include <color_fragment>", 49 + `#include <color_fragment> 50 + vec3 pos = vPos * 3.0; 51 + float len = length(vPos); 52 + // Fade in 53 + float fadeIn = smoothstep(0.96, 0.985, len); 54 + // Fade out 55 + float fadeOut = 1.0 - smoothstep(0.994, 0.999, len); 56 + float causticIntensity = fadeIn * fadeOut * 0.7; 57 + diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), causticIntensity * smoothstep(0.0, 1.0, caustics(vec4(pos, time * 0.05)))); 58 + `, 59 + ); 55 60 56 - shader.uniforms.time = { value: 0 }; 57 - oceansCausticMaterial.userData.shader = shader; 61 + shader.uniforms.time = { value: 0 }; 62 + this.userData.shader = shader; 63 + }; 64 + } 58 65 59 - // console.log("FRAGMENT", shader.fragmentShader); 60 - // console.log(); 61 - // console.log("VERTEX", shader.vertexShader); 62 - }; 66 + update() { 67 + if (this.userData.shader?.uniforms?.time) { 68 + this.userData.shader.uniforms.time.value = performance.now() / 1000; 69 + } 70 + } 71 + } 63 72 64 - export default oceansCausticMaterial; 73 + export default PlanetMaterialWithCaustics;
-1
src/worlds/materials/noise.ts
··· 118 118 m0 = m0 * m0; 119 119 m1 = m1 * m1; 120 120 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)))); 121 - 122 121 }`;
+64 -72
src/worlds/planet.ts
··· 11 11 import { Biome, type BiomeOptions } from "./biome"; 12 12 import { loadModels } from "./models"; 13 13 14 - import oceansCausticMaterial from "./materials/OceanCausticsMaterial"; 14 + import { PlanetMaterialWithCaustics } from "./materials/OceanCausticsMaterial"; 15 15 import { createAtmosphereMaterial } from "./materials/AtmosphereMaterial"; 16 + import { createBufferGeometry } from "./helper/helper"; 16 17 17 18 export type PlanetOptions = { 18 19 scatter?: number; ··· 22 23 detail?: number; 23 24 24 25 atmosphere?: { 26 + enabled?: boolean; 25 27 color?: Vector3; 26 28 height?: number; 27 29 }; 30 + 31 + material?: "normal" | "caustics"; 28 32 29 33 biome?: BiomeOptions; 30 34 }; ··· 73 77 requestId: number; 74 78 }; 75 79 }) { 76 - const { type, data, requestId } = event.data; 80 + const { data, requestId } = event.data; 77 81 78 82 const callback = this.callbacks[requestId]; 79 83 if (!callback) { ··· 81 85 return; 82 86 } 83 87 84 - if (type === "geometry") { 85 - const geometry = new BufferGeometry(); 86 - const oceanGeometry = new BufferGeometry(); 87 - geometry.setAttribute( 88 - "position", 89 - new Float32BufferAttribute(new Float32Array(data.positions), 3), 90 - ); 91 - geometry.setAttribute( 92 - "color", 93 - new Float32BufferAttribute(new Float32Array(data.colors), 3), 94 - ); 95 - geometry.setAttribute( 96 - "normal", 97 - new Float32BufferAttribute(new Float32Array(data.normals), 3), 98 - ); 88 + const geometry = createBufferGeometry( 89 + data.positions, 90 + data.colors, 91 + data.normals, 92 + ); 99 93 100 - oceanGeometry.setAttribute( 101 - "position", 102 - new Float32BufferAttribute(new Float32Array(data.oceanPositions), 3), 103 - ); 104 - oceanGeometry.setAttribute( 105 - "color", 106 - new Float32BufferAttribute(new Float32Array(data.oceanColors), 3), 107 - ); 108 - oceanGeometry.setAttribute( 109 - "normal", 110 - new Float32BufferAttribute(new Float32Array(data.oceanNormals), 3), 111 - ); 112 - // set morph targets 113 - oceanGeometry.morphAttributes.position = [ 114 - new Float32BufferAttribute( 115 - new Float32Array(data.oceanMorphPositions), 116 - 3, 117 - ), 118 - ]; 119 - oceanGeometry.morphAttributes.normal = [ 120 - new Float32BufferAttribute(new Float32Array(data.oceanMorphNormals), 3), 121 - ]; 94 + const oceanGeometry = createBufferGeometry( 95 + data.oceanPositions, 96 + data.oceanColors, 97 + data.oceanNormals, 98 + ); 122 99 123 - this.vegetationPositions = data.vegetation; 100 + oceanGeometry.morphAttributes.position = [ 101 + new Float32BufferAttribute(data.oceanMorphPositions, 3), 102 + ]; 103 + oceanGeometry.morphAttributes.normal = [ 104 + new Float32BufferAttribute(data.oceanMorphNormals, 3), 105 + ]; 124 106 125 - const planetMesh = new Mesh(geometry, oceansCausticMaterial); 126 - planetMesh.castShadow = true; 107 + this.vegetationPositions = data.vegetation; 127 108 109 + const materialOptions = { vertexColors: true }; 110 + 111 + const material = 112 + this.options.material === "caustics" 113 + ? new PlanetMaterialWithCaustics(materialOptions) 114 + : new MeshStandardMaterial(materialOptions); 115 + 116 + const planetMesh = new Mesh(geometry, material); 117 + planetMesh.castShadow = true; 118 + 119 + if (this.options.material === "caustics") { 128 120 planetMesh.onBeforeRender = ( 129 121 renderer, 130 122 scene, ··· 132 124 geometry, 133 125 material, 134 126 ) => { 135 - if (material.userData.shader?.uniforms?.time) { 136 - material.userData.shader.uniforms.time.value = 137 - performance.now() / 1000; 127 + if (material instanceof PlanetMaterialWithCaustics) { 128 + material.update(); 138 129 } 139 - //material.userData.shader.uniforms.time.value = performance.now() / 1000; 140 130 }; 131 + } 141 132 142 - const oceanMesh = new Mesh( 143 - oceanGeometry, 144 - new MeshStandardMaterial({ 145 - vertexColors: true, 146 - transparent: true, 147 - opacity: 0.7, 148 - metalness: 0.5, 149 - roughness: 0.5, 150 - }), 151 - ); 133 + const oceanMesh = new Mesh( 134 + oceanGeometry, 135 + new MeshStandardMaterial({ 136 + vertexColors: true, 137 + transparent: true, 138 + opacity: 0.7, 139 + metalness: 0.5, 140 + roughness: 0.5, 141 + }), 142 + ); 152 143 153 - planetMesh.add(oceanMesh); 154 - oceanMesh.onBeforeRender = ( 155 - renderer, 156 - scene, 157 - camera, 158 - geometry, 159 - material, 160 - ) => { 161 - // update morph targets 162 - if (oceanMesh.morphTargetInfluences) 163 - oceanMesh.morphTargetInfluences[0] = 164 - Math.sin(performance.now() / 1000) * 0.5 + 0.5; 165 - }; 144 + planetMesh.add(oceanMesh); 145 + oceanMesh.onBeforeRender = ( 146 + renderer, 147 + scene, 148 + camera, 149 + geometry, 150 + material, 151 + ) => { 152 + // update morph targets 153 + if (oceanMesh.morphTargetInfluences) 154 + oceanMesh.morphTargetInfluences[0] = 155 + Math.sin(performance.now() / 1000) * 0.5 + 0.5; 156 + }; 166 157 158 + if (this.options.atmosphere?.enabled !== false) { 167 159 this.addAtmosphere(planetMesh); 168 - callback(planetMesh); 169 160 } 161 + callback(planetMesh); 170 162 171 163 delete this.callbacks[requestId]; 172 164 } ··· 193 185 const planet = await planetPromise; 194 186 195 187 for (let i = 0; i < loaded.length - 1; i++) { 196 - const models = await loaded[i]; 188 + const models = (await loaded[i]) as Object3D[]; 197 189 const name = models[0].userData.name; 198 190 199 191 const positions = this.vegetationPositions?.[name]; ··· 214 206 model.traverse((child) => { 215 207 if (child instanceof Mesh) { 216 208 let color = item?.colors?.[child.material.name]; 217 - if (color && color.array) { 209 + if (color?.array) { 218 210 let randomColor = 219 211 color.array[Math.floor(Math.random() * color.array.length)]; 220 212 child.material.color.setHex(randomColor);
+40 -6
src/worlds/presets.ts
··· 1 1 import { type BiomeOptions } from "./biome"; 2 + import { PlanetOptions } from "./planet"; 2 3 3 - const beach: BiomeOptions = { 4 + const beachBiome: BiomeOptions = { 4 5 noise: { 5 6 min: -0.05, 6 7 max: 0.05, ··· 44 45 Brown: { array: [0x8b4513, 0x5b3105] }, 45 46 Green: { array: [0x22851e, 0x22a51e] }, 46 47 DarkGreen: { array: [0x006400] }, 48 + }, 49 + ground: { 50 + color: 0x338800, 47 51 }, 48 52 }, 49 53 { ··· 58 62 }, 59 63 }; 60 64 61 - const forest: BiomeOptions = { 65 + const forestBiome: BiomeOptions = { 62 66 noise: { 63 67 min: -0.05, 64 68 max: 0.05, ··· 154 158 }, 155 159 }; 156 160 157 - const snowForest: BiomeOptions = { 161 + const snowForestBiome: BiomeOptions = { 158 162 noise: { 159 163 min: -0.05, 160 164 max: 0.05, ··· 251 255 }; 252 256 253 257 export const biomePresets: Record<string, BiomeOptions> = { 254 - beach, 255 - forest, 256 - snowForest, 258 + beach: beachBiome, 259 + forest: forestBiome, 260 + snowForest: snowForestBiome, 261 + }; 262 + 263 + const beachPlanet: PlanetOptions = { 264 + biome: { 265 + preset: "beach", 266 + }, 267 + 268 + material: "caustics", 269 + }; 270 + 271 + const forestPlanet: PlanetOptions = { 272 + biome: { 273 + preset: "forest", 274 + }, 275 + 276 + material: "normal", 277 + }; 278 + 279 + const snowForestPlanet: PlanetOptions = { 280 + biome: { 281 + preset: "snowForest", 282 + }, 283 + 284 + material: "normal", 285 + }; 286 + 287 + export const planetPresets: Record<string, PlanetOptions> = { 288 + beach: beachPlanet, 289 + forest: forestPlanet, 290 + snowForest: snowForestPlanet, 257 291 };
+73 -56
src/worlds/worker.ts
··· 6 6 Color, 7 7 } from "three"; 8 8 9 - import { Biome } from "./biome"; 9 + import { Biome, type VegetationItem } from "./biome"; 10 10 import { type PlanetOptions } from "./planet"; 11 11 import UberNoise from "uber-noise"; 12 12 ··· 110 110 a.fromBufferAttribute(vertices, 0); 111 111 b.fromBufferAttribute(vertices, 1); 112 112 113 - // default to scatter = distance of first edge 113 + // scatterAmount is based on side length of face (all faces are the same size) 114 114 const scatterAmount = (planetOptions.scatter ?? 1) * b.distanceTo(a); 115 115 const scatterScale = 100; 116 116 ··· 136 136 137 137 const temp = new Vector3(); 138 138 139 - let normHeightMax = 0; 140 - let normHeightMin = 0; 141 - 139 + // go through all faces 140 + // - calculate height and scatter for vertices 141 + // - calculate height for ocean vertices 142 + // - calculate height for ocean morph vertices 143 + // - calculate color for vertices and ocean vertices 144 + // - calculate normal for vertices and ocean vertices 145 + // - add vegetation 142 146 for (let i = 0; i < vertices.count; i += 3) { 143 147 a.fromBufferAttribute(vertices, i); 144 148 b.fromBufferAttribute(vertices, i + 1); ··· 153 157 154 158 let normalizedHeight = 0; 155 159 160 + // go through all vertices of the face 156 161 for (let j = 0; j < 3; j++) { 157 162 let v = a; 158 163 if (j === 1) v = b; 159 164 if (j === 2) v = c; 160 165 166 + // lets see if we already have info for this vertex 161 167 const key = `${v.x.toFixed(5)},${v.y.toFixed(5)},${v.z.toFixed(5)}`; 162 - 163 168 let move = calculatedVertices.get(key); 164 169 170 + // if not, calculate it 165 171 if (!move) { 172 + // calculate height and scatter 166 173 const height = biome.getHeight(v) + 1; 167 174 const scatterX = scatterNoise.get(v); 168 175 const scatterY = scatterNoise.get( ··· 175 182 v.x + scatterScale * 200, 176 183 v.y - scatterScale * 200, 177 184 ); 185 + // calculate sea height and sea morph height 178 186 const seaHeight = biome.getSeaHeight(v) + 1; 179 187 const secondSeaHeight = biome.getSeaHeight(v.addScalar(100)) + 1; 180 188 ··· 189 197 calculatedVertices.set(key, move); 190 198 } 191 199 200 + // we store this info for later use (vegetation placement) 192 201 calculatedVerticesArray[i + j] = move; 193 202 203 + // we add height here so we can calculate the average normalized height of the face later 194 204 normalizedHeight += move.height - 1; 205 + 206 + // move vertex based on height and scatter 195 207 v.add(move.scatter).normalize().multiplyScalar(move.height); 196 208 vertices.setXYZ(i + j, v.x, v.y, v.z); 197 209 210 + // move ocean vertex based on sea height and scatter 198 211 let oceanV = oceanA; 199 212 if (j === 1) oceanV = oceanB; 200 213 if (j === 2) oceanV = oceanC; 201 - 202 214 oceanV.add(move.scatter).normalize().multiplyScalar(move.seaMorph); 203 215 oceanMorphPositions.push(oceanV.x, oceanV.y, oceanV.z); 204 216 217 + // move ocean morph vertex based on sea height and scatter 205 218 if (j === 0) { 206 219 oceanD.copy(oceanV); 207 220 } else if (j === 1) { ··· 209 222 } else if (j === 2) { 210 223 oceanF.copy(oceanV); 211 224 } 212 - 213 225 oceanV.normalize().multiplyScalar(move.seaHeight); 214 226 oceanVertices.setXYZ(i + j, oceanV.x, oceanV.y, oceanV.z); 215 227 } 216 228 229 + // calculate normalized height for the face (between -1 and 1, 0 is sea level) 217 230 normalizedHeight /= 3; 218 - 219 231 normalizedHeight = 220 232 Math.min(-normalizedHeight / biome.min, 0) + 221 233 Math.max(normalizedHeight / biome.max, 0); 222 234 // now normalizedHeight should be between -1 and 1 (0 is sea level) 235 + // this will be used for color calculation and vegetation placement 223 236 224 - normHeightMax = Math.max(normHeightMax, normalizedHeight); 225 - normHeightMin = Math.min(normHeightMin, normalizedHeight); 226 - 227 - // calculate new normal 237 + // calculate face normal 228 238 temp.crossVectors(b.clone().sub(a), c.clone().sub(a)).normalize(); 229 - 230 239 // flat shading, so all normals for the face are the same 231 240 normals.setXYZ(i, temp.x, temp.y, temp.z); 232 241 normals.setXYZ(i + 1, temp.x, temp.y, temp.z); ··· 236 245 // (up vector = old mid point on sphere) 237 246 const steepness = Math.acos(Math.abs(temp.dot(mid))); 238 247 // steepness is between 0 and PI/2 248 + // this will be used for color calculation and vegetation placement 239 249 250 + // calculate color for face 240 251 const color = biome.getColor(mid, normalizedHeight, steepness); 241 - 242 252 // flat shading, so all colors for the face are the same 243 253 if (color) { 244 254 colors[i * 3] = color.r; ··· 254 264 colors[i * 3 + 8] = color.b; 255 265 } 256 266 267 + // calculate ocean face color 268 + const oceanColor = biome.getSeaColor(mid, normalizedHeight); 269 + 270 + if (oceanColor) { 271 + oceanColors[i * 3] = oceanColor.r; 272 + oceanColors[i * 3 + 1] = oceanColor.g; 273 + oceanColors[i * 3 + 2] = oceanColor.b; 274 + 275 + oceanColors[i * 3 + 3] = oceanColor.r; 276 + oceanColors[i * 3 + 4] = oceanColor.g; 277 + oceanColors[i * 3 + 5] = oceanColor.b; 278 + 279 + oceanColors[i * 3 + 6] = oceanColor.r; 280 + oceanColors[i * 3 + 7] = oceanColor.g; 281 + oceanColors[i * 3 + 8] = oceanColor.b; 282 + } 283 + 284 + // calculate ocean normals 285 + temp 286 + .crossVectors(oceanB.clone().sub(oceanA), oceanC.clone().sub(oceanA)) 287 + .normalize(); 288 + oceanNormals.setXYZ(i, temp.x, temp.y, temp.z); 289 + oceanNormals.setXYZ(i + 1, temp.x, temp.y, temp.z); 290 + oceanNormals.setXYZ(i + 2, temp.x, temp.y, temp.z); 291 + 292 + // calculate ocean morph normals 293 + temp 294 + .crossVectors(oceanE.clone().sub(oceanD), oceanF.clone().sub(oceanD)) 295 + .normalize(); 296 + oceanMorphNormals.push(temp.x, temp.y, temp.z); 297 + oceanMorphNormals.push(temp.x, temp.y, temp.z); 298 + oceanMorphNormals.push(temp.x, temp.y, temp.z); 299 + 257 300 // place vegetation 258 301 for ( 259 302 let j = 0; ··· 314 357 break; 315 358 } 316 359 } 317 - 318 - // calculate ocean vertices 319 - const oceanColor = biome.getSeaColor(mid, normalizedHeight); 320 - 321 - if (oceanColor) { 322 - oceanColors[i * 3] = oceanColor.r; 323 - oceanColors[i * 3 + 1] = oceanColor.g; 324 - oceanColors[i * 3 + 2] = oceanColor.b; 325 - 326 - oceanColors[i * 3 + 3] = oceanColor.r; 327 - oceanColors[i * 3 + 4] = oceanColor.g; 328 - oceanColors[i * 3 + 5] = oceanColor.b; 329 - 330 - oceanColors[i * 3 + 6] = oceanColor.r; 331 - oceanColors[i * 3 + 7] = oceanColor.g; 332 - oceanColors[i * 3 + 8] = oceanColor.b; 333 - } 334 - 335 - // calculate ocean normals 336 - temp 337 - .crossVectors(oceanB.clone().sub(oceanA), oceanC.clone().sub(oceanA)) 338 - .normalize(); 339 - 340 - oceanNormals.setXYZ(i, temp.x, temp.y, temp.z); 341 - oceanNormals.setXYZ(i + 1, temp.x, temp.y, temp.z); 342 - oceanNormals.setXYZ(i + 2, temp.x, temp.y, temp.z); 343 - 344 - temp 345 - .crossVectors(oceanE.clone().sub(oceanD), oceanF.clone().sub(oceanD)) 346 - .normalize(); 347 - 348 - oceanMorphNormals.push(temp.x, temp.y, temp.z); 349 - oceanMorphNormals.push(temp.x, temp.y, temp.z); 350 - oceanMorphNormals.push(temp.x, temp.y, temp.z); 351 360 } 352 361 353 362 const maxDist = 0.14; ··· 355 364 for (let i = 0; i < vertices.count; i += 3) { 356 365 let found = false; 357 366 let closestDistAll = 1; 367 + let closestVegetation: VegetationItem | undefined = undefined; 368 + 358 369 for (let j = 0; j < 3; j++) { 359 370 a.fromBufferAttribute(vertices, i + j); 360 371 a.normalize(); ··· 379 390 ); 380 391 381 392 vertices.setXYZ(i + j, a.x, a.y, a.z); 393 + 394 + if (closestDist < closestDistAll) { 395 + closestVegetation = closest.data; 396 + } 382 397 383 398 closestDistAll = Math.min(closestDist, closestDistAll); 384 399 found = true; 385 400 } 386 401 } 387 402 388 - if (found) { 389 - let existingColor = new Color( 390 - colors[i * 3], 391 - colors[i * 3 + 1], 392 - colors[i * 3 + 2], 393 - ); 403 + if (!found) continue; 394 404 405 + let existingColor = new Color( 406 + colors[i * 3], 407 + colors[i * 3 + 1], 408 + colors[i * 3 + 2], 409 + ); 410 + 411 + if (closestVegetation?.ground?.color) { 395 412 // set color 396 - let newColor = new Color(0.1, 0.3, 0); 413 + let newColor = new Color(closestVegetation.ground.color); 397 414 398 415 newColor.lerp(existingColor, closestDistAll / maxDist); 399 416