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

news2024/12/25 9:12:30

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

目录

项目搭建

初始化three代码

camera-controls控制器使用

添加画框

画框处理事件

添加机器人模型


项目搭建

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

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

npm install reset.css --save

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

<template>
    <div class="container">
        <Show></Show>
    </div>
</template>

<script setup>
import Show from "./pages/index.vue"

</script>

<style lang="scss">
.container {
    width: 100%;
    height: 100%;
}
</style>

初始化three代码

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

导入three库

import * as THREE from 'three'

初始化场景

const scene = new THREE.Scene()

初始化相机

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

初始化渲染器

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

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

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

导入轨道控制器

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

设置渲染函数: 

const render = () =>{ 
  controls.update()
  renderer.render(scene,camera)
  requestAnimationFrame(render)
}

页面加载调用

<template>
    <div class="exhibition" ref="exhibition"></div>
</template>

<script setup>
import { ref, onMounted } from 'vue'

// 获取dom实例
let exhibition = ref(null)

onMounted(() => {
    exhibition.value.appendChild(renderer.domElement)
    render()
})
</script>

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

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)

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

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

npm i camera-controls

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

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

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

呈现的效果如下所示:

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

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

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

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

<template>
    <div 
      class="exhibition" 
      ref="exhibition" 
      @click="handleClick"
      @mousedown="handleMouseDown" 
      @mouseup="handleMouseUp"
    >
    </div>
</template>

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

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
  }
}

最终呈现的效果如下:

添加画框

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

// 添加画框
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)

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

如何把画框贴到墙壁上,换句话说如何知道画框与墙壁之间的具体位置呢?这里我们需要借助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控制器移动画框到墙壁上,并通过监听事件拿到对应的位置数据:

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

export const items = [
    {
        id: 1,
        url: "./assets/pictures/01.jpg",
        position: { x: 54.44612606517201, y: 3.679549096713978, z: 50.93531019361985 }, 
        scale: { x: 1, y: 1, z: 1 }, 
        rotation: { x: 0, y: 0, z: 0 },
    },
    {
        id: 1,
        url: "./assets/pictures/02.jpg",
        position: { x: 42.53326413580169, y: 3.679549096713978, z: 50.93531019361985 }, 
        scale: { x: 1, y: 1, z: 1 }, 
        rotation: { x: 0, y: 0, z: 0 },
    },
    {
        id: 1,
        url: "./assets/pictures/03.jpg",
        position: { x: 29.007956809298168, y: 3.679549096713978, z: 50.93531019361985 }, 
        scale: { x: 1, y: 1, z: 1 }, 
        rotation: { x: 0, y: 0, z: 0 },
    },
];

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

画框处理事件

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

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

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

npm i zoomtastic

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

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

// 导入第三方库
import Zoomtastic from 'zoomtastic';
// 挂载
Zoomtastic.mount();

// 设置画框点击事件
const handleClickPicture = (item) => {
  // 展示当前的图片
  Zoomtastic.show(item.url);
}

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

添加机器人模型

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

// 加载机器人模型
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)
})

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

let animateFuns = []
const clock = new THREE.Clock();
// 设置渲染函数
const render = () =>{ 
  const delta = clock.getDelta();
  controls.update( delta );
  renderer.render(scene,camera)
  animateFuns.forEach((fun) => {
    fun(delta)
  })
  requestAnimationFrame(render)
}

效果如下:

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

let robotLoader1 = new GLTFLoader();
robotLoader1.load("/public/assets/robot/robot1.gltf", (gltf) => {
  gltf.scene.scale.set(5, 5, 5)
  gltf.scene.position.set(0.25734022000060963, -10.237542382614008, 30.602748751354614)
  gltf.scene.rotation.set(-3.1353226226906985, -0.014796136198272362, -3.141104770940116)
  eventMeshs.push(gltf.scene)
  gltf.scene.odata = { id: "robot1" }
  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)
})

最终呈现的效果如下:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1611400.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一文学会 ts 构建工具 —— tsup

文章目录 能打包什么&#xff1f;安装用法自定义配置文件条件配置在 package.json 中配置多入口打包生成类型声明文件sourcemap生成格式自定义输出文件代码分割产物目标环境支持 es5编译的环境变量对开发命令行工具友好监听模式 watch提供成功构建的钩子 onSuccess压缩产物 min…

LLMs之Llama3:Llama 3的简介、安装和使用方法、案例应用之详细攻略

LLMs之Llama3&#xff1a;Llama 3的简介、安装和使用方法、案例应用之详细攻略 导读&#xff1a;2024年4月18日&#xff0c;Meta 重磅推出了Meta Llama 3&#xff0c;本文章主要介绍了Meta推出的新的开源大语言模型Meta Llama 3。模型架构 Llama 3 是一种自回归语言模型&#x…

gdb 调试常用命令

运行命令 run简写r 运行程序&#xff0c;当遇到断点后&#xff0c;程序会在断点处停止运行continue简写c 运行到下一个断点next简写n 执行下一步语句&#xff0c;不进入函数step简写s 执行下一步语句until运行到循环体结束until 行数运行到某一行call 函数(参数)调用函数finis…

Windows10安装配置nodejs环境

一、下载 下载地址&#xff1a;https://nodejs.cn/download/ ​ 二、安装 1、找到node-v16.17.0-x64.msi安装包, 根据默认提示安装, 过程中间的弹窗不勾选 2、安装完成后, 打开powershell(管理员身份) ​ 3、命令行输入 node -v 和 npm -v 如下图所示则nodejs安装成功 ​ 三…

人大金仓参与编写《通信行业信息技术应用创新发展白皮书(2024年)》正式发布...

近日&#xff0c;由中国信息通信研究院技术与标准研究所主办的通信行业信创发展分论坛在京成功举办&#xff0c;人大金仓受邀分享行业信创实践经验及成果。论坛上正式发布了人大金仓参与编写的《通信行业信息技术应用创新发展白皮书&#xff08;2024年&#xff09;》。 本次论坛…

pycharm创建的项目

pycharm生成django templates删出 settings.py

解锁ApplicationContext vs BeanFactory: 谁更具选择性?

目录 一、聚焦源码回顾 &#xff08;一&#xff09;源码分析和理解 &#xff08;二&#xff09;简短的回顾对比建议 二、ApplicationContext vs BeanFactory特性对比 &#xff08;一&#xff09;主要特性总结 &#xff08;二&#xff09;直接建议 三、案例简单说明 &am…

21.组件组成

组件组成 组件最大的优势就是可复用性 当使用构建步骤时&#xff0c;我们一般会将 Vue 组件定义在一个单独的 .vue 文件中&#xff0c;这被叫做单文件组件(简称 SFC) 组件组成结构 <template><div>承载标签</div> </template> <script> expor…

矽塔SA8321 单通道 2.7-12.0V 持续电流 3.0A H 桥驱动芯片

描述 SA8321是为消费类产品&#xff0c;玩具和其他低压或者电池供电的运动控制类应用提供了一个集成的电机驱动器解决方案。此器件能够驱动一个直流无刷电机&#xff0c;由一个内部电荷泵生成所需的栅极驱动电压电路和4个功率 NMOS组成H桥驱动&#xff0c;集成了电机正转/反…

【ThinkPHP框架教程·Part-01】ThinkPHP6.x框架安装教程

文章目录 一、框架介绍1、框架简介和版本选择2、主要新特性 二、安装步骤1、下载并运行Composer-Setup.exe2、安装TP前切换镜像3、安装稳定版4、测试运行 一、框架介绍 1、框架简介和版本选择 Thinkphp是一种基于php的开源web应用程序开发框架ThinkPHP框架&#xff0c;是免费开…

封装个js分页插件

// 分页插件类 class PaginationPlugin {constructor(fetchDataURL, options {}) {this.fetchDataURL fetchDataURL;this.options {containerId: options.containerId || paginationContainer,dataSizeAttr: options.dataSizeAttr || toatalsize, // 修改为实际API返回的数据…

easyx库的学习(文字绘制)

前言 昨天刚刚写完了基本图形的制作&#xff0c;今天直接可以来看看&#xff0c;在easyx中使用文字 直接看代码吧 文字绘制 void drawTest() {printf("hello,EasyX");//指的是在控制台打印//设置字体大小&#xff0c;样式settextstyle(30, 0, "微软雅黑&quo…

二维码门楼牌管理应用平台建设:核实与审核的关键作用

文章目录 前言一、二维码门楼牌管理应用平台的建设背景二、核实与审核在二维码门楼牌管理中的应用三、核实与审核的重要性四、优化建议 前言 随着信息技术的快速发展&#xff0c;二维码门楼牌管理应用平台在社区管理中发挥着越来越重要的作用。本文将深入探讨该平台建设过程中…

数据结构––kmp算法(串)

kmp算法作为串的一个重要内容&#xff0c;必然有一定的难度&#xff0c;而在看到各类教辅书里的概念与解释后&#xff0c;其晦涩难懂的内容直接劝退一部分人&#xff0c;现在&#xff0c;让我们来看看吧 KMP解决的问题类型 KMP算法的作用就是在一个已知的字符串中查找子串的位…

一次Redis访问超时的“捉虫”之旅

01 引言 作为后端开发人员&#xff0c;对Redis肯定不陌生&#xff0c;它是一款基于内存的数据库&#xff0c;读写速度非常快。在爱奇艺海外后端的项目中&#xff0c;我们也广泛使用Redis&#xff0c;主要用于缓存、消息队列和分布式锁等场景。最近在对一个老项目使用的docker镜…

HTML、CSS常用的vscode插件 +Css reset 和Normalize.css

个人主页&#xff1a;学习前端的小z 个人专栏&#xff1a;HTML5和CSS3悦读 本专栏旨在分享记录每日学习的前端知识和学习笔记的归纳总结&#xff0c;欢迎大家在评论区交流讨论&#xff01; 文章目录 ✍HTML、CSS常用的vscode插件&#x1f34e;1 HTML 标签同步重命名 – Auto Re…

ardunio中自定义的库文件

1、Arduino的扩展库都是放在 libraries目录下的。完整路径为&#xff1a;C:\Users\41861\AppData\Local\Arduino15\libraries 所以我们需要在这个目录下创建一个文件夹&#xff0c;比如上面的例子是esp32上led灯控制程序&#xff0c;于是我创建了 m_led文件夹&#xff08;前面加…

音视频封装格式解析(1)——H264格式简析,I/P/B帧是什么?H264压缩原理

文章目录 1. H264编码参数2. H264编码原理2.1 压缩原理2.2 编码结构解析 3. NALU结构4. H264 annexb模式5. 补充说明5.1 I帧5.2 P帧5.3 B帧 1. H264编码参数 视频质量和⽹络带宽占⽤是相⽭盾的。通常情况下&#xff0c;视频流占⽤的带宽越⾼则视频质量也越⾼&#xff0c;需要的…

zig v0.12.0 发布 — x-cmd 提供 zig 快捷安装方法和 x zig 模块

文章目录 简介功能特点v0.12.0 新特性[重新设计 Autodoc 的工作原理](https://ziglang.org/download/0.12.0/release-notes.html#Redesign-How-Autodoc-Works)语法变更各类标准库变更构建系统变更 常见用法**使用案例**:竞品和相关项目进一步阅读 简介 Zig 是一种通用编程语言…

OpenCV基本图像处理操作(九)——特征匹配

Brute-Force蛮力匹配 Brute-Force蛮力匹配是一种简单直接的模式识别方法&#xff0c;经常用于计算机视觉和数字图像处理领域中的特征匹配。该方法通过逐一比较目标图像中的所有特征点与源图像中的特征点来寻找最佳匹配。这种方法的主要步骤包括&#xff1a; 特征提取&#xff…