从模型转到屏幕上的过程说开
由于图形显示的基本单位是三角形,那就先从一个三角形从世界坐标转到屏幕坐标说起,例如三角形abc
乘以模型视图矩阵就进入了视点坐标系,其实就是相机所在的坐标系,如下图:
这个时候整个三角形就位于中心位于坐标系原点,边长为2的立方体内,在这个立方体内,三角形要计算光照,要裁剪,然后乘以视口矩阵,最后转到屏幕上。
转到屏幕上后,三角形的所有点的Z坐标就是深度坐标,一定在(0, 1)这个区间内,那么哪些点的Z坐标是0呢,在投影坐标系中,一定是投影视景体的前剪切平面上的点,而投影视景体的后剪切平面上的点的Z坐标就是1。
思路来了
根据以上三角形转换到屏幕坐标上的过程可以分析出,鼠标在屏幕上点击的时候,可以得到二维坐标p(x, y),再加上深度坐标的范围(0, 1), 就可以形成两个三位坐标A(x, y, 0), B(x, y, 1), 由于它们的Z轴坐标是0和1,则转变到投影坐标系的话,一定分别是前剪切平面上的点和后剪切平面上的点,也就是说,在投影坐标系中,A点一定在能看见的所有模型的最前面,B点一定在能看见的所有的模型的最后边,假设视口矩阵的逆矩帧,投影矩阵的逆矩阵,模型视图矩阵的逆矩阵为M, N, P,则 P * N * M * A = A1, P * N * M * B = B1, 在世界坐标系中,点A1B1就可以形成一个射线,此射线和模型再求交,就能选中模型。如下图是在视点坐标系中的情形。注意,求交可以在视点坐标系或者世界坐标系计算都可以,但一般会在世界坐标坐标系中计算。
拾取的优化,射线和AABB包围盒求交
如果射线和所有的模型求交,显然不是一个好办法,一般情况下会进行一些优化,比如先和模型的包围盒求交,如果和模型的包围盒不相交的话,就放过去,否则就接着往下进行,和模型的所有三角面片求交。
那么什么是包围盒呢?在计算机图形学与计算几何领域,一组物体的包围体就是将物体组合完全包容起来的一个封闭空间。将复杂物体封装在简单的包围体中,就可以提高几何运算的效率。通常简单的物体比较容易检查相互之间的重叠。其中有一种包围盒叫做AABB, AABB的全称是axis aligned bounding box,就是我们常常提到轴向包围盒,这个盒子的边是平行于x/y/z轴的。 所有的2d和3d物体都是由点组成的,所以只要找出这些物体的最大值点和最小值点,那么就可以使用这两个点表示该物体的AABB包围盒了。
检测碰撞的时候我们只需要检测这些物体的AABB(即他们的最大值点和最小值点)是否相交,就可以判断是否碰撞了。
射线和三角形相交
判断射线和包围盒是否求交后,就轮到判断是否和三角形求交了,最先想到的是 首先判断射线是否与三角形所在的平面相交,如果相交,再判断交点是否在三角形内。判断射线是否与平面相交, 判断点是否在三角形内.
最后附上three.js选中物体源码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>hiwebpage</title> <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> <style> body { font-family: Monospace; font-size: 13px; text-align: center; margin: 0px; background-color: #fff; overflow: hidden; } #info { position: absolute; z-index: 1; width: 100%; padding: 5px; text-align: center; } </style> </head> <body> <script src="https://threejs.org/build/three.js"></script> <script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script> <script> var renderer, scene, camera, controls; var data = {} renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio); document.body.appendChild(renderer.domElement); camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 1000); camera.position.set(15, 15, 15); scene = new THREE.Scene(); scene.background = new THREE.Color(0xffffff); controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableRotate = true; var box1 = new THREE.BoxGeometry(10, 10, 10); var materialP = new THREE.MeshBasicMaterial({ color: 0x0000ff, side: THREE.DoubleSide }); var mesh1 = new THREE.Mesh(box1, materialP); data[mesh1.uuid] = { name: '胖虎' } mesh1.position.set(-80, -40, 0); var materialP2 = new THREE.MeshBasicMaterial({ color: 0x0000ff, side: THREE.DoubleSide }); var mesh2 = new THREE.Mesh(box1, materialP2); data[mesh2.uuid] = { name: '狗子' } scene.add(mesh1) scene.add(mesh2) function animate() { renderer.render(scene, camera); controls.update() requestAnimationFrame(animate); } animate() window.addEventListener('click', onMouseClick, false); var mouse = new THREE.Vector2(); function onMouseClick(event) { mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; var raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, camera); // 计算物体和射线的焦点 var intersects = raycaster.intersectObjects(scene.children, true); // console.log(scene.children) // console.log(intersects) if (intersects.length > 0) { // for (var i = 0; i < intersects.length; i++) { var sceneChildren = scene.children for (var i = 0; i < sceneChildren.length; i++) { sceneChildren[i].material.color.set(0x0000ff) } intersects[0].object.material.color.set(0xff0000) // } intersects[0].object.material.color.set(0xff0000); console.log(data[intersects[0].object.uuid].name) } else { var intersects = scene.children for (var i = 0; i < intersects.length; i++) { intersects[i].material.color.set(0x0000ff) } } } </script> </body> </html>
注释后面补充