From c77c078efee9ece70c3f82793ed002f82a95eb85 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sun, 18 Oct 2026 07:28:16 +0400 Subject: [PATCH] fix: preserve native sky sprite screen depth --- .../shaders/triangle.vert | 9 +- .../shaders/triangle.vert.spv | Bin 18580 -> 19136 bytes .../fparkan-render-vulkan/src/ffi/smoke.rs | 15 +- .../src/ffi/smoke_types.rs | 38 +++++ .../fparkan-render-vulkan/src/ffi/tests.rs | 2 +- apps/fparkan-game/src/main.rs | 156 ++++++++++++++++-- docs/reference/environment-effects.md | 29 ++++ 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 9574e1c5e16bb68a0ecff330a8e57bebd80e973d..535bde0dde838809f991c2721ebb52f2fc046d28 100644 GIT binary patch delta 5411 zcmZveOK@CA6^48Cn2|J^@i?-iST9R*Ah8{YO>BsrU@VegC)n5~FNefTuBEX&jx-}i z<5!4%!!zIjW_)cHYzQkTD5y$##f}ZdhJs>66)RY<;IR)-82G4AqkO+GF~@>UbXzJ+*0H9}O(qM)@#G(c_L@(ns8npD zY0%K6bj)-I4i@M4;IvKDsxP6kudRL+!2hH(UM6C+rb(+f9_Lk%>x+#_rB-gPcn!r{ zztAk7tuL3)MvKk5_K+wPbhTDqtFNn2>NPQaZd}}2)QDD7-+s4EINc-F@v5p9KMsNX zYW2oy)E*<}9ulWwPySFG9YyBDk3K7(hmy>uk$8_ZmUnr5t+IGwB~mi!$RCmJy^;Ie z-Y)#nF8r}B{74smYy-}^Lp?4@HjPHlY~cIcb6xm_E_|vBzu1LOZ@@VhC4;5OX)XxX zJYBmDGtIf>WT7;fIiH*^ZOhb>tEKU+H)OrMQa({>JYQ)vE0^1=mUTChpOwa^(Q3m} zi+Z)ze5U%EU)P*0{;XIxlDqo9b!Y45zhvUxwBF?7j%Z;iBfjnJuERmYanqO`K5zp8 z*Xubh1GB>iI~~*x$E^gnB@W1}C+`h7?j{VU0*(RUW&>`}bN2<@kmnu?~+wT9;<^MQHeNyzjZXD|hgp0^)NF@yOX09GL%5Yg*6v-(Rj zM&_eZ`A86s1mWmYE_r8cBy;1=ug1pvvomg9XE!NMT})=DhT~Zc@?RNpDmc=Qddy1z zw@bLHo@$+lk+kUt#IdsIha@wBeSy9pPLoLEzpL2nob2P2_`3+UPF)AHb>JBV zWNWTVrZK>7Vg02%34>XAI7{eB0T#jVQ^MRehK~vtC~6~)i{RK2@UI$~tzt%o*{Tq; zRUu}pz$AD@vi{s#YE#1B5n#1$tIbGamzYd;xBf#(!|}W734#FwgIk_MpCp-?UGaHY z@T-tT#tNL3m0>o32iq|HJptxI;6XP1Zxo9M*)V#Z%@Ff!dK^0pcsD}>-pvs6ZW^ZN z-89UAhXPDI-cQqmc|SwU``N~a`_cq>Ktlr_P{Z^*poRxEV66+UB@5dw#S1z#6m*Xo zMbEx{Tx-g9^?H6-2;3)(2@PFMb{vj=$>VirtB1E(aA7QB5)1nY$&80>=@khOHv37* zl=4sfOs6EnF;iwUBbkrcpqo}(RU=_!+s--3Z0A7o!?}^pCU5s_P8v(+K5_8o`2lZp zEU@{YIQSEq0LyW|(Hiw--IdswpV6{q;&o>$*f?h`Sq#^Q6N!N{CuW`)d2COFBwt$_O_bG@<$ z48qNIKp6hGZgmEIR5ILw z>X`zWk|CK9JGGS$mlk%mYzvaL{;|8opuXNdi9ulWc-zSRAh3Bt9J~ekl(%_Oy~Q~! z8Q#|ah-6~pi_UzX=1@@uPl+_=PfLC}Fghj~o1=lvXC%X0pcCHacwlohjB(sGzziJf0OTzG0 z1TRa5%e<6U!HW{1tO`Ca*+PU>P!^AlVNa_ETNQ9WPz6kZR7sK4h>a~ihty+7s=OR$ zB&#ywPQg;3iy~3r5UB^7Lq)P2I&;JRtS~(GOX|U9e@-%k+JV5nDh!YPvid4jU=FW} zz`+&|hZSLX9L}o;b7jt`=QdmQI+FdJD#a4{qkL@;^KiXU23u7!=v6WL+{9bW+R8z1@@y$7kFH&ndqSchPf;| z3B%j$*~+gAvz0$q9Ppcx;eMnf!QYUK-JLxUSBpDvf wZwbSj&9}YHO<#~;Y*+z{NzWvAuw*yA0|64rx71o{%!pM1-@J46{_(m013^}00RR91 delta 4820 zcmZ9OPjH*X8OB$(EZdTm*l}VJ|4ZV8KpJTI6DR>fNfU@m6XHM`2n2*I$&RQj8CiCm zl$q~dPVPrI(q0QQ44Is`!HF5BGaZg_V1@$+h7QNlGC=$MKJ6n>V&T9gZ>%O>@0n zTxu>CmrCV!v*p}?erQ)~6jz&TwRWx95R-O4iCZnTN-GXc+NBg{rCF&83J_`U38UteGRNp_Cv+YZX+CTj#8 zkgif|Rm%*eR4*AqMvE+$`0^NK!pGGOJNLw_U9kOKa8QYH7Jz-1K9J8zU2md+ZkX z6?4BmGVw62$K|e6+of9F+B1gEa_;Q$T@~*5;%`Ti70c!)lPCpwnj6(ty>zu3#-iNT z>r)sX<>h=5Mxw*1fYgAjIc3gb2gi4Zq13eLiyw3c<3ro0qC4aXGxksqKHP)v>cMyS;CpYu8TWv&ExI4LdQZFg zj_!r-QF-c?hG}=Y2cPM|Pxs(wZoz}@LOe4%mAV)o9i5utNOOd3AeGwE()wCe0~vC~ z__fhJsdBtETG+iI-EzISP;FhPw%XOr&LU>qM*PueA+-^IrdVk7i|$0dwtBM0Aud;w zY}ts%#->u6H}4+%Dix)Zn`2hew8|;{<;DQ>opSxjp(510211tb?eA1wj$MNzhUt*P zaZrux_uPnakyJE7IBKLgZVEWgjWyRLpq2LY#Tvm1jd1pZ8 zWW(JRaEuo2o`B1H?nuCG^V~xL$CX0cM+1(D!ac73yEEZ(fjp^Vv)kdh`GDh=L3cXf zSS7fHfZOG{^8q*Ex#zoFa4o^Tu0n$H45Cw&Bq-NLzwCMWu=y-U4AY*^G$bsV0XjJlk4=n6 z2h|jcF0l9E4)C08KK#7G48i=o$Y4?wKSvo%dV+bW!K5Z<%8n{ ze3!}abY>{|&CezZ0|y^b)bBJ4-hEu)Z)9_N^{d|IxafbHx)zU44@GY-Y!DFiGu-vS!p;6 z@UQ^WHT;+`=g;tQVa}f6lfs-k;(e`=uroG5Fgw$){<{LUCKRwWU}jvAtzY+s%Czvh z0JFcYGAoPyU`BBGD73qxni%*Q2zb+T_^-vc&mN9Ak^JjLQlNTOHUD~rm}k}SKcwY3 zHB5UR!VvQihM0#i#5{x^M~(ozgeIWoB{WQjyo83)^8$vL2Qb7ufFb4q3^5O22N%+b z!3$`DVRcxJukX1M9n*Sni~UawDg2n$leGdL)gfWU*%}TgJbqFi?l^v;s*um*F!5+J zWvSa2n&Z#?!Lt;9SsnMi|~SvtIK-pxGl1-ZcBXX0K`+=$vf$ zC>ezYv0s4TG{%XLytq@gW@ zC^^=@5`p8at}r^iM>ZUVPm?3xl7O5e{KvI?Z;884wgnsyR^fi}_!tCd4SdvP-66F( zBKuy|cd-AAoDnlpf+MC8HvmFg#s_7y9dD|kE#t3+;Z5_9*Zeim92Ey|nn%2b3-?&0 zW#)K1DuBbb5=cw9ETX?`mk&2bgeoRAG~nkQtViB9T|iJp{= z!v=9mHo<9(4;|a{JK2^K^RmI_`@L+-iKk?PS|w(%8&~~; zFudi&IoWU&zAz`A7Kh(*;#t`iFU*Pa;_)#cuEX=}za_;VM35BBh>es|8;%IZ5VO$EqW)T{ioK|2J*QYTPMa70Nw)7eup>)P*Gh~ ziRHHe^P(?#-g0DFnEW}%{*(QeML_b2Ot_lo*k|Kj^&HX#J!a&|9|dTS9{&+>TZ)0N zE==cWm`GVQ90TD!Sz-TEBJ6=_$X-$5PJdTHcto1Q@XUxu#32;sIpHwzg7#uSMU%mPFLxp|+ E2Xyh#8~^|S 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) -> Result [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, не разрешает материалы