nova64 0.2.6 → 0.2.8

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