通过模拟在地面上滚动的真实世界轮子旋转一个圆来平移平面的数学是什么?
What is the math to translate a plane by rotating a circle simulating a real world wheel rolling on the ground?
这在互联网上就像是一个天大的秘密。
在所有游戏中,我都能发现轮子相对于地面运动的旋转速度较慢或较快。
给定 radius of the circle
和 intersection point with the plane at 90 deg
角度 translate the ground on X axis
来模拟真实世界中轮子在地面上的滚动。
在下面的示例中,将鼠标悬停在圆上以旋转它。地面波纹管应该像您在现实世界中期望的那样移动。
const plane = $('#plane')
const planeX = plane.offset().left
const wheel = $('#wheel>div')
const radius = wheel.width() / 2
let degrees = 0
const offset = wheel.parent().offset()
$(document).on('mouseenter', '.interactive', event => {
$('.interactive').css('background', 'rgba(172, 255, 47, 0.25)')
const radians = Math.atan2(
event.pageX - (offset.left + radius),
event.pageY - (offset.top + radius)
)
const degreeWheelOffset = radians * (180 / Math.PI) * -1 - degrees // to start from where left off
$(document).on('mousemove', event2 => {
const radians = Math.atan2(
event2.pageX - (offset.left + radius),
event2.pageY - (offset.top + radius)
)
degrees = radians * (180 / Math.PI) * -1 - degreeWheelOffset
wheel.css('transform', 'rotate(' + degrees + 'deg)').data('degree', degrees)
plane.css('left', planeX - (Math.sin(radians) * radius) + 'px')
})
})
$(document).on('mouseleave', '.interactive', () => {
$('.interactive').css('background', '')
$(document).off('mousemove')
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="wheel" style="position:fixed;bottom:1em;left:50%;transform:translateX(-50%);">
<div style="width:150px;height:150px;border:1px solid;border-radius:50%;background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAAAAAA7suyFAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAFuSURBVDjLfdRBa8IwGMbxx7cjEklIiVQqjsoCnvb9P8FACBQKngQHXpTJhJx2mRDZoXUmqfW9WX68f5tAR1tEw3jD5/EjUPKT1e/68NRQvil/X1MUGSbXeno9LRMUGmJ1NbsgSxHFIXEBeoiSENBHlITayc4hojTUTbiJbiGrulC36WT+UWuI28U82BIjakNNlccEwPGWIwCUWy16BNm520QAk+tydsWD6f44gUSzmF6AYUSU11oNkO6cRl92cAsA+PLgX+ybfEKQHVacijOejZc/R1qJHXtChLDf5Iza0SDhvK4mBGf0ng0R3RRjEOCWeiCn1Ech27twS7V9kPOM2WqC7k6d0Xvqh1RTjnEzcEZ9JjkvxFpPcDdwRsdv5zmrK4nQwBkV5jxXmzYUGDij7kfghbBdKDRw5v52nNWlRN/cD9MztSkneGTgjNoxwAtV64DE34T27gSzhcSQgTNqz1kUAjBKvmN6h4uQ8bM/WtmCgAk7YV0AAAAASUVORK5CYII=')">
<div class="interactive" style="border-radius:50%;position:absolute;top:0;left:0;bottom:0;right:0"></div>
<div style="position: absolute;top:50%;bottom:0;left:50%;border-left:1px solid;"></div>
</div>
</div>
<div id="plane" style="position:fixed;bottom:0;left:-100vw;width:300vw;height:1em;border:1px solid;display:flex;justify-content:space-around;background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAAAAAA7suyFAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAFuSURBVDjLfdRBa8IwGMbxx7cjEklIiVQqjsoCnvb9P8FACBQKngQHXpTJhJx2mRDZoXUmqfW9WX68f5tAR1tEw3jD5/EjUPKT1e/68NRQvil/X1MUGSbXeno9LRMUGmJ1NbsgSxHFIXEBeoiSENBHlITayc4hojTUTbiJbiGrulC36WT+UWuI28U82BIjakNNlccEwPGWIwCUWy16BNm520QAk+tydsWD6f44gUSzmF6AYUSU11oNkO6cRl92cAsA+PLgX+ybfEKQHVacijOejZc/R1qJHXtChLDf5Iza0SDhvK4mBGf0ng0R3RRjEOCWeiCn1Ech27twS7V9kPOM2WqC7k6d0Xvqh1RTjnEzcEZ9JjkvxFpPcDdwRsdv5zmrK4nQwBkV5jxXmzYUGDij7kfghbBdKDRw5v52nNWlRN/cD9MztSkneGTgjNoxwAtV64DE34T27gSzhcSQgTNqz1kUAjBKvmN6h4uQ8bM/WtmCgAk7YV0AAAAASUVORK5CYII=')"><b>1</b><b>2</b><b>3</b><b>4</b><b>5</b><b>6</b><b>7</b><b>8</b><b>9</b></div>
车辆前进2π米,车轮转动2π米,即角度为2π/r弧度。
车辆前进d m时,车轮转动d m,即d/r弧度的角度。
简单回答
对于 mousemove
的每次调用:
- 计算鼠标坐标自上次调用以来的变化
- 计算此鼠标移动产生的有符号角度变化
- 将此添加到全局累积角度
- 计算水平平移=(以弧度为单位的全局角度)*(车轮半径)
角度的有符号变化由下图中向量之间的叉积给出:
不过最好使用 atan2
,因为在 90 度点附近的数值稳定性:
(我确定这里至少有一位公认的几何大师 - 例如 Yves Daoust - 有 post 解释上述工作原理,所以我不会在这里这样做。)
工作代码:
const plane = $('#plane')
const planeX = plane.offset().left
const wheel = $('#wheel>div')
const radius = wheel.width() / 2
const offset = wheel.parent().offset()
let degrees = 0;
$(document).on('mouseenter', '.interactive', event => {
$('.interactive').css('background', 'rgba(172, 255, 47, 0.25)')
let mouseX1 = event.pageX, mouseY1 = event.pageY;
$(document).on('mousemove', event2 => {
const mouseX2 = event2.pageX, mouseY2 = event2.pageY;
// center position
const centerX = offset.left + radius,
centerY = offset.top + radius;
// vectors A - C and B - C
const deltaX1 = mouseX1 - centerX, deltaY1 = mouseY1 - centerY;
const deltaX2 = mouseX2 - centerX, deltaY2 = mouseY2 - centerY;
// change in angle formula
const deltaA = Math.atan2(deltaX1 * deltaY2 - deltaY1 * deltaX2,
deltaX1 * deltaX2 + deltaY1 * deltaY2);
// increment
degrees += deltaA * (180 / Math.PI);
const radians = degrees * (Math.PI / 180);
// set previous coordinates
mouseX1 = mouseX2; mouseY1 = mouseY2;
// apply
wheel.css('transform', 'rotate(' + degrees + 'deg)').data('degree', degrees)
plane.css('left', planeX - radians * radius + 'px') // simpler formula
})
})
$(document).on('mouseleave', '.interactive', () => {
$('.interactive').css('background', '')
$(document).off('mousemove')
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="wheel" style="position:fixed;bottom:1em;left:50%;transform:translateX(-50%);">
<div style="width:150px;height:150px;border:1px solid;border-radius:50%;background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAAAAAA7suyFAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAFuSURBVDjLfdRBa8IwGMbxx7cjEklIiVQqjsoCnvb9P8FACBQKngQHXpTJhJx2mRDZoXUmqfW9WX68f5tAR1tEw3jD5/EjUPKT1e/68NRQvil/X1MUGSbXeno9LRMUGmJ1NbsgSxHFIXEBeoiSENBHlITayc4hojTUTbiJbiGrulC36WT+UWuI28U82BIjakNNlccEwPGWIwCUWy16BNm520QAk+tydsWD6f44gUSzmF6AYUSU11oNkO6cRl92cAsA+PLgX+ybfEKQHVacijOejZc/R1qJHXtChLDf5Iza0SDhvK4mBGf0ng0R3RRjEOCWeiCn1Ech27twS7V9kPOM2WqC7k6d0Xvqh1RTjnEzcEZ9JjkvxFpPcDdwRsdv5zmrK4nQwBkV5jxXmzYUGDij7kfghbBdKDRw5v52nNWlRN/cD9MztSkneGTgjNoxwAtV64DE34T27gSzhcSQgTNqz1kUAjBKvmN6h4uQ8bM/WtmCgAk7YV0AAAAASUVORK5CYII=')">
<div class="interactive" style="border-radius:50%;position:absolute;top:0;left:0;bottom:0;right:0"></div>
<div style="position: absolute;top:50%;bottom:0;left:50%;border-left:1px solid;"></div>
</div>
</div>
<div id="plane" style="position:fixed;bottom:0;left:-100vw;width:300vw;height:1em;border:1px solid;display:flex;justify-content:space-around;background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAAAAAA7suyFAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAFuSURBVDjLfdRBa8IwGMbxx7cjEklIiVQqjsoCnvb9P8FACBQKngQHXpTJhJx2mRDZoXUmqfW9WX68f5tAR1tEw3jD5/EjUPKT1e/68NRQvil/X1MUGSbXeno9LRMUGmJ1NbsgSxHFIXEBeoiSENBHlITayc4hojTUTbiJbiGrulC36WT+UWuI28U82BIjakNNlccEwPGWIwCUWy16BNm520QAk+tydsWD6f44gUSzmF6AYUSU11oNkO6cRl92cAsA+PLgX+ybfEKQHVacijOejZc/R1qJHXtChLDf5Iza0SDhvK4mBGf0ng0R3RRjEOCWeiCn1Ech27twS7V9kPOM2WqC7k6d0Xvqh1RTjnEzcEZ9JjkvxFpPcDdwRsdv5zmrK4nQwBkV5jxXmzYUGDij7kfghbBdKDRw5v52nNWlRN/cD9MztSkneGTgjNoxwAtV64DE34T27gSzhcSQgTNqz1kUAjBKvmN6h4uQ8bM/WtmCgAk7YV0AAAAASUVORK5CYII=')"><b>1</b><b>2</b><b>3</b><b>4</b><b>5</b><b>6</b><b>7</b><b>8</b><b>9</b></div>
稍微复杂一点的答案
上面的代码近似认为在鼠标轮询周期(连续 mousemove
调用之间的间隔)滚轮保持静止。对于连续移动,这当然不是真的 - 滚轮的中心会随着鼠标不断移动。
由此产生了一个一阶非线性微分方程,它给出了每次鼠标移动的"physically correct"角度增量(我不会在这里展示推导) :
其中m
为鼠标位置,p
为滚轮位置,theta
为累计角度。通过一些变量替换和重新安排,这可以通过分析来解决;否则它可以很容易地与例如数字集成自适应 RK4.
...但是,当然,如果轮询频率足够高(通常是这样)——即每个轮询周期的增量角很小,那么近似值就足够准确了。
这在互联网上就像是一个天大的秘密。
在所有游戏中,我都能发现轮子相对于地面运动的旋转速度较慢或较快。
给定 radius of the circle
和 intersection point with the plane at 90 deg
角度 translate the ground on X axis
来模拟真实世界中轮子在地面上的滚动。
在下面的示例中,将鼠标悬停在圆上以旋转它。地面波纹管应该像您在现实世界中期望的那样移动。
const plane = $('#plane')
const planeX = plane.offset().left
const wheel = $('#wheel>div')
const radius = wheel.width() / 2
let degrees = 0
const offset = wheel.parent().offset()
$(document).on('mouseenter', '.interactive', event => {
$('.interactive').css('background', 'rgba(172, 255, 47, 0.25)')
const radians = Math.atan2(
event.pageX - (offset.left + radius),
event.pageY - (offset.top + radius)
)
const degreeWheelOffset = radians * (180 / Math.PI) * -1 - degrees // to start from where left off
$(document).on('mousemove', event2 => {
const radians = Math.atan2(
event2.pageX - (offset.left + radius),
event2.pageY - (offset.top + radius)
)
degrees = radians * (180 / Math.PI) * -1 - degreeWheelOffset
wheel.css('transform', 'rotate(' + degrees + 'deg)').data('degree', degrees)
plane.css('left', planeX - (Math.sin(radians) * radius) + 'px')
})
})
$(document).on('mouseleave', '.interactive', () => {
$('.interactive').css('background', '')
$(document).off('mousemove')
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="wheel" style="position:fixed;bottom:1em;left:50%;transform:translateX(-50%);">
<div style="width:150px;height:150px;border:1px solid;border-radius:50%;background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAAAAAA7suyFAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAFuSURBVDjLfdRBa8IwGMbxx7cjEklIiVQqjsoCnvb9P8FACBQKngQHXpTJhJx2mRDZoXUmqfW9WX68f5tAR1tEw3jD5/EjUPKT1e/68NRQvil/X1MUGSbXeno9LRMUGmJ1NbsgSxHFIXEBeoiSENBHlITayc4hojTUTbiJbiGrulC36WT+UWuI28U82BIjakNNlccEwPGWIwCUWy16BNm520QAk+tydsWD6f44gUSzmF6AYUSU11oNkO6cRl92cAsA+PLgX+ybfEKQHVacijOejZc/R1qJHXtChLDf5Iza0SDhvK4mBGf0ng0R3RRjEOCWeiCn1Ech27twS7V9kPOM2WqC7k6d0Xvqh1RTjnEzcEZ9JjkvxFpPcDdwRsdv5zmrK4nQwBkV5jxXmzYUGDij7kfghbBdKDRw5v52nNWlRN/cD9MztSkneGTgjNoxwAtV64DE34T27gSzhcSQgTNqz1kUAjBKvmN6h4uQ8bM/WtmCgAk7YV0AAAAASUVORK5CYII=')">
<div class="interactive" style="border-radius:50%;position:absolute;top:0;left:0;bottom:0;right:0"></div>
<div style="position: absolute;top:50%;bottom:0;left:50%;border-left:1px solid;"></div>
</div>
</div>
<div id="plane" style="position:fixed;bottom:0;left:-100vw;width:300vw;height:1em;border:1px solid;display:flex;justify-content:space-around;background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAAAAAA7suyFAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAFuSURBVDjLfdRBa8IwGMbxx7cjEklIiVQqjsoCnvb9P8FACBQKngQHXpTJhJx2mRDZoXUmqfW9WX68f5tAR1tEw3jD5/EjUPKT1e/68NRQvil/X1MUGSbXeno9LRMUGmJ1NbsgSxHFIXEBeoiSENBHlITayc4hojTUTbiJbiGrulC36WT+UWuI28U82BIjakNNlccEwPGWIwCUWy16BNm520QAk+tydsWD6f44gUSzmF6AYUSU11oNkO6cRl92cAsA+PLgX+ybfEKQHVacijOejZc/R1qJHXtChLDf5Iza0SDhvK4mBGf0ng0R3RRjEOCWeiCn1Ech27twS7V9kPOM2WqC7k6d0Xvqh1RTjnEzcEZ9JjkvxFpPcDdwRsdv5zmrK4nQwBkV5jxXmzYUGDij7kfghbBdKDRw5v52nNWlRN/cD9MztSkneGTgjNoxwAtV64DE34T27gSzhcSQgTNqz1kUAjBKvmN6h4uQ8bM/WtmCgAk7YV0AAAAASUVORK5CYII=')"><b>1</b><b>2</b><b>3</b><b>4</b><b>5</b><b>6</b><b>7</b><b>8</b><b>9</b></div>
车辆前进2π米,车轮转动2π米,即角度为2π/r弧度。
车辆前进d m时,车轮转动d m,即d/r弧度的角度。
简单回答
对于 mousemove
的每次调用:
- 计算鼠标坐标自上次调用以来的变化
- 计算此鼠标移动产生的有符号角度变化
- 将此添加到全局累积角度
- 计算水平平移=(以弧度为单位的全局角度)*(车轮半径)
角度的有符号变化由下图中向量之间的叉积给出:
不过最好使用 atan2
,因为在 90 度点附近的数值稳定性:
(我确定这里至少有一位公认的几何大师 - 例如 Yves Daoust - 有 post 解释上述工作原理,所以我不会在这里这样做。)
工作代码:
const plane = $('#plane')
const planeX = plane.offset().left
const wheel = $('#wheel>div')
const radius = wheel.width() / 2
const offset = wheel.parent().offset()
let degrees = 0;
$(document).on('mouseenter', '.interactive', event => {
$('.interactive').css('background', 'rgba(172, 255, 47, 0.25)')
let mouseX1 = event.pageX, mouseY1 = event.pageY;
$(document).on('mousemove', event2 => {
const mouseX2 = event2.pageX, mouseY2 = event2.pageY;
// center position
const centerX = offset.left + radius,
centerY = offset.top + radius;
// vectors A - C and B - C
const deltaX1 = mouseX1 - centerX, deltaY1 = mouseY1 - centerY;
const deltaX2 = mouseX2 - centerX, deltaY2 = mouseY2 - centerY;
// change in angle formula
const deltaA = Math.atan2(deltaX1 * deltaY2 - deltaY1 * deltaX2,
deltaX1 * deltaX2 + deltaY1 * deltaY2);
// increment
degrees += deltaA * (180 / Math.PI);
const radians = degrees * (Math.PI / 180);
// set previous coordinates
mouseX1 = mouseX2; mouseY1 = mouseY2;
// apply
wheel.css('transform', 'rotate(' + degrees + 'deg)').data('degree', degrees)
plane.css('left', planeX - radians * radius + 'px') // simpler formula
})
})
$(document).on('mouseleave', '.interactive', () => {
$('.interactive').css('background', '')
$(document).off('mousemove')
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="wheel" style="position:fixed;bottom:1em;left:50%;transform:translateX(-50%);">
<div style="width:150px;height:150px;border:1px solid;border-radius:50%;background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAAAAAA7suyFAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAFuSURBVDjLfdRBa8IwGMbxx7cjEklIiVQqjsoCnvb9P8FACBQKngQHXpTJhJx2mRDZoXUmqfW9WX68f5tAR1tEw3jD5/EjUPKT1e/68NRQvil/X1MUGSbXeno9LRMUGmJ1NbsgSxHFIXEBeoiSENBHlITayc4hojTUTbiJbiGrulC36WT+UWuI28U82BIjakNNlccEwPGWIwCUWy16BNm520QAk+tydsWD6f44gUSzmF6AYUSU11oNkO6cRl92cAsA+PLgX+ybfEKQHVacijOejZc/R1qJHXtChLDf5Iza0SDhvK4mBGf0ng0R3RRjEOCWeiCn1Ech27twS7V9kPOM2WqC7k6d0Xvqh1RTjnEzcEZ9JjkvxFpPcDdwRsdv5zmrK4nQwBkV5jxXmzYUGDij7kfghbBdKDRw5v52nNWlRN/cD9MztSkneGTgjNoxwAtV64DE34T27gSzhcSQgTNqz1kUAjBKvmN6h4uQ8bM/WtmCgAk7YV0AAAAASUVORK5CYII=')">
<div class="interactive" style="border-radius:50%;position:absolute;top:0;left:0;bottom:0;right:0"></div>
<div style="position: absolute;top:50%;bottom:0;left:50%;border-left:1px solid;"></div>
</div>
</div>
<div id="plane" style="position:fixed;bottom:0;left:-100vw;width:300vw;height:1em;border:1px solid;display:flex;justify-content:space-around;background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAAAAAA7suyFAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAFuSURBVDjLfdRBa8IwGMbxx7cjEklIiVQqjsoCnvb9P8FACBQKngQHXpTJhJx2mRDZoXUmqfW9WX68f5tAR1tEw3jD5/EjUPKT1e/68NRQvil/X1MUGSbXeno9LRMUGmJ1NbsgSxHFIXEBeoiSENBHlITayc4hojTUTbiJbiGrulC36WT+UWuI28U82BIjakNNlccEwPGWIwCUWy16BNm520QAk+tydsWD6f44gUSzmF6AYUSU11oNkO6cRl92cAsA+PLgX+ybfEKQHVacijOejZc/R1qJHXtChLDf5Iza0SDhvK4mBGf0ng0R3RRjEOCWeiCn1Ech27twS7V9kPOM2WqC7k6d0Xvqh1RTjnEzcEZ9JjkvxFpPcDdwRsdv5zmrK4nQwBkV5jxXmzYUGDij7kfghbBdKDRw5v52nNWlRN/cD9MztSkneGTgjNoxwAtV64DE34T27gSzhcSQgTNqz1kUAjBKvmN6h4uQ8bM/WtmCgAk7YV0AAAAASUVORK5CYII=')"><b>1</b><b>2</b><b>3</b><b>4</b><b>5</b><b>6</b><b>7</b><b>8</b><b>9</b></div>
稍微复杂一点的答案
上面的代码近似认为在鼠标轮询周期(连续 mousemove
调用之间的间隔)滚轮保持静止。对于连续移动,这当然不是真的 - 滚轮的中心会随着鼠标不断移动。
由此产生了一个一阶非线性微分方程,它给出了每次鼠标移动的"physically correct"角度增量(我不会在这里展示推导) :
其中m
为鼠标位置,p
为滚轮位置,theta
为累计角度。通过一些变量替换和重新安排,这可以通过分析来解决;否则它可以很容易地与例如数字集成自适应 RK4.
...但是,当然,如果轮询频率足够高(通常是这样)——即每个轮询周期的增量角很小,那么近似值就足够准确了。