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,47 @@
1
+ // runtime/gpu-canvas2d.js
2
+ import { Framebuffer64 } from './framebuffer.js';
3
+
4
+ export class GpuCanvas2D {
5
+ constructor(canvas, w, h) {
6
+ this.canvas = canvas;
7
+ this.ctx = canvas.getContext('2d', { willReadFrequently: false });
8
+ this.fb = new Framebuffer64(w, h);
9
+ this.imageData = this.ctx.createImageData(w, h);
10
+ this.tmp8 = this.imageData.data;
11
+ this.w = w;
12
+ this.h = h;
13
+ }
14
+
15
+ beginFrame() {
16
+ // no-op
17
+ }
18
+
19
+ endFrame() {
20
+ // Down-convert RGBA64 (16-bit per channel) to RGBA8 with simple ordered dithering
21
+ const { w, h } = { w: this.w, h: this.h };
22
+ const p = this.fb.pixels;
23
+ const dither = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5]; // 4x4 Bayer normalized by /16
24
+ let k = 0;
25
+ for (let y = 0; y < h; y++) {
26
+ for (let x = 0; x < w; x++) {
27
+ const i = (y * w + x) * 4;
28
+ const t = dither[(y & 3) * 4 + (x & 3)] / 16;
29
+ // 16-bit -> 8-bit (divide by 257), add tiny dither
30
+ const r8 = Math.max(0, Math.min(255, p[i] / 257 + t)) | 0;
31
+ const g8 = Math.max(0, Math.min(255, p[i + 1] / 257 + t)) | 0;
32
+ const b8 = Math.max(0, Math.min(255, p[i + 2] / 257 + t)) | 0;
33
+ const a8 = Math.max(0, Math.min(255, p[i + 3] / 257)) | 0;
34
+ this.tmp8[k++] = r8;
35
+ this.tmp8[k++] = g8;
36
+ this.tmp8[k++] = b8;
37
+ this.tmp8[k++] = a8;
38
+ }
39
+ }
40
+ this.ctx.putImageData(this.imageData, 0, 0);
41
+ }
42
+
43
+ // API surface used by stdApi to draw into the FB
44
+ getFramebuffer() {
45
+ return this.fb;
46
+ }
47
+ }
@@ -0,0 +1,639 @@
1
+ // runtime/gpu-threejs.js
2
+ // Three.js backend for 3D rendering with N64-style effects and 2D overlay support
3
+ import * as THREE from 'three';
4
+ import { PMREMGenerator } from 'three';
5
+ import { Framebuffer64 } from './framebuffer.js';
6
+
7
+ export class GpuThreeJS {
8
+ constructor(canvas, w, h) {
9
+ this.canvas = canvas;
10
+ this.w = w;
11
+ this.h = h;
12
+
13
+ // Initialize Three.js renderer with maximum quality settings
14
+ this.renderer = new THREE.WebGLRenderer({
15
+ canvas,
16
+ antialias: true, // Enable for smoother graphics
17
+ alpha: false,
18
+ premultipliedAlpha: false,
19
+ powerPreference: 'high-performance',
20
+ precision: 'highp',
21
+ stencil: true,
22
+ preserveDrawingBuffer: false,
23
+ failIfMajorPerformanceCaveat: false,
24
+ });
25
+
26
+ this.renderer.setSize(canvas.width, canvas.height);
27
+ this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // Enhanced pixel density
28
+ this.renderer.outputColorSpace = THREE.SRGBColorSpace;
29
+
30
+ // Dramatically enhanced visual rendering setup
31
+ this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
32
+ this.renderer.toneMappingExposure = 1.6;
33
+ this.renderer.shadowMap.enabled = true;
34
+ // Note: PCFSoftShadowMap is deprecated in r182, PCFShadowMap now provides soft shadows
35
+ this.renderer.shadowMap.type = THREE.PCFShadowMap;
36
+ this.renderer.shadowMap.autoUpdate = true;
37
+
38
+ // Enable advanced rendering features (using modern Three.js approach)
39
+ // Note: physicallyCorrectLights and useLegacyLights are deprecated in latest Three.js
40
+
41
+ // Enable additional WebGL capabilities
42
+ this.renderer.sortObjects = true;
43
+ this.renderer.setClearColor(0x0a0a0f, 1.0);
44
+
45
+ const gl = this.renderer.getContext();
46
+ gl.enable(gl.DEPTH_TEST);
47
+ gl.enable(gl.CULL_FACE);
48
+ gl.cullFace(gl.BACK);
49
+
50
+ // Create main scene and camera
51
+ this.scene = new THREE.Scene();
52
+ this.camera = new THREE.PerspectiveCamera(75, canvas.width / canvas.height, 0.1, 1000);
53
+ this.camera.position.z = 5;
54
+
55
+ // N64-style lighting setup
56
+ this.setupN64Lighting();
57
+
58
+ // 2D overlay system - maintain compatibility with existing 2D API
59
+ this.fb = new Framebuffer64(w, h);
60
+ this.overlay2D = this.create2DOverlay(w, h);
61
+
62
+ // Sprite batching for 2D elements
63
+ this.spriteBatches = new Map();
64
+ this.texCache = new WeakMap();
65
+
66
+ // Registered animated mesh list — avoids scene.traverse() every frame
67
+ this.animatedMeshes = [];
68
+
69
+ // Frustum for per-frame culling of animated material updates
70
+ this.frustum = new THREE.Frustum();
71
+ this._projScreenMatrix = new THREE.Matrix4();
72
+
73
+ // Camera controls and state
74
+ this.cameraTarget = new THREE.Vector3(0, 0, 0);
75
+ this.cameraOffset = new THREE.Vector3(0, 0, 5);
76
+
77
+ // Performance tracking
78
+ this.stats = {
79
+ triangles: 0,
80
+ drawCalls: 0,
81
+ geometries: 0,
82
+ };
83
+ }
84
+
85
+ setupN64Lighting() {
86
+ // Multi-layered ambient lighting for rich atmosphere
87
+ const ambientLight = new THREE.AmbientLight(0x404060, 0.3);
88
+ this.scene.add(ambientLight);
89
+
90
+ // Hemisphere light for more natural lighting
91
+ const hemisphereLight = new THREE.HemisphereLight(0xffffbb, 0x080820, 0.4);
92
+ this.scene.add(hemisphereLight);
93
+
94
+ // Main directional light with ultra-high quality shadows
95
+ this.mainLight = new THREE.DirectionalLight(0xffffff, 1.8);
96
+ this.mainLight.position.set(5, 8, 3);
97
+ this.mainLight.castShadow = true;
98
+ this.mainLight.shadow.mapSize.width = 4096;
99
+ this.mainLight.shadow.mapSize.height = 4096;
100
+ this.mainLight.shadow.camera.near = 0.1;
101
+ this.mainLight.shadow.camera.far = 200;
102
+ this.mainLight.shadow.camera.left = -100;
103
+ this.mainLight.shadow.camera.right = 100;
104
+ this.mainLight.shadow.camera.top = 100;
105
+ this.mainLight.shadow.camera.bottom = -100;
106
+ this.mainLight.shadow.bias = -0.00005;
107
+ this.mainLight.shadow.normalBias = 0.02;
108
+ this.scene.add(this.mainLight);
109
+
110
+ // Dramatic colored fill lights for cinematic atmosphere
111
+ const fillLight1 = new THREE.DirectionalLight(0x4080ff, 0.8);
112
+ fillLight1.position.set(-8, 4, -5);
113
+ fillLight1.castShadow = false;
114
+ this.scene.add(fillLight1);
115
+
116
+ const fillLight2 = new THREE.DirectionalLight(0xff4080, 0.6);
117
+ fillLight2.position.set(5, -3, 8);
118
+ fillLight2.castShadow = false;
119
+ this.scene.add(fillLight2);
120
+
121
+ const fillLight3 = new THREE.DirectionalLight(0x80ff40, 0.4);
122
+ fillLight3.position.set(-3, 6, -2);
123
+ fillLight3.castShadow = false;
124
+ this.scene.add(fillLight3);
125
+
126
+ // Point lights for localized dramatic effects
127
+ const pointLight1 = new THREE.PointLight(0xffaa00, 2, 30);
128
+ pointLight1.position.set(10, 15, 10);
129
+ pointLight1.castShadow = true;
130
+ pointLight1.shadow.mapSize.width = 1024;
131
+ pointLight1.shadow.mapSize.height = 1024;
132
+ this.scene.add(pointLight1);
133
+
134
+ const pointLight2 = new THREE.PointLight(0x00aaff, 1.5, 25);
135
+ pointLight2.position.set(-10, 10, -10);
136
+ pointLight2.castShadow = true;
137
+ pointLight2.shadow.mapSize.width = 1024;
138
+ pointLight2.shadow.mapSize.height = 1024;
139
+ this.scene.add(pointLight2);
140
+
141
+ // Generate a procedural environment map so metallic/holographic surfaces
142
+ // actually show specular reflections (envMapIntensity was previously inert).
143
+ // We use a simple gradient sky as the env source.
144
+ try {
145
+ const pmremGenerator = new PMREMGenerator(this.renderer);
146
+ pmremGenerator.compileEquirectangularShader();
147
+ const skyColor = new THREE.Color(0x1a2040);
148
+ const envScene = new THREE.Scene();
149
+ envScene.background = skyColor;
150
+ this.scene.environment = pmremGenerator.fromScene(envScene).texture;
151
+ pmremGenerator.dispose();
152
+ } catch (_) {
153
+ // Envmap setup is non-critical; silently skip on unsupported renderers
154
+ }
155
+
156
+ // Dramatic volumetric fog with color gradients
157
+ this.scene.fog = new THREE.FogExp2(0x202050, 0.008);
158
+
159
+ // Store lights for dynamic control
160
+ this.lights = {
161
+ main: this.mainLight,
162
+ fill1: fillLight1,
163
+ fill2: fillLight2,
164
+ fill3: fillLight3,
165
+ point1: pointLight1,
166
+ point2: pointLight2,
167
+ ambient: ambientLight,
168
+ hemisphere: hemisphereLight,
169
+ };
170
+ }
171
+
172
+ create2DOverlay(w, h) {
173
+ // Create orthographic camera for 2D overlay
174
+ const overlay2DCamera = new THREE.OrthographicCamera(0, w, h, 0, -1, 1);
175
+ const overlay2DScene = new THREE.Scene();
176
+
177
+ // Create texture from framebuffer for 2D overlay
178
+ // Keep a persistent Uint8Array - modify in-place rather than replacing ref
179
+ const overlayPixels = new Uint8Array(w * h * 4);
180
+ const overlayTexture = new THREE.DataTexture(overlayPixels, w, h, THREE.RGBAFormat);
181
+ overlayTexture.needsUpdate = true;
182
+ // flipY=false means data row 0 = bottom of screen; we account for this in update
183
+ overlayTexture.flipY = false;
184
+
185
+ // Create plane for 2D overlay
186
+ const overlayGeometry = new THREE.PlaneGeometry(w, h);
187
+ const overlayMaterial = new THREE.MeshBasicMaterial({
188
+ map: overlayTexture,
189
+ transparent: true,
190
+ depthTest: false,
191
+ depthWrite: false,
192
+ blending: THREE.NormalBlending,
193
+ });
194
+ const overlayMesh = new THREE.Mesh(overlayGeometry, overlayMaterial);
195
+ overlayMesh.position.set(w / 2, h / 2, 0);
196
+ overlay2DScene.add(overlayMesh);
197
+
198
+ return {
199
+ camera: overlay2DCamera,
200
+ scene: overlay2DScene,
201
+ texture: overlayTexture,
202
+ pixels: overlayPixels,
203
+ };
204
+ }
205
+
206
+ beginFrame() {
207
+ // Clear sprite batches
208
+ this.spriteBatches.clear();
209
+
210
+ // Clear 2D framebuffer
211
+ this.fb.fill(0, 0, 0, 0);
212
+ }
213
+
214
+ endFrame() {
215
+ // Update animations
216
+ this.update(0.016);
217
+
218
+ // Update LOD levels based on current camera position
219
+ if (typeof globalThis.updateLODs === 'function') {
220
+ globalThis.updateLODs();
221
+ }
222
+
223
+ // Render 3D scene first - check if post-processing effects are enabled
224
+ if (typeof globalThis.isEffectsEnabled === 'function' && globalThis.isEffectsEnabled()) {
225
+ // Use post-processing composer
226
+ if (typeof globalThis.renderEffects === 'function') {
227
+ globalThis.renderEffects();
228
+ } else {
229
+ this.renderer.render(this.scene, this.camera);
230
+ }
231
+ } else {
232
+ // Standard rendering
233
+ this.renderer.render(this.scene, this.camera);
234
+ }
235
+
236
+ // RENDER 2D HUD OVERLAY!
237
+ this.update2DOverlay();
238
+ }
239
+
240
+ update2DOverlay() {
241
+ // Update 2D overlay texture from framebuffer
242
+ // Framebuffer is Uint16Array with R,G,B,A as separate 16-bit values
243
+ const fb = this.fb.pixels;
244
+ const W = this.fb.width;
245
+ const H = this.fb.height;
246
+ // Modify the persistent pixel buffer in-place (more reliable than replacing ref)
247
+ const textureData = this.overlay2D.pixels;
248
+
249
+ // fb row 0 = top of screen; WebGL textures have row 0 at bottom (flipY=false).
250
+ // Flip Y only: fb row y → texture row (H-1-y) so the image appears right-side-up.
251
+ // No X flip: fb col x → texture col x → UV u=x/W → screen position x (left→right).
252
+ for (let y = 0; y < H; y++) {
253
+ const srcRow = y * W * 4; // framebuffer row (y=0 = top of screen)
254
+ const dstRow = (H - 1 - y) * W * 4; // texture row (row 0 = GL bottom = UV v=0)
255
+ for (let x = 0; x < W; x++) {
256
+ const src = srcRow + x * 4;
257
+ const dst = dstRow + x * 4; // same column — no X flip
258
+ textureData[dst] = fb[src] / 257; // R
259
+ textureData[dst + 1] = fb[src + 1] / 257; // G
260
+ textureData[dst + 2] = fb[src + 2] / 257; // B
261
+ textureData[dst + 3] = fb[src + 3] / 257; // A
262
+ }
263
+ }
264
+
265
+ // Mark texture for GPU upload on this frame
266
+ this.overlay2D.texture.needsUpdate = true;
267
+
268
+ // CRITICAL: Reset render target to screen (null) before overlay render.
269
+ // EffectComposer can leave the renderer pointing at an internal buffer.
270
+ this.renderer.setRenderTarget(null);
271
+ this.renderer.autoClear = false;
272
+ this.renderer.render(this.overlay2D.scene, this.overlay2D.camera);
273
+ this.renderer.autoClear = true;
274
+ }
275
+
276
+ updateCameraPosition() {
277
+ // Update camera based on target and offset
278
+ this.camera.position.copy(this.cameraOffset).add(this.cameraTarget);
279
+ this.camera.lookAt(this.cameraTarget);
280
+ }
281
+
282
+ // Scene accessors
283
+ getScene() {
284
+ return this.scene;
285
+ }
286
+ getCamera() {
287
+ return this.camera;
288
+ }
289
+ getRenderer() {
290
+ return this.renderer;
291
+ }
292
+
293
+ setCameraPosition(x, y, z) {
294
+ this.camera.position.set(x, y, z);
295
+ }
296
+
297
+ setCameraTarget(x, y, z) {
298
+ this.cameraTarget.set(x, y, z);
299
+ this.camera.lookAt(this.cameraTarget);
300
+ }
301
+
302
+ setCameraFOV(fov) {
303
+ this.camera.fov = fov;
304
+ this.camera.updateProjectionMatrix();
305
+ }
306
+
307
+ setFog(color, near = 10, far = 50) {
308
+ this.scene.fog = new THREE.Fog(color, near, far);
309
+ }
310
+
311
+ setLightDirection(x, y, z) {
312
+ if (this.mainLight) {
313
+ this.mainLight.position.set(x, y, z);
314
+ }
315
+ }
316
+
317
+ setLightColor(color) {
318
+ if (this.mainLight) {
319
+ this.mainLight.color.setHex(color);
320
+ }
321
+ }
322
+
323
+ setAmbientLight(color, intensity) {
324
+ if (this.lights && this.lights.ambient) {
325
+ this.lights.ambient.color.setHex(color);
326
+ if (typeof intensity === 'number') {
327
+ this.lights.ambient.intensity = intensity;
328
+ }
329
+ }
330
+ }
331
+
332
+ // Enhanced material creation with stunning visuals but simplified shaders
333
+ createN64Material(options = {}) {
334
+ const {
335
+ color = 0xffffff,
336
+ texture = null,
337
+ normalMap = null,
338
+ roughnessMap = null,
339
+ aoMap = null,
340
+ metallic = false,
341
+ metalness = metallic ? 0.9 : 0.0,
342
+ emissive = 0x000000,
343
+ emissiveIntensity = 0,
344
+ roughness = 0.6,
345
+ transparent = false,
346
+ alphaTest = 0.5,
347
+ animated = false,
348
+ holographic = false,
349
+ } = options;
350
+
351
+ let material;
352
+
353
+ if (holographic || emissiveIntensity > 0.5) {
354
+ // Create stunning holographic/glowing materials - simplified to avoid shader errors
355
+ material = new THREE.MeshStandardMaterial({
356
+ color: color,
357
+ emissive: new THREE.Color(emissive),
358
+ emissiveIntensity: Math.max(emissiveIntensity, 0.4),
359
+ metalness: 0.8,
360
+ roughness: 0.1,
361
+ transparent: true,
362
+ opacity: 0.9,
363
+ side: THREE.DoubleSide,
364
+ fog: true,
365
+ });
366
+ } else if (metallic) {
367
+ // Enhanced metallic materials with environment reflections
368
+ material = new THREE.MeshStandardMaterial({
369
+ color: color,
370
+ metalness: 0.9,
371
+ roughness: roughness * 0.4,
372
+ envMapIntensity: 2.5,
373
+ transparent: transparent,
374
+ alphaTest: alphaTest,
375
+ side: THREE.DoubleSide,
376
+ fog: true,
377
+ });
378
+ } else {
379
+ // Enhanced standard materials with better lighting
380
+ material = new THREE.MeshPhongMaterial({
381
+ color: color,
382
+ transparent: transparent,
383
+ alphaTest: alphaTest,
384
+ side: THREE.DoubleSide,
385
+ shininess: 60,
386
+ specular: 0x444444,
387
+ fog: true,
388
+ reflectivity: 0.2,
389
+ });
390
+
391
+ // Add emissive glow if specified
392
+ if (emissive !== 0x000000) {
393
+ material.emissive = new THREE.Color(emissive);
394
+ material.emissiveIntensity = Math.max(emissiveIntensity, 0.3);
395
+ }
396
+ }
397
+
398
+ // Enhanced texture handling with better filtering
399
+ if (texture) {
400
+ material.map = texture;
401
+ texture.magFilter = THREE.NearestFilter;
402
+ texture.minFilter = THREE.NearestFilter;
403
+ texture.generateMipmaps = false;
404
+ texture.wrapS = THREE.RepeatWrapping;
405
+ texture.wrapT = THREE.RepeatWrapping;
406
+
407
+ // Add texture animation for dynamic effects
408
+ if (animated) {
409
+ texture.offset.set(Math.random() * 0.1, Math.random() * 0.1);
410
+ material.userData.animateTexture = true;
411
+ }
412
+ }
413
+
414
+ // Normal mapping — upgrades MeshPhong to MeshStandard for TBN support
415
+ if (normalMap && material.isMeshPhongMaterial) {
416
+ const std = new THREE.MeshStandardMaterial({
417
+ color: material.color,
418
+ emissive: material.emissive || new THREE.Color(0),
419
+ emissiveIntensity: material.emissiveIntensity || 0,
420
+ roughness: roughness,
421
+ metalness: metalness,
422
+ map: material.map || null,
423
+ transparent: material.transparent,
424
+ alphaTest: material.alphaTest,
425
+ side: material.side,
426
+ fog: material.fog,
427
+ });
428
+ material.dispose();
429
+ material = std;
430
+ }
431
+ if (normalMap && material.normalMap !== undefined) {
432
+ material.normalMap = normalMap;
433
+ normalMap.wrapS = THREE.RepeatWrapping;
434
+ normalMap.wrapT = THREE.RepeatWrapping;
435
+ material.normalMapType = THREE.TangentSpaceNormalMap;
436
+ }
437
+ if (roughnessMap && material.roughnessMap !== undefined) {
438
+ material.roughnessMap = roughnessMap;
439
+ }
440
+ if (aoMap && material.aoMap !== undefined) {
441
+ material.aoMap = aoMap;
442
+ material.aoMapIntensity = 1.0;
443
+ }
444
+
445
+ // Store animation flags
446
+ material.userData.animated = animated;
447
+ material.userData.holographic = holographic;
448
+
449
+ return material;
450
+ }
451
+
452
+ // Geometry helpers
453
+ createBoxGeometry(width = 1, height = 1, depth = 1) {
454
+ return new THREE.BoxGeometry(width, height, depth);
455
+ }
456
+
457
+ createSphereGeometry(radius = 1, segments = 8) {
458
+ return new THREE.SphereGeometry(radius, segments, segments);
459
+ }
460
+
461
+ createPlaneGeometry(width = 1, height = 1) {
462
+ return new THREE.PlaneGeometry(width, height);
463
+ }
464
+
465
+ createCylinderGeometry(radiusTop = 1, radiusBottom = 1, height = 1, segments = 16) {
466
+ return new THREE.CylinderGeometry(radiusTop, radiusBottom, height, segments);
467
+ }
468
+
469
+ createConeGeometry(radius = 1, height = 2, segments = 16) {
470
+ return new THREE.ConeGeometry(radius, height, segments);
471
+ }
472
+
473
+ createCapsuleGeometry(radius = 0.5, height = 1, segments = 8) {
474
+ // Capsule = cylinder + two hemisphere caps
475
+ return new THREE.CapsuleGeometry(radius, height, segments, segments * 2);
476
+ }
477
+
478
+ // 2D compatibility methods
479
+ getFramebuffer() {
480
+ return this.fb;
481
+ }
482
+ supportsSpriteBatch() {
483
+ return true;
484
+ }
485
+
486
+ queueSprite(img, sx, sy, sw, sh, dx, dy, scale = 1) {
487
+ const gltex = this._getTexture(img);
488
+ let arr = this.spriteBatches.get(gltex);
489
+ if (!arr) {
490
+ arr = [];
491
+ this.spriteBatches.set(gltex, arr);
492
+ }
493
+ arr.push({
494
+ sx,
495
+ sy,
496
+ sw,
497
+ sh,
498
+ dx,
499
+ dy,
500
+ scale,
501
+ tex: gltex,
502
+ iw: img.naturalWidth,
503
+ ih: img.naturalHeight,
504
+ });
505
+ }
506
+
507
+ _getTexture(img) {
508
+ let tex = this.texCache.get(img);
509
+ if (tex) return tex;
510
+
511
+ tex = new THREE.Texture(img);
512
+ tex.generateMipmaps = false;
513
+ tex.minFilter = THREE.NearestFilter;
514
+ tex.magFilter = THREE.NearestFilter;
515
+ tex.needsUpdate = true;
516
+ this.texCache.set(img, tex);
517
+ return tex;
518
+ }
519
+
520
+ flushSprites() {
521
+ // For now, just render sprite batches to 2D overlay
522
+ for (const [, sprites] of this.spriteBatches) {
523
+ for (const sprite of sprites) {
524
+ this.renderSpriteToFramebuffer(sprite);
525
+ }
526
+ }
527
+ }
528
+
529
+ renderSpriteToFramebuffer(_sprite) {
530
+ // Placeholder — sprite-to-framebuffer compositing not yet implemented
531
+ }
532
+
533
+ // Performance stats
534
+ getStats() {
535
+ return {
536
+ ...this.stats,
537
+ memory: this.renderer.info.memory,
538
+ render: this.renderer.info.render,
539
+ };
540
+ }
541
+
542
+ // N64-style post-processing
543
+ enablePixelation(factor = 2) {
544
+ this.renderer.setPixelRatio(1 / factor);
545
+ }
546
+
547
+ enableDithering(enabled = true) {
548
+ this.renderer.dithering = enabled;
549
+ }
550
+
551
+ enableBloom(enabled = true) {
552
+ // For now, just increase exposure for bloom-like effect
553
+ if (enabled) {
554
+ this.renderer.toneMappingExposure = 1.8;
555
+ } else {
556
+ this.renderer.toneMappingExposure = 1.4;
557
+ }
558
+ }
559
+
560
+ enableMotionBlur(factor = 0.5) {
561
+ // Motion blur would require post-processing pipeline
562
+ // For now, just store the setting
563
+ this.motionBlurFactor = factor;
564
+ }
565
+
566
+ // Register a mesh with animated material so update() can skip scene.traverse()
567
+ registerAnimatedMesh(mesh) {
568
+ if (mesh && !this.animatedMeshes.includes(mesh)) {
569
+ this.animatedMeshes.push(mesh);
570
+ }
571
+ }
572
+
573
+ // Called by clearScene() in api-3d.js to reset the list
574
+ clearAnimatedMeshes() {
575
+ this.animatedMeshes = [];
576
+ }
577
+
578
+ // Animation system for dynamic materials and effects
579
+ update(deltaTime) {
580
+ const time = performance.now() * 0.001;
581
+
582
+ // Rebuild frustum from current camera state for this frame
583
+ this.camera.updateMatrixWorld();
584
+ this._projScreenMatrix.multiplyMatrices(
585
+ this.camera.projectionMatrix,
586
+ this.camera.matrixWorldInverse
587
+ );
588
+ this.frustum.setFromProjectionMatrix(this._projScreenMatrix);
589
+
590
+ // Prune disposed objects then update only registered animated meshes
591
+ this.animatedMeshes = this.animatedMeshes.filter(m => m.parent);
592
+ for (const object of this.animatedMeshes) {
593
+ const material = object.material;
594
+ if (!material || !material.userData.animated) continue;
595
+
596
+ // Skip material updates for objects outside the view frustum
597
+ if (!this.frustum.intersectsObject(object)) continue;
598
+
599
+ // Animate texture offsets for flowing effects
600
+ if (material.userData.animateTexture && material.map) {
601
+ material.map.offset.x += deltaTime * 0.1;
602
+ material.map.offset.y += deltaTime * 0.05;
603
+ }
604
+
605
+ // Animate emissive pulsing for holographic materials
606
+ if (material.emissive && material.userData.holographic) {
607
+ material.emissiveIntensity = 0.3 + Math.sin(time * 4) * 0.2;
608
+ }
609
+ }
610
+
611
+ // Dynamic lighting effects — position only, no HSL cycling
612
+ if (this.lights) {
613
+ // Subtle light movement for atmosphere
614
+ this.lights.point1.position.x = 10 + Math.sin(time * 0.5) * 3;
615
+ this.lights.point1.position.y = 15 + Math.cos(time * 0.7) * 2;
616
+
617
+ this.lights.point2.position.x = -10 + Math.cos(time * 0.6) * 4;
618
+ this.lights.point2.position.z = -10 + Math.sin(time * 0.4) * 3;
619
+ // NOTE: fill2 / fill3 colors are now static — carts own mood lighting
620
+ }
621
+
622
+ // Fog animation for atmospheric depth
623
+ if (this.scene.fog && this.scene.fog.density) {
624
+ this.scene.fog.density = 0.008 + Math.sin(time * 0.5) * 0.002;
625
+ }
626
+ }
627
+
628
+ // Enhanced rendering with post-processing effects
629
+ render() {
630
+ // This method is for direct rendering calls
631
+ // Main rendering is handled by endFrame()
632
+ this.renderer.render(this.scene, this.camera);
633
+
634
+ // Update performance stats
635
+ this.stats.triangles = this.renderer.info.render.triangles;
636
+ this.stats.drawCalls = this.renderer.info.render.calls;
637
+ this.stats.geometries = this.renderer.info.memory.geometries;
638
+ }
639
+ }