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