diff --git a/CREDITS.md b/CREDITS.md index 5571f75c20..ecc3a47d1b 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -25,3 +25,4 @@ * Low poly fox [by PixelMannen](https://opengameart.org/content/fox-and-shiba) (CC0 1.0 Universal) * Rigging and animation [by @tomkranis on Sketchfab](https://sketchfab.com/models/371dea88d7e04a76af5763f2a36866bc) ([CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/)) * FiraMono by The Mozilla Foundation and Telefonica S.A (SIL Open Font License, Version 1.1: assets/fonts/FiraMono-LICENSE) +* Barycentric from [mk_bary_gltf](https://github.com/komadori/mk_bary_gltf) (MIT OR Apache-2.0) diff --git a/Cargo.toml b/Cargo.toml index d1cc871372..7564cb9716 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -326,6 +326,16 @@ description = "Renders a rectangle, circle, and hexagon" category = "2D Rendering" wasm = true +[[example]] +name = "custom_gltf_vertex_attribute" +path = "examples/2d/custom_gltf_vertex_attribute.rs" + +[package.metadata.example.custom_gltf_vertex_attribute] +name = "Custom glTF vertex attribute 2D" +description = "Renders a glTF mesh in 2D with a custom vertex attribute" +category = "2D Rendering" +wasm = true + [[example]] name = "2d_gizmos" path = "examples/2d/2d_gizmos.rs" diff --git a/assets/models/barycentric/barycentric.gltf b/assets/models/barycentric/barycentric.gltf new file mode 100644 index 0000000000..eecb2bb52c --- /dev/null +++ b/assets/models/barycentric/barycentric.gltf @@ -0,0 +1,80 @@ +{ + "accessors": [ + { + "bufferView": 0, + "byteOffset": 0, + "count": 4, + "componentType": 5126, + "type": "VEC3", + "min": [ + -1.0, + -1.0, + 0.0 + ], + "max": [ + 1.0, + 1.0, + 0.0 + ] + }, + { + "bufferView": 0, + "byteOffset": 12, + "count": 4, + "componentType": 5126, + "type": "VEC4" + }, + { + "bufferView": 0, + "byteOffset": 28, + "count": 4, + "componentType": 5126, + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 0, + "count": 6, + "componentType": 5123, + "type": "SCALAR" + } + ], + "asset": { + "version": "2.0" + }, + "buffers": [ + { + "byteLength": 172, + "uri": "data:application/gltf-buffer;base64,AACAvwAAgL8AAAAAAACAPwAAAAAAAAAAAACAPwAAgD8AAAAAAAAAAAAAgD8AAIC/AAAAAAAAAD8AAAA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIC/AACAPwAAAAAAAAA/AAAAPwAAAAAAAIA/AAAAAAAAAAAAAIA/AACAPwAAgD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAAAAAAAAAQACAAIAAQADAA==" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteLength": 160, + "byteOffset": 0, + "byteStride": 40, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 12, + "byteOffset": 160, + "target": 34962 + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "POSITION": 0, + "COLOR_0": 1, + "__BARYCENTRIC": 2 + }, + "indices": 3 + } + ] + } + ] +} \ No newline at end of file diff --git a/assets/shaders/custom_gltf_2d.wgsl b/assets/shaders/custom_gltf_2d.wgsl new file mode 100644 index 0000000000..d841a17d37 --- /dev/null +++ b/assets/shaders/custom_gltf_2d.wgsl @@ -0,0 +1,36 @@ +#import bevy_sprite::mesh2d_view_bindings +#import bevy_sprite::mesh2d_bindings +#import bevy_sprite::mesh2d_functions + +struct Vertex { + @location(0) position: vec3, + @location(1) color: vec4, + @location(2) barycentric: vec3, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) color: vec4, + @location(1) barycentric: vec3, +}; + +@vertex +fn vertex(vertex: Vertex) -> VertexOutput { + var out: VertexOutput; + out.clip_position = mesh2d_position_local_to_clip(mesh.model, vec4(vertex.position, 1.0)); + out.color = vertex.color; + out.barycentric = vertex.barycentric; + return out; +} + +struct FragmentInput { + @location(0) color: vec4, + @location(1) barycentric: vec3, +}; + +@fragment +fn fragment(input: FragmentInput) -> @location(0) vec4 { + let d = min(input.barycentric.x, min(input.barycentric.y, input.barycentric.z)); + let t = 0.05 * (0.85 + sin(5.0 * globals.time)); + return mix(vec4(1.0,1.0,1.0,1.0), input.color, smoothstep(t, t+0.01, d)); +} diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index c454255496..934c0dae23 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -5,6 +5,7 @@ use bevy_animation::AnimationClip; use bevy_utils::HashMap; mod loader; +mod vertex_attributes; pub use loader::*; use bevy_app::prelude::*; @@ -12,21 +13,47 @@ use bevy_asset::{AddAsset, Handle}; use bevy_ecs::{prelude::Component, reflect::ReflectComponent}; use bevy_pbr::StandardMaterial; use bevy_reflect::{Reflect, TypeUuid}; -use bevy_render::mesh::Mesh; +use bevy_render::{ + mesh::{Mesh, MeshVertexAttribute}, + renderer::RenderDevice, + texture::CompressedImageFormats, +}; use bevy_scene::Scene; /// Adds support for glTF file loading to the app. #[derive(Default)] -pub struct GltfPlugin; +pub struct GltfPlugin { + custom_vertex_attributes: HashMap, +} + +impl GltfPlugin { + pub fn add_custom_vertex_attribute( + mut self, + name: &str, + attribute: MeshVertexAttribute, + ) -> Self { + self.custom_vertex_attributes + .insert(name.to_string(), attribute); + self + } +} impl Plugin for GltfPlugin { fn build(&self, app: &mut App) { - app.init_asset_loader::() - .register_type::() - .add_asset::() - .add_asset::() - .add_asset::() - .add_asset::(); + let supported_compressed_formats = match app.world.get_resource::() { + Some(render_device) => CompressedImageFormats::from_features(render_device.features()), + + None => CompressedImageFormats::all(), + }; + app.add_asset_loader::(GltfLoader { + supported_compressed_formats, + custom_vertex_attributes: self.custom_vertex_attributes.clone(), + }) + .register_type::() + .add_asset::() + .add_asset::() + .add_asset::() + .add_asset::(); } } diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index cc55af11c4..0cf0cc6b37 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -4,7 +4,7 @@ use bevy_asset::{ }; use bevy_core::Name; use bevy_core_pipeline::prelude::Camera3dBundle; -use bevy_ecs::{entity::Entity, prelude::FromWorld, world::World}; +use bevy_ecs::{entity::Entity, world::World}; use bevy_hierarchy::{BuildWorldChildren, WorldChildBuilder}; use bevy_log::warn; use bevy_math::{Mat4, Vec3}; @@ -17,12 +17,11 @@ use bevy_render::{ color::Color, mesh::{ skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, - Indices, Mesh, VertexAttributeValues, + Indices, Mesh, MeshVertexAttribute, VertexAttributeValues, }, prelude::SpatialBundle, primitives::Aabb, render_resource::{AddressMode, Face, FilterMode, PrimitiveTopology, SamplerDescriptor}, - renderer::RenderDevice, texture::{CompressedImageFormats, Image, ImageSampler, ImageType, TextureError}, }; use bevy_scene::Scene; @@ -32,13 +31,14 @@ use bevy_transform::components::Transform; use bevy_utils::{HashMap, HashSet}; use gltf::{ - mesh::Mode, + mesh::{util::ReadIndices, Mode}, texture::{MagFilter, MinFilter, WrappingMode}, Material, Node, Primitive, }; use std::{collections::VecDeque, path::Path}; use thiserror::Error; +use crate::vertex_attributes::*; use crate::{Gltf, GltfExtras, GltfNode}; /// An error that occurs when loading a glTF file. @@ -68,7 +68,8 @@ pub enum GltfError { /// Loads glTF files with all of their data as their corresponding bevy representations. pub struct GltfLoader { - supported_compressed_formats: CompressedImageFormats, + pub(crate) supported_compressed_formats: CompressedImageFormats, + pub(crate) custom_vertex_attributes: HashMap, } impl AssetLoader for GltfLoader { @@ -77,9 +78,7 @@ impl AssetLoader for GltfLoader { bytes: &'a [u8], load_context: &'a mut LoadContext, ) -> BoxedFuture<'a, Result<()>> { - Box::pin(async move { - Ok(load_gltf(bytes, load_context, self.supported_compressed_formats).await?) - }) + Box::pin(async move { Ok(load_gltf(bytes, load_context, self).await?) }) } fn extensions(&self) -> &[&str] { @@ -87,24 +86,11 @@ impl AssetLoader for GltfLoader { } } -impl FromWorld for GltfLoader { - fn from_world(world: &mut World) -> Self { - let supported_compressed_formats = match world.get_resource::() { - Some(render_device) => CompressedImageFormats::from_features(render_device.features()), - - None => CompressedImageFormats::all(), - }; - Self { - supported_compressed_formats, - } - } -} - /// Loads an entire glTF file. async fn load_gltf<'a, 'b>( bytes: &'a [u8], load_context: &'a mut LoadContext<'b>, - supported_compressed_formats: CompressedImageFormats, + loader: &GltfLoader, ) -> Result<(), GltfError> { let gltf = gltf::Gltf::from_slice(bytes)?; let buffer_data = load_buffers(&gltf, load_context, load_context.path()).await?; @@ -233,53 +219,31 @@ async fn load_gltf<'a, 'b>( let mut primitives = vec![]; for primitive in mesh.primitives() { let primitive_label = primitive_label(&mesh, &primitive); - let reader = primitive.reader(|buffer| Some(&buffer_data[buffer.index()])); let primitive_topology = get_primitive_topology(primitive.mode())?; let mut mesh = Mesh::new(primitive_topology); - if let Some(vertex_attribute) = reader - .read_positions() - .map(|v| VertexAttributeValues::Float32x3(v.collect())) - { - mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, vertex_attribute); - } - - if let Some(vertex_attribute) = reader - .read_normals() - .map(|v| VertexAttributeValues::Float32x3(v.collect())) - { - mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, vertex_attribute); - } - - if let Some(vertex_attribute) = reader - .read_tex_coords(0) - .map(|v| VertexAttributeValues::Float32x2(v.into_f32().collect())) - { - mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, vertex_attribute); - } - - if let Some(vertex_attribute) = reader - .read_colors(0) - .map(|v| VertexAttributeValues::Float32x4(v.into_rgba_f32().collect())) - { - mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, vertex_attribute); - } - - if let Some(iter) = reader.read_joints(0) { - let vertex_attribute = VertexAttributeValues::Uint16x4(iter.into_u16().collect()); - mesh.insert_attribute(Mesh::ATTRIBUTE_JOINT_INDEX, vertex_attribute); - } - - if let Some(vertex_attribute) = reader - .read_weights(0) - .map(|v| VertexAttributeValues::Float32x4(v.into_f32().collect())) - { - mesh.insert_attribute(Mesh::ATTRIBUTE_JOINT_WEIGHT, vertex_attribute); + // Read vertex attributes + for (semantic, accessor) in primitive.attributes() { + match convert_attribute( + semantic, + accessor, + &buffer_data, + &loader.custom_vertex_attributes, + ) { + Ok((attribute, values)) => mesh.insert_attribute(attribute, values), + Err(err) => warn!("{}", err), + } } + // Read vertex indices + let reader = primitive.reader(|buffer| Some(buffer_data[buffer.index()].as_slice())); if let Some(indices) = reader.read_indices() { - mesh.set_indices(Some(Indices::U32(indices.into_u32().collect()))); + mesh.set_indices(Some(match indices { + ReadIndices::U8(is) => Indices::U16(is.map(|x| x as u16).collect()), + ReadIndices::U16(is) => Indices::U16(is.collect()), + ReadIndices::U32(is) => Indices::U32(is.collect()), + })); }; if mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_none() @@ -403,7 +367,7 @@ async fn load_gltf<'a, 'b>( &buffer_data, &linear_textures, load_context, - supported_compressed_formats, + loader.supported_compressed_formats, ) .await?; load_context.set_labeled_asset(&label, LoadedAsset::new(texture)); @@ -422,7 +386,7 @@ async fn load_gltf<'a, 'b>( buffer_data, linear_textures, load_context, - supported_compressed_formats, + loader.supported_compressed_formats, ) .await }); diff --git a/crates/bevy_gltf/src/vertex_attributes.rs b/crates/bevy_gltf/src/vertex_attributes.rs new file mode 100644 index 0000000000..558000cc1b --- /dev/null +++ b/crates/bevy_gltf/src/vertex_attributes.rs @@ -0,0 +1,287 @@ +use bevy_render::{ + mesh::{MeshVertexAttribute, VertexAttributeValues as Values}, + prelude::Mesh, + render_resource::VertexFormat, +}; +use bevy_utils::HashMap; +use gltf::{ + accessor::{DataType, Dimensions}, + mesh::util::{ReadColors, ReadJoints, ReadTexCoords}, +}; +use thiserror::Error; + +/// Represents whether integer data requires normalization +#[derive(Copy, Clone)] +struct Normalization(bool); + +impl Normalization { + fn apply_either( + self, + value: T, + normalized_ctor: impl Fn(T) -> U, + unnormalized_ctor: impl Fn(T) -> U, + ) -> U { + if self.0 { + normalized_ctor(value) + } else { + unnormalized_ctor(value) + } + } +} + +/// An error that occurs when accessing buffer data +#[derive(Error, Debug)] +pub(crate) enum AccessFailed { + #[error("Malformed vertex attribute data")] + MalformedData, + #[error("Unsupported vertex attribute format")] + UnsupportedFormat, +} + +/// Helper for reading buffer data +struct BufferAccessor<'a> { + accessor: gltf::Accessor<'a>, + buffer_data: &'a Vec>, + normalization: Normalization, +} + +impl<'a> BufferAccessor<'a> { + /// Creates an iterator over the elements in this accessor + fn iter(self) -> Result, AccessFailed> { + gltf::accessor::Iter::new(self.accessor, |buffer: gltf::Buffer| { + self.buffer_data.get(buffer.index()).map(|v| v.as_slice()) + }) + .ok_or(AccessFailed::MalformedData) + } + + /// Applies the element iterator to a constructor or fails if normalization is required + fn with_no_norm( + self, + ctor: impl Fn(gltf::accessor::Iter<'a, T>) -> U, + ) -> Result { + if self.normalization.0 { + return Err(AccessFailed::UnsupportedFormat); + } + self.iter().map(ctor) + } + + /// Applies the element iterator and the normalization flag to a constructor + fn with_norm( + self, + ctor: impl Fn(gltf::accessor::Iter<'a, T>, Normalization) -> U, + ) -> Result { + let normalized = self.normalization; + self.iter().map(|v| ctor(v, normalized)) + } +} + +/// An enum of the iterators user by different vertex attribute formats +enum VertexAttributeIter<'a> { + // For reading native WGPU formats + F32(gltf::accessor::Iter<'a, f32>), + U32(gltf::accessor::Iter<'a, u32>), + F32x2(gltf::accessor::Iter<'a, [f32; 2]>), + U32x2(gltf::accessor::Iter<'a, [u32; 2]>), + F32x3(gltf::accessor::Iter<'a, [f32; 3]>), + U32x3(gltf::accessor::Iter<'a, [u32; 3]>), + F32x4(gltf::accessor::Iter<'a, [f32; 4]>), + U32x4(gltf::accessor::Iter<'a, [u32; 4]>), + S16x2(gltf::accessor::Iter<'a, [i16; 2]>, Normalization), + U16x2(gltf::accessor::Iter<'a, [u16; 2]>, Normalization), + S16x4(gltf::accessor::Iter<'a, [i16; 4]>, Normalization), + U16x4(gltf::accessor::Iter<'a, [u16; 4]>, Normalization), + S8x2(gltf::accessor::Iter<'a, [i8; 2]>, Normalization), + U8x2(gltf::accessor::Iter<'a, [u8; 2]>, Normalization), + S8x4(gltf::accessor::Iter<'a, [i8; 4]>, Normalization), + U8x4(gltf::accessor::Iter<'a, [u8; 4]>, Normalization), + // Additional on-disk formats used for RGB colors + U16x3(gltf::accessor::Iter<'a, [u16; 3]>, Normalization), + U8x3(gltf::accessor::Iter<'a, [u8; 3]>, Normalization), +} + +impl<'a> VertexAttributeIter<'a> { + /// Creates an iterator over the elements in a vertex attribute accessor + fn from_accessor( + accessor: gltf::Accessor<'a>, + buffer_data: &'a Vec>, + ) -> Result, AccessFailed> { + let normalization = Normalization(accessor.normalized()); + let format = (accessor.data_type(), accessor.dimensions()); + let acc = BufferAccessor { + accessor, + buffer_data, + normalization, + }; + match format { + (DataType::F32, Dimensions::Scalar) => acc.with_no_norm(VertexAttributeIter::F32), + (DataType::U32, Dimensions::Scalar) => acc.with_no_norm(VertexAttributeIter::U32), + (DataType::F32, Dimensions::Vec2) => acc.with_no_norm(VertexAttributeIter::F32x2), + (DataType::U32, Dimensions::Vec2) => acc.with_no_norm(VertexAttributeIter::U32x2), + (DataType::F32, Dimensions::Vec3) => acc.with_no_norm(VertexAttributeIter::F32x3), + (DataType::U32, Dimensions::Vec3) => acc.with_no_norm(VertexAttributeIter::U32x3), + (DataType::F32, Dimensions::Vec4) => acc.with_no_norm(VertexAttributeIter::F32x4), + (DataType::U32, Dimensions::Vec4) => acc.with_no_norm(VertexAttributeIter::U32x4), + (DataType::I16, Dimensions::Vec2) => acc.with_norm(VertexAttributeIter::S16x2), + (DataType::U16, Dimensions::Vec2) => acc.with_norm(VertexAttributeIter::U16x2), + (DataType::I16, Dimensions::Vec4) => acc.with_norm(VertexAttributeIter::S16x4), + (DataType::U16, Dimensions::Vec4) => acc.with_norm(VertexAttributeIter::U16x4), + (DataType::I8, Dimensions::Vec2) => acc.with_norm(VertexAttributeIter::S8x2), + (DataType::U8, Dimensions::Vec2) => acc.with_norm(VertexAttributeIter::U8x2), + (DataType::I8, Dimensions::Vec4) => acc.with_norm(VertexAttributeIter::S8x4), + (DataType::U8, Dimensions::Vec4) => acc.with_norm(VertexAttributeIter::U8x4), + (DataType::U16, Dimensions::Vec3) => acc.with_norm(VertexAttributeIter::U16x3), + (DataType::U8, Dimensions::Vec3) => acc.with_norm(VertexAttributeIter::U8x3), + _ => Err(AccessFailed::UnsupportedFormat), + } + } + + /// Materializes values for any supported format of vertex attribute + fn into_any_values(self) -> Result { + match self { + VertexAttributeIter::F32(it) => Ok(Values::Float32(it.collect())), + VertexAttributeIter::U32(it) => Ok(Values::Uint32(it.collect())), + VertexAttributeIter::F32x2(it) => Ok(Values::Float32x2(it.collect())), + VertexAttributeIter::U32x2(it) => Ok(Values::Uint32x2(it.collect())), + VertexAttributeIter::F32x3(it) => Ok(Values::Float32x3(it.collect())), + VertexAttributeIter::U32x3(it) => Ok(Values::Uint32x3(it.collect())), + VertexAttributeIter::F32x4(it) => Ok(Values::Float32x4(it.collect())), + VertexAttributeIter::U32x4(it) => Ok(Values::Uint32x4(it.collect())), + VertexAttributeIter::S16x2(it, n) => { + Ok(n.apply_either(it.collect(), Values::Snorm16x2, Values::Sint16x2)) + } + VertexAttributeIter::U16x2(it, n) => { + Ok(n.apply_either(it.collect(), Values::Unorm16x2, Values::Uint16x2)) + } + VertexAttributeIter::S16x4(it, n) => { + Ok(n.apply_either(it.collect(), Values::Snorm16x4, Values::Sint16x4)) + } + VertexAttributeIter::U16x4(it, n) => { + Ok(n.apply_either(it.collect(), Values::Unorm16x4, Values::Uint16x4)) + } + VertexAttributeIter::S8x2(it, n) => { + Ok(n.apply_either(it.collect(), Values::Snorm8x2, Values::Sint8x2)) + } + VertexAttributeIter::U8x2(it, n) => { + Ok(n.apply_either(it.collect(), Values::Unorm8x2, Values::Uint8x2)) + } + VertexAttributeIter::S8x4(it, n) => { + Ok(n.apply_either(it.collect(), Values::Snorm8x4, Values::Sint8x4)) + } + VertexAttributeIter::U8x4(it, n) => { + Ok(n.apply_either(it.collect(), Values::Unorm8x4, Values::Uint8x4)) + } + _ => Err(AccessFailed::UnsupportedFormat), + } + } + + /// Materializes RGBA values, converting compatible formats to Float32x4 + fn into_rgba_values(self) -> Result { + match self { + VertexAttributeIter::U8x3(it, Normalization(true)) => Ok(Values::Float32x4( + ReadColors::RgbU8(it).into_rgba_f32().collect(), + )), + VertexAttributeIter::U16x3(it, Normalization(true)) => Ok(Values::Float32x4( + ReadColors::RgbU16(it).into_rgba_f32().collect(), + )), + VertexAttributeIter::F32x3(it) => Ok(Values::Float32x4( + ReadColors::RgbF32(it).into_rgba_f32().collect(), + )), + VertexAttributeIter::U8x4(it, Normalization(true)) => Ok(Values::Float32x4( + ReadColors::RgbaU8(it).into_rgba_f32().collect(), + )), + VertexAttributeIter::U16x4(it, Normalization(true)) => Ok(Values::Float32x4( + ReadColors::RgbaU16(it).into_rgba_f32().collect(), + )), + s => s.into_any_values(), + } + } + + /// Materializes joint index values, converting compatible formats to Uint16x4 + fn into_joint_index_values(self) -> Result { + match self { + VertexAttributeIter::U8x4(it, Normalization(false)) => { + Ok(Values::Uint16x4(ReadJoints::U8(it).into_u16().collect())) + } + s => s.into_any_values(), + } + } + + /// Materializes texture coordinate values, converting compatible formats to Float32x2 + fn into_tex_coord_values(self) -> Result { + match self { + VertexAttributeIter::U8x2(it, Normalization(true)) => Ok(Values::Float32x2( + ReadTexCoords::U8(it).into_f32().collect(), + )), + VertexAttributeIter::U16x2(it, Normalization(true)) => Ok(Values::Float32x2( + ReadTexCoords::U16(it).into_f32().collect(), + )), + s => s.into_any_values(), + } + } +} + +enum ConversionMode { + Any, + Rgba, + JointIndex, + TexCoord, +} + +#[derive(Error, Debug)] +pub(crate) enum ConvertAttributeError { + #[error("Vertex attribute {0} has format {1:?} but expected {3:?} for target attribute {2}")] + WrongFormat(String, VertexFormat, String, VertexFormat), + #[error("{0} in accessor {1}")] + AccessFailed(AccessFailed, usize), + #[error("Unknown vertex attribute {0}")] + UnknownName(String), +} + +pub(crate) fn convert_attribute( + semantic: gltf::Semantic, + accessor: gltf::Accessor, + buffer_data: &Vec>, + custom_vertex_attributes: &HashMap, +) -> Result<(MeshVertexAttribute, Values), ConvertAttributeError> { + if let Some((attribute, conversion)) = match &semantic { + gltf::Semantic::Positions => Some((Mesh::ATTRIBUTE_POSITION, ConversionMode::Any)), + gltf::Semantic::Normals => Some((Mesh::ATTRIBUTE_NORMAL, ConversionMode::Any)), + gltf::Semantic::Tangents => Some((Mesh::ATTRIBUTE_TANGENT, ConversionMode::Any)), + gltf::Semantic::Colors(0) => Some((Mesh::ATTRIBUTE_COLOR, ConversionMode::Rgba)), + gltf::Semantic::TexCoords(0) => Some((Mesh::ATTRIBUTE_UV_0, ConversionMode::TexCoord)), + gltf::Semantic::Joints(0) => { + Some((Mesh::ATTRIBUTE_JOINT_INDEX, ConversionMode::JointIndex)) + } + gltf::Semantic::Weights(0) => Some((Mesh::ATTRIBUTE_JOINT_WEIGHT, ConversionMode::Any)), + gltf::Semantic::Extras(name) => custom_vertex_attributes + .get(name) + .map(|attr| (attr.clone(), ConversionMode::Any)), + _ => None, + } { + let raw_iter = VertexAttributeIter::from_accessor(accessor.clone(), buffer_data); + let converted_values = raw_iter.and_then(|iter| match conversion { + ConversionMode::Any => iter.into_any_values(), + ConversionMode::Rgba => iter.into_rgba_values(), + ConversionMode::TexCoord => iter.into_tex_coord_values(), + ConversionMode::JointIndex => iter.into_joint_index_values(), + }); + match converted_values { + Ok(values) => { + let loaded_format = VertexFormat::from(&values); + if attribute.format == loaded_format { + Ok((attribute, values)) + } else { + Err(ConvertAttributeError::WrongFormat( + semantic.to_string(), + loaded_format, + attribute.name.to_string(), + attribute.format, + )) + } + } + Err(err) => Err(ConvertAttributeError::AccessFailed(err, accessor.index())), + } + } else { + Err(ConvertAttributeError::UnknownName(semantic.to_string())) + } +} diff --git a/examples/2d/custom_gltf_vertex_attribute.rs b/examples/2d/custom_gltf_vertex_attribute.rs new file mode 100644 index 0000000000..a0d3c21c84 --- /dev/null +++ b/examples/2d/custom_gltf_vertex_attribute.rs @@ -0,0 +1,83 @@ +//! Renders a glTF mesh in 2D with a custom vertex attribute. + +use bevy::gltf::GltfPlugin; +use bevy::prelude::*; +use bevy::reflect::TypeUuid; +use bevy::render::mesh::{MeshVertexAttribute, MeshVertexBufferLayout}; +use bevy::render::render_resource::*; +use bevy::sprite::{ + Material2d, Material2dKey, Material2dPlugin, MaterialMesh2dBundle, Mesh2dHandle, +}; + +/// This vertex attribute supplies barycentric coordinates for each triangle. +/// Each component of the vector corresponds to one corner of a triangle. It's +/// equal to 1.0 in that corner and 0.0 in the other two. Hence, its value in +/// the fragment shader indicates proximity to a corner or the opposite edge. +const ATTRIBUTE_BARYCENTRIC: MeshVertexAttribute = + MeshVertexAttribute::new("Barycentric", 2137464976, VertexFormat::Float32x3); + +fn main() { + App::new() + .insert_resource(AmbientLight { + color: Color::WHITE, + brightness: 1.0 / 5.0f32, + }) + .add_plugins( + DefaultPlugins.set( + GltfPlugin::default() + // Map a custom glTF attribute name to a `MeshVertexAttribute`. + .add_custom_vertex_attribute("_BARYCENTRIC", ATTRIBUTE_BARYCENTRIC), + ), + ) + .add_plugin(Material2dPlugin::::default()) + .add_systems(Startup, setup) + .run(); +} + +fn setup( + mut commands: Commands, + asset_server: Res, + mut materials: ResMut>, +) { + // Add a mesh loaded from a glTF file. This mesh has data for `ATTRIBUTE_BARYCENTRIC`. + let mesh = asset_server.load("models/barycentric/barycentric.gltf#Mesh0/Primitive0"); + commands.spawn(MaterialMesh2dBundle { + mesh: Mesh2dHandle(mesh), + material: materials.add(CustomMaterial {}), + transform: Transform::from_scale(150.0 * Vec3::ONE), + ..default() + }); + + // Add a camera + commands.spawn(Camera2dBundle { ..default() }); +} + +/// This custom material uses barycentric coordinates from +/// `ATTRIBUTE_BARYCENTRIC` to shade a white border around each triangle. The +/// thickness of the border is animated using the global time shader uniform. +#[derive(AsBindGroup, TypeUuid, Debug, Clone)] +#[uuid = "50ffce9e-1582-42e9-87cb-2233724426c0"] +struct CustomMaterial {} + +impl Material2d for CustomMaterial { + fn fragment_shader() -> ShaderRef { + "shaders/custom_gltf_2d.wgsl".into() + } + fn vertex_shader() -> ShaderRef { + "shaders/custom_gltf_2d.wgsl".into() + } + + fn specialize( + descriptor: &mut RenderPipelineDescriptor, + layout: &MeshVertexBufferLayout, + _key: Material2dKey, + ) -> Result<(), SpecializedMeshPipelineError> { + let vertex_layout = layout.get_layout(&[ + Mesh::ATTRIBUTE_POSITION.at_shader_location(0), + Mesh::ATTRIBUTE_COLOR.at_shader_location(1), + ATTRIBUTE_BARYCENTRIC.at_shader_location(2), + ])?; + descriptor.vertex.buffers = vec![vertex_layout]; + Ok(()) + } +} diff --git a/examples/README.md b/examples/README.md index 380ed3974f..4396c9f2dd 100644 --- a/examples/README.md +++ b/examples/README.md @@ -92,6 +92,7 @@ Example | Description [2D Gizmos](../examples/2d/2d_gizmos.rs) | A scene showcasing 2D gizmos [2D Rotation](../examples/2d/rotation.rs) | Demonstrates rotating entities in 2D with quaternions [2D Shapes](../examples/2d/2d_shapes.rs) | Renders a rectangle, circle, and hexagon +[Custom glTF vertex attribute 2D](../examples/2d/custom_gltf_vertex_attribute.rs) | Renders a glTF mesh in 2D with a custom vertex attribute [Manual Mesh 2D](../examples/2d/mesh2d_manual.rs) | Renders a custom mesh "manually" with "mid-level" renderer apis [Mesh 2D](../examples/2d/mesh2d.rs) | Renders a 2d mesh [Mesh 2D With Vertex Colors](../examples/2d/mesh2d_vertex_color_texture.rs) | Renders a 2d mesh with vertex color attributes