diff options
Diffstat (limited to 'client/src/render/scene')
-rw-r--r-- | client/src/render/scene/draw.rs | 104 | ||||
-rw-r--r-- | client/src/render/scene/meshops.rs | 81 | ||||
-rw-r--r-- | client/src/render/scene/mod.rs | 518 | ||||
-rw-r--r-- | client/src/render/scene/pipelines.rs | 196 | ||||
-rw-r--r-- | client/src/render/scene/textures.rs | 284 | ||||
-rw-r--r-- | client/src/render/scene/vertex_buffers.rs | 138 |
6 files changed, 1321 insertions, 0 deletions
diff --git a/client/src/render/scene/draw.rs b/client/src/render/scene/draw.rs new file mode 100644 index 0000000..dabb9cd --- /dev/null +++ b/client/src/render/scene/draw.rs @@ -0,0 +1,104 @@ +/* + wearechat - generic multiplayer game with voip + Copyright (C) 2025 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ +use super::{DemandMap, RPrefab}; +use glam::{EulerRot, Mat3, Mat4}; +use std::sync::Arc; +use weareshared::{packets::Resource, resources::Prefab, tree::SceneTree}; +use wgpu::{ + Color, CommandEncoder, IndexFormat, LoadOp, Operations, RenderPassColorAttachment, + RenderPassDepthStencilAttachment, RenderPassDescriptor, ShaderStages, StoreOp, TextureView, +}; + +pub struct ScenePipeline; + +impl ScenePipeline { + pub fn draw( + &mut self, + commands: &mut CommandEncoder, + target: &TextureView, + depth: &TextureView, + scene: &SceneTree, + prefabs: &DemandMap<Resource<Prefab>, Arc<RPrefab>>, + projection: Mat4, + ) { + let mut rpass = commands.begin_render_pass(&RenderPassDescriptor { + label: None, + color_attachments: &[Some(RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: Operations { + store: StoreOp::Store, + load: LoadOp::Clear(Color { + r: 0.01, + g: 0.01, + b: 0.01, + a: 1., + }), + }, + })], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &depth, + depth_ops: Some(Operations { + load: LoadOp::Clear(1.), + store: StoreOp::Store, + }), + stencil_ops: None, + }), + ..Default::default() + }); + + for ob in scene.objects.values() { + let prefab_projection = projection + * Mat4::from_translation(ob.pos.into()) + * Mat4::from_mat3(Mat3::from_euler( + EulerRot::YXZ, + ob.rot.x, + ob.rot.y, + ob.rot.z, + )); + if let Some(prefab) = prefabs.try_get(ob.res.clone()) { + for (affine, part) in &prefab.0 { + let part_projection = prefab_projection + * Mat4::from_translation(affine.translation.into()) + * Mat4::from_mat3a(affine.matrix3); + let projection = part_projection.to_cols_array().map(|v| v.to_le_bytes()); + let mb = affine.matrix3.to_cols_array(); // TODO apply object rotation + // add padding for gpu mat3x3 repr + let model_basis = [ + mb[0], mb[1], mb[2], 0., // + mb[3], mb[4], mb[5], 0., // + mb[6], mb[7], mb[8], 0., // + ]; + let model_basis = bytemuck::cast_slice(&model_basis); + + rpass.set_pipeline(&part.pipeline); + rpass.set_bind_group(0, &*part.tex_albedo, &[]); + rpass.set_bind_group(1, &*part.tex_normal, &[]); + rpass.set_bind_group(2, &*part.material, &[]); + rpass.set_push_constants(ShaderStages::VERTEX, 0, projection.as_flattened()); + rpass.set_push_constants(ShaderStages::VERTEX, 64, model_basis); + rpass.set_index_buffer(part.index.slice(..), IndexFormat::Uint32); + rpass.set_vertex_buffer(0, part.va_position.slice(..)); + rpass.set_vertex_buffer(1, part.va_normal.slice(..)); + rpass.set_vertex_buffer(2, part.va_tangent.slice(..)); + rpass.set_vertex_buffer(3, part.va_texcoord.slice(..)); + rpass.draw_indexed(0..part.index_count, 0, 0..1); + } + } + } + } +} diff --git a/client/src/render/scene/meshops.rs b/client/src/render/scene/meshops.rs new file mode 100644 index 0000000..0a3f963 --- /dev/null +++ b/client/src/render/scene/meshops.rs @@ -0,0 +1,81 @@ +/* + wearechat - generic multiplayer game with voip + Copyright (C) 2025 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ +use glam::{Vec2, Vec3, vec2}; + +pub fn generate_normals(index: &[[u32; 3]], position: &[Vec3]) -> Vec<Vec3> { + let mut normal_denom = vec![0; position.len()]; + let mut normal = vec![Vec3::ZERO; position.len()]; + + for &[a, b, c] in index { + let pos_a = position[a as usize]; + let pos_b = position[b as usize]; + let pos_c = position[c as usize]; + + // TODO is this right? + let norm = (pos_b - pos_a).cross(pos_c - pos_a).normalize(); + + normal[a as usize] += norm; + normal[b as usize] += norm; + normal[c as usize] += norm; + normal_denom[a as usize] += 1; + normal_denom[b as usize] += 1; + normal_denom[c as usize] += 1; + } + for (denom, norm) in normal_denom.iter().zip(normal.iter_mut()) { + *norm /= *denom as f32; + } + normal +} + +pub fn generate_tangents(index: &[[u32; 3]], position: &[Vec3], texcoord: &[Vec2]) -> Vec<Vec3> { + let mut tangent_denom = vec![0; position.len()]; + let mut tangent = vec![Vec3::ZERO; position.len()]; + + for &[a, b, c] in index { + let (pos_a, uv_a) = (position[a as usize], texcoord[a as usize]); + let (pos_b, uv_b) = (position[b as usize], texcoord[b as usize]); + let (pos_c, uv_c) = (position[c as usize], texcoord[c as usize]); + + let pd_ba = pos_b - pos_a; + let pd_ca = pos_c - pos_a; + let td_ba = uv_b - uv_a; + let td_ca = uv_c - uv_a; + + let face_tangent = + (pd_ba * td_ca.y - pd_ca * td_ba.y) * (td_ba.x * td_ca.y - td_ba.y * td_ca.x); + + tangent[a as usize] += face_tangent; + tangent[b as usize] += face_tangent; + tangent[c as usize] += face_tangent; + tangent_denom[a as usize] += 1; + tangent_denom[b as usize] += 1; + tangent_denom[c as usize] += 1; + } + for (denom, tang) in tangent_denom.iter().zip(tangent.iter_mut()) { + *tang /= *denom as f32; + } + + tangent +} + +pub fn generate_texcoords(index: &[[u32; 3]], position: &[Vec3]) -> Vec<Vec2> { + let _ = (index, position); + // TODO implement equirectangular projection or something + (0..position.len()) + .map(|i| vec2(i as f32 * 0.01, 0.)) + .collect() +} diff --git a/client/src/render/scene/mod.rs b/client/src/render/scene/mod.rs new file mode 100644 index 0000000..d83cb95 --- /dev/null +++ b/client/src/render/scene/mod.rs @@ -0,0 +1,518 @@ +/* + wearechat - generic multiplayer game with voip + Copyright (C) 2025 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ +pub mod draw; +pub mod meshops; +pub mod pipelines; +pub mod textures; +pub mod vertex_buffers; + +use crate::{armature::RArmature, download::Downloader, shaders::SceneShaders}; +use anyhow::Result; +use bytemuck::{Pod, Zeroable}; +use egui::{Grid, Widget}; +use glam::{UVec3, UVec4, Vec2, Vec3, Vec3A, uvec3, uvec4}; +use humansize::DECIMAL; +use log::{debug, trace}; +use pipelines::SceneBgLayouts; +use std::{ + collections::{HashMap, HashSet}, + hash::Hash, + marker::PhantomData, + sync::{Arc, RwLock}, + time::Instant, +}; +use textures::MipGenerationPipeline; +use weareshared::{ + Affine3A, + packets::Resource, + resources::{Image, MeshPart, Prefab}, +}; +use wgpu::{ + BindGroup, BindGroupDescriptor, BindGroupEntry, Buffer, BufferUsages, Device, Queue, + RenderPipeline, Texture, TextureFormat, + util::{BufferInitDescriptor, DeviceExt}, +}; + +pub struct DemandMap<K, V> { + inner: RwLock<DemandMapState<K, V>>, +} +struct DemandMapState<K, V> { + values: HashMap<K, V>, + needed: HashSet<K>, + size_metric: usize, +} +impl<K: Hash + Eq + Clone, V: Clone> DemandMap<K, V> { + pub fn new() -> Self { + Self { + inner: DemandMapState { + needed: HashSet::new(), + values: HashMap::new(), + size_metric: 0, + } + .into(), + } + } + pub fn needed(&self) -> Vec<K> { + self.inner.read().unwrap().needed.iter().cloned().collect() + } + pub fn insert(&self, key: K, value: V, size: usize) { + let mut s = self.inner.write().unwrap(); + s.needed.remove(&key); + s.values.insert(key, value); + s.size_metric += size; + } + pub fn try_get(&self, key: K) -> Option<V> { + let mut s = self.inner.write().unwrap(); + if let Some(k) = s.values.get(&key) { + Some(k.to_owned()) + } else { + s.needed.insert(key); + None + } + } +} + +struct GraphicsConfig { + max_anisotropy: u16, + max_mip_count: u32, +} + +pub struct ScenePreparer { + device: Arc<Device>, + queue: Arc<Queue>, + layouts: SceneBgLayouts, + shaders: SceneShaders, + render_format: TextureFormat, + config: GraphicsConfig, + downloader: Arc<Downloader>, + + textures: DemandMap<TextureSpec, (Arc<Texture>, Arc<BindGroup>)>, + placeholder_textures: DemandMap<TextureIdentityKind, (Arc<Texture>, Arc<BindGroup>)>, + index_buffers: DemandMap<Resource<Vec<[u32; 3]>>, (Arc<Buffer>, u32)>, + vertex_buffers: DemandMap<Resource<Vec<f32>>, Arc<Buffer>>, + generated_tangent_buffers: DemandMap<TangentBufferSpec, Arc<Buffer>>, + generated_normal_buffers: DemandMap<NormalBufferSpec, Arc<Buffer>>, + generated_texcoord_buffers: DemandMap<TexcoordBufferSpec, Arc<Buffer>>, + mesh_parts: DemandMap<Resource<MeshPart>, Arc<RMeshPart>>, + materials: DemandMap<Material, Arc<BindGroup>>, + pipelines: DemandMap<PipelineSpec, Arc<RenderPipeline>>, + mip_generation_pipelines: DemandMap<TextureFormat, Arc<MipGenerationPipeline>>, + pub prefabs: DemandMap<Resource<Prefab>, Arc<RPrefab>>, +} + +pub struct RPrefab(pub Vec<(Affine3A, Arc<RMeshPart>)>); +pub struct RMeshPart { + pub pipeline: Arc<RenderPipeline>, + pub index_count: u32, + pub index: Arc<Buffer>, + pub va_position: Arc<Buffer>, + pub va_normal: Arc<Buffer>, + pub va_tangent: Arc<Buffer>, + pub va_texcoord: Arc<Buffer>, + pub tex_albedo: Arc<BindGroup>, + pub tex_normal: Arc<BindGroup>, + pub material: Arc<BindGroup>, + pub double_sided: bool, + + pub va_joint_index: Option<Arc<Buffer>>, + pub va_joint_weight: Option<Arc<Buffer>>, + pub joint_uniform: Option<Arc<Buffer>>, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct PipelineSpec { + format: TextureFormat, + skin: bool, + backface_culling: bool, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +struct TextureSpec { + data: Resource<Image<'static>>, + linear: bool, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +struct TangentBufferSpec { + index: Resource<Vec<[u32; 3]>>, + position: Resource<Vec<Vec3>>, + texcoord: Option<Resource<Vec<Vec2>>>, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +struct NormalBufferSpec { + index: Resource<Vec<[u32; 3]>>, + position: Resource<Vec<Vec3>>, +} +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +struct TexcoordBufferSpec { + index: Resource<Vec<[u32; 3]>>, + position: Resource<Vec<Vec3>>, +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +enum TextureIdentityKind { + Normal, + Multiply, +} + +#[derive(Debug, Clone, Copy, Pod, Zeroable, Hash, PartialEq, Eq)] +#[repr(C)] +struct Material { + roughness: u32, + metallic: u32, + _pad1: [u32; 2], + albedo_alpha: UVec4, + emission: UVec3, + _pad2: u32, +} + +impl ScenePreparer { + pub fn new( + device: Arc<Device>, + queue: Arc<Queue>, + render_format: TextureFormat, + downloader: Arc<Downloader>, + ) -> Self { + Self { + // TODO normal mipmap requires linear texture, also demand map? + render_format, + config: GraphicsConfig { + max_anisotropy: 16, + max_mip_count: 16, + }, + layouts: SceneBgLayouts::load(&device), + shaders: SceneShaders::load(&device), + device, + queue, + downloader, + index_buffers: DemandMap::new(), + vertex_buffers: DemandMap::new(), + mesh_parts: DemandMap::new(), + prefabs: DemandMap::new(), + textures: DemandMap::new(), + placeholder_textures: DemandMap::new(), + generated_tangent_buffers: DemandMap::new(), + generated_normal_buffers: DemandMap::new(), + generated_texcoord_buffers: DemandMap::new(), + materials: DemandMap::new(), + pipelines: DemandMap::new(), + mip_generation_pipelines: DemandMap::new(), + } + } + pub fn update(&self) -> Result<usize> { + let mut num_done = 0; + + self.update_textures(&mut num_done)?; + self.update_vertex_buffers(&mut num_done)?; + + for pres in self.prefabs.needed() { + if let Some(prefab) = self.downloader.try_get(pres.clone())? { + let mut rprefab = RPrefab(Vec::new()); + for (aff, partres) in &prefab.mesh { + if let Some(part) = self.mesh_parts.try_get(partres.clone()) { + rprefab.0.push((*aff, part.clone())); + } + } + if rprefab.0.len() == prefab.mesh.len() { + self.prefabs.insert(pres.clone(), Arc::new(rprefab), 0); + debug!("prefab created ({pres})"); + num_done += 1; + } + } + } + + for spec in self.materials.needed() { + let buffer = self.device.create_buffer_init(&BufferInitDescriptor { + label: Some("material props"), + usage: BufferUsages::COPY_DST | BufferUsages::UNIFORM, + contents: bytemuck::cast_slice(&[spec]), + }); + let bind_group = self.device.create_bind_group(&BindGroupDescriptor { + label: Some("material"), + layout: &self.layouts.material, + entries: &[BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); + self.materials.insert(spec, Arc::new(bind_group), 0); + } + for spec in self.pipelines.needed() { + self.pipelines.insert( + spec.clone(), + Arc::new(spec.create(&self.device, &self.layouts, &self.shaders)), + 0, + ); + } + for pres in self.mesh_parts.needed() { + let start = Instant::now(); + if let Some(part) = self.downloader.try_get(pres.clone())? { + if let (Some(indexres), Some(positionres)) = (part.index, part.va_position) { + let index = self.index_buffers.try_get(indexres.clone()); + let position = self + .vertex_buffers + .try_get(Resource(positionres.0, PhantomData)); + + let normal = if let Some(res) = part.va_normal.clone() { + self.vertex_buffers.try_get(Resource(res.0, PhantomData)) + } else { + self.generated_normal_buffers.try_get(NormalBufferSpec { + index: indexres.clone(), + position: Resource(positionres.0, PhantomData), + }) + }; + + let texcoord = if let Some(res) = part.va_texcoord.clone() { + self.vertex_buffers.try_get(Resource(res.0, PhantomData)) + } else { + self.generated_texcoord_buffers.try_get(TexcoordBufferSpec { + index: indexres.clone(), + position: Resource(positionres.0, PhantomData), + }) + }; + let tangent = if let Some(res) = part.va_tangent.clone() { + self.vertex_buffers.try_get(Resource(res.0, PhantomData)) + } else { + self.generated_tangent_buffers.try_get(TangentBufferSpec { + index: indexres, + position: Resource(positionres.0, PhantomData), + texcoord: part.va_texcoord, + }) + }; + + let joint_weight = if let Some(res) = part.va_joint_weight.clone() { + self.vertex_buffers + .try_get(Resource(res.0, PhantomData)) + .map(Some) + } else { + Some(None) + }; + let joint_index = if let Some(res) = part.va_joint_index.clone() { + self.vertex_buffers + .try_get(Resource(res.0, PhantomData)) + .map(Some) + } else { + Some(None) + }; + + let mut tex_albedo = None; + if let Some(albedores) = part.tex_albedo { + if let Some((_tex, bg)) = self.textures.try_get(TextureSpec { + data: albedores, + linear: false, + }) { + tex_albedo = Some(bg) + } + } else { + if let Some((_tex, bg)) = self + .placeholder_textures + .try_get(TextureIdentityKind::Multiply) + { + tex_albedo = Some(bg) + } + } + let mut tex_normal = None; + if let Some(normalres) = part.tex_normal { + if let Some((_tex, bg)) = self.textures.try_get(TextureSpec { + data: normalres, + linear: true, + }) { + tex_normal = Some(bg) + } + } else { + if let Some((_tex, bg)) = self + .placeholder_textures + .try_get(TextureIdentityKind::Normal) + { + tex_normal = Some(bg) + } + } + + let material = self.materials.try_get({ + let albedo = part.g_albedo.unwrap_or(Vec3A::ONE); + let emission = part.g_emission.unwrap_or(Vec3A::ONE); + Material { + roughness: part.g_roughness.unwrap_or(1.).to_bits(), + metallic: part.g_metallic.unwrap_or(0.).to_bits(), + _pad1: [0, 0], + albedo_alpha: uvec4( + albedo.x.to_bits(), + albedo.y.to_bits(), + albedo.z.to_bits(), + part.g_alpha.unwrap_or(1.).to_bits(), + ), + emission: uvec3( + emission.x.to_bits(), + emission.y.to_bits(), + emission.z.to_bits(), + ), + _pad2: 0, + } + }); + + let armature = if let Some(res) = part.armature.clone() { + Some(self.downloader.try_get(res)?) + } else { + Some(None) + }; + + let pipeline = self.pipelines.try_get(PipelineSpec { + format: self.render_format, + skin: false, + backface_culling: part.g_double_sided.is_none(), + }); + + if let ( + Some(pipeline), + Some((index, index_count)), + Some(va_normal), + Some(va_tangent), + Some(va_texcoord), + Some(va_position), + Some(va_joint_index), + Some(va_joint_weight), + Some(armature), + Some(tex_normal), + Some(tex_albedo), + Some(material), + ) = ( + pipeline, + index, + normal, + tangent, + texcoord, + position, + joint_index, + joint_weight, + armature, + tex_normal, + tex_albedo, + material, + ) { + let double_sided = part.g_double_sided.is_some(); + + let joint_uniform = if let Some(a) = armature { + let ra = RArmature::new(&self.device, a); + Some(ra.joint_mat_uniform_buffer.clone()) + } else { + None + }; + + debug!("part created (took {:?}) {pres}", start.elapsed()); + self.mesh_parts.insert( + pres, + Arc::new(RMeshPart { + pipeline, + index_count, + index, + va_normal, + va_tangent, + va_position, + va_texcoord, + va_joint_index, + va_joint_weight, + tex_albedo, + tex_normal, + material, + double_sided, + joint_uniform, + }), + 0, + ); + num_done += 1; + } + } + } + } + self.print_missing(); + Ok(num_done) + } +} + +impl<K, V> Widget for &DemandMap<K, V> { + fn ui(self, ui: &mut egui::Ui) -> egui::Response { + let state = self.inner.read().unwrap(); + ui.label(state.needed.len().to_string()); + ui.label(state.values.len().to_string()); + ui.label(humansize::format_size(state.size_metric, DECIMAL)); + ui.end_row(); + ui.response() + } +} +impl ScenePreparer { + pub fn print_missing(&self) { + fn visit<K, V>(name: &str, m: &DemandMap<K, V>) + where + K: Clone, + K: Hash, + K: std::cmp::Eq, + V: Clone, + { + let nl = m.needed().len(); + if nl > 0 { + trace!("{name}: need {nl}") + } + } + visit("prefabs", &self.prefabs); + visit("mesh_parts", &self.mesh_parts); + visit("vertex_buffers", &self.vertex_buffers); + visit("index_buffers", &self.index_buffers); + visit("placeholder_textures", &self.placeholder_textures); + visit("generated_tangent_buffers", &self.generated_tangent_buffers); + visit("generated_normal_buffers", &self.generated_normal_buffers); + visit( + "generated_texcoord_buffers", + &self.generated_texcoord_buffers, + ); + visit("textures", &self.textures); + visit("materials", &self.materials); + visit("pipelines", &self.pipelines); + } +} + +impl Widget for &ScenePreparer { + fn ui(self, ui: &mut egui::Ui) -> egui::Response { + Grid::new("sp") + .num_columns(4) + .show(ui, |ui| { + ui.label("prefabs"); + self.prefabs.ui(ui); + ui.label("mesh_parts"); + self.mesh_parts.ui(ui); + ui.label("vertex_buffers"); + self.vertex_buffers.ui(ui); + ui.label("index_buffers"); + self.index_buffers.ui(ui); + ui.label("placeholder_textures"); + self.placeholder_textures.ui(ui); + ui.label("generated_tangent_buffers"); + self.generated_tangent_buffers.ui(ui); + ui.label("generated_normal_buffers"); + self.generated_normal_buffers.ui(ui); + ui.label("generated_texcoord_buffers"); + self.generated_texcoord_buffers.ui(ui); + ui.label("textures"); + self.textures.ui(ui); + ui.label("materials"); + self.materials.ui(ui); + ui.label("pipelines"); + self.pipelines.ui(ui); + }) + .response + } +} diff --git a/client/src/render/scene/pipelines.rs b/client/src/render/scene/pipelines.rs new file mode 100644 index 0000000..3b6758e --- /dev/null +++ b/client/src/render/scene/pipelines.rs @@ -0,0 +1,196 @@ +/* + wearechat - generic multiplayer game with voip + Copyright (C) 2025 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ +use wgpu::{ + BindGroupLayout, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BlendState, + BufferBindingType, ColorTargetState, ColorWrites, CompareFunction, DepthBiasState, + DepthStencilState, Device, Face, FragmentState, FrontFace, MultisampleState, + PipelineCompilationOptions, PipelineLayoutDescriptor, PolygonMode, PrimitiveState, + PrimitiveTopology, PushConstantRange, RenderPipeline, RenderPipelineDescriptor, + SamplerBindingType, ShaderStages, StencilState, TextureFormat, TextureSampleType, + TextureViewDimension, VertexAttribute, VertexBufferLayout, VertexFormat, VertexState, + VertexStepMode, +}; + +use crate::shaders::SceneShaders; + +use super::PipelineSpec; + +pub struct SceneBgLayouts { + pub texture: BindGroupLayout, + pub material: BindGroupLayout, + pub joints: BindGroupLayout, +} + +impl SceneBgLayouts { + pub fn load(device: &Device) -> Self { + Self { + texture: device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[ + BindGroupLayoutEntry { + binding: 0, + count: None, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2, + multisampled: false, + }, + }, + BindGroupLayoutEntry { + binding: 1, + count: None, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler(SamplerBindingType::Filtering), + }, + ], + label: None, + }), + material: device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[BindGroupLayoutEntry { + binding: 0, + count: None, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + }], + label: None, + }), + joints: device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[BindGroupLayoutEntry { + binding: 0, + count: None, + visibility: ShaderStages::VERTEX, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + }], + label: None, + }), + } + } +} + +impl PipelineSpec { + pub fn create( + &self, + device: &Device, + layouts: &SceneBgLayouts, + shaders: &SceneShaders, + ) -> RenderPipeline { + let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[&layouts.texture, &layouts.texture, &layouts.material], + push_constant_ranges: &[PushConstantRange { + // 4x4 view projections + // 3x3(+1 pad) model basis + range: 0..((4 * 4 + 3 * 4) * size_of::<f32>() as u32), + stages: ShaderStages::VERTEX, + }], + }); + device.create_render_pipeline(&RenderPipelineDescriptor { + label: None, + layout: Some(&pipeline_layout), + fragment: Some(FragmentState { + module: &shaders.fragment_pbr, + entry_point: Some("main"), + targets: &[Some(ColorTargetState { + blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING), + format: self.format, + write_mask: ColorWrites::all(), + })], + compilation_options: PipelineCompilationOptions::default(), + }), + vertex: VertexState { + module: if self.skin { + &shaders.vertex_world_skin + } else { + &shaders.vertex_world + }, + entry_point: Some("main"), + buffers: &[ + // position + VertexBufferLayout { + step_mode: VertexStepMode::Vertex, + array_stride: 3 * size_of::<f32>() as u64, + attributes: &[VertexAttribute { + format: VertexFormat::Float32x3, + offset: 0, + shader_location: 0, + }], + }, + // normal + VertexBufferLayout { + step_mode: VertexStepMode::Vertex, + array_stride: 3 * size_of::<f32>() as u64, + attributes: &[VertexAttribute { + format: VertexFormat::Float32x3, + offset: 0, + shader_location: 1, + }], + }, + // tangent + VertexBufferLayout { + step_mode: VertexStepMode::Vertex, + array_stride: 3 * size_of::<f32>() as u64, + attributes: &[VertexAttribute { + format: VertexFormat::Float32x3, + offset: 0, + shader_location: 2, + }], + }, + // texcoord + VertexBufferLayout { + step_mode: VertexStepMode::Vertex, + array_stride: 2 * size_of::<f32>() as u64, + attributes: &[VertexAttribute { + format: VertexFormat::Float32x2, + offset: 0, + shader_location: 3, + }], + }, + ], + compilation_options: PipelineCompilationOptions::default(), + }, + primitive: PrimitiveState { + topology: PrimitiveTopology::TriangleList, + front_face: FrontFace::Ccw, + cull_mode: if self.backface_culling { + Some(Face::Back) + } else { + None + }, + polygon_mode: PolygonMode::Fill, + ..Default::default() + }, + depth_stencil: Some(DepthStencilState { + depth_write_enabled: true, + depth_compare: CompareFunction::Less, + format: TextureFormat::Depth32Float, + bias: DepthBiasState::default(), + stencil: StencilState::default(), + }), + multisample: MultisampleState::default(), + multiview: None, + cache: None, + }) + } +} diff --git a/client/src/render/scene/textures.rs b/client/src/render/scene/textures.rs new file mode 100644 index 0000000..0a042bd --- /dev/null +++ b/client/src/render/scene/textures.rs @@ -0,0 +1,284 @@ +/* + wearechat - generic multiplayer game with voip + Copyright (C) 2025 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ +use super::{GraphicsConfig, ScenePreparer, TextureIdentityKind}; +use anyhow::Result; +use image::ImageReader; +use log::debug; +use std::{io::Cursor, sync::Arc, time::Instant}; +use wgpu::{ + AddressMode, BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindingResource, + Color, ColorTargetState, ColorWrites, CommandEncoderDescriptor, Device, Extent3d, FilterMode, + ImageDataLayout, LoadOp, Operations, Queue, RenderPassColorAttachment, RenderPassDescriptor, + RenderPipeline, SamplerDescriptor, StoreOp, Texture, TextureAspect, TextureDescriptor, + TextureDimension, TextureFormat, TextureUsages, TextureViewDescriptor, include_wgsl, +}; + +pub struct MipGenerationPipeline { + pipeline: RenderPipeline, +} +impl MipGenerationPipeline { + pub fn load(device: &Device, format: TextureFormat) -> Self { + let shader = device.create_shader_module(include_wgsl!("../../shaders/texture_copy.wgsl")); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("mip generator"), + layout: None, + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + compilation_options: Default::default(), + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + compilation_options: Default::default(), + targets: &[Some(ColorTargetState { + format, + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + cache: None, + }); + Self { pipeline } + } +} + +impl ScenePreparer { + pub fn update_textures(&self, num_done: &mut usize) -> Result<()> { + for format in self.mip_generation_pipelines.needed() { + self.mip_generation_pipelines.insert( + format, + Arc::new(MipGenerationPipeline::load(&self.device, format)), + 0, + ); + *num_done += 1 + } + + for kind in self.placeholder_textures.needed() { + let (linear, color) = match kind { + TextureIdentityKind::Normal => (true, [128, 128, 255, 255]), + TextureIdentityKind::Multiply => (false, [255, 255, 255, 255]), + }; + let tex_bg = create_texture( + &self.device, + &self.queue, + &self.layouts.texture, + &color, + 1, + 1, + if linear { + TextureFormat::Rgba8Unorm + } else { + TextureFormat::Rgba8UnormSrgb + }, + None, + &self.config, + ); + self.placeholder_textures.insert(kind, tex_bg, 4); + *num_done += 1; + } + for spec in self.textures.needed() { + let start = Instant::now(); + let format = if spec.linear { + TextureFormat::Rgba8Unorm + } else { + TextureFormat::Rgba8UnormSrgb + }; + if let Some(mipgen) = self.mip_generation_pipelines.try_get(format) { + if let Some(buf) = self.downloader.try_get(spec.data.clone())? { + let image = ImageReader::new(Cursor::new(buf.0)).with_guessed_format()?; + let image = image.decode()?; + let dims = (image.width(), image.height()); + let image = image.into_rgba8(); + let image = image.into_vec(); + let tex_bg = create_texture( + &self.device, + &self.queue, + &self.layouts.texture, + &image, + dims.0, + dims.1, + format, + Some(&mipgen), + &self.config, + ); + self.textures.insert(spec, tex_bg, image.len()); + debug!( + "texture created (res={}x{}, took {:?})", + dims.0, + dims.1, + start.elapsed() + ); + *num_done += 1; + } + } + } + Ok(()) + } +} + +fn create_texture( + device: &Device, + queue: &Queue, + bgl: &BindGroupLayout, + data: &[u8], + width: u32, + height: u32, + format: TextureFormat, + mipgen: Option<&MipGenerationPipeline>, + config: &GraphicsConfig, +) -> (Arc<Texture>, Arc<BindGroup>) { + let mip_level_count = (width.ilog2().max(4) - 3).min(config.max_mip_count); + + let extent = Extent3d { + depth_or_array_layers: 1, + width, + height, + }; + let texture = device.create_texture(&TextureDescriptor { + label: None, + size: extent, + mip_level_count, + sample_count: 1, + dimension: TextureDimension::D2, + format, + usage: TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_DST + | TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + let textureview = texture.create_view(&TextureViewDescriptor::default()); + let sampler = device.create_sampler(&SamplerDescriptor { + address_mode_u: AddressMode::Repeat, + address_mode_v: AddressMode::Repeat, + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + mipmap_filter: FilterMode::Linear, + anisotropy_clamp: config.max_anisotropy, + ..Default::default() + }); + let bind_group = device.create_bind_group(&BindGroupDescriptor { + label: None, + layout: &bgl, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(&textureview), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&sampler), + }, + ], + }); + + let level_views = (0..mip_level_count) + .map(|mip| { + texture.create_view(&TextureViewDescriptor { + label: Some("mip generation level view"), + format: None, + dimension: None, + aspect: TextureAspect::All, + base_mip_level: mip, + mip_level_count: Some(1), + base_array_layer: 0, + array_layer_count: None, + }) + }) + .collect::<Vec<_>>(); + + let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor { label: None }); + + // TODO why does copy_buffer_to_texture have more restrictive alignment requirements?! + // let upload_buffer = device.create_buffer_init(&BufferInitDescriptor { + // label: Some("texture upload"), + // contents: data, + // usage: BufferUsages::COPY_DST | BufferUsages::COPY_SRC, + // }); + // encoder.copy_buffer_to_texture( + // ImageCopyBuffer { + // buffer: &upload_buffer, + // layout: ImageDataLayout { + // offset: 0, + // bytes_per_row: Some(width * 4), + // rows_per_image: None, + // }, + // }, + // texture.as_image_copy(), + // extent, + // ); + queue.write_texture( + texture.as_image_copy(), + data, + ImageDataLayout { + bytes_per_row: Some(width * 4), + rows_per_image: None, + offset: 0, + }, + extent, + ); + + for level in 1..mip_level_count { + let mip_pipeline = &mipgen.unwrap().pipeline; + let source_view = &level_views[level as usize - 1]; + let target_view = &level_views[level as usize]; + let mip_bind_group = device.create_bind_group(&BindGroupDescriptor { + layout: &mip_pipeline.get_bind_group_layout(0), + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(source_view), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&sampler), + }, + ], + label: None, + }); + let mut rpass = encoder.begin_render_pass(&RenderPassDescriptor { + label: None, + color_attachments: &[Some(RenderPassColorAttachment { + view: target_view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(Color::WHITE), + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + rpass.set_pipeline(&mip_pipeline); + rpass.set_bind_group(0, &mip_bind_group, &[]); + rpass.draw(0..3, 0..1); + } + + queue.submit(Some(encoder.finish())); + + (Arc::new(texture), Arc::new(bind_group)) +} diff --git a/client/src/render/scene/vertex_buffers.rs b/client/src/render/scene/vertex_buffers.rs new file mode 100644 index 0000000..0c34746 --- /dev/null +++ b/client/src/render/scene/vertex_buffers.rs @@ -0,0 +1,138 @@ +/* + wearechat - generic multiplayer game with voip + Copyright (C) 2025 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ +use super::{meshops::{generate_normals, generate_tangents, generate_texcoords}, ScenePreparer}; +use anyhow::Result; +use log::debug; +use std::{sync::Arc, time::Instant}; +use wgpu::{ + BufferUsages, + util::{BufferInitDescriptor, DeviceExt}, +}; + +impl ScenePreparer { + pub fn update_vertex_buffers(&self, num_done: &mut usize) -> Result<()> { + for pres in self.index_buffers.needed() { + let start = Instant::now(); + if let Some(buf) = self.downloader.try_get(pres.clone())? { + let buffer = self.device.create_buffer_init(&BufferInitDescriptor { + label: Some("index"), + contents: bytemuck::cast_slice(buf.as_slice()), + usage: BufferUsages::INDEX | BufferUsages::COPY_DST, + }); + self.index_buffers.insert( + pres.clone(), + (Arc::new(buffer), (buf.len() * 3) as u32), + buf.len() * size_of::<u32>() * 3, + ); + debug!( + "index buffer created (len={}, took {:?}) {pres}", + buf.len() / size_of::<u32>(), + start.elapsed(), + ); + *num_done += 1; + } + } + for pres in self.vertex_buffers.needed() { + let start = Instant::now(); + if let Some(buf) = self.downloader.try_get(pres.clone())? { + let buffer = self.device.create_buffer_init(&BufferInitDescriptor { + contents: bytemuck::cast_slice(buf.as_slice()), + label: Some("vertex attribute"), + usage: BufferUsages::VERTEX | BufferUsages::COPY_DST, + }); + self.vertex_buffers.insert( + pres.clone(), + Arc::new(buffer), + buf.len() * size_of::<f32>(), + ); + debug!( + "vertex attribute buffer created (len={}, took {:?}) {pres}", + buf.len() / size_of::<f32>(), + start.elapsed() + ); + *num_done += 1; + } + } + + for spec in self.generated_tangent_buffers.needed() { + if let (Some(index), Some(position), texcoord) = ( + self.downloader.try_get(spec.index.clone())?, + self.downloader.try_get(spec.position.clone())?, + spec.texcoord + .clone() + .map(|r| self.downloader.try_get(r)) + .transpose()?, + ) { + let texcoord = match texcoord { + Some(Some(x)) => Some(x), + Some(None) => continue, // tangents provided but still loading + None => None, + }; + let texcoord = texcoord.unwrap_or_else(|| generate_texcoords(&index, &position)); + let tangents = generate_tangents(&index, &position, &texcoord); + let buffer = self.device.create_buffer_init(&BufferInitDescriptor { + label: Some("generated tangent"), + usage: BufferUsages::COPY_DST | BufferUsages::VERTEX, + contents: bytemuck::cast_slice(tangents.as_slice()), + }); + self.generated_tangent_buffers.insert( + spec, + Arc::new(buffer), + size_of::<f32>() * tangents.len() * 3, + ); + } + } + for spec in self.generated_normal_buffers.needed() { + if let (Some(index), Some(position)) = ( + self.downloader.try_get(spec.index.clone())?, + self.downloader.try_get(spec.position.clone())?, + ) { + let normals = generate_normals(&index, &position); + let buffer = self.device.create_buffer_init(&BufferInitDescriptor { + label: Some("generated normal"), + usage: BufferUsages::COPY_DST | BufferUsages::VERTEX, + contents: bytemuck::cast_slice(normals.as_slice()), + }); + self.generated_normal_buffers.insert( + spec, + Arc::new(buffer), + size_of::<f32>() * normals.len() * 3, + ); + } + } + for spec in self.generated_texcoord_buffers.needed() { + if let (Some(index), Some(position)) = ( + self.downloader.try_get(spec.index.clone())?, + self.downloader.try_get(spec.position.clone())?, + ) { + let texcoords = generate_texcoords(&index, &position); + let buffer = self.device.create_buffer_init(&BufferInitDescriptor { + label: Some("generated texcoord"), + usage: BufferUsages::COPY_DST | BufferUsages::VERTEX, + contents: bytemuck::cast_slice(texcoords.as_slice()), + }); + self.generated_texcoord_buffers.insert( + spec, + Arc::new(buffer), + size_of::<f32>() * texcoords.len() * 3, + ); + } + } + + Ok(()) + } +} |