fix(effects): implement XYZ axis rotation for CC Cylinder on CPU and GPU

This commit is contained in:
Nigel Ihza Firdausy committed 2026-10-11 11:06:51 +07:00
1 parent 9a7d3e7699
commit f29436580e
4 files changed
+139 -22

No files matched your search

+83 -12
View File
@@ -219,28 +219,71 @@ fn cylinder(ctx: &EffectCtx, mut b: Buf) -> Buf {
let (_, _, w, h) = rect;
let r = (ctx.params.f("radius") / 100.0 * w / std::f64::consts::TAU).max(0.5);
let c = b.to_px(ctx.params.v2("position"));
// Keep the pre-existing Rotation control as an additional texture spin.
let spin = ctx.params.f("rotation").to_radians();
let (rx, ry, rz) = (ctx.params.f("rotationX"), ctx.params.f("rotationY"), ctx.params.f("rotationZ"));
let tilted = rx != 0.0 || ry != 0.0 || rz != 0.0;
let m = rot_xyz(rx, ry, rz);
// The cylinder's local Y axis after rotation, in viewer coordinates.
let axis = [m[0][1], m[1][1], m[2][1]];
let a = 1.0 - axis[2] * axis[2];
let mode = ctx.params.e("render");
let sh = Shading::from(ctx);
let src = b.img.clone();
b.img.rows_mut().for_each(|(y, row)| {
let v = (y as f64 + 0.5 - c.1) / h + 0.5;
let dy = y as f64 + 0.5 - c.1;
for (x, px) in row.iter_mut().enumerate() {
let s = (x as f64 + 0.5 - c.0) / r;
if s.abs() >= 1.0 {
let dx = x as f64 + 0.5 - c.0;
if !tilted {
// Preserve exactly the mapping of projects saved before axis rotations.
let v = dy / h + 0.5;
let s = dx / r;
if s.abs() >= 1.0 {
*px = [0.0; 4];
continue;
}
let th = s.asin();
let cz = th.cos();
let front = || sh.shade(texel(&src, rect, (th + spin) / std::f64::consts::TAU + 0.5, v), [s, 0.0, cz]);
let back = || sh.shade(texel(&src, rect, (std::f64::consts::PI - th + spin) / std::f64::consts::TAU + 0.5, v), [-s, 0.0, cz]);
*px = match mode {
1 => front(),
2 => back(),
_ => over(front(), back()),
};
continue;
}
// Intersect the viewing ray (dx,dy,z) with the rotated cylindrical
// side wall: |q - dot(q,axis)*axis|^2 = radius^2.
let da = dx * axis[0] + dy * axis[1];
let bq = -da * axis[2];
let discriminant = bq * bq - a * (dx * dx + dy * dy - da * da - r * r);
if a <= 1e-10 || !discriminant.is_finite() || discriminant <= 0.0 {
*px = [0.0; 4];
continue;
}
let th = s.asin();
let cz = th.cos();
let front = || {
let u = (th + spin) / std::f64::consts::TAU + 0.5;
sh.shade(texel(&src, rect, u, v), [s, 0.0, cz])
};
let back = || {
let u = (std::f64::consts::PI - th + spin) / std::f64::consts::TAU + 0.5;
sh.shade(texel(&src, rect, u, v), [-s, 0.0, cz])
let root = discriminant.sqrt();
let face = |z: f64, back: bool| {
// Rᵀ turns the intersection into cylinder-local coordinates.
let local = [
m[0][0] * dx + m[1][0] * dy + m[2][0] * z,
m[0][1] * dx + m[1][1] * dy + m[2][1] * z,
m[0][2] * dx + m[1][2] * dy + m[2][2] * z,
];
let u = (local[0].atan2(local[2]) + spin) / std::f64::consts::TAU + 0.5;
let v = local[1] / h + 0.5;
let sign = if back { -1.0 } else { 1.0 };
let nx = local[0] / r * sign;
let nz = local[2] / r * sign;
let normal = [
m[0][0] * nx + m[0][2] * nz,
m[1][0] * nx + m[1][2] * nz,
m[2][0] * nx + m[2][2] * nz,
];
sh.shade(texel(&src, rect, u, v), normal)
};
let front = || face((-bq + root) / a, false);
let back = || face((-bq - root) / a, true);
*px = match mode {
1 => front(),
2 => back(),
@@ -362,6 +405,9 @@ pub fn specs() -> Vec<EffectSpec> {
p("radius", "Radius", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
p("position", "Position", pt(0.5, 0.5), ParamUi::Point),
p("rotation", "Rotation", num(0.0), ParamUi::Angle),
p("rotationX", "Rotation X", num(0.0), ParamUi::Angle),
p("rotationY", "Rotation Y", num(0.0), ParamUi::Angle),
p("rotationZ", "Rotation Z", num(0.0), ParamUi::Angle),
p("render", "Render", Value::Enum(0), render()),
];
cyl.extend(shading());
@@ -527,6 +573,31 @@ mod tests {
assert_eq!(out.get(50 + 18, 10)[3], 0.0);
}
#[test]
fn cylinder_axis_rotations_affect_geometry_and_texture() {
let img = Image::filled(100, 100, [0.5, 0.5, 0.5, 1.0]);
let default = run("ec.perspective.cccylinder", &img, &[]).img;
let x_tilt = run("ec.perspective.cccylinder", &img, &[("rotationX", num(70.0))]).img;
// Tilting the finite cylinder shortens its vertical projection.
assert!(default.get(50, 10)[3] > 0.99);
assert_eq!(x_tilt.get(50, 10)[3], 0.0);
assert!(x_tilt.get(50, 50)[3] > 0.99);
let z_roll = run("ec.perspective.cccylinder", &img, &[("rotationZ", num(90.0))]).img;
// A quarter turn moves the side wall from vertical to horizontal.
assert_eq!(default.get(5, 50)[3], 0.0);
assert!(z_roll.get(5, 50)[3] > 0.99);
let mut striped = Image::filled(100, 100, [0.0, 0.0, 0.0, 1.0]);
for y in 0..100 {
for x in 0..100 {
striped.set(x, y, [x as f32 / 100.0, 0.2, 0.3, 1.0]);
}
}
let baseline = run("ec.perspective.cccylinder", &striped, &[]).img;
let spun = run("ec.perspective.cccylinder", &striped, &[("rotationY", num(90.0))]).img;
assert!((baseline.get(50, 50)[0] - spun.get(50, 50)[0]).abs() > 0.05);
}
#[test]
fn spotlight_darkens_outside_cone() {
let img = Image::filled(80, 80, [1.0, 1.0, 1.0, 1.0]);
+7
View File
@@ -448,6 +448,13 @@ fn cylinder(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
p.u[0][0] = pr.e("render");
p.f[0] = f4([c.0, c.1, r, h]);
p.f[2][0] = pr.f("rotation").to_radians() as f32;
let (rx, ry, rz) = (pr.f("rotationX"), pr.f("rotationY"), pr.f("rotationZ"));
p.u[0][1] = (rx != 0.0 || ry != 0.0 || rz != 0.0) as u32;
let m = rot_xyz(rx, ry, rz);
// Matrix rows, as for CC Sphere; the kernel uses Rᵀ for mapping.
for (i, row) in m.iter().enumerate() {
p.f[7 + i] = f4([row[0], row[1], row[2], 0.0]);
}
run(e, "ftr_cylinder", &p, b, None, None)
}
+44 -10
View File
@@ -666,7 +666,24 @@ fn ftr_faces(mode: u32, front: vec4<f32>, back: vec4<f32>) -> vec4<f32> {
return ftr_over(front, back);
}
// Cylinder: u[0].x = render; f[0] = (cx, cy, radius, layer height); f[2].x = rotation.
// Cylinder: u[0].x = render, u[0].y = axis rotation enabled;
// f[0] = (cx, cy, radius, layer height); f[2].x = legacy texture spin;
// f[7..10] = rows of the local-to-view rotation matrix.
fn ftr_cylinder_face(d: vec2<f32>, z: f32, back: bool) -> vec4<f32> {
let m0 = P.f[7].xyz;
let m1 = P.f[8].xyz;
let m2 = P.f[9].xyz;
let q = vec3<f32>(d, z);
let local = vec3<f32>(m0.x * q.x + m1.x * q.y + m2.x * q.z,
m0.y * q.x + m1.y * q.y + m2.y * q.z,
m0.z * q.x + m1.z * q.y + m2.z * q.z);
let u = (atan2(local.x, local.z) + P.f[2].x) / 6.283185307179586 + 0.5;
let v = local.y / P.f[0].w + 0.5;
let flip = select(1.0, -1.0, back);
let n = vec3<f32>(local.x / P.f[0].z * flip, 0.0, local.z / P.f[0].z * flip);
return ftr_shade(ftr_texel(u, v), vec3<f32>(dot(m0, n), dot(m1, n), dot(m2, n)));
}
@compute @workgroup_size(16, 16)
fn ftr_cylinder(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
@@ -674,18 +691,35 @@ fn ftr_cylinder(@builtin(global_invocation_id) gid: vec3<u32>) {
return;
}
let d = ftr_centre(p) - P.f[0].xy;
let v = d.y / P.f[0].w + 0.5;
let s = d.x / P.f[0].z;
if (abs(s) >= 1.0) {
if (P.u[0].y == 0u) {
// Original path is unchanged for old projects and default parameters.
let v = d.y / P.f[0].w + 0.5;
let s = d.x / P.f[0].z;
if (abs(s) >= 1.0) {
textureStore(out, p, vec4<f32>(0.0));
return;
}
let tau = 6.283185307179586;
let th = asin_p(s);
let cz = cos(th);
let spin = P.f[2].x;
let front = ftr_shade(ftr_texel((th + spin) / tau + 0.5, v), vec3<f32>(s, 0.0, cz));
let back = ftr_shade(ftr_texel((3.141592653589793 - th + spin) / tau + 0.5, v), vec3<f32>(-s, 0.0, cz));
textureStore(out, p, ftr_faces(P.u[0].x, front, back));
return;
}
let axis = vec3<f32>(P.f[7].y, P.f[8].y, P.f[9].y);
let a = 1.0 - axis.z * axis.z;
let da = dot(d, axis.xy);
let bq = -da * axis.z;
let disc = bq * bq - a * (dot(d, d) - da * da - P.f[0].z * P.f[0].z);
if (a <= 1e-10 || !(disc > 0.0) || !isFinite(disc)) {
textureStore(out, p, vec4<f32>(0.0));
return;
}
let tau = 6.283185307179586;
let th = asin_p(s);
let cz = cos(th);
let spin = P.f[2].x;
let front = ftr_shade(ftr_texel((th + spin) / tau + 0.5, v), vec3<f32>(s, 0.0, cz));
let back = ftr_shade(ftr_texel((3.141592653589793 - th + spin) / tau + 0.5, v), vec3<f32>(-s, 0.0, cz));
let root = sqrt(disc);
let front = ftr_cylinder_face(d, (-bq + root) / a, false);
let back = ftr_cylinder_face(d, (-bq - root) / a, true);
textureStore(out, p, ftr_faces(P.u[0].x, front, back));
}
+5
View File
@@ -89,6 +89,11 @@ fn perspective() {
effect_case("ec.perspective.cccylinder", &[("position", pt(35.0, 22.0)), ("rotation", n(30.0))]);
effect_case("ec.perspective.cccylinder", &[("radius", n(60.0)), ("render", e(1)), ("lightDirection", n(70.0)), ("roughness", n(0.2))]);
effect_case("ec.perspective.cccylinder", &[("render", e(2)), ("lightHeight", n(20.0)), ("specular", n(60.0))]);
// All three axes: independently and combined, against the CPU reference.
effect_case("ec.perspective.cccylinder", &[("rotationX", n(35.0)), ("render", e(1))]);
effect_case("ec.perspective.cccylinder", &[("rotationY", n(65.0)), ("rotation", n(15.0))]);
effect_case("ec.perspective.cccylinder", &[("rotationZ", n(-25.0)), ("render", e(2))]);
effect_case("ec.perspective.cccylinder", &[("rotationX", n(30.0)), ("rotationY", n(20.0)), ("rotationZ", n(15.0))]);
effect_case("ec.perspective.ccsphere", &[("offset", pt(35.0, 22.0)), ("radius", n(18.0)), ("rotationX", n(20.0)), ("rotationY", n(35.0))]);
effect_case("ec.perspective.ccsphere", &[("radius", n(20.0)), ("rotationZ", n(50.0)), ("render", e(2))]);
effect_case("ec.perspective.ccspotlight", &[("from", pt(15.0, 8.0)), ("to", pt(35.0, 22.0))]);