调整相机以匹配光线和屏幕点

时间:2016-01-07 07:22:39

标签: 3d camera three.js

我正在开发使用three.js进行360度全景的自由视图工具。我想让用户从屏幕上拖动时离开相机,在鼠标指针下方完全点。

几何是世界起源的简单盒子几何体,相机是位于原点的透视相机:

this.mesh = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2, 0, 0, 0),
    new THREE.MeshFaceMaterial(_array_of_THREE.MeshBasicMaterial_));
this.camera = new THREE.PerspectiveCamera(90, width / height, 0.1, 2);

我的解决方案工作不准确,它基于以下步骤:

  1. 当用户开始拖动时,我记得世界坐标中的光线 指向拖动开始的屏幕点。
  2. 每当我想调整相机时(目前只在拖拽结束时),我会计算指向鼠标指针当前位置的世界坐标中的光线。
  3. 然后我计算将第一条射线带到的所需的轴和角度 第二轮。
  4. 我将相机方向的矢量旋转到该轴周围的角度。
  5. 最后,我设置相机以查看新方向。
  6. 以下是步骤2-5的代码:

    adjustCamera: function(cameraDirection, worldRay, screenPoint){
            var angle;
            this.camera.lookAt(cameraDirection);
            this.raycaster.setFromCamera(screenPoint, this.camera);
            this.axis.copy(this.raycaster.ray.direction);
            angle = this.axis.angleTo(worldRay);
            this.axis.cross(worldRay).normalize();
            this.ray.copy(cameraDirection);
            this.ray.applyAxisAngle(this.axis, angle).normalize();
            this.camera.lookAt(this.ray);
        }
    

    我意识到为什么这个架构不起作用。通过这种方式改变相机方向得到一些 roll (当旋转轴具有非零z坐标时),这被lookAt消除 - 它剥离滚动,只留下音高并且偏航。这导致一些不准确性,并且当初始和最终光线更远时以及当初始相机位置具有更高音高时它会增长。我现在卡在这里,不知道如何在没有滚动的情况下计算相机位置。

    所以,问题是:如何准确旋转相机以将特定光线带到特定的屏幕点?(最适合我使用的模式)

    编辑: 实际上可能有多个(似乎不超过两个,如果光线没有指向天底或天顶)正确(没有滚动)摄像机位置,将世界光线投射到屏幕上的特定点。 / p>

    想象一下以下示例:靠近天顶的光线应该与屏幕上半部分的点匹配。第一个相机选项很明显,第二个相机在这种情况下围绕垂直轴旋转180度并具有更高的音高。在第一个选项中,天顶投影在屏幕上方锁定点,在第二个天顶中显示以下

    在这种模棱两可的情况下,应选择最接近初始摄像机方向的选项。

2 个答案:

答案 0 :(得分:1)

为了类似的全景目的解决此问题花了很长时间。

这里的复杂性是如何根据相机外观directionup矢量构造旋转矩阵来固定滚动方向。矩阵最终会突出显示常规的X,Y和Z轴,以便:

  • Z匹配lookat direction
  • X垂直于Z和up向量。
  • Y垂直于X和Z,使轴正交。

像这样:

axisZ = direction;
axisX = normalize(cross(up, direction));
axisY = cross(direction, axisX);

由于updirection不垂直,我们需要将X轴标准化,除以平方根。

我们将它们放在矩阵的行或列中(取决于您乘以哪种方式,如果向量是行或列)并得到如下公式:

v = M rotation w或view = M_rotation * world

您可以展开所有项的所有项,以获得view的X,Y和Z分量的方程式,并尝试提取direction的组件。由于那里的平方根,你得到一个更高阶多项式系统,其中3个方程和变量几乎全部相互指向。由于所有轴都是正交的,你可以使用Z 2 = 1 - X 2 * Y 2 来消除一个变量和方程,但得到的两个多项式是4度,都共享两个变量。我一开始无法解决它,但也偶然尝试过:

w = M -1 rotation v = M T rotation v

反转相机方向交换输入和输出,并且转置旋转矩阵使方程式看起来完全不同。

如果你然后将up向量固定到Y轴(你总是可以在之前和之后旋转世界以便稍后允许任意up方向),你最终可以得到2个合理的方程: / p>

(v y *(1 - d y 2 )) 2 - (w y - v z * d y 2 *(1 - d y 2 )= 0

(v x * sqrt(1 - d y 2 - d x 2 < / sup>) - v y * d y * d x 2 - (w x - w z * d x 2 *(1 - d y 2 )= 0

现在,第一个是用于d y 的符号可解的双二次方程,相机的Y分量为direction。用Wolfram Alpha求解d x 的第二个得到4个多项式,每个多项式约300个项。定义一些辅助变量然后产生一个合理的算法。它不短或不快(不要把它放在顶点着色器中),但绝对符合直观反应鼠标移动的目的。我想出的启发式算法都没有同样有效。

Here it is in TypeScript

请注意,在YZ平面或XZ平面的错误一侧旋转天顶或天底点的外观direction根本不存在。这意味着除非您允许更改向上矢量,否则用户无法完全随意拖动屏幕上的点。

答案 1 :(得分:0)

每当你处理轮换时,最好使用Quaternions

我会说这个答案并不完整,因为它不能防止相机滚动,但无论旋转如何,它都会确保您的起点位于光标下方。希望它可能有助于找到一个完整的解决方案!代码也可能更高效,这是一个非常冗长的黑客会话。

注意:您可以忽略着色器材质,这只是为了在测试时给我一个参考点。

var canvas = document.getElementById('canvas');
var scene = new THREE.Scene();
var renderer = new THREE.WebGLRenderer({canvas: canvas, antialias: true});
var camera = new THREE.PerspectiveCamera(70, canvas.clientWidth / canvas.clientWidth, 1, 1000);

var geometry = new THREE.BoxGeometry(10, 10, 10);
//var geometry = new THREE.SphereGeometry(500, 50, 50);
var material = new THREE.ShaderMaterial({
  vertexShader: document.getElementById('vertex-shader').textContent,
  fragmentShader: document.getElementById('fragment-shader').textContent,
  side: THREE.BackSide
});
var mesh = new THREE.Mesh(geometry, material);

scene.add(mesh);

render();

function render() {
  requestAnimationFrame(render);
  
  if (canvas.width !== canvas.clientWidth || canvas.height !== canvas.clientHeight) {
    renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
    camera.aspect = canvas.clientWidth /  canvas.clientHeight;
    camera.updateProjectionMatrix();
  }
  
  renderer.render(scene, camera);
}

// controls

var mousedown = false;
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
var lastQuat = new THREE.Quaternion();
var lastMouse = new THREE.Vector3();
var v = new THREE.Vector3();
var cam = camera.clone();

function mouseToNDC(v, x, y) {
  v.set(x / canvas.clientWidth * 2 - 1, -y / canvas.clientHeight * 2 + 1);
}

canvas.addEventListener('mousedown', function (event) {
  mousedown = true;
  mouseToNDC(mouse, event.layerX, event.layerY);
  raycaster.setFromCamera(mouse, camera);
  lastMouse.copy(raycaster.ray.direction);
  lastQuat.copy(camera.quaternion);
  cam.copy(camera);
});

window.addEventListener('mouseup', function () {
  mousedown = false;
});

canvas.addEventListener('mousemove', function (event) {
  if (mousedown) {
    mouseToNDC(mouse, event.layerX, event.layerY);
    raycaster.setFromCamera(mouse, cam);
    camera.quaternion.setFromUnitVectors(raycaster.ray.direction, lastMouse).multiply(lastQuat);
  }
});
html, body, #canvas {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
}
<canvas id="canvas"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r73/three.min.js"></script>
<script id="vertex-shader" type="x-shader/x-vertex">
  varying vec2 vUv;

  void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
  #define M_TAU 6.2831853071795864769252867665590

  varying vec2 vUv;

  void main() {
    float x = floor(sin(5.0 * M_TAU * vUv.x) / 2.0 + 1.0);
    float y = floor(sin(5.0 * M_TAU * vUv.y) / 2.0 + 1.0);
    float c = min(x, y);
    gl_FragColor = vec4(vec3(c), 1.0);
  }
</script>