rhai
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
2691
Cargo.lock
generated
Normal file
2691
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "gui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bytemuck = "1.23.1"
|
||||
glam = { version = "0.30.4", features = ["bytemuck"] }
|
||||
pollster = "0.4.0"
|
||||
wgpu = "25.0.2"
|
||||
winit = "0.30.11"
|
||||
rhai = { git = "https://github.com/rhaiscript/rhai", features = ["f32_float"] }
|
||||
notify = "8.0.0"
|
||||
|
||||
15
data/test.rhai
Normal file
15
data/test.rhai
Normal file
@@ -0,0 +1,15 @@
|
||||
let a = rect();
|
||||
a.top_left = anchor_offset(0.5, 0.5, -200.0, -100.0);
|
||||
a.bottom_right = anchor_offset(0.5, 0.5, 100.0, 100.0);
|
||||
a.radius = 40.0;
|
||||
a.inner_radius = 30.0;
|
||||
a.thickness = 10.0;
|
||||
|
||||
let b = rect();
|
||||
b.top_left = anchor_offset(0.25, 0.25, -100.0, -100.0);
|
||||
b.bottom_right = anchor_offset(0.25, 0.25, 100.0, 100.0);
|
||||
b.radius = 40.0;
|
||||
b.inner_radius = 30.0;
|
||||
b.thickness = 10.0;
|
||||
|
||||
[a, b]
|
||||
36
src/app.rs
Normal file
36
src/app.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use winit::{
|
||||
application::ApplicationHandler,
|
||||
event::WindowEvent,
|
||||
event_loop::{ActiveEventLoop, EventLoop},
|
||||
window::{Window, WindowId},
|
||||
};
|
||||
|
||||
use super::Client;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct App {
|
||||
client: Option<Client>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn run() {
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
event_loop.run_app(&mut App::default()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler for App {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.client.is_none() {
|
||||
let window = event_loop
|
||||
.create_window(Window::default_attributes())
|
||||
.unwrap();
|
||||
let client = Client::new(window.into());
|
||||
self.client = Some(client);
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
|
||||
self.client.as_mut().unwrap().event(event, event_loop);
|
||||
}
|
||||
}
|
||||
36
src/main.rs
Normal file
36
src/main.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use app::App;
|
||||
use render::Renderer;
|
||||
use winit::{event::WindowEvent, event_loop::ActiveEventLoop, window::Window};
|
||||
|
||||
mod app;
|
||||
mod render;
|
||||
|
||||
fn main() {
|
||||
App::run();
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
window: Arc<Window>,
|
||||
renderer: Renderer,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(window: Arc<Window>) -> Self {
|
||||
let renderer = Renderer::new(window.clone());
|
||||
Self {
|
||||
window,
|
||||
renderer,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
|
||||
match event {
|
||||
WindowEvent::CloseRequested => event_loop.exit(),
|
||||
WindowEvent::RedrawRequested => self.renderer.draw(),
|
||||
WindowEvent::Resized(size) => self.renderer.resize(&size),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
199
src/render/mod.rs
Normal file
199
src/render/mod.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use std::sync::{
|
||||
mpsc::{channel, Receiver},
|
||||
Arc,
|
||||
};
|
||||
|
||||
use notify::{
|
||||
event::{CreateKind, ModifyKind},
|
||||
EventKind, RecommendedWatcher, Watcher,
|
||||
};
|
||||
use pollster::FutureExt;
|
||||
use primitive::{RoundedRect, UIPos};
|
||||
use shape::ShapePipeline;
|
||||
use wgpu::util::StagingBelt;
|
||||
use winit::{dpi::PhysicalSize, window::Window};
|
||||
|
||||
pub mod primitive;
|
||||
mod shape;
|
||||
|
||||
pub const CLEAR_COLOR: wgpu::Color = wgpu::Color::BLACK;
|
||||
|
||||
pub struct Renderer {
|
||||
surface: wgpu::Surface<'static>,
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
config: wgpu::SurfaceConfiguration,
|
||||
adapter: wgpu::Adapter,
|
||||
encoder: wgpu::CommandEncoder,
|
||||
staging_belt: StagingBelt,
|
||||
shape_pipeline: ShapePipeline,
|
||||
recv: Receiver<Vec<RoundedRect>>,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
pub fn new(window: Arc<Window>) -> Self {
|
||||
let size = window.inner_size();
|
||||
|
||||
let (send, recv) = channel();
|
||||
|
||||
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("data/test.rhai");
|
||||
let w2 = window.clone();
|
||||
std::thread::spawn(move || {
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let mut watcher = RecommendedWatcher::new(tx, Default::default()).unwrap();
|
||||
watcher
|
||||
.watch("data".as_ref(), notify::RecursiveMode::Recursive)
|
||||
.unwrap();
|
||||
|
||||
for res in rx {
|
||||
let Ok(ev) = res else {
|
||||
continue;
|
||||
};
|
||||
if !ev.paths.contains(&path) {
|
||||
continue;
|
||||
}
|
||||
if !matches!(
|
||||
ev.kind,
|
||||
EventKind::Create(CreateKind::File) | EventKind::Modify(ModifyKind::Data(_))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
println!("reloaded {ev:?}");
|
||||
let mut engine = rhai::Engine::new();
|
||||
engine.build_type::<RoundedRect>();
|
||||
engine.register_type::<UIPos>();
|
||||
engine.register_fn("anchor_offset", UIPos::anchor_offset);
|
||||
engine.register_fn("rect", RoundedRect::default);
|
||||
let Ok(code) = std::fs::read_to_string(&path) else {
|
||||
continue;
|
||||
};
|
||||
let rects = engine
|
||||
.eval::<rhai::Array>(&code)
|
||||
.map_err(|e| println!("{e:?}"))
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|d| match rhai::Dynamic::try_cast(d.clone()) {
|
||||
Some(v) => Some(v),
|
||||
None => {
|
||||
println!("{d:?} is not a RoundedRect");
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<RoundedRect>>();
|
||||
send.send(rects).unwrap();
|
||||
w2.request_redraw();
|
||||
}
|
||||
});
|
||||
|
||||
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
backends: wgpu::Backends::PRIMARY,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let surface = instance
|
||||
.create_surface(window.clone())
|
||||
.expect("Could not create window surface!");
|
||||
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::default(),
|
||||
compatible_surface: Some(&surface),
|
||||
force_fallback_adapter: false,
|
||||
})
|
||||
.block_on()
|
||||
.expect("Could not get adapter!");
|
||||
|
||||
let (device, queue) = adapter
|
||||
.request_device(&wgpu::DeviceDescriptor {
|
||||
required_features: wgpu::Features::empty(),
|
||||
required_limits: wgpu::Limits::default(),
|
||||
..Default::default()
|
||||
})
|
||||
.block_on()
|
||||
.expect("Could not get device!");
|
||||
|
||||
let surface_caps = surface.get_capabilities(&adapter);
|
||||
let surface_format = surface_caps
|
||||
.formats
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|f| f.is_srgb())
|
||||
.unwrap_or(surface_caps.formats[0]);
|
||||
|
||||
let config = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format: surface_format,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
present_mode: surface_caps.present_modes[0],
|
||||
alpha_mode: surface_caps.alpha_modes[0],
|
||||
desired_maximum_frame_latency: 2,
|
||||
view_formats: vec![],
|
||||
};
|
||||
|
||||
surface.configure(&device, &config);
|
||||
|
||||
let staging_belt = StagingBelt::new(4096 * 4);
|
||||
let encoder = Self::create_encoder(&device);
|
||||
|
||||
let shape_pipeline = ShapePipeline::new(&device, &config);
|
||||
|
||||
Self {
|
||||
surface,
|
||||
device,
|
||||
queue,
|
||||
config,
|
||||
adapter,
|
||||
encoder,
|
||||
staging_belt,
|
||||
shape_pipeline,
|
||||
recv,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_encoder(device: &wgpu::Device) -> wgpu::CommandEncoder {
|
||||
device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("Render Encoder"),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn draw(&mut self) {
|
||||
if let Some(rects) = self.recv.try_iter().last() {
|
||||
self.shape_pipeline
|
||||
.update(&self.device, &self.queue, &rects);
|
||||
}
|
||||
let output = self.surface.get_current_texture().unwrap();
|
||||
let view = output
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let mut encoder = std::mem::replace(&mut self.encoder, Self::create_encoder(&self.device));
|
||||
{
|
||||
let render_pass = &mut encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(CLEAR_COLOR),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
..Default::default()
|
||||
});
|
||||
self.shape_pipeline.draw(render_pass);
|
||||
}
|
||||
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
self.staging_belt.finish();
|
||||
output.present();
|
||||
self.staging_belt.recall();
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
|
||||
self.config.width = size.width;
|
||||
self.config.height = size.height;
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
self.shape_pipeline.resize(size, &self.queue);
|
||||
}
|
||||
}
|
||||
73
src/render/primitive.rs
Normal file
73
src/render/primitive.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use glam::Vec2;
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, CustomType)]
|
||||
pub struct RoundedRect {
|
||||
pub top_left: UIPos,
|
||||
pub bottom_right: UIPos,
|
||||
pub colors: [[f32; 4]; 4],
|
||||
pub radius: f32,
|
||||
pub inner_radius: f32,
|
||||
pub thickness: f32,
|
||||
}
|
||||
|
||||
impl Default for RoundedRect {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
top_left: Default::default(),
|
||||
bottom_right: Default::default(),
|
||||
colors: [
|
||||
[1.0, 0.0, 0.0, 1.0],
|
||||
[0.0, 1.0, 0.0, 1.0],
|
||||
[0.0, 0.0, 1.0, 1.0],
|
||||
[1.0, 1.0, 1.0, 1.0],
|
||||
],
|
||||
radius: Default::default(),
|
||||
inner_radius: Default::default(),
|
||||
thickness: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone)]
|
||||
pub struct Text {
|
||||
pub content: String,
|
||||
pub align: Align,
|
||||
pub pos: Vec2,
|
||||
pub bounds: (f32, f32),
|
||||
}
|
||||
|
||||
impl Text {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
content: String::new(),
|
||||
align: Align::Left,
|
||||
pos: Vec2::default(),
|
||||
bounds: (0.0, 0.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
||||
pub struct UIPos {
|
||||
pub anchor: Vec2,
|
||||
pub offset: Vec2,
|
||||
}
|
||||
|
||||
impl UIPos {
|
||||
pub fn anchor_offset(anchor_x: f32, anchor_y: f32, offset_x: f32, offset_y: f32) -> Self {
|
||||
Self {
|
||||
anchor: Vec2::new(anchor_x, anchor_y),
|
||||
offset: Vec2::new(offset_x, offset_y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone)]
|
||||
pub enum Align {
|
||||
Left,
|
||||
Right,
|
||||
Center,
|
||||
}
|
||||
74
src/render/shape/data.rs
Normal file
74
src/render/shape/data.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use wgpu::{RenderPass, VertexAttribute};
|
||||
|
||||
use crate::render::primitive::RoundedRect;
|
||||
|
||||
pub struct RoundedRectBuffer {
|
||||
buffer: wgpu::Buffer,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
||||
pub struct WindowUniform {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
|
||||
impl RoundedRectBuffer {
|
||||
pub fn new(device: &wgpu::Device) -> Self {
|
||||
Self {
|
||||
buffer: Self::init_buf(device, 0),
|
||||
len: 0,
|
||||
}
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
rects: &[RoundedRect],
|
||||
) {
|
||||
if self.len != rects.len() {
|
||||
self.len = rects.len();
|
||||
self.buffer = Self::init_buf(device, std::mem::size_of_val(rects));
|
||||
}
|
||||
queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(rects));
|
||||
}
|
||||
fn init_buf(device: &wgpu::Device, size: usize) -> wgpu::Buffer {
|
||||
device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Instance Buffer"),
|
||||
size: size as u64,
|
||||
mapped_at_creation: false,
|
||||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||
})
|
||||
}
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
pub fn set_in<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
||||
pass.set_vertex_buffer(0, self.buffer.slice(..));
|
||||
}
|
||||
}
|
||||
|
||||
impl RoundedRect {
|
||||
const ATTRIBS: [VertexAttribute; 11] = wgpu::vertex_attr_array![
|
||||
0 => Float32x2,
|
||||
1 => Float32x2,
|
||||
2 => Float32x2,
|
||||
3 => Float32x2,
|
||||
4 => Float32x4,
|
||||
5 => Float32x4,
|
||||
6 => Float32x4,
|
||||
7 => Float32x4,
|
||||
8 => Float32,
|
||||
9 => Float32,
|
||||
10 => Float32,
|
||||
];
|
||||
pub fn desc() -> wgpu::VertexBufferLayout<'static> {
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<RoundedRect>() as wgpu::BufferAddress,
|
||||
step_mode: wgpu::VertexStepMode::Instance,
|
||||
attributes: &Self::ATTRIBS,
|
||||
}
|
||||
}
|
||||
}
|
||||
106
src/render/shape/layout.rs
Normal file
106
src/render/shape/layout.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use wgpu::{
|
||||
util::{BufferInitDescriptor, DeviceExt},
|
||||
BufferUsages,
|
||||
};
|
||||
|
||||
use crate::render::primitive::RoundedRect;
|
||||
|
||||
use super::{
|
||||
data::{RoundedRectBuffer, WindowUniform},
|
||||
ShapeBuffers, ShapePipeline, SHAPE_SHADER,
|
||||
};
|
||||
|
||||
impl ShapePipeline {
|
||||
pub fn new(device: &wgpu::Device, config: &wgpu::SurfaceConfiguration) -> Self {
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("UI Shape Shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(SHAPE_SHADER.into()),
|
||||
});
|
||||
|
||||
let window_uniform = WindowUniform::default();
|
||||
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
|
||||
label: Some("Camera Buffer"),
|
||||
contents: bytemuck::cast_slice(&[window_uniform]),
|
||||
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
||||
});
|
||||
|
||||
let instance_buffer = RoundedRectBuffer::new(device);
|
||||
|
||||
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
label: Some("camera_bind_group_layout"),
|
||||
});
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
layout: &bind_group_layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: window_buffer.as_entire_binding(),
|
||||
}],
|
||||
label: Some("camera_bind_group"),
|
||||
});
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("UI Shape Pipeline Layout"),
|
||||
bind_group_layouts: &[&bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("UI Shape Pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[RoundedRect::desc()],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: config.format,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleStrip,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Cw,
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
unclipped_depth: false,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
let buffers = ShapeBuffers {
|
||||
window: window_buffer,
|
||||
instance: instance_buffer,
|
||||
};
|
||||
|
||||
Self {
|
||||
bind_group,
|
||||
pipeline,
|
||||
buffers,
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/render/shape/mod.rs
Normal file
44
src/render/shape/mod.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use crate::render::primitive::RoundedRect;
|
||||
use data::{RoundedRectBuffer, WindowUniform};
|
||||
use wgpu::{BindGroup, Buffer, RenderPass, RenderPipeline};
|
||||
use winit::dpi::PhysicalSize;
|
||||
|
||||
mod data;
|
||||
mod layout;
|
||||
|
||||
pub const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
|
||||
|
||||
pub struct ShapeBuffers {
|
||||
pub window: Buffer,
|
||||
pub instance: RoundedRectBuffer,
|
||||
}
|
||||
|
||||
pub struct ShapePipeline {
|
||||
pub bind_group: BindGroup,
|
||||
pub pipeline: RenderPipeline,
|
||||
|
||||
pub buffers: ShapeBuffers,
|
||||
}
|
||||
|
||||
impl ShapePipeline {
|
||||
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &self.bind_group, &[]);
|
||||
if self.buffers.instance.len() != 0 {
|
||||
self.buffers.instance.set_in(pass);
|
||||
pass.draw(0..4, 0..self.buffers.instance.len() as u32);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, rects: &[RoundedRect]) {
|
||||
self.buffers.instance.update(device, queue, rects);
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: &PhysicalSize<u32>, queue: &wgpu::Queue) {
|
||||
let slice = &[WindowUniform {
|
||||
width: size.width as f32,
|
||||
height: size.height as f32,
|
||||
}];
|
||||
queue.write_buffer(&self.buffers.window, 0, bytemuck::cast_slice(slice));
|
||||
}
|
||||
}
|
||||
97
src/render/shape/shader.wgsl
Normal file
97
src/render/shape/shader.wgsl
Normal file
@@ -0,0 +1,97 @@
|
||||
|
||||
// vertex shader
|
||||
|
||||
struct VertexOutput {
|
||||
@location(0) color: vec4<f32>,
|
||||
@location(1) center: vec2<f32>,
|
||||
@location(2) corner: vec2<f32>,
|
||||
@location(3) radius: f32,
|
||||
@location(4) inner_radius: f32,
|
||||
@location(5) thickness: f32,
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
};
|
||||
|
||||
struct WindowUniform {
|
||||
dim: vec2<f32>,
|
||||
};
|
||||
|
||||
struct InstanceInput {
|
||||
@location(0) top_left_anchor: vec2<f32>,
|
||||
@location(1) top_left_offset: vec2<f32>,
|
||||
@location(2) bottom_right_anchor: vec2<f32>,
|
||||
@location(3) bottom_right_offset: vec2<f32>,
|
||||
@location(4) top_right_color: vec4<f32>,
|
||||
@location(5) top_left_color: vec4<f32>,
|
||||
@location(6) bottom_right_color: vec4<f32>,
|
||||
@location(7) bottom_left_color: vec4<f32>,
|
||||
@location(8) radius: f32,
|
||||
@location(9) inner_radius: f32,
|
||||
@location(10) thickness: f32,
|
||||
}
|
||||
|
||||
@group(0) @binding(0)
|
||||
var<uniform> window: WindowUniform;
|
||||
|
||||
@vertex
|
||||
fn vs_main(
|
||||
@builtin(vertex_index) vi: u32,
|
||||
in: InstanceInput,
|
||||
) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
|
||||
let top_left = in.top_left_anchor * window.dim + in.top_left_offset;
|
||||
let bottom_right = in.bottom_right_anchor * window.dim + in.bottom_right_offset;
|
||||
let size = bottom_right - top_left;
|
||||
|
||||
var pos = top_left + vec2<f32>(
|
||||
f32(vi % 2u),
|
||||
f32(vi / 2u)
|
||||
) * size;
|
||||
pos = pos / window.dim * 2.0 - 1.0;
|
||||
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
|
||||
|
||||
if vi == 0u {
|
||||
out.color = in.top_left_color;
|
||||
} else if vi == 1u {
|
||||
out.color = in.top_right_color;
|
||||
} else if vi == 2u {
|
||||
out.color = in.bottom_left_color;
|
||||
} else if vi == 3u {
|
||||
out.color = in.bottom_right_color;
|
||||
}
|
||||
|
||||
out.corner = size / 2.0;
|
||||
out.center = top_left + out.corner;
|
||||
out.radius = in.radius;
|
||||
out.inner_radius = in.inner_radius;
|
||||
out.thickness = in.thickness;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(
|
||||
in: VertexOutput
|
||||
) -> @location(0) vec4<f32> {
|
||||
var color = in.color;
|
||||
|
||||
let edge = 0.5;
|
||||
|
||||
let dist = distance_from_rect(in.clip_position.xy, in.center, in.corner, in.radius);
|
||||
color.a *= 1.0 - smoothstep(-min(edge, in.radius), edge, dist);
|
||||
|
||||
if in.thickness > 0.0 {
|
||||
let dist2 = distance_from_rect(in.clip_position.xy, in.center, in.corner - in.thickness, in.inner_radius);
|
||||
color.a *= smoothstep(-min(edge, in.inner_radius), edge, dist2);
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, radius: f32) -> f32 {
|
||||
// vec from center to pixel
|
||||
let p = pixel_pos - rect_center;
|
||||
// vec from inner rect corner to pixel
|
||||
let q = abs(p) - (rect_corner - radius);
|
||||
return length(max(q, vec2<f32>(0.0, 0.0))) - radius;
|
||||
}
|
||||
1
src/render/text/mod.rs
Normal file
1
src/render/text/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod pipeline;
|
||||
127
src/render/text/pipeline.rs
Normal file
127
src/render/text/pipeline.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use glyphon::{
|
||||
Attrs, Buffer, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, TextArea,
|
||||
TextAtlas, TextBounds, TextRenderer,
|
||||
};
|
||||
use wgpu::{MultisampleState, RenderPass};
|
||||
|
||||
use crate::{
|
||||
client::ui::element::Align,
|
||||
render::{primitive::TextPrimitive, surface::RenderSurface},
|
||||
};
|
||||
|
||||
pub struct TextPipeline {
|
||||
pub renderer: glyphon::TextRenderer,
|
||||
pub font_system: glyphon::FontSystem,
|
||||
pub atlas: glyphon::TextAtlas,
|
||||
pub cache: glyphon::SwashCache,
|
||||
pub text_buffers: Vec<glyphon::Buffer>,
|
||||
pub old_text: Vec<TextPrimitive>,
|
||||
}
|
||||
|
||||
impl TextPipeline {
|
||||
pub fn new(surface: &RenderSurface) -> Self {
|
||||
let RenderSurface {
|
||||
device,
|
||||
config,
|
||||
queue,
|
||||
..
|
||||
} = surface;
|
||||
|
||||
let font_system = FontSystem::new();
|
||||
let cache = SwashCache::new();
|
||||
let mut atlas = TextAtlas::new(&device, &queue, config.format);
|
||||
let renderer = TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None);
|
||||
|
||||
Self {
|
||||
font_system,
|
||||
atlas,
|
||||
cache,
|
||||
renderer,
|
||||
text_buffers: Vec::new(),
|
||||
old_text: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
||||
self.renderer.render(&self.atlas, pass).unwrap();
|
||||
}
|
||||
|
||||
pub fn update(&mut self, surface: &RenderSurface, text: &[TextPrimitive]) {
|
||||
let buffers = &mut self.text_buffers;
|
||||
if buffers.len() < text.len() {
|
||||
self.old_text.resize(text.len(), TextPrimitive::empty());
|
||||
buffers.resize_with(text.len(), || {
|
||||
Buffer::new(&mut self.font_system, Metrics::new(20.0, 25.0))
|
||||
})
|
||||
}
|
||||
for ((buffer, text), old) in buffers.iter_mut().zip(text).zip(&mut self.old_text) {
|
||||
if text != old {
|
||||
*old = text.clone();
|
||||
buffer.set_size(&mut self.font_system, f32::MAX, f32::MAX);
|
||||
buffer.set_text(
|
||||
&mut self.font_system,
|
||||
&text.content,
|
||||
Attrs::new().family(Family::SansSerif),
|
||||
Shaping::Basic,
|
||||
);
|
||||
}
|
||||
}
|
||||
let color = Color::rgb(255, 255, 255);
|
||||
let areas = buffers.iter().zip(text).map(|(buffer, text)| {
|
||||
let width = measure(&buffer).0;
|
||||
let mut left = text.pos.x
|
||||
- match text.align {
|
||||
Align::Left => 0.0,
|
||||
Align::Center => width / 2.0,
|
||||
Align::Right => width,
|
||||
};
|
||||
let x = text.pos.x;
|
||||
let w = text.bounds.0;
|
||||
let x_bounds = match text.align {
|
||||
Align::Left => (x, x + w),
|
||||
Align::Center => (x - w / 2.0, x + w / 2.0),
|
||||
Align::Right => (x - w, x),
|
||||
};
|
||||
if left < x_bounds.0 {
|
||||
left = x_bounds.0;
|
||||
}
|
||||
TextArea {
|
||||
buffer: &buffer,
|
||||
left,
|
||||
top: text.pos.y,
|
||||
scale: 1.0,
|
||||
bounds: TextBounds {
|
||||
left: x_bounds.0 as i32,
|
||||
top: text.pos.y as i32,
|
||||
right: x_bounds.1 as i32,
|
||||
bottom: (text.pos.y + text.bounds.1) as i32,
|
||||
},
|
||||
default_color: color,
|
||||
}
|
||||
});
|
||||
self.renderer
|
||||
.prepare(
|
||||
&surface.device,
|
||||
&surface.queue,
|
||||
&mut self.font_system,
|
||||
&mut self.atlas,
|
||||
Resolution {
|
||||
width: surface.config.width,
|
||||
height: surface.config.height,
|
||||
},
|
||||
areas,
|
||||
&mut self.cache,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn measure(buffer: &glyphon::Buffer) -> (f32, f32) {
|
||||
let (width, total_lines) = buffer
|
||||
.layout_runs()
|
||||
.fold((0.0, 0usize), |(width, total_lines), run| {
|
||||
(run.line_w.max(width), total_lines + 1)
|
||||
});
|
||||
|
||||
(width, total_lines as f32 * buffer.metrics().line_height)
|
||||
}
|
||||
129
src/render/texture/init.rs
Normal file
129
src/render/texture/init.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use wgpu::{
|
||||
util::DeviceExt,
|
||||
BindGroup, BindGroupLayout, Device, Queue,
|
||||
};
|
||||
|
||||
use crate::render::surface::RenderSurface;
|
||||
|
||||
use super::{
|
||||
pipeline::{TexturePipeline, TEXTURE_SHADER},
|
||||
vertex::{TextureVertex, TEXTURE_VERTICES}, texture::GameTexture,
|
||||
};
|
||||
|
||||
impl TexturePipeline {
|
||||
pub fn new(surface: &RenderSurface) -> Self {
|
||||
let RenderSurface {
|
||||
device,
|
||||
config,
|
||||
queue,
|
||||
..
|
||||
} = surface;
|
||||
|
||||
let (bind_group_layout, diffuse_bind_group) = Self::init_textures(device, queue);
|
||||
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("UI Texture Shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(TEXTURE_SHADER.into()),
|
||||
});
|
||||
|
||||
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Texture Vertex Buffer"),
|
||||
contents: bytemuck::cast_slice(TEXTURE_VERTICES),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("UI Texture Pipeline Layout"),
|
||||
bind_group_layouts: &[&bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("UI Texture Pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: "vs_main",
|
||||
buffers: &[TextureVertex::desc()],
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: "fs_main",
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: config.format,
|
||||
blend: Some(wgpu::BlendState::REPLACE),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleStrip,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
unclipped_depth: false,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview: None,
|
||||
});
|
||||
|
||||
Self {
|
||||
pipeline,
|
||||
vertex_buffer,
|
||||
diffuse_bind_group,
|
||||
}
|
||||
}
|
||||
|
||||
fn init_textures(device: &Device, queue: &Queue) -> (BindGroupLayout, BindGroup) {
|
||||
let diffuse_bytes = include_bytes!("./textures/happy-tree.png");
|
||||
let diffuse_texture =
|
||||
GameTexture::from_bytes(&device, &queue, diffuse_bytes, "happy-tree.png").unwrap();
|
||||
|
||||
let texture_bind_group_layout =
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
multisampled: false,
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
// This should match the filterable field of the
|
||||
// corresponding Texture entry above.
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
label: Some("texture_bind_group_layout"),
|
||||
});
|
||||
|
||||
let diffuse_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
layout: &texture_bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&diffuse_texture.view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(&diffuse_texture.sampler),
|
||||
},
|
||||
],
|
||||
label: Some("diffuse_bind_group"),
|
||||
});
|
||||
|
||||
(texture_bind_group_layout, diffuse_bind_group)
|
||||
}
|
||||
}
|
||||
22
src/render/texture/mod.rs
Normal file
22
src/render/texture/mod.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
mod init;
|
||||
mod vertex;
|
||||
mod texture;
|
||||
|
||||
use wgpu::{RenderPass, RenderPipeline};
|
||||
|
||||
pub const TEXTURE_SHADER: &str = include_str!("./shader.wgsl");
|
||||
|
||||
pub struct TexturePipeline {
|
||||
pub pipeline: RenderPipeline,
|
||||
pub vertex_buffer: wgpu::Buffer,
|
||||
pub diffuse_bind_group: wgpu::BindGroup,
|
||||
}
|
||||
|
||||
impl TexturePipeline {
|
||||
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &self.diffuse_bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
|
||||
pass.draw(0..4, 0..1);
|
||||
}
|
||||
}
|
||||
34
src/render/texture/shader.wgsl
Normal file
34
src/render/texture/shader.wgsl
Normal file
@@ -0,0 +1,34 @@
|
||||
// Vertex shader
|
||||
|
||||
struct VertexInput {
|
||||
@location(0) position: vec2<f32>,
|
||||
@location(1) tex_coords: vec2<f32>,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) tex_coords: vec2<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(
|
||||
model: VertexInput,
|
||||
) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
out.tex_coords = model.tex_coords;
|
||||
out.clip_position = vec4<f32>(model.position, 0.0, 1.0);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Fragment shader
|
||||
|
||||
@group(0) @binding(0)
|
||||
var t_diffuse: texture_2d<f32>;
|
||||
@group(0)@binding(1)
|
||||
var s_diffuse: sampler;
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return textureSample(t_diffuse, s_diffuse, in.tex_coords);
|
||||
}
|
||||
|
||||
78
src/render/texture/texture.rs
Normal file
78
src/render/texture/texture.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use image::{GenericImageView, ImageResult};
|
||||
|
||||
pub struct GameTexture {
|
||||
pub texture: wgpu::Texture,
|
||||
pub view: wgpu::TextureView,
|
||||
pub sampler: wgpu::Sampler,
|
||||
}
|
||||
|
||||
impl GameTexture {
|
||||
pub fn from_bytes(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
bytes: &[u8],
|
||||
label: &str
|
||||
) -> ImageResult<Self> {
|
||||
let img = image::load_from_memory(bytes)?;
|
||||
Self::from_image(device, queue, &img, Some(label))
|
||||
}
|
||||
|
||||
pub fn from_image(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
img: &image::DynamicImage,
|
||||
label: Option<&str>
|
||||
) -> ImageResult<Self> {
|
||||
let rgba = img.to_rgba8();
|
||||
let dimensions = img.dimensions();
|
||||
|
||||
let size = wgpu::Extent3d {
|
||||
width: dimensions.0,
|
||||
height: dimensions.1,
|
||||
depth_or_array_layers: 1,
|
||||
};
|
||||
let texture = device.create_texture(
|
||||
&wgpu::TextureDescriptor {
|
||||
label,
|
||||
size,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
}
|
||||
);
|
||||
|
||||
queue.write_texture(
|
||||
wgpu::ImageCopyTexture {
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
texture: &texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
},
|
||||
&rgba,
|
||||
wgpu::ImageDataLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(4 * dimensions.0),
|
||||
rows_per_image: Some(dimensions.1),
|
||||
},
|
||||
size,
|
||||
);
|
||||
|
||||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let sampler = device.create_sampler(
|
||||
&wgpu::SamplerDescriptor {
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
min_filter: wgpu::FilterMode::Nearest,
|
||||
mipmap_filter: wgpu::FilterMode::Nearest,
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
|
||||
Ok(Self { texture, view, sampler })
|
||||
}
|
||||
}
|
||||
BIN
src/render/texture/textures/happy-tree.png
Normal file
BIN
src/render/texture/textures/happy-tree.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
35
src/render/texture/vertex.rs
Normal file
35
src/render/texture/vertex.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct TextureVertex {
|
||||
position: [f32; 2],
|
||||
tex_coords: [f32; 2],
|
||||
}
|
||||
|
||||
impl TextureVertex {
|
||||
const ATTRIBS: [wgpu::VertexAttribute; 2] =
|
||||
wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2];
|
||||
|
||||
pub fn desc() -> wgpu::VertexBufferLayout<'static> {
|
||||
use std::mem;
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: mem::size_of::<TextureVertex>() as wgpu::BufferAddress,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &Self::ATTRIBS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const TEXTURE_VERTICES: &[TextureVertex] = &[
|
||||
TextureVertex { position: [0.0, 0.0], tex_coords: [0.0, 1.0], },
|
||||
TextureVertex { position: [0.5, 0.0], tex_coords: [1.0, 1.0], },
|
||||
TextureVertex { position: [0.0, 0.5], tex_coords: [0.0, 0.0], },
|
||||
TextureVertex { position: [0.5, 0.5], tex_coords: [1.0, 0.0], },
|
||||
];
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct ShapeVertex {
|
||||
position: [f32; 2],
|
||||
tex_coords: [f32; 2],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user