虚拟现实VR展厅演示

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

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

体验地址:地址

项目搭建

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

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

js 复制代码
npm install reset.css --save

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

js 复制代码
npm install sass

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

js 复制代码
npm install element-plus @element-plus/icons-vue

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

js 复制代码
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文件夹,其用于存放路由组件相关的内容,如下:

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

js 复制代码
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代码必须要基于下面的基础代码才能实现:

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

初始化相机

js 复制代码
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)

初始化渲染器

js 复制代码
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(window.innerWidth, window.innerHeight)

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

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

导入轨道控制器

js 复制代码
// 添加轨道控制器
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 // 设置控制阻尼

设置渲染函数

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

页面加载调用

js 复制代码
<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实操。

设置登录界面

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

js 复制代码
// 加载图片
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渲染函数中调用一下:

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

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

js 复制代码
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() // 星星移动
}

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

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

js 复制代码
// 渲染星云的运动效果 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 } } }

最终达到的效果如下:

接下来开始撰写html上面的内容:

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

camera-controls使用

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

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

js 复制代码
// 加载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)

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

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

js 复制代码
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
  }
}

最终呈现的效果如下:

添加画框

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

js 复制代码
// 添加画框
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)
  })
}

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

js 复制代码
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)

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

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

js 复制代码
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控制器移动画框到墙壁上,并通过监听事件拿到对应的位置数据:

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

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

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

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

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

js 复制代码
// 加载机器人模型
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)
})

最终呈现的效果如下:

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

体验地址:地址

相关推荐
续亮~2 小时前
6、Redis系统-数据结构-05-整数
java·前端·数据结构·redis·算法
顶顶年华正版软件官方3 小时前
剪辑抽帧技巧有哪些 剪辑抽帧怎么做视频 剪辑抽帧补帧怎么操作 剪辑抽帧有什么用 视频剪辑哪个软件好用在哪里学
前端·音视频·视频·会声会影·视频剪辑软件·视频剪辑教程·剪辑抽帧技巧
MarkHD4 小时前
javascript 常见设计模式
开发语言·javascript·设计模式
托尼沙滩裤4 小时前
【js面试题】js的数据结构
前端·javascript·数据结构
不熬夜的臭宝4 小时前
每天10个vue面试题(一)
前端·vue.js·面试
朝阳395 小时前
vue3【实战】来回拖拽放置图片
javascript·vue.js
不如喫茶去5 小时前
VUE自定义新增、复制、删除dom元素
前端·javascript·vue.js
长而不宰5 小时前
vue3+electron项目搭建,遇到的坑
前端·vue.js·electron
阿垚啊5 小时前
vue事件参数
前端·javascript·vue.js
加仑小铁5 小时前
【区分vue2和vue3下的element UI Dialog 对话框组件,分别详细介绍属性,事件,方法如何使用,并举例】
javascript·vue.js·ui