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

news2024/11/24 6:24:40

在前一篇文章《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); // 允许平移,默认值:true

g3d.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.coloradmin.cn/o/746227.html

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

相关文章

vue3使用monaco-editor插件,报错Unexpected usage

在使用monaco-editor插件的时候&#xff0c;运行时没有报错&#xff0c;打开页面报如上错误&#xff0c;可以关掉&#xff0c;但是页面变了 还会再出现&#xff0c;上网找了一圈报错&#xff0c;是因为插件引入时产生的问题 原来的 import * as Monaco from monaco-editor更改…

论文笔记--TinyBERT: Distilling BERT for Natural Language Understanding

论文笔记--TinyBERT: Distilling BERT for Natural Language Understanding 1. 文章简介2. 文章概括3 文章重点技术3.1 Transformer Distillation3.2 两阶段蒸馏 4. 数值实验5. 文章亮点5. 原文传送门6. References 1. 文章简介 标题&#xff1a;TinyBERT: Distilling BERT fo…

Java四个月学不学的会?我分享一下我的经历来告诉你

先说结论&#xff0c;java培训 四个月一般是能达到能上手工作的水平的&#xff0c;但是高中毕业就算了。高中毕业哪怕你能上手工作了&#xff0c;需求能完成了&#xff0c;也是大概率找不到工作的&#xff0c;这一行可能最看重学历的行业之一了。虽然说句实在话&#xff0c;就…

【Linux】基础开发工具——gcc/g++篇

文章目录 一、预处理1.1 头文件展开1.2 条件编译 二、编译三、汇编四、链接4.1 什么是库?4.2 库的分类4.3 目标文件和库是如何链接的&#xff1f;4.3.1 动态链接4.3.2 静态链接 4.4 动静态链接的优缺点对比 五、Debug&&release 前言 &#xff1a;  在前面的文章里给大…

VMware将虚拟机网络设置为NAT模式

虚拟机有vmware和desktop&#xff0c;本人一直使用的是vmware。安装好vmware并激活后&#xff0c;创建完虚拟机。(需要vmware和激活码的可留言) 进入虚拟机设置&#xff0c;网络适配器选择NAT模式 在虚拟机工具栏->菜单栏点击编辑&#xff0c;选择“虚拟网络编辑器”。 选择…

数据库应用:MySQL数据库用户管理

目录 一、理论 1.用户管理 2.授权控制 二、实验 1.数据库用户管理 2.数据库用户授权 三、总结 一、理论 1.用户管理 &#xff08;1&#xff09;用户信息 MySQL 中的用户信息&#xff0c;都存储在系统数据库 mysql 的 user 表中。 use mysql; select * from user lim…

【038】解码C++ STL:探索string容器的无限可能与鲜为人知的技巧

解码C STL&#xff1a;探索string容器的无限可能与鲜为人知的技巧 引言一、STL概述1.1、STL的基本概念1.2、STL的六大组件 二、string类2.1、string容器基本概念2.2、string容器常用操作2.2.1、string构造函数2.2.2、string基本赋值操作2.2.3、string存取字符操作原型2.2.4、st…

我们搬家啦!新家园,新征程,新篇章

7月10日 KaiwuDB 上海新总部开业典礼暨 “浪潮数据库产业联合实验室”揭牌仪式 于上海市浦东新区顺利举行 新起点&#xff0c;新征程&#xff0c;新篇章 &#x1f389;&#x1f389; 剪彩仪式 7月10日&#xff0c;上海浦东新区科技和经济委员会副主任夏玉忠&#xff0c;上…

行业追踪,2023-07-11,关于自动生成文章代码重构,明天开始发布新的追踪文章,敬请期待

自动复盘 2023-07-12 成交额超过 100 亿 排名靠前&#xff0c;macd柱由绿转红 成交量要大于均线 有必要给每个行业加一个上级的归类&#xff0c;这样更能体现主流方向 rps 有时候比较滞后&#xff0c;但不少是欲杨先抑&#xff0c; 应该持续跟踪&#xff0c;等 macd 反转时参与…

GoLand导入redis的github包失败

GoLand导入redis依赖失败 网上有下载guryburd和gomodel的&#xff0c;这里按照官网文档安装依赖 以下命令在项目的根目录执行 初始化一个Go模块&#xff1a; go mod init github.com/my/repoTo install go-redis/v9:要安装go-redis/v9&#xff1a; go get github.com/redis/…

大模型技术发展概述 -(二)

文本内容参考论文《A Survey of Large Language Models》 论文标题&#xff1a;A Survey of Large Language Models 论文链接&#xff1a;https://arxiv.org/pdf/2303.18223v10.pdf 大模型技术发展概述 -&#xff08;二&#xff09; 4. LLM预训练4.1 数据收集4.1.1 数据源4.1.2…

Zookeeper集群

目录 Zookeeper 概述 Zookeeper 定义 Zookeeper 工作机制 Zookeeper 特点 Zookeeper 数据结构 Zookeeper 应用场景 Zookeeper 选举机制 ●第一次启动选举机制 ●非第一次启动选举机制 部署 Zookeeper 集群 1.安装前准备 关闭防火墙 安装 JDK 下载安装包 2.安装 …

从网络安全行业人才需求讲讲【个人规划】

如果你是一名正在找工作的网络安全方向大学生&#xff0c;或者是刚刚踏入网络安全领域的新手&#xff0c;这篇文章很适合你&#xff0c;如果你是一名老网安人&#xff0c;看看有哪些你中招了。 当你打开BOSS直聘、拉钩等招聘类网站后&#xff0c;在首页的快速导航页面很难找到关…

ruoyi导入excel报错数据越界

一、报错截图&#xff1a; 二、报错原因 实体类 三、修改方式 将实体类Excel注释 修改去掉 readConverterExp部分&#xff0c;重新导入后成功。 四、出错原因

【雕爷学编程】Arduino动手做(138)---64位WS2812点阵屏模块3

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

【基于openEuler上安装单机openGauss2.1.0企业版】

【基于openEuler上安装openGauss2.1.0企业版】 一、环境说明二、安装步骤 一、环境说明 华为云ECS 规格 8核32G操作系统openEuler 20.03 TLS 二、安装步骤 修改操作系统字符集为utf8 cat >>/etc/profile<<EOF export LANGen_US.UTF-8 EOF让环境变量生效 source /…

ASCII码对照表 十六进制的字符对照表

ASCII码对照表&#xff08;包括二进制、十进制十六进制和字符&#xff09; 可以显示 不可以显示

天津良心web前端培训学校品牌汇总(Web前端的起步薪资)

现在很多小伙伴想要了解web前端开发技术知识&#xff0c;但是不知道现在学习web前端值不值得&#xff0c;会有很多小伙伴开始犹豫学不学&#xff0c;学完能不能找到工作&#xff0c;那下面小编就通过下面三点&#xff0c;来给大家简单分析一下web行业&#xff0c;以及要不要报班…

Layui之可调参数的动态轮播图---好玩的小玩意儿~

⭐ 本期精彩&#xff1a;利用Layui制作轮播图 效果图&#xff1a; 前台代码&#xff1a; JS代码&#xff1a; 设置长宽高的事件代码&#xff1a; //事件carousel.on(change(test1), function(res){console.log(res)});var $ layui.$, active {set: function(othis){var THIS…

intellij idea开发微信远程小程序

原理 intellij idea开发微信原生小程序的基本原理是&#xff0c;在idea中编辑目标文件&#xff08;wxml、wxss&#xff09;&#xff0c;微信开发者工具热加载改动&#xff0c;从而实时看到效果&#xff0c;微信开发者工具的提示、高亮等能力&#xff0c;都太弱了&#xff0c;所…