Skip to main content

scenix_renderer/
gpu_scene.rs

1use std::collections::HashMap;
2
3use scenix_core::{Color, LightId, MaterialId, MeshId, TextureId, ValidationError};
4use scenix_light::{AmbientLight, DirectionalLight, PointLight, SpotLight};
5use scenix_material::{
6    LambertMaterial, Material, NormalMaterial, PbrMaterial, PhysicalMaterial, PipelineKey,
7    ToonMaterial, UnlitMaterial, WireframeMaterial,
8};
9use scenix_math::{Aabb, Mat4, Vec2, Vec3, Vec4};
10use scenix_mesh::Geometry;
11use scenix_texture::{AddressMode, CompareFunction, FilterMode, Sampler, Texture2D, TextureFormat};
12use wgpu::util::DeviceExt;
13
14/// Interleaved GPU vertex layout used by the v0.6 renderer.
15#[repr(C)]
16#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
17pub struct PackedVertex {
18    /// Vertex position.
19    pub position: [f32; 3],
20    /// Vertex normal.
21    pub normal: [f32; 3],
22    /// Primary texture coordinate.
23    pub uv: [f32; 2],
24    /// Vertex color.
25    pub color: [f32; 4],
26    /// Tangent vector and handedness.
27    pub tangent: [f32; 4],
28}
29
30impl PackedVertex {
31    /// Returns the wgpu vertex-buffer layout.
32    pub const fn layout() -> wgpu::VertexBufferLayout<'static> {
33        const ATTRIBUTES: [wgpu::VertexAttribute; 5] = wgpu::vertex_attr_array![
34            0 => Float32x3,
35            1 => Float32x3,
36            2 => Float32x2,
37            3 => Float32x4,
38            4 => Float32x4
39        ];
40        wgpu::VertexBufferLayout {
41            array_stride: core::mem::size_of::<PackedVertex>() as u64,
42            step_mode: wgpu::VertexStepMode::Vertex,
43            attributes: &ATTRIBUTES,
44        }
45    }
46}
47
48/// Index type chosen for packed geometry.
49#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
50#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
51pub enum GpuIndexFormat {
52    /// 16-bit index buffer.
53    Uint16,
54    /// 32-bit index buffer.
55    Uint32,
56}
57
58impl GpuIndexFormat {
59    /// Returns the matching wgpu index format.
60    #[inline]
61    pub const fn to_wgpu(self) -> wgpu::IndexFormat {
62        match self {
63            Self::Uint16 => wgpu::IndexFormat::Uint16,
64            Self::Uint32 => wgpu::IndexFormat::Uint32,
65        }
66    }
67}
68
69/// CPU-packed geometry ready for GPU upload.
70#[derive(Clone, Debug, PartialEq)]
71pub struct PackedGeometry {
72    /// Interleaved vertices.
73    pub vertices: Vec<PackedVertex>,
74    /// Raw index bytes in `index_format`.
75    pub index_bytes: Vec<u8>,
76    /// Number of indices.
77    pub index_count: u32,
78    /// Index storage format.
79    pub index_format: GpuIndexFormat,
80    /// Local-space geometry bounds.
81    pub aabb: Aabb,
82}
83
84/// Uploaded GPU mesh buffers.
85#[derive(Debug)]
86pub struct GpuMesh {
87    vertex_buffer: wgpu::Buffer,
88    index_buffer: wgpu::Buffer,
89    packed: PackedGeometry,
90}
91
92impl GpuMesh {
93    /// Returns the vertex buffer.
94    #[inline]
95    pub const fn vertex_buffer(&self) -> &wgpu::Buffer {
96        &self.vertex_buffer
97    }
98
99    /// Returns the index buffer.
100    #[inline]
101    pub const fn index_buffer(&self) -> &wgpu::Buffer {
102        &self.index_buffer
103    }
104
105    /// Returns packed geometry metadata.
106    #[inline]
107    pub const fn packed(&self) -> &PackedGeometry {
108        &self.packed
109    }
110}
111
112/// Renderer-side material registry entry.
113#[derive(Clone, Debug, PartialEq)]
114pub enum RendererMaterial {
115    /// Metallic-roughness material.
116    Pbr(PbrMaterial),
117    /// Advanced physical material.
118    Physical(PhysicalMaterial),
119    /// Constant-color unlit material.
120    Unlit(UnlitMaterial),
121    /// Diffuse Lambert material.
122    Lambert(LambertMaterial),
123    /// Cel-shaded material.
124    Toon(ToonMaterial),
125    /// Wireframe/debug preview material.
126    Wireframe(WireframeMaterial),
127    /// Normal visualization material.
128    Normal(NormalMaterial),
129}
130
131impl RendererMaterial {
132    /// Returns the material pipeline key.
133    #[inline]
134    pub fn pipeline_key(&self) -> PipelineKey {
135        match self {
136            Self::Pbr(material) => material.pipeline_key(),
137            Self::Physical(material) => material.pipeline_key(),
138            Self::Unlit(material) => material.pipeline_key(),
139            Self::Lambert(material) => material.pipeline_key(),
140            Self::Toon(material) => material.pipeline_key(),
141            Self::Wireframe(material) => material.pipeline_key(),
142            Self::Normal(material) => material.pipeline_key(),
143        }
144    }
145
146    /// Returns whether the material should be depth-sorted with transparent draws.
147    #[inline]
148    pub fn is_transparent(&self) -> bool {
149        match self {
150            Self::Pbr(material) => material.is_transparent(),
151            Self::Physical(material) => material.is_transparent(),
152            Self::Unlit(material) => material.is_transparent(),
153            Self::Lambert(material) => material.is_transparent(),
154            Self::Toon(material) => material.is_transparent(),
155            Self::Wireframe(material) => material.is_transparent(),
156            Self::Normal(material) => material.is_transparent(),
157        }
158    }
159
160    /// Returns the base preview color used by the stable v1 renderer path.
161    #[inline]
162    pub fn preview_color(&self) -> Color {
163        match self {
164            Self::Pbr(material) => material.albedo,
165            Self::Physical(material) => material.base.albedo,
166            Self::Unlit(material) => material.color,
167            Self::Lambert(material) => material.color,
168            Self::Toon(material) => material.color,
169            Self::Wireframe(material) => Color {
170                a: material.opacity,
171                ..material.color
172            },
173            Self::Normal(_) => Color::WHITE,
174        }
175    }
176
177    /// Returns a compact shader-family code for the shared v1 preview shader.
178    #[inline]
179    pub fn preview_shader_code(&self) -> f32 {
180        match self {
181            Self::Pbr(_) => 0.0,
182            Self::Physical(_) => 1.0,
183            Self::Unlit(_) => 2.0,
184            Self::Lambert(_) => 3.0,
185            Self::Toon(_) => 4.0,
186            Self::Wireframe(_) => 5.0,
187            Self::Normal(_) => 6.0,
188        }
189    }
190}
191
192/// Renderer-side texture metadata.
193#[derive(Clone, Debug, PartialEq, Eq)]
194pub struct GpuTexture {
195    /// Texture width.
196    pub width: u32,
197    /// Texture height.
198    pub height: u32,
199    /// CPU texture format.
200    pub format: TextureFormat,
201    /// Matching wgpu texture format, when supported.
202    pub wgpu_format: Option<wgpu::TextureFormat>,
203    /// Sampler metadata.
204    pub sampler: Sampler,
205    /// Number of mip levels stored in the CPU texture.
206    pub mip_levels: u32,
207}
208
209/// Texture metadata store used by `GpuMaterial`.
210pub type TextureStore = HashMap<TextureId, GpuTexture>;
211
212/// Renderer-side light registry entry.
213#[derive(Clone, Copy, Debug, PartialEq)]
214pub enum RendererLight {
215    /// Ambient light.
216    Ambient(AmbientLight),
217    /// Directional light.
218    Directional(DirectionalLight),
219    /// Point light.
220    Point(PointLight),
221    /// Spot light.
222    Spot(SpotLight),
223}
224
225/// Visible draw submission generated from a scene node.
226#[derive(Clone, Copy, Debug, PartialEq)]
227pub struct DrawSubmission {
228    /// Mesh resource ID.
229    pub mesh_id: MeshId,
230    /// Material resource ID.
231    pub material_id: MaterialId,
232    /// Node world transform.
233    pub world_matrix: Mat4,
234    /// World-space bounds used for culling.
235    pub world_aabb: Aabb,
236    /// Distance from camera position to bounds center.
237    pub distance_to_camera: f32,
238    /// Whether this draw needs transparent sorting.
239    pub transparent: bool,
240    /// Stable render order.
241    pub render_order: u32,
242}
243
244/// Renderer-owned GPU scene resources and CPU metadata.
245#[derive(Debug, Default)]
246pub struct GpuScene {
247    meshes: HashMap<MeshId, GpuMesh>,
248    materials: HashMap<MaterialId, RendererMaterial>,
249    textures: TextureStore,
250    lights: HashMap<LightId, RendererLight>,
251}
252
253impl GpuScene {
254    /// Creates an empty GPU scene registry.
255    #[inline]
256    pub fn new() -> Self {
257        Self::default()
258    }
259
260    /// Packs geometry into the renderer interleaved vertex/index layout.
261    pub fn pack_geometry(geometry: &Geometry) -> Result<PackedGeometry, ValidationError> {
262        geometry.validate()?;
263        let vertex_count = geometry.positions.len();
264        let mut vertices = Vec::with_capacity(vertex_count);
265        for index in 0..vertex_count {
266            let position = geometry.positions[index];
267            let normal = geometry.normals.get(index).copied().unwrap_or(Vec3::Y);
268            let uv = geometry.uvs.get(index).copied().unwrap_or(Vec2::ZERO);
269            let color = geometry.colors.get(index).copied().unwrap_or(Color::WHITE);
270            let tangent = geometry
271                .tangents
272                .get(index)
273                .copied()
274                .unwrap_or(Vec4::new(1.0, 0.0, 0.0, 1.0));
275            vertices.push(PackedVertex {
276                position: [position.x, position.y, position.z],
277                normal: [normal.x, normal.y, normal.z],
278                uv: [uv.x, uv.y],
279                color: color.to_array(),
280                tangent: [tangent.x, tangent.y, tangent.z, tangent.w],
281            });
282        }
283
284        let source_indices: Vec<u32> = if geometry.indices.is_empty() {
285            (0..vertex_count as u32).collect()
286        } else {
287            geometry.indices.clone()
288        };
289        let can_use_u16 = vertex_count <= u16::MAX as usize
290            && source_indices.iter().all(|index| *index <= u16::MAX as u32);
291        let (index_bytes, index_format) = if can_use_u16 {
292            let indices: Vec<u16> = source_indices.iter().map(|index| *index as u16).collect();
293            (
294                bytemuck::cast_slice(&indices).to_vec(),
295                GpuIndexFormat::Uint16,
296            )
297        } else {
298            (
299                bytemuck::cast_slice(source_indices.as_slice()).to_vec(),
300                GpuIndexFormat::Uint32,
301            )
302        };
303
304        Ok(PackedGeometry {
305            vertices,
306            index_bytes,
307            index_count: source_indices.len() as u32,
308            index_format,
309            aabb: geometry.aabb(),
310        })
311    }
312
313    /// Uploads and stores a mesh.
314    pub fn register_mesh(
315        &mut self,
316        device: &wgpu::Device,
317        mesh_id: MeshId,
318        geometry: &Geometry,
319    ) -> Result<(), ValidationError> {
320        if mesh_id.is_null() {
321            return Err(ValidationError::InvalidId);
322        }
323        let packed = Self::pack_geometry(geometry)?;
324        let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
325            label: Some("scenix.mesh.vertices"),
326            contents: bytemuck::cast_slice(packed.vertices.as_slice()),
327            usage: wgpu::BufferUsages::VERTEX,
328        });
329        let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
330            label: Some("scenix.mesh.indices"),
331            contents: packed.index_bytes.as_slice(),
332            usage: wgpu::BufferUsages::INDEX,
333        });
334        self.meshes.insert(
335            mesh_id,
336            GpuMesh {
337                vertex_buffer,
338                index_buffer,
339                packed,
340            },
341        );
342        Ok(())
343    }
344
345    /// Registers a PBR material.
346    pub fn register_pbr_material(
347        &mut self,
348        material_id: MaterialId,
349        material: &PbrMaterial,
350    ) -> Result<(), ValidationError> {
351        self.register_material(material_id, RendererMaterial::Pbr(material.clone()))
352    }
353
354    /// Registers a physical material.
355    pub fn register_physical_material(
356        &mut self,
357        material_id: MaterialId,
358        material: &PhysicalMaterial,
359    ) -> Result<(), ValidationError> {
360        self.register_material(material_id, RendererMaterial::Physical(material.clone()))
361    }
362
363    /// Registers an unlit material.
364    pub fn register_unlit_material(
365        &mut self,
366        material_id: MaterialId,
367        material: &UnlitMaterial,
368    ) -> Result<(), ValidationError> {
369        self.register_material(material_id, RendererMaterial::Unlit(material.clone()))
370    }
371
372    /// Registers a Lambert material.
373    pub fn register_lambert_material(
374        &mut self,
375        material_id: MaterialId,
376        material: &LambertMaterial,
377    ) -> Result<(), ValidationError> {
378        self.register_material(material_id, RendererMaterial::Lambert(material.clone()))
379    }
380
381    /// Registers a toon material.
382    pub fn register_toon_material(
383        &mut self,
384        material_id: MaterialId,
385        material: &ToonMaterial,
386    ) -> Result<(), ValidationError> {
387        self.register_material(material_id, RendererMaterial::Toon(material.clone()))
388    }
389
390    /// Registers a wireframe preview material.
391    pub fn register_wireframe_material(
392        &mut self,
393        material_id: MaterialId,
394        material: &WireframeMaterial,
395    ) -> Result<(), ValidationError> {
396        self.register_material(material_id, RendererMaterial::Wireframe(*material))
397    }
398
399    /// Registers a normal visualization material.
400    pub fn register_normal_material(
401        &mut self,
402        material_id: MaterialId,
403        material: &NormalMaterial,
404    ) -> Result<(), ValidationError> {
405        self.register_material(material_id, RendererMaterial::Normal(*material))
406    }
407
408    /// Registers a renderer material.
409    pub fn register_material(
410        &mut self,
411        material_id: MaterialId,
412        material: RendererMaterial,
413    ) -> Result<(), ValidationError> {
414        if material_id.is_null() {
415            return Err(ValidationError::InvalidId);
416        }
417        self.materials.insert(material_id, material);
418        Ok(())
419    }
420
421    /// Registers validated 2D texture metadata and sampler state.
422    pub fn register_texture2d(
423        &mut self,
424        texture_id: TextureId,
425        texture: &Texture2D,
426        sampler: Sampler,
427    ) -> Result<(), ValidationError> {
428        if texture_id.is_null() {
429            return Err(ValidationError::InvalidId);
430        }
431        texture.validate()?;
432        self.textures.insert(
433            texture_id,
434            GpuTexture {
435                width: texture.width,
436                height: texture.height,
437                format: texture.format,
438                wgpu_format: to_wgpu_texture_format(texture.format),
439                sampler,
440                mip_levels: texture.mip_levels.max(1),
441            },
442        );
443        Ok(())
444    }
445
446    /// Registers a light.
447    pub fn register_light(
448        &mut self,
449        light_id: LightId,
450        light: RendererLight,
451    ) -> Result<(), ValidationError> {
452        if light_id.is_null() {
453            return Err(ValidationError::InvalidId);
454        }
455        self.lights.insert(light_id, light);
456        Ok(())
457    }
458
459    /// Returns a mesh by ID.
460    #[inline]
461    pub fn mesh(&self, mesh_id: MeshId) -> Option<&GpuMesh> {
462        self.meshes.get(&mesh_id)
463    }
464
465    /// Returns a material by ID.
466    #[inline]
467    pub fn material(&self, material_id: MaterialId) -> Option<&RendererMaterial> {
468        self.materials.get(&material_id)
469    }
470
471    /// Returns texture metadata by ID.
472    #[inline]
473    pub fn texture(&self, texture_id: TextureId) -> Option<&GpuTexture> {
474        self.textures.get(&texture_id)
475    }
476
477    /// Returns registered texture metadata.
478    #[inline]
479    pub const fn textures(&self) -> &TextureStore {
480        &self.textures
481    }
482
483    /// Returns the number of registered lights.
484    #[inline]
485    pub fn light_count(&self) -> usize {
486        self.lights.len()
487    }
488
489    /// Returns the number of registered meshes.
490    #[inline]
491    pub fn mesh_count(&self) -> usize {
492        self.meshes.len()
493    }
494
495    /// Returns the number of registered materials.
496    #[inline]
497    pub fn material_count(&self) -> usize {
498        self.materials.len()
499    }
500}
501
502/// Converts scenix texture format metadata to a wgpu format.
503pub const fn to_wgpu_texture_format(format: TextureFormat) -> Option<wgpu::TextureFormat> {
504    match format {
505        TextureFormat::Rgba8Unorm => Some(wgpu::TextureFormat::Rgba8Unorm),
506        TextureFormat::Rgba8UnormSrgb => Some(wgpu::TextureFormat::Rgba8UnormSrgb),
507        TextureFormat::Rgba16Float => Some(wgpu::TextureFormat::Rgba16Float),
508        TextureFormat::Depth32Float => Some(wgpu::TextureFormat::Depth32Float),
509        TextureFormat::Bc7RgbaUnorm => Some(wgpu::TextureFormat::Bc7RgbaUnorm),
510        TextureFormat::Astc4x4RgbaUnorm => Some(wgpu::TextureFormat::Astc {
511            block: wgpu::AstcBlock::B4x4,
512            channel: wgpu::AstcChannel::Unorm,
513        }),
514        TextureFormat::Etc2Rgba8Unorm => Some(wgpu::TextureFormat::Etc2Rgba8Unorm),
515    }
516}
517
518/// Converts sampler filter modes to wgpu filter modes.
519pub const fn to_wgpu_filter_mode(filter: FilterMode) -> wgpu::FilterMode {
520    match filter {
521        FilterMode::Nearest => wgpu::FilterMode::Nearest,
522        FilterMode::Linear => wgpu::FilterMode::Linear,
523    }
524}
525
526/// Converts sampler address modes to wgpu address modes.
527pub const fn to_wgpu_address_mode(address: AddressMode) -> wgpu::AddressMode {
528    match address {
529        AddressMode::Repeat => wgpu::AddressMode::Repeat,
530        AddressMode::MirrorRepeat => wgpu::AddressMode::MirrorRepeat,
531        AddressMode::ClampToEdge => wgpu::AddressMode::ClampToEdge,
532    }
533}
534
535/// Converts optional compare state to wgpu compare state.
536pub const fn to_wgpu_compare(compare: Option<CompareFunction>) -> Option<wgpu::CompareFunction> {
537    match compare {
538        Some(CompareFunction::Less) => Some(wgpu::CompareFunction::Less),
539        Some(CompareFunction::LessEqual) => Some(wgpu::CompareFunction::LessEqual),
540        Some(CompareFunction::Greater) => Some(wgpu::CompareFunction::Greater),
541        Some(CompareFunction::GreaterEqual) => Some(wgpu::CompareFunction::GreaterEqual),
542        Some(CompareFunction::Equal) => Some(wgpu::CompareFunction::Equal),
543        Some(CompareFunction::NotEqual) => Some(wgpu::CompareFunction::NotEqual),
544        Some(CompareFunction::Always) => Some(wgpu::CompareFunction::Always),
545        Some(CompareFunction::Never) => Some(wgpu::CompareFunction::Never),
546        None => None,
547    }
548}