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

flo-bit.github.io / src / lib / 3D / worlds / biome.ts
5.3 kB 237 lines
1import { UberNoise, type NoiseOptions } from 'uber-noise'; 2import { Color, type ColorRepresentation, Vector3 } from 'three'; 3 4import { ColorGradient, type ColorGradientOptions } from './helper/colorgradient'; 5import { biomePresets } from './presets'; 6import { Octree } from './helper/octree'; 7 8export 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}; 30 31export type BiomeOptions = { 32 name?: string; 33 34 preset?: string; 35 36 noise?: NoiseOptions; 37 colors?: ColorGradientOptions; 38 39 seaNoise?: NoiseOptions; 40 seaColors?: ColorGradientOptions; 41 42 tintColor?: number; 43 44 vegetation?: { 45 defaults?: Omit<Partial<VegetationItem>, 'name'>; 46 47 items: VegetationItem[]; 48 }; 49}; 50 51export class Biome { 52 noise: UberNoise | undefined; 53 colors: ColorGradient | undefined; 54 55 seaNoise: UberNoise | undefined; 56 seaColors: ColorGradient | undefined; 57 58 options: BiomeOptions; 59 60 vegetationPositions: Octree<VegetationItem> = new Octree(); 61 62 constructor(opts: BiomeOptions = {}) { 63 if (opts.preset) { 64 const preset = biomePresets[opts.preset]; 65 66 if (preset) { 67 opts = { 68 ...preset, 69 ...opts 70 }; 71 } 72 } 73 74 this.options = opts; 75 76 if (opts.noise) this.noise = new UberNoise(opts.noise); 77 78 if (opts.colors) { 79 this.colors = new ColorGradient(opts.colors); 80 } 81 82 if (opts.seaNoise) this.seaNoise = new UberNoise(opts.seaNoise); 83 84 if (opts.seaColors) { 85 this.seaColors = new ColorGradient(opts.seaColors); 86 } 87 } 88 89 get min(): number { 90 return this.noise?.min ?? -1; 91 } 92 93 get max(): number { 94 return this.noise?.max ?? 1; 95 } 96 97 getHeight(pos: Vector3): number { 98 if (this.noise) return this.noise.get(pos); 99 100 return 0; 101 } 102 103 getColor( 104 pos: Vector3, 105 height: number | undefined = undefined, 106 steepness: number = 0 107 ): Color | undefined { 108 if (this.noise && height === undefined) { 109 height = this.noise.normalized(pos); 110 } 111 112 if (this.colors) { 113 const color = this.colors.get(height); 114 if (this.options.tintColor) { 115 return color.lerp(new Color(this.options.tintColor), steepness / Math.PI); 116 } 117 return color; 118 } 119 120 return undefined; 121 } 122 123 getSeaHeight(pos: Vector3): number { 124 if (this.seaNoise) return this.seaNoise.get(pos); 125 126 return 0; 127 } 128 129 getSeaColor(pos: Vector3, ground: number | undefined): Color | undefined { 130 ground ??= this.noise?.normalized(pos) ?? 0; 131 132 if (this.seaColors) { 133 return this.seaColors.get(ground); 134 } 135 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 }; 236 } 237}