A crates/client/src/components.rs => crates/client/src/components.rs +28 -0
@@ 0,0 1,28 @@
+use bevy_ecs::{component::Component, system::Resource};
+use nalgebra::{Matrix4, Rotation3, Scale3, Translation3};
+
+#[derive(Component)]
+pub struct Texture {
+ pub name: String,
+}
+
+#[derive(Component, Debug)]
+pub struct Transform {
+ pub translation: Translation3<f32>,
+ pub rotation: Rotation3<f32>,
+ pub scale: Scale3<f32>,
+}
+impl Transform {
+ pub fn to_matrix(&self) -> Matrix4<f32> {
+ self.translation.to_homogeneous() * self.rotation.to_homogeneous() * self.scale.to_homogeneous()
+ }
+}
+
+#[derive(Resource, Debug)]
+pub struct Camera {
+ pub x: f32,
+ pub y: f32,
+ pub zoom: f32,
+ pub width: u32, // screen width (these are for aspect ratio)
+ pub height: u32, // screen height
+}
M crates/client/src/lib.rs => crates/client/src/lib.rs +23 -3
@@ 1,4 1,7 @@
-use rendering::App;
+use bevy_ecs::world::World;
+use components::{Camera, Texture, Transform};
+use nalgebra::{Rotation2, Rotation3, Scale2, Scale3, Translation2, Translation3, Vector3};
+use rendering::{assets::Assets, App};
use tracing::info;
use winit::event_loop::{ControlFlow, EventLoop};
@@ 10,6 13,7 @@ pub mod platform;
pub mod platform;
pub mod rendering;
+pub mod components;
// Hi, you've found the real main function! This is called AFTER platform-specific initialization code.
pub fn start() {
@@ 24,10 28,26 @@ pub fn start() {
);
info!("Creating the ECS world...");
+ let mut world = World::new();
+
+ world.insert_resource(Camera {
+ x: 0.0,
+ y: 0.0,
+ zoom: 1.0,
+ width: 0,
+ height: 0,
+ });
+
+ world.insert_resource(Assets::new());
+
+ world.spawn((Transform {
+ translation: Translation3::new(0.0, 0.0, 0.0),
+ rotation: Rotation3::from_axis_angle(&Vector3::z_axis(), 0.0),
+ scale: Scale3::new(20.0, 20.0, 1.0),
+ }, Texture { name: "hearty.svg".to_string() }));
let event_loop = EventLoop::new().unwrap();
-
event_loop.set_control_flow(ControlFlow::Wait);
- event_loop.run_app(&mut App::default()).unwrap();
+ event_loop.run_app(&mut App::new(world)).unwrap();
}
A crates/client/src/rendering/assets_native.rs => crates/client/src/rendering/assets_native.rs +15 -0
@@ 0,0 1,15 @@
+use bevy_ecs::system::Resource;
+
+#[derive(Resource)]
+pub struct Assets {
+
+}
+impl Assets {
+ pub fn new() -> Self {
+ Assets { }
+ }
+ pub fn get(&self, local_path: impl Into<String>) -> Option<Vec<u8>> {
+ std::fs::read(format!("src/textures/{}", local_path.into())).ok()
+ }
+}
+
A crates/client/src/rendering/assets_wasm.rs => crates/client/src/rendering/assets_wasm.rs +111 -0
@@ 0,0 1,111 @@
+use std::{collections::HashMap, fmt::Display, sync::{Arc, Mutex}};
+
+use bevy_ecs::system::Resource;
+use image::EncodableLayout;
+use poll_promise::Promise;
+use resvg::{tiny_skia, usvg};
+
+#[derive(Debug, Clone)]
+pub enum AssetError {
+ AssetNotFound,
+ ResponseNotOk(u16),
+}
+impl Display for AssetError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ AssetError::AssetNotFound => write!(f, "Asset not found"),
+ AssetError::ResponseNotOk(code) => write!(f, "Server response was not ok {}", code),
+ }
+ }
+}
+impl std::error::Error for AssetError {}
+
+#[derive(Debug, Clone)]
+pub struct ImgData {
+ pub bytes: Vec<u8>,
+ pub width: u32,
+ pub height: u32,
+}
+
+#[derive(Resource)]
+pub struct Assets {
+ texture_promises: Arc<Mutex<HashMap<String, Promise<ImgData>>>>,
+ textures: Arc<Mutex<HashMap<String, ImgData>>>,
+}
+
+impl Assets {
+ pub fn new() -> Self {
+ Assets {
+ textures: Arc::new(Mutex::new(HashMap::new())),
+ texture_promises: Arc::new(Mutex::new(HashMap::new())),
+ }
+ }
+ pub fn get(&self, local_path: impl Into<String>) -> Option<ImgData> {
+ let local_path = local_path.into();
+ let contains_texture = {
+ self.textures.lock().unwrap().contains_key(&local_path)
+ };
+ let contains_texture_promise = {
+ self.texture_promises.lock().unwrap().contains_key(&local_path)
+ };
+ if !contains_texture && !contains_texture_promise {
+ let local_path_clone = local_path.clone();
+ let request_promise = poll_promise::Promise::spawn_local(async move {
+ let window = web_sys::window().unwrap();
+ let request = ehttp::Request::get(format!("{}/src/assets/{}", window.location().origin().unwrap(), local_path_clone));
+ let response = match ehttp::fetch_async(request).await {
+ Ok(resp) => resp,
+ Err(e) => {
+ panic!("{}", e);
+ },
+ };
+ if local_path_clone.ends_with(".svg") {
+ let opt = usvg::Options {
+ default_size: usvg::Size::from_wh(20.0, 20.0).unwrap(),
+ ..Default::default()
+ };
+ let tree = usvg::Tree::from_data(&response.bytes, &opt).expect(&format!("Couldn't parse svg {}", local_path_clone));
+ let tree_size = tree.size().to_int_size();
+ let size = usvg::Size::from_wh(200.0, 200.0).unwrap().to_int_size();
+ assert!(size.width() > 0 && size.height() > 0);
+ let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).expect("Failed to construct pixmap");
+ resvg::render(&tree, tiny_skia::Transform::from_scale((size.width() as f32)/(tree_size.height() as f32), (size.height() as f32)/(tree_size.height() as f32)), &mut pixmap.as_mut());
+ let data = ImgData {
+ bytes: pixmap.data().to_vec(),
+ width: size.width(),
+ height: size.height(),
+ };
+
+ data
+ } else if local_path_clone.ends_with(".png") {
+ let img = image::load_from_memory(&response.bytes).unwrap();
+ let rgba = img.to_rgba8();
+ let data = ImgData {
+ bytes: rgba.as_bytes().to_vec(),
+ width: rgba.width(),
+ height: rgba.height(),
+ };
+ data
+ } else {
+ panic!("Unsupported sprite type");
+ }
+ });
+ {
+ self.texture_promises.lock().unwrap().insert(local_path.clone(), request_promise);
+ }
+ None
+ } else if !contains_texture {
+ let mut texture_promises = self.texture_promises.lock().unwrap();
+ let promise = texture_promises.get_mut(&local_path).unwrap();
+ let mut returned_value = None;
+ if let Some(texture) = promise.ready() {
+ self.textures.lock().unwrap().insert(local_path.clone(), texture.clone());
+ returned_value = Some(texture.clone());
+ texture_promises.remove(&local_path);
+ }
+ return returned_value
+ } else {
+ self.textures.lock().unwrap().get(&local_path).cloned()
+ }
+ }
+}
D crates/client/src/rendering/init.rs => crates/client/src/rendering/init.rs +0 -0
M crates/client/src/rendering/mod.rs => crates/client/src/rendering/mod.rs +75 -15
@@ 1,6 1,9 @@
+use std::collections::HashMap;
use std::num::NonZeroU32;
use std::sync::Arc;
+use assets::Assets;
+use bevy_ecs::world::World;
use egui_glow::EguiGlow;
use glow::{HasContext, PixelUnpackData};
#[cfg(not(target_arch = "wasm32"))]
@@ 18,22 21,30 @@ use winit::event_loop::ControlFlow;
use winit::platform::web::{WindowAttributesExtWebSys, WindowExtWebSys};
use winit::{application::ApplicationHandler, dpi::LogicalSize, event::WindowEvent, event_loop::ActiveEventLoop, raw_window_handle::HasWindowHandle, window::{Window, WindowAttributes}};
-pub mod init;
+use crate::components::{Camera, Texture, Transform};
+#[cfg(not(target_arch="wasm32"))]
+#[path = "assets_native.rs"]
+pub mod assets;
+#[cfg(target_arch="wasm32")]
+#[path = "assets_wasm.rs"]
+pub mod assets;
#[derive(Default)]
pub struct App {
window: Option<Window>,
+ world: World,
+
program: Option<glow::Program>,
vertex_array: Option<glow::VertexArray>,
vertex_buffer: Option<glow::Buffer>,
element_buffer: Option<glow::Buffer>,
- texture_object: Option<glow::Texture>,
#[cfg(not(target_arch = "wasm32"))]
gl_surface: Option<Surface<WindowSurface>>,
#[cfg(not(target_arch = "wasm32"))]
gl_context: Option<PossiblyCurrentContext>,
egui_glow: Option<EguiGlow>,
gl: Option<Arc<glow::Context>>,
+ textures: HashMap<String, glow::Texture>,
}
const VERTICES: [f32; 16] = [
@@ 47,6 58,15 @@ const INDICES: [u32; 6] = [
2, 3, 0,
];
+impl App {
+ pub fn new(world: World) -> Self {
+ Self {
+ world,
+ ..Default::default()
+ }
+ }
+}
+
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
#[cfg(target_arch = "wasm32")]
@@ 157,15 177,6 @@ impl ApplicationHandler for App {
gl.vertex_attrib_pointer_f32(1, 2, glow::FLOAT, false, 4*size_of::<f32>() as i32, 2*size_of::<f32>() as i32);
gl.enable_vertex_attrib_array(1);
- let texture = gl.create_texture().expect("Failed to create texture object");
- gl.active_texture(glow::TEXTURE0);
- gl.bind_texture(glow::TEXTURE_2D, Some(texture));
- let image = image::load_from_memory(include_bytes!("../assets/happy-tree.png")).unwrap();
- let image = image.to_rgba8();
- gl.tex_image_2d(glow::TEXTURE_2D, 0, glow::RGBA as i32,
- image.width() as i32, image.height() as i32, 0, glow::RGBA,
- glow::UNSIGNED_BYTE, PixelUnpackData::Slice(Some(&image.into_raw())));
- gl.generate_mipmap(glow::TEXTURE_2D);
gl.clear_color(0.0, 0.0, 0.0, 0.0);
gl.viewport(0, 0, window.inner_size().width as i32, window.inner_size().height as i32);
@@ 176,7 187,6 @@ impl ApplicationHandler for App {
self.vertex_array = Some(vertex_array);
self.vertex_buffer = Some(vertex_buffer);
self.element_buffer = Some(element_buffer);
- self.texture_object = Some(texture);
}
#[cfg(target_arch = "wasm32")]
web_sys::window().unwrap().set_onresize(Some(Closure::<dyn Fn(Event)>::new(move |_| {
@@ 207,6 217,10 @@ impl ApplicationHandler for App {
self.gl_surface.as_ref().unwrap().resize(self.gl_context.as_ref().unwrap(),
NonZeroU32::new(size.width).unwrap(), NonZeroU32::new(size.height).unwrap());
+ let mut camera = self.world.get_resource_mut::<Camera>().unwrap();
+ camera.width = size.width;
+ camera.height = size.height;
+
unsafe {
self.gl.as_ref().unwrap().viewport(0, 0, size.width as i32, size.height as i32);
}
@@ 230,15 244,61 @@ impl ApplicationHandler for App {
},
);
+ let camera = self.world.get_resource::<Camera>().unwrap();
+ let x_scale = camera.zoom / camera.width as f32 * 2.0;
+ let y_scale = camera.zoom / camera.height as f32 * 2.0;
+ let view = &[
+ x_scale, 0.0, 0.0, camera.x*x_scale,
+ 0.0, y_scale,0.0, camera.y*y_scale,
+ 0.0, 0.0, 1.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ ];
+
+ let mut sprite_query = self.world.query::<(&Transform, &mut Texture)>();
+
+ let mut sprites = Vec::new();
+ for (transform, texture) in sprite_query.iter(&self.world) {
+ sprites.push((transform, texture));
+ }
+
unsafe {
gl.clear(glow::COLOR_BUFFER_BIT);
gl.use_program(self.program);
- gl.active_texture(glow::TEXTURE0);
- gl.bind_texture(glow::TEXTURE_2D, self.texture_object);
gl.bind_vertex_array(self.vertex_array);
gl.bind_buffer(glow::ARRAY_BUFFER, self.vertex_buffer);
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, self.element_buffer);
- gl.draw_elements(glow::TRIANGLES, 6, glow::UNSIGNED_INT, 0);
+ gl.active_texture(glow::TEXTURE0);
+
+ let view_loc = gl.get_uniform_location(self.program.unwrap(), "view");
+ gl.uniform_matrix_4_f32_slice(view_loc.as_ref(), true, view);
+ let model_loc = gl.get_uniform_location(self.program.unwrap(), "model");
+
+ for (transform, texture) in sprites {
+ if !self.textures.contains_key(&texture.name) {
+ let assets = self.world.resource::<Assets>();
+ let image = match assets.get(texture.name.clone()) {
+ Some(t) => t,
+ None => continue
+ };
+
+ let texture_object = gl.create_texture().expect("Failed to create texture object");
+ gl.bind_texture(glow::TEXTURE_2D, Some(texture_object));
+ gl.tex_image_2d(glow::TEXTURE_2D, 0, glow::RGBA as i32,
+ image.width as i32, image.height as i32, 0, glow::RGBA,
+ glow::UNSIGNED_BYTE, PixelUnpackData::Slice(Some(&image.bytes)));
+ gl.generate_mipmap(glow::TEXTURE_2D);
+
+ self.textures.insert(texture.name.clone(), texture_object);
+ }
+ // now the texture must exist
+
+ let model = transform.to_matrix();
+ let model = model.as_slice();
+ gl.uniform_matrix_4_f32_slice(model_loc.as_ref(), false, model);
+
+ gl.bind_texture(glow::TEXTURE_2D, self.textures.get(&texture.name).copied());
+ gl.draw_elements(glow::TRIANGLES, 6, glow::UNSIGNED_INT, 0);
+ }
}
self.egui_glow.as_mut().unwrap().paint(self.window.as_ref().unwrap());
M crates/client/src/shaders/vertex.glsl => crates/client/src/shaders/vertex.glsl +4 -3
@@ 3,11 3,12 @@ precision mediump float;
in vec2 pos;
in vec2 texcoord;
-out vec2 v_pos;
out vec2 v_texcoord;
+uniform mat4 model;
+uniform mat4 view;
+
void main() {
- v_pos = pos;
v_texcoord = texcoord;
- gl_Position = vec4(v_pos, 0.0, 1.0);
+ gl_Position = view * model * vec4(pos, 0.0, 1.0);
}