nova64 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +786 -0
  3. package/index.html +651 -0
  4. package/package.json +255 -0
  5. package/public/os9-shell/assets/index-B1Uvacma.js +32825 -0
  6. package/public/os9-shell/assets/index-B1Uvacma.js.map +1 -0
  7. package/public/os9-shell/assets/index-DIHfrTaW.css +1 -0
  8. package/public/os9-shell/index.html +14 -0
  9. package/public/os9-shell/nova-icon.svg +12 -0
  10. package/runtime/api-2d.js +878 -0
  11. package/runtime/api-3d/camera.js +73 -0
  12. package/runtime/api-3d/instancing.js +180 -0
  13. package/runtime/api-3d/lights.js +51 -0
  14. package/runtime/api-3d/materials.js +47 -0
  15. package/runtime/api-3d/models.js +84 -0
  16. package/runtime/api-3d/pbr.js +69 -0
  17. package/runtime/api-3d/primitives.js +304 -0
  18. package/runtime/api-3d/scene.js +169 -0
  19. package/runtime/api-3d/transforms.js +161 -0
  20. package/runtime/api-3d.js +154 -0
  21. package/runtime/api-effects.js +753 -0
  22. package/runtime/api-presets.js +85 -0
  23. package/runtime/api-skybox.js +178 -0
  24. package/runtime/api-sprites.js +100 -0
  25. package/runtime/api-voxel.js +601 -0
  26. package/runtime/api.js +201 -0
  27. package/runtime/assets.js +27 -0
  28. package/runtime/audio.js +114 -0
  29. package/runtime/collision.js +47 -0
  30. package/runtime/console.js +101 -0
  31. package/runtime/editor.js +233 -0
  32. package/runtime/font.js +233 -0
  33. package/runtime/framebuffer.js +28 -0
  34. package/runtime/fullscreen-button.js +185 -0
  35. package/runtime/gpu-canvas2d.js +47 -0
  36. package/runtime/gpu-threejs.js +639 -0
  37. package/runtime/gpu-webgl2.js +310 -0
  38. package/runtime/index.js +22 -0
  39. package/runtime/input.js +225 -0
  40. package/runtime/logger.js +60 -0
  41. package/runtime/physics.js +101 -0
  42. package/runtime/screens.js +213 -0
  43. package/runtime/storage.js +38 -0
  44. package/runtime/store.js +151 -0
  45. package/runtime/textinput.js +68 -0
  46. package/runtime/ui/buttons.js +124 -0
  47. package/runtime/ui/panels.js +105 -0
  48. package/runtime/ui/text.js +86 -0
  49. package/runtime/ui/widgets.js +141 -0
  50. package/runtime/ui.js +111 -0
  51. package/src/main.js +474 -0
  52. package/vite.config.js +63 -0
@@ -0,0 +1,753 @@
1
+ // runtime/api-effects.js
2
+ // Advanced effects, shading, and post-processing API for Nova64
3
+ import * as THREE from 'three';
4
+ import { logger } from './logger.js';
5
+ import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
6
+ import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
7
+ import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
8
+ import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
9
+ import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader.js';
10
+
11
+ // Chromatic Aberration shader
12
+ const ChromaticAberrationShader = {
13
+ uniforms: {
14
+ tDiffuse: { value: null },
15
+ amount: { value: 0.002 },
16
+ },
17
+ vertexShader: `
18
+ varying vec2 vUv;
19
+ void main() {
20
+ vUv = uv;
21
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
22
+ }
23
+ `,
24
+ fragmentShader: `
25
+ uniform sampler2D tDiffuse;
26
+ uniform float amount;
27
+ varying vec2 vUv;
28
+ void main() {
29
+ vec2 dir = vUv - 0.5;
30
+ float dist = length(dir);
31
+ vec2 offset = normalize(dir) * dist * amount;
32
+ float r = texture2D(tDiffuse, vUv + offset).r;
33
+ float g = texture2D(tDiffuse, vUv).g;
34
+ float b = texture2D(tDiffuse, vUv - offset).b;
35
+ gl_FragColor = vec4(r, g, b, 1.0);
36
+ }
37
+ `,
38
+ };
39
+
40
+ // Vignette shader
41
+ const VignetteShader = {
42
+ uniforms: {
43
+ tDiffuse: { value: null },
44
+ darkness: { value: 1.5 },
45
+ offset: { value: 0.95 },
46
+ },
47
+ vertexShader: `
48
+ varying vec2 vUv;
49
+ void main() {
50
+ vUv = uv;
51
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
52
+ }
53
+ `,
54
+ fragmentShader: `
55
+ uniform sampler2D tDiffuse;
56
+ uniform float darkness;
57
+ uniform float offset;
58
+ varying vec2 vUv;
59
+ void main() {
60
+ vec4 texel = texture2D(tDiffuse, vUv);
61
+ vec2 uv = (vUv - vec2(0.5)) * vec2(offset);
62
+ float vignette = 1.0 - dot(uv, uv) * darkness;
63
+ gl_FragColor = vec4(texel.rgb * clamp(vignette, 0.0, 1.0), texel.a);
64
+ }
65
+ `,
66
+ };
67
+
68
+ export function effectsApi(gpu) {
69
+ if (!gpu.getScene || !gpu.getCamera || !gpu.getRenderer) {
70
+ return { exposeTo: () => {} };
71
+ }
72
+
73
+ const scene = gpu.getScene();
74
+ const camera = gpu.getCamera();
75
+ const renderer = gpu.getRenderer();
76
+
77
+ // Post-processing composer
78
+ let composer = null;
79
+ let renderPass = null;
80
+ let bloomPass = null;
81
+ let fxaaPass = null;
82
+ let chromaticAberrationPass = null;
83
+ let vignettePass = null;
84
+
85
+ // Effect states
86
+ let effectsEnabled = false;
87
+
88
+ // Custom shader materials
89
+ const customShaders = new Map();
90
+
91
+ // Initialize post-processing
92
+ function initPostProcessing() {
93
+ if (composer) return; // Already initialized
94
+
95
+ composer = new EffectComposer(renderer);
96
+
97
+ // Base render pass
98
+ renderPass = new RenderPass(scene, camera);
99
+ composer.addPass(renderPass);
100
+
101
+ effectsEnabled = true;
102
+ }
103
+
104
+ // === BLOOM EFFECTS ===
105
+ function enableBloom(strength = 1.0, radius = 0.5, threshold = 0.85) {
106
+ initPostProcessing();
107
+
108
+ if (bloomPass) {
109
+ composer.removePass(bloomPass);
110
+ }
111
+
112
+ bloomPass = new UnrealBloomPass(
113
+ new THREE.Vector2(window.innerWidth, window.innerHeight),
114
+ strength,
115
+ radius,
116
+ threshold
117
+ );
118
+
119
+ composer.addPass(bloomPass);
120
+
121
+ return true;
122
+ }
123
+
124
+ function disableBloom() {
125
+ if (bloomPass && composer) {
126
+ composer.removePass(bloomPass);
127
+ bloomPass = null;
128
+ }
129
+ }
130
+
131
+ function setBloomStrength(strength) {
132
+ if (bloomPass) {
133
+ bloomPass.strength = strength;
134
+ }
135
+ }
136
+
137
+ function setBloomRadius(radius) {
138
+ if (bloomPass) {
139
+ bloomPass.radius = radius;
140
+ }
141
+ }
142
+
143
+ function setBloomThreshold(threshold) {
144
+ if (bloomPass) {
145
+ bloomPass.threshold = threshold;
146
+ }
147
+ }
148
+
149
+ // === ANTI-ALIASING ===
150
+ function enableFXAA() {
151
+ initPostProcessing();
152
+
153
+ if (fxaaPass) return;
154
+
155
+ fxaaPass = new ShaderPass(FXAAShader);
156
+ const pixelRatio = renderer.getPixelRatio();
157
+ fxaaPass.material.uniforms['resolution'].value.x = 1 / (window.innerWidth * pixelRatio);
158
+ fxaaPass.material.uniforms['resolution'].value.y = 1 / (window.innerHeight * pixelRatio);
159
+
160
+ composer.addPass(fxaaPass);
161
+ }
162
+
163
+ function disableFXAA() {
164
+ if (fxaaPass && composer) {
165
+ composer.removePass(fxaaPass);
166
+ fxaaPass = null;
167
+ }
168
+ }
169
+
170
+ // === CHROMATIC ABERRATION ===
171
+ function enableChromaticAberration(amount = 0.002) {
172
+ initPostProcessing();
173
+ if (chromaticAberrationPass) {
174
+ chromaticAberrationPass.uniforms['amount'].value = amount;
175
+ return;
176
+ }
177
+ chromaticAberrationPass = new ShaderPass(ChromaticAberrationShader);
178
+ chromaticAberrationPass.uniforms['amount'].value = amount;
179
+ composer.addPass(chromaticAberrationPass);
180
+ }
181
+
182
+ function disableChromaticAberration() {
183
+ if (chromaticAberrationPass && composer) {
184
+ composer.removePass(chromaticAberrationPass);
185
+ chromaticAberrationPass = null;
186
+ }
187
+ }
188
+
189
+ // === VIGNETTE ===
190
+ function enableVignette(darkness = 1.5, offset = 0.95) {
191
+ initPostProcessing();
192
+ if (vignettePass) {
193
+ vignettePass.uniforms['darkness'].value = darkness;
194
+ vignettePass.uniforms['offset'].value = offset;
195
+ return;
196
+ }
197
+ vignettePass = new ShaderPass(VignetteShader);
198
+ vignettePass.uniforms['darkness'].value = darkness;
199
+ vignettePass.uniforms['offset'].value = offset;
200
+ composer.addPass(vignettePass);
201
+ }
202
+
203
+ function disableVignette() {
204
+ if (vignettePass && composer) {
205
+ composer.removePass(vignettePass);
206
+ vignettePass = null;
207
+ }
208
+ }
209
+
210
+ // === CUSTOM SHADERS ===
211
+
212
+ // Holographic shader
213
+ const holographicShader = {
214
+ uniforms: {
215
+ time: { value: 0 },
216
+ color: { value: new THREE.Color(0x00ffff) },
217
+ scanlineSpeed: { value: 2.0 },
218
+ glitchAmount: { value: 0.1 },
219
+ opacity: { value: 0.8 },
220
+ },
221
+ vertexShader: `
222
+ varying vec2 vUv;
223
+ varying vec3 vPosition;
224
+
225
+ void main() {
226
+ vUv = uv;
227
+ vPosition = position;
228
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
229
+ }
230
+ `,
231
+ fragmentShader: `
232
+ uniform float time;
233
+ uniform vec3 color;
234
+ uniform float scanlineSpeed;
235
+ uniform float glitchAmount;
236
+ uniform float opacity;
237
+
238
+ varying vec2 vUv;
239
+ varying vec3 vPosition;
240
+
241
+ void main() {
242
+ // Scanlines
243
+ float scanline = sin(vUv.y * 100.0 + time * scanlineSpeed) * 0.5 + 0.5;
244
+
245
+ // Glitch effect
246
+ float glitch = step(0.95, sin(time * 10.0 + vUv.y * 50.0)) * glitchAmount;
247
+
248
+ // Edge glow
249
+ float edge = 1.0 - abs(vUv.x - 0.5) * 2.0;
250
+ edge = pow(edge, 3.0);
251
+
252
+ // Combine effects
253
+ vec3 finalColor = color * (scanline * 0.5 + 0.5) + vec3(edge * 0.3);
254
+ finalColor += vec3(glitch);
255
+
256
+ gl_FragColor = vec4(finalColor, opacity);
257
+ }
258
+ `,
259
+ };
260
+
261
+ // Energy shield shader
262
+ const shieldShader = {
263
+ uniforms: {
264
+ time: { value: 0 },
265
+ hitPosition: { value: new THREE.Vector3(0, 0, 0) },
266
+ hitStrength: { value: 0 },
267
+ color: { value: new THREE.Color(0x00ffff) },
268
+ opacity: { value: 0.6 },
269
+ },
270
+ vertexShader: `
271
+ varying vec2 vUv;
272
+ varying vec3 vNormal;
273
+ varying vec3 vPosition;
274
+
275
+ void main() {
276
+ vUv = uv;
277
+ vNormal = normalize(normalMatrix * normal);
278
+ vPosition = (modelMatrix * vec4(position, 1.0)).xyz;
279
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
280
+ }
281
+ `,
282
+ fragmentShader: `
283
+ uniform float time;
284
+ uniform vec3 hitPosition;
285
+ uniform float hitStrength;
286
+ uniform vec3 color;
287
+ uniform float opacity;
288
+
289
+ varying vec2 vUv;
290
+ varying vec3 vNormal;
291
+ varying vec3 vPosition;
292
+
293
+ void main() {
294
+ // Fresnel effect
295
+ vec3 viewDirection = normalize(cameraPosition - vPosition);
296
+ float fresnel = pow(1.0 - dot(viewDirection, vNormal), 3.0);
297
+
298
+ // Hexagon pattern
299
+ vec2 hexUv = vUv * 20.0;
300
+ float hexPattern = abs(sin(hexUv.x * 3.14159) * sin(hexUv.y * 3.14159));
301
+ hexPattern = step(0.5, hexPattern);
302
+
303
+ // Hit ripple
304
+ float distToHit = length(vPosition - hitPosition);
305
+ float ripple = sin(distToHit * 5.0 - time * 10.0) * 0.5 + 0.5;
306
+ ripple *= smoothstep(2.0, 0.0, distToHit) * hitStrength;
307
+
308
+ // Pulsing energy
309
+ float pulse = sin(time * 2.0) * 0.2 + 0.8;
310
+
311
+ // Combine effects
312
+ vec3 finalColor = color * (fresnel + hexPattern * 0.3 + ripple) * pulse;
313
+ float finalOpacity = opacity * (fresnel * 0.5 + 0.5 + ripple);
314
+
315
+ gl_FragColor = vec4(finalColor, finalOpacity);
316
+ }
317
+ `,
318
+ };
319
+
320
+ // Water/liquid shader
321
+ const waterShader = {
322
+ uniforms: {
323
+ time: { value: 0 },
324
+ color: { value: new THREE.Color(0x0088ff) },
325
+ waveSpeed: { value: 1.0 },
326
+ waveHeight: { value: 0.5 },
327
+ transparency: { value: 0.7 },
328
+ },
329
+ vertexShader: `
330
+ uniform float time;
331
+ uniform float waveSpeed;
332
+ uniform float waveHeight;
333
+
334
+ varying vec2 vUv;
335
+ varying vec3 vNormal;
336
+ varying float vElevation;
337
+
338
+ void main() {
339
+ vUv = uv;
340
+ vNormal = normal;
341
+
342
+ // Wave displacement
343
+ vec3 pos = position;
344
+ float wave1 = sin(pos.x * 2.0 + time * waveSpeed) * waveHeight;
345
+ float wave2 = sin(pos.z * 3.0 + time * waveSpeed * 1.5) * waveHeight * 0.5;
346
+ pos.y += wave1 + wave2;
347
+ vElevation = wave1 + wave2;
348
+
349
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
350
+ }
351
+ `,
352
+ fragmentShader: `
353
+ uniform float time;
354
+ uniform vec3 color;
355
+ uniform float transparency;
356
+
357
+ varying vec2 vUv;
358
+ varying vec3 vNormal;
359
+ varying float vElevation;
360
+
361
+ void main() {
362
+ // Foam on wave peaks
363
+ float foam = smoothstep(0.3, 0.5, vElevation);
364
+
365
+ // Caustics pattern
366
+ vec2 causticsUv = vUv * 10.0 + time * 0.1;
367
+ float caustics = abs(sin(causticsUv.x * 3.14159) * sin(causticsUv.y * 3.14159));
368
+
369
+ // Depth fade
370
+ float depth = vUv.y;
371
+ vec3 deepColor = color * 0.5;
372
+ vec3 shallowColor = color * 1.5;
373
+ vec3 finalColor = mix(deepColor, shallowColor, depth);
374
+
375
+ // Add foam and caustics
376
+ finalColor += vec3(foam * 0.5);
377
+ finalColor += vec3(caustics * 0.2);
378
+
379
+ gl_FragColor = vec4(finalColor, transparency);
380
+ }
381
+ `,
382
+ };
383
+
384
+ // Fire/plasma shader
385
+ const fireShader = {
386
+ uniforms: {
387
+ time: { value: 0 },
388
+ color1: { value: new THREE.Color(0xff4400) },
389
+ color2: { value: new THREE.Color(0xffff00) },
390
+ intensity: { value: 1.0 },
391
+ speed: { value: 2.0 },
392
+ },
393
+ vertexShader: `
394
+ varying vec2 vUv;
395
+ varying vec3 vPosition;
396
+
397
+ void main() {
398
+ vUv = uv;
399
+ vPosition = position;
400
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
401
+ }
402
+ `,
403
+ fragmentShader: `
404
+ uniform float time;
405
+ uniform vec3 color1;
406
+ uniform vec3 color2;
407
+ uniform float intensity;
408
+ uniform float speed;
409
+
410
+ varying vec2 vUv;
411
+ varying vec3 vPosition;
412
+
413
+ // Noise function
414
+ float random(vec2 st) {
415
+ return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
416
+ }
417
+
418
+ float noise(vec2 st) {
419
+ vec2 i = floor(st);
420
+ vec2 f = fract(st);
421
+ float a = random(i);
422
+ float b = random(i + vec2(1.0, 0.0));
423
+ float c = random(i + vec2(0.0, 1.0));
424
+ float d = random(i + vec2(1.0, 1.0));
425
+ vec2 u = f * f * (3.0 - 2.0 * f);
426
+ return mix(a, b, u.x) + (c - a)* u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
427
+ }
428
+
429
+ void main() {
430
+ vec2 uv = vUv;
431
+
432
+ // Multiple layers of noise for fire effect
433
+ float n1 = noise(uv * 5.0 + time * speed);
434
+ float n2 = noise(uv * 10.0 + time * speed * 1.5);
435
+ float n3 = noise(uv * 20.0 + time * speed * 2.0);
436
+
437
+ // Combine noise layers
438
+ float firePattern = n1 * 0.5 + n2 * 0.3 + n3 * 0.2;
439
+
440
+ // Vertical gradient (fire rises)
441
+ float gradient = 1.0 - uv.y;
442
+ firePattern *= gradient;
443
+
444
+ // Color mixing
445
+ vec3 fireColor = mix(color1, color2, firePattern);
446
+
447
+ // Intensity and flickering
448
+ float flicker = sin(time * 10.0) * 0.1 + 0.9;
449
+ fireColor *= intensity * flicker;
450
+
451
+ // Alpha based on pattern
452
+ float alpha = firePattern * intensity;
453
+
454
+ gl_FragColor = vec4(fireColor, alpha);
455
+ }
456
+ `,
457
+ };
458
+
459
+ // Create material with custom shader
460
+ function createShaderMaterial(shaderName, customUniforms = {}) {
461
+ let shader;
462
+
463
+ switch (shaderName) {
464
+ case 'holographic':
465
+ shader = holographicShader;
466
+ break;
467
+ case 'shield':
468
+ shader = shieldShader;
469
+ break;
470
+ case 'water':
471
+ shader = waterShader;
472
+ break;
473
+ case 'fire':
474
+ shader = fireShader;
475
+ break;
476
+ default:
477
+ logger.warn(`Unknown shader: ${shaderName}`);
478
+ return null;
479
+ }
480
+
481
+ // Merge custom uniforms with shader uniforms
482
+ const uniforms = { ...shader.uniforms };
483
+ for (const key in customUniforms) {
484
+ if (uniforms[key]) {
485
+ uniforms[key].value = customUniforms[key];
486
+ }
487
+ }
488
+
489
+ const material = new THREE.ShaderMaterial({
490
+ uniforms: uniforms,
491
+ vertexShader: shader.vertexShader,
492
+ fragmentShader: shader.fragmentShader,
493
+ transparent: true,
494
+ side: THREE.DoubleSide,
495
+ });
496
+
497
+ const id = `shader_${Date.now()}_${Math.random()}`;
498
+ customShaders.set(id, material);
499
+
500
+ return { id, material };
501
+ }
502
+
503
+ // Update shader uniforms
504
+ function updateShaderUniform(shaderId, uniformName, value) {
505
+ const material = customShaders.get(shaderId);
506
+ if (material && material.uniforms[uniformName]) {
507
+ material.uniforms[uniformName].value = value;
508
+ return true;
509
+ }
510
+ return false;
511
+ }
512
+
513
+ // Update all shader time uniforms
514
+ function updateShaderTime(deltaTime) {
515
+ customShaders.forEach(material => {
516
+ if (material.uniforms.time) {
517
+ material.uniforms.time.value += deltaTime;
518
+ }
519
+ });
520
+ }
521
+
522
+ // === PARTICLE EFFECTS ===
523
+ function createParticleSystem(count, options = {}) {
524
+ const {
525
+ color = 0xffffff,
526
+ size = 0.1,
527
+ speed = 1.0,
528
+ lifetime = 2.0,
529
+ spread = 1.0,
530
+ gravity = -1.0,
531
+ } = options;
532
+
533
+ const geometry = new THREE.BufferGeometry();
534
+ const positions = new Float32Array(count * 3);
535
+ const velocities = new Float32Array(count * 3);
536
+ const lifetimes = new Float32Array(count);
537
+
538
+ for (let i = 0; i < count; i++) {
539
+ const i3 = i * 3;
540
+
541
+ // Random positions in spread area
542
+ positions[i3] = (Math.random() - 0.5) * spread;
543
+ positions[i3 + 1] = (Math.random() - 0.5) * spread;
544
+ positions[i3 + 2] = (Math.random() - 0.5) * spread;
545
+
546
+ // Random velocities
547
+ velocities[i3] = (Math.random() - 0.5) * speed;
548
+ velocities[i3 + 1] = Math.random() * speed;
549
+ velocities[i3 + 2] = (Math.random() - 0.5) * speed;
550
+
551
+ // Random lifetimes
552
+ lifetimes[i] = Math.random() * lifetime;
553
+ }
554
+
555
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
556
+ geometry.setAttribute('velocity', new THREE.BufferAttribute(velocities, 3));
557
+ geometry.setAttribute('lifetime', new THREE.BufferAttribute(lifetimes, 1));
558
+
559
+ const material = new THREE.PointsMaterial({
560
+ color: color,
561
+ size: size,
562
+ transparent: true,
563
+ opacity: 0.8,
564
+ blending: THREE.AdditiveBlending,
565
+ });
566
+
567
+ const particles = new THREE.Points(geometry, material);
568
+ particles.userData.velocities = velocities;
569
+ particles.userData.lifetimes = lifetimes;
570
+ particles.userData.maxLifetime = lifetime;
571
+ particles.userData.gravity = gravity;
572
+ particles.userData.speed = speed;
573
+ particles.userData.spread = spread;
574
+
575
+ scene.add(particles);
576
+
577
+ return particles;
578
+ }
579
+
580
+ // Update particle system
581
+ function updateParticles(particleSystem, deltaTime) {
582
+ if (!particleSystem || !particleSystem.geometry) return;
583
+
584
+ const positions = particleSystem.geometry.attributes.position.array;
585
+ const velocities = particleSystem.userData.velocities;
586
+ const lifetimes = particleSystem.userData.lifetimes;
587
+ const maxLifetime = particleSystem.userData.maxLifetime;
588
+ const gravity = particleSystem.userData.gravity;
589
+ const speed = particleSystem.userData.speed;
590
+ const spread = particleSystem.userData.spread;
591
+
592
+ for (let i = 0; i < positions.length; i += 3) {
593
+ const idx = i / 3;
594
+
595
+ // Update lifetime
596
+ lifetimes[idx] -= deltaTime;
597
+
598
+ if (lifetimes[idx] <= 0) {
599
+ // Reset particle
600
+ positions[i] = (Math.random() - 0.5) * spread;
601
+ positions[i + 1] = 0;
602
+ positions[i + 2] = (Math.random() - 0.5) * spread;
603
+
604
+ velocities[i] = (Math.random() - 0.5) * speed;
605
+ velocities[i + 1] = Math.random() * speed;
606
+ velocities[i + 2] = (Math.random() - 0.5) * speed;
607
+
608
+ lifetimes[idx] = maxLifetime;
609
+ } else {
610
+ // Update position
611
+ positions[i] += velocities[i] * deltaTime;
612
+ positions[i + 1] += velocities[i + 1] * deltaTime;
613
+ positions[i + 2] += velocities[i + 2] * deltaTime;
614
+
615
+ // Apply gravity
616
+ velocities[i + 1] += gravity * deltaTime;
617
+ }
618
+ }
619
+
620
+ particleSystem.geometry.attributes.position.needsUpdate = true;
621
+ }
622
+
623
+ // === CONVENIENCE: enable a full retro N64/PS1 post-processing stack in one call ===
624
+ /**
625
+ * enableRetroEffects(opts)
626
+ * One-call setup for bloom + FXAA + vignette + optional chromatic aberration.
627
+ * opts: {
628
+ * bloom: { strength, radius, threshold } | false to skip (default: {1.5, 0.4, 0.1})
629
+ * fxaa: true | false (default: true)
630
+ * vignette: { darkness, offset } | false to skip (default: {1.3, 0.9})
631
+ * chromatic: number (amount) | false to skip (default: false)
632
+ * pixelation: number (factor) | false to skip (default: 1)
633
+ * dithering: boolean (default: true)
634
+ * }
635
+ * Call with no args for sensible defaults that match Star Fox / Crystal Cathedral look.
636
+ */
637
+ function enableRetroEffects(opts = {}) {
638
+ // Pixelation — delegated to gpu (api-3d)
639
+ const pixelFactor = opts.pixelation !== undefined ? opts.pixelation : 1;
640
+ if (pixelFactor !== false && typeof globalThis.enablePixelation === 'function') {
641
+ globalThis.enablePixelation(pixelFactor);
642
+ }
643
+
644
+ // Dithering — delegated to gpu (api-3d)
645
+ const dither = opts.dithering !== undefined ? opts.dithering : true;
646
+ if (dither !== false && typeof globalThis.enableDithering === 'function') {
647
+ globalThis.enableDithering(dither);
648
+ }
649
+
650
+ // Bloom
651
+ const bloom = opts.bloom !== undefined ? opts.bloom : {};
652
+ if (bloom !== false) {
653
+ const b = typeof bloom === 'object' ? bloom : {};
654
+ enableBloom(b.strength ?? 1.5, b.radius ?? 0.4, b.threshold ?? 0.1);
655
+ }
656
+
657
+ // FXAA
658
+ const fxaa = opts.fxaa !== undefined ? opts.fxaa : true;
659
+ if (fxaa !== false) {
660
+ enableFXAA();
661
+ }
662
+
663
+ // Vignette
664
+ const vig = opts.vignette !== undefined ? opts.vignette : {};
665
+ if (vig !== false) {
666
+ const v = typeof vig === 'object' ? vig : {};
667
+ enableVignette(v.darkness ?? 1.3, v.offset ?? 0.9);
668
+ }
669
+
670
+ // Chromatic aberration (off by default — slight colour fringing)
671
+ const chrom = opts.chromatic !== undefined ? opts.chromatic : false;
672
+ if (chrom !== false) {
673
+ enableChromaticAberration(typeof chrom === 'number' ? chrom : 0.002);
674
+ }
675
+
676
+ return true;
677
+ }
678
+
679
+ /**
680
+ * disableRetroEffects()
681
+ * Tear down everything enableRetroEffects() set up.
682
+ */
683
+ function disableRetroEffects() {
684
+ disableBloom();
685
+ disableFXAA();
686
+ disableVignette();
687
+ disableChromaticAberration();
688
+ if (typeof globalThis.enablePixelation === 'function') globalThis.enablePixelation(0);
689
+ if (typeof globalThis.enableDithering === 'function') globalThis.enableDithering(false);
690
+ }
691
+
692
+ // === RENDERING ===
693
+ function renderEffects() {
694
+ if (effectsEnabled && composer) {
695
+ composer.render();
696
+ } else {
697
+ renderer.render(scene, camera);
698
+ }
699
+ }
700
+
701
+ // Update effects (called every frame)
702
+ function updateEffects(deltaTime) {
703
+ updateShaderTime(deltaTime);
704
+ }
705
+
706
+ // === EXPOSE API ===
707
+ return {
708
+ exposeTo(target) {
709
+ Object.assign(target, {
710
+ // Post-processing
711
+ enableBloom: enableBloom,
712
+ disableBloom: disableBloom,
713
+ setBloomStrength: setBloomStrength,
714
+ setBloomRadius: setBloomRadius,
715
+ setBloomThreshold: setBloomThreshold,
716
+ enableFXAA: enableFXAA,
717
+ disableFXAA: disableFXAA,
718
+ enableChromaticAberration: enableChromaticAberration,
719
+ disableChromaticAberration: disableChromaticAberration,
720
+ enableVignette: enableVignette,
721
+ disableVignette: disableVignette,
722
+
723
+ // Convenience
724
+ enableRetroEffects: enableRetroEffects,
725
+ disableRetroEffects: disableRetroEffects,
726
+
727
+ // Custom shaders
728
+ createShaderMaterial: createShaderMaterial,
729
+ updateShaderUniform: updateShaderUniform,
730
+
731
+ // Particles
732
+ createParticleSystem: createParticleSystem,
733
+ updateParticles: updateParticles,
734
+
735
+ // Utility
736
+ isEffectsEnabled: () => effectsEnabled,
737
+
738
+ // Called by gpu-threejs.js endFrame() to apply the effect composer
739
+ renderEffects: renderEffects,
740
+ });
741
+ },
742
+
743
+ // Internal update called by main loop
744
+ update(deltaTime) {
745
+ updateEffects(deltaTime);
746
+ },
747
+
748
+ // Internal render called by main loop
749
+ render() {
750
+ renderEffects();
751
+ },
752
+ };
753
+ }