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/README.md ADDED
@@ -0,0 +1,303 @@
1
+ # V1V2 Engine
2
+
3
+ Engine in development for [Mana Blade](https://manablade.com/).
4
+
5
+ A minimal, high-performance WebGPU/WebGL rendering engine with a Three.js-like API.
6
+
7
+ ## Quickstart
8
+
9
+ ```ts
10
+ import { createScene, Mesh, OrbitControls, Scheduler, cubeVertices, cubeIndices } from '@v1v2/engine'
11
+
12
+ const canvas = document.querySelector('canvas')!
13
+ const scene = await createScene(canvas)
14
+
15
+ // Register geometry
16
+ const cubeGeo = scene.registerGeometry(cubeVertices, cubeIndices)
17
+
18
+ // Add a mesh
19
+ const cube = scene.add(
20
+ new Mesh({
21
+ geometry: cubeGeo,
22
+ position: [0, 0, 0],
23
+ color: [1, 0.3, 0.3],
24
+ }),
25
+ )
26
+
27
+ // Camera
28
+ const orbit = new OrbitControls(canvas)
29
+ scene.camera.fov = Math.PI / 3
30
+ scene.camera.near = 0.1
31
+ scene.camera.far = 1000
32
+
33
+ // Lighting
34
+ scene.setDirectionalLight([-1, -1, -2], [0.5, 0.5, 0.5])
35
+ scene.setAmbientLight([0.8, 0.8, 0.8])
36
+
37
+ // Shadows
38
+ scene.shadow.enabled = true
39
+ scene.shadow.target.set([0, 0, 0])
40
+
41
+ // Scheduler
42
+ const scheduler = new Scheduler(scene)
43
+ scheduler.maxFps = 60
44
+
45
+ scheduler.register(({ dt }) => {
46
+ cube.rotation[2]! += dt
47
+ scene.camera.eye.set(orbit.eye)
48
+ scene.camera.target.set(orbit.target)
49
+ scene.render()
50
+ })
51
+
52
+ scheduler.start()
53
+ ```
54
+
55
+ ## Features
56
+
57
+ - WebGPU with automatic WebGL2 fallback
58
+ - Structure-of-Arrays internals for cache-friendly rendering
59
+ - Frustum culling with bounding spheres
60
+ - Shadow mapping (directional light)
61
+ - Bloom post-processing (5-mip downsample/upsample chain)
62
+ - Outline rendering (MRT-based, configurable thickness/color)
63
+ - GPU skinning with animation crossfade blending
64
+ - Bone attachment system (parent meshes to skeleton bones)
65
+ - BVH-accelerated raycasting (SAH-binned, zero allocation in hot path)
66
+ - GLB/glTF loading with Draco mesh compression
67
+ - KTX2/Basis Universal texture loading
68
+ - HTML overlay (project DOM elements to 3D world positions)
69
+ - Orbit controls (pan, rotate, zoom)
70
+ - Transparent object sorting (back-to-front)
71
+ - Priority-based rAF scheduler with global and per-callback FPS throttling
72
+ - Zero per-frame allocations in the hot path
73
+
74
+ ## API
75
+
76
+ ### Scene
77
+
78
+ ```ts
79
+ const scene = await createScene(canvas, { maxEntities: 10_000, backend: 'webgpu' })
80
+ ```
81
+
82
+ ### Geometry
83
+
84
+ ```ts
85
+ const geo = scene.registerGeometry(vertices, indices)
86
+ const skinnedGeo = scene.registerSkinnedGeometry(vertices, indices, joints, weights)
87
+ const texturedGeo = scene.registerTexturedGeometry(vertices, indices, uvs)
88
+ ```
89
+
90
+ ### Built-in Primitives
91
+
92
+ ```ts
93
+ import { createBoxGeometry, createSphereGeometry, mergeGeometries } from '@v1v2/engine'
94
+
95
+ const box = createBoxGeometry(1, 1, 1)
96
+ const sphere = createSphereGeometry(0.5, 32, 16)
97
+ const merged = mergeGeometries([
98
+ { vertices: box.vertices, indices: box.indices, color: [1, 0, 0] },
99
+ { vertices: sphere.vertices, indices: sphere.indices, color: [0, 0, 1] },
100
+ ])
101
+ ```
102
+
103
+ ### Meshes
104
+
105
+ ```ts
106
+ const mesh = scene.add(
107
+ new Mesh({
108
+ geometry: geo,
109
+ position: [0, 0, 0],
110
+ rotation: [0, 0, 0],
111
+ scale: [1, 1, 1],
112
+ color: [1, 1, 1],
113
+ alpha: 1,
114
+ unlit: false,
115
+ }),
116
+ )
117
+
118
+ mesh.position[0]! += 1 // direct mutation
119
+ mesh.visible = false // hide
120
+ mesh.bloom = 1.5 // emissive glow (0 = off)
121
+ mesh.outline = 1 // outline group (0 = off)
122
+ scene.remove(mesh) // remove from scene
123
+ ```
124
+
125
+ ### Camera
126
+
127
+ ```ts
128
+ scene.camera.fov = Math.PI / 3
129
+ scene.camera.near = 0.1
130
+ scene.camera.far = 5000
131
+ scene.camera.eye.set([0, -10, 5])
132
+ scene.camera.target.set([0, 0, 0])
133
+ scene.camera.up.set([0, 0, 1])
134
+ ```
135
+
136
+ ### Lighting
137
+
138
+ ```ts
139
+ scene.setDirectionalLight([dx, dy, dz], [r, g, b])
140
+ scene.setAmbientLight([r, g, b])
141
+ ```
142
+
143
+ ### Shadows
144
+
145
+ ```ts
146
+ scene.shadow.enabled = true
147
+ scene.shadow.target.set([0, 0, 0])
148
+ scene.shadow.distance = 400
149
+ scene.shadow.extent = 150
150
+ scene.shadow.near = 1
151
+ scene.shadow.far = 800
152
+ scene.shadow.bias = 0.0001
153
+ ```
154
+
155
+ ### Bloom
156
+
157
+ ```ts
158
+ scene.bloom.enabled = true
159
+ scene.bloom.intensity = 1.0
160
+ scene.bloom.threshold = 0.0
161
+ scene.bloom.radius = 1.0
162
+ scene.bloom.whiten = 0.0
163
+ ```
164
+
165
+ ### Outline
166
+
167
+ ```ts
168
+ scene.outline.enabled = true
169
+ scene.outline.thickness = 3
170
+ scene.outline.color = [1, 0.5, 0]
171
+ scene.outline.distanceFactor = 0 // distance-based scaling
172
+ ```
173
+
174
+ ### Raycasting
175
+
176
+ ```ts
177
+ import { createRaycastHit } from '@v1v2/engine'
178
+
179
+ const hit = createRaycastHit() // reusable receiver — no per-frame allocation
180
+ scene.buildBVH(groundMesh.geometry) // optional pre-build (lazy-built on first raycast otherwise)
181
+
182
+ if (scene.raycast(x, y, z + 50, 0, 0, -1, hit, [groundMesh])) {
183
+ player.position[2] = hit.pointZ
184
+ // hit.distance, hit.normalX/Y/Z, hit.faceIndex, hit.mesh
185
+ }
186
+ ```
187
+
188
+ ### GLB/glTF Loading
189
+
190
+ ```ts
191
+ import { loadGlb } from '@v1v2/engine'
192
+
193
+ const glb = await loadGlb('/model.glb', '/draco-1.5.7/')
194
+ // glb.meshes, glb.skins, glb.animations, glb.nodeTransforms
195
+ ```
196
+
197
+ ### Skinning & Animation
198
+
199
+ ```ts
200
+ import { createSkeleton, createSkinInstance, updateSkinInstance, transitionTo } from '@v1v2/engine'
201
+
202
+ const skeleton = createSkeleton(glb.skins[0], glb.nodeTransforms)
203
+ const skin = createSkinInstance(skeleton, 0) // start with clip 0
204
+ scene.skinInstances.push(skin)
205
+
206
+ // In render loop:
207
+ updateSkinInstance(skin, glb.animations, dt)
208
+
209
+ // Crossfade to a new clip:
210
+ transitionTo(skin, 2, 0.2) // blend to clip 2 over 0.2s
211
+ ```
212
+
213
+ ### Bone Attachment
214
+
215
+ ```ts
216
+ scene.attachToBone(weaponMesh, characterMesh, 'Hand.R')
217
+ scene.detachFromBone(weaponMesh)
218
+ ```
219
+
220
+ ### Textures
221
+
222
+ ```ts
223
+ import { loadKTX2 } from '@v1v2/engine'
224
+
225
+ const tex = await loadKTX2('/texture.ktx2', '/basis/')
226
+ const texId = scene.registerTexture(tex.data, tex.width, tex.height)
227
+ const geo = scene.registerTexturedGeometry(vertices, indices, uvs)
228
+ const mesh = scene.add(new Mesh({ geometry: geo, aoMap: texId }))
229
+ ```
230
+
231
+ ### HTML Overlay
232
+
233
+ ```ts
234
+ import { HtmlOverlay, HtmlElement } from '@v1v2/engine'
235
+
236
+ const overlay = new HtmlOverlay(containerDiv)
237
+ const label = overlay.add(new HtmlElement({ position: [0, 0, 5], element: myDiv, distanceFactor: 1 }))
238
+ label.mesh = someMesh // track a mesh
239
+
240
+ // In render loop:
241
+ overlay.update(scene)
242
+ ```
243
+
244
+ ### Rendering
245
+
246
+ ```ts
247
+ scene.render() // sync + draw in one call
248
+ scene.resize(width, height) // resize canvas/backbuffer
249
+ scene.drawCalls // frame draw call count
250
+ ```
251
+
252
+ ### Scheduler
253
+
254
+ Priority-based rAF loop with optional per-callback and global FPS throttling.
255
+
256
+ ```ts
257
+ import { Scheduler } from '@v1v2/engine'
258
+
259
+ const scheduler = new Scheduler(scene)
260
+
261
+ // Global FPS cap — throttles the entire loop (0 = uncapped)
262
+ scheduler.maxFps = 60
263
+
264
+ // Register callbacks with priority ordering (lower = earlier)
265
+ const unsub = scheduler.register(
266
+ ({ scene, dt, elapsed, frame }) => {
267
+ // dt = seconds since last tick (capped at 0.1s)
268
+ player.position[1]! += speed * dt
269
+ },
270
+ { priority: -1 },
271
+ )
272
+
273
+ // Per-callback FPS throttle (independent of global cap)
274
+ scheduler.register(({ scene }) => scene.render(), { priority: 0, fps: 30 })
275
+
276
+ scheduler.start()
277
+ scheduler.stop() // pause (resumable)
278
+ unsub() // remove a callback
279
+ scheduler.destroy() // stop + remove all
280
+ ```
281
+
282
+ ### Backend switching
283
+
284
+ ```ts
285
+ await scene.switchBackend(newCanvas, 'webgl')
286
+ ```
287
+
288
+ ### Cleanup
289
+
290
+ ```ts
291
+ scene.destroy()
292
+ ```
293
+
294
+ ## Coordinate system
295
+
296
+ Right-handed Z-up (Blender convention): +X right, +Y forward, +Z up.
297
+
298
+ ## Development
299
+
300
+ ```bash
301
+ bun install
302
+ bun run dev
303
+ ```
@@ -0,0 +1,158 @@
1
+ interface WasmExports {
2
+ memory: WebAssembly.Memory;
3
+ vc_init: (pages: number) => number;
4
+ vc_perspective: (offset: number, fov: number, aspect: number, near: number, far: number) => void;
5
+ vc_look_at: (offset: number, ex: number, ey: number, ez: number, tx: number, ty: number, tz: number, ux: number, uy: number, uz: number) => void;
6
+ vc_m4_multiply: (out: number, a: number, b: number) => void;
7
+ vc_frame_reset: () => void;
8
+ vc_compute_world_matrices: (count: number) => number;
9
+ vc_get_positions_ptr: () => number;
10
+ vc_get_euler_rotations_ptr: () => number;
11
+ vc_get_scales_ptr: () => number;
12
+ vc_get_world_matrices_ptr: () => number;
13
+ vc_get_colors_ptr: () => number;
14
+ vc_get_flags_ptr: () => number;
15
+ vc_get_bspheres_ptr: () => number;
16
+ vc_get_geometry_ids_ptr: () => number;
17
+ vc_get_sort_keys_ptr: () => number;
18
+ vc_get_visible_indices_ptr: () => number;
19
+ vc_extract_frustum_planes: (vpOffset: number, planesOffset: number) => void;
20
+ vc_frustum_cull: (count: number, planesOffset: number, bspheresOffset: number, flagsOffset: number, outOffset: number) => number;
21
+ vc_build_sort_keys: (count: number, visibleOffset: number, geoIdsOffset: number, keysOutOffset: number) => void;
22
+ vc_sort_draw_calls: (count: number, keysOffset: number, indicesOffset: number) => void;
23
+ }
24
+ interface WasmCore {
25
+ exports: WasmExports;
26
+ memory: WebAssembly.Memory;
27
+ f32: Float32Array;
28
+ u32: Uint32Array;
29
+ u8: Uint8Array;
30
+ scratchOffset: number;
31
+ positionsPtr: number;
32
+ eulerRotationsPtr: number;
33
+ scalesPtr: number;
34
+ worldMatricesPtr: number;
35
+ colorsPtr: number;
36
+ flagsPtr: number;
37
+ bspheresPtr: number;
38
+ geometryIdsPtr: number;
39
+ sortKeysPtr: number;
40
+ visibleIndicesPtr: number;
41
+ }
42
+ declare function loadWasm(): Promise<WasmCore>;
43
+
44
+ interface DrawEntity {
45
+ worldMatrix: Float32Array;
46
+ color: Float32Array;
47
+ geometryId: number;
48
+ unlit: boolean;
49
+ }
50
+ interface Renderer {
51
+ backend: Backend;
52
+ registerGeometry(id: number, vertices: Float32Array, indices: Uint16Array | Uint32Array): void;
53
+ updateCamera(view: Float32Array, projection: Float32Array): void;
54
+ updateLighting(dir: Float32Array, dirColor: Float32Array, ambient: Float32Array): void;
55
+ draw(entities: DrawEntity[], count: number): void;
56
+ resize(width: number, height: number): void;
57
+ destroy(): void;
58
+ }
59
+
60
+ type Backend = 'webgpu' | 'webgl';
61
+ declare function createRenderer(canvas: HTMLCanvasElement, backend?: Backend): Promise<Renderer>;
62
+
63
+ declare class Camera {
64
+ eye: Float32Array<ArrayBuffer>;
65
+ target: Float32Array<ArrayBuffer>;
66
+ up: Float32Array<ArrayBuffer>;
67
+ fov: number;
68
+ near: number;
69
+ far: number;
70
+ view: Float32Array<ArrayBuffer>;
71
+ projection: Float32Array<ArrayBuffer>;
72
+ vp: Float32Array<ArrayBuffer>;
73
+ private viewOffset;
74
+ private projOffset;
75
+ private vpOffset;
76
+ constructor(scratchOffset: number);
77
+ update(wasm: WasmCore, aspect: number): void;
78
+ }
79
+
80
+ interface MeshOptions {
81
+ geometryId: number;
82
+ position?: [number, number, number];
83
+ rotation?: [number, number, number];
84
+ scale?: [number, number, number];
85
+ color?: [number, number, number, number];
86
+ visible?: boolean;
87
+ unlit?: boolean;
88
+ }
89
+ declare class Mesh {
90
+ entityId: number;
91
+ position: Float32Array;
92
+ rotation: Float32Array;
93
+ scale: Float32Array;
94
+ color: Float32Array;
95
+ worldMatrix: Float32Array;
96
+ private wasm;
97
+ private flags;
98
+ geometryId: number;
99
+ private initOptions;
100
+ bsphereRadius: number;
101
+ bsphereCenterOffset: Float32Array<ArrayBuffer>;
102
+ constructor(options: MeshOptions);
103
+ /** Called by Scene when entity is assigned */
104
+ _bind(wasm: WasmCore, entityId: number): void;
105
+ setDirty(): void;
106
+ get visible(): boolean;
107
+ set visible(v: boolean);
108
+ get unlit(): boolean;
109
+ set unlit(v: boolean);
110
+ /** Update bounding sphere in WASM memory */
111
+ updateBsphere(): void;
112
+ }
113
+
114
+ interface Geometry {
115
+ vertices: Float32Array;
116
+ indices: Uint16Array;
117
+ }
118
+ declare function createBoxGeometry(w?: number, h?: number, d?: number): Geometry;
119
+ declare function createSphereGeometry(radius?: number, wSegs?: number, hSegs?: number): Geometry;
120
+
121
+ interface SceneConfig {
122
+ backend?: Backend;
123
+ }
124
+ declare class Scene {
125
+ wasm: WasmCore;
126
+ renderer: Renderer;
127
+ camera: Camera;
128
+ canvas: HTMLCanvasElement;
129
+ private meshes;
130
+ private activeCount;
131
+ private geometryRegistry;
132
+ private nextGeometryId;
133
+ private config;
134
+ private lightDir;
135
+ private lightColor;
136
+ private ambientColor;
137
+ visibleCount: number;
138
+ drawCalls: number;
139
+ private planesOffset;
140
+ private constructor();
141
+ static create(canvas: HTMLCanvasElement, config?: SceneConfig): Promise<Scene>;
142
+ registerGeometry(geometry: Geometry): number;
143
+ add(mesh: Mesh): Mesh;
144
+ remove(mesh: Mesh): void;
145
+ setDirectionalLight(direction: [number, number, number], color: [number, number, number]): void;
146
+ setAmbientLight(color: [number, number, number]): void;
147
+ render(): void;
148
+ resize(width: number, height: number): void;
149
+ switchBackend(canvas: HTMLCanvasElement, backend: Backend): Promise<void>;
150
+ destroy(): void;
151
+ }
152
+
153
+ declare const shaderSource = "\nstruct CameraUniforms {\n view: mat4x4f,\n projection: mat4x4f,\n}\n\nstruct ModelUniforms {\n world: mat4x4f,\n color: vec4f,\n flags: vec4f, // x = unlit\n}\n\nstruct LightUniforms {\n direction: vec4f,\n color: vec4f,\n ambient: vec4f,\n}\n\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n@group(1) @binding(0) var<uniform> model: ModelUniforms;\n@group(2) @binding(0) var<uniform> light: LightUniforms;\n\nstruct VertexInput {\n @location(0) position: vec3f,\n @location(1) normal: vec3f,\n}\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) worldNormal: vec3f,\n @location(1) color: vec4f,\n @location(2) unlit: f32,\n}\n\n@vertex\nfn vs_main(input: VertexInput) -> VertexOutput {\n var output: VertexOutput;\n\n let worldPos = model.world * vec4f(input.position, 1.0);\n output.position = camera.projection * camera.view * worldPos;\n\n // Transform normal by upper 3x3 of world matrix\n let normalMat = mat3x3f(\n model.world[0].xyz,\n model.world[1].xyz,\n model.world[2].xyz,\n );\n output.worldNormal = normalize(normalMat * input.normal);\n output.color = model.color;\n output.unlit = model.flags.x;\n\n return output;\n}\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4f {\n let normal = normalize(input.worldNormal);\n let lightDir = normalize(-light.direction.xyz);\n\n // Lambert diffuse\n let NdotL = max(dot(normal, lightDir), 0.0);\n let diffuse = light.color.rgb * NdotL;\n let ambient = light.ambient.rgb;\n\n var finalColor: vec3f;\n if (input.unlit > 0.5) {\n finalColor = input.color.rgb;\n } else {\n finalColor = input.color.rgb * (diffuse + ambient);\n }\n\n return vec4f(finalColor, input.color.a);\n}\n";
154
+
155
+ declare const vertexShaderGLSL = "#version 300 es\nprecision highp float;\n\nlayout(std140) uniform CameraUniforms {\n mat4 view;\n mat4 projection;\n} camera;\n\nlayout(std140) uniform ModelUniforms {\n mat4 world;\n vec4 color;\n vec4 flags; // x = unlit\n} model;\n\nlayout(location = 0) in vec3 a_position;\nlayout(location = 1) in vec3 a_normal;\n\nout vec3 v_worldNormal;\nout vec4 v_color;\nout float v_unlit;\n\nvoid main() {\n vec4 worldPos = model.world * vec4(a_position, 1.0);\n gl_Position = camera.projection * camera.view * worldPos;\n\n mat3 normalMat = mat3(model.world);\n v_worldNormal = normalize(normalMat * a_normal);\n v_color = model.color;\n v_unlit = model.flags.x;\n}\n";
156
+ declare const fragmentShaderGLSL = "#version 300 es\nprecision highp float;\n\nlayout(std140) uniform LightUniforms {\n vec4 direction;\n vec4 color;\n vec4 ambient;\n} light;\n\nin vec3 v_worldNormal;\nin vec4 v_color;\nin float v_unlit;\n\nout vec4 fragColor;\n\nvoid main() {\n vec3 normal = normalize(v_worldNormal);\n vec3 lightDir = normalize(-light.direction.xyz);\n\n float NdotL = max(dot(normal, lightDir), 0.0);\n vec3 diffuse = light.color.rgb * NdotL;\n vec3 ambient = light.ambient.rgb;\n\n vec3 finalColor;\n if (v_unlit > 0.5) {\n finalColor = v_color.rgb;\n } else {\n finalColor = v_color.rgb * (diffuse + ambient);\n }\n\n fragColor = vec4(finalColor, v_color.a);\n}\n";
157
+
158
+ export { type Backend, Camera, type Geometry, Mesh, type MeshOptions, type Renderer, Scene, type SceneConfig, type WasmCore, type WasmExports, createBoxGeometry, createRenderer, createSphereGeometry, fragmentShaderGLSL, loadWasm, shaderSource, vertexShaderGLSL };