fix(render): use native shifted water reflection camera
Docs Deploy / Build and Deploy MkDocs (push) Successful in 35s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 35s
This commit is contained in:
7 files changed
+168
-25
No files matched your search
@@ -12,8 +12,9 @@
|
|||||||
Рельеф переключает исходные LOD0/LOD1 по ячейкам относительно текущей камеры;
|
Рельеф переключает исходные LOD0/LOD1 по ячейкам относительно текущей камеры;
|
||||||
под основаниями строений сохраняется подробная сетка.
|
под основаниями строений сохраняется подробная сетка.
|
||||||
Горизонтальная вода использует плоское отражение ближайшей активной высоты;
|
Горизонтальная вода использует плоское отражение ближайшей активной высоты;
|
||||||
волны используют карту Du/Dv и часы оригинала, а shifted-камера отражения
|
волны используют карту Du/Dv и часы оригинала. Камера отражения смотрит вверх
|
||||||
пока заменена обычным зеркалом.
|
из отражённой точки наблюдения через прямоугольник водоёма; глубина и часть
|
||||||
|
параметров отражённого вида пока заимствуются из основного прохода.
|
||||||
|
|
||||||
## Запуск
|
## Запуск
|
||||||
|
|
||||||
|
|||||||
@@ -82,12 +82,15 @@ void main() {
|
|||||||
vec2 bump_uv = plane_uv * frame.embm_wave.y + frame.embm_wave.x;
|
vec2 bump_uv = plane_uv * frame.embm_wave.y + frame.embm_wave.x;
|
||||||
vec2 duv = max(vec2(-1.0), (texture(embm_bump, bump_uv).rg * 255.0 - 128.0) / 127.0);
|
vec2 duv = max(vec2(-1.0), (texture(embm_bump, bump_uv).rg * 255.0 - 128.0) / 127.0);
|
||||||
vec2 displacement = duv * frame.embm_wave.zw;
|
vec2 displacement = duv * frame.embm_wave.zw;
|
||||||
// shortcut: convert native map-UV displacement to the current mirror projection;
|
if ((mode & 128) != 0) {
|
||||||
// replace this projection when accepting the original shifted reflection camera.
|
reflection_uv = plane_uv + displacement;
|
||||||
vec3 displaced_world = in_world_position - vec3(displacement.yx * span, 0.0);
|
} else {
|
||||||
vec4 displaced_clip = frame.clip_from_world * vec4(displaced_world, 1.0);
|
// shortcut: non-perspective API cameras retain the projective mirror fallback.
|
||||||
if (displaced_clip.w > 0.000001) {
|
vec3 displaced_world = in_world_position - vec3(displacement.yx * span, 0.0);
|
||||||
reflection_uv = displaced_clip.xy / displaced_clip.w * 0.5 + 0.5;
|
vec4 displaced_clip = frame.clip_from_world * vec4(displaced_world, 1.0);
|
||||||
|
if (displaced_clip.w > 0.000001) {
|
||||||
|
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 rgb = texture(reflection_color, reflection_uv).rgb * in_base_diffuse + in_base_specular;
|
||||||
out_color = vec4(mix(rgb * in_color, frame.fog_color.rgb, in_fog_factor), material.diffuse_alpha);
|
out_color = vec4(mix(rgb * in_color, frame.fog_color.rgb, in_fog_factor), material.diffuse_alpha);
|
||||||
|
|||||||
Binary file not shown.
@@ -373,7 +373,7 @@ impl VulkanSmokeRenderer {
|
|||||||
plane_heights.sort_unstable();
|
plane_heights.sort_unstable();
|
||||||
plane_heights.dedup();
|
plane_heights.dedup();
|
||||||
if !plane_heights.is_empty() {
|
if !plane_heights.is_empty() {
|
||||||
eprintln!("render_coverage planar_reflection_shifted_camera_unsupported={} (native bump map and clock; using a projective mirror until shifted-camera mapping is implemented)", plane_heights.len());
|
eprintln!("render_coverage planar_reflection_render_context_approx={} (native shifted window and bump UV; main-view depth range, sky/fog/FX and swapchain-sized target remain approximations; non-perspective cameras use mirror fallback)", plane_heights.len());
|
||||||
eprintln!("render_coverage planar_reflection_other_planes_unsupported={} (nearest active plane is reflected)", plane_heights.len().saturating_sub(1));
|
eprintln!("render_coverage planar_reflection_other_planes_unsupported={} (nearest active plane is reflected)", plane_heights.len().saturating_sub(1));
|
||||||
}
|
}
|
||||||
if unsupported_nonplanar != 0 {
|
if unsupported_nonplanar != 0 {
|
||||||
@@ -1564,11 +1564,18 @@ impl VulkanSmokeRenderer {
|
|||||||
.active_reflection_plane()
|
.active_reflection_plane()
|
||||||
.map(|height| {
|
.map(|height| {
|
||||||
let mut frame = self.frame_uniforms;
|
let mut frame = self.frame_uniforms;
|
||||||
frame.clip_from_world =
|
frame.clip_from_world = fparkan_render::water::legacy_embm_reflection_clip(
|
||||||
|
frame.clip_from_world,
|
||||||
|
frame.camera_position,
|
||||||
|
height,
|
||||||
|
frame.embm_bounds,
|
||||||
|
)
|
||||||
|
.or_else(|| {
|
||||||
fparkan_render::reflect_clip_from_world_about_z(frame.clip_from_world, height)
|
fparkan_render::reflect_clip_from_world_about_z(frame.clip_from_world, height)
|
||||||
.ok_or(VulkanSmokeRendererError::InvalidStaticCamera {
|
})
|
||||||
context: "non-finite reflected clip transform",
|
.ok_or(VulkanSmokeRendererError::InvalidStaticCamera {
|
||||||
})?;
|
context: "non-finite reflected clip transform",
|
||||||
|
})?;
|
||||||
frame.camera_position[2] = 2.0 * height - frame.camera_position[2];
|
frame.camera_position[2] = 2.0 * height - frame.camera_position[2];
|
||||||
if !frame.is_finite() {
|
if !frame.is_finite() {
|
||||||
return Err(VulkanSmokeRendererError::InvalidStaticCamera {
|
return Err(VulkanSmokeRendererError::InvalidStaticCamera {
|
||||||
@@ -1819,6 +1826,15 @@ impl VulkanSmokeRenderer {
|
|||||||
self.draw_order_dirty = false;
|
self.draw_order_dirty = false;
|
||||||
}
|
}
|
||||||
let reflection_plane = self.active_reflection_plane();
|
let reflection_plane = self.active_reflection_plane();
|
||||||
|
let shifted_reflection = reflection_plane.is_some_and(|height| {
|
||||||
|
fparkan_render::water::legacy_embm_reflection_clip(
|
||||||
|
self.frame_uniforms.clip_from_world,
|
||||||
|
self.frame_uniforms.camera_position,
|
||||||
|
height,
|
||||||
|
self.frame_uniforms.embm_bounds,
|
||||||
|
)
|
||||||
|
.is_some()
|
||||||
|
});
|
||||||
let device = self.device_ref()?;
|
let device = self.device_ref()?;
|
||||||
let swapchain = self.swapchain_ref()?;
|
let swapchain = self.swapchain_ref()?;
|
||||||
let resources = self.resources_ref()?;
|
let resources = self.resources_ref()?;
|
||||||
@@ -2053,7 +2069,7 @@ impl VulkanSmokeRenderer {
|
|||||||
&& reflection_plane.is_some()
|
&& reflection_plane.is_some()
|
||||||
&& self.reflection_planes[range_index] == reflection_plane
|
&& self.reflection_planes[range_index] == reflection_plane
|
||||||
{
|
{
|
||||||
64
|
64 | if shifted_reflection { 128 } else { 0 }
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}) as f32;
|
}) as f32;
|
||||||
|
|||||||
@@ -51,10 +51,118 @@ pub fn legacy_embm_uv(world_xy: [f32; 2], bounds: [f32; 4]) -> Option<[f32; 2]>
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The EMBM camera looks up from the mirrored eye, with its window on the water rectangle.
|
||||||
|
/// Terrain.dll+4EFF0 supplies `[min_y-eye_y, min_x-eye_x, span_y, span_x, eye_z-height]`.
|
||||||
|
/// The ordinary perspective's depth mapping is retained; orthographic inputs use the caller's fallback.
|
||||||
|
#[must_use]
|
||||||
|
pub fn legacy_embm_reflection_clip(
|
||||||
|
main_clip: [f32; 16],
|
||||||
|
eye: [f32; 3],
|
||||||
|
height: f32,
|
||||||
|
bounds: [f32; 4],
|
||||||
|
) -> Option<[f32; 16]> {
|
||||||
|
let span = [bounds[2] - bounds[0], bounds[3] - bounds[1]];
|
||||||
|
let distance = eye[2] - height;
|
||||||
|
if !main_clip
|
||||||
|
.into_iter()
|
||||||
|
.chain(eye)
|
||||||
|
.chain([height])
|
||||||
|
.chain(bounds)
|
||||||
|
.chain(span)
|
||||||
|
.all(f32::is_finite)
|
||||||
|
|| distance <= 0.0
|
||||||
|
|| span.into_iter().any(|v| v <= 0.0)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let w = [main_clip[3], main_clip[7], main_clip[11]];
|
||||||
|
let squared = w.into_iter().map(|v| v * v).sum::<f32>();
|
||||||
|
if !squared.is_finite() || squared <= f32::EPSILON {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let depth_a = (main_clip[2] * w[0] + main_clip[6] * w[1] + main_clip[10] * w[2]) / squared;
|
||||||
|
// shortcut: retain the main depth range until the native shifted depth matrix is verified.
|
||||||
|
let depth_b = (main_clip[14] - depth_a * main_clip[15]) / squared.sqrt();
|
||||||
|
if !depth_a.is_finite() || !depth_b.is_finite() || depth_a < 1.0 || depth_b >= 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let z = 2.0 * height - eye[2];
|
||||||
|
let x_scale = -2.0 * distance / span[1];
|
||||||
|
let y_scale = -2.0 * distance / span[0];
|
||||||
|
let x_shift = 1.0 + 2.0 * (bounds[1] - eye[1]) / span[1];
|
||||||
|
let y_shift = 1.0 + 2.0 * (bounds[0] - eye[0]) / span[0];
|
||||||
|
let matrix = [
|
||||||
|
0.0,
|
||||||
|
y_scale,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
x_scale,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
x_shift,
|
||||||
|
y_shift,
|
||||||
|
depth_a,
|
||||||
|
1.0,
|
||||||
|
-x_scale * eye[1] - x_shift * z,
|
||||||
|
-y_scale * eye[0] - y_shift * z,
|
||||||
|
depth_b - depth_a * z,
|
||||||
|
-z,
|
||||||
|
];
|
||||||
|
matrix.into_iter().all(f32::is_finite).then_some(matrix)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shifted_camera_maps_native_water_corners_and_retains_perspective_depth() {
|
||||||
|
let projection = crate::LegacyD3d7Projection {
|
||||||
|
viewport: [0, 0, 1280, 1024],
|
||||||
|
near_plane: 0.5,
|
||||||
|
far_plane: 700.0,
|
||||||
|
field_of_view_radians: 1.3,
|
||||||
|
}
|
||||||
|
.try_direct3d7_projection_row_major()
|
||||||
|
.unwrap();
|
||||||
|
let eye = [1200.0, 800.0, 185.0];
|
||||||
|
let mut main = [0.0; 16];
|
||||||
|
main[2] = projection[10];
|
||||||
|
main[3] = projection[11];
|
||||||
|
main[14] = -eye[0] * projection[10] + projection[14];
|
||||||
|
main[15] = -eye[0] * projection[11];
|
||||||
|
let bounds = [1000.0, 500.0, 1500.0, 1200.0];
|
||||||
|
let matrix = legacy_embm_reflection_clip(main, eye, 150.0, bounds).unwrap();
|
||||||
|
let project = |point: [f32; 3]| -> [f32; 4] {
|
||||||
|
std::array::from_fn(|column| {
|
||||||
|
point[0] * matrix[column]
|
||||||
|
+ point[1] * matrix[4 + column]
|
||||||
|
+ point[2] * matrix[8 + column]
|
||||||
|
+ matrix[12 + column]
|
||||||
|
})
|
||||||
|
};
|
||||||
|
for point in [
|
||||||
|
[1000.0, 500.0, 150.0],
|
||||||
|
[1500.0, 1200.0, 150.0],
|
||||||
|
[1125.0, 1025.0, 150.0],
|
||||||
|
] {
|
||||||
|
let clip = project(point);
|
||||||
|
let expected = legacy_embm_uv([point[0], point[1]], bounds).unwrap();
|
||||||
|
for axis in 0..2 {
|
||||||
|
assert!((clip[axis] / clip[3] * 0.5 + 0.5 - expected[axis]).abs() < 0.00001);
|
||||||
|
}
|
||||||
|
assert_eq!(clip[3], 35.0);
|
||||||
|
}
|
||||||
|
let near = project([1200.0, 800.0, 115.5]);
|
||||||
|
assert!((near[2] / near[3]).abs() < 0.001);
|
||||||
|
let far = project([1200.0, 800.0, 815.0]);
|
||||||
|
assert!((far[2] / far[3] - 1.0).abs() < 0.00001);
|
||||||
|
assert!(legacy_embm_reflection_clip([0.0; 16], eye, 150.0, bounds).is_none());
|
||||||
|
assert!(legacy_embm_reflection_clip(main, eye, 185.0, bounds).is_none());
|
||||||
|
assert!(legacy_embm_reflection_clip(main, eye, 150.0, [1.0; 4]).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wave_bytes_match_native_generation_and_clock_wraps_without_float_time_loss() {
|
fn wave_bytes_match_native_generation_and_clock_wraps_without_float_time_loss() {
|
||||||
let texels = legacy_embm_bump_map();
|
let texels = legacy_embm_bump_map();
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ MSH-запрос со свободной preview-камерой; это не п
|
|||||||
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
|
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
|
||||||
log на минимальных сценах с одним типом материала.
|
log на минимальных сценах с одним типом материала.
|
||||||
|
|
||||||
Горизонтальные грани Land.msh с флагом `0x20000` используют Vulkan-отражение и исходную карту волн Du/Dv; перенос её смещений в обычное зеркало приближённый: `planar_reflection_shifted_camera_unsupported`.
|
Горизонтальные грани Land.msh с флагом `0x20000` используют исходную карту Du/Dv и окно shifted-камеры, проверенное исполнением Terrain.dll. Диапазон глубины, небо/туман/FX основного вида и размер target остаются приближениями: `planar_reflection_render_context_approx`; неперспективная камера API использует обычное зеркало.
|
||||||
Raw queue clock задаёт фазу волн отдельно от времени суток и sky elapsed; старые захваты без него считаются в `captured_water_phase_unverified`.
|
Raw queue clock задаёт фазу волн отдельно от времени суток и sky elapsed; старые захваты без него считаются в `captured_water_phase_unverified`.
|
||||||
Один target отражает ближайшую активную высоту воды; остальные высоты и неплоские грани считаются в `planar_reflection_other_planes_unsupported` и `planar_reflection_nonplanar_unsupported` и сохраняют обычный terrain shader.
|
Один target отражает ближайшую активную высоту воды; остальные высоты и неплоские грани считаются в `planar_reflection_other_planes_unsupported` и `planar_reflection_nonplanar_unsupported` и сохраняют обычный terrain shader.
|
||||||
`terrain_face8_pipeline_unverified` теперь считает только исходные грани с `0x8` без `0x20000`; сама семантика оставшихся специальных проходов ещё не установлена.
|
`terrain_face8_pipeline_unverified` теперь считает только исходные грани с `0x8` без `0x20000`; сама семантика оставшихся специальных проходов ещё не установлена.
|
||||||
|
|||||||
+25
-10
@@ -1147,13 +1147,27 @@ lightmap state передаются в `VulkanStaticMaterial`, а shader пос
|
|||||||
В [эталонной сборке](../reference/original-binaries.md) выбор грани находится
|
В [эталонной сборке](../reference/original-binaries.md) выбор грани находится
|
||||||
в Terrain.dll RVA `0x28A50`, а подготовка EMBM-пакета — в RVA `0x1B540`.
|
в Terrain.dll RVA `0x28A50`, а подготовка EMBM-пакета — в RVA `0x1B540`.
|
||||||
|
|
||||||
Первый Vulkan-проход строит плоское зеркало. Для горизонтальной воды на
|
Для горизонтальной воды на высоте `h` камера отражения находится в точке
|
||||||
высоте `h` отражённая координата точки равна `z′ = 2h − z`. Это преобразование
|
`[eye_x, eye_y, 2h − eye_z]`. Но она не повторяет наклон основной камеры:
|
||||||
применяется перед обычной проекцией. Оно меняет порядок вершин треугольника,
|
смотрит вертикально вверх, а её окно лежит на прямоугольнике водоёма. Поэтому
|
||||||
поэтому проход меняет сторону отсечения и отбрасывает геометрию ниже воды.
|
текстура отражения привязана к местности, а не к экрану игрока. В плоскости
|
||||||
|
воды координаты изображения равны `U = 1 − Ynorm`, `V = 1 − Xnorm`.
|
||||||
|
|
||||||
|
Terrain.dll RVA `0x4EFF0` передаёт shifted-проекции пять значений:
|
||||||
|
`[min_y − eye_y, min_x − eye_x, span_y, span_x, eye_z − h]`. Например,
|
||||||
|
для наблюдателя `[1200, 800, 185]`, воды `150` и границ
|
||||||
|
`[1000, 500, 1500, 1200]` это `[-300, -200, 700, 500, 35]`.
|
||||||
|
Матрица камеры и эти аргументы проверены исполнением оригинальной функции
|
||||||
|
при двух положениях и разных поворотах основной камеры. Оракул подменяет
|
||||||
|
ввод-вывод очереди, ландшафта и камеры; сам renderer и матрица глубины в эту
|
||||||
|
проверку не входят. Vulkan-проекция сохраняет near/far основного вида;
|
||||||
|
Rust-тест отдельно проверяет углы водоёма и границы глубины.
|
||||||
|
|
||||||
|
Отражённый вид меняет сторону отсечения и отбрасывает геометрию ниже воды.
|
||||||
Небо и оставшаяся сцена попадают в отдельную цветовую текстуру с собственной
|
Небо и оставшаяся сцена попадают в отдельную цветовую текстуру с собственной
|
||||||
глубиной. В основном проходе водные треугольники читают её в экранной
|
глубиной. В основном проходе водные треугольники читают её по координатам
|
||||||
координате: точки самой плоскости не сдвигаются при отражении.
|
водоёма с добавленным смещением волн. Для неперспективной камеры API остаётся
|
||||||
|
обычное зеркало с экранными координатами.
|
||||||
|
|
||||||
Цветовой target переходит из attachment layout в shader-read layout после
|
Цветовой target переходит из attachment layout в shader-read layout после
|
||||||
записи. Следующий зеркальный проход ждёт предыдущего чтения этой текстуры;
|
записи. Следующий зеркальный проход ждёт предыдущего чтения этой текстуры;
|
||||||
@@ -1182,10 +1196,11 @@ lightmap state передаются в `VulkanStaticMaterial`, а shader пос
|
|||||||
В Vulkan карта хранится в двух каналах несжатой текстуры и возвращается к
|
В Vulkan карта хранится в двух каналах несжатой текстуры и возвращается к
|
||||||
знаковым значениям перед вычислением смещения.
|
знаковым значениям перед вычислением смещения.
|
||||||
|
|
||||||
Обычное зеркало ещё не повторяет shifted-камеру оригинала: смещения Du/Dv
|
Матрица положения камеры, её окно и координаты волн восстановлены, но весь
|
||||||
сейчас переводятся из координат водоёма в текущую зеркальную проекцию.
|
контекст отражённого вида ещё не повторён. Диапазон глубины, небо, туман и FX
|
||||||
Это приближение учитывается в `planar_reflection_shifted_camera_unsupported`;
|
заимствуются из основного вида, а размер target совпадает со swapchain.
|
||||||
совпадение генератора волн не доказывает совпадения итогового отражения.
|
Эти ограничения учитываются в `planar_reflection_render_context_approx`;
|
||||||
|
совпадение генератора волн и окна не доказывает совпадения итогового отражения.
|
||||||
Один target обслуживает ближайшую
|
Один target обслуживает ближайшую
|
||||||
активную высоту воды; другие высоты и негоризонтальные поверхности явно
|
активную высоту воды; другие высоты и негоризонтальные поверхности явно
|
||||||
считаются как неподдержанные. Это промежуточный водный проход, а не полная
|
считаются как неподдержанные. Это промежуточный водный проход, а не полная
|
||||||
|
|||||||
Reference in new issue
Block a user