mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-26 19:18:14 -06:00
- card3d: shared Camera System (Camera Position with rotations, X/Y position and Transform Order; Corner Pins with focal length; Comp Camera via EffectHost::comp_scene), Lighting (Distant / Point / First Comp Light, colour, position, depth, ambient) and Material (diffuse, specular, highlight sharpness). - Shatter: Custom Shatter Map (+ White Tiles Fixed), Gradient group, Textures group (front/side/back modes and layers, colour, opacity) and extrusion side rendering. - Card Wipe: true 3D cards, Back Layer as a layer parameter (old None/Self popup migrated), Flip Direction, Position / Rotation Jitter. - Renderer supplies the comp camera and first light to effects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
414 lines
18 KiB
Rust
414 lines
18 KiB
Rust
//! The 3D system shared by Card Dance, Shatter and Card Wipe: Camera System (Camera Position,
|
||
//! Corner Pins, Comp Camera), Lighting (Distant Source, Point Source, First Comp Light) and
|
||
//! Material (diffuse, specular, highlight sharpness).
|
||
//!
|
||
//! Pieces live in "buffer world" space: x, y in buffer pixels, z in buffer pixels away from the
|
||
//! viewer (the layer plane is z = 0). A [`Proj`] maps buffer-world points to buffer pixels
|
||
//! (homogeneous), so every camera system reduces to one 3×4 matrix.
|
||
|
||
use effectcraft_keyframe::Value;
|
||
use effectcraft_project::ParamUi;
|
||
|
||
use crate::sim::pt;
|
||
use crate::{Buf, EffectCtx, ParamSpec, col, num, p, popup, slider};
|
||
|
||
pub(crate) type M3 = [[f64; 3]; 3];
|
||
|
||
/// Camera System options.
|
||
pub const CAMERA_SYSTEMS: [&str; 3] = ["Camera Position", "Corner Pins", "Comp Camera"];
|
||
/// Transform Order options (rotation order, and whether the camera position applies before or
|
||
/// after the rotation).
|
||
pub const TRANSFORM_ORDERS: [&str; 12] = [
|
||
"Rotate XYZ, Position",
|
||
"Rotate XZY, Position",
|
||
"Rotate YXZ, Position",
|
||
"Rotate YZX, Position",
|
||
"Rotate ZXY, Position",
|
||
"Rotate ZYX, Position",
|
||
"Position, Rotate XYZ",
|
||
"Position, Rotate XZY",
|
||
"Position, Rotate YXZ",
|
||
"Position, Rotate YZX",
|
||
"Position, Rotate ZXY",
|
||
"Position, Rotate ZYX",
|
||
];
|
||
/// Light Type options.
|
||
pub const LIGHT_TYPES: [&str; 3] = ["Distant Source", "Point Source", "First Comp Light"];
|
||
|
||
/// Camera System, Camera Position and Corner Pins parameters. `z` is the default camera
|
||
/// distance (Z Position, in layer sizes at the 70 mm-equivalent Focal Length).
|
||
pub(crate) fn camera_params(z: f64) -> Vec<ParamSpec> {
|
||
vec![
|
||
p("cameraSystem", "Camera System", Value::Enum(0), popup(&CAMERA_SYSTEMS)),
|
||
p("cameraPosition/xRotation", "X Rotation", num(0.0), ParamUi::Angle),
|
||
p("cameraPosition/yRotation", "Y Rotation", num(0.0), ParamUi::Angle),
|
||
p("cameraPosition/zRotation", "Z Rotation", num(0.0), ParamUi::Angle),
|
||
p("cameraPosition/xyPosition", "X, Y Position", pt(0.5, 0.5), ParamUi::Point),
|
||
p("cameraPosition/cameraZ", "Z Position", num(z), slider(0.05, 10.0, 0.5, 5.0, 2)),
|
||
p("cameraPosition/focalLength", "Focal Length", num(70.0), slider(1.0, 500.0, 10.0, 200.0, 1)),
|
||
p("cameraPosition/transformOrder", "Transform Order", Value::Enum(0), popup(&TRANSFORM_ORDERS)),
|
||
p("cornerPins/upperLeftCorner", "Upper Left Corner", pt(0.0, 0.0), ParamUi::Point),
|
||
p("cornerPins/upperRightCorner", "Upper Right Corner", pt(1.0, 0.0), ParamUi::Point),
|
||
p("cornerPins/lowerLeftCorner", "Lower Left Corner", pt(0.0, 1.0), ParamUi::Point),
|
||
p("cornerPins/lowerRightCorner", "Lower Right Corner", pt(1.0, 1.0), ParamUi::Point),
|
||
p("cornerPins/autoFocalLength", "Auto Focal Length", Value::Bool(true), ParamUi::Checkbox),
|
||
p("cornerPins/focalLength", "Focal Length", num(70.0), slider(1.0, 500.0, 10.0, 200.0, 1)),
|
||
]
|
||
}
|
||
|
||
/// Lighting parameters.
|
||
pub(crate) fn lighting_params(intensity: f64, ambient: f64) -> Vec<ParamSpec> {
|
||
vec![
|
||
p("lighting/lightType", "Light Type", Value::Enum(0), popup(&LIGHT_TYPES)),
|
||
p("lighting/lightIntensity", "Light Intensity", num(intensity), slider(0.0, 4.0, 0.0, 2.0, 2)),
|
||
p("lighting/lightColor", "Light Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||
p("lighting/lightPosition", "Light Position", pt(0.5, 0.5), ParamUi::Point),
|
||
p("lighting/lightDepth", "Light Depth", num(1.0), slider(-10.0, 10.0, 0.0, 5.0, 3)),
|
||
p("lighting/ambientLight", "Ambient Light", num(ambient), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||
]
|
||
}
|
||
|
||
/// Material parameters. Specular defaults to 0 so pieces at rest reproduce the layer.
|
||
pub(crate) fn material_params(diffuse: f64) -> Vec<ParamSpec> {
|
||
vec![
|
||
p("material/diffuse", "Diffuse Reflection", num(diffuse), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||
p("material/specular", "Specular Reflection", num(0.0), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||
p("material/highlightSharpness", "Highlight Sharpness", num(15.0), slider(1.0, 100.0, 1.0, 100.0, 1)),
|
||
]
|
||
}
|
||
|
||
/// Whether the camera / lighting parameter `param` is shown for the current choices.
|
||
pub(crate) fn shown(param: &str, e: &dyn Fn(&str) -> u32) -> Option<bool> {
|
||
if param == "cameraPosition" || param.starts_with("cameraPosition/") {
|
||
return Some(e("cameraSystem") == 0);
|
||
}
|
||
if param == "cornerPins" || param.starts_with("cornerPins/") {
|
||
return Some(e("cameraSystem") == 1);
|
||
}
|
||
if param == "lighting/lightPosition" || param == "lighting/lightDepth" {
|
||
return Some(e("lighting/lightType") != 2);
|
||
}
|
||
None
|
||
}
|
||
|
||
pub(crate) fn mat_mul(a: &M3, b: &M3) -> M3 {
|
||
std::array::from_fn(|i| std::array::from_fn(|j| (0..3).map(|k| a[i][k] * b[k][j]).sum()))
|
||
}
|
||
|
||
fn rx(a: f64) -> M3 {
|
||
let (s, c) = a.sin_cos();
|
||
[[1.0, 0.0, 0.0], [0.0, c, -s], [0.0, s, c]]
|
||
}
|
||
fn ry(a: f64) -> M3 {
|
||
let (s, c) = a.sin_cos();
|
||
[[c, 0.0, s], [0.0, 1.0, 0.0], [-s, 0.0, c]]
|
||
}
|
||
fn rz(a: f64) -> M3 {
|
||
let (s, c) = a.sin_cos();
|
||
[[c, -s, 0.0], [s, c, 0.0], [0.0, 0.0, 1.0]]
|
||
}
|
||
|
||
/// Rotation applying the axes in `order` (0 XYZ, 1 XZY, 2 YXZ, 3 YZX, 4 ZXY, 5 ZYX; the first
|
||
/// letter is applied first). Angles in radians.
|
||
pub(crate) fn rot_ordered(x: f64, y: f64, z: f64, order: u32) -> M3 {
|
||
let (mx, my, mz) = (rx(x), ry(y), rz(z));
|
||
let seq: [&M3; 3] = match order % 6 {
|
||
1 => [&mx, &mz, &my],
|
||
2 => [&my, &mx, &mz],
|
||
3 => [&my, &mz, &mx],
|
||
4 => [&mz, &mx, &my],
|
||
5 => [&mz, &my, &mx],
|
||
_ => [&mx, &my, &mz],
|
||
};
|
||
mat_mul(seq[2], &mat_mul(seq[1], seq[0]))
|
||
}
|
||
|
||
/// A camera: buffer-world → buffer pixels (homogeneous rows X, Y, W; W is the depth in front
|
||
/// of the camera, in the units `near` uses).
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub(crate) struct Proj {
|
||
pub m: [[f64; 4]; 3],
|
||
/// Points with W below this are behind / too close to the camera.
|
||
pub near: f64,
|
||
}
|
||
|
||
impl Proj {
|
||
/// Looking down +z at the layer plane from `d` pixels in front of buffer point `c`.
|
||
pub fn simple(c: [f64; 2], d: f64) -> Proj {
|
||
Proj { m: [[d, 0.0, c[0], 0.0], [0.0, d, c[1], 0.0], [0.0, 0.0, 1.0, d]], near: 0.05 * d }
|
||
}
|
||
|
||
/// `self` after the affine scene transform `p ↦ a·p + t`.
|
||
fn after(&self, a: &M3, t: [f64; 3]) -> Proj {
|
||
let mut m = [[0.0; 4]; 3];
|
||
for i in 0..3 {
|
||
for j in 0..3 {
|
||
m[i][j] = (0..3).map(|k| self.m[i][k] * a[k][j]).sum();
|
||
}
|
||
m[i][3] = (0..3).map(|k| self.m[i][k] * t[k]).sum::<f64>() + self.m[i][3];
|
||
}
|
||
Proj { m, near: self.near }
|
||
}
|
||
|
||
/// A screen-space homography applied after `self`.
|
||
fn then(&self, h: &M3) -> Proj {
|
||
let mut m = [[0.0; 4]; 3];
|
||
for i in 0..3 {
|
||
for j in 0..4 {
|
||
m[i][j] = (0..3).map(|k| h[i][k] * self.m[k][j]).sum();
|
||
}
|
||
}
|
||
// Keep W's scale (and so `near`) comparable when H rescales it.
|
||
Proj { m, near: self.near * h[2][2].abs().max(1e-9) }
|
||
}
|
||
|
||
/// Project a buffer-world point (None behind the camera).
|
||
#[cfg(test)]
|
||
pub fn project(&self, p: [f64; 3]) -> Option<[f64; 2]> {
|
||
let r = |i: usize| self.m[i][0] * p[0] + self.m[i][1] * p[1] + self.m[i][2] * p[2] + self.m[i][3];
|
||
let w = r(2);
|
||
(w > self.near).then(|| [r(0) / w, r(1) / w])
|
||
}
|
||
|
||
/// The camera's centre in buffer-world space (where the projection collapses), or a point
|
||
/// far in front of the layer when it has none (orthographic).
|
||
pub fn eye(&self) -> [f64; 3] {
|
||
let a: M3 = std::array::from_fn(|i| [self.m[i][0], self.m[i][1], self.m[i][2]]);
|
||
match inv3(&a) {
|
||
Some(ai) => {
|
||
let t = [-self.m[0][3], -self.m[1][3], -self.m[2][3]];
|
||
std::array::from_fn(|i| (0..3).map(|k| ai[i][k] * t[k]).sum())
|
||
}
|
||
None => [0.0, 0.0, -1e9],
|
||
}
|
||
}
|
||
}
|
||
|
||
pub(crate) fn inv3(m: &M3) -> Option<M3> {
|
||
let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
|
||
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
|
||
if det.abs() < 1e-12 {
|
||
return None;
|
||
}
|
||
let d = 1.0 / det;
|
||
Some([
|
||
[(m[1][1] * m[2][2] - m[1][2] * m[2][1]) * d, (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * d, (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * d],
|
||
[(m[1][2] * m[2][0] - m[1][0] * m[2][2]) * d, (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * d, (m[0][2] * m[1][0] - m[0][0] * m[1][2]) * d],
|
||
[(m[1][0] * m[2][1] - m[1][1] * m[2][0]) * d, (m[0][1] * m[2][0] - m[0][0] * m[2][1]) * d, (m[0][0] * m[1][1] - m[0][1] * m[1][0]) * d],
|
||
])
|
||
}
|
||
|
||
/// Homography taking the unit square's corners (0,0) (1,0) (1,1) (0,1) to `q`.
|
||
fn square_to_quad(q: [[f64; 2]; 4]) -> M3 {
|
||
let v = |p: [f64; 2]| effectcraft_geom::Vec2 { x: p[0], y: p[1] };
|
||
effectcraft_geom::Mat3::square_to_quad([v(q[0]), v(q[1]), v(q[2]), v(q[3])]).0
|
||
}
|
||
|
||
/// The camera an effect renders through, from its Camera System parameters.
|
||
pub(crate) fn projection(ctx: &EffectCtx, b: &Buf) -> Proj {
|
||
let pr = ctx.params;
|
||
let [lw, lh] = ctx.layer_size;
|
||
let s = b.scale;
|
||
let c = [b.offset[0] + lw * 0.5 * s, b.offset[1] + lh * 0.5 * s];
|
||
let size = lw.max(lh) * s;
|
||
let f = |id: &str, d: f64| pr.get(id).map(Value::as_f64).unwrap_or(d);
|
||
match pr.e("cameraSystem") {
|
||
1 => {
|
||
// Corner Pins: the layer's corners land on the four pins; the focal length sets how
|
||
// strongly depth (pieces leaving the plane) is foreshortened.
|
||
let focal = if pr.get("cornerPins/autoFocalLength").is_none_or(|v| v.as_bool()) { 70.0 } else { f("cornerPins/focalLength", 70.0).max(1.0) };
|
||
let base = Proj::simple(c, size * 2.0 * focal / 70.0);
|
||
let pin = |id: &str| {
|
||
let (x, y) = b.to_px(pr.v2(id));
|
||
[x, y]
|
||
};
|
||
let quad =
|
||
[pin("cornerPins/upperLeftCorner"), pin("cornerPins/upperRightCorner"), pin("cornerPins/lowerRightCorner"), pin("cornerPins/lowerLeftCorner")];
|
||
let rect = [
|
||
[b.offset[0], b.offset[1]],
|
||
[b.offset[0] + lw * s, b.offset[1]],
|
||
[b.offset[0] + lw * s, b.offset[1] + lh * s],
|
||
[b.offset[0], b.offset[1] + lh * s],
|
||
];
|
||
match inv3(&square_to_quad(rect)) {
|
||
Some(ri) => base.then(&mat_mul(&square_to_quad(quad), &ri)),
|
||
None => base,
|
||
}
|
||
}
|
||
2 => {
|
||
// Comp Camera: the comp's camera looking at this layer (layer px → layer px).
|
||
let base = Proj::simple(c, size * 2.0);
|
||
let Some(cam) = ctx.env.host.and_then(|h| h.comp_scene()).and_then(|sc| sc.camera) else { return base };
|
||
// buffer world → layer: (p − offset) / s; layer → buffer: · s + offset.
|
||
let mut m = [[0.0; 4]; 3];
|
||
for i in 0..3 {
|
||
for j in 0..3 {
|
||
m[i][j] = cam[i][j] / s;
|
||
}
|
||
m[i][3] = cam[i][3] - (cam[i][0] * b.offset[0] + cam[i][1] * b.offset[1]) / s;
|
||
}
|
||
let back: M3 = [[s, 0.0, b.offset[0]], [0.0, s, b.offset[1]], [0.0, 0.0, 1.0]];
|
||
let p = Proj { m, near: 1e-6 }.then(&back);
|
||
// W of the layer centre sets the scale of `near`.
|
||
let wc = p.m[2][0] * c[0] + p.m[2][1] * c[1] + p.m[2][3];
|
||
Proj { near: (wc.abs() * 0.02).max(1e-9), ..p }
|
||
}
|
||
_ => {
|
||
let zpos = f("cameraPosition/cameraZ", 2.0).max(0.05);
|
||
let focal = f("cameraPosition/focalLength", 70.0).max(1.0);
|
||
let base = Proj::simple(c, size * zpos * focal / 140.0);
|
||
let order = pr.e("cameraPosition/transformOrder");
|
||
let r = rot_ordered(
|
||
f("cameraPosition/xRotation", 0.0).to_radians(),
|
||
f("cameraPosition/yRotation", 0.0).to_radians(),
|
||
f("cameraPosition/zRotation", 0.0).to_radians(),
|
||
order,
|
||
);
|
||
let xy = pr.get("cameraPosition/xyPosition").map(|v| v.as_vec2()).unwrap_or([lw * 0.5, lh * 0.5]);
|
||
let (px, py) = b.to_px(xy);
|
||
let shift = [px - c[0], py - c[1], 0.0];
|
||
// Rotate about the layer centre, then move by the X, Y Position offset (or the
|
||
// other way round).
|
||
let c3 = [c[0], c[1], 0.0];
|
||
let rc: [f64; 3] = std::array::from_fn(|i| c3[i] - (0..3).map(|k| r[i][k] * c3[k]).sum::<f64>());
|
||
let t = if order >= 6 {
|
||
// p ↦ R(p + shift − c) + c
|
||
std::array::from_fn(|i| rc[i] + (0..3).map(|k| r[i][k] * shift[k]).sum::<f64>())
|
||
} else {
|
||
std::array::from_fn(|i| rc[i] + shift[i])
|
||
};
|
||
base.after(&r, t)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The comp's first light as the effect sees it (layer pixels), from [`crate::EffectHost::comp_scene`].
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||
pub struct CompLight {
|
||
/// Position in layer pixels (z away from the viewer).
|
||
pub pos: [f64; 3],
|
||
/// Direction the light travels (parallel / spot lights), unit, layer space.
|
||
pub dir: [f64; 3],
|
||
/// Colour × intensity (1 = 100 %).
|
||
pub color: [f32; 3],
|
||
/// 0 parallel, 1 point / spot, 2 ambient.
|
||
pub kind: u8,
|
||
}
|
||
|
||
/// The comp's camera and lights relative to the effect's layer.
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||
pub struct CompScene {
|
||
/// Layer pixels (x, y, z) → layer pixels, homogeneous rows X, Y, W: the comp camera's view
|
||
/// of the layer brought back into the layer's own pixel grid (identity on the layer plane
|
||
/// for the default camera).
|
||
pub camera: Option<[[f64; 4]; 3]>,
|
||
pub light: Option<CompLight>,
|
||
}
|
||
|
||
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
||
}
|
||
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
|
||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||
}
|
||
fn norm(a: [f64; 3]) -> [f64; 3] {
|
||
let l = dot(a, a).sqrt();
|
||
if l < 1e-12 { [0.0, 0.0, -1.0] } else { [a[0] / l, a[1] / l, a[2] / l] }
|
||
}
|
||
|
||
/// Lighting and Material, ready to shade pieces.
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub(crate) struct Lighting {
|
||
/// 0 distant, 1 point, 2 ambient only.
|
||
kind: u8,
|
||
color: [f32; 3],
|
||
pos: [f64; 3],
|
||
/// Distant light: direction towards the light.
|
||
to_light: [f64; 3],
|
||
ambient: f32,
|
||
kd: f32,
|
||
ks: f32,
|
||
sharp: f32,
|
||
}
|
||
|
||
impl Lighting {
|
||
pub fn from(ctx: &EffectCtx, b: &Buf) -> Lighting {
|
||
let pr = ctx.params;
|
||
let f = |id: &str, d: f64| pr.get(id).map(Value::as_f64).unwrap_or(d);
|
||
let [lw, lh] = ctx.layer_size;
|
||
let s = b.scale;
|
||
let centre = [b.offset[0] + lw * 0.5 * s, b.offset[1] + lh * 0.5 * s, 0.0];
|
||
let intensity = f("lighting/lightIntensity", 1.0) as f32;
|
||
let lc = pr.get("lighting/lightColor").map(|v| v.as_color()).unwrap_or([1.0; 4]);
|
||
let mut color = [lc[0] * intensity, lc[1] * intensity, lc[2] * intensity];
|
||
let lp = pr.get("lighting/lightPosition").map(|v| v.as_vec2()).unwrap_or([lw * 0.5, lh * 0.5]);
|
||
let (x, y) = b.to_px(lp);
|
||
let mut pos = [x, y, -f("lighting/lightDepth", 1.0) * lw.max(lh) * s];
|
||
let mut kind = if pr.e("lighting/lightType") == 1 { 1 } else { 0 };
|
||
let mut to_light = norm(sub(pos, centre));
|
||
if pr.e("lighting/lightType") == 2 {
|
||
match ctx.env.host.and_then(|h| h.comp_scene()).and_then(|sc| sc.light) {
|
||
Some(l) => {
|
||
color = [l.color[0] * intensity, l.color[1] * intensity, l.color[2] * intensity];
|
||
pos = [l.pos[0] * s + b.offset[0], l.pos[1] * s + b.offset[1], l.pos[2] * s];
|
||
kind = l.kind;
|
||
to_light = norm([-l.dir[0], -l.dir[1], -l.dir[2]]);
|
||
}
|
||
// No light in the comp: ambient only.
|
||
None => kind = 2,
|
||
}
|
||
}
|
||
Lighting {
|
||
kind,
|
||
color,
|
||
pos,
|
||
to_light,
|
||
ambient: f("lighting/ambientLight", 0.25) as f32,
|
||
kd: f("material/diffuse", 0.75) as f32,
|
||
ks: f("material/specular", 0.0) as f32,
|
||
sharp: f("material/highlightSharpness", 15.0).max(1.0) as f32,
|
||
}
|
||
}
|
||
|
||
/// Multiplier and additive highlight for a piece with unit normal `n` (rotated front normal)
|
||
/// at `at`, seen from `eye`. Pieces are lit on whichever side faces the viewer.
|
||
pub fn shade(&self, n: [f64; 3], at: [f64; 3], eye: [f64; 3]) -> ([f32; 3], [f32; 3]) {
|
||
let v = norm(sub(eye, at));
|
||
let n = if dot(n, v) < 0.0 { [-n[0], -n[1], -n[2]] } else { n };
|
||
if self.kind == 2 {
|
||
let a = self.ambient;
|
||
return ([a + self.kd * self.color[0], a + self.kd * self.color[1], a + self.kd * self.color[2]], [0.0; 3]);
|
||
}
|
||
let l = if self.kind == 1 { norm(sub(self.pos, at)) } else { self.to_light };
|
||
let ndl = dot(n, l).max(0.0) as f32;
|
||
let h = norm([l[0] + v[0], l[1] + v[1], l[2] + v[2]]);
|
||
let spec = if self.ks > 0.0 && ndl > 0.0 { self.ks * (dot(n, h).max(0.0) as f32).powf(self.sharp) } else { 0.0 };
|
||
(std::array::from_fn(|k| self.ambient + self.kd * ndl * self.color[k]), std::array::from_fn(|k| spec * self.color[k]))
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn simple_camera_keeps_the_layer_plane() {
|
||
let p = Proj::simple([10.0, 20.0], 100.0);
|
||
assert_eq!(p.project([3.0, 4.0, 0.0]), Some([3.0, 4.0]));
|
||
let e = p.eye();
|
||
assert!((e[0] - 10.0).abs() < 1e-9 && (e[1] - 20.0).abs() < 1e-9 && (e[2] + 100.0).abs() < 1e-9);
|
||
// Further away looks smaller (towards the centre).
|
||
let q = p.project([30.0, 20.0, 100.0]).unwrap();
|
||
assert!((q[0] - 20.0).abs() < 1e-9);
|
||
}
|
||
|
||
#[test]
|
||
fn rotation_orders_differ() {
|
||
let a = rot_ordered(0.5, 0.3, 0.2, 0);
|
||
let b = rot_ordered(0.5, 0.3, 0.2, 5);
|
||
assert!((0..3).any(|i| (0..3).any(|j| (a[i][j] - b[i][j]).abs() > 1e-3)));
|
||
let z = rot_ordered(0.0, 0.0, 0.0, 3);
|
||
assert_eq!(z, [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
|
||
}
|
||
}
|