aboutsummaryrefslogtreecommitdiff
path: root/src/main/scala/sims/collision/narrowphase/SAT.scala
blob: 2e276b808f0908504b1b798dfb707b56bf3b3a04 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
/*    _____ _ __  ________    ___                                      *\
**   / ___/(_)  |/  / ___/   |__ \  Simple Mechanics Simulator 2       **
**   \__ \/ / /|_/ /\__ \    __/ /  copyright (c) 2011 Jakob Odersky   **
**  ___/ / / /  / /___/ /   / __/                                      **
** /____/_/_/  /_//____/   /____/                                      **
\*                                                                     */

package sims.collision.narrowphase

import sims.collision._
import sims.math._

class SAT[A <: Collidable: ClassManifest] extends NarrowPhaseDetector[A] {
	
	def collision(pair: (A, A)): Option[Collision[A]] = {
		var c = getCollision(pair)
		c orElse getCollision(pair.swap)
	}
	
	private def getCollision(pair: (A,A)): Option[Collision[A]] = pair match {
		
		case (c1: Circle, c2: Circle) =>
			collisionCircleCircle(c1, c2)(pair)
			
		case (p1: ConvexPolygon, p2: ConvexPolygon) =>
			sat(pair, p1.sides.map(_.rightNormal0) ++ p2.sides.map(_.rightNormal0))
			
		case _ => None
		
	}
	
	private def collisionCircleCircle(c1: Circle, c2: Circle)(pair: (A, A)) = {
		val d = (c2.position - c1.position)
		val l = d.length
		if (l <= c1.radius + c2.radius) {
			val p = c1.position + d.unit * (l - c2.radius)
			Some(new Collision[A] {
				val item1 = pair._1
				val item2 = pair._2
				val normal = d.unit
				val points = List(p)
				val overlap = (c1.radius + c2.radius - l)
			})
		} else None
	}
	
	private def collisionPolyPoly(p1: ConvexPolygon, p2: ConvexPolygon)(pair: (A, A)): Option[Collision[A]] = {
		var minOverlap = Double.PositiveInfinity
		var reference: ConvexPolygon = null
		var incident: ConvexPolygon = null
		var referenceSide: Segment = null
		var incidentVerticeNumber = 0
		
		for (i <- 0 until p1.sides.length) {
			var overlaps = false
			
			for (j <- 0 until p2.vertices.length) {
				val s = p1.sides(i)
				val v = p2.vertices(j)
				
				val overlap = (s.rightNormal0 dot s.point1) - (s.rightNormal0 dot v)
				if (overlap > 0) overlaps = true
				if (overlap > 0 && overlap < minOverlap.abs) {
					minOverlap = overlap
					
					reference = p1
					referenceSide = s
					incident = p2
					incidentVerticeNumber = j
				}
			}
			if (!overlaps) return None
		}
		
		for (i <- 0 until p2.sides.length) {	
			var overlaps = false
			
			for (j <- 0 until p1.vertices.length) {
				val s = p2.sides(i)
				val v = p1.vertices(j)
				
				val overlap = (s.rightNormal0 dot s.point1) - (s.rightNormal0 dot v)
				if (overlap > 0) overlaps = true
				if (overlap > 0 && overlap < minOverlap.abs) {
					minOverlap = overlap
					
					reference = p2
					referenceSide = s
					incident = p1
					incidentVerticeNumber = j
				}
			}
			if (!overlaps) return None
		}
		
		val i = incidentVerticeNumber
		val side1 = incident.sides(i)
		val side2 = incident.sides(mod(i-1, incident.sides.length))
		
		val incidentSide = if ((side1.direction dot referenceSide.rightNormal0).abs <
													(side2.direction dot referenceSide.rightNormal0).abs) side1
											 else side2
											 
		val clipped: Segment = null //incidentSide clippedToSegment referenceSide
		
		Some(new Collision[A] {
			val item1 = reference.asInstanceOf[A]
			val item2 = incident.asInstanceOf[A]
			val normal = referenceSide.rightNormal0
			val points = List(clipped.point1, clipped.point2)
			val overlap = minOverlap
			
		})
		
	}
	
	
	
	
	def farthestFeature(collidable: Collidable, direction: Vector2D): Either[Vector2D, Segment] = collidable match {
		
		case c: Circle => Left(c.position + direction.unit * c.radius)
		
		case p: ConvexPolygon => {
			var max = p.vertices(0) dot direction.unit
			
			//maximum vertice index
			var i = 0
  	
			for (j <- 0 until p.vertices.length) {
				val d = p.vertices(j) dot direction.unit
				if (d > max) {
					max = d
					i = j
				}
			}
			
			/* 1) vertex is considered to be the first point of a segment
			 * 2) polygons vertices are ordered counter-clockwise
			 * 
			 * implies:
			 * previous segment is the (i-1)th segment
			 * next segment is the ith segment */
			val prev = if (i == 0) p.sides.last else p.sides(i - 1)
			val next = p.sides(i)
			
			// check which segment is less parallel to direction
			val side =
				if ((prev.direction.unit dot direction).abs <= (next.direction.unit dot direction).abs) prev
				else next
			
				Right(side)
		}
		
		case _ => throw new IllegalArgumentException("Collidable is of unknown type.")
		
	}
	

	
	//normal is considered pointing from _1 to _2
	//_1 reference, _2 incident
	def getCollisionPoints(pair: (A, A), normal: Vector2D, overlap: Double): Array[Vector2D] = {
		var points = new scala.collection.mutable.ArrayBuffer[Vector2D]
		
		val feature1 = farthestFeature(pair._1, normal)
		
		//is feature 1 a vertex?
		if (feature1.isLeft) {
			return Array(feature1.left.get)
		}
		
		val feature2 = farthestFeature(pair._2, -normal)
		
		//is feature 2 a vertex?
		if (feature2.isLeft) {
			return Array(feature2.left.get)
		}
		
		//neither feature is a vertex
		val side1 = feature1.right.get
		val side2 = feature2.right.get
		
		
		val flipped = (side1.direction.unit dot normal).abs > (side2.direction.unit dot -normal).abs
		val reference = if (!flipped) side1 else side2
		val incident = if (!flipped) side2 else side1
		
		
		//both features are sides, clip feature2 to feature1
		val clipped: Option[Segment] = None //incident clipped reference
		
		clipped match {
			case None => Array()
			case Some(Segment(point1, point2)) => Array(point1, point2) filter ((v: Vector2D) => ((v - reference.point1) dot reference.rightNormal0) <= 0)
		}
	}
	
	def sat(pair: (A, A), axes: Seq[Vector2D]): Option[Collision[A]] = {
		var min = Double.PositiveInfinity
		var n = axes(0)
		
		for (axis <- axes) {
			val overlap = pair._1.project(axis) overlap pair._2.project(axis)
			if (overlap < 0) return None
			if (overlap < min) {
				min = overlap
				n = axis
			}
		}
			
		val pts = getCollisionPoints(pair, n, min)

		if (pts.length == 0) return None
		
		Some(new Collision[A] {
			val item1 = pair._1
			val item2 = pair._2
			val normal = n
			val points = pts.toSeq
			val overlap = min
		})
		
		
			
	}
	
	
	
}

object SAT {
	
	def apply[A <: Collidable: ClassManifest] = new SAT[A]
	
}