diff options
| author | Valentin Popov <valentin@popov.link> | 2026-11-01 13:39:37 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-11-01 13:39:37 +0300 |
| commit | 55e98d7ff6224aa3bf4b5d4260a594705ee2133f (patch) | |
| tree | 828a6ea2ad0ebf7cc127e20fb8a0a03673636a35 | |
| parent | bbd31fc25336d6a2e6c5927ad4d7101696f8bd78 (diff) | |
| download | fparkan-55e98d7ff6224aa3bf4b5d4260a594705ee2133f.tar.xz fparkan-55e98d7ff6224aa3bf4b5d4260a594705ee2133f.zip | |
fix(render): draw translucent water into planar reflectionstrunk
| -rw-r--r-- | adapters/fparkan-render-vulkan/shaders/triangle.frag | 11 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/shaders/triangle.frag.spv | bin | 14716 -> 15060 bytes | |||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke.rs | 40 | ||||
| -rw-r--r-- | crates/fparkan-render/src/water.rs | 37 | ||||
| -rw-r--r-- | docs/appendices/knowledge-boundaries.md | 1 | ||||
| -rw-r--r-- | docs/reference/render-frame.md | 17 | ||||
| -rw-r--r-- | docs/tomes/05-render.md | 15 |
7 files changed, 112 insertions, 9 deletions
diff --git a/adapters/fparkan-render-vulkan/shaders/triangle.frag b/adapters/fparkan-render-vulkan/shaders/triangle.frag index 2c71278..b945905 100644 --- a/adapters/fparkan-render-vulkan/shaders/triangle.frag +++ b/adapters/fparkan-render-vulkan/shaders/triangle.frag @@ -75,7 +75,9 @@ void main() { if ((mode & 32) != 0 && (mode & 4) == 0 && in_world_position.z < frame.camera_position.w) { discard; } - if ((mode & 64) != 0) { + vec2 base_uv = vec2(material.base_page_x, material.base_page_y) + + in_uv * vec2(material.base_page_w, material.base_page_h); + if ((mode & (64 | 256)) != 0) { vec2 reflection_uv = gl_FragCoord.xy / vec2(textureSize(reflection_color, 0)); vec2 span = frame.embm_bounds.zw - frame.embm_bounds.xy; vec2 plane_uv = vec2(1.0) - (in_world_position.yx - frame.embm_bounds.yx) / span.yx; @@ -92,15 +94,16 @@ void main() { reflection_uv = displaced_clip.xy / displaced_clip.w * 0.5 + 0.5; } } - vec3 rgb = texture(reflection_color, reflection_uv).rgb * in_base_diffuse + in_base_specular; + vec3 water_color = (mode & 256) != 0 + ? texture(base_color, base_uv).rgb + : texture(reflection_color, reflection_uv).rgb; + vec3 rgb = water_color * in_base_diffuse + in_base_specular; out_color = vec4(mix(rgb * in_color, frame.fog_color.rgb, in_fog_factor), material.diffuse_alpha); if (out_color.a < material.alpha_cutoff) { discard; } return; } - vec2 base_uv = vec2(material.base_page_x, material.base_page_y) - + in_uv * vec2(material.base_page_w, material.base_page_h); vec2 detail_uv = vec2(material.detail_page_x, material.detail_page_y) + in_detail_uv * vec2(material.detail_page_w, material.detail_page_h); vec2 overlay_uv = vec2(material.overlay_page_x, material.overlay_page_y) diff --git a/adapters/fparkan-render-vulkan/shaders/triangle.frag.spv b/adapters/fparkan-render-vulkan/shaders/triangle.frag.spv Binary files differindex 6deef9d..02596d1 100644 --- a/adapters/fparkan-render-vulkan/shaders/triangle.frag.spv +++ b/adapters/fparkan-render-vulkan/shaders/triangle.frag.spv diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index c4f0264..8499c54 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -1936,7 +1936,16 @@ impl VulkanSmokeRenderer { &[], ); let mut viewport_depth = None; - for &range_index in &self.draw_order { + // Native reflection queues the translucent water after the reflected world. + let draw_order = + self.draw_order + .iter() + .copied() + .filter(|&index| !reflected || !self.draw_ranges[index].terrain_reflection) + .chain(self.draw_order.iter().copied().filter(|&index| { + reflected && self.draw_ranges[index].terrain_reflection + })); + for range_index in draw_order { let range = self.draw_ranges.get(range_index).ok_or( VulkanSmokeRendererError::InvariantViolation { context: "stable draw order range handle", @@ -1948,11 +1957,17 @@ impl VulkanSmokeRenderer { }, )?; let mode = self.material_unlit[texture_index].round() as i32; - if reflected && (range.terrain_reflection || mode & 8 != 0) { + if reflected && mode & 8 != 0 { continue; } let state = if reflected { - super::reflection::reflected_pipeline_state(range.pipeline_state) + let state = + super::reflection::reflected_pipeline_state(range.pipeline_state); + if range.terrain_reflection { + fparkan_render::water::legacy_reflected_water_pipeline(state) + } else { + state + } } else { range.pipeline_state }; @@ -1998,7 +2013,12 @@ impl VulkanSmokeRenderer { std::slice::from_ref(descriptor_set), &[frame_offset, material_offset, point_light_offset], ); - let alpha_cutoff = range.alpha_test_cutoff().to_ne_bytes(); + let alpha_cutoff = if state.alpha_test { + f32::from(range.alpha_test_reference) / 255.0 + } else { + 0.0 + } + .to_ne_bytes(); device.device().cmd_push_constants( command_buffer, resources.pipeline_layout, @@ -2017,8 +2037,13 @@ impl VulkanSmokeRenderer { .ok_or(VulkanSmokeRendererError::InvariantViolation { context: "draw-range diffuse-alpha override", })?; + let diffuse_alpha_override = if reflected && range.terrain_reflection { + Some(fparkan_render::water::REFLECTED_WATER_ALPHA) + } else { + *diffuse_alpha_override + }; let material_alphas = - draw_material_alphas(*material_alpha, *diffuse_alpha_override) + draw_material_alphas(*material_alpha, diffuse_alpha_override) .map(f32::to_ne_bytes) .concat(); device.device().cmd_push_constants( @@ -2075,6 +2100,11 @@ impl VulkanSmokeRenderer { }; let material_mode = (material_mode.round() as i32 | if reflected { 32 } else { 0 } + | if reflected && range.terrain_reflection { + 256 + } else { + 0 + } | if !reflected && reflection_plane.is_some() && self.reflection_planes[range_index] == reflection_plane diff --git a/crates/fparkan-render/src/water.rs b/crates/fparkan-render/src/water.rs index 58a2817..7a5ccbd 100644 --- a/crates/fparkan-render/src/water.rs +++ b/crates/fparkan-render/src/water.rs @@ -7,6 +7,21 @@ pub const EMBM_BUMP_TILE: f32 = 100.0; /// Native diagonal bump matrix, without luminance modulation. pub const EMBM_COEFFICIENTS: [f32; 2] = [0.01, 0.01]; +/// Terrain draws ordinary textured water into its reflection with this alpha. +pub const REFLECTED_WATER_ALPHA: f32 = 0.1; + +/// Native packet category 4 enables source-alpha blending without changing depth. +#[must_use] +pub fn legacy_reflected_water_pipeline( + state: crate::LegacyPipelineState, +) -> crate::LegacyPipelineState { + crate::LegacyPipelineState { + blend: crate::LegacyBlendMode::SourceAlpha, + alpha_test: true, + ..state + } +} + /// Square reflection size chosen by Terrain.dll RVA 0x27FC7 from renderer dimensions. #[must_use] pub fn legacy_embm_reflection_size(extent: [u32; 2]) -> u32 { @@ -128,6 +143,28 @@ mod tests { use super::*; #[test] + fn reflected_water_blends_material_alpha_and_preserves_depth_and_cull() { + use crate::{LegacyBlendMode, LegacyCullMode, LegacyDepthMode, LegacyPipelineState}; + for depth in [ + LegacyDepthMode::Disabled, + LegacyDepthMode::TestWrite, + LegacyDepthMode::TestReadOnly, + ] { + let original = LegacyPipelineState { + depth, + cull: LegacyCullMode::FrontFace, + ..LegacyPipelineState::default() + }; + let fallback = legacy_reflected_water_pipeline(original); + assert_eq!(fallback.blend, LegacyBlendMode::SourceAlpha); + assert!(fallback.alpha_test); + assert_eq!(fallback.depth, original.depth); + assert_eq!(fallback.cull, original.cull); + } + assert_eq!(REFLECTED_WATER_ALPHA.to_bits(), 0x3dcc_cccd); + } + + #[test] fn reflection_size_matches_native_renderer_dimension_vectors() { for (extent, expected) in [ ([1280, 1024], 1024), diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md index ed821e4..f0c0b0c 100644 --- a/docs/appendices/knowledge-boundaries.md +++ b/docs/appendices/knowledge-boundaries.md @@ -78,6 +78,7 @@ MSH-запрос со свободной preview-камерой; это не пÐ log на минимальных сценах с одним типом материала. Горизонтальные грани Land.msh с флагом `0x20000` используют исходную карту Du/Dv, окно shifted-камеры и квадратный размер текстуры, проверенные исполнением DLL. Диапазон глубины, небо/туман/FX основного вида и соответствие размеров при обрезанном capture остаются приближениями: `planar_reflection_render_context_approx`; неперспективная камера API использует обычное зеркало. +Обычный полупрозрачный слой воды внутри отражения восстановлен по живым пакетам и таблицам Ngi32. Ближняя вода в согласованных кадрах ещё отличается по яркости; полный контекст отражённого вида остаётся открытым. Raw queue clock задаёт фазу волн отдельно от времени суток и sky elapsed; старые захваты без него считаются в `captured_water_phase_unverified`. Один target отражает ближайшую активную высоту воды; остальные высоты и неплоские грани считаются в `planar_reflection_other_planes_unsupported` и `planar_reflection_nonplanar_unsupported` и сохраняют обычный terrain shader. `terrain_face8_pipeline_unverified` теперь считает только исходные грани с `0x8` без `0x20000`; сама семантика оставшихся специальных проходов ещё не установлена. diff --git a/docs/reference/render-frame.md b/docs/reference/render-frame.md index b3c0be5..4ee0cc3 100644 --- a/docs/reference/render-frame.md +++ b/docs/reference/render-frame.md @@ -404,3 +404,20 @@ D3D7 в `fparkan-render`, а затем один раз меняет знак в GPU может содержать результат смешивания поверхностей; повторное применение его к готовому цвету затемнило бы кадр ещё раз. Поэтому конвертер и сборщик сравнительных пар игнорируют эту альфу, а raw-файл сохраняют без изменений. + +### Вода внутри отражённого вида + +В эталонных DLL из [таблицы сборки](original-binaries.md) Terrain RVA +`0x28A50` задаёт водному пакету material alpha `0,1` и packet category `4`, +когда уже строится отражение. Живой CShade-захват подтверждает одинаковые +водные вершины при `eye_z=115` и `185`, высоте воды `150`: отражённые пакеты +имеют alpha `0,1`, основные — `1`. Это общий проход водных граней, +а не поправка цвета карты. + +Ngi32 mode 7, descriptors RVA `0x36C14` / `0x36C40`, содержит одну текстурную +стадию: `COLOR = TEXTURE × DIFFUSE`, `ALPHA = DIFFUSE`. Packet category 4 +в таблице RVA `0x346E0 + category × 40` включает SRCALPHA / INVSRCALPHA +и альфа-тест GREATEREQUAL. Это отдельная таблица от MAT0 categories. +Depth enable/write передаются из байтов пакета `+0x12C/+0x12D` отдельно; +в захваченном водном проходе оба равны `1`. Начальные ZFUNC=LESSEQUAL, +ALPHAREF=1 задаются конструктором Ngi32; категория их не меняет. diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index 8c059f2..387db11 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -1197,6 +1197,21 @@ Rust-тест отдельно проверяет углы водоёма и гÑ водоёма с добавленным смещением волн. Для неперспективной камеры API остаётся обычное зеркало с экранными координатами. +В отражённом виде видна и сама вода. После остального мира оригинал рисует +её обычной текстурой с непрозрачностью `0,1`; этот слой приглушает отражение +и сохраняет оттенок поверхности. Подготовка ландшафта задаёт такую альфу +и отдельную категорию пакета, когда уже строится отражение. Живой захват +подтверждает те же водные вершины у зеркальной камеры и у основной камеры: +в первом случае альфа равна `0,1`, во втором — `1`. + +Внутренний водный слой использует texture mode 7: цвет текстуры умножается +на diffuse, а альфа берётся только из diffuse. Затем обычное смешивание +`src × alpha + dst × (1 − alpha)` соединяет его с отражённым миром. +Проверка и запись глубины сохраняются, альфа-тест пропускает значения +не меньше `1/255`. FParkan воспроизводит этот проход после отражённой сцены +и читает исходную текстуру материала, поэтому изображение отражения +можно закончить до его чтения основной камерой. + Размер изображения отражения выбирается отдельно от размера основного кадра. Terrain.dll в конструкторе CShade (RVA `0x278E0`, цикл с `0x27FC7`) берёт ширину и высоту renderer через Ngi32.dll RVA `0x7FB0`. Сторона квадратной |
