diff options
| author | Valentin Popov <valentin@popov.link> | 2026-11-01 12:40:12 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-11-01 12:40:12 +0300 |
| commit | e8942caa233a663155ffec14c1070ad5dbbb9c7b (patch) | |
| tree | 3cd95541668969db5139c528e50e4a32aea83ab1 | |
| parent | 315afff2202f3d7cfd7299a29c0ca67a757b9e2e (diff) | |
| download | fparkan-e8942caa233a663155ffec14c1070ad5dbbb9c7b.tar.xz fparkan-e8942caa233a663155ffec14c1070ad5dbbb9c7b.zip | |
fix(render): preserve object-local point lights on models
| -rw-r--r-- | adapters/fparkan-render-vulkan/shaders/triangle.vert | 12 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/shaders/triangle.vert.spv | bin | 20384 -> 20860 bytes | |||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/asset_mesh.rs | 6 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/reflection.rs | 1 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/resources.rs | 3 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke.rs | 12 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs | 12 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/tests.rs | 11 | ||||
| -rw-r--r-- | apps/fparkan-game/src/main.rs | 69 | ||||
| -rw-r--r-- | apps/fparkan-game/src/terrain_lod.rs | 1 | ||||
| -rw-r--r-- | docs/appendices/knowledge-boundaries.md | 3 | ||||
| -rw-r--r-- | docs/reference/environment-effects.md | 9 | ||||
| -rw-r--r-- | docs/reference/render-frame.md | 17 | ||||
| -rw-r--r-- | 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 Binary files differindex 439e2a0..74adcc7 100644 --- a/adapters/fparkan-render-vulkan/shaders/triangle.vert.spv +++ b/adapters/fparkan-render-vulkan/shaders/triangle.vert.spv 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::<f32>()] .copy_from_slice(&value.to_ne_bytes()); offset += std::mem::size_of::<f32>(); } + 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::<f32>(); /// 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>(); 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::<Vec<_>>(); 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<VulkanPointLight> { 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::<Vec<_>>(), + [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. |
