mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 20:23:28 +00:00
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:
parent
5dec3236ac
commit
d74533b407
9 changed files with 561 additions and 72 deletions
|
@ -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)
|
||||
|
|
10
Cargo.toml
10
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"
|
||||
|
|
80
assets/models/barycentric/barycentric.gltf
Normal file
80
assets/models/barycentric/barycentric.gltf
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
36
assets/shaders/custom_gltf_2d.wgsl
Normal file
36
assets/shaders/custom_gltf_2d.wgsl
Normal 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));
|
||||
}
|
|
@ -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,16 +13,42 @@ 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>()
|
||||
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>()
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
287
crates/bevy_gltf/src/vertex_attributes.rs
Normal file
287
crates/bevy_gltf/src/vertex_attributes.rs
Normal 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()))
|
||||
}
|
||||
}
|
83
examples/2d/custom_gltf_vertex_attribute.rs
Normal file
83
examples/2d/custom_gltf_vertex_attribute.rs
Normal 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(())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue