使用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介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!