最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 天空之城

    正文概述 掘金(E1e)   2021-03-06   712

    使用Three.js构建天空之城

    壹(序)

    手中有很多模型,其中包括建筑模型,汽车模型若干,人物模型,直升机模型...于是产生了使用这些模型做点事情的想法。 说干就干,但是干什么呢,做个天空之城吧!

    贰(准备工作)

    • 模型并不缺,但是缺少一个天空,我需要构建一个天空盒,最后在github找到一个满意的SkyBox
    • 看着士兵模型,我觉得他们可以去保卫城市;
    • 看着Cesium小人,我觉得让他闲逛就行;
    • 看着直升机,正好建筑模型里面有停机坪;
    • 看着几辆车,建筑模型有两条直直的大马路;

    叁(动手)

    • 先将场景初始化出来
      const threeContainer = document.getElementById('three');
      
      scene = new THREE.Scene();
      
      camera = new THREE.PerspectiveCamera(
        90,
        window.innerWidth / window.innerHeight,
        1,
        2000,
      );
      camera.position.set(0, 50, 300);
      scene.add(camera);
      
      // 地板
      const groundTexture = new THREE.TextureLoader().load(floorBackground);
      groundTexture.wrapS = groundTexture.wrapT = THREE.RepeatWrapping;
      const groundMaterial = new THREE.MeshLambertMaterial({
        map: groundTexture,
      });
      const mesh = new THREE.Mesh(
        new THREE.PlaneBufferGeometry(500, 500),
        groundMaterial,
      );
      mesh.rotation.x = -Math.PI / 2;
      mesh.position.set(50, -1, 50);
      mesh.receiveShadow = true;
      scene.add(mesh);
      
      // 天空盒
      const cubeTextureLoader = new THREE.CubeTextureLoader();
      const texture = cubeTextureLoader.load([
        skyBoxPX,
        skyBoxNX,
        skyBoxPY,
        skyBoxNY,
        skyBoxPZ,
        skyBoxNZ,
      ]);
      scene.background = texture;
      
    • 再将所有模型加载进去,可是加载模型是异步操作,建筑模型是比较大的,人物经常先加载完成,那么我需要等建筑模型加载完成再加载其他模型;需要加载很多模型,那么封装一个函数吧:
       /**
       * 
       * @param {Object} loader Three.js的加载器
       * @param {String} url 模型路径
       * @param {Object} objectOptions 模型需要调整的属性
       * @param {String} modelName 模型名称
       * @param {Function} callback 回调函数
       */
      const loadModel = (loader, url, objectOptions, modelName, callback) => {
        loader.load(url, (object) => {
          console.log(object);
          // 获取模型,gltf需取object.scene,fbx直接取object
          const model = object.scene || object;
          // const model = gltf.scene;
      
          model.traverse((child) => {
            if (child.isMesh) {
              child.castShadow = true;
              child.receiveShadow = true;
            }
          });
      
          Object.keys(objectOptions).forEach((key) => {
            model[key].set(...objectOptions[key]);
          });
      
          if (object.animations.length > 0) {
            const mixer = new THREE.AnimationMixer(model);
            // 士兵有多个动画,使用第二个-RUN
            const action = mixer.clipAction(
              object.animations[1] ? object.animations[1] : object.animations[0],
            );
      
            mixer.type = modelName;
      
            action.play();
            mixers.push(mixer);
          }
      
          scene.add(model);
      
          callback && callback(model);
        });
      };
      
    • 士兵们具有动画,那么让他们绕着城市奔跑,以保卫天空之城; 围城跑的思路:使用requestAnimationFrame更新模型位置,给定一个初始朝向状态(如top),表示此时往什么方向移动,再判断是否已经到达临界点,到达后需转向奔跑;
       const soldierMove = (model, status = 'top') => {
        requestAnimationFrame(() => soldierMove(model, status));
      
        switch (status) {
          case 'top':
            model.position.z -= 0.1;
            if (model.position.z <= -110) {
              model.rotation.y += Math.PI / 2;
              status = 'left';
            }
            break;
          case 'left':
            model.position.x -= 0.1;
            if (model.position.x <= -110) {
              model.rotation.y += Math.PI / 2;
              status = 'bottom';
            }
            break;
          case 'bottom':
            model.position.z += 0.1;
            if (model.position.z >= 210) {
              model.rotation.y += Math.PI / 2;
              status = 'right';
            }
            break;
          case 'right':
            model.position.x += 0.1;
            if (model.position.x >= 210) {
              model.rotation.y += Math.PI / 2;
              status = 'top';
            }
            break;
          default:
            break;
        }
      };
      
    • 汽车的移动
      const moveCarOne = (object, isBack = false) => {
        requestAnimationFrame(() => moveCarOne(object, isBack));
      
        if (isBack) {
          object.position.z += 2;
          if (object.position.z >= 195) {
            isBack = false;
            object.rotation.set(0, 0, 0);
          }
        } else {
          object.position.z -= 2;
          if (object.position.z <= -95) {
            isBack = true;
            object.rotation.set(0, Math.PI, 0);
          }
        }
      };
      
    • 另一个方向汽车,做一个暂停等待的功能;
      const moveCarTwo = (object, isBack, isPause, timer) => {
        if (isBack) {
          object.position.x += 1;
      
          // 回去路上停车等待
          if (!isPause && object.position.x === 30) {
            isPause = true;
            clearInterval(timer);
            setTimeout(() => {
              isPause = false;
              timer = setInterval(
                () => moveCarTwo(object, isBack, isPause, timer),
                60 / 1000,
              );
            }, 1000);
          }
      
          // 调转车头
          if (object.position.x >= 195) {
            isBack = false;
            object.rotation.set(0, Math.PI / 2, 0);
            clearInterval(timer);
            timer = setInterval(
              () => moveCarTwo(object, isBack, isPause, timer),
              60 / 1000,
            );
          }
        } else {
          object.position.x -= 1;
      
          // 暂停等待
          if (!isPause && object.position.x === 70) {
            isPause = true;
            clearInterval(timer);
            setTimeout(() => {
              isPause = false;
              timer = setInterval(
                () => moveCarTwo(object, isBack, isPause, timer),
                60 / 1000,
              );
            }, 1000);
          }
      
          // 调转车头
          if (object.position.x <= -95) {
            isBack = true;
            object.rotation.set(0, (Math.PI * 3) / 2, 0);
            clearInterval(timer);
            timer = setInterval(
              () => moveCarTwo(object, isBack, isPause, timer),
              60 / 1000,
            );
          }
        }
      };
      
    • 直升机的操控,监听键盘事件,对直升机进行操控,前后左右(w/s/a/d)及上下(shift+w/shift+s)
      const onKeyDown = (event) => {
        if (!runDroneAnimation) {
          return;
        }
        switch (event.keyCode) {
          case 87: // w:前
            if (isClickShift) {
              // shift + w:上
              droneModel.position.y += 1;
            } else {
              droneModel.rotation.y = Math.PI;
              droneModel.position.z -= 1;
            }
            break;
          case 83: // s:后
            if (isClickShift) {
              // shift + s:下
              droneModel.position.y -= 1;
            } else {
              droneModel.rotation.y = 0;
              droneModel.position.z += 1;
            }
            break;
          case 65: // a:左
            droneModel.rotation.y = -Math.PI / 2;
            droneModel.position.x -= 1;
            break;
          case 68: // d: 右
            droneModel.rotation.y = Math.PI / 2;
            droneModel.position.x += 1;
            break;
          case 16: // shift
            isClickShift = true;
            break;
          default:
            break;
        }
      };
      
    • Cesium人物模型的移动与其他模型的移动是一样的,只是转向比较多,需要多处理一下,相当于重复功能,所以增加一个漫游功能,随着人物的第一视角漫游;
      // 使用Raycaster,增加鼠标与人物模型的碰撞监测:
      const onMouseClick = (event) => {
        event.preventDefault();
        // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
        mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
        mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
        raycaster.setFromCamera(mouse, camera);
        if (manModel) {
          const intersects = raycaster.intersectObject(manModel, true);
          if (intersects.length > 0) {
            isFlowMan = true;
          }
        }
      };
      // 点击到漫游者后,更改相机position及rotation,实现漫游
      if (isFlowMan) {
        camera.position.set(
          manModel.position.x,
          manModel.position.y + 5,
          manModel.position.z,
        );
        camera.rotation.set(
          manModel.rotation.x,
          manModel.rotation.y + Math.PI,
          manModel.rotation.z,
        );
      }
      

    肆(细节)

    • 自此所有主要功能已完成,再增加一点点细节,比如增加平行光模拟太阳光(天空盒中正好有太阳),但是阴影会让城市看起来太暗,所有增加点光源,调节亮度;
      const light = new THREE.PointLight(0xddeeff, 0.2);
      light.position.set(0, 200, 0);
      scene.add(light);
      
      const dirLight = new THREE.DirectionalLight(0xffffff, 2);
      dirLight.position.set(-500, 500, 500);
      dirLight.castShadow = true;
      dirLight.shadow.camera.far = 1000;
      dirLight.shadow.camera.top = 200;
      dirLight.shadow.camera.bottom = -200;
      dirLight.shadow.camera.left = -200;
      dirLight.shadow.camera.right = 300;
      scene.add(dirLight);
      
    • 还有一开始不启动直升机的动画,而是按下键盘空格再启动,直升机动画效果启动之后才能操控;

    伍(完结)

    完善所有功能及细节后,部署到我的个人博客;

    使天空之城能预览:天空之城


    起源地下载网 » 天空之城

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元