feat(render): apply placement in xy preview
This commit is contained in:
@@ -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.
|
/// 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
|
/// 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]);
|
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]
|
#[test]
|
||||||
fn world_space_msh_applies_each_node_fallback_pose_before_root_transform() {
|
fn world_space_msh_applies_each_node_fallback_pose_before_root_transform() {
|
||||||
let mut nodes = vec![0_u8; 76];
|
let mut nodes = vec![0_u8; 76];
|
||||||
|
|||||||
@@ -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, 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_node_fallback_poses,
|
||||||
project_msh_to_static_mesh_in_world_space_with_transform,
|
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::{
|
pub use self::capabilities::{
|
||||||
probe_vulkan_runtime_capabilities, probe_vulkan_runtime_capabilities_for_request,
|
probe_vulkan_runtime_capabilities, probe_vulkan_runtime_capabilities_for_request,
|
||||||
|
|||||||
@@ -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_legacy_world_space,
|
||||||
project_land_msh_to_static_mesh_in_xy_frame,
|
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_world_space_with_node_fallback_poses,
|
||||||
project_msh_to_static_mesh_in_xy_frame, VulkanPlanningBackend, VulkanSmokeFrameOutcome,
|
project_msh_to_static_mesh_in_xy_frame_with_node_fallback_poses, VulkanPlanningBackend,
|
||||||
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanStaticCamera, VulkanStaticMaterial,
|
VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo,
|
||||||
VulkanStaticMesh, VulkanStaticTexture, VulkanStaticXyFrame,
|
VulkanStaticCamera, VulkanStaticMaterial, VulkanStaticMesh, VulkanStaticTexture,
|
||||||
|
VulkanStaticXyFrame,
|
||||||
};
|
};
|
||||||
use fparkan_runtime::{
|
use fparkan_runtime::{
|
||||||
create, frame, load_mission, load_mission_static_preview, load_mission_static_preview_roots,
|
create, frame, load_mission, load_mission_static_preview, load_mission_static_preview_roots,
|
||||||
@@ -310,10 +311,13 @@ fn static_preview_mesh_and_materials(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let frame = xy_frame.ok_or_else(|| "missing diagnostic XY frame".to_string())?;
|
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,
|
&model.validated,
|
||||||
frame,
|
frame,
|
||||||
root.position,
|
LegacyIron3dEulerTransform {
|
||||||
|
translation: root.position,
|
||||||
|
orientation_radians: root.orientation_raw,
|
||||||
|
},
|
||||||
root.scale,
|
root.scale,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -422,16 +426,16 @@ fn static_preview_xy_frame(
|
|||||||
format!("static preview visual {visual_id:?} references unknown model {model_id:?}")
|
format!("static preview visual {visual_id:?} references unknown model {model_id:?}")
|
||||||
})?;
|
})?;
|
||||||
for position in &model.validated.positions {
|
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(
|
extend_static_preview_xy_bounds(
|
||||||
[
|
position, &mut min_x, &mut max_x, &mut min_y, &mut max_y,
|
||||||
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,
|
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| 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
|
## Rules
|
||||||
@@ -36,10 +36,11 @@
|
|||||||
synthetic window descriptor и `VulkanPlanningBackend`. Opt-in `--backend static-vulkan`
|
synthetic window descriptor и `VulkanPlanningBackend`. Opt-in `--backend static-vulkan`
|
||||||
создаёт native `winit` window. Для GOG `MISSIONS/Autodemo.00/data.tma` он уже
|
создаёт native `winit` window. Для GOG `MISSIONS/Autodemo.00/data.tma` он уже
|
||||||
рендерит весь статический набор из восьми mission objects и 66 MSH components с
|
рендерит весь статический набор из восьми 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
|
fallback node hierarchy и Land2 base terrain. Последний validation-clean
|
||||||
трёхкадровый запуск имел 67 material descriptors, `clip_visible_vertices=3415`
|
текущий трёхкадровый запуск после применения placement transform в XY path
|
||||||
и readback hash `1275533143935640133`. Это `covered-gpu` для ограниченного
|
имел 71 material descriptor, `clip_visible_vertices=75543` и readback hash
|
||||||
|
`5444013368935681345`. Это `covered-gpu` для ограниченного
|
||||||
static-scene bridge, но не pixel parity и не доказательство material phase,
|
static-scene bridge, но не pixel parity и не доказательство material phase,
|
||||||
Land1 blend, microtexture, lightmap, dynamic animation, FX, live camera
|
Land1 blend, microtexture, lightmap, dynamic animation, FX, live camera
|
||||||
selection или gameplay rendering.
|
selection или gameplay rendering.
|
||||||
|
|||||||
+18
-13
@@ -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
|
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 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
|
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`.
|
and the root models after applying decoded TMA placement: non-uniform `scale`,
|
||||||
Raw TMA orientation is deliberately excluded: its convention is not proven.
|
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
|
Terrain is assigned an explicit 1x1 white diagnostic texture, rather than a
|
||||||
guessed terrain material. `Land.msh` slot selection, `material_tag`, masks,
|
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
|
### Multiple static-preview roots
|
||||||
|
|
||||||
The native bridge now combines every mesh-backed visual of each selected root,
|
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
|
using that root's recovered static TMA placement (`Rz * Ry * Rx` after scale)
|
||||||
XY frame. It reports the actual selected root count as `preview_roots`, rather
|
in the shared diagnostic XY frame. It reports the actual selected root count as
|
||||||
than implying that all mission objects are rendered. Raw orientation, original
|
`preview_roots`, rather than implying that all mission objects are rendered.
|
||||||
camera/frustum and visibility remain unresolved.
|
The original camera/frustum, dynamic orientation ownership and visibility
|
||||||
|
remain unresolved.
|
||||||
|
|
||||||
Fresh canonical GOG `Autodemo.00` evidence with `--preview-roots 2` completed
|
Fresh canonical GOG `Autodemo.00` evidence with `--preview-roots 2` completed
|
||||||
in 59.7 seconds: 25 submitted MSH components, one terrain component and 26
|
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
|
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
|
has source-world projections for `Land.msh` and MSH: terrain positions retain
|
||||||
all three decoded coordinates; model positions retain Z and apply only the
|
all three decoded coordinates; model positions retain Z and apply the recovered
|
||||||
already decoded mission translation and scale. These APIs preserve triangle
|
static mission placement. The diagnostic XY counterpart uses that same placement
|
||||||
order, batch ranges and packed UV decoding, but do not interpret the still
|
before normalizing X/Y, so it no longer silently omits authored object rotation.
|
||||||
unproven raw object orientation. The current game preview deliberately stays
|
These APIs preserve triangle order, batch ranges and packed UV decoding. The
|
||||||
on its XY diagnostic projection until a runtime supplies a real legacy camera;
|
current game preview deliberately stays on its XY diagnostic projection until a
|
||||||
selecting source-world coordinates with identity camera would be a misleading
|
runtime supplies a real legacy camera; selecting source-world coordinates with
|
||||||
empty/off-screen viewer rather than a compatibility result.
|
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
|
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
|
`0x36610`: it evaluates a row-major affine `Rz(z) * Ry(y) * Rx(x)` and writes
|
||||||
|
|||||||
Reference in New Issue
Block a user