use std::collections::HashMap;
use scenix_core::{Color, LightId, MaterialId, MeshId, TextureId, ValidationError};
use scenix_light::{AmbientLight, DirectionalLight, PointLight, SpotLight};
use scenix_material::{LambertMaterial, Material, PbrMaterial, PipelineKey, UnlitMaterial};
use scenix_math::{Aabb, Mat4, Vec2, Vec3, Vec4};
use scenix_mesh::Geometry;
use scenix_texture::{AddressMode, CompareFunction, FilterMode, Sampler, Texture2D, TextureFormat};
use wgpu::util::DeviceExt;
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct PackedVertex {
pub position: [f32; 3],
pub normal: [f32; 3],
pub uv: [f32; 2],
pub color: [f32; 4],
pub tangent: [f32; 4],
}
impl PackedVertex {
pub const fn layout() -> wgpu::VertexBufferLayout<'static> {
const ATTRIBUTES: [wgpu::VertexAttribute; 5] = wgpu::vertex_attr_array![
0 => Float32x3,
1 => Float32x3,
2 => Float32x2,
3 => Float32x4,
4 => Float32x4
];
wgpu::VertexBufferLayout {
array_stride: core::mem::size_of::<PackedVertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &ATTRIBUTES,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum GpuIndexFormat {
Uint16,
Uint32,
}
impl GpuIndexFormat {
#[inline]
pub const fn to_wgpu(self) -> wgpu::IndexFormat {
match self {
Self::Uint16 => wgpu::IndexFormat::Uint16,
Self::Uint32 => wgpu::IndexFormat::Uint32,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct PackedGeometry {
pub vertices: Vec<PackedVertex>,
pub index_bytes: Vec<u8>,
pub index_count: u32,
pub index_format: GpuIndexFormat,
pub aabb: Aabb,
}
#[derive(Debug)]
pub struct GpuMesh {
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
packed: PackedGeometry,
}
impl GpuMesh {
#[inline]
pub const fn vertex_buffer(&self) -> &wgpu::Buffer {
&self.vertex_buffer
}
#[inline]
pub const fn index_buffer(&self) -> &wgpu::Buffer {
&self.index_buffer
}
#[inline]
pub const fn packed(&self) -> &PackedGeometry {
&self.packed
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum RendererMaterial {
Pbr(PbrMaterial),
Unlit(UnlitMaterial),
Lambert(LambertMaterial),
}
impl RendererMaterial {
#[inline]
pub fn pipeline_key(&self) -> PipelineKey {
match self {
Self::Pbr(material) => material.pipeline_key(),
Self::Unlit(material) => material.pipeline_key(),
Self::Lambert(material) => material.pipeline_key(),
}
}
#[inline]
pub fn is_transparent(&self) -> bool {
match self {
Self::Pbr(material) => material.is_transparent(),
Self::Unlit(material) => material.is_transparent(),
Self::Lambert(material) => material.is_transparent(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GpuTexture {
pub width: u32,
pub height: u32,
pub format: TextureFormat,
pub wgpu_format: Option<wgpu::TextureFormat>,
pub sampler: Sampler,
pub mip_levels: u32,
}
pub type TextureStore = HashMap<TextureId, GpuTexture>;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum RendererLight {
Ambient(AmbientLight),
Directional(DirectionalLight),
Point(PointLight),
Spot(SpotLight),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DrawSubmission {
pub mesh_id: MeshId,
pub material_id: MaterialId,
pub world_matrix: Mat4,
pub world_aabb: Aabb,
pub distance_to_camera: f32,
pub transparent: bool,
pub render_order: u32,
}
#[derive(Debug, Default)]
pub struct GpuScene {
meshes: HashMap<MeshId, GpuMesh>,
materials: HashMap<MaterialId, RendererMaterial>,
textures: TextureStore,
lights: HashMap<LightId, RendererLight>,
}
impl GpuScene {
#[inline]
pub fn new() -> Self {
Self::default()
}
pub fn pack_geometry(geometry: &Geometry) -> Result<PackedGeometry, ValidationError> {
geometry.validate()?;
let vertex_count = geometry.positions.len();
let mut vertices = Vec::with_capacity(vertex_count);
for index in 0..vertex_count {
let position = geometry.positions[index];
let normal = geometry.normals.get(index).copied().unwrap_or(Vec3::Y);
let uv = geometry.uvs.get(index).copied().unwrap_or(Vec2::ZERO);
let color = geometry.colors.get(index).copied().unwrap_or(Color::WHITE);
let tangent = geometry
.tangents
.get(index)
.copied()
.unwrap_or(Vec4::new(1.0, 0.0, 0.0, 1.0));
vertices.push(PackedVertex {
position: [position.x, position.y, position.z],
normal: [normal.x, normal.y, normal.z],
uv: [uv.x, uv.y],
color: color.to_array(),
tangent: [tangent.x, tangent.y, tangent.z, tangent.w],
});
}
let source_indices: Vec<u32> = if geometry.indices.is_empty() {
(0..vertex_count as u32).collect()
} else {
geometry.indices.clone()
};
let can_use_u16 = vertex_count <= u16::MAX as usize
&& source_indices.iter().all(|index| *index <= u16::MAX as u32);
let (index_bytes, index_format) = if can_use_u16 {
let indices: Vec<u16> = source_indices.iter().map(|index| *index as u16).collect();
(
bytemuck::cast_slice(&indices).to_vec(),
GpuIndexFormat::Uint16,
)
} else {
(
bytemuck::cast_slice(source_indices.as_slice()).to_vec(),
GpuIndexFormat::Uint32,
)
};
Ok(PackedGeometry {
vertices,
index_bytes,
index_count: source_indices.len() as u32,
index_format,
aabb: geometry.aabb(),
})
}
pub fn register_mesh(
&mut self,
device: &wgpu::Device,
mesh_id: MeshId,
geometry: &Geometry,
) -> Result<(), ValidationError> {
if mesh_id.is_null() {
return Err(ValidationError::InvalidId);
}
let packed = Self::pack_geometry(geometry)?;
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("scenix.mesh.vertices"),
contents: bytemuck::cast_slice(packed.vertices.as_slice()),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("scenix.mesh.indices"),
contents: packed.index_bytes.as_slice(),
usage: wgpu::BufferUsages::INDEX,
});
self.meshes.insert(
mesh_id,
GpuMesh {
vertex_buffer,
index_buffer,
packed,
},
);
Ok(())
}
pub fn register_pbr_material(
&mut self,
material_id: MaterialId,
material: &PbrMaterial,
) -> Result<(), ValidationError> {
self.register_material(material_id, RendererMaterial::Pbr(material.clone()))
}
pub fn register_unlit_material(
&mut self,
material_id: MaterialId,
material: &UnlitMaterial,
) -> Result<(), ValidationError> {
self.register_material(material_id, RendererMaterial::Unlit(material.clone()))
}
pub fn register_lambert_material(
&mut self,
material_id: MaterialId,
material: &LambertMaterial,
) -> Result<(), ValidationError> {
self.register_material(material_id, RendererMaterial::Lambert(material.clone()))
}
pub fn register_material(
&mut self,
material_id: MaterialId,
material: RendererMaterial,
) -> Result<(), ValidationError> {
if material_id.is_null() {
return Err(ValidationError::InvalidId);
}
self.materials.insert(material_id, material);
Ok(())
}
pub fn register_texture2d(
&mut self,
texture_id: TextureId,
texture: &Texture2D,
sampler: Sampler,
) -> Result<(), ValidationError> {
if texture_id.is_null() {
return Err(ValidationError::InvalidId);
}
texture.validate()?;
self.textures.insert(
texture_id,
GpuTexture {
width: texture.width,
height: texture.height,
format: texture.format,
wgpu_format: to_wgpu_texture_format(texture.format),
sampler,
mip_levels: texture.mip_levels.max(1),
},
);
Ok(())
}
pub fn register_light(
&mut self,
light_id: LightId,
light: RendererLight,
) -> Result<(), ValidationError> {
if light_id.is_null() {
return Err(ValidationError::InvalidId);
}
self.lights.insert(light_id, light);
Ok(())
}
#[inline]
pub fn mesh(&self, mesh_id: MeshId) -> Option<&GpuMesh> {
self.meshes.get(&mesh_id)
}
#[inline]
pub fn material(&self, material_id: MaterialId) -> Option<&RendererMaterial> {
self.materials.get(&material_id)
}
#[inline]
pub fn texture(&self, texture_id: TextureId) -> Option<&GpuTexture> {
self.textures.get(&texture_id)
}
#[inline]
pub const fn textures(&self) -> &TextureStore {
&self.textures
}
#[inline]
pub fn light_count(&self) -> usize {
self.lights.len()
}
#[inline]
pub fn mesh_count(&self) -> usize {
self.meshes.len()
}
#[inline]
pub fn material_count(&self) -> usize {
self.materials.len()
}
}
pub const fn to_wgpu_texture_format(format: TextureFormat) -> Option<wgpu::TextureFormat> {
match format {
TextureFormat::Rgba8Unorm => Some(wgpu::TextureFormat::Rgba8Unorm),
TextureFormat::Rgba8UnormSrgb => Some(wgpu::TextureFormat::Rgba8UnormSrgb),
TextureFormat::Rgba16Float => Some(wgpu::TextureFormat::Rgba16Float),
TextureFormat::Depth32Float => Some(wgpu::TextureFormat::Depth32Float),
TextureFormat::Bc7RgbaUnorm => Some(wgpu::TextureFormat::Bc7RgbaUnorm),
TextureFormat::Astc4x4RgbaUnorm => Some(wgpu::TextureFormat::Astc {
block: wgpu::AstcBlock::B4x4,
channel: wgpu::AstcChannel::Unorm,
}),
TextureFormat::Etc2Rgba8Unorm => Some(wgpu::TextureFormat::Etc2Rgba8Unorm),
}
}
pub const fn to_wgpu_filter_mode(filter: FilterMode) -> wgpu::FilterMode {
match filter {
FilterMode::Nearest => wgpu::FilterMode::Nearest,
FilterMode::Linear => wgpu::FilterMode::Linear,
}
}
pub const fn to_wgpu_address_mode(address: AddressMode) -> wgpu::AddressMode {
match address {
AddressMode::Repeat => wgpu::AddressMode::Repeat,
AddressMode::MirrorRepeat => wgpu::AddressMode::MirrorRepeat,
AddressMode::ClampToEdge => wgpu::AddressMode::ClampToEdge,
}
}
pub const fn to_wgpu_compare(compare: Option<CompareFunction>) -> Option<wgpu::CompareFunction> {
match compare {
Some(CompareFunction::Less) => Some(wgpu::CompareFunction::Less),
Some(CompareFunction::LessEqual) => Some(wgpu::CompareFunction::LessEqual),
Some(CompareFunction::Greater) => Some(wgpu::CompareFunction::Greater),
Some(CompareFunction::GreaterEqual) => Some(wgpu::CompareFunction::GreaterEqual),
Some(CompareFunction::Equal) => Some(wgpu::CompareFunction::Equal),
Some(CompareFunction::NotEqual) => Some(wgpu::CompareFunction::NotEqual),
Some(CompareFunction::Always) => Some(wgpu::CompareFunction::Always),
Some(CompareFunction::Never) => Some(wgpu::CompareFunction::Never),
None => None,
}
}