three.js 三维模型交互相关

2022-06-05 14 three.js

介绍

three.js 三维模型交互相关的内容

# 什么是三维模型的节点

开发一些web3d项目,比如一个小区、学校的可视化,场景中会有多个楼房模型对象,这时候程序员通过什么方式区分每一栋楼是一个问题。

通过获取指定模型的节点 我们可以和其建立交互效果 就跟前端的获取Dom 绑定事件是一样的

# 三维建模软件和three.js模型名称对应

  • 一般三维建模软件的目录树都有模型的名称,three.js加载外部模型,外部模型的名称会解析为three.js对象的 .name属性。
  • 类似以前端的Dom节点一样 我们通过class或者id给其绑定一个类名 然后通过一些选择Dom的方法 绑定其Dom节点 就可以实现一些交互效果

# 获取三维模型的节点 .getObjectByName()

  • three.js通过基类Object3D (opens new window).getObjectByName('模型节点名称name')方法快速查找某个模型对象,就像普通前端绑定DOM的方法 getElementById()一样。
    • .getObjectByName() 不光具备绑定模型节点操作节点 也可以进行模型节点进行筛选(类似于filter) 筛选符合条件的模型节点内容 找不到会返回undefined
  • 我们可以通过给模型对象添加父类 通过获取父类的三维模型的节点 实现批量修改模型对象的方式
const model = new THREE.Group();//声明一个组对象,用来添加加载成功的三维场景
const loader = new GLTFLoader(); //创建一个GLTF加载器
loader.load("./scene/scene.gltf", function (gltf) {
    model.add(gltf.scene);
    
    // 我有两个模型 名称为: 立方体 和 Cylinder 我们通过getObjectByName()获取其三维模型的节点 
    const mesh = gltf.scene.getObjectByName('立方体');
    console.log('立方体',mesh);//控制台查看返回结果

    const Cylinder = gltf.scene.getObjectByName('Cylinder');//圆柱体
    console.log('圆柱体',Cylinder);//控制台查看返回结果
    
    // 查找符合条件的name名称
    const ret = gltf.scene.getObjectByName('粮仓')
    // 查找符合条件的name
    console.log(ret)
    // 我们还可以批量修改某个模型的内容 比如以上两模型都在 Group这个父类 通过获取父类批量修改里面的子类
    var group = gltf.scene.getObjectByName('Group');//获得立方体和Cylinder的父对象Group
    // 批量更改group所有后代的颜色
    group.children.forEach(function(mesh){
        mesh.material.color.set(0x00ffff);
    })
    
})
复制代码

# 模型命名

  • 命名:中文名、英文名、汉语拼音都可以,可以根据公司规定或开发习惯命名。

  • 规范:模型命名也可以参考编程命名规范,目前大多数公司提倡英文命名,中文命名很少见。

  • 相关:模型名称可以尝试参考前后端API接口名称,比如前后端接口定义,P_01对应1号平房粮仓,P_02对应2号平房粮仓,那么美术和前端之间模型的名称也可以使用P_01、P_02分别表示1号平房粮仓、2号平房粮仓。毕竟前端WebGL工程师要同时和后端、美术协作,名称都一致更方便。

# 命名其他问题

  • 通过三维软件建模的时候,每个模型软件都会默认生成一个名字( 唯一名字 ),如果前端代码不需要查询某个模型进行特定操作,那么该模型名称随意是什么都无所谓 并不会重名 除非自己修改。
  • 如果需要选中进行特定操作的模型,一般需要处于独立状态,不能和其它Mesh合在一起,比如需要获取模型世界坐标、射线拾取模型对象等。

命名建议

  1. 需要进行交互的模型 一定要独立绘制 并且名称需要唯一
  2. 需要交互的不同模型 建议创建父级名称进行归类

image-20220609175247508

# 鼠标交互 射线拾取操作

通过.getObjectByName (opens new window)筛选出需要交互的模型 然后通过Object3D.traverse (opens new window)方法遍历递归出他们的网格模型Mesh 最后通过 Raycaster光摄投线 (opens new window)的方法进行鼠标拾取(在三维空间中计算出鼠标移过了什么物体)判断是否点击的是需要交互的网格模型节点 如果是 那么就给选中的网格模型节点进行一些交互效果

射线拾取网格模型——四步走

  1. 获取需要鼠标交互的网格模型集合(父对象) .getObjectByName (opens new window)
  2. 坐标转化(鼠标单击的屏幕坐标转WebGL标准设备坐标)
  3. 射线生成计算 (通过鼠标单击位置+相机参数计算射线值)
  4. 射线拾取计算.intersectObjects (opens new window)

# 获取需要鼠标交互的网格模型集合

// 声明一个储存需要交互的网格模型的数组
const granaryArr = []
loader.load("建模文件.glb", function (gltf) {//gltf加载成功后返回一个对象
    // // 所有需要交互的网格模型父对象名称:'粮仓'
    const group = gltf.scene.getObjectByName('粮仓');
    //console.log('粮仓', group);
    group.traverse(function (obj) {
        if (obj.type === 'Mesh') {
            granaryArr.push(obj);
        }
    })
    model.add(gltf.scene);
})
复制代码

# 坐标转化

  • 鼠标单击canvas画布,通过返回事件对象属性 window.event.clientXwindow.event.clientY鼠标单机位置的屏幕坐标,然后把屏幕坐标转化为WebGL标准设备坐标,WebGL标准设备坐标坐标范围[-1,1]。
    • WebGL标准设备坐标公式:
      • 宽: ( 当前鼠标宽 / 当前设备的宽 ) * 2 - 1
      • 高: -( 当前鼠标高 / 当前设备的高 ) * 2 + 1
  const Sx = window.event.clientX // 鼠标单击位置横坐标
  const Sy = window.event.clientY // 鼠标单击位置纵坐标
  //屏幕坐标转WebGL标准设备坐标
  const x = (Sx / window.innerWidth) * 2 - 1; //WebGL标准设备横坐标
  const y = -(Sy / window.innerHeight) * 2 + 1; //WebGL标准设备纵坐标
复制代码

# 射线生成计算 .setFromCamera()

把鼠标单击位置坐标和相机参数作为 .setFromCamera(Vector2, Camera) (opens new window)方法的参数,计算射线投射器 Raycaster的射线属性 .ray值。

//创建一个射线投射器`Raycaster`
const raycaster = new THREE.Raycaster();
//通过鼠标单击位置标准设备坐标和相机参数计算射线投射器Raycaster的射线属性 .ray
raycaster.setFromCamera(new THREE.Vector2(x, y), camera);
复制代码

# 射线拾取计算 .intersectObjects()

通过 .intersectObjects(交互模组集合array) (opens new window)方法可以计算出来射线相交的网格模型。

//返回.intersectObjects()参数中射线选中的网格模型对象
// 未选中对象返回空数组[],选中一个数组1个元素,选中两个数组两个元素
const intersects = raycaster.intersectObjects([boxMesh, sphereMesh, cylinderMesh]);
复制代码

# 射线拾取获取场景点击坐标

  • 通过射线拾取计算 .intersectObjects() 可以获得当前拾取(场景点击)的 x y z坐标 他是一个Vector3三维向量 Object3D对象可以直接通过.copy(Vector3) (opens new window) 进行 x y z赋值
  • 有时候会有多个射线拾取 其实是模型重叠 把后面的模型也选中了 **.renderOrder (opens new window)**设置渲染顺序即可(不确定) 或者选中第一个对象
Object3D.position.copy(intersects[0].point) // 选择第一个对象 有时候模型重叠会存在多个对象 但是点击对象通常是第一个
复制代码

# 整体写法Vue3

  • 我们需要记录 上一次所选中的网格模型对象 当选择下一个的时候 让原来那个恢复未选中的样式效果
  • 修改材质颜色的时候 需要用 颜色(Color)的color.set (opens new window) 方法进行材质颜色修改
  •  注意: 如果材质是一张贴图 将无法修改
<template>
  <div>
    <!-- 渲染模板 -->
    <div ref="stateDom" @click="checked" />
  </div>
</template>
<script setup>
// 引入Three.js
import * as THREE from 'three'
// 导入Vue组合API
import { reactive } from 'vue'
import { camera } from './settings/RendererCamera.js'
// 获取需要交互的模型对象集合
import { contentModel } from './settings/model.js'
// 储存选中的网格模型对象
const context = reactive({
  chooseMesh: null
})
// 绑定渲染模板的点击事件
const checked = () => {
  // 判断上次是否有选过网格模型对象
  if (context.chooseMesh) {
    context.chooseMesh.material.color.set('#ffffff')// 把上次选中的mesh设置为原来的颜色
  }
  const Sx = window.event.clientX // 鼠标单击位置横坐标
  const Sy = window.event.clientY // 鼠标单击位置纵坐标
  // 屏幕坐标转WebGL标准设备坐标
  const x = (Sx / window.innerWidth) * 2 - 1 // WebGL标准设备横坐标
  const y = -(Sy / window.innerHeight) * 2 + 1 // WebGL标准设备纵坐标
  // 创建一个射线投射器Raycaster
  const raycaster = new THREE.Raycaster()
  // 通过鼠标单击位置标准设备坐标和相机参数计算射线投射器Raycaster的射线属性.ray
  raycaster.setFromCamera(new THREE.Vector2(x, y), camera)
  // 返回.intersectObjects()参数中射线选中的网格模型对象
  // 未选中对象返回空数组[],选中一个数组1个元素,选中两个数组两个元素
  const intersects = raycaster.intersectObjects(contentModel.granaryArr)
  console.log(intersects)
  // console.log("射线器返回的对象", intersects);
  // console.log("射线投射器返回的对象 点point", intersects[0].point);
  // console.log("射线投射器的对象 几何体",intersects[0].object.geometry.vertices)
  // intersects.length大于0说明,说明选中了模型
  if (intersects.length > 0) {
      // 赋值被点中的交互模型
    context.chooseMesh = intersects[0].object
      // 打印当前鼠标点击在场景的坐标 x y z
    console.log(intersects[0].point)
    context.chooseMesh.material.color.set('#00ffff')// 选中改变颜色,这样材质颜色贴图.map和color颜色会相乘
  }
}
</script>
复制代码

# 参考文献

Three.js零基础入门教程(郭隆邦) (opens new window)

Last Updated: 2023/1/9 08:44:30
来发评论吧~
Powered By Valine
v1.4.14
未加载音频 - (ಗ ‸ ಗ )
00:00 / 00:00