// Texture-side coordinate systems.
//
// UV: origin at the TOP-LEFT of the image, u -> right, v -> DOWN (WebGPU / Vulkan / Metal / D3D
// / glTF layout). Texel (i, j) covers uv [i/w, (i+1)/w) x [j/h, (j+1)/h); its centre is
// ((i + 0.5)/w, (j + 0.5)/h). Use `flip_v` for OpenGL's bottom-left origin images.
///|
/// UV -> continuous texel coordinates (texel i spans [i, i+1)). Pixel centre = i + 0.5.
pub fn uv_to_texel(uv : Vec2, width : Int, height : Int) -> Vec2 {
{ x: uv.x * Double::from_int(width), y: uv.y * Double::from_int(height), }
}
///|
pub fn texel_to_uv(t : Vec2, width : Int, height : Int) -> Vec2 {
{ x: t.x / Double::from_int(width), y: t.y / Double::from_int(height), }
}
///|
/// UV of the CENTRE of texel (i, j).
pub fn texel_center_uv(i : Int, j : Int, width : Int, height : Int) -> Vec2 {
{
x: (Double::from_int(i) + 0.5) / Double::from_int(width),
y: (Double::from_int(j) + 0.5) / Double::from_int(height),
}
}
///|
/// Switch between top-left (v down) and bottom-left (v up) conventions.
pub fn flip_v(uv : Vec2) -> Vec2 {
{ x: uv.x, y: 1.0 - uv.y, }
}
///|
pub(all) enum WrapMode {
Clamp
Repeat
MirroredRepeat
} derive(Eq, Debug)
///|
/// Wrap a normalised coordinate into [0, 1].
pub fn wrap_coord(u : Double, mode : WrapMode) -> Double {
match mode {
Clamp => clamp(u, 0.0, 1.0)
Repeat => u - @math.floor(u)
MirroredRepeat => {
let t = u - 2.0 * @math.floor(u * 0.5)
if t > 1.0 {
2.0 - t
} else {
t
}
}
}
}
///|
/// Wrap an integer texel index into [0, n).
pub fn wrap_texel(i : Int, n : Int, mode : WrapMode) -> Int {
match mode {
Clamp => i.clamp(min=0, max=n - 1)
Repeat => mod_floor(i, n)
MirroredRepeat => {
let m = mod_floor(i, 2 * n)
if m >= n {
2 * n - 1 - m
} else {
m
}
}
}
}
///|
/// The 2x2 texel neighbourhood and weights of a bilinear fetch at `uv`.
/// Result texel = (1-fx)(1-fy) T[x0,y0] + fx(1-fy) T[x1,y0] + (1-fx)fy T[x0,y1] + fx fy T[x1,y1].
#valtype
pub(all) struct Bilinear {
x0 : Int
y0 : Int
x1 : Int
y1 : Int
fx : Double
fy : Double
} derive(Eq, Debug)
///|
pub fn bilinear_footprint(
uv : Vec2,
width : Int,
height : Int,
wrap_u : WrapMode,
wrap_v : WrapMode,
) -> Bilinear {
let x = uv.x * Double::from_int(width) - 0.5
let y = uv.y * Double::from_int(height) - 0.5
let fxl = @math.floor(x)
let fyl = @math.floor(y)
let ix = fxl.to_int()
let iy = fyl.to_int()
{
x0: wrap_texel(ix, width, wrap_u),
y0: wrap_texel(iy, height, wrap_v),
x1: wrap_texel(ix + 1, width, wrap_u),
y1: wrap_texel(iy + 1, height, wrap_v),
fx: x - fxl,
fy: y - fyl,
}
}
///|
/// Size of a mip level (>= 1).
pub fn mip_extent(size : Int, level : Int) -> Int {
let s = size >> level
if s < 1 {
1
} else {
s
}
}
///|
/// Number of mip levels of a full chain down to 1x1.
pub fn mip_level_count(width : Int, height : Int) -> Int {
let mut m = if width > height { width } else { height }
let mut n = 1
while m > 1 {
m = m >> 1
n += 1
}
n
}
///|
/// Level of detail from screen-space UV derivatives (what `textureSample` computes):
/// lod = log2(max(|d(uv * size)/dx|, |d(uv * size)/dy|)). May be negative (magnification).
pub fn mip_lod(
duv_dx : Vec2,
duv_dy : Vec2,
width : Int,
height : Int,
) -> Double {
let sz : Vec2 = { x: Double::from_int(width), y: Double::from_int(height), }
let lx = (duv_dx * sz).length()
let ly = (duv_dy * sz).length()
let m = lx.max(ly)
if m <= 0.0 {
-@double.infinity
} else {
@math.ln(m) / @math.ln(2.0)
}
}
// ---------------------------------------------------------------------------
// Cube maps
// ---------------------------------------------------------------------------
///|
/// Cube face indices (same order as OpenGL / D3D / Vulkan array layers).
pub const CUBE_POS_X : Int = 0
///|
pub const CUBE_NEG_X : Int = 1
///|
pub const CUBE_POS_Y : Int = 2
///|
pub const CUBE_NEG_Y : Int = 3
///|
pub const CUBE_POS_Z : Int = 4
///|
pub const CUBE_NEG_Z : Int = 5
///|
/// Face + coordinates inside that face image (u right, v down, both in [0, 1]).
#valtype
pub(all) struct CubeCoord {
face : Int
u : Double
v : Double
} derive(Eq, Debug)
///|
/// Direction -> (face, u, v) following the OpenGL cube-map selection table (also used by
/// D3D and Vulkan). `dir` need not be unit; zero gives face 0 centre. Ties between equal
/// major axes resolve X, then Y, then Z.
pub fn cube_from_direction(dir : Vec3) -> CubeCoord {
let ax = dir.x.abs()
let ay = dir.y.abs()
let az = dir.z.abs()
let mut face = 0
let mut sc = 0.0
let mut tc = 0.0
let mut ma = 0.0
if ax >= ay && ax >= az {
ma = ax
if dir.x >= 0.0 {
face = CUBE_POS_X
sc = -dir.z
tc = -dir.y
} else {
face = CUBE_NEG_X
sc = dir.z
tc = -dir.y
}
} else if ay >= az {
ma = ay
if dir.y >= 0.0 {
face = CUBE_POS_Y
sc = dir.x
tc = dir.z
} else {
face = CUBE_NEG_Y
sc = dir.x
tc = -dir.z
}
} else {
ma = az
if dir.z >= 0.0 {
face = CUBE_POS_Z
sc = dir.x
tc = -dir.y
} else {
face = CUBE_NEG_Z
sc = -dir.x
tc = -dir.y
}
}
if ma == 0.0 {
return { face: 0, u: 0.5, v: 0.5, }
}
{ face, u: (sc / ma + 1.0) * 0.5, v: (tc / ma + 1.0) * 0.5, }
}
///|
/// (face, u, v) -> UNIT direction through that point of the face (exact inverse of
/// `cube_from_direction`).
pub fn cube_to_direction(face : Int, u : Double, v : Double) -> Vec3 {
let sc = 2.0 * u - 1.0
let tc = 2.0 * v - 1.0
let d : Vec3 = match face {
0 => { x: 1.0, y: -tc, z: -sc, }
1 => { x: -1.0, y: -tc, z: sc, }
2 => { x: sc, y: 1.0, z: tc, }
3 => { x: sc, y: -1.0, z: -tc, }
4 => { x: sc, y: -tc, z: 1.0, }
5 => { x: -sc, y: -tc, z: -1.0, }
_ => abort("cube_to_direction: face must be 0..5")
}
d.normalize()
}
// ---------------------------------------------------------------------------
// Equirectangular (lat/long) panoramas
// ---------------------------------------------------------------------------
///|
/// Direction -> equirectangular UV. The image centre (0.5, 0.5) looks down -Z (forward);
/// u increases toward +X (turning right), v increases DOWNWARD (v = 0 is straight up).
pub fn equirect_from_direction(dir : Vec3) -> Vec2 {
let lon = @math.atan2(dir.x, -dir.z)
let lat = @math.atan2(dir.y, @math.hypot(dir.x, dir.z))
{ x: 0.5 + lon / TAU, y: 0.5 - lat / PI, }
}
///|
pub fn equirect_to_direction(uv : Vec2) -> Vec3 {
let lon = (uv.x - 0.5) * TAU
let lat = (0.5 - uv.y) * PI
let c = @math.cos(lat)
{ x: @math.sin(lon) * c, y: @math.sin(lat), z: -@math.cos(lon) * c, }
}
// ---------------------------------------------------------------------------
// Octahedral mapping (Cigolle et al. 2014): unit vector <-> [-1, 1]^2, uniform and cheap.
// Used for normal compression, probes, environment atlases.
// ---------------------------------------------------------------------------
///|
pub fn oct_encode(n : Vec3) -> Vec2 {
let l1 = n.x.abs() + n.y.abs() + n.z.abs()
let px = n.x / l1
let py = n.y / l1
if n.z < 0.0 {
{ x: (1.0 - py.abs()) * sign_nz(px), y: (1.0 - px.abs()) * sign_nz(py), }
} else {
{ x: px, y: py, }
}
}
///|
pub fn oct_decode(e : Vec2) -> Vec3 {
let z = 1.0 - e.x.abs() - e.y.abs()
let v : Vec3 = if z < 0.0 {
{
x: (1.0 - e.y.abs()) * sign_nz(e.x),
y: (1.0 - e.x.abs()) * sign_nz(e.y),
z,
}
} else {
{ x: e.x, y: e.y, z, }
}
v.normalize()
}
///|
/// [-1,1]^2 octahedral coordinate -> [0,1]^2 texture UV.
pub fn oct_to_uv(e : Vec2) -> Vec2 {
{ x: e.x * 0.5 + 0.5, y: e.y * 0.5 + 0.5, }
}
///|
pub fn uv_to_oct(uv : Vec2) -> Vec2 {
{ x: uv.x * 2.0 - 1.0, y: uv.y * 2.0 - 1.0, }
}