[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.

tiny-planets / src / worlds / worker.ts
13 kB 415 lines
1import { 2 IcosahedronGeometry, 3 Vector3, 4 BufferAttribute, 5 Float32BufferAttribute, 6 Color, 7} from "three"; 8 9import { Biome, type VegetationItem } from "./biome"; 10import { type PlanetOptions } from "./planet"; 11import UberNoise from "uber-noise"; 12import { type VertexInfo } from "./types"; 13 14onmessage = function (e) { 15 const { type, data, requestId } = e.data; 16 17 if (type === "createGeometry") { 18 const [geometry, oceanGeometry, vegetation] = createGeometry(data); 19 20 const positions = geometry.getAttribute("position").array.buffer; 21 const colors = geometry.getAttribute("color").array.buffer; 22 const normals = geometry.getAttribute("normal").array.buffer; 23 24 const oceanPositions = oceanGeometry.getAttribute("position").array.buffer; 25 const oceanColors = oceanGeometry.getAttribute("color").array.buffer; 26 const oceanNormals = oceanGeometry.getAttribute("normal").array.buffer; 27 const oceanMorphPositions = 28 oceanGeometry.morphAttributes.position[0].array.buffer; 29 const oceanMorphNormals = 30 oceanGeometry.morphAttributes.normal[0].array.buffer; 31 32 postMessage( 33 { 34 type: "geometry", 35 data: { 36 positions, 37 colors, 38 normals, 39 oceanPositions, 40 oceanColors, 41 oceanNormals, 42 vegetation, 43 oceanMorphPositions, 44 oceanMorphNormals, 45 }, 46 requestId, 47 }, 48 // @ts-expect-error - hmm 49 [ 50 positions, 51 colors, 52 normals, 53 oceanPositions, 54 oceanColors, 55 oceanNormals, 56 oceanMorphPositions, 57 oceanMorphNormals, 58 ], 59 ); 60 } else { 61 console.error("Unknown message type", type); 62 } 63}; 64 65function createGeometry( 66 planetOptions: PlanetOptions, 67): [IcosahedronGeometry, IcosahedronGeometry, Record<string, Vector3[]>] { 68 const sphere = new IcosahedronGeometry(1, planetOptions.detail ?? 50); 69 const oceanSphere = new IcosahedronGeometry(1, planetOptions.detail ?? 50); 70 71 const biome = new Biome(planetOptions.biome); 72 73 const vertices = sphere.getAttribute("position"); 74 const oceanVertices = oceanSphere.getAttribute("position"); 75 const faceCount = vertices.count / 3; 76 const faceSize = (Math.PI * 4) / faceCount; 77 console.log("faces:", faceCount); 78 79 // store calculated vertices so we don't have to recalculate them 80 // once store by hashed position (so we can find vertices of different faces that have the same position) 81 const calculatedVertices = new Map<string, VertexInfo>(); 82 // and once by index for vegetation placement 83 const calculatedVerticesArray: VertexInfo[] = new Array(faceCount); 84 85 const colors = new Float32Array(vertices.count * 3); 86 const oceanColors = new Float32Array(oceanVertices.count * 3); 87 88 const normals = sphere.getAttribute("normal"); 89 const oceanNormals = oceanSphere.getAttribute("normal"); 90 91 const a = new Vector3(), 92 b = new Vector3(), 93 c = new Vector3(); 94 95 const mid = new Vector3(); 96 97 const placedVegetation: Record<string, Vector3[]> = {}; 98 a.fromBufferAttribute(vertices, 0); 99 b.fromBufferAttribute(vertices, 1); 100 101 const faceSideLength = a.distanceTo(b); 102 103 // scatterAmount is based on side length of face (all faces have the same size) 104 const scatterAmount = (planetOptions.scatter ?? 1.2) * faceSideLength; 105 const scatterScale = 100; 106 107 const scatterNoise = new UberNoise({ 108 min: -scatterAmount / 2, 109 max: scatterAmount / 2, 110 scale: scatterScale, 111 seed: 0, 112 }); 113 114 oceanSphere.morphAttributes.position = []; 115 oceanSphere.morphAttributes.normal = []; 116 117 const oceanMorphPositions: number[] = []; 118 const oceanMorphNormals: number[] = []; 119 120 const oceanA = new Vector3(), 121 oceanB = new Vector3(), 122 oceanC = new Vector3(), 123 oceanD = new Vector3(), 124 oceanE = new Vector3(), 125 oceanF = new Vector3(); 126 127 const temp = new Vector3(); 128 129 // go through all faces 130 // - calculate height and scatter for vertices 131 // - calculate height for ocean vertices 132 // - calculate height for ocean morph vertices 133 // - calculate color for vertices and ocean vertices 134 // - calculate normal for vertices and ocean vertices 135 // - add vegetation 136 for (let i = 0; i < vertices.count; i += 3) { 137 a.fromBufferAttribute(vertices, i); 138 b.fromBufferAttribute(vertices, i + 1); 139 c.fromBufferAttribute(vertices, i + 2); 140 141 oceanA.fromBufferAttribute(oceanVertices, i); 142 oceanB.fromBufferAttribute(oceanVertices, i + 1); 143 oceanC.fromBufferAttribute(oceanVertices, i + 2); 144 145 mid.set(0, 0, 0); 146 mid.addVectors(a, b).add(c).divideScalar(3); 147 148 let normalizedHeight = 0; 149 150 // go through all vertices of the face 151 for (let j = 0; j < 3; j++) { 152 let v = a; 153 if (j === 1) v = b; 154 if (j === 2) v = c; 155 156 // lets see if we already have info for this vertex 157 const key = `${v.x.toFixed(5)},${v.y.toFixed(5)},${v.z.toFixed(5)}`; 158 let move = calculatedVertices.get(key); 159 160 // if not, calculate it 161 if (!move) { 162 // calculate height and scatter 163 const height = biome.getHeight(v) + 1; 164 const scatterX = scatterNoise.get(v); 165 const scatterY = scatterNoise.get( 166 v.y + scatterScale * 100, 167 v.z - scatterScale * 100, 168 v.x + scatterScale * 100, 169 ); 170 const scatterZ = scatterNoise.get( 171 v.z - scatterScale * 200, 172 v.x + scatterScale * 200, 173 v.y - scatterScale * 200, 174 ); 175 // calculate sea height and sea morph height 176 const seaHeight = biome.getSeaHeight(v) + 1; 177 const secondSeaHeight = biome.getSeaHeight(v.addScalar(100)) + 1; 178 179 v.subScalar(100); 180 181 move = { 182 height, 183 scatter: new Vector3(scatterX, scatterY, scatterZ), 184 seaHeight, 185 seaMorph: secondSeaHeight, 186 }; 187 calculatedVertices.set(key, move); 188 } 189 190 // we store this info for later use (vegetation placement) 191 calculatedVerticesArray[i + j] = move; 192 193 // we add height here so we can calculate the average normalized height of the face later 194 normalizedHeight += move.height - 1; 195 196 // move vertex based on height and scatter 197 v.add(move.scatter).normalize().multiplyScalar(move.height); 198 vertices.setXYZ(i + j, v.x, v.y, v.z); 199 200 // move ocean vertex based on sea height and scatter 201 let oceanV = oceanA; 202 if (j === 1) oceanV = oceanB; 203 if (j === 2) oceanV = oceanC; 204 oceanV.add(move.scatter).normalize().multiplyScalar(move.seaMorph); 205 oceanMorphPositions.push(oceanV.x, oceanV.y, oceanV.z); 206 207 // move ocean morph vertex based on sea height and scatter 208 if (j === 0) { 209 oceanD.copy(oceanV); 210 } else if (j === 1) { 211 oceanE.copy(oceanV); 212 } else if (j === 2) { 213 oceanF.copy(oceanV); 214 } 215 oceanV.normalize().multiplyScalar(move.seaHeight); 216 oceanVertices.setXYZ(i + j, oceanV.x, oceanV.y, oceanV.z); 217 } 218 219 // calculate normalized height for the face (between -1 and 1, 0 is sea level) 220 normalizedHeight /= 3; 221 normalizedHeight = 222 Math.min(-normalizedHeight / biome.min, 0) + 223 Math.max(normalizedHeight / biome.max, 0); 224 // now normalizedHeight should be between -1 and 1 (0 is sea level) 225 // this will be used for color calculation and vegetation placement 226 227 // calculate face normal 228 temp.crossVectors(b.clone().sub(a), c.clone().sub(a)).normalize(); 229 // flat shading, so all normals for the face are the same 230 normals.setXYZ(i, temp.x, temp.y, temp.z); 231 normals.setXYZ(i + 1, temp.x, temp.y, temp.z); 232 normals.setXYZ(i + 2, temp.x, temp.y, temp.z); 233 234 // calculate steepness (acos of dot product of normal and up vector) 235 // (up vector = old mid point on sphere) 236 const steepness = Math.acos(Math.abs(temp.dot(mid))); 237 // steepness is between 0 and PI/2 238 // this will be used for color calculation and vegetation placement 239 240 // calculate color for face 241 const color = biome.getColor(mid, normalizedHeight, steepness); 242 // flat shading, so all colors for the face are the same 243 if (color) { 244 colors[i * 3] = color.r; 245 colors[i * 3 + 1] = color.g; 246 colors[i * 3 + 2] = color.b; 247 248 colors[i * 3 + 3] = color.r; 249 colors[i * 3 + 4] = color.g; 250 colors[i * 3 + 5] = color.b; 251 252 colors[i * 3 + 6] = color.r; 253 colors[i * 3 + 7] = color.g; 254 colors[i * 3 + 8] = color.b; 255 } 256 257 // calculate ocean face color 258 const oceanColor = biome.getSeaColor(mid, normalizedHeight); 259 260 if (oceanColor) { 261 oceanColors[i * 3] = oceanColor.r; 262 oceanColors[i * 3 + 1] = oceanColor.g; 263 oceanColors[i * 3 + 2] = oceanColor.b; 264 265 oceanColors[i * 3 + 3] = oceanColor.r; 266 oceanColors[i * 3 + 4] = oceanColor.g; 267 oceanColors[i * 3 + 5] = oceanColor.b; 268 269 oceanColors[i * 3 + 6] = oceanColor.r; 270 oceanColors[i * 3 + 7] = oceanColor.g; 271 oceanColors[i * 3 + 8] = oceanColor.b; 272 } 273 274 // calculate ocean normals 275 temp 276 .crossVectors(oceanB.clone().sub(oceanA), oceanC.clone().sub(oceanA)) 277 .normalize(); 278 oceanNormals.setXYZ(i, temp.x, temp.y, temp.z); 279 oceanNormals.setXYZ(i + 1, temp.x, temp.y, temp.z); 280 oceanNormals.setXYZ(i + 2, temp.x, temp.y, temp.z); 281 282 // calculate ocean morph normals 283 temp 284 .crossVectors(oceanE.clone().sub(oceanD), oceanF.clone().sub(oceanD)) 285 .normalize(); 286 oceanMorphNormals.push(temp.x, temp.y, temp.z); 287 oceanMorphNormals.push(temp.x, temp.y, temp.z); 288 oceanMorphNormals.push(temp.x, temp.y, temp.z); 289 290 // place vegetation 291 for ( 292 let j = 0; 293 biome.options.vegetation && j < biome.options.vegetation.items.length; 294 j++ 295 ) { 296 const vegetation = biome.options.vegetation.items[j]; 297 if (Math.random() < faceSize * (vegetation.density ?? 1)) { 298 // discard if point is below or above height limits 299 if ( 300 vegetation.minimumHeight !== undefined && 301 normalizedHeight < vegetation.minimumHeight 302 ) { 303 continue; 304 } 305 // default minimumHeight is 0 (= above sea level) 306 if (vegetation.minimumHeight === undefined && normalizedHeight < 0) { 307 continue; 308 } 309 if ( 310 vegetation.maximumHeight !== undefined && 311 normalizedHeight > vegetation.maximumHeight 312 ) { 313 continue; 314 } 315 316 // discard if point is below or above slope limits 317 if ( 318 vegetation.minimumSlope !== undefined && 319 steepness < vegetation.minimumSlope 320 ) { 321 continue; 322 } 323 if ( 324 vegetation.maximumSlope !== undefined && 325 steepness > vegetation.maximumSlope 326 ) { 327 continue; 328 } 329 330 if (!placedVegetation[vegetation.name]) { 331 placedVegetation[vegetation.name] = []; 332 } 333 let height = a.length(); 334 placedVegetation[vegetation.name].push( 335 a 336 .clone() 337 .normalize() 338 .multiplyScalar(height + 0.005), 339 ); 340 341 biome.addVegetation( 342 vegetation, 343 a.normalize(), 344 normalizedHeight, 345 steepness, 346 ); 347 break; 348 } 349 } 350 } 351 352 const maxDist = 0.14; 353 354 const color = new Color(); 355 356 // go through all vertices again and update height and color based on vegetation 357 for (let i = 0; i < vertices.count; i += 3) { 358 a.fromBufferAttribute(vertices, i); 359 a.normalize(); 360 b.fromBufferAttribute(vertices, i + 1); 361 b.normalize(); 362 c.fromBufferAttribute(vertices, i + 2); 363 c.normalize(); 364 365 color.setRGB(colors[i * 3], colors[i * 3 + 1], colors[i * 3 + 2]); 366 367 const output = biome.vegetationHeightAndColorForFace( 368 a, 369 b, 370 c, 371 color, 372 faceSideLength, 373 ); 374 375 const moveDataA = calculatedVerticesArray[i]; 376 const moveDataB = calculatedVerticesArray[i + 1]; 377 const moveDataC = calculatedVerticesArray[i + 2]; 378 379 // update height based on vegetation 380 a.normalize().multiplyScalar(moveDataA.height + output.heightA); 381 b.normalize().multiplyScalar(moveDataB.height + output.heightB); 382 c.normalize().multiplyScalar(moveDataC.height + output.heightC); 383 384 vertices.setXYZ(i, a.x, a.y, a.z); 385 vertices.setXYZ(i + 1, b.x, b.y, b.z); 386 vertices.setXYZ(i + 2, c.x, c.y, c.z); 387 388 // update color based on vegetation 389 colors[i * 3] = output.color.r; 390 colors[i * 3 + 1] = output.color.g; 391 colors[i * 3 + 2] = output.color.b; 392 393 colors[i * 3 + 3] = output.color.r; 394 colors[i * 3 + 4] = output.color.g; 395 colors[i * 3 + 5] = output.color.b; 396 397 colors[i * 3 + 6] = output.color.r; 398 colors[i * 3 + 7] = output.color.g; 399 colors[i * 3 + 8] = output.color.b; 400 } 401 402 oceanSphere.morphAttributes.position[0] = new Float32BufferAttribute( 403 oceanMorphPositions, 404 3, 405 ); 406 oceanSphere.morphAttributes.normal[0] = new Float32BufferAttribute( 407 oceanMorphNormals, 408 3, 409 ); 410 411 sphere.setAttribute("color", new BufferAttribute(colors, 3)); 412 oceanSphere.setAttribute("color", new BufferAttribute(oceanColors, 3)); 413 414 return [sphere, oceanSphere, placedVegetation]; 415}