Update controlled component geometry and mounts
Docs Deploy / Build and Deploy MkDocs (push) Successful in 33s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 33s
This commit is contained in:
2 files changed
+764
-20
No files matched your search
@@ -32,7 +32,7 @@ use fparkan_msh::{
|
|||||||
};
|
};
|
||||||
use fparkan_path::ResourceName;
|
use fparkan_path::ResourceName;
|
||||||
use fparkan_platform_winit::{window_native_handles, WinitWindow, WinitWindowPlan};
|
use fparkan_platform_winit::{window_native_handles, WinitWindow, WinitWindowPlan};
|
||||||
use fparkan_prototype::{unit_component_tree, PROTOTYPE_TYPE_EXTO};
|
use fparkan_prototype::{unit_component_tree, UnitComponentTreeNode, PROTOTYPE_TYPE_EXTO};
|
||||||
use fparkan_render::{
|
use fparkan_render::{
|
||||||
LegacyBlendMode, LegacyCullMode, LegacyD3d7Projection, LegacyDepthMode,
|
LegacyBlendMode, LegacyCullMode, LegacyD3d7Projection, LegacyDepthMode,
|
||||||
LegacyIron3dEulerTransform, LegacyPipelineState, RawCameraTransform,
|
LegacyIron3dEulerTransform, LegacyPipelineState, RawCameraTransform,
|
||||||
@@ -1588,6 +1588,49 @@ fn static_mesh_shadow_triangles(mesh: &VulkanStaticMesh) -> Vec<ShadowTriangle>
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn expand_shadow_sphere_to_triangles(
|
||||||
|
sphere: &mut ShadowSphere,
|
||||||
|
triangles: &[ShadowTriangle],
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
if !sphere.radius.is_finite()
|
||||||
|
|| sphere.radius < 0.0
|
||||||
|
|| !sphere.center.iter().all(|value| value.is_finite())
|
||||||
|
{
|
||||||
|
return Err("animated shadow caster has an invalid bounding sphere".to_string());
|
||||||
|
}
|
||||||
|
let mut changed = false;
|
||||||
|
for point in triangles.iter().flat_map(|triangle| triangle.positions) {
|
||||||
|
if !point.iter().all(|value| value.is_finite()) {
|
||||||
|
return Err("animated shadow triangle has a non-finite position".to_string());
|
||||||
|
}
|
||||||
|
let offset = sub3(point, sphere.center);
|
||||||
|
let distance = length3(offset)
|
||||||
|
.ok_or_else(|| "animated shadow point distance is non-finite".to_string())?;
|
||||||
|
if distance <= sphere.radius {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let next_radius = 0.5 * (sphere.radius + distance);
|
||||||
|
let center_shift = if distance > f32::EPSILON {
|
||||||
|
scale3(offset, (next_radius - sphere.radius) / distance)
|
||||||
|
} else {
|
||||||
|
[0.0; 3]
|
||||||
|
};
|
||||||
|
sphere.center = add3(sphere.center, center_shift);
|
||||||
|
sphere.radius = next_radius;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
Ok(changed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shadow_sphere_enclosing_triangles(
|
||||||
|
baseline: ShadowSphere,
|
||||||
|
triangles: &[ShadowTriangle],
|
||||||
|
) -> Result<ShadowSphere, String> {
|
||||||
|
let mut sphere = baseline;
|
||||||
|
expand_shadow_sphere_to_triangles(&mut sphere, triangles)?;
|
||||||
|
Ok(sphere)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
struct ShadowComponentSphere {
|
struct ShadowComponentSphere {
|
||||||
center: [f32; 3],
|
center: [f32; 3],
|
||||||
@@ -3781,9 +3824,16 @@ struct StaticPreviewScene {
|
|||||||
material_animations: Vec<MaterialAnimationBinding>,
|
material_animations: Vec<MaterialAnimationBinding>,
|
||||||
sun_occlusion_range_indices: Vec<usize>,
|
sun_occlusion_range_indices: Vec<usize>,
|
||||||
shadow_casters: Vec<ShadowCaster>,
|
shadow_casters: Vec<ShadowCaster>,
|
||||||
|
shadow_sphere_baselines: Vec<ShadowSphere>,
|
||||||
shadow_receivers: Vec<ShadowTriangle>,
|
shadow_receivers: Vec<ShadowTriangle>,
|
||||||
shadow_material_index: usize,
|
shadow_material_index: usize,
|
||||||
terrain: Arc<TerrainWorld>,
|
terrain: Arc<TerrainWorld>,
|
||||||
|
component_pose_buffers: Vec<Vec<Option<NodePoseBuffer>>>,
|
||||||
|
component_mount_poses: Vec<Vec<Option<VulkanNodePose>>>,
|
||||||
|
control_animation_instances: Vec<ControlAnimationInstance>,
|
||||||
|
dynamic_component_geometry: Vec<DynamicComponentGeometry>,
|
||||||
|
dynamic_component_mounts: Vec<DynamicComponentMount>,
|
||||||
|
shadow_caster_indices_by_object: Vec<Option<usize>>,
|
||||||
camera: VulkanStaticCamera,
|
camera: VulkanStaticCamera,
|
||||||
view_camera: PreviewCameraFrame,
|
view_camera: PreviewCameraFrame,
|
||||||
captured_viewport: Option<CapturedViewport>,
|
captured_viewport: Option<CapturedViewport>,
|
||||||
@@ -3793,6 +3843,35 @@ struct StaticPreviewScene {
|
|||||||
terrain_components: usize,
|
terrain_components: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct ControlAnimationInstance {
|
||||||
|
object_index: usize,
|
||||||
|
component_index: usize,
|
||||||
|
model: Arc<ModelAsset>,
|
||||||
|
control: PreparedComponentControl,
|
||||||
|
state: ControlInstanceState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct DynamicComponentGeometry {
|
||||||
|
object_index: usize,
|
||||||
|
component_index: usize,
|
||||||
|
model: Arc<ModelAsset>,
|
||||||
|
transform: LegacyIron3dEulerTransform,
|
||||||
|
scale: [f32; 3],
|
||||||
|
vertex_start: usize,
|
||||||
|
vertex_count: usize,
|
||||||
|
shadow_triangle_range: Option<std::ops::Range<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
struct DynamicComponentMount {
|
||||||
|
object_index: usize,
|
||||||
|
component_index: usize,
|
||||||
|
parent_index: usize,
|
||||||
|
socket: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
struct CapturedViewport {
|
struct CapturedViewport {
|
||||||
/// Original D3D7 viewport rectangle `(left, top, right, bottom)`. The
|
/// Original D3D7 viewport rectangle `(left, top, right, bottom)`. The
|
||||||
@@ -3998,6 +4077,90 @@ fn initial_control_gate_values(control: &PreparedComponentControl) -> Vec<Option
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn control_has_tag21(control: &PreparedComponentControl) -> bool {
|
||||||
|
control.typed_records.iter().any(|record| record.tag == 21)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn component_chain_has_tag21(
|
||||||
|
tree: Option<&[UnitComponentTreeNode]>,
|
||||||
|
controls: &[Option<PreparedComponentControl>],
|
||||||
|
component_index: usize,
|
||||||
|
) -> bool {
|
||||||
|
let Some(tree) = tree else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let mut current = Some(component_index);
|
||||||
|
for _ in 0..=tree.len() {
|
||||||
|
let Some(index) = current else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if controls
|
||||||
|
.get(index)
|
||||||
|
.and_then(Option::as_ref)
|
||||||
|
.is_some_and(control_has_tag21)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
current = tree.get(index).and_then(|node| node.parent_index);
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_dynamic_component_mounts(
|
||||||
|
mounts: &[DynamicComponentMount],
|
||||||
|
pose_buffers: &[Vec<Option<NodePoseBuffer>>],
|
||||||
|
mount_poses: &mut [Vec<Option<VulkanNodePose>>],
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
let mut changed = false;
|
||||||
|
for mount in mounts {
|
||||||
|
let parent_mounted = mount_poses
|
||||||
|
.get(mount.object_index)
|
||||||
|
.and_then(|poses| poses.get(mount.parent_index))
|
||||||
|
.copied()
|
||||||
|
.flatten();
|
||||||
|
let parent_hierarchy = pose_buffers
|
||||||
|
.get(mount.object_index)
|
||||||
|
.and_then(|buffers| buffers.get(mount.parent_index))
|
||||||
|
.and_then(Option::as_ref)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"dynamic component {} parent {} has no sampled Node38 hierarchy",
|
||||||
|
mount.component_index, mount.parent_index
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let socket_pose = if parent_mounted.is_some() {
|
||||||
|
node38_pose_relative_to_root_from_hierarchy(parent_hierarchy, mount.socket)
|
||||||
|
} else {
|
||||||
|
node38_pose_from_hierarchy(parent_hierarchy, mount.socket)
|
||||||
|
}
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"dynamic component {} mount socket {} is absent from parent {}",
|
||||||
|
mount.component_index, mount.socket, mount.parent_index
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let root_pose = parent_mounted.unwrap_or(VulkanNodePose {
|
||||||
|
translation: [0.0; 3],
|
||||||
|
rotation: [0.0, 0.0, 0.0, 1.0],
|
||||||
|
});
|
||||||
|
let next_pose = compose_unit_pose(root_pose, socket_pose);
|
||||||
|
let slot = mount_poses
|
||||||
|
.get_mut(mount.object_index)
|
||||||
|
.and_then(|poses| poses.get_mut(mount.component_index))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"dynamic component {} mount slot is outside object {}",
|
||||||
|
mount.component_index, mount.object_index
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if *slot != Some(next_pose) {
|
||||||
|
*slot = Some(next_pose);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(changed)
|
||||||
|
}
|
||||||
|
|
||||||
fn static_preview_control_pose_buffer(
|
fn static_preview_control_pose_buffer(
|
||||||
model: &ModelAsset,
|
model: &ModelAsset,
|
||||||
control: &PreparedComponentControl,
|
control: &PreparedComponentControl,
|
||||||
@@ -4045,6 +4208,35 @@ fn static_preview_control_pose_buffer(
|
|||||||
.or_else(|| node38_fallback_hierarchy(model)))
|
.or_else(|| node38_fallback_hierarchy(model)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn project_dynamic_component_geometry(
|
||||||
|
dynamic: &DynamicComponentGeometry,
|
||||||
|
pose: &NodePoseBuffer,
|
||||||
|
mount: Option<VulkanNodePose>,
|
||||||
|
) -> Result<VulkanStaticMesh, String> {
|
||||||
|
let projected = if let Some(mount) = mount {
|
||||||
|
project_msh_to_static_mesh_in_world_space_with_node_pose_buffer_and_mount(
|
||||||
|
&dynamic.model,
|
||||||
|
dynamic.transform,
|
||||||
|
dynamic.scale,
|
||||||
|
pose,
|
||||||
|
mount,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
project_msh_to_static_mesh_in_world_space_with_node_pose_buffer(
|
||||||
|
&dynamic.model,
|
||||||
|
dynamic.transform,
|
||||||
|
dynamic.scale,
|
||||||
|
pose,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
projected.map_err(|error| {
|
||||||
|
format!(
|
||||||
|
"update mission object {} component {} geometry: {error}",
|
||||||
|
dynamic.object_index, dynamic.component_index
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn static_preview_mesh_and_materials(
|
fn static_preview_mesh_and_materials(
|
||||||
assets: &MissionAssets,
|
assets: &MissionAssets,
|
||||||
terrain: &TerrainWorld,
|
terrain: &TerrainWorld,
|
||||||
@@ -4068,6 +4260,13 @@ fn static_preview_mesh_and_materials(
|
|||||||
let mut material_animations = Vec::new();
|
let mut material_animations = Vec::new();
|
||||||
let mut sun_occlusion_range_indices = Vec::new();
|
let mut sun_occlusion_range_indices = Vec::new();
|
||||||
let mut shadow_casters = Vec::new();
|
let mut shadow_casters = Vec::new();
|
||||||
|
let mut shadow_sphere_baselines = Vec::new();
|
||||||
|
let mut shadow_caster_indices_by_object = Vec::with_capacity(roots.len());
|
||||||
|
let mut component_pose_buffers_by_object = Vec::with_capacity(roots.len());
|
||||||
|
let mut component_mount_poses_by_object = Vec::with_capacity(roots.len());
|
||||||
|
let mut control_animation_instances = Vec::new();
|
||||||
|
let mut dynamic_component_geometry = Vec::new();
|
||||||
|
let mut dynamic_component_mounts = Vec::new();
|
||||||
let shadow_receivers = terrain_shadow_receivers(terrain_mesh)?;
|
let shadow_receivers = terrain_shadow_receivers(terrain_mesh)?;
|
||||||
let mut terrain_material_loader = StandaloneWearMaterialLoader::new(root);
|
let mut terrain_material_loader = StandaloneWearMaterialLoader::new(root);
|
||||||
let terrain_component = project_land_msh_to_static_mesh_in_world_space(terrain_mesh)
|
let terrain_component = project_land_msh_to_static_mesh_in_world_space(terrain_mesh)
|
||||||
@@ -4136,6 +4335,7 @@ fn static_preview_mesh_and_materials(
|
|||||||
component_controls.map_or(0, Vec::len)
|
component_controls.map_or(0, Vec::len)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
let component_controls = component_controls.map_or(&[][..], Vec::as_slice);
|
||||||
let root_native_kind = component_refs
|
let root_native_kind = component_refs
|
||||||
.first()
|
.first()
|
||||||
.and_then(|(visual, _)| native_shadow_kind(visual.source_type));
|
.and_then(|(visual, _)| native_shadow_kind(visual.source_type));
|
||||||
@@ -4153,15 +4353,25 @@ fn static_preview_mesh_and_materials(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let component_control = component_controls
|
let component_control = component_controls
|
||||||
.and_then(|controls| controls.get(component_index))
|
.get(component_index)
|
||||||
.and_then(Option::as_ref);
|
.and_then(Option::as_ref);
|
||||||
|
let control_animates_component = component_control.is_some_and(control_has_tag21);
|
||||||
|
let timeline_moves_component = static_animation_frame.is_none()
|
||||||
|
&& (control_animates_component
|
||||||
|
|| component_chain_has_tag21(
|
||||||
|
tree.as_deref(),
|
||||||
|
component_controls,
|
||||||
|
component_index,
|
||||||
|
));
|
||||||
|
let runtime_model = timeline_moves_component.then(|| Arc::new(model.validated.clone()));
|
||||||
|
let mut control_state = ControlInstanceState::default();
|
||||||
component_pose_buffers[component_index] =
|
component_pose_buffers[component_index] =
|
||||||
match (static_animation_frame, component_control) {
|
match (static_animation_frame, component_control) {
|
||||||
(None, Some(control)) => static_preview_control_pose_buffer(
|
(None, Some(control)) => static_preview_control_pose_buffer(
|
||||||
&model.validated,
|
&model.validated,
|
||||||
control,
|
control,
|
||||||
0,
|
0,
|
||||||
&mut ControlInstanceState::default(),
|
&mut control_state,
|
||||||
)?,
|
)?,
|
||||||
(animation_frame, control) => static_preview_component_pose_buffer(
|
(animation_frame, control) => static_preview_component_pose_buffer(
|
||||||
&model.validated,
|
&model.validated,
|
||||||
@@ -4169,6 +4379,20 @@ fn static_preview_mesh_and_materials(
|
|||||||
animation_frame,
|
animation_frame,
|
||||||
)?,
|
)?,
|
||||||
};
|
};
|
||||||
|
if control_animates_component && static_animation_frame.is_none() {
|
||||||
|
control_animation_instances.push(ControlAnimationInstance {
|
||||||
|
object_index,
|
||||||
|
component_index,
|
||||||
|
model: runtime_model
|
||||||
|
.as_ref()
|
||||||
|
.map(Arc::clone)
|
||||||
|
.ok_or_else(|| "animated component model was not retained".to_string())?,
|
||||||
|
control: component_control
|
||||||
|
.ok_or_else(|| "tag-21 component control disappeared".to_string())?
|
||||||
|
.clone(),
|
||||||
|
state: control_state,
|
||||||
|
});
|
||||||
|
}
|
||||||
let should_render = tree.as_ref().map_or(true, |_| {
|
let should_render = tree.as_ref().map_or(true, |_| {
|
||||||
unit_component_is_visible(component_index, visual.source_type)
|
unit_component_is_visible(component_index, visual.source_type)
|
||||||
});
|
});
|
||||||
@@ -4263,6 +4487,31 @@ fn static_preview_mesh_and_materials(
|
|||||||
if let Some(mount_pose) = mount_pose {
|
if let Some(mount_pose) = mount_pose {
|
||||||
component_mount_poses[component_index] = Some(mount_pose);
|
component_mount_poses[component_index] = Some(mount_pose);
|
||||||
}
|
}
|
||||||
|
if timeline_moves_component && mount_pose.is_some() {
|
||||||
|
let parent_index = tree
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|tree| tree[component_index].parent_index)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!("mounted dynamic component {component_index} has no parent")
|
||||||
|
})?;
|
||||||
|
let socket = usize::try_from(
|
||||||
|
records
|
||||||
|
.get(component_index)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!("unit component {component_index} is outside decoded records")
|
||||||
|
})?
|
||||||
|
.parent_or_link,
|
||||||
|
)
|
||||||
|
.map_err(|_| {
|
||||||
|
format!("unit component {component_index} has a negative mount socket")
|
||||||
|
})?;
|
||||||
|
dynamic_component_mounts.push(DynamicComponentMount {
|
||||||
|
object_index,
|
||||||
|
component_index,
|
||||||
|
parent_index,
|
||||||
|
socket,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let transform = LegacyIron3dEulerTransform {
|
let transform = LegacyIron3dEulerTransform {
|
||||||
translation: mission_root.position,
|
translation: mission_root.position,
|
||||||
@@ -4328,6 +4577,11 @@ fn static_preview_mesh_and_materials(
|
|||||||
native_msh_cull_mode(range.batch_flags, component_handedness);
|
native_msh_cull_mode(range.batch_flags, component_handedness);
|
||||||
}
|
}
|
||||||
let component_shadow_triangles = static_mesh_shadow_triangles(&component);
|
let component_shadow_triangles = static_mesh_shadow_triangles(&component);
|
||||||
|
let shadow_triangle_range = root_native_kind.map(|_| {
|
||||||
|
let start = root_shadow_triangles.len();
|
||||||
|
let end = start.saturating_add(component_shadow_triangles.len());
|
||||||
|
start..end
|
||||||
|
});
|
||||||
if root_native_kind.is_some() {
|
if root_native_kind.is_some() {
|
||||||
root_shadow_triangles.extend(component_shadow_triangles);
|
root_shadow_triangles.extend(component_shadow_triangles);
|
||||||
let node1_pose = mount_pose.and_then(|_| match pose_buffer {
|
let node1_pose = mount_pose.and_then(|_| match pose_buffer {
|
||||||
@@ -4356,6 +4610,8 @@ fn static_preview_mesh_and_materials(
|
|||||||
&mut material_animations,
|
&mut material_animations,
|
||||||
)?;
|
)?;
|
||||||
let lightmap_textures = static_preview_lightmap_textures(assets, visual)?;
|
let lightmap_textures = static_preview_lightmap_textures(assets, visual)?;
|
||||||
|
let vertex_start = mesh.vertices.len();
|
||||||
|
let vertex_count = component.vertices.len();
|
||||||
let range_start = mesh.draw_ranges.len();
|
let range_start = mesh.draw_ranges.len();
|
||||||
append_static_preview_component_with_lightmaps(
|
append_static_preview_component_with_lightmaps(
|
||||||
&mut mesh,
|
&mut mesh,
|
||||||
@@ -4371,15 +4627,31 @@ fn static_preview_mesh_and_materials(
|
|||||||
if root_sun_eligible {
|
if root_sun_eligible {
|
||||||
sun_occlusion_range_indices.extend(range_start..mesh.draw_ranges.len());
|
sun_occlusion_range_indices.extend(range_start..mesh.draw_ranges.len());
|
||||||
}
|
}
|
||||||
|
if timeline_moves_component && vertex_count != 0 {
|
||||||
|
dynamic_component_geometry.push(DynamicComponentGeometry {
|
||||||
|
object_index,
|
||||||
|
component_index,
|
||||||
|
model: runtime_model.as_ref().map(Arc::clone).ok_or_else(|| {
|
||||||
|
"animated component model was not retained".to_string()
|
||||||
|
})?,
|
||||||
|
transform,
|
||||||
|
scale: mission_root.scale,
|
||||||
|
vertex_start,
|
||||||
|
vertex_count,
|
||||||
|
shadow_triangle_range,
|
||||||
|
});
|
||||||
|
}
|
||||||
mesh_components += 1;
|
mesh_components += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(native_kind) =
|
component_pose_buffers_by_object.push(component_pose_buffers);
|
||||||
|
component_mount_poses_by_object.push(component_mount_poses);
|
||||||
|
let shadow_caster_index = if let Some(native_kind) =
|
||||||
root_native_kind.filter(|kind| native_shadow_caster_enabled(*kind))
|
root_native_kind.filter(|kind| native_shadow_caster_enabled(*kind))
|
||||||
{
|
{
|
||||||
let caster_id = u32::try_from(shadow_casters.len())
|
let caster_id = u32::try_from(shadow_casters.len())
|
||||||
.map_err(|_| "shadow caster id exceeds u32".to_string())?;
|
.map_err(|_| "shadow caster id exceeds u32".to_string())?;
|
||||||
if let Some(caster) = aggregate_shadow_caster(
|
aggregate_shadow_caster(
|
||||||
caster_id,
|
caster_id,
|
||||||
native_kind,
|
native_kind,
|
||||||
LegacyIron3dEulerTransform {
|
LegacyIron3dEulerTransform {
|
||||||
@@ -4389,10 +4661,17 @@ fn static_preview_mesh_and_materials(
|
|||||||
mission_root.scale,
|
mission_root.scale,
|
||||||
root_shadow_triangles,
|
root_shadow_triangles,
|
||||||
&root_shadow_spheres,
|
&root_shadow_spheres,
|
||||||
) {
|
)
|
||||||
|
.map(|caster| {
|
||||||
|
let index = shadow_casters.len();
|
||||||
|
shadow_sphere_baselines.push(caster.sphere);
|
||||||
shadow_casters.push(caster);
|
shadow_casters.push(caster);
|
||||||
}
|
index
|
||||||
}
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
shadow_caster_indices_by_object.push(shadow_caster_index);
|
||||||
}
|
}
|
||||||
if mesh_components == 0 {
|
if mesh_components == 0 {
|
||||||
return Err("selected static preview roots have no mesh-backed visual".to_string());
|
return Err("selected static preview roots have no mesh-backed visual".to_string());
|
||||||
@@ -4422,9 +4701,16 @@ fn static_preview_mesh_and_materials(
|
|||||||
material_animations,
|
material_animations,
|
||||||
sun_occlusion_range_indices,
|
sun_occlusion_range_indices,
|
||||||
shadow_casters,
|
shadow_casters,
|
||||||
|
shadow_sphere_baselines,
|
||||||
shadow_receivers,
|
shadow_receivers,
|
||||||
shadow_material_index,
|
shadow_material_index,
|
||||||
terrain: Arc::new(terrain.clone()),
|
terrain: Arc::new(terrain.clone()),
|
||||||
|
component_pose_buffers: component_pose_buffers_by_object,
|
||||||
|
component_mount_poses: component_mount_poses_by_object,
|
||||||
|
control_animation_instances,
|
||||||
|
dynamic_component_geometry,
|
||||||
|
dynamic_component_mounts,
|
||||||
|
shadow_caster_indices_by_object,
|
||||||
camera,
|
camera,
|
||||||
view_camera,
|
view_camera,
|
||||||
captured_viewport,
|
captured_viewport,
|
||||||
@@ -5186,6 +5472,13 @@ struct StaticVulkanApp {
|
|||||||
material_animations: Vec<MaterialAnimationBinding>,
|
material_animations: Vec<MaterialAnimationBinding>,
|
||||||
sun_occlusion_range_indices: Vec<usize>,
|
sun_occlusion_range_indices: Vec<usize>,
|
||||||
terrain: Arc<TerrainWorld>,
|
terrain: Arc<TerrainWorld>,
|
||||||
|
component_pose_buffers: Vec<Vec<Option<NodePoseBuffer>>>,
|
||||||
|
component_mount_poses: Vec<Vec<Option<VulkanNodePose>>>,
|
||||||
|
control_animation_instances: Vec<ControlAnimationInstance>,
|
||||||
|
dynamic_component_geometry: Vec<DynamicComponentGeometry>,
|
||||||
|
dynamic_component_mounts: Vec<DynamicComponentMount>,
|
||||||
|
shadow_caster_indices_by_object: Vec<Option<usize>>,
|
||||||
|
shadow_sphere_baselines: Vec<ShadowSphere>,
|
||||||
world_range_indices: Vec<usize>,
|
world_range_indices: Vec<usize>,
|
||||||
world_range_index_counts: HashMap<usize, u32>,
|
world_range_index_counts: HashMap<usize, u32>,
|
||||||
camera: VulkanStaticCamera,
|
camera: VulkanStaticCamera,
|
||||||
@@ -5267,6 +5560,13 @@ impl StaticVulkanApp {
|
|||||||
material_animations: preview.material_animations,
|
material_animations: preview.material_animations,
|
||||||
sun_occlusion_range_indices: preview.sun_occlusion_range_indices,
|
sun_occlusion_range_indices: preview.sun_occlusion_range_indices,
|
||||||
terrain: preview.terrain,
|
terrain: preview.terrain,
|
||||||
|
component_pose_buffers: preview.component_pose_buffers,
|
||||||
|
component_mount_poses: preview.component_mount_poses,
|
||||||
|
control_animation_instances: preview.control_animation_instances,
|
||||||
|
dynamic_component_geometry: preview.dynamic_component_geometry,
|
||||||
|
dynamic_component_mounts: preview.dynamic_component_mounts,
|
||||||
|
shadow_caster_indices_by_object: preview.shadow_caster_indices_by_object,
|
||||||
|
shadow_sphere_baselines: preview.shadow_sphere_baselines,
|
||||||
world_range_indices,
|
world_range_indices,
|
||||||
world_range_index_counts,
|
world_range_index_counts,
|
||||||
camera: preview.camera,
|
camera: preview.camera,
|
||||||
@@ -5469,6 +5769,151 @@ impl StaticVulkanApp {
|
|||||||
self.material_time_ms = self.material_elapsed.as_millis() as u32;
|
self.material_time_ms = self.material_elapsed.as_millis() as u32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_controlled_component_geometry(&mut self) -> Result<(), String> {
|
||||||
|
let mut pose_changed = false;
|
||||||
|
for instance in &mut self.control_animation_instances {
|
||||||
|
let pose = static_preview_control_pose_buffer(
|
||||||
|
&instance.model,
|
||||||
|
&instance.control,
|
||||||
|
self.material_time_ms,
|
||||||
|
&mut instance.state,
|
||||||
|
)?;
|
||||||
|
let slot = self
|
||||||
|
.component_pose_buffers
|
||||||
|
.get_mut(instance.object_index)
|
||||||
|
.and_then(|buffers| buffers.get_mut(instance.component_index))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"animated component {} pose slot is outside object {}",
|
||||||
|
instance.component_index, instance.object_index
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if *slot != pose {
|
||||||
|
*slot = pose;
|
||||||
|
pose_changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !pose_changed {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mounts are recalculated from the sampled hierarchy even when the
|
||||||
|
// parent has no visible draw geometry of its own.
|
||||||
|
let mounts_changed = refresh_dynamic_component_mounts(
|
||||||
|
&self.dynamic_component_mounts,
|
||||||
|
&self.component_pose_buffers,
|
||||||
|
&mut self.component_mount_poses,
|
||||||
|
)?;
|
||||||
|
let mut geometry_changed = false;
|
||||||
|
let mut shadow_updates = Vec::new();
|
||||||
|
let mut updated_shadow_casters = HashSet::new();
|
||||||
|
for dynamic in &self.dynamic_component_geometry {
|
||||||
|
let pose = self
|
||||||
|
.component_pose_buffers
|
||||||
|
.get(dynamic.object_index)
|
||||||
|
.and_then(|buffers| buffers.get(dynamic.component_index))
|
||||||
|
.and_then(Option::as_ref)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"animated component {} has no current Node38 hierarchy",
|
||||||
|
dynamic.component_index
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let mount = self
|
||||||
|
.component_mount_poses
|
||||||
|
.get(dynamic.object_index)
|
||||||
|
.and_then(|poses| poses.get(dynamic.component_index))
|
||||||
|
.copied()
|
||||||
|
.flatten();
|
||||||
|
let projected = project_dynamic_component_geometry(dynamic, pose, mount)?;
|
||||||
|
if projected.vertices.len() != dynamic.vertex_count {
|
||||||
|
return Err(format!(
|
||||||
|
"animated component {} changed projected vertex count from {} to {}",
|
||||||
|
dynamic.component_index,
|
||||||
|
dynamic.vertex_count,
|
||||||
|
projected.vertices.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let end = dynamic
|
||||||
|
.vertex_start
|
||||||
|
.checked_add(dynamic.vertex_count)
|
||||||
|
.ok_or_else(|| "animated component vertex range overflowed".to_string())?;
|
||||||
|
let destination = self
|
||||||
|
.mesh
|
||||||
|
.vertices
|
||||||
|
.get_mut(dynamic.vertex_start..end)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"animated component {} vertex range is outside the shared mesh",
|
||||||
|
dynamic.component_index
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if destination != projected.vertices.as_slice() {
|
||||||
|
destination.copy_from_slice(&projected.vertices);
|
||||||
|
geometry_changed = true;
|
||||||
|
}
|
||||||
|
if let Some(range) = dynamic.shadow_triangle_range.clone() {
|
||||||
|
let triangles = static_mesh_shadow_triangles(&projected);
|
||||||
|
if triangles.len() != range.len() {
|
||||||
|
return Err(format!(
|
||||||
|
"animated component {} changed shadow triangle count from {} to {}",
|
||||||
|
dynamic.component_index,
|
||||||
|
range.len(),
|
||||||
|
triangles.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
shadow_updates.push((dynamic.object_index, range, triangles));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (object_index, range, triangles) in shadow_updates {
|
||||||
|
let Some(caster_index) = self
|
||||||
|
.shadow_caster_indices_by_object
|
||||||
|
.get(object_index)
|
||||||
|
.copied()
|
||||||
|
.flatten()
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let caster = self
|
||||||
|
.shadow_casters
|
||||||
|
.get_mut(caster_index)
|
||||||
|
.ok_or_else(|| format!("shadow caster {caster_index} is unavailable"))?;
|
||||||
|
let destination = caster.triangles.get_mut(range).ok_or_else(|| {
|
||||||
|
format!("animated component shadow range is outside caster {caster_index}")
|
||||||
|
})?;
|
||||||
|
destination.clone_from_slice(&triangles);
|
||||||
|
updated_shadow_casters.insert(caster_index);
|
||||||
|
}
|
||||||
|
for caster_index in updated_shadow_casters {
|
||||||
|
let baseline = self
|
||||||
|
.shadow_sphere_baselines
|
||||||
|
.get(caster_index)
|
||||||
|
.copied()
|
||||||
|
.ok_or_else(|| format!("shadow sphere baseline {caster_index} is unavailable"))?;
|
||||||
|
let caster = self
|
||||||
|
.shadow_casters
|
||||||
|
.get_mut(caster_index)
|
||||||
|
.ok_or_else(|| format!("shadow caster {caster_index} is unavailable"))?;
|
||||||
|
// shortcut: native animated sphere updates are unverified; derive
|
||||||
|
// a conservative envelope from the native startup sphere and all
|
||||||
|
// current triangles, never from a prior frame's expanded bounds.
|
||||||
|
caster.sphere = shadow_sphere_enclosing_triangles(baseline, &caster.triangles)?;
|
||||||
|
}
|
||||||
|
if geometry_changed || mounts_changed {
|
||||||
|
let sort_keys = self.world_draw_sort_keys(self.view_camera.position)?;
|
||||||
|
if let Some(renderer) = self.renderer.as_mut() {
|
||||||
|
for (range_index, sort_key) in sort_keys {
|
||||||
|
renderer
|
||||||
|
.set_draw_range_sort_key(range_index, sort_key)
|
||||||
|
.map_err(|error| {
|
||||||
|
format!("update animated component sort key {range_index}: {error}")
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn update_shadow(
|
fn update_shadow(
|
||||||
mesh: &mut VulkanStaticMesh,
|
mesh: &mut VulkanStaticMesh,
|
||||||
shadow_cache: &mut ShadowPageCache,
|
shadow_cache: &mut ShadowPageCache,
|
||||||
@@ -5971,6 +6416,11 @@ impl ApplicationHandler for StaticVulkanApp {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.update_material_clock();
|
self.update_material_clock();
|
||||||
|
if let Err(error) = self.update_controlled_component_geometry() {
|
||||||
|
self.error = Some(error);
|
||||||
|
event_loop.exit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Err(error) = self.update_environment() {
|
if let Err(error) = self.update_environment() {
|
||||||
self.error = Some(error);
|
self.error = Some(error);
|
||||||
event_loop.exit();
|
event_loop.exit();
|
||||||
@@ -6318,6 +6768,39 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn control_pose_drawable_test_model() -> ModelAsset {
|
||||||
|
let mut model = control_pose_test_model();
|
||||||
|
model.nodes_raw[8..10].copy_from_slice(&0_u16.to_le_bytes());
|
||||||
|
model.slots = vec![fparkan_msh::Slot {
|
||||||
|
tri_start: 0,
|
||||||
|
tri_count: 1,
|
||||||
|
batch_start: 0,
|
||||||
|
batch_count: 1,
|
||||||
|
aabb_min: [0.0; 3],
|
||||||
|
aabb_max: [1.0; 3],
|
||||||
|
sphere_center: [0.0; 3],
|
||||||
|
sphere_radius: 1.0,
|
||||||
|
opaque: [0; 5],
|
||||||
|
}];
|
||||||
|
model.positions = vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
|
||||||
|
model.normals = Some(vec![[0, 0, 127, 0]; 3]);
|
||||||
|
model.uv0 = Some(vec![[0, 0]; 3]);
|
||||||
|
model.uv1 = None;
|
||||||
|
model.indices = vec![0, 1, 2];
|
||||||
|
model.batches = vec![fparkan_msh::Batch {
|
||||||
|
batch_flags: 0,
|
||||||
|
material_index_hi: 0,
|
||||||
|
material_index: 0,
|
||||||
|
lightmap_index: u8::MAX,
|
||||||
|
local_batch_index: 0,
|
||||||
|
index_count: 3,
|
||||||
|
index_start: 0,
|
||||||
|
vertex_count: 3,
|
||||||
|
base_vertex: 0,
|
||||||
|
}];
|
||||||
|
model
|
||||||
|
}
|
||||||
|
|
||||||
fn control_pose_test_control() -> PreparedComponentControl {
|
fn control_pose_test_control() -> PreparedComponentControl {
|
||||||
let mut fixed_words = [0; 44];
|
let mut fixed_words = [0; 44];
|
||||||
fixed_words[2] = 0;
|
fixed_words[2] = 0;
|
||||||
@@ -6432,6 +6915,222 @@ mod tests {
|
|||||||
assert!(mismatch.contains("does not match MSH node count"));
|
assert!(mismatch.contains("does not match MSH node count"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exto_parent_control_updates_visible_child_mount_without_draw_geometry() {
|
||||||
|
let mut model = control_pose_test_model();
|
||||||
|
let mut socket_node = vec![u8::MAX; model.node_stride];
|
||||||
|
socket_node[..2].copy_from_slice(&[0, 0]);
|
||||||
|
socket_node[2..4].copy_from_slice(&0_u16.to_le_bytes());
|
||||||
|
socket_node[4..6].copy_from_slice(&u16::MAX.to_le_bytes());
|
||||||
|
socket_node[6..8].copy_from_slice(&1_u16.to_le_bytes());
|
||||||
|
model.nodes_raw.extend(socket_node);
|
||||||
|
model.node_count = 2;
|
||||||
|
let mut control = control_pose_test_control();
|
||||||
|
control.model_node_count = 2;
|
||||||
|
let unit_record = |immediate_child_count| fparkan_prototype::UnitComponentRecord {
|
||||||
|
archive_raw: [0; 32],
|
||||||
|
resource_raw: [0; 32],
|
||||||
|
kind: 0,
|
||||||
|
parent_or_link: 1,
|
||||||
|
description_raw: [0; 32],
|
||||||
|
tail0: 0,
|
||||||
|
immediate_child_count,
|
||||||
|
};
|
||||||
|
let unit_tree = unit_component_tree(&[unit_record(1), unit_record(1), unit_record(0)])
|
||||||
|
.expect("preorder Unit DAT tree");
|
||||||
|
let controls = vec![None, Some(control.clone()), None];
|
||||||
|
assert!(unit_component_is_visible(1, Some(PROTOTYPE_TYPE_EXTO)));
|
||||||
|
assert!(unit_component_is_visible(2, Some(PROTOTYPE_TYPE_EXTO)));
|
||||||
|
assert!(component_chain_has_tag21(Some(&unit_tree), &controls, 2));
|
||||||
|
assert_eq!(unit_tree[1].parent_index, Some(0));
|
||||||
|
assert_eq!(unit_tree[2].parent_index, Some(1));
|
||||||
|
assert!(
|
||||||
|
unit_tree[2]
|
||||||
|
.parent_index
|
||||||
|
.is_some_and(|parent_index| parent_index < 2),
|
||||||
|
"Unit DAT preorder places a parent before its child"
|
||||||
|
);
|
||||||
|
let mut state = ControlInstanceState::default();
|
||||||
|
let initial_hierarchy = static_preview_control_pose_buffer(&model, &control, 0, &mut state)
|
||||||
|
.expect("initial EXTO parent pose")
|
||||||
|
.expect("initial parent hierarchy");
|
||||||
|
let later_hierarchy = static_preview_control_pose_buffer(&model, &control, 225, &mut state)
|
||||||
|
.expect("animated EXTO parent pose")
|
||||||
|
.expect("animated parent hierarchy");
|
||||||
|
let mut pose_buffers = vec![vec![None, Some(initial_hierarchy), None]];
|
||||||
|
let mounts = [DynamicComponentMount {
|
||||||
|
object_index: 0,
|
||||||
|
component_index: 2,
|
||||||
|
parent_index: 1,
|
||||||
|
socket: 1,
|
||||||
|
}];
|
||||||
|
let initial = node38_pose_from_hierarchy(
|
||||||
|
pose_buffers[0][1].as_ref().expect("EXTO parent hierarchy"),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
.expect("parent socket");
|
||||||
|
let mut mount_poses = vec![vec![None, None, Some(initial)]];
|
||||||
|
pose_buffers[0][1] = Some(later_hierarchy);
|
||||||
|
let parent_geometry = DynamicComponentGeometry {
|
||||||
|
object_index: 0,
|
||||||
|
component_index: 1,
|
||||||
|
model: Arc::new(model.clone()),
|
||||||
|
transform: LegacyIron3dEulerTransform {
|
||||||
|
translation: [0.0; 3],
|
||||||
|
orientation_radians: [0.0; 3],
|
||||||
|
},
|
||||||
|
scale: [1.0; 3],
|
||||||
|
vertex_start: 0,
|
||||||
|
vertex_count: 0,
|
||||||
|
shadow_triangle_range: None,
|
||||||
|
};
|
||||||
|
let empty_parent_mesh = project_dynamic_component_geometry(
|
||||||
|
&parent_geometry,
|
||||||
|
pose_buffers[0][1].as_ref().expect("EXTO parent hierarchy"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("zero-geometry EXTO parent");
|
||||||
|
assert!(empty_parent_mesh.vertices.is_empty());
|
||||||
|
assert!(empty_parent_mesh.indices.is_empty());
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
refresh_dynamic_component_mounts(&mounts, &pose_buffers, &mut mount_poses)
|
||||||
|
.expect("refresh child mount")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
mount_poses[0][2].expect("child mount").translation,
|
||||||
|
[0.0, 2.45, 0.0]
|
||||||
|
);
|
||||||
|
// The EXTO parent has a sampled Node38 hierarchy but no projected draw
|
||||||
|
// vertices. Its hierarchy still drives the visible child's mount.
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn controlled_component_update_keeps_vertices_and_shadow_triangles_in_sync() {
|
||||||
|
let model = Arc::new(control_pose_drawable_test_model());
|
||||||
|
let control = control_pose_test_control();
|
||||||
|
let dynamic = DynamicComponentGeometry {
|
||||||
|
object_index: 0,
|
||||||
|
component_index: 0,
|
||||||
|
model: Arc::clone(&model),
|
||||||
|
transform: LegacyIron3dEulerTransform {
|
||||||
|
translation: [0.0; 3],
|
||||||
|
orientation_radians: [0.0; 3],
|
||||||
|
},
|
||||||
|
scale: [1.0; 3],
|
||||||
|
vertex_start: 0,
|
||||||
|
vertex_count: 3,
|
||||||
|
shadow_triangle_range: Some(0..1),
|
||||||
|
};
|
||||||
|
let mut state = ControlInstanceState::default();
|
||||||
|
let initial_pose = static_preview_control_pose_buffer(&model, &control, 0, &mut state)
|
||||||
|
.expect("initial hierarchy")
|
||||||
|
.expect("initial Node38 pose");
|
||||||
|
let initial_mesh = project_dynamic_component_geometry(&dynamic, &initial_pose, None)
|
||||||
|
.expect("initial projected component");
|
||||||
|
let initial_triangles = static_mesh_shadow_triangles(&initial_mesh);
|
||||||
|
let baseline = ShadowSphere {
|
||||||
|
center: [0.0; 3],
|
||||||
|
radius: 1.0,
|
||||||
|
};
|
||||||
|
let view_camera = FreeFlightCamera {
|
||||||
|
position: [0.0; 3],
|
||||||
|
yaw: 0.0,
|
||||||
|
pitch: 0.0,
|
||||||
|
horizontal_fov: 1.0,
|
||||||
|
near_plane: 0.1,
|
||||||
|
far_plane: 100.0,
|
||||||
|
move_speed: 1.0,
|
||||||
|
}
|
||||||
|
.preview_frame(16.0 / 9.0);
|
||||||
|
let preview = StaticPreviewScene {
|
||||||
|
mesh: initial_mesh.clone(),
|
||||||
|
native_handedness_by_range: HashMap::new(),
|
||||||
|
materials: Vec::new(),
|
||||||
|
material_animations: Vec::new(),
|
||||||
|
sun_occlusion_range_indices: Vec::new(),
|
||||||
|
shadow_casters: vec![ShadowCaster {
|
||||||
|
id: 0,
|
||||||
|
world_axes: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
|
||||||
|
sphere: baseline,
|
||||||
|
native_kind: 4,
|
||||||
|
triangles: initial_triangles.clone(),
|
||||||
|
}],
|
||||||
|
shadow_sphere_baselines: vec![baseline],
|
||||||
|
shadow_receivers: Vec::new(),
|
||||||
|
shadow_material_index: 0,
|
||||||
|
terrain: Arc::new(TerrainWorld::default()),
|
||||||
|
component_pose_buffers: vec![vec![Some(initial_pose)]],
|
||||||
|
component_mount_poses: vec![vec![None]],
|
||||||
|
control_animation_instances: vec![ControlAnimationInstance {
|
||||||
|
object_index: 0,
|
||||||
|
component_index: 0,
|
||||||
|
model: Arc::clone(&model),
|
||||||
|
control,
|
||||||
|
state,
|
||||||
|
}],
|
||||||
|
dynamic_component_geometry: vec![dynamic],
|
||||||
|
dynamic_component_mounts: Vec::new(),
|
||||||
|
shadow_caster_indices_by_object: vec![Some(0)],
|
||||||
|
camera: view_camera.vulkan_camera(),
|
||||||
|
view_camera,
|
||||||
|
captured_viewport: None,
|
||||||
|
free_camera: None,
|
||||||
|
camera_mode: "test",
|
||||||
|
mesh_components: 1,
|
||||||
|
terrain_components: 0,
|
||||||
|
};
|
||||||
|
let mut app = StaticVulkanApp::new(
|
||||||
|
preview,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
EnvironmentGpuRange {
|
||||||
|
range_index: usize::MAX,
|
||||||
|
vertex_start: initial_mesh.vertices.len(),
|
||||||
|
vertex_capacity: 0,
|
||||||
|
material_index: 0,
|
||||||
|
},
|
||||||
|
initial_mesh.vertices.len(),
|
||||||
|
initial_mesh.indices.len(),
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
"control-test",
|
||||||
|
1,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
StartupTimings::default(),
|
||||||
|
);
|
||||||
|
app.material_time_ms = 225;
|
||||||
|
app.update_controlled_component_geometry()
|
||||||
|
.expect("update projected component and caster");
|
||||||
|
|
||||||
|
assert_eq!(app.mesh.indices, initial_mesh.indices);
|
||||||
|
assert_eq!(app.mesh.vertices.len(), initial_mesh.vertices.len());
|
||||||
|
assert_ne!(
|
||||||
|
initial_mesh.vertices[0].position,
|
||||||
|
app.mesh.vertices[0].position
|
||||||
|
);
|
||||||
|
assert!((app.mesh.vertices[0].position[1] - 0.45).abs() < 1.0e-4);
|
||||||
|
let b_vertices = app.mesh.vertices.clone();
|
||||||
|
let b_triangles = app.shadow_casters[0].triangles.clone();
|
||||||
|
let b_sphere = app.shadow_casters[0].sphere;
|
||||||
|
assert_ne!(b_triangles, initial_triangles);
|
||||||
|
assert!(b_sphere.radius > baseline.radius);
|
||||||
|
|
||||||
|
app.update_controlled_component_geometry()
|
||||||
|
.expect("repeat the same control timestamp");
|
||||||
|
assert_eq!(app.mesh.vertices, b_vertices);
|
||||||
|
assert_eq!(app.shadow_casters[0].triangles, b_triangles);
|
||||||
|
assert_eq!(app.shadow_casters[0].sphere, b_sphere);
|
||||||
|
|
||||||
|
app.material_time_ms = 0;
|
||||||
|
app.update_controlled_component_geometry()
|
||||||
|
.expect("return to first control pose");
|
||||||
|
assert_eq!(app.mesh.vertices, initial_mesh.vertices);
|
||||||
|
assert_eq!(app.shadow_casters[0].triangles, initial_triangles);
|
||||||
|
assert_eq!(app.shadow_casters[0].sphere, baseline);
|
||||||
|
}
|
||||||
|
|
||||||
fn set_test_terrain_lod0_range(terrain: &mut fparkan_terrain_format::LandMeshDocument) {
|
fn set_test_terrain_lod0_range(terrain: &mut fparkan_terrain_format::LandMeshDocument) {
|
||||||
const NODE_STRIDE: usize = 38;
|
const NODE_STRIDE: usize = 38;
|
||||||
const SLOT_STRIDE: usize = 68;
|
const SLOT_STRIDE: usize = 68;
|
||||||
@@ -6737,6 +7436,39 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn animated_shadow_bounds_rebuild_from_native_baseline() {
|
||||||
|
let baseline = ShadowSphere {
|
||||||
|
center: [0.0; 3],
|
||||||
|
radius: 1.0,
|
||||||
|
};
|
||||||
|
let pose_a = [shadow_triangle(
|
||||||
|
[[0.5, 0.0, 0.0], [0.0, 0.5, 0.0], [0.0, 0.0, 0.5]],
|
||||||
|
0,
|
||||||
|
)];
|
||||||
|
let pose_b = [shadow_triangle(
|
||||||
|
[[3.0, 0.0, 0.0], [4.0, 0.0, 0.0], [3.0, 1.0, 0.0]],
|
||||||
|
0,
|
||||||
|
)];
|
||||||
|
|
||||||
|
let first_a =
|
||||||
|
shadow_sphere_enclosing_triangles(baseline, &pose_a).expect("initial animated bounds");
|
||||||
|
assert_eq!(first_a, baseline);
|
||||||
|
let at_b =
|
||||||
|
shadow_sphere_enclosing_triangles(baseline, &pose_b).expect("expanded animated bounds");
|
||||||
|
assert!((at_b.center[0] - 1.5).abs() < 1.0e-5);
|
||||||
|
assert!((at_b.radius - 2.5).abs() < 1.0e-5);
|
||||||
|
assert!(length3(at_b.center).expect("finite sphere center") + 1.0 <= at_b.radius);
|
||||||
|
for point in pose_b.iter().flat_map(|triangle| triangle.positions) {
|
||||||
|
assert!(
|
||||||
|
length3(sub3(point, at_b.center)).expect("finite point distance") <= at_b.radius
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let second_a = shadow_sphere_enclosing_triangles(baseline, &pose_a)
|
||||||
|
.expect("return to first animated pose");
|
||||||
|
assert_eq!(second_a, first_a);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mounted_shadow_bounds_ignore_child_translation_and_scale_aggregate_once() {
|
fn mounted_shadow_bounds_ignore_child_translation_and_scale_aggregate_once() {
|
||||||
let parent = VulkanNodePose {
|
let parent = VulkanNodePose {
|
||||||
|
|||||||
+24
-12
@@ -362,18 +362,30 @@ local.set_translation(pose.position);
|
|||||||
world[n] = world[parent(n)] * local;
|
world[n] = world[parent(n)] * local;
|
||||||
```
|
```
|
||||||
|
|
||||||
Статический Vulkan preview берёт начальный кадр узла из явной ссылки прототипа
|
Статический Vulkan preview берёт CTLD `.ctl` из явной ссылки прототипа.
|
||||||
на CTLD `.ctl`. Для поддерживаемых Control timeline и tag 21 он выполняет один
|
Поддерживаемые Control timeline с tag 21 сэмплируются при запуске на native
|
||||||
сэмпл при native clock `0`, передавая конечные type-1 значения разрешённых NDP
|
clock `0`; разрешённые NDP providers передают конечные type-1 значения.
|
||||||
providers; неподдерживаемый режим/variable mapping и отсутствующий или
|
Неподдерживаемый режим или variable mapping, а также отсутствующее или
|
||||||
неразрешённый gate value возвращают preview к статическим CTLD bindings.
|
неразрешённое gate value оставляют компонент на статических CTLD bindings. В
|
||||||
Используются только включённые строки, а при повторе node ID поздняя строка
|
timeline участвуют только включённые строки; при повторном node ID поздняя
|
||||||
заменяет раннюю. Без
|
строка заменяет раннюю.
|
||||||
`--static-animation-frame` preview применяет эти настройки узлов; узлы без
|
|
||||||
настройки остаются на кадре `0`. Явный `--static-animation-frame N`
|
Без `--static-animation-frame` preview сохраняет экземпляр Control и при
|
||||||
переопределяет их и задаёт `N` всем узлам, пропуская Control-сэмпл.
|
каждом redraw сэмплирует поддерживаемый timeline на текущем native clock.
|
||||||
Результат — одна стартовая статическая поза, не обновление анимации, Control
|
Обновлённая Node38-поза перепроецирует видимую геометрию в заранее выделенный
|
||||||
или AI по ходу кадров.
|
vertex span, оставляя индексы и draw ranges неизменными. Если в пути Unit DAT
|
||||||
|
компонента есть такой Control, заново вычисляется его EXTO mount socket и
|
||||||
|
перепроецируется дочерняя геометрия; дочерний mount использует preorder-порядок
|
||||||
|
Unit DAT. Внутренние INTO-компоненты здесь не считаются глобальными MSH
|
||||||
|
socket mounts. Обновлённые вершины и shadow-треугольники попадают в общий
|
||||||
|
mesh upload перед следующим кадром. Culling sphere восстанавливается от
|
||||||
|
начальной native sphere и расширяется до текущих shadow-треугольников; это
|
||||||
|
`shortcut:`, потому что native обновление bounding sphere для анимированной
|
||||||
|
геометрии пока не установлено. Это путь сэмплирования Control-позы, не AI или
|
||||||
|
анимация эффектов.
|
||||||
|
|
||||||
|
Явный `--static-animation-frame N` переопределяет timeline и задаёт `N` всем
|
||||||
|
узлам, пропуская runtime Control-сэмпл.
|
||||||
MSH sampler выбирает type 19 frame округлением `(time - 0.5)` к ближайшему
|
MSH sampler выбирает type 19 frame округлением `(time - 0.5)` к ближайшему
|
||||||
целому с ties-to-even, затем интерполирует выбранный type 8 key и следующий
|
целому с ties-to-even, затем интерполирует выбранный type 8 key и следующий
|
||||||
key в исходном `time`; это portable-вариант native x87 conversion в default
|
key в исходном `time`; это portable-вариант native x87 conversion в default
|
||||||
|
|||||||
Reference in new issue
Block a user