mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-26 02:57:34 -06:00
921 lines
40 KiB
Rust
921 lines
40 KiB
Rust
//! The Advanced 3D rasteriser on wgpu: a render pipeline with a reversed-Z depth buffer
|
||
//! (`Depth32Float`, Greater), the scene's materials, textures, lights and shadow maps in
|
||
//! storage buffers, and `advanced3d.wgsl`'s physically based fragment shader. Opaque triangles
|
||
//! are drawn with depth writes, then the sorted transparent ones blended over (premultiplied).
|
||
//! Colour (`Rgba16Float`) and camera depth (`R32Uint` holding f32 bits) stay on the GPU for `adv3d.wgsl`'s
|
||
//! compute kernels, which run the rest of `effectcraft_render::three_d::adv::render_prepared`:
|
||
//! the 2×2 supersampling resolve, the motion-blur sub-sample average (nearest depth), the
|
||
//! depth-based iris depth of field (the Classic 3D bokeh kernel's row spans and prefix-sum
|
||
//! gather, highlight boost, progressive blend between blur levels), the sRGB encode and the
|
||
//! premultiplied "over" onto the compositor's canvas. Only the depth of field reads anything
|
||
//! back (its 8-byte radius range, to choose the blur levels as the CPU does).
|
||
//!
|
||
//! Wireframe-quality layers draw their outlines here too ([`wireframe`]).
|
||
|
||
use effectcraft_render::three_d::Dof;
|
||
use effectcraft_render::three_d::adv::{Prepared, Rendered, Scene, Target};
|
||
use effectcraft_render::three_d::bokeh;
|
||
use wgpu::util::DeviceExt;
|
||
|
||
use crate::context::{Enc, GpuContext, GpuImage};
|
||
|
||
/// Pipelines (built on first use).
|
||
pub(crate) struct Pipes {
|
||
bgl: wgpu::BindGroupLayout,
|
||
/// (opaque, transparent); `None` when the adapter can't rasterise ([`raster_unsupported`]):
|
||
/// scenes then render on the CPU.
|
||
raster: Option<(wgpu::RenderPipeline, wgpu::RenderPipeline)>,
|
||
post: Post,
|
||
}
|
||
|
||
fn pipes(g: &GpuContext) -> &Pipes {
|
||
g.adv3d.get_or_init(|| Pipes::new(&g.device, g.adv3d_raster))
|
||
}
|
||
|
||
const COLOR: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
|
||
/// Camera depth as the bits of an f32: integer targets render on every backend, while `R32Float`
|
||
/// needs `EXT_color_buffer_float` on GLES (absent on Mesa's llvmpipe, for one).
|
||
const DEPTH_OUT: wgpu::TextureFormat = wgpu::TextureFormat::R32Uint;
|
||
/// [`DEPTH_OUT`]'s clear value: the bits of −1.0 ("nothing drawn").
|
||
const NO_DEPTH: f64 = 0xBF80_0000u32 as f64;
|
||
const DEPTH: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
|
||
const VERTEX_SIZE: u64 = 52;
|
||
|
||
/// Why `adapter` can't run the rasteriser, or `None` when it can. Building the pipelines anyway
|
||
/// would be a validation error, which release builds only log (and then draw nothing), so the
|
||
/// caller keeps the scenes on the CPU instead.
|
||
pub(crate) fn raster_unsupported(adapter: &wgpu::Adapter) -> Option<String> {
|
||
use wgpu::TextureFormatFeatureFlags as F;
|
||
let needs = [(COLOR, F::BLENDABLE), (DEPTH_OUT, F::empty()), (DEPTH, F::empty())];
|
||
for (format, flags) in needs {
|
||
let f = adapter.get_texture_format_features(format);
|
||
if !f.allowed_usages.contains(wgpu::TextureUsages::RENDER_ATTACHMENT) || !f.flags.contains(flags) {
|
||
return Some(format!("{format:?} is not a {}render target", if flags.is_empty() { "" } else { "blendable " }));
|
||
}
|
||
}
|
||
// The transparent pipeline blends colour and masks the depth target off.
|
||
if !adapter.get_downlevel_capabilities().flags.contains(wgpu::DownlevelFlags::INDEPENDENT_BLEND) {
|
||
return Some("no independent blending".into());
|
||
}
|
||
None
|
||
}
|
||
|
||
fn storage(binding: u32) -> wgpu::BindGroupLayoutEntry {
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding,
|
||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||
ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, min_binding_size: None },
|
||
count: None,
|
||
}
|
||
}
|
||
|
||
impl Pipes {
|
||
fn new(device: &wgpu::Device, raster: bool) -> Pipes {
|
||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||
label: Some("effectcraft advanced 3d"),
|
||
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/advanced3d.wgsl").into()),
|
||
});
|
||
let mut entries = vec![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,
|
||
}];
|
||
entries.extend((1..=6).map(storage));
|
||
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("advanced 3d"), entries: &entries });
|
||
let layout =
|
||
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("advanced 3d"), bind_group_layouts: &[Some(&bgl)], immediate_size: 0 });
|
||
let attrs = [
|
||
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x3, offset: 0, shader_location: 0 },
|
||
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x3, offset: 12, shader_location: 1 },
|
||
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x2, offset: 24, shader_location: 2 },
|
||
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x4, offset: 32, shader_location: 3 },
|
||
wgpu::VertexAttribute { format: wgpu::VertexFormat::Uint32, offset: 48, shader_location: 4 },
|
||
];
|
||
let make = |transparent: bool| {
|
||
let blend = transparent.then_some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING);
|
||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||
label: Some(if transparent { "advanced 3d transparent" } else { "advanced 3d opaque" }),
|
||
layout: Some(&layout),
|
||
vertex: wgpu::VertexState {
|
||
module: &module,
|
||
entry_point: Some("vs"),
|
||
compilation_options: Default::default(),
|
||
buffers: &[Some(wgpu::VertexBufferLayout { array_stride: VERTEX_SIZE, step_mode: wgpu::VertexStepMode::Vertex, attributes: &attrs })],
|
||
},
|
||
primitive: wgpu::PrimitiveState {
|
||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||
front_face: wgpu::FrontFace::Ccw,
|
||
cull_mode: None,
|
||
..Default::default()
|
||
},
|
||
depth_stencil: Some(wgpu::DepthStencilState {
|
||
format: DEPTH,
|
||
depth_write_enabled: Some(!transparent),
|
||
depth_compare: Some(wgpu::CompareFunction::Greater),
|
||
stencil: Default::default(),
|
||
bias: Default::default(),
|
||
}),
|
||
multisample: Default::default(),
|
||
fragment: Some(wgpu::FragmentState {
|
||
module: &module,
|
||
entry_point: Some("fs"),
|
||
compilation_options: Default::default(),
|
||
targets: &[
|
||
Some(wgpu::ColorTargetState { format: COLOR, blend, write_mask: wgpu::ColorWrites::ALL }),
|
||
Some(wgpu::ColorTargetState {
|
||
format: DEPTH_OUT,
|
||
blend: None,
|
||
write_mask: if transparent { wgpu::ColorWrites::empty() } else { wgpu::ColorWrites::ALL },
|
||
}),
|
||
],
|
||
}),
|
||
multiview_mask: None,
|
||
cache: None,
|
||
})
|
||
};
|
||
Pipes { raster: raster.then(|| (make(false), make(true))), bgl, post: Post::new(device) }
|
||
}
|
||
}
|
||
|
||
fn f32s(v: impl IntoIterator<Item = f32>) -> Vec<u8> {
|
||
let mut b: Vec<u8> = v.into_iter().flat_map(f32::to_le_bytes).collect();
|
||
while b.len() < 16 {
|
||
b.push(0);
|
||
}
|
||
b
|
||
}
|
||
|
||
/// Pack the scene's arrays as the shader's storage buffers.
|
||
fn pack(s: &Scene) -> [Vec<u8>; 7] {
|
||
let mut globals: Vec<f32> = vec![];
|
||
// Column-major clip matrix.
|
||
for c in 0..4 {
|
||
for r in 0..4 {
|
||
globals.push(s.clip[r][c]);
|
||
}
|
||
}
|
||
globals.extend(s.view[2]);
|
||
globals.extend([s.eye[0], s.eye[1], s.eye[2], s.ortho as u32 as f32]);
|
||
globals.extend([s.cam_fwd[0], s.cam_fwd[1], s.cam_fwd[2], s.lights.len() as f32]);
|
||
globals.extend([s.ambient[0], s.ambient[1], s.ambient[2], s.env.is_some() as u32 as f32]);
|
||
let e = s.env.unwrap_or(effectcraft_render::three_d::adv::EnvInfo { radiance: 0, mips: 1, irradiance: 0, intensity: 0.0, rotation: 0.0 });
|
||
globals.extend([e.radiance as f32, e.mips as f32, e.irradiance as f32, e.intensity]);
|
||
let lit = !(s.lights.is_empty() && s.env.is_none() && s.ambient == [0.0; 3]);
|
||
globals.extend([e.rotation, lit as u32 as f32, 0.0, 0.0]);
|
||
let mats = s.materials.iter().flat_map(|m| {
|
||
let flags = (m.double_sided as u32) | (m.unlit as u32) << 1 | (m.accepts_lights as u32) << 2 | (m.receives_shadows as u32) << 3 | (m.alpha_mode << 8);
|
||
[
|
||
m.base[0],
|
||
m.base[1],
|
||
m.base[2],
|
||
m.base[3],
|
||
m.emissive[0],
|
||
m.emissive[1],
|
||
m.emissive[2],
|
||
m.metallic,
|
||
m.roughness,
|
||
m.normal_scale,
|
||
m.occlusion_strength,
|
||
m.alpha_cutoff,
|
||
m.tex_base as f32,
|
||
m.tex_mr as f32,
|
||
m.tex_normal as f32,
|
||
m.tex_occlusion as f32,
|
||
m.tex_emissive as f32,
|
||
flags as f32,
|
||
m.diffuse_k,
|
||
m.specular_k,
|
||
m.ambient_k,
|
||
m.opacity,
|
||
0.0,
|
||
0.0,
|
||
]
|
||
});
|
||
let mut tex: Vec<u8> = s.textures.iter().flat_map(|t| [t.offset, t.width, t.height, t.wrap_u | t.wrap_v << 8]).flat_map(u32::to_le_bytes).collect();
|
||
while tex.len() < 16 {
|
||
tex.push(0);
|
||
}
|
||
let texels = f32s(s.texels.iter().flatten().copied());
|
||
let lights = s.lights.iter().flat_map(|l| {
|
||
[
|
||
l.pos[0],
|
||
l.pos[1],
|
||
l.pos[2],
|
||
l.kind as f32,
|
||
l.dir[0],
|
||
l.dir[1],
|
||
l.dir[2],
|
||
l.cos_outer,
|
||
l.color[0],
|
||
l.color[1],
|
||
l.color[2],
|
||
l.cos_inner,
|
||
l.falloff as f32,
|
||
l.radius,
|
||
l.falloff_distance,
|
||
l.shadow as f32,
|
||
l.shadow_darkness,
|
||
0.0,
|
||
0.0,
|
||
0.0,
|
||
]
|
||
});
|
||
let shadows = s.shadows.iter().flat_map(|m| {
|
||
let mut v: Vec<f32> = m.view.iter().flatten().copied().collect();
|
||
v.extend([m.focal, m.ortho as u32 as f32, f32::from_bits(m.size), f32::from_bits(m.offset), m.bias, m.radius, 0.0, 0.0]);
|
||
v
|
||
});
|
||
[f32s(globals), f32s(mats), tex, texels, f32s(lights), f32s(shadows), f32s(s.shadow_texels.iter().copied())]
|
||
}
|
||
|
||
fn half_to_f32(h: u16) -> f32 {
|
||
let s = if h & 0x8000 != 0 { -1.0 } else { 1.0 };
|
||
let e = ((h >> 10) & 0x1f) as i32;
|
||
let m = (h & 0x3ff) as f32;
|
||
match e {
|
||
0 => s * m * 2f32.powi(-24),
|
||
31 => {
|
||
if m == 0.0 {
|
||
s * f32::INFINITY
|
||
} else {
|
||
f32::NAN
|
||
}
|
||
}
|
||
_ => s * (1.0 + m / 1024.0) * 2f32.powi(e - 15),
|
||
}
|
||
}
|
||
|
||
/// Record the render pass of one scene into `e`: (colour `Rgba16Float`, camera depth `R32Uint`
|
||
/// with −1 where nothing was drawn) at the scene's raster size. `None` when the device can't
|
||
/// (render targets, size or buffer limits).
|
||
fn raster_into(e: &mut Enc, s: &Scene) -> Option<(wgpu::Texture, wgpu::Texture)> {
|
||
let g = e.g;
|
||
if !g.fits(s.width, s.height) || s.indices.is_empty() {
|
||
return None;
|
||
}
|
||
let limits = g.device.limits();
|
||
let bufs = pack(s);
|
||
if bufs.iter().any(|b| b.len() as u64 > limits.max_storage_buffer_binding_size) {
|
||
return None;
|
||
}
|
||
let p = pipes(g);
|
||
let Some((opaque, transparent)) = &p.raster else { return None };
|
||
let dev = &g.device;
|
||
let vb: Vec<u8> = s
|
||
.vertices
|
||
.iter()
|
||
.flat_map(|v| {
|
||
let mut b = Vec::with_capacity(VERTEX_SIZE as usize);
|
||
for x in v.pos.iter().chain(&v.normal).chain(&v.uv).chain(&v.tangent) {
|
||
b.extend_from_slice(&x.to_le_bytes());
|
||
}
|
||
b.extend_from_slice(&v.material.to_le_bytes());
|
||
b
|
||
})
|
||
.collect();
|
||
let vbuf = dev.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("adv3d vertices"), contents: &vb, usage: wgpu::BufferUsages::VERTEX });
|
||
let ib: Vec<u8> = s.indices.iter().flat_map(|i| i.to_le_bytes()).collect();
|
||
let ibuf = dev.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("adv3d indices"), contents: &ib, usage: wgpu::BufferUsages::INDEX });
|
||
let ub = dev.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("adv3d globals"), contents: &bufs[0], usage: wgpu::BufferUsages::UNIFORM });
|
||
let sbs: Vec<wgpu::Buffer> = bufs[1..]
|
||
.iter()
|
||
.map(|b| dev.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("adv3d data"), contents: b, usage: wgpu::BufferUsages::STORAGE }))
|
||
.collect();
|
||
let mut entries = vec![wgpu::BindGroupEntry { binding: 0, resource: ub.as_entire_binding() }];
|
||
for (i, b) in sbs.iter().enumerate() {
|
||
entries.push(wgpu::BindGroupEntry { binding: i as u32 + 1, resource: b.as_entire_binding() });
|
||
}
|
||
let bg = dev.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("adv3d"), layout: &p.bgl, entries: &entries });
|
||
let tex = |format: wgpu::TextureFormat, usage: wgpu::TextureUsages| {
|
||
dev.create_texture(&wgpu::TextureDescriptor {
|
||
label: Some("adv3d target"),
|
||
size: wgpu::Extent3d { width: s.width, height: s.height, depth_or_array_layers: 1 },
|
||
mip_level_count: 1,
|
||
sample_count: 1,
|
||
dimension: wgpu::TextureDimension::D2,
|
||
format,
|
||
usage,
|
||
view_formats: &[],
|
||
})
|
||
};
|
||
let rt = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING;
|
||
let color = tex(COLOR, rt);
|
||
let zout = tex(DEPTH_OUT, rt);
|
||
let depth = tex(DEPTH, wgpu::TextureUsages::RENDER_ATTACHMENT);
|
||
let (cv, zv, dv) = (color.create_view(&Default::default()), zout.create_view(&Default::default()), depth.create_view(&Default::default()));
|
||
let enc = e.encoder();
|
||
{
|
||
let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||
label: Some("adv3d"),
|
||
color_attachments: &[
|
||
Some(wgpu::RenderPassColorAttachment {
|
||
view: &cv,
|
||
depth_slice: None,
|
||
resolve_target: None,
|
||
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), store: wgpu::StoreOp::Store },
|
||
}),
|
||
Some(wgpu::RenderPassColorAttachment {
|
||
view: &zv,
|
||
depth_slice: None,
|
||
resolve_target: None,
|
||
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { r: NO_DEPTH, g: 0.0, b: 0.0, a: 0.0 }), store: wgpu::StoreOp::Store },
|
||
}),
|
||
],
|
||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||
view: &dv,
|
||
depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(0.0), store: wgpu::StoreOp::Store }),
|
||
stencil_ops: None,
|
||
}),
|
||
timestamp_writes: None,
|
||
occlusion_query_set: None,
|
||
multiview_mask: None,
|
||
});
|
||
pass.set_bind_group(0, &bg, &[]);
|
||
pass.set_vertex_buffer(0, vbuf.slice(..));
|
||
pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
|
||
let n = s.indices.len() as u32;
|
||
if s.opaque_count > 0 {
|
||
pass.set_pipeline(opaque);
|
||
pass.draw_indexed(0..s.opaque_count, 0, 0..1);
|
||
}
|
||
if n > s.opaque_count {
|
||
pass.set_pipeline(transparent);
|
||
pass.draw_indexed(s.opaque_count..n, 0, 0..1);
|
||
}
|
||
}
|
||
Some((color, zout))
|
||
}
|
||
|
||
/// Rasterise a scene and read it back ([`effectcraft_render::Accelerator::raster_3d`]). `None`
|
||
/// when the device can't (no readback, size or buffer limits).
|
||
///
|
||
/// With deferred readbacks (a browser worker) both targets are read back under the scene's key
|
||
/// ([`crate::deferred`]): the first pass starts them and gets an empty target (the pass misses).
|
||
pub(crate) fn render(g: &GpuContext, s: &Scene) -> Option<Target> {
|
||
if !g.can_readback() {
|
||
return None;
|
||
}
|
||
let mut e = Enc::new(g);
|
||
let (cb, zb) = match g.deferred.clone() {
|
||
Some(d) => {
|
||
use crate::deferred::Lookup::*;
|
||
let mut k = crate::deferred::Key::new(4);
|
||
key_scene(&mut k, s);
|
||
let kc = k.finish();
|
||
let kz = kc ^ 1;
|
||
match (d.lookup(kc), d.lookup(kz)) {
|
||
(Ready(c), Ready(z)) => (c?.bytes, z?.bytes),
|
||
(Absent, Absent) => {
|
||
let (color, zout) = raster_into(&mut e, s)?;
|
||
e.read_texture_async(&color, s.width, s.height, 8, d.start(kc, s.width, s.height, [0.0; 2], 1.0));
|
||
e.read_texture_async(&zout, s.width, s.height, 4, d.start(kz, s.width, s.height, [0.0; 2], 1.0));
|
||
d.miss();
|
||
return Some(empty_target(s.width, s.height));
|
||
}
|
||
_ => {
|
||
d.miss();
|
||
return Some(empty_target(s.width, s.height));
|
||
}
|
||
}
|
||
}
|
||
None => {
|
||
let (color, zout) = raster_into(&mut e, s)?;
|
||
(e.read_texture(&color, s.width, s.height, 8)?, e.read_texture(&zout, s.width, s.height, 4)?)
|
||
}
|
||
};
|
||
if cb.len() != (s.width * s.height * 8) as usize || zb.len() != (s.width * s.height * 4) as usize {
|
||
return None;
|
||
}
|
||
let color = cb.as_chunks::<8>().0.iter().map(|c| [0, 1, 2, 3].map(|k| half_to_f32(u16::from_le_bytes([c[2 * k], c[2 * k + 1]])))).collect();
|
||
let depth = zb
|
||
.as_chunks::<4>()
|
||
.0
|
||
.iter()
|
||
.map(|c| {
|
||
let z = f32::from_le_bytes([c[0], c[1], c[2], c[3]]);
|
||
if z < 0.0 { f32::INFINITY } else { z }
|
||
})
|
||
.collect();
|
||
Some(Target { width: s.width, height: s.height, color, depth })
|
||
}
|
||
|
||
/// A placeholder target (a pass whose readbacks are in flight).
|
||
fn empty_target(w: u32, h: u32) -> Target {
|
||
let n = (w * h) as usize;
|
||
Target { width: w, height: h, color: vec![[0.0; 4]; n], depth: vec![f32::INFINITY; n] }
|
||
}
|
||
|
||
/// Everything that determines a scene's pixels, for deferred readback keys: the packed
|
||
/// buffers, the geometry, the target size.
|
||
fn key_scene(k: &mut crate::deferred::Key, s: &Scene) {
|
||
k.u64(s.width as u64);
|
||
k.u64(s.height as u64);
|
||
k.u64(s.ssaa as u64);
|
||
k.u64(s.opaque_count as u64);
|
||
for b in pack(s) {
|
||
k.u64(b.len() as u64);
|
||
k.bytes(&b);
|
||
}
|
||
let mut v = Vec::with_capacity(s.vertices.len() * VERTEX_SIZE as usize);
|
||
for x in &s.vertices {
|
||
for f in x.pos.iter().chain(&x.normal).chain(&x.uv).chain(&x.tangent) {
|
||
v.extend_from_slice(&f.to_le_bytes());
|
||
}
|
||
v.extend_from_slice(&x.material.to_le_bytes());
|
||
}
|
||
k.bytes(&v);
|
||
k.bytes(&s.indices.iter().flat_map(|i| i.to_le_bytes()).collect::<Vec<u8>>());
|
||
}
|
||
|
||
// ---------------------------------------------------------------- after the rasteriser
|
||
|
||
/// Compute kernels of `adv3d.wgsl` (resolve, motion-blur accumulation, depth of field,
|
||
/// encoding, compositing, wireframes) with their shared bind group layout and the placeholder
|
||
/// resources bound to the slots a kernel doesn't use.
|
||
pub(crate) struct Post {
|
||
bgl: wgpu::BindGroupLayout,
|
||
kernels: Vec<(&'static str, wgpu::ComputePipeline)>,
|
||
/// Placeholders for bindings 3–9 (distinct buffers: writable bindings must not alias).
|
||
dummy_bufs: Vec<wgpu::Buffer>,
|
||
dummy_color: wgpu::TextureView,
|
||
dummy_z: wgpu::TextureView,
|
||
dummy_out: wgpu::TextureView,
|
||
}
|
||
|
||
const KERNELS: &[&str] = &["resolve", "radius_of", "boost", "prefix_rows", "gather", "finish", "wire", "occlude"];
|
||
|
||
impl Post {
|
||
fn new(device: &wgpu::Device) -> Post {
|
||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||
label: Some("effectcraft advanced 3d post"),
|
||
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/adv3d.wgsl").into()),
|
||
});
|
||
let tex = |binding, sample_type| wgpu::BindGroupLayoutEntry {
|
||
binding,
|
||
visibility: wgpu::ShaderStages::COMPUTE,
|
||
ty: wgpu::BindingType::Texture { sample_type, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false },
|
||
count: None,
|
||
};
|
||
let buf = |binding, read_only| wgpu::BindGroupLayoutEntry {
|
||
binding,
|
||
visibility: wgpu::ShaderStages::COMPUTE,
|
||
ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only }, has_dynamic_offset: false, min_binding_size: None },
|
||
count: None,
|
||
};
|
||
let entries = [
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 0,
|
||
visibility: wgpu::ShaderStages::COMPUTE,
|
||
ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None },
|
||
count: None,
|
||
},
|
||
tex(1, wgpu::TextureSampleType::Float { filterable: false }),
|
||
tex(2, wgpu::TextureSampleType::Uint),
|
||
buf(3, false),
|
||
buf(4, false),
|
||
buf(5, false),
|
||
buf(6, true),
|
||
buf(7, false),
|
||
buf(8, false),
|
||
buf(9, false),
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 10,
|
||
visibility: wgpu::ShaderStages::COMPUTE,
|
||
ty: wgpu::BindingType::StorageTexture {
|
||
access: wgpu::StorageTextureAccess::WriteOnly,
|
||
format: crate::context::FORMAT,
|
||
view_dimension: wgpu::TextureViewDimension::D2,
|
||
},
|
||
count: None,
|
||
},
|
||
];
|
||
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("advanced 3d post"), entries: &entries });
|
||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||
label: Some("advanced 3d post"),
|
||
bind_group_layouts: &[Some(&bgl)],
|
||
immediate_size: 0,
|
||
});
|
||
let kernels = KERNELS
|
||
.iter()
|
||
.map(|k| {
|
||
let p = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
||
label: Some(k),
|
||
layout: Some(&layout),
|
||
module: &module,
|
||
entry_point: Some(k),
|
||
compilation_options: Default::default(),
|
||
cache: None,
|
||
});
|
||
(*k, p)
|
||
})
|
||
.collect();
|
||
let dummy_bufs = (3..=9)
|
||
.map(|_| {
|
||
device.create_buffer(&wgpu::BufferDescriptor {
|
||
label: Some("adv3d dummy"),
|
||
size: 16,
|
||
usage: wgpu::BufferUsages::STORAGE,
|
||
mapped_at_creation: false,
|
||
})
|
||
})
|
||
.collect();
|
||
let tex1 = |format, usage| {
|
||
device
|
||
.create_texture(&wgpu::TextureDescriptor {
|
||
label: Some("adv3d dummy"),
|
||
size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
|
||
mip_level_count: 1,
|
||
sample_count: 1,
|
||
dimension: wgpu::TextureDimension::D2,
|
||
format,
|
||
usage,
|
||
view_formats: &[],
|
||
})
|
||
.create_view(&Default::default())
|
||
};
|
||
Post {
|
||
bgl,
|
||
kernels,
|
||
dummy_bufs,
|
||
dummy_color: tex1(COLOR, wgpu::TextureUsages::TEXTURE_BINDING),
|
||
dummy_z: tex1(DEPTH_OUT, wgpu::TextureUsages::TEXTURE_BINDING),
|
||
dummy_out: tex1(crate::context::FORMAT, wgpu::TextureUsages::STORAGE_BINDING),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The resources of one post kernel dispatch (unset slots get placeholders).
|
||
#[derive(Default)]
|
||
struct Binds<'b> {
|
||
color: Option<&'b wgpu::TextureView>,
|
||
z: Option<&'b wgpu::TextureView>,
|
||
/// Bindings 3–9: acc, depth, src, table, radius, range, prefix.
|
||
bufs: [Option<&'b wgpu::Buffer>; 7],
|
||
out: Option<&'b wgpu::TextureView>,
|
||
}
|
||
|
||
/// Uniform block of the post kernels (`Post` in `adv3d.wgsl`).
|
||
fn params(u0: [u32; 4], f0: [f32; 4]) -> Vec<u8> {
|
||
let mut v: Vec<u8> = u0.iter().flat_map(|x| x.to_le_bytes()).collect();
|
||
v.extend([0u8; 16]);
|
||
v.extend(f0.iter().flat_map(|x| x.to_le_bytes()));
|
||
v.extend([0u8; 16]);
|
||
v
|
||
}
|
||
|
||
fn dispatch(e: &mut Enc, kernel: &str, u0: [u32; 4], f0: [f32; 4], b: Binds, groups: (u32, u32)) {
|
||
let g = e.g;
|
||
let post = &pipes(g).post;
|
||
let Some((_, pipe)) = post.kernels.iter().find(|(k, _)| *k == kernel) else { return };
|
||
let ub = g.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: None, contents: ¶ms(u0, f0), usage: wgpu::BufferUsages::UNIFORM });
|
||
let mut entries = vec![
|
||
wgpu::BindGroupEntry { binding: 0, resource: ub.as_entire_binding() },
|
||
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(b.color.unwrap_or(&post.dummy_color)) },
|
||
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(b.z.unwrap_or(&post.dummy_z)) },
|
||
];
|
||
for (i, buf) in b.bufs.iter().enumerate() {
|
||
entries.push(wgpu::BindGroupEntry { binding: 3 + i as u32, resource: buf.unwrap_or(&post.dummy_bufs[i]).as_entire_binding() });
|
||
}
|
||
entries.push(wgpu::BindGroupEntry { binding: 10, resource: wgpu::BindingResource::TextureView(b.out.unwrap_or(&post.dummy_out)) });
|
||
let bg = g.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some(kernel), layout: &post.bgl, entries: &entries });
|
||
let enc = e.encoder();
|
||
let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some(kernel), timestamp_writes: None });
|
||
pass.set_pipeline(pipe);
|
||
pass.set_bind_group(0, &bg, &[]);
|
||
pass.dispatch_workgroups(groups.0.max(1), groups.1.max(1), 1);
|
||
}
|
||
|
||
fn pixel_groups(w: u32, h: u32) -> (u32, u32) {
|
||
(w.div_ceil(16), h.div_ceil(16))
|
||
}
|
||
|
||
fn storage_buf(g: &GpuContext, size: u64, label: &str) -> wgpu::Buffer {
|
||
g.device.create_buffer(&wgpu::BufferDescriptor {
|
||
label: Some(label),
|
||
size: size.max(16),
|
||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
|
||
mapped_at_creation: false,
|
||
})
|
||
}
|
||
|
||
/// Read a storage buffer back (submits and waits).
|
||
fn read_buffer(e: &mut Enc, buf: &wgpu::Buffer, size: u64) -> Option<Vec<u8>> {
|
||
if !e.g.can_wait() {
|
||
return None;
|
||
}
|
||
let rb = e.g.device.create_buffer(&wgpu::BufferDescriptor {
|
||
label: Some("adv3d readback"),
|
||
size,
|
||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||
mapped_at_creation: false,
|
||
});
|
||
e.encoder().copy_buffer_to_buffer(buf, 0, &rb, 0, size);
|
||
e.submit();
|
||
let (tx, rx) = std::sync::mpsc::channel();
|
||
rb.map_async(wgpu::MapMode::Read, .., move |r| {
|
||
let _ = tx.send(r);
|
||
});
|
||
e.g.device.poll(wgpu::PollType::wait_indefinitely()).ok()?;
|
||
rx.recv().ok()?.ok()?;
|
||
let out = rb.get_mapped_range(..).ok()?.to_vec();
|
||
rb.unmap();
|
||
Some(out)
|
||
}
|
||
|
||
/// A resolved run on the GPU: premultiplied linear (or working-space) colour and camera depth
|
||
/// (`BIG` where nothing was drawn), one entry per output pixel.
|
||
pub(crate) struct Resolved {
|
||
acc: wgpu::Buffer,
|
||
depth: wgpu::Buffer,
|
||
width: u32,
|
||
height: u32,
|
||
}
|
||
|
||
/// The rasteriser's "nothing drawn" depth on the GPU (∞ on the CPU).
|
||
const BIG: f32 = 3.0e38;
|
||
|
||
/// Rasterise every motion-blur sub-sample of a prepared run, resolve and average them, then
|
||
/// apply the depth of field (`render_prepared`'s steps). `None` = can't run here (sizes,
|
||
/// limits, or a depth of field without readback); `Some(None)` = nothing drawn.
|
||
pub(crate) fn resolve_run(e: &mut Enc, prep: &Prepared) -> Option<Option<Resolved>> {
|
||
let (w, h) = prep.out;
|
||
let g = e.g;
|
||
if !g.fits(w, h) || (prep.dof.is_some() && !g.can_wait()) {
|
||
return None;
|
||
}
|
||
let px = w as u64 * h as u64;
|
||
let max = g.device.limits().max_storage_buffer_binding_size;
|
||
if px * 16 > max {
|
||
return None;
|
||
}
|
||
let acc = storage_buf(g, px * 16, "adv3d acc");
|
||
let depth = storage_buf(g, px * 4, "adv3d depth");
|
||
let n = prep.samples.max(1);
|
||
let weight = if n <= 1 { 1.0 } else { 1.0 / n as f32 };
|
||
let mut first = true;
|
||
for i in 0..n {
|
||
let s = prep.scene(i);
|
||
if s.indices.is_empty() {
|
||
continue;
|
||
}
|
||
if s.width != w * s.ssaa.max(1) || s.height != h * s.ssaa.max(1) {
|
||
return None;
|
||
}
|
||
let (color, zout) = raster_into(e, &s)?;
|
||
let (cv, zv) = (color.create_view(&Default::default()), zout.create_view(&Default::default()));
|
||
let b = Binds { color: Some(&cv), z: Some(&zv), bufs: [Some(&acc), Some(&depth), None, None, None, None, None], out: None };
|
||
dispatch(e, "resolve", [w, h, s.ssaa.max(1), first as u32], [weight, 0.0, 0.0, 0.0], b, pixel_groups(w, h));
|
||
first = false;
|
||
// One sub-sample's targets alive at a time.
|
||
e.submit();
|
||
}
|
||
if first {
|
||
return Some(None);
|
||
}
|
||
if let Some(dof) = &prep.dof {
|
||
depth_of_field(e, &acc, &depth, (w, h), dof, prep.scale)?;
|
||
}
|
||
Some(Some(Resolved { acc, depth, width: w, height: h }))
|
||
}
|
||
|
||
/// `adv::depth_of_field` on the resolved buffers: radii, highlights, prefix sums, the iris
|
||
/// gather at the levels around each pixel's radius. The result replaces `acc`.
|
||
fn depth_of_field(e: &mut Enc, acc: &wgpu::Buffer, depth: &wgpu::Buffer, (w, h): (u32, u32), dof: &Dof, scale: f64) -> Option<()> {
|
||
let g = e.g;
|
||
let px = w as u64 * h as u64;
|
||
if (w as u64 + 1) * h as u64 * 16 > g.device.limits().max_storage_buffer_binding_size {
|
||
return None;
|
||
}
|
||
let radius = storage_buf(g, px * 4, "adv3d radius");
|
||
let mut init = Vec::with_capacity(16);
|
||
for v in [f32::INFINITY.to_bits(), 0, 0, 0] {
|
||
init.extend_from_slice(&v.to_le_bytes());
|
||
}
|
||
let range = g.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||
label: Some("adv3d radius range"),
|
||
contents: &init,
|
||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
||
});
|
||
let f0 = [dof.focus as f32, dof.aperture as f32, dof.blur_level as f32, scale as f32];
|
||
let b = Binds { bufs: [None, Some(depth), None, None, Some(&radius), Some(&range), None], ..Default::default() };
|
||
dispatch(e, "radius_of", [w, h, 0, 0], f0, b, pixel_groups(w, h));
|
||
let rb = read_buffer(e, &range, 16)?;
|
||
let lo = f32::from_bits(u32::from_le_bytes([rb[0], rb[1], rb[2], rb[3]]));
|
||
let hi = f32::from_bits(u32::from_le_bytes([rb[4], rb[5], rb[6], rb[7]]));
|
||
if hi < 0.5 {
|
||
return Some(());
|
||
}
|
||
let Some(levels) = bokeh::blur_levels(&[lo, hi]) else { return Some(()) };
|
||
// Level headers, then every level's weighted spans.
|
||
let mut heads: Vec<[f32; 4]> = vec![];
|
||
let mut spans: Vec<[f32; 4]> = vec![];
|
||
for &r in &levels {
|
||
match bokeh::kernel_spans(&dof.iris, r as f64) {
|
||
Some((kernels, norm)) => {
|
||
let first = levels.len() + spans.len();
|
||
for (wt, sp) in &kernels {
|
||
spans.extend(sp.iter().map(|&(dy, x0, x1)| [dy as f32, x0 as f32, x1 as f32, *wt]));
|
||
}
|
||
let count = levels.len() + spans.len() - first;
|
||
heads.push([first as f32, count as f32, 1.0 / norm, 0.0]);
|
||
}
|
||
None => heads.push([0.0, 0.0, 0.0, 1.0]),
|
||
}
|
||
}
|
||
let table: Vec<u8> = heads.iter().chain(&spans).flatten().flat_map(|x| x.to_le_bytes()).collect();
|
||
if table.len() as u64 > g.device.limits().max_storage_buffer_binding_size {
|
||
return None;
|
||
}
|
||
let table = e.bytes(table);
|
||
let src = storage_buf(g, px * 16, "adv3d dof source");
|
||
let prefix = storage_buf(g, (w as u64 + 1) * h as u64 * 16, "adv3d prefix");
|
||
let hl = dof.highlight;
|
||
let b = Binds { bufs: [Some(acc), None, Some(&src), None, Some(&radius), None, None], ..Default::default() };
|
||
dispatch(e, "boost", [w, h, 0, 0], [hl.gain as f32, hl.threshold as f32, hl.saturation as f32, 0.0], b, pixel_groups(w, h));
|
||
let b = Binds { bufs: [None, None, Some(&src), None, None, None, Some(&prefix)], ..Default::default() };
|
||
dispatch(e, "prefix_rows", [w, h, 0, 0], [0.0; 4], b, (h.div_ceil(64), 1));
|
||
let b = Binds { bufs: [Some(acc), None, Some(&src), Some(&table), Some(&radius), None, Some(&prefix)], ..Default::default() };
|
||
dispatch(e, "gather", [w, h, levels.len() as u32, 0], [lo, hi, 0.0, 0.0], b, pixel_groups(w, h));
|
||
e.submit();
|
||
Some(())
|
||
}
|
||
|
||
/// The resolved run as an image in the canvas's encoding, composited over `canvas` when given.
|
||
pub(crate) fn finish(e: &mut Enc, r: &Resolved, encode: bool, canvas: Option<&GpuImage>) -> GpuImage {
|
||
let out = e.image(r.width, r.height);
|
||
let ov = out.texture.create_view(&Default::default());
|
||
let cv = canvas.map(|c| c.texture.create_view(&Default::default()));
|
||
let b = Binds { color: cv.as_ref(), bufs: [Some(&r.acc), None, None, None, None, None, None], out: Some(&ov), ..Default::default() };
|
||
dispatch(e, "finish", [r.width, r.height, encode as u32, canvas.is_some() as u32], [0.0; 4], b, pixel_groups(r.width, r.height));
|
||
out
|
||
}
|
||
|
||
/// [`effectcraft_render::Accelerator::render_3d`]: the whole prepared run on the GPU, read
|
||
/// back.
|
||
pub(crate) fn render_prepared(g: &GpuContext, prep: &Prepared) -> Option<Rendered> {
|
||
if !g.can_readback() {
|
||
return None;
|
||
}
|
||
if let Some(d) = g.deferred.clone() {
|
||
return render_prepared_deferred(g, &d, prep);
|
||
}
|
||
let mut e = Enc::new(g);
|
||
let Some(r) = resolve_run(&mut e, prep)? else { return Some(None) };
|
||
let img = finish(&mut e, &r, !prep.linear, None);
|
||
let img = e.download(&img)?;
|
||
let px = r.width as u64 * r.height as u64;
|
||
let db = read_buffer(&mut e, &r.depth, px * 4)?;
|
||
let depth = db
|
||
.as_chunks::<4>()
|
||
.0
|
||
.iter()
|
||
.map(|c| {
|
||
let z = f32::from_le_bytes([c[0], c[1], c[2], c[3]]);
|
||
if z >= BIG * 0.5 { f32::INFINITY } else { z }
|
||
})
|
||
.collect();
|
||
Some(Some((img, depth)))
|
||
}
|
||
|
||
/// [`render_prepared`] with deferred readbacks: the image and the depth are read back under the
|
||
/// run's key (every sub-sample's scene, the output size and encoding). A depth of field needs a
|
||
/// readback in the middle of the run: such runs go to the CPU path (whose scenes still
|
||
/// rasterise here, [`render`]).
|
||
fn render_prepared_deferred(g: &GpuContext, d: &std::sync::Arc<crate::deferred::Deferred>, prep: &Prepared) -> Option<Rendered> {
|
||
use crate::deferred::Lookup::*;
|
||
if prep.dof.is_some() {
|
||
return None;
|
||
}
|
||
let (w, h) = prep.out;
|
||
let mut k = crate::deferred::Key::new(5);
|
||
k.u64(w as u64);
|
||
k.u64(h as u64);
|
||
k.u64(prep.samples as u64);
|
||
k.f64(prep.scale);
|
||
k.u64(prep.linear as u64);
|
||
let mut any = false;
|
||
for i in 0..prep.samples.max(1) {
|
||
let s = prep.scene(i);
|
||
any |= !s.indices.is_empty();
|
||
key_scene(&mut k, &s);
|
||
}
|
||
if !any {
|
||
return Some(None);
|
||
}
|
||
let ki = k.finish();
|
||
let kz = ki ^ 1;
|
||
match (d.lookup(ki), d.lookup(kz)) {
|
||
(Ready(a), Ready(b)) => {
|
||
let (a, b) = (a?, b?);
|
||
let mut img = effectcraft_render::Image::new(a.width, a.height);
|
||
let dst: &mut [u8] = bytemuck::cast_slice_mut(&mut img.data);
|
||
if dst.len() != a.bytes.len() || b.bytes.len() != dst.len() / 4 {
|
||
return None;
|
||
}
|
||
dst.copy_from_slice(&a.bytes);
|
||
let depth = b
|
||
.bytes
|
||
.as_chunks::<4>()
|
||
.0
|
||
.iter()
|
||
.map(|c| {
|
||
let z = f32::from_le_bytes([c[0], c[1], c[2], c[3]]);
|
||
if z >= BIG * 0.5 { f32::INFINITY } else { z }
|
||
})
|
||
.collect();
|
||
return Some(Some((img, depth)));
|
||
}
|
||
(Absent, Absent) => {}
|
||
_ => {
|
||
d.miss();
|
||
return Some(None);
|
||
}
|
||
}
|
||
let mut e = Enc::new(g);
|
||
let Some(r) = resolve_run(&mut e, prep)? else { return Some(None) };
|
||
let img = finish(&mut e, &r, !prep.linear, None);
|
||
let _ = e.read_keyed(&img, ki, [0.0; 2], 1.0);
|
||
let size = w as u64 * h as u64 * 4;
|
||
let rb = g.device.create_buffer(&wgpu::BufferDescriptor {
|
||
label: Some("adv3d depth readback"),
|
||
size,
|
||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||
mapped_at_creation: false,
|
||
});
|
||
e.encoder().copy_buffer_to_buffer(&r.depth, 0, &rb, 0, size);
|
||
e.submit();
|
||
let done = d.start(kz, w, h, [0.0; 2], 1.0);
|
||
let b = rb.clone();
|
||
rb.map_async(wgpu::MapMode::Read, .., move |res| {
|
||
let out = res.ok().and_then(|_| b.get_mapped_range(..).ok().map(|v| v.to_vec()));
|
||
b.unmap();
|
||
done(out);
|
||
});
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
let _ = g.device.poll(wgpu::PollType::Poll);
|
||
d.miss();
|
||
Some(None)
|
||
}
|
||
|
||
/// Draw a prepared run over the GPU canvas (the compositor's walk; no readback unless the
|
||
/// depth of field needs its radius range). `None` = render it on the CPU.
|
||
pub(crate) fn draw_run(e: &mut Enc, prep: &Prepared, canvas: &GpuImage) -> Option<GpuImage> {
|
||
if (canvas.width, canvas.height) != prep.out {
|
||
return None;
|
||
}
|
||
match resolve_run(e, prep)? {
|
||
None => Some(canvas.clone()),
|
||
Some(r) => Some(finish(e, &r, !prep.linear, Some(canvas))),
|
||
}
|
||
}
|
||
|
||
/// A layer rendered on its own (`iso`), finished, and hidden where the run's main scene is
|
||
/// nearer (`adv::draw_run`'s 2D compositing path).
|
||
pub(crate) fn occluded(e: &mut Enc, iso: &Resolved, main: &Resolved, encode: bool) -> GpuImage {
|
||
let img = finish(e, iso, encode, None);
|
||
let (w, h) = (iso.width, iso.height);
|
||
let out = e.image(w, h);
|
||
let (iv, ov) = (img.texture.create_view(&Default::default()), out.texture.create_view(&Default::default()));
|
||
let b =
|
||
Binds { color: Some(&iv), bufs: [None, Some(&iso.depth), Some(&main.acc), None, Some(&main.depth), None, None], out: Some(&ov), ..Default::default() };
|
||
dispatch(e, "occlude", [w, h, 0, 0], [0.0; 4], b, pixel_groups(w, h));
|
||
out
|
||
}
|
||
|
||
/// Kernels of `sky.wgsl` (the shared compute module).
|
||
pub(crate) const SKY_KERNELS: &[&str] = &["adv_sky"];
|
||
|
||
/// An Environment Light Background layer drawn over `canvas` (`three_d::compose::draw_sky`).
|
||
pub(crate) fn sky(e: &mut Enc, sky: &effectcraft_render::three_d::SkyDraw, canvas: &GpuImage) -> Option<GpuImage> {
|
||
let img = e.g.upload_buf(&sky.buf)?;
|
||
let f3 = |v: [f64; 3], w: f64| [v[0] as f32, v[1] as f32, v[2] as f32, w as f32];
|
||
let mut p = crate::context::Params::default();
|
||
p.f[0] = f3(sky.fwd, sky.zoom);
|
||
p.f[1] = f3(sky.right, sky.ortho as u32 as f64);
|
||
p.f[2] = [sky.down[0] as f32, sky.down[1] as f32, sky.down[2] as f32, sky.rotation];
|
||
let s = sky.scale;
|
||
p.f[3] = [(sky.roi.0 / s - sky.view.0 / 2.0) as f32, (sky.roi.1 / s - sky.view.1 / 2.0) as f32, (1.0 / s) as f32, sky.opacity];
|
||
p.f[4] = [img.width as f32, img.height as f32, 0.0, 0.0];
|
||
let out = e.scratch(canvas.width, canvas.height);
|
||
e.pixels("adv_sky", &p, canvas, Some(&img), &out, None);
|
||
Some(out)
|
||
}
|
||
|
||
/// Wireframe-quality layer outlines: `canvas` with `pixels` set to opaque white
|
||
/// ([`effectcraft_render::Renderer::wireframe_pixels`]).
|
||
pub(crate) fn wireframe(e: &mut Enc, canvas: &GpuImage, pixels: &[(u32, u32)]) -> GpuImage {
|
||
let out = e.image(canvas.width, canvas.height);
|
||
e.copy_into(canvas, &out, 0, 0);
|
||
if pixels.is_empty() {
|
||
return out;
|
||
}
|
||
let table: Vec<u8> = pixels.iter().flat_map(|&(x, y)| [x as f32, y as f32, 0.0, 0.0]).flat_map(f32::to_le_bytes).collect();
|
||
let table = e.bytes(table);
|
||
let ov = out.texture.create_view(&Default::default());
|
||
let b = Binds { bufs: [None, None, None, Some(&table), None, None, None], out: Some(&ov), ..Default::default() };
|
||
let n = pixels.len() as u32;
|
||
dispatch(e, "wire", [n, 0, 0, 0], [0.0; 4], b, (n.div_ceil(64), 1));
|
||
out
|
||
}
|