Implement 2D #31

Closed
opened 2024-10-30 19:16:17 +00:00 by SeanOMik · 7 comments
Owner

Currently, the engine is only 3D. I want to implement 2D since its easier to make art for, and I am no artist lmao. The renderer graph is hard coded by the BasicRenderer::new() function, I need to add a way to get the renderer and manually create the graph. This would make it more versatile and I could maybe create something like Renderer2DPlugin that constructs the render graph for 2D.

Currently, the engine is only 3D. I want to implement 2D since its easier to make art for, and I am no artist lmao. The renderer graph is hard coded by the `BasicRenderer::new()` function, I need to add a way to get the renderer and manually create the graph. This would make it more versatile and I could maybe create something like `Renderer2DPlugin` that constructs the render graph for 2D.
SeanOMik added the
Kind/Feature
Priority
High
labels 2024-10-30 19:16:17 +00:00
Author
Owner

I got a 2D sprite rendering!
image
Its an egg from a free sprite pack I found on itch.io.

Since the Sprite rendering code is just a node in the render graph, I didn't have to do anything special to render 3d (the cube) at the same time.

Sprites are massive though since the dimensions of the images are used to create the quad, meaning that 1 pixel is the same as 1 unit. The cube in the above screenshot is a 1x1x1 cube, so each pixel in the egg texture is the size of the cube.

The camera movement when its projection is orthographic doesn't work very well at all. I need to take a look at that.

I got a 2D sprite rendering! ![image](/attachments/47ce61c3-ab66-4794-b36a-295d58faf22a) Its an egg from a [free sprite pack I found on itch.io](https://cupnooble.itch.io/sprout-lands-asset-pack). Since the Sprite rendering code is just a node in the render graph, I didn't have to do anything special to render 3d (the cube) at the same time. Sprites are massive though since the dimensions of the images are used to create the quad, meaning that 1 pixel is the same as 1 unit. The cube in the above screenshot is a 1x1x1 cube, so each pixel in the egg texture is the size of the cube. The camera movement when its projection is orthographic doesn't work very well at all. I need to take a look at that.
Author
Owner

Not long after the last post, I implemented a working orthographic projection. The camera movement I talked about was actually issues with the projection (who would've expected lol).

2024-11-15_23-12

I also implemented scaling modes for the projection, so you can set the size of the screen to fit a certain amount of world units. The above screenshot was taken with a fix height of 180 world units:

world.spawn((
    Camera2dBundle {
        projection: CameraProjection::Orthographic(OrthographicProjection {
            scale_mode: ScaleMode::Height(180.0),
            ..Default::default()
        }),
        ..Default::default()
    },
    Transform::from_xyz(0.0, 0.0, 0.0),
    FreeFly3dCamera::default(),
));

I still don't have a 2d camera controller implemented, still reusing the 3d one. It should be easy to write a top down controller, so I'll do that next.

Not long after the last post, I implemented a working orthographic projection. The camera movement I talked about was actually issues with the projection (who would've expected lol). ![2024-11-15_23-12](/attachments/6f097fd8-e532-4657-8d7c-7b04f41596d1) I also implemented scaling modes for the projection, so you can set the size of the screen to fit a certain amount of world units. The above screenshot was taken with a fix height of 180 world units: ```rust world.spawn(( Camera2dBundle { projection: CameraProjection::Orthographic(OrthographicProjection { scale_mode: ScaleMode::Height(180.0), ..Default::default() }), ..Default::default() }, Transform::from_xyz(0.0, 0.0, 0.0), FreeFly3dCamera::default(), )); ``` I still don't have a 2d camera controller implemented, still reusing the 3d one. It should be easy to write a top down controller, so I'll do that next.
Author
Owner

The top down controller is complete! I also implemented zooming, but its kind of wonky when I have a fixed height scale mode on the projection since it doesn't zoom into the origin of the screen. I think its because the origin of the projection is not the origin of the screen. I'm not really sure how I would fix that yet...

The top down controller requires the system (top_down_2d_camera_controller) to be added to world, this can be done with the TopDown2dCameraPlugin plugin, or you can manually add it. After its added, just add the TopDown2dCamera component to the camera entity and configure its settings:

TopDown2dCamera {
    speed: 5.0,
    zoom_speed: Some(0.1),
    max_zoom: 0.36,
    min_zoom: 1.0,
}

The zoom can be disabled by setting the zoom_speed to None, then the max and min zoom can be whatever since they wont be used. Since the zoom is actually just modifying the scale parameter of the projection, the max is actually the minimum scale value. As the scale approaches zero, the camera appears to be more zoomed in.

The top down controller is complete! I also implemented zooming, but its kind of wonky when I have a fixed height scale mode on the projection since it doesn't zoom into the origin of the screen. I think its because the origin of the projection is not the origin of the screen. I'm not really sure how I would fix that yet... The top down controller requires the system (`top_down_2d_camera_controller`) to be added to world, this can be done with the `TopDown2dCameraPlugin` plugin, or you can manually add it. After its added, just add the `TopDown2dCamera` component to the camera entity and configure its settings: ```rust TopDown2dCamera { speed: 5.0, zoom_speed: Some(0.1), max_zoom: 0.36, min_zoom: 1.0, } ``` The zoom can be disabled by setting the `zoom_speed` to `None`, then the max and min zoom can be whatever since they wont be used. Since the zoom is actually just modifying the scale parameter of the projection, the max is actually the minimum scale value. As the scale approaches zero, the camera appears to be more zoomed in.
SeanOMik added spent time 2024-11-16 04:52:42 +00:00
3 hours
SeanOMik added spent time 2024-11-16 04:52:54 +00:00
5 hours
Author
Owner

I implemented a TextureAtlas for using sprites from an atlas. I made a little demo that would create an entity with an AtlasSprite (a sprite from an atlas) and animate it!

Video

This was done by a system that would manually move the "frame" of the sprite and wrapping it around when it needs to restart. Its hacky, I'll make something more general next.

Its pretty easy to load an atlas. This code example creates one using a loaded texture, and sets up the grid:

// Load the texture for the atlas
let soldier = resman.request::<Image>("../assets/tiny_rpg_characters/Characters(100x100)/Soldier/Soldier/Soldier.png").unwrap();
soldier.wait_recurse_dependencies_load().unwrap();

// Create a texture atlas and store it as an asset to get a handle
let atlas = resman.store_new(TextureAtlas {
    texture: soldier,
    grid_offset: UVec2::ZERO,
    grid_size: UVec2::new(9, 7),
    cell_size: UVec2::new(100, 100),
    sprite_color: Vec3::ONE,
    pivot: Pivot::default(),
});

// Get a sprite from the atlas grid
let sprite = AtlasSprite::from_atlas_index(atlas, 9);

world.spawn((
    sprite,
    WorldTransform::default(),
    Transform::from_xyz(0.0, 0.0, -10.0),
));

This isn't very useful for more unique atlases, so maybe I should make it just store Vec<URect> inside of it, then a new_grid function that would automatically fill the vec with a grid of rects.

I implemented a `TextureAtlas` for using sprites from an atlas. I made a little demo that would create an entity with an `AtlasSprite` (a sprite from an atlas) and animate it! ![Video](/attachments/2936fe0e-9a29-4d56-b75d-95135060ded0) This was done by a system that would manually move the "frame" of the sprite and wrapping it around when it needs to restart. Its hacky, I'll make something more general next. Its pretty easy to load an atlas. This code example creates one using a loaded texture, and sets up the grid: ```rust // Load the texture for the atlas let soldier = resman.request::<Image>("../assets/tiny_rpg_characters/Characters(100x100)/Soldier/Soldier/Soldier.png").unwrap(); soldier.wait_recurse_dependencies_load().unwrap(); // Create a texture atlas and store it as an asset to get a handle let atlas = resman.store_new(TextureAtlas { texture: soldier, grid_offset: UVec2::ZERO, grid_size: UVec2::new(9, 7), cell_size: UVec2::new(100, 100), sprite_color: Vec3::ONE, pivot: Pivot::default(), }); // Get a sprite from the atlas grid let sprite = AtlasSprite::from_atlas_index(atlas, 9); world.spawn(( sprite, WorldTransform::default(), Transform::from_xyz(0.0, 0.0, -10.0), )); ``` This isn't very useful for more unique atlases, so maybe I should make it just store `Vec<URect>` inside of it, then a `new_grid` function that would automatically fill the vec with a grid of rects.
Author
Owner

Its now easier to animate sprites!

I created an AtlasAnimations component that stores multiple animations for a sprite in it. You can then use AtlasAnimations::get_active to get an ActiveAtlasAnimation component that will define the current animation, and the position in it.

Here's some code:

let animations = AtlasAnimations::new(atlas, &[
    // name, frame_time, frame_indexes
    ("soldier_run", 0.1, 9..=16),
]);
let active_anim = animations.get_active("soldier_run");

world.spawn((
    animations,
    active_anim,
    WorldTransform::default(),
    Transform::from_xyz(0.0, 0.0, -10.0),
));

There's an ECS system that does the actual animation, system_sprite_atlas_animation.

You can also insert a ResHandle<AtlasAnimations> on an entity to use as its animations. This lowers the amount of data being cloned and animations will likely be put on many entities. Updated code example:

let animations = AtlasAnimations::new(atlas, &[
    ("soldier_run", 0.1, 9..=16),
]);
let run_anim = animations.get_active("soldier_run");
let animations = resman.store_new(animations);
Its now easier to animate sprites! I created an [`AtlasAnimations`](https://git.seanomik.net/SeanOMik/lyra-engine/src/commit/558f027b190f6ee1f3447c2f93db7040f5dff872/crates/lyra-game/src/sprite/animation_sheet.rs#L76) component that stores multiple animations for a sprite in it. You can then use `AtlasAnimations::get_active` to get an `ActiveAtlasAnimation` component that will define the current animation, and the position in it. Here's some code: ```rust let animations = AtlasAnimations::new(atlas, &[ // name, frame_time, frame_indexes ("soldier_run", 0.1, 9..=16), ]); let active_anim = animations.get_active("soldier_run"); world.spawn(( animations, active_anim, WorldTransform::default(), Transform::from_xyz(0.0, 0.0, -10.0), )); ``` There's an ECS system that does the actual animation, [`system_sprite_atlas_animation`](https://git.seanomik.net/SeanOMik/lyra-engine/src/commit/558f027b190f6ee1f3447c2f93db7040f5dff872/crates/lyra-game/src/sprite/animation_sheet.rs#L186). You can also insert a `ResHandle<AtlasAnimations>` on an entity to use as its animations. This lowers the amount of data being cloned and animations will likely be put on many entities. Updated code example: ```rust let animations = AtlasAnimations::new(atlas, &[ ("soldier_run", 0.1, 9..=16), ]); let run_anim = animations.get_active("soldier_run"); let animations = resman.store_new(animations); ```
Author
Owner

I've implemented a TileMap!

image

The tile textures in the map were randomly chosen using a weighted distribution. This is the code used to create the tile map:

let grass_tileset = resman
    .request::<Image>("../assets/sprout_lands/Tilesets/Grass.png")
    .unwrap();
grass_tileset.wait_recurse_dependencies_load().unwrap();

let tile_size = UVec2::new(16, 16);
let atlas = resman.store_new(TextureAtlas::from_grid(
    grass_tileset,
    UVec2::new(11, 7),
    tile_size,
));

let map_size = UVec2::new(32, 16);
let mut tilemap = TileMap::new(atlas, map_size, 1, tile_size);

let textures = [
    12, // flat grass
    55, // tall grass
    56, // small two grass
    57, // small three grass
    58, // water puddle
    60, // three flower
];
let weights = [80, 15, 20, 20, 2, 10];

let dist = WeightedIndex::new(&weights).unwrap();
let mut rng = rand::thread_rng();

for y in 0..map_size.y {
    for x in 0..map_size.x {
        let tex = textures[dist.sample(&mut rng)];
        tilemap.insert_tile(0, tex as _, x, y);
    }
}

Its pretty easy, even when using a weighted distribution. To make it easier to position things on the grid of the map, I created a TileMapPos component. You specify the entity that the tile map is spawned on, the x, y position on the tile map and the z-level. Here's some code that spawns eggs at random positions on the tilemap.

There is a small issue where if the camera is moved, randomly lines between the tiles of the grid will appear. I think this is because of floating point precision, but I haven't looked into it yet. I think I would just remove a tiny amount of padding between the tile Transforms.

I've implemented a TileMap! ![image](/attachments/465ec66b-d6e1-48da-a8f9-bc5c0982c955) The tile textures in the map were randomly chosen using a weighted distribution. This is the [code](https://git.seanomik.net/SeanOMik/lyra-engine/src/commit/b25dfa2ade0d287897168c1c0b9a6a22cb94bc56/examples/2d/src/main.rs#L124-L156) used to create the tile map: ```rust let grass_tileset = resman .request::<Image>("../assets/sprout_lands/Tilesets/Grass.png") .unwrap(); grass_tileset.wait_recurse_dependencies_load().unwrap(); let tile_size = UVec2::new(16, 16); let atlas = resman.store_new(TextureAtlas::from_grid( grass_tileset, UVec2::new(11, 7), tile_size, )); let map_size = UVec2::new(32, 16); let mut tilemap = TileMap::new(atlas, map_size, 1, tile_size); let textures = [ 12, // flat grass 55, // tall grass 56, // small two grass 57, // small three grass 58, // water puddle 60, // three flower ]; let weights = [80, 15, 20, 20, 2, 10]; let dist = WeightedIndex::new(&weights).unwrap(); let mut rng = rand::thread_rng(); for y in 0..map_size.y { for x in 0..map_size.x { let tex = textures[dist.sample(&mut rng)]; tilemap.insert_tile(0, tex as _, x, y); } } ``` Its pretty easy, even when using a weighted distribution. To make it easier to position things on the grid of the map, I created a [`TileMapPos`](https://git.seanomik.net/SeanOMik/lyra-engine/src/branch/feat/rendering-2d-31/crates/lyra-game/src/sprite/tilemap.rs#L20-L33) component. You specify the entity that the tile map is spawned on, the x, y position on the tile map and the z-level. Here's some [code](https://git.seanomik.net/SeanOMik/lyra-engine/src/commit/b25dfa2ade0d287897168c1c0b9a6a22cb94bc56/examples/2d/src/main.rs#L205-L241) that spawns eggs at random positions on the tilemap. There is a small issue where if the camera is moved, randomly lines between the tiles of the grid will appear. I think this is because of floating point precision, but I haven't looked into it yet. I think I would just remove a tiny amount of padding between the tile Transforms.
Author
Owner

This was merged with #32

This was merged with #32
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Total time spent: 8 hours
SeanOMik
8 hours
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: SeanOMik/lyra-engine#31
No description provided.