From 4174a429888ef87273b8513556a6a259b52798b9 Mon Sep 17 00:00:00 2001 From: ghostly_zsh Date: Sat, 22 Mar 2025 11:51:40 -0500 Subject: [PATCH] camera and other transform added, also bevy --- crates/client/src/components.rs | 28 +++++ crates/client/src/lib.rs | 26 ++++- crates/client/src/rendering/assets_native.rs | 15 +++ crates/client/src/rendering/assets_wasm.rs | 111 +++++++++++++++++++ crates/client/src/rendering/init.rs | 0 crates/client/src/rendering/mod.rs | 90 ++++++++++++--- crates/client/src/shaders/vertex.glsl | 7 +- 7 files changed, 256 insertions(+), 21 deletions(-) create mode 100644 crates/client/src/components.rs create mode 100644 crates/client/src/rendering/assets_native.rs create mode 100644 crates/client/src/rendering/assets_wasm.rs delete mode 100644 crates/client/src/rendering/init.rs diff --git a/crates/client/src/components.rs b/crates/client/src/components.rs new file mode 100644 index 0000000000000000000000000000000000000000..0015b147040c1e7a4d2439b6bbe399acdcf29c83 --- /dev/null +++ b/crates/client/src/components.rs @@ -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, + pub rotation: Rotation3, + pub scale: Scale3, +} +impl Transform { + pub fn to_matrix(&self) -> Matrix4 { + 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 +} diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index bdc3cc01fb7dee029441c703258ce4f4c15b1b63..164964d2594365513f086d72b5d7541b309744f7 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -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(); } diff --git a/crates/client/src/rendering/assets_native.rs b/crates/client/src/rendering/assets_native.rs new file mode 100644 index 0000000000000000000000000000000000000000..2ef454a7c946821caddfe21aa78771a457ef5cd5 --- /dev/null +++ b/crates/client/src/rendering/assets_native.rs @@ -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) -> Option> { + std::fs::read(format!("src/textures/{}", local_path.into())).ok() + } +} + diff --git a/crates/client/src/rendering/assets_wasm.rs b/crates/client/src/rendering/assets_wasm.rs new file mode 100644 index 0000000000000000000000000000000000000000..24063090370e1c2b1d0afcb5cd661e0fa59afbd1 --- /dev/null +++ b/crates/client/src/rendering/assets_wasm.rs @@ -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, + pub width: u32, + pub height: u32, +} + +#[derive(Resource)] +pub struct Assets { + texture_promises: Arc>>>, + textures: Arc>>, +} + +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) -> Option { + 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() + } + } +} diff --git a/crates/client/src/rendering/init.rs b/crates/client/src/rendering/init.rs deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/crates/client/src/rendering/mod.rs b/crates/client/src/rendering/mod.rs index 622ca3f5631a7f6b213d09e5026cfae8e762f6a5..41da4acd8305933d4b2fd85f832536220efa74bf 100644 --- a/crates/client/src/rendering/mod.rs +++ b/crates/client/src/rendering/mod.rs @@ -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, + world: World, + program: Option, vertex_array: Option, vertex_buffer: Option, element_buffer: Option, - texture_object: Option, #[cfg(not(target_arch = "wasm32"))] gl_surface: Option>, #[cfg(not(target_arch = "wasm32"))] gl_context: Option, egui_glow: Option, gl: Option>, + textures: HashMap, } 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::() as i32, 2*size_of::() 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::::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::().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::().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::(); + 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()); diff --git a/crates/client/src/shaders/vertex.glsl b/crates/client/src/shaders/vertex.glsl index 0c787a293fe20cd5742973151443f05dd5c40bed..0ad0d5de78fc873078777832ab900cf5b4fa30de 100644 --- a/crates/client/src/shaders/vertex.glsl +++ b/crates/client/src/shaders/vertex.glsl @@ -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); }