241 lines
9.1 KiB
Rust
241 lines
9.1 KiB
Rust
//! Classic 3D runs on the GPU (`classic3d.wgsl`): the CPU prepares the planes
|
|
//! ([`Renderer::prepare_3d_run`](effectcraft_render::Renderer::prepare_3d_run): layer buffers,
|
|
//! homographies, materials, lights, track mattes, bokeh depth of field), the GPU packs the
|
|
//! buffers into one atlas texture and runs the per-pixel depth sort, lighting, ray-cast shadows
|
|
//! and blending of `render::three_d::compose` in one kernel.
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use effectcraft_geom::Mat4;
|
|
use effectcraft_project::LightKind;
|
|
use effectcraft_render::three_d::{LightState, PlaneGeo, Run3d};
|
|
|
|
use crate::context::{Enc, GpuImage, Params};
|
|
use crate::ops::mode_id;
|
|
|
|
/// Fragments kept per pixel (`MAX_FRAGS` in the shader). Runs that could stack more planes on
|
|
/// one pixel render on the CPU.
|
|
const MAX_FRAGS: usize = 32;
|
|
|
|
/// Little-endian words for the shader's storage structs.
|
|
#[derive(Default)]
|
|
struct Words(Vec<u8>);
|
|
|
|
impl Words {
|
|
fn f(&mut self, v: [f32; 4]) {
|
|
for x in v {
|
|
self.0.extend_from_slice(&x.to_le_bytes());
|
|
}
|
|
}
|
|
fn u(&mut self, v: [u32; 4]) {
|
|
for x in v {
|
|
self.0.extend_from_slice(&x.to_le_bytes());
|
|
}
|
|
}
|
|
fn d(&mut self, v: [f64; 4]) {
|
|
self.f(v.map(|x| x as f32));
|
|
}
|
|
/// Pad an empty array to one element (`size` bytes) so its binding is valid.
|
|
fn at_least(mut self, size: usize) -> Vec<u8> {
|
|
if self.0.len() < size {
|
|
self.0.resize(size, 0);
|
|
}
|
|
self.0
|
|
}
|
|
}
|
|
|
|
fn affine_rows(w: &mut Words, m: &Mat4) {
|
|
for r in 0..3 {
|
|
w.d(m.0[r]);
|
|
}
|
|
}
|
|
|
|
/// Shelf-pack rectangles into an atlas no larger than `max` on a side: (width, height,
|
|
/// positions).
|
|
fn pack(sizes: &[(u32, u32)], max: u32) -> Option<(u32, u32, Vec<(u32, u32)>)> {
|
|
let maxw = sizes.iter().map(|s| s.0).max()?;
|
|
if maxw > max {
|
|
return None;
|
|
}
|
|
let area: f64 = sizes.iter().map(|s| s.0 as f64 * s.1 as f64).sum();
|
|
let limit = maxw.max((area.sqrt() * 1.25).ceil() as u32).min(max);
|
|
let mut order: Vec<usize> = (0..sizes.len()).collect();
|
|
order.sort_by_key(|&i| std::cmp::Reverse(sizes[i].1));
|
|
let mut pos = vec![(0, 0); sizes.len()];
|
|
let (mut x, mut y, mut shelf, mut width) = (0u32, 0u32, 0u32, 0u32);
|
|
for i in order {
|
|
let (w, h) = sizes[i];
|
|
if x + w > limit {
|
|
y += shelf;
|
|
x = 0;
|
|
shelf = 0;
|
|
}
|
|
pos[i] = (x, y);
|
|
x += w;
|
|
shelf = shelf.max(h);
|
|
width = width.max(x);
|
|
}
|
|
let height = y + shelf;
|
|
(height <= max).then_some((width.max(1), height.max(1), pos))
|
|
}
|
|
|
|
/// Upper bound of the planes drawn on any one pixel: a pixel covered by a set of planes lies in
|
|
/// each one's box, so every plane of the set overlaps the box of any member.
|
|
fn max_overlap(boxes: &[[i64; 4]]) -> usize {
|
|
boxes.iter().map(|a| boxes.iter().filter(|b| a[0] < b[2] && b[0] < a[2] && a[1] < b[3] && b[1] < a[3]).count()).max().unwrap_or(0)
|
|
}
|
|
|
|
fn light_kind(k: LightKind) -> u32 {
|
|
match k {
|
|
LightKind::Ambient | LightKind::Environment => 0,
|
|
LightKind::Parallel => 1,
|
|
LightKind::Point => 2,
|
|
LightKind::Spot => 3,
|
|
}
|
|
}
|
|
|
|
fn write_geo(w: &mut Words, g: &PlaneGeo) {
|
|
// The homography is defined up to scale: normalise it (by a positive factor, keeping the
|
|
// sign tests) into f32's comfortable range.
|
|
let m = g.hinv.0.iter().flatten().fold(0.0f64, |a, v| a.max(v.abs()));
|
|
let k = if m > 0.0 { 1.0 / m } else { 1.0 };
|
|
for r in 0..3 {
|
|
w.d([g.hinv.0[r][0] * k, g.hinv.0[r][1] * k, g.hinv.0[r][2] * k, 0.0]);
|
|
}
|
|
w.d([g.depth[0], g.depth[1], g.depth[2], 0.0]);
|
|
affine_rows(w, &g.world);
|
|
w.d([g.normal.x, g.normal.y, g.normal.z, 0.0]);
|
|
w.d([g.cam.eye.x, g.cam.eye.y, g.cam.eye.z, if g.cam.ortho { 1.0 } else { 0.0 }]);
|
|
let f = g.cam.forward();
|
|
w.d([f.x, f.y, f.z, 0.0]);
|
|
}
|
|
|
|
fn write_light(w: &mut Words, l: &LightState) {
|
|
w.u([light_kind(l.kind), l.falloff, l.casts_shadows as u32, 0]);
|
|
w.d([l.pos.x, l.pos.y, l.pos.z, 0.0]);
|
|
w.d([l.dir.x, l.dir.y, l.dir.z, 0.0]);
|
|
w.f([l.color[0], l.color[1], l.color[2], 0.0]);
|
|
w.d([l.cone_half, l.feather, l.radius, l.falloff_distance]);
|
|
w.d([l.shadow_darkness, l.shadow_diffusion, 0.0, 0.0]);
|
|
}
|
|
|
|
const PLANE_BYTES: usize = 12 * 16;
|
|
const GEO_BYTES: usize = 10 * 16;
|
|
const LIGHT_BYTES: usize = 6 * 16;
|
|
|
|
/// Composite a prepared Classic 3D run over `canvas`. `None` when it cannot run here (the
|
|
/// caller draws it on the CPU).
|
|
pub(crate) fn draw_run(e: &mut Enc, run: &Run3d, canvas: &GpuImage) -> Option<GpuImage> {
|
|
let (w, h) = (canvas.width, canvas.height);
|
|
let boxes: Vec<[i64; 4]> = run.planes.iter().filter(|p| p.draw).map(|p| p.bbox).collect();
|
|
if boxes.is_empty() {
|
|
return Some(canvas.clone());
|
|
}
|
|
if max_overlap(&boxes) > MAX_FRAGS
|
|
|| run.planes.iter().any(|p| p.winv.0[3] != [0.0, 0.0, 0.0, 1.0] || p.geos.iter().any(|g| g.world.0[3] != [0.0, 0.0, 0.0, 1.0]))
|
|
{
|
|
return None;
|
|
}
|
|
// Plane textures: the layer uploads (cached per buffer), with bokeh depth of field blurred
|
|
// here when the plane has it.
|
|
let mut texs: Vec<(GpuImage, [f64; 2])> = Vec::with_capacity(run.planes.len());
|
|
for p in &run.planes {
|
|
let up = e.g.upload_buf(&p.buf)?;
|
|
match &p.dof {
|
|
Some(d) => {
|
|
let pad = d.pad as f64;
|
|
texs.push((crate::bokeh::apply(e, &up, d)?, [p.buf.offset[0] + pad, p.buf.offset[1] + pad]));
|
|
}
|
|
None => texs.push((up, p.buf.offset)),
|
|
}
|
|
}
|
|
let sizes: Vec<(u32, u32)> = texs.iter().map(|(t, _)| (t.width, t.height)).collect();
|
|
let (aw, ah, pos) = pack(&sizes, e.g.max_dim)?;
|
|
if (aw as u64) * (ah as u64) * 16 > 2 << 30 {
|
|
return None;
|
|
}
|
|
let atlas = e.image(aw, ah);
|
|
for ((t, _), &(x, y)) in texs.iter().zip(&pos) {
|
|
e.copy_into(t, &atlas, x, y);
|
|
}
|
|
// Track mattes (shared by the planes of one layer).
|
|
let mut mattes: Vec<f32> = vec![];
|
|
let mut matte_ix: HashMap<usize, u32> = HashMap::new();
|
|
let n_px = w as usize * h as usize;
|
|
let mut planes = Words::default();
|
|
let mut geos = Words::default();
|
|
let mut n_geos = 0u32;
|
|
for ((p, (t, off)), &(ax, ay)) in run.planes.iter().zip(&texs).zip(&pos) {
|
|
let matte = match &p.matte {
|
|
Some(m) if m.len() == n_px => {
|
|
let key = std::sync::Arc::as_ptr(m) as usize;
|
|
let next = matte_ix.len() as u32 + 1;
|
|
let ix = *matte_ix.entry(key).or_insert(next);
|
|
if ix == next {
|
|
mattes.extend_from_slice(m);
|
|
}
|
|
ix
|
|
}
|
|
Some(_) => return None,
|
|
None => 0,
|
|
};
|
|
planes.f([ax as f32, ay as f32, t.width as f32, t.height as f32]);
|
|
planes.d([off[0], off[1], p.buf.scale, if p.bicubic { 1.0 } else { 0.0 }]);
|
|
planes.d(p.bbox.map(|v| v as f64));
|
|
planes.d(p.bounds);
|
|
affine_rows(&mut planes, &p.winv);
|
|
planes.f([p.mat.ambient, p.mat.diffuse, p.mat.specular, p.mat.exponent() as f32]);
|
|
planes.f([p.mat.metal, p.mat.light_transmission, p.opacity, 0.0]);
|
|
let flags = p.draw as u32 | (p.preserve as u32) << 1 | (p.mat.accepts_lights as u32) << 2;
|
|
planes.u([n_geos, p.geos.len() as u32, flags, mode_id(p.mode)]);
|
|
planes.u([p.mat.casts_shadows, p.mat.accepts_shadows, p.layer_id, p.order as u32]);
|
|
planes.u([matte, run.seed ^ p.layer_id, 0, 0]);
|
|
for g in &p.geos {
|
|
write_geo(&mut geos, g);
|
|
}
|
|
n_geos += p.geos.len() as u32;
|
|
}
|
|
if (mattes.len() * 4) as u64 > e.g.device.limits().max_storage_buffer_binding_size {
|
|
return None;
|
|
}
|
|
let mut lights = Words::default();
|
|
for l in &run.lights {
|
|
write_light(&mut lights, l);
|
|
}
|
|
let mut casters = Words::default();
|
|
for c in &run.casters {
|
|
casters.0.extend_from_slice(&(*c as u32).to_le_bytes());
|
|
}
|
|
let pb = e.bytes(planes.at_least(PLANE_BYTES));
|
|
let gb = e.bytes(geos.at_least(GEO_BYTES));
|
|
let lb = e.bytes(lights.at_least(LIGHT_BYTES));
|
|
let cb = e.bytes(casters.at_least(16));
|
|
let mb = e.data(&mattes);
|
|
let mut p = Params::default();
|
|
p.u[0] = [run.planes.len() as u32, run.lights.len() as u32, run.casters.len() as u32, 0];
|
|
let out = e.scratch(w, h);
|
|
e.dispatch_ext("classic3d", &p, &atlas, Some(canvas), &out, Some(&mb), (w.div_ceil(16), h.div_ceil(16)), Some([&pb, &gb, &lb, &cb]));
|
|
Some(out)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn shelf_packing_fits_and_does_not_overlap() {
|
|
let sizes = [(100, 40), (30, 90), (60, 60), (100, 10), (5, 5)];
|
|
let (w, h, pos) = pack(&sizes, 256).unwrap();
|
|
for (i, (&(x, y), &(sw, sh))) in pos.iter().zip(&sizes).enumerate() {
|
|
assert!(x + sw <= w && y + sh <= h);
|
|
for (j, (&(x2, y2), &(sw2, sh2))) in pos.iter().zip(&sizes).enumerate() {
|
|
if i != j {
|
|
assert!(x + sw <= x2 || x2 + sw2 <= x || y + sh <= y2 || y2 + sh2 <= y, "{i} and {j} overlap");
|
|
}
|
|
}
|
|
}
|
|
assert!(pack(&[(300, 10)], 256).is_none());
|
|
assert_eq!(max_overlap(&[[0, 0, 10, 10], [5, 5, 15, 15], [20, 20, 30, 30]]), 2);
|
|
}
|
|
}
|