From d30795542ab8ca396f20d43d4d4fd397d2fe9029 Mon Sep 17 00:00:00 2001 From: kynd Date: Wed, 28 Jun 2017 00:24:30 -0700 Subject: [PATCH] add collision detection --- collision.html | 65 ++++++++ dev.html | 23 +++ extra/wave_function/bundle.js | 4 +- handy_math.html | 4 +- js/examples/collision/collision_circles.js | 64 ++++++++ js/examples/collision/collision_rectangles.js | 74 +++++++++ js/examples/collision/collision_triangles.js | 141 ++++++++++++++++++ js/examples/dev/reflection.js | 0 js/examples/lib.js | 5 + 9 files changed, 375 insertions(+), 5 deletions(-) create mode 100644 collision.html create mode 100644 dev.html create mode 100644 js/examples/collision/collision_circles.js create mode 100644 js/examples/collision/collision_rectangles.js create mode 100644 js/examples/collision/collision_triangles.js create mode 100644 js/examples/dev/reflection.js diff --git a/collision.html b/collision.html new file mode 100644 index 0000000..33dbb5b --- /dev/null +++ b/collision.html @@ -0,0 +1,65 @@ + + + + + + +Detecting Collision + + + + + + +
+

Detecting Collision

+

Sketching with Math and Quasi Physics

+ +

Circles

+

Two circles, +\(A\) and +\(B\) collide if the distance between the center points is equal or less than the two radii added together.

+
+

$$distance(A_{center},B_{center}) \leq A_{radius}+B_{radius}\Rightarrow$$

+

$$distance(A_{center},B_{center})-A_{radius}-B_{radius}\leq 0$$

+
+ +
+ +

Rectangles

+

Two rectangles, +\(A\) and +\(B\) that are axis aligned collide if

+
+

$$(A_{left}\leq B_{right})\;\wedge\;(A_{right}\geq B_{left})\;\wedge\;(A_{top}\leq B_{bottom})\wedge(A_{bottom}\geq B_{top})$$

+
+

This is to project the shapes to x and y-axis and checking overlap on each axis. Note that screen coordinate system in which y increase from top to bottom is assumed.

+ +
+ + +

Triangles

+

Similarly, collision between two convex polygons can be detected by projecting the shapes to the vectors perpendicular to each side of one of the polygons.

+ +
+ +

Learn More

+

Collision Detection Using the Separating Axis Theorem

+ +
+
+ +

index

+
kynd 2017 | Please suggest edits and/or better code at Github
+
+ + + + + + + + + diff --git a/dev.html b/dev.html new file mode 100644 index 0000000..5e870c7 --- /dev/null +++ b/dev.html @@ -0,0 +1,23 @@ + + + + + + +Dev + + + + + + + + + + + + + + diff --git a/extra/wave_function/bundle.js b/extra/wave_function/bundle.js index 5f6be34..d4498c5 100644 --- a/extra/wave_function/bundle.js +++ b/extra/wave_function/bundle.js @@ -16,12 +16,10 @@ class glRenderer { init() { let gl = this.shell.gl; - this.shell.canvas.setAttribute("width", 640); - this.shell.canvas.setAttribute("height", 640); //this.shell.on("tick", this.update.bind(this)); this.shell.on("gl-render", this.draw.bind(this)); - this.updateShader = glShader( gl, glslify(["#define GLSLIFY 1\nattribute vec2 position;\nvarying vec2 uv;\n\nvoid main() {\n gl_Position = vec4(position,0.0,1.0);\n uv = 0.5 * (position+1.0);\n}\n"]), glslify(["\nprecision mediump float;\n#define GLSLIFY 1\nuniform float frameCount;\nuniform sampler2D buffer;\nuniform vec2 dims;\nvarying vec2 uv;\n\nvoid main() {\n\n vec4 color;\n if (length(uv - vec2(0.5, 0.7)) < 0.025) {\n // source\n color = vec4(0.5 + sin(frameCount * 0.5) * 0.2 * (sin(frameCount * 0.05) + 1.0), 0.5, 0.5, 1.0);\n } else if (abs(uv.x - 0.5) < 0.3 && abs(uv.y - 0.5) < 0.02) {\n color = vec4(0.5, 0.5, 0.5, 1.0);\n } else {\n vec4 samp = texture2D(buffer, uv);\n float va = samp.r;\n\n vec2 d[4];\n d[0] = vec2(-1.0, -0.0);\n d[1] = vec2(0.0, -1.0);\n d[2] = vec2(1.0, 0.0);\n d[3] = vec2(-0.0, 1.0);\n\n float vb = 0.0;\n for (int i = 0; i < 4; i ++) {\n vb += texture2D(buffer, uv + d[i] / dims * 2.0).r;\n }\n float vc = samp.g;\n samp.b = va * 2.0 + 0.2 * (vb - va * 4.0) - vc;\n color = clamp(vec4(0.0), vec4(1.0), samp.brga);\n }\n\n gl_FragColor = color;\n}\n"]) ); + this.updateShader = glShader( gl, glslify(["#define GLSLIFY 1\nattribute vec2 position;\nvarying vec2 uv;\n\nvoid main() {\n gl_Position = vec4(position,0.0,1.0);\n uv = 0.5 * (position+1.0);\n}\n"]), glslify(["\nprecision mediump float;\n#define GLSLIFY 1\nuniform float frameCount;\nuniform sampler2D buffer;\nuniform vec2 dims;\nvarying vec2 uv;\n\nvoid main() {\n\n vec4 color;\n if (length(uv - vec2(0.5, 0.7)) < 0.025) {\n // Source\n color = vec4(0.5 + sin(frameCount * 0.5) * 0.2 * (sin(frameCount * 0.05) + 1.0), 0.5, 0.5, 1.0);\n } else if (abs(uv.x - 0.5) < 0.3 && abs(uv.y - 0.5) < 0.02) {\n // Obstacle\n color = vec4(0.5, 0.5, 0.5, 1.0);\n } else {\n vec4 samp = texture2D(buffer, uv);\n float va = samp.r;\n\n vec2 d[4];\n d[0] = vec2(-1.0, -0.0);\n d[1] = vec2(0.0, -1.0);\n d[2] = vec2(1.0, 0.0);\n d[3] = vec2(-0.0, 1.0);\n\n float vb = 0.0;\n for (int i = 0; i < 4; i ++) {\n vb += texture2D(buffer, uv + d[i] / dims * 2.0).r;\n }\n \n float vc = samp.g;\n samp.b = va * 2.0 + 0.2 * (vb - va * 4.0) - vc;\n color = clamp(vec4(0.0), vec4(1.0), samp.brga);\n }\n\n gl_FragColor = color;\n}\n"]) ); this.renderShader = glShader( gl, glslify(["#define GLSLIFY 1\nattribute vec2 position;\nvarying vec2 uv;\n\nvoid main() {\n gl_Position = vec4(position,0.0,1.0);\n uv = 0.5 * (position+1.0);\n}\n"]), glslify(["precision mediump float;\n#define GLSLIFY 1\nuniform sampler2D buffer;\nuniform int channelIndex;\nuniform vec2 dims;\nvarying vec2 uv;\n\nvoid main() {\n vec4 samp0 = texture2D(buffer, uv + vec2(0.0, -1.0) / dims);\n vec4 samp1 = texture2D(buffer, uv + vec2(0.0, 1.0) / dims);\n\n vec4 color = vec4(0.88, 0.94, 0.95, 1.0);\n if (abs(uv.x - 0.5) < 0.3 && abs(uv.y - 0.5) < 0.02) {\n color = vec4(0.0, 0.0, 0.0, 1.0);\n }\n color.rgb -= smoothstep(0.026, 0.025, length(uv - vec2(0.5, 0.7)));\n\n float v = smoothstep(0.0, 0.1, samp0.r - samp1.r);\n color = vec4(color.rgb * (1.0 - v), 1.0);\n gl_FragColor = color;\n}\n"]) ); let w = 640, h = 640; diff --git a/handy_math.html b/handy_math.html index 40e61cb..4fc475f 100644 --- a/handy_math.html +++ b/handy_math.html @@ -34,7 +34,7 @@

Rotate 2D points

Learn more

A Gentle Primer on 2D Rotations

-

Euclidean distance between two points

+

Euclidean distance between two points

Two dimensions

@@ -62,7 +62,7 @@

Perspective Projection (Fixed Camera)

-

Dot Product

+

Dot Product

Two dimensions

$${\boldsymbol v}_\mathbf1=\lbrack x_1,y_1\rbrack,\hspace{8px}{\boldsymbol v}_\mathbf2=\lbrack x_2,y_2\rbrack$$

diff --git a/js/examples/collision/collision_circles.js b/js/examples/collision/collision_circles.js new file mode 100644 index 0000000..cbce404 --- /dev/null +++ b/js/examples/collision/collision_circles.js @@ -0,0 +1,64 @@ +let A, B; + +function setup() { + canvas = createCanvas(windowWidth, windowHeight); + + A = {center: createVector(), radius: 80}; + B = {center: createVector(), radius: 60}; +} + +function draw() { + clear(); + background(249, 246, 236); + let t = radians(frameCount) / 2; + A.center.x = cos(t) * 120; + A.center.y = sin((t + 1) * 2) * 120; + B.center.x = -cos((t + 2) * 3) * 100; + B.center.y = -sin((t + 3) * 4) * 100; + + // test + + let result = A.center.dist(B.center) - A.radius - B.radius <= 0; + + // rendering + + push(); + translate(width / 2, height / 2); + + noStroke(); + if (result) { + fill(225, 81, 106); + } else { + fill(255); + } + ellipse(A.center.x, A.center.y, A.radius * 2); + ellipse(B.center.x, B.center.y, B.radius * 2); + + noFill(); stroke(0); strokeWeight(1); + ellipse(A.center.x, A.center.y, A.radius * 2); + ellipse(B.center.x, B.center.y, B.radius * 2); + + let pA = A.center.copy().add(B.center.copy().sub(A.center).normalize().mult(A.radius)); + let pB = B.center.copy().add(A.center.copy().sub(B.center).normalize().mult(B.radius)); + + strokeWeight(1); + let boundary = {x: -width / 2, y: -height / 2, w: width, h: height}; + let l = new Line().fromTwoPoints(A.center, B.center); + let perpA = l.getPerpendicular(pA); + let perpB = l.getPerpendicular(pB); + l.draw(boundary); + + stroke(0, 0, 0, 128); + perpA.draw(boundary); + perpB.draw(boundary); + + if (result) { + strokeWeight(3); + stroke(0, 0, 0, 255); + line(pA.x, pA.y, pB.x, pB.y); + } + + pop(); +} + +/** Line **/ diff --git a/js/examples/collision/collision_rectangles.js b/js/examples/collision/collision_rectangles.js new file mode 100644 index 0000000..f719a69 --- /dev/null +++ b/js/examples/collision/collision_rectangles.js @@ -0,0 +1,74 @@ +let A, B; + +function setup() { + canvas = createCanvas(windowWidth, windowHeight); + + A = {x: 0, y: 0, w: 160, h: 90}; + B = {x: 0, y: 0, w: 90, h: 160}; +} + +function draw() { + clear(); + background(249, 246, 236); + let t = radians(frameCount) / 2; + A.x = cos(t) * 120 - A.w / 2; + A.y = sin(t * 2) * 120 - A.h / 2; + B.x = -cos(t * 3) * 120 - B.w / 2; + B.y = -sin(t * 4) * 120 - B.h / 2; + + // test + + let xResult = A.x <= B.x + B.w && A.x + A.w >= B.x; + let yResult = A.y <= B.y + B.h && A.h + A.y >= B.y; + let result = xResult && yResult; + + // rendering + + push(); + translate(width / 2, height / 2); + + noStroke(); + if (result) { + fill(225, 81, 106); + } else { + fill(255); + } + rect(A.x, A.y, A.w, A.h); + rect(B.x, B.y, B.w, B.h); + + noFill(); stroke(0); strokeWeight(1); + rect(A.x, A.y, A.w, A.h); + rect(B.x, B.y, B.w, B.h); + + fill(0); strokeWeight(1); + let l0 = -width / 2, l1 = l0 + 16; + let b0 = height / 2, b1 = b0 - 16; + + line(-width / 2, b1, width / 2, b1); + line(l1, -height / 2, l1, height / 2); + + stroke(0, 0, 0, 32); + line(A.x, A.y, A.x, b0); + line(A.x + A.w, A.y, A.x + A.w, b0); + line(A.x, A.y, l0, A.y); + line(A.x, A.y + A.h, l0, A.y + A.h); + + line(B.x, B.y, l0, B.y); + line(B.x, B.y + B.h, l0, B.y + B.h); + line(B.x, B.y, B.x, b0); + line(B.x + B.w, B.y, B.x + B.w, b0); + + stroke(0); + strokeWeight(3); + if (xResult) { + let sorted = [A.x, B.x + B.w, A.x + A.w, B.x].sort((a,b)=>{return a > b ? 1: -1}); + line(sorted[1], b1, sorted[2], b1); + } + + if (yResult) { + let sorted = [A.y, B.y + B.h, A.y + A.h, B.y].sort((a,b)=>{return a > b ? 1: -1}); + line(l1, sorted[1], l1, sorted[2]); + } + + pop(); +} diff --git a/js/examples/collision/collision_triangles.js b/js/examples/collision/collision_triangles.js new file mode 100644 index 0000000..089956f --- /dev/null +++ b/js/examples/collision/collision_triangles.js @@ -0,0 +1,141 @@ +let A, B; + +function setup() { + canvas = createCanvas(windowWidth, windowHeight); + + A = new Triangle(0, 0, createVector(0, -80), createVector(-80, 40), createVector(80, 40)); + B = new Triangle(0, 0, createVector(0, -80), createVector(-40, 40), createVector(40, 40)); +} + +function draw() { + clear(); + background(249, 246, 236); + let t = radians(frameCount) / 2; + A.position.x = cos(t) * 120; + A.position.y = sin(t * 2) * 120; + B.position.x = -cos(t * 3) * 120; + B.position.y = -sin(t * 4) * 120; + A.rotation = t; + B.rotation = -t; + A.update(); + B.update(); + + // test + + let renderingData = []; + let result = true; + for (let i = 0; i < 3; i++) { + let i0 = i; + let i1 = (i + 1) % 3; + let i2 = (i + 2) % 3; + let vec = A.p[i1].copy().sub(A.p[i0]).normalize(); + let axis = createVector(vec.y, - vec.x); + let o = axis.dot(A.p[i0]); + let dA0 = axis.dot(A.p[i1]); + let dA1 = axis.dot(A.p[i2]); + let Amin = min(dA0, dA1); + let Amax = max(dA0, dA1); + + let dB0 = axis.dot(B.p[0]); + let dB1 = axis.dot(B.p[1]); + let dB2 = axis.dot(B.p[2]); + let Bmin = min(dB0, dB1, dB2); + let Bmax = max(dB0, dB1, dB2); + + renderingData.push({vec: vec, axis: axis, o: o, Amin: Amin, Amax: Amax, Bmin, Bmax, result: false}); + if ( + ( Bmin <= Amin && Amin <= Bmax ) || + ( Bmin <= Amax && Amax <= Bmax ) || + ( Amin <= Bmin && Bmin <= Amax ) || + ( Amin <= Bmax && Bmax <= Amax ) + ) { + renderingData[i].result = true; + continue; + } + result = false; + } + + // rendering + + push(); + translate(width / 2, height / 2); + noStroke(); + if (result) { + fill(225, 81, 106); + } else { + fill(255); + } + A.draw(); + B.draw(); + + noFill(); stroke(0); strokeWeight(1); + A.draw(); + B.draw(); + + fill(0); + + let n = floor(frameCount / 180) % 4; + let boundary = {x: -width / 2, y: -height / 2, w: width, h: height}; + for (let i = 0; i < n; i ++) { + let d = renderingData[i]; + let origin = A.p[i].copy(); + let axis = new Line().fromPointAndVector(origin, d.axis); + + stroke(0); strokeWeight(1); + axis.draw(boundary); + + let pAmin = origin.copy().add(d.axis.copy().mult(d.Amin - d.o)); + let pAmax = origin.copy().add(d.axis.copy().mult(d.Amax - d.o)); + let pBmin = origin.copy().add(d.axis.copy().mult(d.Bmin - d.o)); + let pBmax = origin.copy().add(d.axis.copy().mult(d.Bmax - d.o)); + + stroke(0, 0, 0, 32); + axis.getPerpendicular(pAmin).draw(boundary); + axis.getPerpendicular(pAmax).draw(boundary); + axis.getPerpendicular(pBmin).draw(boundary); + axis.getPerpendicular(pBmax).draw(boundary); + + if (d.result) { + let sorted = [d.Amin, d.Amax, d.Bmin, d.Bmax].sort((a,b)=>{return a > b ? 1: -1}); + let p0 = origin.copy().add(d.axis.copy().mult(sorted[1] - d.o)); + let p1 = origin.copy().add(d.axis.copy().mult(sorted[2] - d.o)); + stroke(0); strokeWeight(3); + line(p0.x, p0.y, p1.x, p1.y); + } + } + pop(); +} + +class Triangle { + constructor(x, y, p0, p1, p2) { + this.position = createVector(x, y); + this.op = []; + this.p = []; + this.op.push(p0); + this.op.push(p1); + this.op.push(p2); + this.p.push(createVector()); + this.p.push(createVector()); + this.p.push(createVector()); + this.rotation = 0; + this.update(); + } + + update() { + for (let i = 0; i < 3; i ++) { + this.p[i] = rotate2d(this.op[i], this.rotation).add(this.position); + } + } + + draw() { + beginShape(); + for (let i = 0; i < 3; i ++) { + vertex(this.p[i].x, this.p[i].y); + } + endShape(CLOSE); + } +} + +/** rotate2d **/ + +/** Line **/ diff --git a/js/examples/dev/reflection.js b/js/examples/dev/reflection.js new file mode 100644 index 0000000..e69de29 diff --git a/js/examples/lib.js b/js/examples/lib.js index 93f07ff..548148f 100644 --- a/js/examples/lib.js +++ b/js/examples/lib.js @@ -151,6 +151,11 @@ class Line { return this.fromTwoPoints(p0, p1); } + fromPointAndVector(p0, v) { + let p1 = {x: p0.x + v.x, y: p0.y + v.y}; + return this.fromTwoPoints(p0, p1); + } + intersects(o) { if (o instanceof Line) { let d = this.a * o.b - o.a * this.b;