0


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)

  2. 创建场景

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. 到这里基本搭建已经完成, 后面就需要添加一些模型(可自行选择添加)

  2. 添加一块地板

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

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

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

  1. 逻辑操作

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

//监听键盘事件
        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)
        },
  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 -= 4
     const 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 = 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
        }
  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 = 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>
标签: javascript 前端 html

本文转载自: https://blog.csdn.net/m0_60648668/article/details/128953617
版权归原作者 怡宝丶加冰 所有, 如有侵权,请联系我们删除。

“three.js 第一人称漫游”的评论:

还没有评论