feat(fx): animate native point light color and radius jitter
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s
This commit is contained in:
6 files changed
+222
-22
No files matched your search
@@ -29,8 +29,9 @@ use fparkan_fx::ambient_op3::{
|
|||||||
use fparkan_fx::ambient_sound::{Op2SoundCommand, Op2SoundState};
|
use fparkan_fx::ambient_sound::{Op2SoundCommand, Op2SoundState};
|
||||||
use fparkan_fx::atmosphere::{AtmosphereFrame, TypedAtmosphere};
|
use fparkan_fx::atmosphere::{AtmosphereFrame, TypedAtmosphere};
|
||||||
use fparkan_fx::environment::{
|
use fparkan_fx::environment::{
|
||||||
decode_env_lightning_fxid, evaluate_op1_point_light_with_basis, Camera as FxCamera,
|
decode_env_lightning_fxid, evaluate_op1_point_light_with_basis_and_jitter, Camera as FxCamera,
|
||||||
EnvironmentFrame, EnvironmentPrimitive, EnvironmentSystem, PrecipitationKind, ScreenBillboard,
|
EnvironmentFrame, EnvironmentPrimitive, EnvironmentSystem, Op1JitterState, PrecipitationKind,
|
||||||
|
ScreenBillboard,
|
||||||
};
|
};
|
||||||
use fparkan_fx::fx_visual::{evaluate_f_gener_ball, evaluate_f_gener_ball_mesh};
|
use fparkan_fx::fx_visual::{evaluate_f_gener_ball, evaluate_f_gener_ball_mesh};
|
||||||
use fparkan_fx::op7::{
|
use fparkan_fx::op7::{
|
||||||
@@ -5314,6 +5315,7 @@ fn control_effect_world_from_component_pose_with_random_shift(
|
|||||||
|
|
||||||
fn update_control_point_lights(
|
fn update_control_point_lights(
|
||||||
slots: &mut [ControlEffectSlot],
|
slots: &mut [ControlEffectSlot],
|
||||||
|
jitter: &mut Op1JitterState,
|
||||||
component_pose_buffers: &[Vec<Option<NodePoseBuffer>>],
|
component_pose_buffers: &[Vec<Option<NodePoseBuffer>>],
|
||||||
component_mount_poses: &[Vec<Option<VulkanNodePose>>],
|
component_mount_poses: &[Vec<Option<VulkanNodePose>>],
|
||||||
random_samples: &HashMap<ControlEffectInstanceKey, ControlEffectFrameSample>,
|
random_samples: &HashMap<ControlEffectInstanceKey, ControlEffectFrameSample>,
|
||||||
@@ -5352,8 +5354,13 @@ fn update_control_point_lights(
|
|||||||
[matrix[0][1], matrix[1][1], matrix[2][1]],
|
[matrix[0][1], matrix[1][1], matrix[2][1]],
|
||||||
[matrix[0][2], matrix[1][2], matrix[2][2]],
|
[matrix[0][2], matrix[1][2], matrix[2][2]],
|
||||||
];
|
];
|
||||||
let Some(frame) = evaluate_op1_point_light_with_basis(&slot.command, phase, origin, basis)
|
let Some(frame) = evaluate_op1_point_light_with_basis_and_jitter(
|
||||||
else {
|
&slot.command,
|
||||||
|
phase,
|
||||||
|
origin,
|
||||||
|
basis,
|
||||||
|
jitter,
|
||||||
|
) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if !frame.active || frame.light_selector != 1 {
|
if !frame.active || frame.light_selector != 1 {
|
||||||
@@ -5942,7 +5949,12 @@ fn ambient_effect_op1_supported(command: &FxCommand) -> bool {
|
|||||||
else {
|
else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
command.raw_body.len() >= 132 && matches!(effect_type, 1 | 2 | 5 | 6 | 7)
|
matches!(effect_type, 1 | 2 | 5 | 6 | 7)
|
||||||
|
&& command.raw_body.get(4..132).is_some_and(|bytes| {
|
||||||
|
bytes.chunks_exact(4).all(|word| {
|
||||||
|
f32::from_le_bytes(word.try_into().expect("four-byte Op1 field")).is_finite()
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ambient_effect_pool_capacity(command: &FxCommand) -> Option<usize> {
|
fn ambient_effect_pool_capacity(command: &FxCommand) -> Option<usize> {
|
||||||
@@ -9413,6 +9425,7 @@ fn run_static_vulkan_mode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct StaticVulkanApp {
|
struct StaticVulkanApp {
|
||||||
|
op1_jitter: Op1JitterState,
|
||||||
fixed_water_clock_ms: Option<u32>,
|
fixed_water_clock_ms: Option<u32>,
|
||||||
mesh: VulkanStaticMesh,
|
mesh: VulkanStaticMesh,
|
||||||
dynamic_vertex_start: usize,
|
dynamic_vertex_start: usize,
|
||||||
@@ -9561,6 +9574,8 @@ impl StaticVulkanApp {
|
|||||||
shadow_last_evidence: ShadowFrameEvidence::default(),
|
shadow_last_evidence: ShadowFrameEvidence::default(),
|
||||||
last_environment_control_clock_ms: 0,
|
last_environment_control_clock_ms: 0,
|
||||||
control_point_lights_diagnostic_reported: false,
|
control_point_lights_diagnostic_reported: false,
|
||||||
|
// shortcut: native initialization and cross-effect RNG ordering remain for G.
|
||||||
|
op1_jitter: Op1JitterState::new(1, 1),
|
||||||
simulation_last_tick: Instant::now(),
|
simulation_last_tick: Instant::now(),
|
||||||
live_clock_paused_for_zero_extent: false,
|
live_clock_paused_for_zero_extent: false,
|
||||||
simulation_elapsed: Duration::ZERO,
|
simulation_elapsed: Duration::ZERO,
|
||||||
@@ -9616,7 +9631,7 @@ impl StaticVulkanApp {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.count();
|
.count();
|
||||||
eprintln!("render_coverage control_op1_jitter_unsupported={randomized_lights} (using authored color and radius; native RNG sampling pending)");
|
eprintln!("render_coverage control_op1_rng_schedule_approx={randomized_lights} (native color/radius jitter; fixed preview seeds and cross-effect RNG ordering unverified)");
|
||||||
eprintln!("render_coverage control_op2_supported={} control_op2_unsupported={} (unsupported timing, backend mode or parameters are skipped)", self.control_sound_slots.len(), self.unsupported_control_sounds);
|
eprintln!("render_coverage control_op2_supported={} control_op2_unsupported={} (unsupported timing, backend mode or parameters are skipped)", self.control_sound_slots.len(), self.unsupported_control_sounds);
|
||||||
eprintln!("render_coverage fog_pass_eligibility_approx={} (global world fog and horizon RGB; native shade-state/color verification pending)", self.world_range_indices.len());
|
eprintln!("render_coverage fog_pass_eligibility_approx={} (global world fog and horizon RGB; native shade-state/color verification pending)", self.world_range_indices.len());
|
||||||
for &range_index in &self.world_range_indices {
|
for &range_index in &self.world_range_indices {
|
||||||
@@ -10031,6 +10046,7 @@ impl StaticVulkanApp {
|
|||||||
);
|
);
|
||||||
let control_point_lights = update_control_point_lights(
|
let control_point_lights = update_control_point_lights(
|
||||||
&mut self.control_point_light_slots,
|
&mut self.control_point_light_slots,
|
||||||
|
&mut self.op1_jitter,
|
||||||
&self.component_pose_buffers,
|
&self.component_pose_buffers,
|
||||||
&self.component_mount_poses,
|
&self.component_mount_poses,
|
||||||
&control_effect_random_samples,
|
&control_effect_random_samples,
|
||||||
@@ -13875,6 +13891,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn live_cpu_updates_sample_materials_effect_rng_and_environment_without_renderer() {
|
fn live_cpu_updates_sample_materials_effect_rng_and_environment_without_renderer() {
|
||||||
let (mut app, key) = clock_test_app(None);
|
let (mut app, key) = clock_test_app(None);
|
||||||
|
let initial_op1_jitter = app.op1_jitter;
|
||||||
app.render_scene_update()
|
app.render_scene_update()
|
||||||
.expect("an early redraw without a packet is a no-op");
|
.expect("an early redraw without a packet is a no-op");
|
||||||
assert!(app.pending_scene_update.is_none());
|
assert!(app.pending_scene_update.is_none());
|
||||||
@@ -13901,6 +13918,8 @@ mod tests {
|
|||||||
.vertices
|
.vertices
|
||||||
.is_empty());
|
.is_empty());
|
||||||
let first_random = control_random_snapshot(&app.control_effect_instance_states[&key]);
|
let first_random = control_random_snapshot(&app.control_effect_instance_states[&key]);
|
||||||
|
let first_op1_jitter = app.op1_jitter;
|
||||||
|
assert_ne!(first_op1_jitter, initial_op1_jitter);
|
||||||
assert_eq!(first_random.3, Some(SceneSampleId::Live(0)));
|
assert_eq!(first_random.3, Some(SceneSampleId::Live(0)));
|
||||||
assert_ne!(first_random.0, 0);
|
assert_ne!(first_random.0, 0);
|
||||||
assert_ne!(first_random.1, 0);
|
assert_ne!(first_random.1, 0);
|
||||||
@@ -13921,6 +13940,10 @@ mod tests {
|
|||||||
first_environment_time
|
first_environment_time
|
||||||
);
|
);
|
||||||
assert_eq!(app.mesh.vertices, first_vertices);
|
assert_eq!(app.mesh.vertices, first_vertices);
|
||||||
|
assert_eq!(
|
||||||
|
app.op1_jitter, first_op1_jitter,
|
||||||
|
"redraw must not resample light jitter"
|
||||||
|
);
|
||||||
assert_eq!(app.simulation_update_generation, 1);
|
assert_eq!(app.simulation_update_generation, 1);
|
||||||
|
|
||||||
let mut next = app.sample_frame_clock();
|
let mut next = app.sample_frame_clock();
|
||||||
@@ -13939,6 +13962,7 @@ mod tests {
|
|||||||
first_random
|
first_random
|
||||||
);
|
);
|
||||||
assert!(app.environment.as_ref().unwrap().time_seconds > first_environment_time);
|
assert!(app.environment.as_ref().unwrap().time_seconds > first_environment_time);
|
||||||
|
assert_ne!(app.op1_jitter, first_op1_jitter);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.pending_scene_update
|
app.pending_scene_update
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -16757,6 +16781,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert!(update_control_point_lights(
|
assert!(update_control_point_lights(
|
||||||
&mut [inactive],
|
&mut [inactive],
|
||||||
|
&mut Op1JitterState::new(1, 1),
|
||||||
&poses,
|
&poses,
|
||||||
&mounts,
|
&mounts,
|
||||||
&random_samples,
|
&random_samples,
|
||||||
@@ -16773,6 +16798,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let lights = update_control_point_lights(
|
let lights = update_control_point_lights(
|
||||||
std::slice::from_mut(&mut slot),
|
std::slice::from_mut(&mut slot),
|
||||||
|
&mut Op1JitterState::new(1, 1),
|
||||||
&poses,
|
&poses,
|
||||||
&mounts,
|
&mounts,
|
||||||
&random_samples,
|
&random_samples,
|
||||||
@@ -16782,7 +16808,7 @@ mod tests {
|
|||||||
let (light, flags) = lights[0];
|
let (light, flags) = lights[0];
|
||||||
assert_eq!(light.position, [13.0, 22.0, 33.0]);
|
assert_eq!(light.position, [13.0, 22.0, 33.0]);
|
||||||
assert_eq!(light.rgb, [0.25, 0.5, 0.75]);
|
assert_eq!(light.rgb, [0.25, 0.5, 0.75]);
|
||||||
assert_eq!(light.range, 15.0);
|
assert_eq!(light.range, 14.500397);
|
||||||
assert_eq!(light.coefficients, [1.0, 0.5, 0.25]);
|
assert_eq!(light.coefficients, [1.0, 0.5, 0.25]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
light.owner_scope, 1,
|
light.owner_scope, 1,
|
||||||
|
|||||||
@@ -615,7 +615,7 @@ pub struct PointLightFrame {
|
|||||||
pub range: f32,
|
pub range: f32,
|
||||||
/// Native attenuation coefficients `(a0, a1, a2)`.
|
/// Native attenuation coefficients `(a0, a1, a2)`.
|
||||||
pub attenuation: [f32; 3],
|
pub attenuation: [f32; 3],
|
||||||
/// Radius jitter amplitude at body offset 116; retained but not sampled yet.
|
/// Radius jitter amplitude at body offset 116 (legacy field name).
|
||||||
pub effect_factor: f32,
|
pub effect_factor: f32,
|
||||||
/// Whether the opcode 1 command is active at `command_time`.
|
/// Whether the opcode 1 command is active at `command_time`.
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
@@ -627,12 +627,59 @@ pub struct PointLightFrame {
|
|||||||
/// `command_time` is resolved by the caller; FX header duration and lifecycle
|
/// `command_time` is resolved by the caller; FX header duration and lifecycle
|
||||||
/// remain with that caller. Native Op1 scales the radius by the transformed
|
/// remain with that caller. Native Op1 scales the radius by the transformed
|
||||||
/// direction length and replaces a non-positive result with 0.01.
|
/// direction length and replaces a non-positive result with 0.01.
|
||||||
|
/// This samples the authored curves; Control FX use
|
||||||
|
/// [`evaluate_op1_point_light_with_basis_and_jitter`] for stochastic additions.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn evaluate_op1_point_light_with_basis(
|
pub fn evaluate_op1_point_light_with_basis(
|
||||||
command: &FxCommand,
|
command: &FxCommand,
|
||||||
command_time: f32,
|
command_time: f32,
|
||||||
origin: [f32; 3],
|
origin: [f32; 3],
|
||||||
basis: [[f32; 3]; 3],
|
basis: [[f32; 3]; 3],
|
||||||
|
) -> Option<PointLightFrame> {
|
||||||
|
evaluate_op1_point_light(command, command_time, origin, basis, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The two native streams used by Op1 color and radius jitter.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct Op1JitterState {
|
||||||
|
general: u32,
|
||||||
|
light: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Op1JitterState {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(general: u32, light: u32) -> Self {
|
||||||
|
Self { general, light }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample(&mut self) -> ([u16; 4], u16) {
|
||||||
|
let alpha = crate::advance_native_fx_random_state(&mut self.light);
|
||||||
|
let blue = crate::advance_native_fx_random_state(&mut self.light);
|
||||||
|
let green = crate::advance_native_fx_random_state(&mut self.light);
|
||||||
|
let red = crate::advance_native_fx_random_state(&mut self.general);
|
||||||
|
let radius = crate::advance_native_fx_random_state(&mut self.light);
|
||||||
|
([red, green, blue, alpha], radius)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the native signed uniform jitter before transforming the light radius.
|
||||||
|
/// Streams advance even outside the command interval and with zero amplitudes.
|
||||||
|
pub fn evaluate_op1_point_light_with_basis_and_jitter(
|
||||||
|
command: &FxCommand,
|
||||||
|
command_time: f32,
|
||||||
|
origin: [f32; 3],
|
||||||
|
basis: [[f32; 3]; 3],
|
||||||
|
jitter: &mut Op1JitterState,
|
||||||
|
) -> Option<PointLightFrame> {
|
||||||
|
evaluate_op1_point_light(command, command_time, origin, basis, Some(jitter))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_op1_point_light(
|
||||||
|
command: &FxCommand,
|
||||||
|
command_time: f32,
|
||||||
|
origin: [f32; 3],
|
||||||
|
basis: [[f32; 3]; 3],
|
||||||
|
jitter: Option<&mut Op1JitterState>,
|
||||||
) -> Option<PointLightFrame> {
|
) -> Option<PointLightFrame> {
|
||||||
if command.opcode != FxOpcode::Op1
|
if command.opcode != FxOpcode::Op1
|
||||||
|| !command_time.is_finite()
|
|| !command_time.is_finite()
|
||||||
@@ -653,14 +700,27 @@ pub fn evaluate_op1_point_light_with_basis(
|
|||||||
|| FLOAT_OFFSETS
|
|| FLOAT_OFFSETS
|
||||||
.iter()
|
.iter()
|
||||||
.any(|&offset| read_f32(body, offset).is_none_or(|value| !value.is_finite()))
|
.any(|&offset| read_f32(body, offset).is_none_or(|value| !value.is_finite()))
|
||||||
|
|| (jitter.is_some()
|
||||||
|
&& (92..108)
|
||||||
|
.step_by(4)
|
||||||
|
.any(|offset| read_f32(body, offset).is_none_or(|value| !value.is_finite())))
|
||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
let jitter = jitter.map(Op1JitterState::sample);
|
||||||
let normalized_time = native_opcode1_interval(body, command_time)?;
|
let normalized_time = native_opcode1_interval(body, command_time)?;
|
||||||
if !normalized_time.is_finite() {
|
if !normalized_time.is_finite() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let sample = sample_op1_point_light(body, normalized_time)?;
|
let mut sample = sample_op1_point_light(body, normalized_time)?;
|
||||||
|
if let Some((color, radius)) = jitter {
|
||||||
|
let centered =
|
||||||
|
|word, amplitude| f32::from(word) * (amplitude * (1.0 / 65_536.0)) - amplitude * 0.5;
|
||||||
|
for (axis, word) in color.into_iter().enumerate() {
|
||||||
|
sample.color[axis] += centered(word, read_f32(body, 92 + axis * 4)?);
|
||||||
|
}
|
||||||
|
sample.range += centered(radius, read_f32(body, 116)?);
|
||||||
|
}
|
||||||
let (light_selector, native_flags) = opcode1_light_manager_mapping(sample.effect_type)?;
|
let (light_selector, native_flags) = opcode1_light_manager_mapping(sample.effect_type)?;
|
||||||
if light_selector != 1 {
|
if light_selector != 1 {
|
||||||
return None;
|
return None;
|
||||||
@@ -681,8 +741,6 @@ pub fn evaluate_op1_point_light_with_basis(
|
|||||||
.map(|component| component * component)
|
.map(|component| component * component)
|
||||||
.sum::<f32>()
|
.sum::<f32>()
|
||||||
.sqrt();
|
.sqrt();
|
||||||
// shortcut: Op1 color/radius jitter is omitted; add native RNG sampling
|
|
||||||
// when the reported randomized Control lights are handled in phase R4.
|
|
||||||
let scaled_range = sample.range * direction_length;
|
let scaled_range = sample.range * direction_length;
|
||||||
if !direction_length.is_finite() || !scaled_range.is_finite() {
|
if !direction_length.is_finite() || !scaled_range.is_finite() {
|
||||||
return None;
|
return None;
|
||||||
@@ -2981,6 +3039,94 @@ mod tests {
|
|||||||
assert_eq!(sample.range, 0.01);
|
assert_eq!(sample.range, 0.01);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn opcode1_jitter_matches_native_color_words_and_advances_outside_the_interval() {
|
||||||
|
let mut command = op1_test_command(7, 0.0, 1.0);
|
||||||
|
let mut body = command.raw_body.to_vec();
|
||||||
|
for (start, values) in [
|
||||||
|
(60, [0.25_f32, 0.5, 0.75, 1.0]),
|
||||||
|
(76, [0.75, 1.0, 1.25, 1.5]),
|
||||||
|
(92, [0.5, 1.0, 2.0, 4.0]),
|
||||||
|
] {
|
||||||
|
body[start..start + 16].copy_from_slice(&values.map(f32::to_le_bytes).concat());
|
||||||
|
}
|
||||||
|
body[48..60].copy_from_slice(&[1.0_f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
|
||||||
|
body[116..120].copy_from_slice(&4.0_f32.to_le_bytes());
|
||||||
|
command.raw_body = Arc::from(body);
|
||||||
|
let mut state = super::Op1JitterState::new(0x12345678, 0x89abcdef);
|
||||||
|
// Effect.dll+F6E0, CW007F, identity owner; native FastProc length is approximate.
|
||||||
|
let native = [
|
||||||
|
(
|
||||||
|
[0x3ef7de00, 0x3f310300, 0x3f123400, 0x3ef50000],
|
||||||
|
0x4150a0fb,
|
||||||
|
0xb7debec4,
|
||||||
|
0xa29eea1f,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[0x3f08dc80, 0x3ec15400, 0x3fd95800, 0xbe011000],
|
||||||
|
0x417a34ee,
|
||||||
|
0x91b9ca56,
|
||||||
|
0xa8f3b8a6,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[0x3f06e480, 0x3f190600, 0x3f885b00, 0x3fcb8c00],
|
||||||
|
0x4181f650,
|
||||||
|
0x4dc90515,
|
||||||
|
0x2fd33350,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[0x3f508380, 0x3f2a5100, 0x40099880, 0x3f8d3400],
|
||||||
|
0x41c31747,
|
||||||
|
0x610747e3,
|
||||||
|
0xf8ebfdc3,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[0x3f2f2100, 0x3f56ed00, 0x3f0c9c00, 0x3f9e3000],
|
||||||
|
0x417d7bd4,
|
||||||
|
0xde42eec1,
|
||||||
|
0xb60ffd79,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (phase, (color, radius, general, light)) in
|
||||||
|
[0.25, 0.5, 0.75, 1.25, 0.5].into_iter().zip(native)
|
||||||
|
{
|
||||||
|
let frame = super::evaluate_op1_point_light_with_basis_and_jitter(
|
||||||
|
&command,
|
||||||
|
phase,
|
||||||
|
[0.0; 3],
|
||||||
|
identity_basis(),
|
||||||
|
&mut state,
|
||||||
|
);
|
||||||
|
if phase > 1.0 {
|
||||||
|
assert!(frame.is_none());
|
||||||
|
} else {
|
||||||
|
let frame = frame.unwrap();
|
||||||
|
assert_eq!(frame.color.map(f32::to_bits), color);
|
||||||
|
let native_radius = f32::from_bits(radius);
|
||||||
|
assert!((frame.range - native_radius).abs() < native_radius * 0.00013);
|
||||||
|
}
|
||||||
|
assert_eq!((state.general, state.light), (general, light));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn opcode1_jitter_rejects_nonfinite_amplitudes_without_consuming_random_state() {
|
||||||
|
let mut command = op1_test_command(7, 0.0, 1.0);
|
||||||
|
let mut body = command.raw_body.to_vec();
|
||||||
|
body[92..96].copy_from_slice(&f32::NAN.to_le_bytes());
|
||||||
|
command.raw_body = Arc::from(body);
|
||||||
|
let mut state = super::Op1JitterState::new(123, 456);
|
||||||
|
assert!(super::evaluate_op1_point_light_with_basis_and_jitter(
|
||||||
|
&command,
|
||||||
|
0.5,
|
||||||
|
[0.0; 3],
|
||||||
|
identity_basis(),
|
||||||
|
&mut state
|
||||||
|
)
|
||||||
|
.is_none());
|
||||||
|
assert_eq!((state.general, state.light), (123, 456));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generic_opcode1_keeps_zero_direction_zero() {
|
fn generic_opcode1_keeps_zero_direction_zero() {
|
||||||
let mut command = op1_test_command(5, 0.0, 1.0);
|
let mut command = op1_test_command(5, 0.0, 1.0);
|
||||||
|
|||||||
@@ -84,12 +84,13 @@ Raw queue clock задаёт фазу волн отдельно от време
|
|||||||
|
|
||||||
## FXID field-level semantics
|
## FXID field-level semantics
|
||||||
|
|
||||||
Control Op1 пока использует цвет и радиус без случайных добавок команды;
|
Control Op1 воспроизводит добавки к RGBA и радиусу двумя сохраняемыми RNG-потоками.
|
||||||
это явное приближение считается в `render_coverage control_op1_jitter_unsupported`.
|
Seeds просмотра `1/1` и общий порядок других FX ещё не совпадают с оригиналом;
|
||||||
|
это явное приближение считается в `render_coverage control_op1_rng_schedule_approx`.
|
||||||
Масштаб радиуса длиной преобразованного направления и нижняя замена 0,01 восстановлены.
|
Масштаб радиуса длиной преобразованного направления и нижняя замена 0,01 восстановлены.
|
||||||
Локальный флаг `0x80000000` сохраняет свет на MSH владельца, исключая его
|
Локальный флаг `0x80000000` сохраняет свет на MSH владельца, исключая его
|
||||||
из общего света рельефа. Мерцание жёлтого источника над заводом ещё использует
|
из общего света рельефа. Мерцание жёлтого источника над заводом подключено;
|
||||||
авторские цвет и радиус; порядок общего native RNG пока не воспроизводится.
|
конкретная последовательность пока не является эталоном совпадения во времени.
|
||||||
|
|
||||||
Control Op2 воспроизводит modes 0/2; modes 1/3, body+76 и неподдержанные
|
Control Op2 воспроизводит modes 0/2; modes 1/3, body+76 и неподдержанные
|
||||||
time modes/привязки считаются как `render_coverage control_op2_unsupported`.
|
time modes/привязки считаются как `render_coverage control_op2_unsupported`.
|
||||||
|
|||||||
@@ -165,13 +165,32 @@ opcode 1; обе границы включены, а коэффициент вы
|
|||||||
|
|
||||||
Позиция, направление, цвет и range интерполируются по концам команды.
|
Позиция, направление, цвет и range интерполируются по концам команды.
|
||||||
Позиция и направление переводятся через основу родителя; направление после
|
Позиция и направление переводятся через основу родителя; направление после
|
||||||
этого нормализуется, а нулевой вектор остаётся нулевым. Range сохраняет
|
этого нормализуется, а нулевой вектор остаётся нулевым. Радиус умножается на
|
||||||
авторское значение без ограничения снизу. Типы Op1 `1`, `2`, `5`, `6` и `7`
|
длину преобразованного направления; неположительный результат заменяется на
|
||||||
|
`0,01`. Типы Op1 `1`, `2`, `5`, `6` и `7`
|
||||||
выбирают point-light selector `1`; их native flags сохраняются. Типы `3` и
|
выбирают point-light selector `1`; их native flags сохраняются. Типы `3` и
|
||||||
`4` выбирают другие классы записей LightManager и не попадают в generic список
|
`4` выбирают другие классы записей LightManager и не попадают в generic список
|
||||||
точечного света. У `LightningEffect` выборка тех же полей использует отдельные
|
точечного света. У `LightningEffect` выборка тех же полей использует отдельные
|
||||||
ограничение по длительности FXID и правила безопасной обработки.
|
ограничение по длительности FXID и правила безопасной обработки.
|
||||||
|
|
||||||
|
Generic Control Op1 добавляет к интерполированным RGBA и радиусу равномерный
|
||||||
|
шум `U × (amplitude / 65536) − amplitude / 2`, где `U` — очередное 16-битное
|
||||||
|
слово native FX-генератора. Цветовые амплитуды лежат в body `92..108`,
|
||||||
|
амплитуда радиуса — в `116..120`. Радиус с этой добавкой затем умножается на
|
||||||
|
длину преобразованного направления и ограничивается снизу `0,01`.
|
||||||
|
В [эталонной Effect.dll](original-binaries.md) RVA `0xF6E0` расходует слова
|
||||||
|
в порядке A, B, G, R, радиус: R использует общий поток DLL, остальные четыре
|
||||||
|
выборки — отдельный поток Op1. Нулевая амплитуда и выход за интервал команды
|
||||||
|
не отменяют расход этих слов. Четыре активные выборки RGBA и состояния обоих
|
||||||
|
генераторов после пяти вызовов совпали побитно с DLL при CW `0x007F`;
|
||||||
|
сравнение радиуса учитывает известное приближение native FastProc длины.
|
||||||
|
|
||||||
|
Приложение сохраняет состояния между обновлениями сцены, не пересчитывая их
|
||||||
|
при повторной отправке кадра. Начальные seeds просмотра равны `1/1`;
|
||||||
|
инициализация и общий порядок выборок остальных FX ещё не воспроизведены.
|
||||||
|
Эта граница учитывается в `control_op1_rng_schedule_approx`: мерцание
|
||||||
|
подключено, но его конкретная последовательность ещё не служит эталоном parity.
|
||||||
|
|
||||||
## Молния и звуковые события
|
## Молния и звуковые события
|
||||||
|
|
||||||
`decode_env_lightning_fxid` извлекает из FXID длительность, ссылки opcode 3 и
|
`decode_env_lightning_fxid` извлекает из FXID длительность, ссылки opcode 3 и
|
||||||
|
|||||||
@@ -122,9 +122,11 @@ RVA `0xF6E0`, преобразует направление текущей ма
|
|||||||
завода локальный радиус 7 и длина `0x409B1F91` дали радиус `0x4207BB9F`.
|
завода локальный радиус 7 и длина `0x409B1F91` дали радиус `0x4207BB9F`.
|
||||||
Это же значение проверяется Rust-тестом общего Op1 evaluator.
|
Это же значение проверяется Rust-тестом общего Op1 evaluator.
|
||||||
|
|
||||||
Случайные добавки цвета и радиуса ещё не воспроизводятся; приложение сообщает
|
Случайные добавки к RGBA и радиусу воспроизводятся двумя сохраняемыми потоками;
|
||||||
число таких Control-команд как `control_op1_jitter_unsupported` и использует
|
формула и расход слов внутри Op1 сверены исполнением DLL. Инициализация
|
||||||
их авторские значения. У синего источника в проверенном захвате добавки нулевые.
|
и общий порядок других FX ещё приближённые: такие команды считаются в
|
||||||
|
`control_op1_rng_schedule_approx`. У синего источника в проверенном захвате
|
||||||
|
амплитуды нулевые; у жёлтого света над трубой они меняют освещение крыши.
|
||||||
|
|
||||||
Флаг источника `0x80000000` отделяет локальный свет объекта от общего света
|
Флаг источника `0x80000000` отделяет локальный свет объекта от общего света
|
||||||
мира. В эталонной `Terrain.dll` (SHA-256
|
мира. В эталонной `Terrain.dll` (SHA-256
|
||||||
|
|||||||
@@ -864,8 +864,14 @@ shadows. Радиус источника масштабируется длино
|
|||||||
направления до нормализации. Например, локальный радиус 7 при масштабе узла
|
направления до нормализации. Например, локальный радиус 7 при масштабе узла
|
||||||
около 4,85 освещает область радиусом около 34. Потеря этого множителя оставляла
|
около 4,85 освещает область радиусом около 34. Потеря этого множителя оставляла
|
||||||
синий свет завода почти незаметным. Неположительный итог заменяется на 0,01.
|
синий свет завода почти незаметным. Неположительный итог заменяется на 0,01.
|
||||||
Случайные добавки к цвету и радиусу пока явно считаются в
|
К плавным кривым добавляется мерцание: каждый канал RGBA и радиус получают
|
||||||
`render_coverage control_op1_jitter_unsupported`.
|
случайную поправку от минус половины авторской амплитуды до плюс половины.
|
||||||
|
Она не накапливается, а заново прибавляется к текущей кривой. Два потока
|
||||||
|
случайных чисел сохраняются между обновлениями сцены; даже погасший на время
|
||||||
|
свет расходует их. Повторная отправка кадра не меняет уже выбранный свет.
|
||||||
|
Формула и порядок выборок внутри команды сверены с исполнением оригинала.
|
||||||
|
Начальные значения просмотра и общий порядок вызовов других FX пока
|
||||||
|
приблизительные и считаются в `render_coverage control_op1_rng_schedule_approx`.
|
||||||
|
|
||||||
Эффект может одновременно рисовать поверхность и издавать звук. Для Op2
|
Эффект может одновременно рисовать поверхность и издавать звук. Для Op2
|
||||||
приложение создаёт отдельный звуковой источник у каждого экземпляра команды:
|
приложение создаёт отдельный звуковой источник у каждого экземпляра команды:
|
||||||
|
|||||||
Reference in new issue
Block a user