feat(render): consume world snapshot transforms
This commit is contained in:
@@ -54,7 +54,7 @@ use fparkan_terrain::TerrainWorld;
|
|||||||
use fparkan_vfs::DirectoryVfs;
|
use fparkan_vfs::DirectoryVfs;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use fparkan_world::OriginalObjectId;
|
use fparkan_world::OriginalObjectId;
|
||||||
use fparkan_world::WorldSnapshot;
|
use fparkan_world::{TransformState, WorldSnapshot};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -1024,12 +1024,14 @@ fn render_snapshot_with_assets(
|
|||||||
material_slots: vec![material],
|
material_slots: vec![material],
|
||||||
material_index: 0,
|
material_index: 0,
|
||||||
pipeline_state: fparkan_render::LegacyPipelineState::default(),
|
pipeline_state: fparkan_render::LegacyPipelineState::default(),
|
||||||
transform: mission_drafts
|
transform: snapshot_transform(snapshot, *handle).unwrap_or_else(|| {
|
||||||
.and_then(|drafts| drafts.get(index))
|
mission_drafts
|
||||||
.map_or_else(
|
.and_then(|drafts| drafts.get(index))
|
||||||
|| identity_transform(index_to_f32(index)),
|
.map_or_else(
|
||||||
mission_position_scale_transform,
|
|| identity_transform(index_to_f32(index)),
|
||||||
),
|
mission_position_scale_transform,
|
||||||
|
)
|
||||||
|
}),
|
||||||
range: IndexRange { start: 0, count: 3 },
|
range: IndexRange { start: 0, count: 3 },
|
||||||
stable_order,
|
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] {
|
fn mission_position_scale_transform(draft: &MissionObjectDraft) -> [f32; 16] {
|
||||||
[
|
[
|
||||||
draft.scale[0],
|
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]
|
#[test]
|
||||||
fn render_snapshot_preserves_mission_original_object_id() -> Result<(), String> {
|
fn render_snapshot_preserves_mission_original_object_id() -> Result<(), String> {
|
||||||
let snapshot = WorldSnapshot {
|
let snapshot = WorldSnapshot {
|
||||||
|
|||||||
@@ -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
|
controller; it does not claim recovered velocity, collision or original
|
||||||
behavior-controller semantics.
|
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 и значения
|
Записывается начальная миссия, seed, input events, network messages и значения
|
||||||
внешних часов. На контрольных ticks сохраняется canonical state hash:
|
внешних часов. На контрольных ticks сохраняется canonical state hash:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user