引入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 - 8
let y = i%3*4 - 4
let z = -i%3*4 - 8
boxMesh.position.set(x, y, z)
//物体投射阴影
boxMesh.castShadow = true
//物体接收阴影
boxMesh.receiveShadow = true
this.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 = false
break
case "KeyA": //左
case "ArrowLeft":
this.left = false
break
case "KeyD": //右
case "ArrowRight":
this.right = false
break
case "KeyS": //后
case "ArrowDown":
this.back = false
break
case "ShiftLeft": // 加速
this.accelerated = false
break
}
}
let keyDown = (e) => {
console.log(e.code)
switch(e.code){
case "KeyW": //前
case "ArrowUp":
this.forward = true
break
case "KeyA": //左
case "ArrowLeft":
this.left = true
break
case "KeyD": //右
case "ArrowRight":
this.right = true
break
case "KeyS": //后
case "ArrowDown":
this.back = true
break
case "ShiftLeft": //加速
this.accelerated = true
break
case "Space": // 跳
if(this.canJump){
this.velocity.y += 30
}
this.canJump = false
break
}
}
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 -= 4
const 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 = 0
this.velocity.z = 0
this.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 = false
let rightCollide = false
let forwardCollide = false
let backCollide = false
//碰撞检测 collide check
if (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 = 0
this.control.getObject().position.y = 0
this.canJump = true
}
//设置光源大小及跟随相机移动
this.followLight.intensity = 2
this.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 = true
this.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 = false
break
case "KeyA": //左
case "ArrowLeft":
this.left = false
break
case "KeyD": //右
case "ArrowRight":
this.right = false
break
case "KeyS": //后
case "ArrowDown":
this.back = false
break
case "ShiftLeft": // 加速
this.accelerated = false
break
}
}
let keyDown = (e) => {
console.log(e.code)
switch(e.code){
case "KeyW": //前
case "ArrowUp":
this.forward = true
break
case "KeyA": //左
case "ArrowLeft":
this.left = true
break
case "KeyD": //右
case "ArrowRight":
this.right = true
break
case "KeyS": //后
case "ArrowDown":
this.back = true
break
case "ShiftLeft": //加速
this.accelerated = true
break
case "Space": // 跳
if(this.canJump){
this.velocity.y += 30
}
this.canJump = false
break
}
}
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 / 2
obj.scene.rotation.z = Math.PI / 12
this.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 - 8
let y = i%3*4 - 4
let z = -i%3*4 - 8
boxMesh.position.set(x, y, z)
//物体投射阴影
boxMesh.castShadow = true
//物体接收阴影
boxMesh.receiveShadow = true
this.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 -= 4
const 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 = 0
this.velocity.z = 0
this.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 = false
let rightCollide = false
let forwardCollide = false
let backCollide = false
//碰撞检测 collide check
if (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 = 0
this.control.getObject().position.y = 0
this.canJump = true
}
//设置光源大小及跟随相机移动
this.followLight.intensity = 2
this.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>
本文转载自: https://blog.csdn.net/m0_60648668/article/details/128953617
版权归原作者 怡宝丶加冰 所有, 如有侵权,请联系我们删除。
版权归原作者 怡宝丶加冰 所有, 如有侵权,请联系我们删除。