@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.
Files changed (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +231 -0
  3. package/native/android/Cargo.lock +1848 -0
  4. package/native/android/Cargo.toml +24 -0
  5. package/native/android/src/lib.rs +702 -0
  6. package/native/ios/Cargo.lock +1690 -0
  7. package/native/ios/Cargo.toml +32 -0
  8. package/native/ios/src/lib.rs +1267 -0
  9. package/native/linux/Cargo.lock +3279 -0
  10. package/native/linux/Cargo.toml +29 -0
  11. package/native/linux/src/lib.rs +1331 -0
  12. package/native/macos/Cargo.lock +3310 -0
  13. package/native/macos/Cargo.toml +46 -0
  14. package/native/macos/src/lib.rs +1302 -0
  15. package/native/shared/Cargo.lock +1899 -0
  16. package/native/shared/Cargo.toml +62 -0
  17. package/native/shared/assets/default_font.ttf +0 -0
  18. package/native/shared/build.rs +270 -0
  19. package/native/shared/shaders/common/clouds.wgsl +122 -0
  20. package/native/shared/shaders/common/fog.wgsl +16 -0
  21. package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
  22. package/native/shared/shaders/common/imposter.wgsl +112 -0
  23. package/native/shared/shaders/common/pbr.wgsl +186 -0
  24. package/native/shared/shaders/common/shadows.wgsl +186 -0
  25. package/native/shared/shaders/common/sky.wgsl +8 -0
  26. package/native/shared/shaders/common/tonemap.wgsl +25 -0
  27. package/native/shared/shaders/impulse_field.wgsl +57 -0
  28. package/native/shared/shaders/material_abi.wgsl +383 -0
  29. package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
  30. package/native/shared/src/anim_mixer.rs +61 -0
  31. package/native/shared/src/attach.rs +263 -0
  32. package/native/shared/src/audio/decode.rs +123 -0
  33. package/native/shared/src/audio/mod.rs +863 -0
  34. package/native/shared/src/audio/render.rs +892 -0
  35. package/native/shared/src/audio/spsc.rs +156 -0
  36. package/native/shared/src/audio/stream.rs +226 -0
  37. package/native/shared/src/custom_shaders.rs +104 -0
  38. package/native/shared/src/decals.rs +245 -0
  39. package/native/shared/src/drs.rs +211 -0
  40. package/native/shared/src/engine.rs +261 -0
  41. package/native/shared/src/ffi.rs +116 -0
  42. package/native/shared/src/ffi_core/assets.rs +388 -0
  43. package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
  44. package/native/shared/src/ffi_core/draw.rs +334 -0
  45. package/native/shared/src/ffi_core/game_loop.rs +577 -0
  46. package/native/shared/src/ffi_core/input.rs +234 -0
  47. package/native/shared/src/ffi_core/mod.rs +127 -0
  48. package/native/shared/src/ffi_core/models.rs +1154 -0
  49. package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
  50. package/native/shared/src/ffi_core/scene.rs +626 -0
  51. package/native/shared/src/ffi_core/vfx.rs +212 -0
  52. package/native/shared/src/ffi_core/visual.rs +691 -0
  53. package/native/shared/src/frame_callbacks.rs +122 -0
  54. package/native/shared/src/geometry.rs +236 -0
  55. package/native/shared/src/handles.rs +182 -0
  56. package/native/shared/src/input.rs +448 -0
  57. package/native/shared/src/jolt_sys.rs +822 -0
  58. package/native/shared/src/lib.rs +55 -0
  59. package/native/shared/src/models.rs +1093 -0
  60. package/native/shared/src/models_gltf.rs +1280 -0
  61. package/native/shared/src/particles.rs +391 -0
  62. package/native/shared/src/physics_jolt.rs +1908 -0
  63. package/native/shared/src/picking.rs +298 -0
  64. package/native/shared/src/postfx.rs +345 -0
  65. package/native/shared/src/profiler.rs +492 -0
  66. package/native/shared/src/ragdoll.rs +474 -0
  67. package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
  68. package/native/shared/src/renderer/brdf_lut.rs +154 -0
  69. package/native/shared/src/renderer/draw2d.rs +143 -0
  70. package/native/shared/src/renderer/formats.rs +822 -0
  71. package/native/shared/src/renderer/froxel.rs +421 -0
  72. package/native/shared/src/renderer/gi_bake.rs +653 -0
  73. package/native/shared/src/renderer/graph.rs +462 -0
  74. package/native/shared/src/renderer/hiz.rs +269 -0
  75. package/native/shared/src/renderer/hot_reload.rs +390 -0
  76. package/native/shared/src/renderer/impulse_field.rs +456 -0
  77. package/native/shared/src/renderer/lighting.rs +154 -0
  78. package/native/shared/src/renderer/material_instancing.rs +171 -0
  79. package/native/shared/src/renderer/material_pipeline.rs +700 -0
  80. package/native/shared/src/renderer/material_system.rs +1996 -0
  81. package/native/shared/src/renderer/material_system_tests.rs +601 -0
  82. package/native/shared/src/renderer/material_system_wasm.rs +41 -0
  83. package/native/shared/src/renderer/mod.rs +12556 -0
  84. package/native/shared/src/renderer/model_draw.rs +641 -0
  85. package/native/shared/src/renderer/occlusion.rs +429 -0
  86. package/native/shared/src/renderer/planar_pass.rs +593 -0
  87. package/native/shared/src/renderer/planar_reflection.rs +499 -0
  88. package/native/shared/src/renderer/post_pass.rs +249 -0
  89. package/native/shared/src/renderer/postfx_chain.rs +728 -0
  90. package/native/shared/src/renderer/pt_pass.rs +577 -0
  91. package/native/shared/src/renderer/scene_pass.rs +607 -0
  92. package/native/shared/src/renderer/shader_include.rs +205 -0
  93. package/native/shared/src/renderer/shader_library.rs +135 -0
  94. package/native/shared/src/renderer/shaders/ao.rs +570 -0
  95. package/native/shared/src/renderer/shaders/core.rs +1243 -0
  96. package/native/shared/src/renderer/shaders/env.rs +907 -0
  97. package/native/shared/src/renderer/shaders/gi.rs +810 -0
  98. package/native/shared/src/renderer/shaders/mod.rs +19 -0
  99. package/native/shared/src/renderer/shaders/post.rs +1558 -0
  100. package/native/shared/src/renderer/shaders/pt.rs +1859 -0
  101. package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
  102. package/native/shared/src/renderer/shadow_pass.rs +731 -0
  103. package/native/shared/src/renderer/ssgi_pass.rs +392 -0
  104. package/native/shared/src/renderer/ssr_pass.rs +188 -0
  105. package/native/shared/src/renderer/texture_store.rs +473 -0
  106. package/native/shared/src/renderer/transient.rs +591 -0
  107. package/native/shared/src/renderer/types.rs +941 -0
  108. package/native/shared/src/renderer/util.rs +152 -0
  109. package/native/shared/src/scene.rs +1362 -0
  110. package/native/shared/src/sdf_cache.rs +274 -0
  111. package/native/shared/src/shadows.rs +1036 -0
  112. package/native/shared/src/staging.rs +102 -0
  113. package/native/shared/src/string_header.rs +266 -0
  114. package/native/shared/src/text_renderer.rs +502 -0
  115. package/native/shared/src/textures.rs +197 -0
  116. package/native/tvos/Cargo.lock +1693 -0
  117. package/native/tvos/Cargo.toml +36 -0
  118. package/native/tvos/metal-patched/Cargo.toml +178 -0
  119. package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
  120. package/native/tvos/metal-patched/LICENSE-MIT +25 -0
  121. package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
  122. package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
  123. package/native/tvos/metal-patched/src/argument.rs +366 -0
  124. package/native/tvos/metal-patched/src/blitpass.rs +102 -0
  125. package/native/tvos/metal-patched/src/buffer.rs +71 -0
  126. package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
  127. package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
  128. package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
  129. package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
  130. package/native/tvos/metal-patched/src/computepass.rs +107 -0
  131. package/native/tvos/metal-patched/src/constants.rs +152 -0
  132. package/native/tvos/metal-patched/src/counters.rs +119 -0
  133. package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
  134. package/native/tvos/metal-patched/src/device.rs +2134 -0
  135. package/native/tvos/metal-patched/src/drawable.rs +39 -0
  136. package/native/tvos/metal-patched/src/encoder.rs +2041 -0
  137. package/native/tvos/metal-patched/src/heap.rs +281 -0
  138. package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
  139. package/native/tvos/metal-patched/src/lib.rs +657 -0
  140. package/native/tvos/metal-patched/src/library.rs +902 -0
  141. package/native/tvos/metal-patched/src/mps.rs +575 -0
  142. package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
  143. package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
  144. package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
  145. package/native/tvos/metal-patched/src/renderpass.rs +443 -0
  146. package/native/tvos/metal-patched/src/resource.rs +182 -0
  147. package/native/tvos/metal-patched/src/sampler.rs +165 -0
  148. package/native/tvos/metal-patched/src/sync.rs +178 -0
  149. package/native/tvos/metal-patched/src/texture.rs +352 -0
  150. package/native/tvos/metal-patched/src/types.rs +90 -0
  151. package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
  152. package/native/tvos/src/audio_backend.rs +197 -0
  153. package/native/tvos/src/lib.rs +1891 -0
  154. package/native/visionos/Cargo.lock +1693 -0
  155. package/native/visionos/Cargo.toml +40 -0
  156. package/native/visionos/src/audio_backend.rs +197 -0
  157. package/native/visionos/src/lib.rs +1887 -0
  158. package/native/watchos/Cargo.lock +16 -0
  159. package/native/watchos/Cargo.toml +19 -0
  160. package/native/watchos/shaders/bloom_postfx.metal +99 -0
  161. package/native/watchos/src/BloomWatchApp.swift +1267 -0
  162. package/native/watchos/src/BloomWatchAudio.swift +179 -0
  163. package/native/watchos/src/audio.rs +55 -0
  164. package/native/watchos/src/draw_list.rs +229 -0
  165. package/native/watchos/src/ffi_stubs.rs +915 -0
  166. package/native/watchos/src/ffi_stubs_manual.rs +35 -0
  167. package/native/watchos/src/lib.rs +1124 -0
  168. package/native/watchos/src/models.rs +746 -0
  169. package/native/watchos/src/postfx.rs +95 -0
  170. package/native/watchos/src/scene.rs +534 -0
  171. package/native/watchos/src/textures.rs +184 -0
  172. package/native/web/Cargo.lock +1657 -0
  173. package/native/web/Cargo.toml +43 -0
  174. package/native/web/bloom_glue.js +695 -0
  175. package/native/web/build.sh +131 -0
  176. package/native/web/index.html +35 -0
  177. package/native/web/jolt_bridge.js +1519 -0
  178. package/native/web/src/input_ffi.rs +286 -0
  179. package/native/web/src/lib.rs +1796 -0
  180. package/native/web/src/material_ffi.rs +710 -0
  181. package/native/web/src/parity_ffi.rs +343 -0
  182. package/native/web/src/physics_ffi.rs +643 -0
  183. package/native/web/src/ragdoll_ffi.rs +250 -0
  184. package/native/web/src/render_settings.rs +98 -0
  185. package/native/windows/Cargo.lock +1815 -0
  186. package/native/windows/Cargo.toml +68 -0
  187. package/native/windows/src/lib.rs +1486 -0
  188. package/package.json +4279 -0
  189. package/src/audio/index.ts +315 -0
  190. package/src/core/colors.ts +63 -0
  191. package/src/core/index.ts +1206 -0
  192. package/src/core/keys.ts +63 -0
  193. package/src/core/types.ts +104 -0
  194. package/src/index.ts +171 -0
  195. package/src/math/index.ts +516 -0
  196. package/src/mobile/index.ts +294 -0
  197. package/src/models/index.ts +1258 -0
  198. package/src/physics/index.ts +1134 -0
  199. package/src/scene/index.ts +698 -0
  200. package/src/shapes/index.ts +120 -0
  201. package/src/text/index.ts +48 -0
  202. package/src/textures/index.ts +187 -0
  203. package/src/vfx/index.ts +191 -0
  204. package/src/world/index.ts +24 -0
  205. package/src/world/loader.ts +423 -0
  206. package/src/world/prefab.ts +217 -0
  207. package/src/world/render.ts +172 -0
  208. package/src/world/saver.ts +108 -0
  209. package/src/world/serialize.ts +301 -0
  210. package/src/world/terrain.ts +355 -0
  211. package/src/world/types.ts +160 -0
  212. package/src/world/validate.ts +319 -0
  213. package/src/world/version.ts +114 -0
@@ -0,0 +1,95 @@
1
+ //! Post-processing effect state. Atomic floats the Swift root view polls
2
+ //! each frame and pushes to either SwiftUI view modifiers (cheap effects)
3
+ //! or the SceneKit SCNTechnique uniform binding (Metal-shader effects).
4
+ //!
5
+ //! What maps cleanly onto built-in SwiftUI modifiers (applies over both
6
+ //! the 2D Canvas and 3D layers):
7
+ //! - vignette → `.overlay(RadialGradient)` (strength + softness)
8
+ //! - manual exposure → `.brightness(ev - 1.0)` approximation
9
+ //! - auto exposure → identity (SceneKit's own tone mapping handles most of it)
10
+ //!
11
+ //! What runs through the Metal shader in shaders/bloom_postfx.metal,
12
+ //! attached via SCNTechnique to the 3D SceneView (only affects the 3D
13
+ //! layer — 2D-only games see just the SwiftUI modifiers above):
14
+ //! - chromatic aberration (per-channel position offset)
15
+ //! - film grain (time-animated noise)
16
+ //! - sun shafts (radial blur + light scattering)
17
+ //!
18
+ //! See `BloomPostFXTechnique` in BloomWatchApp.swift — uniforms reach the
19
+ //! technique via keyed-subscript `technique[symbol] = NSData(...)`, the
20
+ //! one binding path that exists on watchOS (no SCNRenderer in the SDK).
21
+
22
+ use std::sync::atomic::{AtomicU64, AtomicBool, Ordering};
23
+
24
+ static ENABLED: AtomicBool = AtomicBool::new(true);
25
+ static VIGNETTE_STRENGTH: AtomicU64 = AtomicU64::new(0);
26
+ static VIGNETTE_SOFTNESS: AtomicU64 = AtomicU64::new(0);
27
+ static CA_STRENGTH: AtomicU64 = AtomicU64::new(0);
28
+ static FILM_GRAIN: AtomicU64 = AtomicU64::new(0);
29
+ static EXPOSURE: AtomicU64 = AtomicU64::new(1.0f64.to_bits());
30
+ static AUTO_EXPOSURE: AtomicBool = AtomicBool::new(false);
31
+ static SUN_STRENGTH: AtomicU64 = AtomicU64::new(0);
32
+ static SUN_DECAY: AtomicU64 = AtomicU64::new(0.85f64.to_bits());
33
+ static SUN_R: AtomicU64 = AtomicU64::new(1.0f64.to_bits());
34
+ static SUN_G: AtomicU64 = AtomicU64::new(0.9f64.to_bits());
35
+ static SUN_B: AtomicU64 = AtomicU64::new(0.7f64.to_bits());
36
+
37
+ fn store(a: &AtomicU64, v: f64) { a.store(v.to_bits(), Ordering::Relaxed); }
38
+ fn load(a: &AtomicU64) -> f64 { f64::from_bits(a.load(Ordering::Relaxed)) }
39
+
40
+ pub fn set_enabled(on: bool) { ENABLED.store(on, Ordering::Relaxed); }
41
+ pub fn set_vignette(strength: f64, softness: f64) {
42
+ store(&VIGNETTE_STRENGTH, strength);
43
+ store(&VIGNETTE_SOFTNESS, softness);
44
+ }
45
+ pub fn set_chromatic_aberration(s: f64) { store(&CA_STRENGTH, s); }
46
+ pub fn set_film_grain(s: f64) { store(&FILM_GRAIN, s); }
47
+ pub fn set_exposure(v: f64) {
48
+ store(&EXPOSURE, v);
49
+ AUTO_EXPOSURE.store(false, Ordering::Relaxed);
50
+ }
51
+ pub fn set_auto_exposure(on: bool) { AUTO_EXPOSURE.store(on, Ordering::Relaxed); }
52
+
53
+ pub fn set_sun_shafts(strength: f64, decay: f64, r: f64, g: f64, b: f64) {
54
+ store(&SUN_STRENGTH, strength);
55
+ store(&SUN_DECAY, decay);
56
+ store(&SUN_R, r);
57
+ store(&SUN_G, g);
58
+ store(&SUN_B, b);
59
+ }
60
+
61
+ #[repr(C)]
62
+ #[derive(Clone, Copy)]
63
+ pub struct PostFxState {
64
+ pub enabled: u32,
65
+ pub auto_exposure: u32,
66
+ pub vignette_strength: f32,
67
+ pub vignette_softness: f32,
68
+ pub chromatic_aberration: f32,
69
+ pub film_grain: f32,
70
+ pub exposure: f32,
71
+ pub sun_strength: f32,
72
+ pub sun_decay: f32,
73
+ pub sun_r: f32,
74
+ pub sun_g: f32,
75
+ pub sun_b: f32,
76
+ }
77
+
78
+ pub fn snapshot(out: *mut PostFxState) {
79
+ if out.is_null() { return; }
80
+ let s = PostFxState {
81
+ enabled: if ENABLED.load(Ordering::Relaxed) { 1 } else { 0 },
82
+ auto_exposure: if AUTO_EXPOSURE.load(Ordering::Relaxed) { 1 } else { 0 },
83
+ vignette_strength: load(&VIGNETTE_STRENGTH) as f32,
84
+ vignette_softness: load(&VIGNETTE_SOFTNESS) as f32,
85
+ chromatic_aberration: load(&CA_STRENGTH) as f32,
86
+ film_grain: load(&FILM_GRAIN) as f32,
87
+ exposure: load(&EXPOSURE) as f32,
88
+ sun_strength: load(&SUN_STRENGTH) as f32,
89
+ sun_decay: load(&SUN_DECAY) as f32,
90
+ sun_r: load(&SUN_R) as f32,
91
+ sun_g: load(&SUN_G) as f32,
92
+ sun_b: load(&SUN_B) as f32,
93
+ };
94
+ unsafe { *out = s; }
95
+ }
@@ -0,0 +1,534 @@
1
+ //! Retained scene graph for `bloom_scene_*` FFI.
2
+ //!
3
+ //! Mirrors `bloom-shared/scene.rs` but stripped down to what SceneKit can
4
+ //! consume: nodes carry a 4x4 transform, color/PBR material, optional
5
+ //! texture handle, and an optional indexed triangle mesh. Swift polls a
6
+ //! snapshot each frame via `bloom_watchos_scene_*` FFI and mirrors the
7
+ //! graph into SCNNodes.
8
+ //!
9
+ //! `geometry_version` bumps on every `update_geometry` call so the Swift
10
+ //! side can cache `SCNGeometry` objects across frames — rebuilding geometry
11
+ //! each frame for static meshes would burn the watch's GPU.
12
+
13
+ use std::sync::Mutex;
14
+
15
+ // Dirty-flag bits — Node.dirty_flags is the bitwise-OR of these. Each Node
16
+ // mutator sets its corresponding bit; drain_dirty reports them to Swift and
17
+ // clears on consumption. Newly created nodes start with DIRTY_ALL so Swift
18
+ // sees every field on the first drain.
19
+ pub const DIRTY_TRANSFORM: u32 = 1 << 0;
20
+ pub const DIRTY_MATERIAL: u32 = 1 << 1;
21
+ pub const DIRTY_VISIBILITY: u32 = 1 << 2;
22
+ pub const DIRTY_PARENT: u32 = 1 << 3;
23
+ pub const DIRTY_GEOMETRY: u32 = 1 << 4;
24
+ pub const DIRTY_SKIN: u32 = 1 << 5;
25
+ pub const DIRTY_ALL: u32 = DIRTY_TRANSFORM | DIRTY_MATERIAL | DIRTY_VISIBILITY
26
+ | DIRTY_PARENT | DIRTY_GEOMETRY | DIRTY_SKIN;
27
+
28
+ #[repr(C)]
29
+ #[derive(Clone, Copy)]
30
+ pub struct SceneNodeInfo {
31
+ pub dirty_flags: u32,
32
+ pub handle: u32,
33
+ pub parent: u32,
34
+ pub visible: u32,
35
+ pub geometry_version: u32,
36
+
37
+ pub color: [f32; 4],
38
+ pub roughness: f32,
39
+ pub metalness: f32,
40
+ /// Legacy single-slot texture handle (diffuse / base color). Kept for
41
+ /// bloom_scene_set_material_texture which predates the PBR slot set.
42
+ pub texture: u32,
43
+ pub has_geometry: u32,
44
+
45
+ pub transform: [f32; 16],
46
+
47
+ /// PBR texture slots — set via bloom_scene_attach_model from a loaded
48
+ /// glTF material. 0 = not set (fall back to color / roughness /
49
+ /// metalness factors instead).
50
+ pub tex_base_color: u32,
51
+ pub tex_normal: u32,
52
+ pub tex_metallic_roughness: u32,
53
+ pub tex_emissive: u32,
54
+ pub tex_occlusion: u32,
55
+ /// Bumps whenever any skin_* data changes on this node. Swift rebuilds
56
+ /// the SCNSkinner when it sees a new version. 0 = no skin.
57
+ pub skin_version: u32,
58
+ }
59
+
60
+ struct Node {
61
+ visible: bool,
62
+ parent: u32,
63
+ transform: [f32; 16],
64
+ color: [f32; 4],
65
+ roughness: f32,
66
+ metalness: f32,
67
+ texture: u32,
68
+ tex_base_color: u32,
69
+ tex_normal: u32,
70
+ tex_metallic_roughness: u32,
71
+ tex_emissive: u32,
72
+ tex_occlusion: u32,
73
+ positions: Vec<f32>,
74
+ normals: Vec<f32>,
75
+ uvs: Vec<f32>,
76
+ indices: Vec<u32>,
77
+ geometry_version: u32,
78
+ /// Bitmask of DIRTY_* flags that need to go out to Swift on the next
79
+ /// drain. Freshly created nodes start with DIRTY_ALL so Swift builds a
80
+ /// full SCNNode on first sight.
81
+ dirty_flags: u32,
82
+
83
+ // Skinning data (SCNSkinner). Present only on the mesh-carrying node of
84
+ // a skinned glTF primitive. skin_version bumps when any of the fields
85
+ // below change, signalling Swift to rebuild the skinner.
86
+ skin_joint_handles: Vec<u32>, // bloom scene handles of the bones
87
+ skin_inverse_bind: Vec<[f32; 16]>, // one per joint
88
+ skin_vertex_joints: Vec<u32>, // 4 per vertex (as in glTF JOINTS_0)
89
+ skin_vertex_weights: Vec<f32>, // 4 per vertex (as in glTF WEIGHTS_0)
90
+ skin_version: u32,
91
+
92
+ // Animation tracks attached to this skinned mesh. Each track targets a
93
+ // bone by bloom handle. Swift builds CAKeyframeAnimations from them when
94
+ // attaching the SCNSkinner. anim_version bumps alongside skin_version.
95
+ anim_tracks: Vec<AnimTrack>,
96
+ }
97
+
98
+ pub struct AnimTrack {
99
+ pub bone_handle: u32,
100
+ pub path: u32, // 0 = translation, 1 = rotation, 2 = scale
101
+ pub times: Vec<f32>,
102
+ pub values: Vec<f32>,
103
+ }
104
+
105
+ impl Node {
106
+ fn empty() -> Self {
107
+ let mut t = [0.0f32; 16];
108
+ t[0] = 1.0; t[5] = 1.0; t[10] = 1.0; t[15] = 1.0; // identity
109
+ Self {
110
+ visible: true,
111
+ parent: 0,
112
+ transform: t,
113
+ color: [1.0, 1.0, 1.0, 1.0],
114
+ roughness: 0.6,
115
+ metalness: 0.0,
116
+ texture: 0,
117
+ tex_base_color: 0, tex_normal: 0, tex_metallic_roughness: 0,
118
+ tex_emissive: 0, tex_occlusion: 0,
119
+ positions: Vec::new(),
120
+ normals: Vec::new(),
121
+ uvs: Vec::new(),
122
+ indices: Vec::new(),
123
+ geometry_version: 0,
124
+ dirty_flags: DIRTY_ALL,
125
+ skin_joint_handles: Vec::new(),
126
+ skin_inverse_bind: Vec::new(),
127
+ skin_vertex_joints: Vec::new(),
128
+ skin_vertex_weights: Vec::new(),
129
+ skin_version: 0,
130
+ anim_tracks: Vec::new(),
131
+ }
132
+ }
133
+ }
134
+
135
+ #[repr(C)]
136
+ #[derive(Clone, Copy)]
137
+ pub struct Light {
138
+ pub kind: u32, // 1 = directional, 2 = point
139
+ pub _pad: u32,
140
+ pub pos_or_dir: [f32; 3],
141
+ pub range: f32,
142
+ pub color: [f32; 3],
143
+ pub intensity: f32,
144
+ }
145
+
146
+ struct Inner {
147
+ nodes: Vec<Option<Node>>, // 1-based: nodes[0] is always None (handle 0 = "none")
148
+ lights: Vec<Light>,
149
+ /// Handles of nodes destroyed since the last Swift drain. Swift consumes
150
+ /// via drain_destroyed() and removes the matching SCNNodes from its cache.
151
+ destroyed_queue: Vec<u32>,
152
+ }
153
+
154
+ static INNER: Mutex<Inner> = Mutex::new(Inner {
155
+ nodes: Vec::new(),
156
+ lights: Vec::new(),
157
+ destroyed_queue: Vec::new(),
158
+ });
159
+
160
+ fn with<F, R>(f: F) -> R where F: FnOnce(&mut Inner) -> R {
161
+ let mut g = INNER.lock().unwrap();
162
+ if g.nodes.is_empty() {
163
+ g.nodes.push(None); // sentinel at index 0
164
+ }
165
+ f(&mut *g)
166
+ }
167
+
168
+ // ============================================================
169
+ // Mutations — called from bloom_scene_* FFI in lib.rs
170
+ // ============================================================
171
+
172
+ pub fn create() -> u32 {
173
+ with(|inner| {
174
+ // Reuse a freed slot if one exists.
175
+ for (i, slot) in inner.nodes.iter_mut().enumerate().skip(1) {
176
+ if slot.is_none() {
177
+ *slot = Some(Node::empty());
178
+ return i as u32;
179
+ }
180
+ }
181
+ inner.nodes.push(Some(Node::empty()));
182
+ (inner.nodes.len() - 1) as u32
183
+ })
184
+ }
185
+
186
+ pub fn destroy(handle: u32) {
187
+ with(|inner| {
188
+ let existed = if let Some(slot) = inner.nodes.get_mut(handle as usize) {
189
+ if slot.is_some() { *slot = None; true } else { false }
190
+ } else { false };
191
+ if existed { inner.destroyed_queue.push(handle); }
192
+ // Any node with this as parent becomes a root — flag PARENT dirty
193
+ // so Swift reparents it.
194
+ for n in inner.nodes.iter_mut().flatten() {
195
+ if n.parent == handle {
196
+ n.parent = 0;
197
+ n.dirty_flags |= DIRTY_PARENT;
198
+ }
199
+ }
200
+ });
201
+ }
202
+
203
+ fn mutate<F: FnOnce(&mut Node)>(handle: u32, f: F) {
204
+ with(|inner| {
205
+ if let Some(Some(n)) = inner.nodes.get_mut(handle as usize) {
206
+ f(n);
207
+ }
208
+ });
209
+ }
210
+
211
+ fn mark_dirty<F: FnOnce(&mut Node)>(handle: u32, bits: u32, f: F) {
212
+ mutate(handle, |n| {
213
+ f(n);
214
+ n.dirty_flags |= bits;
215
+ });
216
+ }
217
+
218
+ pub fn set_visible(handle: u32, v: bool) {
219
+ mark_dirty(handle, DIRTY_VISIBILITY, |n| n.visible = v);
220
+ }
221
+ pub fn set_parent(handle: u32, parent: u32) {
222
+ mark_dirty(handle, DIRTY_PARENT, |n| n.parent = parent);
223
+ }
224
+ pub fn set_transform(handle: u32, m: [f32; 16]) {
225
+ mark_dirty(handle, DIRTY_TRANSFORM, |n| n.transform = m);
226
+ }
227
+ pub fn set_color(handle: u32, rgba: [f32; 4]) {
228
+ mark_dirty(handle, DIRTY_MATERIAL, |n| n.color = rgba);
229
+ }
230
+ pub fn set_pbr(handle: u32, rough: f32, metal: f32) {
231
+ mark_dirty(handle, DIRTY_MATERIAL, |n| { n.roughness = rough; n.metalness = metal; });
232
+ }
233
+ pub fn set_texture(handle: u32, tex: u32) {
234
+ mark_dirty(handle, DIRTY_MATERIAL, |n| n.texture = tex);
235
+ }
236
+
237
+ pub fn set_pbr_textures(handle: u32,
238
+ base_color: u32, normal: u32, metallic_roughness: u32,
239
+ emissive: u32, occlusion: u32,
240
+ ) {
241
+ mark_dirty(handle, DIRTY_MATERIAL, |n| {
242
+ n.tex_base_color = base_color;
243
+ n.tex_normal = normal;
244
+ n.tex_metallic_roughness = metallic_roughness;
245
+ n.tex_emissive = emissive;
246
+ n.tex_occlusion = occlusion;
247
+ });
248
+ }
249
+
250
+ pub fn set_skin(handle: u32,
251
+ joint_handles: Vec<u32>, inverse_bind: Vec<[f32; 16]>,
252
+ vertex_joints: Vec<u32>, vertex_weights: Vec<f32>,
253
+ ) {
254
+ mark_dirty(handle, DIRTY_SKIN, |n| {
255
+ n.skin_joint_handles = joint_handles;
256
+ n.skin_inverse_bind = inverse_bind;
257
+ n.skin_vertex_joints = vertex_joints;
258
+ n.skin_vertex_weights = vertex_weights;
259
+ n.skin_version = n.skin_version.wrapping_add(1);
260
+ });
261
+ }
262
+
263
+ /// Replace the animation tracks on a skinned mesh node. Bumps skin_version
264
+ /// so Swift re-picks them up alongside the skinner rebuild.
265
+ pub fn set_anim_tracks(handle: u32, tracks: Vec<AnimTrack>) {
266
+ mark_dirty(handle, DIRTY_SKIN, |n| {
267
+ n.anim_tracks = tracks;
268
+ n.skin_version = n.skin_version.wrapping_add(1);
269
+ });
270
+ }
271
+
272
+ /// TS → native FFI layout: 12 f64s per vertex — xyz, nx ny nz, rgba, uv.
273
+ /// Color is ignored (material color wins via bloom_scene_set_material_color).
274
+ /// Directly install pre-decoded geometry (from a loaded model) into a scene
275
+ /// node, bypassing the 12-f64-per-vertex TS FFI layout.
276
+ pub fn set_geometry(handle: u32,
277
+ positions: Vec<f32>, normals: Vec<f32>, uvs: Vec<f32>, indices: Vec<u32>,
278
+ ) {
279
+ mark_dirty(handle, DIRTY_GEOMETRY, |n| {
280
+ n.positions = positions;
281
+ n.normals = normals;
282
+ n.uvs = uvs;
283
+ n.indices = indices;
284
+ n.geometry_version = n.geometry_version.wrapping_add(1);
285
+ });
286
+ }
287
+
288
+ pub fn update_geometry_f64(handle: u32, verts: &[f64], indices: &[f64]) {
289
+ mark_dirty(handle, DIRTY_GEOMETRY, |n| {
290
+ let count = verts.len() / 12;
291
+ n.positions.clear();
292
+ n.normals.clear();
293
+ n.uvs.clear();
294
+ n.positions.reserve(count * 3);
295
+ n.normals.reserve(count * 3);
296
+ n.uvs.reserve(count * 2);
297
+ for i in 0..count {
298
+ let base = i * 12;
299
+ n.positions.push(verts[base] as f32);
300
+ n.positions.push(verts[base+1] as f32);
301
+ n.positions.push(verts[base+2] as f32);
302
+ n.normals.push(verts[base+3] as f32);
303
+ n.normals.push(verts[base+4] as f32);
304
+ n.normals.push(verts[base+5] as f32);
305
+ n.uvs.push(verts[base+10] as f32);
306
+ n.uvs.push(verts[base+11] as f32);
307
+ }
308
+ n.indices.clear();
309
+ n.indices.reserve(indices.len());
310
+ for &i in indices { n.indices.push(i as u32); }
311
+ n.geometry_version = n.geometry_version.wrapping_add(1);
312
+ });
313
+ }
314
+
315
+ pub fn add_directional_light(dx: f32, dy: f32, dz: f32, r: f32, g: f32, b: f32, intensity: f32) {
316
+ with(|inner| {
317
+ inner.lights.push(Light {
318
+ kind: 1, _pad: 0,
319
+ pos_or_dir: [dx, dy, dz], range: 0.0,
320
+ color: [r, g, b], intensity,
321
+ });
322
+ });
323
+ }
324
+
325
+ pub fn add_point_light(x: f32, y: f32, z: f32, range: f32, r: f32, g: f32, b: f32, intensity: f32) {
326
+ with(|inner| {
327
+ inner.lights.push(Light {
328
+ kind: 2, _pad: 0,
329
+ pos_or_dir: [x, y, z], range,
330
+ color: [r, g, b], intensity,
331
+ });
332
+ });
333
+ }
334
+
335
+ pub fn clear_lights() {
336
+ with(|inner| inner.lights.clear());
337
+ }
338
+
339
+ pub fn node_count() -> usize {
340
+ with(|inner| inner.nodes.iter().filter(|n| n.is_some()).count())
341
+ }
342
+
343
+ // ============================================================
344
+ // Snapshot API — called from Swift each frame.
345
+ // ============================================================
346
+
347
+ /// Delta-sync: emit only nodes whose dirty_flags are non-zero, and clear
348
+ /// flags on the nodes we report. Swift calls this once per frame and
349
+ /// applies the reported changes — static scenes get O(0) cost, moving
350
+ /// scenes scale with the number of actually-changing nodes.
351
+ pub fn drain_dirty(dst: *mut SceneNodeInfo, max: i64) -> i64 {
352
+ if dst.is_null() || max <= 0 { return 0; }
353
+ with(|inner| {
354
+ let mut written = 0i64;
355
+ for (i, slot) in inner.nodes.iter_mut().enumerate() {
356
+ if written >= max { break; }
357
+ let Some(n) = slot else { continue };
358
+ if n.dirty_flags == 0 { continue; }
359
+ let info = SceneNodeInfo {
360
+ dirty_flags: n.dirty_flags,
361
+ handle: i as u32,
362
+ parent: n.parent,
363
+ visible: if n.visible { 1 } else { 0 },
364
+ geometry_version: n.geometry_version,
365
+ color: n.color,
366
+ roughness: n.roughness,
367
+ metalness: n.metalness,
368
+ texture: n.texture,
369
+ has_geometry: if n.indices.is_empty() { 0 } else { 1 },
370
+ transform: n.transform,
371
+ tex_base_color: n.tex_base_color,
372
+ tex_normal: n.tex_normal,
373
+ tex_metallic_roughness: n.tex_metallic_roughness,
374
+ tex_emissive: n.tex_emissive,
375
+ tex_occlusion: n.tex_occlusion,
376
+ skin_version: n.skin_version,
377
+ };
378
+ unsafe { *dst.add(written as usize) = info; }
379
+ n.dirty_flags = 0;
380
+ written += 1;
381
+ }
382
+ written
383
+ })
384
+ }
385
+
386
+ /// Drain the list of handles destroyed since the last call. Swift pulls
387
+ /// these and removes the matching SCNNodes from its retainedNodes map.
388
+ pub fn drain_destroyed(dst: *mut u32, max: i64) -> i64 {
389
+ if dst.is_null() || max <= 0 { return 0; }
390
+ with(|inner| {
391
+ let n = inner.destroyed_queue.len().min(max as usize);
392
+ unsafe {
393
+ std::ptr::copy_nonoverlapping(inner.destroyed_queue.as_ptr(), dst, n);
394
+ }
395
+ inner.destroyed_queue.drain(..n);
396
+ n as i64
397
+ })
398
+ }
399
+
400
+ pub fn copy_lights(dst: *mut Light, max: i64) -> i64 {
401
+ if dst.is_null() || max <= 0 { return 0; }
402
+ with(|inner| {
403
+ let n = inner.lights.len().min(max as usize);
404
+ unsafe { std::ptr::copy_nonoverlapping(inner.lights.as_ptr(), dst, n); }
405
+ n as i64
406
+ })
407
+ }
408
+
409
+ /// Pointer + counts for a node's geometry arrays. Pointers are valid as long
410
+ /// as the node exists and `update_geometry` isn't called on it — Swift reads
411
+ /// synchronously in one pass so this is safe for the sync protocol.
412
+ #[repr(C)]
413
+ #[derive(Clone, Copy)]
414
+ pub struct GeometryPtrs {
415
+ pub positions: *const f32,
416
+ pub position_count: u32,
417
+ pub normals: *const f32,
418
+ pub normal_count: u32,
419
+ pub uvs: *const f32,
420
+ pub uv_count: u32,
421
+ pub indices: *const u32,
422
+ pub index_count: u32,
423
+ }
424
+
425
+ #[repr(C)]
426
+ #[derive(Clone, Copy)]
427
+ pub struct SkinPtrs {
428
+ pub joint_handles: *const u32,
429
+ pub joint_count: u32,
430
+ pub inverse_bind: *const f32, // flat 16-float matrices
431
+ pub inverse_bind_matrix_count: u32,
432
+ pub vertex_joints: *const u32, // 4 per vertex
433
+ pub vertex_joint_count: u32, // total entries (= vertex_count * 4)
434
+ pub vertex_weights: *const f32, // 4 per vertex
435
+ pub vertex_weight_count: u32,
436
+ }
437
+
438
+ #[repr(C)]
439
+ #[derive(Clone, Copy)]
440
+ pub struct AnimTrackInfo {
441
+ pub bone_handle: u32,
442
+ pub path: u32, // 0 = translation, 1 = rotation, 2 = scale
443
+ pub key_count: u32,
444
+ pub _pad: u32,
445
+ pub times: *const f32, // length = key_count
446
+ pub values: *const f32, // length = key_count * (3 or 4)
447
+ }
448
+
449
+ pub fn anim_track_count(handle: u32) -> i64 {
450
+ with(|inner| {
451
+ if let Some(Some(n)) = inner.nodes.get(handle as usize) {
452
+ n.anim_tracks.len() as i64
453
+ } else { 0 }
454
+ })
455
+ }
456
+
457
+ pub fn anim_track_info(handle: u32, idx: i64, out: *mut AnimTrackInfo) {
458
+ if out.is_null() { return; }
459
+ with(|inner| {
460
+ if let Some(Some(n)) = inner.nodes.get(handle as usize) {
461
+ if let Some(t) = n.anim_tracks.get(idx as usize) {
462
+ unsafe {
463
+ *out = AnimTrackInfo {
464
+ bone_handle: t.bone_handle,
465
+ path: t.path,
466
+ key_count: t.times.len() as u32,
467
+ _pad: 0,
468
+ times: t.times.as_ptr(),
469
+ values: t.values.as_ptr(),
470
+ };
471
+ }
472
+ }
473
+ }
474
+ });
475
+ }
476
+
477
+ pub fn skin_ptrs(handle: u32) -> SkinPtrs {
478
+ with(|inner| {
479
+ let empty = SkinPtrs {
480
+ joint_handles: std::ptr::null(), joint_count: 0,
481
+ inverse_bind: std::ptr::null(), inverse_bind_matrix_count: 0,
482
+ vertex_joints: std::ptr::null(), vertex_joint_count: 0,
483
+ vertex_weights: std::ptr::null(), vertex_weight_count: 0,
484
+ };
485
+ if let Some(Some(n)) = inner.nodes.get(handle as usize) {
486
+ SkinPtrs {
487
+ joint_handles: n.skin_joint_handles.as_ptr(),
488
+ joint_count: n.skin_joint_handles.len() as u32,
489
+ inverse_bind: n.skin_inverse_bind.as_ptr() as *const f32,
490
+ inverse_bind_matrix_count: n.skin_inverse_bind.len() as u32,
491
+ vertex_joints: n.skin_vertex_joints.as_ptr(),
492
+ vertex_joint_count: n.skin_vertex_joints.len() as u32,
493
+ vertex_weights: n.skin_vertex_weights.as_ptr(),
494
+ vertex_weight_count: n.skin_vertex_weights.len() as u32,
495
+ }
496
+ } else {
497
+ empty
498
+ }
499
+ })
500
+ }
501
+
502
+ pub fn geometry_ptrs(handle: u32) -> GeometryPtrs {
503
+ with(|inner| {
504
+ if let Some(Some(n)) = inner.nodes.get(handle as usize) {
505
+ GeometryPtrs {
506
+ positions: n.positions.as_ptr(),
507
+ position_count: n.positions.len() as u32,
508
+ normals: n.normals.as_ptr(),
509
+ normal_count: n.normals.len() as u32,
510
+ uvs: n.uvs.as_ptr(),
511
+ uv_count: n.uvs.len() as u32,
512
+ indices: n.indices.as_ptr(),
513
+ index_count: n.indices.len() as u32,
514
+ }
515
+ } else {
516
+ GeometryPtrs {
517
+ positions: std::ptr::null(), position_count: 0,
518
+ normals: std::ptr::null(), normal_count: 0,
519
+ uvs: std::ptr::null(), uv_count: 0,
520
+ indices: std::ptr::null(), index_count: 0,
521
+ }
522
+ }
523
+ })
524
+ }
525
+
526
+ pub fn get_transform(handle: u32) -> [f32; 16] {
527
+ with(|inner| {
528
+ if let Some(Some(n)) = inner.nodes.get(handle as usize) {
529
+ n.transform
530
+ } else {
531
+ let mut t = [0.0f32; 16]; t[0] = 1.0; t[5] = 1.0; t[10] = 1.0; t[15] = 1.0; t
532
+ }
533
+ })
534
+ }