Hightopo 使用心得(4)- 3D 场景 Graph3dView 与 Obj 模型

在前一篇文章《Hightopo 使用心得(3)- 吸附与锚点》中,我们在结尾处提到过 HT3D 场景。这里我们通过代码建立一个 3D 场景并添加一个 Obj 模型来介绍一下 HT for Web3D 场景和模型加载方面的使用。

这是我们最终实现的效果:
在这里插入图片描述

3D坐标系

在搭建 3D 场景之前,先介绍一下基本的 3D 概念。

HT for Web3D 场景中采用的是右手坐标系,遵循右手螺旋法则。也就是:x轴正方向朝右,y轴正方向朝上,z轴正方向朝向屏幕外。
在这里插入图片描述
2D 坐标系(x, y)相比,这里多了一条坐标轴,也就是高度轴。2/3D 坐标系具体对应关系如下:

2D3D
坐标轴xx
yz
y

从图片和表格中可以看到,在右手坐标系下,2D坐标系中的 x,y 平面,在 3D 中对应的是 x,z 平面,也就是地平面。而在 3D 中多出来的一条坐标轴是高度坐标轴,也就是 y 轴。

有了三条坐标轴后,显而易见,我们在配置节点(ht.Node)属性时就不能使用原来的方法。为此,HTht.Node 扩展了一些新的方法。其中比较常用的有:
3D位置函数

  • 设置位置(坐标):setPosition3d(x, y, z)|setPosition3d([x, y, z]),可传入x, y, z三个参数,或传入[x, y, z]的数组
  • 获取位置(坐标):getPosition3d()的新函数,返回[x, y, z]数组值,即[getPosition().x, getElevation(), getPosition().y]
  • 设置大小(尺寸):setSize3d(x, y, z)|setSize3d([x, y, z]),可传入x, y, z三个参数,或传入[x, y, z]的数组
  • 获取大小(尺寸):getSize3d()的新函数,返回[x, y, z]数组值,即[getWidth(), getTall(), getHeight()]

3D锚点

3D中节点同样有锚点的概念,同样HTht.Node图元增加了以下新函数:

-设置 3D 锚点: setAnchor3d(x, y, z)|setAnchor3d([x, y, z]),可传入x, y, z三个参数,或传入[x, y, z]的数组
-设置获取y轴方向锚点: getAnchorElevation()|setAnchorElevation(elevation)
-获取 3D 锚点 getAnchor3d()的新函数,返回[x, y, z]数组值,即[getAnchor().x, getAnchorElevation(), getAnchor().y]

3D旋转函数

ht.Node2D坐标系下由getRotation()setRotation(rotation)函数控制旋转,该参数对应于3D坐标系下沿y轴的负旋转值。 同时3D坐标系下增加了rotationXrotationZ两个分别沿着x轴和z轴的新旋转变量,同时增加以下新函数:

  • setRotationY(y)设置沿y轴旋转弧度,相当于setRotation(-y)
  • getRotationY()获取沿y轴旋转弧度,相当于getRotation()
  • 设置围绕三个坐标轴的旋转角度:setRotation3d(x, y, z)|setRotation3d([x, y, z]),可传入x, y, z三个参数,或传入[x, y, z]的数组
  • 获取当前在三个坐标轴的旋转角度:getRotation3d()的新函数,返回[x, y, z]数组值,即[getRotationX(), -getRotation(), getRotationZ()]

3D场景搭建 - ht.graph3d.Graph3dView

在前面文章的例子中,创建一张 2D 图纸,使用的是 new ht.graph.GraphView(); 而在这里,创建一个 3D 场景,我们需要使用 new ht.graph3d.Graph3dView();

HT其他视图组件一样, ht.graph3d.Graph3dView也是基于于统一的ht.DataModel数据模型来驱动图形显示。熟悉了2D图纸的同学可能会发现,其在场景配置,节点配置上与 2D 相似。

使用下面的代码,我们创建和配置了一个 3D 场景,并获取了其对应的数据模型(dataModel):

/*************** 创建一个3D场景,添加到body下,并配置各种属性 ******************/
const g3d = new ht.graph3d.Graph3dView();
g3d.addToDOM(); // 添加到DOM
g3d.setGridVisible(true); // 显示网格
g3d.setEye(2000, 1000, 0); // 设置相机位置
g3d.setCenter(0, 0, 0); // 设置中心点
g3d.setUp(0, 1, 0); // 设置相机角度;这里默认值就是 [0, 1, 0]g3d.setRotatable(true); // 允许旋转,默认值:true
g3d.setZoomable(true); // 允许滚轮缩放,默认值:true
g3d.setPannable(true); // 允许平移,默认值:trueg3d.setEditable(true); // 允许在场景中对节点进行编辑const dm = g3d.getDataModel(); // 获取场景的 DataModel,简写形式:g3d.dm()
dm.setBackground('white'); // 同 dm.setBackground('rgba(255, 255 255, 1)'); 默认为黑色

在这里插入图片描述
创建场景后,我们又让它显示了辅助网格。我们可以将这些网格理解成地平面。模型在网格上方就相当于在地面之上。反之就是在地面下方。

相机的up坐标

在上例中,比较特殊的一个操作是 g3d.setUp(0, 1, 0)。这里是指设置相机的 up 坐标。在计算机图形学中,相机的 up 坐标通常是指相机坐标系中的一个向量,用于定义相机的上方向。

例如在一个场景中,在水平面上有一栋房子,如果相机的 up 坐标是 (0,1,0),则相机将看到一个朝上的房子。如果相机的 up 坐标是(1,0,0),则相机将看到一个朝右的房子和竖直的地面(整个视角都会旋转)。在 HT for Web 中,默认的相机 up 坐标是 [0, 1, 0],也就是我们会看到一个正常的朝上的房子。

OBJ模型

HT3D 场景支持 FBX,OBJ,GLTF 等多种模型格式。这里我们选择比较通用的 OBJ 模型来进行举例。

要使用 OBJ 模型,首先需要在 index.html 中引入 ht-obj.js 插件:

<script src="../../lib/plugin/ht-obj.js"></script>

加载OBJ模型

通过使用 ht.Default.loadObj() 方法可以将 OBJ 模型加载到内存中。在执行loadObj() 时,需要配置 OBJ 路径,材质路径以及相关参数。其中参数 params 的详细说明可以从以下连接获取:

Global | HT for Web (hightopo.com)

它里面比较常用的几个参数有:

centerboolean模型是否居中,默认为false,设置为true则会移动模型位置使其内容居中
prefixstring图片路径前缀,即在map_kd值之前增加的前缀,如果是相对路径则以加载obj的html页面的路径为参考
shape3dstring如果指定了shape3d名称,则HT将自动将加载解析后的所有材质模型构建成数组的方式,以该名称进行注册
finishFuncfunction用于加载后的回调处理
/*** 加载 obj 模型** @param {*} modelName* @return {*} */
function loadObj(objPath, mtlPath, modelName) {return new Promise((resolve, reject) => {/*** 模型参数,具体参数参考:https://www.hightopo.com/guide/doc/global.html#LoadObjParams*/const params = {center: true,prefix: 'obj/',shape3d: modelName,finishFunc: (modelMap, array, rawS3) => {resolve({modelMap, array, rawS3});},};// 加载模型ht.Default.loadObj(objPath, mtlPath, params);});
}

其中的 shape3d 参数是一个自定义字符串,可以将该字符串理解为我们为模型配置了一个名字。HT 在加载完 OBJ 模型后,它会把该模型存储到内存中。存储的方式就是通过 ht.Default.setShape3dModel(name, model) 方法。

在上面的代码中,我们为要加载的模型起了一个名字:modelName。在想使用该模型的时候,再通过ht.Default.getShape3dModel(name) 方法便可把模型从内存中取出来。

将Obj模型添加到3D场景

在上面的 loadObj() 只是将 OBJ 模型添加到了内存中。我们还需要在之后将 OBJ 模型添加到场景中。

由于 loadObj() 方法为异步执行,因此其参数里面需要携带一个 finishFunc 作为回调参数。为了减少代码层级,我们将上面的方法封装成了Promise 性质。后面我们可以等待这个 Promise 完成后再执行添加动作。
在这里插入图片描述

const MODELS = {// 直升机HELICOPTER: {name: 'helicopter',obj: 'obj/helicopterhspt_1002_01.obj',mtl: 'obj/helicopterhspt_1002_01.mtl',},// 螺旋桨PROPELLER: {name: 'propeller',obj: 'obj/helicopterhspt_1002_02.obj',mtl: 'obj/helicopterhspt_1002_02.mtl',},
};/*** 加载模型;模型初始化;创建模型Node; 添加模型到3D场景中** @return {*} */
async function createObj(name, obj, mtl) {const objInfo = await loadObj(obj, mtl, name); // 加载计量表模型,此处为异步// * @param {*} modelMap 调用ht.Default.parseObj解析后的返回值,若加载或解析失败则返回值为空// * @param {*} array 所有材质模型组成的数组// * @param {*} rawS3 包含所有模型的原始尺寸const {modelMap, array, rawS3} = objInfo;console.log('createObj: ', objInfo);if (!modelMap) {return;}// 创建 Node 用来存放该模型,后续对模型的操作通过该 Node 进行const node = new ht.Node();node.s({'shape3d': name, // 对应ht.Default.getShape3dModel(name)注册的模型'shape3d.scaleable': false});node.setSize3d(rawS3); // 存放模型在三个坐标轴方向上的大小。简写:node.s3()node.setPosition3d(0, 0, 0); // 此处可以将其放到水平面上。简写:node.p3()dm.add(node);return node;
}const helicopterNode = await createObj(MODELS.HELICOPTER.name, MODELS.HELICOPTER.obj, MODELS.HELICOPTER.mtl);
const propellerNode = await createObj(MODELS.PROPELLER.name, MODELS.PROPELLER.obj, MODELS.PROPELLER.mtl);

直升机模型分为两部分,分别是机体和螺旋桨。由于他们是两个模型,因此需要分别添加。

loadObj 结束后,HT 会将模型通过 ht.Default.setShape3dModel(name, model) 注册到内存中,之后会给 finishFunc 传递三个参数:modelMap, array, rawS3。其解释参考上面代码注释。目前我们用到的只有 rawS3 参数,也就是模型尺寸(大小)。

有了模型和尺寸(大小),我们便可以创建 ht.Node 用来对模型进行管理。将模型添加到 3D 场景中进行管理的主要逻辑如下:

模型 -(绑定到)→ ht.Node -(添加到)→ dataModel -(绑定到)→ Graph3dView

这里面的一个关键步骤是设置 ht.Nodeshape3d 属性。由于在 loadObj 的时候系统已经对模型进行注册,因此这里我们只需要通过将注册的模型名称赋值给 ht.Nodeshape3d 属性,HT 便可自动匹配到内存中对应的 OBJ 模型。

需要注意的是:在加载了模型并将模型绑定到 ht.Node 后并不能使其在 3D 场景中显示。只有通过 dataModel.add(node) 将节点添加到 3D 场景对应的数据模型中时,HT 才会在场景中将模型渲染出来。

模型位置

在上图中我们可以发现,直升机和螺旋桨重合了,并且二者也不在地面上。这里我们详细解释一下。

仔细查看代码,在创建 ht.Node 时,我们执行了下面的操作:

node.setSize3d(rawS3); // 存放模型在三个坐标轴方向上的大小。简写:node.s3()
node.setPosition3d(0, 0, 0); // 此处可以将其放到水平面上。简写:node.p3()

这两行命令分别是设置节点的大小和位置。这里的节点尺寸采用的是模型尺寸。而位置默认放到的坐标系中心点。

由于在 3D 场景中,ht.Node 的默认锚点是 [0.5, 0.5, 0.5],也就是在模型的三维中心点。因此其位置坐标也要对应到其中心点。这样,模型就会有一半在网格上方,另一半在网格下方。

该如何将直升机放到地平面上呢?我们可以通过模型的高度来计算出对应的位置从而将模型放到地平面上。具体代码如下:

// 由于默认创建 Node 的时候,其锚点是在 [0.5, 0.5, 0.5],位置是在 [0, 0, 0]。导致模型并不在水平面以上。let size3d = helicopterNode.getSize3d(); // 获取直升机模型的 [长,宽,高]let height = size3d[1]; // 获取模型高度helicopterNode.setPosition3d([0, height/2, 0]); // 将直升机放到地面上

在这里插入图片描述
而对于螺旋桨,情况又有些复杂。这里需要一些技巧才能将其配置到合适的位置。

我们通过手动调整螺旋桨来获取其应该摆放的位置和角度。这里就用到了g3d.setEditable(true)功能。打开编辑功能后,选中模型,场景中会显示坐标轴,通过拖动不同的坐标轴我们可以对模型进行移动,旋转和缩放。
在这里插入图片描述
将螺旋桨移动到机体合适的位置后,在console中通过 node.getPosition3d()node.getRotation3d() 来获取螺旋桨当前的位置和角度:
在这里插入图片描述
然后配置到代码中。与此同时,我们通过 setHost() 将螺旋桨吸附到了直升机上。这样,后面直升机移动时会带着螺旋桨移动。使二者不会脱离。

propellerNode.setRotation3d([0.10506443461595279, 4.550746858974086, -0.007825951889059535]); // 让螺旋桨水平
propellerNode.setPosition3d([0, 215, -99.00152946490829]); // 将螺旋桨放到直升机上
propellerNode.setHost(helicopterNode); // 螺旋桨吸附到直升机上

在这里插入图片描述

直升机动画

在直升机和螺旋桨都加载完成后,我们现在就可以为其增加相应的动画。
在这里插入图片描述

这里的动画分为两部分:

1. 螺旋桨旋转
2. 直升机移动

/*** 循环前进与后退** @param {*} node*/
function startAnim(node) {const p1 = node.p3(); // 原始位置const p2 = [p1[0], p1[1], p1[2] - 400]; // 目标位置,const forwardParams = {duration: 3 * 1000, // 动画帧数easing: (t) => { return t; }, // 动画缓动函数,默认采用`ht.Default.animEasing`finishFunc: () => {ht.Default.startAnim(backwardParams);// 循环播放该动画}, // 动画结束后调用的函数。action: (v, t) => { // action函数必须提供,实现动画过程中的属性变化。node.setPosition3d( // 此例子展示将节点`node`从位置`p1`动画到位置`p2`。p1[0] + (p2[0] - p1[0]) * v,p1[1] + (p2[1] - p1[1]) * v,p1[2] + (p2[2] - p1[2]) * v,);}};const backwardParams = {duration: 3 * 1000, // 动画帧数easing: (t) => { return t; }, // 动画缓动函数,默认采用`ht.Default.animEasing`finishFunc: () => {ht.Default.startAnim(forwardParams);// 循环播放该动画}, // 动画结束后调用的函数。action: (v, t) => { // action函数必须提供,实现动画过程中的属性变化。node.setPosition3d( // 此例子展示将节点`node`从位置`p1`动画到位置`p2`。p2[0] + (p1[0] - p2[0]) * v,p2[1] + (p1[1] - p2[1]) * v,p2[2] + (p1[2] - p2[2]) * v,);}};ht.Default.startAnim(forwardParams);
}/*** 螺旋桨旋转动画**/
function startPropellerAnim(node) {setInterval(() => {const r3 = node.getRotation3d();node.setRotation3d([r3[0], r3[1] + 0.4, r3[2]]); // 绕 Y 轴旋转。单位:弧度}, 20);
}

螺旋桨旋转动画比较简单。我们只需要让其绕着 y 轴转动就可以了。这里我们利用 setInterval() 起一个定时器,每隔 20 毫秒让其沿着 y 轴旋转 0.4°

关于直升机动画,我们为其找了两个点,让它在这两点之间来回移动。在动画的实现上,我们依然采用前几篇文章提到的 ht.Default.startAnim() 方法。具体实现见上面代码部分。

总结

这篇文章介绍了如何使用 HT for WebGraph3dViewOBJ 模型来创建 3D 场景。里面介绍了 3D 的一些基本概念以及 3D 场景的基本搭建与配置。另外,除了 3D 场景,我这里还重点描述了如何加载 OBJ 文件,如何添加模型节点到 3D 场景中,以及如何为节点添加动画。希望这些基本知识能对大家有所帮助。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

QML Canvas 几何变换(平移/旋转/缩放)

作者: 一去、二三里 个人微信号: iwaleon 微信公众号: 高效程序员 几何变换是 Canvas 提供的一项强大的图形处理能力,主要涉及平移、旋转、缩放。通过运用这些功能,我们有了更大的创作自由度,可以很容易的在 QML 中实现一些出色的游戏效果、动态图表、交互式数据可视化等…

装机——固态硬盘的选择

文章目录 问题描述知识学习硬盘分类PCIe接口SATA接口M.2接口 通道PCI-E通道SATA通道SAS通道FC通道 通信协议IDE协议AHCI协议NVMe协议 硬盘参数表主控存储颗粒SLCMLCTLCQLC失败的颗粒&#xff08;需要购买原装厂商的存储颗粒&#xff09; 问题解决问题总结 问题描述 女朋友笔记本…

2023-7-10-第十五式命令模式

&#x1f37f;*★,*:.☆(&#xffe3;▽&#xffe3;)/$:*.★* &#x1f37f; &#x1f4a5;&#x1f4a5;&#x1f4a5;欢迎来到&#x1f91e;汤姆&#x1f91e;的csdn博文&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f49f;&#x1f49f;喜欢的朋友可以关注一下&#xf…

【洛谷】P1342 请柬(正反建图+dijkstra)

1&#xff1a;思考&#xff1a; 从1到所用顶点简单&#xff08;单源最短路径。&#xff09;&#xff0c;重点在怎么解决所用点到1&#xff08;单终点最短路径&#xff09; 答案&#xff1a;反向建图使&#xff08;单终点最短路径→单源最短路径。&#xff09; 复杂度&#xf…

openGauss学习笔记-07 openGauss 语法

文章目录 openGauss学习笔记-07 openGauss 语法7.1 帮助7.2 SQL语句格式7.3 SQL语法ABORTALTER AUDIT POLICYALTER DATA SOURCEALTER DATABASEALTER DEFAULT PRIVILEGESALTER DIRECTORYALTER EXTENSIONALTER FOREIGN TABLEALTER FUNCTIONALTER GROUPALTER INDEXALTER LARGE OBJ…

【Python+selenium】生成测试报告

批量执行完用例后&#xff0c;生成的测试报告是文本形式的&#xff0c;不够直观&#xff0c;为了更好的展示测试报告&#xff0c;最好是生成HTML格式的。 unittest里面是不能生成html格式报告的&#xff0c;需要导入一个第三方的模块&#xff1a;HTMLTestRunner 一、导入HTMLT…

IIS8 安装 .net1.1

直接上正文&#xff1b; 一 安装补丁 安装顺序&#xff1a; 1 dotnetfx.exe 2 NDP1.1sp1-KB867460-X86 3 NDP1.1sp1-KB886903-X86 .NET_Framework_Cleanup_Tool.zip&#xff08;这个是卸载.net framework的软件&#xff0c;后面可能会用&#xff09; 链接&#xff1a;ht…

云计算的学习(二)

二、计算虚拟化 1.计算虚拟化的介绍 1.1虚拟化简介 a.什么是虚拟化 将物理设备逻辑化&#xff0c;转化成文件或者文件夹&#xff0c;这个文件或文件夹一定包含两个部分&#xff1a;一部分用于记录设备配置信息&#xff0c;另一部分记录用户数据。 虚拟机摆脱了服务器的禁锢…

FIR滤波器与IIR滤波器的区别与特点

目录 FIR滤波器与IIR滤波器的区别与特点 FIR滤波器定义&#xff1a; 特点&#xff1a; IIR滤波器定义&#xff1a; 特点&#xff1a; 区别&#xff1a; IIR滤波器有以下几个特点&#xff1a; IIR与FIR数字滤波器的比较&#xff1a; 1、从性能上比较 2、从结…

解决ubuntu cuda版本nvcc -V和nvidia-smi不一致问题

在使用nvcc -V和nvidia-smi查看cuda版本时不一致&#xff1a; nvcc -V版本是10.1 nvidia-smi的版本是12.2 上面如果能显示版本&#xff0c;所以是已经有驱动&#xff0c;首先要删除之前的驱动&#xff1a; 1、执行以下命令&#xff0c;删除旧版本的驱动 sudo apt-get purge …

Nuxt3引入Element-plus和sass

1.引入Element-plus 打开编辑器终端 运行npm install element-plus/nuxt 或者命令行cd到项目文件 运行npm install element-plus/nuxt package.json文件会出现 使用Element-plus 在nuxt.config.ts文件添加代码 export default defineNuxtConfig({devtools: { enabled: true }…

Apache Calcite 简介

这张图上列的,是直接使用 Apache Calcite 或者至少相关联的项目。大家肯定能在里面找到很多自己熟悉的项目。 那 Apache Calcite 究竟是干嘛的,又为什么能这么流行呢? 首先,摆一个应该没多少人会反对的共识:SQL 是编程领域最流行的语言。 有 MySQL、Oracle 之类使用 SQL…