OpenGL 与 3D第 3 / 4 章
旋转、平移与缩放算法
把画布输入转换为相机空间和世界空间中的具体动作。
camera-actions
function getAutoRotationAngle(deltaTime) {
if (deltaTime !== null) {
return ((2 * Math.PI) / 60) * scope.autoRotateSpeed * deltaTime
} else {
return ((2 * Math.PI) / 60 / 60) * scope.autoRotateSpeed
}
}
function rotateLeft(angle) {
spherical.theta -= angle * 0.6
}
function rotateUp(angle) {
spherical.phi -= angle * 0.6
}
//实际dolly的函数
function dollyToCanvasPos(optionalTargetWorldPos, targetCanvasPos, dollyDelta) {
const camera = scope.object;
let dolliedThroughSurface = false
if (optionalTargetWorldPos) {
const eyeToWorldPosVec = new Vector3().subVectors(optionalTargetWorldPos, camera.position);
const eyeWorldPosDist = eyeToWorldPosVec.length();
dolliedThroughSurface = (eyeWorldPosDist < dollyDelta);
}
if (camera.type === "PerspectiveCamera") {
const unprojectedWorldPos= _unproject(targetCanvasPos)
const offset = new Vector3().subVectors(unprojectedWorldPos, camera.position);
const moveVec = offset.clone().normalize().multiplyScalar( -dollyDelta);
camera.position.sub(moveVec)
scope.target.sub(moveVec)
//if (optionalTargetWorldPos) {
// const eyeTargetVec = new Vector3().subVectors(optionalTargetWorldPos, camera.position);
// const lenEyeTargetVec = eyeTargetVec.length();
//const eyeLookVec = eyeTargetVec.normalize().multiplyScalar(lenEyeTargetVec);
//scope.target.add(eyeLookVec)
//}
}
return dolliedThroughSurface
}
//将画布坐标转换为世界坐标
function _unproject(canvasPos, worldPos = new Vector3()) {
const camera = scope.object;
// 获取投影矩阵
const projMatrix = camera.projectionMatrix;
// 获取转置矩阵的第三行和第四行
// Three.js中矩阵是列主序4x4矩阵,存储在Float32Array中
const Pt3 = [
projMatrix.elements[2], // m20
projMatrix.elements[6], // m21
projMatrix.elements[10], // m22
projMatrix.elements[14] // m23
];
const Pt4 = [
projMatrix.elements[3], // m30
projMatrix.elements[7], // m31
projMatrix.elements[11], // m32
projMatrix.elements[15] // m33
];
const D = [0, 0, -1.0, 1];
// 计算screenZ
const screenZ = (
D[0] * Pt3[0] + D[1] * Pt3[1] + D[2] * Pt3[2] + D[3] * Pt3[3]
) / (
D[0] * Pt4[0] + D[1] * Pt4[1] + D[2] * Pt4[2] + D[3] * Pt4[3]
);
// 将画布坐标转换为标准化设备坐标 (NDC)
const ndcX = (canvasPos.x / scope.domElement.clientWidth) * 2 - 1;
const ndcY = -(canvasPos.y / scope.domElement.clientHeight) * 2 + 1;
// 创建NDC坐标
const ndcPos = new Vector3(ndcX, ndcY, screenZ);
// 创建投影矩阵和视图矩阵的逆矩阵
const projInverse = camera.projectionMatrix.clone().invert();
const viewInverse = camera.matrixWorld; // 相机的世界矩阵就是视图矩阵的逆
// 应用逆变换
ndcPos.applyMatrix4(projInverse);
ndcPos.applyMatrix4(viewInverse);
// 将结果复制到worldPos
worldPos.set(ndcPos.x, ndcPos.y, ndcPos.z);
return worldPos;
}
//
// event callbacks - update the object state
//
function handleMouseDownDolly(event) {
dollyStart.set(event.clientX, event.clientY)
}
/**
* 鼠标点击平移
* @param {*} event
*/
function handleMouseDownPan(event) {
panStart.set(event.clientX, event.clientY)
const mouseDownPicked = getIntersectPoint(event)
dollyState.changePos = false
if(mouseDownPicked){
panTargrt.copy(intersectPoint)
} else {
panTargrt.copy(scope.target)
}
}
/**
* 获取鼠标点击的射线与场景的交点
* @param {*} event
* @returns
*/
function getIntersectPoint(event) {
const intersects = scope.three.setMouseRayCaster(event.clientX, event.clientY)
if (intersects.length) {
intersectPoint.copy(intersects[0].point)
return true
}
return false
}
function genViewMat4(pos, target, up,dest=tempMatrix4){
const zAxis = new Vector3().subVectors(pos, target).normalize()
const xAxis = new Vector3().crossVectors(up, zAxis).normalize()
const yAxis = new Vector3().crossVectors(zAxis, xAxis).normalize()
//得到一个正交矩阵,这个矩阵是相机坐标系下的lookat矩阵
dest.makeBasis(xAxis, yAxis, zAxis, dest)
//求逆矩阵,得到view矩阵,但是由于正交矩阵的逆矩阵就是其转置矩阵,所以直接转置即可,避免计算逆矩阵
dest.transpose()
dest.elements[12] = -pos.dot(xAxis)
dest.elements[13] = -pos.dot(yAxis)
dest.elements[14] = -pos.dot(zAxis)
return dest
}
/**
* 鼠标点击旋转
* @param {*} event
*/
function handleMouseDownRotate(event) {
rotateStart.set(event.clientX, event.clientY)
const position = scope.object.position
const hasIntersectPoint = getIntersectPoint(event)
if(hasIntersectPoint){
scope.pivot.copy(intersectPoint)
}
// 相机的lookat矩阵,即相机的视图矩阵
let lookat = scope.object.matrixWorldInverse.clone()
// 将pivot点转换到相机坐标系下
cameraOffset.copy(scope.pivot).applyMatrix4(lookat)
// 计算相机位置到pivot点的距离
const distance = position.distanceTo(scope.pivot);
// 将距离加到相机偏移量的z轴上
cameraOffset.z += distance;
tempMatrix3.setFromMatrix4(scope.object.matrixWorld)
// 将pivot点在相机坐标系下的偏移后的坐标转换到世界坐标系下
const offset = cameraOffset.clone().applyMatrix3(tempMatrix3)
tempDiff.subVectors(position, scope.pivot).add(offset);
spherical.setFromVector3(tempDiff)
}
// 鼠标移动旋转
function handleMouseMoveRotate(event) {
rotateEnd.set(event.clientX, event.clientY)
rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed)
const element = scope.domElement
const rotateX = (2 * Math.PI * rotateDelta.x) / element.clientHeight // yes, height
const rotateY = (2 * Math.PI * rotateDelta.y) / element.clientHeight
rotateLeft(rotateX)
rotateUp(rotateY)
// 确保球面坐标系中的角度在安全范围内
spherical.makeSafe()
// pos是pivotPos的坐标
const pos = new Vector3().setFromSpherical(spherical);
// 计算当前相机视点(eye)到观察点(look)的距离,这个距离在旋转过程中需要保持不变
const eyeLookLen = scope.object.position.distanceTo(scope.target);
// 获取当前的pivot点位置(旋转中心)
const pivotPos = scope.pivot;
// 将之前计算的球面坐标pos(相对于pivot点的偏移)加上pivot点的位置,得到新的相机位置
pos.add(pivotPos);
// 使用新的相机位置(pos)、pivot点位置和世界坐标系的up向量创建视图矩阵
// 这个矩阵定义了相机的新姿态
let lookat = lookAtMat4v(pos, pivotPos, new Vector3(0, 1, 0));
// 将视图矩阵求逆,得到相机的世界变换矩阵
// 这个矩阵包含了相机的位置和方向信息
lookat.invert()
// 将相机偏移量从相机空间转换到世界空间
const offset = cameraOffset.clone().applyMatrix3(tempMatrix3.setFromMatrix4(lookat))
// 应用偏移量,调整相机的最终位置
lookat.setPosition(lookat.elements[12] - offset.x, lookat.elements[13] - offset.y, lookat.elements[14] - offset.z)
// 从lookat矩阵中提取相机的前向方向(z轴),也就是相机空间的z轴,在世界空间下的表示
const zAxis = new Vector3().setFromMatrixColumn(lookat, 2).normalize()
// 设置新的相机位置(position)
const position = new Vector3().setFromMatrixPosition(lookat)
scope.object.position.copy(position);
// 计算新的观察点(look)位置
// 通过从相机位置沿着视线方向(zAxis)前进eyeLookLen距离
scope.target.subVectors(scope.object.position, zAxis.multiplyScalar(eyeLookLen));
// 设置相机的up向量
scope.object.up.set(lookat.elements[4], lookat.elements[5], lookat.elements[6])
rotateStart.set(event.clientX, event.clientY)
}
function handleMouseMoveDolly(event) {
dollyEnd.set(event.clientX, event.clientY)
dollyDelta.subVectors(dollyEnd, dollyStart)
if (dollyDelta.y > 0) {
calculatePanDelta(-1,event)
} else if (dollyDelta.y < 0) {
calculatePanDelta(1,event)
}
dollyStart.copy(dollyEnd)
}
/**
* 新的pan函数,平移
* @param {*} pan
*/
function newPan(pan) {
const eye2 = tempVec3.subVectors(scope.object.position, scope.target);
const vec = new Vector3()
if (pan.x !== 0) {
const left = tempVec3b.crossVectors(eye2.normalize(), scope.object.up.normalize());
vec.add(left.multiplyScalar(pan.x))
}
if (pan.y !== 0) {
vec.add(tempVec3b.copy(scope.object.up).multiplyScalar(pan.y))
}
if (pan.z !== 0) {
vec.add(tempVec3b.copy(eye2).multiplyScalar(pan.z))
}
scope.object.position.add(vec)
scope.target.add(vec)
}
function panLeftUp(panOffset) {
const panDepth = scope.object.position.distanceTo(panTargrt)
const targetDistance = panDepth * Math.tan((scope.object.fov / 2) * Math.PI / 180.0);
panDelta.x += (panOffset.x * targetDistance / scope.domElement.clientHeight);
panDelta.y += ( panOffset.y * targetDistance / scope.domElement.clientHeight);
if (Math.abs(panDelta.x) < EPSILON) {
panDelta.x = 0;
}
if (Math.abs(panDelta.y) < EPSILON) {
panDelta.y = 0;
}
if (Math.abs(panDelta.z) < EPSILON) {
panDelta.z = 0;
}
newPan(panDelta)
panDelta.set(0,0,0)
}
// 鼠标移动平移
function handleMouseMovePan(event) {
panEnd.set(event.clientX, event.clientY)
panOffset.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed)
panOffset.z = 0
panLeftUp(panOffset)
panStart.set(event.clientX, event.clientY)
}
// 响应dolly操作,delta为放大或缩小
function calculatePanDelta(delta,event){
const secsNow = performance.now() / 1000.0;
let secsElapsed = (secsNowLast !== null) ? (secsNow - secsNowLast) : 0;
secsNowLast = secsNow;
if (secsElapsed > maxElapsed) {
secsElapsed = maxElapsed;
}
if (secsElapsed < minElapsed) {
secsElapsed = minElapsed;
}
if (Math.abs(dollyDelta1) < EPSILON) {
dollyDelta1 = 0;
}
if (delta === 0) {
return;
}
const normalizedDelta = delta / Math.abs(delta);
dollyDelta1 += -normalizedDelta * secsElapsed * mouseWheelDollyRate;
let target = null
if(dollyState.changePos ){
const hasIntersectPoint = getIntersectPoint(event)
if(hasIntersectPoint){
target = intersectPoint
dollyState.changePos = false
} else {
dollyDistFactor = 1.0
target = null
}
}
if(target){
const dist = Math.abs(scope.object.position.distanceTo(target));
dollyDistFactor = dist / dollyProximityThreshold;
}
dollyDistFactor = Math.max(Math.min(dollyDistFactor, dollyMaxSpeed), dollyMinSpeed)
let dollyDelta = dollyDelta1 * dollyDistFactor
const isPass = dollyToCanvasPos(target, new Vector2(event.clientX, event.clientY), -dollyDelta)
if(isPass){
dollyState.changePos = true
}
dollyDelta1 = 0
}
function handleMouseWheel(event) {
//放大delta >0 缩小delta <0
const delta = Math.max(-1, Math.min(1, -event.deltaY * 40));
calculatePanDelta(delta,event)
}
function handleKeyDown(event) {
let needsUpdate = false
let speed = 1
if(event.shiftKey){
//加速
speed *= scope.shiftSpeed
}
const filterRes = Object.entries(scope.keys).filter(([_, value]) => value.includes(event.code))
if (filterRes.length) {
const [aspect] = filterRes[0]
switch(aspect){
case 'UP':
panLeftUp(new Vector3(0, -speed*scope.keyPanSpeed, 0))
break
case 'BOTTOM':
panLeftUp(new Vector3(0, speed*scope.keyPanSpeed, 0))
break
case 'LEFT':
panLeftUp(new Vector3(-speed*scope.keyPanSpeed,0, 0))
break
case 'RIGHT':
panLeftUp(new Vector3(speed*scope.keyPanSpeed,0, 0))
break
case 'ZOOM_IN':
dollyToCanvasPos(null, new Vector2(scope.domElement.clientWidth/2, scope.domElement.clientHeight/2), speed*scope.keyDollySpeed)
break
case 'ZOOM_OUT':
dollyToCanvasPos(null, new Vector2(scope.domElement.clientWidth/2, scope.domElement.clientHeight/2), -speed*scope.keyDollySpeed)
break
}
needsUpdate = true
}
if (needsUpdate) {
// prevent the browser from scrolling on cursor keys
event.preventDefault()
}
}
//
// event handlers - FSM: listen for events and reset state
//