three.js实现第三视角人物漫游

1,197 阅读1分钟

three.js实现第三视角人物漫游

<!DOCTYPE html>
<html>
  <head>
    <title>threejs</title>
    <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=2,maximum-scale=1,user-scalable=yes" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />

    <style>
      html,
      body {
        padding: 0;
        margin: 0;
        overflow: hidden;
      }
      canvas {
        width: 200vh;
        height: 200vw;
        transform: scale(0.5);
        transform-origin: 0 0;
      }
    </style>
  </head>

  <body>
    <script type="module">
      import * as THREE from "https://cdn.skypack.dev/three@v0.129.0";
      import { GLTFLoader } from "https://cdn.skypack.dev/three@v0.129.0/examples//jsm/loaders/GLTFLoader.js";
      import { OrbitControls } from "https://cdn.skypack.dev/three@v0.129.0/examples/jsm/controls/OrbitControls.js";
      import { OBB } from "https://cdn.skypack.dev/three@v0.129.0/examples/jsm/math/OBB.js";
      import * as CANNON from "http://182.43.179.137:81/public/cannon-es/cannon-es-0.19.0.js";

      // 创建cannonjs世界坐标系
      let world, defaultMaterial;
      !(() => {
        world = new CANNON.World();
        world.gravity.set(0, -9.8, 0); // 重力

        // 材料
        defaultMaterial = new CANNON.Material("default");
        const defalutContactMaterial = new CANNON.ContactMaterial(defaultMaterial, defaultMaterial, {
          // 关联材料
          friction: 0, // 摩擦
          restitution: 0, // 弹性
        });
        world.addContactMaterial(defalutContactMaterial); // 添加

        // 更新
        const clock = new THREE.Clock();
        !(function update(time) {
          requestAnimationFrame(update);
          const dt = clock.getDelta();
          dt > 0 && world.step(dt);
        })();
      })();

      // 创建渲染器
      var renderer = new THREE.WebGLRenderer();
      renderer.setSize(window.innerWidth * 2, window.innerHeight * 2); // 设置canvas画布大小
      document.body.appendChild(renderer.domElement); // canvas画布插入dom树

      // 创建场景
      var scene = new THREE.Scene();
      scene.background = new THREE.Color("#347897");

      // 辅助线
      var axisHelper = new THREE.AxisHelper(500);
      scene.add(axisHelper);

      // 添加点光源
      let light1 = new THREE.PointLight("#fff", 1.3);
      light1.position.set(10000, 2160, -22160);
      scene.add(light1);
      let light2 = new THREE.PointLight("#fff", 1.3);
      light2.position.set(-10000, 2160, 22160);
      scene.add(light2);

      //环境光
      let ambient = new THREE.AmbientLight("#fff", 1);
      scene.add(ambient);

      // 创建相机
      var camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 4000);
      camera.position.set(100, 600, -750); // 设置相机位置

      // 创建控制器
      let controls = new OrbitControls(camera, renderer.domElement);
      controls.autoRotate = true;

      // 渲染
      requestAnimationFrame(function render() {
        controls.update(); // Update controls
        renderer.render(scene, camera);
        requestAnimationFrame(render);
      });

      // 收集可在其上行走的物体
      const arr = [];

      // 创建地面,几何体,材质
      let plane;
      !(() => {
        const length = 3000;
        const width = 3000;
        const texture = new THREE.TextureLoader().load("http://182.43.179.137:81/public/images/texture-wood2.jpg"); // 立方纹理加载器
        texture.wrapS = THREE.RepeatWrapping;
        texture.wrapT = THREE.RepeatWrapping;
        texture.repeat.set(10, 10);
        var geometry = new THREE.PlaneGeometry(length, width); // 平面几何体
        var material = new THREE.MeshPhongMaterial({ map: texture, side: THREE.DoubleSide });
        plane = new THREE.Mesh(geometry, material); // 创建模型
        plane.rotateX((Math.PI / 180) * 90);
        plane.name = "plane";
        scene.add(plane); // 加入场景
        arr.push(plane);

        // cannonjs模拟地面
        var groundBody = new CANNON.Body({
          mass: 0, // 质量
          material: defaultMaterial,
          shape: new CANNON.Plane(), // 形状
        });
        // 旋转90度
        groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(-1, 0, 0), Math.PI * 0.5);
        world.addBody(groundBody); // 地面添加进物理世界
        groundBody.name = "plane";
      })();

      // 箱子,几何体,材质
      let box;
      !(() => {
        const length = 1000;
        const height = 100;
        const width = 1000;
        const position = [0, height / 2, 0];
        var boxGeometry = new THREE.BoxGeometry(length, height, width); // 平面几何体
        const texture = new THREE.TextureLoader().load("http://182.43.179.137:81/public/images/texture-stone5.jpg"); // 立方纹理加载器
        var boxMaterial = new THREE.MeshStandardMaterial({ color: "#777887", map: texture, side: THREE.DoubleSide });
        box = new THREE.Mesh(boxGeometry, boxMaterial); // 创建模型
        box.position.set(...position);
        box.name = "box";
        box._height = height;
        scene.add(box); // 加入场景
        arr.push(box);

        // cannonjs模拟箱子
        var body = new CANNON.Body(
          {
          mass: 0, // 质量
          position: new CANNON.Vec3(...position), // 位置
          shape: new CANNON.Box(new CANNON.Vec3(length / 2, height / 2, width / 2)), // 形状
          quaternion: new CANNON.Quaternion(0, 0, 0, 1),
          material: defaultMaterial, // 添加材料
        }
        );
        body.name = "box";
        world.addBody(body); // 添加到world
      })();

      // 箱子1,几何体,材质
      let box1;
      !(() => {
        const length = 420;
        const height = 100;
        const width = 420;
        const position = [0, height / 2 + 100, 0];
        var boxGeometry = new THREE.BoxGeometry(length, height, width); // 平面几何体
        const texture = new THREE.TextureLoader().load("http://182.43.179.137:81/public/images/texture-stone3.jpg"); // 立方纹理加载器
        var boxMaterial = new THREE.MeshStandardMaterial({ color: "#999", map: texture, side: THREE.DoubleSide });
        box1 = new THREE.Mesh(boxGeometry, boxMaterial); // 创建模型
        box1.position.set(...position);
        box1.name = "box1";
        box1._height = height;
        scene.add(box1); // 加入场景
        arr.push(box1);

        // cannonjs模拟箱子
        var body = new CANNON.Body({
          mass: 0, // 质量
          position: new CANNON.Vec3(...position), // 位置
          shape: new CANNON.Box(new CANNON.Vec3(length / 2, height / 2, width / 2)), // 形状
          quaternion: new CANNON.Quaternion(0, 0, 0, 1),
          material: defaultMaterial, // 添加材料
        });
        body.name = "box1";
        world.addBody(body); // 添加到world
      })();

      // 加载gltf模型
      let gltfModel;
      new GLTFLoader().load("http://182.43.179.137:81/public/3d-models/Soldier.glb", function (gltf) {
        gltfModel = gltf.scene;
        gltfModel.children[0].scale.set(1, 1, 1); // 修正缩放
        gltfModel._size = { length: 100, height: 182, width: 20 };

        // 修正姿态
        gltfModel.children[0].position.add(new THREE.Vector3(0, -gltfModel._size.height / 2, 0));
        gltfModel.children[0].applyMatrix4(new THREE.Matrix4().makeRotationY((Math.PI / 180) * -180));
        gltfModel.position.add(new THREE.Vector3(0, gltfModel._size.height / 2 + 200, 0));
        gltfModel.rotateY((Math.PI / 180) * 90);
        gltfModel.updateMatrix();
        scene.add(gltfModel); // 加入场景

        // 动画处理
        const animations = gltf.animations;
        let mixer = new THREE.AnimationMixer(gltfModel);
        const idleClip = animations[0];
        const runClip = animations[1];
        const walkClip = animations[3]; // 散步帧
        gltfModel.idleAction = mixer.clipAction(idleClip); // 空闲
        gltfModel.idleAction.timeScale = 2; // 2倍速播放
        gltfModel.runAction = mixer.clipAction(runClip); // 跑步
        gltfModel.walkAction = mixer.clipAction(walkClip); // 散步
        gltfModel.walkAction.timeScale = 2; // 3倍速播放
        gltfModel.idleAction.play(); // 开启空闲

        // 更新混合器
        var clock = new THREE.Clock();
        setInterval(() => {
          mixer.update(clock.getDelta());
        }, 50);

        // 模型移动
        gltfModel.walk = function (destination) {
          gltfModel.walkData = {
            speed: 350, // 速度
            originPos: gltfModel.position.clone(), // 老位置
            cameraOriginPos: camera.position.clone(), // 老位置
            destination, // 目标位置
            vector: new THREE.Vector3(destination.x - gltfModel.position.x, 0, destination.z - gltfModel.position.z), // 移动向量
            distance: new THREE.Vector3(destination.x - gltfModel.position.x, 0, destination.z - gltfModel.position.z).length(), // 距离
            state: "walk", // 状态,walk,upStage上台阶,downState下台阶
          };

          // cannonjs模拟gltfModel对象
          const position = gltfModel.position.clone();
          const quaternion = gltfModel.quaternion;
          const size = gltfModel._size;
          var body = new CANNON.Body({
            mass: 1, // 质量
            position: new CANNON.Vec3(position.x, position.y, position.z), // 位置
            shape: new CANNON.Box(new CANNON.Vec3(size.length / 2, size.height / 2, size.width / 2)), // 形状
            quaternion: new CANNON.Quaternion(quaternion.x, quaternion.y, quaternion.z, quaternion.w),
            material: defaultMaterial, // 添加材料
            velocity: new CANNON.Vec3(0, 0, 0),
          });
          body.name = "gltf";
          body.fixedRotation = true; // 旋转约束
          body.updateMassProperties(); // 旋转约束
          gltfModel.walkData.cannon = body;

          const { speed, originPos, cameraOriginPos, vector, distance } = gltfModel.walkData; // 提取数据

          // 修正方向
          !(function () {
            const quaternion = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), vector.clone().normalize());
            gltfModel.quaternion.copy(quaternion);
            gltfModel.updateMatrix();
            body.quaternion.set(quaternion.x, quaternion.y, quaternion.z, quaternion.w);
          })();

          // 位置矫正
          body.position.set(originPos.x, originPos.y, originPos.z);

          // 速度
          const speedVector = vector.clone().normalize().setLength(speed);
          body.velocity = new CANNON.Vec3(speedVector.x, speedVector.y, speedVector.z);

          // 切换动画
          gltfModel.walkAction.play();
          gltfModel.idleAction.stop();

          // 添加
          world.addBody(body);

          // 更新gltf模型
          gltfModel.walkData.refreshFrameId = requestAnimationFrame(function h() {
            if (!gltfModel.walkData) return;
            gltfModel.walkData.refreshFrameId = requestAnimationFrame(h);
            if (world.bodies.find((item) => item.name == "gltf")) {
              const position = body.position; // 位置
              const quaternion = body.quaternion; // 旋转和缩放四元数
              gltfModel.position.set(position.x, position.y, position.z);
              gltfModel.quaternion.set(quaternion.x, quaternion.y, quaternion.z, quaternion.w);
              camera.position.copy(cameraOriginPos.clone().add(gltfModel.position.clone().sub(originPos.clone())));
              controls.target.copy(gltfModel.position); // 控制器更新
            }
          });

          // 上下台阶
          gltfModel.walkData.watchFrameId = requestAnimationFrame(function h1() {
            if (!gltfModel.walkData) return;
            gltfModel.walkData.watchFrameId = requestAnimationFrame(h1);
            // 上台阶
            gltfModel.walk_upStage();

            // 下台阶
            gltfModel.walk_downStage();
          });

          // 清定时器
          gltfModel.walkData.timer = setTimeout(() => {
            gltfModel.walk_over(destination, cameraOriginPos, vector);
          }, (distance / speed) * 1000);
        };

        // 上台阶
        gltfModel.walk_upStage = function () {
          const { state, cannon: body, vector, speed } = gltfModel.walkData;
          const speedVector = vector.clone().normalize().setLength(speed);
          if (state !== "walk") return;

          // 碰撞检测,采样200+个点
          let collisionArr = []; // 碰撞对象数组
          const originArr = []; // 点数组
          const { length, height, width } = gltfModel._size;
          originArr.push(...new THREE.LineCurve3(new THREE.Vector3(-length / 2, height / 2, width / 2), new THREE.Vector3(length / 2, height / 2, width / 2)).getPoints(50));
          originArr.push(...new THREE.LineCurve3(new THREE.Vector3(length / 2, height / 2, width / 2), new THREE.Vector3(length / 2, -height / 2, width / 2)).getPoints(50));
          originArr.push(...new THREE.LineCurve3(new THREE.Vector3(length / 2, -height / 2, width / 2), new THREE.Vector3(-length / 2, -height / 2, width / 2)).getPoints(50));
          originArr.push(...new THREE.LineCurve3(new THREE.Vector3(-length / 2, -height / 2, width / 2), new THREE.Vector3(-length / 2, height / 2, width / 2)).getPoints(50));
          originArr.forEach((origin) => {
            const origin_t = origin.applyMatrix4(gltfModel.matrix).add(vector.clone().normalize().multiplyScalar(-2)).add(new THREE.Vector3(0, 2, 0));
            let raycaster = new THREE.Raycaster(origin_t, vector.clone().normalize(), 0.1, 100000);
            let intersects = raycaster.intersectObjects(arr);
            intersects[0] && collisionArr.push(intersects[0]);
          });

          // 最小距离
          collisionArr = collisionArr
            .map((item) => {
              item.distance -= 2;
              return item;
            })
            .filter((item) => {
              return item.distance >= 0;
            })
            .sort((item2, item1) => {
              if (item1.distance <= item2.distance) return 1;
              if (item1.distance > item2.distance) return -1;
            });
          if (!collisionArr.length) return;

          if (collisionArr[0].distance < 10) {
            gltfModel.walkData.state = "upStage";
            collisionFilterMask: 2 ** 1123; // 碰撞过滤
            body.avoidRepeat = true;
            body.sleep(); // 立即睡眠
            // 修正速度和位置
            const { x, y, z } = speedVector;
            body.velocity = new CANNON.Vec3(x, y, z);

            // 上台阶
            const { x: x1, y: y1, z: z1 } = gltfModel.position;
            const _vector = vector
              .clone()
              .normalize()
              .multiplyScalar(collisionArr[0].distance + 1);
            const position = [x1 + _vector.x, y1 + collisionArr[0].object._height, z1 + _vector.z]; // 高度上到台阶上面,然后位置稍微往前挪1个单位的距离
            body.position.set(...position);
            gltfModel.position.set(...position);

            // 结束
            gltfModel.walkData.upStage_timer = setTimeout(() => {
              if (!gltfModel.walkData) return;
              // 唤醒睡眠
              body.applyImpulse(new CANNON.Vec3(0.0001, 0.0001, 0.0001));
              collisionFilterMask: -1; // 碰撞过滤恢复
              body.avoidRepeat = false; // 防重复字段,重置
              gltfModel.walkData.state = "walk";
            }, 50);
          }
        };

        // 下台阶
        gltfModel.walk_downStage = function () {
          const { state, cannon: body, vector, speed } = gltfModel.walkData;
          const speedVector = vector.clone().normalize().setLength(speed);
          if (state !== "walk") return;

          // 碰撞检测,采样200+个点
          const distanceArr = []; // 距离数组
          const originArr = []; // 点数组
          const { length, height, width } = gltfModel._size;
          originArr.push(...new THREE.LineCurve3(new THREE.Vector3(-length / 2, -height / 2, -width / 2), new THREE.Vector3(length / 2, -height / 2, -width / 2)).getPoints(50));
          originArr.push(...new THREE.LineCurve3(new THREE.Vector3(length / 2, -height / 2, -width / 2), new THREE.Vector3(length / 2, -height / 2, width / 2)).getPoints(50));
          originArr.push(...new THREE.LineCurve3(new THREE.Vector3(length / 2, -height / 2, width / 2), new THREE.Vector3(-length / 2, -height / 2, width / 2)).getPoints(50));
          originArr.push(...new THREE.LineCurve3(new THREE.Vector3(-length / 2, -height / 2, width / 2), new THREE.Vector3(-length / 2, -height / 2, -width / 2)).getPoints(50));
          originArr.forEach((origin) => {
            const origin_t = origin.applyMatrix4(gltfModel.matrix).add(new THREE.Vector3(0, 2, 0));
            let raycaster = new THREE.Raycaster(origin_t, new THREE.Vector3(0, -1, 0), 0.1, 100000);
            let intersects = raycaster.intersectObjects(arr);
            distanceArr.push(intersects[0]?.distance || 0);
          });

          // 最小距离
          const minDistance = Math.min(...distanceArr) - 2;
          if (minDistance > 5) {
            gltfModel.walkData.state = "downStage";
            collisionFilterMask: 2 ** 1123; // 碰撞过滤
            body.avoidRepeat = true;
            body.sleep(); // 立即睡眠

            // 修正速度和位置
            const { x, y, z } = speedVector;
            body.velocity = new CANNON.Vec3(x, y, z);

            // 下台阶
            const { x: x1, y: y1, z: z1 } = gltfModel.position;
            const _vector = vector.clone().normalize();
            const position = [x1 + _vector.x, y1 - minDistance, z1 + _vector.z];
            body.position.set(...position);
            gltfModel.position.set(...position);

            // 结束
            gltfModel.walkData.downStage_timer = setTimeout(() => {
              if (!gltfModel.walkData) return;
              // 唤醒睡眠
              body.applyImpulse(new CANNON.Vec3(0.0001, 0.0001, 0.0001));
              collisionFilterMask: -1; // 碰撞过滤恢复
              body.avoidRepeat = false; // 防重复字段,重置
              gltfModel.walkData.state = "walk";
            }, 50);
          }
        };

        // 结束移动
        gltfModel.walk_over = function (destination, cameraOriginPos, moveVector) {
          gltfModel.position.set(destination.x, destination.y, destination.z);
          camera.position.copy(cameraOriginPos.clone().add(moveVector));
          controls.target.copy(destination);
          gltfModel.idleAction.play();
          gltfModel.walkAction.stop();

          // 清理定时器
          gltfModel.walkData?.timer && clearTimeout(gltfModel.walkData.timer);
          gltfModel.walkData.downStage_timer && clearTimeout(gltfModel.walkData.downStage_timer);
          gltfModel.walkData.upStage_timer && clearTimeout(gltfModel.walkData.upStage_timer);
          gltfModel.walkData.refreshFrameId && cancelAnimationFrame(gltfModel.walkData.refreshFrameId);
          gltfModel.walkData.watchFrameId && cancelAnimationFrame(gltfModel.walkData.watchFrameId);

          // 移除数据
          gltfModel.walkData?.cannon && world.removeBody(gltfModel.walkData.cannon);
          gltfModel.walkData = null;
        };
      });

      // 点击地面,移动模型
      !(function () {
        let initPos = null;

        // 初始位置
        renderer.domElement.addEventListener("pointerdown", function (event) {
          event = event || window.event;
          initPos = {
            x: event.clientX,
            y: event.clientY,
          };
        });

        // 监听pointerup
        renderer.domElement.addEventListener("pointerup", function (event) {
          event = event || window.event;
          // 鼠标位移超过2像素,视为移动,不触发pointerup事件
          if (!initPos || Math.abs(initPos.x - event.clientX) > 2 || Math.abs(initPos.y - event.clientY) > 2) return;
          initPos = null;

          // 触点坐标
          let x = event.clientX - renderer.domElement.getBoundingClientRect().left;
          let y = event.clientY - renderer.domElement.getBoundingClientRect().top;
          let canvasWidth = renderer.domElement.clientWidth; // canvas画布宽度
          let canvasHeight = renderer.domElement.clientHeight; // canvas画布高度

          // 归一化
          var sx = -1 + ((x * 2) / canvasWidth) * 2;
          var sy = 1 - ((y * 2) / canvasHeight) * 2;

          // 射线投射器
          var raycaster = new THREE.Raycaster();
          raycaster.setFromCamera(new THREE.Vector2(sx, sy), camera);

          // 碰撞检测
          var intersects = raycaster.intersectObjects(arr);

          // 拾取成功
          if (intersects.length > 0) {
            controls.autoRotate = false; // 关掉控制器自动旋转

            // 清理上一次移动
            if (gltfModel.walkData) {
              const currPos = gltfModel.position.clone();
              const { cameraOriginPos, originPos } = gltfModel.walkData;
              gltfModel.walk_over(currPos, cameraOriginPos, currPos.clone().sub(originPos));
            }

            // 开启新移动
            const { x, y, z } = intersects[0].point; // 目标位置
            const dest = new THREE.Vector3(x, y + gltfModel._size.height / 2, z);
            gltfModel.walk(dest);
          }
        });
      })();

      setTimeout(() => {
        controls.autoRotate = false; // 关掉控制器自动旋转

        // 清理上一次移动
        if (gltfModel.walkData) {
          const currPos = gltfModel.position.clone();
          const { cameraOriginPos, originPos } = gltfModel.walkData;
          gltfModel.walk_over(currPos, cameraOriginPos, currPos.clone().sub(originPos));
        }

        // 开启新移动
        const dest = new THREE.Vector3(900, 0 + gltfModel._size.height / 2, -700);
        gltfModel.walk(dest);
      }, 3000);

      setTimeout(() => {
        // 清理上一次移动
        if (gltfModel.walkData) {
          const currPos = gltfModel.position.clone();
          const { cameraOriginPos, originPos } = gltfModel.walkData;
          gltfModel.walk_over(currPos, cameraOriginPos, currPos.clone().sub(originPos));
        }

        // 开启新移动
        const dest = new THREE.Vector3(0, 0 + gltfModel._size.height / 2, -700);
        gltfModel.walk(dest);
      }, 7000);

      setTimeout(() => {
        // 清理上一次移动
        if (gltfModel.walkData) {
          const currPos = gltfModel.position.clone();
          const { cameraOriginPos, originPos } = gltfModel.walkData;
          gltfModel.walk_over(currPos, cameraOriginPos, currPos.clone().sub(originPos));
        }

        // 开启新移动
        const dest = new THREE.Vector3(0, 200 + gltfModel._size.height / 2, 0);
        gltfModel.walk(dest);
      }, 9000);

      setTimeout(() => {
        // 清理上一次移动
        if (gltfModel.walkData) {
          const currPos = gltfModel.position.clone();
          const { cameraOriginPos, originPos } = gltfModel.walkData;
          gltfModel.walk_over(currPos, cameraOriginPos, currPos.clone().sub(originPos));
        }

        // 开启新移动
        const dest = new THREE.Vector3(0, 200 + gltfModel._size.height / 2, -50);
        gltfModel.walk(dest);
      }, 12000);
    </script>
  </body>
</html>