Merge pull request #2085 from bungoboingo/shader-widget

[Feature] Custom Shader Widget
This commit is contained in:
Héctor Ramón 2023-11-14 20:59:49 +01:00 committed by GitHub
commit b474a2b7a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 2192 additions and 15 deletions

View file

@ -20,7 +20,7 @@ maintenance = { status = "actively-developed" }
[features]
default = ["wgpu"]
# Enable the `wgpu` GPU-accelerated renderer backend
wgpu = ["iced_renderer/wgpu"]
wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"]
# Enables the `Image` widget
image = ["iced_widget/image", "dep:image"]
# Enables the `Svg` widget

View file

@ -0,0 +1,17 @@
[package]
name = "custom_shader"
version = "0.1.0"
authors = ["Bingus <shankern@protonmail.com>"]
edition = "2021"
[dependencies]
iced.workspace = true
iced.features = ["debug", "advanced"]
image.workspace = true
bytemuck.workspace = true
glam.workspace = true
glam.features = ["bytemuck"]
rand = "0.8.5"

View file

@ -0,0 +1,168 @@
mod scene;
use scene::Scene;
use iced::executor;
use iced::time::Instant;
use iced::widget::shader::wgpu;
use iced::widget::{
checkbox, column, container, row, shader, slider, text, vertical_space,
};
use iced::window;
use iced::{
Alignment, Application, Color, Command, Element, Length, Renderer,
Subscription, Theme,
};
fn main() -> iced::Result {
IcedCubes::run(iced::Settings::default())
}
struct IcedCubes {
start: Instant,
scene: Scene,
}
#[derive(Debug, Clone)]
enum Message {
CubeAmountChanged(u32),
CubeSizeChanged(f32),
Tick(Instant),
ShowDepthBuffer(bool),
LightColorChanged(Color),
}
impl Application for IcedCubes {
type Executor = executor::Default;
type Message = Message;
type Theme = Theme;
type Flags = ();
fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) {
(
Self {
start: Instant::now(),
scene: Scene::new(),
},
Command::none(),
)
}
fn title(&self) -> String {
"Iced Cubes".to_string()
}
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message {
Message::CubeAmountChanged(amount) => {
self.scene.change_amount(amount);
}
Message::CubeSizeChanged(size) => {
self.scene.size = size;
}
Message::Tick(time) => {
self.scene.update(time - self.start);
}
Message::ShowDepthBuffer(show) => {
self.scene.show_depth_buffer = show;
}
Message::LightColorChanged(color) => {
self.scene.light_color = color;
}
}
Command::none()
}
fn view(&self) -> Element<'_, Self::Message, Renderer<Self::Theme>> {
let top_controls = row![
control(
"Amount",
slider(
1..=scene::MAX,
self.scene.cubes.len() as u32,
Message::CubeAmountChanged
)
.width(100)
),
control(
"Size",
slider(0.1..=0.25, self.scene.size, Message::CubeSizeChanged)
.step(0.01)
.width(100),
),
checkbox(
"Show Depth Buffer",
self.scene.show_depth_buffer,
Message::ShowDepthBuffer
),
]
.spacing(40);
let bottom_controls = row![
control(
"R",
slider(0.0..=1.0, self.scene.light_color.r, move |r| {
Message::LightColorChanged(Color {
r,
..self.scene.light_color
})
})
.step(0.01)
.width(100)
),
control(
"G",
slider(0.0..=1.0, self.scene.light_color.g, move |g| {
Message::LightColorChanged(Color {
g,
..self.scene.light_color
})
})
.step(0.01)
.width(100)
),
control(
"B",
slider(0.0..=1.0, self.scene.light_color.b, move |b| {
Message::LightColorChanged(Color {
b,
..self.scene.light_color
})
})
.step(0.01)
.width(100)
)
]
.spacing(40);
let controls = column![top_controls, bottom_controls,]
.spacing(10)
.align_items(Alignment::Center);
let shader =
shader(&self.scene).width(Length::Fill).height(Length::Fill);
container(
column![shader, controls, vertical_space(20),]
.spacing(40)
.align_items(Alignment::Center),
)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
}
fn subscription(&self) -> Subscription<Self::Message> {
window::frames().map(Message::Tick)
}
}
fn control<'a>(
label: &'static str,
control: impl Into<Element<'a, Message>>,
) -> Element<'a, Message> {
row![text(label), control.into()].spacing(10).into()
}

View file

@ -0,0 +1,186 @@
mod camera;
mod pipeline;
use camera::Camera;
use pipeline::Pipeline;
use crate::wgpu;
use pipeline::cube::{self, Cube};
use iced::mouse;
use iced::time::Duration;
use iced::widget::shader;
use iced::{Color, Rectangle, Size};
use glam::Vec3;
use rand::Rng;
use std::cmp::Ordering;
use std::iter;
pub const MAX: u32 = 500;
#[derive(Clone)]
pub struct Scene {
pub size: f32,
pub cubes: Vec<Cube>,
pub camera: Camera,
pub show_depth_buffer: bool,
pub light_color: Color,
}
impl Scene {
pub fn new() -> Self {
let mut scene = Self {
size: 0.2,
cubes: vec![],
camera: Camera::default(),
show_depth_buffer: false,
light_color: Color::WHITE,
};
scene.change_amount(MAX);
scene
}
pub fn update(&mut self, time: Duration) {
for cube in self.cubes.iter_mut() {
cube.update(self.size, time.as_secs_f32());
}
}
pub fn change_amount(&mut self, amount: u32) {
let curr_cubes = self.cubes.len() as u32;
match amount.cmp(&curr_cubes) {
Ordering::Greater => {
// spawn
let cubes_2_spawn = (amount - curr_cubes) as usize;
let mut cubes = 0;
self.cubes.extend(iter::from_fn(|| {
if cubes < cubes_2_spawn {
cubes += 1;
Some(Cube::new(self.size, rnd_origin()))
} else {
None
}
}));
}
Ordering::Less => {
// chop
let cubes_2_cut = curr_cubes - amount;
let new_len = self.cubes.len() - cubes_2_cut as usize;
self.cubes.truncate(new_len);
}
Ordering::Equal => {}
}
}
}
impl<Message> shader::Program<Message> for Scene {
type State = ();
type Primitive = Primitive;
fn draw(
&self,
_state: &Self::State,
_cursor: mouse::Cursor,
bounds: Rectangle,
) -> Self::Primitive {
Primitive::new(
&self.cubes,
&self.camera,
bounds,
self.show_depth_buffer,
self.light_color,
)
}
}
/// A collection of `Cube`s that can be rendered.
#[derive(Debug)]
pub struct Primitive {
cubes: Vec<cube::Raw>,
uniforms: pipeline::Uniforms,
show_depth_buffer: bool,
}
impl Primitive {
pub fn new(
cubes: &[Cube],
camera: &Camera,
bounds: Rectangle,
show_depth_buffer: bool,
light_color: Color,
) -> Self {
let uniforms = pipeline::Uniforms::new(camera, bounds, light_color);
Self {
cubes: cubes
.iter()
.map(cube::Raw::from_cube)
.collect::<Vec<cube::Raw>>(),
uniforms,
show_depth_buffer,
}
}
}
impl shader::Primitive for Primitive {
fn prepare(
&self,
format: wgpu::TextureFormat,
device: &wgpu::Device,
queue: &wgpu::Queue,
target_size: Size<u32>,
_scale_factor: f32,
_transform: shader::Transformation,
storage: &mut shader::Storage,
) {
if !storage.has::<Pipeline>() {
storage.store(Pipeline::new(device, queue, format, target_size));
}
let pipeline = storage.get_mut::<Pipeline>().unwrap();
//upload data to GPU
pipeline.update(
device,
queue,
target_size,
&self.uniforms,
self.cubes.len(),
&self.cubes,
);
}
fn render(
&self,
storage: &shader::Storage,
bounds: Rectangle<u32>,
target: &wgpu::TextureView,
_target_size: Size<u32>,
encoder: &mut wgpu::CommandEncoder,
) {
//at this point our pipeline should always be initialized
let pipeline = storage.get::<Pipeline>().unwrap();
//render primitive
pipeline.render(
target,
encoder,
bounds,
self.cubes.len() as u32,
self.show_depth_buffer,
);
}
}
fn rnd_origin() -> Vec3 {
Vec3::new(
rand::thread_rng().gen_range(-4.0..4.0),
rand::thread_rng().gen_range(-4.0..4.0),
rand::thread_rng().gen_range(-4.0..2.0),
)
}

View file

@ -0,0 +1,53 @@
use glam::{mat4, vec3, vec4};
use iced::Rectangle;
#[derive(Copy, Clone)]
pub struct Camera {
eye: glam::Vec3,
target: glam::Vec3,
up: glam::Vec3,
fov_y: f32,
near: f32,
far: f32,
}
impl Default for Camera {
fn default() -> Self {
Self {
eye: vec3(0.0, 2.0, 3.0),
target: glam::Vec3::ZERO,
up: glam::Vec3::Y,
fov_y: 45.0,
near: 0.1,
far: 100.0,
}
}
}
pub const OPENGL_TO_WGPU_MATRIX: glam::Mat4 = mat4(
vec4(1.0, 0.0, 0.0, 0.0),
vec4(0.0, 1.0, 0.0, 0.0),
vec4(0.0, 0.0, 0.5, 0.0),
vec4(0.0, 0.0, 0.5, 1.0),
);
impl Camera {
pub fn build_view_proj_matrix(&self, bounds: Rectangle) -> glam::Mat4 {
//TODO looks distorted without padding; base on surface texture size instead?
let aspect_ratio = bounds.width / (bounds.height + 150.0);
let view = glam::Mat4::look_at_rh(self.eye, self.target, self.up);
let proj = glam::Mat4::perspective_rh(
self.fov_y,
aspect_ratio,
self.near,
self.far,
);
OPENGL_TO_WGPU_MATRIX * proj * view
}
pub fn position(&self) -> glam::Vec4 {
glam::Vec4::from((self.eye, 0.0))
}
}

View file

@ -0,0 +1,614 @@
pub mod cube;
mod buffer;
mod uniforms;
mod vertex;
pub use uniforms::Uniforms;
use buffer::Buffer;
use vertex::Vertex;
use crate::wgpu;
use crate::wgpu::util::DeviceExt;
use iced::{Rectangle, Size};
const SKY_TEXTURE_SIZE: u32 = 128;
pub struct Pipeline {
pipeline: wgpu::RenderPipeline,
vertices: wgpu::Buffer,
cubes: Buffer,
uniforms: wgpu::Buffer,
uniform_bind_group: wgpu::BindGroup,
depth_texture_size: Size<u32>,
depth_view: wgpu::TextureView,
depth_pipeline: DepthPipeline,
}
impl Pipeline {
pub fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
target_size: Size<u32>,
) -> Self {
//vertices of one cube
let vertices =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("cubes vertex buffer"),
contents: bytemuck::cast_slice(&cube::Raw::vertices()),
usage: wgpu::BufferUsages::VERTEX,
});
//cube instance data
let cubes_buffer = Buffer::new(
device,
"cubes instance buffer",
std::mem::size_of::<cube::Raw>() as u64,
wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
);
//uniforms for all cubes
let uniforms = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("cubes uniform buffer"),
size: std::mem::size_of::<Uniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
//depth buffer
let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("cubes depth texture"),
size: wgpu::Extent3d {
width: target_size.width,
height: target_size.height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth32Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let depth_view =
depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
let normal_map_data = load_normal_map_data();
//normal map
let normal_texture = device.create_texture_with_data(
queue,
&wgpu::TextureDescriptor {
label: Some("cubes normal map texture"),
size: wgpu::Extent3d {
width: 1024,
height: 1024,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
&normal_map_data,
);
let normal_view =
normal_texture.create_view(&wgpu::TextureViewDescriptor::default());
//skybox texture for reflection/refraction
let skybox_data = load_skybox_data();
let skybox_texture = device.create_texture_with_data(
queue,
&wgpu::TextureDescriptor {
label: Some("cubes skybox texture"),
size: wgpu::Extent3d {
width: SKY_TEXTURE_SIZE,
height: SKY_TEXTURE_SIZE,
depth_or_array_layers: 6, //one for each face of the cube
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
&skybox_data,
);
let sky_view =
skybox_texture.create_view(&wgpu::TextureViewDescriptor {
label: Some("cubes skybox texture view"),
dimension: Some(wgpu::TextureViewDimension::Cube),
..Default::default()
});
let sky_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("cubes skybox sampler"),
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::Linear,
mipmap_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("cubes uniform bind group layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float {
filterable: true,
},
view_dimension: wgpu::TextureViewDimension::Cube,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(
wgpu::SamplerBindingType::Filtering,
),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float {
filterable: true,
},
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
],
});
let uniform_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("cubes uniform bind group"),
layout: &uniform_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uniforms.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&sky_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&sky_sampler),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(
&normal_view,
),
},
],
});
let layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("cubes pipeline layout"),
bind_group_layouts: &[&uniform_bind_group_layout],
push_constant_ranges: &[],
});
let shader =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("cubes shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
include_str!("../shaders/cubes.wgsl"),
)),
});
let pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("cubes pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[Vertex::desc(), cube::Raw::desc()],
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::Less,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Max,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview: None,
});
let depth_pipeline = DepthPipeline::new(
device,
format,
depth_texture.create_view(&wgpu::TextureViewDescriptor::default()),
);
Self {
pipeline,
cubes: cubes_buffer,
uniforms,
uniform_bind_group,
vertices,
depth_texture_size: target_size,
depth_view,
depth_pipeline,
}
}
fn update_depth_texture(&mut self, device: &wgpu::Device, size: Size<u32>) {
if self.depth_texture_size.height != size.height
|| self.depth_texture_size.width != size.width
{
let text = device.create_texture(&wgpu::TextureDescriptor {
label: Some("cubes depth texture"),
size: wgpu::Extent3d {
width: size.width,
height: size.height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth32Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
self.depth_view =
text.create_view(&wgpu::TextureViewDescriptor::default());
self.depth_texture_size = size;
self.depth_pipeline.update(device, &text);
}
}
pub fn update(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
target_size: Size<u32>,
uniforms: &Uniforms,
num_cubes: usize,
cubes: &[cube::Raw],
) {
//recreate depth texture if surface texture size has changed
self.update_depth_texture(device, target_size);
// update uniforms
queue.write_buffer(&self.uniforms, 0, bytemuck::bytes_of(uniforms));
//resize cubes vertex buffer if cubes amount changed
let new_size = num_cubes * std::mem::size_of::<cube::Raw>();
self.cubes.resize(device, new_size as u64);
//always write new cube data since they are constantly rotating
queue.write_buffer(&self.cubes.raw, 0, bytemuck::cast_slice(cubes));
}
pub fn render(
&self,
target: &wgpu::TextureView,
encoder: &mut wgpu::CommandEncoder,
bounds: Rectangle<u32>,
num_cubes: u32,
show_depth: bool,
) {
{
let mut pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("cubes.pipeline.pass"),
color_attachments: &[Some(
wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
},
)],
depth_stencil_attachment: Some(
wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
},
),
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_scissor_rect(
bounds.x,
bounds.y,
bounds.width,
bounds.height,
);
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_vertex_buffer(0, self.vertices.slice(..));
pass.set_vertex_buffer(1, self.cubes.raw.slice(..));
pass.draw(0..36, 0..num_cubes);
}
if show_depth {
self.depth_pipeline.render(encoder, target, bounds);
}
}
}
struct DepthPipeline {
pipeline: wgpu::RenderPipeline,
bind_group_layout: wgpu::BindGroupLayout,
bind_group: wgpu::BindGroup,
sampler: wgpu::Sampler,
depth_view: wgpu::TextureView,
}
impl DepthPipeline {
pub fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
depth_texture: wgpu::TextureView,
) -> Self {
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("cubes.depth_pipeline.sampler"),
..Default::default()
});
let bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("cubes.depth_pipeline.bind_group_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(
wgpu::SamplerBindingType::NonFiltering,
),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float {
filterable: false,
},
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
],
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("cubes.depth_pipeline.bind_group"),
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(
&depth_texture,
),
},
],
});
let layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("cubes.depth_pipeline.layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let shader =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("cubes.depth_pipeline.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
include_str!("../shaders/depth.wgsl"),
)),
});
let pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("cubes.depth_pipeline.pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[],
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::Less,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview: None,
});
Self {
pipeline,
bind_group_layout,
bind_group,
sampler,
depth_view: depth_texture,
}
}
pub fn update(
&mut self,
device: &wgpu::Device,
depth_texture: &wgpu::Texture,
) {
self.depth_view =
depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
self.bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("cubes.depth_pipeline.bind_group"),
layout: &self.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(
&self.depth_view,
),
},
],
});
}
pub fn render(
&self,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
bounds: Rectangle<u32>,
) {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("cubes.pipeline.depth_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(
wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: None,
stencil_ops: None,
},
),
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_scissor_rect(bounds.x, bounds.y, bounds.width, bounds.height);
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.bind_group, &[]);
pass.draw(0..6, 0..1);
}
}
fn load_skybox_data() -> Vec<u8> {
let pos_x: &[u8] = include_bytes!("../../textures/skybox/pos_x.jpg");
let neg_x: &[u8] = include_bytes!("../../textures/skybox/neg_x.jpg");
let pos_y: &[u8] = include_bytes!("../../textures/skybox/pos_y.jpg");
let neg_y: &[u8] = include_bytes!("../../textures/skybox/neg_y.jpg");
let pos_z: &[u8] = include_bytes!("../../textures/skybox/pos_z.jpg");
let neg_z: &[u8] = include_bytes!("../../textures/skybox/neg_z.jpg");
let data: [&[u8]; 6] = [pos_x, neg_x, pos_y, neg_y, pos_z, neg_z];
data.iter().fold(vec![], |mut acc, bytes| {
let i = image::load_from_memory_with_format(
bytes,
image::ImageFormat::Jpeg,
)
.unwrap()
.to_rgba8()
.into_raw();
acc.extend(i);
acc
})
}
fn load_normal_map_data() -> Vec<u8> {
let bytes: &[u8] = include_bytes!("../../textures/ice_cube_normal_map.png");
image::load_from_memory_with_format(bytes, image::ImageFormat::Png)
.unwrap()
.to_rgba8()
.into_raw()
}

View file

@ -0,0 +1,41 @@
use crate::wgpu;
// A custom buffer container for dynamic resizing.
pub struct Buffer {
pub raw: wgpu::Buffer,
label: &'static str,
size: u64,
usage: wgpu::BufferUsages,
}
impl Buffer {
pub fn new(
device: &wgpu::Device,
label: &'static str,
size: u64,
usage: wgpu::BufferUsages,
) -> Self {
Self {
raw: device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size,
usage,
mapped_at_creation: false,
}),
label,
size,
usage,
}
}
pub fn resize(&mut self, device: &wgpu::Device, new_size: u64) {
if new_size > self.size {
self.raw = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(self.label),
size: new_size,
usage: self.usage,
mapped_at_creation: false,
});
}
}
}

View file

@ -0,0 +1,326 @@
use crate::scene::pipeline::Vertex;
use crate::wgpu;
use glam::{vec2, vec3, Vec3};
use rand::{thread_rng, Rng};
/// A single instance of a cube.
#[derive(Debug, Clone)]
pub struct Cube {
pub rotation: glam::Quat,
pub position: Vec3,
pub size: f32,
rotation_dir: f32,
rotation_axis: glam::Vec3,
}
impl Default for Cube {
fn default() -> Self {
Self {
rotation: glam::Quat::IDENTITY,
position: glam::Vec3::ZERO,
size: 0.1,
rotation_dir: 1.0,
rotation_axis: glam::Vec3::Y,
}
}
}
impl Cube {
pub fn new(size: f32, origin: Vec3) -> Self {
let rnd = thread_rng().gen_range(0.0..=1.0f32);
Self {
rotation: glam::Quat::IDENTITY,
position: origin + Vec3::new(0.1, 0.1, 0.1),
size,
rotation_dir: if rnd <= 0.5 { -1.0 } else { 1.0 },
rotation_axis: if rnd <= 0.33 {
glam::Vec3::Y
} else if rnd <= 0.66 {
glam::Vec3::X
} else {
glam::Vec3::Z
},
}
}
pub fn update(&mut self, size: f32, time: f32) {
self.rotation = glam::Quat::from_axis_angle(
self.rotation_axis,
time / 2.0 * self.rotation_dir,
);
self.size = size;
}
}
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, Debug)]
#[repr(C)]
pub struct Raw {
transformation: glam::Mat4,
normal: glam::Mat3,
_padding: [f32; 3],
}
impl Raw {
const ATTRIBS: [wgpu::VertexAttribute; 7] = wgpu::vertex_attr_array![
//cube transformation matrix
4 => Float32x4,
5 => Float32x4,
6 => Float32x4,
7 => Float32x4,
//normal rotation matrix
8 => Float32x3,
9 => Float32x3,
10 => Float32x3,
];
pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &Self::ATTRIBS,
}
}
}
impl Raw {
pub fn from_cube(cube: &Cube) -> Raw {
Raw {
transformation: glam::Mat4::from_scale_rotation_translation(
glam::vec3(cube.size, cube.size, cube.size),
cube.rotation,
cube.position,
),
normal: glam::Mat3::from_quat(cube.rotation),
_padding: [0.0; 3],
}
}
pub fn vertices() -> [Vertex; 36] {
[
//face 1
Vertex {
pos: vec3(-0.5, -0.5, -0.5),
normal: vec3(0.0, 0.0, -1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 1.0),
},
Vertex {
pos: vec3(0.5, -0.5, -0.5),
normal: vec3(0.0, 0.0, -1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 1.0),
},
Vertex {
pos: vec3(0.5, 0.5, -0.5),
normal: vec3(0.0, 0.0, -1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(0.5, 0.5, -0.5),
normal: vec3(0.0, 0.0, -1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(-0.5, 0.5, -0.5),
normal: vec3(0.0, 0.0, -1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 0.0),
},
Vertex {
pos: vec3(-0.5, -0.5, -0.5),
normal: vec3(0.0, 0.0, -1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 1.0),
},
//face 2
Vertex {
pos: vec3(-0.5, -0.5, 0.5),
normal: vec3(0.0, 0.0, 1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 1.0),
},
Vertex {
pos: vec3(0.5, -0.5, 0.5),
normal: vec3(0.0, 0.0, 1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 1.0),
},
Vertex {
pos: vec3(0.5, 0.5, 0.5),
normal: vec3(0.0, 0.0, 1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(0.5, 0.5, 0.5),
normal: vec3(0.0, 0.0, 1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(-0.5, 0.5, 0.5),
normal: vec3(0.0, 0.0, 1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 0.0),
},
Vertex {
pos: vec3(-0.5, -0.5, 0.5),
normal: vec3(0.0, 0.0, 1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 1.0),
},
//face 3
Vertex {
pos: vec3(-0.5, 0.5, 0.5),
normal: vec3(-1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(0.0, 1.0),
},
Vertex {
pos: vec3(-0.5, 0.5, -0.5),
normal: vec3(-1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(1.0, 1.0),
},
Vertex {
pos: vec3(-0.5, -0.5, -0.5),
normal: vec3(-1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(-0.5, -0.5, -0.5),
normal: vec3(-1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(-0.5, -0.5, 0.5),
normal: vec3(-1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(0.0, 0.0),
},
Vertex {
pos: vec3(-0.5, 0.5, 0.5),
normal: vec3(-1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(0.0, 1.0),
},
//face 4
Vertex {
pos: vec3(0.5, 0.5, 0.5),
normal: vec3(1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(0.0, 1.0),
},
Vertex {
pos: vec3(0.5, 0.5, -0.5),
normal: vec3(1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(1.0, 1.0),
},
Vertex {
pos: vec3(0.5, -0.5, -0.5),
normal: vec3(1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(0.5, -0.5, -0.5),
normal: vec3(1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(0.5, -0.5, 0.5),
normal: vec3(1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(0.0, 0.0),
},
Vertex {
pos: vec3(0.5, 0.5, 0.5),
normal: vec3(1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(0.0, 1.0),
},
//face 5
Vertex {
pos: vec3(-0.5, -0.5, -0.5),
normal: vec3(0.0, -1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 1.0),
},
Vertex {
pos: vec3(0.5, -0.5, -0.5),
normal: vec3(0.0, -1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 1.0),
},
Vertex {
pos: vec3(0.5, -0.5, 0.5),
normal: vec3(0.0, -1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(0.5, -0.5, 0.5),
normal: vec3(0.0, -1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(-0.5, -0.5, 0.5),
normal: vec3(0.0, -1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 0.0),
},
Vertex {
pos: vec3(-0.5, -0.5, -0.5),
normal: vec3(0.0, -1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 1.0),
},
//face 6
Vertex {
pos: vec3(-0.5, 0.5, -0.5),
normal: vec3(0.0, 1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 1.0),
},
Vertex {
pos: vec3(0.5, 0.5, -0.5),
normal: vec3(0.0, 1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 1.0),
},
Vertex {
pos: vec3(0.5, 0.5, 0.5),
normal: vec3(0.0, 1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(0.5, 0.5, 0.5),
normal: vec3(0.0, 1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(-0.5, 0.5, 0.5),
normal: vec3(0.0, 1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 0.0),
},
Vertex {
pos: vec3(-0.5, 0.5, -0.5),
normal: vec3(0.0, 1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 1.0),
},
]
}
}

View file

@ -0,0 +1,23 @@
use crate::scene::Camera;
use iced::{Color, Rectangle};
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct Uniforms {
camera_proj: glam::Mat4,
camera_pos: glam::Vec4,
light_color: glam::Vec4,
}
impl Uniforms {
pub fn new(camera: &Camera, bounds: Rectangle, light_color: Color) -> Self {
let camera_proj = camera.build_view_proj_matrix(bounds);
Self {
camera_proj,
camera_pos: camera.position(),
light_color: glam::Vec4::from(light_color.into_linear()),
}
}
}

View file

@ -0,0 +1,31 @@
use crate::wgpu;
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct Vertex {
pub pos: glam::Vec3,
pub normal: glam::Vec3,
pub tangent: glam::Vec3,
pub uv: glam::Vec2,
}
impl Vertex {
const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
//position
0 => Float32x3,
//normal
1 => Float32x3,
//tangent
2 => Float32x3,
//uv
3 => Float32x2,
];
pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &Self::ATTRIBS,
}
}
}

View file

@ -0,0 +1,123 @@
struct Uniforms {
projection: mat4x4<f32>,
camera_pos: vec4<f32>,
light_color: vec4<f32>,
}
const LIGHT_POS: vec3<f32> = vec3<f32>(0.0, 3.0, 3.0);
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var sky_texture: texture_cube<f32>;
@group(0) @binding(2) var tex_sampler: sampler;
@group(0) @binding(3) var normal_texture: texture_2d<f32>;
struct Vertex {
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
@location(2) tangent: vec3<f32>,
@location(3) uv: vec2<f32>,
}
struct Cube {
@location(4) matrix_0: vec4<f32>,
@location(5) matrix_1: vec4<f32>,
@location(6) matrix_2: vec4<f32>,
@location(7) matrix_3: vec4<f32>,
@location(8) normal_matrix_0: vec3<f32>,
@location(9) normal_matrix_1: vec3<f32>,
@location(10) normal_matrix_2: vec3<f32>,
}
struct Output {
@builtin(position) clip_pos: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) tangent_pos: vec3<f32>,
@location(2) tangent_camera_pos: vec3<f32>,
@location(3) tangent_light_pos: vec3<f32>,
}
@vertex
fn vs_main(vertex: Vertex, cube: Cube) -> Output {
let cube_matrix = mat4x4<f32>(
cube.matrix_0,
cube.matrix_1,
cube.matrix_2,
cube.matrix_3,
);
let normal_matrix = mat3x3<f32>(
cube.normal_matrix_0,
cube.normal_matrix_1,
cube.normal_matrix_2,
);
//convert to tangent space to calculate lighting in same coordinate space as normal map sample
let tangent = normalize(normal_matrix * vertex.tangent);
let normal = normalize(normal_matrix * vertex.normal);
let bitangent = cross(tangent, normal);
//shift everything into tangent space
let tbn = transpose(mat3x3<f32>(tangent, bitangent, normal));
let world_pos = cube_matrix * vec4<f32>(vertex.position, 1.0);
var out: Output;
out.clip_pos = uniforms.projection * world_pos;
out.uv = vertex.uv;
out.tangent_pos = tbn * world_pos.xyz;
out.tangent_camera_pos = tbn * uniforms.camera_pos.xyz;
out.tangent_light_pos = tbn * LIGHT_POS;
return out;
}
//cube properties
const CUBE_BASE_COLOR: vec4<f32> = vec4<f32>(0.294118, 0.462745, 0.611765, 0.6);
const SHINE_DAMPER: f32 = 1.0;
const REFLECTIVITY: f32 = 0.8;
const REFRACTION_INDEX: f32 = 1.31;
//fog, for the ~* cinematic effect *~
const FOG_DENSITY: f32 = 0.15;
const FOG_GRADIENT: f32 = 8.0;
const FOG_COLOR: vec4<f32> = vec4<f32>(1.0, 1.0, 1.0, 1.0);
@fragment
fn fs_main(in: Output) -> @location(0) vec4<f32> {
let to_camera = in.tangent_camera_pos - in.tangent_pos;
//normal sample from texture
var normal = textureSample(normal_texture, tex_sampler, in.uv).xyz;
normal = normal * 2.0 - 1.0;
//diffuse
let dir_to_light: vec3<f32> = normalize(in.tangent_light_pos - in.tangent_pos);
let brightness = max(dot(normal, dir_to_light), 0.0);
let diffuse: vec3<f32> = brightness * uniforms.light_color.xyz;
//specular
let dir_to_camera = normalize(to_camera);
let light_dir = -dir_to_light;
let reflected_light_dir = reflect(light_dir, normal);
let specular_factor = max(dot(reflected_light_dir, dir_to_camera), 0.0);
let damped_factor = pow(specular_factor, SHINE_DAMPER);
let specular: vec3<f32> = damped_factor * uniforms.light_color.xyz * REFLECTIVITY;
//fog
let distance = length(to_camera);
let visibility = clamp(exp(-pow((distance * FOG_DENSITY), FOG_GRADIENT)), 0.0, 1.0);
//reflection
let reflection_dir = reflect(dir_to_camera, normal);
let reflection_color = textureSample(sky_texture, tex_sampler, reflection_dir);
let refraction_dir = refract(dir_to_camera, normal, REFRACTION_INDEX);
let refraction_color = textureSample(sky_texture, tex_sampler, refraction_dir);
let final_reflect_color = mix(reflection_color, refraction_color, 0.5);
//mix it all together!
var color = vec4<f32>(CUBE_BASE_COLOR.xyz * diffuse + specular, CUBE_BASE_COLOR.w);
color = mix(color, final_reflect_color, 0.8);
color = mix(FOG_COLOR, color, visibility);
return color;
}

View file

@ -0,0 +1,48 @@
var<private> positions: array<vec2<f32>, 6> = array<vec2<f32>, 6>(
vec2<f32>(-1.0, 1.0),
vec2<f32>(-1.0, -1.0),
vec2<f32>(1.0, -1.0),
vec2<f32>(-1.0, 1.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(1.0, -1.0)
);
var<private> uvs: array<vec2<f32>, 6> = array<vec2<f32>, 6>(
vec2<f32>(0.0, 0.0),
vec2<f32>(0.0, 1.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(0.0, 0.0),
vec2<f32>(1.0, 0.0),
vec2<f32>(1.0, 1.0)
);
@group(0) @binding(0) var depth_sampler: sampler;
@group(0) @binding(1) var depth_texture: texture_2d<f32>;
struct Output {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@vertex
fn vs_main(@builtin(vertex_index) v_index: u32) -> Output {
var out: Output;
out.position = vec4<f32>(positions[v_index], 0.0, 1.0);
out.uv = uvs[v_index];
return out;
}
@fragment
fn fs_main(input: Output) -> @location(0) vec4<f32> {
let depth = textureSample(depth_texture, depth_sampler, input.uv).r;
if (depth > .9999) {
discard;
}
let c = 1.0 - depth;
return vec4<f32>(c, c, c, 1.0);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -271,6 +271,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
&queue,
&mut encoder,
None,
frame.texture.format(),
&view,
primitive,
&viewport,

View file

@ -16,7 +16,6 @@ all-features = true
[features]
geometry = ["lyon_path"]
opengl = []
image = ["dep:image", "kamadak-exif"]
web-colors = []

View file

@ -1,6 +1,9 @@
#![forbid(rust_2018_idioms)]
#![deny(unsafe_code, unused_results, rustdoc::broken_intra_doc_links)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#[cfg(feature = "wgpu")]
pub use iced_wgpu as wgpu;
pub mod compositor;
#[cfg(feature = "geometry")]
@ -271,3 +274,23 @@ impl<T> crate::graphics::geometry::Renderer for Renderer<T> {
}
}
}
#[cfg(feature = "wgpu")]
impl<T> iced_wgpu::primitive::pipeline::Renderer for Renderer<T> {
fn draw_pipeline_primitive(
&mut self,
bounds: Rectangle,
primitive: impl wgpu::primitive::pipeline::Primitive,
) {
match self {
Self::TinySkia(_renderer) => {
log::warn!(
"Custom shader primitive is unavailable with tiny-skia."
);
}
Self::Wgpu(renderer) => {
renderer.draw_pipeline_primitive(bounds, primitive);
}
}
}
}

View file

@ -1,11 +0,0 @@
#[cfg(feature = "canvas")]
pub mod canvas;
#[cfg(feature = "canvas")]
pub use canvas::Canvas;
#[cfg(feature = "qr_code")]
pub mod qr_code;
#[cfg(feature = "qr_code")]
pub use qr_code::QRCode;

View file

@ -2,6 +2,7 @@ use crate::core::{Color, Size};
use crate::graphics::backend;
use crate::graphics::color;
use crate::graphics::{Transformation, Viewport};
use crate::primitive::pipeline;
use crate::primitive::{self, Primitive};
use crate::quad;
use crate::text;
@ -25,6 +26,7 @@ pub struct Backend {
quad_pipeline: quad::Pipeline,
text_pipeline: text::Pipeline,
triangle_pipeline: triangle::Pipeline,
pipeline_storage: pipeline::Storage,
#[cfg(any(feature = "image", feature = "svg"))]
image_pipeline: image::Pipeline,
@ -50,6 +52,7 @@ impl Backend {
quad_pipeline,
text_pipeline,
triangle_pipeline,
pipeline_storage: pipeline::Storage::default(),
#[cfg(any(feature = "image", feature = "svg"))]
image_pipeline,
@ -66,6 +69,7 @@ impl Backend {
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
clear_color: Option<Color>,
format: wgpu::TextureFormat,
frame: &wgpu::TextureView,
primitives: &[Primitive],
viewport: &Viewport,
@ -88,6 +92,7 @@ impl Backend {
self.prepare(
device,
queue,
format,
encoder,
scale_factor,
target_size,
@ -117,6 +122,7 @@ impl Backend {
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
_encoder: &mut wgpu::CommandEncoder,
scale_factor: f32,
target_size: Size<u32>,
@ -179,6 +185,20 @@ impl Backend {
target_size,
);
}
if !layer.pipelines.is_empty() {
for pipeline in &layer.pipelines {
pipeline.primitive.prepare(
format,
device,
queue,
target_size,
scale_factor,
transformation,
&mut self.pipeline_storage,
);
}
}
}
}
@ -202,7 +222,7 @@ impl Backend {
let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu::quad render pass"),
label: Some("iced_wgpu render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
@ -265,7 +285,7 @@ impl Backend {
render_pass = ManuallyDrop::new(encoder.begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu::quad render pass"),
label: Some("iced_wgpu render pass"),
color_attachments: &[Some(
wgpu::RenderPassColorAttachment {
view: target,
@ -302,6 +322,45 @@ impl Backend {
text_layer += 1;
}
if !layer.pipelines.is_empty() {
let _ = ManuallyDrop::into_inner(render_pass);
for pipeline in &layer.pipelines {
let bounds = (pipeline.bounds * scale_factor).snap();
if bounds.width < 1 || bounds.height < 1 {
continue;
}
pipeline.primitive.render(
&self.pipeline_storage,
bounds,
target,
target_size,
encoder,
);
}
render_pass = ManuallyDrop::new(encoder.begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu render pass"),
color_attachments: &[Some(
wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
},
)],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
},
));
}
}
let _ = ManuallyDrop::into_inner(render_pass);

View file

@ -34,6 +34,9 @@ pub struct Layer<'a> {
/// The images of the [`Layer`].
pub images: Vec<Image>,
/// The custom pipelines of this [`Layer`].
pub pipelines: Vec<primitive::Pipeline>,
}
impl<'a> Layer<'a> {
@ -45,6 +48,7 @@ impl<'a> Layer<'a> {
meshes: Vec::new(),
text: Vec::new(),
images: Vec::new(),
pipelines: Vec::new(),
}
}
@ -308,6 +312,23 @@ impl<'a> Layer<'a> {
}
}
},
primitive::Custom::Pipeline(pipeline) => {
let layer = &mut layers[current_layer];
let bounds = Rectangle::new(
Point::new(translation.x, translation.y),
pipeline.bounds.size(),
);
if let Some(clip_bounds) =
layer.bounds.intersection(&bounds)
{
layer.pipelines.push(primitive::Pipeline {
bounds: clip_bounds,
primitive: pipeline.primitive.clone(),
});
}
}
},
}
}

View file

@ -1,7 +1,13 @@
//! Draw using different graphical primitives.
pub mod pipeline;
pub use pipeline::Pipeline;
use crate::core::Rectangle;
use crate::graphics::{Damage, Mesh};
use std::fmt::Debug;
/// The graphical primitives supported by `iced_wgpu`.
pub type Primitive = crate::graphics::Primitive<Custom>;
@ -10,12 +16,15 @@ pub type Primitive = crate::graphics::Primitive<Custom>;
pub enum Custom {
/// A mesh primitive.
Mesh(Mesh),
/// A custom pipeline primitive.
Pipeline(Pipeline),
}
impl Damage for Custom {
fn bounds(&self) -> Rectangle {
match self {
Self::Mesh(mesh) => mesh.bounds(),
Self::Pipeline(pipeline) => pipeline.bounds,
}
}
}

View file

@ -0,0 +1,117 @@
//! Draw primitives using custom pipelines.
use crate::core::{Rectangle, Size};
use crate::graphics::Transformation;
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::Arc;
#[derive(Clone, Debug)]
/// A custom primitive which can be used to render primitives associated with a custom pipeline.
pub struct Pipeline {
/// The bounds of the [`Pipeline`].
pub bounds: Rectangle,
/// The [`Primitive`] to render.
pub primitive: Arc<dyn Primitive>,
}
impl Pipeline {
/// Creates a new [`Pipeline`] with the given [`Primitive`].
pub fn new(bounds: Rectangle, primitive: impl Primitive) -> Self {
Pipeline {
bounds,
primitive: Arc::new(primitive),
}
}
}
impl PartialEq for Pipeline {
fn eq(&self, other: &Self) -> bool {
self.primitive.type_id() == other.primitive.type_id()
}
}
/// A set of methods which allows a [`Primitive`] to be rendered.
pub trait Primitive: Debug + Send + Sync + 'static {
/// Processes the [`Primitive`], allowing for GPU buffer allocation.
fn prepare(
&self,
format: wgpu::TextureFormat,
device: &wgpu::Device,
queue: &wgpu::Queue,
target_size: Size<u32>,
scale_factor: f32,
transform: Transformation,
storage: &mut Storage,
);
/// Renders the [`Primitive`].
fn render(
&self,
storage: &Storage,
bounds: Rectangle<u32>,
target: &wgpu::TextureView,
target_size: Size<u32>,
encoder: &mut wgpu::CommandEncoder,
);
}
/// A renderer than can draw custom pipeline primitives.
pub trait Renderer: crate::core::Renderer {
/// Draws a custom pipeline primitive.
fn draw_pipeline_primitive(
&mut self,
bounds: Rectangle,
primitive: impl Primitive,
);
}
impl<Theme> Renderer for crate::Renderer<Theme> {
fn draw_pipeline_primitive(
&mut self,
bounds: Rectangle,
primitive: impl Primitive,
) {
self.draw_primitive(super::Primitive::Custom(super::Custom::Pipeline(
Pipeline::new(bounds, primitive),
)));
}
}
/// Stores custom, user-provided pipelines.
#[derive(Default, Debug)]
pub struct Storage {
pipelines: HashMap<TypeId, Box<dyn Any>>,
}
impl Storage {
/// Returns `true` if `Storage` contains a pipeline with type `T`.
pub fn has<T: 'static>(&self) -> bool {
self.pipelines.get(&TypeId::of::<T>()).is_some()
}
/// Inserts the pipeline `T` in to [`Storage`].
pub fn store<T: 'static>(&mut self, pipeline: T) {
let _ = self.pipelines.insert(TypeId::of::<T>(), Box::new(pipeline));
}
/// Returns a reference to pipeline with type `T` if it exists in [`Storage`].
pub fn get<T: 'static>(&self) -> Option<&T> {
self.pipelines.get(&TypeId::of::<T>()).map(|pipeline| {
pipeline
.downcast_ref::<T>()
.expect("Pipeline with this type does not exist in Storage.")
})
}
/// Returns a mutable reference to pipeline `T` if it exists in [`Storage`].
pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
self.pipelines.get_mut(&TypeId::of::<T>()).map(|pipeline| {
pipeline
.downcast_mut::<T>()
.expect("Pipeline with this type does not exist in Storage.")
})
}
}

View file

@ -178,6 +178,7 @@ pub fn present<Theme, T: AsRef<str>>(
&compositor.queue,
&mut encoder,
Some(background_color),
frame.texture.format(),
view,
primitives,
viewport,
@ -357,6 +358,7 @@ pub fn screenshot<Theme, T: AsRef<str>>(
&compositor.queue,
&mut encoder,
Some(background_color),
texture.format(),
&view,
primitives,
viewport,

View file

@ -20,6 +20,7 @@ image = ["iced_renderer/image"]
svg = ["iced_renderer/svg"]
canvas = ["iced_renderer/geometry"]
qr_code = ["canvas", "qrcode"]
wgpu = ["iced_renderer/wgpu"]
[dependencies]
iced_renderer.workspace = true

View file

@ -385,6 +385,17 @@ where
crate::Canvas::new(program)
}
/// Creates a new [`Shader`].
///
/// [`Shader`]: crate::Shader
#[cfg(feature = "wgpu")]
pub fn shader<Message, P>(program: P) -> crate::Shader<Message, P>
where
P: crate::shader::Program<Message>,
{
crate::Shader::new(program)
}
/// Focuses the previous focusable widget.
pub fn focus_previous<Message>() -> Command<Message>
where

View file

@ -97,6 +97,13 @@ pub use tooltip::Tooltip;
#[doc(no_inline)]
pub use vertical_slider::VerticalSlider;
#[cfg(feature = "wgpu")]
pub mod shader;
#[cfg(feature = "wgpu")]
#[doc(no_inline)]
pub use shader::Shader;
#[cfg(feature = "svg")]
pub mod svg;

221
widget/src/shader.rs Normal file
View file

@ -0,0 +1,221 @@
//! A custom shader widget for wgpu applications.
mod event;
mod program;
pub use event::Event;
pub use program::Program;
use crate::core;
use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::tree::{self, Tree};
use crate::core::widget::{self, Widget};
use crate::core::window;
use crate::core::{Clipboard, Element, Length, Rectangle, Shell, Size};
use crate::renderer::wgpu::primitive::pipeline;
use std::marker::PhantomData;
pub use crate::graphics::Transformation;
pub use crate::renderer::wgpu::wgpu;
pub use pipeline::{Primitive, Storage};
/// A widget which can render custom shaders with Iced's `wgpu` backend.
///
/// Must be initialized with a [`Program`], which describes the internal widget state & how
/// its [`Program::Primitive`]s are drawn.
#[allow(missing_debug_implementations)]
pub struct Shader<Message, P: Program<Message>> {
width: Length,
height: Length,
program: P,
_message: PhantomData<Message>,
}
impl<Message, P: Program<Message>> Shader<Message, P> {
/// Create a new custom [`Shader`].
pub fn new(program: P) -> Self {
Self {
width: Length::Fixed(100.0),
height: Length::Fixed(100.0),
program,
_message: PhantomData,
}
}
/// Set the `width` of the custom [`Shader`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Set the `height` of the custom [`Shader`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
}
impl<P, Message, Renderer> Widget<Message, Renderer> for Shader<Message, P>
where
P: Program<Message>,
Renderer: pipeline::Renderer,
{
fn tag(&self) -> tree::Tag {
struct Tag<T>(T);
tree::Tag::of::<Tag<P::State>>()
}
fn state(&self) -> tree::State {
tree::State::new(P::State::default())
}
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
self.height
}
fn layout(
&self,
_tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.width(self.width).height(self.height);
let size = limits.resolve(Size::ZERO);
layout::Node::new(size)
}
fn on_event(
&mut self,
tree: &mut Tree,
event: crate::core::Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
let bounds = layout.bounds();
let custom_shader_event = match event {
core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)),
core::Event::Keyboard(keyboard_event) => {
Some(Event::Keyboard(keyboard_event))
}
core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)),
core::Event::Window(window::Event::RedrawRequested(instant)) => {
Some(Event::RedrawRequested(instant))
}
_ => None,
};
if let Some(custom_shader_event) = custom_shader_event {
let state = tree.state.downcast_mut::<P::State>();
let (event_status, message) = self.program.update(
state,
custom_shader_event,
bounds,
cursor,
shell,
);
if let Some(message) = message {
shell.publish(message);
}
return event_status;
}
event::Status::Ignored
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
let bounds = layout.bounds();
let state = tree.state.downcast_ref::<P::State>();
self.program.mouse_interaction(state, bounds, cursor)
}
fn draw(
&self,
tree: &widget::Tree,
renderer: &mut Renderer,
_theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
let state = tree.state.downcast_ref::<P::State>();
renderer.draw_pipeline_primitive(
bounds,
self.program.draw(state, cursor_position, bounds),
);
}
}
impl<'a, Message, Renderer, P> From<Shader<Message, P>>
for Element<'a, Message, Renderer>
where
Message: 'a,
Renderer: pipeline::Renderer,
P: Program<Message> + 'a,
{
fn from(custom: Shader<Message, P>) -> Element<'a, Message, Renderer> {
Element::new(custom)
}
}
impl<Message, T> Program<Message> for &T
where
T: Program<Message>,
{
type State = T::State;
type Primitive = T::Primitive;
fn update(
&self,
state: &mut Self::State,
event: Event,
bounds: Rectangle,
cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
) -> (event::Status, Option<Message>) {
T::update(self, state, event, bounds, cursor, shell)
}
fn draw(
&self,
state: &Self::State,
cursor: mouse::Cursor,
bounds: Rectangle,
) -> Self::Primitive {
T::draw(self, state, cursor, bounds)
}
fn mouse_interaction(
&self,
state: &Self::State,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> mouse::Interaction {
T::mouse_interaction(self, state, bounds, cursor)
}
}

View file

@ -0,0 +1,25 @@
//! Handle events of a custom shader widget.
use crate::core::keyboard;
use crate::core::mouse;
use crate::core::time::Instant;
use crate::core::touch;
pub use crate::core::event::Status;
/// A [`Shader`] event.
///
/// [`Shader`]: crate::Shader
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Event {
/// A mouse event.
Mouse(mouse::Event),
/// A touch event.
Touch(touch::Event),
/// A keyboard event.
Keyboard(keyboard::Event),
/// A window requested a redraw.
RedrawRequested(Instant),
}

View file

@ -0,0 +1,62 @@
use crate::core::event;
use crate::core::mouse;
use crate::core::{Rectangle, Shell};
use crate::renderer::wgpu::primitive::pipeline;
use crate::shader;
/// The state and logic of a [`Shader`] widget.
///
/// A [`Program`] can mutate the internal state of a [`Shader`] widget
/// and produce messages for an application.
///
/// [`Shader`]: crate::Shader
pub trait Program<Message> {
/// The internal state of the [`Program`].
type State: Default + 'static;
/// The type of primitive this [`Program`] can draw.
type Primitive: pipeline::Primitive + 'static;
/// Update the internal [`State`] of the [`Program`]. This can be used to reflect state changes
/// based on mouse & other events. You can use the [`Shell`] to publish messages, request a
/// redraw for the window, etc.
///
/// By default, this method does and returns nothing.
///
/// [`State`]: Self::State
fn update(
&self,
_state: &mut Self::State,
_event: shader::Event,
_bounds: Rectangle,
_cursor: mouse::Cursor,
_shell: &mut Shell<'_, Message>,
) -> (event::Status, Option<Message>) {
(event::Status::Ignored, None)
}
/// Draws the [`Primitive`].
///
/// [`Primitive`]: Self::Primitive
fn draw(
&self,
state: &Self::State,
cursor: mouse::Cursor,
bounds: Rectangle,
) -> Self::Primitive;
/// Returns the current mouse interaction of the [`Program`].
///
/// The interaction returned will be in effect even if the cursor position is out of
/// bounds of the [`Shader`]'s program.
///
/// [`Shader`]: crate::Shader
fn mouse_interaction(
&self,
_state: &Self::State,
_bounds: Rectangle,
_cursor: mouse::Cursor,
) -> mouse::Interaction {
mouse::Interaction::default()
}
}