如何模拟链物理(游戏设计)
How to simulate chain physics (game design)
我正在尝试为游戏 (AS3) 创建一系列移动对象。到目前为止,我可以将对象拖到另一个对象后面,但我所能做的就是根据距离使 link 靠近另一个 link。它在现实中行不通,它仍然只遵循一个方向。如果我尝试向相反的方向拉动链条,那是行不通的。
我需要一个公式来在 link 被拉动的任何东西后面拉动链条。
我还需要一个公式来使 link 在静止时低于链的其余部分。按照我现在设置的方式,link 试图掉落,但它们只是直接掉落而不是被拉到链条其余部分下方。
那里有一些链条和弦线教程,但其中 none 似乎包含重力或双向拉动。
我会尝试使用 Box2D。我很确定一旦你启动了引擎,运行 你就可以 link objects 一起形成一个 "chain" 一样的组合。请参阅 link 中的演示。一些附加的身体部位已经 "chained" 在一起了。
Verlet 模拟
你最好的选择是 verlet 链模拟。它包含重力、约束,并且可以在任何点施加力。
在 verlet 集成(将数学划分为步骤)中,您只存储顶点的当前位置和前一帧中的位置。与存储当前位置和当前速度和方向的欧几里德积分不同,verlet 积分将当前速度和方向存储为位置之间的差异。这非常适合您所追求的模拟类型,在这种情况下,复杂的交互不必担心动量、旋转和许多其他问题。您所要做的就是记住最后一个位置并设置新位置,其他一切都会自动发生。
我将使用 javascript 表示法,因为我从未使用过 actionscript。
基础。
模拟从点(顶点)开始。顶点是没有大小的自由点。它具有每帧应用的重力,并且受到地面等环境物体的约束。
存储顶点所需的信息
vertex = {
x : 100, // current position
y : 100,
lx : 100, // last position This point is not moving hence the last pos
ly : 100, // is the same as the current
}
对于动画的每一帧,您都将各种力应用于顶点
移点函数和一些常量
GRAV = 0.9; // force of gravity
GROUND = 400; // Y position of the ground. Y is down the screen
GROUND_BOUNCE = 0.5; // Amount of bounce from the ground
DRAG = 0.9; // Amount of friction or drag to apply per frame
dx = (vertex.x-vertex.lx) * DRAG; // get the speed and direction as a vector
dy = (vertex.y-vertex.ly) * DRAG; // including drag
vertex.lx = vertex.x; // set the last position to the current
vertex.ly = vertex.y;
vertex.x += dx; // add the current movement
vertex.y += dy;
vertex.y += GRAV; // add the gravity
每一帧你还需要应用约束。它们几乎可以是任何东西。
现在这只是地面。如果该位置大于地线,则将该点移离地面。该点也可以是固定的(不需要上述移动拖动和重力应用)或固定到移动对象,如鼠标。
约束到地线。
因为最后的位置(vertex.lx
,vertex.ly
)有助于定义当前的运动。当我们改变方向(撞到地面)时,我们必须改变最后一个位置以正确描述新方向,这会将最后一个位置放在地面下。
约束点函数
if(vertex.y > GROUND){
// we need the current speed
dx = (vertex.x-vertex.lx) * DRAG;
dy = (vertex.y-vertex.ly) * DRAG;
speed = sqrt(dx*dx+dy*dy);
vertex.y = GROUND; // put the current y on the ground
vertex.ly = GROUND + dy * GROUND_BOUNCE; // set the last point into the
// ground and reduce the distance
// to account for bounce
vertex.lx += (dy / speed) * vx; // depending on the angle of contact
// reduce the change in x and set
// the new last x position
}
一堆积分。
这是重力、空气摩擦和约束的基本数学运算,如果我们创建一个点并应用数学运算,它就会落到地上,反弹几次然后停下来。
因为我们将需要许多顶点,所以您创建了一个数组来调用此答案points
。
现在是时候连接这些点并将一堆自由浮动的顶点变成各种各样的模拟结构了。
线路约束
对于这个答案,直线代表链中的一个 link。
上图是为了帮助形象化概念。 A和B是两个顶点,旁边的红点是顶点的最后位置。红色箭头表示顶点将被线约束移动的大致方向。线的长度是固定的,下面的算法试图找到使所有线尽可能接近该长度的最佳解决方案。
描述一行
line = {
pointIndex1 : 0, // array index of connected point
pointIndex2 : 1, // array index of second connected point
length : 100, // the length of the line.
// in the demo below I also include a image index and
// a draw function
}
一条线连接两个顶点。一个顶点可以连接多条线。
在我们应用移动、拖动、重力和任何其他约束后,我们应用线约束的每一帧。从上图中,您可以看到两个顶点的最后位置相距太远,无法连接线。为了固定线,我们将两个顶点等量地移向线的中心点(红色箭头)。如果两点距离线长太近,我们会将两点分开。
以下是用于执行此操作的数学。
约束线函数
p1 = points[line.pointIndex1]; // get first point
p2 = points[line.pointIndex2]; // get second point
// get the distance between the points
dx = p2.x - p1.x;
dy = p2.y - p1.y;
distance = sqrt(dx * dx + dy * dy);
// get the fractional distance the points need to move toward or away from center of
// line to make line length correct
fraction = ((line.length - distance) / distance) / 2; // divide by 2 as each point moves half the distance to
// correct the line length
dx *= fraction; // convert that fraction to actual amount of movement needed
dy *= fraction;
p1.x -=dx; // move first point to the position to correct the line length
p1.y -=dy;
p2.x +=dx; // move the second point in the opposite direction to do the same.
p2.y +=dy;
这个约束非常简单,因为我们使用的是 verlet 集成,所以我们不必担心点或线的速度和方向。最重要的是,我们不必处理任何轮换,因为它也得到了处理。
很多点,很多行
至此我们已经完成了单条线所需的所有数学运算,我们可以添加更多线,将第一条线的终点连接到第二条线的起点等等,无论我们需要多长链。一旦我们连接了所有点,我们就将标准约束应用于所有点,然后一一应用线约束。
这是我们运行陷入的一个小问题。当我们移动点以校正第一条线的长度时,我们移动下一条线的起点,然后当我们移动下一条线的点时,我们移动第一条线的端点,打破其长度限制。当我们遍历所有行时,唯一具有正确长度的行将是最后一行。所有其他行将稍微拉向最后一行。
我们可以保持原样,这会给链条带来一些弹性感(在这种情况下不受欢迎,但对于鞭子和绳子来说效果很好),我们可以为线段做一个完整的逆运动学端点解决方案(很难)或者我们可以作弊。如果您再次应用线约束,您会将所有点向正确的解决方案移动更多一点。我们一次又一次地这样做,对之前每次传递中引入的错误进行一些更正。
这个迭代过程将朝着一个解决方案前进,但它永远不会是完美的,但我们可以很快达到错误在视觉上不明显的情况。为了方便起见,我喜欢将迭代次数称为 sim 的刚度。值为 1 表示线条有弹性,值为 10 表示线条几乎没有明显的拉伸。
注意链条越长,拉伸变得越明显,因此需要进行更多的迭代才能获得所需的刚度。
注意这种求解法有缺陷。在很多情况下,点和线的排列有不止一种解决方案。如果系统中有很多运动,它可能会开始在两个(或更多)可能的解决方案之间振荡。如果发生这种情况,您应该限制用户可以添加到系统中的移动量。
主循环。
综上所述,我们需要每帧 运行 模拟一次。我们有一组点,以及一组连接这些点的线。我们移动所有点,然后应用线约束。然后渲染结果准备好在下一帧再次执行所有操作。
STIFFNESS = 10; // how much the lines resist stretching
points.forEach(point => { // for each point in the sim
move(point); // add gravity drag
constrainGround(point); // stop it from going through the ground line
})
for(i = 0; i < STIFFNESS; i+= 1){ // number of times to apply line constraint
lines.forEach(line => { // for each line in the sim
constrainLine(line);
})
}
drawPoints(); // draw all the points
drawLines(); // draw all the lines.
提问具体问题
上述方法提供了很好的线条模拟,它还可以模拟刚体、布娃娃、桥梁、盒子、牛、山羊、猫和狗。通过固定点,您可以做各种吊绳和链条,制作滑轮、坦克履带。非常适合模拟 2D 汽车和自行车。但请记住,它们在视觉上都是可以接受的,但根本不是真正的物理模拟。
你想要一条链子。要制作链条,我们需要给线条一些宽度。所以每个顶点都需要一个半径,地面约束需要考虑到这一点。我们还希望链条不会落在自己身上,所以我们需要一个新的约束来防止球(又名顶点)相互重叠。这会给 sim 增加相当多的额外负载,因为每个球都需要相互测试,并且当我们调整位置以停止重叠时,我们会增加线长度的误差,因此我们需要多次依次执行每个约束以每帧得到一个好的解决方案。
最后一部分是图形的细节,每一行都需要引用一个图像,该图像是链的视觉表示。
我会把所有的事情都留给你来决定如何在 actionscript 中做到最好。
演示
以下演示显示了上述所有操作的结果。它可能不是你想要的,还有其他方法可以解决问题。这种方法有几个问题
- 离散质量,质量由点定义,如果没有更多的数学知识,你不能让点更轻或更重
- 振荡状态。有时系统刚开始震荡,尽量保持在合理的范围内震荡。
- 拉伸。尽管几乎可以消除拉伸,但在某些情况下解决方案并不完美。就像松紧带一样,如果松开弹力链,它就会弹开。我没有增加迭代次数来匹配链条长度,所以较长的链条会显示拉伸,如果你摆动它,你会看到链条分开。
您会感兴趣的函数是 constrainPoint
、constrainLine
、movePoint
和 doSim
(就在 if(points.length > 0){
之后的位 运行Sim) 剩下的只是支持和样板文件。
最好以全页形式观看(我把图片弄得太大了哎呀...:(
要查看链,请单击并按住鼠标右键添加第一个块,然后将 links 添加到链中。我没有限制链条的长度。单击并按住左键以抓取并拖动链条和块的任何部分。
var points = [];
var lines = [];
var pointsStart;
var fric = 0.999; // drag or air friction
var surF = 0.999; // ground and box friction
var grav = 0.9; // gravity
var ballRad = 10; // chain radius set as ball radius
var stiffness = 12; // number of itterations for line constraint
const fontSize = 33;
var chainImages = [new Image(),new Image(),new Image()];
chainImages[0].src = "https://i.stack.imgur.com/m0xqQ.png";
chainImages[1].src = "https://i.stack.imgur.com/fv77t.png";
chainImages[2].src = "https://i.stack.imgur.com/tVSqL.png";
// add a point
function addPoint(x,y,vx,vy,rad = 10,fixed = false){
points.push({
x:x,
y:y,
ox:x-vx,
oy:y-vy,
fixed : fixed,
radius : rad,
})
return points[points.length-1];
}
// add a constrained line
function addLine(p1,p2,image){
lines.push({
p1,p2,image,
len : Math.hypot(p1.x - p2.x,p1.y-p2.y),
draw(){
if(this.image !== undefined){
var img = chainImages[this.image];
var xdx = this.p2.x - this.p1.x;
var xdy = this.p2.y - this.p1.y;
var len = Math.hypot(xdx,xdy);
xdx /= len;
xdy /= len;
if(this.image === 2){ // oops block drawn in wrong direction. Fix just rotate here
// also did not like the placement of
// the block so this line's image
// is centered on the lines endpoint
ctx.setTransform(xdx,xdy,-xdy,xdx,this.p2.x, this.p2.y);
ctx.rotate(-Math.PI /2);
}else{
ctx.setTransform(xdx,xdy,-xdy,xdx,(this.p1.x + this.p2.x)/2,(this.p1.y + this.p2.y)/2);
}
ctx.drawImage(img,-img.width /2,- img.height / 2);
}
}
})
return lines[lines.length-1];
}
// Constrain a point to the edge of the canvas
function constrainPoint(p){
if(p.fixed){
return;
}
var vx = (p.x - p.ox) * fric;
var vy = (p.y - p.oy) * fric;
var len = Math.hypot(vx,vy);
var r = p.radius;
if(p.y <= r){
p.y = r;
p.oy = r + vy * surF;
}
if(p.y >= h - r){
var c = vy / len
p.y = h - r
p.oy = h - r + vy * surF;
p.ox += c * vx;
}
if(p.x < r){
p.x = r;
p.ox = r + vx * surF;
}
if(p.x > w - r){
p.x = w - r;
p.ox = w - r + vx * surF;
}
}
// move a point
function movePoint(p){
if(p.fixed){
return;
}
var vx = (p.x - p.ox) * fric;
var vy = (p.y - p.oy) * fric;
p.ox = p.x;
p.oy = p.y;
p.x += vx;
p.y += vy;
p.y += grav;
}
// move a line's end points constrain the points to the lines length
function constrainLine(l){
var dx = l.p2.x - l.p1.x;
var dy = l.p2.y - l.p1.y;
var ll = Math.hypot(dx,dy);
var fr = ((l.len - ll) / ll) / 2;
dx *= fr;
dy *= fr;
if(l.p2.fixed){
if(!l.p1.fixed){
l.p1.x -=dx * 2;
l.p1.y -=dy * 2;
}
}else if(l.p1.fixed){
if(!l.p2.fixed){
l.p2.x +=dx * 2;
l.p2.y +=dy * 2;
}
}else{
l.p1.x -=dx;
l.p1.y -=dy;
l.p2.x +=dx;
l.p2.y +=dy;
}
}
// locate the poitn closest to x,y (used for editing)
function closestPoint(x,y){
var min = 40;
var index = -2;
for(var i = 0; i < points.length; i ++){
var p = points[i];
var dist = Math.hypot(p.x-x,p.y-y);
p.mouseDist = dist;
if(dist < min){
min = dist;
index = i;
}
}
return index;
}
function constrainPoints(){
for(var i = 0; i < points.length; i ++){
constrainPoint(points[i]);
}
}
function movePoints(){
for(var i = 0; i < points.length; i ++){
movePoint(points[i]);
}
}
function constrainLines(){
for(var i = 0; i < lines.length; i ++){
constrainLine(lines[i]);
}
}
function drawLines(){
// draw back images first
for(var i = 0; i < lines.length; i ++){
if(lines[i].image !== 1){
lines[i].draw();
}
}
for(var i = 0; i < lines.length; i ++){
if(lines[i].image === 1){
lines[i].draw();
}
}
}
// Adds the block at end of chain
function createBlock(x,y){
var i = chainImages[2];
var w = i.width;
var h = i.height;
var p1 = addPoint(x,y+16,0,0,8);
var p2 = addPoint(x-w/2,y+27,0,0,1);
var p3 = addPoint(x+w/2,y+27,0,0,1);
var p4 = addPoint(x+w/2,y+h,0,0,1);
var p5 = addPoint(x-w/2,y+h,0,0,1);
var p6 = addPoint(x,y+h/2,0,0,1);
addLine(p1,p2);
addLine(p1,p3);
addLine(p1,p4);
addLine(p1,p5);
addLine(p1,p6,2);
addLine(p2,p3);
addLine(p2,p4);
addLine(p2,p5);
addLine(p2,p6);
addLine(p3,p4);
addLine(p3,p5);
addLine(p3,p6);
addLine(p4,p5);
addLine(p4,p6);
addLine(p5,p6);
var p7 = addPoint(x,y + 16-(chainImages[0].width-ballRad * 2),0,0,ballRad);
addLine(p1,p7,1);
}
var lastChainLink = 0;
function addChainLink(){
var lp = points[points.length-1];
addPoint(lp.x,lp.y-(chainImages[0].width-ballRad*2),0,0,ballRad);
addLine(points[points.length-2],points[points.length-1],lastChainLink % 2);
lastChainLink += 1;
}
function loading(){
ctx.setTransform(1,0,0,1,0,0)
ctx.clearRect(0,0,w,h);
ctx.fillStyle = "black";
ctx.fillText("Loading media pleaase wait!!",w/2,30);
if(chainImages.every(image=>image.complete)){
doSim = runSim;
}
}
var onResize = function(){ // called from boilerplate
blockAttached = false;
lines.length = 0; // remove all lines and points.
points.length = 0;
lastChainLink = 0; // controls which chain image to use next
holdingCount = 0;
holding = -1;
mouse.buttonRaw = 0;
}
var blockAttached = false;
var linkAddSpeed = 20;
var linkAddCount = 0;
var holding = -1; // the index of the link the mouse has grabbed
var holdingCount = 0;
function runSim(){
ctx.setTransform(1,0,0,1,0,0)
ctx.clearRect(0,0,w,h);
ctx.fillStyle = "black";
if(points.length < 12){
ctx.fillText("Right mouse button click hold to add chain.",w/2,30);
}
if(holdingCount < 180){
if(mouse.buttonRaw & 1 && holding === -2){
ctx.fillText("Nothing to grab here.",w/2,66);
}else{
ctx.fillText("Left mouse button to grab and move chain.",w/2,66);
}
}
if(mouse.buttonRaw & 4){
if(linkAddCount > 0){ // delay adding links
linkAddCount-=1;
}else{
if(!blockAttached ){
createBlock(mouse.x,mouse.y)
blockAttached = true;
}else{
addChainLink(mouse.x,mouse.y);
}
linkAddCount = linkAddSpeed;
}
}
if(points.length > 0){
if(mouse.buttonRaw & 1){
if(holding < 0){
holding = closestPoint(mouse.x,mouse.y);
}
}else{
holding = -1;
}
movePoints();
constrainPoints();
// attach the last link to the mouse
if(holding > -1){
var mousehold = points[holding];
mousehold.ox = mousehold.x = mouse.x;
mousehold.oy = mousehold.y = mouse.y;
holdingCount += 1; // used to hide help;
}
for(var i = 0; i < stiffness; i++){
constrainLines();
if(holding > -1){
mousehold.ox = mousehold.x = mouse.x;
mousehold.oy = mousehold.y = mouse.y;
}
}
drawLines();
}else{
holding = -1;
}
}
var doSim = loading;
/*********************************************************************************************/
/* Boilerplate not part of answer from here down */
/*********************************************************************************************/
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0, firstRun = true;
function start(x,y,col,w){ctx.lineWidth = w;ctx.strokeStyle = col;ctx.beginPath();ctx.moveTo(x,y)}
function line(x,y){ctx.lineTo(x,y)}
function end(){ctx.stroke()}
function drawLine(l) {ctx.lineWidth = 1;ctx.strokeStyle = "Black";ctx.beginPath();ctx.moveTo(l.p1.x,l.p1.y);ctx.lineTo(l.p2.x,l.p2.y); ctx.stroke();}
function drawPoint(p,col = "black", size = 3){ctx.fillStyle = col;ctx.beginPath();ctx.arc(p.x,p.y,size,0,Math.PI * 2);ctx.fill();}
;(function(){
const RESIZE_DEBOUNCE_TIME = 100;
var createCanvas, resizeCanvas, setGlobals, resizeCount = 0;
createCanvas = function () {
var c, cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === undefined) {
canvas = createCanvas();
}
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") {
setGlobals();
}
if (typeof onResize === "function") {
if(firstRun){
onResize();
firstRun = false;
}else{
resizeCount += 1;
setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
}
}
}
function debounceResize() {
resizeCount -= 1;
if (resizeCount <= 0) {
onResize();
}
}
setGlobals = function () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
ctx.font = fontSize + "px arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
}
mouse = (function () {
function preventDefault(e) {
e.preventDefault();
}
var mouse = {
x : 0,
y : 0,
w : 0,
buttonRaw : 0,
over : false,
bm : [1, 2, 4, 6, 5, 3],
active : false,
bounds : null,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left;
m.y = e.pageY - m.bounds.top;
if (t === "mousedown") {
m.buttonRaw |= m.bm[e.which - 1];
} else if (t === "mouseup") {
m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") {
m.buttonRaw = 0;
m.over = false;
} else if (t === "mouseover") {
m.over = true;
} else if (t === "mousewheel") {
m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") {
m.w = -e.detail;
}
e.preventDefault();
}
m.start = function (element) {
m.element = element === undefined ? document : element;
m.mouseEvents.forEach(n => {
m.element.addEventListener(n, mouseMove);
});
m.element.addEventListener("contextmenu", preventDefault, false);
m.active = true;
}
return mouse;
})();
function update(timer) { // Main update loop
doSim(); // call demo code
requestAnimationFrame(update);
}
setTimeout(function(){
resizeCanvas();
mouse.start(canvas, true);
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
},0);
})();
演示中使用的图片
我正在尝试为游戏 (AS3) 创建一系列移动对象。到目前为止,我可以将对象拖到另一个对象后面,但我所能做的就是根据距离使 link 靠近另一个 link。它在现实中行不通,它仍然只遵循一个方向。如果我尝试向相反的方向拉动链条,那是行不通的。
我需要一个公式来在 link 被拉动的任何东西后面拉动链条。
我还需要一个公式来使 link 在静止时低于链的其余部分。按照我现在设置的方式,link 试图掉落,但它们只是直接掉落而不是被拉到链条其余部分下方。
那里有一些链条和弦线教程,但其中 none 似乎包含重力或双向拉动。
我会尝试使用 Box2D。我很确定一旦你启动了引擎,运行 你就可以 link objects 一起形成一个 "chain" 一样的组合。请参阅 link 中的演示。一些附加的身体部位已经 "chained" 在一起了。
Verlet 模拟
你最好的选择是 verlet 链模拟。它包含重力、约束,并且可以在任何点施加力。
在 verlet 集成(将数学划分为步骤)中,您只存储顶点的当前位置和前一帧中的位置。与存储当前位置和当前速度和方向的欧几里德积分不同,verlet 积分将当前速度和方向存储为位置之间的差异。这非常适合您所追求的模拟类型,在这种情况下,复杂的交互不必担心动量、旋转和许多其他问题。您所要做的就是记住最后一个位置并设置新位置,其他一切都会自动发生。
我将使用 javascript 表示法,因为我从未使用过 actionscript。
基础。
模拟从点(顶点)开始。顶点是没有大小的自由点。它具有每帧应用的重力,并且受到地面等环境物体的约束。
存储顶点所需的信息
vertex = {
x : 100, // current position
y : 100,
lx : 100, // last position This point is not moving hence the last pos
ly : 100, // is the same as the current
}
对于动画的每一帧,您都将各种力应用于顶点
移点函数和一些常量
GRAV = 0.9; // force of gravity
GROUND = 400; // Y position of the ground. Y is down the screen
GROUND_BOUNCE = 0.5; // Amount of bounce from the ground
DRAG = 0.9; // Amount of friction or drag to apply per frame
dx = (vertex.x-vertex.lx) * DRAG; // get the speed and direction as a vector
dy = (vertex.y-vertex.ly) * DRAG; // including drag
vertex.lx = vertex.x; // set the last position to the current
vertex.ly = vertex.y;
vertex.x += dx; // add the current movement
vertex.y += dy;
vertex.y += GRAV; // add the gravity
每一帧你还需要应用约束。它们几乎可以是任何东西。
现在这只是地面。如果该位置大于地线,则将该点移离地面。该点也可以是固定的(不需要上述移动拖动和重力应用)或固定到移动对象,如鼠标。
约束到地线。
因为最后的位置(vertex.lx
,vertex.ly
)有助于定义当前的运动。当我们改变方向(撞到地面)时,我们必须改变最后一个位置以正确描述新方向,这会将最后一个位置放在地面下。
约束点函数
if(vertex.y > GROUND){
// we need the current speed
dx = (vertex.x-vertex.lx) * DRAG;
dy = (vertex.y-vertex.ly) * DRAG;
speed = sqrt(dx*dx+dy*dy);
vertex.y = GROUND; // put the current y on the ground
vertex.ly = GROUND + dy * GROUND_BOUNCE; // set the last point into the
// ground and reduce the distance
// to account for bounce
vertex.lx += (dy / speed) * vx; // depending on the angle of contact
// reduce the change in x and set
// the new last x position
}
一堆积分。
这是重力、空气摩擦和约束的基本数学运算,如果我们创建一个点并应用数学运算,它就会落到地上,反弹几次然后停下来。
因为我们将需要许多顶点,所以您创建了一个数组来调用此答案points
。
现在是时候连接这些点并将一堆自由浮动的顶点变成各种各样的模拟结构了。
线路约束
对于这个答案,直线代表链中的一个 link。
上图是为了帮助形象化概念。 A和B是两个顶点,旁边的红点是顶点的最后位置。红色箭头表示顶点将被线约束移动的大致方向。线的长度是固定的,下面的算法试图找到使所有线尽可能接近该长度的最佳解决方案。
描述一行
line = {
pointIndex1 : 0, // array index of connected point
pointIndex2 : 1, // array index of second connected point
length : 100, // the length of the line.
// in the demo below I also include a image index and
// a draw function
}
一条线连接两个顶点。一个顶点可以连接多条线。
在我们应用移动、拖动、重力和任何其他约束后,我们应用线约束的每一帧。从上图中,您可以看到两个顶点的最后位置相距太远,无法连接线。为了固定线,我们将两个顶点等量地移向线的中心点(红色箭头)。如果两点距离线长太近,我们会将两点分开。
以下是用于执行此操作的数学。
约束线函数
p1 = points[line.pointIndex1]; // get first point
p2 = points[line.pointIndex2]; // get second point
// get the distance between the points
dx = p2.x - p1.x;
dy = p2.y - p1.y;
distance = sqrt(dx * dx + dy * dy);
// get the fractional distance the points need to move toward or away from center of
// line to make line length correct
fraction = ((line.length - distance) / distance) / 2; // divide by 2 as each point moves half the distance to
// correct the line length
dx *= fraction; // convert that fraction to actual amount of movement needed
dy *= fraction;
p1.x -=dx; // move first point to the position to correct the line length
p1.y -=dy;
p2.x +=dx; // move the second point in the opposite direction to do the same.
p2.y +=dy;
这个约束非常简单,因为我们使用的是 verlet 集成,所以我们不必担心点或线的速度和方向。最重要的是,我们不必处理任何轮换,因为它也得到了处理。
很多点,很多行
至此我们已经完成了单条线所需的所有数学运算,我们可以添加更多线,将第一条线的终点连接到第二条线的起点等等,无论我们需要多长链。一旦我们连接了所有点,我们就将标准约束应用于所有点,然后一一应用线约束。
这是我们运行陷入的一个小问题。当我们移动点以校正第一条线的长度时,我们移动下一条线的起点,然后当我们移动下一条线的点时,我们移动第一条线的端点,打破其长度限制。当我们遍历所有行时,唯一具有正确长度的行将是最后一行。所有其他行将稍微拉向最后一行。
我们可以保持原样,这会给链条带来一些弹性感(在这种情况下不受欢迎,但对于鞭子和绳子来说效果很好),我们可以为线段做一个完整的逆运动学端点解决方案(很难)或者我们可以作弊。如果您再次应用线约束,您会将所有点向正确的解决方案移动更多一点。我们一次又一次地这样做,对之前每次传递中引入的错误进行一些更正。
这个迭代过程将朝着一个解决方案前进,但它永远不会是完美的,但我们可以很快达到错误在视觉上不明显的情况。为了方便起见,我喜欢将迭代次数称为 sim 的刚度。值为 1 表示线条有弹性,值为 10 表示线条几乎没有明显的拉伸。
注意链条越长,拉伸变得越明显,因此需要进行更多的迭代才能获得所需的刚度。
注意这种求解法有缺陷。在很多情况下,点和线的排列有不止一种解决方案。如果系统中有很多运动,它可能会开始在两个(或更多)可能的解决方案之间振荡。如果发生这种情况,您应该限制用户可以添加到系统中的移动量。
主循环。
综上所述,我们需要每帧 运行 模拟一次。我们有一组点,以及一组连接这些点的线。我们移动所有点,然后应用线约束。然后渲染结果准备好在下一帧再次执行所有操作。
STIFFNESS = 10; // how much the lines resist stretching
points.forEach(point => { // for each point in the sim
move(point); // add gravity drag
constrainGround(point); // stop it from going through the ground line
})
for(i = 0; i < STIFFNESS; i+= 1){ // number of times to apply line constraint
lines.forEach(line => { // for each line in the sim
constrainLine(line);
})
}
drawPoints(); // draw all the points
drawLines(); // draw all the lines.
提问具体问题
上述方法提供了很好的线条模拟,它还可以模拟刚体、布娃娃、桥梁、盒子、牛、山羊、猫和狗。通过固定点,您可以做各种吊绳和链条,制作滑轮、坦克履带。非常适合模拟 2D 汽车和自行车。但请记住,它们在视觉上都是可以接受的,但根本不是真正的物理模拟。
你想要一条链子。要制作链条,我们需要给线条一些宽度。所以每个顶点都需要一个半径,地面约束需要考虑到这一点。我们还希望链条不会落在自己身上,所以我们需要一个新的约束来防止球(又名顶点)相互重叠。这会给 sim 增加相当多的额外负载,因为每个球都需要相互测试,并且当我们调整位置以停止重叠时,我们会增加线长度的误差,因此我们需要多次依次执行每个约束以每帧得到一个好的解决方案。
最后一部分是图形的细节,每一行都需要引用一个图像,该图像是链的视觉表示。
我会把所有的事情都留给你来决定如何在 actionscript 中做到最好。
演示
以下演示显示了上述所有操作的结果。它可能不是你想要的,还有其他方法可以解决问题。这种方法有几个问题
- 离散质量,质量由点定义,如果没有更多的数学知识,你不能让点更轻或更重
- 振荡状态。有时系统刚开始震荡,尽量保持在合理的范围内震荡。
- 拉伸。尽管几乎可以消除拉伸,但在某些情况下解决方案并不完美。就像松紧带一样,如果松开弹力链,它就会弹开。我没有增加迭代次数来匹配链条长度,所以较长的链条会显示拉伸,如果你摆动它,你会看到链条分开。
您会感兴趣的函数是 constrainPoint
、constrainLine
、movePoint
和 doSim
(就在 if(points.length > 0){
之后的位 运行Sim) 剩下的只是支持和样板文件。
最好以全页形式观看(我把图片弄得太大了哎呀...:(
要查看链,请单击并按住鼠标右键添加第一个块,然后将 links 添加到链中。我没有限制链条的长度。单击并按住左键以抓取并拖动链条和块的任何部分。
var points = [];
var lines = [];
var pointsStart;
var fric = 0.999; // drag or air friction
var surF = 0.999; // ground and box friction
var grav = 0.9; // gravity
var ballRad = 10; // chain radius set as ball radius
var stiffness = 12; // number of itterations for line constraint
const fontSize = 33;
var chainImages = [new Image(),new Image(),new Image()];
chainImages[0].src = "https://i.stack.imgur.com/m0xqQ.png";
chainImages[1].src = "https://i.stack.imgur.com/fv77t.png";
chainImages[2].src = "https://i.stack.imgur.com/tVSqL.png";
// add a point
function addPoint(x,y,vx,vy,rad = 10,fixed = false){
points.push({
x:x,
y:y,
ox:x-vx,
oy:y-vy,
fixed : fixed,
radius : rad,
})
return points[points.length-1];
}
// add a constrained line
function addLine(p1,p2,image){
lines.push({
p1,p2,image,
len : Math.hypot(p1.x - p2.x,p1.y-p2.y),
draw(){
if(this.image !== undefined){
var img = chainImages[this.image];
var xdx = this.p2.x - this.p1.x;
var xdy = this.p2.y - this.p1.y;
var len = Math.hypot(xdx,xdy);
xdx /= len;
xdy /= len;
if(this.image === 2){ // oops block drawn in wrong direction. Fix just rotate here
// also did not like the placement of
// the block so this line's image
// is centered on the lines endpoint
ctx.setTransform(xdx,xdy,-xdy,xdx,this.p2.x, this.p2.y);
ctx.rotate(-Math.PI /2);
}else{
ctx.setTransform(xdx,xdy,-xdy,xdx,(this.p1.x + this.p2.x)/2,(this.p1.y + this.p2.y)/2);
}
ctx.drawImage(img,-img.width /2,- img.height / 2);
}
}
})
return lines[lines.length-1];
}
// Constrain a point to the edge of the canvas
function constrainPoint(p){
if(p.fixed){
return;
}
var vx = (p.x - p.ox) * fric;
var vy = (p.y - p.oy) * fric;
var len = Math.hypot(vx,vy);
var r = p.radius;
if(p.y <= r){
p.y = r;
p.oy = r + vy * surF;
}
if(p.y >= h - r){
var c = vy / len
p.y = h - r
p.oy = h - r + vy * surF;
p.ox += c * vx;
}
if(p.x < r){
p.x = r;
p.ox = r + vx * surF;
}
if(p.x > w - r){
p.x = w - r;
p.ox = w - r + vx * surF;
}
}
// move a point
function movePoint(p){
if(p.fixed){
return;
}
var vx = (p.x - p.ox) * fric;
var vy = (p.y - p.oy) * fric;
p.ox = p.x;
p.oy = p.y;
p.x += vx;
p.y += vy;
p.y += grav;
}
// move a line's end points constrain the points to the lines length
function constrainLine(l){
var dx = l.p2.x - l.p1.x;
var dy = l.p2.y - l.p1.y;
var ll = Math.hypot(dx,dy);
var fr = ((l.len - ll) / ll) / 2;
dx *= fr;
dy *= fr;
if(l.p2.fixed){
if(!l.p1.fixed){
l.p1.x -=dx * 2;
l.p1.y -=dy * 2;
}
}else if(l.p1.fixed){
if(!l.p2.fixed){
l.p2.x +=dx * 2;
l.p2.y +=dy * 2;
}
}else{
l.p1.x -=dx;
l.p1.y -=dy;
l.p2.x +=dx;
l.p2.y +=dy;
}
}
// locate the poitn closest to x,y (used for editing)
function closestPoint(x,y){
var min = 40;
var index = -2;
for(var i = 0; i < points.length; i ++){
var p = points[i];
var dist = Math.hypot(p.x-x,p.y-y);
p.mouseDist = dist;
if(dist < min){
min = dist;
index = i;
}
}
return index;
}
function constrainPoints(){
for(var i = 0; i < points.length; i ++){
constrainPoint(points[i]);
}
}
function movePoints(){
for(var i = 0; i < points.length; i ++){
movePoint(points[i]);
}
}
function constrainLines(){
for(var i = 0; i < lines.length; i ++){
constrainLine(lines[i]);
}
}
function drawLines(){
// draw back images first
for(var i = 0; i < lines.length; i ++){
if(lines[i].image !== 1){
lines[i].draw();
}
}
for(var i = 0; i < lines.length; i ++){
if(lines[i].image === 1){
lines[i].draw();
}
}
}
// Adds the block at end of chain
function createBlock(x,y){
var i = chainImages[2];
var w = i.width;
var h = i.height;
var p1 = addPoint(x,y+16,0,0,8);
var p2 = addPoint(x-w/2,y+27,0,0,1);
var p3 = addPoint(x+w/2,y+27,0,0,1);
var p4 = addPoint(x+w/2,y+h,0,0,1);
var p5 = addPoint(x-w/2,y+h,0,0,1);
var p6 = addPoint(x,y+h/2,0,0,1);
addLine(p1,p2);
addLine(p1,p3);
addLine(p1,p4);
addLine(p1,p5);
addLine(p1,p6,2);
addLine(p2,p3);
addLine(p2,p4);
addLine(p2,p5);
addLine(p2,p6);
addLine(p3,p4);
addLine(p3,p5);
addLine(p3,p6);
addLine(p4,p5);
addLine(p4,p6);
addLine(p5,p6);
var p7 = addPoint(x,y + 16-(chainImages[0].width-ballRad * 2),0,0,ballRad);
addLine(p1,p7,1);
}
var lastChainLink = 0;
function addChainLink(){
var lp = points[points.length-1];
addPoint(lp.x,lp.y-(chainImages[0].width-ballRad*2),0,0,ballRad);
addLine(points[points.length-2],points[points.length-1],lastChainLink % 2);
lastChainLink += 1;
}
function loading(){
ctx.setTransform(1,0,0,1,0,0)
ctx.clearRect(0,0,w,h);
ctx.fillStyle = "black";
ctx.fillText("Loading media pleaase wait!!",w/2,30);
if(chainImages.every(image=>image.complete)){
doSim = runSim;
}
}
var onResize = function(){ // called from boilerplate
blockAttached = false;
lines.length = 0; // remove all lines and points.
points.length = 0;
lastChainLink = 0; // controls which chain image to use next
holdingCount = 0;
holding = -1;
mouse.buttonRaw = 0;
}
var blockAttached = false;
var linkAddSpeed = 20;
var linkAddCount = 0;
var holding = -1; // the index of the link the mouse has grabbed
var holdingCount = 0;
function runSim(){
ctx.setTransform(1,0,0,1,0,0)
ctx.clearRect(0,0,w,h);
ctx.fillStyle = "black";
if(points.length < 12){
ctx.fillText("Right mouse button click hold to add chain.",w/2,30);
}
if(holdingCount < 180){
if(mouse.buttonRaw & 1 && holding === -2){
ctx.fillText("Nothing to grab here.",w/2,66);
}else{
ctx.fillText("Left mouse button to grab and move chain.",w/2,66);
}
}
if(mouse.buttonRaw & 4){
if(linkAddCount > 0){ // delay adding links
linkAddCount-=1;
}else{
if(!blockAttached ){
createBlock(mouse.x,mouse.y)
blockAttached = true;
}else{
addChainLink(mouse.x,mouse.y);
}
linkAddCount = linkAddSpeed;
}
}
if(points.length > 0){
if(mouse.buttonRaw & 1){
if(holding < 0){
holding = closestPoint(mouse.x,mouse.y);
}
}else{
holding = -1;
}
movePoints();
constrainPoints();
// attach the last link to the mouse
if(holding > -1){
var mousehold = points[holding];
mousehold.ox = mousehold.x = mouse.x;
mousehold.oy = mousehold.y = mouse.y;
holdingCount += 1; // used to hide help;
}
for(var i = 0; i < stiffness; i++){
constrainLines();
if(holding > -1){
mousehold.ox = mousehold.x = mouse.x;
mousehold.oy = mousehold.y = mouse.y;
}
}
drawLines();
}else{
holding = -1;
}
}
var doSim = loading;
/*********************************************************************************************/
/* Boilerplate not part of answer from here down */
/*********************************************************************************************/
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0, firstRun = true;
function start(x,y,col,w){ctx.lineWidth = w;ctx.strokeStyle = col;ctx.beginPath();ctx.moveTo(x,y)}
function line(x,y){ctx.lineTo(x,y)}
function end(){ctx.stroke()}
function drawLine(l) {ctx.lineWidth = 1;ctx.strokeStyle = "Black";ctx.beginPath();ctx.moveTo(l.p1.x,l.p1.y);ctx.lineTo(l.p2.x,l.p2.y); ctx.stroke();}
function drawPoint(p,col = "black", size = 3){ctx.fillStyle = col;ctx.beginPath();ctx.arc(p.x,p.y,size,0,Math.PI * 2);ctx.fill();}
;(function(){
const RESIZE_DEBOUNCE_TIME = 100;
var createCanvas, resizeCanvas, setGlobals, resizeCount = 0;
createCanvas = function () {
var c, cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === undefined) {
canvas = createCanvas();
}
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") {
setGlobals();
}
if (typeof onResize === "function") {
if(firstRun){
onResize();
firstRun = false;
}else{
resizeCount += 1;
setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
}
}
}
function debounceResize() {
resizeCount -= 1;
if (resizeCount <= 0) {
onResize();
}
}
setGlobals = function () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
ctx.font = fontSize + "px arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
}
mouse = (function () {
function preventDefault(e) {
e.preventDefault();
}
var mouse = {
x : 0,
y : 0,
w : 0,
buttonRaw : 0,
over : false,
bm : [1, 2, 4, 6, 5, 3],
active : false,
bounds : null,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left;
m.y = e.pageY - m.bounds.top;
if (t === "mousedown") {
m.buttonRaw |= m.bm[e.which - 1];
} else if (t === "mouseup") {
m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") {
m.buttonRaw = 0;
m.over = false;
} else if (t === "mouseover") {
m.over = true;
} else if (t === "mousewheel") {
m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") {
m.w = -e.detail;
}
e.preventDefault();
}
m.start = function (element) {
m.element = element === undefined ? document : element;
m.mouseEvents.forEach(n => {
m.element.addEventListener(n, mouseMove);
});
m.element.addEventListener("contextmenu", preventDefault, false);
m.active = true;
}
return mouse;
})();
function update(timer) { // Main update loop
doSim(); // call demo code
requestAnimationFrame(update);
}
setTimeout(function(){
resizeCanvas();
mouse.start(canvas, true);
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
},0);
})();