feat(render): apply placement in xy preview

This commit is contained in:
2026-07-18 18:19:55 +04:00
parent cc7a1a0faa
commit 4d024223bb
5 changed files with 102 additions and 32 deletions
@@ -174,6 +174,38 @@ pub fn project_msh_to_static_mesh_in_xy_frame(
})
}
/// Projects a static MSH preview through the recovered placement transform and
/// then into a shared diagnostic XY frame.
///
/// This is the diagnostic-camera counterpart of
/// [`project_msh_to_static_mesh_in_world_space_with_node_fallback_poses`]. It
/// deliberately keeps the diagnostic camera itself separate from the original
/// D3D7 camera, while ensuring that mission placement uses the same proven
/// `Rz(z) * Ry(y) * Rx(x)` convention in both preview paths.
///
/// # Errors
///
/// Returns [`VulkanAssetMeshError`] when the model cannot be represented by
/// the static bridge or a transformed coordinate is non-finite.
pub fn project_msh_to_static_mesh_in_xy_frame_with_node_fallback_poses(
model: &ModelAsset,
frame: VulkanStaticXyFrame,
transform: LegacyIron3dEulerTransform,
scale: [f32; 3],
) -> Result<VulkanStaticMesh, VulkanAssetMeshError> {
let mut mesh = project_msh_to_static_mesh_in_world_space_with_node_fallback_poses(
model, transform, scale,
)?;
for vertex in &mut mesh.vertices {
if !vertex.position.iter().all(|value| value.is_finite()) {
return Err(VulkanAssetMeshError::NonFinitePosition);
}
let projected = frame.project(vertex.position);
vertex.position = [projected[0], projected[1], 0.0];
}
Ok(mesh)
}
/// Projects MSH geometry into source world coordinates with known translation and scale.
///
/// Unlike [`project_msh_to_static_mesh_in_xy_frame`], this does not normalize
@@ -906,6 +938,32 @@ mod tests {
assert_eq!(mesh.vertices[2].position, [10.0, 22.0, 30.0]);
}
#[test]
fn xy_preview_applies_recovered_iron3d_orientation_before_framing() {
let source = model(
vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]],
vec![0, 1, 2],
vec![batch(0, 3, 0)],
);
let frame =
VulkanStaticXyFrame::from_bounds(0.0, 4.0, 0.0, 4.0).expect("valid diagnostic frame");
let mesh = project_msh_to_static_mesh_in_xy_frame_with_node_fallback_poses(
&source,
frame,
LegacyIron3dEulerTransform {
translation: [2.0, 1.0, 0.0],
orientation_radians: [0.0, 0.0, std::f32::consts::FRAC_PI_2],
},
[1.0; 3],
)
.expect("diagnostic static mesh");
assert!(mesh.vertices[0].position[0].abs() < 0.0001);
assert!(mesh.vertices[0].position[1].abs() < 0.0001);
assert_eq!(mesh.vertices[0].position[2], 0.0);
}
#[test]
fn world_space_msh_applies_each_node_fallback_pose_before_root_transform() {
let mut nodes = vec![0_u8; 76];
+3 -1
View File
@@ -45,7 +45,9 @@ pub use self::asset_mesh::{
project_msh_to_static_mesh, project_msh_to_static_mesh_in_world_space,
project_msh_to_static_mesh_in_world_space_with_node_fallback_poses,
project_msh_to_static_mesh_in_world_space_with_transform,
project_msh_to_static_mesh_in_xy_frame, VulkanAssetMeshError, VulkanStaticXyFrame,
project_msh_to_static_mesh_in_xy_frame,
project_msh_to_static_mesh_in_xy_frame_with_node_fallback_poses, VulkanAssetMeshError,
VulkanStaticXyFrame,
};
pub use self::capabilities::{
probe_vulkan_runtime_capabilities, probe_vulkan_runtime_capabilities_for_request,
+18 -14
View File
@@ -35,9 +35,10 @@ use fparkan_render_vulkan::{
project_land_msh_to_static_mesh_in_legacy_world_space,
project_land_msh_to_static_mesh_in_xy_frame,
project_msh_to_static_mesh_in_world_space_with_node_fallback_poses,
project_msh_to_static_mesh_in_xy_frame, VulkanPlanningBackend, VulkanSmokeFrameOutcome,
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanStaticCamera, VulkanStaticMaterial,
VulkanStaticMesh, VulkanStaticTexture, VulkanStaticXyFrame,
project_msh_to_static_mesh_in_xy_frame_with_node_fallback_poses, VulkanPlanningBackend,
VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo,
VulkanStaticCamera, VulkanStaticMaterial, VulkanStaticMesh, VulkanStaticTexture,
VulkanStaticXyFrame,
};
use fparkan_runtime::{
create, frame, load_mission, load_mission_static_preview, load_mission_static_preview_roots,
@@ -310,10 +311,13 @@ fn static_preview_mesh_and_materials(
)
} else {
let frame = xy_frame.ok_or_else(|| "missing diagnostic XY frame".to_string())?;
project_msh_to_static_mesh_in_xy_frame(
project_msh_to_static_mesh_in_xy_frame_with_node_fallback_poses(
&model.validated,
frame,
root.position,
LegacyIron3dEulerTransform {
translation: root.position,
orientation_radians: root.orientation_raw,
},
root.scale,
)
}
@@ -422,16 +426,16 @@ fn static_preview_xy_frame(
format!("static preview visual {visual_id:?} references unknown model {model_id:?}")
})?;
for position in &model.validated.positions {
let position = LegacyIron3dEulerTransform {
translation: root.position,
orientation_radians: root.orientation_raw,
}
.try_transform_scaled_point(*position, root.scale)
.ok_or_else(|| {
"static preview contains a non-finite transformed XY position".to_string()
})?;
extend_static_preview_xy_bounds(
[
position[0] * root.scale[0] + root.position[0],
position[1] * root.scale[1] + root.position[1],
position[2] * root.scale[2] + root.position[2],
],
&mut min_x,
&mut max_x,
&mut min_y,
&mut max_y,
position, &mut min_x, &mut max_x, &mut min_y, &mut max_y,
)?;
}
}
+5 -4
View File
@@ -13,7 +13,7 @@
| `RecordingBackend` | No | No | Optional CPU-side IDs only | `covered-planning` | Stable command capture for backend-neutral tests | Native window, Vulkan, GPU resource lifetime, pixels |
| `NullBackend` | No | No | Optional CPU-side IDs only | Usually `covered` for validation-only rows | Command stream framing and bounds validation | Capture stability, GPU execution, pixels |
| `VulkanAssetRenderer` | Yes | Yes | Yes | `covered-gpu` | Static original asset rendering: MSH/Texm/WEAR/MAT0/terrain through Vulkan | Animation/FX parity unless explicitly wired |
| `fparkan-game --backend static-vulkan` | Yes, GOG `Autodemo.00` | Yes, full static AutoDemo preview | Eight mission objects, 66 MSH components, original diffuse TEXM and Land2 terrain base layer | `covered-gpu` for the bounded static-scene bridge | Captured legacy camera, placed translation/scale/Z Euler transform, fallback node hierarchy, source-world geometry, 32-bit GPU indices, diffuse descriptors, terrain base draws and synchronized teardown telemetry | Material phases, Land1 blend/microtexture/lightmaps, animated keys, FX, camera control and gameplay parity |
| `fparkan-game --backend static-vulkan` | Yes, GOG `Autodemo.00` | Yes, full static AutoDemo preview | Eight mission objects, 66 MSH components, original diffuse TEXM and Land2 terrain base layer | `covered-gpu` for the bounded static-scene bridge | Diagnostic XY framing with recovered translation/scale/Z Euler placement, fallback node hierarchy, optional captured legacy camera, 32-bit GPU indices, diffuse descriptors, terrain base draws and synchronized teardown telemetry | Material phases, Land1 blend/microtexture/lightmaps, animated keys, FX, camera control and gameplay parity |
| Future rendered `fparkan-game` mode | Yes | Yes | Yes | `covered-gpu` plus original-evidence IDs | Mission-driven render snapshot execution and pixel capture | Original-runtime parity for animation/FX/x87 without dedicated captures |
## Rules
@@ -36,10 +36,11 @@
synthetic window descriptor и `VulkanPlanningBackend`. Opt-in `--backend static-vulkan`
создаёт native `winit` window. Для GOG `MISSIONS/Autodemo.00/data.tma` он уже
рендерит весь статический набор из восьми mission objects и 66 MSH components с
captured legacy camera, доказанным `Rz * Ry * Rx` placement transform,
diagnostic XY camera (or an optional captured legacy camera), доказанным `Rz * Ry * Rx` placement transform,
fallback node hierarchy и Land2 base terrain. Последний validation-clean
трёхкадровый запуск имел 67 material descriptors, `clip_visible_vertices=3415`
и readback hash `1275533143935640133`. Это `covered-gpu` для ограниченного
текущий трёхкадровый запуск после применения placement transform в XY path
имел 71 material descriptor, `clip_visible_vertices=75543` и readback hash
`5444013368935681345`. Это `covered-gpu` для ограниченного
static-scene bridge, но не pixel parity и не доказательство material phase,
Land1 blend, microtexture, lightmap, dynamic animation, FX, live camera
selection или gameplay rendering.
+18 -13
View File
@@ -985,8 +985,11 @@ The bounded native preview now retains the already validated `Land.msh` inside
runtime boundary instead of decoding archive formats a second time. It merges
one terrain component with the first TMA root's 14 reachable MSH components in
one explicit top-down XY frame. The frame is computed from terrain positions
and the root models after applying only the decoded TMA `position` and `scale`.
Raw TMA orientation is deliberately excluded: its convention is not proven.
and the root models after applying decoded TMA placement: non-uniform `scale`,
then the recovered `Rz * Ry * Rx` orientation, then `position`. The XY camera
itself remains diagnostic; applying the established placement transform here
prevents differently oriented mission objects from being framed or rasterized
at a false location.
Terrain is assigned an explicit 1x1 white diagnostic texture, rather than a
guessed terrain material. `Land.msh` slot selection, `material_tag`, masks,
@@ -1004,10 +1007,11 @@ geometry and descriptor set changed; it is not an original-frame comparison.
### Multiple static-preview roots
The native bridge now combines every mesh-backed visual of each selected root,
using that root's preserved TMA `position` and `scale` in the shared diagnostic
XY frame. It reports the actual selected root count as `preview_roots`, rather
than implying that all mission objects are rendered. Raw orientation, original
camera/frustum and visibility remain unresolved.
using that root's recovered static TMA placement (`Rz * Ry * Rx` after scale)
in the shared diagnostic XY frame. It reports the actual selected root count as
`preview_roots`, rather than implying that all mission objects are rendered.
The original camera/frustum, dynamic orientation ownership and visibility
remain unresolved.
Fresh canonical GOG `Autodemo.00` evidence with `--preview-roots 2` completed
in 59.7 seconds: 25 submitted MSH components, one terrain component and 26
@@ -1430,13 +1434,14 @@ runtime wiring, not a claim of scene parity.
The geometry side of that handoff is also explicit. The static mesh adapter now
has source-world projections for `Land.msh` and MSH: terrain positions retain
all three decoded coordinates; model positions retain Z and apply only the
already decoded mission translation and scale. These APIs preserve triangle
order, batch ranges and packed UV decoding, but do not interpret the still
unproven raw object orientation. The current game preview deliberately stays
on its XY diagnostic projection until a runtime supplies a real legacy camera;
selecting source-world coordinates with identity camera would be a misleading
empty/off-screen viewer rather than a compatibility result.
all three decoded coordinates; model positions retain Z and apply the recovered
static mission placement. The diagnostic XY counterpart uses that same placement
before normalizing X/Y, so it no longer silently omits authored object rotation.
These APIs preserve triangle order, batch ranges and packed UV decoding. The
current game preview deliberately stays on its XY diagnostic projection until a
runtime supplies a real legacy camera; selecting source-world coordinates with
identity camera would be a misleading empty/off-screen viewer rather than a
compatibility result.
Static analysis of GOG `iron3d.dll` recovers the placement primitive at RVA
`0x36610`: it evaluates a row-major affine `Rz(z) * Ry(y) * Rx(x)` and writes