fix(render): preserve native world viewport depth range
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s

This commit is contained in:
Valentin Popov committed 2026-10-30 18:28:45 +04:00
1 parent bce6e3b263
commit 2e5058078c
3 files changed
+88 -18

No files matched your search

@@ -1808,6 +1808,7 @@ impl VulkanSmokeRenderer {
0,
vk::IndexType::UINT32,
);
let mut viewport_depth = None;
for &range_index in &self.draw_order {
let range = self.draw_ranges.get(range_index).ok_or(
VulkanSmokeRendererError::InvariantViolation {
@@ -1929,6 +1930,17 @@ impl VulkanSmokeRenderer {
} else {
*material_mode
};
let viewport = super::swapchain_resources::draw_viewport(
swapchain.report.plan.extent,
material_mode,
);
let next_viewport_depth = [viewport.min_depth, viewport.max_depth];
if viewport_depth != Some(next_viewport_depth) {
device
.device()
.cmd_set_viewport(command_buffer, 0, &[viewport]);
viewport_depth = Some(next_viewport_depth);
}
let material_mode = [material_mode.to_ne_bytes()];
device.device().cmd_push_constants(
command_buffer,
@@ -922,8 +922,17 @@ fn create_texture_descriptor_bundle(
Ok((layout, pool, descriptor_sets, sampler))
}
fn extent_component_to_f32(value: u32) -> f32 {
value as f32
pub(super) fn draw_viewport(extent: (u32, u32), material_mode: f32) -> vk::Viewport {
// Ngi32 RVA 0x7170 maps world depth to 0.1..0.99; TL and sky depth bypass it.
let explicit_depth = material_mode.round() as u32 & 12 != 0;
vk::Viewport {
x: 0.0,
y: 0.0,
width: extent.0 as f32,
height: extent.1 as f32,
min_depth: if explicit_depth { 0.0 } else { 0.1 },
max_depth: if explicit_depth { 1.0 } else { 0.99 },
}
}
fn create_graphics_pipeline_cache(
@@ -1033,14 +1042,7 @@ fn create_graphics_pipeline(
let input_assembly_state = vk::PipelineInputAssemblyStateCreateInfo::default()
.topology(vk::PrimitiveTopology::TRIANGLE_LIST)
.primitive_restart_enable(false);
let viewports = [vk::Viewport {
x: 0.0,
y: 0.0,
width: extent_component_to_f32(extent.0),
height: extent_component_to_f32(extent.1),
min_depth: 0.0,
max_depth: 1.0,
}];
let viewports = [draw_viewport(extent, 0.0)];
let scissors = [vk::Rect2D {
offset: vk::Offset2D { x: 0, y: 0 },
extent: vk::Extent2D {
@@ -1105,6 +1107,9 @@ fn create_graphics_pipeline(
let color_blend_state = vk::PipelineColorBlendStateCreateInfo::default()
.logic_op_enable(false)
.attachments(&color_blend_attachments);
let dynamic_states = [vk::DynamicState::VIEWPORT];
let dynamic_state =
vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states);
let create_info = vk::GraphicsPipelineCreateInfo::default()
.stages(&shader_stages)
.vertex_input_state(&vertex_input_state)
@@ -1114,6 +1119,7 @@ fn create_graphics_pipeline(
.multisample_state(&multisample_state)
.depth_stencil_state(&depth_stencil_state)
.color_blend_state(&color_blend_state)
.dynamic_state(&dynamic_state)
.layout(pipeline_layout)
.render_pass(render_pass)
.subpass(0);
@@ -1328,3 +1334,39 @@ fn destroy_partial_swapchain_resources(
}
}
}
#[cfg(test)]
mod tests {
use super::draw_viewport;
#[test]
fn world_viewport_depth_keeps_distant_geometry_in_front_of_screen_sun() {
let projection = fparkan_render::LegacyD3d7Projection {
viewport: [0, 0, 1280, 1024],
near_plane: 0.5,
far_plane: 700.0,
field_of_view_radians: 1.04,
}
.try_direct3d7_projection_row_major()
.expect("native projection");
for mode in [0.0, 1.0, 2.0, 3.0, 16.0, 17.0] {
let viewport = draw_viewport((1280, 1024), mode);
assert_eq!([viewport.width, viewport.height], [1280.0, 1024.0]);
assert_eq!([viewport.min_depth, viewport.max_depth], [0.1, 0.99]);
for distance in [0.5, 100.0, 350.0, 600.0, 700.0] {
let clip_z = distance * projection[10] + projection[14];
let clip_w = distance * projection[11];
let depth = viewport.min_depth
+ (viewport.max_depth - viewport.min_depth) * (clip_z / clip_w);
assert!(
depth < 0.999,
"mode {mode}, distance {distance}, depth {depth}"
);
}
}
for mode in [4.0, 5.0, 6.0, 7.0, 9.0] {
let viewport = draw_viewport((640, 480), mode);
assert_eq!([viewport.min_depth, viewport.max_depth], [0.0, 1.0]);
}
}
}
+24 -8
View File
@@ -256,14 +256,30 @@ Vertex shader пропускает `clip_from_world` для строк солн
остаются отдельным путём. Материал экранного quad не освещается, vertex fog
отключён, а sky far-depth bit к нему не применяется.
Native `z = 0.999` не означает, что спрайт гарантированно лежит за любой
поверхностью. Vulkan использует `LESS_OR_EQUAL` и не записывает depth для
прозрачного ряда. При near `0.5`, far `700` мировая поверхность на расстоянии
`320` получает depth
`700 / (700 - 0.5) * (1 - 0.5 / 320) ≈ 0.999151`; поэтому экранный `0.999`
чуть ближе этой поверхности и проходит depth test перед ней. Это фиксированное
native значение сохраняется для совместимости, но само по себе не обещает
окклюзию солнца всей геометрией местности.
Для мировой геометрии
оригинал задаёт viewport `MinZ = 0.1`, `MaxZ = 0.99`: после отсечения
проекционная глубина отображается в этот диапазон:
```text
world_depth = 0.1 + (0.99 - 0.1) * (clip_z / clip_w)
```
Даже дальняя гора получает глубину не больше `0.99` и закрывает экранный
диск с `z = 0.999`. Если оставить мировой диапазон `0–1`, при near `0.5`
и far `700` поверхность дальше примерно `292` единиц окажется позади диска:
солнце начнёт просвечивать сквозь неё при движении камеры. Одного переноса
нативного `z` в экранные вершины недостаточно.
В Vulkan диапазон задаётся динамическим viewport отдельно для мировых
пакетов; экранные TL и фоновые sky-пакеты сохраняют диапазон `0–1`.
Матрица проекции и плоскости отсечения при этом не меняются. Константы
мирового профиля записывает GOG `Terrain.dll` RVA `0x27422–0x27452` из таблицы
`0x65088/0x6508C/0x650A0/0x650A4` (near, far, MinZ, MaxZ). GOG `Ngi32.dll`
RVA `0x7170`, участок `0x71E7–0x723C`, копирует MinZ/MaxZ в renderer
`+0x30/+0x34` и передаёт их `IDirect3DDevice7::SetViewport`; сборки указаны в
[таблице бинарников](original-binaries.md). Нативные значения этих двух полей
имеют биты `0x3DCCCCCD` и `0x3F7D70A4`.
## Геометрия визуальных FX-команд