From 26370126f92241ccfb76899634bb52a8cb44b6f1 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sun, 18 Oct 2026 12:18:30 +0400 Subject: [PATCH] fix(render): honor terrain base-only face flags --- .../shaders/triangle.frag | 3 +- .../shaders/triangle.frag.spv | Bin 10388 -> 10632 bytes .../fparkan-render-vulkan/src/asset_mesh.rs | 63 ++++++++++++------ .../fparkan-render-vulkan/src/ffi/smoke.rs | 19 +++++- .../src/ffi/smoke_types.rs | 6 ++ apps/fparkan-game/src/main.rs | 4 ++ docs/tomes/04-world.md | 24 +++++++ 7 files changed, 96 insertions(+), 23 deletions(-) diff --git a/adapters/fparkan-render-vulkan/shaders/triangle.frag b/adapters/fparkan-render-vulkan/shaders/triangle.frag index 89896cf..6a6e752 100644 --- a/adapters/fparkan-render-vulkan/shaders/triangle.frag +++ b/adapters/fparkan-render-vulkan/shaders/triangle.frag @@ -114,7 +114,8 @@ void main() { 0.0, 1.0 ); - vec3 base_rgb = 2.0 * base.rgb * detail.rgb; + bool terrain_base_only = (mode & 16) != 0; + vec3 base_rgb = terrain_base_only ? base.rgb : 2.0 * base.rgb * detail.rgb; vec3 overlay_rgb = 2.0 * overlay.rgb * overlay_detail_sample.rgb; vec3 base_lit_rgb = base_rgb * in_base_diffuse + in_base_specular; vec3 overlay_lit_rgb = overlay_rgb * in_overlay_diffuse + in_overlay_specular; diff --git a/adapters/fparkan-render-vulkan/shaders/triangle.frag.spv b/adapters/fparkan-render-vulkan/shaders/triangle.frag.spv index 8b867bb70b7c098418f9252e0a896c8b9a066a89..7b7c3da115cd10e93af5e22f0f5962ab72c73075 100644 GIT binary patch delta 2361 zcmZ9NU2jxX6o&V-oz4_yfOf0{j=)F-KcFb$2a3g3)T*eo;#Wn}v^q$W3ZbpWUY!^r zG3rfoGMShG%$=H^omuQmpFTCe zj8^97I%}payfX{Tv>Mw_zu#GypIM$(E{ERGNLQ-OEf$DL-kv*g;=RQV-b&b<{#0$1 zTIrwF=2nXn6BER+EUumQ_qBEo5ivivl;U@w%Sr5bMWM88+vfAnUy=R|g zw`I+RFdllcca-;Ha!bmYM#kH#XEtmvPqlGf&gF!4H{2~fg!7CZgr5hPUMX@%SmdTJ z^7iV5MzfTd+R<9ot!G&sXOaKChCAA#zp;k5)^JBvjCWKyAIJ(0|1X5I>6yVs|42dk zjPj$a=LXN$k~;7zwut@8h~?LEZGiPVG>7z8u7AUND0F9Us5ZhI>c4^c)|c^RQvqMY zr~VJtP5CRKE5q)YLMxTRY&Z@$%1Ql*wTmFc%%EAGE>;UVGW}OcqQhh+jmgU3Vy<4e zS~>k~M{~FWQt>m6JF4Kf$QrBn!PTm1|4=hMIaHg9b=RR5>ssF|_Pic!Z^rsD%VUK) z_++8N2Dn;1>uDoMrP$LZ_`8K2{cyES_LK~O+@)$gkBwUFoHGUQ>Mn#!tP?Qb><+kEv|4_m?nL7^?8QuKB4aJ;CDI>@+znQX zd^h+@_Qe<5lOPzR!vx;CJ(wH3H+#W;fqt9cnS0=B-Wl)3ePAE&g}!~*BbY~791HIU zn|q8rzYu>v_-EGSNB$V(<#_;6M?Ad`g4Hx17w?YoPV6Cs3#@Ts2jFVa;=~?C<2Rd_ zjhNS1UN{N&0u1mn_%?R#8^q2Jg6-Vt7%MNvx+lgy3O2@Azpmu^oa7;JagvW2SK1%K zMVyZEaReVnsdEf_7PBF-qw`ztaC&K1BY6VlaIS7deG*(_y@F9luoiU0rr delta 2155 zcmZ9M$xl;Z6ve*>v=pT%)>;)65GNdP9#Bynuns6VY>bPh)~Zb^5vwu3Q=_<}Q^+iiNS# z&7yO4E)nsSh6?2ymm-Eqf1>_S%zbsvUCfUbb0b$S3c8p+f3#k|SsW?n#{)U$;x4Z8 zfv;Q|`|pjrgnuiMSxSSYpN(fuVe zc$?puZb?5>)=iNe_}1!!^s~nNdBN|B3yJ|f_d8mDgc>@RPP!V!BTbO~p9(z8p+}9A zV@gp_SdL$zHs!asX1afiaSC;xs|{`&%}!I|s<@r@C`tHUN2XFEgc7Y+D}F6n5j{ihkZLNk`%^=P7$6S zn)&AlQ`mU3I4_4aIZqf5x_Rb%75#iASdp|a#`y}0@JgWBfs8PPjV~0&1Ly5sB>Y9| zpcr?)SRBowF?6clZ6xc&(JX8v>!s=Ix)c;54PNf3Lo55=)ez0n7!6uuUBWbG zUU=xO1ZEDId8051F}G=j+o(1RM@O|q9PP83Vp7|MxtJ7t+Y~(t8UiyJ{s49e^Q)g= z;3Vhe*eQ_>OmoC{2{&r}tT0cvG<2RVXLbv74&6L^gkNaSJbT6Qp>tDPwW2YB?G*vK zqf6^QhHlOE3eRlrfHZU~d{CHk=vEjlD6Eyb6-H*A9}=cJ%=~A<+2f~j zSeVCQ-acVGr^87c5yr!1S~k>>O* TerrainFace28 { + terrain_face_with_tag_and_flags(material_tag, 0, vertices) + } + + fn terrain_face_with_tag_and_flags( + material_tag: u16, + flags: u32, + vertices: [u16; 3], + ) -> TerrainFace28 { TerrainFace28 { - flags: FullSurfaceMask(0), + flags: FullSurfaceMask(flags), material_tag, aux_tag: 0, vertices, @@ -2201,7 +2218,7 @@ mod tests { } #[test] - fn terrain_ranges_preserve_face_order_and_packed_material_tags() { + fn terrain_ranges_merge_only_contiguous_equal_material_and_surface_mode() { let mut terrain = LandMeshDocument { streams: Vec::new(), nodes_raw: Vec::new(), @@ -2216,27 +2233,35 @@ mod tests { aux14: Vec::new(), aux18: Vec::new(), faces: vec![ - terrain_face_with_tag(0xff01, [0, 1, 2]), - terrain_face_with_tag(0xff01, [0, 1, 2]), + terrain_face_with_tag_and_flags(0xff00, 0x400, [0, 1, 2]), + terrain_face_with_tag_and_flags(0xff00, 0x400, [0, 1, 2]), + terrain_face_with_tag_and_flags(0xff00, 0, [0, 1, 2]), terrain_face_with_tag(0x0102, [0, 1, 2]), - terrain_face_with_tag(0xff01, [0, 1, 2]), + terrain_face_with_tag_and_flags(0xff00, 0x400, [0, 1, 2]), ], }; - set_terrain_lod_slots(&mut terrain, &[[0, u16::MAX]], &[[0, 4]]); + set_terrain_lod_slots(&mut terrain, &[[0, u16::MAX]], &[[0, 5]]); let lod0 = static_terrain_lod0_assignments(&terrain).expect("terrain LOD0"); let ranges = static_terrain_draw_ranges(&terrain, &lod0).expect("representable terrain"); - assert_eq!(ranges.len(), 3); + assert_eq!(ranges.len(), 4); assert_eq!(ranges[0].first_index, 0); assert_eq!(ranges[0].index_count, 6); - assert_eq!(ranges[0].material_index, 0xff01); + assert_eq!(ranges[0].material_index, 0xff00); + assert!(!ranges[0].terrain_base_only); assert_eq!(ranges[1].first_index, 6); assert_eq!(ranges[1].index_count, 3); - assert_eq!(ranges[1].material_index, 0x0102); + assert_eq!(ranges[1].material_index, 0xff00); + assert!(ranges[1].terrain_base_only); assert_eq!(ranges[2].first_index, 9); assert_eq!(ranges[2].index_count, 3); - assert_eq!(ranges[2].material_index, 0xff01); + assert_eq!(ranges[2].material_index, 0x0102); + assert!(ranges[2].terrain_base_only); + assert_eq!(ranges[3].first_index, 12); + assert_eq!(ranges[3].index_count, 3); + assert_eq!(ranges[3].material_index, 0xff00); + assert!(!ranges[3].terrain_base_only); assert!(ranges.iter().all(|range| { range.alpha_test_reference == VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE })); diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index 4ad1c1b..30a42dd 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -1902,18 +1902,23 @@ impl VulkanSmokeRenderer { 60, &material_uv_transforms, ); - let unlit = self.material_unlit.get(texture_index).ok_or( + let material_mode = self.material_unlit.get(texture_index).ok_or( VulkanSmokeRendererError::InvariantViolation { context: "static material unlit flag", }, )?; - let unlit = [unlit.to_ne_bytes()]; + let material_mode = if range.terrain_base_only { + ((*material_mode).round() as i32 | 16) as f32 + } else { + *material_mode + }; + 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, - &unlit.concat(), + &material_mode.concat(), ); device.device().cmd_draw_indexed( command_buffer, @@ -2577,6 +2582,7 @@ mod tests { lightmap_index: 4, batch_flags: 0x108, pipeline_state: base_state, + terrain_base_only: false, alpha_test_reference: 17, }, VulkanStaticDrawRange { @@ -2586,6 +2592,7 @@ mod tests { lightmap_index: u8::MAX, batch_flags: 0, pipeline_state: LegacyPipelineState::default(), + terrain_base_only: false, alpha_test_reference: 0, }, ]; @@ -2769,6 +2776,7 @@ mod tests { lightmap_index: u8::MAX, batch_flags: 0, pipeline_state: fparkan_render::LegacyPipelineState::default(), + terrain_base_only: false, alpha_test_reference: 0, }) .collect::>(); @@ -2792,6 +2800,7 @@ mod tests { lightmap_index: u8::MAX, batch_flags: 0, pipeline_state: fparkan_render::LegacyPipelineState::default(), + terrain_base_only: false, alpha_test_reference: 0, }, super::super::VulkanStaticDrawRange { @@ -2801,6 +2810,7 @@ mod tests { lightmap_index: u8::MAX, batch_flags: 0, pipeline_state: fparkan_render::LegacyPipelineState::default(), + terrain_base_only: false, alpha_test_reference: 0, }, super::super::VulkanStaticDrawRange { @@ -2810,6 +2820,7 @@ mod tests { lightmap_index: u8::MAX, batch_flags: 0, pipeline_state: fparkan_render::LegacyPipelineState::default(), + terrain_base_only: false, alpha_test_reference: 0, }, ]; @@ -2833,6 +2844,7 @@ mod tests { lightmap_index: u8::MAX, batch_flags: 0, pipeline_state: fparkan_render::LegacyPipelineState::default(), + terrain_base_only: false, alpha_test_reference: 0, }) .collect::>(); @@ -2861,6 +2873,7 @@ mod tests { lightmap_index: u8::MAX, batch_flags: 0, pipeline_state: LegacyPipelineState::default(), + terrain_base_only: false, alpha_test_reference: 0, }) .collect::>(); diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index ac31e9a..51f8384 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -422,6 +422,8 @@ pub struct VulkanStaticDrawRange { /// Bits `0x100` and `0x008` opt a range into that queue independently of /// the material blend category. pub batch_flags: u16, + /// Terrain ranges that use the native lit base-only path (face flag 0x400 clear). + pub terrain_base_only: bool, /// Backend-neutral fixed-function state for this source range. pub pipeline_state: LegacyPipelineState, /// Alpha-test reference in legacy 0..=255 units; dynamic material data. @@ -754,6 +756,7 @@ impl VulkanStaticMesh { material_index: 0, lightmap_index: u8::MAX, batch_flags: 0, + terrain_base_only: false, pipeline_state: LegacyPipelineState::default(), alpha_test_reference: 0, }], @@ -989,6 +992,7 @@ mod static_mesh_tests { material_index: 0, lightmap_index: u8::MAX, batch_flags: 0, + terrain_base_only: false, pipeline_state: LegacyPipelineState::default(), alpha_test_reference: 0, }], @@ -1019,6 +1023,7 @@ mod static_mesh_tests { material_index: 7, lightmap_index: u8::MAX, batch_flags: 0, + terrain_base_only: false, pipeline_state: LegacyPipelineState::default(), alpha_test_reference: 0, }, @@ -1028,6 +1033,7 @@ mod static_mesh_tests { material_index: 2, lightmap_index: u8::MAX, batch_flags: 0, + terrain_base_only: false, pipeline_state: LegacyPipelineState::default(), alpha_test_reference: 0, }, diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs index 3b3969e..6aeaaec 100644 --- a/apps/fparkan-game/src/main.rs +++ b/apps/fparkan-game/src/main.rs @@ -1944,6 +1944,7 @@ fn append_environment_range( lightmap_index: u8::MAX, batch_flags: 0, pipeline_state, + terrain_base_only: false, alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE, }); Ok(EnvironmentGpuRange { @@ -6347,6 +6348,7 @@ mod tests { LegacyBlendMode::Opaque, LegacyDepthMode::TestReadOnly, ), + terrain_base_only: false, alpha_test_reference: 0, }; let mut mesh = VulkanStaticMesh { @@ -6405,6 +6407,7 @@ mod tests { lightmap_index: u8::MAX, batch_flags, pipeline_state: world_pipeline_state(blend, LegacyDepthMode::TestReadOnly), + terrain_base_only: false, alpha_test_reference: 0, }; @@ -7645,6 +7648,7 @@ mod tests { lightmap_index: 0, batch_flags, pipeline_state: LegacyPipelineState::default(), + terrain_base_only: false, alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE, }; let mesh = VulkanStaticMesh { diff --git a/docs/tomes/04-world.md b/docs/tomes/04-world.md index f9c0e67..e486e18 100644 --- a/docs/tomes/04-world.md +++ b/docs/tomes/04-world.md @@ -430,6 +430,30 @@ full 00000040 -> compact 2000 full 00200000 -> compact 8000 ``` +Бит `0x400` в маске грани рельефа управляет detail-текстурой. В `Terrain.dll` +метод `CShade` по смещению `+0x10` таблицы `0x10065108` ведёт к `0x10028A50`: +он читает `flags` по смещению `+0x3C` и проверяет бит в `0x10029191`. Ветка для +очищенного бита обнуляет указатель detail-текстуры в `0x10029427`. Затем +`Ngi32` применяет режим 1 или 7: для RGB stage 0 умножает текстуру на diffuse- +цвет, а stage 1 отключён. Alpha режима 1 берётся из texture, режима 7 — из +diffuse; на проверенных непрозрачных полосах основания это различие не меняет +цвет фрагмента. Текущий код сохраняет выбор в каждой полосе отрисовки рельефа: + +```rust +let terrain_base_only = first_face.flags.0 & 0x400 == 0; +``` + +Фрагментный шейдер пропускает умножение на detail-текстуру: + +```glsl +bool terrain_base_only = (mode & 16) != 0; +vec3 base_rgb = terrain_base_only ? base.rgb : 2.0 * base.rgb * detail.rgb; +``` + +В исходных картах бит установлен у всех 275 882 граней рельефа. У полос +основания, которые создаются процедурно при загрузке миссии, бит очищен; +именно поэтому им нужен этот вариант без detail-текстуры. + Для шестибитного material-поля используются full-биты `0x100`, `0x8000`, `0x10000`, `0x40000`, `0x80000` и `0x80`; они переходят соответственно в compact-биты `1`, `2`, `4`, `8`, `0x10`, `0x20`.