From 9f643149a69a149612cffd65a953527630e95647 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 31 Oct 2026 19:13:43 +0400 Subject: [PATCH] fix(render): use native squared distance for vertex fog --- .../shaders/triangle.vert | 9 ++-- .../shaders/triangle.vert.spv | Bin 20096 -> 20196 bytes .../src/ffi/smoke_types.rs | 40 +++++++++++++++++- apps/fparkan-game/src/main.rs | 7 +-- docs/appendices/knowledge-boundaries.md | 4 ++ docs/reference/render-frame.md | 20 +++++++++ docs/tomes/05-render.md | 3 ++ 7 files changed, 75 insertions(+), 8 deletions(-) diff --git a/adapters/fparkan-render-vulkan/shaders/triangle.vert b/adapters/fparkan-render-vulkan/shaders/triangle.vert index d1e2e04..078a0d1 100644 --- a/adapters/fparkan-render-vulkan/shaders/triangle.vert +++ b/adapters/fparkan-render-vulkan/shaders/triangle.vert @@ -130,12 +130,13 @@ float native_specular_power(float cosine, float power) { } float native_fog_factor(vec3 world_position) { - float fog_span = frame.fog_distances.y - frame.fog_distances.x; - float distance_to_camera = distance(world_position, frame.camera_position.xyz); - if (fog_span <= 0.000001 || distance_to_camera <= frame.fog_distances.x) { + float fog_span_squared = frame.fog_distances.w - frame.fog_distances.z; + vec3 to_camera = world_position - frame.camera_position.xyz; + float distance_squared = dot(to_camera, to_camera); + if (fog_span_squared <= 0.000001 || distance_squared <= frame.fog_distances.z) { return 0.0; } - return clamp((distance_to_camera - frame.fog_distances.x) / fog_span, 0.0, 1.0); + return clamp((distance_squared - frame.fog_distances.z) / fog_span_squared, 0.0, 1.0); } vec3 normalized_or(vec3 value, vec3 fallback) { diff --git a/adapters/fparkan-render-vulkan/shaders/triangle.vert.spv b/adapters/fparkan-render-vulkan/shaders/triangle.vert.spv index f74b1f81e5a1360c88f6f3752ec1e3d8e9ae40ff..4fc9a1b5a2843a9d466379f569fb8f1d0c7fd504 100644 GIT binary patch literal 20196 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 YJLYlk$n|xM{XW}X*!^}Z_S^0M0Wen-)Bpeg literal 20096 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 self.fog_start { + [self.fog_start * self.fog_start, self.fog_end * self.fog_end] + } else { + [0.0; 2] + }; + values[84..88].copy_from_slice(&[ + self.fog_start, + self.fog_end, + squared_fog_range[0], + squared_fog_range[1], + ]); values[88..92].copy_from_slice(&[ self.camera_position[0], self.camera_position[1], @@ -874,6 +887,31 @@ mod static_mesh_tests { assert_eq!(mesh.validate(), Ok(())); } + #[test] + fn fog_uniforms_match_live_native_squared_distance_range() { + // Terrain ShadeIndexedStrided: far=0x442389e6, far²=0x48d0f1ca; + // distance²=0x47707b04 gives visibility=0x3f5b2b94. + let frame = VulkanFrameUniforms { + fog_start: 0.0, + fog_end: f32::from_bits(0x4423_89e6), + ..Default::default() + }; + assert!(frame.is_finite()); + let bytes = frame.to_ne_bytes(); + let read = |offset| f32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()); + assert_eq!(read(344), 0.0); + assert_eq!(read(348).to_bits(), 0x48d0_f1ca); + let visibility = 1.0 - (f32::from_bits(0x4770_7b04) - read(344)) / (read(348) - read(344)); + assert!((visibility - f32::from_bits(0x3f5b_2b94)).abs() < 1.0e-6); + assert!(VulkanFrameUniforms::default().is_finite()); + assert!(!VulkanFrameUniforms { + fog_start: 0.0, + fog_end: f32::MAX, + ..frame + } + .is_finite()); + } + #[test] fn static_camera_composes_recovered_d3d7_view_before_projection() { let transform = RawCameraTransform { diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs index 30380ed..279fa2c 100644 --- a/apps/fparkan-game/src/main.rs +++ b/apps/fparkan-game/src/main.rs @@ -9454,6 +9454,7 @@ impl StaticVulkanApp { }) .count(); eprintln!("render_coverage control_op1_jitter_unsupported={randomized_lights} (using authored color and radius; native RNG sampling pending)"); + 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 { let range = self .mesh @@ -14499,7 +14500,7 @@ mod tests { } #[test] - fn captured_native_camera_uses_view_dependent_sky_fog_color() { + fn captured_native_camera_selects_view_dependent_screen_horizon_color() { use fparkan_fx::atmosphere::{PackedColor, SkySample, SKY_COLOR_COUNT}; // Compact camera fields from the native 1493.337-second AutoDemo capture. @@ -14521,12 +14522,12 @@ mod tests { values: [0.0; 2], packed: [PackedColor(0); 2], }; - let fog_color = + let horizon_color = fparkan_fx::sky::screen_gradient_frame(&sample, camera.native_sky_heading(), [0.0; 3]) .color; assert_eq!(capture.atmosphere_seconds, Some(1493.337)); - assert_eq!(fog_color, PackedColor(0xFF3E_3E64)); + assert_eq!(horizon_color, PackedColor(0xFF3E_3E64)); } #[test] diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md index 6fbedf4..7f049fc 100644 --- a/docs/appendices/knowledge-boundaries.md +++ b/docs/appendices/knowledge-boundaries.md @@ -10,6 +10,10 @@ authoring API без динамического подтверждения. Не доказаны символами точные имена renderer vtable slots, полный набор CShade state transitions и окончательный порядок части transparent/FX/shadow subpasses. +Дистанционная кривая тумана по квадратам границ подтверждена в живом CShade. +Выбор проходов по native shade-state и GPU fog color пока заменён общим туманом мира с цветом горизонта; +это приближение считается как `render_coverage fog_pass_eligibility_approx`. + После ограничения цели рельефом интерактивная камера проверяет движение против каждого отрисовываемого корневого строения класса `FORT` и его отрисовываемых компонентов. Для этого пути используются текущая Node38-поза diff --git a/docs/reference/render-frame.md b/docs/reference/render-frame.md index 3244d1b..b2a6a93 100644 --- a/docs/reference/render-frame.md +++ b/docs/reference/render-frame.md @@ -71,6 +71,26 @@ RVA `0x1B0F0`, заменяет diffuse alpha пакета без type 16 с bat путь и ещё не декодирован. Это ограничивает приёмку на частично отсечённых порталах и не позволяет считать интерьер принятой эталонной сценой. +## Дистанционная кривая тумана + +Программный `CShade::ShadeIndexedStrided` в GOG Terrain.dll сравнивает квадрат +дистанции с квадратами ближней и дальней границ. По RVA `0x2EEED–0x2EF45` +видимость равна `1 - (distance² - near²) / (far² - near²)` между границами, +единице перед ближней и нулю за дальней. Такой же закон использует отдельный +путь цвета пакета `0x2F230`. Линейная интерполяция по самой дистанции давала +слишком плотный туман в середине диапазона. + +В работающем оригинале границы 0 и `0x442389E6`, квадрат дистанции +`0x47707B04` дали видимость `0x3F5B2B94`. Rust-тест проверяет передаваемые GPU +квадраты границ по этому наблюдению. GLSL вычисляет квадрат расстояния вершины, +после чего растеризатор интерполирует коэффициент по треугольнику. +`native_fog.effective.sink_rgb` совпадает с `sky.packed[1]`, нижней границей +освещения CShade. Это не подтверждает значение GPU fog color. Приложение пока +сохраняет цвет экранного горизонта; выбор цвета и проходов требует отдельной +проверки GPU-состояния. +Выбор проходов по native shade-state пока приближён общим туманом мира и +считается как `render_coverage fog_pass_eligibility_approx`. + ## Радиус Control Op1 GOG `Effect.dll` (SHA-256 diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index bd23875..72b0685 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -845,6 +845,9 @@ projected shadows. Радиус источника масштабируется синий свет завода почти незаметным. Неположительный итог заменяется на 0,01. Случайные добавки к цвету и радиусу пока явно считаются в `render_coverage control_op1_jitter_unsupported`. +Туман также зависит от расстояния нелинейно: между ближней и дальней границами +движок интерполирует по квадрату дистанции. При границах 0 и 700 точка в 350 +получает четверть тумана, а не половину. Это сохраняет контраст среднего плана. Effect renderer подавляет header bit `0x800` только при render selector 0. Native AniMesh component path захвачен с selector 1, а в его 53-entry manager присутствуют FX entries с header `0xD00`; preview