fix(render): apply celestial birth matrix to column vectors
Docs Deploy / Build and Deploy MkDocs (push) Successful in 40s

This commit is contained in:
Valentin Popov committed 2026-10-31 20:31:22 +04:00
1 parent f08476e655
commit 9df0d8594f
4 files changed
+51 -22

No files matched your search

+21 -8
View File
@@ -1143,12 +1143,11 @@ impl SunFrame {
/// Converts an interpolated atmosphere sun sample to a renderer-facing frame. /// Converts an interpolated atmosphere sun sample to a renderer-facing frame.
/// ///
/// The original update computes `orbit_direction = [cos(theta), 0, /// The original update computes `orbit_direction = [cos(theta), 0,
/// -sin(theta)]`, then applies the row-vector product of the two birth-angle /// -sin(theta)]`, then applies the column-vector product of the two birth-angle
/// matrices. With `A` and `B` in radians, the resulting direction is: /// matrices. With `A` and `B` in radians, the resulting direction is:
/// ///
/// ```text /// ```text
/// [x*cos(A), -x*sin(A)*cos(B) + z*sin(B), /// [x*cos(A) + z*sin(A)*sin(B), x*sin(A) - z*cos(A)*sin(B), z*cos(B)]
/// x*sin(A)*sin(B) + z*cos(B)]
/// ``` /// ```
/// ///
/// where `x` and `z` are the two non-zero orbit components. /// where `x` and `z` are the two non-zero orbit components.
@@ -1178,9 +1177,9 @@ pub fn sun_frame(sample: &SunSample, now_seconds: f32, config: SunConfig) -> Sun
let x = orbit_direction[0]; let x = orbit_direction[0];
let z = orbit_direction[2]; let z = orbit_direction[2];
let direction = normalize3([ let direction = normalize3([
x * cos_a, x * cos_a + z * sin_a * sin_b,
-x * sin_a * cos_b + z * sin_b, x * sin_a - z * cos_a * sin_b,
x * sin_a * sin_b + z * cos_b, z * cos_b,
]); ]);
SunFrame { SunFrame {
active: true, active: true,
@@ -1892,15 +1891,29 @@ mod tests {
let sample = sun(true); let sample = sun(true);
let at_start = sun_frame(&sample, 0.0, SunConfig::new(0.0, 1.0, [90.0, 0.0])); let at_start = sun_frame(&sample, 0.0, SunConfig::new(0.0, 1.0, [90.0, 0.0]));
assert!((at_start.direction[0]).abs() < 1.0e-5); assert!((at_start.direction[0]).abs() < 1.0e-5);
assert!((at_start.direction[1] + at_start.orbit_direction[0]).abs() < 1.0e-5); assert!((at_start.direction[1] - at_start.orbit_direction[0]).abs() < 1.0e-5);
assert!((at_start.direction[2] - at_start.orbit_direction[2]).abs() < 1.0e-5); assert!((at_start.direction[2] - at_start.orbit_direction[2]).abs() < 1.0e-5);
let side = sun_frame(&sample, 0.0, SunConfig::new(0.0, 1.0, [0.0, 90.0])); let side = sun_frame(&sample, 0.0, SunConfig::new(0.0, 1.0, [0.0, 90.0]));
assert!((side.direction[0] - side.orbit_direction[0]).abs() < 1.0e-5); assert!((side.direction[0] - side.orbit_direction[0]).abs() < 1.0e-5);
assert!((side.direction[1] - side.orbit_direction[2]).abs() < 1.0e-5); assert!((side.direction[1] + side.orbit_direction[2]).abs() < 1.0e-5);
assert!(side.direction[2].abs() < 1.0e-5); assert!(side.direction[2].abs() < 1.0e-5);
} }
#[test]
fn sun_direction_matches_live_native_birth_matrix() {
// Terrain CSun::Update, elapsed 282184 ms, lifetime 525000 ms.
let frame = sun_frame(
&sun(true),
282.184,
SunConfig::new(0.0, 525.0, [90.0, 30.0]),
);
let expected = [0xbefd_7269, 0xbe10_4168, 0xbf5b_7dda].map(f32::from_bits);
for (actual, expected) in frame.direction.into_iter().zip(expected) {
assert!((actual - expected).abs() < 3.0e-6, "{actual} != {expected}");
}
}
#[test] #[test]
fn native_birth_keeps_the_real_start_time() { fn native_birth_keeps_the_real_start_time() {
assert!(!celestial_frame(&sun(false), 100.0).active); assert!(!celestial_frame(&sun(false), 100.0).active);
+3 -1
View File
@@ -55,7 +55,9 @@ MSH-запрос со свободной preview-камерой; это не п
Данные type 16 ещё не декодированы. Внутренний ракурс «Конструктора» после Данные type 16 ещё не декодированы. Внутренний ракурс «Конструктора» после
настоящего входа игрока подтверждает исчезновение чёрной поверхности в проходе; настоящего входа игрока подтверждает исчезновение чёрной поверхности в проходе;
для всей приёмки R2 ещё нужны пары обеих миссий на трёх дистанциях. наружные окна обеих миссий сверены на расстояниях около 60/85/110, а ближние
окна — с внутренней стороны. Внутренние дальние ракурсы и clipping на краю
кадра ещё не приняты; общей готовности R2 эти пары не доказывают.
Закрывающий эксперимент: запустить оригинал в совместимой Windows/DirectX Закрывающий эксперимент: запустить оригинал в совместимой Windows/DirectX
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
+19 -1
View File
@@ -50,7 +50,25 @@ key_time = floor(duration * (key_hour * 60 + key_minute) * 60 / 86400)
theta = (clamp((now - birth_start) / lifetime, 0, 1) * 1.2 - 0.1) * pi theta = (clamp((now - birth_start) / lifetime, 0, 1) * 1.2 - 0.1) * pi
``` ```
Затем к нему применяется матрица рождения. У солнца и луны независимые Затем к нему применяется матрица рождения как к вектору-столбцу. Если `A` и
`B` — углы рождения, а `x = cos(theta)` и `z = -sin(theta)`, результат равен
```text
[x*cos(A) + z*sin(A)*sin(B),
x*sin(A) - z*cos(A)*sin(B),
z*cos(B)]
```
Порядок важен для всего освещения: транспонированное умножение переносит
солнце на другую сторону мира и меняет освещённые склоны, блики и тени.
В GOG `Terrain.dll` (SHA-256
`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`)
матрицу строит RVA `0x4A550`, а вектор преобразует `CSun::Update`, RVA
`0x4B570`. Живой замер после 282184 мс при длительности 525000 мс и углах
`[90, 30]` дал направление `[-0.4950135, -0.1408745, -0.8573891]`;
Rust-тест проверяет этот результат с допуском `3e-6`.
У солнца и луны независимые
интервалы и независимые направления: при отсутствии активной луны нельзя интервалы и независимые направления: при отсутствии активной луны нельзя
подменять её направление противоположностью солнца. `SunFrame::color` — это подменять её направление противоположностью солнца. `SunFrame::color` — это
единственный primary directional RGB. Второй источник получает направление единственный primary directional RGB. Второй источник получает направление
+8 -12
View File
@@ -134,21 +134,17 @@ RVA `0xF6E0`, преобразует направление текущей ма
глубину. За фиксированными диапазонами окружения сохраняются их явные состояния. глубину. За фиксированными диапазонами окружения сохраняются их явные состояния.
Для MSH без потока цветов type 16 диапазон, цвет которого вычисляет CPU, при Для MSH без потока цветов type 16 диапазон, цвет которого вычисляет CPU, при
raw-флаге `Batch20` `0x0008` получает переопределение alpha только в одном raw-флаге `Batch20` `0x0008` получает переопределение alpha по
подтверждённом случае. В GOG `Terrain.dll` с SHA-256 [дистанционной кривой портала](#прозрачность-порталов-msh). Bit `0x0010`
`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead` проверка имеет приоритет; иначе bit `0x0040` задаёт нулевую alpha, а оставшаяся
по RVA `0x1B0F0` задаёт alpha `0`, когда установлен bit `0x0040` и сброшен ветвь использует границы 75/95 с поправкой на FOV. При наличии type 16 или
`0x0010`; поэтому для `0x0148` alpha равна нулю. Для `0x0108` и `0x0118` отсутствии `0x0008` переопределения нет. Оно относится к одному draw range и не меняет общий
превью сохраняет alpha MAT0; для `0x0058` действует приоритет bit `0x0010`,
также оставляя alpha MAT0. При наличии type 16 или отсутствии `0x0008`
переопределения нет. Оно относится к одному draw range и не меняет общий
материал; alpha `0` временно задаёт число индексов `0`, а при очистке материал; alpha `0` временно задаёт число индексов `0`, а при очистке
переопределения исходное число индексов восстанавливается. переопределения исходное число индексов восстанавливается.
Общий fade по расстоянию пока не реализован. Открыты точная метрика расстояния, Fade обновляется с камерой каждый кадр. Открыт выбор первой принятой вершины
выбор первого CPU-принятого треугольника диапазона и эквивалентность frustum- и после native CPU clipping: приложение пока берёт первую индексированную
culling-отбору оригинала. Поэтому состояния `0x0108` и `0x0118` оставляют alpha вершину диапазона и считает это приближение в `render_coverage`.
MAT0, пока эти входные данные не установлены.
В превью с фиксированной камерой диапазоны туманности и звёзд, купола и облаков В превью с фиксированной камерой диапазоны туманности и звёзд, купола и облаков
явно помечаются как прозрачный фон. Непрозрачные диапазоны мира остаются на явно помечаются как прозрачный фон. Непрозрачные диапазоны мира остаются на