@bornengine/engine 0.4.16
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/LICENSE +21 -0
- package/README.md +231 -0
- package/native/android/Cargo.lock +1848 -0
- package/native/android/Cargo.toml +24 -0
- package/native/android/src/lib.rs +702 -0
- package/native/ios/Cargo.lock +1690 -0
- package/native/ios/Cargo.toml +32 -0
- package/native/ios/src/lib.rs +1267 -0
- package/native/linux/Cargo.lock +3279 -0
- package/native/linux/Cargo.toml +29 -0
- package/native/linux/src/lib.rs +1331 -0
- package/native/macos/Cargo.lock +3310 -0
- package/native/macos/Cargo.toml +46 -0
- package/native/macos/src/lib.rs +1302 -0
- package/native/shared/Cargo.lock +1899 -0
- package/native/shared/Cargo.toml +62 -0
- package/native/shared/assets/default_font.ttf +0 -0
- package/native/shared/build.rs +270 -0
- package/native/shared/shaders/common/clouds.wgsl +122 -0
- package/native/shared/shaders/common/fog.wgsl +16 -0
- package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
- package/native/shared/shaders/common/imposter.wgsl +112 -0
- package/native/shared/shaders/common/pbr.wgsl +186 -0
- package/native/shared/shaders/common/shadows.wgsl +186 -0
- package/native/shared/shaders/common/sky.wgsl +8 -0
- package/native/shared/shaders/common/tonemap.wgsl +25 -0
- package/native/shared/shaders/impulse_field.wgsl +57 -0
- package/native/shared/shaders/material_abi.wgsl +383 -0
- package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
- package/native/shared/src/anim_mixer.rs +61 -0
- package/native/shared/src/attach.rs +263 -0
- package/native/shared/src/audio/decode.rs +123 -0
- package/native/shared/src/audio/mod.rs +863 -0
- package/native/shared/src/audio/render.rs +892 -0
- package/native/shared/src/audio/spsc.rs +156 -0
- package/native/shared/src/audio/stream.rs +226 -0
- package/native/shared/src/custom_shaders.rs +104 -0
- package/native/shared/src/decals.rs +245 -0
- package/native/shared/src/drs.rs +211 -0
- package/native/shared/src/engine.rs +261 -0
- package/native/shared/src/ffi.rs +116 -0
- package/native/shared/src/ffi_core/assets.rs +388 -0
- package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
- package/native/shared/src/ffi_core/draw.rs +334 -0
- package/native/shared/src/ffi_core/game_loop.rs +577 -0
- package/native/shared/src/ffi_core/input.rs +234 -0
- package/native/shared/src/ffi_core/mod.rs +127 -0
- package/native/shared/src/ffi_core/models.rs +1154 -0
- package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
- package/native/shared/src/ffi_core/scene.rs +626 -0
- package/native/shared/src/ffi_core/vfx.rs +212 -0
- package/native/shared/src/ffi_core/visual.rs +691 -0
- package/native/shared/src/frame_callbacks.rs +122 -0
- package/native/shared/src/geometry.rs +236 -0
- package/native/shared/src/handles.rs +182 -0
- package/native/shared/src/input.rs +448 -0
- package/native/shared/src/jolt_sys.rs +822 -0
- package/native/shared/src/lib.rs +55 -0
- package/native/shared/src/models.rs +1093 -0
- package/native/shared/src/models_gltf.rs +1280 -0
- package/native/shared/src/particles.rs +391 -0
- package/native/shared/src/physics_jolt.rs +1908 -0
- package/native/shared/src/picking.rs +298 -0
- package/native/shared/src/postfx.rs +345 -0
- package/native/shared/src/profiler.rs +492 -0
- package/native/shared/src/ragdoll.rs +474 -0
- package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
- package/native/shared/src/renderer/brdf_lut.rs +154 -0
- package/native/shared/src/renderer/draw2d.rs +143 -0
- package/native/shared/src/renderer/formats.rs +822 -0
- package/native/shared/src/renderer/froxel.rs +421 -0
- package/native/shared/src/renderer/gi_bake.rs +653 -0
- package/native/shared/src/renderer/graph.rs +462 -0
- package/native/shared/src/renderer/hiz.rs +269 -0
- package/native/shared/src/renderer/hot_reload.rs +390 -0
- package/native/shared/src/renderer/impulse_field.rs +456 -0
- package/native/shared/src/renderer/lighting.rs +154 -0
- package/native/shared/src/renderer/material_instancing.rs +171 -0
- package/native/shared/src/renderer/material_pipeline.rs +700 -0
- package/native/shared/src/renderer/material_system.rs +1996 -0
- package/native/shared/src/renderer/material_system_tests.rs +601 -0
- package/native/shared/src/renderer/material_system_wasm.rs +41 -0
- package/native/shared/src/renderer/mod.rs +12556 -0
- package/native/shared/src/renderer/model_draw.rs +641 -0
- package/native/shared/src/renderer/occlusion.rs +429 -0
- package/native/shared/src/renderer/planar_pass.rs +593 -0
- package/native/shared/src/renderer/planar_reflection.rs +499 -0
- package/native/shared/src/renderer/post_pass.rs +249 -0
- package/native/shared/src/renderer/postfx_chain.rs +728 -0
- package/native/shared/src/renderer/pt_pass.rs +577 -0
- package/native/shared/src/renderer/scene_pass.rs +607 -0
- package/native/shared/src/renderer/shader_include.rs +205 -0
- package/native/shared/src/renderer/shader_library.rs +135 -0
- package/native/shared/src/renderer/shaders/ao.rs +570 -0
- package/native/shared/src/renderer/shaders/core.rs +1243 -0
- package/native/shared/src/renderer/shaders/env.rs +907 -0
- package/native/shared/src/renderer/shaders/gi.rs +810 -0
- package/native/shared/src/renderer/shaders/mod.rs +19 -0
- package/native/shared/src/renderer/shaders/post.rs +1558 -0
- package/native/shared/src/renderer/shaders/pt.rs +1859 -0
- package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
- package/native/shared/src/renderer/shadow_pass.rs +731 -0
- package/native/shared/src/renderer/ssgi_pass.rs +392 -0
- package/native/shared/src/renderer/ssr_pass.rs +188 -0
- package/native/shared/src/renderer/texture_store.rs +473 -0
- package/native/shared/src/renderer/transient.rs +591 -0
- package/native/shared/src/renderer/types.rs +941 -0
- package/native/shared/src/renderer/util.rs +152 -0
- package/native/shared/src/scene.rs +1362 -0
- package/native/shared/src/sdf_cache.rs +274 -0
- package/native/shared/src/shadows.rs +1036 -0
- package/native/shared/src/staging.rs +102 -0
- package/native/shared/src/string_header.rs +266 -0
- package/native/shared/src/text_renderer.rs +502 -0
- package/native/shared/src/textures.rs +197 -0
- package/native/tvos/Cargo.lock +1693 -0
- package/native/tvos/Cargo.toml +36 -0
- package/native/tvos/metal-patched/Cargo.toml +178 -0
- package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
- package/native/tvos/metal-patched/LICENSE-MIT +25 -0
- package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
- package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
- package/native/tvos/metal-patched/src/argument.rs +366 -0
- package/native/tvos/metal-patched/src/blitpass.rs +102 -0
- package/native/tvos/metal-patched/src/buffer.rs +71 -0
- package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
- package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
- package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
- package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
- package/native/tvos/metal-patched/src/computepass.rs +107 -0
- package/native/tvos/metal-patched/src/constants.rs +152 -0
- package/native/tvos/metal-patched/src/counters.rs +119 -0
- package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
- package/native/tvos/metal-patched/src/device.rs +2134 -0
- package/native/tvos/metal-patched/src/drawable.rs +39 -0
- package/native/tvos/metal-patched/src/encoder.rs +2041 -0
- package/native/tvos/metal-patched/src/heap.rs +281 -0
- package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
- package/native/tvos/metal-patched/src/lib.rs +657 -0
- package/native/tvos/metal-patched/src/library.rs +902 -0
- package/native/tvos/metal-patched/src/mps.rs +575 -0
- package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
- package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
- package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
- package/native/tvos/metal-patched/src/renderpass.rs +443 -0
- package/native/tvos/metal-patched/src/resource.rs +182 -0
- package/native/tvos/metal-patched/src/sampler.rs +165 -0
- package/native/tvos/metal-patched/src/sync.rs +178 -0
- package/native/tvos/metal-patched/src/texture.rs +352 -0
- package/native/tvos/metal-patched/src/types.rs +90 -0
- package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
- package/native/tvos/src/audio_backend.rs +197 -0
- package/native/tvos/src/lib.rs +1891 -0
- package/native/visionos/Cargo.lock +1693 -0
- package/native/visionos/Cargo.toml +40 -0
- package/native/visionos/src/audio_backend.rs +197 -0
- package/native/visionos/src/lib.rs +1887 -0
- package/native/watchos/Cargo.lock +16 -0
- package/native/watchos/Cargo.toml +19 -0
- package/native/watchos/shaders/bloom_postfx.metal +99 -0
- package/native/watchos/src/BloomWatchApp.swift +1267 -0
- package/native/watchos/src/BloomWatchAudio.swift +179 -0
- package/native/watchos/src/audio.rs +55 -0
- package/native/watchos/src/draw_list.rs +229 -0
- package/native/watchos/src/ffi_stubs.rs +915 -0
- package/native/watchos/src/ffi_stubs_manual.rs +35 -0
- package/native/watchos/src/lib.rs +1124 -0
- package/native/watchos/src/models.rs +746 -0
- package/native/watchos/src/postfx.rs +95 -0
- package/native/watchos/src/scene.rs +534 -0
- package/native/watchos/src/textures.rs +184 -0
- package/native/web/Cargo.lock +1657 -0
- package/native/web/Cargo.toml +43 -0
- package/native/web/bloom_glue.js +695 -0
- package/native/web/build.sh +131 -0
- package/native/web/index.html +35 -0
- package/native/web/jolt_bridge.js +1519 -0
- package/native/web/src/input_ffi.rs +286 -0
- package/native/web/src/lib.rs +1796 -0
- package/native/web/src/material_ffi.rs +710 -0
- package/native/web/src/parity_ffi.rs +343 -0
- package/native/web/src/physics_ffi.rs +643 -0
- package/native/web/src/ragdoll_ffi.rs +250 -0
- package/native/web/src/render_settings.rs +98 -0
- package/native/windows/Cargo.lock +1815 -0
- package/native/windows/Cargo.toml +68 -0
- package/native/windows/src/lib.rs +1486 -0
- package/package.json +4279 -0
- package/src/audio/index.ts +315 -0
- package/src/core/colors.ts +63 -0
- package/src/core/index.ts +1206 -0
- package/src/core/keys.ts +63 -0
- package/src/core/types.ts +104 -0
- package/src/index.ts +171 -0
- package/src/math/index.ts +516 -0
- package/src/mobile/index.ts +294 -0
- package/src/models/index.ts +1258 -0
- package/src/physics/index.ts +1134 -0
- package/src/scene/index.ts +698 -0
- package/src/shapes/index.ts +120 -0
- package/src/text/index.ts +48 -0
- package/src/textures/index.ts +187 -0
- package/src/vfx/index.ts +191 -0
- package/src/world/index.ts +24 -0
- package/src/world/loader.ts +423 -0
- package/src/world/prefab.ts +217 -0
- package/src/world/render.ts +172 -0
- package/src/world/saver.ts +108 -0
- package/src/world/serialize.ts +301 -0
- package/src/world/terrain.ts +355 -0
- package/src/world/types.ts +160 -0
- package/src/world/validate.ts +319 -0
- package/src/world/version.ts +114 -0
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
//! Minimal .glb (glTF 2.0 binary) loader for watchOS.
|
|
2
|
+
//!
|
|
3
|
+
//! Supports a single mesh's first primitive — POSITION / NORMAL / TEXCOORD_0 /
|
|
4
|
+
//! indices, plus PBR baseColorFactor / metallicFactor / roughnessFactor from
|
|
5
|
+
//! the referenced material. Multi-primitive meshes, skinning, animations,
|
|
6
|
+
//! and external texture files are intentionally out of scope for this first
|
|
7
|
+
//! cut; they'll be added as concrete games need them.
|
|
8
|
+
//!
|
|
9
|
+
//! Uses a hand-rolled JSON value parser to avoid pulling in the full serde /
|
|
10
|
+
//! serde_json compile tree — the glTF JSON schema is well-formed output from
|
|
11
|
+
//! exporters, so we don't need serde's robustness, just its shape.
|
|
12
|
+
|
|
13
|
+
use std::sync::{Arc, Mutex};
|
|
14
|
+
|
|
15
|
+
// ---------- Model registry ----------
|
|
16
|
+
|
|
17
|
+
/// A Model mirrors glTF's layout: a list of Meshes (each with Primitives),
|
|
18
|
+
/// a list of Nodes (transform + optional mesh reference + children), and
|
|
19
|
+
/// scene roots (node indices the scene starts from). bloom_scene_attach_model
|
|
20
|
+
/// walks the node tree and spawns one bloom scene node per glTF node, with
|
|
21
|
+
/// the glTF node's transform baked in.
|
|
22
|
+
pub struct Model {
|
|
23
|
+
pub meshes: Vec<Mesh>,
|
|
24
|
+
pub nodes: Vec<Node>,
|
|
25
|
+
pub scene_roots: Vec<usize>,
|
|
26
|
+
pub skins: Vec<Skin>,
|
|
27
|
+
pub animations: Vec<Animation>,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// One node in the glTF scene graph. Translation/rotation/scale compose into
|
|
31
|
+
/// the local transform; if `matrix` is set it wins outright (glTF spec).
|
|
32
|
+
pub struct Node {
|
|
33
|
+
pub translation: [f32; 3],
|
|
34
|
+
pub rotation: [f32; 4], // quaternion xyzw
|
|
35
|
+
pub scale: [f32; 3],
|
|
36
|
+
pub matrix: Option<[f32; 16]>, // column-major if present
|
|
37
|
+
pub mesh: Option<usize>,
|
|
38
|
+
pub skin: Option<usize>,
|
|
39
|
+
pub children: Vec<usize>,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
impl Node {
|
|
43
|
+
pub fn identity() -> Self {
|
|
44
|
+
Self {
|
|
45
|
+
translation: [0.0; 3],
|
|
46
|
+
rotation: [0.0, 0.0, 0.0, 1.0],
|
|
47
|
+
scale: [1.0; 3],
|
|
48
|
+
matrix: None,
|
|
49
|
+
mesh: None,
|
|
50
|
+
skin: None,
|
|
51
|
+
children: Vec::new(),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Compose TRS into a column-major 4x4 matrix, or return `matrix` if set.
|
|
56
|
+
pub fn local_transform(&self) -> [f32; 16] {
|
|
57
|
+
if let Some(m) = self.matrix { return m; }
|
|
58
|
+
let [tx, ty, tz] = self.translation;
|
|
59
|
+
let [qx, qy, qz, qw] = self.rotation;
|
|
60
|
+
let [sx, sy, sz] = self.scale;
|
|
61
|
+
// Rotation matrix from quaternion.
|
|
62
|
+
let xx = qx * qx; let yy = qy * qy; let zz = qz * qz;
|
|
63
|
+
let xy = qx * qy; let xz = qx * qz; let yz = qy * qz;
|
|
64
|
+
let wx = qw * qx; let wy = qw * qy; let wz = qw * qz;
|
|
65
|
+
[
|
|
66
|
+
(1.0 - 2.0 * (yy + zz)) * sx,
|
|
67
|
+
(2.0 * (xy + wz)) * sx,
|
|
68
|
+
(2.0 * (xz - wy)) * sx,
|
|
69
|
+
0.0,
|
|
70
|
+
|
|
71
|
+
(2.0 * (xy - wz)) * sy,
|
|
72
|
+
(1.0 - 2.0 * (xx + zz)) * sy,
|
|
73
|
+
(2.0 * (yz + wx)) * sy,
|
|
74
|
+
0.0,
|
|
75
|
+
|
|
76
|
+
(2.0 * (xz + wy)) * sz,
|
|
77
|
+
(2.0 * (yz - wx)) * sz,
|
|
78
|
+
(1.0 - 2.0 * (xx + yy)) * sz,
|
|
79
|
+
0.0,
|
|
80
|
+
|
|
81
|
+
tx, ty, tz, 1.0,
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
pub struct Mesh {
|
|
87
|
+
pub primitives: Vec<Primitive>,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
pub struct Primitive {
|
|
91
|
+
pub positions: Vec<f32>,
|
|
92
|
+
pub normals: Vec<f32>,
|
|
93
|
+
pub uvs: Vec<f32>,
|
|
94
|
+
pub indices: Vec<u32>,
|
|
95
|
+
pub color: [f32; 4],
|
|
96
|
+
pub metallic: f32,
|
|
97
|
+
pub roughness: f32,
|
|
98
|
+
/// PBR texture slots — texture registry handles (0 = none).
|
|
99
|
+
pub tex_base_color: u32,
|
|
100
|
+
pub tex_normal: u32,
|
|
101
|
+
pub tex_metallic_roughness: u32,
|
|
102
|
+
pub tex_emissive: u32,
|
|
103
|
+
pub tex_occlusion: u32,
|
|
104
|
+
/// Per-vertex skinning attributes (glTF JOINTS_0 / WEIGHTS_0). 4 per
|
|
105
|
+
/// vertex; empty if the primitive isn't skinned.
|
|
106
|
+
pub joint_indices: Vec<u32>,
|
|
107
|
+
pub weights: Vec<f32>,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// A glTF skin: list of bone node indices + inverse-bind matrices. Used
|
|
111
|
+
/// alongside a skinned primitive to drive SCNSkinner.
|
|
112
|
+
pub struct Skin {
|
|
113
|
+
pub joints: Vec<usize>, // glTF node indices
|
|
114
|
+
pub inverse_bind_matrices: Vec<[f32; 16]>, // one per joint
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// One glTF animation — an ordered bundle of per-bone keyframe tracks.
|
|
118
|
+
pub struct Animation {
|
|
119
|
+
pub name: String,
|
|
120
|
+
pub duration: f32,
|
|
121
|
+
pub channels: Vec<Channel>,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#[repr(u32)]
|
|
125
|
+
#[derive(Clone, Copy)]
|
|
126
|
+
pub enum ChannelPath { Translation = 0, Rotation = 1, Scale = 2 }
|
|
127
|
+
|
|
128
|
+
pub struct Channel {
|
|
129
|
+
pub target_node: usize, // glTF node index (will be resolved to bloom handle on attach)
|
|
130
|
+
pub path: ChannelPath,
|
|
131
|
+
/// Flat key/value arrays. For Translation/Scale: 3 floats per keyframe.
|
|
132
|
+
/// For Rotation (quaternion xyzw): 4 floats per keyframe.
|
|
133
|
+
pub times: Vec<f32>,
|
|
134
|
+
pub values: Vec<f32>,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static MODELS: Mutex<Vec<Option<Arc<Model>>>> = Mutex::new(Vec::new());
|
|
138
|
+
|
|
139
|
+
pub fn load(path: &str) -> u32 {
|
|
140
|
+
let Ok(bytes) = std::fs::read(path) else { return 0; };
|
|
141
|
+
let Some(model) = parse_glb(&bytes) else { return 0; };
|
|
142
|
+
|
|
143
|
+
let mut reg = MODELS.lock().unwrap();
|
|
144
|
+
if reg.is_empty() { reg.push(None); } // sentinel 0 handle
|
|
145
|
+
reg.push(Some(Arc::new(model)));
|
|
146
|
+
(reg.len() - 1) as u32
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
pub fn get(handle: u32) -> Option<Arc<Model>> {
|
|
150
|
+
let reg = MODELS.lock().unwrap();
|
|
151
|
+
reg.get(handle as usize).and_then(|m| m.as_ref().cloned())
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------- GLB parsing ----------
|
|
155
|
+
|
|
156
|
+
const GLB_MAGIC: u32 = 0x46546C67; // "glTF" LE
|
|
157
|
+
const CHUNK_JSON: u32 = 0x4E4F534A; // "JSON"
|
|
158
|
+
const CHUNK_BIN: u32 = 0x004E4942; // "BIN\0"
|
|
159
|
+
|
|
160
|
+
fn parse_glb(bytes: &[u8]) -> Option<Model> {
|
|
161
|
+
if bytes.len() < 12 { return None; }
|
|
162
|
+
if u32_le(bytes, 0) != GLB_MAGIC { return None; }
|
|
163
|
+
let _version = u32_le(bytes, 4);
|
|
164
|
+
let _total_len = u32_le(bytes, 8);
|
|
165
|
+
|
|
166
|
+
// Walk chunks. Expect JSON first, then BIN.
|
|
167
|
+
let mut off = 12usize;
|
|
168
|
+
let mut json_bytes: &[u8] = &[];
|
|
169
|
+
let mut bin_bytes: &[u8] = &[];
|
|
170
|
+
while off + 8 <= bytes.len() {
|
|
171
|
+
let chunk_len = u32_le(bytes, off) as usize;
|
|
172
|
+
let chunk_type = u32_le(bytes, off + 4);
|
|
173
|
+
let data_start = off + 8;
|
|
174
|
+
let data_end = data_start + chunk_len;
|
|
175
|
+
if data_end > bytes.len() { break; }
|
|
176
|
+
let data = &bytes[data_start..data_end];
|
|
177
|
+
match chunk_type {
|
|
178
|
+
CHUNK_JSON => json_bytes = data,
|
|
179
|
+
CHUNK_BIN => bin_bytes = data,
|
|
180
|
+
_ => {}
|
|
181
|
+
}
|
|
182
|
+
off = data_end;
|
|
183
|
+
}
|
|
184
|
+
if json_bytes.is_empty() { return None; }
|
|
185
|
+
|
|
186
|
+
let json = std::str::from_utf8(json_bytes).ok()?;
|
|
187
|
+
let root = json_parse(json)?;
|
|
188
|
+
|
|
189
|
+
let meshes_js = root.get("meshes")?.arr()?;
|
|
190
|
+
let accessors = root.get("accessors")?.arr()?;
|
|
191
|
+
let buf_views = root.get("bufferViews")?.arr()?;
|
|
192
|
+
|
|
193
|
+
let mut meshes: Vec<Mesh> = Vec::with_capacity(meshes_js.len());
|
|
194
|
+
for mesh_js in meshes_js {
|
|
195
|
+
let prims_js = match mesh_js.get("primitives").and_then(|v| v.arr()) {
|
|
196
|
+
Some(p) => p,
|
|
197
|
+
None => continue,
|
|
198
|
+
};
|
|
199
|
+
let mut primitives: Vec<Primitive> = Vec::with_capacity(prims_js.len());
|
|
200
|
+
for prim in prims_js {
|
|
201
|
+
if let Some(p) = parse_primitive(prim, &root, accessors, buf_views, bin_bytes) {
|
|
202
|
+
primitives.push(p);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
meshes.push(Mesh { primitives });
|
|
206
|
+
}
|
|
207
|
+
if meshes.is_empty() { return None; }
|
|
208
|
+
|
|
209
|
+
// Parse nodes + scenes (glTF hierarchy).
|
|
210
|
+
let mut nodes: Vec<Node> = Vec::new();
|
|
211
|
+
if let Some(nodes_js) = root.get("nodes").and_then(|v| v.arr()) {
|
|
212
|
+
nodes.reserve(nodes_js.len());
|
|
213
|
+
for n in nodes_js {
|
|
214
|
+
nodes.push(parse_node(n));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
let mut scene_roots: Vec<usize> = Vec::new();
|
|
218
|
+
if let Some(scenes_js) = root.get("scenes").and_then(|v| v.arr()) {
|
|
219
|
+
// Pick the default scene (glTF's "scene" field) or scene 0.
|
|
220
|
+
let default_idx = root.get("scene").and_then(|v| v.num()).unwrap_or(0.0) as usize;
|
|
221
|
+
if let Some(scene) = scenes_js.get(default_idx).or_else(|| scenes_js.first()) {
|
|
222
|
+
if let Some(arr) = scene.get("nodes").and_then(|v| v.arr()) {
|
|
223
|
+
for n in arr {
|
|
224
|
+
if let Some(i) = n.num() { scene_roots.push(i as usize); }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Fallback: if the file has no scenes/ but has nodes, treat node 0 as root.
|
|
230
|
+
if scene_roots.is_empty() && !nodes.is_empty() {
|
|
231
|
+
scene_roots.push(0);
|
|
232
|
+
}
|
|
233
|
+
// Fallback: if no nodes at all but meshes exist, synthesize a single root
|
|
234
|
+
// node pointing at mesh 0 so older callers still get a valid hierarchy.
|
|
235
|
+
if nodes.is_empty() && !meshes.is_empty() {
|
|
236
|
+
let mut n = Node::identity();
|
|
237
|
+
n.mesh = Some(0);
|
|
238
|
+
nodes.push(n);
|
|
239
|
+
scene_roots.push(0);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Parse skins: joints (node indices) + inverse-bind matrices.
|
|
243
|
+
let mut skins: Vec<Skin> = Vec::new();
|
|
244
|
+
if let Some(skins_js) = root.get("skins").and_then(|v| v.arr()) {
|
|
245
|
+
skins.reserve(skins_js.len());
|
|
246
|
+
for sk in skins_js {
|
|
247
|
+
let mut joints: Vec<usize> = Vec::new();
|
|
248
|
+
if let Some(j) = sk.get("joints").and_then(|v| v.arr()) {
|
|
249
|
+
for v in j {
|
|
250
|
+
if let Some(n) = v.num() { joints.push(n as usize); }
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
let mut ibms: Vec<[f32; 16]> = Vec::new();
|
|
254
|
+
if let Some(ibm_acc_idx) = sk.get("inverseBindMatrices").and_then(|v| v.num()) {
|
|
255
|
+
let raw = read_f32_vec(accessors, buf_views, bin_bytes, ibm_acc_idx as usize, 16)
|
|
256
|
+
.unwrap_or_default();
|
|
257
|
+
for chunk in raw.chunks(16) {
|
|
258
|
+
if chunk.len() == 16 {
|
|
259
|
+
let mut m = [0.0f32; 16];
|
|
260
|
+
m.copy_from_slice(chunk);
|
|
261
|
+
ibms.push(m);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// If IBMs are missing, fall back to identity per joint (spec
|
|
266
|
+
// default). Happens rarely but keeps Swift's SCNSkinner happy.
|
|
267
|
+
if ibms.is_empty() {
|
|
268
|
+
let mut id = [0.0f32; 16];
|
|
269
|
+
id[0] = 1.0; id[5] = 1.0; id[10] = 1.0; id[15] = 1.0;
|
|
270
|
+
ibms = vec![id; joints.len()];
|
|
271
|
+
}
|
|
272
|
+
skins.push(Skin { joints, inverse_bind_matrices: ibms });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Parse animations.
|
|
277
|
+
let mut animations: Vec<Animation> = Vec::new();
|
|
278
|
+
if let Some(anims_js) = root.get("animations").and_then(|v| v.arr()) {
|
|
279
|
+
for a in anims_js {
|
|
280
|
+
let name = a.get("name").and_then(|v| match v { JVal::Str(s) => Some(s.clone()), _ => None })
|
|
281
|
+
.unwrap_or_default();
|
|
282
|
+
let samplers = match a.get("samplers").and_then(|v| v.arr()) {
|
|
283
|
+
Some(s) => s,
|
|
284
|
+
None => continue,
|
|
285
|
+
};
|
|
286
|
+
let channels_js = match a.get("channels").and_then(|v| v.arr()) {
|
|
287
|
+
Some(c) => c,
|
|
288
|
+
None => continue,
|
|
289
|
+
};
|
|
290
|
+
let mut channels: Vec<Channel> = Vec::new();
|
|
291
|
+
let mut duration = 0.0f32;
|
|
292
|
+
for ch in channels_js {
|
|
293
|
+
let Some(smp_idx) = ch.get("sampler").and_then(|v| v.num()) else { continue };
|
|
294
|
+
let Some(tgt) = ch.get("target") else { continue };
|
|
295
|
+
let Some(tgt_node) = tgt.get("node").and_then(|v| v.num()) else { continue };
|
|
296
|
+
let path_str = tgt.get("path").and_then(|v| match v { JVal::Str(s) => Some(s.as_str()), _ => None });
|
|
297
|
+
let path = match path_str {
|
|
298
|
+
Some("translation") => ChannelPath::Translation,
|
|
299
|
+
Some("rotation") => ChannelPath::Rotation,
|
|
300
|
+
Some("scale") => ChannelPath::Scale,
|
|
301
|
+
_ => continue, // 'weights' (morph targets) not supported yet
|
|
302
|
+
};
|
|
303
|
+
let Some(smp) = samplers.get(smp_idx as usize) else { continue };
|
|
304
|
+
let Some(input_idx) = smp.get("input").and_then(|v| v.num()) else { continue };
|
|
305
|
+
let Some(output_idx) = smp.get("output").and_then(|v| v.num()) else { continue };
|
|
306
|
+
|
|
307
|
+
let times = read_f32_vec(accessors, buf_views, bin_bytes, input_idx as usize, 1)
|
|
308
|
+
.unwrap_or_default();
|
|
309
|
+
let comps = match path { ChannelPath::Rotation => 4, _ => 3 };
|
|
310
|
+
let values = read_f32_vec(accessors, buf_views, bin_bytes, output_idx as usize, comps)
|
|
311
|
+
.unwrap_or_default();
|
|
312
|
+
|
|
313
|
+
if let Some(&t) = times.last() { if t > duration { duration = t; } }
|
|
314
|
+
channels.push(Channel { target_node: tgt_node as usize, path, times, values });
|
|
315
|
+
}
|
|
316
|
+
animations.push(Animation { name, duration, channels });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
Some(Model { meshes, nodes, scene_roots, skins, animations })
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
fn parse_node(n: &JVal) -> Node {
|
|
324
|
+
let mut node = Node::identity();
|
|
325
|
+
if let Some(t) = n.get("translation").and_then(|v| v.arr()) {
|
|
326
|
+
for (i, v) in t.iter().take(3).enumerate() {
|
|
327
|
+
if let Some(f) = v.num() { node.translation[i] = f as f32; }
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if let Some(r) = n.get("rotation").and_then(|v| v.arr()) {
|
|
331
|
+
for (i, v) in r.iter().take(4).enumerate() {
|
|
332
|
+
if let Some(f) = v.num() { node.rotation[i] = f as f32; }
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if let Some(s) = n.get("scale").and_then(|v| v.arr()) {
|
|
336
|
+
for (i, v) in s.iter().take(3).enumerate() {
|
|
337
|
+
if let Some(f) = v.num() { node.scale[i] = f as f32; }
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if let Some(m) = n.get("matrix").and_then(|v| v.arr()) {
|
|
341
|
+
let mut arr = [0.0f32; 16];
|
|
342
|
+
for (i, v) in m.iter().take(16).enumerate() {
|
|
343
|
+
if let Some(f) = v.num() { arr[i] = f as f32; }
|
|
344
|
+
}
|
|
345
|
+
node.matrix = Some(arr);
|
|
346
|
+
}
|
|
347
|
+
if let Some(mi) = n.get("mesh").and_then(|v| v.num()) {
|
|
348
|
+
node.mesh = Some(mi as usize);
|
|
349
|
+
}
|
|
350
|
+
if let Some(si) = n.get("skin").and_then(|v| v.num()) {
|
|
351
|
+
node.skin = Some(si as usize);
|
|
352
|
+
}
|
|
353
|
+
if let Some(kids) = n.get("children").and_then(|v| v.arr()) {
|
|
354
|
+
for v in kids {
|
|
355
|
+
if let Some(i) = v.num() { node.children.push(i as usize); }
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
node
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
fn parse_primitive(
|
|
362
|
+
prim: &JVal, root: &JVal,
|
|
363
|
+
accessors: &[JVal], buf_views: &[JVal], bin_bytes: &[u8],
|
|
364
|
+
) -> Option<Primitive> {
|
|
365
|
+
let attrs = prim.get("attributes")?;
|
|
366
|
+
let pos_acc = attrs.get("POSITION")?.num()? as usize;
|
|
367
|
+
let nrm_acc = attrs.get("NORMAL").and_then(|v| v.num().map(|n| n as usize));
|
|
368
|
+
let uv_acc = attrs.get("TEXCOORD_0").and_then(|v| v.num().map(|n| n as usize));
|
|
369
|
+
let j0_acc = attrs.get("JOINTS_0").and_then(|v| v.num().map(|n| n as usize));
|
|
370
|
+
let w0_acc = attrs.get("WEIGHTS_0").and_then(|v| v.num().map(|n| n as usize));
|
|
371
|
+
let idx_acc = prim.get("indices").and_then(|v| v.num().map(|n| n as usize));
|
|
372
|
+
let mat_idx = prim.get("material").and_then(|v| v.num().map(|n| n as usize));
|
|
373
|
+
|
|
374
|
+
let positions = read_f32_vec(accessors, buf_views, bin_bytes, pos_acc, 3)?;
|
|
375
|
+
let normals = nrm_acc
|
|
376
|
+
.and_then(|a| read_f32_vec(accessors, buf_views, bin_bytes, a, 3))
|
|
377
|
+
.unwrap_or_default();
|
|
378
|
+
let uvs = uv_acc
|
|
379
|
+
.and_then(|a| read_f32_vec(accessors, buf_views, bin_bytes, a, 2))
|
|
380
|
+
.unwrap_or_default();
|
|
381
|
+
// Indexless primitives (POSITION-only triangle soups) — synthesize
|
|
382
|
+
// sequential indices so the SceneKit SCNGeometryElement path stays
|
|
383
|
+
// uniform. Fox.glb uses this layout.
|
|
384
|
+
let indices = match idx_acc {
|
|
385
|
+
Some(a) => read_index_vec(accessors, buf_views, bin_bytes, a)?,
|
|
386
|
+
None => (0..(positions.len() / 3) as u32).collect(),
|
|
387
|
+
};
|
|
388
|
+
// JOINTS_0 is typically unsigned byte or short vec4 per vertex.
|
|
389
|
+
let joint_indices = j0_acc
|
|
390
|
+
.and_then(|a| read_joint_indices(accessors, buf_views, bin_bytes, a))
|
|
391
|
+
.unwrap_or_default();
|
|
392
|
+
// WEIGHTS_0 is f32 vec4 per vertex (spec allows normalized u8/u16 but
|
|
393
|
+
// current Khronos samples use float; stick with float for v1).
|
|
394
|
+
let weights = w0_acc
|
|
395
|
+
.and_then(|a| read_f32_vec(accessors, buf_views, bin_bytes, a, 4))
|
|
396
|
+
.unwrap_or_default();
|
|
397
|
+
|
|
398
|
+
let mut color = [1.0f32; 4];
|
|
399
|
+
let mut metallic = 1.0f32;
|
|
400
|
+
let mut roughness = 1.0f32;
|
|
401
|
+
let mut tex_base_color = 0u32;
|
|
402
|
+
let mut tex_normal = 0u32;
|
|
403
|
+
let mut tex_metallic_roughness = 0u32;
|
|
404
|
+
let mut tex_emissive = 0u32;
|
|
405
|
+
let mut tex_occlusion = 0u32;
|
|
406
|
+
|
|
407
|
+
if let Some(mi) = mat_idx {
|
|
408
|
+
if let Some(mats) = root.get("materials").and_then(|v| v.arr()) {
|
|
409
|
+
if let Some(mat) = mats.get(mi) {
|
|
410
|
+
if let Some(pbr) = mat.get("pbrMetallicRoughness") {
|
|
411
|
+
if let Some(bc) = pbr.get("baseColorFactor").and_then(|v| v.arr()) {
|
|
412
|
+
for (i, n) in bc.iter().take(4).enumerate() {
|
|
413
|
+
if let Some(f) = n.num() { color[i] = f as f32; }
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if let Some(m) = pbr.get("metallicFactor").and_then(|v| v.num()) {
|
|
417
|
+
metallic = m as f32;
|
|
418
|
+
}
|
|
419
|
+
if let Some(r) = pbr.get("roughnessFactor").and_then(|v| v.num()) {
|
|
420
|
+
roughness = r as f32;
|
|
421
|
+
}
|
|
422
|
+
tex_base_color = resolve_texture(root, pbr, "baseColorTexture", bin_bytes);
|
|
423
|
+
tex_metallic_roughness = resolve_texture(root, pbr, "metallicRoughnessTexture", bin_bytes);
|
|
424
|
+
}
|
|
425
|
+
tex_normal = resolve_texture(root, mat, "normalTexture", bin_bytes);
|
|
426
|
+
tex_emissive = resolve_texture(root, mat, "emissiveTexture", bin_bytes);
|
|
427
|
+
tex_occlusion = resolve_texture(root, mat, "occlusionTexture", bin_bytes);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
Some(Primitive {
|
|
433
|
+
positions, normals, uvs, indices,
|
|
434
|
+
color, metallic, roughness,
|
|
435
|
+
tex_base_color, tex_normal, tex_metallic_roughness, tex_emissive, tex_occlusion,
|
|
436
|
+
joint_indices, weights,
|
|
437
|
+
})
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/// Read a JOINTS_0 accessor as u32 quads. Handles componentType 5121
|
|
441
|
+
/// (UNSIGNED_BYTE) and 5123 (UNSIGNED_SHORT); returns empty for anything
|
|
442
|
+
/// else.
|
|
443
|
+
fn read_joint_indices(
|
|
444
|
+
accessors: &[JVal], buf_views: &[JVal], bin: &[u8], acc_idx: usize,
|
|
445
|
+
) -> Option<Vec<u32>> {
|
|
446
|
+
let acc = accessors.get(acc_idx)?;
|
|
447
|
+
let comp_type = acc.get("componentType")?.num()? as u32;
|
|
448
|
+
let bv_idx = acc.get("bufferView")?.num()? as usize;
|
|
449
|
+
let count = acc.get("count")?.num()? as usize;
|
|
450
|
+
let byte_off_acc = acc.get("byteOffset").and_then(|v| v.num()).unwrap_or(0.0) as usize;
|
|
451
|
+
let bv = buf_views.get(bv_idx)?;
|
|
452
|
+
let byte_off_bv = bv.get("byteOffset").and_then(|v| v.num()).unwrap_or(0.0) as usize;
|
|
453
|
+
let base = byte_off_bv + byte_off_acc;
|
|
454
|
+
|
|
455
|
+
let stride_per_element = match comp_type {
|
|
456
|
+
5121 => 1, // u8
|
|
457
|
+
5123 => 2, // u16
|
|
458
|
+
_ => return None,
|
|
459
|
+
};
|
|
460
|
+
let total_bytes = count * 4 * stride_per_element;
|
|
461
|
+
if base + total_bytes > bin.len() { return None; }
|
|
462
|
+
|
|
463
|
+
let mut out = Vec::with_capacity(count * 4);
|
|
464
|
+
for i in 0..(count * 4) {
|
|
465
|
+
let o = base + i * stride_per_element;
|
|
466
|
+
let v = match comp_type {
|
|
467
|
+
5121 => bin[o] as u32,
|
|
468
|
+
5123 => u16::from_le_bytes([bin[o], bin[o + 1]]) as u32,
|
|
469
|
+
_ => return None,
|
|
470
|
+
};
|
|
471
|
+
out.push(v);
|
|
472
|
+
}
|
|
473
|
+
Some(out)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/// Chase a material.<slotName>.index → textures[i].source → images[j]
|
|
477
|
+
/// → bufferViews[k] → BIN bytes, register via crate::textures. Returns the
|
|
478
|
+
/// texture handle or 0 on any missing link.
|
|
479
|
+
fn resolve_texture(root: &JVal, parent: &JVal, slot_name: &str, bin: &[u8]) -> u32 {
|
|
480
|
+
let Some(slot) = parent.get(slot_name) else { return 0 };
|
|
481
|
+
let Some(tex_idx) = slot.get("index").and_then(|v| v.num()) else { return 0 };
|
|
482
|
+
let Some(textures) = root.get("textures").and_then(|v| v.arr()) else { return 0 };
|
|
483
|
+
let Some(tex) = textures.get(tex_idx as usize) else { return 0 };
|
|
484
|
+
let Some(src_idx) = tex.get("source").and_then(|v| v.num()) else { return 0 };
|
|
485
|
+
let Some(images) = root.get("images").and_then(|v| v.arr()) else { return 0 };
|
|
486
|
+
let Some(img) = images.get(src_idx as usize) else { return 0 };
|
|
487
|
+
let Some(bv_idx) = img.get("bufferView").and_then(|v| v.num()) else { return 0 };
|
|
488
|
+
let Some(buf_views) = root.get("bufferViews").and_then(|v| v.arr()) else { return 0 };
|
|
489
|
+
let Some(bv) = buf_views.get(bv_idx as usize) else { return 0 };
|
|
490
|
+
|
|
491
|
+
let off = bv.get("byteOffset").and_then(|v| v.num()).unwrap_or(0.0) as usize;
|
|
492
|
+
let len = bv.get("byteLength").and_then(|v| v.num()).unwrap_or(0.0) as usize;
|
|
493
|
+
if off + len > bin.len() { return 0; }
|
|
494
|
+
crate::textures::register_bytes(&bin[off..off + len])
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
fn u32_le(b: &[u8], o: usize) -> u32 {
|
|
498
|
+
u32::from_le_bytes([b[o], b[o+1], b[o+2], b[o+3]])
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/// Resolve accessor → raw float slice from the bin chunk. `components` is
|
|
502
|
+
/// the expected vector width (3 for position/normal, 2 for uv). glTF stores
|
|
503
|
+
/// accessor.componentType = 5126 (FLOAT) for these.
|
|
504
|
+
fn read_f32_vec(
|
|
505
|
+
accessors: &[JVal], buf_views: &[JVal], bin: &[u8],
|
|
506
|
+
acc_idx: usize, components: usize,
|
|
507
|
+
) -> Option<Vec<f32>> {
|
|
508
|
+
let acc = accessors.get(acc_idx)?;
|
|
509
|
+
let bv_idx = acc.get("bufferView")?.num()? as usize;
|
|
510
|
+
let count = acc.get("count")?.num()? as usize;
|
|
511
|
+
let byte_off_acc = acc.get("byteOffset").and_then(|v| v.num()).unwrap_or(0.0) as usize;
|
|
512
|
+
|
|
513
|
+
let bv = buf_views.get(bv_idx)?;
|
|
514
|
+
let byte_off_bv = bv.get("byteOffset").and_then(|v| v.num()).unwrap_or(0.0) as usize;
|
|
515
|
+
let stride = bv.get("byteStride").and_then(|v| v.num()).map(|n| n as usize)
|
|
516
|
+
.unwrap_or(components * 4);
|
|
517
|
+
|
|
518
|
+
let mut out = Vec::with_capacity(count * components);
|
|
519
|
+
for i in 0..count {
|
|
520
|
+
let base = byte_off_bv + byte_off_acc + i * stride;
|
|
521
|
+
if base + components * 4 > bin.len() { return None; }
|
|
522
|
+
for c in 0..components {
|
|
523
|
+
let b = base + c * 4;
|
|
524
|
+
out.push(f32::from_le_bytes([bin[b], bin[b+1], bin[b+2], bin[b+3]]));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
Some(out)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/// Indices — componentType 5121 (u8), 5123 (u16), or 5125 (u32). Always
|
|
531
|
+
/// returned as u32.
|
|
532
|
+
fn read_index_vec(
|
|
533
|
+
accessors: &[JVal], buf_views: &[JVal], bin: &[u8], acc_idx: usize,
|
|
534
|
+
) -> Option<Vec<u32>> {
|
|
535
|
+
let acc = accessors.get(acc_idx)?;
|
|
536
|
+
let comp_type = acc.get("componentType")?.num()? as u32;
|
|
537
|
+
let bv_idx = acc.get("bufferView")?.num()? as usize;
|
|
538
|
+
let count = acc.get("count")?.num()? as usize;
|
|
539
|
+
let byte_off_acc = acc.get("byteOffset").and_then(|v| v.num()).unwrap_or(0.0) as usize;
|
|
540
|
+
|
|
541
|
+
let bv = buf_views.get(bv_idx)?;
|
|
542
|
+
let byte_off_bv = bv.get("byteOffset").and_then(|v| v.num()).unwrap_or(0.0) as usize;
|
|
543
|
+
let base = byte_off_bv + byte_off_acc;
|
|
544
|
+
|
|
545
|
+
let mut out = Vec::with_capacity(count);
|
|
546
|
+
match comp_type {
|
|
547
|
+
5121 => { // u8
|
|
548
|
+
if base + count > bin.len() { return None; }
|
|
549
|
+
for i in 0..count { out.push(bin[base + i] as u32); }
|
|
550
|
+
}
|
|
551
|
+
5123 => { // u16
|
|
552
|
+
if base + count * 2 > bin.len() { return None; }
|
|
553
|
+
for i in 0..count {
|
|
554
|
+
let o = base + i * 2;
|
|
555
|
+
out.push(u16::from_le_bytes([bin[o], bin[o+1]]) as u32);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
5125 => { // u32
|
|
559
|
+
if base + count * 4 > bin.len() { return None; }
|
|
560
|
+
for i in 0..count {
|
|
561
|
+
let o = base + i * 4;
|
|
562
|
+
out.push(u32::from_le_bytes([bin[o], bin[o+1], bin[o+2], bin[o+3]]));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
_ => return None,
|
|
566
|
+
}
|
|
567
|
+
Some(out)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ---------- Tiny JSON value parser ----------
|
|
571
|
+
//
|
|
572
|
+
// Only what we need for glTF: objects, arrays, numbers, strings. No booleans,
|
|
573
|
+
// null, escapes, or unicode. Exporter output tends to be well-formed.
|
|
574
|
+
|
|
575
|
+
#[derive(Debug, Clone)]
|
|
576
|
+
pub enum JVal {
|
|
577
|
+
Obj(Vec<(String, JVal)>),
|
|
578
|
+
Arr(Vec<JVal>),
|
|
579
|
+
Num(f64),
|
|
580
|
+
Str(String),
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
impl JVal {
|
|
584
|
+
pub fn get(&self, key: &str) -> Option<&JVal> {
|
|
585
|
+
match self {
|
|
586
|
+
JVal::Obj(v) => v.iter().find(|(k, _)| k == key).map(|(_, v)| v),
|
|
587
|
+
_ => None,
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
pub fn arr(&self) -> Option<&[JVal]> {
|
|
591
|
+
if let JVal::Arr(a) = self { Some(a.as_slice()) } else { None }
|
|
592
|
+
}
|
|
593
|
+
pub fn num(&self) -> Option<f64> {
|
|
594
|
+
if let JVal::Num(n) = self { Some(*n) } else { None }
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
struct P<'a> { bytes: &'a [u8], pos: usize }
|
|
599
|
+
|
|
600
|
+
impl<'a> P<'a> {
|
|
601
|
+
fn peek(&self) -> Option<u8> { self.bytes.get(self.pos).copied() }
|
|
602
|
+
fn bump(&mut self) -> Option<u8> {
|
|
603
|
+
let c = self.peek()?; self.pos += 1; Some(c)
|
|
604
|
+
}
|
|
605
|
+
fn skip_ws(&mut self) {
|
|
606
|
+
while let Some(c) = self.peek() {
|
|
607
|
+
if c == b' ' || c == b'\t' || c == b'\n' || c == b'\r' { self.pos += 1; }
|
|
608
|
+
else { break; }
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
fn val(&mut self) -> Option<JVal> {
|
|
612
|
+
self.skip_ws();
|
|
613
|
+
match self.peek()? {
|
|
614
|
+
b'{' => self.obj(),
|
|
615
|
+
b'[' => self.arr(),
|
|
616
|
+
b'"' => self.string().map(JVal::Str),
|
|
617
|
+
b'-' | b'0'..=b'9' => self.number().map(JVal::Num),
|
|
618
|
+
b't' => { for c in b"true" { if self.bump() != Some(*c) { return None; } } Some(JVal::Num(1.0)) }
|
|
619
|
+
b'f' => { for c in b"false" { if self.bump() != Some(*c) { return None; } } Some(JVal::Num(0.0)) }
|
|
620
|
+
b'n' => { for c in b"null" { if self.bump() != Some(*c) { return None; } } Some(JVal::Num(0.0)) }
|
|
621
|
+
_ => None,
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
fn obj(&mut self) -> Option<JVal> {
|
|
625
|
+
if self.bump() != Some(b'{') { return None; }
|
|
626
|
+
let mut kv = Vec::new();
|
|
627
|
+
loop {
|
|
628
|
+
self.skip_ws();
|
|
629
|
+
match self.peek()? {
|
|
630
|
+
b'}' => { self.bump(); return Some(JVal::Obj(kv)); }
|
|
631
|
+
b',' => { self.bump(); continue; }
|
|
632
|
+
b'"' => {
|
|
633
|
+
let key = self.string()?;
|
|
634
|
+
self.skip_ws();
|
|
635
|
+
if self.bump() != Some(b':') { return None; }
|
|
636
|
+
let v = self.val()?;
|
|
637
|
+
kv.push((key, v));
|
|
638
|
+
}
|
|
639
|
+
_ => return None,
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
fn arr(&mut self) -> Option<JVal> {
|
|
644
|
+
if self.bump() != Some(b'[') { return None; }
|
|
645
|
+
let mut items = Vec::new();
|
|
646
|
+
loop {
|
|
647
|
+
self.skip_ws();
|
|
648
|
+
match self.peek()? {
|
|
649
|
+
b']' => { self.bump(); return Some(JVal::Arr(items)); }
|
|
650
|
+
b',' => { self.bump(); continue; }
|
|
651
|
+
_ => items.push(self.val()?),
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
fn string(&mut self) -> Option<String> {
|
|
656
|
+
if self.bump() != Some(b'"') { return None; }
|
|
657
|
+
let start = self.pos;
|
|
658
|
+
while let Some(c) = self.peek() {
|
|
659
|
+
if c == b'"' {
|
|
660
|
+
let s = std::str::from_utf8(&self.bytes[start..self.pos]).ok()?.to_string();
|
|
661
|
+
self.bump();
|
|
662
|
+
return Some(s);
|
|
663
|
+
}
|
|
664
|
+
if c == b'\\' { self.bump(); } // skip escape + next char
|
|
665
|
+
self.bump();
|
|
666
|
+
}
|
|
667
|
+
None
|
|
668
|
+
}
|
|
669
|
+
fn number(&mut self) -> Option<f64> {
|
|
670
|
+
let start = self.pos;
|
|
671
|
+
if self.peek() == Some(b'-') { self.bump(); }
|
|
672
|
+
while let Some(c) = self.peek() {
|
|
673
|
+
match c {
|
|
674
|
+
b'0'..=b'9' | b'.' | b'e' | b'E' | b'+' | b'-' => { self.bump(); }
|
|
675
|
+
_ => break,
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
std::str::from_utf8(&self.bytes[start..self.pos]).ok()?.parse().ok()
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
pub fn json_parse(s: &str) -> Option<JVal> {
|
|
683
|
+
P { bytes: s.as_bytes(), pos: 0 }.val()
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// ---------- Procedural cube mesh — for bloom_gen_mesh_cube ----------
|
|
687
|
+
|
|
688
|
+
pub fn gen_cube_mesh(w: f32, h: f32, d: f32) -> u32 {
|
|
689
|
+
let hx = w * 0.5;
|
|
690
|
+
let hy = h * 0.5;
|
|
691
|
+
let hz = d * 0.5;
|
|
692
|
+
|
|
693
|
+
// 6 faces × 4 verts each, positions + normals + uvs per vertex.
|
|
694
|
+
// Face data: normal direction and (a, b) UV-space corners relative.
|
|
695
|
+
let faces: &[[f32; 3]] = &[
|
|
696
|
+
[ 0.0, 0.0, 1.0], [ 0.0, 0.0, -1.0],
|
|
697
|
+
[ 1.0, 0.0, 0.0], [-1.0, 0.0, 0.0],
|
|
698
|
+
[ 0.0, 1.0, 0.0], [ 0.0, -1.0, 0.0],
|
|
699
|
+
];
|
|
700
|
+
let corners: &[[[f32; 3]; 4]] = &[
|
|
701
|
+
[[-hx,-hy, hz],[ hx,-hy, hz],[ hx, hy, hz],[-hx, hy, hz]], // +Z
|
|
702
|
+
[[ hx,-hy,-hz],[-hx,-hy,-hz],[-hx, hy,-hz],[ hx, hy,-hz]], // -Z
|
|
703
|
+
[[ hx,-hy, hz],[ hx,-hy,-hz],[ hx, hy,-hz],[ hx, hy, hz]], // +X
|
|
704
|
+
[[-hx,-hy,-hz],[-hx,-hy, hz],[-hx, hy, hz],[-hx, hy,-hz]], // -X
|
|
705
|
+
[[-hx, hy, hz],[ hx, hy, hz],[ hx, hy,-hz],[-hx, hy,-hz]], // +Y
|
|
706
|
+
[[-hx,-hy,-hz],[ hx,-hy,-hz],[ hx,-hy, hz],[-hx,-hy, hz]], // -Y
|
|
707
|
+
];
|
|
708
|
+
let uvs_per_corner = [[0.0_f32, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
|
|
709
|
+
|
|
710
|
+
let mut positions = Vec::with_capacity(6 * 4 * 3);
|
|
711
|
+
let mut normals = Vec::with_capacity(6 * 4 * 3);
|
|
712
|
+
let mut uvs = Vec::with_capacity(6 * 4 * 2);
|
|
713
|
+
let mut indices = Vec::with_capacity(6 * 6);
|
|
714
|
+
|
|
715
|
+
for f in 0..6 {
|
|
716
|
+
let n = faces[f];
|
|
717
|
+
for c in 0..4 {
|
|
718
|
+
let p = corners[f][c];
|
|
719
|
+
positions.extend_from_slice(&p);
|
|
720
|
+
normals.extend_from_slice(&n);
|
|
721
|
+
uvs.extend_from_slice(&uvs_per_corner[c]);
|
|
722
|
+
}
|
|
723
|
+
let base = (f * 4) as u32;
|
|
724
|
+
indices.extend_from_slice(&[base, base+1, base+2, base, base+2, base+3]);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
let mut reg = MODELS.lock().unwrap();
|
|
728
|
+
if reg.is_empty() { reg.push(None); }
|
|
729
|
+
let mut root = Node::identity();
|
|
730
|
+
root.mesh = Some(0);
|
|
731
|
+
reg.push(Some(Arc::new(Model {
|
|
732
|
+
meshes: vec![Mesh { primitives: vec![Primitive {
|
|
733
|
+
positions, normals, uvs, indices,
|
|
734
|
+
color: [1.0, 1.0, 1.0, 1.0],
|
|
735
|
+
metallic: 0.0, roughness: 0.6,
|
|
736
|
+
tex_base_color: 0, tex_normal: 0, tex_metallic_roughness: 0,
|
|
737
|
+
tex_emissive: 0, tex_occlusion: 0,
|
|
738
|
+
joint_indices: Vec::new(), weights: Vec::new(),
|
|
739
|
+
}] }],
|
|
740
|
+
nodes: vec![root],
|
|
741
|
+
scene_roots: vec![0],
|
|
742
|
+
skins: Vec::new(),
|
|
743
|
+
animations: Vec::new(),
|
|
744
|
+
})));
|
|
745
|
+
(reg.len() - 1) as u32
|
|
746
|
+
}
|