voidcore 0.0.0

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/index.js ADDED
@@ -0,0 +1,1162 @@
1
+ // src/engine/wasm.ts
2
+ async function loadWasm() {
3
+ const response = await fetch("/voidcore.wasm");
4
+ const memory = new WebAssembly.Memory({ initial: 512 });
5
+ const { instance } = await WebAssembly.instantiateStreaming(response, {
6
+ env: { memory }
7
+ });
8
+ const exports = instance.exports;
9
+ const buffer = memory.buffer;
10
+ const f32 = new Float32Array(buffer);
11
+ const u32 = new Uint32Array(buffer);
12
+ const u8 = new Uint8Array(buffer);
13
+ const scratchOffset = exports.vc_init(512);
14
+ return {
15
+ exports,
16
+ memory,
17
+ f32,
18
+ u32,
19
+ u8,
20
+ scratchOffset,
21
+ positionsPtr: exports.vc_get_positions_ptr(),
22
+ eulerRotationsPtr: exports.vc_get_euler_rotations_ptr(),
23
+ scalesPtr: exports.vc_get_scales_ptr(),
24
+ worldMatricesPtr: exports.vc_get_world_matrices_ptr(),
25
+ colorsPtr: exports.vc_get_colors_ptr(),
26
+ flagsPtr: exports.vc_get_flags_ptr(),
27
+ bspheresPtr: exports.vc_get_bspheres_ptr(),
28
+ geometryIdsPtr: exports.vc_get_geometry_ids_ptr(),
29
+ sortKeysPtr: exports.vc_get_sort_keys_ptr(),
30
+ visibleIndicesPtr: exports.vc_get_visible_indices_ptr()
31
+ };
32
+ }
33
+
34
+ // src/engine/webgl-shaders.ts
35
+ var vertexShaderGLSL = (
36
+ /* glsl */
37
+ `#version 300 es
38
+ precision highp float;
39
+
40
+ layout(std140) uniform CameraUniforms {
41
+ mat4 view;
42
+ mat4 projection;
43
+ } camera;
44
+
45
+ layout(std140) uniform ModelUniforms {
46
+ mat4 world;
47
+ vec4 color;
48
+ vec4 flags; // x = unlit
49
+ } model;
50
+
51
+ layout(location = 0) in vec3 a_position;
52
+ layout(location = 1) in vec3 a_normal;
53
+
54
+ out vec3 v_worldNormal;
55
+ out vec4 v_color;
56
+ out float v_unlit;
57
+
58
+ void main() {
59
+ vec4 worldPos = model.world * vec4(a_position, 1.0);
60
+ gl_Position = camera.projection * camera.view * worldPos;
61
+
62
+ mat3 normalMat = mat3(model.world);
63
+ v_worldNormal = normalize(normalMat * a_normal);
64
+ v_color = model.color;
65
+ v_unlit = model.flags.x;
66
+ }
67
+ `
68
+ );
69
+ var fragmentShaderGLSL = (
70
+ /* glsl */
71
+ `#version 300 es
72
+ precision highp float;
73
+
74
+ layout(std140) uniform LightUniforms {
75
+ vec4 direction;
76
+ vec4 color;
77
+ vec4 ambient;
78
+ } light;
79
+
80
+ in vec3 v_worldNormal;
81
+ in vec4 v_color;
82
+ in float v_unlit;
83
+
84
+ out vec4 fragColor;
85
+
86
+ void main() {
87
+ vec3 normal = normalize(v_worldNormal);
88
+ vec3 lightDir = normalize(-light.direction.xyz);
89
+
90
+ float NdotL = max(dot(normal, lightDir), 0.0);
91
+ vec3 diffuse = light.color.rgb * NdotL;
92
+ vec3 ambient = light.ambient.rgb;
93
+
94
+ vec3 finalColor;
95
+ if (v_unlit > 0.5) {
96
+ finalColor = v_color.rgb;
97
+ } else {
98
+ finalColor = v_color.rgb * (diffuse + ambient);
99
+ }
100
+
101
+ fragColor = vec4(finalColor, v_color.a);
102
+ }
103
+ `
104
+ );
105
+
106
+ // src/engine/webgl-renderer.ts
107
+ function compileShader(gl, type, source) {
108
+ const shader = gl.createShader(type);
109
+ gl.shaderSource(shader, source);
110
+ gl.compileShader(shader);
111
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
112
+ const info = gl.getShaderInfoLog(shader);
113
+ gl.deleteShader(shader);
114
+ throw new Error(`Shader compile error: ${info}`);
115
+ }
116
+ return shader;
117
+ }
118
+ function createProgram(gl, vsSource, fsSource) {
119
+ const vs = compileShader(gl, gl.VERTEX_SHADER, vsSource);
120
+ const fs = compileShader(gl, gl.FRAGMENT_SHADER, fsSource);
121
+ const program = gl.createProgram();
122
+ gl.attachShader(program, vs);
123
+ gl.attachShader(program, fs);
124
+ gl.linkProgram(program);
125
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
126
+ const info = gl.getProgramInfoLog(program);
127
+ throw new Error(`Program link error: ${info}`);
128
+ }
129
+ gl.deleteShader(vs);
130
+ gl.deleteShader(fs);
131
+ return program;
132
+ }
133
+ function createWebGLRenderer(canvas) {
134
+ const gl = canvas.getContext("webgl2", { antialias: true });
135
+ if (!gl) throw new Error("WebGL2 not supported");
136
+ gl.enable(gl.DEPTH_TEST);
137
+ gl.depthFunc(gl.LESS);
138
+ gl.enable(gl.CULL_FACE);
139
+ gl.cullFace(gl.BACK);
140
+ gl.clearColor(0.1, 0.1, 0.1, 1);
141
+ const program = createProgram(gl, vertexShaderGLSL, fragmentShaderGLSL);
142
+ const cameraBlockIdx = gl.getUniformBlockIndex(program, "CameraUniforms");
143
+ const modelBlockIdx = gl.getUniformBlockIndex(program, "ModelUniforms");
144
+ const lightBlockIdx = gl.getUniformBlockIndex(program, "LightUniforms");
145
+ gl.uniformBlockBinding(program, cameraBlockIdx, 0);
146
+ gl.uniformBlockBinding(program, modelBlockIdx, 1);
147
+ gl.uniformBlockBinding(program, lightBlockIdx, 2);
148
+ const cameraUBO = gl.createBuffer();
149
+ gl.bindBuffer(gl.UNIFORM_BUFFER, cameraUBO);
150
+ gl.bufferData(gl.UNIFORM_BUFFER, 128, gl.DYNAMIC_DRAW);
151
+ const MODEL_UBO_SIZE = 96;
152
+ const modelUBO = gl.createBuffer();
153
+ gl.bindBuffer(gl.UNIFORM_BUFFER, modelUBO);
154
+ gl.bufferData(gl.UNIFORM_BUFFER, MODEL_UBO_SIZE, gl.DYNAMIC_DRAW);
155
+ const lightUBO = gl.createBuffer();
156
+ gl.bindBuffer(gl.UNIFORM_BUFFER, lightUBO);
157
+ gl.bufferData(gl.UNIFORM_BUFFER, 48, gl.DYNAMIC_DRAW);
158
+ const geometries = /* @__PURE__ */ new Map();
159
+ const modelData = new Float32Array(24);
160
+ const renderer = {
161
+ backend: "webgl",
162
+ registerGeometry(id, vertices, indices) {
163
+ const vao = gl.createVertexArray();
164
+ gl.bindVertexArray(vao);
165
+ const vbo = gl.createBuffer();
166
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
167
+ gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
168
+ gl.enableVertexAttribArray(0);
169
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0);
170
+ gl.enableVertexAttribArray(1);
171
+ gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12);
172
+ const ebo = gl.createBuffer();
173
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo);
174
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
175
+ gl.bindVertexArray(null);
176
+ geometries.set(id, { vao, indexCount: indices.length });
177
+ },
178
+ updateCamera(view, projection) {
179
+ const data = new Float32Array(32);
180
+ data.set(view, 0);
181
+ data.set(projection, 16);
182
+ gl.bindBuffer(gl.UNIFORM_BUFFER, cameraUBO);
183
+ gl.bufferSubData(gl.UNIFORM_BUFFER, 0, data);
184
+ },
185
+ updateLighting(dir, dirColor, ambient) {
186
+ const data = new Float32Array(12);
187
+ data.set(dir.subarray(0, 3), 0);
188
+ data.set(dirColor.subarray(0, 3), 4);
189
+ data.set(ambient.subarray(0, 3), 8);
190
+ gl.bindBuffer(gl.UNIFORM_BUFFER, lightUBO);
191
+ gl.bufferSubData(gl.UNIFORM_BUFFER, 0, data);
192
+ },
193
+ draw(entities, count) {
194
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
195
+ gl.useProgram(program);
196
+ gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, cameraUBO);
197
+ gl.bindBufferBase(gl.UNIFORM_BUFFER, 2, lightUBO);
198
+ for (let i = 0; i < count; i++) {
199
+ const entity = entities[i];
200
+ const geo = geometries.get(entity.geometryId);
201
+ if (!geo) continue;
202
+ modelData.set(entity.worldMatrix, 0);
203
+ modelData.set(entity.color, 16);
204
+ modelData[20] = entity.unlit ? 1 : 0;
205
+ gl.bindBuffer(gl.UNIFORM_BUFFER, modelUBO);
206
+ gl.bufferSubData(gl.UNIFORM_BUFFER, 0, modelData);
207
+ gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, modelUBO);
208
+ gl.bindVertexArray(geo.vao);
209
+ gl.drawElements(gl.TRIANGLES, geo.indexCount, gl.UNSIGNED_SHORT, 0);
210
+ }
211
+ gl.bindVertexArray(null);
212
+ },
213
+ resize(width, height) {
214
+ if (width === 0 || height === 0) return;
215
+ gl.viewport(0, 0, width, height);
216
+ },
217
+ destroy() {
218
+ gl.deleteBuffer(cameraUBO);
219
+ gl.deleteBuffer(modelUBO);
220
+ gl.deleteBuffer(lightUBO);
221
+ gl.deleteProgram(program);
222
+ for (const geo of geometries.values()) {
223
+ gl.deleteVertexArray(geo.vao);
224
+ }
225
+ }
226
+ };
227
+ return renderer;
228
+ }
229
+
230
+ // src/engine/shaders.ts
231
+ var shaderSource = (
232
+ /* wgsl */
233
+ `
234
+ struct CameraUniforms {
235
+ view: mat4x4f,
236
+ projection: mat4x4f,
237
+ }
238
+
239
+ struct ModelUniforms {
240
+ world: mat4x4f,
241
+ color: vec4f,
242
+ flags: vec4f, // x = unlit
243
+ }
244
+
245
+ struct LightUniforms {
246
+ direction: vec4f,
247
+ color: vec4f,
248
+ ambient: vec4f,
249
+ }
250
+
251
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
252
+ @group(1) @binding(0) var<uniform> model: ModelUniforms;
253
+ @group(2) @binding(0) var<uniform> light: LightUniforms;
254
+
255
+ struct VertexInput {
256
+ @location(0) position: vec3f,
257
+ @location(1) normal: vec3f,
258
+ }
259
+
260
+ struct VertexOutput {
261
+ @builtin(position) position: vec4f,
262
+ @location(0) worldNormal: vec3f,
263
+ @location(1) color: vec4f,
264
+ @location(2) unlit: f32,
265
+ }
266
+
267
+ @vertex
268
+ fn vs_main(input: VertexInput) -> VertexOutput {
269
+ var output: VertexOutput;
270
+
271
+ let worldPos = model.world * vec4f(input.position, 1.0);
272
+ output.position = camera.projection * camera.view * worldPos;
273
+
274
+ // Transform normal by upper 3x3 of world matrix
275
+ let normalMat = mat3x3f(
276
+ model.world[0].xyz,
277
+ model.world[1].xyz,
278
+ model.world[2].xyz,
279
+ );
280
+ output.worldNormal = normalize(normalMat * input.normal);
281
+ output.color = model.color;
282
+ output.unlit = model.flags.x;
283
+
284
+ return output;
285
+ }
286
+
287
+ @fragment
288
+ fn fs_main(input: VertexOutput) -> @location(0) vec4f {
289
+ let normal = normalize(input.worldNormal);
290
+ let lightDir = normalize(-light.direction.xyz);
291
+
292
+ // Lambert diffuse
293
+ let NdotL = max(dot(normal, lightDir), 0.0);
294
+ let diffuse = light.color.rgb * NdotL;
295
+ let ambient = light.ambient.rgb;
296
+
297
+ var finalColor: vec3f;
298
+ if (input.unlit > 0.5) {
299
+ finalColor = input.color.rgb;
300
+ } else {
301
+ finalColor = input.color.rgb * (diffuse + ambient);
302
+ }
303
+
304
+ return vec4f(finalColor, input.color.a);
305
+ }
306
+ `
307
+ );
308
+
309
+ // src/engine/renderer.ts
310
+ var MODEL_UNIFORM_SIZE = 256;
311
+ var MAX_ENTITIES = 4096;
312
+ async function createWebGPURenderer(canvas) {
313
+ const adapter = await navigator.gpu.requestAdapter();
314
+ if (!adapter) throw new Error("No WebGPU adapter");
315
+ const device = await adapter.requestDevice();
316
+ const context = canvas.getContext("webgpu");
317
+ const format = navigator.gpu.getPreferredCanvasFormat();
318
+ context.configure({ device, format, alphaMode: "premultiplied" });
319
+ const shaderModule = device.createShaderModule({
320
+ code: shaderSource
321
+ });
322
+ const cameraBindGroupLayout = device.createBindGroupLayout({
323
+ entries: [
324
+ {
325
+ binding: 0,
326
+ visibility: GPUShaderStage.VERTEX,
327
+ buffer: { type: "uniform" }
328
+ }
329
+ ]
330
+ });
331
+ const modelBindGroupLayout = device.createBindGroupLayout({
332
+ entries: [
333
+ {
334
+ binding: 0,
335
+ visibility: GPUShaderStage.VERTEX,
336
+ buffer: { type: "uniform", hasDynamicOffset: true }
337
+ }
338
+ ]
339
+ });
340
+ const lightBindGroupLayout = device.createBindGroupLayout({
341
+ entries: [
342
+ {
343
+ binding: 0,
344
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
345
+ buffer: { type: "uniform" }
346
+ }
347
+ ]
348
+ });
349
+ const pipelineLayout = device.createPipelineLayout({
350
+ bindGroupLayouts: [
351
+ cameraBindGroupLayout,
352
+ modelBindGroupLayout,
353
+ lightBindGroupLayout
354
+ ]
355
+ });
356
+ let depthTexture = device.createTexture({
357
+ size: [canvas.width || 1, canvas.height || 1],
358
+ format: "depth24plus",
359
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
360
+ sampleCount: 4
361
+ });
362
+ let msaaTexture = device.createTexture({
363
+ size: [canvas.width || 1, canvas.height || 1],
364
+ format,
365
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
366
+ sampleCount: 4
367
+ });
368
+ const pipeline = device.createRenderPipeline({
369
+ layout: pipelineLayout,
370
+ vertex: {
371
+ module: shaderModule,
372
+ entryPoint: "vs_main",
373
+ buffers: [
374
+ {
375
+ arrayStride: 24,
376
+ // 6 floats × 4 bytes
377
+ attributes: [
378
+ { shaderLocation: 0, offset: 0, format: "float32x3" },
379
+ // position
380
+ { shaderLocation: 1, offset: 12, format: "float32x3" }
381
+ // normal
382
+ ]
383
+ }
384
+ ]
385
+ },
386
+ fragment: {
387
+ module: shaderModule,
388
+ entryPoint: "fs_main",
389
+ targets: [{ format }]
390
+ },
391
+ primitive: {
392
+ topology: "triangle-list",
393
+ cullMode: "back"
394
+ },
395
+ depthStencil: {
396
+ depthWriteEnabled: true,
397
+ depthCompare: "less",
398
+ format: "depth24plus"
399
+ },
400
+ multisample: { count: 4 }
401
+ });
402
+ const cameraBuffer = device.createBuffer({
403
+ size: 128,
404
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
405
+ });
406
+ const cameraBindGroup = device.createBindGroup({
407
+ layout: cameraBindGroupLayout,
408
+ entries: [{ binding: 0, resource: { buffer: cameraBuffer } }]
409
+ });
410
+ const modelBuffer = device.createBuffer({
411
+ size: MODEL_UNIFORM_SIZE * MAX_ENTITIES,
412
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
413
+ });
414
+ const modelBindGroup = device.createBindGroup({
415
+ layout: modelBindGroupLayout,
416
+ entries: [
417
+ {
418
+ binding: 0,
419
+ resource: { buffer: modelBuffer, size: MODEL_UNIFORM_SIZE }
420
+ }
421
+ ]
422
+ });
423
+ const lightBuffer = device.createBuffer({
424
+ size: 48,
425
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
426
+ });
427
+ const lightBindGroup = device.createBindGroup({
428
+ layout: lightBindGroupLayout,
429
+ entries: [{ binding: 0, resource: { buffer: lightBuffer } }]
430
+ });
431
+ const modelData = new Float32Array(
432
+ MODEL_UNIFORM_SIZE / 4 * MAX_ENTITIES
433
+ );
434
+ const geometries = /* @__PURE__ */ new Map();
435
+ const renderer = {
436
+ backend: "webgpu",
437
+ registerGeometry(id, vertices, indices) {
438
+ const vertexBuffer = device.createBuffer({
439
+ size: vertices.byteLength,
440
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
441
+ });
442
+ device.queue.writeBuffer(vertexBuffer, 0, new Float32Array(vertices));
443
+ const indexBuffer = device.createBuffer({
444
+ size: indices.byteLength,
445
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST
446
+ });
447
+ device.queue.writeBuffer(
448
+ indexBuffer,
449
+ 0,
450
+ indices instanceof Uint16Array ? new Uint16Array(indices) : new Uint32Array(indices)
451
+ );
452
+ geometries.set(id, {
453
+ vertexBuffer,
454
+ indexBuffer,
455
+ indexCount: indices.length
456
+ });
457
+ },
458
+ updateCamera(view, projection) {
459
+ device.queue.writeBuffer(cameraBuffer, 0, new Float32Array(view));
460
+ device.queue.writeBuffer(cameraBuffer, 64, new Float32Array(projection));
461
+ },
462
+ updateLighting(dir, dirColor, ambient) {
463
+ const data = new Float32Array(12);
464
+ data.set(dir.subarray(0, 3), 0);
465
+ data.set(dirColor.subarray(0, 3), 4);
466
+ data.set(ambient.subarray(0, 3), 8);
467
+ device.queue.writeBuffer(lightBuffer, 0, data);
468
+ },
469
+ draw(entities, count) {
470
+ const commandEncoder = device.createCommandEncoder();
471
+ const colorView = msaaTexture.createView();
472
+ const resolveTarget = context.getCurrentTexture().createView();
473
+ const renderPass = commandEncoder.beginRenderPass({
474
+ colorAttachments: [
475
+ {
476
+ view: colorView,
477
+ resolveTarget,
478
+ clearValue: { r: 0.1, g: 0.1, b: 0.1, a: 1 },
479
+ loadOp: "clear",
480
+ storeOp: "discard"
481
+ }
482
+ ],
483
+ depthStencilAttachment: {
484
+ view: depthTexture.createView(),
485
+ depthClearValue: 1,
486
+ depthLoadOp: "clear",
487
+ depthStoreOp: "discard"
488
+ }
489
+ });
490
+ renderPass.setPipeline(pipeline);
491
+ renderPass.setBindGroup(0, cameraBindGroup);
492
+ renderPass.setBindGroup(2, lightBindGroup);
493
+ for (let i = 0; i < count; i++) {
494
+ const entity = entities[i];
495
+ const base = MODEL_UNIFORM_SIZE / 4 * i;
496
+ modelData.set(entity.worldMatrix, base);
497
+ modelData.set(entity.color, base + 16);
498
+ modelData[base + 20] = entity.unlit ? 1 : 0;
499
+ }
500
+ device.queue.writeBuffer(
501
+ modelBuffer,
502
+ 0,
503
+ modelData.buffer,
504
+ 0,
505
+ MODEL_UNIFORM_SIZE * count
506
+ );
507
+ for (let i = 0; i < count; i++) {
508
+ const entity = entities[i];
509
+ const geo = geometries.get(entity.geometryId);
510
+ if (!geo) continue;
511
+ renderPass.setBindGroup(1, modelBindGroup, [MODEL_UNIFORM_SIZE * i]);
512
+ renderPass.setVertexBuffer(0, geo.vertexBuffer);
513
+ renderPass.setIndexBuffer(geo.indexBuffer, "uint16");
514
+ renderPass.drawIndexed(geo.indexCount);
515
+ }
516
+ renderPass.end();
517
+ device.queue.submit([commandEncoder.finish()]);
518
+ },
519
+ resize(width, height) {
520
+ if (width === 0 || height === 0) return;
521
+ depthTexture.destroy();
522
+ msaaTexture.destroy();
523
+ depthTexture = device.createTexture({
524
+ size: [width, height],
525
+ format: "depth24plus",
526
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
527
+ sampleCount: 4
528
+ });
529
+ msaaTexture = device.createTexture({
530
+ size: [width, height],
531
+ format,
532
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
533
+ sampleCount: 4
534
+ });
535
+ },
536
+ destroy() {
537
+ cameraBuffer.destroy();
538
+ modelBuffer.destroy();
539
+ lightBuffer.destroy();
540
+ depthTexture.destroy();
541
+ msaaTexture.destroy();
542
+ for (const geo of geometries.values()) {
543
+ geo.vertexBuffer.destroy();
544
+ geo.indexBuffer.destroy();
545
+ }
546
+ device.destroy();
547
+ }
548
+ };
549
+ return renderer;
550
+ }
551
+
552
+ // src/engine/gpu.ts
553
+ async function createRenderer(canvas, backend) {
554
+ if (backend === "webgl") {
555
+ return createWebGLRenderer(canvas);
556
+ }
557
+ if (backend === "webgpu" || navigator.gpu) {
558
+ try {
559
+ return await createWebGPURenderer(canvas);
560
+ } catch {
561
+ }
562
+ }
563
+ return createWebGLRenderer(canvas);
564
+ }
565
+
566
+ // src/engine/camera.ts
567
+ var Camera = class {
568
+ eye = new Float32Array([0, -10, 5]);
569
+ target = new Float32Array([0, 0, 0]);
570
+ up = new Float32Array([0, 0, 1]);
571
+ fov = Math.PI / 4;
572
+ // 45 degrees
573
+ near = 0.1;
574
+ far = 1e3;
575
+ // Output matrices
576
+ view = new Float32Array(16);
577
+ projection = new Float32Array(16);
578
+ vp = new Float32Array(16);
579
+ // Byte offsets in WASM scratch area
580
+ viewOffset;
581
+ projOffset;
582
+ vpOffset;
583
+ constructor(scratchOffset) {
584
+ this.viewOffset = scratchOffset;
585
+ this.projOffset = scratchOffset + 64;
586
+ this.vpOffset = scratchOffset + 128;
587
+ }
588
+ update(wasm, aspect) {
589
+ const { exports, f32 } = wasm;
590
+ exports.vc_look_at(
591
+ this.viewOffset,
592
+ this.eye[0],
593
+ this.eye[1],
594
+ this.eye[2],
595
+ this.target[0],
596
+ this.target[1],
597
+ this.target[2],
598
+ this.up[0],
599
+ this.up[1],
600
+ this.up[2]
601
+ );
602
+ exports.vc_perspective(this.projOffset, this.fov, aspect, this.near, this.far);
603
+ exports.vc_m4_multiply(this.vpOffset, this.projOffset, this.viewOffset);
604
+ const vi = this.viewOffset / 4;
605
+ const pi = this.projOffset / 4;
606
+ const vpi = this.vpOffset / 4;
607
+ this.view.set(f32.subarray(vi, vi + 16));
608
+ this.projection.set(f32.subarray(pi, pi + 16));
609
+ this.vp.set(f32.subarray(vpi, vpi + 16));
610
+ }
611
+ };
612
+
613
+ // src/engine/mesh.ts
614
+ var FLAG_DIRTY = 1;
615
+ var FLAG_VISIBLE = 2;
616
+ var FLAG_UNLIT = 4;
617
+ var Mesh = class {
618
+ entityId = -1;
619
+ // Sub-views into WASM SoA arrays (set when added to scene)
620
+ position;
621
+ rotation;
622
+ scale;
623
+ color;
624
+ worldMatrix;
625
+ wasm;
626
+ flags;
627
+ geometryId;
628
+ initOptions;
629
+ // Bounding sphere (center offset from entity origin, radius)
630
+ bsphereRadius = 0.5;
631
+ bsphereCenterOffset = new Float32Array(3);
632
+ constructor(options) {
633
+ this.geometryId = options.geometryId;
634
+ this.initOptions = options;
635
+ }
636
+ /** Called by Scene when entity is assigned */
637
+ _bind(wasm, entityId) {
638
+ this.entityId = entityId;
639
+ this.wasm = wasm;
640
+ const { f32, u32 } = wasm;
641
+ const posBase = wasm.positionsPtr / 4 + entityId * 3;
642
+ const rotBase = wasm.eulerRotationsPtr / 4 + entityId * 3;
643
+ const scBase = wasm.scalesPtr / 4 + entityId * 3;
644
+ const colBase = wasm.colorsPtr / 4 + entityId * 4;
645
+ const wmBase = wasm.worldMatricesPtr / 4 + entityId * 16;
646
+ this.position = f32.subarray(posBase, posBase + 3);
647
+ this.rotation = f32.subarray(rotBase, rotBase + 3);
648
+ this.scale = f32.subarray(scBase, scBase + 3);
649
+ this.color = f32.subarray(colBase, colBase + 4);
650
+ this.worldMatrix = f32.subarray(wmBase, wmBase + 16);
651
+ this.flags = u32.subarray(
652
+ wasm.flagsPtr / 4 + entityId,
653
+ wasm.flagsPtr / 4 + entityId + 1
654
+ );
655
+ const opts = this.initOptions;
656
+ if (opts.position) {
657
+ this.position[0] = opts.position[0];
658
+ this.position[1] = opts.position[1];
659
+ this.position[2] = opts.position[2];
660
+ }
661
+ if (opts.rotation) {
662
+ this.rotation[0] = opts.rotation[0];
663
+ this.rotation[1] = opts.rotation[1];
664
+ this.rotation[2] = opts.rotation[2];
665
+ }
666
+ if (opts.scale) {
667
+ this.scale[0] = opts.scale[0];
668
+ this.scale[1] = opts.scale[1];
669
+ this.scale[2] = opts.scale[2];
670
+ }
671
+ if (opts.color) {
672
+ this.color[0] = opts.color[0];
673
+ this.color[1] = opts.color[1];
674
+ this.color[2] = opts.color[2];
675
+ this.color[3] = opts.color[3];
676
+ }
677
+ let fl = FLAG_DIRTY | FLAG_VISIBLE;
678
+ if (opts.visible === false) fl &= ~FLAG_VISIBLE;
679
+ if (opts.unlit) fl |= FLAG_UNLIT;
680
+ this.flags[0] = fl;
681
+ u32[wasm.geometryIdsPtr / 4 + entityId] = this.geometryId;
682
+ }
683
+ setDirty() {
684
+ if (this.flags) {
685
+ this.flags[0] |= FLAG_DIRTY;
686
+ }
687
+ }
688
+ get visible() {
689
+ return (this.flags[0] & FLAG_VISIBLE) !== 0;
690
+ }
691
+ set visible(v) {
692
+ if (v) this.flags[0] |= FLAG_VISIBLE;
693
+ else this.flags[0] &= ~FLAG_VISIBLE;
694
+ }
695
+ get unlit() {
696
+ return (this.flags[0] & FLAG_UNLIT) !== 0;
697
+ }
698
+ set unlit(v) {
699
+ if (v) this.flags[0] |= FLAG_UNLIT;
700
+ else this.flags[0] &= ~FLAG_UNLIT;
701
+ }
702
+ /** Update bounding sphere in WASM memory */
703
+ updateBsphere() {
704
+ const f32 = this.wasm.f32;
705
+ const base = this.wasm.bspheresPtr / 4 + this.entityId * 4;
706
+ f32[base] = this.position[0] + this.bsphereCenterOffset[0];
707
+ f32[base + 1] = this.position[1] + this.bsphereCenterOffset[1];
708
+ f32[base + 2] = this.position[2] + this.bsphereCenterOffset[2];
709
+ const maxScale = Math.max(this.scale[0], this.scale[1], this.scale[2]);
710
+ f32[base + 3] = this.bsphereRadius * maxScale;
711
+ }
712
+ };
713
+
714
+ // src/engine/scene.ts
715
+ var FLAG_VISIBLE2 = 2;
716
+ var FLAG_UNLIT2 = 4;
717
+ var Scene = class _Scene {
718
+ wasm;
719
+ renderer;
720
+ camera;
721
+ canvas;
722
+ meshes = [];
723
+ activeCount = 0;
724
+ geometryRegistry = /* @__PURE__ */ new Map();
725
+ nextGeometryId = 0;
726
+ config;
727
+ // Lighting
728
+ lightDir = new Float32Array([0.5, -1, 0.3]);
729
+ lightColor = new Float32Array([1, 1, 1]);
730
+ ambientColor = new Float32Array([0.15, 0.15, 0.15]);
731
+ // Stats
732
+ visibleCount = 0;
733
+ drawCalls = 0;
734
+ // Scratch offsets for frustum planes (6 planes × 4 floats = 96 bytes)
735
+ planesOffset = 0;
736
+ constructor(canvas, config) {
737
+ this.canvas = canvas;
738
+ this.config = config;
739
+ }
740
+ static async create(canvas, config = {}) {
741
+ const scene = new _Scene(canvas, config);
742
+ scene.wasm = await loadWasm();
743
+ scene.renderer = await createRenderer(canvas, config.backend);
744
+ scene.camera = new Camera(scene.wasm.scratchOffset);
745
+ scene.planesOffset = scene.wasm.scratchOffset + 192;
746
+ return scene;
747
+ }
748
+ registerGeometry(geometry) {
749
+ const id = this.nextGeometryId++;
750
+ this.geometryRegistry.set(id, geometry);
751
+ this.renderer.registerGeometry(id, geometry.vertices, geometry.indices);
752
+ return id;
753
+ }
754
+ add(mesh) {
755
+ const entityId = this.activeCount++;
756
+ mesh._bind(this.wasm, entityId);
757
+ this.meshes[entityId] = mesh;
758
+ const geo = this.geometryRegistry.get(mesh.geometryId);
759
+ if (geo) {
760
+ const { center, radius } = computeBoundingSphere(geo.vertices);
761
+ mesh.bsphereRadius = radius;
762
+ mesh.bsphereCenterOffset.set(center);
763
+ mesh.updateBsphere();
764
+ }
765
+ return mesh;
766
+ }
767
+ remove(mesh) {
768
+ if (mesh.entityId < 0) return;
769
+ const lastIdx = this.activeCount - 1;
770
+ if (mesh.entityId !== lastIdx && lastIdx >= 0) {
771
+ const lastMesh = this.meshes[lastIdx];
772
+ if (lastMesh) {
773
+ copyEntityData(this.wasm, lastIdx, mesh.entityId);
774
+ lastMesh._bind(this.wasm, mesh.entityId);
775
+ this.meshes[mesh.entityId] = lastMesh;
776
+ }
777
+ }
778
+ this.meshes[lastIdx] = null;
779
+ mesh.entityId = -1;
780
+ this.activeCount--;
781
+ }
782
+ setDirectionalLight(direction, color) {
783
+ this.lightDir.set(direction);
784
+ this.lightColor.set(color);
785
+ }
786
+ setAmbientLight(color) {
787
+ this.ambientColor.set(color);
788
+ }
789
+ render() {
790
+ const { wasm, renderer, camera, activeCount } = this;
791
+ const aspect = this.canvas.width / this.canvas.height;
792
+ wasm.exports.vc_compute_world_matrices(activeCount);
793
+ camera.update(wasm, aspect);
794
+ for (let i = 0; i < activeCount; i++) {
795
+ this.meshes[i]?.updateBsphere();
796
+ }
797
+ let visibleIndices;
798
+ let visibleCount;
799
+ if (wasm.exports.vc_extract_frustum_planes && wasm.exports.vc_frustum_cull) {
800
+ const vpOffset = wasm.scratchOffset + 128;
801
+ wasm.exports.vc_extract_frustum_planes(vpOffset, this.planesOffset);
802
+ visibleCount = wasm.exports.vc_frustum_cull(
803
+ activeCount,
804
+ this.planesOffset,
805
+ wasm.bspheresPtr,
806
+ wasm.flagsPtr,
807
+ wasm.visibleIndicesPtr
808
+ );
809
+ if (wasm.exports.vc_build_sort_keys && wasm.exports.vc_sort_draw_calls) {
810
+ wasm.exports.vc_build_sort_keys(
811
+ visibleCount,
812
+ wasm.visibleIndicesPtr,
813
+ wasm.geometryIdsPtr,
814
+ wasm.sortKeysPtr
815
+ );
816
+ wasm.exports.vc_sort_draw_calls(
817
+ visibleCount,
818
+ wasm.sortKeysPtr,
819
+ wasm.visibleIndicesPtr
820
+ );
821
+ }
822
+ visibleIndices = [];
823
+ const u32 = wasm.u32;
824
+ const base = wasm.visibleIndicesPtr / 4;
825
+ for (let i = 0; i < visibleCount; i++) {
826
+ visibleIndices.push(u32[base + i]);
827
+ }
828
+ } else {
829
+ visibleIndices = [];
830
+ for (let i = 0; i < activeCount; i++) {
831
+ const flags = wasm.u32[wasm.flagsPtr / 4 + i];
832
+ if (flags & FLAG_VISIBLE2) {
833
+ visibleIndices.push(i);
834
+ }
835
+ }
836
+ visibleCount = visibleIndices.length;
837
+ }
838
+ this.visibleCount = visibleCount;
839
+ const drawEntities = [];
840
+ for (const idx of visibleIndices) {
841
+ const mesh = this.meshes[idx];
842
+ if (!mesh) continue;
843
+ drawEntities.push({
844
+ worldMatrix: mesh.worldMatrix,
845
+ color: mesh.color,
846
+ geometryId: mesh.geometryId,
847
+ unlit: (wasm.u32[wasm.flagsPtr / 4 + idx] & FLAG_UNLIT2) !== 0
848
+ });
849
+ }
850
+ renderer.updateCamera(camera.view, camera.projection);
851
+ renderer.updateLighting(this.lightDir, this.lightColor, this.ambientColor);
852
+ renderer.draw(drawEntities, drawEntities.length);
853
+ this.drawCalls = drawEntities.length;
854
+ wasm.exports.vc_frame_reset();
855
+ }
856
+ resize(width, height) {
857
+ this.canvas.width = width;
858
+ this.canvas.height = height;
859
+ this.renderer.resize(width, height);
860
+ }
861
+ async switchBackend(canvas, backend) {
862
+ this.renderer.destroy();
863
+ this.canvas = canvas;
864
+ this.renderer = await createRenderer(canvas, backend);
865
+ for (const [id, geo] of this.geometryRegistry) {
866
+ this.renderer.registerGeometry(id, geo.vertices, geo.indices);
867
+ }
868
+ }
869
+ destroy() {
870
+ this.renderer.destroy();
871
+ }
872
+ };
873
+ function copyEntityData(wasm, from, to) {
874
+ const { f32, u32 } = wasm;
875
+ const pFrom = wasm.positionsPtr / 4 + from * 3;
876
+ const pTo = wasm.positionsPtr / 4 + to * 3;
877
+ f32.copyWithin(pTo, pFrom, pFrom + 3);
878
+ const rFrom = wasm.eulerRotationsPtr / 4 + from * 3;
879
+ const rTo = wasm.eulerRotationsPtr / 4 + to * 3;
880
+ f32.copyWithin(rTo, rFrom, rFrom + 3);
881
+ const sFrom = wasm.scalesPtr / 4 + from * 3;
882
+ const sTo = wasm.scalesPtr / 4 + to * 3;
883
+ f32.copyWithin(sTo, sFrom, sFrom + 3);
884
+ const wmFrom = wasm.worldMatricesPtr / 4 + from * 16;
885
+ const wmTo = wasm.worldMatricesPtr / 4 + to * 16;
886
+ f32.copyWithin(wmTo, wmFrom, wmFrom + 16);
887
+ const cFrom = wasm.colorsPtr / 4 + from * 4;
888
+ const cTo = wasm.colorsPtr / 4 + to * 4;
889
+ f32.copyWithin(cTo, cFrom, cFrom + 4);
890
+ u32[wasm.flagsPtr / 4 + to] = u32[wasm.flagsPtr / 4 + from];
891
+ const bFrom = wasm.bspheresPtr / 4 + from * 4;
892
+ const bTo = wasm.bspheresPtr / 4 + to * 4;
893
+ f32.copyWithin(bTo, bFrom, bFrom + 4);
894
+ u32[wasm.geometryIdsPtr / 4 + to] = u32[wasm.geometryIdsPtr / 4 + from];
895
+ }
896
+ function computeBoundingSphere(vertices) {
897
+ const stride = 6;
898
+ const count = vertices.length / stride;
899
+ let cx = 0, cy = 0, cz = 0;
900
+ for (let i = 0; i < count; i++) {
901
+ cx += vertices[i * stride];
902
+ cy += vertices[i * stride + 1];
903
+ cz += vertices[i * stride + 2];
904
+ }
905
+ cx /= count;
906
+ cy /= count;
907
+ cz /= count;
908
+ let maxR2 = 0;
909
+ for (let i = 0; i < count; i++) {
910
+ const dx = vertices[i * stride] - cx;
911
+ const dy = vertices[i * stride + 1] - cy;
912
+ const dz = vertices[i * stride + 2] - cz;
913
+ maxR2 = Math.max(maxR2, dx * dx + dy * dy + dz * dz);
914
+ }
915
+ return {
916
+ center: new Float32Array([cx, cy, cz]),
917
+ radius: Math.sqrt(maxR2)
918
+ };
919
+ }
920
+
921
+ // src/engine/geometry.ts
922
+ function createBoxGeometry(w = 1, h = 1, d = 1) {
923
+ const hw = w / 2, hh = h / 2, hd = d / 2;
924
+ const vertices = new Float32Array([
925
+ // +X face
926
+ hw,
927
+ -hh,
928
+ -hd,
929
+ 1,
930
+ 0,
931
+ 0,
932
+ hw,
933
+ hh,
934
+ -hd,
935
+ 1,
936
+ 0,
937
+ 0,
938
+ hw,
939
+ hh,
940
+ hd,
941
+ 1,
942
+ 0,
943
+ 0,
944
+ hw,
945
+ -hh,
946
+ hd,
947
+ 1,
948
+ 0,
949
+ 0,
950
+ // -X face
951
+ -hw,
952
+ -hh,
953
+ hd,
954
+ -1,
955
+ 0,
956
+ 0,
957
+ -hw,
958
+ hh,
959
+ hd,
960
+ -1,
961
+ 0,
962
+ 0,
963
+ -hw,
964
+ hh,
965
+ -hd,
966
+ -1,
967
+ 0,
968
+ 0,
969
+ -hw,
970
+ -hh,
971
+ -hd,
972
+ -1,
973
+ 0,
974
+ 0,
975
+ // +Y face
976
+ -hw,
977
+ hh,
978
+ -hd,
979
+ 0,
980
+ 1,
981
+ 0,
982
+ hw,
983
+ hh,
984
+ -hd,
985
+ 0,
986
+ 1,
987
+ 0,
988
+ hw,
989
+ hh,
990
+ hd,
991
+ 0,
992
+ 1,
993
+ 0,
994
+ // intentional: not -hw
995
+ -hw,
996
+ hh,
997
+ hd,
998
+ 0,
999
+ 1,
1000
+ 0,
1001
+ // -Y face
1002
+ -hw,
1003
+ -hh,
1004
+ hd,
1005
+ 0,
1006
+ -1,
1007
+ 0,
1008
+ hw,
1009
+ -hh,
1010
+ hd,
1011
+ 0,
1012
+ -1,
1013
+ 0,
1014
+ hw,
1015
+ -hh,
1016
+ -hd,
1017
+ 0,
1018
+ -1,
1019
+ 0,
1020
+ -hw,
1021
+ -hh,
1022
+ -hd,
1023
+ 0,
1024
+ -1,
1025
+ 0,
1026
+ // +Z face
1027
+ -hw,
1028
+ -hh,
1029
+ hd,
1030
+ 0,
1031
+ 0,
1032
+ 1,
1033
+ hw,
1034
+ -hh,
1035
+ hd,
1036
+ 0,
1037
+ 0,
1038
+ 1,
1039
+ hw,
1040
+ hh,
1041
+ hd,
1042
+ 0,
1043
+ 0,
1044
+ 1,
1045
+ -hw,
1046
+ hh,
1047
+ hd,
1048
+ 0,
1049
+ 0,
1050
+ 1,
1051
+ // -Z face
1052
+ hw,
1053
+ -hh,
1054
+ -hd,
1055
+ 0,
1056
+ 0,
1057
+ -1,
1058
+ -hw,
1059
+ -hh,
1060
+ -hd,
1061
+ 0,
1062
+ 0,
1063
+ -1,
1064
+ -hw,
1065
+ hh,
1066
+ -hd,
1067
+ 0,
1068
+ 0,
1069
+ -1,
1070
+ hw,
1071
+ hh,
1072
+ -hd,
1073
+ 0,
1074
+ 0,
1075
+ -1
1076
+ ]);
1077
+ const indices = new Uint16Array([
1078
+ 0,
1079
+ 1,
1080
+ 2,
1081
+ 0,
1082
+ 2,
1083
+ 3,
1084
+ // +X
1085
+ 4,
1086
+ 5,
1087
+ 6,
1088
+ 4,
1089
+ 6,
1090
+ 7,
1091
+ // -X
1092
+ 8,
1093
+ 10,
1094
+ 9,
1095
+ 8,
1096
+ 11,
1097
+ 10,
1098
+ // +Y
1099
+ 12,
1100
+ 14,
1101
+ 13,
1102
+ 12,
1103
+ 15,
1104
+ 14,
1105
+ // -Y
1106
+ 16,
1107
+ 17,
1108
+ 18,
1109
+ 16,
1110
+ 18,
1111
+ 19,
1112
+ // +Z
1113
+ 20,
1114
+ 21,
1115
+ 22,
1116
+ 20,
1117
+ 22,
1118
+ 23
1119
+ // -Z
1120
+ ]);
1121
+ return { vertices, indices };
1122
+ }
1123
+ function createSphereGeometry(radius = 0.5, wSegs = 16, hSegs = 12) {
1124
+ const verts = [];
1125
+ const idxs = [];
1126
+ for (let y = 0; y <= hSegs; y++) {
1127
+ const v = y / hSegs;
1128
+ const phi = v * Math.PI;
1129
+ for (let x = 0; x <= wSegs; x++) {
1130
+ const u = x / wSegs;
1131
+ const theta = u * Math.PI * 2;
1132
+ const nx = Math.sin(phi) * Math.cos(theta);
1133
+ const ny = Math.cos(phi);
1134
+ const nz = Math.sin(phi) * Math.sin(theta);
1135
+ verts.push(nx * radius, ny * radius, nz * radius, nx, ny, nz);
1136
+ }
1137
+ }
1138
+ for (let y = 0; y < hSegs; y++) {
1139
+ for (let x = 0; x < wSegs; x++) {
1140
+ const a = y * (wSegs + 1) + x;
1141
+ const b = a + wSegs + 1;
1142
+ idxs.push(a, b, a + 1);
1143
+ idxs.push(b, b + 1, a + 1);
1144
+ }
1145
+ }
1146
+ return {
1147
+ vertices: new Float32Array(verts),
1148
+ indices: new Uint16Array(idxs)
1149
+ };
1150
+ }
1151
+ export {
1152
+ Camera,
1153
+ Mesh,
1154
+ Scene,
1155
+ createBoxGeometry,
1156
+ createRenderer,
1157
+ createSphereGeometry,
1158
+ fragmentShaderGLSL,
1159
+ loadWasm,
1160
+ shaderSource,
1161
+ vertexShaderGLSL
1162
+ };