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