three.js 中的四元数旋转在旋转超过 90° 时变得混乱
Quaternion rotation in three.js going haywire when rotating past about 90°
我正在使用四元数使用双指触摸事件对 THREE.Mesh
对象进行捏合-旋转-缩放。这是我第一次使用这种旋转方法,并且由于我确定有些 属性 的四元数我似乎无法理解,所以当总触摸拖动旋转时,旋转逐渐开始到处抖动超过约 90° 的物体。然后,当再次拖回 90° 以下时,它逐渐 returns 到原始的平滑旋转,尽管明显是非线性的。 (90° 只是一个猜测)。
我完全被难住了。这是我用来旋转对象的代码 obj
:
// global state
var
// keeps track of original object scale
pinchScale = new THREE.Vector3(),
// current first touch
touch1 = new THREE.Vector2(),
// current second touch
touch2 = new THREE.Vector2(),
// first touch at touch start
touch1OnHold = new THREE.Vector2(),
// second touch at touch start
touch2OnHold = new THREE.Vector2(),
// keeps track of total held rotation at last fired touchmove event
angularHoldPrev = new THREE.Quaternion();
⋮
// key-value pairs inside an addEventListener utility
touchstart: function (event) {
event.preventDefault();
if ( event.touches.length === 1 ) {
…
} else if ( event.touches.length === 2 ) {
touch1OnHold.set(event.touches[0].pageX, event.touches[0].pageY);
touch2OnHold.set(event.touches[1].pageX, event.touches[1].pageY);
angularHoldPrev.set(0, 0, 0, 1)
}
},
touchmove: function (event) {
event.preventDefault();
if ( event.touches.length === 1 ) {
…
} else if ( event.touches.length === 2 ) {
touch1.set(event.touches[0].pageX, event.touches[0].pageY);
touch2.set(event.touches[1].pageX, event.touches[1].pageY);
var
// get touch spread at present event firing, and at the start of current hold
touchDiff = touch2.clone().sub(touch1),
touchDiffOnHold = touch2OnHold.clone().sub(touch1OnHold),
// camera is on z-axis; get this axis regardless of obj orientation
axis1 = new THREE.Vector3(0, 0, 1).applyQuaternion(obj.quaternion.clone().inverse()),
// get a touch rotation around this axis
rot1 = new THREE.Quaternion().setFromAxisAngle(axis1, (Math.atan2(touchDiffOnHold.y, touchDiffOnHold.x) - Math.atan2(touchDiff.y, touchDiff.x))).normalize(),
// get touch barycentre at present event firing, and at the start of current hold
touchCentre = touch1.clone().add(touch2).multiplyScalar(.5),
touchCentreOnHold = touch1OnHold.clone().add(touch2OnHold).multiplyScalar(.5),
// get axis of touch barycentre movement on the xy plane, regardless of obj orientation
axis2 = new THREE.Vector3(touchCentre.y - touchCentreOnHold.y, touchCentre.x - touchCentreOnHold.x, 0).applyQuaternion(obj.quaternion.clone().inverse()),
// get a rotation proportional to magnitude of touch movement
rot2 = new THREE.Quaternion().setFromAxisAngle(axis2, axis2.length() * rotationSensitivity).normalize(),
// combine the two rotations
rot = rot1.multiply(rot2);
// undo last rotation if not the empty quaternion
if (!angularHoldPrev.equals(new THREE.Quaternion())) obj.quaternion.multiply(angularHoldPrev.inverse());
// perform the currently calculated rotation
obj.quaternion.multiply(rot);
// save this rotation for next event firing
angularHoldPrev.copy(rot);
// resize object according to change in touch spread
obj.scale.copy(pinchScale.clone().multiplyScalar(touchDiff.length() / touchDiffOnHold.length()))
}
},
touchend: function (event) {
event.preventDefault();
// reset original object scale
pinchScale.copy(obj.scale)
}
如果我能为所有两点触控输入保持比例旋转提供任何线索,将不胜感激。干杯。
我通过使所有 touchmove 旋转增量而不是相对于 touchstart 来让它工作,它实际上清理了一些东西。 rotV
是我为动画循环设置的 angular 速度变量,当您四处移动时,它会为对象提供良好的惯性踢动。 (感谢@WestLangley 的优化建议)
var touch1 = new THREE.Vector2(),
touch2 = new THREE.Vector2(),
touch1Prev = new THREE.Vector2(),
touch2Prev = new THREE.Vector2(),
rotV = new THREE.Quaternion(),
touchDiff = new THREE.Vector2(),
touchDiffPrev = new THREE.Vector2(),
touchCentre = new THREE.Vector2(),
touchCentrePrev = new THREE.Vector2(),
axis1 = new THREE.Vector3(),
axis2 = new THREE.Vector3(),
rot1 = new THREE.Quaternion(),
rot2 = new THREE.Quaternion(),
adjq = new THREE.Quaternion();
⋮
touchstart: function (event) {
event.preventDefault();
if ( event.touches.length === 1 ) {
…
} else if ( event.touches.length === 2 ) {
touch1Prev.set(event.touches[0].pageX, event.touches[0].pageY);
touch2Prev.set(event.touches[1].pageX, event.touches[1].pageY)
}
},
touchmove: function (event) {
event.preventDefault();
adjq.copy(obj.quaternion).inverse();
if ( event.touches.length === 1 ) {
…
} else if ( event.touches.length === 2 ) {
touch1.set(event.touches[0].pageX, event.touches[0].pageY);
touch2.set(event.touches[1].pageX, event.touches[1].pageY);
touchDiff.copy(touch2).sub(touch1);
touchDiffPrev.copy(touch2Prev).sub(touch1Prev);
axis1.set(0, 0, 1).applyQuaternion(adjq);
rot1.setFromAxisAngle(axis1, (Math.atan2(touchDiffPrev.y, touchDiffPrev.x) - Math.atan2(touchDiff.y, touchDiff.x))).normalize();
touchCentre.copy(touch1).add(touch2).multiplyScalar(.5);
touchCentrePrev.copy(touch1Prev).add(touch2Prev).multiplyScalar(.5);
axis2.set(touchCentre.y - touchCentrePrev.y, touchCentre.x - touchCentrePrev.x, 0).applyQuaternion(adjq);
rot2.setFromAxisAngle(axis2, axis2.length() * rotationSensitivity * 10).normalize();
obj.quaternion.multiply(rot1.multiply(rot2));
rotV.multiply(rot1.slerp(adjq.set(0, 0, 0, 1), .9));
obj.scale.multiplyScalar(touchDiff.length() / touchDiffPrev.length());
touch1Prev.copy(touch1);
touch2Prev.copy(touch2)
}
},
touchend: function (event) {
event.preventDefault()
}
我仍然希望我知道是什么导致了最初的问题 :P
我正在使用四元数使用双指触摸事件对 THREE.Mesh
对象进行捏合-旋转-缩放。这是我第一次使用这种旋转方法,并且由于我确定有些 属性 的四元数我似乎无法理解,所以当总触摸拖动旋转时,旋转逐渐开始到处抖动超过约 90° 的物体。然后,当再次拖回 90° 以下时,它逐渐 returns 到原始的平滑旋转,尽管明显是非线性的。 (90° 只是一个猜测)。
我完全被难住了。这是我用来旋转对象的代码 obj
:
// global state
var
// keeps track of original object scale
pinchScale = new THREE.Vector3(),
// current first touch
touch1 = new THREE.Vector2(),
// current second touch
touch2 = new THREE.Vector2(),
// first touch at touch start
touch1OnHold = new THREE.Vector2(),
// second touch at touch start
touch2OnHold = new THREE.Vector2(),
// keeps track of total held rotation at last fired touchmove event
angularHoldPrev = new THREE.Quaternion();
⋮
// key-value pairs inside an addEventListener utility
touchstart: function (event) {
event.preventDefault();
if ( event.touches.length === 1 ) {
…
} else if ( event.touches.length === 2 ) {
touch1OnHold.set(event.touches[0].pageX, event.touches[0].pageY);
touch2OnHold.set(event.touches[1].pageX, event.touches[1].pageY);
angularHoldPrev.set(0, 0, 0, 1)
}
},
touchmove: function (event) {
event.preventDefault();
if ( event.touches.length === 1 ) {
…
} else if ( event.touches.length === 2 ) {
touch1.set(event.touches[0].pageX, event.touches[0].pageY);
touch2.set(event.touches[1].pageX, event.touches[1].pageY);
var
// get touch spread at present event firing, and at the start of current hold
touchDiff = touch2.clone().sub(touch1),
touchDiffOnHold = touch2OnHold.clone().sub(touch1OnHold),
// camera is on z-axis; get this axis regardless of obj orientation
axis1 = new THREE.Vector3(0, 0, 1).applyQuaternion(obj.quaternion.clone().inverse()),
// get a touch rotation around this axis
rot1 = new THREE.Quaternion().setFromAxisAngle(axis1, (Math.atan2(touchDiffOnHold.y, touchDiffOnHold.x) - Math.atan2(touchDiff.y, touchDiff.x))).normalize(),
// get touch barycentre at present event firing, and at the start of current hold
touchCentre = touch1.clone().add(touch2).multiplyScalar(.5),
touchCentreOnHold = touch1OnHold.clone().add(touch2OnHold).multiplyScalar(.5),
// get axis of touch barycentre movement on the xy plane, regardless of obj orientation
axis2 = new THREE.Vector3(touchCentre.y - touchCentreOnHold.y, touchCentre.x - touchCentreOnHold.x, 0).applyQuaternion(obj.quaternion.clone().inverse()),
// get a rotation proportional to magnitude of touch movement
rot2 = new THREE.Quaternion().setFromAxisAngle(axis2, axis2.length() * rotationSensitivity).normalize(),
// combine the two rotations
rot = rot1.multiply(rot2);
// undo last rotation if not the empty quaternion
if (!angularHoldPrev.equals(new THREE.Quaternion())) obj.quaternion.multiply(angularHoldPrev.inverse());
// perform the currently calculated rotation
obj.quaternion.multiply(rot);
// save this rotation for next event firing
angularHoldPrev.copy(rot);
// resize object according to change in touch spread
obj.scale.copy(pinchScale.clone().multiplyScalar(touchDiff.length() / touchDiffOnHold.length()))
}
},
touchend: function (event) {
event.preventDefault();
// reset original object scale
pinchScale.copy(obj.scale)
}
如果我能为所有两点触控输入保持比例旋转提供任何线索,将不胜感激。干杯。
我通过使所有 touchmove 旋转增量而不是相对于 touchstart 来让它工作,它实际上清理了一些东西。 rotV
是我为动画循环设置的 angular 速度变量,当您四处移动时,它会为对象提供良好的惯性踢动。 (感谢@WestLangley 的优化建议)
var touch1 = new THREE.Vector2(),
touch2 = new THREE.Vector2(),
touch1Prev = new THREE.Vector2(),
touch2Prev = new THREE.Vector2(),
rotV = new THREE.Quaternion(),
touchDiff = new THREE.Vector2(),
touchDiffPrev = new THREE.Vector2(),
touchCentre = new THREE.Vector2(),
touchCentrePrev = new THREE.Vector2(),
axis1 = new THREE.Vector3(),
axis2 = new THREE.Vector3(),
rot1 = new THREE.Quaternion(),
rot2 = new THREE.Quaternion(),
adjq = new THREE.Quaternion();
⋮
touchstart: function (event) {
event.preventDefault();
if ( event.touches.length === 1 ) {
…
} else if ( event.touches.length === 2 ) {
touch1Prev.set(event.touches[0].pageX, event.touches[0].pageY);
touch2Prev.set(event.touches[1].pageX, event.touches[1].pageY)
}
},
touchmove: function (event) {
event.preventDefault();
adjq.copy(obj.quaternion).inverse();
if ( event.touches.length === 1 ) {
…
} else if ( event.touches.length === 2 ) {
touch1.set(event.touches[0].pageX, event.touches[0].pageY);
touch2.set(event.touches[1].pageX, event.touches[1].pageY);
touchDiff.copy(touch2).sub(touch1);
touchDiffPrev.copy(touch2Prev).sub(touch1Prev);
axis1.set(0, 0, 1).applyQuaternion(adjq);
rot1.setFromAxisAngle(axis1, (Math.atan2(touchDiffPrev.y, touchDiffPrev.x) - Math.atan2(touchDiff.y, touchDiff.x))).normalize();
touchCentre.copy(touch1).add(touch2).multiplyScalar(.5);
touchCentrePrev.copy(touch1Prev).add(touch2Prev).multiplyScalar(.5);
axis2.set(touchCentre.y - touchCentrePrev.y, touchCentre.x - touchCentrePrev.x, 0).applyQuaternion(adjq);
rot2.setFromAxisAngle(axis2, axis2.length() * rotationSensitivity * 10).normalize();
obj.quaternion.multiply(rot1.multiply(rot2));
rotV.multiply(rot1.slerp(adjq.set(0, 0, 0, 1), .9));
obj.scale.multiplyScalar(touchDiff.length() / touchDiffPrev.length());
touch1Prev.copy(touch1);
touch2Prev.copy(touch2)
}
},
touchend: function (event) {
event.preventDefault()
}
我仍然希望我知道是什么导致了最初的问题 :P