Files
effectcraft/crates/gpu/src/fx_pixel2.rs
T
Minher0 5b2f305f72 instance3d: a dedicated 3D Layer Instance effect, and the copy is cached
CC Composite stays After Effects-pure: its Layer parameter is gone, so
parameter indices match AE again (scripts and expressions that address
parameters by index). The model reference lives in its own effect,
ec.3d.instance3d (3D Channel, CPU, one layer parameter), which replaces
the layer's pixels with the referenced layer's render — alpha included.

A referenced model's solo render is now hashed into the owner's cache key
instead of declining the entry (Bflat, review of #2410): the layer, its
animated values and transform, the mesh item, the comp camera and lights,
draft 3D, the renderer setting and the in/out gate. Covered by a render
test: the copy is cached, served on the second render, and re-rendered
when the camera pans or the model leaves its in/out range.
2026-10-29 10:18:17 +00:00

529 lines
21 KiB
Rust

//! GPU effects, pixel ports of part C (kernels in `shaders/fx_pixel2.wgsl`, every entry point
//! prefixed `fp2_`): CC Cross Blur, CC Radial Blur, CC Vector Blur, Reduce Interlace Flicker,
//! CC Composite, Color Link, Cineon Converter, HDR Compander, HDR Highlight Compression, Grow
//! Bounds, CC Overbrights, CC Block Load, CC Burn Film, CC Glass, CC HexTile, CC Mr. Smoothie,
//! CC Plastic, Inner/Outer Key, CC Simple Wire Removal and Basic 3D, with the CPU effects'
//! exact steps (padding, box-blur radii, parameter conversions in f64 on the CPU).
//!
//! What only depends on parameters and geometry is prepared on the CPU and shared with the CPU
//! effect: Inner/Outer Key's trimap and Cleanup strokes (`effects::inner_outer_plan`), CC Glass /
//! Plastic's light (`effects::BumpLight`). Color Link's sample statistics (sorting, trimmed
//! means) run on the CPU over the read-back layer (`effects::color_link_sample`) unless a Source
//! Layer is chosen; the colouring runs here.
use effectcraft_color::BlendMode;
use effectcraft_effects::EffectCtx;
use effectcraft_raster::Image;
use crate::context::{Enc, GpuImage, Params};
use crate::effects::{GBuf, box_passes, gaussian_blur};
use crate::ops::mode_id;
/// Compute entry points in `fx_pixel2.wgsl`.
pub(crate) const KERNELS: &[&str] = &[
"fp2_cross",
"fp2_box",
"fp2_radial",
"fp2_plane",
"fp2_vector",
"fp2_point",
"fp2_block_means",
"fp2_block_load",
"fp2_burn",
"fp2_bump",
"fp2_hextile",
"fp2_smoothie",
"fp2_io_prep",
"fp2_io_known",
"fp2_io_est",
"fp2_io_strokes",
"fp2_io_final",
"fp2_wire",
"fp2_basic3d",
];
/// Effect ids implemented here.
pub(crate) const IDS: &[&str] = &[
"ec.blur.cccross",
"ec.blur.ccradial",
"ec.blur.ccvector",
"ec.blur.reduceflicker",
"ec.channel.cccomposite",
"ec.color.colorlink",
"ec.utility.cineon",
"ec.utility.hdrcompander",
"ec.utility.hdrcompression",
"ec.utility.growbounds",
"ec.utility.ccoverbrights",
"ec.stylize.ccblockload",
"ec.stylize.ccburnfilm",
"ec.stylize.ccglass",
"ec.stylize.cchextile",
"ec.stylize.ccmrsmoothie",
"ec.stylize.ccplastic",
"ec.key.innerouter",
"ec.key.ccsimplewireremoval",
"ec.obsolete.basic3d",
];
/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU.
pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
match id {
"ec.blur.cccross" => cross_blur(e, ctx, b),
"ec.blur.ccradial" => radial_blur(e, ctx, b),
"ec.blur.ccvector" => vector_blur(e, ctx, b),
"ec.blur.reduceflicker" => reduce_flicker(e, ctx, b),
"ec.utility.growbounds" => grow_bounds(e, ctx, b),
"ec.stylize.ccblockload" => block_load(e, ctx, b),
"ec.stylize.ccburnfilm" => burn_film(e, ctx, b),
"ec.stylize.ccglass" | "ec.stylize.ccplastic" => bump(e, id, ctx, b),
"ec.stylize.cchextile" => hextile(e, ctx, b),
"ec.stylize.ccmrsmoothie" => smoothie(e, ctx, b),
"ec.key.innerouter" => inner_outer(e, ctx, b),
"ec.key.ccsimplewireremoval" => wire_removal(e, ctx, b),
"ec.obsolete.basic3d" => basic_3d(e, ctx, b),
_ => point(e, id, ctx, b),
}
}
/// A same-size per-pixel kernel over the buffer.
fn run(e: &mut Enc, entry: &str, p: &Params, mut b: GBuf, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> Option<GBuf> {
let out = e.scratch(b.img.width, b.img.height);
e.pixels(entry, p, &b.img, aux, &out, data);
b.img = out;
Some(b)
}
/// A one-value plane (x) of `img` (`fp2_plane` kind / property), softened by util::gauss_plane.
fn plane(e: &mut Enc, img: &GpuImage, kind: u32, prop: u32, soft: f64) -> GpuImage {
let mut p = Params::default();
p.u[0] = [kind, prop, 0, 0];
let pl = e.scratch(img.width, img.height);
e.pixels("fp2_plane", &p, img, None, &pl, None);
gauss_plane(e, &pl, soft, soft)
}
/// util::gauss_plane on every channel: 3 box passes per axis (edges repeated), each window
/// summed directly (the CPU sums in f64; see `fp2_box`).
pub(crate) fn gauss_plane(e: &mut Enc, img: &GpuImage, sx: f64, sy: f64) -> GpuImage {
let mut cur = img.clone();
for (vertical, sigma) in [(0, sx), (1, sy)] {
if sigma <= 0.05 {
continue;
}
for r in crate::effects::box_radii(sigma, 3) {
if r == 0 {
continue;
}
let mut p = Params::default();
p.u[0] = [r as u32, vertical, 0, 0];
let out = e.scratch(cur.width, cur.height);
e.pixels("fp2_box", &p, &cur, None, &out, None);
cur = out;
}
}
cur
}
// ---------------------------------------------------------------- blurs (blur2, blur3)
fn cross_blur(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let rx = (ctx.params.f("radiusX").max(0.0) * b.scale).round() as usize;
let ry = (ctx.params.f("radiusY").max(0.0) * b.scale).round() as usize;
if rx == 0 && ry == 0 {
return Some(b);
}
if !ctx.adjustment {
b.pad(e, (rx.max(ry) * 3) as u32 + 1)?;
}
let repeat = ctx.adjustment;
let hz = box_passes(e, &b.img, &[rx; 3], &[], repeat);
let vt = box_passes(e, &b.img, &[], &[ry; 3], repeat);
let mut p = Params::default();
p.u[0][0] = ctx.params.e("transferMode");
let out = e.scratch(b.img.width, b.img.height);
e.pixels("fp2_cross", &p, &hz, Some(&vt), &out, None);
b.img = out;
Some(b)
}
fn radial_blur(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let amount = ctx.params.f("amount");
if amount == 0.0 {
return Some(b);
}
let c = b.to_px(ctx.params.v2("center"));
let mut p = Params::default();
p.u[0][0] = ctx.params.e("type");
p.f[0] = [c.0 as f32, c.1 as f32, ((amount / 100.0).clamp(-1.0, 1.0) * 0.5) as f32, (amount.to_radians() * 0.25) as f32];
p.f[1][0] = ctx.params.f("quality").clamp(1.0, 100.0) as f32;
run(e, "fp2_radial", &p, b, None, None)
}
fn vector_blur(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let amount = ctx.params.f("amount").max(0.0) * b.scale;
if amount < 0.5 {
return Some(b);
}
let soft = ctx.params.f("mapSoftness").max(0.0) * 0.5 * b.scale;
let map = plane(e, &b.img, 0, 0, soft);
let mut p = Params::default();
p.u[0][0] = ctx.params.e("type");
p.f[0] = [amount as f32, ctx.params.f("angleOffset").to_radians() as f32, 0.0, 0.0];
run(e, "fp2_vector", &p, b, Some(&map), None)
}
fn reduce_flicker(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let s = ctx.params.f("softness").max(0.0) * b.scale;
if s < 0.05 {
return Some(b);
}
b.img = gaussian_blur(e, &b.img, 0.0, s, true);
Some(b)
}
fn grow_bounds(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let px = (ctx.params.f("pixels") * b.scale).round().max(0.0) as u32;
if !ctx.adjustment {
b.pad(e, px)?;
}
Some(b)
}
// ---------------------------------------------------------------- per-pixel colour effects
/// channel2::cc_composite's Composite Original modes.
fn composite_mode(m: u32) -> BlendMode {
match m {
2 => BlendMode::Add,
3 => BlendMode::Multiply,
4 => BlendMode::Screen,
5 => BlendMode::Overlay,
6 => BlendMode::SoftLight,
7 => BlendMode::HardLight,
8 => BlendMode::Darken,
9 => BlendMode::Lighten,
10 => BlendMode::Difference,
11 => BlendMode::Hue,
12 => BlendMode::Saturation,
13 => BlendMode::Color,
14 => BlendMode::Luminosity,
15 => BlendMode::StencilAlpha,
_ => BlendMode::Normal,
}
}
fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let mut p = Params::default();
let mut original = None;
match id {
"ec.utility.cineon" => {
// utility::Cineon's offset and knee, in the CPU's f32.
let black = pr.f("tenBitBlackPoint") as f32;
let white = pr.f("tenBitWhitePoint") as f32;
let rolloff = pr.f("highlightRolloff") as f32;
let offset = |black: f32, white: f32| 10f32.powf((black - white) * 0.002 / 0.6);
let knee = 1.0 - (rolloff / 100.0).clamp(0.0, 0.99);
p.u[0] = [0, pr.e("conversionType"), 0, 0];
p.f[0] = [offset(black, white), knee, white, pr.f("gamma") as f32];
p.f[1] = [pr.f("internalBlackPoint") as f32, pr.f("internalWhitePoint") as f32, 0.0, 0.0];
p.f[2] = [offset(95.0, 685.0), knee, 685.0, 1.7];
}
"ec.utility.hdrcompander" => {
p.u[0] = [1, (pr.e("mode") == 1) as u32, 0, 0];
p.f[0] = [pr.f("gain").max(1e-6) as f32, pr.f("gamma").max(1e-3) as f32, 0.0, 0.0];
}
"ec.utility.hdrcompression" => {
let t = (pr.f("amount") / 100.0).clamp(0.0, 1.0) as f32;
if t <= 0.0 {
return Some(b);
}
p.u[0][0] = 2;
p.f[0][0] = 1.0 - 0.5 * t;
}
"ec.utility.ccoverbrights" => {
p.u[0] = [3, pr.e("channel"), 0, 0];
p.f[0] = pr.color("highlightColor");
}
"ec.channel.cccomposite" => {
if let Some(source) = ctx.env.host.and_then(|h| h.original()) {
let fitted = effectcraft_effects::util::fit_buffer(source, b.img.width, b.img.height, b.offset, b.scale);
original = Some(e.g.upload_image(&fitted)?);
}
p.u[1][0] = original.is_some() as u32;
let m = pr.e("compositeOriginal");
let mode = if m == 1 { BlendMode::Normal } else { composite_mode(m) };
p.u[0] = [6, (m == 1) as u32, mode_id(mode), pr.b("rgbOnly") as u32];
p.f[0][0] = (pr.f("opacity") as f32 / 100.0).clamp(0.0, 1.0);
}
"ec.color.colorlink" => {
// The sample statistics run on the CPU (the layer's own pixels are read back).
let own = if ctx.layer_param("sourceLayer", false).is_none() { Some(e.download(&b.img)?) } else { None };
let (c, sa) = effectcraft_effects::color_link_sample(ctx, own.as_ref())?;
let op = (pr.f("opacity") / 100.0).clamp(0.0, 1.0) as f32;
let stencil = pr.b("stencilOriginalAlpha") as u32;
if pr.e("sampleSource") >= 6 {
p.u[0] = [4, stencil, 0, 0];
p.f[0] = [sa, op, 0.0, 0.0];
} else {
p.u[0] = [5, stencil, mode_id(effectcraft_effects::color_link_mode(ctx)), 0];
p.f[0] = [c[0] * op, c[1] * op, c[2] * op, op];
}
}
_ => return None,
}
run(e, "fp2_point", &p, b, original.as_ref(), None)
}
// ---------------------------------------------------------------- CC Block Load (stylize3)
fn block_load(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let c = (ctx.params.f("completion") / 100.0).clamp(0.0, 1.0);
if c >= 1.0 {
return Some(b);
}
let (w, h) = (b.img.width as usize, b.img.height as usize);
let nlev = (w.max(h).max(2) as f64).log2().ceil() as u32;
let lp = c * (nlev + 1) as f64;
let done = lp.floor() as u32;
let frac = (lp - done as f64) as f32;
let size = |k: u32| 1usize << (nlev - k.min(nlev));
let means = |e: &mut Enc, s: usize| {
let mut p = Params::default();
p.u[0][0] = s as u32;
let g = e.scratch(w.div_ceil(s).max(1) as u32, h.div_ceil(s).max(1) as u32);
e.pixels("fp2_block_means", &p, &b.img, None, &g, None);
g
};
let s_new = size(done);
let new = means(e, s_new);
let old = (done > 0).then(|| (size(done - 1), means(e, size(done - 1))));
let flags = ctx.params.b("scanlines") as u32
| (ctx.params.b("smoothing") as u32) << 1
| (ctx.params.b("bilinear") as u32) << 2
| (old.is_some() as u32) << 3
| (ctx.params.b("startCleared") as u32) << 4;
let mut p = Params::default();
p.u[0] = [s_new as u32, old.as_ref().map_or(1, |o| o.0 as u32), flags, 0];
p.f[0] = [frac, h.div_ceil(s_new) as f32, 0.0, 0.0];
let out = e.scratch(w as u32, h as u32);
e.pixels("fp2_block_load", &p, &new, old.as_ref().map(|o| &o.1), &out, None);
b.img = out;
Some(b)
}
// ---------------------------------------------------------------- CC Burn Film (stylize3)
fn burn_film(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let burn = (ctx.params.f("burn") / 100.0).clamp(0.0, 1.0) as f32;
if burn <= 0.0 {
return Some(b);
}
let c = b.to_px(ctx.params.v2("center"));
let seed = ctx.params.f("randomSeed").max(0.0) as u32;
let s = b.scale;
let diag = (ctx.layer_size[0].hypot(ctx.layer_size[1]) * s).max(1.0);
let feat = (60.0 * s).max(1e-3);
let mut p = Params::default();
p.u[0][0] = seed.wrapping_add(17);
p.f[0] = [c.0 as f32, c.1 as f32, diag as f32, feat as f32];
p.f[1] = [1.0 - burn * 1.25, 0.12 * (burn * 10.0).min(1.0), 0.0, 0.0];
run(e, "fp2_burn", &p, b, None, None)
}
// ---------------------------------------------------------------- CC Glass / CC Plastic (stylize3)
fn bump(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let glass = id == "ec.stylize.ccglass";
// stylize3::height_field.
let src = crate::fx_key::fitted(e, ctx, &b, effectcraft_effects::bump_layer_id(ctx), true, false)?.unwrap_or_else(|| b.img.clone());
let soft = ctx.params.f("softness") * b.scale * 0.5;
let mut hf = plane(e, &src, 1, ctx.params.e("property"), soft);
let height = ctx.params.f("height") as f32 / 100.0;
let mut disp = 0.0;
if glass {
disp = (ctx.params.f("displacement") * b.scale * 0.25) as f32;
} else {
let lo = (ctx.params.f("cutMin") / 100.0) as f32;
let hi = (ctx.params.f("cutMax") / 100.0) as f32;
if lo > 0.0 || hi < 1.0 {
hf = clamp_plane(e, &hf, lo.min(hi), hi.max(lo));
}
}
let l = effectcraft_effects::BumpLight::from(ctx, &geo(&b));
let mut p = Params::default();
p.u[0] = [glass as u32, l.point as u32, 0, 0];
p.f[0] = [height * 25.0, disp, (height != 0.0) as u32 as f32, l.height];
p.f[1] = [l.intensity, l.ambient, l.diffuse, l.specular];
p.f[2] = [l.shininess, l.metal, l.pos.0 as f32, l.pos.1 as f32];
p.f[3] = [l.color[0], l.color[1], l.color[2], 0.0];
p.f[4] = [l.dir[0], l.dir[1], l.dir[2], 0.0];
run(e, "fp2_bump", &p, b, Some(&hf), None)
}
/// An empty CPU buffer with `b`'s geometry (for CPU helpers that only read positions).
fn geo(b: &GBuf) -> effectcraft_effects::Buf {
effectcraft_effects::Buf { img: Image { width: b.img.width, height: b.img.height, data: vec![] }, offset: b.offset, scale: b.scale }
}
/// Clamp a plane (x) into [lo, hi] (Plane::map with f32::clamp).
fn clamp_plane(e: &mut Enc, pl: &GpuImage, lo: f32, hi: f32) -> GpuImage {
let mut p = Params::default();
p.u[0] = [3, 0, 0, 0];
p.f[0] = [lo, hi, 0.0, 0.0];
let out = e.scratch(pl.width, pl.height);
e.pixels("fp2_plane", &p, pl, None, &out, None);
out
}
// ---------------------------------------------------------------- CC HexTile / CC Mr. Smoothie (stylize3)
fn hextile(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let r = (ctx.params.f("radius") * b.scale).max(0.5);
let c = b.to_px(ctx.params.v2("center"));
let (sn, cs) = ctx.params.f("rotate").to_radians().sin_cos();
let mut p = Params::default();
p.u[0][0] = ctx.params.e("render");
p.f[0] = [c.0 as f32, c.1 as f32, r as f32, (ctx.params.f("smearing") / 100.0).clamp(0.0, 1.0) as f32];
p.f[1] = [sn as f32, cs as f32, 0.0, 0.0];
run(e, "fp2_hextile", &p, b, None, None)
}
fn smoothie(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let flow = crate::fx_key::fitted(e, ctx, &b, "flowLayer", true, false)?.unwrap_or_else(|| b.img.clone());
let sm = ctx.params.f("smoothness") * b.scale * 0.5;
let pl = plane(e, &flow, 2, ctx.params.e("property"), sm);
let a = b.to_px(ctx.params.v2("sampleA"));
let bb = b.to_px(ctx.params.v2("sampleB"));
let mut p = Params::default();
p.f[0] = [a.0 as f32, a.1 as f32, bb.0 as f32, bb.1 as f32];
p.f[1] = [(ctx.params.f("phase") / 360.0) as f32, (ctx.params.e("colorLoop") + 1) as f32, 0.0, 0.0];
run(e, "fp2_smoothie", &p, b, Some(&pl), None)
}
// ---------------------------------------------------------------- Inner/Outer Key (keying2)
fn inner_outer(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let Some(plan) = effectcraft_effects::inner_outer_plan(ctx, &geo(&b)) else { return Some(b) };
let (w, h) = (b.img.width, b.img.height);
let mut tri = Image::new(w, h);
for (px, &t) in tri.data.iter_mut().zip(&plan.tri.data) {
px[0] = t;
}
let tri = e.g.upload_image(&tri)?;
// The matte (x, or w of the estimate) and the estimate's colours.
let (mut matte, est) = if plan.unknown {
let t = e.scratch(w, h);
e.pixels("fp2_io_prep", &Params::default(), &b.img, Some(&tri), &t, None);
let known = |e: &mut Enc, side: u32| {
let mut p = Params::default();
p.u[0][0] = side;
let k = e.scratch(w, h);
e.pixels("fp2_io_known", &p, &t, None, &k, None);
gauss_plane(e, &k, plan.sigma, plan.sigma)
};
let fg = known(e, 0);
let bg = known(e, 1);
let (rows, stride) = e.image_rows(&t);
let mut p = Params::default();
p.u[0][0] = stride;
let est = e.scratch(w, h);
e.pixels("fp2_io_est", &p, &fg, Some(&bg), &est, Some(&rows));
(est.clone(), Some(est))
} else {
(tri, None)
};
let from_w = est.is_some() as u32;
if !plan.strokes.is_empty() || from_w != 0 {
let n = plan.strokes.len();
let mut data = Vec::with_capacity(n + n * (w * h) as usize);
data.extend(plan.strokes.iter().map(|s| s.0));
for (_, cov) in &plan.strokes {
data.extend_from_slice(cov);
}
let buf = e.data(&data);
let mut p = Params::default();
p.u[0] = [n as u32, from_w, 0, 0];
let out = e.scratch(w, h);
e.pixels("fp2_io_strokes", &p, &matte, None, &out, Some(&buf));
matte = out;
}
if plan.thin.abs() > 0.01 {
let modes = if plan.thin < 0.0 { crate::fx_color::DILATE_X } else { crate::fx_color::ERODE_X };
matte = crate::fx_color::morph_frac(e, &matte, plan.thin.abs(), modes);
}
if plan.feather > 0.05 {
matte = gauss_plane(e, &matte, plan.feather * 0.5, plan.feather * 0.5);
}
let mut p = Params::default();
p.u[0] = [0, from_w, plan.invert as u32, 0];
p.f[0] = [plan.threshold, plan.original, 0.0, 0.0];
match est {
Some(est) => {
let (rows, stride) = e.image_rows(&est);
p.u[0][0] = stride;
run(e, "fp2_io_final", &p, b, Some(&matte), Some(&rows))
}
None => run(e, "fp2_io_final", &p, b, Some(&matte), None),
}
}
// ---------------------------------------------------------------- CC Simple Wire Removal (keying2)
fn wire_removal(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let half = (ctx.params.f("thickness") * b.scale * 0.5).max(0.0);
if half <= 0.0 {
return Some(b);
}
// Frame Offset reads another frame from the effect host: that renders on the CPU.
if ctx.params.e("removalStyle") == 1 && ctx.params.f("frameOffset").round() != 0.0 && ctx.env.host.is_some() {
return None;
}
let slope = (ctx.params.f("slope") / 100.0).clamp(0.0, 1.0) as f32;
let mirror = (ctx.params.f("mirrorBlend") / 100.0).clamp(0.0, 1.0) as f32;
let a = b.to_px(ctx.params.v2("pointA"));
let z = b.to_px(ctx.params.v2("pointB"));
let (dx, dy) = (z.0 - a.0, z.1 - a.1);
let len = (dx * dx + dy * dy).sqrt();
if len < 1e-6 {
return Some(b);
}
let (tx, ty) = (dx / len, dy / len);
let mut p = Params::default();
p.u[0][0] = ctx.params.e("removalStyle");
p.f[0] = [a.0 as f32, a.1 as f32, -ty as f32, tx as f32];
p.f[1] = [half as f32, (half * slope as f64) as f32, mirror, len as f32];
run(e, "fp2_wire", &p, b, None, None)
}
// ---------------------------------------------------------------- Basic 3D (obsolete)
fn basic_3d(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let swivel = ctx.params.f("swivel").to_radians();
let tilt = ctx.params.f("tilt").to_radians();
let dist = ctx.params.f("distanceToImage") * b.scale;
let spec_on = ctx.params.b("specularHighlight");
let wire = ctx.params.b("preview/drawPreviewWireframe");
if swivel == 0.0 && tilt == 0.0 && dist == 0.0 && !spec_on && !wire {
return Some(b);
}
let (lx, ly, lw, lh) = (b.offset[0], b.offset[1], ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale);
let (cx, cy) = (lx + lw * 0.5, ly + lh * 0.5);
let f = lw.max(lh).max(1.0) * 1.4;
let (cs, ss) = (swivel.cos(), swivel.sin());
let (ct, st) = (tilt.cos(), tilt.sin());
let v3 = |v: [f64; 3]| [v[0] as f32, v[1] as f32, v[2] as f32, 0.0];
let l = [-0.5f64, -0.6, -1.0];
let k = 1.0 / (l[0] * l[0] + l[1] * l[1] + l[2] * l[2]).sqrt();
let mut p = Params::default();
p.u[0] = [spec_on as u32, wire as u32, 0, 0];
p.f[0] = [cx as f32, cy as f32, f as f32, dist as f32];
p.f[1] = v3([cs, 0.0, -ss]);
p.f[2] = v3([ss * st, ct, cs * st]);
p.f[3] = v3([ss * ct, -st, cs * ct]);
p.f[4] = v3(l.map(|v| v * k));
p.f[5] = [(lw * 0.5) as f32, (lh * 0.5) as f32, 0.0, 0.0];
run(e, "fp2_basic3d", &p, b, None, None)
}