···11# tiny planets
2233-procedurally generated tiny planets in three.js
33+procedurally generated tiny planets in three.js. work in progress.
44+55+66+https://github.com/user-attachments/assets/55ba1872-d064-483b-9a31-2c95adaa201a
77+4859## features
610711- generated using web workers
812- adjustable detail level
913- with vegetation
1010-- presets1414+- presets
+11-3
features.md
···99 - [x] height
1010 - [ ] slope
1111- [ ] different materials
1212+- [ ] make vegetation sway in the wind
1313+- [ ] make all random stuff dependent on seed
12141315### weather
1416- [ ] clouds
1517- [ ] particles (snow, rain, etc)
16181719### water
1818-- [ ] moving water
2020+- [x] moving water
1921- [ ] water reflections
2022- [ ] water refractions
2121-- [ ] water caustics
2323+- [x] water caustics
2224- [ ] water foam
23252426### biomes
···3436- [ ] shadows
3537- [ ] ambient occlusion
36383939+- [ ] physics
37403838-- [ ] physics4141+## filters
4242+- [ ] selective bloom (for glowy things)
4343+- [ ] tilt shift
4444+4545+## other
4646+- [ ]
···11-import { IcosahedronGeometry, Vector3, BufferAttribute } from "three";
11+import {
22+ IcosahedronGeometry,
33+ Vector3,
44+ BufferAttribute,
55+ Float32BufferAttribute,
66+ Color,
77+} from "three";
2839import { Biome, type BiomeOptions } from "./biome";
410import { type PlanetOptions } from "./planet";
···1723 const oceanPositions = oceanGeometry.getAttribute("position").array.buffer;
1824 const oceanColors = oceanGeometry.getAttribute("color").array.buffer;
1925 const oceanNormals = oceanGeometry.getAttribute("normal").array.buffer;
2626+ const oceanMorphPositions =
2727+ oceanGeometry.morphAttributes.position[0].array.buffer;
2828+ const oceanMorphNormals =
2929+ oceanGeometry.morphAttributes.normal[0].array.buffer;
20302131 postMessage(
2232 {
···2939 oceanColors,
3040 oceanNormals,
3141 vegetation,
4242+ oceanMorphPositions,
4343+ oceanMorphNormals,
3244 },
3345 requestId,
3446 },
3547 // @ts-expect-error - hmm
3636- [positions, colors, normals, oceanPositions, oceanColors, oceanNormals],
4848+ [
4949+ positions,
5050+ colors,
5151+ normals,
5252+ oceanPositions,
5353+ oceanColors,
5454+ oceanNormals,
5555+ oceanMorphPositions,
5656+ oceanMorphNormals,
5757+ ],
3758 );
3859 } else {
3960 console.error("Unknown message type", type);
4061 }
4162};
42634343-function createGeometry({
4444- biomeOptions,
4545- planetOptions,
4646-}: {
4747- biomeOptions: BiomeOptions;
4848- planetOptions: PlanetOptions;
4949-}): [IcosahedronGeometry, IcosahedronGeometry, Record<string, Vector3[]>] {
6464+function createGeometry(
6565+ planetOptions: PlanetOptions,
6666+): [IcosahedronGeometry, IcosahedronGeometry, Record<string, Vector3[]>] {
5067 const sphere = new IcosahedronGeometry(1, planetOptions.detail ?? 50);
5168 const oceanSphere = new IcosahedronGeometry(1, planetOptions.detail ?? 50);
52695353- const biome = new Biome(biomeOptions);
7070+ const biome = new Biome(planetOptions.biome);
54715572 const vertices = sphere.getAttribute("position");
5673 const oceanVertices = oceanSphere.getAttribute("position");
···6279 string,
6380 {
6481 height: number;
6565- seaHeight: number;
6682 scatter: Vector3;
8383+8484+ seaHeight: number;
8585+ seaMorph: number;
6786 }
6887 >();
8888+8989+ const calculatedVerticesArray: {
9090+ height: number;
9191+ scatter: Vector3;
9292+9393+ seaHeight: number;
9494+ seaMorph: number;
9595+ }[] = new Array(faceCount);
69967097 const colors = new Float32Array(vertices.count * 3);
7198 const oceanColors = new Float32Array(oceanVertices.count * 3);
···84111 b.fromBufferAttribute(vertices, 1);
8511286113 // default to scatter = distance of first edge
8787- const scatterAmount = planetOptions.scatter ?? b.distanceTo(a);
114114+ const scatterAmount = (planetOptions.scatter ?? 1) * b.distanceTo(a);
88115 const scatterScale = 100;
8911690117 const scatterNoise = new UberNoise({
···94121 seed: 0,
95122 });
96123124124+ oceanSphere.morphAttributes.position = [];
125125+ oceanSphere.morphAttributes.normal = [];
126126+127127+ const oceanMorphPositions: number[] = [];
128128+ const oceanMorphNormals: number[] = [];
129129+130130+ const oceanA = new Vector3(),
131131+ oceanB = new Vector3(),
132132+ oceanC = new Vector3(),
133133+ oceanD = new Vector3(),
134134+ oceanE = new Vector3(),
135135+ oceanF = new Vector3();
136136+137137+ const temp = new Vector3();
138138+139139+ let normHeightMax = 0;
140140+ let normHeightMin = 0;
141141+97142 for (let i = 0; i < vertices.count; i += 3) {
98143 a.fromBufferAttribute(vertices, i);
99144 b.fromBufferAttribute(vertices, i + 1);
100145 c.fromBufferAttribute(vertices, i + 2);
146146+147147+ oceanA.fromBufferAttribute(oceanVertices, i);
148148+ oceanB.fromBufferAttribute(oceanVertices, i + 1);
149149+ oceanC.fromBufferAttribute(oceanVertices, i + 2);
101150102151 mid.set(0, 0, 0);
103152 mid.addVectors(a, b).add(c).divideScalar(3);
···108157 let v = a;
109158 if (j === 1) v = b;
110159 if (j === 2) v = c;
160160+111161 const key = `${v.x.toFixed(5)},${v.y.toFixed(5)},${v.z.toFixed(5)}`;
112162113163 let move = calculatedVertices.get(key);
···126176 v.y - scatterScale * 200,
127177 );
128178 const seaHeight = biome.getSeaHeight(v) + 1;
179179+ const secondSeaHeight = biome.getSeaHeight(v.addScalar(100)) + 1;
180180+181181+ v.subScalar(100);
129182130183 move = {
131184 height,
132185 scatter: new Vector3(scatterX, scatterY, scatterZ),
133186 seaHeight,
187187+ seaMorph: secondSeaHeight,
134188 };
135189 calculatedVertices.set(key, move);
136190 }
137191192192+ calculatedVerticesArray[i + j] = move;
193193+138194 normalizedHeight += move.height - 1;
139195 v.add(move.scatter).normalize().multiplyScalar(move.height);
140140-141196 vertices.setXYZ(i + j, v.x, v.y, v.z);
142197143143- const oceanV = v.clone().normalize().multiplyScalar(move.seaHeight);
198198+ let oceanV = oceanA;
199199+ if (j === 1) oceanV = oceanB;
200200+ if (j === 2) oceanV = oceanC;
201201+202202+ oceanV.add(move.scatter).normalize().multiplyScalar(move.seaMorph);
203203+ oceanMorphPositions.push(oceanV.x, oceanV.y, oceanV.z);
204204+205205+ if (j === 0) {
206206+ oceanD.copy(oceanV);
207207+ } else if (j === 1) {
208208+ oceanE.copy(oceanV);
209209+ } else if (j === 2) {
210210+ oceanF.copy(oceanV);
211211+ }
212212+213213+ oceanV.normalize().multiplyScalar(move.seaHeight);
144214 oceanVertices.setXYZ(i + j, oceanV.x, oceanV.y, oceanV.z);
145215 }
146216147217 normalizedHeight /= 3;
148148- normalizedHeight = (normalizedHeight - biome.min) / (biome.max - biome.min);
149149- normalizedHeight = normalizedHeight * 2 - 1;
150150- // now averageHeight is between -1 and 1 (0 is sea level)
151218152152- for (
153153- let j = 0;
154154- biome.options.vegetation && j < biome.options.vegetation.items.length;
155155- j++
156156- ) {
157157- const vegetation = biome.options.vegetation.items[j];
158158- if (Math.random() < faceSize * vegetation.density) {
159159- if (
160160- vegetation.minimumHeight !== undefined &&
161161- normalizedHeight < vegetation.minimumHeight
162162- ) {
163163- continue;
164164- }
219219+ normalizedHeight =
220220+ Math.min(-normalizedHeight / biome.min, 0) +
221221+ Math.max(normalizedHeight / biome.max, 0);
222222+ // now normalizedHeight should be between -1 and 1 (0 is sea level)
165223166166- if (vegetation.minimumHeight === undefined && normalizedHeight < 0) {
167167- continue;
168168- }
169169-170170- if (
171171- vegetation.maximumHeight !== undefined &&
172172- normalizedHeight > vegetation.maximumHeight
173173- ) {
174174- continue;
175175- }
176176- if (!placedVegetation[vegetation.name]) {
177177- placedVegetation[vegetation.name] = [];
178178- }
179179- placedVegetation[vegetation.name].push(a.clone());
180180- break;
181181- }
182182- }
224224+ normHeightMax = Math.max(normHeightMax, normalizedHeight);
225225+ normHeightMin = Math.min(normHeightMin, normalizedHeight);
183226184227 // calculate new normal
185185- const normal = new Vector3();
186186- normal.crossVectors(b.clone().sub(a), c.clone().sub(a)).normalize();
228228+ temp.crossVectors(b.clone().sub(a), c.clone().sub(a)).normalize();
187229188230 // flat shading, so all normals for the face are the same
189189- normals.setXYZ(i, normal.x, normal.y, normal.z);
190190- normals.setXYZ(i + 1, normal.x, normal.y, normal.z);
191191- normals.setXYZ(i + 2, normal.x, normal.y, normal.z);
231231+ normals.setXYZ(i, temp.x, temp.y, temp.z);
232232+ normals.setXYZ(i + 1, temp.x, temp.y, temp.z);
233233+ normals.setXYZ(i + 2, temp.x, temp.y, temp.z);
192234193235 // calculate steepness (acos of dot product of normal and up vector)
194236 // (up vector = old mid point on sphere)
195195- const steepness = Math.acos(Math.abs(normal.dot(mid)));
237237+ const steepness = Math.acos(Math.abs(temp.dot(mid)));
196238 // steepness is between 0 and PI/2
197239198240 const color = biome.getColor(mid, normalizedHeight, steepness);
···212254 colors[i * 3 + 8] = color.b;
213255 }
214256257257+ // place vegetation
258258+ for (
259259+ let j = 0;
260260+ biome.options.vegetation && j < biome.options.vegetation.items.length;
261261+ j++
262262+ ) {
263263+ const vegetation = biome.options.vegetation.items[j];
264264+ if (Math.random() < faceSize * vegetation.density) {
265265+ // discard if point is below or above height limits
266266+ if (
267267+ vegetation.minimumHeight !== undefined &&
268268+ normalizedHeight < vegetation.minimumHeight
269269+ ) {
270270+ continue;
271271+ }
272272+ // default minimumHeight is 0 (= above sea level)
273273+ if (vegetation.minimumHeight === undefined && normalizedHeight < 0) {
274274+ continue;
275275+ }
276276+ if (
277277+ vegetation.maximumHeight !== undefined &&
278278+ normalizedHeight > vegetation.maximumHeight
279279+ ) {
280280+ continue;
281281+ }
282282+283283+ // discard if point is below or above slope limits
284284+ if (
285285+ vegetation.minimumSlope !== undefined &&
286286+ steepness < vegetation.minimumSlope
287287+ ) {
288288+ continue;
289289+ }
290290+ if (
291291+ vegetation.maximumSlope !== undefined &&
292292+ steepness > vegetation.maximumSlope
293293+ ) {
294294+ continue;
295295+ }
296296+297297+ if (!placedVegetation[vegetation.name]) {
298298+ placedVegetation[vegetation.name] = [];
299299+ }
300300+ let height = a.length();
301301+ placedVegetation[vegetation.name].push(
302302+ a
303303+ .clone()
304304+ .normalize()
305305+ .multiplyScalar(height + 0.005),
306306+ );
307307+308308+ biome.addVegetation(
309309+ vegetation,
310310+ a.normalize(),
311311+ normalizedHeight,
312312+ steepness,
313313+ );
314314+ break;
315315+ }
316316+ }
317317+215318 // calculate ocean vertices
216319 const oceanColor = biome.getSeaColor(mid, normalizedHeight);
217320···229332 oceanColors[i * 3 + 8] = oceanColor.b;
230333 }
231334232232- oceanNormals.setXYZ(i, mid.x, mid.y, mid.z);
233233- oceanNormals.setXYZ(i + 1, mid.x, mid.y, mid.z);
234234- oceanNormals.setXYZ(i + 2, mid.x, mid.y, mid.z);
335335+ // calculate ocean normals
336336+ temp
337337+ .crossVectors(oceanB.clone().sub(oceanA), oceanC.clone().sub(oceanA))
338338+ .normalize();
339339+340340+ oceanNormals.setXYZ(i, temp.x, temp.y, temp.z);
341341+ oceanNormals.setXYZ(i + 1, temp.x, temp.y, temp.z);
342342+ oceanNormals.setXYZ(i + 2, temp.x, temp.y, temp.z);
343343+344344+ temp
345345+ .crossVectors(oceanE.clone().sub(oceanD), oceanF.clone().sub(oceanD))
346346+ .normalize();
347347+348348+ oceanMorphNormals.push(temp.x, temp.y, temp.z);
349349+ oceanMorphNormals.push(temp.x, temp.y, temp.z);
350350+ oceanMorphNormals.push(temp.x, temp.y, temp.z);
235351 }
352352+353353+ console.log("normHeightMax", normHeightMax);
354354+ console.log("normHeightMin", normHeightMin);
355355+356356+ const maxDist = 0.14;
357357+ // go through all vertices again and update height and color based on vegetation
358358+ for (let i = 0; i < vertices.count; i += 3) {
359359+ let found = false;
360360+ let closestDistAll = 1;
361361+ for (let j = 0; j < 3; j++) {
362362+ a.fromBufferAttribute(vertices, i + j);
363363+ a.normalize();
364364+365365+ let p = biome.itemsAround(a, maxDist);
366366+ if (p.length > 0) {
367367+ // find closest point
368368+ let closest = p[0];
369369+ let closestDist = a.distanceTo(closest);
370370+ for (let k = 1; k < p.length; k++) {
371371+ let dist = a.distanceTo(p[k]);
372372+ if (dist < closestDist) {
373373+ closest = p[k];
374374+ closestDist = dist;
375375+ }
376376+ }
377377+378378+ let moveInfo = calculatedVerticesArray[i + j];
379379+380380+ a.multiplyScalar(
381381+ moveInfo.height + ((maxDist - closestDist) / maxDist) * 0.015,
382382+ );
383383+384384+ vertices.setXYZ(i + j, a.x, a.y, a.z);
385385+386386+ closestDistAll = Math.min(closestDist, closestDistAll);
387387+ found = true;
388388+ }
389389+ }
390390+391391+ if (found) {
392392+ let existingColor = new Color(
393393+ colors[i * 3],
394394+ colors[i * 3 + 1],
395395+ colors[i * 3 + 2],
396396+ );
397397+398398+ // set color
399399+ let newColor = new Color(0.1, 0.3, 0);
400400+401401+ newColor.lerp(existingColor, closestDistAll / maxDist);
402402+403403+ for (let j = 0; j < 3; j++) {
404404+ colors[(i + j) * 3] = newColor.r;
405405+ colors[(i + j) * 3 + 1] = newColor.g;
406406+ colors[(i + j) * 3 + 2] = newColor.b;
407407+ }
408408+ }
409409+ }
410410+411411+ oceanSphere.morphAttributes.position[0] = new Float32BufferAttribute(
412412+ oceanMorphPositions,
413413+ 3,
414414+ );
415415+ oceanSphere.morphAttributes.normal[0] = new Float32BufferAttribute(
416416+ oceanMorphNormals,
417417+ 3,
418418+ );
236419237420 sphere.setAttribute("color", new BufferAttribute(colors, 3));
238421 oceanSphere.setAttribute("color", new BufferAttribute(oceanColors, 3));
+291
src/worlds/helper/octree.ts
···11+import {
22+ Vector3,
33+ Box3,
44+ Material,
55+ Object3D,
66+ Mesh,
77+ BoxGeometry,
88+ MeshStandardMaterial,
99+ BufferAttribute,
1010+ BufferGeometry,
1111+ Points,
1212+ PointsMaterial,
1313+} from "three";
1414+1515+export type OctreeOptions = {
1616+ bounds?: Box3;
1717+1818+ size?: number;
1919+2020+ min?: Vector3;
2121+ max?: Vector3;
2222+2323+ points?: Vector3[];
2424+2525+ capacity?: number;
2626+};
2727+2828+export type Vector3Data = Vector3 & { data?: unknown };
2929+3030+export class Octree {
3131+ boundary: Box3;
3232+3333+ points: Vector3[];
3434+3535+ capacity: number;
3636+3737+ subdivisions: Octree[] | undefined = undefined;
3838+3939+ constructor(opts: OctreeOptions = {}) {
4040+ opts ??= {};
4141+4242+ this.points = [];
4343+4444+ if (opts.bounds) {
4545+ this.boundary = opts.bounds.clone();
4646+ } else if (opts.size) {
4747+ const s = opts.size;
4848+ this.boundary = new Box3(new Vector3(-s, -s, -s), new Vector3(s, s, s));
4949+ } else if (opts.min || opts.max) {
5050+ const min = opts.min || new Vector3(-1, -1, -1);
5151+ const max = opts.max || new Vector3(1, 1, 1);
5252+ this.boundary = new Box3(min, max);
5353+ } else if (opts.points && opts.points.length > 0) {
5454+ const min = opts.points[0].clone();
5555+ const max = opts.points[0].clone();
5656+ for (const p of opts.points) {
5757+ min.x = Math.min(min.x, p.x);
5858+ min.y = Math.min(min.y, p.y);
5959+ min.z = Math.min(min.z, p.z);
6060+6161+ max.x = Math.max(max.x, p.x);
6262+ max.y = Math.max(max.y, p.y);
6363+ max.z = Math.max(max.z, p.z);
6464+ }
6565+ this.boundary = new Box3(min, max);
6666+ } else {
6767+ this.boundary = new Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1));
6868+ }
6969+7070+ this.capacity = opts.capacity || 4;
7171+7272+ if (opts.points) {
7373+ for (const p of opts.points) {
7474+ this.insertXYZ(p.x, p.y, p.z);
7575+ }
7676+ }
7777+ }
7878+7979+ subdivide() {
8080+ // if already subdivided exit silently
8181+ if (this.subdivisions != undefined) return;
8282+8383+ // divide each dimension => 2 * 2 * 2 = 8 subdivisions
8484+ const size = new Vector3();
8585+ const subdivisions: Octree[] = [];
8686+ for (let x = 0; x < 2; x++) {
8787+ for (let y = 0; y < 2; y++) {
8888+ for (let z = 0; z < 2; z++) {
8989+ const min = this.boundary.min.clone();
9090+ const max = this.boundary.max.clone();
9191+ this.boundary.getSize(size);
9292+ size.divideScalar(2);
9393+9494+ min.x += x * size.x;
9595+ min.y += y * size.y;
9696+ min.z += z * size.z;
9797+ max.x -= (1 - x) * size.x;
9898+ max.y -= (1 - y) * size.y;
9999+ max.z -= (1 - z) * size.z;
100100+101101+ subdivisions.push(
102102+ new Octree({
103103+ min: min,
104104+ max: max,
105105+ capacity: this.capacity,
106106+ }),
107107+ );
108108+ }
109109+ }
110110+ }
111111+ this.subdivisions = subdivisions;
112112+ }
113113+114114+ // returns array of points where
115115+ // distance between pos and point is less than dist
116116+ query(pos: Vector3Data, dist = 1) {
117117+ const points = this.queryXYZ(pos.x, pos.y, pos.z, dist);
118118+ for (let i = points.length - 1; i >= 0; i--) {
119119+ if (points[i].distanceTo(pos) > dist) points.splice(i, 1);
120120+ }
121121+ return points;
122122+ }
123123+124124+ // vector3 free version, returns points in box around xyz
125125+ queryXYZ(x: number, y: number, z: number, s: number) {
126126+ const min = new Vector3(x - s, y - s, z - s),
127127+ max = new Vector3(x + s, y + s, z + s);
128128+ const box = new Box3(min, max);
129129+130130+ return this.queryBox(box);
131131+ }
132132+133133+ queryBox(box: Box3, found: Vector3Data[] = []) {
134134+ found ??= [];
135135+136136+ if (!box.intersectsBox(this.boundary)) return found;
137137+138138+ for (const p of this.points) {
139139+ if (box.containsPoint(p)) found.push(p);
140140+ }
141141+ if (this.subdivisions) {
142142+ for (const sub of this.subdivisions) {
143143+ sub.queryBox(box, found);
144144+ }
145145+ }
146146+ return found;
147147+ }
148148+149149+ // returns true if no points are closer than dist to point
150150+ minDist(pos: Vector3, dist: number) {
151151+ return this.query(pos, dist).length < 1;
152152+ }
153153+154154+ // insert point with optional data (sets vec.data = data)
155155+ insert(pos: Vector3Data, data: unknown = undefined) {
156156+ return this.insertPoint(pos, data);
157157+ }
158158+159159+ // vector3 free version
160160+ insertXYZ(x: number, y: number, z: number, data: unknown = undefined) {
161161+ return this.insertPoint(new Vector3(x, y, z), data);
162162+ }
163163+164164+ insertPoint(p: Vector3, data: unknown = undefined) {
165165+ p = p.clone();
166166+167167+ // @ts-expect-error - data is not a property of Vector3
168168+ if (data) p.data = data;
169169+170170+ if (!this.boundary.containsPoint(p)) return false;
171171+172172+ if (this.points.length < this.capacity) {
173173+ this.points.push(p);
174174+ return true;
175175+ } else {
176176+ this.subdivide();
177177+ let added = false;
178178+ for (const sub of this.subdivisions ?? []) {
179179+ if (sub.insertPoint(p, data)) added = true;
180180+ }
181181+ return added;
182182+ }
183183+ }
184184+185185+ showBoxes(mat: Material, parent: Object3D | undefined = undefined) {
186186+ const size = new Vector3();
187187+ this.boundary.getSize(size);
188188+189189+ const box = new BoxGeometry(size.x * 2, size.y * 2, size.z * 2);
190190+ const mesh = new Mesh(
191191+ box,
192192+ mat ||
193193+ new MeshStandardMaterial({
194194+ wireframe: true,
195195+ }),
196196+ );
197197+ this.boundary.getCenter(mesh.position);
198198+199199+ parent ??= new Object3D();
200200+ parent.add(mesh);
201201+202202+ if (this.subdivisions) {
203203+ for (const sub of this.subdivisions) sub.showBoxes(mat, parent);
204204+ }
205205+ return parent;
206206+ }
207207+208208+ show(
209209+ opts: {
210210+ pointsOnly?: boolean;
211211+ mat?: Material;
212212+ size?: number;
213213+ sizeAttenuation?: boolean;
214214+ p?: Vector3;
215215+ min?: number;
216216+ } = {},
217217+ ) {
218218+ opts ??= {};
219219+220220+ const pointsOnly = opts.pointsOnly;
221221+ let mat = opts.mat;
222222+ const points = this.all();
223223+224224+ const pointsGeo = new BufferGeometry();
225225+ const positionData = new Float32Array(points.length * 3);
226226+ const colorData = new Float32Array(points.length * 3);
227227+228228+ let q;
229229+230230+ if (opts.p && opts.min) {
231231+ for (const point of points) {
232232+ // @ts-expect-error - close is not a property of Vector3
233233+ point.close = false;
234234+ }
235235+ q = this.query(opts.p, opts.min);
236236+237237+ for (const point of q) {
238238+ // @ts-expect-error - close is not a property of Vector3
239239+ point.close = true;
240240+ }
241241+ }
242242+243243+ for (let i = 0; i < points.length; i++) {
244244+ positionData[i * 3] = points[i].x;
245245+ positionData[i * 3 + 1] = points[i].y;
246246+ positionData[i * 3 + 2] = points[i].z;
247247+248248+ // @ts-expect-error - close is not a property of Vector3
249249+ colorData[i * 3] = points[i].close ? 1 : 0.7;
250250+251251+ // @ts-expect-error - close is not a property of Vector3
252252+ colorData[i * 3 + 1] = points[i].close ? 0 : 0.7;
253253+254254+ // @ts-expect-error - close is not a property of Vector3
255255+ colorData[i * 3 + 2] = points[i].close ? 0 : 0.7;
256256+ }
257257+ pointsGeo.setAttribute("position", new BufferAttribute(positionData, 3));
258258+ pointsGeo.setAttribute("color", new BufferAttribute(colorData, 3));
259259+ const pointMesh = new Points(
260260+ pointsGeo,
261261+ new PointsMaterial({
262262+ size: opts.size || 1,
263263+ sizeAttenuation: opts.sizeAttenuation || false,
264264+ vertexColors: true,
265265+ }),
266266+ );
267267+ if (pointsOnly) return pointMesh;
268268+269269+ mat =
270270+ mat ||
271271+ new MeshStandardMaterial({
272272+ transparent: true,
273273+ opacity: 0.01,
274274+ depthTest: false,
275275+ });
276276+ const boxes = this.showBoxes(mat);
277277+ boxes.add(pointMesh);
278278+ return boxes;
279279+ }
280280+281281+ all(arr: Vector3Data[] = []) {
282282+ arr ??= [];
283283+ for (const p of this.points) {
284284+ arr.push(p);
285285+ }
286286+ if (this.subdivisions) {
287287+ for (const subs of this.subdivisions) subs.all(arr);
288288+ }
289289+ return arr;
290290+ }
291291+}
-275
src/worlds/helper/octtree.ts
···11-import * as THREE from 'three';
22-33-export type OcttreeOptions = {
44- bounds?: THREE.Box3;
55-66- size?: number;
77-88- min?: THREE.Vector3;
99- max?: THREE.Vector3;
1010-1111- points?: THREE.Vector3[];
1212-1313- capacity?: number;
1414-};
1515-1616-export class Octtree {
1717- boundary: THREE.Box3;
1818-1919- points: THREE.Vector3[];
2020-2121- capacity: number;
2222-2323- subdivisions: Octtree[] | undefined = undefined;
2424-2525- constructor(opts: OcttreeOptions = {}) {
2626- opts ??= {};
2727-2828- this.points = [];
2929-3030- if (opts.bounds) {
3131- this.boundary = opts.bounds.clone();
3232- } else if (opts.size) {
3333- const s = opts.size;
3434- this.boundary = new THREE.Box3(new THREE.Vector3(-s, -s, -s), new THREE.Vector3(s, s, s));
3535- } else if (opts.min || opts.max) {
3636- const min = opts.min || new THREE.Vector3(-1, -1, -1);
3737- const max = opts.max || new THREE.Vector3(1, 1, 1);
3838- this.boundary = new THREE.Box3(min, max);
3939- } else if (opts.points && opts.points.length > 0) {
4040- const min = opts.points[0].clone();
4141- const max = opts.points[0].clone();
4242- for (const p of opts.points) {
4343- min.x = Math.min(min.x, p.x);
4444- min.y = Math.min(min.y, p.y);
4545- min.z = Math.min(min.z, p.z);
4646-4747- max.x = Math.max(max.x, p.x);
4848- max.y = Math.max(max.y, p.y);
4949- max.z = Math.max(max.z, p.z);
5050- }
5151- this.boundary = new THREE.Box3(min, max);
5252- } else {
5353- this.boundary = new THREE.Box3(new THREE.Vector3(-1, -1, -1), new THREE.Vector3(1, 1, 1));
5454- }
5555-5656- this.capacity = opts.capacity || 4;
5757-5858- if (opts.points) {
5959- for (const p of opts.points) {
6060- this.insertXYZ(p.x, p.y, p.z);
6161- }
6262- }
6363- }
6464-6565- subdivide() {
6666- // if already subdivided exit silently
6767- if (this.subdivisions != undefined) return;
6868-6969- // divide each dimension => 2 * 2 * 2 = 8 subdivisions
7070- const size = new THREE.Vector3();
7171- const subdivisions: Octtree[] = [];
7272- for (let x = 0; x < 2; x++) {
7373- for (let y = 0; y < 2; y++) {
7474- for (let z = 0; z < 2; z++) {
7575- const min = this.boundary.min.clone();
7676- const max = this.boundary.max.clone();
7777- this.boundary.getSize(size);
7878- size.divideScalar(2);
7979-8080- min.x += x * size.x;
8181- min.y += y * size.y;
8282- min.z += z * size.z;
8383- max.x -= (1 - x) * size.x;
8484- max.y -= (1 - y) * size.y;
8585- max.z -= (1 - z) * size.z;
8686-8787- subdivisions.push(
8888- new Octtree({
8989- min: min,
9090- max: max,
9191- capacity: this.capacity
9292- })
9393- );
9494- }
9595- }
9696- }
9797- this.subdivisions = subdivisions;
9898- }
9999-100100- // returns array of points where
101101- // distance between pos and point is less than dist
102102- query(pos: THREE.Vector3, dist = 1) {
103103- const points = this.queryXYZ(pos.x, pos.y, pos.z, dist);
104104- for (let i = points.length - 1; i >= 0; i--) {
105105- if (points[i].distanceTo(pos) > dist) points.splice(i, 1);
106106- }
107107- return points;
108108- }
109109-110110- // vector3 free version, returns points in box around xyz
111111- queryXYZ(x: number, y: number, z: number, s: number) {
112112- const min = new THREE.Vector3(x - s, y - s, z - s),
113113- max = new THREE.Vector3(x + s, y + s, z + s);
114114- const box = new THREE.Box3(min, max);
115115-116116- return this.queryBox(box);
117117- }
118118-119119- queryBox(box: THREE.Box3, found: THREE.Vector3[] = []) {
120120- found ??= [];
121121-122122- if (!box.intersectsBox(this.boundary)) return found;
123123-124124- for (const p of this.points) {
125125- if (box.containsPoint(p)) found.push(p);
126126- }
127127- if (this.subdivisions) {
128128- for (const sub of this.subdivisions) {
129129- sub.queryBox(box, found);
130130- }
131131- }
132132- return found;
133133- }
134134-135135- // returns true if no points are closer than dist to point
136136- minDist(pos: THREE.Vector3, dist: number) {
137137- return this.query(pos, dist).length < 1;
138138- }
139139-140140- // insert point with optional data (sets vec.data = data)
141141- insert(pos: THREE.Vector3, data: unknown = undefined) {
142142- return this.insertPoint(pos, data);
143143- }
144144- // vector3 free version
145145- insertXYZ(x: number, y: number, z: number, data: unknown = undefined) {
146146- return this.insertPoint(new THREE.Vector3(x, y, z), data);
147147- }
148148- insertPoint(p: THREE.Vector3, data: unknown = undefined) {
149149- p = p.clone();
150150-151151- // @ts-expect-error - data is not a property of Vector3
152152- if (data) p.data = data;
153153-154154- if (!this.boundary.containsPoint(p)) return false;
155155-156156- if (this.points.length < this.capacity) {
157157- this.points.push(p);
158158- return true;
159159- } else {
160160- this.subdivide();
161161- let added = false;
162162- for (const sub of this.subdivisions ?? []) {
163163- if (sub.insertPoint(p, data)) added = true;
164164- }
165165- return added;
166166- }
167167- }
168168-169169- showBoxes(mat: THREE.Material, parent: THREE.Object3D | undefined = undefined) {
170170- const size = new THREE.Vector3();
171171- this.boundary.getSize(size);
172172-173173- const box = new THREE.BoxGeometry(size.x * 2, size.y * 2, size.z * 2);
174174- const mesh = new THREE.Mesh(
175175- box,
176176- mat ||
177177- new THREE.MeshStandardMaterial({
178178- wireframe: true
179179- })
180180- );
181181- this.boundary.getCenter(mesh.position);
182182-183183- parent ??= new THREE.Object3D();
184184- parent.add(mesh);
185185-186186- if (this.subdivisions) {
187187- for (const sub of this.subdivisions) sub.showBoxes(mat, parent);
188188- }
189189- return parent;
190190- }
191191-192192- show(
193193- opts: {
194194- pointsOnly?: boolean;
195195- mat?: THREE.Material;
196196- size?: number;
197197- sizeAttenuation?: boolean;
198198- p?: THREE.Vector3;
199199- min?: number;
200200- } = {}
201201- ) {
202202- opts ??= {};
203203-204204- const pointsOnly = opts.pointsOnly;
205205- let mat = opts.mat;
206206- const points = this.all();
207207-208208- const pointsGeo = new THREE.BufferGeometry();
209209- const positionData = new Float32Array(points.length * 3);
210210- const colorData = new Float32Array(points.length * 3);
211211-212212- let q;
213213-214214- if (opts.p && opts.min) {
215215- for (const point of points) {
216216- // @ts-expect-error - close is not a property of Vector3
217217- point.close = false;
218218- }
219219- q = this.query(opts.p, opts.min);
220220-221221- for (const point of q) {
222222- // @ts-expect-error - close is not a property of Vector3
223223- point.close = true;
224224- }
225225- }
226226-227227- for (let i = 0; i < points.length; i++) {
228228- positionData[i * 3] = points[i].x;
229229- positionData[i * 3 + 1] = points[i].y;
230230- positionData[i * 3 + 2] = points[i].z;
231231-232232- // @ts-expect-error - close is not a property of Vector3
233233- colorData[i * 3] = points[i].close ? 1 : 0.7;
234234-235235- // @ts-expect-error - close is not a property of Vector3
236236- colorData[i * 3 + 1] = points[i].close ? 0 : 0.7;
237237-238238- // @ts-expect-error - close is not a property of Vector3
239239- colorData[i * 3 + 2] = points[i].close ? 0 : 0.7;
240240- }
241241- pointsGeo.setAttribute('position', new THREE.BufferAttribute(positionData, 3));
242242- pointsGeo.setAttribute('color', new THREE.BufferAttribute(colorData, 3));
243243- const pointMesh = new THREE.Points(
244244- pointsGeo,
245245- new THREE.PointsMaterial({
246246- size: opts.size || 1,
247247- sizeAttenuation: opts.sizeAttenuation || false,
248248- vertexColors: true
249249- })
250250- );
251251- if (pointsOnly) return pointMesh;
252252-253253- mat =
254254- mat ||
255255- new THREE.MeshStandardMaterial({
256256- transparent: true,
257257- opacity: 0.01,
258258- depthTest: false
259259- });
260260- const boxes = this.showBoxes(mat);
261261- boxes.add(pointMesh);
262262- return boxes;
263263- }
264264-265265- all(arr: THREE.Vector3[] = []) {
266266- arr ??= [];
267267- for (const p of this.points) {
268268- arr.push(p);
269269- }
270270- if (this.subdivisions) {
271271- for (const subs of this.subdivisions) subs.all(arr);
272272- }
273273- return arr;
274274- }
275275-}
+77
src/worlds/materials/AtmosphereMaterial.ts
···11+import { ShaderMaterial, Vector3 } from "three";
22+33+export function createAtmosphereMaterial(
44+ color: Vector3 | undefined = undefined,
55+ lightDirection: Vector3 | undefined = undefined,
66+) {
77+ const uniforms = {
88+ lightDirection: {
99+ value: (lightDirection ?? new Vector3(1.0, 1.0, 0.0)).normalize(),
1010+ },
1111+ atmosphereColor: { value: color ?? new Vector3(0.3, 0.6, 1.0) },
1212+ };
1313+1414+ const atmosphereShader = {
1515+ uniforms,
1616+ vertexShader: `
1717+ varying vec3 vNormal;
1818+ varying vec3 vViewLightDirection; // Light direction relative to the camera
1919+ varying vec3 vViewPosition; // View position
2020+2121+ uniform vec3 lightDirection; // Light direction in world space
2222+2323+ void main() {
2424+ // Transform the vertex normal to world space
2525+ vNormal = normalize(normalMatrix * normal);
2626+ // Transform the light direction to view space
2727+ vViewLightDirection = (viewMatrix * vec4(lightDirection, 0.0)).xyz;
2828+ vViewPosition = (modelViewMatrix * vec4(position, 1.0)).xyz;
2929+3030+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
3131+ }
3232+ `,
3333+ fragmentShader: `
3434+ varying vec3 vNormal;
3535+ varying vec3 vViewLightDirection;
3636+ varying vec3 vViewPosition;
3737+ uniform vec3 atmosphereColor;
3838+3939+ void main() {
4040+4141+ // Normalize the normal and the view direction
4242+ vec3 viewDirection = normalize(vViewPosition);
4343+4444+ // Calculate how much of the surface is perpendicular to the view direction
4545+ float viewFactor = dot(normalize(vNormal), -viewDirection);
4646+4747+ // Calculate the dot product of the light direction and the surface normal
4848+ float lightFactor = dot(normalize(vNormal), normalize(vViewLightDirection));
4949+5050+ // Use smoothstep to soften the transition from light to dark side
5151+ lightFactor = smoothstep(-0.2, 0.4, lightFactor);
5252+5353+ // Ensure a minimum glow, even on the dark side
5454+ float minGlow = 0.2; // Adjust this to control how much it glows on the dark side
5555+ lightFactor = mix(minGlow, 1.0, lightFactor);
5656+5757+ // Adjust the intensity for the atmosphere's glow, including the minimum glow
5858+ float dotProduct = dot(normalize(vNormal), vec3(0, 0.0, 1.0));
5959+ float intensity = pow(dotProduct, 8.0);
6060+ intensity *= lightFactor;
6161+6262+ viewFactor = clamp(1.0 - viewFactor, 0.0, 1.0);
6363+6464+ // Use smoothstep for a smooth transition on the edges
6565+ float atmosphereFactor = smoothstep(0.2, 0.6, viewFactor);
6666+6767+ // Output the final color
6868+ gl_FragColor = vec4(atmosphereColor, intensity * atmosphereFactor);
6969+ // Set the final fragment color with the computed intensity
7070+ //gl_FragColor = vec4(0.3, 0.6, 1.0, 1.0) * intensity;
7171+ }`,
7272+ transparent: true,
7373+ depthWrite: false,
7474+ };
7575+7676+ return new ShaderMaterial(atmosphereShader);
7777+}