diff --git a/src/ray.jai b/src/ray.jai index cf1144a..b713308 100644 --- a/src/ray.jai +++ b/src/ray.jai @@ -83,13 +83,16 @@ does_ray_hit_cube :: (og_ray: Ray, cube: Collision_Cube) -> Ray_Collision { collision.distance = t[6]; collision.point = ray.origin + ray.direction * collision.distance; - collision.normal = lerp(bmin, bmax, 0.5); - collision.normal = collision.point - collision.normal; - - collision.normal.x = cast(float) cast(s64) collision.normal.x; - collision.normal.y = cast(float) cast(s64) collision.normal.y; - collision.normal.z = cast(float) cast(s64) collision.normal.z; - collision.normal = normalize(collision.normal); + tx := min(t[0], t[1]); + ty := min(t[2], t[3]); + tz := min(t[4], t[5]); + if tx >= ty && tx >= tz { + collision.normal = .{ifx t[0] < t[1] then -1.0 else 1.0, 0, 0}; + } else if ty >= tx && ty >= tz { + collision.normal = .{0, ifx t[2] < t[3] then -1.0 else 1.0, 0}; + } else { + collision.normal = .{0, 0, ifx t[4] < t[5] then -1.0 else 1.0}; + } if insideBox { ray.direction = -1.0 * ray.direction; @@ -180,3 +183,137 @@ unproject :: (source: Vector3, projection: Matrix4, view: Matrix4) -> Vector3 { return result; } + +#if FLAG_TEST_ENGINE { + eps :: 0.001; + approx :: (a: float, b: float) -> bool { return abs(a - b) < eps; } + approx3 :: (a: Vector3, b: Vector3) -> bool { + return abs(a.x - b.x) < eps && abs(a.y - b.y) < eps && abs(a.z - b.z) < eps; + } + + test_ray_cube_hit :: () { + s := begin_suite("ray cube hit"); + ray := Ray.{ origin = .{0, 0, -5}, direction = .{0, 0, 1} }; + cube := Collision_Cube.{ position = .{-1, -1, -1}, size = .{2, 2, 2} }; + col := does_ray_hit_cube(ray, cube); + check(*s, "ray along +Z hits cube", col.hit); + check(*s, "distance to front face is 4.0", approx(col.distance, 4.0)); + check(*s, "hit point lands on front face", approx3(col.point, .{0, 0, -1})); + end_suite(s); + } + + test_ray_cube_miss :: () { + s := begin_suite("ray cube miss"); + { + ray := Ray.{ origin = .{3, 0, -5}, direction = .{0, 0, 1} }; + cube := Collision_Cube.{ position = .{-1, -1, -1}, size = .{2, 2, 2} }; + col := does_ray_hit_cube(ray, cube); + check(*s, "ray offset in X misses cube", !col.hit); + } + { + ray := Ray.{ origin = .{0, 0, 5}, direction = .{0, 0, 1} }; + cube := Collision_Cube.{ position = .{-1, -1, -1}, size = .{2, 2, 2} }; + col := does_ray_hit_cube(ray, cube); + check(*s, "ray pointing away misses cube", !col.hit); + } + end_suite(s); + } + + // Face normals using size-2 cubes centered at origin. + // NOTE: the current integer-truncation normal computation breaks for cubes + // smaller than size 2 (e.g. trixel-sized cubes). Fix does_ray_hit_cube to + // use the entry-face t-value comparison instead of truncating to s64. + test_ray_cube_normals :: () { + s := begin_suite("ray cube face normals"); + cube := Collision_Cube.{ position = .{-1, -1, -1}, size = .{2, 2, 2} }; + + col_neg_z := does_ray_hit_cube(Ray.{ origin = .{0, 0, -5}, direction = .{0, 0, 1} }, cube); + col_pos_z := does_ray_hit_cube(Ray.{ origin = .{0, 0, 5}, direction = .{0, 0, -1} }, cube); + col_pos_y := does_ray_hit_cube(Ray.{ origin = .{0, 5, 0}, direction = .{0, -1, 0} }, cube); + col_neg_y := does_ray_hit_cube(Ray.{ origin = .{0,-5, 0}, direction = .{0, 1, 0} }, cube); + col_pos_x := does_ray_hit_cube(Ray.{ origin = .{ 5, 0, 0}, direction = .{-1, 0, 0} }, cube); + col_neg_x := does_ray_hit_cube(Ray.{ origin = .{-5, 0, 0}, direction = .{ 1, 0, 0} }, cube); + + check(*s, "-Z face normal is ( 0, 0,-1)", col_neg_z.hit && approx3(col_neg_z.normal, .{ 0, 0, -1})); + check(*s, "+Z face normal is ( 0, 0, 1)", col_pos_z.hit && approx3(col_pos_z.normal, .{ 0, 0, 1})); + check(*s, "+Y face normal is ( 0, 1, 0)", col_pos_y.hit && approx3(col_pos_y.normal, .{ 0, 1, 0})); + check(*s, "-Y face normal is ( 0,-1, 0)", col_neg_y.hit && approx3(col_neg_y.normal, .{ 0, -1, 0})); + check(*s, "+X face normal is ( 1, 0, 0)", col_pos_x.hit && approx3(col_pos_x.normal, .{ 1, 0, 0})); + check(*s, "-X face normal is (-1, 0, 0)", col_neg_x.hit && approx3(col_neg_x.normal, .{-1, 0, 0})); + end_suite(s); + } + + // ray_plane_collision_point returns Vector2.{world_x, world_z} + test_ray_plane :: () { + s := begin_suite("ray plane collision"); + { + ray := Ray.{ origin = .{3, 5, 2}, direction = .{0, -1, 0} }; + hit, point := ray_plane_collision_point(ray, 0.0); + check(*s, "downward ray hits horizontal plane", hit); + check(*s, "hit world-X matches ray origin X", approx(point.x, 3.0)); + check(*s, "hit world-Z matches ray origin Z", approx(point.y, 2.0)); + } + { + ray := Ray.{ origin = .{0, 0, 0}, direction = .{0, 1, 0} }; + hit, point := ray_plane_collision_point(ray, 10.0); + check(*s, "upward ray hits plane above", hit); + check(*s, "hit world-X is 0", approx(point.x, 0.0)); + check(*s, "hit world-Z is 0", approx(point.y, 0.0)); + } + { + ray := Ray.{ origin = .{0, 5, 0}, direction = .{1, 0, 0} }; + hit, _ := ray_plane_collision_point(ray, 0.0); + check(*s, "ray parallel to plane misses", !hit); + } + { + ray := Ray.{ origin = .{0, 5, 0}, direction = .{0, 1, 0} }; + hit, _ := ray_plane_collision_point(ray, 0.0); + check(*s, "ray pointing away misses", !hit); + } + // acceptable_radius is XZ distance from ray origin to hit point — needs a diagonal ray + { + ray := Ray.{ origin = .{0, 5, 0}, direction = normalize(Vector3.{0.1, -1, 0}) }; + hit, _ := ray_plane_collision_point(ray, 0.0, acceptable_radius = 10.0); + check(*s, "hit within acceptable_radius succeeds", hit); + } + { + ray := Ray.{ origin = .{0, 1, 0}, direction = normalize(Vector3.{100, -1, 0}) }; + hit, _ := ray_plane_collision_point(ray, 0.0, acceptable_radius = 1.0); + check(*s, "hit outside acceptable_radius fails", !hit); + } + end_suite(s); + } + + test_ray_trixel_normals :: () { + s := begin_suite("ray trixel-sized cube face normals"); + TS : float : 1.0/16.0; // TRIXEL_SIZE + // Trixel at grid position (4,7,3): position = (4*TS, 7*TS, 3*TS) + cube := Collision_Cube.{ position = .{4*TS, 7*TS, 3*TS}, size = .{TS, TS, TS} }; + cx := 4*TS + TS*0.5; + cy := 7*TS + TS*0.5; + cz := 3*TS + TS*0.5; + + col_neg_z := does_ray_hit_cube(Ray.{ origin = .{cx, cy, cz - 1}, direction = .{0, 0, 1} }, cube); + col_pos_z := does_ray_hit_cube(Ray.{ origin = .{cx, cy, cz + 1}, direction = .{0, 0, -1} }, cube); + col_pos_y := does_ray_hit_cube(Ray.{ origin = .{cx, cy + 1, cz}, direction = .{0, -1, 0} }, cube); + col_neg_y := does_ray_hit_cube(Ray.{ origin = .{cx, cy - 1, cz}, direction = .{0, 1, 0} }, cube); + col_pos_x := does_ray_hit_cube(Ray.{ origin = .{cx + 1, cy, cz}, direction = .{-1, 0, 0} }, cube); + col_neg_x := does_ray_hit_cube(Ray.{ origin = .{cx - 1, cy, cz}, direction = .{ 1, 0, 0} }, cube); + + check(*s, "-Z face normal is ( 0, 0,-1)", col_neg_z.hit && approx3(col_neg_z.normal, .{ 0, 0, -1})); + check(*s, "+Z face normal is ( 0, 0, 1)", col_pos_z.hit && approx3(col_pos_z.normal, .{ 0, 0, 1})); + check(*s, "+Y face normal is ( 0, 1, 0)", col_pos_y.hit && approx3(col_pos_y.normal, .{ 0, 1, 0})); + check(*s, "-Y face normal is ( 0,-1, 0)", col_neg_y.hit && approx3(col_neg_y.normal, .{ 0, -1, 0})); + check(*s, "+X face normal is ( 1, 0, 0)", col_pos_x.hit && approx3(col_pos_x.normal, .{ 1, 0, 0})); + check(*s, "-X face normal is (-1, 0, 0)", col_neg_x.hit && approx3(col_neg_x.normal, .{-1, 0, 0})); + end_suite(s); + } + + #run { + test_ray_cube_hit(); + test_ray_cube_miss(); + test_ray_cube_normals(); + test_ray_trixel_normals(); + test_ray_plane(); + } +} diff --git a/src/tests/index.jai b/src/tests/index.jai index ec09463..8d79d38 100644 --- a/src/tests/index.jai +++ b/src/tests/index.jai @@ -1,4 +1 @@ -#run { - // print("1st test!!!\n"); - // main(); -} +#load "utils.jai"; diff --git a/src/tests/utils.jai b/src/tests/utils.jai new file mode 100644 index 0000000..666ee5b --- /dev/null +++ b/src/tests/utils.jai @@ -0,0 +1,30 @@ +Test_Suite :: struct { + name : string; + passed : int; + failed : int; +} + +begin_suite :: (name: string) -> Test_Suite { + print("\n== % ==\n", name); + return .{ name = name }; +} + +check :: (suite: *Test_Suite, name: string, condition: bool) { + if condition { + suite.passed += 1; + print(" [PASS] %\n", name); + } else { + suite.failed += 1; + print(" [FAIL] %\n", name); + } +} + +end_suite :: (suite: Test_Suite) { + total := suite.passed + suite.failed; + if suite.failed == 0 { + print(" => All % tests passed.\n", total); + } else { + print(" => %/% passed, % FAILED.\n", suite.passed, total, suite.failed); + assert(false, "Test suite '%' had % failure(s).", suite.name, suite.failed); + } +}