fix(fx): orient Type 1 quads along their authored axis
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:
2 files changed
+89
-11
No files matched your search
@@ -471,6 +471,7 @@ fn native_op3_type1_matrix(
|
|||||||
/// `view · axis` value to exactly ±1 before building the cross-product frame;
|
/// `view · axis` value to exactly ±1 before building the cross-product frame;
|
||||||
/// neither input vector is normalized. The native path normalizes only
|
/// neither input vector is normalized. The native path normalizes only
|
||||||
/// `view × axis`, then computes `axis × U` without normalizing the result.
|
/// `view × axis`, then computes `axis × U` without normalizing the result.
|
||||||
|
/// The authored axis is the quad's X column (Effect.dll RVA 0x97F6..0x983C).
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn native_op3_type1_basis(
|
fn native_op3_type1_basis(
|
||||||
view: [f32; 3],
|
view: [f32; 3],
|
||||||
@@ -501,9 +502,9 @@ fn native_op3_type1_basis(
|
|||||||
}
|
}
|
||||||
let w = cross3(axis, u);
|
let w = cross3(axis, u);
|
||||||
let result = [
|
let result = [
|
||||||
[u[0], w[0], axis[0], position[0]],
|
[axis[0], u[0], w[0], position[0]],
|
||||||
[u[1], w[1], axis[1], position[1]],
|
[axis[1], u[1], w[1], position[1]],
|
||||||
[u[2], w[2], axis[2], position[2]],
|
[axis[2], u[2], w[2], position[2]],
|
||||||
[0.0, 0.0, 0.0, 1.0],
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
];
|
];
|
||||||
result
|
result
|
||||||
@@ -837,19 +838,19 @@ mod tests {
|
|||||||
// cross-product frame instead of being normalized before the gate.
|
// cross-product frame instead of being normalized before the gate.
|
||||||
let frame =
|
let frame =
|
||||||
native_op3_type1_basis([2.0, 1.0, 0.0], [1.0, 0.0, 0.0], [3.0, 4.0, 5.0]).unwrap();
|
native_op3_type1_basis([2.0, 1.0, 0.0], [1.0, 0.0, 0.0], [3.0, 4.0, 5.0]).unwrap();
|
||||||
assert_eq!(frame[0], [0.0, 0.0, 1.0, 3.0]);
|
assert_eq!(frame[0], [1.0, 0.0, 0.0, 3.0]);
|
||||||
assert_eq!(frame[1], [0.0, 1.0, 0.0, 4.0]);
|
assert_eq!(frame[1], [0.0, 0.0, 1.0, 4.0]);
|
||||||
assert_eq!(frame[2], [-1.0, 0.0, 0.0, 5.0]);
|
assert_eq!(frame[2], [0.0, -1.0, 0.0, 5.0]);
|
||||||
|
|
||||||
// The axis is not normalized: W and the axis column keep its scale.
|
// The axis is not normalized: W and the axis column keep its scale.
|
||||||
let axis_scaled =
|
let axis_scaled =
|
||||||
native_op3_type1_basis([2.0, 1.0, 0.0], [0.0, 0.0, 2.0], [0.0; 3]).unwrap();
|
native_op3_type1_basis([2.0, 1.0, 0.0], [0.0, 0.0, 2.0], [0.0; 3]).unwrap();
|
||||||
let inv_sqrt_5 = 1.0 / 5.0_f32.sqrt();
|
let inv_sqrt_5 = 1.0 / 5.0_f32.sqrt();
|
||||||
assert_eq!(axis_scaled[0][0], inv_sqrt_5);
|
assert_eq!(axis_scaled[0][1], inv_sqrt_5);
|
||||||
assert_eq!(axis_scaled[1][0], -2.0 * inv_sqrt_5);
|
assert_eq!(axis_scaled[1][1], -2.0 * inv_sqrt_5);
|
||||||
assert_eq!(axis_scaled[0][1], 4.0 * inv_sqrt_5);
|
assert_eq!(axis_scaled[0][2], 4.0 * inv_sqrt_5);
|
||||||
assert_eq!(axis_scaled[1][1], 2.0 * inv_sqrt_5);
|
assert_eq!(axis_scaled[1][2], 2.0 * inv_sqrt_5);
|
||||||
assert_eq!(axis_scaled[2][2], 2.0);
|
assert_eq!(axis_scaled[2][0], 2.0);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
native_op3_type1_basis([f32::NAN, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0; 3]),
|
native_op3_type1_basis([f32::NAN, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0; 3]),
|
||||||
@@ -907,6 +908,10 @@ mod tests {
|
|||||||
assert_eq!(frame.normalized_time, 0.5);
|
assert_eq!(frame.normalized_time, 0.5);
|
||||||
assert_eq!(frame.color, [1.0, 1.0, 1.0, 0.5]);
|
assert_eq!(frame.color, [1.0, 1.0, 1.0, 0.5]);
|
||||||
assert_eq!(frame.material_phase, 0.5);
|
assert_eq!(frame.material_phase, 0.5);
|
||||||
|
let authored_axis_edge = std::array::from_fn::<_, 3, _>(|axis| {
|
||||||
|
frame.vertices[2][axis] - frame.vertices[1][axis]
|
||||||
|
});
|
||||||
|
assert_eq!(authored_axis_edge, [0.0, 0.0, 1.0]);
|
||||||
assert!(evaluate_op3_type1_command(
|
assert!(evaluate_op3_type1_command(
|
||||||
&command,
|
&command,
|
||||||
4,
|
4,
|
||||||
@@ -918,6 +923,61 @@ mod tests {
|
|||||||
.is_none());
|
.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires the original GOG installation"]
|
||||||
|
fn original_deflector_quad_keeps_its_authored_axis_over_time_and_camera_motion() {
|
||||||
|
let root = std::env::var_os("FPARKAN_CORPUS_PART1_ROOT")
|
||||||
|
.expect("set FPARKAN_CORPUS_PART1_ROOT to the original GOG installation");
|
||||||
|
let bytes = std::fs::read(std::path::Path::new(&root).join("effects.rlb"))
|
||||||
|
.expect("read effects.rlb");
|
||||||
|
let archive = fparkan_nres::decode(bytes.into(), fparkan_nres::ReadProfile::Compatible)
|
||||||
|
.expect("decode effects.rlb");
|
||||||
|
let entry = archive.find("deflector").expect("deflector FXID");
|
||||||
|
let document = crate::decode_fxid(
|
||||||
|
archive
|
||||||
|
.payload(entry)
|
||||||
|
.expect("FXID payload")
|
||||||
|
.to_vec()
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
.expect("decode deflector");
|
||||||
|
let command = &document.commands()[0];
|
||||||
|
let parent = [
|
||||||
|
[12.0, 0.0, 0.0, 100.0],
|
||||||
|
[0.0, 3.0, 0.0, 200.0],
|
||||||
|
[0.0, 0.0, 3.0, 20.0],
|
||||||
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
|
];
|
||||||
|
for elapsed in [0.0_f32, 0.125, 0.5, 1.0, 2.5] {
|
||||||
|
for camera in [
|
||||||
|
[160.0, 230.0, 20.0],
|
||||||
|
[160.0, 200.0, 50.0],
|
||||||
|
[160.0, 170.0, 5.0],
|
||||||
|
] {
|
||||||
|
let frame = evaluate_op3_type1_command(
|
||||||
|
command,
|
||||||
|
0,
|
||||||
|
&parent,
|
||||||
|
elapsed,
|
||||||
|
elapsed.min(1.0),
|
||||||
|
camera,
|
||||||
|
)
|
||||||
|
.expect("active deflector quad");
|
||||||
|
assert!(frame.active);
|
||||||
|
assert_eq!(frame.material.name.to_ascii_lowercase(), "defl_b");
|
||||||
|
let axis_edge = std::array::from_fn::<_, 3, _>(|axis| {
|
||||||
|
frame.vertices[2][axis] - frame.vertices[1][axis]
|
||||||
|
});
|
||||||
|
assert_eq!(axis_edge, [12.0, 0.0, 0.0]);
|
||||||
|
let center = std::array::from_fn::<_, 3, _>(|axis| {
|
||||||
|
(frame.vertices[0][axis] + frame.vertices[2][axis]) * 0.5
|
||||||
|
});
|
||||||
|
assert_eq!(center, [106.0, 200.0, 20.0]);
|
||||||
|
assert_eq!(frame.material_phase, (elapsed * 7.0).fract());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_truncated_bodies_and_non_finite_tracks_or_inputs() {
|
fn rejects_truncated_bodies_and_non_finite_tracks_or_inputs() {
|
||||||
let identity = [
|
let identity = [
|
||||||
|
|||||||
@@ -294,6 +294,24 @@ D110-порядок. Все три evaluator-а принимают уже соб
|
|||||||
`context + 0x60 < 8`, а API этих evaluator-ов не выбирает другой context.
|
`context + 0x60 < 8`, а API этих evaluator-ов не выбирает другой context.
|
||||||
Evaluator типа 2 принимает индекс context явно и учитывает этот gate в `active`.
|
Evaluator типа 2 принимает индекс context явно и учитывает этот gate в `active`.
|
||||||
|
|
||||||
|
У Op3 mode 1 ось задаёт длину прямоугольника, а не его нормаль. Для
|
||||||
|
локального вектора к камере `v` и анимируемой оси `a` столбцы базиса имеют
|
||||||
|
порядок `[a, u, w]`, где `u = normalize(v × a)`, `w = a × u`. Плоскость
|
||||||
|
прямоугольника — XY, поэтому первая сторона идёт вдоль авторской оси, а
|
||||||
|
вторая поворачивается к камере. Это сохраняет соединение между точками
|
||||||
|
локатора при движении камеры. Если поставить ось в третий столбец,
|
||||||
|
прямоугольник займёт поперечную плоскость и связь превратится в полоску или
|
||||||
|
небольшой диск.
|
||||||
|
|
||||||
|
Порядок подтверждён аргументами конструктора матрицы в GOG `Effect.dll`
|
||||||
|
(сборка из [таблицы бинарников](original-binaries.md), SHA-256
|
||||||
|
`386a3e8db1ce23f61957825d633c7dae1ed40c25cab0cf10a534385fd7cee789`):
|
||||||
|
RVA `0x93D0`, ветка mode 1 с `0x9739`, сборка столбцов `0x97F6–0x983C`.
|
||||||
|
Например, `a = (1, 0, 0)` и `v = (2, 1, 0)` дают столбцы
|
||||||
|
`(1, 0, 0)`, `(0, 0, -1)`, `(0, 1, 0)`. При нативном вызове длины последних
|
||||||
|
двух столбцов могут немного отличаться от единицы из-за приближённой
|
||||||
|
нормализации выбранного `g_FastProc`; порядок осей от этого не меняется.
|
||||||
|
|
||||||
## Частицы Op7
|
## Частицы Op7
|
||||||
|
|
||||||
`fparkan-fx::op7` вычисляет фиксированный набор частиц Op7 по body команды,
|
`fparkan-fx::op7` вычисляет фиксированный набор частиц Op7 по body команды,
|
||||||
|
|||||||
Reference in new issue
Block a user