Add support for custom glTF vertex attributes. (#5370)

# Objective

The objective is to be able to load data from "application-specific"
(see glTF spec 3.7.2.1.) vertex attribute semantics from glTF files into
Bevy meshes.

## Solution

Rather than probe the glTF for the specific attributes supported by
Bevy, this PR changes the loader to iterate through all the attributes
and map them onto `MeshVertexAttribute`s. This mapping includes all the
previously supported attributes, plus it is now possible to add mappings
using the `add_custom_vertex_attribute()` method on `GltfPlugin`.

## Changelog

- Add support for loading custom vertex attributes from glTF files.
- Add the `custom_gltf_vertex_attribute.rs` example to illustrate
loading custom vertex attributes.

## Migration Guide

- If you were instantiating `GltfPlugin` using the unit-like struct
syntax, you must instead use `GltfPlugin::default()` as the type is no
longer unit-like.
This commit is contained in:
Robin KAY 2023-04-24 15:20:13 +01:00 committed by GitHub
parent 5dec3236ac
commit d74533b407
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 561 additions and 72 deletions

View file

@ -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)

View file

@ -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"

View file

@ -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
}
]
}
]
}

View file

@ -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<f32>,
@location(1) color: vec4<f32>,
@location(2) barycentric: vec3<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) barycentric: vec3<f32>,
};
@vertex
fn vertex(vertex: Vertex) -> VertexOutput {
var out: VertexOutput;
out.clip_position = mesh2d_position_local_to_clip(mesh.model, vec4<f32>(vertex.position, 1.0));
out.color = vertex.color;
out.barycentric = vertex.barycentric;
return out;
}
struct FragmentInput {
@location(0) color: vec4<f32>,
@location(1) barycentric: vec3<f32>,
};
@fragment
fn fragment(input: FragmentInput) -> @location(0) vec4<f32> {
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));
}

View file

@ -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<String, MeshVertexAttribute>,
}
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::<GltfLoader>()
.register_type::<GltfExtras>()
.add_asset::<Gltf>()
.add_asset::<GltfNode>()
.add_asset::<GltfPrimitive>()
.add_asset::<GltfMesh>();
let supported_compressed_formats = match app.world.get_resource::<RenderDevice>() {
Some(render_device) => CompressedImageFormats::from_features(render_device.features()),
None => CompressedImageFormats::all(),
};
app.add_asset_loader::<GltfLoader>(GltfLoader {
supported_compressed_formats,
custom_vertex_attributes: self.custom_vertex_attributes.clone(),
})
.register_type::<GltfExtras>()
.add_asset::<Gltf>()
.add_asset::<GltfNode>()
.add_asset::<GltfPrimitive>()
.add_asset::<GltfMesh>();
}
}

View file

@ -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<String, MeshVertexAttribute>,
}
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::<RenderDevice>() {
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
});

View file

@ -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<T, U>(
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<Vec<u8>>,
normalization: Normalization,
}
impl<'a> BufferAccessor<'a> {
/// Creates an iterator over the elements in this accessor
fn iter<T: gltf::accessor::Item>(self) -> Result<gltf::accessor::Iter<'a, T>, 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<T: gltf::accessor::Item, U>(
self,
ctor: impl Fn(gltf::accessor::Iter<'a, T>) -> U,
) -> Result<U, AccessFailed> {
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<T: gltf::accessor::Item, U>(
self,
ctor: impl Fn(gltf::accessor::Iter<'a, T>, Normalization) -> U,
) -> Result<U, AccessFailed> {
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<Vec<u8>>,
) -> Result<VertexAttributeIter<'a>, 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<Values, AccessFailed> {
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<Values, AccessFailed> {
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<Values, AccessFailed> {
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<Values, AccessFailed> {
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<Vec<u8>>,
custom_vertex_attributes: &HashMap<String, MeshVertexAttribute>,
) -> 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()))
}
}

View file

@ -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::<CustomMaterial>::default())
.add_systems(Startup, setup)
.run();
}
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut materials: ResMut<Assets<CustomMaterial>>,
) {
// 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<Self>,
) -> 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(())
}
}

View file

@ -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