feat(render): animate water with native EMBM waves
Docs Deploy / Build and Deploy MkDocs (push) Successful in 41s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 41s
This commit is contained in:
15 files changed
+380
-32
No files matched your search
@@ -12,7 +12,8 @@
|
|||||||
Рельеф переключает исходные LOD0/LOD1 по ячейкам относительно текущей камеры;
|
Рельеф переключает исходные LOD0/LOD1 по ячейкам относительно текущей камеры;
|
||||||
под основаниями строений сохраняется подробная сетка.
|
под основаниями строений сохраняется подробная сетка.
|
||||||
Горизонтальная вода использует плоское отражение ближайшей активной высоты;
|
Горизонтальная вода использует плоское отражение ближайшей активной высоты;
|
||||||
волны EMBM и shifted-камера оригинала пока не воспроизводятся.
|
волны используют карту Du/Dv и часы оригинала, а shifted-камера отражения
|
||||||
|
пока заменена обычным зеркалом.
|
||||||
|
|
||||||
## Запуск
|
## Запуск
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ 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 = 9) in vec3 in_world_position;
|
||||||
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;
|
||||||
@@ -17,6 +17,7 @@ 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(set = 1, binding = 0) uniform sampler2D reflection_color;
|
||||||
|
layout(set = 1, binding = 1) uniform sampler2D embm_bump;
|
||||||
|
|
||||||
layout(std140, set = 0, binding = 4) uniform FrameConstants {
|
layout(std140, set = 0, binding = 4) uniform FrameConstants {
|
||||||
mat4 clip_from_world;
|
mat4 clip_from_world;
|
||||||
@@ -30,6 +31,8 @@ layout(std140, set = 0, binding = 4) uniform FrameConstants {
|
|||||||
vec4 fog_color;
|
vec4 fog_color;
|
||||||
vec4 fog_distances;
|
vec4 fog_distances;
|
||||||
vec4 camera_position;
|
vec4 camera_position;
|
||||||
|
vec4 embm_bounds;
|
||||||
|
vec4 embm_wave;
|
||||||
} frame;
|
} frame;
|
||||||
|
|
||||||
layout(push_constant) uniform MaterialConstants {
|
layout(push_constant) uniform MaterialConstants {
|
||||||
@@ -69,11 +72,23 @@ layout(push_constant) uniform MaterialConstants {
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
int mode = int(round(material.material_mode));
|
int mode = int(round(material.material_mode));
|
||||||
if ((mode & 32) != 0 && (mode & 4) == 0 && in_world_z < frame.camera_position.w) {
|
if ((mode & 32) != 0 && (mode & 4) == 0 && in_world_position.z < frame.camera_position.w) {
|
||||||
discard;
|
discard;
|
||||||
}
|
}
|
||||||
if ((mode & 64) != 0) {
|
if ((mode & 64) != 0) {
|
||||||
vec2 reflection_uv = gl_FragCoord.xy / vec2(textureSize(reflection_color, 0));
|
vec2 reflection_uv = gl_FragCoord.xy / vec2(textureSize(reflection_color, 0));
|
||||||
|
vec2 span = frame.embm_bounds.zw - frame.embm_bounds.xy;
|
||||||
|
vec2 plane_uv = vec2(1.0) - (in_world_position.yx - frame.embm_bounds.yx) / span.yx;
|
||||||
|
vec2 bump_uv = plane_uv * frame.embm_wave.y + frame.embm_wave.x;
|
||||||
|
vec2 duv = max(vec2(-1.0), (texture(embm_bump, bump_uv).rg * 255.0 - 128.0) / 127.0);
|
||||||
|
vec2 displacement = duv * frame.embm_wave.zw;
|
||||||
|
// shortcut: convert native map-UV displacement to the current mirror projection;
|
||||||
|
// replace this projection when accepting the original shifted reflection camera.
|
||||||
|
vec3 displaced_world = in_world_position - vec3(displacement.yx * span, 0.0);
|
||||||
|
vec4 displaced_clip = frame.clip_from_world * vec4(displaced_world, 1.0);
|
||||||
|
if (displaced_clip.w > 0.000001) {
|
||||||
|
reflection_uv = displaced_clip.xy / displaced_clip.w * 0.5 + 0.5;
|
||||||
|
}
|
||||||
vec3 rgb = texture(reflection_color, reflection_uv).rgb * in_base_diffuse + in_base_specular;
|
vec3 rgb = texture(reflection_color, reflection_uv).rgb * in_base_diffuse + in_base_specular;
|
||||||
out_color = vec4(mix(rgb * in_color, frame.fog_color.rgb, in_fog_factor), material.diffuse_alpha);
|
out_color = vec4(mix(rgb * in_color, frame.fog_color.rgb, in_fog_factor), material.diffuse_alpha);
|
||||||
if (out_color.a < material.alpha_cutoff) {
|
if (out_color.a < material.alpha_cutoff) {
|
||||||
|
|||||||
Binary file not shown.
@@ -19,7 +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(location = 9) out vec3 out_world_position;
|
||||||
|
|
||||||
layout(std140, set = 0, binding = 4) uniform FrameConstants {
|
layout(std140, set = 0, binding = 4) uniform FrameConstants {
|
||||||
mat4 clip_from_world;
|
mat4 clip_from_world;
|
||||||
@@ -33,6 +33,8 @@ layout(std140, set = 0, binding = 4) uniform FrameConstants {
|
|||||||
vec4 fog_color;
|
vec4 fog_color;
|
||||||
vec4 fog_distances;
|
vec4 fog_distances;
|
||||||
vec4 camera_position;
|
vec4 camera_position;
|
||||||
|
vec4 embm_bounds;
|
||||||
|
vec4 embm_wave;
|
||||||
} frame;
|
} frame;
|
||||||
|
|
||||||
layout(std430, set = 0, binding = 5) readonly buffer MaterialSpecular {
|
layout(std430, set = 0, binding = 5) readonly buffer MaterialSpecular {
|
||||||
@@ -242,7 +244,7 @@ LightingTerms native_material_lighting(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
out_world_z = in_position.z;
|
out_world_position = in_position;
|
||||||
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.
@@ -36,14 +36,42 @@ pub(super) fn mesh_reflection_planes(mesh: &super::VulkanStaticMesh) -> Vec<Opti
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn mesh_reflection_bounds(mesh: &super::VulkanStaticMesh) -> Option<[f32; 4]> {
|
||||||
|
let mut bounds = [
|
||||||
|
f32::INFINITY,
|
||||||
|
f32::INFINITY,
|
||||||
|
f32::NEG_INFINITY,
|
||||||
|
f32::NEG_INFINITY,
|
||||||
|
];
|
||||||
|
for range in mesh
|
||||||
|
.draw_ranges
|
||||||
|
.iter()
|
||||||
|
.filter(|range| range.terrain_reflection)
|
||||||
|
{
|
||||||
|
for &index in &mesh.indices
|
||||||
|
[range.first_index as usize..range.first_index as usize + range.index_count as usize]
|
||||||
|
{
|
||||||
|
let position = mesh.vertices[index as usize].position;
|
||||||
|
bounds[0] = bounds[0].min(position[0]);
|
||||||
|
bounds[1] = bounds[1].min(position[1]);
|
||||||
|
bounds[2] = bounds[2].max(position[0]);
|
||||||
|
bounds[3] = bounds[3].max(position[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(bounds.iter().all(|v| v.is_finite()) && bounds[2] > bounds[0] && bounds[3] > bounds[1])
|
||||||
|
.then_some(bounds)
|
||||||
|
}
|
||||||
|
|
||||||
/// Owned before pipeline creation; a dummy set prevents attachment feedback in the mirror pass.
|
/// Owned before pipeline creation; a dummy set prevents attachment feedback in the mirror pass.
|
||||||
pub(super) struct ReflectionResources {
|
pub(super) struct ReflectionResources {
|
||||||
device: ash::Device,
|
device: ash::Device,
|
||||||
pub layout: vk::DescriptorSetLayout,
|
pub layout: vk::DescriptorSetLayout,
|
||||||
pool: vk::DescriptorPool,
|
pool: vk::DescriptorPool,
|
||||||
sampler: vk::Sampler,
|
sampler: vk::Sampler,
|
||||||
|
bump_sampler: vk::Sampler,
|
||||||
pub sets: [vk::DescriptorSet; 2],
|
pub sets: [vk::DescriptorSet; 2],
|
||||||
dummy: Option<VulkanAllocatedImage>,
|
dummy: Option<VulkanAllocatedImage>,
|
||||||
|
bump: Option<VulkanAllocatedImage>,
|
||||||
pub color: Option<VulkanAllocatedImage>,
|
pub color: Option<VulkanAllocatedImage>,
|
||||||
depth: Option<VulkanDepthAttachment>,
|
depth: Option<VulkanDepthAttachment>,
|
||||||
pub framebuffer: vk::Framebuffer,
|
pub framebuffer: vk::Framebuffer,
|
||||||
@@ -54,25 +82,31 @@ impl ReflectionResources {
|
|||||||
instance: &VulkanInstanceProbe,
|
instance: &VulkanInstanceProbe,
|
||||||
device: &VulkanLogicalDeviceProbe,
|
device: &VulkanLogicalDeviceProbe,
|
||||||
command_pool: vk::CommandPool,
|
command_pool: vk::CommandPool,
|
||||||
|
enabled: bool,
|
||||||
) -> Result<Self, VulkanSmokeRendererError> {
|
) -> Result<Self, VulkanSmokeRendererError> {
|
||||||
let mut owned = Self {
|
let mut owned = Self {
|
||||||
device: device.device().clone(),
|
device: device.device().clone(),
|
||||||
layout: vk::DescriptorSetLayout::null(),
|
layout: vk::DescriptorSetLayout::null(),
|
||||||
pool: vk::DescriptorPool::null(),
|
pool: vk::DescriptorPool::null(),
|
||||||
sampler: vk::Sampler::null(),
|
sampler: vk::Sampler::null(),
|
||||||
|
bump_sampler: vk::Sampler::null(),
|
||||||
sets: [vk::DescriptorSet::null(); 2],
|
sets: [vk::DescriptorSet::null(); 2],
|
||||||
dummy: None,
|
dummy: None,
|
||||||
|
bump: None,
|
||||||
color: None,
|
color: None,
|
||||||
depth: None,
|
depth: None,
|
||||||
framebuffer: vk::Framebuffer::null(),
|
framebuffer: vk::Framebuffer::null(),
|
||||||
};
|
};
|
||||||
let binding = vk::DescriptorSetLayoutBinding::default()
|
let bindings: Vec<_> = (0..2)
|
||||||
.binding(0)
|
.map(|binding| {
|
||||||
|
vk::DescriptorSetLayoutBinding::default()
|
||||||
|
.binding(binding)
|
||||||
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
||||||
.descriptor_count(1)
|
.descriptor_count(1)
|
||||||
.stage_flags(vk::ShaderStageFlags::FRAGMENT);
|
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
|
||||||
let layout_info =
|
})
|
||||||
vk::DescriptorSetLayoutCreateInfo::default().bindings(std::slice::from_ref(&binding));
|
.collect();
|
||||||
|
let layout_info = vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings);
|
||||||
// SAFETY: The device is live; owned rolls back every handle on later failure.
|
// SAFETY: The device is live; owned rolls back every handle on later failure.
|
||||||
owned.layout = unsafe {
|
owned.layout = unsafe {
|
||||||
owned
|
owned
|
||||||
@@ -85,7 +119,7 @@ impl ReflectionResources {
|
|||||||
})?;
|
})?;
|
||||||
let size = vk::DescriptorPoolSize::default()
|
let size = vk::DescriptorPoolSize::default()
|
||||||
.ty(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
.ty(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
||||||
.descriptor_count(2);
|
.descriptor_count(4);
|
||||||
let pool_info = vk::DescriptorPoolCreateInfo::default()
|
let pool_info = vk::DescriptorPoolCreateInfo::default()
|
||||||
.max_sets(2)
|
.max_sets(2)
|
||||||
.pool_sizes(std::slice::from_ref(&size));
|
.pool_sizes(std::slice::from_ref(&size));
|
||||||
@@ -136,6 +170,42 @@ impl ReflectionResources {
|
|||||||
mip_levels: Vec::new(),
|
mip_levels: Vec::new(),
|
||||||
},
|
},
|
||||||
)?);
|
)?);
|
||||||
|
if enabled {
|
||||||
|
let sampler = sampler
|
||||||
|
.address_mode_u(vk::SamplerAddressMode::REPEAT)
|
||||||
|
.address_mode_v(vk::SamplerAddressMode::REPEAT)
|
||||||
|
.address_mode_w(vk::SamplerAddressMode::REPEAT);
|
||||||
|
// SAFETY: The immutable bump map uses supported linear filtering and a single mip level.
|
||||||
|
owned.bump_sampler =
|
||||||
|
unsafe { owned.device.create_sampler(&sampler, None) }.map_err(|result| {
|
||||||
|
VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkCreateSampler(EMBM)",
|
||||||
|
result,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
let rgba8 = fparkan_render::water::legacy_embm_bump_map()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|duv| {
|
||||||
|
[
|
||||||
|
(i16::from(duv[0]) + 128) as u8,
|
||||||
|
(i16::from(duv[1]) + 128) as u8,
|
||||||
|
0,
|
||||||
|
255,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
owned.bump = Some(super::create_static_texture_image(
|
||||||
|
instance,
|
||||||
|
device,
|
||||||
|
command_pool,
|
||||||
|
&super::VulkanStaticTexture {
|
||||||
|
width: fparkan_render::water::EMBM_BUMP_SIZE,
|
||||||
|
height: fparkan_render::water::EMBM_BUMP_SIZE,
|
||||||
|
rgba8,
|
||||||
|
mip_levels: Vec::new(),
|
||||||
|
},
|
||||||
|
)?);
|
||||||
|
}
|
||||||
for set in owned.sets {
|
for set in owned.sets {
|
||||||
owned.write_image(set, owned.dummy.as_ref().expect("dummy image exists").view);
|
owned.write_image(set, owned.dummy.as_ref().expect("dummy image exists").view);
|
||||||
}
|
}
|
||||||
@@ -255,13 +325,35 @@ impl ReflectionResources {
|
|||||||
.sampler(self.sampler)
|
.sampler(self.sampler)
|
||||||
.image_view(view)
|
.image_view(view)
|
||||||
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
|
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
|
||||||
let write = vk::WriteDescriptorSet::default()
|
let bump = self
|
||||||
|
.bump
|
||||||
|
.as_ref()
|
||||||
|
.or(self.dummy.as_ref())
|
||||||
|
.expect("dummy image exists");
|
||||||
|
let images = [
|
||||||
|
image,
|
||||||
|
vk::DescriptorImageInfo::default()
|
||||||
|
.sampler(if self.bump.is_some() {
|
||||||
|
self.bump_sampler
|
||||||
|
} else {
|
||||||
|
self.sampler
|
||||||
|
})
|
||||||
|
.image_view(bump.view)
|
||||||
|
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL),
|
||||||
|
];
|
||||||
|
let writes: Vec<_> = images
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(binding, image)| {
|
||||||
|
vk::WriteDescriptorSet::default()
|
||||||
.dst_set(set)
|
.dst_set(set)
|
||||||
.dst_binding(0)
|
.dst_binding(binding as u32)
|
||||||
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
||||||
.image_info(std::slice::from_ref(&image));
|
.image_info(std::slice::from_ref(image))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
// SAFETY: Sets are initialized before submission; the dummy remains valid for every pass.
|
// SAFETY: Sets are initialized before submission; the dummy remains valid for every pass.
|
||||||
unsafe { self.device.update_descriptor_sets(&[write], &[]) };
|
unsafe { self.device.update_descriptor_sets(&writes, &[]) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,12 +365,14 @@ impl Drop for ReflectionResources {
|
|||||||
self.device.destroy_framebuffer(self.framebuffer, None);
|
self.device.destroy_framebuffer(self.framebuffer, None);
|
||||||
self.device.destroy_descriptor_pool(self.pool, None);
|
self.device.destroy_descriptor_pool(self.pool, None);
|
||||||
self.device.destroy_sampler(self.sampler, None);
|
self.device.destroy_sampler(self.sampler, None);
|
||||||
|
self.device.destroy_sampler(self.bump_sampler, None);
|
||||||
self.device.destroy_descriptor_set_layout(self.layout, None);
|
self.device.destroy_descriptor_set_layout(self.layout, None);
|
||||||
for image in self
|
for image in self
|
||||||
.color
|
.color
|
||||||
.iter()
|
.iter()
|
||||||
.chain(self.depth.iter().map(|depth| &depth.image))
|
.chain(self.depth.iter().map(|depth| &depth.image))
|
||||||
.chain(self.dummy.iter())
|
.chain(self.dummy.iter())
|
||||||
|
.chain(self.bump.iter())
|
||||||
{
|
{
|
||||||
self.device.destroy_image_view(image.view, None);
|
self.device.destroy_image_view(image.view, None);
|
||||||
self.device.destroy_image(image.image, None);
|
self.device.destroy_image(image.image, None);
|
||||||
@@ -320,12 +414,19 @@ mod tests {
|
|||||||
alpha_test_reference: 1,
|
alpha_test_reference: 1,
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
mesh.vertices[1].position = [10.0, 0.0, 150.0];
|
||||||
|
mesh.vertices[2].position = [5.0, 20.0, 150.0];
|
||||||
|
let mut unused = mesh.vertices[0];
|
||||||
|
unused.position = [-10_000.0, -10_000.0, 150.0];
|
||||||
|
mesh.vertices.push(unused);
|
||||||
|
assert_eq!(mesh_reflection_bounds(&mesh), Some([0.0, 0.0, 10.0, 20.0]));
|
||||||
assert_eq!(mesh_reflection_planes(&mesh), vec![Some(150.0)]);
|
assert_eq!(mesh_reflection_planes(&mesh), vec![Some(150.0)]);
|
||||||
mesh.vertices[2].position[2] = 151.0;
|
mesh.vertices[2].position[2] = 151.0;
|
||||||
assert_eq!(mesh_reflection_planes(&mesh), vec![None]);
|
assert_eq!(mesh_reflection_planes(&mesh), vec![None]);
|
||||||
mesh.vertices[2].position[2] = 150.0;
|
mesh.vertices[2].position[2] = 150.0;
|
||||||
mesh.draw_ranges[0].terrain_reflection = false;
|
mesh.draw_ranges[0].terrain_reflection = false;
|
||||||
assert_eq!(mesh_reflection_planes(&mesh), vec![None]);
|
assert_eq!(mesh_reflection_planes(&mesh), vec![None]);
|
||||||
|
assert_eq!(mesh_reflection_bounds(&mesh), None);
|
||||||
for cull in [
|
for cull in [
|
||||||
fparkan_render::LegacyCullMode::Disabled,
|
fparkan_render::LegacyCullMode::Disabled,
|
||||||
fparkan_render::LegacyCullMode::BackFace,
|
fparkan_render::LegacyCullMode::BackFace,
|
||||||
|
|||||||
@@ -373,12 +373,17 @@ impl VulkanSmokeRenderer {
|
|||||||
plane_heights.sort_unstable();
|
plane_heights.sort_unstable();
|
||||||
plane_heights.dedup();
|
plane_heights.dedup();
|
||||||
if !plane_heights.is_empty() {
|
if !plane_heights.is_empty() {
|
||||||
eprintln!("render_coverage planar_reflection_embm_unsupported={} (flat reflection; native shifted camera and wave distortion pending)", plane_heights.len());
|
eprintln!("render_coverage planar_reflection_shifted_camera_unsupported={} (native bump map and clock; using a projective mirror until shifted-camera mapping is implemented)", plane_heights.len());
|
||||||
eprintln!("render_coverage planar_reflection_other_planes_unsupported={} (nearest active plane is reflected)", plane_heights.len().saturating_sub(1));
|
eprintln!("render_coverage planar_reflection_other_planes_unsupported={} (nearest active plane is reflected)", plane_heights.len().saturating_sub(1));
|
||||||
}
|
}
|
||||||
if unsupported_nonplanar != 0 {
|
if unsupported_nonplanar != 0 {
|
||||||
eprintln!("render_coverage planar_reflection_nonplanar_unsupported={unsupported_nonplanar} (ordinary terrain fallback)");
|
eprintln!("render_coverage planar_reflection_nonplanar_unsupported={unsupported_nonplanar} (ordinary terrain fallback)");
|
||||||
}
|
}
|
||||||
|
let mut frame_uniforms = super::VulkanFrameUniforms::from_camera(create_info.camera);
|
||||||
|
if let Some(bounds) = super::reflection::mesh_reflection_bounds(&create_info.mesh) {
|
||||||
|
frame_uniforms.embm_bounds = bounds;
|
||||||
|
eprintln!("render_reflection embm_bounds={bounds:?}");
|
||||||
|
}
|
||||||
let mut renderer = Self {
|
let mut renderer = Self {
|
||||||
instance: Some(instance),
|
instance: Some(instance),
|
||||||
validation,
|
validation,
|
||||||
@@ -424,7 +429,7 @@ impl VulkanSmokeRenderer {
|
|||||||
material_specular_dirty: true,
|
material_specular_dirty: true,
|
||||||
readback_enabled: false,
|
readback_enabled: false,
|
||||||
camera: create_info.camera,
|
camera: create_info.camera,
|
||||||
frame_uniforms: super::VulkanFrameUniforms::from_camera(create_info.camera),
|
frame_uniforms,
|
||||||
point_lights: Vec::new(),
|
point_lights: Vec::new(),
|
||||||
point_lights_explicit: false,
|
point_lights_explicit: false,
|
||||||
frame_sync: Vec::new(),
|
frame_sync: Vec::new(),
|
||||||
@@ -1571,8 +1576,8 @@ impl VulkanSmokeRenderer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
let mut bytes = frame.to_ne_bytes();
|
let mut bytes = frame.to_ne_bytes();
|
||||||
let length = bytes.len();
|
let offset = super::smoke_types::REFLECTION_CLIP_HEIGHT_OFFSET;
|
||||||
bytes[length - 4..].copy_from_slice(&height.to_ne_bytes());
|
bytes[offset..offset + 4].copy_from_slice(&height.to_ne_bytes());
|
||||||
Ok(bytes)
|
Ok(bytes)
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|||||||
@@ -192,7 +192,8 @@ impl Default for VulkanPointLight {
|
|||||||
/// every device. Four directional lights and one point light fit in a small
|
/// every device. Four directional lights and one point light fit in a small
|
||||||
/// dynamic UBO while the push-constant range remains available for material
|
/// dynamic UBO while the push-constant range remains available for material
|
||||||
/// state.
|
/// state.
|
||||||
pub const VULKAN_FRAME_UNIFORM_BYTES: usize = 368;
|
pub const VULKAN_FRAME_UNIFORM_BYTES: usize = 400;
|
||||||
|
pub(super) const REFLECTION_CLIP_HEIGHT_OFFSET: usize = 364;
|
||||||
|
|
||||||
/// Number of CPU/GPU frame slots shared by the swapchain renderer.
|
/// Number of CPU/GPU frame slots shared by the swapchain renderer.
|
||||||
pub const VULKAN_FRAME_SLOT_COUNT: usize = 2;
|
pub const VULKAN_FRAME_SLOT_COUNT: usize = 2;
|
||||||
@@ -235,6 +236,10 @@ pub struct VulkanFrameUniforms {
|
|||||||
pub fog_end: f32,
|
pub fog_end: f32,
|
||||||
/// World-space camera position used for fog distance.
|
/// World-space camera position used for fog distance.
|
||||||
pub camera_position: [f32; 3],
|
pub camera_position: [f32; 3],
|
||||||
|
/// XY bounds of the original reflective faces: min X/Y, max X/Y.
|
||||||
|
pub embm_bounds: [f32; 4],
|
||||||
|
/// Native wave scroll fraction in the ten-second cycle.
|
||||||
|
pub embm_phase: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VulkanFrameUniforms {
|
impl VulkanFrameUniforms {
|
||||||
@@ -258,6 +263,8 @@ impl VulkanFrameUniforms {
|
|||||||
fog_start: f32::MAX,
|
fog_start: f32::MAX,
|
||||||
fog_end: f32::MAX,
|
fog_end: f32::MAX,
|
||||||
camera_position: [0.0; 3],
|
camera_position: [0.0; 3],
|
||||||
|
embm_bounds: [0.0, 0.0, 1.0, 1.0],
|
||||||
|
embm_phase: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +290,14 @@ impl VulkanFrameUniforms {
|
|||||||
.chain(self.fog_color)
|
.chain(self.fog_color)
|
||||||
.chain([self.fog_start, self.fog_end])
|
.chain([self.fog_start, self.fog_end])
|
||||||
.chain(self.camera_position)
|
.chain(self.camera_position)
|
||||||
|
.chain(self.embm_bounds)
|
||||||
|
.chain([self.embm_phase])
|
||||||
.all(f32::is_finite)
|
.all(f32::is_finite)
|
||||||
|
&& self.embm_bounds[2] > self.embm_bounds[0]
|
||||||
|
&& self.embm_bounds[3] > self.embm_bounds[1]
|
||||||
|
&& (self.embm_bounds[2] - self.embm_bounds[0]).is_finite()
|
||||||
|
&& (self.embm_bounds[3] - self.embm_bounds[1]).is_finite()
|
||||||
|
&& (0.0..1.0).contains(&self.embm_phase)
|
||||||
&& (self.fog_end <= self.fog_start
|
&& (self.fog_end <= self.fog_start
|
||||||
|| ((self.fog_start * self.fog_start).is_finite()
|
|| ((self.fog_start * self.fog_start).is_finite()
|
||||||
&& (self.fog_end * self.fog_end).is_finite()))
|
&& (self.fog_end * self.fog_end).is_finite()))
|
||||||
@@ -364,6 +378,13 @@ impl VulkanFrameUniforms {
|
|||||||
self.camera_position[2],
|
self.camera_position[2],
|
||||||
0.0,
|
0.0,
|
||||||
]);
|
]);
|
||||||
|
values[92..96].copy_from_slice(&self.embm_bounds);
|
||||||
|
values[96..100].copy_from_slice(&[
|
||||||
|
self.embm_phase,
|
||||||
|
fparkan_render::water::EMBM_BUMP_TILE,
|
||||||
|
fparkan_render::water::EMBM_COEFFICIENTS[0],
|
||||||
|
fparkan_render::water::EMBM_COEFFICIENTS[1],
|
||||||
|
]);
|
||||||
let mut bytes = [0_u8; VULKAN_FRAME_UNIFORM_BYTES];
|
let mut bytes = [0_u8; VULKAN_FRAME_UNIFORM_BYTES];
|
||||||
for (index, value) in values.into_iter().enumerate() {
|
for (index, value) in values.into_iter().enumerate() {
|
||||||
let offset = index * std::mem::size_of::<f32>();
|
let offset = index * std::mem::size_of::<f32>();
|
||||||
@@ -862,6 +883,8 @@ mod static_mesh_tests {
|
|||||||
fog_start: 13.0,
|
fog_start: 13.0,
|
||||||
fog_end: 14.0,
|
fog_end: 14.0,
|
||||||
camera_position: [15.0, 16.0, 17.0],
|
camera_position: [15.0, 16.0, 17.0],
|
||||||
|
embm_bounds: [100.0, 200.0, 300.0, 600.0],
|
||||||
|
embm_phase: 0.25,
|
||||||
};
|
};
|
||||||
let bytes = frame.to_ne_bytes();
|
let bytes = frame.to_ne_bytes();
|
||||||
assert_eq!(bytes.len(), VULKAN_FRAME_UNIFORM_BYTES);
|
assert_eq!(bytes.len(), VULKAN_FRAME_UNIFORM_BYTES);
|
||||||
@@ -880,6 +903,28 @@ mod static_mesh_tests {
|
|||||||
assert_eq!(read(320), 10.0);
|
assert_eq!(read(320), 10.0);
|
||||||
assert_eq!(read(336), 13.0);
|
assert_eq!(read(336), 13.0);
|
||||||
assert_eq!(read(352), 15.0);
|
assert_eq!(read(352), 15.0);
|
||||||
|
assert_eq!(read(368), 100.0);
|
||||||
|
assert_eq!(read(380), 600.0);
|
||||||
|
assert_eq!(read(384), 0.25);
|
||||||
|
assert_eq!(read(388), 100.0);
|
||||||
|
assert_eq!(read(392), 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn embm_uniforms_reject_invalid_bounds_and_phase() {
|
||||||
|
for (bounds, phase) in [
|
||||||
|
([0.0; 4], 0.0),
|
||||||
|
([-f32::MAX, 0.0, f32::MAX, 1.0], 0.0),
|
||||||
|
([0.0, 0.0, 1.0, 1.0], f32::NAN),
|
||||||
|
([0.0, 0.0, 1.0, 1.0], 1.0),
|
||||||
|
] {
|
||||||
|
assert!(!VulkanFrameUniforms {
|
||||||
|
embm_bounds: bounds,
|
||||||
|
embm_phase: phase,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.is_finite());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -249,8 +249,12 @@ 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 =
|
partial.reflection = match super::reflection::ReflectionResources::new(
|
||||||
match super::reflection::ReflectionResources::new(instance, device, command_pool) {
|
instance,
|
||||||
|
device,
|
||||||
|
command_pool,
|
||||||
|
reflection_enabled,
|
||||||
|
) {
|
||||||
Ok(reflection) => Some(reflection),
|
Ok(reflection) => Some(reflection),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
destroy_partial_swapchain_resources(device, command_pool, partial);
|
destroy_partial_swapchain_resources(device, command_pool, partial);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ pub(super) struct LegacyCameraCapture {
|
|||||||
pub(super) struct SkyUvClock {
|
pub(super) struct SkyUvClock {
|
||||||
pub(super) verified: bool,
|
pub(super) verified: bool,
|
||||||
pub(super) elapsed_milliseconds: u32,
|
pub(super) elapsed_milliseconds: u32,
|
||||||
|
pub(super) raw_clock: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||||
|
|||||||
@@ -4638,6 +4638,7 @@ fn multiply_row_major(left: [f32; 16], right: [f32; 16]) -> [f32; 16] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct StaticPreviewScene {
|
struct StaticPreviewScene {
|
||||||
|
fixed_water_clock_ms: Option<u32>,
|
||||||
mesh: VulkanStaticMesh,
|
mesh: VulkanStaticMesh,
|
||||||
dynamic_vertex_start: usize,
|
dynamic_vertex_start: usize,
|
||||||
terrain_lod: Option<terrain_lod::TerrainLodDraws>,
|
terrain_lod: Option<terrain_lod::TerrainLodDraws>,
|
||||||
@@ -8554,7 +8555,23 @@ fn static_preview_mesh_and_materials(
|
|||||||
};
|
};
|
||||||
let camera = view_camera.vulkan_camera();
|
let camera = view_camera.vulkan_camera();
|
||||||
let shadow_material_index = append_shadow_material(&mut materials)?;
|
let shadow_material_index = append_shadow_material(&mut materials)?;
|
||||||
|
let fixed_water_clock_ms = legacy_camera.as_ref().and_then(|capture| {
|
||||||
|
capture
|
||||||
|
.sky_uv_clock
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|clock| clock.raw_clock)
|
||||||
|
});
|
||||||
|
if legacy_camera.is_some()
|
||||||
|
&& fixed_water_clock_ms.is_none()
|
||||||
|
&& mesh
|
||||||
|
.draw_ranges
|
||||||
|
.iter()
|
||||||
|
.any(|range| range.terrain_reflection)
|
||||||
|
{
|
||||||
|
eprintln!("render_coverage captured_water_phase_unverified=1 (capture has no verified raw queue clock; using replay elapsed time)");
|
||||||
|
}
|
||||||
Ok(StaticPreviewScene {
|
Ok(StaticPreviewScene {
|
||||||
|
fixed_water_clock_ms,
|
||||||
mesh,
|
mesh,
|
||||||
dynamic_vertex_start,
|
dynamic_vertex_start,
|
||||||
terrain_lod,
|
terrain_lod,
|
||||||
@@ -9369,6 +9386,7 @@ fn run_static_vulkan_mode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct StaticVulkanApp {
|
struct StaticVulkanApp {
|
||||||
|
fixed_water_clock_ms: Option<u32>,
|
||||||
mesh: VulkanStaticMesh,
|
mesh: VulkanStaticMesh,
|
||||||
dynamic_vertex_start: usize,
|
dynamic_vertex_start: usize,
|
||||||
terrain_lod: Option<terrain_lod::TerrainLodDraws>,
|
terrain_lod: Option<terrain_lod::TerrainLodDraws>,
|
||||||
@@ -9497,6 +9515,7 @@ impl StaticVulkanApp {
|
|||||||
dynamic_shadow_casters: preview.dynamic_shadow_casters,
|
dynamic_shadow_casters: preview.dynamic_shadow_casters,
|
||||||
sun_occlusion_range_indices: preview.sun_occlusion_range_indices,
|
sun_occlusion_range_indices: preview.sun_occlusion_range_indices,
|
||||||
terrain: preview.terrain,
|
terrain: preview.terrain,
|
||||||
|
fixed_water_clock_ms: preview.fixed_water_clock_ms,
|
||||||
world_range_indices,
|
world_range_indices,
|
||||||
world_range_index_counts,
|
world_range_index_counts,
|
||||||
camera: preview.camera,
|
camera: preview.camera,
|
||||||
@@ -10333,6 +10352,21 @@ impl StaticVulkanApp {
|
|||||||
&mut self.material_animations,
|
&mut self.material_animations,
|
||||||
renderer,
|
renderer,
|
||||||
)?;
|
)?;
|
||||||
|
let water_clock_ms = self
|
||||||
|
.fixed_water_clock_ms
|
||||||
|
.map_or(frame.clock.time_ms, |captured| {
|
||||||
|
if self.target_frames == 0 {
|
||||||
|
captured.wrapping_add(frame.clock.time_ms)
|
||||||
|
} else {
|
||||||
|
captured
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mut uniforms = renderer.frame_uniforms();
|
||||||
|
uniforms.camera_position = frame.camera.position;
|
||||||
|
uniforms.embm_phase = fparkan_render::water::legacy_embm_phase(water_clock_ms);
|
||||||
|
renderer
|
||||||
|
.set_frame_uniforms(uniforms)
|
||||||
|
.map_err(|error| format!("update water clock uniforms: {error}"))?;
|
||||||
for update in &frame.effect_updates {
|
for update in &frame.effect_updates {
|
||||||
renderer
|
renderer
|
||||||
.set_draw_range_index_count(update.range_index, update.index_count)
|
.set_draw_range_index_count(update.range_index, update.index_count)
|
||||||
@@ -13664,6 +13698,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let view_camera = free_camera.preview_frame(16.0 / 9.0);
|
let view_camera = free_camera.preview_frame(16.0 / 9.0);
|
||||||
let preview = StaticPreviewScene {
|
let preview = StaticPreviewScene {
|
||||||
|
fixed_water_clock_ms: None,
|
||||||
terrain_lod: None,
|
terrain_lod: None,
|
||||||
dynamic_vertex_start: 3,
|
dynamic_vertex_start: 3,
|
||||||
mesh: VulkanStaticMesh {
|
mesh: VulkanStaticMesh {
|
||||||
@@ -15730,13 +15765,18 @@ mod tests {
|
|||||||
let mut value: serde_json::Value =
|
let mut value: serde_json::Value =
|
||||||
serde_json::from_slice(include_bytes!("../testdata/legacy-camera-parented.json"))
|
serde_json::from_slice(include_bytes!("../testdata/legacy-camera-parented.json"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
value["sky_uv_clock"] =
|
value["sky_uv_clock"] = serde_json::json!({"verified": true, "elapsed_milliseconds": 4051271258_u32, "raw_clock": 272342176_u32});
|
||||||
serde_json::json!({"verified": true, "elapsed_milliseconds": 4051271258_u32});
|
|
||||||
let capture = parse_legacy_camera_capture(&serde_json::to_vec(&value).unwrap()).unwrap();
|
let capture = parse_legacy_camera_capture(&serde_json::to_vec(&value).unwrap()).unwrap();
|
||||||
assert_eq!(
|
let clock = capture.sky_uv_clock.unwrap();
|
||||||
capture.sky_uv_clock.unwrap().elapsed_milliseconds,
|
assert_eq!(clock.elapsed_milliseconds, 4051271258);
|
||||||
4051271258
|
assert_eq!(clock.raw_clock, Some(272342176));
|
||||||
);
|
value["sky_uv_clock"]
|
||||||
|
.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.remove("raw_clock");
|
||||||
|
let old_capture =
|
||||||
|
parse_legacy_camera_capture(&serde_json::to_vec(&value).unwrap()).unwrap();
|
||||||
|
assert_eq!(old_capture.sky_uv_clock.unwrap().raw_clock, None);
|
||||||
value["sky_uv_clock"]["verified"] = serde_json::json!(false);
|
value["sky_uv_clock"]["verified"] = serde_json::json!(false);
|
||||||
assert!(parse_legacy_camera_capture(&serde_json::to_vec(&value).unwrap()).is_err());
|
assert!(parse_legacy_camera_capture(&serde_json::to_vec(&value).unwrap()).is_err());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
//! Camera mathematics and graphics pipeline state.
|
//! Camera mathematics and graphics pipeline state.
|
||||||
|
|
||||||
|
pub mod water;
|
||||||
|
|
||||||
/// A 64-byte transform block returned by the original Terrain camera ABI.
|
/// A 64-byte transform block returned by the original Terrain camera ABI.
|
||||||
///
|
///
|
||||||
/// The original uses two selector-dependent transform pointers. Their exact
|
/// The original uses two selector-dependent transform pointers. Their exact
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
//! The GOG Terrain EMBM wave map and its world-space animation clock.
|
||||||
|
|
||||||
|
/// Side length of the native signed Du/Dv texture.
|
||||||
|
pub const EMBM_BUMP_SIZE: u32 = 128;
|
||||||
|
/// Native default wave tiling across the bounding rectangle of reflective faces.
|
||||||
|
pub const EMBM_BUMP_TILE: f32 = 100.0;
|
||||||
|
/// Native diagonal bump matrix, without luminance modulation.
|
||||||
|
pub const EMBM_COEFFICIENTS: [f32; 2] = [0.01, 0.01];
|
||||||
|
|
||||||
|
/// Generates the signed bytes written by Terrain.dll RVA 0x2BF50 with CW 0x007F.
|
||||||
|
#[must_use]
|
||||||
|
pub fn legacy_embm_bump_map() -> Vec<[i8; 2]> {
|
||||||
|
let mut texels = Vec::with_capacity((EMBM_BUMP_SIZE * EMBM_BUMP_SIZE) as usize);
|
||||||
|
for y in 0..EMBM_BUMP_SIZE {
|
||||||
|
for x in 0..EMBM_BUMP_SIZE {
|
||||||
|
// The native constant is slightly smaller than mathematical 4*pi.
|
||||||
|
let angle = ((y as f32 / EMBM_BUMP_SIZE as f32 - 0.5)
|
||||||
|
+ (x as f32 / EMBM_BUMP_SIZE as f32 - 0.5))
|
||||||
|
* f32::from_bits(0x4149_0fd0);
|
||||||
|
texels.push([
|
||||||
|
(angle.cos() * 64.0).round_ties_even() as i8,
|
||||||
|
(angle.sin() * 64.0).round_ties_even() as i8,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
texels
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queue clock modulo the native ten-second wave scroll period.
|
||||||
|
#[must_use]
|
||||||
|
pub fn legacy_embm_phase(clock_ms: u32) -> f32 {
|
||||||
|
(clock_ms % 10_000) as f32 / 10_000.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Native reflection coordinates: U reverses world Y and V reverses world X.
|
||||||
|
#[must_use]
|
||||||
|
pub fn legacy_embm_uv(world_xy: [f32; 2], bounds: [f32; 4]) -> Option<[f32; 2]> {
|
||||||
|
let span = [bounds[2] - bounds[0], bounds[3] - bounds[1]];
|
||||||
|
if !world_xy
|
||||||
|
.into_iter()
|
||||||
|
.chain(bounds)
|
||||||
|
.chain(span)
|
||||||
|
.all(f32::is_finite)
|
||||||
|
|| span.iter().any(|&value| value <= 0.0)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some([
|
||||||
|
1.0 - (world_xy[1] - bounds[1]) / span[1],
|
||||||
|
1.0 - (world_xy[0] - bounds[0]) / span[0],
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wave_bytes_match_native_generation_and_clock_wraps_without_float_time_loss() {
|
||||||
|
let texels = legacy_embm_bump_map();
|
||||||
|
assert_eq!(texels.len(), 128 * 128);
|
||||||
|
assert_eq!(
|
||||||
|
&texels[..16],
|
||||||
|
&[
|
||||||
|
[64, 0],
|
||||||
|
[64, 6],
|
||||||
|
[63, 12],
|
||||||
|
[61, 19],
|
||||||
|
[59, 24],
|
||||||
|
[56, 30],
|
||||||
|
[53, 36],
|
||||||
|
[49, 41],
|
||||||
|
[45, 45],
|
||||||
|
[41, 49],
|
||||||
|
[36, 53],
|
||||||
|
[30, 56],
|
||||||
|
[24, 59],
|
||||||
|
[19, 61],
|
||||||
|
[12, 63],
|
||||||
|
[6, 64],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(texels[16], [0, 64]);
|
||||||
|
assert_eq!(texels[32], [-64, 0]);
|
||||||
|
assert_eq!(texels[48], [0, -64]);
|
||||||
|
let native_hash = texels
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.fold(0xcbf29ce484222325u64, |hash, &byte| {
|
||||||
|
(hash ^ u64::from(byte as u8)).wrapping_mul(0x100000001b3)
|
||||||
|
});
|
||||||
|
assert_eq!(native_hash, 0x0b92dc6a9e482625);
|
||||||
|
assert_eq!(legacy_embm_phase(0), 0.0);
|
||||||
|
assert_eq!(legacy_embm_phase(5_000), 0.5);
|
||||||
|
assert_eq!(legacy_embm_phase(10_000), 0.0);
|
||||||
|
assert_eq!(legacy_embm_phase(u32::MAX), 0.7295);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapping_uses_the_reflective_rectangle_and_keeps_axis_order() {
|
||||||
|
let bounds = [100.0, 200.0, 300.0, 600.0];
|
||||||
|
assert_eq!(legacy_embm_uv([100.0, 200.0], bounds), Some([1.0, 1.0]));
|
||||||
|
assert_eq!(legacy_embm_uv([300.0, 600.0], bounds), Some([0.0, 0.0]));
|
||||||
|
assert_eq!(legacy_embm_uv([150.0, 500.0], bounds), Some([0.25, 0.75]));
|
||||||
|
assert_eq!(legacy_embm_uv([0.0; 2], [1.0; 4]), None);
|
||||||
|
assert_eq!(legacy_embm_uv([f32::NAN, 0.0], bounds), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,7 +73,8 @@ MSH-запрос со свободной preview-камерой; это не п
|
|||||||
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
|
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
|
||||||
log на минимальных сценах с одним типом материала.
|
log на минимальных сценах с одним типом материала.
|
||||||
|
|
||||||
Горизонтальные грани Land.msh с флагом `0x20000` используют плоское Vulkan-отражение; shifted-камера, волны и native EMBM combiner остаются заглушкой `planar_reflection_embm_unsupported`.
|
Горизонтальные грани Land.msh с флагом `0x20000` используют Vulkan-отражение и исходную карту волн Du/Dv; перенос её смещений в обычное зеркало приближённый: `planar_reflection_shifted_camera_unsupported`.
|
||||||
|
Raw queue clock задаёт фазу волн отдельно от времени суток и sky elapsed; старые захваты без него считаются в `captured_water_phase_unverified`.
|
||||||
Один target отражает ближайшую активную высоту воды; остальные высоты и неплоские грани считаются в `planar_reflection_other_planes_unsupported` и `planar_reflection_nonplanar_unsupported` и сохраняют обычный terrain shader.
|
Один target отражает ближайшую активную высоту воды; остальные высоты и неплоские грани считаются в `planar_reflection_other_planes_unsupported` и `planar_reflection_nonplanar_unsupported` и сохраняют обычный terrain shader.
|
||||||
`terrain_face8_pipeline_unverified` теперь считает только исходные грани с `0x8` без `0x20000`; сама семантика оставшихся специальных проходов ещё не установлена.
|
`terrain_face8_pipeline_unverified` теперь считает только исходные грани с `0x8` без `0x20000`; сама семантика оставшихся специальных проходов ещё не установлена.
|
||||||
|
|
||||||
|
|||||||
+26
-3
@@ -1161,9 +1161,32 @@ lightmap state передаются в `VulkanStaticMaterial`, а shader пос
|
|||||||
записи отражения привязан отдельный нейтральный descriptor, чтобы проход
|
записи отражения привязан отдельный нейтральный descriptor, чтобы проход
|
||||||
не читал собственное вложение.
|
не читал собственное вложение.
|
||||||
|
|
||||||
Такое зеркало ещё не повторяет shifted-камеру и EMBM оригинала: искажение
|
Волны не требуют готовой текстуры из ресурсов: Terrain.dll RVA `0x2BF50`
|
||||||
отражения волнами пока отсутствует и учитывается в
|
создаёт карту 128×128 со знаковыми парами Du/Dv. Для нормированных координат
|
||||||
`planar_reflection_embm_unsupported`. Один target обслуживает ближайшую
|
текселя `x` и `y` угол равен `(x + y − 1) × 12,566360473632812`, а компоненты —
|
||||||
|
округлённые `64 cos(угол)` и `64 sin(угол)`. Все 32 768 байт Rust-генератора
|
||||||
|
сверены с исполнением этой функции DLL при игровом x87 CW `0x007F`.
|
||||||
|
|
||||||
|
Рябь привязана к водоёму, поэтому не плывёт вслед за экраном. Конструктор
|
||||||
|
ландшафта собирает прямоугольник всех отражающих граней; U меняется против
|
||||||
|
мировой Y, V — против X. Эти координаты повторяются сто раз, к обеим прибавляется
|
||||||
|
`(queue_clock % 10000) / 10000`. Это самостоятельный clock: время суток и
|
||||||
|
относительный clock неба не задают фазу волн. Захват хранит исходный
|
||||||
|
`sky_uv_clock.raw_clock`; ограниченный replay фиксирует его, интерактивный
|
||||||
|
продолжает с этого значения. Старые захваты без raw clock явно считаются в
|
||||||
|
`captured_water_phase_unverified`.
|
||||||
|
|
||||||
|
Карта Du/Dv смещает координаты следующей текстуры с диагональными коэффициентами
|
||||||
|
`0,01`, без изменения яркости. Это правило
|
||||||
|
[Direct3D BUMPENVMAP](https://learn.microsoft.com/en-us/windows/win32/direct3d9/bump-mapping-formulas).
|
||||||
|
В Vulkan карта хранится в двух каналах несжатой текстуры и возвращается к
|
||||||
|
знаковым значениям перед вычислением смещения.
|
||||||
|
|
||||||
|
Обычное зеркало ещё не повторяет shifted-камеру оригинала: смещения Du/Dv
|
||||||
|
сейчас переводятся из координат водоёма в текущую зеркальную проекцию.
|
||||||
|
Это приближение учитывается в `planar_reflection_shifted_camera_unsupported`;
|
||||||
|
совпадение генератора волн не доказывает совпадения итогового отражения.
|
||||||
|
Один target обслуживает ближайшую
|
||||||
активную высоту воды; другие высоты и негоризонтальные поверхности явно
|
активную высоту воды; другие высоты и негоризонтальные поверхности явно
|
||||||
считаются как неподдержанные. Это промежуточный водный проход, а не полная
|
считаются как неподдержанные. Это промежуточный водный проход, а не полная
|
||||||
приёмка воды или лавы.
|
приёмка воды или лавы.
|
||||||
|
|||||||
Reference in new issue
Block a user