[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.

Merge pull request #2 from flo-bit/main

deploy

authored by

Florian and committed by
GitHub
(Oct 3, 2024, 9:14 PM +0200) 887b3c74 5fe29b2e

+575 -920
+2 -1
.gitignore
··· 1 1 node_modules 2 2 old 3 - dist 3 + dist 4 + stuff
bun.lockb

This is a binary file and will not be displayed.

+4 -1
features.md
··· 8 8 - [ ] noise 9 9 - [x] height 10 10 - [ ] slope 11 + - [ ] distance 11 12 - [ ] different materials 12 13 - [ ] make vegetation sway in the wind 13 14 - [ ] make all random stuff dependent on seed 15 + - [ ] instanced vegetation 14 16 15 17 ### weather 16 18 - [ ] clouds ··· 18 20 19 21 ### water 20 22 - [x] moving water 23 + - [ ] option to turn off water 21 24 - [ ] water reflections 22 25 - [ ] water refractions 23 26 - [x] water caustics ··· 43 46 - [ ] tilt shift 44 47 45 48 ## other 46 - - [ ] 49 + - [ ] walking on planet
+14 -1
index.html
··· 1 1 <!doctype html> 2 2 <html lang="en"> 3 + 3 4 <head> 4 - <title>three.js scaffold</title> 5 + <script> 6 + !function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]); 7 + posthog.init('phc_1Q4q6SdTwvStnxFWbmdOIusLc5ve0u6Fk7WpsHPoAlD',{api_host:'https://eu.i.posthog.com', person_profiles: 'identified_only', persistence: 'memory'}) 8 + </script> 9 + 10 + <title>tiny planets</title> 5 11 <meta charset="utf-8" /> 6 12 <meta 7 13 name="viewport" 8 14 content="width=device-width, initial-scale=1.0, user-scalable=no" 15 + /> 16 + 17 + <link 18 + rel="icon" 19 + href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🌎</text></svg>" 9 20 /> 10 21 11 22 <link rel="stylesheet" href="./index.css" /> ··· 20 31 </button> 21 32 22 33 <canvas id="root"></canvas> 34 + 35 + <a class="absolute bottom-2 left-3 text-cyan-400 text-sm font-semibold hover:text-cyan-300 transition-colors duration-100" href="https://flo-bit.dev" target="_blank">made by flo-bit</a> 23 36 24 37 <script src="src/script.ts" type="module"></script> 25 38 </body>
-624
src/noise.ts
··· 1 - import { 2 - type NoiseFunction2D, 3 - type NoiseFunction3D, 4 - type NoiseFunction4D, 5 - createNoise2D, 6 - createNoise3D, 7 - createNoise4D, 8 - } from "simplex-noise"; 9 - import alea from "alea"; 10 - 11 - /** 12 - * 13 - * @property {number} seed - seed for the noise, if not provided, Math.random() will be used, 14 - * currently same results can only be guaranteed for newly created noise objects with same seed 15 - * (as opposed to an old noise object where you changed the seed) 16 - * 17 - * @property {number} min - minimun value of noise 18 - * @property {number} max - maximum value of noise 19 - * 20 - * @property {number} scale - scale of the noise 21 - * @property {number} power - power of the noise (1 = linear, 2 = quadratic, etc) 22 - * @property {Vector} shift - move noise in 2D, 3D or 4D space 23 - * 24 - * @property {number} octaves - number of layers for fbm noise 25 - * @property {number} gain - how much to multiply amplitude per layer 26 - * @property {number} lacunarity - how much to multiply scale per layer 27 - * @property {array} amps - array of amplitudes for each layer 28 - * @property {number} erosion - how much previous layers influence amplitude of later layers 29 - * @property {number} sharpness - billowed or rigded noise (0 = normal, 1 = billowed, -1 = ridged) 30 - * @property {number} steps - will turn noise into steps (integer, number of steps) 31 - * 32 - * @property {number} warp - how much to warp the noise 33 - * @property {number} warpNoise - noise to warp the noise with 34 - * @property {number} warp2 - second warp, can only be used if warp is used too 35 - * @property {number} warpNoise2 - second warp noise 36 - * 37 - * @property {boolean} invert - invert the noise 38 - * @property {boolean} abs - absolute value of the noise 39 - * @property {boolean} clamp - clamp the noise between min and max 40 - * @property {boolean} tileX - tile the noise in x direction 41 - * @property {boolean} tileY - tile the noise in y direction 42 - * @property {boolean} tile - tile the noise in all directions (will override tileX and tileY) 43 - * 44 - * @constructor 45 - */ 46 - 47 - function lerp(a: number, b: number, t: number): number { 48 - return (b - a) * t + a; 49 - } 50 - 51 - function setLength(vector: number[], length: number): number[] { 52 - const oldLength = Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0)); 53 - if (oldLength === 0) return vector; 54 - const scale = length / oldLength; 55 - return vector.map((v) => v * scale); 56 - } 57 - 58 - // angle between two 3D vectors 59 - function angleTo(a: number[], b: number[]): number { 60 - const dot = a.reduce((sum, v, i) => sum + v * b[i], 0); 61 - const length = Math.sqrt( 62 - a.reduce((sum, v) => sum + v * v, 0) * b.reduce((sum, v) => sum + v * v, 0), 63 - ); 64 - return Math.acos(dot / length); 65 - } 66 - 67 - type ErosionUpFunction = (derivative: number[]) => number[]; 68 - 69 - export type VectorObject = { 70 - x: number; 71 - y: number; 72 - z?: number; 73 - w?: number; 74 - }; 75 - 76 - export type NoiseParameterInput = number | UberNoise | UberNoiseOptions; 77 - export type NoiseParameter = number | UberNoise; 78 - 79 - export type UberNoiseOptions = { 80 - seed?: string | number; 81 - 82 - min?: NoiseParameterInput; 83 - max?: NoiseParameterInput; 84 - 85 - scale?: NoiseParameterInput; 86 - power?: NoiseParameterInput; 87 - 88 - shift?: number[]; 89 - 90 - octaves?: number; 91 - gain?: NoiseParameterInput; 92 - lacunarity?: NoiseParameterInput; 93 - 94 - amps?: number[]; 95 - 96 - layers?: UberNoiseOptions[]; 97 - 98 - erosion?: NoiseParameterInput; 99 - erosionUp?: ErosionUpFunction; 100 - 101 - sharpness?: NoiseParameterInput; 102 - steps?: NoiseParameterInput; 103 - warp?: NoiseParameterInput; 104 - warpNoise?: UberNoise; 105 - warp2?: NoiseParameterInput; 106 - warpNoise2?: UberNoise; 107 - 108 - invert?: boolean; 109 - abs?: boolean; 110 - clamp?: boolean; 111 - tileX?: boolean; 112 - tileY?: boolean; 113 - tile?: boolean; 114 - }; 115 - 116 - export class UberNoise { 117 - private noise2D: NoiseFunction2D | undefined; 118 - private noise3D: NoiseFunction3D | undefined; 119 - private noise4D: NoiseFunction4D | undefined; 120 - private seed: string | number; 121 - 122 - private _min: NoiseParameter = -1; 123 - private _max: NoiseParameter = 1; 124 - 125 - private _scale: NoiseParameter = 1; 126 - private _power: NoiseParameter = 1; 127 - 128 - private shift: number[] | undefined = undefined; 129 - 130 - private octaves = 0; 131 - private _gain: NoiseParameter = 0.5; 132 - private _lacunarity: NoiseParameter = 2; 133 - 134 - private amps: number[] = []; 135 - 136 - private _erosion: NoiseParameter = 0; 137 - private _sharpness: NoiseParameter = 0; 138 - private _steps: NoiseParameter = 0; 139 - 140 - private _warp: NoiseParameter = 0; 141 - private _warpNoise: UberNoise | undefined; 142 - private _warp2: NoiseParameter = 0; 143 - private _warpNoise2: UberNoise | undefined; 144 - 145 - private tileX = false; 146 - private tileY = false; 147 - 148 - private erosionDelta = 0; 149 - private erosionDerivative?: number[] = undefined; 150 - private erosionUp?: ErosionUpFunction; 151 - 152 - private static erosionDefaultUp = [0, 0, 1]; 153 - 154 - private position = [0, 0]; 155 - 156 - private pngr; 157 - 158 - private layers: UberNoise[] = []; 159 - 160 - constructor(options: UberNoiseOptions = {}) { 161 - this.seed = options.seed ?? Math.random(); 162 - this.pngr = alea(this.seed); 163 - 164 - this.min = options.min ?? -1; 165 - this.max = options.max ?? 1; 166 - 167 - this.scale = options.scale ?? 1; 168 - this.power = options.power ?? 1; 169 - 170 - this.octaves = options.octaves ?? options.layers?.length ?? 0; 171 - this.gain = options.gain ?? 0.5; 172 - this.lacunarity = options.lacunarity ?? 2; 173 - 174 - this.erosion = options.erosion ?? 0; 175 - this.sharpness = options.sharpness ?? 0; 176 - this.steps = options.steps ?? 0; 177 - 178 - this.warp = options.warp ?? 0; 179 - this.warpNoise = options.warpNoise; 180 - this.warp2 = options.warp2 ?? 0; 181 - this.warpNoise2 = options.warpNoise2; 182 - 183 - this.processLayers(options); 184 - 185 - this.tileX = options.tileX ?? options.tile ?? false; 186 - this.tileY = options.tileY ?? options.tile ?? false; 187 - } 188 - 189 - processLayers(options: UberNoiseOptions) { 190 - this.amps = options.amps ?? []; 191 - 192 - for (let i = 0; i < this.octaves; i++) { 193 - const layerOptions = options.layers?.[i] ?? {}; 194 - if (layerOptions instanceof UberNoise) { 195 - this.layers[i] = layerOptions; 196 - } else { 197 - if (layerOptions.seed === undefined) layerOptions.seed = this.pngr(); 198 - this.layers[i] = new UberNoise(layerOptions); 199 - } 200 - } 201 - if (this.layers.length == 0) { 202 - this.noise2D = createNoise2D(this.pngr); 203 - this.noise3D = createNoise3D(this.pngr); 204 - this.noise4D = createNoise4D(this.pngr); 205 - } 206 - } 207 - 208 - // same as normalized but returns between min and max 209 - get( 210 - x: number | VectorObject | Array<number>, 211 - y: number | undefined = undefined, 212 - z: number | undefined = undefined, 213 - w: number | undefined = undefined, 214 - ): number { 215 - const norm = this.normalized(x, y, z, w); 216 - return this.toMinMax(norm); 217 - } 218 - 219 - // same as get but returns between -1 and 1 220 - normalized( 221 - x: number | VectorObject | Array<number>, 222 - y: number | undefined = undefined, 223 - z: number | undefined = undefined, 224 - w: number | undefined = undefined, 225 - ): number { 226 - // if x is an array, treat it as a vector 227 - if (Array.isArray(x)) { 228 - w = x[3]; 229 - z = x[2]; 230 - y = x[1]; 231 - x = x[0]; 232 - } else if (typeof x === "object") { 233 - w = x.w; 234 - z = x.z; 235 - y = x.y; 236 - x = x.x; 237 - } 238 - // if y is undefined, treat it as 2D noise with y = 0 239 - y = y ?? 0; 240 - 241 - return this.getNoise(x, y, z, w); 242 - } 243 - 244 - private warpPosition(warp: number, warpNoise: UberNoise | undefined) { 245 - if (warp == undefined || warp == 0) return; 246 - 247 - if (warpNoise != undefined) warpNoise.position = this.position; 248 - 249 - let x = this.position[0], 250 - y = this.position[1], 251 - z = this.position[2], 252 - w = this.position[3]; 253 - 254 - const noise = warpNoise ?? this; 255 - const scl = this.scale; 256 - 257 - this.position = [x + 54.47 * scl, y + 34.98 * scl]; 258 - if (z != undefined) this.position.push(z + 21.63 * scl); 259 - if (w != undefined) this.position.push(w + 67.1 * scl); 260 - 261 - x += noise.getFBM() * warp; 262 - y += noise.getFBM() * warp; 263 - if (z != undefined) z += noise.getFBM() * warp; 264 - if (w != undefined) w += noise.getFBM() * warp; 265 - 266 - this.position = [x, y, z, w]; 267 - } 268 - 269 - private tilePosition() { 270 - if (!this.tileX && !this.tileY) return; 271 - const x = this.position[0]; 272 - const y = this.position[1]; 273 - let newX = 0, 274 - newY = 0, 275 - newZ = 0, 276 - newW = 0; 277 - if (this.tileX) { 278 - newX = Math.sin(x * Math.PI * 2); 279 - newY = Math.cos(x * Math.PI * 2); 280 - } 281 - if (this.tileY) { 282 - newZ = Math.sin(y * Math.PI * 2); 283 - newW = Math.cos(y * Math.PI * 2); 284 - } 285 - if (this.tileX && !this.tileY) { 286 - this.position = [newX, newY + y]; 287 - } else if (this.tileY && !this.tileX) { 288 - this.position = [newZ + x, newW]; 289 - } else if (this.tileX && this.tileY) { 290 - this.position = [newX, newY, newZ, newW]; 291 - } 292 - } 293 - 294 - private getDerivative( 295 - x: number, 296 - y: number, 297 - z: number, 298 - n: number | undefined = undefined, 299 - ): number[] | undefined { 300 - // left side or central difference 301 - // very expensive (four/six noise calls), should be changed to analytical derivatives 302 - // see https://iquilezles.org/www/articles/morenoise/morenoise.htm 303 - 304 - if (z == undefined) return undefined; 305 - 306 - const mov = this.erosionDelta; 307 - 308 - const dx = (n ?? this.get(x - mov, y, z)) - this.get(x + mov, y, z); 309 - const dy = (n ?? this.get(x, y - mov, z)) - this.get(x, y + mov, z); 310 - const dz = (n ?? this.get(x, y, z - mov)) - this.get(x, y, z + mov); 311 - 312 - this.erosionDerivative = [dx, dy, dz]; 313 - return this.erosionDerivative; 314 - } 315 - 316 - private getBaseLayer( 317 - scale: number, 318 - x: number, 319 - y: number, 320 - z?: number, 321 - w?: number, 322 - ): number { 323 - if (z != undefined && w != undefined && this.noise4D != undefined) { 324 - return this.noise4D(x * scale, y * scale, z * scale, w * scale); 325 - } 326 - if (z != undefined && this.noise3D != undefined) { 327 - return this.noise3D(x * scale, y * scale, z * scale); 328 - } 329 - if (this.noise2D != undefined) { 330 - return this.noise2D(x * scale, y * scale); 331 - } 332 - return 0; 333 - } 334 - 335 - private getFBM(useErosion = false): number { 336 - const x = this.position[0], 337 - y = this.position[1], 338 - z = this.position[2], 339 - w = this.position[3]; 340 - 341 - const scale = this.scale; 342 - 343 - if (this.layers.length === 0) { 344 - return this.getBaseLayer(scale, x, y, z, w); 345 - } 346 - 347 - let maxAmp = 1; 348 - let amp = 1, 349 - freq = scale; 350 - 351 - const lacunarity = this.lacunarity; 352 - const gain = this.gain; 353 - 354 - // for now we'll always ignore erosion 355 - // remove the following line later to enable erosion 356 - useErosion = false; 357 - const erosion = useErosion ? this.erosion : 0; 358 - const up = 359 - this.erosionUp != undefined 360 - ? this.erosionUp(this.position) 361 - : UberNoise.erosionDefaultUp; 362 - const sum = [0, 0, 0]; 363 - 364 - let n = 0; 365 - 366 - for (let i = 0; i < this.octaves; i++) { 367 - const layer = this.layers[i]; 368 - 369 - const layerAmp = this.amps[i] ?? 1; 370 - 371 - const value = 372 - layer.get( 373 - x * freq, 374 - y * freq, 375 - z != undefined ? z * freq : undefined, 376 - w != undefined ? w * freq : undefined, 377 - ) * 378 - amp * 379 - layerAmp; 380 - if (erosion > 0) { 381 - const derivative = layer.getDerivative(x * freq, y * freq, z * freq); 382 - 383 - if (derivative != undefined) { 384 - setLength(derivative, amp * layerAmp); 385 - sum[0] += derivative[0]; 386 - sum[1] += derivative[1]; 387 - sum[2] += derivative[2]; 388 - 389 - const mult = Math.abs(1 - angleTo(up, sum) / Math.PI); 390 - 391 - n += value * (mult * erosion + 1 - erosion); 392 - } 393 - } else { 394 - n += value; 395 - } 396 - 397 - amp *= gain; 398 - freq *= lacunarity; 399 - maxAmp += amp * layerAmp; 400 - } 401 - return n / maxAmp; 402 - } 403 - 404 - move( 405 - x: number, 406 - y: number, 407 - z: number | undefined = undefined, 408 - w: number | undefined = undefined, 409 - ) { 410 - if (this.shift == undefined) this.shift = [0, 0, 0]; 411 - 412 - this.shift[0] += x; 413 - this.shift[1] += y; 414 - if (z != undefined) this.shift[2] += z; 415 - if (w != undefined) this.shift[3] += w; 416 - } 417 - 418 - private getNoise( 419 - x: number, 420 - y: number, 421 - z: number | undefined = undefined, 422 - w: number | undefined = undefined, 423 - ): number { 424 - // set position 425 - this.position[0] = x; 426 - this.position[1] = y; 427 - if (z !== undefined) { 428 - this.position[2] = z; 429 - } 430 - if (w !== undefined) { 431 - this.position[3] = w; 432 - } 433 - 434 - // apply shift 435 - const shift = this.shift; 436 - if (shift !== undefined) { 437 - for (let i = 0; i < this.position.length && i < shift.length; i++) { 438 - this.position[i] += shift[i]; 439 - } 440 - } 441 - 442 - // apply tiling 443 - this.tilePosition(); 444 - 445 - // apply warp 446 - this.warpPosition(this.warp, this.warpNoise); 447 - this.warpPosition(this.warp2, this.warpNoise2); 448 - 449 - let norm = this.getFBM(true); 450 - 451 - // apply power 452 - const power = this.power; 453 - if (power != 1) { 454 - // convert to [0 - 1], apply power and back to [-1, 1] 455 - norm = (Math.pow((norm + 1) * 0.5, power) - 0.5) * 2; 456 - } 457 - 458 - // apply sharpness 459 - const sharpness = this.sharpness; 460 - if (sharpness != 0) { 461 - const billow = (Math.abs(norm) - 0.5) * 2; 462 - const ridged = (0.5 - Math.abs(norm)) * 2; 463 - 464 - norm = lerp(norm, billow, Math.max(0, sharpness)); 465 - norm = lerp(norm, ridged, Math.max(0, -sharpness)); 466 - } 467 - 468 - // apply steps 469 - const steps = this.steps; 470 - if (steps != 0) { 471 - // convert from -1 to 1 to 0 to 1 472 - norm = (norm + 1) * 0.5; 473 - // apply steps 474 - norm = Math.floor(norm * steps) / steps; 475 - // convert back to -1 to 1 476 - norm = norm * 2 - 1; 477 - } 478 - 479 - return norm; 480 - } 481 - 482 - /** 483 - * value between min and max to normalized value between -1 and 1 484 - * 485 - * @param value {number} 486 - * @returns {number} 487 - */ 488 - toNormalized(value: number): number { 489 - return ((value - this.min) / (this.max - this.min)) * 2 - 1; 490 - } 491 - 492 - /** 493 - * normlized value to value between min and max 494 - * 495 - * @param value {number} 496 - * @returns {number} 497 - */ 498 - toMinMax(value: number): number { 499 - return (value + 1) * 0.5 * (this.max - this.min) + this.min; 500 - } 501 - 502 - private checkParameterInput(value: NoiseParameterInput): UberNoise | number { 503 - if (typeof value === "object" && !(value instanceof UberNoise)) { 504 - if (value.seed === undefined) { 505 - value.seed = this.pngr(); 506 - } 507 - value = new UberNoise(value); 508 - } 509 - return value; 510 - } 511 - 512 - private getParameter(value: NoiseParameter): number { 513 - if (typeof value === "number") { 514 - return value; 515 - } 516 - return value.get(this.position); 517 - } 518 - 519 - // getter and setter for min and max 520 - get min(): number { 521 - return this.getParameter(this._min); 522 - } 523 - set min(value: NoiseParameterInput) { 524 - this._min = this.checkParameterInput(value); 525 - } 526 - get max(): number { 527 - return this.getParameter(this._max); 528 - } 529 - set max(value: NoiseParameterInput) { 530 - this._max = this.checkParameterInput(value); 531 - } 532 - 533 - // getter and setter for scale and power 534 - get scale(): number { 535 - return this.getParameter(this._scale); 536 - } 537 - set scale(value: NoiseParameterInput) { 538 - this._scale = this.checkParameterInput(value); 539 - } 540 - get power(): number { 541 - return this.getParameter(this._power); 542 - } 543 - set power(value: NoiseParameterInput) { 544 - this._power = this.checkParameterInput(value); 545 - } 546 - 547 - // getter and setter for gain and lacunarity 548 - get gain(): number { 549 - return this.getParameter(this._gain); 550 - } 551 - set gain(value: NoiseParameterInput) { 552 - this._gain = this.checkParameterInput(value); 553 - } 554 - get lacunarity(): number { 555 - return this.getParameter(this._lacunarity); 556 - } 557 - set lacunarity(value: NoiseParameterInput) { 558 - this._lacunarity = this.checkParameterInput(value); 559 - } 560 - 561 - // getter and setter for erosion, sharpness and steps 562 - get erosion(): number { 563 - return this.getParameter(this._erosion); 564 - } 565 - set erosion(value: NoiseParameterInput) { 566 - this._erosion = this.checkParameterInput(value); 567 - } 568 - get sharpness(): number { 569 - return this.getParameter(this._sharpness); 570 - } 571 - set sharpness(value: NoiseParameterInput) { 572 - this._sharpness = this.checkParameterInput(value); 573 - } 574 - get steps(): number { 575 - return Math.round(this.getParameter(this._steps)); 576 - } 577 - set steps(value: NoiseParameterInput) { 578 - this._steps = this.checkParameterInput(value); 579 - } 580 - 581 - // getter and setter for warp, warpNoise, warp2 and warpNoise2 582 - get warp(): number { 583 - return this.getParameter(this._warp); 584 - } 585 - set warp(value: NoiseParameterInput) { 586 - this._warp = this.checkParameterInput(value); 587 - } 588 - get warpNoise(): UberNoise | undefined { 589 - return this._warpNoise; 590 - } 591 - set warpNoise(value: UberNoise | UberNoiseOptions | undefined) { 592 - if (value == undefined) { 593 - this._warpNoise = undefined; 594 - return; 595 - } 596 - 597 - const processed = this.checkParameterInput(value); 598 - if (processed instanceof UberNoise) { 599 - this._warpNoise = processed; 600 - } 601 - } 602 - get warp2(): number { 603 - return this.getParameter(this._warp2); 604 - } 605 - set warp2(value: NoiseParameterInput) { 606 - this._warp2 = this.checkParameterInput(value); 607 - } 608 - get warpNoise2(): UberNoise | undefined { 609 - return this._warpNoise2; 610 - } 611 - set warpNoise2(value: UberNoise | UberNoiseOptions | undefined) { 612 - if (value == undefined) { 613 - this._warpNoise2 = undefined; 614 - return; 615 - } 616 - 617 - const processed = this.checkParameterInput(value); 618 - if (processed instanceof UberNoise) { 619 - this._warpNoise2 = processed; 620 - } 621 - } 622 - } 623 - 624 - export default UberNoise;
+13 -5
src/script.ts
··· 1 1 import * as THREE from "three"; 2 2 import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; 3 3 import { Planet } from "./worlds/planet"; 4 + import { Stars } from "./worlds/stars"; 5 + import { planetPresets } from "./worlds/presets"; 4 6 5 7 const presets = ["beach", "forest", "snowForest"]; 6 8 ··· 13 15 throw new Error("Canvas not found"); 14 16 } 15 17 const scene = new THREE.Scene(); 16 - const camera = new THREE.PerspectiveCamera(70, width / height, 0.01, 10); 18 + const camera = new THREE.PerspectiveCamera(70, width / height, 0.01, 30); 17 19 camera.position.set(0, 0, 2.5); 18 20 19 21 const renderer = new THREE.WebGLRenderer({ ··· 23 25 }); 24 26 25 27 renderer.toneMapping = THREE.ACESFilmicToneMapping; 26 - 27 28 renderer.shadowMap.enabled = true; 28 29 29 30 const _ = new OrbitControls(camera, renderer.domElement); ··· 36 37 wireframe: true, 37 38 wireframeLinewidth: 10, 38 39 }); 39 - let planetMesh = new THREE.Mesh(sphereGeometry, sphereMaterial); 40 + let planetMesh: THREE.Mesh = new THREE.Mesh(sphereGeometry, sphereMaterial); 40 41 scene.add(planetMesh); 41 42 42 43 const light = new THREE.DirectionalLight(); ··· 58 59 const ambientLight = new THREE.AmbientLight(0xffffff, 0.1); 59 60 scene.add(ambientLight); 60 61 61 - 62 62 let total = 0; 63 63 let lastDelta = 0; 64 64 renderer.setAnimationLoop((delta) => { ··· 77 77 hasPlanet = true; 78 78 } 79 79 }); 80 + 81 + let stars = new Stars(); 82 + 83 + scene.add(stars); 80 84 81 85 // add keydown event listener 82 86 document.addEventListener("keydown", (event) => { ··· 107 111 console.time("planet"); 108 112 const planet = new Planet({ 109 113 detail: 50, 110 - biome: { preset }, 114 + ...planetPresets[preset], 111 115 }); 112 116 let mesh = await planet.create(); 113 117 scene.remove(planetMesh); 114 118 scene.add(mesh); 115 119 planetMesh = mesh; 120 + 121 + // planetMesh.add(camera); 122 + // planet.updatePosition(camera, new THREE.Vector3(0, 0, 1.1)); 123 + 116 124 console.timeEnd("planet"); 117 125 } 118 126
+122 -8
src/worlds/biome.ts
··· 1 1 import { UberNoise, type NoiseOptions } from "uber-noise"; 2 - import { Color, Vector3 } from "three"; 2 + import { Color, ColorRepresentation, Vector3 } from "three"; 3 3 4 4 import { 5 5 ColorGradient, ··· 7 7 } from "./helper/colorgradient"; 8 8 import { biomePresets } from "./presets"; 9 9 import { Octree } from "./helper/octree"; 10 + import { type VertexInfo } from "./types"; 10 11 11 12 export type VegetationItem = { 12 13 name: string; 13 - density: number; 14 + density?: number; 14 15 15 16 minimumHeight?: number; 16 17 maximumHeight?: number; ··· 21 22 minimumDistance?: number; 22 23 maximumDistance?: number; 23 24 colors?: Record<string, { array?: number[] }>; 25 + 26 + ground?: { 27 + raise?: number; 28 + color?: ColorRepresentation; 29 + radius?: number; 30 + 31 + noise?: NoiseOptions; 32 + }; 24 33 }; 25 34 26 35 export type BiomeOptions = { ··· 37 46 tintColor?: number; 38 47 39 48 vegetation?: { 40 - defaults?: { 41 - density?: number; 42 - }; 49 + defaults?: Omit<Partial<VegetationItem>, "name">; 50 + 43 51 items: VegetationItem[]; 44 52 }; 45 53 }; ··· 53 61 54 62 options: BiomeOptions; 55 63 56 - vegetationPositions: Octree = new Octree(); 64 + vegetationPositions: Octree<VegetationItem> = new Octree(); 57 65 58 66 constructor(opts: BiomeOptions = {}) { 59 67 if (opts.preset) { 60 68 const preset = biomePresets[opts.preset]; 69 + 61 70 if (preset) { 62 71 opts = { 63 72 ...preset, ··· 143 152 this.vegetationPositions.insert(position, item); 144 153 } 145 154 146 - itemsAround(position: Vector3, radius: number): Vector3[] { 147 - return this.vegetationPositions.query(position, radius); 155 + closestVegetationDistance( 156 + position: Vector3, 157 + radius: number, 158 + ): number | undefined { 159 + const items = this.vegetationPositions.queryBoxXYZ( 160 + position.x, 161 + position.y, 162 + position.z, 163 + radius, 164 + ); 165 + if (items.length === 0) return undefined; 166 + 167 + let closest = Infinity; 168 + for (const item of items) { 169 + const distance = position.distanceTo(item); 170 + if (distance < closest) closest = distance; 171 + } 172 + 173 + return closest < radius ? closest : undefined; 174 + } 175 + 176 + itemsAround( 177 + position: Vector3, 178 + radius: number, 179 + ): (Vector3 & { data?: VegetationItem })[] { 180 + return this.vegetationPositions.queryBoxXYZ( 181 + position.x, 182 + position.y, 183 + position.z, 184 + radius, 185 + ); 186 + } 187 + 188 + maxVegetationRadius(): number { 189 + let max = 0; 190 + for (const item of this.options.vegetation?.items ?? []) { 191 + if (item.ground?.radius) { 192 + max = Math.max(max, item.ground.radius); 193 + } 194 + } 195 + 196 + return max; 197 + } 198 + 199 + vegetationHeightAndColorForFace( 200 + a: Vector3, 201 + b: Vector3, 202 + c: Vector3, 203 + color: Color, 204 + sideLength: number, 205 + ): { 206 + heightA: number; 207 + heightB: number; 208 + heightC: number; 209 + color: Color; 210 + } { 211 + const maxDist = this.maxVegetationRadius(); 212 + // use a to find all vegetation items, we add sideLength so that we also find vegetation from b and c 213 + // that otherwise would be missed, because they are too far away from a 214 + const vegetations = this.itemsAround(a, maxDist + sideLength * 2); 215 + 216 + // go through a, b and c and add heights for all vegetation items that are close enough (distance is closer than item.ground.radius) 217 + let heightA = 0; 218 + let heightB = 0; 219 + let heightC = 0; 220 + 221 + let all = [a, b, c]; 222 + for (let j = 0; j < 3; j++) { 223 + let p = all[j]; 224 + 225 + for (const vegetation of vegetations) { 226 + if (!vegetation.data?.ground?.radius) continue; 227 + 228 + let distance = p.distanceTo(vegetation); 229 + 230 + if (distance < vegetation.data.ground?.radius) { 231 + let amount = Math.max( 232 + 0, 233 + 1 - distance / vegetation.data.ground.radius, 234 + ); 235 + 236 + amount = Math.pow(amount, 0.5); 237 + 238 + let height = vegetation.data.ground?.raise ?? 0; 239 + height *= amount; 240 + 241 + if (j === 0) heightA += height; 242 + if (j === 1) heightB += height; 243 + if (j === 2) heightC += height; 244 + 245 + if (!vegetation.data.ground.color) continue; 246 + 247 + let newColor = new Color(vegetation.data.ground.color); 248 + 249 + // only lerp a third of the way, because we have three vertices 250 + // so if all vertices are close enough, we lerp 3 times 251 + color.lerp(newColor, amount / 3); 252 + } 253 + } 254 + } 255 + 256 + return { 257 + heightA, 258 + heightB, 259 + heightC, 260 + color, 261 + }; 148 262 } 149 263 }
+68 -76
src/worlds/planet.ts
··· 11 11 import { Biome, type BiomeOptions } from "./biome"; 12 12 import { loadModels } from "./models"; 13 13 14 - import oceansCausticMaterial from "./materials/OceanCausticsMaterial"; 14 + import { PlanetMaterialWithCaustics } from "./materials/OceanCausticsMaterial"; 15 15 import { createAtmosphereMaterial } from "./materials/AtmosphereMaterial"; 16 + import { createBufferGeometry } from "./helper/helper"; 16 17 17 18 export type PlanetOptions = { 18 19 scatter?: number; ··· 22 23 detail?: number; 23 24 24 25 atmosphere?: { 26 + enabled?: boolean; 25 27 color?: Vector3; 26 28 height?: number; 27 29 }; 30 + 31 + material?: "normal" | "caustics"; 28 32 29 33 biome?: BiomeOptions; 30 34 }; ··· 73 77 requestId: number; 74 78 }; 75 79 }) { 76 - const { type, data, requestId } = event.data; 80 + const { data, requestId } = event.data; 77 81 78 82 const callback = this.callbacks[requestId]; 79 83 if (!callback) { ··· 81 85 return; 82 86 } 83 87 84 - if (type === "geometry") { 85 - const geometry = new BufferGeometry(); 86 - const oceanGeometry = new BufferGeometry(); 87 - geometry.setAttribute( 88 - "position", 89 - new Float32BufferAttribute(new Float32Array(data.positions), 3), 90 - ); 91 - geometry.setAttribute( 92 - "color", 93 - new Float32BufferAttribute(new Float32Array(data.colors), 3), 94 - ); 95 - geometry.setAttribute( 96 - "normal", 97 - new Float32BufferAttribute(new Float32Array(data.normals), 3), 98 - ); 88 + const geometry = createBufferGeometry( 89 + data.positions, 90 + data.colors, 91 + data.normals, 92 + ); 99 93 100 - oceanGeometry.setAttribute( 101 - "position", 102 - new Float32BufferAttribute(new Float32Array(data.oceanPositions), 3), 103 - ); 104 - oceanGeometry.setAttribute( 105 - "color", 106 - new Float32BufferAttribute(new Float32Array(data.oceanColors), 3), 107 - ); 108 - oceanGeometry.setAttribute( 109 - "normal", 110 - new Float32BufferAttribute(new Float32Array(data.oceanNormals), 3), 111 - ); 112 - // set morph targets 113 - oceanGeometry.morphAttributes.position = [ 114 - new Float32BufferAttribute( 115 - new Float32Array(data.oceanMorphPositions), 116 - 3, 117 - ), 118 - ]; 119 - oceanGeometry.morphAttributes.normal = [ 120 - new Float32BufferAttribute(new Float32Array(data.oceanMorphNormals), 3), 121 - ]; 94 + const oceanGeometry = createBufferGeometry( 95 + data.oceanPositions, 96 + data.oceanColors, 97 + data.oceanNormals, 98 + ); 122 99 123 - this.vegetationPositions = data.vegetation; 100 + oceanGeometry.morphAttributes.position = [ 101 + new Float32BufferAttribute(data.oceanMorphPositions, 3), 102 + ]; 103 + oceanGeometry.morphAttributes.normal = [ 104 + new Float32BufferAttribute(data.oceanMorphNormals, 3), 105 + ]; 124 106 125 - const planetMesh = new Mesh(geometry, oceansCausticMaterial); 126 - planetMesh.castShadow = true; 107 + this.vegetationPositions = data.vegetation; 127 108 109 + const materialOptions = { vertexColors: true }; 110 + 111 + const material = 112 + this.options.material === "caustics" 113 + ? new PlanetMaterialWithCaustics(materialOptions) 114 + : new MeshStandardMaterial(materialOptions); 115 + 116 + const planetMesh = new Mesh(geometry, material); 117 + planetMesh.castShadow = true; 118 + 119 + if (this.options.material === "caustics") { 128 120 planetMesh.onBeforeRender = ( 129 121 renderer, 130 122 scene, ··· 132 124 geometry, 133 125 material, 134 126 ) => { 135 - if (material.userData.shader?.uniforms?.time) { 136 - material.userData.shader.uniforms.time.value = 137 - performance.now() / 1000; 127 + if (material instanceof PlanetMaterialWithCaustics) { 128 + material.update(); 138 129 } 139 - //material.userData.shader.uniforms.time.value = performance.now() / 1000; 140 130 }; 141 - 142 - const oceanMesh = new Mesh( 143 - oceanGeometry, 144 - new MeshStandardMaterial({ 145 - vertexColors: true, 146 - transparent: true, 147 - opacity: 0.7, 148 - metalness: 0.5, 149 - roughness: 0.5, 150 - }), 151 - ); 152 - 153 - planetMesh.add(oceanMesh); 154 - oceanMesh.onBeforeRender = ( 155 - renderer, 156 - scene, 157 - camera, 158 - geometry, 159 - material, 160 - ) => { 161 - // update morph targets 162 - if (oceanMesh.morphTargetInfluences) 163 - oceanMesh.morphTargetInfluences[0] = 164 - Math.sin(performance.now() / 1000) * 0.5 + 0.5; 165 - }; 166 - 167 - this.addAtmosphere(planetMesh); 168 - callback(planetMesh); 169 131 } 132 + 133 + const oceanMesh = new Mesh( 134 + oceanGeometry, 135 + new MeshStandardMaterial({ 136 + vertexColors: true, 137 + transparent: true, 138 + opacity: 0.7, 139 + metalness: 0.5, 140 + roughness: 0.5, 141 + }), 142 + ); 143 + 144 + planetMesh.add(oceanMesh); 145 + oceanMesh.onBeforeRender = ( 146 + renderer, 147 + scene, 148 + camera, 149 + geometry, 150 + material, 151 + ) => { 152 + // update morph targets 153 + if (oceanMesh.morphTargetInfluences) 154 + oceanMesh.morphTargetInfluences[0] = 155 + Math.sin(performance.now() / 1000) * 0.5 + 0.5; 156 + }; 157 + 158 + if (this.options.atmosphere?.enabled !== false) { 159 + this.addAtmosphere(planetMesh); 160 + } 161 + callback(planetMesh); 170 162 171 163 delete this.callbacks[requestId]; 172 164 } ··· 193 185 const planet = await planetPromise; 194 186 195 187 for (let i = 0; i < loaded.length - 1; i++) { 196 - const models = await loaded[i]; 188 + const models = (await loaded[i]) as Object3D[]; 197 189 const name = models[0].userData.name; 198 190 199 191 const positions = this.vegetationPositions?.[name]; ··· 214 206 model.traverse((child) => { 215 207 if (child instanceof Mesh) { 216 208 let color = item?.colors?.[child.material.name]; 217 - if (color && color.array) { 209 + if (color?.array) { 218 210 let randomColor = 219 211 color.array[Math.floor(Math.random() * color.array.length)]; 220 212 child.material.color.setHex(randomColor);
+49 -15
src/worlds/presets.ts
··· 1 1 import { type BiomeOptions } from "./biome"; 2 + import { 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, ··· 37 38 vegetation: { 38 39 items: [ 39 40 { 41 + name: "Rock", 42 + density: 50, 43 + minimumHeight: 0.1, 44 + colors: { 45 + Gray: { array: [0x775544] }, 46 + }, 47 + }, 48 + { 40 49 name: "PalmTree", 41 50 density: 50, 42 51 minimumHeight: 0.1, ··· 45 54 Green: { array: [0x22851e, 0x22a51e] }, 46 55 DarkGreen: { array: [0x006400] }, 47 56 }, 48 - }, 49 - { 50 - name: "Rock", 51 - density: 10, 52 - minimumHeight: 0.1, 53 - colors: { 54 - Gray: { array: [0x775544] }, 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/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/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 + };
+105 -114
src/worlds/worker.ts
··· 6 6 Color, 7 7 } from "three"; 8 8 9 - import { Biome, type BiomeOptions } from "./biome"; 9 + import { Biome, type VegetationItem } from "./biome"; 10 10 import { type PlanetOptions } from "./planet"; 11 11 import UberNoise from "uber-noise"; 12 + import { type VertexInfo } from "./types"; 12 13 13 14 onmessage = function (e) { 14 15 const { type, data, requestId } = e.data; ··· 75 76 const faceSize = (Math.PI * 4) / faceCount; 76 77 console.log("faces:", faceCount); 77 78 78 - const calculatedVertices = new Map< 79 - string, 80 - { 81 - height: number; 82 - scatter: Vector3; 83 - 84 - seaHeight: number; 85 - seaMorph: number; 86 - } 87 - >(); 88 - 89 - const calculatedVerticesArray: { 90 - height: number; 91 - scatter: Vector3; 92 - 93 - seaHeight: number; 94 - seaMorph: number; 95 - }[] = new Array(faceCount); 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); 96 84 97 85 const colors = new Float32Array(vertices.count * 3); 98 86 const oceanColors = new Float32Array(oceanVertices.count * 3); ··· 110 98 a.fromBufferAttribute(vertices, 0); 111 99 b.fromBufferAttribute(vertices, 1); 112 100 113 - // default to scatter = distance of first edge 114 - const scatterAmount = (planetOptions.scatter ?? 1) * 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; 115 105 const scatterScale = 100; 116 106 117 107 const scatterNoise = new UberNoise({ ··· 136 126 137 127 const temp = new Vector3(); 138 128 139 - let normHeightMax = 0; 140 - let normHeightMin = 0; 141 - 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 142 136 for (let i = 0; i < vertices.count; i += 3) { 143 137 a.fromBufferAttribute(vertices, i); 144 138 b.fromBufferAttribute(vertices, i + 1); ··· 153 147 154 148 let normalizedHeight = 0; 155 149 150 + // go through all vertices of the face 156 151 for (let j = 0; j < 3; j++) { 157 152 let v = a; 158 153 if (j === 1) v = b; 159 154 if (j === 2) v = c; 160 155 156 + // lets see if we already have info for this vertex 161 157 const key = `${v.x.toFixed(5)},${v.y.toFixed(5)},${v.z.toFixed(5)}`; 162 - 163 158 let move = calculatedVertices.get(key); 164 159 160 + // if not, calculate it 165 161 if (!move) { 162 + // calculate height and scatter 166 163 const height = biome.getHeight(v) + 1; 167 164 const scatterX = scatterNoise.get(v); 168 165 const scatterY = scatterNoise.get( ··· 175 172 v.x + scatterScale * 200, 176 173 v.y - scatterScale * 200, 177 174 ); 175 + // calculate sea height and sea morph height 178 176 const seaHeight = biome.getSeaHeight(v) + 1; 179 177 const secondSeaHeight = biome.getSeaHeight(v.addScalar(100)) + 1; 180 178 ··· 189 187 calculatedVertices.set(key, move); 190 188 } 191 189 190 + // we store this info for later use (vegetation placement) 192 191 calculatedVerticesArray[i + j] = move; 193 192 193 + // we add height here so we can calculate the average normalized height of the face later 194 194 normalizedHeight += move.height - 1; 195 + 196 + // move vertex based on height and scatter 195 197 v.add(move.scatter).normalize().multiplyScalar(move.height); 196 198 vertices.setXYZ(i + j, v.x, v.y, v.z); 197 199 200 + // move ocean vertex based on sea height and scatter 198 201 let oceanV = oceanA; 199 202 if (j === 1) oceanV = oceanB; 200 203 if (j === 2) oceanV = oceanC; 201 - 202 204 oceanV.add(move.scatter).normalize().multiplyScalar(move.seaMorph); 203 205 oceanMorphPositions.push(oceanV.x, oceanV.y, oceanV.z); 204 206 207 + // move ocean morph vertex based on sea height and scatter 205 208 if (j === 0) { 206 209 oceanD.copy(oceanV); 207 210 } else if (j === 1) { ··· 209 212 } else if (j === 2) { 210 213 oceanF.copy(oceanV); 211 214 } 212 - 213 215 oceanV.normalize().multiplyScalar(move.seaHeight); 214 216 oceanVertices.setXYZ(i + j, oceanV.x, oceanV.y, oceanV.z); 215 217 } 216 218 219 + // calculate normalized height for the face (between -1 and 1, 0 is sea level) 217 220 normalizedHeight /= 3; 218 - 219 221 normalizedHeight = 220 222 Math.min(-normalizedHeight / biome.min, 0) + 221 223 Math.max(normalizedHeight / biome.max, 0); 222 224 // now normalizedHeight should be between -1 and 1 (0 is sea level) 225 + // this will be used for color calculation and vegetation placement 223 226 224 - normHeightMax = Math.max(normHeightMax, normalizedHeight); 225 - normHeightMin = Math.min(normHeightMin, normalizedHeight); 226 - 227 - // calculate new normal 227 + // calculate face normal 228 228 temp.crossVectors(b.clone().sub(a), c.clone().sub(a)).normalize(); 229 - 230 229 // flat shading, so all normals for the face are the same 231 230 normals.setXYZ(i, temp.x, temp.y, temp.z); 232 231 normals.setXYZ(i + 1, temp.x, temp.y, temp.z); ··· 236 235 // (up vector = old mid point on sphere) 237 236 const steepness = Math.acos(Math.abs(temp.dot(mid))); 238 237 // steepness is between 0 and PI/2 238 + // this will be used for color calculation and vegetation placement 239 239 240 + // calculate color for face 240 241 const color = biome.getColor(mid, normalizedHeight, steepness); 241 - 242 242 // flat shading, so all colors for the face are the same 243 243 if (color) { 244 244 colors[i * 3] = color.r; ··· 254 254 colors[i * 3 + 8] = color.b; 255 255 } 256 256 257 + // calculate ocean face color 258 + const oceanColor = biome.getSeaColor(mid, normalizedHeight); 259 + 260 + if (oceanColor) { 261 + oceanColors[i * 3] = oceanColor.r; 262 + oceanColors[i * 3 + 1] = oceanColor.g; 263 + oceanColors[i * 3 + 2] = oceanColor.b; 264 + 265 + oceanColors[i * 3 + 3] = oceanColor.r; 266 + oceanColors[i * 3 + 4] = oceanColor.g; 267 + oceanColors[i * 3 + 5] = oceanColor.b; 268 + 269 + oceanColors[i * 3 + 6] = oceanColor.r; 270 + oceanColors[i * 3 + 7] = oceanColor.g; 271 + oceanColors[i * 3 + 8] = oceanColor.b; 272 + } 273 + 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 + 257 290 // place vegetation 258 291 for ( 259 292 let j = 0; ··· 261 294 j++ 262 295 ) { 263 296 const vegetation = biome.options.vegetation.items[j]; 264 - if (Math.random() < faceSize * vegetation.density) { 297 + if (Math.random() < faceSize * (vegetation.density ?? 1)) { 265 298 // discard if point is below or above height limits 266 299 if ( 267 300 vegetation.minimumHeight !== undefined && ··· 314 347 break; 315 348 } 316 349 } 317 - 318 - // calculate ocean vertices 319 - const oceanColor = biome.getSeaColor(mid, normalizedHeight); 320 - 321 - if (oceanColor) { 322 - oceanColors[i * 3] = oceanColor.r; 323 - oceanColors[i * 3 + 1] = oceanColor.g; 324 - oceanColors[i * 3 + 2] = oceanColor.b; 325 - 326 - oceanColors[i * 3 + 3] = oceanColor.r; 327 - oceanColors[i * 3 + 4] = oceanColor.g; 328 - oceanColors[i * 3 + 5] = oceanColor.b; 329 - 330 - oceanColors[i * 3 + 6] = oceanColor.r; 331 - oceanColors[i * 3 + 7] = oceanColor.g; 332 - oceanColors[i * 3 + 8] = oceanColor.b; 333 - } 334 - 335 - // calculate ocean normals 336 - temp 337 - .crossVectors(oceanB.clone().sub(oceanA), oceanC.clone().sub(oceanA)) 338 - .normalize(); 339 - 340 - oceanNormals.setXYZ(i, temp.x, temp.y, temp.z); 341 - oceanNormals.setXYZ(i + 1, temp.x, temp.y, temp.z); 342 - oceanNormals.setXYZ(i + 2, temp.x, temp.y, temp.z); 343 - 344 - temp 345 - .crossVectors(oceanE.clone().sub(oceanD), oceanF.clone().sub(oceanD)) 346 - .normalize(); 347 - 348 - oceanMorphNormals.push(temp.x, temp.y, temp.z); 349 - oceanMorphNormals.push(temp.x, temp.y, temp.z); 350 - oceanMorphNormals.push(temp.x, temp.y, temp.z); 351 350 } 352 351 353 - console.log("normHeightMax", normHeightMax); 354 - console.log("normHeightMin", normHeightMin); 355 - 356 352 const maxDist = 0.14; 353 + 354 + const color = new Color(); 355 + 357 356 // go through all vertices again and update height and color based on vegetation 358 357 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(); 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 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 - } 365 + color.setRGB(colors[i * 3], colors[i * 3 + 1], colors[i * 3 + 2]); 377 366 378 - let moveInfo = calculatedVerticesArray[i + j]; 367 + const output = biome.vegetationHeightAndColorForFace( 368 + a, 369 + b, 370 + c, 371 + color, 372 + faceSideLength, 373 + ); 379 374 380 - a.multiplyScalar( 381 - moveInfo.height + ((maxDist - closestDist) / maxDist) * 0.015, 382 - ); 375 + const moveDataA = calculatedVerticesArray[i]; 376 + const moveDataB = calculatedVerticesArray[i + 1]; 377 + const moveDataC = calculatedVerticesArray[i + 2]; 383 378 384 - vertices.setXYZ(i + j, a.x, a.y, a.z); 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); 385 383 386 - closestDistAll = Math.min(closestDist, closestDistAll); 387 - found = true; 388 - } 389 - } 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); 390 387 391 - if (found) { 392 - let existingColor = new Color( 393 - colors[i * 3], 394 - colors[i * 3 + 1], 395 - colors[i * 3 + 2], 396 - ); 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; 397 392 398 - // set color 399 - let newColor = new Color(0.1, 0.3, 0); 393 + colors[i * 3 + 3] = output.color.r; 394 + colors[i * 3 + 4] = output.color.g; 395 + colors[i * 3 + 5] = output.color.b; 400 396 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 - } 397 + colors[i * 3 + 6] = output.color.r; 398 + colors[i * 3 + 7] = output.color.g; 399 + colors[i * 3 + 8] = output.color.b; 409 400 } 410 401 411 402 oceanSphere.morphAttributes.position[0] = new Float32BufferAttribute(
+28
src/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 + }
+19 -18
src/worlds/helper/octree.ts
··· 25 25 capacity?: number; 26 26 }; 27 27 28 - export type Vector3Data = Vector3 & { data?: unknown }; 29 - 30 - export class Octree { 28 + export class Octree<T = unknown> { 31 29 boundary: Box3; 32 30 33 - points: Vector3[]; 31 + points: (Vector3 & { data?: T })[]; 34 32 35 33 capacity: number; 36 34 ··· 113 111 114 112 // returns array of points where 115 113 // distance between pos and point is less than dist 116 - 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; 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); 122 118 } 123 119 124 - // vector3 free version, returns points in box around xyz 125 - queryXYZ(x: number, y: number, z: number, s: number) { 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) { 126 128 const min = new Vector3(x - s, y - s, z - s), 127 129 max = new Vector3(x + s, y + s, z + s); 128 130 const box = new Box3(min, max); ··· 130 132 return this.queryBox(box); 131 133 } 132 134 133 - queryBox(box: Box3, found: Vector3Data[] = []) { 135 + queryBox(box: Box3, found: (Vector3 & { data?: T })[] = []) { 134 136 found ??= []; 135 137 136 138 if (!box.intersectsBox(this.boundary)) return found; ··· 152 154 } 153 155 154 156 // insert point with optional data (sets vec.data = data) 155 - insert(pos: Vector3Data, data: unknown = undefined) { 157 + insert(pos: Vector3 & { data?: T }, data: T | undefined = undefined) { 156 158 return this.insertPoint(pos, data); 157 159 } 158 160 159 161 // vector3 free version 160 - insertXYZ(x: number, y: number, z: number, data: unknown = undefined) { 162 + insertXYZ(x: number, y: number, z: number, data: T | undefined = undefined) { 161 163 return this.insertPoint(new Vector3(x, y, z), data); 162 164 } 163 165 164 - insertPoint(p: Vector3, data: unknown = undefined) { 166 + insertPoint(p: Vector3, data: T | undefined = undefined) { 165 167 p = p.clone(); 166 168 167 169 // @ts-expect-error - data is not a property of Vector3 ··· 235 237 q = this.query(opts.p, opts.min); 236 238 237 239 for (const point of q) { 238 - // @ts-expect-error - close is not a property of Vector3 239 240 point.close = true; 240 241 } 241 242 } ··· 278 279 return boxes; 279 280 } 280 281 281 - all(arr: Vector3Data[] = []) { 282 + all(arr: (Vector3 & { data?: T })[] = []) { 282 283 arr ??= []; 283 284 for (const p of this.points) { 284 285 arr.push(p);
+65 -56
src/worlds/materials/OceanCausticsMaterial.ts
··· 1 - import { MeshStandardMaterial } from "three"; 1 + import { 2 + MeshStandardMaterial, 3 + type MeshStandardMaterialParameters, 4 + } from "three"; 2 5 import { noise } from "./noise"; 3 6 4 - const oceansCausticMaterial = new MeshStandardMaterial({ 5 - vertexColors: true, 6 - }); 7 - oceansCausticMaterial.onBeforeCompile = (shader) => { 8 - const caustics = ` 9 - float caustics(vec4 vPos) { 10 - // More intricate warping for marble patterns 11 - // float warpFactor = 2.0; 12 - // vec4 warpedPos = vPos * warpFactor + snoise(vPos * warpFactor * 0.5); 13 - // vec4 warpedPos2 = warpedPos * warpFactor * 0.3 + snoise(warpedPos * warpFactor * 0.5 + vec4(0, 2, 4, 8)) + vPos; 7 + export class PlanetMaterialWithCaustics extends MeshStandardMaterial { 8 + constructor(parameters: MeshStandardMaterialParameters) { 9 + super(parameters); 14 10 15 - // // Modulate the color intensity based on the noise 16 - // float vein = snoise(warpedPos2 * warpFactor) * snoise(warpedPos); 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 + ); 17 39 18 - // float a = 1.0 - (sin(vein * 12.0) + 1.0) * 0.5; 19 - // float diff = snoise(vPos * warpFactor); 20 - // diff = diff * snoise(diff * vPos) * a; 21 - // return vec3((diff)); 40 + shader.fragmentShader = ` 41 + uniform float time; 42 + varying vec3 vPos; 43 + ${noise} 44 + ${caustics} 45 + ${shader.fragmentShader}`; 22 46 23 - vec4 warpedPos = vPos * 2.0 + snoise(vPos * 3.0); 24 - vec4 warpedPos2 = warpedPos * 0.3 + snoise(warpedPos * 2.0 + vec4(0, 2, 4, 8)) + vPos; 25 - float vein = snoise(warpedPos2) * snoise(warpedPos); 26 - float a = 1.0 - (sin(vein * 2.0) + 1.0) * 0.5; 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 + ); 27 60 28 - return snoise(vPos + warpedPos + warpedPos2) * a * 1.5; 29 - }`; 30 - shader.vertexShader = `varying vec3 vPos;\n${shader.vertexShader}`.replace( 31 - `#include <begin_vertex>`, 32 - `#include <begin_vertex>\nvPos = position;`, 33 - ); 61 + shader.uniforms.time = { value: 0 }; 62 + this.userData.shader = shader; 63 + }; 64 + } 34 65 35 - shader.fragmentShader = ` 36 - uniform float time; 37 - varying vec3 vPos; 38 - ${noise} 39 - ${caustics} 40 - ${shader.fragmentShader}`; 66 + update() { 67 + if (this.userData.shader?.uniforms?.time) { 68 + this.userData.shader.uniforms.time.value = performance.now() / 1000; 69 + } 70 + } 71 + } 41 72 42 - shader.fragmentShader = shader.fragmentShader.replace( 43 - "#include <color_fragment>", 44 - `#include <color_fragment> 45 - vec3 pos = vPos * 3.0; 46 - float len = length(vPos); 47 - // Fade in 48 - float fadeIn = smoothstep(0.96, 0.985, len); 49 - // Fade out 50 - float fadeOut = 1.0 - smoothstep(0.994, 0.999, len); 51 - float causticIntensity = fadeIn * fadeOut * 0.7; 52 - diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), causticIntensity * smoothstep(0.0, 1.0, caustics(vec4(pos, time * 0.05)))); 53 - `, 54 - ); 55 - 56 - shader.uniforms.time = { value: 0 }; 57 - oceansCausticMaterial.userData.shader = shader; 58 - 59 - // console.log("FRAGMENT", shader.fragmentShader); 60 - // console.log(); 61 - // console.log("VERTEX", shader.vertexShader); 62 - }; 63 - 64 - export default oceansCausticMaterial; 73 + export default PlanetMaterialWithCaustics;
-1
src/worlds/materials/noise.ts
··· 118 118 m0 = m0 * m0; 119 119 m1 = m1 * m1; 120 120 return 49.0 * (dot(m0 * m0, vec3(dot(p0, x0), dot(p1, x1), dot(p2, x2))) + dot(m1 * m1, vec2(dot(p3, x3), dot(p4, x4)))); 121 - 122 121 }`;