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