aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/fparkan-fx/src/sky.rs29
-rw-r--r--docs/appendices/knowledge-boundaries.md7
-rw-r--r--docs/reference/atmosphere.md40
-rw-r--r--docs/reference/render-frame.md30
-rw-r--r--tools/native-frame-capture/NativeFrameCapture.cs67
-rw-r--r--tools/native-frame-capture/README.md7
6 files changed, 141 insertions, 39 deletions
diff --git a/crates/fparkan-fx/src/sky.rs b/crates/fparkan-fx/src/sky.rs
index 26e68ab..d9ba3ed 100644
--- a/crates/fparkan-fx/src/sky.rs
+++ b/crates/fparkan-fx/src/sky.rs
@@ -1143,12 +1143,11 @@ impl SunFrame {
/// Converts an interpolated atmosphere sun sample to a renderer-facing frame.
///
/// 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:
///
/// ```text
-/// [x*cos(A), -x*sin(A)*cos(B) + z*sin(B),
-/// x*sin(A)*sin(B) + z*cos(B)]
+/// [x*cos(A) + z*sin(A)*sin(B), x*sin(A) - z*cos(A)*sin(B), z*cos(B)]
/// ```
///
/// 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 z = orbit_direction[2];
let direction = normalize3([
- x * cos_a,
- -x * sin_a * cos_b + z * sin_b,
- x * sin_a * sin_b + z * cos_b,
+ x * cos_a + z * sin_a * sin_b,
+ x * sin_a - z * cos_a * sin_b,
+ z * cos_b,
]);
SunFrame {
active: true,
@@ -1892,16 +1891,30 @@ mod tests {
let sample = sun(true);
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[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);
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[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);
}
#[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]
fn native_birth_keeps_the_real_start_time() {
assert!(!celestial_frame(&sun(false), 100.0).active);
let birth = SunBirth {
diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md
index 7f049fc..7c9daae 100644
--- a/docs/appendices/knowledge-boundaries.md
+++ b/docs/appendices/knowledge-boundaries.md
@@ -11,7 +11,8 @@ authoring API без динамического подтверждения.
state transitions и окончательный порядок части transparent/FX/shadow subpasses.
Дистанционная кривая тумана по квадратам границ подтверждена в живом CShade.
-Выбор проходов по native shade-state и GPU fog color пока заменён общим туманом мира с цветом горизонта;
+Обычный GPU fog color из горизонта подтверждён исполнением; выбор проходов по
+native shade-state и их временные изменения цвета пока заменены общим туманом мира;
это приближение считается как `render_coverage fog_pass_eligibility_approx`.
После ограничения цели рельефом интерактивная камера проверяет движение
@@ -55,7 +56,9 @@ MSH-запрос со свободной preview-камерой; это не пÐ
Данные type 16 ещё не декодированы. Внутренний ракурс «Конструктора» после
настоящего входа игрока подтверждает исчезновение чёрной поверхности в проходе;
-для всей приёмки R2 ещё нужны пары обеих миссий на трёх дистанциях.
+наружные окна обеих миссий сверены на расстояниях около 60/85/110, а ближние
+окна — с внутренней стороны. Внутренние дальние ракурсы и clipping на краю
+кадра ещё не приняты; общей готовности R2 эти пары не доказывают.
Закрывающий эксперимент: запустить оригинал в совместимой Windows/DirectX
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
diff --git a/docs/reference/atmosphere.md b/docs/reference/atmosphere.md
index 65ed176..8d44e4e 100644
--- a/docs/reference/atmosphere.md
+++ b/docs/reference/atmosphere.md
@@ -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
```
-Затем к нему применяется матрица рождения. У солнца и луны независимые
+Затем к нему применяется матрица рождения как к вектору-столбцу. Если `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` — это
единственный primary directional RGB. Второй источник получает направление
@@ -167,21 +185,27 @@ RGB palette 12 с минимумом `80/255` на канал и alpha `150/255`
## Туман и свет
-Нативное обновление `CSky` записывает две дальности и один packed RGB.
+Нативное обновление `CSky` записывает две дальности и нижнюю границу освещения.
`FogFrame` повторяет этот контракт:
```text
fog.start = sky.values[0] * 700
fog.end = sky.values[1] * 700
-fog.color = RGB(sky.packed[1]) / 255
+lighting_floor = RGB(sky.packed[1]) / 255
```
Поля не скрывают порядок исходных параметров: значения сохраняются в
-`SkySample`, а преобразование выполняется только в `fog_frame`. Vulkan path
-передаёт цвет и дальности в frame uniforms; vertex shader вычисляет линейный
-коэффициент по расстоянию до камеры, а fragment shader смешивает RGB материала
-с fog color в ветвях combiner, которые используют туман. Конкретный draw state
-может обходить это смешивание.
+`SkySample`, а преобразование выполняется только в `fog_frame`. Нижняя граница
+освещения ограничивает результат расчёта света снизу; она не задаёт цвет тумана.
+Обычный цвет тумана выбирается из `sky.colors[0..4]` по направлению камеры,
+как цвет горизонта. Это подтверждено в `CSky::Render` и живым совпадением
+его значения с `D3DRENDERSTATE_FOGCOLOR` в том же игровом тике.
+
+Vulkan path передаёт эти параметры раздельно. Vertex shader вычисляет
+коэффициент по квадратам расстояния и границ, а fragment shader смешивает
+RGB материала с цветом тумана. Конкретный native draw state может обходить
+смешивание или временно менять его цвет, например на чёрный для отдельного
+прохода. Полный выбор таких проходов ещё не воспроизведён.
Цвет primary света солнца — RGB palette 12, нормированный делением на 255 и
умноженный на trailing parameter 2. `SunFrame::direction` — нормированный
diff --git a/docs/reference/render-frame.md b/docs/reference/render-frame.md
index b2a6a93..19ccf3e 100644
--- a/docs/reference/render-frame.md
+++ b/docs/reference/render-frame.md
@@ -85,9 +85,13 @@ RVA `0x1B0F0`, заменяет diffuse alpha пакета без type 16 с bat
квадраты границ по этому наблюдению. GLSL вычисляет квадрат расстояния вершины,
после чего растеризатор интерполирует коэффициент по треугольнику.
`native_fog.effective.sink_rgb` совпадает с `sky.packed[1]`, нижней границей
-освещения CShade. Это не подтверждает значение GPU fog color. Приложение пока
-сохраняет цвет экранного горизонта; выбор цвета и проходов требует отдельной
-проверки GPU-состояния.
+освещения CShade. Обычный GPU fog color подтверждён отдельно: `CSky::Render`
+по RVA `0x48341` задаёт renderer default из цвета горизонта. В живом замере
+`0xFF404064` дошёл до `IDirect3DDevice7::SetRenderState(34)` в том же игровом
+тике. В другом ракурсе палитра `[0xFFA8A5CE, 0xFFABA9D5, 0xFFBAA9D5,
+0xFFABA9D5]` дала default `0xFFA9A7D1`. Это не покрывает временные изменения
+цвета отдельных проходов: например, native render path устанавливает чёрный
+цвет и затем восстанавливает сохранённый.
Выбор проходов по native shade-state пока приближён общим туманом мира и
считается как `render_coverage fog_pass_eligibility_approx`.
@@ -134,21 +138,17 @@ RVA `0xF6E0`, преобразует направление текущей маÑ
глубину. За фиксированными диапазонами окружения сохраняются их явные состояния.
Для MSH без потока цветов type 16 диапазон, цвет которого вычисляет CPU, при
-raw-флаге `Batch20` `0x0008` получает переопределение alpha только в одном
-подтверждённом случае. В GOG `Terrain.dll` с SHA-256
-`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead` проверка
-по RVA `0x1B0F0` задаёт alpha `0`, когда установлен bit `0x0040` и сброшен
-`0x0010`; поэтому для `0x0148` alpha равна нулю. Для `0x0108` и `0x0118`
-превью сохраняет alpha MAT0; для `0x0058` действует приоритет bit `0x0010`,
-также оставляя alpha MAT0. При наличии type 16 или отсутствии `0x0008`
-переопределения нет. Оно относится к одному draw range и не меняет общий
+raw-флаге `Batch20` `0x0008` получает переопределение alpha по
+[дистанционной кривой портала](#прозрачность-порталов-msh). Bit `0x0010`
+имеет приоритет; иначе bit `0x0040` задаёт нулевую alpha, а оставшаяся
+ветвь использует границы 75/95 с поправкой на FOV. При наличии type 16 или
+отсутствии `0x0008` переопределения нет. Оно относится к одному draw range и не меняет общий
материал; alpha `0` временно задаёт число индексов `0`, а при очистке
переопределения исходное число индексов восстанавливается.
-Общий fade по расстоянию пока не реализован. Открыты точная метрика расстояния,
-выбор первого CPU-принятого треугольника диапазона и эквивалентность frustum- и
-culling-отбору оригинала. Поэтому состояния `0x0108` и `0x0118` оставляют alpha
-MAT0, пока эти входные данные не установлены.
+Fade обновляется с камерой каждый кадр. Открыт выбор первой принятой вершины
+после native CPU clipping: приложение пока берёт первую индексированную
+вершину диапазона и считает это приближение в `render_coverage`.
В превью с фиксированной камерой диапазоны туманности и звёзд, купола и облаков
явно помечаются как прозрачный фон. Непрозрачные диапазоны мира остаются на
diff --git a/tools/native-frame-capture/NativeFrameCapture.cs b/tools/native-frame-capture/NativeFrameCapture.cs
index 6371ae6..f41fb42 100644
--- a/tools/native-frame-capture/NativeFrameCapture.cs
+++ b/tools/native-frame-capture/NativeFrameCapture.cs
@@ -201,6 +201,8 @@ internal static partial class NativeFrameCapture
private sealed class SelectedCameraState
{
public SelectedCameraInput Input;
+ public int PrecedingFramesRemaining;
+ public int PrecedingFramesCompleted;
public uint CandidateCameraPointer;
public uint CandidateThreadId;
public uint CandidateGeneration;
@@ -226,6 +228,47 @@ internal static partial class NativeFrameCapture
public uint OriginalParentPointer;
}
+ private static SelectedCameraState NextSelectedWarmupFrame(SelectedCameraState previous)
+ {
+ if (previous.PrecedingFramesRemaining <= 0 || !previous.RestoreVerified
+ || !previous.Restored || !previous.MatrixIntactAtReturn
+ || !previous.ProjectionMatchesInput || previous.PixelAttributionInvalidated)
+ throw new InvalidOperationException("Cannot advance a selected camera before a verified frame and exact restoration.");
+ return new SelectedCameraState
+ {
+ Input = previous.Input,
+ PrecedingFramesRemaining = previous.PrecedingFramesRemaining - 1,
+ PrecedingFramesCompleted = previous.PrecedingFramesCompleted + 1,
+ CandidateCameraPointer = previous.CameraPointer,
+ CandidateThreadId = previous.ThreadId,
+ CandidateGeneration = previous.Generation,
+ CandidateProjectionVerified = true
+ };
+ }
+
+ private static void SelfCheckSelectedCameraWarmup()
+ {
+ SelectedCameraState first = new SelectedCameraState
+ {
+ PrecedingFramesRemaining = 2, CameraPointer = 123, ThreadId = 11, Generation = 8,
+ Restored = true, RestoreVerified = true, MatrixIntactAtReturn = true,
+ ProjectionMatchesInput = true
+ };
+ SelectedCameraState second = NextSelectedWarmupFrame(first);
+ if (second.PrecedingFramesRemaining != 1 || second.PrecedingFramesCompleted != 1
+ || second.CandidateCameraPointer != 123 || second.CandidateThreadId != 11
+ || second.CandidateGeneration != 8 || second.Applied || second.RestoreVerified)
+ throw new InvalidOperationException("Selected camera warmup transition self-check failed.");
+ bool unrestoredRejected = false;
+ try { NextSelectedWarmupFrame(second); }
+ catch (InvalidOperationException) { unrestoredRejected = true; }
+ if (!unrestoredRejected) throw new InvalidOperationException("Unrestored warmup frame was accepted.");
+ second.Restored = second.RestoreVerified = second.MatrixIntactAtReturn = second.ProjectionMatchesInput = true;
+ SelectedCameraState final = NextSelectedWarmupFrame(second);
+ if (final.PrecedingFramesRemaining != 0 || final.PrecedingFramesCompleted != 2)
+ throw new InvalidOperationException("Selected camera final frame self-check failed.");
+ }
+
private sealed class BoundaryTraceProgress
{
public bool BeginSceneSeen;
@@ -1236,6 +1279,7 @@ internal static partial class NativeFrameCapture
return CanArmPresentForFrame(frame, threadId)
&& (!frame.SelectedCameraRequested || (frame.SelectedCamera != null
&& frame.SelectedCamera.Applied && frame.SelectedCamera.ProjectionMatchesInput
+ && frame.SelectedCamera.PrecedingFramesRemaining == 0
&& frame.SelectedCamera.CandidateProjectionVerified
&& frame.SelectedCamera.Restored && frame.SelectedCamera.RestoreVerified
&& frame.SelectedCamera.MatrixIntactAtReturn
@@ -1276,6 +1320,7 @@ internal static partial class NativeFrameCapture
&& frame.CameraThreadId == threadId && frame.ProjectionThreadId == threadId
&& (!frame.SelectedCameraRequested || (frame.SelectedCamera != null
&& frame.SelectedCamera.Applied && frame.SelectedCamera.ProjectionMatchesInput
+ && frame.SelectedCamera.PrecedingFramesRemaining == 0
&& frame.SelectedCamera.CandidateProjectionVerified
&& !frame.SelectedCamera.PixelAttributionInvalidated
&& SelectedMatrixMatchesProjection(frame)));
@@ -1848,6 +1893,7 @@ internal static partial class NativeFrameCapture
{
SelectedCameraState selected = frame.SelectedCamera;
json.AppendLine(" \"selected_camera\": {");
+ json.AppendLine(" \"preceding_selected_world_frames\": " + selected.PrecedingFramesCompleted + ",");
json.AppendLine(" \"input_json\": " + JsonString(selected.Input.Path) + ",");
json.AppendLine(" \"input_kind\": " + JsonString(selected.Input.Synthetic
? "synthetic-camera-plan" : "native-capture") + ",");
@@ -1949,6 +1995,7 @@ internal static partial class NativeFrameCapture
{
if (IntPtr.Size != 4) throw new InvalidOperationException("This helper must be compiled and run as x86.");
SelfCheckCameraInput();
+ SelfCheckSelectedCameraWarmup();
SelfCheckNativeFogPairing();
SelfCheckRenderInvocationGates();
SelfCheckDebugEventHandleOwnership();
@@ -2088,6 +2135,10 @@ internal static partial class NativeFrameCapture
bool wrongCameraThreadRejected = !IsMatchingPerspectiveFrame(matched, 12);
bool emptyCameraFrameRejected = !IsMatchingPerspectiveFrame(new FrameSnapshot(), 11);
bool selectedFrameValid = IsMatchingPerspectiveFrame(selectedMatched, 11);
+ selectedMatched.SelectedCamera.PrecedingFramesRemaining = 1;
+ bool selectedWarmupRejected = !CanArmPresentForFrame(selectedMatched, 11)
+ && !IsMatchingPerspectiveFrame(selectedMatched, 11);
+ selectedMatched.SelectedCamera.PrecedingFramesRemaining = 0;
bool selectedUnrestoredRejected = !IsMatchingPerspectiveFrame(selectedUnrestored, 11);
bool selectedUnrestoredCaptureEligible = CanArmPresentForFrame(selectedUnrestored, 11);
bool selectedIntervenedCaptureRejected = !CanArmPresentForFrame(selectedIntervened, 11);
@@ -2189,7 +2240,7 @@ internal static partial class NativeFrameCapture
|| !atmosphereFrameValid
|| !atmosphereClockMismatchRejected
|| !cameraFrameValid || !wrongCameraThreadRejected || !emptyCameraFrameRejected
- || !selectedFrameValid || !selectedUnrestoredRejected || !selectedUnrestoredCaptureEligible
+ || !selectedFrameValid || !selectedWarmupRejected || !selectedUnrestoredRejected || !selectedUnrestoredCaptureEligible
|| !selectedIntervenedCaptureRejected || !selectedIntervenedRejected
|| !selectedChangedMatrixRejected
|| !selectedBoundaryReadAllowed || !boundaryReadRejectsUnselected
@@ -2919,7 +2970,8 @@ internal static partial class NativeFrameCapture
bool projectionSelectorMode = boundaryTrace || boundaryCapture;
frame.SelectedCameraRequested = selectedCameraInput != null;
SelectedCameraState selectedCamera = selectedCameraInput == null ? null
- : new SelectedCameraState { Input = selectedCameraInput };
+ : new SelectedCameraState { Input = selectedCameraInput,
+ PrecedingFramesRemaining = boundaryCapture ? 0 : 2 };
try
{
if (identityProcess == null || identityProcess.HasExited
@@ -3330,6 +3382,12 @@ internal static partial class NativeFrameCapture
Log("CAMERA_SETTER_REGION_FREED phase=Restore");
else Log("CAMERA_SETTER_REGION_FREE_FAILED phase=Restore error=" + Marshal.GetLastWin32Error());
cameraSetter = null;
+ if (!stopAfterAttempt && selectedCamera.PrecedingFramesRemaining > 0)
+ {
+ selectedCamera = NextSelectedWarmupFrame(selectedCamera);
+ Log("SELECTED_CAMERA_WARMUP_COMPLETE completed=" + selectedCamera.PrecedingFramesCompleted
+ + " remaining=" + selectedCamera.PrecedingFramesRemaining);
+ }
}
}
}
@@ -3870,7 +3928,8 @@ internal static partial class NativeFrameCapture
&& targetProjectionMatchesInput)
+ " rawWorldTime=0x" + frame.WorldGameTimeWord.ToString("X8"));
if (cameraWordsCurrent && frame.Projection.Verified && frame.Projection.Mode != 0
- && targetProjectionMatchesInput && ngiBase != 0 && presentBoundaryVerified)
+ && targetProjectionMatchesInput && ngiBase != 0 && presentBoundaryVerified
+ && (selectedCamera == null || selectedCamera.PrecedingFramesRemaining == 0))
EnsurePresentBreakpoint(process, ngiBase, frame, eventTid,
presentOriginalByte, ref presentBreakpoint);
}
@@ -3975,7 +4034,7 @@ internal static partial class NativeFrameCapture
}
cameraSetter = StartCameraSetter(process, eventTid, terrainBase,
selectedCamera.CameraPointer, selectedCamera.OriginalMatrixBytes,
- stoppedContext, returnBreakpoint, false, CameraSetterPhase.Restore,
+ stoppedContext, returnBreakpoint, selectedCamera.PrecedingFramesRemaining > 0, CameraSetterPhase.Restore,
selectedCamera.EntryEsp, selectedCamera.ReturnAddress);
Log("SELECTED_CAMERA_RESTORE_STARTED generation=" + selectedCamera.Generation
+ " camera=0x" + selectedCamera.CameraPointer.ToString("X8")
diff --git a/tools/native-frame-capture/README.md b/tools/native-frame-capture/README.md
index 75f47e1..232cc1c 100644
--- a/tools/native-frame-capture/README.md
+++ b/tools/native-frame-capture/README.md
@@ -74,6 +74,8 @@ if ($LASTEXITCODE -ne 0) { throw 'Selected-camera capture failed; inspect its lo
This verifies repeatable camera handling, not deterministic whole-scene pixels. The helper has no setter for FOV, near/far planes, or fog, and does not freeze simulation state; `atmosphere_seconds` remains an observed phase sample rather than a restored simulation clock. If the callsite probe cannot verify a mission path, `mission_path` remains `null` and `mission_identity` remains `unknown`.
+For `--capture --camera-input`, the requested pose is rendered on three consecutive world frames. The first two are discarded so portal visibility and other render caches can settle. The original local matrix is restored and checked after every frame; the third frame alone supplies the pixels, projection and atmosphere sample. `selected_camera.preceding_selected_world_frames` records the two preceding verified frames. A failed matrix or restoration check stops the sequence. Passive and EndScene-boundary capture keep their single-frame behavior.
+
For a second capture, reuse the still-running PID only after the first run detached successfully, and choose a new output basename. Reuse the same validated camera input if comparing the same pose. The helper rejects any existing JSON, PNG, or log with the chosen basename and never overwrites those files. After a successful one-shot capture it detaches and leaves the scratch game running. It explicitly closes only image-file handles from `CREATE_PROCESS` and `LOAD_DLL`; the system owns the process/thread handles supplied by `CREATE_PROCESS` and `CREATE_THREAD` until the corresponding exit event is continued. If guarded recovery must terminate the scratch process, the helper drains its outstanding and queued debug events through `EXIT_PROCESS` within five seconds, then verifies the process handle before detaching. It withholds output when process exit cannot be confirmed.
To compare a viewer frame with a native capture, use the same phase JSON for the camera and the PNG conversion. When `atmosphere_seconds` is present, the viewer uses it unless `--atmosphere-seconds` is given, and holds the atmosphere schedule at that sample for the fixed-camera readback. Choose fresh `viewer.bin` and `viewer.png` paths because the viewer writes the raw file and conversion refuses to overwrite an existing PNG.
@@ -146,5 +148,6 @@ viewport, то есть к PNG просмотрщика. ОригинальныÐ
без перевода в линейное цветовое пространство. Скрипт отклоняет несовпадающие
размеры, области за границей кадра и существующие выходные файлы. Совпадение
средних цветов не доказывает совпадение геометрии, поэтому пару нужно также
-просмотреть. Камера и фаза должны происходить из одного нативного захвата;
-интерьер снимается после настоящего входа игрока в здание.
+просмотреть. Камера и фаза должны происходить из одного нативного захвата.
+Для интерьера нужно проверить наличие комнат в PNG: выбранная камера проходит
+два предварительных мировых кадра, а при пассивном захвате игрок входит в здание.