旋转旋转的计算(没有物理引擎)

Calculation of a swivel rotation (without physics engine)

给定连接体的旋转角度,我尝试获得旋转轮的旋转角度:

我第一次使用车轮角度 attempt:i 已经存储了这些信息。为了获得车轮的旋转角度,我尝试通过使用系数作为简单的摩擦模拟来计算与旋转轴的距离:

var dx = cartCX - wheelCX,
    dy = cartCY - wheelCY,
    dist = Math.sqrt(cartCX * wheelCX + cartCY * wheelCY);

var wheelRotation = Math.atan2(-dy, -dx) * dist * friction;

考虑以下情况:

  1. 车轮处于稳定位置,与旋转对齐 整个系统顺时针旋转足够长时间后的汽车周长。

  2. 现在,整个系统逆时针旋转:那么旋转应该领先 一些轮子执行初始的 CCW 旋转(轮子 示例图片中的右上角)然后应充分 补间直到车轮对齐。车轮左上、左下和右下应为 顺时针旋转。

有没有不用物理引擎就能计算出车轮旋转角度的简单方法?

我不需要精确的物理刚体模拟,只需要一个简单的旋转效果。

简单旋转

假设轮子是脚轮。

描述一个

// ? for numbers save me making up numbers
var wheel = {
   swivel : {x : ?, y : ?}, // world position of swivel (rotation point 
   length : ?,  // distance from swivel to ground Contact when viewed from above
   angle : ?, // current angle
} 

当小车移动时旋转 and/or 行驶时,车轮旋转处有一个局部运动矢量

var delta = { x : ? , y : ? };

然后将此运动沿相反方向应用到与地面接触的车轮上。

var wheelForce = {};
wheelForce.x = -delta.x;
wheelForce.y = -delta.y;

轮子上会有两种运动,一种是旋转,一种是平移。它受到摇臂的限制,所以我们只需要旋转,因为平移将来自推车的运动。

首先归一化wheelForce

nwf = {};  // for (n)ormalised (w)heel (f)orce
var dist = Math.hypot(wheelForce.x,wheelForce.y);
nwf.x = wheelForce.x / dist;
nwf.y = wheelForce.y / dist;

然后得到从转环到车轮接地点的归一化向量

var dir = {};
dir.x = Math.cos(wheel.angle);
dir.y = Math.sin(wheel.angle);

然后用叉积得到nwf和dir夹角的sin

var fs = nwf.x * dir.y - nwf.y * dir.x;

现在只需从逆罪中获得角度并将轮子旋转该量。

wheel.angle -= Math.asin(fs); // I never get this right (may have to subtract, been up to long to think this out see demo)

更新

OP 要求进行一些改进。跳过更新

上面的行

允许轮子打滑(或转动)将减少转环上的转动力。

滑移量与车轮力和车轮方向之间的角度的余弦值有关。如果角度为 0,则 cos 将 return 1,这意味着车轮可以完全自由滚动/打滑,如果车轮方向与力成 90 度,则它们不会转动(就像横向拖动汽车)cos 90 return 0

但是如果我们允许轮子在没有阻力的情况下完全转动,那效果就不好了。因此,在限制因素的情况下,此修改将改善模拟。

从而计算出滑动量

    var wheelTurnResistance = 0.8;
    var slip = Math.abs(Math.cos(Math.asin(fs))) * wheelTurnResistance;

现在应用减少的转向力来解决打滑问题,我还添加了额外的减少来缓和旋转速度

    var beNiceFactor = 0.6; // this further reduces the tendency to turn valid values 1-0 where smaller number reduce the tendency to swivel
    wheel.angle -= Math.asin(fs) * Math.abs(fs) * (1 - slip) * beNiceFactor;

还将把它添加到下面的演示代码中。

这是此方法允许的范围。如果您想要更好的模拟,我们将不得不从头开始并使用更复杂的解决方案

就是这样。

演示版

它接缝方式很简单所以不得不在代码中尝试,下面是使用上述方法的演示。您可以通过限制添加车轮角度的量来改进它,乘以您添加的角度的正弦值会使它的响应速度稍差一些,并且添加滑移只需乘以另一个分数。

因此在函数updateTrolly change中改变了车轮角度

ww.angle -= Math.asin(cross); 

ww.angle -= Math.asin(cross) * Math.abs(cross) * 0.1; 

其中 Math.abs(cross) 是所加角度的正弦值,0.1 是滑动量(< 1 和 > 0 的任何值,1 没有滑动)

在购物车上或附近点击并拖动以移动它。轮子会跟着走。

该演示有一个 updateTrolly 函数来处理轮子,但取决于 displayTrolly 函数中计算的轮子位置。

您感兴趣的代码大约从一半开始,剩下的只是处理鼠标和 canvas。

DEMO 已更新 请参阅关于滑点的答案中的更新。

/** SimpleFullCanvasMouse.js begin **/
const CANVAS_ELEMENT_ID = "canv";
const U = undefined;
var w, h, cw, ch; // short cut vars 
var canvas, ctx, mouse;
var globalTime = 0; 
var createCanvas, resizeCanvas, setGlobals;
var L = typeof log === "function" ? log : function(d){ console.log(d); }
createCanvas = function () {
    var c,cs;
    cs = (c = document.createElement("canvas")).style; 
    c.id = CANVAS_ELEMENT_ID;    
    cs.position = "absolute";
    cs.top = cs.left = "0px";
    cs.zIndex = 1000;
    document.body.appendChild(c); 
    return c;
}
resizeCanvas = function () {
    if (canvas === U) { canvas = createCanvas(); }
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight; 
    ctx = canvas.getContext("2d"); 
    if (typeof setGlobals === "function") { setGlobals(); }
}
setGlobals = function(){ cw = (w = canvas.width) / 2; ch = (h = canvas.height) / 2; }
mouse = (function(){
    function preventDefault(e) { e.preventDefault(); }
    var mouse = {
        x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false, buttonRaw : 0,
        over : false,  // mouse is over the element
        bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
        mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
    };
    var m = mouse;
    function mouseMove(e) {
        var t = e.type;
        m.x = e.offsetX; m.y = e.offsetY;
        if (m.x === U) { m.x = e.clientX; m.y = e.clientY; }
        m.alt = e.altKey; m.shift = e.shiftKey; m.ctrl = e.ctrlKey;
        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; }
        if (m.callbacks) { m.callbacks.forEach(c => c(e)); }
        e.preventDefault();
    }
    m.addCallback = function (callback) {
        if (typeof callback === "function") {
            if (m.callbacks === U) { m.callbacks = [callback]; }
            else { m.callbacks.push(callback); }
        } else { throw new TypeError("mouse.addCallback argument must be a function"); }
    }
    m.start = function (element, blockContextMenu) {
        if (m.element !== U) { m.removeMouse(); }        
        m.element = element === U ? document : element;
        m.blockContextMenu = blockContextMenu === U ? false : blockContextMenu;
        m.mouseEvents.forEach( n => { m.element.addEventListener(n, mouseMove); } );
        if (m.blockContextMenu === true) { m.element.addEventListener("contextmenu", preventDefault, false); }
    }
    m.remove = function () {
        if (m.element !== U) {
            m.mouseEvents.forEach(n => { m.element.removeEventListener(n, mouseMove); } );
            if (m.contextMenuBlocked === true) { m.element.removeEventListener("contextmenu", preventDefault);}
            m.element = m.callbacks = m.contextMenuBlocked = U;
        }
    }
    return mouse;
})();
var done = function(){
    window.removeEventListener("resize",resizeCanvas)
    mouse.remove();
    document.body.removeChild(canvas);    
    canvas = ctx = mouse = U;
    L("All done!")
}

resizeCanvas(); // create and size canvas
mouse.start(canvas,true); // start mouse on canvas and block context menu
window.addEventListener("resize",resizeCanvas); // add resize event


// ================================================================================
// Start of answer code

const SYSTEM_DRAG = 0.99; // add drag to stop everything flying around. value > 0 and < 1 the closer to 1 the less the drag (friction)
const MOUSE_FORCE = 600; // multiplies mouse movement force bigger number more force
const TROLLY_WIDTH = 100;
const TROLLY_LENGTH = 200;
const WHEEL_INSET = 0;
const WHEEL_WIDTH = 10;

const WHEEL_SWING_LENGTH = 20;
//const WHEEL_LENGTH = WHEEL_SWING_LENGTH * (2/3);
const WHEEL_LENGTH =30;
const PIXEL_MASS = 2; // mass per pixel. Need mass for better sim

var trolly = {
    wheels : [],
    x : 200, 
    y : 200,
    r : 0,
    dx : 0,
    dy : 0,
    dr : 0,
    w : TROLLY_WIDTH,
    l : TROLLY_LENGTH,
    mass : TROLLY_WIDTH * TROLLY_LENGTH * PIXEL_MASS
};
function addWheel(t,x,y,dist,angle){
    t.wheels.push({
        x:x,
        y:y, // relative to the trolly
        rx : x,  // to keep it simple r is for real worl position
        ry : y,
        lrx : x, // and lr is for last real world position. That will give delta at wheel
        lry : y,
        length : dist,
        angle : angle, // absolute angle relative to the world
    })
    t.mass += WHEEL_WIDTH * WHEEL_LENGTH * PIXEL_MASS;
}
addWheel(trolly,-(TROLLY_LENGTH / 2 - WHEEL_INSET),-(TROLLY_WIDTH / 2 - WHEEL_INSET),WHEEL_SWING_LENGTH, 0);
addWheel(trolly,(TROLLY_LENGTH / 2 - WHEEL_INSET),-(TROLLY_WIDTH / 2 - WHEEL_INSET),WHEEL_SWING_LENGTH, 0);
addWheel(trolly,(TROLLY_LENGTH / 2 - WHEEL_INSET),(TROLLY_WIDTH / 2 - WHEEL_INSET),WHEEL_SWING_LENGTH, 0);
addWheel(trolly,-(TROLLY_LENGTH / 2 - WHEEL_INSET),(TROLLY_WIDTH / 2 - WHEEL_INSET),WHEEL_SWING_LENGTH, 0);

function drawTrolly(t){
    ctx.setTransform(1,0,0,1,t.x,t.y);
    ctx.rotate(t.r);
    ctx.lineWidth = 2;
    ctx.strokeStyle = "black";
    ctx.beginPath();
    ctx.moveTo(-t.l/2,-t.w/2);
    ctx.lineTo(t.l/2,-t.w/2);
    ctx.lineTo(t.l/2,t.w/2);
    ctx.lineTo(-t.l/2,t.w/2);
    ctx.closePath();
    ctx.setTransform(1,0,0,1,0,0); // reset transform
    var dx = Math.cos(t.r); // x axis
    var dy = Math.sin(t.r);
    for(var i = 0; i < t.wheels.length; i ++){
        var w = t.wheels[i];
        var x = w.x * dx + w.y * - dy; 
        var y = w.x * dy + w.y *  dx; 
        var wx = Math.cos(w.angle);  // vector to the wheel
        var wy = Math.sin(w.angle);
        w.lrx = w.rx;  // save last pos
        w.lry = w.ry;
        w.rx = t.x + x;  // save new pos
        w.ry = t.y + y;
        // get ground contact point 
        var gx = t.x + x + wx * w.length;
        var gy = t.y + y + wy * w.length;
        ctx.setTransform(1,0,0,1,w.rx, w.ry); // reset transform
        ctx.moveTo(0,0);
        ctx.setTransform(1,0,0,1,gx,gy); // move to the wheel
        ctx.lineTo(0,0);
        ctx.rotate(w.angle);
        ctx.moveTo(-WHEEL_LENGTH / 2, -WHEEL_WIDTH / 2);
        ctx.lineTo(WHEEL_LENGTH / 2, -WHEEL_WIDTH / 2);
        ctx.lineTo(WHEEL_LENGTH / 2, WHEEL_WIDTH / 2);
        ctx.lineTo(-WHEEL_LENGTH / 2, WHEEL_WIDTH / 2);
        ctx.closePath();    
    }
    ctx.stroke();
}
function updateTrolly(t){
    for(var i = 0; i < t.wheels.length; i ++){
        var ww = t.wheels[i];
        var dx = ww.rx - ww.lrx; // get delta change at wheels
        var dy = ww.ry - ww.lry;
        var dist = Math.hypot(dx,dy);
        if(dist > 0.00001){ // not to small to bother
            var nx = -dx / dist;
            var ny = -dy / dist;
            var wx = Math.cos(ww.angle);
            var wy = Math.sin(ww.angle);
            var cross = nx * wy - ny * wx;
            var slip = Math.abs(Math.cos(Math.asin(cross))) * 0.7;
            ww.angle -= Math.asin(cross) * Math.abs(cross) * (1-slip) * 0.6;
        }
    }
    t.x += t.dx;
    t.y += t.dy;
    t.r += t.dr;
    t.dx *= SYSTEM_DRAG;
    t.dy *= SYSTEM_DRAG;
    t.dr *= SYSTEM_DRAG;
    t.x = ((t.x % w) + w) % w; // keep onscreen
    t.y = ((t.y % h) + h) % h; // keep onscreen
    
}

function applyForceCenter(object, force, direction){ // force is a vector
    force /= object.mass; // now use F = m * a in the form a = F/m
    object.dx += Math.cos(direction) * force;
    object.dy += Math.sin(direction) * force;
}
function applyForce(object, force, direction, locx,locy){ // force is a vector, loc is a coordinate
    var radius = Math.hypot(object.y - locy, object.x - locx);
    if(radius <= 0.00001){
        applyForceCenter(object,force,direction);
        return;
    }
    var toCenter = Math.atan2(object.y - locy, object.x - locx);
    var pheta = toCenter - direction;
    var Fv = Math.cos(pheta) * force;
    var Fa = Math.sin(pheta) * force;
    Fv /= object.mass; // now use F = m * a in the form a = F/m
    var Fvx = Math.cos(toCenter) * Fv;
    var Fvy = Math.sin(toCenter) * Fv;
    object.dx += Fvx; 
    object.dy += Fvy;
    Fa /= (radius  * object.mass); // for the angular component get the rotation
                                                   // acceleration
    object.dr += Fa;// now add that to the box delta r
}
function applyForceToTrolly(t,x,y,dx,dy){
    var f = Math.hypot(dx,dy) * MOUSE_FORCE;
    var dir = Math.atan2(dy,dx);
    applyForce(t,f,dir,x,y);
}

var lx,ly;
function display(){  // put code in here
    ctx.setTransform(1,0,0,1,0,0); // reset transform
    ctx.globalAlpha = 1;           // reset alpha
    ctx.clearRect(0,0,w,h);
    if(mouse.buttonRaw & 1){
        applyForceToTrolly(trolly,mouse.x,mouse.y,mouse.x-lx,mouse.y-ly);
        
    }
    updateTrolly(trolly);
    drawTrolly(trolly);
    lx = mouse.x;
    ly = mouse.y;
}
function update(timer){ // Main update loop
    globalTime = timer;
    display();  // call demo code
    requestAnimationFrame(update);
}
requestAnimationFrame(update);

/** SimpleFullCanvasMouse.js end **/