aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke.rs12
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs62
-rw-r--r--crates/fparkan-fx/src/ambient_op3.rs136
-rw-r--r--docs/reference/environment-effects.md43
4 files changed, 235 insertions, 18 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
index a2481f7..90bce14 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
@@ -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,
diff --git a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs
index 89ca43a..05ce927 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs
@@ -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]);
+ }
+ }
+}
diff --git a/crates/fparkan-fx/src/ambient_op3.rs b/crates/fparkan-fx/src/ambient_op3.rs
index 8f9a05c..1ec2973 100644
--- a/crates/fparkan-fx/src/ambient_op3.rs
+++ b/crates/fparkan-fx/src/ambient_op3.rs
@@ -979,6 +979,142 @@ mod tests {
}
#[test]
+ #[ignore = "requires the original GOG installation"]
+ fn original_tree_ray_58_matches_native_type1_geometry_samples() {
+ let root = std::env::var_os("FPARKAN_CORPUS_PART1_ROOT")
+ .expect("set FPARKAN_CORPUS_PART1_ROOT to the original GOG installation");
+ let bytes = std::fs::read(std::path::Path::new(&root).join("effects.rlb"))
+ .expect("read effects.rlb");
+ let archive = fparkan_nres::decode(bytes.into(), fparkan_nres::ReadProfile::Compatible)
+ .expect("decode effects.rlb");
+ let entry = archive.find("tree_ray_58").expect("tree_ray_58 FXID");
+ let document = crate::decode_fxid(
+ archive
+ .payload(entry)
+ .expect("FXID payload")
+ .to_vec()
+ .into(),
+ )
+ .expect("decode tree_ray_58");
+ assert_eq!(document.header().time_mode, 2);
+ assert_eq!(document.header().duration_seconds, 3.0);
+ assert_eq!(document.commands().len(), 3);
+
+ let parent = [
+ [0.999_924_123, 0.012_319_660, 0.0, 386.673_400_879],
+ [-0.012_319_660, 0.999_924_123, 0.0, 603.692_321_777],
+ [0.0, 0.0, 1.0, 180.414_703_369],
+ [0.0, 0.0, 0.0, 1.0],
+ ];
+ let center = [390.273_132, 603.647_949, 180.414_703];
+ // Original GOG Effect.dll RVA 0x93D0, x87 CW 0x007F; Rust-sampled keys are supplied separately.
+ let native_samples = [
+ (
+ 0,
+ "defl_y_add",
+ [0.0, 35.0, 22.0],
+ [
+ [386.280, 604.229_431, 179.568_024],
+ [386.266_876, 603.165_039, 181.261_383],
+ [394.266_266, 603.066_467, 181.261_383],
+ [394.279_4, 604.130_859, 179.568_024],
+ ],
+ ),
+ (
+ 0,
+ "defl_y_add",
+ [20.0, -30.0, 15.0],
+ [
+ [386.279, 604.147_4, 181.307_663],
+ [386.267_883, 603.247_1, 179.521_744],
+ [394.267_273, 603.148_5, 179.521_744],
+ [394.278_381, 604.048_8, 181.307_663],
+ ],
+ ),
+ (
+ 1,
+ "defl_y1_add",
+ [0.0, 35.0, 22.0],
+ [
+ [386.276_7, 603.963_3, 179.991_364],
+ [386.270_172, 603.431_152, 180.838_043],
+ [394.269_562, 603.332_6, 180.838_043],
+ [394.276_1, 603.864_746, 179.991_364],
+ ],
+ ),
+ ];
+ for (command_index, material_name, camera_offset, expected_vertices) in native_samples {
+ let camera = std::array::from_fn(|axis| center[axis] + camera_offset[axis]);
+ let frame = evaluate_op3_type1_command(
+ &document.commands()[command_index],
+ command_index,
+ &parent,
+ 0.0,
+ 0.0,
+ camera,
+ )
+ .expect("active tree ray quad");
+ assert_eq!(frame.material.name, material_name);
+ for (actual, expected) in frame
+ .vertices
+ .iter()
+ .flatten()
+ .zip(expected_vertices.iter().flatten())
+ {
+ assert!(
+ (actual - expected).abs() < 0.001,
+ "native sample vertex mismatch: {actual} vs {expected}"
+ );
+ }
+ }
+
+ for (command_index, material_name) in [(0, "defl_y_add"), (1, "defl_y1_add")] {
+ let command = &document.commands()[command_index];
+ assert_eq!(command.opcode, FxOpcode::Op3);
+ assert_eq!(
+ read_u32(&command.raw_body, 0),
+ Some(TYPE1_BODY_DISCRIMINATOR)
+ );
+ let camera_offsets = [[0.0, 35.0, 22.0], [20.0, -30.0, 15.0], [-25.0, 18.0, 10.0]];
+ for elapsed in [0.0_f32, 0.375, 0.75, 1.5, 2.25, 2.999, 3.125] {
+ let phase = elapsed.rem_euclid(3.0) / 3.0;
+ for camera_offset in camera_offsets {
+ let camera = std::array::from_fn(|axis| center[axis] + camera_offset[axis]);
+ let frame = evaluate_op3_type1_command(
+ command,
+ command_index,
+ &parent,
+ elapsed,
+ phase,
+ camera,
+ )
+ .expect("active tree ray quad");
+ assert_eq!(frame.material.name, material_name);
+ assert!(frame.active);
+ let actual_center: [f32; 3] = std::array::from_fn(|axis| {
+ frame
+ .vertices
+ .iter()
+ .map(|vertex| vertex[axis])
+ .sum::<f32>()
+ * 0.25
+ });
+ for axis in 0..3 {
+ assert!((actual_center[axis] - center[axis]).abs() < 0.001);
+ }
+ let authored_axis_edge: [f32; 3] = std::array::from_fn(|axis| {
+ frame.vertices[2][axis] - frame.vertices[1][axis]
+ });
+ let native_axis_edge = [7.999_393, -0.098_557_28, 0.0];
+ for axis in 0..3 {
+ assert!((authored_axis_edge[axis] - native_axis_edge[axis]).abs() < 0.001);
+ }
+ }
+ }
+ }
+ }
+
+ #[test]
fn rejects_truncated_bodies_and_non_finite_tracks_or_inputs() {
let identity = [
[1.0, 0.0, 0.0, 0.0],
diff --git a/docs/reference/environment-effects.md b/docs/reference/environment-effects.md
index 42e203c..799a969 100644
--- a/docs/reference/environment-effects.md
+++ b/docs/reference/environment-effects.md
@@ -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-команд
@@ -312,6 +328,17 @@ RVA `0x93D0`, ветка mode 1 с `0x9739`, сборка столбцов `0x97
двух столбцов могут немного отличаться от единицы из-за приближённой
нормализации выбранного `g_FastProc`; порядок осей от этого не меняется.
+У сооружения `s_tree_58` в миссии «Конструктор» FXID `tree_ray_58` задаёт два
+слоя жёлтого луча: команды Op3 mode 1 с ресурсами `defl_y_add` и `defl_y1_add`.
+Их геометрию сверили с CPU-вызовом GOG `Effect.dll` `0x93D0`. На семи временных
+фазах и трёх направлениях камеры для каждой команды (42 выборки) вершины
+совпали с Rust evaluator с максимальной покомпонентной разницей `0.0001373291`.
+Оракул получает уже выбранные Rust позицию, ось и масштаб, а world-camera
+переводится в `cameraLocal` тем же component-wise projection, что и native
+`FastProc + 0x68`. Поэтому это подтверждает только геометрию Type 1 helper-а на
+этих входах: обновление и расписание эффекта, UV/material и пиксельное
+совпадение здесь не проверялись.
+
## Частицы Op7
`fparkan-fx::op7` вычисляет фиксированный набор частиц Op7 по body команды,