trueno/src/ray.jai
2026-03-20 22:06:04 +02:00

320 lines
16 KiB
Plaintext

Ray :: struct {
origin : Vector3;
direction : Vector3;
};
get_mouse_ray :: (cam: *Camera) -> Ray {
sw, sh := get_window_size();
x := (2.0 * input_mouse_x)/sw - 1.0;
y := 1.0 - (2.0 * input_mouse_y)/sh;
z := 1.0;
deviceCoords := Vector3.{x,y,z};
matView := create_lookat(cam);
matProj := create_perspective(cam);
nearPoint := unproject(.{deviceCoords.x, deviceCoords.y, 0.0}, matProj, matView);
farPoint := unproject(.{deviceCoords.x, deviceCoords.y, 1.0}, matProj, matView);
direction := normalize(farPoint - nearPoint);
return .{
cam.position,
direction
};
}
Collision_Cube :: struct {
position : Vector3;
size : Vector3;
};
Ray_Collision :: struct {
distance : float = 99999;
hit : bool = false;
point : Vector3;
normal : Vector3;
}
// At which 2D XZ point does a ray hit the horizontal plane at plane_height.
// Works whether the camera is above or below the plane.
ray_plane_collision_point :: (ray: Ray, plane_height: float, acceptable_radius: float = -1) -> (bool, Vector2) {
if abs(ray.direction.y) < 0.0001 then return false, .{0,0}; // parallel to plane
multi := (plane_height - ray.origin.y) / ray.direction.y;
if multi < 0 then return false, .{0,0}; // plane is behind the camera
planePoint := ray.origin + ray.direction * multi;
if acceptable_radius > 0 && length(Vector2.{planePoint.x, planePoint.z} - Vector2.{ray.origin.x, ray.origin.z}) > acceptable_radius {
return false, .{};
}
return true, .{planePoint.x, planePoint.z};
}
// Ported over from Raylib.
does_ray_hit_cube :: (og_ray: Ray, cube: Collision_Cube) -> Ray_Collision {
ray := og_ray;
collision : Ray_Collision;
bmin := cube.position;
bmax := cube.position + cube.size;
insideBox := (ray.origin.x > bmin.x) && (ray.origin.x < bmax.x) &&
(ray.origin.y > bmin.y) && (ray.origin.y < bmax.y) &&
(ray.origin.z > bmin.z) && (ray.origin.z < bmax.z);
t : [11] float;
t[8] = 1.0/ray.direction.x;
t[9] = 1.0/ray.direction.y;
t[10] = 1.0/ray.direction.z;
t[0] = (bmin.x - ray.origin.x) * t[8];
t[1] = (bmax.x - ray.origin.x) * t[8];
t[2] = (bmin.y - ray.origin.y) * t[9];
t[3] = (bmax.y - ray.origin.y) * t[9];
t[4] = (bmin.z - ray.origin.z) * t[10];
t[5] = (bmax.z - ray.origin.z) * t[10];
t[6] = max(max(min(t[0], t[1]), min(t[2], t[3])), min(t[4], t[5]));
t[7] = min(min(max(t[0], t[1]), max(t[2], t[3])), max(t[4], t[5]));
collision.hit = !((t[7] < 0) || (t[6] > t[7]));
collision.distance = t[6];
collision.point = ray.origin + ray.direction * collision.distance;
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;
collision.distance = -1.0 * collision.distance;
collision.normal = -1.0 * collision.normal;
}
return collision;
}
unproject :: (source: Vector3, projection: Matrix4, view: Matrix4) -> Vector3 {
result : Vector3;
// Calculate unprojected matrix (multiply view matrix by projection matrix) and invert it
matViewProj := Matrix4.{ // MatrixMultiply(view, projection);
view.floats[0]*projection.floats[0 ]+ view.floats[1]*projection.floats[4 ]+ view.floats[2]*projection.floats[8 ]+ view.floats[3]*projection.floats[12],
view.floats[0]*projection.floats[1 ]+ view.floats[1]*projection.floats[5 ]+ view.floats[2]*projection.floats[9 ]+ view.floats[3]*projection.floats[13],
view.floats[0]*projection.floats[2 ]+ view.floats[1]*projection.floats[6 ]+ view.floats[2]*projection.floats[10 ]+ view.floats[3]*projection.floats[14],
view.floats[0]*projection.floats[3 ]+ view.floats[1]*projection.floats[7 ]+ view.floats[2]*projection.floats[11 ]+ view.floats[3]*projection.floats[15],
view.floats[4]*projection.floats[0 ]+ view.floats[5]*projection.floats[4 ]+ view.floats[6]*projection.floats[8 ]+ view.floats[7]*projection.floats[12],
view.floats[4]*projection.floats[1 ]+ view.floats[5]*projection.floats[5 ]+ view.floats[6]*projection.floats[9 ]+ view.floats[7]*projection.floats[13],
view.floats[4]*projection.floats[2 ]+ view.floats[5]*projection.floats[6 ]+ view.floats[6]*projection.floats[10 ]+ view.floats[7]*projection.floats[14],
view.floats[4]*projection.floats[3 ]+ view.floats[5]*projection.floats[7 ]+ view.floats[6]*projection.floats[11 ]+ view.floats[7]*projection.floats[15],
view.floats[8]*projection.floats[0 ]+ view.floats[9]*projection.floats[4 ]+ view.floats[10]*projection.floats[8 ]+ view.floats[11]*projection.floats[12],
view.floats[8]*projection.floats[1 ]+ view.floats[9]*projection.floats[5 ]+ view.floats[10]*projection.floats[9 ]+ view.floats[11]*projection.floats[13],
view.floats[8]*projection.floats[2 ]+ view.floats[9]*projection.floats[6 ]+ view.floats[10]*projection.floats[10 ]+ view.floats[11]*projection.floats[14],
view.floats[8]*projection.floats[3 ]+ view.floats[9]*projection.floats[7 ]+ view.floats[10]*projection.floats[11 ]+ view.floats[11]*projection.floats[15],
view.floats[12]*projection.floats[0 ]+ view.floats[13]*projection.floats[4 ]+ view.floats[14]*projection.floats[8 ]+ view.floats[15]*projection.floats[12],
view.floats[12]*projection.floats[1 ]+ view.floats[13]*projection.floats[5 ]+ view.floats[14]*projection.floats[9 ]+ view.floats[15]*projection.floats[13],
view.floats[12]*projection.floats[2 ]+ view.floats[13]*projection.floats[6 ]+ view.floats[14]*projection.floats[10 ]+ view.floats[15]*projection.floats[14],
view.floats[12]*projection.floats[3 ]+ view.floats[13]*projection.floats[7 ]+ view.floats[14]*projection.floats[11 ]+ view.floats[15]*projection.floats[15 ]};
// Calculate inverted matrix -> MatrixInvert(matViewProj);
// Cache the matrix values (speed optimization)
a00 := matViewProj.floats[0]; a01 := matViewProj.floats[1]; a02 := matViewProj.floats[2]; a03 := matViewProj.floats[3];
a10 := matViewProj.floats[4]; a11 := matViewProj.floats[5]; a12 := matViewProj.floats[6]; a13 := matViewProj.floats[7];
a20 := matViewProj.floats[8]; a21 := matViewProj.floats[9]; a22 := matViewProj.floats[10]; a23 := matViewProj.floats[11];
a30 := matViewProj.floats[12]; a31 := matViewProj.floats[13]; a32 := matViewProj.floats[14]; a33 := matViewProj.floats[15];
b00 := a00*a11 - a01*a10;
b01 := a00*a12 - a02*a10;
b02 := a00*a13 - a03*a10;
b03 := a01*a12 - a02*a11;
b04 := a01*a13 - a03*a11;
b05 := a02*a13 - a03*a12;
b06 := a20*a31 - a21*a30;
b07 := a20*a32 - a22*a30;
b08 := a20*a33 - a23*a30;
b09 := a21*a32 - a22*a31;
b10 := a21*a33 - a23*a31;
b11 := a22*a33 - a23*a32;
// Calculate the invert determinant (inlined to avoid double-caching)
invDet : float = 1.0/(b00*b11 - b01*b10 + b02*b09 + b03*b08 - b04*b07 + b05*b06);
matViewProjInv := Matrix4.{
(a11*b11 - a12*b10 + a13*b09)*invDet,
(-a01*b11 + a02*b10 - a03*b09)*invDet,
(a31*b05 - a32*b04 + a33*b03)*invDet,
(-a21*b05 + a22*b04 - a23*b03)*invDet,
(-a10*b11 + a12*b08 - a13*b07)*invDet,
(a00*b11 - a02*b08 + a03*b07)*invDet,
(-a30*b05 + a32*b02 - a33*b01)*invDet,
(a20*b05 - a22*b02 + a23*b01)*invDet,
(a10*b10 - a11*b08 + a13*b06)*invDet,
(-a00*b10 + a01*b08 - a03*b06)*invDet,
(a30*b04 - a31*b02 + a33*b00)*invDet,
(-a20*b04 + a21*b02 - a23*b00)*invDet,
(-a10*b09 + a11*b07 - a12*b06)*invDet,
(a00*b09 - a01*b07 + a02*b06)*invDet,
(-a30*b03 + a31*b01 - a32*b00)*invDet,
(a20*b03 - a21*b01 + a22*b00)*invDet };
// Create quaternion from source point
quat: Quaternion = .{ source.x, source.y, source.z, 1.0 };
// Multiply quat point by unprojecte matrix
qtransformed : Quaternion = .{ // QuaternionTransform(quat, matViewProjInv)
matViewProjInv.floats[0]*quat.x + matViewProjInv.floats[4]*quat.y + matViewProjInv.floats[8]*quat.z + matViewProjInv.floats[12]*quat.w,
matViewProjInv.floats[1]*quat.x + matViewProjInv.floats[5]*quat.y + matViewProjInv.floats[9]*quat.z + matViewProjInv.floats[13]*quat.w,
matViewProjInv.floats[2]*quat.x + matViewProjInv.floats[6]*quat.y + matViewProjInv.floats[10]*quat.z + matViewProjInv.floats[14]*quat.w,
matViewProjInv.floats[3]*quat.x + matViewProjInv.floats[7]*quat.y + matViewProjInv.floats[11]*quat.z + matViewProjInv.floats[15]*quat.w };
// Normalized world points in vectors
result.x = qtransformed.x/qtransformed.w;
result.y = qtransformed.y/qtransformed.w;
result.z = qtransformed.z/qtransformed.w;
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();
}
}