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.
- package/dist/examples/strider-demo-3d/fix-game.sh +0 -0
- package/dist/os9-shell/assets/index-3Hr_q5dj.js +483 -0
- package/dist/os9-shell/assets/index-3Hr_q5dj.js.map +1 -0
- package/dist/os9-shell/index.html +2 -1
- package/dist/runtime/api-2d.js +1158 -0
- package/dist/runtime/api-3d/camera.js +73 -0
- package/dist/runtime/api-3d/instancing.js +180 -0
- package/dist/runtime/api-3d/lights.js +51 -0
- package/dist/runtime/api-3d/materials.js +47 -0
- package/dist/runtime/api-3d/models.js +84 -0
- package/dist/runtime/api-3d/particles.js +296 -0
- package/dist/runtime/api-3d/pbr.js +113 -0
- package/dist/runtime/api-3d/primitives.js +304 -0
- package/dist/runtime/api-3d/scene.js +169 -0
- package/dist/runtime/api-3d/transforms.js +161 -0
- package/dist/runtime/api-3d.js +166 -0
- package/dist/runtime/api-effects.js +840 -0
- package/dist/runtime/api-gameutils.js +476 -0
- package/dist/runtime/api-generative.js +610 -0
- package/dist/runtime/api-presets.js +85 -0
- package/dist/runtime/api-skybox.js +232 -0
- package/dist/runtime/api-sprites.js +100 -0
- package/dist/runtime/api-voxel.js +712 -0
- package/dist/runtime/api.js +201 -0
- package/dist/runtime/assets.js +27 -0
- package/dist/runtime/audio.js +114 -0
- package/dist/runtime/collision.js +47 -0
- package/dist/runtime/console.js +101 -0
- package/dist/runtime/editor.js +233 -0
- package/dist/runtime/font.js +233 -0
- package/dist/runtime/framebuffer.js +28 -0
- package/dist/runtime/fullscreen-button.js +185 -0
- package/dist/runtime/gpu-canvas2d.js +47 -0
- package/dist/runtime/gpu-threejs.js +643 -0
- package/dist/runtime/gpu-webgl2.js +310 -0
- package/dist/runtime/index.d.ts +682 -0
- package/dist/runtime/index.js +22 -0
- package/dist/runtime/input.js +225 -0
- package/dist/runtime/logger.js +60 -0
- package/dist/runtime/physics.js +101 -0
- package/dist/runtime/screens.js +213 -0
- package/dist/runtime/storage.js +38 -0
- package/dist/runtime/store.js +151 -0
- package/dist/runtime/textinput.js +68 -0
- package/dist/runtime/ui/buttons.js +124 -0
- package/dist/runtime/ui/panels.js +105 -0
- package/dist/runtime/ui/text.js +86 -0
- package/dist/runtime/ui/widgets.js +141 -0
- package/dist/runtime/ui.js +111 -0
- package/package.json +34 -32
- package/public/os9-shell/assets/index-3Hr_q5dj.js +483 -0
- package/public/os9-shell/assets/index-3Hr_q5dj.js.map +1 -0
- 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
|
+
}
|