feat(render): add planar water reflections
Docs Deploy / Build and Deploy MkDocs (push) Successful in 1m22s

This commit is contained in:
Valentin Popov committed 2026-11-01 05:31:43 +04:00
1 parent 2de24962e0
commit dfa2db65cc
17 files changed
+905 -191

No files matched your search

+2
View File
@@ -11,6 +11,8 @@
позе.
Рельеф переключает исходные LOD0/LOD1 по ячейкам относительно текущей камеры;
под основаниями строений сохраняется подробная сетка.
Горизонтальная вода использует плоское отражение ближайшей активной высоты;
волны EMBM и shifted-камера оригинала пока не воспроизводятся.
## Запуск
@@ -9,12 +9,14 @@ layout(location = 5) in vec3 in_base_diffuse;
layout(location = 6) in vec3 in_base_specular;
layout(location = 7) in vec3 in_overlay_diffuse;
layout(location = 8) in vec3 in_overlay_specular;
layout(location = 9) in float in_world_z;
layout(location = 0) out vec4 out_color;
layout(set = 0, binding = 0) uniform sampler2D base_color;
layout(set = 0, binding = 1) uniform sampler2D base_detail;
layout(set = 0, binding = 2) uniform sampler2D overlay_color;
layout(set = 0, binding = 3) uniform sampler2D overlay_detail;
layout(set = 1, binding = 0) uniform sampler2D reflection_color;
layout(std140, set = 0, binding = 4) uniform FrameConstants {
mat4 clip_from_world;
@@ -66,6 +68,19 @@ layout(push_constant) uniform MaterialConstants {
} material;
void main() {
int mode = int(round(material.material_mode));
if ((mode & 32) != 0 && (mode & 4) == 0 && in_world_z < frame.camera_position.w) {
discard;
}
if ((mode & 64) != 0) {
vec2 reflection_uv = gl_FragCoord.xy / vec2(textureSize(reflection_color, 0));
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);
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)
@@ -78,7 +93,6 @@ void main() {
vec4 detail = texture(base_detail, detail_uv);
vec4 overlay = texture(overlay_color, overlay_uv);
vec4 overlay_detail_sample = texture(overlay_detail, overlay_detail_uv);
int mode = int(round(material.material_mode));
int combiner_mode = mode & 3;
// Native sky mode 4 combines nebula and stars as
@@ -19,6 +19,7 @@ layout(location = 5) out vec3 out_base_diffuse;
layout(location = 6) out vec3 out_base_specular;
layout(location = 7) out vec3 out_overlay_diffuse;
layout(location = 8) out vec3 out_overlay_specular;
layout(location = 9) out float out_world_z;
layout(std140, set = 0, binding = 4) uniform FrameConstants {
mat4 clip_from_world;
@@ -241,6 +242,7 @@ LightingTerms native_material_lighting(
}
void main() {
out_world_z = in_position.z;
out_color = in_color;
out_uv = in_uv;
out_detail_uv = in_detail_uv;
@@ -866,6 +866,7 @@ fn append_node_fallback_batch(
batch_flags: batch.batch_flags,
pipeline_state: world_pipeline_state(),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
});
Ok(())
@@ -1226,6 +1227,7 @@ fn static_terrain_draw_ranges(
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
let material_index = first_face.material_tag;
let terrain_base_only = first_face.flags.0 & 0x400 == 0;
let terrain_reflection = first_face.flags.0 & 0x20000 != 0;
let mut end_face = start_face + 1;
while end_face < lod0.len() {
let next_face = terrain
@@ -1234,6 +1236,10 @@ fn static_terrain_draw_ranges(
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
if next_face.material_tag != material_index
|| (next_face.flags.0 & 0x400 == 0) != terrain_base_only
|| (next_face.flags.0 & 0x20000 != 0) != terrain_reflection
|| (terrain_reflection
&& terrain.positions[usize::from(first_face.vertices[0])][2]
!= terrain.positions[usize::from(next_face.vertices[0])][2])
{
break;
}
@@ -1253,6 +1259,7 @@ fn static_terrain_draw_ranges(
batch_flags: 0,
pipeline_state: world_pipeline_state(),
terrain_base_only,
terrain_reflection,
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
});
start_face = end_face;
@@ -1299,6 +1306,7 @@ fn static_model_indices_and_ranges(
batch_flags: batch.batch_flags,
pipeline_state: world_pipeline_state(),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
});
}
@@ -1506,6 +1514,7 @@ mod tests {
batch_flags: 0,
pipeline_state: world_pipeline_state(),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
}]
);
@@ -2036,6 +2045,7 @@ mod tests {
batch_flags: 0,
pipeline_state: world_pipeline_state(),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference:
VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
},
@@ -2047,6 +2057,7 @@ mod tests {
batch_flags: 0,
pipeline_state: world_pipeline_state(),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference:
VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
},
@@ -2298,5 +2309,21 @@ mod tests {
assert!(ranges.iter().all(|range| {
range.alpha_test_reference == VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE
}));
terrain.faces = vec![
terrain_face_with_tag_and_flags(0xff04, 0x400, [0, 1, 2]),
terrain_face_with_tag_and_flags(0xff04, 0x20400, [0, 1, 2]),
terrain_face_with_tag_and_flags(0xff04, 0x20400, [0, 1, 2]),
terrain_face_with_tag_and_flags(0xff04, 0x20400, [3, 4, 5]),
];
terrain.positions.extend([[0.0, 0.0, 10.0]; 3]);
set_terrain_lod_slots(&mut terrain, &[[0, u16::MAX]], &[[0, 4]]);
let assignments = static_terrain_lod0_assignments(&terrain).unwrap();
let ranges = static_terrain_draw_ranges(&terrain, &assignments).unwrap();
assert_eq!(ranges.len(), 3);
assert!(!ranges[0].terrain_reflection);
assert!(ranges[1].terrain_reflection);
assert_eq!(ranges[1].index_count, 6);
assert!(ranges[2].terrain_reflection);
assert_eq!(ranges[2].index_count, 3);
}
}
@@ -11,6 +11,7 @@
mod asset_mesh;
mod capabilities;
mod instance;
mod reflection;
mod resources;
mod runtime;
mod smoke;
@@ -0,0 +1,348 @@
#![allow(unsafe_code)]
use super::{
VulkanAllocatedImage, VulkanDepthAttachment, VulkanInstanceProbe, VulkanLogicalDeviceProbe,
VulkanSmokeRendererError,
};
use ash::vk;
pub(super) fn reflected_pipeline_state(
mut state: fparkan_render::LegacyPipelineState,
) -> fparkan_render::LegacyPipelineState {
use fparkan_render::LegacyCullMode;
state.cull = match state.cull {
LegacyCullMode::Disabled => LegacyCullMode::Disabled,
LegacyCullMode::BackFace => LegacyCullMode::FrontFace,
LegacyCullMode::FrontFace => LegacyCullMode::BackFace,
};
state
}
pub(super) fn mesh_reflection_planes(mesh: &super::VulkanStaticMesh) -> Vec<Option<f32>> {
mesh.draw_ranges
.iter()
.map(|range| {
if !range.terrain_reflection || range.index_count == 0 {
return None;
}
let indices = &mesh.indices[range.first_index as usize
..range.first_index as usize + range.index_count as usize];
let height = mesh.vertices[indices[0] as usize].position[2];
indices
.iter()
.all(|&index| (mesh.vertices[index as usize].position[2] - height).abs() <= 1.0e-4)
.then_some(height)
})
.collect()
}
/// Owned before pipeline creation; a dummy set prevents attachment feedback in the mirror pass.
pub(super) struct ReflectionResources {
device: ash::Device,
pub layout: vk::DescriptorSetLayout,
pool: vk::DescriptorPool,
sampler: vk::Sampler,
pub sets: [vk::DescriptorSet; 2],
dummy: Option<VulkanAllocatedImage>,
pub color: Option<VulkanAllocatedImage>,
depth: Option<VulkanDepthAttachment>,
pub framebuffer: vk::Framebuffer,
}
impl ReflectionResources {
pub fn new(
instance: &VulkanInstanceProbe,
device: &VulkanLogicalDeviceProbe,
command_pool: vk::CommandPool,
) -> Result<Self, VulkanSmokeRendererError> {
let mut owned = Self {
device: device.device().clone(),
layout: vk::DescriptorSetLayout::null(),
pool: vk::DescriptorPool::null(),
sampler: vk::Sampler::null(),
sets: [vk::DescriptorSet::null(); 2],
dummy: None,
color: None,
depth: None,
framebuffer: vk::Framebuffer::null(),
};
let binding = vk::DescriptorSetLayoutBinding::default()
.binding(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::FRAGMENT);
let layout_info =
vk::DescriptorSetLayoutCreateInfo::default().bindings(std::slice::from_ref(&binding));
// SAFETY: The device is live; owned rolls back every handle on later failure.
owned.layout = unsafe {
owned
.device
.create_descriptor_set_layout(&layout_info, None)
}
.map_err(|result| VulkanSmokeRendererError::VulkanOperation {
context: "vkCreateDescriptorSetLayout(reflection)",
result,
})?;
let size = vk::DescriptorPoolSize::default()
.ty(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(2);
let pool_info = vk::DescriptorPoolCreateInfo::default()
.max_sets(2)
.pool_sizes(std::slice::from_ref(&size));
// SAFETY: The pool owns only the two reflection descriptor sets below.
owned.pool =
unsafe { owned.device.create_descriptor_pool(&pool_info, None) }.map_err(|result| {
VulkanSmokeRendererError::VulkanOperation {
context: "vkCreateDescriptorPool(reflection)",
result,
}
})?;
let layouts = [owned.layout; 2];
let allocation = vk::DescriptorSetAllocateInfo::default()
.descriptor_pool(owned.pool)
.set_layouts(&layouts);
// SAFETY: The pool has capacity for these two live layouts.
let sets =
unsafe { owned.device.allocate_descriptor_sets(&allocation) }.map_err(|result| {
VulkanSmokeRendererError::VulkanOperation {
context: "vkAllocateDescriptorSets(reflection)",
result,
}
})?;
owned.sets.copy_from_slice(&sets);
let sampler = vk::SamplerCreateInfo::default()
.mag_filter(vk::Filter::LINEAR)
.min_filter(vk::Filter::LINEAR)
.address_mode_u(vk::SamplerAddressMode::CLAMP_TO_EDGE)
.address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE)
.address_mode_w(vk::SamplerAddressMode::CLAMP_TO_EDGE);
// SAFETY: No optional device feature is used by this sampler.
owned.sampler =
unsafe { owned.device.create_sampler(&sampler, None) }.map_err(|result| {
VulkanSmokeRendererError::VulkanOperation {
context: "vkCreateSampler(reflection)",
result,
}
})?;
// Material images can be replaced by animation, so the dummy has its own lifetime.
owned.dummy = Some(super::create_static_texture_image(
instance,
device,
command_pool,
&super::VulkanStaticTexture {
width: 1,
height: 1,
rgba8: vec![255; 4],
mip_levels: Vec::new(),
},
)?);
for set in owned.sets {
owned.write_image(set, owned.dummy.as_ref().expect("dummy image exists").view);
}
Ok(owned)
}
pub fn create_target(
&mut self,
instance: &VulkanInstanceProbe,
device: &VulkanLogicalDeviceProbe,
extent: (u32, u32),
format: i32,
depth_request: fparkan_platform::DepthStencilSupport,
render_pass: vk::RenderPass,
) -> Result<(), VulkanSmokeRendererError> {
let info = vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(vk::Format::from_raw(format))
.extent(vk::Extent3D {
width: extent.0,
height: extent.1,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::OPTIMAL)
.usage(vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::SAMPLED)
.sharing_mode(vk::SharingMode::EXCLUSIVE)
.initial_layout(vk::ImageLayout::UNDEFINED);
// SAFETY: This target uses the validated non-zero swapchain extent and format.
let image = unsafe { self.device.create_image(&info, None) }.map_err(|result| {
VulkanSmokeRendererError::VulkanOperation {
context: "vkCreateImage(reflection)",
result,
}
})?;
self.color = Some(VulkanAllocatedImage {
image,
memory: vk::DeviceMemory::null(),
view: vk::ImageView::null(),
width: extent.0,
height: extent.1,
mip_levels: 1,
});
// SAFETY: The image belongs to this device and has not been bound yet.
let requirements = unsafe { self.device.get_image_memory_requirements(image) };
let memory_type_index = super::resources::find_memory_type(
instance,
device.physical_device(),
requirements.memory_type_bits,
vk::MemoryPropertyFlags::DEVICE_LOCAL,
)
.ok_or(VulkanSmokeRendererError::MissingMemoryType {
context: "reflection target",
})?;
let allocation = vk::MemoryAllocateInfo::default()
.allocation_size(requirements.size)
.memory_type_index(memory_type_index);
// SAFETY: The allocation follows this image's memory requirements.
let memory =
unsafe { self.device.allocate_memory(&allocation, None) }.map_err(|result| {
VulkanSmokeRendererError::VulkanOperation {
context: "vkAllocateMemory(reflection)",
result,
}
})?;
self.color.as_mut().expect("color image exists").memory = memory;
// SAFETY: The allocation is compatible with the image and offset zero.
unsafe { self.device.bind_image_memory(image, memory, 0) }.map_err(|result| {
VulkanSmokeRendererError::VulkanOperation {
context: "vkBindImageMemory(reflection)",
result,
}
})?;
let view_info = vk::ImageViewCreateInfo::default()
.image(image)
.view_type(vk::ImageViewType::TYPE_2D)
.format(vk::Format::from_raw(format))
.subresource_range(super::color_subresource_range(1));
// SAFETY: The image is bound and the view covers its single color subresource.
let view =
unsafe { self.device.create_image_view(&view_info, None) }.map_err(|result| {
VulkanSmokeRendererError::VulkanOperation {
context: "vkCreateImageView(reflection)",
result,
}
})?;
self.color.as_mut().expect("color image exists").view = view;
self.depth = Some(super::create_depth_attachment(
instance,
device,
extent,
depth_request,
)?);
let attachments = [view, self.depth.as_ref().expect("depth exists").image.view];
let info = vk::FramebufferCreateInfo::default()
.render_pass(render_pass)
.attachments(&attachments)
.width(extent.0)
.height(extent.1)
.layers(1);
// SAFETY: The target formats and extent match the shared render pass.
self.framebuffer =
unsafe { self.device.create_framebuffer(&info, None) }.map_err(|result| {
VulkanSmokeRendererError::VulkanOperation {
context: "vkCreateFramebuffer(reflection)",
result,
}
})?;
self.write_image(self.sets[1], view);
Ok(())
}
fn write_image(&self, set: vk::DescriptorSet, view: vk::ImageView) {
let image = vk::DescriptorImageInfo::default()
.sampler(self.sampler)
.image_view(view)
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
let write = vk::WriteDescriptorSet::default()
.dst_set(set)
.dst_binding(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(std::slice::from_ref(&image));
// SAFETY: Sets are initialized before submission; the dummy remains valid for every pass.
unsafe { self.device.update_descriptor_sets(&[write], &[]) };
}
}
impl Drop for ReflectionResources {
fn drop(&mut self) {
// SAFETY: The parent waits for submissions and drops this owner before the render pass/device.
// Null handles also cover partial construction, including failures before memory binding.
unsafe {
self.device.destroy_framebuffer(self.framebuffer, None);
self.device.destroy_descriptor_pool(self.pool, None);
self.device.destroy_sampler(self.sampler, None);
self.device.destroy_descriptor_set_layout(self.layout, None);
for image in self
.color
.iter()
.chain(self.depth.iter().map(|depth| &depth.image))
.chain(self.dummy.iter())
{
self.device.destroy_image_view(image.view, None);
self.device.destroy_image(image.image, None);
self.device.free_memory(image.memory, None);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{VulkanStaticDrawRange, VulkanStaticMesh, VulkanStaticVertex};
#[test]
fn reflection_requires_a_flat_flagged_surface_and_reverses_only_culling() {
let mut mesh = VulkanStaticMesh {
vertices: vec![
VulkanStaticVertex {
position: [0.0, 0.0, 150.0],
color: [1.0; 3],
normal: [0.0, 0.0, 1.0],
uv: [0.0; 2],
detail_uv: [0.0; 2],
overlay_alpha: 0.0,
};
3
],
indices: vec![0, 1, 2],
draw_ranges: vec![VulkanStaticDrawRange {
first_index: 0,
index_count: 3,
material_index: 0,
lightmap_index: 255,
batch_flags: 0,
terrain_base_only: false,
terrain_reflection: true,
pipeline_state: fparkan_render::LegacyPipelineState::default(),
alpha_test_reference: 1,
}],
};
assert_eq!(mesh_reflection_planes(&mesh), vec![Some(150.0)]);
mesh.vertices[2].position[2] = 151.0;
assert_eq!(mesh_reflection_planes(&mesh), vec![None]);
mesh.vertices[2].position[2] = 150.0;
mesh.draw_ranges[0].terrain_reflection = false;
assert_eq!(mesh_reflection_planes(&mesh), vec![None]);
for cull in [
fparkan_render::LegacyCullMode::Disabled,
fparkan_render::LegacyCullMode::BackFace,
fparkan_render::LegacyCullMode::FrontFace,
] {
let mut state = mesh.draw_ranges[0].pipeline_state;
state.cull = cull;
state.alpha_test = true;
state.depth_bias = fparkan_render::LegacyDepthBias::ProjectedDecal;
assert_eq!(
reflected_pipeline_state(reflected_pipeline_state(state)),
state
);
let reflected = reflected_pipeline_state(state);
assert_eq!(reflected.depth, state.depth);
assert_eq!(reflected.blend, state.blend);
assert_eq!(reflected.alpha_test, state.alpha_test);
}
}
}
@@ -528,7 +528,7 @@ pub(super) fn create_static_texture_image(
})
}
/// Allocates two aligned camera slots used by the dynamic frame UBO.
/// Allocates a main and reflected view slot for each in-flight frame.
pub(super) fn create_frame_uniform_buffer(
instance: &VulkanInstanceProbe,
device: &VulkanLogicalDeviceProbe,
@@ -545,7 +545,7 @@ pub(super) fn create_frame_uniform_buffer(
)? / alignment
* alignment;
let size = stride
.checked_mul(super::VULKAN_FRAME_SLOT_COUNT as u64)
.checked_mul(2 * super::VULKAN_FRAME_SLOT_COUNT as u64)
.ok_or(VulkanSmokeRendererError::InvalidStaticMesh {
context: "frame uniform buffer size overflow",
})?;
@@ -888,7 +888,7 @@ pub(super) fn write_frame_uniform(
.ok_or(VulkanSmokeRendererError::InvariantViolation {
context: "frame uniform slot offset",
})?;
// SAFETY: The offset is one of the two allocated aligned slots and the
// SAFETY: The caller supplies an allocated, fence-safe main or reflection slot; the
// mapped range is exactly one complete frame block.
let mapped = unsafe {
device.device().map_memory(
@@ -1385,7 +1385,7 @@ fn create_host_visible_buffer(
Ok(VulkanAllocatedBuffer { buffer, memory })
}
fn find_memory_type(
pub(super) fn find_memory_type(
instance: &VulkanInstanceProbe,
physical_device: vk::PhysicalDevice,
type_bits: u32,
+339 -172
View File
@@ -357,6 +357,28 @@ impl VulkanSmokeRenderer {
.map(|material| material.material_index)
.collect()
};
let reflection_planes = super::reflection::mesh_reflection_planes(&create_info.mesh);
let unsupported_nonplanar = create_info
.mesh
.draw_ranges
.iter()
.zip(&reflection_planes)
.filter(|(range, plane)| range.terrain_reflection && plane.is_none())
.count();
let mut plane_heights = reflection_planes
.iter()
.flatten()
.map(|z| z.to_bits())
.collect::<Vec<_>>();
plane_heights.sort_unstable();
plane_heights.dedup();
if !plane_heights.is_empty() {
eprintln!("render_coverage planar_reflection_embm_unsupported={} (flat reflection; native shifted camera and wave distortion pending)", plane_heights.len());
eprintln!("render_coverage planar_reflection_other_planes_unsupported={} (nearest active plane is reflected)", plane_heights.len().saturating_sub(1));
}
if unsupported_nonplanar != 0 {
eprintln!("render_coverage planar_reflection_nonplanar_unsupported={unsupported_nonplanar} (ordinary terrain fallback)");
}
let mut renderer = Self {
instance: Some(instance),
validation,
@@ -379,6 +401,7 @@ impl VulkanSmokeRenderer {
.map(|range| range.index_count)
.collect(),
draw_ranges: create_info.mesh.draw_ranges.clone(),
reflection_planes,
draw_texture_indices,
draw_order: (0..create_info.mesh.draw_ranges.len()).collect(),
draw_range_sort_keys: vec![None; create_info.mesh.draw_ranges.len()],
@@ -521,6 +544,22 @@ impl VulkanSmokeRenderer {
self.frame_uniforms
}
fn active_reflection_plane(&self) -> Option<f32> {
// shortcut: one target serves the nearest active water height; add per-plane targets
// when accepting maps whose visible water surfaces have different heights.
self.draw_ranges
.iter()
.zip(&self.reflection_planes)
.filter(|(range, _)| range.index_count != 0)
.filter_map(|(_, plane)| *plane)
.filter(|&z| z < self.frame_uniforms.camera_position[2])
.min_by(|a, b| {
(self.frame_uniforms.camera_position[2] - a)
.abs()
.total_cmp(&(self.frame_uniforms.camera_position[2] - b).abs())
})
}
/// Replaces the camera for subsequent frames without recreating GPU resources.
///
/// # Errors
@@ -1516,6 +1555,27 @@ impl VulkanSmokeRenderer {
}
self.images_in_flight[image_index_usize] = in_flight_fence;
let frame_bytes = self.frame_uniforms.to_ne_bytes();
let reflection_bytes = self
.active_reflection_plane()
.map(|height| {
let mut frame = self.frame_uniforms;
frame.clip_from_world =
fparkan_render::reflect_clip_from_world_about_z(frame.clip_from_world, height)
.ok_or(VulkanSmokeRendererError::InvalidStaticCamera {
context: "non-finite reflected clip transform",
})?;
frame.camera_position[2] = 2.0 * height - frame.camera_position[2];
if !frame.is_finite() {
return Err(VulkanSmokeRendererError::InvalidStaticCamera {
context: "non-finite reflected camera position",
});
}
let mut bytes = frame.to_ne_bytes();
let length = bytes.len();
bytes[length - 4..].copy_from_slice(&height.to_ne_bytes());
Ok(bytes)
})
.transpose()?;
{
let explicit_point_lights = self.point_lights_explicit;
let legacy_point_light = self.frame_uniforms.point_light;
@@ -1541,9 +1601,18 @@ impl VulkanSmokeRenderer {
device,
&resources.frame_uniform_buffer,
resources.frame_uniform_stride,
self.current_frame,
self.current_frame * 2,
&frame_bytes,
)?;
if let Some(bytes) = &reflection_bytes {
write_frame_uniform(
device,
&resources.frame_uniform_buffer,
resources.frame_uniform_stride,
self.current_frame * 2 + 1,
bytes,
)?;
}
write_point_light_buffer(
device,
&resources.point_light_buffer,
@@ -1686,6 +1755,9 @@ impl VulkanSmokeRenderer {
self.textures_ref()?,
&self.texture_slot_indices,
&self.draw_range_base_pipeline_states,
self.draw_ranges
.iter()
.any(|range| range.terrain_reflection),
self.depth_request,
reuse_command_pool,
)?,
@@ -1741,6 +1813,7 @@ impl VulkanSmokeRenderer {
self.rebuild_draw_order();
self.draw_order_dirty = false;
}
let reflection_plane = self.active_reflection_plane();
let device = self.device_ref()?;
let swapchain = self.swapchain_ref()?;
let resources = self.resources_ref()?;
@@ -1787,185 +1860,269 @@ impl VulkanSmokeRenderer {
height: swapchain.report.plan.extent.1,
},
};
let render_pass_info = vk::RenderPassBeginInfo::default()
.render_pass(resources.render_pass)
.framebuffer(resources.framebuffers[image_index])
.render_area(render_area)
.clear_values(&clear_values);
// SAFETY: All commands target live frame resources owned by this renderer.
unsafe {
device.device().cmd_begin_render_pass(
command_buffer,
&render_pass_info,
vk::SubpassContents::INLINE,
);
let vertex_buffers = [self.vertex_buffer_ref()?.buffer];
let offsets = [0_u64];
device
.device()
.cmd_bind_vertex_buffers(command_buffer, 0, &vertex_buffers, &offsets);
device.device().cmd_bind_index_buffer(
command_buffer,
self.index_buffer_ref()?.buffer,
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 {
context: "stable draw order range handle",
},
)?;
let texture_index = *self.draw_texture_indices.get(range_index).ok_or(
VulkanSmokeRendererError::InvariantViolation {
context: "stable draw texture range handle",
},
)?;
let pipeline = resources.pipelines.get(&range.pipeline_key()).ok_or(
VulkanSmokeRendererError::InvariantViolation {
context: "static draw pipeline variant",
},
)?;
device.device().cmd_bind_pipeline(
for reflected in [true, false] {
if reflected && reflection_plane.is_none() {
continue;
}
let framebuffer = if reflected {
resources.reflection.framebuffer
} else {
resources.framebuffers[image_index]
};
let render_pass_info = vk::RenderPassBeginInfo::default()
.render_pass(resources.render_pass)
.framebuffer(framebuffer)
.render_area(render_area)
.clear_values(&clear_values);
// SAFETY: All commands target live frame resources owned by this renderer.
unsafe {
device.device().cmd_begin_render_pass(
command_buffer,
vk::PipelineBindPoint::GRAPHICS,
*pipeline,
&render_pass_info,
vk::SubpassContents::INLINE,
);
let vertex_buffers = [self.vertex_buffer_ref()?.buffer];
let offsets = [0_u64];
device.device().cmd_bind_vertex_buffers(
command_buffer,
0,
&vertex_buffers,
&offsets,
);
device.device().cmd_bind_index_buffer(
command_buffer,
self.index_buffer_ref()?.buffer,
0,
vk::IndexType::UINT32,
);
let frame_offset = u32::try_from(
resources
.frame_uniform_stride
.saturating_mul(u64::try_from(self.current_frame).unwrap_or(u64::MAX)),
)
.unwrap_or(u32::MAX);
let material_offset = u32::try_from(
resources
.material_specular_stride
.saturating_mul(u64::try_from(texture_index).unwrap_or(u64::MAX)),
)
.unwrap_or(u32::MAX);
let point_light_offset = u32::try_from(
resources
.point_light_stride
.saturating_mul(u64::try_from(self.current_frame).unwrap_or(u64::MAX)),
)
.unwrap_or(u32::MAX);
let descriptor_set = resources.descriptor_sets.get(texture_index).ok_or(
VulkanSmokeRendererError::InvariantViolation {
context: "static material descriptor set",
},
)?;
device.device().cmd_bind_descriptor_sets(
command_buffer,
vk::PipelineBindPoint::GRAPHICS,
resources.pipeline_layout,
0,
std::slice::from_ref(descriptor_set),
&[frame_offset, material_offset, point_light_offset],
);
let alpha_cutoff = range.alpha_test_cutoff().to_ne_bytes();
device.device().cmd_push_constants(
command_buffer,
resources.pipeline_layout,
vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
0,
&alpha_cutoff,
);
let material_alpha = self.material_alphas.get(texture_index).ok_or(
VulkanSmokeRendererError::InvariantViolation {
context: "static material alpha",
},
)?;
let diffuse_alpha_override = self
.draw_range_diffuse_alpha_overrides
.get(range_index)
.ok_or(VulkanSmokeRendererError::InvariantViolation {
context: "draw-range diffuse-alpha override",
})?;
let material_alphas =
draw_material_alphas(*material_alpha, *diffuse_alpha_override)
.map(f32::to_ne_bytes)
.concat();
device.device().cmd_push_constants(
command_buffer,
resources.pipeline_layout,
vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
4,
&material_alphas,
);
let material_lighting = self.material_lighting.get(texture_index).ok_or(
VulkanSmokeRendererError::InvariantViolation {
context: "static material lighting",
},
)?;
let material_lighting = material_lighting
.iter()
.flatten()
.flat_map(|value| value.to_ne_bytes())
.collect::<Vec<_>>();
device.device().cmd_push_constants(
command_buffer,
resources.pipeline_layout,
vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
12,
&material_lighting,
);
let material_uv_transforms = self.material_uv_transforms.get(texture_index).ok_or(
VulkanSmokeRendererError::InvariantViolation {
context: "static material UV transforms",
},
)?;
let material_uv_transforms = material_uv_transforms
.iter()
.flatten()
.flat_map(|value| value.to_ne_bytes())
.collect::<Vec<_>>();
device.device().cmd_push_constants(
command_buffer,
resources.pipeline_layout,
vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
60,
&material_uv_transforms,
);
let material_mode = self.material_unlit.get(texture_index).ok_or(
VulkanSmokeRendererError::InvariantViolation {
context: "static material unlit flag",
},
)?;
let material_mode = if range.terrain_base_only {
((*material_mode).round() as i32 | 16) as f32
} 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,
resources.pipeline_layout,
vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
124,
&material_mode.concat(),
);
device.device().cmd_draw_indexed(
command_buffer,
range.index_count,
1,
range.first_index,
0,
0,
&[resources.reflection.sets
[usize::from(!reflected && reflection_plane.is_some())]],
&[],
);
let mut viewport_depth = None;
for &range_index in &self.draw_order {
let range = self.draw_ranges.get(range_index).ok_or(
VulkanSmokeRendererError::InvariantViolation {
context: "stable draw order range handle",
},
)?;
let texture_index = *self.draw_texture_indices.get(range_index).ok_or(
VulkanSmokeRendererError::InvariantViolation {
context: "stable draw texture range handle",
},
)?;
let mode = self.material_unlit[texture_index].round() as i32;
if reflected && (range.terrain_reflection || mode & 8 != 0) {
continue;
}
let state = if reflected {
super::reflection::reflected_pipeline_state(range.pipeline_state)
} else {
range.pipeline_state
};
let pipeline = resources.pipelines.get(&state.into()).ok_or(
VulkanSmokeRendererError::InvariantViolation {
context: "static draw pipeline variant",
},
)?;
device.device().cmd_bind_pipeline(
command_buffer,
vk::PipelineBindPoint::GRAPHICS,
*pipeline,
);
let frame_offset = u32::try_from(
resources.frame_uniform_stride.saturating_mul(
u64::try_from(self.current_frame * 2 + usize::from(reflected))
.unwrap_or(u64::MAX),
),
)
.unwrap_or(u32::MAX);
let material_offset = u32::try_from(
resources
.material_specular_stride
.saturating_mul(u64::try_from(texture_index).unwrap_or(u64::MAX)),
)
.unwrap_or(u32::MAX);
let point_light_offset = u32::try_from(
resources
.point_light_stride
.saturating_mul(u64::try_from(self.current_frame).unwrap_or(u64::MAX)),
)
.unwrap_or(u32::MAX);
let descriptor_set = resources.descriptor_sets.get(texture_index).ok_or(
VulkanSmokeRendererError::InvariantViolation {
context: "static material descriptor set",
},
)?;
device.device().cmd_bind_descriptor_sets(
command_buffer,
vk::PipelineBindPoint::GRAPHICS,
resources.pipeline_layout,
0,
std::slice::from_ref(descriptor_set),
&[frame_offset, material_offset, point_light_offset],
);
let alpha_cutoff = range.alpha_test_cutoff().to_ne_bytes();
device.device().cmd_push_constants(
command_buffer,
resources.pipeline_layout,
vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
0,
&alpha_cutoff,
);
let material_alpha = self.material_alphas.get(texture_index).ok_or(
VulkanSmokeRendererError::InvariantViolation {
context: "static material alpha",
},
)?;
let diffuse_alpha_override = self
.draw_range_diffuse_alpha_overrides
.get(range_index)
.ok_or(VulkanSmokeRendererError::InvariantViolation {
context: "draw-range diffuse-alpha override",
})?;
let material_alphas =
draw_material_alphas(*material_alpha, *diffuse_alpha_override)
.map(f32::to_ne_bytes)
.concat();
device.device().cmd_push_constants(
command_buffer,
resources.pipeline_layout,
vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
4,
&material_alphas,
);
let material_lighting = self.material_lighting.get(texture_index).ok_or(
VulkanSmokeRendererError::InvariantViolation {
context: "static material lighting",
},
)?;
let material_lighting = material_lighting
.iter()
.flatten()
.flat_map(|value| value.to_ne_bytes())
.collect::<Vec<_>>();
device.device().cmd_push_constants(
command_buffer,
resources.pipeline_layout,
vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
12,
&material_lighting,
);
let material_uv_transforms = self
.material_uv_transforms
.get(texture_index)
.ok_or(VulkanSmokeRendererError::InvariantViolation {
context: "static material UV transforms",
})?;
let material_uv_transforms = material_uv_transforms
.iter()
.flatten()
.flat_map(|value| value.to_ne_bytes())
.collect::<Vec<_>>();
device.device().cmd_push_constants(
command_buffer,
resources.pipeline_layout,
vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
60,
&material_uv_transforms,
);
let material_mode = self.material_unlit.get(texture_index).ok_or(
VulkanSmokeRendererError::InvariantViolation {
context: "static material unlit flag",
},
)?;
let material_mode = if range.terrain_base_only {
((*material_mode).round() as i32 | 16) as f32
} else {
*material_mode
};
let material_mode = (material_mode.round() as i32
| if reflected { 32 } else { 0 }
| if !reflected
&& reflection_plane.is_some()
&& self.reflection_planes[range_index] == reflection_plane
{
64
} else {
0
}) as f32;
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,
resources.pipeline_layout,
vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
124,
&material_mode.concat(),
);
device.device().cmd_draw_indexed(
command_buffer,
range.index_count,
1,
range.first_index,
0,
0,
);
}
device.device().cmd_end_render_pass(command_buffer);
if reflected {
let image = resources.reflection.color.as_ref().ok_or(
VulkanSmokeRendererError::InvariantViolation {
context: "reflection target",
},
)?;
let barrier = vk::ImageMemoryBarrier::default()
.old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
.new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE)
.dst_access_mask(vk::AccessFlags::SHADER_READ)
.image(image.image)
.subresource_range(super::color_subresource_range(1));
device.device().cmd_pipeline_barrier(
command_buffer,
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
vk::PipelineStageFlags::FRAGMENT_SHADER,
vk::DependencyFlags::empty(),
&[],
&[],
&[barrier],
);
}
}
device.device().cmd_end_render_pass(command_buffer);
}
// SAFETY: The main pass has completed its writes to this acquired swapchain image.
unsafe {
let barrier = vk::ImageMemoryBarrier::default()
.old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
.new_layout(vk::ImageLayout::PRESENT_SRC_KHR)
.src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE)
.image(resources.images[image_index])
.subresource_range(super::color_subresource_range(1));
device.device().cmd_pipeline_barrier(
command_buffer,
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
vk::PipelineStageFlags::BOTTOM_OF_PIPE,
vk::DependencyFlags::empty(),
&[],
&[],
&[barrier],
);
if self.readback_enabled {
if let (Some(image), Some(readback)) = (
resources.images.get(image_index),
@@ -2028,7 +2185,7 @@ impl VulkanSmokeRenderer {
}
}
// SAFETY: The render pass owns the attachment layout transitions for this clear-and-present path.
// SAFETY: Every render pass and attachment transition has been recorded in this buffer.
unsafe { device.device().end_command_buffer(command_buffer) }.map_err(|error| {
VulkanSmokeRendererError::VulkanOperation {
context: "vkEndCommandBuffer",
@@ -2720,6 +2877,7 @@ mod tests {
batch_flags: 0x108,
pipeline_state: base_state,
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 17,
},
VulkanStaticDrawRange {
@@ -2730,6 +2888,7 @@ mod tests {
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 0,
},
];
@@ -2886,6 +3045,7 @@ mod tests {
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 0,
}];
let mut capacities = [6];
@@ -3072,6 +3232,7 @@ mod tests {
batch_flags: 0,
pipeline_state: fparkan_render::LegacyPipelineState::default(),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 0,
})
.collect::<Vec<_>>();
@@ -3096,6 +3257,7 @@ mod tests {
batch_flags: 0,
pipeline_state: fparkan_render::LegacyPipelineState::default(),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 0,
},
super::super::VulkanStaticDrawRange {
@@ -3106,6 +3268,7 @@ mod tests {
batch_flags: 0,
pipeline_state: fparkan_render::LegacyPipelineState::default(),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 0,
},
super::super::VulkanStaticDrawRange {
@@ -3116,6 +3279,7 @@ mod tests {
batch_flags: 0,
pipeline_state: fparkan_render::LegacyPipelineState::default(),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 0,
},
];
@@ -3140,6 +3304,7 @@ mod tests {
batch_flags: 0,
pipeline_state: fparkan_render::LegacyPipelineState::default(),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 0,
})
.collect::<Vec<_>>();
@@ -3233,6 +3398,7 @@ mod tests {
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 0,
})
.collect::<Vec<_>>();
@@ -3291,6 +3457,7 @@ mod tests {
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 0,
})
.collect::<Vec<_>>();
@@ -437,6 +437,8 @@ pub struct VulkanStaticDrawRange {
pub batch_flags: u16,
/// Terrain ranges that use the native lit base-only path (face flag 0x400 clear).
pub terrain_base_only: bool,
/// Terrain face flag 0x20000 requests a water reflection surface.
pub terrain_reflection: bool,
/// Backend-neutral fixed-function state for this source range.
pub pipeline_state: LegacyPipelineState,
/// Alpha-test reference in legacy 0..=255 units; dynamic material data.
@@ -771,6 +773,7 @@ impl VulkanStaticMesh {
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 0,
}],
}
@@ -1032,6 +1035,7 @@ mod static_mesh_tests {
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 0,
}],
};
@@ -1063,6 +1067,7 @@ mod static_mesh_tests {
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 0,
},
VulkanStaticDrawRange {
@@ -1073,6 +1078,7 @@ mod static_mesh_tests {
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 0,
},
];
@@ -1191,6 +1197,7 @@ mod static_mesh_tests {
let base = VulkanStaticMesh::smoke_triangle().draw_ranges[0];
let disabled = VulkanStaticDrawRange {
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 200,
..base
};
@@ -1200,6 +1207,7 @@ mod static_mesh_tests {
..LegacyPipelineState::default()
},
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 128,
..base
};
@@ -1211,6 +1219,7 @@ mod static_mesh_tests {
..LegacyPipelineState::default()
},
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 0,
..base
};
@@ -1221,6 +1230,7 @@ mod static_mesh_tests {
..LegacyPipelineState::default()
},
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
..base
};
@@ -1501,6 +1511,7 @@ pub struct VulkanSmokeRenderer {
pub(super) index_capacity: usize,
pub(super) draw_range_capacities: Vec<u32>,
pub(super) draw_ranges: Vec<VulkanStaticDrawRange>,
pub(super) reflection_planes: Vec<Option<f32>>,
pub(super) draw_texture_indices: Vec<usize>,
/// Stable range handles used when recording the native pass order.
pub(super) draw_order: Vec<usize>,
@@ -18,6 +18,7 @@ use super::{
};
pub(super) struct VulkanSwapchainResources {
pub(super) reflection: super::reflection::ReflectionResources,
pub(super) image_views: Vec<vk::ImageView>,
pub(super) images: Vec<vk::Image>,
pub(super) readback_buffers: Vec<VulkanAllocatedBuffer>,
@@ -146,6 +147,7 @@ pub(super) fn ensure_point_light_capacity(
}
struct PartialSwapchainResources {
reflection: Option<super::reflection::ReflectionResources>,
image_views: Vec<vk::ImageView>,
readback_buffers: Vec<VulkanAllocatedBuffer>,
depth_attachments: Vec<VulkanDepthAttachment>,
@@ -180,6 +182,7 @@ pub(super) fn create_swapchain_resources(
textures: &[VulkanAllocatedImage],
texture_slot_indices: &[usize],
base_pipeline_states: &[LegacyPipelineState],
reflection_enabled: bool,
depth_request: DepthStencilSupport,
reuse_command_pool: bool,
) -> Result<VulkanSwapchainResources, VulkanSmokeRendererError> {
@@ -194,6 +197,7 @@ pub(super) fn create_swapchain_resources(
result: error,
})?;
let mut partial = PartialSwapchainResources {
reflection: None,
image_views: create_swapchain_image_views(
device,
&images,
@@ -245,6 +249,14 @@ pub(super) fn create_swapchain_resources(
};
partial.frame_uniform_stride = frame_uniform_stride;
partial.frame_uniform_buffer = Some(frame_uniform_buffer);
partial.reflection =
match super::reflection::ReflectionResources::new(instance, device, command_pool) {
Ok(reflection) => Some(reflection),
Err(error) => {
destroy_partial_swapchain_resources(device, command_pool, partial);
return Err(error);
}
};
let material_count = texture_slot_indices.len() / 4;
let (material_specular_buffer, material_specular_stride) =
match create_material_specular_buffer(instance, device, material_count) {
@@ -288,6 +300,12 @@ pub(super) fn create_swapchain_resources(
textures,
texture_slot_indices,
base_pipeline_states,
partial
.reflection
.as_ref()
.expect("reflection descriptors exist")
.layout,
reflection_enabled,
depth_request,
partial
.frame_uniform_buffer
@@ -319,6 +337,24 @@ pub(super) fn create_swapchain_resources(
partial.descriptor_pool = Some(descriptor_pool);
partial.sampler = Some(sampler);
partial.pipelines = pipelines;
if reflection_enabled {
if let Err(error) = partial
.reflection
.as_mut()
.expect("reflection descriptors exist")
.create_target(
instance,
device,
swapchain.report.plan.extent,
swapchain.report.plan.format.format,
depth_request,
render_pass,
)
{
destroy_partial_swapchain_resources(device, command_pool, partial);
return Err(error);
}
}
for _ in 1..images.len() {
match create_depth_attachment(
instance,
@@ -394,6 +430,10 @@ pub(super) fn create_swapchain_resources(
});
}
Ok(VulkanSwapchainResources {
reflection: partial
.reflection
.take()
.expect("reflection descriptors exist"),
image_views: partial.image_views,
images,
readback_buffers: partial.readback_buffers,
@@ -457,6 +497,8 @@ fn create_swapchain_pipeline_bundle(
textures: &[VulkanAllocatedImage],
texture_slot_indices: &[usize],
base_pipeline_states: &[LegacyPipelineState],
reflection_layout: vk::DescriptorSetLayout,
reflection_enabled: bool,
depth_request: DepthStencilSupport,
frame_uniform_buffer: &VulkanAllocatedBuffer,
material_specular_buffer: &VulkanAllocatedBuffer,
@@ -498,8 +540,8 @@ fn create_swapchain_pipeline_bundle(
unsafe { device.device().destroy_render_pass(render_pass, None) };
destroy_depth_attachment(device, &depth_attachment);
})?;
let pipeline_layout =
create_pipeline_layout(device, descriptor_set_layout).inspect_err(|_| {
let pipeline_layout = create_pipeline_layout(device, descriptor_set_layout, reflection_layout)
.inspect_err(|_| {
// SAFETY: The descriptor resources and render pass were created above and are rolled back on this device.
unsafe {
device.device().destroy_sampler(sampler, None);
@@ -520,6 +562,7 @@ fn create_swapchain_pipeline_bundle(
pipeline_layout,
extent,
base_pipeline_states,
reflection_enabled,
)
.inspect_err(|_| {
// SAFETY: These objects were created above on this live logical device and are destroyed on setup failure.
@@ -636,7 +679,7 @@ fn create_render_pass(
.load_op(vk::AttachmentLoadOp::CLEAR)
.store_op(vk::AttachmentStoreOp::STORE)
.initial_layout(vk::ImageLayout::UNDEFINED)
.final_layout(vk::ImageLayout::PRESENT_SRC_KHR);
.final_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
let color_attachment_ref = vk::AttachmentReference::default()
.attachment(0)
.layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
@@ -661,7 +704,8 @@ fn create_render_pass(
.src_subpass(vk::SUBPASS_EXTERNAL)
.dst_subpass(0)
.src_stage_mask(
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
vk::PipelineStageFlags::FRAGMENT_SHADER
| vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
| vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS
| vk::PipelineStageFlags::LATE_FRAGMENT_TESTS,
)
@@ -670,7 +714,11 @@ fn create_render_pass(
| vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS
| vk::PipelineStageFlags::LATE_FRAGMENT_TESTS,
)
.src_access_mask(vk::AccessFlags::empty())
.src_access_mask(
vk::AccessFlags::SHADER_READ
| vk::AccessFlags::COLOR_ATTACHMENT_WRITE
| vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE,
)
.dst_access_mask(
vk::AccessFlags::COLOR_ATTACHMENT_WRITE
| vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE,
@@ -694,8 +742,9 @@ fn create_render_pass(
fn create_pipeline_layout(
device: &VulkanLogicalDeviceProbe,
descriptor_set_layout: vk::DescriptorSetLayout,
reflection_layout: vk::DescriptorSetLayout,
) -> Result<vk::PipelineLayout, VulkanSmokeRendererError> {
let set_layouts = [descriptor_set_layout];
let set_layouts = [descriptor_set_layout, reflection_layout];
// Material data occupies one 128-byte push-constant allocation shared by
// both stages; the vertex stage also reads its specular pair from the
// dynamic storage buffer at descriptor binding 5. Camera data is supplied
@@ -960,12 +1009,18 @@ fn create_graphics_pipeline_cache(
pipeline_layout: vk::PipelineLayout,
extent: (u32, u32),
base_pipeline_states: &[LegacyPipelineState],
reflection_enabled: bool,
) -> Result<BTreeMap<PipelineKey, vk::Pipeline>, VulkanSmokeRendererError> {
let mut pipelines = BTreeMap::new();
for state in base_pipeline_states
.iter()
.copied()
.flat_map(super::smoke::pipeline_variants)
.flat_map(|state| {
std::iter::once(state).chain(
reflection_enabled.then_some(super::reflection::reflected_pipeline_state(state)),
)
})
{
let key = PipelineKey::from(state);
if pipelines.contains_key(&key) {
@@ -1238,6 +1293,7 @@ pub(super) fn destroy_swapchain_resources(
command_pool: vk::CommandPool,
resources: VulkanSwapchainResources,
) {
drop(resources.reflection);
// SAFETY: All handles belong to this live logical device and are destroyed during renderer teardown/recreation.
unsafe {
if !resources.command_buffers.is_empty() {
@@ -1303,6 +1359,7 @@ fn destroy_partial_swapchain_resources(
command_pool: vk::CommandPool,
partial: PartialSwapchainResources,
) {
drop(partial.reflection);
// SAFETY: All handles in the partial bundle belong to this live logical device and are destroyed on setup failure.
unsafe {
if !partial.command_buffers.is_empty() {
+19 -5
View File
@@ -2760,6 +2760,7 @@ fn append_environment_range(
batch_flags: 0,
pipeline_state,
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
});
Ok(EnvironmentGpuRange {
@@ -7606,7 +7607,7 @@ fn project_basement_foundation(
let mut shadow_receivers = Vec::with_capacity(foundation.fragments.len());
for (fragment_index, fragment) in foundation.fragments.iter().enumerate() {
let (material_index, base_only, vertices) = match fragment.source_face {
let (material_index, base_only, reflection, vertices) = match fragment.source_face {
Some(face_index) => {
let face = terrain.faces.get(face_index).ok_or_else(|| {
format!("foundation fragment references unknown terrain face {face_index}")
@@ -7654,6 +7655,7 @@ fn project_basement_foundation(
(
face.material_tag,
fragment.surface_mask.0 & 0x400 == 0,
fragment.surface_mask.0 & 0x20000 != 0,
vertices,
)
}
@@ -7671,7 +7673,7 @@ fn project_basement_foundation(
fragment_uv0_words,
fragment_normals,
);
(0xff00, true, vertices)
(0xff00, true, false, vertices)
}
};
@@ -7684,7 +7686,13 @@ fn project_basement_foundation(
mesh.indices.push(index);
}
if let Some(range) = mesh.draw_ranges.last_mut().filter(|range| {
range.material_index == material_index && range.terrain_base_only == base_only
range.material_index == material_index
&& range.terrain_base_only == base_only
&& range.terrain_reflection == reflection
&& (!reflection
|| vertices[0].position[2]
== mesh.vertices[mesh.indices[range.first_index as usize] as usize]
.position[2])
}) {
range.index_count = range
.index_count
@@ -7699,6 +7707,7 @@ fn project_basement_foundation(
batch_flags: 0,
pipeline_state,
terrain_base_only: base_only,
terrain_reflection: reflection,
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
});
}
@@ -8004,9 +8013,9 @@ fn static_preview_mesh_and_materials(
let unverified_special_faces = terrain_mesh
.faces
.iter()
.filter(|face| face.flags.0 & 8 != 0)
.filter(|face| face.flags.0 & 8 != 0 && face.flags.0 & 0x20000 == 0)
.count();
eprintln!("render_coverage terrain_face8_pipeline_unverified={unverified_special_faces} (source faces across LODs use the ordinary terrain shader; native special pass and reflection/EMBM mapping pending)");
eprintln!("render_coverage terrain_face8_pipeline_unverified={unverified_special_faces} (non-reflective source faces across LODs; native special-pass mapping pending)");
let footprint_sources = mission_basement_footprints(assets, roots)?;
let foundation = if footprint_sources.is_empty() {
None
@@ -11208,6 +11217,7 @@ mod tests {
batch_flags,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
};
let mesh = VulkanStaticMesh {
@@ -12467,6 +12477,7 @@ mod tests {
LegacyDepthMode::TestReadOnly,
),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 0,
};
let mut mesh = VulkanStaticMesh {
@@ -13149,6 +13160,7 @@ mod tests {
LegacyDepthMode::TestReadOnly,
),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 0,
};
let mut mesh = VulkanStaticMesh {
@@ -13210,6 +13222,7 @@ mod tests {
batch_flags,
pipeline_state: world_pipeline_state(blend, LegacyDepthMode::TestReadOnly),
terrain_base_only: false,
terrain_reflection: false,
alpha_test_reference: 0,
};
@@ -13673,6 +13686,7 @@ mod tests {
lightmap_index: u8::MAX,
batch_flags: 0,
terrain_base_only: false,
terrain_reflection: false,
pipeline_state: world_pipeline_state(
LegacyBlendMode::Opaque,
LegacyDepthMode::TestReadOnly,
+1
View File
@@ -239,6 +239,7 @@ mod tests {
lightmap_index: u8::MAX,
batch_flags: 0,
terrain_base_only: true,
terrain_reflection: false,
pipeline_state: super::super::world_pipeline_state(
fparkan_render::LegacyBlendMode::Opaque,
fparkan_render::LegacyDepthMode::TestWrite,
+40
View File
@@ -349,6 +349,23 @@ pub enum LegacyDepthMode {
TestReadOnly,
}
/// Reflects world Z before a row-vector clip transform, preserving points on the plane.
#[must_use]
pub fn reflect_clip_from_world_about_z(matrix: [f32; 16], height: f32) -> Option<[f32; 16]> {
if !height.is_finite() || !matrix.iter().all(|value| value.is_finite()) {
return None;
}
let mut reflected = matrix;
for column in 0..4 {
reflected[8 + column] = -matrix[8 + column];
reflected[12 + column] = matrix[12 + column] + 2.0 * height * matrix[8 + column];
}
reflected
.iter()
.all(|value| value.is_finite())
.then_some(reflected)
}
/// Triangle culling behaviour represented without a graphics API type.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum LegacyCullMode {
@@ -556,6 +573,29 @@ mod tests {
result
}
#[test]
fn water_reflection_preserves_the_plane_and_reverses_height_before_projection() {
let matrix = [
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
];
let reflected = reflect_clip_from_world_about_z(matrix, 150.0).unwrap();
let project = |m: [f32; 16], p: [f32; 4]| -> [f32; 4] {
std::array::from_fn(|column| (0..4).map(|row| p[row] * m[row * 4 + column]).sum())
};
for z in [125.0, 150.0, 185.0] {
assert_eq!(
project(reflected, [7.0, 11.0, z, 1.0]),
project(matrix, [7.0, 11.0, 300.0 - z, 1.0])
);
}
assert_eq!(
reflect_clip_from_world_about_z(reflected, 150.0),
Some(matrix)
);
assert!(reflect_clip_from_world_about_z(matrix, f32::NAN).is_none());
assert!(reflect_clip_from_world_about_z([f32::MAX; 16], f32::MAX).is_none());
}
fn assert_matrix_approximately_identity(matrix: [f32; 16]) {
for (index, value) in matrix.into_iter().enumerate() {
let expected = if index / 4 == index % 4 { 1.0 } else { 0.0 };
+3 -3
View File
@@ -73,9 +73,9 @@ MSH-запрос со свободной preview-камерой; это не п
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
log на минимальных сценах с одним типом материала.
Полигоны Land.msh с флагом `0x8` пока используют обычный terrain shader; их исходные записи во всех LOD считает `terrain_face8_pipeline_unverified`.
На водоёме «Конструктора» берег близок к оригиналу, а поверхность воды слишком светлая; точные special-pass, UV и combiner остаются открытыми.
В GOG Terrain.dll подтверждена отдельная камера `REFLECTION`/`REFLECTION_SHIFTED` (инициализация RVA `0x13CF0`, render RVA `0x4EFF0`); Vulkan-проход отражения пока отсутствует.
Горизонтальные грани Land.msh с флагом `0x20000` используют плоское Vulkan-отражение; shifted-камера, волны и native EMBM combiner остаются заглушкой `planar_reflection_embm_unsupported`.
Один target отражает ближайшую активную высоту воды; остальные высоты и неплоские грани считаются в `planar_reflection_other_planes_unsupported` и `planar_reflection_nonplanar_unsupported` и сохраняют обычный terrain shader.
`terrain_face8_pipeline_unverified` теперь считает только исходные грани с `0x8` без `0x20000`; сама семантика оставшихся специальных проходов ещё не установлена.
## FXID field-level semantics
+30
View File
@@ -1138,6 +1138,36 @@ lightmap state передаются в `VulkanStaticMaterial`, а shader пос
сэмплирует эти четыре стадии. Animation clock обновляет выбранные фазы и
коэффициенты без пересборки terrain mesh, сохраняя native type-14 overlay mask.
### Отражение на воде
Поверхность воды нельзя рисовать тем же умножением текстур, что и землю.
В проходе ландшафта GOG Terrain.dll флаг грани `0x20000` участвует в выборе
отражения; при включённом EMBM живой водный кадр использует texture mode 10.
Оригинал создаёт для этого отдельную камеру `REFLECTION_SHIFTED`.
В [эталонной сборке](../reference/original-binaries.md) выбор грани находится
в Terrain.dll RVA `0x28A50`, а подготовка EMBM-пакета — в RVA `0x1B540`.
Первый Vulkan-проход строит плоское зеркало. Для горизонтальной воды на
высоте `h` отражённая координата точки равна `z′ = 2h − z`. Это преобразование
применяется перед обычной проекцией. Оно меняет порядок вершин треугольника,
поэтому проход меняет сторону отсечения и отбрасывает геометрию ниже воды.
Небо и оставшаяся сцена попадают в отдельную цветовую текстуру с собственной
глубиной. В основном проходе водные треугольники читают её в экранной
координате: точки самой плоскости не сдвигаются при отражении.
Цветовой target переходит из attachment layout в shader-read layout после
записи. Следующий зеркальный проход ждёт предыдущего чтения этой текстуры;
основной и отражённый виды используют разные слоты uniform buffer. Во время
записи отражения привязан отдельный нейтральный descriptor, чтобы проход
не читал собственное вложение.
Такое зеркало ещё не повторяет shifted-камеру и EMBM оригинала: искажение
отражения волнами пока отсутствует и учитывается в
`planar_reflection_embm_unsupported`. Один target обслуживает ближайшую
активную высоту воды; другие высоты и негоризонтальные поверхности явно
считаются как неподдержанные. Это промежуточный водный проход, а не полная
приёмка воды или лавы.
## Реализация Vulkan
`LegacyPipelineState` хранит blend, depth, cull и alpha-test. Из этих значений