///|
/// Hitable geometry with BVH-accelerated hit testing.
pub(all) enum Hitable {
Sphere(Sphere)
Plane(Plane)
Triangle(Triangle)
BoxShape(BoxShape)
Cylinder(Cylinder)
Disk(Disk)
Cone(Cone)
Torus(Torus)
} derive(Debug)
pub(all) struct HitRecord {
p : Vec3
normal : Vec3
t : Double
front_face : Bool
material : Material
} derive(Debug)
pub fn HitRecord::set_face_normal(self : HitRecord, r : Ray, outward_normal : Vec3) -> HitRecord {
let front_face = r.dir.dot(outward_normal) < 0.0
let normal = if front_face { outward_normal } else { -outward_normal }
{ ..self, front_face, normal }
}
/// Per-object hit test.
pub fn Hitable::hit(self : Hitable, r : Ray, t_min~ : Double, t_max~ : Double) -> HitRecord? {
match self {
Sphere(s) => {
let oc = r.orig - s.center
let a = r.dir.length_squared()
let half_b = oc.dot(r.dir)
let c = oc.length_squared() - s.radius * s.radius
let discriminant = half_b * half_b - a * c
if discriminant < 0.0 { return None }
let sqrtd = discriminant.sqrt()
let mut root = (-half_b - sqrtd) / a
if root < t_min || root > t_max {
root = (-half_b + sqrtd) / a
if root < t_min || root > t_max { return None }
}
let p = r.at(root)
let outward_normal = (p - s.center).div_scalar(s.radius)
let mut rec = { p, normal: outward_normal, t: root, front_face: false, material: s.material }
rec = rec.set_face_normal(r, outward_normal)
Some(rec)
}
Plane(pl) => pl.hit_plane(r, t_min=t_min, t_max=t_max)
Triangle(tri) => tri.hit_triangle(r, t_min=t_min, t_max=t_max)
BoxShape(bx) => bx.hit_box(r, t_min=t_min, t_max=t_max)
Cylinder(cyl) => cyl.hit_cylinder(r, t_min=t_min, t_max=t_max)
Disk(dk) => dk.hit_disk(r, t_min=t_min, t_max=t_max)
Cone(cn) => cn.hit_cone(r, t_min=t_min, t_max=t_max)
Torus(tor) => tor.hit_torus(r, t_min=t_min, t_max=t_max)
}
}
/// World with optional BVH acceleration.
pub(all) struct HitableList {
objects : Array[Hitable]
bvh_ready : Bool
} derive(Debug)
/// Module-level BVH cache: maps objects array identity to BVH root.
/// Since MoonBit doesn't support mutable struct fields, we cache here.
let _bvh_cache : Array[(Int, BVHNode?)] = Array::new(capacity=8)
fn bvh_cache_key(objects : Array[Hitable]) -> Int {
// Use object count + first object's bounding box as a simple key
// This is a heuristic; for correctness, the cache is cleared on build_bvh
objects.length()
}
fn bvh_cache_lookup(objects : Array[Hitable]) -> BVHNode? {
let key = bvh_cache_key(objects)
for i in 0..<_bvh_cache.length() {
if _bvh_cache[i].0 == key {
return _bvh_cache[i].1
}
}
None
}
fn bvh_cache_store(objects : Array[Hitable], root : BVHNode?) -> Unit {
let key = bvh_cache_key(objects)
// Replace existing entry or add new
for i in 0..<_bvh_cache.length() {
if _bvh_cache[i].0 == key {
_bvh_cache[i] = (key, root)
return
}
}
_bvh_cache.push((key, root))
}
pub fn HitableList::new() -> HitableList {
{ objects: Array::new(capacity=16), bvh_ready: false }
}
pub fn HitableList::add(self : HitableList, object : Hitable) -> HitableList {
let objs = self.objects
objs.push(object)
{ objects: objs, bvh_ready: false }
}
/// Build BVH acceleration structure. Must be called before rendering
/// for scenes with more than a handful of objects.
pub fn HitableList::build_bvh(self : HitableList) -> HitableList {
if self.objects.length() <= 1 || self.bvh_ready {
return self
}
let root = bvh_build_from(self.objects)
bvh_cache_store(self.objects, root)
{ ..self, bvh_ready: true }
}
/// BVH-accelerated hit. When BVH is built, uses hierarchical traversal.
/// Otherwise falls back to linear scan.
pub fn HitableList::hit(self : HitableList, r : Ray, t_min~ : Double, t_max~ : Double) -> HitRecord? {
if !(self.bvh_ready) || self.objects.length() < 1 {
return self.hit_linear(r, t_min=t_min, t_max=t_max)
}
let cached = bvh_cache_lookup(self.objects)
match cached {
None => {
// Fallback: rebuild (shouldn't happen if build_bvh was called)
let root = bvh_build_from(self.objects)
match root {
None => self.hit_linear(r, t_min=t_min, t_max=t_max)
Some(node) => bvh_hit(node, self.objects, r, t_min=t_min, t_max=t_max)
}
}
Some(node) => bvh_hit(node, self.objects, r, t_min=t_min, t_max=t_max)
}
}
/// Linear scan hit (fallback).
pub fn HitableList::hit_linear(self : HitableList, r : Ray, t_min~ : Double, t_max~ : Double) -> HitRecord? {
let mut closest = t_max
let mut result : HitRecord? = None
for i in 0.. ()
Some(rec) => { closest = rec.t; result = Some(rec) }
}
}
result
}