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