fix(render): scale Control light radius with emitter transform
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:
5 files changed
+92
-23
No files matched your search
@@ -9440,6 +9440,20 @@ impl StaticVulkanApp {
|
||||
})
|
||||
.count();
|
||||
eprintln!("render_coverage portal_fade_packet_selection_approx={approximate_portals} (first indexed vertex; native CPU clipping selection pending)");
|
||||
let randomized_lights = self
|
||||
.control_point_light_slots
|
||||
.iter()
|
||||
.filter(|slot| {
|
||||
[92..108, 116..120].into_iter().any(|range| {
|
||||
slot.command.raw_body.get(range).is_some_and(|bytes| {
|
||||
bytes
|
||||
.chunks_exact(4)
|
||||
.any(|word| f32::from_le_bytes(word.try_into().unwrap()) != 0.0)
|
||||
})
|
||||
})
|
||||
})
|
||||
.count();
|
||||
eprintln!("render_coverage control_op1_jitter_unsupported={randomized_lights} (using authored color and radius; native RNG sampling pending)");
|
||||
for &range_index in &self.world_range_indices {
|
||||
let range = self
|
||||
.mesh
|
||||
|
||||
@@ -590,7 +590,7 @@ impl LightningBounds {
|
||||
/// Effect.dll's opcode 1 handler (`1000F6E0`) updates a LightManager record.
|
||||
/// The A and B groups are the submitted position and direction, C is the
|
||||
/// four-component HDR light color, the scalar pair at body offsets 108 and
|
||||
/// 112 is the light range, and body offsets 120, 124, and 128 are the native
|
||||
/// 112 is the local light radius, and body offsets 120, 124, and 128 are the native
|
||||
/// attenuation coefficients. These values do not describe lightning mesh
|
||||
/// geometry or texture coordinates.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
@@ -615,8 +615,7 @@ pub struct PointLightFrame {
|
||||
pub range: f32,
|
||||
/// Native attenuation coefficients `(a0, a1, a2)`.
|
||||
pub attenuation: [f32; 3],
|
||||
/// The scalar at body offset 116. Its native role is an effect factor,
|
||||
/// not alpha or a UV parameter, so it is preserved without reinterpretation.
|
||||
/// Radius jitter amplitude at body offset 116; retained but not sampled yet.
|
||||
pub effect_factor: f32,
|
||||
/// Whether the opcode 1 command is active at `command_time`.
|
||||
pub active: bool,
|
||||
@@ -626,8 +625,8 @@ pub struct PointLightFrame {
|
||||
/// applies a parent basis to its local position and direction. `basis` stores
|
||||
/// the world-space vectors corresponding to local +X, +Y, and +Z.
|
||||
/// `command_time` is resolved by the caller; FX header duration and lifecycle
|
||||
/// remain with that caller. The interpolated range is retained as authored;
|
||||
/// native Op1 does not clamp it to zero.
|
||||
/// remain with that caller. Native Op1 scales the radius by the transformed
|
||||
/// direction length and replaces a non-positive result with 0.01.
|
||||
#[must_use]
|
||||
pub fn evaluate_op1_point_light_with_basis(
|
||||
command: &FxCommand,
|
||||
@@ -677,7 +676,22 @@ pub fn evaluate_op1_point_light_with_basis(
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let direction = normalize_op1_direction(transformed_direction);
|
||||
let direction_length = transformed_direction
|
||||
.into_iter()
|
||||
.map(|component| component * component)
|
||||
.sum::<f32>()
|
||||
.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;
|
||||
if !direction_length.is_finite() || !scaled_range.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let direction = if direction_length > 0.0 {
|
||||
transformed_direction.map(|component| component / direction_length)
|
||||
} else {
|
||||
[0.0; 3]
|
||||
};
|
||||
|
||||
Some(PointLightFrame {
|
||||
effect_type: sample.effect_type,
|
||||
@@ -688,7 +702,11 @@ pub fn evaluate_op1_point_light_with_basis(
|
||||
position,
|
||||
direction,
|
||||
color: sample.color,
|
||||
range: sample.range,
|
||||
range: if scaled_range <= 0.0 {
|
||||
0.01
|
||||
} else {
|
||||
scaled_range
|
||||
},
|
||||
attenuation: sample.attenuation,
|
||||
effect_factor: sample.effect_factor,
|
||||
active: true,
|
||||
@@ -722,19 +740,6 @@ fn sample_op1_point_light(body: &[u8], normalized_time: f32) -> Option<Op1PointL
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_op1_direction(direction: [f32; 3]) -> [f32; 3] {
|
||||
// Native FastProc normalization preserves a degenerate vector as zero;
|
||||
// this path must not invent a fallback axis.
|
||||
let length =
|
||||
(direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2])
|
||||
.sqrt();
|
||||
if length.is_finite() && length > 0.0 {
|
||||
direction.map(|component| component / length)
|
||||
} else {
|
||||
[0.0; 3]
|
||||
}
|
||||
}
|
||||
|
||||
fn lightning_point_light_direction(local_direction: [f32; 3], basis: [[f32; 3]; 3]) -> [f32; 3] {
|
||||
// Preserve the pre-existing Lightning consumer fallback, which is
|
||||
// separate from the generic native Op1 behavior.
|
||||
@@ -2964,7 +2969,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generic_opcode1_preserves_negative_native_range() {
|
||||
fn generic_opcode1_clamps_non_positive_scaled_range() {
|
||||
let mut command = op1_test_command(5, 0.0, 1.0);
|
||||
let mut body = command.raw_body.to_vec();
|
||||
body[108..112].copy_from_slice(&(-2.0_f32).to_le_bytes());
|
||||
@@ -2973,7 +2978,7 @@ mod tests {
|
||||
|
||||
let sample = evaluate_op1_point_light_identity(&command, 0.5, [0.0; 3])
|
||||
.expect("native Op1 sample with a negative range");
|
||||
assert_eq!(sample.range, -3.0);
|
||||
assert_eq!(sample.range, 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2986,6 +2991,31 @@ mod tests {
|
||||
let sample = evaluate_op1_point_light_identity(&command, 0.5, [0.0; 3])
|
||||
.expect("native Op1 sample with a zero direction");
|
||||
assert_eq!(sample.direction, [0.0; 3]);
|
||||
assert_eq!(sample.range, 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generic_opcode1_scales_radius_like_live_factory_light() {
|
||||
// Live Effect.dll Op1: authored radius 7, FastProc direction length
|
||||
// 0x409b1f91, submitted LightManager radius 0x4207bb9f.
|
||||
let mut command = op1_test_command(7, 0.75, 1.0);
|
||||
let mut body = command.raw_body.to_vec();
|
||||
body[48..60].copy_from_slice(&[1.0_f32, 0.0, 0.0].map(f32::to_le_bytes).concat());
|
||||
body[112..116].copy_from_slice(&7.0_f32.to_le_bytes());
|
||||
command.raw_body = Arc::from(body.into_boxed_slice());
|
||||
let sample = evaluate_op1_point_light_with_basis(
|
||||
&command,
|
||||
1.0,
|
||||
[0.0; 3],
|
||||
[
|
||||
[f32::from_bits(0x409b_1f91), 0.0, 0.0],
|
||||
[0.0, 1.0, 0.0],
|
||||
[0.0, 0.0, 1.0],
|
||||
],
|
||||
)
|
||||
.expect("live factory radius");
|
||||
assert_eq!(sample.range.to_bits(), 0x4207_bb9f);
|
||||
assert_eq!(sample.direction, [1.0, 0.0, 0.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -59,6 +59,10 @@ log на минимальных сценах с одним типом матер
|
||||
|
||||
## FXID field-level semantics
|
||||
|
||||
Control Op1 пока использует цвет и радиус без случайных добавок команды;
|
||||
это явное приближение считается в `render_coverage control_op1_jitter_unsupported`.
|
||||
Масштаб радиуса длиной преобразованного направления и нижняя замена 0,01 восстановлены.
|
||||
|
||||
Размеры команд, resource references, lifecycle, flags families и используемые
|
||||
time modes известны. Не закрыто значение каждого поля body opcodes 1--10,
|
||||
отсутствующий во всех проверенных каталогах opcode 6 и точные формулы редких
|
||||
|
||||
@@ -71,6 +71,21 @@ RVA `0x1B0F0`, заменяет diffuse alpha пакета без type 16 с bat
|
||||
путь и ещё не декодирован. Это ограничивает приёмку на частично отсечённых
|
||||
порталах и не позволяет считать интерьер принятой эталонной сценой.
|
||||
|
||||
## Радиус Control Op1
|
||||
|
||||
GOG `Effect.dll` (SHA-256
|
||||
`386a3e8db1ce23f61957825d633c7dae1ed40c25cab0cf10a534385fd7cee789`),
|
||||
RVA `0xF6E0`, преобразует направление текущей матрицей, сохраняет его длину
|
||||
перед нормализацией и умножает на неё интерполированный локальный радиус.
|
||||
Умножение видно по RVA `0xFB13`; перед LightManager `+0x10` неположительный
|
||||
результат заменяется на `0.01`. В живом «Конструкторе» для синего источника
|
||||
завода локальный радиус 7 и длина `0x409B1F91` дали радиус `0x4207BB9F`.
|
||||
Это же значение проверяется Rust-тестом общего Op1 evaluator.
|
||||
|
||||
Случайные добавки цвета и радиуса ещё не воспроизводятся; приложение сообщает
|
||||
число таких Control-команд как `control_op1_jitter_unsupported` и использует
|
||||
их авторские значения. У синего источника в проверенном захвате добавки нулевые.
|
||||
|
||||
## Элемент отрисовки
|
||||
|
||||
Подготовленный элемент отрисовки содержит:
|
||||
|
||||
@@ -839,7 +839,13 @@ node poses входят в transform до локальной FX basis. Реал
|
||||
охватывают Op3 modes 0–2, Op4, Op7/8 particle pools и Op9 generated mesh. Op1
|
||||
selector 1 добавляет point lights без native flag `0x80000000` в общий diffuse
|
||||
список; остальные active records сохраняют flags для специальных путей и
|
||||
projected shadows. Effect renderer подавляет header bit `0x800` только при
|
||||
projected shadows. Радиус источника масштабируется длиной его преобразованного
|
||||
направления до нормализации. Например, локальный радиус 7 при масштабе узла
|
||||
около 4,85 освещает область радиусом около 34. Потеря этого множителя оставляла
|
||||
синий свет завода почти незаметным. Неположительный итог заменяется на 0,01.
|
||||
Случайные добавки к цвету и радиусу пока явно считаются в
|
||||
`render_coverage control_op1_jitter_unsupported`.
|
||||
Effect renderer подавляет header bit `0x800` только при
|
||||
render selector 0. Native AniMesh component path захвачен с selector 1, а в
|
||||
его 53-entry manager присутствуют FX entries с header `0xD00`; preview
|
||||
использует selector 1 и пропускает такие visual commands. Изменения manager
|
||||
|
||||
Reference in new issue
Block a user