技术分享 Technology to share

three.js选中物体原理

从模型转到屏幕上的过程说开


由于图形显示的基本单位是三角形,那就先从一个三角形从世界坐标转到屏幕坐标说起,例如三角形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/jsm/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>


注释后面补充