diff --git a/native/shared/src/ffi_core/models.rs b/native/shared/src/ffi_core/models.rs index 80232df..6e0c562 100644 --- a/native/shared/src/ffi_core/models.rs +++ b/native/shared/src/ffi_core/models.rs @@ -115,6 +115,81 @@ macro_rules! __bloom_ffi_models { $crate::ffi::feature_off_warn_once("bloom_draw_model_rotated", "models3d"); } + // bloom_draw_model_transform16 — EN-039. [gated: models3d] + // + // The immediate-mode twin of bloom_scene_set_transform16: a full + // column-major 4x4, so an immediate draw can finally pitch and roll. + // bloom_draw_model_rotated only takes a Y rotation, which is why the + // shooter's gun cannot tilt with the aim. + // + // The ticket proposed routing this through the mesh scratch on the + // grounds that "Perry can't pass 16 f64 args". It can: + // bloom_scene_set_transform16 already passes 17 and works. Spelling the + // matrix out keeps this STATELESS — no scratch to reset, no ordering + // hazard between a reset and a draw — which is the same reasoning + // recorded on the scene twin. + // + // SKINNED models are rejected rather than silently mis-drawn: their + // joint matrices already bake world orientation, so a caller-supplied + // model matrix would double-transform them. bloom_draw_model_rotated + // has the same carve-out (it ignores rot_y for skinned). + #[cfg(feature = "models3d")] + #[no_mangle] + #[allow(clippy::too_many_arguments)] + pub extern "C" fn bloom_draw_model_transform16( + handle: f64, + m0: f64, m1: f64, m2: f64, m3: f64, + m4: f64, m5: f64, m6: f64, m7: f64, + m8: f64, m9: f64, m10: f64, m11: f64, + m12: f64, m13: f64, m14: f64, m15: f64, + color_packed_argb: f64, + ) { + $crate::ffi::guard("bloom_draw_model_transform16", move || { + let bits = color_packed_argb as u32; + let a = ((bits >> 24) & 0xff) as f32 / 255.0; + let r = ((bits >> 16) & 0xff) as f32 / 255.0; + let g = ((bits >> 8) & 0xff) as f32 / 255.0; + let b = ( bits & 0xff) as f32 / 255.0; + let s = [ + m0, m1, m2, m3, + m4, m5, m6, m7, + m8, m9, m10, m11, + m12, m13, m14, m15, + ]; + let mut mat = [[0.0f32; 4]; 4]; + for col in 0..4 { + for row in 0..4 { + mat[col][row] = s[col * 4 + row] as f32; + } + } + let eng = engine(); + if let Some(model) = eng.models.get(handle) { + let handle_bits = handle.to_bits(); + if eng.renderer.cache_model_if_static(handle_bits, &model.meshes) { + if eng.renderer.is_model_skinned(handle_bits) { + return; // see the note above + } + eng.renderer.draw_model_cached_transform( + handle_bits, mat, [r, g, b, a], + ); + } + } + }) + } + #[cfg(not(feature = "models3d"))] + #[no_mangle] + #[allow(clippy::too_many_arguments)] + pub extern "C" fn bloom_draw_model_transform16( + _handle: f64, + _m0: f64, _m1: f64, _m2: f64, _m3: f64, + _m4: f64, _m5: f64, _m6: f64, _m7: f64, + _m8: f64, _m9: f64, _m10: f64, _m11: f64, + _m12: f64, _m13: f64, _m14: f64, _m15: f64, + _color_packed_argb: f64, + ) { + $crate::ffi::feature_off_warn_once("bloom_draw_model_transform16", "models3d"); + } + // bloom_unload_model [source: linux; gated: models3d] #[cfg(feature = "models3d")] #[no_mangle] diff --git a/native/shared/src/renderer/model_draw.rs b/native/shared/src/renderer/model_draw.rs index b985745..45b71e2 100644 --- a/native/shared/src/renderer/model_draw.rs +++ b/native/shared/src/renderer/model_draw.rs @@ -107,6 +107,51 @@ impl Renderer { } } + /// EN-039 — `draw_model_cached` taking the model matrix WHOLE, instead of + /// composing it from a position/scale/yaw. `draw_model_cached_rotated` above + /// can only express a Y rotation, so an immediate-mode caller could not + /// pitch or roll: the shooter's weapon stays level while the camera looks up + /// or down, and the same wall hits any held prop, thrown object or debris + /// chunk. Everything downstream (alpha cutout, normal/MR maps, foliage wind, + /// cutout shadows, planar reflections) is identical — this only skips the + /// composition step and trusts the caller's matrix. + pub fn draw_model_cached_transform( + &mut self, + handle_bits: u64, + model_matrix: [[f32; 4]; 4], + tint: [f32; 4], + ) { + let mesh_count = match self.model_gpu_cache.get(&handle_bits) { + Some(Some(meshes)) => meshes.len(), + _ => return, + }; + + let foliage = self.foliage_wind.get(&handle_bits).copied().unwrap_or(0.0); + + for mesh_idx in 0..mesh_count { + let slot = self.next_model_uniform_slot; + self.next_model_uniform_slot += 1; + self.ensure_model_uniform_slot(slot); + + let model_mvp = mat4_multiply(self.current_vp_matrix, model_matrix); + self.stage_model_uniform(slot, &Uniforms3D { + mvp: model_mvp, model: model_matrix, + prev_mvp: model_mvp, model_tint: tint, + misc: [0.0, 0.0, foliage, 0.0], + }); + + self.model_draw_commands.push(CachedModelDraw { + uniform_slot: slot, + cache_handle: handle_bits, + mesh_idx, + model: model_matrix, + skinned: false, + joint_offset: 0.0, + bounds_override: None, + }); + } + } + /// Cached-path draw for a SKINNED model: the bind-pose VB/IB stay /// GPU-resident (raw joint indices) and the scene VS skins from the /// shared joint buffer — replacing the immediate path's per-frame diff --git a/package.json b/package.json index e194817..5b3f1ed 100644 --- a/package.json +++ b/package.json @@ -831,6 +831,30 @@ ], "returns": "void" }, + { + "name": "bloom_draw_model_transform16", + "params": [ + "f64", + "f64", + "f64", + "f64", + "f64", + "f64", + "f64", + "f64", + "f64", + "f64", + "f64", + "f64", + "f64", + "f64", + "f64", + "f64", + "f64", + "f64" + ], + "returns": "void" + }, { "name": "bloom_unload_model", "params": [ diff --git a/src/index.ts b/src/index.ts index 35353bd..822ec69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -67,7 +67,7 @@ export { } from './textures/index'; export { - loadModel, drawModel, drawModelRotated, unloadModel, getModelBounds, genMeshSplineRibbon, + loadModel, drawModel, drawModelRotated, drawModelTransform, unloadModel, getModelBounds, genMeshSplineRibbon, setModelFoliageWind, setFoliageShadowMotion, drawCube, drawCubeWires, drawSphere, drawSphereWires, drawCylinder, drawPlane, drawGrid, drawRay, genMeshCube, genMeshHeightmap, diff --git a/src/models/index.ts b/src/models/index.ts index 346aefb..695275f 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -5,6 +5,14 @@ import { Color, Model, Vec3, Mat4, BoundingBox } from '../core/types'; declare function bloom_load_model(path: number): number; declare function bloom_draw_model(handle: number, x: number, y: number, z: number, scale: number, r: number, g: number, b: number, a: number): void; declare function bloom_draw_model_rotated(handle: number, x: number, y: number, z: number, scale: number, rotY: number, colorPackedArgb: number): void; +declare function bloom_draw_model_transform16( + handle: number, + m0: number, m1: number, m2: number, m3: number, + m4: number, m5: number, m6: number, m7: number, + m8: number, m9: number, m10: number, m11: number, + m12: number, m13: number, m14: number, m15: number, + colorPackedArgb: number, +): void; declare function bloom_set_model_foliage_wind(handle: number, amount: number): void; declare function bloom_set_foliage_shadow_motion(on: number): void; declare function bloom_unload_model(handle: number): void; @@ -182,6 +190,40 @@ export function drawModelRotated( bloom_draw_model_rotated(model.handle, position.x, position.y, position.z, scale, rotY * Math.PI / 180, packed); } +/** + * EN-039 — draw a model under a full column-major 4x4 transform, so an + * immediate-mode draw can pitch and roll. `drawModelRotated` above can only + * express a Y rotation: a held weapon cannot tilt with the aim, and neither can + * a thrown prop or a debris chunk. + * + * `m16` is column-major (same layout as `setSceneNodeTransform16` and as + * `mat4_*` in this engine — note the two conventions warning in the PT docs: + * `mat4_multiply` is RAW). It carries translation and scale too, so this + * REPLACES position/scale rather than combining with them. + * + * Skinned models are a no-op here on purpose: their joint matrices already bake + * world orientation, so applying a model matrix on top double-transforms them. + * Use `animUpdate` for those. + * + * Tint components are 0-255. RGBA packs into one f64 (ARGB byte order), as in + * `drawModelRotated`. + */ +export function drawModelTransform(model: Model, m16: number[], tint: Color): void { + const a = (tint.a & 0xff) << 24; + const r = (tint.r & 0xff) << 16; + const g = (tint.g & 0xff) << 8; + const b = tint.b & 0xff; + const packed = (a | r | g | b) >>> 0; + bloom_draw_model_transform16( + model.handle, + m16[0], m16[1], m16[2], m16[3], + m16[4], m16[5], m16[6], m16[7], + m16[8], m16[9], m16[10], m16[11], + m16[12], m16[13], m16[14], m16[15], + packed, + ); +} + export function unloadModel(model: Model): void { bloom_unload_model(model.handle); }