mirror of
https://github.com/bevyengine/bevy
synced 2024-11-24 21:53:07 +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)
|
* 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/))
|
* 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)
|
* 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"
|
category = "2D Rendering"
|
||||||
wasm = true
|
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]]
|
[[example]]
|
||||||
name = "2d_gizmos"
|
name = "2d_gizmos"
|
||||||
path = "examples/2d/2d_gizmos.rs"
|
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;
|
use bevy_utils::HashMap;
|
||||||
|
|
||||||
mod loader;
|
mod loader;
|
||||||
|
mod vertex_attributes;
|
||||||
pub use loader::*;
|
pub use loader::*;
|
||||||
|
|
||||||
use bevy_app::prelude::*;
|
use bevy_app::prelude::*;
|
||||||
|
@ -12,21 +13,47 @@ use bevy_asset::{AddAsset, Handle};
|
||||||
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
|
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
|
||||||
use bevy_pbr::StandardMaterial;
|
use bevy_pbr::StandardMaterial;
|
||||||
use bevy_reflect::{Reflect, TypeUuid};
|
use bevy_reflect::{Reflect, TypeUuid};
|
||||||
use bevy_render::mesh::Mesh;
|
use bevy_render::{
|
||||||
|
mesh::{Mesh, MeshVertexAttribute},
|
||||||
|
renderer::RenderDevice,
|
||||||
|
texture::CompressedImageFormats,
|
||||||
|
};
|
||||||
use bevy_scene::Scene;
|
use bevy_scene::Scene;
|
||||||
|
|
||||||
/// Adds support for glTF file loading to the app.
|
/// Adds support for glTF file loading to the app.
|
||||||
#[derive(Default)]
|
#[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 {
|
impl Plugin for GltfPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.init_asset_loader::<GltfLoader>()
|
let supported_compressed_formats = match app.world.get_resource::<RenderDevice>() {
|
||||||
.register_type::<GltfExtras>()
|
Some(render_device) => CompressedImageFormats::from_features(render_device.features()),
|
||||||
.add_asset::<Gltf>()
|
|
||||||
.add_asset::<GltfNode>()
|
None => CompressedImageFormats::all(),
|
||||||
.add_asset::<GltfPrimitive>()
|
};
|
||||||
.add_asset::<GltfMesh>();
|
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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ use bevy_asset::{
|
||||||
};
|
};
|
||||||
use bevy_core::Name;
|
use bevy_core::Name;
|
||||||
use bevy_core_pipeline::prelude::Camera3dBundle;
|
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_hierarchy::{BuildWorldChildren, WorldChildBuilder};
|
||||||
use bevy_log::warn;
|
use bevy_log::warn;
|
||||||
use bevy_math::{Mat4, Vec3};
|
use bevy_math::{Mat4, Vec3};
|
||||||
|
@ -17,12 +17,11 @@ use bevy_render::{
|
||||||
color::Color,
|
color::Color,
|
||||||
mesh::{
|
mesh::{
|
||||||
skinning::{SkinnedMesh, SkinnedMeshInverseBindposes},
|
skinning::{SkinnedMesh, SkinnedMeshInverseBindposes},
|
||||||
Indices, Mesh, VertexAttributeValues,
|
Indices, Mesh, MeshVertexAttribute, VertexAttributeValues,
|
||||||
},
|
},
|
||||||
prelude::SpatialBundle,
|
prelude::SpatialBundle,
|
||||||
primitives::Aabb,
|
primitives::Aabb,
|
||||||
render_resource::{AddressMode, Face, FilterMode, PrimitiveTopology, SamplerDescriptor},
|
render_resource::{AddressMode, Face, FilterMode, PrimitiveTopology, SamplerDescriptor},
|
||||||
renderer::RenderDevice,
|
|
||||||
texture::{CompressedImageFormats, Image, ImageSampler, ImageType, TextureError},
|
texture::{CompressedImageFormats, Image, ImageSampler, ImageType, TextureError},
|
||||||
};
|
};
|
||||||
use bevy_scene::Scene;
|
use bevy_scene::Scene;
|
||||||
|
@ -32,13 +31,14 @@ use bevy_transform::components::Transform;
|
||||||
|
|
||||||
use bevy_utils::{HashMap, HashSet};
|
use bevy_utils::{HashMap, HashSet};
|
||||||
use gltf::{
|
use gltf::{
|
||||||
mesh::Mode,
|
mesh::{util::ReadIndices, Mode},
|
||||||
texture::{MagFilter, MinFilter, WrappingMode},
|
texture::{MagFilter, MinFilter, WrappingMode},
|
||||||
Material, Node, Primitive,
|
Material, Node, Primitive,
|
||||||
};
|
};
|
||||||
use std::{collections::VecDeque, path::Path};
|
use std::{collections::VecDeque, path::Path};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::vertex_attributes::*;
|
||||||
use crate::{Gltf, GltfExtras, GltfNode};
|
use crate::{Gltf, GltfExtras, GltfNode};
|
||||||
|
|
||||||
/// An error that occurs when loading a glTF file.
|
/// 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.
|
/// Loads glTF files with all of their data as their corresponding bevy representations.
|
||||||
pub struct GltfLoader {
|
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 {
|
impl AssetLoader for GltfLoader {
|
||||||
|
@ -77,9 +78,7 @@ impl AssetLoader for GltfLoader {
|
||||||
bytes: &'a [u8],
|
bytes: &'a [u8],
|
||||||
load_context: &'a mut LoadContext,
|
load_context: &'a mut LoadContext,
|
||||||
) -> BoxedFuture<'a, Result<()>> {
|
) -> BoxedFuture<'a, Result<()>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move { Ok(load_gltf(bytes, load_context, self).await?) })
|
||||||
Ok(load_gltf(bytes, load_context, self.supported_compressed_formats).await?)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extensions(&self) -> &[&str] {
|
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.
|
/// Loads an entire glTF file.
|
||||||
async fn load_gltf<'a, 'b>(
|
async fn load_gltf<'a, 'b>(
|
||||||
bytes: &'a [u8],
|
bytes: &'a [u8],
|
||||||
load_context: &'a mut LoadContext<'b>,
|
load_context: &'a mut LoadContext<'b>,
|
||||||
supported_compressed_formats: CompressedImageFormats,
|
loader: &GltfLoader,
|
||||||
) -> Result<(), GltfError> {
|
) -> Result<(), GltfError> {
|
||||||
let gltf = gltf::Gltf::from_slice(bytes)?;
|
let gltf = gltf::Gltf::from_slice(bytes)?;
|
||||||
let buffer_data = load_buffers(&gltf, load_context, load_context.path()).await?;
|
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![];
|
let mut primitives = vec![];
|
||||||
for primitive in mesh.primitives() {
|
for primitive in mesh.primitives() {
|
||||||
let primitive_label = primitive_label(&mesh, &primitive);
|
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 primitive_topology = get_primitive_topology(primitive.mode())?;
|
||||||
|
|
||||||
let mut mesh = Mesh::new(primitive_topology);
|
let mut mesh = Mesh::new(primitive_topology);
|
||||||
|
|
||||||
if let Some(vertex_attribute) = reader
|
// Read vertex attributes
|
||||||
.read_positions()
|
for (semantic, accessor) in primitive.attributes() {
|
||||||
.map(|v| VertexAttributeValues::Float32x3(v.collect()))
|
match convert_attribute(
|
||||||
{
|
semantic,
|
||||||
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, vertex_attribute);
|
accessor,
|
||||||
}
|
&buffer_data,
|
||||||
|
&loader.custom_vertex_attributes,
|
||||||
if let Some(vertex_attribute) = reader
|
) {
|
||||||
.read_normals()
|
Ok((attribute, values)) => mesh.insert_attribute(attribute, values),
|
||||||
.map(|v| VertexAttributeValues::Float32x3(v.collect()))
|
Err(err) => warn!("{}", err),
|
||||||
{
|
}
|
||||||
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 indices
|
||||||
|
let reader = primitive.reader(|buffer| Some(buffer_data[buffer.index()].as_slice()));
|
||||||
if let Some(indices) = reader.read_indices() {
|
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()
|
if mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_none()
|
||||||
|
@ -403,7 +367,7 @@ async fn load_gltf<'a, 'b>(
|
||||||
&buffer_data,
|
&buffer_data,
|
||||||
&linear_textures,
|
&linear_textures,
|
||||||
load_context,
|
load_context,
|
||||||
supported_compressed_formats,
|
loader.supported_compressed_formats,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
load_context.set_labeled_asset(&label, LoadedAsset::new(texture));
|
load_context.set_labeled_asset(&label, LoadedAsset::new(texture));
|
||||||
|
@ -422,7 +386,7 @@ async fn load_gltf<'a, 'b>(
|
||||||
buffer_data,
|
buffer_data,
|
||||||
linear_textures,
|
linear_textures,
|
||||||
load_context,
|
load_context,
|
||||||
supported_compressed_formats,
|
loader.supported_compressed_formats,
|
||||||
)
|
)
|
||||||
.await
|
.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 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 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
|
[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
|
[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](../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
|
[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