summaryrefslogtreecommitdiff
path: root/client/src/render/scene
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-01-23 22:45:35 +0100
committermetamuffin <metamuffin@disroot.org>2025-01-23 22:45:35 +0100
commit3344eb2d678f9c5973c8e38083760254b54c20fc (patch)
tree793678d4230dd012285169ba34005064690c7af0 /client/src/render/scene
parentdd40803458695abcd4100fffb874cc25a71ea758 (diff)
downloadweareserver-3344eb2d678f9c5973c8e38083760254b54c20fc.tar
weareserver-3344eb2d678f9c5973c8e38083760254b54c20fc.tar.bz2
weareserver-3344eb2d678f9c5973c8e38083760254b54c20fc.tar.zst
split scene_prepare to many files
Diffstat (limited to 'client/src/render/scene')
-rw-r--r--client/src/render/scene/draw.rs104
-rw-r--r--client/src/render/scene/meshops.rs81
-rw-r--r--client/src/render/scene/mod.rs518
-rw-r--r--client/src/render/scene/pipelines.rs196
-rw-r--r--client/src/render/scene/textures.rs284
-rw-r--r--client/src/render/scene/vertex_buffers.rs138
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(())
+ }
+}