mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
fix(effects): implement XYZ axis rotation for CC Cylinder on CPU and GPU
This commit is contained in:
1 parent
9a7d3e7699
commit
f29436580e
4 files changed
+139
-22
No files matched your search
@@ -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]);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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))]);
|
||||
|
||||
Reference in new issue
Block a user