基于Three.js的点击移动ClickMoveControl

该博客介绍如何使用Three.js库在Web端实现3D场景的交互功能。通过鼠标点击和拖动,可以实现视角的旋转;点击后移动鼠标则能平移相机。此外,博客还提供了移动端的适配方案,使得在手机上也能流畅操作。源代码中详细注释了关键步骤,包括相机旋转、相机移动的实现,并给出了实际运行的示例链接。
摘要由CSDN通过智能技术生成

一直想写一个Three.js端的点击移动功能,参考的这个。鼠标左键拖动旋转视角,鼠标左键点击移动相机。

这样可以很方便的预览一些场景。

移动流程参考外网的一个源码,旋转视角功能参照官方demo的旋转方法写了一个。

编写流程估计也没人高兴看,重要的地方都注释了,移动端也做了适配,最终效果:http://eevee.com.cn/Examples/ClickMoveSample/index.html

源码:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Click Move Sample</title>
  <style>
    body {
      margin: 0;
      overflow: hidden;
      padding: 0;
    }

    canvas {
      display: block;
    }
  </style>
</head>

<body>
  <script type="module">
    import * as THREE from './build/three.module.js';

    //设置全局变量.
    var container, renderer, camera, scene;
    var characterSize = 1;

    // Track all objects and collisions.
    var objects = [];

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

    // Store movements.
    var movements = [];
    var playerSpeed = 0.1;

    init();
    function init() {

      container = document.createElement('div');
      document.body.appendChild(container);

      scene = new THREE.Scene();
      scene.background = new THREE.Color(0xccddff);
      scene.fog = new THREE.Fog(0xccddff, 5, 100);

      var ambient = new THREE.AmbientLight(0xffffff);
      scene.add(ambient);

      var hemisphereLight = new THREE.HemisphereLight(0xdddddd, 0x000000, 0.5);
      scene.add(hemisphereLight);

      createFloor();
      createTree(3, 3);
      createTree(8, -3);
      createTree(-3, 8);
      createTree(-8, -8);

      // Create the camera.
      camera = new THREE.PerspectiveCamera(
        60, // Angle
        window.innerWidth / window.innerHeight, // Aspect Ratio.
        0.01, // Near view.
        200 // Far view.
      );
      camera.position.z = 10;
      camera.position.y = 2;
      scene.add(camera);

      // Build the renderer
      renderer = new THREE.WebGLRenderer({ antialias: true });

      var element = renderer.domElement;
      renderer.setSize(window.innerWidth, window.innerHeight);
      container.appendChild(element);

      document.addEventListener('mousedown', onDocumentMouseDown);
      document.addEventListener('mouseup', onDocumentMouseUp);
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchstart', onDocumentTouchDown);
      // document.addEventListener('touchend', onDocumentTouchUp);
      document.addEventListener('touchmove', onDocumentTouchMove);
    }

    window.onresize = function () {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    };

    function onDocumentTouchDown(event) {
      event.preventDefault();
      toucheMovementX = event.touches[0].clientX;
      toucheMovementY = event.touches[0].clientY;
    }

    var toucheMovementX, toucheMovementY;
    function onDocumentTouchMove(event) {
      event.preventDefault();

      //0.5为移动端旋转角度的速度
      event.movementX = 0.5 * (event.touches[0].clientX - toucheMovementX);
      event.movementY = 0.5 * (event.touches[0].clientY - toucheMovementY);
      toucheMovementX = event.touches[0].clientX;
      toucheMovementY = event.touches[0].clientY;
      onMouseMove(event, true);
    }

    //#region 相机旋转
    var minPolarAngle = 0; // radians
    var maxPolarAngle = Math.PI; // radians
    var pointerSpeed = 1.0;
    const _PI_2 = Math.PI / 2;
    const _euler = new THREE.Euler(0, 0, 0, 'YXZ');

    //相机旋转
    function onMouseMove(event, ismobile = false) {
      event.preventDefault();
      if (!isClicked && !ismobile)
        return;

      const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
      const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0;

      _euler.setFromQuaternion(camera.quaternion);

      _euler.y += movementX * 0.002 * pointerSpeed;
      _euler.x += movementY * 0.002 * pointerSpeed;

      _euler.x = Math.max(_PI_2 - maxPolarAngle, Math.min(_PI_2 - minPolarAngle, _euler.x));

      camera.quaternion.setFromEuler(_euler);
    }
    //#endregion

    //#region 相机移动
    var clickPointX, clickPointY;
    var isClicked = false;
    function onDocumentMouseDown(event, ismobile = false) {
      event.preventDefault();
      //鼠标按下时,记录点击位置
      if (event.which == 1 || ismobile) {
        clickPointX = event.clientX;
        clickPointY = event.clientY;
        isClicked = true;
      }
    }

    //相机移动
    function onDocumentMouseUp(event, ismobile = false) {
      event.preventDefault();
      if (event.which == 1 || ismobile)
        isClicked = false;
      // 鼠标抬起时对比点击位置,如果移动了,则执行旋转视角,如果点击点未移动则执行相机移动
      if (((event.which == 1 || ismobile) && clickPointX == event.clientX && clickPointY == event.clientY)) {
        stopMovement();

        // Grab the coordinates.
        mouse.x = (event.clientX / renderer.domElement.clientWidth) * 2 - 1;
        mouse.y = - (event.clientY / renderer.domElement.clientHeight) * 2 + 1;

        // Use the raycaster to detect intersections.
        raycaster.setFromCamera(mouse, camera);

        // Grab all objects that can be intersected.
        var intersects = raycaster.intersectObjects(objects);
        if (intersects.length > 0) {
          movements.push(intersects[0].point);
        }
      }
    }

    function move(location, destination, speed = playerSpeed) {
      var moveDistance = speed;

      // Translate over to the position.
      var posX = location.position.x;
      var posZ = location.position.z;
      var newPosX = destination.x;
      var newPosZ = destination.z;

      // 设置一个乘数,以防我们需要负值。
      var multiplierX = 1;
      var multiplierZ = 1;

      // 检测当前位置和目标之间的距离。
      var diffX = Math.abs(posX - newPosX);
      var diffZ = Math.abs(posZ - newPosZ);
      var distance = Math.sqrt(diffX * diffX + diffZ * diffZ);

      // 如有必要,使用负乘数。
      if (posX > newPosX) {
        multiplierX = -1;
      }

      if (posZ > newPosZ) {
        multiplierZ = -1;
      }

      // Update the main position.
      location.position.x = location.position.x + (moveDistance * (diffX / distance)) * multiplierX;
      location.position.z = location.position.z + (moveDistance * (diffZ / distance)) * multiplierZ;

      // If the position is close we can call the movement complete.
      if ((location.position.x <= newPosX + moveDistance &&
        location.position.x >= newPosX - moveDistance) &&
        (location.position.z <= newPosZ + moveDistance &&
          location.position.z >= newPosZ - moveDistance)) {
        location.position.x = (location.position.x);
        location.position.z = (location.position.z);

        // Reset any movements.
        stopMovement();

        // Maybe move should return a boolean. True if completed, false if not. 
      }

    }

    /**
     * Stop character movement.
     */
    function stopMovement() {
      movements = [];
      scene.remove(indicatorTop);
      scene.remove(indicatorBottom);
    }
    //#endregion

    function render() {
      renderer.render(scene, camera);

      // If any movement was added, run it!
      if (movements.length > 0) {
        // Set an indicator point to destination.
        if (scene.getObjectByName('indicator_top') === undefined) {
          drawIndicator();
        } else {
          if (indicatorTop.position.y > 0.1) {
            indicatorTop.position.y -= 0.01;
          } else {
            indicatorTop.position.y = 0.5;
          }
        }

        move(camera, movements[0]);
      }

    }

    animate();
    function animate() {
      requestAnimationFrame(animate);
      render();
    }

    //#region 创建参照物
    // 创建地面.
    function createFloor() {
      var geometry = new THREE.PlaneBufferGeometry(100, 100);
      var material = new THREE.MeshToonMaterial({ color: 0x6e6e6e });
      var plane = new THREE.Mesh(geometry, material);
      plane.rotation.x = -1 * Math.PI / 2;
      plane.position.y = 0;
      scene.add(plane);
      objects.push(plane);
    }

    // 创建树.
    function createTree(posX, posZ) {
      // Set some random values so our trees look different.
      var randomScale = (Math.random() * 3) + 0.8;
      var randomRotateY = Math.PI / (Math.floor((Math.random() * 32) + 1));

      // Create the trunk.
      var geometry = new THREE.CylinderGeometry(characterSize / 3.5, characterSize / 2.5, characterSize * 1.3, 8);
      var material = new THREE.MeshToonMaterial({ color: 0x664422 });
      var trunk = new THREE.Mesh(geometry, material);
      trunk.position.set(posX, ((characterSize * 1.3 * randomScale) / 2), posZ);
      trunk.scale.x = trunk.scale.y = trunk.scale.z = randomScale;
      scene.add(trunk);

      var geometry = new THREE.DodecahedronGeometry(characterSize);
      var material = new THREE.MeshToonMaterial({ color: 0x44aa44 });
      var treeTop = new THREE.Mesh(geometry, material);
      treeTop.position.set(posX, ((characterSize * 1.3 * randomScale) / 2) + characterSize * randomScale, posZ);
      treeTop.scale.x = treeTop.scale.y = treeTop.scale.z = randomScale;
      treeTop.rotation.y = randomRotateY;
      scene.add(treeTop);
    }

    // 移动目的地指示器。
    var indicatorTop;
    var indicatorBottom;
    // 绘制3d目标点.
    function drawIndicator() {
      // Store variables.
      var topSize = characterSize / 8;
      var bottomRadius = characterSize / 4;

      // Create the top indicator.
      var geometry = new THREE.TetrahedronGeometry(topSize, 0);
      var material = new THREE.MeshToonMaterial({ color: 0x00ccff, emissive: 0x00ccff });
      indicatorTop = new THREE.Mesh(geometry, material);
      indicatorTop.position.y = 0.05; // Flat surface so hardcode Y position for now.
      indicatorTop.position.x = movements[0].x; // Get the X destination.
      indicatorTop.position.z = movements[0].z; // Get the Z destination.
      indicatorTop.rotation.x = -0.97;
      indicatorTop.rotation.y = Math.PI / 4;
      indicatorTop.name = 'indicator_top'
      scene.add(indicatorTop);

      // Create the bottom indicator.
      var geometry = new THREE.TorusGeometry(bottomRadius, (bottomRadius * 0.25), 2, 12);
      geometry.dynamic = true;
      var material = new THREE.MeshToonMaterial({ color: 0x00ccff, emissive: 0x00ccff });
      indicatorBottom = new THREE.Mesh(geometry, material);
      indicatorBottom.position.y = 0.025;
      indicatorBottom.position.x = movements[0].x;
      indicatorBottom.position.z = movements[0].z;
      indicatorBottom.rotation.x = -Math.PI / 2;
      scene.add(indicatorBottom);
    }
//#endregion
  </script>
</body>

</html>
  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
three.js中,实现地面点击移动的方法可以通过以下步骤来实现: 1. 首先,你需要创建一个地面模型,可以使用Three.js中的几何体或加载外部模型文件。你可以使用平面几何体或者导入一个地面模型。 2. 接下来,你需要监听鼠标点击事件或者触摸事件。在事件处理函数中,你可以获取鼠标点击或触摸点的屏幕坐标。 3. 使用Three.js中的Raycaster类,将屏幕坐标转换为三维空间中的射线。射线的起点是相机位置,方向是从相机位置指向屏幕坐标。 4. 使用Raycaster类的intersectObjects方法,检测射线与地面模型的交点。你可以将地面模型添加到一个对象数组中,然后传递给intersectObjects方法。 5. 如果射线与地面模型有交点,你可以获取交点的坐标。根据你的需求,你可以将物体移动到交点的位置,或者根据交点的坐标计算移动的偏移量。 这是一个基本的实现思路,具体的代码实现可能会有所不同,取决于你的项目需求和three.js的版本。你可以参考\[1\]中的代码示例和\[2\]中的HTML结构示例来实现地面点击移动功能。 #### 引用[.reference_title] - *1* [Three 之 three.jswebgl)鼠标/手指通过射线移动物体的简单整理封装](https://blog.csdn.net/u014361280/article/details/128190669)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [Three.js不同模型在不同轨道上的动画和移动](https://blog.csdn.net/Mr_Bobcp/article/details/129777103)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值