bones_bevy_renderer/
render.rs

1use super::*;
2
3use bevy::render::{camera::ScalingMode, Extract};
4use bevy::sprite::{Anchor, ExtractedSprite, ExtractedSprites};
5use bevy::utils::HashMap;
6use bevy::window::WindowMode;
7use bevy_prototype_lyon::prelude as lyon;
8use bones::{BitSet, ComponentIterBitset};
9use glam::*;
10
11/// Marker component for entities that are rendered in Bevy for bones.
12#[derive(Component)]
13pub struct BevyBonesEntity;
14
15/// Resource containing the entity spawned for all of the bones game renderables.
16#[derive(Resource)]
17pub struct BonesGameEntity(pub Entity);
18impl FromWorld for BonesGameEntity {
19    fn from_world(world: &mut World) -> Self {
20        Self(world.spawn(VisibilityBundle::default()).id())
21    }
22}
23
24/// Resource mapping bones image IDs to their bevy handles.
25#[derive(Resource, Debug, Deref, DerefMut)]
26pub struct BonesImageIds {
27    #[deref]
28    map: HashMap<u32, Handle<Image>>,
29    next_id: u32,
30}
31impl Default for BonesImageIds {
32    fn default() -> Self {
33        Self {
34            map: Default::default(),
35            next_id: 1,
36        }
37    }
38}
39
40impl BonesImageIds {
41    /// Load all bones images into bevy.
42    pub fn load_bones_images(
43        &mut self,
44        bones_assets: &bones::AssetServer,
45        bones_egui_textures: &mut bones::EguiTextures,
46        bevy_images: &mut Assets<Image>,
47        bevy_egui_textures: &mut bevy_egui::EguiUserTextures,
48    ) {
49        for entry in bones_assets.store.asset_ids.iter() {
50            let handle = entry.key();
51            let cid = entry.value();
52            let mut asset = bones_assets.store.assets.get_mut(cid).unwrap();
53            if let Ok(image) = asset.data.try_cast_mut::<bones::Image>() {
54                self.load_bones_image(
55                    handle.typed(),
56                    image,
57                    bones_egui_textures,
58                    bevy_images,
59                    bevy_egui_textures,
60                )
61            }
62        }
63    }
64
65    /// Load a bones image into bevy.
66    pub fn load_bones_image(
67        &mut self,
68        bones_handle: bones::Handle<bones::Image>,
69        image: &mut bones::Image,
70        bones_egui_textures: &mut bones::EguiTextures,
71        bevy_images: &mut Assets<Image>,
72        bevy_egui_textures: &mut bevy_egui::EguiUserTextures,
73    ) {
74        let Self { map, next_id } = self;
75        let mut taken_image = bones::Image::External(0); // Dummy value temporarily
76        std::mem::swap(image, &mut taken_image);
77        if let bones::Image::Data(data) = taken_image {
78            let handle = bevy_images.add(Image::from_dynamic(data, true));
79            let egui_texture = bevy_egui_textures.add_image(handle.clone());
80            bones_egui_textures.insert(bones_handle, egui_texture);
81            map.insert(*next_id, handle);
82            *image = bones::Image::External(*next_id);
83            *next_id += 1;
84
85        // The image has already been loaded. This may happen if multiple asset handles use the same
86        // image data. We will end up visiting the same data twice.
87        } else {
88            // Swap the image back to it's previous value.
89            std::mem::swap(image, &mut taken_image);
90        }
91    }
92}
93
94/// Updates the [`BonesGame`]'s [`bones::ClearColor`] with the [`ClearColor`] from bevy.
95pub fn sync_clear_color(mut clear_color: ResMut<ClearColor>, game: Res<BonesGame>) {
96    for name in &game.sorted_session_keys {
97        let session = game.sessions.get(*name).unwrap();
98        if !session.visible {
99            continue;
100        }
101        if let Some(bones_clear_color) = session.world.get_resource::<bones::ClearColor>() {
102            clear_color.0 = bones_clear_color.0.into_bevy();
103        }
104    }
105}
106
107/// Syncs elements of the bones window
108pub fn sync_bones_window(mut game: ResMut<BonesGame>, mut window_query: Query<&mut Window>) {
109    let mut window = window_query.get_single_mut().unwrap();
110    let bones_window = match game.shared_resource_cell::<bones::Window>() {
111        Some(w) => w,
112        None => {
113            game.insert_shared_resource(bones::Window {
114                size: vec2(window.width(), window.height()),
115                fullscreen: matches!(&window.mode, WindowMode::BorderlessFullscreen),
116                focused: window.focused,
117            });
118            game.shared_resource_cell().unwrap()
119        }
120    };
121    let mut bones_window = bones_window.borrow_mut().unwrap();
122
123    let is_fullscreen = matches!(&window.mode, WindowMode::BorderlessFullscreen);
124    if is_fullscreen != bones_window.fullscreen {
125        window.mode = if bones_window.fullscreen {
126            WindowMode::BorderlessFullscreen
127        } else {
128            WindowMode::Windowed
129        };
130    }
131    bones_window.focused = window.focused;
132    bones_window.size = vec2(window.width(), window.height());
133}
134
135/// Sync bones cameras with Bevy
136pub fn sync_cameras(
137    game: Res<BonesGame>,
138    mut commands: Commands,
139    mut bevy_bones_cameras: Query<Entity, (With<BevyBonesEntity>, With<Camera>)>,
140) {
141    // Collect the bevy cameras that we've created for the bones game
142    let mut bevy_bones_cameras = bevy_bones_cameras.iter_mut();
143
144    // Create a helper callback to add/update a bones camera into the bevy world
145    let mut add_bones_camera = |bones_camera: &bones::Camera,
146                                bones_transform: &bones::Transform| {
147        // Get or spawn an entity for the camera
148        let mut camera_ent = match bevy_bones_cameras.next() {
149            Some(ent) => commands.entity(ent),
150            None => commands.spawn((Camera2dBundle::default(), BevyBonesEntity)),
151        };
152
153        // Insert our updated components on the camera
154        camera_ent.insert((
155            Camera {
156                is_active: bones_camera.active,
157                viewport: bones_camera.viewport.option().map(|x| x.into_bevy()),
158                order: bones_camera.priority as isize,
159                ..default()
160            },
161            OrthographicProjection {
162                scaling_mode: match bones_camera.size {
163                    bones::CameraSize::Fixed { width, height } => {
164                        ScalingMode::Fixed { width, height }
165                    }
166                    bones::CameraSize::Max {
167                        max_width,
168                        max_height,
169                    } => ScalingMode::AutoMax {
170                        max_width,
171                        max_height,
172                    },
173                    bones::CameraSize::Min {
174                        min_width,
175                        min_height,
176                    } => ScalingMode::AutoMin {
177                        min_width,
178                        min_height,
179                    },
180                    bones::CameraSize::Window { pixels_per_unit } => {
181                        ScalingMode::WindowSize(pixels_per_unit)
182                    }
183                    bones::CameraSize::FixedHeight(h) => ScalingMode::FixedVertical(h),
184                    bones::CameraSize::FixedWidth(w) => ScalingMode::FixedHorizontal(w),
185                },
186                ..default()
187            },
188            bones_transform.into_bevy(),
189        ));
190    };
191
192    // Loop through all sessions
193    for session_name in &game.sorted_session_keys {
194        let session = game.sessions.get(*session_name).unwrap();
195        if !session.visible {
196            continue;
197        }
198
199        let world = &session.world;
200
201        // Skip worlds without cameras and transforms
202        if !(world
203            .components
204            .get::<bones::Transform>()
205            .borrow()
206            .bitset()
207            .bit_any()
208            && world
209                .components
210                .get::<bones::Camera>()
211                .borrow()
212                .bitset()
213                .bit_any())
214        {
215            continue;
216        }
217
218        let entities = world.resource::<bones::Entities>();
219        let transforms = world.components.get::<bones::Transform>().borrow();
220        let cameras = world.components.get::<bones::Camera>().borrow();
221
222        // Sync cameras
223        for (_ent, (transform, camera)) in entities.iter_with((&transforms, &cameras)) {
224            // Add each camera to the bevy world
225            add_bones_camera(camera, transform)
226        }
227    }
228
229    // Delete any remaining bevy cameras that don't have bones equivalents.
230    for remaining_ent in bevy_bones_cameras {
231        commands.entity(remaining_ent).despawn()
232    }
233}
234
235/// Extracts the [`bones::Sprite`]s to be used in bevy's rendering.
236pub fn extract_bones_sprites(
237    mut extracted_sprites: ResMut<ExtractedSprites>,
238    game: Extract<Res<BonesGame>>,
239    bones_image_ids: Extract<Res<BonesImageIds>>,
240    bones_renderable_entity: Extract<Res<BonesGameEntity>>,
241) {
242    let Some(bones_assets) = &game.asset_server() else {
243        return;
244    };
245
246    for session_name in &game.sorted_session_keys {
247        let session = game.sessions.get(*session_name).unwrap();
248        if !session.visible {
249            continue;
250        }
251
252        let world = &session.world;
253
254        // Skip worlds without cameras and transforms
255        if !(world
256            .components
257            .get::<bones::Transform>()
258            .borrow()
259            .bitset()
260            .bit_any()
261            && world
262                .components
263                .get::<bones::Camera>()
264                .borrow()
265                .bitset()
266                .bit_any()
267            && (world
268                .components
269                .get::<bones::Sprite>()
270                .borrow()
271                .bitset()
272                .bit_any()
273                || world
274                    .components
275                    .get::<bones::AtlasSprite>()
276                    .borrow()
277                    .bitset()
278                    .bit_any()))
279        {
280            continue;
281        }
282
283        let entities = world.resource::<bones::Entities>();
284        let transforms = world.components.get::<bones::Transform>().borrow();
285        let sprites = world.components.get::<bones::Sprite>().borrow();
286        let atlas_sprites = world.components.get::<bones::AtlasSprite>().borrow();
287
288        // Extract normal sprites
289        let mut z_offset = 0.0;
290        for (_, (sprite, transform)) in entities.iter_with((&sprites, &transforms)) {
291            let sprite_image = match bones_assets.try_get(sprite.image) {
292                Some(Ok(image)) => image,
293                Some(Err(err)) => {
294                    warn!("Sprite {:?} has invalid handle: {err:?}", sprite.image);
295                    continue;
296                }
297                None => {
298                    warn!("Sprite not loaded: {:?}", sprite.image);
299                    continue;
300                }
301            };
302
303            let image_id = if let bones::Image::External(id) = &*sprite_image {
304                *id
305            } else {
306                panic!(
307                    "Images added at runtime not supported yet, \
308                please open an issue."
309                );
310            };
311            extracted_sprites.sprites.push(ExtractedSprite {
312                entity: bones_renderable_entity.0,
313                transform: {
314                    let mut t: Transform = transform.into_bevy();
315                    // Add tiny z offset to enforce a consistent z-sort
316                    t.translation.z += z_offset;
317                    z_offset += 0.00001;
318                    t.into()
319                },
320                color: sprite.color.into_bevy(),
321                rect: None,
322                custom_size: None,
323                image_handle_id: bones_image_ids.get(&image_id).unwrap().id(),
324                flip_x: sprite.flip_x,
325                flip_y: sprite.flip_y,
326                anchor: Anchor::Center.as_vec(),
327            });
328        }
329
330        // Extract atlas sprites
331        for (_, (atlas_sprite, transform)) in entities.iter_with((&atlas_sprites, &transforms)) {
332            let atlas = bones_assets.get(atlas_sprite.atlas);
333            let atlas_image = bones_assets.get(atlas.image);
334            let image_id = if let bones::Image::External(id) = &*atlas_image {
335                *id
336            } else {
337                panic!(
338                    "Images added at runtime not supported yet, \
339                        please open an issue."
340                );
341            };
342            let index = atlas_sprite.index;
343            let y = index / atlas.columns;
344            let x = index - (y * atlas.columns);
345            let cell = Vec2::new(x as f32, y as f32);
346            let current_padding = atlas.padding
347                * Vec2::new(if x > 0 { 1.0 } else { 0.0 }, if y > 0 { 1.0 } else { 0.0 });
348            let min = (atlas.tile_size + current_padding) * cell + atlas.offset;
349            let rect = Rect {
350                min,
351                max: min + atlas.tile_size,
352            };
353            extracted_sprites.sprites.push(ExtractedSprite {
354                entity: bones_renderable_entity.0,
355                transform: transform.into_bevy().into(),
356                color: atlas_sprite.color.into_bevy(),
357                rect: Some(rect),
358                custom_size: None,
359                image_handle_id: bones_image_ids.get(&image_id).unwrap().id(),
360                flip_x: atlas_sprite.flip_x,
361                flip_y: atlas_sprite.flip_y,
362                anchor: Anchor::Center.as_vec(),
363            });
364        }
365    }
366}
367
368/// Extracts the [`bones::TileLayer`]s & [`bones::Tile`]s to be used in bevy's rendering.
369pub fn extract_bones_tilemaps(
370    mut extracted_sprites: ResMut<ExtractedSprites>,
371    game: Extract<Res<BonesGame>>,
372    bones_image_ids: Extract<Res<BonesImageIds>>,
373    bones_renderable_entity: Extract<Res<BonesGameEntity>>,
374) {
375    let Some(bones_assets) = &game.asset_server() else {
376        return;
377    };
378
379    for session_name in &game.sorted_session_keys {
380        let session = game.sessions.get(*session_name).unwrap();
381        if !session.visible {
382            continue;
383        }
384
385        let world = &session.world;
386
387        // Skip worlds without cameras renderable tile layers
388        if !(world
389            .components
390            .get::<bones::Transform>()
391            .borrow()
392            .bitset()
393            .bit_any()
394            && world
395                .components
396                .get::<bones::Camera>()
397                .borrow()
398                .bitset()
399                .bit_any()
400            && world
401                .components
402                .get::<bones::TileLayer>()
403                .borrow()
404                .bitset()
405                .bit_any())
406        {
407            continue;
408        }
409
410        let entities = world.resource::<bones::Entities>();
411        let transforms = world.components.get::<bones::Transform>().borrow();
412        let tile_layers = world.components.get::<bones::TileLayer>().borrow();
413        let tiles = world.components.get::<bones::Tile>().borrow();
414
415        // Extract tiles as sprites
416        for (_, (tile_layer, transform)) in entities.iter_with((&tile_layers, &transforms)) {
417            let atlas = bones_assets.get(tile_layer.atlas);
418            let atlas_image = bones_assets.get(atlas.image);
419            let image_id = if let bones::Image::External(id) = &*atlas_image {
420                *id
421            } else {
422                panic!(
423                    "Images added at runtime not supported yet, \
424                        please open an issue."
425                );
426            };
427
428            for (tile_pos_idx, tile_ent) in tile_layer.tiles.iter().enumerate() {
429                let Some(tile_ent) = tile_ent else { continue };
430                let tile = tiles.get(*tile_ent).unwrap();
431
432                let tile_pos = tile_layer.pos(tile_pos_idx as u32);
433                let tile_offset = tile_pos.as_vec2() * tile_layer.tile_size;
434
435                let sprite_idx = tile.idx;
436                let y = sprite_idx / atlas.columns;
437                let x = sprite_idx - (y * atlas.columns);
438                let cell = Vec2::new(x as f32, y as f32);
439                let current_padding = atlas.padding
440                    * Vec2::new(if x > 0 { 1.0 } else { 0.0 }, if y > 0 { 1.0 } else { 0.0 });
441                let min = (atlas.tile_size + current_padding) * cell + atlas.offset;
442                let rect = Rect {
443                    min,
444                    max: min + atlas.tile_size,
445                };
446                let mut transform = transform.into_bevy();
447                transform.translation += tile_offset.extend(0.0);
448                // Scale up slightly to avoid bleeding between tiles.
449                // TODO: Improve tile rendering
450                // Currently we do a small hack here, scaling up the tiles a little bit, to prevent
451                // visible gaps between tiles. This solution isn't perfect and we probably need to
452                // create a proper tile renderer. That can render multiple tiles on one quad instead
453                // of using a separate quad for each tile.
454                transform.scale += Vec3::new(0.01, 0.01, 0.0);
455                extracted_sprites.sprites.push(ExtractedSprite {
456                    entity: bones_renderable_entity.0,
457                    transform: transform.into(),
458                    color: Color::WHITE,
459                    rect: Some(rect),
460                    custom_size: None,
461                    image_handle_id: bones_image_ids.get(&image_id).unwrap().id(),
462                    flip_x: tile.flip_x,
463                    flip_y: tile.flip_y,
464                    anchor: Anchor::BottomLeft.as_vec(),
465                });
466            }
467        }
468    }
469}
470
471/// Syncs the [`bones::Path2d`]s with bevy's [`lyon::Path`]s.
472pub fn sync_bones_path2ds(
473    game: Res<BonesGame>,
474    mut commands: Commands,
475    mut bevy_bones_path2ds: Query<
476        (Entity, &mut lyon::Path, &mut lyon::Stroke, &mut Transform),
477        With<BevyBonesEntity>,
478    >,
479) {
480    // Collect the bevy path2ds that we've created for the bones game
481    let mut bevy_bones_path2ds = bevy_bones_path2ds.iter_mut();
482
483    // Create a helper callback to add/update a bones path2d into the bevy world
484    let mut add_bones_path2d = |bones_path2d: &bones::Path2d,
485                                bones_transform: &bones::Transform| {
486        // Get or create components for the entity
487        let mut new_components = None;
488        let mut existing_components;
489        let (path, stroke, transform) = match bevy_bones_path2ds.next() {
490            Some((_ent, path, stroke, transform)) => {
491                existing_components = (path, stroke, transform);
492                let (path, stroke, transform) = &mut existing_components;
493                (&mut **path, &mut **stroke, &mut **transform)
494            }
495            None => {
496                let bundle = lyon::ShapeBundle::default();
497                new_components = Some((
498                    bundle.path,
499                    lyon::Stroke::new(Color::default(), 1.0),
500                    Transform::default(),
501                ));
502                let (path, stroke, transform) = new_components.as_mut().unwrap();
503                (path, stroke, transform)
504            }
505        };
506
507        // Update the components
508        *stroke = lyon::Stroke::new(bones_path2d.color.into_bevy(), bones_path2d.thickness);
509        *path = bones_path2d
510            .points
511            .iter()
512            .copied()
513            .enumerate()
514            .fold(lyon::PathBuilder::new(), |mut builder, (i, point)| {
515                if i > 0 && !bones_path2d.line_breaks.contains(&i) {
516                    builder.line_to(point);
517                }
518                builder.move_to(point);
519
520                builder
521            })
522            .build();
523        *transform = bones_transform.into_bevy();
524        // Offset the path towards the camera slightly to make sure it renders on top of a
525        // sprite/etc. if it is applied to an entity with both a sprite and a path.
526        transform.translation.z += 0.0001;
527
528        // Spawn the shape if it doesn't already exist
529        if let Some((path, stroke, transform)) = new_components {
530            commands
531                .spawn(lyon::ShapeBundle { path, ..default() })
532                .insert(transform)
533                .insert(stroke)
534                .insert(BevyBonesEntity);
535        }
536    };
537
538    for session_name in &game.sorted_session_keys {
539        let session = game.sessions.get(*session_name).unwrap();
540        if !session.visible {
541            continue;
542        }
543
544        let world = &session.world;
545
546        // Skip worlds without cameras renderable tile layers
547        if !(world
548            .components
549            .get::<bones::Transform>()
550            .borrow()
551            .bitset()
552            .bit_any()
553            && world
554                .components
555                .get::<bones::Camera>()
556                .borrow()
557                .bitset()
558                .bit_any()
559            && world
560                .components
561                .get::<bones::Path2d>()
562                .borrow()
563                .bitset()
564                .bit_any())
565        {
566            continue;
567        }
568
569        let entities = world.resource::<bones::Entities>();
570        let transforms = world.components.get::<bones::Transform>().borrow();
571        let path2ds = world.components.get::<bones::Path2d>().borrow();
572
573        // Extract tiles as sprites
574        for (_, (path2d, transform)) in entities.iter_with((&path2ds, &transforms)) {
575            add_bones_path2d(path2d, transform);
576        }
577    }
578
579    // Despawn extra path 2ds
580    for (ent, ..) in bevy_bones_path2ds {
581        commands.entity(ent).despawn()
582    }
583}