feat(render): apply recovered TMA orientation
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
use crate::{VulkanStaticDrawRange, VulkanStaticMesh, VulkanStaticVertex};
|
||||
use fparkan_msh::ModelAsset;
|
||||
use fparkan_render::LegacyPipelineState;
|
||||
use fparkan_render::{LegacyIron3dEulerTransform, LegacyPipelineState};
|
||||
use fparkan_terrain_format::LandMeshDocument;
|
||||
|
||||
/// Error returned when a validated MSH cannot enter the current static GPU path.
|
||||
@@ -185,8 +185,36 @@ pub fn project_msh_to_static_mesh_in_world_space(
|
||||
translation: [f32; 3],
|
||||
scale: [f32; 3],
|
||||
) -> Result<VulkanStaticMesh, VulkanAssetMeshError> {
|
||||
if !translation
|
||||
project_msh_to_static_mesh_in_world_space_with_transform(
|
||||
model,
|
||||
LegacyIron3dEulerTransform {
|
||||
translation,
|
||||
orientation_radians: [0.0; 3],
|
||||
},
|
||||
scale,
|
||||
)
|
||||
}
|
||||
|
||||
/// Projects MSH geometry using the recovered `Iron3D` placement transform.
|
||||
///
|
||||
/// This preserves source-world coordinates and applies the proven
|
||||
/// `Rz(z) * Ry(y) * Rx(x)` rotation after local component-wise scale. It is
|
||||
/// intended for the opt-in legacy-camera static preview, not yet for animated
|
||||
/// or gameplay-owned transforms.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`VulkanAssetMeshError`] when geometry or transform values cannot
|
||||
/// be represented by the static Vulkan input contract.
|
||||
pub fn project_msh_to_static_mesh_in_world_space_with_transform(
|
||||
model: &ModelAsset,
|
||||
transform: LegacyIron3dEulerTransform,
|
||||
scale: [f32; 3],
|
||||
) -> Result<VulkanStaticMesh, VulkanAssetMeshError> {
|
||||
if !transform
|
||||
.translation
|
||||
.iter()
|
||||
.chain(transform.orientation_radians.iter())
|
||||
.chain(scale.iter())
|
||||
.all(|value| value.is_finite())
|
||||
{
|
||||
@@ -201,11 +229,9 @@ pub fn project_msh_to_static_mesh_in_world_space(
|
||||
if !position.iter().all(|value| value.is_finite()) {
|
||||
return Err(VulkanAssetMeshError::NonFinitePosition);
|
||||
}
|
||||
let position = [
|
||||
position[0] * scale[0] + translation[0],
|
||||
position[1] * scale[1] + translation[1],
|
||||
position[2] * scale[2] + translation[2],
|
||||
];
|
||||
let position = transform
|
||||
.try_transform_scaled_point(*position, scale)
|
||||
.ok_or(VulkanAssetMeshError::NonFinitePosition)?;
|
||||
Ok(VulkanStaticVertex {
|
||||
position,
|
||||
color: [0.82, 0.72, 0.31],
|
||||
@@ -552,6 +578,29 @@ mod tests {
|
||||
assert_eq!(mesh.vertices[0].uv, [1.0, -0.5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn world_space_msh_applies_recovered_iron3d_orientation_after_scale() {
|
||||
let source = model(
|
||||
vec![[2.0, 3.0, 4.0], [0.0, 0.0, 0.0], [1.0, 0.0, 0.0]],
|
||||
vec![0, 1, 2],
|
||||
vec![batch(0, 3, 0)],
|
||||
);
|
||||
|
||||
let mesh = project_msh_to_static_mesh_in_world_space_with_transform(
|
||||
&source,
|
||||
LegacyIron3dEulerTransform {
|
||||
translation: [10.0, 20.0, 30.0],
|
||||
orientation_radians: [0.0, 0.0, std::f32::consts::FRAC_PI_2],
|
||||
},
|
||||
[2.0, 1.0, 0.5],
|
||||
)
|
||||
.expect("finite recovered transform");
|
||||
|
||||
assert_eq!(mesh.vertices[0].position, [7.0, 24.0, 32.0]);
|
||||
assert_eq!(mesh.vertices[1].position, [10.0, 20.0, 30.0]);
|
||||
assert_eq!(mesh.vertices[2].position, [10.0, 22.0, 30.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retains_each_source_batch_material_selector() {
|
||||
let mut second = batch(3, 3, 0);
|
||||
|
||||
@@ -42,8 +42,9 @@ mod validation;
|
||||
pub use self::asset_mesh::{
|
||||
project_land_msh_to_static_mesh, project_land_msh_to_static_mesh_in_world_space,
|
||||
project_land_msh_to_static_mesh_in_xy_frame, project_msh_to_static_mesh,
|
||||
project_msh_to_static_mesh_in_world_space, project_msh_to_static_mesh_in_xy_frame,
|
||||
VulkanAssetMeshError, VulkanStaticXyFrame,
|
||||
project_msh_to_static_mesh_in_world_space,
|
||||
project_msh_to_static_mesh_in_world_space_with_transform,
|
||||
project_msh_to_static_mesh_in_xy_frame, VulkanAssetMeshError, VulkanStaticXyFrame,
|
||||
};
|
||||
pub use self::capabilities::{
|
||||
probe_vulkan_runtime_capabilities, probe_vulkan_runtime_capabilities_for_request,
|
||||
|
||||
@@ -26,15 +26,16 @@ use fparkan_platform::WindowPort;
|
||||
use fparkan_platform_winit::{window_native_handles, WinitWindow, WinitWindowPlan};
|
||||
use fparkan_render::{
|
||||
build_commands, CameraSnapshot, DrawId, GpuMaterialId, GpuMeshId, IndexRange,
|
||||
LegacyD3d7Projection, RawCameraTransform, RenderBackend, RenderCommand, RenderCommandList,
|
||||
RenderPhase, RenderProfile, RenderSnapshot, RenderSnapshotDraw,
|
||||
LegacyD3d7Projection, LegacyIron3dEulerTransform, RawCameraTransform, RenderBackend,
|
||||
RenderCommand, RenderCommandList, RenderPhase, RenderProfile, RenderSnapshot,
|
||||
RenderSnapshotDraw,
|
||||
};
|
||||
use fparkan_render_vulkan::{
|
||||
project_land_msh_to_static_mesh_in_world_space, project_land_msh_to_static_mesh_in_xy_frame,
|
||||
project_msh_to_static_mesh_in_world_space, project_msh_to_static_mesh_in_xy_frame,
|
||||
VulkanPlanningBackend, VulkanSmokeFrameOutcome, VulkanSmokeRenderer,
|
||||
VulkanSmokeRendererCreateInfo, VulkanStaticCamera, VulkanStaticMaterial, VulkanStaticMesh,
|
||||
VulkanStaticTexture, VulkanStaticXyFrame,
|
||||
project_msh_to_static_mesh_in_world_space_with_transform,
|
||||
project_msh_to_static_mesh_in_xy_frame, 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,
|
||||
@@ -285,9 +286,12 @@ fn static_preview_mesh_and_materials(
|
||||
format!("static preview visual {visual_id:?} references unknown model {model_id:?}")
|
||||
})?;
|
||||
let component = if legacy_camera.is_some() {
|
||||
project_msh_to_static_mesh_in_world_space(
|
||||
project_msh_to_static_mesh_in_world_space_with_transform(
|
||||
&model.validated,
|
||||
root.position,
|
||||
LegacyIron3dEulerTransform {
|
||||
translation: root.position,
|
||||
orientation_radians: root.orientation_raw,
|
||||
},
|
||||
root.scale,
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -304,6 +304,37 @@ impl LegacyIron3dEulerTransform {
|
||||
.all(|value| value.is_finite())
|
||||
.then_some(matrix)
|
||||
}
|
||||
|
||||
/// Applies the recovered rotation after a component-wise local scale.
|
||||
///
|
||||
/// This is the affine `R * (scale * point) + translation` convention used
|
||||
/// by the static source-world bridge. The original dynamic transform path
|
||||
/// needs its own capture evidence before it may replace this static path.
|
||||
#[must_use]
|
||||
pub fn try_transform_scaled_point(self, point: [f32; 3], scale: [f32; 3]) -> Option<[f32; 3]> {
|
||||
if !point
|
||||
.iter()
|
||||
.chain(scale.iter())
|
||||
.all(|value| value.is_finite())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let matrix = self.try_row_major()?;
|
||||
let scaled = [
|
||||
point[0] * scale[0],
|
||||
point[1] * scale[1],
|
||||
point[2] * scale[2],
|
||||
];
|
||||
let transformed = [
|
||||
matrix[0] * scaled[0] + matrix[1] * scaled[1] + matrix[2] * scaled[2] + matrix[3],
|
||||
matrix[4] * scaled[0] + matrix[5] * scaled[1] + matrix[6] * scaled[2] + matrix[7],
|
||||
matrix[8] * scaled[0] + matrix[9] * scaled[1] + matrix[10] * scaled[2] + matrix[11],
|
||||
];
|
||||
transformed
|
||||
.iter()
|
||||
.all(|value| value.is_finite())
|
||||
.then_some(transformed)
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw camera state observed through the original Terrain camera interface.
|
||||
@@ -1210,6 +1241,14 @@ mod tests {
|
||||
assert_eq!(matrix[7], 717.433);
|
||||
assert_eq!(matrix[11], 3.040_938_9);
|
||||
assert_eq!(matrix[15], 1.0);
|
||||
assert_eq!(
|
||||
LegacyIron3dEulerTransform {
|
||||
translation: [10.0, 20.0, 30.0],
|
||||
orientation_radians: [0.0, 0.0, std::f32::consts::FRAC_PI_2],
|
||||
}
|
||||
.try_transform_scaled_point([2.0, 3.0, 4.0], [2.0, 1.0, 0.5]),
|
||||
Some([7.0, 24.0, 32.0])
|
||||
);
|
||||
assert_eq!(
|
||||
LegacyIron3dEulerTransform {
|
||||
translation: [0.0, 0.0, 0.0],
|
||||
|
||||
+20
-13
@@ -1437,16 +1437,20 @@ 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` now recovers an exact adjacent placement
|
||||
primitive: RVA `0x36610` evaluates a row-major affine
|
||||
`Rz(z) * Ry(y) * Rx(x)` and writes translation in the final column. The
|
||||
backend-neutral `LegacyIron3dEulerTransform` preserves that finite portable
|
||||
formula with a golden yaw-vector test. This is deliberately not wired to TMA:
|
||||
AutoDemo records strongly suggest the candidate `(0, 0, z)` radians (all eight
|
||||
objects have zero first two values and `z` in approximately `[-pi, pi]`), but
|
||||
the current static trace has not proved that those raw record fields reach this
|
||||
builder unchanged for every mission object category. The remaining link needs
|
||||
a direct loader-to-builder dataflow or a dynamic placement capture.
|
||||
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
|
||||
translation in the final column. The backend-neutral
|
||||
`LegacyIron3dEulerTransform` preserves that finite portable formula with golden
|
||||
yaw and scaled-point tests. A read-only elevated AutoDemo scan closes the
|
||||
previous TMA binding gap for static placement: it finds the exact raw mission
|
||||
records (position, `(0, 0, z)`, scale) and live object blocks at the same
|
||||
world positions. For example, `z = 1.6262363` produces the observed pair
|
||||
`-sin(z) = -0.99846`, `cos(z) = -0.05541`; matching pairs occur for the other
|
||||
observed objects. The opt-in captured-camera path therefore applies
|
||||
`R * (scale * local_position) + translation` before Vulkan upload. It remains
|
||||
a static-placement contract: dynamic/animated transforms, physics ownership,
|
||||
terrain material selection, lighting and gameplay parity are still separate
|
||||
evidence tasks.
|
||||
|
||||
`VulkanSmokeRenderer::set_camera` now accepts a new finite camera between frame
|
||||
submissions and uses it for the next command buffer without rebuilding the
|
||||
@@ -1494,9 +1498,12 @@ indices remain decoded in their original 16-bit representations and are widened
|
||||
only at the static-render bridge. A regression test crosses the former boundary.
|
||||
The full captured-camera AutoDemo preview completed at 13.369 seconds and
|
||||
rendered three 1280×720 native frames: 8 mission objects, 66 mesh components,
|
||||
67 material descriptors, and zero Vulkan validation warnings or errors. It is
|
||||
still a static source-world preview: raw mission orientation, terrain material
|
||||
selection, lighting and animation have not yet been claimed as parity.
|
||||
67 material descriptors, and zero Vulkan validation warnings or errors. A later
|
||||
orientation-enabled run rendered the same full scene for three 1280×720 frames
|
||||
with `validation_warnings=0`, `validation_errors=0` and readback hash
|
||||
`6406107678925513509`. It is still a static source-world preview: terrain
|
||||
material selection, lighting, animation, physics and gameplay behavior have
|
||||
not yet been claimed as parity.
|
||||
|
||||
A fresh no-input launch of the canonical `iron_3d.exe` did create a responsive
|
||||
window titled `Parkan. Железная Стратегия`. A read-only probe then requested
|
||||
|
||||
Reference in New Issue
Block a user