fix: preserve native sky sprite screen depth
Docs Deploy / Build and Deploy MkDocs (push) Successful in 33s

This commit is contained in:
Valentin Popov committed 2026-10-18 07:28:16 +04:00
1 parent d97c8d40bf
commit c77c078efe
7 files changed
+221 -28

No files matched your search

@@ -232,9 +232,10 @@ void main() {
out_uv = in_uv;
out_detail_uv = in_detail_uv;
out_overlay_alpha = in_overlay_alpha;
out_fog_factor = native_fog_factor(in_position);
int mode = int(round(material.material_mode));
int combiner_mode = mode & 3;
bool screen_tl = (mode & 8) != 0;
out_fog_factor = screen_tl ? 0.0 : native_fog_factor(in_position);
if (combiner_mode == 0) {
LightingTerms base_terms = native_material_lighting(
in_position,
@@ -262,12 +263,14 @@ void main() {
out_overlay_diffuse = vec3(1.0);
out_overlay_specular = vec3(0.0);
}
vec4 clip_position = frame.clip_from_world * vec4(in_position, 1.0);
vec4 clip_position = screen_tl
? vec4(in_position, 1.0)
: frame.clip_from_world * vec4(in_position, 1.0);
// Sky far-depth is a Vulkan projection choice for layers authored around
// the camera. Keep world positions, normals, and UVs intact while placing
// the resulting primitive at the far depth; this does not assert native
// D3D depth-bit equivalence.
if ((mode & 4) != 0) {
if ((mode & 4) != 0 && !screen_tl) {
clip_position.z = clip_position.w;
}
gl_Position = clip_position;
@@ -3,7 +3,7 @@
use ash::vk;
use fparkan_render::{LegacyDepthMode, LegacyPipelineState};
use super::smoke_types::resolve_draw_texture_indices;
use super::smoke_types::{resolve_draw_texture_indices, VulkanStaticMaterial};
use super::update_material_texture_descriptor;
use super::{
create_command_pool, create_frame_sync, create_index_buffer, create_static_mesh_index_buffer,
@@ -361,18 +361,7 @@ impl VulkanSmokeRenderer {
create_info
.materials
.iter()
.map(|material| {
let combiner_mode = if material.lightmap_mode {
3.0
} else if material.sky_nebula_stars {
2.0
} else if material.unlit {
1.0
} else {
0.0
};
combiner_mode + if material.sky_far_depth { 4.0 } else { 0.0 }
})
.map(VulkanStaticMaterial::encoded_material_mode)
.collect()
};
let material_specular = if create_info.materials.is_empty() {
@@ -505,6 +505,33 @@ pub struct VulkanStaticMaterial {
/// renderer projection choice; it is not a claim about the original D3D
/// depth-bit encoding.
pub sky_far_depth: bool,
/// Push material mode bit 8 and treat positions as Vulkan clip-space
/// `XYZRHW` vertices. Their x/y coordinates are converted from viewport
/// pixels by the caller and z retains the native `0.999` depth.
pub screen_tl: bool,
}
impl VulkanStaticMaterial {
pub(crate) fn encoded_material_mode(&self) -> f32 {
let combiner_mode = if self.screen_tl {
1.0
} else if self.lightmap_mode {
3.0
} else if self.sky_nebula_stars {
2.0
} else if self.unlit {
1.0
} else {
0.0
};
combiner_mode
+ if self.sky_far_depth && !self.screen_tl {
4.0
} else {
0.0
}
+ if self.screen_tl { 8.0 } else { 0.0 }
}
}
/// One authored RGBA8 mip level accepted by the Vulkan texture upload path.
@@ -1028,6 +1055,7 @@ mod static_mesh_tests {
sky_nebula_stars: false,
lightmap_mode: false,
sky_far_depth: false,
screen_tl: false,
},
VulkanStaticMaterial {
material_index: 7,
@@ -1054,9 +1082,19 @@ mod static_mesh_tests {
sky_nebula_stars: false,
lightmap_mode: false,
sky_far_depth: false,
screen_tl: false,
},
];
let mut screen_tl = materials[0].clone();
screen_tl.screen_tl = true;
screen_tl.sky_far_depth = true;
assert_eq!(screen_tl.encoded_material_mode(), 9.0);
let mut far_sky = materials[0].clone();
far_sky.unlit = true;
far_sky.sky_far_depth = true;
assert_eq!(far_sky.encoded_material_mode(), 5.0);
assert_eq!(
resolve_draw_texture_indices(&ranges, &materials),
Ok(vec![1, 0])
@@ -533,7 +533,7 @@ fn native_specular_reference_uses_squared_power_and_total_overflow_curve() {
fn shader_computes_radial_fog_per_vertex_and_interpolates_it() {
let vertex = include_str!("../../shaders/triangle.vert");
let fragment = include_str!("../../shaders/triangle.frag");
assert!(vertex.contains("out_fog_factor = native_fog_factor(in_position);"));
assert!(vertex.contains("out_fog_factor = screen_tl ? 0.0 : native_fog_factor(in_position);"));
assert!(fragment.contains("layout(location = 4) in float in_fog_factor;"));
assert!(!fragment.contains("distance(in_world_position"));
assert!(!fragment.contains("float fog_factor()"));
+145 -11
View File
@@ -1261,6 +1261,7 @@ fn append_environment_material(
sky_nebula_stars: false,
lightmap_mode: false,
sky_far_depth: matches!(fallback_row, 0..=2),
screen_tl: ENVIRONMENT_SPRITE_ROWS.contains(&fallback_row),
});
phase_material_indices.push(index);
}
@@ -1321,6 +1322,7 @@ fn append_environment_gradient_material(
sky_nebula_stars: false,
lightmap_mode: false,
sky_far_depth: true,
screen_tl: false,
});
Ok(index)
}
@@ -1371,6 +1373,7 @@ fn append_shadow_material(materials: &mut Vec<VulkanStaticMaterial>) -> Result<u
sky_nebula_stars: false,
lightmap_mode: false,
sky_far_depth: false,
screen_tl: false,
});
Ok(index)
}
@@ -2648,17 +2651,33 @@ impl EnvironmentGpuScene {
viewport,
horizontal_fov(camera, viewport[0] / viewport[1].max(1.0)),
) {
vertices = Some(sprite_quad_vertices_pixels(
camera,
frame.direction,
(camera.far_plane * 0.5).max(camera.near_plane * 2.0),
half_width,
half_height,
viewport,
frame.color,
frame.alpha,
frame.uv,
));
if ENVIRONMENT_SPRITE_ROWS.contains(&sprite.row) {
if let Some(center_pixels) = projected {
vertices = Some(screen_tl_quad_vertices_pixels(
center_pixels,
half_width,
half_height,
viewport,
frame.color,
frame.alpha,
frame.uv,
));
} else {
active = false;
}
} else {
vertices = Some(sprite_quad_vertices_pixels(
camera,
frame.direction,
(camera.far_plane * 0.5).max(camera.near_plane * 2.0),
half_width,
half_height,
viewport,
frame.color,
frame.alpha,
frame.uv,
));
}
} else {
active = false;
}
@@ -3083,6 +3102,50 @@ fn sprite_quad_vertices_pixels(
]
}
fn screen_tl_quad_vertices_pixels(
center_pixels: [f32; 2],
half_width_pixels: f32,
half_height_pixels: f32,
viewport: [f32; 2],
color: [f32; 3],
alpha: f32,
uv: [[f32; 2]; 4],
) -> [VulkanStaticVertex; 4] {
let width = viewport[0].max(1.0);
let height = viewport[1].max(1.0);
let half_width = half_width_pixels.max(0.0);
let half_height = half_height_pixels.max(0.0);
let pixel_corners = [
[
center_pixels[0] - half_width,
center_pixels[1] + half_height,
],
[
center_pixels[0] + half_width,
center_pixels[1] + half_height,
],
[
center_pixels[0] - half_width,
center_pixels[1] - half_height,
],
[
center_pixels[0] + half_width,
center_pixels[1] - half_height,
],
];
// Positive-height Vulkan viewports map NDC -Y to top-down screen pixels.
let uv = [uv[1], uv[2], uv[0], uv[3]];
std::array::from_fn(|index| {
let pixel = pixel_corners[index];
let position = [
pixel[0] * (2.0 / width) - 1.0,
pixel[1] * (2.0 / height) - 1.0,
0.999,
];
environment_vertex_with_alpha(position, color, alpha, uv[index])
})
}
fn precipitation_quad_vertices(
kind: PrecipitationKind,
screen: &ScreenBillboard,
@@ -4305,6 +4368,7 @@ fn static_preview_terrain_base_materials(
sky_nebula_stars: false,
lightmap_mode: false,
sky_far_depth: false,
screen_tl: false,
});
phase_material_indices.push(usize::from(preview_selector));
}
@@ -4510,6 +4574,7 @@ fn static_preview_component_materials(
sky_nebula_stars: false,
lightmap_mode: false,
sky_far_depth: false,
screen_tl: false,
});
phase_material_indices.push(usize::from(preview_selector));
}
@@ -7336,4 +7401,73 @@ mod tests {
[uv[1], uv[2], uv[0], uv[3]]
);
}
#[test]
fn native_tl_sprite_pixels_use_current_viewport_and_preserve_depth_999() {
let uv = [[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]];
let small = screen_tl_quad_vertices_pixels(
[160.0, 120.0],
40.0,
30.0,
[640.0, 480.0],
[1.0; 3],
1.0,
uv,
);
let resized = screen_tl_quad_vertices_pixels(
[320.0, 240.0],
80.0,
60.0,
[1280.0, 960.0],
[1.0; 3],
1.0,
uv,
);
let wide = screen_tl_quad_vertices_pixels(
[640.0, 360.0],
100.0,
45.0,
[1280.0, 720.0],
[1.0; 3],
1.0,
uv,
);
for (small, resized) in small.iter().zip(resized) {
for axis in 0..2 {
assert!((small.position[axis] - resized.position[axis]).abs() < 1.0e-6);
}
assert_eq!(small.position[2], 0.999);
assert_eq!(resized.position[2], 0.999);
}
assert_eq!(small[0].uv, [0.0, 1.0]);
assert_eq!(small[2].uv, [0.0, 0.0]);
for (actual, expected) in [
(wide[0].position, [-0.15625, 0.125, 0.999]),
(wide[2].position, [-0.15625, -0.125, 0.999]),
] {
for axis in 0..3 {
assert!((actual[axis] - expected[axis]).abs() < 1.0e-6);
}
}
}
#[test]
fn native_tl_depth_999_is_nearer_than_opaque_geometry_at_320_units() {
let camera = FreeFlightCamera {
position: [0.0; 3],
yaw: 0.0,
pitch: 0.0,
horizontal_fov: 1.3,
near_plane: 0.5,
far_plane: 700.0,
move_speed: 1.0,
};
let frame = camera.preview_frame(16.0 / 9.0);
let opaque_clip = row_vector_transform([320.0, 0.0, 0.0, 1.0], frame.clip_from_world);
let opaque_depth = opaque_clip[2] / opaque_clip[3];
assert!((opaque_depth - 0.999151_18).abs() < 1.0e-6);
assert!(0.999 < opaque_depth);
}
}
+29
View File
@@ -207,6 +207,35 @@ packed: [key.colors[10], key.colors[14]],
`AtmosphereFrame` пока не несёт этот бит обновления, поэтому sampler
выдаёт интерполированный цвет при каждом запросе.
## Экранная геометрия солнца и луны
`CSun::Render` сначала переводит направление небесного тела в пиксели текущего
viewport, затем собирает quad с native XYZRHW-позицией. При положительной высоте
viewport Vulkan пиксели переводятся в clip-space так:
```rust
position = [
pixel[0] * (2.0 / width) - 1.0,
pixel[1] * (2.0 / height) - 1.0,
0.999,
];
```
Vertex shader пропускает `clip_from_world` для строк солнца и луны. CPU каждый
кадр пересчитывает центр и размер quad по текущему viewport; старые пиксельные
координаты не переносятся на новый размер. World-space billboards бликов
остаются отдельным путём. Материал экранного quad не освещается, vertex fog
отключён, а sky far-depth bit к нему не применяется.
Native `z = 0.999` не означает, что спрайт гарантированно лежит за любой
поверхностью. Vulkan использует `LESS_OR_EQUAL` и не записывает depth для
прозрачного ряда. При near `0.5`, far `700` мировая поверхность на расстоянии
`320` получает depth
`700 / (700 - 0.5) * (1 - 0.5 / 320) ≈ 0.999151`; поэтому экранный `0.999`
чуть ближе этой поверхности и проходит depth test перед ней. Это фиксированное
native значение сохраняется для совместимости, но само по себе не обещает
окклюзию солнца всей геометрией местности.
## Что остаётся за владельцем
`EnvironmentSystem` не извлекает камеру из renderer, не разрешает материалы