[READ-ONLY] Mirror of https://github.com/flo-bit/tiny-planets. procedurally generated tiny planets in the browsers flo-bit.dev/tiny-planets/
low-poly planets procedural-generation threejs
0

Configure Feed

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

ground color,height around vegetation

Florian (Sep 23, 2024, 12:30 PM +0200) 64f120d0 1b36d97c

+561 -393
+4 -1
src/script.ts
··· 105 105 } 106 106 107 107 console.time("planet"); 108 - const planet = new Planet({ biome: { preset } }); 108 + const planet = new Planet({ 109 + detail: 50, 110 + biome: { preset }, 111 + }); 109 112 let mesh = await planet.create(); 110 113 scene.remove(planetMesh); 111 114 scene.add(mesh);
+35 -11
src/worlds/biome.ts
··· 6 6 type ColorGradientOptions, 7 7 } from "./helper/colorgradient"; 8 8 import { biomePresets } from "./presets"; 9 + import { Octree } from "./helper/octree"; 10 + 11 + export type VegetationItem = { 12 + name: string; 13 + density: number; 14 + 15 + minimumHeight?: number; 16 + maximumHeight?: number; 17 + 18 + minimumSlope?: number; 19 + maximumSlope?: number; 20 + 21 + minimumDistance?: number; 22 + maximumDistance?: number; 23 + colors?: Record<string, { array?: number[] }>; 24 + }; 9 25 10 26 export type BiomeOptions = { 11 27 name?: string; ··· 21 37 tintColor?: number; 22 38 23 39 vegetation?: { 24 - items: { 25 - name: string; 26 - density: number; 27 - 28 - minimumHeight?: number; 29 - maximumHeight?: number; 30 - 31 - minimumDistance?: number; 32 - maximumDistance?: number; 33 - colors?: Record<string, { array?: number[] }>; 34 - }[]; 40 + defaults?: { 41 + density?: number; 42 + }; 43 + items: VegetationItem[]; 35 44 }; 36 45 }; 37 46 ··· 43 52 seaColors: ColorGradient | undefined; 44 53 45 54 options: BiomeOptions; 55 + 56 + vegetationPositions: Octree = new Octree(); 46 57 47 58 constructor(opts: BiomeOptions = {}) { 48 59 if (opts.preset) { ··· 121 132 } 122 133 123 134 return undefined; 135 + } 136 + 137 + addVegetation( 138 + item: VegetationItem, 139 + position: Vector3, 140 + normalizedHeight: number, 141 + steepness: number, 142 + ) { 143 + this.vegetationPositions.insert(position, item); 144 + } 145 + 146 + itemsAround(position: Vector3, radius: number): Vector3[] { 147 + return this.vegetationPositions.query(position, radius); 124 148 } 125 149 }
+18 -5
src/worlds/planet.ts
··· 12 12 import { loadModels } from "./models"; 13 13 14 14 import oceansCausticMaterial from "./materials/OceanCausticsMaterial"; 15 - import atmosphereMaterial from "./materials/AtmosphereMaterial"; 15 + import { createAtmosphereMaterial } from "./materials/AtmosphereMaterial"; 16 16 17 17 export type PlanetOptions = { 18 18 scatter?: number; ··· 20 20 ground?: number; 21 21 22 22 detail?: number; 23 + 24 + atmosphere?: { 25 + color?: Vector3; 26 + height?: number; 27 + }; 23 28 24 29 biome?: BiomeOptions; 25 30 }; ··· 100 105 "color", 101 106 new Float32BufferAttribute(new Float32Array(data.oceanColors), 3), 102 107 ); 108 + oceanGeometry.setAttribute( 109 + "normal", 110 + new Float32BufferAttribute(new Float32Array(data.oceanNormals), 3), 111 + ); 103 112 // set morph targets 104 113 oceanGeometry.morphAttributes.position = [ 105 114 new Float32BufferAttribute( ··· 110 119 oceanGeometry.morphAttributes.normal = [ 111 120 new Float32BufferAttribute(new Float32Array(data.oceanMorphNormals), 3), 112 121 ]; 113 - 114 - oceanGeometry.computeVertexNormals(); 115 122 116 123 this.vegetationPositions = data.vegetation; 117 124 ··· 243 250 244 251 addAtmosphere(planet: Mesh) { 245 252 // Create the atmosphere geometry 246 - const atmosphereGeometry = new IcosahedronGeometry(1.2, 20); 247 - const atmosphere = new Mesh(atmosphereGeometry, atmosphereMaterial); 253 + const atmosphereGeometry = new IcosahedronGeometry( 254 + this.options.atmosphere?.height ?? 1.2, 255 + this.options.detail ?? 20, 256 + ); 257 + const atmosphere = new Mesh( 258 + atmosphereGeometry, 259 + createAtmosphereMaterial(this.options.atmosphere?.color), 260 + ); 248 261 atmosphere.renderOrder = 1; 249 262 planet.add(atmosphere); 250 263 }
+1 -2
src/worlds/presets.ts
··· 31 31 seaNoise: { 32 32 min: -0.008, 33 33 max: 0.008, 34 - scale: 8, 35 - octaves: 2, 34 + scale: 6, 36 35 }, 37 36 38 37 vegetation: {
+139 -33
src/worlds/worker.ts
··· 3 3 Vector3, 4 4 BufferAttribute, 5 5 Float32BufferAttribute, 6 + Color, 6 7 } from "three"; 7 8 8 9 import { Biome, type BiomeOptions } from "./biome"; ··· 85 86 } 86 87 >(); 87 88 89 + const calculatedVerticesArray: { 90 + height: number; 91 + scatter: Vector3; 92 + 93 + seaHeight: number; 94 + seaMorph: number; 95 + }[] = new Array(faceCount); 96 + 88 97 const colors = new Float32Array(vertices.count * 3); 89 98 const oceanColors = new Float32Array(oceanVertices.count * 3); 90 99 ··· 126 135 oceanF = new Vector3(); 127 136 128 137 const temp = new Vector3(); 138 + 139 + let normHeightMax = 0; 140 + let normHeightMin = 0; 129 141 130 142 for (let i = 0; i < vertices.count; i += 3) { 131 143 a.fromBufferAttribute(vertices, i); ··· 177 189 calculatedVertices.set(key, move); 178 190 } 179 191 192 + calculatedVerticesArray[i + j] = move; 193 + 180 194 normalizedHeight += move.height - 1; 181 195 v.add(move.scatter).normalize().multiplyScalar(move.height); 182 196 vertices.setXYZ(i + j, v.x, v.y, v.z); ··· 201 215 } 202 216 203 217 normalizedHeight /= 3; 204 - normalizedHeight = (normalizedHeight - biome.min) / (biome.max - biome.min); 205 - normalizedHeight = normalizedHeight * 2 - 1; 206 - // now averageHeight is between -1 and 1 (0 is sea level) 207 218 208 - for ( 209 - let j = 0; 210 - biome.options.vegetation && j < biome.options.vegetation.items.length; 211 - j++ 212 - ) { 213 - const vegetation = biome.options.vegetation.items[j]; 214 - if (Math.random() < faceSize * vegetation.density) { 215 - if ( 216 - vegetation.minimumHeight !== undefined && 217 - normalizedHeight < vegetation.minimumHeight 218 - ) { 219 - continue; 220 - } 219 + normalizedHeight = 220 + Math.min(-normalizedHeight / biome.min, 0) + 221 + Math.max(normalizedHeight / biome.max, 0); 222 + // now normalizedHeight should be between -1 and 1 (0 is sea level) 221 223 222 - if (vegetation.minimumHeight === undefined && normalizedHeight < 0) { 223 - continue; 224 - } 225 - 226 - if ( 227 - vegetation.maximumHeight !== undefined && 228 - normalizedHeight > vegetation.maximumHeight 229 - ) { 230 - continue; 231 - } 232 - if (!placedVegetation[vegetation.name]) { 233 - placedVegetation[vegetation.name] = []; 234 - } 235 - placedVegetation[vegetation.name].push(a.clone()); 236 - break; 237 - } 238 - } 224 + normHeightMax = Math.max(normHeightMax, normalizedHeight); 225 + normHeightMin = Math.min(normHeightMin, normalizedHeight); 239 226 240 227 // calculate new normal 241 228 temp.crossVectors(b.clone().sub(a), c.clone().sub(a)).normalize(); ··· 265 252 colors[i * 3 + 6] = color.r; 266 253 colors[i * 3 + 7] = color.g; 267 254 colors[i * 3 + 8] = color.b; 255 + } 256 + 257 + // place vegetation 258 + for ( 259 + let j = 0; 260 + biome.options.vegetation && j < biome.options.vegetation.items.length; 261 + j++ 262 + ) { 263 + const vegetation = biome.options.vegetation.items[j]; 264 + if (Math.random() < faceSize * vegetation.density) { 265 + // discard if point is below or above height limits 266 + if ( 267 + vegetation.minimumHeight !== undefined && 268 + normalizedHeight < vegetation.minimumHeight 269 + ) { 270 + continue; 271 + } 272 + // default minimumHeight is 0 (= above sea level) 273 + if (vegetation.minimumHeight === undefined && normalizedHeight < 0) { 274 + continue; 275 + } 276 + if ( 277 + vegetation.maximumHeight !== undefined && 278 + normalizedHeight > vegetation.maximumHeight 279 + ) { 280 + continue; 281 + } 282 + 283 + // discard if point is below or above slope limits 284 + if ( 285 + vegetation.minimumSlope !== undefined && 286 + steepness < vegetation.minimumSlope 287 + ) { 288 + continue; 289 + } 290 + if ( 291 + vegetation.maximumSlope !== undefined && 292 + steepness > vegetation.maximumSlope 293 + ) { 294 + continue; 295 + } 296 + 297 + if (!placedVegetation[vegetation.name]) { 298 + placedVegetation[vegetation.name] = []; 299 + } 300 + let height = a.length(); 301 + placedVegetation[vegetation.name].push( 302 + a 303 + .clone() 304 + .normalize() 305 + .multiplyScalar(height + 0.005), 306 + ); 307 + 308 + biome.addVegetation( 309 + vegetation, 310 + a.normalize(), 311 + normalizedHeight, 312 + steepness, 313 + ); 314 + break; 315 + } 268 316 } 269 317 270 318 // calculate ocean vertices ··· 300 348 oceanMorphNormals.push(temp.x, temp.y, temp.z); 301 349 oceanMorphNormals.push(temp.x, temp.y, temp.z); 302 350 oceanMorphNormals.push(temp.x, temp.y, temp.z); 351 + } 352 + 353 + console.log("normHeightMax", normHeightMax); 354 + console.log("normHeightMin", normHeightMin); 355 + 356 + const maxDist = 0.14; 357 + // go through all vertices again and update height and color based on vegetation 358 + for (let i = 0; i < vertices.count; i += 3) { 359 + let found = false; 360 + let closestDistAll = 1; 361 + for (let j = 0; j < 3; j++) { 362 + a.fromBufferAttribute(vertices, i + j); 363 + a.normalize(); 364 + 365 + let p = biome.itemsAround(a, maxDist); 366 + if (p.length > 0) { 367 + // find closest point 368 + let closest = p[0]; 369 + let closestDist = a.distanceTo(closest); 370 + for (let k = 1; k < p.length; k++) { 371 + let dist = a.distanceTo(p[k]); 372 + if (dist < closestDist) { 373 + closest = p[k]; 374 + closestDist = dist; 375 + } 376 + } 377 + 378 + let moveInfo = calculatedVerticesArray[i + j]; 379 + 380 + a.multiplyScalar( 381 + moveInfo.height + ((maxDist - closestDist) / maxDist) * 0.015, 382 + ); 383 + 384 + vertices.setXYZ(i + j, a.x, a.y, a.z); 385 + 386 + closestDistAll = Math.min(closestDist, closestDistAll); 387 + found = true; 388 + } 389 + } 390 + 391 + if (found) { 392 + let existingColor = new Color( 393 + colors[i * 3], 394 + colors[i * 3 + 1], 395 + colors[i * 3 + 2], 396 + ); 397 + 398 + // set color 399 + let newColor = new Color(0.1, 0.3, 0); 400 + 401 + newColor.lerp(existingColor, closestDistAll / maxDist); 402 + 403 + for (let j = 0; j < 3; j++) { 404 + colors[(i + j) * 3] = newColor.r; 405 + colors[(i + j) * 3 + 1] = newColor.g; 406 + colors[(i + j) * 3 + 2] = newColor.b; 407 + } 408 + } 303 409 } 304 410 305 411 oceanSphere.morphAttributes.position[0] = new Float32BufferAttribute(
+291
src/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 type Vector3Data = Vector3 & { data?: unknown }; 29 + 30 + export class Octree { 31 + boundary: Box3; 32 + 33 + points: Vector3[]; 34 + 35 + capacity: number; 36 + 37 + subdivisions: Octree[] | undefined = undefined; 38 + 39 + constructor(opts: OctreeOptions = {}) { 40 + opts ??= {}; 41 + 42 + this.points = []; 43 + 44 + if (opts.bounds) { 45 + this.boundary = opts.bounds.clone(); 46 + } else if (opts.size) { 47 + const s = opts.size; 48 + this.boundary = new Box3(new Vector3(-s, -s, -s), new Vector3(s, s, s)); 49 + } else if (opts.min || opts.max) { 50 + const min = opts.min || new Vector3(-1, -1, -1); 51 + const max = opts.max || new Vector3(1, 1, 1); 52 + this.boundary = new Box3(min, max); 53 + } else if (opts.points && opts.points.length > 0) { 54 + const min = opts.points[0].clone(); 55 + const max = opts.points[0].clone(); 56 + for (const p of opts.points) { 57 + min.x = Math.min(min.x, p.x); 58 + min.y = Math.min(min.y, p.y); 59 + min.z = Math.min(min.z, p.z); 60 + 61 + max.x = Math.max(max.x, p.x); 62 + max.y = Math.max(max.y, p.y); 63 + max.z = Math.max(max.z, p.z); 64 + } 65 + this.boundary = new Box3(min, max); 66 + } else { 67 + this.boundary = new Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1)); 68 + } 69 + 70 + this.capacity = opts.capacity || 4; 71 + 72 + if (opts.points) { 73 + for (const p of opts.points) { 74 + this.insertXYZ(p.x, p.y, p.z); 75 + } 76 + } 77 + } 78 + 79 + subdivide() { 80 + // if already subdivided exit silently 81 + if (this.subdivisions != undefined) return; 82 + 83 + // divide each dimension => 2 * 2 * 2 = 8 subdivisions 84 + const size = new Vector3(); 85 + const subdivisions: Octree[] = []; 86 + for (let x = 0; x < 2; x++) { 87 + for (let y = 0; y < 2; y++) { 88 + for (let z = 0; z < 2; z++) { 89 + const min = this.boundary.min.clone(); 90 + const max = this.boundary.max.clone(); 91 + this.boundary.getSize(size); 92 + size.divideScalar(2); 93 + 94 + min.x += x * size.x; 95 + min.y += y * size.y; 96 + min.z += z * size.z; 97 + max.x -= (1 - x) * size.x; 98 + max.y -= (1 - y) * size.y; 99 + max.z -= (1 - z) * size.z; 100 + 101 + subdivisions.push( 102 + new Octree({ 103 + min: min, 104 + max: max, 105 + capacity: this.capacity, 106 + }), 107 + ); 108 + } 109 + } 110 + } 111 + this.subdivisions = subdivisions; 112 + } 113 + 114 + // returns array of points where 115 + // distance between pos and point is less than dist 116 + query(pos: Vector3Data, dist = 1) { 117 + const points = this.queryXYZ(pos.x, pos.y, pos.z, dist); 118 + for (let i = points.length - 1; i >= 0; i--) { 119 + if (points[i].distanceTo(pos) > dist) points.splice(i, 1); 120 + } 121 + return points; 122 + } 123 + 124 + // vector3 free version, returns points in box around xyz 125 + queryXYZ(x: number, y: number, z: number, s: number) { 126 + const min = new Vector3(x - s, y - s, z - s), 127 + max = new Vector3(x + s, y + s, z + s); 128 + const box = new Box3(min, max); 129 + 130 + return this.queryBox(box); 131 + } 132 + 133 + queryBox(box: Box3, found: Vector3Data[] = []) { 134 + found ??= []; 135 + 136 + if (!box.intersectsBox(this.boundary)) return found; 137 + 138 + for (const p of this.points) { 139 + if (box.containsPoint(p)) found.push(p); 140 + } 141 + if (this.subdivisions) { 142 + for (const sub of this.subdivisions) { 143 + sub.queryBox(box, found); 144 + } 145 + } 146 + return found; 147 + } 148 + 149 + // returns true if no points are closer than dist to point 150 + minDist(pos: Vector3, dist: number) { 151 + return this.query(pos, dist).length < 1; 152 + } 153 + 154 + // insert point with optional data (sets vec.data = data) 155 + insert(pos: Vector3Data, data: unknown = undefined) { 156 + return this.insertPoint(pos, data); 157 + } 158 + 159 + // vector3 free version 160 + insertXYZ(x: number, y: number, z: number, data: unknown = undefined) { 161 + return this.insertPoint(new Vector3(x, y, z), data); 162 + } 163 + 164 + insertPoint(p: Vector3, data: unknown = undefined) { 165 + p = p.clone(); 166 + 167 + // @ts-expect-error - data is not a property of Vector3 168 + if (data) p.data = data; 169 + 170 + if (!this.boundary.containsPoint(p)) return false; 171 + 172 + if (this.points.length < this.capacity) { 173 + this.points.push(p); 174 + return true; 175 + } else { 176 + this.subdivide(); 177 + let added = false; 178 + for (const sub of this.subdivisions ?? []) { 179 + if (sub.insertPoint(p, data)) added = true; 180 + } 181 + return added; 182 + } 183 + } 184 + 185 + showBoxes(mat: Material, parent: Object3D | undefined = undefined) { 186 + const size = new Vector3(); 187 + this.boundary.getSize(size); 188 + 189 + const box = new BoxGeometry(size.x * 2, size.y * 2, size.z * 2); 190 + const mesh = new Mesh( 191 + box, 192 + mat || 193 + new MeshStandardMaterial({ 194 + wireframe: true, 195 + }), 196 + ); 197 + this.boundary.getCenter(mesh.position); 198 + 199 + parent ??= new Object3D(); 200 + parent.add(mesh); 201 + 202 + if (this.subdivisions) { 203 + for (const sub of this.subdivisions) sub.showBoxes(mat, parent); 204 + } 205 + return parent; 206 + } 207 + 208 + show( 209 + opts: { 210 + pointsOnly?: boolean; 211 + mat?: Material; 212 + size?: number; 213 + sizeAttenuation?: boolean; 214 + p?: Vector3; 215 + min?: number; 216 + } = {}, 217 + ) { 218 + opts ??= {}; 219 + 220 + const pointsOnly = opts.pointsOnly; 221 + let mat = opts.mat; 222 + const points = this.all(); 223 + 224 + const pointsGeo = new BufferGeometry(); 225 + const positionData = new Float32Array(points.length * 3); 226 + const colorData = new Float32Array(points.length * 3); 227 + 228 + let q; 229 + 230 + if (opts.p && opts.min) { 231 + for (const point of points) { 232 + // @ts-expect-error - close is not a property of Vector3 233 + point.close = false; 234 + } 235 + q = this.query(opts.p, opts.min); 236 + 237 + for (const point of q) { 238 + // @ts-expect-error - close is not a property of Vector3 239 + point.close = true; 240 + } 241 + } 242 + 243 + for (let i = 0; i < points.length; i++) { 244 + positionData[i * 3] = points[i].x; 245 + positionData[i * 3 + 1] = points[i].y; 246 + positionData[i * 3 + 2] = points[i].z; 247 + 248 + // @ts-expect-error - close is not a property of Vector3 249 + colorData[i * 3] = points[i].close ? 1 : 0.7; 250 + 251 + // @ts-expect-error - close is not a property of Vector3 252 + colorData[i * 3 + 1] = points[i].close ? 0 : 0.7; 253 + 254 + // @ts-expect-error - close is not a property of Vector3 255 + colorData[i * 3 + 2] = points[i].close ? 0 : 0.7; 256 + } 257 + pointsGeo.setAttribute("position", new BufferAttribute(positionData, 3)); 258 + pointsGeo.setAttribute("color", new BufferAttribute(colorData, 3)); 259 + const pointMesh = new Points( 260 + pointsGeo, 261 + new PointsMaterial({ 262 + size: opts.size || 1, 263 + sizeAttenuation: opts.sizeAttenuation || false, 264 + vertexColors: true, 265 + }), 266 + ); 267 + if (pointsOnly) return pointMesh; 268 + 269 + mat = 270 + mat || 271 + new MeshStandardMaterial({ 272 + transparent: true, 273 + opacity: 0.01, 274 + depthTest: false, 275 + }); 276 + const boxes = this.showBoxes(mat); 277 + boxes.add(pointMesh); 278 + return boxes; 279 + } 280 + 281 + all(arr: Vector3Data[] = []) { 282 + arr ??= []; 283 + for (const p of this.points) { 284 + arr.push(p); 285 + } 286 + if (this.subdivisions) { 287 + for (const subs of this.subdivisions) subs.all(arr); 288 + } 289 + return arr; 290 + } 291 + }
-275
src/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 - }
+73 -66
src/worlds/materials/AtmosphereMaterial.ts
··· 1 1 import { ShaderMaterial, Vector3 } from "three"; 2 2 3 - const atmosphereShader = { 4 - uniforms: { 5 - lightDirection: { value: new Vector3(1.0, 1.0, 0.0).normalize() }, // Sunlight direction 6 - atmosphereColor: { value: new Vector3(0.3, 0.6, 1.0) }, // Blue tint for atmosphere 7 - }, 8 - vertexShader: ` 9 - varying vec3 vNormal; 10 - varying vec3 vViewLightDirection; // Light direction relative to the camera 11 - varying vec3 vViewPosition; // View position 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 + }; 12 13 13 - uniform vec3 lightDirection; // Light direction in world space 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 + }; 14 75 15 - void main() { 16 - // Transform the vertex normal to world space 17 - vNormal = normalize(normalMatrix * normal); 18 - // Transform the light direction to view space 19 - vViewLightDirection = (viewMatrix * vec4(lightDirection, 0.0)).xyz; 20 - vViewPosition = (modelViewMatrix * vec4(position, 1.0)).xyz; 21 - 22 - gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 23 - } 24 - `, 25 - fragmentShader: ` 26 - varying vec3 vNormal; 27 - varying vec3 vViewLightDirection; 28 - varying vec3 vViewPosition; 29 - uniform vec3 atmosphereColor; 30 - 31 - void main() { 32 - 33 - // Normalize the normal and the view direction 34 - vec3 viewDirection = normalize(vViewPosition); 35 - 36 - // Calculate how much of the surface is perpendicular to the view direction 37 - float viewFactor = dot(normalize(vNormal), -viewDirection); 38 - 39 - // Calculate the dot product of the light direction and the surface normal 40 - float lightFactor = dot(normalize(vNormal), normalize(vViewLightDirection)); 41 - 42 - // Use smoothstep to soften the transition from light to dark side 43 - lightFactor = smoothstep(-0.2, 0.4, lightFactor); 44 - 45 - // Ensure a minimum glow, even on the dark side 46 - float minGlow = 0.2; // Adjust this to control how much it glows on the dark side 47 - lightFactor = mix(minGlow, 1.0, lightFactor); 48 - 49 - // Adjust the intensity for the atmosphere's glow, including the minimum glow 50 - float dotProduct = dot(normalize(vNormal), vec3(0, 0.0, 1.0)); 51 - float intensity = pow(dotProduct, 8.0); 52 - intensity *= lightFactor; 53 - 54 - viewFactor = clamp(1.0 - viewFactor, 0.0, 1.0); 55 - 56 - // Use smoothstep for a smooth transition on the edges 57 - float atmosphereFactor = smoothstep(0.2, 0.6, viewFactor); 58 - 59 - // Output the final color 60 - gl_FragColor = vec4(atmosphereColor, intensity * atmosphereFactor); 61 - // Set the final fragment color with the computed intensity 62 - //gl_FragColor = vec4(0.3, 0.6, 1.0, 1.0) * intensity; 63 - }`, 64 - transparent: true, 65 - depthWrite: false, 66 - }; 67 - 68 - const atmosphereMaterial = new ShaderMaterial(atmosphereShader); 69 - 70 - export default atmosphereMaterial; 76 + return new ShaderMaterial(atmosphereShader); 77 + }