three.js 第一人称漫游

2023-11-10 13:50
文章标签 js 漫游 three 第一人称

本文主要是介绍three.js 第一人称漫游,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

  1. 引入three.js

这里第一人称所以选择用PointerLockControls控制器

import * as THREE from "three"
import { PointerLockControls } from "three/examples/jsm/controls/PointerLockControls.js"
  1. 创建容器展示场景

<div class="container" ref="container"></div>
  1. 创建对应变量并初始化(我这里使用的vue)

  1. 创建场景

this.scene = new THREE.Scene()
  1. 创建透视相机并设置初始位置

//创建相机
this.camera = new THREE.PerspectiveCamera(75, this.$refs.container.clientWidth/this.$refs.container.clientHeight, 0.1, 2000)
//设置相机位置
this.camera.position.set(0, 0, 50)
  1. 创建渲染器

//创建渲染器
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)
  1. 创建第一人称控制器

//添加第一人称控制器
this.control = new PointerLockControls(this.camera, this.renderer.domElement)
this.scene.add(this.control.getObject()) //this.control.getObject() 返回的是相机对象
//点击屏幕后开始第一人称模式
document.addEventListener('click', () => {this.control.lock()
})
  1. 渲染方法render

render(){//每一帧都执行requestAnimationFrame(this.render)//渲染内容this.renderer.render(this.scene, this.camera)
}
  1. 到这里基本搭建已经完成, 后面就需要添加一些模型(可自行选择添加)

  1. 添加一块地板

//地板 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)
},
  1. 添加一些障碍物(盒子)

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)
},
  1. 因为使用的是网络标准材质(添加光源)

//添加环境光
let AmbientLight = new THREE.AmbientLight(0x404040, 0.8)
this.scene.add(AmbientLight)

操作完成后如下图, 可以看到一块地板及一些障碍物(只能原地踏步)

  1. 逻辑操作

  1. 监听键盘时间来控制控制器移动,从而改变视角效果

//监听键盘事件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)},
  1. 碰撞检测

//根据传入角度判断附近是否有障碍物
//移动是根据按键决定的 在按下左键的时候进行左侧检测 右侧就右侧检测 利用射线判断有没有与物体相交 如果撞到物体上了就阻止这一侧的移动
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
},
  1. 完善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}
  1. 完善之后就可正常移动跳跃了, 最后贴上完整代码

<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 第一人称漫游的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/383039

相关文章

Node.js 中 http 模块的深度剖析与实战应用小结

《Node.js中http模块的深度剖析与实战应用小结》本文详细介绍了Node.js中的http模块,从创建HTTP服务器、处理请求与响应,到获取请求参数,每个环节都通过代码示例进行解析,旨在帮... 目录Node.js 中 http 模块的深度剖析与实战应用一、引言二、创建 HTTP 服务器:基石搭建(一

使用Vue.js报错:ReferenceError: “Vue is not defined“ 的原因与解决方案

《使用Vue.js报错:ReferenceError:“Vueisnotdefined“的原因与解决方案》在前端开发中,ReferenceError:Vueisnotdefined是一个常见... 目录一、错误描述二、错误成因分析三、解决方案1. 检查 vue.js 的引入方式2. 验证 npm 安装3.

JS常用组件收集

收集了一些平时遇到的前端比较优秀的组件,方便以后开发的时候查找!!! 函数工具: Lodash 页面固定: stickUp、jQuery.Pin 轮播: unslider、swiper 开关: switch 复选框: icheck 气泡: grumble 隐藏元素: Headroom

在JS中的设计模式的单例模式、策略模式、代理模式、原型模式浅讲

1. 单例模式(Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点。 示例代码: class Singleton {constructor() {if (Singleton.instance) {return Singleton.instance;}Singleton.instance = this;this.data = [];}addData(value)

Node.js学习记录(二)

目录 一、express 1、初识express 2、安装express 3、创建并启动web服务器 4、监听 GET&POST 请求、响应内容给客户端 5、获取URL中携带的查询参数 6、获取URL中动态参数 7、静态资源托管 二、工具nodemon 三、express路由 1、express中路由 2、路由的匹配 3、路由模块化 4、路由模块添加前缀 四、中间件

EasyPlayer.js网页H5 Web js播放器能力合集

最近遇到一个需求,要求做一款播放器,发现能力上跟EasyPlayer.js基本一致,满足要求: 需求 功性能 分类 需求描述 功能 预览 分屏模式 单分屏(单屏/全屏) 多分屏(2*2) 多分屏(3*3) 多分屏(4*4) 播放控制 播放(单个或全部) 暂停(暂停时展示最后一帧画面) 停止(单个或全部) 声音控制(开关/音量调节) 主辅码流切换 辅助功能 屏

使用JS/Jquery获得父窗口的几个方法(笔记)

<pre name="code" class="javascript">取父窗口的元素方法:$(selector, window.parent.document);那么你取父窗口的父窗口的元素就可以用:$(selector, window.parent.parent.document);如题: $(selector, window.top.document);//获得顶级窗口里面的元素 $(

js异步提交form表单的解决方案

1.定义异步提交表单的方法 (通用方法) /*** 异步提交form表单* @param options {form:form表单元素,success:执行成功后处理函数}* <span style="color:#ff0000;"><strong>@注意 后台接收参数要解码否则中文会导致乱码 如:URLDecoder.decode(param,"UTF-8")</strong></span>

js react 笔记 2

起因, 目的: 记录一些 js, react, css 1. 生成一个随机的 uuid // 需要先安装 crypto 模块const { randomUUID } = require('crypto');const uuid = randomUUID();console.log(uuid); // 输出类似 '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'

学习记录:js算法(二十八):删除排序链表中的重复元素、删除排序链表中的重复元素II

文章目录 删除排序链表中的重复元素我的思路解法一:循环解法二:递归 网上思路 删除排序链表中的重复元素 II我的思路网上思路 总结 删除排序链表中的重复元素 给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。 图一 图二 示例 1:(图一)输入:head = [1,1,2]输出:[1,2]示例 2:(图