nova64 0.2.5 → 0.2.7
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/README.md +25 -8
- package/bin/nova64.js +165 -0
- package/dist/assets/console-CY_kygm3.js +14 -0
- package/dist/assets/console-CY_kygm3.js.map +1 -0
- package/dist/assets/main-l0sNRNKZ.js.map +1 -0
- package/dist/assets/sky/studio/nx.png +0 -0
- package/dist/assets/sky/studio/ny.png +0 -0
- package/dist/assets/sky/studio/nz.png +0 -0
- package/dist/assets/sky/studio/px.png +0 -0
- package/dist/assets/sky/studio/py.png +0 -0
- package/dist/assets/sky/studio/pz.png +0 -0
- package/dist/assets/vanilla-Dcuy32gi.js +2 -0
- package/dist/assets/vanilla-Dcuy32gi.js.map +1 -0
- package/dist/console.html +899 -0
- package/dist/docs/BENCHMARK.md +77 -0
- package/dist/docs/CHEATSHEET.md +255 -0
- package/dist/docs/EFFECTS_API_GUIDE.md +577 -0
- package/dist/docs/EFFECTS_QUICK_REFERENCE.md +331 -0
- package/dist/docs/FONT_CHARACTER_REFERENCE.md +219 -0
- package/dist/docs/FREE_GLB_ASSETS.md +330 -0
- package/dist/docs/FULLSCREEN_BUTTON_FEATURE.md +296 -0
- package/dist/docs/GAMEPAD_SUPPORT.md +348 -0
- package/dist/docs/GAME_IMPROVEMENTS.md +278 -0
- package/dist/docs/GAME_QUALITY_STATUS.md +300 -0
- package/dist/docs/MIGRATION_GUIDE.md +553 -0
- package/dist/docs/NOVA64_3D_API.md +356 -0
- package/dist/docs/NOVA64_API_REFERENCE.md +1406 -0
- package/dist/docs/NOVA64_UI_API.md +503 -0
- package/dist/docs/UI_SYSTEM_SUMMARY.md +445 -0
- package/dist/docs/VOXEL_ENGINE_GUIDE.md +662 -0
- package/dist/docs/VOXEL_QUICK_REFERENCE.md +386 -0
- package/dist/docs/api-3d.html +750 -0
- package/dist/docs/api-effects.html +385 -0
- package/dist/docs/api-improvements.md +121 -0
- package/dist/docs/api-skybox.html +407 -0
- package/dist/docs/api-sprites.html +321 -0
- package/dist/docs/api-voxel.html +337 -0
- package/dist/docs/api.html +543 -0
- package/dist/docs/assets.html +306 -0
- package/dist/docs/audio.html +340 -0
- package/dist/docs/blogs.html +286 -0
- package/dist/docs/collision.html +316 -0
- package/dist/docs/console.html +247 -0
- package/dist/docs/editor.html +297 -0
- package/dist/docs/font.html +247 -0
- package/dist/docs/framebuffer.html +247 -0
- package/dist/docs/fullscreen-button.html +297 -0
- package/dist/docs/gpu-systems.html +247 -0
- package/dist/docs/index.html +580 -0
- package/dist/docs/input.html +491 -0
- package/dist/docs/physics.html +311 -0
- package/dist/docs/screens.html +311 -0
- package/dist/docs/storage.html +311 -0
- package/dist/docs/textinput.html +332 -0
- package/dist/docs/ui.html +488 -0
- package/dist/examples/3d-advanced/code.js +695 -0
- package/dist/examples/adventure-comic-3d/code.js +342 -0
- package/dist/examples/audio-lab/code.js +150 -0
- package/dist/examples/boids-flocking/code.js +270 -0
- package/dist/examples/crystal-cathedral-3d/code.js +706 -0
- package/dist/examples/cyberpunk-city-3d/code.js +1383 -0
- package/dist/examples/demoscene/README.md +192 -0
- package/dist/examples/demoscene/code.js +1081 -0
- package/dist/examples/demoscene/meta.json +21 -0
- package/dist/examples/dungeon-crawler-3d/code.js +1117 -0
- package/dist/examples/f-zero-nova-3d/code.js +865 -0
- package/dist/examples/f-zero-nova-3d/code_old.js +1555 -0
- package/dist/examples/fps-demo-3d/code.js +744 -0
- package/dist/examples/game-of-life-3d/code.js +338 -0
- package/dist/examples/generative-art/code.js +632 -0
- package/dist/examples/hello-3d/code.js +325 -0
- package/dist/examples/hello-skybox/code.js +183 -0
- package/dist/examples/hello-world/code.js +19 -0
- package/dist/examples/input-showcase/code.js +109 -0
- package/dist/examples/instancing-demo/code.js +315 -0
- package/dist/examples/minecraft-demo/code.js +387 -0
- package/dist/examples/model-viewer-3d/code.js +114 -0
- package/dist/examples/mystical-realm-3d/code.js +1203 -0
- package/dist/examples/nature-explorer-3d/code.js +1318 -0
- package/dist/examples/particles-demo/code.js +522 -0
- package/dist/examples/pbr-showcase/code.js +140 -0
- package/dist/examples/physics-demo-3d/code.js +948 -0
- package/dist/examples/screen-demo/code.js +267 -0
- package/dist/examples/shooter-demo-3d/code.js +1286 -0
- package/dist/examples/space-combat-3d/IMPLEMENTATION_SUMMARY.md +109 -0
- package/dist/examples/space-combat-3d/README.md +135 -0
- package/dist/examples/space-combat-3d/code.js +1332 -0
- package/dist/examples/space-harrier-3d/code.js +923 -0
- package/dist/examples/star-fox-nova-3d/code.js +1116 -0
- package/dist/examples/star-fox-nova-3d/code_backup.js +410 -0
- package/dist/examples/star-fox-nova-3d/code_broken.js +1821 -0
- package/dist/examples/storage-quest/code.js +209 -0
- package/dist/examples/strider-demo-3d/IMPROVEMENT_OPTIONS.md +285 -0
- package/dist/examples/strider-demo-3d/cache-test.html +132 -0
- package/dist/examples/strider-demo-3d/code-fixed.js +582 -0
- package/dist/examples/strider-demo-3d/code-old.js +1537 -0
- package/dist/examples/strider-demo-3d/code.js +1462 -0
- package/dist/examples/strider-demo-3d/code.js.bak2 +1169 -0
- package/dist/examples/strider-demo-3d/fix-game.sh +53 -0
- package/dist/examples/super-plumber-64/README.md +128 -0
- package/dist/examples/super-plumber-64/code.js +1185 -0
- package/dist/examples/super-plumber-64/index.html +88 -0
- package/dist/examples/test-2d-overlay/code.js +32 -0
- package/dist/examples/test-font/code.js +51 -0
- package/dist/examples/test-minimal/code.js +21 -0
- package/dist/examples/ui-demo/code.js +306 -0
- package/dist/examples/wing-commander-space/README.md +180 -0
- package/dist/examples/wing-commander-space/code.js +1285 -0
- package/dist/examples/wizardry-3d/CHANGELOG.md +366 -0
- package/dist/examples/wizardry-3d/code.js +3928 -0
- package/dist/index.html +666 -0
- package/dist/os9-shell/assets/index-DIHfrTaW.css +1 -0
- package/dist/os9-shell/assets/index-KchE_ngx.js +483 -0
- package/dist/os9-shell/assets/index-KchE_ngx.js.map +1 -0
- package/dist/os9-shell/index.html +23 -0
- package/dist/os9-shell/nova-icon.svg +12 -0
- 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/index.html +6 -1
- package/package.json +9 -2
- package/public/assets/sky/studio/nx.png +0 -0
- package/public/assets/sky/studio/ny.png +0 -0
- package/public/assets/sky/studio/nz.png +0 -0
- package/public/assets/sky/studio/px.png +0 -0
- package/public/assets/sky/studio/py.png +0 -0
- package/public/assets/sky/studio/pz.png +0 -0
- package/public/os9-shell/assets/index-KchE_ngx.js +483 -0
- package/public/os9-shell/assets/index-KchE_ngx.js.map +1 -0
- package/public/os9-shell/index.html +10 -1
- package/runtime/api-2d.js +301 -21
- package/runtime/api-3d/pbr.js +45 -1
- package/runtime/api-3d.js +1 -0
- package/runtime/api-effects.js +90 -3
- package/runtime/api-gameutils.js +476 -0
- package/runtime/api-generative.js +610 -0
- package/runtime/api-skybox.js +54 -0
- package/runtime/api-voxel.js +139 -28
- package/runtime/gpu-threejs.js +13 -9
- package/runtime/ui.js +2 -2
- package/src/main.js +20 -0
- package/public/os9-shell/assets/index-B1Uvacma.js +0 -32825
- package/public/os9-shell/assets/index-B1Uvacma.js.map +0 -1
|
@@ -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,643 @@
|
|
|
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.25;
|
|
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(0x606080, 0.5);
|
|
88
|
+
this.scene.add(ambientLight);
|
|
89
|
+
|
|
90
|
+
// Hemisphere light for more natural lighting
|
|
91
|
+
const hemisphereLight = new THREE.HemisphereLight(0xffffbb, 0x080820, 0.6);
|
|
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
|
+
// Atmospheric fog with subtle color
|
|
157
|
+
this.scene.fog = new THREE.FogExp2(0x202050, 0.005);
|
|
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
|
+
if (factor <= 0) {
|
|
545
|
+
this.renderer.setPixelRatio(window.devicePixelRatio || 1);
|
|
546
|
+
} else {
|
|
547
|
+
this.renderer.setPixelRatio(1 / factor);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
enableDithering(enabled = true) {
|
|
552
|
+
this.renderer.dithering = enabled;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
enableBloom(enabled = true) {
|
|
556
|
+
// For now, just increase exposure for bloom-like effect
|
|
557
|
+
if (enabled) {
|
|
558
|
+
this.renderer.toneMappingExposure = 1.2;
|
|
559
|
+
} else {
|
|
560
|
+
this.renderer.toneMappingExposure = 1.0;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
enableMotionBlur(factor = 0.5) {
|
|
565
|
+
// Motion blur would require post-processing pipeline
|
|
566
|
+
// For now, just store the setting
|
|
567
|
+
this.motionBlurFactor = factor;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Register a mesh with animated material so update() can skip scene.traverse()
|
|
571
|
+
registerAnimatedMesh(mesh) {
|
|
572
|
+
if (mesh && !this.animatedMeshes.includes(mesh)) {
|
|
573
|
+
this.animatedMeshes.push(mesh);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Called by clearScene() in api-3d.js to reset the list
|
|
578
|
+
clearAnimatedMeshes() {
|
|
579
|
+
this.animatedMeshes = [];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Animation system for dynamic materials and effects
|
|
583
|
+
update(deltaTime) {
|
|
584
|
+
const time = performance.now() * 0.001;
|
|
585
|
+
|
|
586
|
+
// Rebuild frustum from current camera state for this frame
|
|
587
|
+
this.camera.updateMatrixWorld();
|
|
588
|
+
this._projScreenMatrix.multiplyMatrices(
|
|
589
|
+
this.camera.projectionMatrix,
|
|
590
|
+
this.camera.matrixWorldInverse
|
|
591
|
+
);
|
|
592
|
+
this.frustum.setFromProjectionMatrix(this._projScreenMatrix);
|
|
593
|
+
|
|
594
|
+
// Prune disposed objects then update only registered animated meshes
|
|
595
|
+
this.animatedMeshes = this.animatedMeshes.filter(m => m.parent);
|
|
596
|
+
for (const object of this.animatedMeshes) {
|
|
597
|
+
const material = object.material;
|
|
598
|
+
if (!material || !material.userData.animated) continue;
|
|
599
|
+
|
|
600
|
+
// Skip material updates for objects outside the view frustum
|
|
601
|
+
if (!this.frustum.intersectsObject(object)) continue;
|
|
602
|
+
|
|
603
|
+
// Animate texture offsets for flowing effects
|
|
604
|
+
if (material.userData.animateTexture && material.map) {
|
|
605
|
+
material.map.offset.x += deltaTime * 0.1;
|
|
606
|
+
material.map.offset.y += deltaTime * 0.05;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Animate emissive pulsing for holographic materials
|
|
610
|
+
if (material.emissive && material.userData.holographic) {
|
|
611
|
+
material.emissiveIntensity = 0.3 + Math.sin(time * 4) * 0.2;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Dynamic lighting effects — position only, no HSL cycling
|
|
616
|
+
if (this.lights) {
|
|
617
|
+
// Subtle light movement for atmosphere
|
|
618
|
+
this.lights.point1.position.x = 10 + Math.sin(time * 0.5) * 3;
|
|
619
|
+
this.lights.point1.position.y = 15 + Math.cos(time * 0.7) * 2;
|
|
620
|
+
|
|
621
|
+
this.lights.point2.position.x = -10 + Math.cos(time * 0.6) * 4;
|
|
622
|
+
this.lights.point2.position.z = -10 + Math.sin(time * 0.4) * 3;
|
|
623
|
+
// NOTE: fill2 / fill3 colors are now static — carts own mood lighting
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Fog animation for atmospheric depth
|
|
627
|
+
if (this.scene.fog && this.scene.fog.density) {
|
|
628
|
+
this.scene.fog.density = 0.005 + Math.sin(time * 0.5) * 0.001;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Enhanced rendering with post-processing effects
|
|
633
|
+
render() {
|
|
634
|
+
// This method is for direct rendering calls
|
|
635
|
+
// Main rendering is handled by endFrame()
|
|
636
|
+
this.renderer.render(this.scene, this.camera);
|
|
637
|
+
|
|
638
|
+
// Update performance stats
|
|
639
|
+
this.stats.triangles = this.renderer.info.render.triangles;
|
|
640
|
+
this.stats.drawCalls = this.renderer.info.render.calls;
|
|
641
|
+
this.stats.geometries = this.renderer.info.memory.geometries;
|
|
642
|
+
}
|
|
643
|
+
}
|