···
61
61
62
62
let total = 0;
63
63
let lastDelta = 0;
64
64
+
65
65
+
let rotate = true;
64
66
renderer.setAnimationLoop((delta) => {
65
67
renderer.render(scene, camera);
66
68
67
67
-
planetMesh.rotation.y += 0.001;
69
69
+
if (rotate) planetMesh.rotation.y += 0.001;
68
70
69
71
if (lastDelta > 0) {
70
72
total += delta - lastDelta;
···
90
92
createPlanet("forest");
91
93
} else if (event.key === "3") {
92
94
createPlanet("snowForest");
95
95
+
}
96
96
+
97
97
+
if (event.key === " ") {
98
98
+
rotate = !rotate;
93
99
}
94
100
});
95
101
···
1
1
import { UberNoise, type NoiseOptions } from "uber-noise";
2
2
-
import { Color, ColorRepresentation, Vector3 } from "three";
2
2
+
import { Color, type ColorRepresentation, Vector3 } from "three";
3
3
4
4
import {
5
5
ColorGradient,
···
7
7
} from "./helper/colorgradient";
8
8
import { biomePresets } from "./presets";
9
9
import { Octree } from "./helper/octree";
10
10
-
import { type VertexInfo } from "./types";
11
10
12
11
export type VegetationItem = {
13
12
name: string;
···
5
5
import { noise } from "./noise";
6
6
7
7
export class PlanetMaterialWithCaustics extends MeshStandardMaterial {
8
8
-
constructor(parameters: MeshStandardMaterialParameters) {
8
8
+
constructor(
9
9
+
parameters: MeshStandardMaterialParameters & { shape: "sphere" | "plane" },
10
10
+
) {
9
11
super(parameters);
10
12
11
13
this.onBeforeCompile = (shader) => {
···
48
50
"#include <color_fragment>",
49
51
`#include <color_fragment>
50
52
vec3 pos = vPos * 3.0;
51
51
-
float len = length(vPos);
53
53
+
${parameters.shape == "plane" ? "float len = vPos.y;" : "float len = length(vPos) - 1.0;"}
52
54
// Fade in
53
53
-
float fadeIn = smoothstep(0.96, 0.985, len);
55
55
+
float fadeIn = smoothstep(-0.04, -0.015, len);
54
56
// Fade out
55
55
-
float fadeOut = 1.0 - smoothstep(0.994, 0.999, len);
57
57
+
float fadeOut = 1.0 - smoothstep(-0.006, -0.001, len);
56
58
float causticIntensity = fadeIn * fadeOut * 0.7;
57
59
diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), causticIntensity * smoothstep(0.0, 1.0, caustics(vec4(pos, time * 0.05))));
58
60
`,
···
31
31
material?: "normal" | "caustics";
32
32
33
33
biome?: BiomeOptions;
34
34
+
35
35
+
shape?: "sphere" | "plane";
34
36
};
35
37
36
38
export class Planet {
···
46
48
47
49
vegetationPositions?: Record<string, Vector3[]>;
48
50
51
51
+
shape: "sphere" | "plane" = "sphere";
52
52
+
53
53
+
tempQuaternion = new Quaternion();
54
54
+
49
55
constructor(options: PlanetOptions = {}) {
56
56
+
this.shape = options.shape ?? this.shape;
57
57
+
options.shape = this.shape;
58
58
+
50
59
this.options = options;
51
60
52
61
this.biome = new Biome(options.biome);
···
110
119
111
120
const material =
112
121
this.options.material === "caustics"
113
113
-
? new PlanetMaterialWithCaustics(materialOptions)
122
122
+
? new PlanetMaterialWithCaustics({
123
123
+
...materialOptions,
124
124
+
shape: this.shape,
125
125
+
})
114
126
: new MeshStandardMaterial(materialOptions);
115
127
116
128
const planetMesh = new Mesh(geometry, material);
···
257
269
updatePosition(item: Object3D, pos: Vector3) {
258
270
item.position.copy(pos);
259
271
260
260
-
const currentRotation = new Quaternion();
261
261
-
const a = item.up.clone().normalize();
262
262
-
const b = pos.clone().normalize();
272
272
+
if (this.shape === "sphere") {
273
273
+
const a = item.up.clone().normalize();
274
274
+
const b = pos.clone().normalize();
263
275
264
264
-
currentRotation.setFromUnitVectors(a, b);
276
276
+
this.tempQuaternion.setFromUnitVectors(a, b);
265
277
266
266
-
item.quaternion.copy(currentRotation);
278
278
+
item.quaternion.copy(this.tempQuaternion);
267
279
268
268
-
item.up = b;
280
280
+
item.up = b;
281
281
+
}
269
282
}
270
283
}
···
1
1
import { type BiomeOptions } from "./biome";
2
2
-
import { PlanetOptions } from "./planet";
2
2
+
import { type PlanetOptions } from "./planet";
3
3
4
4
const beachBiome: BiomeOptions = {
5
5
noise: {
···
4
4
BufferAttribute,
5
5
Float32BufferAttribute,
6
6
Color,
7
7
+
PlaneGeometry,
8
8
+
BufferGeometry,
7
9
} from "three";
8
10
9
9
-
import { Biome, type VegetationItem } from "./biome";
11
11
+
import { Biome } from "./biome";
10
12
import { type PlanetOptions } from "./planet";
11
13
import UberNoise from "uber-noise";
12
14
import { type VertexInfo } from "./types";
···
64
66
65
67
function createGeometry(
66
68
planetOptions: PlanetOptions,
67
67
-
): [IcosahedronGeometry, IcosahedronGeometry, Record<string, Vector3[]>] {
68
68
-
const sphere = new IcosahedronGeometry(1, planetOptions.detail ?? 50);
69
69
-
const oceanSphere = new IcosahedronGeometry(1, planetOptions.detail ?? 50);
69
69
+
): [BufferGeometry, BufferGeometry, Record<string, Vector3[]>] {
70
70
+
const detail = planetOptions.detail ?? 50;
71
71
+
72
72
+
const mainGeometry =
73
73
+
planetOptions.shape == "plane"
74
74
+
? new PlaneGeometry(3, 3, detail, detail).toNonIndexed()
75
75
+
: new IcosahedronGeometry(1, detail);
76
76
+
const oceanGeometry =
77
77
+
planetOptions.shape == "plane"
78
78
+
? new PlaneGeometry(3, 3, detail, detail).toNonIndexed()
79
79
+
: new IcosahedronGeometry(1, detail);
70
80
71
81
const biome = new Biome(planetOptions.biome);
72
82
73
73
-
const vertices = sphere.getAttribute("position");
74
74
-
const oceanVertices = oceanSphere.getAttribute("position");
83
83
+
const vertices = mainGeometry.getAttribute("position");
84
84
+
const oceanVertices = oceanGeometry.getAttribute("position");
75
85
const faceCount = vertices.count / 3;
76
86
const faceSize = (Math.PI * 4) / faceCount;
77
87
console.log("faces:", faceCount);
···
85
95
const colors = new Float32Array(vertices.count * 3);
86
96
const oceanColors = new Float32Array(oceanVertices.count * 3);
87
97
88
88
-
const normals = sphere.getAttribute("normal");
89
89
-
const oceanNormals = oceanSphere.getAttribute("normal");
98
98
+
const normals = mainGeometry.getAttribute("normal");
99
99
+
const oceanNormals = oceanGeometry.getAttribute("normal");
100
100
+
101
101
+
const planeUp = new Vector3(0, 1, 0);
90
102
91
103
const a = new Vector3(),
92
104
b = new Vector3(),
···
111
123
seed: 0,
112
124
});
113
125
114
114
-
oceanSphere.morphAttributes.position = [];
115
115
-
oceanSphere.morphAttributes.normal = [];
126
126
+
oceanGeometry.morphAttributes.position = [];
127
127
+
oceanGeometry.morphAttributes.normal = [];
116
128
117
129
const oceanMorphPositions: number[] = [];
118
130
const oceanMorphNormals: number[] = [];
···
142
154
oceanB.fromBufferAttribute(oceanVertices, i + 1);
143
155
oceanC.fromBufferAttribute(oceanVertices, i + 2);
144
156
157
157
+
if (planetOptions.shape == "plane") {
158
158
+
// switch y and z
159
159
+
let temp = a.y;
160
160
+
a.y = a.z;
161
161
+
a.z = temp;
162
162
+
163
163
+
temp = b.y;
164
164
+
b.y = b.z;
165
165
+
b.z = temp;
166
166
+
167
167
+
temp = c.y;
168
168
+
c.y = c.z;
169
169
+
c.z = temp;
170
170
+
171
171
+
temp = oceanA.y;
172
172
+
oceanA.y = oceanA.z;
173
173
+
oceanA.z = temp;
174
174
+
175
175
+
temp = oceanB.y;
176
176
+
oceanB.y = oceanB.z;
177
177
+
oceanB.z = temp;
178
178
+
179
179
+
temp = oceanC.y;
180
180
+
oceanC.y = oceanC.z;
181
181
+
oceanC.z = temp;
182
182
+
183
183
+
// switch a and c
184
184
+
let tempVector = a.clone();
185
185
+
a.copy(c);
186
186
+
c.copy(tempVector);
187
187
+
188
188
+
tempVector = oceanA.clone();
189
189
+
oceanA.copy(oceanC);
190
190
+
oceanC.copy(tempVector);
191
191
+
}
192
192
+
145
193
mid.set(0, 0, 0);
146
194
mid.addVectors(a, b).add(c).divideScalar(3);
147
195
···
160
208
// if not, calculate it
161
209
if (!move) {
162
210
// calculate height and scatter
163
163
-
const height = biome.getHeight(v) + 1;
211
211
+
const height = biome.getHeight(v);
164
212
const scatterX = scatterNoise.get(v);
165
213
const scatterY = scatterNoise.get(
166
214
v.y + scatterScale * 100,
···
173
221
v.y - scatterScale * 200,
174
222
);
175
223
// calculate sea height and sea morph height
176
176
-
const seaHeight = biome.getSeaHeight(v) + 1;
177
177
-
const secondSeaHeight = biome.getSeaHeight(v.addScalar(100)) + 1;
224
224
+
const seaHeight = biome.getSeaHeight(v);
225
225
+
const secondSeaHeight = biome.getSeaHeight(v.addScalar(100));
178
226
179
227
v.subScalar(100);
180
228
···
191
239
calculatedVerticesArray[i + j] = move;
192
240
193
241
// we add height here so we can calculate the average normalized height of the face later
194
194
-
normalizedHeight += move.height - 1;
242
242
+
normalizedHeight += move.height;
195
243
196
244
// move vertex based on height and scatter
197
197
-
v.add(move.scatter).normalize().multiplyScalar(move.height);
245
245
+
v.add(move.scatter);
246
246
+
if (planetOptions.shape == "plane") {
247
247
+
v.y = move.height;
248
248
+
} else {
249
249
+
v.normalize().multiplyScalar(move.height + 1);
250
250
+
}
251
251
+
198
252
vertices.setXYZ(i + j, v.x, v.y, v.z);
199
253
200
200
-
// move ocean vertex based on sea height and scatter
254
254
+
// move ocean morph vertex based on sea morph height and scatter
201
255
let oceanV = oceanA;
202
256
if (j === 1) oceanV = oceanB;
203
257
if (j === 2) oceanV = oceanC;
204
204
-
oceanV.add(move.scatter).normalize().multiplyScalar(move.seaMorph);
258
258
+
oceanV.add(move.scatter);
259
259
+
260
260
+
if (planetOptions.shape == "plane") {
261
261
+
oceanV.y = move.seaMorph;
262
262
+
} else {
263
263
+
oceanV.normalize().multiplyScalar(move.seaMorph + 1);
264
264
+
}
205
265
oceanMorphPositions.push(oceanV.x, oceanV.y, oceanV.z);
206
266
207
207
-
// move ocean morph vertex based on sea height and scatter
267
267
+
// move ocean vertex based on sea height and scatter
208
268
if (j === 0) {
209
269
oceanD.copy(oceanV);
270
270
+
oceanV = oceanD;
210
271
} else if (j === 1) {
211
272
oceanE.copy(oceanV);
273
273
+
oceanV = oceanE;
212
274
} else if (j === 2) {
213
275
oceanF.copy(oceanV);
276
276
+
oceanV = oceanF;
214
277
}
215
215
-
oceanV.normalize().multiplyScalar(move.seaHeight);
278
278
+
if (planetOptions.shape == "plane") {
279
279
+
oceanV.y = move.seaHeight;
280
280
+
} else {
281
281
+
oceanV.normalize().multiplyScalar(move.seaHeight + 1);
282
282
+
}
216
283
oceanVertices.setXYZ(i + j, oceanV.x, oceanV.y, oceanV.z);
217
284
}
218
285
···
233
300
234
301
// calculate steepness (acos of dot product of normal and up vector)
235
302
// (up vector = old mid point on sphere)
236
236
-
const steepness = Math.acos(Math.abs(temp.dot(mid)));
303
303
+
const steepness = Math.acos(
304
304
+
Math.abs(temp.dot(planetOptions.shape == "plane" ? planeUp : mid)),
305
305
+
);
237
306
// steepness is between 0 and PI/2
238
307
// this will be used for color calculation and vegetation placement
239
308
···
330
399
if (!placedVegetation[vegetation.name]) {
331
400
placedVegetation[vegetation.name] = [];
332
401
}
333
333
-
let height = a.length();
334
334
-
placedVegetation[vegetation.name].push(
335
335
-
a
336
336
-
.clone()
337
337
-
.normalize()
338
338
-
.multiplyScalar(height + 0.005),
339
339
-
);
402
402
+
403
403
+
placedVegetation[vegetation.name].push(a.clone());
340
404
341
341
-
biome.addVegetation(
342
342
-
vegetation,
343
343
-
a.normalize(),
344
344
-
normalizedHeight,
345
345
-
steepness,
346
346
-
);
405
405
+
if (planetOptions.shape == "plane") {
406
406
+
a.y = 0;
407
407
+
} else {
408
408
+
a.normalize();
409
409
+
}
410
410
+
411
411
+
biome.addVegetation(vegetation, a, normalizedHeight, steepness);
347
412
break;
348
413
}
349
414
}
350
415
}
351
416
352
352
-
const maxDist = 0.14;
353
353
-
354
417
const color = new Color();
355
418
356
419
// go through all vertices again and update height and color based on vegetation
357
420
for (let i = 0; i < vertices.count; i += 3) {
358
421
a.fromBufferAttribute(vertices, i);
359
359
-
a.normalize();
360
422
b.fromBufferAttribute(vertices, i + 1);
361
361
-
b.normalize();
362
423
c.fromBufferAttribute(vertices, i + 2);
363
363
-
c.normalize();
424
424
+
425
425
+
if (planetOptions.shape == "plane") {
426
426
+
a.y = 0;
427
427
+
b.y = 0;
428
428
+
c.y = 0;
429
429
+
} else {
430
430
+
a.normalize();
431
431
+
b.normalize();
432
432
+
c.normalize();
433
433
+
}
364
434
365
435
color.setRGB(colors[i * 3], colors[i * 3 + 1], colors[i * 3 + 2]);
366
436
···
377
447
const moveDataC = calculatedVerticesArray[i + 2];
378
448
379
449
// update height based on vegetation
380
380
-
a.normalize().multiplyScalar(moveDataA.height + output.heightA);
381
381
-
b.normalize().multiplyScalar(moveDataB.height + output.heightB);
382
382
-
c.normalize().multiplyScalar(moveDataC.height + output.heightC);
450
450
+
if (planetOptions.shape == "plane") {
451
451
+
a.y = moveDataA.height + output.heightA;
452
452
+
b.y = moveDataB.height + output.heightB;
453
453
+
c.y = moveDataC.height + output.heightC;
454
454
+
} else {
455
455
+
a.normalize().multiplyScalar(moveDataA.height + output.heightA + 1);
456
456
+
b.normalize().multiplyScalar(moveDataB.height + output.heightB + 1);
457
457
+
c.normalize().multiplyScalar(moveDataC.height + output.heightC + 1);
458
458
+
}
383
459
384
460
vertices.setXYZ(i, a.x, a.y, a.z);
385
461
vertices.setXYZ(i + 1, b.x, b.y, b.z);
···
399
475
colors[i * 3 + 8] = output.color.b;
400
476
}
401
477
402
402
-
oceanSphere.morphAttributes.position[0] = new Float32BufferAttribute(
478
478
+
oceanGeometry.morphAttributes.position[0] = new Float32BufferAttribute(
403
479
oceanMorphPositions,
404
480
3,
405
481
);
406
406
-
oceanSphere.morphAttributes.normal[0] = new Float32BufferAttribute(
482
482
+
oceanGeometry.morphAttributes.normal[0] = new Float32BufferAttribute(
407
483
oceanMorphNormals,
408
484
3,
409
485
);
410
486
411
411
-
sphere.setAttribute("color", new BufferAttribute(colors, 3));
412
412
-
oceanSphere.setAttribute("color", new BufferAttribute(oceanColors, 3));
487
487
+
mainGeometry.setAttribute("color", new BufferAttribute(colors, 3));
488
488
+
oceanGeometry.setAttribute("color", new BufferAttribute(oceanColors, 3));
413
489
414
414
-
return [sphere, oceanSphere, placedVegetation];
490
490
+
return [mainGeometry, oceanGeometry, placedVegetation];
415
491
}