feat(render): add planar water reflections
Docs Deploy / Build and Deploy MkDocs (push) Successful in 1m22s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 1m22s
This commit is contained in:
17 files changed
+905
-191
No files matched your search
@@ -11,6 +11,8 @@
|
|||||||
позе.
|
позе.
|
||||||
Рельеф переключает исходные LOD0/LOD1 по ячейкам относительно текущей камеры;
|
Рельеф переключает исходные 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 = 6) in vec3 in_base_specular;
|
||||||
layout(location = 7) in vec3 in_overlay_diffuse;
|
layout(location = 7) in vec3 in_overlay_diffuse;
|
||||||
layout(location = 8) in vec3 in_overlay_specular;
|
layout(location = 8) in vec3 in_overlay_specular;
|
||||||
|
layout(location = 9) in float in_world_z;
|
||||||
layout(location = 0) out vec4 out_color;
|
layout(location = 0) out vec4 out_color;
|
||||||
|
|
||||||
layout(set = 0, binding = 0) uniform sampler2D base_color;
|
layout(set = 0, binding = 0) uniform sampler2D base_color;
|
||||||
layout(set = 0, binding = 1) uniform sampler2D base_detail;
|
layout(set = 0, binding = 1) uniform sampler2D base_detail;
|
||||||
layout(set = 0, binding = 2) uniform sampler2D overlay_color;
|
layout(set = 0, binding = 2) uniform sampler2D overlay_color;
|
||||||
layout(set = 0, binding = 3) uniform sampler2D overlay_detail;
|
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 {
|
layout(std140, set = 0, binding = 4) uniform FrameConstants {
|
||||||
mat4 clip_from_world;
|
mat4 clip_from_world;
|
||||||
@@ -66,6 +68,19 @@ layout(push_constant) uniform MaterialConstants {
|
|||||||
} material;
|
} material;
|
||||||
|
|
||||||
void main() {
|
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)
|
vec2 base_uv = vec2(material.base_page_x, material.base_page_y)
|
||||||
+ in_uv * vec2(material.base_page_w, material.base_page_h);
|
+ in_uv * vec2(material.base_page_w, material.base_page_h);
|
||||||
vec2 detail_uv = vec2(material.detail_page_x, material.detail_page_y)
|
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 detail = texture(base_detail, detail_uv);
|
||||||
vec4 overlay = texture(overlay_color, overlay_uv);
|
vec4 overlay = texture(overlay_color, overlay_uv);
|
||||||
vec4 overlay_detail_sample = texture(overlay_detail, overlay_detail_uv);
|
vec4 overlay_detail_sample = texture(overlay_detail, overlay_detail_uv);
|
||||||
int mode = int(round(material.material_mode));
|
|
||||||
int combiner_mode = mode & 3;
|
int combiner_mode = mode & 3;
|
||||||
|
|
||||||
// Native sky mode 4 combines nebula and stars as
|
// Native sky mode 4 combines nebula and stars as
|
||||||
|
|||||||
Binary file not shown.
@@ -19,6 +19,7 @@ layout(location = 5) out vec3 out_base_diffuse;
|
|||||||
layout(location = 6) out vec3 out_base_specular;
|
layout(location = 6) out vec3 out_base_specular;
|
||||||
layout(location = 7) out vec3 out_overlay_diffuse;
|
layout(location = 7) out vec3 out_overlay_diffuse;
|
||||||
layout(location = 8) out vec3 out_overlay_specular;
|
layout(location = 8) out vec3 out_overlay_specular;
|
||||||
|
layout(location = 9) out float out_world_z;
|
||||||
|
|
||||||
layout(std140, set = 0, binding = 4) uniform FrameConstants {
|
layout(std140, set = 0, binding = 4) uniform FrameConstants {
|
||||||
mat4 clip_from_world;
|
mat4 clip_from_world;
|
||||||
@@ -241,6 +242,7 @@ LightingTerms native_material_lighting(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
out_world_z = in_position.z;
|
||||||
out_color = in_color;
|
out_color = in_color;
|
||||||
out_uv = in_uv;
|
out_uv = in_uv;
|
||||||
out_detail_uv = in_detail_uv;
|
out_detail_uv = in_detail_uv;
|
||||||
|
|||||||
Binary file not shown.
@@ -866,6 +866,7 @@ fn append_node_fallback_batch(
|
|||||||
batch_flags: batch.batch_flags,
|
batch_flags: batch.batch_flags,
|
||||||
pipeline_state: world_pipeline_state(),
|
pipeline_state: world_pipeline_state(),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1226,6 +1227,7 @@ fn static_terrain_draw_ranges(
|
|||||||
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
|
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
|
||||||
let material_index = first_face.material_tag;
|
let material_index = first_face.material_tag;
|
||||||
let terrain_base_only = first_face.flags.0 & 0x400 == 0;
|
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;
|
let mut end_face = start_face + 1;
|
||||||
while end_face < lod0.len() {
|
while end_face < lod0.len() {
|
||||||
let next_face = terrain
|
let next_face = terrain
|
||||||
@@ -1234,6 +1236,10 @@ fn static_terrain_draw_ranges(
|
|||||||
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
|
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
|
||||||
if next_face.material_tag != material_index
|
if next_face.material_tag != material_index
|
||||||
|| (next_face.flags.0 & 0x400 == 0) != terrain_base_only
|
|| (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;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1253,6 +1259,7 @@ fn static_terrain_draw_ranges(
|
|||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
pipeline_state: world_pipeline_state(),
|
pipeline_state: world_pipeline_state(),
|
||||||
terrain_base_only,
|
terrain_base_only,
|
||||||
|
terrain_reflection,
|
||||||
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
||||||
});
|
});
|
||||||
start_face = end_face;
|
start_face = end_face;
|
||||||
@@ -1299,6 +1306,7 @@ fn static_model_indices_and_ranges(
|
|||||||
batch_flags: batch.batch_flags,
|
batch_flags: batch.batch_flags,
|
||||||
pipeline_state: world_pipeline_state(),
|
pipeline_state: world_pipeline_state(),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1506,6 +1514,7 @@ mod tests {
|
|||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
pipeline_state: world_pipeline_state(),
|
pipeline_state: world_pipeline_state(),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
@@ -2036,6 +2045,7 @@ mod tests {
|
|||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
pipeline_state: world_pipeline_state(),
|
pipeline_state: world_pipeline_state(),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference:
|
alpha_test_reference:
|
||||||
VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
||||||
},
|
},
|
||||||
@@ -2047,6 +2057,7 @@ mod tests {
|
|||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
pipeline_state: world_pipeline_state(),
|
pipeline_state: world_pipeline_state(),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference:
|
alpha_test_reference:
|
||||||
VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
||||||
},
|
},
|
||||||
@@ -2298,5 +2309,21 @@ mod tests {
|
|||||||
assert!(ranges.iter().all(|range| {
|
assert!(ranges.iter().all(|range| {
|
||||||
range.alpha_test_reference == VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE
|
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 asset_mesh;
|
||||||
mod capabilities;
|
mod capabilities;
|
||||||
mod instance;
|
mod instance;
|
||||||
|
mod reflection;
|
||||||
mod resources;
|
mod resources;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
mod smoke;
|
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(
|
pub(super) fn create_frame_uniform_buffer(
|
||||||
instance: &VulkanInstanceProbe,
|
instance: &VulkanInstanceProbe,
|
||||||
device: &VulkanLogicalDeviceProbe,
|
device: &VulkanLogicalDeviceProbe,
|
||||||
@@ -545,7 +545,7 @@ pub(super) fn create_frame_uniform_buffer(
|
|||||||
)? / alignment
|
)? / alignment
|
||||||
* alignment;
|
* alignment;
|
||||||
let size = stride
|
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 {
|
.ok_or(VulkanSmokeRendererError::InvalidStaticMesh {
|
||||||
context: "frame uniform buffer size overflow",
|
context: "frame uniform buffer size overflow",
|
||||||
})?;
|
})?;
|
||||||
@@ -888,7 +888,7 @@ pub(super) fn write_frame_uniform(
|
|||||||
.ok_or(VulkanSmokeRendererError::InvariantViolation {
|
.ok_or(VulkanSmokeRendererError::InvariantViolation {
|
||||||
context: "frame uniform slot offset",
|
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.
|
// mapped range is exactly one complete frame block.
|
||||||
let mapped = unsafe {
|
let mapped = unsafe {
|
||||||
device.device().map_memory(
|
device.device().map_memory(
|
||||||
@@ -1385,7 +1385,7 @@ fn create_host_visible_buffer(
|
|||||||
Ok(VulkanAllocatedBuffer { buffer, memory })
|
Ok(VulkanAllocatedBuffer { buffer, memory })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_memory_type(
|
pub(super) fn find_memory_type(
|
||||||
instance: &VulkanInstanceProbe,
|
instance: &VulkanInstanceProbe,
|
||||||
physical_device: vk::PhysicalDevice,
|
physical_device: vk::PhysicalDevice,
|
||||||
type_bits: u32,
|
type_bits: u32,
|
||||||
|
|||||||
@@ -357,6 +357,28 @@ impl VulkanSmokeRenderer {
|
|||||||
.map(|material| material.material_index)
|
.map(|material| material.material_index)
|
||||||
.collect()
|
.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 {
|
let mut renderer = Self {
|
||||||
instance: Some(instance),
|
instance: Some(instance),
|
||||||
validation,
|
validation,
|
||||||
@@ -379,6 +401,7 @@ impl VulkanSmokeRenderer {
|
|||||||
.map(|range| range.index_count)
|
.map(|range| range.index_count)
|
||||||
.collect(),
|
.collect(),
|
||||||
draw_ranges: create_info.mesh.draw_ranges.clone(),
|
draw_ranges: create_info.mesh.draw_ranges.clone(),
|
||||||
|
reflection_planes,
|
||||||
draw_texture_indices,
|
draw_texture_indices,
|
||||||
draw_order: (0..create_info.mesh.draw_ranges.len()).collect(),
|
draw_order: (0..create_info.mesh.draw_ranges.len()).collect(),
|
||||||
draw_range_sort_keys: vec![None; create_info.mesh.draw_ranges.len()],
|
draw_range_sort_keys: vec![None; create_info.mesh.draw_ranges.len()],
|
||||||
@@ -521,6 +544,22 @@ impl VulkanSmokeRenderer {
|
|||||||
self.frame_uniforms
|
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.
|
/// Replaces the camera for subsequent frames without recreating GPU resources.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
@@ -1516,6 +1555,27 @@ impl VulkanSmokeRenderer {
|
|||||||
}
|
}
|
||||||
self.images_in_flight[image_index_usize] = in_flight_fence;
|
self.images_in_flight[image_index_usize] = in_flight_fence;
|
||||||
let frame_bytes = self.frame_uniforms.to_ne_bytes();
|
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 explicit_point_lights = self.point_lights_explicit;
|
||||||
let legacy_point_light = self.frame_uniforms.point_light;
|
let legacy_point_light = self.frame_uniforms.point_light;
|
||||||
@@ -1541,9 +1601,18 @@ impl VulkanSmokeRenderer {
|
|||||||
device,
|
device,
|
||||||
&resources.frame_uniform_buffer,
|
&resources.frame_uniform_buffer,
|
||||||
resources.frame_uniform_stride,
|
resources.frame_uniform_stride,
|
||||||
self.current_frame,
|
self.current_frame * 2,
|
||||||
&frame_bytes,
|
&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(
|
write_point_light_buffer(
|
||||||
device,
|
device,
|
||||||
&resources.point_light_buffer,
|
&resources.point_light_buffer,
|
||||||
@@ -1686,6 +1755,9 @@ impl VulkanSmokeRenderer {
|
|||||||
self.textures_ref()?,
|
self.textures_ref()?,
|
||||||
&self.texture_slot_indices,
|
&self.texture_slot_indices,
|
||||||
&self.draw_range_base_pipeline_states,
|
&self.draw_range_base_pipeline_states,
|
||||||
|
self.draw_ranges
|
||||||
|
.iter()
|
||||||
|
.any(|range| range.terrain_reflection),
|
||||||
self.depth_request,
|
self.depth_request,
|
||||||
reuse_command_pool,
|
reuse_command_pool,
|
||||||
)?,
|
)?,
|
||||||
@@ -1741,6 +1813,7 @@ impl VulkanSmokeRenderer {
|
|||||||
self.rebuild_draw_order();
|
self.rebuild_draw_order();
|
||||||
self.draw_order_dirty = false;
|
self.draw_order_dirty = false;
|
||||||
}
|
}
|
||||||
|
let reflection_plane = self.active_reflection_plane();
|
||||||
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()?;
|
||||||
@@ -1787,185 +1860,269 @@ impl VulkanSmokeRenderer {
|
|||||||
height: swapchain.report.plan.extent.1,
|
height: swapchain.report.plan.extent.1,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let render_pass_info = vk::RenderPassBeginInfo::default()
|
for reflected in [true, false] {
|
||||||
.render_pass(resources.render_pass)
|
if reflected && reflection_plane.is_none() {
|
||||||
.framebuffer(resources.framebuffers[image_index])
|
continue;
|
||||||
.render_area(render_area)
|
}
|
||||||
.clear_values(&clear_values);
|
let framebuffer = if reflected {
|
||||||
// SAFETY: All commands target live frame resources owned by this renderer.
|
resources.reflection.framebuffer
|
||||||
unsafe {
|
} else {
|
||||||
device.device().cmd_begin_render_pass(
|
resources.framebuffers[image_index]
|
||||||
command_buffer,
|
};
|
||||||
&render_pass_info,
|
let render_pass_info = vk::RenderPassBeginInfo::default()
|
||||||
vk::SubpassContents::INLINE,
|
.render_pass(resources.render_pass)
|
||||||
);
|
.framebuffer(framebuffer)
|
||||||
let vertex_buffers = [self.vertex_buffer_ref()?.buffer];
|
.render_area(render_area)
|
||||||
let offsets = [0_u64];
|
.clear_values(&clear_values);
|
||||||
device
|
// SAFETY: All commands target live frame resources owned by this renderer.
|
||||||
.device()
|
unsafe {
|
||||||
.cmd_bind_vertex_buffers(command_buffer, 0, &vertex_buffers, &offsets);
|
device.device().cmd_begin_render_pass(
|
||||||
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(
|
|
||||||
command_buffer,
|
command_buffer,
|
||||||
vk::PipelineBindPoint::GRAPHICS,
|
&render_pass_info,
|
||||||
*pipeline,
|
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(
|
device.device().cmd_bind_descriptor_sets(
|
||||||
command_buffer,
|
command_buffer,
|
||||||
vk::PipelineBindPoint::GRAPHICS,
|
vk::PipelineBindPoint::GRAPHICS,
|
||||||
resources.pipeline_layout,
|
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,
|
1,
|
||||||
range.first_index,
|
&[resources.reflection.sets
|
||||||
0,
|
[usize::from(!reflected && reflection_plane.is_some())]],
|
||||||
0,
|
&[],
|
||||||
);
|
);
|
||||||
|
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 self.readback_enabled {
|
||||||
if let (Some(image), Some(readback)) = (
|
if let (Some(image), Some(readback)) = (
|
||||||
resources.images.get(image_index),
|
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| {
|
unsafe { device.device().end_command_buffer(command_buffer) }.map_err(|error| {
|
||||||
VulkanSmokeRendererError::VulkanOperation {
|
VulkanSmokeRendererError::VulkanOperation {
|
||||||
context: "vkEndCommandBuffer",
|
context: "vkEndCommandBuffer",
|
||||||
@@ -2720,6 +2877,7 @@ mod tests {
|
|||||||
batch_flags: 0x108,
|
batch_flags: 0x108,
|
||||||
pipeline_state: base_state,
|
pipeline_state: base_state,
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 17,
|
alpha_test_reference: 17,
|
||||||
},
|
},
|
||||||
VulkanStaticDrawRange {
|
VulkanStaticDrawRange {
|
||||||
@@ -2730,6 +2888,7 @@ mod tests {
|
|||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
pipeline_state: LegacyPipelineState::default(),
|
pipeline_state: LegacyPipelineState::default(),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 0,
|
alpha_test_reference: 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -2886,6 +3045,7 @@ mod tests {
|
|||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
pipeline_state: LegacyPipelineState::default(),
|
pipeline_state: LegacyPipelineState::default(),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 0,
|
alpha_test_reference: 0,
|
||||||
}];
|
}];
|
||||||
let mut capacities = [6];
|
let mut capacities = [6];
|
||||||
@@ -3072,6 +3232,7 @@ mod tests {
|
|||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
pipeline_state: fparkan_render::LegacyPipelineState::default(),
|
pipeline_state: fparkan_render::LegacyPipelineState::default(),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 0,
|
alpha_test_reference: 0,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -3096,6 +3257,7 @@ mod tests {
|
|||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
pipeline_state: fparkan_render::LegacyPipelineState::default(),
|
pipeline_state: fparkan_render::LegacyPipelineState::default(),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 0,
|
alpha_test_reference: 0,
|
||||||
},
|
},
|
||||||
super::super::VulkanStaticDrawRange {
|
super::super::VulkanStaticDrawRange {
|
||||||
@@ -3106,6 +3268,7 @@ mod tests {
|
|||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
pipeline_state: fparkan_render::LegacyPipelineState::default(),
|
pipeline_state: fparkan_render::LegacyPipelineState::default(),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 0,
|
alpha_test_reference: 0,
|
||||||
},
|
},
|
||||||
super::super::VulkanStaticDrawRange {
|
super::super::VulkanStaticDrawRange {
|
||||||
@@ -3116,6 +3279,7 @@ mod tests {
|
|||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
pipeline_state: fparkan_render::LegacyPipelineState::default(),
|
pipeline_state: fparkan_render::LegacyPipelineState::default(),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 0,
|
alpha_test_reference: 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -3140,6 +3304,7 @@ mod tests {
|
|||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
pipeline_state: fparkan_render::LegacyPipelineState::default(),
|
pipeline_state: fparkan_render::LegacyPipelineState::default(),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 0,
|
alpha_test_reference: 0,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -3233,6 +3398,7 @@ mod tests {
|
|||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
pipeline_state: LegacyPipelineState::default(),
|
pipeline_state: LegacyPipelineState::default(),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 0,
|
alpha_test_reference: 0,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -3291,6 +3457,7 @@ mod tests {
|
|||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
pipeline_state: LegacyPipelineState::default(),
|
pipeline_state: LegacyPipelineState::default(),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 0,
|
alpha_test_reference: 0,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|||||||
@@ -437,6 +437,8 @@ pub struct VulkanStaticDrawRange {
|
|||||||
pub batch_flags: u16,
|
pub batch_flags: u16,
|
||||||
/// Terrain ranges that use the native lit base-only path (face flag 0x400 clear).
|
/// Terrain ranges that use the native lit base-only path (face flag 0x400 clear).
|
||||||
pub terrain_base_only: bool,
|
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.
|
/// Backend-neutral fixed-function state for this source range.
|
||||||
pub pipeline_state: LegacyPipelineState,
|
pub pipeline_state: LegacyPipelineState,
|
||||||
/// Alpha-test reference in legacy 0..=255 units; dynamic material data.
|
/// Alpha-test reference in legacy 0..=255 units; dynamic material data.
|
||||||
@@ -771,6 +773,7 @@ impl VulkanStaticMesh {
|
|||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
pipeline_state: LegacyPipelineState::default(),
|
pipeline_state: LegacyPipelineState::default(),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 0,
|
alpha_test_reference: 0,
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
@@ -1032,6 +1035,7 @@ mod static_mesh_tests {
|
|||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
pipeline_state: LegacyPipelineState::default(),
|
pipeline_state: LegacyPipelineState::default(),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 0,
|
alpha_test_reference: 0,
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
@@ -1063,6 +1067,7 @@ mod static_mesh_tests {
|
|||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
pipeline_state: LegacyPipelineState::default(),
|
pipeline_state: LegacyPipelineState::default(),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 0,
|
alpha_test_reference: 0,
|
||||||
},
|
},
|
||||||
VulkanStaticDrawRange {
|
VulkanStaticDrawRange {
|
||||||
@@ -1073,6 +1078,7 @@ mod static_mesh_tests {
|
|||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
pipeline_state: LegacyPipelineState::default(),
|
pipeline_state: LegacyPipelineState::default(),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 0,
|
alpha_test_reference: 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -1191,6 +1197,7 @@ mod static_mesh_tests {
|
|||||||
let base = VulkanStaticMesh::smoke_triangle().draw_ranges[0];
|
let base = VulkanStaticMesh::smoke_triangle().draw_ranges[0];
|
||||||
let disabled = VulkanStaticDrawRange {
|
let disabled = VulkanStaticDrawRange {
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 200,
|
alpha_test_reference: 200,
|
||||||
..base
|
..base
|
||||||
};
|
};
|
||||||
@@ -1200,6 +1207,7 @@ mod static_mesh_tests {
|
|||||||
..LegacyPipelineState::default()
|
..LegacyPipelineState::default()
|
||||||
},
|
},
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 128,
|
alpha_test_reference: 128,
|
||||||
..base
|
..base
|
||||||
};
|
};
|
||||||
@@ -1211,6 +1219,7 @@ mod static_mesh_tests {
|
|||||||
..LegacyPipelineState::default()
|
..LegacyPipelineState::default()
|
||||||
},
|
},
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 0,
|
alpha_test_reference: 0,
|
||||||
..base
|
..base
|
||||||
};
|
};
|
||||||
@@ -1221,6 +1230,7 @@ mod static_mesh_tests {
|
|||||||
..LegacyPipelineState::default()
|
..LegacyPipelineState::default()
|
||||||
},
|
},
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
||||||
..base
|
..base
|
||||||
};
|
};
|
||||||
@@ -1501,6 +1511,7 @@ pub struct VulkanSmokeRenderer {
|
|||||||
pub(super) index_capacity: usize,
|
pub(super) index_capacity: usize,
|
||||||
pub(super) draw_range_capacities: Vec<u32>,
|
pub(super) draw_range_capacities: Vec<u32>,
|
||||||
pub(super) draw_ranges: Vec<VulkanStaticDrawRange>,
|
pub(super) draw_ranges: Vec<VulkanStaticDrawRange>,
|
||||||
|
pub(super) reflection_planes: Vec<Option<f32>>,
|
||||||
pub(super) draw_texture_indices: Vec<usize>,
|
pub(super) draw_texture_indices: Vec<usize>,
|
||||||
/// Stable range handles used when recording the native pass order.
|
/// Stable range handles used when recording the native pass order.
|
||||||
pub(super) draw_order: Vec<usize>,
|
pub(super) draw_order: Vec<usize>,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use super::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub(super) struct VulkanSwapchainResources {
|
pub(super) struct VulkanSwapchainResources {
|
||||||
|
pub(super) reflection: super::reflection::ReflectionResources,
|
||||||
pub(super) image_views: Vec<vk::ImageView>,
|
pub(super) image_views: Vec<vk::ImageView>,
|
||||||
pub(super) images: Vec<vk::Image>,
|
pub(super) images: Vec<vk::Image>,
|
||||||
pub(super) readback_buffers: Vec<VulkanAllocatedBuffer>,
|
pub(super) readback_buffers: Vec<VulkanAllocatedBuffer>,
|
||||||
@@ -146,6 +147,7 @@ pub(super) fn ensure_point_light_capacity(
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct PartialSwapchainResources {
|
struct PartialSwapchainResources {
|
||||||
|
reflection: Option<super::reflection::ReflectionResources>,
|
||||||
image_views: Vec<vk::ImageView>,
|
image_views: Vec<vk::ImageView>,
|
||||||
readback_buffers: Vec<VulkanAllocatedBuffer>,
|
readback_buffers: Vec<VulkanAllocatedBuffer>,
|
||||||
depth_attachments: Vec<VulkanDepthAttachment>,
|
depth_attachments: Vec<VulkanDepthAttachment>,
|
||||||
@@ -180,6 +182,7 @@ pub(super) fn create_swapchain_resources(
|
|||||||
textures: &[VulkanAllocatedImage],
|
textures: &[VulkanAllocatedImage],
|
||||||
texture_slot_indices: &[usize],
|
texture_slot_indices: &[usize],
|
||||||
base_pipeline_states: &[LegacyPipelineState],
|
base_pipeline_states: &[LegacyPipelineState],
|
||||||
|
reflection_enabled: bool,
|
||||||
depth_request: DepthStencilSupport,
|
depth_request: DepthStencilSupport,
|
||||||
reuse_command_pool: bool,
|
reuse_command_pool: bool,
|
||||||
) -> Result<VulkanSwapchainResources, VulkanSmokeRendererError> {
|
) -> Result<VulkanSwapchainResources, VulkanSmokeRendererError> {
|
||||||
@@ -194,6 +197,7 @@ pub(super) fn create_swapchain_resources(
|
|||||||
result: error,
|
result: error,
|
||||||
})?;
|
})?;
|
||||||
let mut partial = PartialSwapchainResources {
|
let mut partial = PartialSwapchainResources {
|
||||||
|
reflection: None,
|
||||||
image_views: create_swapchain_image_views(
|
image_views: create_swapchain_image_views(
|
||||||
device,
|
device,
|
||||||
&images,
|
&images,
|
||||||
@@ -245,6 +249,14 @@ pub(super) fn create_swapchain_resources(
|
|||||||
};
|
};
|
||||||
partial.frame_uniform_stride = frame_uniform_stride;
|
partial.frame_uniform_stride = frame_uniform_stride;
|
||||||
partial.frame_uniform_buffer = Some(frame_uniform_buffer);
|
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_count = texture_slot_indices.len() / 4;
|
||||||
let (material_specular_buffer, material_specular_stride) =
|
let (material_specular_buffer, material_specular_stride) =
|
||||||
match create_material_specular_buffer(instance, device, material_count) {
|
match create_material_specular_buffer(instance, device, material_count) {
|
||||||
@@ -288,6 +300,12 @@ pub(super) fn create_swapchain_resources(
|
|||||||
textures,
|
textures,
|
||||||
texture_slot_indices,
|
texture_slot_indices,
|
||||||
base_pipeline_states,
|
base_pipeline_states,
|
||||||
|
partial
|
||||||
|
.reflection
|
||||||
|
.as_ref()
|
||||||
|
.expect("reflection descriptors exist")
|
||||||
|
.layout,
|
||||||
|
reflection_enabled,
|
||||||
depth_request,
|
depth_request,
|
||||||
partial
|
partial
|
||||||
.frame_uniform_buffer
|
.frame_uniform_buffer
|
||||||
@@ -319,6 +337,24 @@ pub(super) fn create_swapchain_resources(
|
|||||||
partial.descriptor_pool = Some(descriptor_pool);
|
partial.descriptor_pool = Some(descriptor_pool);
|
||||||
partial.sampler = Some(sampler);
|
partial.sampler = Some(sampler);
|
||||||
partial.pipelines = pipelines;
|
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() {
|
for _ in 1..images.len() {
|
||||||
match create_depth_attachment(
|
match create_depth_attachment(
|
||||||
instance,
|
instance,
|
||||||
@@ -394,6 +430,10 @@ pub(super) fn create_swapchain_resources(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(VulkanSwapchainResources {
|
Ok(VulkanSwapchainResources {
|
||||||
|
reflection: partial
|
||||||
|
.reflection
|
||||||
|
.take()
|
||||||
|
.expect("reflection descriptors exist"),
|
||||||
image_views: partial.image_views,
|
image_views: partial.image_views,
|
||||||
images,
|
images,
|
||||||
readback_buffers: partial.readback_buffers,
|
readback_buffers: partial.readback_buffers,
|
||||||
@@ -457,6 +497,8 @@ fn create_swapchain_pipeline_bundle(
|
|||||||
textures: &[VulkanAllocatedImage],
|
textures: &[VulkanAllocatedImage],
|
||||||
texture_slot_indices: &[usize],
|
texture_slot_indices: &[usize],
|
||||||
base_pipeline_states: &[LegacyPipelineState],
|
base_pipeline_states: &[LegacyPipelineState],
|
||||||
|
reflection_layout: vk::DescriptorSetLayout,
|
||||||
|
reflection_enabled: bool,
|
||||||
depth_request: DepthStencilSupport,
|
depth_request: DepthStencilSupport,
|
||||||
frame_uniform_buffer: &VulkanAllocatedBuffer,
|
frame_uniform_buffer: &VulkanAllocatedBuffer,
|
||||||
material_specular_buffer: &VulkanAllocatedBuffer,
|
material_specular_buffer: &VulkanAllocatedBuffer,
|
||||||
@@ -498,8 +540,8 @@ fn create_swapchain_pipeline_bundle(
|
|||||||
unsafe { device.device().destroy_render_pass(render_pass, None) };
|
unsafe { device.device().destroy_render_pass(render_pass, None) };
|
||||||
destroy_depth_attachment(device, &depth_attachment);
|
destroy_depth_attachment(device, &depth_attachment);
|
||||||
})?;
|
})?;
|
||||||
let pipeline_layout =
|
let pipeline_layout = create_pipeline_layout(device, descriptor_set_layout, reflection_layout)
|
||||||
create_pipeline_layout(device, descriptor_set_layout).inspect_err(|_| {
|
.inspect_err(|_| {
|
||||||
// SAFETY: The descriptor resources and render pass were created above and are rolled back on this device.
|
// SAFETY: The descriptor resources and render pass were created above and are rolled back on this device.
|
||||||
unsafe {
|
unsafe {
|
||||||
device.device().destroy_sampler(sampler, None);
|
device.device().destroy_sampler(sampler, None);
|
||||||
@@ -520,6 +562,7 @@ fn create_swapchain_pipeline_bundle(
|
|||||||
pipeline_layout,
|
pipeline_layout,
|
||||||
extent,
|
extent,
|
||||||
base_pipeline_states,
|
base_pipeline_states,
|
||||||
|
reflection_enabled,
|
||||||
)
|
)
|
||||||
.inspect_err(|_| {
|
.inspect_err(|_| {
|
||||||
// SAFETY: These objects were created above on this live logical device and are destroyed on setup failure.
|
// 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)
|
.load_op(vk::AttachmentLoadOp::CLEAR)
|
||||||
.store_op(vk::AttachmentStoreOp::STORE)
|
.store_op(vk::AttachmentStoreOp::STORE)
|
||||||
.initial_layout(vk::ImageLayout::UNDEFINED)
|
.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()
|
let color_attachment_ref = vk::AttachmentReference::default()
|
||||||
.attachment(0)
|
.attachment(0)
|
||||||
.layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
|
.layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
|
||||||
@@ -661,7 +704,8 @@ fn create_render_pass(
|
|||||||
.src_subpass(vk::SUBPASS_EXTERNAL)
|
.src_subpass(vk::SUBPASS_EXTERNAL)
|
||||||
.dst_subpass(0)
|
.dst_subpass(0)
|
||||||
.src_stage_mask(
|
.src_stage_mask(
|
||||||
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
|
vk::PipelineStageFlags::FRAGMENT_SHADER
|
||||||
|
| vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
|
||||||
| vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS
|
| vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS
|
||||||
| vk::PipelineStageFlags::LATE_FRAGMENT_TESTS,
|
| vk::PipelineStageFlags::LATE_FRAGMENT_TESTS,
|
||||||
)
|
)
|
||||||
@@ -670,7 +714,11 @@ fn create_render_pass(
|
|||||||
| vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS
|
| vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS
|
||||||
| vk::PipelineStageFlags::LATE_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(
|
.dst_access_mask(
|
||||||
vk::AccessFlags::COLOR_ATTACHMENT_WRITE
|
vk::AccessFlags::COLOR_ATTACHMENT_WRITE
|
||||||
| vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE,
|
| vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE,
|
||||||
@@ -694,8 +742,9 @@ fn create_render_pass(
|
|||||||
fn create_pipeline_layout(
|
fn create_pipeline_layout(
|
||||||
device: &VulkanLogicalDeviceProbe,
|
device: &VulkanLogicalDeviceProbe,
|
||||||
descriptor_set_layout: vk::DescriptorSetLayout,
|
descriptor_set_layout: vk::DescriptorSetLayout,
|
||||||
|
reflection_layout: vk::DescriptorSetLayout,
|
||||||
) -> Result<vk::PipelineLayout, VulkanSmokeRendererError> {
|
) -> 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
|
// Material data occupies one 128-byte push-constant allocation shared by
|
||||||
// both stages; the vertex stage also reads its specular pair from the
|
// both stages; the vertex stage also reads its specular pair from the
|
||||||
// dynamic storage buffer at descriptor binding 5. Camera data is supplied
|
// dynamic storage buffer at descriptor binding 5. Camera data is supplied
|
||||||
@@ -960,12 +1009,18 @@ fn create_graphics_pipeline_cache(
|
|||||||
pipeline_layout: vk::PipelineLayout,
|
pipeline_layout: vk::PipelineLayout,
|
||||||
extent: (u32, u32),
|
extent: (u32, u32),
|
||||||
base_pipeline_states: &[LegacyPipelineState],
|
base_pipeline_states: &[LegacyPipelineState],
|
||||||
|
reflection_enabled: bool,
|
||||||
) -> Result<BTreeMap<PipelineKey, vk::Pipeline>, VulkanSmokeRendererError> {
|
) -> Result<BTreeMap<PipelineKey, vk::Pipeline>, VulkanSmokeRendererError> {
|
||||||
let mut pipelines = BTreeMap::new();
|
let mut pipelines = BTreeMap::new();
|
||||||
for state in base_pipeline_states
|
for state in base_pipeline_states
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.flat_map(super::smoke::pipeline_variants)
|
.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);
|
let key = PipelineKey::from(state);
|
||||||
if pipelines.contains_key(&key) {
|
if pipelines.contains_key(&key) {
|
||||||
@@ -1238,6 +1293,7 @@ pub(super) fn destroy_swapchain_resources(
|
|||||||
command_pool: vk::CommandPool,
|
command_pool: vk::CommandPool,
|
||||||
resources: VulkanSwapchainResources,
|
resources: VulkanSwapchainResources,
|
||||||
) {
|
) {
|
||||||
|
drop(resources.reflection);
|
||||||
// SAFETY: All handles belong to this live logical device and are destroyed during renderer teardown/recreation.
|
// SAFETY: All handles belong to this live logical device and are destroyed during renderer teardown/recreation.
|
||||||
unsafe {
|
unsafe {
|
||||||
if !resources.command_buffers.is_empty() {
|
if !resources.command_buffers.is_empty() {
|
||||||
@@ -1303,6 +1359,7 @@ fn destroy_partial_swapchain_resources(
|
|||||||
command_pool: vk::CommandPool,
|
command_pool: vk::CommandPool,
|
||||||
partial: PartialSwapchainResources,
|
partial: PartialSwapchainResources,
|
||||||
) {
|
) {
|
||||||
|
drop(partial.reflection);
|
||||||
// SAFETY: All handles in the partial bundle belong to this live logical device and are destroyed on setup failure.
|
// SAFETY: All handles in the partial bundle belong to this live logical device and are destroyed on setup failure.
|
||||||
unsafe {
|
unsafe {
|
||||||
if !partial.command_buffers.is_empty() {
|
if !partial.command_buffers.is_empty() {
|
||||||
|
|||||||
@@ -2760,6 +2760,7 @@ fn append_environment_range(
|
|||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
pipeline_state,
|
pipeline_state,
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
||||||
});
|
});
|
||||||
Ok(EnvironmentGpuRange {
|
Ok(EnvironmentGpuRange {
|
||||||
@@ -7606,7 +7607,7 @@ fn project_basement_foundation(
|
|||||||
let mut shadow_receivers = Vec::with_capacity(foundation.fragments.len());
|
let mut shadow_receivers = Vec::with_capacity(foundation.fragments.len());
|
||||||
|
|
||||||
for (fragment_index, fragment) in foundation.fragments.iter().enumerate() {
|
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) => {
|
Some(face_index) => {
|
||||||
let face = terrain.faces.get(face_index).ok_or_else(|| {
|
let face = terrain.faces.get(face_index).ok_or_else(|| {
|
||||||
format!("foundation fragment references unknown terrain face {face_index}")
|
format!("foundation fragment references unknown terrain face {face_index}")
|
||||||
@@ -7654,6 +7655,7 @@ fn project_basement_foundation(
|
|||||||
(
|
(
|
||||||
face.material_tag,
|
face.material_tag,
|
||||||
fragment.surface_mask.0 & 0x400 == 0,
|
fragment.surface_mask.0 & 0x400 == 0,
|
||||||
|
fragment.surface_mask.0 & 0x20000 != 0,
|
||||||
vertices,
|
vertices,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -7671,7 +7673,7 @@ fn project_basement_foundation(
|
|||||||
fragment_uv0_words,
|
fragment_uv0_words,
|
||||||
fragment_normals,
|
fragment_normals,
|
||||||
);
|
);
|
||||||
(0xff00, true, vertices)
|
(0xff00, true, false, vertices)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -7684,7 +7686,13 @@ fn project_basement_foundation(
|
|||||||
mesh.indices.push(index);
|
mesh.indices.push(index);
|
||||||
}
|
}
|
||||||
if let Some(range) = mesh.draw_ranges.last_mut().filter(|range| {
|
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
|
range.index_count = range
|
||||||
.index_count
|
.index_count
|
||||||
@@ -7699,6 +7707,7 @@ fn project_basement_foundation(
|
|||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
pipeline_state,
|
pipeline_state,
|
||||||
terrain_base_only: base_only,
|
terrain_base_only: base_only,
|
||||||
|
terrain_reflection: reflection,
|
||||||
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
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
|
let unverified_special_faces = terrain_mesh
|
||||||
.faces
|
.faces
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|face| face.flags.0 & 8 != 0)
|
.filter(|face| face.flags.0 & 8 != 0 && face.flags.0 & 0x20000 == 0)
|
||||||
.count();
|
.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 footprint_sources = mission_basement_footprints(assets, roots)?;
|
||||||
let foundation = if footprint_sources.is_empty() {
|
let foundation = if footprint_sources.is_empty() {
|
||||||
None
|
None
|
||||||
@@ -11208,6 +11217,7 @@ mod tests {
|
|||||||
batch_flags,
|
batch_flags,
|
||||||
pipeline_state: LegacyPipelineState::default(),
|
pipeline_state: LegacyPipelineState::default(),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
|
||||||
};
|
};
|
||||||
let mesh = VulkanStaticMesh {
|
let mesh = VulkanStaticMesh {
|
||||||
@@ -12467,6 +12477,7 @@ mod tests {
|
|||||||
LegacyDepthMode::TestReadOnly,
|
LegacyDepthMode::TestReadOnly,
|
||||||
),
|
),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 0,
|
alpha_test_reference: 0,
|
||||||
};
|
};
|
||||||
let mut mesh = VulkanStaticMesh {
|
let mut mesh = VulkanStaticMesh {
|
||||||
@@ -13149,6 +13160,7 @@ mod tests {
|
|||||||
LegacyDepthMode::TestReadOnly,
|
LegacyDepthMode::TestReadOnly,
|
||||||
),
|
),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 0,
|
alpha_test_reference: 0,
|
||||||
};
|
};
|
||||||
let mut mesh = VulkanStaticMesh {
|
let mut mesh = VulkanStaticMesh {
|
||||||
@@ -13210,6 +13222,7 @@ mod tests {
|
|||||||
batch_flags,
|
batch_flags,
|
||||||
pipeline_state: world_pipeline_state(blend, LegacyDepthMode::TestReadOnly),
|
pipeline_state: world_pipeline_state(blend, LegacyDepthMode::TestReadOnly),
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
alpha_test_reference: 0,
|
alpha_test_reference: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -13673,6 +13686,7 @@ mod tests {
|
|||||||
lightmap_index: u8::MAX,
|
lightmap_index: u8::MAX,
|
||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
terrain_base_only: false,
|
terrain_base_only: false,
|
||||||
|
terrain_reflection: false,
|
||||||
pipeline_state: world_pipeline_state(
|
pipeline_state: world_pipeline_state(
|
||||||
LegacyBlendMode::Opaque,
|
LegacyBlendMode::Opaque,
|
||||||
LegacyDepthMode::TestReadOnly,
|
LegacyDepthMode::TestReadOnly,
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ mod tests {
|
|||||||
lightmap_index: u8::MAX,
|
lightmap_index: u8::MAX,
|
||||||
batch_flags: 0,
|
batch_flags: 0,
|
||||||
terrain_base_only: true,
|
terrain_base_only: true,
|
||||||
|
terrain_reflection: false,
|
||||||
pipeline_state: super::super::world_pipeline_state(
|
pipeline_state: super::super::world_pipeline_state(
|
||||||
fparkan_render::LegacyBlendMode::Opaque,
|
fparkan_render::LegacyBlendMode::Opaque,
|
||||||
fparkan_render::LegacyDepthMode::TestWrite,
|
fparkan_render::LegacyDepthMode::TestWrite,
|
||||||
|
|||||||
@@ -349,6 +349,23 @@ pub enum LegacyDepthMode {
|
|||||||
TestReadOnly,
|
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.
|
/// Triangle culling behaviour represented without a graphics API type.
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
pub enum LegacyCullMode {
|
pub enum LegacyCullMode {
|
||||||
@@ -556,6 +573,29 @@ mod tests {
|
|||||||
result
|
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]) {
|
fn assert_matrix_approximately_identity(matrix: [f32; 16]) {
|
||||||
for (index, value) in matrix.into_iter().enumerate() {
|
for (index, value) in matrix.into_iter().enumerate() {
|
||||||
let expected = if index / 4 == index % 4 { 1.0 } else { 0.0 };
|
let expected = if index / 4 == index % 4 { 1.0 } else { 0.0 };
|
||||||
|
|||||||
@@ -73,9 +73,9 @@ MSH-запрос со свободной preview-камерой; это не п
|
|||||||
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
|
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
|
||||||
log на минимальных сценах с одним типом материала.
|
log на минимальных сценах с одним типом материала.
|
||||||
|
|
||||||
Полигоны Land.msh с флагом `0x8` пока используют обычный terrain shader; их исходные записи во всех LOD считает `terrain_face8_pipeline_unverified`.
|
Горизонтальные грани Land.msh с флагом `0x20000` используют плоское Vulkan-отражение; shifted-камера, волны и native EMBM combiner остаются заглушкой `planar_reflection_embm_unsupported`.
|
||||||
На водоёме «Конструктора» берег близок к оригиналу, а поверхность воды слишком светлая; точные special-pass, UV и combiner остаются открытыми.
|
Один target отражает ближайшую активную высоту воды; остальные высоты и неплоские грани считаются в `planar_reflection_other_planes_unsupported` и `planar_reflection_nonplanar_unsupported` и сохраняют обычный terrain shader.
|
||||||
В GOG Terrain.dll подтверждена отдельная камера `REFLECTION`/`REFLECTION_SHIFTED` (инициализация RVA `0x13CF0`, render RVA `0x4EFF0`); Vulkan-проход отражения пока отсутствует.
|
`terrain_face8_pipeline_unverified` теперь считает только исходные грани с `0x8` без `0x20000`; сама семантика оставшихся специальных проходов ещё не установлена.
|
||||||
|
|
||||||
## FXID field-level semantics
|
## FXID field-level semantics
|
||||||
|
|
||||||
|
|||||||
@@ -1138,6 +1138,36 @@ lightmap state передаются в `VulkanStaticMaterial`, а shader пос
|
|||||||
сэмплирует эти четыре стадии. Animation clock обновляет выбранные фазы и
|
сэмплирует эти четыре стадии. Animation clock обновляет выбранные фазы и
|
||||||
коэффициенты без пересборки terrain mesh, сохраняя native type-14 overlay mask.
|
коэффициенты без пересборки 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
|
## Реализация Vulkan
|
||||||
|
|
||||||
`LegacyPipelineState` хранит blend, depth, cull и alpha-test. Из этих значений
|
`LegacyPipelineState` хранит blend, depth, cull и alpha-test. Из этих значений
|
||||||
|
|||||||
Reference in new issue
Block a user