Merge branch 'worktree-agent-a51902829f462d739'
This commit is contained in:
commit
3ee5823429
|
|
@ -29,7 +29,7 @@ Measured feature by feature in [docs/parity.md](docs/parity.md) (3 October 2026,
|
|||
|
||||
- **≈ 98% of After Effects' features, weighted by importance**, counting partial features as half
|
||||
done (≈ 99.6% with per-feature fractions): 87 of 92 features done, 5 partial, none missing; all
|
||||
306 effects implemented in full, 166 of them on the GPU.
|
||||
306 effects implemented in full, 236 of them on the GPU.
|
||||
- **≈ 9–11 agent-hours of work remain**: about **2–2.5 hours of wall-clock time** with five Claude
|
||||
Opus 5.5 agents in parallel, ≈ 1.5 hours for the essentials and daily-use features.
|
||||
- What is left: more GPU effects, GPU effects and a disk cache in the web app, a template gallery
|
||||
|
|
|
|||
|
|
@ -63,10 +63,7 @@ pub fn param_shown(effect: &str, param: &str, value: &dyn Fn(&str) -> Option<eff
|
|||
/// Controls of GPU-accelerated effects that only the CPU implements: while any of them is away
|
||||
/// from its default the effect renders on the CPU. (Prefixes ending in `/` cover a whole group.)
|
||||
const CPU_ONLY_CONTROLS: &[(&str, &[&str])] = &[
|
||||
(
|
||||
"ec.color.lumetri",
|
||||
&["highDynamicRange", "basicCorrection/inputLut", "creative/look", "curves/rgbCurves/", "curves/hueSaturationCurves/", "hslSecondary/"],
|
||||
),
|
||||
("ec.color.lumetri", &["highDynamicRange", "curves/rgbCurves/", "curves/hueSaturationCurves/", "hslSecondary/"]),
|
||||
(
|
||||
"ec.color.colorama",
|
||||
&["inputPhase/addPhase", "outputCycle/usePresetPalette", "modify/", "pixelSelection/matchingMode", "masking/maskingMode", "masking/compositeOverLayer"],
|
||||
|
|
@ -100,6 +97,19 @@ pub fn gpu_supported(id: &str, ctx: &crate::EffectCtx) -> bool {
|
|||
match id {
|
||||
"ec.generate.cellpattern" if (6..=10).contains(&ctx.params.e("cellPattern")) => return false,
|
||||
"ec.distort.turbulentdisplace" if ctx.params.e("pinning") >= 8 => return false,
|
||||
// Shatter's wireframe views; Foam's User Defined texture, Environment Map and flow-map
|
||||
// preview (the GPU draws the rendered pieces and the sprite bubbles only).
|
||||
"ec.sim.shatter" if ctx.params.e("view") != 0 => return false,
|
||||
"ec.sim.foam" => {
|
||||
let set = |id: &str| matches!(ctx.params.get(id), Some(effectcraft_keyframe::Value::Layer(Some(_))));
|
||||
let view = ctx.params.e("view");
|
||||
if (view == 2 && ctx.params.e("rendering/bubbleTexture") == 5)
|
||||
|| (view == 2 && ctx.params.f("rendering/reflectionStrength") > 0.0 && set("rendering/environmentMap"))
|
||||
|| (view == 1 && set("flowMap/flowMap"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Timewarp's Matte Layer (foreground and background on their own vectors).
|
||||
"ec.time.timewarp" if ctx.params.get("matteLayer").and_then(effectcraft_keyframe::Value::as_layer).is_some() => return false,
|
||||
// Fisheye / Twist (a crease at the unit circle) bent past 50 % or with Horizontal /
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ fn plane_of(b: &Buf, idx: &[Option<usize>], ch: &[f32], fill: f32) -> Plane {
|
|||
}
|
||||
|
||||
/// Distinct colour for an ID (hash → hue).
|
||||
fn id_color(id: f32) -> [f32; 3] {
|
||||
pub fn id_color(id: f32) -> [f32; 3] {
|
||||
let h = hash1(id.to_bits(), 0x1d, 0x5eed);
|
||||
let (r, g, b) = effectcraft_color::hsl_to_rgb(h, 0.75, 0.5);
|
||||
if id == 0.0 { [0.0; 3] } else { [r, g, b] }
|
||||
|
|
|
|||
|
|
@ -98,6 +98,13 @@ pub use textfx::{TextLayer, text_layer};
|
|||
pub use time_fx::{PixelMotion, TimewarpPlan, TwStep, posterized_time, time_frames, timewarp_crop, timewarp_plan};
|
||||
// effectcraft-gpu fx_tone.
|
||||
pub use ocio::color_stabilizer_maps;
|
||||
// GPU effects, part B (3D Channel, Immersive Video, colour management, simulation render passes).
|
||||
pub use channel3d::{CRYPTO_LAYERS, id_color, selection_hashes};
|
||||
pub use ocio::{ColorOp, Straight, Tf, color_program};
|
||||
pub use sim::{Acc, Blob, Bubble, Post, Shape, Sprite, SpritePlan, Tint, bubble_list, drizzle_drops, mercury_blobs, sprite_plan};
|
||||
pub use sim2::{CausticsSetup, Piece, PiecePlan, PieceTex, WavePlan, Waves, caustics_setup, piece_plan, wave_world_plan};
|
||||
pub use utility::{Lut, load_lut};
|
||||
pub use vr::{equi_dir as vr_equi_dir, rotation as vr_rotation};
|
||||
|
||||
/// Effect categories in Effects & Presets order.
|
||||
pub const CATEGORIES: &[&str] = &[
|
||||
|
|
@ -932,6 +939,50 @@ pub const GPU_EFFECTS: &[&str] = &[
|
|||
"ec.perspective.bevelalpha",
|
||||
"ec.perspective.beveledges",
|
||||
"ec.obsolete.gaussianlegacy",
|
||||
// GPU effects, part B: 3D Channel.
|
||||
"ec.3d.channelextract",
|
||||
"ec.3d.cryptomatte",
|
||||
"ec.3d.depthmatte",
|
||||
"ec.3d.depthoffield",
|
||||
"ec.3d.extractor",
|
||||
"ec.3d.fog3d",
|
||||
"ec.3d.idmatte",
|
||||
"ec.3d.identifier",
|
||||
// Colour management.
|
||||
"ec.utility.applylut",
|
||||
"ec.color.ociocdl",
|
||||
"ec.color.ociocolorspace",
|
||||
"ec.color.ociodisplay",
|
||||
"ec.color.ociofile",
|
||||
"ec.color.ociolook",
|
||||
"ec.utility.colorprofileconverter",
|
||||
// Immersive Video.
|
||||
"ec.vr.blur",
|
||||
"ec.vr.chromaticaberrations",
|
||||
"ec.vr.colorgradients",
|
||||
"ec.vr.converter",
|
||||
"ec.vr.denoise",
|
||||
"ec.vr.digitalglitch",
|
||||
"ec.vr.fractalnoise",
|
||||
"ec.vr.glow",
|
||||
"ec.vr.planetosphere",
|
||||
"ec.vr.rotatesphere",
|
||||
"ec.vr.sharpen",
|
||||
"ec.vr.spheretoplane",
|
||||
// Simulation render passes.
|
||||
"ec.sim.ccrainfall",
|
||||
"ec.sim.ccsnowfall",
|
||||
"ec.sim.ccstarburst",
|
||||
"ec.sim.ccbubbles",
|
||||
"ec.sim.ccdrizzle",
|
||||
"ec.sim.cchair",
|
||||
"ec.sim.ccmrmercury",
|
||||
"ec.sim.caustics",
|
||||
"ec.sim.waveworld",
|
||||
"ec.sim.foam",
|
||||
"ec.sim.shatter",
|
||||
"ec.sim.carddance",
|
||||
"ec.transition.cardwipe",
|
||||
// effectcraft-gpu fx_light (the CC light family)
|
||||
"ec.generate.cclightrays",
|
||||
"ec.generate.cclightburst",
|
||||
|
|
|
|||
|
|
@ -865,6 +865,177 @@ fn profile_converter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
b
|
||||
}
|
||||
|
||||
// ---- Colour programs (the GPU compositor's colour-management kernels)
|
||||
|
||||
/// One per-pixel step of a colour transform, in the order the CPU effect applies them (see
|
||||
/// [`color_program`]).
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ColorOp {
|
||||
/// Transfer function, encoded → linear.
|
||||
Decode(Tf),
|
||||
/// Transfer function, linear → encoded.
|
||||
Encode(Tf),
|
||||
/// Linear-RGB matrix.
|
||||
Matrix([[f32; 3]; 3]),
|
||||
/// ASC CDL (forward or exact inverse; `clamp` = the v1.2 style).
|
||||
Cdl { cdl: Cdl, clamp: bool, inverse: bool },
|
||||
/// The Tone Mapped view's roll-off (or its inverse).
|
||||
Tonemap { inverse: bool },
|
||||
/// Color Profile Converter's perceptual gamut compression toward luminance `luma`.
|
||||
Gamut([f64; 3]),
|
||||
/// Negative channels to 0 (the saturation intent).
|
||||
Max0,
|
||||
/// A LUT (0 nearest, 1 trilinear, 2 tetrahedral) with an optional per-channel shaper, forward
|
||||
/// or inverse (as [`FileXform::apply`]).
|
||||
Lut { lut: Arc<Lut>, shaper: Option<Arc<[Vec<(f32, f32)>; 3]>>, interp: u32, inverse: bool },
|
||||
}
|
||||
|
||||
/// How a colour program meets premultiplied pixels.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Straight {
|
||||
/// `unpremul` (colour 0 below alpha 1e-6); fully transparent pixels untouched (OCIO effects).
|
||||
Unpremul,
|
||||
/// Colour ÷ alpha; fully transparent pixels untouched (`Image::map_straight`, Apply Color LUT).
|
||||
Divide,
|
||||
}
|
||||
|
||||
fn xform_ops(x: &Xform, ops: &mut Vec<ColorOp>) {
|
||||
if x.dec != Tf::Linear {
|
||||
ops.push(ColorOp::Decode(x.dec));
|
||||
}
|
||||
if !x.ident {
|
||||
ops.push(ColorOp::Matrix(x.m));
|
||||
}
|
||||
if x.enc != Tf::Linear {
|
||||
ops.push(ColorOp::Encode(x.enc));
|
||||
}
|
||||
}
|
||||
|
||||
/// Effect `id`'s per-pixel transform as a list of [`ColorOp`]s that reproduces the CPU effect
|
||||
/// (empty = the layer passes through). `None` when it has no such form (a custom OCIO config,
|
||||
/// another effect).
|
||||
pub fn color_program(id: &str, ctx: &EffectCtx) -> Option<(Vec<ColorOp>, Straight)> {
|
||||
let pr = ctx.params;
|
||||
let mut ops = vec![];
|
||||
match id {
|
||||
"ec.utility.applylut" => {
|
||||
if let Some(lut) = crate::utility::load_lut(pr.s("lut")) {
|
||||
ops.push(ColorOp::Lut { lut, shaper: None, interp: 2, inverse: false });
|
||||
}
|
||||
return Some((ops, Straight::Divide));
|
||||
}
|
||||
"ec.color.ociocolorspace" => {
|
||||
if pr.e("config") == 1 {
|
||||
return custom_config(ctx).is_none().then_some((ops, Straight::Unpremul));
|
||||
}
|
||||
let (a, z) = (space_at(pr.e("source")), space_at(pr.e("destination")));
|
||||
let (a, z) = if pr.e("direction") == 1 { (z, a) } else { (a, z) };
|
||||
if a.name != z.name {
|
||||
xform_ops(&Xform::new(a, z), &mut ops);
|
||||
}
|
||||
}
|
||||
"ec.color.ociocdl" => {
|
||||
let cdl = cdl_from_params(pr);
|
||||
if cdl != Cdl::default() {
|
||||
ops.push(ColorOp::Cdl { cdl, clamp: pr.e("style") == 0, inverse: pr.e("direction") == 1 });
|
||||
}
|
||||
}
|
||||
"ec.color.ociodisplay" => {
|
||||
if pr.e("config") == 1 {
|
||||
return custom_config(ctx).is_none().then_some((ops, Straight::Unpremul));
|
||||
}
|
||||
let input = space_at(pr.e("source"));
|
||||
let (display, view, inverse) = (pr.e("display") as usize, pr.e("view") as usize, pr.e("direction") == 1);
|
||||
let (_, prims, tf) = DISPLAYS[display.min(DISPLAYS.len() - 1)];
|
||||
if view != 2 && !input.raw {
|
||||
let disp = Space { name: "display", prims: Some((prims, D65)), tf: Tf::Linear, raw: false };
|
||||
let lin = Space { tf: Tf::Linear, ..*input };
|
||||
if !inverse {
|
||||
ops.push(ColorOp::Decode(input.tf));
|
||||
xform_ops(&Xform::new(&lin, &disp), &mut ops);
|
||||
if view == 1 {
|
||||
ops.push(ColorOp::Tonemap { inverse: false });
|
||||
}
|
||||
ops.push(ColorOp::Encode(tf));
|
||||
} else {
|
||||
ops.push(ColorOp::Decode(tf));
|
||||
if view == 1 {
|
||||
ops.push(ColorOp::Tonemap { inverse: true });
|
||||
}
|
||||
xform_ops(&Xform::new(&disp, &lin), &mut ops);
|
||||
ops.push(ColorOp::Encode(input.tf));
|
||||
}
|
||||
}
|
||||
}
|
||||
"ec.color.ociofile" => {
|
||||
if let Some(x) = load_file(pr.s("file")) {
|
||||
let interp = match pr.e("interpolation") {
|
||||
0 => 0,
|
||||
1 => 1,
|
||||
_ => 2,
|
||||
};
|
||||
let inverse = pr.e("direction") == 1;
|
||||
match &*x {
|
||||
FileXform::Cdl(list) => {
|
||||
let id = pr.s("cccId");
|
||||
let cdl = list.iter().find(|(i, _)| !id.is_empty() && i == id).or(list.first()).map(|(_, c)| *c).unwrap_or_default();
|
||||
ops.push(ColorOp::Cdl { cdl, clamp: true, inverse });
|
||||
}
|
||||
FileXform::Lut { lut, shaper } => {
|
||||
ops.push(ColorOp::Lut { lut: Arc::new(lut.clone()), shaper: shaper.clone().map(Arc::new), interp, inverse });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"ec.color.ociolook" => {
|
||||
let mut list = parse_looks(pr.s("looks"));
|
||||
let pick = pr.e("look");
|
||||
if pick > 0
|
||||
&& let Some((_, c)) = looks().get(pick as usize - 1)
|
||||
{
|
||||
list.insert(0, (*c, false));
|
||||
}
|
||||
if !list.is_empty() {
|
||||
let (src, dst) = (space_at(pr.e("source")), space_at(pr.e("destination")));
|
||||
let inv = pr.e("direction") == 1;
|
||||
let cct = space_by_name("ACEScct").expect("built-in");
|
||||
let (a, z) = if inv { (dst, src) } else { (src, dst) };
|
||||
if inv {
|
||||
list.reverse();
|
||||
}
|
||||
xform_ops(&Xform::new(a, cct), &mut ops);
|
||||
for (cdl, li) in &list {
|
||||
ops.push(ColorOp::Cdl { cdl: *cdl, clamp: false, inverse: *li != inv });
|
||||
}
|
||||
xform_ops(&Xform::new(cct, z), &mut ops);
|
||||
}
|
||||
}
|
||||
"ec.utility.colorprofileconverter" => {
|
||||
let (inp, in_lin_default) = profile(ctx, pr.e("inputProfile"));
|
||||
let (out, out_lin_default) = profile(ctx, pr.e("outputProfile"));
|
||||
let in_lin = pr.b("linearizeInputProfile") || in_lin_default && pr.e("inputProfile") == 0;
|
||||
let out_lin = pr.b("linearizeOutputProfile") || out_lin_default && pr.e("outputProfile") == 0;
|
||||
let intent = pr.e("intent");
|
||||
let dec = if in_lin { Tf::Linear } else { inp.tf };
|
||||
let enc = if out_lin { Tf::Linear } else { out.tf };
|
||||
let m = if intent == 3 { mul(&invert(&out.to_ref(false)), &inp.to_ref(false)) } else { matrix(&inp, &out) };
|
||||
let identity = dec == enc && (0..3).all(|i| (0..3).all(|j| (m[i][j] - IDENTITY[i][j]).abs() < 1e-9));
|
||||
if !identity {
|
||||
ops.push(ColorOp::Decode(dec));
|
||||
ops.push(ColorOp::Matrix(m.map(|r| r.map(|v| v as f32))));
|
||||
match intent {
|
||||
0 => ops.push(ColorOp::Gamut(out.to_ref(true)[1])),
|
||||
2 => ops.push(ColorOp::Max0),
|
||||
_ => {}
|
||||
}
|
||||
ops.push(ColorOp::Encode(enc));
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
Some((ops, Straight::Unpremul))
|
||||
}
|
||||
|
||||
// ---- Color Stabilizer
|
||||
|
||||
/// Mean straight colour of a square (half-size `r` layer pixels) around layer point `pt`.
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const BAND: usize = 16;
|
|||
|
||||
/// How rasterised items combine with what is already in the target.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub(crate) enum Acc {
|
||||
pub enum Acc {
|
||||
/// Premultiplied "over" (later items on top).
|
||||
Over,
|
||||
/// Additive (glows, light).
|
||||
|
|
@ -128,7 +128,7 @@ pub(crate) fn raster<T: Sync>(
|
|||
// ------------------------------------------------------------------ sprites
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub(crate) enum Shape {
|
||||
pub enum Shape {
|
||||
/// Soft round dot (Gaussian-like falloff).
|
||||
Soft,
|
||||
/// Hard anti-aliased disc.
|
||||
|
|
@ -151,7 +151,7 @@ pub(crate) enum Shape {
|
|||
|
||||
/// A particle sprite in buffer pixels; `c` is straight colour + opacity.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct Sprite {
|
||||
pub struct Sprite {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub r: f32,
|
||||
|
|
@ -166,6 +166,112 @@ impl Sprite {
|
|||
}
|
||||
}
|
||||
|
||||
/// Where a sprite's colour comes from when it depends on the layer's pixels (resolved by
|
||||
/// [`SpritePlan::resolve`] on the CPU, per pixel by the GPU compositor). Positions are buffer
|
||||
/// pixels.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Tint {
|
||||
/// The sprite's own colour.
|
||||
Fixed,
|
||||
/// CC Rainfall's refracting drops: where the layer shows (alpha > 0), colour × (layer
|
||||
/// colour × 0.6 + 0.5) sampled at (x, y).
|
||||
Refract { x: f64, y: f64 },
|
||||
/// CC Star Burst: the layer's colour at (x, y), opacity × its alpha (none where clear).
|
||||
Source { x: f64, y: f64 },
|
||||
/// CC Hair: the root's colour at (x, y) mixed with the hair colour (`inherit` = 1 keeps the
|
||||
/// root's), × `shade`, + `spec`; no strand where the root's alpha is below ½.
|
||||
Root { x: f64, y: f64, hair: [f32; 3], inherit: f32, shade: f32, spec: f32 },
|
||||
}
|
||||
|
||||
/// How a sprite layer meets the original layer.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Post {
|
||||
/// [`combine`] with this mode (0 Composite … 5 sprites only).
|
||||
Combine(u32),
|
||||
/// The sprites alone.
|
||||
Replace,
|
||||
/// The sprites, mixed toward the original by this fraction.
|
||||
Lerp(f32),
|
||||
/// The sprites over opaque black.
|
||||
OverBlack,
|
||||
}
|
||||
|
||||
/// What a sprite-drawing effect draws this frame: built on the CPU (closed-form or from the
|
||||
/// simulation state), rasterised by [`SpritePlan::finish`] or the GPU compositor.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SpritePlan {
|
||||
pub sprites: Vec<Sprite>,
|
||||
/// Per sprite (empty = all [`Tint::Fixed`]).
|
||||
pub tints: Vec<Tint>,
|
||||
pub acc: Acc,
|
||||
pub post: Post,
|
||||
}
|
||||
|
||||
impl SpritePlan {
|
||||
/// Resolve layer-dependent colours against the layer `src`.
|
||||
pub fn resolve(&mut self, src: &Image) {
|
||||
let tints = std::mem::take(&mut self.tints);
|
||||
self.sprites.par_iter_mut().zip(tints.par_iter()).for_each(|(sp, t)| match *t {
|
||||
Tint::Fixed => {}
|
||||
Tint::Refract { x, y } => {
|
||||
let (sc, sa) = unpremul(src.sample_bilinear(x, y));
|
||||
if sa > 0.0 {
|
||||
let c = sp.c;
|
||||
sp.c = [(sc[0] * 0.6 + 0.5) * c[0], (sc[1] * 0.6 + 0.5) * c[1], (sc[2] * 0.6 + 0.5) * c[2], c[3]];
|
||||
}
|
||||
}
|
||||
Tint::Source { x, y } => {
|
||||
let (c, a) = unpremul(src.sample_bilinear(x, y));
|
||||
sp.c = if a <= 0.0 { [0.0; 4] } else { [c[0], c[1], c[2], a * sp.c[3]] };
|
||||
}
|
||||
Tint::Root { x, y, hair, inherit, shade, spec } => {
|
||||
let root = src.sample_bilinear(x, y);
|
||||
if root[3] < 0.5 {
|
||||
sp.c = [0.0; 4];
|
||||
return;
|
||||
}
|
||||
let (rc, _) = unpremul(root);
|
||||
let base: [f32; 3] = std::array::from_fn(|k| rc[k] + (hair[k] - rc[k]) * (1.0 - inherit));
|
||||
sp.c = [base[0] * shade + spec, base[1] * shade + spec, base[2] * shade + spec, sp.c[3]];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Rasterise (after [`SpritePlan::resolve`]) and meet the original layer `b`.
|
||||
pub fn finish(&self, mut b: Buf) -> Buf {
|
||||
let fx = splat(b.img.width, b.img.height, &self.sprites, self.acc);
|
||||
b.img = match self.post {
|
||||
Post::Combine(m) => combine(&b.img, &fx, m),
|
||||
Post::Replace => fx,
|
||||
Post::Lerp(t) => {
|
||||
let mut o = fx;
|
||||
o.data.par_iter_mut().zip(b.img.data.par_iter()).for_each(|(o, s0)| *o = crate::util::lerp4(*o, *s0, t));
|
||||
o
|
||||
}
|
||||
Post::OverBlack => combine(&Image::filled(b.img.width, b.img.height, [0.0, 0.0, 0.0, 1.0]), &fx, 0),
|
||||
};
|
||||
b
|
||||
}
|
||||
}
|
||||
|
||||
/// The sprite plan of a sprite-drawing simulation effect for buffer geometry `b` (its pixels
|
||||
/// are not read), `None` for other effects or settings drawn otherwise (the GPU compositor's
|
||||
/// render pass).
|
||||
pub fn sprite_plan(id: &str, ctx: &EffectCtx, b: &Buf) -> Option<SpritePlan> {
|
||||
match id {
|
||||
"ec.sim.ccrainfall" => Some(rainfall_plan(ctx, b)),
|
||||
"ec.sim.ccsnowfall" => Some(snowfall_plan(ctx, b)),
|
||||
"ec.sim.ccstarburst" => Some(star_burst_plan(ctx, b)),
|
||||
"ec.sim.cchair" => crate::sim3::hair_plan(ctx, b),
|
||||
"ec.sim.waveworld" => crate::sim2::wave_world_plan(ctx, b).and_then(|p| match p {
|
||||
crate::sim2::WavePlan::Wire(sp) => Some(sp),
|
||||
crate::sim2::WavePlan::Height { .. } => None,
|
||||
}),
|
||||
"ec.sim.foam" => crate::sim2::foam_plan(ctx, b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn sprite_bbox(s: &Sprite) -> Option<[f32; 4]> {
|
||||
if s.c[3] <= 1e-5 || s.r <= 0.0 {
|
||||
return None;
|
||||
|
|
@ -316,6 +422,12 @@ fn wrap(v: f32, lo: f32, span: f32) -> f32 {
|
|||
}
|
||||
|
||||
fn rainfall(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
let mut plan = rainfall_plan(ctx, &b);
|
||||
plan.resolve(&b.img);
|
||||
plan.finish(b)
|
||||
}
|
||||
|
||||
fn rainfall_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan {
|
||||
let pr = ctx.params;
|
||||
let n = pr.f("drops").clamp(0.0, 100_000.0) as u32;
|
||||
let size = pr.f("size").max(0.0) as f32;
|
||||
|
|
@ -334,11 +446,10 @@ fn rainfall(ctx: &EffectCtx, b: Buf) -> Buf {
|
|||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let s = b.scale as f32;
|
||||
let t = ctx.time as f32;
|
||||
let src = &b.img;
|
||||
let (ox, oy) = (off[0] as f32 - lw * 0.5, off[1] as f32 - lh * 0.5);
|
||||
let sprites: Vec<Sprite> = (0..n)
|
||||
let (sprites, tints): (Vec<Sprite>, Vec<Tint>) = (0..n)
|
||||
.into_par_iter()
|
||||
.filter_map(|i| {
|
||||
.map(|i| {
|
||||
let z = h(i, 1, seed);
|
||||
let k = depth_k(z, depth);
|
||||
let vy = speed / 4000.0 * lh * 2.2 * k;
|
||||
|
|
@ -349,29 +460,25 @@ fn rainfall(ctx: &EffectCtx, b: Buf) -> Buf {
|
|||
let y = wrap(h(i, 3, seed) * (lh + 2.0 * margin) + vy * t + oy, -margin, lh + 2.0 * margin);
|
||||
let x = wrap(h(i, 4, seed) * (lw + 2.0 * margin) + w * t + ox, -margin, lw + 2.0 * margin);
|
||||
let (bx, by) = b.to_px([x as f64, y as f64]);
|
||||
let mut c = [color[0], color[1], color[2], opacity * (0.35 + 0.65 * k)];
|
||||
if refract {
|
||||
// Refracting drops show the scene behind them, brightened and offset.
|
||||
let (sc, sa) = unpremul(src.sample_bilinear(bx + 2.0 * s as f64, by));
|
||||
let m = |a: f32, cc: f32| (a * 0.6 + 0.5) * cc;
|
||||
if sa > 0.0 {
|
||||
c = [m(sc[0], color[0]), m(sc[1], color[1]), m(sc[2], color[2]), c[3]];
|
||||
}
|
||||
}
|
||||
let c = [color[0], color[1], color[2], opacity * (0.35 + 0.65 * k)];
|
||||
// Refracting drops show the scene behind them, brightened and offset.
|
||||
let tint = if refract { Tint::Refract { x: bx + 2.0 * s as f64, y: by } } else { Tint::Fixed };
|
||||
let r = (size * 0.6 * k * s).max(0.25);
|
||||
Some(Sprite::new(bx as f32, by as f32, r, c, Shape::Line { dx: -lenx * s, dy: -len * s }))
|
||||
(Sprite::new(bx as f32, by as f32, r, c, Shape::Line { dx: -lenx * s, dy: -len * s }), tint)
|
||||
})
|
||||
.collect();
|
||||
weather_out(b, &sprites, mode, with_orig)
|
||||
.unzip();
|
||||
SpritePlan { sprites, tints, acc: Acc::Over, post: weather_post(mode, with_orig) }
|
||||
}
|
||||
|
||||
fn weather_out(mut b: Buf, sprites: &[Sprite], mode: u32, with_orig: bool) -> Buf {
|
||||
let fx = splat(b.img.width, b.img.height, sprites, Acc::Over);
|
||||
b.img = if with_orig { combine(&b.img, &fx, if mode == 1 { 3 } else { 0 }) } else { fx };
|
||||
b
|
||||
fn weather_post(mode: u32, with_orig: bool) -> Post {
|
||||
if with_orig { Post::Combine(if mode == 1 { 3 } else { 0 }) } else { Post::Replace }
|
||||
}
|
||||
|
||||
fn snowfall(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
snowfall_plan(ctx, &b).finish(b)
|
||||
}
|
||||
|
||||
fn snowfall_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan {
|
||||
let pr = ctx.params;
|
||||
let n = pr.f("flakes").clamp(0.0, 200_000.0) as u32;
|
||||
let size = pr.f("size").max(0.0) as f32;
|
||||
|
|
@ -414,35 +521,36 @@ fn snowfall(ctx: &EffectCtx, b: Buf) -> Buf {
|
|||
Sprite::new(bx as f32, by as f32, r * s, [color[0], color[1], color[2], opacity * (0.4 + 0.6 * k)], Shape::Soft)
|
||||
})
|
||||
.collect();
|
||||
weather_out(b, &sprites, mode, with_orig)
|
||||
SpritePlan { sprites, tints: vec![], acc: Acc::Over, post: weather_post(mode, with_orig) }
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC Bubbles
|
||||
|
||||
struct Bubble {
|
||||
x: f32,
|
||||
y: f32,
|
||||
r: f32,
|
||||
/// A CC Bubbles bubble in buffer pixels.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Bubble {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub r: f32,
|
||||
/// Source sample centre (buffer px).
|
||||
sx: f32,
|
||||
sy: f32,
|
||||
pub sx: f32,
|
||||
pub sy: f32,
|
||||
}
|
||||
|
||||
fn bubbles(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// CC Bubbles' bubbles this frame (closed-form in time) for buffer geometry `b`.
|
||||
pub fn bubble_list(ctx: &EffectCtx, b: &Buf) -> Vec<Bubble> {
|
||||
let pr = ctx.params;
|
||||
let n = pr.f("bubbleAmount").clamp(0.0, 10_000.0) as u32;
|
||||
let speed = pr.f("bubbleSpeed") as f32;
|
||||
let wamp = pr.f("wobbleAmplitude") as f32;
|
||||
let wfreq = pr.f("wobbleFrequency") as f32;
|
||||
let bsize = pr.f("bubbleSize").max(0.0) as f32;
|
||||
let metal = pr.e("reflectionType") == 1;
|
||||
let shading = pr.e("shadingType");
|
||||
let seed = ctx.seed.wrapping_mul(0x27d4);
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let s = b.scale as f32;
|
||||
let t = ctx.time as f32;
|
||||
let base = lw.min(lh) * 0.035 * bsize;
|
||||
let list: Vec<Bubble> = (0..n)
|
||||
(0..n)
|
||||
.map(|i| {
|
||||
let r = base * (0.5 + h(i, 1, seed));
|
||||
let travel = lh + 2.0 * r;
|
||||
|
|
@ -454,7 +562,14 @@ fn bubbles(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let (sx, sy) = b.to_px([x0 as f64, y as f64]);
|
||||
Bubble { x: bx as f32, y: by as f32, r: r * s, sx: sx as f32, sy: sy as f32 }
|
||||
})
|
||||
.collect();
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn bubbles(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let metal = pr.e("reflectionType") == 1;
|
||||
let shading = pr.e("shadingType");
|
||||
let list = bubble_list(ctx, &b);
|
||||
let src = b.img.clone();
|
||||
let mut out = Image::new(src.width, src.height);
|
||||
raster(
|
||||
|
|
@ -495,28 +610,20 @@ fn bubbles(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
|
||||
// ------------------------------------------------------------------ CC Drizzle
|
||||
|
||||
fn drizzle(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// CC Drizzle's live drops this frame (buffer px: centre x, y, ring radius, amplitude);
|
||||
/// `None` = no drops yet (the layer passes through).
|
||||
pub fn drizzle_drops(ctx: &EffectCtx, b: &Buf) -> Option<Vec<[f32; 4]>> {
|
||||
let pr = ctx.params;
|
||||
let rate = pr.f("dripRate").max(0.0);
|
||||
let life = pr.f("longevity").max(0.01);
|
||||
let rippling = (pr.f("rippling") / 360.0).max(0.05) as f32;
|
||||
let disp = pr.f("displacement") as f32;
|
||||
let height = pr.f("rippleHeight") as f32 / 100.0;
|
||||
let spreading = pr.f("spreading") as f32;
|
||||
let li = pr.f("light/lightIntensity") as f32 / 100.0;
|
||||
let lc = pr.color("light/lightColor");
|
||||
let lheight = pr.f("light/lightHeight") as f32 / 100.0;
|
||||
let ldir = (pr.f("light/lightDirection") as f32).to_radians();
|
||||
let ambient = pr.f("shading/ambient") as f32 / 100.0;
|
||||
let diffuse = pr.f("shading/diffuse") as f32 / 100.0;
|
||||
let specular = pr.f("shading/specular") as f32 / 100.0;
|
||||
let rough = pr.f("shading/roughness").max(0.001) as f32;
|
||||
let height = pr.f("rippleHeight") as f32 / 100.0;
|
||||
let seed = ctx.seed.wrapping_mul(0x165667b1);
|
||||
let t = ctx.time;
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let s = b.scale as f32;
|
||||
if rate <= 0.0 || t < 0.0 {
|
||||
return b;
|
||||
return None;
|
||||
}
|
||||
// Drops born at k / rate with hashed positions; alive while younger than the longevity.
|
||||
let k0 = ((t - life) * rate).floor().max(0.0) as u32;
|
||||
|
|
@ -531,13 +638,31 @@ fn drizzle(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let (x, y) = b.to_px([(h(k, 1, seed) * lw) as f64, (h(k, 2, seed) * lh) as f64]);
|
||||
let radius = spreading * age * lh / 400.0 * s;
|
||||
let amp = height * (1.0 - age / life as f32) * (0.6 + 0.4 * h(k, 3, seed));
|
||||
drops.push((x as f32, y as f32, radius, amp));
|
||||
drops.push([x as f32, y as f32, radius, amp]);
|
||||
}
|
||||
Some(drops)
|
||||
}
|
||||
|
||||
fn drizzle(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let Some(drops) = drizzle_drops(ctx, &b) else { return b };
|
||||
let rippling = (pr.f("rippling") / 360.0).max(0.05) as f32;
|
||||
let disp = pr.f("displacement") as f32;
|
||||
let li = pr.f("light/lightIntensity") as f32 / 100.0;
|
||||
let lc = pr.color("light/lightColor");
|
||||
let lheight = pr.f("light/lightHeight") as f32 / 100.0;
|
||||
let ldir = (pr.f("light/lightDirection") as f32).to_radians();
|
||||
let ambient = pr.f("shading/ambient") as f32 / 100.0;
|
||||
let diffuse = pr.f("shading/diffuse") as f32 / 100.0;
|
||||
let specular = pr.f("shading/specular") as f32 / 100.0;
|
||||
let rough = pr.f("shading/roughness").max(0.001) as f32;
|
||||
let lh = ctx.layer_size[1] as f32;
|
||||
let s = b.scale as f32;
|
||||
let wavelen = (lh * 0.04 * s).max(2.0) / rippling.clamp(0.25, 4.0);
|
||||
let width = wavelen * (1.0 + rippling * 2.0);
|
||||
let field = |x: f32, y: f32| -> f32 {
|
||||
let mut v = 0.0;
|
||||
for &(dx, dy, r, a) in &drops {
|
||||
for &[dx, dy, r, a] in &drops {
|
||||
let d = ((x - dx).powi(2) + (y - dy).powi(2)).sqrt();
|
||||
let u = (d - r) / width;
|
||||
if u.abs() < 1.0 {
|
||||
|
|
@ -586,7 +711,13 @@ fn drizzle(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
|
||||
// ------------------------------------------------------------------ CC Star Burst
|
||||
|
||||
fn star_burst(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn star_burst(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
let mut plan = star_burst_plan(ctx, &b);
|
||||
plan.resolve(&b.img);
|
||||
plan.finish(b)
|
||||
}
|
||||
|
||||
fn star_burst_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan {
|
||||
let pr = ctx.params;
|
||||
let scatter = pr.f("scatter") as f32;
|
||||
let speed = pr.f("speed") as f32;
|
||||
|
|
@ -600,17 +731,12 @@ fn star_burst(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let t = ctx.time as f32;
|
||||
let nx = (lw / spacing).ceil().max(1.0) as u32;
|
||||
let ny = (lh / spacing).ceil().max(1.0) as u32;
|
||||
let src = b.img.clone();
|
||||
let (cx, cy) = (lw * 0.5, lh * 0.5);
|
||||
let mut list: Vec<(f32, Sprite)> = (0..nx * ny)
|
||||
let mut list: Vec<(f32, Sprite, Tint)> = (0..nx * ny)
|
||||
.into_par_iter()
|
||||
.filter_map(|i| {
|
||||
.map(|i| {
|
||||
let (gx, gy) = ((i % nx) as f32 * spacing + spacing * 0.5, (i / nx) as f32 * spacing + spacing * 0.5);
|
||||
let (sx, sy) = b.to_px([gx as f64, gy as f64]);
|
||||
let (c, a) = unpremul(src.sample_bilinear(sx, sy));
|
||||
if a <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
// Depth cycles towards the viewer: z in [0, 1), perspective 1 / (1 - 0.9 z).
|
||||
let z = (h(i, 1, seed) + t * speed * 0.25 + phase).rem_euclid(1.0);
|
||||
let persp = 1.0 / (1.0 - 0.9 * z);
|
||||
|
|
@ -621,19 +747,13 @@ fn star_burst(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let (bx, by) = b.to_px([x as f64, y as f64]);
|
||||
let r = spacing * 0.5 * size * persp * s;
|
||||
let fade = (z * 8.0).min(1.0) * ((1.0 - z) * 6.0).min(1.0);
|
||||
Some((z, Sprite::new(bx as f32, by as f32, r, [c[0], c[1], c[2], a * fade], Shape::Disc)))
|
||||
// The layer's colour at the grid point (none where it is clear).
|
||||
(z, Sprite::new(bx as f32, by as f32, r, [0.0, 0.0, 0.0, fade], Shape::Disc), Tint::Source { x: sx, y: sy })
|
||||
})
|
||||
.collect();
|
||||
list.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
let sprites: Vec<Sprite> = list.into_iter().map(|(_, s)| s).collect();
|
||||
let fx = splat(src.width, src.height, &sprites, Acc::Over);
|
||||
b.img = fx;
|
||||
if blend > 0.0 {
|
||||
b.img.data.par_iter_mut().zip(src.data.par_iter()).for_each(|(o, s0)| {
|
||||
*o = crate::util::lerp4(*o, *s0, blend);
|
||||
});
|
||||
}
|
||||
b
|
||||
let (sprites, tints) = list.into_iter().map(|(_, s, t)| (s, t)).unzip();
|
||||
SpritePlan { sprites, tints, acc: Acc::Over, post: if blend > 0.0 { Post::Lerp(blend) } else { Post::Replace } }
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ particle engine
|
||||
|
|
@ -1067,13 +1187,16 @@ fn particle_systems2(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
|
||||
static MERCURY_CACHE: SimCache<PState> = SimCache::new(6);
|
||||
|
||||
struct Blob {
|
||||
x: f32,
|
||||
y: f32,
|
||||
r: f32,
|
||||
/// A CC Mr. Mercury blob (buffer px).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Blob {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub r: f32,
|
||||
}
|
||||
|
||||
fn mr_mercury(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// CC Mr. Mercury's blobs this frame, from its particle simulation (for buffer geometry `b`).
|
||||
pub fn mercury_blobs(ctx: &EffectCtx, b: &Buf) -> Vec<Blob> {
|
||||
let pr = ctx.params;
|
||||
let lh = ctx.layer_size[1] as f32;
|
||||
let unit = lh.max(1.0);
|
||||
|
|
@ -1097,10 +1220,8 @@ fn mr_mercury(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
};
|
||||
let st = simulate(&MERCURY_CACHE, params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 3), ctx.time, &phys);
|
||||
let (bs, ds) = (pr.f("blobBirthSize") as f32, pr.f("blobDeathSize") as f32);
|
||||
let influence = pr.f("blobInfluence") as f32 / 100.0;
|
||||
let s = b.scale as f32;
|
||||
let blobs: Vec<Blob> = st
|
||||
.parts
|
||||
st.parts
|
||||
.iter()
|
||||
.map(|q| {
|
||||
let u = (q.age / q.life.max(1e-4)).clamp(0.0, 1.0);
|
||||
|
|
@ -1109,7 +1230,14 @@ fn mr_mercury(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
Blob { x: bx as f32, y: by as f32, r: (size * unit * 0.08 * s).max(0.0) }
|
||||
})
|
||||
.filter(|q| q.r > 0.1)
|
||||
.collect();
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn mr_mercury(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let blobs = mercury_blobs(ctx, &b);
|
||||
let influence = pr.f("blobInfluence") as f32 / 100.0;
|
||||
let s = b.scale as f32;
|
||||
let (w, hh) = (b.img.width, b.img.height);
|
||||
// Field and gradient accumulate additively: [f, gx, gy, _].
|
||||
let mut field = Image::new(w, hh);
|
||||
|
|
|
|||
|
|
@ -80,31 +80,78 @@ impl Cam {
|
|||
}
|
||||
|
||||
/// A planar textured piece ready to draw.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct Piece {
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Piece {
|
||||
/// Screen (buffer px, homogeneous) → piece-local (a, b, w).
|
||||
inv: M3,
|
||||
pub inv: [[f64; 3]; 3],
|
||||
/// Rest centre of the piece in texture pixels.
|
||||
c0: [f32; 2],
|
||||
pub c0: [f32; 2],
|
||||
/// Convex polygon (texture pixels), counter-clockwise in y-down space.
|
||||
poly: [[f32; 2]; 6],
|
||||
n: u8,
|
||||
bbox: [f32; 4],
|
||||
shade: f32,
|
||||
pub poly: [[f32; 2]; 6],
|
||||
pub n: u8,
|
||||
pub bbox: [f32; 4],
|
||||
pub shade: f32,
|
||||
/// Lighting: colour multiplier and additive specular highlight.
|
||||
tint: [f32; 3],
|
||||
spec: [f32; 3],
|
||||
pub(crate) alpha: f32,
|
||||
pub tint: [f32; 3],
|
||||
pub spec: [f32; 3],
|
||||
pub alpha: f32,
|
||||
/// Flat straight colour instead of the texture.
|
||||
pub(crate) flat: Option<[f32; 3]>,
|
||||
pub flat: Option<[f32; 3]>,
|
||||
/// Facing away from the camera (draw the back texture).
|
||||
pub(crate) back: bool,
|
||||
pub back: bool,
|
||||
/// Back faces sample their texture mirrored about the piece centre: 0 no, 1 vertically
|
||||
/// (flipped about the X axis), 2 horizontally (about the Y axis).
|
||||
pub(crate) back_mirror: u8,
|
||||
pub back_mirror: u8,
|
||||
depth: f64,
|
||||
}
|
||||
|
||||
/// A piece texture: the layer itself or another image (buffer-aligned).
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PieceTex {
|
||||
Layer,
|
||||
Image(Image),
|
||||
}
|
||||
|
||||
/// What a piece-drawing effect (Card Dance, Shatter, Card Wipe) draws this frame: pieces sorted
|
||||
/// far to near over a transparent frame, textured by `front` (and `back` for pieces facing
|
||||
/// away; none = they take `front`).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PiecePlan {
|
||||
pub pieces: Vec<Piece>,
|
||||
pub front: PieceTex,
|
||||
pub back: Option<PieceTex>,
|
||||
}
|
||||
|
||||
impl PiecePlan {
|
||||
/// Rasterise over a transparent frame of the layer `b`'s size.
|
||||
pub fn finish(&self, mut b: Buf) -> Buf {
|
||||
let mut out = Image::new(b.img.width, b.img.height);
|
||||
let front = match &self.front {
|
||||
PieceTex::Layer => &b.img,
|
||||
PieceTex::Image(i) => i,
|
||||
};
|
||||
let back = self.back.as_ref().map(|t| match t {
|
||||
PieceTex::Layer => &b.img,
|
||||
PieceTex::Image(i) => i,
|
||||
});
|
||||
draw_pieces(&mut out, &self.pieces, front, back);
|
||||
b.img = out;
|
||||
b
|
||||
}
|
||||
}
|
||||
|
||||
/// The piece plan of Card Dance, Shatter (Rendered view) or Card Wipe for layer `b` (its pixels
|
||||
/// may be read: gradient maps and textures default to the layer), `None` for other effects or
|
||||
/// views drawn otherwise.
|
||||
pub fn piece_plan(id: &str, ctx: &EffectCtx, b: &Buf) -> Option<PiecePlan> {
|
||||
match id {
|
||||
"ec.sim.carddance" => Some(card_dance_plan(ctx, b)),
|
||||
"ec.sim.shatter" if ctx.params.e("view") == 0 => shatter_impl(ctx, b, true).err(),
|
||||
"ec.transition.cardwipe" => crate::transition::card_wipe_plan(ctx, b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl Piece {
|
||||
/// Place polygon `poly` (texture px, around rest centre `c0`) with rotation `r` and its
|
||||
/// centre at 3D `pos` (buffer px; z away from the camera), seen through `cam`.
|
||||
|
|
@ -462,22 +509,25 @@ fn card_source(src: u32, g1: [f32; 4], g2: [f32; 4]) -> f32 {
|
|||
}
|
||||
}
|
||||
|
||||
fn card_dance(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn card_dance(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
card_dance_plan(ctx, &b).finish(b)
|
||||
}
|
||||
|
||||
fn card_dance_plan(ctx: &EffectCtx, b: &Buf) -> PiecePlan {
|
||||
let pr = ctx.params;
|
||||
let rows = pr.f("rows").clamp(1.0, 1000.0) as u32;
|
||||
let cols = if pr.e("rowsColumns") == 1 { rows } else { pr.f("columns").clamp(1.0, 1000.0) as u32 };
|
||||
let g1 = layer_or_self(ctx, &b, "gradientLayer1", true, true);
|
||||
let g2 = layer_or_self(ctx, &b, "gradientLayer2", true, true);
|
||||
let back = ctx.layer_param("backLayer", true).map(|o| crate::util::fit_layer(ctx, &b, &o, true));
|
||||
let g1 = layer_or_self(ctx, b, "gradientLayer1", true, true);
|
||||
let g2 = layer_or_self(ctx, b, "gradientLayer2", true, true);
|
||||
let back = ctx.layer_param("backLayer", true).map(|o| crate::util::fit_layer(ctx, b, &o, true));
|
||||
let [lw, lh] = ctx.layer_size;
|
||||
let s = b.scale;
|
||||
let (cw, ch) = (lw / cols as f64, lh / rows as f64);
|
||||
let cam = crate::card3d::projection(ctx, &b);
|
||||
let cam = crate::card3d::projection(ctx, b);
|
||||
let eye = cam.eye();
|
||||
let light = Lighting::from(ctx, &b);
|
||||
let light = Lighting::from(ctx, b);
|
||||
let props: Vec<(u32, f64, f64)> =
|
||||
CD_PROPS.iter().map(|(id, g)| (pr.e(&format!("{g}/{id}Source")), pr.f(&format!("{g}/{id}Multiplier")), pr.f(&format!("{g}/{id}Offset")))).collect();
|
||||
let src = b.img.clone();
|
||||
let mut pieces: Vec<Piece> = (0..rows * cols)
|
||||
.into_par_iter()
|
||||
.filter_map(|i| {
|
||||
|
|
@ -502,10 +552,7 @@ fn card_dance(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
})
|
||||
.collect();
|
||||
sort_far_first(&mut pieces);
|
||||
let mut out = Image::new(src.width, src.height);
|
||||
draw_pieces(&mut out, &pieces, &src, back.as_ref());
|
||||
b.img = out;
|
||||
b
|
||||
PiecePlan { pieces, front: PieceTex::Layer, back: back.map(PieceTex::Image) }
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Shatter
|
||||
|
|
@ -638,7 +685,15 @@ fn map_key(c: [f32; 4]) -> u32 {
|
|||
q(c[0]) << 8 | q(c[1]) << 4 | q(c[2])
|
||||
}
|
||||
|
||||
fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn shatter(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
match shatter_impl(ctx, &b, false) {
|
||||
Ok(img) => Buf { img, ..b },
|
||||
Err(plan) => plan.finish(b),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shatter: the frame (`Ok`), or with `plan_only` in the Rendered view its pieces (`Err`).
|
||||
fn shatter_impl(ctx: &EffectCtx, b: &Buf, plan_only: bool) -> Result<Image, PiecePlan> {
|
||||
let pr = ctx.params;
|
||||
let view = pr.e("view");
|
||||
let render = pr.e("render");
|
||||
|
|
@ -666,21 +721,21 @@ fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let unit = lw * 0.1;
|
||||
let cell = lw / reps;
|
||||
let extrude = pr.f("shape/extrusionDepth").max(0.0) * cell * s;
|
||||
let cam = crate::card3d::projection(ctx, &b);
|
||||
let cam = crate::card3d::projection(ctx, b);
|
||||
let eye = cam.eye();
|
||||
let light = Lighting::from(ctx, &b);
|
||||
let light = Lighting::from(ctx, b);
|
||||
// Gradient: with a gradient layer only pieces at least as bright as 1 − Shatter Threshold
|
||||
// (Invert Gradient: as dark) break.
|
||||
let gradient = ctx.layer_param("gradient/gradientLayer", true).map(|o| crate::util::fit_layer(ctx, &b, &o, true));
|
||||
let gradient = ctx.layer_param("gradient/gradientLayer", true).map(|o| crate::util::fit_layer(ctx, b, &o, true));
|
||||
let threshold = pr.get("gradient/shatterThreshold").map(Value::as_f64).unwrap_or(0.0) / 100.0;
|
||||
let invert_gradient = pr.b("gradient/invertGradient");
|
||||
// Textures.
|
||||
let tex_color = pr.get("textures/color").map(|v| v.as_color()).unwrap_or([1.0; 4]);
|
||||
let tex_opacity = pr.get("textures/opacity").map(Value::as_f64).unwrap_or(1.0).clamp(0.0, 1.0) as f32;
|
||||
let front = Face::from(ctx, &b, "textures/frontMode", "textures/frontLayer", 1);
|
||||
let side = Face::from(ctx, &b, "textures/sideMode", "textures/sideLayer", 0);
|
||||
let backf = Face::from(ctx, &b, "textures/backMode", "textures/backLayer", 1);
|
||||
let custom_map = custom.then(|| layer_or_self(ctx, &b, "shape/customShatterMap", true, true));
|
||||
let front = Face::from(ctx, b, "textures/frontMode", "textures/frontLayer", 1);
|
||||
let side = Face::from(ctx, b, "textures/sideMode", "textures/sideLayer", 0);
|
||||
let backf = Face::from(ctx, b, "textures/backMode", "textures/backLayer", 1);
|
||||
let custom_map = custom.then(|| layer_or_self(ctx, b, "shape/customShatterMap", true, true));
|
||||
let white_fixed = pr.b("shape/whiteTilesFixed");
|
||||
let (sd, cd) = dir.sin_cos();
|
||||
let to_layer = |q: [f64; 2]| [origin[0] + q[0] * cd - q[1] * sd, origin[1] + q[0] * sd + q[1] * cd];
|
||||
|
|
@ -843,12 +898,18 @@ fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
})
|
||||
.collect();
|
||||
sort_far_first(&mut pieces);
|
||||
let mut out = Image::new(src.width, src.height);
|
||||
let front_img = front.img.as_ref().unwrap_or(&src);
|
||||
let back_img = backf.img.as_ref().unwrap_or(front_img);
|
||||
if view == 0 {
|
||||
draw_pieces(&mut out, &pieces, front_img, Some(back_img));
|
||||
} else {
|
||||
let tex = |i: &Option<Image>| i.clone().map(PieceTex::Image);
|
||||
let front_t = tex(&front.img).unwrap_or(PieceTex::Layer);
|
||||
let back_t = tex(&backf.img).unwrap_or_else(|| front_t.clone());
|
||||
let plan = PiecePlan { pieces, front: front_t, back: Some(back_t) };
|
||||
if plan_only {
|
||||
return Err(plan);
|
||||
}
|
||||
return Ok(plan.finish(Buf { img: src, offset: b.offset, scale: b.scale }).img);
|
||||
}
|
||||
let out;
|
||||
{
|
||||
// Wireframes: piece outlines (front view: at rest; otherwise animated), plus forces.
|
||||
let animated = view == 2 || view == 4;
|
||||
let mut lines = Vec::new();
|
||||
|
|
@ -889,8 +950,7 @@ fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
out = splat(src.width, src.height, &lines, Acc::Over);
|
||||
}
|
||||
b.img = out;
|
||||
b
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Caustics
|
||||
|
|
@ -911,35 +971,62 @@ fn sample_repeat(img: &Image, x: f64, y: f64, mode: u32) -> Px {
|
|||
}
|
||||
}
|
||||
|
||||
fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// Everything Caustics' per-pixel pass needs, prepared on the CPU (the other layers it reads
|
||||
/// and the parameter conversions); the layer's own pixels are not read.
|
||||
pub struct CausticsSetup {
|
||||
/// The Bottom layer fitted to the buffer, `None` = the layer itself.
|
||||
pub bottom: Option<Image>,
|
||||
/// Gaussian σ (buffer px) blurring the bottom (transparent edges), 0 = none.
|
||||
pub bottom_sigma: f64,
|
||||
/// The water surface's height field (smoothed luminance), `None` = flat.
|
||||
pub height: Option<Plane>,
|
||||
/// The Sky layer fitted to the buffer.
|
||||
pub sky: Option<Image>,
|
||||
pub scaling: f64,
|
||||
pub repeat: u32,
|
||||
pub wave_h: f64,
|
||||
/// Displacement per unit of height gradient.
|
||||
pub k: f64,
|
||||
/// Layer centre (buffer px).
|
||||
pub cx: f64,
|
||||
pub cy: f64,
|
||||
/// Layer width (buffer px).
|
||||
pub lw: f64,
|
||||
pub surf: [f32; 4],
|
||||
pub surf_op: f32,
|
||||
pub cstr: f32,
|
||||
pub lc: [f32; 4],
|
||||
pub li: f32,
|
||||
/// Light position (buffer px) and height (layer widths); `point` = from the position to
|
||||
/// each pixel, else one direction from above the layer centre.
|
||||
pub lx: f64,
|
||||
pub ly: f64,
|
||||
pub lheight: f64,
|
||||
pub point: bool,
|
||||
pub ambient: f32,
|
||||
pub diffuse: f32,
|
||||
pub specular: f32,
|
||||
pub sharp: f32,
|
||||
pub sky_scaling: f64,
|
||||
pub sky_repeat: u32,
|
||||
pub sky_int: f32,
|
||||
pub convergence: f64,
|
||||
}
|
||||
|
||||
/// Caustics' setup for layer geometry `b` (see [`CausticsSetup`]).
|
||||
pub fn caustics_setup(ctx: &EffectCtx, b: &Buf) -> CausticsSetup {
|
||||
let pr = ctx.params;
|
||||
let s = b.scale;
|
||||
let mut bottom = layer_or_self(ctx, &b, "bottom/bottom", true, pr.e("bottom/bottomSizeDiffers") == 1);
|
||||
let bottom = ctx.layer_param("bottom/bottom", true).map(|o| crate::util::fit_layer(ctx, b, &o, pr.e("bottom/bottomSizeDiffers") == 1));
|
||||
let blur = pr.f("bottom/blur") * s;
|
||||
if blur > 0.05 {
|
||||
bottom = effectcraft_raster::gaussian_blur(&bottom, blur * 0.5, blur * 0.5, false);
|
||||
}
|
||||
let scaling = pr.f("bottom/scaling").max(0.01);
|
||||
let repeat = pr.e("bottom/repeatMode");
|
||||
let wave_h = pr.f("water/waveHeight");
|
||||
let smoothing = pr.f("water/smoothing") * s;
|
||||
let depth = pr.f("water/waterDepth");
|
||||
let ior = pr.f("water/refractiveIndex").max(1.0);
|
||||
let surf = pr.color("water/surfaceColor");
|
||||
let surf_op = pr.f("water/surfaceOpacity") as f32;
|
||||
let cstr = pr.f("water/causticsStrength") as f32;
|
||||
let li = pr.f("lighting/lightIntensity") as f32;
|
||||
let lc = pr.color("lighting/lightColor");
|
||||
let lpos = pr.v2("lighting/lightPosition");
|
||||
let lheight = pr.f("lighting/lightHeight").max(0.01);
|
||||
let ambient = pr.f("lighting/ambientLight") as f32;
|
||||
let diffuse = pr.f("material/diffuse") as f32;
|
||||
let specular = pr.f("material/specular") as f32;
|
||||
let sharp = pr.f("material/highlightSharpness").max(1.0) as f32;
|
||||
let (w, hh) = (b.img.width as usize, b.img.height as usize);
|
||||
// Height field of the water surface (luminance of the chosen layer; none = flat).
|
||||
let height = ctx.layer_param("water/waterSurface", true).map(|o| {
|
||||
let img = crate::util::fit_layer(ctx, &b, &o, true);
|
||||
let img = crate::util::fit_layer(ctx, b, &o, true);
|
||||
let pl = Plane::luma(&img);
|
||||
if smoothing > 0.05 { gauss_plane(&pl, smoothing * 0.5, smoothing * 0.5) } else { pl }
|
||||
});
|
||||
|
|
@ -947,6 +1034,7 @@ fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let k = wave_h * depth * (1.0 - 1.0 / ior) * lw * 2.0;
|
||||
let (cx, cy) = (b.offset[0] + ctx.layer_size[0] * 0.5 * s, b.offset[1] + ctx.layer_size[1] * 0.5 * s);
|
||||
let (lx, ly) = b.to_px(lpos);
|
||||
let (lc, li, lheight) = (pr.color("lighting/lightColor"), pr.f("lighting/lightIntensity") as f32, pr.f("lighting/lightHeight").max(0.01));
|
||||
// Light Type: Distant Source (one direction, from the light position above the layer
|
||||
// centre), Point Source (from the light position to each pixel) or First Comp Light.
|
||||
let light_type = pr.e("lighting/lightType");
|
||||
|
|
@ -962,6 +1050,75 @@ fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
(1, _) => (lc, li, lx, ly, lheight, true),
|
||||
_ => (lc, li, lx, ly, lheight, false),
|
||||
};
|
||||
// Sky: the layer the water surface reflects (Surface Opacity 1 = a mirror of it).
|
||||
let sky = ctx.layer_param("sky/sky", true).map(|o| crate::util::fit_layer(ctx, b, &o, pr.e("sky/skySizeDiffers") == 1));
|
||||
CausticsSetup {
|
||||
bottom,
|
||||
bottom_sigma: if blur > 0.05 { blur * 0.5 } else { 0.0 },
|
||||
height,
|
||||
sky,
|
||||
scaling: pr.f("bottom/scaling").max(0.01),
|
||||
repeat: pr.e("bottom/repeatMode"),
|
||||
wave_h,
|
||||
k,
|
||||
cx,
|
||||
cy,
|
||||
lw,
|
||||
surf: pr.color("water/surfaceColor"),
|
||||
surf_op: pr.f("water/surfaceOpacity") as f32,
|
||||
cstr: pr.f("water/causticsStrength") as f32,
|
||||
lc,
|
||||
li,
|
||||
lx,
|
||||
ly,
|
||||
lheight,
|
||||
point,
|
||||
ambient: pr.f("lighting/ambientLight") as f32,
|
||||
diffuse: pr.f("material/diffuse") as f32,
|
||||
specular: pr.f("material/specular") as f32,
|
||||
sharp: pr.f("material/highlightSharpness").max(1.0) as f32,
|
||||
sky_scaling: pr.get("sky/scaling").map(Value::as_f64).unwrap_or(1.0).max(0.01),
|
||||
sky_repeat: pr.e("sky/repeatMode"),
|
||||
sky_int: pr.get("sky/intensity").map(Value::as_f64).unwrap_or(0.3) as f32,
|
||||
convergence: pr.get("sky/convergence").map(Value::as_f64).unwrap_or(0.5).clamp(0.0, 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let st = caustics_setup(ctx, &b);
|
||||
let mut bottom = st.bottom.clone().unwrap_or_else(|| b.img.clone());
|
||||
if st.bottom_sigma > 0.0 {
|
||||
bottom = effectcraft_raster::gaussian_blur(&bottom, st.bottom_sigma, st.bottom_sigma, false);
|
||||
}
|
||||
let CausticsSetup {
|
||||
scaling,
|
||||
repeat,
|
||||
wave_h,
|
||||
k,
|
||||
cx,
|
||||
cy,
|
||||
lw,
|
||||
surf,
|
||||
surf_op,
|
||||
cstr,
|
||||
lc,
|
||||
li,
|
||||
lx,
|
||||
ly,
|
||||
lheight,
|
||||
point,
|
||||
ambient,
|
||||
diffuse,
|
||||
specular,
|
||||
sharp,
|
||||
sky_scaling,
|
||||
sky_repeat,
|
||||
sky_int,
|
||||
convergence,
|
||||
..
|
||||
} = st;
|
||||
let (height, sky) = (&st.height, &st.sky);
|
||||
let (w, hh) = (b.img.width as usize, b.img.height as usize);
|
||||
let norm3 = |v: [f64; 3]| {
|
||||
let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-12);
|
||||
[(v[0] / n) as f32, (v[1] / n) as f32, (v[2] / n) as f32]
|
||||
|
|
@ -973,14 +1130,8 @@ fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
norm3([(lx - cx) / lw.max(1.0), (ly - cy) / lw.max(1.0), lheight])
|
||||
}
|
||||
};
|
||||
// Sky: the layer the water surface reflects (Surface Opacity 1 = a mirror of it).
|
||||
let sky = ctx.layer_param("sky/sky", true).map(|o| crate::util::fit_layer(ctx, &b, &o, pr.e("sky/skySizeDiffers") == 1));
|
||||
let sky_scaling = pr.get("sky/scaling").map(Value::as_f64).unwrap_or(1.0).max(0.01);
|
||||
let sky_repeat = pr.e("sky/repeatMode");
|
||||
let sky_int = pr.get("sky/intensity").map(Value::as_f64).unwrap_or(0.3) as f32;
|
||||
let convergence = pr.get("sky/convergence").map(Value::as_f64).unwrap_or(0.5).clamp(0.0, 1.0);
|
||||
let grad = |x: usize, y: usize| -> (f64, f64) {
|
||||
match &height {
|
||||
match height {
|
||||
Some(pl) => {
|
||||
let gx = (pl.get_clamped(x as i64 + 1, y as i64) - pl.get_clamped(x as i64 - 1, y as i64)) as f64 * 0.5;
|
||||
let gy = (pl.get_clamped(x as i64, y as i64 + 1) - pl.get_clamped(x as i64, y as i64 - 1)) as f64 * 0.5;
|
||||
|
|
@ -1052,12 +1203,13 @@ fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
|
||||
// ------------------------------------------------------------------ Wave World
|
||||
|
||||
/// Wave World's simulated surface: an `nx` × `ny` grid of heights `u` and velocities `v`.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Waves {
|
||||
nx: usize,
|
||||
ny: usize,
|
||||
u: Vec<f32>,
|
||||
v: Vec<f32>,
|
||||
pub struct Waves {
|
||||
pub nx: usize,
|
||||
pub ny: usize,
|
||||
pub u: Vec<f32>,
|
||||
pub v: Vec<f32>,
|
||||
}
|
||||
|
||||
struct Producer {
|
||||
|
|
@ -1091,7 +1243,27 @@ impl Producer {
|
|||
|
||||
static WAVE_CACHE: SimCache<Waves> = SimCache::new(4);
|
||||
|
||||
fn wave_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// What Wave World draws: the Height Map view of the simulated surface (grid heights and,
|
||||
/// with a ground layer, water depths per grid point), or the wireframe preview's lines.
|
||||
pub enum WavePlan {
|
||||
Height { st: std::sync::Arc<Waves>, depth: Option<Vec<f32>> },
|
||||
Wire(crate::sim::SpritePlan),
|
||||
}
|
||||
|
||||
fn wave_world(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
match wave_world_impl(ctx, &b, false) {
|
||||
Ok(img) => Buf { img, ..b },
|
||||
Err(WavePlan::Wire(plan)) => plan.finish(b),
|
||||
Err(WavePlan::Height { .. }) => b,
|
||||
}
|
||||
}
|
||||
|
||||
/// Wave World's simulation state for the frame, as a [`WavePlan`] (its pixels are not read).
|
||||
pub fn wave_world_plan(ctx: &EffectCtx, b: &Buf) -> Option<WavePlan> {
|
||||
wave_world_impl(ctx, b, true).err()
|
||||
}
|
||||
|
||||
fn wave_world_impl(ctx: &EffectCtx, b: &Buf, plan_only: bool) -> Result<Image, WavePlan> {
|
||||
let pr = ctx.params;
|
||||
let [lw, lh] = ctx.layer_size;
|
||||
let res = pr.f("simulation/gridResolution").clamp(1.0, 400.0) as usize;
|
||||
|
|
@ -1239,7 +1411,11 @@ fn wave_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let c = g(x0, y1) + (g(x1, y1) - g(x0, y1)) * tx;
|
||||
a + (c - a) * ty
|
||||
};
|
||||
if plan_only && pr.e("view") == 1 {
|
||||
return Err(WavePlan::Height { st, depth });
|
||||
}
|
||||
if pr.e("view") == 1 {
|
||||
let mut img = Image::new(bw, bh);
|
||||
let bright = pr.f("heightMapControls/brightness") as f32;
|
||||
let contrast = pr.f("heightMapControls/contrast") as f32;
|
||||
let gamma = pr.f("heightMapControls/gamma").max(0.01) as f32;
|
||||
|
|
@ -1251,7 +1427,7 @@ fn wave_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let gy = (y / lw.max(1.0) * (st.nx - 1) as f64).round().clamp(0.0, (st.ny - 1) as f64) as usize;
|
||||
d[gy * st.nx + gx] <= 0.0
|
||||
};
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let lx = (x as f64 + 0.5 - b.offset[0]) / s;
|
||||
let ly = (y as f64 + 0.5 - b.offset[1]) / s;
|
||||
|
|
@ -1260,6 +1436,7 @@ fn wave_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
*px = [v * al, v * al, v * al, al];
|
||||
}
|
||||
});
|
||||
Ok(img)
|
||||
} else {
|
||||
// Wireframe preview: the grid turned by Horizontal Rotation about the vertical axis,
|
||||
// tilted by Vertical Rotation, heights scaled by Vertical Scale.
|
||||
|
|
@ -1291,10 +1468,12 @@ fn wave_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
}
|
||||
}
|
||||
let wire = splat(bw, bh, &lines, Acc::Over);
|
||||
b.img = crate::sim::combine(&Image::filled(bw, bh, [0.0, 0.0, 0.0, 1.0]), &wire, 0);
|
||||
let plan = crate::sim::SpritePlan { sprites: lines, tints: vec![], acc: Acc::Over, post: crate::sim::Post::OverBlack };
|
||||
if plan_only {
|
||||
return Err(WavePlan::Wire(plan));
|
||||
}
|
||||
Ok(plan.finish(Buf { img: Image::new(bw, bh), offset: b.offset, scale: b.scale }).img)
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Foam
|
||||
|
|
@ -1336,7 +1515,23 @@ fn foam_pop_age(id: u32, lifespan: f32, frailty: f32, seed: u32) -> f32 {
|
|||
/// other, wobble, and pop at the end of their (Strength-shortened) lifespan, pushing their
|
||||
/// neighbours away at Pop Velocity. Rendered bubbles use a built-in texture or the Bubble
|
||||
/// Texture Layer, oriented by Bubble Orientation, and reflect the Environment Map.
|
||||
fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn foam(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
match foam_impl(ctx, &b, false) {
|
||||
Ok(img) => Buf { img, ..b },
|
||||
Err(plan) => plan.finish(b),
|
||||
}
|
||||
}
|
||||
|
||||
/// Foam's bubbles as sprites, `None` when it draws more than sprites (a User Defined bubble
|
||||
/// texture, an Environment Map reflection or the Draft + Flow Map view's flow map).
|
||||
pub(crate) fn foam_plan(ctx: &EffectCtx, b: &Buf) -> Option<crate::sim::SpritePlan> {
|
||||
if !crate::catalog::gpu_supported("ec.sim.foam", ctx) {
|
||||
return None;
|
||||
}
|
||||
foam_impl(ctx, b, true).err()
|
||||
}
|
||||
|
||||
fn foam_impl(ctx: &EffectCtx, b: &Buf, plan_only: bool) -> Result<Image, crate::sim::SpritePlan> {
|
||||
let pr = ctx.params;
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let unit = lw * 0.01;
|
||||
|
|
@ -1558,7 +1753,11 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
sp
|
||||
})
|
||||
.collect();
|
||||
let mut out = splat(b.img.width, b.img.height, &sprites, if blend == 0 && view == 2 { Acc::Add } else { Acc::Over });
|
||||
let acc = if blend == 0 && view == 2 { Acc::Add } else { Acc::Over };
|
||||
if plan_only {
|
||||
return Err(crate::sim::SpritePlan { sprites, tints: vec![], acc, post: crate::sim::Post::Replace });
|
||||
}
|
||||
let mut out = splat(b.img.width, b.img.height, &sprites, acc);
|
||||
// User Defined: the texture layer fills each bubble's disc.
|
||||
if let Some(tex) = &user_tex {
|
||||
for q in &order {
|
||||
|
|
@ -1581,7 +1780,7 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
&& strength > 0.0
|
||||
&& let Some(env) = ctx.layer_param("rendering/environmentMap", true)
|
||||
{
|
||||
let env_img = crate::util::fit_layer(ctx, &b, &env, true);
|
||||
let env_img = crate::util::fit_layer(ctx, b, &env, true);
|
||||
let conv = pr.f("rendering/reflectionConvergence").clamp(0.0, 1.0);
|
||||
let (w, h) = (b.img.width as f64, b.img.height as f64);
|
||||
for q in &order {
|
||||
|
|
@ -1602,7 +1801,7 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
if view == 1
|
||||
&& let Some(lp) = &flow
|
||||
{
|
||||
let fm = crate::util::fit_layer(ctx, &b, lp, true);
|
||||
let fm = crate::util::fit_layer(ctx, b, lp, true);
|
||||
out.data.par_iter_mut().zip(fm.data.par_iter()).for_each(|(o, f)| {
|
||||
let k = 1.0 - o[3];
|
||||
for c in 0..4 {
|
||||
|
|
@ -1610,8 +1809,7 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
});
|
||||
}
|
||||
b.img = out;
|
||||
b
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Add `f(u, v)` (premultiplied; u, v ∈ −1..1 across the disc) over the disc of radius `r` at
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ use effectcraft_project::ParamUi;
|
|||
use effectcraft_raster::{Image, Px};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::sim::{Acc, SPS, Shape, Sprite, combine, splat, steps_at};
|
||||
use crate::sim::{Acc, Post, SPS, Shape, Sprite, SpritePlan, Tint, combine, splat, steps_at};
|
||||
use crate::util::{SimCache, hash1, params_key, point_in_poly, unpremul};
|
||||
use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider};
|
||||
|
||||
|
|
@ -897,12 +897,20 @@ fn particle_playground(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
|
||||
// ---------------------------------------------------------------- CC Hair
|
||||
|
||||
fn cc_hair(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn cc_hair(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
let Some(mut plan) = hair_plan(ctx, &b) else { return b };
|
||||
plan.resolve(&b.img);
|
||||
plan.finish(b)
|
||||
}
|
||||
|
||||
/// CC Hair's strands as line sprites (each coloured from its root, see [`Tint::Root`]);
|
||||
/// `None` = no hair (the layer passes through).
|
||||
pub(crate) fn hair_plan(ctx: &EffectCtx, b: &Buf) -> Option<SpritePlan> {
|
||||
let pr = ctx.params;
|
||||
let len = pr.f("length").max(0.0) as f32;
|
||||
let density = pr.f("density").max(0.0) as f32 / 100.0;
|
||||
if len <= 0.0 || density <= 0.0 {
|
||||
return b;
|
||||
return None;
|
||||
}
|
||||
let thick = pr.f("thickness").max(0.05) as f32;
|
||||
let weight = pr.f("weight") as f32;
|
||||
|
|
@ -924,19 +932,13 @@ fn cc_hair(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
// Roots: a jittered grid over the layer, density per 1 000 px².
|
||||
let count = ((lw * lh / 1000.0) * density * 10.0).min(200_000.0) as u32;
|
||||
let src = b.img.clone();
|
||||
let segs = 6u32;
|
||||
let strands: Vec<Vec<Sprite>> = (0..count)
|
||||
let strands: Vec<Vec<(Sprite, Tint)>> = (0..count)
|
||||
.into_par_iter()
|
||||
.filter_map(|i| {
|
||||
.map(|i| {
|
||||
let rx = hash1(i, 1, seed) * lw;
|
||||
let ry = hash1(i, 2, seed) * lh;
|
||||
let (bx, by) = b.to_px([rx as f64, ry as f64]);
|
||||
let root = src.sample_bilinear(bx, by);
|
||||
if root[3] < 0.5 {
|
||||
return None;
|
||||
}
|
||||
let (rc, _) = unpremul(root);
|
||||
let mut l = len * (0.75 + 0.5 * hash1(i, 3, seed));
|
||||
// Lean: random, plus the map's local gradient (hairfall map).
|
||||
let mut lean = [(hash1(i, 4, seed) * 2.0 - 1.0) * 0.6, -1.0 + hash1(i, 5, seed) * 0.4];
|
||||
|
|
@ -957,7 +959,6 @@ fn cc_hair(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let dir = [lean[0] / ll, lean[1] / ll];
|
||||
// Droop: heavier (or thinner with Constant Mass off) hair bends more.
|
||||
let droop = weight * if const_mass { 1.0 } else { 1.0 / thick.max(0.2) } * l;
|
||||
let base = [rc[0] + (hc[0] - rc[0]) * (1.0 - inherit), rc[1] + (hc[1] - rc[1]) * (1.0 - inherit), rc[2] + (hc[2] - rc[2]) * (1.0 - inherit)];
|
||||
let pt = |s: f32| [rx + dir[0] * l * s, ry + dir[1] * l * s + droop * s * s];
|
||||
let mut out = Vec::with_capacity(segs as usize);
|
||||
for k in 0..segs {
|
||||
|
|
@ -974,24 +975,23 @@ fn cc_hair(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let spec = (1.0 - tl.abs()).powf(1.0 / rough) * spe;
|
||||
let shade = (amb + dif * diff * light_i) * bright;
|
||||
let tip = 1.0 - 0.6 * s1;
|
||||
let c3 = [base[0] * shade + spec * light_i, base[1] * shade + spec * light_i, base[2] * shade + spec * light_i];
|
||||
let (pa, pc) = (b.to_px([a[0] as f64, a[1] as f64]), b.to_px([c[0] as f64, c[1] as f64]));
|
||||
out.push(Sprite {
|
||||
let sp = Sprite {
|
||||
x: pa.0 as f32,
|
||||
y: pa.1 as f32,
|
||||
r: thick * 0.5 * b.scale as f32 * tip,
|
||||
c: [c3[0], c3[1], c3[2], opacity],
|
||||
c: [0.0, 0.0, 0.0, opacity],
|
||||
shape: Shape::Line { dx: (pc.0 - pa.0) as f32, dy: (pc.1 - pa.1) as f32 },
|
||||
rot: 0.0,
|
||||
});
|
||||
};
|
||||
// The root's colour mixed with the hair colour (no strand where it is clear).
|
||||
out.push((sp, Tint::Root { x: bx, y: by, hair: [hc[0], hc[1], hc[2]], inherit, shade, spec: spec * light_i }));
|
||||
}
|
||||
Some(out)
|
||||
out
|
||||
})
|
||||
.collect();
|
||||
let sprites: Vec<Sprite> = strands.into_iter().flatten().collect();
|
||||
let fx = splat(b.img.width, b.img.height, &sprites, Acc::Over);
|
||||
b.img = combine(&b.img, &fx, 0);
|
||||
b
|
||||
let (sprites, tints) = strands.into_iter().flatten().unzip();
|
||||
Some(SpritePlan { sprites, tints, acc: Acc::Over, post: Post::Combine(0) })
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use effectcraft_project::ParamUi;
|
|||
use effectcraft_raster::Px;
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::sim2::{Piece, draw_pieces, rot_axis, sort_far_first};
|
||||
use crate::sim2::{Piece, PiecePlan, PieceTex, rot_axis, sort_far_first};
|
||||
use crate::util::{Plane, gauss_plane, hash1, layer_rect, remap, unpremul};
|
||||
use crate::{Buf, EffectCtx, EffectSpec, ParamSpec, col, num, p, popup, slider};
|
||||
|
||||
|
|
@ -175,7 +175,15 @@ fn iris_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
/// through a 180° turn about Flip Axis, as textured planes in 3D seen through the Camera
|
||||
/// System and lit by Lighting / Material. The flipped side shows Back Layer (none: the cards
|
||||
/// vanish as they turn away). Position / Rotation Jitter wobble the cards over time.
|
||||
fn card_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn card_wipe(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
match card_wipe_plan(ctx, &b) {
|
||||
Some(plan) => plan.finish(b),
|
||||
None => b,
|
||||
}
|
||||
}
|
||||
|
||||
/// Card Wipe's cards (`None` = nothing has moved: the layer as is).
|
||||
pub(crate) fn card_wipe_plan(ctx: &EffectCtx, b: &Buf) -> Option<PiecePlan> {
|
||||
let done = completion(ctx);
|
||||
let pr = ctx.params;
|
||||
let f = |id: &str| pr.get(id).map(Value::as_f64).unwrap_or(0.0);
|
||||
|
|
@ -199,7 +207,7 @@ fn card_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
// Nothing has moved: the layer as is (exactly).
|
||||
let xy = pr.get("cameraPosition/xyPosition").map(|v| v.as_vec2());
|
||||
if xy.is_none_or(|p| (p[0] - ctx.layer_size[0] * 0.5).abs() < 1e-9 && (p[1] - ctx.layer_size[1] * 0.5).abs() < 1e-9) {
|
||||
return b;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let tw = (pr.f("transitionWidth") / 100.0).clamp(0.01, 1.0);
|
||||
|
|
@ -212,25 +220,25 @@ fn card_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let order = pr.e("flipOrder");
|
||||
let jitter = pr.f("timingRandomness").clamp(0.0, 1.0);
|
||||
let seed = pr.f("randomSeed") as u32 ^ 0xca7d;
|
||||
let (x0, y0, w, h) = layer_rect(ctx, &b);
|
||||
let (x0, y0, w, h) = layer_rect(ctx, b);
|
||||
let (cw, ch) = (w / cols as f64, h / rows as f64);
|
||||
let nx = |i: i64| if cols > 1 { i as f64 / (cols - 1) as f64 } else { 0.0 };
|
||||
let ny = |j: i64| if rows > 1 { j as f64 / (rows - 1) as f64 } else { 0.0 };
|
||||
// Flip Order "Gradient": cards flip first where the gradient layer (default: this layer) is dark.
|
||||
let grad = (order == 8).then(|| match ctx.layer_param("gradientLayer", false) {
|
||||
Some(o) => place_layer(ctx, &b, &o, 2),
|
||||
Some(o) => place_layer(ctx, b, &o, 2),
|
||||
None => b.img.clone(),
|
||||
});
|
||||
// Back Layer: a chosen layer (stretched to fit), or this layer for projects saved with the
|
||||
// old "Self" choice; none hides the cards' backs.
|
||||
let back = match ctx.layer_param("backLayer", true) {
|
||||
Some(o) => Some(crate::util::fit_layer(ctx, &b, &o, true)),
|
||||
None if pr.b("backSelf") => Some(b.img.clone()),
|
||||
Some(o) => Some(PieceTex::Image(crate::util::fit_layer(ctx, b, &o, true))),
|
||||
None if pr.b("backSelf") => Some(PieceTex::Layer),
|
||||
None => None,
|
||||
};
|
||||
let cam = crate::card3d::projection(ctx, &b);
|
||||
let cam = crate::card3d::projection(ctx, b);
|
||||
let eye = cam.eye();
|
||||
let light = crate::card3d::Lighting::from(ctx, &b);
|
||||
let light = crate::card3d::Lighting::from(ctx, b);
|
||||
let t = ctx.time;
|
||||
let jit = |i: i64, j: i64, k: u32, speed: f64| -> f64 {
|
||||
let ph = hash1(i as u32, j as u32, seed ^ k) as f64 * std::f64::consts::TAU;
|
||||
|
|
@ -303,10 +311,7 @@ fn card_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
})
|
||||
.collect();
|
||||
sort_far_first(&mut pieces);
|
||||
let mut out = effectcraft_raster::Image::new(b.img.width, b.img.height);
|
||||
draw_pieces(&mut out, &pieces, &b.img, back.as_ref());
|
||||
b.img = out;
|
||||
b
|
||||
Some(PiecePlan { pieces, front: PieceTex::Layer, back })
|
||||
}
|
||||
|
||||
fn grid_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
|
|
|
|||
|
|
@ -326,7 +326,7 @@ impl Lut {
|
|||
type LutCache = Mutex<HashMap<String, Option<Arc<Lut>>>>;
|
||||
|
||||
/// Load (and cache) a LUT from inline `.cube` text or a file path.
|
||||
pub(crate) fn load_lut(src: &str) -> Option<Arc<Lut>> {
|
||||
pub fn load_lut(src: &str) -> Option<Arc<Lut>> {
|
||||
if src.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -316,6 +316,10 @@ impl GpuContext {
|
|||
include_str!("shaders/fx_tone.wgsl"),
|
||||
include_str!("shaders/fx_warp.wgsl"),
|
||||
include_str!("shaders/fx_extra.wgsl"),
|
||||
include_str!("shaders/fx_depth.wgsl"),
|
||||
include_str!("shaders/fx_lut.wgsl"),
|
||||
include_str!("shaders/fx_sim.wgsl"),
|
||||
include_str!("shaders/fx_vr.wgsl"),
|
||||
include_str!("shaders/fx_light.wgsl"),
|
||||
include_str!("shaders/fx_transition.wgsl"),
|
||||
include_str!("shaders/fx_text.wgsl"),
|
||||
|
|
@ -380,6 +384,10 @@ impl GpuContext {
|
|||
.chain(crate::fx_tone::KERNELS)
|
||||
.chain(crate::fx_warp::KERNELS)
|
||||
.chain(crate::fx_extra::KERNELS)
|
||||
.chain(crate::fx_depth::KERNELS)
|
||||
.chain(crate::fx_lut::KERNELS)
|
||||
.chain(crate::fx_sim::KERNELS)
|
||||
.chain(crate::fx_vr::KERNELS)
|
||||
.chain(crate::fx_light::KERNELS)
|
||||
.chain(crate::fx_transition::KERNELS)
|
||||
.chain(crate::fx_text::KERNELS)
|
||||
|
|
|
|||
|
|
@ -167,6 +167,10 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
_ if crate::fx_tone::IDS.contains(&id) => crate::fx_tone::apply(e, id, ctx, b),
|
||||
_ if crate::fx_warp::IDS.contains(&id) => crate::fx_warp::apply(e, id, ctx, b),
|
||||
_ if crate::fx_extra::IDS.contains(&id) => crate::fx_extra::apply(e, id, ctx, b),
|
||||
_ if crate::fx_depth::IDS.contains(&id) => crate::fx_depth::apply(e, id, ctx, b),
|
||||
_ if crate::fx_lut::IDS.contains(&id) => crate::fx_lut::apply(e, id, ctx, b),
|
||||
_ if crate::fx_sim::IDS.contains(&id) => crate::fx_sim::apply(e, id, ctx, b),
|
||||
_ if crate::fx_vr::IDS.contains(&id) => crate::fx_vr::apply(e, id, ctx, b),
|
||||
_ if crate::fx_light::IDS.contains(&id) => crate::fx_light::apply(e, id, ctx, b),
|
||||
_ if crate::fx_transition::IDS.contains(&id) => crate::fx_transition::apply(e, id, ctx, b),
|
||||
_ if crate::fx_text::IDS.contains(&id) => crate::fx_text::apply(e, id, ctx, b),
|
||||
|
|
|
|||
|
|
@ -197,6 +197,17 @@ fn lumetri(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
(lw / lh.max(1e-6)) as f32,
|
||||
]);
|
||||
debug_assert_eq!(d.len(), 50);
|
||||
// Input LUT and Look (data[50..53]: look, input LUT offset, look LUT offset; tables after).
|
||||
let look = pr.e("creative/look");
|
||||
let input_lut = if pr.e("basicCorrection/inputLut") == 1 { effectcraft_effects::load_lut(pr.s("basicCorrection/inputLutFile")) } else { None };
|
||||
let look_lut = if look == 1 { effectcraft_effects::load_lut(pr.s("creative/lookFile")) } else { None };
|
||||
d.extend([look as f32, -1.0, -1.0]);
|
||||
if let Some(l) = &input_lut {
|
||||
d[51] = crate::fx_lut::push_lut(&mut d, l);
|
||||
}
|
||||
if let Some(l) = &look_lut {
|
||||
d[52] = crate::fx_lut::push_lut(&mut d, l);
|
||||
}
|
||||
let buf = e.data(&d);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxc_lumetri", &Params::default(), &b.img, None, &out, Some(&buf));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,335 @@
|
|||
//! GPU effects, 3D Channel family (kernels in `shaders/fx_depth.wgsl`, every entry point
|
||||
//! prefixed `fxd_`): 3D Channel Extract, Cryptomatte, Depth Matte, Depth of Field, EXtractoR,
|
||||
//! Fog 3D, ID Matte and IDentifier.
|
||||
//!
|
||||
//! The layer's auxiliary channels ([`effectcraft_raster::AuxChannels`]) go to the GPU as an
|
||||
//! extra texture at the aux resolution: the planes an effect reads are packed into its four
|
||||
//! channels (alpha = 1 marks "inside the aux image"). `fxd_lookup` resamples it nearest-neighbour
|
||||
//! onto the buffer grid with the CPU's exact index arithmetic (layer position × aux scale,
|
||||
//! floored), and the effect kernels work on that buffer-sized plane. Cryptomatte packs a
|
||||
//! per-aux-pixel reduction of its rank layers (selection coverage and the coverage-weighted ID
|
||||
//! colours, measured on the CPU) instead of the raw ranks. Fog 3D's Gradient Layer is rendered
|
||||
//! by the host on the CPU (it is another layer) and uploaded.
|
||||
|
||||
use effectcraft_effects::{Buf, EffectCtx};
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_raster::channels3d::BACKGROUND_DEPTH;
|
||||
use effectcraft_raster::{AuxChannels, Image};
|
||||
|
||||
use crate::context::{Enc, GpuImage, Params};
|
||||
use crate::effects::{GBuf, gaussian_blur};
|
||||
|
||||
/// Compute entry points in `fx_depth.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &["fxd_lookup", "fxd_point", "fxd_dof_f", "fxd_dof_level", "fxd_avg"];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] = &[
|
||||
"ec.3d.channelextract",
|
||||
"ec.3d.cryptomatte",
|
||||
"ec.3d.depthmatte",
|
||||
"ec.3d.depthoffield",
|
||||
"ec.3d.extractor",
|
||||
"ec.3d.fog3d",
|
||||
"ec.3d.idmatte",
|
||||
"ec.3d.identifier",
|
||||
];
|
||||
|
||||
/// 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> {
|
||||
let aux = ctx.env.host.and_then(|h| h.aux());
|
||||
if id == "ec.3d.extractor" {
|
||||
return extractor(e, ctx, b, aux.as_deref());
|
||||
}
|
||||
let Some(a) = aux else { return Some(b) };
|
||||
match id {
|
||||
"ec.3d.channelextract" => channel_extract(e, ctx, b, &a),
|
||||
"ec.3d.cryptomatte" => cryptomatte(e, ctx, b, &a),
|
||||
"ec.3d.depthmatte" => depth_matte(e, ctx, b, &a),
|
||||
"ec.3d.depthoffield" => depth_of_field(e, ctx, b, &a),
|
||||
"ec.3d.fog3d" => fog(e, ctx, b, &a),
|
||||
"ec.3d.idmatte" => id_matte(e, ctx, b, &a),
|
||||
"ec.3d.identifier" => identifier(e, ctx, b, &a),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn op(code: u32) -> Params {
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = code;
|
||||
p
|
||||
}
|
||||
|
||||
/// Pack up to four aux planes (aux resolution; `None` planes are 0, alpha defaults to 1 = inside)
|
||||
/// and upload them.
|
||||
fn aux_texture(e: &mut Enc, a: &AuxChannels, planes: [Option<&[f32]>; 4]) -> Option<GpuImage> {
|
||||
let n = (a.width * a.height) as usize;
|
||||
if planes.iter().flatten().any(|p| p.len() < n) {
|
||||
return None;
|
||||
}
|
||||
let mut img = Image::new(a.width, a.height);
|
||||
for (i, px) in img.data.iter_mut().enumerate() {
|
||||
*px = [0, 1, 2, 3].map(|k| planes[k].map_or(if k == 3 { 1.0 } else { 0.0 }, |p| p[i]));
|
||||
}
|
||||
e.g.upload_image(&img)
|
||||
}
|
||||
|
||||
/// Resample an aux texture onto the buffer grid (nearest; `fill` outside the aux image),
|
||||
/// sampling each buffer pixel at sub-pixel position `at`.
|
||||
fn lookup(e: &mut Enc, b: &GBuf, a: &AuxChannels, tex: &GpuImage, at: [f64; 2], fill: [f32; 4]) -> GpuImage {
|
||||
let inv = 1.0 / b.scale.max(1e-9);
|
||||
let mut p = Params::default();
|
||||
p.f[0] = [b.offset[0] as f32, b.offset[1] as f32, inv as f32, a.scale as f32];
|
||||
p.f[1] = [at[0] as f32, at[1] as f32, 0.0, 0.0];
|
||||
p.f[2] = fill;
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxd_lookup", &p, tex, None, &out, None);
|
||||
out
|
||||
}
|
||||
|
||||
fn point(e: &mut Enc, p: &Params, b: GBuf, plane: &GpuImage) -> Option<GBuf> {
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxd_point", p, &b.img, Some(plane), &out, None);
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 3D Channel Extract
|
||||
|
||||
fn channel_extract(e: &mut Enc, ctx: &EffectCtx, b: GBuf, a: &AuxChannels) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let get3 = |n: [&[&str]; 3]| -> Option<[&[f32]; 3]> { Some([a.find(n[0])?, a.find(n[1])?, a.find(n[2])?]) };
|
||||
let u = ["UV.U", "U", "uv.x"];
|
||||
let v = ["UV.V", "V", "uv.y"];
|
||||
// (planes, mode): mode 0 grey (lv), 1 UV (lv on R, G; B = 0), 2 normals (lv of ×0.5 + 0.5),
|
||||
// 3 RGB (lv), 4 unclamped RGB (no mapping).
|
||||
let (planes, mode): ([&[f32]; 3], u32) = match pr.e("channel") {
|
||||
0 => (a.depth().map(|z| [z, z, z])?, 0),
|
||||
1 => (a.object_id().map(|c| [c, c, c])?, 0),
|
||||
7 => (a.material_id().map(|c| [c, c, c])?, 0),
|
||||
4 => (a.coverage().map(|c| [c, c, c])?, 0),
|
||||
2 => (get3([&u, &v, &["UV.W", "W", "uv.z"]]).or_else(|| Some([a.find(&u)?, a.find(&v)?, a.find(&v)?]))?, 1),
|
||||
3 => (get3([&["N.X", "Normal.X", "normals.x"], &["N.Y", "Normal.Y", "normals.y"], &["N.Z", "Normal.Z", "normals.z"]])?, 2),
|
||||
5 => (get3([&["BG.R", "Background.R"], &["BG.G", "Background.G"], &["BG.B", "Background.B"]])?, 3),
|
||||
_ => (get3([&["R"], &["G"], &["B"]])?, 4),
|
||||
};
|
||||
let tex = aux_texture(e, a, [Some(planes[0]), Some(planes[1]), Some(planes[2]), None])?;
|
||||
let mut p = op(0);
|
||||
p.u[0] = [0, mode, pr.b("clampOutput") as u32, pr.b("invertDepthMap") as u32];
|
||||
p.f[0] = [pr.f("blackPoint") as f32, pr.f("whitePoint") as f32, 0.0, 0.0];
|
||||
if !pr.b("antialias") {
|
||||
let plane = lookup(e, &b, a, &tex, [0.5, 0.5], [0.0; 4]);
|
||||
return point(e, &p, b, &plane);
|
||||
}
|
||||
// Anti-alias: four sub-pixel samples, summed in order and scaled once (as the CPU does).
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let mut acc: Option<GpuImage> = None;
|
||||
for s in [[0.25, 0.25], [0.75, 0.25], [0.25, 0.75], [0.75, 0.75]] {
|
||||
let plane = lookup(e, &b, a, &tex, s, [0.0; 4]);
|
||||
let one = e.scratch(w, h);
|
||||
e.pixels("fxd_point", &p, &b.img, Some(&plane), &one, None);
|
||||
acc = Some(match acc {
|
||||
None => one,
|
||||
Some(prev) => {
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fxd_avg", &op(0), &prev, Some(&one), &out, None);
|
||||
out
|
||||
}
|
||||
});
|
||||
}
|
||||
let acc = acc?;
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fxd_avg", &op(1), &acc, None, &out, None);
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Depth Matte / Fog 3D
|
||||
|
||||
fn depth_plane(e: &mut Enc, b: &GBuf, a: &AuxChannels) -> Option<GpuImage> {
|
||||
let z = a.depth()?;
|
||||
let tex = aux_texture(e, a, [Some(z), None, None, None])?;
|
||||
Some(lookup(e, b, a, &tex, [0.5, 0.5], [BACKGROUND_DEPTH, 0.0, 0.0, 0.0]))
|
||||
}
|
||||
|
||||
fn depth_matte(e: &mut Enc, ctx: &EffectCtx, b: GBuf, a: &AuxChannels) -> Option<GBuf> {
|
||||
let Some(plane) = depth_plane(e, &b, a) else { return if a.depth().is_none() { Some(b) } else { None } };
|
||||
let mut p = op(1);
|
||||
p.u[0][1] = ctx.params.b("invert") as u32;
|
||||
p.f[0] = [ctx.params.f("depth") as f32, ctx.params.f("feather").max(0.0) as f32, 0.0, 0.0];
|
||||
point(e, &p, b, &plane)
|
||||
}
|
||||
|
||||
fn fog(e: &mut Enc, ctx: &EffectCtx, b: GBuf, a: &AuxChannels) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let Some(plane) = depth_plane(e, &b, a) else { return if a.depth().is_none() { Some(b) } else { None } };
|
||||
let contrib = (pr.f("layerContribution") / 100.0) as f32;
|
||||
let grad = if contrib > 0.0 && matches!(pr.get("gradientLayer"), Some(Value::Layer(Some(_)))) {
|
||||
ctx.layer_param("gradientLayer", true).map(|o| {
|
||||
let cpu = Buf { img: Image::new(b.img.width, b.img.height), offset: b.offset, scale: b.scale };
|
||||
effectcraft_effects::util::fit_layer(ctx, &cpu, &o, true)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let grad = match grad {
|
||||
Some(g) => Some(e.g.upload_image(&g)?),
|
||||
None => None,
|
||||
};
|
||||
let fc = pr.color("fogColor");
|
||||
let mut p = op(2);
|
||||
p.u[0] = [2, pr.b("foggyBackground") as u32, grad.is_some() as u32, 0];
|
||||
p.f[0] = [pr.f("fogStartDepth") as f32, pr.f("fogEndDepth") as f32, (pr.f("fogOpacity") / 100.0) as f32, (pr.f("scatteringDensity") / 100.0) as f32];
|
||||
p.f[1] = [fc[0], fc[1], fc[2], contrib];
|
||||
p.f[2][0] = BACKGROUND_DEPTH;
|
||||
let Some(g) = grad else { return point(e, &p, b, &plane) };
|
||||
// The gradient's factor joins the depth plane (G) first: kernels read two images.
|
||||
let mut q = op(3);
|
||||
q.f[0][0] = contrib;
|
||||
let merged = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxd_point", &q, &g, Some(&plane), &merged, None);
|
||||
point(e, &p, b, &merged)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Depth of Field
|
||||
|
||||
fn depth_of_field(e: &mut Enc, ctx: &EffectCtx, b: GBuf, a: &AuxChannels) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
if a.depth().is_none() {
|
||||
return Some(b);
|
||||
}
|
||||
let max_r = (pr.f("maximumRadius").max(0.0) * b.scale) as f32;
|
||||
if max_r <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let plane = depth_plane(e, &b, a)?;
|
||||
const LEVELS: usize = 5;
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let stack: Vec<GpuImage> = (0..LEVELS)
|
||||
.map(|k| {
|
||||
let r = max_r * k as f32 / (LEVELS - 1) as f32;
|
||||
if r < 0.3 { b.img.clone() } else { gaussian_blur(e, &b.img, (r / 2.0) as f64, (r / 2.0) as f64, true) }
|
||||
})
|
||||
.collect();
|
||||
// Per-pixel stack position, then each level pair writes its own pixels.
|
||||
let mut p = Params::default();
|
||||
let gamma = 2f32.powf((50.0 - pr.f("focalBias") as f32) / 25.0);
|
||||
p.f[0] = [pr.f("focalPlane") as f32, pr.f("focalPlaneThickness").max(0.0) as f32, max_r, gamma];
|
||||
p.u[0][0] = LEVELS as u32;
|
||||
let fpl = e.scratch(w, h);
|
||||
e.pixels("fxd_dof_f", &p, &plane, None, &fpl, None);
|
||||
let (rows, row_len) = e.image_rows(&fpl);
|
||||
let out = e.scratch(w, h);
|
||||
for k in 0..LEVELS - 1 {
|
||||
let mut q = Params::default();
|
||||
q.u[0] = [k as u32, row_len, LEVELS as u32, 0];
|
||||
e.pixels("fxd_dof_level", &q, &stack[k], Some(&stack[k + 1]), &out, Some(&rows));
|
||||
}
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- ID Matte / IDentifier
|
||||
|
||||
fn id_plane(a: &AuxChannels, which: u32) -> Option<&[f32]> {
|
||||
if which == 1 { a.material_id() } else { a.object_id() }
|
||||
}
|
||||
|
||||
fn id_matte(e: &mut Enc, ctx: &EffectCtx, b: GBuf, a: &AuxChannels) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let Some(ids) = id_plane(a, pr.e("auxChannel")) else { return Some(b) };
|
||||
let cov = if pr.b("useCoverage") { a.coverage() } else { None };
|
||||
let tex = aux_texture(e, a, [Some(ids), cov, None, None])?;
|
||||
let plane = lookup(e, &b, a, &tex, [0.5, 0.5], [0.0; 4]);
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let mut p = op(4);
|
||||
p.u[0][1] = cov.is_some() as u32;
|
||||
p.f[0][0] = pr.f("idSelection").round() as f32;
|
||||
let mut m = e.scratch(w, h);
|
||||
e.pixels("fxd_point", &p, &plane, None, &m, None);
|
||||
let feather = pr.f("feather").max(0.0) * b.scale;
|
||||
if feather > 0.0 {
|
||||
m = gaussian_blur(e, &m, feather * 0.5, feather * 0.5, true);
|
||||
}
|
||||
let mut p = op(5);
|
||||
p.u[0][1] = pr.b("invert") as u32;
|
||||
point(e, &p, b, &m)
|
||||
}
|
||||
|
||||
fn identifier(e: &mut Enc, ctx: &EffectCtx, b: GBuf, a: &AuxChannels) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let Some(ids) = id_plane(a, pr.e("channelType")) else { return Some(b) };
|
||||
let max_id = ids.iter().cloned().fold(1.0f32, f32::max);
|
||||
let tex = aux_texture(e, a, [Some(ids), None, None, None])?;
|
||||
let plane = lookup(e, &b, a, &tex, [0.5, 0.5], [0.0; 4]);
|
||||
let mut p = op(6);
|
||||
p.u[0][1] = pr.e("display");
|
||||
p.f[0] = [pr.f("id").round() as f32, max_id, 0.0, 0.0];
|
||||
point(e, &p, b, &plane)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Cryptomatte
|
||||
|
||||
fn cryptomatte(e: &mut Enc, ctx: &EffectCtx, b: GBuf, a: &AuxChannels) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let layers = a.crypto_layers();
|
||||
if layers.is_empty() {
|
||||
return Some(b);
|
||||
}
|
||||
let want = effectcraft_effects::CRYPTO_LAYERS[(pr.e("layer") as usize).min(2)];
|
||||
let layer = layers.iter().find(|l| l.as_str() == want || l.ends_with(want)).unwrap_or(&layers[0]).clone();
|
||||
let manifest = a.manifests.iter().find(|(l, _)| *l == layer).map(|(_, m)| m.as_slice());
|
||||
let sel = effectcraft_effects::selection_hashes(pr.s("selection"), manifest);
|
||||
// Per aux pixel: coverage-weighted ID colours (RGB) and the selection's coverage (A).
|
||||
let n = (a.width * a.height) as usize;
|
||||
let mut img = Image::new(a.width, a.height);
|
||||
for (i, px) in img.data.iter_mut().enumerate().take(n) {
|
||||
let ranks = a.crypto_ranks(&layer, i);
|
||||
let m: f32 = ranks.iter().filter(|(id, c)| *c > 0.0 && sel.contains(&id.to_bits())).map(|(_, c)| *c).sum::<f32>().clamp(0.0, 1.0);
|
||||
let mut c = [0.0f32; 3];
|
||||
for (id, cov) in &ranks {
|
||||
let k = effectcraft_effects::id_color(*id);
|
||||
for j in 0..3 {
|
||||
c[j] += k[j] * cov;
|
||||
}
|
||||
}
|
||||
*px = [c[0], c[1], c[2], m];
|
||||
}
|
||||
let tex = e.g.upload_image(&img)?;
|
||||
let plane = lookup(e, &b, a, &tex, [0.5, 0.5], [0.0; 4]);
|
||||
let mut p = op(7);
|
||||
p.u[0] = [7, pr.e("display"), pr.b("matteOnly") as u32, 0];
|
||||
point(e, &p, b, &plane)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- EXtractoR
|
||||
|
||||
fn extractor(e: &mut Enc, ctx: &EffectCtx, b: GBuf, a: Option<&AuxChannels>) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let names = [pr.s("red"), pr.s("green"), pr.s("blue"), pr.s("alpha")];
|
||||
let (bp, wp) = (pr.f("blackPoint") as f32, pr.f("whitePoint") as f32);
|
||||
let unmult = pr.b("unMult");
|
||||
let own = |n: &str| match n.trim().to_ascii_uppercase().as_str() {
|
||||
"R" | "RED" => Some(0u32),
|
||||
"G" | "GREEN" => Some(1),
|
||||
"B" | "BLUE" => Some(2),
|
||||
"A" | "ALPHA" => Some(3),
|
||||
_ => None,
|
||||
};
|
||||
let src_ch: Vec<Option<&[f32]>> = names.iter().map(|n| a.and_then(|a| if n.trim().is_empty() { None } else { a.get(n.trim()) })).collect();
|
||||
if src_ch.iter().all(Option::is_none) && bp == 0.0 && wp == 1.0 && !unmult && names.iter().enumerate().all(|(k, n)| own(n) == Some(k as u32)) {
|
||||
return Some(b);
|
||||
}
|
||||
// Per output channel: 0..3 = the layer's own channel, 4 = aux, 5 = default (0, alpha 1).
|
||||
let sources: [u32; 4] = std::array::from_fn(|k| if src_ch[k].is_some() { 4 } else { own(names[k]).unwrap_or(5) });
|
||||
let plane = match a {
|
||||
Some(a) if src_ch.iter().any(Option::is_some) => {
|
||||
let tex = aux_texture(e, a, [src_ch[0], src_ch[1], src_ch[2], src_ch[3]])?;
|
||||
Some(lookup(e, &b, a, &tex, [0.5, 0.5], [0.0; 4]))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let mut p = op(8);
|
||||
p.u[0] = [8, unmult as u32, pr.b("clip") as u32, 0];
|
||||
p.u[1] = sources;
|
||||
p.f[0] = [bp, wp, 0.0, 0.0];
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxd_point", &p, &b.img, plane.as_ref(), &out, None);
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
//! GPU effects, colour management (kernels in `shaders/fx_lut.wgsl`, every entry point
|
||||
//! prefixed `fxl_`): Apply Color LUT, OCIO CDL / Color Space / Display / File / Look Transform
|
||||
//! and Color Profile Converter; Lumetri Color's Input LUT and Look also use the LUT helpers.
|
||||
//!
|
||||
//! Each effect compiles to a colour program ([`effectcraft_effects::color_program`]: transfer
|
||||
//! functions, matrices, ASC CDLs, tone mapping, gamut compression, LUTs) built next to the CPU
|
||||
//! effect, which `fxl_point` interprets per pixel. LUTs (1D tables and 3D lattices, with a
|
||||
//! cineSpace shaper) travel in the kernel's storage buffer and are interpolated in the kernel
|
||||
//! exactly as the CPU does (nearest, trilinear or tetrahedral; inverse 1D tables and the
|
||||
//! iterative 3D inverse): hardware 3D-texture filtering would round the interpolation weights
|
||||
//! and cannot do tetrahedral interpolation. The CPU evaluates transfer functions, CDLs and
|
||||
//! Color Profile Converter's matrix in f64; the kernel in f32 (within the 1e-3 tolerance).
|
||||
//! Custom `.ocio` configurations render on the CPU.
|
||||
|
||||
use effectcraft_effects::{ColorOp, EffectCtx, Lut, Straight, Tf};
|
||||
|
||||
use crate::context::{Enc, Params};
|
||||
use crate::effects::GBuf;
|
||||
|
||||
/// Compute entry points in `fx_lut.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &["fxl_point"];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] = &[
|
||||
"ec.utility.applylut",
|
||||
"ec.color.ociocdl",
|
||||
"ec.color.ociocolorspace",
|
||||
"ec.color.ociodisplay",
|
||||
"ec.color.ociofile",
|
||||
"ec.color.ociolook",
|
||||
"ec.utility.colorprofileconverter",
|
||||
];
|
||||
|
||||
/// Largest LUT the kernels take (floats in the storage buffer); bigger ones render on the CPU.
|
||||
const MAX_FLOATS: usize = 1 << 24;
|
||||
|
||||
/// 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> {
|
||||
let (ops, straight) = effectcraft_effects::color_program(id, ctx)?;
|
||||
if ops.is_empty() {
|
||||
return Some(b);
|
||||
}
|
||||
let data = encode(&ops)?;
|
||||
let buf = e.data(&data);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = (straight == Straight::Divide) as u32;
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxl_point", &p, &b.img, None, &out, Some(&buf));
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
||||
fn tf_code(t: Tf) -> [f32; 2] {
|
||||
match t {
|
||||
Tf::Linear => [0.0, 0.0],
|
||||
Tf::Srgb => [1.0, 0.0],
|
||||
Tf::Gamma(g) => [2.0, g as f32],
|
||||
Tf::AcesCct => [3.0, 0.0],
|
||||
Tf::Pq => [4.0, 0.0],
|
||||
}
|
||||
}
|
||||
|
||||
/// A LUT in the layout `fxl_lut_*` read at the returned offset: n1, n3, domain min (3), domain
|
||||
/// max (3), the 1D table (n1 × RGB), then the lattice (n3³ × RGB, red fastest).
|
||||
pub(crate) fn push_lut(d: &mut Vec<f32>, lut: &Lut) -> f32 {
|
||||
let at = d.len() as f32;
|
||||
d.extend([lut.n1 as f32, lut.n3 as f32]);
|
||||
d.extend(lut.dmin);
|
||||
d.extend(lut.dmax);
|
||||
for v in lut.d1.iter().chain(&lut.d3) {
|
||||
d.extend(v);
|
||||
}
|
||||
at
|
||||
}
|
||||
|
||||
/// Program layout: a stream of (opcode, arguments…) ending in 0, followed by LUT data.
|
||||
fn encode(ops: &[ColorOp]) -> Option<Vec<f32>> {
|
||||
let mut prog: Vec<f32> = vec![];
|
||||
let mut tables: Vec<Vec<f32>> = vec![];
|
||||
// LUT offsets are patched once the program's length is known.
|
||||
let mut patches: Vec<(usize, usize)> = vec![];
|
||||
for op in ops {
|
||||
match op {
|
||||
ColorOp::Decode(Tf::Linear) | ColorOp::Encode(Tf::Linear) => {}
|
||||
ColorOp::Decode(t) => {
|
||||
prog.push(1.0);
|
||||
prog.extend(tf_code(*t));
|
||||
}
|
||||
ColorOp::Encode(t) => {
|
||||
prog.push(2.0);
|
||||
prog.extend(tf_code(*t));
|
||||
}
|
||||
ColorOp::Matrix(m) => {
|
||||
prog.push(3.0);
|
||||
for r in m {
|
||||
prog.extend(r);
|
||||
}
|
||||
}
|
||||
ColorOp::Cdl { cdl, clamp, inverse } => {
|
||||
prog.push(4.0);
|
||||
for v in cdl.slope.iter().chain(&cdl.offset).chain(&cdl.power) {
|
||||
prog.push(*v as f32);
|
||||
}
|
||||
prog.extend([cdl.sat as f32, *clamp as u32 as f32, *inverse as u32 as f32]);
|
||||
}
|
||||
ColorOp::Tonemap { inverse } => prog.extend([5.0, *inverse as u32 as f32]),
|
||||
ColorOp::Gamut(l) => prog.extend([6.0, l[0] as f32, l[1] as f32, l[2] as f32]),
|
||||
ColorOp::Max0 => prog.push(7.0),
|
||||
ColorOp::Lut { lut, shaper, interp, inverse } => {
|
||||
prog.extend([8.0, 0.0, *interp as f32, *inverse as u32 as f32, -1.0]);
|
||||
let mut t = vec![];
|
||||
push_lut(&mut t, lut);
|
||||
let lut_at = prog.len() - 4;
|
||||
patches.push((lut_at, tables.len()));
|
||||
tables.push(t);
|
||||
if let Some(s) = shaper {
|
||||
// Shaper: three point counts, then each channel's (x, y) points.
|
||||
let mut t: Vec<f32> = s.iter().map(|c| c.len() as f32).collect();
|
||||
for c in s.iter() {
|
||||
for (x, y) in c {
|
||||
t.extend([*x, *y]);
|
||||
}
|
||||
}
|
||||
patches.push((prog.len() - 1, tables.len()));
|
||||
tables.push(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
prog.push(0.0);
|
||||
let mut starts = vec![];
|
||||
let mut at = prog.len();
|
||||
for t in &tables {
|
||||
starts.push(at);
|
||||
at += t.len();
|
||||
}
|
||||
if at > MAX_FLOATS {
|
||||
return None;
|
||||
}
|
||||
// Offsets travel as f32: exact below 2^24.
|
||||
for (slot, table) in patches {
|
||||
prog[slot] = starts[table] as f32;
|
||||
}
|
||||
for t in tables {
|
||||
prog.extend(t);
|
||||
}
|
||||
Some(prog)
|
||||
}
|
||||
|
|
@ -154,7 +154,7 @@ pub(crate) fn fractal(e: &mut Enc, ctx: &EffectCtx, b: GBuf, turbulent: bool) ->
|
|||
// ---------------------------------------------------------------- Median family (noise.rs, noise2.rs)
|
||||
|
||||
/// noise::median_image (of the straight colour with alpha 1 when `straight`).
|
||||
fn median_image(e: &mut Enc, img: &GpuImage, r: usize, straight: bool) -> GpuImage {
|
||||
pub(crate) fn median_image(e: &mut Enc, img: &GpuImage, r: usize, straight: bool) -> GpuImage {
|
||||
let q = run(e, "fxn_quant", &with_u0([straight as u32, 0, 0, 0]), img, None, None);
|
||||
if r <= MEDIAN_BISECT_R {
|
||||
return run(e, "fxn_median", &with_u0([r as u32, 0, 0, 0]), &q, None, None);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,424 @@
|
|||
//! GPU effects, simulation render passes (kernels in `shaders/fx_sim.wgsl`, every entry point
|
||||
//! prefixed `fxm_`): CC Rainfall, CC Snowfall, CC Star Burst, CC Bubbles, CC Drizzle, CC Hair,
|
||||
//! CC Mr. Mercury, Caustics, Wave World, Foam, Shatter, Card Dance and Card Wipe.
|
||||
//!
|
||||
//! The simulation (or closed-form layout) of every frame stays on the CPU, shared with the CPU
|
||||
//! effect: sprite plans (`effects::sprite_plan`: rain drops, snow flakes, star-burst discs, hair
|
||||
//! strands, Foam's bubbles, Wave World's wireframe), piece plans (`effects::piece_plan`: Card
|
||||
//! Dance's cards, Shatter's shards, Card Wipe's cards, sorted far to near), CC Bubbles' and CC
|
||||
//! Mr. Mercury's lists, CC Drizzle's drops, Wave World's height grid and Caustics' prepared
|
||||
//! layers. The raster / render pass runs here: items are binned into 16 × 16 tiles on the CPU
|
||||
//! and `fxm_raster` composites each pixel's items in the CPU's order with the CPU's shading
|
||||
//! (colours that depend on the layer's own pixels — refracting rain, star-burst and hair-root
|
||||
//! colours, bubble refraction — are sampled per pixel from the layer texture). Piece plans read
|
||||
//! the layer's pixels on the CPU (gradient maps and textures default to the layer), so those
|
||||
//! effects read the frame back once. Settings drawn otherwise (Shatter's wireframe views,
|
||||
//! Foam's User Defined texture, Environment Map and flow-map preview) render on the CPU.
|
||||
|
||||
use effectcraft_effects::{Acc, EffectCtx, PieceTex, Post, Shape, SpritePlan, Tint};
|
||||
use effectcraft_raster::Image;
|
||||
|
||||
use crate::context::{Enc, GpuImage, Params};
|
||||
use crate::effects::{GBuf, gaussian_blur};
|
||||
|
||||
/// Compute entry points in `fx_sim.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &["fxm_raster", "fxm_post", "fxm_mercury", "fxm_drizzle", "fxm_wave", "fxm_caustics"];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] = &[
|
||||
"ec.sim.ccrainfall",
|
||||
"ec.sim.ccsnowfall",
|
||||
"ec.sim.ccstarburst",
|
||||
"ec.sim.ccbubbles",
|
||||
"ec.sim.ccdrizzle",
|
||||
"ec.sim.cchair",
|
||||
"ec.sim.ccmrmercury",
|
||||
"ec.sim.caustics",
|
||||
"ec.sim.waveworld",
|
||||
"ec.sim.foam",
|
||||
"ec.sim.shatter",
|
||||
"ec.sim.carddance",
|
||||
"ec.transition.cardwipe",
|
||||
];
|
||||
|
||||
/// Largest item table (floats) the raster kernel takes; bigger plans render on the CPU.
|
||||
const MAX_FLOATS: usize = 1 << 24;
|
||||
const TILE: i32 = 16;
|
||||
|
||||
/// 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> {
|
||||
let geo = effectcraft_effects::Buf { img: Image::new(0, 0), offset: b.offset, scale: b.scale };
|
||||
match id {
|
||||
"ec.sim.ccbubbles" => bubbles(e, ctx, b, &geo),
|
||||
"ec.sim.ccdrizzle" => drizzle(e, ctx, b, &geo),
|
||||
"ec.sim.ccmrmercury" => mercury(e, ctx, b, &geo),
|
||||
"ec.sim.caustics" => caustics(e, ctx, b, &geo),
|
||||
"ec.sim.waveworld" => match effectcraft_effects::wave_world_plan(ctx, &geo)? {
|
||||
effectcraft_effects::WavePlan::Wire(plan) => sprites(e, b, &plan),
|
||||
effectcraft_effects::WavePlan::Height { st, depth } => wave_height(e, ctx, b, &st, depth.as_deref()),
|
||||
},
|
||||
"ec.sim.shatter" | "ec.sim.carddance" | "ec.transition.cardwipe" => pieces(e, id, ctx, b),
|
||||
"ec.sim.cchair" => match effectcraft_effects::sprite_plan(id, ctx, &geo) {
|
||||
Some(plan) => sprites(e, b, &plan),
|
||||
None => Some(b),
|
||||
},
|
||||
_ => {
|
||||
let plan = effectcraft_effects::sprite_plan(id, ctx, &geo)?;
|
||||
sprites(e, b, &plan)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- tiled item raster
|
||||
|
||||
/// Items with their integer pixel bounds, binned into tiles (sim::raster's bands, finer).
|
||||
struct Items {
|
||||
stride: usize,
|
||||
recs: Vec<f32>,
|
||||
w: i32,
|
||||
h: i32,
|
||||
tiles: Vec<Vec<u32>>,
|
||||
tx: i32,
|
||||
}
|
||||
|
||||
impl Items {
|
||||
fn new(stride: usize, w: u32, h: u32) -> Items {
|
||||
let (w, h) = (w as i32, h as i32);
|
||||
let tx = (w + TILE - 1) / TILE;
|
||||
let ty = (h + TILE - 1) / TILE;
|
||||
Items { stride, recs: vec![], w, h, tiles: vec![vec![]; (tx * ty).max(0) as usize], tx }
|
||||
}
|
||||
|
||||
/// Add an item (`rec` = its fields, without the bounds) covering float bounds `bb`
|
||||
/// (`None` or empty = skipped, as sim::raster).
|
||||
fn push(&mut self, bb: Option<[f32; 4]>, rec: &[f32]) {
|
||||
let Some(b) = bb else { return };
|
||||
if !(b[0].is_finite() && b[1].is_finite() && b[2].is_finite() && b[3].is_finite()) {
|
||||
return;
|
||||
}
|
||||
let x0 = b[0].floor().max(0.0) as i32;
|
||||
let y0 = b[1].floor().max(0.0) as i32;
|
||||
let x1 = (b[2].ceil() as i32).min(self.w - 1);
|
||||
let y1 = (b[3].ceil() as i32).min(self.h - 1);
|
||||
if x1 < x0 || y1 < y0 || b[2] < 0.0 || b[3] < 0.0 {
|
||||
return;
|
||||
}
|
||||
let i = (self.recs.len() / self.stride) as u32;
|
||||
let start = self.recs.len();
|
||||
self.recs.extend_from_slice(rec);
|
||||
self.recs.resize(start + self.stride - 4, 0.0);
|
||||
self.recs.extend([x0 as f32, y0 as f32, x1 as f32, y1 as f32]);
|
||||
for ty in y0 / TILE..=y1 / TILE {
|
||||
for tx in x0 / TILE..=x1 / TILE {
|
||||
self.tiles[(ty * self.tx + tx) as usize].push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Kernel data: tile offsets (tiles + 1), item indices, items. `None` when too large.
|
||||
fn data(self) -> Option<(Vec<f32>, u32, u32)> {
|
||||
let n_tiles = self.tiles.len();
|
||||
let n_idx: usize = self.tiles.iter().map(Vec::len).sum();
|
||||
let total = n_tiles + 1 + n_idx + self.recs.len();
|
||||
if total > MAX_FLOATS {
|
||||
return None;
|
||||
}
|
||||
let mut d = Vec::with_capacity(total);
|
||||
let base = (n_tiles + 1) as f32;
|
||||
let mut at = 0usize;
|
||||
for t in &self.tiles {
|
||||
d.push(base + at as f32);
|
||||
at += t.len();
|
||||
}
|
||||
d.push(base + at as f32);
|
||||
for t in &self.tiles {
|
||||
d.extend(t.iter().map(|&i| i as f32));
|
||||
}
|
||||
let items_at = d.len() as u32;
|
||||
d.extend(self.recs);
|
||||
Some((d, items_at, self.stride as u32))
|
||||
}
|
||||
}
|
||||
|
||||
/// Rasterise items over a transparent frame. `kind`: 0 sprites, 1 CC Bubbles, 2 Mr. Mercury
|
||||
/// field, 3 pieces; `src` / `aux` = the textures the items sample.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn raster(e: &mut Enc, items: Items, kind: u32, add: bool, src: &GpuImage, aux: Option<&GpuImage>, mut p: Params, w: u32, h: u32) -> Option<GpuImage> {
|
||||
let tx = items.tx as u32;
|
||||
let (data, items_at, stride) = items.data()?;
|
||||
let buf = e.data(&data);
|
||||
p.u[0] = [kind, add as u32, items_at, stride];
|
||||
p.u[1][0] = tx;
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fxm_raster", &p, src, aux, &out, Some(&buf));
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// sim::sprite_bbox.
|
||||
fn sprite_bbox(x: f32, y: f32, r: f32, c3: f32, shape: Shape) -> Option<[f32; 4]> {
|
||||
if c3 <= 1e-5 || r <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let r = r.max(0.5) + 1.0;
|
||||
Some(match shape {
|
||||
Shape::Line { dx, dy } => [x.min(x + dx) - r, y.min(y + dy) - r, x.max(x + dx) + r, y.max(y + dy) + r],
|
||||
Shape::Star => [x - r * 2.0, y - r * 2.0, x + r * 2.0, y + r * 2.0],
|
||||
_ => [x - r, y - r, x + r, y + r],
|
||||
})
|
||||
}
|
||||
|
||||
fn sprites(e: &mut Enc, b: GBuf, plan: &SpritePlan) -> Option<GBuf> {
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let mut items = Items::new(24, w, h);
|
||||
for (i, s) in plan.sprites.iter().enumerate() {
|
||||
let (kind, dx, dy) = match s.shape {
|
||||
Shape::Soft => (0.0, 0.0, 0.0),
|
||||
Shape::Disc => (1.0, 0.0, 0.0),
|
||||
Shape::Sphere => (2.0, 0.0, 0.0),
|
||||
Shape::Faded => (3.0, 0.0, 0.0),
|
||||
Shape::Bubble => (4.0, 0.0, 0.0),
|
||||
Shape::Star => (5.0, 0.0, 0.0),
|
||||
Shape::Square => (6.0, 0.0, 0.0),
|
||||
Shape::Tri => (7.0, 0.0, 0.0),
|
||||
Shape::Line { dx, dy } => (8.0, dx, dy),
|
||||
};
|
||||
let mut rec = [kind, s.x, s.y, s.r, s.c[0], s.c[1], s.c[2], s.c[3], s.rot, dx, dy, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
|
||||
match plan.tints.get(i).copied().unwrap_or(Tint::Fixed) {
|
||||
Tint::Fixed => {}
|
||||
Tint::Refract { x, y } => rec[11..14].copy_from_slice(&[1.0, x as f32, y as f32]),
|
||||
Tint::Source { x, y } => rec[11..14].copy_from_slice(&[2.0, x as f32, y as f32]),
|
||||
Tint::Root { x, y, hair, inherit, shade, spec } => {
|
||||
rec[11..20].copy_from_slice(&[3.0, x as f32, y as f32, hair[0], hair[1], hair[2], inherit, shade, spec]);
|
||||
}
|
||||
}
|
||||
items.push(sprite_bbox(s.x, s.y, s.r, s.c[3], s.shape), &rec);
|
||||
}
|
||||
let fx = raster(e, items, 0, plan.acc == Acc::Add, &b.img, None, Params::default(), w, h)?;
|
||||
let mut p = Params::default();
|
||||
match plan.post {
|
||||
Post::Combine(m) => p.u[0] = [0, m, 0, 0],
|
||||
Post::Replace => p.u[0] = [0, 5, 0, 0],
|
||||
Post::Lerp(t) => {
|
||||
p.u[0][0] = 1;
|
||||
p.f[0][0] = t;
|
||||
}
|
||||
Post::OverBlack => p.u[0][0] = 2,
|
||||
}
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fxm_post", &p, &b.img, Some(&fx), &out, None);
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Bubbles / Mr. Mercury
|
||||
|
||||
fn bubbles(e: &mut Enc, ctx: &EffectCtx, b: GBuf, geo: &effectcraft_effects::Buf) -> Option<GBuf> {
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let mut items = Items::new(12, w, h);
|
||||
for q in effectcraft_effects::bubble_list(ctx, geo) {
|
||||
items.push(Some([q.x - q.r - 1.0, q.y - q.r - 1.0, q.x + q.r + 1.0, q.y + q.r + 1.0]), &[9.0, q.x, q.y, q.r, q.sx, q.sy]);
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[1] = [0, (ctx.params.e("reflectionType") == 1) as u32, ctx.params.e("shadingType"), 0];
|
||||
let img = raster(e, items, 1, false, &b.img, None, p, w, h)?;
|
||||
Some(GBuf { img, ..b })
|
||||
}
|
||||
|
||||
fn mercury(e: &mut Enc, ctx: &EffectCtx, b: GBuf, geo: &effectcraft_effects::Buf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let infl = 1.0 + pr.f("blobInfluence") as f32 / 100.0;
|
||||
let mut items = Items::new(8, w, h);
|
||||
for q in effectcraft_effects::mercury_blobs(ctx, geo) {
|
||||
let ex = q.r * 2.0 * infl;
|
||||
items.push(Some([q.x - ex, q.y - ex, q.x + ex, q.y + ex]), &[10.0, q.x, q.y, q.r]);
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.f[0][0] = infl;
|
||||
let field = raster(e, items, 2, true, &b.img, None, p, w, h)?;
|
||||
let ldir = (pr.f("light/lightDirection") as f32).to_radians();
|
||||
let lheight = pr.f("light/lightHeight") as f32 / 100.0;
|
||||
let l = norm3([ldir.sin(), -ldir.cos(), lheight.max(0.05)]);
|
||||
let lc = pr.color("light/lightColor");
|
||||
let mut p = Params::default();
|
||||
p.f[0] = [
|
||||
pr.f("shading/ambient") as f32 / 100.0,
|
||||
pr.f("shading/diffuse") as f32 / 100.0,
|
||||
pr.f("shading/specular") as f32 / 100.0,
|
||||
pr.f("shading/roughness").max(0.001) as f32,
|
||||
];
|
||||
p.f[1] = [pr.f("shading/metal") as f32 / 100.0, pr.f("light/lightIntensity") as f32 / 100.0, b.scale as f32, 0.0];
|
||||
p.f[2] = [l[0], l[1], l[2], 0.0];
|
||||
p.f[3] = [lc[0], lc[1], lc[2], 0.0];
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fxm_mercury", &p, &b.img, Some(&field), &out, None);
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
||||
/// sim::norm3.
|
||||
fn norm3(v: [f32; 3]) -> [f32; 3] {
|
||||
let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
|
||||
if l > 1e-9 { [v[0] / l, v[1] / l, v[2] / l] } else { [0.0, -1.0, 0.0] }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Drizzle
|
||||
|
||||
fn drizzle(e: &mut Enc, ctx: &EffectCtx, b: GBuf, geo: &effectcraft_effects::Buf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let Some(drops) = effectcraft_effects::drizzle_drops(ctx, geo) else { return Some(b) };
|
||||
let rippling = (pr.f("rippling") / 360.0).max(0.05) as f32;
|
||||
let lh = ctx.layer_size[1] as f32;
|
||||
let s = b.scale as f32;
|
||||
let wavelen = (lh * 0.04 * s).max(2.0) / rippling.clamp(0.25, 4.0);
|
||||
let width = wavelen * (1.0 + rippling * 2.0);
|
||||
let ldir = (pr.f("light/lightDirection") as f32).to_radians();
|
||||
let lheight = pr.f("light/lightHeight") as f32 / 100.0;
|
||||
let (ls, lcs) = ldir.sin_cos();
|
||||
let l = {
|
||||
let v = [lcs, ls, lheight.max(0.05)];
|
||||
let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
|
||||
[v[0] / n, v[1] / n, v[2] / n]
|
||||
};
|
||||
let hz = (l[2] + 1.0) * 0.5;
|
||||
let hv = {
|
||||
let v = [l[0] * 0.5, l[1] * 0.5, hz];
|
||||
let m = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
|
||||
[v[0] / m, v[1] / m, v[2] / m]
|
||||
};
|
||||
let lc = pr.color("light/lightColor");
|
||||
let d: Vec<f32> = drops.iter().flatten().copied().collect();
|
||||
let buf = e.data(&d);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = drops.len() as u32;
|
||||
p.f[0] = [wavelen, width, pr.f("displacement") as f32 * s * 4.0, pr.f("shading/roughness").max(0.001) as f32];
|
||||
p.f[1] = [l[0], l[1], l[2], pr.f("light/lightIntensity") as f32 / 100.0];
|
||||
p.f[2] = [hv[0], hv[1], hv[2], pr.f("shading/specular") as f32 / 100.0];
|
||||
p.f[3] = [lc[0], lc[1], lc[2], 0.0];
|
||||
p.f[4] = [pr.f("shading/ambient") as f32 / 100.0, pr.f("shading/diffuse") as f32 / 100.0, 0.0, 0.0];
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxm_drizzle", &p, &b.img, None, &out, Some(&buf));
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Wave World (Height Map)
|
||||
|
||||
fn wave_height(e: &mut Enc, ctx: &EffectCtx, b: GBuf, st: &effectcraft_effects::Waves, depth: Option<&[f32]>) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let mut d = st.u.clone();
|
||||
if let Some(dp) = depth {
|
||||
d.extend_from_slice(dp);
|
||||
}
|
||||
let buf = e.data(&d);
|
||||
let mut p = Params::default();
|
||||
let dry_transparent = pr.e("heightMapControls/renderDryAreasAs") == 1 && depth.is_some();
|
||||
p.u[0] = [st.nx as u32, st.ny as u32, dry_transparent as u32, 0];
|
||||
p.f[0] = [
|
||||
pr.f("heightMapControls/brightness") as f32,
|
||||
pr.f("heightMapControls/contrast") as f32,
|
||||
pr.f("heightMapControls/gamma").max(0.01) as f32,
|
||||
1.0 - pr.f("heightMapControls/transparency") as f32,
|
||||
];
|
||||
p.f[1] = [b.offset[0] as f32, b.offset[1] as f32, b.scale as f32, ctx.layer_size[0].max(1.0) as f32];
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxm_wave", &p, &b.img, None, &out, Some(&buf));
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- pieces
|
||||
|
||||
fn pieces(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
// Planning reads the layer (gradient maps and textures default to it).
|
||||
let img = e.download(&b.img)?;
|
||||
let cpu = effectcraft_effects::Buf { img, offset: b.offset, scale: b.scale };
|
||||
let Some(plan) = effectcraft_effects::piece_plan(id, ctx, &cpu) else {
|
||||
// Card Wipe before anything moves: the layer as is; Shatter's wireframes: the CPU.
|
||||
return (id == "ec.transition.cardwipe").then_some(b);
|
||||
};
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let tex = |t: &PieceTex| -> Option<GpuImage> {
|
||||
match t {
|
||||
PieceTex::Layer => Some(b.img.clone()),
|
||||
PieceTex::Image(i) => e.g.upload_image(i),
|
||||
}
|
||||
};
|
||||
let front = tex(&plan.front)?;
|
||||
let back = match &plan.back {
|
||||
Some(t) => Some(tex(t)?),
|
||||
None => None,
|
||||
};
|
||||
let mut items = Items::new(48, w, h);
|
||||
for q in &plan.pieces {
|
||||
let m = q.inv;
|
||||
let mut rec = vec![11.0];
|
||||
for r in m {
|
||||
rec.extend(r.map(|v| v as f32));
|
||||
}
|
||||
rec.extend(q.c0);
|
||||
for v in q.poly {
|
||||
rec.extend(v);
|
||||
}
|
||||
rec.extend([q.n as f32, q.shade]);
|
||||
rec.extend(q.tint);
|
||||
rec.extend(q.spec);
|
||||
rec.push(q.alpha);
|
||||
match q.flat {
|
||||
Some(c) => rec.extend([1.0, c[0], c[1], c[2]]),
|
||||
None => rec.extend([0.0; 4]),
|
||||
}
|
||||
rec.extend([q.back as u32 as f32, q.back_mirror as f32]);
|
||||
items.push(Some(q.bbox), &rec);
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[1][1] = back.is_some() as u32;
|
||||
let img = raster(e, items, 3, false, &front, back.as_ref(), p, w, h)?;
|
||||
Some(GBuf { img, ..b })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Caustics
|
||||
|
||||
fn caustics(e: &mut Enc, ctx: &EffectCtx, b: GBuf, geo: &effectcraft_effects::Buf) -> Option<GBuf> {
|
||||
let st = effectcraft_effects::caustics_setup(ctx, geo);
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let mut bottom = match &st.bottom {
|
||||
Some(i) => e.g.upload_image(i)?,
|
||||
None => b.img.clone(),
|
||||
};
|
||||
if st.bottom_sigma > 0.0 {
|
||||
bottom = gaussian_blur(e, &bottom, st.bottom_sigma, st.bottom_sigma, false);
|
||||
}
|
||||
let height = match &st.height {
|
||||
Some(pl) => {
|
||||
let mut img = Image::new(pl.w as u32, pl.h as u32);
|
||||
for (px, v) in img.data.iter_mut().zip(&pl.data) {
|
||||
*px = [*v, 0.0, 0.0, 0.0];
|
||||
}
|
||||
Some(e.g.upload_image(&img)?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let sky = match &st.sky {
|
||||
Some(i) => {
|
||||
let g = e.g.upload_image(i)?;
|
||||
Some((e.image_rows(&g), g.width, g.height))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let distant = {
|
||||
let lw = st.lw.max(1.0);
|
||||
let v = [(st.lx - st.cx) / lw, (st.ly - st.cy) / lw, st.lheight];
|
||||
let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-12);
|
||||
[(v[0] / n) as f32, (v[1] / n) as f32, (v[2] / n) as f32]
|
||||
};
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [height.is_some() as u32, st.repeat, st.point as u32, st.sky_repeat];
|
||||
p.u[1] = [sky.is_some() as u32, sky.as_ref().map_or(0, |s| s.0.1), sky.as_ref().map_or(0, |s| s.1), sky.as_ref().map_or(0, |s| s.2)];
|
||||
p.f[0] = [st.scaling as f32, st.wave_h as f32, st.k as f32, st.lw as f32];
|
||||
p.f[1] = [st.cx as f32, st.cy as f32, st.lx as f32, st.ly as f32];
|
||||
p.f[2] = [st.surf[0], st.surf[1], st.surf[2], st.surf_op];
|
||||
p.f[3] = [st.lc[0], st.lc[1], st.lc[2], st.li];
|
||||
p.f[4] = [st.lheight as f32, st.cstr, st.ambient, st.diffuse];
|
||||
p.f[5] = [st.specular, st.sharp, st.sky_scaling as f32, st.sky_int];
|
||||
p.f[6] = [distant[0], distant[1], distant[2], st.convergence as f32];
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fxm_caustics", &p, &bottom, height.as_ref(), &out, sky.as_ref().map(|s| &s.0.0));
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
|
@ -0,0 +1,431 @@
|
|||
//! GPU effects, Immersive Video family (kernels in `shaders/fx_vr.wgsl`, every entry point
|
||||
//! prefixed `fxv_`): VR Blur, VR Chromatic Aberrations, VR Color Gradients, VR Converter, VR
|
||||
//! De-Noise, VR Digital Glitch, VR Fractal Noise, VR Glow, VR Plane to Sphere, VR Rotate Sphere,
|
||||
//! VR Sharpen and VR Sphere to Plane.
|
||||
//!
|
||||
//! The equirectangular maths of `effects::vr` (view directions, the projections and cube-map
|
||||
//! layouts of VR Converter, seam-wrapping and pole-continuing bilinear sampling, the
|
||||
//! latitude-widened seam-aware box blurs) is ported to WGSL operation for operation; rotation
|
||||
//! matrices, per-row blur radii and VR Digital Glitch's per-band offsets are computed on the CPU
|
||||
//! exactly as the CPU effect does. Stereo layouts run each eye on its own cropped image. Angles
|
||||
//! between directions use atan2(|a × b|, a · b), the f32-stable form of the CPU's f64 acos;
|
||||
//! VR Color Gradients' inverse-distance weights are normalised by the nearest point (the same
|
||||
//! ratios without f32 overflow). VR De-Noise reuses the noise family's guided filter and median.
|
||||
|
||||
use effectcraft_effects::EffectCtx;
|
||||
use effectcraft_raster::hash_noise;
|
||||
|
||||
use crate::context::{Enc, GpuImage, Params};
|
||||
use crate::effects::GBuf;
|
||||
|
||||
/// Compute entry points in `fx_vr.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &["fxv_convert", "fxv_box_h", "fxv_box_v", "fxv_point", "fxv_gen", "fxv_wrap_pad"];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] = &[
|
||||
"ec.vr.blur",
|
||||
"ec.vr.chromaticaberrations",
|
||||
"ec.vr.colorgradients",
|
||||
"ec.vr.converter",
|
||||
"ec.vr.denoise",
|
||||
"ec.vr.digitalglitch",
|
||||
"ec.vr.fractalnoise",
|
||||
"ec.vr.glow",
|
||||
"ec.vr.planetosphere",
|
||||
"ec.vr.rotatesphere",
|
||||
"ec.vr.sharpen",
|
||||
"ec.vr.spheretoplane",
|
||||
];
|
||||
|
||||
/// 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> {
|
||||
if b.img.width == 0 || b.img.height == 0 {
|
||||
return Some(b);
|
||||
}
|
||||
let img = match id {
|
||||
"ec.vr.blur" => blur(e, ctx, &b.img, b.scale)?,
|
||||
"ec.vr.sharpen" => sharpen(e, ctx, &b.img, b.scale)?,
|
||||
"ec.vr.glow" => glow(e, ctx, &b.img, b.scale)?,
|
||||
"ec.vr.denoise" => denoise(e, ctx, &b)?,
|
||||
"ec.vr.rotatesphere" => rotate(e, ctx, &b.img)?,
|
||||
"ec.vr.converter" => converter(e, ctx, &b.img)?,
|
||||
"ec.vr.spheretoplane" => {
|
||||
let m = rot(ctx, "tilt", "pan", "roll");
|
||||
let fov = ctx.params.f("horizontalFov") as f32;
|
||||
per_eye(e, &b.img, layout(ctx), |e, img, _| Some(convert(e, img, (0, 360.0), (5, fov), &m, img.width, img.height, None)))?
|
||||
}
|
||||
"ec.vr.planetosphere" => {
|
||||
let fov = ctx.params.f("scale").clamp(1.0, 179.0);
|
||||
let m = rot(ctx, "tilt", "pan", "roll");
|
||||
let feather = (ctx.params.f("feather") / 100.0).clamp(0.0, 1.0);
|
||||
let t = (fov / 2.0).to_radians().tan();
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
convert(e, &b.img, (5, fov as f32), (0, 360.0), &m, w, h, (feather > 0.0).then_some([feather as f32, t as f32]))
|
||||
}
|
||||
"ec.vr.chromaticaberrations" => chromatic(e, ctx, &b.img)?,
|
||||
"ec.vr.colorgradients" => gradients(e, ctx, &b.img)?,
|
||||
"ec.vr.fractalnoise" => fractal(e, ctx, &b.img)?,
|
||||
"ec.vr.digitalglitch" => glitch(e, ctx, &b.img)?,
|
||||
_ => return None,
|
||||
};
|
||||
Some(GBuf { img, ..b })
|
||||
}
|
||||
|
||||
fn layout(ctx: &EffectCtx) -> u32 {
|
||||
ctx.params.e("frameLayout")
|
||||
}
|
||||
|
||||
/// vr::eyes.
|
||||
fn eyes(w: u32, h: u32, layout: u32) -> Vec<(u32, u32, u32, u32)> {
|
||||
match layout {
|
||||
1 if h >= 2 => vec![(0, 0, w, h / 2), (0, h / 2, w, h - h / 2)],
|
||||
2 if w >= 2 => vec![(0, 0, w / 2, h), (w / 2, 0, w - w / 2, h)],
|
||||
_ => vec![(0, 0, w, h)],
|
||||
}
|
||||
}
|
||||
|
||||
/// vr::per_eye: run `f` on each eye's own image and reassemble.
|
||||
fn per_eye(e: &mut Enc, img: &GpuImage, layout: u32, mut f: impl FnMut(&mut Enc, &GpuImage, usize) -> Option<GpuImage>) -> Option<GpuImage> {
|
||||
let rects = eyes(img.width, img.height, layout);
|
||||
if rects.len() == 1 {
|
||||
return f(e, img, 0);
|
||||
}
|
||||
let out = e.scratch(img.width, img.height);
|
||||
for (i, &(x, y, w, h)) in rects.iter().enumerate() {
|
||||
let crop = e.scratch(w, h);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [x, y, 0, 0];
|
||||
e.pixels("fxn_crop", &p, img, None, &crop, None);
|
||||
let r = f(e, &crop, i)?;
|
||||
e.copy_into(&r, &out, x, y);
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn rot(ctx: &EffectCtx, t: &str, p: &str, r: &str) -> [[f32; 3]; 3] {
|
||||
effectcraft_effects::vr_rotation(ctx.params.f(t), ctx.params.f(p), ctx.params.f(r)).map(|r| r.map(|v| v as f32))
|
||||
}
|
||||
|
||||
fn put_matrix(p: &mut Params, at: usize, m: &[[f32; 3]; 3]) {
|
||||
for (i, r) in m.iter().enumerate() {
|
||||
p.f[at + i] = [r[0], r[1], r[2], 0.0];
|
||||
}
|
||||
}
|
||||
|
||||
/// vr::convert: re-project `src` (projection `from` = (kind, fov)) into a `w`×`h` frame in `to`,
|
||||
/// output direction → source direction = mᵀ · d. `feather` = (feather, tan(fov / 2)) for VR
|
||||
/// Plane to Sphere's edge.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn convert(e: &mut Enc, src: &GpuImage, from: (u32, f32), to: (u32, f32), m: &[[f32; 3]; 3], w: u32, h: u32, feather: Option<[f32; 2]>) -> GpuImage {
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [from.0, to.0, feather.is_some() as u32, 0];
|
||||
p.f[0] = [from.1, to.1, 0.0, 0.0];
|
||||
put_matrix(&mut p, 1, m);
|
||||
if let Some(f) = feather {
|
||||
p.f[4] = [f[0], f[1], 0.0, 0.0];
|
||||
}
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fxv_convert", &p, src, None, &out, None);
|
||||
out
|
||||
}
|
||||
|
||||
fn rotate(e: &mut Enc, ctx: &EffectCtx, img: &GpuImage) -> Option<GpuImage> {
|
||||
let pr = ctx.params;
|
||||
if pr.f("tilt") == 0.0 && pr.f("pan") == 0.0 && pr.f("roll") == 0.0 {
|
||||
return Some(img.clone());
|
||||
}
|
||||
let mut m = rot(ctx, "tilt", "pan", "roll");
|
||||
if pr.b("invertRotation") {
|
||||
m = [[m[0][0], m[1][0], m[2][0]], [m[0][1], m[1][1], m[2][1]], [m[0][2], m[1][2], m[2][2]]];
|
||||
}
|
||||
per_eye(e, img, layout(ctx), |e, img, _| Some(convert(e, img, (0, 360.0), (0, 360.0), &m, img.width, img.height, None)))
|
||||
}
|
||||
|
||||
fn converter(e: &mut Enc, ctx: &EffectCtx, img: &GpuImage) -> Option<GpuImage> {
|
||||
let pr = ctx.params;
|
||||
let from = (pr.e("sourceProjection"), pr.f("sourceHorizontalFov"));
|
||||
let to = (pr.e("targetProjection"), pr.f("targetHorizontalFov"));
|
||||
if from.0 == to.0 && (!matches!(from.0, 4 | 5) || from.1 == to.1) && pr.f("tilt") == 0.0 && pr.f("pan") == 0.0 && pr.f("roll") == 0.0 {
|
||||
return Some(img.clone());
|
||||
}
|
||||
let m = rot(ctx, "tilt", "pan", "roll");
|
||||
let (from, to) = ((from.0, from.1 as f32), (to.0, to.1 as f32));
|
||||
per_eye(e, img, layout(ctx), |e, img, _| Some(convert(e, img, from, to, &m, img.width, img.height, None)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- seam-aware blur
|
||||
|
||||
/// vr::sphere_blur: 3 latitude-widened, seam-wrapping horizontal box passes, then 3
|
||||
/// pole-continuing vertical ones.
|
||||
fn sphere_blur(e: &mut Enc, img: &GpuImage, sigma: f64) -> GpuImage {
|
||||
if sigma < 0.1 {
|
||||
return img.clone();
|
||||
}
|
||||
let (w, h) = (img.width as usize, img.height as usize);
|
||||
let radii = effectcraft_effects::util::box_radii(sigma, 3);
|
||||
let mut cur = img.clone();
|
||||
for &r in &radii {
|
||||
let rows: Vec<f32> = (0..h)
|
||||
.map(|y| {
|
||||
let lat = (0.5 - (y as f64 + 0.5) / h as f64) * std::f64::consts::PI;
|
||||
let rr = ((r as f64) / lat.cos().max(1e-3)).round().min((w / 2) as f64) as usize;
|
||||
// box_row_wrap's own limit.
|
||||
let rr = if w == 0 { 0 } else { rr.min((w - 1) / 2) };
|
||||
rr as f32
|
||||
})
|
||||
.collect();
|
||||
if rows.iter().all(|r| *r == 0.0) {
|
||||
continue;
|
||||
}
|
||||
let buf = e.data(&rows);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = 256;
|
||||
let out = e.scratch(cur.width, cur.height);
|
||||
e.dispatch("fxv_box_h", &p, &cur, None, &out, Some(&buf), (cur.height.div_ceil(64), cur.width.div_ceil(256)));
|
||||
cur = out;
|
||||
}
|
||||
for &r in &radii {
|
||||
if r == 0 {
|
||||
continue;
|
||||
}
|
||||
let block = (4 * r).clamp(32, 256) as u32;
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [block, r as u32, 0, 0];
|
||||
let out = e.scratch(cur.width, cur.height);
|
||||
e.dispatch("fxv_box_v", &p, &cur, None, &out, None, (cur.width.div_ceil(64), cur.height.div_ceil(block)));
|
||||
cur = out;
|
||||
}
|
||||
cur
|
||||
}
|
||||
|
||||
fn op(code: u32) -> Params {
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = code;
|
||||
p
|
||||
}
|
||||
|
||||
fn point(e: &mut Enc, p: &Params, src: &GpuImage, aux: Option<&GpuImage>) -> GpuImage {
|
||||
let out = e.scratch(src.width, src.height);
|
||||
e.pixels("fxv_point", p, src, aux, &out, None);
|
||||
out
|
||||
}
|
||||
|
||||
fn blur(e: &mut Enc, ctx: &EffectCtx, img: &GpuImage, scale: f64) -> Option<GpuImage> {
|
||||
let s = ctx.params.f("blurriness").max(0.0) * scale * 0.5;
|
||||
if s < 0.1 {
|
||||
return Some(img.clone());
|
||||
}
|
||||
per_eye(e, img, layout(ctx), |e, img, _| Some(sphere_blur(e, img, s)))
|
||||
}
|
||||
|
||||
fn sharpen(e: &mut Enc, ctx: &EffectCtx, img: &GpuImage, scale: f64) -> Option<GpuImage> {
|
||||
let amt = ctx.params.f("sharpenAmount") as f32 / 100.0;
|
||||
if amt == 0.0 {
|
||||
return Some(img.clone());
|
||||
}
|
||||
let s = (1.5 * scale).max(0.5);
|
||||
per_eye(e, img, layout(ctx), |e, img, _| {
|
||||
let bl = sphere_blur(e, img, s);
|
||||
let mut p = op(2);
|
||||
p.f[0][0] = amt;
|
||||
Some(point(e, &p, img, Some(&bl)))
|
||||
})
|
||||
}
|
||||
|
||||
fn glow(e: &mut Enc, ctx: &EffectCtx, img: &GpuImage, scale: f64) -> Option<GpuImage> {
|
||||
let pr = ctx.params;
|
||||
let bright = pr.f("glowBrightness") as f32 / 100.0;
|
||||
if bright <= 0.0 {
|
||||
return Some(img.clone());
|
||||
}
|
||||
let radius = pr.f("glowRadius").max(0.0) * scale;
|
||||
let tc = pr.color("tintColor");
|
||||
let mut hp = op(0);
|
||||
hp.u[0][1] = pr.b("useTintColor") as u32;
|
||||
hp.f[0] = [pr.f("luminanceThreshold") as f32 / 100.0, pr.f("glowSaturation") as f32 / 100.0, 0.0, 0.0];
|
||||
hp.f[1] = [tc[0], tc[1], tc[2], 0.0];
|
||||
let mut add = op(1);
|
||||
add.f[0][0] = bright;
|
||||
per_eye(e, img, layout(ctx), |e, img, _| {
|
||||
let hi = point(e, &hp, img, None);
|
||||
let g = sphere_blur(e, &hi, (radius * 0.5).max(0.5));
|
||||
Some(point(e, &add, img, Some(&g)))
|
||||
})
|
||||
}
|
||||
|
||||
fn denoise(e: &mut Enc, ctx: &EffectCtx, b: &GBuf) -> Option<GpuImage> {
|
||||
let pr = ctx.params;
|
||||
let level = pr.f("noiseLevel").max(0.0) / 100.0;
|
||||
if level <= 0.0 {
|
||||
return Some(b.img.clone());
|
||||
}
|
||||
let r = ((1.0 + level * 4.0) * b.scale).max(1.0) as usize;
|
||||
if pr.e("noiseType") == 1 {
|
||||
let mr = ((level * 3.0 * b.scale).round() as usize).max(1);
|
||||
return per_eye(e, &b.img, layout(ctx), |e, img, _| Some(crate::fx_noise::median_image(e, img, mr, false)));
|
||||
}
|
||||
let eps = (level * level * 0.02) as f32;
|
||||
let detail = (pr.f("detail") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
per_eye(e, &b.img, layout(ctx), |e, img, _| {
|
||||
let pad = (r * 2 + 1) as u32;
|
||||
let (pw, ph) = (img.width + 2 * pad, img.height + 2 * pad);
|
||||
if !e.g.fits(pw, ph) {
|
||||
return None;
|
||||
}
|
||||
let padded = e.scratch(pw, ph);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = pad;
|
||||
e.pixels("fxv_wrap_pad", &p, img, None, &padded, None);
|
||||
let guide = point(e, &op(4), &padded, None);
|
||||
let f = crate::fx_noise::guided_filter(e, &guide, &padded, r, eps.max(1e-6));
|
||||
let crop = e.scratch(img.width, img.height);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [pad, pad, 0, 0];
|
||||
e.pixels("fxn_crop", &p, &f, None, &crop, None);
|
||||
let mut p = op(3);
|
||||
p.f[0][0] = detail;
|
||||
Some(point(e, &p, &crop, Some(img)))
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- generators
|
||||
|
||||
/// BLEND_OPTS → (none, blend mode id).
|
||||
fn blend(i: u32) -> (u32, u32) {
|
||||
use effectcraft_color::BlendMode::*;
|
||||
let m = match i {
|
||||
0 => return (1, 0),
|
||||
2 => Add,
|
||||
3 => Multiply,
|
||||
4 => Screen,
|
||||
5 => Overlay,
|
||||
_ => Normal,
|
||||
};
|
||||
(0, crate::ops::mode_id(m))
|
||||
}
|
||||
|
||||
fn chromatic(e: &mut Enc, ctx: &EffectCtx, img: &GpuImage) -> Option<GpuImage> {
|
||||
let pr = ctx.params;
|
||||
let k = [pr.f("aberrationRed"), pr.f("aberrationGreen"), pr.f("aberrationBlue")].map(|v| (v / 100.0 * 0.1) as f32);
|
||||
if k.iter().all(|v| *v == 0.0) {
|
||||
return Some(img.clone());
|
||||
}
|
||||
let r = effectcraft_effects::vr_rotation(pr.f("centerTilt"), pr.f("centerPan"), 0.0);
|
||||
let c = [r[0][2], r[1][2], r[2][2]].map(|v| v as f32);
|
||||
let mut p = op(0);
|
||||
p.u[0][1] = pr.b("falloffInvert") as u32;
|
||||
p.f[0] = [k[0], k[1], k[2], (pr.f("falloff") / 100.0).clamp(0.0, 1.0) as f32];
|
||||
p.f[1] = [c[0], c[1], c[2], 0.0];
|
||||
per_eye(e, img, layout(ctx), |e, img, _| Some(gen_pass(e, &p, img, None)))
|
||||
}
|
||||
|
||||
fn gen_pass(e: &mut Enc, p: &Params, img: &GpuImage, data: Option<&wgpu::Buffer>) -> GpuImage {
|
||||
let out = e.scratch(img.width, img.height);
|
||||
e.pixels("fxv_gen", p, img, None, &out, data);
|
||||
out
|
||||
}
|
||||
|
||||
fn gradients(e: &mut Enc, ctx: &EffectCtx, img: &GpuImage) -> Option<GpuImage> {
|
||||
let pr = ctx.params;
|
||||
let (lw, lh) = (ctx.layer_size[0].max(1.0), ctx.layer_size[1].max(1.0));
|
||||
let mut d = vec![];
|
||||
for i in 1..=5 {
|
||||
if !pr.b(&format!("enablePoint{i}")) {
|
||||
continue;
|
||||
}
|
||||
let pt = pr.v2(&format!("point{i}"));
|
||||
let dir = effectcraft_effects::vr_equi_dir(pt[0], pt[1], lw, lh);
|
||||
let c = pr.color(&format!("color{i}"));
|
||||
d.extend([dir[0] as f32, dir[1] as f32, dir[2] as f32, c[0], c[1], c[2]]);
|
||||
}
|
||||
if d.is_empty() {
|
||||
return Some(img.clone());
|
||||
}
|
||||
let (none, mode) = blend(pr.e("blendingMode"));
|
||||
let mut p = op(1);
|
||||
p.u[0] = [1, (d.len() / 6) as u32, none, mode];
|
||||
p.f[0] = [pr.f("blend").max(0.1) as f32, (pr.f("opacity") / 100.0) as f32, 0.0, 0.0];
|
||||
let buf = e.data(&d);
|
||||
per_eye(e, img, layout(ctx), |e, img, _| Some(gen_pass(e, &p, img, Some(&buf))))
|
||||
}
|
||||
|
||||
fn fractal(e: &mut Enc, ctx: &EffectCtx, img: &GpuImage) -> Option<GpuImage> {
|
||||
let pr = ctx.params;
|
||||
let g = |id: &str, d: f64| pr.get(id).map(|v| v.as_f64()).unwrap_or(d);
|
||||
let octaves = pr.f("complexity").clamp(1.0, 20.0);
|
||||
let scale = (pr.f("transform/scale") / 100.0).max(0.01) as f32;
|
||||
let (none, mode) = blend(pr.e("blendingMode"));
|
||||
let mut p = op(2);
|
||||
p.u[0] = [2, pr.e("fractalType"), none, mode];
|
||||
p.u[1] = [octaves.ceil() as u32, pr.f("randomSeed") as u32, pr.b("invert") as u32, 0];
|
||||
p.f[0] = [
|
||||
pr.f("contrast") as f32 / 100.0,
|
||||
pr.f("brightness") as f32 / 100.0,
|
||||
(g("subSettings/subInfluence", 50.0) / 100.0).clamp(0.0, 1.0) as f32,
|
||||
(g("subSettings/subScaling", 50.0) / 100.0).clamp(0.1, 1.0) as f32,
|
||||
];
|
||||
p.f[1] = [4.0 / scale, (pr.f("evolution") / 360.0) as f32, (pr.f("opacity") / 100.0) as f32, octaves.fract() as f32];
|
||||
put_matrix(&mut p, 2, &rot(ctx, "transform/tilt", "transform/pan", "transform/roll"));
|
||||
per_eye(e, img, layout(ctx), |e, img, _| Some(gen_pass(e, &p, img, None)))
|
||||
}
|
||||
|
||||
fn glitch(e: &mut Enc, ctx: &EffectCtx, img: &GpuImage) -> Option<GpuImage> {
|
||||
let pr = ctx.params;
|
||||
let amp = (pr.f("masterAmplitude") / 100.0).max(0.0);
|
||||
if amp <= 0.0 {
|
||||
return Some(img.clone());
|
||||
}
|
||||
let g = |id: &str, d: f64| pr.get(id).map(|v| v.as_f64()).unwrap_or(d);
|
||||
let rate = pr.f("distortionRate").max(0.0);
|
||||
let geo = pr.f("geometricDistortion") / 100.0 * amp;
|
||||
let (hdisp, vdisp) = (g("geometric/horizontalDisplacement", 100.0) / 100.0, g("geometric/verticalDisplacement", 0.0) / 100.0);
|
||||
let complexity = pr.f("distortionComplexity").clamp(1.0, 100.0);
|
||||
let rgb = pr.f("colorDistortion") / 100.0 * amp;
|
||||
let offs = [g("color/redOffset", -1.0), g("color/greenOffset", 0.0), g("color/blueOffset", 1.0)];
|
||||
let scan = (pr.f("scanlines") / 100.0 * amp) as f32;
|
||||
let spacing = g("scanlineSpacing", 2.0).round().max(2.0) as usize;
|
||||
let seed = (pr.f("randomSeed") as u32) ^ ctx.seed;
|
||||
let mut evo = g("distortionEvolution", 0.0) / 360.0;
|
||||
if pr.b("evolutionOptions/cycleEvolution") {
|
||||
evo = evo.rem_euclid(g("evolutionOptions/cycle", 1.0).round().max(1.0));
|
||||
}
|
||||
let base = if rate > 0.0 { ctx.time * rate } else { 0.0 } + evo;
|
||||
let slot = base.floor() as i64 as u32;
|
||||
let frac = base - base.floor();
|
||||
let tr = effectcraft_effects::vr_rotation(g("target/tilt", 0.0), g("target/pan", 0.0), 0.0);
|
||||
let target = [tr[0][2], tr[1][2], tr[2][2]].map(|v| v as f32);
|
||||
let radius = g("target/radius", 0.0).to_radians() as f32;
|
||||
let feather = g("target/feather", 0.0).to_radians() as f32;
|
||||
per_eye(e, img, layout(ctx), |e, img, eye| {
|
||||
let (w, h) = (img.width as f64, img.height as f64);
|
||||
let bands = complexity.round() as u32;
|
||||
// Per row: horizontal / vertical shift, channel split and scan-line darkening.
|
||||
let mut d = Vec::with_capacity(img.height as usize * 4);
|
||||
for y in 0..img.height as usize {
|
||||
let band = ((y as f64 / h) * bands as f64) as u32;
|
||||
let at = |s: u32| {
|
||||
let r0 = hash_noise(band, s, seed ^ (eye as u32 * 977));
|
||||
let on = r0 < 0.35;
|
||||
let shift = if on { (hash_noise(band, s + 1, seed) as f64 * 2.0 - 1.0) * geo } else { 0.0 };
|
||||
let split = if on { rgb * w * 0.01 * (1.0 + hash_noise(band, s + 2, seed) as f64) } else { 0.0 };
|
||||
(shift, split)
|
||||
};
|
||||
let (s0, p0) = at(slot);
|
||||
let (shift, split) = if frac > 0.0 {
|
||||
let (s1, p1) = at(slot + 1);
|
||||
(s0 + (s1 - s0) * frac, p0 + (p1 - p0) * frac)
|
||||
} else {
|
||||
(s0, p0)
|
||||
};
|
||||
let dark = if y % spacing == 0 { 1.0 - scan * 0.5 } else { 1.0 };
|
||||
d.extend([(shift * w * 0.15 * hdisp) as f32, (shift * h * 0.15 * vdisp) as f32, split as f32, dark]);
|
||||
}
|
||||
let buf = e.data(&d);
|
||||
let mut p = op(3);
|
||||
p.u[0][0] = 3;
|
||||
p.f[0] = [offs[0] as f32, offs[1] as f32, offs[2] as f32, 0.0];
|
||||
p.f[1] = [target[0], target[1], target[2], 0.0];
|
||||
p.f[2] = [radius, feather, 0.0, 0.0];
|
||||
Some(gen_pass(e, &p, img, Some(&buf)))
|
||||
})
|
||||
}
|
||||
|
|
@ -22,10 +22,14 @@
|
|||
//! GPU effects ([`effectcraft_effects::GPU_EFFECTS`]) run as compute kernels with the CPU
|
||||
//! effect's exact steps (padding, box-blur radii, parameter conversions); chains of them are
|
||||
//! uploaded and read back once. Each family lives in its own module with its own WGSL file
|
||||
//! (`fx_color`, `fx_distort`, `fx_extra`, `fx_generate`, `fx_key`, `fx_light`, `fx_noise`,
|
||||
//! `fx_stylize`, `fx_text`, `fx_time`, `fx_tone`, `fx_transition`, `fx_warp`); settings a
|
||||
//! kernel cannot match fall back to the CPU
|
||||
//! (`fx_color`, `fx_depth`, `fx_distort`, `fx_extra`, `fx_generate`, `fx_key`, `fx_light`,
|
||||
//! `fx_lut`, `fx_noise`, `fx_sim`, `fx_stylize`, `fx_text`, `fx_time`, `fx_tone`,
|
||||
//! `fx_transition`, `fx_vr`, `fx_warp`); settings a kernel cannot match fall back to the CPU
|
||||
//! (`catalog::gpu_supported`, or `None` from the family's `apply`).
|
||||
//! The 3D Channel effects read the layer's aux channels as an extra texture (`fx_depth`), the
|
||||
//! colour-management effects interpret colour programs built next to the CPU effects
|
||||
//! (`fx_lut`), and simulations keep their state on the CPU and rasterise its per-frame plan here
|
||||
//! (`fx_sim`).
|
||||
//!
|
||||
//! Working textures come from a pool (`context::Pool`): a frame allocates several full-frame
|
||||
//! RGBA f32 images per layer, and creating and zeroing those cost more than compositing a small
|
||||
|
|
@ -52,17 +56,21 @@ mod context;
|
|||
pub mod deferred;
|
||||
mod effects;
|
||||
mod fx_color;
|
||||
mod fx_depth;
|
||||
mod fx_distort;
|
||||
mod fx_extra;
|
||||
mod fx_generate;
|
||||
mod fx_key;
|
||||
mod fx_light;
|
||||
mod fx_lut;
|
||||
mod fx_noise;
|
||||
mod fx_sim;
|
||||
mod fx_stylize;
|
||||
mod fx_text;
|
||||
mod fx_time;
|
||||
mod fx_tone;
|
||||
mod fx_transition;
|
||||
mod fx_vr;
|
||||
mod fx_warp;
|
||||
mod ops;
|
||||
mod particles;
|
||||
|
|
@ -308,6 +316,8 @@ mod tests_deferred;
|
|||
#[cfg(test)]
|
||||
mod tests_fx_color;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_depth;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_distort;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_extra;
|
||||
|
|
@ -318,8 +328,12 @@ mod tests_fx_key;
|
|||
#[cfg(test)]
|
||||
mod tests_fx_light;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_lut;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_noise;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_sim;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_stylize;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_text;
|
||||
|
|
@ -330,6 +344,8 @@ mod tests_fx_tone;
|
|||
#[cfg(test)]
|
||||
mod tests_fx_transition;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_vr;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_warp;
|
||||
#[cfg(test)]
|
||||
mod tests_particles;
|
||||
|
|
|
|||
|
|
@ -310,6 +310,43 @@ fn fxc_d3(i: u32) -> vec3<f32> {
|
|||
// green, blue × shadows / midtones / highlights), 32..41 wheels (shadows, midtones, highlights),
|
||||
// 41 vignette amount, 42 vignette start, 43 roundness, 44 feather, 45 cx, 46 cy, 47 lw, 48 lh,
|
||||
// 49 aspect.
|
||||
// color3::builtin_look.
|
||||
fn fxc_builtin_look(look: u32, c: vec3<f32>) -> vec3<f32> {
|
||||
let l = luminance(c);
|
||||
switch (look) {
|
||||
case 2u: {
|
||||
return vec3<f32>(c.x * 1.06 + 0.015, c.y * 1.01 + 0.01, c.z * 0.88);
|
||||
}
|
||||
case 3u: {
|
||||
return vec3<f32>(c.x * 0.82, c.y * 0.92, c.z * 1.08 + 0.02) * 0.92;
|
||||
}
|
||||
case 4u: {
|
||||
let d = l + (c - l) * 0.45;
|
||||
var o: vec3<f32>;
|
||||
if (l < 0.5) {
|
||||
o = 2.0 * d * l;
|
||||
} else {
|
||||
o = 1.0 - 2.0 * (1.0 - d) * (1.0 - l);
|
||||
}
|
||||
return d * 0.5 + o * 0.5;
|
||||
}
|
||||
case 5u: {
|
||||
let t = clamp(l - 0.5, -0.5, 0.5);
|
||||
return vec3<f32>(c.x + 0.18 * t, c.y + 0.03 * t, c.z - 0.16 * t);
|
||||
}
|
||||
case 6u: {
|
||||
let v = 0.08 + c * 0.84;
|
||||
return l + (v - l) * 0.8;
|
||||
}
|
||||
case 7u: {
|
||||
return vec3<f32>(l);
|
||||
}
|
||||
default: {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxc_lumetri(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = fxc_pixel(gid);
|
||||
|
|
@ -323,6 +360,9 @@ fn fxc_lumetri(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
return;
|
||||
}
|
||||
var c = fxc_unpremul(px);
|
||||
if (data[51] >= 0.0) {
|
||||
c = fxl_lut_apply(u32(data[51]), c, 2u);
|
||||
}
|
||||
let temp = data[0];
|
||||
let tint = data[1];
|
||||
if (temp != 0.0 || tint != 0.0) {
|
||||
|
|
@ -359,8 +399,15 @@ fn fxc_lumetri(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
let st = fxc_d3(14u);
|
||||
let ht = fxc_d3(17u);
|
||||
let zero = vec3<f32>(0.0);
|
||||
if (intensity != 0.0 && (faded != 0.0 || vib != 0.0 || csat != 1.0 || any(st != zero) || any(ht != zero))) {
|
||||
let look = u32(data[50]);
|
||||
let has_look = look >= 2u || data[52] >= 0.0;
|
||||
if (intensity != 0.0 && (has_look || faded != 0.0 || vib != 0.0 || csat != 1.0 || any(st != zero) || any(ht != zero))) {
|
||||
var d = c;
|
||||
if (data[52] >= 0.0) {
|
||||
d = fxl_lut_apply(u32(data[52]), c, 2u);
|
||||
} else {
|
||||
d = fxc_builtin_look(look, c);
|
||||
}
|
||||
if (faded != 0.0) {
|
||||
d = d * (1.0 - 0.25 * faded) + 0.12 * faded;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,307 @@
|
|||
// ---------------------------------------------------------------- 3D Channel family (fx_depth.rs)
|
||||
//
|
||||
// `aux` planes are buffer-sized resamplings of the layer's auxiliary channels (fxd_lookup); the
|
||||
// alpha of a plane is 1 inside the aux image and 0 outside (where the effect's fill applies).
|
||||
|
||||
fn fxd_unpremul(p: vec4<f32>) -> vec4<f32> {
|
||||
if (p.w > 1e-6) {
|
||||
return vec4<f32>(p.xyz / p.w, p.w);
|
||||
}
|
||||
return vec4<f32>(0.0, 0.0, 0.0, max(p.w, 0.0));
|
||||
}
|
||||
|
||||
// f32::signum (+0 → 1).
|
||||
fn fxd_signum(v: f32) -> f32 {
|
||||
return select(-1.0, 1.0, v >= 0.0);
|
||||
}
|
||||
|
||||
// Nearest aux texel under each buffer pixel (AuxChannels::index_at), `fill` outside.
|
||||
// f[0] = (offset x, offset y, 1 / buffer scale, aux scale); f[1].xy = sub-pixel position; f[2] = fill.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxd_lookup(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let lx = (f32(p.x) + P.f[1].x - P.f[0].x) * P.f[0].z;
|
||||
let ly = (f32(p.y) + P.f[1].y - P.f[0].y) * P.f[0].z;
|
||||
let ax = floor(lx * P.f[0].w);
|
||||
let ay = floor(ly * P.f[0].w);
|
||||
let ad = vec2<f32>(textureDimensions(src));
|
||||
var v = P.f[2];
|
||||
if (ax >= 0.0 && ay >= 0.0 && ax < ad.x && ay < ad.y) {
|
||||
v = textureLoad(src, vec2<i32>(i32(ax), i32(ay)), 0);
|
||||
}
|
||||
textureStore(out, p, v);
|
||||
}
|
||||
|
||||
// 3D Channel Extract's black / white point mapping.
|
||||
fn fxd_lv(v: f32) -> f32 {
|
||||
let bp = P.f[0].x;
|
||||
let wp = P.f[0].y;
|
||||
let clampo = P.u[0].z != 0u;
|
||||
var o: f32;
|
||||
if (wp >= bp) {
|
||||
o = (v - bp) / max(abs(wp - bp), 1e-9) * fxd_signum(wp - bp);
|
||||
if (clampo) {
|
||||
o = clamp(o, 0.0, 1.0);
|
||||
}
|
||||
} else {
|
||||
var q = (v - wp) / max(bp - wp, 1e-9);
|
||||
if (clampo) {
|
||||
q = clamp(q, 0.0, 1.0);
|
||||
}
|
||||
o = 1.0 - q;
|
||||
}
|
||||
if (P.u[0].w != 0u) {
|
||||
o = 1.0 - o;
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
// channel3d::id_color.
|
||||
fn fxd_id_color(id: f32) -> vec3<f32> {
|
||||
if (id == 0.0) {
|
||||
return vec3<f32>(0.0);
|
||||
}
|
||||
let h = hash_noise(bitcast<u32>(id), 0x1du, 0x5eedu);
|
||||
return hsl_to_rgb(h, 0.75, 0.5);
|
||||
}
|
||||
|
||||
// u[0].x = op; the effects' per-pixel steps (src = layer, aux = plane unless noted).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxd_point(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let q = tex_get(aux, p.x, p.y);
|
||||
var o = px;
|
||||
switch (P.u[0].x) {
|
||||
// 3D Channel Extract. u[0] = (op, mode, clamp, invert); f[0] = (black, white).
|
||||
case 0u: {
|
||||
if (q.w == 0.0) {
|
||||
o = vec4<f32>(0.0);
|
||||
} else {
|
||||
var c = q.xyz;
|
||||
switch (P.u[0].y) {
|
||||
case 1u: {
|
||||
c = vec3<f32>(fxd_lv(c.x), fxd_lv(c.y), 0.0);
|
||||
}
|
||||
case 2u: {
|
||||
c = vec3<f32>(fxd_lv(c.x * 0.5 + 0.5), fxd_lv(c.y * 0.5 + 0.5), fxd_lv(c.z * 0.5 + 0.5));
|
||||
}
|
||||
case 4u: {}
|
||||
default: {
|
||||
c = vec3<f32>(fxd_lv(c.x), fxd_lv(c.y), fxd_lv(c.z));
|
||||
}
|
||||
}
|
||||
o = vec4<f32>(c, 1.0);
|
||||
}
|
||||
}
|
||||
// Depth Matte. u[0].y = invert; f[0] = (depth, feather).
|
||||
case 1u: {
|
||||
let zv = q.x;
|
||||
let depth = P.f[0].x;
|
||||
let feather = P.f[0].y;
|
||||
var k: f32;
|
||||
if (feather > 0.0) {
|
||||
k = clamp((zv - depth) / feather + 0.5, 0.0, 1.0);
|
||||
} else {
|
||||
k = select(0.0, 1.0, zv >= depth);
|
||||
}
|
||||
if (P.u[0].y != 0u) {
|
||||
k = 1.0 - k;
|
||||
}
|
||||
o = px * k;
|
||||
}
|
||||
// Fog 3D. u[0] = (op, foggy background, gradient in aux.y); f[0] = (start, end, opacity,
|
||||
// density); f[1] = (fog colour, contribution); f[2].x = background depth.
|
||||
case 2u: {
|
||||
let zv = q.x;
|
||||
let bg = zv >= P.f[2].x * 0.5;
|
||||
let foggy = P.u[0].y != 0u;
|
||||
var f: f32;
|
||||
if (bg) {
|
||||
f = select(0.0, 1.0, foggy);
|
||||
} else {
|
||||
let t = clamp((zv - P.f[0].x) / max(abs(P.f[0].y - P.f[0].x), 1e-6), 0.0, 1.0);
|
||||
f = 1.0 - powz(1.0 - t, 1.0 + P.f[0].w * 3.0);
|
||||
}
|
||||
if (P.u[0].z != 0u) {
|
||||
f *= q.y;
|
||||
}
|
||||
f = clamp(f * P.f[0].z, 0.0, 1.0);
|
||||
let s = fxd_unpremul(px);
|
||||
var al = s.w;
|
||||
if (bg && foggy) {
|
||||
al = s.w + (1.0 - s.w) * f;
|
||||
}
|
||||
let c = s.xyz + (P.f[1].xyz - s.xyz) * f;
|
||||
o = vec4<f32>(c * al, al);
|
||||
}
|
||||
// Fog 3D gradient factor (src = fitted gradient layer, aux = depth plane). f[0].x = contribution.
|
||||
case 3u: {
|
||||
let g = fxd_unpremul(px);
|
||||
let l = luminance(g.xyz) * g.w;
|
||||
o = vec4<f32>(q.x, 1.0 - P.f[0].x + P.f[0].x * l, 0.0, 0.0);
|
||||
}
|
||||
// ID Matte's matte (src = plane). u[0].y = use coverage; f[0].x = ID.
|
||||
case 4u: {
|
||||
var m = 0.0;
|
||||
if (px.w != 0.0 && round_away(px.x) == P.f[0].x) {
|
||||
m = select(1.0, clamp(px.y, 0.0, 1.0), P.u[0].y != 0u);
|
||||
}
|
||||
o = vec4<f32>(m);
|
||||
}
|
||||
// ID Matte applied (aux = blurred matte). u[0].y = invert.
|
||||
case 5u: {
|
||||
var k = q.x;
|
||||
if (P.u[0].y != 0u) {
|
||||
k = 1.0 - k;
|
||||
}
|
||||
o = px * k;
|
||||
}
|
||||
// IDentifier. u[0].y = display; f[0] = (ID, largest ID).
|
||||
case 6u: {
|
||||
let display = P.u[0].y;
|
||||
if (q.w == 0.0) {
|
||||
if (display != 2u) {
|
||||
o = vec4<f32>(0.0);
|
||||
}
|
||||
} else {
|
||||
let id = round_away(q.x);
|
||||
let sel = P.f[0].x;
|
||||
let hit = id == sel;
|
||||
switch (display) {
|
||||
case 0u: {
|
||||
var c = fxd_id_color(id);
|
||||
if (hit && sel != 0.0) {
|
||||
c = vec3<f32>(1.0);
|
||||
}
|
||||
o = vec4<f32>(c, 1.0);
|
||||
}
|
||||
case 1u: {
|
||||
let v = select(0.0, 1.0, hit);
|
||||
o = vec4<f32>(v, v, v, 1.0);
|
||||
}
|
||||
case 2u: {
|
||||
o = select(vec4<f32>(0.0), px, hit);
|
||||
}
|
||||
default: {
|
||||
let v = id / P.f[0].y;
|
||||
o = vec4<f32>(v, v, v, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cryptomatte (aux = (ID colours, selection coverage)). u[0] = (op, display, matte only).
|
||||
case 7u: {
|
||||
let m = q.w;
|
||||
if (P.u[0].z != 0u) {
|
||||
o = vec4<f32>(m);
|
||||
} else if (P.u[0].y == 0u) {
|
||||
o = vec4<f32>(q.xyz * (1.0 - 0.5 * m) + 0.5 * m, 1.0);
|
||||
} else if (P.u[0].y == 1u) {
|
||||
o = px * m;
|
||||
} else {
|
||||
o = vec4<f32>(px.x * (1.0 - 0.5 * m) + 0.5 * m * px.w, px.y * (1.0 - 0.5 * m) + 0.5 * m * px.w, px.z * (1.0 - 0.5 * m), px.w);
|
||||
}
|
||||
}
|
||||
// EXtractoR. u[0] = (op, unmult, clip); u[1] = per-channel source (0..3 own, 4 aux, 5 default);
|
||||
// f[0] = (black, white).
|
||||
case 8u: {
|
||||
let s = fxd_unpremul(px);
|
||||
let straight = vec4<f32>(s.xyz, s.w);
|
||||
var v = vec4<f32>(0.0);
|
||||
for (var k = 0; k < 4; k++) {
|
||||
let kind = P.u[1][k];
|
||||
var x: f32;
|
||||
if (kind == 4u) {
|
||||
x = q[k];
|
||||
} else if (kind < 4u) {
|
||||
x = straight[kind];
|
||||
} else {
|
||||
x = select(0.0, 1.0, k == 3);
|
||||
}
|
||||
if (k < 3) {
|
||||
let bp = P.f[0].x;
|
||||
let wp = P.f[0].y;
|
||||
x = (x - bp) / max(abs(wp - bp), 1e-9) * fxd_signum(wp - bp);
|
||||
if (P.u[0].z != 0u) {
|
||||
x = clamp(x, 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
v[k] = x;
|
||||
}
|
||||
let al = clamp(v.w, 0.0, 1.0);
|
||||
var c = v.xyz;
|
||||
if (P.u[0].y != 0u && al > 1e-6) {
|
||||
c = v.xyz / al;
|
||||
}
|
||||
o = vec4<f32>(c * al, al);
|
||||
}
|
||||
default: {}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// Depth of Field: each pixel's position in the blur stack (src = depth plane).
|
||||
// u[0].x = levels; f[0] = (focal plane, thickness, maximum radius, bias exponent).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxd_dof_f(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let z = textureLoad(src, p, 0).x;
|
||||
let focal = P.f[0].x;
|
||||
let max_r = P.f[0].z;
|
||||
let d = max(abs(z - focal) - P.f[0].y * 0.5, 0.0);
|
||||
let range = max(abs(focal), 1.0);
|
||||
let n = min(d / range, 1.0);
|
||||
let r = max_r * powz(n, P.f[0].w);
|
||||
let top = f32(P.u[0].x - 1u);
|
||||
let f = clamp(r / max_r * top, 0.0, top);
|
||||
textureStore(out, p, vec4<f32>(f, 0.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// Depth of Field: pixels between stack levels k (src) and k + 1 (aux); the positions come
|
||||
// through `data` (rows of RGBA f32). u[0] = (k, row length, levels).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxd_dof_level(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let f = data[(u32(p.y) * P.u[0].y + u32(p.x)) * 4u];
|
||||
let k0 = min(u32(floor(f)), P.u[0].z - 2u);
|
||||
if (k0 != P.u[0].x) {
|
||||
return;
|
||||
}
|
||||
let t = f - f32(k0);
|
||||
let a0 = textureLoad(src, p, 0);
|
||||
let a1 = textureLoad(aux, p, 0);
|
||||
textureStore(out, p, a0 + (a1 - a0) * t);
|
||||
}
|
||||
|
||||
// u[0].x = 0: src + aux; 1: src × 0.25.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxd_avg(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let a = textureLoad(src, p, 0);
|
||||
if (P.u[0].x == 0u) {
|
||||
textureStore(out, p, a + textureLoad(aux, p, 0));
|
||||
} else {
|
||||
textureStore(out, p, a * 0.25);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,474 @@
|
|||
// ---------------------------------------------------------------- colour management (fx_lut.rs)
|
||||
//
|
||||
// A colour program in `data`: opcodes with their arguments, ending in 0, then LUT tables
|
||||
// (layout in fx_lut.rs::push_lut). Every step mirrors effects::ocio / effects::utility.
|
||||
|
||||
fn fxl_d3(i: u32) -> vec3<f32> {
|
||||
return vec3<f32>(data[i], data[i + 1u], data[i + 2u]);
|
||||
}
|
||||
|
||||
// ---- LUTs (utility::Lut)
|
||||
|
||||
fn fxl_lut_norm(o: u32, c: vec3<f32>) -> vec3<f32> {
|
||||
let dmin = fxl_d3(o + 2u);
|
||||
let dmax = fxl_d3(o + 5u);
|
||||
return clamp((c - dmin) / max(dmax - dmin, vec3<f32>(1e-9)), vec3<f32>(0.0), vec3<f32>(1.0));
|
||||
}
|
||||
|
||||
fn fxl_lut_3d(base: u32, n: u32, r: u32, g: u32, b: u32) -> vec3<f32> {
|
||||
return fxl_d3(base + (r + g * n + b * n * n) * 3u);
|
||||
}
|
||||
|
||||
// Lut::apply_interp: 0 nearest, 1 trilinear, otherwise tetrahedral (1D tables linear unless 0).
|
||||
fn fxl_lut_apply(o: u32, c0: vec3<f32>, interp: u32) -> vec3<f32> {
|
||||
let n1 = u32(data[o]);
|
||||
let n3 = u32(data[o + 1u]);
|
||||
let b1 = o + 8u;
|
||||
let b3 = b1 + n1 * 3u;
|
||||
var c = c0;
|
||||
if (n1 > 1u) {
|
||||
let t = fxl_lut_norm(o, c);
|
||||
for (var i = 0u; i < 3u; i++) {
|
||||
let x = t[i] * f32(n1 - 1u);
|
||||
if (interp == 0u) {
|
||||
c[i] = data[b1 + min(u32(round_away(x)), n1 - 1u) * 3u + i];
|
||||
} else {
|
||||
let i0 = min(u32(floor(x)), n1 - 2u);
|
||||
let f = x - f32(i0);
|
||||
let a = data[b1 + i0 * 3u + i];
|
||||
let b = data[b1 + (i0 + 1u) * 3u + i];
|
||||
c[i] = a + (b - a) * f;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (n3 > 1u) {
|
||||
let t = fxl_lut_norm(o, c);
|
||||
let s = f32(n3 - 1u);
|
||||
if (interp == 0u) {
|
||||
let ir = min(u32(round_away(t.x * s)), n3 - 1u);
|
||||
let ig = min(u32(round_away(t.y * s)), n3 - 1u);
|
||||
let ib = min(u32(round_away(t.z * s)), n3 - 1u);
|
||||
return fxl_lut_3d(b3, n3, ir, ig, ib);
|
||||
}
|
||||
let v = t * s;
|
||||
let r = min(u32(floor(v.x)), n3 - 2u);
|
||||
let g = min(u32(floor(v.y)), n3 - 2u);
|
||||
let b = min(u32(floor(v.z)), n3 - 2u);
|
||||
let fr = v.x - f32(r);
|
||||
let fg = v.y - f32(g);
|
||||
let fb = v.z - f32(b);
|
||||
if (interp == 1u) {
|
||||
let c00 = mix_exact(fxl_lut_3d(b3, n3, r, g, b), fxl_lut_3d(b3, n3, r + 1u, g, b), fr);
|
||||
let c10 = mix_exact(fxl_lut_3d(b3, n3, r, g + 1u, b), fxl_lut_3d(b3, n3, r + 1u, g + 1u, b), fr);
|
||||
let c01 = mix_exact(fxl_lut_3d(b3, n3, r, g, b + 1u), fxl_lut_3d(b3, n3, r + 1u, g, b + 1u), fr);
|
||||
let c11 = mix_exact(fxl_lut_3d(b3, n3, r, g + 1u, b + 1u), fxl_lut_3d(b3, n3, r + 1u, g + 1u, b + 1u), fr);
|
||||
return mix_exact(mix_exact(c00, c10, fg), mix_exact(c01, c11, fg), fb);
|
||||
}
|
||||
let c000 = fxl_lut_3d(b3, n3, r, g, b);
|
||||
let c111 = fxl_lut_3d(b3, n3, r + 1u, g + 1u, b + 1u);
|
||||
var w1: f32;
|
||||
var w2: f32;
|
||||
var w3: f32;
|
||||
var p1: vec3<f32>;
|
||||
var p2: vec3<f32>;
|
||||
if (fr > fg) {
|
||||
if (fg > fb) {
|
||||
w1 = fr;
|
||||
w2 = fg;
|
||||
w3 = fb;
|
||||
p1 = fxl_lut_3d(b3, n3, r + 1u, g, b);
|
||||
p2 = fxl_lut_3d(b3, n3, r + 1u, g + 1u, b);
|
||||
} else if (fr > fb) {
|
||||
w1 = fr;
|
||||
w2 = fb;
|
||||
w3 = fg;
|
||||
p1 = fxl_lut_3d(b3, n3, r + 1u, g, b);
|
||||
p2 = fxl_lut_3d(b3, n3, r + 1u, g, b + 1u);
|
||||
} else {
|
||||
w1 = fb;
|
||||
w2 = fr;
|
||||
w3 = fg;
|
||||
p1 = fxl_lut_3d(b3, n3, r, g, b + 1u);
|
||||
p2 = fxl_lut_3d(b3, n3, r + 1u, g, b + 1u);
|
||||
}
|
||||
} else if (fb > fg) {
|
||||
w1 = fb;
|
||||
w2 = fg;
|
||||
w3 = fr;
|
||||
p1 = fxl_lut_3d(b3, n3, r, g, b + 1u);
|
||||
p2 = fxl_lut_3d(b3, n3, r, g + 1u, b + 1u);
|
||||
} else if (fb > fr) {
|
||||
w1 = fg;
|
||||
w2 = fb;
|
||||
w3 = fr;
|
||||
p1 = fxl_lut_3d(b3, n3, r, g + 1u, b);
|
||||
p2 = fxl_lut_3d(b3, n3, r, g + 1u, b + 1u);
|
||||
} else {
|
||||
w1 = fg;
|
||||
w2 = fr;
|
||||
w3 = fb;
|
||||
p1 = fxl_lut_3d(b3, n3, r, g + 1u, b);
|
||||
p2 = fxl_lut_3d(b3, n3, r + 1u, g + 1u, b);
|
||||
}
|
||||
return c000 + w1 * (p1 - c000) + w2 * (p2 - p1) + w3 * (c111 - p2);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
// a + (b - a) t, per component (the CPU's lerp, not WGSL's mix).
|
||||
fn mix_exact(a: vec3<f32>, b: vec3<f32>, t: f32) -> vec3<f32> {
|
||||
return a + (b - a) * t;
|
||||
}
|
||||
|
||||
// ocio::shape over `n` (x, y) points at `at` (`flip`: the curve with x and y swapped).
|
||||
fn fxl_shape(at: u32, n: u32, x: f32, flip: bool) -> f32 {
|
||||
if (n < 2u) {
|
||||
return x;
|
||||
}
|
||||
let kx = select(0u, 1u, flip);
|
||||
let ky = 1u - kx;
|
||||
// partition_point(|p| p.x < x).
|
||||
var lo = 0u;
|
||||
var hi = n;
|
||||
while (lo < hi) {
|
||||
let mid = (lo + hi) / 2u;
|
||||
if (data[at + mid * 2u + kx] < x) {
|
||||
lo = mid + 1u;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
let i = clamp(lo, 1u, n - 1u);
|
||||
let ax = data[at + (i - 1u) * 2u + kx];
|
||||
let ay = data[at + (i - 1u) * 2u + ky];
|
||||
let bx = data[at + i * 2u + kx];
|
||||
let by = data[at + i * 2u + ky];
|
||||
let t = clamp((x - ax) / max(bx - ax, 1e-12), 0.0, 1.0);
|
||||
return ay + (by - ay) * t;
|
||||
}
|
||||
|
||||
// Shaper channel k's points (three counts, then the channels' (x, y) pairs).
|
||||
fn fxl_shaper_at(s: u32, k: u32) -> vec2<u32> {
|
||||
var at = s + 3u;
|
||||
for (var j = 0u; j < k; j++) {
|
||||
at += u32(data[s + j]) * 2u;
|
||||
}
|
||||
return vec2<u32>(at, u32(data[s + k]));
|
||||
}
|
||||
|
||||
fn fxl_file_fwd(o: u32, sh: i32, c0: vec3<f32>, interp: u32) -> vec3<f32> {
|
||||
var c = c0;
|
||||
if (sh >= 0) {
|
||||
for (var k = 0u; k < 3u; k++) {
|
||||
let a = fxl_shaper_at(u32(sh), k);
|
||||
c[k] = fxl_shape(a.x, a.y, c[k], false);
|
||||
}
|
||||
}
|
||||
return fxl_lut_apply(o, c, interp);
|
||||
}
|
||||
|
||||
// The inverse of a 1D table channel (ocio::FileXform::apply: points (table, domain)).
|
||||
fn fxl_inv_1d(o: u32, i: u32, y: f32) -> f32 {
|
||||
let n = u32(data[o]);
|
||||
let dmin = data[o + 2u + i];
|
||||
let dmax = data[o + 5u + i];
|
||||
let b1 = o + 8u;
|
||||
// partition_point over the table's values.
|
||||
var lo = 0u;
|
||||
var hi = n;
|
||||
while (lo < hi) {
|
||||
let mid = (lo + hi) / 2u;
|
||||
if (data[b1 + mid * 3u + i] < y) {
|
||||
lo = mid + 1u;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
let k = clamp(lo, 1u, n - 1u);
|
||||
let ax = data[b1 + (k - 1u) * 3u + i];
|
||||
let bx = data[b1 + k * 3u + i];
|
||||
let ay = dmin + (dmax - dmin) * f32(k - 1u) / f32(n - 1u);
|
||||
let by = dmin + (dmax - dmin) * f32(k) / f32(n - 1u);
|
||||
let t = clamp((y - ax) / max(bx - ax, 1e-12), 0.0, 1.0);
|
||||
return ay + (by - ay) * t;
|
||||
}
|
||||
|
||||
fn fxl_file(o: u32, sh: i32, c: vec3<f32>, interp: u32, inverse: bool) -> vec3<f32> {
|
||||
if (!inverse) {
|
||||
return fxl_file_fwd(o, sh, c, interp);
|
||||
}
|
||||
let n1 = u32(data[o]);
|
||||
let n3 = u32(data[o + 1u]);
|
||||
if (n3 == 0u) {
|
||||
var x = c;
|
||||
if (n1 > 1u) {
|
||||
x = vec3<f32>(fxl_inv_1d(o, 0u, c.x), fxl_inv_1d(o, 1u, c.y), fxl_inv_1d(o, 2u, c.z));
|
||||
}
|
||||
if (sh >= 0) {
|
||||
for (var k = 0u; k < 3u; k++) {
|
||||
let a = fxl_shaper_at(u32(sh), k);
|
||||
x[k] = fxl_shape(a.x, a.y, x[k], true);
|
||||
}
|
||||
}
|
||||
return x;
|
||||
}
|
||||
// Damped fixed point from the target, as the CPU.
|
||||
var x = c;
|
||||
for (var it = 0; it < 40; it++) {
|
||||
let y = fxl_file_fwd(o, sh, x, interp);
|
||||
let e = c - y;
|
||||
if (all(abs(e) < vec3<f32>(1e-6))) {
|
||||
break;
|
||||
}
|
||||
x = x + e;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
// ---- transfer functions (ocio::Tf)
|
||||
|
||||
const FXL_PQ_M1: f32 = 0.1593017578125;
|
||||
const FXL_PQ_M2: f32 = 78.84375;
|
||||
const FXL_PQ_C1: f32 = 0.8359375;
|
||||
const FXL_PQ_C2: f32 = 18.8515625;
|
||||
const FXL_PQ_C3: f32 = 18.6875;
|
||||
|
||||
fn fxl_copysign(m: f32, v: f32) -> f32 {
|
||||
return select(abs(m), -abs(m), v < 0.0 || (v == 0.0 && bitcast<u32>(v) != 0u));
|
||||
}
|
||||
|
||||
fn fxl_decode(tf: u32, g: f32, v: f32) -> f32 {
|
||||
switch (tf) {
|
||||
case 1u: {
|
||||
let a = abs(v);
|
||||
var l: f32;
|
||||
if (a <= 0.04045) {
|
||||
l = a / 12.92;
|
||||
} else {
|
||||
l = pow((a + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
return fxl_copysign(l, v);
|
||||
}
|
||||
case 2u: {
|
||||
return fxl_copysign(powz(abs(v), g), v);
|
||||
}
|
||||
case 3u: {
|
||||
if (v <= 0.155251141552511) {
|
||||
return (v - 0.0729055341958355) / 10.5402377416545;
|
||||
}
|
||||
if (v < (log2(65504.0) + 9.72) / 17.52) {
|
||||
return exp2(v * 17.52 - 9.72);
|
||||
}
|
||||
return 65504.0;
|
||||
}
|
||||
case 4u: {
|
||||
let e = powz(max(v, 0.0), 1.0 / FXL_PQ_M2);
|
||||
let l = powz(max(e - FXL_PQ_C1, 0.0) / (FXL_PQ_C2 - FXL_PQ_C3 * e), 1.0 / FXL_PQ_M1);
|
||||
return l * 100.0;
|
||||
}
|
||||
default: {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fxl_encode(tf: u32, g: f32, v: f32) -> f32 {
|
||||
switch (tf) {
|
||||
case 1u: {
|
||||
let a = abs(v);
|
||||
var e: f32;
|
||||
if (a <= 0.0031308) {
|
||||
e = a * 12.92;
|
||||
} else {
|
||||
e = 1.055 * pow(a, 1.0 / 2.4) - 0.055;
|
||||
}
|
||||
return fxl_copysign(e, v);
|
||||
}
|
||||
case 2u: {
|
||||
return fxl_copysign(powz(abs(v), 1.0 / g), v);
|
||||
}
|
||||
case 3u: {
|
||||
if (v <= 0.0078125) {
|
||||
return 10.5402377416545 * v + 0.0729055341958355;
|
||||
}
|
||||
return (log2(v) + 9.72) / 17.52;
|
||||
}
|
||||
case 4u: {
|
||||
let y = powz(max(v / 100.0, 0.0), FXL_PQ_M1);
|
||||
return powz((FXL_PQ_C1 + FXL_PQ_C2 * y) / (1.0 + FXL_PQ_C3 * y), FXL_PQ_M2);
|
||||
}
|
||||
default: {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- ASC CDL (ocio::Cdl), args at `a`: slope (3), offset (3), power (3), sat, clamp, inverse.
|
||||
|
||||
const FXL_LUMA: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);
|
||||
|
||||
fn fxl_cdl(a: u32, c0: vec3<f32>) -> vec3<f32> {
|
||||
let slope = fxl_d3(a);
|
||||
let offset = fxl_d3(a + 3u);
|
||||
let power = fxl_d3(a + 6u);
|
||||
let sat = data[a + 9u];
|
||||
let clampo = data[a + 10u] != 0.0;
|
||||
let zero = vec3<f32>(0.0);
|
||||
let one = vec3<f32>(1.0);
|
||||
if (data[a + 11u] == 0.0) {
|
||||
var o = vec3<f32>(0.0);
|
||||
for (var i = 0u; i < 3u; i++) {
|
||||
let v = c0[i] * slope[i] + offset[i];
|
||||
if (clampo) {
|
||||
o[i] = powz(clamp(v, 0.0, 1.0), power[i]);
|
||||
} else if (v >= 0.0) {
|
||||
o[i] = powz(v, power[i]);
|
||||
} else {
|
||||
o[i] = v;
|
||||
}
|
||||
}
|
||||
let l = o.x * FXL_LUMA.x + o.y * FXL_LUMA.y + o.z * FXL_LUMA.z;
|
||||
o = l + sat * (o - l);
|
||||
if (clampo) {
|
||||
o = clamp(o, zero, one);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
var c = c0;
|
||||
if (clampo) {
|
||||
c = clamp(c, zero, one);
|
||||
}
|
||||
let l = c.x * FXL_LUMA.x + c.y * FXL_LUMA.y + c.z * FXL_LUMA.z;
|
||||
var s = sat;
|
||||
if (abs(s) < 1e-9) {
|
||||
s = 1e-9;
|
||||
}
|
||||
let o = l + (c - l) / s;
|
||||
var r = vec3<f32>(0.0);
|
||||
for (var i = 0u; i < 3u; i++) {
|
||||
var v = o[i];
|
||||
if (clampo) {
|
||||
v = clamp(v, 0.0, 1.0);
|
||||
}
|
||||
let pw = max(power[i], 1e-9);
|
||||
var u = v;
|
||||
if (v >= 0.0) {
|
||||
u = powz(v, 1.0 / pw);
|
||||
}
|
||||
var sl = slope[i];
|
||||
if (abs(sl) < 1e-9) {
|
||||
sl = 1e-9;
|
||||
}
|
||||
r[i] = (u - offset[i]) / sl;
|
||||
}
|
||||
if (clampo) {
|
||||
r = clamp(r, zero, one);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
fn fxl_tonemap(v: f32, inverse: bool) -> f32 {
|
||||
if (!inverse) {
|
||||
if (v <= 0.0) {
|
||||
return v;
|
||||
}
|
||||
return v * (1.0 + v / 256.0) / (1.0 + v);
|
||||
}
|
||||
if (v <= 0.0) {
|
||||
return v;
|
||||
}
|
||||
let a = 1.0 / 256.0;
|
||||
let b = 1.0 - v;
|
||||
let c = -v;
|
||||
return (-b + sqrt(b * b - 4.0 * a * c)) / (2.0 * a);
|
||||
}
|
||||
|
||||
// Run the colour program at data[0..] on a straight colour.
|
||||
fn fxl_run(c0: vec3<f32>) -> vec3<f32> {
|
||||
var c = c0;
|
||||
var pc = 0u;
|
||||
for (var guard = 0; guard < 256; guard++) {
|
||||
let op = u32(data[pc]);
|
||||
switch (op) {
|
||||
case 1u, 2u: {
|
||||
let tf = u32(data[pc + 1u]);
|
||||
let g = data[pc + 2u];
|
||||
for (var i = 0u; i < 3u; i++) {
|
||||
if (op == 1u) {
|
||||
c[i] = fxl_decode(tf, g, c[i]);
|
||||
} else {
|
||||
c[i] = fxl_encode(tf, g, c[i]);
|
||||
}
|
||||
}
|
||||
pc += 3u;
|
||||
}
|
||||
case 3u: {
|
||||
let r0 = fxl_d3(pc + 1u);
|
||||
let r1 = fxl_d3(pc + 4u);
|
||||
let r2 = fxl_d3(pc + 7u);
|
||||
c = vec3<f32>(r0.x * c.x + r0.y * c.y + r0.z * c.z, r1.x * c.x + r1.y * c.y + r1.z * c.z, r2.x * c.x + r2.y * c.y + r2.z * c.z);
|
||||
pc += 10u;
|
||||
}
|
||||
case 4u: {
|
||||
c = fxl_cdl(pc + 1u, c);
|
||||
pc += 13u;
|
||||
}
|
||||
case 5u: {
|
||||
let inv = data[pc + 1u] != 0.0;
|
||||
c = vec3<f32>(fxl_tonemap(c.x, inv), fxl_tonemap(c.y, inv), fxl_tonemap(c.z, inv));
|
||||
pc += 2u;
|
||||
}
|
||||
case 6u: {
|
||||
let luma = fxl_d3(pc + 1u);
|
||||
let l = c.x * luma.x + c.y * luma.y + c.z * luma.z;
|
||||
if (l <= 0.0) {
|
||||
c = max(c, vec3<f32>(0.0));
|
||||
} else {
|
||||
let mn = min(min(c.x, c.y), c.z);
|
||||
if (mn < 0.0) {
|
||||
let t = clamp(l / (l - mn), 0.0, 1.0);
|
||||
c = l + (c - l) * t;
|
||||
}
|
||||
}
|
||||
pc += 4u;
|
||||
}
|
||||
case 7u: {
|
||||
c = max(c, vec3<f32>(0.0));
|
||||
pc += 1u;
|
||||
}
|
||||
case 8u: {
|
||||
let o = u32(data[pc + 1u]);
|
||||
let interp = u32(data[pc + 2u]);
|
||||
let inv = data[pc + 3u] != 0.0;
|
||||
let sh = i32(data[pc + 4u]);
|
||||
c = fxl_file(o, sh, c, interp, inv);
|
||||
pc += 5u;
|
||||
}
|
||||
default: {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
// u[0].x = 0: OCIO pixels (unpremul); 1: colour ÷ alpha (Image::map_straight).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxl_point(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let a = px.w;
|
||||
if (a <= 0.0) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
var c = px.xyz / a;
|
||||
if (P.u[0].x == 0u && a <= 1e-6) {
|
||||
c = vec3<f32>(0.0);
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(fxl_run(c) * a, a));
|
||||
}
|
||||
|
|
@ -0,0 +1,653 @@
|
|||
// ---------------------------------------------------------------- simulation render passes (fx_sim.rs)
|
||||
//
|
||||
// Items (sprites, CC Bubbles bubbles, Mr. Mercury blobs, textured pieces) are binned into 16 × 16
|
||||
// tiles: data = tile offsets, item indices, then the item records (u[0].w floats each, the last
|
||||
// four the item's integer pixel bounds). Every shading step mirrors effects::sim / sim2.
|
||||
|
||||
fn fxm_unpremul(p: vec4<f32>) -> vec4<f32> {
|
||||
if (p.w > 1e-6) {
|
||||
return vec4<f32>(p.xyz / p.w, p.w);
|
||||
}
|
||||
return vec4<f32>(0.0, 0.0, 0.0, max(p.w, 0.0));
|
||||
}
|
||||
|
||||
fn fxm_aa(edge: f32) -> f32 {
|
||||
return clamp(edge + 0.5, 0.0, 1.0);
|
||||
}
|
||||
|
||||
struct FxmShade {
|
||||
ok: bool,
|
||||
c: vec4<f32>,
|
||||
}
|
||||
|
||||
fn fxm_none() -> FxmShade {
|
||||
return FxmShade(false, vec4<f32>(0.0));
|
||||
}
|
||||
|
||||
// sim::sprite_shade, after resolving the sprite's tint (sim::SpritePlan::resolve) against `src`.
|
||||
fn fxm_sprite(at: u32, x: f32, y: f32) -> FxmShade {
|
||||
let kind = u32(data[at]);
|
||||
let sx = data[at + 1u];
|
||||
let sy = data[at + 2u];
|
||||
let r0 = data[at + 3u];
|
||||
var c = vec4<f32>(data[at + 4u], data[at + 5u], data[at + 6u], data[at + 7u]);
|
||||
let tk = u32(data[at + 11u]);
|
||||
if (tk != 0u) {
|
||||
let smp = sample_bilinear(src, data[at + 12u], data[at + 13u]);
|
||||
let s = fxm_unpremul(smp);
|
||||
if (tk == 1u) {
|
||||
if (s.w > 0.0) {
|
||||
c = vec4<f32>((s.xyz * 0.6 + 0.5) * c.xyz, c.w);
|
||||
}
|
||||
} else if (tk == 2u) {
|
||||
if (s.w <= 0.0) {
|
||||
c = vec4<f32>(0.0);
|
||||
} else {
|
||||
c = vec4<f32>(s.xyz, s.w * c.w);
|
||||
}
|
||||
} else {
|
||||
if (smp.w < 0.5) {
|
||||
c = vec4<f32>(0.0);
|
||||
} else {
|
||||
let hair = vec3<f32>(data[at + 14u], data[at + 15u], data[at + 16u]);
|
||||
let inherit = data[at + 17u];
|
||||
let base = s.xyz + (hair - s.xyz) * (1.0 - inherit);
|
||||
c = vec4<f32>(base * data[at + 18u] + data[at + 19u], c.w);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (c.w <= 1e-5) {
|
||||
return fxm_none();
|
||||
}
|
||||
var r = r0;
|
||||
var fade = 1.0;
|
||||
if (r0 < 0.5) {
|
||||
r = 0.5;
|
||||
fade = r0 / 0.5;
|
||||
}
|
||||
let dx = x - sx;
|
||||
let dy = y - sy;
|
||||
var rgb = c.xyz;
|
||||
var cov = 0.0;
|
||||
switch (kind) {
|
||||
case 0u: {
|
||||
let d2 = (dx * dx + dy * dy) / (r * r);
|
||||
if (d2 >= 1.0) {
|
||||
return fxm_none();
|
||||
}
|
||||
let q = 1.0 - d2;
|
||||
cov = q * q;
|
||||
}
|
||||
case 1u: {
|
||||
cov = fxm_aa(r - sqrt(dx * dx + dy * dy));
|
||||
}
|
||||
case 2u: {
|
||||
let d = sqrt(dx * dx + dy * dy);
|
||||
let cv = fxm_aa(r - d);
|
||||
if (cv <= 0.0) {
|
||||
return fxm_none();
|
||||
}
|
||||
let nx = dx / r;
|
||||
let ny = dy / r;
|
||||
let nz = sqrt(max(1.0 - nx * nx - ny * ny, 0.0));
|
||||
let diff = max(nx * -0.48 + ny * -0.58 + nz * 0.66, 0.0);
|
||||
let rz = 2.0 * diff * nz - 0.66;
|
||||
let spec = powz(max(rz, 0.0), 20.0) * 0.6;
|
||||
let k = 0.2 + 0.8 * diff;
|
||||
rgb = rgb * k + spec;
|
||||
cov = cv;
|
||||
}
|
||||
case 3u: {
|
||||
let d = sqrt(dx * dx + dy * dy) / r;
|
||||
cov = powz(max(1.0 - d, 0.0), 1.5);
|
||||
}
|
||||
case 4u: {
|
||||
let d = sqrt(dx * dx + dy * dy) / r;
|
||||
if (d > 1.0 + 1.0 / r) {
|
||||
return fxm_none();
|
||||
}
|
||||
let rr = (d - 0.9) / 0.12;
|
||||
let ring = max(1.0 - rr * rr, 0.0);
|
||||
let hx = dx / r + 0.4;
|
||||
let hy = dy / r + 0.4;
|
||||
let hl = max(1.0 - (hx * hx + hy * hy) / 0.04, 0.0);
|
||||
cov = min(0.12 * select(0.0, 1.0, d <= 1.0) + ring * 0.8 + hl, 1.0);
|
||||
}
|
||||
case 5u: {
|
||||
let sn = sin(data[at + 8u]);
|
||||
let cs = cos(data[at + 8u]);
|
||||
let ux = (dx * cs + dy * sn) / r;
|
||||
let uy = (-dx * sn + dy * cs) / r;
|
||||
let d = sqrt(ux * ux + uy * uy);
|
||||
let core = max(1.0 - d, 0.0) * max(1.0 - d, 0.0);
|
||||
let stx = max(1.0 - abs(ux) / 2.0, 0.0) * max(1.0 - abs(uy) * 6.0, 0.0);
|
||||
let sty = max(1.0 - abs(uy) / 2.0, 0.0) * max(1.0 - abs(ux) * 6.0, 0.0);
|
||||
cov = min(core + stx + sty, 1.0);
|
||||
}
|
||||
case 6u: {
|
||||
let sn = sin(data[at + 8u]);
|
||||
let cs = cos(data[at + 8u]);
|
||||
let ux = dx * cs + dy * sn;
|
||||
let uy = -dx * sn + dy * cs;
|
||||
let e = r * 0.75;
|
||||
cov = fxm_aa(e - abs(ux)) * fxm_aa(e - abs(uy));
|
||||
}
|
||||
case 7u: {
|
||||
let sn = sin(data[at + 8u]);
|
||||
let cs = cos(data[at + 8u]);
|
||||
let ux = dx * cs + dy * sn;
|
||||
let uy = -dx * sn + dy * cs;
|
||||
let e = r * 0.5;
|
||||
var m = 3.4e38;
|
||||
for (var k = 0; k < 3; k++) {
|
||||
let a = 1.5707964 + f32(k) * 6.2831855 / 3.0;
|
||||
m = min(m, e - (ux * cos(a) + uy * sin(a)));
|
||||
}
|
||||
cov = fxm_aa(m);
|
||||
}
|
||||
default: {
|
||||
let lx = data[at + 9u];
|
||||
let ly = data[at + 10u];
|
||||
let l2 = lx * lx + ly * ly;
|
||||
var t = 0.0;
|
||||
if (l2 > 0.0) {
|
||||
t = clamp((dx * lx + dy * ly) / l2, 0.0, 1.0);
|
||||
}
|
||||
let px = dx - lx * t;
|
||||
let py = dy - ly * t;
|
||||
cov = fxm_aa(r - sqrt(px * px + py * py));
|
||||
}
|
||||
}
|
||||
let a = cov * c.w * fade;
|
||||
if (a <= 1e-6) {
|
||||
return fxm_none();
|
||||
}
|
||||
return FxmShade(true, vec4<f32>(rgb * a, a));
|
||||
}
|
||||
|
||||
// CC Bubbles' bubble (sim::bubbles). u[1].y = metal, u[1].z = shading type.
|
||||
fn fxm_bubble(at: u32, x: f32, y: f32) -> FxmShade {
|
||||
let qx = data[at + 1u];
|
||||
let qy = data[at + 2u];
|
||||
let qr = data[at + 3u];
|
||||
let dx = (x - qx) / max(qr, 0.5);
|
||||
let dy = (y - qy) / max(qr, 0.5);
|
||||
let d2 = dx * dx + dy * dy;
|
||||
let d = sqrt(d2);
|
||||
let cov = fxm_aa((1.0 - d) * qr);
|
||||
if (cov <= 0.0) {
|
||||
return fxm_none();
|
||||
}
|
||||
let k = select(-0.7, 1.6, P.u[1].y != 0u);
|
||||
let nz = sqrt(max(1.0 - d2, 0.0));
|
||||
let s = fxm_unpremul(sample_bilinear(src, data[at + 4u] + dx * qr * k, data[at + 5u] + dy * qr * k));
|
||||
var c = s.xyz;
|
||||
if (s.w <= 0.0) {
|
||||
c = vec3<f32>(0.6, 0.7, 0.8);
|
||||
}
|
||||
var a = cov;
|
||||
switch (P.u[1].z) {
|
||||
case 1u: {
|
||||
c = c * 0.7 + 0.3 * (1.0 - nz);
|
||||
}
|
||||
case 2u: {
|
||||
c = c * (0.5 + 0.5 * nz);
|
||||
}
|
||||
case 3u: {
|
||||
a *= powz(nz, 0.5);
|
||||
}
|
||||
case 4u: {
|
||||
a *= 1.0 - nz * 0.8;
|
||||
}
|
||||
default: {}
|
||||
}
|
||||
let hx = dx + 0.35;
|
||||
let hy = dy + 0.35;
|
||||
let hl = max(1.0 - (hx * hx + hy * hy) / 0.05, 0.0) * 0.8;
|
||||
c = c + hl;
|
||||
return FxmShade(true, vec4<f32>(c * a, a));
|
||||
}
|
||||
|
||||
// Mr. Mercury's blob field (f, ∂f/∂x, ∂f/∂y). f[0].x = 1 + influence.
|
||||
fn fxm_blob(at: u32, x: f32, y: f32) -> FxmShade {
|
||||
let dx = x - data[at + 1u];
|
||||
let dy = y - data[at + 2u];
|
||||
let rr = data[at + 3u] * 2.0 * P.f[0].x;
|
||||
let d2 = (dx * dx + dy * dy) / (rr * rr);
|
||||
if (d2 >= 1.0) {
|
||||
return fxm_none();
|
||||
}
|
||||
let k = 1.0 - d2;
|
||||
let g = -6.0 * k * k / (rr * rr);
|
||||
return FxmShade(true, vec4<f32>(k * k * k, g * dx, g * dy, 0.0));
|
||||
}
|
||||
|
||||
// A textured piece (sim2::draw_pieces): src = front texture, aux = back (u[1].y = 1 when set).
|
||||
fn fxm_piece(at: u32, x: f32, y: f32) -> FxmShade {
|
||||
let m0 = vec3<f32>(data[at + 1u], data[at + 2u], data[at + 3u]);
|
||||
let m1 = vec3<f32>(data[at + 4u], data[at + 5u], data[at + 6u]);
|
||||
let m2 = vec3<f32>(data[at + 7u], data[at + 8u], data[at + 9u]);
|
||||
let w = m2.x * x + m2.y * y + m2.z;
|
||||
if (abs(w) < 1e-12) {
|
||||
return fxm_none();
|
||||
}
|
||||
let c0 = vec2<f32>(data[at + 10u], data[at + 11u]);
|
||||
var u = (m0.x * x + m0.y * y + m0.z) / w + c0.x;
|
||||
var v = (m1.x * x + m1.y * y + m1.z) / w + c0.y;
|
||||
let n = u32(data[at + 24u]);
|
||||
var md = 3.4e38;
|
||||
for (var i = 0u; i < n; i++) {
|
||||
let j = (i + 1u) % n;
|
||||
let ax = data[at + 12u + i * 2u];
|
||||
let ay = data[at + 13u + i * 2u];
|
||||
let ex = data[at + 12u + j * 2u] - ax;
|
||||
let ey = data[at + 13u + j * 2u] - ay;
|
||||
let l = max(sqrt(ex * ex + ey * ey), 1e-6);
|
||||
md = min(md, (ex * (v - ay) - ey * (u - ax)) / l);
|
||||
}
|
||||
let cov = clamp(md + 1.0, 0.0, 1.0);
|
||||
if (cov <= 0.0) {
|
||||
return fxm_none();
|
||||
}
|
||||
var c: vec3<f32>;
|
||||
var a = 1.0;
|
||||
if (data[at + 33u] != 0.0) {
|
||||
c = vec3<f32>(data[at + 34u], data[at + 35u], data[at + 36u]);
|
||||
} else {
|
||||
let back = data[at + 37u] != 0.0;
|
||||
let mirror = u32(data[at + 38u]);
|
||||
if (back && mirror == 1u) {
|
||||
v = 2.0 * c0.y - v;
|
||||
} else if (back && mirror == 2u) {
|
||||
u = 2.0 * c0.x - u;
|
||||
}
|
||||
var t: vec4<f32>;
|
||||
if (back && P.u[1].y != 0u) {
|
||||
t = sample_bilinear(aux, u, v);
|
||||
} else {
|
||||
t = sample_bilinear(src, u, v);
|
||||
}
|
||||
let s = fxm_unpremul(t);
|
||||
c = s.xyz;
|
||||
a = s.w;
|
||||
}
|
||||
a = a * cov * data[at + 32u];
|
||||
if (a <= 1e-6) {
|
||||
return FxmShade(false, vec4<f32>(0.0));
|
||||
}
|
||||
let tint = vec3<f32>(data[at + 26u], data[at + 27u], data[at + 28u]);
|
||||
let spec = vec3<f32>(data[at + 29u], data[at + 30u], data[at + 31u]);
|
||||
let o = c * data[at + 25u] * tint + spec;
|
||||
return FxmShade(true, vec4<f32>(o * a, a));
|
||||
}
|
||||
|
||||
// sim::raster over a transparent frame. u[0] = (kind: 0 sprites, 1 bubbles, 2 blob field,
|
||||
// 3 pieces; additive; items offset; item stride); u[1].x = tiles across.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxm_raster(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let tile = u32(p.y / 16) * P.u[1].x + u32(p.x / 16);
|
||||
let start = u32(data[tile]);
|
||||
let end = u32(data[tile + 1u]);
|
||||
let stride = P.u[0].w;
|
||||
let add = P.u[0].y != 0u;
|
||||
let x = f32(p.x) + 0.5;
|
||||
let y = f32(p.y) + 0.5;
|
||||
var d = vec4<f32>(0.0);
|
||||
for (var k = start; k < end; k++) {
|
||||
let at = P.u[0].z + u32(data[k]) * stride;
|
||||
let bb = at + stride - 4u;
|
||||
if (f32(p.x) < data[bb] || f32(p.y) < data[bb + 1u] || f32(p.x) > data[bb + 2u] || f32(p.y) > data[bb + 3u]) {
|
||||
continue;
|
||||
}
|
||||
var s: FxmShade;
|
||||
switch (P.u[0].x) {
|
||||
case 0u: {
|
||||
s = fxm_sprite(at, x, y);
|
||||
}
|
||||
case 1u: {
|
||||
s = fxm_bubble(at, x, y);
|
||||
}
|
||||
case 2u: {
|
||||
s = fxm_blob(at, x, y);
|
||||
}
|
||||
default: {
|
||||
s = fxm_piece(at, x, y);
|
||||
}
|
||||
}
|
||||
if (!s.ok) {
|
||||
continue;
|
||||
}
|
||||
let c = s.c;
|
||||
if (add) {
|
||||
d = vec4<f32>(d.xyz + c.xyz, min(d.w + c.w, 1.0));
|
||||
} else {
|
||||
d = c + d * (1.0 - c.w);
|
||||
}
|
||||
}
|
||||
textureStore(out, p, d);
|
||||
}
|
||||
|
||||
// The sprite layer (aux) meets the original (src). u[0].x = 0: sim::combine with mode u[0].y;
|
||||
// 1: mixed toward the original by f[0].x; 2: over opaque black.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxm_post(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let s = textureLoad(src, p, 0);
|
||||
let f = textureLoad(aux, p, 0);
|
||||
var o = f;
|
||||
if (P.u[0].x == 1u) {
|
||||
o = f + (s - f) * P.f[0].x;
|
||||
} else if (P.u[0].x == 2u) {
|
||||
o = vec4<f32>(f.xyz, f.w + (1.0 - f.w));
|
||||
} else {
|
||||
switch (P.u[0].y) {
|
||||
case 0u: {
|
||||
o = f + s * (1.0 - f.w);
|
||||
}
|
||||
case 1u: {
|
||||
o = s + f - s * f;
|
||||
}
|
||||
case 2u: {
|
||||
o = vec4<f32>(s.xyz + f.xyz, min(s.w + f.w, 1.0));
|
||||
}
|
||||
case 3u: {
|
||||
o = vec4<f32>(max(s.xyz, f.xyz), s.w + f.w - s.w * f.w);
|
||||
}
|
||||
case 4u: {
|
||||
let fu = fxm_unpremul(f);
|
||||
let m = s.xyz - (s.xyz - min(s.xyz, fu.xyz * s.w)) * fu.w;
|
||||
o = vec4<f32>(m, s.w);
|
||||
}
|
||||
default: {}
|
||||
}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
fn fxm_norm3(v: vec3<f32>) -> vec3<f32> {
|
||||
let l = sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
|
||||
if (l > 1e-9) {
|
||||
return v / l;
|
||||
}
|
||||
return vec3<f32>(0.0, -1.0, 0.0);
|
||||
}
|
||||
|
||||
// Mr. Mercury's shading (src = layer, aux = blob field). f[0] = (ambient, diffuse, specular,
|
||||
// roughness); f[1] = (metal, light intensity, render scale); f[2] = light direction; f[3] = colour.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxm_mercury(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let fv = textureLoad(aux, p, 0);
|
||||
let f = fv.x;
|
||||
let thr = 0.35;
|
||||
if (f <= thr * 0.8) {
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
return;
|
||||
}
|
||||
let cov = clamp((f - thr * 0.8) / (thr * 0.4), 0.0, 1.0);
|
||||
let n = fxm_norm3(vec3<f32>(fv.y * 3.0, fv.z * 3.0, 1.0 / (1.0 + f)));
|
||||
let s = P.f[1].z;
|
||||
let smp = fxm_unpremul(sample_bilinear_clamped(src, f32(p.x) + 0.5 - n.x * 12.0 * s, f32(p.y) + 0.5 - n.y * 12.0 * s));
|
||||
var base = smp.xyz;
|
||||
if (smp.w <= 0.0) {
|
||||
base = vec3<f32>(0.7, 0.7, 0.75);
|
||||
}
|
||||
let l = P.f[2].xyz;
|
||||
let nl = max(dot(n, l), 0.0);
|
||||
let hv = fxm_norm3(vec3<f32>(l.x, l.y, l.z + 1.0));
|
||||
let li = P.f[1].y;
|
||||
let spec = powz(max(dot(n, hv), 0.0), 1.0 / P.f[0].w) * P.f[0].z * li;
|
||||
let shade = P.f[0].x + P.f[0].y * nl * li;
|
||||
let metal = P.f[1].x;
|
||||
let cc = base * shade + spec * (P.f[3].xyz * (1.0 - metal) + base * metal);
|
||||
textureStore(out, p, vec4<f32>(cc * cov, cov));
|
||||
}
|
||||
|
||||
// CC Drizzle's ripple height at (x, y): data = drops (x, y, ring radius, amplitude).
|
||||
fn fxm_ripples(x: f32, y: f32) -> f32 {
|
||||
var v = 0.0;
|
||||
for (var i = 0u; i < P.u[0].x; i++) {
|
||||
let dx = data[i * 4u];
|
||||
let dy = data[i * 4u + 1u];
|
||||
let ex = x - dx;
|
||||
let ey = y - dy;
|
||||
let d = sqrt(ex * ex + ey * ey);
|
||||
let r = data[i * 4u + 2u];
|
||||
let u = (d - r) / P.f[0].y;
|
||||
if (abs(u) < 1.0) {
|
||||
let env = 0.5 + 0.5 * cos(u * 3.1415927);
|
||||
v += data[i * 4u + 3u] * env * cos((d - r) / P.f[0].x * 6.2831855);
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// CC Drizzle. u[0].x = drops; f[0] = (wavelength, ring width, displacement, roughness);
|
||||
// f[1] = (light direction, intensity); f[2] = (half vector, specular); f[3] = light colour;
|
||||
// f[4] = (ambient, diffuse).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxm_drizzle(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let fx = f32(p.x) + 0.5;
|
||||
let fy = f32(p.y) + 0.5;
|
||||
let gx = fxm_ripples(fx + 1.0, fy) - fxm_ripples(fx - 1.0, fy);
|
||||
let gy = fxm_ripples(fx, fy + 1.0) - fxm_ripples(fx, fy - 1.0);
|
||||
if (gx == 0.0 && gy == 0.0) {
|
||||
textureStore(out, p, textureLoad(src, p, 0));
|
||||
return;
|
||||
}
|
||||
let smp = sample_bilinear_clamped(src, fx - gx * P.f[0].z, fy - gy * P.f[0].z);
|
||||
let v = vec3<f32>(-gx * 6.0, -gy * 6.0, 1.0);
|
||||
let n = v / sqrt(v.x * v.x + v.y * v.y + 1.0);
|
||||
let l = P.f[1].xyz;
|
||||
let li = P.f[1].w;
|
||||
let nl = max(dot(n, l), 0.0);
|
||||
let sp = powz(max(dot(n, P.f[2].xyz), 0.0), 1.0 / P.f[0].w) * P.f[2].w * li;
|
||||
let shade = 1.0 + (P.f[4].x + P.f[4].y * nl - P.f[4].y * l.z) * li;
|
||||
let a = smp.w;
|
||||
textureStore(out, p, vec4<f32>(smp.xyz * shade + sp * P.f[3].xyz * a, a));
|
||||
}
|
||||
|
||||
// Wave World's Height Map view. data = grid heights (nx × ny), then water depths when
|
||||
// u[0].z (dry areas transparent); f[0] = (brightness, contrast, gamma, alpha);
|
||||
// f[1] = (buffer offset, render scale, layer width).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxm_wave(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let nx = P.u[0].x;
|
||||
let ny = P.u[0].y;
|
||||
let lw = P.f[1].w;
|
||||
let lx = (f32(p.x) + 0.5 - P.f[1].x) / P.f[1].z;
|
||||
let ly = (f32(p.y) + 0.5 - P.f[1].y) / P.f[1].z;
|
||||
let gx = clamp(lx / lw * f32(nx - 1u), 0.0, f32(nx - 1u));
|
||||
let gy = clamp(ly / lw * f32(nx - 1u), 0.0, f32(ny - 1u));
|
||||
let x0 = u32(floor(gx));
|
||||
let y0 = u32(floor(gy));
|
||||
let x1 = min(x0 + 1u, nx - 1u);
|
||||
let y1 = min(y0 + 1u, ny - 1u);
|
||||
let tx = gx - f32(x0);
|
||||
let ty = gy - f32(y0);
|
||||
let g00 = data[y0 * nx + x0];
|
||||
let g10 = data[y0 * nx + x1];
|
||||
let g01 = data[y1 * nx + x0];
|
||||
let g11 = data[y1 * nx + x1];
|
||||
let a = g00 + (g10 - g00) * tx;
|
||||
let c = g01 + (g11 - g01) * tx;
|
||||
let h = a + (c - a) * ty;
|
||||
let v = powz(clamp(P.f[0].x + P.f[0].y * h, 0.0, 1.0), 1.0 / P.f[0].z);
|
||||
var al = P.f[0].w;
|
||||
if (P.u[0].z != 0u) {
|
||||
let rx = u32(clamp(round_away(lx / lw * f32(nx - 1u)), 0.0, f32(nx - 1u)));
|
||||
let ry = u32(clamp(round_away(ly / lw * f32(nx - 1u)), 0.0, f32(ny - 1u)));
|
||||
if (data[nx * ny + ry * nx + rx] <= 0.0) {
|
||||
al = 0.0;
|
||||
}
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(v * al, v * al, v * al, al));
|
||||
}
|
||||
|
||||
// ---- Caustics
|
||||
|
||||
// The water surface's height gradient at (x, y) (aux = height plane, clamped; none = flat).
|
||||
fn fxm_grad(x: i32, y: i32) -> vec2<f32> {
|
||||
if (P.u[0].x == 0u) {
|
||||
return vec2<f32>(0.0);
|
||||
}
|
||||
let gx = (tex_get_clamped(aux, x + 1, y).x - tex_get_clamped(aux, x - 1, y).x) * 0.5;
|
||||
let gy = (tex_get_clamped(aux, x, y + 1).x - tex_get_clamped(aux, x, y - 1).x) * 0.5;
|
||||
return vec2<f32>(gx, gy);
|
||||
}
|
||||
|
||||
fn fxm_rem(v: f32, n: f32) -> f32 {
|
||||
return v - n * floor(v / n);
|
||||
}
|
||||
|
||||
fn fxm_mirror(v: f32, n: f32) -> f32 {
|
||||
let r = fxm_rem(v, 2.0 * n);
|
||||
return select(r, 2.0 * n - r, r > n);
|
||||
}
|
||||
|
||||
// sim2::sample_repeat of `src`.
|
||||
fn fxm_sample_repeat(x: f32, y: f32, mode: u32) -> vec4<f32> {
|
||||
let d = vec2<f32>(textureDimensions(src));
|
||||
if (mode == 1u) {
|
||||
return sample_bilinear_clamped(src, fxm_rem(x, d.x), fxm_rem(y, d.y));
|
||||
}
|
||||
if (mode == 2u) {
|
||||
return sample_bilinear_clamped(src, fxm_mirror(x, d.x), fxm_mirror(y, d.y));
|
||||
}
|
||||
return sample_bilinear(src, x, y);
|
||||
}
|
||||
|
||||
// A sky texel from `data` (RGBA f32 rows of u[1].y pixels; u[1].zw = size).
|
||||
fn fxm_sky_texel(x: i32, y: i32, clamped: bool) -> vec4<f32> {
|
||||
let w = i32(P.u[1].z);
|
||||
let h = i32(P.u[1].w);
|
||||
var xi = x;
|
||||
var yi = y;
|
||||
if (clamped) {
|
||||
xi = clamp(x, 0, w - 1);
|
||||
yi = clamp(y, 0, h - 1);
|
||||
} else if (x < 0 || y < 0 || x >= w || y >= h) {
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
let i = (u32(yi) * P.u[1].y + u32(xi)) * 4u;
|
||||
return vec4<f32>(data[i], data[i + 1u], data[i + 2u], data[i + 3u]);
|
||||
}
|
||||
|
||||
fn fxm_sky_bilinear(x: f32, y: f32, clamped: bool) -> vec4<f32> {
|
||||
let fx = x - 0.5;
|
||||
let fy = y - 0.5;
|
||||
let x0 = floor(fx);
|
||||
let y0 = floor(fy);
|
||||
let tx = fx - x0;
|
||||
let ty = fy - y0;
|
||||
let xi = i32(x0);
|
||||
let yi = i32(y0);
|
||||
let a = fxm_sky_texel(xi, yi, clamped);
|
||||
let b = fxm_sky_texel(xi + 1, yi, clamped);
|
||||
let c = fxm_sky_texel(xi, yi + 1, clamped);
|
||||
let d = fxm_sky_texel(xi + 1, yi + 1, clamped);
|
||||
let top = a + (b - a) * tx;
|
||||
let bot = c + (d - c) * tx;
|
||||
return top + (bot - top) * ty;
|
||||
}
|
||||
|
||||
fn fxm_sky_repeat(x: f32, y: f32, mode: u32) -> vec4<f32> {
|
||||
let w = f32(P.u[1].z);
|
||||
let h = f32(P.u[1].w);
|
||||
if (mode == 1u) {
|
||||
return fxm_sky_bilinear(fxm_rem(x, w), fxm_rem(y, h), true);
|
||||
}
|
||||
if (mode == 2u) {
|
||||
return fxm_sky_bilinear(fxm_mirror(x, w), fxm_mirror(y, h), true);
|
||||
}
|
||||
return fxm_sky_bilinear(x, y, false);
|
||||
}
|
||||
|
||||
// Caustics (sim2::caustics; src = the blurred bottom, aux = height plane, data = sky rows).
|
||||
// u[0] = (height, repeat, point light, sky repeat); u[1] = (sky, row, width, height);
|
||||
// f[0] = (scaling, wave height, displacement k, layer width); f[1] = (centre, light position);
|
||||
// f[2] = (surface colour, opacity); f[3] = (light colour, intensity); f[4] = (light height,
|
||||
// caustics strength, ambient, diffuse); f[5] = (specular, sharpness, sky scaling, sky
|
||||
// intensity); f[6] = (distant light direction, convergence).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxm_caustics(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let k = P.f[0].z;
|
||||
let x = p.x;
|
||||
let y = p.y;
|
||||
let dd = -fxm_grad(x, y) * k;
|
||||
let dxr = -fxm_grad(min(x + 1, dims.x - 1), y).x * k;
|
||||
let dyd = -fxm_grad(x, min(y + 1, dims.y - 1)).y * k;
|
||||
let dxl = -fxm_grad(max(x - 1, 0), y).x * k;
|
||||
let dyu = -fxm_grad(x, max(y - 1, 0)).y * k;
|
||||
let jac = (1.0 + (dxr - dxl) * 0.5) * (1.0 + (dyd - dyu) * 0.5);
|
||||
let focus = clamp(1.0 / max(abs(jac), 0.2) - 1.0, -1.0, 4.0);
|
||||
let cx = P.f[1].x;
|
||||
let cy = P.f[1].y;
|
||||
let fx = f32(x) + 0.5;
|
||||
let fy = f32(y) + 0.5;
|
||||
let sx = cx + (fx + dd.x - cx) / P.f[0].x;
|
||||
let sy = cy + (fy + dd.y - cy) / P.f[0].x;
|
||||
let bot = fxm_unpremul(fxm_sample_repeat(sx, sy, P.u[0].y));
|
||||
let g = fxm_grad(x, y);
|
||||
let wh = P.f[0].y;
|
||||
let v = vec3<f32>(-g.x * wh * 20.0, -g.y * wh * 20.0, 1.0);
|
||||
let n = v / sqrt(v.x * v.x + v.y * v.y + 1.0);
|
||||
var lv = P.f[6].xyz;
|
||||
if (P.u[0].z != 0u) {
|
||||
let lw = max(P.f[0].w, 1.0);
|
||||
let q = vec3<f32>((P.f[1].z - fx) / lw, (P.f[1].w - fy) / lw, P.f[4].x);
|
||||
lv = q / max(sqrt(q.x * q.x + q.y * q.y + q.z * q.z), 1e-12);
|
||||
}
|
||||
let li = P.f[3].w;
|
||||
let nl = max(dot(n, lv), 0.0);
|
||||
let hq = vec3<f32>(lv.x, lv.y, lv.z + 1.0);
|
||||
let hv = hq / sqrt(hq.x * hq.x + hq.y * hq.y + hq.z * hq.z);
|
||||
let spec = powz(max(dot(n, hv), 0.0), P.f[5].y) * P.f[5].x * li;
|
||||
let ambient = P.f[4].z;
|
||||
let diffuse = P.f[4].w;
|
||||
let light = ambient + diffuse * li * nl + P.f[4].y * focus * li;
|
||||
let surf = P.f[2].xyz;
|
||||
let surf_op = P.f[2].w;
|
||||
var surface: vec3<f32>;
|
||||
if (P.u[1].x != 0u) {
|
||||
let reach = (1.0 - P.f[6].w) * P.f[0].w * 0.5;
|
||||
let rx = cx + (fx + n.x * reach - cx) / P.f[5].z;
|
||||
let ry = cy + (fy + n.y * reach - cy) / P.f[5].z;
|
||||
let sc = fxm_unpremul(fxm_sky_repeat(rx, ry, P.u[0].w)).xyz;
|
||||
surface = sc * P.f[5].w * 3.0 * surf;
|
||||
} else {
|
||||
surface = surf * (ambient + diffuse * li * nl);
|
||||
}
|
||||
let lc = P.f[3].xyz;
|
||||
let cc = bot.xyz * light * lc * (1.0 - surf_op) + surface * surf_op + spec * lc;
|
||||
let a = min(bot.w + surf_op * (1.0 - bot.w), 1.0);
|
||||
textureStore(out, p, vec4<f32>(cc * a, a));
|
||||
}
|
||||
|
|
@ -0,0 +1,640 @@
|
|||
// ---------------------------------------------------------------- Immersive Video (fx_vr.rs)
|
||||
//
|
||||
// Equirectangular frames: longitude across, latitude down, the front (+Z) at the centre, y up
|
||||
// (effects::vr). Every function mirrors its CPU namesake.
|
||||
|
||||
const FXV_PI: f32 = 3.14159265358979;
|
||||
|
||||
fn fxv_unpremul(p: vec4<f32>) -> vec4<f32> {
|
||||
if (p.w > 1e-6) {
|
||||
return vec4<f32>(p.xyz / p.w, p.w);
|
||||
}
|
||||
return vec4<f32>(0.0, 0.0, 0.0, max(p.w, 0.0));
|
||||
}
|
||||
|
||||
// vr::equi_dir.
|
||||
fn fxv_equi_dir(x: f32, y: f32, w: f32, h: f32) -> vec3<f32> {
|
||||
let lon = (x / w - 0.5) * 2.0 * FXV_PI;
|
||||
let lat = (0.5 - y / h) * FXV_PI;
|
||||
return vec3<f32>(cos(lat) * sin(lon), sin(lat), cos(lat) * cos(lon));
|
||||
}
|
||||
|
||||
// vr::equi_px.
|
||||
fn fxv_equi_px(d: vec3<f32>, w: f32, h: f32) -> vec2<f32> {
|
||||
let lon = atan2(d.x, d.z);
|
||||
let lat = asin(clamp(d.y, -1.0, 1.0));
|
||||
return vec2<f32>((lon / (2.0 * FXV_PI) + 0.5) * w, (0.5 - lat / FXV_PI) * h);
|
||||
}
|
||||
|
||||
// Angle between two unit directions (the CPU's acos of the dot, in its f32-stable form).
|
||||
fn fxv_angle(a: vec3<f32>, b: vec3<f32>) -> f32 {
|
||||
return atan2(length(cross(a, b)), dot(a, b));
|
||||
}
|
||||
|
||||
// vr::sample_equi's texel fetch: wraps horizontally, continues over the poles.
|
||||
fn fxv_equi_at(xi0: i32, yi0: i32, w: i32, h: i32) -> vec4<f32> {
|
||||
var xi = xi0;
|
||||
var yi = yi0;
|
||||
if (yi < 0) {
|
||||
yi = -1 - yi;
|
||||
xi += w / 2;
|
||||
} else if (yi >= h) {
|
||||
yi = 2 * h - 1 - yi;
|
||||
xi += w / 2;
|
||||
}
|
||||
let xm = ((xi % w) + w) % w;
|
||||
return textureLoad(src, vec2<i32>(xm, clamp(yi, 0, h - 1)), 0);
|
||||
}
|
||||
|
||||
// vr::sample_equi (bilinear).
|
||||
fn fxv_sample_equi(x: f32, y: f32) -> vec4<f32> {
|
||||
let d = vec2<i32>(textureDimensions(src));
|
||||
let fx = x - 0.5;
|
||||
let fy = y - 0.5;
|
||||
let x0 = floor(fx);
|
||||
let y0 = floor(fy);
|
||||
let tx = fx - x0;
|
||||
let ty = fy - y0;
|
||||
let xi = i32(x0);
|
||||
let yi = i32(y0);
|
||||
let a = fxv_equi_at(xi, yi, d.x, d.y);
|
||||
let b = fxv_equi_at(xi + 1, yi, d.x, d.y);
|
||||
let c = fxv_equi_at(xi, yi + 1, d.x, d.y);
|
||||
let e = fxv_equi_at(xi + 1, yi + 1, d.x, d.y);
|
||||
let top = a + (b - a) * tx;
|
||||
let bot = c + (e - c) * tx;
|
||||
return top + (bot - top) * ty;
|
||||
}
|
||||
|
||||
// ---- VR Converter's projections (vr::Proj)
|
||||
|
||||
fn fxv_face_fw(f: u32) -> vec3<f32> {
|
||||
var v = array<vec3<f32>, 6>(vec3<f32>(1.0, 0.0, 0.0), vec3<f32>(-1.0, 0.0, 0.0), vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(0.0, -1.0, 0.0), vec3<f32>(0.0, 0.0, 1.0), vec3<f32>(0.0, 0.0, -1.0));
|
||||
return v[f];
|
||||
}
|
||||
|
||||
fn fxv_face_r(f: u32) -> vec3<f32> {
|
||||
var v = array<vec3<f32>, 6>(vec3<f32>(0.0, 0.0, -1.0), vec3<f32>(0.0, 0.0, 1.0), vec3<f32>(1.0, 0.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), vec3<f32>(-1.0, 0.0, 0.0));
|
||||
return v[f];
|
||||
}
|
||||
|
||||
fn fxv_face_u(f: u32) -> vec3<f32> {
|
||||
var v = array<vec3<f32>, 6>(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(0.0, 0.0, -1.0), vec3<f32>(0.0, 0.0, 1.0), vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(0.0, 1.0, 0.0));
|
||||
return v[f];
|
||||
}
|
||||
|
||||
// vr::cube_grid: the grid size.
|
||||
fn fxv_grid(kind: u32) -> vec2<f32> {
|
||||
switch (kind) {
|
||||
case 1u: {
|
||||
return vec2<f32>(4.0, 3.0);
|
||||
}
|
||||
case 2u, 6u, 7u: {
|
||||
return vec2<f32>(3.0, 2.0);
|
||||
}
|
||||
default: {
|
||||
return vec2<f32>(6.0, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// vr::cube_grid: (column, row) of face f.
|
||||
fn fxv_cell(kind: u32, f: u32) -> vec2<f32> {
|
||||
switch (kind) {
|
||||
case 1u: {
|
||||
var c = array<vec2<f32>, 6>(vec2<f32>(2.0, 1.0), vec2<f32>(0.0, 1.0), vec2<f32>(1.0, 0.0), vec2<f32>(1.0, 2.0), vec2<f32>(1.0, 1.0), vec2<f32>(3.0, 1.0));
|
||||
return c[f];
|
||||
}
|
||||
case 2u: {
|
||||
var c = array<vec2<f32>, 6>(vec2<f32>(0.0, 0.0), vec2<f32>(1.0, 0.0), vec2<f32>(2.0, 0.0), vec2<f32>(0.0, 1.0), vec2<f32>(1.0, 1.0), vec2<f32>(2.0, 1.0));
|
||||
return c[f];
|
||||
}
|
||||
case 6u, 7u: {
|
||||
var c = array<vec2<f32>, 6>(vec2<f32>(2.0, 0.0), vec2<f32>(0.0, 0.0), vec2<f32>(2.0, 1.0), vec2<f32>(0.0, 1.0), vec2<f32>(1.0, 0.0), vec2<f32>(1.0, 1.0));
|
||||
return c[f];
|
||||
}
|
||||
default: {
|
||||
return vec2<f32>(f32(f), 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fxv_is_cube(kind: u32) -> bool {
|
||||
return (kind >= 1u && kind <= 3u) || kind == 6u || kind == 7u;
|
||||
}
|
||||
|
||||
// Proj::dir: the direction seen at frame position (x, y); w = 2 when outside the image.
|
||||
fn fxv_dir(kind: u32, fov: f32, x: f32, y: f32, w: f32, h: f32) -> vec4<f32> {
|
||||
let none = vec4<f32>(0.0, 0.0, 0.0, 2.0);
|
||||
if (kind == 0u) {
|
||||
return vec4<f32>(fxv_equi_dir(x, y, w, h), 1.0);
|
||||
}
|
||||
if (fxv_is_cube(kind)) {
|
||||
let g = fxv_grid(kind);
|
||||
let cw = w / g.x;
|
||||
let ch = h / g.y;
|
||||
let cx = floor(x / cw);
|
||||
let cy = floor(y / ch);
|
||||
var f = 6u;
|
||||
for (var i = 0u; i < 6u; i++) {
|
||||
let c = fxv_cell(kind, i);
|
||||
if (f == 6u && c.x == cx && c.y == cy) {
|
||||
f = i;
|
||||
}
|
||||
}
|
||||
if (f == 6u) {
|
||||
return none;
|
||||
}
|
||||
var a = (x - cx * cw) / cw * 2.0 - 1.0;
|
||||
var b = (y - cy * ch) / ch * 2.0 - 1.0;
|
||||
if (kind == 7u) {
|
||||
a = tan(a * FXV_PI / 4.0);
|
||||
b = tan(b * FXV_PI / 4.0);
|
||||
}
|
||||
return vec4<f32>(normalize(fxv_face_fw(f) + a * fxv_face_r(f) - b * fxv_face_u(f)), 1.0);
|
||||
}
|
||||
if (kind == 8u) {
|
||||
let s = min(w, h) / 2.0;
|
||||
let u = (x - w / 2.0) / s;
|
||||
let v = (y - h / 2.0) / s;
|
||||
let r = sqrt(u * u + v * v);
|
||||
if (r > 1.0) {
|
||||
return none;
|
||||
}
|
||||
let th = r * FXV_PI / 2.0;
|
||||
var cu = 0.0;
|
||||
var cv = 0.0;
|
||||
if (r > 1e-12) {
|
||||
cu = u / r;
|
||||
cv = v / r;
|
||||
}
|
||||
return vec4<f32>(sin(th) * cu, cos(th), sin(th) * cv, 1.0);
|
||||
}
|
||||
if (kind == 4u) {
|
||||
let half = radians(clamp(fov, 1.0, 360.0) / 2.0);
|
||||
let s = min(w, h) / 2.0;
|
||||
let u = (x - w / 2.0) / s;
|
||||
let v = (h / 2.0 - y) / s;
|
||||
let r = sqrt(u * u + v * v);
|
||||
if (r > 1.0) {
|
||||
return none;
|
||||
}
|
||||
let th = r * half;
|
||||
var cu = 0.0;
|
||||
var cv = 0.0;
|
||||
if (r > 1e-12) {
|
||||
cu = u / r;
|
||||
cv = v / r;
|
||||
}
|
||||
return vec4<f32>(sin(th) * cu, sin(th) * cv, cos(th), 1.0);
|
||||
}
|
||||
let t = tan(radians(clamp(fov, 1.0, 179.0) / 2.0));
|
||||
let u = (x / w * 2.0 - 1.0) * t;
|
||||
let v = (1.0 - y / h * 2.0) * t * h / w;
|
||||
return vec4<f32>(normalize(vec3<f32>(u, v, 1.0)), 1.0);
|
||||
}
|
||||
|
||||
// Proj::sample of `src` (in projection `kind`) in direction d.
|
||||
fn fxv_proj_sample(kind: u32, fov: f32, d: vec3<f32>) -> vec4<f32> {
|
||||
let dims = vec2<f32>(textureDimensions(src));
|
||||
let w = dims.x;
|
||||
let h = dims.y;
|
||||
if (kind == 0u) {
|
||||
let q = fxv_equi_px(d, w, h);
|
||||
return fxv_sample_equi(q.x, q.y);
|
||||
}
|
||||
if (fxv_is_cube(kind)) {
|
||||
// The face facing d most (the last one on ties, as max_by).
|
||||
var f = 0u;
|
||||
var best = dot(d, fxv_face_fw(0u));
|
||||
for (var i = 1u; i < 6u; i++) {
|
||||
let k = dot(d, fxv_face_fw(i));
|
||||
if (k >= best) {
|
||||
best = k;
|
||||
f = i;
|
||||
}
|
||||
}
|
||||
let fw = fxv_face_fw(f);
|
||||
let k = dot(d, fw);
|
||||
var a = dot(d, fxv_face_r(f)) / k;
|
||||
var b = -dot(d, fxv_face_u(f)) / k;
|
||||
if (kind == 7u) {
|
||||
a = atan(a) / (FXV_PI / 4.0);
|
||||
b = atan(b) / (FXV_PI / 4.0);
|
||||
}
|
||||
let g = fxv_grid(kind);
|
||||
let cw = w / g.x;
|
||||
let ch = h / g.y;
|
||||
let c = fxv_cell(kind, f);
|
||||
let x0 = c.x * cw;
|
||||
let y0 = c.y * ch;
|
||||
let x = clamp(x0 + (a + 1.0) * 0.5 * cw, x0 + 0.5, x0 + cw - 0.5);
|
||||
let y = clamp(y0 + (b + 1.0) * 0.5 * ch, y0 + 0.5, y0 + ch - 0.5);
|
||||
return sample_bilinear_clamped(src, x, y);
|
||||
}
|
||||
if (kind == 8u) {
|
||||
let th = acos(clamp(d.y, -1.0, 1.0));
|
||||
if (th > FXV_PI / 2.0 + 1e-6) {
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
let s = min(w, h) / 2.0;
|
||||
let r = th / (FXV_PI / 2.0);
|
||||
let l = sqrt(d.x * d.x + d.z * d.z);
|
||||
var cu = 0.0;
|
||||
var cv = 0.0;
|
||||
if (l > 1e-12) {
|
||||
cu = d.x / l;
|
||||
cv = d.z / l;
|
||||
}
|
||||
return sample_bilinear_clamped(src, w / 2.0 + cu * r * s, h / 2.0 + cv * r * s);
|
||||
}
|
||||
if (kind == 4u) {
|
||||
let half = radians(clamp(fov, 1.0, 360.0) / 2.0);
|
||||
let th = acos(clamp(d.z, -1.0, 1.0));
|
||||
if (th > half + 1e-6) {
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
let s = min(w, h) / 2.0;
|
||||
let r = th / half;
|
||||
let l = sqrt(d.x * d.x + d.y * d.y);
|
||||
var cu = 0.0;
|
||||
var cv = 0.0;
|
||||
if (l > 1e-12) {
|
||||
cu = d.x / l;
|
||||
cv = d.y / l;
|
||||
}
|
||||
return sample_bilinear_clamped(src, w / 2.0 + cu * r * s, h / 2.0 - cv * r * s);
|
||||
}
|
||||
if (d.z <= 1e-9) {
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
let t = tan(radians(clamp(fov, 1.0, 179.0) / 2.0));
|
||||
let u = d.x / d.z / t;
|
||||
let v = d.y / d.z / (t * h / w);
|
||||
if (abs(u) > 1.0 || abs(v) > 1.0) {
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
return sample_bilinear_clamped(src, (u + 1.0) * 0.5 * w, (1.0 - v) * 0.5 * h);
|
||||
}
|
||||
|
||||
// mᵀ · d with m's rows in f[at..at + 3].
|
||||
fn fxv_mtv(at: u32, d: vec3<f32>) -> vec3<f32> {
|
||||
let r0 = P.f[at].xyz;
|
||||
let r1 = P.f[at + 1u].xyz;
|
||||
let r2 = P.f[at + 2u].xyz;
|
||||
return r0 * d.x + r1 * d.y + r2 * d.z;
|
||||
}
|
||||
|
||||
// m · d.
|
||||
fn fxv_mv(at: u32, d: vec3<f32>) -> vec3<f32> {
|
||||
return vec3<f32>(dot(P.f[at].xyz, d), dot(P.f[at + 1u].xyz, d), dot(P.f[at + 2u].xyz, d));
|
||||
}
|
||||
|
||||
// vr::convert. u[0] = (from kind, to kind, feather); f[0] = (from fov, to fov);
|
||||
// f[1..4] = rotation rows; f[4] = (feather, tan(fov / 2)) (VR Plane to Sphere).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxv_convert(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let w = f32(dims.x);
|
||||
let h = f32(dims.y);
|
||||
let x = f32(p.x) + 0.5;
|
||||
let y = f32(p.y) + 0.5;
|
||||
let dd = fxv_dir(P.u[0].y, P.f[0].y, x, y, w, h);
|
||||
var o = vec4<f32>(0.0);
|
||||
if (dd.w < 1.5) {
|
||||
o = fxv_proj_sample(P.u[0].x, P.f[0].x, fxv_mtv(1u, dd.xyz));
|
||||
}
|
||||
if (P.u[0].z != 0u) {
|
||||
let d = fxv_mtv(1u, fxv_equi_dir(x, y, w, h));
|
||||
if (d.z > 0.0) {
|
||||
let t = P.f[4].y;
|
||||
let u = abs(d.x / d.z / t);
|
||||
let v = abs(d.y / d.z / (t * h / w));
|
||||
let edge = 1.0 - max(u, v);
|
||||
o = o * clamp(edge / P.f[4].x, 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---- seam-aware box blurs
|
||||
|
||||
// Horizontal wrap-around box mean (vr::box_row_wrap); each row's radius in data[y].
|
||||
// u[0].x = block; dispatched over (row, block).
|
||||
@compute @workgroup_size(64, 1)
|
||||
fn fxv_box_h(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let y = i32(gid.x);
|
||||
let block = i32(P.u[0].x);
|
||||
let b0 = i32(gid.y) * block;
|
||||
let w = dims.x;
|
||||
if (y >= dims.y || b0 >= w) {
|
||||
return;
|
||||
}
|
||||
let r = i32(data[y]);
|
||||
let b1 = min(b0 + block, w);
|
||||
if (r == 0) {
|
||||
for (var x = b0; x < b1; x++) {
|
||||
textureStore(out, vec2<i32>(x, y), textureLoad(src, vec2<i32>(x, y), 0));
|
||||
}
|
||||
return;
|
||||
}
|
||||
let norm = 1.0 / f32(2 * r + 1);
|
||||
var acc = vec4<f32>(0.0);
|
||||
for (var i = b0 - r; i <= b0 + r; i++) {
|
||||
acc += textureLoad(src, vec2<i32>(((i % w) + w) % w, y), 0);
|
||||
}
|
||||
for (var x = b0; x < b1; x++) {
|
||||
textureStore(out, vec2<i32>(x, y), acc * norm);
|
||||
let add = textureLoad(src, vec2<i32>((x + r + 1) % w, y), 0);
|
||||
let sub = textureLoad(src, vec2<i32>(((x - r) % w + w) % w, y), 0);
|
||||
acc += add - sub;
|
||||
}
|
||||
}
|
||||
|
||||
// vr::box_cols_pole's fetch.
|
||||
fn fxv_pole_at(x0: i32, y0: i32, w: i32, h: i32) -> vec4<f32> {
|
||||
var x = x0;
|
||||
var y = y0;
|
||||
if (y < 0) {
|
||||
y = -1 - y;
|
||||
x = (x + w / 2) % w;
|
||||
} else if (y >= h) {
|
||||
y = 2 * h - 1 - y;
|
||||
x = (x + w / 2) % w;
|
||||
}
|
||||
return textureLoad(src, vec2<i32>(x, clamp(y, 0, h - 1)), 0);
|
||||
}
|
||||
|
||||
// Vertical box mean continuing over the poles. u[0] = (block, radius); dispatched over
|
||||
// (column, block).
|
||||
@compute @workgroup_size(64, 1)
|
||||
fn fxv_box_v(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let x = i32(gid.x);
|
||||
let block = i32(P.u[0].x);
|
||||
let r = i32(P.u[0].y);
|
||||
let b0 = i32(gid.y) * block;
|
||||
if (x >= dims.x || b0 >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let norm = 1.0 / f32(2 * r + 1);
|
||||
var acc = vec4<f32>(0.0);
|
||||
for (var j = b0 - r; j <= b0 + r; j++) {
|
||||
acc += fxv_pole_at(x, j, dims.x, dims.y);
|
||||
}
|
||||
let b1 = min(b0 + block, dims.y);
|
||||
for (var y = b0; y < b1; y++) {
|
||||
textureStore(out, vec2<i32>(x, y), acc * norm);
|
||||
acc += fxv_pole_at(x, y + r + 1, dims.x, dims.y) - fxv_pole_at(x, y - r, dims.x, dims.y);
|
||||
}
|
||||
}
|
||||
|
||||
// vr::wrap_pad: u[0].x = pad.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxv_wrap_pad(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let s = vec2<i32>(textureDimensions(src));
|
||||
let pad = i32(P.u[0].x);
|
||||
var sx = p.x - pad;
|
||||
var sy = p.y - pad;
|
||||
if (sy < 0) {
|
||||
sy = -1 - sy;
|
||||
sx += s.x / 2;
|
||||
} else if (sy >= s.y) {
|
||||
sy = 2 * s.y - 1 - sy;
|
||||
sx += s.x / 2;
|
||||
}
|
||||
textureStore(out, p, textureLoad(src, vec2<i32>(((sx % s.x) + s.x) % s.x, clamp(sy, 0, s.y - 1)), 0));
|
||||
}
|
||||
|
||||
// Per-pixel steps. u[0].x = 0: VR Glow's highlights (u[0].y tint; f[0] = (threshold,
|
||||
// saturation); f[1] = tint); 1: add the glow (aux) × f[0].x; 2: VR Sharpen (aux = blurred,
|
||||
// f[0].x = amount); 3: VR De-Noise's detail mix (src = filtered, aux = original, f[0].x);
|
||||
// 4: the guided filter's guide (straight luminance).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxv_point(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let q = tex_get(aux, p.x, p.y);
|
||||
var o = px;
|
||||
switch (P.u[0].x) {
|
||||
case 0u: {
|
||||
let s = fxv_unpremul(px);
|
||||
let th = P.f[0].x;
|
||||
let l = luminance(s.xyz);
|
||||
var k = 0.0;
|
||||
if (l > th) {
|
||||
k = min((l - th) / max(1.0 - th, 1e-3), 4.0);
|
||||
}
|
||||
var c = s.xyz * k;
|
||||
let lc = luminance(c);
|
||||
c = lc + (c - lc) * P.f[0].y;
|
||||
if (P.u[0].y != 0u) {
|
||||
c = lc * P.f[1].xyz;
|
||||
}
|
||||
o = vec4<f32>(c * s.w, s.w * min(k, 1.0));
|
||||
}
|
||||
case 1u: {
|
||||
let b = P.f[0].x;
|
||||
o = vec4<f32>(px.xyz + q.xyz * b, min(px.w + q.w * b, 1.0));
|
||||
}
|
||||
case 2u: {
|
||||
let hi = max(px.w, 0.0) * 4.0;
|
||||
let c = clamp(px.xyz + (px.xyz - q.xyz) * P.f[0].x, vec3<f32>(0.0), vec3<f32>(hi));
|
||||
o = vec4<f32>(c, px.w);
|
||||
}
|
||||
case 3u: {
|
||||
var m = px + (q - px) * P.f[0].x;
|
||||
m.w = clamp(m.w, 0.0, 1.0);
|
||||
o = vec4<f32>(max(m.xyz, vec3<f32>(0.0)), m.w);
|
||||
}
|
||||
case 4u: {
|
||||
o = vec4<f32>(luminance(fxv_unpremul(px).xyz));
|
||||
}
|
||||
default: {}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// vr::composite of a generated straight colour (alpha 1) over the pixel. u[0].z = 1: mode None
|
||||
// (replace); u[0].w = blend mode id; f[0].y = opacity.
|
||||
fn fxv_composite(d: vec4<f32>, c: vec3<f32>, opacity: f32) -> vec4<f32> {
|
||||
let s = vec4<f32>(c * opacity, opacity);
|
||||
if (P.u[0].z != 0u) {
|
||||
return s;
|
||||
}
|
||||
var r = blend_pixel(P.u[0].w, d, s, 0.5);
|
||||
let a = d.w;
|
||||
if (a < 1.0) {
|
||||
let k = a / max(r.w, 1e-6);
|
||||
if (k < 1.0) {
|
||||
r = r * k;
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// Generated looks. u[0].x = 0: VR Chromatic Aberrations; 1: VR Color Gradients; 2: VR Fractal
|
||||
// Noise; 3: VR Digital Glitch (parameter layouts in fx_vr.rs).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxv_gen(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let w = f32(dims.x);
|
||||
let h = f32(dims.y);
|
||||
let fx = f32(p.x) + 0.5;
|
||||
let fy = f32(p.y) + 0.5;
|
||||
let px = textureLoad(src, p, 0);
|
||||
var o = px;
|
||||
switch (P.u[0].x) {
|
||||
// f[0] = (aberration R, G, B, falloff); f[1] = centre; u[0].y = invert.
|
||||
case 0u: {
|
||||
let d = fxv_equi_dir(fx, fy, w, h);
|
||||
let center = P.f[1].xyz;
|
||||
let ax = cross(center, d);
|
||||
let al = length(ax);
|
||||
let th = atan2(al, dot(center, d));
|
||||
var weight = powz(th / FXV_PI, 1.0 + P.f[0].w * 3.0);
|
||||
if (P.u[0].y != 0u) {
|
||||
weight = 1.0 - weight;
|
||||
}
|
||||
var res = vec4<f32>(0.0);
|
||||
for (var c = 0; c < 3; c++) {
|
||||
var s = px;
|
||||
if (al >= 1e-9) {
|
||||
let new_th = th * (1.0 - P.f[0][c] * weight);
|
||||
let perp = normalize(d - center * cos(th));
|
||||
let nd = center * cos(new_th) + perp * sin(new_th);
|
||||
let q = fxv_equi_px(nd, w, h);
|
||||
s = fxv_sample_equi(q.x, q.y);
|
||||
}
|
||||
res[c] = s[c];
|
||||
res.w = max(res.w, s.w);
|
||||
}
|
||||
o = res;
|
||||
}
|
||||
// data: points (direction, colour); u[0].y = count; f[0] = (power, opacity).
|
||||
case 1u: {
|
||||
let d = fxv_equi_dir(fx, fy, w, h);
|
||||
let n = P.u[0].y;
|
||||
var amin = 10.0;
|
||||
var exact = -1;
|
||||
for (var i = 0u; i < n; i++) {
|
||||
let a = fxv_angle(d, vec3<f32>(data[i * 6u], data[i * 6u + 1u], data[i * 6u + 2u]));
|
||||
if (a < 1e-6 && exact < 0) {
|
||||
exact = i32(i);
|
||||
}
|
||||
amin = min(amin, a);
|
||||
}
|
||||
var c: vec3<f32>;
|
||||
if (exact >= 0) {
|
||||
let e = u32(exact);
|
||||
c = vec3<f32>(data[e * 6u + 3u], data[e * 6u + 4u], data[e * 6u + 5u]);
|
||||
} else {
|
||||
var acc = vec3<f32>(0.0);
|
||||
var ws = 0.0;
|
||||
for (var i = 0u; i < n; i++) {
|
||||
let a = fxv_angle(d, vec3<f32>(data[i * 6u], data[i * 6u + 1u], data[i * 6u + 2u]));
|
||||
let wgt = pow(amin / a, P.f[0].x);
|
||||
acc += vec3<f32>(data[i * 6u + 3u], data[i * 6u + 4u], data[i * 6u + 5u]) * wgt;
|
||||
ws += wgt;
|
||||
}
|
||||
c = acc / ws;
|
||||
}
|
||||
o = fxv_composite(px, c, P.f[0].y);
|
||||
}
|
||||
// u[0].y = type; u[1] = (octaves, seed, invert); f[0] = (contrast, brightness, sub
|
||||
// influence, sub scaling); f[1] = (base frequency, evolution, opacity, octave fraction);
|
||||
// f[2..5] = rotation rows.
|
||||
case 2u: {
|
||||
let d = fxv_mv(2u, fxv_equi_dir(fx, fy, w, h));
|
||||
let kind = P.u[0].y;
|
||||
let n_oct = P.u[1].x;
|
||||
let evo = P.f[1].y;
|
||||
var v = 0.0;
|
||||
var best = 0.0;
|
||||
var amp = 1.0;
|
||||
var freq = P.f[1].x;
|
||||
var norm = 0.0;
|
||||
for (var oc = 0u; oc < n_oct; oc++) {
|
||||
var wgt = 1.0;
|
||||
if (oc + 1u == n_oct && P.f[1].w > 0.0) {
|
||||
wgt = P.f[1].w;
|
||||
}
|
||||
let q = vec3<f32>(d.x * freq + evo * 0.37, d.y * freq + evo * 0.71, d.z * freq + evo * 0.59);
|
||||
var nv = value_noise(q.x + f32(oc) * 17.3, q.y, q.z, P.u[1].y + oc);
|
||||
if (kind == 1u) {
|
||||
nv = 1.0 - abs(nv * 2.0 - 1.0);
|
||||
} else if (kind == 3u) {
|
||||
let t = clamp(abs(nv * 2.0 - 1.0) / 0.12, 0.0, 1.0);
|
||||
nv = 1.0 - t * t;
|
||||
}
|
||||
best = max(best, nv * amp);
|
||||
v += nv * amp * wgt;
|
||||
norm += amp * wgt;
|
||||
amp *= P.f[0].z;
|
||||
freq /= P.f[0].w;
|
||||
}
|
||||
if (kind == 2u) {
|
||||
v = best;
|
||||
} else {
|
||||
v = v / max(norm, 1e-6);
|
||||
}
|
||||
v = (v - 0.5) * P.f[0].x + 0.5 + P.f[0].y;
|
||||
if (P.u[1].z != 0u) {
|
||||
v = 1.0 - v;
|
||||
}
|
||||
v = clamp(v, 0.0, 1.0);
|
||||
o = fxv_composite(px, vec3<f32>(v), P.f[1].z);
|
||||
}
|
||||
// data: per row (dx, dy, split, dark); f[0] = channel offsets; f[1] = target direction;
|
||||
// f[2] = (radius, feather) in radians.
|
||||
case 3u: {
|
||||
let y = u32(p.y);
|
||||
let dx = data[y * 4u];
|
||||
let dy = data[y * 4u + 1u];
|
||||
let split = data[y * 4u + 2u];
|
||||
let dark = data[y * 4u + 3u];
|
||||
let radius = P.f[2].x;
|
||||
let feather = P.f[2].y;
|
||||
var k = 1.0;
|
||||
if (radius > 0.0) {
|
||||
let a = fxv_angle(fxv_equi_dir(fx, fy, w, h), P.f[1].xyz);
|
||||
if (a <= radius) {
|
||||
k = 1.0;
|
||||
} else if (feather > 0.0) {
|
||||
k = clamp(1.0 - (a - radius) / feather, 0.0, 1.0);
|
||||
} else {
|
||||
k = 0.0;
|
||||
}
|
||||
}
|
||||
if (k > 0.0) {
|
||||
let sx = fx - dx * k;
|
||||
let sy = clamp(fy - dy * k, 0.5, h - 0.5);
|
||||
let c0 = fxv_sample_equi(sx + P.f[0].x * split * k, sy);
|
||||
let c1 = fxv_sample_equi(sx + P.f[0].y * split * k, sy);
|
||||
let c2 = fxv_sample_equi(sx + P.f[0].z * split * k, sy);
|
||||
let dk = 1.0 - (1.0 - dark) * k;
|
||||
o = vec4<f32>(c0.x * dk, c1.y * dk, c2.z * dk, max(max(c0.w, c1.w), c2.w));
|
||||
}
|
||||
}
|
||||
default: {}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
//! GPU 3D Channel family vs the CPU effects (the oracle): direct on a buffer (full and half
|
||||
//! resolution, padded offsets, aux channels at 1× and 2× the layer resolution) and composited
|
||||
//! at 8 and 32 bpc from footage that carries auxiliary channels.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use effectcraft_effects::{EffectEnv, EffectHost, LayerPixels};
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::{BitDepth, Footage, ItemId};
|
||||
use effectcraft_raster::channels3d::{BACKGROUND_DEPTH, crypto_float, crypto_hash};
|
||||
use effectcraft_raster::{AuxChannels, Image};
|
||||
use effectcraft_render::{Backend, FootageSource, RenderOpts, Renderer};
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
use crate::tests::{Pattern, Scene, c, check, diff, gpu, n, opts, pattern, set, tolerance};
|
||||
|
||||
pub(crate) struct AuxHost(pub(crate) Arc<AuxChannels>);
|
||||
|
||||
impl EffectHost for AuxHost {
|
||||
fn layer(&self, _: u64, _: bool) -> Option<LayerPixels> {
|
||||
None
|
||||
}
|
||||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
fn aux(&self) -> Option<Arc<AuxChannels>> {
|
||||
Some(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
const NAMES: [&str; 5] = ["alpha", "beta", "gamma", "delta", "epsilon"];
|
||||
|
||||
/// Aux channels for a `w`×`h` layer at `scale` aux pixels per layer pixel: a depth ramp with a
|
||||
/// background corner, object / material ID blocks, coverage, UVs, normals, background RGB, raw
|
||||
/// RGB and a two-rank Cryptomatte layer with a manifest.
|
||||
pub(crate) fn aux_scene(w: u32, h: u32, scale: f64) -> AuxChannels {
|
||||
let (aw, ah) = ((w as f64 * scale) as u32, (h as f64 * scale) as u32);
|
||||
let mut a = AuxChannels::new(aw, ah, scale);
|
||||
let n = (aw * ah) as usize;
|
||||
let mut ch: Vec<(String, Vec<f32>)> = [
|
||||
"Z",
|
||||
"ObjectID",
|
||||
"MaterialID",
|
||||
"Coverage",
|
||||
"UV.U",
|
||||
"UV.V",
|
||||
"N.X",
|
||||
"N.Y",
|
||||
"N.Z",
|
||||
"BG.R",
|
||||
"BG.G",
|
||||
"BG.B",
|
||||
"R",
|
||||
"G",
|
||||
"B",
|
||||
"CryptoObject00.R",
|
||||
"CryptoObject00.G",
|
||||
"CryptoObject00.B",
|
||||
"CryptoObject00.A",
|
||||
]
|
||||
.iter()
|
||||
.map(|s| (s.to_string(), vec![0.0; n]))
|
||||
.collect();
|
||||
let hashes: Vec<u32> = NAMES.iter().map(|s| crypto_hash(s)).collect();
|
||||
for y in 0..ah {
|
||||
for x in 0..aw {
|
||||
let i = (y * aw + x) as usize;
|
||||
let (u, v) = (x as f32 / aw as f32, y as f32 / ah as f32);
|
||||
let bg = u > 0.8 && v < 0.25;
|
||||
let obj = ((x as f64 / scale) as u32 / 11 + (y as f64 / scale) as u32 / 9 * 3) % 5;
|
||||
let vals = [
|
||||
if bg { BACKGROUND_DEPTH } else { 50.0 + 900.0 * u + 300.0 * (v * 7.0).sin() },
|
||||
if bg { 0.0 } else { obj as f32 + 1.0 },
|
||||
((obj * 7) % 3) as f32,
|
||||
0.5 + 0.5 * (u * 9.0).sin(),
|
||||
u,
|
||||
v,
|
||||
(u - 0.5) * 1.6,
|
||||
(0.5 - v) * 1.4,
|
||||
0.7,
|
||||
0.2 + 0.6 * v,
|
||||
0.4,
|
||||
0.9 - 0.5 * u,
|
||||
u * 1.8,
|
||||
v * 0.7,
|
||||
0.3 + u * v * 2.5,
|
||||
crypto_float(hashes[obj as usize]),
|
||||
0.7,
|
||||
crypto_float(hashes[(obj as usize + 1) % 5]),
|
||||
0.3,
|
||||
];
|
||||
for (k, val) in vals.iter().enumerate() {
|
||||
ch[k].1[i] = *val;
|
||||
}
|
||||
}
|
||||
}
|
||||
a.channels = ch;
|
||||
a.manifests = vec![("CryptoObject".into(), NAMES.iter().zip(&hashes).map(|(n, h)| (n.to_string(), *h)).collect())];
|
||||
a
|
||||
}
|
||||
|
||||
/// The effect alone on a buffer with aux channels (`allow` = fraction of pixels that may
|
||||
/// differ by more than 1e-3).
|
||||
pub(crate) fn direct(id: &str, vals: &[(&str, Value)], allow: f64) {
|
||||
let Some(g) = gpu() else { return };
|
||||
let spec = effectcraft_effects::find(id).unwrap();
|
||||
let size = [70.0, 44.0];
|
||||
let mut params =
|
||||
effectcraft_effects::Params { values: spec.params.iter().map(|ps| (ps.id.to_string(), effectcraft_effects::default_value(ps, size))).collect() };
|
||||
for (k, v) in vals {
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
for aux_scale in [1.0, 2.0] {
|
||||
let host = AuxHost(Arc::new(aux_scene(70, 44, aux_scale)));
|
||||
for (scale, offset) in [(1.0, [0.0, 0.0]), (0.5, [0.0, 0.0]), (1.0, [3.0, 2.0])] {
|
||||
let (w, h) = ((size[0] * scale) as u32, (size[1] * scale) as u32);
|
||||
let mut img = pattern(7, w + 2 * offset[0] as u32, h + 2 * offset[1] as u32);
|
||||
if offset[0] > 0.0 {
|
||||
// A padded buffer: transparent margins.
|
||||
for (i, px) in img.data.iter_mut().enumerate() {
|
||||
let (x, y) = (i as u32 % img.width, i as u32 / img.width);
|
||||
if x < offset[0] as u32 || y < offset[1] as u32 || x >= w + offset[0] as u32 || y >= h + offset[1] as u32 {
|
||||
*px = [0.0; 4];
|
||||
}
|
||||
}
|
||||
}
|
||||
let buf = effectcraft_effects::Buf { img, offset, scale };
|
||||
let ctx = || effectcraft_effects::EffectCtx {
|
||||
params: ¶ms,
|
||||
time: 0.25,
|
||||
layer_size: size,
|
||||
seed: 11,
|
||||
adjustment: false,
|
||||
env: EffectEnv { host: Some(&host), ..Default::default() },
|
||||
};
|
||||
let cpu = (spec.render)(&ctx(), buf.clone());
|
||||
let out =
|
||||
effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).expect("the GPU runs the effect");
|
||||
assert_eq!((out.offset, out.scale), (cpu.offset, cpu.scale), "{id}: geometry");
|
||||
let d = diff(&cpu.img, &out.img, 1e-3);
|
||||
assert!(
|
||||
d.over as f64 <= allow * d.total as f64,
|
||||
"{id} {vals:?} aux {aux_scale} scale {scale} offset {offset:?}: {} of {} pixels over 1e-3 (max {}); worst {:?}",
|
||||
d.over,
|
||||
d.total,
|
||||
d.max,
|
||||
d.worst
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Footage frames from [`Pattern`] with [`aux_scene`] channels.
|
||||
struct AuxSource;
|
||||
|
||||
impl FootageSource for AuxSource {
|
||||
fn frame(&self, item: ItemId, f: &Footage, t: Tick) -> Option<Arc<Image>> {
|
||||
Pattern.frame(item, f, t)
|
||||
}
|
||||
fn aux(&self, _item: ItemId, f: &Footage, _t: Tick) -> Option<Arc<AuxChannels>> {
|
||||
Some(Arc::new(aux_scene(f.width, f.height, 1.0)))
|
||||
}
|
||||
}
|
||||
|
||||
fn compare(s: &Scene, o: RenderOpts) -> Option<crate::tests::Diff> {
|
||||
let g = gpu()?;
|
||||
let cpu = Renderer::new(&s.p, &AuxSource, RenderOpts { backend: Backend::Cpu, ..o }).comp_frame_cpu(s.cid, Tick::ZERO);
|
||||
let mut r = Renderer::new(&s.p, &AuxSource, RenderOpts { backend: Backend::Gpu, ..o });
|
||||
r.accel = Some(g);
|
||||
let img = g.render(&r, s.cid, Tick::ZERO).expect("the GPU renders the frame");
|
||||
let mut d = diff(&cpu, &img, tolerance(s.p.settings.bit_depth));
|
||||
d.quantized = s.p.settings.bit_depth != BitDepth::Bpc32;
|
||||
Some(d)
|
||||
}
|
||||
|
||||
/// Direct, then on a rotated footage layer over a background at 8 and 32 bpc (full and half
|
||||
/// resolution).
|
||||
fn case(id: &str, vals: &[(&str, Value)]) {
|
||||
direct(id, vals, 0.0);
|
||||
composited(id, vals, 0.0);
|
||||
}
|
||||
|
||||
fn composited(id: &str, vals: &[(&str, Value)], allow: f64) {
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let mut s = Scene::new(depth);
|
||||
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
|
||||
let bg_id = s.push(bg);
|
||||
let mut l = s.footage(70, 44);
|
||||
let mut vals = vals.to_vec();
|
||||
for v in vals.iter_mut() {
|
||||
if v.1 == Value::Layer(Some(0)) {
|
||||
v.1 = Value::Layer(Some(bg_id.0));
|
||||
}
|
||||
}
|
||||
s.effect(&mut l, id, &vals);
|
||||
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
||||
s.push(l);
|
||||
check(&format!("{id} {vals:?} {depth:?}"), compare(&s, opts()), allow);
|
||||
check(&format!("{id} {vals:?} {depth:?} half"), compare(&s, RenderOpts { scale: 0.5, ..opts() }), allow);
|
||||
}
|
||||
}
|
||||
|
||||
fn e(v: u32) -> Value {
|
||||
Value::Enum(v)
|
||||
}
|
||||
|
||||
fn on() -> Value {
|
||||
Value::Bool(true)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_extract() {
|
||||
case("ec.3d.channelextract", &[("blackPoint", n(0.0)), ("whitePoint", n(1200.0))]);
|
||||
case("ec.3d.channelextract", &[("whitePoint", n(400.0)), ("clampOutput", Value::Bool(false)), ("invertDepthMap", on())]);
|
||||
case("ec.3d.channelextract", &[("blackPoint", n(900.0)), ("whitePoint", n(100.0))]);
|
||||
case("ec.3d.channelextract", &[("channel", e(1)), ("whitePoint", n(5.0))]);
|
||||
case("ec.3d.channelextract", &[("channel", e(2)), ("whitePoint", n(1.0))]);
|
||||
case("ec.3d.channelextract", &[("channel", e(3)), ("whitePoint", n(1.0))]);
|
||||
case("ec.3d.channelextract", &[("channel", e(4)), ("whitePoint", n(1.0)), ("antialias", on())]);
|
||||
case("ec.3d.channelextract", &[("channel", e(5)), ("whitePoint", n(1.0))]);
|
||||
case("ec.3d.channelextract", &[("channel", e(6))]);
|
||||
case("ec.3d.channelextract", &[("channel", e(7)), ("whitePoint", n(2.0))]);
|
||||
case("ec.3d.channelextract", &[("blackPoint", n(0.0)), ("whitePoint", n(1000.0)), ("antialias", on())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn depth_matte_fog_and_dof() {
|
||||
case("ec.3d.depthmatte", &[("depth", n(500.0))]);
|
||||
case("ec.3d.depthmatte", &[("depth", n(400.0)), ("feather", n(300.0)), ("invert", on())]);
|
||||
case("ec.3d.fog3d", &[]);
|
||||
case("ec.3d.fog3d", &[("fogColor", c(0.3, 0.5, 0.9)), ("fogStartDepth", n(200.0)), ("fogEndDepth", n(900.0)), ("scatteringDensity", n(10.0))]);
|
||||
case("ec.3d.fog3d", &[("foggyBackground", Value::Bool(false)), ("fogOpacity", n(70.0))]);
|
||||
composited("ec.3d.fog3d", &[("gradientLayer", Value::Layer(Some(0))), ("layerContribution", n(60.0))], 0.0);
|
||||
case("ec.3d.depthoffield", &[("focalPlane", n(400.0)), ("maximumRadius", n(8.0))]);
|
||||
case("ec.3d.depthoffield", &[("focalPlane", n(200.0)), ("maximumRadius", n(5.0)), ("focalPlaneThickness", n(150.0)), ("focalBias", n(80.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ids_and_cryptomatte() {
|
||||
case("ec.3d.idmatte", &[("idSelection", n(2.0))]);
|
||||
case("ec.3d.idmatte", &[("idSelection", n(3.0)), ("feather", n(3.0)), ("useCoverage", on()), ("invert", on())]);
|
||||
case("ec.3d.idmatte", &[("auxChannel", e(1)), ("idSelection", n(1.0))]);
|
||||
case("ec.3d.identifier", &[]);
|
||||
case("ec.3d.identifier", &[("id", n(2.0))]);
|
||||
case("ec.3d.identifier", &[("display", e(1)), ("id", n(4.0))]);
|
||||
case("ec.3d.identifier", &[("display", e(2)), ("id", n(1.0))]);
|
||||
case("ec.3d.identifier", &[("display", e(3)), ("channelType", e(1))]);
|
||||
case("ec.3d.cryptomatte", &[("selection", Value::Str("beta, delta".into()))]);
|
||||
case("ec.3d.cryptomatte", &[("selection", Value::Str("*a".into())), ("display", e(0))]);
|
||||
case("ec.3d.cryptomatte", &[("selection", Value::Str("gamma".into())), ("display", e(1))]);
|
||||
case("ec.3d.cryptomatte", &[("selection", Value::Str("alpha".into())), ("matteOnly", on())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extractor() {
|
||||
case("ec.3d.extractor", &[("red", Value::Str("Z".into())), ("green", Value::Str("UV.U".into())), ("whitePoint", n(1000.0))]);
|
||||
case(
|
||||
"ec.3d.extractor",
|
||||
&[("red", Value::Str("G".into())), ("blue", Value::Str("Coverage".into())), ("alpha", Value::Str("UV.V".into())), ("unMult", on())],
|
||||
);
|
||||
case("ec.3d.extractor", &[("red", Value::Str("A".into())), ("blackPoint", n(0.2)), ("whitePoint", n(0.8)), ("clip", on())]);
|
||||
case("ec.3d.extractor", &[("green", Value::Str("".into())), ("alpha", Value::Str("nothing".into()))]);
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
//! GPU colour management vs the CPU effects (the oracle): Apply Color LUT, the OCIO effects,
|
||||
//! Color Profile Converter and Lumetri's Input LUT / Look, direct on a buffer (full and half
|
||||
//! resolution, as adjustment) and composited at 8 and 32 bpc.
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
|
||||
use effectcraft_project::BitDepth;
|
||||
use effectcraft_render::RenderOpts;
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
use crate::tests::{Scene, check, compare_at, effect_case, effect_direct, n, opts, set};
|
||||
|
||||
/// [`effect_case`] for nearest-neighbour lattice lookups: an 8 bpc input that lands exactly on a
|
||||
/// rounding boundary between two lattice points may pick the other one (the GPU's division is
|
||||
/// not correctly rounded), so up to `allow` of the composited pixels may differ.
|
||||
fn case_allow(id: &str, vals: &[(&str, Value)], allow: f64) {
|
||||
effect_direct(id, vals, false);
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let mut s = Scene::new(depth);
|
||||
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
|
||||
s.push(bg);
|
||||
let mut l = s.footage(70, 44);
|
||||
s.effect(&mut l, id, vals);
|
||||
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
||||
s.push(l);
|
||||
check(&format!("{id} {depth:?}"), compare_at(&s, opts(), Tick::ZERO), allow);
|
||||
check(&format!("{id} {depth:?} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), allow);
|
||||
}
|
||||
}
|
||||
|
||||
fn e(v: u32) -> Value {
|
||||
Value::Enum(v)
|
||||
}
|
||||
|
||||
fn s(v: &str) -> Value {
|
||||
Value::Str(v.to_string())
|
||||
}
|
||||
|
||||
/// A graded lattice: `f` over an `n`³ grid (red fastest).
|
||||
fn lattice(n: usize, f: impl Fn([f32; 3]) -> [f32; 3]) -> String {
|
||||
let mut out = String::new();
|
||||
for b in 0..n {
|
||||
for g in 0..n {
|
||||
for r in 0..n {
|
||||
let c = f([r, g, b].map(|i| i as f32 / (n - 1) as f32));
|
||||
out += &format!("{} {} {}\n", c[0], c[1], c[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn grade(c: [f32; 3]) -> [f32; 3] {
|
||||
[c[0].powf(0.8) * 1.05, c[1] * 0.9 + 0.08 * (c[0] * 5.0).sin(), c[2] * 0.7 + c[0] * 0.2 + 0.03]
|
||||
}
|
||||
|
||||
fn cube_3d() -> String {
|
||||
format!("TITLE \"grade\"\nLUT_3D_SIZE 9\n{}", lattice(9, grade))
|
||||
}
|
||||
|
||||
fn cube_1d_3d() -> String {
|
||||
let mut s = String::from("LUT_1D_SIZE 16\nLUT_3D_SIZE 7\nDOMAIN_MIN 0 0 0\nDOMAIN_MAX 1.2 1.2 1.2\n");
|
||||
for i in 0..16 {
|
||||
let v = (i as f32 / 15.0).powf(0.6);
|
||||
s += &format!("{} {} {}\n", v, v * 0.95, v.powf(1.2));
|
||||
}
|
||||
s + &lattice(7, grade)
|
||||
}
|
||||
|
||||
fn cube_1d() -> String {
|
||||
let mut s = String::from("LUT_1D_SIZE 32\n");
|
||||
for i in 0..32 {
|
||||
let v = i as f32 / 31.0;
|
||||
s += &format!("{} {} {}\n", v.powf(0.7), v.powf(1.3), 0.05 + v * 0.9);
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn csp() -> String {
|
||||
let mut s = String::from("CSPLUTV100\n3D\n\n3\n0 0.5 1\n0 0.3 1\n2\n0 1\n0 1\n4\n0 0.2 0.6 1\n0 0.4 0.7 1\n\n5 5 5\n");
|
||||
for b in 0..5 {
|
||||
for g in 0..5 {
|
||||
for r in 0..5 {
|
||||
let c = grade([r, g, b].map(|i| i as f32 / 4.0));
|
||||
s += &format!("{} {} {}\n", c[0], c[1], c[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_color_lut() {
|
||||
effect_case("ec.utility.applylut", &[("lut", s(&cube_3d()))]);
|
||||
effect_case("ec.utility.applylut", &[("lut", s(&cube_1d_3d()))]);
|
||||
effect_case("ec.utility.applylut", &[("lut", s(&cube_1d()))]);
|
||||
effect_case("ec.utility.applylut", &[("lut", s("/nonexistent/grade.cube"))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ocio_file_transform() {
|
||||
for interp in 1..4 {
|
||||
effect_case("ec.color.ociofile", &[("file", s(&cube_3d())), ("interpolation", e(interp))]);
|
||||
}
|
||||
case_allow("ec.color.ociofile", &[("file", s(&cube_3d())), ("interpolation", e(0))], 0.01);
|
||||
case_allow("ec.color.ociofile", &[("file", s(&cube_1d_3d())), ("interpolation", e(0))], 0.01);
|
||||
effect_case("ec.color.ociofile", &[("file", s(&cube_1d())), ("direction", e(1))]);
|
||||
effect_case("ec.color.ociofile", &[("file", s(&cube_3d())), ("direction", e(1))]);
|
||||
effect_case("ec.color.ociofile", &[("file", s(&csp())), ("interpolation", e(1))]);
|
||||
effect_case("ec.color.ociofile", &[("file", s(&csp())), ("direction", e(1))]);
|
||||
let ccc = r#"<ColorCorrectionCollection><ColorCorrection id="a"><SOPNode><Slope>1.2 1 0.9</Slope><Offset>0.02 0 -0.01</Offset><Power>1.1 0.9 1</Power></SOPNode></ColorCorrection>
|
||||
<ColorCorrection id='b'><SOPNode><Slope>0.8 0.8 0.8</Slope></SOPNode><SatNode><Saturation>1.4</Saturation></SatNode></ColorCorrection></ColorCorrectionCollection>"#;
|
||||
effect_case("ec.color.ociofile", &[("file", s(ccc))]);
|
||||
effect_case("ec.color.ociofile", &[("file", s(ccc)), ("cccId", s("b")), ("direction", e(1))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ocio_cdl_space_display_look() {
|
||||
let cdl = [("slopeRed", n(1.3)), ("offsetGreen", n(0.05)), ("powerBlue", n(1.4)), ("saturation", n(1.3))];
|
||||
effect_case("ec.color.ociocdl", &cdl);
|
||||
effect_case("ec.color.ociocdl", &[cdl.as_slice(), &[("style", e(1)), ("offsetRed", n(-0.1))]].concat());
|
||||
effect_case("ec.color.ociocdl", &[cdl.as_slice(), &[("direction", e(1))]].concat());
|
||||
effect_case("ec.color.ociocdl", &[cdl.as_slice(), &[("direction", e(1)), ("style", e(1))]].concat());
|
||||
let spaces = effectcraft_effects::find("ec.color.ociocolorspace").unwrap();
|
||||
let names = match &spaces.params.iter().find(|p| p.id == "source").unwrap().ui {
|
||||
effectcraft_project::ParamUi::Popup { options } => options.clone(),
|
||||
_ => vec![],
|
||||
};
|
||||
for (a, z) in [("sRGB", "ACEScg"), ("ACEScct", "Display P3"), ("Gamma 2.4 Rec.2020", "ACES2065-1"), ("CIE-XYZ-D65", "sRGB"), ("Raw", "sRGB")] {
|
||||
let i = |n: &str| e(names.iter().position(|x| x == n).unwrap() as u32);
|
||||
effect_case("ec.color.ociocolorspace", &[("source", i(a)), ("destination", i(z))]);
|
||||
effect_case("ec.color.ociocolorspace", &[("source", i(a)), ("destination", i(z)), ("direction", e(1))]);
|
||||
}
|
||||
for display in 0..4 {
|
||||
for view in 0..2 {
|
||||
effect_case("ec.color.ociodisplay", &[("display", e(display)), ("view", e(view))]);
|
||||
}
|
||||
}
|
||||
// (Inverting PQ and the roll-off together reaches values near 1000 whose matrix mix cancels
|
||||
// in the small channels: f32 agrees to ~1e-5 of the pixel's largest channel there.)
|
||||
effect_case("ec.color.ociodisplay", &[("display", e(3)), ("direction", e(1))]);
|
||||
effect_case("ec.color.ociodisplay", &[("display", e(2)), ("view", e(1)), ("direction", e(1))]);
|
||||
effect_case("ec.color.ociodisplay", &[("display", e(1)), ("direction", e(1))]);
|
||||
effect_case("ec.color.ociolook", &[("look", e(3))]);
|
||||
effect_case("ec.color.ociolook", &[("look", e(1)), ("looks", s("Vivid, -Cool"))]);
|
||||
effect_case("ec.color.ociolook", &[("looks", s("High Contrast")), ("direction", e(1))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_profile_converter() {
|
||||
for (inp, out, intent) in [(0, 1, 1), (1, 3, 0), (4, 5, 2), (6, 2, 3), (5, 1, 0), (3, 4, 1)] {
|
||||
effect_case("ec.utility.colorprofileconverter", &[("inputProfile", e(inp)), ("outputProfile", e(out)), ("intent", e(intent))]);
|
||||
}
|
||||
effect_case(
|
||||
"ec.utility.colorprofileconverter",
|
||||
&[("inputProfile", e(1)), ("outputProfile", e(3)), ("linearizeInputProfile", Value::Bool(true)), ("linearizeOutputProfile", Value::Bool(true))],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lumetri_luts_and_looks() {
|
||||
for look in 2..8 {
|
||||
effect_case("ec.color.lumetri", &[("creative/look", e(look))]);
|
||||
}
|
||||
effect_case("ec.color.lumetri", &[("creative/look", e(4)), ("creative/lookIntensity", n(60.0)), ("basicCorrection/tone/exposure", n(0.3))]);
|
||||
effect_case("ec.color.lumetri", &[("creative/look", e(1)), ("creative/lookFile", s(&cube_3d()))]);
|
||||
effect_case(
|
||||
"ec.color.lumetri",
|
||||
&[("basicCorrection/inputLut", e(1)), ("basicCorrection/inputLutFile", s(&cube_1d_3d())), ("basicCorrection/saturation", n(120.0))],
|
||||
);
|
||||
effect_case(
|
||||
"ec.color.lumetri",
|
||||
&[("basicCorrection/inputLut", e(1)), ("basicCorrection/inputLutFile", s(&cube_1d())), ("creative/look", e(1)), ("creative/lookFile", s(&cube_3d()))],
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
//! GPU simulation render passes vs the CPU effects (the oracle): the frame's plan (sprites,
|
||||
//! pieces, blobs, drops, height grid) is shared, the raster / shading pass runs on each side.
|
||||
//! Direct on a buffer (full and half resolution) and composited at 8 and 32 bpc, at a time
|
||||
//! where the simulations have something to show.
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::BitDepth;
|
||||
use effectcraft_render::RenderOpts;
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
use crate::tests::{Scene, c, check, compare_at, diff, gpu, n, opts, pattern, set};
|
||||
|
||||
fn e(v: u32) -> Value {
|
||||
Value::Enum(v)
|
||||
}
|
||||
|
||||
fn on() -> Value {
|
||||
Value::Bool(true)
|
||||
}
|
||||
|
||||
fn off() -> Value {
|
||||
Value::Bool(false)
|
||||
}
|
||||
|
||||
/// The effect alone on a buffer at layer time `t` (`allow` = fraction of pixels over 1e-3).
|
||||
fn direct(id: &str, vals: &[(&str, Value)], t: f64, allow: f64) {
|
||||
let Some(g) = gpu() else { return };
|
||||
let spec = effectcraft_effects::find(id).unwrap();
|
||||
let size = [70.0, 44.0];
|
||||
let mut params =
|
||||
effectcraft_effects::Params { values: spec.params.iter().map(|ps| (ps.id.to_string(), effectcraft_effects::default_value(ps, size))).collect() };
|
||||
for (k, v) in vals {
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
for scale in [1.0, 0.5] {
|
||||
let img = pattern(7, (size[0] * scale) as u32, (size[1] * scale) as u32);
|
||||
let buf = effectcraft_effects::Buf { img, offset: [0.0, 0.0], scale };
|
||||
let ctx = || effectcraft_effects::EffectCtx { params: ¶ms, time: t, layer_size: size, seed: 11, adjustment: false, env: Default::default() };
|
||||
let cpu = (spec.render)(&ctx(), buf.clone());
|
||||
let out = effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).expect("the GPU runs the effect");
|
||||
assert_eq!((out.offset, out.scale), (cpu.offset, cpu.scale), "{id}: geometry");
|
||||
let d = diff(&cpu.img, &out.img, 1e-3);
|
||||
assert!(
|
||||
d.over as f64 <= allow * d.total as f64,
|
||||
"{id} {vals:?} scale {scale}: {} of {} pixels over 1e-3 (max {}); worst {:?}",
|
||||
d.over,
|
||||
d.total,
|
||||
d.max,
|
||||
d.worst
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A footage layer with the effect over a background and a second footage layer (the target of
|
||||
/// `Value::Layer(Some(0))` parameters), composited at 8 and 32 bpc at comp time `t`.
|
||||
fn composited(id: &str, vals: &[(&str, Value)], t: f64, allow: f64) {
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let mut s = Scene::new(depth);
|
||||
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
|
||||
s.push(bg);
|
||||
let other = s.footage(60, 50);
|
||||
let other_id = s.push(other);
|
||||
let mut l = s.footage(70, 44);
|
||||
let vals: Vec<(&str, Value)> =
|
||||
vals.iter().map(|(k, v)| (*k, if *v == Value::Layer(Some(0)) { Value::Layer(Some(other_id.0)) } else { v.clone() })).collect();
|
||||
s.effect(&mut l, id, &vals);
|
||||
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
||||
s.push(l);
|
||||
let at = Tick::from_seconds_f64(t);
|
||||
check(&format!("{id} {vals:?} {depth:?}"), compare_at(&s, opts(), at), allow);
|
||||
check(&format!("{id} {vals:?} {depth:?} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, at), allow);
|
||||
}
|
||||
}
|
||||
|
||||
fn case(id: &str, vals: &[(&str, Value)], t: f64, allow: f64) {
|
||||
direct(id, vals, t, allow);
|
||||
composited(id, vals, t, allow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weather_and_star_burst() {
|
||||
case("ec.sim.ccrainfall", &[("drops", n(800.0)), ("size", n(3.0))], 0.5, 0.0);
|
||||
case("ec.sim.ccrainfall", &[("drops", n(500.0)), ("extras/appearance", e(1)), ("transferMode", e(0)), ("wind", n(800.0))], 0.7, 0.0);
|
||||
case("ec.sim.ccrainfall", &[("drops", n(300.0)), ("compositeWithOriginal", off()), ("size", n(6.0))], 0.3, 0.0);
|
||||
case("ec.sim.ccsnowfall", &[("flakes", n(600.0)), ("size", n(6.0))], 0.5, 0.0);
|
||||
case("ec.sim.ccsnowfall", &[("flakes", n(400.0)), ("transferMode", e(1)), ("wiggle/wiggleAmount", n(8.0))], 1.2, 0.0);
|
||||
case("ec.sim.ccstarburst", &[("gridSpacing", n(6.0))], 0.4, 0.0);
|
||||
case("ec.sim.ccstarburst", &[("gridSpacing", n(4.0)), ("blendWithOriginal", n(40.0)), ("size", n(160.0))], 0.9, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bubbles_drizzle_mercury() {
|
||||
case("ec.sim.ccbubbles", &[("bubbleAmount", n(60.0)), ("bubbleSize", n(3.0))], 0.5, 0.0);
|
||||
for shading in 1..5 {
|
||||
case(
|
||||
"ec.sim.ccbubbles",
|
||||
&[("bubbleAmount", n(40.0)), ("bubbleSize", n(4.0)), ("shadingType", e(shading)), ("reflectionType", e(shading % 2))],
|
||||
0.8,
|
||||
0.0,
|
||||
);
|
||||
}
|
||||
case("ec.sim.ccdrizzle", &[("dripRate", n(12.0)), ("spreading", n(300.0))], 0.6, 0.0);
|
||||
case("ec.sim.ccdrizzle", &[("dripRate", n(20.0)), ("shading/diffuse", n(40.0)), ("shading/ambient", n(-20.0)), ("light/lightHeight", n(60.0))], 1.1, 0.0);
|
||||
case("ec.sim.ccmrmercury", &[("birthRate", n(4.0)), ("blobBirthSize", n(1.5)), ("blobDeathSize", n(1.0))], 1.0, 0.0);
|
||||
case("ec.sim.ccmrmercury", &[("birthRate", n(3.0)), ("blobInfluence", n(60.0)), ("shading/metal", n(80.0)), ("gravity", n(2.0))], 1.5, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hair_wave_world_foam() {
|
||||
case("ec.sim.cchair", &[("density", n(300.0)), ("length", n(10.0))], 0.0, 0.0);
|
||||
case("ec.sim.cchair", &[("density", n(200.0)), ("length", n(14.0)), ("hairColor/colorInheritance", n(30.0)), ("thickness", n(1.5))], 0.0, 0.0);
|
||||
case("ec.sim.waveworld", &[], 0.5, 0.0);
|
||||
case("ec.sim.waveworld", &[("view", e(1))], 0.8, 0.0);
|
||||
composited("ec.sim.waveworld", &[("view", e(1)), ("ground/ground", Value::Layer(Some(0))), ("heightMapControls/renderDryAreasAs", e(1))], 0.6, 0.0);
|
||||
case("ec.sim.foam", &[], 1.0, 0.0);
|
||||
case("ec.sim.foam", &[("view", e(2))], 1.5, 0.0);
|
||||
case("ec.sim.foam", &[("view", e(2)), ("rendering/bubbleTexture", e(4)), ("rendering/blendMode", e(2))], 1.2, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pieces() {
|
||||
case("ec.sim.carddance", &[("rows", n(5.0)), ("columns", n(7.0)), ("zRotation/zRotMultiplier", n(20.0))], 0.0, 0.0);
|
||||
composited("ec.sim.carddance", &[("rows", n(4.0)), ("backLayer", Value::Layer(Some(0))), ("yRotation/yRotMultiplier", n(150.0))], 0.0, 0.0);
|
||||
case("ec.sim.shatter", &[("view", e(0))], 1.0, 0.0);
|
||||
case("ec.sim.shatter", &[("view", e(0)), ("shape/pattern", e(2)), ("physics/gravity", n(4.0))], 1.6, 0.0);
|
||||
case("ec.transition.cardwipe", &[("completion", n(40.0))], 0.0, 0.0);
|
||||
composited("ec.transition.cardwipe", &[("completion", n(60.0)), ("backLayer", Value::Layer(Some(0))), ("flipAxis", e(1))], 0.0, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caustics() {
|
||||
case("ec.sim.caustics", &[], 0.0, 0.0);
|
||||
composited(
|
||||
"ec.sim.caustics",
|
||||
&[("water/waterSurface", Value::Layer(Some(0))), ("water/waveHeight", n(0.3)), ("water/smoothing", n(4.0)), ("lighting/lightType", e(1))],
|
||||
0.0,
|
||||
0.0,
|
||||
);
|
||||
composited(
|
||||
"ec.sim.caustics",
|
||||
&[
|
||||
("water/waterSurface", Value::Layer(Some(0))),
|
||||
("sky/sky", Value::Layer(Some(0))),
|
||||
("water/surfaceOpacity", n(0.6)),
|
||||
("bottom/repeatMode", e(2)),
|
||||
("bottom/scaling", n(1.4)),
|
||||
("bottom/blur", n(3.0)),
|
||||
("sky/repeatMode", e(1)),
|
||||
],
|
||||
0.0,
|
||||
0.0,
|
||||
);
|
||||
let _ = (c(0.0, 0.0, 0.0), on());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpu_only_views_fall_back() {
|
||||
// Shatter's wireframe views and Foam's User Defined texture render on the CPU; composited
|
||||
// frames still match.
|
||||
composited("ec.sim.shatter", &[], 1.0, 0.0);
|
||||
composited("ec.sim.foam", &[("view", e(2)), ("rendering/bubbleTexture", e(5)), ("rendering/bubbleTextureLayer", Value::Layer(Some(0)))], 1.0, 0.0);
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
//! GPU Immersive Video family vs the CPU effects (the oracle): direct on a buffer (full and half
|
||||
//! resolution, as adjustment) and composited at 8 and 32 bpc, in mono and both stereo layouts.
|
||||
//!
|
||||
//! Exception: re-projections that cross a cube-map face edge or a fisheye rim may pick the
|
||||
//! neighbouring face / side for directions within f32 rounding of the edge (the CPU decides in
|
||||
//! f64), so up to 0.5 % of the pixels of those cases may differ.
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::BitDepth;
|
||||
use effectcraft_render::RenderOpts;
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
use crate::tests::{Scene, c, check, compare_at, diff, gpu, n, opts, pattern, set};
|
||||
|
||||
fn e(v: u32) -> Value {
|
||||
Value::Enum(v)
|
||||
}
|
||||
|
||||
fn on() -> Value {
|
||||
Value::Bool(true)
|
||||
}
|
||||
|
||||
/// The effect alone on a buffer (`allow` = fraction of pixels allowed over 1e-3).
|
||||
fn direct(id: &str, vals: &[(&str, Value)], allow: f64) {
|
||||
let Some(g) = gpu() else { return };
|
||||
let spec = effectcraft_effects::find(id).unwrap();
|
||||
let size = [96.0, 48.0];
|
||||
let mut params =
|
||||
effectcraft_effects::Params { values: spec.params.iter().map(|ps| (ps.id.to_string(), effectcraft_effects::default_value(ps, size))).collect() };
|
||||
for (k, v) in vals {
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
for adjustment in [false, true] {
|
||||
for scale in [1.0, 0.5] {
|
||||
let img = pattern(7, (size[0] * scale) as u32, (size[1] * scale) as u32);
|
||||
let buf = effectcraft_effects::Buf { img, offset: [0.0, 0.0], scale };
|
||||
let ctx = || effectcraft_effects::EffectCtx { params: ¶ms, time: 0.4, layer_size: size, seed: 11, adjustment, env: Default::default() };
|
||||
let cpu = (spec.render)(&ctx(), buf.clone());
|
||||
let out =
|
||||
effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).expect("the GPU runs the effect");
|
||||
assert_eq!((out.offset, out.scale), (cpu.offset, cpu.scale), "{id}: geometry");
|
||||
let d = diff(&cpu.img, &out.img, 1e-3);
|
||||
assert!(
|
||||
d.over as f64 <= allow * d.total as f64,
|
||||
"{id} {vals:?} adj {adjustment} scale {scale}: {} of {} pixels over 1e-3 (max {}); worst {:?}",
|
||||
d.over,
|
||||
d.total,
|
||||
d.max,
|
||||
d.worst
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn case_allow(id: &str, vals: &[(&str, Value)], allow: f64) {
|
||||
direct(id, vals, allow);
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let mut s = Scene::new(depth);
|
||||
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
|
||||
s.push(bg);
|
||||
let mut l = s.footage(96, 48);
|
||||
s.effect(&mut l, id, vals);
|
||||
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
||||
s.push(l);
|
||||
check(&format!("{id} {vals:?} {depth:?}"), compare_at(&s, opts(), Tick::ZERO), allow);
|
||||
check(&format!("{id} {vals:?} {depth:?} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), allow);
|
||||
}
|
||||
}
|
||||
|
||||
fn case(id: &str, vals: &[(&str, Value)]) {
|
||||
case_allow(id, vals, 0.0);
|
||||
}
|
||||
|
||||
/// `vals` in mono and in both stereo layouts.
|
||||
fn layouts(id: &str, vals: &[(&str, Value)]) {
|
||||
for l in 0..3 {
|
||||
case(id, &[vals, &[("frameLayout", e(l))]].concat());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blur_sharpen_glow() {
|
||||
layouts("ec.vr.blur", &[("blurriness", n(12.0))]);
|
||||
case("ec.vr.blur", &[("blurriness", n(60.0))]);
|
||||
layouts("ec.vr.sharpen", &[("sharpenAmount", n(60.0))]);
|
||||
layouts("ec.vr.glow", &[("luminanceThreshold", n(40.0)), ("glowRadius", n(10.0))]);
|
||||
case("ec.vr.glow", &[("useTintColor", on()), ("glowSaturation", n(150.0)), ("glowBrightness", n(250.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn denoise() {
|
||||
layouts("ec.vr.denoise", &[("noiseLevel", n(40.0))]);
|
||||
case("ec.vr.denoise", &[("noiseLevel", n(60.0)), ("detail", n(30.0))]);
|
||||
case("ec.vr.denoise", &[("noiseType", e(1)), ("noiseLevel", n(50.0))]);
|
||||
case("ec.vr.denoise", &[("noiseType", e(1)), ("noiseLevel", n(80.0)), ("frameLayout", e(1))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotate_and_planes() {
|
||||
layouts("ec.vr.rotatesphere", &[("tilt", n(20.0)), ("pan", n(35.0)), ("roll", n(-10.0))]);
|
||||
case("ec.vr.rotatesphere", &[("tilt", n(-40.0)), ("pan", n(170.0)), ("invertRotation", on())]);
|
||||
layouts("ec.vr.spheretoplane", &[("horizontalFov", n(100.0)), ("tilt", n(10.0)), ("pan", n(-30.0))]);
|
||||
case("ec.vr.planetosphere", &[("scale", n(80.0)), ("pan", n(20.0)), ("tilt", n(-15.0))]);
|
||||
case("ec.vr.planetosphere", &[("scale", n(120.0)), ("feather", n(30.0)), ("roll", n(25.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converter() {
|
||||
for from in 0..9 {
|
||||
for to in [0u32, 1, 7] {
|
||||
if from == to {
|
||||
continue;
|
||||
}
|
||||
case_allow(
|
||||
"ec.vr.converter",
|
||||
&[("sourceProjection", e(from)), ("targetProjection", e(to)), ("sourceHorizontalFov", n(200.0)), ("pan", n(15.0))],
|
||||
0.005,
|
||||
);
|
||||
}
|
||||
}
|
||||
for to in [2u32, 3, 4, 5, 6, 8] {
|
||||
case_allow("ec.vr.converter", &[("targetProjection", e(to)), ("targetHorizontalFov", n(120.0)), ("tilt", n(12.0))], 0.005);
|
||||
}
|
||||
case_allow("ec.vr.converter", &[("targetProjection", e(1)), ("frameLayout", e(1))], 0.005);
|
||||
case_allow("ec.vr.converter", &[("targetProjection", e(6)), ("frameLayout", e(2)), ("roll", n(30.0))], 0.005);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated() {
|
||||
layouts("ec.vr.chromaticaberrations", &[("aberrationRed", n(40.0)), ("aberrationBlue", n(-30.0))]);
|
||||
case("ec.vr.chromaticaberrations", &[("centerTilt", n(30.0)), ("centerPan", n(-60.0)), ("falloff", n(10.0)), ("falloffInvert", on())]);
|
||||
layouts("ec.vr.colorgradients", &[]);
|
||||
case("ec.vr.colorgradients", &[("blend", n(5.0)), ("opacity", n(60.0)), ("blendingMode", e(5)), ("enablePoint5", on())]);
|
||||
for mode in 1..5 {
|
||||
case("ec.vr.colorgradients", &[("blendingMode", e(mode)), ("color2", c(0.2, 0.9, 0.4))]);
|
||||
}
|
||||
layouts("ec.vr.fractalnoise", &[]);
|
||||
for kind in 1..4 {
|
||||
case("ec.vr.fractalnoise", &[("fractalType", e(kind)), ("complexity", n(4.5)), ("evolution", n(70.0)), ("transform/pan", n(30.0))]);
|
||||
}
|
||||
case("ec.vr.fractalnoise", &[("blendingMode", e(4)), ("opacity", n(70.0)), ("invert", on()), ("transform/scale", n(250.0))]);
|
||||
layouts("ec.vr.digitalglitch", &[]);
|
||||
case(
|
||||
"ec.vr.digitalglitch",
|
||||
&[
|
||||
("distortionEvolution", n(100.0)),
|
||||
("geometric/verticalDisplacement", n(80.0)),
|
||||
("target/radius", n(60.0)),
|
||||
("target/feather", n(30.0)),
|
||||
("target/pan", n(40.0)),
|
||||
("scanlineSpacing", n(3.0)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -208,11 +208,15 @@ Preserve Transparency / blend kernels (a 3D track matte renders solo through the
|
|||
Environment Light Background layers draw their sky in a kernel (`adv_sky`, from
|
||||
`Renderer::sky_draw`) before the rest of the run, in Classic and Advanced 3D comps. Wireframe
|
||||
outlines draw on the GPU from the CPU's pixel list (`Renderer::wireframe_pixels`). GPU effects
|
||||
(`effects::GPU_EFFECTS`, 196 of them: blurs, colour correction, keying incl. Key Light, mattes,
|
||||
(`effects::GPU_EFFECTS`, 236 of them: blurs, colour correction, keying incl. Key Light, mattes,
|
||||
channel, stylize, distortion and warps (Warp, Bezier Warp, Smear, Reshape, CC Bend It, CC Page
|
||||
Turn, CC Bender, CC Blobbylize), Cartoon, bevels, shapes, the transitions (CC light family, CC
|
||||
transitions, Block Dissolve), perspective (Radial Shadow, CC Cylinder / Sphere / Spotlight /
|
||||
Environment, 3D Glasses), generators, noise, grain, text (Numbers, Timecode) and time; see
|
||||
Environment, 3D Glasses), generators, noise, grain, text (Numbers, Timecode), time, 3D Channel (aux channels as an
|
||||
extra texture), Immersive Video, colour management (colour programs from
|
||||
`effects::color_program`, LUTs in a storage buffer) and the simulations' render passes (the
|
||||
simulation stays on the CPU and hands its plan — sprites, pieces, blobs, grids — to a tiled
|
||||
rasteriser); see
|
||||
[effects.md](effects.md)) repeat the CPU effect's steps (padding, box radii, parameters, hashes)
|
||||
as kernels, in one module per family (`gpu::fx_*` with `shaders/fx_*.wgsl`); consecutive GPU
|
||||
effects run as one chain with one upload and one readback. Statistics that need the whole frame
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
EffectCraft ships 306 effects with 3009 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over.
|
||||
|
||||
- **GPU**: 196 effects also run on the GPU compositor with identical results.
|
||||
- **GPU**: 236 effects also run on the GPU compositor with identical results.
|
||||
- **32**: 306 effects process 32-bit float (HDR, overbright) pixels without clamping.
|
||||
- **Status**: 306 implemented in full, 0 partial (what is missing is listed).
|
||||
|
||||
|
|
@ -15,14 +15,14 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| 3D Channel Extract | `ec.3d.channelextract` | 6 | | 32 | Implemented |
|
||||
| Cryptomatte | `ec.3d.cryptomatte` | 4 | | 32 | Implemented |
|
||||
| Depth Matte | `ec.3d.depthmatte` | 3 | | 32 | Implemented |
|
||||
| Depth of Field | `ec.3d.depthoffield` | 4 | | 32 | Implemented |
|
||||
| EXtractoR | `ec.3d.extractor` | 8 | | 32 | Implemented |
|
||||
| Fog 3D | `ec.3d.fog3d` | 8 | | 32 | Implemented |
|
||||
| ID Matte | `ec.3d.idmatte` | 5 | | 32 | Implemented |
|
||||
| IDentifier | `ec.3d.identifier` | 3 | | 32 | Implemented |
|
||||
| 3D Channel Extract | `ec.3d.channelextract` | 6 | GPU | 32 | Implemented |
|
||||
| Cryptomatte | `ec.3d.cryptomatte` | 4 | GPU | 32 | Implemented |
|
||||
| Depth Matte | `ec.3d.depthmatte` | 3 | GPU | 32 | Implemented |
|
||||
| Depth of Field | `ec.3d.depthoffield` | 4 | GPU | 32 | Implemented |
|
||||
| EXtractoR | `ec.3d.extractor` | 8 | GPU | 32 | Implemented |
|
||||
| Fog 3D | `ec.3d.fog3d` | 8 | GPU | 32 | Implemented |
|
||||
| ID Matte | `ec.3d.idmatte` | 5 | GPU | 32 | Implemented |
|
||||
| IDentifier | `ec.3d.identifier` | 3 | GPU | 32 | Implemented |
|
||||
|
||||
## Audio
|
||||
|
||||
|
|
@ -112,11 +112,11 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| Levels | `ec.color.levels` | 36 | GPU | 32 | Implemented |
|
||||
| Levels (Individual Controls) | `ec.color.levelsic` | 27 | GPU | 32 | Implemented |
|
||||
| Lumetri Color | `ec.color.lumetri` | 66 | GPU | 32 | Implemented |
|
||||
| OCIO CDL Transform | `ec.color.ociocdl` | 12 | | 32 | Implemented |
|
||||
| OCIO Color Space Transform | `ec.color.ociocolorspace` | 7 | | 32 | Implemented |
|
||||
| OCIO Display Transform | `ec.color.ociodisplay` | 9 | | 32 | Implemented |
|
||||
| OCIO File Transform | `ec.color.ociofile` | 4 | | 32 | Implemented |
|
||||
| OCIO Look Transform | `ec.color.ociolook` | 6 | | 32 | Implemented |
|
||||
| OCIO CDL Transform | `ec.color.ociocdl` | 12 | GPU | 32 | Implemented |
|
||||
| OCIO Color Space Transform | `ec.color.ociocolorspace` | 7 | GPU | 32 | Implemented |
|
||||
| OCIO Display Transform | `ec.color.ociodisplay` | 9 | GPU | 32 | Implemented |
|
||||
| OCIO File Transform | `ec.color.ociofile` | 4 | GPU | 32 | Implemented |
|
||||
| OCIO Look Transform | `ec.color.ociolook` | 6 | GPU | 32 | Implemented |
|
||||
| PS Arbitrary Map | `ec.color.psarbitrarymap` | 2 | GPU | 32 | Implemented |
|
||||
| Photo Filter | `ec.color.photofilter` | 4 | GPU | 32 | Implemented |
|
||||
| Selective Color | `ec.color.selectivecolor` | 38 | GPU | 32 | Implemented |
|
||||
|
|
@ -219,18 +219,18 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| VR Blur | `ec.vr.blur` | 2 | | 32 | Implemented |
|
||||
| VR Chromatic Aberrations | `ec.vr.chromaticaberrations` | 8 | | 32 | Implemented |
|
||||
| VR Color Gradients | `ec.vr.colorgradients` | 19 | | 32 | Implemented |
|
||||
| VR Converter | `ec.vr.converter` | 8 | | 32 | Implemented |
|
||||
| VR De-Noise | `ec.vr.denoise` | 4 | | 32 | Implemented |
|
||||
| VR Digital Glitch | `ec.vr.digitalglitch` | 21 | | 32 | Implemented |
|
||||
| VR Fractal Noise | `ec.vr.fractalnoise` | 16 | | 32 | Implemented |
|
||||
| VR Glow | `ec.vr.glow` | 7 | | 32 | Implemented |
|
||||
| VR Plane to Sphere | `ec.vr.planetosphere` | 6 | | 32 | Implemented |
|
||||
| VR Rotate Sphere | `ec.vr.rotatesphere` | 5 | | 32 | Implemented |
|
||||
| VR Sharpen | `ec.vr.sharpen` | 2 | | 32 | Implemented |
|
||||
| VR Sphere to Plane | `ec.vr.spheretoplane` | 5 | | 32 | Implemented |
|
||||
| VR Blur | `ec.vr.blur` | 2 | GPU | 32 | Implemented |
|
||||
| VR Chromatic Aberrations | `ec.vr.chromaticaberrations` | 8 | GPU | 32 | Implemented |
|
||||
| VR Color Gradients | `ec.vr.colorgradients` | 19 | GPU | 32 | Implemented |
|
||||
| VR Converter | `ec.vr.converter` | 8 | GPU | 32 | Implemented |
|
||||
| VR De-Noise | `ec.vr.denoise` | 4 | GPU | 32 | Implemented |
|
||||
| VR Digital Glitch | `ec.vr.digitalglitch` | 21 | GPU | 32 | Implemented |
|
||||
| VR Fractal Noise | `ec.vr.fractalnoise` | 16 | GPU | 32 | Implemented |
|
||||
| VR Glow | `ec.vr.glow` | 7 | GPU | 32 | Implemented |
|
||||
| VR Plane to Sphere | `ec.vr.planetosphere` | 6 | GPU | 32 | Implemented |
|
||||
| VR Rotate Sphere | `ec.vr.rotatesphere` | 5 | GPU | 32 | Implemented |
|
||||
| VR Sharpen | `ec.vr.sharpen` | 2 | GPU | 32 | Implemented |
|
||||
| VR Sphere to Plane | `ec.vr.spheretoplane` | 5 | GPU | 32 | Implemented |
|
||||
|
||||
## Keying
|
||||
|
||||
|
|
@ -318,23 +318,23 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| CC Ball Action | `ec.sim.ccballaction` | 8 | | 32 | Implemented |
|
||||
| CC Bubbles | `ec.sim.ccbubbles` | 7 | | 32 | Implemented |
|
||||
| CC Drizzle | `ec.sim.ccdrizzle` | 14 | | 32 | Implemented |
|
||||
| CC Hair | `ec.sim.cchair` | 20 | | 32 | Implemented |
|
||||
| CC Mr. Mercury | `ec.sim.ccmrmercury` | 23 | | 32 | Implemented |
|
||||
| CC Bubbles | `ec.sim.ccbubbles` | 7 | GPU | 32 | Implemented |
|
||||
| CC Drizzle | `ec.sim.ccdrizzle` | 14 | GPU | 32 | Implemented |
|
||||
| CC Hair | `ec.sim.cchair` | 20 | GPU | 32 | Implemented |
|
||||
| CC Mr. Mercury | `ec.sim.ccmrmercury` | 23 | GPU | 32 | Implemented |
|
||||
| CC Particle Systems II | `ec.sim.ccparticlesystems2` | 21 | | 32 | Implemented |
|
||||
| CC Particle World | `ec.sim.ccparticleworld` | 30 | | 32 | Implemented |
|
||||
| CC Pixel Polly | `ec.sim.ccpixelpolly` | 10 | | 32 | Implemented |
|
||||
| CC Rainfall | `ec.sim.ccrainfall` | 14 | | 32 | Implemented |
|
||||
| CC Rainfall | `ec.sim.ccrainfall` | 14 | GPU | 32 | Implemented |
|
||||
| CC Scatterize | `ec.sim.ccscatterize` | 4 | | 32 | Implemented |
|
||||
| CC Snowfall | `ec.sim.ccsnowfall` | 19 | | 32 | Implemented |
|
||||
| CC Star Burst | `ec.sim.ccstarburst` | 6 | | 32 | Implemented |
|
||||
| Card Dance | `ec.sim.carddance` | 53 | | 32 | Implemented |
|
||||
| Caustics | `ec.sim.caustics` | 28 | | 32 | Implemented |
|
||||
| Foam | `ec.sim.foam` | 36 | | 32 | Implemented |
|
||||
| CC Snowfall | `ec.sim.ccsnowfall` | 19 | GPU | 32 | Implemented |
|
||||
| CC Star Burst | `ec.sim.ccstarburst` | 6 | GPU | 32 | Implemented |
|
||||
| Card Dance | `ec.sim.carddance` | 53 | GPU | 32 | Implemented |
|
||||
| Caustics | `ec.sim.caustics` | 28 | GPU | 32 | Implemented |
|
||||
| Foam | `ec.sim.foam` | 36 | GPU | 32 | Implemented |
|
||||
| Particle Playground | `ec.sim.particleplayground` | 92 | | 32 | Implemented |
|
||||
| Shatter | `ec.sim.shatter` | 60 | | 32 | Implemented |
|
||||
| Wave World | `ec.sim.waveworld` | 35 | | 32 | Implemented |
|
||||
| Shatter | `ec.sim.shatter` | 60 | GPU | 32 | Implemented |
|
||||
| Wave World | `ec.sim.waveworld` | 35 | GPU | 32 | Implemented |
|
||||
|
||||
## Stylize
|
||||
|
||||
|
|
@ -401,7 +401,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| CC Scale Wipe | `ec.transition.ccscalewipe` | 3 | GPU | 32 | Implemented |
|
||||
| CC Twister | `ec.transition.cctwister` | 5 | GPU | 32 | Implemented |
|
||||
| CC WarpoMatic | `ec.transition.ccwarpomatic` | 8 | GPU | 32 | Implemented |
|
||||
| Card Wipe | `ec.transition.cardwipe` | 44 | | 32 | Implemented |
|
||||
| Card Wipe | `ec.transition.cardwipe` | 44 | GPU | 32 | Implemented |
|
||||
| Gradient Wipe | `ec.transition.gradientwipe` | 5 | GPU | 32 | Implemented |
|
||||
| Iris Wipe | `ec.transition.iriswipe` | 7 | GPU | 32 | Implemented |
|
||||
| Linear Wipe | `ec.transition.linearwipe` | 3 | GPU | 32 | Implemented |
|
||||
|
|
@ -412,10 +412,10 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Apply Color LUT | `ec.utility.applylut` | 0 | | 32 | Implemented |
|
||||
| Apply Color LUT | `ec.utility.applylut` | 0 | GPU | 32 | Implemented |
|
||||
| CC Overbrights | `ec.utility.ccoverbrights` | 2 | | 32 | Implemented |
|
||||
| Cineon Converter | `ec.utility.cineon` | 7 | | 32 | Implemented |
|
||||
| Color Profile Converter | `ec.utility.colorprofileconverter` | 7 | | 32 | Implemented |
|
||||
| Color Profile Converter | `ec.utility.colorprofileconverter` | 7 | GPU | 32 | Implemented |
|
||||
| Face Measurements | `ec.utility.facemeasurements` | 14 | | 32 | Implemented |
|
||||
| Face Track Points | `ec.utility.facetrackpoints` | 26 | | 32 | Implemented |
|
||||
| Grow Bounds | `ec.utility.growbounds` | 1 | | 32 | Implemented |
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ How close EffectCraft is to After Effects 2026, feature by feature, and how much
|
|||
| Unweighted | ≈ 97% (half-credit) / 99% (fractions) |
|
||||
| P0 / P1 / P2 | ≈ 99% / 96% / 90% (half-credit); 99.8% / 99.2% / 97.5% (fractions) |
|
||||
| Features done / partial / missing | 87 / 5 / 0 of 92 |
|
||||
| **Effects** | **306** effects (all After Effects 2026 effects), every one implemented in full ([effects.md](effects.md)); 196 run on the GPU |
|
||||
| **Effects** | **306** effects (all After Effects 2026 effects), every one implemented in full ([effects.md](effects.md)); 236 run on the GPU |
|
||||
| Disabled menu entries left | 1, on purpose: Import ▸ Vanishing Point (.vpe), whose format has no public specification |
|
||||
| Remaining work | ≈ 9–11 agent-hours at the pace measured so far (≈ 29 on the conservative audit scale); ≈ 6–7 without the learned-model items |
|
||||
| **Wall-clock estimate** | **≈ 2–2.5 hours** with five agents in parallel; ≈ 1.5 hours for 100% of P0 + P1 |
|
||||
|
|
@ -19,7 +19,7 @@ Partial features:
|
|||
|
||||
| id | Tier | Done | What is missing |
|
||||
|---|---|---|---|
|
||||
| EFF-5 GPU effects | P1 | 0.85 | GPU kernels for the CC light / transition families, Numbers and Timecode, 3D Channel, the VR family, OCIO / LUT, Time effects and the simulations; Advanced 3D runs with blend modes, track mattes or environment backgrounds composite on the CPU |
|
||||
| EFF-5 GPU effects | P1 | 0.97 | GPU kernels for the audio-driven effects and the stepped particle systems' sprite pass (the CC light / transition families, Numbers, Timecode and Time effects landed in M13.22; 3D Channel, the VR family, OCIO / LUT and the simulations' render passes in M13.23); custom `.ocio` configs, Shatter's wireframe views and Foam's texture / environment / flow-map extras render on the CPU |
|
||||
| WEB-1 Web app | P0 | 0.97 | GPU effects in Render Queue / analysis job workers (they render on their CPU); layer buffers in the browser's disk cache (frames only); threads inside one engine instance (the decided design is one engine instance per worker; a threaded build needs nightly `build-std`). M13.24 added GPU effects and the GPU compositor in the frame workers (their own WebGPU devices, deferred readbacks: frames render in passes; Backend Auto per comp), the disk cache in the Origin Private File System (written by the workers with sync access handles, LRU under the settings' limit, served after a reload), the storage manager (Settings ▸ Disk ▸ Browser Storage, `storage.*`) and fixed a WGSL constant Chrome rejected (it disabled every GPU kernel in Chrome) |
|
||||
| UI-7 Home screen | P1 | 0.95 | A "start from a template" gallery |
|
||||
| MSK-4 Roto Brush | P2 | 0.85 | Segmentation is classical (graph cut + optical flow), not a learned model |
|
||||
|
|
@ -156,11 +156,11 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
|
|||
| Text | ≈ 96% | 0.3 | no extruded strokes (M13.12: the Variable Font Axes animator re-spaces the text — advances follow the animated axes; M13.6: variable font axes in the character style — `layer.setText variations`, the Character panel's Variable Font Axes fields — shape with HVAR / gvar advances and draw at that design-space position; OpenType features — stylistic sets, discretionary ligatures, contextual / stylistic alternates, swash, titling, ordinals, fractions, figure styles, true small caps / all small caps and superior / inferior glyphs with faux fallback — per character with the Character panel's OpenType popup and `text.fontFeatures` landed in M13.2; vertical Roman / Tate-Chu-Yoko, forced LTR paragraphs, caret on animated and path text, Variable Font Axes and Lottie style runs landed in M13.5; extruded, bevelled text in M7.6; per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API in M9.9–M9.10) |
|
||||
| Web | 97% | 0.2 | GPU effects in Render Queue / analysis workers (job workers render on their CPU), layer buffers in the browser's disk cache (frames only), no shared-memory threads inside one engine instance (the decided design is one engine instance per worker; a threaded build needs nightly `build-std`) (browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2; viewer frames in frame workers fed by project diffs, Roto Brush propagation in a worker, non-blocking `wait: true` jobs and a browser Media Browser (File System Access folders, browser storage) in M13.10; WEB-1 in M13.24: GPU effects and the GPU compositor in the frame workers on their own WebGPU devices with deferred readbacks (frames render in passes; Backend Auto per comp), the disk cache in the Origin Private File System (written by the workers with sync access handles, LRU under the settings' limit, served after a reload), the storage manager (Settings ▸ Disk ▸ Browser Storage, `storage.info` / `storage.persist` / `storage.clear`), and a WGSL constant Chrome rejected (it disabled every GPU kernel in Chrome) fixed) |
|
||||
| 3D | 88% | 5.5 | multi-view layouts, the Extended Viewer for Advanced 3D comps (Classic 3D Extended Viewer landed in M13.5 UI completion); collapsed precomps of another size seen through the parent's camera render (fixed in M13.2); stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D depth of field with the iris and highlight options, collapsed precomps as real Advanced 3D geometry and extruded text/shape strokes landed in M13.8; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6; Advanced 3D end to end on the GPU (motion blur, iris depth of field, compositing) in M13.11 |
|
||||
| Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (196 run on the GPU since M13.22, EFF-5: the CC light family, the CC transitions, Block Dissolve, Radial Shadow, CC Bender / Blobbylize / Cylinder / Sphere / Spotlight / Environment, 3D Glasses, Numbers, Timecode and the time effects joined the 166 of M13.13; simulations, 3D channel, VR, audio-driven, OCIO and Card Wipe still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
|
||||
| Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (236 run on the GPU since M13.23, EFF-5: M13.23 added the 3D Channel and Immersive Video families, Apply Color LUT, the OCIO effects, Color Profile Converter and the simulations' render passes (Card Wipe included); M13.22 added the CC light family, the CC transitions, Block Dissolve, Radial Shadow, CC Bender / Blobbylize / Cylinder / Sphere / Spotlight / Environment, 3D Glasses, Numbers, Timecode and the time effects joined the 166 of M13.13; audio-driven effects and the stepped particle systems still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
|
||||
| Interface | 75% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (the Home ▸ Templates gallery (eight original built-in templates, user templates from File ▸ Save as Template…) and View ▸ Simulate Output ▸ My Custom RGB… landed in M13.25; Timeline layer reordering by drag, a non-snapping viewer pan, a working rename field and twirl arrows, and the full set of property reveal shortcuts — double presses, Alt+Shift keyframes, Ctrl+` — landed in M13.17–M13.20; the Home ▸ Learn tab with interactive tutorials and a UI fidelity pass landed in M13.5 UI completion; visual editors for Lumetri RGB / hue-saturation curves, Colorama's output cycle wheel, Glow's colour map and Reshape's correspondence points (viewer handles), a scrolling Preview panel and AE-style Composition / Timeline tabs (close, label swatch, viewer lock) landed in M13.10; Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) |
|
||||
| Project | ≈ 68% | 5.5 | auto-save, folder moves, OCIO displays beyond the built-in tone map (Color Engine with OCIO/ACES working spaces, HDR compand/tone mapping, Rec. 2100 PQ/HLG output, Feet + Frames, display colour management, Simulate Output and the locked viewer landed in M7.7; proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3) |
|
||||
| Masks & roto | 74% | 5.0 | Roto Brush's learned (3.0) segmentation model (variable-width mask feather points with the Mask Feather tool landed in M13.5; mask tracking and Mask Interpolation landed in M6.6; Roto Brush & Refine Edge with graph-cut segmentation, flow propagation, edge matting, decontamination and Freeze in M6.7) |
|
||||
| Preview | 77% | 2.8 | GPU effects for simulations, 3D channel, VR, OCIO and Card Wipe (Advanced 3D layers with blend modes / track mattes / Preserve Transparency and environment backgrounds composite on the GPU since M13.22; pooled GPU textures and fused quantisation since M13.13: Lower Third GPU warm 21 → 4 ms/frame, and Auto now picks the CPU or the GPU per comp from measured frame times; Advanced 3D runs — raster, motion blur, iris depth of field, compositing — and wireframes run on the GPU since M13.11: CPU warm 3394 → GPU warm 290 ms/frame at 1920×1080 (11.7×; Half 968 → 85 ms), GPU ≠ CPU on 0.005 % of pixels, on an M4 Pro under load; Classic 3D bokeh depth of field — iris shapes, highlights, fringe, progressive blur on tilted planes — runs in WGSL since M13.6: 3D Showcase GPU warm 205 → 54 ms/frame at full size on an M4 Pro; the Preview panel's five shortcuts with their own Include / Loop / Cache Before Playback / Range / Play From / Frame Rate / Skip / Resolution / Full Screen / stop options and `playback.settings.get/set` landed in M13.8; Classic 3D runs and adjustment layers composite on the GPU since M12.7; persistent disk cache with the blue cache bar landed; region of interest, snapshots, exposure and Fast Previews landed in M0.13) |
|
||||
| Preview | 77% | 2.8 | GPU effects for the audio-driven effects and the stepped particle systems (3D channel, VR, OCIO, Card Wipe and the simulations' render passes run on the GPU since M13.23; Advanced 3D layers with blend modes / track mattes / Preserve Transparency and environment backgrounds composite on the GPU since M13.22; pooled GPU textures and fused quantisation since M13.13: Lower Third GPU warm 21 → 4 ms/frame, and Auto now picks the CPU or the GPU per comp from measured frame times; Advanced 3D runs — raster, motion blur, iris depth of field, compositing — and wireframes run on the GPU since M13.11: CPU warm 3394 → GPU warm 290 ms/frame at 1920×1080 (11.7×; Half 968 → 85 ms), GPU ≠ CPU on 0.005 % of pixels, on an M4 Pro under load; Classic 3D bokeh depth of field — iris shapes, highlights, fringe, progressive blur on tilted planes — runs in WGSL since M13.6: 3D Showcase GPU warm 205 → 54 ms/frame at full size on an M4 Pro; the Preview panel's five shortcuts with their own Include / Loop / Cache Before Playback / Range / Play From / Frame Rate / Skip / Resolution / Full Screen / stop options and `playback.settings.get/set` landed in M13.8; Classic 3D runs and adjustment layers composite on the GPU since M12.7; persistent disk cache with the blue cache bar landed; region of interest, snapshots, exposure and Fast Previews landed in M0.13) |
|
||||
| Tracking | ≈ 96% | 0.3 | face tracking is a classical (skin model + feature components + shape model) fitter, not a learned detector: profile views and occluded faces are weak; Rolling Shutter Ripple is approximated by Subspace Warp's mesh density (face tracking (Outline Only / Detailed Features with Face Track Points and Extract & Copy Face Measurements), Subspace Warp's content-preserving mesh warp on subspace-smoothed trajectories, and radial lens distortion (k1, k2) in the camera bundle adjustment with Undistort Footage landed in M13.3; Rolling Shutter Repair in M9.11; point tracker, mask tracking, Warp Stabilizer and the 3D Camera Tracker in M6.x / M12.5 / M12.6) |
|
||||
| Paint | 0% | 4.0 | Brush, Clone Stamp, Eraser |
|
||||
|
||||
|
|
@ -411,6 +411,44 @@ in Settings (`customRgb`) and simulated like the built-in profiles, with Preserv
|
|||
`form.field.<key>`, `form.ok` and `form.cancel` automation ids. Not yet: LUT-based (`A2B0`)
|
||||
ICC profiles.
|
||||
|
||||
## Update: M13.23 GPU effects, part B (EFF-5)
|
||||
|
||||
40 more effects run on the GPU (236 in all, with M13.22's 30), each checked against the CPU oracle (≤ 1/255 at
|
||||
8 bpc, ≤ 1e-3 at 32 bpc) directly on a buffer (full and half resolution, adjustment) and
|
||||
composited at 8 and 32 bpc:
|
||||
|
||||
- **3D Channel** (`gpu::fx_depth`): 3D Channel Extract (every channel, Anti-alias), Cryptomatte,
|
||||
Depth Matte, Depth of Field, EXtractoR, Fog 3D (with a Gradient Layer), ID Matte and
|
||||
IDentifier. The layer's auxiliary channels upload as an extra texture at the aux resolution and
|
||||
are resampled onto the buffer with the CPU's index arithmetic (tested at 1× and 2× aux
|
||||
resolution and on padded buffers). Cryptomatte's ranks are reduced per aux pixel on the CPU
|
||||
(selection coverage and ID colours), then looked up on the GPU.
|
||||
- **Immersive Video** (`gpu::fx_vr`): VR Blur, Chromatic Aberrations, Color Gradients, Converter
|
||||
(all nine projections and cube layouts), De-Noise (guided filter and median), Digital Glitch,
|
||||
Fractal Noise, Glow, Plane to Sphere, Rotate Sphere, Sharpen and Sphere to Plane, in mono and
|
||||
both stereo layouts: the equirectangular maths (seam wrap, pole continuation, latitude-widened
|
||||
box blurs) ported operation for operation. Exception: VR Converter re-projections that cross a
|
||||
cube-face edge or a fisheye rim may pick the neighbouring face for directions within f32
|
||||
rounding of the edge (the CPU decides in f64): up to 0.5 % of pixels allowed (measured 0).
|
||||
- **Colour management** (`gpu::fx_lut`): Apply Color LUT, OCIO CDL / Color Space / Display /
|
||||
File / Look Transform and Color Profile Converter compile to colour programs
|
||||
(`effects::color_program`, next to the CPU effects) interpreted per pixel; 1D / 3D LUTs and
|
||||
cineSpace shapers are read from a storage buffer with the CPU's nearest / trilinear /
|
||||
tetrahedral interpolation and inverses (not a hardware 3D texture: its filtering rounds the
|
||||
weights and cannot do tetrahedral). Lumetri's Input LUT and Look no longer force the CPU.
|
||||
Custom `.ocio` configurations render on the CPU. Exception: nearest-neighbour lattice lookups
|
||||
may pick the other lattice point for 8 bpc inputs exactly on a rounding boundary (up to 1 %
|
||||
of pixels allowed).
|
||||
- **Simulation render passes** (`gpu::fx_sim`): CC Rainfall, CC Snowfall, CC Star Burst, CC
|
||||
Bubbles, CC Drizzle, CC Hair, CC Mr. Mercury, Caustics, Wave World, Foam, Shatter, Card Dance
|
||||
and Card Wipe. The per-frame simulation / layout stays on the CPU and is shared with the CPU
|
||||
effect as a plan (sprites, textured pieces, blobs, drops, the wave grid, Caustics' prepared
|
||||
layers); a tiled rasteriser composites each pixel's items in the CPU's order and shading, with
|
||||
layer-dependent colours (refracting rain, star-burst and hair roots, bubble refraction)
|
||||
sampled per pixel. Piece plans read the frame back once (their gradient maps and textures
|
||||
default to the layer). Shatter's wireframe views and Foam's User Defined texture,
|
||||
Environment Map and flow-map preview render on the CPU.
|
||||
|
||||
## Highest-value gaps, in order
|
||||
|
||||
1. ~~On-canvas text editing and per-character styles~~ (landed: M9.9–M9.10).
|
||||
|
|
|
|||
Loading…
Reference in New Issue