From e8942caa233a663155ffec14c1070ad5dbbb9c7b Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sun, 1 Nov 2026 13:40:12 +0400 Subject: [PATCH] fix(render): preserve object-local point lights on models --- .../shaders/triangle.vert | 12 ++- .../shaders/triangle.vert.spv | Bin 20384 -> 20860 bytes .../fparkan-render-vulkan/src/asset_mesh.rs | 6 ++ .../src/ffi/reflection.rs | 1 + .../src/ffi/resources.rs | 3 +- .../fparkan-render-vulkan/src/ffi/smoke.rs | 12 ++- .../src/ffi/smoke_types.rs | 12 ++- .../fparkan-render-vulkan/src/ffi/tests.rs | 11 ++- apps/fparkan-game/src/main.rs | 69 ++++++++++++++---- apps/fparkan-game/src/terrain_lod.rs | 1 + docs/appendices/knowledge-boundaries.md | 3 + docs/reference/environment-effects.md | 9 ++- docs/reference/render-frame.md | 17 +++++ docs/tomes/05-render.md | 10 ++- 14 files changed, 136 insertions(+), 30 deletions(-) diff --git a/adapters/fparkan-render-vulkan/shaders/triangle.vert b/adapters/fparkan-render-vulkan/shaders/triangle.vert index 8e04536..3accd2e 100644 --- a/adapters/fparkan-render-vulkan/shaders/triangle.vert +++ b/adapters/fparkan-render-vulkan/shaders/triangle.vert @@ -45,7 +45,8 @@ layout(std430, set = 0, binding = 5) readonly buffer MaterialSpecular { struct PointLightRecord { vec4 position_range; vec4 rgb_active; - vec4 coefficients_active; + vec3 coefficients; + uint owner_scope; }; layout(std430, set = 0, binding = 6) readonly buffer PointLightBuffer { @@ -197,6 +198,9 @@ LightingTerms native_material_lighting( } for (uint index = 0u; index < point_light_buffer.header.x; ++index) { PointLightRecord point_light = point_light_buffer.records[index]; + if (point_light.owner_scope != 0u && point_light.owner_scope != uint(gl_InstanceIndex)) { + continue; + } vec3 to_light = point_light.position_range.xyz - world_position; float distance_squared = dot(to_light, to_light); float range = point_light.position_range.w; @@ -209,9 +213,9 @@ LightingTerms native_material_lighting( float ndotl = dot(normal, light_direction); if (ndotl > 0.0) { float t = (range - distance_to_light) / range; - float attenuation = point_light.coefficients_active.x - + point_light.coefficients_active.y * t - + point_light.coefficients_active.z * t * t; + float attenuation = point_light.coefficients.x + + point_light.coefficients.y * t + + point_light.coefficients.z * t * t; attenuation = max(attenuation, 0.0); accumulated += ndotl * directional_rgb * point_light.rgb_active.rgb * attenuation; specular += specular_from_light( diff --git a/adapters/fparkan-render-vulkan/shaders/triangle.vert.spv b/adapters/fparkan-render-vulkan/shaders/triangle.vert.spv index 439e2a0439a9041547de4a0752980cda375aaa7e..74adcc718f77d03e6d91d84a2ec3a23412c97b29 100644 GIT binary patch literal 20860 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 diff --git a/adapters/fparkan-render-vulkan/src/asset_mesh.rs b/adapters/fparkan-render-vulkan/src/asset_mesh.rs index d813b91..f25ab89 100644 --- a/adapters/fparkan-render-vulkan/src/asset_mesh.rs +++ b/adapters/fparkan-render-vulkan/src/asset_mesh.rs @@ -859,6 +859,7 @@ fn append_node_fallback_batch( indices.push(vertex_index); } draw_ranges.push(VulkanStaticDrawRange { + owner_scope: 0, first_index, index_count: u32::from(batch.index_count), material_index: u16::from(batch.material_index), @@ -1246,6 +1247,7 @@ fn static_terrain_draw_ranges( end_face += 1; } ranges.push(VulkanStaticDrawRange { + owner_scope: 0, first_index: u32::try_from(start_face) .map_err(|_| VulkanAssetMeshError::IndexOutOfRange)? .checked_mul(3) @@ -1299,6 +1301,7 @@ fn static_model_indices_and_ranges( indices.push(index); } draw_ranges.push(VulkanStaticDrawRange { + owner_scope: 0, first_index, index_count: u32::from(batch.index_count), material_index: u16::from(batch.material_index), @@ -1507,6 +1510,7 @@ mod tests { assert_eq!( mesh.draw_ranges, vec![VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 3, material_index: 0, @@ -2038,6 +2042,7 @@ mod tests { mesh.draw_ranges, vec![ VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 3, material_index: 0, @@ -2050,6 +2055,7 @@ mod tests { VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE, }, VulkanStaticDrawRange { + owner_scope: 0, first_index: 3, index_count: 3, material_index: 7, diff --git a/adapters/fparkan-render-vulkan/src/ffi/reflection.rs b/adapters/fparkan-render-vulkan/src/ffi/reflection.rs index 1a9c9ae..6eb6c67 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/reflection.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/reflection.rs @@ -406,6 +406,7 @@ mod tests { ], indices: vec![0, 1, 2], draw_ranges: vec![VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 3, material_index: 0, diff --git a/adapters/fparkan-render-vulkan/src/ffi/resources.rs b/adapters/fparkan-render-vulkan/src/ffi/resources.rs index 498a472..449750f 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/resources.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/resources.rs @@ -687,12 +687,13 @@ pub(super) fn encode_point_lights( .chain(light.rgb) .chain([if light.active { 1.0 } else { 0.0 }]) .chain(light.coefficients) - .chain([if light.active { 1.0 } else { 0.0 }]) { bytes[offset..offset + std::mem::size_of::()] .copy_from_slice(&value.to_ne_bytes()); offset += std::mem::size_of::(); } + bytes[offset..offset + 4].copy_from_slice(&light.owner_scope.to_ne_bytes()); + offset += 4; } debug_assert_eq!(offset, total); Ok(bytes) diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index a016645..c4f0264 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -2105,7 +2105,7 @@ impl VulkanSmokeRenderer { 1, range.first_index, 0, - 0, + range.owner_scope, ); } device.device().cmd_end_render_pass(command_buffer); @@ -2898,6 +2898,7 @@ mod tests { }; let mut ranges = vec![ VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 3, material_index: 7, @@ -2909,6 +2910,7 @@ mod tests { alpha_test_reference: 17, }, VulkanStaticDrawRange { + owner_scope: 0, first_index: 3, index_count: 3, material_index: 9, @@ -3066,6 +3068,7 @@ mod tests { #[test] fn geometry_updates_dirty_world_order_on_activation_and_deactivation() { let mut ranges = [VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 0, material_index: 0, @@ -3253,6 +3256,7 @@ mod tests { fn native_transparent_order_is_descending_and_keeps_fixed_pass_slots() { let ranges = (0..5) .map(|_| super::super::VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 3, material_index: 0, @@ -3278,6 +3282,7 @@ mod tests { fn inactive_world_range_is_not_reused_as_a_sort_slot() { let ranges = vec![ super::super::VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 3, material_index: 0, @@ -3289,6 +3294,7 @@ mod tests { alpha_test_reference: 0, }, super::super::VulkanStaticDrawRange { + owner_scope: 0, first_index: 3, index_count: 0, material_index: 0, @@ -3300,6 +3306,7 @@ mod tests { alpha_test_reference: 0, }, super::super::VulkanStaticDrawRange { + owner_scope: 0, first_index: 3, index_count: 3, material_index: 0, @@ -3325,6 +3332,7 @@ mod tests { fn native_equal_key_insertion_uses_inclusive_midpoint() { let ranges = (0..4) .map(|index| super::super::VulkanStaticDrawRange { + owner_scope: 0, first_index: index * 3, index_count: 3, material_index: 0, @@ -3419,6 +3427,7 @@ mod tests { const WEATHER: usize = 10; let ranges = (0..11) .map(|index| VulkanStaticDrawRange { + owner_scope: 0, first_index: index * 3, index_count: 3, material_index: 0, @@ -3478,6 +3487,7 @@ mod tests { const WEATHER: usize = 10; let mut ranges = (0..11) .map(|index| VulkanStaticDrawRange { + owner_scope: 0, first_index: index * 3, index_count: 3, material_index: 0, diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index 90fd7b4..9fd0582 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -158,6 +158,8 @@ impl Default for VulkanDirectionalLight { /// One point light in the frame lighting contract. #[derive(Clone, Copy, Debug, PartialEq)] pub struct VulkanPointLight { + /// Zero lights the world; a nonzero scope lights only matching object ranges. + pub owner_scope: u32, /// World-space position. pub position: [f32; 3], /// Light RGB multiplier. @@ -173,6 +175,7 @@ pub struct VulkanPointLight { impl Default for VulkanPointLight { fn default() -> Self { Self { + owner_scope: 0, position: [0.0; 3], rgb: [0.0; 3], range: 0.0, @@ -201,7 +204,7 @@ pub const VULKAN_FRAME_SLOT_COUNT: usize = 2; /// `uvec4` header bytes for the dynamic point-light storage buffer. pub const VULKAN_POINT_LIGHT_HEADER_BYTES: usize = 16; -/// Three `vec4` records per dynamically uploaded point light. +/// Two vec4 records, three attenuation floats and one u32 owner scope. pub const VULKAN_POINT_LIGHT_RECORD_BYTES: usize = 3 * 4 * std::mem::size_of::(); /// Bytes occupied by one material's base and overlay specular records. @@ -444,6 +447,8 @@ pub struct VulkanDynamicDrawRange { /// One indexed triangle-list draw retained from an original mesh batch. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct VulkanStaticDrawRange { + /// Root-object lighting scope; zero denotes terrain or unowned geometry. + pub owner_scope: u32, /// First index in the shared index buffer. pub first_index: u32, /// Number of indices in this draw. @@ -787,6 +792,7 @@ impl VulkanStaticMesh { ], indices: vec![0, 1, 2], draw_ranges: vec![VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 3, material_index: 0, @@ -872,6 +878,7 @@ mod static_mesh_tests { VulkanDirectionalLight::default(), ], point_light: VulkanPointLight { + owner_scope: 0, position: [28.0, 29.0, 30.0], rgb: [31.0, 32.0, 33.0], range: 34.0, @@ -1073,6 +1080,7 @@ mod static_mesh_tests { vertices: VulkanStaticMesh::smoke_triangle().vertices, indices: vec![0, 1], draw_ranges: vec![VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 2, material_index: 0, @@ -1105,6 +1113,7 @@ mod static_mesh_tests { fn material_descriptors_follow_source_batch_selectors() { let ranges = [ VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 3, material_index: 7, @@ -1116,6 +1125,7 @@ mod static_mesh_tests { alpha_test_reference: 0, }, VulkanStaticDrawRange { + owner_scope: 0, first_index: 3, index_count: 3, material_index: 2, diff --git a/adapters/fparkan-render-vulkan/src/ffi/tests.rs b/adapters/fparkan-render-vulkan/src/ffi/tests.rs index 4b2f7d2..8dda737 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/tests.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/tests.rs @@ -518,8 +518,9 @@ fn fragment_shader_dispatches_all_four_material_modes() { } #[test] -fn point_light_storage_payload_is_counted_and_packed_as_three_vec4s() { +fn point_light_storage_payload_preserves_integer_owner_scope_and_stride() { let lights = [VulkanPointLight { + owner_scope: 0x1234_5678, position: [1.0, 2.0, 3.0], rgb: [4.0, 5.0, 6.0], range: 7.0, @@ -532,7 +533,7 @@ fn point_light_storage_payload_is_counted_and_packed_as_three_vec4s() { VULKAN_POINT_LIGHT_HEADER_BYTES + VULKAN_POINT_LIGHT_RECORD_BYTES ); assert_eq!(u32::from_ne_bytes(bytes[0..4].try_into().unwrap()), 1); - let values = (0..12) + let values = (0..11) .map(|index| { let offset = VULKAN_POINT_LIGHT_HEADER_BYTES + index * std::mem::size_of::(); f32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()) @@ -540,7 +541,11 @@ fn point_light_storage_payload_is_counted_and_packed_as_three_vec4s() { .collect::>(); assert_eq!( values, - vec![1.0, 2.0, 3.0, 7.0, 4.0, 5.0, 6.0, 1.0, 8.0, 9.0, 10.0, 1.0] + vec![1.0, 2.0, 3.0, 7.0, 4.0, 5.0, 6.0, 1.0, 8.0, 9.0, 10.0] + ); + assert_eq!( + u32::from_ne_bytes(bytes[60..64].try_into().unwrap()), + 0x1234_5678 ); let many = vec![VulkanPointLight::default(); 33]; diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs index 0d4fd0c..80585e1 100644 --- a/apps/fparkan-game/src/main.rs +++ b/apps/fparkan-game/src/main.rs @@ -723,6 +723,7 @@ struct AmbientEffectMeshPrimitive { #[derive(Clone, Debug)] struct ControlEffectSlot { + light_owner_scope: u32, time_mode: u32, header_flags: u32, random_instance_key: ControlEffectInstanceKey, @@ -2757,6 +2758,7 @@ fn append_environment_range( let range_index = mesh.draw_ranges.len(); mesh.draw_ranges .push(fparkan_render_vulkan::VulkanStaticDrawRange { + owner_scope: 0, first_index, index_count, material_index, @@ -5384,6 +5386,11 @@ fn update_control_point_lights( } lights.push(( VulkanPointLight { + owner_scope: if frame.native_flags & 0x8000_0000 != 0 { + slot.light_owner_scope + } else { + 0 + }, position: frame.position, rgb: [frame.color[0], frame.color[1], frame.color[2]], range: frame.range, @@ -5429,12 +5436,12 @@ fn report_control_point_lights_once( *reported = true; } -fn ordinary_control_point_lights( +fn active_control_point_lights( raw_control_lights: &[(VulkanPointLight, u32)], ) -> Vec { raw_control_lights .iter() - .filter(|(light, flags)| light.active && flags & 0x8000_0000 == 0) + .filter(|(light, _)| light.active) .map(|(light, _)| *light) .collect() } @@ -5494,6 +5501,11 @@ fn append_initial_control_effect_slots( let Some(point_list) = control.point_list.as_ref() else { return Ok(()); }; + let light_owner_scope = u32::try_from(object_index) + .ok() + .and_then(|index| index.checked_add(1)) + .ok_or_else(|| "Control light owner exceeds GPU scope range".to_string())?; + let initial_range_count = mesh.draw_ranges.len(); let mut unsupported_commands = 0; let mut state_dependent_commands = 0; for row in @@ -5620,6 +5632,7 @@ fn append_initial_control_effect_slots( for (command_index, command) in document.commands().iter().enumerate() { if ambient_effect_op1_supported(command) || command.opcode == FxOpcode::Op2 { let slot = ControlEffectSlot { + light_owner_scope, time_mode: header.time_mode, header_flags: header.flags, random_instance_key, @@ -5847,6 +5860,9 @@ fn append_initial_control_effect_slots( }); } } + for range in &mut mesh.draw_ranges[initial_range_count..] { + range.owner_scope = light_owner_scope; + } eprintln!("render_coverage control_fx_unsupported={unsupported_commands} control_fx_requires_game_state={state_dependent_commands}"); Ok(()) } @@ -7710,6 +7726,7 @@ fn project_basement_foundation( .ok_or_else(|| "basement terrain draw range exceeds u32".to_string())?; } else { mesh.draw_ranges.push(VulkanStaticDrawRange { + owner_scope: 0, first_index, index_count: 3, material_index, @@ -8403,6 +8420,10 @@ fn static_preview_mesh_and_materials( .map_err(|err| format!("project mission MSH for Vulkan: {err}"))?; let component_handedness = native_msh_handedness(transform, mission_root.scale); for range in &mut component.draw_ranges { + range.owner_scope = u32::try_from(object_index) + .ok() + .and_then(|index| index.checked_add(1)) + .ok_or_else(|| "MSH light owner exceeds GPU scope range".to_string())?; range.pipeline_state.cull = native_msh_cull_mode(range.batch_flags, component_handedness); } @@ -10019,7 +10040,7 @@ impl StaticVulkanApp { .control_clock_target_ms .is_none_or(|target_ms| control_clock_ms >= target_ms), ); - let control_vulkan_lights = ordinary_control_point_lights(&control_point_lights); + let control_vulkan_lights = active_control_point_lights(&control_point_lights); let effect_updates = update_ambient_effect_geometry( &mut self.ambient_effect_slots, &self.component_pose_buffers, @@ -10433,6 +10454,7 @@ impl StaticVulkanApp { .iter() .find(|light| light.active) .map(|light| VulkanPointLight { + owner_scope: 0, position: light.position, rgb: [light.color[0], light.color[1], light.color[2]], range: light.range, @@ -10448,6 +10470,7 @@ impl StaticVulkanApp { .iter() .filter(|light| light.active) .map(|light| VulkanPointLight { + owner_scope: 0, position: light.position, rgb: [light.color[0], light.color[1], light.color[2]], range: light.range, @@ -11227,6 +11250,7 @@ mod tests { positions: &[[f32; 3]], ) -> (VulkanStaticMesh, VulkanStaticDrawRange) { let range = VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: u32::try_from(positions.len()).expect("fixture length fits u32"), material_index: 0, @@ -12484,6 +12508,7 @@ mod tests { .map(|index| index + environment_vertex_start), ); let draw_range = |first_index, index_count| VulkanStaticDrawRange { + owner_scope: 0, first_index, index_count, material_index: 0, @@ -13196,6 +13221,7 @@ mod tests { #[test] fn draw_range_reorder_remaps_all_material_animation_bindings() { let range = |first_index, material_index| VulkanStaticDrawRange { + owner_scope: 0, first_index, index_count: 3, material_index, @@ -13261,6 +13287,7 @@ mod tests { #[test] fn native_world_transparency_uses_sampled_alpha_and_native_batch_flags() { let range = |blend, batch_flags| VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 3, material_index: 0, @@ -13624,6 +13651,7 @@ mod tests { row_index: 0, }; let control_slot = ControlEffectSlot { + light_owner_scope: 1, time_mode: 1, header_flags: 9, random_instance_key: key, @@ -13728,6 +13756,7 @@ mod tests { .collect(), indices: vec![0, 1, 2], draw_ranges: vec![VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 3, material_index: 0, @@ -16680,6 +16709,7 @@ mod tests { #[test] fn op1_control_lights_use_native_phase_owner_transform_and_flags() { let mut slot = ControlEffectSlot { + light_owner_scope: 1, time_mode: 1, header_flags: 0, random_instance_key: ControlEffectInstanceKey { @@ -16754,6 +16784,10 @@ mod tests { assert_eq!(light.rgb, [0.25, 0.5, 0.75]); assert_eq!(light.range, 15.0); assert_eq!(light.coefficients, [1.0, 0.5, 0.25]); + assert_eq!( + light.owner_scope, 1, + "native local light reaches its owning root" + ); assert_eq!(flags, 0xA000_0000); assert_eq!( [ @@ -16874,8 +16908,9 @@ mod tests { } #[test] - fn op1_generic_diffuse_list_excludes_bit31_and_clears_inactive_lights() { - let light = |position, active| VulkanPointLight { + fn op1_diffuse_list_retains_owner_lights_and_clears_inactive_lights() { + let light = |position, owner_scope, active| VulkanPointLight { + owner_scope, position, rgb: [0.25, 0.5, 0.75], range: 15.0, @@ -16883,15 +16918,21 @@ mod tests { active, }; let raw_records = vec![ - (light([1.0, 2.0, 3.0], true), 0xA000_0000), - (light([4.0, 5.0, 6.0], true), 0x2000_0000), - (light([7.0, 8.0, 9.0], true), 0x8000_0000), - (light([10.0, 11.0, 12.0], false), 0x0000_0000), + (light([1.0, 2.0, 3.0], 5, true), 0xA000_0000), + (light([4.0, 5.0, 6.0], 0, true), 0x2000_0000), + (light([7.0, 8.0, 9.0], 2, true), 0x8000_0000), + (light([10.0, 11.0, 12.0], 0, false), 0x0000_0000), ]; - let ordinary = ordinary_control_point_lights(&raw_records); - assert_eq!(ordinary.len(), 1); - assert_eq!(ordinary[0].position, [4.0, 5.0, 6.0]); + let active = active_control_point_lights(&raw_records); + assert_eq!(active.len(), 3); + assert_eq!( + active + .iter() + .map(|light| light.owner_scope) + .collect::>(), + [5, 0, 2] + ); assert_eq!(raw_records.len(), 4); assert_eq!( raw_records @@ -16901,9 +16942,9 @@ mod tests { vec![0xA000_0000, 0x2000_0000, 0x8000_0000, 0] ); - assert!(ordinary_control_point_lights(&[]).is_empty()); + assert!(active_control_point_lights(&[]).is_empty()); assert!( - ordinary_control_point_lights(&[(light([13.0, 14.0, 15.0], false), 0x2000_0000)]) + active_control_point_lights(&[(light([13.0, 14.0, 15.0], 0, false), 0x2000_0000)]) .is_empty() ); } diff --git a/apps/fparkan-game/src/terrain_lod.rs b/apps/fparkan-game/src/terrain_lod.rs index 190f660..1dd707d 100644 --- a/apps/fparkan-game/src/terrain_lod.rs +++ b/apps/fparkan-game/src/terrain_lod.rs @@ -233,6 +233,7 @@ mod tests { #[test] fn cell_ranges_keep_material_state_and_separate_generated_bands() { let range = VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 12, material_index: 7, diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md index baf5ed5..103769c 100644 --- a/docs/appendices/knowledge-boundaries.md +++ b/docs/appendices/knowledge-boundaries.md @@ -87,6 +87,9 @@ Raw queue clock задаёт фазу волн отдельно от време Control Op1 пока использует цвет и радиус без случайных добавок команды; это явное приближение считается в `render_coverage control_op1_jitter_unsupported`. Масштаб радиуса длиной преобразованного направления и нижняя замена 0,01 восстановлены. +Локальный флаг `0x80000000` сохраняет свет на MSH владельца, исключая его +из общего света рельефа. Мерцание жёлтого источника над заводом ещё использует +авторские цвет и радиус; порядок общего native RNG пока не воспроизводится. Control Op2 воспроизводит modes 0/2; modes 1/3, body+76 и неподдержанные time modes/привязки считаются как `render_coverage control_op2_unsupported`. diff --git a/docs/reference/environment-effects.md b/docs/reference/environment-effects.md index 0e709d9..2a17838 100644 --- a/docs/reference/environment-effects.md +++ b/docs/reference/environment-effects.md @@ -350,9 +350,12 @@ Renderer preview поддерживает FXID-команды Op3 modes 0–2, O Op3 mode0 строит camera-facing поверхность из анимируемых локальных позиции и масштаба; Op7/Op8 обновляют отдельные элементы particle pool; Op9 создаёт индексированную полусферическую сетку. Op1 вычисляется отдельно как список -point lights: selector 1 и записи без `flags & 0x80000000` входят в общий -diffuse light set. Записи с этим битом сохраняют native flags для специальных -путей и projected shadows, но не добавляются в общий diffuse список. +point lights: selector 1 без `flags & 0x80000000` освещает мир, а с этим битом — +собственное строение или юнит. Локальный источник нельзя удалять из освещения +MSH вместе с исключением из общего списка рельефа. Preview сохраняет исходные +flags для projected shadows и передаёт в renderer область действия корневого +объекта. Область ноль означает общий свет; у локального источника и диапазонов +его модели одинаковый ненулевой идентификатор. Body discriminator Op3 читается из первого DWORD `FxCommand.raw_body`; offsets в evaluator-е относительны к данным после четырёхбайтового command word. Mode 1 diff --git a/docs/reference/render-frame.md b/docs/reference/render-frame.md index ebe3971..8bd548f 100644 --- a/docs/reference/render-frame.md +++ b/docs/reference/render-frame.md @@ -126,6 +126,23 @@ RVA `0xF6E0`, преобразует направление текущей ма число таких Control-команд как `control_op1_jitter_unsupported` и использует их авторские значения. У синего источника в проверенном захвате добавки нулевые. +Флаг источника `0x80000000` отделяет локальный свет объекта от общего света +мира. В эталонной `Terrain.dll` (SHA-256 +`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`) +`CLightManager` RVA `0x4C1D0` ведёт отдельные числа всех активных и общих +источников; второй счётчик исключает этот бит. Живой вход `CShade` RVA +`0x2E4A0` подтверждает различие: завод получает жёлтый источник с flags +`0xA0000000`, а рельеф — только общие источники. Для выбранной вершины крыши +native diffuse `1,205386/1,095748/1,893271` и specular после переноса переполнения +`0,160597/0,147096/0,358346` воспроизводятся формулой shader с теми же входами +с отклонением менее `0,0002`. Менять формулу блика для этого дефекта не нужно. + +В Vulkan каждый свет хранит `owner_scope` в последнем DWORD 48-байтовой +SSBO-записи; область диапазона передаётся через `firstInstance`. Нулевая +область света доступна всем диапазонам, ненулевая — только совпадающему +владельцу. Terrain и геометрия без владельца имеют область ноль. Один общий +буфер сохраняется; отдельные наборы света для каждого строения не создаются. + ## Элемент отрисовки Подготовленный элемент отрисовки содержит: diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index 7cbe7d7..1329268 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -854,9 +854,13 @@ emit не должен повторно менять игровое состоя gate. Type 4 point list задаёт локальную матрицу; текущие MSH owner и mounted node poses входят в transform до локальной FX basis. Реализованные visuals охватывают Op3 modes 0–2, Op4, Op7/8 particle pools и Op9 generated mesh. Op1 -selector 1 добавляет point lights без native flag `0x80000000` в общий diffuse -список; остальные active records сохраняют flags для специальных путей и -projected shadows. Радиус источника масштабируется длиной его преобразованного +selector 1 добавляет point lights в освещение. Native flag `0x80000000` +ограничивает источник собственным объектом: такая лампа освещает завод, но +не рельеф вокруг него. Это не запрет на обычный расчёт света. Потеря локального +жёлтого источника над трубой делала крышу завода заметно холоднее оригинала. +Renderer сопоставляет владельца лампы и диапазона MSH; общие источники +доступны всем поверхностям. Исходные flags сохраняются также для projected +shadows. Радиус источника масштабируется длиной его преобразованного направления до нормализации. Например, локальный радиус 7 при масштабе узла около 4,85 освещает область радиусом около 34. Потеря этого множителя оставляла синий свет завода почти незаметным. Неположительный итог заменяется на 0,01.