Last active
April 20, 2025 21:00
-
-
Save DGriffin91/e63e5f7a90b633250c2cf4bf8fd61ef8 to your computer and use it in GitHub Desktop.
Bevy Combine Meshes with Transforms
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// License: Apache-2.0 / MIT | |
// Combine multiple meshes in Bevy with Transforms | |
// Adapted from https://github.com/bevyengine/bevy/blob/main/examples/3d/3d_shapes.rs | |
use std::f32::consts::PI; | |
use bevy::{ | |
math::Vec4Swizzles, | |
prelude::*, | |
render::{ | |
mesh::{Indices, VertexAttributeValues}, | |
render_resource::{Extent3d, PrimitiveTopology, TextureDimension, TextureFormat}, | |
texture::ImageSettings, | |
}, | |
}; | |
fn main() { | |
App::new() | |
.insert_resource(ImageSettings::default_nearest()) | |
.add_plugins(DefaultPlugins) | |
.add_startup_system(setup) | |
.run(); | |
} | |
const X_EXTENT: f32 = 14.; | |
fn setup( | |
mut commands: Commands, | |
mut meshes: ResMut<Assets<Mesh>>, | |
mut images: ResMut<Assets<Image>>, | |
mut materials: ResMut<Assets<StandardMaterial>>, | |
) { | |
let debug_material = materials.add(StandardMaterial { | |
base_color_texture: Some(images.add(uv_debug_texture())), | |
..default() | |
}); | |
let shapes = [ | |
shape::Cube::default().into(), | |
shape::Box::default().into(), | |
shape::Capsule::default().into(), | |
shape::Torus::default().into(), | |
shape::Icosphere::default().into(), | |
shape::UVSphere::default().into(), | |
]; | |
let transforms = (0..shapes.len()) | |
.map(|i| { | |
Transform::from_xyz( | |
-X_EXTENT / 2. + i as f32 / (shapes.len() - 1) as f32 * X_EXTENT, | |
2.0, | |
0.0, | |
) | |
.with_rotation(Quat::from_rotation_x(-PI / 4.)) | |
}) | |
.collect::<Vec<Transform>>(); | |
let combined_mesh = combine_meshes(&shapes, &transforms, true, false, true, false); | |
commands.spawn_bundle(PbrBundle { | |
mesh: meshes.add(combined_mesh), | |
material: debug_material, | |
..default() | |
}); | |
commands.spawn_bundle(PointLightBundle { | |
point_light: PointLight { | |
intensity: 9000.0, | |
range: 100., | |
shadows_enabled: true, | |
..default() | |
}, | |
transform: Transform::from_xyz(8.0, 16.0, 8.0), | |
..default() | |
}); | |
// ground plane | |
commands.spawn_bundle(PbrBundle { | |
mesh: meshes.add(shape::Plane { size: 50. }.into()), | |
material: materials.add(Color::SILVER.into()), | |
..default() | |
}); | |
commands.spawn_bundle(Camera3dBundle { | |
transform: Transform::from_xyz(0.0, 6., 12.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y), | |
..default() | |
}); | |
} | |
/// Creates a colorful test pattern | |
fn uv_debug_texture() -> Image { | |
const TEXTURE_SIZE: usize = 8; | |
let mut palette: [u8; 32] = [ | |
255, 102, 159, 255, 255, 159, 102, 255, 236, 255, 102, 255, 121, 255, 102, 255, 102, 255, | |
198, 255, 102, 198, 255, 255, 121, 102, 255, 255, 236, 102, 255, 255, | |
]; | |
let mut texture_data = [0; TEXTURE_SIZE * TEXTURE_SIZE * 4]; | |
for y in 0..TEXTURE_SIZE { | |
let offset = TEXTURE_SIZE * y * 4; | |
texture_data[offset..(offset + TEXTURE_SIZE * 4)].copy_from_slice(&palette); | |
palette.rotate_right(4); | |
} | |
Image::new_fill( | |
Extent3d { | |
width: TEXTURE_SIZE as u32, | |
height: TEXTURE_SIZE as u32, | |
depth_or_array_layers: 1, | |
}, | |
TextureDimension::D2, | |
&texture_data, | |
TextureFormat::Rgba8UnormSrgb, | |
) | |
} | |
fn combine_meshes( | |
meshes: &[Mesh], | |
transforms: &[Transform], | |
use_normals: bool, | |
use_tangents: bool, | |
use_uvs: bool, | |
use_colors: bool, | |
) -> Mesh { | |
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList); | |
let mut positions: Vec<[f32; 3]> = Vec::new(); | |
let mut normals: Vec<[f32; 3]> = Vec::new(); | |
let mut tangets: Vec<[f32; 4]> = Vec::new(); | |
let mut uvs: Vec<[f32; 2]> = Vec::new(); | |
let mut colors: Vec<[f32; 4]> = Vec::new(); | |
let mut indices: Vec<u32> = Vec::new(); | |
let mut indices_offset = 0; | |
if meshes.len() != transforms.len() { | |
panic!( | |
"meshes.len({}) != transforms.len({})", | |
meshes.len(), | |
transforms.len() | |
); | |
} | |
for (mesh, trans) in meshes.iter().zip(transforms) { | |
if let Indices::U32(mesh_indices) = &mesh.indices().unwrap() { | |
let mat = trans.compute_matrix(); | |
let positions_len; | |
if let Some(VertexAttributeValues::Float32x3(vert_positions)) = | |
&mesh.attribute(Mesh::ATTRIBUTE_POSITION) | |
{ | |
positions_len = vert_positions.len(); | |
for p in vert_positions { | |
positions.push(mat.transform_point3(Vec3::from(*p)).into()); | |
} | |
} else { | |
panic!("no positions") | |
} | |
if use_uvs { | |
if let Some(VertexAttributeValues::Float32x2(vert_uv)) = | |
&mesh.attribute(Mesh::ATTRIBUTE_UV_0) | |
{ | |
for uv in vert_uv { | |
uvs.push(*uv); | |
} | |
} else { | |
panic!("no uvs") | |
} | |
} | |
if use_normals { | |
// Comment below taken from mesh_normal_local_to_world() in mesh_functions.wgsl regarding | |
// transform normals from local to world coordinates: | |
// NOTE: The mikktspace method of normal mapping requires that the world normal is | |
// re-normalized in the vertex shader to match the way mikktspace bakes vertex tangents | |
// and normal maps so that the exact inverse process is applied when shading. Blender, Unity, | |
// Unreal Engine, Godot, and more all use the mikktspace method. Do not change this code | |
// unless you really know what you are doing. | |
// http://www.mikktspace.com/ | |
let inverse_transpose_model = mat.inverse().transpose(); | |
let inverse_transpose_model = Mat3 { | |
x_axis: inverse_transpose_model.x_axis.xyz(), | |
y_axis: inverse_transpose_model.y_axis.xyz(), | |
z_axis: inverse_transpose_model.z_axis.xyz(), | |
}; | |
if let Some(VertexAttributeValues::Float32x3(vert_normals)) = | |
&mesh.attribute(Mesh::ATTRIBUTE_NORMAL) | |
{ | |
for n in vert_normals { | |
normals.push( | |
inverse_transpose_model | |
.mul_vec3(Vec3::from(*n)) | |
.normalize_or_zero() | |
.into(), | |
); | |
} | |
} else { | |
panic!("no normals") | |
} | |
} | |
if use_tangents { | |
if let Some(VertexAttributeValues::Float32x4(vert_tangets)) = | |
&mesh.attribute(Mesh::ATTRIBUTE_TANGENT) | |
{ | |
for t in vert_tangets { | |
tangets.push(*t); | |
} | |
} else { | |
panic!("no tangets") | |
} | |
} | |
if use_colors { | |
if let Some(VertexAttributeValues::Float32x4(vert_colors)) = | |
&mesh.attribute(Mesh::ATTRIBUTE_COLOR) | |
{ | |
for c in vert_colors { | |
colors.push(*c); | |
} | |
} else { | |
panic!("no colors") | |
} | |
} | |
for i in mesh_indices { | |
indices.push(*i + indices_offset); | |
} | |
indices_offset += positions_len as u32; | |
} | |
} | |
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); | |
if use_normals { | |
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals); | |
} | |
if use_tangents { | |
mesh.insert_attribute(Mesh::ATTRIBUTE_TANGENT, tangets); | |
} | |
if use_uvs { | |
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs); | |
} | |
if use_colors { | |
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors); | |
} | |
mesh.set_indices(Some(Indices::U32(indices))); | |
mesh | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment