···
3
3
import { Planet } from "./worlds/planet";
4
4
5
5
import { useSuspense } from '@threlte/extras'
6
6
+
import { planetPresets } from "./worlds/presets";
6
7
const suspend = useSuspense()
7
8
8
9
let presets = ['forest', 'beach', 'snowForest'];
9
10
10
10
-
let planet = new Planet({ preset: 'beach' });
11
11
+
let planet = new Planet(planetPresets['beach']);
11
12
let planetMesh = suspend(planet.create());
12
13
13
14
export const redo = async () => {
14
14
-
planet = new Planet({ preset: presets[Math.floor(Math.random() * presets.length)] });
15
15
+
planet = new Planet(planetPresets[presets[Math.floor(Math.random() * presets.length)]]);
15
16
planetMesh = planet.create();
16
17
}
17
18
</script>
···
157
157
</Transform>
158
158
</SheetObject>
159
159
160
160
-
<T.AmbientLight intensity={0.2} />
160
160
+
<T.AmbientLight intensity={0.1} />
161
161
<T.DirectionalLight
162
162
intensity={2}
163
163
-
position={[-pos * 10 + 5, 2 + pos * 3, 2]}
163
163
+
position={[5, 2, 2]}
164
164
castShadow
165
165
shadow.bias={0.0001}
166
166
shadow.mapSize.width={256}
···
1
1
import { UberNoise, type NoiseOptions } from 'uber-noise';
2
2
-
import { Color, Vector3 } from 'three';
2
2
+
import { Color, type ColorRepresentation, Vector3 } from 'three';
3
3
4
4
import { ColorGradient, type ColorGradientOptions } from './helper/colorgradient';
5
5
import { biomePresets } from './presets';
6
6
+
import { Octree } from './helper/octree';
7
7
+
8
8
+
export type VegetationItem = {
9
9
+
name: string;
10
10
+
density?: number;
11
11
+
12
12
+
minimumHeight?: number;
13
13
+
maximumHeight?: number;
14
14
+
15
15
+
minimumSlope?: number;
16
16
+
maximumSlope?: number;
17
17
+
18
18
+
minimumDistance?: number;
19
19
+
maximumDistance?: number;
20
20
+
colors?: Record<string, { array?: number[] }>;
21
21
+
22
22
+
ground?: {
23
23
+
raise?: number;
24
24
+
color?: ColorRepresentation;
25
25
+
radius?: number;
26
26
+
27
27
+
noise?: NoiseOptions;
28
28
+
};
29
29
+
};
6
30
7
31
export type BiomeOptions = {
8
32
name?: string;
···
18
42
tintColor?: number;
19
43
20
44
vegetation?: {
21
21
-
items: {
22
22
-
name: string;
23
23
-
density: number;
45
45
+
defaults?: Omit<Partial<VegetationItem>, 'name'>;
24
46
25
25
-
minimumHeight?: number;
26
26
-
maximumHeight?: number;
27
27
-
28
28
-
minimumDistance?: number;
29
29
-
maximumDistance?: number;
30
30
-
colors?: Record<string, { array?: number[] }>;
31
31
-
}[];
47
47
+
items: VegetationItem[];
32
48
};
33
49
};
34
50
···
41
57
42
58
options: BiomeOptions;
43
59
60
60
+
vegetationPositions: Octree<VegetationItem> = new Octree();
61
61
+
44
62
constructor(opts: BiomeOptions = {}) {
45
63
if (opts.preset) {
46
64
const preset = biomePresets[opts.preset];
65
65
+
47
66
if (preset) {
48
67
opts = {
49
68
...preset,
···
115
134
}
116
135
117
136
return undefined;
137
137
+
}
138
138
+
139
139
+
addVegetation(
140
140
+
item: VegetationItem,
141
141
+
position: Vector3,
142
142
+
normalizedHeight: number,
143
143
+
steepness: number
144
144
+
) {
145
145
+
this.vegetationPositions.insert(position, item);
146
146
+
}
147
147
+
148
148
+
closestVegetationDistance(position: Vector3, radius: number): number | undefined {
149
149
+
const items = this.vegetationPositions.queryBoxXYZ(position.x, position.y, position.z, radius);
150
150
+
if (items.length === 0) return undefined;
151
151
+
152
152
+
let closest = Infinity;
153
153
+
for (const item of items) {
154
154
+
const distance = position.distanceTo(item);
155
155
+
if (distance < closest) closest = distance;
156
156
+
}
157
157
+
158
158
+
return closest < radius ? closest : undefined;
159
159
+
}
160
160
+
161
161
+
itemsAround(position: Vector3, radius: number): (Vector3 & { data?: VegetationItem })[] {
162
162
+
return this.vegetationPositions.queryBoxXYZ(position.x, position.y, position.z, radius);
163
163
+
}
164
164
+
165
165
+
maxVegetationRadius(): number {
166
166
+
let max = 0;
167
167
+
for (const item of this.options.vegetation?.items ?? []) {
168
168
+
if (item.ground?.radius) {
169
169
+
max = Math.max(max, item.ground.radius);
170
170
+
}
171
171
+
}
172
172
+
173
173
+
return max;
174
174
+
}
175
175
+
176
176
+
vegetationHeightAndColorForFace(
177
177
+
a: Vector3,
178
178
+
b: Vector3,
179
179
+
c: Vector3,
180
180
+
color: Color,
181
181
+
sideLength: number
182
182
+
): {
183
183
+
heightA: number;
184
184
+
heightB: number;
185
185
+
heightC: number;
186
186
+
color: Color;
187
187
+
} {
188
188
+
const maxDist = this.maxVegetationRadius();
189
189
+
// use a to find all vegetation items, we add sideLength so that we also find vegetation from b and c
190
190
+
// that otherwise would be missed, because they are too far away from a
191
191
+
const vegetations = this.itemsAround(a, maxDist + sideLength * 2);
192
192
+
193
193
+
// go through a, b and c and add heights for all vegetation items that are close enough (distance is closer than item.ground.radius)
194
194
+
let heightA = 0;
195
195
+
let heightB = 0;
196
196
+
let heightC = 0;
197
197
+
198
198
+
let all = [a, b, c];
199
199
+
for (let j = 0; j < 3; j++) {
200
200
+
let p = all[j];
201
201
+
202
202
+
for (const vegetation of vegetations) {
203
203
+
if (!vegetation.data?.ground?.radius) continue;
204
204
+
205
205
+
let distance = p.distanceTo(vegetation);
206
206
+
207
207
+
if (distance < vegetation.data.ground?.radius) {
208
208
+
let amount = Math.max(0, 1 - distance / vegetation.data.ground.radius);
209
209
+
210
210
+
amount = Math.pow(amount, 0.5);
211
211
+
212
212
+
let height = vegetation.data.ground?.raise ?? 0;
213
213
+
height *= amount;
214
214
+
215
215
+
if (j === 0) heightA += height;
216
216
+
if (j === 1) heightB += height;
217
217
+
if (j === 2) heightC += height;
218
218
+
219
219
+
if (!vegetation.data.ground.color) continue;
220
220
+
221
221
+
let newColor = new Color(vegetation.data.ground.color);
222
222
+
223
223
+
// only lerp a third of the way, because we have three vertices
224
224
+
// so if all vertices are close enough, we lerp 3 times
225
225
+
color.lerp(newColor, amount / 3);
226
226
+
}
227
227
+
}
228
228
+
}
229
229
+
230
230
+
return {
231
231
+
heightA,
232
232
+
heightB,
233
233
+
heightC,
234
234
+
color
235
235
+
};
118
236
}
119
237
}
···
1
1
+
import { BufferGeometry, Float32BufferAttribute } from "three";
2
2
+
3
3
+
export function createBufferGeometry(
4
4
+
positions: number[],
5
5
+
colors?: number[],
6
6
+
normals?: number[],
7
7
+
) {
8
8
+
const geometry = new BufferGeometry();
9
9
+
geometry.setAttribute(
10
10
+
"position",
11
11
+
new Float32BufferAttribute(new Float32Array(positions), 3),
12
12
+
);
13
13
+
14
14
+
if (colors) {
15
15
+
geometry.setAttribute(
16
16
+
"color",
17
17
+
new Float32BufferAttribute(new Float32Array(colors), 3),
18
18
+
);
19
19
+
}
20
20
+
if (normals) {
21
21
+
geometry.setAttribute(
22
22
+
"normal",
23
23
+
new Float32BufferAttribute(new Float32Array(normals), 3),
24
24
+
);
25
25
+
}
26
26
+
27
27
+
return geometry;
28
28
+
}
···
1
1
+
import {
2
2
+
Vector3,
3
3
+
Box3,
4
4
+
Material,
5
5
+
Object3D,
6
6
+
Mesh,
7
7
+
BoxGeometry,
8
8
+
MeshStandardMaterial,
9
9
+
BufferAttribute,
10
10
+
BufferGeometry,
11
11
+
Points,
12
12
+
PointsMaterial,
13
13
+
} from "three";
14
14
+
15
15
+
export type OctreeOptions = {
16
16
+
bounds?: Box3;
17
17
+
18
18
+
size?: number;
19
19
+
20
20
+
min?: Vector3;
21
21
+
max?: Vector3;
22
22
+
23
23
+
points?: Vector3[];
24
24
+
25
25
+
capacity?: number;
26
26
+
};
27
27
+
28
28
+
export class Octree<T = unknown> {
29
29
+
boundary: Box3;
30
30
+
31
31
+
points: (Vector3 & { data?: T })[];
32
32
+
33
33
+
capacity: number;
34
34
+
35
35
+
subdivisions: Octree[] | undefined = undefined;
36
36
+
37
37
+
constructor(opts: OctreeOptions = {}) {
38
38
+
opts ??= {};
39
39
+
40
40
+
this.points = [];
41
41
+
42
42
+
if (opts.bounds) {
43
43
+
this.boundary = opts.bounds.clone();
44
44
+
} else if (opts.size) {
45
45
+
const s = opts.size;
46
46
+
this.boundary = new Box3(new Vector3(-s, -s, -s), new Vector3(s, s, s));
47
47
+
} else if (opts.min || opts.max) {
48
48
+
const min = opts.min || new Vector3(-1, -1, -1);
49
49
+
const max = opts.max || new Vector3(1, 1, 1);
50
50
+
this.boundary = new Box3(min, max);
51
51
+
} else if (opts.points && opts.points.length > 0) {
52
52
+
const min = opts.points[0].clone();
53
53
+
const max = opts.points[0].clone();
54
54
+
for (const p of opts.points) {
55
55
+
min.x = Math.min(min.x, p.x);
56
56
+
min.y = Math.min(min.y, p.y);
57
57
+
min.z = Math.min(min.z, p.z);
58
58
+
59
59
+
max.x = Math.max(max.x, p.x);
60
60
+
max.y = Math.max(max.y, p.y);
61
61
+
max.z = Math.max(max.z, p.z);
62
62
+
}
63
63
+
this.boundary = new Box3(min, max);
64
64
+
} else {
65
65
+
this.boundary = new Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1));
66
66
+
}
67
67
+
68
68
+
this.capacity = opts.capacity || 4;
69
69
+
70
70
+
if (opts.points) {
71
71
+
for (const p of opts.points) {
72
72
+
this.insertXYZ(p.x, p.y, p.z);
73
73
+
}
74
74
+
}
75
75
+
}
76
76
+
77
77
+
subdivide() {
78
78
+
// if already subdivided exit silently
79
79
+
if (this.subdivisions != undefined) return;
80
80
+
81
81
+
// divide each dimension => 2 * 2 * 2 = 8 subdivisions
82
82
+
const size = new Vector3();
83
83
+
const subdivisions: Octree[] = [];
84
84
+
for (let x = 0; x < 2; x++) {
85
85
+
for (let y = 0; y < 2; y++) {
86
86
+
for (let z = 0; z < 2; z++) {
87
87
+
const min = this.boundary.min.clone();
88
88
+
const max = this.boundary.max.clone();
89
89
+
this.boundary.getSize(size);
90
90
+
size.divideScalar(2);
91
91
+
92
92
+
min.x += x * size.x;
93
93
+
min.y += y * size.y;
94
94
+
min.z += z * size.z;
95
95
+
max.x -= (1 - x) * size.x;
96
96
+
max.y -= (1 - y) * size.y;
97
97
+
max.z -= (1 - z) * size.z;
98
98
+
99
99
+
subdivisions.push(
100
100
+
new Octree({
101
101
+
min: min,
102
102
+
max: max,
103
103
+
capacity: this.capacity,
104
104
+
}),
105
105
+
);
106
106
+
}
107
107
+
}
108
108
+
}
109
109
+
this.subdivisions = subdivisions;
110
110
+
}
111
111
+
112
112
+
// returns array of points where
113
113
+
// distance between pos and point is less than dist
114
114
+
query(pos: Vector3 & { data?: T }, dist = 1): (Vector3 & { data?: T })[] {
115
115
+
const points = this.queryBoxXYZ(pos.x, pos.y, pos.z, dist);
116
116
+
117
117
+
return points.filter((p) => p.distanceTo(pos) < dist);
118
118
+
}
119
119
+
120
120
+
// vector3 free version, returns points around xyz
121
121
+
queryXYZ(x: number, y: number, z: number, dist: number) {
122
122
+
const point = new Vector3(x, y, z);
123
123
+
124
124
+
return this.query(point, dist);
125
125
+
}
126
126
+
127
127
+
queryBoxXYZ(x: number, y: number, z: number, s: number) {
128
128
+
const min = new Vector3(x - s, y - s, z - s),
129
129
+
max = new Vector3(x + s, y + s, z + s);
130
130
+
const box = new Box3(min, max);
131
131
+
132
132
+
return this.queryBox(box);
133
133
+
}
134
134
+
135
135
+
queryBox(box: Box3, found: (Vector3 & { data?: T })[] = []) {
136
136
+
found ??= [];
137
137
+
138
138
+
if (!box.intersectsBox(this.boundary)) return found;
139
139
+
140
140
+
for (const p of this.points) {
141
141
+
if (box.containsPoint(p)) found.push(p);
142
142
+
}
143
143
+
if (this.subdivisions) {
144
144
+
for (const sub of this.subdivisions) {
145
145
+
sub.queryBox(box, found);
146
146
+
}
147
147
+
}
148
148
+
return found;
149
149
+
}
150
150
+
151
151
+
// returns true if no points are closer than dist to point
152
152
+
minDist(pos: Vector3, dist: number) {
153
153
+
return this.query(pos, dist).length < 1;
154
154
+
}
155
155
+
156
156
+
// insert point with optional data (sets vec.data = data)
157
157
+
insert(pos: Vector3 & { data?: T }, data: T | undefined = undefined) {
158
158
+
return this.insertPoint(pos, data);
159
159
+
}
160
160
+
161
161
+
// vector3 free version
162
162
+
insertXYZ(x: number, y: number, z: number, data: T | undefined = undefined) {
163
163
+
return this.insertPoint(new Vector3(x, y, z), data);
164
164
+
}
165
165
+
166
166
+
insertPoint(p: Vector3, data: T | undefined = undefined) {
167
167
+
p = p.clone();
168
168
+
169
169
+
// @ts-expect-error - data is not a property of Vector3
170
170
+
if (data) p.data = data;
171
171
+
172
172
+
if (!this.boundary.containsPoint(p)) return false;
173
173
+
174
174
+
if (this.points.length < this.capacity) {
175
175
+
this.points.push(p);
176
176
+
return true;
177
177
+
} else {
178
178
+
this.subdivide();
179
179
+
let added = false;
180
180
+
for (const sub of this.subdivisions ?? []) {
181
181
+
if (sub.insertPoint(p, data)) added = true;
182
182
+
}
183
183
+
return added;
184
184
+
}
185
185
+
}
186
186
+
187
187
+
showBoxes(mat: Material, parent: Object3D | undefined = undefined) {
188
188
+
const size = new Vector3();
189
189
+
this.boundary.getSize(size);
190
190
+
191
191
+
const box = new BoxGeometry(size.x * 2, size.y * 2, size.z * 2);
192
192
+
const mesh = new Mesh(
193
193
+
box,
194
194
+
mat ||
195
195
+
new MeshStandardMaterial({
196
196
+
wireframe: true,
197
197
+
}),
198
198
+
);
199
199
+
this.boundary.getCenter(mesh.position);
200
200
+
201
201
+
parent ??= new Object3D();
202
202
+
parent.add(mesh);
203
203
+
204
204
+
if (this.subdivisions) {
205
205
+
for (const sub of this.subdivisions) sub.showBoxes(mat, parent);
206
206
+
}
207
207
+
return parent;
208
208
+
}
209
209
+
210
210
+
show(
211
211
+
opts: {
212
212
+
pointsOnly?: boolean;
213
213
+
mat?: Material;
214
214
+
size?: number;
215
215
+
sizeAttenuation?: boolean;
216
216
+
p?: Vector3;
217
217
+
min?: number;
218
218
+
} = {},
219
219
+
) {
220
220
+
opts ??= {};
221
221
+
222
222
+
const pointsOnly = opts.pointsOnly;
223
223
+
let mat = opts.mat;
224
224
+
const points = this.all();
225
225
+
226
226
+
const pointsGeo = new BufferGeometry();
227
227
+
const positionData = new Float32Array(points.length * 3);
228
228
+
const colorData = new Float32Array(points.length * 3);
229
229
+
230
230
+
let q;
231
231
+
232
232
+
if (opts.p && opts.min) {
233
233
+
for (const point of points) {
234
234
+
// @ts-expect-error - close is not a property of Vector3
235
235
+
point.close = false;
236
236
+
}
237
237
+
q = this.query(opts.p, opts.min);
238
238
+
239
239
+
for (const point of q) {
240
240
+
point.close = true;
241
241
+
}
242
242
+
}
243
243
+
244
244
+
for (let i = 0; i < points.length; i++) {
245
245
+
positionData[i * 3] = points[i].x;
246
246
+
positionData[i * 3 + 1] = points[i].y;
247
247
+
positionData[i * 3 + 2] = points[i].z;
248
248
+
249
249
+
// @ts-expect-error - close is not a property of Vector3
250
250
+
colorData[i * 3] = points[i].close ? 1 : 0.7;
251
251
+
252
252
+
// @ts-expect-error - close is not a property of Vector3
253
253
+
colorData[i * 3 + 1] = points[i].close ? 0 : 0.7;
254
254
+
255
255
+
// @ts-expect-error - close is not a property of Vector3
256
256
+
colorData[i * 3 + 2] = points[i].close ? 0 : 0.7;
257
257
+
}
258
258
+
pointsGeo.setAttribute("position", new BufferAttribute(positionData, 3));
259
259
+
pointsGeo.setAttribute("color", new BufferAttribute(colorData, 3));
260
260
+
const pointMesh = new Points(
261
261
+
pointsGeo,
262
262
+
new PointsMaterial({
263
263
+
size: opts.size || 1,
264
264
+
sizeAttenuation: opts.sizeAttenuation || false,
265
265
+
vertexColors: true,
266
266
+
}),
267
267
+
);
268
268
+
if (pointsOnly) return pointMesh;
269
269
+
270
270
+
mat =
271
271
+
mat ||
272
272
+
new MeshStandardMaterial({
273
273
+
transparent: true,
274
274
+
opacity: 0.01,
275
275
+
depthTest: false,
276
276
+
});
277
277
+
const boxes = this.showBoxes(mat);
278
278
+
boxes.add(pointMesh);
279
279
+
return boxes;
280
280
+
}
281
281
+
282
282
+
all(arr: (Vector3 & { data?: T })[] = []) {
283
283
+
arr ??= [];
284
284
+
for (const p of this.points) {
285
285
+
arr.push(p);
286
286
+
}
287
287
+
if (this.subdivisions) {
288
288
+
for (const subs of this.subdivisions) subs.all(arr);
289
289
+
}
290
290
+
return arr;
291
291
+
}
292
292
+
}
···
1
1
-
import * as THREE from 'three';
2
2
-
3
3
-
export type OcttreeOptions = {
4
4
-
bounds?: THREE.Box3;
5
5
-
6
6
-
size?: number;
7
7
-
8
8
-
min?: THREE.Vector3;
9
9
-
max?: THREE.Vector3;
10
10
-
11
11
-
points?: THREE.Vector3[];
12
12
-
13
13
-
capacity?: number;
14
14
-
};
15
15
-
16
16
-
export class Octtree {
17
17
-
boundary: THREE.Box3;
18
18
-
19
19
-
points: THREE.Vector3[];
20
20
-
21
21
-
capacity: number;
22
22
-
23
23
-
subdivisions: Octtree[] | undefined = undefined;
24
24
-
25
25
-
constructor(opts: OcttreeOptions = {}) {
26
26
-
opts ??= {};
27
27
-
28
28
-
this.points = [];
29
29
-
30
30
-
if (opts.bounds) {
31
31
-
this.boundary = opts.bounds.clone();
32
32
-
} else if (opts.size) {
33
33
-
const s = opts.size;
34
34
-
this.boundary = new THREE.Box3(new THREE.Vector3(-s, -s, -s), new THREE.Vector3(s, s, s));
35
35
-
} else if (opts.min || opts.max) {
36
36
-
const min = opts.min || new THREE.Vector3(-1, -1, -1);
37
37
-
const max = opts.max || new THREE.Vector3(1, 1, 1);
38
38
-
this.boundary = new THREE.Box3(min, max);
39
39
-
} else if (opts.points && opts.points.length > 0) {
40
40
-
const min = opts.points[0].clone();
41
41
-
const max = opts.points[0].clone();
42
42
-
for (const p of opts.points) {
43
43
-
min.x = Math.min(min.x, p.x);
44
44
-
min.y = Math.min(min.y, p.y);
45
45
-
min.z = Math.min(min.z, p.z);
46
46
-
47
47
-
max.x = Math.max(max.x, p.x);
48
48
-
max.y = Math.max(max.y, p.y);
49
49
-
max.z = Math.max(max.z, p.z);
50
50
-
}
51
51
-
this.boundary = new THREE.Box3(min, max);
52
52
-
} else {
53
53
-
this.boundary = new THREE.Box3(new THREE.Vector3(-1, -1, -1), new THREE.Vector3(1, 1, 1));
54
54
-
}
55
55
-
56
56
-
this.capacity = opts.capacity || 4;
57
57
-
58
58
-
if (opts.points) {
59
59
-
for (const p of opts.points) {
60
60
-
this.insertXYZ(p.x, p.y, p.z);
61
61
-
}
62
62
-
}
63
63
-
}
64
64
-
65
65
-
subdivide() {
66
66
-
// if already subdivided exit silently
67
67
-
if (this.subdivisions != undefined) return;
68
68
-
69
69
-
// divide each dimension => 2 * 2 * 2 = 8 subdivisions
70
70
-
const size = new THREE.Vector3();
71
71
-
const subdivisions: Octtree[] = [];
72
72
-
for (let x = 0; x < 2; x++) {
73
73
-
for (let y = 0; y < 2; y++) {
74
74
-
for (let z = 0; z < 2; z++) {
75
75
-
const min = this.boundary.min.clone();
76
76
-
const max = this.boundary.max.clone();
77
77
-
this.boundary.getSize(size);
78
78
-
size.divideScalar(2);
79
79
-
80
80
-
min.x += x * size.x;
81
81
-
min.y += y * size.y;
82
82
-
min.z += z * size.z;
83
83
-
max.x -= (1 - x) * size.x;
84
84
-
max.y -= (1 - y) * size.y;
85
85
-
max.z -= (1 - z) * size.z;
86
86
-
87
87
-
subdivisions.push(
88
88
-
new Octtree({
89
89
-
min: min,
90
90
-
max: max,
91
91
-
capacity: this.capacity
92
92
-
})
93
93
-
);
94
94
-
}
95
95
-
}
96
96
-
}
97
97
-
this.subdivisions = subdivisions;
98
98
-
}
99
99
-
100
100
-
// returns array of points where
101
101
-
// distance between pos and point is less than dist
102
102
-
query(pos: THREE.Vector3, dist = 1) {
103
103
-
const points = this.queryXYZ(pos.x, pos.y, pos.z, dist);
104
104
-
for (let i = points.length - 1; i >= 0; i--) {
105
105
-
if (points[i].distanceTo(pos) > dist) points.splice(i, 1);
106
106
-
}
107
107
-
return points;
108
108
-
}
109
109
-
110
110
-
// vector3 free version, returns points in box around xyz
111
111
-
queryXYZ(x: number, y: number, z: number, s: number) {
112
112
-
const min = new THREE.Vector3(x - s, y - s, z - s),
113
113
-
max = new THREE.Vector3(x + s, y + s, z + s);
114
114
-
const box = new THREE.Box3(min, max);
115
115
-
116
116
-
return this.queryBox(box);
117
117
-
}
118
118
-
119
119
-
queryBox(box: THREE.Box3, found: THREE.Vector3[] = []) {
120
120
-
found ??= [];
121
121
-
122
122
-
if (!box.intersectsBox(this.boundary)) return found;
123
123
-
124
124
-
for (const p of this.points) {
125
125
-
if (box.containsPoint(p)) found.push(p);
126
126
-
}
127
127
-
if (this.subdivisions) {
128
128
-
for (const sub of this.subdivisions) {
129
129
-
sub.queryBox(box, found);
130
130
-
}
131
131
-
}
132
132
-
return found;
133
133
-
}
134
134
-
135
135
-
// returns true if no points are closer than dist to point
136
136
-
minDist(pos: THREE.Vector3, dist: number) {
137
137
-
return this.query(pos, dist).length < 1;
138
138
-
}
139
139
-
140
140
-
// insert point with optional data (sets vec.data = data)
141
141
-
insert(pos: THREE.Vector3, data: unknown = undefined) {
142
142
-
return this.insertPoint(pos, data);
143
143
-
}
144
144
-
// vector3 free version
145
145
-
insertXYZ(x: number, y: number, z: number, data: unknown = undefined) {
146
146
-
return this.insertPoint(new THREE.Vector3(x, y, z), data);
147
147
-
}
148
148
-
insertPoint(p: THREE.Vector3, data: unknown = undefined) {
149
149
-
p = p.clone();
150
150
-
151
151
-
// @ts-expect-error - data is not a property of Vector3
152
152
-
if (data) p.data = data;
153
153
-
154
154
-
if (!this.boundary.containsPoint(p)) return false;
155
155
-
156
156
-
if (this.points.length < this.capacity) {
157
157
-
this.points.push(p);
158
158
-
return true;
159
159
-
} else {
160
160
-
this.subdivide();
161
161
-
let added = false;
162
162
-
for (const sub of this.subdivisions ?? []) {
163
163
-
if (sub.insertPoint(p, data)) added = true;
164
164
-
}
165
165
-
return added;
166
166
-
}
167
167
-
}
168
168
-
169
169
-
showBoxes(mat: THREE.Material, parent: THREE.Object3D | undefined = undefined) {
170
170
-
const size = new THREE.Vector3();
171
171
-
this.boundary.getSize(size);
172
172
-
173
173
-
const box = new THREE.BoxGeometry(size.x * 2, size.y * 2, size.z * 2);
174
174
-
const mesh = new THREE.Mesh(
175
175
-
box,
176
176
-
mat ||
177
177
-
new THREE.MeshStandardMaterial({
178
178
-
wireframe: true
179
179
-
})
180
180
-
);
181
181
-
this.boundary.getCenter(mesh.position);
182
182
-
183
183
-
parent ??= new THREE.Object3D();
184
184
-
parent.add(mesh);
185
185
-
186
186
-
if (this.subdivisions) {
187
187
-
for (const sub of this.subdivisions) sub.showBoxes(mat, parent);
188
188
-
}
189
189
-
return parent;
190
190
-
}
191
191
-
192
192
-
show(
193
193
-
opts: {
194
194
-
pointsOnly?: boolean;
195
195
-
mat?: THREE.Material;
196
196
-
size?: number;
197
197
-
sizeAttenuation?: boolean;
198
198
-
p?: THREE.Vector3;
199
199
-
min?: number;
200
200
-
} = {}
201
201
-
) {
202
202
-
opts ??= {};
203
203
-
204
204
-
const pointsOnly = opts.pointsOnly;
205
205
-
let mat = opts.mat;
206
206
-
const points = this.all();
207
207
-
208
208
-
const pointsGeo = new THREE.BufferGeometry();
209
209
-
const positionData = new Float32Array(points.length * 3);
210
210
-
const colorData = new Float32Array(points.length * 3);
211
211
-
212
212
-
let q;
213
213
-
214
214
-
if (opts.p && opts.min) {
215
215
-
for (const point of points) {
216
216
-
// @ts-expect-error - close is not a property of Vector3
217
217
-
point.close = false;
218
218
-
}
219
219
-
q = this.query(opts.p, opts.min);
220
220
-
221
221
-
for (const point of q) {
222
222
-
// @ts-expect-error - close is not a property of Vector3
223
223
-
point.close = true;
224
224
-
}
225
225
-
}
226
226
-
227
227
-
for (let i = 0; i < points.length; i++) {
228
228
-
positionData[i * 3] = points[i].x;
229
229
-
positionData[i * 3 + 1] = points[i].y;
230
230
-
positionData[i * 3 + 2] = points[i].z;
231
231
-
232
232
-
// @ts-expect-error - close is not a property of Vector3
233
233
-
colorData[i * 3] = points[i].close ? 1 : 0.7;
234
234
-
235
235
-
// @ts-expect-error - close is not a property of Vector3
236
236
-
colorData[i * 3 + 1] = points[i].close ? 0 : 0.7;
237
237
-
238
238
-
// @ts-expect-error - close is not a property of Vector3
239
239
-
colorData[i * 3 + 2] = points[i].close ? 0 : 0.7;
240
240
-
}
241
241
-
pointsGeo.setAttribute('position', new THREE.BufferAttribute(positionData, 3));
242
242
-
pointsGeo.setAttribute('color', new THREE.BufferAttribute(colorData, 3));
243
243
-
const pointMesh = new THREE.Points(
244
244
-
pointsGeo,
245
245
-
new THREE.PointsMaterial({
246
246
-
size: opts.size || 1,
247
247
-
sizeAttenuation: opts.sizeAttenuation || false,
248
248
-
vertexColors: true
249
249
-
})
250
250
-
);
251
251
-
if (pointsOnly) return pointMesh;
252
252
-
253
253
-
mat =
254
254
-
mat ||
255
255
-
new THREE.MeshStandardMaterial({
256
256
-
transparent: true,
257
257
-
opacity: 0.01,
258
258
-
depthTest: false
259
259
-
});
260
260
-
const boxes = this.showBoxes(mat);
261
261
-
boxes.add(pointMesh);
262
262
-
return boxes;
263
263
-
}
264
264
-
265
265
-
all(arr: THREE.Vector3[] = []) {
266
266
-
arr ??= [];
267
267
-
for (const p of this.points) {
268
268
-
arr.push(p);
269
269
-
}
270
270
-
if (this.subdivisions) {
271
271
-
for (const subs of this.subdivisions) subs.all(arr);
272
272
-
}
273
273
-
return arr;
274
274
-
}
275
275
-
}
···
1
1
+
import { ShaderMaterial, Vector3 } from "three";
2
2
+
3
3
+
export function createAtmosphereMaterial(
4
4
+
color: Vector3 | undefined = undefined,
5
5
+
lightDirection: Vector3 | undefined = undefined,
6
6
+
) {
7
7
+
const uniforms = {
8
8
+
lightDirection: {
9
9
+
value: (lightDirection ?? new Vector3(1.0, 1.0, 0.0)).normalize(),
10
10
+
},
11
11
+
atmosphereColor: { value: color ?? new Vector3(0.3, 0.6, 1.0) },
12
12
+
};
13
13
+
14
14
+
const atmosphereShader = {
15
15
+
uniforms,
16
16
+
vertexShader: `
17
17
+
varying vec3 vNormal;
18
18
+
varying vec3 vViewLightDirection; // Light direction relative to the camera
19
19
+
varying vec3 vViewPosition; // View position
20
20
+
21
21
+
uniform vec3 lightDirection; // Light direction in world space
22
22
+
23
23
+
void main() {
24
24
+
// Transform the vertex normal to world space
25
25
+
vNormal = normalize(normalMatrix * normal);
26
26
+
// Transform the light direction to view space
27
27
+
vViewLightDirection = (viewMatrix * vec4(lightDirection, 0.0)).xyz;
28
28
+
vViewPosition = (modelViewMatrix * vec4(position, 1.0)).xyz;
29
29
+
30
30
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
31
31
+
}
32
32
+
`,
33
33
+
fragmentShader: `
34
34
+
varying vec3 vNormal;
35
35
+
varying vec3 vViewLightDirection;
36
36
+
varying vec3 vViewPosition;
37
37
+
uniform vec3 atmosphereColor;
38
38
+
39
39
+
void main() {
40
40
+
41
41
+
// Normalize the normal and the view direction
42
42
+
vec3 viewDirection = normalize(vViewPosition);
43
43
+
44
44
+
// Calculate how much of the surface is perpendicular to the view direction
45
45
+
float viewFactor = dot(normalize(vNormal), -viewDirection);
46
46
+
47
47
+
// Calculate the dot product of the light direction and the surface normal
48
48
+
float lightFactor = dot(normalize(vNormal), normalize(vViewLightDirection));
49
49
+
50
50
+
// Use smoothstep to soften the transition from light to dark side
51
51
+
lightFactor = smoothstep(-0.2, 0.4, lightFactor);
52
52
+
53
53
+
// Ensure a minimum glow, even on the dark side
54
54
+
float minGlow = 0.2; // Adjust this to control how much it glows on the dark side
55
55
+
lightFactor = mix(minGlow, 1.0, lightFactor);
56
56
+
57
57
+
// Adjust the intensity for the atmosphere's glow, including the minimum glow
58
58
+
float dotProduct = dot(normalize(vNormal), vec3(0, 0.0, 1.0));
59
59
+
float intensity = pow(dotProduct, 8.0);
60
60
+
intensity *= lightFactor;
61
61
+
62
62
+
viewFactor = clamp(1.0 - viewFactor, 0.0, 1.0);
63
63
+
64
64
+
// Use smoothstep for a smooth transition on the edges
65
65
+
float atmosphereFactor = smoothstep(0.2, 0.6, viewFactor);
66
66
+
67
67
+
// Output the final color
68
68
+
gl_FragColor = vec4(atmosphereColor, intensity * atmosphereFactor);
69
69
+
// Set the final fragment color with the computed intensity
70
70
+
//gl_FragColor = vec4(0.3, 0.6, 1.0, 1.0) * intensity;
71
71
+
}`,
72
72
+
transparent: true,
73
73
+
depthWrite: false,
74
74
+
};
75
75
+
76
76
+
return new ShaderMaterial(atmosphereShader);
77
77
+
}
···
1
1
+
import {
2
2
+
MeshStandardMaterial,
3
3
+
type MeshStandardMaterialParameters,
4
4
+
} from "three";
5
5
+
import { noise } from "./noise";
6
6
+
7
7
+
export class PlanetMaterialWithCaustics extends MeshStandardMaterial {
8
8
+
constructor(parameters: MeshStandardMaterialParameters) {
9
9
+
super(parameters);
10
10
+
11
11
+
this.onBeforeCompile = (shader) => {
12
12
+
const caustics = `
13
13
+
float caustics(vec4 vPos) {
14
14
+
// More intricate warping for marble patterns
15
15
+
// float warpFactor = 2.0;
16
16
+
// vec4 warpedPos = vPos * warpFactor + snoise(vPos * warpFactor * 0.5);
17
17
+
// vec4 warpedPos2 = warpedPos * warpFactor * 0.3 + snoise(warpedPos * warpFactor * 0.5 + vec4(0, 2, 4, 8)) + vPos;
18
18
+
19
19
+
// // Modulate the color intensity based on the noise
20
20
+
// float vein = snoise(warpedPos2 * warpFactor) * snoise(warpedPos);
21
21
+
22
22
+
// float a = 1.0 - (sin(vein * 12.0) + 1.0) * 0.5;
23
23
+
// float diff = snoise(vPos * warpFactor);
24
24
+
// diff = diff * snoise(diff * vPos) * a;
25
25
+
// return vec3((diff));
26
26
+
27
27
+
vec4 warpedPos = vPos * 2.0 + snoise(vPos * 3.0);
28
28
+
vec4 warpedPos2 = warpedPos * 0.3 + snoise(warpedPos * 2.0 + vec4(0, 2, 4, 8)) + vPos;
29
29
+
float vein = snoise(warpedPos2) * snoise(warpedPos);
30
30
+
float a = 1.0 - (sin(vein * 2.0) + 1.0) * 0.5;
31
31
+
32
32
+
return snoise(vPos + warpedPos + warpedPos2) * a * 1.5;
33
33
+
}`;
34
34
+
shader.vertexShader =
35
35
+
`varying vec3 vPos;\n${shader.vertexShader}`.replace(
36
36
+
`#include <begin_vertex>`,
37
37
+
`#include <begin_vertex>\nvPos = position;`,
38
38
+
);
39
39
+
40
40
+
shader.fragmentShader = `
41
41
+
uniform float time;
42
42
+
varying vec3 vPos;
43
43
+
${noise}
44
44
+
${caustics}
45
45
+
${shader.fragmentShader}`;
46
46
+
47
47
+
shader.fragmentShader = shader.fragmentShader.replace(
48
48
+
"#include <color_fragment>",
49
49
+
`#include <color_fragment>
50
50
+
vec3 pos = vPos * 3.0;
51
51
+
float len = length(vPos);
52
52
+
// Fade in
53
53
+
float fadeIn = smoothstep(0.96, 0.985, len);
54
54
+
// Fade out
55
55
+
float fadeOut = 1.0 - smoothstep(0.994, 0.999, len);
56
56
+
float causticIntensity = fadeIn * fadeOut * 0.7;
57
57
+
diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), causticIntensity * smoothstep(0.0, 1.0, caustics(vec4(pos, time * 0.05))));
58
58
+
`,
59
59
+
);
60
60
+
61
61
+
shader.uniforms.time = { value: 0 };
62
62
+
this.userData.shader = shader;
63
63
+
};
64
64
+
}
65
65
+
66
66
+
update() {
67
67
+
if (this.userData.shader?.uniforms?.time) {
68
68
+
this.userData.shader.uniforms.time.value = performance.now() / 1000;
69
69
+
}
70
70
+
}
71
71
+
}
72
72
+
73
73
+
export default PlanetMaterialWithCaustics;
···
1
1
+
export const noise = /* GLSL */ `
2
2
+
// Description : Array and textureless GLSL 2D/3D/4D simplex
3
3
+
// noise functions.
4
4
+
// Author : Ian McEwan, Ashima Arts.
5
5
+
// Maintainer : stegu
6
6
+
// Lastmod : 20110822 (ijm)
7
7
+
// License : Copyright (C) 2011 Ashima Arts. All rights reserved.
8
8
+
// Distributed under the MIT License. See LICENSE file.
9
9
+
// https://github.com/ashima/webgl-noise
10
10
+
// https://github.com/stegu/webgl-noise
11
11
+
//
12
12
+
13
13
+
vec4 mod289(vec4 x) {
14
14
+
return x - floor(x * (1.0 / 289.0)) * 289.0;
15
15
+
}
16
16
+
17
17
+
float mod289(float x) {
18
18
+
return x - floor(x * (1.0 / 289.0)) * 289.0;
19
19
+
}
20
20
+
21
21
+
vec4 permute(vec4 x) {
22
22
+
return mod289(((x * 34.0) + 10.0) * x);
23
23
+
}
24
24
+
25
25
+
float permute(float x) {
26
26
+
return mod289(((x * 34.0) + 10.0) * x);
27
27
+
}
28
28
+
29
29
+
vec4 taylorInvSqrt(vec4 r) {
30
30
+
return 1.79284291400159 - 0.85373472095314 * r;
31
31
+
}
32
32
+
33
33
+
float taylorInvSqrt(float r) {
34
34
+
return 1.79284291400159 - 0.85373472095314 * r;
35
35
+
}
36
36
+
37
37
+
vec4 grad4(float j, vec4 ip) {
38
38
+
const vec4 ones = vec4(1.0, 1.0, 1.0, -1.0);
39
39
+
vec4 p, s;
40
40
+
41
41
+
p.xyz = floor(fract(vec3(j) * ip.xyz) * 7.0) * ip.z - 1.0;
42
42
+
p.w = 1.5 - dot(abs(p.xyz), ones.xyz);
43
43
+
s = vec4(lessThan(p, vec4(0.0)));
44
44
+
p.xyz = p.xyz + (s.xyz * 2.0 - 1.0) * s.www;
45
45
+
46
46
+
return p;
47
47
+
}
48
48
+
49
49
+
// (sqrt(5) - 1)/4 = F4, used once below
50
50
+
#define F4 0.309016994374947451
51
51
+
52
52
+
float snoise(vec4 v) {
53
53
+
const vec4 C = vec4(0.138196601125011, // (5 - sqrt(5))/20 G4
54
54
+
0.276393202250021, // 2 * G4
55
55
+
0.414589803375032, // 3 * G4
56
56
+
-0.447213595499958); // -1 + 4 * G4
57
57
+
58
58
+
// First corner
59
59
+
vec4 i = floor(v + dot(v, vec4(F4)));
60
60
+
vec4 x0 = v - i + dot(i, C.xxxx);
61
61
+
62
62
+
// Other corners
63
63
+
64
64
+
// Rank sorting originally contributed by Bill Licea-Kane, AMD (formerly ATI)
65
65
+
vec4 i0;
66
66
+
vec3 isX = step(x0.yzw, x0.xxx);
67
67
+
vec3 isYZ = step(x0.zww, x0.yyz);
68
68
+
// i0.x = dot( isX, vec3( 1.0 ) );
69
69
+
i0.x = isX.x + isX.y + isX.z;
70
70
+
i0.yzw = 1.0 - isX;
71
71
+
// i0.y += dot( isYZ.xy, vec2( 1.0 ) );
72
72
+
i0.y += isYZ.x + isYZ.y;
73
73
+
i0.zw += 1.0 - isYZ.xy;
74
74
+
i0.z += isYZ.z;
75
75
+
i0.w += 1.0 - isYZ.z;
76
76
+
77
77
+
// i0 now contains the unique values 0,1,2,3 in each channel
78
78
+
vec4 i3 = clamp(i0, 0.0, 1.0);
79
79
+
vec4 i2 = clamp(i0 - 1.0, 0.0, 1.0);
80
80
+
vec4 i1 = clamp(i0 - 2.0, 0.0, 1.0);
81
81
+
82
82
+
// x0 = x0 - 0.0 + 0.0 * C.xxxx
83
83
+
// x1 = x0 - i1 + 1.0 * C.xxxx
84
84
+
// x2 = x0 - i2 + 2.0 * C.xxxx
85
85
+
// x3 = x0 - i3 + 3.0 * C.xxxx
86
86
+
// x4 = x0 - 1.0 + 4.0 * C.xxxx
87
87
+
vec4 x1 = x0 - i1 + C.xxxx;
88
88
+
vec4 x2 = x0 - i2 + C.yyyy;
89
89
+
vec4 x3 = x0 - i3 + C.zzzz;
90
90
+
vec4 x4 = x0 + C.wwww;
91
91
+
92
92
+
// Permutations
93
93
+
i = mod289(i);
94
94
+
float j0 = permute(permute(permute(permute(i.w) + i.z) + i.y) + i.x);
95
95
+
vec4 j1 = permute(permute(permute(permute(i.w + vec4(i1.w, i2.w, i3.w, 1.0)) + i.z + vec4(i1.z, i2.z, i3.z, 1.0)) + i.y + vec4(i1.y, i2.y, i3.y, 1.0)) + i.x + vec4(i1.x, i2.x, i3.x, 1.0));
96
96
+
97
97
+
// Gradients: 7x7x6 points over a cube, mapped onto a 4-cross polytope
98
98
+
// 7*7*6 = 294, which is close to the ring size 17*17 = 289.
99
99
+
vec4 ip = vec4(1.0 / 294.0, 1.0 / 49.0, 1.0 / 7.0, 0.0);
100
100
+
101
101
+
vec4 p0 = grad4(j0, ip);
102
102
+
vec4 p1 = grad4(j1.x, ip);
103
103
+
vec4 p2 = grad4(j1.y, ip);
104
104
+
vec4 p3 = grad4(j1.z, ip);
105
105
+
vec4 p4 = grad4(j1.w, ip);
106
106
+
107
107
+
// Normalise gradients
108
108
+
vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
109
109
+
p0 *= norm.x;
110
110
+
p1 *= norm.y;
111
111
+
p2 *= norm.z;
112
112
+
p3 *= norm.w;
113
113
+
p4 *= taylorInvSqrt(dot(p4, p4));
114
114
+
115
115
+
// Mix contributions from the five corners
116
116
+
vec3 m0 = max(0.6 - vec3(dot(x0, x0), dot(x1, x1), dot(x2, x2)), 0.0);
117
117
+
vec2 m1 = max(0.6 - vec2(dot(x3, x3), dot(x4, x4)), 0.0);
118
118
+
m0 = m0 * m0;
119
119
+
m1 = m1 * m1;
120
120
+
return 49.0 * (dot(m0 * m0, vec3(dot(p0, x0), dot(p1, x1), dot(p2, x2))) + dot(m1 * m1, vec2(dot(p3, x3), dot(p4, x4))));
121
121
+
}`;
···
1
1
import * as THREE from "three";
2
2
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
3
3
4
4
+
const basePath = '';
5
5
+
4
6
const lowPolyNatureCollectionModels: Record<
5
7
string,
6
8
{ versions?: number; materials: string[] }
···
85
87
}
86
88
87
89
export function getModelPathsAndMaterials(
88
88
-
name: string,
89
89
-
collection: Collections = "lowpoly_nature",
90
90
+
name: string,
91
91
+
collection: Collections = 'lowpoly_nature'
90
92
): {
91
91
-
filePaths: string[];
92
92
-
materials: string[];
93
93
+
filePaths: string[];
94
94
+
materials: string[];
93
95
} | null {
94
94
-
const model = collections[collection].models[name];
95
95
-
if (!model) {
96
96
-
console.error(`Model ${name} not found.`);
97
97
-
return null;
98
98
-
}
96
96
+
const model = collections[collection].models[name];
97
97
+
if (!model) {
98
98
+
console.error(`Model ${name} not found.`);
99
99
+
return null;
100
100
+
}
99
101
100
100
-
// Construct file paths based on the number of versions
101
101
-
const filePaths: string[] = [];
102
102
-
if (model.versions) {
103
103
-
for (let i = 1; i <= model.versions; i++) {
104
104
-
filePaths.push(`${collections[collection].name}/${name}_${i}.gltf`);
105
105
-
}
106
106
-
} else {
107
107
-
filePaths.push(`${collections[collection].name}/${name}.gltf`);
108
108
-
}
102
102
+
// Construct file paths based on the number of versions
103
103
+
const filePaths: string[] = [];
104
104
+
if (model.versions) {
105
105
+
for (let i = 1; i <= model.versions; i++) {
106
106
+
filePaths.push(`${basePath}${collections[collection].name}/${name}_${i}.gltf`);
107
107
+
}
108
108
+
} else {
109
109
+
filePaths.push(`${basePath}${collections[collection].name}/${name}.gltf`);
110
110
+
}
109
111
110
110
-
return {
111
111
-
filePaths,
112
112
-
materials: model.materials,
113
113
-
};
112
112
+
return {
113
113
+
filePaths,
114
114
+
materials: model.materials
115
115
+
};
114
116
}
115
117
116
118
export async function loadModels(
117
117
-
name: string,
118
118
-
collection: Collections = "lowpoly_nature",
119
119
+
name: string,
120
120
+
collection: Collections = 'lowpoly_nature'
119
121
): Promise<THREE.Object3D[]> {
120
120
-
const loader = new GLTFLoader();
121
121
-
const modelInfo = getModelPathsAndMaterials(name, collection);
122
122
+
const loader = new GLTFLoader();
123
123
+
const modelInfo = getModelPathsAndMaterials(name, collection);
122
124
123
123
-
if (!modelInfo) {
124
124
-
return [];
125
125
-
}
125
125
+
if (!modelInfo) {
126
126
+
return [];
127
127
+
}
126
128
127
127
-
const promises = modelInfo.filePaths.map((filePath) => {
128
128
-
return new Promise<THREE.Object3D>((resolve, reject) => {
129
129
-
loader.load(
130
130
-
filePath,
131
131
-
(gltf) => {
132
132
-
gltf.scene.userData = {
133
133
-
name,
134
134
-
path: filePath,
135
135
-
};
136
136
-
gltf.scene.traverse((child) => {
137
137
-
if (child instanceof THREE.Mesh) {
138
138
-
// child.castShadow = true;
129
129
+
const promises = modelInfo.filePaths.map((filePath) => {
130
130
+
return new Promise<THREE.Object3D>((resolve, reject) => {
131
131
+
loader.load(
132
132
+
filePath,
133
133
+
(gltf) => {
134
134
+
gltf.scene.userData = {
135
135
+
name,
136
136
+
path: filePath
137
137
+
};
138
138
+
gltf.scene.traverse((child) => {
139
139
+
if (child instanceof THREE.Mesh) {
139
140
child.receiveShadow = true;
141
141
+
child.castShadow = false;
140
142
}
141
141
-
});
142
142
-
resolve(gltf.scene);
143
143
-
},
144
144
-
undefined,
145
145
-
(error) => {
146
146
-
console.error(`Failed to load model ${filePath}:`, error);
147
147
-
reject(error);
148
148
-
},
149
149
-
);
150
150
-
});
151
151
-
});
143
143
+
});
144
144
+
resolve(gltf.scene);
145
145
+
},
146
146
+
undefined,
147
147
+
(error) => {
148
148
+
console.error(`Failed to load model ${filePath}:`, error);
149
149
+
reject(error);
150
150
+
}
151
151
+
);
152
152
+
});
153
153
+
});
152
154
153
153
-
return Promise.all(promises);
155
155
+
return Promise.all(promises);
154
156
}
···
1
1
import {
2
2
-
BufferGeometry,
3
3
-
Float32BufferAttribute,
4
4
-
Mesh,
5
5
-
MeshStandardMaterial,
6
6
-
Object3D,
7
7
-
Quaternion,
8
8
-
Vector3,
9
9
-
} from "three";
10
10
-
import { Biome, type BiomeOptions } from "./biome";
11
11
-
import { loadModels } from "./models";
2
2
+
BufferGeometry,
3
3
+
Float32BufferAttribute,
4
4
+
Mesh,
5
5
+
MeshStandardMaterial,
6
6
+
Object3D,
7
7
+
Quaternion,
8
8
+
Vector3,
9
9
+
IcosahedronGeometry
10
10
+
} from 'three';
11
11
+
import { Biome, type BiomeOptions } from './biome';
12
12
+
import { loadModels } from './models';
13
13
+
14
14
+
import { PlanetMaterialWithCaustics } from './materials/OceanCausticsMaterial';
15
15
+
import { createAtmosphereMaterial } from './materials/AtmosphereMaterial';
16
16
+
import { createBufferGeometry } from './helper/helper';
12
17
13
18
export type PlanetOptions = {
14
14
-
scatter?: number;
19
19
+
scatter?: number;
20
20
+
21
21
+
ground?: number;
22
22
+
23
23
+
detail?: number;
24
24
+
25
25
+
atmosphere?: {
26
26
+
enabled?: boolean;
27
27
+
color?: Vector3;
28
28
+
height?: number;
29
29
+
};
15
30
16
16
-
ground?: number;
31
31
+
material?: 'normal' | 'caustics';
17
32
18
18
-
detail?: number;
33
33
+
biome?: BiomeOptions;
19
34
};
20
35
21
36
export class Planet {
22
22
-
worker: Worker;
37
37
+
worker: Worker;
23
38
24
24
-
callbacks: Record<number, (data: Mesh) => void>;
25
25
-
requestId: number;
39
39
+
callbacks: Record<number, (data: Mesh) => void>;
40
40
+
requestId: number;
26
41
27
27
-
biome: Biome;
42
42
+
biome: Biome;
28
43
29
29
-
biomeOptions: BiomeOptions;
30
30
-
options: PlanetOptions;
44
44
+
biomeOptions: BiomeOptions;
45
45
+
options: PlanetOptions;
31
46
32
32
-
vegetationPositions?: Record<string, Vector3[]>;
47
47
+
vegetationPositions?: Record<string, Vector3[]>;
33
48
34
34
-
constructor(biomeOptions: BiomeOptions, planetOptions: PlanetOptions = {}) {
35
35
-
this.options = planetOptions;
49
49
+
constructor(options: PlanetOptions = {}) {
50
50
+
this.options = options;
36
51
37
37
-
this.biome = new Biome(biomeOptions);
38
38
-
this.biomeOptions = this.biome.options;
52
52
+
this.biome = new Biome(options.biome);
53
53
+
this.biomeOptions = this.biome.options;
39
54
40
40
-
this.worker = new Worker(new URL("worker.ts", import.meta.url), {
41
41
-
type: "module",
42
42
-
});
43
43
-
this.worker.onmessage = this.handleMessage.bind(this);
44
44
-
this.callbacks = {};
45
45
-
this.requestId = 0;
46
46
-
}
55
55
+
this.worker = new Worker(new URL('worker.ts', import.meta.url), {
56
56
+
type: 'module'
57
57
+
});
58
58
+
this.worker.onmessage = this.handleMessage.bind(this);
59
59
+
this.callbacks = {};
60
60
+
this.requestId = 0;
61
61
+
}
47
62
48
48
-
handleMessage(event: {
49
49
-
data: {
50
50
-
type: "geometry";
51
51
-
data: {
52
52
-
positions: number[];
53
53
-
colors: number[];
54
54
-
normals: number[];
55
55
-
oceanPositions: number[];
56
56
-
oceanColors: number[];
57
57
-
oceanNormals: number[];
58
58
-
vegetation: Record<string, Vector3[]>;
59
59
-
};
60
60
-
requestId: number;
61
61
-
};
62
62
-
}) {
63
63
-
const { type, data, requestId } = event.data;
63
63
+
handleMessage(event: {
64
64
+
data: {
65
65
+
type: 'geometry';
66
66
+
data: {
67
67
+
positions: number[];
68
68
+
colors: number[];
69
69
+
normals: number[];
70
70
+
oceanPositions: number[];
71
71
+
oceanColors: number[];
72
72
+
oceanNormals: number[];
73
73
+
vegetation: Record<string, Vector3[]>;
74
74
+
oceanMorphPositions: number[];
75
75
+
oceanMorphNormals: number[];
76
76
+
};
77
77
+
requestId: number;
78
78
+
};
79
79
+
}) {
80
80
+
const { data, requestId } = event.data;
64
81
65
65
-
const callback = this.callbacks[requestId];
66
66
-
if (!callback) {
67
67
-
console.error("No callback found for requestId:", requestId);
68
68
-
return;
69
69
-
}
82
82
+
const callback = this.callbacks[requestId];
83
83
+
if (!callback) {
84
84
+
console.error('No callback found for requestId:', requestId);
85
85
+
return;
86
86
+
}
70
87
71
71
-
if (type === "geometry") {
72
72
-
const geometry = new BufferGeometry();
73
73
-
const oceanGeometry = new BufferGeometry();
74
74
-
geometry.setAttribute(
75
75
-
"position",
76
76
-
new Float32BufferAttribute(new Float32Array(data.positions), 3),
77
77
-
);
78
78
-
geometry.setAttribute(
79
79
-
"color",
80
80
-
new Float32BufferAttribute(new Float32Array(data.colors), 3),
81
81
-
);
82
82
-
geometry.setAttribute(
83
83
-
"normal",
84
84
-
new Float32BufferAttribute(new Float32Array(data.normals), 3),
85
85
-
);
88
88
+
const geometry = createBufferGeometry(data.positions, data.colors, data.normals);
86
89
87
87
-
oceanGeometry.setAttribute(
88
88
-
"position",
89
89
-
new Float32BufferAttribute(new Float32Array(data.oceanPositions), 3),
90
90
-
);
91
91
-
oceanGeometry.setAttribute(
92
92
-
"color",
93
93
-
new Float32BufferAttribute(new Float32Array(data.oceanColors), 3),
94
94
-
);
90
90
+
const oceanGeometry = createBufferGeometry(
91
91
+
data.oceanPositions,
92
92
+
data.oceanColors,
93
93
+
data.oceanNormals
94
94
+
);
95
95
96
96
-
oceanGeometry.computeVertexNormals();
96
96
+
oceanGeometry.morphAttributes.position = [
97
97
+
new Float32BufferAttribute(data.oceanMorphPositions, 3)
98
98
+
];
99
99
+
oceanGeometry.morphAttributes.normal = [new Float32BufferAttribute(data.oceanMorphNormals, 3)];
97
100
98
98
-
this.vegetationPositions = data.vegetation;
101
101
+
this.vegetationPositions = data.vegetation;
99
102
100
100
-
const planetMesh = new Mesh(
101
101
-
geometry,
102
102
-
new MeshStandardMaterial({
103
103
-
vertexColors: true,
104
104
-
}),
105
105
-
);
106
106
-
planetMesh.castShadow = true;
103
103
+
const materialOptions = { vertexColors: true };
107
104
108
108
-
const oceanMesh = new Mesh(
109
109
-
oceanGeometry,
110
110
-
new MeshStandardMaterial({
111
111
-
vertexColors: true,
112
112
-
transparent: true,
113
113
-
opacity: 0.7,
114
114
-
metalness: 0.5,
115
115
-
roughness: 0.5,
116
116
-
}),
117
117
-
);
105
105
+
const material =
106
106
+
this.options.material === 'caustics'
107
107
+
? new PlanetMaterialWithCaustics(materialOptions)
108
108
+
: new MeshStandardMaterial(materialOptions);
109
109
+
110
110
+
const planetMesh = new Mesh(geometry, material);
111
111
+
planetMesh.castShadow = true;
112
112
+
113
113
+
if (this.options.material === 'caustics') {
114
114
+
planetMesh.onBeforeRender = (renderer, scene, camera, geometry, material) => {
115
115
+
if (material instanceof PlanetMaterialWithCaustics) {
116
116
+
material.update();
117
117
+
}
118
118
+
};
119
119
+
}
120
120
+
121
121
+
const oceanMesh = new Mesh(
122
122
+
oceanGeometry,
123
123
+
new MeshStandardMaterial({
124
124
+
vertexColors: true,
125
125
+
transparent: true,
126
126
+
opacity: 0.7,
127
127
+
metalness: 0.5,
128
128
+
roughness: 0.5
129
129
+
})
130
130
+
);
131
131
+
132
132
+
planetMesh.add(oceanMesh);
133
133
+
oceanMesh.onBeforeRender = (renderer, scene, camera, geometry, material) => {
134
134
+
// update morph targets
135
135
+
if (oceanMesh.morphTargetInfluences)
136
136
+
oceanMesh.morphTargetInfluences[0] = Math.sin(performance.now() / 1000) * 0.5 + 0.5;
137
137
+
};
138
138
+
139
139
+
if (this.options.atmosphere?.enabled !== false) {
140
140
+
this.addAtmosphere(planetMesh);
141
141
+
}
142
142
+
callback(planetMesh);
118
143
119
119
-
planetMesh.add(oceanMesh);
120
120
-
callback(planetMesh);
121
121
-
}
144
144
+
delete this.callbacks[requestId];
145
145
+
}
122
146
123
123
-
delete this.callbacks[requestId];
124
124
-
}
147
147
+
async create(): Promise<Mesh> {
148
148
+
// let collection = "stylized_nature";
125
149
126
126
-
async create(): Promise<Mesh> {
127
127
-
const models = this.biomeOptions.vegetation?.items.map((item) => {
128
128
-
return item.name;
129
129
-
});
150
150
+
const models = this.biomeOptions.vegetation?.items.map((item) => {
151
151
+
return item.name;
152
152
+
});
130
153
131
131
-
const loaded: Promise<Object3D[] | Mesh>[] = [];
154
154
+
const loaded: Promise<Object3D[] | Mesh>[] = [];
132
155
133
133
-
for (const model of models ?? []) {
134
134
-
const loadedModels = loadModels(model);
135
135
-
loaded.push(loadedModels);
136
136
-
}
156
156
+
for (const model of models ?? []) {
157
157
+
const loadedModels = loadModels(model); //, collection);
158
158
+
loaded.push(loadedModels);
159
159
+
}
137
160
138
138
-
const planetPromise = this.createMesh();
139
139
-
loaded.push(planetPromise);
161
161
+
const planetPromise = this.createMesh();
162
162
+
loaded.push(planetPromise);
140
163
141
141
-
await Promise.all(loaded);
164
164
+
await Promise.all(loaded);
142
165
143
143
-
const planet = await planetPromise;
166
166
+
const planet = await planetPromise;
144
167
145
145
-
for (let i = 0; i < loaded.length - 1; i++) {
146
146
-
const models = await loaded[i];
147
147
-
const name = models[0].userData.name;
168
168
+
for (let i = 0; i < loaded.length - 1; i++) {
169
169
+
const models = (await loaded[i]) as Object3D[];
170
170
+
const name = models[0].userData.name;
148
171
149
149
-
const positions = this.vegetationPositions?.[name];
172
172
+
const positions = this.vegetationPositions?.[name];
150
173
151
151
-
if (!positions) continue;
174
174
+
if (!positions) continue;
152
175
153
153
-
const item = this.biomeOptions.vegetation?.items[i];
176
176
+
let item = this.biomeOptions.vegetation?.items[i];
154
177
155
178
for (const position of positions) {
156
179
const model = models[Math.floor(Math.random() * models.length)].clone();
···
158
181
this.updatePosition(model, new Vector3(position.x, position.y, position.z));
159
182
model.scale.setScalar(0.04);
160
183
161
161
-
model.traverse((child: Object3D) => {
184
184
+
model.traverse((child) => {
162
185
if (child instanceof Mesh) {
163
163
-
const color = item?.colors?.[child.material.name];
186
186
+
let color = item?.colors?.[child.material.name];
164
187
if (color?.array) {
165
165
-
const randomColor = color.array[Math.floor(Math.random() * color.array.length)];
188
188
+
let randomColor = color.array[Math.floor(Math.random() * color.array.length)];
166
189
child.material.color.setHex(randomColor);
167
190
}
168
191
···
170
193
child.material.roughness = 0.2;
171
194
child.material.color.setHex(0xffffff);
172
195
}
196
196
+
child.castShadow = false;
197
197
+
child.receiveShadow = true;
173
198
}
174
199
});
175
200
planet.add(model);
176
201
}
177
177
-
}
202
202
+
}
178
203
179
179
-
return planetPromise;
180
180
-
}
204
204
+
return planetPromise;
205
205
+
}
181
206
182
182
-
createMesh(): Promise<Mesh> {
183
183
-
return new Promise((resolve) => {
184
184
-
const requestId = this.requestId++;
185
185
-
this.callbacks[requestId] = resolve;
207
207
+
async createMesh(): Promise<Mesh> {
208
208
+
return new Promise((resolve) => {
209
209
+
const requestId = this.requestId++;
210
210
+
this.callbacks[requestId] = resolve;
186
211
187
187
-
this.worker.postMessage({
188
188
-
type: "createGeometry",
189
189
-
requestId,
190
190
-
data: {
191
191
-
biomeOptions: this.biome.options,
192
192
-
planetOptions: this.options,
193
193
-
},
194
194
-
});
195
195
-
});
196
196
-
}
212
212
+
this.worker.postMessage({
213
213
+
type: 'createGeometry',
214
214
+
requestId,
215
215
+
data: this.options
216
216
+
});
217
217
+
});
218
218
+
}
219
219
+
220
220
+
addAtmosphere(planet: Mesh) {
221
221
+
// Create the atmosphere geometry
222
222
+
const atmosphereGeometry = new IcosahedronGeometry(
223
223
+
this.options.atmosphere?.height ?? 1.2,
224
224
+
this.options.detail ?? 20
225
225
+
);
226
226
+
const atmosphere = new Mesh(
227
227
+
atmosphereGeometry,
228
228
+
createAtmosphereMaterial(this.options.atmosphere?.color, new Vector3(5, 2, 2))
229
229
+
);
230
230
+
atmosphere.renderOrder = 1;
231
231
+
planet.add(atmosphere);
232
232
+
}
197
233
198
198
-
updatePosition(item: Object3D, pos: Vector3) {
199
199
-
item.position.copy(pos);
234
234
+
updatePosition(item: Object3D, pos: Vector3) {
235
235
+
item.position.copy(pos);
200
236
201
201
-
const currentRotation = new Quaternion();
202
202
-
const a = item.up.clone().normalize();
203
203
-
const b = pos.clone().normalize();
237
237
+
const currentRotation = new Quaternion();
238
238
+
const a = item.up.clone().normalize();
239
239
+
const b = pos.clone().normalize();
204
240
205
205
-
currentRotation.setFromUnitVectors(a, b);
241
241
+
currentRotation.setFromUnitVectors(a, b);
206
242
207
207
-
item.quaternion.copy(currentRotation);
243
243
+
item.quaternion.copy(currentRotation);
208
244
209
209
-
item.up = b;
210
210
-
}
245
245
+
item.up = b;
246
246
+
}
211
247
}
···
1
1
import { type BiomeOptions } from './biome';
2
2
+
import { type PlanetOptions } from './planet';
2
3
3
3
-
const beach: BiomeOptions = {
4
4
+
const beachBiome: BiomeOptions = {
4
5
noise: {
5
6
min: -0.05,
6
7
max: 0.05,
···
13
14
},
14
15
warp: 0.3,
15
16
scale: 1,
16
16
-
power: 1.5
17
17
+
power: 1.3
17
18
},
18
19
19
20
colors: [
···
25
26
26
27
seaColors: [
27
28
[-1, 0x000066],
28
28
-
[-0.52, 0x0000aa],
29
29
+
[-0.55, 0x0000aa],
29
30
[-0.1, 0x00f2e5]
30
31
],
31
32
seaNoise: {
32
32
-
min: -0.005,
33
33
-
max: 0.005,
34
34
-
scale: 5
33
33
+
min: -0.008,
34
34
+
max: 0.008,
35
35
+
scale: 6
35
36
},
36
37
37
38
vegetation: {
38
39
items: [
39
40
{
40
40
-
name: 'PalmTree',
41
41
+
name: 'Rock',
41
42
density: 50,
42
43
minimumHeight: 0.1,
43
44
colors: {
44
44
-
Brown: { array: [0x8b4513, 0x5b3105] },
45
45
-
Green: { array: [0x22851e, 0x22a51e] },
46
46
-
DarkGreen: { array: [0x006400] }
45
45
+
Gray: { array: [0x775544] }
47
46
}
48
47
},
49
48
{
50
50
-
name: 'Rock',
51
51
-
density: 10,
49
49
+
name: 'PalmTree',
50
50
+
density: 50,
52
51
minimumHeight: 0.1,
53
52
colors: {
54
54
-
Gray: { array: [0x775544] }
53
53
+
Brown: { array: [0x8b4513, 0x5b3105] },
54
54
+
Green: { array: [0x22851e, 0x22a51e] },
55
55
+
DarkGreen: { array: [0x006400] }
56
56
+
},
57
57
+
ground: {
58
58
+
color: 0x229900,
59
59
+
radius: 0.1,
60
60
+
raise: 0.01
55
61
}
56
62
}
57
63
]
58
64
}
59
65
};
60
66
61
61
-
const forest: BiomeOptions = {
67
67
+
const forestBiome: BiomeOptions = {
62
68
noise: {
63
69
min: -0.05,
64
70
max: 0.05,
···
154
160
}
155
161
};
156
162
157
157
-
const snowForest: BiomeOptions = {
163
163
+
const snowForestBiome: BiomeOptions = {
158
164
noise: {
159
165
min: -0.05,
160
166
max: 0.05,
···
185
191
[-0.1, 0xaaccff]
186
192
],
187
193
seaNoise: {
188
188
-
min: -0.005,
189
189
-
max: 0.005,
194
194
+
min: -0.0,
195
195
+
max: 0.001,
190
196
scale: 5
191
197
},
192
198
···
251
257
};
252
258
253
259
export const biomePresets: Record<string, BiomeOptions> = {
254
254
-
beach,
255
255
-
forest,
256
256
-
snowForest
260
260
+
beach: beachBiome,
261
261
+
forest: forestBiome,
262
262
+
snowForest: snowForestBiome
263
263
+
};
264
264
+
265
265
+
const beachPlanet: PlanetOptions = {
266
266
+
biome: {
267
267
+
preset: 'beach'
268
268
+
},
269
269
+
270
270
+
material: 'caustics'
271
271
+
};
272
272
+
273
273
+
const forestPlanet: PlanetOptions = {
274
274
+
biome: {
275
275
+
preset: 'forest'
276
276
+
},
277
277
+
278
278
+
material: 'normal'
279
279
+
};
280
280
+
281
281
+
const snowForestPlanet: PlanetOptions = {
282
282
+
biome: {
283
283
+
preset: 'snowForest'
284
284
+
}
285
285
+
};
286
286
+
287
287
+
export const planetPresets: Record<string, PlanetOptions> = {
288
288
+
beach: beachPlanet,
289
289
+
forest: forestPlanet,
290
290
+
snowForest: snowForestPlanet
257
291
};
···
1
1
+
import * as THREE from "three";
2
2
+
3
3
+
export type StarsOptions = {
4
4
+
particleCount: number;
5
5
+
minimumDistance: number;
6
6
+
maximumDistance: number;
7
7
+
};
8
8
+
9
9
+
export class Stars extends THREE.Group {
10
10
+
private particleCount: number = 5000;
11
11
+
12
12
+
private minimumDistance: number = 10;
13
13
+
private maximumDistance: number = 20;
14
14
+
15
15
+
private positions: Float32Array;
16
16
+
private colors: Float32Array;
17
17
+
private geometry: THREE.BufferGeometry;
18
18
+
private material: THREE.PointsMaterial;
19
19
+
private particles: THREE.Points;
20
20
+
21
21
+
private tempVector = new THREE.Vector3();
22
22
+
23
23
+
constructor(opts: Partial<StarsOptions> = {}) {
24
24
+
super();
25
25
+
26
26
+
this.particleCount = opts.particleCount ?? this.particleCount;
27
27
+
this.minimumDistance = opts.minimumDistance ?? this.minimumDistance;
28
28
+
this.maximumDistance = opts.maximumDistance ?? this.maximumDistance;
29
29
+
30
30
+
this.positions = new Float32Array(this.particleCount * 3);
31
31
+
this.colors = new Float32Array(this.particleCount * 3);
32
32
+
33
33
+
this.setupParticles();
34
34
+
}
35
35
+
36
36
+
private setupParticles() {
37
37
+
for (let i = 0; i < this.particleCount; i++) {
38
38
+
this.resetParticle(i);
39
39
+
}
40
40
+
41
41
+
this.geometry = new THREE.BufferGeometry();
42
42
+
this.geometry.setAttribute(
43
43
+
"position",
44
44
+
new THREE.BufferAttribute(this.positions, 3),
45
45
+
);
46
46
+
this.geometry.setAttribute(
47
47
+
"color",
48
48
+
new THREE.BufferAttribute(this.colors, 3),
49
49
+
);
50
50
+
51
51
+
this.material = new THREE.PointsMaterial({
52
52
+
size: 0.04,
53
53
+
vertexColors: true,
54
54
+
});
55
55
+
56
56
+
this.particles = new THREE.Points(this.geometry, this.material);
57
57
+
58
58
+
this.add(this.particles);
59
59
+
}
60
60
+
61
61
+
private resetParticle(i: number) {
62
62
+
const index = i * 3;
63
63
+
64
64
+
const distance =
65
65
+
Math.random() * (this.maximumDistance - this.minimumDistance) +
66
66
+
this.minimumDistance;
67
67
+
this.tempVector.randomDirection().multiplyScalar(distance);
68
68
+
69
69
+
this.positions[index] = this.tempVector.x;
70
70
+
this.positions[index + 1] = this.tempVector.y;
71
71
+
this.positions[index + 2] = this.tempVector.z;
72
72
+
73
73
+
this.colors[index] = Math.random() < 0.2 ? 0.3 : 1.0;
74
74
+
this.colors[index + 1] = Math.random() < 0.2 ? 0.3 : 1.0;
75
75
+
this.colors[index + 2] = Math.random() < 0.2 ? 0.3 : 1.0;
76
76
+
}
77
77
+
}
···
1
1
+
import { Vector3 } from "three";
2
2
+
3
3
+
export type VertexInfo = {
4
4
+
height: number;
5
5
+
scatter: Vector3;
6
6
+
7
7
+
seaHeight: number;
8
8
+
seaMorph: number;
9
9
+
};
···
1
1
-
import { IcosahedronGeometry, Vector3, BufferAttribute } from "three";
1
1
+
import {
2
2
+
IcosahedronGeometry,
3
3
+
Vector3,
4
4
+
BufferAttribute,
5
5
+
Float32BufferAttribute,
6
6
+
Color,
7
7
+
} from "three";
2
8
3
3
-
import { Biome, type BiomeOptions } from "./biome";
9
9
+
import { Biome } from "./biome";
4
10
import { type PlanetOptions } from "./planet";
5
5
-
import UberNoise from 'uber-noise';
11
11
+
import UberNoise from "uber-noise";
12
12
+
import { type VertexInfo } from "./types";
6
13
7
14
onmessage = function (e) {
8
15
const { type, data, requestId } = e.data;
···
17
24
const oceanPositions = oceanGeometry.getAttribute("position").array.buffer;
18
25
const oceanColors = oceanGeometry.getAttribute("color").array.buffer;
19
26
const oceanNormals = oceanGeometry.getAttribute("normal").array.buffer;
27
27
+
const oceanMorphPositions =
28
28
+
oceanGeometry.morphAttributes.position[0].array.buffer;
29
29
+
const oceanMorphNormals =
30
30
+
oceanGeometry.morphAttributes.normal[0].array.buffer;
20
31
21
32
postMessage(
22
33
{
···
29
40
oceanColors,
30
41
oceanNormals,
31
42
vegetation,
43
43
+
oceanMorphPositions,
44
44
+
oceanMorphNormals,
32
45
},
33
46
requestId,
34
47
},
35
48
// @ts-expect-error - hmm
36
36
-
[positions, colors, normals, oceanPositions, oceanColors, oceanNormals],
49
49
+
[
50
50
+
positions,
51
51
+
colors,
52
52
+
normals,
53
53
+
oceanPositions,
54
54
+
oceanColors,
55
55
+
oceanNormals,
56
56
+
oceanMorphPositions,
57
57
+
oceanMorphNormals,
58
58
+
],
37
59
);
38
60
} else {
39
61
console.error("Unknown message type", type);
40
62
}
41
63
};
42
64
43
43
-
function createGeometry({
44
44
-
biomeOptions,
45
45
-
planetOptions,
46
46
-
}: {
47
47
-
biomeOptions: BiomeOptions;
48
48
-
planetOptions: PlanetOptions;
49
49
-
}): [IcosahedronGeometry, IcosahedronGeometry, Record<string, Vector3[]>] {
65
65
+
function createGeometry(
66
66
+
planetOptions: PlanetOptions,
67
67
+
): [IcosahedronGeometry, IcosahedronGeometry, Record<string, Vector3[]>] {
50
68
const sphere = new IcosahedronGeometry(1, planetOptions.detail ?? 50);
51
69
const oceanSphere = new IcosahedronGeometry(1, planetOptions.detail ?? 50);
52
70
53
53
-
const biome = new Biome(biomeOptions);
71
71
+
const biome = new Biome(planetOptions.biome);
54
72
55
73
const vertices = sphere.getAttribute("position");
56
74
const oceanVertices = oceanSphere.getAttribute("position");
···
58
76
const faceSize = (Math.PI * 4) / faceCount;
59
77
console.log("faces:", faceCount);
60
78
61
61
-
const calculatedVertices = new Map<
62
62
-
string,
63
63
-
{
64
64
-
height: number;
65
65
-
seaHeight: number;
66
66
-
scatter: Vector3;
67
67
-
}
68
68
-
>();
79
79
+
// store calculated vertices so we don't have to recalculate them
80
80
+
// once store by hashed position (so we can find vertices of different faces that have the same position)
81
81
+
const calculatedVertices = new Map<string, VertexInfo>();
82
82
+
// and once by index for vegetation placement
83
83
+
const calculatedVerticesArray: VertexInfo[] = new Array(faceCount);
69
84
70
85
const colors = new Float32Array(vertices.count * 3);
71
86
const oceanColors = new Float32Array(oceanVertices.count * 3);
···
83
98
a.fromBufferAttribute(vertices, 0);
84
99
b.fromBufferAttribute(vertices, 1);
85
100
86
86
-
// default to scatter = distance of first edge
87
87
-
const scatterAmount = planetOptions.scatter ?? b.distanceTo(a);
101
101
+
const faceSideLength = a.distanceTo(b);
102
102
+
103
103
+
// scatterAmount is based on side length of face (all faces have the same size)
104
104
+
const scatterAmount = (planetOptions.scatter ?? 1.2) * faceSideLength;
88
105
const scatterScale = 100;
89
106
90
107
const scatterNoise = new UberNoise({
···
94
111
seed: 0,
95
112
});
96
113
114
114
+
oceanSphere.morphAttributes.position = [];
115
115
+
oceanSphere.morphAttributes.normal = [];
116
116
+
117
117
+
const oceanMorphPositions: number[] = [];
118
118
+
const oceanMorphNormals: number[] = [];
119
119
+
120
120
+
const oceanA = new Vector3(),
121
121
+
oceanB = new Vector3(),
122
122
+
oceanC = new Vector3(),
123
123
+
oceanD = new Vector3(),
124
124
+
oceanE = new Vector3(),
125
125
+
oceanF = new Vector3();
126
126
+
127
127
+
const temp = new Vector3();
128
128
+
129
129
+
// go through all faces
130
130
+
// - calculate height and scatter for vertices
131
131
+
// - calculate height for ocean vertices
132
132
+
// - calculate height for ocean morph vertices
133
133
+
// - calculate color for vertices and ocean vertices
134
134
+
// - calculate normal for vertices and ocean vertices
135
135
+
// - add vegetation
97
136
for (let i = 0; i < vertices.count; i += 3) {
98
137
a.fromBufferAttribute(vertices, i);
99
138
b.fromBufferAttribute(vertices, i + 1);
100
139
c.fromBufferAttribute(vertices, i + 2);
101
140
141
141
+
oceanA.fromBufferAttribute(oceanVertices, i);
142
142
+
oceanB.fromBufferAttribute(oceanVertices, i + 1);
143
143
+
oceanC.fromBufferAttribute(oceanVertices, i + 2);
144
144
+
102
145
mid.set(0, 0, 0);
103
146
mid.addVectors(a, b).add(c).divideScalar(3);
104
147
105
148
let normalizedHeight = 0;
106
149
150
150
+
// go through all vertices of the face
107
151
for (let j = 0; j < 3; j++) {
108
152
let v = a;
109
153
if (j === 1) v = b;
110
154
if (j === 2) v = c;
111
111
-
const key = `${v.x.toFixed(5)},${v.y.toFixed(5)},${v.z.toFixed(5)}`;
112
155
156
156
+
// lets see if we already have info for this vertex
157
157
+
const key = `${v.x.toFixed(5)},${v.y.toFixed(5)},${v.z.toFixed(5)}`;
113
158
let move = calculatedVertices.get(key);
114
159
160
160
+
// if not, calculate it
115
161
if (!move) {
162
162
+
// calculate height and scatter
116
163
const height = biome.getHeight(v) + 1;
117
164
const scatterX = scatterNoise.get(v);
118
165
const scatterY = scatterNoise.get(
···
125
172
v.x + scatterScale * 200,
126
173
v.y - scatterScale * 200,
127
174
);
175
175
+
// calculate sea height and sea morph height
128
176
const seaHeight = biome.getSeaHeight(v) + 1;
177
177
+
const secondSeaHeight = biome.getSeaHeight(v.addScalar(100)) + 1;
178
178
+
179
179
+
v.subScalar(100);
129
180
130
181
move = {
131
182
height,
132
183
scatter: new Vector3(scatterX, scatterY, scatterZ),
133
184
seaHeight,
185
185
+
seaMorph: secondSeaHeight,
134
186
};
135
187
calculatedVertices.set(key, move);
136
188
}
137
189
190
190
+
// we store this info for later use (vegetation placement)
191
191
+
calculatedVerticesArray[i + j] = move;
192
192
+
193
193
+
// we add height here so we can calculate the average normalized height of the face later
138
194
normalizedHeight += move.height - 1;
195
195
+
196
196
+
// move vertex based on height and scatter
139
197
v.add(move.scatter).normalize().multiplyScalar(move.height);
140
140
-
141
198
vertices.setXYZ(i + j, v.x, v.y, v.z);
142
199
143
143
-
const oceanV = v.clone().normalize().multiplyScalar(move.seaHeight);
144
144
-
oceanVertices.setXYZ(i + j, oceanV.x, oceanV.y, oceanV.z);
145
145
-
}
146
146
-
147
147
-
normalizedHeight /= 3;
148
148
-
normalizedHeight = (normalizedHeight - biome.min) / (biome.max - biome.min);
149
149
-
normalizedHeight = normalizedHeight * 2 - 1;
150
150
-
// now averageHeight is between -1 and 1 (0 is sea level)
151
151
-
152
152
-
for (
153
153
-
let j = 0;
154
154
-
biome.options.vegetation && j < biome.options.vegetation.items.length;
155
155
-
j++
156
156
-
) {
157
157
-
const vegetation = biome.options.vegetation.items[j];
158
158
-
if (Math.random() < faceSize * vegetation.density) {
159
159
-
if (
160
160
-
vegetation.minimumHeight !== undefined &&
161
161
-
normalizedHeight < vegetation.minimumHeight
162
162
-
) {
163
163
-
continue;
164
164
-
}
165
165
-
166
166
-
if (vegetation.minimumHeight === undefined && normalizedHeight < 0) {
167
167
-
continue;
168
168
-
}
200
200
+
// move ocean vertex based on sea height and scatter
201
201
+
let oceanV = oceanA;
202
202
+
if (j === 1) oceanV = oceanB;
203
203
+
if (j === 2) oceanV = oceanC;
204
204
+
oceanV.add(move.scatter).normalize().multiplyScalar(move.seaMorph);
205
205
+
oceanMorphPositions.push(oceanV.x, oceanV.y, oceanV.z);
169
206
170
170
-
if (
171
171
-
vegetation.maximumHeight !== undefined &&
172
172
-
normalizedHeight > vegetation.maximumHeight
173
173
-
) {
174
174
-
continue;
175
175
-
}
176
176
-
if (!placedVegetation[vegetation.name]) {
177
177
-
placedVegetation[vegetation.name] = [];
178
178
-
}
179
179
-
placedVegetation[vegetation.name].push(a.clone());
180
180
-
break;
207
207
+
// move ocean morph vertex based on sea height and scatter
208
208
+
if (j === 0) {
209
209
+
oceanD.copy(oceanV);
210
210
+
} else if (j === 1) {
211
211
+
oceanE.copy(oceanV);
212
212
+
} else if (j === 2) {
213
213
+
oceanF.copy(oceanV);
181
214
}
215
215
+
oceanV.normalize().multiplyScalar(move.seaHeight);
216
216
+
oceanVertices.setXYZ(i + j, oceanV.x, oceanV.y, oceanV.z);
182
217
}
183
218
184
184
-
// calculate new normal
185
185
-
const normal = new Vector3();
186
186
-
normal.crossVectors(b.clone().sub(a), c.clone().sub(a)).normalize();
219
219
+
// calculate normalized height for the face (between -1 and 1, 0 is sea level)
220
220
+
normalizedHeight /= 3;
221
221
+
normalizedHeight =
222
222
+
Math.min(-normalizedHeight / biome.min, 0) +
223
223
+
Math.max(normalizedHeight / biome.max, 0);
224
224
+
// now normalizedHeight should be between -1 and 1 (0 is sea level)
225
225
+
// this will be used for color calculation and vegetation placement
187
226
227
227
+
// calculate face normal
228
228
+
temp.crossVectors(b.clone().sub(a), c.clone().sub(a)).normalize();
188
229
// flat shading, so all normals for the face are the same
189
189
-
normals.setXYZ(i, normal.x, normal.y, normal.z);
190
190
-
normals.setXYZ(i + 1, normal.x, normal.y, normal.z);
191
191
-
normals.setXYZ(i + 2, normal.x, normal.y, normal.z);
230
230
+
normals.setXYZ(i, temp.x, temp.y, temp.z);
231
231
+
normals.setXYZ(i + 1, temp.x, temp.y, temp.z);
232
232
+
normals.setXYZ(i + 2, temp.x, temp.y, temp.z);
192
233
193
234
// calculate steepness (acos of dot product of normal and up vector)
194
235
// (up vector = old mid point on sphere)
195
195
-
const steepness = Math.acos(Math.abs(normal.dot(mid)));
236
236
+
const steepness = Math.acos(Math.abs(temp.dot(mid)));
196
237
// steepness is between 0 and PI/2
238
238
+
// this will be used for color calculation and vegetation placement
197
239
240
240
+
// calculate color for face
198
241
const color = biome.getColor(mid, normalizedHeight, steepness);
199
199
-
200
242
// flat shading, so all colors for the face are the same
201
243
if (color) {
202
244
colors[i * 3] = color.r;
···
212
254
colors[i * 3 + 8] = color.b;
213
255
}
214
256
215
215
-
// calculate ocean vertices
257
257
+
// calculate ocean face color
216
258
const oceanColor = biome.getSeaColor(mid, normalizedHeight);
217
259
218
260
if (oceanColor) {
···
229
271
oceanColors[i * 3 + 8] = oceanColor.b;
230
272
}
231
273
232
232
-
oceanNormals.setXYZ(i, mid.x, mid.y, mid.z);
233
233
-
oceanNormals.setXYZ(i + 1, mid.x, mid.y, mid.z);
234
234
-
oceanNormals.setXYZ(i + 2, mid.x, mid.y, mid.z);
274
274
+
// calculate ocean normals
275
275
+
temp
276
276
+
.crossVectors(oceanB.clone().sub(oceanA), oceanC.clone().sub(oceanA))
277
277
+
.normalize();
278
278
+
oceanNormals.setXYZ(i, temp.x, temp.y, temp.z);
279
279
+
oceanNormals.setXYZ(i + 1, temp.x, temp.y, temp.z);
280
280
+
oceanNormals.setXYZ(i + 2, temp.x, temp.y, temp.z);
281
281
+
282
282
+
// calculate ocean morph normals
283
283
+
temp
284
284
+
.crossVectors(oceanE.clone().sub(oceanD), oceanF.clone().sub(oceanD))
285
285
+
.normalize();
286
286
+
oceanMorphNormals.push(temp.x, temp.y, temp.z);
287
287
+
oceanMorphNormals.push(temp.x, temp.y, temp.z);
288
288
+
oceanMorphNormals.push(temp.x, temp.y, temp.z);
289
289
+
290
290
+
// place vegetation
291
291
+
for (
292
292
+
let j = 0;
293
293
+
biome.options.vegetation && j < biome.options.vegetation.items.length;
294
294
+
j++
295
295
+
) {
296
296
+
const vegetation = biome.options.vegetation.items[j];
297
297
+
if (Math.random() < faceSize * (vegetation.density ?? 1)) {
298
298
+
// discard if point is below or above height limits
299
299
+
if (
300
300
+
vegetation.minimumHeight !== undefined &&
301
301
+
normalizedHeight < vegetation.minimumHeight
302
302
+
) {
303
303
+
continue;
304
304
+
}
305
305
+
// default minimumHeight is 0 (= above sea level)
306
306
+
if (vegetation.minimumHeight === undefined && normalizedHeight < 0) {
307
307
+
continue;
308
308
+
}
309
309
+
if (
310
310
+
vegetation.maximumHeight !== undefined &&
311
311
+
normalizedHeight > vegetation.maximumHeight
312
312
+
) {
313
313
+
continue;
314
314
+
}
315
315
+
316
316
+
// discard if point is below or above slope limits
317
317
+
if (
318
318
+
vegetation.minimumSlope !== undefined &&
319
319
+
steepness < vegetation.minimumSlope
320
320
+
) {
321
321
+
continue;
322
322
+
}
323
323
+
if (
324
324
+
vegetation.maximumSlope !== undefined &&
325
325
+
steepness > vegetation.maximumSlope
326
326
+
) {
327
327
+
continue;
328
328
+
}
329
329
+
330
330
+
if (!placedVegetation[vegetation.name]) {
331
331
+
placedVegetation[vegetation.name] = [];
332
332
+
}
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
+
);
340
340
+
341
341
+
biome.addVegetation(
342
342
+
vegetation,
343
343
+
a.normalize(),
344
344
+
normalizedHeight,
345
345
+
steepness,
346
346
+
);
347
347
+
break;
348
348
+
}
349
349
+
}
235
350
}
351
351
+
352
352
+
const maxDist = 0.14;
353
353
+
354
354
+
const color = new Color();
355
355
+
356
356
+
// go through all vertices again and update height and color based on vegetation
357
357
+
for (let i = 0; i < vertices.count; i += 3) {
358
358
+
a.fromBufferAttribute(vertices, i);
359
359
+
a.normalize();
360
360
+
b.fromBufferAttribute(vertices, i + 1);
361
361
+
b.normalize();
362
362
+
c.fromBufferAttribute(vertices, i + 2);
363
363
+
c.normalize();
364
364
+
365
365
+
color.setRGB(colors[i * 3], colors[i * 3 + 1], colors[i * 3 + 2]);
366
366
+
367
367
+
const output = biome.vegetationHeightAndColorForFace(
368
368
+
a,
369
369
+
b,
370
370
+
c,
371
371
+
color,
372
372
+
faceSideLength,
373
373
+
);
374
374
+
375
375
+
const moveDataA = calculatedVerticesArray[i];
376
376
+
const moveDataB = calculatedVerticesArray[i + 1];
377
377
+
const moveDataC = calculatedVerticesArray[i + 2];
378
378
+
379
379
+
// 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);
383
383
+
384
384
+
vertices.setXYZ(i, a.x, a.y, a.z);
385
385
+
vertices.setXYZ(i + 1, b.x, b.y, b.z);
386
386
+
vertices.setXYZ(i + 2, c.x, c.y, c.z);
387
387
+
388
388
+
// update color based on vegetation
389
389
+
colors[i * 3] = output.color.r;
390
390
+
colors[i * 3 + 1] = output.color.g;
391
391
+
colors[i * 3 + 2] = output.color.b;
392
392
+
393
393
+
colors[i * 3 + 3] = output.color.r;
394
394
+
colors[i * 3 + 4] = output.color.g;
395
395
+
colors[i * 3 + 5] = output.color.b;
396
396
+
397
397
+
colors[i * 3 + 6] = output.color.r;
398
398
+
colors[i * 3 + 7] = output.color.g;
399
399
+
colors[i * 3 + 8] = output.color.b;
400
400
+
}
401
401
+
402
402
+
oceanSphere.morphAttributes.position[0] = new Float32BufferAttribute(
403
403
+
oceanMorphPositions,
404
404
+
3,
405
405
+
);
406
406
+
oceanSphere.morphAttributes.normal[0] = new Float32BufferAttribute(
407
407
+
oceanMorphNormals,
408
408
+
3,
409
409
+
);
236
410
237
411
sphere.setAttribute("color", new BufferAttribute(colors, 3));
238
412
oceanSphere.setAttribute("color", new BufferAttribute(oceanColors, 3));