课程表

three.js课程

工具箱
速查手册

Three 层级模型、树结构

当前位置:免费教程 » JS/JS库/框架 » three.js

比如一辆车,在Threejs中你可以使用一个网格模型去描述车上面的一个零件,多个零件就需要多个网格模型表示,这些网格模型之间就会构成父子或兄弟关系,从而形成一个层级结构。在机械、建筑相关的Web3D应用中,通常会用到层级模型的知识,一个层级模型就是一本书的目录一样。

本章主要目的是帮助你建立Threejs层级模型的概念,通过Threejs的组对象Group可以组织各个模型,构成一个层级结构。学习本节课你也可以参考前端中DOM树去理解,Threejs一个一个的模型对象就像HTML元素一样可以组成一个树结构,可以通过特定id或name属性选择某个或某些模型对象。

在具体开发过程中,3D美术给你一个包含多个网格模型对象的层级模型,你可能需要操作某个网格模型,这时候3D美术只要通过对模型命名标记模型,那么对于程序员来说,直接调用Threejs的某个方法就可以遍历整个模型,找到某个你想要操作的模型对象。


组对象Group、层级模型

本节课的目的是为了大家建立层级模型的概念,所谓层级模型,比如一个机器人,人头、胳膊都是人的一部分,眼睛是头的一部分,手是个胳膊的一部分,手指是手的一部分...这样的话就构成一个一个层级结构或者说树结构。

Group案例

在详细讲解层级模型之前先通过Threejs的类Group实现一个网格模型简单的案例。

下面代码创建了两个网格模型mesh1、mesh2,通过THREE.Group类创建一个组对象group,然后通过add方法把网格模型mesh1、mesh2作为设置为组对象group的子对象,然后在通过执行scene.add(group)把组对象group作为场景对象的scene的子对象。也就是说场景对象是scene是group的父对象,group是mesh1、mesh2的父对象。这样就构成了一个三层的层级结构,当然了你也可以通过Group自己创建新模型节点作为层级结构中的一层。

//创建两个网格模型mesh1、mesh2
var geometry = new THREE.BoxGeometry(20, 20, 20);
var material = new THREE.MeshLambertMaterial({color: 0x0000ff});
var group = new THREE.Group();
var mesh1 = new THREE.Mesh(geometry, material);
var mesh2 = new THREE.Mesh(geometry, material);
mesh2.translateX(25);
//把mesh1型插入到组group中,mesh1作为group的子对象
group.add(mesh1);
//把mesh2型插入到组group中,mesh2作为group的子对象
group.add(mesh2);
//把group插入到场景中作为场景子对象
scene.add(group);

网格模型mesh1、mesh2作为设置为父对象group的子对象,如果父对象group进行旋转、缩放、平移变换,子对象同样跟着变换,就像你的头旋转了,眼睛会跟着头旋转。

//沿着Y轴平移mesh1和mesh2的父对象,mesh1和mesh2跟着平移
group.translateY(100);
//父对象缩放,子对象跟着缩放
group.scale.set(4,4,4);
//父对象旋转,子对象跟着旋转
group.rotateY(Math.PI/6)

查看子对象.children

Threejs场景对象Scene、组对象Group都有一个子对象属性.children,通过该属性可以访问父对象的子对象,子对象属性.children的值是数组,所有子对象是数组的值,你可以在浏览器控制台打印测试上面案例代码。

执行console.log(group.children)你可以在浏览器控制控制看到group的子对象是案例代码中通过add方法添加的两个网格模型对象Mesh。

console.log('查看group的子对象',group.children);

场景对象结构

执行console.log(scene.children)你在浏览器控制台查看场景对象Scene的子对象,除了可以看到案例代码通过add方法添加的组对象group之外,还可以看到通过add方法插入到场景中的环境光AmbientLight、点光源PointLight、辅助坐标对象AxesHelper等子对象。

console.log('查看Scene的子对象',scene.children);

场景对象对象scene构成的层级模型本身是一个树结构,场景对象层级模型的第一层,也就是树结构的根节点,一般来说网格模型Mesh、点模型Points、线模型Line是树结构的最外层叶子结点。构建层级模型的中间层一般都是通过Threejs的Group类来完成,Group类实例化的对象可以称为组对象。

1.jpg

Threejs渲染的时候从根节点场景对象开始解析渲染,如果一个模型要想被渲染出来就要直接或间接插入到场景scene中,一个光源如果要在光照计算中起作用同样需要通过add方法插入到场景中。

.add()方法

场景对象Scene、组对象Group、网格模型对象Mesh、光源对象Light的.add()方法都是继承自它们共同的基类Object3D。

父对象执行.add()方法的本质就是把参数中的子对象添加到自身的子对象属性.children中。

.add()方法可以单独插入一个对象,也可以同时插入多个子对象。

group.add(mesh1);
group.add(mesh2);
group.add(mesh1,mesh2);

.remove()方法

.add()方法是给父对象添加一个子对象,.remove()方法是删除父对象中的一个子对象。 一个对象的全部子对象可以通过该对象的.children()属性访问获得,执行该对象的删除方法.remove()和添加方法.add()一样改变的都是父对象的.children()属性。

场景Scene或组对象Group的.remove()方法使用规则可以查看它们的基类Object3D。

// 删除父对象group的子对象网格模型mesh1
group.remove(mesh1)
// 一次删除场景中多个对象
scene.remove(light,group)

在线运行案例


层级模型节点命名、查找、遍历

上一节说过Threejs场景对象Scene和各种子对象构成的层级模型就是一个树结构。如果你有一定的算法基础对树结构肯定会非常了解,如果你了解前端的DOM树结构也非常有助于本节课的学习,如果这些都不了解也没有关系,直接体验本节课的案例源码。

本文通过Three.js的一个类Group来介绍Threejs层级模型的概念,如果你对WebGL层级模型已经有一定的概念,直接把重点放在Group的了解上,如果没有层级模型的概念,就借着对Three.js APIGroup的介绍了解下该概念。

模型命名(.name属性)

在层级模型中可以给一些模型对象通过.name属性命名进行标记。

group.add(Mesh)
// 网格模型命名
Mesh.name = "眼睛"
// mesh父对象对象命名
group.name = "头"

树结构层级模型

实际开发的时候,可能会加载外部的模型,然后从模型对象通过节点的名称.name查找某个子对象,为了大家更容易理解,本节课不加载外部模型,直接通过代码创建一个非常简易的机器人模型,然后在机器人基础上进行相关操作。

// 头部网格模型和组
var headMesh = sphereMesh(10, 0, 0, 0);
headMesh.name = "脑壳"
var leftEyeMesh = sphereMesh(1, 8, 5, 4);
leftEyeMesh.name = "左眼"
var rightEyeMesh = sphereMesh(1, 8, 5, -4);
rightEyeMesh.name = "右眼"
var headGroup = new THREE.Group();
headGroup.name = "头部"
headGroup.add(headMesh, leftEyeMesh, rightEyeMesh);
// 身体网格模型和组
var neckMesh = cylinderMesh(3, 10, 0, -15, 0);
neckMesh.name = "脖子"
var bodyMesh = cylinderMesh(14, 30, 0, -35, 0);
bodyMesh.name = "腹部"
var leftLegMesh = cylinderMesh(4, 60, 0, -80, -7);
leftLegMesh.name = "左腿"
var rightLegMesh = cylinderMesh(4, 60, 0, -80, 7);
rightLegMesh.name = "右腿"
var legGroup = new THREE.Group();
legGroup.name = "腿"
legGroup.add(leftLegMesh, rightLegMesh);
var bodyGroup = new THREE.Group();
bodyGroup.name = "身体"
bodyGroup.add(neckMesh, bodyMesh, legGroup);
// 人Group
var personGroup = new THREE.Group();
personGroup.name = "人"
personGroup.add(headGroup, bodyGroup)
personGroup.translateY(50)
scene.add(personGroup);

// 球体网格模型创建函数
function sphereMesh(R, x, y, z) {
  var geometry = new THREE.SphereGeometry(R, 25, 25); //球体几何体
  var material = new THREE.MeshPhongMaterial({
    color: 0x0000ff
  }); //材质对象Material
  var mesh = new THREE.Mesh(geometry, material); // 创建网格模型对象
  mesh.position.set(x, y, z);
  return mesh;
}
// 圆柱体网格模型创建函数
function cylinderMesh(R, h, x, y, z) {
  var geometry = new THREE.CylinderGeometry(R, R, h, 25, 25); //球体几何体
  var material = new THREE.MeshPhongMaterial({
    color: 0x0000ff
  }); //材质对象Material
  var mesh = new THREE.Mesh(geometry, material); // 创建网格模型对象
  mesh.position.set(x, y, z);
  return mesh;
}

递归遍历方法.traverse()

Threejs层级模型就是一个树结构,可以通过递归遍历的算法去遍历Threejs一个模型对象的所有后代,可以通过下面代码递归遍历上面创建一个机器人模型或者一个外部加载的三维模型。

scene.traverse(function(obj) {
  if (obj.type === "Group") {
    console.log(obj.name);
  }
  if (obj.type === "Mesh") {
    console.log('  ' + obj.name);
    obj.material.color.set(0xffff00);
  }
  if (obj.name === "左眼" | obj.name === "右眼") {
    obj.material.color.set(0x000000)
  }
  // 打印id属性
  console.log(obj.id);
  // 打印该对象的父对象
  console.log(obj.parent);
  // 打印该对象的子对象
  console.log(obj.children);
})

查找某个具体的模型

看到Threejs的.getObjectById()、.getObjectByName()等方法,如果已有前端基础,很容易联想到DOM的一些方法。

Threejs和前端DOM一样,可以通过一个方法查找树结构父元素的某个后代对象,对于普通前端而言可以通过name或id等方式查找一个或多个DOM元素,Threejs同样可以通过一些方法查找一个模型树中的某个节点。更多的查找方法和方法的使用细节可以查看基类Object3D。

// 遍历查找scene中复合条件的子对象,并返回id对应的对象
var idNode = scene.getObjectById ( 4 );
console.log(idNode);
// 遍历查找对象的子对象,返回name对应的对象(name是可以重名的,返回第一个)
var nameNode = scene.getObjectByName ( "左腿" );
nameNode.material.color.set(0xff0000);

在线运行案例


获得世界坐标.getWorldPosition()

通过前两节课的学习,想必你已经对Threejs的层级模型有了一定认识,那么本节课就在层级模型概念的基础上,继续给家讲解两个新的概念,即本地坐标系和世界坐标系。

如果你对本地坐标系和世界坐标系已经有了一定概念,那么可以直接访问模型的位置属性.position获得模型在本地坐标系或者说模型坐标系下的三维坐标,通过模型的.getWorldPosition()方法获得该模型在世界坐标下的三维坐标。

.getWorldPosition()方法

模型对象的方法.getWorldPosition()方法和位置属性.position一样继承自基类Object3D。

// 声明一个三维向量用来保存世界坐标
var worldPosition = new THREE.Vector3();
// 执行getWorldPosition方法把模型的世界坐标保存到参数worldPosition中
mesh.getWorldPosition(worldPosition);

建立世界坐标系概念

如果你没有本地坐标系和世界坐标系的概念,可以通过下面的案例源码很快的建立两个坐标系的概念。

你首先在案例中测试下面源码,通过位置属性.position和.getWorldPosition()分别返回模型的本地位置坐标和世界坐标,查看两个坐标x分量有什么不同。你可以看到网格模型mesh通过位置属性.position返回的坐标x分量是50,通过.getWorldPosition()返回的坐标x分量是120,也就是说mesh的世界坐标是mesh位置属性.position和mesh父对象group位置属性.position的累加。

var mesh = new THREE.Mesh(geometry, material);
// mesh的本地坐标设置为(50, 0, 0)
mesh.position.set(50, 0, 0);
var group = new THREE.Group();
// group本地坐标设置为(70, 0, 0)
// mesh父对象设置position会影响得到mesh的世界坐标
group.position.set(70, 0, 0);
group.add(mesh);
scene.add(group);

// .position属性获得本地坐标
console.log('本地坐标',mesh.position);

// getWorldPosition()方法获得世界坐标
//该语句默认在threejs渲染的过程中执行,如果渲染之前想获得世界矩阵属性、世界位置属性等属性,需要通过代码更新
scene.updateMatrixWorld(true);
var worldPosition = new THREE.Vector3();
mesh.getWorldPosition(worldPosition);
console.log('世界坐标',worldPosition);

在线运行案例

总结

下面对上面的案例实验进行总结。

所谓本地坐标系或者说模型坐标系,就是模型对象相对模型的父对象而言,模型位置属性.position表示的坐标值就是以本地坐标系为参考,表示子对象相对本地坐标系原点(0,0,0)的偏移量。

前面两节课说过Threejs场景Scene是一个树结构,一个模型对象可能有多个父对象节点。世界坐标系默认就是对Threejs整个场景Scene建立一个坐标系,一个模型相对世界坐标系的坐标值就是该模型对象所有父对象以及模型本身位置属性.position的叠加。

本地缩放系数.scale

通过前面的论述,模型的位置属性.position可以称为本地坐标或者说局部坐标,对于属性.scale一样,可以称为模型的本地缩放系数或者局部的缩放系数,通过.getWorldScale()方法可以获得一个模型的世界缩放系数,就像执行.getWorldPosition()方法一样获得世界坐标,关于.getWorldScale()方法可以查看基类Object3D。

本地矩阵.materix和世界矩阵.matrixWorld

如果你对WebGL顶点的旋转、缩放、平移矩阵变换有一定的了解,可以继续阅读,如果没有概念也可以暂时跳过。

本地矩阵.materix是以本地坐标系为参考的模型矩阵,世界矩阵.matrixWorld自然就是以是世界坐标系为参照的模型矩阵。Three.js本地矩阵是

Three.js模型的位置属性.position、缩放系数属性.scale和角度属性.rotation记录了模型的所有平移、缩放和旋转变换,本地矩阵.materix是以线性代数矩阵的形式表示.position、.scale和.rotation。世界矩阵.matrixWorld自然是用矩阵的形式表示模型以及模型父对象的所有旋转缩放平移变换。更多内容可以参考文章《本地矩阵和世界矩阵》

转载本站内容时,请务必注明来自W3xue,违者必究。
 友情链接: NPS