--[[ geometric intersection routines from simple point tests to shape vs shape tests optimised pretty well in most places. options for boolean or minimum separating vector results continuous sweeps (where provided) also return the time-domain position of first intersection TODO: refactor vector storage to be pooled rather than fully local so these functions can be reentrant ]] --import vec2 if not defined globally local global_vec2 = vec2 local vec2 = global_vec2 if not vec2 then local vec2_path = (...):gsub("intersect", "vec2") vec2 = require(vec2_path) end --module storage local intersect = {} --epsilon for collisions local COLLIDE_EPS = 1e-6 ------------------------------------------------------------------------------ -- circles function intersect.circle_point_overlap(pos, rad, v) return pos:distance_squared(v) < rad * rad end function intersect.circle_circle_overlap(a_pos, a_rad, b_pos, b_rad) local rad = a_rad + b_rad return a_pos:distance_squared(b_pos) < rad * rad end local _ccc_delta = vec2:zero() function intersect.circle_circle_collide(a_pos, a_rad, b_pos, b_rad, into) --get delta _ccc_delta:vset(a_pos):vsubi(b_pos) --squared threshold local rad = a_rad + b_rad local dist = _ccc_delta:length_squared() if dist < rad * rad then if dist == 0 then --singular case; just resolve vertically dist = 1 _ccc_delta:sset(0,1) else --get actual distance dist = math.sqrt(dist) end --allocate if needed if into == nil then into = vec2:zero() end --normalise, scale to separating distance into:vset(_ccc_delta):sdiv(dist):smuli(rad - dist) return into end return false end ------------------------------------------------------------------------------ -- line segments -- todo: separate double-sided, one-sided, and pull-through (along normal) collisions? --vector from line seg to point function intersect._line_to_point(a_start, a_end, b_pos, into) if into == nil then into = vec2:zero() end --direction of line into:vset(a_end):vsub(a_start) --detect degenerate case if into:length_squared() <= COLLIDE_EPS then return intersect.circle_circle_collide(a_start, a_rad, b_pos, b_rad) end --solve for factor along line local dx = (b_pos.x - a_start.x) * (a_end.x - a_start.x) local dy = (b_pos.y - a_start.y) * (a_end.y - a_start.y) local u = (dx + dy) / into:length_squared() --clamp onto segment u = math.clamp01(u) --get the displacement to the nearest point (invalidate direction) into:smuli(u):vaddi(a_start):vsubi(b_pos) return into end --line displacement vector from separation vector function intersect._line_displacement_to_sep(a_start, a_end, separation, total_rad) local distance = separation:normalisei_len() local sep = distance - total_rad if sep <= 0 then if distance <= COLLIDE_EPS then --point intersecting the line; push out along normal separation:vset(a_end):vsub(a_start):normalisei():rot90li() else separation:smuli(-sep) end return separation end return false end --collide a line segment with a circle function intersect.line_circle_collide(a_start, a_end, a_rad, b_pos, b_rad, into) into = intersect._line_to_point(a_start, a_end, b_pos, into) return intersect._line_displacement_to_sep(a_start, a_end, into, a_rad + b_rad) end --collide 2 line segments function intersect.line_line_collide(a_start, a_end, a_rad, b_start, b_end, b_rad, into) --segment directions from start points local a_dir = a_end:vsub(a_start) local b_dir = b_end:vsub(b_start) --detect degenerate cases local a_degen = a_dir:length_squared() <= COLLIDE_EPS local b_degen = a_dir:length_squared() <= COLLIDE_EPS if a_degen and b_degen then --actually just circles return intersect.circle_circle_collide(a_start, a_rad, b_start, b_rad, into) elseif a_degen then -- a is just circle; annoying, need reversed msv local collided = intersect.line_circle_collide(b_start, b_end, b_rad, a_start, a_rad, into) if collided then collided:smuli(-1) end return collided elseif b_degen then --b is just circle return intersect.line_circle_collide(a_start, a_end, a_rad, b_start, b_rad, into) end --otherwise we're _actually_ 2 line segs :) if into == nil then into = vec2:zero() end --first, check intersection --(c to lua translation of paul bourke's -- line intersection algorithm) local dx1 = (a_end.x - a_start.x) local dx2 = (b_end.x - b_start.x) local dy1 = (a_end.y - a_start.y) local dy2 = (b_end.y - b_start.y) local dxab = (a_start.x - b_start.x) local dyab = (a_start.y - b_start.y) local denom = dy2 * dx1 - dx2 * dy1 local numera = dx2 * dyab - dy2 * dxab local numerb = dx1 * dyab - dy1 * dxab --check coincident lines local intersected = "none" if math.abs(numera) < COLLIDE_EPS and math.abs(numerb) < COLLIDE_EPS and math.abs(denom) < COLLIDE_EPS then intersected = "both" else --check parallel, non-coincident lines if math.abs(denom) < COLLIDE_EPS then intersected = "none" else --get interpolants along segments local mua = numera / denom local mub = numerb / denom --intersection outside segment bounds? local outside_a = mua < 0 or mua > 1 local outside_b = mub < 0 or mub > 1 if outside_a and outside_b then intersected = "none" elseif outside_a then intersected = "b" elseif outside_b then intersected = "a" else intersected = "both" --collision point = --[[vec2:xy( a_start.x + mua * dx1, a_start.y + mua * dy1, )]] end end end if intersected == "both" then --simply displace along A normal return into:vset(a_dir):normalisei():smuli(a_rad + b_rad):rot90li() else --dumb as a rock check-corners approach --todo: pool storage --todo calculus from http://geomalgorithms.com/a07-_distance.html local search_tab = {} --only insert corners from the non-intersected line --since intersected line is potentially the apex if intersected ~= "a" then --a endpoints table.insert(search_tab, {intersect._line_to_point(b_start, b_end, a_start), 1}) table.insert(search_tab, {intersect._line_to_point(b_start, b_end, a_end), 1}) end if intersected ~= "b" then --b endpoints table.insert(search_tab, {intersect._line_to_point(a_start, a_end, b_start), -1}) table.insert(search_tab, {intersect._line_to_point(a_start, a_end, b_end), -1}) end local best = table.find_best(search_tab, function(v) return -(v[1]:length_squared()) end) --fix direction into:vset(best[1]):smuli(best[2]) return intersect._line_displacement_to_sep(a_start, a_end, into, a_rad + b_rad) end return false end ------------------------------------------------------------------------------ -- axis aligned bounding boxes --return true on overlap, false otherwise local _apo_delta = vec2:zero() function intersect.aabb_point_overlap(pos, hs, v) _apo_delta:vset(pos):vsubi(v):absi() return _apo_delta.x < hs.x and _apo_delta.y < hs.y end --return true on overlap, false otherwise local _aao_abs_delta = vec2:zero() local _aao_total_size = vec2:zero() function intersect.aabb_aabb_overlap(pos, hs, opos, ohs) _aao_abs_delta:vset(pos):vsubi(opos):absi() _aao_total_size:vset(hs):vaddi(ohs) return _aao_abs_delta.x < _aao_total_size.x and _aao_abs_delta.y < _aao_total_size.y end --discrete displacement --return msv on collision, false otherwise local _aac_delta = vec2:zero() local _aac_abs_delta = vec2:zero() local _aac_size = vec2:zero() local _aac_abs_amount = vec2:zero() function intersect.aabb_aabb_collide(apos, ahs, bpos, bhs, into) if not into then into = vec2:zero() end _aac_delta:vset(apos):vsubi(bpos) _aac_abs_delta:vset(_aac_delta):absi() _aac_size:vset(ahs):vaddi(bhs) _aac_abs_amount:vset(_aac_size):vsubi(_aac_abs_delta) if _aac_abs_amount.x > COLLIDE_EPS and _aac_abs_amount.y > COLLIDE_EPS then --actually collided if _aac_abs_amount.x <= _aac_abs_amount.y then --x min if _aac_delta.x < 0 then return into:sset(-_aac_abs_amount.x, 0) else return into:sset(_aac_abs_amount.x, 0) end else --y min if _aac_delta.y < 0 then return into:sset(0, -_aac_abs_amount.y) else return into:sset(0, _aac_abs_amount.y) end end end return false end --return normal and fraction of dt encountered on collision, false otherwise --TODO: re-pool storage here function intersect.aabb_aabb_collide_continuous( a_startpos, a_endpos, ahs, b_startpos, b_endpos, bhs, into ) if not into then into = vec2:zero() end --compute delta motion local _self_delta_motion = a_endpos:vsub(a_startpos) local _other_delta_motion = b_endpos:vsub(b_startpos) --cheap "is this even possible" early-out do local _self_half_delta = _self_delta_motion:smul(0.5) local _self_bounds_pos = _self_half_delta:vadd(a_endpos) local _self_bounds_hs = _self_half_delta:vadd(ahs) local _other_half_delta = _other_delta_motion:smul(0.5) local _other_bounds_pos = _other_half_delta:vadd(b_endpos) local _other_bounds_hs = _other_half_delta:vadd(bhs) if not body._overlap_raw( _self_bounds_pos, _self_bounds_hs, _other_bounds_pos, _other_bounds_hs ) then return false end end --get ccd minkowski box --this is a relative-space box local _relative_delta_motion = _self_delta_motion:vsub(_other_delta_motion) local _minkowski_halfsize = ahs:vadd(bhs) local _minkowski_pos = b_startpos:vsub(a_startpos) --if a line seg from our relative motion hits the minkowski box, we're in luck --slab raycast is speedy --alias local _rmx = _relative_delta_motion.x local _rmy = _relative_delta_motion.y local _inv_x = math.huge if _rmx ~= 0 then _inv_x = 1 / _rmx end local _inv_y = math.huge if _rmy ~= 0 then _inv_y = 1 / _rmy end local _minkowski_tl = _minkowski_pos:vsub(_minkowski_halfsize) local _minkowski_br = _minkowski_pos:vadd(_minkowski_halfsize) --clip x --get edge t along line local tx1 = (_minkowski_tl.x) * _inv_x local tx2 = (_minkowski_br.x) * _inv_x --clip to existing clip space local txmin = math.min(tx1, tx2) local txmax = math.max(tx1, tx2) --clip y --get edge t along line local ty1 = (_minkowski_tl.y) * _inv_y local ty2 = (_minkowski_br.y) * _inv_y --clip to existing clip space local tymin = math.min(ty1, ty2) local tymax = math.max(ty1, ty2) --clip space local tmin = math.max(0, txmin, tymin) local tmax = math.min(1, txmax, tymax) --still some unclipped? collision! if tmin <= tmax then --"was colliding at start" if tmin == 0 then --todo: maybe collide at old pos, not new pos local msv = self:collide(other, into) if msv then return msv, tmin else return false end end --delta before colliding local _self_collide_pre = _self_delta_motion:smul(tmin) --delta after colliding (to be discarded or projected or whatever) local _self_collide_post = _self_delta_motion:smul(1.0 - tmin) --get the collision normal --(whichever boundary crossed _last_ -> normal) local _self_collide_normal = vec2:zero() if txmin > tymin then _self_collide_normal.x = -math.sign(_self_delta_motion.x) else _self_collide_normal.y = -math.sign(_self_delta_motion.y) end --travelling away from normal? if _self_collide_normal:dot(_self_delta_motion) >= 0 then return false end --just "slide" projection for now _self_collide_post:vreji(_self_collide_normal) --combine local _final_delta = _self_collide_pre:vadd(_self_collide_post) --construct the target position local _target_pos = a_startpos:vadd(_final_delta) --return delta to target pos local msv = _target_pos:vsub(a_endpos) if math.abs(msv.x) > COLLIDE_EPS or math.abs(msv.y) > COLLIDE_EPS then into:vset(msv) return into, tmin end end return false end --check if a point is in a polygon --point is the point to test --poly is a list of points in order --based on winding number, so re-intersecting areas are counted as solid rather than inverting function intersect.point_in_poly(point, poly) local wn = 0 for i, a in ipairs(poly) do local b = poly[i + 1] or poly[1] if a.y <= point.y then if b.y > point.y and vec2.winding_side(a, b, point) > 0 then wn = wn + 1 end else if b.y <= point.y and vec2.winding_side(a, b, point) < 0 then wn = wn - 1 end end end return wn ~= 0 end return intersect