虚拟现实VR展厅演示

608 阅读9分钟

今天简单实现一个three.js的小Demo,加强自己对three知识的掌握与学习,只有在项目中才能灵活将所学知识运用起来,话不多说直接开始。

本案例的源代码及相关模型下载链接:点击链接跳转

体验地址:地址

项目搭建

本案例还是借助框架书写three项目,借用vite构建工具搭建vue项目,搭建完成之后,用编辑器打开该项目,在终端执行 npm i 安装一下依赖,安装完成之后终端在安装 npm i three 即可。

重置默认样式:在项目中我们都会用到一些标签,但是这些标签可能本身自带一些默认样式,这些默认样式可能会影响我们的排版布局,如果每次引用就去清除一遍默认样式有点太过繁琐,因此这里需要我们清除一下默认样式。执行如下命令安装第三方包:

npm install reset.css --save

1713686945680_图片.png

配置scss预处理器:SASS是一种预编译的CSS,作用类似于Less,这里我们在vue项目中采用该预处理器进行处理样式,终端执行如下命令安装相应的插件:

npm install sass

配置element-plus组件库:因为本项目需要采用 element-plus 组件库进行创建项目,其官方地址为:element-plus ,所以接下来需要对组件库进行一个安装配置,具体的实现过程如下,终端执行如下安装命令:

npm install element-plus @element-plus/icons-vue

安装完成之后,在入口文件main.js对element的插件进行一个挂载,这里顺便配置一下国际化:

import { createApp } from 'vue' 
import 'reset.css' 
import App from './App.vue' 
import ElementPlus from 'element-plus' // 引入element-plus插件与样式 
import 'element-plus/dist/index.css' 
import zhCn from 'element-plus/dist/locale/zh-cn.mjs' 

createApp(App) 
    .use(ElementPlus, { locale: zhCn }) // 安装element-plus插件并进行国际化配置 
    .mount('#app')

路由配置:因为本项目主要展示的是3D场景的登陆页面,所有这里也是需要配置一些路由的,先我们要先创建几个路由作为路由模块,在src目录下新建一个pages文件夹,其用于存放路由组件相关的内容,如下:

1713687090534_图片.png

安装完插件之后,接下来就可以对路由进行相关配置了,在src,目录下新建router文件,如下:

import { createRouter, createWebHistory } from "vue-router"
 
const routes = [
    {
        path: '/', 
        redirect: '/index', // 重定向
    },
    {
        path: '/index',
        name: 'home',
        component: () => import('../pages/home/index.vue'),
        meta: {
            title: '首页'
        }
    },
    {
        path: '/login',
        name: 'login',
        component: () => import('../pages/login/index.vue'),
        meta: {
            title: '登录页'
        }
    }
]
 
// createRouter用于创建路由器实例,可以管理多个路由
const router = createRouter({
    // 路由的模式的设置
    history: createWebHistory(),
    routes
})
 
export default router

初始化three代码

本次项目使用three.js代码必须要基于下面的基础代码才能实现:

import * as THREE from 'three' 
const scene = new THREE.Scene() 
scene.fog = new THREE.Fog(0x000000, 0, 10000) // 添加雾的效果

初始化相机

const camera = new THREE.PerspectiveCamera(15, window.innerWidth / window.innerHeight, 1, 30000)
// 计算相机距离物体的位置
const distance = window.innerWidth / 2 / Math.tan(Math.PI / 12)
const zAxisNumber = Math.floor(distance - 1400 / 2)
camera.position.set(0, 0, zAxisNumber) // 设置相机所在位置
camera.lookAt(0, 0, 0) // 看向原点
scene.add(camera)

初始化渲染器

const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(window.innerWidth, window.innerHeight)

监听屏幕大小的改变,修改渲染器的宽高和相机的比例

window.addEventListener("resize",()=>{ 
  renderer.setSize(window.innerWidth, window.innerHeight)
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()
})

导入轨道控制器

// 添加轨道控制器
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
 
// 添加控制器
const controls = new OrbitControls(camera, renderer.domElement)
controls.enabled = true // 设置控制是否可用
// 设置缩放范围
controls.minDistance = zAxisNumber // 设置最小缩放距离
controls.maxDistance = zAxisNumber + 500 // 设置最大缩放距离
controls.enableDamping = true // 设置控制阻尼

设置渲染函数

// 设置渲染函数
const render = (time) =>{ 
  controls.update()
  renderer.render(scene,camera)
  requestAnimationFrame(render)
}

页面加载调用

<template>
    <div class="loginBg">
        <div class="login" ref="login"></div>
    </div>
</template>
 
<script setup>
import { ref, onMounted } from 'vue'
 
// 获取div实例对象
let login = ref(null)
onMounted(() => {
    login.value.appendChild(renderer.domElement) // 添加渲染器到div中
    render()    
})
</script>

ok,写完基础代码之后,接下来开始具体的Demo实操。

设置登录界面

首先我们先添加好背景和地球的贴图:

// 加载图片
const SKYIMG = new URL('../../assets/images/sky.png', import.meta.url).href
const EARTHIMG = new URL('../../assets/images/earth_bg.png', import.meta.url).href
// 添加背景
let texture = new THREE.TextureLoader().load(SKYIMG)
const geometry = new THREE.BoxGeometry(window.innerWidth, window.innerHeight, 1400) // 创建立方体
const material = new THREE.MeshBasicMaterial({ // 创建材质
    map: texture, // 纹理贴图
    side: THREE.BackSide, // 背面
})
const mesh = new THREE.Mesh(geometry, material) // 创建网格模型
scene.add(mesh) // 添加到场景

这里顺便设置一下地球模型的自传效果,代码如下我们在render渲染函数中调用一下:

// 球体自转
const renderSphereRotate = () => {
    sphere.rotateY(0.001)
}
// 设置渲染函数
const render = () =>{ 
  controls.update()
  renderer.render(scene, camera)
  requestAnimationFrame(render)
  renderSphereRotate() // 自转
}

接下来我们给正对我们电脑屏幕的角度后面添加星星,让其向着我们的角度进行运动,这里我们要先引入一下星星的图片,这里准备了两张星星的图片,在页面刚加载的时候将获取星星的位置数据,然后在渲染函数中进行调用即可:

onMounted(() => {
    login.value.appendChild(renderer.domElement) // 添加渲染器到div中
    initSceneStar(initZposition) // 初始化星星
    zprogress_first = initSceneStar(zprogress1) // 初始化点1
    zprogress_second = initSceneStar(zprogress2) // 初始化点2
    render()    
})
 
// 设置渲染函数
const render = () =>{ 
  controls.update()
  renderer.render(scene, camera)
  requestAnimationFrame(render)
  renderSphereRotate() // 自转
  renderStarMove() // 星星移动
}

最终实现的效果如下,总体来说还是不错的:

1713707220018_3493311436a94178bdb558a4c974dcf8.gif 接下来我们给星云设置运动效果,这里借助three中的CatmullRomCurve3创建3维曲线:

// 渲染星云的运动效果 const renderCloudMove = (cloud, route, speed) => { let cloudProgress = 0 // 星云位置 // 创建三维曲线 const curve = new THREE.CatmullRomCurve3(route) // 创建星云的运动轨迹 return () => { if(cloudProgress <= 1) { cloudProgress += speed const point = curve.getPoint(cloudProgress) // 获取当前位置 if (point && point.x) { cloud.position.set(point.x, point.y, point.z) // 设置位置 } } else { cloudProgress = 0 } } }

最终达到的效果如下:

1713707276102_740128fde8944416a8b164926907552d.gif 接下来开始撰写html上面的内容:

1713707330303_图片.png

最终的效果如下,有那味了!

1713707456484_图片.png

camera-controls使用

因为本次项目vr展厅需要我们去进行视角的移动,采用three本身的控制器是无法满足我们的需求的,所以这里我们需要换一个新的控制器去进行视角的移动和切换,首先我们先加载好我们的场景,借助three库自带的GLTFLoader函数来加载场景,GLTFLoader函数是一个用于加载和解析 glTF(GL Transmission Format)文件的 JavaScript 库,其可以让开发人员在Web应用程序中轻松地加载和显示 glTF 格式的3D模型和场景。它提供了一种简单而有效的方式来将 glTF 文件加载到WebGL渲染器中,使开发人员能够通过JavaScript代码轻松地操作和展示3D内容。

接下来我们直接引入该库,然后加载场景,并给场景中添加环境光源:

// 加载GLTF模型 
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; 
// 加载模型 
let gltfLoader = new GLTFLoader(); 
gltfLoader.load("/public/assets/room1/msg.gltf", (gltf) => { scene.add(gltf.scene) }) 
// 添加环境光源 
const ambientLight = new THREE.AmbientLight(0xffffff, 1) 
// 环境光 
scene.add(ambientLight)

添加完成之后,我们运行我们的项目,可以看到如下场景,说明我们的场景已经加载完成:

1713707571956_980cf9294d0c463f9eaee11aad6515e1.gif

接下来设置监控事件来控制相机的移动,代替人视角的移动:

let isDragging = false // 判断是否拖动
// 获取容器div点击事件
const handleClick = (e) => {
  // 如果发生了拖动,则不执行点击事件
  if (isDragging) return
  // 获取鼠标位置
  mouse.x = (e.offsetX / window.innerWidth) * 2 - 1 
  mouse.y = -(e.offsetY / window.innerHeight) * 2 + 1 
  // 计算射线坐标
  raycaster.setFromCamera(mouse, camera)
  // 计算物体和射线的焦点
  const intersects = raycaster.intersectObjects(eventMeshs)
  // 判断是否有焦点
  const mesh = intersects[0]
  if (mesh) {
    const v3 = mesh.point // 获取焦点位置
    if (mesh.object.name === 'meishu01') {
      cameraControls.moveTo(v3.x, 1, v3.z, true)
    }
  }
}
 
let startXY
// 获取容器div鼠标按下事件
const handleMouseDown = (e) => {
  // 获取鼠标位置
  startXY = [e.offsetX, e.offsetY]
}
 
// 获取容器div鼠标抬起事件
const handleMouseUp = (e) => {
  // 获取鼠标位置
  const [ endX, endY ] = startXY
  if (Math.abs(e.offsetX - endX) > 3 || Math.abs(endY - e.offsetY) > 3) {
  // 标记发生了拖动
    isDragging = true
  } else {
    // 标记未发生拖动
    isDragging = false
  }
}

最终呈现的效果如下:

1713707651102_65d5a065d90d4a339ffe995dc346845c.gif

添加画框

接下来开始编写相应的函数给展厅场景中添加对应的图片了,如下:

// 添加画框
const loadItem = (items, deepth) => {
  items.forEach(async (item) => {
    // 加入到画布当中
    const { id, url, position, scale, rotation } = item
    // 绘制画框,贴图
    const texture = await new THREE.TextureLoader().loadAsync(url)
    let width, height
    let originwidth = texture.image.width // 获取图片原始宽度
    let originheight = texture.image.height // 获取图片原始高度
    let maxSize = 10 // 最大尺寸
    if (width > maxSize) {
      width = maxSize
      height = (maxSize / originwidth) * originheight
    } else {
      height = maxSize
      width = (maxSize / originheight) * originwidth
    }
    
    const geometry = new THREE.BoxGeometry(width, height, deepth) // 创建画框
    const material = new THREE.MeshBasicMaterial({ color: 0xffffff }) // 创建贴图
    const imgMaterial = new THREE.MeshBasicMaterial({ 
      color: 0xffffff,
      map: texture
    })
    const mesh = new THREE.Mesh(geometry, [ material, material, material, material, material, imgMaterial ]) // 创建画框
    scene.add(mesh)
  })
}

执行如下函数,给图片添加对应的信息,函数如下:

loadItem([
  { 
    url: "/public/assets/pictures2/1.jpg",
    name: "名称",
    desc: "信息描述",
    scale: { x: 0.1, y: 0.1, z: 0.1 },
    position: { x: 24.23375412142995, y: 2.3, z: 10.729648829537796 },
    view: { x: 24.011, y: 2.1, z: 4.379 },
    id: "1",
    rotation: { x: 0, y: 0, z: 0 },
    type: "picture",
  }
], 0.1)

最终呈现的效果如下,总体来说还是不错的,现在的问题就是将图片贴到场景的墙壁上:

1713707747917_图片.png

如何把画框贴到墙壁上,换句话说如何知道画框与墙壁之间的具体位置呢?这里我们需要借助three给我们提供的TransformControls库,使用TransformControls可以为用户提供更直观、友好的界面,使他们能够轻松地在 3D 场景中进行对象的编辑和操作,代码如下:

import { TransformControls } from 'three/examples/jsm/controls/TransformControls';
 
// 实例化TransformControls
const transformControls = new TransformControls(camera, renderer.domElement)
transformControls.setSpace('local') // 设置空间
transformControls.addEventListener('mousedownn', () => {
  controls.enabled = false
})
transformControls.addEventListener('mouseup', () => {
  controls.enabled = true
})
transformControls.addEventListener('objectChange', () => {
  const { position, scale, rotation } = transformControls.object
  console.log(JSON.stringify({ position, scale, rotation: { x: rotation.x, y: rotation.y, z: rotation.z } }))
})
scene.add(transformControls)

我们通过 TransformControls控制器移动画框到墙壁上,并通过监听事件拿到对应的位置数据:

1713707790618_图片.png

后面通过手动修改参数,将图片全部铁道墙壁上,最终达到的效果如下,还是很完美的:

1713707826390_图片.png

后面通过插件zoomtastic来设置点击图片进行放大展示的效果,代码如下:

// 导入第三方库
import Zoomtastic from 'zoomtastic';
// 挂载
Zoomtastic.mount();
 
// 设置画框点击事件
const handleClickPicture = (item) => {
  // 展示当前的图片
  Zoomtastic.show(item.url);
}

现在当我点击对应的图片之后,得到如下结果:

1713707907023_be914e36e3e9433aa4be5fcb9dadbe30.gif

接下来再在场景中添加一个机器人模型:

// 加载机器人模型
let robotLoader = new GLTFLoader();
robotLoader.load("/public/assets/robot/robot.glb", (gltf) => {
  gltf.scene.scale.set(5, 5, 5)
  gltf.scene.position.set(0.1324808945523861, -10.232245896556929, -30.95853005109946)
  eventMeshs.push(gltf.scene)
  gltf.scene.odata = { id: "robot" }
  const mixer = new THREE.AnimationMixer(gltf.scene) // 创建动画控制器
  const ani = gltf.animations[0] // 获取动画
  mixer.clipAction(ani).setDuration(5).play() // 播放动画
  mixer.update(0) // 更新动画
  animateFuns.push(d => mixer.update(d))
  
  scene.add(gltf.scene)
})

最终呈现的效果如下:

1713707954501_ffdb8e07b83d4c22a9ecd8a932d428e2.gif

本案例的源代码及相关模型下载链接:点击链接跳转

体验地址:地址