#![allow(dead_code)]
#[inline]
fn sub3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
fn add3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
fn scale3(v: [f32; 3], s: f32) -> [f32; 3] {
[v[0] * s, v[1] * s, v[2] * s]
}
#[inline]
fn cross3(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],
]
}
#[inline]
fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[inline]
fn len3(v: [f32; 3]) -> f32 {
dot3(v, v).sqrt()
}
#[inline]
fn normalize3(v: [f32; 3]) -> [f32; 3] {
let l = len3(v);
if l < 1e-12 {
[0.0, 0.0, 1.0]
} else {
scale3(v, 1.0 / l)
}
}
#[inline]
fn lerp3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
add3(scale3(a, 1.0 - t), scale3(b, t))
}
#[allow(dead_code)]
pub struct BezierPatch {
pub ctrl: [[f32; 3]; 16],
}
#[allow(dead_code)]
pub struct PatchConfig {
pub resolution: u32,
pub normal_eps: f32,
pub flip_normal: bool,
}
#[allow(dead_code)]
pub struct PatchSample {
pub position: [f32; 3],
pub normal: [f32; 3],
pub tangent_u: [f32; 3],
pub tangent_v: [f32; 3],
pub uv: [f32; 2],
}
#[allow(dead_code)]
pub struct PatchTessellation {
pub positions: Vec<[f32; 3]>,
pub normals: Vec<[f32; 3]>,
pub uvs: Vec<[f32; 2]>,
pub indices: Vec<u32>,
}
pub type SubPatches = [BezierPatch; 4];
#[allow(dead_code)]
pub fn default_patch_config() -> PatchConfig {
PatchConfig {
resolution: 8,
normal_eps: 1e-4,
flip_normal: false,
}
}
#[allow(dead_code)]
pub fn new_bezier_patch(ctrl: [[f32; 3]; 16]) -> BezierPatch {
BezierPatch { ctrl }
}
fn casteljau_cubic(p: &[[f32; 3]; 4], t: f32) -> [f32; 3] {
let q0 = lerp3(p[0], p[1], t);
let q1 = lerp3(p[1], p[2], t);
let q2 = lerp3(p[2], p[3], t);
let r0 = lerp3(q0, q1, t);
let r1 = lerp3(q1, q2, t);
lerp3(r0, r1, t)
}
type Row4 = [[f32; 3]; 4];
fn row_ctrl_v(patch: &BezierPatch, row: usize) -> Row4 {
[
patch.ctrl[row * 4],
patch.ctrl[row * 4 + 1],
patch.ctrl[row * 4 + 2],
patch.ctrl[row * 4 + 3],
]
}
fn col_ctrl_v(patch: &BezierPatch, col: usize) -> Row4 {
[
patch.ctrl[col],
patch.ctrl[4 + col],
patch.ctrl[8 + col],
patch.ctrl[12 + col],
]
}
#[allow(dead_code)]
pub fn evaluate_patch(patch: &BezierPatch, u: f32, v: f32) -> [f32; 3] {
let row_pts: Row4 = [
casteljau_cubic(&row_ctrl_v(patch, 0), u),
casteljau_cubic(&row_ctrl_v(patch, 1), u),
casteljau_cubic(&row_ctrl_v(patch, 2), u),
casteljau_cubic(&row_ctrl_v(patch, 3), u),
];
casteljau_cubic(&row_pts, v)
}
#[allow(dead_code)]
pub fn patch_tangent_u(patch: &BezierPatch, u: f32, v: f32, cfg: &PatchConfig) -> [f32; 3] {
let eps = cfg.normal_eps;
let u0 = (u - eps).clamp(0.0, 1.0);
let u1 = (u + eps).clamp(0.0, 1.0);
let p0 = evaluate_patch(patch, u0, v);
let p1 = evaluate_patch(patch, u1, v);
normalize3(sub3(p1, p0))
}
#[allow(dead_code)]
pub fn patch_tangent_v(patch: &BezierPatch, u: f32, v: f32, cfg: &PatchConfig) -> [f32; 3] {
let eps = cfg.normal_eps;
let v0 = (v - eps).clamp(0.0, 1.0);
let v1 = (v + eps).clamp(0.0, 1.0);
let p0 = evaluate_patch(patch, u, v0);
let p1 = evaluate_patch(patch, u, v1);
normalize3(sub3(p1, p0))
}
#[allow(dead_code)]
pub fn patch_normal(patch: &BezierPatch, u: f32, v: f32, cfg: &PatchConfig) -> [f32; 3] {
let tu = patch_tangent_u(patch, u, v, cfg);
let tv = patch_tangent_v(patch, u, v, cfg);
let n = normalize3(cross3(tu, tv));
if cfg.flip_normal {
scale3(n, -1.0)
} else {
n
}
}
#[allow(dead_code)]
pub fn patch_bounding_box(patch: &BezierPatch) -> ([f32; 3], [f32; 3]) {
let mut mn = patch.ctrl[0];
let mut mx = patch.ctrl[0];
for &p in &patch.ctrl[1..] {
mn[0] = mn[0].min(p[0]);
mn[1] = mn[1].min(p[1]);
mn[2] = mn[2].min(p[2]);
mx[0] = mx[0].max(p[0]);
mx[1] = mx[1].max(p[1]);
mx[2] = mx[2].max(p[2]);
}
(mn, mx)
}
#[allow(dead_code)]
pub fn patch_midpoint(patch: &BezierPatch) -> [f32; 3] {
let mut sum = [0.0f32; 3];
for p in &patch.ctrl {
sum = add3(sum, *p);
}
scale3(sum, 1.0 / 16.0)
}
#[allow(dead_code)]
pub fn patch_vertex_count(resolution: u32) -> usize {
let n = resolution as usize + 1;
n * n
}
#[allow(dead_code)]
pub fn patch_triangle_count(resolution: u32) -> usize {
let n = resolution as usize;
n * n * 2
}
#[allow(dead_code)]
pub fn tessellate_patch(patch: &BezierPatch, cfg: &PatchConfig) -> PatchTessellation {
let res = cfg.resolution.max(1) as usize;
let n = res + 1;
let mut positions = Vec::with_capacity(n * n);
let mut normals = Vec::with_capacity(n * n);
let mut uvs = Vec::with_capacity(n * n);
for row in 0..n {
let v = row as f32 / res as f32;
for col in 0..n {
let u = col as f32 / res as f32;
positions.push(evaluate_patch(patch, u, v));
normals.push(patch_normal(patch, u, v, cfg));
uvs.push([u, v]);
}
}
let mut indices = Vec::with_capacity(res * res * 6);
for row in 0..res {
for col in 0..res {
let i0 = (row * n + col) as u32;
let i1 = i0 + 1;
let i2 = i0 + n as u32;
let i3 = i2 + 1;
indices.push(i0);
indices.push(i1);
indices.push(i3);
indices.push(i0);
indices.push(i3);
indices.push(i2);
}
}
PatchTessellation {
positions,
normals,
uvs,
indices,
}
}
fn split_cubic_at_half(p: &Row4) -> (Row4, Row4) {
let q0 = lerp3(p[0], p[1], 0.5);
let q1 = lerp3(p[1], p[2], 0.5);
let q2 = lerp3(p[2], p[3], 0.5);
let r0 = lerp3(q0, q1, 0.5);
let r1 = lerp3(q1, q2, 0.5);
let s = lerp3(r0, r1, 0.5);
([p[0], q0, r0, s], [s, r1, q2, p[3]])
}
#[allow(dead_code)]
pub fn subdivide_patch(patch: &BezierPatch) -> SubPatches {
let mut left_rows: [Row4; 4] = [[patch.ctrl[0]; 4]; 4];
let mut right_rows: [Row4; 4] = [[patch.ctrl[0]; 4]; 4];
for r in 0..4usize {
let row = row_ctrl_v(patch, r);
let (l, ri) = split_cubic_at_half(&row);
left_rows[r] = l;
right_rows[r] = ri;
}
let from_rows = |rows: &[Row4; 4]| -> BezierPatch {
let mut ctrl = [[0.0f32; 3]; 16];
for (r, row) in rows.iter().enumerate() {
for (c, &pt) in row.iter().enumerate() {
ctrl[r * 4 + c] = pt;
}
}
BezierPatch { ctrl }
};
let split_cols = |rows: &[Row4; 4]| -> (BezierPatch, BezierPatch) {
let mut bot_rows: [Row4; 4] = [rows[0]; 4];
let mut top_rows: [Row4; 4] = [rows[0]; 4];
for c in 0..4usize {
let col: Row4 = [rows[0][c], rows[1][c], rows[2][c], rows[3][c]];
let (b, t) = split_cubic_at_half(&col);
for r in 0..4 {
bot_rows[r][c] = b[r];
top_rows[r][c] = t[r];
}
}
(from_rows(&bot_rows), from_rows(&top_rows))
};
let (ll, lu) = split_cols(&left_rows);
let (rl, ru) = split_cols(&right_rows);
[ll, rl, lu, ru]
}
#[cfg(test)]
mod tests {
use super::*;
fn flat_patch() -> BezierPatch {
let mut ctrl = [[0.0f32; 3]; 16];
for r in 0..4usize {
for c in 0..4usize {
ctrl[r * 4 + c] = [c as f32 / 3.0, 0.0, r as f32 / 3.0];
}
}
BezierPatch { ctrl }
}
#[test]
fn test_default_patch_config() {
let cfg = default_patch_config();
assert_eq!(cfg.resolution, 8);
assert!(!cfg.flip_normal);
}
#[test]
fn test_new_bezier_patch_corners() {
let mut ctrl = [[0.0f32; 3]; 16];
ctrl[0] = [1.0, 2.0, 3.0];
let p = new_bezier_patch(ctrl);
assert_eq!(p.ctrl[0], [1.0, 2.0, 3.0]);
}
#[test]
fn test_evaluate_patch_corners() {
let p = flat_patch();
let pt = evaluate_patch(&p, 0.0, 0.0);
assert!((pt[0]).abs() < 1e-5, "x should be ~0, got {}", pt[0]);
assert!((pt[1]).abs() < 1e-5);
assert!((pt[2]).abs() < 1e-5);
}
#[test]
fn test_evaluate_patch_far_corner() {
let p = flat_patch();
let pt = evaluate_patch(&p, 1.0, 1.0);
assert!((pt[0] - 1.0).abs() < 1e-5, "x={}", pt[0]);
assert!((pt[2] - 1.0).abs() < 1e-5, "z={}", pt[2]);
}
#[test]
fn test_patch_tangent_u_not_zero() {
let p = flat_patch();
let cfg = default_patch_config();
let tu = patch_tangent_u(&p, 0.5, 0.5, &cfg);
let mag = len3(tu);
assert!(mag > 0.5, "tangent_u should be nonzero");
}
#[test]
fn test_patch_tangent_v_not_zero() {
let p = flat_patch();
let cfg = default_patch_config();
let tv = patch_tangent_v(&p, 0.5, 0.5, &cfg);
let mag = len3(tv);
assert!(mag > 0.5, "tangent_v should be nonzero");
}
#[test]
fn test_patch_normal_approximately_up() {
let p = flat_patch();
let cfg = default_patch_config();
let n = patch_normal(&p, 0.5, 0.5, &cfg);
assert!((len3(n) - 1.0).abs() < 0.02, "normal mag={}", len3(n));
assert!(n[1].abs() > 0.9, "y component={}", n[1]);
}
#[test]
fn test_patch_normal_flip() {
let p = flat_patch();
let cfg_no = default_patch_config();
let mut cfg_flip = default_patch_config();
cfg_flip.flip_normal = true;
let n_no = patch_normal(&p, 0.5, 0.5, &cfg_no);
let n_flip = patch_normal(&p, 0.5, 0.5, &cfg_flip);
assert!((n_no[0] + n_flip[0]).abs() < 1e-5);
assert!((n_no[1] + n_flip[1]).abs() < 1e-5);
assert!((n_no[2] + n_flip[2]).abs() < 1e-5);
}
#[test]
fn test_patch_bounding_box() {
let p = flat_patch();
let (mn, mx) = patch_bounding_box(&p);
assert!((mn[0]).abs() < 1e-5);
assert!((mx[0] - 1.0).abs() < 1e-5);
}
#[test]
fn test_patch_midpoint() {
let p = flat_patch();
let mid = patch_midpoint(&p);
assert!((mid[0] - 0.5).abs() < 0.01, "mid.x={}", mid[0]);
assert!((mid[2] - 0.5).abs() < 0.01, "mid.z={}", mid[2]);
}
#[test]
fn test_patch_vertex_count() {
assert_eq!(patch_vertex_count(8), 81);
assert_eq!(patch_vertex_count(1), 4);
}
#[test]
fn test_patch_triangle_count() {
assert_eq!(patch_triangle_count(8), 128);
assert_eq!(patch_triangle_count(1), 2);
}
#[test]
fn test_tessellate_patch_counts() {
let p = flat_patch();
let cfg = default_patch_config();
let tess = tessellate_patch(&p, &cfg);
let res = cfg.resolution as usize;
let n = res + 1;
assert_eq!(tess.positions.len(), n * n);
assert_eq!(tess.normals.len(), n * n);
assert_eq!(tess.uvs.len(), n * n);
assert_eq!(tess.indices.len(), res * res * 6);
}
#[test]
fn test_tessellate_patch_resolution_1() {
let p = flat_patch();
let mut cfg = default_patch_config();
cfg.resolution = 1;
let tess = tessellate_patch(&p, &cfg);
assert_eq!(tess.positions.len(), 4);
assert_eq!(tess.indices.len(), 6);
}
#[test]
fn test_subdivide_patch_produces_four() {
let p = flat_patch();
let subs = subdivide_patch(&p);
assert_eq!(subs.len(), 4);
}
#[test]
fn test_subdivide_preserves_corners() {
let p = flat_patch();
let subs = subdivide_patch(&p);
let (orig_mn, orig_mx) = patch_bounding_box(&p);
let mut all_mn = subs[0].ctrl[0];
let mut all_mx = subs[0].ctrl[0];
for sub in &subs {
for &pt in &sub.ctrl {
all_mn[0] = all_mn[0].min(pt[0]);
all_mn[1] = all_mn[1].min(pt[1]);
all_mn[2] = all_mn[2].min(pt[2]);
all_mx[0] = all_mx[0].max(pt[0]);
all_mx[1] = all_mx[1].max(pt[1]);
all_mx[2] = all_mx[2].max(pt[2]);
}
}
assert!((all_mn[0] - orig_mn[0]).abs() < 1e-5);
assert!((all_mx[0] - orig_mx[0]).abs() < 1e-5);
}
#[test]
fn test_subdivide_midpoint_continuity() {
let p = flat_patch();
let mid_orig = evaluate_patch(&p, 0.5, 0.5);
let subs = subdivide_patch(&p);
let mid_sub = evaluate_patch(&subs[0], 1.0, 1.0);
assert!((mid_orig[0] - mid_sub[0]).abs() < 1e-4, "x diff");
assert!((mid_orig[1] - mid_sub[1]).abs() < 1e-4, "y diff");
assert!((mid_orig[2] - mid_sub[2]).abs() < 1e-4, "z diff");
}
}