Merge pull request #2620 from becauseno7/cc-bubbles-layer-bounds

CC Bubbles: bubbles refract the layer's edge pixels on padded buffers too (#2024)
This commit is contained in:
BFlat
2026-10-30 09:00:31 +07:00
committed by GitHub
8 changed files with 92 additions and 9 deletions
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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;
+39
View File
@@ -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: &params, 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);
+13 -1
View File
@@ -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;
+4 -4
View File
@@ -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),
+1
View File
@@ -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 })
}
+5 -2
View File
@@ -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);
+28
View File
@@ -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: &params, 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);