[READ-ONLY] Mirror of https://github.com/flo-bit/flo-bit.github.io. my personal website, w/ astro, svelte, tailwind, typescript, threlte flo-bit.dev/
portfolio portfolio-website svelte sveltekit tailwind threejs threlte typescript
0

Configure Feed

Select the types of activity you want to include in your feed.

update planets

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