Path: blob/main/crates/bevy_gizmos_render/src/transform_gizmo_render.rs
30635 views
//! Mesh-based rendering for the transform gizmo.1//!2//! Uses [`StandardMaterial`] with `unlit: true` and a dedicated overlay camera3//! on a separate [`RenderLayers`] to render gizmo meshes always-on-top.45use bevy_app::{App, Plugin, PostUpdate, Startup};6use bevy_asset::{Assets, Handle};7use bevy_camera::{8visibility::{RenderLayers, Visibility},9Camera, Camera3d,10};11use bevy_color::Color;12use bevy_ecs::{13component::Component,14hierarchy::ChildOf,15query::{Or, With, Without},16resource::Resource,17schedule::IntoScheduleConfigs,18system::{Commands, Query, Res, ResMut},19};20use bevy_math::{21primitives::{Cone, Cuboid, Cylinder, Torus},22Quat, Vec3,23};24use bevy_mesh::{Mesh, Mesh3d, MeshBuilder, Meshable};25use bevy_pbr::{MeshMaterial3d, StandardMaterial};26use bevy_transform::{27components::{GlobalTransform, Transform},28systems::propagate_transforms_for,29};3031use bevy_gizmos::transform_gizmo::{32TransformGizmoAxis, TransformGizmoCamera, TransformGizmoFocus, TransformGizmoMeshMarker,33TransformGizmoMode, TransformGizmoRoot, TransformGizmoSettings, TransformGizmoState,34AXIS_START_OFFSET, COLOR_VIEW, COLOR_X, COLOR_Y, COLOR_Z, CONE_HEIGHT, CONE_RADIUS,35INACTIVE_ALPHA, ROTATE_RING_RADIUS, SCALE_CUBE_SIZE, SHAFT_LENGTH, SHAFT_RADIUS,36VIEW_CIRCLE_MAJOR, VIEW_CIRCLE_MINOR, VIEW_RING_MAJOR, VIEW_RING_MINOR,37};3839/// The render layer used exclusively for gizmo meshes.40const GIZMO_RENDER_LAYER: usize = 15;4142/// Marker for the internal overlay camera that renders gizmo meshes.43#[derive(Component)]44struct GizmoOverlayCamera;4546#[derive(Resource)]47struct TransformGizmoMaterials {48normal_colors: [Color; 4],49highlight_colors: [Color; 4],50inactive_colors: [Color; 4],51}5253impl TransformGizmoMaterials {54fn axis_index(axis: TransformGizmoAxis) -> usize {55match axis {56TransformGizmoAxis::X => 0,57TransformGizmoAxis::Y => 1,58TransformGizmoAxis::Z => 2,59TransformGizmoAxis::View => 3,60}61}6263fn color(&self, axis: TransformGizmoAxis, highlight: bool, inactive: bool) -> Color {64let i = Self::axis_index(axis);65if highlight {66self.highlight_colors[i]67} else if inactive {68self.inactive_colors[i]69} else {70self.normal_colors[i]71}72}73}7475/// Plugin that adds mesh-based rendering for the transform gizmo.76///77/// Requires [`bevy_gizmos::transform_gizmo::TransformGizmoPlugin`] to be added first78/// for the interaction logic (hover, drag, state).79pub struct TransformGizmoRenderPlugin;8081impl Plugin for TransformGizmoRenderPlugin {82fn build(&self, app: &mut App) {83app.add_systems(84Startup,85spawn_gizmo_meshes.run_if(86bevy_ecs::schedule::common_conditions::resource_exists::<TransformGizmoSettings>,87),88)89.add_systems(90PostUpdate,91(92update_gizmo_meshes,93propagate_transforms_for::<94Or<(95With<TransformGizmoRoot>,96With<GizmoOverlayCamera>,97With<TransformGizmoMeshMarker>,98)>,99>100.ambiguous_with_all(),101)102.chain()103.after(bevy_transform::TransformSystems::Propagate)104.after(bevy_camera::visibility::VisibilitySystems::VisibilityPropagate),105);106}107}108109fn axis_vec(axis: TransformGizmoAxis) -> Vec3 {110match axis {111TransformGizmoAxis::X => Vec3::X,112TransformGizmoAxis::Y => Vec3::Y,113TransformGizmoAxis::Z => Vec3::Z,114TransformGizmoAxis::View => Vec3::ZERO,115}116}117118fn make_unlit_material(color: Color) -> StandardMaterial {119StandardMaterial {120base_color: color,121unlit: true,122cull_mode: None,123..Default::default()124}125}126127fn highlight_color(color: Color) -> Color {128let srgba = color.to_srgba();129Color::srgba(130(srgba.red * 1.4).min(1.0),131(srgba.green * 1.4).min(1.0),132(srgba.blue * 1.4).min(1.0),133srgba.alpha,134)135}136137fn inactive_color(color: Color) -> Color {138let srgba = color.to_srgba();139Color::srgba(140srgba.red * INACTIVE_ALPHA,141srgba.green * INACTIVE_ALPHA,142srgba.blue * INACTIVE_ALPHA,1431.0,144)145}146147fn spawn_gizmo_meshes(148mut commands: Commands,149mut meshes: ResMut<Assets<Mesh>>,150mut materials: ResMut<Assets<StandardMaterial>>,151) {152let gizmo_layer = RenderLayers::layer(GIZMO_RENDER_LAYER);153154let colors = [COLOR_X, COLOR_Y, COLOR_Z, COLOR_VIEW];155let mat_res = TransformGizmoMaterials {156normal_colors: colors,157highlight_colors: colors.map(highlight_color),158inactive_colors: colors.map(inactive_color),159};160161// Helper: create a unique unlit material for a given axis162let mut make_mat = |axis: TransformGizmoAxis| {163materials.add(make_unlit_material(164colors[TransformGizmoMaterials::axis_index(axis)],165))166};167168// Pre-create meshes169let shaft_mesh = meshes.add(Cylinder::new(SHAFT_RADIUS, SHAFT_LENGTH).mesh().build());170let cone_mesh = meshes.add(Cone::new(CONE_RADIUS, CONE_HEIGHT).mesh().build());171let scale_cube_mesh = meshes.add(172Cuboid::new(SCALE_CUBE_SIZE, SCALE_CUBE_SIZE, SCALE_CUBE_SIZE)173.mesh()174.build(),175);176let rotate_torus_mesh = meshes.add(177Torus {178minor_radius: 0.015,179major_radius: ROTATE_RING_RADIUS,180}181.mesh()182.build(),183);184let view_circle_mesh = meshes.add(185Torus {186minor_radius: VIEW_CIRCLE_MINOR,187major_radius: VIEW_CIRCLE_MAJOR,188}189.mesh()190.build(),191);192let view_ring_mesh = meshes.add(193Torus {194minor_radius: VIEW_RING_MINOR,195major_radius: VIEW_RING_MAJOR,196}197.mesh()198.build(),199);200201// Axis rotations: cylinder default is Y-up202let axis_rotation = |axis: TransformGizmoAxis| -> Quat {203match axis {204TransformGizmoAxis::X => Quat::from_rotation_z(-core::f32::consts::FRAC_PI_2),205TransformGizmoAxis::Y | TransformGizmoAxis::View => Quat::IDENTITY,206TransformGizmoAxis::Z => Quat::from_rotation_x(core::f32::consts::FRAC_PI_2),207}208};209210// Spawn root211let root_entity = commands212.spawn((TransformGizmoRoot, Transform::IDENTITY, Visibility::Hidden))213.id();214215// Helper: spawn a child mesh on the gizmo render layer216let spawn_child = |commands: &mut Commands,217mesh: Handle<Mesh>,218material: Handle<StandardMaterial>,219transform: Transform,220axis: TransformGizmoAxis,221mode: TransformGizmoMode| {222let child = commands223.spawn((224Mesh3d(mesh),225MeshMaterial3d(material),226transform,227TransformGizmoMeshMarker { axis, mode },228Visibility::Hidden,229gizmo_layer.clone(),230))231.id();232commands.entity(child).insert(ChildOf(root_entity));233};234235// --- Translate mode ---236for axis in [237TransformGizmoAxis::X,238TransformGizmoAxis::Y,239TransformGizmoAxis::Z,240] {241let mat = make_mat(axis);242spawn_child(243&mut commands,244shaft_mesh.clone(),245mat.clone(),246Transform::from_translation(axis_vec(axis) * (AXIS_START_OFFSET + SHAFT_LENGTH / 2.0))247.with_rotation(axis_rotation(axis)),248axis,249TransformGizmoMode::Translate,250);251spawn_child(252&mut commands,253cone_mesh.clone(),254mat,255Transform::from_translation(256axis_vec(axis) * (AXIS_START_OFFSET + SHAFT_LENGTH + CONE_HEIGHT / 2.0),257)258.with_rotation(axis_rotation(axis)),259axis,260TransformGizmoMode::Translate,261);262}263264// View-plane circle (translate)265spawn_child(266&mut commands,267view_circle_mesh,268make_mat(TransformGizmoAxis::View),269Transform::IDENTITY,270TransformGizmoAxis::View,271TransformGizmoMode::Translate,272);273274// --- Rotate mode ---275for axis in [276TransformGizmoAxis::X,277TransformGizmoAxis::Y,278TransformGizmoAxis::Z,279] {280let mat = make_mat(axis);281let torus_rot = match axis {282TransformGizmoAxis::X => Quat::from_rotation_z(core::f32::consts::FRAC_PI_2),283TransformGizmoAxis::Y | TransformGizmoAxis::View => Quat::IDENTITY,284TransformGizmoAxis::Z => Quat::from_rotation_x(core::f32::consts::FRAC_PI_2),285};286spawn_child(287&mut commands,288rotate_torus_mesh.clone(),289mat,290Transform::from_rotation(torus_rot),291axis,292TransformGizmoMode::Rotate,293);294}295296// View-axis ring (rotate)297spawn_child(298&mut commands,299view_ring_mesh,300make_mat(TransformGizmoAxis::View),301Transform::IDENTITY,302TransformGizmoAxis::View,303TransformGizmoMode::Rotate,304);305306// --- Scale mode ---307for axis in [308TransformGizmoAxis::X,309TransformGizmoAxis::Y,310TransformGizmoAxis::Z,311] {312let mat = make_mat(axis);313spawn_child(314&mut commands,315shaft_mesh.clone(),316mat.clone(),317Transform::from_translation(axis_vec(axis) * (AXIS_START_OFFSET + SHAFT_LENGTH / 2.0))318.with_rotation(axis_rotation(axis)),319axis,320TransformGizmoMode::Scale,321);322spawn_child(323&mut commands,324scale_cube_mesh.clone(),325mat,326Transform::from_translation(327axis_vec(axis) * (AXIS_START_OFFSET + SHAFT_LENGTH + CONE_HEIGHT / 2.0),328),329axis,330TransformGizmoMode::Scale,331);332}333334// --- Overlay camera ---335// This camera renders only the gizmo layer, after the main camera (order: 1),336// without clearing the color buffer — so gizmo meshes appear on top of everything.337commands.spawn((338Camera3d::default(),339Camera {340order: 1,341..Default::default()342},343GizmoOverlayCamera,344RenderLayers::layer(GIZMO_RENDER_LAYER),345Transform::default(),346));347348commands.insert_resource(mat_res);349}350351fn update_gizmo_meshes(352focus: Option<bevy_ecs::system::Single<&GlobalTransform, With<TransformGizmoFocus>>>,353marked_cameras: Query<&GlobalTransform, (With<TransformGizmoCamera>, With<Camera>)>,354all_cameras: Query<355&GlobalTransform,356(357Without<GizmoOverlayCamera>,358Without<TransformGizmoRoot>,359With<Camera>,360),361>,362settings: Option<Res<TransformGizmoSettings>>,363state: Option<Res<TransformGizmoState>>,364materials_res: Option<Res<TransformGizmoMaterials>>,365mut root_query: Query<366(&mut Transform, &mut Visibility),367(With<TransformGizmoRoot>, Without<TransformGizmoMeshMarker>),368>,369mut handle_query: Query<370(371&TransformGizmoMeshMarker,372&mut Visibility,373&MeshMaterial3d<StandardMaterial>,374),375Without<TransformGizmoRoot>,376>,377mut std_materials: ResMut<Assets<StandardMaterial>>,378mut overlay_cam: Query<379&mut Transform,380(381With<GizmoOverlayCamera>,382Without<TransformGizmoRoot>,383Without<TransformGizmoMeshMarker>,384),385>,386) {387let (Some(materials_res), Some(settings), Some(state)) = (materials_res, settings, state)388else {389return;390};391392let Ok((mut root_tf, mut root_vis)) = root_query.single_mut() else {393return;394};395396let Some(global_tf) = focus else {397*root_vis = Visibility::Hidden;398return;399};400let Some(cam_tf): Option<&GlobalTransform> =401bevy_gizmos::resolve_gizmo_camera!(marked_cameras, all_cameras)402else {403*root_vis = Visibility::Hidden;404return;405};406407// Copy main camera transform to overlay camera408if let Ok(mut overlay_tf) = overlay_cam.single_mut() {409*overlay_tf = cam_tf.compute_transform();410}411412*root_vis = Visibility::Inherited;413let pos = global_tf.translation();414415let space = bevy_gizmos::transform_gizmo::effective_space(&settings);416let rotation = bevy_gizmos::transform_gizmo::gizmo_rotation(*global_tf, space);417418let scale = if settings.screen_scale_factor > 0.0 {419(cam_tf.translation() - pos).length() * settings.screen_scale_factor420} else {4211.0422};423424root_tf.translation = pos;425root_tf.rotation = rotation;426root_tf.scale = Vec3::splat(scale);427428let active_axis = if state.active {429state.axis430} else {431state.hovered_axis432};433let dragging = state.active;434435for (handle, mut vis, mat) in &mut handle_query {436if handle.mode != settings.mode {437*vis = Visibility::Hidden;438continue;439}440*vis = Visibility::Inherited;441442// Update the material color in-place (avoids writing MeshMaterial3d)443let is_active = active_axis == Some(handle.axis);444let desired_color = materials_res.color(handle.axis, is_active, dragging && !is_active);445if let Some(mut material) = std_materials.get_mut(&mat.0)446&& material.base_color != desired_color447{448material.base_color = desired_color;449}450}451}452453454