Three.js鼠标点击平面实现任意画线功能

本文详细介绍如何使用three.js在三维网格平面上绘制线段。通过Raycaster获取鼠标坐标,实现鼠标左键绘制线段,鼠标右键结束绘制,ESC键退回上一步。文章提供了完整的代码示例,包括场景、相机、渲染器、灯光、控制器的初始化,以及如何创建网格、射线和平面相交的计算。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

操作步骤:鼠标指针移入三维网格平面之中,按下左键即可画线,画线过程中,若鼠标移出平面则停止绘制,再次移入则进行上次继续画线,鼠标右键结束绘制,Esc键退回上一步骤。

通过Raycaster获取鼠标坐标
在网格平面上任意位置绘制线段,就需要获取点击位置的向量Vector3,获取之后就可以确定第一个点的位置,一条直线由两个点来确定,由此,需要获取鼠标点击位置。
在写代码之前,需要了解一下THREE.Raycaster这个方法。
前面文章13 - three.js 笔记 - 通过 THREE.Raycaster 实现模型选中与信息显示也曾对这个对象做过介绍但是并未介绍其中所有的方法。
因为,我们需要获取不是与射线相交的物体,而是与射线相交的点也就是一个THREE.Vector3()对象。
通过查看官方文档,可以使用Three.Raycaster类的一个属性Ray的一个方法intersectPlane()器返回值是一个Vector3对象。
Ray.intersectPlane():将这条射线与平面相交,如果没有交点,则返回交点或null。
当然这里,不仅仅可以通过平面来获取交点还有别的方法,但是这里使用plane,别的方法请查看官方文档。
--------------------- 
版权声明:本文为优快云博主「ithanmang」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/ithanmang/article/details/81625996


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>画直线</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
        }
    </style>
    	<script src="../JS/ThreeJS/build/three.js"></script>
        <script src="../JS/ThreeJS/examples/js/libs/stats.min.js"></script>
        <script src="../JS/ThreeJS/examples/js/controls/OrbitControls.js"></script>
</head>
<body>
<script>

    var scene, camera, renderer, controls;
    var stats = initStats();

    /* 地面网格所需变量 */
    var length = 200;  /*线段长度*/

    /* 场景 */
    function initScene() {

        scene = new THREE.Scene();

    }

    /* 相机 */
    function initCamera() {

        camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 10000);
        camera.position.set(0, 200, 250);
        camera.lookAt(new THREE.Vector3(0, 0, 0));

    }

    /* 渲染器 */
    function initRender() {

        renderer = new THREE.WebGLRenderer({antialias: true});
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

    }

    /* 灯光 */
    function initLight() {

        var ambientLight = new THREE.AmbientLight(0x333333);
        scene.add(ambientLight);

        var directionalLight = new THREE.DirectionalLight(0xffffff, 1);
        directionalLight.position.set(100, 300, 200);
        scene.add(directionalLight);

    }

    /* 控制器 */
    function initControls() {

        controls = new THREE.OrbitControls(camera, renderer.domElement);
        /* 其它属性默认 */

    }

    /* 场景内容 */
    function initContent() {

        var geometry = new THREE.Geometry();/* 简单基础几何 */
        var lineMaterial = new THREE.LineBasicMaterial({color: 0x808080});/* 基础线材质 */

        var planeGeometry = new THREE.PlaneGeometry(length, 10);/* 平面 width:200,、height:10 */
        var planeMaterial = new THREE.MeshBasicMaterial({color: 0xD9D9D9, side: THREE.DoubleSide});/* 平面材质 */

        geometry.vertices.push(new THREE.Vector3(-length / 2, 0, 0));/* 顶点(-100, 0, 0) */
        geometry.vertices.push(new THREE.Vector3(length / 2, 0, 0)); /* 顶点( 100, 0, 0) */

        /* 循环创建线段 */
        for (var i = 0; i <= length / 10; i++){

            /* 横向线段 */
            var lineX = new THREE.Line(geometry, lineMaterial);
            lineX.position.z = (i * 10) - length / 2;
            scene.add(lineX);

            /* 纵向线段 */
            var lineY = new THREE.Line(geometry, lineMaterial);
            lineY.rotation.y = 0.5 * Math.PI;
            lineY.position.x = (i * 10) - length / 2;
            scene.add(lineY);

        }

        /* 创建包围平面 */
        var planeX_left = new THREE.Mesh(planeGeometry, planeMaterial);
        planeX_left.rotation.y = 0.5 * Math.PI;
        planeX_left.position.x = -length / 2;

        var planeX_right = planeX_left.clone();
        planeX_right.position.x = length / 2;

        var planeY_top = new THREE.Mesh(planeGeometry, planeMaterial);
        planeY_top.position.z = -length / 2;

        var planeY_bottom = planeY_top.clone();
        planeY_bottom.position.z = length / 2;

        scene.add(planeY_bottom);
        scene.add(planeY_top);
        scene.add(planeX_left);
        scene.add(planeX_right);

        /* 四个包围面的位置 y轴向上5 */
        scene.traverse(function (object) {

            if (object.isMesh){

                if (object.geometry.type === 'PlaneGeometry'){

                    object.position.y = 5;

                }

            }

        });

    }

    /* 获取射线与平面相交的交点 */
    function getIntersects(event) {

        var raycaster = new THREE.Raycaster();
        var mouse = new THREE.Vector2();

        mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
        mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

        var normal = new THREE.Vector3(0, 1, 0);
        /* 创建平面 */
        var planeGround = new THREE.Plane(normal, 0);

        /* 从相机发出一条射线经过鼠标点击的位置 */
        raycaster.setFromCamera(mouse, camera);

        /* 获取射线 */
        var ray = raycaster.ray;

        /* 计算相机到射线的对象,可能有多个对象,返回一个数组,按照距离相机远近排列 */
        var intersects = ray.intersectPlane(planeGround);

        /* 返回向量 */
        return intersects;

    }

    var pointsArray = [];
    var window_mouse = true;
    /* 鼠标按下事件 */
    function onMouseDown(event) {

        /* 获取相机发出的射线与 Plane 相交点*/
        var intersects = getIntersects(event);

        /* 存放网格的三维坐标 */
        var vector3_x, vector3_z;

        /* 鼠标左键按下时,创建点和线段 */
        if (event.button === 0) {

            if (!window_mouse){

                window.addEventListener('mousemove', onMouseMove, false);

                /* 依据 windwo_mouse 标识避免事件的重复添加 */
                window_mouse = true;

            }

            /* 判断交点是否在 x(-100, 100) ,z(-100, 100)(平面)之间 */
            if (Math.abs(intersects.x) < length / 2 && Math.abs(intersects.z) < length / 2){

                /* 若交点此时在平面之内则创建点(Points) */
                var pointsGeometry = new THREE.Geometry();
                pointsGeometry.vertices.push(intersects);

                var pointsMaterial = new THREE.PointsMaterial({color:0xff0000, size: 3});
                var points = new THREE.Points(pointsGeometry, pointsMaterial);

                pointsArray.push(points);

                /* 创建线段 */
                var lineGeometry = new THREE.Geometry();
                var lineMaterial = new THREE.LineBasicMaterial({color: 0x00ff00});

                if (pointsArray.length >= 2) {

                    lineGeometry.vertices.push(pointsArray[0].geometry.vertices[0], pointsArray[1].geometry.vertices[0]);

                    var line = new THREE.Line(lineGeometry, lineMaterial);
                    pointsArray.shift();
                    scene.add(line);

                }

                scene.add(points);

            }

        }

        /* 鼠标右键按下时 回退到上一步的点,并中断绘制 */
        if (event.button === 2) {

            window.removeEventListener('mousemove', onMouseMove, false);

            /* 移除事件之后,要设置为 false 为了避免事件的重复添加 */
            window_mouse = false;

            /* 鼠标左键未点击时线段的移动状态 */
            if (scene.getObjectByName('line_move')) {

                scene.remove(scene.getObjectByName('line_move'));

                /* 删除数组中的元素,否则的话再次重绘会链接之前的点接着重绘 */
                pointsArray.shift();

            }

        }

    }

    /* 鼠标移动事件 */
    function onMouseMove(event) {

        var intersects = getIntersects(event);

        /* 判断交点是否在 x(-100, 100) ,z(-100, 100)(平面)之间 */
        if (Math.abs(intersects.x) < length / 2 && Math.abs(intersects.z) < length / 2) {

            /* 鼠标左键未点击时线段的移动状态 */
            if (scene.getObjectByName('line_move')) {

                scene.remove(scene.getObjectByName('line_move'));

            }
            /* 创建线段 */
            var lineGeometry = new THREE.Geometry();
            var lineMaterial = new THREE.LineBasicMaterial({color: 0x00ff00});

            if (pointsArray.length > 0){

                lineGeometry.vertices.push(pointsArray[0].geometry.vertices[0]);

                var mouseVector3 = new THREE.Vector3(intersects.x, 0, intersects.z);

                lineGeometry.vertices.push(mouseVector3);

                var line = new THREE.Line(lineGeometry, lineMaterial);
                line.name = 'line_move';

                scene.add(line);

            }

        }

    }

    /* 键盘按下事件 */
    function onKeyDown(event) {

        /* ESC键 回退上一步绘制,结束绘制*/
        if (event.key === 'Escape'){

            window.removeEventListener('mousemove', onMouseMove, false);

            /* 移除事件之后,要设置为 false 为了避免 mousemove 事件的重复添加 */
            window_mouse = false;

            /* 鼠标左键未点击时线段的移动状态 */
            if (scene.getObjectByName('line_move')) {

                scene.remove(scene.getObjectByName('line_move'));

                /* 删除数组中的元素,否则的话再次重绘会链接之前的点接着重绘 */
                pointsArray.shift();

            }

            var length = scene.children.length - 1;
            /* 按步骤移除点和先 */
            if (scene.children[length].isLine || scene.children[length].isPoints){

                scene.children.pop();
                length = scene.children.length - 1;

                /* 若最后一项不是线段或者点就不移除 */
                if (!scene.children[length].isMesh) {

                    scene.children.pop();

                }

            }

        }

    }

    /* 更新数据 */
    function update() {

        stats.update();
        controls.update();

    }

    /* 性能插件 */
    function initStats() {

        var stats = new Stats();

        stats.setMode(0);
        stats.domElement.style.position = 'absolute';
        stats.domElement.style.left = '0px';
        stats.domElement.style.right = '0px';

        document.body.appendChild(stats.domElement);

        return stats;

    }

    /* 窗口自动适应 */
    function onWindowResize() {

        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);

    }

    /* 循环调用 */
    function animate() {

        requestAnimationFrame(animate);
        renderer.render(scene, camera);
        update();

    }

    /* 初始化 */
    function init() {

        /* 兼容性判断 */
        //if (!Detector.webgl) Detector.addGetWebGLMessage();

        initScene();
        initCamera();
        initRender();
        initLight();
        initContent();
        initControls();

        /* 事件监听 */
        window.addEventListener('resize', onWindowResize, false);
        window.addEventListener('mousedown', onMouseDown, false);/* 使用mousedown的时候可以判断出点击的鼠标左右键之分 */
        window.addEventListener('mousemove', onMouseMove, false);
        window.addEventListener('keydown', onKeyDown, false);/* 使用事件的时候要把前面的on给去掉 */

    }

    /* 初始加载 */
    (function () {

        console.log('three start...');

        init();
        animate();

        console.log('three end...');

    })();

</script>
</body>
</html>

 

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值