// Viewport: NDC <-> window ("screen" / framebuffer pixel) coordinates.
//
// Window space: origin at the TOP-LEFT, +x right, +y DOWN, units = pixels. Pixel (i, j) covers
// [i, i+1) x [j, j+1) and its centre is (i + 0.5, j + 0.5) (D3D10+/Vulkan/Metal/WebGPU rule).
// Window depth = min_depth + unit_depth * (max_depth - min_depth).
///|
#valtype
pub(all) struct Viewport {
x : Double
y : Double
width : Double
height : Double
min_depth : Double
max_depth : Double
} derive(Eq, Debug)
///|
pub fn Viewport::new(
x : Double,
y : Double,
width : Double,
height : Double,
) -> Viewport {
if !(width > 0.0 && height > 0.0) {
abort("Viewport::new: width and height must be > 0")
}
{ x, y, width, height, min_depth: 0.0, max_depth: 1.0, }
}
///|
pub fn Viewport::full(width : Double, height : Double) -> Viewport {
Viewport::new(0.0, 0.0, width, height)
}
///|
/// Viewport for a window given in LOGICAL (DPI-independent) units; the pixel size is the
/// logical size times `scale`, rounded to whole device pixels.
pub fn Viewport::from_logical(
logical_width : Double,
logical_height : Double,
scale : Double,
) -> Viewport {
Viewport::full(
@math.round(logical_width * scale),
@math.round(logical_height * scale),
)
}
///|
pub fn Viewport::aspect(self : Viewport) -> Double {
self.width / self.height
}
///|
/// NDC (x, y, z) -> window coordinates (x px, y px, depth). `mode` tells how ndc z maps to
/// [0,1] before min/max depth are applied.
pub fn Viewport::ndc_to_screen(
self : Viewport,
ndc : Vec3,
mode : DepthMode,
) -> Vec3 {
let u = mode.to_unit(ndc.z)
{
x: self.x + (ndc.x * 0.5 + 0.5) * self.width,
y: self.y + (0.5 - ndc.y * 0.5) * self.height,
z: self.min_depth + u * (self.max_depth - self.min_depth),
}
}
///|
/// Window coordinates + window depth -> NDC.
pub fn Viewport::screen_to_ndc(
self : Viewport,
sx : Double,
sy : Double,
depth : Double,
mode : DepthMode,
) -> Vec3 {
let u = (depth - self.min_depth) / (self.max_depth - self.min_depth)
{
x: (sx - self.x) / self.width * 2.0 - 1.0,
y: 1.0 - (sy - self.y) / self.height * 2.0,
z: mode.from_unit(u),
}
}
///|
/// Window xy -> NDC xy only.
pub fn Viewport::screen_to_ndc_xy(self : Viewport, p : Vec2) -> Vec2 {
{
x: (p.x - self.x) / self.width * 2.0 - 1.0,
y: 1.0 - (p.y - self.y) / self.height * 2.0,
}
}
///|
pub fn Viewport::ndc_to_screen_xy(self : Viewport, ndc : Vec2) -> Vec2 {
{
x: self.x + (ndc.x * 0.5 + 0.5) * self.width,
y: self.y + (0.5 - ndc.y * 0.5) * self.height,
}
}
///|
/// Window position -> normalised [0,1]^2 coordinates inside the viewport (origin top-left;
/// the same layout as texture UVs of a full-screen image).
pub fn Viewport::to_uv(self : Viewport, p : Vec2) -> Vec2 {
{ x: (p.x - self.x) / self.width, y: (p.y - self.y) / self.height, }
}
///|
pub fn Viewport::from_uv(self : Viewport, uv : Vec2) -> Vec2 {
{ x: self.x + uv.x * self.width, y: self.y + uv.y * self.height, }
}
///|
/// Does the window position fall inside the viewport rectangle?
pub fn Viewport::contains(self : Viewport, p : Vec2) -> Bool {
p.x >= self.x &&
p.x < self.x + self.width &&
p.y >= self.y &&
p.y < self.y + self.height
}
///|
/// Centre of pixel (ix, iy) in window coordinates.
pub fn pixel_center(ix : Int, iy : Int) -> Vec2 {
{ x: Double::from_int(ix) + 0.5, y: Double::from_int(iy) + 0.5, }
}
///|
/// Integer pixel containing a window position.
pub fn pixel_of(p : Vec2) -> (Int, Int) {
(@math.floor(p.x).to_int(), @math.floor(p.y).to_int())
}
///|
pub fn logical_to_physical(p : Vec2, scale : Double) -> Vec2 {
p.scale(scale)
}
///|
pub fn physical_to_logical(p : Vec2, scale : Double) -> Vec2 {
p.scale(1.0 / scale)
}