From efdb9b1995399003b2f5cba85c1513ce3ec32504 Mon Sep 17 00:00:00 2001 From: core Date: Sun, 5 Jan 2025 20:31:12 -0500 Subject: [PATCH] add things --- Cargo.lock | 30 ++ starkingdoms-client/Cargo.toml | 1 + starkingdoms-client/src/ecs.rs | 14 +- starkingdoms-client/src/lib.rs | 39 ++- starkingdoms-client/src/native/mod.rs | 2 + starkingdoms-client/src/rendering/mipmap.rs | 126 +++++++ starkingdoms-client/src/rendering/mod.rs | 212 +++++++++++ starkingdoms-client/src/rendering/renderer.rs | 328 ++++++++++++++++++ starkingdoms-client/src/rendering/texture.rs | 74 ++++ starkingdoms-client/src/rendering/ui.rs | 5 + starkingdoms-client/src/wasm/mod.rs | 1 + 11 files changed, 820 insertions(+), 12 deletions(-) create mode 100644 starkingdoms-client/src/rendering/mipmap.rs create mode 100644 starkingdoms-client/src/rendering/mod.rs create mode 100644 starkingdoms-client/src/rendering/renderer.rs create mode 100644 starkingdoms-client/src/rendering/texture.rs create mode 100644 starkingdoms-client/src/rendering/ui.rs diff --git a/Cargo.lock b/Cargo.lock index 0e71c07653573df82837d0c6e205d3a9e7da55d9..170d7b28de57de29cf6611e506f167229de32986 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2611,6 +2611,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -2627,6 +2642,17 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.30" @@ -2675,10 +2701,13 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -5196,6 +5225,7 @@ dependencies = [ "egui", "egui-wgpu", "egui-winit", + "futures", "image 0.25.5", "pollster", "thiserror 2.0.9", diff --git a/starkingdoms-client/Cargo.toml b/starkingdoms-client/Cargo.toml index c4e3847aa0bc05de3a7ca20fbca6091f68fa0367..860e56fd23ec8f86e4315500593befcd405407f5 100644 --- a/starkingdoms-client/Cargo.toml +++ b/starkingdoms-client/Cargo.toml @@ -19,6 +19,7 @@ image = "0.25" egui-winit = { version = "0.30", default-features = false, features = ["links", "wayland", "x11"] } egui-wgpu = "0.30" web-time = "1" +futures = "0.3" # WASM dependencies [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/starkingdoms-client/src/ecs.rs b/starkingdoms-client/src/ecs.rs index 548eef2820ba3d2c5c58075088c9ae429610e65c..619a9302a48e72eb8af8836bfdb03cf9c30b723d 100644 --- a/starkingdoms-client/src/ecs.rs +++ b/starkingdoms-client/src/ecs.rs @@ -4,13 +4,13 @@ use bevy_ecs::system::Resource; #[derive(Component, Debug, Clone, Copy)] pub struct Position { - pub x: f64, - pub y: f64 + pub x: f32, + pub y: f32 } #[derive(Component, Debug, Clone, Copy)] pub struct Scale { - pub width: f64, - pub height: f64, + pub width: f32, + pub height: f32, } #[derive(Component, Debug, Clone)] @@ -27,7 +27,7 @@ pub struct SpriteBundle { #[derive(Resource, Debug)] pub struct Camera { - pub x: f64, - pub y: f64, - pub zoom: f64 + pub x: f32, + pub y: f32, + pub zoom: f32 } \ No newline at end of file diff --git a/starkingdoms-client/src/lib.rs b/starkingdoms-client/src/lib.rs index ff8ef0b1357783332fb2e709e5c65ed9ca37d76f..498b1ff9dbb6dec7a9137fee3159d8b29cb97bb2 100644 --- a/starkingdoms-client/src/lib.rs +++ b/starkingdoms-client/src/lib.rs @@ -1,11 +1,15 @@ +use std::ops::Add; +use std::time::Duration; use bevy_ecs::event::Events; use bevy_ecs::schedule::Schedule; use bevy_ecs::world::World; use egui::Context; use tracing::info; -use crate::ecs::Camera; +use web_time::Instant; +use winit::event_loop::{ControlFlow, EventLoop}; +use crate::ecs::{Camera, Position, Scale, SpriteBundle, SpriteTexture}; use crate::input::MouseWheelEvent; -use crate::rendering::Renderer; +use crate::rendering::App; use crate::rendering::ui::UiRenderable; #[cfg(target_arch = "wasm32")] @@ -26,21 +30,46 @@ pub fn start() { info!("Creating the ECS world..."); let mut world = World::new(); - + world.insert_resource::>(Events::default()); world.insert_resource(Camera { x: 0.0, y: 0.0, zoom: 1.0, }); - + let mut start_schedule = Schedule::default(); // Add startup things here // Caution: This will run before there are things on-screen start_schedule.run(&mut world); - + let mut update_schedule = Schedule::default(); // Add things to run every frame here // Caution: This will run once before there are things on screen + + world.spawn(SpriteBundle { + position: Position { x: 0.0, y: 0.0 }, + scale: Scale { width: 50.0, height: 50.0 }, + texture: SpriteTexture { texture: "happy-tree".to_string() }, + }); + let event_loop = EventLoop::new().unwrap(); + event_loop.set_control_flow(ControlFlow::Poll); + event_loop.run_app(&mut App::new( + world, + update_schedule, + Gui {} + )).unwrap(); } + +pub struct Gui {} +impl UiRenderable for Gui { + fn render(&mut self, ctx: &Context, world: &mut World) { + egui::Window::new("Main Menu") + .resizable(false) + .show(ctx, |ui| { + ui.heading("StarKingdoms.TK"); + ui.label("A game about floating through space"); + }); + } +} \ No newline at end of file diff --git a/starkingdoms-client/src/native/mod.rs b/starkingdoms-client/src/native/mod.rs index 3036a1c569114ef6204acb0851dabfdfdf0ea9e0..e19eda26493f88b6661f3279f9054f98554b71c0 100644 --- a/starkingdoms-client/src/native/mod.rs +++ b/starkingdoms-client/src/native/mod.rs @@ -1,3 +1,5 @@ +use wgpu::{Backends, Limits}; + /// --- IMPORTANT: THIS IS A DUAL TARGET CRATE --- /// THIS WILL ONLY EXECUTE ON NATIVE /// DO ONLY PLATFORM SPECIFIC INITIALIZATION HERE diff --git a/starkingdoms-client/src/rendering/mipmap.rs b/starkingdoms-client/src/rendering/mipmap.rs new file mode 100644 index 0000000000000000000000000000000000000000..6a10bba47a87f6a01292f5339b77098a6df2b1c4 --- /dev/null +++ b/starkingdoms-client/src/rendering/mipmap.rs @@ -0,0 +1,126 @@ +use std::collections::HashMap; +use tracing::debug; +use wgpu::{BindGroupDescriptor, BindGroupEntry, BindingResource, Color, ColorTargetState, ColorWrites, CommandEncoderDescriptor, Device, FilterMode, FragmentState, include_wgsl, LoadOp, Operations, Queue, RenderPassColorAttachment, RenderPassDescriptor, RenderPipeline, RenderPipelineDescriptor, Sampler, SamplerDescriptor, ShaderModule, StoreOp, TextureFormat, TextureViewDescriptor, VertexState}; +use crate::rendering::texture::Texture; + +pub struct MipGenerator { + shader: ShaderModule, + sampler: Sampler, + pipelines: HashMap, +} +impl MipGenerator { + pub fn new(device: &Device) -> Self { + debug!("initializing MipGenerator, compiling shader module"); + let shader = + device.create_shader_module(include_wgsl!("../shaders/text_quad_mips.wgsl")); + Self { + shader, + sampler: device.create_sampler(&SamplerDescriptor { + min_filter: FilterMode::Linear, + label: Some("MipGenerator sampler"), + ..Default::default() + }), + pipelines: HashMap::new(), + } + } + + pub fn generate_mips(&mut self, texture: &Texture, device: &Device, queue: &Queue) { + let pipeline = self.pipelines.entry(texture.texture.format()).or_insert_with(|| { + device.create_render_pipeline(&RenderPipelineDescriptor { + label: Some("MipGenerator format pipeline"), + layout: None, + vertex: VertexState { + module: &self.shader, + entry_point: Some("vs"), + compilation_options: Default::default(), + buffers: &[], + }, + primitive: Default::default(), + depth_stencil: None, + multisample: Default::default(), + fragment: Some(FragmentState { + module: &self.shader, + entry_point: Some("fs"), + compilation_options: Default::default(), + targets: &[Some(ColorTargetState { + format: texture.texture.format(), + blend: None, + write_mask: ColorWrites::default(), + })], + }), + multiview: None, + cache: None, + }) + }); + + let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("MipGenerator command encoder"), + }); + + let mut width = texture.texture.width(); + let mut height = texture.texture.height(); + let mut base_mip_level = 0; + + while width > 1 || height > 1 { + width = 1.max(width / 2); + height = 1.max(height / 2); + + let bind_group = device.create_bind_group(&BindGroupDescriptor { + label: Some("MipGenerator bind group"), + layout: &pipeline.get_bind_group_layout(0), + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::Sampler(&self.sampler), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::TextureView(&texture.texture.create_view( + &TextureViewDescriptor { + base_mip_level, + mip_level_count: Some(1), + ..Default::default() + }, + )), + }, + ], + }); + + base_mip_level += 1; + + let texture_view = texture.texture.create_view(&TextureViewDescriptor { + base_mip_level, + mip_level_count: Some(1), + ..Default::default() + }); + + let render_pass_descriptor = RenderPassDescriptor { + label: Some("MipGenerator render pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: &texture_view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0, + }), + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; + + let mut pass = encoder.begin_render_pass(&render_pass_descriptor); + pass.set_pipeline(pipeline); + pass.set_bind_group(0, Some(&bind_group), &[]); + pass.draw(0..6, 0..1); + } + + let command_buffer = encoder.finish(); + queue.submit(std::iter::once(command_buffer)); + } +} \ No newline at end of file diff --git a/starkingdoms-client/src/rendering/mod.rs b/starkingdoms-client/src/rendering/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..e9ad2aa66c0408859edbd96dd05aa19c86162de4 --- /dev/null +++ b/starkingdoms-client/src/rendering/mod.rs @@ -0,0 +1,212 @@ +mod renderer; +pub mod ui; +mod texture; +mod mipmap; + +use std::ops::Add; +use std::process::exit; +use std::sync::Arc; +use std::time::Duration; +use bevy_ecs::schedule::Schedule; +use bevy_ecs::world::World; +use tracing::{debug, error, info}; +use web_time::Instant; +use winit::application::ApplicationHandler; +use winit::dpi::LogicalSize; +use winit::event::{MouseScrollDelta, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; +use winit::window::{Window, WindowId}; +use crate::input::MouseWheelEvent; +use crate::rendering::renderer::{Renderer, RenderInitRes}; +use crate::rendering::renderer::RenderInitRes::{Initialized, NotReadyYet}; +use crate::rendering::ui::UiRenderable; + +pub struct App { + window: Option>, + renderer: Option>, + + #[cfg(target_arch = "wasm32")] + renderer_rx: Option>>, + + world: Option, + update_schedule: Option, + ui_renderable: Option +} + +impl App { + pub fn new(world: World, update_schedule: Schedule, ui_renderable: T) -> Self { + Self { + window: None, + renderer: None, + #[cfg(target_arch = "wasm32")] + renderer_rx: None, + world: Some(world), + update_schedule: Some(update_schedule), + ui_renderable: Some(ui_renderable) + } + } +} +impl ApplicationHandler for App { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + if self.window.is_none() { + let attributes = Window::default_attributes() + .with_title("StarKingdoms.TK"); + + let window = Arc::new(event_loop.create_window(attributes).unwrap()); + self.window = Some(window.clone()); + + let world = self.world.take().unwrap(); + let update_schedule = self.update_schedule.take().unwrap(); + let ui_renderable = self.ui_renderable.take().unwrap(); + + #[cfg(not(target_arch = "wasm32"))] { + let renderer = pollster::block_on(async move { + Renderer::new(window.clone(), world, update_schedule, ui_renderable).await + }); + match renderer { + Initialized(r) => { + self.renderer = Some(r); + }, + NotReadyYet(w, u, t) => { + self.world = Some(w); + self.update_schedule = Some(u); + self.ui_renderable = Some(t); + } + } + } + #[cfg(target_arch = "wasm32")] { + use winit::platform::web::WindowExtWebSys; + // Add it to the DOM + web_sys::window() + .unwrap() + .document() + .unwrap() + .body() + .unwrap() + .append_child(&window.canvas().unwrap()) + .unwrap(); + + let (tx, rx) = futures::channel::oneshot::channel(); + self.renderer_rx = Some(rx); + wasm_bindgen_futures::spawn_local(async move { + let renderer = Renderer::new(window.clone(), world, update_schedule, ui_renderable).await; + tx.send(renderer.into()).unwrap(); + }); + } + } + + } + + fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) { + if event == WindowEvent::CloseRequested { + info!("Close requested by underlying event system"); + event_loop.exit(); + exit(1); + } + if let Some(renderer) = &mut self.renderer { + let egui_response = renderer.gui_winit.on_window_event(&renderer.window, &event); + if egui_response.consumed { + return; + } + } + + match event { + WindowEvent::Resized(new) => { + if let Some(renderer) = &mut self.renderer { + if new.width > 0 && new.height > 0 { + renderer.size = new; + renderer.scale_factor = renderer.window.scale_factor(); + renderer.logical_size = LogicalSize::from_physical(renderer.size, renderer.scale_factor); + + renderer.surface_configuration.width = new.width; + renderer.surface_configuration.height = new.height; + renderer.surface.configure(&renderer.device, &renderer.surface_configuration); + } + } + }, + WindowEvent::ScaleFactorChanged { scale_factor, .. } => { + if let Some(renderer) = &mut self.renderer { + renderer.scale_factor = scale_factor; + renderer.logical_size = LogicalSize::from_physical(renderer.size, renderer.scale_factor); + } + }, + WindowEvent::MouseWheel { delta, .. } => { + if let Some(renderer) = &mut self.renderer { + renderer.world.send_event(match delta { + MouseScrollDelta::PixelDelta(pos) => MouseWheelEvent::Pixel { + x: pos.x, + y: pos.y + }, + MouseScrollDelta::LineDelta(x, y) => MouseWheelEvent::Line { + x: x as f64, + y: y as f64 + } + }); + } + }, + _ => {} + } + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + if let Some(window) = self.window.clone() { + #[cfg(target_arch = "wasm32")] { + let mut renderer_rxd = false; + if let Some(rx) = self.renderer_rx.as_mut() { + if let Ok(Some(renderer)) = rx.try_recv() { + match renderer { + Initialized(r) => { + self.renderer = Some(r); + renderer_rxd = true; + }, + NotReadyYet(w, u, t) => { + let (tx, rx) = futures::channel::oneshot::channel(); + self.renderer_rx = Some(rx); + wasm_bindgen_futures::spawn_local(async move { + let renderer = Renderer::new(window.clone(), w, u, t).await; + tx.send(renderer.into()).unwrap(); + }); + } + } + } + } + if renderer_rxd { + self.renderer_rx = None; + } + } + + #[cfg(not(target_arch = "wasm32"))] { + if self.renderer.is_none() { + if let Some(window) = self.window.clone() { + let world = self.world.take().unwrap(); + let update_schedule = self.update_schedule.take().unwrap(); + let ui_renderable = self.ui_renderable.take().unwrap(); + + let renderer = pollster::block_on(async move { + Renderer::new(window.clone(), world, update_schedule, ui_renderable).await + }); + + match renderer { + Initialized(r) => { + self.renderer = Some(r); + }, + NotReadyYet(w, u, t) => { + self.world = Some(w); + self.update_schedule = Some(u); + self.ui_renderable = Some(t); + } + } + + return; + } + } + } + + let Some(renderer) = &mut self.renderer else { return; }; + + renderer.render(); + + event_loop.set_control_flow(ControlFlow::WaitUntil(Instant::now().add(Duration::from_secs_f64(1.0 / 60.0)))); + } + } +} \ No newline at end of file diff --git a/starkingdoms-client/src/rendering/renderer.rs b/starkingdoms-client/src/rendering/renderer.rs new file mode 100644 index 0000000000000000000000000000000000000000..a818a12167c6035ad4cc4638bea010e9ac5715e3 --- /dev/null +++ b/starkingdoms-client/src/rendering/renderer.rs @@ -0,0 +1,328 @@ +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; +use std::sync::Arc; +use bevy_ecs::schedule::Schedule; +use bevy_ecs::world::World; +use egui::ViewportId; +use tracing::info; +use web_time::Instant; +use wgpu::{Adapter, Backends, BindGroupDescriptor, BindGroupEntry, BindingResource, Buffer, BufferDescriptor, BufferUsages, Color, ColorTargetState, CommandEncoderDescriptor, Device, DeviceDescriptor, Features, FragmentState, include_wgsl, Instance, InstanceDescriptor, Limits, LoadOp, Operations, Queue, RenderPassColorAttachment, RenderPassDescriptor, RenderPipeline, RenderPipelineDescriptor, RequestAdapterOptions, ShaderModule, StoreOp, Surface, TextureViewDescriptor, VertexState}; +use wgpu::SurfaceConfiguration; +use winit::dpi::{LogicalSize, PhysicalSize}; +use winit::window::Window; +use crate::ecs::{Camera, Position, Scale, SpriteTexture}; +use crate::rendering::mipmap::MipGenerator; +use crate::rendering::renderer::RenderInitRes::{Initialized, NotReadyYet}; +use crate::rendering::texture; +use crate::rendering::ui::UiRenderable; + +pub struct Renderer { + pub last_frame_time: Instant, + + pub surface: Surface<'static>, + pub surface_configuration: SurfaceConfiguration, + pub device: Device, + pub queue: Queue, + pub adapter: Adapter, + + pub sprite_shader_module: ShaderModule, + pub sprite_pipeline: RenderPipeline, + + pub world: World, + pub update_schedule: Schedule, + + pub gui_ctx: egui::Context, + pub gui_winit: egui_winit::State, + pub gui_renderer: egui_wgpu::Renderer, + pub gui_renderable: T, + + pub textures: HashMap, + pub mip_generator: MipGenerator, + + pub uniform_buffer: Buffer, + pub scale_factor: f64, + + pub window: Arc, + + pub size: PhysicalSize, + pub logical_size: LogicalSize +} + +pub enum RenderInitRes { + Initialized(Renderer), + NotReadyYet(World, Schedule, T) +} +impl Debug for RenderInitRes { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Initialized(..) => write!(f, "[initialized renderer]"), + NotReadyYet(..) => write!(f, "[pending initialization]") + } + } +} + +impl Renderer { + pub async fn new(window: Arc, world: World, update_schedule: Schedule, gui_renderable: T) -> RenderInitRes { + let size = window.inner_size(); + if size.width == 0 || size.height == 0 { + return NotReadyYet(world, update_schedule, gui_renderable); + } + + // First, create an instance. This is our handle to wgpu, and is the equivalent of navigator.gpu in WebGPU + let instance = Instance::new(InstanceDescriptor { + backends: Backends::all(), // Select the appropriate backend for the HAL to use. Defined in the platform module, as it's platform-specific + ..Default::default() // Other fields aren't relevant here + }); + + // Next, get our render surface + let surface = instance.create_surface(window.clone()).unwrap(); + + // Next, request out adapter + let adapter = instance.request_adapter(&RequestAdapterOptions { + power_preference: Default::default(), // Don't care + force_fallback_adapter: false, // We want a real GPU + compatible_surface: Some(&surface), // Find an adapter that is able to render to our window + }).await.unwrap(); + + let (device, queue) = adapter.request_device(&DeviceDescriptor { + label: Some("Basic render device"), + required_features: Features::default(), + required_limits: Limits::downlevel_webgl2_defaults(), + memory_hints: Default::default(), + }, None).await.unwrap(); + + let format = surface.get_default_config(&adapter, size.width, size.height).unwrap(); + + surface.configure(&device, &format); + + let sprite_shader_module = device.create_shader_module(include_wgsl!("../shaders/sprite.wgsl")); + let sprite_pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { + label: Some("Sprite pipeline"), + layout: None, + vertex: VertexState { + module: &sprite_shader_module, + entry_point: Some("vs"), + compilation_options: Default::default(), + buffers: &[], + }, + primitive: Default::default(), + depth_stencil: None, + multisample: Default::default(), + fragment: Some(FragmentState { + module: &sprite_shader_module, + entry_point: Some("fs"), + compilation_options: Default::default(), + targets: &[Some(ColorTargetState { + format: format.format, + blend: None, + write_mask: Default::default(), + })], + }), + multiview: None, + cache: None, + }); + + let gui_context = egui::Context::default(); + let gui_winit = egui_winit::State::new( + gui_context.clone(), + ViewportId::ROOT, + &window, + Some(window.scale_factor() as f32), + None, + Some(device.limits().max_texture_dimension_2d as usize) + ); + let gui_renderer = egui_wgpu::Renderer::new( + &device, + format.format, + None, + 1, + false + ); + + let uniform_buffer = device.create_buffer(&BufferDescriptor { + label: Some("quad uniforms"), + size: 16, + usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + Initialized(Self { + mip_generator: MipGenerator::new(&device), + textures: HashMap::new(), + last_frame_time: Instant::now(), + surface, + device, + queue, + adapter, + sprite_shader_module, + sprite_pipeline, + world, + update_schedule, + gui_ctx: gui_context, + gui_winit, + gui_renderer, + gui_renderable, + logical_size: LogicalSize::from_physical(size, window.scale_factor()), + scale_factor: window.scale_factor(), + window, + uniform_buffer, + size, + surface_configuration: format + }) + } + + pub fn render(&mut self) { + // update the world + self.update_schedule.run(&mut self.world); + // update the UI + let egui_output = self.gui_ctx.run( + self.gui_winit.take_egui_input(&self.window), + |ctx| { + self.gui_renderable.render(ctx, &mut self.world) + } + ); + self.gui_winit.handle_platform_output(&self.window, egui_output.platform_output); + + let output = self.surface.get_current_texture().unwrap(); + let view = output.texture.create_view(&TextureViewDescriptor::default()); + + let render_pass_descriptor = RenderPassDescriptor { + label: Some("basic render pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(Color { + r: 0.3, + g: 0.3, + b: 0.3, + a: 1.0, + }), + store: StoreOp::Store, + }, + })], + ..Default::default() + }; + let mut encoder = self + .device + .create_command_encoder(&CommandEncoderDescriptor { + label: Some("command encoder"), + }); + + let mut sprites_to_render: Vec<(Position, Scale, SpriteTexture)> = vec![]; + + let mut things_to_render = self.world.query::<(&Position, &Scale, &SpriteTexture)>(); + for thing in things_to_render.iter_mut(&mut self.world) { + sprites_to_render.push((*thing.0, *thing.1, thing.2.clone())); + } + + let cam = self.world.resource::(); + + for (pos, scale, tex) in sprites_to_render { + let tex = self.textures.entry(tex.texture.clone()).or_insert_with(|| { + info!("loading texture {}", &tex.texture); + let b: &[u8] = match tex.texture.as_str() { + "f" => include_bytes!("../textures/f.png"), + "happy-tree" => include_bytes!("../textures/happy-tree.png"), + "uv" => include_bytes!("../textures/uv.png"), + u => panic!("unknown texture {u}, has it been added in rendering::renderer::::render()?") + }; + texture::Texture::new( + b, + &tex.texture, + &self.device, + &self.queue, + &mut self.mip_generator, + ) + }); + + // need to calculate width, height, x, and y, using logical size & aspect + + // calculate "viewport position" w/ the camera + let viewport_x = pos.x - cam.x; + let viewport_y = pos.y - cam.y; + let scaled_w = scale.width * cam.zoom; + let scaled_h = scale.height * cam.zoom; + + let x_screen = viewport_x / self.logical_size.width as f32; + let y_screen = viewport_y / self.logical_size.height as f32; + let w_screen = scaled_w / self.logical_size.width as f32; + let h_screen = scaled_h / self.logical_size.height as f32; + + let mut uniform_buffer = vec![]; + let uniform_buffer_data = [w_screen, h_screen, x_screen, y_screen]; + for i in uniform_buffer_data { + let mut bytes = i.to_ne_bytes().to_vec(); + uniform_buffer.append(&mut bytes); + } + + self.queue + .write_buffer(&self.uniform_buffer, 0, &uniform_buffer); + + let bind_group = self.device.create_bind_group(&BindGroupDescriptor { + label: Some("test_bind_group"), + layout: &self.sprite_pipeline.get_bind_group_layout(0), + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::Sampler(&tex.sampler), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::TextureView( + &tex.texture.create_view(&TextureViewDescriptor::default()), + ), + }, + BindGroupEntry { + binding: 2, + resource: BindingResource::Buffer( + self.uniform_buffer.as_entire_buffer_binding(), + ), + }, + ], + }); + let mut pass = encoder.begin_render_pass(&render_pass_descriptor); + pass.set_pipeline(&self.sprite_pipeline); + pass.set_bind_group(0, Some(&bind_group), &[]); + pass.draw(0..6, 0..1); + } + // main game rendering done + // next up: egui UI rendering + for (id, image_delta) in &egui_output.textures_delta.set { + self.gui_renderer.update_texture(&self.device, &self.queue, *id, image_delta); + } + { + let paint_jobs = self.gui_ctx.tessellate(egui_output.shapes, self.scale_factor as f32); + let screen_descriptor = egui_wgpu::ScreenDescriptor { + size_in_pixels: [self.size.width, self.size.height], + pixels_per_point: self.scale_factor as f32 + }; + self.gui_renderer.update_buffers(&self.device, &self.queue, &mut encoder, &paint_jobs, &screen_descriptor); + let render_pass = encoder.begin_render_pass(&RenderPassDescriptor { + label: Some("ui render pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: Operations { + load: LoadOp::Load, + store: StoreOp::Store + } + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + let mut forgotten_render_pass = render_pass.forget_lifetime(); + self.gui_renderer.render(&mut forgotten_render_pass, &paint_jobs, &screen_descriptor); + for id in egui_output.textures_delta.free { + self.gui_renderer.free_texture(&id); + } + } + + + let buffer = encoder.finish(); + self.queue.submit(std::iter::once(buffer)); + + output.present(); + } +} \ No newline at end of file diff --git a/starkingdoms-client/src/rendering/texture.rs b/starkingdoms-client/src/rendering/texture.rs new file mode 100644 index 0000000000000000000000000000000000000000..48620b2000a173cde1023403abdacc3400401d3d --- /dev/null +++ b/starkingdoms-client/src/rendering/texture.rs @@ -0,0 +1,74 @@ +use image::EncodableLayout; +use wgpu::{Device, Extent3d, FilterMode, ImageCopyTexture, ImageDataLayout, Origin3d, Queue, SamplerDescriptor, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages}; +use crate::rendering::mipmap::MipGenerator; + +#[derive(Debug)] +pub struct Texture { + pub texture: wgpu::Texture, + pub sampler: wgpu::Sampler, +} +impl Texture { + pub fn new( + bytes: &[u8], + label: &str, + device: &Device, + queue: &Queue, + mip_generator: &mut MipGenerator, + ) -> Self { + let img = image::load_from_memory(bytes).unwrap(); + let rgba = img.to_rgba8(); + + let max_size = rgba.width().max(rgba.height()); + let log_factor = if max_size == 0 { 0 } else { max_size.ilog2() }; + let optimal_mip_levels = 1 + log_factor; + + let texture = device.create_texture(&TextureDescriptor { + label: Some(label), + size: Extent3d { + width: rgba.width(), + height: rgba.height(), + depth_or_array_layers: 1, + }, + mip_level_count: optimal_mip_levels, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8UnormSrgb, + usage: TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_DST + | TextureUsages::RENDER_ATTACHMENT, + view_formats: &[TextureFormat::Rgba8UnormSrgb], + }); + queue.write_texture( + ImageCopyTexture { + texture: &texture, + mip_level: 0, + origin: Origin3d::ZERO, + aspect: Default::default(), + }, + rgba.as_bytes(), + ImageDataLayout { + offset: 0, + bytes_per_row: Some(rgba.width() * 4), + rows_per_image: Some(rgba.height()), + }, + Extent3d { + width: rgba.width(), + height: rgba.height(), + depth_or_array_layers: 1, + }, + ); + + let sampler = device.create_sampler(&SamplerDescriptor { + label: Some("test_sampler"), + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + ..Default::default() + }); + + let tex = Self { texture, sampler }; + + mip_generator.generate_mips(&tex, device, queue); + + tex + } +} \ No newline at end of file diff --git a/starkingdoms-client/src/rendering/ui.rs b/starkingdoms-client/src/rendering/ui.rs new file mode 100644 index 0000000000000000000000000000000000000000..97bf7f4c550be67c3ee0f0ce947dc5682a10dc97 --- /dev/null +++ b/starkingdoms-client/src/rendering/ui.rs @@ -0,0 +1,5 @@ +use bevy_ecs::world::World; + +pub trait UiRenderable { + fn render(&mut self, ctx: &egui::Context, world: &mut World); +} \ No newline at end of file diff --git a/starkingdoms-client/src/wasm/mod.rs b/starkingdoms-client/src/wasm/mod.rs index b32e87200ed4b6f532ca6cc7b8befa0949f8933c..59d6af455669cf5dd63e231037ce201c46654608 100644 --- a/starkingdoms-client/src/wasm/mod.rs +++ b/starkingdoms-client/src/wasm/mod.rs @@ -1,6 +1,7 @@ use tracing::Level; use tracing_subscriber::fmt::format::Pretty; use tracing_subscriber::prelude::*; +use wgpu::{Backends, Limits}; use wasm_bindgen::prelude::wasm_bindgen; use tracing_web::{MakeWebConsoleWriter, performance_layer};