use bytemuck::{Pod, Zeroable};
use cranpose_render_common::layer_shadow::ShadowRRect;
use cranpose_ui_graphics::{BlendMode, Rect};
use crate::{
geometry::{canonicalize_device_coordinate, snap_delta_for_anchor},
render::{blend_state_for_mode, create_render_pipeline_logged, overlay_depth_state},
scene::RRectShadowDraw,
shared_shader::SharedShader,
};
pub(crate) const SHADOW_QUAD_CORNERS: u32 = 4;
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub(crate) struct ShadowInstance {
draw: [f32; 4],
bounds: [f32; 4],
radii: [f32; 4],
ramp: [f32; 4],
color: [f32; 4],
}
impl ShadowInstance {
const ATTRIBS: [wgpu::VertexAttribute; 5] = wgpu::vertex_attr_array![
0 => Float32x4,
1 => Float32x4,
2 => Float32x4,
3 => Float32x4,
4 => Float32x4,
];
pub(crate) fn desc() -> wgpu::VertexBufferLayout<'static> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &Self::ATTRIBS,
}
}
}
pub(crate) fn device_shadow(draw: &RRectShadowDraw, root_scale: f32) -> ShadowRRect {
let snap = draw
.snap_anchor
.map(|anchor| snap_delta_for_anchor(anchor, root_scale))
.unwrap_or_default();
draw.shadow.translated(snap.x, snap.y).scaled(root_scale)
}
pub(crate) fn rrect_shadow_bounds(draw: &RRectShadowDraw, root_scale: f32) -> Option<Rect> {
let snap = draw
.snap_anchor
.map(|anchor| snap_delta_for_anchor(anchor, root_scale))
.unwrap_or_default();
let bounds = draw.shadow.bounds.translate(snap.x, snap.y);
match draw.clip {
Some(clip) => clip.translate(snap.x, snap.y).intersect(bounds),
None => Some(bounds),
}
}
pub(crate) fn device_clip(draw: &RRectShadowDraw, root_scale: f32) -> Option<[f32; 4]> {
let snap = draw
.snap_anchor
.map(|anchor| snap_delta_for_anchor(anchor, root_scale))
.unwrap_or_default();
draw.clip.map(|clip| {
let clip = clip.translate(snap.x, snap.y);
let edge = |value: f32| canonicalize_device_coordinate(value * root_scale);
[
edge(clip.x).floor(),
edge(clip.y).floor(),
edge(clip.x + clip.width).ceil(),
edge(clip.y + clip.height).ceil(),
]
})
}
fn edges(rect: Rect) -> [f32; 4] {
[rect.x, rect.y, rect.x + rect.width, rect.y + rect.height]
}
fn intersect(a: [f32; 4], b: [f32; 4]) -> Option<[f32; 4]> {
let cut = [
a[0].max(b[0]),
a[1].max(b[1]),
a[2].min(b[2]),
a[3].min(b[3]),
];
(cut[0] < cut[2] && cut[1] < cut[3]).then_some(cut)
}
pub(crate) fn append_shadow_instances(
draw: &RRectShadowDraw,
root_scale: f32,
instances: &mut Vec<ShadowInstance>,
) {
let shadow = device_shadow(draw, root_scale);
let bounds = edges(shadow.bounds);
let Some(area) =
device_clip(draw, root_scale).map_or(Some(bounds), |clip| intersect(bounds, clip))
else {
return;
};
let color = [
draw.color.r(),
draw.color.g(),
draw.color.b(),
draw.color.a(),
];
let ramp = |hole_inset: f32, hole_radius: f32| {
[
shadow.umbra_inset,
shadow.distance_correction,
hole_inset,
hole_radius,
]
};
let instance = |rect: [f32; 4], ramp: [f32; 4]| ShadowInstance {
draw: rect,
bounds,
radii: shadow.radii,
ramp,
color,
};
match (shadow.hole, shadow.hole_rect()) {
(Some(hole), Some(rect)) if hole.radius <= 0.0 => {
let [left, top, right, bottom] = edges(rect);
let bands = [
[bounds[0], bounds[1], bounds[2], top],
[bounds[0], bottom, bounds[2], bounds[3]],
[bounds[0], top, left, bottom],
[right, top, bounds[2], bottom],
];
instances.extend(
bands
.into_iter()
.filter_map(|band| intersect(band, area))
.map(|band| instance(band, ramp(0.0, -1.0))),
);
}
(Some(hole), _) => instances.push(instance(area, ramp(hole.inset, hole.radius))),
(None, _) => instances.push(instance(area, ramp(0.0, -1.0))),
}
}
pub(crate) fn create_rrect_shadow_pipeline(
device: &wgpu::Device,
cache: Option<&wgpu::PipelineCache>,
surface_format: wgpu::TextureFormat,
shader: &SharedShader,
depth: bool,
) -> wgpu::RenderPipeline {
let module = shader.module();
create_render_pipeline_logged(
device,
cache,
if depth {
"rrect-shadow depth"
} else {
"rrect-shadow"
},
wgpu::RenderPipelineDescriptor {
label: Some("Round Rect Shadow Pipeline"),
layout: Some(shader.layout()),
vertex: wgpu::VertexState {
module,
entry_point: Some("rrect_shadow_vs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
buffers: &[Some(ShadowInstance::desc())],
},
fragment: Some(wgpu::FragmentState {
module,
entry_point: Some("rrect_shadow_fs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(blend_state_for_mode(BlendMode::SrcOver)),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: overlay_depth_state(depth),
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
},
)
}