Mercurial > games > semicongine
view src/semicongine/collision.nim @ 363:451b7ccfe722
improve 2D collision, add some vector functionality, allow shaders/pipelines to be ordered for deterministic rendering order
author | Sam <sam@basx.dev> |
---|---|
date | Sun, 01 Oct 2023 20:53:35 +0700 |
parents | 6f61e0bb89b7 |
children |
line wrap: on
line source
import ./core const MAX_COLLISON_DETECTION_ITERATIONS = 20 const MAX_COLLISON_POINT_CALCULATION_ITERATIONS = 20 type ColliderType* = enum Box, Sphere, Points Collider* = object transform*: Mat4 = Unit4F32 case theType*: ColliderType of Box: discard of Sphere: radius*: float32 of Points: points*: seq[Vec3f] func between(value, b1, b2: float32): bool = min(b1, b2) <= value and value <= max(b1, b2) func contains*(collider: Collider, x: Vec3f): bool = # from https://math.stackexchange.com/questions/1472049/check-if-a-point-is-inside-a-rectangular-shaped-area-3d case collider.theType: of Box: let P1 = collider.transform * newVec3f(0, 0, 0) # origin P2 = collider.transform * Z P4 = collider.transform * X P5 = collider.transform * Y u = (P1 - P4).cross(P1 - P5) v = (P1 - P2).cross(P1 - P5) w = (P1 - P2).cross(P1 - P4) uP1 = u.dot(P1) uP2 = u.dot(P2) vP1 = v.dot(P1) vP4 = v.dot(P4) wP1 = w.dot(P1) wP5 = w.dot(P5) ux = u.dot(x) vx = v.dot(x) wx = w.dot(x) ux.between(uP1, uP2) and vx.between(vP1, vP4) and wx.between(wP1, wP5) of Sphere: (collider.transform * x).length < (collider.transform * newVec3f()).length of Points: raise newException(Exception, "Points are not supported yet for 'contains'") # implementation of GJK, based on https://blog.winter.dev/2020/gjk-algorithm/ # most generic implementation of findFurthestPoint # add other implementations of findFurthestPoint for other kind of geometry or optimization # (will be selected depening on type of the first parameter) func findFurthestPoint(points: openArray[Vec3f], direction: Vec3f): Vec3f = var maxDist = low(float32) for p in points: let dist = direction.dot(p) if dist > maxDist: maxDist = dist result = p func findFurthestPoint(transform: Mat4, direction: Vec3f): Vec3f = return findFurthestPoint( [ transform * newVec3f(0, 0, 0), transform * X, transform * Y, transform * Z, transform * (X + Y), transform * (X + Z), transform * (Y + Z), transform * (X + Y + Z), ], direction ) func findFurthestPoint(collider: Collider, direction: Vec3f): Vec3f = case collider.theType of Sphere: let directionNormalizedToSphere = ((direction / direction.length) * collider.radius) collider.transform * directionNormalizedToSphere of Box: findFurthestPoint(collider.transform, direction) of Points: findFurthestPoint(collider.points, direction) func supportPoint(a, b: Collider, direction: Vec3f): Vec3f = a.findFurthestPoint(direction) - b.findFurthestPoint(-direction) func sameDirection(direction: Vec3f, ao: Vec3f): bool = direction.dot(ao) > 0 func line(simplex: var seq[Vec3f], direction: var Vec3f): bool = let a = simplex[0] b = simplex[1] ab = b - a ao = - a if sameDirection(ab, ao): direction = cross(cross(ab, ao), ab) else: simplex = @[a] direction = ao return false func triangle(simplex: var seq[Vec3f], direction: var Vec3f, twoDimensional=false): bool = let a = simplex[0] b = simplex[1] c = simplex[2] ab = b - a ac = c - a ao = - a abc = ab.cross(ac) if sameDirection(abc.cross(ac), ao): if sameDirection(ac, ao): simplex = @[a, c] direction = ac.cross(ao).cross(ac) else: simplex = @[a, b] return line(simplex, direction) else: if (sameDirection(ab.cross(abc), ao)): simplex = @[a, b] return line(simplex, direction) else: if twoDimensional: return true if (sameDirection(abc, ao)): direction = abc else: simplex = @[ a, c, b] direction = -abc return false func tetrahedron(simplex: var seq[Vec3f], direction: var Vec3f): bool = let a = simplex[0] b = simplex[1] c = simplex[2] d = simplex[3] ab = b - a ac = c - a ad = d - a ao = - a abc = ab.cross(ac) acd = ac.cross(ad) adb = ad.cross(ab) if sameDirection(abc, ao): simplex = @[a, b, c] return triangle(simplex, direction) if sameDirection(acd, ao): simplex = @[a, c, d] return triangle(simplex, direction) if sameDirection(adb, ao): simplex = @[a, d, b] return triangle(simplex, direction) return true func getFaceNormals(polytope: seq[Vec3f], faces: seq[int]): (seq[Vec4f], int) = var normals: seq[Vec4f] minTriangle = 0 minDistance = high(float32) for i in countup(0, faces.len - 1, 3): let a = polytope[faces[i + 0]] b = polytope[faces[i + 1]] c = polytope[faces[i + 2]] var normal = (b - a).cross(c - a).normalized() var distance = normal.dot(a) if distance < 0: normal = normal * -1'f32 distance = distance * -1'f32 normals.add normal.toVec4(distance) if distance < minDistance: minTriangle = i div 3 minDistance = distance return (normals, minTriangle) func addIfUniqueEdge(edges: var seq[(int, int)], faces: seq[int], a: int, b: int) = let reverse = edges.find((faces[b], faces[a])) if (reverse >= 0): edges.delete(reverse) else: edges.add (faces[a], faces[b]) func nextSimplex(simplex: var seq[Vec3f], direction: var Vec3f, twoDimensional=false): bool = case simplex.len of 2: simplex.line(direction) of 3: simplex.triangle(direction, twoDimensional) of 4: simplex.tetrahedron(direction) else: raise newException(Exception, "Error in simplex") func collisionPoint3D(simplex: var seq[Vec3f], a, b: Collider): tuple[normal: Vec3f, penetrationDepth: float32] = var polytope = simplex faces = @[ 0, 1, 2, 0, 3, 1, 0, 2, 3, 1, 3, 2 ] (normals, minFace) = getFaceNormals(polytope, faces) minNormal: Vec3f minDistance = high(float32) iterCount = 0 while minDistance == high(float32) and iterCount < MAX_COLLISON_POINT_CALCULATION_ITERATIONS: minNormal = normals[minFace].xyz minDistance = normals[minFace].w var support = supportPoint(a, b, minNormal) sDistance = minNormal.dot(support) if abs(sDistance - minDistance) > 0.001'f32: minDistance = high(float32) var uniqueEdges: seq[(int, int)] var i = 0 while i < normals.len: if sameDirection(normals[i], support): var f = i * 3 addIfUniqueEdge(uniqueEdges, faces, f + 0, f + 1) addIfUniqueEdge(uniqueEdges, faces, f + 1, f + 2) addIfUniqueEdge(uniqueEdges, faces, f + 2, f + 0) faces[f + 2] = faces.pop() faces[f + 1] = faces.pop() faces[f + 0] = faces.pop() normals[i] = normals.pop() dec i inc i var newFaces: seq[int] for (edgeIndex1, edgeIndex2) in uniqueEdges: newFaces.add edgeIndex1 newFaces.add edgeIndex2 newFaces.add polytope.len polytope.add support var (newNormals, newMinFace) = getFaceNormals(polytope, newFaces) if newNormals.len == 0: break var oldMinDistance = high(float32) for j in 0 ..< normals.len: if normals[j].w < oldMinDistance: oldMinDistance = normals[j].w minFace = j if (newNormals[newMinFace].w < oldMinDistance): minFace = newMinFace + normals.len for f in newFaces: faces.add f for n in newNormals: normals.add n inc iterCount result = (normal: minNormal, penetrationDepth: minDistance + 0.001'f32) func collisionPoint2D(polytopeIn: seq[Vec3f], a, b: Collider): tuple[normal: Vec3f, penetrationDepth: float32] = var polytope = polytopeIn minIndex = 0 minDistance = high(float32) iterCount = 0 minNormal: Vec2f while minDistance == high(float32) and iterCount < MAX_COLLISON_POINT_CALCULATION_ITERATIONS: for i in 0 ..< polytope.len: let j = (i + 1) mod polytope.len vertexI = polytope[i] vertexJ = polytope[j] ij = vertexJ - vertexI var normal = newVec2f(ij.y, -ij.x).normalized() distance = normal.dot(vertexI) if (distance < 0): distance *= -1'f32 normal = normal * -1'f32 if distance < minDistance: minDistance = distance minNormal = normal minIndex = j let support = supportPoint(a, b, minNormal.toVec3) sDistance = minNormal.dot(support) if(abs(sDistance - minDistance) > 0.001): minDistance = high(float32) polytope.insert(support, minIndex) inc iterCount result = (normal: newVec3f(minNormal.x, minNormal.y), penetrationDepth: minDistance + 0.001'f32) func intersects*(a, b: Collider, as2D=false): bool = var support = supportPoint(a, b, newVec3f(0.8153, -0.4239, if as2D: 0.0 else: 0.5786)) # just random initial vector simplex = newSeq[Vec3f]() direction = -support n = 0 simplex.insert(support, 0) while n < MAX_COLLISON_DETECTION_ITERATIONS: support = supportPoint(a, b, direction) if support.dot(direction) <= 0: return false simplex.insert(support, 0) if nextSimplex(simplex, direction, twoDimensional=as2D): return true # prevent numeric instability if direction == newVec3f(0, 0, 0): direction[0] = 0.0001 inc n func collision*(a, b: Collider, as2D=false): tuple[hasCollision: bool, normal: Vec3f, penetrationDepth: float32] = var support = supportPoint(a, b, newVec3f(0.8153, -0.4239, if as2D: 0.0 else: 0.5786)) # just random initial vector simplex = newSeq[Vec3f]() direction = -support n = 0 simplex.insert(support, 0) while n < MAX_COLLISON_DETECTION_ITERATIONS: support = supportPoint(a, b, direction) if support.dot(direction) <= 0: return result simplex.insert(support, 0) if nextSimplex(simplex, direction, twoDimensional=as2D): let (normal, depth) = if as2D: collisionPoint2D(simplex, a, b) else: collisionPoint3D(simplex, a, b) return (true, normal, depth) # prevent numeric instability if direction == newVec3f(0, 0, 0): direction[0] = 0.0001 inc n func calculateCollider*(points: openArray[Vec3f], theType: ColliderType): Collider = var minX = high(float32) maxX = low(float32) minY = high(float32) maxY = low(float32) minZ = high(float32) maxZ = low(float32) center: Vec3f for p in points: minX = min(minX, p.x) maxX = max(maxX, p.x) minY = min(minY, p.y) maxY = max(maxY, p.y) minZ = min(minZ, p.z) maxZ = max(maxz, p.z) center = center + p center = center / float32(points.len) let scaleX = (maxX - minX) scaleY = (maxY - minY) scaleZ = (maxZ - minZ) if theType == Points: result = Collider(theType: Points, points: @points) else: result = Collider(theType: theType, transform: translate(minX, minY, minZ) * scale(scaleX, scaleY, scaleZ)) if theType == Sphere: result.transform = translate(center) for p in points: result.radius = max(result.radius, (p - center).length)