From 66930534a0647e2613360658a6a99eed945e2f0f Mon Sep 17 00:00:00 2001 From: metamuffin Date: Sun, 26 Jan 2025 15:10:37 +0100 Subject: move files around, graphics config, msaa --- client/src/interfaces/mod.rs | 4 +- client/src/main.rs | 2 - client/src/render/mod.rs | 130 ++++-- client/src/render/scene/draw.rs | 5 +- client/src/render/scene/mod.rs | 16 +- client/src/render/scene/pipelines.rs | 12 +- client/src/render/scene/textures.rs | 2 +- client/src/render/shaders/fragment_pbr.wgsl | 64 +++ client/src/render/shaders/fragment_ui.wgsl | 28 ++ client/src/render/shaders/mod.rs | 37 ++ client/src/render/shaders/texture_copy.wgsl | 37 ++ client/src/render/shaders/vertex_ui.wgsl | 43 ++ client/src/render/shaders/vertex_world.wgsl | 48 ++ client/src/render/shaders/vertex_world_skin.wgsl | 58 +++ client/src/render/ui.rs | 535 +++++++++++++++++++++++ client/src/shaders/fragment_pbr.wgsl | 64 --- client/src/shaders/fragment_ui.wgsl | 28 -- client/src/shaders/mod.rs | 21 - client/src/shaders/texture_copy.wgsl | 37 -- client/src/shaders/vertex_ui.wgsl | 43 -- client/src/shaders/vertex_world.wgsl | 48 -- client/src/shaders/vertex_world_skin.wgsl | 58 --- client/src/state.rs | 7 +- client/src/ui.rs | 523 ---------------------- 24 files changed, 973 insertions(+), 877 deletions(-) create mode 100644 client/src/render/shaders/fragment_pbr.wgsl create mode 100644 client/src/render/shaders/fragment_ui.wgsl create mode 100644 client/src/render/shaders/mod.rs create mode 100644 client/src/render/shaders/texture_copy.wgsl create mode 100644 client/src/render/shaders/vertex_ui.wgsl create mode 100644 client/src/render/shaders/vertex_world.wgsl create mode 100644 client/src/render/shaders/vertex_world_skin.wgsl create mode 100644 client/src/render/ui.rs delete mode 100644 client/src/shaders/fragment_pbr.wgsl delete mode 100644 client/src/shaders/fragment_ui.wgsl delete mode 100644 client/src/shaders/mod.rs delete mode 100644 client/src/shaders/texture_copy.wgsl delete mode 100644 client/src/shaders/vertex_ui.wgsl delete mode 100644 client/src/shaders/vertex_world.wgsl delete mode 100644 client/src/shaders/vertex_world_skin.wgsl delete mode 100644 client/src/ui.rs diff --git a/client/src/interfaces/mod.rs b/client/src/interfaces/mod.rs index d7e8c7e..5fb9e6e 100644 --- a/client/src/interfaces/mod.rs +++ b/client/src/interfaces/mod.rs @@ -18,7 +18,9 @@ pub mod prefabindex; pub mod profiler; use crate::{ - download::Downloader, network::Network, render::scene::ScenePreparer, ui::UI_POSITION_OFFSET, + download::Downloader, + network::Network, + render::{scene::ScenePreparer, ui::UI_POSITION_OFFSET}, }; use egui::{Pos2, Widget}; use prefabindex::PrefabIndexInterface; diff --git a/client/src/main.rs b/client/src/main.rs index 4d696b7..e5141c7 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -22,9 +22,7 @@ pub mod download; pub mod interfaces; pub mod network; pub mod render; -pub mod shaders; pub mod state; -pub mod ui; pub mod window; use anyhow::Result; diff --git a/client/src/render/mod.rs b/client/src/render/mod.rs index 6964337..db961f4 100644 --- a/client/src/render/mod.rs +++ b/client/src/render/mod.rs @@ -15,10 +15,11 @@ along with this program. If not, see . */ pub mod scene; +pub mod shaders; +pub mod ui; use crate::{ camera::Camera, download::Downloader, interfaces::profiler::TimingProfiler, state::InputState, - ui::UiRenderer, }; use anyhow::{Result, anyhow}; use log::{info, warn}; @@ -30,6 +31,7 @@ use std::{ thread::{sleep, spawn}, time::Duration, }; +use ui::UiRenderer; use weareshared::tree::SceneTree; use wgpu::{ AdapterInfo, Backends, CommandEncoderDescriptor, Device, DeviceDescriptor, Extent3d, Features, @@ -52,7 +54,18 @@ pub struct Renderer<'a> { pub timing: TimingProfiler, pub timing_submit: Arc>, pub adapter_info: Arc, + + color_msaa: TextureView, + config: GraphicsConfig, +} + +#[derive(Debug, Clone)] +pub struct GraphicsConfig { + max_anisotropy: u16, + max_mip_count: u32, + sample_count: u32, } + impl<'a> Renderer<'a> { pub fn new(window: &'a Window, downloader: Arc) -> Result { info!("wgpu init"); @@ -95,31 +108,60 @@ impl<'a> Renderer<'a> { let device = Arc::new(device); let queue = Arc::new(queue); + let config = GraphicsConfig { + max_anisotropy: 16, + max_mip_count: 16, + sample_count: 4, + }; + + let depth = device + .create_texture(&TextureDescriptor { + label: None, + size: Extent3d { + height: 256, + width: 256, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: config.sample_count, + dimension: TextureDimension::D2, + format: TextureFormat::Depth32Float, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }) + .create_view(&TextureViewDescriptor::default()); + + let color_msaa = device + .create_texture(&TextureDescriptor { + label: None, + size: Extent3d { + height: 256, + width: 256, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: config.sample_count, + dimension: TextureDimension::D2, + format: surface_configuration.format, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }) + .create_view(&TextureViewDescriptor::default()); + let scene_prepare = Arc::new(ScenePreparer::new( device.clone(), queue.clone(), surface_configuration.format, downloader, + config.clone(), )); - let ui_renderer = - UiRenderer::new(device.clone(), queue.clone(), surface_configuration.format); - - let depth = device.create_texture(&TextureDescriptor { - label: None, - size: Extent3d { - height: 256, - width: 256, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: TextureFormat::Depth32Float, - usage: TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }); - let depth = depth.create_view(&TextureViewDescriptor::default()); + let ui_renderer = UiRenderer::new( + device.clone(), + queue.clone(), + surface_configuration.format, + config.clone(), + ); // TODO multithreading introduces double-loading some resources. fix that before increasing thread count for _ in 0..1 { @@ -146,9 +188,11 @@ impl<'a> Renderer<'a> { queue, surface_configuration, ui_renderer, + config, surface_needs_reconfigure: false, timing: Default::default(), timing_submit: Default::default(), + color_msaa, }) } @@ -158,23 +202,38 @@ impl<'a> Renderer<'a> { self.surface .configure(&self.device, &self.surface_configuration); + let size = Extent3d { + height, + width, + depth_or_array_layers: 1, + }; self.depth = self .device .create_texture(&TextureDescriptor { label: None, - size: Extent3d { - height, - width, - depth_or_array_layers: 1, - }, + size, mip_level_count: 1, - sample_count: 1, + sample_count: self.config.sample_count, dimension: TextureDimension::D2, format: TextureFormat::Depth32Float, usage: TextureUsages::RENDER_ATTACHMENT, view_formats: &[], }) .create_view(&TextureViewDescriptor::default()); + + self.color_msaa = self + .device + .create_texture(&TextureDescriptor { + label: None, + size, + mip_level_count: 1, + sample_count: self.config.sample_count, + dimension: TextureDimension::D2, + format: self.surface_configuration.format, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }) + .create_view(&TextureViewDescriptor::default()); } pub fn draw( @@ -189,12 +248,12 @@ impl<'a> Renderer<'a> { .configure(&self.device, &self.surface_configuration); self.surface_needs_reconfigure = false } - let target = self.surface.get_current_texture()?; - if target.suboptimal { + let surface = self.surface.get_current_texture()?; + if surface.suboptimal { warn!("suboptimal swapchain texture"); self.surface_needs_reconfigure = true; } - let target_view = target + let target_view = surface .texture .create_view(&TextureViewDescriptor::default()); @@ -205,10 +264,17 @@ impl<'a> Renderer<'a> { let view = camera.view_matrix(); let project = camera.project_matrix(); + let (target, resolve_target) = if self.config.sample_count == 1 { + (&target_view, None) + } else { + (&self.color_msaa, Some(&target_view)) + }; + self.timing.checkpoint("draw scene"); self.scene_pipeline.draw( &mut commands, - &target_view, + target, + resolve_target, &self.depth, scene, &self.scene_prepare.prefabs, @@ -219,7 +285,8 @@ impl<'a> Renderer<'a> { self.timing.checkpoint("draw ui"); self.ui_renderer.draw( &mut commands, - &target_view, + target, + resolve_target, &self.depth, project * view, input_state, @@ -232,10 +299,9 @@ impl<'a> Renderer<'a> { self.device.poll(MaintainBase::WaitForSubmissionIndex(i)); self.timing.checkpoint("present"); - target.present(); + surface.present(); self.timing.checkpoint(""); - let mut ts = self.timing_submit.lock().unwrap(); swap(&mut *ts, &mut self.timing); diff --git a/client/src/render/scene/draw.rs b/client/src/render/scene/draw.rs index d133d3b..1109401 100644 --- a/client/src/render/scene/draw.rs +++ b/client/src/render/scene/draw.rs @@ -14,7 +14,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -use super::{demand_map::DemandMap, RPrefab}; +use super::{RPrefab, demand_map::DemandMap}; use glam::{EulerRot, Mat3, Mat4}; use std::sync::Arc; use weareshared::{packets::Resource, resources::Prefab, tree::SceneTree}; @@ -30,6 +30,7 @@ impl ScenePipeline { &mut self, commands: &mut CommandEncoder, target: &TextureView, + resolve_target: Option<&TextureView>, depth: &TextureView, scene: &SceneTree, prefabs: &DemandMap, Arc>, @@ -40,7 +41,7 @@ impl ScenePipeline { label: None, color_attachments: &[Some(RenderPassColorAttachment { view: target, - resolve_target: None, + resolve_target, ops: Operations { store: StoreOp::Store, load: LoadOp::Clear(Color { diff --git a/client/src/render/scene/mod.rs b/client/src/render/scene/mod.rs index 7471fc8..ad7e0ce 100644 --- a/client/src/render/scene/mod.rs +++ b/client/src/render/scene/mod.rs @@ -21,7 +21,8 @@ pub mod pipelines; pub mod textures; pub mod vertex_buffers; -use crate::{armature::RArmature, download::Downloader, shaders::SceneShaders}; +use super::{shaders::SceneShaders, GraphicsConfig}; +use crate::{armature::RArmature, download::Downloader}; use anyhow::Result; use bytemuck::{Pod, Zeroable}; use demand_map::DemandMap; @@ -42,11 +43,6 @@ use wgpu::{ util::{BufferInitDescriptor, DeviceExt}, }; -struct GraphicsConfig { - max_anisotropy: u16, - max_mip_count: u32, -} - pub struct ScenePreparer { device: Arc, queue: Arc, @@ -143,13 +139,11 @@ impl ScenePreparer { queue: Arc, render_format: TextureFormat, downloader: Arc, + config: GraphicsConfig, ) -> Self { Self { render_format, - config: GraphicsConfig { - max_anisotropy: 16, - max_mip_count: 16, - }, + config, layouts: SceneBgLayouts::load(&device), shaders: SceneShaders::load(&device), device, @@ -210,7 +204,7 @@ impl ScenePreparer { for spec in self.pipelines.needed() { self.pipelines.insert( spec.clone(), - Arc::new(spec.create(&self.device, &self.layouts, &self.shaders)), + Arc::new(spec.create(&self.device, &self.layouts, &self.shaders, &self.config)), 0, ); } diff --git a/client/src/render/scene/pipelines.rs b/client/src/render/scene/pipelines.rs index dfc5d19..53064c9 100644 --- a/client/src/render/scene/pipelines.rs +++ b/client/src/render/scene/pipelines.rs @@ -14,6 +14,8 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ +use super::{GraphicsConfig, PipelineSpec}; +use crate::render::shaders::SceneShaders; use wgpu::{ BindGroupLayout, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BlendState, BufferBindingType, ColorTargetState, ColorWrites, CompareFunction, DepthBiasState, @@ -25,10 +27,6 @@ use wgpu::{ VertexStepMode, }; -use crate::shaders::SceneShaders; - -use super::PipelineSpec; - pub struct SceneBgLayouts { pub texture: BindGroupLayout, pub material: BindGroupLayout, @@ -95,6 +93,7 @@ impl PipelineSpec { device: &Device, layouts: &SceneBgLayouts, shaders: &SceneShaders, + config: &GraphicsConfig, ) -> RenderPipeline { let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { label: None, @@ -188,7 +187,10 @@ impl PipelineSpec { bias: DepthBiasState::default(), stencil: StencilState::default(), }), - multisample: MultisampleState::default(), + multisample: MultisampleState { + count: config.sample_count, + ..Default::default() + }, multiview: None, cache: None, }) diff --git a/client/src/render/scene/textures.rs b/client/src/render/scene/textures.rs index 463e8f1..f85f21f 100644 --- a/client/src/render/scene/textures.rs +++ b/client/src/render/scene/textures.rs @@ -32,7 +32,7 @@ pub struct MipGenerationPipeline { } impl MipGenerationPipeline { pub fn load(device: &Device, format: TextureFormat) -> Self { - let shader = device.create_shader_module(include_wgsl!("../../shaders/texture_copy.wgsl")); + 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, diff --git a/client/src/render/shaders/fragment_pbr.wgsl b/client/src/render/shaders/fragment_pbr.wgsl new file mode 100644 index 0000000..c8fb857 --- /dev/null +++ b/client/src/render/shaders/fragment_pbr.wgsl @@ -0,0 +1,64 @@ +// 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 . + +struct VertexOut { + @builtin(position) clip: vec4, + @location(0) normal: vec3, + @location(1) tangent: vec3, + @location(2) texcoord: vec2, + @location(3) position: vec3, +} + +struct Material { + roughness: f32, + metallic: f32, + albedo_alpha: vec4, + emission: vec3, +} + +@group(0) @binding(0) var tex_albedo: texture_2d; +@group(0) @binding(1) var tex_albedo_sampler: sampler; +@group(1) @binding(0) var tex_normal: texture_2d; +@group(1) @binding(1) var tex_normal_sampler: sampler; +@group(2) @binding(0) var material: Material; + +@fragment +fn main(vo: VertexOut) -> @location(0) vec4 { + let t_albedo = textureSample(tex_albedo, tex_albedo_sampler, vo.texcoord); + let t_normal = textureSample(tex_normal, tex_normal_sampler, vo.texcoord); + + let tangent_basis = mat3x3(vo.tangent, cross(vo.tangent, vo.normal), vo.normal); + let normal = tangent_basis * (t_normal.rgb * 2. - 1.); + + let light = vec3(0.64, 0.64, 0.64); + // let view = normalize(-vo.position); + + let ambient = 0.1; + let diffuse = saturate(dot(light, normal)); + // let specular = pow(saturate(dot(reflect(-light, normal), view)), 2.); + + let lighting = ambient + diffuse; + + let color = t_albedo.rgb * lighting; + // let color = vec3(dot(normal, view) * 0.5 + 0.5) ; + // let color = view * 0.5 + 0.5; + let alpha = t_albedo.a; + + // TODO better (and faster?) randomness for alpha dither + if fract(dot(sin(vo.clip * 123.) * 1213., vec4(3., 2., 1., 4.))) > alpha { + discard; + } + return vec4(color, 1.); +} diff --git a/client/src/render/shaders/fragment_ui.wgsl b/client/src/render/shaders/fragment_ui.wgsl new file mode 100644 index 0000000..7cb66ab --- /dev/null +++ b/client/src/render/shaders/fragment_ui.wgsl @@ -0,0 +1,28 @@ +// 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 . + +struct VertexOut { + @builtin(position) clip: vec4, + @location(0) uv: vec2, + @location(1) color: vec4, +} + +@group(0) @binding(0) var texture: texture_2d; +@group(0) @binding(1) var texture_sampler: sampler; + +@fragment +fn main(vo: VertexOut) -> @location(0) vec4 { + return pow(textureSample(texture, texture_sampler, vo.uv) * vo.color, vec4(2.2)); +} diff --git a/client/src/render/shaders/mod.rs b/client/src/render/shaders/mod.rs new file mode 100644 index 0000000..7e4e3be --- /dev/null +++ b/client/src/render/shaders/mod.rs @@ -0,0 +1,37 @@ +/* + 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 . +*/ +use log::info; +use wgpu::{Device, ShaderModule, include_wgsl}; + +pub struct SceneShaders { + pub fragment_pbr: ShaderModule, + pub vertex_world: ShaderModule, + pub vertex_world_skin: ShaderModule, +} + +impl SceneShaders { + pub fn load(device: &Device) -> Self { + info!("compiling shaders..."); + let s = Self { + fragment_pbr: device.create_shader_module(include_wgsl!("fragment_pbr.wgsl")), + vertex_world: device.create_shader_module(include_wgsl!("vertex_world.wgsl")), + vertex_world_skin: device.create_shader_module(include_wgsl!("vertex_world_skin.wgsl")), + }; + info!("done"); + s + } +} diff --git a/client/src/render/shaders/texture_copy.wgsl b/client/src/render/shaders/texture_copy.wgsl new file mode 100644 index 0000000..be38a76 --- /dev/null +++ b/client/src/render/shaders/texture_copy.wgsl @@ -0,0 +1,37 @@ +// 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 . + +struct VertexOut { + @builtin(position) clip: vec4, + @location(0) texcoord: vec2, +}; + +@group(0) @binding(0) var texture: texture_2d; +@group(0) @binding(1) var texture_sampler: sampler; + +@vertex +fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOut { + let x = i32(vertex_index) / 2; + let y = i32(vertex_index) & 1; + return VertexOut( + vec4(f32(x) * 4.0 - 1.0, 1.0 - f32(y) * 4.0, 0.0, 1.0), + vec2(f32(x) * 2.0, f32(y) * 2.0) + ); +} + +@fragment +fn fs_main(vertex: VertexOut) -> @location(0) vec4 { + return textureSample(texture, texture_sampler, vertex.texcoord); +} diff --git a/client/src/render/shaders/vertex_ui.wgsl b/client/src/render/shaders/vertex_ui.wgsl new file mode 100644 index 0000000..7cbac8b --- /dev/null +++ b/client/src/render/shaders/vertex_ui.wgsl @@ -0,0 +1,43 @@ +// 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 . + +struct VertexIn { + @location(0) pos: vec2, + @location(1) uv: vec2, + @location(2) color: u32, +} +struct VertexOut { + @builtin(position) clip: vec4, + @location(0) uv: vec2, + @location(1) color: vec4, +} + +var project: mat4x4; + +fn unpack_color(color: u32) -> vec4 { + return vec4( + f32(color & 255u), + f32((color >> 8u) & 255u), + f32((color >> 16u) & 255u), + f32((color >> 24u) & 255u), + ) / 255.0; +} + +@vertex +fn main(@builtin(vertex_index) vindex: u32, vi: VertexIn) -> VertexOut { + var clip = project * vec4(vi.pos, 0., 1.); + let vo = VertexOut(clip, vi.uv, unpack_color(vi.color)); + return vo; +} diff --git a/client/src/render/shaders/vertex_world.wgsl b/client/src/render/shaders/vertex_world.wgsl new file mode 100644 index 0000000..4f342fd --- /dev/null +++ b/client/src/render/shaders/vertex_world.wgsl @@ -0,0 +1,48 @@ +// 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 . + +struct VertexIn { + @location(0) position: vec3, + @location(1) normal: vec3, + @location(2) tangent: vec3, // TODO maybe compress this + @location(3) texcoord: vec2, +} +struct VertexOut { + @builtin(position) clip: vec4, + @location(0) normal: vec3, + @location(1) tangent: vec3, + @location(2) texcoord: vec2, + @location(3) position: vec3, +} + +struct PushConst { + modelviewproject: mat4x4, + model: mat4x4, +} + +var pc: PushConst; + +@vertex +fn main(vi: VertexIn) -> VertexOut { + let clip = pc.modelviewproject * vec4(vi.position, 1.); + let vo = VertexOut( + clip, + normalize((pc.model * vec4(vi.normal, 0.)).xyz), + normalize((pc.model * vec4(vi.tangent, 0.)).xyz), + vi.texcoord, + (pc.model * vec4(vi.position, 1.)).xyz, + ); + return vo; +} diff --git a/client/src/render/shaders/vertex_world_skin.wgsl b/client/src/render/shaders/vertex_world_skin.wgsl new file mode 100644 index 0000000..6e2b308 --- /dev/null +++ b/client/src/render/shaders/vertex_world_skin.wgsl @@ -0,0 +1,58 @@ +// 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 . + +struct VertexIn { + @location(0) position: vec3, + @location(1) normal: vec3, + @location(2) tangent: vec3, // TODO maybe compress this + @location(3) texcoord: vec2, + @location(4) joint_index: vec4, + @location(5) joint_weight: vec4, +} +struct VertexOut { + @builtin(position) clip: vec4, + @location(0) normal: vec3, + @location(1) tangent: vec3, + @location(2) texcoord: vec2, + @location(3) position: vec3, +} + +struct PushConst { + modelviewproject: mat4x4, + model: mat4x4, +} + +@group(3) @binding(0) var joints: array, 128>; +var pc: PushConst; + +@vertex +fn main(vi: VertexIn) -> VertexOut { + let pos_in = vec4(vi.position, 1.); + let j0 = vi.joint_weight.x * (joints[vi.joint_index.x] * pos_in); + let j1 = vi.joint_weight.y * (joints[vi.joint_index.y] * pos_in); + let j2 = vi.joint_weight.z * (joints[vi.joint_index.z] * pos_in); + let j3 = vi.joint_weight.w * (joints[vi.joint_index.w] * pos_in); + let position = j0 + j1 + j2 + j3; + + let clip = pc.modelviewproject * position; + let vo = VertexOut( + clip, + normalize((pc.model * vec4(vi.normal, 0.)).xyz), + normalize((pc.model * vec4(vi.tangent, 0.)).xyz), + vi.texcoord, + (pc.model * vec4(vi.position, 1.)).xyz, + ); + return vo; +} diff --git a/client/src/render/ui.rs b/client/src/render/ui.rs new file mode 100644 index 0000000..e27b9cb --- /dev/null +++ b/client/src/render/ui.rs @@ -0,0 +1,535 @@ +/* + 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 . +*/ +use super::GraphicsConfig; +use crate::state::InputState; +use egui::{ + Context, Event, ImageData, PointerButton, TextureId, ViewportId, ViewportInfo, + epaint::{ImageDelta, Primitive, Vertex}, +}; +use glam::{Affine3A, Mat2, Mat3, Mat4, Vec2, Vec3, Vec3Swizzles, Vec4Swizzles, vec2, vec4}; +use log::{debug, warn}; +use rand::random; +use std::{ + collections::HashMap, + num::NonZeroU64, + sync::{Arc, RwLock}, +}; +use wgpu::{ + AddressMode, BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, + BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, BlendState, + Buffer, BufferDescriptor, BufferUsages, ColorTargetState, ColorWrites, CommandEncoder, + CompareFunction, DepthStencilState, Device, Extent3d, FilterMode, FragmentState, FrontFace, + ImageCopyTexture, ImageDataLayout, IndexFormat, LoadOp, MultisampleState, Operations, Origin3d, + PipelineCompilationOptions, PipelineLayoutDescriptor, PolygonMode, PrimitiveState, + PrimitiveTopology, PushConstantRange, Queue, RenderPassColorAttachment, + RenderPassDepthStencilAttachment, RenderPassDescriptor, RenderPipeline, + RenderPipelineDescriptor, SamplerBindingType, SamplerDescriptor, ShaderStages, StoreOp, + SurfaceConfiguration, Texture, TextureAspect, TextureDescriptor, TextureDimension, + TextureFormat, TextureSampleType, TextureUsages, TextureView, TextureViewDescriptor, + TextureViewDimension, VertexBufferLayout, VertexState, VertexStepMode, include_wgsl, + util::{DeviceExt, TextureDataOrder}, + vertex_attr_array, +}; +use winit::event::MouseButton; + +pub const UI_POSITION_OFFSET: f32 = 1000.; + +pub struct UiRenderer { + device: Arc, + queue: Arc, + _config: GraphicsConfig, + ctx: Context, + pipeline: RenderPipeline, + bind_group_layout: BindGroupLayout, + textures: RwLock>, + surfaces: RwLock>, + + last_pointer: Vec2, +} + +pub struct UiSurface { + pub transform: Affine3A, + pub content: Arc bool + Send + Sync + 'static>, + size: Vec2, + index: Buffer, + index_capacity: usize, + vertex: Buffer, + vertex_capacity: usize, +} + +pub enum UiEvent { + Click(Vec2, MouseButton, bool), +} + +impl UiRenderer { + pub fn new( + device: Arc, + queue: Arc, + format: TextureFormat, + config: GraphicsConfig, + ) -> Self { + let frag_shader = device.create_shader_module(include_wgsl!("shaders/fragment_ui.wgsl")); + let vert_shader = device.create_shader_module(include_wgsl!("shaders/vertex_ui.wgsl")); + + let bind_group_layout = 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, + }); + let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[PushConstantRange { + range: 0..(4 * 4 * size_of::() as u32), + stages: ShaderStages::VERTEX, + }], + }); + let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { + label: None, + layout: Some(&pipeline_layout), + fragment: Some(FragmentState { + module: &frag_shader, + entry_point: Some("main"), + targets: &[Some(ColorTargetState { + blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING), + format, + write_mask: ColorWrites::all(), + })], + compilation_options: PipelineCompilationOptions::default(), + }), + vertex: VertexState { + module: &vert_shader, + entry_point: Some("main"), + buffers: &[VertexBufferLayout { + array_stride: size_of::() as u64, + step_mode: VertexStepMode::Vertex, + attributes: &vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32], + }], + compilation_options: PipelineCompilationOptions::default(), + }, + primitive: PrimitiveState { + topology: PrimitiveTopology::TriangleList, + front_face: FrontFace::Ccw, + cull_mode: None, //Some(Face::Back), + polygon_mode: PolygonMode::Fill, + ..Default::default() + }, + depth_stencil: Some(DepthStencilState { + format: TextureFormat::Depth32Float, + depth_write_enabled: false, + depth_compare: CompareFunction::LessEqual, + stencil: Default::default(), + bias: Default::default(), + }), + multisample: MultisampleState { + count: config.sample_count, + ..Default::default() + }, + multiview: None, + cache: None, + }); + Self { + ctx: Context::default(), + pipeline, + device, + queue, + bind_group_layout, + _config: config, + last_pointer: Vec2::ZERO, + textures: HashMap::new().into(), + surfaces: HashMap::new().into(), + } + } + + pub fn add_surface( + &mut self, + transform: Affine3A, + content: impl Fn(&Context) -> bool + Send + Sync + 'static, + ) { + let index_capacity = 1024; + let vertex_capacity = 1024; + let index = self.device.create_buffer(&BufferDescriptor { + label: None, + size: (size_of::() * index_capacity) as u64, + usage: BufferUsages::INDEX | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let vertex = self.device.create_buffer(&BufferDescriptor { + label: None, + size: (size_of::() * vertex_capacity) as u64, + usage: BufferUsages::VERTEX | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let id = ViewportId::from_hash_of(random::()); + debug!("ui surface added: {id:?}"); + self.surfaces.write().unwrap().insert(id, UiSurface { + transform, + content: Arc::new(content), + index, + vertex, + index_capacity, + vertex_capacity, + size: Vec2::ZERO, + }); + } + + pub fn apply_texture_delta(&self, texid: TextureId, delta: ImageDelta) { + let mut textures = self.textures.write().unwrap(); + let size = Extent3d { + depth_or_array_layers: 1, + width: delta.image.width() as u32, + height: delta.image.height() as u32, + }; + let pixels = match &delta.image { + ImageData::Color(color_image) => color_image.pixels.clone(), + ImageData::Font(font_image) => font_image.srgba_pixels(None).collect(), + }; + + if let Some((_texbg, tex, texsize)) = textures.get_mut(&texid) { + let pos = delta.pos.unwrap_or([0, 0]); + debug!("updating UI texture at {pos:?}"); + self.queue.write_texture( + ImageCopyTexture { + texture: &tex, + mip_level: 0, + origin: Origin3d { + x: pos[0] as u32, + y: pos[1] as u32, + z: 0, + }, + aspect: TextureAspect::All, + }, + bytemuck::cast_slice::<_, u8>(&pixels), + ImageDataLayout { + offset: 0, + bytes_per_row: Some(texsize[0] * 4), + rows_per_image: None, + }, + size, + ); + } else { + assert_eq!( + delta.pos, None, + "partial update impossible; texture does not yet exist" + ); + debug!( + "uploading new UI texture: width={}, height={}", + delta.image.width(), + delta.image.height() + ); + + let texture = self.device.create_texture_with_data( + &self.queue, + &TextureDescriptor { + label: None, + size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8UnormSrgb, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + view_formats: &[], + }, + TextureDataOrder::LayerMajor, + bytemuck::cast_slice::<_, u8>(&pixels), + ); + let textureview = texture.create_view(&TextureViewDescriptor::default()); + let sampler = self.device.create_sampler(&SamplerDescriptor { + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + ..Default::default() + }); + let bindgroup = self.device.create_bind_group(&BindGroupDescriptor { + label: None, + layout: &self.bind_group_layout, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(&textureview), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&sampler), + }, + ], + }); + textures.insert( + texid, + (bindgroup, texture, delta.image.size().map(|e| e as u32)), + ); + } + } + + pub fn draw( + &mut self, + commands: &mut CommandEncoder, + target: &TextureView, + resolve_target: Option<&TextureView>, + depth: &TextureView, + projection: Mat4, + input_state: &mut InputState, + surface_configuration: &SurfaceConfiguration, + ) { + let mut surfaces = self.surfaces.write().unwrap(); + if surfaces.is_empty() { + return; + } + + let mut rpass = commands.begin_render_pass(&RenderPassDescriptor { + label: None, + color_attachments: &[Some(RenderPassColorAttachment { + view: target, + resolve_target, + ops: Operations { + load: LoadOp::Load, + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &depth, + depth_ops: Some(Operations { + load: LoadOp::Load, + store: StoreOp::Store, + }), + stencil_ops: None, + }), + ..Default::default() + }); + + rpass.set_pipeline(&self.pipeline); + + let mut raw_input = egui::RawInput::default(); + raw_input.viewport_id = surfaces.keys().next().copied().unwrap(); + raw_input.viewports = surfaces + .keys() + .map(|k| { + (*k, ViewportInfo { + native_pixels_per_point: Some(2.), + ..Default::default() + }) + }) + .collect(); + + let mut surfaces_closed = Vec::new(); + for (viewport_id, surf) in surfaces.iter_mut() { + let scale = 0.005; + let projection = projection + * Mat4::from_translation(surf.transform.translation.into()) + * Mat4::from_mat3a(surf.transform.matrix3) + * Mat4::from_mat3(Mat3::from_mat2(Mat2::from_cols_array(&[ + scale, 0., 0., -scale, + ]))) + * Mat4::from_translation(-Vec3::new(UI_POSITION_OFFSET, UI_POSITION_OFFSET, 0.)); + + let screen_size = vec2( + surface_configuration.width as f32, + surface_configuration.height as f32, + ); + + if projection.determinant() < 0e-4 { + warn!("bad UI projection") + } + + let unproject = projection.inverse(); + + let unproject_mouse = |pos: Vec2| { + let mouse_xy_clip = (pos / screen_size) * 2. - 1.; + + let mouse_clip_1 = vec4(mouse_xy_clip.x, -mouse_xy_clip.y, 0.0, 1.0); + let mouse_clip_2 = vec4(mouse_xy_clip.x, -mouse_xy_clip.y, 1.0, 1.0); + let mut mouse_world_1 = unproject * mouse_clip_1; + let mut mouse_world_2 = unproject * mouse_clip_2; + mouse_world_1 /= mouse_world_1.w; + mouse_world_2 /= mouse_world_2.w; + let mouse_world_1 = mouse_world_1.xyz(); + let mouse_world_2 = mouse_world_2.xyz(); + + let ray_norm = (mouse_world_2 - mouse_world_1).normalize(); + let ray_t = mouse_world_1.z / ray_norm.z; + let ray_hit = mouse_world_1 - ray_norm * ray_t; + + debug_assert!(ray_hit.z.abs() < 0.1, "mouse was not projected properly"); + + ray_hit.xy() + }; + + let cursor_pos = unproject_mouse(input_state.cursor_pos); + + let mut raw_input = raw_input.clone(); + if cursor_pos != self.last_pointer { + raw_input.events.push(Event::PointerMoved(egui::Pos2::new( + cursor_pos.x, + cursor_pos.y, + ))); + self.last_pointer = cursor_pos; + } + raw_input + .events + .extend(input_state.ui_events.iter().map(|e| match e { + UiEvent::Click(pos, button, down) => egui::Event::PointerButton { + pos: egui::Pos2::from(unproject_mouse(*pos).to_array()), + button: match button { + MouseButton::Left => PointerButton::Primary, + MouseButton::Right => PointerButton::Secondary, + MouseButton::Middle => PointerButton::Middle, + MouseButton::Back => PointerButton::Extra1, + MouseButton::Forward => PointerButton::Extra2, + MouseButton::Other(_) => PointerButton::Extra1, + }, + pressed: *down, + modifiers: egui::Modifiers::default(), + }, + })); + + let mut close = false; + let full_output = self.ctx.run(raw_input.clone(), |ctx| { + close = !(surf.content)(ctx); + surf.size = Vec2::new(ctx.used_size().x, ctx.used_size().y) + }); + if close { + surfaces_closed.push(*viewport_id) + } + + for (texid, delta) in full_output.textures_delta.set { + self.apply_texture_delta(texid, delta); + } + + let clipped_primitives = self + .ctx + .tessellate(full_output.shapes, full_output.pixels_per_point); + + let mut index_count = 0; + let mut vertex_count = 0; + for p in &clipped_primitives { + if let Primitive::Mesh(mesh) = &p.primitive { + index_count += mesh.indices.len(); + vertex_count += mesh.vertices.len(); + } + } + + if index_count == 0 || vertex_count == 0 { + return; + } + + while index_count > surf.index_capacity { + debug!( + "index buffer overflow ({index_count}). expanding {} -> {}", + surf.index_capacity, + surf.index_capacity * 2 + ); + surf.index_capacity *= 2; + surf.index = self.device.create_buffer(&BufferDescriptor { + label: None, + size: (size_of::() * surf.index_capacity) as u64, + usage: BufferUsages::INDEX | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + } + while vertex_count > surf.vertex_capacity { + debug!( + "vertex buffer overflow ({vertex_count}). expanding {} -> {}", + surf.vertex_capacity, + surf.vertex_capacity * 2 + ); + surf.vertex_capacity *= 2; + surf.vertex = self.device.create_buffer(&BufferDescriptor { + label: None, + size: (size_of::() * surf.vertex_capacity) as u64, + usage: BufferUsages::VERTEX | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + } + + let mut mapped_index = self + .queue + .write_buffer_with( + &surf.index, + 0, + NonZeroU64::new((size_of::() * index_count) as u64).unwrap(), + ) + .expect("ui index buffer overflow"); + let mut mapped_vertex = self + .queue + .write_buffer_with( + &surf.vertex, + 0, + NonZeroU64::new((size_of::() * vertex_count) as u64).unwrap(), + ) + .expect("ui vertex buffer overflow"); + + let mut index_offset = 0; + let mut vertex_offset = 0; + let mut slices = Vec::new(); + for p in clipped_primitives { + if let Primitive::Mesh(mesh) = p.primitive { + mapped_index[index_offset * size_of::() + ..(index_offset + mesh.indices.len()) * size_of::()] + .copy_from_slice(bytemuck::cast_slice(&mesh.indices)); + mapped_vertex[vertex_offset * size_of::() + ..(vertex_offset + mesh.vertices.len()) * size_of::()] + .copy_from_slice(bytemuck::cast_slice(&mesh.vertices)); + slices.push(( + index_offset as u32..index_offset as u32 + mesh.indices.len() as u32, + vertex_offset as i32, + mesh.texture_id, + )); + index_offset += mesh.indices.len(); + vertex_offset += mesh.vertices.len(); + } + } + + assert_eq!(index_count, index_offset); + assert_eq!(vertex_count, vertex_offset); + + let projection = projection.to_cols_array().map(|v| v.to_le_bytes()); + + rpass.set_push_constants(ShaderStages::VERTEX, 0, projection.as_flattened()); + rpass.set_index_buffer(surf.index.slice(..), IndexFormat::Uint32); + rpass.set_vertex_buffer(0, surf.vertex.slice(..)); + for (index, base_vertex, texid) in slices { + let tex_guard = self.textures.read().unwrap(); + let bind_group = &tex_guard.get(&texid).unwrap().0; + rpass.set_bind_group(0, bind_group, &[]); + rpass.draw_indexed(index, base_vertex, 0..1); + } + } + for s in surfaces_closed { + debug!("ui surface closed: {s:?}"); + surfaces.remove(&s); + } + + input_state.ui_events.clear(); + } +} diff --git a/client/src/shaders/fragment_pbr.wgsl b/client/src/shaders/fragment_pbr.wgsl deleted file mode 100644 index c8fb857..0000000 --- a/client/src/shaders/fragment_pbr.wgsl +++ /dev/null @@ -1,64 +0,0 @@ -// 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 . - -struct VertexOut { - @builtin(position) clip: vec4, - @location(0) normal: vec3, - @location(1) tangent: vec3, - @location(2) texcoord: vec2, - @location(3) position: vec3, -} - -struct Material { - roughness: f32, - metallic: f32, - albedo_alpha: vec4, - emission: vec3, -} - -@group(0) @binding(0) var tex_albedo: texture_2d; -@group(0) @binding(1) var tex_albedo_sampler: sampler; -@group(1) @binding(0) var tex_normal: texture_2d; -@group(1) @binding(1) var tex_normal_sampler: sampler; -@group(2) @binding(0) var material: Material; - -@fragment -fn main(vo: VertexOut) -> @location(0) vec4 { - let t_albedo = textureSample(tex_albedo, tex_albedo_sampler, vo.texcoord); - let t_normal = textureSample(tex_normal, tex_normal_sampler, vo.texcoord); - - let tangent_basis = mat3x3(vo.tangent, cross(vo.tangent, vo.normal), vo.normal); - let normal = tangent_basis * (t_normal.rgb * 2. - 1.); - - let light = vec3(0.64, 0.64, 0.64); - // let view = normalize(-vo.position); - - let ambient = 0.1; - let diffuse = saturate(dot(light, normal)); - // let specular = pow(saturate(dot(reflect(-light, normal), view)), 2.); - - let lighting = ambient + diffuse; - - let color = t_albedo.rgb * lighting; - // let color = vec3(dot(normal, view) * 0.5 + 0.5) ; - // let color = view * 0.5 + 0.5; - let alpha = t_albedo.a; - - // TODO better (and faster?) randomness for alpha dither - if fract(dot(sin(vo.clip * 123.) * 1213., vec4(3., 2., 1., 4.))) > alpha { - discard; - } - return vec4(color, 1.); -} diff --git a/client/src/shaders/fragment_ui.wgsl b/client/src/shaders/fragment_ui.wgsl deleted file mode 100644 index 7cb66ab..0000000 --- a/client/src/shaders/fragment_ui.wgsl +++ /dev/null @@ -1,28 +0,0 @@ -// 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 . - -struct VertexOut { - @builtin(position) clip: vec4, - @location(0) uv: vec2, - @location(1) color: vec4, -} - -@group(0) @binding(0) var texture: texture_2d; -@group(0) @binding(1) var texture_sampler: sampler; - -@fragment -fn main(vo: VertexOut) -> @location(0) vec4 { - return pow(textureSample(texture, texture_sampler, vo.uv) * vo.color, vec4(2.2)); -} diff --git a/client/src/shaders/mod.rs b/client/src/shaders/mod.rs deleted file mode 100644 index 42b2164..0000000 --- a/client/src/shaders/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -use log::info; -use wgpu::{Device, ShaderModule, include_wgsl}; - -pub struct SceneShaders { - pub fragment_pbr: ShaderModule, - pub vertex_world: ShaderModule, - pub vertex_world_skin: ShaderModule, -} - -impl SceneShaders { - pub fn load(device: &Device) -> Self { - info!("compiling shaders..."); - let s = Self { - fragment_pbr: device.create_shader_module(include_wgsl!("fragment_pbr.wgsl")), - vertex_world: device.create_shader_module(include_wgsl!("vertex_world.wgsl")), - vertex_world_skin: device.create_shader_module(include_wgsl!("vertex_world_skin.wgsl")), - }; - info!("done"); - s - } -} diff --git a/client/src/shaders/texture_copy.wgsl b/client/src/shaders/texture_copy.wgsl deleted file mode 100644 index be38a76..0000000 --- a/client/src/shaders/texture_copy.wgsl +++ /dev/null @@ -1,37 +0,0 @@ -// 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 . - -struct VertexOut { - @builtin(position) clip: vec4, - @location(0) texcoord: vec2, -}; - -@group(0) @binding(0) var texture: texture_2d; -@group(0) @binding(1) var texture_sampler: sampler; - -@vertex -fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOut { - let x = i32(vertex_index) / 2; - let y = i32(vertex_index) & 1; - return VertexOut( - vec4(f32(x) * 4.0 - 1.0, 1.0 - f32(y) * 4.0, 0.0, 1.0), - vec2(f32(x) * 2.0, f32(y) * 2.0) - ); -} - -@fragment -fn fs_main(vertex: VertexOut) -> @location(0) vec4 { - return textureSample(texture, texture_sampler, vertex.texcoord); -} diff --git a/client/src/shaders/vertex_ui.wgsl b/client/src/shaders/vertex_ui.wgsl deleted file mode 100644 index 7cbac8b..0000000 --- a/client/src/shaders/vertex_ui.wgsl +++ /dev/null @@ -1,43 +0,0 @@ -// 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 . - -struct VertexIn { - @location(0) pos: vec2, - @location(1) uv: vec2, - @location(2) color: u32, -} -struct VertexOut { - @builtin(position) clip: vec4, - @location(0) uv: vec2, - @location(1) color: vec4, -} - -var project: mat4x4; - -fn unpack_color(color: u32) -> vec4 { - return vec4( - f32(color & 255u), - f32((color >> 8u) & 255u), - f32((color >> 16u) & 255u), - f32((color >> 24u) & 255u), - ) / 255.0; -} - -@vertex -fn main(@builtin(vertex_index) vindex: u32, vi: VertexIn) -> VertexOut { - var clip = project * vec4(vi.pos, 0., 1.); - let vo = VertexOut(clip, vi.uv, unpack_color(vi.color)); - return vo; -} diff --git a/client/src/shaders/vertex_world.wgsl b/client/src/shaders/vertex_world.wgsl deleted file mode 100644 index 4f342fd..0000000 --- a/client/src/shaders/vertex_world.wgsl +++ /dev/null @@ -1,48 +0,0 @@ -// 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 . - -struct VertexIn { - @location(0) position: vec3, - @location(1) normal: vec3, - @location(2) tangent: vec3, // TODO maybe compress this - @location(3) texcoord: vec2, -} -struct VertexOut { - @builtin(position) clip: vec4, - @location(0) normal: vec3, - @location(1) tangent: vec3, - @location(2) texcoord: vec2, - @location(3) position: vec3, -} - -struct PushConst { - modelviewproject: mat4x4, - model: mat4x4, -} - -var pc: PushConst; - -@vertex -fn main(vi: VertexIn) -> VertexOut { - let clip = pc.modelviewproject * vec4(vi.position, 1.); - let vo = VertexOut( - clip, - normalize((pc.model * vec4(vi.normal, 0.)).xyz), - normalize((pc.model * vec4(vi.tangent, 0.)).xyz), - vi.texcoord, - (pc.model * vec4(vi.position, 1.)).xyz, - ); - return vo; -} diff --git a/client/src/shaders/vertex_world_skin.wgsl b/client/src/shaders/vertex_world_skin.wgsl deleted file mode 100644 index 6e2b308..0000000 --- a/client/src/shaders/vertex_world_skin.wgsl +++ /dev/null @@ -1,58 +0,0 @@ -// 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 . - -struct VertexIn { - @location(0) position: vec3, - @location(1) normal: vec3, - @location(2) tangent: vec3, // TODO maybe compress this - @location(3) texcoord: vec2, - @location(4) joint_index: vec4, - @location(5) joint_weight: vec4, -} -struct VertexOut { - @builtin(position) clip: vec4, - @location(0) normal: vec3, - @location(1) tangent: vec3, - @location(2) texcoord: vec2, - @location(3) position: vec3, -} - -struct PushConst { - modelviewproject: mat4x4, - model: mat4x4, -} - -@group(3) @binding(0) var joints: array, 128>; -var pc: PushConst; - -@vertex -fn main(vi: VertexIn) -> VertexOut { - let pos_in = vec4(vi.position, 1.); - let j0 = vi.joint_weight.x * (joints[vi.joint_index.x] * pos_in); - let j1 = vi.joint_weight.y * (joints[vi.joint_index.y] * pos_in); - let j2 = vi.joint_weight.z * (joints[vi.joint_index.z] * pos_in); - let j3 = vi.joint_weight.w * (joints[vi.joint_index.w] * pos_in); - let position = j0 + j1 + j2 + j3; - - let clip = pc.modelviewproject * position; - let vo = VertexOut( - clip, - normalize((pc.model * vec4(vi.normal, 0.)).xyz), - normalize((pc.model * vec4(vi.tangent, 0.)).xyz), - vi.texcoord, - (pc.model * vec4(vi.position, 1.)).xyz, - ); - return vo; -} diff --git a/client/src/state.rs b/client/src/state.rs index d132e3e..ae29bae 100644 --- a/client/src/state.rs +++ b/client/src/state.rs @@ -15,7 +15,12 @@ along with this program. If not, see . */ use crate::{ - audio::Audio, camera::Camera, download::Downloader, interfaces::{ui_selector, InterfaceData}, network::Network, render::Renderer, ui::UiEvent + audio::Audio, + camera::Camera, + download::Downloader, + interfaces::{ui_selector, InterfaceData}, + network::Network, + render::{ui::UiEvent, Renderer}, }; use anyhow::{Context, Result}; use glam::{Vec2, Vec3}; diff --git a/client/src/ui.rs b/client/src/ui.rs deleted file mode 100644 index fd94dfb..0000000 --- a/client/src/ui.rs +++ /dev/null @@ -1,523 +0,0 @@ -/* - 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 . -*/ -use crate::state::InputState; -use egui::{ - Context, Event, ImageData, PointerButton, TextureId, ViewportId, ViewportInfo, - epaint::{ImageDelta, Primitive, Vertex}, -}; -use glam::{Affine3A, Mat2, Mat3, Mat4, Vec2, Vec3, Vec3Swizzles, Vec4Swizzles, vec2, vec4}; -use log::{debug, warn}; -use rand::random; -use std::{ - collections::HashMap, - num::NonZeroU64, - sync::{Arc, RwLock}, -}; -use wgpu::{ - AddressMode, BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, - BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, BlendState, - Buffer, BufferDescriptor, BufferUsages, ColorTargetState, ColorWrites, CommandEncoder, - CompareFunction, DepthStencilState, Device, Extent3d, FilterMode, FragmentState, FrontFace, - ImageCopyTexture, ImageDataLayout, IndexFormat, LoadOp, MultisampleState, Operations, Origin3d, - PipelineCompilationOptions, PipelineLayoutDescriptor, PolygonMode, PrimitiveState, - PrimitiveTopology, PushConstantRange, Queue, RenderPassColorAttachment, - RenderPassDepthStencilAttachment, RenderPassDescriptor, RenderPipeline, - RenderPipelineDescriptor, SamplerBindingType, SamplerDescriptor, ShaderStages, StoreOp, - SurfaceConfiguration, Texture, TextureAspect, TextureDescriptor, TextureDimension, - TextureFormat, TextureSampleType, TextureUsages, TextureView, TextureViewDescriptor, - TextureViewDimension, VertexBufferLayout, VertexState, VertexStepMode, include_wgsl, - util::{DeviceExt, TextureDataOrder}, - vertex_attr_array, -}; -use winit::event::MouseButton; - -pub const UI_POSITION_OFFSET: f32 = 1000.; - -pub struct UiRenderer { - device: Arc, - queue: Arc, - ctx: Context, - pipeline: RenderPipeline, - bind_group_layout: BindGroupLayout, - textures: RwLock>, - surfaces: RwLock>, - - last_pointer: Vec2, -} - -pub struct UiSurface { - pub transform: Affine3A, - pub content: Arc bool + Send + Sync + 'static>, - size: Vec2, - index: Buffer, - index_capacity: usize, - vertex: Buffer, - vertex_capacity: usize, -} - -pub enum UiEvent { - Click(Vec2, MouseButton, bool), -} - -impl UiRenderer { - pub fn new(device: Arc, queue: Arc, format: TextureFormat) -> Self { - let frag_shader = device.create_shader_module(include_wgsl!("shaders/fragment_ui.wgsl")); - let vert_shader = device.create_shader_module(include_wgsl!("shaders/vertex_ui.wgsl")); - - let bind_group_layout = 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, - }); - let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { - label: None, - bind_group_layouts: &[&bind_group_layout], - push_constant_ranges: &[PushConstantRange { - range: 0..(4 * 4 * size_of::() as u32), - stages: ShaderStages::VERTEX, - }], - }); - let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { - label: None, - layout: Some(&pipeline_layout), - fragment: Some(FragmentState { - module: &frag_shader, - entry_point: Some("main"), - targets: &[Some(ColorTargetState { - blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING), - format, - write_mask: ColorWrites::all(), - })], - compilation_options: PipelineCompilationOptions::default(), - }), - vertex: VertexState { - module: &vert_shader, - entry_point: Some("main"), - buffers: &[VertexBufferLayout { - array_stride: size_of::() as u64, - step_mode: VertexStepMode::Vertex, - attributes: &vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32], - }], - compilation_options: PipelineCompilationOptions::default(), - }, - primitive: PrimitiveState { - topology: PrimitiveTopology::TriangleList, - front_face: FrontFace::Ccw, - cull_mode: None, //Some(Face::Back), - polygon_mode: PolygonMode::Fill, - ..Default::default() - }, - depth_stencil: Some(DepthStencilState { - format: TextureFormat::Depth32Float, - depth_write_enabled: false, - depth_compare: CompareFunction::LessEqual, - stencil: Default::default(), - bias: Default::default(), - }), - multisample: MultisampleState::default(), - multiview: None, - cache: None, - }); - Self { - ctx: Context::default(), - pipeline, - device, - queue, - bind_group_layout, - last_pointer: Vec2::ZERO, - textures: HashMap::new().into(), - surfaces: HashMap::new().into(), - } - } - - pub fn add_surface( - &mut self, - transform: Affine3A, - content: impl Fn(&Context) -> bool + Send + Sync + 'static, - ) { - let index_capacity = 1024; - let vertex_capacity = 1024; - let index = self.device.create_buffer(&BufferDescriptor { - label: None, - size: (size_of::() * index_capacity) as u64, - usage: BufferUsages::INDEX | BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - let vertex = self.device.create_buffer(&BufferDescriptor { - label: None, - size: (size_of::() * vertex_capacity) as u64, - usage: BufferUsages::VERTEX | BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - let id = ViewportId::from_hash_of(random::()); - debug!("ui surface added: {id:?}"); - self.surfaces.write().unwrap().insert(id, UiSurface { - transform, - content: Arc::new(content), - index, - vertex, - index_capacity, - vertex_capacity, - size: Vec2::ZERO, - }); - } - - pub fn apply_texture_delta(&self, texid: TextureId, delta: ImageDelta) { - let mut textures = self.textures.write().unwrap(); - let size = Extent3d { - depth_or_array_layers: 1, - width: delta.image.width() as u32, - height: delta.image.height() as u32, - }; - let pixels = match &delta.image { - ImageData::Color(color_image) => color_image.pixels.clone(), - ImageData::Font(font_image) => font_image.srgba_pixels(None).collect(), - }; - - if let Some((_texbg, tex, texsize)) = textures.get_mut(&texid) { - let pos = delta.pos.unwrap_or([0, 0]); - debug!("updating UI texture at {pos:?}"); - self.queue.write_texture( - ImageCopyTexture { - texture: &tex, - mip_level: 0, - origin: Origin3d { - x: pos[0] as u32, - y: pos[1] as u32, - z: 0, - }, - aspect: TextureAspect::All, - }, - bytemuck::cast_slice::<_, u8>(&pixels), - ImageDataLayout { - offset: 0, - bytes_per_row: Some(texsize[0] * 4), - rows_per_image: None, - }, - size, - ); - } else { - assert_eq!( - delta.pos, None, - "partial update impossible; texture does not yet exist" - ); - debug!( - "uploading new UI texture: width={}, height={}", - delta.image.width(), - delta.image.height() - ); - - let texture = self.device.create_texture_with_data( - &self.queue, - &TextureDescriptor { - label: None, - size, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: TextureFormat::Rgba8UnormSrgb, - usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, - view_formats: &[], - }, - TextureDataOrder::LayerMajor, - bytemuck::cast_slice::<_, u8>(&pixels), - ); - let textureview = texture.create_view(&TextureViewDescriptor::default()); - let sampler = self.device.create_sampler(&SamplerDescriptor { - address_mode_u: AddressMode::ClampToEdge, - address_mode_v: AddressMode::ClampToEdge, - mag_filter: FilterMode::Linear, - min_filter: FilterMode::Linear, - ..Default::default() - }); - let bindgroup = self.device.create_bind_group(&BindGroupDescriptor { - label: None, - layout: &self.bind_group_layout, - entries: &[ - BindGroupEntry { - binding: 0, - resource: BindingResource::TextureView(&textureview), - }, - BindGroupEntry { - binding: 1, - resource: BindingResource::Sampler(&sampler), - }, - ], - }); - textures.insert( - texid, - (bindgroup, texture, delta.image.size().map(|e| e as u32)), - ); - } - } - - pub fn draw( - &mut self, - commands: &mut CommandEncoder, - target: &TextureView, - depth: &TextureView, - projection: Mat4, - input_state: &mut InputState, - surface_configuration: &SurfaceConfiguration, - ) { - let mut surfaces = self.surfaces.write().unwrap(); - if surfaces.is_empty() { - return; - } - - let mut rpass = commands.begin_render_pass(&RenderPassDescriptor { - label: None, - color_attachments: &[Some(RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: Operations { - load: LoadOp::Load, - store: StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: &depth, - depth_ops: Some(Operations { - load: LoadOp::Load, - store: StoreOp::Store, - }), - stencil_ops: None, - }), - ..Default::default() - }); - - rpass.set_pipeline(&self.pipeline); - - let mut raw_input = egui::RawInput::default(); - raw_input.viewport_id = surfaces.keys().next().copied().unwrap(); - raw_input.viewports = surfaces - .keys() - .map(|k| { - (*k, ViewportInfo { - native_pixels_per_point: Some(2.), - ..Default::default() - }) - }) - .collect(); - - let mut surfaces_closed = Vec::new(); - for (viewport_id, surf) in surfaces.iter_mut() { - let scale = 0.005; - let projection = projection - * Mat4::from_translation(surf.transform.translation.into()) - * Mat4::from_mat3a(surf.transform.matrix3) - * Mat4::from_mat3(Mat3::from_mat2(Mat2::from_cols_array(&[ - scale, 0., 0., -scale, - ]))) - * Mat4::from_translation(-Vec3::new(UI_POSITION_OFFSET, UI_POSITION_OFFSET, 0.)); - - let screen_size = vec2( - surface_configuration.width as f32, - surface_configuration.height as f32, - ); - - if projection.determinant() < 0e-4 { - warn!("bad UI projection") - } - - let unproject = projection.inverse(); - - let unproject_mouse = |pos: Vec2| { - let mouse_xy_clip = (pos / screen_size) * 2. - 1.; - - let mouse_clip_1 = vec4(mouse_xy_clip.x, -mouse_xy_clip.y, 0.0, 1.0); - let mouse_clip_2 = vec4(mouse_xy_clip.x, -mouse_xy_clip.y, 1.0, 1.0); - let mut mouse_world_1 = unproject * mouse_clip_1; - let mut mouse_world_2 = unproject * mouse_clip_2; - mouse_world_1 /= mouse_world_1.w; - mouse_world_2 /= mouse_world_2.w; - let mouse_world_1 = mouse_world_1.xyz(); - let mouse_world_2 = mouse_world_2.xyz(); - - let ray_norm = (mouse_world_2 - mouse_world_1).normalize(); - let ray_t = mouse_world_1.z / ray_norm.z; - let ray_hit = mouse_world_1 - ray_norm * ray_t; - - debug_assert!(ray_hit.z.abs() < 0.1, "mouse was not projected properly"); - - ray_hit.xy() - }; - - let cursor_pos = unproject_mouse(input_state.cursor_pos); - - let mut raw_input = raw_input.clone(); - if cursor_pos != self.last_pointer { - raw_input.events.push(Event::PointerMoved(egui::Pos2::new( - cursor_pos.x, - cursor_pos.y, - ))); - self.last_pointer = cursor_pos; - } - raw_input - .events - .extend(input_state.ui_events.iter().map(|e| match e { - UiEvent::Click(pos, button, down) => egui::Event::PointerButton { - pos: egui::Pos2::from(unproject_mouse(*pos).to_array()), - button: match button { - MouseButton::Left => PointerButton::Primary, - MouseButton::Right => PointerButton::Secondary, - MouseButton::Middle => PointerButton::Middle, - MouseButton::Back => PointerButton::Extra1, - MouseButton::Forward => PointerButton::Extra2, - MouseButton::Other(_) => PointerButton::Extra1, - }, - pressed: *down, - modifiers: egui::Modifiers::default(), - }, - })); - - let mut close = false; - let full_output = self.ctx.run(raw_input.clone(), |ctx| { - close = !(surf.content)(ctx); - surf.size = Vec2::new(ctx.used_size().x, ctx.used_size().y) - }); - if close { - surfaces_closed.push(*viewport_id) - } - - for (texid, delta) in full_output.textures_delta.set { - self.apply_texture_delta(texid, delta); - } - - let clipped_primitives = self - .ctx - .tessellate(full_output.shapes, full_output.pixels_per_point); - - let mut index_count = 0; - let mut vertex_count = 0; - for p in &clipped_primitives { - if let Primitive::Mesh(mesh) = &p.primitive { - index_count += mesh.indices.len(); - vertex_count += mesh.vertices.len(); - } - } - - if index_count == 0 || vertex_count == 0 { - return; - } - - while index_count > surf.index_capacity { - debug!( - "index buffer overflow ({index_count}). expanding {} -> {}", - surf.index_capacity, - surf.index_capacity * 2 - ); - surf.index_capacity *= 2; - surf.index = self.device.create_buffer(&BufferDescriptor { - label: None, - size: (size_of::() * surf.index_capacity) as u64, - usage: BufferUsages::INDEX | BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - } - while vertex_count > surf.vertex_capacity { - debug!( - "vertex buffer overflow ({vertex_count}). expanding {} -> {}", - surf.vertex_capacity, - surf.vertex_capacity * 2 - ); - surf.vertex_capacity *= 2; - surf.vertex = self.device.create_buffer(&BufferDescriptor { - label: None, - size: (size_of::() * surf.vertex_capacity) as u64, - usage: BufferUsages::VERTEX | BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - } - - let mut mapped_index = self - .queue - .write_buffer_with( - &surf.index, - 0, - NonZeroU64::new((size_of::() * index_count) as u64).unwrap(), - ) - .expect("ui index buffer overflow"); - let mut mapped_vertex = self - .queue - .write_buffer_with( - &surf.vertex, - 0, - NonZeroU64::new((size_of::() * vertex_count) as u64).unwrap(), - ) - .expect("ui vertex buffer overflow"); - - let mut index_offset = 0; - let mut vertex_offset = 0; - let mut slices = Vec::new(); - for p in clipped_primitives { - if let Primitive::Mesh(mesh) = p.primitive { - mapped_index[index_offset * size_of::() - ..(index_offset + mesh.indices.len()) * size_of::()] - .copy_from_slice(bytemuck::cast_slice(&mesh.indices)); - mapped_vertex[vertex_offset * size_of::() - ..(vertex_offset + mesh.vertices.len()) * size_of::()] - .copy_from_slice(bytemuck::cast_slice(&mesh.vertices)); - slices.push(( - index_offset as u32..index_offset as u32 + mesh.indices.len() as u32, - vertex_offset as i32, - mesh.texture_id, - )); - index_offset += mesh.indices.len(); - vertex_offset += mesh.vertices.len(); - } - } - - assert_eq!(index_count, index_offset); - assert_eq!(vertex_count, vertex_offset); - - let projection = projection.to_cols_array().map(|v| v.to_le_bytes()); - - rpass.set_push_constants(ShaderStages::VERTEX, 0, projection.as_flattened()); - rpass.set_index_buffer(surf.index.slice(..), IndexFormat::Uint32); - rpass.set_vertex_buffer(0, surf.vertex.slice(..)); - for (index, base_vertex, texid) in slices { - let tex_guard = self.textures.read().unwrap(); - let bind_group = &tex_guard.get(&texid).unwrap().0; - rpass.set_bind_group(0, bind_group, &[]); - rpass.draw_indexed(index, base_vertex, 0..1); - } - } - for s in surfaces_closed { - debug!("ui surface closed: {s:?}"); - surfaces.remove(&s); - } - - input_state.ui_events.clear(); - } -} -- cgit v1.2.3-70-g09d2