@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,1796 @@
|
|
|
1
|
+
use bloom_shared::engine::EngineState;
|
|
2
|
+
use bloom_shared::renderer::Renderer;
|
|
3
|
+
|
|
4
|
+
use wasm_bindgen::prelude::*;
|
|
5
|
+
use std::sync::OnceLock;
|
|
6
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
7
|
+
|
|
8
|
+
static mut ENGINE: OnceLock<EngineState> = OnceLock::new();
|
|
9
|
+
static mut LAST_PROJECT: (f64, f64) = (0.0, 0.0);
|
|
10
|
+
static mut LAST_PICK: Option<bloom_shared::picking::PickResult> = None;
|
|
11
|
+
// Guards against double wgpu init when bloom_init_window is called twice
|
|
12
|
+
// (once by the JS orchestrator before Perry boots, and once by Perry's main).
|
|
13
|
+
static INIT_STARTED: AtomicBool = AtomicBool::new(false);
|
|
14
|
+
|
|
15
|
+
fn engine() -> &'static mut EngineState {
|
|
16
|
+
unsafe { ENGINE.get_mut().expect("Engine not initialized") }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Log to the browser console.
|
|
20
|
+
#[wasm_bindgen]
|
|
21
|
+
extern "C" {
|
|
22
|
+
#[wasm_bindgen(js_namespace = console, js_name = log)]
|
|
23
|
+
pub(crate) fn console_log(s: &str);
|
|
24
|
+
#[wasm_bindgen(js_namespace = console, js_name = warn)]
|
|
25
|
+
pub(crate) fn console_warn(s: &str);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Display handle for the wgpu-core (WebGL2) path.
|
|
29
|
+
///
|
|
30
|
+
/// When wgpu falls back to WebGL2 it goes through wgpu-core, whose
|
|
31
|
+
/// `create_surface` passes `raw_display_handle: None` for a canvas target and
|
|
32
|
+
/// then errors with `MissingDisplayHandle` unless the *instance* carries one
|
|
33
|
+
/// (wgpu-core 29 instance.rs:254). The WebGPU backend never looks at it. On the
|
|
34
|
+
/// web a display handle holds nothing, so this is pure ceremony — but without it
|
|
35
|
+
/// the WebGL2 fallback can't create a surface at all.
|
|
36
|
+
#[derive(Debug)]
|
|
37
|
+
struct WebDisplay;
|
|
38
|
+
|
|
39
|
+
impl wgpu::rwh::HasDisplayHandle for WebDisplay {
|
|
40
|
+
fn display_handle(&self) -> Result<wgpu::rwh::DisplayHandle<'_>, wgpu::rwh::HandleError> {
|
|
41
|
+
let raw = wgpu::rwh::RawDisplayHandle::Web(wgpu::rwh::WebDisplayHandle::new());
|
|
42
|
+
// SAFETY: a web display handle carries no pointer and cannot dangle.
|
|
43
|
+
Ok(unsafe { wgpu::rwh::DisplayHandle::borrow_raw(raw) })
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ============================================================
|
|
48
|
+
// Window
|
|
49
|
+
// ============================================================
|
|
50
|
+
|
|
51
|
+
/// Returns 1.0 once `ENGINE` has been populated by the async wgpu setup, 0.0
|
|
52
|
+
/// before then. Non-panicking — safe to call at any time. Used by the JS
|
|
53
|
+
/// orchestrator to gate Perry boot until the engine is actually usable.
|
|
54
|
+
#[wasm_bindgen]
|
|
55
|
+
pub fn bloom_is_initialized() -> f64 {
|
|
56
|
+
unsafe { if ENGINE.get().is_some() { 1.0 } else { 0.0 } }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Host-surface attach (PerryTS/perry#5519). Not applicable on web: the
|
|
60
|
+
/// engine builds its surface from the `bloom-canvas` DOM element in
|
|
61
|
+
/// `bloom_init_window`, and a wasm module can't be handed a native view
|
|
62
|
+
/// pointer. Present for FFI-surface uniformity; always returns 0.0 so web
|
|
63
|
+
/// hosts use the canvas-based `bloom_init_window` path instead.
|
|
64
|
+
#[wasm_bindgen]
|
|
65
|
+
pub fn bloom_attach_native(_handle: f64, _width: f64, _height: f64) -> f64 {
|
|
66
|
+
0.0
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[wasm_bindgen]
|
|
70
|
+
pub fn bloom_init_window(width: f64, height: f64, _title: f64, fullscreen: f64) {
|
|
71
|
+
// Set up panic hook for better error messages in the browser console
|
|
72
|
+
console_error_panic_hook::set_once();
|
|
73
|
+
|
|
74
|
+
// Idempotent: once an init is in flight (or done), later calls are no-ops.
|
|
75
|
+
// Perry's main() typically calls initWindow after the JS orchestrator has
|
|
76
|
+
// already kicked off wgpu setup — the second call must not start a new one.
|
|
77
|
+
if INIT_STARTED.swap(true, Ordering::SeqCst) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let log_w = width as u32;
|
|
82
|
+
let log_h = height as u32;
|
|
83
|
+
let _fullscreen = fullscreen != 0.0;
|
|
84
|
+
|
|
85
|
+
wasm_bindgen_futures::spawn_local(async move {
|
|
86
|
+
let window = web_sys::window().expect("no global window");
|
|
87
|
+
let document = window.document().expect("no document");
|
|
88
|
+
let canvas = document
|
|
89
|
+
.get_element_by_id("bloom-canvas")
|
|
90
|
+
.expect("no element with id 'bloom-canvas'")
|
|
91
|
+
.dyn_into::<web_sys::HtmlCanvasElement>()
|
|
92
|
+
.expect("element is not a canvas");
|
|
93
|
+
|
|
94
|
+
// HiDPI: size the canvas *backing store* (set_width/set_height,
|
|
95
|
+
// i.e. the WebGPU surface) to logical × devicePixelRatio so a
|
|
96
|
+
// 4K Retina display actually renders at 4K. CSS layout dimensions
|
|
97
|
+
// stay in logical pixels — index.html keeps `width: 100%` on
|
|
98
|
+
// the canvas — so the document layout is unchanged. Clamp to
|
|
99
|
+
// [1, 3] because higher dprs (some phones report 4) are wasted
|
|
100
|
+
// shading work and can crash low-end mobile GPUs.
|
|
101
|
+
let dpr = window.device_pixel_ratio().max(1.0).min(3.0);
|
|
102
|
+
let phys_w = ((log_w as f64) * dpr).round() as u32;
|
|
103
|
+
let phys_h = ((log_h as f64) * dpr).round() as u32;
|
|
104
|
+
canvas.set_width(phys_w);
|
|
105
|
+
canvas.set_height(phys_h);
|
|
106
|
+
|
|
107
|
+
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
|
108
|
+
backends: wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL,
|
|
109
|
+
..wgpu::InstanceDescriptor::new_with_display_handle(Box::new(WebDisplay))
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
let surface = instance
|
|
113
|
+
.create_surface(wgpu::SurfaceTarget::Canvas(canvas))
|
|
114
|
+
.expect("Failed to create surface from canvas");
|
|
115
|
+
|
|
116
|
+
let adapter = instance
|
|
117
|
+
.request_adapter(&wgpu::RequestAdapterOptions {
|
|
118
|
+
compatible_surface: Some(&surface),
|
|
119
|
+
power_preference: wgpu::PowerPreference::HighPerformance,
|
|
120
|
+
..Default::default()
|
|
121
|
+
})
|
|
122
|
+
.await
|
|
123
|
+
.expect("No WebGPU/WebGL adapter found");
|
|
124
|
+
|
|
125
|
+
// BC feature -> cooked BC7 textures upload compressed (else CPU-decode).
|
|
126
|
+
let required_features =
|
|
127
|
+
adapter.features() & wgpu::Features::TEXTURE_COMPRESSION_BC;
|
|
128
|
+
// On GL, `Limits::default()` is unsatisfiable: WebGL2 has no compute, so
|
|
129
|
+
// `max_compute_workgroups_per_dimension` is 0 against a default request of
|
|
130
|
+
// 65535 and device creation fails outright. Ask that backend for exactly what
|
|
131
|
+
// it reports instead.
|
|
132
|
+
//
|
|
133
|
+
// On WebGPU, raise the per-stage resource limits to whatever the adapter
|
|
134
|
+
// offers (EN-063): the material ABI's fragment stage samples 19 textures
|
|
135
|
+
// once the wasm scene-inputs fold-in lands in group 0, and the WebGPU
|
|
136
|
+
// DEFAULT of 16 rejects the pipeline layout — while desktop adapters
|
|
137
|
+
// advertise 48+. requiredLimits is the sanctioned way to claim them
|
|
138
|
+
// (unlike maxBindGroups, which is a hard cap). Everything else keeps the
|
|
139
|
+
// defaults so we notice, rather than silently depend on, exotic limits.
|
|
140
|
+
let required_limits = if adapter.get_info().backend == wgpu::Backend::Gl {
|
|
141
|
+
adapter.limits()
|
|
142
|
+
} else {
|
|
143
|
+
let a = adapter.limits();
|
|
144
|
+
let mut l = wgpu::Limits::default();
|
|
145
|
+
l.max_sampled_textures_per_shader_stage =
|
|
146
|
+
a.max_sampled_textures_per_shader_stage
|
|
147
|
+
.max(l.max_sampled_textures_per_shader_stage);
|
|
148
|
+
l.max_samplers_per_shader_stage =
|
|
149
|
+
a.max_samplers_per_shader_stage.max(l.max_samplers_per_shader_stage);
|
|
150
|
+
l.max_texture_array_layers =
|
|
151
|
+
a.max_texture_array_layers.max(l.max_texture_array_layers);
|
|
152
|
+
l
|
|
153
|
+
};
|
|
154
|
+
let (device, queue) = adapter
|
|
155
|
+
.request_device(
|
|
156
|
+
&wgpu::DeviceDescriptor {
|
|
157
|
+
label: Some("bloom_device"),
|
|
158
|
+
required_features,
|
|
159
|
+
required_limits,
|
|
160
|
+
..Default::default()
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
.await
|
|
164
|
+
.expect("Failed to create device");
|
|
165
|
+
|
|
166
|
+
let surface_caps = surface.get_capabilities(&adapter);
|
|
167
|
+
let format = surface_caps
|
|
168
|
+
.formats
|
|
169
|
+
.iter()
|
|
170
|
+
.find(|f| f.is_srgb())
|
|
171
|
+
.copied()
|
|
172
|
+
.unwrap_or(surface_caps.formats[0]);
|
|
173
|
+
|
|
174
|
+
// COPY_SRC (readback of the swapchain image) is unsupported on a WebGL2
|
|
175
|
+
// surface — asking for it there fails Surface::configure validation — so keep
|
|
176
|
+
// it only where the surface advertises it.
|
|
177
|
+
let usage = (wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC)
|
|
178
|
+
& surface_caps.usages;
|
|
179
|
+
|
|
180
|
+
let surface_config = wgpu::SurfaceConfiguration {
|
|
181
|
+
usage,
|
|
182
|
+
format,
|
|
183
|
+
width: phys_w,
|
|
184
|
+
height: phys_h,
|
|
185
|
+
present_mode: wgpu::PresentMode::Fifo,
|
|
186
|
+
alpha_mode: surface_caps.alpha_modes[0],
|
|
187
|
+
view_formats: vec![],
|
|
188
|
+
desired_maximum_frame_latency: 2,
|
|
189
|
+
};
|
|
190
|
+
surface.configure(&device, &surface_config);
|
|
191
|
+
|
|
192
|
+
let renderer = Renderer::new(device, queue, surface, surface_config, log_w, log_h);
|
|
193
|
+
let engine_state = EngineState::new(renderer);
|
|
194
|
+
|
|
195
|
+
unsafe {
|
|
196
|
+
let _ = ENGINE.set(engine_state);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let info = adapter.get_info();
|
|
200
|
+
console_log(&format!(
|
|
201
|
+
"Bloom engine initialized ({:?} on {})",
|
|
202
|
+
info.backend, info.name
|
|
203
|
+
));
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#[wasm_bindgen]
|
|
208
|
+
pub fn bloom_close_window() {
|
|
209
|
+
// TODO: Phase 3 — clean up resources if needed
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/// Web-only resize hook. Called from index.html when a ResizeObserver
|
|
213
|
+
/// or matchMedia(`(resolution: ...)`) signals the canvas's CSS box
|
|
214
|
+
/// changed or devicePixelRatio shifted (e.g. browser zoom). The JS
|
|
215
|
+
/// glue does the dpr math; we just receive both sizes and forward to
|
|
216
|
+
/// the renderer.
|
|
217
|
+
#[wasm_bindgen]
|
|
218
|
+
pub fn bloom_resize(physical_w: f64, physical_h: f64, logical_w: f64, logical_h: f64) {
|
|
219
|
+
let pw = (physical_w as u32).max(1);
|
|
220
|
+
let ph = (physical_h as u32).max(1);
|
|
221
|
+
let lw = (logical_w as u32).max(1);
|
|
222
|
+
let lh = (logical_h as u32).max(1);
|
|
223
|
+
unsafe {
|
|
224
|
+
if let Some(eng) = ENGINE.get_mut() {
|
|
225
|
+
if pw != eng.renderer.physical_width() || ph != eng.renderer.physical_height() {
|
|
226
|
+
eng.renderer.resize(pw, ph, lw, lh);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#[wasm_bindgen]
|
|
233
|
+
pub fn bloom_window_should_close() -> f64 {
|
|
234
|
+
// Web windows don't close the same way; always return false
|
|
235
|
+
0.0
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#[wasm_bindgen]
|
|
239
|
+
pub fn bloom_toggle_fullscreen() {
|
|
240
|
+
// Handled by JS glue — uses Fullscreen API on the canvas element
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#[wasm_bindgen]
|
|
244
|
+
pub fn bloom_set_window_title(_title: f64) {
|
|
245
|
+
// TODO: Phase 3 — set document.title (need string conversion)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#[wasm_bindgen]
|
|
249
|
+
pub fn bloom_set_window_icon(_path: f64) {
|
|
250
|
+
// TODO: Phase 3 — set favicon (need string conversion + fetch)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ============================================================
|
|
254
|
+
// Drawing
|
|
255
|
+
// ============================================================
|
|
256
|
+
|
|
257
|
+
#[wasm_bindgen]
|
|
258
|
+
pub fn bloom_begin_drawing() {
|
|
259
|
+
// No event polling needed on web — events are injected from JS via bloom_inject_*
|
|
260
|
+
engine().begin_frame();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#[wasm_bindgen]
|
|
264
|
+
pub fn bloom_end_drawing() {
|
|
265
|
+
engine().end_frame();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#[wasm_bindgen]
|
|
269
|
+
pub fn bloom_clear_background(r: f64, g: f64, b: f64, a: f64) {
|
|
270
|
+
engine().renderer.set_clear_color(r, g, b, a);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ============================================================
|
|
274
|
+
// Timing
|
|
275
|
+
// ============================================================
|
|
276
|
+
|
|
277
|
+
#[wasm_bindgen]
|
|
278
|
+
pub fn bloom_set_target_fps(fps: f64) {
|
|
279
|
+
engine().target_fps = fps;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
#[wasm_bindgen]
|
|
283
|
+
pub fn bloom_set_direct_2d_mode(on: f64) {
|
|
284
|
+
engine().direct_2d_mode = on > 0.5;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#[wasm_bindgen]
|
|
288
|
+
pub fn bloom_get_delta_time() -> f64 {
|
|
289
|
+
engine().delta_time
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#[wasm_bindgen]
|
|
293
|
+
pub fn bloom_get_fps() -> f64 {
|
|
294
|
+
engine().get_fps()
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
#[wasm_bindgen]
|
|
298
|
+
pub fn bloom_get_screen_width() -> f64 {
|
|
299
|
+
engine().screen_width()
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#[wasm_bindgen]
|
|
303
|
+
pub fn bloom_get_screen_height() -> f64 {
|
|
304
|
+
engine().screen_height()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#[wasm_bindgen]
|
|
308
|
+
pub fn bloom_get_time() -> f64 {
|
|
309
|
+
engine().get_time()
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Input FFI (keyboard/mouse/gamepad/touch + JS-event injection) lives
|
|
313
|
+
// in input_ffi.rs (2000-line file policy).
|
|
314
|
+
mod input_ffi;
|
|
315
|
+
mod material_ffi;
|
|
316
|
+
pub use input_ffi::*;
|
|
317
|
+
|
|
318
|
+
// EN-063 — web FFI parity for full-3D games: mesh/instance/texture-array
|
|
319
|
+
// scratch builders, staged-model bytes, env-HDR bytes, profiler text,
|
|
320
|
+
// splat impulses, host stubs (parity_ffi) and ragdolls via the Jolt JS
|
|
321
|
+
// bridge (ragdoll_ffi).
|
|
322
|
+
#[cfg(feature = "models3d")]
|
|
323
|
+
mod parity_ffi;
|
|
324
|
+
#[cfg(feature = "models3d")]
|
|
325
|
+
pub use parity_ffi::*;
|
|
326
|
+
#[cfg(all(feature = "jolt", feature = "models3d"))]
|
|
327
|
+
mod ragdoll_ffi;
|
|
328
|
+
#[cfg(all(feature = "jolt", feature = "models3d"))]
|
|
329
|
+
pub use ragdoll_ffi::*;
|
|
330
|
+
|
|
331
|
+
// ============================================================
|
|
332
|
+
// 2D Drawing - Shapes
|
|
333
|
+
// ============================================================
|
|
334
|
+
|
|
335
|
+
#[wasm_bindgen]
|
|
336
|
+
pub fn bloom_draw_line(x1: f64, y1: f64, x2: f64, y2: f64, thickness: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
337
|
+
engine().renderer.draw_line(x1, y1, x2, y2, thickness, r, g, b, a);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#[wasm_bindgen]
|
|
341
|
+
pub fn bloom_draw_rect(x: f64, y: f64, w: f64, h: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
342
|
+
engine().renderer.draw_rect(x, y, w, h, r, g, b, a);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
#[wasm_bindgen]
|
|
346
|
+
pub fn bloom_draw_rect_lines(x: f64, y: f64, w: f64, h: f64, thickness: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
347
|
+
engine().renderer.draw_rect_lines(x, y, w, h, thickness, r, g, b, a);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
#[wasm_bindgen]
|
|
351
|
+
pub fn bloom_draw_circle(cx: f64, cy: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
352
|
+
engine().renderer.draw_circle(cx, cy, radius, r, g, b, a);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
#[wasm_bindgen]
|
|
356
|
+
pub fn bloom_draw_circle_lines(cx: f64, cy: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
357
|
+
engine().renderer.draw_circle_lines(cx, cy, radius, r, g, b, a);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
#[wasm_bindgen]
|
|
361
|
+
pub fn bloom_draw_triangle(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
362
|
+
engine().renderer.draw_triangle(x1, y1, x2, y2, x3, y3, r, g, b, a);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
#[wasm_bindgen]
|
|
366
|
+
pub fn bloom_draw_poly(cx: f64, cy: f64, sides: f64, radius: f64, rotation: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
367
|
+
engine().renderer.draw_poly(cx, cy, sides, radius, rotation, r, g, b, a);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ============================================================
|
|
371
|
+
// Camera 2D
|
|
372
|
+
// ============================================================
|
|
373
|
+
|
|
374
|
+
#[wasm_bindgen]
|
|
375
|
+
pub fn bloom_begin_mode_2d(offset_x: f64, offset_y: f64, target_x: f64, target_y: f64, rotation: f64, zoom: f64) {
|
|
376
|
+
engine().renderer.begin_mode_2d(
|
|
377
|
+
offset_x as f32, offset_y as f32,
|
|
378
|
+
target_x as f32, target_y as f32,
|
|
379
|
+
rotation as f32, zoom as f32,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
#[wasm_bindgen]
|
|
384
|
+
pub fn bloom_end_mode_2d() {
|
|
385
|
+
engine().renderer.end_mode_2d();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ============================================================
|
|
389
|
+
// Camera 3D and 3D Drawing
|
|
390
|
+
// ============================================================
|
|
391
|
+
|
|
392
|
+
#[wasm_bindgen]
|
|
393
|
+
pub fn bloom_begin_mode_3d(
|
|
394
|
+
pos_x: f64, pos_y: f64, pos_z: f64,
|
|
395
|
+
target_x: f64, target_y: f64, target_z: f64,
|
|
396
|
+
up_x: f64, up_y: f64, up_z: f64,
|
|
397
|
+
fovy: f64, projection: f64,
|
|
398
|
+
) {
|
|
399
|
+
engine().renderer.begin_mode_3d(
|
|
400
|
+
pos_x as f32, pos_y as f32, pos_z as f32,
|
|
401
|
+
target_x as f32, target_y as f32, target_z as f32,
|
|
402
|
+
up_x as f32, up_y as f32, up_z as f32,
|
|
403
|
+
fovy as f32, projection as f32,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
#[wasm_bindgen]
|
|
408
|
+
pub fn bloom_end_mode_3d() {
|
|
409
|
+
engine().renderer.end_mode_3d();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
#[wasm_bindgen]
|
|
413
|
+
pub fn bloom_draw_cube(x: f64, y: f64, z: f64, w: f64, h: f64, d: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
414
|
+
engine().renderer.draw_cube(x, y, z, w, h, d, r, g, b, a);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
#[wasm_bindgen]
|
|
418
|
+
pub fn bloom_draw_cube_wires(x: f64, y: f64, z: f64, w: f64, h: f64, d: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
419
|
+
engine().renderer.draw_cube_wires(x, y, z, w, h, d, r, g, b, a);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
#[wasm_bindgen]
|
|
423
|
+
pub fn bloom_draw_sphere(cx: f64, cy: f64, cz: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
424
|
+
engine().renderer.draw_sphere(cx, cy, cz, radius, r, g, b, a);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
#[wasm_bindgen]
|
|
428
|
+
pub fn bloom_draw_sphere_wires(cx: f64, cy: f64, cz: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
429
|
+
engine().renderer.draw_sphere_wires(cx, cy, cz, radius, r, g, b, a);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
#[wasm_bindgen]
|
|
433
|
+
pub fn bloom_draw_cylinder(x: f64, y: f64, z: f64, radius_top: f64, radius_bottom: f64, height: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
434
|
+
engine().renderer.draw_cylinder(x, y, z, radius_top, radius_bottom, height, r, g, b, a);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
#[wasm_bindgen]
|
|
438
|
+
pub fn bloom_draw_plane(cx: f64, cy: f64, cz: f64, w: f64, d: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
439
|
+
engine().renderer.draw_plane(cx, cy, cz, w, d, r, g, b, a);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
#[wasm_bindgen]
|
|
443
|
+
pub fn bloom_draw_grid(slices: f64, spacing: f64) {
|
|
444
|
+
engine().renderer.draw_grid(slices as i32, spacing);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
#[wasm_bindgen]
|
|
448
|
+
pub fn bloom_draw_ray(origin_x: f64, origin_y: f64, origin_z: f64, dir_x: f64, dir_y: f64, dir_z: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
449
|
+
engine().renderer.draw_ray(origin_x, origin_y, origin_z, dir_x, dir_y, dir_z, r, g, b, a);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ============================================================
|
|
453
|
+
// Text
|
|
454
|
+
// ============================================================
|
|
455
|
+
|
|
456
|
+
// The original FFI functions accept f64 (NaN-boxed string handles from Perry WASM).
|
|
457
|
+
// The JS glue intercepts these and calls the _str variants below with actual strings.
|
|
458
|
+
#[wasm_bindgen]
|
|
459
|
+
pub fn bloom_draw_text(_text: f64, _x: f64, _y: f64, _size: f64, _r: f64, _g: f64, _b: f64, _a: f64) {
|
|
460
|
+
// No-op: JS glue calls bloom_draw_text_str instead
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
#[wasm_bindgen]
|
|
464
|
+
pub fn bloom_draw_text_str(text: &str, x: f64, y: f64, size: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
465
|
+
let eng = engine();
|
|
466
|
+
let mut text_renderer = std::mem::replace(&mut eng.text, bloom_shared::text_renderer::TextRenderer::empty());
|
|
467
|
+
text_renderer.draw_text(&mut eng.renderer, text, x, y, size as u32, r, g, b, a);
|
|
468
|
+
eng.text = text_renderer;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
#[wasm_bindgen]
|
|
472
|
+
pub fn bloom_measure_text(_text: f64, _size: f64) -> f64 { 0.0 }
|
|
473
|
+
|
|
474
|
+
#[wasm_bindgen]
|
|
475
|
+
pub fn bloom_measure_text_str(text: &str, size: f64) -> f64 {
|
|
476
|
+
engine().text.measure_text(text, size as u32)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
#[wasm_bindgen]
|
|
480
|
+
pub fn bloom_load_font(_path: f64, _size: f64) -> f64 { 0.0 }
|
|
481
|
+
|
|
482
|
+
/// Load a font from raw bytes (fetched by JS glue via fetch()).
|
|
483
|
+
#[wasm_bindgen]
|
|
484
|
+
pub fn bloom_load_font_bytes(data: &[u8]) -> f64 {
|
|
485
|
+
engine().text.load_font(data) as f64
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
#[wasm_bindgen]
|
|
489
|
+
pub fn bloom_unload_font(font_handle: f64) {
|
|
490
|
+
engine().text.unload_font(font_handle as usize);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
#[wasm_bindgen]
|
|
494
|
+
pub fn bloom_draw_text_ex(_font_handle: f64, _text: f64, _x: f64, _y: f64, _size: f64, _spacing: f64, _r: f64, _g: f64, _b: f64, _a: f64) {
|
|
495
|
+
// No-op: JS glue calls bloom_draw_text_ex_str instead
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
#[wasm_bindgen]
|
|
499
|
+
pub fn bloom_draw_text_ex_str(font_handle: f64, text: &str, x: f64, y: f64, size: f64, spacing: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
500
|
+
let eng = engine();
|
|
501
|
+
let mut text_renderer = std::mem::replace(&mut eng.text, bloom_shared::text_renderer::TextRenderer::empty());
|
|
502
|
+
text_renderer.draw_text_ex(&mut eng.renderer, font_handle as usize, text, x, y, size as u32, spacing as f32, r, g, b, a);
|
|
503
|
+
eng.text = text_renderer;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
#[wasm_bindgen]
|
|
507
|
+
pub fn bloom_measure_text_ex(_font_handle: f64, _text: f64, _size: f64, _spacing: f64) -> f64 { 0.0 }
|
|
508
|
+
|
|
509
|
+
#[wasm_bindgen]
|
|
510
|
+
pub fn bloom_measure_text_ex_str(font_handle: f64, text: &str, size: f64, spacing: f64) -> f64 {
|
|
511
|
+
engine().text.measure_text_ex(font_handle as usize, text, size as u32, spacing as f32)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// --- Texture loading from bytes (fetched by JS glue) ---
|
|
515
|
+
|
|
516
|
+
/// Load a texture from raw image bytes (PNG/JPEG/etc.), fetched by JS glue via fetch().
|
|
517
|
+
#[wasm_bindgen]
|
|
518
|
+
pub fn bloom_load_texture_bytes(data: &[u8]) -> f64 {
|
|
519
|
+
let eng = engine();
|
|
520
|
+
let renderer_ptr = &mut eng.renderer as *mut bloom_shared::renderer::Renderer;
|
|
521
|
+
eng.textures.load_texture(unsafe { &mut *renderer_ptr }, data)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ============================================================
|
|
525
|
+
// Textures
|
|
526
|
+
// ============================================================
|
|
527
|
+
|
|
528
|
+
#[wasm_bindgen]
|
|
529
|
+
pub fn bloom_load_texture(_path: f64) -> f64 {
|
|
530
|
+
// TODO: Phase 3 — need fetch() for texture file + string conversion
|
|
531
|
+
0.0
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
#[wasm_bindgen]
|
|
535
|
+
pub fn bloom_unload_texture(handle: f64) {
|
|
536
|
+
let eng = engine();
|
|
537
|
+
let renderer_ptr = &mut eng.renderer as *mut Renderer;
|
|
538
|
+
eng.textures.unload_texture(handle, unsafe { &mut *renderer_ptr });
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
#[wasm_bindgen]
|
|
542
|
+
pub fn bloom_draw_texture(handle: f64, x: f64, y: f64, tint_r: f64, tint_g: f64, tint_b: f64, tint_a: f64) {
|
|
543
|
+
let eng = engine();
|
|
544
|
+
if let Some(tex) = eng.textures.get(handle) {
|
|
545
|
+
let bind_group_idx = tex.bind_group_idx;
|
|
546
|
+
eng.renderer.draw_texture(bind_group_idx, x, y, tint_r, tint_g, tint_b, tint_a);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
#[wasm_bindgen]
|
|
551
|
+
pub fn bloom_draw_texture_pro(
|
|
552
|
+
handle: f64,
|
|
553
|
+
src_x: f64, src_y: f64, src_w: f64, src_h: f64,
|
|
554
|
+
dst_x: f64, dst_y: f64, dst_w: f64, dst_h: f64,
|
|
555
|
+
origin_x: f64, origin_y: f64, rotation: f64,
|
|
556
|
+
tint_r: f64, tint_g: f64, tint_b: f64, tint_a: f64,
|
|
557
|
+
) {
|
|
558
|
+
let eng = engine();
|
|
559
|
+
if let Some(tex) = eng.textures.get(handle) {
|
|
560
|
+
let bind_group_idx = tex.bind_group_idx;
|
|
561
|
+
eng.renderer.draw_texture_pro(
|
|
562
|
+
bind_group_idx,
|
|
563
|
+
src_x, src_y, src_w, src_h,
|
|
564
|
+
dst_x, dst_y, dst_w, dst_h,
|
|
565
|
+
origin_x, origin_y, rotation,
|
|
566
|
+
tint_r, tint_g, tint_b, tint_a,
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
#[wasm_bindgen]
|
|
572
|
+
pub fn bloom_draw_texture_rec(
|
|
573
|
+
handle: f64,
|
|
574
|
+
src_x: f64, src_y: f64, src_w: f64, src_h: f64,
|
|
575
|
+
dst_x: f64, dst_y: f64,
|
|
576
|
+
tint_r: f64, tint_g: f64, tint_b: f64, tint_a: f64,
|
|
577
|
+
) {
|
|
578
|
+
let eng = engine();
|
|
579
|
+
if let Some(tex) = eng.textures.get(handle) {
|
|
580
|
+
let bind_group_idx = tex.bind_group_idx;
|
|
581
|
+
eng.renderer.draw_texture_rec(
|
|
582
|
+
bind_group_idx,
|
|
583
|
+
src_x, src_y, src_w, src_h,
|
|
584
|
+
dst_x, dst_y,
|
|
585
|
+
tint_r, tint_g, tint_b, tint_a,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
#[wasm_bindgen]
|
|
591
|
+
pub fn bloom_get_texture_width(handle: f64) -> f64 {
|
|
592
|
+
engine().textures.get(handle).map(|t| t.width as f64).unwrap_or(0.0)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
#[wasm_bindgen]
|
|
596
|
+
pub fn bloom_get_texture_height(handle: f64) -> f64 {
|
|
597
|
+
engine().textures.get(handle).map(|t| t.height as f64).unwrap_or(0.0)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
#[wasm_bindgen]
|
|
601
|
+
pub fn bloom_load_image(_path: f64) -> f64 { 0.0 }
|
|
602
|
+
|
|
603
|
+
#[wasm_bindgen]
|
|
604
|
+
pub fn bloom_load_image_bytes(data: &[u8]) -> f64 {
|
|
605
|
+
engine().textures.load_image(data)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
#[wasm_bindgen]
|
|
609
|
+
pub fn bloom_image_resize(handle: f64, w: f64, h: f64) {
|
|
610
|
+
engine().textures.image_resize(handle, w as u32, h as u32);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
#[wasm_bindgen]
|
|
614
|
+
pub fn bloom_image_crop(handle: f64, x: f64, y: f64, w: f64, h: f64) {
|
|
615
|
+
engine().textures.image_crop(handle, x as u32, y as u32, w as u32, h as u32);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
#[wasm_bindgen]
|
|
619
|
+
pub fn bloom_image_flip_h(handle: f64) {
|
|
620
|
+
engine().textures.image_flip_h(handle);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
#[wasm_bindgen]
|
|
624
|
+
pub fn bloom_image_flip_v(handle: f64) {
|
|
625
|
+
engine().textures.image_flip_v(handle);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
#[wasm_bindgen]
|
|
629
|
+
pub fn bloom_load_texture_from_image(handle: f64) -> f64 {
|
|
630
|
+
let eng = engine();
|
|
631
|
+
let renderer_ptr = &mut eng.renderer as *mut Renderer;
|
|
632
|
+
eng.textures.load_texture_from_image(handle, unsafe { &mut *renderer_ptr })
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
#[wasm_bindgen]
|
|
636
|
+
pub fn bloom_gen_texture_mipmaps(_handle: f64) {
|
|
637
|
+
// Mipmap generation is handled by the GPU texture creation pipeline
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
#[wasm_bindgen]
|
|
641
|
+
pub fn bloom_set_texture_filter(handle: f64, mode: f64) {
|
|
642
|
+
let eng = engine();
|
|
643
|
+
if let Some(tex) = eng.textures.get(handle) {
|
|
644
|
+
let bind_group_idx = tex.bind_group_idx;
|
|
645
|
+
eng.renderer.set_texture_filter(bind_group_idx, mode > 0.5);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ============================================================
|
|
650
|
+
// Models
|
|
651
|
+
// ============================================================
|
|
652
|
+
|
|
653
|
+
#[wasm_bindgen]
|
|
654
|
+
pub fn bloom_load_model(_path: f64) -> f64 { 0.0 }
|
|
655
|
+
|
|
656
|
+
#[wasm_bindgen]
|
|
657
|
+
pub fn bloom_load_model_bytes(data: &[u8]) -> f64 {
|
|
658
|
+
let eng = engine();
|
|
659
|
+
eng.models.load_model_with_textures(data, &mut eng.renderer)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
#[wasm_bindgen]
|
|
663
|
+
pub fn bloom_draw_model(handle: f64, x: f64, y: f64, z: f64, scale: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
664
|
+
let eng = engine();
|
|
665
|
+
if let Some(model) = eng.models.get(handle) {
|
|
666
|
+
let position = [x as f32, y as f32, z as f32];
|
|
667
|
+
let scale = scale as f32;
|
|
668
|
+
let tint = [(r / 255.0) as f32, (g / 255.0) as f32, (b / 255.0) as f32, (a / 255.0) as f32];
|
|
669
|
+
let handle_bits = handle.to_bits();
|
|
670
|
+
if eng.renderer.cache_model_if_static(handle_bits, &model.meshes) {
|
|
671
|
+
// Skinned models cache too (bind-pose VB with raw joint indices,
|
|
672
|
+
// skinned in the scene VS) — they MUST take the skinned cached
|
|
673
|
+
// draw, which pops the staged pose ONCE for the whole model and
|
|
674
|
+
// shares the joint offset across every primitive. Routing them
|
|
675
|
+
// through the unskinned draw (as this did) leaves each primitive
|
|
676
|
+
// reading joint offset 0 — another model's matrices — so every
|
|
677
|
+
// character animated wrongly on web while native was correct.
|
|
678
|
+
// Mirrors the shared macro in ffi_core/models.rs.
|
|
679
|
+
if eng.renderer.is_model_skinned(handle_bits) {
|
|
680
|
+
eng.renderer.draw_model_cached_skinned(handle_bits, position, scale, tint);
|
|
681
|
+
} else {
|
|
682
|
+
eng.renderer.draw_model_cached(handle_bits, position, scale, tint);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/// EN-039 — immediate draws that can pitch and roll, not just yaw.
|
|
689
|
+
///
|
|
690
|
+
/// The web crate was the only platform without it, which failed ffi-parity:
|
|
691
|
+
/// every manifest function must be reachable from every platform, and this one
|
|
692
|
+
/// is NOT a legitimate gap. The documented WEB_GAP_ALLOWLIST entries are all
|
|
693
|
+
/// pointer-taking calls blocked on the Perry-WASM linear-memory bridge — this
|
|
694
|
+
/// takes the matrix as 16 loose f64 scalars precisely so it can cross any FFI,
|
|
695
|
+
/// so allowlisting it would have been recording a TODO for work that is four
|
|
696
|
+
/// lines of typing.
|
|
697
|
+
///
|
|
698
|
+
/// Mirrors the shared macro (ffi_core/models.rs) exactly, including the skinned
|
|
699
|
+
/// early-out: a skinned model's pose is driven by the anim palette, so drawing
|
|
700
|
+
/// it through the static cache would fight the skinning.
|
|
701
|
+
#[wasm_bindgen]
|
|
702
|
+
#[allow(clippy::too_many_arguments)]
|
|
703
|
+
pub fn bloom_draw_model_transform16(
|
|
704
|
+
handle: f64,
|
|
705
|
+
m0: f64, m1: f64, m2: f64, m3: f64,
|
|
706
|
+
m4: f64, m5: f64, m6: f64, m7: f64,
|
|
707
|
+
m8: f64, m9: f64, m10: f64, m11: f64,
|
|
708
|
+
m12: f64, m13: f64, m14: f64, m15: f64,
|
|
709
|
+
color_packed_argb: f64,
|
|
710
|
+
) {
|
|
711
|
+
let bits = color_packed_argb as u32;
|
|
712
|
+
let a = ((bits >> 24) & 0xff) as f32 / 255.0;
|
|
713
|
+
let r = ((bits >> 16) & 0xff) as f32 / 255.0;
|
|
714
|
+
let g = ((bits >> 8) & 0xff) as f32 / 255.0;
|
|
715
|
+
let b = ( bits & 0xff) as f32 / 255.0;
|
|
716
|
+
let s = [
|
|
717
|
+
m0, m1, m2, m3,
|
|
718
|
+
m4, m5, m6, m7,
|
|
719
|
+
m8, m9, m10, m11,
|
|
720
|
+
m12, m13, m14, m15,
|
|
721
|
+
];
|
|
722
|
+
// Column-major, same unpacking as the shared macro.
|
|
723
|
+
let mut mat = [[0.0f32; 4]; 4];
|
|
724
|
+
for col in 0..4 {
|
|
725
|
+
for row in 0..4 {
|
|
726
|
+
mat[col][row] = s[col * 4 + row] as f32;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
let eng = engine();
|
|
730
|
+
if let Some(model) = eng.models.get(handle) {
|
|
731
|
+
let handle_bits = handle.to_bits();
|
|
732
|
+
if eng.renderer.cache_model_if_static(handle_bits, &model.meshes) {
|
|
733
|
+
if eng.renderer.is_model_skinned(handle_bits) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
eng.renderer.draw_model_cached_transform(handle_bits, mat, [r, g, b, a]);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
#[wasm_bindgen]
|
|
742
|
+
pub fn bloom_draw_model_rotated(
|
|
743
|
+
handle: f64, x: f64, y: f64, z: f64,
|
|
744
|
+
scale: f64, rot_y: f64,
|
|
745
|
+
color_packed_argb: f64,
|
|
746
|
+
) {
|
|
747
|
+
let bits = color_packed_argb as u32;
|
|
748
|
+
let a = ((bits >> 24) & 0xff) as f32 / 255.0;
|
|
749
|
+
let r = ((bits >> 16) & 0xff) as f32 / 255.0;
|
|
750
|
+
let g = ((bits >> 8) & 0xff) as f32 / 255.0;
|
|
751
|
+
let b = ( bits & 0xff) as f32 / 255.0;
|
|
752
|
+
let eng = engine();
|
|
753
|
+
if let Some(model) = eng.models.get(handle) {
|
|
754
|
+
let position = [x as f32, y as f32, z as f32];
|
|
755
|
+
let scale = scale as f32;
|
|
756
|
+
let tint = [r, g, b, a];
|
|
757
|
+
let handle_bits = handle.to_bits();
|
|
758
|
+
// Static models go through the cached scene pipeline (alpha cutout,
|
|
759
|
+
// normal/MR maps, foliage wind + transmission, cutout shadows,
|
|
760
|
+
// planar reflections) — the old immediate path had none of that and
|
|
761
|
+
// rendered cutout foliage as opaque cards. Skinned models take the
|
|
762
|
+
// skinned cached draw and IGNORE the rotation — their joint matrices
|
|
763
|
+
// bake orientation, exactly as the old immediate fallback behaved.
|
|
764
|
+
//
|
|
765
|
+
// This mirrors the shared macro (ffi_core/models.rs). It was still on
|
|
766
|
+
// the immediate path here: round 5 fixed native and the web copy was
|
|
767
|
+
// never updated, so every tree in a browser rendered as torn opaque
|
|
768
|
+
// sheets — the very artifact that fix was written for.
|
|
769
|
+
if eng.renderer.cache_model_if_static(handle_bits, &model.meshes) {
|
|
770
|
+
if eng.renderer.is_model_skinned(handle_bits) {
|
|
771
|
+
eng.renderer.draw_model_cached_skinned(handle_bits, position, scale, tint);
|
|
772
|
+
} else {
|
|
773
|
+
eng.renderer.draw_model_cached_rotated(
|
|
774
|
+
handle_bits, position, scale, rot_y as f32, tint,
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
#[wasm_bindgen]
|
|
782
|
+
pub fn bloom_unload_model(handle: f64) {
|
|
783
|
+
let eng = engine();
|
|
784
|
+
// Evict cached GPU meshes (keyed by handle bits) before the handle dies
|
|
785
|
+
// — without this the buffers leak and a slot-reusing future model would
|
|
786
|
+
// render the stale geometry. Mirrors the shared macro.
|
|
787
|
+
eng.renderer.evict_model_cache(handle.to_bits());
|
|
788
|
+
eng.models.unload_model(handle);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
#[wasm_bindgen]
|
|
792
|
+
pub fn bloom_gen_mesh_cube(w: f64, h: f64, d: f64) -> f64 {
|
|
793
|
+
engine().models.gen_mesh_cube(w as f32, h as f32, d as f32)
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
#[wasm_bindgen]
|
|
797
|
+
pub fn bloom_gen_mesh_heightmap(image_handle: f64, size_x: f64, size_y: f64, size_z: f64) -> f64 {
|
|
798
|
+
let eng = engine();
|
|
799
|
+
if let Some(img) = eng.textures.images.get(image_handle) {
|
|
800
|
+
let data = img.data.clone();
|
|
801
|
+
let w = img.width;
|
|
802
|
+
let h = img.height;
|
|
803
|
+
eng.models.gen_mesh_heightmap(&data, w, h, size_x as f32, size_y as f32, size_z as f32)
|
|
804
|
+
} else {
|
|
805
|
+
0.0
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
#[wasm_bindgen]
|
|
810
|
+
pub fn bloom_load_shader(_source: f64) -> f64 {
|
|
811
|
+
// TODO: Phase 4 — need string conversion from NaN-boxed f64 handle
|
|
812
|
+
0.0
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
#[wasm_bindgen]
|
|
816
|
+
pub fn bloom_create_mesh(_vertex_ptr: f64, _vertex_count: f64, _index_ptr: f64, _index_count: f64) -> f64 {
|
|
817
|
+
// TODO: Phase 4 — need to handle pointer passing from WASM linear memory
|
|
818
|
+
0.0
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// ============================================================
|
|
822
|
+
// Lighting
|
|
823
|
+
// ============================================================
|
|
824
|
+
|
|
825
|
+
#[wasm_bindgen]
|
|
826
|
+
pub fn bloom_set_ambient_light(r: f64, g: f64, b: f64, intensity: f64) {
|
|
827
|
+
engine().renderer.set_ambient_light(r, g, b, intensity);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
#[wasm_bindgen]
|
|
831
|
+
pub fn bloom_set_directional_light(dx: f64, dy: f64, dz: f64, r: f64, g: f64, b: f64, intensity: f64) {
|
|
832
|
+
engine().renderer.set_directional_light(dx, dy, dz, r, g, b, intensity);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
#[wasm_bindgen]
|
|
836
|
+
pub fn bloom_set_procedural_sky(enabled: f64, rayleigh_density: f64, mie_density: f64, ground_albedo: f64) {
|
|
837
|
+
engine().renderer.set_procedural_sky(
|
|
838
|
+
enabled != 0.0,
|
|
839
|
+
rayleigh_density as f32,
|
|
840
|
+
mie_density as f32,
|
|
841
|
+
ground_albedo as f32,
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
#[wasm_bindgen]
|
|
846
|
+
pub fn bloom_set_sun_direction(dx: f64, dy: f64, dz: f64, intensity: f64) {
|
|
847
|
+
engine().renderer.set_sun_direction(dx as f32, dy as f32, dz as f32, intensity as f32);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
#[wasm_bindgen]
|
|
851
|
+
pub fn bloom_set_joint_test(joint_index: f64, angle: f64) {
|
|
852
|
+
engine().renderer.set_joint_test(joint_index as usize, angle as f32);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
#[wasm_bindgen]
|
|
856
|
+
pub fn bloom_add_directional_light(
|
|
857
|
+
dx: f64, dy: f64, dz: f64,
|
|
858
|
+
r: f64, g: f64, b: f64,
|
|
859
|
+
intensity: f64,
|
|
860
|
+
) {
|
|
861
|
+
engine().renderer.add_directional_light(
|
|
862
|
+
dx as f32, dy as f32, dz as f32,
|
|
863
|
+
r as f32, g as f32, b as f32,
|
|
864
|
+
intensity as f32,
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
#[wasm_bindgen]
|
|
869
|
+
pub fn bloom_add_point_light(
|
|
870
|
+
x: f64, y: f64, z: f64, range: f64,
|
|
871
|
+
r: f64, g: f64, b: f64,
|
|
872
|
+
intensity: f64,
|
|
873
|
+
) {
|
|
874
|
+
engine().renderer.add_point_light(
|
|
875
|
+
x as f32, y as f32, z as f32, range as f32,
|
|
876
|
+
r as f32, g as f32, b as f32,
|
|
877
|
+
intensity as f32,
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// ============================================================
|
|
882
|
+
// Audio
|
|
883
|
+
// ============================================================
|
|
884
|
+
|
|
885
|
+
#[wasm_bindgen]
|
|
886
|
+
pub fn bloom_init_audio() {
|
|
887
|
+
// Audio initialization is handled by JS glue (Web Audio API AudioContext).
|
|
888
|
+
// The Rust AudioMixer is already initialized as part of EngineState.
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
#[wasm_bindgen]
|
|
892
|
+
pub fn bloom_close_audio() {
|
|
893
|
+
// AudioContext cleanup handled by JS glue.
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
#[wasm_bindgen]
|
|
897
|
+
pub fn bloom_load_sound(_path: f64) -> f64 { 0.0 }
|
|
898
|
+
|
|
899
|
+
/// Load a sound from raw file bytes (WAV or OGG). Fetched by JS glue via fetch().
|
|
900
|
+
#[wasm_bindgen]
|
|
901
|
+
pub fn bloom_load_sound_bytes(data: &[u8]) -> f64 {
|
|
902
|
+
if let Some(sound) = bloom_shared::audio::parse_wav(data)
|
|
903
|
+
.or_else(|| bloom_shared::audio::parse_ogg(data))
|
|
904
|
+
{
|
|
905
|
+
engine().audio.load_sound(sound)
|
|
906
|
+
} else {
|
|
907
|
+
0.0
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
#[wasm_bindgen]
|
|
912
|
+
pub fn bloom_play_sound(handle: f64) {
|
|
913
|
+
engine().audio.play_sound(handle);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
#[wasm_bindgen]
|
|
917
|
+
pub fn bloom_stop_sound(handle: f64) {
|
|
918
|
+
engine().audio.stop_sound(handle);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
#[wasm_bindgen]
|
|
922
|
+
pub fn bloom_set_sound_volume(handle: f64, volume: f64) {
|
|
923
|
+
engine().audio.set_sound_volume(handle, volume as f32);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
#[wasm_bindgen]
|
|
927
|
+
pub fn bloom_set_master_volume(volume: f64) {
|
|
928
|
+
engine().audio.set_master_volume(volume as f32);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
#[wasm_bindgen]
|
|
932
|
+
pub fn bloom_play_sound_3d(handle: f64, x: f64, y: f64, z: f64) {
|
|
933
|
+
engine().audio.play_sound_3d(handle, x as f32, y as f32, z as f32);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
#[wasm_bindgen]
|
|
937
|
+
pub fn bloom_set_listener_position(x: f64, y: f64, z: f64, fx: f64, fy: f64, fz: f64) {
|
|
938
|
+
engine().audio.set_listener_position(x as f32, y as f32, z as f32, fx as f32, fy as f32, fz as f32);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// ---- EN-062: live spatial voices (see shared audio_ffi.rs for docs) --------
|
|
942
|
+
|
|
943
|
+
#[wasm_bindgen]
|
|
944
|
+
pub fn bloom_play_sound_3d_ex(
|
|
945
|
+
handle: f64, x: f64, y: f64, z: f64,
|
|
946
|
+
looping: f64, ref_dist: f64, max_dist: f64, rolloff: f64,
|
|
947
|
+
) -> f64 {
|
|
948
|
+
engine().audio.play_sound_3d_ex(
|
|
949
|
+
handle, x as f32, y as f32, z as f32,
|
|
950
|
+
looping != 0.0, ref_dist as f32, max_dist as f32, rolloff as f32)
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
#[wasm_bindgen]
|
|
954
|
+
pub fn bloom_voice_set_position(voice: f64, x: f64, y: f64, z: f64) {
|
|
955
|
+
engine().audio.set_voice_position(voice, x as f32, y as f32, z as f32);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
#[wasm_bindgen]
|
|
959
|
+
pub fn bloom_voice_stop(voice: f64) {
|
|
960
|
+
engine().audio.stop_voice(voice);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
#[wasm_bindgen]
|
|
964
|
+
pub fn bloom_voice_set_volume(voice: f64, volume: f64) {
|
|
965
|
+
engine().audio.set_voice_volume(voice, volume as f32);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
#[wasm_bindgen]
|
|
969
|
+
pub fn bloom_voice_set_pitch(voice: f64, pitch: f64) {
|
|
970
|
+
engine().audio.set_voice_pitch(voice, pitch as f32);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
#[wasm_bindgen]
|
|
974
|
+
pub fn bloom_voice_set_lowpass(voice: f64, cutoff: f64) {
|
|
975
|
+
engine().audio.set_voice_lowpass(voice, cutoff as f32);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
#[wasm_bindgen]
|
|
979
|
+
pub fn bloom_load_music(_path: f64) -> f64 { 0.0 }
|
|
980
|
+
|
|
981
|
+
/// Load music from raw file bytes (WAV or OGG). Fetched by JS glue via fetch().
|
|
982
|
+
#[wasm_bindgen]
|
|
983
|
+
pub fn bloom_load_music_bytes(data: &[u8]) -> f64 {
|
|
984
|
+
if let Some(sound) = bloom_shared::audio::parse_wav(data)
|
|
985
|
+
.or_else(|| bloom_shared::audio::parse_ogg(data))
|
|
986
|
+
{
|
|
987
|
+
engine().audio.load_music(sound)
|
|
988
|
+
} else {
|
|
989
|
+
0.0
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/// Mix audio into an interleaved stereo f32 buffer.
|
|
994
|
+
/// Called by JS Web Audio ScriptProcessorNode/AudioWorklet each audio frame.
|
|
995
|
+
#[wasm_bindgen]
|
|
996
|
+
pub fn bloom_audio_mix(output: &mut [f32]) {
|
|
997
|
+
engine().audio.mix_output(output);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
#[wasm_bindgen]
|
|
1001
|
+
pub fn bloom_play_music(handle: f64) {
|
|
1002
|
+
engine().audio.play_music(handle);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
#[wasm_bindgen]
|
|
1006
|
+
pub fn bloom_stop_music(handle: f64) {
|
|
1007
|
+
engine().audio.stop_music(handle);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
#[wasm_bindgen]
|
|
1011
|
+
pub fn bloom_update_music_stream(handle: f64) {
|
|
1012
|
+
engine().audio.update_music_stream(handle);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
#[wasm_bindgen]
|
|
1016
|
+
pub fn bloom_set_music_volume(handle: f64, volume: f64) {
|
|
1017
|
+
engine().audio.set_music_volume(handle, volume as f32);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
#[wasm_bindgen]
|
|
1021
|
+
pub fn bloom_is_music_playing(handle: f64) -> f64 {
|
|
1022
|
+
if engine().audio.is_music_playing(handle) { 1.0 } else { 0.0 }
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// ============================================================
|
|
1026
|
+
// Scene graph (retained mode)
|
|
1027
|
+
// ============================================================
|
|
1028
|
+
|
|
1029
|
+
#[wasm_bindgen]
|
|
1030
|
+
pub fn bloom_scene_create_node() -> f64 {
|
|
1031
|
+
engine().scene.create_node()
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
#[wasm_bindgen]
|
|
1035
|
+
pub fn bloom_scene_destroy_node(handle: f64) {
|
|
1036
|
+
engine().scene.destroy_node(handle);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
#[wasm_bindgen]
|
|
1040
|
+
pub fn bloom_scene_set_visible(handle: f64, visible: f64) {
|
|
1041
|
+
engine().scene.set_visible(handle, visible != 0.0);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
#[wasm_bindgen]
|
|
1045
|
+
pub fn bloom_scene_set_cast_shadow(handle: f64, cast: f64) {
|
|
1046
|
+
engine().scene.set_cast_shadow(handle, cast != 0.0);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
#[wasm_bindgen]
|
|
1050
|
+
pub fn bloom_scene_set_receive_shadow(handle: f64, receive: f64) {
|
|
1051
|
+
engine().scene.set_receive_shadow(handle, receive != 0.0);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
#[wasm_bindgen]
|
|
1055
|
+
pub fn bloom_scene_set_parent(handle: f64, parent: f64) {
|
|
1056
|
+
engine().scene.set_parent(handle, parent);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
#[wasm_bindgen]
|
|
1060
|
+
pub fn bloom_scene_set_transform(_handle: f64, _matrix_ptr: f64) {
|
|
1061
|
+
// Pointer-taking form — unreachable from Perry on wasm (a `number[]`
|
|
1062
|
+
// cannot cross an i64 param), like the other pointer stubs here. The
|
|
1063
|
+
// 16-scalar `_transform16` below is the live path; it used to carry
|
|
1064
|
+
// THIS name, which meant setSceneNodeTransform resolved to nothing and
|
|
1065
|
+
// was auto-stubbed by Perry's FFI proxy — a silent no-op on web while
|
|
1066
|
+
// the working implementation sat one rename away.
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
#[wasm_bindgen]
|
|
1070
|
+
#[allow(clippy::too_many_arguments)]
|
|
1071
|
+
pub fn bloom_scene_set_transform16(
|
|
1072
|
+
handle: f64,
|
|
1073
|
+
m00: f64, m01: f64, m02: f64, m03: f64,
|
|
1074
|
+
m10: f64, m11: f64, m12: f64, m13: f64,
|
|
1075
|
+
m20: f64, m21: f64, m22: f64, m23: f64,
|
|
1076
|
+
m30: f64, m31: f64, m32: f64, m33: f64,
|
|
1077
|
+
) {
|
|
1078
|
+
// On web we pass the 16 matrix elements as individual f64 args
|
|
1079
|
+
// (no raw pointer passing from WASM)
|
|
1080
|
+
let mat = [
|
|
1081
|
+
[m00 as f32, m01 as f32, m02 as f32, m03 as f32],
|
|
1082
|
+
[m10 as f32, m11 as f32, m12 as f32, m13 as f32],
|
|
1083
|
+
[m20 as f32, m21 as f32, m22 as f32, m23 as f32],
|
|
1084
|
+
[m30 as f32, m31 as f32, m32 as f32, m33 as f32],
|
|
1085
|
+
];
|
|
1086
|
+
engine().scene.set_transform(handle, mat);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
#[wasm_bindgen]
|
|
1090
|
+
pub fn bloom_scene_update_geometry(
|
|
1091
|
+
_handle: f64,
|
|
1092
|
+
_vert_ptr: f64,
|
|
1093
|
+
_vert_count: f64,
|
|
1094
|
+
_idx_ptr: f64,
|
|
1095
|
+
_idx_count: f64,
|
|
1096
|
+
) {
|
|
1097
|
+
// TODO: Phase 4 — need to handle pointer/buffer passing from WASM linear memory.
|
|
1098
|
+
// On web, vertex and index data will need to be passed via a different mechanism
|
|
1099
|
+
// (e.g., typed arrays through JS interop).
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
#[wasm_bindgen]
|
|
1103
|
+
pub fn bloom_scene_set_material_color(handle: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
1104
|
+
engine().scene.set_material_color(handle, r as f32, g as f32, b as f32, a as f32);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
#[wasm_bindgen]
|
|
1108
|
+
pub fn bloom_scene_set_material_pbr(handle: f64, roughness: f64, metalness: f64) {
|
|
1109
|
+
engine().scene.set_material_pbr(handle, roughness as f32, metalness as f32);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
#[wasm_bindgen]
|
|
1113
|
+
pub fn bloom_scene_set_material_texture(handle: f64, texture_idx: f64) {
|
|
1114
|
+
engine().scene.set_material_texture(handle, texture_idx as u32);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
#[wasm_bindgen]
|
|
1118
|
+
pub fn bloom_scene_node_count() -> f64 {
|
|
1119
|
+
engine().scene.node_count() as f64
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
#[wasm_bindgen]
|
|
1123
|
+
pub fn bloom_scene_node_vertex_count(handle: f64) -> f64 {
|
|
1124
|
+
match engine().scene.nodes.get(handle) {
|
|
1125
|
+
Some(node) => node.vertices.len() as f64,
|
|
1126
|
+
None => -1.0,
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
#[wasm_bindgen]
|
|
1131
|
+
pub fn bloom_scene_node_index_count(handle: f64) -> f64 {
|
|
1132
|
+
match engine().scene.nodes.get(handle) {
|
|
1133
|
+
Some(node) => node.indices.len() as f64,
|
|
1134
|
+
None => -1.0,
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
#[wasm_bindgen]
|
|
1139
|
+
pub fn bloom_scene_attach_model(node_handle: f64, model_handle: f64, mesh_index: f64) {
|
|
1140
|
+
let eng = engine();
|
|
1141
|
+
let mi = mesh_index as usize;
|
|
1142
|
+
|
|
1143
|
+
let model_data = match eng.models.models.get(model_handle) {
|
|
1144
|
+
Some(md) => md,
|
|
1145
|
+
None => return,
|
|
1146
|
+
};
|
|
1147
|
+
if mi >= model_data.meshes.len() { return; }
|
|
1148
|
+
let mesh = &model_data.meshes[mi];
|
|
1149
|
+
|
|
1150
|
+
let vertices = mesh.vertices.clone();
|
|
1151
|
+
let indices = mesh.indices.clone();
|
|
1152
|
+
let base_color_tex = mesh.texture_idx;
|
|
1153
|
+
let normal_tex = mesh.normal_texture_idx;
|
|
1154
|
+
let mr_tex = mesh.metallic_roughness_texture_idx;
|
|
1155
|
+
let emissive_tex = mesh.emissive_texture_idx;
|
|
1156
|
+
let emissive_factor = mesh.emissive_factor;
|
|
1157
|
+
let roughness_factor = mesh.roughness_factor;
|
|
1158
|
+
let metallic_factor = mesh.metallic_factor;
|
|
1159
|
+
let alpha_cutoff = mesh.alpha_cutoff;
|
|
1160
|
+
eng.scene.update_geometry(node_handle, vertices, indices);
|
|
1161
|
+
|
|
1162
|
+
if let Some(tex_idx) = base_color_tex {
|
|
1163
|
+
eng.scene.set_material_texture(node_handle, tex_idx);
|
|
1164
|
+
}
|
|
1165
|
+
if let Some(tex_idx) = normal_tex {
|
|
1166
|
+
eng.scene.set_material_normal_texture(node_handle, tex_idx);
|
|
1167
|
+
}
|
|
1168
|
+
if let Some(tex_idx) = mr_tex {
|
|
1169
|
+
eng.scene.set_material_metallic_roughness_texture(node_handle, tex_idx);
|
|
1170
|
+
}
|
|
1171
|
+
if let Some(tex_idx) = emissive_tex {
|
|
1172
|
+
eng.scene.set_material_emissive_texture(node_handle, tex_idx);
|
|
1173
|
+
}
|
|
1174
|
+
eng.scene.set_material_emissive_factor(
|
|
1175
|
+
node_handle,
|
|
1176
|
+
emissive_factor[0],
|
|
1177
|
+
emissive_factor[1],
|
|
1178
|
+
emissive_factor[2],
|
|
1179
|
+
);
|
|
1180
|
+
// Carry the glTF material factors + MASK cutoff too (mirrors the shared
|
|
1181
|
+
// macro) — dropping them left attached foliage opaque (solid cards) and
|
|
1182
|
+
// every attached mesh at the default roughness 0.8 regardless of its
|
|
1183
|
+
// authored material.
|
|
1184
|
+
eng.scene.set_material_pbr(node_handle, roughness_factor, metallic_factor);
|
|
1185
|
+
eng.scene.set_material_alpha_cutoff(node_handle, alpha_cutoff);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// ============================================================
|
|
1189
|
+
// Geometry generation
|
|
1190
|
+
// ============================================================
|
|
1191
|
+
|
|
1192
|
+
#[wasm_bindgen]
|
|
1193
|
+
pub fn bloom_scene_extrude_polygon(
|
|
1194
|
+
_handle: f64,
|
|
1195
|
+
_polygon_ptr: f64,
|
|
1196
|
+
_polygon_count: f64,
|
|
1197
|
+
_depth: f64,
|
|
1198
|
+
) {
|
|
1199
|
+
// TODO: Phase 4 — need to handle pointer/buffer passing from WASM linear memory
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
#[wasm_bindgen]
|
|
1203
|
+
pub fn bloom_scene_subtract_box(
|
|
1204
|
+
handle: f64,
|
|
1205
|
+
min_x: f64, min_y: f64, min_z: f64,
|
|
1206
|
+
max_x: f64, max_y: f64, max_z: f64,
|
|
1207
|
+
) {
|
|
1208
|
+
let eng = engine();
|
|
1209
|
+
if let Some(node) = eng.scene.nodes.get(handle) {
|
|
1210
|
+
let current = bloom_shared::geometry::GeometryData {
|
|
1211
|
+
vertices: node.vertices.clone(),
|
|
1212
|
+
indices: node.indices.clone(),
|
|
1213
|
+
};
|
|
1214
|
+
let result = bloom_shared::geometry::subtract_box(
|
|
1215
|
+
¤t,
|
|
1216
|
+
[min_x as f32, min_y as f32, min_z as f32],
|
|
1217
|
+
[max_x as f32, max_y as f32, max_z as f32],
|
|
1218
|
+
);
|
|
1219
|
+
eng.scene.update_geometry(handle, result.vertices, result.indices);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// ============================================================
|
|
1224
|
+
// Shadow mapping
|
|
1225
|
+
// ============================================================
|
|
1226
|
+
|
|
1227
|
+
#[wasm_bindgen]
|
|
1228
|
+
pub fn bloom_enable_shadows() {
|
|
1229
|
+
engine().renderer.shadow_map.enable();
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
#[wasm_bindgen]
|
|
1233
|
+
pub fn bloom_disable_shadows() {
|
|
1234
|
+
engine().renderer.shadow_map.disable();
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// ============================================================
|
|
1238
|
+
// Post-processing
|
|
1239
|
+
// ============================================================
|
|
1240
|
+
|
|
1241
|
+
#[wasm_bindgen]
|
|
1242
|
+
pub fn bloom_enable_postfx() {
|
|
1243
|
+
let eng = engine();
|
|
1244
|
+
let w = eng.renderer.width();
|
|
1245
|
+
let h = eng.renderer.height();
|
|
1246
|
+
let fmt = eng.renderer.surface_format();
|
|
1247
|
+
eng.postfx = Some(bloom_shared::postfx::PostFxPipeline::new(
|
|
1248
|
+
&eng.renderer.device, w, h, fmt,
|
|
1249
|
+
));
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
#[wasm_bindgen]
|
|
1253
|
+
pub fn bloom_disable_postfx() {
|
|
1254
|
+
engine().postfx = None;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
#[wasm_bindgen]
|
|
1258
|
+
pub fn bloom_postfx_set_selected(handle: f64) {
|
|
1259
|
+
if let Some(pfx) = &mut engine().postfx {
|
|
1260
|
+
if handle == 0.0 {
|
|
1261
|
+
pfx.set_selected(Vec::new());
|
|
1262
|
+
} else {
|
|
1263
|
+
pfx.set_selected(vec![handle]);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
#[wasm_bindgen]
|
|
1269
|
+
pub fn bloom_postfx_set_hovered(handle: f64) {
|
|
1270
|
+
if let Some(pfx) = &mut engine().postfx {
|
|
1271
|
+
pfx.set_hovered(handle);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
#[wasm_bindgen]
|
|
1276
|
+
pub fn bloom_postfx_set_outline_color(r: f64, g: f64, b: f64, a: f64) {
|
|
1277
|
+
if let Some(pfx) = &mut engine().postfx {
|
|
1278
|
+
pfx.outline_params.color_selected = [r as f32, g as f32, b as f32, a as f32];
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
#[wasm_bindgen]
|
|
1283
|
+
pub fn bloom_postfx_set_outline_thickness(thickness: f64) {
|
|
1284
|
+
if let Some(pfx) = &mut engine().postfx {
|
|
1285
|
+
pfx.outline_params.thickness[0] = thickness as f32;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// ============================================================
|
|
1290
|
+
// Picking
|
|
1291
|
+
// ============================================================
|
|
1292
|
+
|
|
1293
|
+
#[wasm_bindgen]
|
|
1294
|
+
pub fn bloom_scene_pick(screen_x: f64, screen_y: f64) -> f64 {
|
|
1295
|
+
let eng = engine();
|
|
1296
|
+
let inv_vp = eng.renderer.inverse_vp_matrix();
|
|
1297
|
+
let cam_pos = eng.renderer.camera_pos();
|
|
1298
|
+
let w = eng.renderer.width() as f32;
|
|
1299
|
+
let h = eng.renderer.height() as f32;
|
|
1300
|
+
|
|
1301
|
+
let (origin, direction) = bloom_shared::picking::screen_to_ray(
|
|
1302
|
+
screen_x as f32, screen_y as f32,
|
|
1303
|
+
w, h, &inv_vp, &cam_pos,
|
|
1304
|
+
);
|
|
1305
|
+
|
|
1306
|
+
let result = bloom_shared::picking::raycast_scene(&eng.scene, &origin, &direction);
|
|
1307
|
+
let hit = result.hit;
|
|
1308
|
+
unsafe { LAST_PICK = Some(result); }
|
|
1309
|
+
if hit { 1.0 } else { 0.0 }
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
#[wasm_bindgen]
|
|
1313
|
+
pub fn bloom_pick_hit_handle() -> f64 {
|
|
1314
|
+
unsafe { LAST_PICK.as_ref().map(|r| r.handle).unwrap_or(0.0) }
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
#[wasm_bindgen]
|
|
1318
|
+
pub fn bloom_pick_hit_distance() -> f64 {
|
|
1319
|
+
unsafe { LAST_PICK.as_ref().map(|r| r.distance as f64).unwrap_or(0.0) }
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
#[wasm_bindgen]
|
|
1323
|
+
pub fn bloom_pick_hit_x() -> f64 {
|
|
1324
|
+
unsafe { LAST_PICK.as_ref().map(|r| r.point[0] as f64).unwrap_or(0.0) }
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
#[wasm_bindgen]
|
|
1328
|
+
pub fn bloom_pick_hit_y() -> f64 {
|
|
1329
|
+
unsafe { LAST_PICK.as_ref().map(|r| r.point[1] as f64).unwrap_or(0.0) }
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
#[wasm_bindgen]
|
|
1333
|
+
pub fn bloom_pick_hit_z() -> f64 {
|
|
1334
|
+
unsafe { LAST_PICK.as_ref().map(|r| r.point[2] as f64).unwrap_or(0.0) }
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
#[wasm_bindgen]
|
|
1338
|
+
pub fn bloom_pick_hit_normal_x() -> f64 {
|
|
1339
|
+
unsafe { LAST_PICK.as_ref().map(|r| r.normal[0] as f64).unwrap_or(0.0) }
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
#[wasm_bindgen]
|
|
1343
|
+
pub fn bloom_pick_hit_normal_y() -> f64 {
|
|
1344
|
+
unsafe { LAST_PICK.as_ref().map(|r| r.normal[1] as f64).unwrap_or(0.0) }
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
#[wasm_bindgen]
|
|
1348
|
+
pub fn bloom_pick_hit_normal_z() -> f64 {
|
|
1349
|
+
unsafe { LAST_PICK.as_ref().map(|r| r.normal[2] as f64).unwrap_or(0.0) }
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// ============================================================
|
|
1353
|
+
// 3D -> 2D Projection
|
|
1354
|
+
// ============================================================
|
|
1355
|
+
|
|
1356
|
+
#[wasm_bindgen]
|
|
1357
|
+
pub fn bloom_project_to_screen(wx: f64, wy: f64, wz: f64) -> f64 {
|
|
1358
|
+
let eng = engine();
|
|
1359
|
+
let vp = eng.renderer.vp_matrix();
|
|
1360
|
+
let w = eng.renderer.width() as f32;
|
|
1361
|
+
let h = eng.renderer.height() as f32;
|
|
1362
|
+
|
|
1363
|
+
let x = wx as f32;
|
|
1364
|
+
let y = wy as f32;
|
|
1365
|
+
let z = wz as f32;
|
|
1366
|
+
let clip_x = vp[0][0]*x + vp[1][0]*y + vp[2][0]*z + vp[3][0];
|
|
1367
|
+
let clip_y = vp[0][1]*x + vp[1][1]*y + vp[2][1]*z + vp[3][1];
|
|
1368
|
+
let clip_w = vp[0][3]*x + vp[1][3]*y + vp[2][3]*z + vp[3][3];
|
|
1369
|
+
|
|
1370
|
+
if clip_w <= 0.0 {
|
|
1371
|
+
unsafe { LAST_PROJECT = (-9999.0, -9999.0); }
|
|
1372
|
+
return -9999.0;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
let ndc_x = clip_x / clip_w;
|
|
1376
|
+
let ndc_y = clip_y / clip_w;
|
|
1377
|
+
let screen_x = ((ndc_x + 1.0) * 0.5 * w) as f64;
|
|
1378
|
+
let screen_y = ((1.0 - ndc_y) * 0.5 * h) as f64;
|
|
1379
|
+
|
|
1380
|
+
unsafe { LAST_PROJECT = (screen_x, screen_y); }
|
|
1381
|
+
screen_x
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
#[wasm_bindgen]
|
|
1385
|
+
pub fn bloom_project_screen_y() -> f64 {
|
|
1386
|
+
unsafe { LAST_PROJECT.1 }
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// ============================================================
|
|
1390
|
+
// File I/O
|
|
1391
|
+
// ============================================================
|
|
1392
|
+
|
|
1393
|
+
#[wasm_bindgen]
|
|
1394
|
+
pub fn bloom_write_file(_path: f64, _data: f64) -> f64 {
|
|
1395
|
+
// Handled by JS glue — uses localStorage
|
|
1396
|
+
0.0
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
#[wasm_bindgen]
|
|
1400
|
+
pub fn bloom_file_exists(_path: f64) -> f64 {
|
|
1401
|
+
// Handled by JS glue — checks localStorage
|
|
1402
|
+
0.0
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
#[wasm_bindgen]
|
|
1406
|
+
pub fn bloom_read_file(_path: f64) -> f64 {
|
|
1407
|
+
// Handled by JS glue — reads from localStorage
|
|
1408
|
+
0.0
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// ============================================================
|
|
1412
|
+
// Cursor
|
|
1413
|
+
// ============================================================
|
|
1414
|
+
|
|
1415
|
+
#[wasm_bindgen]
|
|
1416
|
+
pub fn bloom_disable_cursor() {
|
|
1417
|
+
let input = &mut engine().input;
|
|
1418
|
+
input.cursor_disabled = true;
|
|
1419
|
+
input.clear_mouse_delta();
|
|
1420
|
+
// JS glue also calls canvas.requestPointerLock()
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
#[wasm_bindgen]
|
|
1424
|
+
pub fn bloom_enable_cursor() {
|
|
1425
|
+
engine().input.cursor_disabled = false;
|
|
1426
|
+
// JS glue also calls document.exitPointerLock()
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// ============================================================
|
|
1430
|
+
// Frame callbacks & game loop
|
|
1431
|
+
// ============================================================
|
|
1432
|
+
|
|
1433
|
+
#[wasm_bindgen]
|
|
1434
|
+
pub fn bloom_register_frame_callback(_priority: f64, _callback: f64) -> f64 {
|
|
1435
|
+
// On web, frame callbacks are managed by the JS glue layer (bloom_glue.js)
|
|
1436
|
+
// since the callback is a Perry WASM closure that can only be invoked
|
|
1437
|
+
// through Perry's runtime. The JS glue intercepts this call.
|
|
1438
|
+
0.0
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
#[wasm_bindgen]
|
|
1442
|
+
pub fn bloom_unregister_frame_callback(_id: f64) {
|
|
1443
|
+
// Managed by JS glue layer
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/// Emscripten-style game loop entry point.
|
|
1447
|
+
/// On native: blocks in a while loop calling begin_drawing/callback/end_drawing.
|
|
1448
|
+
/// On web: returns immediately. The JS glue layer (bloom_glue.js) intercepts
|
|
1449
|
+
/// this call and drives the game loop via requestAnimationFrame.
|
|
1450
|
+
#[wasm_bindgen]
|
|
1451
|
+
pub fn bloom_run_game(_callback: f64) {
|
|
1452
|
+
// On web, this is a no-op — the JS glue intercepts the FFI call
|
|
1453
|
+
// before it reaches here and sets up the rAF loop with the callback.
|
|
1454
|
+
// The game's while(!windowShouldClose()) loop should exit after this
|
|
1455
|
+
// (bloom_glue.js makes windowShouldClose return 1.0 once runGame is called).
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// ============================================================
|
|
1459
|
+
// Staging (async asset loading)
|
|
1460
|
+
// ============================================================
|
|
1461
|
+
|
|
1462
|
+
#[wasm_bindgen]
|
|
1463
|
+
pub fn bloom_stage_texture(_path: f64) -> f64 {
|
|
1464
|
+
// TODO: Phase 4 — fetch() texture then decode_and_stage_texture
|
|
1465
|
+
0.0
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
#[wasm_bindgen]
|
|
1469
|
+
pub fn bloom_stage_model(_path: f64) -> f64 {
|
|
1470
|
+
// TODO: Phase 4 — fetch() model then stage
|
|
1471
|
+
0.0
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
#[wasm_bindgen]
|
|
1475
|
+
pub fn bloom_stage_sound(_path: f64) -> f64 {
|
|
1476
|
+
// TODO: Phase 4 — fetch() sound then stage
|
|
1477
|
+
0.0
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
#[wasm_bindgen]
|
|
1481
|
+
pub fn bloom_commit_texture(staging_handle: f64) -> f64 {
|
|
1482
|
+
let staged = match bloom_shared::staging::take_texture(staging_handle) {
|
|
1483
|
+
Some(s) => s,
|
|
1484
|
+
None => return 0.0,
|
|
1485
|
+
};
|
|
1486
|
+
let eng = engine();
|
|
1487
|
+
let bind_group_idx = eng.renderer.register_texture(staged.width, staged.height, &staged.data);
|
|
1488
|
+
eng.textures.textures.alloc(bloom_shared::textures::TextureData {
|
|
1489
|
+
bind_group_idx, width: staged.width, height: staged.height,
|
|
1490
|
+
})
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
#[wasm_bindgen]
|
|
1494
|
+
pub fn bloom_commit_model(staging_handle: f64) -> f64 {
|
|
1495
|
+
let staged = match bloom_shared::staging::take_model(staging_handle) {
|
|
1496
|
+
Some(s) => s,
|
|
1497
|
+
None => return 0.0,
|
|
1498
|
+
};
|
|
1499
|
+
let eng = engine();
|
|
1500
|
+
// Normal maps must go through the kind-aware registration (linear space
|
|
1501
|
+
// + LEADR mips) or the committed model shades visibly flatter than the
|
|
1502
|
+
// same GLB through loadModel. Kept in lockstep with the native
|
|
1503
|
+
// bloom_commit_model in ffi_core/models.rs.
|
|
1504
|
+
let mut tex_map: Vec<u32> = Vec::with_capacity(staged.textures.len());
|
|
1505
|
+
for tex in &staged.textures {
|
|
1506
|
+
tex_map.push(eng.renderer.register_texture_kind(
|
|
1507
|
+
tex.width, tex.height, &tex.data, tex.is_normal));
|
|
1508
|
+
}
|
|
1509
|
+
let mut model = staged.model;
|
|
1510
|
+
// Remap EVERY texture slot, not just the base colour — dropping the
|
|
1511
|
+
// normal/MR/emissive/occlusion references silently strips the character
|
|
1512
|
+
// maps on any model loaded through the staged path (the round-5 "stale
|
|
1513
|
+
// aux-texture remap" bug, which this web copy used to reproduce).
|
|
1514
|
+
let remap = |idx: &mut Option<u32>| {
|
|
1515
|
+
if let Some(i) = *idx {
|
|
1516
|
+
let s = i as usize;
|
|
1517
|
+
*idx = if s > 0 && s <= tex_map.len() {
|
|
1518
|
+
Some(tex_map[s - 1])
|
|
1519
|
+
} else {
|
|
1520
|
+
None
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
for mesh in &mut model.meshes {
|
|
1525
|
+
remap(&mut mesh.texture_idx);
|
|
1526
|
+
remap(&mut mesh.normal_texture_idx);
|
|
1527
|
+
remap(&mut mesh.metallic_roughness_texture_idx);
|
|
1528
|
+
remap(&mut mesh.emissive_texture_idx);
|
|
1529
|
+
remap(&mut mesh.occlusion_texture_idx);
|
|
1530
|
+
}
|
|
1531
|
+
eng.models.models.alloc(model)
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
#[wasm_bindgen]
|
|
1535
|
+
pub fn bloom_commit_sound(staging_handle: f64) -> f64 {
|
|
1536
|
+
match bloom_shared::staging::take_sound(staging_handle) {
|
|
1537
|
+
Some(sd) => engine().audio.load_sound(sd),
|
|
1538
|
+
None => 0.0,
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
#[wasm_bindgen]
|
|
1543
|
+
pub fn bloom_commit_music(staging_handle: f64) -> f64 {
|
|
1544
|
+
match bloom_shared::staging::take_sound(staging_handle) {
|
|
1545
|
+
Some(sd) => engine().audio.load_music(sd),
|
|
1546
|
+
None => 0.0,
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// ============================================================
|
|
1551
|
+
// Platform detection
|
|
1552
|
+
// ============================================================
|
|
1553
|
+
|
|
1554
|
+
#[wasm_bindgen]
|
|
1555
|
+
pub fn bloom_get_platform() -> f64 {
|
|
1556
|
+
7.0 // Web platform ID
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
#[wasm_bindgen]
|
|
1560
|
+
pub fn bloom_get_language() -> f64 {
|
|
1561
|
+
// navigator.language (e.g. "en-US" / "zh-Hans") -> packed 2-letter code.
|
|
1562
|
+
let lang = web_sys::window().and_then(|w| w.navigator().language()).unwrap_or_default();
|
|
1563
|
+
let b = lang.to_ascii_lowercase().into_bytes();
|
|
1564
|
+
if b.len() >= 2 { (b[0] as f64) * 256.0 + (b[1] as f64) } else { 25966.0 }
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
#[wasm_bindgen]
|
|
1568
|
+
pub fn bloom_is_any_input_pressed() -> f64 {
|
|
1569
|
+
if engine().input.is_any_input_pressed() { 1.0 } else { 0.0 }
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
#[wasm_bindgen]
|
|
1573
|
+
pub fn bloom_get_crown_rotation() -> f64 {
|
|
1574
|
+
engine().input.consume_crown_rotation()
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
|
|
1578
|
+
// Scene graph QoL — Q4/Q5/Q6/Q7
|
|
1579
|
+
#[wasm_bindgen]
|
|
1580
|
+
pub fn bloom_scene_get_transform(handle: f64, index: f64) -> f64 {
|
|
1581
|
+
let mat = engine().scene.get_transform(handle);
|
|
1582
|
+
let i = index as usize;
|
|
1583
|
+
let col = i / 4;
|
|
1584
|
+
let row = i % 4;
|
|
1585
|
+
if col < 4 && row < 4 { mat[col][row] as f64 } else { 0.0 }
|
|
1586
|
+
}
|
|
1587
|
+
#[wasm_bindgen]
|
|
1588
|
+
pub fn bloom_scene_get_bounds_min_x(handle: f64) -> f64 { engine().scene.get_bounds(handle).0[0] as f64 }
|
|
1589
|
+
#[wasm_bindgen]
|
|
1590
|
+
pub fn bloom_scene_get_bounds_min_y(handle: f64) -> f64 { engine().scene.get_bounds(handle).0[1] as f64 }
|
|
1591
|
+
#[wasm_bindgen]
|
|
1592
|
+
pub fn bloom_scene_get_bounds_min_z(handle: f64) -> f64 { engine().scene.get_bounds(handle).0[2] as f64 }
|
|
1593
|
+
#[wasm_bindgen]
|
|
1594
|
+
pub fn bloom_scene_get_bounds_max_x(handle: f64) -> f64 { engine().scene.get_bounds(handle).1[0] as f64 }
|
|
1595
|
+
#[wasm_bindgen]
|
|
1596
|
+
pub fn bloom_scene_get_bounds_max_y(handle: f64) -> f64 { engine().scene.get_bounds(handle).1[1] as f64 }
|
|
1597
|
+
#[wasm_bindgen]
|
|
1598
|
+
pub fn bloom_scene_get_bounds_max_z(handle: f64) -> f64 { engine().scene.get_bounds(handle).1[2] as f64 }
|
|
1599
|
+
#[wasm_bindgen]
|
|
1600
|
+
pub fn bloom_scene_set_user_data(handle: f64, data: f64) { engine().scene.set_user_data(handle, data as i64); }
|
|
1601
|
+
#[wasm_bindgen]
|
|
1602
|
+
pub fn bloom_scene_get_user_data(handle: f64) -> f64 { engine().scene.get_user_data(handle) as f64 }
|
|
1603
|
+
|
|
1604
|
+
// Q1: Render texture FFI (stub)
|
|
1605
|
+
#[wasm_bindgen]
|
|
1606
|
+
pub fn bloom_load_render_texture(width: f64, height: f64) -> f64 {
|
|
1607
|
+
engine().textures.load_render_texture(width as u32, height as u32)
|
|
1608
|
+
}
|
|
1609
|
+
#[wasm_bindgen]
|
|
1610
|
+
pub fn bloom_unload_render_texture(handle: f64) { engine().textures.unload_render_texture(handle); }
|
|
1611
|
+
#[wasm_bindgen]
|
|
1612
|
+
pub fn bloom_begin_texture_mode(_handle: f64) { /* stub */ }
|
|
1613
|
+
#[wasm_bindgen]
|
|
1614
|
+
pub fn bloom_end_texture_mode() { /* stub */ }
|
|
1615
|
+
#[wasm_bindgen]
|
|
1616
|
+
pub fn bloom_get_render_texture_texture(handle: f64) -> f64 { engine().textures.get_render_texture_texture(handle) }
|
|
1617
|
+
|
|
1618
|
+
// Q8: Water material
|
|
1619
|
+
#[wasm_bindgen]
|
|
1620
|
+
pub fn bloom_scene_set_material_water(handle: f64, wave_amp: f64, wave_speed: f64, r: f64, g: f64, b: f64, a: f64) {
|
|
1621
|
+
engine().scene.set_material_water(handle, wave_amp as f32, wave_speed as f32, r as f32, g as f32, b as f32, a as f32);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// Q9: Spline ribbon mesh
|
|
1625
|
+
#[wasm_bindgen]
|
|
1626
|
+
pub fn bloom_gen_mesh_spline_ribbon(points_ptr: *const u8, point_count: f64, widths_ptr: *const u8, width_count: f64) -> f64 {
|
|
1627
|
+
let n = point_count as usize;
|
|
1628
|
+
let wn = width_count as usize;
|
|
1629
|
+
let points = unsafe { std::slice::from_raw_parts(points_ptr as *const f32, n * 3) };
|
|
1630
|
+
let widths = unsafe { std::slice::from_raw_parts(widths_ptr as *const f32, wn) };
|
|
1631
|
+
engine().models.gen_mesh_spline_ribbon(points, widths)
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// Q6: Multi-hit picking
|
|
1635
|
+
static mut LAST_PICK_ALL: Vec<bloom_shared::picking::PickResult> = Vec::new();
|
|
1636
|
+
|
|
1637
|
+
#[wasm_bindgen]
|
|
1638
|
+
pub fn bloom_scene_pick_all(screen_x: f64, screen_y: f64, max_results: f64) -> f64 {
|
|
1639
|
+
let eng = engine();
|
|
1640
|
+
let inv_vp = eng.renderer.inverse_vp_matrix();
|
|
1641
|
+
let cam_pos = eng.renderer.camera_pos();
|
|
1642
|
+
let w = eng.renderer.width() as f32;
|
|
1643
|
+
let h = eng.renderer.height() as f32;
|
|
1644
|
+
let (origin, direction) = bloom_shared::picking::screen_to_ray(
|
|
1645
|
+
screen_x as f32, screen_y as f32, w, h, &inv_vp, &cam_pos,
|
|
1646
|
+
);
|
|
1647
|
+
let results = bloom_shared::picking::raycast_scene_all(&eng.scene, &origin, &direction, max_results as usize);
|
|
1648
|
+
let count = results.len();
|
|
1649
|
+
unsafe { LAST_PICK_ALL = results; }
|
|
1650
|
+
count as f64
|
|
1651
|
+
}
|
|
1652
|
+
#[wasm_bindgen]
|
|
1653
|
+
pub fn bloom_pick_all_handle(index: f64) -> f64 {
|
|
1654
|
+
let i = index as usize;
|
|
1655
|
+
unsafe { LAST_PICK_ALL.get(i).map(|r| r.handle).unwrap_or(0.0) }
|
|
1656
|
+
}
|
|
1657
|
+
#[wasm_bindgen]
|
|
1658
|
+
pub fn bloom_pick_all_distance(index: f64) -> f64 {
|
|
1659
|
+
let i = index as usize;
|
|
1660
|
+
unsafe { LAST_PICK_ALL.get(i).map(|r| r.distance as f64).unwrap_or(0.0) }
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// Q2: Cursor shape
|
|
1664
|
+
#[wasm_bindgen]
|
|
1665
|
+
pub fn bloom_set_cursor_shape(shape: f64) {
|
|
1666
|
+
engine().input.cursor_shape = shape as u32;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// E4: Clipboard (stub)
|
|
1670
|
+
#[wasm_bindgen]
|
|
1671
|
+
pub fn bloom_set_clipboard_text(_text_ptr: *const u8) {}
|
|
1672
|
+
#[wasm_bindgen]
|
|
1673
|
+
pub fn bloom_get_clipboard_text() -> f64 { 0.0 }
|
|
1674
|
+
|
|
1675
|
+
// E5b: File dialogs (stub)
|
|
1676
|
+
#[wasm_bindgen]
|
|
1677
|
+
pub fn bloom_open_file_dialog(_filter_ptr: *const u8, _title_ptr: *const u8) -> f64 { 0.0 }
|
|
1678
|
+
#[wasm_bindgen]
|
|
1679
|
+
pub fn bloom_save_file_dialog(_default_name_ptr: *const u8, _title_ptr: *const u8) -> f64 { 0.0 }
|
|
1680
|
+
|
|
1681
|
+
// ============================================================
|
|
1682
|
+
// Render quality toggles (individual + preset) — ticket 011
|
|
1683
|
+
// Mirror of the macOS FFI surface added in commit 95da6af, exposed to
|
|
1684
|
+
// the browser via wasm_bindgen. Without these the TS API's
|
|
1685
|
+
// setQualityPreset / setShadowsEnabled / etc. would fail with
|
|
1686
|
+
// "bloom_set_quality_preset is not a function" on the web target.
|
|
1687
|
+
// ============================================================
|
|
1688
|
+
|
|
1689
|
+
#[wasm_bindgen]
|
|
1690
|
+
pub fn bloom_set_quality_preset(preset: f64) {
|
|
1691
|
+
engine().renderer.apply_quality_preset(preset as u32);
|
|
1692
|
+
}
|
|
1693
|
+
#[wasm_bindgen]
|
|
1694
|
+
pub fn bloom_set_shadows_enabled(on: f64) {
|
|
1695
|
+
engine().renderer.set_shadows_enabled(on != 0.0);
|
|
1696
|
+
}
|
|
1697
|
+
#[wasm_bindgen]
|
|
1698
|
+
pub fn bloom_set_shadows_always_fresh(on: f64) {
|
|
1699
|
+
engine().renderer.set_shadows_always_fresh(on != 0.0);
|
|
1700
|
+
}
|
|
1701
|
+
#[wasm_bindgen]
|
|
1702
|
+
pub fn bloom_set_bloom_enabled(on: f64) {
|
|
1703
|
+
engine().renderer.set_bloom_enabled(on != 0.0);
|
|
1704
|
+
}
|
|
1705
|
+
#[wasm_bindgen]
|
|
1706
|
+
pub fn bloom_set_ssao_enabled(on: f64) {
|
|
1707
|
+
engine().renderer.set_ssao_enabled(on != 0.0);
|
|
1708
|
+
}
|
|
1709
|
+
#[wasm_bindgen]
|
|
1710
|
+
pub fn bloom_set_ssao_intensity(value: f64) {
|
|
1711
|
+
engine().renderer.set_ssao_strength(value as f32);
|
|
1712
|
+
}
|
|
1713
|
+
#[wasm_bindgen]
|
|
1714
|
+
pub fn bloom_set_ssao_radius(world_radius: f64) {
|
|
1715
|
+
engine().renderer.set_ssao_radius(world_radius as f32);
|
|
1716
|
+
}
|
|
1717
|
+
#[wasm_bindgen]
|
|
1718
|
+
pub fn bloom_set_wind(dir_x: f64, dir_z: f64, amplitude: f64, frequency: f64) {
|
|
1719
|
+
engine().renderer.set_wind(dir_x as f32, dir_z as f32, amplitude as f32, frequency as f32);
|
|
1720
|
+
}
|
|
1721
|
+
#[wasm_bindgen]
|
|
1722
|
+
pub fn bloom_launch_process(_cmd: f64, _args: f64, _cwd: f64) -> f64 {
|
|
1723
|
+
// A web page does not get to launch processes.
|
|
1724
|
+
0.0
|
|
1725
|
+
}
|
|
1726
|
+
#[wasm_bindgen]
|
|
1727
|
+
pub fn bloom_set_output_scale(scale: f64) {
|
|
1728
|
+
engine().renderer.set_output_scale(scale as f32);
|
|
1729
|
+
}
|
|
1730
|
+
#[wasm_bindgen]
|
|
1731
|
+
pub fn bloom_get_output_scale() -> f64 {
|
|
1732
|
+
engine().renderer.output_scale() as f64
|
|
1733
|
+
}
|
|
1734
|
+
#[wasm_bindgen]
|
|
1735
|
+
pub fn bloom_set_model_foliage_wind(model: f64, amount: f64) {
|
|
1736
|
+
engine().renderer.set_model_foliage_wind(model.to_bits(), amount as f32);
|
|
1737
|
+
}
|
|
1738
|
+
#[wasm_bindgen]
|
|
1739
|
+
pub fn bloom_set_foliage_shadow_motion(on: f64) {
|
|
1740
|
+
engine().renderer.set_foliage_shadow_motion(on > 0.5);
|
|
1741
|
+
}
|
|
1742
|
+
#[wasm_bindgen]
|
|
1743
|
+
pub fn bloom_set_cloud_shadows(strength: f64, deck_height: f64, feature_scale: f64, drift_speed: f64) {
|
|
1744
|
+
engine().renderer.set_cloud_shadows(
|
|
1745
|
+
strength as f32, deck_height as f32, feature_scale as f32, drift_speed as f32);
|
|
1746
|
+
}
|
|
1747
|
+
#[wasm_bindgen]
|
|
1748
|
+
pub fn bloom_set_ssr_enabled(on: f64) {
|
|
1749
|
+
engine().renderer.set_ssr_enabled(on != 0.0);
|
|
1750
|
+
}
|
|
1751
|
+
#[wasm_bindgen]
|
|
1752
|
+
pub fn bloom_set_motion_blur_enabled(on: f64) {
|
|
1753
|
+
engine().renderer.set_motion_blur_enabled(on != 0.0);
|
|
1754
|
+
}
|
|
1755
|
+
#[wasm_bindgen]
|
|
1756
|
+
pub fn bloom_set_sss_enabled(on: f64) {
|
|
1757
|
+
engine().renderer.set_sss_enabled(on != 0.0);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// ============================================================
|
|
1761
|
+
// Profiler — CPU phase timings (always available). TIMESTAMP_QUERY
|
|
1762
|
+
// is not part of the WebGPU spec as of wgpu 29, so the GPU path
|
|
1763
|
+
// returns 0 on web regardless of adapter — the profiler stays
|
|
1764
|
+
// CPU-only, which is still useful for frame-phase profiling.
|
|
1765
|
+
// ============================================================
|
|
1766
|
+
|
|
1767
|
+
#[wasm_bindgen]
|
|
1768
|
+
pub fn bloom_set_profiler_enabled(on: f64) {
|
|
1769
|
+
engine().profiler.set_enabled(on != 0.0);
|
|
1770
|
+
}
|
|
1771
|
+
#[wasm_bindgen]
|
|
1772
|
+
pub fn bloom_get_profiler_frame_cpu_us() -> f64 {
|
|
1773
|
+
engine().profiler.avg_frame_cpu_us()
|
|
1774
|
+
}
|
|
1775
|
+
#[wasm_bindgen]
|
|
1776
|
+
pub fn bloom_get_profiler_frame_gpu_us() -> f64 {
|
|
1777
|
+
engine().profiler.avg_frame_gpu_us()
|
|
1778
|
+
}
|
|
1779
|
+
#[wasm_bindgen]
|
|
1780
|
+
pub fn bloom_print_profiler_summary() {
|
|
1781
|
+
// No stdout in the browser — route through the existing
|
|
1782
|
+
// console_log binding so the summary lands in devtools.
|
|
1783
|
+
console_log(&engine().profiler.summary());
|
|
1784
|
+
}
|
|
1785
|
+
// ============================================================
|
|
1786
|
+
|
|
1787
|
+
// Physics (Jolt via JoltPhysics.js) — the whole bloom_physics_* surface
|
|
1788
|
+
// lives in physics_ffi.rs, kept in lockstep with jolt_bridge.js and the
|
|
1789
|
+
// package.json manifest (tools/validate-ffi.js checks the name coverage).
|
|
1790
|
+
mod physics_ffi;
|
|
1791
|
+
pub use physics_ffi::*;
|
|
1792
|
+
|
|
1793
|
+
// Renderer settings + LOD surface live in render_settings.rs (2000-line
|
|
1794
|
+
// file policy); same shared engine methods the native macro wraps.
|
|
1795
|
+
mod render_settings;
|
|
1796
|
+
pub use render_settings::*;
|