当前位置:   article > 正文

Three.js--》探秘虚拟现实VR展厅的视觉盛宴

Three.js--》探秘虚拟现实VR展厅的视觉盛宴

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

源码下载地址:地址

在线体验地址:地址

目录

项目搭建

初始化three代码

camera-controls控制器使用

添加画框

画框处理事件

添加机器人模型


项目搭建

本案例还是借助框架书写three项目,借用vite构建工具搭建vue项目,vite这个构建工具如果有不了解的朋友,可以参考我之前对其讲解的文章:vite脚手架的搭建与使用。搭建完成之后,用编辑器打开该项目,在终端执行 npm i 安装一下依赖即可。接下来对项目进行一些初始化操作

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

npm install reset.css --save

因为我搭建的是vue3项目,为了便于代码的可读性,所以我将three.js代码单独抽离放在一个组件当中,在App根组件中进入引入该组件。具体如下:

  1. <template>
  2. <div class="container">
  3. <Show></Show>
  4. </div>
  5. </template>
  6. <script setup>
  7. import Show from "./pages/index.vue"
  8. </script>
  9. <style lang="scss">
  10. .container {
  11. width: 100%;
  12. height: 100%;
  13. }
  14. </style>

初始化three代码

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

导入three库

import * as THREE from 'three'

初始化场景

const scene = new THREE.Scene()

初始化相机

  1. const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
  2. camera.position.set(5, 1, 0)
  3. camera.lookAt(0, 0, 0)
  4. scene.add(camera)

初始化渲染器

  1. const renderer = new THREE.WebGLRenderer({ antialias: true })
  2. renderer.setSize(window.innerWidth, window.innerHeight) // 设置渲染器大小
  3. renderer.setPixelRatio(window.devicePixelRatio) // 设置像素比

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

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

导入轨道控制器

  1. const controls = new OrbitControls(camera, renderer.domElement)
  2. controls.enableDamping = true // 设置控制阻尼

设置渲染函数: 

  1. const render = () =>{
  2. controls.update()
  3. renderer.render(scene,camera)
  4. requestAnimationFrame(render)
  5. }

页面加载调用

  1. <template>
  2. <div class="exhibition" ref="exhibition"></div>
  3. </template>
  4. <script setup>
  5. import { ref, onMounted } from 'vue'
  6. // 获取dom实例
  7. let exhibition = ref(null)
  8. onMounted(() => {
  9. exhibition.value.appendChild(renderer.domElement)
  10. render()
  11. })
  12. </script>

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

camera-controls控制器使用

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

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

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

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

接下来我们开始安装新的控制器,终端执行如下命令安装新的控制器,详情查看:官网

npm i camera-controls

我们在官网的案例中,随便打开一个demo,可以看到该控制器的效果还是不错的,如下:

接下来我们通过three.js 中的 Raycaster(射线投射器):一个用于在3D场景中进行射线投射和检测碰撞的工具。允许开发人员在三维空间中进行准确的拾取操作,即确定射线与场景中的对象是否相交,并获取与射线相交的对象的相关信息。来获取场景当中的模型信息,代码如下:

  1. // 获取容器div点击事件
  2. const handleClick = (e) => {
  3. // 获取鼠标位置
  4. mouse.x = (e.offsetX / window.innerWidth) * 2 - 1
  5. mouse.y = -(e.offsetY / window.innerHeight) * 2 + 1
  6. // 计算射线坐标
  7. raycaster.setFromCamera(mouse, camera)
  8. // 计算物体和射线的焦点
  9. const intersects = raycaster.intersectObjects(scene.children)
  10. // 判断是否有焦点
  11. if (intersects.length > 0) {
  12. console.log(intersects[0].object.name)
  13. }
  14. }

呈现的效果如下所示:

点击事件肯定只适用展厅中的内容,为了防止点击其他模型触发点击事件,我们需要给计算物体和射线的焦点处设置展厅场景内容,具体修改如下:

接下来我们开始引入camera-controls库中的内容:

  1. import * as THREE from 'three'
  2. import CameraControls from 'camera-controls';
  3. CameraControls.install( { THREE: THREE } );

在html中,这里我给了场景容器的div设置了点击事件,鼠标按下和抬起事件,三种事件,点击事件很容易理解,鼠标的按下和抬起事件合并起来就是鼠标的拖动事件:

  1. <template>
  2. <div
  3. class="exhibition"
  4. ref="exhibition"
  5. @click="handleClick"
  6. @mousedown="handleMouseDown"
  7. @mouseup="handleMouseUp"
  8. >
  9. </div>
  10. </template>

接下来就是对这三个事件进行处理了,判断用户是执行了点击事件还是拖动事件:

  1. let isDragging = false // 判断是否拖动
  2. // 获取容器div点击事件
  3. const handleClick = (e) => {
  4. // 如果发生了拖动,则不执行点击事件
  5. if (isDragging) return
  6. // 获取鼠标位置
  7. mouse.x = (e.offsetX / window.innerWidth) * 2 - 1
  8. mouse.y = -(e.offsetY / window.innerHeight) * 2 + 1
  9. // 计算射线坐标
  10. raycaster.setFromCamera(mouse, camera)
  11. // 计算物体和射线的焦点
  12. const intersects = raycaster.intersectObjects(eventMeshs)
  13. // 判断是否有焦点
  14. const mesh = intersects[0]
  15. if (mesh) {
  16. const v3 = mesh.point // 获取焦点位置
  17. if (mesh.object.name === 'meishu01') {
  18. cameraControls.moveTo(v3.x, 1, v3.z, true)
  19. }
  20. }
  21. }
  22. let startXY
  23. // 获取容器div鼠标按下事件
  24. const handleMouseDown = (e) => {
  25. // 获取鼠标位置
  26. startXY = [e.offsetX, e.offsetY]
  27. }
  28. // 获取容器div鼠标抬起事件
  29. const handleMouseUp = (e) => {
  30. // 获取鼠标位置
  31. const [ endX, endY ] = startXY
  32. if (Math.abs(e.offsetX - endX) > 3 || Math.abs(endY - e.offsetY) > 3) {
  33. // 标记发生了拖动
  34. isDragging = true
  35. } else {
  36. // 标记未发生拖动
  37. isDragging = false
  38. }
  39. }

最终呈现的效果如下:

添加画框

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

  1. // 添加画框
  2. const loadItem = (items, deepth) => {
  3. items.forEach(async (item) => {
  4. // 加入到画布当中
  5. const { id, url, position, scale, rotation } = item
  6. // 绘制画框,贴图
  7. const texture = await new THREE.TextureLoader().loadAsync(url)
  8. let width, height
  9. let originwidth = texture.image.width // 获取图片原始宽度
  10. let originheight = texture.image.height // 获取图片原始高度
  11. let maxSize = 10 // 最大尺寸
  12. if (width > maxSize) {
  13. width = maxSize
  14. height = (maxSize / originwidth) * originheight
  15. } else {
  16. height = maxSize
  17. width = (maxSize / originheight) * originwidth
  18. }
  19. const geometry = new THREE.BoxGeometry(width, height, deepth) // 创建画框
  20. const material = new THREE.MeshBasicMaterial({ color: 0xffffff }) // 创建贴图
  21. const imgMaterial = new THREE.MeshBasicMaterial({
  22. color: 0xffffff,
  23. map: texture
  24. })
  25. const mesh = new THREE.Mesh(geometry, [ material, material, material, material, material, imgMaterial ]) // 创建画框
  26. scene.add(mesh)
  27. })
  28. }

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

  1. loadItem([
  2. {
  3. url: "/public/assets/pictures2/1.jpg",
  4. name: "名称",
  5. desc: "信息描述",
  6. scale: { x: 0.1, y: 0.1, z: 0.1 },
  7. position: { x: 24.23375412142995, y: 2.3, z: 10.729648829537796 },
  8. view: { x: 24.011, y: 2.1, z: 4.379 },
  9. id: "1",
  10. rotation: { x: 0, y: 0, z: 0 },
  11. type: "picture",
  12. }
  13. ], 0.1)

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

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

  1. import { TransformControls } from 'three/examples/jsm/controls/TransformControls';
  2. // 实例化TransformControls
  3. const transformControls = new TransformControls(camera, renderer.domElement)
  4. transformControls.setSpace('local') // 设置空间
  5. transformControls.addEventListener('mousedownn', () => {
  6. controls.enabled = false
  7. })
  8. transformControls.addEventListener('mouseup', () => {
  9. controls.enabled = true
  10. })
  11. transformControls.addEventListener('objectChange', () => {
  12. const { position, scale, rotation } = transformControls.object
  13. console.log(JSON.stringify({ position, scale, rotation: { x: rotation.x, y: rotation.y, z: rotation.z } }))
  14. })
  15. scene.add(transformControls)

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

将数据复制到外部存放图片相关信息资源的js文件当中:

  1. export const items = [
  2. {
  3. id: 1,
  4. url: "./assets/pictures/01.jpg",
  5. position: { x: 54.44612606517201, y: 3.679549096713978, z: 50.93531019361985 },
  6. scale: { x: 1, y: 1, z: 1 },
  7. rotation: { x: 0, y: 0, z: 0 },
  8. },
  9. {
  10. id: 1,
  11. url: "./assets/pictures/02.jpg",
  12. position: { x: 42.53326413580169, y: 3.679549096713978, z: 50.93531019361985 },
  13. scale: { x: 1, y: 1, z: 1 },
  14. rotation: { x: 0, y: 0, z: 0 },
  15. },
  16. {
  17. id: 1,
  18. url: "./assets/pictures/03.jpg",
  19. position: { x: 29.007956809298168, y: 3.679549096713978, z: 50.93531019361985 },
  20. scale: { x: 1, y: 1, z: 1 },
  21. rotation: { x: 0, y: 0, z: 0 },
  22. },
  23. ];

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

画框处理事件

在上文讲述贴好图片之后,接下来我们需要给图片设计一个点击事件,拿到当前点击图片的相关讯息进行进一步的处理,首先我们先思考一下,该如何设计点击事件拿到相关讯息呢?步骤如下:

在展厅中当我们点击相应的图片的时候,控制台会给出对应的信息:

ok,拿到相应的图片信息之后,接下来就是如何展示数据了,这里用到一款插件,安装命令如下:

npm i zoomtastic

当然我们也可以在npm平台找到这个对应的包,可以看看相关的使用教程:地址

我们这里就不再具体讲解该包的使用了,这里直接拿来用,上展示:

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

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

添加机器人模型

接下来给场景中添加两个机器人模型并设置一下动画效果,代码如下:

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

在渲染函数的时候调用数组当中的动画数据:

  1. let animateFuns = []
  2. const clock = new THREE.Clock();
  3. // 设置渲染函数
  4. const render = () =>{
  5. const delta = clock.getDelta();
  6. controls.update( delta );
  7. renderer.render(scene,camera)
  8. animateFuns.forEach((fun) => {
  9. fun(delta)
  10. })
  11. requestAnimationFrame(render)
  12. }

效果如下:

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

  1. let robotLoader1 = new GLTFLoader();
  2. robotLoader1.load("/public/assets/robot/robot1.gltf", (gltf) => {
  3. gltf.scene.scale.set(5, 5, 5)
  4. gltf.scene.position.set(0.25734022000060963, -10.237542382614008, 30.602748751354614)
  5. gltf.scene.rotation.set(-3.1353226226906985, -0.014796136198272362, -3.141104770940116)
  6. eventMeshs.push(gltf.scene)
  7. gltf.scene.odata = { id: "robot1" }
  8. const mixer = new THREE.AnimationMixer(gltf.scene) // 创建动画控制器
  9. const ani = gltf.animations[0] // 获取动画
  10. mixer.clipAction(ani).setDuration(5).play() // 播放动画
  11. mixer.update(0) // 更新动画
  12. animateFuns.push(d => mixer.update(d))
  13. scene.add(gltf.scene)
  14. })

最终呈现的效果如下:

ok,后面展厅添加的图片和上文讲解的原理一样,这里就不再赘述了,demo写完给个赞吧!

源码下载地址:地址

在线体验地址:地址

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号