@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,1280 @@
|
|
|
1
|
+
//! glTF/GLB loading — everything that touches the optional `gltf` /
|
|
2
|
+
//! `image_dds` dependencies (the EN-014 `models3d` feature). Split out
|
|
3
|
+
//! of `models.rs` (EN-052 line ratchet) by EN-063, which re-gated the
|
|
4
|
+
//! loaders: this whole module compiles only with `models3d`, so pure-2D
|
|
5
|
+
//! and web-without-3D builds drop the parsers and their deps entirely.
|
|
6
|
+
//! The always-compiled half of the model system (structs, mixer-driven
|
|
7
|
+
//! animation update, procedural mesh gen) stays in `models.rs`.
|
|
8
|
+
|
|
9
|
+
use super::*;
|
|
10
|
+
|
|
11
|
+
/// Walk the scene graph and collect EVERY world-space transform that
|
|
12
|
+
/// references each mesh. Unlike `walk_scene_for_mesh_transforms` which
|
|
13
|
+
/// records only the first occurrence, this version captures every
|
|
14
|
+
/// instance — so glTF scenes with heavy mesh reuse (Bistro: 5910 nodes
|
|
15
|
+
/// referencing 551 unique meshes) render every chair / bollard / chain
|
|
16
|
+
/// / bush instead of collapsing to a single copy each.
|
|
17
|
+
fn walk_scene_collect_instances(
|
|
18
|
+
node: &gltf::Node,
|
|
19
|
+
parent: &[[f32; 4]; 4],
|
|
20
|
+
out: &mut [Vec<[[f32; 4]; 4]>],
|
|
21
|
+
) {
|
|
22
|
+
let local = node.transform().matrix();
|
|
23
|
+
let world = mat4_mul(parent, &local);
|
|
24
|
+
if let Some(mesh) = node.mesh() {
|
|
25
|
+
let idx = mesh.index();
|
|
26
|
+
if idx < out.len() {
|
|
27
|
+
out[idx].push(world);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
for child in node.children() {
|
|
31
|
+
walk_scene_collect_instances(&child, &world, out);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Transform a 3D point by a 4x4 matrix (column-major). Treats the
|
|
36
|
+
/// point as having w=1 and drops w from the result.
|
|
37
|
+
fn mat4_transform_point(m: &[[f32; 4]; 4], p: &[f32; 3]) -> [f32; 3] {
|
|
38
|
+
[
|
|
39
|
+
m[0][0]*p[0] + m[1][0]*p[1] + m[2][0]*p[2] + m[3][0],
|
|
40
|
+
m[0][1]*p[0] + m[1][1]*p[1] + m[2][1]*p[2] + m[3][1],
|
|
41
|
+
m[0][2]*p[0] + m[1][2]*p[1] + m[2][2]*p[2] + m[3][2],
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Transform a direction vector by a 3x3 matrix (extracted from a 4x4
|
|
46
|
+
/// column-major stored as the top-left 3x3). Used for normals under
|
|
47
|
+
/// the inverse-transpose matrix.
|
|
48
|
+
fn mat3_transform_vec(m: &[[f32; 3]; 3], v: &[f32; 3]) -> [f32; 3] {
|
|
49
|
+
[
|
|
50
|
+
m[0][0]*v[0] + m[1][0]*v[1] + m[2][0]*v[2],
|
|
51
|
+
m[0][1]*v[0] + m[1][1]*v[1] + m[2][1]*v[2],
|
|
52
|
+
m[0][2]*v[0] + m[1][2]*v[1] + m[2][2]*v[2],
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Inverse-transpose of the 3x3 rotation+scale part of a 4x4 matrix.
|
|
57
|
+
/// Correct way to transform normals when the matrix has non-uniform
|
|
58
|
+
/// scale; falls back to identity if the 3x3 block isn't invertible.
|
|
59
|
+
fn mat4_inverse_transpose_3x3(m: &[[f32; 4]; 4]) -> [[f32; 3]; 3] {
|
|
60
|
+
let a = m[0][0]; let b = m[1][0]; let c = m[2][0];
|
|
61
|
+
let d = m[0][1]; let e = m[1][1]; let f = m[2][1];
|
|
62
|
+
let g = m[0][2]; let h = m[1][2]; let i = m[2][2];
|
|
63
|
+
|
|
64
|
+
let inv00 = e*i - f*h;
|
|
65
|
+
let inv01 = f*g - d*i;
|
|
66
|
+
let inv02 = d*h - e*g;
|
|
67
|
+
let inv10 = c*h - b*i;
|
|
68
|
+
let inv11 = a*i - c*g;
|
|
69
|
+
let inv12 = b*g - a*h;
|
|
70
|
+
let inv20 = b*f - c*e;
|
|
71
|
+
let inv21 = c*d - a*f;
|
|
72
|
+
let inv22 = a*e - b*d;
|
|
73
|
+
|
|
74
|
+
let det = a*inv00 + b*inv01 + c*inv02;
|
|
75
|
+
if det.abs() < 1e-10 {
|
|
76
|
+
return [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
|
|
77
|
+
}
|
|
78
|
+
let inv_det = 1.0 / det;
|
|
79
|
+
// Store in column-major like the rest of the file (columns first).
|
|
80
|
+
// The result is the inverse-transpose, so rows/cols are swapped
|
|
81
|
+
// from the plain inverse.
|
|
82
|
+
[
|
|
83
|
+
[inv00 * inv_det, inv01 * inv_det, inv02 * inv_det],
|
|
84
|
+
[inv10 * inv_det, inv11 * inv_det, inv12 * inv_det],
|
|
85
|
+
[inv20 * inv_det, inv21 * inv_det, inv22 * inv_det],
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ============================================================
|
|
90
|
+
// glTF animation loader
|
|
91
|
+
// ============================================================
|
|
92
|
+
|
|
93
|
+
fn read_accessor_f32(_gltf: &gltf::Gltf, buffer_data: &[Vec<u8>], accessor: &gltf::Accessor) -> Vec<f32> {
|
|
94
|
+
let view = match accessor.view() {
|
|
95
|
+
Some(v) => v,
|
|
96
|
+
None => return Vec::new(),
|
|
97
|
+
};
|
|
98
|
+
let buf_idx = view.buffer().index();
|
|
99
|
+
if buf_idx >= buffer_data.len() { return Vec::new(); }
|
|
100
|
+
let buf = &buffer_data[buf_idx];
|
|
101
|
+
let offset = view.offset() + accessor.offset();
|
|
102
|
+
let count = accessor.count();
|
|
103
|
+
let stride = view.stride().unwrap_or(accessor.size());
|
|
104
|
+
let component_count = match accessor.dimensions() {
|
|
105
|
+
gltf::accessor::Dimensions::Scalar => 1,
|
|
106
|
+
gltf::accessor::Dimensions::Vec2 => 2,
|
|
107
|
+
gltf::accessor::Dimensions::Vec3 => 3,
|
|
108
|
+
gltf::accessor::Dimensions::Vec4 => 4,
|
|
109
|
+
gltf::accessor::Dimensions::Mat4 => 16,
|
|
110
|
+
_ => 1,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
let mut result = Vec::with_capacity(count * component_count);
|
|
114
|
+
for i in 0..count {
|
|
115
|
+
let base = offset + i * stride;
|
|
116
|
+
for c in 0..component_count {
|
|
117
|
+
let byte_offset = base + c * 4;
|
|
118
|
+
if byte_offset + 4 <= buf.len() {
|
|
119
|
+
let val = f32::from_le_bytes([buf[byte_offset], buf[byte_offset+1], buf[byte_offset+2], buf[byte_offset+3]]);
|
|
120
|
+
result.push(val);
|
|
121
|
+
} else {
|
|
122
|
+
result.push(0.0);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
result
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
pub(super) fn load_gltf_animation(data: &[u8]) -> Option<ModelAnimation> {
|
|
130
|
+
let gltf = gltf::Gltf::from_slice(data).ok()?;
|
|
131
|
+
|
|
132
|
+
// Get buffer data
|
|
133
|
+
let mut buffer_data: Vec<Vec<u8>> = Vec::new();
|
|
134
|
+
for buffer in gltf.buffers() {
|
|
135
|
+
match buffer.source() {
|
|
136
|
+
gltf::buffer::Source::Bin => {
|
|
137
|
+
if let Some(blob) = gltf.blob.as_ref() {
|
|
138
|
+
buffer_data.push(blob.clone());
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
gltf::buffer::Source::Uri(uri) => {
|
|
142
|
+
if let Some(encoded) = uri.strip_prefix("data:application/octet-stream;base64,") {
|
|
143
|
+
let mut decoded = Vec::new();
|
|
144
|
+
let _ = base64_decode(encoded, &mut decoded);
|
|
145
|
+
buffer_data.push(decoded);
|
|
146
|
+
} else {
|
|
147
|
+
buffer_data.push(Vec::new());
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Parse skeleton from the first skin
|
|
154
|
+
let skeleton = if let Some(skin) = gltf.skins().next() {
|
|
155
|
+
let joints_nodes: Vec<_> = skin.joints().collect();
|
|
156
|
+
let joint_count = joints_nodes.len();
|
|
157
|
+
|
|
158
|
+
// Build a mapping from node index to joint index
|
|
159
|
+
let mut node_to_joint = std::collections::HashMap::new();
|
|
160
|
+
for (ji, node) in joints_nodes.iter().enumerate() {
|
|
161
|
+
node_to_joint.insert(node.index(), ji);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Read inverse bind matrices
|
|
165
|
+
let ibm_data = if let Some(accessor) = skin.inverse_bind_matrices() {
|
|
166
|
+
read_accessor_f32(&gltf, &buffer_data, &accessor)
|
|
167
|
+
} else {
|
|
168
|
+
let mut default_ibm = Vec::with_capacity(joint_count * 16);
|
|
169
|
+
for _ in 0..joint_count {
|
|
170
|
+
default_ibm.extend_from_slice(&[
|
|
171
|
+
1.0, 0.0, 0.0, 0.0,
|
|
172
|
+
0.0, 1.0, 0.0, 0.0,
|
|
173
|
+
0.0, 0.0, 1.0, 0.0,
|
|
174
|
+
0.0, 0.0, 0.0, 1.0,
|
|
175
|
+
]);
|
|
176
|
+
}
|
|
177
|
+
default_ibm
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
let mut joints = Vec::with_capacity(joint_count);
|
|
181
|
+
let mut root_joints = Vec::new();
|
|
182
|
+
|
|
183
|
+
for (ji, node) in joints_nodes.iter().enumerate() {
|
|
184
|
+
let mut ibm = [[0.0f32; 4]; 4];
|
|
185
|
+
let base = ji * 16;
|
|
186
|
+
if base + 16 <= ibm_data.len() {
|
|
187
|
+
// glTF stores column-major; read directly (we also use column-major)
|
|
188
|
+
for a in 0..4 {
|
|
189
|
+
for b in 0..4 {
|
|
190
|
+
ibm[a][b] = ibm_data[base + a * 4 + b];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
ibm = mat4_identity();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Blender FBX export bakes 100x scale into IBMs (converts m→cm for bone space).
|
|
198
|
+
// This is NEEDED because Blender also pre-scales vertex positions to meters.
|
|
199
|
+
// The 100x in IBMs converts meter-space vertices to cm-space bone transforms.
|
|
200
|
+
// DO NOT normalize — the scale is intentional and required.
|
|
201
|
+
|
|
202
|
+
let children: Vec<usize> = node.children()
|
|
203
|
+
.filter_map(|child| node_to_joint.get(&child.index()).copied())
|
|
204
|
+
.collect();
|
|
205
|
+
|
|
206
|
+
let name = node.name().unwrap_or("").to_string();
|
|
207
|
+
let (t, r, s) = node.transform().decomposed();
|
|
208
|
+
|
|
209
|
+
joints.push(JointData {
|
|
210
|
+
inverse_bind: ibm, children, name,
|
|
211
|
+
rest_translation: t,
|
|
212
|
+
rest_rotation: r,
|
|
213
|
+
rest_scale: s,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Find root joints (joints that are not children of any other joint)
|
|
218
|
+
let mut is_child = vec![false; joint_count];
|
|
219
|
+
for joint in &joints {
|
|
220
|
+
for &child in &joint.children {
|
|
221
|
+
if child < joint_count { is_child[child] = true; }
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
for i in 0..joint_count {
|
|
225
|
+
if !is_child[i] { root_joints.push(i); }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#[cfg(debug_assertions)]
|
|
229
|
+
{
|
|
230
|
+
eprintln!("[anim] Skeleton: {} joints, {} roots", joints.len(), root_joints.len());
|
|
231
|
+
for (i, j) in joints.iter().enumerate() {
|
|
232
|
+
if i < 5 || i == joints.len() - 1 {
|
|
233
|
+
eprintln!("[anim] joint {}: '{}' children={:?}", i, j.name, j.children);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
Some(SkeletonData { joints, root_joints })
|
|
239
|
+
} else {
|
|
240
|
+
#[cfg(debug_assertions)]
|
|
241
|
+
eprintln!("[anim] No skin found in glTF!");
|
|
242
|
+
None
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Parse animations
|
|
246
|
+
let mut animations = Vec::new();
|
|
247
|
+
for anim in gltf.animations() {
|
|
248
|
+
let mut channels = Vec::new();
|
|
249
|
+
let mut duration: f32 = 0.0;
|
|
250
|
+
|
|
251
|
+
// Build node-to-joint mapping for channel resolution
|
|
252
|
+
let node_to_joint: std::collections::HashMap<usize, usize> = if let Some(skin) = gltf.skins().next() {
|
|
253
|
+
skin.joints().enumerate().map(|(ji, node)| (node.index(), ji)).collect()
|
|
254
|
+
} else {
|
|
255
|
+
std::collections::HashMap::new()
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// Group channels by target node: (trans_ts, translations, rot_ts, rotations, scale_ts, scales)
|
|
259
|
+
let mut node_channels: std::collections::HashMap<usize, (Vec<f32>, Vec<[f32; 3]>, Vec<f32>, Vec<[f32; 4]>, Vec<f32>, Vec<[f32; 3]>)> = std::collections::HashMap::new();
|
|
260
|
+
|
|
261
|
+
#[cfg(debug_assertions)]
|
|
262
|
+
let mut skipped_channels = 0usize;
|
|
263
|
+
#[cfg(debug_assertions)]
|
|
264
|
+
let mut mapped_channels = 0usize;
|
|
265
|
+
#[cfg(debug_assertions)]
|
|
266
|
+
{
|
|
267
|
+
eprintln!("[anim] Animation '{}' has {} channels, node_to_joint map has {} entries",
|
|
268
|
+
anim.name().unwrap_or("?"), anim.channels().count(), node_to_joint.len());
|
|
269
|
+
for (ci, ch) in anim.channels().enumerate() {
|
|
270
|
+
if ci < 5 {
|
|
271
|
+
let tn = ch.target().node();
|
|
272
|
+
eprintln!("[anim] channel {} targets node {} '{}' mapped={}",
|
|
273
|
+
ci, tn.index(), tn.name().unwrap_or("?"),
|
|
274
|
+
node_to_joint.contains_key(&tn.index()));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
for channel in anim.channels() {
|
|
279
|
+
let target_node = channel.target().node().index();
|
|
280
|
+
let joint_index = match node_to_joint.get(&target_node) {
|
|
281
|
+
Some(&ji) => {
|
|
282
|
+
#[cfg(debug_assertions)]
|
|
283
|
+
{ mapped_channels += 1; }
|
|
284
|
+
ji
|
|
285
|
+
},
|
|
286
|
+
None => {
|
|
287
|
+
#[cfg(debug_assertions)]
|
|
288
|
+
{ skipped_channels += 1; }
|
|
289
|
+
continue;
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
let sampler = channel.sampler();
|
|
294
|
+
let input_accessor = sampler.input();
|
|
295
|
+
let output_accessor = sampler.output();
|
|
296
|
+
|
|
297
|
+
let timestamps = read_accessor_f32(&gltf, &buffer_data, &input_accessor);
|
|
298
|
+
let values = read_accessor_f32(&gltf, &buffer_data, &output_accessor);
|
|
299
|
+
|
|
300
|
+
if let Some(&last) = timestamps.last() {
|
|
301
|
+
if last > duration { duration = last; }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let entry = node_channels.entry(joint_index).or_insert_with(|| (Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new()));
|
|
305
|
+
|
|
306
|
+
match channel.target().property() {
|
|
307
|
+
gltf::animation::Property::Translation => {
|
|
308
|
+
entry.0 = timestamps;
|
|
309
|
+
entry.1 = values.chunks(3).map(|c| [c[0], c[1], c[2]]).collect();
|
|
310
|
+
}
|
|
311
|
+
gltf::animation::Property::Rotation => {
|
|
312
|
+
entry.2 = timestamps;
|
|
313
|
+
entry.3 = values.chunks(4).map(|c| [c[0], c[1], c[2], c[3]]).collect();
|
|
314
|
+
}
|
|
315
|
+
gltf::animation::Property::Scale => {
|
|
316
|
+
entry.4 = timestamps;
|
|
317
|
+
entry.5 = values.chunks(3).map(|c| [c[0], c[1], c[2]]).collect();
|
|
318
|
+
}
|
|
319
|
+
_ => {}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
for (joint_index, (trans_ts, translations, rot_ts, rotations, scale_ts, scales)) in node_channels {
|
|
324
|
+
// Use the longest timestamp array as the primary (for backward compat)
|
|
325
|
+
let timestamps = if rot_ts.len() >= trans_ts.len() && rot_ts.len() >= scale_ts.len() {
|
|
326
|
+
rot_ts.clone()
|
|
327
|
+
} else if trans_ts.len() >= scale_ts.len() {
|
|
328
|
+
trans_ts.clone()
|
|
329
|
+
} else {
|
|
330
|
+
scale_ts.clone()
|
|
331
|
+
};
|
|
332
|
+
channels.push(AnimationChannel {
|
|
333
|
+
joint_index,
|
|
334
|
+
timestamps,
|
|
335
|
+
translations,
|
|
336
|
+
rotation_timestamps: rot_ts,
|
|
337
|
+
rotations,
|
|
338
|
+
scale_timestamps: scale_ts,
|
|
339
|
+
scales,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let name = anim.name().unwrap_or("").to_string();
|
|
344
|
+
#[cfg(debug_assertions)]
|
|
345
|
+
{
|
|
346
|
+
let total_kf: usize = channels.iter().map(|c| c.timestamps.len()).sum();
|
|
347
|
+
let avg_kf = if !channels.is_empty() { total_kf / channels.len() } else { 0 };
|
|
348
|
+
eprintln!("[anim] Animation '{}': {} channels mapped, {} skipped, duration={:.2}s, avg {}/ch keyframes",
|
|
349
|
+
name, mapped_channels, skipped_channels, duration, avg_kf);
|
|
350
|
+
}
|
|
351
|
+
animations.push(AnimationData { channels, duration, name });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
let joint_count = skeleton.as_ref().map(|s| s.joints.len()).unwrap_or(0);
|
|
355
|
+
// Build reference rest rotations from the first animation at t=0
|
|
356
|
+
let ref_rest_rotations = if animations.len() > 1 {
|
|
357
|
+
if let Some(ref skel) = skeleton {
|
|
358
|
+
let joint_count_s = skel.joints.len();
|
|
359
|
+
let mut rest_rots = vec![[0.0f32, 0.0, 0.0, 1.0]; joint_count_s];
|
|
360
|
+
// Sample first animation at t=0 to get reference rest rotations
|
|
361
|
+
let anim0 = &animations[0];
|
|
362
|
+
for ch in &anim0.channels {
|
|
363
|
+
if ch.joint_index < joint_count_s && !ch.rotations.is_empty() {
|
|
364
|
+
rest_rots[ch.joint_index] = if ch.rotations.len() > 0 { ch.rotations[0] } else { [0.0, 0.0, 0.0, 1.0] };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
#[cfg(debug_assertions)]
|
|
368
|
+
eprintln!("[retarget] Built reference rest rotations from anim 0 for {} joints", joint_count_s);
|
|
369
|
+
Some(rest_rots)
|
|
370
|
+
} else { None }
|
|
371
|
+
} else { None };
|
|
372
|
+
|
|
373
|
+
Some(ModelAnimation {
|
|
374
|
+
skeleton: skeleton.map(Arc::new),
|
|
375
|
+
animations: Arc::new(animations),
|
|
376
|
+
joint_matrices: vec![mat4_identity(); joint_count],
|
|
377
|
+
ref_rest_rotations: ref_rest_rotations.map(Arc::new),
|
|
378
|
+
mixer: AnimMixer::default(),
|
|
379
|
+
joint_world: vec![mat4_identity(); joint_count],
|
|
380
|
+
mask_weights: vec![0.0; joint_count],
|
|
381
|
+
mask_cached_root: -1,
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
pub(super) fn load_gltf_with_textures(
|
|
386
|
+
data: &[u8],
|
|
387
|
+
renderer: &mut crate::renderer::Renderer,
|
|
388
|
+
base_dir: Option<&std::path::Path>,
|
|
389
|
+
) -> Option<ModelData> {
|
|
390
|
+
let gltf = gltf::Gltf::from_slice(data).ok()?;
|
|
391
|
+
|
|
392
|
+
// Get buffer data
|
|
393
|
+
let mut buffer_data: Vec<Vec<u8>> = Vec::new();
|
|
394
|
+
for buffer in gltf.buffers() {
|
|
395
|
+
match buffer.source() {
|
|
396
|
+
gltf::buffer::Source::Bin => {
|
|
397
|
+
if let Some(blob) = gltf.blob.as_ref() { buffer_data.push(blob.clone()); }
|
|
398
|
+
}
|
|
399
|
+
gltf::buffer::Source::Uri(uri) => {
|
|
400
|
+
if let Some(encoded) = uri.strip_prefix("data:application/octet-stream;base64,") {
|
|
401
|
+
let mut decoded = Vec::new();
|
|
402
|
+
let _ = base64_decode(encoded, &mut decoded);
|
|
403
|
+
buffer_data.push(decoded);
|
|
404
|
+
} else if let Some(dir) = base_dir {
|
|
405
|
+
// External .bin file alongside the .gltf.
|
|
406
|
+
let path = dir.join(uri);
|
|
407
|
+
match std::fs::read(&path) {
|
|
408
|
+
Ok(bytes) => buffer_data.push(bytes),
|
|
409
|
+
Err(_) => buffer_data.push(Vec::new()),
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
buffer_data.push(Vec::new());
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Pre-walk materials to identify which image indices are normal
|
|
419
|
+
// maps. They need LEADR-style vector-space mip generation and per-
|
|
420
|
+
// mip variance baked into alpha; see register_texture_kind.
|
|
421
|
+
let mut normal_image_set: std::collections::HashSet<usize> = Default::default();
|
|
422
|
+
for mat in gltf.materials() {
|
|
423
|
+
if let Some(nt) = mat.normal_texture() {
|
|
424
|
+
normal_image_set.insert(nt.texture().source().index());
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Extract and register textures
|
|
429
|
+
let mut texture_indices: Vec<u32> = Vec::new(); // maps glTF image index -> renderer texture index
|
|
430
|
+
for (image_idx, image) in gltf.images().enumerate() {
|
|
431
|
+
let is_normal = normal_image_set.contains(&image_idx);
|
|
432
|
+
match image.source() {
|
|
433
|
+
gltf::image::Source::View { view, .. } => {
|
|
434
|
+
let buf_idx = view.buffer().index();
|
|
435
|
+
if buf_idx < buffer_data.len() {
|
|
436
|
+
let offset = view.offset();
|
|
437
|
+
let length = view.length();
|
|
438
|
+
if offset + length <= buffer_data[buf_idx].len() {
|
|
439
|
+
let img_data = &buffer_data[buf_idx][offset..offset + length];
|
|
440
|
+
// Decode image (PNG/JPEG)
|
|
441
|
+
if let Ok(img) = image::load_from_memory(img_data) {
|
|
442
|
+
let rgba = img.to_rgba8();
|
|
443
|
+
let (w, h) = (rgba.width(), rgba.height());
|
|
444
|
+
let tex_idx = renderer.register_texture_kind(w, h, &rgba, is_normal);
|
|
445
|
+
texture_indices.push(tex_idx);
|
|
446
|
+
} else {
|
|
447
|
+
texture_indices.push(0); // fallback to white
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
texture_indices.push(0);
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
texture_indices.push(0);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
gltf::image::Source::Uri { uri, .. } => {
|
|
457
|
+
// External image file (loose glTF). Resolve relative to
|
|
458
|
+
// the .gltf file's directory.
|
|
459
|
+
let (bytes, effective_uri): (Option<Vec<u8>>, String) =
|
|
460
|
+
if let Some(encoded) = uri.strip_prefix("data:") {
|
|
461
|
+
let decoded = encoded.find(";base64,").map(|pos| {
|
|
462
|
+
let b64 = &encoded[pos + 8..];
|
|
463
|
+
let mut out = Vec::new();
|
|
464
|
+
let _ = base64_decode(b64, &mut out);
|
|
465
|
+
out
|
|
466
|
+
});
|
|
467
|
+
(decoded, uri.to_string())
|
|
468
|
+
} else if let Some(dir) = base_dir {
|
|
469
|
+
let primary = dir.join(uri);
|
|
470
|
+
if let Ok(b) = std::fs::read(&primary) {
|
|
471
|
+
(Some(b), uri.to_string())
|
|
472
|
+
} else {
|
|
473
|
+
// Asset packs sometimes ship DDS-only while the
|
|
474
|
+
// glTF still references a .png URI (Lumberyard
|
|
475
|
+
// Bistro does this). Retry with a .dds
|
|
476
|
+
// sibling before giving up.
|
|
477
|
+
let swapped = swap_extension(uri, "dds");
|
|
478
|
+
let alt = dir.join(&swapped);
|
|
479
|
+
match std::fs::read(&alt) {
|
|
480
|
+
Ok(b) => (Some(b), swapped),
|
|
481
|
+
Err(_) => (None, uri.to_string()),
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
} else {
|
|
485
|
+
(None, uri.to_string())
|
|
486
|
+
};
|
|
487
|
+
match bytes.and_then(|b| decode_texture_bytes(&b, &effective_uri)) {
|
|
488
|
+
Some((rgba, w, h)) => {
|
|
489
|
+
texture_indices.push(renderer.register_texture_kind(w, h, &rgba, is_normal));
|
|
490
|
+
}
|
|
491
|
+
None => texture_indices.push(0),
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Detect armature scale for skinned meshes.
|
|
498
|
+
// Blender FBX imports set armature scale to 0.01 (cm→m conversion).
|
|
499
|
+
// Vertex positions inherit this scale but bone transforms don't,
|
|
500
|
+
// creating a unit mismatch. We apply the inverse to vertex positions.
|
|
501
|
+
let skin_vertex_scale: f32 = {
|
|
502
|
+
let mut scale = 1.0f32;
|
|
503
|
+
for node in gltf.nodes() {
|
|
504
|
+
if node.mesh().is_some() && node.skin().is_some() {
|
|
505
|
+
// Found a skinned mesh node — look for parent with scale
|
|
506
|
+
for parent in gltf.nodes() {
|
|
507
|
+
for child in parent.children() {
|
|
508
|
+
if child.index() == node.index() {
|
|
509
|
+
let (_, _, s) = parent.transform().decomposed();
|
|
510
|
+
let avg_scale = (s[0] + s[1] + s[2]) / 3.0;
|
|
511
|
+
if avg_scale > 0.001 && (avg_scale - 1.0).abs() > 0.01 {
|
|
512
|
+
scale = 1.0 / avg_scale;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// Fallback: check IBMs for large scale (Blender FBX baked 100x)
|
|
520
|
+
if (scale - 1.0).abs() < 0.01 {
|
|
521
|
+
if let Some(skin) = gltf.skins().next() {
|
|
522
|
+
if let Some(accessor) = skin.inverse_bind_matrices() {
|
|
523
|
+
let view = accessor.view().unwrap();
|
|
524
|
+
let buf_idx = view.buffer().index();
|
|
525
|
+
if buf_idx < buffer_data.len() {
|
|
526
|
+
let offset = view.offset() + accessor.offset();
|
|
527
|
+
let data = &buffer_data[buf_idx];
|
|
528
|
+
if offset + 12 <= data.len() {
|
|
529
|
+
// Read first 3 floats (first column of first IBM)
|
|
530
|
+
let f0 = f32::from_le_bytes([data[offset], data[offset+1], data[offset+2], data[offset+3]]);
|
|
531
|
+
let f1 = f32::from_le_bytes([data[offset+4], data[offset+5], data[offset+6], data[offset+7]]);
|
|
532
|
+
let f2 = f32::from_le_bytes([data[offset+8], data[offset+9], data[offset+10], data[offset+11]]);
|
|
533
|
+
let diag = (f0*f0 + f1*f1 + f2*f2).sqrt();
|
|
534
|
+
if diag > 10.0 {
|
|
535
|
+
scale = diag;
|
|
536
|
+
#[cfg(debug_assertions)]
|
|
537
|
+
eprintln!("[skin] IBM col0 len={:.1}, applying {:.0}x vertex scale", diag, scale);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (scale - 1.0).abs() > 0.01 {
|
|
545
|
+
#[cfg(debug_assertions)]
|
|
546
|
+
eprintln!("[skin] Applying {:.0}x vertex scale to compensate armature transform", scale);
|
|
547
|
+
}
|
|
548
|
+
scale
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
let mut meshes = Vec::new();
|
|
552
|
+
let mut bbox_min = [f32::MAX; 3];
|
|
553
|
+
let mut bbox_max = [f32::MIN; 3];
|
|
554
|
+
|
|
555
|
+
// Walk the scene node tree to collect world-space transforms for
|
|
556
|
+
// each mesh-referencing node. glTF supports instancing by having
|
|
557
|
+
// multiple nodes reference the same mesh at different transforms
|
|
558
|
+
// — Bistro uses this heavily (5910 nodes, 551 meshes: chairs,
|
|
559
|
+
// bollards, chains, foliage repeated everywhere). We emit one
|
|
560
|
+
// MeshData PER (mesh, transform) pair so every instance actually
|
|
561
|
+
// shows up in the scene. Memory cost is linear in node count;
|
|
562
|
+
// not great for deep instancing but correct. Animated / skinned
|
|
563
|
+
// meshes are unaffected — the armature transforms apply on top.
|
|
564
|
+
let mesh_count = gltf.meshes().count();
|
|
565
|
+
let mut mesh_instances: Vec<Vec<[[f32; 4]; 4]>> = vec![Vec::new(); mesh_count];
|
|
566
|
+
let identity = [[1.0f32, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]];
|
|
567
|
+
for scene in gltf.scenes() {
|
|
568
|
+
for node in scene.nodes() {
|
|
569
|
+
walk_scene_collect_instances(&node, &identity, &mut mesh_instances);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
for mesh in gltf.meshes() {
|
|
574
|
+
let instances = mesh_instances[mesh.index()].clone();
|
|
575
|
+
// Meshes reachable from no scene node would have no instances;
|
|
576
|
+
// fall back to a single identity transform so orphan meshes
|
|
577
|
+
// still render (matches prior behaviour for simple models).
|
|
578
|
+
let instance_transforms: Vec<Option<[[f32; 4]; 4]>> = if instances.is_empty() {
|
|
579
|
+
vec![None]
|
|
580
|
+
} else {
|
|
581
|
+
instances.into_iter().map(Some).collect()
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
for mesh_world in &instance_transforms {
|
|
585
|
+
let mesh_world = *mesh_world;
|
|
586
|
+
// Inverse-transpose 3×3 for normals under non-uniform scale.
|
|
587
|
+
let normal_xform = mesh_world.map(|m| mat4_inverse_transpose_3x3(&m));
|
|
588
|
+
for primitive in mesh.primitives() {
|
|
589
|
+
let reader = primitive.reader(|buf| buffer_data.get(buf.index()).map(|d| d.as_slice()));
|
|
590
|
+
let positions: Vec<[f32; 3]> = match reader.read_positions() {
|
|
591
|
+
Some(iter) => iter.collect(),
|
|
592
|
+
None => continue,
|
|
593
|
+
};
|
|
594
|
+
let normals: Vec<[f32; 3]> = reader.read_normals()
|
|
595
|
+
.map(|iter| iter.collect())
|
|
596
|
+
.unwrap_or_else(|| vec![[0.0, 1.0, 0.0]; positions.len()]);
|
|
597
|
+
let tex_coords: Vec<[f32; 2]> = reader.read_tex_coords(0)
|
|
598
|
+
.map(|iter| iter.into_f32().collect())
|
|
599
|
+
.unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
|
|
600
|
+
// Tangents (vec4: xyz = tangent, w = bitangent sign ±1).
|
|
601
|
+
// If absent, we leave them as zero so the shader knows to
|
|
602
|
+
// skip normal-map perturbation for this mesh.
|
|
603
|
+
let tangents: Vec<[f32; 4]> = reader.read_tangents()
|
|
604
|
+
.map(|iter| iter.collect())
|
|
605
|
+
.unwrap_or_else(|| vec![[0.0; 4]; positions.len()]);
|
|
606
|
+
|
|
607
|
+
// Get vertex colors if available
|
|
608
|
+
let vert_colors: Option<Vec<[f32; 4]>> = reader.read_colors(0)
|
|
609
|
+
.map(|iter| iter.into_rgba_f32().collect());
|
|
610
|
+
|
|
611
|
+
let mat = primitive.material();
|
|
612
|
+
let pbr = mat.pbr_metallic_roughness();
|
|
613
|
+
let emissive_factor = mat.emissive_factor();
|
|
614
|
+
|
|
615
|
+
let tex_idx_of = |img_idx: usize| -> Option<u32> {
|
|
616
|
+
texture_indices.get(img_idx).copied()
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
let normal_tex_idx = mat.normal_texture()
|
|
620
|
+
.and_then(|info| tex_idx_of(info.texture().source().index()));
|
|
621
|
+
let emissive_tex_idx = mat.emissive_texture()
|
|
622
|
+
.and_then(|info| tex_idx_of(info.texture().source().index()));
|
|
623
|
+
let occlusion_tex_idx = mat.occlusion_texture()
|
|
624
|
+
.and_then(|info| tex_idx_of(info.texture().source().index()));
|
|
625
|
+
|
|
626
|
+
// Metallic-roughness first; fall back to
|
|
627
|
+
// KHR_materials_pbrSpecularGlossiness when only that's
|
|
628
|
+
// authored (Lumberyard Bistro + many FBX exports).
|
|
629
|
+
// Conversion matches the load_gltf_staged path — see
|
|
630
|
+
// specgloss_to_metalrough for the algorithm.
|
|
631
|
+
let (mut base_color, mut metallic_factor, mut roughness_factor, tex_idx, mr_tex_idx) =
|
|
632
|
+
if pbr.base_color_texture().is_none() {
|
|
633
|
+
if let Some(sg) = mat.pbr_specular_glossiness() {
|
|
634
|
+
let diffuse = sg.diffuse_factor();
|
|
635
|
+
let spec = sg.specular_factor();
|
|
636
|
+
let (base_color, metallic) =
|
|
637
|
+
specgloss_to_metalrough(diffuse, spec);
|
|
638
|
+
let roughness = 1.0 - sg.glossiness_factor();
|
|
639
|
+
let diffuse_tex = sg.diffuse_texture()
|
|
640
|
+
.and_then(|info| tex_idx_of(info.texture().source().index()));
|
|
641
|
+
(base_color, metallic, roughness, diffuse_tex, None)
|
|
642
|
+
} else {
|
|
643
|
+
(pbr.base_color_factor(), pbr.metallic_factor(), pbr.roughness_factor(), None, None)
|
|
644
|
+
}
|
|
645
|
+
} else {
|
|
646
|
+
let tex = pbr.base_color_texture()
|
|
647
|
+
.and_then(|info| tex_idx_of(info.texture().source().index()));
|
|
648
|
+
let mr = pbr.metallic_roughness_texture()
|
|
649
|
+
.and_then(|info| tex_idx_of(info.texture().source().index()));
|
|
650
|
+
(pbr.base_color_factor(), pbr.metallic_factor(), pbr.roughness_factor(), tex, mr)
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
if let Some(t) = mat.transmission() {
|
|
654
|
+
apply_transmission_hack(
|
|
655
|
+
t.transmission_factor(),
|
|
656
|
+
&mut base_color,
|
|
657
|
+
&mut metallic_factor,
|
|
658
|
+
&mut roughness_factor,
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
let mut vertices = Vec::with_capacity(positions.len());
|
|
663
|
+
for i in 0..positions.len() {
|
|
664
|
+
let p = positions[i];
|
|
665
|
+
for k in 0..3 {
|
|
666
|
+
if p[k] < bbox_min[k] { bbox_min[k] = p[k]; }
|
|
667
|
+
if p[k] > bbox_max[k] { bbox_max[k] = p[k]; }
|
|
668
|
+
}
|
|
669
|
+
let color = if let Some(ref vc) = vert_colors {
|
|
670
|
+
vc[i]
|
|
671
|
+
} else {
|
|
672
|
+
[base_color[0], base_color[1], base_color[2], base_color[3]]
|
|
673
|
+
};
|
|
674
|
+
// Skin data (joints + weights)
|
|
675
|
+
let joint_vals: Option<Vec<[u16; 4]>> = reader.read_joints(0)
|
|
676
|
+
.map(|iter| iter.into_u16().collect());
|
|
677
|
+
let weight_vals: Option<Vec<[f32; 4]>> = reader.read_weights(0)
|
|
678
|
+
.map(|iter| iter.into_f32().collect());
|
|
679
|
+
|
|
680
|
+
let jv = if let Some(ref j) = joint_vals {
|
|
681
|
+
[j[i][0] as f32, j[i][1] as f32, j[i][2] as f32, j[i][3] as f32]
|
|
682
|
+
} else {
|
|
683
|
+
[0.0; 4]
|
|
684
|
+
};
|
|
685
|
+
let wv = if let Some(ref w) = weight_vals {
|
|
686
|
+
w[i]
|
|
687
|
+
} else {
|
|
688
|
+
[0.0; 4]
|
|
689
|
+
};
|
|
690
|
+
// Apply inverse armature scale to skinned vertex positions
|
|
691
|
+
let is_skinned = wv[0] + wv[1] + wv[2] + wv[3] > 0.01;
|
|
692
|
+
let base_pos = if is_skinned && (skin_vertex_scale - 1.0).abs() > 0.01 {
|
|
693
|
+
[p[0] * skin_vertex_scale, p[1] * skin_vertex_scale, p[2] * skin_vertex_scale]
|
|
694
|
+
} else {
|
|
695
|
+
p
|
|
696
|
+
};
|
|
697
|
+
// Bake the mesh's scene node transform into world-space
|
|
698
|
+
// position/normal. Skinned meshes are NOT world-baked:
|
|
699
|
+
// their node transform is expected to be consumed by the
|
|
700
|
+
// armature, and the pose is driven by joint matrices at
|
|
701
|
+
// draw time. Static (non-skinned) meshes get the baked
|
|
702
|
+
// transform so drawModel's position/scale arguments
|
|
703
|
+
// apply on top of the correct base pose.
|
|
704
|
+
let (final_pos, final_normal, final_tangent) = if is_skinned {
|
|
705
|
+
(base_pos, normals[i], tangents[i])
|
|
706
|
+
} else if let Some(xform) = mesh_world {
|
|
707
|
+
let t_in = [tangents[i][0], tangents[i][1], tangents[i][2]];
|
|
708
|
+
let t_out = match normal_xform {
|
|
709
|
+
// Tangents transform like positions (as directions)
|
|
710
|
+
// under the linear part of the transform — we use
|
|
711
|
+
// the upper 3×3 of the model matrix, not its
|
|
712
|
+
// inverse-transpose. But since our mesh_world is
|
|
713
|
+
// rigid-ish (no shear), the normal_xform gets us
|
|
714
|
+
// close enough for the common case. For a purely
|
|
715
|
+
// orthonormal node transform these are identical.
|
|
716
|
+
Some(ref n) => mat3_transform_vec(n, &t_in),
|
|
717
|
+
None => t_in,
|
|
718
|
+
};
|
|
719
|
+
(
|
|
720
|
+
mat4_transform_point(&xform, &base_pos),
|
|
721
|
+
match normal_xform {
|
|
722
|
+
Some(ref n) => mat3_transform_vec(n, &normals[i]),
|
|
723
|
+
None => normals[i],
|
|
724
|
+
},
|
|
725
|
+
[t_out[0], t_out[1], t_out[2], tangents[i][3]],
|
|
726
|
+
)
|
|
727
|
+
} else {
|
|
728
|
+
(base_pos, normals[i], tangents[i])
|
|
729
|
+
};
|
|
730
|
+
// Update bbox to reflect the final (possibly transformed)
|
|
731
|
+
// position so the camera auto-framing still works right.
|
|
732
|
+
for k in 0..3 {
|
|
733
|
+
if final_pos[k] < bbox_min[k] { bbox_min[k] = final_pos[k]; }
|
|
734
|
+
if final_pos[k] > bbox_max[k] { bbox_max[k] = final_pos[k]; }
|
|
735
|
+
}
|
|
736
|
+
vertices.push(Vertex3D {
|
|
737
|
+
position: final_pos,
|
|
738
|
+
normal: final_normal,
|
|
739
|
+
color,
|
|
740
|
+
uv: tex_coords[i],
|
|
741
|
+
joints: jv,
|
|
742
|
+
weights: wv,
|
|
743
|
+
tangent: final_tangent,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
let indices: Vec<u32> = match reader.read_indices() {
|
|
747
|
+
Some(iter) => iter.into_u32().collect(),
|
|
748
|
+
None => (0..positions.len() as u32).collect(),
|
|
749
|
+
};
|
|
750
|
+
meshes.push(MeshData {
|
|
751
|
+
vertices,
|
|
752
|
+
indices,
|
|
753
|
+
texture_idx: tex_idx,
|
|
754
|
+
normal_texture_idx: normal_tex_idx,
|
|
755
|
+
metallic_roughness_texture_idx: mr_tex_idx,
|
|
756
|
+
emissive_texture_idx: emissive_tex_idx,
|
|
757
|
+
occlusion_texture_idx: occlusion_tex_idx,
|
|
758
|
+
metallic_factor,
|
|
759
|
+
roughness_factor,
|
|
760
|
+
emissive_factor,
|
|
761
|
+
alpha_cutoff: alpha_cutoff_from_material(&mat),
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
} // end instance loop
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if meshes.is_empty() { return None; }
|
|
768
|
+
Some(ModelData { meshes, bbox_min, bbox_max })
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/// Like load_gltf_with_textures but decodes textures to RGBA without GPU registration.
|
|
772
|
+
/// Returns a StagedModel with decoded textures that can later be committed on the main thread.
|
|
773
|
+
pub fn load_gltf_staged(data: &[u8]) -> Option<crate::staging::StagedModel> {
|
|
774
|
+
use crate::staging::{StagedTexture, StagedModel};
|
|
775
|
+
|
|
776
|
+
let gltf = gltf::Gltf::from_slice(data).ok()?;
|
|
777
|
+
|
|
778
|
+
let mut buffer_data: Vec<Vec<u8>> = Vec::new();
|
|
779
|
+
for buffer in gltf.buffers() {
|
|
780
|
+
match buffer.source() {
|
|
781
|
+
gltf::buffer::Source::Bin => {
|
|
782
|
+
if let Some(blob) = gltf.blob.as_ref() { buffer_data.push(blob.clone()); }
|
|
783
|
+
}
|
|
784
|
+
gltf::buffer::Source::Uri(uri) => {
|
|
785
|
+
if let Some(encoded) = uri.strip_prefix("data:application/octet-stream;base64,") {
|
|
786
|
+
let mut decoded = Vec::new();
|
|
787
|
+
let _ = base64_decode(encoded, &mut decoded);
|
|
788
|
+
buffer_data.push(decoded);
|
|
789
|
+
} else {
|
|
790
|
+
buffer_data.push(Vec::new());
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Pre-walk materials for the image indices used as normal maps — they
|
|
797
|
+
// must be registered via register_texture_kind's linear/LEADR path at
|
|
798
|
+
// commit time (same pre-walk as load_gltf_with_textures).
|
|
799
|
+
let mut normal_image_set: std::collections::HashSet<usize> = Default::default();
|
|
800
|
+
for mat in gltf.materials() {
|
|
801
|
+
if let Some(nt) = mat.normal_texture() {
|
|
802
|
+
normal_image_set.insert(nt.texture().source().index());
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Decode textures to RGBA without GPU registration.
|
|
807
|
+
// staged_textures[i] corresponds to glTF image index i.
|
|
808
|
+
// texture_indices maps glTF image index -> 1-based index into staged_textures (0 = no texture).
|
|
809
|
+
let mut staged_textures: Vec<StagedTexture> = Vec::new();
|
|
810
|
+
let mut texture_indices: Vec<u32> = Vec::new();
|
|
811
|
+
for (image_idx, image) in gltf.images().enumerate() {
|
|
812
|
+
match image.source() {
|
|
813
|
+
gltf::image::Source::View { view, .. } => {
|
|
814
|
+
let buf_idx = view.buffer().index();
|
|
815
|
+
if buf_idx < buffer_data.len() {
|
|
816
|
+
let offset = view.offset();
|
|
817
|
+
let length = view.length();
|
|
818
|
+
if offset + length <= buffer_data[buf_idx].len() {
|
|
819
|
+
let img_data = &buffer_data[buf_idx][offset..offset + length];
|
|
820
|
+
if let Ok(img) = image::load_from_memory(img_data) {
|
|
821
|
+
let rgba = img.to_rgba8();
|
|
822
|
+
let (w, h) = (rgba.width(), rgba.height());
|
|
823
|
+
staged_textures.push(StagedTexture {
|
|
824
|
+
data: rgba.into_raw(),
|
|
825
|
+
width: w,
|
|
826
|
+
height: h,
|
|
827
|
+
is_normal: normal_image_set.contains(&image_idx),
|
|
828
|
+
});
|
|
829
|
+
// 1-based index into staged_textures
|
|
830
|
+
texture_indices.push(staged_textures.len() as u32);
|
|
831
|
+
} else {
|
|
832
|
+
texture_indices.push(0);
|
|
833
|
+
}
|
|
834
|
+
} else {
|
|
835
|
+
texture_indices.push(0);
|
|
836
|
+
}
|
|
837
|
+
} else {
|
|
838
|
+
texture_indices.push(0);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
_ => { texture_indices.push(0); }
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Detect armature scale (same logic as load_gltf_with_textures)
|
|
846
|
+
let skin_vertex_scale: f32 = {
|
|
847
|
+
let mut scale = 1.0f32;
|
|
848
|
+
for node in gltf.nodes() {
|
|
849
|
+
if node.mesh().is_some() && node.skin().is_some() {
|
|
850
|
+
for parent in gltf.nodes() {
|
|
851
|
+
for child in parent.children() {
|
|
852
|
+
if child.index() == node.index() {
|
|
853
|
+
let (_, _, s) = parent.transform().decomposed();
|
|
854
|
+
let avg_scale = (s[0] + s[1] + s[2]) / 3.0;
|
|
855
|
+
if avg_scale > 0.001 && (avg_scale - 1.0).abs() > 0.01 {
|
|
856
|
+
scale = 1.0 / avg_scale;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (scale - 1.0).abs() < 0.01 {
|
|
864
|
+
if let Some(skin) = gltf.skins().next() {
|
|
865
|
+
if let Some(accessor) = skin.inverse_bind_matrices() {
|
|
866
|
+
let view = accessor.view().unwrap();
|
|
867
|
+
let buf_idx = view.buffer().index();
|
|
868
|
+
if buf_idx < buffer_data.len() {
|
|
869
|
+
let offset = view.offset() + accessor.offset();
|
|
870
|
+
let data = &buffer_data[buf_idx];
|
|
871
|
+
if offset + 12 <= data.len() {
|
|
872
|
+
let f0 = f32::from_le_bytes([data[offset], data[offset+1], data[offset+2], data[offset+3]]);
|
|
873
|
+
let f1 = f32::from_le_bytes([data[offset+4], data[offset+5], data[offset+6], data[offset+7]]);
|
|
874
|
+
let f2 = f32::from_le_bytes([data[offset+8], data[offset+9], data[offset+10], data[offset+11]]);
|
|
875
|
+
let diag = (f0*f0 + f1*f1 + f2*f2).sqrt();
|
|
876
|
+
if diag > 10.0 {
|
|
877
|
+
scale = diag;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
scale
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
let mut meshes = Vec::new();
|
|
888
|
+
let mut bbox_min = [f32::MAX; 3];
|
|
889
|
+
let mut bbox_max = [f32::MIN; 3];
|
|
890
|
+
|
|
891
|
+
for mesh in gltf.meshes() {
|
|
892
|
+
for primitive in mesh.primitives() {
|
|
893
|
+
let reader = primitive.reader(|buf| buffer_data.get(buf.index()).map(|d| d.as_slice()));
|
|
894
|
+
let positions: Vec<[f32; 3]> = match reader.read_positions() {
|
|
895
|
+
Some(iter) => iter.collect(),
|
|
896
|
+
None => continue,
|
|
897
|
+
};
|
|
898
|
+
let normals: Vec<[f32; 3]> = reader.read_normals()
|
|
899
|
+
.map(|iter| iter.collect())
|
|
900
|
+
.unwrap_or_else(|| vec![[0.0, 1.0, 0.0]; positions.len()]);
|
|
901
|
+
let tex_coords: Vec<[f32; 2]> = reader.read_tex_coords(0)
|
|
902
|
+
.map(|iter| iter.into_f32().collect())
|
|
903
|
+
.unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
|
|
904
|
+
let tangents: Vec<[f32; 4]> = reader.read_tangents()
|
|
905
|
+
.map(|iter| iter.collect())
|
|
906
|
+
.unwrap_or_else(|| vec![[0.0; 4]; positions.len()]);
|
|
907
|
+
let vert_colors: Option<Vec<[f32; 4]>> = reader.read_colors(0)
|
|
908
|
+
.map(|iter| iter.into_rgba_f32().collect());
|
|
909
|
+
|
|
910
|
+
let mat = primitive.material();
|
|
911
|
+
let pbr = mat.pbr_metallic_roughness();
|
|
912
|
+
let emissive_factor = mat.emissive_factor();
|
|
913
|
+
|
|
914
|
+
let tex_idx_of = |img_idx: usize| -> Option<u32> {
|
|
915
|
+
texture_indices.get(img_idx).copied()
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
let normal_tex_idx = mat.normal_texture()
|
|
919
|
+
.and_then(|info| tex_idx_of(info.texture().source().index()));
|
|
920
|
+
let emissive_tex_idx = mat.emissive_texture()
|
|
921
|
+
.and_then(|info| tex_idx_of(info.texture().source().index()));
|
|
922
|
+
let occlusion_tex_idx = mat.occlusion_texture()
|
|
923
|
+
.and_then(|info| tex_idx_of(info.texture().source().index()));
|
|
924
|
+
|
|
925
|
+
// Prefer the glTF 2.0 metallic-roughness model. Fall back
|
|
926
|
+
// to KHR_materials_pbrSpecularGlossiness when the material
|
|
927
|
+
// only ships the legacy spec-gloss extension (Lumberyard
|
|
928
|
+
// Bistro and many FBX-exported scenes do). Conversion
|
|
929
|
+
// follows the reference Khronos algorithm: pick metallic
|
|
930
|
+
// that best explains the diffuse/specular split under the
|
|
931
|
+
// assumption of a 0.04 dielectric baseline, then blend
|
|
932
|
+
// base_color between diffuse and specular weighted by
|
|
933
|
+
// metallic² (metals tint their reflection, dielectrics
|
|
934
|
+
// show their diffuse).
|
|
935
|
+
let (mut base_color, mut metallic_factor, mut roughness_factor, tex_idx, mr_tex_idx) =
|
|
936
|
+
if pbr.base_color_texture().is_none() {
|
|
937
|
+
if let Some(sg) = mat.pbr_specular_glossiness() {
|
|
938
|
+
let diffuse = sg.diffuse_factor();
|
|
939
|
+
let spec = sg.specular_factor();
|
|
940
|
+
let (base_color, metallic) =
|
|
941
|
+
specgloss_to_metalrough(diffuse, spec);
|
|
942
|
+
let roughness = 1.0 - sg.glossiness_factor();
|
|
943
|
+
let diffuse_tex = sg.diffuse_texture()
|
|
944
|
+
.and_then(|info| tex_idx_of(info.texture().source().index()));
|
|
945
|
+
(base_color, metallic, roughness, diffuse_tex, None)
|
|
946
|
+
} else {
|
|
947
|
+
(pbr.base_color_factor(), pbr.metallic_factor(), pbr.roughness_factor(), None, None)
|
|
948
|
+
}
|
|
949
|
+
} else {
|
|
950
|
+
let tex = pbr.base_color_texture()
|
|
951
|
+
.and_then(|info| tex_idx_of(info.texture().source().index()));
|
|
952
|
+
let mr = pbr.metallic_roughness_texture()
|
|
953
|
+
.and_then(|info| tex_idx_of(info.texture().source().index()));
|
|
954
|
+
(pbr.base_color_factor(), pbr.metallic_factor(), pbr.roughness_factor(), tex, mr)
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
if let Some(t) = mat.transmission() {
|
|
958
|
+
apply_transmission_hack(
|
|
959
|
+
t.transmission_factor(),
|
|
960
|
+
&mut base_color,
|
|
961
|
+
&mut metallic_factor,
|
|
962
|
+
&mut roughness_factor,
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
let mut vertices = Vec::with_capacity(positions.len());
|
|
967
|
+
for i in 0..positions.len() {
|
|
968
|
+
let p = positions[i];
|
|
969
|
+
for k in 0..3 {
|
|
970
|
+
if p[k] < bbox_min[k] { bbox_min[k] = p[k]; }
|
|
971
|
+
if p[k] > bbox_max[k] { bbox_max[k] = p[k]; }
|
|
972
|
+
}
|
|
973
|
+
let color = if let Some(ref vc) = vert_colors {
|
|
974
|
+
vc[i]
|
|
975
|
+
} else {
|
|
976
|
+
[base_color[0], base_color[1], base_color[2], base_color[3]]
|
|
977
|
+
};
|
|
978
|
+
let joint_vals: Option<Vec<[u16; 4]>> = reader.read_joints(0)
|
|
979
|
+
.map(|iter| iter.into_u16().collect());
|
|
980
|
+
let weight_vals: Option<Vec<[f32; 4]>> = reader.read_weights(0)
|
|
981
|
+
.map(|iter| iter.into_f32().collect());
|
|
982
|
+
let jv = if let Some(ref j) = joint_vals {
|
|
983
|
+
[j[i][0] as f32, j[i][1] as f32, j[i][2] as f32, j[i][3] as f32]
|
|
984
|
+
} else {
|
|
985
|
+
[0.0; 4]
|
|
986
|
+
};
|
|
987
|
+
let wv = if let Some(ref w) = weight_vals { w[i] } else { [0.0; 4] };
|
|
988
|
+
let is_skinned = wv[0] + wv[1] + wv[2] + wv[3] > 0.01;
|
|
989
|
+
let final_pos = if is_skinned && (skin_vertex_scale - 1.0).abs() > 0.01 {
|
|
990
|
+
[p[0] * skin_vertex_scale, p[1] * skin_vertex_scale, p[2] * skin_vertex_scale]
|
|
991
|
+
} else {
|
|
992
|
+
p
|
|
993
|
+
};
|
|
994
|
+
vertices.push(Vertex3D {
|
|
995
|
+
position: final_pos,
|
|
996
|
+
normal: normals[i],
|
|
997
|
+
color,
|
|
998
|
+
uv: tex_coords[i],
|
|
999
|
+
joints: jv,
|
|
1000
|
+
weights: wv,
|
|
1001
|
+
tangent: tangents[i],
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
let indices: Vec<u32> = match reader.read_indices() {
|
|
1005
|
+
Some(iter) => iter.into_u32().collect(),
|
|
1006
|
+
None => (0..positions.len() as u32).collect(),
|
|
1007
|
+
};
|
|
1008
|
+
meshes.push(MeshData {
|
|
1009
|
+
vertices,
|
|
1010
|
+
indices,
|
|
1011
|
+
texture_idx: tex_idx,
|
|
1012
|
+
normal_texture_idx: normal_tex_idx,
|
|
1013
|
+
metallic_roughness_texture_idx: mr_tex_idx,
|
|
1014
|
+
emissive_texture_idx: emissive_tex_idx,
|
|
1015
|
+
occlusion_texture_idx: occlusion_tex_idx,
|
|
1016
|
+
metallic_factor,
|
|
1017
|
+
roughness_factor,
|
|
1018
|
+
emissive_factor,
|
|
1019
|
+
alpha_cutoff: alpha_cutoff_from_material(&mat),
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if meshes.is_empty() { return None; }
|
|
1025
|
+
Some(StagedModel {
|
|
1026
|
+
model: ModelData { meshes, bbox_min, bbox_max },
|
|
1027
|
+
textures: staged_textures,
|
|
1028
|
+
})
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
pub(super) fn load_gltf(data: &[u8]) -> Option<ModelData> {
|
|
1032
|
+
let gltf = gltf::Gltf::from_slice(data).ok()?;
|
|
1033
|
+
|
|
1034
|
+
// Get buffer data (for .glb, embedded; for .gltf, inline base64)
|
|
1035
|
+
let mut buffer_data: Vec<Vec<u8>> = Vec::new();
|
|
1036
|
+
for buffer in gltf.buffers() {
|
|
1037
|
+
match buffer.source() {
|
|
1038
|
+
gltf::buffer::Source::Bin => {
|
|
1039
|
+
if let Some(blob) = gltf.blob.as_ref() {
|
|
1040
|
+
buffer_data.push(blob.clone());
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
gltf::buffer::Source::Uri(uri) => {
|
|
1044
|
+
if let Some(encoded) = uri.strip_prefix("data:application/octet-stream;base64,") {
|
|
1045
|
+
// Try to decode base64 inline data
|
|
1046
|
+
let mut decoded = Vec::new();
|
|
1047
|
+
let _ = base64_decode(encoded, &mut decoded);
|
|
1048
|
+
buffer_data.push(decoded);
|
|
1049
|
+
} else {
|
|
1050
|
+
buffer_data.push(Vec::new());
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
let mut meshes = Vec::new();
|
|
1057
|
+
let mut bbox_min = [f32::MAX; 3];
|
|
1058
|
+
let mut bbox_max = [f32::MIN; 3];
|
|
1059
|
+
|
|
1060
|
+
for mesh in gltf.meshes() {
|
|
1061
|
+
for primitive in mesh.primitives() {
|
|
1062
|
+
let reader = primitive.reader(|buf| buffer_data.get(buf.index()).map(|d| d.as_slice()));
|
|
1063
|
+
|
|
1064
|
+
let positions: Vec<[f32; 3]> = match reader.read_positions() {
|
|
1065
|
+
Some(iter) => iter.collect(),
|
|
1066
|
+
None => continue,
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
let normals: Vec<[f32; 3]> = reader.read_normals()
|
|
1070
|
+
.map(|iter| iter.collect())
|
|
1071
|
+
.unwrap_or_else(|| vec![[0.0, 1.0, 0.0]; positions.len()]);
|
|
1072
|
+
|
|
1073
|
+
let tex_coords: Vec<[f32; 2]> = reader.read_tex_coords(0)
|
|
1074
|
+
.map(|iter| iter.into_f32().collect())
|
|
1075
|
+
.unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
|
|
1076
|
+
|
|
1077
|
+
// Material base color
|
|
1078
|
+
let base_color = primitive.material().pbr_metallic_roughness()
|
|
1079
|
+
.base_color_factor();
|
|
1080
|
+
let color = [base_color[0], base_color[1], base_color[2], base_color[3]];
|
|
1081
|
+
|
|
1082
|
+
let mut vertices = Vec::with_capacity(positions.len());
|
|
1083
|
+
for i in 0..positions.len() {
|
|
1084
|
+
let p = positions[i];
|
|
1085
|
+
for k in 0..3 {
|
|
1086
|
+
if p[k] < bbox_min[k] { bbox_min[k] = p[k]; }
|
|
1087
|
+
if p[k] > bbox_max[k] { bbox_max[k] = p[k]; }
|
|
1088
|
+
}
|
|
1089
|
+
// Read skin data if available
|
|
1090
|
+
let joint_vals: Option<Vec<[u16; 4]>> = reader.read_joints(0)
|
|
1091
|
+
.map(|iter| iter.into_u16().collect());
|
|
1092
|
+
let weight_vals: Option<Vec<[f32; 4]>> = reader.read_weights(0)
|
|
1093
|
+
.map(|iter| iter.into_f32().collect());
|
|
1094
|
+
let jv = if let Some(ref j) = joint_vals {
|
|
1095
|
+
[j[i][0] as f32, j[i][1] as f32, j[i][2] as f32, j[i][3] as f32]
|
|
1096
|
+
} else { [0.0; 4] };
|
|
1097
|
+
let wv = if let Some(ref w) = weight_vals { w[i] } else { [0.0; 4] };
|
|
1098
|
+
|
|
1099
|
+
vertices.push(Vertex3D {
|
|
1100
|
+
position: p,
|
|
1101
|
+
normal: normals[i],
|
|
1102
|
+
color,
|
|
1103
|
+
uv: tex_coords[i],
|
|
1104
|
+
joints: jv,
|
|
1105
|
+
weights: wv,
|
|
1106
|
+
tangent: [0.0; 4],
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
let indices: Vec<u32> = match reader.read_indices() {
|
|
1111
|
+
Some(iter) => iter.into_u32().collect(),
|
|
1112
|
+
None => (0..positions.len() as u32).collect(),
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
meshes.push(MeshData { vertices, indices, texture_idx: None, normal_texture_idx: None, metallic_roughness_texture_idx: None, emissive_texture_idx: None, occlusion_texture_idx: None, metallic_factor: 0.0, roughness_factor: 1.0, emissive_factor: [0.0; 3], alpha_cutoff: 0.0 });
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
if meshes.is_empty() { return None; }
|
|
1120
|
+
Some(ModelData { meshes, bbox_min, bbox_max })
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/// Convert a KHR_materials_pbrSpecularGlossiness (diffuse + specular
|
|
1124
|
+
/// + glossiness) material to the metallic-roughness model. Uses the
|
|
1125
|
+
/// reference Khronos two-path formula so materials authored in
|
|
1126
|
+
/// Substance/3ds Max/FBX pipelines (Lumberyard Bistro, many ORCA
|
|
1127
|
+
/// assets) render correctly on a metal-rough pipeline.
|
|
1128
|
+
///
|
|
1129
|
+
/// High-level idea: assume a 0.04 dielectric reflectance baseline,
|
|
1130
|
+
/// solve for the metallic factor that best reconciles the authored
|
|
1131
|
+
/// diffuse and specular colors, then blend base_color between the
|
|
1132
|
+
/// two weighted by metallic². Metals have specular ≈ albedo, so the
|
|
1133
|
+
/// specular color becomes their base_color; dielectrics carry their
|
|
1134
|
+
/// diffuse color through at metallic ≈ 0.
|
|
1135
|
+
///
|
|
1136
|
+
/// Map glTF alpha mode + cutoff to a single shader cutoff value.
|
|
1137
|
+
/// OPAQUE → 0.0 (fragment shader's `< cutoff` discard never fires).
|
|
1138
|
+
/// MASK → material-authored cutoff (default 0.5 per glTF spec).
|
|
1139
|
+
/// BLEND → treated as MASK @ 0.5 since we don't yet have a sorted
|
|
1140
|
+
/// transparent pipeline. Better than silently rendering
|
|
1141
|
+
/// foliage + fabric as fully opaque — an alpha-cutout leaf
|
|
1142
|
+
/// card is at least the right *shape*.
|
|
1143
|
+
fn alpha_cutoff_from_material(mat: &gltf::Material) -> f32 {
|
|
1144
|
+
match mat.alpha_mode() {
|
|
1145
|
+
gltf::material::AlphaMode::Opaque => 0.0,
|
|
1146
|
+
gltf::material::AlphaMode::Mask => mat.alpha_cutoff().unwrap_or(0.5),
|
|
1147
|
+
gltf::material::AlphaMode::Blend => 0.5,
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/// Fake KHR_materials_transmission as a near-mirror dielectric.
|
|
1152
|
+
///
|
|
1153
|
+
/// We don't implement real refractive transmission (no back-buffer
|
|
1154
|
+
/// refraction pass, no thin-walled/volume distinction), so a
|
|
1155
|
+
/// transmission=0.9 glass pane loads as a plain diffuse white surface
|
|
1156
|
+
/// that drowns the 4% Fresnel specular in a bright diffuse term — the
|
|
1157
|
+
/// classic "painted white window" look.
|
|
1158
|
+
///
|
|
1159
|
+
/// As a stand-in we force heavy transmission materials to behave like
|
|
1160
|
+
/// chrome: metallic=1 so f0=base_color (not 0.04), roughness ≤ 0.05
|
|
1161
|
+
/// so reflections stay crisp, and a mild (0.85×) tint on base_color
|
|
1162
|
+
/// so pure-white glass doesn't read as perfectly reflective chrome.
|
|
1163
|
+
/// Not physically correct for glass — real glass only reflects 4% at
|
|
1164
|
+
/// normal incidence — but it matches how windows *read* in photos
|
|
1165
|
+
/// (reflecting sky/buildings) far better than a flat diffuse surface.
|
|
1166
|
+
fn apply_transmission_hack(
|
|
1167
|
+
transmission: f32,
|
|
1168
|
+
base_color: &mut [f32; 4],
|
|
1169
|
+
metallic: &mut f32,
|
|
1170
|
+
roughness: &mut f32,
|
|
1171
|
+
) {
|
|
1172
|
+
if transmission > 0.5 {
|
|
1173
|
+
*metallic = 1.0;
|
|
1174
|
+
*roughness = roughness.min(0.05);
|
|
1175
|
+
base_color[0] *= 0.85;
|
|
1176
|
+
base_color[1] *= 0.85;
|
|
1177
|
+
base_color[2] *= 0.85;
|
|
1178
|
+
base_color[3] = 1.0;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/// Reference: Khronos glTF sample specGloss→metallicRoughness
|
|
1183
|
+
/// converter (https://github.com/KhronosGroup/glTF/pull/1355).
|
|
1184
|
+
fn specgloss_to_metalrough(diffuse: [f32; 4], specular: [f32; 3]) -> ([f32; 4], f32) {
|
|
1185
|
+
let dielectric_specular = 0.04_f32;
|
|
1186
|
+
let epsilon = 1e-6_f32;
|
|
1187
|
+
|
|
1188
|
+
let one_minus_dielectric = 1.0 - dielectric_specular;
|
|
1189
|
+
let diffuse_max = diffuse[0].max(diffuse[1]).max(diffuse[2]);
|
|
1190
|
+
let specular_max = specular[0].max(specular[1]).max(specular[2]);
|
|
1191
|
+
|
|
1192
|
+
// Solve a quadratic for metallic. Coefficients from the Khronos
|
|
1193
|
+
// reference: mapping perceived brightness split between diffuse
|
|
1194
|
+
// and specular back to a single metallic parameter.
|
|
1195
|
+
let a = dielectric_specular;
|
|
1196
|
+
let b = diffuse_max * one_minus_dielectric / dielectric_specular.max(epsilon)
|
|
1197
|
+
+ specular_max
|
|
1198
|
+
- 2.0 * dielectric_specular;
|
|
1199
|
+
let c = dielectric_specular - specular_max;
|
|
1200
|
+
let discriminant = (b * b - 4.0 * a * c).max(0.0);
|
|
1201
|
+
let metallic = if specular_max < dielectric_specular {
|
|
1202
|
+
0.0
|
|
1203
|
+
} else {
|
|
1204
|
+
(((-b + discriminant.sqrt()) / (2.0 * a)).clamp(0.0, 1.0)).min(1.0)
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
// base_color = mix(diffuse, specular, metallic²) with the diffuse
|
|
1208
|
+
// branch scaled to undo the dielectric energy split.
|
|
1209
|
+
let diffuse_branch_scale = one_minus_dielectric
|
|
1210
|
+
/ (1.0 - metallic * dielectric_specular).max(epsilon);
|
|
1211
|
+
let metal_weight = metallic * metallic;
|
|
1212
|
+
let lerp = |a: f32, b: f32, t: f32| a * (1.0 - t) + b * t;
|
|
1213
|
+
let r = lerp(diffuse[0] * diffuse_branch_scale, specular[0], metal_weight);
|
|
1214
|
+
let g = lerp(diffuse[1] * diffuse_branch_scale, specular[1], metal_weight);
|
|
1215
|
+
let bl = lerp(diffuse[2] * diffuse_branch_scale, specular[2], metal_weight);
|
|
1216
|
+
([r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), bl.clamp(0.0, 1.0), diffuse[3]], metallic)
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/// Replace the extension on a URI (keeps directories / query strings
|
|
1220
|
+
/// untouched). Used to fall back from `foo.png` → `foo.dds` when a
|
|
1221
|
+
/// glTF references a PNG URI that isn't on disk but the DDS sibling is.
|
|
1222
|
+
fn swap_extension(uri: &str, new_ext: &str) -> String {
|
|
1223
|
+
let q = uri.find('?').unwrap_or(uri.len());
|
|
1224
|
+
let (path, query) = uri.split_at(q);
|
|
1225
|
+
let new_path = match path.rfind('.') {
|
|
1226
|
+
Some(dot) if dot > path.rfind('/').unwrap_or(0) => {
|
|
1227
|
+
format!("{}.{}", &path[..dot], new_ext)
|
|
1228
|
+
}
|
|
1229
|
+
_ => format!("{}.{}", path, new_ext),
|
|
1230
|
+
};
|
|
1231
|
+
format!("{}{}", new_path, query)
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/// Decode a texture byte slice into RGBA8 pixels + dimensions. Tries
|
|
1235
|
+
/// DDS first when the URI extension suggests it (for asset packs like
|
|
1236
|
+
/// Lumberyard Bistro that ship BC-compressed textures), falling back
|
|
1237
|
+
/// to the `image` crate for PNG/JPEG/etc. Returns None on failure.
|
|
1238
|
+
fn decode_texture_bytes(bytes: &[u8], uri: &str) -> Option<(Vec<u8>, u32, u32)> {
|
|
1239
|
+
let is_dds = uri.to_ascii_lowercase().ends_with(".dds")
|
|
1240
|
+
|| bytes.len() >= 4 && &bytes[..4] == b"DDS ";
|
|
1241
|
+
if is_dds {
|
|
1242
|
+
if let Ok(dds) = image_dds::ddsfile::Dds::read(bytes) {
|
|
1243
|
+
// Decode mip 0 → RGBA8. image_from_dds handles the common
|
|
1244
|
+
// BC1–BC7 formats; anything it can't decode falls through
|
|
1245
|
+
// to the image crate which will almost certainly fail too.
|
|
1246
|
+
if let Ok(rgba) = image_dds::image_from_dds(&dds, 0) {
|
|
1247
|
+
let (w, h) = (rgba.width(), rgba.height());
|
|
1248
|
+
return Some((rgba.into_raw(), w, h));
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
let img = image::load_from_memory(bytes).ok()?;
|
|
1253
|
+
let rgba = img.to_rgba8();
|
|
1254
|
+
let (w, h) = (rgba.width(), rgba.height());
|
|
1255
|
+
Some((rgba.into_raw(), w, h))
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
fn base64_decode(input: &str, output: &mut Vec<u8>) {
|
|
1259
|
+
let mut buf = 0u32;
|
|
1260
|
+
let mut bits = 0u32;
|
|
1261
|
+
for &b in input.as_bytes() {
|
|
1262
|
+
let val = match b {
|
|
1263
|
+
b'A'..=b'Z' => b - b'A',
|
|
1264
|
+
b'a'..=b'z' => b - b'a' + 26,
|
|
1265
|
+
b'0'..=b'9' => b - b'0' + 52,
|
|
1266
|
+
b'+' => 62,
|
|
1267
|
+
b'/' => 63,
|
|
1268
|
+
b'=' | b'\n' | b'\r' => continue,
|
|
1269
|
+
_ => continue,
|
|
1270
|
+
};
|
|
1271
|
+
buf = (buf << 6) | val as u32;
|
|
1272
|
+
bits += 6;
|
|
1273
|
+
if bits >= 8 {
|
|
1274
|
+
bits -= 8;
|
|
1275
|
+
output.push((buf >> bits) as u8);
|
|
1276
|
+
buf &= (1 << bits) - 1;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|