aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-18 06:28:16 +0300
committerValentin Popov <valentin@popov.link>2026-10-18 06:28:16 +0300
commitc77c078efee9ece70c3f82793ed002f82a95eb85 (patch)
tree2e12bd0f82dcf2a1b728b3a00e8477414d6fb90f
parentd97c8d40bfbae097435f10e46ecad5431504c729 (diff)
downloadfparkan-c77c078efee9ece70c3f82793ed002f82a95eb85.tar.xz
fparkan-c77c078efee9ece70c3f82793ed002f82a95eb85.zip
fix: preserve native sky sprite screen depth
-rw-r--r--adapters/fparkan-render-vulkan/shaders/triangle.vert9
-rw-r--r--adapters/fparkan-render-vulkan/shaders/triangle.vert.spvbin18580 -> 19136 bytes
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke.rs15
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs38
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/tests.rs2
-rw-r--r--apps/fparkan-game/src/main.rs156
-rw-r--r--docs/reference/environment-effects.md29
7 files changed, 221 insertions, 28 deletions
diff --git a/adapters/fparkan-render-vulkan/shaders/triangle.vert b/adapters/fparkan-render-vulkan/shaders/triangle.vert
index 201c31a..83a1751 100644
--- a/adapters/fparkan-render-vulkan/shaders/triangle.vert
+++ b/adapters/fparkan-render-vulkan/shaders/triangle.vert
@@ -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;
diff --git a/adapters/fparkan-render-vulkan/shaders/triangle.vert.spv b/adapters/fparkan-render-vulkan/shaders/triangle.vert.spv
index 9574e1c..535bde0 100644
--- a/adapters/fparkan-render-vulkan/shaders/triangle.vert.spv
+++ b/adapters/fparkan-render-vulkan/shaders/triangle.vert.spv
Binary files differ
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
index c9de5ba..ac359c2 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
@@ -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() {
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs
index f628e23..7a8837d 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs
@@ -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])
diff --git a/adapters/fparkan-render-vulkan/src/ffi/tests.rs b/adapters/fparkan-render-vulkan/src/ffi/tests.rs
index b8a3d71..4ed61f7 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/tests.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/tests.rs
@@ -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()"));
diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs
index 438e345..9b5ea36 100644
--- a/apps/fparkan-game/src/main.rs
+++ b/apps/fparkan-game/src/main.rs
@@ -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);
+ }
}
diff --git a/docs/reference/environment-effects.md b/docs/reference/environment-effects.md
index 8045d68..fdc29f0 100644
--- a/docs/reference/environment-effects.md
+++ b/docs/reference/environment-effects.md
@@ -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, не разрешает материалы