victor HF Staff commited on
Commit
0e2a721
·
verified ·
1 Parent(s): fed753b

Upload folder using huggingface_hub

Browse files
Files changed (6) hide show
  1. .gitignore +35 -0
  2. README.md +15 -1
  3. bun.lock +29 -0
  4. index.html +787 -0
  5. package.json +12 -0
  6. tsconfig.json +27 -0
.gitignore ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # dependencies (bun install)
2
+ node_modules
3
+
4
+ # output
5
+ out
6
+ dist
7
+ *.tgz
8
+
9
+ # code coverage
10
+ coverage
11
+ *.lcov
12
+
13
+ # logs
14
+ logs
15
+ _.log
16
+ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
17
+
18
+ # dotenv environment variable files
19
+ .env
20
+ .env.development.local
21
+ .env.test.local
22
+ .env.production.local
23
+ .env.local
24
+
25
+ # caches
26
+ .eslintcache
27
+ .cache
28
+ *.tsbuildinfo
29
+
30
+ # IntelliJ based IDEs
31
+ .idea
32
+
33
+ # Finder (MacOS) folder config
34
+ .DS_Store
35
+ .aider*
README.md CHANGED
@@ -1 +1,15 @@
1
- a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # forest
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ To run:
10
+
11
+ ```bash
12
+ bun run index.ts
13
+ ```
14
+
15
+ This project was created using `bun init` in bun v1.2.4. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
bun.lock ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "lockfileVersion": 1,
3
+ "workspaces": {
4
+ "": {
5
+ "name": "forest",
6
+ "devDependencies": {
7
+ "@types/bun": "latest",
8
+ },
9
+ "peerDependencies": {
10
+ "typescript": "^5",
11
+ },
12
+ },
13
+ },
14
+ "packages": {
15
+ "@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
16
+
17
+ "@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="],
18
+
19
+ "@types/react": ["@types/[email protected]", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
20
+
21
+ "bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
22
+
23
+ "csstype": ["[email protected]", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
24
+
25
+ "typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
26
+
27
+ "undici-types": ["[email protected]", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
28
+ }
29
+ }
index.html ADDED
@@ -0,0 +1,787 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>Three.js Forest with Orbit + Arrow Move + Lighting + Stars</title>
7
+ <style>
8
+ :root { color-scheme: dark; }
9
+ html, body { height: 100%; }
10
+ body {
11
+ margin: 0;
12
+ overflow: hidden;
13
+ background: #0a0f12;
14
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
15
+ }
16
+ .ui {
17
+ position: absolute;
18
+ top: 12px;
19
+ left: 12px;
20
+ padding: 10px 12px;
21
+ background: rgba(0,0,0,0.35);
22
+ border: 1px solid rgba(255,255,255,0.1);
23
+ border-radius: 8px;
24
+ backdrop-filter: blur(6px);
25
+ -webkit-backdrop-filter: blur(6px);
26
+ color: #cfe3d4;
27
+ font-size: 12px;
28
+ line-height: 1.4;
29
+ user-select: none;
30
+ max-width: 360px;
31
+ }
32
+ .ui h1 {
33
+ margin: 0 0 6px 0;
34
+ font-size: 14px;
35
+ font-weight: 600;
36
+ color: #e8f5ea;
37
+ letter-spacing: 0.3px;
38
+ }
39
+ .ui .row {
40
+ display: flex;
41
+ align-items: center;
42
+ gap: 8px;
43
+ margin: 6px 0;
44
+ }
45
+ .ui label {
46
+ min-width: 90px;
47
+ color: #cde2cf;
48
+ }
49
+ .ui input[type=range] { width: 180px; }
50
+ .ui .hint { opacity: 0.85; font-size: 11px; margin-top: 6px; }
51
+ .kbd {
52
+ display: inline-block;
53
+ padding: 1px 6px;
54
+ border: 1px solid rgba(255,255,255,0.2);
55
+ border-radius: 4px;
56
+ background: rgba(255,255,255,0.06);
57
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
58
+ font-size: 11px;
59
+ line-height: 16px;
60
+ }
61
+ </style>
62
+ </head>
63
+ <body>
64
+ <div class="ui">
65
+ <h1>Procedural Forest</h1>
66
+ <div class="row">
67
+ <label for="density">Density</label>
68
+ <input id="density" type="range" min="50" max="1200" value="550" />
69
+ <span id="densityVal">550</span>
70
+ </div>
71
+ <div class="row">
72
+ <label for="wind">Wind</label>
73
+ <input id="wind" type="range" min="0" max="100" value="35" />
74
+ <span id="windVal">0.35</span>
75
+ </div>
76
+ <div class="row">
77
+ <label for="size">Area</label>
78
+ <input id="size" type="range" min="150" max="600" value="350" />
79
+ <span id="sizeVal">350</span>
80
+ </div>
81
+
82
+ <h1 style="margin-top:10px;">Lighting</h1>
83
+ <div class="row">
84
+ <label for="sunInt">Sun Intensity</label>
85
+ <input id="sunInt" type="range" min="0" max="2000" value="1000" />
86
+ <span id="sunIntVal">10.00</span>
87
+ </div>
88
+ <div class="row">
89
+ <label for="sunElev">Sun Elev</label>
90
+ <input id="sunElev" type="range" min="5" max="85" value="60" />
91
+ <span id="sunElevVal">60°</span>
92
+ </div>
93
+ <div class="row">
94
+ <label for="sunAzim">Sun Azim</label>
95
+ <input id="sunAzim" type="range" min="0" max="360" value="145" />
96
+ <span id="sunAzimVal">145°</span>
97
+ </div>
98
+ <div class="row">
99
+ <label for="hemi">Sky/Amb</label>
100
+ <input id="hemi" type="range" min="0" max="150" value="60" />
101
+ <span id="hemiVal">0.60</span>
102
+ </div>
103
+
104
+ <div class="hint">
105
+ Orbit: Left drag • Pan: Right drag • Zoom: Wheel<br/>
106
+ Move camera with <span class="kbd">↑</span><span class="kbd">↓</span><span class="kbd">←</span><span class="kbd">→</span> • Boost: <span class="kbd">Shift</span><br/>
107
+ Tip: Click the canvas once to ensure it has focus for arrow keys.
108
+ </div>
109
+ </div>
110
+
111
+ <script type="importmap">
112
+ {
113
+ "imports": {
114
+ "three": "https://unpkg.com/[email protected]/build/three.module.js",
115
+ "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
116
+ }
117
+ }
118
+ </script>
119
+ <script type="module">
120
+ import * as THREE from 'three';
121
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
122
+
123
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
124
+ renderer.setSize(window.innerWidth, window.innerHeight);
125
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
126
+ renderer.shadowMap.enabled = true;
127
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
128
+ renderer.domElement.tabIndex = 0; // make focusable for key events
129
+ document.body.appendChild(renderer.domElement);
130
+
131
+ const scene = new THREE.Scene();
132
+ scene.fog = new THREE.FogExp2(0x0d1713, 0.0022);
133
+
134
+ // Lights
135
+ const hemiLight = new THREE.HemisphereLight(0xbfdde3, 0x1e3222, 0.6);
136
+ scene.add(hemiLight);
137
+
138
+ const dirLight = new THREE.DirectionalLight(0xfff4d6, 3.0); // default x3 intensity
139
+ dirLight.position.set(-60, 120, 40);
140
+ dirLight.castShadow = true;
141
+ dirLight.shadow.mapSize.set(2048, 2048);
142
+ const cam = dirLight.shadow.camera;
143
+ const shadowExtent = 300;
144
+ cam.left = -shadowExtent;
145
+ cam.right = shadowExtent;
146
+ cam.top = shadowExtent;
147
+ cam.bottom = -shadowExtent;
148
+ cam.near = 10;
149
+ cam.far = 400;
150
+ scene.add(dirLight);
151
+
152
+ // Hook up UI controls
153
+ const densityEl = document.getElementById('density');
154
+ const densityValEl = document.getElementById('densityVal');
155
+ const windEl = document.getElementById('wind');
156
+ const windValEl = document.getElementById('windVal');
157
+ const sizeEl = document.getElementById('size');
158
+ const sizeValEl = document.getElementById('sizeVal');
159
+
160
+ const sunIntEl = document.getElementById('sunInt');
161
+ const sunIntValEl = document.getElementById('sunIntVal');
162
+ const sunElevEl = document.getElementById('sunElev');
163
+ const sunElevValEl = document.getElementById('sunElevVal');
164
+ const sunAzimEl = document.getElementById('sunAzim');
165
+ const sunAzimValEl = document.getElementById('sunAzimVal');
166
+ const hemiEl = document.getElementById('hemi');
167
+ const hemiValEl = document.getElementById('hemiVal');
168
+
169
+ // Initial UI state sync
170
+ function setSunFromUI() {
171
+ const intensity = Number(sunIntEl.value) / 100; // 0..20
172
+ dirLight.intensity = intensity;
173
+ sunIntValEl.textContent = intensity.toFixed(2);
174
+
175
+ const elevDeg = Number(sunElevEl.value);
176
+ const azimDeg = Number(sunAzimEl.value);
177
+ sunElevValEl.textContent = elevDeg + '°';
178
+ sunAzimValEl.textContent = azimDeg + '°';
179
+
180
+ // Position the directional light based on elevation/azimuth around origin
181
+ const elev = THREE.MathUtils.degToRad(elevDeg);
182
+ const azim = THREE.MathUtils.degToRad(azimDeg);
183
+ const r = 200;
184
+ const y = Math.sin(elev) * r;
185
+ const x = Math.cos(elev) * Math.cos(azim) * r;
186
+ const z = Math.cos(elev) * Math.sin(azim) * r;
187
+ dirLight.position.set(x, y, z);
188
+ dirLight.target.position.set(0, 0, 0);
189
+ dirLight.target.updateMatrixWorld();
190
+ }
191
+
192
+ function setHemiFromUI() {
193
+ const hemiIntensity = Number(hemiEl.value) / 100; // 0..1.5
194
+ hemiLight.intensity = hemiIntensity;
195
+ hemiValEl.textContent = hemiIntensity.toFixed(2);
196
+ }
197
+
198
+ function setWindFromUI() {
199
+ const w = Number(windEl.value) / 100;
200
+ wind.strength = w;
201
+ windValEl.textContent = w.toFixed(2);
202
+ windUniforms.uStrength.value = w;
203
+ }
204
+
205
+ function setDensityFromUI() {
206
+ const d = Number(densityEl.value);
207
+ densityValEl.textContent = String(d);
208
+ regenerateForest(d, areaSize.value);
209
+ regenerateDecor(areaSize.value);
210
+ }
211
+
212
+ function setAreaFromUI() {
213
+ const s = Number(sizeEl.value);
214
+ areaSize.value = s;
215
+ sizeValEl.textContent = String(s);
216
+ regenerateForest(Number(densityEl.value), s);
217
+ regenerateDecor(s);
218
+ }
219
+
220
+ sunIntEl.addEventListener('input', setSunFromUI);
221
+ sunElevEl.addEventListener('input', setSunFromUI);
222
+ sunAzimEl.addEventListener('input', setSunFromUI);
223
+ hemiEl.addEventListener('input', setHemiFromUI);
224
+ windEl.addEventListener('input', setWindFromUI);
225
+ densityEl.addEventListener('change', setDensityFromUI);
226
+ sizeEl.addEventListener('change', setAreaFromUI);
227
+
228
+ // Ground
229
+ const areaSize = { value: 350 };
230
+ const groundSegments = 256;
231
+ const groundGeo = new THREE.PlaneGeometry(1000, 1000, groundSegments, groundSegments);
232
+ groundGeo.rotateX(-Math.PI / 2);
233
+ const pos = groundGeo.attributes.position;
234
+ const tmp = new THREE.Vector3();
235
+ for (let i = 0; i < pos.count; i++) {
236
+ tmp.fromBufferAttribute(pos, i);
237
+ const nx = tmp.x * 0.01;
238
+ const nz = tmp.z * 0.01;
239
+ const h =
240
+ Math.sin(nx) * Math.cos(nz) * 1.2 +
241
+ Math.sin(nx * 0.21 + 2.7) * Math.sin(nz * 0.19 + 1.5) * 0.7 +
242
+ Math.cos(nx * 0.07 - 1.1) * Math.sin(nz * 0.09 + 0.4) * 2.0;
243
+ pos.setY(i, h);
244
+ }
245
+ groundGeo.computeVertexNormals();
246
+
247
+ function makeGrassTexture(w = 256, h = 256) {
248
+ const size = w * h * 3;
249
+ const data = new Uint8Array(size);
250
+ for (let y = 0; y < h; y++) {
251
+ for (let x = 0; x < w; x++) {
252
+ const i = (y * w + x) * 3;
253
+ const u = x / w, v = y / h;
254
+ const noise =
255
+ Math.sin(u * 20.0) * 0.5 + Math.cos(v * 18.0) * 0.5 +
256
+ Math.sin((u + v) * 8.0) * 0.4;
257
+ const base = 80 + noise * 40;
258
+ const g = THREE.MathUtils.clamp(base + (v - 0.5) * 30, 50, 190);
259
+ const r = g * 0.6;
260
+ const b = g * 0.5;
261
+ data[i] = r; data[i + 1] = g; data[i + 2] = b;
262
+ }
263
+ }
264
+ const tex = new THREE.DataTexture(data, w, h, THREE.RGBFormat);
265
+ tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
266
+ tex.needsUpdate = true;
267
+ return tex;
268
+ }
269
+ const grassTex = makeGrassTexture(256, 256);
270
+ grassTex.repeat.set(20, 20);
271
+ const groundMat = new THREE.MeshStandardMaterial({
272
+ map: grassTex,
273
+ roughness: 1.0,
274
+ metalness: 0.0,
275
+ color: new THREE.Color(0x5c8b63).multiplyScalar(0.9)
276
+ });
277
+ const ground = new THREE.Mesh(groundGeo, groundMat);
278
+ ground.receiveShadow = true;
279
+ scene.add(ground);
280
+
281
+ // Trees
282
+ const treeGroup = new THREE.Group();
283
+ scene.add(treeGroup);
284
+
285
+ const trunkGeo = new THREE.CylinderGeometry(1, 1.8, 10, 8, 1, false);
286
+ trunkGeo.translate(0, 5, 0);
287
+
288
+ function makeFoliageGeometry() {
289
+ const g = new THREE.BufferGeometry();
290
+ const geometries = [];
291
+
292
+ const cone1 = new THREE.ConeGeometry(6, 10, 8);
293
+ cone1.translate(0, 14, 0); geometries.push(cone1);
294
+ const cone2 = new THREE.ConeGeometry(5, 9, 8);
295
+ cone2.translate(0, 20, 0); geometries.push(cone2);
296
+ const cone3 = new THREE.ConeGeometry(4, 8, 8);
297
+ cone3.translate(0, 25, 0); geometries.push(cone3);
298
+ const sph = new THREE.SphereGeometry(3.5, 8, 6);
299
+ sph.translate(0, 28.5, 0); geometries.push(sph);
300
+
301
+ let totalVertices = 0, totalIndices = 0;
302
+ for (const geo of geometries) {
303
+ geo.computeVertexNormals();
304
+ totalVertices += geo.attributes.position.count;
305
+ totalIndices += geo.index ? geo.index.count : geo.attributes.position.count;
306
+ }
307
+ const posArr = new Float32Array(totalVertices * 3);
308
+ const normArr = new Float32Array(totalVertices * 3);
309
+ const uvArr = new Float32Array(totalVertices * 2);
310
+ const indexArr = new Uint32Array(totalIndices);
311
+
312
+ let vOffset = 0, iOffset = 0, baseVertex = 0;
313
+ for (const geo of geometries) {
314
+ const p = geo.attributes.position.array;
315
+ const n = geo.attributes.normal.array;
316
+ const u = geo.attributes.uv ? geo.attributes.uv.array : new Float32Array(geo.attributes.position.count * 2);
317
+ posArr.set(p, vOffset * 3);
318
+ normArr.set(n, vOffset * 3);
319
+ uvArr.set(u, vOffset * 2);
320
+
321
+ if (geo.index) {
322
+ const idx = geo.index.array;
323
+ for (let i = 0; i < idx.length; i++) indexArr[iOffset + i] = idx[i] + baseVertex;
324
+ iOffset += idx.length;
325
+ } else {
326
+ for (let i = 0; i < geo.attributes.position.count; i++) indexArr[iOffset++] = baseVertex + i;
327
+ }
328
+ baseVertex += geo.attributes.position.count;
329
+ vOffset += geo.attributes.position.count;
330
+ }
331
+ g.setAttribute('position', new THREE.BufferAttribute(posArr, 3));
332
+ g.setAttribute('normal', new THREE.BufferAttribute(normArr, 3));
333
+ g.setAttribute('uv', new THREE.BufferAttribute(uvArr, 2));
334
+ g.setIndex(new THREE.BufferAttribute(indexArr, 1));
335
+ g.computeBoundingSphere(); g.computeBoundingBox();
336
+ return g;
337
+ }
338
+
339
+ const foliageGeo = makeFoliageGeometry();
340
+
341
+ const trunkMat = new THREE.MeshStandardMaterial({
342
+ color: 0x6b4f32, roughness: 0.9, metalness: 0.0
343
+ });
344
+ const foliageMat = new THREE.MeshStandardMaterial({
345
+ color: 0x2f6b3d, roughness: 0.8, metalness: 0.0
346
+ });
347
+
348
+ let trunkInstanced, foliageInstanced;
349
+ function makeInstancedMeshes(count) {
350
+ if (trunkInstanced) treeGroup.remove(trunkInstanced);
351
+ if (foliageInstanced) treeGroup.remove(foliageInstanced);
352
+ trunkInstanced = new THREE.InstancedMesh(trunkGeo, trunkMat, count);
353
+ trunkInstanced.castShadow = true; trunkInstanced.receiveShadow = true;
354
+ foliageInstanced = new THREE.InstancedMesh(foliageGeo, foliageMat, count);
355
+ foliageInstanced.castShadow = true; foliageInstanced.receiveShadow = true;
356
+ treeGroup.add(trunkInstanced, foliageInstanced);
357
+ }
358
+
359
+ const wind = { strength: 0.35 };
360
+ const rng = (seed => () => { seed = (seed * 1664525 + 1013904223) % 4294967296; return seed / 4294967296; })(123456789);
361
+
362
+ const groundHeightAt = (x, z) => {
363
+ const nx = x * 0.01, nz = z * 0.01;
364
+ return Math.sin(nx) * Math.cos(nz) * 1.2 +
365
+ Math.sin(nx * 0.21 + 2.7) * Math.sin(nz * 0.19 + 1.5) * 0.7 +
366
+ Math.cos(nx * 0.07 - 1.1) * Math.sin(nz * 0.09 + 0.4) * 2.0;
367
+ };
368
+
369
+ let treeData = [];
370
+ function regenerateForest(count, radius = areaSize.value) {
371
+ makeInstancedMeshes(count);
372
+ treeData.length = 0;
373
+
374
+ const minSpacing = 4.0;
375
+ const ext = radius;
376
+ const attempts = count * 8;
377
+ const points = [];
378
+ for (let i = 0; i < attempts && points.length < count; i++) {
379
+ const x = (rng() * 2 - 1) * ext;
380
+ const z = (rng() * 2 - 1) * ext;
381
+ const h = groundHeightAt(x, z);
382
+ const hx = groundHeightAt(x + 1.0, z);
383
+ const hz = groundHeightAt(x, z + 1.0);
384
+ const slope = Math.hypot(hx - h, hz - h);
385
+ if (slope > 1.2) continue;
386
+
387
+ let ok = true;
388
+ for (let p = 0; p < points.length; p++) {
389
+ const dx = x - points[p].x;
390
+ const dz = z - points[p].z;
391
+ if (dx * dx + dz * dz < minSpacing * minSpacing) { ok = false; break; }
392
+ }
393
+ if (!ok) continue;
394
+
395
+ const t = {
396
+ x, z, h,
397
+ scale: 0.8 + rng() * 1.6,
398
+ type: rng() < 0.7 ? 'pine' : 'round',
399
+ swayPhase: rng() * Math.PI * 2,
400
+ swayAmp: 0.02 + rng() * 0.04
401
+ };
402
+ points.push({ x, z });
403
+ treeData.push(t);
404
+ }
405
+
406
+ const m = new THREE.Matrix4();
407
+ const q = new THREE.Quaternion();
408
+ const up = new THREE.Vector3(0, 1, 0);
409
+ for (let i = 0; i < treeData.length; i++) {
410
+ const t = treeData[i];
411
+ q.setFromAxisAngle(up, rng() * Math.PI * 2);
412
+ m.compose(new THREE.Vector3(t.x, t.h, t.z), q, new THREE.Vector3(1, t.scale, 1));
413
+ trunkInstanced.setMatrixAt(i, m);
414
+
415
+ const foliageScale = 0.9 * t.scale;
416
+ m.compose(new THREE.Vector3(t.x, t.h, t.z), q, new THREE.Vector3(foliageScale, foliageScale, foliageScale));
417
+ foliageInstanced.setMatrixAt(i, m);
418
+
419
+ const baseLeaf = new THREE.Color(0x2f6b3d);
420
+ const shade = 0.8 + rng() * 0.4;
421
+ const tint = baseLeaf.clone().multiplyScalar(shade);
422
+ foliageInstanced.setColorAt(i, tint);
423
+
424
+ const trunkColor = new THREE.Color(0x5c3f28).offsetHSL(0, 0, (rng() - 0.5) * 0.05);
425
+ trunkInstanced.setColorAt(i, trunkColor);
426
+ }
427
+ trunkInstanced.instanceColor = new THREE.InstancedBufferAttribute(trunkInstanced.instanceColor.array, 3);
428
+ foliageInstanced.instanceColor = new THREE.InstancedBufferAttribute(foliageInstanced.instanceColor.array, 3);
429
+ trunkInstanced.instanceMatrix.needsUpdate = true;
430
+ foliageInstanced.instanceMatrix.needsUpdate = true;
431
+ }
432
+
433
+ const windUniforms = { uTime: { value: 0 }, uStrength: { value: wind.strength } };
434
+ foliageMat.onBeforeCompile = (shader) => {
435
+ shader.uniforms.uTime = windUniforms.uTime;
436
+ shader.uniforms.uStrength = windUniforms.uStrength;
437
+ shader.vertexShader = `
438
+ uniform float uTime;
439
+ uniform float uStrength;
440
+ ` + shader.vertexShader.replace(
441
+ '#include <begin_vertex>',
442
+ `
443
+ #include <begin_vertex>
444
+ float sway = sin(uTime * 1.7 + position.y * 0.35) * 0.5 + sin(uTime * 0.9 + position.y * 0.7) * 0.5;
445
+ transformed.x += sway * uStrength * (0.4 + position.y * 0.06);
446
+ transformed.z += cos(uTime * 1.1 + position.y * 0.42) * uStrength * (0.3 + position.y * 0.05);
447
+ `
448
+ );
449
+ };
450
+ foliageMat.needsUpdate = true;
451
+
452
+ // Decorations
453
+ const decoGroup = new THREE.Group(); scene.add(decoGroup);
454
+ function makeRockGeometry() {
455
+ const geo = new THREE.IcosahedronGeometry(1.0, 1);
456
+ const arr = geo.attributes.position.array;
457
+ for (let i = 0; i < arr.length; i += 3) {
458
+ const r = 0.7 + rng() * 0.6;
459
+ arr[i] *= r; arr[i + 1] *= (0.6 + rng() * 0.4 + 0.2); arr[i + 2] *= r;
460
+ }
461
+ geo.computeVertexNormals();
462
+ return geo;
463
+ }
464
+ const rockGeo = makeRockGeometry();
465
+ const rockMat = new THREE.MeshStandardMaterial({ color: 0x808a8f, roughness: 0.95, metalness: 0.0 });
466
+ const rocksInst = new THREE.InstancedMesh(rockGeo, rockMat, 400);
467
+ rocksInst.castShadow = true; rocksInst.receiveShadow = true; decoGroup.add(rocksInst);
468
+
469
+ const grassBladeGeo = new THREE.PlaneGeometry(0.08, 1.2, 1, 3);
470
+ grassBladeGeo.translate(0, 0.6, 0);
471
+ const grassMat = new THREE.MeshStandardMaterial({ color: 0x5aa35f, side: THREE.DoubleSide, roughness: 1.0, metalness: 0.0 });
472
+ const grassInst = new THREE.InstancedMesh(grassBladeGeo, grassMat, 2000);
473
+ grassInst.castShadow = false; grassInst.receiveShadow = true; decoGroup.add(grassInst);
474
+
475
+ function regenerateDecor(radius = areaSize.value) {
476
+ const m = new THREE.Matrix4();
477
+ const q = new THREE.Quaternion();
478
+
479
+ const rockCount = rocksInst.count;
480
+ const rockExt = radius * 0.95;
481
+ for (let i = 0; i < rockCount; i++) {
482
+ const x = (rng() * 2 - 1) * rockExt;
483
+ const z = (rng() * 2 - 1) * rockExt;
484
+ const h = groundHeightAt(x, z);
485
+ q.setFromAxisAngle(new THREE.Vector3(0,1,0), rng() * Math.PI * 2);
486
+ const s = 0.6 + rng() * 2.0;
487
+ m.compose(new THREE.Vector3(x, h, z), q, new THREE.Vector3(s, s * (0.6 + rng() * 0.4), s));
488
+ rocksInst.setMatrixAt(i, m);
489
+ const shade = 0.8 + rng() * 0.3;
490
+ const c = new THREE.Color().setRGB(0.6 * shade, 0.66 * shade, 0.72 * shade);
491
+ rocksInst.setColorAt(i, c);
492
+ }
493
+ rocksInst.instanceMatrix.needsUpdate = true;
494
+ if (!rocksInst.instanceColor) rocksInst.instanceColor = new THREE.InstancedBufferAttribute(new Float32Array(rockCount * 3), 3);
495
+ rocksInst.instanceColor.needsUpdate = true;
496
+
497
+ const grassCount = grassInst.count;
498
+ const grassExt = radius;
499
+ for (let i = 0; i < grassCount; i++) {
500
+ const x = (rng() * 2 - 1) * grassExt;
501
+ const z = (rng() * 2 - 1) * grassExt;
502
+ const h = groundHeightAt(x, z);
503
+ q.setFromAxisAngle(new THREE.Vector3(0,1,0), rng() * Math.PI * 2);
504
+ const s = 0.7 + rng() * 0.6;
505
+ m.compose(new THREE.Vector3(x, h, z), q, new THREE.Vector3(s, s, s));
506
+ grassInst.setMatrixAt(i, m);
507
+ const g = 0.7 + rng() * 0.3;
508
+ const col = new THREE.Color(0x5aa35f).multiplyScalar(g);
509
+ grassInst.setColorAt(i, col);
510
+ }
511
+ grassInst.instanceMatrix.needsUpdate = true;
512
+ if (!grassInst.instanceColor) grassInst.instanceColor = new THREE.InstancedBufferAttribute(new Float32Array(grassCount * 3), 3);
513
+ grassInst.instanceColor.needsUpdate = true;
514
+ }
515
+
516
+ // Atmosphere particles
517
+ const particleGeo = new THREE.BufferGeometry();
518
+ const PCOUNT = 800;
519
+ const positions = new Float32Array(PCOUNT * 3);
520
+ const speeds = new Float32Array(PCOUNT);
521
+ const pColors = new Float32Array(PCOUNT * 3);
522
+ for (let i = 0; i < PCOUNT; i++) {
523
+ positions[i * 3] = (rng() * 2 - 1) * 250;
524
+ positions[i * 3 + 1] = 1 + rng() * 12;
525
+ positions[i * 3 + 2] = (rng() * 2 - 1) * 250;
526
+ speeds[i] = 0.2 + rng() * 0.6;
527
+ const c = new THREE.Color().setHSL(0.14 + rng() * 0.05, 0.4, 0.6 + rng() * 0.2);
528
+ pColors[i * 3] = c.r; pColors[i * 3 + 1] = c.g; pColors[i * 3 + 2] = c.b;
529
+ }
530
+ particleGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
531
+ particleGeo.setAttribute('aSpeed', new THREE.BufferAttribute(speeds, 1));
532
+ particleGeo.setAttribute('color', new THREE.BufferAttribute(pColors, 3));
533
+ const particleMat = new THREE.PointsMaterial({ size: 0.6, sizeAttenuation: true, transparent: true, opacity: 0.7, depthWrite: false, vertexColors: true });
534
+ const particles = new THREE.Points(particleGeo, particleMat);
535
+ scene.add(particles);
536
+
537
+ // Water
538
+ const waterGeo = new THREE.PlaneGeometry(200, 8, 1, 1);
539
+ waterGeo.rotateX(-Math.PI / 2);
540
+ const waterMat = new THREE.MeshPhysicalMaterial({
541
+ color: 0x2a5b6b, roughness: 0.25, metalness: 0.0,
542
+ transmission: 0.2, thickness: 0.2, transparent: true, opacity: 0.85,
543
+ clearcoat: 0.4, clearcoatRoughness: 0.3
544
+ });
545
+ const water = new THREE.Mesh(waterGeo, waterMat);
546
+ water.position.set(0, groundHeightAt(0, 0) - 0.2, 0);
547
+ water.receiveShadow = true;
548
+ scene.add(water);
549
+
550
+ // Camera and controls
551
+ const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1200);
552
+ camera.position.set(60, 40, 80);
553
+ const controls = new OrbitControls(camera, renderer.domElement);
554
+ controls.enableDamping = true; controls.dampingFactor = 0.05;
555
+ controls.maxPolarAngle = Math.PI * 0.495;
556
+ controls.target.set(0, 8, 0); controls.update();
557
+
558
+ // Initial generation
559
+ regenerateForest(550, areaSize.value);
560
+ regenerateDecor(areaSize.value);
561
+
562
+ // Sync UI to scene after initial generation
563
+ setSunFromUI();
564
+ setHemiFromUI();
565
+ setWindFromUI();
566
+ setAreaFromUI();
567
+
568
+ // STARFIELD
569
+ function addStars() {
570
+ const starGeo = new THREE.BufferGeometry();
571
+ const STAR_COUNT = 4000;
572
+ const starPos = new Float32Array(STAR_COUNT * 3);
573
+ const starCol = new Float32Array(STAR_COUNT * 3);
574
+
575
+ // Distribute stars on a sphere shell to avoid fog-thick inner region
576
+ const radiusMin = 500;
577
+ const radiusMax = 900;
578
+ for (let i = 0; i < STAR_COUNT; i++) {
579
+ const u = rng();
580
+ const v = rng();
581
+ const theta = 2 * Math.PI * u;
582
+ const phi = Math.acos(2 * v - 1);
583
+ const r = radiusMin + rng() * (radiusMax - radiusMin);
584
+ const x = r * Math.sin(phi) * Math.cos(theta);
585
+ const y = r * Math.cos(phi);
586
+ const z = r * Math.sin(phi) * Math.sin(theta);
587
+ starPos[i * 3] = x;
588
+ starPos[i * 3 + 1] = y;
589
+ starPos[i * 3 + 2] = z;
590
+
591
+ const twinkle = 0.85 + rng() * 0.3;
592
+ const c = new THREE.Color().setHSL(0.57 + rng() * 0.1, 0.1 + rng() * 0.2, 0.9 * twinkle);
593
+ starCol[i * 3] = c.r; starCol[i * 3 + 1] = c.g; starCol[i * 3 + 2] = c.b;
594
+ }
595
+ starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
596
+ starGeo.setAttribute('color', new THREE.BufferAttribute(starCol, 3));
597
+
598
+ const starMat = new THREE.PointsMaterial({
599
+ size: 1.4,
600
+ sizeAttenuation: true,
601
+ depthWrite: false,
602
+ transparent: true,
603
+ opacity: 0.95,
604
+ vertexColors: true
605
+ });
606
+
607
+ const stars = new THREE.Points(starGeo, starMat);
608
+ stars.renderOrder = -1;
609
+ scene.add(stars);
610
+
611
+ // Soft nebula background using a big inverted sphere with emissive color
612
+ const skyGeo = new THREE.SphereGeometry(1200, 32, 16);
613
+ skyGeo.scale(-1, 1, 1); // inward facing
614
+ const skyMat = new THREE.MeshBasicMaterial({
615
+ color: 0x0b1216
616
+ });
617
+ const sky = new THREE.Mesh(skyGeo, skyMat);
618
+ scene.add(sky);
619
+
620
+ // Subtle rotation for parallax feel
621
+ return { stars, sky };
622
+ }
623
+ const starfield = addStars();
624
+
625
+
626
+
627
+ densityEl.addEventListener('input', () => {
628
+ const count = parseInt(densityEl.value, 10);
629
+ densityVal.textContent = count;
630
+ regenerateForest(count, areaSize.value);
631
+ });
632
+ windEl.addEventListener('input', () => {
633
+ const v = parseInt(windEl.value, 10) / 100;
634
+ wind.strength = v;
635
+ windUniforms.uStrength.value = v;
636
+ windVal.textContent = v.toFixed(2);
637
+ });
638
+ sizeEl.addEventListener('input', () => {
639
+ const v = parseInt(sizeEl.value, 10);
640
+ areaSize.value = v;
641
+ sizeVal.textContent = v;
642
+ regenerateForest(parseInt(densityEl.value, 10), v);
643
+ regenerateDecor(v);
644
+ });
645
+
646
+ function updateSunFromUI() {
647
+ const intensity = parseInt(sunIntEl.value, 10) / 100;
648
+ const elevDeg = parseInt(sunElevEl.value, 10);
649
+ const azimDeg = parseInt(sunAzimEl.value, 10);
650
+ const elev = THREE.MathUtils.degToRad(elevDeg);
651
+ const azim = THREE.MathUtils.degToRad(azimDeg);
652
+
653
+ dirLight.intensity = intensity;
654
+ const r = 180;
655
+ const y = Math.sin(elev) * r;
656
+ const horiz = Math.cos(elev) * r;
657
+ const x = Math.cos(azim) * horiz;
658
+ const z = Math.sin(azim) * horiz;
659
+ dirLight.position.set(x, y, z);
660
+
661
+ sunIntVal.textContent = intensity.toFixed(2);
662
+ sunElevVal.textContent = elevDeg + '°';
663
+ sunAzimVal.textContent = azimDeg + '°';
664
+ }
665
+ function updateHemiFromUI() {
666
+ const v = parseInt(hemiEl.value, 10) / 100;
667
+ hemiLight.intensity = v;
668
+ hemiVal.textContent = v.toFixed(2);
669
+ }
670
+ sunIntEl.addEventListener('input', updateSunFromUI);
671
+ sunElevEl.addEventListener('input', updateSunFromUI);
672
+ sunAzimEl.addEventListener('input', updateSunFromUI);
673
+ hemiEl.addEventListener('input', updateHemiFromUI);
674
+ updateSunFromUI();
675
+ updateHemiFromUI();
676
+
677
+ // Keyboard movement (arrow keys) - moves camera in view plane
678
+ const keys = { ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false, ShiftLeft: false, ShiftRight: false };
679
+ function setKey(code, down) {
680
+ if (code in keys) {
681
+ keys[code] = down;
682
+ return true;
683
+ }
684
+ return false;
685
+ }
686
+ // Use key property for better cross-browser Arrow naming, fallback to code
687
+ window.addEventListener('keydown', (e) => {
688
+ const hit = setKey(e.key || e.code, true) || setKey(e.code, true);
689
+ if (hit) { e.preventDefault(); }
690
+ }, { passive: false });
691
+ window.addEventListener('keyup', (e) => {
692
+ const hit = setKey(e.key || e.code, false) || setKey(e.code, false);
693
+ if (hit) { e.preventDefault(); }
694
+ }, { passive: false });
695
+ // Also allow WASD
696
+ window.addEventListener('keydown', (e) => {
697
+ if (e.key === 'w' || e.key === 'W') { keys.ArrowUp = true; e.preventDefault(); }
698
+ if (e.key === 's' || e.key === 'S') { keys.ArrowDown = true; e.preventDefault(); }
699
+ if (e.key === 'a' || e.key === 'A') { keys.ArrowLeft = true; e.preventDefault(); }
700
+ if (e.key === 'd' || e.key === 'D') { keys.ArrowRight = true; e.preventDefault(); }
701
+ }, { passive: false });
702
+ window.addEventListener('keyup', (e) => {
703
+ if (e.key === 'w' || e.key === 'W') { keys.ArrowUp = false; e.preventDefault(); }
704
+ if (e.key === 's' || e.key === 'S') { keys.ArrowDown = false; e.preventDefault(); }
705
+ if (e.key === 'a' || e.key === 'A') { keys.ArrowLeft = false; e.preventDefault(); }
706
+ if (e.key === 'd' || e.key === 'D') { keys.ArrowRight = false; e.preventDefault(); }
707
+ }, { passive: false });
708
+
709
+ function updateKeyboardMovement(dt) {
710
+ const speedBase = 25; // units per second
711
+ const boost = (keys.ShiftLeft || keys.ShiftRight) ? 3.0 : 1.0;
712
+ const speed = speedBase * boost;
713
+
714
+ let inputX = 0, inputZ = 0;
715
+ if (keys.ArrowUp) inputZ -= 1;
716
+ if (keys.ArrowDown) inputZ += 1;
717
+ if (keys.ArrowLeft) inputX -= 1;
718
+ if (keys.ArrowRight) inputX += 1;
719
+
720
+ if (inputX !== 0 || inputZ !== 0) {
721
+ const forward = new THREE.Vector3();
722
+ camera.getWorldDirection(forward);
723
+ forward.y = 0; forward.normalize();
724
+ // Right vector should be forward cross up
725
+ const right = new THREE.Vector3().crossVectors(forward, new THREE.Vector3(0,1,0)).normalize();
726
+
727
+ const move = new THREE.Vector3()
728
+ .addScaledVector(forward, inputZ)
729
+ .addScaledVector(right, inputX)
730
+ .normalize()
731
+ .multiplyScalar(speed * dt);
732
+
733
+ camera.position.add(move);
734
+ controls.target.add(move);
735
+ }
736
+ }
737
+
738
+ // Animation loop
739
+ const clock = new THREE.Clock();
740
+ function animate() {
741
+ requestAnimationFrame(animate);
742
+ const t = clock.getElapsedTime();
743
+ const dt = clock.getDelta();
744
+
745
+ windUniforms.uTime.value = t;
746
+
747
+ // Particle drift
748
+ const posAttr = particles.geometry.getAttribute('position');
749
+ const spdAttr = particles.geometry.getAttribute('aSpeed');
750
+ for (let i = 0; i < PCOUNT; i++) {
751
+ const y = posAttr.getY(i);
752
+ const x = posAttr.getX(i);
753
+ const z = posAttr.getZ(i);
754
+ const s = spdAttr.getX(i);
755
+ const nx = x + Math.sin(t * 0.6 + i) * 0.02;
756
+ const nz = z + Math.cos(t * 0.5 + i * 1.3) * 0.02;
757
+ let ny = y + (Math.sin(t * s + i) * 0.003 + 0.002);
758
+ if (ny > 14) ny = 1 + rng() * 2;
759
+ posAttr.setXYZ(i, nx, ny, nz);
760
+ }
761
+ posAttr.needsUpdate = true;
762
+
763
+ // Subtle star rotation for life
764
+ if (starfield && starfield.stars) {
765
+ starfield.stars.rotation.y = t * 0.002;
766
+ starfield.stars.rotation.x = Math.sin(t * 0.05) * 0.02;
767
+ }
768
+
769
+ // Keyboard move
770
+ updateKeyboardMovement(dt);
771
+
772
+ controls.update();
773
+ renderer.render(scene, camera);
774
+ }
775
+ animate();
776
+
777
+ window.addEventListener('resize', () => {
778
+ camera.aspect = window.innerWidth / window.innerHeight;
779
+ camera.updateProjectionMatrix();
780
+ renderer.setSize(window.innerWidth, window.innerHeight);
781
+ });
782
+
783
+ // Ensure canvas focus on click so arrow keys work immediately
784
+ renderer.domElement.addEventListener('pointerdown', () => renderer.domElement.focus());
785
+ </script>
786
+ </body>
787
+ </html>
package.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "forest",
3
+ "module": "index.ts",
4
+ "type": "module",
5
+ "private": true,
6
+ "devDependencies": {
7
+ "@types/bun": "latest"
8
+ },
9
+ "peerDependencies": {
10
+ "typescript": "^5"
11
+ }
12
+ }
tsconfig.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ // Enable latest features
4
+ "lib": ["ESNext", "DOM"],
5
+ "target": "ESNext",
6
+ "module": "ESNext",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+
22
+ // Some stricter flags (disabled by default)
23
+ "noUnusedLocals": false,
24
+ "noUnusedParameters": false,
25
+ "noPropertyAccessFromIndexSignature": false
26
+ }
27
+ }