本文主要是介绍three.js 第一人称漫游,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
引入three.js
这里第一人称所以选择用PointerLockControls控制器
import * as THREE from "three"
import { PointerLockControls } from "three/examples/jsm/controls/PointerLockControls.js"
创建容器展示场景
<div class="container" ref="container"></div>
创建对应变量并初始化(我这里使用的vue)
创建场景
this.scene = new THREE.Scene()
创建透视相机并设置初始位置
//创建相机
this.camera = new THREE.PerspectiveCamera(75, this.$refs.container.clientWidth/this.$refs.container.clientHeight, 0.1, 2000)
//设置相机位置
this.camera.position.set(0, 0, 50)
创建渲染器
//创建渲染器
this.renderer = new THREE.WebGLRenderer({ antialias: true }, this.$refs.container)
//渲染器开启阴影计算
this.renderer.shadowMap.enabled = true
//设置渲染器大小
this.renderer.setSize(this.$refs.container.clientWidth, this.$refs.container.clientHeight)
//将渲染器添加到容器中
this.$refs.container.appendChild(this.renderer.domElement)
创建第一人称控制器
//添加第一人称控制器
this.control = new PointerLockControls(this.camera, this.renderer.domElement)
this.scene.add(this.control.getObject()) //this.control.getObject() 返回的是相机对象
//点击屏幕后开始第一人称模式
document.addEventListener('click', () => {this.control.lock()
})
渲染方法render
render(){//每一帧都执行requestAnimationFrame(this.render)//渲染内容this.renderer.render(this.scene, this.camera)
}
到这里基本搭建已经完成, 后面就需要添加一些模型(可自行选择添加)
添加一块地板
//地板 textureLoader是new THREE.TextureLoader()图片加载器
planeMesh(textureLoader){//添加地板let planeGeomotry = new THREE.PlaneGeometry(200, 200, 50, 50)//颜色贴图let mapImg = textureLoader.load('./planeImg/TilesCeramicSubwayOffsetCrackle002_COL_2K.jpg')//ao贴图let aoImg = textureLoader.load('./planeImg/TilesCeramicSubwayOffsetCrackle002_AO_2K.jpg')//凹凸贴图let bumpImg = textureLoader.load('./planeImg/TilesCeramicSubwayOffsetCrackle002_BUMP_2K.jpg')//位移贴图let dispImg = textureLoader.load('./planeImg/TilesCeramicSubwayOffsetCrackle002_DISP_2K.jpg')//法线贴图let nrmImg = textureLoader.load('./planeImg/TilesCeramicSubwayOffsetCrackle002_NRM_2K.jpg')//材质let planeMeshStandardMaterial = new THREE.MeshStandardMaterial({map: mapImg,side: THREE.DoubleSide, //双面aoMap: aoImg,bumpMap: bumpImg,displacementMap: dispImg,normalMap: nrmImg })let plane = new THREE.Mesh(planeGeomotry, planeMeshStandardMaterial)//物体投射阴影// plane.castShadow = true//物体接收阴影plane.receiveShadow = true//旋转地板至水平plane.rotation.x = -Math.PI / 2 //90°//设置地板位置plane.position.set(0, -7, 0)this.scene.add(plane)
},
添加一些障碍物(盒子)
boxMesh(textureLoader){//添加障碍物//颜色贴图let mapBoxImg = textureLoader.load('./planeImg/TilesCeramicSubwayOffsetCrackle002_BUMP_2K.jpg')for(let i = 0; i <= 4; i++){let boxGeometry = new THREE.BoxGeometry(4, 4, 4)let boxMaterial = new THREE.MeshStandardMaterial({map: mapBoxImg})let boxMesh = new THREE.Mesh(boxGeometry, boxMaterial)let x = 4*i - 8let y = i%3*4 - 4let z = -i%3*4 - 8boxMesh.position.set(x, y, z)//物体投射阴影boxMesh.castShadow = true//物体接收阴影boxMesh.receiveShadow = truethis.boxList.push(boxMesh)this.scene.add(boxMesh)}//添加一个长方体let cuboidGeometry = new THREE.BoxGeometry(15, 8, 6)let cuboidMaterial = new THREE.MeshStandardMaterial({map: mapBoxImg})let cuboidMesh = new THREE.Mesh(cuboidGeometry, cuboidMaterial)cuboidMesh.position.set(30, -4, 20)this.scene.add(cuboidMesh)this.boxList.push(cuboidMesh)
},
因为使用的是网络标准材质(添加光源)
//添加环境光
let AmbientLight = new THREE.AmbientLight(0x404040, 0.8)
this.scene.add(AmbientLight)
操作完成后如下图, 可以看到一块地板及一些障碍物(只能原地踏步)
逻辑操作
监听键盘时间来控制控制器移动,从而改变视角效果
//监听键盘事件handleKey(){let keyUp = (e) => {switch(e.code){case "KeyW": //前case "ArrowUp":this.forward = falsebreakcase "KeyA": //左case "ArrowLeft":this.left = falsebreakcase "KeyD": //右case "ArrowRight":this.right = falsebreakcase "KeyS": //后case "ArrowDown":this.back = falsebreakcase "ShiftLeft": // 加速this.accelerated = falsebreak} }let keyDown = (e) => {console.log(e.code)switch(e.code){case "KeyW": //前case "ArrowUp":this.forward = truebreakcase "KeyA": //左case "ArrowLeft":this.left = truebreakcase "KeyD": //右case "ArrowRight":this.right = truebreakcase "KeyS": //后case "ArrowDown":this.back = truebreakcase "ShiftLeft": //加速this.accelerated = truebreakcase "Space": // 跳if(this.canJump){this.velocity.y += 30}this.canJump = falsebreak}}document.addEventListener("keyup", keyUp, false)document.addEventListener("keydown", keyDown, false)},
碰撞检测
//根据传入角度判断附近是否有障碍物
//移动是根据按键决定的 在按下左键的时候进行左侧检测 右侧就右侧检测 利用射线判断有没有与物体相交 如果撞到物体上了就阻止这一侧的移动
collideCheck(angle){let rotationMatrix = new THREE.Matrix4()rotationMatrix.makeRotationY(angle * Math.PI / 180)const cameraDirection = this.control.getDirection(new THREE.Vector3(0, 0, 0)).clone()cameraDirection.applyMatrix4(rotationMatrix)const raycaster = new THREE.Raycaster(this.control.getObject().position.clone(), cameraDirection, 0, 2)raycaster.ray.origin.y -= 4const intersections = raycaster.intersectObjects(this.boxList, true)return intersections.length
},
完善render方法
//渲染方法render(){//每一帧都执行requestAnimationFrame(this.render)let time = performance.now() //本次渲染时间let delta = (time - this.prevTime) / 1000 //时间差 (s)//移动逻辑/*向前向后移动: +1 || -1向左向右移动: -1 || +1 *//*跳跃逻辑执行每一帧动画时控制器都下落 当控制器高度小于起始高度时 还原按下空格时给一个上升的y高度 每一帧动画都会递减*/ if(this.control.isLocked){this.velocity.x = 0this.velocity.z = 0this.velocity.y -= 70 * delta // 下降速度//获取相机位置let position = this.control.getObject().position//设置射线原点this.raycaster.ray.origin.copy(position)this.raycaster.ray.origin.y -= 4//检测所有相交的物体 let intersects = this.raycaster.intersectObjects(this.boxList, false)//自带的方法判断需要过滤if(intersects.length){this.velocity.y = Math.max(0, this.velocity.y)this.canJump = true}if(this.forward || this.back){//向前才可加速this.velocity.z = (Number(this.forward) - Number(this.back)) * this.speed + (this.forward ? Number(this.accelerated)*0.5 : 0)}if(this.left || this.right){this.velocity.x = (Number(this.right) - Number(this.left)) * this.speed + Number(this.accelerated)*0.5}//四个方位是否产生碰撞let leftCollide = falselet rightCollide = falselet forwardCollide = falselet backCollide = false//碰撞检测 collide checkif (this.forward) forwardCollide = this.collideCheck(0)if (this.back) backCollide = this.collideCheck(180)if (this.left) leftCollide = this.collideCheck(90)if (this.right) rightCollide = this.collideCheck(270)//右侧有障碍物时向右移动 置零if ((this.right && rightCollide) || (this.left && leftCollide)) {this.velocity.x = 0}//前方有障碍物时向前移动 置零if ((this.forward && forwardCollide) || (this.back && backCollide)) {this.velocity.z = 0}//设置控制器移动this.control.moveRight(this.velocity.x)this.control.moveForward(this.velocity.z)this.control.getObject().position.y += this.velocity.y * delta //还原起始高度if(this.control.getObject().position.y < 0){this.velocity.y = 0this.control.getObject().position.y = 0this.canJump = true}//设置光源大小及跟随相机移动this.followLight.intensity = 2this.followLight.position.set(...position)}//渲染内容this.renderer.render(this.scene, this.camera)//上次渲染时间this.prevTime = time}
完善之后就可正常移动跳跃了, 最后贴上完整代码
<template><div style="position: relative"><div class="desc"><span>W / ↑ 向前移动</span><span>S / ↓ 向后移动</span><span>A / ← 向左移动</span><span>D / → 向右移动</span><span>shift 加速移动</span><span>space 跳跃</span><span>碰撞到物体不可继续移动</span></div><div class="container" ref="container"></div></div>
</template><script>
import * as THREE from "three"
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"
import { PointerLockControls } from "three/examples/jsm/controls/PointerLockControls.js"
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader" //解析hdr文件
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"
export default {name: 'ThreeDgpFistviews',data() {return {//相机camera: null,//场景scene: null,//控制器control: null,//渲染器renderer: null,//箱子列表boxList: [],//三维向量存储相机位置信息velocity: new THREE.Vector3(),//跟随相机的聚光灯followLight: null,//射线 用来判断是否与物体相交raycaster: null,//上次渲染时间prevTime: 0,//移动速度speed: 0.3,//是否有加速accelerated: false,//能否跳跃canJump: true,//前后左右forward: false,back: false,left: false,right: false};},mounted() {this.init() },methods: {init(){//创建场景this.scene = new THREE.Scene()//给场景添加背景this.scene.background = new THREE.Color('#F2F4FC')//创建相机this.camera = new THREE.PerspectiveCamera(75, this.$refs.container.clientWidth/this.$refs.container.clientHeight, 0.1, 2000)//设置相机位置this.camera.position.set(0, 0, 50)//创建渲染器this.renderer = new THREE.WebGLRenderer({antialias: true //抗锯齿 }, this.$refs.container)//渲染器开启阴影计算this.renderer.shadowMap.enabled = true//设置渲染器大小this.renderer.setSize(this.$refs.container.clientWidth, this.$refs.container.clientHeight)//将渲染器添加到容器中this.$refs.container.appendChild(this.renderer.domElement)//添加轨道控制器// this.control = new OrbitControls(this.camera, this.renderer.domElement)//添加第一人称控制器this.control = new PointerLockControls(this.camera, this.renderer.domElement)this.scene.add(this.control.getObject())document.addEventListener('click', () => {this.control.lock()})//创建射线 长度为2的射线this.raycaster = new THREE.Raycaster(new THREE.Vector3(),new THREE.Vector3(0, -1, 0), 0, 2)//创建图片加载器let textureLoader = new THREE.TextureLoader()//地板this.planeMesh(textureLoader)//障碍物this.boxMesh(textureLoader)//枪模型this.gunModel()//hdr图片解析器// let rgbeLoader = new RGBELoader()// rgbeLoader.load("./hdr/ersisd-Merksem_Appartment_Living_4k.hdr", texture => {// texture.mapping = THREE.EquirectangularReflectionMapping //映射方式圆柱形// //给场景添加背景及环境贴图// this.scene.background = texture// this.scene.environment = texture// })//添加环境光let AmbientLight = new THREE.AmbientLight(0x404040, 0.8)this.scene.add(AmbientLight)//添加直线光// const directionalLight = new THREE.DirectionalLight( 0xffffff, 0.5 )// directionalLight.position.set(20, 30, 10)//设置光照投影// directionalLight.castShadow = true// this.scene.add(directionalLight)//添加点光源let light = new THREE.PointLight(0xffffff, 1, 100)light.position.set(30, 50, -50)//设置光照投影light.castShadow = truethis.scene.add(light)//添加跟随相机的点光源this.followLight = new THREE.PointLight(0xffffff, 0, 30);this.scene.add(this.followLight)//键盘事件this.handleKey()//渲染this.render()},//监听键盘事件handleKey(){let keyUp = (e) => {switch(e.code){case "KeyW": //前case "ArrowUp":this.forward = falsebreakcase "KeyA": //左case "ArrowLeft":this.left = falsebreakcase "KeyD": //右case "ArrowRight":this.right = falsebreakcase "KeyS": //后case "ArrowDown":this.back = falsebreakcase "ShiftLeft": // 加速this.accelerated = falsebreak} }let keyDown = (e) => {console.log(e.code)switch(e.code){case "KeyW": //前case "ArrowUp":this.forward = truebreakcase "KeyA": //左case "ArrowLeft":this.left = truebreakcase "KeyD": //右case "ArrowRight":this.right = truebreakcase "KeyS": //后case "ArrowDown":this.back = truebreakcase "ShiftLeft": //加速this.accelerated = truebreakcase "Space": // 跳if(this.canJump){this.velocity.y += 30}this.canJump = falsebreak}}document.addEventListener("keyup", keyUp, false)document.addEventListener("keydown", keyDown, false)},//添加枪械模型gunModel(){//添加人物模型let glftLoader = new GLTFLoader()glftLoader.load("./models/c2babbdc-e3db-456f-a7ed-cd43e2d9b451.glb", obj => {obj.scene.scale.set(0.02, 0.02, 0.02)obj.scene.position.set(0, 0, 0)obj.scene.rotation.y = Math.PI / 2obj.scene.rotation.z = Math.PI / 12this.scene.add(obj.scene)this.boxList.push(obj.scene)})},//地板planeMesh(textureLoader){//添加地板let planeGeomotry = new THREE.PlaneGeometry(200, 200, 50, 50)//颜色贴图let mapImg = textureLoader.load('./planeImg/TilesCeramicSubwayOffsetCrackle002_COL_2K.jpg')//ao贴图let aoImg = textureLoader.load('./planeImg/TilesCeramicSubwayOffsetCrackle002_AO_2K.jpg')//凹凸贴图let bumpImg = textureLoader.load('./planeImg/TilesCeramicSubwayOffsetCrackle002_BUMP_2K.jpg')//位移贴图let dispImg = textureLoader.load('./planeImg/TilesCeramicSubwayOffsetCrackle002_DISP_2K.jpg')//法线贴图let nrmImg = textureLoader.load('./planeImg/TilesCeramicSubwayOffsetCrackle002_NRM_2K.jpg')//材质let planeMeshStandardMaterial = new THREE.MeshStandardMaterial({map: mapImg,side: THREE.DoubleSide, //双面aoMap: aoImg,bumpMap: bumpImg,displacementMap: dispImg,normalMap: nrmImg })let plane = new THREE.Mesh(planeGeomotry, planeMeshStandardMaterial)//物体投射阴影// plane.castShadow = true//物体接收阴影plane.receiveShadow = true//旋转地板至水平plane.rotation.x = -Math.PI / 2 //90°//设置地板位置plane.position.set(0, -7, 0)this.scene.add(plane)},//箱子障碍物boxMesh(textureLoader){//添加障碍物//颜色贴图let mapBoxImg = textureLoader.load('./planeImg/TilesCeramicSubwayOffsetCrackle002_BUMP_2K.jpg')for(let i = 0; i <= 4; i++){let boxGeometry = new THREE.BoxGeometry(4, 4, 4)let boxMaterial = new THREE.MeshStandardMaterial({map: mapBoxImg})let boxMesh = new THREE.Mesh(boxGeometry, boxMaterial)let x = 4*i - 8let y = i%3*4 - 4let z = -i%3*4 - 8boxMesh.position.set(x, y, z)//物体投射阴影boxMesh.castShadow = true//物体接收阴影boxMesh.receiveShadow = truethis.boxList.push(boxMesh)this.scene.add(boxMesh)}//添加一个长方体let cuboidGeometry = new THREE.BoxGeometry(15, 8, 6)let cuboidMaterial = new THREE.MeshStandardMaterial({map: mapBoxImg})let cuboidMesh = new THREE.Mesh(cuboidGeometry, cuboidMaterial)cuboidMesh.position.set(30, -4, 20)this.scene.add(cuboidMesh)this.boxList.push(cuboidMesh)},//根据传入角度判断附近是否有障碍物//移动是根据按键决定的 在按下左键的时候进行左侧检测 右侧就右侧检测 利用射线判断有没有与物体相交 如果撞到物体上了就阻止这一侧的移动collideCheck(angle){let rotationMatrix = new THREE.Matrix4()rotationMatrix.makeRotationY(angle * Math.PI / 180)const cameraDirection = this.control.getDirection(new THREE.Vector3(0, 0, 0)).clone()cameraDirection.applyMatrix4(rotationMatrix)const raycaster = new THREE.Raycaster(this.control.getObject().position.clone(), cameraDirection, 0, 2)raycaster.ray.origin.y -= 4const intersections = raycaster.intersectObjects(this.boxList, true)return intersections.length},//渲染方法render(){//每一帧都执行requestAnimationFrame(this.render)let time = performance.now() //本次渲染时间let delta = (time - this.prevTime) / 1000 //时间差 (s)//移动逻辑/*向前向后移动: +1 || -1向左向右移动: -1 || +1 *//*跳跃逻辑执行每一帧动画时控制器都下落 当控制器高度小于起始高度时 还原按下空格时给一个上升的y高度 每一帧动画都会递减*/ if(this.control.isLocked){this.velocity.x = 0this.velocity.z = 0this.velocity.y -= 70 * delta // 下降速度//获取相机位置let position = this.control.getObject().position//设置射线原点this.raycaster.ray.origin.copy(position)this.raycaster.ray.origin.y -= 4//检测所有相交的物体 let intersects = this.raycaster.intersectObjects(this.boxList, false)//自带的方法判断需要过滤if(intersects.length){this.velocity.y = Math.max(0, this.velocity.y)this.canJump = true}if(this.forward || this.back){//向前才可加速this.velocity.z = (Number(this.forward) - Number(this.back)) * this.speed + (this.forward ? Number(this.accelerated)*0.5 : 0)}if(this.left || this.right){this.velocity.x = (Number(this.right) - Number(this.left)) * this.speed + Number(this.accelerated)*0.5}//四个方位是否产生碰撞let leftCollide = falselet rightCollide = falselet forwardCollide = falselet backCollide = false//碰撞检测 collide checkif (this.forward) forwardCollide = this.collideCheck(0)if (this.back) backCollide = this.collideCheck(180)if (this.left) leftCollide = this.collideCheck(90)if (this.right) rightCollide = this.collideCheck(270)//右侧有障碍物时向右移动 置零if ((this.right && rightCollide) || (this.left && leftCollide)) {this.velocity.x = 0}//前方有障碍物时向前移动 置零if ((this.forward && forwardCollide) || (this.back && backCollide)) {this.velocity.z = 0}//设置控制器移动this.control.moveRight(this.velocity.x)this.control.moveForward(this.velocity.z)this.control.getObject().position.y += this.velocity.y * delta //还原起始高度if(this.control.getObject().position.y < 0){this.velocity.y = 0this.control.getObject().position.y = 0this.canJump = true}//设置光源大小及跟随相机移动this.followLight.intensity = 2this.followLight.position.set(...position)//让枪跟着镜头// let x = this.control.getObject().position.x + 2// let y = this.control.getObject().position.x + 2// let z = this.control.getObject().position.x + 2// this.boxList[this.boxList.length - 1].position.set(x, y, z)}//渲染内容this.renderer.render(this.scene, this.camera)//上次渲染时间this.prevTime = time}},
};
</script><style scoped>
.container {width:100vw;height:100vh;
}.desc {position: absolute;left:0;top:0;color: coral;opacity: .5;display: flex;flex-direction: column;padding: 20px;
}
</style>
这篇关于three.js 第一人称漫游的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!