···
12
12
- [ ] different materials
13
13
- [ ] make vegetation sway in the wind
14
14
- [ ] make all random stuff dependent on seed
15
15
+
- [ ] instanced vegetation
15
16
16
17
### weather
17
18
- [ ] clouds
···
45
46
- [ ] tilt shift
46
47
47
48
## other
48
48
-
- [ ]
49
49
+
- [ ] walking on planet
···
1
1
<!doctype html>
2
2
<html lang="en">
3
3
+
3
4
<head>
4
4
-
<title>three.js scaffold</title>
5
5
+
<script>
6
6
+
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
7
7
+
posthog.init('phc_1Q4q6SdTwvStnxFWbmdOIusLc5ve0u6Fk7WpsHPoAlD',{api_host:'https://eu.i.posthog.com', person_profiles: 'identified_only', persistence: 'memory'})
8
8
+
</script>
9
9
+
10
10
+
<title>tiny planets</title>
5
11
<meta charset="utf-8" />
6
12
<meta
7
13
name="viewport"
8
14
content="width=device-width, initial-scale=1.0, user-scalable=no"
9
15
/>
10
16
17
17
+
<link
18
18
+
rel="icon"
19
19
+
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🌎</text></svg>"
20
20
+
/>
21
21
+
11
22
<link rel="stylesheet" href="./index.css" />
12
23
</head>
13
24
···
20
31
</button>
21
32
22
33
<canvas id="root"></canvas>
34
34
+
35
35
+
<a class="absolute bottom-2 left-3 text-cyan-400 text-sm font-semibold hover:text-cyan-300 transition-colors duration-100" href="https://flo-bit.dev" target="_blank">made by flo-bit</a>
23
36
24
37
<script src="src/script.ts" type="module"></script>
25
38
</body>
···
2
2
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
3
3
import { Planet } from "./worlds/planet";
4
4
import { Stars } from "./worlds/stars";
5
5
+
import { planetPresets } from "./worlds/presets";
5
6
6
7
const presets = ["beach", "forest", "snowForest"];
7
8
···
36
37
wireframe: true,
37
38
wireframeLinewidth: 10,
38
39
});
39
39
-
let planetMesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
40
40
+
let planetMesh: THREE.Mesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
40
41
scene.add(planetMesh);
41
42
42
43
const light = new THREE.DirectionalLight();
···
110
111
console.time("planet");
111
112
const planet = new Planet({
112
113
detail: 50,
113
113
-
biome: { preset },
114
114
+
...planetPresets[preset],
114
115
});
115
116
let mesh = await planet.create();
116
117
scene.remove(planetMesh);
117
118
scene.add(mesh);
118
119
planetMesh = mesh;
120
120
+
121
121
+
// planetMesh.add(camera);
122
122
+
// planet.updatePosition(camera, new THREE.Vector3(0, 0, 1.1));
123
123
+
119
124
console.timeEnd("planet");
120
125
}
121
126
···
60
60
61
61
options: BiomeOptions;
62
62
63
63
-
vegetationPositions: Octree = new Octree();
63
63
+
vegetationPositions: Octree<VegetationItem> = new Octree();
64
64
65
65
constructor(opts: BiomeOptions = {}) {
66
66
if (opts.preset) {
···
150
150
this.vegetationPositions.insert(position, item);
151
151
}
152
152
153
153
+
closestVegetationDistance(
154
154
+
position: Vector3,
155
155
+
radius: number,
156
156
+
): number | undefined {
157
157
+
const items = this.vegetationPositions.queryBoxXYZ(
158
158
+
position.x,
159
159
+
position.y,
160
160
+
position.z,
161
161
+
radius,
162
162
+
);
163
163
+
if (items.length === 0) return undefined;
164
164
+
165
165
+
let closest = Infinity;
166
166
+
for (const item of items) {
167
167
+
const distance = position.distanceTo(item);
168
168
+
if (distance < closest) closest = distance;
169
169
+
}
170
170
+
171
171
+
return closest < radius ? closest : undefined;
172
172
+
}
173
173
+
153
174
itemsAround(
154
175
position: Vector3,
155
176
radius: number,
156
156
-
): (Vector3 & { data: VegetationItem })[] {
177
177
+
): (Vector3 & { data?: VegetationItem })[] {
157
178
return this.vegetationPositions.query(position, radius);
158
179
}
159
180
}
···
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
+
}
···
25
25
capacity?: number;
26
26
};
27
27
28
28
-
export type Vector3Data = Vector3 & { data?: unknown };
29
29
-
30
30
-
export class Octree {
28
28
+
export class Octree<T = unknown> {
31
29
boundary: Box3;
32
30
33
33
-
points: Vector3[];
31
31
+
points: (Vector3 & { data?: T })[];
34
32
35
33
capacity: number;
36
34
···
113
111
114
112
// returns array of points where
115
113
// distance between pos and point is less than dist
116
116
-
query(pos: Vector3Data, dist = 1): Vector3Data[] {
114
114
+
query(pos: Vector3 & { data?: T }, dist = 1): (Vector3 & { data?: T })[] {
117
115
const points = this.queryBoxXYZ(pos.x, pos.y, pos.z, dist);
118
116
119
117
return points.filter((p) => p.distanceTo(pos) < dist);
···
134
132
return this.queryBox(box);
135
133
}
136
134
137
137
-
queryBox(box: Box3, found: Vector3Data[] = []) {
135
135
+
queryBox(box: Box3, found: (Vector3 & { data?: T })[] = []) {
138
136
found ??= [];
139
137
140
138
if (!box.intersectsBox(this.boundary)) return found;
···
156
154
}
157
155
158
156
// insert point with optional data (sets vec.data = data)
159
159
-
insert(pos: Vector3Data, data: unknown = undefined) {
157
157
+
insert(pos: Vector3 & { data?: T }, data: T | undefined = undefined) {
160
158
return this.insertPoint(pos, data);
161
159
}
162
160
163
161
// vector3 free version
164
164
-
insertXYZ(x: number, y: number, z: number, data: unknown = undefined) {
162
162
+
insertXYZ(x: number, y: number, z: number, data: T | undefined = undefined) {
165
163
return this.insertPoint(new Vector3(x, y, z), data);
166
164
}
167
165
168
168
-
insertPoint(p: Vector3, data: unknown = undefined) {
166
166
+
insertPoint(p: Vector3, data: T | undefined = undefined) {
169
167
p = p.clone();
170
168
171
169
// @ts-expect-error - data is not a property of Vector3
···
281
279
return boxes;
282
280
}
283
281
284
284
-
all(arr: Vector3Data[] = []) {
282
282
+
all(arr: (Vector3 & { data?: T })[] = []) {
285
283
arr ??= [];
286
284
for (const p of this.points) {
287
285
arr.push(p);
···
1
1
-
import { MeshStandardMaterial } from "three";
1
1
+
import {
2
2
+
MeshStandardMaterial,
3
3
+
type MeshStandardMaterialParameters,
4
4
+
} from "three";
2
5
import { noise } from "./noise";
3
6
4
4
-
const oceansCausticMaterial = new MeshStandardMaterial({
5
5
-
vertexColors: true,
6
6
-
});
7
7
-
oceansCausticMaterial.onBeforeCompile = (shader) => {
8
8
-
const caustics = `
9
9
-
float caustics(vec4 vPos) {
10
10
-
// More intricate warping for marble patterns
11
11
-
// float warpFactor = 2.0;
12
12
-
// vec4 warpedPos = vPos * warpFactor + snoise(vPos * warpFactor * 0.5);
13
13
-
// vec4 warpedPos2 = warpedPos * warpFactor * 0.3 + snoise(warpedPos * warpFactor * 0.5 + vec4(0, 2, 4, 8)) + vPos;
7
7
+
export class PlanetMaterialWithCaustics extends MeshStandardMaterial {
8
8
+
constructor(parameters: MeshStandardMaterialParameters) {
9
9
+
super(parameters);
14
10
15
15
-
// // Modulate the color intensity based on the noise
16
16
-
// float vein = snoise(warpedPos2 * warpFactor) * snoise(warpedPos);
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
+
);
17
39
18
18
-
// float a = 1.0 - (sin(vein * 12.0) + 1.0) * 0.5;
19
19
-
// float diff = snoise(vPos * warpFactor);
20
20
-
// diff = diff * snoise(diff * vPos) * a;
21
21
-
// return vec3((diff));
40
40
+
shader.fragmentShader = `
41
41
+
uniform float time;
42
42
+
varying vec3 vPos;
43
43
+
${noise}
44
44
+
${caustics}
45
45
+
${shader.fragmentShader}`;
22
46
23
23
-
vec4 warpedPos = vPos * 2.0 + snoise(vPos * 3.0);
24
24
-
vec4 warpedPos2 = warpedPos * 0.3 + snoise(warpedPos * 2.0 + vec4(0, 2, 4, 8)) + vPos;
25
25
-
float vein = snoise(warpedPos2) * snoise(warpedPos);
26
26
-
float a = 1.0 - (sin(vein * 2.0) + 1.0) * 0.5;
27
27
-
28
28
-
return snoise(vPos + warpedPos + warpedPos2) * a * 1.5;
29
29
-
}`;
30
30
-
shader.vertexShader = `varying vec3 vPos;\n${shader.vertexShader}`.replace(
31
31
-
`#include <begin_vertex>`,
32
32
-
`#include <begin_vertex>\nvPos = position;`,
33
33
-
);
34
34
-
35
35
-
shader.fragmentShader = `
36
36
-
uniform float time;
37
37
-
varying vec3 vPos;
38
38
-
${noise}
39
39
-
${caustics}
40
40
-
${shader.fragmentShader}`;
41
41
-
42
42
-
shader.fragmentShader = shader.fragmentShader.replace(
43
43
-
"#include <color_fragment>",
44
44
-
`#include <color_fragment>
45
45
-
vec3 pos = vPos * 3.0;
46
46
-
float len = length(vPos);
47
47
-
// Fade in
48
48
-
float fadeIn = smoothstep(0.96, 0.985, len);
49
49
-
// Fade out
50
50
-
float fadeOut = 1.0 - smoothstep(0.994, 0.999, len);
51
51
-
float causticIntensity = fadeIn * fadeOut * 0.7;
52
52
-
diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), causticIntensity * smoothstep(0.0, 1.0, caustics(vec4(pos, time * 0.05))));
53
53
-
`,
54
54
-
);
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
+
);
55
60
56
56
-
shader.uniforms.time = { value: 0 };
57
57
-
oceansCausticMaterial.userData.shader = shader;
61
61
+
shader.uniforms.time = { value: 0 };
62
62
+
this.userData.shader = shader;
63
63
+
};
64
64
+
}
58
65
59
59
-
// console.log("FRAGMENT", shader.fragmentShader);
60
60
-
// console.log();
61
61
-
// console.log("VERTEX", shader.vertexShader);
62
62
-
};
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
+
}
63
72
64
64
-
export default oceansCausticMaterial;
73
73
+
export default PlanetMaterialWithCaustics;
···
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
-
122
121
}`;
···
11
11
import { Biome, type BiomeOptions } from "./biome";
12
12
import { loadModels } from "./models";
13
13
14
14
-
import oceansCausticMaterial from "./materials/OceanCausticsMaterial";
14
14
+
import { PlanetMaterialWithCaustics } from "./materials/OceanCausticsMaterial";
15
15
import { createAtmosphereMaterial } from "./materials/AtmosphereMaterial";
16
16
+
import { createBufferGeometry } from "./helper/helper";
16
17
17
18
export type PlanetOptions = {
18
19
scatter?: number;
···
22
23
detail?: number;
23
24
24
25
atmosphere?: {
26
26
+
enabled?: boolean;
25
27
color?: Vector3;
26
28
height?: number;
27
29
};
30
30
+
31
31
+
material?: "normal" | "caustics";
28
32
29
33
biome?: BiomeOptions;
30
34
};
···
73
77
requestId: number;
74
78
};
75
79
}) {
76
76
-
const { type, data, requestId } = event.data;
80
80
+
const { data, requestId } = event.data;
77
81
78
82
const callback = this.callbacks[requestId];
79
83
if (!callback) {
···
81
85
return;
82
86
}
83
87
84
84
-
if (type === "geometry") {
85
85
-
const geometry = new BufferGeometry();
86
86
-
const oceanGeometry = new BufferGeometry();
87
87
-
geometry.setAttribute(
88
88
-
"position",
89
89
-
new Float32BufferAttribute(new Float32Array(data.positions), 3),
90
90
-
);
91
91
-
geometry.setAttribute(
92
92
-
"color",
93
93
-
new Float32BufferAttribute(new Float32Array(data.colors), 3),
94
94
-
);
95
95
-
geometry.setAttribute(
96
96
-
"normal",
97
97
-
new Float32BufferAttribute(new Float32Array(data.normals), 3),
98
98
-
);
88
88
+
const geometry = createBufferGeometry(
89
89
+
data.positions,
90
90
+
data.colors,
91
91
+
data.normals,
92
92
+
);
99
93
100
100
-
oceanGeometry.setAttribute(
101
101
-
"position",
102
102
-
new Float32BufferAttribute(new Float32Array(data.oceanPositions), 3),
103
103
-
);
104
104
-
oceanGeometry.setAttribute(
105
105
-
"color",
106
106
-
new Float32BufferAttribute(new Float32Array(data.oceanColors), 3),
107
107
-
);
108
108
-
oceanGeometry.setAttribute(
109
109
-
"normal",
110
110
-
new Float32BufferAttribute(new Float32Array(data.oceanNormals), 3),
111
111
-
);
112
112
-
// set morph targets
113
113
-
oceanGeometry.morphAttributes.position = [
114
114
-
new Float32BufferAttribute(
115
115
-
new Float32Array(data.oceanMorphPositions),
116
116
-
3,
117
117
-
),
118
118
-
];
119
119
-
oceanGeometry.morphAttributes.normal = [
120
120
-
new Float32BufferAttribute(new Float32Array(data.oceanMorphNormals), 3),
121
121
-
];
94
94
+
const oceanGeometry = createBufferGeometry(
95
95
+
data.oceanPositions,
96
96
+
data.oceanColors,
97
97
+
data.oceanNormals,
98
98
+
);
122
99
123
123
-
this.vegetationPositions = data.vegetation;
100
100
+
oceanGeometry.morphAttributes.position = [
101
101
+
new Float32BufferAttribute(data.oceanMorphPositions, 3),
102
102
+
];
103
103
+
oceanGeometry.morphAttributes.normal = [
104
104
+
new Float32BufferAttribute(data.oceanMorphNormals, 3),
105
105
+
];
124
106
125
125
-
const planetMesh = new Mesh(geometry, oceansCausticMaterial);
126
126
-
planetMesh.castShadow = true;
107
107
+
this.vegetationPositions = data.vegetation;
127
108
109
109
+
const materialOptions = { vertexColors: true };
110
110
+
111
111
+
const material =
112
112
+
this.options.material === "caustics"
113
113
+
? new PlanetMaterialWithCaustics(materialOptions)
114
114
+
: new MeshStandardMaterial(materialOptions);
115
115
+
116
116
+
const planetMesh = new Mesh(geometry, material);
117
117
+
planetMesh.castShadow = true;
118
118
+
119
119
+
if (this.options.material === "caustics") {
128
120
planetMesh.onBeforeRender = (
129
121
renderer,
130
122
scene,
···
132
124
geometry,
133
125
material,
134
126
) => {
135
135
-
if (material.userData.shader?.uniforms?.time) {
136
136
-
material.userData.shader.uniforms.time.value =
137
137
-
performance.now() / 1000;
127
127
+
if (material instanceof PlanetMaterialWithCaustics) {
128
128
+
material.update();
138
129
}
139
139
-
//material.userData.shader.uniforms.time.value = performance.now() / 1000;
140
130
};
131
131
+
}
141
132
142
142
-
const oceanMesh = new Mesh(
143
143
-
oceanGeometry,
144
144
-
new MeshStandardMaterial({
145
145
-
vertexColors: true,
146
146
-
transparent: true,
147
147
-
opacity: 0.7,
148
148
-
metalness: 0.5,
149
149
-
roughness: 0.5,
150
150
-
}),
151
151
-
);
133
133
+
const oceanMesh = new Mesh(
134
134
+
oceanGeometry,
135
135
+
new MeshStandardMaterial({
136
136
+
vertexColors: true,
137
137
+
transparent: true,
138
138
+
opacity: 0.7,
139
139
+
metalness: 0.5,
140
140
+
roughness: 0.5,
141
141
+
}),
142
142
+
);
152
143
153
153
-
planetMesh.add(oceanMesh);
154
154
-
oceanMesh.onBeforeRender = (
155
155
-
renderer,
156
156
-
scene,
157
157
-
camera,
158
158
-
geometry,
159
159
-
material,
160
160
-
) => {
161
161
-
// update morph targets
162
162
-
if (oceanMesh.morphTargetInfluences)
163
163
-
oceanMesh.morphTargetInfluences[0] =
164
164
-
Math.sin(performance.now() / 1000) * 0.5 + 0.5;
165
165
-
};
144
144
+
planetMesh.add(oceanMesh);
145
145
+
oceanMesh.onBeforeRender = (
146
146
+
renderer,
147
147
+
scene,
148
148
+
camera,
149
149
+
geometry,
150
150
+
material,
151
151
+
) => {
152
152
+
// update morph targets
153
153
+
if (oceanMesh.morphTargetInfluences)
154
154
+
oceanMesh.morphTargetInfluences[0] =
155
155
+
Math.sin(performance.now() / 1000) * 0.5 + 0.5;
156
156
+
};
166
157
158
158
+
if (this.options.atmosphere?.enabled !== false) {
167
159
this.addAtmosphere(planetMesh);
168
168
-
callback(planetMesh);
169
160
}
161
161
+
callback(planetMesh);
170
162
171
163
delete this.callbacks[requestId];
172
164
}
···
193
185
const planet = await planetPromise;
194
186
195
187
for (let i = 0; i < loaded.length - 1; i++) {
196
196
-
const models = await loaded[i];
188
188
+
const models = (await loaded[i]) as Object3D[];
197
189
const name = models[0].userData.name;
198
190
199
191
const positions = this.vegetationPositions?.[name];
···
214
206
model.traverse((child) => {
215
207
if (child instanceof Mesh) {
216
208
let color = item?.colors?.[child.material.name];
217
217
-
if (color && color.array) {
209
209
+
if (color?.array) {
218
210
let randomColor =
219
211
color.array[Math.floor(Math.random() * color.array.length)];
220
212
child.material.color.setHex(randomColor);
···
1
1
import { type BiomeOptions } from "./biome";
2
2
+
import { 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,
···
44
45
Brown: { array: [0x8b4513, 0x5b3105] },
45
46
Green: { array: [0x22851e, 0x22a51e] },
46
47
DarkGreen: { array: [0x006400] },
48
48
+
},
49
49
+
ground: {
50
50
+
color: 0x338800,
47
51
},
48
52
},
49
53
{
···
58
62
},
59
63
};
60
64
61
61
-
const forest: BiomeOptions = {
65
65
+
const forestBiome: BiomeOptions = {
62
66
noise: {
63
67
min: -0.05,
64
68
max: 0.05,
···
154
158
},
155
159
};
156
160
157
157
-
const snowForest: BiomeOptions = {
161
161
+
const snowForestBiome: BiomeOptions = {
158
162
noise: {
159
163
min: -0.05,
160
164
max: 0.05,
···
251
255
};
252
256
253
257
export const biomePresets: Record<string, BiomeOptions> = {
254
254
-
beach,
255
255
-
forest,
256
256
-
snowForest,
258
258
+
beach: beachBiome,
259
259
+
forest: forestBiome,
260
260
+
snowForest: snowForestBiome,
261
261
+
};
262
262
+
263
263
+
const beachPlanet: PlanetOptions = {
264
264
+
biome: {
265
265
+
preset: "beach",
266
266
+
},
267
267
+
268
268
+
material: "caustics",
269
269
+
};
270
270
+
271
271
+
const forestPlanet: PlanetOptions = {
272
272
+
biome: {
273
273
+
preset: "forest",
274
274
+
},
275
275
+
276
276
+
material: "normal",
277
277
+
};
278
278
+
279
279
+
const snowForestPlanet: PlanetOptions = {
280
280
+
biome: {
281
281
+
preset: "snowForest",
282
282
+
},
283
283
+
284
284
+
material: "normal",
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
};
···
6
6
Color,
7
7
} from "three";
8
8
9
9
-
import { Biome } from "./biome";
9
9
+
import { Biome, type VegetationItem } from "./biome";
10
10
import { type PlanetOptions } from "./planet";
11
11
import UberNoise from "uber-noise";
12
12
···
110
110
a.fromBufferAttribute(vertices, 0);
111
111
b.fromBufferAttribute(vertices, 1);
112
112
113
113
-
// default to scatter = distance of first edge
113
113
+
// scatterAmount is based on side length of face (all faces are the same size)
114
114
const scatterAmount = (planetOptions.scatter ?? 1) * b.distanceTo(a);
115
115
const scatterScale = 100;
116
116
···
136
136
137
137
const temp = new Vector3();
138
138
139
139
-
let normHeightMax = 0;
140
140
-
let normHeightMin = 0;
141
141
-
139
139
+
// go through all faces
140
140
+
// - calculate height and scatter for vertices
141
141
+
// - calculate height for ocean vertices
142
142
+
// - calculate height for ocean morph vertices
143
143
+
// - calculate color for vertices and ocean vertices
144
144
+
// - calculate normal for vertices and ocean vertices
145
145
+
// - add vegetation
142
146
for (let i = 0; i < vertices.count; i += 3) {
143
147
a.fromBufferAttribute(vertices, i);
144
148
b.fromBufferAttribute(vertices, i + 1);
···
153
157
154
158
let normalizedHeight = 0;
155
159
160
160
+
// go through all vertices of the face
156
161
for (let j = 0; j < 3; j++) {
157
162
let v = a;
158
163
if (j === 1) v = b;
159
164
if (j === 2) v = c;
160
165
166
166
+
// lets see if we already have info for this vertex
161
167
const key = `${v.x.toFixed(5)},${v.y.toFixed(5)},${v.z.toFixed(5)}`;
162
162
-
163
168
let move = calculatedVertices.get(key);
164
169
170
170
+
// if not, calculate it
165
171
if (!move) {
172
172
+
// calculate height and scatter
166
173
const height = biome.getHeight(v) + 1;
167
174
const scatterX = scatterNoise.get(v);
168
175
const scatterY = scatterNoise.get(
···
175
182
v.x + scatterScale * 200,
176
183
v.y - scatterScale * 200,
177
184
);
185
185
+
// calculate sea height and sea morph height
178
186
const seaHeight = biome.getSeaHeight(v) + 1;
179
187
const secondSeaHeight = biome.getSeaHeight(v.addScalar(100)) + 1;
180
188
···
189
197
calculatedVertices.set(key, move);
190
198
}
191
199
200
200
+
// we store this info for later use (vegetation placement)
192
201
calculatedVerticesArray[i + j] = move;
193
202
203
203
+
// we add height here so we can calculate the average normalized height of the face later
194
204
normalizedHeight += move.height - 1;
205
205
+
206
206
+
// move vertex based on height and scatter
195
207
v.add(move.scatter).normalize().multiplyScalar(move.height);
196
208
vertices.setXYZ(i + j, v.x, v.y, v.z);
197
209
210
210
+
// move ocean vertex based on sea height and scatter
198
211
let oceanV = oceanA;
199
212
if (j === 1) oceanV = oceanB;
200
213
if (j === 2) oceanV = oceanC;
201
201
-
202
214
oceanV.add(move.scatter).normalize().multiplyScalar(move.seaMorph);
203
215
oceanMorphPositions.push(oceanV.x, oceanV.y, oceanV.z);
204
216
217
217
+
// move ocean morph vertex based on sea height and scatter
205
218
if (j === 0) {
206
219
oceanD.copy(oceanV);
207
220
} else if (j === 1) {
···
209
222
} else if (j === 2) {
210
223
oceanF.copy(oceanV);
211
224
}
212
212
-
213
225
oceanV.normalize().multiplyScalar(move.seaHeight);
214
226
oceanVertices.setXYZ(i + j, oceanV.x, oceanV.y, oceanV.z);
215
227
}
216
228
229
229
+
// calculate normalized height for the face (between -1 and 1, 0 is sea level)
217
230
normalizedHeight /= 3;
218
218
-
219
231
normalizedHeight =
220
232
Math.min(-normalizedHeight / biome.min, 0) +
221
233
Math.max(normalizedHeight / biome.max, 0);
222
234
// now normalizedHeight should be between -1 and 1 (0 is sea level)
235
235
+
// this will be used for color calculation and vegetation placement
223
236
224
224
-
normHeightMax = Math.max(normHeightMax, normalizedHeight);
225
225
-
normHeightMin = Math.min(normHeightMin, normalizedHeight);
226
226
-
227
227
-
// calculate new normal
237
237
+
// calculate face normal
228
238
temp.crossVectors(b.clone().sub(a), c.clone().sub(a)).normalize();
229
229
-
230
239
// flat shading, so all normals for the face are the same
231
240
normals.setXYZ(i, temp.x, temp.y, temp.z);
232
241
normals.setXYZ(i + 1, temp.x, temp.y, temp.z);
···
236
245
// (up vector = old mid point on sphere)
237
246
const steepness = Math.acos(Math.abs(temp.dot(mid)));
238
247
// steepness is between 0 and PI/2
248
248
+
// this will be used for color calculation and vegetation placement
239
249
250
250
+
// calculate color for face
240
251
const color = biome.getColor(mid, normalizedHeight, steepness);
241
241
-
242
252
// flat shading, so all colors for the face are the same
243
253
if (color) {
244
254
colors[i * 3] = color.r;
···
254
264
colors[i * 3 + 8] = color.b;
255
265
}
256
266
267
267
+
// calculate ocean face color
268
268
+
const oceanColor = biome.getSeaColor(mid, normalizedHeight);
269
269
+
270
270
+
if (oceanColor) {
271
271
+
oceanColors[i * 3] = oceanColor.r;
272
272
+
oceanColors[i * 3 + 1] = oceanColor.g;
273
273
+
oceanColors[i * 3 + 2] = oceanColor.b;
274
274
+
275
275
+
oceanColors[i * 3 + 3] = oceanColor.r;
276
276
+
oceanColors[i * 3 + 4] = oceanColor.g;
277
277
+
oceanColors[i * 3 + 5] = oceanColor.b;
278
278
+
279
279
+
oceanColors[i * 3 + 6] = oceanColor.r;
280
280
+
oceanColors[i * 3 + 7] = oceanColor.g;
281
281
+
oceanColors[i * 3 + 8] = oceanColor.b;
282
282
+
}
283
283
+
284
284
+
// calculate ocean normals
285
285
+
temp
286
286
+
.crossVectors(oceanB.clone().sub(oceanA), oceanC.clone().sub(oceanA))
287
287
+
.normalize();
288
288
+
oceanNormals.setXYZ(i, temp.x, temp.y, temp.z);
289
289
+
oceanNormals.setXYZ(i + 1, temp.x, temp.y, temp.z);
290
290
+
oceanNormals.setXYZ(i + 2, temp.x, temp.y, temp.z);
291
291
+
292
292
+
// calculate ocean morph normals
293
293
+
temp
294
294
+
.crossVectors(oceanE.clone().sub(oceanD), oceanF.clone().sub(oceanD))
295
295
+
.normalize();
296
296
+
oceanMorphNormals.push(temp.x, temp.y, temp.z);
297
297
+
oceanMorphNormals.push(temp.x, temp.y, temp.z);
298
298
+
oceanMorphNormals.push(temp.x, temp.y, temp.z);
299
299
+
257
300
// place vegetation
258
301
for (
259
302
let j = 0;
···
314
357
break;
315
358
}
316
359
}
317
317
-
318
318
-
// calculate ocean vertices
319
319
-
const oceanColor = biome.getSeaColor(mid, normalizedHeight);
320
320
-
321
321
-
if (oceanColor) {
322
322
-
oceanColors[i * 3] = oceanColor.r;
323
323
-
oceanColors[i * 3 + 1] = oceanColor.g;
324
324
-
oceanColors[i * 3 + 2] = oceanColor.b;
325
325
-
326
326
-
oceanColors[i * 3 + 3] = oceanColor.r;
327
327
-
oceanColors[i * 3 + 4] = oceanColor.g;
328
328
-
oceanColors[i * 3 + 5] = oceanColor.b;
329
329
-
330
330
-
oceanColors[i * 3 + 6] = oceanColor.r;
331
331
-
oceanColors[i * 3 + 7] = oceanColor.g;
332
332
-
oceanColors[i * 3 + 8] = oceanColor.b;
333
333
-
}
334
334
-
335
335
-
// calculate ocean normals
336
336
-
temp
337
337
-
.crossVectors(oceanB.clone().sub(oceanA), oceanC.clone().sub(oceanA))
338
338
-
.normalize();
339
339
-
340
340
-
oceanNormals.setXYZ(i, temp.x, temp.y, temp.z);
341
341
-
oceanNormals.setXYZ(i + 1, temp.x, temp.y, temp.z);
342
342
-
oceanNormals.setXYZ(i + 2, temp.x, temp.y, temp.z);
343
343
-
344
344
-
temp
345
345
-
.crossVectors(oceanE.clone().sub(oceanD), oceanF.clone().sub(oceanD))
346
346
-
.normalize();
347
347
-
348
348
-
oceanMorphNormals.push(temp.x, temp.y, temp.z);
349
349
-
oceanMorphNormals.push(temp.x, temp.y, temp.z);
350
350
-
oceanMorphNormals.push(temp.x, temp.y, temp.z);
351
360
}
352
361
353
362
const maxDist = 0.14;
···
355
364
for (let i = 0; i < vertices.count; i += 3) {
356
365
let found = false;
357
366
let closestDistAll = 1;
367
367
+
let closestVegetation: VegetationItem | undefined = undefined;
368
368
+
358
369
for (let j = 0; j < 3; j++) {
359
370
a.fromBufferAttribute(vertices, i + j);
360
371
a.normalize();
···
379
390
);
380
391
381
392
vertices.setXYZ(i + j, a.x, a.y, a.z);
393
393
+
394
394
+
if (closestDist < closestDistAll) {
395
395
+
closestVegetation = closest.data;
396
396
+
}
382
397
383
398
closestDistAll = Math.min(closestDist, closestDistAll);
384
399
found = true;
385
400
}
386
401
}
387
402
388
388
-
if (found) {
389
389
-
let existingColor = new Color(
390
390
-
colors[i * 3],
391
391
-
colors[i * 3 + 1],
392
392
-
colors[i * 3 + 2],
393
393
-
);
403
403
+
if (!found) continue;
394
404
405
405
+
let existingColor = new Color(
406
406
+
colors[i * 3],
407
407
+
colors[i * 3 + 1],
408
408
+
colors[i * 3 + 2],
409
409
+
);
410
410
+
411
411
+
if (closestVegetation?.ground?.color) {
395
412
// set color
396
396
-
let newColor = new Color(0.1, 0.3, 0);
413
413
+
let newColor = new Color(closestVegetation.ground.color);
397
414
398
415
newColor.lerp(existingColor, closestDistAll / maxDist);
399
416