fix(render): preserve object-local point lights on models
Docs Deploy / Build and Deploy MkDocs (push) Successful in 38s

This commit is contained in:
Valentin Popov committed 2026-11-01 13:40:12 +04:00
1 parent 315afff220
commit e8942caa23
14 files changed
+136 -30

No files matched your search

@@ -45,7 +45,8 @@ layout(std430, set = 0, binding = 5) readonly buffer MaterialSpecular {
struct PointLightRecord { struct PointLightRecord {
vec4 position_range; vec4 position_range;
vec4 rgb_active; vec4 rgb_active;
vec4 coefficients_active; vec3 coefficients;
uint owner_scope;
}; };
layout(std430, set = 0, binding = 6) readonly buffer PointLightBuffer { 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) { for (uint index = 0u; index < point_light_buffer.header.x; ++index) {
PointLightRecord point_light = point_light_buffer.records[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; vec3 to_light = point_light.position_range.xyz - world_position;
float distance_squared = dot(to_light, to_light); float distance_squared = dot(to_light, to_light);
float range = point_light.position_range.w; float range = point_light.position_range.w;
@@ -209,9 +213,9 @@ LightingTerms native_material_lighting(
float ndotl = dot(normal, light_direction); float ndotl = dot(normal, light_direction);
if (ndotl > 0.0) { if (ndotl > 0.0) {
float t = (range - distance_to_light) / range; float t = (range - distance_to_light) / range;
float attenuation = point_light.coefficients_active.x float attenuation = point_light.coefficients.x
+ point_light.coefficients_active.y * t + point_light.coefficients.y * t
+ point_light.coefficients_active.z * t * t; + point_light.coefficients.z * t * t;
attenuation = max(attenuation, 0.0); attenuation = max(attenuation, 0.0);
accumulated += ndotl * directional_rgb * point_light.rgb_active.rgb * attenuation; accumulated += ndotl * directional_rgb * point_light.rgb_active.rgb * attenuation;
specular += specular_from_light( specular += specular_from_light(
@@ -859,6 +859,7 @@ fn append_node_fallback_batch(
indices.push(vertex_index); indices.push(vertex_index);
} }
draw_ranges.push(VulkanStaticDrawRange { draw_ranges.push(VulkanStaticDrawRange {
owner_scope: 0,
first_index, first_index,
index_count: u32::from(batch.index_count), index_count: u32::from(batch.index_count),
material_index: u16::from(batch.material_index), material_index: u16::from(batch.material_index),
@@ -1246,6 +1247,7 @@ fn static_terrain_draw_ranges(
end_face += 1; end_face += 1;
} }
ranges.push(VulkanStaticDrawRange { ranges.push(VulkanStaticDrawRange {
owner_scope: 0,
first_index: u32::try_from(start_face) first_index: u32::try_from(start_face)
.map_err(|_| VulkanAssetMeshError::IndexOutOfRange)? .map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?
.checked_mul(3) .checked_mul(3)
@@ -1299,6 +1301,7 @@ fn static_model_indices_and_ranges(
indices.push(index); indices.push(index);
} }
draw_ranges.push(VulkanStaticDrawRange { draw_ranges.push(VulkanStaticDrawRange {
owner_scope: 0,
first_index, first_index,
index_count: u32::from(batch.index_count), index_count: u32::from(batch.index_count),
material_index: u16::from(batch.material_index), material_index: u16::from(batch.material_index),
@@ -1507,6 +1510,7 @@ mod tests {
assert_eq!( assert_eq!(
mesh.draw_ranges, mesh.draw_ranges,
vec![VulkanStaticDrawRange { vec![VulkanStaticDrawRange {
owner_scope: 0,
first_index: 0, first_index: 0,
index_count: 3, index_count: 3,
material_index: 0, material_index: 0,
@@ -2038,6 +2042,7 @@ mod tests {
mesh.draw_ranges, mesh.draw_ranges,
vec![ vec![
VulkanStaticDrawRange { VulkanStaticDrawRange {
owner_scope: 0,
first_index: 0, first_index: 0,
index_count: 3, index_count: 3,
material_index: 0, material_index: 0,
@@ -2050,6 +2055,7 @@ mod tests {
VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE, VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
}, },
VulkanStaticDrawRange { VulkanStaticDrawRange {
owner_scope: 0,
first_index: 3, first_index: 3,
index_count: 3, index_count: 3,
material_index: 7, material_index: 7,
@@ -406,6 +406,7 @@ mod tests {
], ],
indices: vec![0, 1, 2], indices: vec![0, 1, 2],
draw_ranges: vec![VulkanStaticDrawRange { draw_ranges: vec![VulkanStaticDrawRange {
owner_scope: 0,
first_index: 0, first_index: 0,
index_count: 3, index_count: 3,
material_index: 0, material_index: 0,
@@ -687,12 +687,13 @@ pub(super) fn encode_point_lights(
.chain(light.rgb) .chain(light.rgb)
.chain([if light.active { 1.0 } else { 0.0 }]) .chain([if light.active { 1.0 } else { 0.0 }])
.chain(light.coefficients) .chain(light.coefficients)
.chain([if light.active { 1.0 } else { 0.0 }])
{ {
bytes[offset..offset + std::mem::size_of::<f32>()] bytes[offset..offset + std::mem::size_of::<f32>()]
.copy_from_slice(&value.to_ne_bytes()); .copy_from_slice(&value.to_ne_bytes());
offset += std::mem::size_of::<f32>(); 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); debug_assert_eq!(offset, total);
Ok(bytes) Ok(bytes)
@@ -2105,7 +2105,7 @@ impl VulkanSmokeRenderer {
1, 1,
range.first_index, range.first_index,
0, 0,
0, range.owner_scope,
); );
} }
device.device().cmd_end_render_pass(command_buffer); device.device().cmd_end_render_pass(command_buffer);
@@ -2898,6 +2898,7 @@ mod tests {
}; };
let mut ranges = vec![ let mut ranges = vec![
VulkanStaticDrawRange { VulkanStaticDrawRange {
owner_scope: 0,
first_index: 0, first_index: 0,
index_count: 3, index_count: 3,
material_index: 7, material_index: 7,
@@ -2909,6 +2910,7 @@ mod tests {
alpha_test_reference: 17, alpha_test_reference: 17,
}, },
VulkanStaticDrawRange { VulkanStaticDrawRange {
owner_scope: 0,
first_index: 3, first_index: 3,
index_count: 3, index_count: 3,
material_index: 9, material_index: 9,
@@ -3066,6 +3068,7 @@ mod tests {
#[test] #[test]
fn geometry_updates_dirty_world_order_on_activation_and_deactivation() { fn geometry_updates_dirty_world_order_on_activation_and_deactivation() {
let mut ranges = [VulkanStaticDrawRange { let mut ranges = [VulkanStaticDrawRange {
owner_scope: 0,
first_index: 0, first_index: 0,
index_count: 0, index_count: 0,
material_index: 0, material_index: 0,
@@ -3253,6 +3256,7 @@ mod tests {
fn native_transparent_order_is_descending_and_keeps_fixed_pass_slots() { fn native_transparent_order_is_descending_and_keeps_fixed_pass_slots() {
let ranges = (0..5) let ranges = (0..5)
.map(|_| super::super::VulkanStaticDrawRange { .map(|_| super::super::VulkanStaticDrawRange {
owner_scope: 0,
first_index: 0, first_index: 0,
index_count: 3, index_count: 3,
material_index: 0, material_index: 0,
@@ -3278,6 +3282,7 @@ mod tests {
fn inactive_world_range_is_not_reused_as_a_sort_slot() { fn inactive_world_range_is_not_reused_as_a_sort_slot() {
let ranges = vec![ let ranges = vec![
super::super::VulkanStaticDrawRange { super::super::VulkanStaticDrawRange {
owner_scope: 0,
first_index: 0, first_index: 0,
index_count: 3, index_count: 3,
material_index: 0, material_index: 0,
@@ -3289,6 +3294,7 @@ mod tests {
alpha_test_reference: 0, alpha_test_reference: 0,
}, },
super::super::VulkanStaticDrawRange { super::super::VulkanStaticDrawRange {
owner_scope: 0,
first_index: 3, first_index: 3,
index_count: 0, index_count: 0,
material_index: 0, material_index: 0,
@@ -3300,6 +3306,7 @@ mod tests {
alpha_test_reference: 0, alpha_test_reference: 0,
}, },
super::super::VulkanStaticDrawRange { super::super::VulkanStaticDrawRange {
owner_scope: 0,
first_index: 3, first_index: 3,
index_count: 3, index_count: 3,
material_index: 0, material_index: 0,
@@ -3325,6 +3332,7 @@ mod tests {
fn native_equal_key_insertion_uses_inclusive_midpoint() { fn native_equal_key_insertion_uses_inclusive_midpoint() {
let ranges = (0..4) let ranges = (0..4)
.map(|index| super::super::VulkanStaticDrawRange { .map(|index| super::super::VulkanStaticDrawRange {
owner_scope: 0,
first_index: index * 3, first_index: index * 3,
index_count: 3, index_count: 3,
material_index: 0, material_index: 0,
@@ -3419,6 +3427,7 @@ mod tests {
const WEATHER: usize = 10; const WEATHER: usize = 10;
let ranges = (0..11) let ranges = (0..11)
.map(|index| VulkanStaticDrawRange { .map(|index| VulkanStaticDrawRange {
owner_scope: 0,
first_index: index * 3, first_index: index * 3,
index_count: 3, index_count: 3,
material_index: 0, material_index: 0,
@@ -3478,6 +3487,7 @@ mod tests {
const WEATHER: usize = 10; const WEATHER: usize = 10;
let mut ranges = (0..11) let mut ranges = (0..11)
.map(|index| VulkanStaticDrawRange { .map(|index| VulkanStaticDrawRange {
owner_scope: 0,
first_index: index * 3, first_index: index * 3,
index_count: 3, index_count: 3,
material_index: 0, material_index: 0,
@@ -158,6 +158,8 @@ impl Default for VulkanDirectionalLight {
/// One point light in the frame lighting contract. /// One point light in the frame lighting contract.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct VulkanPointLight { pub struct VulkanPointLight {
/// Zero lights the world; a nonzero scope lights only matching object ranges.
pub owner_scope: u32,
/// World-space position. /// World-space position.
pub position: [f32; 3], pub position: [f32; 3],
/// Light RGB multiplier. /// Light RGB multiplier.
@@ -173,6 +175,7 @@ pub struct VulkanPointLight {
impl Default for VulkanPointLight { impl Default for VulkanPointLight {
fn default() -> Self { fn default() -> Self {
Self { Self {
owner_scope: 0,
position: [0.0; 3], position: [0.0; 3],
rgb: [0.0; 3], rgb: [0.0; 3],
range: 0.0, 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. /// `uvec4` header bytes for the dynamic point-light storage buffer.
pub const VULKAN_POINT_LIGHT_HEADER_BYTES: usize = 16; 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>(); 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. /// 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. /// One indexed triangle-list draw retained from an original mesh batch.
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct VulkanStaticDrawRange { pub struct VulkanStaticDrawRange {
/// Root-object lighting scope; zero denotes terrain or unowned geometry.
pub owner_scope: u32,
/// First index in the shared index buffer. /// First index in the shared index buffer.
pub first_index: u32, pub first_index: u32,
/// Number of indices in this draw. /// Number of indices in this draw.
@@ -787,6 +792,7 @@ impl VulkanStaticMesh {
], ],
indices: vec![0, 1, 2], indices: vec![0, 1, 2],
draw_ranges: vec![VulkanStaticDrawRange { draw_ranges: vec![VulkanStaticDrawRange {
owner_scope: 0,
first_index: 0, first_index: 0,
index_count: 3, index_count: 3,
material_index: 0, material_index: 0,
@@ -872,6 +878,7 @@ mod static_mesh_tests {
VulkanDirectionalLight::default(), VulkanDirectionalLight::default(),
], ],
point_light: VulkanPointLight { point_light: VulkanPointLight {
owner_scope: 0,
position: [28.0, 29.0, 30.0], position: [28.0, 29.0, 30.0],
rgb: [31.0, 32.0, 33.0], rgb: [31.0, 32.0, 33.0],
range: 34.0, range: 34.0,
@@ -1073,6 +1080,7 @@ mod static_mesh_tests {
vertices: VulkanStaticMesh::smoke_triangle().vertices, vertices: VulkanStaticMesh::smoke_triangle().vertices,
indices: vec![0, 1], indices: vec![0, 1],
draw_ranges: vec![VulkanStaticDrawRange { draw_ranges: vec![VulkanStaticDrawRange {
owner_scope: 0,
first_index: 0, first_index: 0,
index_count: 2, index_count: 2,
material_index: 0, material_index: 0,
@@ -1105,6 +1113,7 @@ mod static_mesh_tests {
fn material_descriptors_follow_source_batch_selectors() { fn material_descriptors_follow_source_batch_selectors() {
let ranges = [ let ranges = [
VulkanStaticDrawRange { VulkanStaticDrawRange {
owner_scope: 0,
first_index: 0, first_index: 0,
index_count: 3, index_count: 3,
material_index: 7, material_index: 7,
@@ -1116,6 +1125,7 @@ mod static_mesh_tests {
alpha_test_reference: 0, alpha_test_reference: 0,
}, },
VulkanStaticDrawRange { VulkanStaticDrawRange {
owner_scope: 0,
first_index: 3, first_index: 3,
index_count: 3, index_count: 3,
material_index: 2, material_index: 2,
@@ -518,8 +518,9 @@ fn fragment_shader_dispatches_all_four_material_modes() {
} }
#[test] #[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 { let lights = [VulkanPointLight {
owner_scope: 0x1234_5678,
position: [1.0, 2.0, 3.0], position: [1.0, 2.0, 3.0],
rgb: [4.0, 5.0, 6.0], rgb: [4.0, 5.0, 6.0],
range: 7.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 VULKAN_POINT_LIGHT_HEADER_BYTES + VULKAN_POINT_LIGHT_RECORD_BYTES
); );
assert_eq!(u32::from_ne_bytes(bytes[0..4].try_into().unwrap()), 1); assert_eq!(u32::from_ne_bytes(bytes[0..4].try_into().unwrap()), 1);
let values = (0..12) let values = (0..11)
.map(|index| { .map(|index| {
let offset = VULKAN_POINT_LIGHT_HEADER_BYTES + index * std::mem::size_of::<f32>(); let offset = VULKAN_POINT_LIGHT_HEADER_BYTES + index * std::mem::size_of::<f32>();
f32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()) 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<_>>(); .collect::<Vec<_>>();
assert_eq!( assert_eq!(
values, 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]; let many = vec![VulkanPointLight::default(); 33];
+55 -14
View File
@@ -723,6 +723,7 @@ struct AmbientEffectMeshPrimitive {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct ControlEffectSlot { struct ControlEffectSlot {
light_owner_scope: u32,
time_mode: u32, time_mode: u32,
header_flags: u32, header_flags: u32,
random_instance_key: ControlEffectInstanceKey, random_instance_key: ControlEffectInstanceKey,
@@ -2757,6 +2758,7 @@ fn append_environment_range(
let range_index = mesh.draw_ranges.len(); let range_index = mesh.draw_ranges.len();
mesh.draw_ranges mesh.draw_ranges
.push(fparkan_render_vulkan::VulkanStaticDrawRange { .push(fparkan_render_vulkan::VulkanStaticDrawRange {
owner_scope: 0,
first_index, first_index,
index_count, index_count,
material_index, material_index,
@@ -5384,6 +5386,11 @@ fn update_control_point_lights(
} }
lights.push(( lights.push((
VulkanPointLight { VulkanPointLight {
owner_scope: if frame.native_flags & 0x8000_0000 != 0 {
slot.light_owner_scope
} else {
0
},
position: frame.position, position: frame.position,
rgb: [frame.color[0], frame.color[1], frame.color[2]], rgb: [frame.color[0], frame.color[1], frame.color[2]],
range: frame.range, range: frame.range,
@@ -5429,12 +5436,12 @@ fn report_control_point_lights_once(
*reported = true; *reported = true;
} }
fn ordinary_control_point_lights( fn active_control_point_lights(
raw_control_lights: &[(VulkanPointLight, u32)], raw_control_lights: &[(VulkanPointLight, u32)],
) -> Vec<VulkanPointLight> { ) -> Vec<VulkanPointLight> {
raw_control_lights raw_control_lights
.iter() .iter()
.filter(|(light, flags)| light.active && flags & 0x8000_0000 == 0) .filter(|(light, _)| light.active)
.map(|(light, _)| *light) .map(|(light, _)| *light)
.collect() .collect()
} }
@@ -5494,6 +5501,11 @@ fn append_initial_control_effect_slots(
let Some(point_list) = control.point_list.as_ref() else { let Some(point_list) = control.point_list.as_ref() else {
return Ok(()); 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 unsupported_commands = 0;
let mut state_dependent_commands = 0; let mut state_dependent_commands = 0;
for row in for row in
@@ -5620,6 +5632,7 @@ fn append_initial_control_effect_slots(
for (command_index, command) in document.commands().iter().enumerate() { for (command_index, command) in document.commands().iter().enumerate() {
if ambient_effect_op1_supported(command) || command.opcode == FxOpcode::Op2 { if ambient_effect_op1_supported(command) || command.opcode == FxOpcode::Op2 {
let slot = ControlEffectSlot { let slot = ControlEffectSlot {
light_owner_scope,
time_mode: header.time_mode, time_mode: header.time_mode,
header_flags: header.flags, header_flags: header.flags,
random_instance_key, 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}"); eprintln!("render_coverage control_fx_unsupported={unsupported_commands} control_fx_requires_game_state={state_dependent_commands}");
Ok(()) Ok(())
} }
@@ -7710,6 +7726,7 @@ fn project_basement_foundation(
.ok_or_else(|| "basement terrain draw range exceeds u32".to_string())?; .ok_or_else(|| "basement terrain draw range exceeds u32".to_string())?;
} else { } else {
mesh.draw_ranges.push(VulkanStaticDrawRange { mesh.draw_ranges.push(VulkanStaticDrawRange {
owner_scope: 0,
first_index, first_index,
index_count: 3, index_count: 3,
material_index, material_index,
@@ -8403,6 +8420,10 @@ fn static_preview_mesh_and_materials(
.map_err(|err| format!("project mission MSH for Vulkan: {err}"))?; .map_err(|err| format!("project mission MSH for Vulkan: {err}"))?;
let component_handedness = native_msh_handedness(transform, mission_root.scale); let component_handedness = native_msh_handedness(transform, mission_root.scale);
for range in &mut component.draw_ranges { 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 = range.pipeline_state.cull =
native_msh_cull_mode(range.batch_flags, component_handedness); native_msh_cull_mode(range.batch_flags, component_handedness);
} }
@@ -10019,7 +10040,7 @@ impl StaticVulkanApp {
.control_clock_target_ms .control_clock_target_ms
.is_none_or(|target_ms| control_clock_ms >= 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( let effect_updates = update_ambient_effect_geometry(
&mut self.ambient_effect_slots, &mut self.ambient_effect_slots,
&self.component_pose_buffers, &self.component_pose_buffers,
@@ -10433,6 +10454,7 @@ impl StaticVulkanApp {
.iter() .iter()
.find(|light| light.active) .find(|light| light.active)
.map(|light| VulkanPointLight { .map(|light| VulkanPointLight {
owner_scope: 0,
position: light.position, position: light.position,
rgb: [light.color[0], light.color[1], light.color[2]], rgb: [light.color[0], light.color[1], light.color[2]],
range: light.range, range: light.range,
@@ -10448,6 +10470,7 @@ impl StaticVulkanApp {
.iter() .iter()
.filter(|light| light.active) .filter(|light| light.active)
.map(|light| VulkanPointLight { .map(|light| VulkanPointLight {
owner_scope: 0,
position: light.position, position: light.position,
rgb: [light.color[0], light.color[1], light.color[2]], rgb: [light.color[0], light.color[1], light.color[2]],
range: light.range, range: light.range,
@@ -11227,6 +11250,7 @@ mod tests {
positions: &[[f32; 3]], positions: &[[f32; 3]],
) -> (VulkanStaticMesh, VulkanStaticDrawRange) { ) -> (VulkanStaticMesh, VulkanStaticDrawRange) {
let range = VulkanStaticDrawRange { let range = VulkanStaticDrawRange {
owner_scope: 0,
first_index: 0, first_index: 0,
index_count: u32::try_from(positions.len()).expect("fixture length fits u32"), index_count: u32::try_from(positions.len()).expect("fixture length fits u32"),
material_index: 0, material_index: 0,
@@ -12484,6 +12508,7 @@ mod tests {
.map(|index| index + environment_vertex_start), .map(|index| index + environment_vertex_start),
); );
let draw_range = |first_index, index_count| VulkanStaticDrawRange { let draw_range = |first_index, index_count| VulkanStaticDrawRange {
owner_scope: 0,
first_index, first_index,
index_count, index_count,
material_index: 0, material_index: 0,
@@ -13196,6 +13221,7 @@ mod tests {
#[test] #[test]
fn draw_range_reorder_remaps_all_material_animation_bindings() { fn draw_range_reorder_remaps_all_material_animation_bindings() {
let range = |first_index, material_index| VulkanStaticDrawRange { let range = |first_index, material_index| VulkanStaticDrawRange {
owner_scope: 0,
first_index, first_index,
index_count: 3, index_count: 3,
material_index, material_index,
@@ -13261,6 +13287,7 @@ mod tests {
#[test] #[test]
fn native_world_transparency_uses_sampled_alpha_and_native_batch_flags() { fn native_world_transparency_uses_sampled_alpha_and_native_batch_flags() {
let range = |blend, batch_flags| VulkanStaticDrawRange { let range = |blend, batch_flags| VulkanStaticDrawRange {
owner_scope: 0,
first_index: 0, first_index: 0,
index_count: 3, index_count: 3,
material_index: 0, material_index: 0,
@@ -13624,6 +13651,7 @@ mod tests {
row_index: 0, row_index: 0,
}; };
let control_slot = ControlEffectSlot { let control_slot = ControlEffectSlot {
light_owner_scope: 1,
time_mode: 1, time_mode: 1,
header_flags: 9, header_flags: 9,
random_instance_key: key, random_instance_key: key,
@@ -13728,6 +13756,7 @@ mod tests {
.collect(), .collect(),
indices: vec![0, 1, 2], indices: vec![0, 1, 2],
draw_ranges: vec![VulkanStaticDrawRange { draw_ranges: vec![VulkanStaticDrawRange {
owner_scope: 0,
first_index: 0, first_index: 0,
index_count: 3, index_count: 3,
material_index: 0, material_index: 0,
@@ -16680,6 +16709,7 @@ mod tests {
#[test] #[test]
fn op1_control_lights_use_native_phase_owner_transform_and_flags() { fn op1_control_lights_use_native_phase_owner_transform_and_flags() {
let mut slot = ControlEffectSlot { let mut slot = ControlEffectSlot {
light_owner_scope: 1,
time_mode: 1, time_mode: 1,
header_flags: 0, header_flags: 0,
random_instance_key: ControlEffectInstanceKey { random_instance_key: ControlEffectInstanceKey {
@@ -16754,6 +16784,10 @@ mod tests {
assert_eq!(light.rgb, [0.25, 0.5, 0.75]); assert_eq!(light.rgb, [0.25, 0.5, 0.75]);
assert_eq!(light.range, 15.0); assert_eq!(light.range, 15.0);
assert_eq!(light.coefficients, [1.0, 0.5, 0.25]); 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!(flags, 0xA000_0000);
assert_eq!( assert_eq!(
[ [
@@ -16874,8 +16908,9 @@ mod tests {
} }
#[test] #[test]
fn op1_generic_diffuse_list_excludes_bit31_and_clears_inactive_lights() { fn op1_diffuse_list_retains_owner_lights_and_clears_inactive_lights() {
let light = |position, active| VulkanPointLight { let light = |position, owner_scope, active| VulkanPointLight {
owner_scope,
position, position,
rgb: [0.25, 0.5, 0.75], rgb: [0.25, 0.5, 0.75],
range: 15.0, range: 15.0,
@@ -16883,15 +16918,21 @@ mod tests {
active, active,
}; };
let raw_records = vec![ let raw_records = vec![
(light([1.0, 2.0, 3.0], true), 0xA000_0000), (light([1.0, 2.0, 3.0], 5, true), 0xA000_0000),
(light([4.0, 5.0, 6.0], true), 0x2000_0000), (light([4.0, 5.0, 6.0], 0, true), 0x2000_0000),
(light([7.0, 8.0, 9.0], true), 0x8000_0000), (light([7.0, 8.0, 9.0], 2, true), 0x8000_0000),
(light([10.0, 11.0, 12.0], false), 0x0000_0000), (light([10.0, 11.0, 12.0], 0, false), 0x0000_0000),
]; ];
let ordinary = ordinary_control_point_lights(&raw_records); let active = active_control_point_lights(&raw_records);
assert_eq!(ordinary.len(), 1); assert_eq!(active.len(), 3);
assert_eq!(ordinary[0].position, [4.0, 5.0, 6.0]); assert_eq!(
active
.iter()
.map(|light| light.owner_scope)
.collect::<Vec<_>>(),
[5, 0, 2]
);
assert_eq!(raw_records.len(), 4); assert_eq!(raw_records.len(), 4);
assert_eq!( assert_eq!(
raw_records raw_records
@@ -16901,9 +16942,9 @@ mod tests {
vec![0xA000_0000, 0x2000_0000, 0x8000_0000, 0] vec![0xA000_0000, 0x2000_0000, 0x8000_0000, 0]
); );
assert!(ordinary_control_point_lights(&[]).is_empty()); assert!(active_control_point_lights(&[]).is_empty());
assert!( 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() .is_empty()
); );
} }
+1
View File
@@ -233,6 +233,7 @@ mod tests {
#[test] #[test]
fn cell_ranges_keep_material_state_and_separate_generated_bands() { fn cell_ranges_keep_material_state_and_separate_generated_bands() {
let range = VulkanStaticDrawRange { let range = VulkanStaticDrawRange {
owner_scope: 0,
first_index: 0, first_index: 0,
index_count: 12, index_count: 12,
material_index: 7, material_index: 7,
+3
View File
@@ -87,6 +87,9 @@ Raw queue clock задаёт фазу волн отдельно от време
Control Op1 пока использует цвет и радиус без случайных добавок команды; Control Op1 пока использует цвет и радиус без случайных добавок команды;
это явное приближение считается в `render_coverage control_op1_jitter_unsupported`. это явное приближение считается в `render_coverage control_op1_jitter_unsupported`.
Масштаб радиуса длиной преобразованного направления и нижняя замена 0,01 восстановлены. Масштаб радиуса длиной преобразованного направления и нижняя замена 0,01 восстановлены.
Локальный флаг `0x80000000` сохраняет свет на MSH владельца, исключая его
из общего света рельефа. Мерцание жёлтого источника над заводом ещё использует
авторские цвет и радиус; порядок общего native RNG пока не воспроизводится.
Control Op2 воспроизводит modes 0/2; modes 1/3, body+76 и неподдержанные Control Op2 воспроизводит modes 0/2; modes 1/3, body+76 и неподдержанные
time modes/привязки считаются как `render_coverage control_op2_unsupported`. time modes/привязки считаются как `render_coverage control_op2_unsupported`.
+6 -3
View File
@@ -350,9 +350,12 @@ Renderer preview поддерживает FXID-команды Op3 modes 0–2, O
Op3 mode0 строит camera-facing поверхность из анимируемых локальных позиции и Op3 mode0 строит camera-facing поверхность из анимируемых локальных позиции и
масштаба; Op7/Op8 обновляют отдельные элементы particle pool; Op9 создаёт масштаба; Op7/Op8 обновляют отдельные элементы particle pool; Op9 создаёт
индексированную полусферическую сетку. Op1 вычисляется отдельно как список индексированную полусферическую сетку. Op1 вычисляется отдельно как список
point lights: selector 1 и записи без `flags & 0x80000000` входят в общий point lights: selector 1 без `flags & 0x80000000` освещает мир, а с этим битом —
diffuse light set. Записи с этим битом сохраняют native flags для специальных собственное строение или юнит. Локальный источник нельзя удалять из освещения
путей и projected shadows, но не добавляются в общий diffuse список. MSH вместе с исключением из общего списка рельефа. Preview сохраняет исходные
flags для projected shadows и передаёт в renderer область действия корневого
объекта. Область ноль означает общий свет; у локального источника и диапазонов
его модели одинаковый ненулевой идентификатор.
Body discriminator Op3 читается из первого DWORD `FxCommand.raw_body`; offsets Body discriminator Op3 читается из первого DWORD `FxCommand.raw_body`; offsets
в evaluator-е относительны к данным после четырёхбайтового command word. Mode 1 в evaluator-е относительны к данным после четырёхбайтового command word. Mode 1
+17
View File
@@ -126,6 +126,23 @@ RVA `0xF6E0`, преобразует направление текущей ма
число таких Control-команд как `control_op1_jitter_unsupported` и использует число таких 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 и геометрия без владельца имеют область ноль. Один общий
буфер сохраняется; отдельные наборы света для каждого строения не создаются.
## Элемент отрисовки ## Элемент отрисовки
Подготовленный элемент отрисовки содержит: Подготовленный элемент отрисовки содержит:
+7 -3
View File
@@ -854,9 +854,13 @@ emit не должен повторно менять игровое состоя
gate. Type 4 point list задаёт локальную матрицу; текущие MSH owner и mounted gate. Type 4 point list задаёт локальную матрицу; текущие MSH owner и mounted
node poses входят в transform до локальной FX basis. Реализованные visuals node poses входят в transform до локальной FX basis. Реализованные visuals
охватывают Op3 modes 0–2, Op4, Op7/8 particle pools и Op9 generated mesh. Op1 охватывают Op3 modes 0–2, Op4, Op7/8 particle pools и Op9 generated mesh. Op1
selector 1 добавляет point lights без native flag `0x80000000` в общий diffuse selector 1 добавляет point lights в освещение. Native flag `0x80000000`
список; остальные active records сохраняют flags для специальных путей и ограничивает источник собственным объектом: такая лампа освещает завод, но
projected shadows. Радиус источника масштабируется длиной его преобразованного не рельеф вокруг него. Это не запрет на обычный расчёт света. Потеря локального
жёлтого источника над трубой делала крышу завода заметно холоднее оригинала.
Renderer сопоставляет владельца лампы и диапазона MSH; общие источники
доступны всем поверхностям. Исходные flags сохраняются также для projected
shadows. Радиус источника масштабируется длиной его преобразованного
направления до нормализации. Например, локальный радиус 7 при масштабе узла направления до нормализации. Например, локальный радиус 7 при масштабе узла
около 4,85 освещает область радиусом около 34. Потеря этого множителя оставляла около 4,85 освещает область радиусом около 34. Потеря этого множителя оставляла
синий свет завода почти незаметным. Неположительный итог заменяется на 0,01. синий свет завода почти незаметным. Неположительный итог заменяется на 0,01.