VI. Giao điểm của hai đường thẳng
Từ hai đoạn thẳng (hoặc vector) trong mặt phẳng 2D, ta có thể tìm được giao điểm của chúng để
tính toán các góc phản xạ và hướng di chuyển.
1. Tạo phương trình đường thẳ ng từ đoạn thẳng
Ta có hai điểm A(x1,y1) và B(x2,y2) tạo thành một đoạn thẳng, để tạo được một phương trình
đường thẳng từ hai điểm này, ta cần tính được độ nghiêng của đường thẳng (slope) theo công
thức:
a = (y2 – y1)/(x2 – x1)
Thay thế x2, y2 bằng hai biến x,y:
a = (y – y1)/(x – x1)
=> y – y1 = a(x – x1)
Hay:
y = ax + (y1 – ax1)
Đặt b = y1 – ax1, ta có:
y = ax + b
2. Tính giao điể m của hai đường thẳng
Ta có hai phương trình đường thẳng
(1): y = a1x + b1
(2): y = a2x + b2
Ta có thể tính được giao điểm của chúng bằng cách tìm giao điểm x trước:
a1x + b1 = a2x + b2;
=> x(a1 – a2) = b2 – b1
=> x = (b2 – b1)/(a1 – a2)
Khi đã có x, ta sẽ thế vào (1) hoặc (2 ) để tính được y. Để kiểm tra hai đoạn thẳng có cắt nhau
không, ta chỉ cần kiểm tra điểm {x,y} thuộc cả hai đoạn thẳng hay không. Ngoài ra ta cần loại
trừ trường hợp hai đường thẳng song song hoặc trùng nhau, khi đó giá trị slope của chúng sẽ
bằng nhau.
VII. Va chạm và phản xạ
Để tính được hướng phản xạ của khi một vật thể va chạm vào mặt phẳng, ta có thể dựa vào góc
nghiêng của mặt phẳng và vector theo từng vùng giá trị. Tuy nhiên cách tổng quát hơn là sử dụng
các phép toán trên vector để thực hiện.
1. Kiểm tra hai đoạn thẳng cắt nhau
Từ bài viết trước về tìm giao điể m củ a hai đường th ẳ ng , ta có thể kiểm tra xem giao điểm tìm
được có phải là giao điểm của hai đoạn thẳng hay không. Cách kiểm tra đơn giản là bạn xem
điểm này có thuộc cả hai phần bao hình chữ nhật của hai đoạn thẳng. Ta có phương thức kiểm
tra như sau:
// detect if a point is inside the rectangle bound of this line
Line.prototype.insideRectBound = function(x, y){
if( Math.min(this.p1.x,this.p2.x) - x > EPSILON ||
x - Math.max(this.p1.x,this.p2.x) > EPSILON ||
Math.min(this.p1.y,this.p2.y) - y > EPSILON ||
y - Math.max(this.p1.y,this.p2.y) > EPSILON)
return false;
return true;
}
// ...
var intersected = _line1.insideRectBound(x,y) && _line2.insideRectBound(x,y);
2. Phương pháp
Trong hình minh họa sau, vector v là hướng di chuyển của vật thể va chạm vào u. Vector phản
xạ của v là b. Từ vector b ta phân tích ra hai vector thành phần là a và c (như vậy ta có a + c = b).
Trong đó: a: vector chiếu của v trên u. c: vector chiếu của v trên đường thẳng vuông góc với u.
Đây là vector pháp tuyến của u và có độ dài bằng khoảng cách từ gốc của v đến u. Như vậy để
tìm được vector phản xạ b, ta chỉ cần theo 4 bước:
1. Tìm giao điểm của hai đoạn thẳng (tương ứng với hai vector v và u).
2. Tìm vector chiếu a của v trên u.
3. Tìm vector pháp tuyến của u có độ dài bằng khoảng cách từ gốc của v đến u.
4. Tính tổng vector a và c.
VIII. Va chạm giữa đường tròn và đoạn thẳng
Tìm điểm va chạ m của một đường tròn với đoạn thẳng và xác định hướng phản xạ của di chuyển
thông qua các phép toán trên vector.
1. Va chạm
Để xác định va chạm, ta tính toán khoảng cách từ tâm đường tròn đến đoạn thẳng theo phương
pháp Tính khoả ng cách từ đi ể m đế n đoạ n th ẳ ng và so sánh với bán kính của đường tròn.
Ví dụ trong phần kiềm tra khoảng cách này chưa xét trường hợp va chạm với hai đầu của đoạn
thẳng. Bởi vì việc kiểm tra phản xạ khi va chạm với đầu đoạn thẳng sẽ cần tính toán thêm một
vài bước nên ta sẽ bỏ qua.
Phương thức sau sẽ trả về điểm va chạm của trái bóng với đoạn thẳng hoặc null nếu như không
có.
Ball.prototype.findCollisionPoint = function(line) {
// create a vector from the point line.p1 to the center of this ball.
var v1= {
x: this.cx - line.p1.x,
y: this.cy - line.p1.y
};
var v2 = line.getVector();
var v3 = findProjection(v1,v2);
v3.length = Math.sqrt(v3.x*v3.x+v3.y*v3.y);
var collisionPoint = null;
if(v3.dp>0 && v3.length <= v2.length)
{
var dx = v1.x-v3.x;
var dy = v1.y-v3.y;
var d = Math.sqrt(dx*dx+dy*dy); // distance
if(d>this.radius)
return null;
collisionPoint = {
x: line.p1.x + v3.x,
y: line.p1.y + v3.y
};
// don't overlap
if(d < this.radius)
{
this.cx -=
(this.movement.x/this.speed)*(this.radius-d);
this.cy -=
(this.movement.y/this.speed)*(this.radius-d);
}
}
return collisionPoint;
}
2. Phản xạ
Dựa vào vector chuyển động của trái bóng và vector được tạo ra từ đoạn thẳng, ta có thể tính
được vector chuyển động mới của bóng (xem Vector2D: Va chạ m và phả n x ạ ).
Ball.prototype.bounceAgainst = function(line) {
if(this.findCollisionPoint(line))
{
var v1 = this.movement;
var v2 = line.getVector();
var v3 = findProjection(v1,v2);
// perpendicular vector of v2
var v4 = {
x: v2.y,
y: -v2.x
};
v4 = findProjection(v1,v4);
v4.x = -v4.x;
v4.y = -v4.y;
// bounce vector
var v5 = {
x: v3.x + v4.x,
y: v3.y + v4.y
};
v5.length = Math.sqrt(v5.x*v5.x+v5.y*v5.y);
// normalize vector
v5 = {
x: v5.x/v5.length,
y: v5.y/v5.length
};
this.setMovement(v5);
}
}
Đối với việc kiểm tra phản xạ khi trái bóng va chạm với 1 điểm (như hai đầu của đoạn thẳng),
bạn cần xác định một vector vuông góc với đường thẳng nối tâm trái bóng đến điểm va chạm và
coi đó là mặt phẳng va chạm.
Minh họa:
IX. Va chạm giữa nhiều đường tròn
Để xác đ ịnh hướng di chuyển của hai quả cầu có khối lượng khác nhau sau khi va chạm, ta sử
dụng công thức của định luật bảo toàn động lượng trong một hệ cô lập.
Xét trường hợp va chạm trong không gian một chiều (1D – các vật di chuyển trên một đường
thẳng), ta gọi m là khối lượng và vận tốc của quả cầu C, u là vận tốc trước khi va chạm và v là
vận tốc sau khi va chạm. Tại thời điểm hai quả cầu C1 và C2 va chạm nhau, ta có công thức:
m1u1 + m2u2 = m1v1 + m2v2
Suy ra:
v1 = (u1*(m1- m2) + 2*m2*u2)/(m1+m2)
v2 = (u2*(m2- m1) + 2*m1*u1)/(m1+m2)
Ta có thể áp dụng công thức này trong không gian hai chiều (2D) để tính vận tốc theo một
phương xác định.
Bằng cách chia vector vận tốc thành hai vector thành phần, một vector ux1 nằm trên đường
thẳng chứa hai tâm C1 và C2 (phương ng ang), vector uy1 còn lại vuông góc với vx1 (phương
dọc).ta chuyển không gian 2 chiều thành 1 chiều theo phương chứa đoạn thẳng nối hai tâm
đường tròn.
var angle1 = Math.atan2(u1.y, u1.x);
var ux1 = u1.length * Math.cos(angle1- angle);
var uy1 = u1.length * Math.sin(angle1- angle);
Áp dụng công thức bên trên, ta tính được vector vận tốc mới theo phương ngang là vx1. Do đây
là không gian 1D nên vận tốc uy1 sẽ không ảnh hưởng.
var vx1 = ((ball1.mass- ball2.mass)*ux1+(ball2.mass+ball2.mass)*ux2)/(ball1.mass+ball2.mass);
var vy1 = uy1;
Đã có hai vector thành phần là vx1 và vy1, ta cần chuyển lại các vector mới này sang không gian
2D và hợp thành vector vector chuyển động mới là v1. Do vector vx1 vuông góc với vy1 nên ta
cần cộng với angle một góc 90 độ hay PI/2. Ở đây, angle là góc tạo bởi đoạn thẳng nối hai tâm
đường tròn.
var v1 = {};
v1.x = Math.cos(angle)*vx1+Math.cos(angle+Math.PI/2)*vy1;
v1.y = Math.sin(angle)*vx1+Math.sin(angle+Math.PI/2)*vy1;
1. Xử lý va chạm của nhiều đường tròn
Cách đơn giản nhất và có độ phức tạp tương đối nhỏ (số l ần l ặp là n(n-1)/2) trong việc kiểm tra
và xử l ý va chạm cho nhiều đối tượng l à sử dụng hai vòng l ặp l ồng nhau theo dạng sau:
for(var i=0;i<_balls.length;i++)
{
for(var j=i+1;j<_balls.length;j++)
checkCollision(_balls[i],_balls[j]);
}
Hàm checkCollision:
function checkCollision(ball1,ball2){
var dx = ball1.cx - ball2.cx;
var dy = ball1.cy - ball2.cy;
// check collision between two balls
var distance = Math.sqrt(dx*dx+dy*dy);
if(distance > ball1.radius + ball2.radius)
return;
var angle = Math.atan2(dy,dx);
var u1 = ball1.getVelocity();
var u2 = ball2.getVelocity();
var angle1 = Math.atan2(u1.y, u1.x);
var angle2 = Math.atan2(u2.y, u2.x);
var ux1 = u1.length * Math.cos(angle1-angle);
var uy1 = u1.length * Math.sin(angle1-angle);
var ux2 = u2.length * Math.cos(angle2-angle);
var uy2 = u2.length * Math.sin(angle2-angle);
var vx1 = ((ball1.massball2.mass)*ux1+(ball2.mass+ball2.mass)*ux2)/(ball1.mass+ball2.mass);
var vx2 = ((ball1.mass+ball1.mass)*ux1+(ball2.ma ssball1.mass)*ux2)/(ball1.mass+ball2.mass);
var vy1 = uy1;
var vy2 = uy2;
// now we transform the 1D coordinate back to 2D
var v1 = {}, v2 = {};
v1.x = Math.cos(angle)*vx1+Math.cos(angle+Math.PI/2)*vy1;
v1.y = Math.sin(angle)*vx1+Math.sin(angle+Math.PI/2)*vy1;
v2.x = Math.cos(angle)*vx2+Math.cos(angle+Math.PI/2)*vy2;
v2.y = Math.sin(angle)*vx2+Math.sin(angle+Math.PI/2)*vy2;
ball1.velocity = v1;
ball2.velocity = v2;
}
X. Kiểm tra va chạm dựa trên pixel
Các đối tượng đồ họa (hoặc hình ảnh) trong ga me thường được một giới hạn trong một khung
bao hình chữ nhật có nền trong suốt (pixel có alpha = 0). Như vậy đối với các đối tượng phức tạp
và muốn kiểm tra va chạm chính xác, ta cần kiểm tra các pixel có độ alpha > 0 của hai đối tượng
đồ họa có cùng nằm trên một vị trí hay không.
1. Một wrapper của Image
Để tiện xử lý các hình ảnh trong canvas dưới dạng một đối tượng trong game với các chức năng
cần thiết, ta tạo một lớp ImageObj chứa bên trong một đối tượng Image để lưu trữ hình ảnh.
Phương thức quan trọng mà ImageObj phải có là draw(), tuy nhiên việc nạp ảnh có thể diễn ra
khá lâu vì vậy ta cần một cách thức xử lý trường hợp này.
Chẳng hạn ta sẽ viết ra một dòng thông báo thay thế cho ảnh với kích thước mặc định trong
trường hợp ảnh chưa được tải xong:
function ImageObj(){
// ...
this.draw = function(context){
if(ready){
context.drawImage(this.img,this.left,this.top);
}
else{
// image has not finished loading
// draw something useful instead
context.save();
context.fillText("Image is not
ready",this.left+10,this.top+10);
context.restore();
}
context.strokeRect(this.left,this.top,this.width,this.height);
context.stroke();
};
// ...
}
Chưa đủ, để đối tượng ImageObj có thể tự vẽ lại sau ảnh được nạp xong, đồng thời lấy được đối
tượng ImageData (chứa mảng các pixel), ta sẽ viết một số lệnh xử lý trong sự kiện onload của
Image.
function ImageObj(){
// ...
var self = this;
this.img.onload = function(){
self.width = this.width;
self.height = this.height;
ready = true; // this image is ready to use
// draw image after loading
context.clearRect(self.left,self.top,self.width,self.height);
self.draw(context);
// get ImageData from this image
self.data =
context.getImageData(self.left,self.top,self.width,self.height).data;
};
this.img.src = url;
}
Do vấn đề bảo mật, ta sẽ không lấy được ImageData của các ảnh không nằm trong host hiện tại
(khác host mà script được thực thi).
2. Xác định vùng giao hai hình chữ nhật
Thay vì lặp qua toàn bộ hai hình ảnh và kiểm tra từng pixel của chúng. Ta sẽ giới hạn lại phần
ảnh cần kiểm tra bằng cách xác định vùng giao giữa hai hình ảnh. Phần này cũng giúp ta xác
định nhanh hai đối tượng có thể xảy ra va chạm hay không.
function findIntersectionRect(img1,img2){
var rect = {
left: Math.max(img1.left,img2.left),
top: Math.max(img1.top,img2.top),
right: Math.min(img1.left+img1.width,img2.left+img1.width),
bottom: Math.min(img1.top+img1.height,img2.top+img2.height)
};
if(rect.left>rect.right || rect.top>rect.bottom)
return null;
return rect;
}
3. Kiểm tra va chạm
Nếu đã tìm hiểu về đối tượng ImageData, bạn biết rằng các pixel sẽ được lưu trữ trong một mảng
một chiều. Mỗi pixel bao gồm bốn phần tử đứng liền nhau trong mảng theo thứ tự là [Red,
Green, Blue, Alpha]. Như vậy một ảnh có kích thước 20×30 sẽ có 600 pixel và có 600×4=2400
phần tử trong ImageData.
function checkPixelCollision(img1,img2){
if(!img1.data || !img2.data)
return null;
var rect = findIntersectionRect(img1,img2);
if(rect)
{
// this array will hold all collision points of two images
var points = [];
for(var i=rect.left;i<=rect.right;i++){
for(var j=rect.top;j<=rect.bottom;j++){
var index1 = ((i-img1.left)+(jimg1.top)*img1.width)*4+3;
var index2 = ((i-img2.left)+(jimg2.top)*img2.width)*4+3;
if(img1.data[index1+3]!=0 &&
img2.data[index2+3]!=0)
{
points.push({x:i,y:j});
// you can exit here instead of
continue looping
}
}
}
return {
rect: rect,
points: points
};
}
return null;
}
Để tăng tốc độ kiểm tra va chạm, thay vì lặp qua từng pixel, bạn có thể lặp ngắt quãng n pixel
(theo cả 2 chiều ngang và dọc) tùy theo mức độ yêu cầu chính xác của ứng dụng. Như vậy số lần
lặp để kiểm tra sẽ giảm đi 2n lần tương ứng. Dưới đây là ảnh minh họa việc thay đổi bước tăng
của vòng lặp từ 1 lên 3. Vùng màu đỏ là các pixel có độ alpha > 0 thuộc cả hai ảnh.
Không có nhận xét nào:
Đăng nhận xét