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