summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml2
-rw-r--r--core/src/rectangle.rs11
-rw-r--r--examples/custom_shader/Cargo.toml13
-rw-r--r--examples/custom_shader/src/camera.rs53
-rw-r--r--examples/custom_shader/src/cubes.rs99
-rw-r--r--examples/custom_shader/src/main.rs174
-rw-r--r--examples/custom_shader/src/pipeline.rs600
-rw-r--r--examples/custom_shader/src/primitive.rs95
-rw-r--r--examples/custom_shader/src/primitive/buffer.rs39
-rw-r--r--examples/custom_shader/src/primitive/cube.rs324
-rw-r--r--examples/custom_shader/src/primitive/uniforms.rs22
-rw-r--r--examples/custom_shader/src/primitive/vertex.rs29
-rw-r--r--examples/custom_shader/src/shaders/cubes.wgsl123
-rw-r--r--examples/custom_shader/src/shaders/depth.wgsl48
-rw-r--r--examples/custom_shader/src/textures/ice_cube_normal_map.pngbin0 -> 1773656 bytes
-rw-r--r--examples/custom_shader/src/textures/skybox/neg_x.jpgbin0 -> 7549 bytes
-rw-r--r--examples/custom_shader/src/textures/skybox/neg_y.jpgbin0 -> 2722 bytes
-rw-r--r--examples/custom_shader/src/textures/skybox/neg_z.jpgbin0 -> 3986 bytes
-rw-r--r--examples/custom_shader/src/textures/skybox/pos_x.jpgbin0 -> 5522 bytes
-rw-r--r--examples/custom_shader/src/textures/skybox/pos_y.jpgbin0 -> 3382 bytes
-rw-r--r--examples/custom_shader/src/textures/skybox/pos_z.jpgbin0 -> 5205 bytes
-rw-r--r--examples/integration/src/main.rs1
-rw-r--r--graphics/Cargo.toml1
-rw-r--r--renderer/src/lib.rs21
-rw-r--r--renderer/src/widget.rs3
-rw-r--r--renderer/src/widget/shader.rs215
-rw-r--r--renderer/src/widget/shader/event.rs21
-rw-r--r--renderer/src/widget/shader/program.rs60
-rw-r--r--style/src/theme.rs2
-rw-r--r--wgpu/src/backend.rs64
-rw-r--r--wgpu/src/custom.rs66
-rw-r--r--wgpu/src/layer.rs16
-rw-r--r--wgpu/src/lib.rs1
-rw-r--r--wgpu/src/primitive.rs36
-rw-r--r--wgpu/src/window/compositor.rs2
-rw-r--r--widget/Cargo.toml1
-rw-r--r--widget/src/lib.rs3
37 files changed, 2139 insertions, 6 deletions
diff --git a/Cargo.toml b/Cargo.toml
index d69c95cf..ad4cd1bb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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
diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs
index c1c2eeac..d5437d51 100644
--- a/core/src/rectangle.rs
+++ b/core/src/rectangle.rs
@@ -183,6 +183,17 @@ impl From<Rectangle<u32>> for Rectangle<f32> {
}
}
+impl From<Rectangle<f32>> for Rectangle<u32> {
+ fn from(rectangle: Rectangle<f32>) -> Self {
+ Rectangle {
+ x: rectangle.x as u32,
+ y: rectangle.y as u32,
+ width: rectangle.width as u32,
+ height: rectangle.height as u32,
+ }
+ }
+}
+
impl<T> std::ops::Add<Vector<T>> for Rectangle<T>
where
T: std::ops::Add<Output = T>,
diff --git a/examples/custom_shader/Cargo.toml b/examples/custom_shader/Cargo.toml
new file mode 100644
index 00000000..7a927811
--- /dev/null
+++ b/examples/custom_shader/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "custom_shader"
+version = "0.1.0"
+authors = ["Bingus <shankern@protonmail.com>"]
+edition = "2021"
+
+[dependencies]
+iced = { path = "../..", features = ["debug", "advanced"]}
+image = { version = "0.24.6"}
+wgpu = "0.17"
+bytemuck = { version = "1.13.1" }
+glam = { version = "0.24.0", features = ["bytemuck"] }
+rand = "0.8.5"
diff --git a/examples/custom_shader/src/camera.rs b/examples/custom_shader/src/camera.rs
new file mode 100644
index 00000000..2a49c102
--- /dev/null
+++ b/examples/custom_shader/src/camera.rs
@@ -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))
+ }
+}
diff --git a/examples/custom_shader/src/cubes.rs b/examples/custom_shader/src/cubes.rs
new file mode 100644
index 00000000..8dbba4b1
--- /dev/null
+++ b/examples/custom_shader/src/cubes.rs
@@ -0,0 +1,99 @@
+use crate::camera::Camera;
+use crate::primitive;
+use crate::primitive::cube::Cube;
+use glam::Vec3;
+use iced::widget::shader;
+use iced::{mouse, Color, Rectangle};
+use rand::Rng;
+use std::cmp::Ordering;
+use std::iter;
+use std::time::Duration;
+
+pub const MAX: u32 = 500;
+
+#[derive(Clone)]
+pub struct Cubes {
+ pub size: f32,
+ pub cubes: Vec<Cube>,
+ pub camera: Camera,
+ pub show_depth_buffer: bool,
+ pub light_color: Color,
+}
+
+impl Cubes {
+ pub fn new() -> Self {
+ let mut cubes = Self {
+ size: 0.2,
+ cubes: vec![],
+ camera: Camera::default(),
+ show_depth_buffer: false,
+ light_color: Color::WHITE,
+ };
+
+ cubes.adjust_num_cubes(MAX);
+
+ cubes
+ }
+
+ pub fn update(&mut self, time: Duration) {
+ for cube in self.cubes.iter_mut() {
+ cube.update(self.size, time.as_secs_f32());
+ }
+ }
+
+ pub fn adjust_num_cubes(&mut self, num_cubes: u32) {
+ let curr_cubes = self.cubes.len() as u32;
+
+ match num_cubes.cmp(&curr_cubes) {
+ Ordering::Greater => {
+ // spawn
+ let cubes_2_spawn = (num_cubes - 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 - num_cubes;
+ let new_len = self.cubes.len() - cubes_2_cut as usize;
+ self.cubes.truncate(new_len);
+ }
+ _ => {}
+ }
+ }
+}
+
+impl<Message> shader::Program<Message> for Cubes {
+ type State = ();
+ type Primitive = primitive::Primitive;
+
+ fn draw(
+ &self,
+ _state: &Self::State,
+ _cursor: mouse::Cursor,
+ bounds: Rectangle,
+ ) -> Self::Primitive {
+ primitive::Primitive::new(
+ &self.cubes,
+ &self.camera,
+ bounds,
+ self.show_depth_buffer,
+ self.light_color,
+ )
+ }
+}
+
+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),
+ )
+}
diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs
new file mode 100644
index 00000000..76fa1625
--- /dev/null
+++ b/examples/custom_shader/src/main.rs
@@ -0,0 +1,174 @@
+mod camera;
+mod cubes;
+mod pipeline;
+mod primitive;
+
+use crate::cubes::Cubes;
+use iced::widget::{
+ checkbox, column, container, row, slider, text, vertical_space, Shader,
+};
+use iced::{
+ executor, window, Alignment, Application, Color, Command, Element, Length,
+ Renderer, Subscription, Theme,
+};
+use std::time::Instant;
+
+fn main() -> iced::Result {
+ IcedCubes::run(iced::Settings::default())
+}
+
+struct IcedCubes {
+ start: Instant,
+ cubes: Cubes,
+ num_cubes_slider: u32,
+}
+
+impl Default for IcedCubes {
+ fn default() -> Self {
+ Self {
+ start: Instant::now(),
+ cubes: Cubes::new(),
+ num_cubes_slider: cubes::MAX,
+ }
+ }
+}
+
+#[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>) {
+ (IcedCubes::default(), Command::none())
+ }
+
+ fn title(&self) -> String {
+ "Iced Cubes".to_string()
+ }
+
+ fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
+ match message {
+ Message::CubeAmountChanged(num) => {
+ self.num_cubes_slider = num;
+ self.cubes.adjust_num_cubes(num);
+ }
+ Message::CubeSizeChanged(size) => {
+ self.cubes.size = size;
+ }
+ Message::Tick(time) => {
+ self.cubes.update(time - self.start);
+ }
+ Message::ShowDepthBuffer(show) => {
+ self.cubes.show_depth_buffer = show;
+ }
+ Message::LightColorChanged(color) => {
+ self.cubes.light_color = color;
+ }
+ }
+
+ Command::none()
+ }
+
+ fn view(&self) -> Element<'_, Self::Message, Renderer<Self::Theme>> {
+ let top_controls = row![
+ control(
+ "Amount",
+ slider(
+ 1..=cubes::MAX,
+ self.num_cubes_slider,
+ Message::CubeAmountChanged
+ )
+ .width(100)
+ ),
+ control(
+ "Size",
+ slider(0.1..=0.25, self.cubes.size, Message::CubeSizeChanged)
+ .step(0.01)
+ .width(100),
+ ),
+ checkbox(
+ "Show Depth Buffer",
+ self.cubes.show_depth_buffer,
+ Message::ShowDepthBuffer
+ ),
+ ]
+ .spacing(40);
+
+ let bottom_controls = row![
+ control(
+ "R",
+ slider(0.0..=1.0, self.cubes.light_color.r, move |r| {
+ Message::LightColorChanged(Color {
+ r,
+ ..self.cubes.light_color
+ })
+ })
+ .step(0.01)
+ .width(100)
+ ),
+ control(
+ "G",
+ slider(0.0..=1.0, self.cubes.light_color.g, move |g| {
+ Message::LightColorChanged(Color {
+ g,
+ ..self.cubes.light_color
+ })
+ })
+ .step(0.01)
+ .width(100)
+ ),
+ control(
+ "B",
+ slider(0.0..=1.0, self.cubes.light_color.b, move |b| {
+ Message::LightColorChanged(Color {
+ b,
+ ..self.cubes.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::new(&self.cubes)
+ .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()
+}
diff --git a/examples/custom_shader/src/pipeline.rs b/examples/custom_shader/src/pipeline.rs
new file mode 100644
index 00000000..9dd154e8
--- /dev/null
+++ b/examples/custom_shader/src/pipeline.rs
@@ -0,0 +1,600 @@
+use crate::primitive;
+use crate::primitive::cube;
+use crate::primitive::{Buffer, Uniforms};
+use iced::{Rectangle, Size};
+use wgpu::util::DeviceExt;
+
+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: &[primitive::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: true,
+ },
+ },
+ )],
+ depth_stencil_attachment: Some(
+ wgpu::RenderPassDepthStencilAttachment {
+ view: &self.depth_view,
+ depth_ops: Some(wgpu::Operations {
+ load: wgpu::LoadOp::Clear(1.0),
+ store: true,
+ }),
+ stencil_ops: 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: Default::default(),
+ depth_stencil: Some(wgpu::DepthStencilState {
+ format: wgpu::TextureFormat::Depth32Float,
+ depth_write_enabled: false,
+ depth_compare: wgpu::CompareFunction::Less,
+ stencil: Default::default(),
+ bias: Default::default(),
+ }),
+ multisample: Default::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: true,
+ },
+ })],
+ depth_stencil_attachment: Some(
+ wgpu::RenderPassDepthStencilAttachment {
+ view: &self.depth_view,
+ depth_ops: None,
+ stencil_ops: 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()
+}
diff --git a/examples/custom_shader/src/primitive.rs b/examples/custom_shader/src/primitive.rs
new file mode 100644
index 00000000..2201218f
--- /dev/null
+++ b/examples/custom_shader/src/primitive.rs
@@ -0,0 +1,95 @@
+pub mod cube;
+pub mod vertex;
+
+mod buffer;
+mod uniforms;
+
+use crate::camera::Camera;
+use crate::pipeline::Pipeline;
+use crate::primitive::cube::Cube;
+use iced::advanced::graphics::Transformation;
+use iced::widget::shader;
+use iced::{Color, Rectangle, Size};
+
+pub use crate::primitive::vertex::Vertex;
+pub use buffer::Buffer;
+pub use uniforms::Uniforms;
+
+/// A collection of `Cube`s that can be rendered.
+#[derive(Debug)]
+pub struct Primitive {
+ cubes: Vec<cube::Raw>,
+ uniforms: 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 = 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: 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,
+ )
+ }
+}
diff --git a/examples/custom_shader/src/primitive/buffer.rs b/examples/custom_shader/src/primitive/buffer.rs
new file mode 100644
index 00000000..377ce1bb
--- /dev/null
+++ b/examples/custom_shader/src/primitive/buffer.rs
@@ -0,0 +1,39 @@
+// 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,
+ });
+ }
+ }
+}
diff --git a/examples/custom_shader/src/primitive/cube.rs b/examples/custom_shader/src/primitive/cube.rs
new file mode 100644
index 00000000..c23f2132
--- /dev/null
+++ b/examples/custom_shader/src/primitive/cube.rs
@@ -0,0 +1,324 @@
+use crate::primitive::Vertex;
+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),
+ },
+ ]
+ }
+}
diff --git a/examples/custom_shader/src/primitive/uniforms.rs b/examples/custom_shader/src/primitive/uniforms.rs
new file mode 100644
index 00000000..4fcb413b
--- /dev/null
+++ b/examples/custom_shader/src/primitive/uniforms.rs
@@ -0,0 +1,22 @@
+use crate::camera::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()),
+ }
+ }
+}
diff --git a/examples/custom_shader/src/primitive/vertex.rs b/examples/custom_shader/src/primitive/vertex.rs
new file mode 100644
index 00000000..6d17aa0f
--- /dev/null
+++ b/examples/custom_shader/src/primitive/vertex.rs
@@ -0,0 +1,29 @@
+#[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,
+ }
+ }
+}
diff --git a/examples/custom_shader/src/shaders/cubes.wgsl b/examples/custom_shader/src/shaders/cubes.wgsl
new file mode 100644
index 00000000..cd7f94d8
--- /dev/null
+++ b/examples/custom_shader/src/shaders/cubes.wgsl
@@ -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;
+}
diff --git a/examples/custom_shader/src/shaders/depth.wgsl b/examples/custom_shader/src/shaders/depth.wgsl
new file mode 100644
index 00000000..a3f7e5ec
--- /dev/null
+++ b/examples/custom_shader/src/shaders/depth.wgsl
@@ -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);
+}
diff --git a/examples/custom_shader/src/textures/ice_cube_normal_map.png b/examples/custom_shader/src/textures/ice_cube_normal_map.png
new file mode 100644
index 00000000..7b4b7228
--- /dev/null
+++ b/examples/custom_shader/src/textures/ice_cube_normal_map.png
Binary files differ
diff --git a/examples/custom_shader/src/textures/skybox/neg_x.jpg b/examples/custom_shader/src/textures/skybox/neg_x.jpg
new file mode 100644
index 00000000..00cc783d
--- /dev/null
+++ b/examples/custom_shader/src/textures/skybox/neg_x.jpg
Binary files differ
diff --git a/examples/custom_shader/src/textures/skybox/neg_y.jpg b/examples/custom_shader/src/textures/skybox/neg_y.jpg
new file mode 100644
index 00000000..548f6445
--- /dev/null
+++ b/examples/custom_shader/src/textures/skybox/neg_y.jpg
Binary files differ
diff --git a/examples/custom_shader/src/textures/skybox/neg_z.jpg b/examples/custom_shader/src/textures/skybox/neg_z.jpg
new file mode 100644
index 00000000..5698512e
--- /dev/null
+++ b/examples/custom_shader/src/textures/skybox/neg_z.jpg
Binary files differ
diff --git a/examples/custom_shader/src/textures/skybox/pos_x.jpg b/examples/custom_shader/src/textures/skybox/pos_x.jpg
new file mode 100644
index 00000000..dddecba7
--- /dev/null
+++ b/examples/custom_shader/src/textures/skybox/pos_x.jpg
Binary files differ
diff --git a/examples/custom_shader/src/textures/skybox/pos_y.jpg b/examples/custom_shader/src/textures/skybox/pos_y.jpg
new file mode 100644
index 00000000..361427fd
--- /dev/null
+++ b/examples/custom_shader/src/textures/skybox/pos_y.jpg
Binary files differ
diff --git a/examples/custom_shader/src/textures/skybox/pos_z.jpg b/examples/custom_shader/src/textures/skybox/pos_z.jpg
new file mode 100644
index 00000000..0085a49e
--- /dev/null
+++ b/examples/custom_shader/src/textures/skybox/pos_z.jpg
Binary files differ
diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs
index c26d52fe..0f32fca0 100644
--- a/examples/integration/src/main.rs
+++ b/examples/integration/src/main.rs
@@ -271,6 +271,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
&queue,
&mut encoder,
None,
+ frame.texture.format(),
&view,
primitive,
&viewport,
diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml
index a7aea352..6741d7cf 100644
--- a/graphics/Cargo.toml
+++ b/graphics/Cargo.toml
@@ -16,7 +16,6 @@ all-features = true
[features]
geometry = ["lyon_path"]
-opengl = []
image = ["dep:image", "kamadak-exif"]
web-colors = []
diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs
index 78dec847..8c5ee2f0 100644
--- a/renderer/src/lib.rs
+++ b/renderer/src/lib.rs
@@ -7,6 +7,7 @@ pub mod compositor;
pub mod geometry;
mod settings;
+pub mod widget;
pub use iced_graphics as graphics;
pub use iced_graphics::core;
@@ -59,6 +60,26 @@ impl<T> Renderer<T> {
}
}
}
+
+ pub fn draw_custom<P: widget::shader::Primitive>(
+ &mut self,
+ bounds: Rectangle,
+ primitive: P,
+ ) {
+ match self {
+ Renderer::TinySkia(_) => {
+ log::warn!(
+ "Custom shader primitive is unavailable with tiny-skia."
+ );
+ }
+ #[cfg(feature = "wgpu")]
+ Renderer::Wgpu(renderer) => {
+ renderer.draw_primitive(iced_wgpu::Primitive::Custom(
+ iced_wgpu::primitive::Custom::shader(bounds, primitive),
+ ))
+ }
+ }
+ }
}
impl<T> core::Renderer for Renderer<T> {
diff --git a/renderer/src/widget.rs b/renderer/src/widget.rs
index 6c0c2a83..0422c99c 100644
--- a/renderer/src/widget.rs
+++ b/renderer/src/widget.rs
@@ -9,3 +9,6 @@ pub mod qr_code;
#[cfg(feature = "qr_code")]
pub use qr_code::QRCode;
+
+#[cfg(feature = "wgpu")]
+pub mod shader;
diff --git a/renderer/src/widget/shader.rs b/renderer/src/widget/shader.rs
new file mode 100644
index 00000000..da42a7dd
--- /dev/null
+++ b/renderer/src/widget/shader.rs
@@ -0,0 +1,215 @@
+//! A custom shader widget for wgpu applications.
+use crate::core::event::Status;
+use crate::core::layout::{Limits, Node};
+use crate::core::mouse::{Cursor, Interaction};
+use crate::core::renderer::Style;
+use crate::core::widget::tree::{State, Tag};
+use crate::core::widget::{tree, Tree};
+use crate::core::{
+ self, layout, mouse, widget, Clipboard, Element, Layout, Length, Rectangle,
+ Shell, Size, Widget,
+};
+use std::marker::PhantomData;
+
+mod event;
+mod program;
+
+pub use event::Event;
+pub use iced_wgpu::custom::Primitive;
+pub use iced_wgpu::custom::Storage;
+pub use program::Program;
+
+/// 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, Theme> Widget<Message, crate::Renderer<Theme>>
+ for Shader<Message, P>
+where
+ P: Program<Message>,
+{
+ fn tag(&self) -> Tag {
+ struct Tag<T>(T);
+ tree::Tag::of::<Tag<P::State>>()
+ }
+
+ fn state(&self) -> 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: &crate::Renderer<Theme>,
+ limits: &Limits,
+ ) -> 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: Cursor,
+ _renderer: &crate::Renderer<Theme>,
+ _clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ _viewport: &Rectangle,
+ ) -> 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)),
+ _ => 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: Cursor,
+ _viewport: &Rectangle,
+ _renderer: &crate::Renderer<Theme>,
+ ) -> 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 crate::Renderer<Theme>,
+ _theme: &Theme,
+ _style: &Style,
+ layout: Layout<'_>,
+ cursor_position: mouse::Cursor,
+ _viewport: &Rectangle,
+ ) {
+ let bounds = layout.bounds();
+ let state = tree.state.downcast_ref::<P::State>();
+
+ renderer.draw_custom(
+ bounds,
+ self.program.draw(state, cursor_position, bounds),
+ );
+ }
+}
+
+impl<'a, M, P, Theme> From<Shader<M, P>>
+ for Element<'a, M, crate::Renderer<Theme>>
+where
+ M: 'a,
+ P: Program<M> + 'a,
+{
+ fn from(custom: Shader<M, P>) -> Element<'a, M, crate::Renderer<Theme>> {
+ 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: Cursor,
+ shell: &mut Shell<'_, Message>,
+ ) -> (Status, Option<Message>) {
+ T::update(self, state, event, bounds, cursor, shell)
+ }
+
+ fn draw(
+ &self,
+ state: &Self::State,
+ cursor: Cursor,
+ bounds: Rectangle,
+ ) -> Self::Primitive {
+ T::draw(self, state, cursor, bounds)
+ }
+
+ fn mouse_interaction(
+ &self,
+ state: &Self::State,
+ bounds: Rectangle,
+ cursor: Cursor,
+ ) -> Interaction {
+ T::mouse_interaction(self, state, bounds, cursor)
+ }
+}
diff --git a/renderer/src/widget/shader/event.rs b/renderer/src/widget/shader/event.rs
new file mode 100644
index 00000000..981b30d7
--- /dev/null
+++ b/renderer/src/widget/shader/event.rs
@@ -0,0 +1,21 @@
+//! Handle events of a custom shader widget.
+use crate::core::keyboard;
+use crate::core::mouse;
+use crate::core::touch;
+
+pub use crate::core::event::Status;
+
+/// A [`Shader`] event.
+///
+/// [`Shader`]: crate::widget::shader::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),
+}
diff --git a/renderer/src/widget/shader/program.rs b/renderer/src/widget/shader/program.rs
new file mode 100644
index 00000000..b8871688
--- /dev/null
+++ b/renderer/src/widget/shader/program.rs
@@ -0,0 +1,60 @@
+use crate::core::{event, mouse, Rectangle, Shell};
+use crate::widget;
+use widget::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::widget::shader::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: shader::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::widget::shader::Shader
+ fn mouse_interaction(
+ &self,
+ _state: &Self::State,
+ _bounds: Rectangle,
+ _cursor: mouse::Cursor,
+ ) -> mouse::Interaction {
+ mouse::Interaction::default()
+ }
+}
diff --git a/style/src/theme.rs b/style/src/theme.rs
index 47010728..cc31d72d 100644
--- a/style/src/theme.rs
+++ b/style/src/theme.rs
@@ -1,7 +1,7 @@
//! Use the built-in theme and styles.
pub mod palette;
-pub use palette::Palette;
+pub use self::palette::Palette;
use crate::application;
use crate::button;
diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs
index 2bd29f42..907611d9 100644
--- a/wgpu/src/backend.rs
+++ b/wgpu/src/backend.rs
@@ -3,9 +3,7 @@ use crate::graphics::backend;
use crate::graphics::color;
use crate::graphics::{Transformation, Viewport};
use crate::primitive::{self, Primitive};
-use crate::quad;
-use crate::text;
-use crate::triangle;
+use crate::{custom, quad, text, triangle};
use crate::{Layer, Settings};
#[cfg(feature = "tracing")]
@@ -25,6 +23,7 @@ pub struct Backend {
quad_pipeline: quad::Pipeline,
text_pipeline: text::Pipeline,
triangle_pipeline: triangle::Pipeline,
+ pipeline_storage: custom::Storage,
#[cfg(any(feature = "image", feature = "svg"))]
image_pipeline: image::Pipeline,
@@ -50,6 +49,7 @@ impl Backend {
quad_pipeline,
text_pipeline,
triangle_pipeline,
+ pipeline_storage: custom::Storage::default(),
#[cfg(any(feature = "image", feature = "svg"))]
image_pipeline,
@@ -66,6 +66,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 +89,7 @@ impl Backend {
self.prepare(
device,
queue,
+ format,
encoder,
scale_factor,
target_size,
@@ -117,6 +119,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 +182,20 @@ impl Backend {
target_size,
);
}
+
+ if !layer.shaders.is_empty() {
+ for shader in &layer.shaders {
+ shader.primitive.prepare(
+ format,
+ device,
+ queue,
+ target_size,
+ scale_factor,
+ transformation,
+ &mut self.pipeline_storage,
+ );
+ }
+ }
}
}
@@ -302,6 +319,47 @@ impl Backend {
text_layer += 1;
}
+
+ // kill render pass to let custom shaders get mut access to encoder
+ let _ = ManuallyDrop::into_inner(render_pass);
+
+ if !layer.shaders.is_empty() {
+ for shader in &layer.shaders {
+ //This extra check is needed since each custom pipeline must set it's own
+ //scissor rect, which will panic if bounds.w/h < 1
+ let bounds = shader.bounds * scale_factor;
+
+ if bounds.width < 1.0 || bounds.height < 1.0 {
+ continue;
+ }
+
+ shader.primitive.render(
+ &self.pipeline_storage,
+ bounds.into(),
+ target,
+ target_size,
+ encoder,
+ );
+ }
+ }
+
+ // recreate and continue processing layers
+ render_pass = ManuallyDrop::new(encoder.begin_render_pass(
+ &wgpu::RenderPassDescriptor {
+ label: Some("iced_wgpu::quad render pass"),
+ color_attachments: &[Some(
+ wgpu::RenderPassColorAttachment {
+ view: target,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Load,
+ store: true,
+ },
+ },
+ )],
+ depth_stencil_attachment: None,
+ },
+ ));
}
let _ = ManuallyDrop::into_inner(render_pass);
diff --git a/wgpu/src/custom.rs b/wgpu/src/custom.rs
new file mode 100644
index 00000000..65dd0496
--- /dev/null
+++ b/wgpu/src/custom.rs
@@ -0,0 +1,66 @@
+use crate::core::{Rectangle, Size};
+use crate::graphics::Transformation;
+use std::any::{Any, TypeId};
+use std::collections::HashMap;
+use std::fmt::Debug;
+
+/// 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.")
+ })
+ }
+}
+
+/// 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,
+ );
+}
diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs
index 286801e6..d451cbfd 100644
--- a/wgpu/src/layer.rs
+++ b/wgpu/src/layer.rs
@@ -34,6 +34,9 @@ pub struct Layer<'a> {
/// The images of the [`Layer`].
pub images: Vec<Image>,
+
+ /// The custom shader primitives of this [`Layer`].
+ pub shaders: Vec<primitive::Shader>,
}
impl<'a> Layer<'a> {
@@ -45,6 +48,7 @@ impl<'a> Layer<'a> {
meshes: Vec::new(),
text: Vec::new(),
images: Vec::new(),
+ shaders: Vec::new(),
}
}
@@ -308,6 +312,18 @@ impl<'a> Layer<'a> {
}
}
},
+ primitive::Custom::Shader(shader) => {
+ let layer = &mut layers[current_layer];
+
+ let bounds = Rectangle::new(
+ Point::new(translation.x, translation.y),
+ shader.bounds.size(),
+ );
+
+ if layer.bounds.intersection(&bounds).is_some() {
+ layer.shaders.push(shader.clone());
+ }
+ }
},
}
}
diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs
index 424dfeb3..13d8e886 100644
--- a/wgpu/src/lib.rs
+++ b/wgpu/src/lib.rs
@@ -29,6 +29,7 @@
rustdoc::broken_intra_doc_links
)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
+pub mod custom;
pub mod layer;
pub mod primitive;
pub mod settings;
diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs
index 8dbf3008..4347dcda 100644
--- a/wgpu/src/primitive.rs
+++ b/wgpu/src/primitive.rs
@@ -1,6 +1,10 @@
//! Draw using different graphical primitives.
use crate::core::Rectangle;
+use crate::custom;
use crate::graphics::{Damage, Mesh};
+use std::any::Any;
+use std::fmt::Debug;
+use std::sync::Arc;
/// The graphical primitives supported by `iced_wgpu`.
pub type Primitive = crate::graphics::Primitive<Custom>;
@@ -10,12 +14,44 @@ pub type Primitive = crate::graphics::Primitive<Custom>;
pub enum Custom {
/// A mesh primitive.
Mesh(Mesh),
+ /// A custom shader primitive
+ Shader(Shader),
+}
+
+impl Custom {
+ /// Create a custom [`Shader`] primitive.
+ pub fn shader<P: custom::Primitive>(
+ bounds: Rectangle,
+ primitive: P,
+ ) -> Self {
+ Self::Shader(Shader {
+ bounds,
+ primitive: Arc::new(primitive),
+ })
+ }
}
impl Damage for Custom {
fn bounds(&self) -> Rectangle {
match self {
Self::Mesh(mesh) => mesh.bounds(),
+ Self::Shader(shader) => shader.bounds,
}
}
}
+
+#[derive(Clone, Debug)]
+/// A custom primitive which can be used to render primitives associated with a custom pipeline.
+pub struct Shader {
+ /// The bounds of the [`Shader`].
+ pub bounds: Rectangle,
+
+ /// The [`custom::Primitive`] to render.
+ pub primitive: Arc<dyn custom::Primitive>,
+}
+
+impl PartialEq for Shader {
+ fn eq(&self, other: &Self) -> bool {
+ self.primitive.type_id() == other.primitive.type_id()
+ }
+}
diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs
index 1ddbe5fe..90d64e17 100644
--- a/wgpu/src/window/compositor.rs
+++ b/wgpu/src/window/compositor.rs
@@ -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,
diff --git a/widget/Cargo.toml b/widget/Cargo.toml
index 6d62c181..032f801c 100644
--- a/widget/Cargo.toml
+++ b/widget/Cargo.toml
@@ -20,6 +20,7 @@ image = ["iced_renderer/image"]
svg = ["iced_renderer/svg"]
canvas = ["iced_renderer/geometry"]
qr_code = ["canvas", "qrcode"]
+wgpu = []
[dependencies]
iced_renderer.workspace = true
diff --git a/widget/src/lib.rs b/widget/src/lib.rs
index 2f959370..e052f398 100644
--- a/widget/src/lib.rs
+++ b/widget/src/lib.rs
@@ -97,6 +97,9 @@ pub use tooltip::Tooltip;
#[doc(no_inline)]
pub use vertical_slider::VerticalSlider;
+#[cfg(feature = "wgpu")]
+pub use renderer::widget::shader::{self, Shader};
+
#[cfg(feature = "svg")]
pub mod svg;