diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index 2ca517b..1db0996 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -353,7 +353,7 @@ mod static_mesh_tests { assert!(camera.is_finite()); assert_eq!(camera.clip_from_world[0], 0.65_f32.cos()); - assert_eq!(camera.clip_from_world[6], 700.0_f32 / 699.5); + assert_eq!(camera.clip_from_world[6], 0.65_f32.sin() * 700.0 / 699.5); assert_eq!(camera.clip_from_world[7], 0.65_f32.sin()); } diff --git a/crates/fparkan-render/src/lib.rs b/crates/fparkan-render/src/lib.rs index 89c29a6..96ccc2a 100644 --- a/crates/fparkan-render/src/lib.rs +++ b/crates/fparkan-render/src/lib.rs @@ -166,7 +166,7 @@ impl RawCameraTransform { m20, 0.0, m23.mul_add(m21, m13.mul_add(m11, m03 * m01)), - -(m23.mul_add(m12, m13.mul_add(m02, m03 * m10))), + -(m23.mul_add(m22, m13.mul_add(m12, m03 * m02))), -(m00.mul_add(m03, m23.mul_add(m20, m13 * m10))), 1.0, ]) @@ -193,10 +193,11 @@ pub struct LegacyD3d7Projection { impl LegacyD3d7Projection { /// Reconstructs the exact row-major matrix passed to D3D7 projection state. /// - /// Ngi32 uses the viewport's `height / width`, writes `cos(fov / 2)` to - /// the diagonal and `sin(fov / 2)` to the homogeneous-W term. The ratio - /// after D3D's perspective divide is therefore the expected cotangent - /// scale. This remains legacy D3D7 data rather than a Vulkan projection. + /// Ngi32 uses the viewport's `width / height`, writes `cos(fov / 2)` to + /// the diagonal and `sin(fov / 2)` to both the depth scale and + /// homogeneous-W term. The ratio after D3D's perspective divide is + /// therefore the expected cotangent scale. This remains legacy D3D7 data + /// rather than a Vulkan projection. #[must_use] pub fn try_direct3d7_projection_row_major(self) -> Option<[f32; 16]> { let width = self.viewport[2].checked_sub(self.viewport[0])?; @@ -217,11 +218,11 @@ impl LegacyD3d7Projection { // Ngi32 converts these signed viewport dimensions into single-precision // arithmetic before building the legacy matrix. #[allow(clippy::cast_precision_loss)] - let aspect = (height as f32) / (width as f32); + let aspect = (width as f32) / (height as f32); let half_fov = self.field_of_view_radians * 0.5; let cosine = half_fov.cos(); let sine = half_fov.sin(); - let depth_scale = 1.0 / (1.0 - self.near_plane / self.far_plane); + let depth_scale = sine / (1.0 - self.near_plane / self.far_plane); [aspect, cosine, sine, depth_scale] .iter() .all(|value| value.is_finite()) @@ -1185,7 +1186,7 @@ mod tests { assert_eq!( transform.try_direct3d7_view_row_major(), Some([ - 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, -10.0, -10.0, -20.0, + 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, -10.0, -30.0, -20.0, 1.0, ]) ); @@ -1207,10 +1208,10 @@ mod tests { .try_direct3d7_projection_row_major() .expect("live Ngi32 projection parameters are valid"); let half_fov = 0.65_f32; - let depth_scale = 700.0_f32 / 699.5; + let depth_scale = half_fov.sin() * 700.0_f32 / 699.5; assert_eq!(matrix[0], half_fov.cos()); - assert_eq!(matrix[5], 0.75 * half_fov.cos()); + assert_eq!(matrix[5], (4.0 / 3.0) * half_fov.cos()); assert_eq!(matrix[10], depth_scale); assert_eq!(matrix[11], half_fov.sin()); assert_eq!(matrix[14], -(depth_scale * 0.5)); diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index 07add33..7c1fba7 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -1398,13 +1398,14 @@ RVA `0x7030` exactly builds the row-major D3D7 projection from renderer FOV ```text [ cos(f/2), 0, 0, 0 ] -[ 0, h/w * cos(f/2), 0, 0 ] -[ 0, 0, z/(z-n), sin(f/2) ] -[ 0, 0, -n*z/(z-n), 0 ] +[ 0, w/h * cos(f/2), 0, 0 ] +[ 0, 0, sin(f/2)*z/(z-n), sin(f/2) ] +[ 0, 0, -sin(f/2)*n*z/(z-n), 0 ] ``` -The sine in the homogeneous-W term is intentional: after the D3D perspective -divide the diagonal has the expected cotangent scale. RVA `0x9450` applies a +The sine in the depth scale and homogeneous-W term is intentional: after the +D3D perspective divide the diagonal has the expected cotangent scale and the +depth range remains finite. RVA `0x9450` applies a specific axis permutation/sign change and translated dot products to selector 0 before the view `SetTransform`; it is not merely the generic affine inverse. @@ -1562,23 +1563,30 @@ hashes промежуточных buffers. Без такой трассиров [materials](../reference/materials.md), [Texm](../reference/texm.md) и [render frame](../reference/render-frame.md). -### Live AutoDemo: видимый результат пока не достигнут +### Live AutoDemo: empty frame устранён, но parity ещё не достигнут Read-only `PrintWindow` capture работающего GOG AutoDemo даёт полноценный оригинальный кадр (terrain, варбот и близкая geometry), поэтому теперь есть безвводный source для будущего visual comparison. Текущий полный static-Vulkan -preview нельзя описывать как видимую сцену: хотя он загружает 8 TMA objects, -66 MSH components и 67 descriptors, его изображение состоит только из clear -color. Validation остаётся `0/0`, но это доказывает корректность GPU lifecycle, -а не совпадение с оригинальным renderer. +preview раньше состоял только из clear color. Validation `0/0` тогда доказывал +только GPU lifecycle, но не совпадение с original renderer. Новая CPU-диагностика применяет к тем же source vertices ту же row-major D3D7 matrix, которую GLSL получает как column-major push constant. Для свежего captured Ngi32 camera (`viewport 1024x768`, near `5`, far `700`, FOV `1.3`) -она насчитала `clip_visible_vertices=0`. Следовательно, current empty frame не -объясняется texture selection, face culling (baseline state already disables -it) или Vulkan validation: следующий исследовательский шаг — восстановить -точный live camera transform/clip-space boundary. +она насчитала `clip_visible_vertices=0`. Последующая сверка с +`Ngi32!sub_10009450` и `sub_10007030` нашла два literal translation defects: +Y translation view matrix должна быть `-(m23*m22 + m13*m12 + m03*m02)`, а +projection использует `width/height` и множитель `sin(fov/2)` в обоих depth +coefficients. После исправления того же capture диагностировал +`clip_visible_vertices=938`, а `PrintWindow` native Vulkan window показал +реальную rasterized terrain/model geometry. Это исключает прежний empty-frame +барьер; face culling и texture selection не были его причиной. + +Получившийся кадр всё ещё явно не похож на оригинал: geometry сосредоточена у +границы и material/terrain representation грубая. Значит, следующие задачи — +camera timing/selection, source model-node transforms, terrain material phases, +lighting и visibility, а не объявление pixel parity. Параллельно bridge для legacy-camera path теперь переводит только высоту `Land.msh` в world units с масштабом `1/32`: raw AutoDemo heights