makepad/libs/render/src/impostor.rs
Admin 76170701cd work: realism (render scale applies, near impostor crowns)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 03:41:19 +01:00

474 lines
23 KiB
Rust

//! Far stand-ins for foliage and props: a model rendered from many directions
//! into an atlas pair, drawn as one camera-facing quad from a distance on.
//!
//! [`bake`] rasterises the model's own layers (the packed stream the renderer
//! loads, its embedded base-colour images and cut-out masks) orthographically
//! from [`GRID`] x [`GRID`] directions over the upper hemisphere, laid out
//! hemi-octahedrally: grid point (i, j) maps to u = i / (GRID - 1) * 2 - 1,
//! v = j / (GRID - 1) * 2 - 1, and the direction toward the viewer is
//! normalize((u + v) / 2, 1 - |x| - |z|, (u - v) / 2) — the centre cell is the
//! view from straight above, the rim cells the views along the ground. So a
//! racing camera at a tree's foot and a flight camera above the canopy both
//! find frames made for them.
//!
//! Each frame stores two images: the albedo the model shows from there,
//! times an occlusion term (the vertex AO and a darkening of texels deep
//! behind the silhouette's front, a crown's inside), alpha = coverage, colour
//! bled into the cut-out texels so mips never fringe; and the normal in that
//! view's own frame, x and y in red and green (normal layers are
//! block-compressed to two channels on the GPU; z is rebuilt, the viewer's
//! side). The draw (shaders/impostor.rs) blends the three
//! frames nearest to the view and lights the result with the real sun, so a
//! far forest turns with the camera and shades with the time of day instead
//! of showing one baked picture.
//!
//! The quad carries what the shader needs in its own vertices: positions
//! are the bounding sphere's centre plus the corner times the radius in the
//! model's XY plane, and the uvs are (corner x radius), so the vertex stage
//! recovers centre and radius without a new material field.
use crate::model::StaticModel;
use makepad_draw::ImageBuffer;
/// Views per side of the hemi-octahedral grid. 6 x 6 views 18 degrees
/// apart: the shader blends the three nearest, so the step does not show,
/// and every texel of the atlas is block-compressed on a model's first load
/// (an 8 x 8 grid doubled that encode on a cold start).
pub const GRID: usize = 6;
/// The same, for the shaders (`#(..)` interpolation): the grid size and its
/// last index as floats.
pub const GRID_F: f64 = GRID as f64;
pub const GRID_LAST_F: f64 = (GRID - 1) as f64;
/// The bounding sphere fills this much of a view's cell: the rest is a
/// gutter of the view's own bled colour, so the filtered reads of a far,
/// minified impostor never pull in the neighbouring view (a dark seam
/// through the crown).
pub const FILL: f32 = 0.88;
pub const FILL_F: f64 = FILL as f64;
/// The colour atlas's alpha on texels a leaf card covers (255: the solid
/// core, a trunk); 0 is uncovered. Both count as covered for the draw.
pub const LEAF_ALPHA: u8 = 200;
pub struct ImpostorBake {
/// size x size RGBA8 (straight alpha): albedo, alpha = coverage.
pub albedo: Vec<u8>,
/// size/2 x size/2 RGBA8: the view-space normal's x, y * 0.5 + 0.5
/// in rg. Half the albedo's resolution: shading varies slower than the
/// silhouette, and every texel here is block-compressed on first load.
pub normal: Vec<u8>,
/// The albedo atlas's edge (the normal atlas's is half of it).
pub size: usize,
/// The bounding sphere the frames were taken around (model space).
pub center: [f32; 3],
pub radius: f32,
}
fn f16_to_f32(h: u16) -> f32 {
let s = if h & 0x8000 != 0 { -1.0 } else { 1.0 };
let e = ((h >> 10) & 0x1f) as i32;
let m = (h & 0x3ff) as f32;
s * match e {
0 => m * 2f32.powi(-24),
31 => f32::INFINITY,
_ => (1.0 + m / 1024.0) * 2f32.powi(e - 15),
}
}
/// The direction toward the viewer of grid point (i, j) (shaders/impostor.rs
/// `imp_frame_dir` is the same formula).
pub fn frame_dir(i: usize, j: usize) -> [f32; 3] {
let k = (GRID - 1) as f32;
let u = i as f32 / k * 2.0 - 1.0;
let v = j as f32 / k * 2.0 - 1.0;
let (x, z) = ((u + v) * 0.5, (u - v) * 0.5);
let y = 1.0 - x.abs() - z.abs();
norm([x, y.max(0.0), z])
}
/// A frame's image axes (right, up) for its view direction `f` (toward the
/// viewer). The shader builds the same basis.
pub fn frame_basis(f: [f32; 3]) -> ([f32; 3], [f32; 3]) {
let right = if f[1] > 0.999 { [1.0, 0.0, 0.0] } else { norm(cross([0.0, 1.0, 0.0], f)) };
(right, cross(f, right))
}
fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
}
fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn norm(a: [f32; 3]) -> [f32; 3] {
let l = dot(a, a).sqrt().max(1e-12);
[a[0] / l, a[1] / l, a[2] / l]
}
struct Layer<'a> {
vertices: &'a [f32],
indices: &'a [u32],
uvs: Option<&'a [[f32; 2]]>,
image: Option<ImageBuffer>,
cutoff: Option<f32>,
/// The image's box-filtered mip chain (level 0 = the image): a view
/// texel reads the level that matches its footprint on the image.
mips: Vec<(usize, usize, Vec<u32>)>,
}
/// Box-filtered mips of a straight-alpha ARGB image, colour weighted by
/// alpha (no dark fringes where leaves meet the cut-out), down to 1x1.
fn image_mips(img: &ImageBuffer) -> Vec<(usize, usize, Vec<u32>)> {
let mut out = vec![(img.width, img.height, img.data[..img.width * img.height].to_vec())];
while let Some((w, h, d)) = out.last().filter(|(w, h, _)| *w > 1 || *h > 1) {
let (nw, nh) = ((w / 2).max(1), (h / 2).max(1));
let mut nd = vec![0u32; nw * nh];
for y in 0..nh {
for x in 0..nw {
let mut acc = [0u32; 4];
for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
let p = d[(2 * y + dy).min(h - 1) * w + (2 * x + dx).min(w - 1)];
let a = p >> 24;
acc[0] += ((p >> 16) & 0xff) * a;
acc[1] += ((p >> 8) & 0xff) * a;
acc[2] += (p & 0xff) * a;
acc[3] += a;
}
let a = acc[3].max(1);
nd[y * nw + x] = ((acc[3] / 4) << 24) | ((acc[0] / a) << 16) | ((acc[1] / a) << 8) | (acc[2] / a);
}
}
out.push((nw, nh, nd));
}
out
}
/// A triangle ready to rasterise into every frame: model-space corners, uvs,
/// vertex tint+AO, and the normal it shades with.
struct Tri<'a> {
p: [[f32; 3]; 3],
uv: [[f32; 2]; 3],
col: [[f32; 4]; 3],
n: [f32; 3],
/// The corners' shading normals (the mesh's own, smooth where the
/// model is smooth).
vn: [[f32; 3]; 3],
layer: &'a Layer<'a>,
}
/// None when the model has nothing to draw (or is itself only an impostor).
/// `cell`: each view's pixels per side (even; the atlas is GRID cells).
pub fn bake(glb: &[u8], cell: usize) -> Option<ImpostorBake> {
let cell = (cell.max(8) + 1) & !1;
let model = StaticModel::parse_glb(glb).ok()?;
let image = |png: &Option<Vec<u8>>| png.as_ref().and_then(|p| ImageBuffer::from_png(p).ok());
let mut layers = Vec::new();
if model.draw_layers.is_empty() {
layers.push(Layer { vertices: &model.vertices, indices: &model.indices, uvs: None, image: image(&model.texture_png), cutoff: None, mips: Vec::new() });
} else {
for l in &model.draw_layers {
let surface = l.pbr.surface.as_ref();
if surface.is_some_and(|s| s.impostor > 0.0) { continue; }
let cutoff = surface.filter(|s| s.alpha_mode == 1).map(|s| s.alpha_cutoff);
layers.push(Layer { vertices: &l.vertices, indices: &l.indices, uvs: Some(&l.uvs), image: image(&l.texture_png), cutoff, mips: Vec::new() });
}
}
const F: usize = crate::model::MODEL_VERTEX_FLOATS;
let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]);
for l in &layers {
for v in l.vertices.chunks_exact(F) {
for d in 0..3 { lo[d] = lo[d].min(v[d]); hi[d] = hi[d].max(v[d]); }
}
}
if lo[0] > hi[0] || hi[1] - lo[1] < 1e-3 { return None; }
let center = [(lo[0] + hi[0]) * 0.5, (lo[1] + hi[1]) * 0.5, (lo[2] + hi[2]) * 0.5];
let mut radius = 0.0f32;
for l in &layers {
for v in l.vertices.chunks_exact(F) {
radius = radius.max(dot([v[0] - center[0], v[1] - center[1], v[2] - center[2]], [v[0] - center[0], v[1] - center[1], v[2] - center[2]]));
}
}
let radius = radius.sqrt().max(1e-3);
// The crown's centre (where leaves bend their normals away from): the
// middle of the cut-out layers' bounds, else the model's.
let (mut clo, mut chi) = ([f32::MAX; 3], [f32::MIN; 3]);
for l in layers.iter().filter(|l| l.cutoff.is_some()) {
for v in l.vertices.chunks_exact(F) {
for d in 0..3 { clo[d] = clo[d].min(v[d]); chi[d] = chi[d].max(v[d]); }
}
}
let crown = if clo[0] <= chi[0] { [(clo[0] + chi[0]) * 0.5, (clo[1] + chi[1]) * 0.5, (clo[2] + chi[2]) * 0.5] } else { center };
// Triangles once; every frame reuses them.
for l in &mut layers {
if let Some(img) = &l.image { l.mips = image_mips(img); }
}
let mut tris = Vec::new();
for l in &layers {
for tri in l.indices.chunks_exact(3) {
let mut t = Tri { p: [[0.0; 3]; 3], uv: [[0.0; 2]; 3], col: [[1.0; 4]; 3], n: [0.0; 3], vn: [[0.0; 3]; 3], layer: l };
for k in 0..3 {
let i = tri[k] as usize;
let Some(v) = l.vertices.get(i * F..i * F + F) else { continue };
t.p[k] = [v[0], v[1], v[2]];
let (ox, oy) = crate::skin::unpack2f16_pub(v[3]);
let n = crate::skin::oct_decode_pub(ox, oy);
t.vn[k] = [n.x, n.y, n.z];
t.uv[k] = match l.uvs {
Some(uvs) => uvs.get(i).copied().unwrap_or([0.0, 0.0]),
None => { let b = v[4].to_bits(); [f16_to_f32(b as u16), f16_to_f32((b >> 16) as u16)] }
};
let c = v[5].to_bits();
t.col[k] = [(c & 0xff) as f32 / 255.0, ((c >> 8) & 0xff) as f32 / 255.0, ((c >> 16) & 0xff) as f32 / 255.0, ((c >> 24) & 0xff) as f32 / 255.0];
}
let e1 = [t.p[1][0] - t.p[0][0], t.p[1][1] - t.p[0][1], t.p[1][2] - t.p[0][2]];
let e2 = [t.p[2][0] - t.p[0][0], t.p[2][1] - t.p[0][1], t.p[2][2] - t.p[0][2]];
let n = cross(e1, e2);
if dot(n, n) < 1e-14 { continue; }
t.n = norm(n);
tris.push(t);
}
}
let w = GRID * cell;
let mut albedo = vec![0u8; w * w * 4];
let mut normal = vec![0u8; w * w * 4];
let mut depth = vec![f32::MIN; cell * cell];
let mut occ = vec![0.0f32; cell * cell];
for j in 0..GRID {
for i in 0..GRID {
let f = frame_dir(i, j);
let (right, up) = frame_basis(f);
let (x_off, y_off) = (i * cell, j * cell);
depth.fill(f32::MIN);
for t in &tris {
let mut sp = [[0.0f32; 3]; 3];
for k in 0..3 {
let d = [t.p[k][0] - center[0], t.p[k][1] - center[1], t.p[k][2] - center[2]];
sp[k] = [(dot(d, right) / radius * 0.5 * FILL + 0.5) * cell as f32, (0.5 - dot(d, up) / radius * 0.5 * FILL) * cell as f32, dot(d, f)];
}
let area = (sp[1][0] - sp[0][0]) * (sp[2][1] - sp[0][1]) - (sp[1][1] - sp[0][1]) * (sp[2][0] - sp[0][0]);
if area.abs() < 1e-6 { continue; }
// The side the viewer sees (the face's; per texel the mesh's own
// interpolated normal on that side, below); leaves bend toward
// "out of the crown" per texel so a crown shades as a soft volume.
let n_side = if dot(t.n, f) < 0.0 { [-t.n[0], -t.n[1], -t.n[2]] } else { t.n };
let leaf = t.layer.cutoff.is_some();
// The image level whose texels match one view texel's footprint
// on it: a point read of the full image aliased a leaf card's
// texture into speckle, which a magnified stand-in shows as
// blotchy squares.
let level = t.layer.mips.first().map_or(0, |&(iw, ih, _)| {
let (e1, e2) = ([sp[1][0] - sp[0][0], sp[1][1] - sp[0][1]], [sp[2][0] - sp[0][0], sp[2][1] - sp[0][1]]);
let (d1, d2) = ([t.uv[1][0] - t.uv[0][0], t.uv[1][1] - t.uv[0][1]], [t.uv[2][0] - t.uv[0][0], t.uv[2][1] - t.uv[0][1]]);
let dx = [(d1[0] * e2[1] - d2[0] * e1[1]) / area * iw as f32, (d1[1] * e2[1] - d2[1] * e1[1]) / area * ih as f32];
let dy = [(d2[0] * e1[0] - d1[0] * e2[0]) / area * iw as f32, (d2[1] * e1[0] - d1[1] * e2[0]) / area * ih as f32];
let fp = (dx[0] * dx[0] + dx[1] * dx[1]).max(dy[0] * dy[0] + dy[1] * dy[1]).sqrt();
if fp > 1.0 { (fp.log2().floor() as usize).min(t.layer.mips.len() - 1) } else { 0 }
});
let minx = sp.iter().map(|p| p[0]).fold(f32::MAX, f32::min).floor().max(0.0) as usize;
let maxx = (sp.iter().map(|p| p[0]).fold(f32::MIN, f32::max).ceil().max(0.0) as usize).min(cell);
let miny = sp.iter().map(|p| p[1]).fold(f32::MAX, f32::min).floor().max(0.0) as usize;
let maxy = (sp.iter().map(|p| p[1]).fold(f32::MIN, f32::max).ceil().max(0.0) as usize).min(cell);
for py in miny..maxy {
for px in minx..maxx {
let (fx, fy) = (px as f32 + 0.5, py as f32 + 0.5);
let w0 = ((sp[1][0] - fx) * (sp[2][1] - fy) - (sp[1][1] - fy) * (sp[2][0] - fx)) / area;
let w1 = ((sp[2][0] - fx) * (sp[0][1] - fy) - (sp[2][1] - fy) * (sp[0][0] - fx)) / area;
let w2 = 1.0 - w0 - w1;
if w0 < 0.0 || w1 < 0.0 || w2 < 0.0 { continue; }
let z = w0 * sp[0][2] + w1 * sp[1][2] + w2 * sp[2][2];
let cell = py * cell + px;
if z <= depth[cell] { continue; }
let u = w0 * t.uv[0][0] + w1 * t.uv[1][0] + w2 * t.uv[2][0];
let v = w0 * t.uv[0][1] + w1 * t.uv[1][1] + w2 * t.uv[2][1];
let (mut r, mut g, mut bl, mut al) = (1.0f32, 1.0f32, 1.0f32, 1.0f32);
if let Some((iw, ih, data)) = t.layer.mips.get(level) {
let (iw, ih) = (*iw, *ih);
let tx = ((u.rem_euclid(1.0) * iw as f32) as usize).min(iw - 1);
let ty = ((v.rem_euclid(1.0) * ih as f32) as usize).min(ih - 1);
let p = data[ty * iw + tx];
r = ((p >> 16) & 0xff) as f32 / 255.0;
g = ((p >> 8) & 0xff) as f32 / 255.0;
bl = (p & 0xff) as f32 / 255.0;
al = ((p >> 24) & 0xff) as f32 / 255.0;
}
if let Some(cut) = t.layer.cutoff { if al < cut { continue; } }
let c = [0, 1, 2, 3].map(|k| w0 * t.col[0][k] + w1 * t.col[1][k] + w2 * t.col[2][k]);
depth[cell] = z;
occ[cell] = c[3];
let o = ((y_off + py) * w + x_off + px) * 4;
albedo[o] = (r * c[0] * 255.0).clamp(0.0, 255.0) as u8;
albedo[o + 1] = (g * c[1] * 255.0).clamp(0.0, 255.0) as u8;
albedo[o + 2] = (bl * c[2] * 255.0).clamp(0.0, 255.0) as u8;
// Alpha tells leaves (LEAF_ALPHA) from the solid core and
// trunk (255): the stand-in's shadow lets light through
// some leaf texels (shaders/lm_depth.rs).
albedo[o + 3] = if t.layer.cutoff.is_some() { LEAF_ALPHA } else { 255 };
// A leaf texel's normal leans out from the crown's
// middle at its own position: bent once per card
// (at its centroid) each card lit flat, and a
// magnified stand-in showed the crown in squares.
// The interpolated vertex normal (a low-poly crown core
// shaded with its face normals showed its facets as
// squares on a magnified stand-in).
let vi = [0, 1, 2].map(|d| w0 * t.vn[0][d] + w1 * t.vn[1][d] + w2 * t.vn[2][d]);
let mut n = if dot(vi, vi) > 1e-6 {
let vi = norm(vi);
if dot(vi, n_side) < 0.0 { [-vi[0], -vi[1], -vi[2]] } else { vi }
} else { n_side };
if leaf {
let m = [0, 1, 2].map(|d| w0 * t.p[0][d] + w1 * t.p[1][d] + w2 * t.p[2][d] - crown[d]);
if dot(m, m) > 1e-8 {
let m = norm(m);
n = norm([n[0] + m[0] * 1.5, n[1] + m[1] * 1.5, n[2] + m[2] * 1.5]);
}
}
let e = view_encode(n, right, up, f);
normal[o] = ((e[0] * 0.5 + 0.5) * 255.0).round() as u8;
normal[o + 1] = ((e[1] * 0.5 + 0.5) * 255.0).round() as u8;
normal[o + 2] = 128;
normal[o + 3] = 255;
}
}
}
// Occlusion, multiplied into the albedo (the normal atlas is
// block-compressed to two channels, see the module docs): the
// vertex AO times a darkening of texels far behind the
// silhouette's front (the crown's inside, which a sunlit tree
// shows darker in direct and ambient light alike).
let (mut zmin, mut zmax) = (f32::MAX, f32::MIN);
for &d in &depth { if d > f32::MIN { zmin = zmin.min(d); zmax = zmax.max(d); } }
let span = (zmax - zmin).max(1e-4);
for py in 0..cell {
for px in 0..cell {
let cell = py * cell + px;
if depth[cell] == f32::MIN { continue; }
let k = occ[cell] * (1.0 - 0.4 * ((zmax - depth[cell]) / span));
let o = ((y_off + py) * w + x_off + px) * 4;
for c in 0..3 { albedo[o + c] = (albedo[o + c] as f32 * k) as u8; }
}
}
}
}
bleed(&mut albedo, &mut normal, w);
let normal = half(&normal, w);
Some(ImpostorBake { albedo, normal, size: w, center, radius })
}
/// A normal in its view's own frame (right, up, toward the viewer), the
/// viewer's side only: x and y are stored, z is rebuilt. Every texel then
/// lies on one smooth hemisphere, so the GPU's filtering and the half-size
/// average blend neighbours correctly (an octahedral encoding of the
/// model-space normal folds at the back hemisphere, and filtering across the
/// fold drew dark seams through the crowns).
fn view_encode(n: [f32; 3], right: [f32; 3], up: [f32; 3], f: [f32; 3]) -> [f32; 2] {
let v = norm([dot(n, right), dot(n, up), dot(n, f).max(0.05)]);
[v[0], v[1]]
}
/// The normal atlas at half resolution: each 2x2 block's view-space normals
/// averaged and encoded again.
fn half(normal: &[u8], w: usize) -> Vec<u8> {
let h = w / 2;
let mut out = vec![0u8; h * h * 4];
let dec = |o: usize| -> [f32; 3] {
let e = [normal[o] as f32 / 255.0 * 2.0 - 1.0, normal[o + 1] as f32 / 255.0 * 2.0 - 1.0];
[e[0], e[1], (1.0 - e[0] * e[0] - e[1] * e[1]).max(0.0).sqrt()]
};
for y in 0..h {
for x in 0..h {
let mut n = [0.0f32; 3];
for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
let d = dec(((2 * y + dy) * w + 2 * x + dx) * 4);
for c in 0..3 { n[c] += d[c]; }
}
let n = norm(n);
let e = [n[0], n[1]];
let o = (y * h + x) * 4;
out[o] = ((e[0] * 0.5 + 0.5) * 255.0).round() as u8;
out[o + 1] = ((e[1] * 0.5 + 0.5) * 255.0).round() as u8;
out[o + 2] = 128;
out[o + 3] = 255;
}
}
out
}
/// Spread each covered texel's colour and normal into its uncovered
/// neighbours (alpha stays 0), so mips and bilinear reads never pull black
/// or a zero normal into the silhouette.
fn bleed(albedo: &mut [u8], normal: &mut [u8], w: usize) {
let mut filled: Vec<bool> = albedo.chunks_exact(4).map(|p| p[3] != 0).collect();
let mut front: Vec<usize> = Vec::new();
for _ in 0..8 {
front.clear();
for y in 0..w {
for x in 0..w {
let at = y * w + x;
if filled[at] { continue; }
let (mut sa, mut sn, mut n) = ([0u32; 3], [0u32; 4], 0u32);
for (dx, dy) in [(-1i32, 0i32), (1, 0), (0, -1), (0, 1)] {
let (nx, ny) = (x as i32 + dx, y as i32 + dy);
if nx < 0 || ny < 0 || nx >= w as i32 || ny >= w as i32 { continue; }
let p = ny as usize * w + nx as usize;
if !filled[p] { continue; }
for c in 0..3 { sa[c] += albedo[p * 4 + c] as u32; }
for c in 0..4 { sn[c] += normal[p * 4 + c] as u32; }
n += 1;
}
if n > 0 {
for c in 0..3 { albedo[at * 4 + c] = (sa[c] / n) as u8; }
for c in 0..4 { normal[at * 4 + c] = (sn[c] / n) as u8; }
front.push(at);
}
}
}
if front.is_empty() { break; }
for &at in &front { filled[at] = true; }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn half_floats_decode() {
assert_eq!(f16_to_f32(0x3c00), 1.0);
assert_eq!(f16_to_f32(0x3800), 0.5);
assert_eq!(f16_to_f32(0xc000), -2.0);
}
/// The grid's centre is the view from above, its rim the views along
/// the ground, and every frame's basis is orthonormal.
#[test]
fn hemi_octahedral_frames() {
let top = frame_dir(GRID / 2, GRID / 2);
assert!(top[1] > 0.8, "{top:?}");
let rim = frame_dir(GRID - 1, 0);
assert!(rim[1].abs() < 1e-5, "{rim:?}");
for j in 0..GRID {
for i in 0..GRID {
let f = frame_dir(i, j);
let (r, u) = frame_basis(f);
assert!(dot(r, f).abs() < 1e-4 && dot(u, f).abs() < 1e-4 && dot(r, u).abs() < 1e-4);
assert!((dot(u, u) - 1.0).abs() < 1e-3 && (dot(r, r) - 1.0).abs() < 1e-3);
}
}
}
/// The shader's encode (view direction -> grid coordinate) inverts the
/// bake's frame directions.
#[test]
fn frame_lookup_round_trips() {
for j in 0..GRID {
for i in 0..GRID {
let d = frame_dir(i, j);
let s = d[0].abs() + d[1].abs() + d[2].abs();
let (x, z) = (d[0] / s, d[2] / s);
let (u, v) = (x + z, x - z);
let g = [(u * 0.5 + 0.5) * (GRID - 1) as f32, (v * 0.5 + 0.5) * (GRID - 1) as f32];
assert!((g[0] - i as f32).abs() < 1e-3 && (g[1] - j as f32).abs() < 1e-3, "{i},{j} -> {g:?}");
}
}
}
}