Ứng dụng của vector rất quan trọng trong lĩnh vực lập trình game và đồ họa. Thông qua vector, ta có thể mô phỏng được các chuyển động, tính toán lực, hướng di chuyển sau khi va chạm,…
Khái niệm
“…một vectơ là một phần tử trong một không gian vectơ, được xác định bởi ba yếu tố: điểm đầu (hay điểm gốc), hướng (gồm phương và chiều) và độ lớn (hay độ dài).” (Wiki)
Từ một đoạn thẳng AB ta có thể xác định được vector (mã giả):
u.root = A;
u.x = B.x-A.x;
u.y = B.y-A.y;
u.length = sqrt(u.x*u.x+u.y*u.y); // |u|
Vector đơn vị (Unit Vector, Normalized Vector)
Vector đơn vị của u là vector có chiều dài bằng 1 và kí hiệu là û. Vector u sau được gọi là vector đơn vị của v bằng cách tính (mã giả):
û = u/|u|
Như vậy:
|û| = 1
Tích vô hướng (Dot product, Scalar product)
Phép toán tích vô hướng của hai vector được biểu diễn dấu chấm (nên được gọi dot product) và được tính như sau:
v.u = |v||u|cosθ
= v1u1 + v2u2 + … + vnun
Với θ (theta) là góc giữa v, u và n là chiều của không gian.
Từ công thức trên, ta có thể tính được θ:
cosθ = (v.u)/(|v||u|)
θ = arccos(cosθ)
Phép chiếu (Projection)
Một ứng dụng khác của tích vô hướng là tính phép chiếu của một vector này lên vector khác. Tương tự như việc chiếu một vector v thành 2 giá trị x và y lên trục Oxy. (Hình từ wikipedia)
Một vector a được gọi là vector chiếu của v lên u nếu như nó có cùng phương với u và có độ lớn:
|a| = |v|cosθ
Tổng quát hơn với hai vector bất kì, xét biểu thức:
|a| = |v|cosθ
Với u là vector tạo với v một góc θ, từ công thức:
cosθ = (v.u)/(|v||u|)
Suy ra:
|a| = |v|(v.u)/(|v||u|) = (v.u)/|u|
Vậy ta đã tính được độ lớn của vector v chiếu trên vector u. Từ giá trị |a| này, ta có thể tính được vector a bằng cách nhân với vector đơn vị của u:
a = |a|û
Hiện thực với javascript
Một đối tượng Line được biểu diễn bởi 2 điểm đầu (p1) và cuối (p2) của một đoạn thẳng. Từ hai điểm này, ta có phương thức getVector() để lấy về một đối tượng vector của đoạn thẳng.
Line.prototype.getVector = function() { var x = this.p2.x-this.p1.x; var y = this.p2.y-this.p1.y; return { x: x, y: y, root: this.p1, length: Math.sqrt(x*x+y*y) }; }
Phương thức update() sau được gọi mỗi khi một đoạn thẳng bị thay đổi:
function update(){ var v1 = _line1.getVector(); var v2 = _line2.getVector(); // normalize vector v2 v2.dx = v2.x/v2.length; v2.dy = v2.y/v2.length; // dot product var dp = v1.x*v2.x + v1.y*v2.y; // length of the projection of v1 on v2 var length = dp/v2.length; // the projection vector of v1 on v2 var v3 = { x: length*v2.dx, y: length*v2.dy, }; // the projection line _line3.p2.x = _line3.p1.x+v3.x; _line3.p2.y = _line3.p1.y+v3.y; // calculate the angle between v1 and v2 // and convert it from radians into degrees _angle = Math.acos(dp/(v1.length*v2.length))*(180/Math.PI); _angle = Math.round(_angle*100)/100; }
Trong phần demo sau, tôi thực hiện phép chiếu vector màu đỏ lên vector xanh lá. Kết quả của phép chiếu là vector màu xanh lam. Bạn có thể thay đổi hướng và độ lớn của vector bằng cách nhấn nhấn và rê chuột vào đầu mũi tên.
Mã nguồn:
<html> <head> <script> /***************** Circle ***************/ function Circle(x,y) { this.x = x; this.y = y; this.radius = 5; } Circle.prototype.draw = function(context) { context.beginPath(); context.fillStyle = "black"; context.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true); context.closePath(); context.fill(); } Circle.prototype.setPosition = function(x, y) { this.x = x + this.radius; this.y = y + this.radius; } Circle.prototype.contains = function(x, y) { var dx = this.x - x; var dy = this.y - y; return Math.sqrt(dx * dx + dy * dy) <= this.radius; } /***************** Line ***************/ function Line(x1,y1,x2,y2,color,hasHandles) { this.color = color; this.p1 = new Circle(x1,y1,5); this.p2 = new Circle(x2,y2,5); this.hasHandles = hasHandles; } Line.prototype.captureMouse = function(x,y) { if(this.p2.contains(x,y)) return this.p2; } Line.prototype.getVector = function() { var x = this.p2.x-this.p1.x; var y = this.p2.y-this.p1.y; return { x: x, y: y, root: this.p1, length: Math.sqrt(x*x+y*y) }; } Line.prototype.draw = function(context) { context.beginPath(); context.strokeStyle = this.color; context.lineWidth = this.width; var headsize = 15; var angle = Math.atan2(this.p2.y-this.p1.y,this.p2.x-this.p1.x); context.moveTo(this.p1.x, this.p1.y); context.lineTo(this.p2.x, this.p2.y); context.lineTo(this.p2.x-headsize*Math.cos(angle-Math.PI/6),this.p2.y-headsize*Math.sin(angle-Math.PI/6)); context.moveTo(this.p2.x, this.p2.y); context.lineTo(this.p2.x-headsize*Math.cos(angle+Math.PI/6),this.p2.y-headsize*Math.sin(angle+Math.PI/6)); context.closePath(); context.stroke(); if(this.hasHandles){ this.p1.draw(context); this.p2.draw(context); } } /************** Main *************/ var _canvas; var _context; var _movingShape = false; var _line1; var _line2; var _line3; var _angle; // angle between _line1 and _line2 function canvas_mousedown(e) { var x = e.pageX - _canvas.offsetLeft; var y = e.pageY - _canvas.offsetTop; _movingShape = _line1.captureMouse(x, y); if(!_movingShape) _movingShape = _line2.captureMouse(x, y); } function canvas_mousemove(e) { var x = e.pageX - _canvas.offsetLeft; var y = e.pageY - _canvas.offsetTop; if (_movingShape) { _movingShape.setPosition(x, y); update(); draw(); } } function canvas_mouseup(e) { _movingShape = null; } function clear() { _context.clearRect(0, 0, _canvas.width, _canvas.height); } function update(){ var v1 = _line1.getVector(); var v2 = _line2.getVector(); // normalize vector v2 v2.dx = v2.x/v2.length; v2.dy = v2.y/v2.length; // dot product var dp = v1.x*v2.x + v1.y*v2.y; // length of the projection of v1 on v2 var length = dp/v2.length; // the projection vector of v1 on v2 var v3 = { x: length*v2.dx, y: length*v2.dy, }; // the projection line _line3.p2.x = _line3.p1.x+v3.x; _line3.p2.y = _line3.p1.y+v3.y; // calculate the angle between v1 and v2 // and convert it from radians into degrees _angle = Math.acos(dp/(v1.length*v2.length))*(180/Math.PI); _angle = Math.round(_angle*100)/100; } function draw() { clear(); _line1.draw(_context); _line2.draw(_context); _line3.draw(_context); _context.fillText("Theta: "+_angle+"°",10,20); } window.onload = function(){ _canvas = document.getElementById("canvas"); _context = _canvas.getContext("2d"); _context.font = "16px Arial"; _context.lineWidth = 3; _line1 = new Line(100, 200, 250, 150,"red",true); _line2 = new Line(100, 200, 380, 200,"green",true); _line3 = new Line(100, 200, 100, 200,"blue",false); _canvas.onmousedown = canvas_mousedown; _canvas.onmousemove = canvas_mousemove; _canvas.onmouseup = canvas_mouseup; update(); draw(); } </script> </head> <body> <canvas id="canvas" width="400px" height="400px" style="border: 1px solid"></canvas> </body> </html>
Tham khảo
http://vi.wikipedia.org/wiki/Vect%C6%A1
http://en.wikipedia.org/wiki/Dot_product
Game Angry birds cũng áp dụng theo nguyên lý này cho việc xác định tọa độ vị trí bắn và lực bắn phải không anh?
Mình ko rõ game này tính toán cách nào. Nhưng các game tương tự (Gunbound, Gunny,…) chỉ sử dụng các thuộc tính như sức bắn, lực hút, gió,… để tính toán đường bay.
Cám ơn anh!
Anh ơi anh từng sử dụng qua Box2d Game Engine chưa. Anh có thể chia sẽ về cách sử dụng game engine này được không.
Mình cũng dự tính sử dụng và hướng dẫn về thư viện này sau khi xong các bài hướng dẫn cơ bản.
Chờ tut của anh. hihi :d
Cám ơn Y2! Bài viết rất hay.
Chỗ này rút gọn bị sai rồi thì phải?
|a| = |v|(v.u)/(|v||u|) = (v.u)/|v|
Cảm ơn bạn, đúng là có sai sót.