Preserve pending camera pose on viewport retry
Docs Deploy / Build and Deploy MkDocs (push) Successful in 39s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 39s
This commit is contained in:
3 files changed
+364
-43
No files matched your search
+217
-43
@@ -421,14 +421,54 @@ impl FreeFlightCamera {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn vulkan_camera(self, aspect: f32) -> VulkanStaticCamera {
|
fn vulkan_camera(self, aspect: f32) -> VulkanStaticCamera {
|
||||||
let aspect = if aspect.is_finite() && aspect > f32::EPSILON {
|
VulkanStaticCamera {
|
||||||
|
clip_from_world: clip_from_world_for_view(
|
||||||
|
self.position,
|
||||||
|
self.forward(),
|
||||||
|
self.right(),
|
||||||
|
self.up(),
|
||||||
|
self.vertical_fov(aspect),
|
||||||
|
self.near_plane,
|
||||||
|
self.far_plane,
|
||||||
|
aspect,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview_frame(self, aspect: f32) -> PreviewCameraFrame {
|
||||||
|
let camera = self.vulkan_camera(aspect);
|
||||||
|
PreviewCameraFrame {
|
||||||
|
position: self.position,
|
||||||
|
forward: self.forward(),
|
||||||
|
right: self.right(),
|
||||||
|
up: self.up(),
|
||||||
|
vertical_fov: self.vertical_fov(aspect),
|
||||||
|
near_plane: self.near_plane,
|
||||||
|
far_plane: self.far_plane,
|
||||||
|
clip_from_world: camera.clip_from_world,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview_aspect(aspect: f32) -> f32 {
|
||||||
|
if aspect.is_finite() && aspect > f32::EPSILON {
|
||||||
aspect
|
aspect
|
||||||
} else {
|
} else {
|
||||||
16.0 / 9.0
|
16.0 / 9.0
|
||||||
};
|
}
|
||||||
let forward = self.forward();
|
}
|
||||||
let right = self.right();
|
|
||||||
let up = self.up();
|
fn clip_from_world_for_view(
|
||||||
|
position: [f32; 3],
|
||||||
|
forward: [f32; 3],
|
||||||
|
right: [f32; 3],
|
||||||
|
up: [f32; 3],
|
||||||
|
vertical_fov: f32,
|
||||||
|
near_plane: f32,
|
||||||
|
far_plane: f32,
|
||||||
|
aspect: f32,
|
||||||
|
) -> [f32; 16] {
|
||||||
|
let aspect = preview_aspect(aspect);
|
||||||
// VulkanStaticCamera carries row-major D3D-style data. GLSL interprets
|
// VulkanStaticCamera carries row-major D3D-style data. GLSL interprets
|
||||||
// those bytes as a column-major mat4, so shader multiplication is the
|
// those bytes as a column-major mat4, so shader multiplication is the
|
||||||
// transpose of this row-vector transform: world * V * P. The camera
|
// transpose of this row-vector transform: world * V * P. The camera
|
||||||
@@ -446,15 +486,14 @@ impl FreeFlightCamera {
|
|||||||
up[2],
|
up[2],
|
||||||
forward[2],
|
forward[2],
|
||||||
0.0,
|
0.0,
|
||||||
-dot3(right, self.position),
|
-dot3(right, position),
|
||||||
-dot3(up, self.position),
|
-dot3(up, position),
|
||||||
-dot3(forward, self.position),
|
-dot3(forward, position),
|
||||||
1.0,
|
1.0,
|
||||||
];
|
];
|
||||||
let half_fov = self.vertical_fov(aspect) * 0.5;
|
let focal = (vertical_fov * 0.5).tan().recip();
|
||||||
let focal = half_fov.tan().recip();
|
let near_plane = near_plane.max(0.001);
|
||||||
let near_plane = self.near_plane.max(0.001);
|
let far_plane = far_plane.max(near_plane + 0.001);
|
||||||
let far_plane = self.far_plane.max(near_plane + 0.001);
|
|
||||||
let projection = [
|
let projection = [
|
||||||
focal / aspect,
|
focal / aspect,
|
||||||
0.0,
|
0.0,
|
||||||
@@ -473,24 +512,7 @@ impl FreeFlightCamera {
|
|||||||
-(near_plane * far_plane) / (far_plane - near_plane),
|
-(near_plane * far_plane) / (far_plane - near_plane),
|
||||||
0.0,
|
0.0,
|
||||||
];
|
];
|
||||||
VulkanStaticCamera {
|
multiply_row_major(view, projection)
|
||||||
clip_from_world: multiply_row_major(view, projection),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn preview_frame(self, aspect: f32) -> PreviewCameraFrame {
|
|
||||||
let camera = self.vulkan_camera(aspect);
|
|
||||||
PreviewCameraFrame {
|
|
||||||
position: self.position,
|
|
||||||
forward: self.forward(),
|
|
||||||
right: self.right(),
|
|
||||||
up: self.up(),
|
|
||||||
vertical_fov: self.vertical_fov(aspect),
|
|
||||||
near_plane: self.near_plane,
|
|
||||||
far_plane: self.far_plane,
|
|
||||||
clip_from_world: camera.clip_from_world,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Immutable view data shared by the environment renderers. The optional
|
/// Immutable view data shared by the environment renderers. The optional
|
||||||
@@ -551,6 +573,28 @@ impl PreviewCameraFrame {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn with_aspect(self, previous_aspect: f32, aspect: f32) -> Self {
|
||||||
|
let previous_aspect = preview_aspect(previous_aspect);
|
||||||
|
let aspect = preview_aspect(aspect);
|
||||||
|
let horizontal_half_fov_tangent = (self.vertical_fov * 0.5).tan() * previous_aspect;
|
||||||
|
let vertical_fov = 2.0 * (horizontal_half_fov_tangent / aspect).atan();
|
||||||
|
let clip_from_world = clip_from_world_for_view(
|
||||||
|
self.position,
|
||||||
|
self.forward,
|
||||||
|
self.right,
|
||||||
|
self.up,
|
||||||
|
vertical_fov,
|
||||||
|
self.near_plane,
|
||||||
|
self.far_plane,
|
||||||
|
aspect,
|
||||||
|
);
|
||||||
|
Self {
|
||||||
|
vertical_fov,
|
||||||
|
clip_from_world,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn forward(self) -> [f32; 3] {
|
fn forward(self) -> [f32; 3] {
|
||||||
self.forward
|
self.forward
|
||||||
}
|
}
|
||||||
@@ -3404,6 +3448,22 @@ struct DynamicEnvironmentFrame {
|
|||||||
absolute_time_seconds: f32,
|
absolute_time_seconds: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn environment_camera_for_preview(
|
||||||
|
camera: &PreviewCameraFrame,
|
||||||
|
aspect: f32,
|
||||||
|
viewport: [f32; 2],
|
||||||
|
) -> FxCamera {
|
||||||
|
FxCamera::with_projection(
|
||||||
|
camera.position,
|
||||||
|
camera.forward(),
|
||||||
|
camera.right(),
|
||||||
|
camera.up(),
|
||||||
|
camera.vertical_fov,
|
||||||
|
aspect,
|
||||||
|
)
|
||||||
|
.with_viewport(viewport)
|
||||||
|
}
|
||||||
|
|
||||||
struct DynamicEnvironment {
|
struct DynamicEnvironment {
|
||||||
schedule: TypedAtmosphere,
|
schedule: TypedAtmosphere,
|
||||||
sky: SkySystem,
|
sky: SkySystem,
|
||||||
@@ -3489,15 +3549,7 @@ impl DynamicEnvironment {
|
|||||||
.set_frame_uniforms(uniforms)
|
.set_frame_uniforms(uniforms)
|
||||||
.map_err(|error| format!("update sky frame uniforms: {error}"))?;
|
.map_err(|error| format!("update sky frame uniforms: {error}"))?;
|
||||||
|
|
||||||
let fx_camera = FxCamera::with_projection(
|
let fx_camera = environment_camera_for_preview(camera, aspect, viewport);
|
||||||
camera.position,
|
|
||||||
camera.forward(),
|
|
||||||
camera.right(),
|
|
||||||
camera.up(),
|
|
||||||
camera.vertical_fov,
|
|
||||||
aspect,
|
|
||||||
)
|
|
||||||
.with_viewport(viewport);
|
|
||||||
let environment_frame = self.environment.update_atmosphere_with_materials(
|
let environment_frame = self.environment.update_atmosphere_with_materials(
|
||||||
dt_seconds,
|
dt_seconds,
|
||||||
&atmosphere,
|
&atmosphere,
|
||||||
@@ -6219,8 +6271,7 @@ impl StaticVulkanApp {
|
|||||||
};
|
};
|
||||||
let viewport = [viewport[0].max(1.0), viewport[1].max(1.0)];
|
let viewport = [viewport[0].max(1.0), viewport[1].max(1.0)];
|
||||||
let aspect = viewport[0] / viewport[1];
|
let aspect = viewport[0] / viewport[1];
|
||||||
if let Some(free_camera) = self.free_camera.as_ref() {
|
let camera = update.camera.with_aspect(update.aspect, aspect);
|
||||||
let camera = free_camera.preview_frame(aspect);
|
|
||||||
self.view_camera = camera;
|
self.view_camera = camera;
|
||||||
self.camera = camera.vulkan_camera();
|
self.camera = camera.vulkan_camera();
|
||||||
if let Some(renderer) = self.renderer.as_mut() {
|
if let Some(renderer) = self.renderer.as_mut() {
|
||||||
@@ -6228,8 +6279,12 @@ impl StaticVulkanApp {
|
|||||||
.set_camera(self.camera)
|
.set_camera(self.camera)
|
||||||
.map_err(|error| format!("refresh free-flight camera viewport: {error}"))?;
|
.map_err(|error| format!("refresh free-flight camera viewport: {error}"))?;
|
||||||
}
|
}
|
||||||
|
let environment_camera = environment_camera_for_preview(&camera, aspect, viewport);
|
||||||
|
if let Some(environment) = update.environment.as_mut() {
|
||||||
|
environment
|
||||||
|
.environment_frame
|
||||||
|
.reproject_for_camera(environment_camera);
|
||||||
}
|
}
|
||||||
let camera = self.view_camera;
|
|
||||||
self.apply_environment_view(update.environment.as_ref(), camera, viewport)?;
|
self.apply_environment_view(update.environment.as_ref(), camera, viewport)?;
|
||||||
update.camera = camera;
|
update.camera = camera;
|
||||||
update.viewport = viewport;
|
update.viewport = viewport;
|
||||||
@@ -7024,6 +7079,7 @@ fn usage() -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use fparkan_fx::environment::{WeatherInput, WeatherState};
|
||||||
|
|
||||||
fn control_pose_test_model() -> ModelAsset {
|
fn control_pose_test_model() -> ModelAsset {
|
||||||
let mut nodes = vec![u8::MAX; 38];
|
let mut nodes = vec![u8::MAX; 38];
|
||||||
@@ -7430,13 +7486,54 @@ mod tests {
|
|||||||
let sample = app.sample_live_frame_clock_at(start + Duration::from_millis(225));
|
let sample = app.sample_live_frame_clock_at(start + Duration::from_millis(225));
|
||||||
assert_eq!(sample.time_ms, 225);
|
assert_eq!(sample.time_ms, 225);
|
||||||
assert!((sample.delta_seconds - 0.225).abs() < f32::EPSILON);
|
assert!((sample.delta_seconds - 0.225).abs() < f32::EPSILON);
|
||||||
let update = app
|
let mut update = app
|
||||||
.prepare_scene_update(sample, [1280.0, 720.0])
|
.prepare_scene_update(sample, [1280.0, 720.0])
|
||||||
.expect("apply one full live tick to the scene");
|
.expect("apply one full live tick to the scene");
|
||||||
|
let expected_resized_camera = app
|
||||||
|
.free_camera
|
||||||
|
.expect("free-flight input controller")
|
||||||
|
.preview_frame(4.0 / 3.0);
|
||||||
let camera_after_tick = app.free_camera.expect("free-flight camera").position;
|
let camera_after_tick = app.free_camera.expect("free-flight camera").position;
|
||||||
assert!((camera_after_tick[0] - 0.35).abs() < 1.0e-5);
|
assert!((camera_after_tick[0] - 0.35).abs() < 1.0e-5);
|
||||||
assert!((camera_after_tick[1] - 0.25).abs() < 1.0e-5);
|
assert!((camera_after_tick[1] - 0.25).abs() < 1.0e-5);
|
||||||
assert_eq!(app.material_time_ms, sample.time_ms);
|
assert_eq!(app.material_time_ms, sample.time_ms);
|
||||||
|
let weather = WeatherInput {
|
||||||
|
rain: WeatherState::new(true, 0.03, [0.7, 0.8, 1.0, 0.6]),
|
||||||
|
snow: WeatherState::new(true, 0.03, [0.9, 0.95, 1.0, 0.8]),
|
||||||
|
..WeatherInput::clear()
|
||||||
|
};
|
||||||
|
let mut weather_system = EnvironmentSystem::new(0xC1_225);
|
||||||
|
let first_environment_frame = weather_system.update(
|
||||||
|
0.225,
|
||||||
|
weather,
|
||||||
|
environment_camera_for_preview(&update.camera, update.aspect, update.viewport),
|
||||||
|
);
|
||||||
|
let mut precipitation_counts = [0usize; 2];
|
||||||
|
for primitive in &first_environment_frame.primitives {
|
||||||
|
if let EnvironmentPrimitive::Particle { kind, .. } = primitive {
|
||||||
|
let slot = match kind {
|
||||||
|
PrecipitationKind::Rain => 0,
|
||||||
|
PrecipitationKind::Snow => 1,
|
||||||
|
};
|
||||||
|
precipitation_counts[slot] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(precipitation_counts.iter().all(|count| *count > 0));
|
||||||
|
update.environment = Some(DynamicEnvironmentFrame {
|
||||||
|
atmosphere: TypedAtmosphere {
|
||||||
|
marker: 0,
|
||||||
|
version: 0,
|
||||||
|
tracks: Vec::new(),
|
||||||
|
trailer: Default::default(),
|
||||||
|
}
|
||||||
|
.sample(0.225),
|
||||||
|
sky_mesh: SkySystem::new(Default::default(), SkyMaterials::default())
|
||||||
|
.expect("test sky topology")
|
||||||
|
.mesh()
|
||||||
|
.clone(),
|
||||||
|
environment_frame: first_environment_frame.clone(),
|
||||||
|
absolute_time_seconds: 0.225,
|
||||||
|
});
|
||||||
assert!(app.lifecycle.prepare(update.clone()));
|
assert!(app.lifecycle.prepare(update.clone()));
|
||||||
let control_state_after_tick = app.control_animation_instances[0].state.clone();
|
let control_state_after_tick = app.control_animation_instances[0].state.clone();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -7459,6 +7556,8 @@ mod tests {
|
|||||||
assert_ne!(b_triangles, initial_triangles);
|
assert_ne!(b_triangles, initial_triangles);
|
||||||
assert!(b_sphere.radius > baseline.radius);
|
assert!(b_sphere.radius > baseline.radius);
|
||||||
|
|
||||||
|
app.free_camera.as_mut().unwrap().look_delta(64.0, -24.0);
|
||||||
|
let controller_after_input = app.free_camera.unwrap();
|
||||||
app.refresh_pending_viewport([800.0, 600.0])
|
app.refresh_pending_viewport([800.0, 600.0])
|
||||||
.expect("refresh only viewport-dependent state after recreation");
|
.expect("refresh only viewport-dependent state after recreation");
|
||||||
let refreshed = app.lifecycle.pending.as_ref().expect("pending sample");
|
let refreshed = app.lifecycle.pending.as_ref().expect("pending sample");
|
||||||
@@ -7466,10 +7565,76 @@ mod tests {
|
|||||||
assert_eq!(refreshed.viewport, [800.0, 600.0]);
|
assert_eq!(refreshed.viewport, [800.0, 600.0]);
|
||||||
assert_eq!(refreshed.aspect, 4.0 / 3.0);
|
assert_eq!(refreshed.aspect, 4.0 / 3.0);
|
||||||
assert_eq!(refreshed.camera.position, update.camera.position);
|
assert_eq!(refreshed.camera.position, update.camera.position);
|
||||||
|
assert_eq!(refreshed.camera.forward, update.camera.forward);
|
||||||
|
assert_eq!(refreshed.camera.right, update.camera.right);
|
||||||
|
assert_eq!(refreshed.camera.up, update.camera.up);
|
||||||
|
assert_eq!(refreshed.camera.position, expected_resized_camera.position);
|
||||||
|
assert_eq!(refreshed.camera.forward, expected_resized_camera.forward);
|
||||||
|
assert_eq!(refreshed.camera.right, expected_resized_camera.right);
|
||||||
|
assert_eq!(refreshed.camera.up, expected_resized_camera.up);
|
||||||
|
assert!(
|
||||||
|
(refreshed.camera.vertical_fov - expected_resized_camera.vertical_fov).abs() < 1.0e-6
|
||||||
|
);
|
||||||
|
assert!(refreshed
|
||||||
|
.camera
|
||||||
|
.clip_from_world
|
||||||
|
.iter()
|
||||||
|
.zip(expected_resized_camera.clip_from_world)
|
||||||
|
.all(|(actual, expected)| (actual - expected).abs() < 1.0e-5));
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
refreshed.camera.clip_from_world,
|
refreshed.camera.clip_from_world,
|
||||||
update.camera.clip_from_world
|
update.camera.clip_from_world
|
||||||
);
|
);
|
||||||
|
assert_eq!(app.free_camera, Some(controller_after_input));
|
||||||
|
let refreshed_environment_frame =
|
||||||
|
&refreshed.environment.as_ref().unwrap().environment_frame;
|
||||||
|
assert_eq!(
|
||||||
|
refreshed_environment_frame.point_lights,
|
||||||
|
first_environment_frame.point_lights
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
refreshed_environment_frame.sounds,
|
||||||
|
first_environment_frame.sounds
|
||||||
|
);
|
||||||
|
let mut reprojected_counts = [0usize; 2];
|
||||||
|
for (sampled, refreshed) in first_environment_frame
|
||||||
|
.primitives
|
||||||
|
.iter()
|
||||||
|
.zip(&refreshed_environment_frame.primitives)
|
||||||
|
{
|
||||||
|
let (
|
||||||
|
EnvironmentPrimitive::Particle {
|
||||||
|
kind: sampled_kind,
|
||||||
|
position: sampled_position,
|
||||||
|
velocity: sampled_velocity,
|
||||||
|
size: sampled_size,
|
||||||
|
screen: sampled_screen,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
EnvironmentPrimitive::Particle {
|
||||||
|
kind,
|
||||||
|
position,
|
||||||
|
velocity,
|
||||||
|
size,
|
||||||
|
screen,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) = (sampled, refreshed)
|
||||||
|
else {
|
||||||
|
panic!("rain and snow fixture contains only precipitation particles");
|
||||||
|
};
|
||||||
|
assert_eq!(kind, sampled_kind);
|
||||||
|
assert_eq!(position, sampled_position);
|
||||||
|
assert_eq!(velocity, sampled_velocity);
|
||||||
|
assert_ne!(screen, sampled_screen);
|
||||||
|
assert_ne!(size, sampled_size);
|
||||||
|
let slot = match kind {
|
||||||
|
PrecipitationKind::Rain => 0,
|
||||||
|
PrecipitationKind::Snow => 1,
|
||||||
|
};
|
||||||
|
reprojected_counts[slot] += 1;
|
||||||
|
}
|
||||||
|
assert!(reprojected_counts.iter().all(|count| *count > 0));
|
||||||
assert_eq!(app.mesh.vertices, b_vertices);
|
assert_eq!(app.mesh.vertices, b_vertices);
|
||||||
assert_eq!(app.shadow_casters[0].triangles, b_triangles);
|
assert_eq!(app.shadow_casters[0].triangles, b_triangles);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -7498,6 +7663,15 @@ mod tests {
|
|||||||
assert_eq!(app.mesh.vertices, initial_mesh.vertices);
|
assert_eq!(app.mesh.vertices, initial_mesh.vertices);
|
||||||
assert_eq!(app.shadow_casters[0].triangles, initial_triangles);
|
assert_eq!(app.shadow_casters[0].triangles, initial_triangles);
|
||||||
assert_eq!(app.shadow_casters[0].sphere, baseline);
|
assert_eq!(app.shadow_casters[0].sphere, baseline);
|
||||||
|
|
||||||
|
let next_sample = app.sample_live_frame_clock_at(start + Duration::from_millis(241));
|
||||||
|
assert_eq!(next_sample.id, SceneSampleId::Live(1));
|
||||||
|
assert_eq!(next_sample.time_ms, 241);
|
||||||
|
let next_update = app
|
||||||
|
.prepare_scene_update(next_sample, [800.0, 600.0])
|
||||||
|
.expect("the next sampled frame adopts mouse input received during retry");
|
||||||
|
assert_ne!(next_update.camera.forward, update.camera.forward);
|
||||||
|
assert_eq!(next_update.camera.forward, controller_after_input.forward());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_test_terrain_lod0_range(terrain: &mut fparkan_terrain_format::LandMeshDocument) {
|
fn set_test_terrain_lod0_range(terrain: &mut fparkan_terrain_format::LandMeshDocument) {
|
||||||
|
|||||||
@@ -926,6 +926,50 @@ pub struct EnvironmentFrame {
|
|||||||
pub sounds: Vec<SoundEvent>,
|
pub sounds: Vec<SoundEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EnvironmentFrame {
|
||||||
|
/// Reprojects cached precipitation for a new camera without advancing the
|
||||||
|
/// weather simulation or replaying its sounds.
|
||||||
|
pub fn reproject_for_camera(&mut self, camera: Camera) {
|
||||||
|
let camera = camera.sanitized();
|
||||||
|
let size = camera.precipitation_size_scalar();
|
||||||
|
let mut rain_index = 0;
|
||||||
|
let mut snow_index = 0;
|
||||||
|
for primitive in &mut self.primitives {
|
||||||
|
let EnvironmentPrimitive::Particle {
|
||||||
|
kind,
|
||||||
|
size: particle_size,
|
||||||
|
screen,
|
||||||
|
..
|
||||||
|
} = primitive
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let index = match kind {
|
||||||
|
PrecipitationKind::Rain => {
|
||||||
|
let index = rain_index;
|
||||||
|
rain_index += 1;
|
||||||
|
*particle_size = size * RAIN_SIZE_FACTOR;
|
||||||
|
index
|
||||||
|
}
|
||||||
|
PrecipitationKind::Snow => {
|
||||||
|
let index = snow_index;
|
||||||
|
snow_index += 1;
|
||||||
|
*particle_size = size * SNOW_SIZE_FACTOR;
|
||||||
|
index
|
||||||
|
}
|
||||||
|
};
|
||||||
|
*screen = screen_billboard(
|
||||||
|
*kind,
|
||||||
|
camera,
|
||||||
|
screen.world_head,
|
||||||
|
screen.world_tail,
|
||||||
|
*particle_size,
|
||||||
|
index,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A validated resource name from the `env_lightning` FXID.
|
/// A validated resource name from the `env_lightning` FXID.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct EffectResource {
|
pub struct EffectResource {
|
||||||
@@ -2598,6 +2642,100 @@ mod tests {
|
|||||||
assert!((camera.precipitation_size_scalar() - 1920.0 / expected_fov).abs() < 1.0e-4);
|
assert!((camera.precipitation_size_scalar() - 1920.0 / expected_fov).abs() < 1.0e-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cached_rain_and_snow_reproject_without_advancing_emitter_state() {
|
||||||
|
let old_camera = Camera::with_projection(
|
||||||
|
[10.0, 20.0, 30.0],
|
||||||
|
[1.0, 0.0, 0.0],
|
||||||
|
[0.0, 1.0, 0.0],
|
||||||
|
[0.0, 0.0, 1.0],
|
||||||
|
0.975,
|
||||||
|
16.0 / 9.0,
|
||||||
|
)
|
||||||
|
.with_viewport([1280.0, 720.0]);
|
||||||
|
let new_camera = Camera::with_projection(
|
||||||
|
old_camera.position,
|
||||||
|
old_camera.forward,
|
||||||
|
old_camera.right,
|
||||||
|
old_camera.up,
|
||||||
|
old_camera.vertical_fov,
|
||||||
|
4.0 / 3.0,
|
||||||
|
)
|
||||||
|
.with_viewport([1024.0, 768.0]);
|
||||||
|
let weather = WeatherInput {
|
||||||
|
rain: WeatherState::new(true, 0.03, [0.7, 0.8, 1.0, 0.6]),
|
||||||
|
snow: WeatherState::new(true, 0.03, [0.9, 0.95, 1.0, 0.8]),
|
||||||
|
..WeatherInput::clear()
|
||||||
|
};
|
||||||
|
let mut system = EnvironmentSystem::new(0xC1_225);
|
||||||
|
let sampled = system.update(0.225, weather.clone(), old_camera);
|
||||||
|
let mut control = system.clone();
|
||||||
|
let mut reprojected = sampled.clone();
|
||||||
|
|
||||||
|
reprojected.reproject_for_camera(new_camera);
|
||||||
|
|
||||||
|
assert_eq!(reprojected.point_lights, sampled.point_lights);
|
||||||
|
assert_eq!(reprojected.sounds, sampled.sounds);
|
||||||
|
|
||||||
|
let mut kind_indices = [0usize; 2];
|
||||||
|
let mut counts = [0usize; 2];
|
||||||
|
for (original, refreshed) in sampled.primitives.iter().zip(&reprojected.primitives) {
|
||||||
|
let (
|
||||||
|
EnvironmentPrimitive::Particle {
|
||||||
|
kind: original_kind,
|
||||||
|
position: original_position,
|
||||||
|
velocity: original_velocity,
|
||||||
|
screen: original_screen,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
EnvironmentPrimitive::Particle {
|
||||||
|
kind,
|
||||||
|
position,
|
||||||
|
velocity,
|
||||||
|
size,
|
||||||
|
screen,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) = (original, refreshed)
|
||||||
|
else {
|
||||||
|
panic!("weather fixture contains only precipitation particles");
|
||||||
|
};
|
||||||
|
assert_eq!(kind, original_kind);
|
||||||
|
assert_eq!(position, original_position);
|
||||||
|
assert_eq!(velocity, original_velocity);
|
||||||
|
let slot = match kind {
|
||||||
|
PrecipitationKind::Rain => 0,
|
||||||
|
PrecipitationKind::Snow => 1,
|
||||||
|
};
|
||||||
|
let index = kind_indices[slot];
|
||||||
|
kind_indices[slot] += 1;
|
||||||
|
counts[slot] += 1;
|
||||||
|
let size_factor = match kind {
|
||||||
|
PrecipitationKind::Rain => RAIN_SIZE_FACTOR,
|
||||||
|
PrecipitationKind::Snow => SNOW_SIZE_FACTOR,
|
||||||
|
};
|
||||||
|
assert_eq!(*size, new_camera.precipitation_size_scalar() * size_factor);
|
||||||
|
assert_eq!(
|
||||||
|
*screen,
|
||||||
|
screen_billboard(
|
||||||
|
*kind,
|
||||||
|
new_camera,
|
||||||
|
original_screen.world_head,
|
||||||
|
original_screen.world_tail,
|
||||||
|
*size,
|
||||||
|
index,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(counts[0] >= 17, "rain fixture exercises populated emitters");
|
||||||
|
assert!(counts[1] >= 17, "snow fixture exercises mirror indices");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
system.update(0.016, weather.clone(), new_camera),
|
||||||
|
control.update(0.016, weather, new_camera)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lightning_fixture_extracts_real_material_and_sound_offsets() {
|
fn lightning_fixture_extracts_real_material_and_sound_offsets() {
|
||||||
let mut bytes = header(3, 0.75);
|
let mut bytes = header(3, 0.75);
|
||||||
|
|||||||
@@ -24,6 +24,15 @@
|
|||||||
затенения, вызывается завершение рендерера, снимается `in_render`, затем
|
затенения, вызывается завершение рендерера, снимается `in_render`, затем
|
||||||
рассылается `end-of-render`.
|
рассылается `end-of-render`.
|
||||||
|
|
||||||
|
При retry кадра после пересоздания swapchain сохраняется прежний pending
|
||||||
|
sample, включая уже вычисленные позы и состояния FX и материалов. Для нового
|
||||||
|
drawable обновляются aspect, проекция и геометрия, зависящая от viewport; pose
|
||||||
|
берётся из pending camera. Новые события управления камерой остаются в
|
||||||
|
input controller и применяются следующим sample. Экранные billboards погоды
|
||||||
|
повторно проецируются из закэшированных мировых головы и хвоста, без второго
|
||||||
|
шага симуляции окружения и повторной отправки звуковых событий. Пауза нулевого
|
||||||
|
drawable не добавляет время.
|
||||||
|
|
||||||
## Элемент отрисовки
|
## Элемент отрисовки
|
||||||
|
|
||||||
Подготовленный элемент отрисовки содержит:
|
Подготовленный элемент отрисовки содержит:
|
||||||
|
|||||||
Reference in new issue
Block a user