This commit is contained in:
2025-08-05 14:18:37 -04:00
commit eba8481bde
20 changed files with 3814 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

2691
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

16
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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));
}
}

View 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
View File

@@ -0,0 +1 @@
pub mod pipeline;

127
src/render/text/pipeline.rs Normal file
View 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
View 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
View 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);
}
}

View 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);
}

View 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 })
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View 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],
}