M Cargo.lock => Cargo.lock +30 -0
@@ 2612,6 2612,21 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 2628,6 2643,17 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 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",
M starkingdoms-client/Cargo.toml => starkingdoms-client/Cargo.toml +1 -0
@@ 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]
M starkingdoms-client/src/ecs.rs => starkingdoms-client/src/ecs.rs +7 -7
@@ 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
M starkingdoms-client/src/lib.rs => starkingdoms-client/src/lib.rs +34 -5
@@ 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<MouseWheelEvent>>(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
M starkingdoms-client/src/native/mod.rs => starkingdoms-client/src/native/mod.rs +2 -0
@@ 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
A starkingdoms-client/src/rendering/mipmap.rs => starkingdoms-client/src/rendering/mipmap.rs +126 -0
@@ 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<TextureFormat, RenderPipeline>,
+}
+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
A starkingdoms-client/src/rendering/mod.rs => starkingdoms-client/src/rendering/mod.rs +212 -0
@@ 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<T: UiRenderable> {
+ window: Option<Arc<Window>>,
+ renderer: Option<Renderer<T>>,
+
+ #[cfg(target_arch = "wasm32")]
+ renderer_rx: Option<futures::channel::oneshot::Receiver<RenderInitRes<T>>>,
+
+ world: Option<World>,
+ update_schedule: Option<Schedule>,
+ ui_renderable: Option<T>
+}
+
+impl<T: UiRenderable> App<T> {
+ 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<T: UiRenderable + 'static> ApplicationHandler for App<T> {
+ 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
A starkingdoms-client/src/rendering/renderer.rs => starkingdoms-client/src/rendering/renderer.rs +328 -0
@@ 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<T: UiRenderable> {
+ 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<String, texture::Texture>,
+ pub mip_generator: MipGenerator,
+
+ pub uniform_buffer: Buffer,
+ pub scale_factor: f64,
+
+ pub window: Arc<Window>,
+
+ pub size: PhysicalSize<u32>,
+ pub logical_size: LogicalSize<u32>
+}
+
+pub enum RenderInitRes<T: UiRenderable> {
+ Initialized(Renderer<T>),
+ NotReadyYet(World, Schedule, T)
+}
+impl<T: UiRenderable> Debug for RenderInitRes<T> {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Initialized(..) => write!(f, "[initialized renderer]"),
+ NotReadyYet(..) => write!(f, "[pending initialization]")
+ }
+ }
+}
+
+impl<T: UiRenderable> Renderer<T> {
+ pub async fn new(window: Arc<Window>, world: World, update_schedule: Schedule, gui_renderable: T) -> RenderInitRes<T> {
+ 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::<Camera>();
+
+ 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::<impl 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
A starkingdoms-client/src/rendering/texture.rs => starkingdoms-client/src/rendering/texture.rs +74 -0
@@ 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
A starkingdoms-client/src/rendering/ui.rs => starkingdoms-client/src/rendering/ui.rs +5 -0
@@ 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
M starkingdoms-client/src/wasm/mod.rs => starkingdoms-client/src/wasm/mod.rs +1 -0
@@ 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};