···11-import { IcosahedronGeometry, Vector3, BufferAttribute } from "three";
11+import {
22+ IcosahedronGeometry,
33+ Vector3,
44+ BufferAttribute,
55+ Float32BufferAttribute,
66+ Color,
77+} from "three";
2833-import { Biome, type BiomeOptions } from "./biome";
99+import { Biome } from "./biome";
410import { type PlanetOptions } from "./planet";
55-import UberNoise from 'uber-noise';
1111+import UberNoise from "uber-noise";
1212+import { type VertexInfo } from "./types";
613714onmessage = function (e) {
815 const { type, data, requestId } = e.data;
···1724 const oceanPositions = oceanGeometry.getAttribute("position").array.buffer;
1825 const oceanColors = oceanGeometry.getAttribute("color").array.buffer;
1926 const oceanNormals = oceanGeometry.getAttribute("normal").array.buffer;
2727+ const oceanMorphPositions =
2828+ oceanGeometry.morphAttributes.position[0].array.buffer;
2929+ const oceanMorphNormals =
3030+ oceanGeometry.morphAttributes.normal[0].array.buffer;
20312132 postMessage(
2233 {
···2940 oceanColors,
3041 oceanNormals,
3142 vegetation,
4343+ oceanMorphPositions,
4444+ oceanMorphNormals,
3245 },
3346 requestId,
3447 },
3548 // @ts-expect-error - hmm
3636- [positions, colors, normals, oceanPositions, oceanColors, oceanNormals],
4949+ [
5050+ positions,
5151+ colors,
5252+ normals,
5353+ oceanPositions,
5454+ oceanColors,
5555+ oceanNormals,
5656+ oceanMorphPositions,
5757+ oceanMorphNormals,
5858+ ],
3759 );
3860 } else {
3961 console.error("Unknown message type", type);
4062 }
4163};
42644343-function createGeometry({
4444- biomeOptions,
4545- planetOptions,
4646-}: {
4747- biomeOptions: BiomeOptions;
4848- planetOptions: PlanetOptions;
4949-}): [IcosahedronGeometry, IcosahedronGeometry, Record<string, Vector3[]>] {
6565+function createGeometry(
6666+ planetOptions: PlanetOptions,
6767+): [IcosahedronGeometry, IcosahedronGeometry, Record<string, Vector3[]>] {
5068 const sphere = new IcosahedronGeometry(1, planetOptions.detail ?? 50);
5169 const oceanSphere = new IcosahedronGeometry(1, planetOptions.detail ?? 50);
52705353- const biome = new Biome(biomeOptions);
7171+ const biome = new Biome(planetOptions.biome);
54725573 const vertices = sphere.getAttribute("position");
5674 const oceanVertices = oceanSphere.getAttribute("position");
···5876 const faceSize = (Math.PI * 4) / faceCount;
5977 console.log("faces:", faceCount);
60786161- const calculatedVertices = new Map<
6262- string,
6363- {
6464- height: number;
6565- seaHeight: number;
6666- scatter: Vector3;
6767- }
6868- >();
7979+ // store calculated vertices so we don't have to recalculate them
8080+ // once store by hashed position (so we can find vertices of different faces that have the same position)
8181+ const calculatedVertices = new Map<string, VertexInfo>();
8282+ // and once by index for vegetation placement
8383+ const calculatedVerticesArray: VertexInfo[] = new Array(faceCount);
69847085 const colors = new Float32Array(vertices.count * 3);
7186 const oceanColors = new Float32Array(oceanVertices.count * 3);
···8398 a.fromBufferAttribute(vertices, 0);
8499 b.fromBufferAttribute(vertices, 1);
851008686- // default to scatter = distance of first edge
8787- const scatterAmount = planetOptions.scatter ?? b.distanceTo(a);
101101+ const faceSideLength = a.distanceTo(b);
102102+103103+ // scatterAmount is based on side length of face (all faces have the same size)
104104+ const scatterAmount = (planetOptions.scatter ?? 1.2) * faceSideLength;
88105 const scatterScale = 100;
8910690107 const scatterNoise = new UberNoise({
···94111 seed: 0,
95112 });
96113114114+ oceanSphere.morphAttributes.position = [];
115115+ oceanSphere.morphAttributes.normal = [];
116116+117117+ const oceanMorphPositions: number[] = [];
118118+ const oceanMorphNormals: number[] = [];
119119+120120+ const oceanA = new Vector3(),
121121+ oceanB = new Vector3(),
122122+ oceanC = new Vector3(),
123123+ oceanD = new Vector3(),
124124+ oceanE = new Vector3(),
125125+ oceanF = new Vector3();
126126+127127+ const temp = new Vector3();
128128+129129+ // go through all faces
130130+ // - calculate height and scatter for vertices
131131+ // - calculate height for ocean vertices
132132+ // - calculate height for ocean morph vertices
133133+ // - calculate color for vertices and ocean vertices
134134+ // - calculate normal for vertices and ocean vertices
135135+ // - add vegetation
97136 for (let i = 0; i < vertices.count; i += 3) {
98137 a.fromBufferAttribute(vertices, i);
99138 b.fromBufferAttribute(vertices, i + 1);
100139 c.fromBufferAttribute(vertices, i + 2);
140140+141141+ oceanA.fromBufferAttribute(oceanVertices, i);
142142+ oceanB.fromBufferAttribute(oceanVertices, i + 1);
143143+ oceanC.fromBufferAttribute(oceanVertices, i + 2);
101144102145 mid.set(0, 0, 0);
103146 mid.addVectors(a, b).add(c).divideScalar(3);
104147105148 let normalizedHeight = 0;
106149150150+ // go through all vertices of the face
107151 for (let j = 0; j < 3; j++) {
108152 let v = a;
109153 if (j === 1) v = b;
110154 if (j === 2) v = c;
111111- const key = `${v.x.toFixed(5)},${v.y.toFixed(5)},${v.z.toFixed(5)}`;
112155156156+ // lets see if we already have info for this vertex
157157+ const key = `${v.x.toFixed(5)},${v.y.toFixed(5)},${v.z.toFixed(5)}`;
113158 let move = calculatedVertices.get(key);
114159160160+ // if not, calculate it
115161 if (!move) {
162162+ // calculate height and scatter
116163 const height = biome.getHeight(v) + 1;
117164 const scatterX = scatterNoise.get(v);
118165 const scatterY = scatterNoise.get(
···125172 v.x + scatterScale * 200,
126173 v.y - scatterScale * 200,
127174 );
175175+ // calculate sea height and sea morph height
128176 const seaHeight = biome.getSeaHeight(v) + 1;
177177+ const secondSeaHeight = biome.getSeaHeight(v.addScalar(100)) + 1;
178178+179179+ v.subScalar(100);
129180130181 move = {
131182 height,
132183 scatter: new Vector3(scatterX, scatterY, scatterZ),
133184 seaHeight,
185185+ seaMorph: secondSeaHeight,
134186 };
135187 calculatedVertices.set(key, move);
136188 }
137189138138- normalizedHeight += move.height - 1;
139139- v.add(move.scatter).normalize().multiplyScalar(move.height);
190190+ // we store this info for later use (vegetation placement)
191191+ calculatedVerticesArray[i + j] = move;
140192193193+ // we add height here so we can calculate the average normalized height of the face later
194194+ normalizedHeight += move.height - 1;
195195+196196+ // move vertex based on height and scatter
197197+ v.add(move.scatter).normalize().multiplyScalar(move.height);
141198 vertices.setXYZ(i + j, v.x, v.y, v.z);
142199143143- const oceanV = v.clone().normalize().multiplyScalar(move.seaHeight);
200200+ // move ocean vertex based on sea height and scatter
201201+ let oceanV = oceanA;
202202+ if (j === 1) oceanV = oceanB;
203203+ if (j === 2) oceanV = oceanC;
204204+ oceanV.add(move.scatter).normalize().multiplyScalar(move.seaMorph);
205205+ oceanMorphPositions.push(oceanV.x, oceanV.y, oceanV.z);
206206+207207+ // move ocean morph vertex based on sea height and scatter
208208+ if (j === 0) {
209209+ oceanD.copy(oceanV);
210210+ } else if (j === 1) {
211211+ oceanE.copy(oceanV);
212212+ } else if (j === 2) {
213213+ oceanF.copy(oceanV);
214214+ }
215215+ oceanV.normalize().multiplyScalar(move.seaHeight);
144216 oceanVertices.setXYZ(i + j, oceanV.x, oceanV.y, oceanV.z);
145217 }
146218219219+ // calculate normalized height for the face (between -1 and 1, 0 is sea level)
147220 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)
221221+ normalizedHeight =
222222+ Math.min(-normalizedHeight / biome.min, 0) +
223223+ Math.max(normalizedHeight / biome.max, 0);
224224+ // now normalizedHeight should be between -1 and 1 (0 is sea level)
225225+ // this will be used for color calculation and vegetation placement
151226152152- 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- }
165165-166166- 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- }
183183-184184- // calculate new normal
185185- const normal = new Vector3();
186186- normal.crossVectors(b.clone().sub(a), c.clone().sub(a)).normalize();
187187-227227+ // calculate face normal
228228+ temp.crossVectors(b.clone().sub(a), c.clone().sub(a)).normalize();
188229 // 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);
230230+ normals.setXYZ(i, temp.x, temp.y, temp.z);
231231+ normals.setXYZ(i + 1, temp.x, temp.y, temp.z);
232232+ normals.setXYZ(i + 2, temp.x, temp.y, temp.z);
192233193234 // calculate steepness (acos of dot product of normal and up vector)
194235 // (up vector = old mid point on sphere)
195195- const steepness = Math.acos(Math.abs(normal.dot(mid)));
236236+ const steepness = Math.acos(Math.abs(temp.dot(mid)));
196237 // steepness is between 0 and PI/2
238238+ // this will be used for color calculation and vegetation placement
197239240240+ // calculate color for face
198241 const color = biome.getColor(mid, normalizedHeight, steepness);
199199-200242 // flat shading, so all colors for the face are the same
201243 if (color) {
202244 colors[i * 3] = color.r;
···212254 colors[i * 3 + 8] = color.b;
213255 }
214256215215- // calculate ocean vertices
257257+ // calculate ocean face color
216258 const oceanColor = biome.getSeaColor(mid, normalizedHeight);
217259218260 if (oceanColor) {
···229271 oceanColors[i * 3 + 8] = oceanColor.b;
230272 }
231273232232- 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);
274274+ // calculate ocean normals
275275+ temp
276276+ .crossVectors(oceanB.clone().sub(oceanA), oceanC.clone().sub(oceanA))
277277+ .normalize();
278278+ oceanNormals.setXYZ(i, temp.x, temp.y, temp.z);
279279+ oceanNormals.setXYZ(i + 1, temp.x, temp.y, temp.z);
280280+ oceanNormals.setXYZ(i + 2, temp.x, temp.y, temp.z);
281281+282282+ // calculate ocean morph normals
283283+ temp
284284+ .crossVectors(oceanE.clone().sub(oceanD), oceanF.clone().sub(oceanD))
285285+ .normalize();
286286+ oceanMorphNormals.push(temp.x, temp.y, temp.z);
287287+ oceanMorphNormals.push(temp.x, temp.y, temp.z);
288288+ oceanMorphNormals.push(temp.x, temp.y, temp.z);
289289+290290+ // place vegetation
291291+ for (
292292+ let j = 0;
293293+ biome.options.vegetation && j < biome.options.vegetation.items.length;
294294+ j++
295295+ ) {
296296+ const vegetation = biome.options.vegetation.items[j];
297297+ if (Math.random() < faceSize * (vegetation.density ?? 1)) {
298298+ // discard if point is below or above height limits
299299+ if (
300300+ vegetation.minimumHeight !== undefined &&
301301+ normalizedHeight < vegetation.minimumHeight
302302+ ) {
303303+ continue;
304304+ }
305305+ // default minimumHeight is 0 (= above sea level)
306306+ if (vegetation.minimumHeight === undefined && normalizedHeight < 0) {
307307+ continue;
308308+ }
309309+ if (
310310+ vegetation.maximumHeight !== undefined &&
311311+ normalizedHeight > vegetation.maximumHeight
312312+ ) {
313313+ continue;
314314+ }
315315+316316+ // discard if point is below or above slope limits
317317+ if (
318318+ vegetation.minimumSlope !== undefined &&
319319+ steepness < vegetation.minimumSlope
320320+ ) {
321321+ continue;
322322+ }
323323+ if (
324324+ vegetation.maximumSlope !== undefined &&
325325+ steepness > vegetation.maximumSlope
326326+ ) {
327327+ continue;
328328+ }
329329+330330+ if (!placedVegetation[vegetation.name]) {
331331+ placedVegetation[vegetation.name] = [];
332332+ }
333333+ let height = a.length();
334334+ placedVegetation[vegetation.name].push(
335335+ a
336336+ .clone()
337337+ .normalize()
338338+ .multiplyScalar(height + 0.005),
339339+ );
340340+341341+ biome.addVegetation(
342342+ vegetation,
343343+ a.normalize(),
344344+ normalizedHeight,
345345+ steepness,
346346+ );
347347+ break;
348348+ }
349349+ }
235350 }
351351+352352+ const maxDist = 0.14;
353353+354354+ const color = new Color();
355355+356356+ // go through all vertices again and update height and color based on vegetation
357357+ for (let i = 0; i < vertices.count; i += 3) {
358358+ a.fromBufferAttribute(vertices, i);
359359+ a.normalize();
360360+ b.fromBufferAttribute(vertices, i + 1);
361361+ b.normalize();
362362+ c.fromBufferAttribute(vertices, i + 2);
363363+ c.normalize();
364364+365365+ color.setRGB(colors[i * 3], colors[i * 3 + 1], colors[i * 3 + 2]);
366366+367367+ const output = biome.vegetationHeightAndColorForFace(
368368+ a,
369369+ b,
370370+ c,
371371+ color,
372372+ faceSideLength,
373373+ );
374374+375375+ const moveDataA = calculatedVerticesArray[i];
376376+ const moveDataB = calculatedVerticesArray[i + 1];
377377+ const moveDataC = calculatedVerticesArray[i + 2];
378378+379379+ // update height based on vegetation
380380+ a.normalize().multiplyScalar(moveDataA.height + output.heightA);
381381+ b.normalize().multiplyScalar(moveDataB.height + output.heightB);
382382+ c.normalize().multiplyScalar(moveDataC.height + output.heightC);
383383+384384+ vertices.setXYZ(i, a.x, a.y, a.z);
385385+ vertices.setXYZ(i + 1, b.x, b.y, b.z);
386386+ vertices.setXYZ(i + 2, c.x, c.y, c.z);
387387+388388+ // update color based on vegetation
389389+ colors[i * 3] = output.color.r;
390390+ colors[i * 3 + 1] = output.color.g;
391391+ colors[i * 3 + 2] = output.color.b;
392392+393393+ colors[i * 3 + 3] = output.color.r;
394394+ colors[i * 3 + 4] = output.color.g;
395395+ colors[i * 3 + 5] = output.color.b;
396396+397397+ colors[i * 3 + 6] = output.color.r;
398398+ colors[i * 3 + 7] = output.color.g;
399399+ colors[i * 3 + 8] = output.color.b;
400400+ }
401401+402402+ oceanSphere.morphAttributes.position[0] = new Float32BufferAttribute(
403403+ oceanMorphPositions,
404404+ 3,
405405+ );
406406+ oceanSphere.morphAttributes.normal[0] = new Float32BufferAttribute(
407407+ oceanMorphNormals,
408408+ 3,
409409+ );
236410237411 sphere.setAttribute("color", new BufferAttribute(colors, 3));
238412 oceanSphere.setAttribute("color", new BufferAttribute(oceanColors, 3));
+28
src/lib/3D/worlds/helper/helper.ts
···11+import { BufferGeometry, Float32BufferAttribute } from "three";
22+33+export function createBufferGeometry(
44+ positions: number[],
55+ colors?: number[],
66+ normals?: number[],
77+) {
88+ const geometry = new BufferGeometry();
99+ geometry.setAttribute(
1010+ "position",
1111+ new Float32BufferAttribute(new Float32Array(positions), 3),
1212+ );
1313+1414+ if (colors) {
1515+ geometry.setAttribute(
1616+ "color",
1717+ new Float32BufferAttribute(new Float32Array(colors), 3),
1818+ );
1919+ }
2020+ if (normals) {
2121+ geometry.setAttribute(
2222+ "normal",
2323+ new Float32BufferAttribute(new Float32Array(normals), 3),
2424+ );
2525+ }
2626+2727+ return geometry;
2828+}
+292
src/lib/3D/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 class Octree<T = unknown> {
2929+ boundary: Box3;
3030+3131+ points: (Vector3 & { data?: T })[];
3232+3333+ capacity: number;
3434+3535+ subdivisions: Octree[] | undefined = undefined;
3636+3737+ constructor(opts: OctreeOptions = {}) {
3838+ opts ??= {};
3939+4040+ this.points = [];
4141+4242+ if (opts.bounds) {
4343+ this.boundary = opts.bounds.clone();
4444+ } else if (opts.size) {
4545+ const s = opts.size;
4646+ this.boundary = new Box3(new Vector3(-s, -s, -s), new Vector3(s, s, s));
4747+ } else if (opts.min || opts.max) {
4848+ const min = opts.min || new Vector3(-1, -1, -1);
4949+ const max = opts.max || new Vector3(1, 1, 1);
5050+ this.boundary = new Box3(min, max);
5151+ } else if (opts.points && opts.points.length > 0) {
5252+ const min = opts.points[0].clone();
5353+ const max = opts.points[0].clone();
5454+ for (const p of opts.points) {
5555+ min.x = Math.min(min.x, p.x);
5656+ min.y = Math.min(min.y, p.y);
5757+ min.z = Math.min(min.z, p.z);
5858+5959+ max.x = Math.max(max.x, p.x);
6060+ max.y = Math.max(max.y, p.y);
6161+ max.z = Math.max(max.z, p.z);
6262+ }
6363+ this.boundary = new Box3(min, max);
6464+ } else {
6565+ this.boundary = new Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1));
6666+ }
6767+6868+ this.capacity = opts.capacity || 4;
6969+7070+ if (opts.points) {
7171+ for (const p of opts.points) {
7272+ this.insertXYZ(p.x, p.y, p.z);
7373+ }
7474+ }
7575+ }
7676+7777+ subdivide() {
7878+ // if already subdivided exit silently
7979+ if (this.subdivisions != undefined) return;
8080+8181+ // divide each dimension => 2 * 2 * 2 = 8 subdivisions
8282+ const size = new Vector3();
8383+ const subdivisions: Octree[] = [];
8484+ for (let x = 0; x < 2; x++) {
8585+ for (let y = 0; y < 2; y++) {
8686+ for (let z = 0; z < 2; z++) {
8787+ const min = this.boundary.min.clone();
8888+ const max = this.boundary.max.clone();
8989+ this.boundary.getSize(size);
9090+ size.divideScalar(2);
9191+9292+ min.x += x * size.x;
9393+ min.y += y * size.y;
9494+ min.z += z * size.z;
9595+ max.x -= (1 - x) * size.x;
9696+ max.y -= (1 - y) * size.y;
9797+ max.z -= (1 - z) * size.z;
9898+9999+ subdivisions.push(
100100+ new Octree({
101101+ min: min,
102102+ max: max,
103103+ capacity: this.capacity,
104104+ }),
105105+ );
106106+ }
107107+ }
108108+ }
109109+ this.subdivisions = subdivisions;
110110+ }
111111+112112+ // returns array of points where
113113+ // distance between pos and point is less than dist
114114+ query(pos: Vector3 & { data?: T }, dist = 1): (Vector3 & { data?: T })[] {
115115+ const points = this.queryBoxXYZ(pos.x, pos.y, pos.z, dist);
116116+117117+ return points.filter((p) => p.distanceTo(pos) < dist);
118118+ }
119119+120120+ // vector3 free version, returns points around xyz
121121+ queryXYZ(x: number, y: number, z: number, dist: number) {
122122+ const point = new Vector3(x, y, z);
123123+124124+ return this.query(point, dist);
125125+ }
126126+127127+ queryBoxXYZ(x: number, y: number, z: number, s: number) {
128128+ const min = new Vector3(x - s, y - s, z - s),
129129+ max = new Vector3(x + s, y + s, z + s);
130130+ const box = new Box3(min, max);
131131+132132+ return this.queryBox(box);
133133+ }
134134+135135+ queryBox(box: Box3, found: (Vector3 & { data?: T })[] = []) {
136136+ found ??= [];
137137+138138+ if (!box.intersectsBox(this.boundary)) return found;
139139+140140+ for (const p of this.points) {
141141+ if (box.containsPoint(p)) found.push(p);
142142+ }
143143+ if (this.subdivisions) {
144144+ for (const sub of this.subdivisions) {
145145+ sub.queryBox(box, found);
146146+ }
147147+ }
148148+ return found;
149149+ }
150150+151151+ // returns true if no points are closer than dist to point
152152+ minDist(pos: Vector3, dist: number) {
153153+ return this.query(pos, dist).length < 1;
154154+ }
155155+156156+ // insert point with optional data (sets vec.data = data)
157157+ insert(pos: Vector3 & { data?: T }, data: T | undefined = undefined) {
158158+ return this.insertPoint(pos, data);
159159+ }
160160+161161+ // vector3 free version
162162+ insertXYZ(x: number, y: number, z: number, data: T | undefined = undefined) {
163163+ return this.insertPoint(new Vector3(x, y, z), data);
164164+ }
165165+166166+ insertPoint(p: Vector3, data: T | undefined = undefined) {
167167+ p = p.clone();
168168+169169+ // @ts-expect-error - data is not a property of Vector3
170170+ if (data) p.data = data;
171171+172172+ if (!this.boundary.containsPoint(p)) return false;
173173+174174+ if (this.points.length < this.capacity) {
175175+ this.points.push(p);
176176+ return true;
177177+ } else {
178178+ this.subdivide();
179179+ let added = false;
180180+ for (const sub of this.subdivisions ?? []) {
181181+ if (sub.insertPoint(p, data)) added = true;
182182+ }
183183+ return added;
184184+ }
185185+ }
186186+187187+ showBoxes(mat: Material, parent: Object3D | undefined = undefined) {
188188+ const size = new Vector3();
189189+ this.boundary.getSize(size);
190190+191191+ const box = new BoxGeometry(size.x * 2, size.y * 2, size.z * 2);
192192+ const mesh = new Mesh(
193193+ box,
194194+ mat ||
195195+ new MeshStandardMaterial({
196196+ wireframe: true,
197197+ }),
198198+ );
199199+ this.boundary.getCenter(mesh.position);
200200+201201+ parent ??= new Object3D();
202202+ parent.add(mesh);
203203+204204+ if (this.subdivisions) {
205205+ for (const sub of this.subdivisions) sub.showBoxes(mat, parent);
206206+ }
207207+ return parent;
208208+ }
209209+210210+ show(
211211+ opts: {
212212+ pointsOnly?: boolean;
213213+ mat?: Material;
214214+ size?: number;
215215+ sizeAttenuation?: boolean;
216216+ p?: Vector3;
217217+ min?: number;
218218+ } = {},
219219+ ) {
220220+ opts ??= {};
221221+222222+ const pointsOnly = opts.pointsOnly;
223223+ let mat = opts.mat;
224224+ const points = this.all();
225225+226226+ const pointsGeo = new BufferGeometry();
227227+ const positionData = new Float32Array(points.length * 3);
228228+ const colorData = new Float32Array(points.length * 3);
229229+230230+ let q;
231231+232232+ if (opts.p && opts.min) {
233233+ for (const point of points) {
234234+ // @ts-expect-error - close is not a property of Vector3
235235+ point.close = false;
236236+ }
237237+ q = this.query(opts.p, opts.min);
238238+239239+ for (const point of q) {
240240+ point.close = true;
241241+ }
242242+ }
243243+244244+ for (let i = 0; i < points.length; i++) {
245245+ positionData[i * 3] = points[i].x;
246246+ positionData[i * 3 + 1] = points[i].y;
247247+ positionData[i * 3 + 2] = points[i].z;
248248+249249+ // @ts-expect-error - close is not a property of Vector3
250250+ colorData[i * 3] = points[i].close ? 1 : 0.7;
251251+252252+ // @ts-expect-error - close is not a property of Vector3
253253+ colorData[i * 3 + 1] = points[i].close ? 0 : 0.7;
254254+255255+ // @ts-expect-error - close is not a property of Vector3
256256+ colorData[i * 3 + 2] = points[i].close ? 0 : 0.7;
257257+ }
258258+ pointsGeo.setAttribute("position", new BufferAttribute(positionData, 3));
259259+ pointsGeo.setAttribute("color", new BufferAttribute(colorData, 3));
260260+ const pointMesh = new Points(
261261+ pointsGeo,
262262+ new PointsMaterial({
263263+ size: opts.size || 1,
264264+ sizeAttenuation: opts.sizeAttenuation || false,
265265+ vertexColors: true,
266266+ }),
267267+ );
268268+ if (pointsOnly) return pointMesh;
269269+270270+ mat =
271271+ mat ||
272272+ new MeshStandardMaterial({
273273+ transparent: true,
274274+ opacity: 0.01,
275275+ depthTest: false,
276276+ });
277277+ const boxes = this.showBoxes(mat);
278278+ boxes.add(pointMesh);
279279+ return boxes;
280280+ }
281281+282282+ all(arr: (Vector3 & { data?: T })[] = []) {
283283+ arr ??= [];
284284+ for (const p of this.points) {
285285+ arr.push(p);
286286+ }
287287+ if (this.subdivisions) {
288288+ for (const subs of this.subdivisions) subs.all(arr);
289289+ }
290290+ return arr;
291291+ }
292292+}
-275
src/lib/3D/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/lib/3D/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+}