mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
CC Bubbles: bubbles refract the layer's edge pixels on padded buffers too (#2024)
Refraction samples are clamped to the layer rectangle instead of the buffer, so a sample past the layer's edge in a buffer with transparent padding (a half-resolution preview's, a text or shape layer's) repeats the layer's edge pixels rather than reaching the pale colour that stands in for transparent ones. Transparent pixels inside the layer still show it. Same bounds on the GPU path. Re-pins the four half-resolution CC Bubbles golden hashes (aarch64 macOS); the full-resolution ones are unchanged. Adds a CPU test and a GPU-vs-CPU parity test on padded buffers.
This commit is contained in:
@@ -117,7 +117,7 @@ pub use ocio::color_stabilizer_maps;
|
||||
// GPU effects, part B (3D Channel, Immersive Video, colour management, simulation render passes).
|
||||
pub use channel3d::{CRYPTO_LAYERS, CryptoAux, aux_point, crypto_aux_image, crypto_aux_image_with, 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 sim::{Acc, Blob, Bubble, Post, Shape, Sprite, SpritePlan, Tint, bubble_list, bubble_sample_bounds, drizzle_drops, mercury_blobs, sprite_plan};
|
||||
pub use sim2::{
|
||||
CausticsSetup, FoamDisc, FoamEnv, FoamPlan, Piece, PiecePlan, PieceTex, PixelPlan, WavePlan, Waves, caustics_setup, foam_full_plan, piece_plan, pixel_plan,
|
||||
wave_world_plan,
|
||||
|
||||
@@ -38,7 +38,7 @@ use particle_fx::{PW_RATE, mercury_phys, ps2_phys, pw_phys};
|
||||
pub use registry::{specs, sprite_plan};
|
||||
pub use sprites::{Acc, Post, Shape, Sprite, SpritePlan, Tint};
|
||||
pub(crate) use sprites::{combine, raster, splat};
|
||||
pub use weather::{Bubble, bubble_list, drizzle_drops};
|
||||
pub use weather::{Bubble, bubble_list, bubble_sample_bounds, drizzle_drops};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -112,6 +112,45 @@ fn bubbles_near_edges_show_the_layer() {
|
||||
assert_eq!(pale, [0, 0], "pale bubble pixels (liquid, metal)");
|
||||
}
|
||||
|
||||
/// CC Bubbles on a 40 × 40 layer `img` at `scale`, in a buffer padded by 6 transparent pixels
|
||||
/// (a half-resolution preview's layout).
|
||||
fn padded_bubbles(img: Image, scale: f64, reflection: u32) -> Image {
|
||||
let size = 40.0;
|
||||
let s = crate::find("ec.sim.ccbubbles").unwrap();
|
||||
let mut params = crate::Params { values: s.params.iter().map(|p| (p.id.to_string(), crate::default_value(p, [size; 2]))).collect() };
|
||||
for (k, v) in [("bubbleAmount", num(200.0)), ("bubbleSize", num(3.0)), ("reflectionType", Value::Enum(reflection))] {
|
||||
params.values.insert(k.into(), v);
|
||||
}
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.3, layer_size: [size; 2], seed: 1, adjustment: false, env: EffectEnv::default() };
|
||||
let mut b = Buf { img, offset: [0.0; 2], scale };
|
||||
b.pad(6);
|
||||
crate::apply(s, &ctx, b).img
|
||||
}
|
||||
|
||||
/// #2024: the same when the buffer has transparent padding around the layer: samples past the
|
||||
/// layer's edges repeat its edge pixels, not the padding, and a clear hole inside the layer is
|
||||
/// still refracted as the pale colour.
|
||||
#[test]
|
||||
fn bubbles_near_padded_edges_show_the_layer() {
|
||||
let blue = |side| Image::filled(side, side, [0.0, 0.0, 1.0, 1.0]);
|
||||
let pale = |img: &Image| img.data.iter().filter(|p| (p[2] - p[0] - p[3]).abs() > 1e-4).count();
|
||||
for (scale, side) in [(1.0, 40), (0.5, 20)] {
|
||||
let counts = [0, 1].map(|reflection| {
|
||||
let a = padded_bubbles(blue(side), scale, reflection);
|
||||
assert!(a.data.iter().any(|p| p[3] > 0.5), "bubbles drawn");
|
||||
pale(&a)
|
||||
});
|
||||
assert_eq!(counts, [0, 0], "pale bubble pixels at scale {scale} (liquid, metal)");
|
||||
}
|
||||
let mut holed = blue(40);
|
||||
for y in 12..28 {
|
||||
for x in 12..28 {
|
||||
holed.set(x, y, [0.0; 4]);
|
||||
}
|
||||
}
|
||||
assert!(pale(&padded_bubbles(holed, 1.0, 0)) > 0, "the hole refracts as the pale colour");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drizzle_ripples() {
|
||||
let a = run("ec.sim.ccdrizzle", &[], ramp(48, 48), 0.6);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::common::{h, hs};
|
||||
use super::sprites::{Acc, Post, Shape, Sprite, SpritePlan, Tint, aa, raster};
|
||||
use super::{Buf, EffectCtx, Image, IntoParallelIterator, ParallelIterator, unpremul};
|
||||
use crate::util::layer_rect_at;
|
||||
|
||||
// ------------------------------------------------------------------ CC Rainfall / CC Snowfall
|
||||
|
||||
@@ -160,11 +161,21 @@ pub fn bubble_list(ctx: &EffectCtx, b: &Buf) -> Vec<Bubble> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// CC Bubbles' refraction sample bounds in the pixels of a buffer with `offset` and `scale`: the
|
||||
/// layer rectangle's first and last pixel centres (x0, y0, x1, y1). Samples clamped to them repeat
|
||||
/// the layer's edge pixels instead of reaching transparent padding around it (#2024).
|
||||
pub fn bubble_sample_bounds(ctx: &EffectCtx, offset: [f64; 2], scale: f64) -> [f32; 4] {
|
||||
let (lx, ly, lw, lh) = layer_rect_at(ctx, offset, scale);
|
||||
let (x0, y0) = (lx + 0.5, ly + 0.5);
|
||||
[x0 as f32, y0 as f32, (lx + lw - 0.5).max(x0) as f32, (ly + lh - 0.5).max(y0) as f32]
|
||||
}
|
||||
|
||||
pub(super) 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 [x0, y0, x1, y1] = bubble_sample_bounds(ctx, b.offset, b.scale);
|
||||
let src = b.img.clone();
|
||||
let mut out = Image::new(src.width, src.height);
|
||||
raster(
|
||||
@@ -182,7 +193,8 @@ pub(super) fn bubbles(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
// Liquid refracts (inverted, magnified), metal reflects (wide-angle mirror).
|
||||
let k = if metal { 1.6 } else { -0.7 };
|
||||
let nz = (1.0 - d2).max(0.0).sqrt();
|
||||
let p = src.sample_bilinear_clamped((q.sx + dx * q.r * k) as f64, (q.sy + dy * q.r * k) as f64);
|
||||
let (sx, sy) = ((q.sx + dx * q.r * k).max(x0).min(x1), (q.sy + dy * q.r * k).max(y0).min(y1));
|
||||
let p = src.sample_bilinear_clamped(sx as f64, sy as f64);
|
||||
let (c, sa) = unpremul(p);
|
||||
let mut c = if sa <= 0.0 { [0.6, 0.7, 0.8] } else { c };
|
||||
let mut a = cov;
|
||||
|
||||
@@ -441,10 +441,10 @@ const GOLDEN: &[(&str, u64)] = &[
|
||||
("ec.sim.ccbubbles/0/32bpc/t1.5", 0xd09565e124d8f5db),
|
||||
("ec.sim.ccbubbles/0/32bpc/t0.8", 0x91242f8e1fd0ad84),
|
||||
("ec.sim.ccbubbles/0/32bpc/t2.2", 0x1f9782131e62149d),
|
||||
("ec.sim.ccbubbles/0/8bpc/half/t0.5", 0x931b4ca707604220),
|
||||
("ec.sim.ccbubbles/0/8bpc/half/t1.5", 0x21c25d4555abebe9),
|
||||
("ec.sim.ccbubbles/0/8bpc/half/t0.8", 0x7e05972683359632),
|
||||
("ec.sim.ccbubbles/0/8bpc/half/t2.2", 0xadc4f7219dab4921),
|
||||
("ec.sim.ccbubbles/0/8bpc/half/t0.5", 0x020755e48bdf8f43),
|
||||
("ec.sim.ccbubbles/0/8bpc/half/t1.5", 0x4aac0f1332e6001e),
|
||||
("ec.sim.ccbubbles/0/8bpc/half/t0.8", 0xdc2ab5e04c4a95a2),
|
||||
("ec.sim.ccbubbles/0/8bpc/half/t2.2", 0x30f78adc5d1dd715),
|
||||
("ec.sim.ccbubbles/1/8bpc/t0.5", 0x2f5415c679ff0980),
|
||||
("ec.sim.ccbubbles/1/8bpc/t1.5", 0xfcfc354dcd22e6d0),
|
||||
("ec.sim.ccbubbles/1/8bpc/t0.8", 0xdf90fd57c217019c),
|
||||
|
||||
@@ -253,6 +253,7 @@ fn bubbles(e: &mut Enc, ctx: &EffectCtx, b: GBuf, geo: &effectcraft_effects::Buf
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[1] = [0, (ctx.params.e("reflectionType") == 1) as u32, ctx.params.e("shadingType"), 0];
|
||||
p.f[0] = effectcraft_effects::bubble_sample_bounds(ctx, b.offset, b.scale);
|
||||
let img = raster(e, items, 1, false, &b.img, None, p, w, h)?;
|
||||
Some(GBuf { img, ..b })
|
||||
}
|
||||
|
||||
@@ -165,7 +165,8 @@ fn fxm_sprite(at: u32, x: f32, y: f32) -> FxmShade {
|
||||
return FxmShade(true, vec4<f32>(rgb * a, a));
|
||||
}
|
||||
|
||||
// CC Bubbles' bubble (sim::bubbles). u[1].y = metal, u[1].z = shading type.
|
||||
// CC Bubbles' bubble (sim::bubbles). u[1].y = metal, u[1].z = shading type, f[0] = the layer
|
||||
// rectangle's first and last pixel centres (x0, y0, x1, y1), which refraction samples stay within.
|
||||
fn fxm_bubble(at: u32, x: f32, y: f32) -> FxmShade {
|
||||
let qx = data[at + 1u];
|
||||
let qy = data[at + 2u];
|
||||
@@ -180,7 +181,9 @@ fn fxm_bubble(at: u32, x: f32, y: f32) -> FxmShade {
|
||||
}
|
||||
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_clamped(src, data[at + 4u] + dx * qr * k, data[at + 5u] + dy * qr * k));
|
||||
let sx = min(max(data[at + 4u] + dx * qr * k, P.f[0].x), P.f[0].z);
|
||||
let sy = min(max(data[at + 5u] + dy * qr * k, P.f[0].y), P.f[0].w);
|
||||
let s = fxm_unpremul(sample_bilinear_clamped(src, sx, sy));
|
||||
var c = s.xyz;
|
||||
if (s.w <= 0.0) {
|
||||
c = vec3<f32>(0.6, 0.7, 0.8);
|
||||
|
||||
@@ -105,6 +105,34 @@ fn bubbles_drizzle_mercury() {
|
||||
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);
|
||||
}
|
||||
|
||||
/// CC Bubbles on a buffer with transparent padding around the layer: both sides clamp the
|
||||
/// refraction samples to the layer rectangle, not to the buffer (#2024).
|
||||
#[test]
|
||||
fn bubbles_on_padded_buffers() {
|
||||
let Some(g) = gpu() else { return };
|
||||
let spec = effectcraft_effects::find("ec.sim.ccbubbles").unwrap();
|
||||
let size = [70.0, 44.0];
|
||||
for reflection in [0, 1] {
|
||||
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 [("bubbleAmount", n(80.0)), ("bubbleSize", n(4.0)), ("reflectionType", e(reflection))] {
|
||||
params.values.insert(k.to_string(), v);
|
||||
}
|
||||
for scale in [1.0, 0.5] {
|
||||
let img = pattern(7, (size[0] * scale) as u32, (size[1] * scale) as u32);
|
||||
let mut buf = effectcraft_effects::Buf { img, offset: [0.0, 0.0], scale };
|
||||
buf.pad(6);
|
||||
let ctx = || effectcraft_effects::EffectCtx { params: ¶ms, time: 0.5, 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), "geometry");
|
||||
let d = diff(&cpu.img, &out.img, 1e-3);
|
||||
assert!(d.over == 0, "reflection {reflection} scale {scale}: {} of {} pixels over 1e-3 (max {}); worst {:?}", d.over, d.total, d.max, d.worst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hair_wave_world_foam() {
|
||||
case("ec.sim.cchair", &[("density", n(300.0)), ("length", n(10.0))], 0.0, 0.0);
|
||||
|
||||
Reference in New Issue
Block a user