From a57673dcaf3d2076caf23923cd829046ef14508d Mon Sep 17 00:00:00 2001 From: karai17 Date: Wed, 20 Jul 2016 20:49:28 -0300 Subject: [PATCH] Added frustum and other intersect functions --- modules/intersect.lua | 501 +++++++++++++++++++++++++++++++++--------- modules/mat4.lua | 94 ++++++++ 2 files changed, 491 insertions(+), 104 deletions(-) diff --git a/modules/intersect.lua b/modules/intersect.lua index 117e0e4..1fbab22 100644 --- a/modules/intersect.lua +++ b/modules/intersect.lua @@ -1,24 +1,159 @@ --- Various geometric intersections -- @module intersect -local current_folder = (...):gsub('%.[^%.]+$', '') .. "." +local modules = (...):gsub('%.[^%.]+$', '') .. "." +local constants = require(modules .. "constants") +local vec3 = require(modules .. "vec3") +local mat4 = require(modules .. "mat4") +local DBL_EPSILON = constants.DBL_EPSILON +local sqrt = math.sqrt +local abs = math.abs +local min = math.min +local max = math.max +local intersect = {} -local constants = require(current_folder .. "constants") -local vec3 = require(current_folder .. "vec3") +-- http://www.peroxide.dk/papers/collision/collision.pdf +-- point is a vec3 +-- triangle[1] is a vec3 +-- triangle[2] is a vec3 +-- triangle[3] is a vec3 +function intersect.point_triangle(point, triangle) + local t21 = triangle[2] - triangle[1] + local t31 = triangle[3] - triangle[1] -local abs, min, max = math.abs, math.min, math.max -local FLT_EPSILON = constants.FLT_EPSILON + local a = t21:dot(t21) + local b = t21:dot(t31) + local c = t31:dot(t31) + local ac_bb = a * c - b * b -local intersect = {} + local v = vec3( + point.x - triangle[1].x, + point.y - triangle[1].y, + point.z - triangle[1].z + ) + local d = v:dot(t21) + local e = v:dot(t31) --- ray.position is a vec3 --- ray.direction is a vec3 + local x = d * c - e * b + local y = e * a - d * b + local z = x + y - ac_bb + + return + x >= 0 and + y >= 0 and + z < 0 +end + +-- point is a vec3 -- aabb.min is a vec3 -- aabb.max is a vec3 +function intersect.point_aabb(point, aabb) + return + aabb.min.x <= point.x and + aabb.max.x >= point.x and + aabb.min.y <= point.y and + aabb.max.y >= point.y and + aabb.min.z <= point.z and + aabb.max.z >= point.z +end + +-- http://www.lighthouse3d.com/tutorials/maths/ray-triangle-intersection/ +-- ray.position is a vec3 +-- ray.direction is a vec3 +-- triangle[1] is a vec3 +-- triangle[2] is a vec3 +-- triangle[3] is a vec3 +local h, s, q, e1, e2 = vec3(), vec3(), vec3(), vec3(), vec3() +function intersect.ray_triangle(ray, triangle) + e1:sub(triangle[2], triangle[1]) + e2:sub(triangle[3], triangle[1]) + h:cross(ray.direction, e2) + + local a = h:dot(e1) + + -- if a is too close to 0, ray does not intersect triangle + if abs(a) <= DBL_EPSILON then + return false + end + + local f = 1 / a + s:sub(ray.position, triangle[1]) + local u = s:dot(h) * f + + -- ray does not intersect triangle + if u < 0 or u > 1 then + return false + end + + q:cross(s, e1) + local v = ray.direction:dot(q) * f + + -- ray does not intersect triangle + if v < 0 or u + v > 1 then + return false + end + + -- at this stage we can compute t to find out where + -- the intersection point is on the line + local t = q:dot(e2) * f + + -- return position of intersection + if t >= DBL_EPSILON then + local out = vec3() + :mul(ray.direction, t) + :add(ray.position, out) + + return out + end + + -- ray does not intersect triangle + return false +end + +-- https://gamedev.stackexchange.com/questions/96459/fast-ray-sphere-collision-code +-- ray.position is a vec3 +-- ray.direction is a vec3 +-- sphere.position is a vec3 +-- sphere.radius is a number +function intersect.ray_sphere(ray, sphere) + local offset = ray.position - sphere.position + local b = offset:dot(ray.direction) + local c = offset:dot(offset) - sphere.radius * sphere.radius + + -- ray's position outside sphere (c > 0) + -- ray's direction pointing away from sphere (b > 0) + if c > 0 and b > 0 then + return false + end + + local discr = b * b - c + + -- negative discriminant + if discr < 0 then + return false + end + + local t = -b - sqrt(discr) + + -- Clamp t to 0 + t = t < 0 and 0 or t + + local out = vec3() + :add(ray.position, ray.direction) + :mul(out, t) + + -- Return collision point and distance from ray origin + return out, t +end + +-- ray.position is a vec3 +-- ray.direction is a vec3 +-- aabb.min is a vec3 +-- aabb.max is a vec3 local dir, dirfrac = vec3(), vec3() function intersect.ray_aabb(ray, aabb) - vec3.normalize(dir, ray.direction) + dir:normalize(ray.direction) dirfrac.x = 1 / dir.x dirfrac.y = 1 / dir.y dirfrac.z = 1 / dir.z @@ -47,14 +182,14 @@ function intersect.ray_aabb(ray, aabb) return tmin end --- ray.position is a vec3 --- ray.direction is a vec3 --- plane.position is a vec3 --- plane.normal is a vec3 -- https://www.cs.princeton.edu/courses/archive/fall00/cs426/lectures/raycast/sld017.htm +-- ray.position is a vec3 +-- ray.direction is a vec3 +-- plane.position is a vec3 +-- plane.normal is a vec3 function intersect.ray_plane(ray, plane) - local d = vec3.dist(ray.position, plane.position) - local r = vec3.dot(ray.direction, plane.normal) + local d = ray.position:dist(plane.position) + local r = ray.direction:dot(plane.normal) -- ray does not intersect plane if r <= 0 then @@ -62,13 +197,13 @@ function intersect.ray_plane(ray, plane) end -- distance of direction - local t = -(vec3.dot(ray.position, plane.normal) + d) / r + local t = -(ray.position:dot(plane.normal) + d) / r local out = vec3() - vec3.mul(out, ray.direction, t) - vec3.add(out, ray.position, out) + :mul(ray.direction, t) + :add(ray.position, out) -- return position of intersection - if vec3.dot(out, plane.normal) + d < FLT_EPSILON then + if out:dot(plane.normal) + d < DBL_EPSILON then return out end @@ -76,87 +211,35 @@ function intersect.ray_plane(ray, plane) return false end --- ray.position is a vec3 --- ray.direction is a vec3 --- triangle[1] is a vec3 --- triangle[2] is a vec3 --- triangle[3] is a vec3 --- http://www.lighthouse3d.com/tutorials/maths/ray-triangle-intersection/ -local h, s, q, e1, e2 = vec3(), vec3(), vec3(), vec3(), vec3() -function intersect.ray_triangle(ray, triangle) - vec3.sub(e1, triangle[2], triangle[1]) - vec3.sub(e2, triangle[3], triangle[1]) - - vec3.cross(h, ray.direction, e2) - local a = vec3.dot(h, e1) - - -- if a is too close to 0, ray does not intersect triangle - if abs(a) <= FLT_EPSILON then - return false - end - - local f = 1 / a - vec3.sub(s, ray.position, triangle[1]) - local u = vec3.dot(s, h) * f - - -- ray does not intersect triangle - if u < 0 or u > 1 then - return false - end - - vec3.cross(q, s, e1) - local v = vec3.dot(ray.direction, q) * f - - -- ray does not intersect triangle - if v < 0 or u + v > 1 then - return false - end - - -- at this stage we can compute t to find out where - -- the intersection point is on the line - local t = vec3.dot(q, e2) * f - - -- return position of intersection - if t >= FLT_EPSILON then - local out = vec3() - vec3.mul(out, ray.direction, t) - vec3.add(out, ray.position, out) - return out - end - - -- ray does not intersect triangle - return false -end - +-- Algorithm is ported from the C algorithm of +-- Paul Bourke at http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline3d/ +-- Archive.org am hero \o/ -- a[1] is a vec3 -- a[2] is a vec3 -- b[1] is a vec3 -- b[2] is a vec3 --- Algorithm is ported from the C algorithm of --- Paul Bourke at http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline3d/ --- Archive.org am hero \o/ local p13, p43, p21, out1, out2 = vec3(), vec3(), vec3(), vec3(), vec3() function intersect.line_line(a, b) -- new points - vec3.sub(p13, a[1], b[1]) - vec3.sub(p43, b[2], b[1]) - vec3.sub(p21, a[2], a[1]) + p13:sub(a[1], b[1]) + p43:sub(b[2], b[1]) + p21:sub(a[2], a[1]) -- if lengths are negative or too close to 0, lines do not intersect - if vec3.len2(p43) < FLT_EPSILON or vec3.len2(p21) < FLT_EPSILON then + if p43:len2() < DBL_EPSILON or p21:len2() < DBL_EPSILON then return false end -- dot products - local d1343 = vec3.dot(p13, p43) - local d4321 = vec3.dot(p43, p21) - local d1321 = vec3.dot(p13, p21) - local d4343 = vec3.dot(p43, p43) - local d2121 = vec3.dot(p21, p21) + local d1343 = p13:dot(p43) + local d4321 = p43:dot(p21) + local d1321 = p13:dot(p21) + local d4343 = p43:dot(p43) + local d2121 = p21:dot(p21) local denom = d2121 * d4343 - d4321 * d4321 -- if denom is too close to 0, lines do not intersect - if abs(denom) < FLT_EPSILON then + if abs(denom) < DBL_EPSILON then return false end @@ -165,10 +248,10 @@ function intersect.line_line(a, b) local mub = (d1343 + d4321 * (mua)) / d4343 -- return positions of intersection on each line - vec3.mul(out1, mua, p21) - vec3.add(out1, a[1], out) - vec3.mul(out2, mub, p43) - vec3.add(out2, b[1], out2) + out1:mul(mua, p21) + out1:add(a[1], out1) + out2:mul(mub, p43) + out2:add(b[1], out2) return out1, out2 end @@ -192,19 +275,6 @@ function intersect.segment_segment(a, b) return false end --- point is a vec3 --- aabb.min is a vec3 --- aabb.max is a vec3 -function intersect.point_aabb(point, aabb) - return - aabb.min.x <= point.x and - aabb.max.x >= point.x and - aabb.min.y <= point.y and - aabb.max.y >= point.y and - aabb.min.z <= point.z and - aabb.max.z >= point.z -end - -- a.min is a vec3 -- a.max is a vec3 -- b.min is a vec3 @@ -219,6 +289,184 @@ function intersect.aabb_aabb(a, b) a.max.z >= b.min.z end +-- aabb.position is a vec3 +-- aabb.extent is a vec3 (half-size) +-- obb.position is a vec3 +-- obb.extent is a vec3 (half-size) +-- obb.rotation is a mat4 +function intersect.aabb_obb(aabb, obb) + local a = aabb.extent + local b = obb.extent + local T = obb.position - aabb.position + local rot = mat4():transpose(obb.rotation) + local reps = 1e-6 + local B = {} + + for i = 1, 3 do + B[i] = {} + for j = 1, 3 do + assert((i - 1) * 4 + j < 16 and (i - 1) * 4 + j > 0) + B[i][j] = abs(rot[(i - 1) * 4 + j]) + reps + end + end + + local t, s + local r = 1 + + t = abs(T.x) + if not (t <= (b.x + a.x * B[1][1] + b.y * B[1][2] + b.z * B[1][3])) then return false end + + s = T.x * B[1][1] + T.y*B[2][1] + T.z*B[3][1] + t = abs(s) + if not (t <= (b.x + a.x * B[1][1] + a.y * B[2][1] + a.z * B[3][1])) then return false end + + t = abs(T.y) + if not (t <= (a.y + b.x * B[2][1] + b.y * B[2][2] + b.z * B[2][3])) then return false end + + t = abs(T.z) + if not (t <= (a.z + b.x * B[3][1] + b.y * B[3][2] + b.z * B[3][3])) then return false end + + s = T.x * B[1][2] + T.y * B[2][2] + T.z * B[3][2] + t = abs(s) + if not (t <= (b.y + a.x * B[1][2] + a.y * B[2][2] + a.z * B[3][2])) then return false end + + s = T.x * B[1][3] + T.y * B[2][3] + T.z * B[3][3] + t = abs(s) + if not (t <= (b.z + a.x * B[1][3] + a.y * B[2][3] + a.z * B[3][3])) then return false end + + s = T.z * B[2][1] - T.y * B[3][1] + t = abs(s) + if not (t <= (a.y * B[3][1] + a.z * B[2][1] + b.y * B[1][3] + b.z * B[1][2])) then return false end + + s = T.z * B[2][2] - T.y * B[3][2] + t = abs(s) + if not (t <= (a.y * B[3][2] + a.z * B[2][2] + b.x * B[1][3] + b.z * B[1][1])) then return false end + + s = T.z * B[2][3] - T.y * B[3][3] + t = abs(s) + if not (t <= (a.y * B[3][3] + a.z * B[2][3] + b.x * B[1][2] + b.y * B[1][1])) then return false end + + s = T.x * B[3][1] - T.z * B[1][1] + t = abs(s) + if not (t <= (a.x * B[3][1] + a.z * B[1][1] + b.y * B[2][3] + b.z * B[2][2])) then return false end + + s = T.x * B[3][2] - T.z * B[1][2] + t = abs(s) + if not (t <= (a.x * B[3][2] + a.z * B[1][2] + b.x * B[2][3] + b.z * B[2][1])) then return false end + + s = T.x * B[3][3] - T.z * B[1][3] + t = abs(s) + if not (t <= (a.x * B[3][3] + a.z * B[1][3] + b.x * B[2][2] + b.y * B[2][1])) then return false end + + s = T.y * B[1][1] - T.x * B[2][1] + t = abs(s) + if not (t <= (a.x * B[2][1] + a.y * B[1][1] + b.y * B[3][3] + b.z * B[3][2])) then return false end + + s = T.y * B[1][2] - T.x * B[2][2] + t = abs(s) + if not (t <= (a.x * B[2][2] + a.y * B[1][2] + b.x * B[3][3] + b.z * B[3][1])) then return false end + + s = T.y * B[1][3] - T.x * B[2][3] + t = abs(s) + if not (t <= (a.x * B[2][3] + a.y * B[1][3] + b.x * B[3][2] + b.y * B[3][1])) then return false end + + -- https://gamedev.stackexchange.com/questions/24078/which-side-was-hit + -- Minkowski Sum + local wy = (aabb.extent * 2 + obb.extent * 2) * (aabb.position.y - obb.position.y) + local hx = (aabb.extent * 2 + obb.extent * 2) * (aabb.position.x - obb.position.x) + + if wy > hx then + if wy > -hx then + return vec3(mat4.mul_mat4x1(obb.rotation, { 0, -1, 0, 1 })) + else + return vec3(mat4.mul_mat4x1(obb.rotation, { -1, 0, 0, 1 })) + end + else + if wy > -hx then + return vec3(mat4.mul_mat4x1(obb.rotation, { 1, 0, 0, 1 })) + else + return vec3(mat4.mul_mat4x1(obb.rotation, { 0, 1, 0, 1 })) + end + end +end + +-- aabb.min is a vec3 +-- aabb.max is a vec3 +-- sphere.position is a vec3 +-- sphere.radius is a number +local axes = { "x", "y", "z" } +function intersect.aabb_sphere(aabb, sphere) -- { position, radius } + local dmin = 0 + + for _, axis in ipairs(axes) do + local pos = sphere.position[axis] + local min = box.min[axis] + local max = box.max[axis] + + if pos < min then + dmin = dmin + (pos - min) ^ 2 + elseif pos > max then + dmin = dmin + (pos - max) ^ 2 + end + end + + return dmin <= radius ^ 2 +end + +-- aabb.min is a vec3 +-- aabb.max is a vec3 +-- frustum.top is a plane { a, b, c, d } +-- frustum.bottom is a plane { a, b, c, d } +-- frustum.left is a plane { a, b, c, d } +-- frustum.right is a plane { a, b, c, d } +-- frustum.near is a plane { a, b, c, d } +-- frustum.far is a plane { a, b, c, d } +function intersect.aabb_frustum(aabb, frustum) + -- Indexed for the 'index trick' later + local box = { + aabb.min, + aabb.max + } + + -- We have 6 planes defining the frustum, 5 if infinite. + local planes = { + frustum.top, + frustum.bottom, + frustum.left, + frustum.right, + frustum.near, + frustum.far or false + } + + -- Skip the last test for infinite projections, it'll never fail. + if not planes[6] then + table.remove(planes) + end + + for i = 1, #planes do + -- This is the current plane + local p = planes[i] + + -- p-vertex selection (with the index trick) + -- According to the plane normal we can know the + -- indices of the positive vertex + local px = p.a > 0.0 and 2 or 1 + local py = p.b > 0.0 and 2 or 1 + local pz = (.c > 0.0 and 2 or 1 + + -- project p-vertex on plane normal + -- (How far is p-vertex from the origin) + local dot = (p.a * box[px].x) + (p.b * box[py].y) + (p.c * box[pz].z) + + -- Doesn't intersect if it is behind the plane + if dot < -p.d then + return false + end + end + + return true +end + -- outer.min is a vec3 -- outer.max is a vec3 -- inner.min is a vec3 @@ -230,11 +478,56 @@ function intersect.encapsulate_aabb(outer, inner) end -- a.position is a vec3 --- a.radius is a number +-- a.radius is a number -- b.position is a vec3 --- b.radius is a number +-- b.radius is a number function intersect.circle_circle(a, b) - return vec3.dist(a.position, b.position) <= a.radius + b.radius + return a.position:dist(b.position) <= a.radius + b.radius +end + +-- a.position is a vec3 +-- a.radius is a number +-- b.position is a vec3 +-- b.radius is a number +function intersect.sphere_sphere(a, b) + return intersect.circle_circle(a, b) +end + +-- sphere.position is a vec3 +-- sphere.radius is a number +-- frustum.top is a plane { a, b, c, d } +-- frustum.bottom is a plane { a, b, c, d } +-- frustum.left is a plane { a, b, c, d } +-- frustum.right is a plane { a, b, c, d } +-- frustum.near is a plane { a, b, c, d } +-- frustum.far is a plane { a, b, c, d } +function intersect.sphere_frustum(sphere, frustum) + local x, y, z = sphere.position:unpack() + + local planes = { + frustum.top, + frustum.bottom, + frustum.left, + frustum.right, + frustum.near + } + + if frustum.far then + table.insert(planes, frustum.far, 5) + end + + local dot + for p = 1, #planes do + dot = planes[p].a * x + planes[p].b * y + planes[p].c * z + planes[p].d + + if dot <= -sphere.radius then + return false + end + end + + -- dot + radius is the distance of the object from the near plane. + -- make sure that the near plane is the last test! + return dot + radius end return intersect diff --git a/modules/mat4.lua b/modules/mat4.lua index ec0ba3a..d47e76c 100644 --- a/modules/mat4.lua +++ b/modules/mat4.lua @@ -529,6 +529,100 @@ function mat4.to_quat(a) return q:normalize(q) end +-- frustum = (proj * view):to_frustum(infinite) +-- http://www.crownandcutlass.com/features/technicaldetails/frustum.html +function mat4.to_frustum(a, infinite) + local t + local frustum = {} + + -- Extract the TOP plane + frustum.top = {} + frustum.top.a = a[ 4] - a[ 2] + frustum.top.b = a[ 8] - a[ 6] + frustum.top.c = a[12] - a[10] + frustum.top.d = a[16] - a[14] + + -- Normalize the result + t = sqrt(frustum.top.a * frustum.top.a + frustum.top.b * frustum.top.b + frustum.top.c * frustum.top.c) + frustum.top.a = frustum.top.a / t + frustum.top.b = frustum.top.b / t + frustum.top.c = frustum.top.c / t + frustum.top.d = frustum.top.d / t + + -- Extract the BOTTOM plane + frustum.bottom = {} + frustum.bottom.a = a[ 4] + a[ 2] + frustum.bottom.b = a[ 8] + a[ 6] + frustum.bottom.c = a[12] + a[10] + frustum.bottom.d = a[16] + a[14] + + -- Normalize the result + t = sqrt(frustum.bottom.a * frustum.bottom.a + frustum.bottom.b * frustum.bottom.b + frustum.bottom.c * frustum.bottom.c) + frustum.bottom.a = frustum.bottom.a / t + frustum.bottom.b = frustum.bottom.b / t + frustum.bottom.c = frustum.bottom.c / t + frustum.bottom.d = frustum.bottom.d / t + + -- Extract the LEFT plane + frustum.left.a = a[ 4] + a[ 1] + frustum.left.b = a[ 8] + a[ 5] + frustum.left.c = a[12] + a[ 9] + frustum.left.d = a[16] + a[13] + + -- Normalize the result + t = sqrt(frustum.left.a * frustum.left.a + frustum.left.b * frustum.left.b + frustum.left.c * frustum.left.c) + frustum.left.a = frustum.left.a / t + frustum.left.b = frustum.left.b / t + frustum.left.c = frustum.left.c / t + frustum.left.d = frustum.left.d / t + + -- Extract the RIGHT plane + frustum.right = {} + frustum.right.a = a[ 4] - a[ 1] + frustum.right.b = a[ 8] - a[ 5] + frustum.right.c = a[12] - a[ 9] + frustum.right.d = a[16] - a[13] + + -- Normalize the result + t = sqrt(frustum.right.a * frustum.right.a + frustum.right.b * frustum.right.b + frustum.right.c * frustum.right.c) + frustum.right.a = frustum.right.a / t + frustum.right.b = frustum.right.b / t + frustum.right.c = frustum.right.c / t + frustum.right.d = frustum.right.d / t + + -- Extract the NEAR plane + frustum.near = {} + frustum.near.a = a[ 4] + a[ 3] + frustum.near.b = a[ 8] + a[ 7] + frustum.near.c = a[12] + a[11] + frustum.near.d = a[16] + a[15] + + -- Normalize the result + t = sqrt(frustum.near.a * frustum.near.a + frustum.near.b * frustum.near.b + frustum.near.c * frustum.near.c) + frustum.near.a = frustum.near.a / t + frustum.near.b = frustum.near.b / t + frustum.near.c = frustum.near.c / t + frustum.near.d = frustum.near.d / t + + if not infinite then + -- Extract the FAR plane + frustum.far = {} + frustum.far.a = a[ 4] - a[ 3] + frustum.far.b = a[ 8] - a[ 7] + frustum.far.c = a[12] - a[11] + frustum.far.d = a[16] - a[15] + + -- Normalize the result + t = sqrt(frustum.far.a * frustum.far.a + frustum.far.b * frustum.far.b + frustum.far.c * frustum.far.c) + frustum.far.a = frustum.far.a / t + frustum.far.b = frustum.far.b / t + frustum.far.c = frustum.far.c / t + frustum.far.d = frustum.far.d / t + end + + return frustum +end + local mat4_mt = {} mat4_mt.__index = mat4 mat4_mt.__tostring = mat4.to_string