diff --git a/.gitignore b/.gitignore index 092c88ab..369dd42c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ _* .DS_Store ./wgpu-trace/* +_examples diff --git a/Cargo.lock b/Cargo.lock index 674fd1b9..1f927e29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ab_glyph" @@ -43,6 +43,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-activity" version = "0.6.0" @@ -498,6 +504,15 @@ dependencies = [ "libc", ] +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -581,6 +596,7 @@ dependencies = [ "base64 0.22.0", "bitflags 2.6.0", "bytemuck", + "fontdue", "futures", "genmesh", "glam", @@ -680,6 +696,22 @@ dependencies = [ "spin", ] +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fontdue" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7894823fa221401399e2598f8b63f81ac77ff5c63248b7656779bff1632d7d3d" +dependencies = [ + "hashbrown", + "ttf-parser 0.25.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -903,6 +935,11 @@ name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "heck" @@ -1109,6 +1146,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.0.2" @@ -1635,7 +1678,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4586edfe4c648c71797a74c84bacb32b52b212eff5dfe2bb9f2c599844023e7" dependencies = [ - "ttf-parser", + "ttf-parser 0.20.0", ] [[package]] @@ -2420,6 +2463,15 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + [[package]] name = "unicode-bidi" version = "0.3.15" diff --git a/Cargo.toml b/Cargo.toml index fe616e90..22d483e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,8 +23,13 @@ name = "dotrix-terrain" path = "utils/terrain/main.rs" required-features = ["terrain"] +[[bin]] +name = "ui-demo" +path = "demo/ui/main.rs" +required-features = ["fontdue"] + [features] -default = ["terrain", "skydome"] +default = ["terrain", "skydome", "ui"] skydome = [] terrain = [ "dep:noise", @@ -34,6 +39,8 @@ terrain = [ "dep:toml", "dep:serde", ] +ui = [] +fontdue = ["ui", "dep:fontdue"] [dependencies] bytemuck = { version = "1.4", features = ["derive"] } @@ -61,3 +68,4 @@ rand = { version = "0.8.4", features = ["small_rng"], optional = true } structopt = { version = "0.3", optional = true } toml = { version = "0.8", optional = true } serde = { version = "1.0", optional = true } +fontdue = { version = "0.9", optional = true } diff --git a/_examples/animation/fox.rs b/_examples/animation/fox.rs deleted file mode 100644 index af463662..00000000 --- a/_examples/animation/fox.rs +++ /dev/null @@ -1,58 +0,0 @@ -// The Fox model used in this example is taken from Khronos repository -// https://github.com/KhronosGroup/glTF-Sample-Models -// under following License: -// CC0: Low poly fox by PixelMannen -// CC-BY 4.0: Rigging and animation by @tomkranis on Sketchfab -// glTF conversion by @AsoboStudio and @scurest -// - -use dotrix::assets::{Animation, Mesh, Skin, Texture}; -use dotrix::ecs::Mut; -use dotrix::pbr::{Material, Model}; -use dotrix::renderer::Render; -use dotrix::{Animator, Assets, Id, Pose, Transform, World}; - -use std::collections::HashMap; - -/// Component indentifying fox's entity -pub struct Fox { - /// This is only for editor - to know what animations fox have - pub animations: HashMap>, -} - -#[derive(Debug, PartialEq, std::cmp::Eq, std::hash::Hash)] -pub enum FoxAnimClip { - Walk, - Run, - Survey, -} - -pub fn startup(mut world: Mut, mut assets: Mut) { - assets.import("assets/models/Fox.gltf"); - - let texture = assets.register::("Fox::fox::texture"); - let mesh = assets.register::("Fox::fox::mesh"); - let skin = assets.register::("Fox::fox::skin"); - - let walk = assets.register::("Fox::Walk"); - let run = assets.register::("Fox::Run"); - let survey = assets.register::("Fox::Survey"); - - let mut animations: HashMap> = HashMap::new(); - animations.insert(FoxAnimClip::Walk, walk); - animations.insert(FoxAnimClip::Run, run); - animations.insert(FoxAnimClip::Survey, survey); - - world.spawn(Some(( - Model::from(mesh), - Pose::from(skin), - Material { - texture, - ..Default::default() - }, - Transform::default(), - Animator::looped(walk), - Render::default(), - Fox { animations }, - ))); -} diff --git a/_examples/animation/main.rs b/_examples/animation/main.rs deleted file mode 100644 index 4ceb975a..00000000 --- a/_examples/animation/main.rs +++ /dev/null @@ -1,48 +0,0 @@ -mod fox; -mod settings; - -use dotrix::math::Vec3; -use dotrix::pbr::{self, Light}; -use dotrix::prelude::*; -use dotrix::{egui, overlay}; -use dotrix::{Camera, Color, System, World}; - -use settings::Settings; - -fn main() { - Dotrix::application("Dotrix: Animation Example") - .with(System::from(startup)) - .with(System::from(fox::startup)) - .with(System::from(settings::ui)) - .with(System::from(settings::update_camera)) - .with(System::from(settings::update_fox)) - .with(System::from(settings::update_lights)) - .with(Service::from(Settings::default())) - .with(overlay::extension) - .with(egui::extension) - .with(pbr::extension) - .run(); -} - -pub fn startup(mut camera: Mut, mut world: Mut) { - camera.distance = 222.0; - camera.pan = 0.74; - camera.tilt = 0.25; - camera.target = Vec3::new(0.0, 0.5, 0.0); - - world.spawn(vec![ - (Light::ambient(),), - (Light::Simple { - position: Vec3::new(0.0, 500.0, 0.0), - color: Color::white(), - intensity: 0.8, - enabled: true, - },), - (Light::Simple { - position: Vec3::new(200.0, 50.0, 200.0), - color: Color::white(), - intensity: 0.8, - enabled: true, - },), - ]); -} diff --git a/_examples/animation/settings.rs b/_examples/animation/settings.rs deleted file mode 100644 index dd326a6e..00000000 --- a/_examples/animation/settings.rs +++ /dev/null @@ -1,417 +0,0 @@ -use crate::fox::{Fox, FoxAnimClip}; -use dotrix::animation::State as AnimState; -use dotrix::ecs::{Const, Mut}; -use dotrix::egui::{CollapsingHeader, ComboBox, DragValue, Egui, Grid, SidePanel, Slider}; -use dotrix::input::{Button, State as InputState}; -use dotrix::math::Vec3; -use dotrix::overlay::Overlay; -use dotrix::pbr::Light; -use dotrix::{Animator, Camera, Color, Frame, Input, Transform, World}; - -use std::f32::consts::PI; - -pub struct Settings { - pub fox_transform: Transform, - - pub anim_clip: FoxAnimClip, - pub anim_play: bool, - pub anim_speed: f32, - - pub cam_distance: f32, - pub cam_pan: f32, - pub cam_tilt: f32, - pub cam_roll: f32, - pub cam_target: Vec3, - - // Lights here are only structs, not real components. - pub ambient_light_color: Color, - pub ambient_light_intensity: f32, - pub simple_light_color: Color, - pub simple_light_intensity: f32, -} - -impl Settings { - /// Reset to default values - pub fn reset(&mut self) { - *self = Settings::default(); - } -} - -impl Default for Settings { - fn default() -> Self { - Self { - fox_transform: Transform { - translate: Vec3 { - x: 80.0, - y: 0.0, - z: 0.0, - }, - ..Default::default() - }, - - anim_clip: FoxAnimClip::Walk, - anim_play: true, - anim_speed: 1.0, - - cam_distance: 222.0, - cam_pan: 0.74, - cam_tilt: 0.25, - cam_roll: 0.0, - - cam_target: Vec3::new(0.0, 0.5, 0.0), - - simple_light_color: Color::white(), - simple_light_intensity: 0.8, - ambient_light_color: Color::white(), - ambient_light_intensity: 0.8, - } - } -} - -pub fn ui(mut settings: Mut, overlay: Const) { - let egui = overlay - .get::() - .expect("Renderer does not contain an Overlay instance"); - - SidePanel::left("side_panel").show(&egui.ctx, |ui| { - CollapsingHeader::new("Transform") - .default_open(true) - .show(ui, |ui| { - Grid::new("transform").show(ui, |ui| { - ui.label("Translation"); - ui.horizontal(|ui| { - ui.add( - DragValue::new(&mut settings.fox_transform.translate.x) - .prefix("x: ") - .speed(0.1), - ); - ui.add( - DragValue::new(&mut settings.fox_transform.translate.y) - .prefix("y: ") - .speed(0.1), - ); - ui.add( - DragValue::new(&mut settings.fox_transform.translate.z) - .prefix("z: ") - .speed(0.1), - ); - }); - ui.end_row(); - - ui.label("Rotation"); - ui.horizontal(|ui| { - ui.add( - DragValue::new(&mut settings.fox_transform.rotate.v.x) - .prefix("x: ") - .speed(0.01), - ); - ui.add( - DragValue::new(&mut settings.fox_transform.rotate.v.y) - .prefix("y: ") - .speed(0.01), - ); - ui.add( - DragValue::new(&mut settings.fox_transform.rotate.v.z) - .prefix("z: ") - .speed(0.01), - ); - }); - ui.end_row(); - - ui.label("Scale"); - ui.horizontal(|ui| { - ui.add( - DragValue::new(&mut settings.fox_transform.scale.x) - .prefix("x: ") - .speed(0.01), - ); - ui.add( - DragValue::new(&mut settings.fox_transform.scale.y) - .prefix("y: ") - .speed(0.01), - ); - ui.add( - DragValue::new(&mut settings.fox_transform.scale.z) - .prefix("z: ") - .speed(0.01), - ); - }); - ui.end_row(); - }); - }); - - CollapsingHeader::new("Animation") - .default_open(true) - .show(ui, |ui| { - Grid::new("animation").show(ui, |ui| { - ui.label("Clip"); - ComboBox::from_id_source("Clip") - .selected_text(format!("{:?}", settings.anim_clip)) - .show_ui(ui, |ui| { - // There should be some EnumIterator in real implementation - ui.selectable_value( - &mut settings.anim_clip, - FoxAnimClip::Walk, - format!("{:?}", FoxAnimClip::Walk), - ); - ui.selectable_value( - &mut settings.anim_clip, - FoxAnimClip::Run, - format!("{:?}", FoxAnimClip::Run), - ); - ui.selectable_value( - &mut settings.anim_clip, - FoxAnimClip::Survey, - format!("{:?}", FoxAnimClip::Survey), - ); - }); - - if ui - .button(if settings.anim_play { "Stop" } else { "Play" }) - .clicked() - { - settings.anim_play = !settings.anim_play; - }; - ui.end_row(); - - ui.label("Speed"); - ui.add(Slider::new(&mut settings.anim_speed, 0.0..=3.0).text("")); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.anim_speed = Settings::default().anim_speed; - }; - }); - }); - - CollapsingHeader::new("Camera") - .default_open(true) - .show(ui, |ui| { - Grid::new("Target").show(ui, |ui| { - ui.label("Target"); - ui.horizontal(|ui| { - ui.add( - DragValue::new(&mut settings.cam_target.x) - .prefix("x: ") - .speed(0.1), - ); - ui.add( - DragValue::new(&mut settings.cam_target.y) - .prefix("y: ") - .speed(0.1), - ); - ui.add( - DragValue::new(&mut settings.cam_target.z) - .prefix("z: ") - .speed(0.1), - ); - if ui.button("¤").on_hover_text("Target fox").clicked() { - settings.cam_target.x = settings.fox_transform.translate.x; - settings.cam_target.y = settings.fox_transform.translate.y; - settings.cam_target.z = settings.fox_transform.translate.z; - }; - }); - ui.end_row(); - - ui.label("Distance"); - ui.add(DragValue::new(&mut settings.cam_distance).speed(0.1)); - ui.end_row(); - - ui.label("Pan Angle"); - ui.add(DragValue::new(&mut settings.cam_pan).speed(0.01)); - ui.end_row(); - - ui.label("Tilt Angle"); - ui.add(DragValue::new(&mut settings.cam_tilt).speed(0.01)); - ui.end_row(); - - ui.label("Roll Angle"); - ui.add(DragValue::new(&mut settings.cam_roll).speed(0.01)); - ui.end_row(); - }); - }); - - CollapsingHeader::new("Light") - .default_open(true) - .show(ui, |ui| { - Grid::new("light").show(ui, |ui| { - ui.label("Red"); - ui.add(Slider::new(&mut settings.simple_light_color.r, 0.0..=1.0).text("")); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.simple_light_color.r = Settings::default().simple_light_color.r; - }; - ui.end_row(); - - ui.label("Green"); - ui.add(Slider::new(&mut settings.simple_light_color.g, 0.0..=1.0).text("")); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.simple_light_color.g = Settings::default().simple_light_color.g; - }; - ui.end_row(); - - ui.label("Blue"); - ui.add(Slider::new(&mut settings.simple_light_color.b, 0.0..=1.0).text("")); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.simple_light_color.b = Settings::default().simple_light_color.b; - }; - ui.end_row(); - - ui.label("Intensity"); - ui.add(Slider::new(&mut settings.simple_light_intensity, 0.0..=3.0).text("")); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.simple_light_intensity = - Settings::default().simple_light_intensity; - }; - ui.end_row(); - }); - }); - - CollapsingHeader::new("Ambient Light") - .default_open(true) - .show(ui, |ui| { - Grid::new("ambient light").show(ui, |ui| { - ui.label("Red"); - ui.add(Slider::new(&mut settings.ambient_light_color.r, 0.0..=1.0).text("")); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.ambient_light_color.r = Settings::default().ambient_light_color.r; - }; - ui.end_row(); - - ui.label("Green"); - ui.add(Slider::new(&mut settings.ambient_light_color.g, 0.0..=1.0).text("")); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.ambient_light_color.g = Settings::default().ambient_light_color.g; - }; - ui.end_row(); - - ui.label("Blue"); - ui.add(Slider::new(&mut settings.ambient_light_color.b, 0.0..=1.0).text("")); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.ambient_light_color.b = Settings::default().ambient_light_color.b; - }; - ui.end_row(); - - ui.label("Intensity"); - ui.add(Slider::new(&mut settings.ambient_light_intensity, 0.0..=3.0).text("")); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.ambient_light_intensity = - Settings::default().ambient_light_intensity; - }; - ui.end_row(); - }); - }); - - if ui.button("Reset all").clicked() { - settings.reset(); - }; - }); -} - -/// This func updates camera based on values in settings and controls -pub fn update_camera( - mut camera: Mut, - mut settings: Mut, - input: Const, - frame: Const, -) { - const ROTATE_SPEED: f32 = PI / 10.0; - const ZOOM_SPEED: f32 = 500.0; - - let time_delta = frame.delta().as_secs_f32(); - let mouse_delta = input.mouse_delta(); - let mouse_scroll = input.mouse_scroll(); - - // Get values from settings - let mut distance = settings.cam_distance; - let target = settings.cam_target; - let mut pan = settings.cam_pan; - let mut tilt = settings.cam_tilt; - - // Calculate new values - distance -= ZOOM_SPEED * mouse_scroll * time_delta; - distance = if distance > -1.0 { distance } else { -1.0 }; - - if input.button_state(Button::MouseRight) == Some(InputState::Hold) { - pan += mouse_delta.x * ROTATE_SPEED * time_delta; - tilt = camera.tilt + mouse_delta.y * ROTATE_SPEED * time_delta; - let half_pi = PI / 2.0; - - tilt = if tilt >= half_pi { - half_pi - 0.01 - } else if tilt <= -half_pi { - -half_pi + 0.01 - } else { - tilt - }; - } - - // Apply values to settings - settings.cam_distance = distance; - settings.cam_pan = pan; - settings.cam_tilt = tilt; - - // Apply values to camera - camera.target = target; - camera.distance = distance; - camera.pan = pan; - camera.tilt = tilt; - camera.roll = settings.cam_roll; -} - -/// This func updates fox's entity based on values in settings -pub fn update_fox(settings: Const, world: Mut) { - // Query fox entity - let query = world.query::<(&mut Transform, &mut Animator, &mut Fox)>(); - - // This loop will run only once, because Fox component is assigned to only one entity - for (transform, animator, fox) in query { - // Set transformation - transform.translate = settings.fox_transform.translate; - transform.rotate = settings.fox_transform.rotate; - transform.scale = settings.fox_transform.scale; - - // Set animation state - match animator.state() { - AnimState::Loop(_) => { - if !settings.anim_play { - animator.stop(); - } - } - AnimState::Stop => { - if settings.anim_play { - animator.start_loop(); - } - } - _ => {} - }; - - // Set animation clip - let clip = fox.animations[&settings.anim_clip]; - if animator.animation() != clip { - animator.animate(clip); - } - - // Set animation speed - animator.speed = settings.anim_speed; - } -} - -/// This func updates all light entities based on values in settings -pub fn update_lights(settings: Const, world: Mut) { - // Query ambient light entities - let query = world.query::<(&mut Light,)>(); - - for (light,) in query { - match light { - Light::Ambient { color, intensity } => { - *color = settings.ambient_light_color; - *intensity = settings.ambient_light_intensity; - } - Light::Simple { - color, intensity, .. - } => { - *color = settings.simple_light_color; - *intensity = settings.simple_light_intensity; - } - _ => continue, - } - } -} diff --git a/_examples/compute/compute.wgsl b/_examples/compute/compute.wgsl deleted file mode 100644 index 8cecfd39..00000000 --- a/_examples/compute/compute.wgsl +++ /dev/null @@ -1,53 +0,0 @@ - -struct Particle { - position : vec4; - velocity : vec4; - color: vec4; - life_time: f32; -}; - -struct Params { - position : vec3; - simulation_time : f32; - gravity : f32; - start_color: vec4; - end_color: vec4; -}; - -struct Particles { - particles : [[stride(64)]] array; -}; - -[[group(0), binding(0)]] var uParams : Params; -[[group(0), binding(1)]] var sParticles : Particles; - - -[[stage(compute), workgroup_size(64)]] -fn main([[builtin(global_invocation_id)]] global_invocation_id: vec3) { - let total = arrayLength(&sParticles.particles); - let index = global_invocation_id.x; - if (index >= total) { - return; - } - let gravity : f32 = uParams.gravity; - let time : f32 = uParams.simulation_time; - let col_0 = uParams.start_color; - let col_1 = uParams.end_color; - - let start_time: f32 = 0.; - let life_time : f32 = sParticles.particles[index].life_time; - - let age: f32 = (time - start_time) % life_time; - - var pos : vec3 = uParams.position.xyz; - var vel : vec3 = sParticles.particles[index].velocity.xyz; - // Distance travelled - pos = pos + age * vel.xyz; - // Gravity - pos = pos + vec3(0., gravity, 0.) * age*age; - - sParticles.particles[index].position = vec4(pos, 1.0); - - let age: f32 = time % life_time; - sParticles.particles[index].color = mix(col_0, col_1, smoothStep(0., life_time, age)); -} diff --git a/_examples/compute/main.rs b/_examples/compute/main.rs deleted file mode 100644 index e3ebba79..00000000 --- a/_examples/compute/main.rs +++ /dev/null @@ -1,279 +0,0 @@ -use rand::distributions::{Distribution, Uniform}; -use rand::SeedableRng; - -use dotrix::assets::Shader; -use dotrix::camera; -use dotrix::camera::ProjView; -use dotrix::egui::{self, Egui}; -use dotrix::overlay::{self, Overlay}; -use dotrix::prelude::*; -use dotrix::primitives::Cube; -use dotrix::renderer::{ - BindGroup, Binding, Buffer, Compute, ComputeArgs, ComputeOptions, DrawArgs, PipelineLayout, - Render, RenderOptions, Stage, WorkGroups, -}; -use dotrix::{Assets, Camera, Color, Frame, Globals, Renderer, World}; - -const PARTICLES_COUNT: usize = 2000; -const PARTICLES_PER_WORKGROUP: usize = 32; -const COMPUTE_PIPELINE: &str = "dotrix::example::compute"; -const RENDER_PIPELINE: &str = "dotrix::example::render"; -const PARTICLE_MESH: &str = "dotrix::example::particle"; - -struct ParticlesSpawner { - params: Buffer, - particles: Buffer, - work_group_size: u32, -} - -impl Default for ParticlesSpawner { - fn default() -> Self { - Self { - params: Buffer::uniform("Particles params"), - particles: Buffer::storage("Particles Instances").allow_write(), - work_group_size: 0, - } - } -} - -#[derive(Clone, Copy)] -#[repr(C)] -struct Params { - position: [f32; 3], - simulation_time: f32, - gravity: f32, - start_color: [f32; 4], - end_color: [f32; 4], - padding: [f32; 3], -} - -unsafe impl bytemuck::Zeroable for Params {} -unsafe impl bytemuck::Pod for Params {} - -impl Default for Params { - fn default() -> Self { - Self { - position: [0.0; 3], - simulation_time: 0.0, - gravity: -0.002, - start_color: [0.2, 0., 0., 1.], - end_color: [0.8, 0.3, 0.3, 1.], - padding: [0.0; 3], - } - } -} - -#[derive(Default, Clone, Copy)] -#[repr(C)] -struct Particle { - position: [f32; 4], - velocity: [f32; 4], - color: [f32; 4], - life_time: f32, - padding: [f32; 3], -} - -unsafe impl bytemuck::Zeroable for Particle {} -unsafe impl bytemuck::Pod for Particle {} - -fn main() { - Dotrix::application("Dotrix: Compute Example") - .with(overlay::extension) - .with(egui::extension) - .with(System::from(startup)) - .with(System::from(compute)) - .with(System::from(render)) - .with(System::from(ui)) - .with(System::from(camera::control)) - .run(); -} - -fn startup( - mut assets: Mut, - mut camera: Mut, - mut world: Mut, - mut renderer: Mut, -) { - // set up clear color - renderer.set_clear_color(Color::rgb(0.01, 0.01, 0.01)); - // set up camera - camera.target = [0., 0., 0.].into(); - camera.distance = 2.0; - camera.tilt = 0.0; - camera.pan = std::f32::consts::PI / 2.0; - - // set up particles spawner - let mut spawner = ParticlesSpawner { - work_group_size: (PARTICLES_COUNT as f32 / PARTICLES_PER_WORKGROUP as f32).ceil() as u32, - ..Default::default() - }; - let mut particles = vec![Particle::default(); PARTICLES_COUNT]; - - // set initial particles data - let mut rng = rand::rngs::StdRng::seed_from_u64(16); - let unif = Uniform::new_inclusive(-1.0, 1.0); - for particle in particles.iter_mut() { - particle.velocity[0] = 0.1 * unif.sample(&mut rng); // X - particle.velocity[1] = 0.1 * (unif.sample(&mut rng) + 2.0) / 2.0; // Y - particle.velocity[2] = 0.1 * unif.sample(&mut rng); // Z - particle.life_time = 1.5 + unif.sample(&mut rng); // life time - } - - renderer.load_buffer( - &mut spawner.particles, - bytemuck::cast_slice(particles.as_slice()), - ); - - world.spawn(Some((spawner, Compute::default(), Render::default()))); - - // load shaders - let shaders = [ - (COMPUTE_PIPELINE, include_str!("./compute.wgsl")), - (RENDER_PIPELINE, include_str!("./render.wgsl")), - ]; - for (pipeline_name, pipeline_code) in shaders.iter() { - let mut shader = Shader { - name: String::from(*pipeline_name), - code: String::from(*pipeline_code), - ..Default::default() - }; - shader.load(&renderer); - assets.store_as(shader, pipeline_name); - } - - // add mesh - let mut mesh = Cube::builder(0.01).with_positions().mesh(); - mesh.load(&renderer); - assets.store_as(mesh, PARTICLE_MESH); -} - -fn compute( - assets: Const, - frame: Const, - world: Const, - mut renderer: Mut, -) { - for (spawner, compute) in world.query::<(&mut ParticlesSpawner, &mut Compute)>() { - // Set the shader on the pipeline - if compute.pipeline.shader.is_null() { - compute.pipeline.shader = assets.find::(COMPUTE_PIPELINE).unwrap_or_default(); - } - // update time delta - let params = Params { - simulation_time: frame.time().as_secs_f32(), - ..Default::default() - }; - renderer.load_buffer(&mut spawner.params, bytemuck::cast_slice(&[params])); - - // Bind the uniforms to the shader - if !compute.pipeline.ready(&renderer) { - if let Some(shader) = assets.get(compute.pipeline.shader) { - if !shader.loaded() { - continue; - } - - renderer.bind( - &mut compute.pipeline, - PipelineLayout::Compute { - label: "Compute Particles".into(), - shader, - bindings: &[BindGroup::new( - "Globals", - vec![ - Binding::Uniform("Params", Stage::Compute, &spawner.params), - Binding::Storage("Particles", Stage::Compute, &spawner.particles), - ], - )], - options: ComputeOptions { cs_main: "main" }, - }, - ); - } - } - - // Run the pipeline - let work_group_size = spawner.work_group_size; - renderer.compute( - &mut compute.pipeline, - &ComputeArgs { - work_groups: WorkGroups { - x: work_group_size, - y: 1, - z: 1, - }, - }, - ); - } -} - -fn render( - globals: Const, - world: Const, - assets: Const, - mut renderer: Mut, -) { - for (spawner, render) in world.query::<(&mut ParticlesSpawner, &mut Render)>() { - // Set the shader on the pipeline - if render.pipeline.shader.is_null() { - render.pipeline.shader = assets.find::(RENDER_PIPELINE).unwrap_or_default(); - } - - let mesh = assets - .get(assets.find(PARTICLE_MESH).expect("Mesh must be loaded")) - .unwrap(); - - if !render.pipeline.ready(&renderer) { - if let Some(shader) = assets.get(render.pipeline.shader) { - if !shader.loaded() { - continue; - } - - // Get the uniform that holds the camera's project view - let proj_view = globals - .get::() - .expect("ProjView buffer must be loaded"); - - renderer.bind( - &mut render.pipeline, - PipelineLayout::Render { - label: String::from(RENDER_PIPELINE), - mesh, - shader, - bindings: &[BindGroup::new( - "Globals", - vec![ - Binding::Uniform("ProjView", Stage::Vertex, &proj_view.uniform), - Binding::Storage("Particles", Stage::Vertex, &spawner.particles), - ], - )], - options: RenderOptions::default(), - }, - ); - } - } - - renderer.draw( - &mut render.pipeline, - mesh, - &DrawArgs { - start_index: 0, - end_index: PARTICLES_COUNT as u32, - ..Default::default() - }, - ); - } -} - -fn ui(overlay: Const, frame: Const) { - let egui_overlay = overlay - .get::() - .expect("Egui overlay must be added on startup"); - - egui::Area::new("FPS counter") - .fixed_pos(egui::pos2(16.0, 16.0)) - .show(&egui_overlay.ctx, |ui| { - ui.colored_label( - egui::Rgba::from_rgb(255.0, 255.0, 255.0), - format!("FPS: {:.1}", frame.fps()), - ); - }); -} diff --git a/_examples/compute/render.wgsl b/_examples/compute/render.wgsl deleted file mode 100644 index 49d81eb3..00000000 --- a/_examples/compute/render.wgsl +++ /dev/null @@ -1,39 +0,0 @@ -struct VertexOutput { - [[builtin(position)]] position: vec4; - [[location(0)]] color: vec4; -}; - -struct Renderer { - proj_view: mat4x4; -}; - -struct Particle { - position : vec4; - velocity : vec4; - color: vec4; - life_time: f32; -}; - -struct Particles { -particles : [[stride(64)]] array; -}; - -[[group(0), binding(0)]] var uRenderer: Renderer; -[[group(0), binding(1)]] var uParticles : Particles; - -[[stage(vertex)]] -fn vs_main( - [[location(0)]] position: vec3, - [[builtin(instance_index)]] instance_id: u32, -) -> VertexOutput { - let particle_pos = vec4(uParticles.particles[instance_id].position.xyz, 1.); - var out: VertexOutput; - out.position = uRenderer.proj_view * vec4(position + particle_pos.xyz, 1.0); - out.color = uParticles.particles[instance_id].color; - return out; -} - -[[stage(fragment)]] -fn fs_main(in: VertexOutput) -> [[location(0)]] vec4 { - return in.color; -} diff --git a/_examples/demo/demo.png b/_examples/demo/demo.png deleted file mode 100644 index 5bca0b00..00000000 Binary files a/_examples/demo/demo.png and /dev/null differ diff --git a/_examples/demo/main.rs b/_examples/demo/main.rs deleted file mode 100644 index 23e28958..00000000 --- a/_examples/demo/main.rs +++ /dev/null @@ -1,298 +0,0 @@ -use dotrix::assets::Mesh; -use dotrix::camera; -use dotrix::egui::{self, Egui}; -use dotrix::input::{ActionMapper, Button, KeyCode, Mapper, Modifiers}; -use dotrix::math::{Quat, Rad, Rotation3, Vec3}; -use dotrix::overlay::{self, Overlay}; -use dotrix::pbr::{self, Light, Material, Model}; -use dotrix::prelude::*; -use dotrix::renderer::Render; -use dotrix::sky::{skybox, SkyBox}; -use dotrix::{Animator, Assets, Camera, Color, CubeMap, Frame, Input, Pose, Transform, World}; - -use std::f32::consts::PI; - -const CAMERA_HEIGHT: f32 = 2.0; // Default camera target feight -const TERRAIN_SIZE: usize = 128; // Number of sqaures per side - -fn main() { - Dotrix::application("Dotrix: Demo Example") - .with(System::from(startup)) - .with(System::from(player_control)) - .with(System::from(camera::control)) - .with(System::from(ui)) - .with(overlay::extension) - .with(egui::extension) - .with(pbr::extension) - .with(skybox::extension) - .run(); -} - -/// Initial game routines -fn startup( - mut assets: Mut, - mut camera: Mut, - mut input: Mut, - mut world: Mut, -) { - input.set_mapper(Box::new(Mapper::::new())); - init_camera(&mut camera); - init_skybox(&mut world, &mut assets); - init_terrain(&mut world, &mut assets); - init_light(&mut world); - init_player(&mut world, &mut assets, &mut input); -} - -fn init_camera(camera: &mut Camera) { - camera.pan = PI / 2.0; - camera.tilt = 0.0; - camera.target = Vec3::new(0.0, CAMERA_HEIGHT, 0.0); - camera.distance = 5.0; -} - -fn init_skybox(world: &mut World, assets: &mut Assets) { - // Import skybox textures - assets.import("assets/skybox-compass/skybox_right.png"); - assets.import("assets/skybox-compass/skybox_left.png"); - assets.import("assets/skybox-compass/skybox_top.png"); - assets.import("assets/skybox-compass/skybox_bottom.png"); - assets.import("assets/skybox-compass/skybox_back.png"); - assets.import("assets/skybox-compass/skybox_front.png"); - - // Spawn skybox - world.spawn(Some(( - SkyBox { - view_range: 500.0, - ..Default::default() - }, - CubeMap { - right: assets.register("skybox_right"), - left: assets.register("skybox_left"), - top: assets.register("skybox_top"), - bottom: assets.register("skybox_bottom"), - back: assets.register("skybox_back"), - front: assets.register("skybox_front"), - ..Default::default() - }, - Render::default(), - ))); -} - -fn init_terrain(world: &mut World, assets: &mut Assets) { - // Generate terrain mesh like this: - // 0 1 - // 0 +---+---+---> x - // | / | / | - // 1 +---+---+ - // | / | / | - // +---+---+ - // | - // z - - let size = TERRAIN_SIZE; - let mut positions = Vec::with_capacity(3 * 2 * size * size); - let mut uvs = Vec::new(); - for x in 0..size { - let x0 = x as f32; - let x1 = x0 + 1.0; - for z in 0..size { - let z0 = z as f32; - let z1 = z0 + 1.0; - // Add vertices - positions.push([x0, 0.0, z0]); - positions.push([x0, 0.0, z1]); - positions.push([x1, 0.0, z0]); - positions.push([x1, 0.0, z0]); - positions.push([x0, 0.0, z1]); - positions.push([x1, 0.0, z1]); - // Add texture vertices - uvs.push([0.0, 0.0]); - uvs.push([0.0, 1.0]); - uvs.push([1.0, 0.0]); - uvs.push([1.0, 0.0]); - uvs.push([0.0, 1.0]); - uvs.push([1.0, 1.0]); - } - } - - let normals = Mesh::calculate_normals(&positions, None); - let (tangents, bitangents) = Mesh::calculate_tangents_bitangents(&positions, &uvs, None); - - let mut mesh = Mesh::default(); - - mesh.with_vertices(&positions); - mesh.with_vertices(&normals); - mesh.with_vertices(&tangents); - mesh.with_vertices(&bitangents); - mesh.with_vertices(&uvs); - - // Store mesh and get its ID - let mesh = assets.store_as(mesh, "terrain"); - - // import terrain texture and get its ID - assets.import("assets/textures/terrain.png"); - let texture = assets.register("terrain"); - - // Center terrain tile at coordinate system center (0.0, 0.0, 0.0) by moving the tile on a - // half of its size by X and Z axis - let shift = (size / 2) as f32; - - world.spawn( - (pbr::solid::Entity { - mesh, - texture, - translate: Vec3::new(-shift, 0.0, -shift), - ..Default::default() - }) - .some(), - ); -} - -fn init_player(world: &mut World, assets: &mut Assets, input: &mut Input) { - // Import character model from GLTF file, it provides several assets: mesh, skin, and run - // animation - assets.import("assets/models/character.gltf"); - let mesh = assets.register("character::Cube::mesh"); - let skin = assets.register("character::Cube::skin"); - let run = assets.register("character::run"); - - // spawn model in the world - world.spawn(Some(( - Model::from(mesh), - Pose::from(skin), - Material { - albedo: Color::rgb(0.2, 0.2, 0.2), - ao: 0.2, - ..Default::default() - }, - Transform { - scale: Vec3::new(0.5, 0.5, 0.5), - translate: Vec3::new(0.0, 0.1, 0.0), - ..Default::default() - }, - Animator::new(run), // Animation control (stopped by default) - Render::default(), - Player { is_running: false }, - ))); - - // Map W key to Run Action - input.mapper_mut::>().set(&[( - Action::Run, - Button::Key(KeyCode::W), - Modifiers::empty(), - )]); -} - -fn init_light(world: &mut World) { - // spawn source of white light at (0.0, 100.0, 0.0) - world.spawn(Some((Light::Simple { - // direction: Vec3::new(0.3, -0.5, -0.6), - position: Vec3::new(0.0, 1000.0, 0.0), - color: Color::white(), - intensity: 0.5, - enabled: true, - },))); - // spawn source of white light at (0.0, 100.0, 0.0) - world.spawn(Some((Light::Ambient { - color: Color::white(), - intensity: 0.2, - },))); -} - -// Component indentifying players's entity -struct Player { - is_running: bool, -} - -fn player_control( - world: Mut, - input: Const, - frame: Const, - mut camera: Mut, -) { - const PLAYER_SPEED: f32 = 10.0; - - // Query player entity - let query = world.query::<(&mut Transform, &mut Animator, &mut Player)>(); - // this loop will run only once, because Player component is assigned to only one entity - for (transform, animator, player) in query { - // calculate distance offset if W is pressed and control animation - let distance = if input.is_action_hold(Action::Run) { - if !player.is_running { - // start run animation - animator.start_loop(); - player.is_running = true; - } - PLAYER_SPEED * frame.delta().as_secs_f32() - } else { - if player.is_running { - // stop run animation - animator.stop(); - player.is_running = false; - } - 0.0 - }; - - // get camera angle around Y axis - let pan = camera.pan; - // rotate model to the right direction - transform.rotate = Quat::from_angle_y(Rad(-(PI / 2.0 + pan))); - if distance > 0.00001 { - // calculate X and Z deltas if player is moving - let dx = distance * pan.cos(); - let dz = distance * pan.sin(); - // calculate new model positions - let mut pos_x = transform.translate.x - dx; - let mut pos_z = transform.translate.z - dz; - // check terrain boundaries, so player won't run away from the terrain tile - let half_terrain = TERRAIN_SIZE as f32 / 2.0; - if pos_x < -half_terrain { - pos_x = -half_terrain; - } else if pos_x > half_terrain { - pos_x = half_terrain; - } - if pos_z < -half_terrain { - pos_z = -half_terrain; - } else if pos_z > half_terrain { - pos_z = half_terrain; - } - // apply translation - transform.translate.x = pos_x; - transform.translate.z = pos_z; - - // make camera following the player - camera.target.x = pos_x; - camera.target.y = CAMERA_HEIGHT; - camera.target.z = pos_z; - } - } -} - -/// Enumeration of actions provided by the game -#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] -enum Action { - Run, -} - -fn ui(overlay: Const, frame: Const) { - let egui_overlay = overlay - .get::() - .expect("Egui overlay must be added on startup"); - - egui::Area::new("FPS counter") - .fixed_pos(egui::pos2(16.0, 16.0)) - .show(&egui_overlay.ctx, |ui| { - ui.colored_label( - egui::Rgba::from_rgb(255.0, 255.0, 255.0), - format!("FPS: {:.1}", frame.fps()), - ); - }); -} - -/// Bind Inputs and Actions -impl ActionMapper for Input { - fn action_mapped(&self, action: Action) -> Option<(Button, Modifiers)> { - let mapper = self.mapper::>(); - mapper.get_button(action) - } -} diff --git a/_examples/light/car.rs b/_examples/light/car.rs deleted file mode 100644 index 6f753543..00000000 --- a/_examples/light/car.rs +++ /dev/null @@ -1,172 +0,0 @@ -use dotrix::assets::{Mesh, Texture}; -use dotrix::ecs::{Const, Mut}; -use dotrix::math::Vec3; -use dotrix::pbr::{self, Light}; -use dotrix::{Assets, Color, Frame, World}; - -use crate::settings::Settings; - -pub struct CarLight; - -struct LightController { - increasing: bool, - min: f32, - max: f32, - current: f32, - speed: f32, -} - -pub fn startup(mut world: Mut, mut assets: Mut) { - assets.import("assets/models/car.gltf"); - let car_mesh = assets.register::("car::mesh"); - // In real game, use one shared mesh with different transform - let wh1_mesh = assets.register::("car::wheel-1::mesh"); - let wh2_mesh = assets.register::("car::wheel-2::mesh"); - let wh3_mesh = assets.register::("car::wheel-3::mesh"); - let wh4_mesh = assets.register::("car::wheel-4::mesh"); - let texture = assets.register::("car::texture"); - - world.spawn(vec![ - (pbr::solid::Entity { - mesh: car_mesh, - texture, - ..Default::default() - }) - .tuple(), - (pbr::solid::Entity { - mesh: wh1_mesh, - texture, - ..Default::default() - }) - .tuple(), - (pbr::solid::Entity { - mesh: wh2_mesh, - texture, - ..Default::default() - }) - .tuple(), - (pbr::solid::Entity { - mesh: wh3_mesh, - texture, - ..Default::default() - }) - .tuple(), - (pbr::solid::Entity { - mesh: wh4_mesh, - texture, - ..Default::default() - }) - .tuple(), - ]); - - // Spawn lights - world.spawn(vec![ - ( - Light::Point { - position: Vec3::new(0.28, 1.7, 0.487), - color: Color::rgb(0.0, 0.25, 0.94), - intensity: 3.0, - constant: 1.0, - linear: 0.35, - quadratic: 0.44, - enabled: true, - }, - CarLight {}, - LightController { - increasing: true, - current: 0.0, - min: 0.0, - max: 3.0, - speed: 12.0, - }, - ), - ( - Light::Point { - position: Vec3::new(0.28, 1.7, -0.487), - color: Color::rgb(0.94, 0.0, 0.25), - intensity: 3.0, - constant: 1.0, - linear: 0.35, - quadratic: 0.44, - enabled: true, - }, - CarLight {}, - LightController { - increasing: false, - current: 3.0, - min: 0.0, - max: 3.0, - speed: 12.0, - }, - ), - ]); - - world.spawn(vec![ - ( - Light::Spot { - position: Vec3::new(-1.4, 0.68, 0.58), - direction: Vec3::new(-45.0, -5.0, 0.0), - color: Color::white(), - intensity: 0.9, - cut_off: 0.8, - outer_cut_off: 0.58, - enabled: true, - }, - CarLight {}, - ), - ( - Light::Spot { - position: Vec3::new(-1.4, 0.68, -0.58), - direction: Vec3::new(-45.0, -5.0, 0.0), - color: Color::white(), - intensity: 0.9, - cut_off: 0.8, - outer_cut_off: 0.58, - enabled: true, - }, - CarLight {}, - ), - ]); -} - -pub fn update(world: Mut, frame: Const, settings: Const) { - if settings.car.animate { - animate_lights(&world, &frame); - } - - // Enable/disable point lights - let query = world.query::<(&mut Light, &CarLight)>(); - for (light, _) in query { - match light { - Light::Point { enabled, .. } => *enabled = settings.car.point_lights, - Light::Spot { enabled, .. } => *enabled = settings.car.spot_lights, - _ => continue, - } - } -} - -fn animate_lights(world: &World, frame: &Frame) { - let query = world.query::<(&mut Light, &mut LightController)>(); - for (light, c) in query { - match light { - Light::Point { intensity, .. } => { - if c.increasing { - c.current += c.speed * frame.delta().as_secs_f32(); - } else { - c.current -= c.speed * frame.delta().as_secs_f32(); - } - - if c.current > c.max { - c.current = c.max; - c.increasing = !c.increasing; - } else if c.current < c.min { - c.current = c.min; - c.increasing = !c.increasing; - } - - *intensity = c.current; - } - _ => continue, - } - } -} diff --git a/_examples/light/main.rs b/_examples/light/main.rs deleted file mode 100644 index 190c1119..00000000 --- a/_examples/light/main.rs +++ /dev/null @@ -1,51 +0,0 @@ -mod car; -mod settings; -mod skybox; -mod terrain; - -use dotrix::math::Vec3; -use dotrix::pbr::Light; -use dotrix::prelude::*; -use dotrix::{egui, overlay, pbr, sky}; -use dotrix::{Camera, World}; - -use settings::Settings; - -fn main() { - Dotrix::application("Dotrix: Light Example") - // systems - .with(System::from(car::startup)) - .with(System::from(skybox::startup)) - .with(System::from(terrain::startup)) - .with(System::from(startup)) - .with(System::from(settings::ui)) - .with(System::from(settings::update_camera)) - .with(System::from(settings::update_settings)) - .with(System::from(car::update)) - // Services - .with(Service::from(Settings::default())) - // Extensions - .with(overlay::extension) - .with(egui::extension) - .with(pbr::extension) - .with(sky::skybox::extension) - // Execute - .run(); -} - -/// This will mark light as editable. This exists because we don't want to edit police car lights. -struct Editable; - -/// Spawn editable lights. Light properties is set with settings. -fn startup(mut camera: Mut, mut world: Mut) { - camera.distance = 6.6; - camera.pan = 2.5; - camera.tilt = 0.275; - camera.target = Vec3::new(0.0, 0.5, 0.0); - - world.spawn(Some((Light::ambient(), Editable {}))); - world.spawn(Some((Light::directional(), Editable {}))); - world.spawn(Some((Light::point(), Editable {}))); - world.spawn(Some((Light::simple(), Editable {}))); - world.spawn(Some((Light::spot(), Editable {}))); -} diff --git a/_examples/light/settings.rs b/_examples/light/settings.rs deleted file mode 100644 index fd2ac820..00000000 --- a/_examples/light/settings.rs +++ /dev/null @@ -1,563 +0,0 @@ -use crate::Editable; - -use dotrix::ecs::{Const, Mut}; -use dotrix::egui::{CollapsingHeader, DragValue, Egui, Grid, ScrollArea, SidePanel, Slider}; -use dotrix::input::{Button, State as InputState}; -use dotrix::math::Vec3; -use dotrix::overlay::Overlay; -use dotrix::pbr::Light; -use dotrix::{Camera, Color, Frame, Input, World}; - -use std::f32::consts::PI; - -pub struct CarSettings { - pub animate: bool, - pub point_lights: bool, - pub spot_lights: bool, -} - -pub struct Settings { - // Lights here are only structs, not real components. - pub ambient_light_intensity: f32, - pub directional_light_intensity: f32, - pub simple_light_intensity: f32, - pub point_light_intensity: f32, - pub spot_light_intensity: f32, - - pub ambient_light_color: [f32; 3], - pub directional_light_color: [f32; 3], - pub point_light_color: [f32; 3], - pub simple_light_color: [f32; 3], - pub spot_light_color: [f32; 3], - - pub directional_light_enabled: bool, - pub point_light_enabled: bool, - pub simple_light_enabled: bool, - pub spot_light_enabled: bool, - - pub directional_light_direction: Vec3, - pub spot_light_direction: Vec3, - - pub point_light_position: Vec3, - pub simple_light_position: Vec3, - pub spot_light_position: Vec3, - - pub spot_light_cut_off: f32, - pub spot_light_outer_cut_off: f32, - - pub point_light_constant: f32, - pub point_light_linear: f32, - pub point_light_quadratic: f32, - - pub car: CarSettings, -} - -impl Settings { - /// Reset to default values - pub fn reset(&mut self) { - *self = Settings::default(); - } -} - -impl Default for Settings { - fn default() -> Self { - Self { - ambient_light_color: Color::rgb(0.04, 0.04, 0.08).into(), - directional_light_color: Color::white().into(), - point_light_color: Color::green().into(), - simple_light_color: Color::rgb(0.15, 0.08, 0.08).into(), - spot_light_color: Color::yellow().into(), - - ambient_light_intensity: 0.8, - directional_light_intensity: 0.5, - simple_light_intensity: 0.35, - point_light_intensity: 1.0, - spot_light_intensity: 1.0, - - directional_light_enabled: true, - point_light_enabled: true, - simple_light_enabled: true, - spot_light_enabled: true, - - directional_light_direction: Vec3::new(0.3, -0.5, -0.6), - spot_light_direction: Vec3::new(-20.0, -20.0, 0.0), - - point_light_position: Vec3::new(6.0, 1.0, 0.0), - simple_light_position: Vec3::new(-2.0, 2.0, 2.0), - spot_light_position: Vec3::new(12.0, 2.5, -10.0), - - spot_light_cut_off: 0.8, - spot_light_outer_cut_off: 0.65, - - point_light_constant: 1.0, - point_light_linear: 0.35, - point_light_quadratic: 0.44, - - car: CarSettings { - animate: true, - point_lights: true, - spot_lights: true, - }, - } - } -} - -pub fn ui(mut settings: Mut, overlay: Mut) { - let egui = overlay - .get::() - .expect("Renderer does not contain an Overlay instance"); - - SidePanel::left("side_panel").show(&egui.ctx, |ui| { - ScrollArea::vertical().show(ui, |ui| { - CollapsingHeader::new("Car Settings") - .default_open(true) - .show(ui, |ui| { - Grid::new("car settings").show(ui, |ui| { - ui.label("Animate"); - ui.checkbox(&mut settings.car.animate, ""); - ui.end_row(); - - ui.label("Point lights"); - ui.checkbox(&mut settings.car.point_lights, ""); - ui.end_row(); - - ui.label("Spot lights"); - ui.checkbox(&mut settings.car.spot_lights, ""); - ui.end_row(); - }); - }); - - CollapsingHeader::new("Ambient Light") - .default_open(true) - .show(ui, |ui| { - Grid::new("ambient light").show(ui, |ui| { - ui.label("Color"); - ui.color_edit_button_rgb(&mut settings.ambient_light_color); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.ambient_light_color = Settings::default().ambient_light_color; - }; - ui.end_row(); - - ui.label("Intensity"); - ui.add( - Slider::new(&mut settings.ambient_light_intensity, 0.0..=3.0).text(""), - ); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.ambient_light_intensity = - Settings::default().ambient_light_intensity; - }; - ui.end_row(); - }); - }); - - CollapsingHeader::new("Directional Light") - .default_open(true) - .show(ui, |ui| { - Grid::new("dir light").show(ui, |ui| { - ui.label("Enabled"); - ui.checkbox(&mut settings.directional_light_enabled, ""); - ui.end_row(); - - ui.label("Direction"); - ui.horizontal(|ui| { - ui.add( - DragValue::new(&mut settings.directional_light_direction.x) - .prefix("x: ") - .speed(0.01), - ); - ui.add( - DragValue::new(&mut settings.directional_light_direction.y) - .prefix("y: ") - .speed(0.01), - ); - ui.add( - DragValue::new(&mut settings.directional_light_direction.z) - .prefix("z: ") - .speed(0.01), - ); - }); - ui.end_row(); - - ui.label("Color"); - ui.color_edit_button_rgb(&mut settings.directional_light_color); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.directional_light_color = - Settings::default().directional_light_color; - }; - ui.end_row(); - - ui.label("Intensity"); - ui.add( - Slider::new(&mut settings.directional_light_intensity, 0.0..=3.0) - .text(""), - ); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.directional_light_intensity = - Settings::default().directional_light_intensity; - }; - ui.end_row(); - }); - }); - - CollapsingHeader::new("Point Light") - .default_open(true) - .show(ui, |ui| { - Grid::new("point light").show(ui, |ui| { - ui.label("Enabled"); - ui.checkbox(&mut settings.point_light_enabled, ""); - ui.end_row(); - - ui.label("Position"); - ui.horizontal(|ui| { - ui.add( - DragValue::new(&mut settings.point_light_position.x) - .prefix("x: ") - .speed(0.1), - ); - ui.add( - DragValue::new(&mut settings.point_light_position.y) - .prefix("y: ") - .speed(0.1), - ); - ui.add( - DragValue::new(&mut settings.point_light_position.z) - .prefix("z: ") - .speed(0.1), - ); - }); - ui.end_row(); - - ui.label("Color"); - ui.color_edit_button_rgb(&mut settings.point_light_color); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.point_light_color = Settings::default().point_light_color; - }; - ui.end_row(); - - ui.label("Intensity"); - ui.add( - Slider::new(&mut settings.point_light_intensity, 0.0..=3.0).text(""), - ); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.point_light_intensity = - Settings::default().point_light_intensity; - }; - ui.end_row(); - - ui.label("Constant Attenuation"); - ui.add(Slider::new(&mut settings.point_light_constant, 0.0..=3.0).text("")); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.point_light_constant = - Settings::default().point_light_constant; - }; - ui.end_row(); - - ui.label("Linear Attenuation"); - ui.add(Slider::new(&mut settings.point_light_linear, 0.0..=3.0).text("")); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.point_light_linear = Settings::default().point_light_linear; - }; - ui.end_row(); - - ui.label("Quadratic Attenuation"); - ui.add( - Slider::new(&mut settings.point_light_quadratic, 0.0..=3.0).text(""), - ); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.point_light_quadratic = - Settings::default().point_light_quadratic; - }; - ui.end_row(); - }); - }); - - CollapsingHeader::new("Simple Light") - .default_open(true) - .show(ui, |ui| { - Grid::new("simple light").show(ui, |ui| { - ui.label("Enabled"); - ui.checkbox(&mut settings.simple_light_enabled, ""); - ui.end_row(); - - ui.label("Position"); - ui.horizontal(|ui| { - ui.add( - DragValue::new(&mut settings.simple_light_position.x) - .prefix("x: ") - .speed(0.01), - ); - ui.add( - DragValue::new(&mut settings.simple_light_position.y) - .prefix("y: ") - .speed(0.01), - ); - ui.add( - DragValue::new(&mut settings.simple_light_position.z) - .prefix("z: ") - .speed(0.01), - ); - }); - ui.end_row(); - - ui.label("Color"); - ui.color_edit_button_rgb(&mut settings.simple_light_color); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.simple_light_color = Settings::default().simple_light_color; - }; - ui.end_row(); - - ui.label("Intensity"); - ui.add( - Slider::new(&mut settings.simple_light_intensity, 0.0..=3.0).text(""), - ); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.simple_light_intensity = - Settings::default().simple_light_intensity; - }; - ui.end_row(); - }); - }); - - CollapsingHeader::new("Spot Light") - .default_open(true) - .show(ui, |ui| { - Grid::new("spot light").show(ui, |ui| { - ui.label("Enabled"); - ui.checkbox(&mut settings.spot_light_enabled, ""); - ui.end_row(); - - ui.label("Position"); - ui.horizontal(|ui| { - ui.add( - DragValue::new(&mut settings.spot_light_position.x) - .prefix("x: ") - .speed(0.1), - ); - ui.add( - DragValue::new(&mut settings.spot_light_position.y) - .prefix("y: ") - .speed(0.1), - ); - ui.add( - DragValue::new(&mut settings.spot_light_position.z) - .prefix("z: ") - .speed(0.1), - ); - }); - ui.end_row(); - - ui.label("Direction"); - ui.horizontal(|ui| { - ui.add( - DragValue::new(&mut settings.spot_light_direction.x) - .prefix("x: ") - .speed(0.1), - ); - ui.add( - DragValue::new(&mut settings.spot_light_direction.y) - .prefix("y: ") - .speed(0.1), - ); - ui.add( - DragValue::new(&mut settings.spot_light_direction.z) - .prefix("z: ") - .speed(0.1), - ); - }); - ui.end_row(); - - ui.label("Color"); - ui.color_edit_button_rgb(&mut settings.spot_light_color); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.spot_light_color = Settings::default().spot_light_color; - }; - ui.end_row(); - - ui.label("Intensity"); - ui.add(Slider::new(&mut settings.spot_light_intensity, 0.0..=3.0).text("")); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.spot_light_intensity = - Settings::default().spot_light_intensity; - }; - ui.end_row(); - - ui.label("Cut-off"); - ui.add(Slider::new(&mut settings.spot_light_cut_off, 0.0..=1.0).text("")); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.spot_light_cut_off = Settings::default().spot_light_cut_off; - }; - ui.end_row(); - - ui.label("Outer cut-off"); - ui.add( - Slider::new(&mut settings.spot_light_outer_cut_off, 0.0..=1.0).text(""), - ); - if ui.button("↺").on_hover_text("Reset value").clicked() { - settings.spot_light_outer_cut_off = - Settings::default().spot_light_outer_cut_off; - }; - ui.end_row(); - }); - }); - - if ui.button("Reset all").clicked() { - settings.reset(); - }; - }); - }); -} - -/// This func updates camera based on values in editor and controls -pub fn update_camera( - mut camera: Mut, - input: Const, - frame: Const, - overlay: Mut, -) { - let egui = overlay - .get::() - .expect("Renderer does not contain an Overlay instance"); - const ROTATE_SPEED: f32 = PI / 10.0; - const ZOOM_SPEED: f32 = 10.0; - - let time_delta = frame.delta().as_secs_f32(); - let mouse_delta = input.mouse_delta(); - let mouse_scroll = input.mouse_scroll(); - - // Get values from camera - let mut distance = camera.distance; - let target = camera.target; - let mut pan = camera.pan; - let mut tilt = camera.tilt; - - // Calculate new values - if !egui.wants_pointer_input() { - distance -= ZOOM_SPEED * mouse_scroll * time_delta; - distance = if distance > -1.0 { distance } else { -1.0 }; - - if input.button_state(Button::MouseRight) == Some(InputState::Hold) { - pan += mouse_delta.x * ROTATE_SPEED * time_delta; - tilt = camera.tilt + mouse_delta.y * ROTATE_SPEED * time_delta; - let half_pi = PI / 2.0; - - tilt = if tilt >= half_pi { - half_pi - 0.01 - } else if tilt <= -half_pi { - -half_pi + 0.01 - } else { - tilt - }; - } - } - - // Apply values to camera - camera.target = target; - camera.distance = distance; - camera.pan = pan; - camera.tilt = tilt; -} - -/// This func updates all editable light entities based on values in settings, and car settings. -pub fn update_settings(settings: Const, world: Mut) { - // Set ambient light, should be only one - let query = world.query::<(&mut Light, &Editable)>(); - for (light, _) in query { - match light { - Light::Ambient { - color, intensity, .. - } => { - *intensity = settings.ambient_light_intensity; - *color = Color::from(settings.ambient_light_color); - } - Light::Directional { - color, - intensity, - direction, - enabled, - .. - } => { - *color = settings.directional_light_color.into(); - *intensity = settings.directional_light_intensity; - *direction = settings.directional_light_direction; - *enabled = settings.directional_light_enabled; - } - Light::Point { - color, - intensity, - position, - enabled, - constant, - linear, - quadratic, - .. - } => { - *color = settings.point_light_color.into(); - *intensity = settings.point_light_intensity; - *position = settings.point_light_position; - *constant = settings.point_light_constant; - *linear = settings.point_light_linear; - *quadratic = settings.point_light_quadratic; - *enabled = settings.point_light_enabled; - } - Light::Simple { - color, - intensity, - position, - enabled, - .. - } => { - *color = settings.simple_light_color.into(); - *intensity = settings.simple_light_intensity; - *position = settings.simple_light_position; - *enabled = settings.simple_light_enabled; - } - Light::Spot { - color, - intensity, - position, - direction, - enabled, - cut_off, - outer_cut_off, - .. - } => { - *color = settings.spot_light_color.into(); - *intensity = settings.spot_light_intensity; - *position = settings.spot_light_position; - *direction = settings.spot_light_direction; - *cut_off = settings.spot_light_cut_off; - *outer_cut_off = settings.spot_light_outer_cut_off; - *enabled = settings.spot_light_enabled; - } - } - } - - /* - // Query directional lights entities - let query = world.query::<(&mut DirLight, &Editable)>(); - for (directional_light, _) in query { - directional_light.clone_from(&settings.directional_light); - } - - // Query point light entities - let query = world.query::<(&mut PointLight, &Editable)>(); - for (point_light, _) in query { - point_light.clone_from(&settings.point_light); - point_light.color = Color::from( settings.point_light_color); - } - - // Query simple light entities - let query = world.query::<(&mut SimpleLight, &Editable)>(); - for (simple_light, _) in query { - simple_light.clone_from(&settings.simple_light); - simple_light.color = Color::from( settings.simple_light_color); - } - - // Query spot light entities - let query = world.query::<(&mut SpotLight, &Editable)>(); - for (spot_light, _) in query { - spot_light.clone_from(&settings.spot_light); - spot_light.color = Color::from( settings.spot_light_color); - } - */ -} diff --git a/_examples/light/skybox.rs b/_examples/light/skybox.rs deleted file mode 100644 index a87d6834..00000000 --- a/_examples/light/skybox.rs +++ /dev/null @@ -1,32 +0,0 @@ -use dotrix::ecs::Mut; -use dotrix::renderer::Render; -use dotrix::sky::SkyBox; -use dotrix::{Assets, CubeMap, World}; - -pub fn startup(mut world: Mut, mut assets: Mut) { - // Import skybox textures - assets.import("assets/skybox-night/skybox_right.png"); - assets.import("assets/skybox-night/skybox_left.png"); - assets.import("assets/skybox-night/skybox_top.png"); - assets.import("assets/skybox-night/skybox_bottom.png"); - assets.import("assets/skybox-night/skybox_back.png"); - assets.import("assets/skybox-night/skybox_front.png"); - - // Spawn skybox - world.spawn(Some(( - SkyBox { - view_range: 500.0, - ..Default::default() - }, - CubeMap { - right: assets.register("skybox_right"), - left: assets.register("skybox_left"), - top: assets.register("skybox_top"), - bottom: assets.register("skybox_bottom"), - back: assets.register("skybox_back"), - front: assets.register("skybox_front"), - ..Default::default() - }, - Render::default(), - ))); -} diff --git a/_examples/light/terrain.rs b/_examples/light/terrain.rs deleted file mode 100644 index 5db4ad82..00000000 --- a/_examples/light/terrain.rs +++ /dev/null @@ -1,67 +0,0 @@ -use dotrix::assets::Mesh; -use dotrix::ecs::Mut; -use dotrix::math::Vec3; -use dotrix::pbr; -use dotrix::{Assets, World}; - -const TERRAIN_SIZE: usize = 128; // Number of sqaures per side - -pub fn startup(mut world: Mut, mut assets: Mut) { - let size = TERRAIN_SIZE; - let mut positions = Vec::with_capacity(3 * 2 * size * size); - let mut uvs = Vec::new(); - for x in 0..size { - let x0 = x as f32; - let x1 = x0 + 1.0; - for z in 0..size { - let z0 = z as f32; - let z1 = z0 + 1.0; - // Add vertices - positions.push([x0, 0.0, z0]); - positions.push([x0, 0.0, z1]); - positions.push([x1, 0.0, z0]); - positions.push([x1, 0.0, z0]); - positions.push([x0, 0.0, z1]); - positions.push([x1, 0.0, z1]); - // Add texture vertices - uvs.push([0.0, 0.0]); - uvs.push([0.0, 1.0]); - uvs.push([1.0, 0.0]); - uvs.push([1.0, 0.0]); - uvs.push([0.0, 1.0]); - uvs.push([1.0, 1.0]); - } - } - - let normals = Mesh::calculate_normals(&positions, None); - let (tangents, bitangents) = Mesh::calculate_tangents_bitangents(&positions, &uvs, None); - - let mut mesh = Mesh::default(); - - mesh.with_vertices(&positions); - mesh.with_vertices(&normals); - mesh.with_vertices(&tangents); - mesh.with_vertices(&bitangents); - mesh.with_vertices(&uvs); - - // Store mesh and get its ID - let mesh = assets.store_as(mesh, "terrain"); - - // import terrain texture and get its ID - assets.import("assets/textures/terrain.png"); - let texture = assets.register("terrain"); - - // Center terrain tile at coordinate system center (0.0, 0.0, 0.0) by moving the tile on a - // half of its size by X and Z axis - let shift = (size / 2) as f32; - - world.spawn( - (pbr::solid::Entity { - mesh, - texture, - translate: Vec3::new(-shift, 0.0, -shift), - ..Default::default() - }) - .some(), - ); -} diff --git a/_examples/msaa/main.rs b/_examples/msaa/main.rs deleted file mode 100644 index e0542f7a..00000000 --- a/_examples/msaa/main.rs +++ /dev/null @@ -1,120 +0,0 @@ -use dotrix::assets::Texture; -use dotrix::math::Vec3; -use dotrix::prelude::*; -use dotrix::primitives::Cube; -use dotrix::renderer::{Antialiasing, Pipeline}; -use dotrix::{camera, egui, input, overlay, pbr}; -use dotrix::{Assets, Camera, Color, Frame, Input, Renderer, Transform, World}; - -fn main() { - Dotrix::application("Dotrix: MSAA") - .with(System::from(startup)) - .with(System::from(camera::control)) - .with(System::from(ui)) - .with(pbr::extension) - .with(overlay::extension) - .with(egui::extension) - .run(); -} - -fn startup(mut camera: Mut, mut world: Mut, mut assets: Mut) { - // setup camera - camera.target = [0., 0., 0.].into(); - camera.distance = 3.0; - camera.tilt = 0.0; - - // import material textures - assets.import("assets/stylized-crate/stylized-crate_albedo.jpg"); - assets.import("assets/stylized-crate/stylized-crate_ao.jpg"); - // assets.import("assets/space-crate/stylized-crate_heightmap.jpg"); - assets.import("assets/stylized-crate/stylized-crate_metallic.jpg"); - assets.import("assets/stylized-crate/stylized-crate_normalmap.jpg"); - assets.import("assets/stylized-crate/stylized-crate_roughness.jpg"); - - let albedo = assets.register::("stylized-crate_albedo"); - let ao = assets.register::("stylized-crate_ao"); - // let height = assets.register("stylized-crate_heightmap"); - let metallic = assets.register::("stylized-crate_metallic"); - let normal_ogl = assets.register::("stylized-crate_normalmap"); - let roughness = assets.register::("stylized-crate_roughness"); - - // generate simple cube mesh - let mesh = Cube::builder(1.0) - .with_positions() - .with_normals() - .with_tangents_bitangents() - .with_uvs(Cube::default_uvs()) - .mesh(); - - // store the mesh - let mesh = assets.store(mesh); - - // spawn the crate - world.spawn([( - pbr::Model { - mesh, - ..Default::default() - }, - pbr::Material { - texture: albedo, - roughness_texture: roughness, - metallic_texture: metallic, - normal_texture: normal_ogl, - ao_texture: ao, - ..Default::default() - }, - Transform::default(), - Pipeline::render(Id::default()), - )]); - - // Spawn lights - world.spawn([ - (pbr::Light::Directional { - direction: Vec3::new(1.0, 1.0, 10.0), - color: Color::rgb(1.0, 1.0, 1.0), - intensity: 3.0, - enabled: true, - },), - (pbr::Light::Directional { - direction: Vec3::new(-1.0, -1.0, -10.0), - color: Color::rgb(1.0, 1.0, 1.0), - intensity: 3.0, - enabled: true, - },), - ]); -} - -fn ui( - overlay: Const, - frame: Const, - input: Const, - mut renderer: Mut, -) { - let egui_overlay = overlay - .get::() - .expect("Egui overlay must be added on startup"); - - egui::Area::new("FPS counter") - .fixed_pos(egui::pos2(16.0, 16.0)) - .show(&egui_overlay.ctx, |ui| { - ui.colored_label( - egui::Rgba::from_rgb(255.0, 255.0, 255.0), - format!( - "These PBR material was downloaded from 3dtextures.me\n\ - Press SPACE to toggle antialiasing\n\ - FPS: {:.1}; ", - frame.fps() - ), - ); - }); - - if input.button_state(input::Button::Key(input::KeyCode::Space)) - == Some(input::State::Activated) - { - renderer.antialiasing = if renderer.antialiasing == Antialiasing::Enabled { - Antialiasing::Disabled - } else { - Antialiasing::Enabled - }; - } -} diff --git a/_examples/normal_map/main.rs b/_examples/normal_map/main.rs deleted file mode 100644 index 4f57e9ef..00000000 --- a/_examples/normal_map/main.rs +++ /dev/null @@ -1,74 +0,0 @@ -use dotrix::assets::{Mesh, Texture}; -use dotrix::camera; -use dotrix::ecs::Mut; -use dotrix::math::Vec3; -use dotrix::pbr::{self, Light}; -use dotrix::{Assets, Camera, Color, Dotrix, System, World}; - -fn main() { - Dotrix::application("Dotrix: Normal Map") - .with(System::from(startup)) - .with(System::from(camera::control)) - .with(pbr::extension) - .run(); -} - -fn startup(mut camera: Mut, mut world: Mut, mut assets: Mut) { - camera.target = [0., 0., 0.].into(); - camera.distance = 3.0; - camera.tilt = 0.0; - - assets.import("assets/models/sphere.gltf"); - let mesh_handle = assets.register::("sphere::mesh"); - - assets.import("assets/textures/mossy_bricks/Bricks076C_1K_AmbientOcclusion.jpg"); - assets.import("assets/textures/mossy_bricks/Bricks076C_1K_Color.jpg"); - assets.import("assets/textures/mossy_bricks/Bricks076C_1K_NormalDX.jpg"); - assets.import("assets/textures/mossy_bricks/Bricks076C_1K_Roughness.jpg"); - - let ao = assets.register::("Bricks076C_1K_AmbientOcclusion"); - let albedo = assets.register::("Bricks076C_1K_Color"); - let normal = assets.register::("Bricks076C_1K_NormalDX"); - let roughness = assets.register::("Bricks076C_1K_Roughness"); - - let spheres = vec![ - // Only color - (pbr::solid::Entity { - mesh: mesh_handle, - texture: albedo, - translate: Vec3::new(0., 0., 0.), - ..Default::default() - }) - .tuple(), - // Color, pbr roughness/ao and normal map - (pbr::solid::Entity { - mesh: mesh_handle, - texture: albedo, - roughness_texture: roughness, - ao_texture: ao, - normal_texture: normal, - translate: Vec3::new(-2., 0., 0.), - ..Default::default() - }) - .tuple(), - // Color and pbr roughness/ao - (pbr::solid::Entity { - mesh: mesh_handle, - texture: albedo, - roughness_texture: roughness, - ao_texture: ao, - translate: Vec3::new(2., 0., 0.), - ..Default::default() - }) - .tuple(), - ]; - world.spawn(spheres); - - // Spawn lights - world.spawn(vec![(Light::Directional { - direction: Vec3::new(1.0, 1.0, 10.0), - color: Color::rgb(1.0, 1.0, 1.0), - intensity: 3.0, - enabled: true, - },)]); -} diff --git a/_examples/pbr/main.rs b/_examples/pbr/main.rs deleted file mode 100644 index 5d2b19de..00000000 --- a/_examples/pbr/main.rs +++ /dev/null @@ -1,52 +0,0 @@ -use dotrix::assets::Mesh; -use dotrix::camera; -use dotrix::ecs::Mut; -use dotrix::math::Vec3; -use dotrix::pbr::{self, Light}; -use dotrix::{Assets, Camera, Color, Dotrix, System, World}; - -fn main() { - Dotrix::application("Dotrix: PBR") - .with(System::from(startup)) - .with(System::from(camera::control)) - .with(pbr::extension) - .run(); -} - -fn startup(mut camera: Mut, mut world: Mut, mut assets: Mut) { - camera.target = [0., 0., 0.].into(); - camera.distance = 30.0; - camera.tilt = 0.0; - - assets.import("assets/models/sphere.gltf"); - let mesh_handle = assets.register::("sphere::mesh"); - - let mut spheres = vec![]; - for i in 0..11 { - let x = i as f32 * 3.0 - (10. * 3. / 2.); - let metallic = i as f32 * 0.1; - for j in 0..11 { - let y = j as f32 * 3.0 - (10. * 3. / 2.); - let roughness = j as f32 * 0.1; - let obj = (pbr::solid::Entity { - mesh: mesh_handle, - albedo: [0.2, 0.8, 0.2, 1.].into(), - roughness, - metallic, - translate: Vec3::new(x, y, 0.), - ..Default::default() - }) - .tuple(); - spheres.push(obj); - } - } - world.spawn(spheres); - - // Spawn lights - world.spawn(vec![(Light::Directional { - direction: Vec3::new(1.0, 1.0, 10.0), - color: Color::rgb(1.0, 1.0, 1.0), - intensity: 3.0, - enabled: true, - },)]); -} diff --git a/_examples/shader/main.rs b/_examples/shader/main.rs deleted file mode 100644 index 714f6fc2..00000000 --- a/_examples/shader/main.rs +++ /dev/null @@ -1,43 +0,0 @@ -use dotrix::camera; -use dotrix::prelude::*; -use dotrix::primitives::Cube; -use dotrix::renderer::Render; -use dotrix::{Assets, Camera, Renderer, World}; -mod shader; - -use shader::Gradient; - -fn main() { - Dotrix::application("Dotrix: Shader Example") - .with(System::from(startup)) - .with(System::from(camera::control)) - .with(shader::extension) - .run(); -} - -fn startup( - mut camera: Mut, - mut world: Mut, - mut assets: Mut, - renderer: Const, -) { - camera.target = [0., 0., 0.].into(); - camera.distance = 2.0; - camera.tilt = 0.0; - - let mut mesh = Cube::builder(0.5).with_positions().mesh(); - - mesh.load(&renderer); - - let mesh_handle = assets.store(mesh); - - // Spawn Gradient data and a default pipeline - world.spawn(Some(( - Gradient { - nadir_color: [1., 0., 0.4, 1.], - zenith_color: [0.2, 0.4, 0.8, 1.], - mesh: mesh_handle, - }, - Render::default(), - ))); -} diff --git a/_examples/shader/shader.rs b/_examples/shader/shader.rs deleted file mode 100644 index a8b92473..00000000 --- a/_examples/shader/shader.rs +++ /dev/null @@ -1,156 +0,0 @@ -use dotrix::assets::{Mesh, Shader}; -use dotrix::camera::ProjView; -use dotrix::ecs::{Const, Mut, System}; -use dotrix::renderer::{ - BindGroup, Binding, Buffer, DrawArgs, PipelineLayout, Render, RenderOptions, Stage, -}; -use dotrix::{Application, Assets, Globals, Id, Renderer, World}; - -pub const PIPELINE_LABEL: &str = "example::gradient"; - -// This holds our public level representation of the data -#[derive(Clone, Copy)] -pub struct Gradient { - pub nadir_color: [f32; 4], - pub zenith_color: [f32; 4], - pub mesh: Id, - // Could add things like matrix transform too -} - -// This is the low level representation of the uniform data -// that will go to the gpu -#[repr(C)] -#[derive(Default, Copy, Clone)] -struct GradientUniform { - zenith_color: [f32; 4], - nadir_color: [f32; 4], - // Uniform data should be divisible by 16 - // so we add padding - // - // https://www.w3.org/TR/WGSL/#alignment-and-size - padding: [f32; 8], -} - -// This tranforms from the high level to the low level data -impl From<&Gradient> for GradientUniform { - fn from(obj: &Gradient) -> Self { - Self { - zenith_color: obj.zenith_color, - nadir_color: obj.nadir_color, - ..Default::default() // Just use default to add in the padding - } - } -} - -// Byte muck is used to cast the data into a byte array -unsafe impl bytemuck::Zeroable for GradientUniform {} -unsafe impl bytemuck::Pod for GradientUniform {} - -/// This holds the gpu uniform buffer -pub struct GradientBuffer { - pub uniform: Buffer, -} - -/// startup system -/// -/// We add the uniform buffer at startup -/// where it will then be avaliable for the render to use (and reuse) -/// -/// We also compile our shader -pub fn startup(mut globals: Mut, renderer: Const, mut assets: Mut) { - globals.set(GradientBuffer { - uniform: Buffer::uniform("Gradient Buffer"), - }); - - let mut shader = Shader { - name: String::from(PIPELINE_LABEL), - code: include_str!("./shader.wgsl").to_string(), - ..Default::default() - }; - - shader.load(&renderer); - - assets.store_as(shader, PIPELINE_LABEL); -} - -/// render system -/// -/// Here we update the contents of the uniform buffer -/// then render -pub fn render( - world: Const, - mut renderer: Mut, - mut globals: Mut, - assets: Const, -) { - for (gradient, render) in world.query::<(&Gradient, &mut Render)>() { - // Update the uniform - if let Some(uniform_buffer) = globals.get_mut::() { - let uniform: GradientUniform = gradient.into(); - renderer.load_buffer( - &mut uniform_buffer.uniform, - bytemuck::cast_slice(&[uniform]), - ); - } - - // Get the uniform for render - let gradient_buffer = globals - .get::() - .expect("Gradient buffer must be loaded"); - - // Get the uniform that holds the camera's project view - let proj_view = globals - .get::() - .expect("ProjView buffer must be loaded"); - - // Set the shader on the pipeline - if render.pipeline.shader.is_null() { - render.pipeline.shader = assets.find::(PIPELINE_LABEL).unwrap_or_default(); - } - - // check if model is disabled or already rendered - if !render.pipeline.cycle(&renderer) { - continue; - } - - let mesh = assets.get(gradient.mesh).expect("Mesh must be loaded"); - - // Bind the uniforms to the shader - if !render.pipeline.ready(&renderer) { - if let Some(shader) = assets.get(render.pipeline.shader) { - if !shader.loaded() { - continue; - } - - renderer.bind( - &mut render.pipeline, - PipelineLayout::Render { - label: String::from(PIPELINE_LABEL), - mesh, - shader, - bindings: &[BindGroup::new( - "Globals", - vec![ - Binding::Uniform("ProjView", Stage::Vertex, &proj_view.uniform), - Binding::Uniform( - "Gradient", - Stage::Fragment, - &gradient_buffer.uniform, - ), - ], - )], - options: RenderOptions::default(), - }, - ); - } - } - - // Run the pipeline - renderer.draw(&mut render.pipeline, mesh, &DrawArgs::default()); - } -} - -pub fn extension(app: &mut Application) { - app.add_system(System::from(startup)); - app.add_system(System::from(render)); -} diff --git a/_examples/shader/shader.wgsl b/_examples/shader/shader.wgsl deleted file mode 100644 index 4ab07122..00000000 --- a/_examples/shader/shader.wgsl +++ /dev/null @@ -1,42 +0,0 @@ -// STAGE: VERTEX --------------------------------------------------------------------------------- -struct VertexOutput { - [[builtin(position)]] position: vec4; - [[location(0)]] local_position: vec3; -}; - -struct Renderer { - proj_view: mat4x4; -}; -[[group(0), binding(0)]] -var u_renderer: Renderer; - -[[stage(vertex)]] -fn vs_main( - [[location(0)]] position: vec3, -) -> VertexOutput { - var out: VertexOutput; - out.local_position = position; - out.position = u_renderer.proj_view * vec4(position, 1.0); - return out; -} - -// STAGE: FRAGMENT ------------------------------------------------------------------------------- -struct Gradient { - zenith_color: vec4; - nadir_color: vec4; -}; -[[group(0), binding(1)]] -var u_gradient: Gradient; - -[[stage(fragment)]] -fn fs_main(in: VertexOutput) -> [[location(0)]] vec4 { - let normalized_direction: vec3 = normalize(in.local_position); - let nadir_color: vec3 = u_gradient.nadir_color.xyz; - - let zenith_color: vec3 = u_gradient.zenith_color.xyz; - - let mix_amount: vec3 = vec3(smoothStep(-1., 1., normalized_direction.y)); - let horizon_color: vec3 = mix(nadir_color, zenith_color, mix_amount); - - return vec4(horizon_color , 1.0); -} diff --git a/_examples/skybox/main.rs b/_examples/skybox/main.rs deleted file mode 100644 index caa2feb6..00000000 --- a/_examples/skybox/main.rs +++ /dev/null @@ -1,44 +0,0 @@ -use dotrix::camera; -use dotrix::prelude::*; -use dotrix::renderer::Render; -use dotrix::sky::{skybox, SkyBox}; -use dotrix::{Assets, Camera, CubeMap, World}; - -fn main() { - Dotrix::application("Dotrix: SkyBox Example") - .with(System::from(startup)) - .with(System::from(camera::control)) - .with(skybox::extension) - .run(); -} - -fn startup(mut camera: Mut, mut world: Mut, mut assets: Mut) { - camera.distance = 1.0; - camera.tilt = 0.0; - - // Import skybox textures - assets.import("assets/skybox-day/skybox_right.png"); - assets.import("assets/skybox-day/skybox_left.png"); - assets.import("assets/skybox-day/skybox_top.png"); - assets.import("assets/skybox-day/skybox_bottom.png"); - assets.import("assets/skybox-day/skybox_back.png"); - assets.import("assets/skybox-day/skybox_front.png"); - - // Spawn skybox - world.spawn(Some(( - SkyBox { - view_range: 500.0, - ..Default::default() - }, - CubeMap { - right: assets.register("skybox_right"), - left: assets.register("skybox_left"), - top: assets.register("skybox_top"), - bottom: assets.register("skybox_bottom"), - back: assets.register("skybox_back"), - front: assets.register("skybox_front"), - ..Default::default() - }, - Render::default(), - ))); -} diff --git a/_examples/states/main.rs b/_examples/states/main.rs deleted file mode 100644 index f036b6d2..00000000 --- a/_examples/states/main.rs +++ /dev/null @@ -1,179 +0,0 @@ -use dotrix::camera; -use dotrix::egui::{self, Egui}; -use dotrix::input::{ActionMapper, Button, KeyCode, Mapper, Modifiers}; -use dotrix::overlay::{self, Overlay}; -use dotrix::prelude::*; -use dotrix::renderer::Render; -use dotrix::sky::{skybox, SkyBox}; -use dotrix::{Assets, CubeMap, Frame, Input, State, World}; - -/// In main state you can rotate camera and see FPS counter -struct MainState { - name: String, -} - -/// In paused state you can't rotate camera -struct PauseState { - name: String, - handled: bool, -} - -fn main() { - Dotrix::application("Dotrix: Demo Example") - .with(System::from(startup)) - .with(System::from(ui_main).with(State::off::())) - .with(System::from(ui_paused).with(State::on::())) - .with( - // Camera control should work only in Main state - System::from(camera::control).with(State::on::()), - ) - .with(overlay::extension) - .with(egui::extension) - .with(skybox::extension) - .run(); -} - -/// Initial game routines -fn startup( - mut assets: Mut, - mut input: Mut, - mut state: Mut, - mut world: Mut, -) { - // Push main state - state.push(MainState { - name: String::from("Main state"), - }); - - input.set_mapper(Box::new(Mapper::::new())); - - // Import skybox textures - assets.import("assets/skybox-day/skybox_right.png"); - assets.import("assets/skybox-day/skybox_left.png"); - assets.import("assets/skybox-day/skybox_top.png"); - assets.import("assets/skybox-day/skybox_bottom.png"); - assets.import("assets/skybox-day/skybox_back.png"); - assets.import("assets/skybox-day/skybox_front.png"); - - // Spawn skybox - world.spawn(Some(( - SkyBox { - view_range: 500.0, - ..Default::default() - }, - CubeMap { - right: assets.register("skybox_right"), - left: assets.register("skybox_left"), - top: assets.register("skybox_top"), - bottom: assets.register("skybox_bottom"), - back: assets.register("skybox_back"), - front: assets.register("skybox_front"), - ..Default::default() - }, - Render::default(), - ))); - - // Map Escape key to Pause the game - input.mapper_mut::>().set(&[( - Action::TogglePause, - Button::Key(KeyCode::Escape), - Modifiers::empty(), - )]); -} - -/// Enumeration of actions provided by the game -#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] -enum Action { - TogglePause, -} - -fn ui_main( - mut state: Mut, - input: Const, - overlay: Const, - frame: Const, -) { - let egui_overlay = overlay - .get::() - .expect("Egui overlay must be added on startup"); - - let main_state = state - .get::() - .expect("The system to be run in main state"); - - if input.is_action_activated(Action::TogglePause) { - state.push(PauseState { - name: String::from("Paused State"), - handled: false, - }); - return; - } - - egui::Area::new("Info label") - .fixed_pos(egui::pos2(16.0, 16.0)) - .show(&egui_overlay.ctx, |ui| { - ui.colored_label( - egui::Rgba::from_rgb(255.0, 255.0, 255.0), - format!("Press ESC to exit {} and pause", main_state.name), - ); - }); - - egui::Area::new("FPS counter") - .fixed_pos(egui::pos2(16.0, 32.0)) - .show(&egui_overlay.ctx, |ui| { - ui.colored_label( - egui::Rgba::from_rgb(255.0, 255.0, 255.0), - format!("FPS: {:.1}", frame.fps()), - ); - }); -} - -fn ui_paused(mut state: Mut, input: Const, overlay: Const) { - let egui_overlay = overlay - .get::() - .expect("Egui overlay must be added on startup"); - - let states_stack_dump = state.dump().join(",\n "); - - let mut pause_state = state - .get_mut::() - .expect("The system to be run in pause state"); - - let mut exit_state = pause_state.handled && input.is_action_activated(Action::TogglePause); - pause_state.handled = true; - - egui::containers::Window::new("Paused") - .resizable(false) - .default_width(200.0) - .show(&egui_overlay.ctx, |ui| { - ui.label("Execution is paused. Camera is not controllable"); - ui.label(format!( - "Current states stack: [\n {}\n]", - states_stack_dump - )); - if ui.button("Resume").clicked() { - exit_state = true; - } - }); - - egui::Area::new("Info label") - .fixed_pos(egui::pos2(16.0, 16.0)) - .show(&egui_overlay.ctx, |ui| { - ui.colored_label( - egui::Rgba::from_rgb(255.0, 255.0, 255.0), - format!("Press ESC to exit {} and resume", pause_state.name), - ); - }); - - if exit_state { - state.pop_any(); - } -} - -/// Bind Inputs and Actions -impl ActionMapper for Input { - fn action_mapped(&self, action: Action) -> Option<(Button, Modifiers)> { - let mapper = self.mapper::>(); - mapper.get_button(action) - } -} diff --git a/_examples/window/camera.rs b/_examples/window/camera.rs deleted file mode 100644 index 5ea8320f..00000000 --- a/_examples/window/camera.rs +++ /dev/null @@ -1,23 +0,0 @@ -use dotrix::input::{Button, State as InputState}; -use dotrix::prelude::*; -use dotrix::{Camera, Frame, Input, Window}; - -/// This func updates camera based on controls -pub fn camera_update( - camera: Mut, - frame: Const, - input: Const, - mut window: Mut, -) { - if let Some(state) = input.button_state(Button::MouseRight) { - match state { - InputState::Activated => { - window.set_cursor_visible(false); - } - InputState::Hold => { - dotrix::camera::control(camera, input, frame); - } - InputState::Deactivated => window.set_cursor_visible(true), - } - } -} diff --git a/_examples/window/main.rs b/_examples/window/main.rs deleted file mode 100644 index 82168658..00000000 --- a/_examples/window/main.rs +++ /dev/null @@ -1,30 +0,0 @@ -mod camera; -mod match_finder; -mod settings; - -use camera::camera_update; -use dotrix::prelude::*; -use dotrix::Camera; -use dotrix::{egui, overlay}; - -use match_finder::MatchFinder; -use settings::Settings; - -fn main() { - Dotrix::application("Dotrix: Window Example") - .with(System::from(startup)) - .with(System::from(settings::startup)) - .with(System::from(match_finder::update)) - .with(System::from(settings::ui)) - .with(System::from(camera_update)) - .with(Service::from(MatchFinder::new())) - .with(Service::from(Settings::default())) - .with(overlay::extension) - .with(egui::extension) - .run(); -} - -fn startup(mut camera: Mut) { - camera.distance = 1.0; - camera.tilt = 0.0; -} diff --git a/_examples/window/match_finder.rs b/_examples/window/match_finder.rs deleted file mode 100644 index 9332f483..00000000 --- a/_examples/window/match_finder.rs +++ /dev/null @@ -1,77 +0,0 @@ -use dotrix::prelude::*; -use dotrix::window::UserAttentionType; -use dotrix::{Frame, Window}; - -use std::time::Duration; - -pub struct MatchFinder { - pub attention_type: UserAttentionType, - pub estimated_time: f32, - pub state: MatchFinderState, - wait_ready_time: f32, -} - -pub enum MatchFinderState { - Idle, - Searching(Option), - Stopping(), - Ready(Duration), -} - -impl MatchFinder { - pub fn new() -> Self { - Self { - attention_type: UserAttentionType::Informational, - estimated_time: 10.0, - state: MatchFinderState::Idle, - wait_ready_time: 10.0, - } - } - - pub fn start_searching(&mut self) { - self.state = MatchFinderState::Searching(None); - } - - pub fn stop_searching(&mut self) { - self.state = MatchFinderState::Stopping(); - } -} - -pub fn update(finder: Mut, frame: Const, window: Const) { - match finder.state { - MatchFinderState::Searching(from) => update_searching(from, finder, frame, window), - MatchFinderState::Stopping() => update_stopping(finder, window), - MatchFinderState::Ready(until) => update_ready(until, finder, frame), - _ => {} - } -} - -fn update_searching( - from: Option, - mut finder: Mut, - frame: Const, - window: Const, -) { - if let Some(from) = from { - let dur_secs = (frame.time() - from).as_secs_f32(); - if dur_secs > finder.estimated_time { - let until = frame.time() + Duration::from_secs_f32(finder.wait_ready_time); - window.request_attention(Some(finder.attention_type)); - println!("request attention {:?}", finder.attention_type); - finder.state = MatchFinderState::Ready(until); - } - } else { - finder.state = MatchFinderState::Searching(Some(frame.time())); - } -} - -fn update_stopping(mut finder: Mut, window: Const) { - window.request_attention(None); - finder.state = MatchFinderState::Idle; -} - -fn update_ready(until: Duration, mut finder: Mut, frame: Const) { - if frame.time() > until { - finder.stop_searching(); - } -} diff --git a/_examples/window/settings.rs b/_examples/window/settings.rs deleted file mode 100644 index baf9df93..00000000 --- a/_examples/window/settings.rs +++ /dev/null @@ -1,699 +0,0 @@ -use crate::match_finder::{MatchFinder, MatchFinderState}; -use dotrix::assets::{Assets, Texture}; -use dotrix::ecs::{Const, Mut}; -use dotrix::egui::extras::file_dialog::FileDialog; -use dotrix::egui::{ - CollapsingHeader, ComboBox, Egui, Grid, ScrollArea, SidePanel, Slider, TopBottomPanel, -}; -use dotrix::math::{Vec2i, Vec2u}; -use dotrix::overlay::Overlay; -use dotrix::window::{CursorIcon, Fullscreen, UserAttentionType, VideoMode}; -use dotrix::{Frame, Id, Input, Window}; - -use std::{collections::hash_map::HashMap, path::PathBuf}; - -pub struct Settings { - current_monitor_number: usize, - current_video_mode: Option, - icon: String, - icons: HashMap>, - min_inner_size: Vec2u, - opened_file: Option, - open_file_dialog: Option, - save_file_dialog: Option, - title: String, - window_mode: WindowMode, -} - -impl Default for Settings { - fn default() -> Self { - Self { - current_monitor_number: 0, - current_video_mode: None, - icon: String::from("none"), - icons: HashMap::new(), - min_inner_size: Vec2u::new(640, 480), - opened_file: None, - open_file_dialog: None, - save_file_dialog: None, - title: String::new(), - window_mode: WindowMode::Windowed, - } - } -} - -pub fn ui( - assets: Const, - frame: Const, - input: Const, - mut match_finder: Mut, - overlay: Const, - mut settings: Mut, - mut window: Mut, -) { - let egui = overlay - .get::() - .expect("Renderer does not contain an Overlay instance"); - - TopBottomPanel::top("top_panel").show(&egui.ctx, |ui| { - ui.horizontal(|ui| { - if ui.button("🗙").clicked() { - window.close(); - } - if ui.button("🗕").clicked() { - window.set_minimized(true); - } - if window.maximized() { - if ui.button("S").clicked() { - window.set_maximized(false); - } - } else if ui.button("🗖").clicked() { - window.set_maximized(true); - } - - ui.horizontal(|ui| { - if ui.text_edit_singleline(&mut settings.title).lost_focus() { - window.set_title(settings.title.as_str()); - }; - }); - }); - }); - - SidePanel::left("side_panel") - .show(&egui.ctx, |ui| { - ScrollArea::vertical().show(ui, |ui| { - CollapsingHeader::new("ℹ Info") - .default_open(false) - .show(ui, |ui| { - Grid::new("info_grid").show(ui, |ui| { - ui.label("FPS"); - ui.label(format!("{}", frame.fps())); - ui.end_row(); - - ui.label("Screen size"); - ui.label( - match window.screen_size() { - Some(size) => format!("x: {}, y: {}", size.x, size.y), - None => String::from("Cannot detect current monitor!"), - } - ); - ui.end_row(); - }); - - CollapsingHeader::new("ℹ Cursor") - .default_open(false) - .show(ui, |ui| { - Grid::new("info_cursor_grid").show(ui, |ui| { - ui.label("Position"); - ui.label(format!("x: {}, y: {}", - input.mouse_position().unwrap().x, input.mouse_position().unwrap().y)); - ui.end_row(); - - ui.label("Normalized position"); - ui.label(format!("x: {:.4}, y: {:.4}", - input.mouse_position_normalized().x, - input.mouse_position_normalized().y - )); - ui.end_row(); - - ui.label("Delta"); - ui.label(format!("x: {}, y: {}", - input.mouse_delta().x, - input.mouse_delta().y - )); - ui.end_row(); - }); - }); - - CollapsingHeader::new("ℹ Monitors") - .default_open(false) - .show(ui, |ui| { - for monitor in window.monitors() { - CollapsingHeader::new(format!("Monitor {}", monitor.number)) - .default_open(false) - .show(ui, |ui| { - Grid::new(format!("info_monitors_{}", monitor.name)).show(ui, |ui| { - ui.label("Number"); - ui.label(monitor.number.to_string()); - ui.end_row(); - - ui.label("Name"); - ui.label(monitor.name.to_string()); - ui.end_row(); - - ui.label("Scale Factor"); - ui.label(monitor.scale_factor.to_string()); - ui.end_row(); - - ui.label("Size"); - ui.label(format!("x: {}, y: {}", monitor.size.x, monitor.size.y)); - ui.end_row(); - }); - }); - } - }); - - CollapsingHeader::new("ℹ Window") - .default_open(true) - .show(ui, |ui| { - Grid::new("info_window_grid").show(ui, |ui| { - ui.label("Inner Position"); - ui.label( - match window.inner_position() { - Ok(pos) => format!("x: {}, y: {}", pos.x, pos.y), - Err(e) => format!("Cannot detect inner position! {}", e), - } - ); - ui.end_row(); - - ui.label("Inner Size"); - ui.label(format!("x: {}, y: {}", window.inner_size().x, window.inner_size().y)); - ui.end_row(); - - ui.label("Min. Inner Size"); - ui.label(format!("x: {}, y: {}", window.min_inner_size().x, window.min_inner_size().y)); - ui.end_row(); - - ui.label("Outer Position"); - ui.label( - match window.inner_position() { - Ok(pos) => format!("x: {}, y: {}", pos.x, pos.y), - Err(e) => format!("Cannot detect inner position! {}", e), - } - ); - ui.end_row(); - - ui.label("Outer Size"); - ui.label(format!("x: {}, y: {}", window.outer_size().x, window.outer_size().y)); - ui.end_row(); - - ui.label("Scale factor"); - ui.label(format!("{}", window.scale_factor())); - ui.end_row(); - }); - }); - }); - - CollapsingHeader::new("🖱 Cursor") - .default_open(true) - .show(ui, |ui| { - Grid::new("grid_cursor").show(ui, |ui| { - ui.label("Icon"); - let cur_icon = window.cursor_icon(); - let mut new_cur_icon = cur_icon; - - ComboBox::from_id_source("Icon") - .selected_text(format!("{:?}", new_cur_icon)) - .show_ui(ui, |ui| { - for icon in CURSOR_ICONS.iter() { - ui.selectable_value(&mut new_cur_icon, *icon, format!("{:?}", icon)); - } - }); - - if cur_icon != new_cur_icon { - window.set_cursor_icon(new_cur_icon); - } - ui.end_row(); - - ui.label("set Cursor visible"); - ui.horizontal(|ui| { - if ui.selectable_label(window.cursor_visible(), "true").clicked() { - window.set_cursor_visible(true); - } - if ui.selectable_label(!window.cursor_visible(), "false").clicked() { - window.set_cursor_visible(false); - } - }); - ui.end_row(); - - ui.label("set Cursor grab"); - ui.horizontal(|ui| { - if ui.selectable_label(window.cursor_grab(), "true").clicked() { - if let Err(e) = window.set_cursor_grab(true) { - println!("Cannot grab cursor! {}", e); - } - } - if ui.selectable_label(!window.cursor_grab(), "false").clicked() { - if let Err(e) = window.set_cursor_grab(false) { - println!("Cannot ungrab cursor! {}", e); - } - } - }); - ui.end_row(); - }); - }); - - CollapsingHeader::new("S Window") - .default_open(true) - .show(ui, |ui| { - Grid::new("window").show(ui, |ui| { - ui.label("Display mode"); - - ComboBox::from_id_source("Display mode") - .selected_text(format!("{:?}", settings.window_mode)) - .show_ui(ui, |ui| { - for mode in WINDOW_MODES.iter() { - ui.selectable_value(&mut settings.window_mode, *mode, format!("{:?}", mode)); - } - }); - ui.end_row(); - - if WindowMode::BorderlessFullscreen == settings.window_mode - || WindowMode::Fullscreen == settings.window_mode { - let mut current_monitor_number = settings.current_monitor_number; - let mut current_monitor = &window.monitors()[current_monitor_number]; - - ui.label("Monitor"); - ComboBox::from_id_source("Monitor") - .selected_text(format!("{:?}", current_monitor.name)) - .show_ui(ui, |ui| { - for (i, monitor) in window.monitors().iter().enumerate() { - ui.selectable_value(&mut current_monitor_number, i, &monitor.name); - } - }); - if current_monitor_number != settings.current_monitor_number { - settings.current_monitor_number = current_monitor_number; - settings.current_video_mode = None; - current_monitor = &window.monitors()[current_monitor_number]; - } - ui.end_row(); - - if WindowMode::Fullscreen == settings.window_mode { - let video_modes = ¤t_monitor.video_modes; - ui.label("Video mode"); - if settings.current_video_mode.is_none() { - settings.current_video_mode = Some(video_modes[0]); - } - let mut video_mode = settings.current_video_mode; - - ComboBox::from_id_source("Video mode") - .selected_text(format!("{:?}", video_mode.unwrap())) - .show_ui(ui, |ui| { - for mode in video_modes { - ui.selectable_value(&mut video_mode, Some(*mode), mode.to_string()); - } - }); - settings.current_video_mode = video_mode; - ui.end_row(); - } - } - - ui.label(""); - if ui.button("Apply").clicked() { - match settings.window_mode { - WindowMode::Fullscreen => window.set_fullscreen( - Some(Fullscreen::Exclusive(settings.current_video_mode.unwrap())) - ), - WindowMode::BorderlessFullscreen => window.set_fullscreen( - Some(Fullscreen::Borderless(settings.current_monitor_number)) - ), - WindowMode::Windowed => window.set_fullscreen(None), - } - } - - ui.end_row(); - - ui.label("Icon"); - - let win_icon = String::from(settings.icon.as_str()); - - ComboBox::from_id_source("Icon") - .selected_text(settings.icon.to_string()) - .show_ui(ui, |ui| { - ui.selectable_value(&mut settings.icon, String::from("dotrix"), "Dotrix"); - ui.selectable_value(&mut settings.icon, String::from("lowenware"), "Lowenware"); - ui.selectable_value(&mut settings.icon, String::from("rustacean"), "Rustacean"); - ui.selectable_value(&mut settings.icon, String::from("None"), "None"); - }); - - if win_icon != settings.icon { - if let Some(id) = settings.icons.get(&settings.icon) { - window.set_icon(assets.get(*id)); - } else { - window.set_icon(None); - } - } - ui.end_row(); - - ui.label("set Minimized"); - ui.horizontal(|ui| { - if ui.button("true").clicked() { - window.set_minimized(true); - } - }); - ui.end_row(); - - ui.label("set Maximized"); - ui.horizontal(|ui| { - if ui.selectable_label(window.maximized(), "true").clicked() { - window.set_maximized(true); - } - if ui.selectable_label(!window.maximized(), "false").clicked() { - window.set_maximized(false); - } - }); - ui.end_row(); - - ui.label("set Decorations"); - ui.horizontal(|ui| { - if ui.selectable_label(window.decorations(), "true").clicked() { - window.set_decorations(true); - } - if ui.selectable_label(!window.decorations(), "false").clicked() { - window.set_decorations(false); - } - }); - ui.end_row(); - - ui.label("set Resizable"); - ui.horizontal(|ui| { - if ui.selectable_label(window.resizable(), "true").clicked() { - window.set_resizable(true); - } - if ui.selectable_label(!window.resizable(), "false").clicked() { - window.set_resizable(false); - } - }); - ui.end_row(); - - ui.label("set Always on Top"); - ui.horizontal(|ui| { - if ui.selectable_label(window.always_on_top(), "true").clicked() { - window.set_always_on_top(true); - } - if ui.selectable_label(!window.always_on_top(), "false").clicked() { - window.set_always_on_top(false); - } - }); - ui.end_row(); - - match window.screen_size() { - Some(screen_size) => { - ui.label("set Min Inner Size - x"); - ui.add(Slider::new(&mut settings.min_inner_size.x, 100..=screen_size.x).text("")); - ui.end_row(); - - ui.label("set Min Inner Size - y"); - ui.add(Slider::new(&mut settings.min_inner_size.y, 100..=screen_size.y).text("")); - ui.end_row(); - - if settings.min_inner_size != window.min_inner_size() { - window.set_min_inner_size(settings.min_inner_size); - }; - - let mut inner_size = window.inner_size(); - - ui.label("set Inner Size - x"); - ui.add(Slider::new(&mut inner_size.x, 100..=screen_size.x).text("")); - ui.end_row(); - - ui.label("set Inner Size - y"); - ui.add(Slider::new(&mut inner_size.y, 100..=screen_size.y).text("")); - ui.end_row(); - - if window.inner_size() != inner_size { - window.set_inner_size(inner_size); - } - }, - None => { - ui.label("Cannot detect current monitor!"); - ui.end_row(); - } - }; - - // TODO: we should use work area for that - // https://docs.microsoft.com/en-gb/windows/win32/api/winuser/nf-winuser-systemparametersinfoa - ui.label("Move"); - Grid::new("window_movement_grid").show(ui, |ui| { - ui.label(""); - ui.vertical_centered(|ui| { - if ui.button("Top").clicked() { - move_window_top(&window); - }; - }); - ui.label(""); - ui.end_row(); - - ui.end_row(); - if ui.button("Left").clicked() { - move_window_left(&window); - }; - ui.vertical_centered(|ui| { - if ui.button("Center").clicked() { - move_window_center(&window); - }; - }); - if ui.button("Right").clicked() { - move_window_right(&window); - }; - ui.end_row(); - - ui.label(""); - ui.vertical_centered(|ui| { - if ui.button("Bottom").clicked() { - move_window_bottom(&window); - }; - }); - ui.label(""); - ui.end_row(); - }); - }); - }); - - CollapsingHeader::new("🚨 Match Finder") - .default_open(true) - .show(ui, |ui| { - ui.label("This is a demonstration of user attention type."); - ui.label("Click \"Search for game\" button, then switch to other program. App will request attention after 10 seconds (on windows - icon on taskbar starts blinking)."); - Grid::new("match_finder").show(ui, |ui| { - ui.label("User Attention Type"); - ComboBox::from_id_source("match_finder") - .selected_text(format!("{:?}", match_finder.attention_type)) - .show_ui(ui, |ui| { - for a_type in [UserAttentionType::Informational, UserAttentionType::Critical].iter() { - ui.selectable_value( - &mut match_finder.attention_type, - *a_type, - format!("{:?}", a_type) - ); - } - }); - ui.end_row(); - - match match_finder.state { - MatchFinderState::Idle => { - if ui.button("Search for game").clicked() { - match_finder.start_searching(); - } - }, - MatchFinderState::Searching(from) => { - ui.label("Searching..."); - ui.end_row(); - if let Some(from) = from { - ui.label(format!("Estimated time: {:.0}s", match_finder.estimated_time)); - ui.end_row(); - let search_time_secs = (frame.time() - from).as_secs_f32(); - ui.label(format!("Search time: {:.0}s", search_time_secs)); - ui.end_row(); - } - if ui.button("Stop searching").clicked() { - match_finder.stop_searching(); - } - }, - MatchFinderState::Ready(until) => { - ui.label("Game is ready!"); - ui.end_row(); - let remaining_time_secs = (until - frame.time()).as_secs_f32(); - ui.label(format!("You have {:.0}s to enter the game!", remaining_time_secs)); - ui.end_row(); - if ui.button("Enter the game").clicked() { - match_finder.stop_searching(); - } - }, - _ => {}, - } - }); - }); - - CollapsingHeader::new("📂 File handling") - .default_open(true) - .show(ui, |ui| { - ui.horizontal(|ui| { - let file_name = match &settings.opened_file { - Some(_) => file_path_to_string(&settings.opened_file), - None => String::from("[DROP FILE HERE]"), - }; - - if ui.button(file_name).hovered() { - if let Some(file) = egui.ctx.input().raw.dropped_files.first() { - settings.opened_file = file.path.clone(); - } - } - - if (ui.button("Open")).clicked() { - let mut dialog = FileDialog::open_file(settings.opened_file.clone()); - dialog.open(); - settings.open_file_dialog = Some(dialog); - } - - if let Some(dialog) = &mut settings.open_file_dialog { - if dialog.show(&egui.ctx).selected() { - if let Some(file) = dialog.path() { - settings.opened_file = Some(file); - } - } - } - - if (ui.button("❌")).clicked() { - settings.opened_file = None; - } - }); - - ui.label("Hovering files:"); - let hovered_files = &egui.ctx.input().raw.hovered_files; - if !hovered_files.is_empty() { - for file in hovered_files.iter() { - ui.label(format!("File: {}", file_path_to_string(&file.path))); - } - } else { - ui.label("Nothing"); - } - if (ui.button("Save")).clicked() { - let mut dialog = FileDialog::save_file(settings.opened_file.clone()); - dialog.open(); - settings.save_file_dialog = Some(dialog); - } - - if let Some(dialog) = &mut settings.save_file_dialog { - if dialog.show(&egui.ctx).selected() { - if let Some(file) = dialog.path() { - println!("Should save {:?}", file); - } - } - } - }); - }); - }); -} - -fn file_path_to_string(buf: &Option) -> String { - match buf { - Some(path) => path.display().to_string(), - None => String::from(""), - } -} - -pub fn startup(mut assets: Mut, mut settings: Mut, window: Const) { - settings.title = String::from(window.title()); - - // Load icons - for name in ["dotrix", "lowenware", "rustacean"].iter() { - settings - .icons - .insert(String::from(*name), assets.register::(name)); - assets.import(format!("assets/{}.png", name).as_str()); - } -} - -fn move_window_left(window: &Window) { - if let Ok(pos) = window.outer_position() { - window.set_outer_position(Vec2i::new(0, pos.y)); - } else { - println!("Cannot move window."); - } -} - -fn move_window_right(window: &Window) { - if let (Some(screen_size), Ok(pos)) = (window.screen_size(), window.outer_position()) { - let outer_size = window.outer_size(); - window.set_outer_position(Vec2i::new( - screen_size.x as i32 - outer_size.x as i32, - pos.y, - )); - } else { - println!("Cannot move window."); - } -} - -fn move_window_top(window: &Window) { - if let Ok(pos) = window.outer_position() { - window.set_outer_position(Vec2i::new(pos.x, 0)); - } else { - println!("Cannot move window."); - } -} - -fn move_window_bottom(window: &Window) { - if let (Some(screen_size), Ok(pos)) = (window.screen_size(), window.outer_position()) { - let outer_size = window.outer_size(); - window.set_outer_position(Vec2i::new( - pos.x, - screen_size.y as i32 - outer_size.y as i32, - )); - } else { - println!("Cannot move window."); - } -} - -fn move_window_center(window: &Window) { - if let Some(screen_size) = window.screen_size() { - let outer_size = window.outer_size(); - window.set_outer_position(Vec2i::new( - (screen_size.x - outer_size.x) as i32 / 2, - (screen_size.y - outer_size.y) as i32 / 2, - )); - } else { - println!("Cannot move window."); - } -} - -const CURSOR_ICONS: &[CursorIcon] = &[ - CursorIcon::Default, - CursorIcon::Crosshair, - CursorIcon::Alias, - CursorIcon::AllScroll, - CursorIcon::Arrow, - CursorIcon::Cell, - CursorIcon::ColResize, - CursorIcon::ContextMenu, - CursorIcon::Copy, - CursorIcon::EResize, - CursorIcon::EwResize, - CursorIcon::Grab, - CursorIcon::Grabbing, - CursorIcon::Hand, - CursorIcon::Help, - CursorIcon::Move, - CursorIcon::NResize, - CursorIcon::NeResize, - CursorIcon::NeswResize, - CursorIcon::NoDrop, - CursorIcon::NotAllowed, - CursorIcon::NsResize, - CursorIcon::NwResize, - CursorIcon::NwseResize, - CursorIcon::Progress, - CursorIcon::RowResize, - CursorIcon::SResize, - CursorIcon::SeResize, - CursorIcon::SwResize, - CursorIcon::Text, - CursorIcon::VerticalText, - CursorIcon::WResize, - CursorIcon::Wait, - CursorIcon::ZoomIn, - CursorIcon::ZoomOut, -]; - -#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] -pub enum WindowMode { - Fullscreen, - BorderlessFullscreen, - Windowed, -} - -const WINDOW_MODES: &[WindowMode] = &[ - WindowMode::Fullscreen, - WindowMode::BorderlessFullscreen, - WindowMode::Windowed, -]; diff --git a/demo/ui/main.rs b/demo/ui/main.rs new file mode 100644 index 00000000..bbd2b546 --- /dev/null +++ b/demo/ui/main.rs @@ -0,0 +1,147 @@ +use dotrix::ui::{ + top_left_panel_view, ButtonStyle, Charset, PanelStyle, ProcessUiInput, RenderOverlay, Spacing, + TextStyle, Ui, UiState, +}; +use dotrix::{log, Font}; + +struct UiDemoApp; + +impl Default for UiDemoApp { + fn default() -> Self { + Self + } +} + +struct BuildDemoUi { + ui_font: dotrix::Id, + click_count: u32, +} + +impl Default for BuildDemoUi { + fn default() -> Self { + Self { + ui_font: dotrix::Id::null(), + click_count: 0, + } + } +} + +impl dotrix::Task for BuildDemoUi { + type Context = ( + dotrix::Any, + dotrix::Ref, + dotrix::Ref, + ); + type Output = dotrix::ui::Overlay; + + fn run(&mut self, (frame, assets, ui_state): Self::Context) -> Self::Output { + if self.ui_font.is_null() { + self.ui_font = assets + .find("ui-font-regular-24") + .expect("ui font must be loaded"); + } + + let mut ui = Ui::new(frame.resolution, assets, ui_state); + + ui.view(top_left_panel_view(280.0, 190.0, 16.0), |ui| { + ui.panel( + PanelStyle { + background: dotrix::Color::rgba(0.08, 0.08, 0.12, 0.85), + corner_radius: 10.0, + padding: Spacing::uniform(12.0), + ..Default::default() + }, + |ui| { + ui.label( + TextStyle::default() + .font(self.ui_font) + .color(dotrix::Color::white()), + "Dotrix UI Demo", + ); + ui.spacer(8.0); + ui.label( + TextStyle::default() + .font(self.ui_font) + .color(dotrix::Color::rgba(0.8, 0.8, 0.9, 1.0)), + format!("FPS: {:.0}", frame.fps), + ); + ui.spacer(12.0); + let response = ui.button( + ButtonStyle { + text_style: TextStyle::default() + .font(self.ui_font) + .color(dotrix::Color::white()), + background: dotrix::Color::rgba(0.22, 0.24, 0.32, 0.95), + hover_background: dotrix::Color::rgba(0.32, 0.42, 0.62, 0.95), + pressed_background: dotrix::Color::rgba(0.16, 0.20, 0.30, 0.95), + hover_text_color: dotrix::Color::white(), + pressed_text_color: dotrix::Color::white(), + corner_radius: 6.0, + padding: Spacing::uniform(8.0), + }, + "Click me", + ); + if response.clicked { + self.click_count += 1; + } + ui.spacer(8.0); + ui.label( + TextStyle::default() + .font(self.ui_font) + .color(dotrix::Color::rgba(0.7, 0.7, 0.8, 1.0)), + format!("Clicks: {}", self.click_count), + ); + }, + ); + }); + + ui.finish() + } +} + +impl dotrix::Application for UiDemoApp { + fn app_name(&self) -> &str { + "Dotrix UI Demo" + } + + fn device_type_request(&self) -> Option { + Some(dotrix::DeviceType::Integrated) + } + + fn startup( + self, + scheduler: &dotrix::tasks::Scheduler, + display: &mut dotrix::graphics::Display, + ) { + log::info!("Starting Dotrix UI demo"); + + let mut assets = dotrix::Assets::default(); + assets.set( + Font::from_file( + "ui-font-regular-24", + "resources/fonts/Jura-Regular.ttf", + 24.0, + &Charset::latin_extended(), + ) + .expect("failed to load Inter font"), + ); + + scheduler.add_context(assets); + scheduler.add_context(UiState::default()); + + scheduler.add_task(BuildDemoUi::default()); + scheduler.add_task(ProcessUiInput::default()); + + let overlay_renderer = RenderOverlay::setup().create(display); + scheduler.add_task(overlay_renderer); + } +} + +fn main() { + dotrix::Log::default() + .level("dotrix", log::LevelFilter::Debug) + .level("*", log::LevelFilter::Info) + .subscribe(); + + dotrix::run(UiDemoApp::default()); +} diff --git a/schemas/indirect-drawing.drawio b/schemas/indirect-drawing.drawio deleted file mode 100644 index 3c77c649..00000000 --- a/schemas/indirect-drawing.drawio +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/features.rs b/src/features.rs index bf39c3b8..d5106ac2 100644 --- a/src/features.rs +++ b/src/features.rs @@ -3,3 +3,6 @@ pub mod skydome; #[cfg(feature = "terrain")] pub mod terrain; + +#[cfg(feature = "ui")] +pub mod ui; diff --git a/src/features/ui.rs b/src/features/ui.rs new file mode 100644 index 00000000..0e6da383 --- /dev/null +++ b/src/features/ui.rs @@ -0,0 +1,16 @@ +mod builder; +mod components; +mod font; +mod input; +mod label; +mod layout; +mod renderer; +mod view; + +pub use builder::*; +pub use font::*; +pub use input::*; +pub use label::*; +pub use layout::*; +pub use renderer::*; +pub use view::*; diff --git a/src/features/ui/builder.rs b/src/features/ui/builder.rs new file mode 100644 index 00000000..cf717456 --- /dev/null +++ b/src/features/ui/builder.rs @@ -0,0 +1,294 @@ +use crate::loaders::Assets; +use crate::{Color, Extent2D, Id, Ref}; + +use super::font::Font; +use super::input::{HitKind, HitRegion, UiState}; +use super::label::{layout_label, measure_text, text_height}; +use super::layout::{resolve_view, LayoutRect}; +use super::view::{PositionConstraint, SizeConstraint, View}; + +/// Per-frame UI draw list produced by an app build task. +#[derive(Debug, Default, Clone)] +pub struct Overlay { + pub commands: Vec, + pub hit_regions: Vec, +} + +impl Overlay { + pub fn is_empty(&self) -> bool { + self.commands.is_empty() + } +} + +/// A single UI primitive in the draw list. +#[derive(Debug, Clone, Copy)] +pub enum UiDrawCommand { + Rect { + rect: LayoutRect, + color: Color, + corner_radius: f32, + }, + Glyph { + rect: LayoutRect, + color: Color, + font: Id, + uv_min: [f32; 2], + uv_max: [f32; 2], + }, +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct Spacing { + pub top: f32, + pub right: f32, + pub bottom: f32, + pub left: f32, +} + +impl Spacing { + pub fn uniform(value: f32) -> Self { + Self { + top: value, + right: value, + bottom: value, + left: value, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct TextStyle { + pub font: Id, + pub color: Color, +} + +impl Default for TextStyle { + fn default() -> Self { + Self { + font: Id::null(), + color: Color::white(), + } + } +} + +impl TextStyle { + pub fn font(mut self, font: Id) -> Self { + self.font = font; + self + } + + pub fn color(mut self, color: Color) -> Self { + self.color = color; + self + } +} + +#[derive(Debug, Clone, Copy)] +pub struct PanelStyle { + pub background: Color, + pub corner_radius: f32, + pub padding: Spacing, + pub min_size: LayoutRect, + pub interactive: bool, +} + +impl Default for PanelStyle { + fn default() -> Self { + Self { + background: Color::rgba(0.0, 0.0, 0.0, 0.5), + corner_radius: 8.0, + padding: Spacing::uniform(8.0), + min_size: LayoutRect::default(), + interactive: false, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ButtonStyle { + pub text_style: TextStyle, + pub background: Color, + pub hover_background: Color, + pub pressed_background: Color, + pub hover_text_color: Color, + pub pressed_text_color: Color, + pub corner_radius: f32, + pub padding: Spacing, +} + +impl Default for ButtonStyle { + fn default() -> Self { + Self { + text_style: TextStyle::default(), + background: Color::rgba(0.2, 0.2, 0.2, 0.9), + hover_background: Color::rgba(0.3, 0.3, 0.3, 0.9), + pressed_background: Color::rgba(0.15, 0.15, 0.15, 0.9), + hover_text_color: Color::white(), + pressed_text_color: Color::white(), + corner_radius: 6.0, + padding: Spacing::uniform(8.0), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ButtonResponse { + pub hovered: bool, + pub pressed: bool, + pub clicked: bool, + pub region_index: usize, +} + +struct LayoutScope { + rect: LayoutRect, + cursor_y: f32, + region_index: Option, +} + +/// Immediate-mode UI builder. +pub struct Ui { + resolution: Extent2D, + pub(crate) assets: Ref, + pub(crate) ui_state: Ref, + pub(crate) commands: Vec, + hit_regions: Vec, + next_region_index: usize, + scopes: Vec, +} + +impl Ui { + pub fn new(resolution: Extent2D, assets: Ref, ui_state: Ref) -> Self { + let root = LayoutRect { + x: 0.0, + y: 0.0, + width: resolution.width as f32, + height: resolution.height as f32, + }; + Self { + resolution, + assets, + ui_state, + commands: Vec::new(), + hit_regions: Vec::new(), + next_region_index: 0, + scopes: vec![LayoutScope { + rect: root, + cursor_y: root.y, + region_index: None, + }], + } + } + + pub fn finish(self) -> Overlay { + Overlay { + commands: self.commands, + hit_regions: self.hit_regions, + } + } + + pub fn label(&mut self, style: TextStyle, text: impl AsRef) { + let text = text.as_ref(); + let (text_w, text_h) = { + let font = self + .assets + .get(style.font) + .expect("label font must exist in Assets"); + (measure_text(font, text), text_height(font, text)) + }; + let rect = self.allocate_vertical(LayoutRect { + x: 0.0, + y: 0.0, + width: text_w, + height: text_h, + }); + let font = self + .assets + .get(style.font) + .expect("label font must exist in Assets"); + layout_label( + &mut self.commands, + style.font, + font, + style.color, + rect, + text, + ); + } + + pub fn spacer(&mut self, size: f32) { + self.allocate_vertical(LayoutRect { + x: 0.0, + y: 0.0, + width: 0.0, + height: size, + }); + } + + pub fn view(&mut self, view: View, content: impl FnOnce(&mut Ui)) { + let parent = self.current_scope().rect; + let rect = resolve_view(&view, parent, self.resolution); + self.push_scope(rect, None); + content(self); + self.pop_scope(); + } + + pub(crate) fn push_hit_region(&mut self, rect: LayoutRect, kind: HitKind) -> usize { + let index = self.next_region_index; + self.next_region_index += 1; + self.hit_regions.push(HitRegion { index, rect, kind }); + index + } + + pub(crate) fn next_rect(&mut self, min_size: LayoutRect) -> LayoutRect { + if min_size.width > 0.0 && min_size.height > 0.0 { + self.allocate_vertical(min_size) + } else { + let scope = self.current_scope(); + let remaining = (scope.rect.y + scope.rect.height - scope.cursor_y).max(0.0); + LayoutRect { + x: scope.rect.x, + y: scope.cursor_y, + width: scope.rect.width, + height: remaining, + } + } + } + + pub(crate) fn allocate_vertical(&mut self, size: LayoutRect) -> LayoutRect { + let scope = self.scopes.last_mut().expect("layout scope"); + let rect = LayoutRect { + x: scope.rect.x, + y: scope.cursor_y, + width: size.width.max(scope.rect.width), + height: size.height, + }; + scope.cursor_y += size.height; + rect + } + + fn current_scope(&self) -> &LayoutScope { + self.scopes.last().expect("layout scope") + } + + pub(crate) fn push_scope(&mut self, rect: LayoutRect, region_index: Option) { + self.scopes.push(LayoutScope { + rect, + cursor_y: rect.y, + region_index, + }); + } + + pub(crate) fn pop_scope(&mut self) { + self.scopes.pop(); + } +} + +/// Convenience view for a top-left anchored panel. +pub fn top_left_panel_view(width: f32, height: f32, margin: f32) -> View { + View { + x: PositionConstraint::Pixel(margin), + y: PositionConstraint::Pixel(margin), + width: SizeConstraint::Pixel(width), + height: SizeConstraint::Pixel(height), + } +} diff --git a/src/features/ui/components/button.rs b/src/features/ui/components/button.rs new file mode 100644 index 00000000..7d1ac45f --- /dev/null +++ b/src/features/ui/components/button.rs @@ -0,0 +1,78 @@ +use super::super::label::{layout_label, measure_text, text_height}; +use super::super::layout::LayoutRect; +use super::super::{ButtonResponse, ButtonStyle, Ui, UiDrawCommand}; + +impl Ui { + pub fn button(&mut self, style: ButtonStyle, label: &str) -> ButtonResponse { + let (text_w, text_h) = { + let font = self + .assets + .get(style.text_style.font) + .expect("button font must exist in Assets"); + (measure_text(font, label), text_height(font, label)) + }; + let width = text_w + style.padding.left + style.padding.right; + let height = text_h + style.padding.top + style.padding.bottom; + + let rect = self.allocate_vertical(LayoutRect { + x: 0.0, + y: 0.0, + width, + height, + }); + + let region_index = self.push_hit_region(rect, super::super::HitKind::Button); + let hovered = self.ui_state.hovered_region == Some(region_index); + let pressed = self.ui_state.pressed_region == Some(region_index); + let clicked = self.ui_state.clicked_region == Some(region_index); + + let (bg, fg) = if pressed { + (style.pressed_background, style.pressed_text_color) + } else if hovered { + (style.hover_background, style.hover_text_color) + } else { + (style.background, style.text_style.color) + }; + + self.commands.push(UiDrawCommand::Rect { + rect, + color: bg, + corner_radius: style.corner_radius, + }); + + let text_origin = LayoutRect { + x: rect.x + style.padding.left, + y: rect.y + style.padding.top, + width: text_w, + height: text_h, + }; + let font = self + .assets + .get(style.text_style.font) + .expect("button font must exist in Assets"); + layout_label( + &mut self.commands, + style.text_style.font, + font, + fg, + text_origin, + label, + ); + + ButtonResponse { + hovered, + pressed, + clicked, + region_index, + } + } +} + +impl ButtonStyle { + pub fn new(text_style: super::super::TextStyle) -> Self { + Self { + text_style, + ..Default::default() + } + } +} diff --git a/src/features/ui/components/mod.rs b/src/features/ui/components/mod.rs new file mode 100644 index 00000000..d8913532 --- /dev/null +++ b/src/features/ui/components/mod.rs @@ -0,0 +1,2 @@ +mod button; +mod panel; diff --git a/src/features/ui/components/panel.rs b/src/features/ui/components/panel.rs new file mode 100644 index 00000000..7d463e1f --- /dev/null +++ b/src/features/ui/components/panel.rs @@ -0,0 +1,38 @@ +use super::super::layout::LayoutRect; +use super::super::{PanelStyle, Ui, UiDrawCommand}; + +impl Ui { + pub fn panel(&mut self, style: PanelStyle, content: impl FnOnce(&mut Ui)) { + let rect = self.next_rect(style.min_size); + let region_index = if style.interactive { + Some(self.push_hit_region(rect, super::super::HitKind::Panel)) + } else { + None + }; + + self.commands.push(UiDrawCommand::Rect { + rect, + color: style.background, + corner_radius: style.corner_radius, + }); + + let inner = rect.inner(&style.padding); + self.push_scope(inner, region_index); + content(self); + self.pop_scope(); + } +} + +impl PanelStyle { + pub fn sized(width: f32, height: f32) -> Self { + Self { + min_size: LayoutRect { + x: 0.0, + y: 0.0, + width, + height, + }, + ..Default::default() + } + } +} diff --git a/src/features/ui/font.rs b/src/features/ui/font.rs new file mode 100644 index 00000000..21781d1d --- /dev/null +++ b/src/features/ui/font.rs @@ -0,0 +1,380 @@ +use std::collections::HashMap; +use std::io::{Cursor, Read, Write}; +use std::ops::Range; +use std::path::Path; + +use crate::loaders::Asset; +use crate::log; + +/// Baked font binary format version byte. +pub const BAKED_FONT_VERSION: u8 = 1; + +/// Unicode code-point ranges for font rasterization. +#[derive(Debug, Clone)] +pub struct Charset { + ranges: Vec>, +} + +impl Charset { + /// ASCII + Latin-1 Supplement + Latin Extended-A (European diacritics). + pub fn latin_extended() -> Self { + Self { + ranges: vec![0x0020..0x0080, 0x00A0..0x0100, 0x0100..0x0180], + } + } + + pub fn codepoints(&self) -> Vec { + let mut chars = Vec::new(); + for range in &self.ranges { + for code in range.clone() { + if let Some(ch) = char::from_u32(code) { + chars.push(ch); + } + } + } + chars + } +} + +/// Per-glyph metrics and atlas UVs. +#[derive(Debug, Clone, Copy)] +pub struct GlyphMetrics { + pub advance: f32, + pub bearing_x: f32, + pub bearing_y: f32, + pub width: f32, + pub height: f32, + pub uv_min: [f32; 2], + pub uv_max: [f32; 2], +} + +/// Line metrics for text layout. +#[derive(Debug, Clone, Copy)] +pub struct LineMetrics { + pub ascent: f32, + pub descent: f32, + pub gap: f32, + pub line_size: f32, +} + +/// Font asset — one pixel size per asset instance. +pub struct Font { + name: String, + size_px: f32, + atlas: Vec, + atlas_width: u32, + atlas_height: u32, + line_metrics: LineMetrics, + glyphs: HashMap, +} + +impl Asset for Font { + fn name(&self) -> &str { + &self.name + } +} + +impl Font { + pub fn size_px(&self) -> f32 { + self.size_px + } + + pub fn line_metrics(&self) -> &LineMetrics { + &self.line_metrics + } + + pub fn atlas_width(&self) -> u32 { + self.atlas_width + } + + pub fn atlas_height(&self) -> u32 { + self.atlas_height + } + + pub fn atlas_pixels(&self) -> &[u8] { + &self.atlas + } + + pub fn glyph(&self, ch: char) -> Option<&GlyphMetrics> { + self.glyphs.get(&ch) + } + + /// Load a font from a baked binary produced by [`bake_ttf_to_bytes`] or mythstic `build.rs`. + pub fn from_baked_bin(name: impl Into, bytes: &[u8]) -> Result { + let mut cursor = Cursor::new(bytes); + + let mut version = [0u8; 1]; + cursor.read_exact(&mut version).map_err(|e| e.to_string())?; + if version[0] != BAKED_FONT_VERSION { + return Err(format!( + "unsupported baked font version {} (expected {})", + version[0], BAKED_FONT_VERSION + )); + } + + let size_px = read_f32(&mut cursor)?; + let atlas_width = read_u32(&mut cursor)?; + let atlas_height = read_u32(&mut cursor)?; + let ascent = read_f32(&mut cursor)?; + let descent = read_f32(&mut cursor)?; + let gap = read_f32(&mut cursor)?; + let line_size = read_f32(&mut cursor)?; + let glyph_count = read_u32(&mut cursor)? as usize; + + let atlas_len = (atlas_width * atlas_height) as usize; + let mut atlas = vec![0u8; atlas_len]; + cursor.read_exact(&mut atlas).map_err(|e| e.to_string())?; + + let mut glyphs = HashMap::with_capacity(glyph_count); + for _ in 0..glyph_count { + let codepoint = read_u32(&mut cursor)?; + let ch = char::from_u32(codepoint) + .ok_or_else(|| format!("invalid codepoint {codepoint}"))?; + let metrics = GlyphMetrics { + advance: read_f32(&mut cursor)?, + bearing_x: read_f32(&mut cursor)?, + bearing_y: read_f32(&mut cursor)?, + width: read_f32(&mut cursor)?, + height: read_f32(&mut cursor)?, + uv_min: [read_f32(&mut cursor)?, read_f32(&mut cursor)?], + uv_max: [read_f32(&mut cursor)?, read_f32(&mut cursor)?], + }; + glyphs.insert(ch, metrics); + } + + Ok(Self { + name: name.into(), + size_px, + atlas, + atlas_width, + atlas_height, + line_metrics: LineMetrics { + ascent, + descent, + gap, + line_size, + }, + glyphs, + }) + } + + /// Rasterize a TTF/OTF file at runtime (requires `fontdue` feature). + #[cfg(feature = "fontdue")] + pub fn from_file( + name: impl Into, + path: impl AsRef, + size_px: f32, + charset: &Charset, + ) -> Result { + let bytes = std::fs::read(path.as_ref()).map_err(|e| e.to_string())?; + Self::from_bytes(name, &bytes, size_px, charset) + } + + /// Rasterize font bytes at runtime (requires `fontdue` feature). + #[cfg(feature = "fontdue")] + pub fn from_bytes( + name: impl Into, + bytes: &[u8], + size_px: f32, + charset: &Charset, + ) -> Result { + let baked = bake_ttf_to_bytes(bytes, size_px, charset)?; + Self::from_baked_bin(name, &baked) + } +} + +/// Bake a TTF/OTF into the on-disk `.bin` layout (requires `fontdue` feature). +#[cfg(feature = "fontdue")] +pub fn bake_ttf_to_bytes(bytes: &[u8], size_px: f32, charset: &Charset) -> Result, String> { + let font = fontdue::Font::from_bytes(bytes, fontdue::FontSettings::default()) + .map_err(|e| e.to_string())?; + + let line_metrics = font + .horizontal_line_metrics(size_px) + .map(|m| LineMetrics { + ascent: m.ascent, + descent: m.descent, + gap: m.line_gap, + line_size: m.new_line_size, + }) + .ok_or_else(|| "font missing horizontal line metrics".to_string())?; + + let codepoints = charset.codepoints(); + let mut glyph_data: Vec<(char, fontdue::Metrics, Vec)> = + Vec::with_capacity(codepoints.len()); + + for ch in codepoints { + let (metrics, bitmap) = font.rasterize(ch, size_px); + glyph_data.push((ch, metrics, bitmap)); + } + + let spacing = 2usize; + let (atlas_width, atlas_height) = estimate_atlas_size(&glyph_data, spacing); + let mut atlas = vec![0u8; atlas_width * atlas_height]; + let mut glyphs = HashMap::with_capacity(glyph_data.len()); + + let mut x = spacing; + let mut y = spacing; + let mut row_height = 0usize; + + for (ch, metrics, bitmap) in glyph_data { + let gw = metrics.width.max(0) as usize; + let gh = metrics.height.max(0) as usize; + + if gw > 0 && gh > 0 { + if x + gw + spacing > atlas_width { + x = spacing; + y += row_height + spacing; + row_height = 0; + } + if y + gh + spacing > atlas_height { + return Err("font atlas overflow during bake".to_string()); + } + + for row in 0..gh { + let dst = (y + row) * atlas_width + x; + let src = row * gw; + atlas[dst..dst + gw].copy_from_slice(&bitmap[src..src + gw]); + } + + let u0 = x as f32 / atlas_width as f32; + let v0 = y as f32 / atlas_height as f32; + let u1 = (x + gw) as f32 / atlas_width as f32; + let v1 = (y + gh) as f32 / atlas_height as f32; + + glyphs.insert( + ch, + GlyphMetrics { + advance: metrics.advance_width, + bearing_x: metrics.xmin as f32, + bearing_y: metrics.ymin as f32, + width: gw as f32, + height: gh as f32, + uv_min: [u0, v0], + uv_max: [u1, v1], + }, + ); + + x += gw + spacing; + row_height = row_height.max(gh); + } else { + glyphs.insert( + ch, + GlyphMetrics { + advance: metrics.advance_width, + bearing_x: metrics.xmin as f32, + bearing_y: metrics.ymin as f32, + width: 0.0, + height: 0.0, + uv_min: [0.0, 0.0], + uv_max: [0.0, 0.0], + }, + ); + } + } + + write_baked_bytes( + size_px, + atlas_width as u32, + atlas_height as u32, + &line_metrics, + &atlas, + &glyphs, + ) +} + +#[cfg(feature = "fontdue")] +fn estimate_atlas_size( + glyphs: &[(char, fontdue::Metrics, Vec)], + spacing: usize, +) -> (usize, usize) { + let area: f32 = glyphs + .iter() + .map(|(_, m, _)| { + (m.width.max(1) as f32 + spacing as f32) * (m.height.max(1) as f32 + spacing as f32) + }) + .sum(); + let side = (area.sqrt() * 1.3).ceil() as usize; + (side.max(64), side.max(64)) +} + +pub(crate) fn write_baked_bytes( + size_px: f32, + atlas_width: u32, + atlas_height: u32, + line_metrics: &LineMetrics, + atlas: &[u8], + glyphs: &HashMap, +) -> Result, String> { + let mut out = Vec::new(); + out.write_all(&[BAKED_FONT_VERSION]) + .map_err(|e| e.to_string())?; + write_f32(&mut out, size_px)?; + write_u32(&mut out, atlas_width)?; + write_u32(&mut out, atlas_height)?; + write_f32(&mut out, line_metrics.ascent)?; + write_f32(&mut out, line_metrics.descent)?; + write_f32(&mut out, line_metrics.gap)?; + write_f32(&mut out, line_metrics.line_size)?; + write_u32(&mut out, glyphs.len() as u32)?; + out.write_all(atlas).map_err(|e| e.to_string())?; + + let mut sorted: Vec<_> = glyphs.iter().collect(); + sorted.sort_by_key(|(ch, _)| **ch as u32); + for (ch, metrics) in sorted { + write_u32(&mut out, *ch as u32)?; + write_f32(&mut out, metrics.advance)?; + write_f32(&mut out, metrics.bearing_x)?; + write_f32(&mut out, metrics.bearing_y)?; + write_f32(&mut out, metrics.width)?; + write_f32(&mut out, metrics.height)?; + write_f32(&mut out, metrics.uv_min[0])?; + write_f32(&mut out, metrics.uv_min[1])?; + write_f32(&mut out, metrics.uv_max[0])?; + write_f32(&mut out, metrics.uv_max[1])?; + } + + Ok(out) +} + +fn read_u32(cursor: &mut Cursor<&[u8]>) -> Result { + let mut buf = [0u8; 4]; + cursor.read_exact(&mut buf).map_err(|e| e.to_string())?; + Ok(u32::from_le_bytes(buf)) +} + +fn read_f32(cursor: &mut Cursor<&[u8]>) -> Result { + let mut buf = [0u8; 4]; + cursor.read_exact(&mut buf).map_err(|e| e.to_string())?; + Ok(f32::from_le_bytes(buf)) +} + +fn write_u32(out: &mut Vec, value: u32) -> Result<(), String> { + out.write_all(&value.to_le_bytes()) + .map_err(|e| e.to_string()) +} + +fn write_f32(out: &mut Vec, value: f32) -> Result<(), String> { + out.write_all(&value.to_le_bytes()) + .map_err(|e| e.to_string()) +} + +/// Bake a font file to a `.bin` on disk (requires `fontdue` feature). +#[cfg(feature = "fontdue")] +pub fn bake_ttf_file_to_path( + input: impl AsRef, + output: impl AsRef, + size_px: f32, + charset: &Charset, +) -> Result<(), String> { + let bytes = std::fs::read(input.as_ref()).map_err(|e| e.to_string())?; + let baked = bake_ttf_to_bytes(&bytes, size_px, charset)?; + std::fs::write(output.as_ref(), &baked).map_err(|e| e.to_string())?; + log::info!( + "baked font `{}` -> `{}` ({} bytes)", + input.as_ref().display(), + output.as_ref().display(), + baked.len() + ); + Ok(()) +} diff --git a/src/features/ui/input.rs b/src/features/ui/input.rs new file mode 100644 index 00000000..6719c6be --- /dev/null +++ b/src/features/ui/input.rs @@ -0,0 +1,76 @@ +use crate::window::event::{Button, Event}; +use crate::{Any, Input, Mut, Task}; + +use super::Overlay; + +use super::layout::LayoutRect; + +/// Interaction state from the previous frame (read during build, written during input). +#[derive(Debug, Default, Clone)] +pub struct UiState { + pub hovered_region: Option, + pub pressed_region: Option, + pub clicked_region: Option, +} + +/// Hit-test target appended during UI build. +#[derive(Debug, Clone, Copy)] +pub struct HitRegion { + pub index: usize, + pub rect: LayoutRect, + pub kind: HitKind, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HitKind { + Panel, + Button, +} + +/// Marker output so [`ProcessUiInput`] does not share the `()` output channel with other tasks. +#[derive(Debug, Default, Clone, Copy)] +pub struct UiInputFrame; + +/// Hit-tests the current frame overlay and updates [`UiState`]. +#[derive(Default)] +pub struct ProcessUiInput; + +impl Task for ProcessUiInput { + type Context = (Any, Any, Mut); + type Output = UiInputFrame; + + fn run(&mut self, (overlay, input, mut ui_state): Self::Context) -> Self::Output { + let mx = input.mouse_position.horizontal as f32; + let my = input.mouse_position.vertical as f32; + + let hovered = overlay + .hit_regions + .iter() + .rev() + .find(|region| region.rect.contains(mx, my)) + .map(|region| region.index); + + let left_down = input.hold.contains_key(&Button::MouseLeft); + let left_released = input.events.iter().any(|event| { + matches!( + event, + Event::ButtonRelease { + button: Button::MouseLeft + } + ) + }); + + // Count a click when the button that was pressed is released (even if the + // cursor moved slightly off the widget before release). + let clicked = if left_released { + ui_state.pressed_region + } else { + None + }; + + ui_state.hovered_region = hovered; + ui_state.pressed_region = if left_down { hovered } else { None }; + ui_state.clicked_region = clicked; + UiInputFrame + } +} diff --git a/src/features/ui/label.rs b/src/features/ui/label.rs new file mode 100644 index 00000000..d391a7c8 --- /dev/null +++ b/src/features/ui/label.rs @@ -0,0 +1,98 @@ +use crate::{Color, Id}; + +use super::font::Font; +use super::layout::LayoutRect; +use super::UiDrawCommand; + +/// Ink bounds of a single-line string relative to its baseline. +#[derive(Debug, Clone, Copy)] +pub struct TextExtents { + pub width: f32, + /// Distance from the baseline up to the highest ink pixel. + pub above_baseline: f32, + /// Distance from the baseline down to the lowest ink pixel. + pub below_baseline: f32, +} + +impl TextExtents { + pub fn ink_height(&self) -> f32 { + self.above_baseline + self.below_baseline + } +} + +/// Measure ink bounds for a single-line string. +pub fn measure_text_extents(font: &Font, text: &str) -> TextExtents { + let mut width = 0.0; + let mut above_baseline = 0.0f32; + let mut below_baseline = 0.0f32; + + for ch in text.chars() { + if let Some(glyph) = font.glyph(ch) { + // fontdue ymin is the bitmap bottom-edge offset in y-up space. + let top = -glyph.bearing_y - glyph.height; + let bottom = -glyph.bearing_y; + above_baseline = above_baseline.max(-top); + below_baseline = below_baseline.max(bottom); + width += glyph.advance; + } + } + + if width == 0.0 { + let metrics = font.line_metrics(); + above_baseline = metrics.ascent; + below_baseline = -metrics.descent; + } + + TextExtents { + width, + above_baseline, + below_baseline, + } +} + +/// Measure the pixel width of a single-line string. +pub fn measure_text(font: &Font, text: &str) -> f32 { + measure_text_extents(font, text).width +} + +/// Measure the pixel height of a single-line string's ink. +pub fn text_height(font: &Font, text: &str) -> f32 { + measure_text_extents(font, text).ink_height() +} + +/// Emit glyph draw commands for a single-line label. +pub fn layout_label( + commands: &mut Vec, + font_id: Id, + font: &Font, + color: Color, + origin: LayoutRect, + text: &str, +) { + let extents = measure_text_extents(font, text); + // Bottom-align ink within the allocated rect. + let baseline_y = origin.y + origin.height - extents.below_baseline; + let mut cursor_x = origin.x; + + for ch in text.chars() { + if let Some(glyph) = font.glyph(ch) { + if glyph.width > 0.0 && glyph.height > 0.0 { + let x = cursor_x + glyph.bearing_x; + let y = baseline_y - glyph.bearing_y - glyph.height; + commands.push(UiDrawCommand::Glyph { + rect: LayoutRect { + x, + y, + width: glyph.width, + height: glyph.height, + }, + color, + font: font_id, + uv_min: glyph.uv_min, + uv_max: glyph.uv_max, + }); + } + cursor_x += glyph.advance; + } + } +} diff --git a/src/features/ui/layout.rs b/src/features/ui/layout.rs new file mode 100644 index 00000000..7aa3ce9a --- /dev/null +++ b/src/features/ui/layout.rs @@ -0,0 +1,88 @@ +use crate::Extent2D; + +use super::view::{PositionConstraint, SizeConstraint, View}; + +/// Pixel-space axis-aligned rectangle. +#[derive(Debug, Clone, Copy, Default)] +pub struct LayoutRect { + pub x: f32, + pub y: f32, + pub width: f32, + pub height: f32, +} + +impl LayoutRect { + pub fn contains(&self, px: f32, py: f32) -> bool { + px >= self.x && py >= self.y && px < self.x + self.width && py < self.y + self.height + } + + pub fn inner(&self, padding: &super::Spacing) -> Self { + Self { + x: self.x + padding.left, + y: self.y + padding.top, + width: (self.width - padding.left - padding.right).max(0.0), + height: (self.height - padding.top - padding.bottom).max(0.0), + } + } +} + +pub fn resolve_view(view: &View, parent: LayoutRect, resolution: Extent2D) -> LayoutRect { + let screen_w = resolution.width as f32; + let screen_h = resolution.height as f32; + let width = resolve_width(&view.width, parent.width, screen_w); + let height = resolve_height(&view.height, parent.height, screen_h); + + let x = resolve_x(&view.x, parent, width, screen_w); + let y = resolve_y(&view.y, parent, height, screen_h); + + LayoutRect { + x, + y, + width, + height, + } +} + +fn resolve_width(constraint: &SizeConstraint, parent_width: f32, screen_width: f32) -> f32 { + let value = match constraint { + SizeConstraint::Pixel(value) => *value, + SizeConstraint::Percent(fraction) => parent_width * fraction, + }; + value.min(screen_width).max(0.0) +} + +fn resolve_height(constraint: &SizeConstraint, parent_height: f32, screen_height: f32) -> f32 { + let value = match constraint { + SizeConstraint::Pixel(value) => *value, + SizeConstraint::Percent(fraction) => parent_height * fraction, + }; + value.min(screen_height).max(0.0) +} + +fn resolve_x( + constraint: &PositionConstraint, + parent: LayoutRect, + width: f32, + screen_width: f32, +) -> f32 { + match constraint { + PositionConstraint::Pixel(offset) => parent.x + offset, + PositionConstraint::Percent(fraction) => parent.x + parent.width * fraction, + PositionConstraint::Center => parent.x + (parent.width - width) * 0.5, + } + .clamp(0.0, screen_width.max(width)) +} + +fn resolve_y( + constraint: &PositionConstraint, + parent: LayoutRect, + height: f32, + screen_height: f32, +) -> f32 { + match constraint { + PositionConstraint::Pixel(offset) => parent.y + offset, + PositionConstraint::Percent(fraction) => parent.y + parent.height * fraction, + PositionConstraint::Center => parent.y + (parent.height - height) * 0.5, + } + .clamp(0.0, screen_height.max(height)) +} diff --git a/src/features/ui/renderer.rs b/src/features/ui/renderer.rs new file mode 100644 index 00000000..cf5c7dff --- /dev/null +++ b/src/features/ui/renderer.rs @@ -0,0 +1,921 @@ +use std::collections::HashMap; +use std::io::Cursor; + +use ash::vk::Handle; +use bytemuck::{Pod, Zeroable}; + +use crate::graphics::{vk, Buffer, CommandRecorder, Gpu, RenderSubmit}; +use crate::loaders::Assets; +use crate::models::Color; +use crate::utils::Id; +use crate::{Any, Display, Extent2D, Frame, Ref, Task}; + +use super::builder::{Overlay, UiDrawCommand}; +use super::font::Font; + +/// Maximum UI instances (rects + glyphs) per frame. +pub const UI_MAX_INSTANCES: usize = 8192; + +/// Maximum font atlas layers in the texture array. +pub const UI_MAX_FONT_LAYERS: u32 = 16; + +/// GPU font texture dimensions (atlas pixels are uploaded to the top-left corner). +pub const UI_FONT_TEXTURE_SIZE: u32 = 512; + +const UI_KIND_RECT: f32 = 0.0; +const UI_KIND_GLYPH: f32 = 1.0; + +#[repr(C)] +#[derive(Clone, Copy, Default, Pod, Zeroable)] +struct UiGlobals { + resolution: [f32; 2], + _padding: [f32; 2], +} + +#[repr(C)] +#[derive(Clone, Copy, Default, Pod, Zeroable)] +pub struct UiInstance { + pub rect: [f32; 4], + pub color: [f32; 4], + pub params: [f32; 4], + pub uv: [f32; 4], +} + +const QUAD_VERTICES: [[f32; 2]; 6] = [ + [0.0, 0.0], + [1.0, 0.0], + [1.0, 1.0], + [0.0, 0.0], + [1.0, 1.0], + [0.0, 1.0], +]; + +pub struct RenderOverlay { + gpu: Gpu, + surface_version: u64, + wait_for: Vec, + command_pool: vk::CommandPool, + command_buffer_setup: vk::CommandBuffer, + command_buffer_setup_reuse_fence: vk::Fence, + vertex_buffer: Buffer, + instance_buffer: Buffer, + indirect_buffer: Buffer, + globals_buffer: Buffer, + font_image: vk::Image, + font_image_memory: vk::DeviceMemory, + font_image_view: vk::ImageView, + font_sampler: vk::Sampler, + font_staging_buffer: Buffer, + font_layer_index: HashMap, u32>, + font_layer_count: u32, + descriptor_pool: vk::DescriptorPool, + desc_set_layouts: [vk::DescriptorSetLayout; 1], + descriptor_sets: Vec, + pipeline_layout: vk::PipelineLayout, + pipeline: vk::Pipeline, + shader_vertex: vk::ShaderModule, + shader_fragment: vk::ShaderModule, + instance_count: u32, +} + +impl Drop for RenderOverlay { + fn drop(&mut self) { + unsafe { + self.gpu.device_wait_idle().unwrap(); + if !self.pipeline.is_null() { + self.gpu.destroy_pipeline(self.pipeline); + } + self.gpu.destroy_pipeline_layout(self.pipeline_layout); + self.gpu.destroy_shader_module(self.shader_vertex); + self.gpu.destroy_shader_module(self.shader_fragment); + self.vertex_buffer.free_memory_and_destroy(&self.gpu); + self.instance_buffer.free_memory_and_destroy(&self.gpu); + self.indirect_buffer.free_memory_and_destroy(&self.gpu); + self.globals_buffer.free_memory_and_destroy(&self.gpu); + self.font_staging_buffer.free_memory_and_destroy(&self.gpu); + self.gpu.destroy_sampler(self.font_sampler); + self.gpu.destroy_image_view(self.font_image_view); + self.gpu.free_memory(self.font_image_memory); + self.gpu.destroy_image(self.font_image); + for layout in &self.desc_set_layouts { + self.gpu.destroy_descriptor_set_layout(*layout); + } + self.gpu.destroy_descriptor_pool(self.descriptor_pool); + self.gpu.destroy_command_pool(self.command_pool); + self.gpu + .destroy_fence(self.command_buffer_setup_reuse_fence); + } + } +} + +impl Task for RenderOverlay { + type Context = (Any, Any, Ref, Ref); + type Output = RenderSubmit; + + fn run(&mut self, (overlay, frame, display, assets): Self::Context) -> Self::Output { + if let Some(surface_version) = display.surface_changed(self.surface_version) { + unsafe { + self.gpu.device_wait_idle().unwrap(); + if self.pipeline.is_null() { + self.pipeline = self.create_graphics_pipeline( + display.render_pass(), + display.surface_resolution(), + ); + } + } + self.surface_version = surface_version; + } + + if overlay.is_empty() { + return RenderSubmit::skip::(self.wait_for.as_slice()); + } + + self.instance_count = self.build_instances(&overlay, &assets); + + if self.instance_count == 0 { + return RenderSubmit::skip::(self.wait_for.as_slice()); + } + + let globals = [UiGlobals { + resolution: [ + frame.resolution.width as f32, + frame.resolution.height as f32, + ], + _padding: [0.0, 0.0], + }]; + unsafe { + self.globals_buffer + .map_and_write_to_device_memory(&self.gpu, 0, &globals); + } + + let indirect = [vk::DrawIndirectCommand { + vertex_count: 6, + instance_count: self.instance_count, + first_vertex: 0, + first_instance: 0, + }]; + unsafe { + self.indirect_buffer + .map_and_write_to_device_memory(&self.gpu, 0, &indirect); + } + + let recorder = Recorder { + resolution: frame.resolution, + pipeline_layout: self.pipeline_layout, + pipeline: self.pipeline, + descriptor_sets: self.descriptor_sets.clone(), + vertex_buffer: self.vertex_buffer.handle, + indirect_buffer: self.indirect_buffer.handle, + }; + + RenderSubmit::new::(Box::new(recorder), &self.wait_for) + } +} + +impl RenderOverlay { + pub fn setup() -> RenderOverlaySetup { + RenderOverlaySetup::default() + } + + pub fn new(display: &mut Display, setup: RenderOverlaySetup) -> Self { + let gpu = display.gpu(); + + let pool_create_info = vk::CommandPoolCreateInfo::default() + .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER) + .queue_family_index(gpu.queue_family_index()); + let command_pool = unsafe { + gpu.create_command_pool(&pool_create_info) + .expect("Failed to create UI command pool") + }; + + let command_buffer_allocate_info = vk::CommandBufferAllocateInfo::default() + .command_buffer_count(1) + .command_pool(command_pool) + .level(vk::CommandBufferLevel::PRIMARY); + let command_buffer_setup = unsafe { + gpu.allocate_command_buffers(&command_buffer_allocate_info) + .into() + }; + + let fence_create_info = + vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED); + let command_buffer_setup_reuse_fence = unsafe { gpu.create_fence(&fence_create_info) }; + + let vertex_buffer = unsafe { + create_vertex_buffer( + &gpu, + (QUAD_VERTICES.len() * std::mem::size_of::<[f32; 2]>()) as u64, + ) + .expect("Could not allocate UI vertex buffer") + }; + unsafe { + vertex_buffer.map_and_write_to_device_memory(&gpu, 0, &QUAD_VERTICES); + } + + let instance_buffer = unsafe { + create_storage_buffer( + &gpu, + UI_MAX_INSTANCES as u64 * std::mem::size_of::() as u64, + ) + .expect("Could not allocate UI instance buffer") + }; + + let indirect_buffer = unsafe { + create_indirect_buffer(&gpu, std::mem::size_of::() as u64) + .expect("Could not allocate UI indirect buffer") + }; + + let globals_buffer = unsafe { + create_uniform_buffer(&gpu, std::mem::size_of::() as u64) + .expect("Could not allocate UI globals buffer") + }; + + let font_layer_count = UI_MAX_FONT_LAYERS; + let font_image_create_info = vk::ImageCreateInfo { + image_type: vk::ImageType::TYPE_2D, + format: vk::Format::R8_UNORM, + extent: vk::Extent3D { + width: UI_FONT_TEXTURE_SIZE, + height: UI_FONT_TEXTURE_SIZE, + depth: 1, + }, + mip_levels: 1, + array_layers: font_layer_count, + samples: vk::SampleCountFlags::TYPE_1, + tiling: vk::ImageTiling::OPTIMAL, + usage: vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::SAMPLED, + sharing_mode: vk::SharingMode::EXCLUSIVE, + ..Default::default() + }; + let font_image = unsafe { + gpu.create_image(&font_image_create_info) + .expect("Failed to create UI font image") + }; + let font_image_memory_req = unsafe { gpu.get_image_memory_requirements(font_image) }; + let font_image_memory_index = gpu + .find_memory_type_index( + &font_image_memory_req, + vk::MemoryPropertyFlags::DEVICE_LOCAL, + ) + .expect("Unable to find memory for UI font image"); + let font_image_memory = unsafe { + gpu.allocate_memory(&vk::MemoryAllocateInfo { + allocation_size: font_image_memory_req.size, + memory_type_index: font_image_memory_index, + ..Default::default() + }) + .expect("Failed to allocate UI font image memory") + }; + unsafe { + gpu.bind_image_memory(font_image, font_image_memory, 0) + .expect("Failed to bind UI font image memory"); + } + + let font_image_view = unsafe { + gpu.create_image_view(&vk::ImageViewCreateInfo { + view_type: vk::ImageViewType::TYPE_2D_ARRAY, + format: vk::Format::R8_UNORM, + subresource_range: vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + level_count: 1, + layer_count: font_layer_count, + ..Default::default() + }, + image: font_image, + ..Default::default() + }) + .expect("Failed to create UI font image view") + }; + + let font_sampler = unsafe { + gpu.create_sampler(&vk::SamplerCreateInfo { + mag_filter: vk::Filter::LINEAR, + min_filter: vk::Filter::LINEAR, + address_mode_u: vk::SamplerAddressMode::CLAMP_TO_EDGE, + address_mode_v: vk::SamplerAddressMode::CLAMP_TO_EDGE, + address_mode_w: vk::SamplerAddressMode::CLAMP_TO_EDGE, + ..Default::default() + }) + .expect("Failed to create UI font sampler") + }; + + let font_staging_buffer = unsafe { + create_transfer_buffer(&gpu, (UI_FONT_TEXTURE_SIZE * UI_FONT_TEXTURE_SIZE) as u64) + .expect("Could not allocate UI font staging buffer") + }; + + let descriptor_sizes = [ + vk::DescriptorPoolSize { + ty: vk::DescriptorType::UNIFORM_BUFFER, + descriptor_count: 1, + }, + vk::DescriptorPoolSize { + ty: vk::DescriptorType::STORAGE_BUFFER, + descriptor_count: 1, + }, + vk::DescriptorPoolSize { + ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER, + descriptor_count: 1, + }, + ]; + let descriptor_pool = unsafe { + gpu.create_descriptor_pool( + &vk::DescriptorPoolCreateInfo::default() + .pool_sizes(&descriptor_sizes) + .max_sets(1), + ) + .unwrap() + }; + + let desc_layout_bindings = [ + vk::DescriptorSetLayoutBinding { + binding: 0, + descriptor_type: vk::DescriptorType::UNIFORM_BUFFER, + descriptor_count: 1, + stage_flags: vk::ShaderStageFlags::VERTEX, + ..Default::default() + }, + vk::DescriptorSetLayoutBinding { + binding: 1, + descriptor_type: vk::DescriptorType::STORAGE_BUFFER, + descriptor_count: 1, + stage_flags: vk::ShaderStageFlags::VERTEX, + ..Default::default() + }, + vk::DescriptorSetLayoutBinding { + binding: 2, + descriptor_type: vk::DescriptorType::COMBINED_IMAGE_SAMPLER, + descriptor_count: 1, + stage_flags: vk::ShaderStageFlags::FRAGMENT, + ..Default::default() + }, + ]; + let desc_set_layouts = unsafe { + [gpu.create_descriptor_set_layout( + &vk::DescriptorSetLayoutCreateInfo::default().bindings(&desc_layout_bindings), + ) + .unwrap()] + }; + + let descriptor_sets = unsafe { + gpu.allocate_descriptor_sets( + &vk::DescriptorSetAllocateInfo::default() + .descriptor_pool(descriptor_pool) + .set_layouts(&desc_set_layouts), + ) + .unwrap() + }; + + let globals_descriptor = vk::DescriptorBufferInfo { + buffer: globals_buffer.handle, + offset: 0, + range: globals_buffer.size, + }; + let instance_descriptor = vk::DescriptorBufferInfo { + buffer: instance_buffer.handle, + offset: 0, + range: instance_buffer.size, + }; + let font_descriptor = vk::DescriptorImageInfo { + image_layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, + image_view: font_image_view, + sampler: font_sampler, + }; + + unsafe { + gpu.update_descriptor_sets( + &[ + vk::WriteDescriptorSet { + dst_binding: 0, + dst_set: descriptor_sets[0], + descriptor_count: 1, + descriptor_type: vk::DescriptorType::UNIFORM_BUFFER, + p_buffer_info: &globals_descriptor, + ..Default::default() + }, + vk::WriteDescriptorSet { + dst_binding: 1, + dst_set: descriptor_sets[0], + descriptor_count: 1, + descriptor_type: vk::DescriptorType::STORAGE_BUFFER, + p_buffer_info: &instance_descriptor, + ..Default::default() + }, + vk::WriteDescriptorSet { + dst_binding: 2, + dst_set: descriptor_sets[0], + descriptor_count: 1, + descriptor_type: vk::DescriptorType::COMBINED_IMAGE_SAMPLER, + p_image_info: &font_descriptor, + ..Default::default() + }, + ], + &[], + ); + } + + let pipeline_layout = unsafe { + gpu.create_pipeline_layout( + &vk::PipelineLayoutCreateInfo::default().set_layouts(&desc_set_layouts), + ) + .expect("Failed to create UI pipeline layout") + }; + + let shader_vertex = unsafe { + load_shader_module(&gpu, include_bytes!("shaders/ui.vert.spv")) + .expect("Failed to load UI vertex shader") + }; + let shader_fragment = unsafe { + load_shader_module(&gpu, include_bytes!("shaders/ui.frag.spv")) + .expect("Failed to load UI fragment shader") + }; + + Self { + gpu, + surface_version: 0, + wait_for: setup.wait_for, + command_pool, + command_buffer_setup, + command_buffer_setup_reuse_fence, + vertex_buffer, + instance_buffer, + indirect_buffer, + globals_buffer, + font_image, + font_image_memory, + font_image_view, + font_sampler, + font_staging_buffer, + font_layer_index: HashMap::new(), + font_layer_count, + descriptor_pool, + desc_set_layouts, + descriptor_sets, + pipeline_layout, + pipeline: vk::Pipeline::null(), + shader_vertex, + shader_fragment, + instance_count: 0, + } + } + + fn build_instances(&mut self, overlay: &Overlay, assets: &Assets) -> u32 { + let mut instances = Vec::with_capacity(overlay.commands.len()); + + for command in &overlay.commands { + if instances.len() >= UI_MAX_INSTANCES { + crate::log::warn!( + "UI instance count exceeded UI_MAX_INSTANCES ({})", + UI_MAX_INSTANCES + ); + break; + } + + match command { + UiDrawCommand::Rect { + rect, + color, + corner_radius, + } => { + instances.push(UiInstance { + rect: [rect.x, rect.y, rect.width, rect.height], + color: color_to_array(*color), + params: [UI_KIND_RECT, *corner_radius, 0.0, 0.0], + uv: [0.0, 0.0, 1.0, 1.0], + }); + } + UiDrawCommand::Glyph { + rect, + color, + font, + uv_min, + uv_max, + } => { + let layer = self.register_font(*font, assets); + if layer.is_none() { + continue; + } + let font_asset = assets.get(*font).expect("font must exist"); + // UVs are normalized to the CPU atlas size; scale to the fixed GPU texture. + let uv_scale_x = font_asset.atlas_width() as f32 / UI_FONT_TEXTURE_SIZE as f32; + let uv_scale_y = font_asset.atlas_height() as f32 / UI_FONT_TEXTURE_SIZE as f32; + instances.push(UiInstance { + rect: [rect.x, rect.y, rect.width, rect.height], + color: color_to_array(*color), + params: [UI_KIND_GLYPH, 0.0, layer.unwrap() as f32, 0.0], + uv: [ + uv_min[0] * uv_scale_x, + uv_min[1] * uv_scale_y, + uv_max[0] * uv_scale_x, + uv_max[1] * uv_scale_y, + ], + }); + } + } + } + + if instances.is_empty() { + return 0; + } + + unsafe { + self.instance_buffer + .map_and_write_to_device_memory(&self.gpu, 0, instances.as_slice()); + } + + instances.len() as u32 + } + + fn register_font(&mut self, font_id: Id, assets: &Assets) -> Option { + if let Some(layer) = self.font_layer_index.get(&font_id) { + return Some(*layer); + } + if self.font_layer_index.len() as u32 >= self.font_layer_count { + crate::log::error!("UI font layer limit ({}) exceeded", self.font_layer_count); + return None; + } + + let font = assets.get(font_id)?; + let layer = self.font_layer_index.len() as u32; + let width = font.atlas_width().max(1); + let height = font.atlas_height().max(1); + if width > UI_FONT_TEXTURE_SIZE || height > UI_FONT_TEXTURE_SIZE { + crate::log::error!( + "font atlas {}x{} exceeds UI_FONT_TEXTURE_SIZE ({}); glyphs will be clipped", + width, + height, + UI_FONT_TEXTURE_SIZE + ); + } + let pixels = font.atlas_pixels(); + + unsafe { + self.font_staging_buffer + .map_and_write_to_device_memory(&self.gpu, 0, pixels); + self.upload_font_layer(layer, width, height); + } + + self.font_layer_index.insert(font_id, layer); + Some(layer) + } + + unsafe fn upload_font_layer(&self, layer: u32, width: u32, height: u32) { + self.gpu + .wait_for_fences(&[self.command_buffer_setup_reuse_fence], true, u64::MAX) + .expect("UI font upload fence wait failed"); + self.gpu + .reset_fences(&[self.command_buffer_setup_reuse_fence]) + .expect("UI font upload fence reset failed"); + self.gpu + .reset_command_buffer( + self.command_buffer_setup, + vk::CommandBufferResetFlags::RELEASE_RESOURCES, + ) + .expect("UI font upload command buffer reset failed"); + self.gpu + .begin_command_buffer( + self.command_buffer_setup, + &vk::CommandBufferBeginInfo::default() + .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT), + ) + .expect("UI font upload command buffer begin failed"); + + let barrier_to_transfer = vk::ImageMemoryBarrier { + dst_access_mask: vk::AccessFlags::TRANSFER_WRITE, + new_layout: vk::ImageLayout::TRANSFER_DST_OPTIMAL, + image: self.font_image, + subresource_range: vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + level_count: 1, + layer_count: self.font_layer_count, + ..Default::default() + }, + ..Default::default() + }; + self.gpu.cmd_pipeline_barrier( + self.command_buffer_setup, + vk::PipelineStageFlags::TOP_OF_PIPE, + vk::PipelineStageFlags::TRANSFER, + vk::DependencyFlags::empty(), + &[], + &[], + &[barrier_to_transfer], + ); + + self.gpu.cmd_copy_buffer_to_image( + self.command_buffer_setup, + self.font_staging_buffer.handle, + self.font_image, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + &[vk::BufferImageCopy::default() + .image_subresource( + vk::ImageSubresourceLayers::default() + .aspect_mask(vk::ImageAspectFlags::COLOR) + .mip_level(0) + .base_array_layer(layer) + .layer_count(1), + ) + .image_extent(vk::Extent3D { + width, + height, + depth: 1, + })], + ); + + let barrier_to_shader = vk::ImageMemoryBarrier { + src_access_mask: vk::AccessFlags::TRANSFER_WRITE, + dst_access_mask: vk::AccessFlags::SHADER_READ, + old_layout: vk::ImageLayout::TRANSFER_DST_OPTIMAL, + new_layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, + image: self.font_image, + subresource_range: vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + level_count: 1, + layer_count: self.font_layer_count, + ..Default::default() + }, + ..Default::default() + }; + self.gpu.cmd_pipeline_barrier( + self.command_buffer_setup, + vk::PipelineStageFlags::TRANSFER, + vk::PipelineStageFlags::FRAGMENT_SHADER, + vk::DependencyFlags::empty(), + &[], + &[], + &[barrier_to_shader], + ); + + self.gpu + .end_command_buffer(self.command_buffer_setup) + .expect("UI font upload command buffer end failed"); + + self.gpu + .submit_queue( + &[vk::SubmitInfo::default().command_buffers(&[self.command_buffer_setup])], + self.command_buffer_setup_reuse_fence, + ) + .expect("UI font upload submit failed"); + self.gpu + .wait_for_fences(&[self.command_buffer_setup_reuse_fence], true, u64::MAX) + .expect("UI font upload completion wait failed"); + } + + unsafe fn create_graphics_pipeline( + &self, + render_pass: vk::RenderPass, + surface_resolution: Extent2D, + ) -> vk::Pipeline { + let shader_entry_point = c"main"; + let shader_stages = [ + vk::PipelineShaderStageCreateInfo { + module: self.shader_vertex, + p_name: shader_entry_point.as_ptr(), + stage: vk::ShaderStageFlags::VERTEX, + ..Default::default() + }, + vk::PipelineShaderStageCreateInfo { + module: self.shader_fragment, + p_name: shader_entry_point.as_ptr(), + stage: vk::ShaderStageFlags::FRAGMENT, + ..Default::default() + }, + ]; + + let vertex_bindings = [vk::VertexInputBindingDescription { + binding: 0, + stride: 8, + input_rate: vk::VertexInputRate::VERTEX, + }]; + let vertex_attributes = [vk::VertexInputAttributeDescription { + location: 0, + binding: 0, + format: vk::Format::R32G32_SFLOAT, + offset: 0, + }]; + + let vertex_input = vk::PipelineVertexInputStateCreateInfo::default() + .vertex_binding_descriptions(&vertex_bindings) + .vertex_attribute_descriptions(&vertex_attributes); + let input_assembly = vk::PipelineInputAssemblyStateCreateInfo { + topology: vk::PrimitiveTopology::TRIANGLE_LIST, + ..Default::default() + }; + + let viewports = [vk::Viewport { + x: 0.0, + y: 0.0, + width: surface_resolution.width as f32, + height: surface_resolution.height as f32, + min_depth: 0.0, + max_depth: 1.0, + }]; + let scissors = [vk::Rect2D { + offset: vk::Offset2D { x: 0, y: 0 }, + extent: vk::Extent2D { + width: surface_resolution.width, + height: surface_resolution.height, + }, + }]; + let viewport_state = vk::PipelineViewportStateCreateInfo::default() + .viewports(&viewports) + .scissors(&scissors); + + let rasterization = vk::PipelineRasterizationStateCreateInfo { + polygon_mode: vk::PolygonMode::FILL, + line_width: 1.0, + cull_mode: vk::CullModeFlags::NONE, + front_face: vk::FrontFace::COUNTER_CLOCKWISE, + ..Default::default() + }; + let multisample = vk::PipelineMultisampleStateCreateInfo { + rasterization_samples: vk::SampleCountFlags::TYPE_1, + ..Default::default() + }; + let depth_stencil = vk::PipelineDepthStencilStateCreateInfo { + depth_test_enable: 0, + depth_write_enable: 0, + ..Default::default() + }; + let color_blend_attachments = [vk::PipelineColorBlendAttachmentState { + blend_enable: vk::TRUE, + src_color_blend_factor: vk::BlendFactor::SRC_ALPHA, + dst_color_blend_factor: vk::BlendFactor::ONE_MINUS_SRC_ALPHA, + color_blend_op: vk::BlendOp::ADD, + src_alpha_blend_factor: vk::BlendFactor::ONE, + dst_alpha_blend_factor: vk::BlendFactor::ONE_MINUS_SRC_ALPHA, + alpha_blend_op: vk::BlendOp::ADD, + color_write_mask: vk::ColorComponentFlags::RGBA, + }]; + let color_blend = + vk::PipelineColorBlendStateCreateInfo::default().attachments(&color_blend_attachments); + + let dynamic_state = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR]; + let dynamic_state_info = + vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_state); + + self.gpu + .create_graphics_pipelines( + vk::PipelineCache::null(), + &[vk::GraphicsPipelineCreateInfo::default() + .stages(&shader_stages) + .vertex_input_state(&vertex_input) + .input_assembly_state(&input_assembly) + .viewport_state(&viewport_state) + .rasterization_state(&rasterization) + .multisample_state(&multisample) + .depth_stencil_state(&depth_stencil) + .color_blend_state(&color_blend) + .dynamic_state(&dynamic_state_info) + .layout(self.pipeline_layout) + .render_pass(render_pass)], + ) + .expect("Failed to create UI graphics pipeline")[0] + } +} + +fn color_to_array(color: Color) -> [f32; 4] { + [color.r, color.g, color.b, color.a] +} + +pub struct RenderOverlaySetup { + wait_for: Vec, +} + +impl Default for RenderOverlaySetup { + fn default() -> Self { + Self { + wait_for: Vec::new(), + } + } +} + +impl RenderOverlaySetup { + pub fn wait_for(mut self, dependencies: impl IntoIterator) -> Self { + self.wait_for.extend(dependencies); + self + } + + pub fn create(self, display: &mut Display) -> RenderOverlay { + RenderOverlay::new(display, self) + } +} + +struct Recorder { + resolution: Extent2D, + pipeline_layout: vk::PipelineLayout, + pipeline: vk::Pipeline, + descriptor_sets: Vec, + vertex_buffer: vk::Buffer, + indirect_buffer: vk::Buffer, +} + +impl CommandRecorder for Recorder { + unsafe fn record(&self, gpu: &Gpu, command_buffer: vk::CommandBuffer) { + let viewports = [vk::Viewport { + x: 0.0, + y: 0.0, + width: self.resolution.width as f32, + height: self.resolution.height as f32, + min_depth: 0.0, + max_depth: 1.0, + }]; + let scissors = [vk::Rect2D { + offset: vk::Offset2D { x: 0, y: 0 }, + extent: vk::Extent2D { + width: self.resolution.width, + height: self.resolution.height, + }, + }]; + + gpu.cmd_bind_pipeline( + command_buffer, + vk::PipelineBindPoint::GRAPHICS, + self.pipeline, + ); + gpu.cmd_bind_descriptor_sets( + command_buffer, + vk::PipelineBindPoint::GRAPHICS, + self.pipeline_layout, + 0, + &self.descriptor_sets, + &[], + ); + gpu.cmd_set_viewport(command_buffer, 0, &viewports); + gpu.cmd_set_scissor(command_buffer, 0, &scissors); + gpu.cmd_bind_vertex_buffers(command_buffer, 0, &[self.vertex_buffer], &[0]); + gpu.cmd_draw_indirect( + command_buffer, + self.indirect_buffer, + 0, + 1, + std::mem::size_of::() as u32, + ); + } +} + +unsafe fn create_vertex_buffer(gpu: &Gpu, size: u64) -> Result { + Buffer::create_and_allocate( + gpu, + &vk::BufferCreateInfo { + size, + usage: vk::BufferUsageFlags::VERTEX_BUFFER, + sharing_mode: vk::SharingMode::EXCLUSIVE, + ..Default::default() + }, + ) +} + +unsafe fn create_storage_buffer(gpu: &Gpu, size: u64) -> Result { + Buffer::create_and_allocate( + gpu, + &vk::BufferCreateInfo { + size, + usage: vk::BufferUsageFlags::STORAGE_BUFFER, + sharing_mode: vk::SharingMode::EXCLUSIVE, + ..Default::default() + }, + ) +} + +unsafe fn create_indirect_buffer(gpu: &Gpu, size: u64) -> Result { + Buffer::create_and_allocate( + gpu, + &vk::BufferCreateInfo { + size, + usage: vk::BufferUsageFlags::INDIRECT_BUFFER, + sharing_mode: vk::SharingMode::EXCLUSIVE, + ..Default::default() + }, + ) +} + +unsafe fn create_uniform_buffer(gpu: &Gpu, size: u64) -> Result { + Buffer::create_and_allocate( + gpu, + &vk::BufferCreateInfo { + size, + usage: vk::BufferUsageFlags::UNIFORM_BUFFER, + sharing_mode: vk::SharingMode::EXCLUSIVE, + ..Default::default() + }, + ) +} + +unsafe fn create_transfer_buffer(gpu: &Gpu, size: u64) -> Result { + Buffer::create_and_allocate( + gpu, + &vk::BufferCreateInfo { + size, + usage: vk::BufferUsageFlags::TRANSFER_SRC, + sharing_mode: vk::SharingMode::EXCLUSIVE, + ..Default::default() + }, + ) +} + +unsafe fn load_shader_module(gpu: &Gpu, bytes: &[u8]) -> Result { + let mut cursor = Cursor::new(bytes); + let shader_code = ash::util::read_spv(&mut cursor).expect("Failed to read UI shader SPV"); + gpu.create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&shader_code)) +} diff --git a/src/features/ui/shaders/ui.frag b/src/features/ui/shaders/ui.frag new file mode 100644 index 00000000..e3b19f8c --- /dev/null +++ b/src/features/ui/shaders/ui.frag @@ -0,0 +1,34 @@ +#version 450 +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout(binding = 2) uniform sampler2DArray font_atlas; + +layout(location = 0) in vec4 v_color; +layout(location = 1) flat in float v_kind; +layout(location = 2) flat in float v_corner_radius; +layout(location = 3) flat in float v_font_layer; +layout(location = 4) in vec2 v_local_px; +layout(location = 5) in vec2 v_half_size_px; +layout(location = 6) in vec2 v_uv; + +layout(location = 0) out vec4 out_color; + +float rounded_box_sdf(vec2 p, vec2 half_size, float radius) { + vec2 q = abs(p) - half_size + radius; + return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius; +} + +void main() { + if (v_kind >= 0.5) { + float alpha = texture(font_atlas, vec3(v_uv, v_font_layer)).r * v_color.a; + out_color = vec4(v_color.rgb, alpha); + } else { + vec2 p = v_local_px - v_half_size_px; + float radius = min(v_corner_radius, min(v_half_size_px.x, v_half_size_px.y)); + float dist = rounded_box_sdf(p, v_half_size_px, radius); + float edge = fwidth(dist); + float alpha = (1.0 - smoothstep(-edge, edge, dist)) * v_color.a; + out_color = vec4(v_color.rgb, alpha); + } +} diff --git a/src/features/ui/shaders/ui.frag.spv b/src/features/ui/shaders/ui.frag.spv new file mode 100644 index 00000000..e743e134 Binary files /dev/null and b/src/features/ui/shaders/ui.frag.spv differ diff --git a/src/features/ui/shaders/ui.vert b/src/features/ui/shaders/ui.vert new file mode 100644 index 00000000..ebc42355 --- /dev/null +++ b/src/features/ui/shaders/ui.vert @@ -0,0 +1,46 @@ +#version 450 +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout(location = 0) in vec2 in_pos; + +layout(binding = 0) uniform UiGlobals { + vec2 resolution; + vec2 _padding; +} ui_globals; + +struct UiInstanceData { + vec4 rect; + vec4 color; + vec4 params; + vec4 uv; +}; + +layout(std430, binding = 1) readonly buffer UiInstances { + UiInstanceData instances[]; +}; + +layout(location = 0) out vec4 v_color; +layout(location = 1) flat out float v_kind; +layout(location = 2) flat out float v_corner_radius; +layout(location = 3) flat out float v_font_layer; +layout(location = 4) out vec2 v_local_px; +layout(location = 5) out vec2 v_half_size_px; +layout(location = 6) out vec2 v_uv; + +void main() { + UiInstanceData inst = instances[gl_InstanceIndex]; + // Layout uses top-left origin with +y downward; Vulkan NDC maps y=-1 to the + // top of the framebuffer, so no extra Y flip is needed here. + vec2 screen_pos = inst.rect.xy + in_pos * inst.rect.zw; + vec2 ndc = (screen_pos / ui_globals.resolution) * 2.0 - 1.0; + gl_Position = vec4(ndc, 0.0, 1.0); + + v_color = inst.color; + v_kind = inst.params.x; + v_corner_radius = inst.params.y; + v_font_layer = inst.params.z; + v_local_px = in_pos * inst.rect.zw; + v_half_size_px = inst.rect.zw * 0.5; + v_uv = mix(inst.uv.xy, inst.uv.zw, in_pos); +} diff --git a/src/features/ui/shaders/ui.vert.spv b/src/features/ui/shaders/ui.vert.spv new file mode 100644 index 00000000..5bee9916 Binary files /dev/null and b/src/features/ui/shaders/ui.vert.spv differ diff --git a/src/features/ui/view.rs b/src/features/ui/view.rs new file mode 100644 index 00000000..2d913836 --- /dev/null +++ b/src/features/ui/view.rs @@ -0,0 +1,17 @@ +pub enum PositionConstraint { + Pixel(f32), + Percent(f32), + Center, +} + +pub enum SizeConstraint { + Pixel(f32), + Percent(f32), +} + +pub struct View { + pub x: PositionConstraint, + pub y: PositionConstraint, + pub width: SizeConstraint, + pub height: SizeConstraint, +} diff --git a/src/lib.rs b/src/lib.rs index f9cccf27..8a35028e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,6 +37,9 @@ pub use models::{ VertexPosition, VertexTangent, VertexTexture, VertexWeights, }; +#[cfg(feature = "ui")] +pub use features::ui::Font; + /// Rendering tools and routines pub mod graphics; pub use graphics::{DeviceType, Display, Extent2D, Format, Frame, Gpu, Semaphore}; diff --git a/src/loaders.rs b/src/loaders.rs index 00c7dd9c..6e588222 100644 --- a/src/loaders.rs +++ b/src/loaders.rs @@ -10,6 +10,8 @@ pub use image_loader::ImageLoader; use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; +use crate::models::Material; + pub use crate::tasks::{Any, Mut, Take, Task}; pub use assets::{Asset, Assets}; @@ -114,10 +116,26 @@ impl Task for StoreAssets { fn run(&mut self, (bundle, mut assets): Self::Context) -> Self::Output { let ResourceBundle { resource, bundle } = bundle.take(); - let report = bundle - .into_iter() - .map(|(target, asset)| (target, asset.map(|asset| assets.store(asset)))) - .collect::>(); + let mut materials = Vec::new(); + let mut report = HashMap::new(); + + for (target, asset) in bundle.into_iter() { + if target.type_id == std::any::TypeId::of::() { + materials.push((target, asset)); + continue; + } + let stored = asset.map(|asset| assets.store(asset)); + report.insert(target, stored); + } + + for (target, asset) in materials.into_iter() { + let stored = asset.map(|asset| { + let mut material = *unsafe { Box::from_raw(Box::into_raw(asset) as *mut Material) }; + material.resolve_maps(&assets); + assets.store(Box::new(material)) + }); + report.insert(target, stored); + } ResourceReport { resource, report } } diff --git a/src/loaders/gltf_loader.rs b/src/loaders/gltf_loader.rs index 587c59e3..5894e8f4 100644 --- a/src/loaders/gltf_loader.rs +++ b/src/loaders/gltf_loader.rs @@ -19,8 +19,8 @@ use gltf::Gltf; use crate::log; use crate::math::{Mat4, Quat, Vec3}; use crate::models::{ - Animation, Armature, Color, Image, ImageFormat, Interpolation, Joint, Material, Mesh, - Transform3D, VertexJoints, VertexNormal, VertexPosition, VertexTexture, VertexWeights, + Animation, Armature, Color, ImageFormat, Interpolation, Joint, Material, Mesh, Transform3D, + VertexJoints, VertexNormal, VertexPosition, VertexTexture, VertexWeights, }; use crate::utils::Id; @@ -32,7 +32,7 @@ type ResultIndex = usize; #[derive(Default)] struct Output { result: Vec>, - loaded_images: HashMap, + loaded_images: HashMap, loaded_meshes: HashMap, loaded_materials: HashMap, loaded_armature: HashMap, @@ -337,28 +337,23 @@ impl GltfLoader { let metallic_factor = pbr.metallic_factor(); let roughness_factor = pbr.roughness_factor(); - let albedo_map = pbr + let albedo_map_name = pbr .base_color_texture() - .map(|info| Self::read_image(output, &info.texture(), buffers, name)) - .unwrap_or_default(); + .and_then(|info| Self::read_image(output, &info.texture(), buffers, name)); - let normal_map = material - .normal_texture() - .map(|normals| Self::read_image(output, &normals.texture(), buffers, name)) - .unwrap_or_default(); + if let Some(normals) = material.normal_texture() { + Self::read_image(output, &normals.texture(), buffers, name); + } - let occlusion_map = material - .normal_texture() - .map(|occlusion| Self::read_image(output, &occlusion.texture(), buffers, name)) - .unwrap_or_default(); + if let Some(occlusion) = material.occlusion_texture() { + Self::read_image(output, &occlusion.texture(), buffers, name); + } let asset_name = [name, material.name().unwrap_or("material")].join("::"); let material_asset = Material { name: asset_name, albedo, - albedo_map, - normal_map, - occlusion_map, + albedo_map_name, metallic_factor, roughness_factor, @@ -376,55 +371,57 @@ impl GltfLoader { texture: &gltf::Texture, buffers: &[Vec], name: &str, - ) -> Id { + ) -> Option { let asset_name = [name, texture.name().unwrap_or("material")].join("::"); - let image_index = texture.index(); - let mut result_index = output.loaded_images.get(&image_index).cloned(); - - if result_index.is_none() { - let source = texture.source().source(); - let (data, format) = match source { - gltf::image::Source::Uri { uri, .. } => { - const URI_IMAGE_PNG: &str = "data:image/png;base64,"; - - if !uri.starts_with(URI_IMAGE_PNG) { - log::warn!("Unsupported texture uri"); - return Id::default(); - } + let texture_index = texture.index(); - match base64_decode(&uri[URI_IMAGE_PNG.len()..]) { - Ok(data) => (data, ImageFormat::Png), - Err(err) => { - log::error!("Could not decode texture data: {err:?}"); - return Id::default(); - } - } + if let Some(cached_name) = output.loaded_images.get(&texture_index) { + return Some(cached_name.clone()); + } + + let source = texture.source().source(); + let (data, format) = match source { + gltf::image::Source::Uri { uri, .. } => { + const URI_IMAGE_PNG: &str = "data:image/png;base64,"; + + if !uri.starts_with(URI_IMAGE_PNG) { + log::warn!("Unsupported texture uri"); + return None; } - gltf::image::Source::View { view, mime_type } => { - if mime_type != "image/png" { - log::warn!("Unsupported mime: {mime_type}"); - return Id::default(); + match base64_decode(&uri[URI_IMAGE_PNG.len()..]) { + Ok(data) => (data, ImageFormat::Png), + Err(err) => { + log::error!("Could not decode texture data: {err:?}"); + return None; } + } + } - let index = view.buffer().index(); - let offset = view.offset(); - let tail = offset + view.length(); - let data = &buffers[index][offset..tail]; - - (data.to_vec(), ImageFormat::Png) + gltf::image::Source::View { view, mime_type } => { + if mime_type != "image/png" { + log::warn!("Unsupported mime: {mime_type}"); + return None; } - }; - if let Some(image) = ImageLoader::read_buffer(asset_name, &data, format) { - let index = output.result.len(); - result_index.replace(index); - output.loaded_images.insert(image_index, index); - output.result.push(Box::new(image)); + let index = view.buffer().index(); + let offset = view.offset(); + let tail = offset + view.length(); + let data = &buffers[index][offset..tail]; + + (data.to_vec(), ImageFormat::Png) } - } + }; - Id::new() + if let Some(image) = ImageLoader::read_material_buffer(asset_name.clone(), &data, format) { + output + .loaded_images + .insert(texture_index, asset_name.clone()); + output.result.push(Box::new(image)); + Some(asset_name) + } else { + None + } } fn read_animation( diff --git a/src/loaders/image_loader.rs b/src/loaders/image_loader.rs index 8d24c431..be5573f6 100644 --- a/src/loaders/image_loader.rs +++ b/src/loaders/image_loader.rs @@ -76,6 +76,33 @@ impl ImageLoader { } } + /// Decodes image data into RGBA8 bytes (for material texture upload). + pub fn read_material_buffer( + name: impl Into, + data: &[u8], + format: ImageFormat, + ) -> Option { + let format = match format { + ImageFormat::Png => image::ImageFormat::Png, + ImageFormat::Jpeg => image::ImageFormat::Jpeg, + ImageFormat::Bmp => image::ImageFormat::Bmp, + }; + match image::load_from_memory_with_format(data, format) { + Ok(img) => { + let rgba = img.to_rgba8(); + let resolution = Extent2D { + width: rgba.width(), + height: rgba.height(), + }; + Some(Image::new(name.into(), resolution, rgba.into_raw())) + } + Err(e) => { + log::error!("Could not read material image from buffer: {:?}", e); + None + } + } + } + pub fn read_buffer(name: impl Into, data: &[u8], format: ImageFormat) -> Option { let format = match format { ImageFormat::Png => image::ImageFormat::Png, diff --git a/src/models/materials.rs b/src/models/materials.rs index 4778d0c6..9d4b2b27 100644 --- a/src/models/materials.rs +++ b/src/models/materials.rs @@ -1,10 +1,13 @@ use super::{Color, Image}; -use crate::loaders::Asset; +use crate::loaders::{Asset, Assets}; use crate::utils::Id; // NOTE: 1:albedo_map, 2:occlusion_map, 3:metallic_map, 4:normal_map, 5:roughness_map pub const MAX_MATERIAL_IMAGES: u32 = 5; +/// Texture array layer index indicating no albedo map is bound. +pub const NO_TEXTURE_LAYER: u32 = u32::MAX; + /// Material component #[derive(Debug)] pub struct Material { @@ -14,6 +17,8 @@ pub struct Material { pub albedo: Color, /// Id of a texture asset pub albedo_map: Id, + /// Asset name of the albedo map (resolved via [`Material::resolve_maps`]) + pub albedo_map_name: Option, // Ambient occulsion pub occlusion_factor: f32, /// Id of a ao texture asset @@ -36,6 +41,7 @@ impl Default for Material { name: String::from("dotrix::material"), albedo: Color::white(), albedo_map: Id::default(), + albedo_map_name: None, occlusion_factor: 1.0, occlusion_map: Id::default(), metallic_factor: 1.0, @@ -80,6 +86,17 @@ impl From<&Material> for MaterialUniform { unsafe impl bytemuck::Pod for MaterialUniform {} unsafe impl bytemuck::Zeroable for MaterialUniform {} +impl Material { + /// Resolves texture map references by asset name after images are stored. + pub fn resolve_maps(&mut self, assets: &Assets) { + if self.albedo_map.is_null() { + if let Some(name) = self.albedo_map_name.as_deref() { + self.albedo_map = assets.find(name).unwrap_or_default(); + } + } + } +} + impl Asset for Material { fn name(&self) -> &str { &self.name diff --git a/src/models/renderer.rs b/src/models/renderer.rs index 022883e8..105b8760 100644 --- a/src/models/renderer.rs +++ b/src/models/renderer.rs @@ -4,7 +4,7 @@ use std::io::Cursor; use crate::graphics::{vk, CommandRecorder}; use crate::graphics::{Buffer, RenderSubmit}; use crate::loaders::Assets; -use crate::models::materials::MAX_MATERIAL_IMAGES; +use crate::models::materials::{MAX_MATERIAL_IMAGES, NO_TEXTURE_LAYER}; use crate::utils::Id; use crate::world::{Camera, Entity, World}; use crate::{log, VertexJoints, VertexWeights}; @@ -1037,39 +1037,51 @@ impl RenderModels { fn register_material(&mut self, material_id: Id, assets: &Assets) -> Option { let material_uniform: MaterialUniform = if let Some(material) = assets.get(material_id) { let mut staging_layer_count: u32 = 0; - let (albedo_map_index, base_array_layer) = assets - .get(material.albedo_map) - .map(|image| { - let base_array_layer = self.material_layer_index.len(); - if let std::collections::hash_map::Entry::Vacant(e) = - self.material_layer_index.entry(material.albedo_map) - { - // write to buffer - // TODO: verify material extent - unsafe { - self.material_staging_buffer.map_and_write_to_device_memory( - &self.gpu, - (staging_layer_count - * self.material_layer_size.width - * self.material_layer_size.height - * std::mem::size_of::() as u32) - as u64, - image.data(), - ); - }; - e.insert(base_array_layer); - staging_layer_count += 1; + let (albedo_map_index, base_array_layer) = if material.albedo_map.is_null() { + (NO_TEXTURE_LAYER, 0) + } else if let Some(image) = assets.get(material.albedo_map) { + let base_array_layer = self.material_layer_index.len(); + if let std::collections::hash_map::Entry::Vacant(e) = + self.material_layer_index.entry(material.albedo_map) + { + let texture_data = + Self::prepare_material_texture(image, self.material_layer_size); + let expected_len = (self.material_layer_size.width + * self.material_layer_size.height + * std::mem::size_of::() as u32) + as usize; + if texture_data.len() != expected_len { + log::error!( + "Material texture `{}` has unexpected size ({} bytes, expected {})", + image.name(), + texture_data.len(), + expected_len + ); } - let albedo_map_index = self - .material_layer_index - .get(&material.albedo_map) - .cloned() - .expect("Layer index must be inserted at this stage") - as u32; - (albedo_map_index, base_array_layer) - }) - .unwrap_or((0, 0)); - // flush material staging buffer + unsafe { + self.material_staging_buffer.map_and_write_to_device_memory( + &self.gpu, + (staging_layer_count + * self.material_layer_size.width + * self.material_layer_size.height + * std::mem::size_of::() as u32) + as u64, + texture_data.as_slice(), + ); + }; + e.insert(base_array_layer); + staging_layer_count += 1; + } + let albedo_map_index = self + .material_layer_index + .get(&material.albedo_map) + .cloned() + .expect("Layer index must be inserted at this stage") + as u32; + (albedo_map_index, base_array_layer) + } else { + (NO_TEXTURE_LAYER, 0) + }; unsafe { self.flush_material_staging_buffer(staging_layer_count, base_array_layer as u32); }; @@ -1273,6 +1285,50 @@ impl RenderModels { Buffer::create_and_allocate(gpu, &buffer_create_info) } + fn prepare_material_texture(image: &Image, target_size: Extent2D) -> Vec { + use image::{imageops::FilterType, ImageBuffer, RgbaImage}; + + let width = image.resolution().width; + let height = image.resolution().height; + let data = image.data(); + let rgba_len = (width * height * 4) as usize; + + let rgba: RgbaImage = if data.len() == rgba_len { + ImageBuffer::from_raw(width, height, data.to_vec()) + .expect("RGBA image buffer must match resolution") + } else if data.len() == (width * height * 3) as usize { + let mut raw = vec![0u8; rgba_len]; + for (i, chunk) in data.chunks_exact(3).enumerate() { + let offset = i * 4; + raw[offset] = chunk[0]; + raw[offset + 1] = chunk[1]; + raw[offset + 2] = chunk[2]; + raw[offset + 3] = 255; + } + ImageBuffer::from_raw(width, height, raw) + .expect("RGB image buffer must match resolution") + } else { + log::error!( + "Unsupported material texture `{}` byte length {}", + image.name(), + data.len() + ); + return vec![0; rgba_len]; + }; + + if width != target_size.width || height != target_size.height { + image::imageops::resize( + &rgba, + target_size.width, + target_size.height, + FilterType::Triangle, + ) + .into_raw() + } else { + rgba.into_raw() + } + } + unsafe fn create_transfer_buffer(gpu: &Gpu, size: u64) -> Result { let buffer_create_info = vk::BufferCreateInfo { size, diff --git a/src/models/shaders/only_mesh.frag b/src/models/shaders/only_mesh.frag index c267d249..dccd96a1 100644 --- a/src/models/shaders/only_mesh.frag +++ b/src/models/shaders/only_mesh.frag @@ -19,7 +19,9 @@ void main() { vec3 light_direction = normalize(light_position - world_position); vec3 diffuse = max(dot(normal, light_direction), 0.0) * light_color; - vec4 texture_color = texture(dtx_material_sampler, vertex_texture); + vec4 texture_color = vertex_texture.z < 0.0 + ? vec4(1.0) + : texture(dtx_material_sampler, vertex_texture); o_frag_color = vec4((ambient_light + diffuse), 1.0) * vertex_color * texture_color; } diff --git a/src/models/shaders/only_mesh.frag.spv b/src/models/shaders/only_mesh.frag.spv index d74515ce..7370103b 100644 Binary files a/src/models/shaders/only_mesh.frag.spv and b/src/models/shaders/only_mesh.frag.spv differ diff --git a/src/models/shaders/only_mesh.vert b/src/models/shaders/only_mesh.vert index 9efc33b9..a76ea8e3 100644 --- a/src/models/shaders/only_mesh.vert +++ b/src/models/shaders/only_mesh.vert @@ -49,13 +49,14 @@ void main() { uint material_index = dtx_instance[gl_InstanceIndex].material_index; mat4 model_transform = dtx_transform[transform_index]; vec4 material_color = dtx_material[material_index].color; - float albedo_layer = float(dtx_material[material_index].maps_1.x); + uint albedo_layer = dtx_material[material_index].maps_1.x; + float layer = (albedo_layer == 0xFFFFFFFFu) ? -1.0 : float(albedo_layer); mat4 proj_view = dtx_globals.proj * dtx_globals.view; o_world_position = vec3(model_transform * vec4(pos, 1.0)); o_world_normal = vec3(model_transform * vec4(normal, 1.0)); o_color = vec4(material_color); - o_texture = vec3(texture, albedo_layer); + o_texture = vec3(texture, layer); gl_Position = proj_view * vec4(o_world_position, 1.0); } diff --git a/src/models/shaders/only_mesh.vert.spv b/src/models/shaders/only_mesh.vert.spv index 0d505f2e..d80c3615 100644 Binary files a/src/models/shaders/only_mesh.vert.spv and b/src/models/shaders/only_mesh.vert.spv differ diff --git a/src/models/shaders/skin_mesh.frag b/src/models/shaders/skin_mesh.frag index 6f979130..48901ef6 100644 --- a/src/models/shaders/skin_mesh.frag +++ b/src/models/shaders/skin_mesh.frag @@ -20,7 +20,9 @@ void main() { vec3 light_direction = normalize(light_position - world_position); vec3 diffuse = max(dot(normal, light_direction), 0.0) * light_color; - vec4 texture_color = texture(dtx_material_sampler, vertex_texture); + vec4 texture_color = vertex_texture.z < 0.0 + ? vec4(1.0) + : texture(dtx_material_sampler, vertex_texture); o_frag_color = vec4((ambient_light + diffuse), 1.0) * vertex_color * texture_color; } diff --git a/src/models/shaders/skin_mesh.frag.spv b/src/models/shaders/skin_mesh.frag.spv index d74515ce..7370103b 100644 Binary files a/src/models/shaders/skin_mesh.frag.spv and b/src/models/shaders/skin_mesh.frag.spv differ diff --git a/src/models/shaders/skin_mesh.vert b/src/models/shaders/skin_mesh.vert index 71e60711..7f1cd43b 100644 --- a/src/models/shaders/skin_mesh.vert +++ b/src/models/shaders/skin_mesh.vert @@ -52,7 +52,8 @@ void main() { uint material_index = dtx_instance[gl_InstanceIndex].material_index; mat4 model_transform = dtx_transform[transform_index]; vec4 material_color = dtx_material[material_index].color; - float albedo_layer = float(dtx_material[material_index].maps_1.x); + uint albedo_layer = dtx_material[material_index].maps_1.x; + float layer = (albedo_layer == 0xFFFFFFFFu) ? -1.0 : float(albedo_layer); mat4 skin_transform = mat4(1.0); @@ -68,7 +69,7 @@ void main() { o_world_position = vec3(model_transform * skin_transform * vec4(pos, 1.0)); o_world_normal = vec3(model_transform * vec4(normal, 1.0)); o_color = vec4(material_color); - o_texture = vec3(texture, albedo_layer); + o_texture = vec3(texture, layer); gl_Position = proj_view * vec4(o_world_position, 1.0); } diff --git a/src/models/shaders/skin_mesh.vert.spv b/src/models/shaders/skin_mesh.vert.spv index 418ef84e..3d9b86c9 100644 Binary files a/src/models/shaders/skin_mesh.vert.spv and b/src/models/shaders/skin_mesh.vert.spv differ diff --git a/src/tasks/context.rs b/src/tasks/context.rs index 71062df6..153ff6ca 100644 --- a/src/tasks/context.rs +++ b/src/tasks/context.rs @@ -121,6 +121,9 @@ impl Manager { .get(&std::any::TypeId::of::()) .and_then(|slot| { let total = slot.instances.len(); + if index >= total { + return None; + } (*slot.instances[index].get()) .as_ref() .and_then(|data| data.downcast_ref::()) @@ -155,6 +158,9 @@ impl Manager { .get(&std::any::TypeId::of::()) .and_then(|slot| { let total = slot.instances.len(); + if index >= total { + return None; + } (*slot.instances[index].get()) .take() .and_then(|data| data.downcast::().ok()) @@ -273,6 +279,12 @@ impl Manager { /// Provides an output pub fn provide(&mut self, type_id: TypeId, data: Box) { let entry = self.outputs.entry(type_id).or_default(); + // The scheduler cycle marker must be a single instance; otherwise tasks + // that depend on Any can run once per accumulated instance and + // acquire multiple swapchain images before SubmitFrame presents. + if type_id == TypeId::of::() { + entry.instances.clear(); + } entry.instances.push(UnsafeCell::new(Some(data))); log::debug!( "Provide {} -> {} of {}", @@ -349,6 +361,54 @@ impl Manager { Some(result) } + /// Describes why task dependencies are not satisfied yet. + pub fn describe_unsatisfied_dependencies(&self, dependencies: &Dependencies) -> Vec { + let mut reasons = Vec::new(); + + for (type_id, dependency) in dependencies.data.iter() { + let output = match self.outputs.get(type_id) { + Some(output) => output, + None => { + reasons.push(format!( + "missing output slot for {}", + self.output_name(type_id).unwrap_or("UNKNOWN") + )); + continue; + } + }; + let instances_len = output.instances.len(); + let output_name = output.name.clone(); + + match dependency { + DependencyType::Any(index) => { + if instances_len == 0 || *index >= instances_len { + if output.providers == 0 { + reasons.push(format!( + "{output_name} has no providers in the current queue/state" + )); + } else { + reasons.push(format!( + "{output_name} waiting for instance {} of {} (have {instances_len})", + *index + 1, + output.providers + )); + } + } + } + DependencyType::All(count) => { + if *count != 0 || instances_len < output.providers { + reasons.push(format!( + "{output_name} waiting for {instances_len} of {} instances (All)", + output.providers + )); + } + } + } + } + + reasons + } + /// Fetches dependencies pub unsafe fn fetch(&self, dependencies: &Dependencies) -> T where diff --git a/src/tasks/scheduler.rs b/src/tasks/scheduler.rs index fb62cc29..433ce2fb 100644 --- a/src/tasks/scheduler.rs +++ b/src/tasks/scheduler.rs @@ -70,6 +70,7 @@ pub fn spawn( let mut lock_for_input = false; let mut restart_queue = false; let mut queue_executed = true; + let mut tasks_in_flight = 0usize; loop { let mut command = if lock_for_input { // There is nothing else to do, except for waiting @@ -85,6 +86,7 @@ pub fn spawn( tasks_graph_changed = true; } Message::Output(task, data) => { + tasks_in_flight = tasks_in_flight.saturating_sub(1); let type_id = task.output_type_id(); let output_channel = task.output_channel(); lock_manager.unlock(task.lock()); @@ -146,7 +148,7 @@ pub fn spawn( if restart_queue { // log::debug!("restart queue(queue_executed: {}", queue_executed); - if queue_executed { + if queue_executed && tasks_in_flight == 0 { let mut ctx = context_manager.lock().expect("Mutex to be locked"); ctx.reset_data(tasks_graph_changed); ctx.apply_states_changes(); @@ -216,6 +218,7 @@ pub fn spawn( // move to the end of queue queue.remove(index); queue.push(task_id); + tasks_in_flight += 1; worker_tx.send(Message::Schedule(task)).ok(); stop_index -= 1; continue; @@ -225,6 +228,42 @@ pub fn spawn( } index += 1; } + + if !queue_executed && tasks_in_flight == 0 { + let ctx = context_manager.lock().expect("Mutex to be locked"); + let mut blocked_tasks = Vec::new(); + + for task_id in &queue { + if let Some(task) = pool + .get(*task_id) + .and_then(|slot| slot.executable()) + .filter(|task| !task.is_scheduled()) + { + let reasons = ctx.describe_unsatisfied_dependencies(task.dependencies()); + if !reasons.is_empty() { + blocked_tasks.push((task.name(), reasons)); + } + } + } + + if !blocked_tasks.is_empty() { + log::error!( + "Scheduler stalled waiting for {:?}; {} task(s) cannot progress:", + std::any::type_name::(), + blocked_tasks.len() + ); + for (task_name, reasons) in blocked_tasks { + for reason in reasons { + log::error!(" - {task_name}: {reason}"); + } + } + panic!( + "Task scheduler deadlock while waiting for {}; see log above for blocked tasks", + std::any::type_name::() + ); + } + } + lock_for_input = true; } }) diff --git a/src/tasks/task.rs b/src/tasks/task.rs index 7388c4fd..5bcec23b 100644 --- a/src/tasks/task.rs +++ b/src/tasks/task.rs @@ -195,6 +195,7 @@ where fn reset(&mut self) { self.dependencies.reset(); + self.dependencies_state = None; } } @@ -228,6 +229,10 @@ impl Slot { pub fn name(&self) -> Option<&str> { self.task.as_ref().map(|t| t.name()) } + + pub fn executable(&self) -> Option<&dyn Executable> { + self.task.as_deref() + } } /// Tasks pool @@ -305,6 +310,14 @@ impl Pool { output_type_id: std::any::TypeId, context: &mut context::Manager, ) -> usize { + // Loop is injected by the scheduler at the start of each cycle, not by a + // queued task. Without this, dependency analysis reports 0 providers and + // SubmitFrame runs before any RenderSubmit tasks produce output. + if output_type_id == TypeId::of::() { + context.set_output_providers(output_type_id, 1); + return 1; + } + let tasks = queue.iter().filter_map(|id| { self.tasks .get(id) @@ -323,6 +336,7 @@ impl Pool { for task in tasks { let mut p = 1; let mut will_run_multiple_times = false; + let mut satisfiable = true; for (dep_type_id, dep_type) in task.dependencies().data.iter() { match dep_type { context::DependencyType::Any(_) => { @@ -334,6 +348,7 @@ impl Pool { task.name(), context.output_name(dep_type_id).unwrap_or("UNKNOWN") ); + satisfiable = false; } else if any_providers > 1 { if will_run_multiple_times { panic!( @@ -350,7 +365,9 @@ impl Pool { } }; } - providers += p; + if satisfiable { + providers += p; + } } log::debug!( "{} has {} providers", diff --git a/src/ui/composer.rs b/src/ui/composer.rs deleted file mode 100644 index 57a5fc6c..00000000 --- a/src/ui/composer.rs +++ /dev/null @@ -1,63 +0,0 @@ -use dotrix_gpu as gpu; -use dotrix_input::Input; -use dotrix_types::{Frame, Id}; - -use crate::overlay; -use crate::Rect; - -pub struct Composer<'c, 'i, 'f> { - pub ctx: &'c mut Context, - pub input: &'i Input, - pub frame: &'f Frame, - pub build_stack: Vec, - pub widgets: Vec, -} - -pub trait Compose: Send { - fn compose<'c, 'i, 'f>(&self, rect: &Rect, composer: &mut Composer<'c, 'i, 'f>); -} - -impl<'c, 'i, 'f> Composer<'c, 'i, 'f> { - pub fn new(ctx: &'c mut Context, input: &'i Input, frame: &'f Frame) -> Self { - Self { - ctx, - input, - frame, - build_stack: vec![], - widgets: vec![], - } - } - - pub fn builder<'a>( - &'a mut self, - rect: Rect, - texture: Id, - ) -> (&'a mut overlay::Builder, bool) { - let exists = self - .build_stack - .last() - .and_then(|builder| { - if builder.texture == texture { - Some(builder) - } else { - None - } - }) - .is_some(); - - //let exists = false; - - if !exists { - self.build_stack.push(overlay::Builder::new(rect, texture)); - } - (self.build_stack.last_mut().unwrap(), !exists) - } - - pub fn builder_pop(&mut self) -> Option { - self.build_stack.pop() - } - - pub fn add_widget(&mut self, widget: overlay::Widget) { - self.widgets.push(widget); - } -} diff --git a/src/ui/context.rs b/src/ui/context.rs deleted file mode 100644 index 401dbbba..00000000 --- a/src/ui/context.rs +++ /dev/null @@ -1,2 +0,0 @@ -use crate::{Overlay, Rect, Style, View}; -use std::rc::Rc; diff --git a/src/ui/edit.rs b/src/ui/edit.rs deleted file mode 100644 index d72d2df3..00000000 --- a/src/ui/edit.rs +++ /dev/null @@ -1 +0,0 @@ -pub struct Edit {} diff --git a/src/ui/font.rs b/src/ui/font.rs deleted file mode 100644 index 744fc490..00000000 --- a/src/ui/font.rs +++ /dev/null @@ -1,278 +0,0 @@ -use std::collections::HashMap; -use std::ops::Range; - -use dotrix_types::{vertex, TexUV}; -use dotrix_log as log; - -use crate::Rect; - -pub struct Font { - size: f32, - atlas: Atlas, - map: HashMap, - glyphs: Vec, - line_metrics: LineMetrics, -} - -impl Font { - pub fn from_bytes(font_size: f32, charsets: &[Charset], bytes: &[u8]) -> Self { - let fontdue_font = match fontdue::Font::from_bytes(bytes, fontdue::FontSettings::default()) - { - Ok(fontdue_font) => fontdue_font, - Err(err) => panic!("Could not create font from bytes: {err}"), - }; - - let line_metrics = fontdue_font - .horizontal_line_metrics(font_size) - .map(|fontdue_line_metrics| LineMetrics { - line_ascent: fontdue_line_metrics.ascent, - line_descent: fontdue_line_metrics.descent, - line_gap: fontdue_line_metrics.line_gap, - line_size: fontdue_line_metrics.new_line_size, - }) - .expect("The font is not populated with metrics"); - - let capacity: usize = charsets - .iter() - .map(|charset| charset.range()) - .flatten() - .map(|range| range.len() as usize) - .sum(); - - let mut glyphs = Vec::with_capacity(capacity); - let mut map = HashMap::with_capacity(capacity); - - for charset in charsets.iter() { - for range in charset.range().iter() { - for unicode in range.clone() { - if let Some(character) = std::char::from_u32(unicode) { - let (metrics, bitmap) = fontdue_font.rasterize(character, font_size); - let glyph = Glyph { - bitmap, - uvs: [vertex::TexUV::default(); 4], - rect: Rect { - horizontal: metrics.xmin as f32, - vertical: metrics.ymin as f32, - width: metrics.width as f32, - height: metrics.height as f32, - }, - advance_width: metrics.advance_width, - }; - let index = glyphs.len(); - glyphs.push(glyph); - map.insert(character, index); - } - } - } - } - - let atlas = Atlas::new(&mut glyphs, font_size); - - Self { - size: font_size, - atlas, - glyphs, - map, - line_metrics, - } - } - - pub fn line_metrics(&self) -> &LineMetrics { - &self.line_metrics - } - - pub fn atlas(&self) -> &Atlas { - &self.atlas - } - - /// Returns a tuple of vertices and texture uvs - pub fn tesselate( - &self, - rect: &Rect, - cursor: &mut Cursor, - character: char - ) -> ([[f32; 2]; 4], [[f32; 2]; 4]) { - log::error!("font::tesselate is not implemented!"); - ([[0.0; 2]; 4], [[0.0; 2]; 4]) - } - - pub fn place_word(&self, rect: &Rect, cursor: &mut Cursor, word: &str) { - // we are at the beginning of the line, render word even if width is not enough - if cursor.offset.is_none() { - return; - } - todo!("Place word at the end or go next line, but if not first line"); - } -} - -pub struct Atlas { - bitmap: Vec, - width: u32, - height: u32, -} - -impl Atlas { - const SPACING: f32 = 2.0; - - pub fn new(glyphs: &mut [Glyph], font_size: f32) -> Self { - let spacing = Self::SPACING.ceil() as usize; - let (width, height) = Self::calculate_size(glyphs, font_size); - let mut bitmap = vec![0; width * height]; - let mut horizontal_offset: usize = spacing; - let mut vertical_offset: usize = spacing; - let mut line_height: usize = 0; - - for glyph in glyphs.iter_mut() { - let glyph_width = glyph.rect.width.ceil() as usize; - let glyph_height = glyph.rect.height.ceil() as usize; - - if glyph_width == 0 && glyph_height == 0 { - continue; - } - - let mut glyph_horizontal_offset = horizontal_offset; - horizontal_offset += glyph_width + 2 * spacing; - - if horizontal_offset > width { - // go next line - glyph_horizontal_offset = spacing; - horizontal_offset = glyph_width + 3 * spacing; - vertical_offset += line_height + spacing; - line_height = 0; - } - - if line_height < glyph_height { - line_height = glyph_height; - if vertical_offset + line_height > height { - panic!( - "The font atlas bitmap {}x{} size is not enough", - width, height - ); - } - } - - let u0 = glyph_horizontal_offset as f32 / width as f32; - let v0 = vertical_offset as f32 / height as f32; - let u1 = (glyph_horizontal_offset + glyph_width) as f32 / width as f32; - let v1 = (vertical_offset + glyph_height) as f32 / height as f32; - - glyph.uvs = [ - TexUV::new(u0, v0), - TexUV::new(u1, v0), - TexUV::new(u1, v1), - TexUV::new(u0, v1), - ]; - - for row in 0..glyph_height { - let offset = (vertical_offset + row) * width + glyph_horizontal_offset; - for col in 0..glyph_width { - bitmap[offset + col] = glyph.bitmap[row * glyph_width + col]; - } - } - } - - Self { - bitmap, - width: width as u32, - height: width as u32, - } - } - - fn calculate_size(glyphs: &[Glyph], font_size: f32) -> (usize, usize) { - let reserve_factor = 1.2; - let size = (reserve_factor - * glyphs - .iter() - .map(|glyph| { - (glyph.rect.width + Self::SPACING) * (glyph.rect.height + Self::SPACING) - }) - .sum::() - .sqrt() - .ceil() - + font_size.ceil()) as usize; - - (size, size) - } - - pub fn width(&self) -> u32 { - self.width - } - - pub fn height(&self) -> u32 { - self.height - } - - pub fn bitmap(&self) -> &[u8] { - self.bitmap.as_slice() - } -} - -#[derive(Debug, Clone)] -pub struct LineMetrics { - pub line_ascent: f32, - pub line_descent: f32, - pub line_gap: f32, - pub line_size: f32, -} - -pub struct Glyph { - bitmap: Vec, - uvs: [vertex::TexUV; 4], - rect: Rect, - advance_width: f32, -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub enum Charset { - Greek, - Latin, - Cyrillic, -} - -impl Charset { - pub fn range(&self) -> &'static [Range] { - match self { - Charset::Latin => &[ - 0x0020..0x00FF, - 0x0100..0x017F, - 0x0180..0x024F, - 0x2C60..0x2C7F, - 0xA720..0xA7FF, - 0xAB30..0xAB6F, - ], - Charset::Cyrillic => &[0x0400..0x052F, 0x2DE0..0x2DFF, 0xA640..0xA69F], - Charset::Greek => &[0x0370..0x03FF], - } - } -} - -#[derive(Debug, Default, Clone)] -pub struct Cursor { - pub offset: Option, - pub width: f32, - pub height: f32, -} - -// Note: the following code can be used for debug purposes -/* - let charsets = [ - dotrix::ui::font::Charset::Latin, - dotrix::ui::font::Charset::Cyrillic, - dotrix::ui::font::Charset::Greek, - ]; - let font_bytes = include_bytes!("../resources/fonts/Jura-Regular.ttf") as &[u8]; - let font = dotrix::ui::font::Font::from_bytes(28.0, &charsets, font_bytes); - let atlas = font.atlas(); - let bitmap = atlas.bitmap(); - let width = atlas.width(); - let height = atlas.height(); - - let mut img = image::ImageBuffer::new(width, height); - for (pixel, alpha) in img.pixels_mut().zip(bitmap.iter()) { - *pixel = image::Rgba([0, 0, 0, *alpha]); - } - match img.save("jura-atlas.png") { - Ok(_) => panic!("Saved"), - Err(err) => panic!("Error: {err}"), - } -*/ diff --git a/src/ui/label.rs b/src/ui/label.rs deleted file mode 100644 index e677266c..00000000 --- a/src/ui/label.rs +++ /dev/null @@ -1,84 +0,0 @@ -use dotrix_mesh::Mesh; -use dotrix_types::{vertex, Id, Color}; -use dotrix_gpu as gpu; -use crate::{Rect, State, Widget, overlay, font}; -use crate::style::FontStyle; - -pub struct Label { - rect: Rect, - _mesh: Option, - _texture: Id, -} - -impl Label { - pub fn new<'s>(id: Option<&str>, mut rect: Rect, style: &FontStyle<'s>, text: &str) -> Self { - let mesh = Self::build_mesh(id, &mut rect, style, text); - let texture = style.texture; - Self { - rect, - _mesh: mesh, - _texture: texture, - } - } - - fn build_mesh<'s>(id: Option<&str>, rect: &mut Rect, style: &FontStyle<'s>, text: &str) -> Option { - let text_len = text.len(); - if text_len == 0 { - return None; - } - - let vertices_count = text_len * 4; - let mut positions = Vec::with_capacity(vertices_count); - let colors: Vec = vec![style.color.into(); vertices_count]; - let mut uvs = Vec::with_capacity(vertices_count); - let mut indices = Vec::with_capacity(text_len * 6); - let mut mesh = Mesh::new(id.unwrap_or("label")); - - let mut cursor = font::Cursor::default(); - - for word in text.split_inclusive(&[' ', '\n']) { - style.font.place_word(rect, &mut cursor, word); - - for character in word.chars() { - // positions and uvs - let (ch_pos, ch_uvs) = style.font.tesselate(rect, &mut cursor, character); - positions.extend(&ch_pos); - uvs.extend(&ch_uvs); - - // indices - let indices_offset = indices.len() as u32; - indices.extend_from_slice(&[ - indices_offset + 1, - indices_offset, - indices_offset + 3, - indices_offset + 1, - indices_offset + 3, - indices_offset + 2, - ]); - - } - } - - mesh.set_vertices::(positions); - mesh.set_vertices::(uvs); - mesh.set_vertices::>(colors); - mesh.set_indices(indices); - - - Some(mesh) - } - - pub fn rect(&self) -> &Rect { - &self.rect - } - - pub fn compose( - &mut self, - _state: Option, - _frame_width: f32, - _frame_height: f32, - _scale_factor: f32, - ) -> Option { - todo!("Label::compose is not implemented!"); - } -} \ No newline at end of file diff --git a/src/ui/lib.rs b/src/ui/lib.rs deleted file mode 100644 index a6a4a563..00000000 --- a/src/ui/lib.rs +++ /dev/null @@ -1,450 +0,0 @@ -// pub mod composer; -pub mod context; -pub mod edit; -pub mod font; -pub mod label; -pub mod overlay; -pub mod render; -pub mod style; -pub mod text; -pub mod view; - -use std::collections::HashMap; -use std::time::Instant; - -use dotrix_core as dotrix; -use dotrix_gpu as gpu; -use dotrix_gpu::backend as wgpu; -use dotrix_log as log; - -use dotrix_assets::Assets; -use dotrix_input::Input; -use dotrix_types::{Camera, Frame, Id}; - -pub use edit::Edit; -pub use font::Font; -pub use label::Label; -pub use overlay::{Overlay, Rect, Widget}; -pub use style::{Direction, Style, FontStyle}; -pub use text::Text; -pub use view::View; - -// use composer::Composer; - -const INITIAL_VERTEX_COUNT: u64 = 4 * 64; - -#[derive(Debug, Clone)] -pub enum Value { - None, - Text(String), - Number(i64), - Decimal(f64), -} - -impl Default for Value { - fn default() -> Self { - Self::None - } -} - -#[derive(Clone)] -pub struct State { - last_change: Instant, - vertical_offset: f32, - horizontal_offset: f32, - value: Value, - hovered: bool, - has_focus: bool, -} - -impl State { - pub fn update(&mut self, rect: &Rect, input: &Input) { - log::warn!("State::update is not implemented yet") - } -} - -impl Default for State { - fn default() -> Self { - Self { - last_change: Instant::now(), - vertical_offset: 0.0, - horizontal_offset: 0.0, - value: Value::None, - hovered: false, - has_focus: false, - } - } -} - -#[derive(Default, Debug, Clone, Copy)] -pub struct Viewport { - pub rect: Rect, - pub content_width: f32, - pub content_height: f32, - pub direction: Direction, -} - -impl Viewport { - pub fn new(rect: Rect, direction: Direction) -> Self { - Self { - rect, - direction, - content_width: 0.0, - content_height: 0.0, - } - } - - /// Returns next child empty `Rect` - pub fn place_content(&self) -> Rect { - match self.direction { - Direction::Vertical => Rect { - horizontal: self.rect.horizontal, - vertical: self.rect.vertical + self.content_height, - width: self.rect.width, - height: self.rect.height - self.content_height, - }, - Direction::Horizontal => Rect { - horizontal: self.rect.horizontal + self.content_width, - vertical: self.rect.vertical, - width: self.rect.width - self.content_width, - height: self.content_height, - }, - } - } - - /// Update the view port with child - pub fn append(&mut self, rect: &Rect) { - match self.direction { - Direction::Vertical => { - self.content_height += rect.height; - if self.content_width < rect.width { - self.content_width = rect.width; - } - } - Direction::Horizontal => { - self.content_width += rect.width; - if self.content_height < rect.height { - self.content_height = rect.height; - } - } - } - } -} - -#[derive(Debug, Default, Clone, Copy)] -pub struct Canvas { - pub width: f32, - pub height: f32, - pub scale: f32, -} - -#[derive(Default)] -pub struct Context { - states: HashMap, - canvas: Canvas, - frame_width: f32, - frame_height: f32, - scale_factor: f32, - stack: Vec, - widgets: Vec, - input: Input, -} - -impl Context { - pub fn new() -> Self { - Self::default() - } - - pub fn set(&mut self, input: &Input, frame: &Frame, assets: &Assets) { - self.input = input.clone(); - self.frame_width = frame.width as f32; - self.frame_height = frame.height as f32; - self.scale_factor = frame.scale_factor; - } - - pub fn view(&mut self, id: Option<&str>, style: &Style, callback: F) - where - F: FnOnce(&mut Context), - { - // Get parent viewport - let viewport = self - .stack - .last() - .cloned() - .expect("Overlay stack MUST always contain a viewport"); - - // Calculate rectangle of the next child, taking in account existing content of parent - let rect = viewport.place_content(); - let mut view = View::new(rect.clone(), style); - - // Get viewport with empty content of the current view, taking in account - // its internal direction - self.stack.push(view.viewport()); - - // Run callback for children - callback(self); - - // Retrieve the viewport with updated content size - let viewport = self - .stack - .pop() - .expect("The owner view MUST pop out its viewport"); - - // Update view's rectangle, because now we know its content size - view.update_size(viewport.content_width, viewport.content_height); - - // Update View's state with user inputs - let state = id.map(|id| { - let state = self.states.get(id); - let state = state.cloned(); - let mut state = state.unwrap_or_default(); - - // Here must be handled all the inputs - state.update(&view.inner, &self.input); - - self.states.insert(String::from(id), state.clone()); - state - }); - - // Update parent Viewport's content size with size of this View - { - let viewport = self.stack.last_mut().expect("Parent viewport must exists"); - viewport.append(&view.outer); - } - // If the view has renderable content, make a widget from it - let frame_width = self.frame_width; - let frame_height = self.frame_height; - let scale_factor = self.scale_factor; - if let Some(widget) = view.compose(state, frame_width, frame_height, scale_factor) { - self.widgets.push(widget); - } - } - - pub fn label(&mut self, id: Option<&str>, style: &FontStyle, text: &str) { - // Get parent viewport - let viewport = self - .stack - .last() - .cloned() - .expect("Overlay stack MUST always contain a viewport"); - - // Calculate rectangle of the next child, taking in account existing content of parent - let rect = viewport.place_content(); - let mut label = Label::new(id, rect, style, text); - - // Update View's state with user inputs - let state = id.map(|id| { - let state = self.states.get(id); - let state = state.cloned(); - let mut state = state.unwrap_or_default(); - - // Here must be handled all the inputs - state.update(label.rect(), &self.input); - - self.states.insert(String::from(id), state.clone()); - state - }); - - // Update parent Viewport's content size with size of this View - { - let viewport = self.stack.last_mut().expect("Parent viewport must exists"); - viewport.append(label.rect()); - } - - // If the label has renderable content, make a widget from it - let frame_width = self.frame_width; - let frame_height = self.frame_height; - let scale_factor = self.scale_factor; - if let Some(widget) = label.compose(state, frame_width, frame_height, scale_factor) { - self.widgets.push(widget); - } - } - - pub fn overlay(&mut self, rect: Rect, callback: F) -> Overlay - where - F: FnOnce(&mut Context), - { - let viewport = Viewport::new(rect, Direction::Vertical); - - self.stack.clear(); - self.widgets.clear(); - self.stack.push(viewport); - - callback(self); - - self.stack.pop(); - - let widgets_len = self.widgets.len(); - let widgets = self.widgets.drain(0..widgets_len).collect::>(); - - Overlay { widgets } - } -} - -pub struct DrawTask { - render: Option, - texture_bind_groups: HashMap, wgpu::BindGroup>, -} - -impl dotrix::Task for DrawTask { - type Context = ( - dotrix::Take>, - dotrix::Any, - dotrix::Any, - dotrix::Any, - dotrix::Ref, - ); - type Output = gpu::Commands; - - fn run( - &mut self, - (mut overlay_collection, _camera, frame, input, gpu): Self::Context, - ) -> Self::Output { - let render = self - .render - .get_or_insert_with(|| render::Render::new(&gpu, INITIAL_VERTEX_COUNT)); - - let mut encoder = gpu.encoder(Some("dotrix::ui")); - - let (view, resolve_target) = gpu.color_attachment(); - - render.write_uniform(&gpu, frame.width as f32, frame.height as f32); - - for mut overlay in overlay_collection.drain() { - let mut vertex_buffer_size: u64 = 0; - let mut index_buffer_size: u64 = 0; - let (drawings, vertices, indices) = { - let widgets_len = overlay.widgets.len(); - let mut drawings = Vec::with_capacity(widgets_len); - let (vertices, indices): (Vec<_>, Vec<_>) = overlay - .widgets - .drain(0..widgets_len) - .rev() - .map(|widget| { - let vertices = widget - .mesh - .buffer::() - .expect("Unsupported overlay mesh layout"); - let indices = Vec::from( - widget - .mesh - .indices::() - .expect("Overlay mesh MUST be indexed"), - ); - drawings.push((widget.texture, widget.rect.clone())); - vertex_buffer_size += vertices.len() as u64; - index_buffer_size += indices.len() as u64; - - (vertices, indices) - }) - .unzip(); - (drawings, vertices, indices) - }; - - render.clear_vertex_buffer(&gpu, vertex_buffer_size); - render.clear_index_buffer(&gpu, index_buffer_size); - - render.vertex_buffer.write(&gpu, &vertices); - render.index_buffer.write(&gpu, &indices); - - let drawings = drawings.into_iter().zip( - render - .vertex_buffer - .slices - .iter() - .zip(render.index_buffer.slices.iter()), - ); - - for ((texture_id, rect), (vertex_buffer_slice, index_buffer_slice)) in drawings { - let texture_bind_group = if texture_id.is_null() { - &render.default_texture_bind_group - } else { - if self.texture_bind_groups.get(&texture_id).is_none() { - if let Some(texture) = gpu.get(&texture_id) { - let texture_bind_group = - render.create_texture_bind_group(&gpu, texture); - self.texture_bind_groups - .insert(texture_id, texture_bind_group); - } else { - continue; - } - } - self.texture_bind_groups.get(&texture_id).unwrap() - }; - - encoder.inner.push_debug_group("dotrix::ui::overlay"); - let mut rpass = encoder - .inner - .begin_render_pass(&wgpu::RenderPassDescriptor { - label: None, - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view, - resolve_target, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: true, - }, - })], - depth_stencil_attachment: None, - }); - - rpass.set_scissor_rect( - rect.horizontal.round() as u32, - rect.vertical.round() as u32, - rect.width.round() as u32, - rect.height.round() as u32, - ); - - rpass.set_pipeline(&render.render_pipeline.inner); - rpass.set_bind_group(0, &render.bind_group, &[]); - rpass.set_bind_group(1, texture_bind_group, &[]); - - rpass.set_vertex_buffer( - 0, - render - .vertex_buffer - .buffer - .inner - .slice(vertex_buffer_slice.clone()), - ); - rpass.set_index_buffer( - render - .index_buffer - .buffer - .inner - .slice(index_buffer_slice.clone()), - wgpu::IndexFormat::Uint32, - ); - let indices_count = (index_buffer_slice.end - index_buffer_slice.start) - / std::mem::size_of::() as u64; - - // log::debug!( - // "rpass vertices: {:?}, indices: {}", - // vertex_buffer_slice, - // indices_count - // ); - rpass.draw_indexed(0..indices_count as u32, 0, 0..1); - } - } - - encoder.finish(9000) - } -} - -impl Default for DrawTask { - fn default() -> Self { - Self { - render: None, - texture_bind_groups: HashMap::new(), - } - } -} - -#[derive(Default)] -pub struct Extension {} - -impl dotrix::Extension for Extension { - fn load(&self, manager: &dotrix::Manager) { - manager.schedule(DrawTask::default()) - } -} diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs deleted file mode 100644 index 73ffcd43..00000000 --- a/src/ui/overlay.rs +++ /dev/null @@ -1,143 +0,0 @@ -use crate::style::Spacing; - -use dotrix_gpu as gpu; -use dotrix_log as log; -use dotrix_mesh::Mesh; -use dotrix_types::vertex; -use dotrix_types::{Color, Id}; - -#[derive(Debug, Default, Clone, Copy)] -pub struct Rect { - pub horizontal: f32, - pub vertical: f32, - pub width: f32, - pub height: f32, -} - -pub struct Widget { - pub rect: Rect, - pub mesh: Mesh, - pub texture: Id, -} - -pub struct Overlay { - pub widgets: Vec, - //pub rect: Rect, - //pub view: View, -} - -pub struct Position { - pub value: [f32; 2], -} - -pub type VertexAttributes = (Position, vertex::TexUV, Color); - -pub struct Builder { - pub positions: Vec<[f32; 2]>, - pub uvs: Vec<[f32; 2]>, - pub colors: Vec, - pub indices: Vec, - pub rect: Rect, - pub texture: Id, -} - -impl Rect { - pub fn inner(&self, spacing: &Spacing) -> Rect { - let width = self.width - spacing.left - spacing.right; - let height = self.height - spacing.top - spacing.bottom; - let horizontal = if self.horizontal >= 0.0 { - self.horizontal + spacing.left - } else { - self.horizontal - spacing.right - }; - let vertical = if self.vertical >= 0.0 { - self.vertical + spacing.top - } else { - self.vertical - spacing.bottom - }; - - Rect { - horizontal, - vertical, - width: width.clamp(0.0, self.width), - height: height.clamp(0.0, self.height), - } - } - - pub fn outer(&self, spacing: &Spacing) -> Rect { - let width = self.width + spacing.left + spacing.right; - let height = self.height - spacing.top - spacing.bottom; - let horizontal = if self.horizontal >= 0.0 { - self.horizontal - spacing.left - } else { - self.horizontal + spacing.right - }; - let vertical = if self.vertical >= 0.0 { - self.vertical - spacing.top - } else { - self.vertical + spacing.bottom - }; - - Rect { - horizontal, - vertical, - width: width.clamp(0.0, self.width), - height: height.clamp(0.0, self.height), - } - } -} - -impl Widget { - pub fn new(rect: Rect, mesh: Mesh, texture: Id) -> Self { - if !mesh.contains::() { - panic!("Widget mesh must contain Position, TexUV and Color"); - } - Self { - rect, - mesh, - texture, - } - } -} - -impl vertex::Attribute for Position { - type Raw = [f32; 2]; - fn name() -> &'static str { - "Screen Position" - } - fn format() -> vertex::AttributeFormat { - vertex::AttributeFormat::Float32x2 - } -} - -impl Builder { - pub fn new(rect: Rect, texture: Id) -> Self { - Self { - positions: vec![], - uvs: vec![], - colors: vec![], - indices: vec![], - rect, - texture, - } - } -} - -impl From for Widget { - fn from(builder: Builder) -> Self { - let mut mesh = Mesh::new(String::from("widget")); - mesh.set_vertices::(builder.positions); - mesh.set_vertices::(builder.uvs); - mesh.set_vertices::>(builder.colors); - mesh.set_indices(builder.indices); - - log::debug!("Vertices: {:?}", mesh.vertices::()); - log::debug!("Indices: {:?}", mesh.indices::()); - - Self { - rect: builder.rect, - mesh, - texture: builder.texture, - } - } -} diff --git a/src/ui/render.rs b/src/ui/render.rs deleted file mode 100644 index 463aa597..00000000 --- a/src/ui/render.rs +++ /dev/null @@ -1,359 +0,0 @@ -use std::borrow::Cow; -use std::num::NonZeroU64; -use std::ops::Range; - -use dotrix_gpu as gpu; -use dotrix_gpu::backend as wgpu; -use dotrix_log as log; -use gpu::backend::BindGroupEntry; - -use crate::font; -use crate::overlay::VertexAttributes; - -pub struct Render { - pub render_pipeline: gpu::RenderPipeline, - pub shader_module: gpu::ShaderModule, - pub vertex_buffer: SlicedBuffer, - pub index_buffer: SlicedBuffer, - pub uniform_buffer: gpu::Buffer, - pub uniform_bind_group_layout: gpu::backend::BindGroupLayout, - pub texture_bind_group_layout: gpu::backend::BindGroupLayout, - pub bind_group: gpu::backend::BindGroup, - pub default_texture: gpu::Texture, - pub default_sampler: gpu::backend::Sampler, - pub default_texture_bind_group: gpu::backend::BindGroup, -} - -impl Render { - pub fn new(gpu: &gpu::Gpu, initial_vertex_count: u64) -> Self { - use dotrix_mesh::VertexBufferLayout; - let shader_module = Self::create_shader_module(gpu); - let uniform_buffer = gpu - .buffer("dotrix::ui::uniform_buffer") - .size(std::mem::size_of::() as u64) - .allow_copy_dst() - .use_as_uniform() - .create(); - - let size = VertexAttributes::vertex_size() as u64 * 3 * initial_vertex_count; - let vertex_buffer = SlicedBuffer { - buffer: Self::create_vertex_buffer(gpu, size), - slices: Vec::with_capacity(64), - size, - }; - - let size = std::mem::size_of::() as u64 * 3 * initial_vertex_count; - let index_buffer = SlicedBuffer { - buffer: Self::create_index_buffer(gpu, size), - slices: Vec::with_capacity(64), - size, - }; - - let uniform_bind_group_layout = Self::create_uniform_bind_group_layout(gpu); - let texture_bind_group_layout = Self::create_texture_bind_group_layout(gpu); - let render_pipeline = Self::create_render_pipeline( - gpu, - &shader_module, - None, - &[&uniform_bind_group_layout, &texture_bind_group_layout], - ); - - let bind_group = gpu.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("dotrix::ui::uniform"), - layout: &&uniform_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: uniform_buffer.inner.as_entire_binding(), - }], - }); - - let default_texture = Self::create_default_texture(gpu); - let default_sampler = Self::create_default_sampler(gpu); - let default_texture_bind_group = Self::create_default_texture_bind_group( - gpu, - &texture_bind_group_layout, - &default_texture.create_view(&wgpu::TextureViewDescriptor::default()), - &default_sampler, - ); - - Self { - render_pipeline, - shader_module, - vertex_buffer, - index_buffer, - uniform_buffer, - uniform_bind_group_layout, - texture_bind_group_layout, - bind_group, - default_texture, - default_sampler, - default_texture_bind_group, - } - } - - pub fn clear_vertex_buffer(&mut self, gpu: &gpu::Gpu, size: u64) { - self.vertex_buffer.slices.clear(); - if self.vertex_buffer.size < size { - self.vertex_buffer.buffer = Self::create_vertex_buffer(gpu, size); - self.vertex_buffer.size = size; - } - } - - pub fn clear_index_buffer(&mut self, gpu: &gpu::Gpu, size: u64) { - self.index_buffer.slices.clear(); - if self.index_buffer.size < size { - self.index_buffer.buffer = Self::create_index_buffer(gpu, size); - self.index_buffer.size = size; - } - } - - pub fn write_uniform(&self, gpu: &gpu::Gpu, frame_width: f32, frame_height: f32) { - gpu.write_buffer( - &self.uniform_buffer, - 0, - bytemuck::cast_slice(&[Uniform { - frame_width, - frame_height, - padding: [0; 2], - }]), - ); - } - - fn create_shader_module(gpu: &gpu::Gpu) -> gpu::ShaderModule { - gpu.create_shader_module("dotrix::ui::shader", Cow::Borrowed(include_str!("ui.wgsl"))) - } - - fn create_vertex_buffer(gpu: &gpu::Gpu, size: u64) -> gpu::Buffer { - gpu.buffer("dotrix::ui::vertex_buffer") - .size(size) - .allow_copy_dst() - .use_as_vertex() - .create() - } - - fn create_index_buffer(gpu: &gpu::Gpu, size: u64) -> gpu::Buffer { - gpu.buffer("dotrix::ui::index_buffer") - .size(size) - .allow_copy_dst() - .use_as_index() - .create() - } - - fn create_uniform_bind_group_layout(gpu: &gpu::Gpu) -> wgpu::BindGroupLayout { - gpu.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("dotrix::ui::uniform_bind_group_layout"), - entries: &[gpu::backend::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: gpu::backend::BindingType::Buffer { - has_dynamic_offset: false, - min_binding_size: NonZeroU64::new(std::mem::size_of::() as _), - ty: wgpu::BufferBindingType::Uniform, - }, - count: None, - }], - }) - } - - fn create_texture_bind_group_layout(gpu: &gpu::Gpu) -> wgpu::BindGroupLayout { - gpu.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("dotrix::ui::texture_bind_group_layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: wgpu::TextureViewDimension::D2, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - }) - } - - fn create_render_pipeline( - gpu: &gpu::Gpu, - shader_module: &gpu::ShaderModule, - depth_buffer_format: Option, - bind_group_layouts: &[&wgpu::BindGroupLayout], - ) -> gpu::RenderPipeline { - use dotrix_mesh::VertexBufferLayout; - let pipeline_layout = gpu.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("dotirx::ui::pipeline_layout"), - bind_group_layouts, - push_constant_ranges: &[], - }); - - let depth_stencil = depth_buffer_format.map(|format| wgpu::DepthStencilState { - format, - depth_write_enabled: false, - depth_compare: wgpu::CompareFunction::Always, - stencil: wgpu::StencilState::default(), - bias: wgpu::DepthBiasState::default(), - }); - - let surface_color_format = gpu.surface_format(); - let attributes = VertexAttributes::attributes() - .map( - |(vertex_format, offset, shader_location)| wgpu::VertexAttribute { - format: gpu::map_vertex_format(vertex_format), - offset, - shader_location, - }, - ) - .collect::>(); - - gpu.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("dotrix::ui::render_pipeline"), - layout: Some(&pipeline_layout.inner), - vertex: wgpu::VertexState { - entry_point: "vs_main", - module: &shader_module.inner, - buffers: &[wgpu::VertexBufferLayout { - array_stride: VertexAttributes::vertex_size() as u64, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &attributes, - }], - }, - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - unclipped_depth: false, - conservative: false, - cull_mode: None, - front_face: wgpu::FrontFace::default(), - polygon_mode: wgpu::PolygonMode::default(), - strip_index_format: None, - }, - depth_stencil, - multisample: wgpu::MultisampleState { - alpha_to_coverage_enabled: false, - count: gpu.sample_count(), - mask: !0, - }, - - fragment: Some(wgpu::FragmentState { - module: &shader_module.inner, - entry_point: "fs_main", - targets: &[Some(wgpu::ColorTargetState { - format: surface_color_format, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - }), - multiview: None, - }) - } - - fn create_default_texture(gpu: &gpu::Gpu) -> gpu::Texture { - let default_texture_size = 1; - let default_texture_bytes = [0xFF, 0xFF, 0xFF, 0xFF]; - gpu.texture("dotrix::ui::default_texture") - .size(default_texture_size, default_texture_size) - .allow_copy_dst() - .dimension_d2() - .format_rgba_u8_norm_srgb() - .use_as_texture_binding() - .data(&default_texture_bytes) - .create() - /* TODO: copy that code to application level - let charsets = [ - font::Charset::Latin, - font::Charset::Cyrillic, - font::Charset::Greek, - ]; - let font_bytes = include_bytes!("../../resources/fonts/Jura-Regular.ttf") as &[u8]; - let font = font::Font::from_bytes(28.0, &charsets, font_bytes); - let atlas = font.atlas(); - - gpu.texture("dotrix::ui::default_texture") - .size(atlas.width(), atlas.height()) - .allow_copy_dst() - .dimension_d2() - .format_alpha_u8() - .use_as_texture_binding() - .data(atlas.bitmap()) - .create() - */ - } - - fn create_default_sampler(gpu: &gpu::Gpu) -> wgpu::Sampler { - gpu.create_sampler(&wgpu::SamplerDescriptor { - mag_filter: wgpu::FilterMode::Nearest, - min_filter: wgpu::FilterMode::Nearest, - - ..Default::default() - }) - } - - fn create_default_texture_bind_group( - gpu: &gpu::Gpu, - texture_bind_group_layout: &wgpu::BindGroupLayout, - default_texture_view: &gpu::TextureView, - default_sampler: &wgpu::Sampler, - ) -> wgpu::BindGroup { - gpu.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("dotrix::ui::texture_bind_group"), - layout: texture_bind_group_layout, - entries: &[ - BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&default_texture_view.inner), - }, - BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&default_sampler), - }, - ], - }) - } - - pub fn create_texture_bind_group( - &self, - gpu: &gpu::Gpu, - texture: &gpu::TextureView, - ) -> wgpu::BindGroup { - Self::create_default_texture_bind_group( - gpu, - &self.texture_bind_group_layout, - texture, - &self.default_sampler, - ) - } -} - -#[derive(Clone, Copy, Debug, Default)] -#[repr(C)] -struct Uniform { - frame_width: f32, - frame_height: f32, - padding: [u32; 2], -} - -unsafe impl bytemuck::Pod for Uniform {} -unsafe impl bytemuck::Zeroable for Uniform {} - -pub struct SlicedBuffer { - pub buffer: gpu::Buffer, - pub slices: Vec>, - pub size: u64, -} - -impl SlicedBuffer { - pub fn write(&mut self, gpu: &gpu::Gpu, slices: &[Vec]) { - let mut offset = 0; - for slice in slices.iter() { - let next_offset = offset + slice.len() as u64; - gpu.write_buffer(&self.buffer, offset, slice); - self.slices.push(offset..next_offset); - offset = next_offset; - } - } -} diff --git a/src/ui/style.rs b/src/ui/style.rs deleted file mode 100644 index bf96860f..00000000 --- a/src/ui/style.rs +++ /dev/null @@ -1,134 +0,0 @@ -use dotrix_gpu as gpu; -use dotrix_types::vertex::TexUV; -use dotrix_types::{Color, Id}; - -use crate::Font; - -#[derive(Default)] -pub struct Style { - pub background: Option, - pub direction: Direction, - pub margin: Spacing, - pub padding: Spacing, - pub width: Size, - pub height: Size, -} - -pub struct FontStyle<'f> { - pub texture: Id, - pub font: &'f Font, - pub color: Color, -} - -pub struct Background { - pub color: Corners>, - pub texture: Id, - pub uvs: Corners, -} - -pub struct Corners { - pub top_left: T, - pub top_right: T, - pub bottom_right: T, - pub bottom_left: T, -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum Direction { - Vertical, - Horizontal, -} - -impl Default for Direction { - fn default() -> Self { - Self::Vertical - } -} - -#[derive(Default, Debug, Clone)] -pub struct Spacing { - pub top: f32, - pub right: f32, - pub bottom: f32, - pub left: f32, -} - -impl From for Spacing { - fn from(value: f32) -> Self { - Self { - top: value, - right: value, - bottom: value, - left: value, - } - } -} - -#[derive(Clone, Copy, PartialEq)] -pub enum Size { - /// size required by a widget - Auto, - /// exact size - Px(f32), - /// use available space, according to a factor 0.0..1.0 of the parent size - Grow(f32), -} - -impl Default for Size { - fn default() -> Self { - Size::Auto - } -} - -#[derive(Default, Debug, Clone)] -pub struct Text { - pub font_size: f32, - pub color: Color, -} - -impl Default for Background { - fn default() -> Self { - let white = Color::white(); - let uv = TexUV::default(); - Self { - color: Corners { - top_left: white, - top_right: white, - bottom_right: white, - bottom_left: white, - }, - texture: Id::default(), - uvs: Corners { - top_left: uv, - top_right: uv, - bottom_right: uv, - bottom_left: uv, - }, - } - } -} - -impl Background { - pub fn from_color(color: Color) -> Self { - Self { - color: Corners { - top_left: color, - top_right: color, - bottom_right: color, - bottom_left: color, - }, - ..Default::default() - } - } -} - -impl From<&Corners> for [T; 4] { - fn from(entry: &Corners) -> Self { - [ - entry.top_left.clone(), - entry.top_right.clone(), - entry.bottom_right.clone(), - entry.bottom_left.clone(), - ] - } -} diff --git a/src/ui/text.rs b/src/ui/text.rs deleted file mode 100644 index 7eb51f5d..00000000 --- a/src/ui/text.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::style; -use dotrix_types::Color; - -pub struct Text { - inner: String, - style: style::Text, -} - -impl Text { - pub fn new(value: impl Into) -> Self { - Self { - inner: value.into(), - style: style::Text { - font_size: 18.0, - color: Color::white(), - ..Default::default() - }, - } - } - - pub fn style(mut self, style: style::Text) -> Self { - self.style = style; - self - } -} diff --git a/src/ui/ui.wgsl b/src/ui/ui.wgsl deleted file mode 100644 index 68d04d46..00000000 --- a/src/ui/ui.wgsl +++ /dev/null @@ -1,77 +0,0 @@ -struct VertexOutput { - @location(0) tex_uv: vec2, - @location(1) color: vec4, - @builtin(position) position: vec4, -}; - -struct Overlay { - screen_width: f32, - screen_height: f32, - max_radius: f32, - _padding: u32, -}; -@group(0) @binding(0) var u_overlay: Overlay; - -fn color_from_u32(value: u32) -> vec4 { - return vec4( - f32((value >> 24u) & 255u), - f32((value >> 16u) & 255u), - f32((value >> 8u) & 255u), - f32(value & 255u), - ) / 255.0; -} - -fn position_from_screen(position: vec2) -> vec4 { - return vec4( - 2.0 * position.x / u_overlay.screen_width - 1.0, - 1.0 - 2.0 * position.y / u_overlay.screen_height, - 0.0, - 1.0, - ); -} - -@vertex -fn vs_main( - @builtin(vertex_index) my_index: u32, - @location(0) a_position: vec2, - @location(1) a_tex_uv: vec2, - @location(2) a_color: u32, -) -> VertexOutput { - var out: VertexOutput; - out.tex_uv = a_tex_uv; - out.color = color_from_u32(a_color); - /* - var z: f32 = 0.0; - if my_index > 6u { - z = 1.0; - out.color = vec4(0.0, 0.0, 1.0, 1.0); - } else { - out.color = vec4(1.0, 0.0, 1.0, 0.0); - - } - */ - out.position = position_from_screen(a_position); - return out; -} - -@group(1) @binding(0) var t_texture: texture_2d; -@group(1) @binding(1) var s_sampler: sampler; - -@fragment -fn fs_main( - in: VertexOutput, -) -> @location(0) vec4 { - var color = vec4(in.color.rgba); - - //if in.position.x > 380.0 && in.position.x < 440.0 { - // color = vec4(in.color.rgb, 0.3); - // return vec4(in.color.rga, 0.3); - //} - // We always have an sRGB aware texture at the moment. - let tex_color = textureSample(t_texture, s_sampler, in.tex_uv); - // let tex_gamma = gamma_from_linear_rgba(tex_linear); - // let out_color_gamma = in.color * tex_gamma; - // return vec4(in.color.rgb, 1.0); - // return vec4(1.0, 0.0, 0.0, 1.0); - return color * tex_color; -} diff --git a/src/ui/view.rs b/src/ui/view.rs deleted file mode 100644 index 8a292bc8..00000000 --- a/src/ui/view.rs +++ /dev/null @@ -1,195 +0,0 @@ -use dotrix_gpu as gpu; -// use dotrix_input::Input; -use dotrix_log as log; -use dotrix_mesh::Mesh; -use dotrix_types::{vertex, Color, Frame, Id, TexUV}; -use fontdue::layout::VerticalAlign; - -use crate::{overlay, Direction, Rect, State, Style, Viewport, Widget}; - -pub struct View<'s> { - /// Style reference for the current view - pub style: &'s Style, - /// Outer rectangle - pub outer: Rect, - /// Inner rectangle - pub inner: Rect, -} - -impl<'s> View<'s> { - pub fn new(outer: Rect, style: &'s Style) -> Self { - let inner = outer.inner(&style.padding); - Self { - style, - outer, - inner, - } - } - - pub fn compose( - &mut self, - state: Option, - frame_width: f32, - frame_height: f32, - scale_factor: f32, - ) -> Option { - self.style.background.as_ref().map(|background| { - let (x0, x1) = - Self::calculate_side(frame_width, self.outer.horizontal, self.outer.width); - let (y0, y1) = - Self::calculate_side(frame_height, self.outer.vertical, self.outer.height); - - let positions = vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1]]; - let uvs = vec![ - background.uvs.top_left.pack(), - background.uvs.top_right.pack(), - background.uvs.bottom_right.pack(), - background.uvs.bottom_left.pack(), - ]; - let colors = vec![ - background.color.top_left.into(), - background.color.top_right.into(), - background.color.bottom_right.into(), - background.color.bottom_left.into(), - ]; - let indices = vec![0, 3, 1, 1, 3, 2]; - let mut mesh = Mesh::new("view"); // TODO: use ID here - mesh.set_vertices::(positions); - mesh.set_vertices::(uvs); - mesh.set_vertices::>(colors); - mesh.set_indices(indices); - - Widget { - rect: self.outer.clone(), - mesh, - texture: background.texture, - } - }) - } - - pub fn update_size(&mut self, content_width: f32, content_height: f32) { - self.inner.width = content_width; - self.inner.height = content_height; - match self.style.direction { - Direction::Horizontal => { - let outer_width = - content_width + self.style.padding.left + self.style.padding.right; - if outer_width < self.outer.width { - self.outer.width = outer_width; - } - } - Direction::Vertical => { - let outer_height = - content_height + self.style.padding.top + self.style.padding.bottom; - if outer_height < self.outer.height { - self.outer.height = outer_height; - } - } - }; - } - - pub fn viewport(&self) -> Viewport { - let direction = self.style.direction; - Viewport { - rect: self.inner.clone(), - content_width: 0.0, - content_height: 0.0, - direction, - } - } - - fn calculate_side(frame_side: f32, offset: f32, side: f32) -> (f32, f32) { - if offset < 0.0 { - let v = frame_side + offset; - (v - side, v) - } else { - (offset, offset + side) - } - } -} - -/* -impl Compose for View { - fn compose<'c, 'i, 'f>( - &self, - rect: &Rect, - composer: &mut crate::composer::Composer<'c, 'i, 'f>, - ) { - let outer_rect = rect.inner(&self.style.margin); - let inner_rect = outer_rect.inner(&self.style.padding); - - let pushed = if let Some(background) = self.style.background.as_ref() { - let frame_width = composer.frame.width as f32; - let frame_height = composer.frame.height as f32; - - let (x0, x1) = - Self::calculate_side(frame_width, outer_rect.horizontal, outer_rect.width); - let (y0, y1) = - Self::calculate_side(frame_height, outer_rect.vertical, outer_rect.height); - - let (builder, pushed) = composer.builder(outer_rect, background.texture); - - log::debug!("Builder pushed: {}", pushed); - let vertex_base = builder.positions.len() as u32; - - builder - .positions - .extend_from_slice(&[[x0, y0], [x1, y0], [x1, y1], [x0, y1]]); - builder.uvs.extend_from_slice(&[ - background.uvs.top_left.pack(), - background.uvs.top_right.pack(), - background.uvs.bottom_right.pack(), - background.uvs.bottom_left.pack(), - ]); - builder.colors.extend_from_slice(&[ - background.color.top_left.into(), - background.color.top_right.into(), - background.color.bottom_right.into(), - background.color.bottom_left.into(), - ]); - builder.indices.extend_from_slice(&[ - 0 + vertex_base, - 3 + vertex_base, - 1 + vertex_base, - 1 + vertex_base, - 3 + vertex_base, - 2 + vertex_base, - ]); - pushed - } else { - false - }; - - for child in self.children.iter() { - child.compose(&inner_rect, composer); - } - - if pushed { - if let Some(builder) = composer.builder_pop() { - composer.add_widget(builder.into()) - } - } - } -} - - */ - -/* - pub fn tesselate(mut self, child: T) -> Self { - self.children.push(Box::new(child)); - self - } -*/ - -/* -pub struct Iter<'a> { - view: &'a mut View, - stack: Vec> -} - -impl<'a> Iterator for Iter<'a> { - type Item = Widget; - fn next(&mut self) -> Option { - - } -}*/ diff --git a/src/window.rs b/src/window.rs index 2c7acddc..10a29763 100644 --- a/src/window.rs +++ b/src/window.rs @@ -239,6 +239,12 @@ impl winit::application::ApplicationHandler for EventLoop { }; self.task_manager.provide(input_event); } + winit::event::WindowEvent::CursorMoved { position, .. } => { + self.task_manager.provide(event::Event::CursorPosition { + horizontal: position.x, + vertical: position.y, + }); + } winit::event::WindowEvent::MouseInput { state, button, .. } => { let mouse_button = event::Button::from(&button); let input_event = match state { diff --git a/src/window/input.rs b/src/window/input.rs index 9d3ae5e1..a2b081db 100644 --- a/src/window/input.rs +++ b/src/window/input.rs @@ -108,6 +108,8 @@ impl Task for ReadInput { list.push(event); } + self.mouse_position = mouse_position; + Input { events: list, modifiers: self.modifiers,