Skip to main content

oxide_engine/light/
system.rs

1//! Light buffer management for GPU.
2
3use wgpu::{BindGroup, BindGroupLayout, Buffer, Device, Queue};
4
5use oxide_ecs::world::World;
6
7use super::components::{AmbientLight, DirectionalLight, PointLight};
8use super::uniform::{GpuPointLight, LightUniform, MAX_POINT_LIGHTS};
9
10pub struct LightBuffer {
11    pub uniform_buffer: Buffer,
12    pub point_light_buffer: Buffer,
13    pub bind_group_layout: BindGroupLayout,
14    pub bind_group: BindGroup,
15    point_light_capacity: usize,
16}
17
18impl LightBuffer {
19    pub fn new(device: &Device) -> Self {
20        let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
21            label: Some("Light Uniform Buffer"),
22            size: std::mem::size_of::<LightUniform>() as u64,
23            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
24            mapped_at_creation: false,
25        });
26
27        let point_light_capacity = 1usize;
28        let point_light_buffer = Self::create_point_light_buffer(device, point_light_capacity);
29
30        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
31            label: Some("Light Bind Group Layout"),
32            entries: &[
33                wgpu::BindGroupLayoutEntry {
34                    binding: 0,
35                    visibility: wgpu::ShaderStages::FRAGMENT,
36                    ty: wgpu::BindingType::Buffer {
37                        ty: wgpu::BufferBindingType::Uniform,
38                        has_dynamic_offset: false,
39                        min_binding_size: None,
40                    },
41                    count: None,
42                },
43                wgpu::BindGroupLayoutEntry {
44                    binding: 1,
45                    visibility: wgpu::ShaderStages::FRAGMENT,
46                    ty: wgpu::BindingType::Buffer {
47                        ty: wgpu::BufferBindingType::Storage { read_only: true },
48                        has_dynamic_offset: false,
49                        min_binding_size: None,
50                    },
51                    count: None,
52                },
53            ],
54        });
55
56        let bind_group = Self::create_bind_group(
57            device,
58            &bind_group_layout,
59            &uniform_buffer,
60            &point_light_buffer,
61        );
62
63        Self {
64            uniform_buffer,
65            point_light_buffer,
66            bind_group_layout,
67            bind_group,
68            point_light_capacity,
69        }
70    }
71
72    pub fn update(&mut self, device: &Device, queue: &Queue, world: &mut World) {
73        let ambient = {
74            let mut query = world.query::<&AmbientLight>();
75            let first = query.iter(world).next().copied();
76            first
77        };
78
79        let directional_lights: Vec<DirectionalLight> = {
80            let mut query = world.query::<&DirectionalLight>();
81            query.iter(world).copied().collect()
82        };
83
84        let point_lights: Vec<PointLight> = {
85            let mut query = world.query::<&PointLight>();
86            query.iter(world).copied().take(MAX_POINT_LIGHTS).collect()
87        };
88
89        self.ensure_point_light_capacity(device, point_lights.len().max(1));
90
91        let gpu_point_lights: Vec<GpuPointLight> = point_lights
92            .iter()
93            .map(|light| {
94                GpuPointLight::from_values(
95                    light.position,
96                    light.color,
97                    light.intensity,
98                    light.radius,
99                )
100            })
101            .collect();
102
103        let uniform = LightUniform::new(
104            ambient.as_ref(),
105            &directional_lights.iter().collect::<Vec<_>>(),
106            gpu_point_lights.len(),
107        );
108
109        queue.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
110
111        if !gpu_point_lights.is_empty() {
112            queue.write_buffer(
113                &self.point_light_buffer,
114                0,
115                bytemuck::cast_slice(&gpu_point_lights),
116            );
117        }
118    }
119
120    fn ensure_point_light_capacity(&mut self, device: &Device, required: usize) {
121        if required <= self.point_light_capacity {
122            return;
123        }
124
125        self.point_light_capacity = required.next_power_of_two();
126        self.point_light_buffer =
127            Self::create_point_light_buffer(device, self.point_light_capacity);
128        self.bind_group = Self::create_bind_group(
129            device,
130            &self.bind_group_layout,
131            &self.uniform_buffer,
132            &self.point_light_buffer,
133        );
134    }
135
136    fn create_point_light_buffer(device: &Device, capacity: usize) -> Buffer {
137        let byte_size = (capacity * std::mem::size_of::<GpuPointLight>()) as u64;
138        device.create_buffer(&wgpu::BufferDescriptor {
139            label: Some("Point Light Storage Buffer"),
140            size: byte_size.max(std::mem::size_of::<GpuPointLight>() as u64),
141            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
142            mapped_at_creation: false,
143        })
144    }
145
146    fn create_bind_group(
147        device: &Device,
148        layout: &BindGroupLayout,
149        uniform_buffer: &Buffer,
150        point_light_buffer: &Buffer,
151    ) -> BindGroup {
152        device.create_bind_group(&wgpu::BindGroupDescriptor {
153            label: Some("Light Bind Group"),
154            layout,
155            entries: &[
156                wgpu::BindGroupEntry {
157                    binding: 0,
158                    resource: uniform_buffer.as_entire_binding(),
159                },
160                wgpu::BindGroupEntry {
161                    binding: 1,
162                    resource: point_light_buffer.as_entire_binding(),
163                },
164            ],
165        })
166    }
167}