feat(render): consume world snapshot transforms

This commit is contained in:
2026-07-18 19:30:27 +04:00
parent 77d38e4c12
commit a01c21f865
2 changed files with 99 additions and 7 deletions
+88 -3
View File
@@ -54,7 +54,7 @@ use fparkan_terrain::TerrainWorld;
use fparkan_vfs::DirectoryVfs;
#[cfg(test)]
use fparkan_world::OriginalObjectId;
use fparkan_world::WorldSnapshot;
use fparkan_world::{TransformState, WorldSnapshot};
use serde::Deserialize;
use std::num::NonZeroUsize;
use std::path::PathBuf;
@@ -1024,12 +1024,14 @@ fn render_snapshot_with_assets(
material_slots: vec![material],
material_index: 0,
pipeline_state: fparkan_render::LegacyPipelineState::default(),
transform: mission_drafts
transform: snapshot_transform(snapshot, *handle).unwrap_or_else(|| {
mission_drafts
.and_then(|drafts| drafts.get(index))
.map_or_else(
|| identity_transform(index_to_f32(index)),
mission_position_scale_transform,
),
)
}),
range: IndexRange { start: 0, count: 3 },
stable_order,
});
@@ -1041,6 +1043,40 @@ fn render_snapshot_with_assets(
}
}
fn snapshot_transform(
snapshot: &WorldSnapshot,
handle: fparkan_world::ObjectHandle,
) -> Option<[f32; 16]> {
snapshot
.transforms
.iter()
.find_map(|(candidate, transform)| (*candidate == handle).then_some(*transform))
.map(transform_state_position_scale_transform)
}
fn transform_state_position_scale_transform(transform: TransformState) -> [f32; 16] {
let position = transform.position.map(f32::from_bits);
let scale = transform.scale.map(f32::from_bits);
[
scale[0],
0.0,
0.0,
position[0],
0.0,
scale[1],
0.0,
position[1],
0.0,
0.0,
scale[2],
position[2],
0.0,
0.0,
0.0,
1.0,
]
}
fn mission_position_scale_transform(draft: &MissionObjectDraft) -> [f32; 16] {
[
draft.scale[0],
@@ -1781,6 +1817,55 @@ mod tests {
);
}
#[test]
fn render_snapshot_prefers_world_transform_over_mission_draft() -> Result<(), String> {
let handle = ObjectHandle {
generation: 1,
slot: 0,
};
let snapshot = WorldSnapshot {
tick: Tick(1),
objects: vec![handle],
transforms: vec![(
handle,
TransformState {
position: [
100.0_f32.to_bits(),
200.0_f32.to_bits(),
300.0_f32.to_bits(),
],
orientation: [0; 3],
scale: [4.0_f32.to_bits(), 5.0_f32.to_bits(), 6.0_f32.to_bits()],
},
)],
events: Vec::new(),
hash: StateHash([0; 32]),
};
let drafts = vec![MissionObjectDraft {
original_id: None,
resource_name_raw: Vec::new(),
identity_or_clan_raw: 0,
position: [1.0; 3],
orientation_raw: [0.0; 3],
scale: [1.0; 3],
visual_ids: Vec::new(),
properties: Vec::new(),
}];
let commands = render_snapshot_commands_with_assets(&snapshot, None, Some(&drafts))?;
let RenderCommand::Draw(draw) = &commands.commands[1] else {
return Err("expected draw".to_string());
};
assert_eq!(
draw.transform,
[
4.0, 0.0, 0.0, 100.0, 0.0, 5.0, 0.0, 200.0, 0.0, 0.0, 6.0, 300.0, 0.0, 0.0, 0.0,
1.0
]
);
Ok(())
}
#[test]
fn render_snapshot_preserves_mission_original_object_id() -> Result<(), String> {
let snapshot = WorldSnapshot {
+7
View File
@@ -390,6 +390,13 @@ deterministic ticks. This is the state foundation for a future route/movement
controller; it does not claim recovered velocity, collision or original
behavior-controller semantics.
The ordinary planning renderer now consumes this snapshot state before falling
back to a mission draft. Its current transform bridge applies the preserved
position and non-uniform scale only; raw orientation remains uninterpreted in
this backend-neutral path. A GOG AutoDemo planning run on 2026-07-18 completed
two ticks with eight objects, 66 draws and state hash
`a54855a4f47ffa380911228f295dd49a9a7b88d6ff271a23db48ba318b1fbbb4`.
Записывается начальная миссия, seed, input events, network messages и значения
внешних часов. На контрольных ticks сохраняется canonical state hash: