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 {
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(
@@ -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,
@@ -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,
@@ -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)
@@ -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,
@@ -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,
@@ -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];