当前位置: 首页 > news >正文

HT for Web (Hightopo) 使用心得(5)- 动画的实现

其实,在 HT for Web 中,有多种手段可以用来实现动画。我们这里仍然用直升机为例,只是更换了场景。增加了巡游过程。

使用 HT 开发的一个简单网页直升机巡逻动画(Hightopo 使用心得(5))

这里主要用到的动画实现方式有三种:

  • setInterval
  • ht.Default.startAnim()
  • DataModel.addScheduleTask(task)

场景搭建

这里的主要工作分为:3D 场景配置以及模型加载。其中 3D 场景部分的设置代码如下:

this.g3d = new ht.graph3d.Graph3dView();
this.g3d.setGridVisible(true);
this.g3d.setGridSize(5000);
this.g3d.setGridGap(2000);
this.g3d.setNear(10)
this.g3d.setFar(10000000)
this.g3d.addToDOM();
this.dataModel = this.dm = this.g3d.dm();

为了给直升机搭建一个逼真的环境。这里我们增加了一个山体模型。另外,由于直升机机体与螺旋桨模型是分开的,因此需要分别加载并调整其位置让二者合并成一个模型。

// 加载山体模型
this.mountains = await this.createObj(MODELS.MOUNTAINS.name, MODELS.MOUNTAINS.obj, MODELS.MOUNTAINS.mtl);
this.mountains.s('3d.selectable',false);
this.mountains.s('shape3d.scaleable',true);
this.mountains.setScale3d([0.01, 0.1, 0.01]);
this.mountains.setElevation(1800); // 让山体在地面以上
// 分别加载直升机及螺旋桨模型
this.helicopterNode = await this.createObj(MODELS.HELICOPTER.name, MODELS.HELICOPTER.obj, MODELS.HELICOPTER.mtl);
this.propellerNode = await this.createObj(MODELS.PROPELLER.name, MODELS.PROPELLER.obj, MODELS.PROPELLER.mtl);
// 由于默认创建 Node 的时候,其锚点是在 [0.5, 0.5, 0.5],位置是在 [0, 0, 0]。导致模型并不在水平面以上。
let size3d = this.helicopterNode.getSize3d(); // 获取直升机模型的 [长,宽,高]
let height = size3d[1]; // 获取模型高度
this.helicopterNode.setPosition3d([0, height/2, 0]); // 将直升机放到地面上
this.propellerNode.setRotation3d([0.10506443461595279, 4.550746858974086, -0.007825951889059535]); // 让螺旋桨水平
this.propellerNode.setPosition3d([0, 215, -99.00152946490829]); // 将螺旋桨放到直升机上
this.propellerNode.setHost(this.helicopterNode); // 螺旋桨吸附到直升机上
this.helicopterNode.p3(0,2000,0); // 直升机

螺旋桨动画 - setInterval

螺旋桨动画比较简单,其本质是通过不断地修改螺旋桨节点在竖直方向(Y 轴)的角度。

/**
* 螺旋桨旋转动画
*
*/
startPropellerAnim(node) {
setInterval(() => {
const r3 = node.getRotation3d();
node.setRotation3d([r3[0], r3[1] + 0.4, r3[2]]); // 绕 Y 轴旋转
}, 20);
}

创建直升机巡游路径

有了直升机及环境,我们需要让直升机动起来。例如在这里,我们计划让直升机围绕山体巡逻。这里该如何实现呢?

在 HT for Web 官方手册中,其提供了一种实现方式,我们这里稍微加以改造便可让直升机围绕山体巡逻。

在代码层面,我们创建了一条三维线段(Polyline)。该线段实现的是一个圆环,悬浮在山体上面。有了这条路径,直升机便可沿着该路径前进实现巡游动画。

polyline的形状主要由points和segments这两个属性描述。二者都是数组。其中 points 可以理解成组成 polyline 所要用到的点集合,而 segments 数组主要用来定义如何使用前面的点来组成 polyline。

points 中的每一项为 {x,y,e} 格式,需要注意的是,这里代表高度的是 e(elevation),而不是 y。

segments 数组里面有5种值。分别为:

  • 1: moveTo,占用1个点信息,代表一个新路径的起点
  • 2: lineTo,占用1个点信息,代表从上次最后点连接到该点
  • 3: quadraticCurveTo,占用2个点信息,第一个点作为曲线控制点,第二个点作为曲线结束点
  • 4: bezierCurveTo,占用3个点信息,第一和第二个点作为曲线控制点,第三个点作为曲线结束点
  • 5: closePath,不占用点信息,代表本次路径绘制结束,并闭合到路径的起始点
/**
* 创建直升机巡游路径
*
* @memberof Index3d
*/
createPath() {
this.g3d.setDashDisabled(false); // 显示虚线
let height = 2000; // 线段离地高度
let dataModel = this.dataModel;
let polyline = this.polyline = new ht.Polyline();
polyline.setThickness(5); // 线段粗细
polyline.s({
'shape3d.image': 'assets/flow.png', // 贴图
"shape3d": "cylinder", // polyline类型,这里是圆柱。也可以是
'repeat.uv.length': 400, // 贴图宽度
'shape3d.resolution': 1600, // 管线分辨率,分辨率越高越平滑
});
dataModel.add(polyline);
// 起始点
const points = [{
x: -15000,
y: 0,
e: height,
}];
const segments = [1];
// 二次曲线,占用两个点。生成一条弧线。下同。
points.push({
x: -15000,
y: -15000,
e: height
});
points.push({
x: 0,
y: -15000,
e: height
});
segments.push(3);
points.push({
x: 15000,
y: -15000,
e: height
});
points.push({
x: 15000,
y: 0,
e: height
});
segments.push(3);
points.push({
x: 15000,
y: 15000,
e: height
});
points.push({
x: 0,
y: 15000,
e: height
});
segments.push(3);
points.push({
x: -15000,
y: 15000,
e: height
});
points.push({
x: -15000,
y: 0,
e: height,
});
segments.push(3);
polyline.setPoints(points);
polyline.setSegments(segments);
polyline.setAnchorElevation(0)
}

直升机巡游动画 - ht.Default.startAnim

接下来,我们需要让直升机沿着巡游路径前进。在实现的时候,我们使用了 ht.Default.startAnim() 方法。该方法我们在前几篇文章中都用过,这里就不再详细介绍。

ht.Default.startAnim() 会执行 duration 毫秒,在执行过程中,其会自动计算所需要的帧数并在每一帧都调用一次action 方法。也就是说,如果我们想让直升机 40 秒围绕路径飞行一圈,我们只需要将 duration 设置成40*1000 毫秒,并且在每一帧拿到当前时刻 polyline 上的点的坐标及方向。同时,使用该坐标与方向设置直升机位置及朝向就可以实现巡游动画。

这里面比较关键的一个方法是 g3d.getLineOffset(polyline, length * v) 。该方法会返回一个对象:{point: p.M…h.Vector3, tangent: p.M…h.Vector3}。其分别代表当前时刻 polyline 上的点的坐标及放向。根据这两个值,我们可以进一步配置直升机的位置和朝向。

/**
* 直升机沿着巡游路径飞行
*
* @param {number} [duration=40 * 1000]
* @memberof Index3d
*/
startFly(duration = 40 * 1000) {
const {
g3d,
polyline
} = this;
/** 获取巡游路径总长度 */
let length = g3d.getLineLength(polyline);
const params = {
delay: 0,
duration,
easing: (t) => {
return t;
},
action: (v, t) => {
let offset = g3d.getLineOffset(polyline, length * v),
point = offset.point,
px = point.x,
py = point.y + 200, // 让直升机高于polyline
pz = point.z,
tangent = offset.tangent,
tx = tangent.x,
ty = tangent.y,
tz = tangent.z;
this.helicopterNode.p3(px, py, pz);
this.helicopterNode.lookAt([px + tx, py + ty, pz + tz], 'back'); // 一个模型有6个面,这里需要确定机头处于哪个面
// 视角盯住直升机
if (this._cameraType == 1) {
g3d.setCenter(px, py, pz);
} else if (this._cameraType == 2) { // Camera跟随直升机运动
g3d.setEye(px - tx * 1800 + 1000, py - ty * 1800 + 1000, pz - tz * 1800); // 让镜头高于直升机并在尾部进行观察
g3d.setCenter(px, py, pz);
}
this.helicopterNode.a('angle', v * Math.PI * 120);
},
finishFunc: () => {
ht.Default.startAnim(params);
}
};
ht.Default.startAnim(params);
}

管道流动动画 - DataModel.addScheduleTask()

实现管道流动的动画有多种方式,其本质是定期改变管道的贴图偏移。

这里我们采用DataModel#addScheduleTask(task)实现流动动画。DataModel#addScheduleTask(task)实际上是添加了一个调度任务。由于该方法是在 DataModel 上执行,因此在每次执行的时候,DataModel 里面的每个 Data 都会被调用。我们可以在 action 参数里面对 Data 进行过滤。DataModel#addScheduleTask(task)方法的参数task为json对象,可指定如下属性:

  • interval:间隔毫秒数,默认值为10
  • enabled:是否启用开关,默认为true
  • beforeAction:调度开始之前的动作函数
  • action:间隔动作函数,对DataModel上的每个data节点都会执行一次action操作
  • afterAction:调度结束之后的调度函数

另外,可以用DataModel#removeScheduleTask(task)删除调度任务,其中task为以前添加过的调度任务对象。

/**
* 通过DataModel的addScheduleTask实现流动效果
*
* @memberof Index3d
*/
addScheduleTasks() {
const task = {
interval: 50, // 间隔毫秒数,默认值为10
enabled: true, // 是否启用开关,默认为true
beforeAction: () => {}, // 调度开始之前的动作函数
afterAction: () => {}, // 调度结束之后的调度函数
action: (data) => { // 间隔动作函数,对DataModel上的每个data节点都会执行一次action操作
if (data.getClassName() == 'ht.Polyline') {
const offset = (data.s('shape3d.uv.offset') || [0,0]);
data.s('shape3d.uv.offset', [offset[0] + 0.1, offset[1]]);
}
}
};
this.dataModel.addScheduleTask(task);
// this.dataModel.removeScheduleTask(task); // 删除调度任务
}

这里我们只是举例介绍一下DataModel#addScheduleTask(task)的用法。对于一个 DataModel 中大部分 Data 都需要动画的时候,可以考虑使用该方法。

在代码执行的时候,我们可以选择把巡游路径隐藏。这样看起来直升机就是沿着一个圆形持续巡游。

hidePath() {
this.polyline.s('3d.visible', false);
}

总结

本文介绍了如何通过代码实现一个直升机绕山巡游的动画,包括创建路径和实现直升机的飞行动画。另外,还介绍了如何通过DataModel#addScheduleTask(task)实现流动效果的动画。读完本文,你将了解到如何使用 HT for Web 实现各种动画效果。

相关文章:

HT for Web (Hightopo) 使用心得(5)- 动画的实现

其实,在 HT for Web 中,有多种手段可以用来实现动画。我们这里仍然用直升机为例,只是更换了场景。增加了巡游过程。 使用 HT 开发的一个简单网页直升机巡逻动画(Hightopo 使用心得(5)) 这里主…...

Leetcode(面试题 08.01.)三步问题

文章目录 前言一、题目分析二、算法原理1.状态表示2.状态转移方程3.初始化4.填表顺序5.返回值是什么 三、代码实现总结 前言 在本文章中,我们将要详细介绍一下Leetcode(面试题 08.01.)三步问题相关的内容 一、题目分析 1.小孩可以上一阶,两阶&#xff…...

AIGC: 关于ChatGPT中输出表格/表情/图片/图表这些非文本的方式

ChatGPT 不止是 文本输出 ChatGPT是一个文本模型, 它本身并不能直接去生成图片图表等内容在我们的工作当中,经常需要通过表格, 图表的方式去进行数据的处理和展示在这种情况下,GPT由于不支持去直接的生成图片和图表,我们还能够使用它的GPT帮…...

聊聊logback的addtivity属性

序 本文主要研究一下logback的addtivity属性 LoggerModel ch/qos/logback/classic/model/LoggerModel.java PhaseIndicator(phase ProcessingPhase.SECOND) public class LoggerModel extends Model {private static final long serialVersionUID 5326913660697375316L;S…...

在网络安全护网中,溯源是什么?

在网络安全护网中,溯源是什么? 在网络安全护网中,溯源是指通过收集、分析和解释数字证据来追踪和还原网络攻击或其他网络犯罪活动的过程。它旨在确定攻击者的身份、行为和意图,以便采取适当的对策,并为法律机构提供必…...

【刷题】动态规划

动态规划 139. 单词拆分(一维) 给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。 注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。 示例 1&…...

hadoop操作

文件操作 注意当前所在的路径,创建一个mytest文件夹 创建一个1.txt文件 将1.txt文件移动到mytest中,通过mv改名字,然后查看mytest文件夹的txt文件变成了test.txt 删除文件 上传下载文件 新建1.txt 然后编辑它 随便输入什么 上传 然后看看网…...

角色管理--高级产品经理岗

研发组织管理--角色管理--高级产品经理岗 定位 产品从规划到推进落地的绝对主力,同时能赋能新人,带领新人高质,高效的完成产品的各项工作; 所需资质 某一领域产品专家,有产品架构能力,熟悉产品落地流程…...

nginx: [alert] could not open error log file

先把cmd的报错信息粘出来 nginx: [alert] could not open error log file: CreateFile() “logs/error.log” failed (3: The system cannot find the path specified) 2023/11/29 11:27:37 [emerg] 5040#18772: CreateDirectory() “D:\enviroment\nginx-1.24.0\conf/temp/cli…...

MySQL数据库:外键、唯一键、唯一索引

目录 说明 一、如果要使用外键,表的存储引擎选择哪个? 1.1 答 1.2 示范 1.2.1 主表 (1)MyISAM的表:masterTable2 (2)InnoDB的表:masterTable1 1.2.2 从表 (1&am…...

CSS核心功能手册:从熟悉到精通

CSS核心功能代码 文章目录 CSS核心功能代码[toc]参考HTML代码尺寸操作设置元素尺寸内边距外边距设置默认布局边距用途和使用场景: 背景设置**背景颜色 (background-color)**:**背景图像 (background-image)**:**背景重复 (background-repeat)**:**背景位置 (backgro…...

编程的重要性及解决技术难题的方法

看到这个话题之后,出于好奇,使用某chat,输入相应主题得到的一篇文章,分享给大家。 PS:现在不同版本的chat和其快速更新升级也可以说是编程的结果,其重要性和发展历程也反映了编程的重要性。 一、编程的重要…...

如何成为一名高效的前端开发者(10X开发者)

如今,每个人都想成为我们所说的“10倍开发者”。然而,这个术语经常被误解和高估。 本质上,一个高效或者10倍开发者,在我看来,是指那些能够充分利用所有可用工具的人,通过让这些工具处理冗余和重复的任务&am…...

Docker port 命令

docker port:列出指定的容器的端口映射,或者查找将PRIVATE_PORT NAT到面向公众的端口。 语法 docker port [OPTIONS] CONTAINER [PRIVATE_PORT[/PROTO]]实例 查看容器mymysql的端口映射情况: docker port mymysql##效果如下: …...

PostgreSQL-SQL联表查询LEFT JOIN 数据去重复

我们在使用left join联表查询时,如果table1中的一条记录对应了table2的多条记录,则会重复查出id相同的多条记录。 1、解决方法一 SELECT t1.* FROM table1 t1 LEFT JOIN table2 t2 ON t1.id t2.tid 第一种方法我们发现还是有重复数据 2、解决方法二…...

Golang与MongoDB的完美组合

引言 在现代开发中,数据存储是一个至关重要的环节。随着数据量的增加和复杂性的提高,开发人员需要寻找一种高效、可扩展且易于使用的数据库解决方案。MongoDB作为一种NoSQL数据库,提供了强大的功能和灵活的数据模型,与Golang的高…...

初识Java 18-2 泛型

目录 构建复杂模型 类型擦除 C中的泛型 迁移的兼容性 类型擦除存在的问题 边界的行为 对类型擦除的补偿 创建类型实例 泛型数组 本笔记参考自: 《On Java 中文版》 构建复杂模型 泛型的一个优点就是,能够简单且安全地创建复杂模型。 【例子&am…...

vue分环境打包及案例代码

Vue分环境打包可以帮助我们针对不同的环境(如开发环境、测试环境、生产环境等)打包出不同的版本,以满足不同的需求。下面是一个简单的Vue分环境打包的示例代码: 安装cross-env: npm install --save-dev cross-env在项目的根目录下创建不同的环境配置文件,如test.env.js…...

基于springboot+vue的在线考试系统(前后端分离)

博主主页:猫头鹰源码 博主简介:Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容:毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…...

重装linux后需要做的配置

1. linux中 vim如果输入中文乱码 打开/etc/vim/vimrc输入: set fileencodingsutf-8,gbk set termencodingutf-8 set encodingutf-8 把vim的缩进格式顺便改了 http://t.csdnimg.cn/K3ncc 2. 配置sudo授权用户 3. 新导入项目后 , chmod -R x 添加权限 4. 查询主机i…...

【入坑系列】TiDB 强制索引在不同库下不生效问题

文章目录 背景SQL 优化情况线上SQL运行情况分析怀疑1:执行计划绑定问题?尝试:SHOW WARNINGS 查看警告探索 TiDB 的 USE_INDEX 写法Hint 不生效问题排查解决参考背景 项目中使用 TiDB 数据库,并对 SQL 进行优化了,添加了强制索引。 UAT 环境已经生效,但 PROD 环境强制索…...

dedecms 织梦自定义表单留言增加ajax验证码功能

增加ajax功能模块&#xff0c;用户不点击提交按钮&#xff0c;只要输入框失去焦点&#xff0c;就会提前提示验证码是否正确。 一&#xff0c;模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...

Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; 目前2025年06月05日更新到&#xff1a; AI炼丹日志-28 - Aud…...

DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI

前一阵子在百度 AI 开发者大会上&#xff0c;看到基于小智 AI DIY 玩具的演示&#xff0c;感觉有点意思&#xff0c;想着自己也来试试。 如果只是想烧录现成的固件&#xff0c;乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外&#xff0c;还提供了基于网页版的 ESP LA…...

大模型多显卡多服务器并行计算方法与实践指南

一、分布式训练概述 大规模语言模型的训练通常需要分布式计算技术,以解决单机资源不足的问题。分布式训练主要分为两种模式: 数据并行:将数据分片到不同设备,每个设备拥有完整的模型副本 模型并行:将模型分割到不同设备,每个设备处理部分模型计算 现代大模型训练通常结合…...

今日科技热点速览

&#x1f525; 今日科技热点速览 &#x1f3ae; 任天堂Switch 2 正式发售 任天堂新一代游戏主机 Switch 2 今日正式上线发售&#xff0c;主打更强图形性能与沉浸式体验&#xff0c;支持多模态交互&#xff0c;受到全球玩家热捧 。 &#x1f916; 人工智能持续突破 DeepSeek-R1&…...

Map相关知识

数据结构 二叉树 二叉树&#xff0c;顾名思义&#xff0c;每个节点最多有两个“叉”&#xff0c;也就是两个子节点&#xff0c;分别是左子 节点和右子节点。不过&#xff0c;二叉树并不要求每个节点都有两个子节点&#xff0c;有的节点只 有左子节点&#xff0c;有的节点只有…...

分布式增量爬虫实现方案

之前我们在讨论的是分布式爬虫如何实现增量爬取。增量爬虫的目标是只爬取新产生或发生变化的页面&#xff0c;避免重复抓取&#xff0c;以节省资源和时间。 在分布式环境下&#xff0c;增量爬虫的实现需要考虑多个爬虫节点之间的协调和去重。 另一种思路&#xff1a;将增量判…...

Mysql8 忘记密码重置,以及问题解决

1.使用免密登录 找到配置MySQL文件&#xff0c;我的文件路径是/etc/mysql/my.cnf&#xff0c;有的人的是/etc/mysql/mysql.cnf 在里最后加入 skip-grant-tables重启MySQL服务 service mysql restartShutting down MySQL… SUCCESS! Starting MySQL… SUCCESS! 重启成功 2.登…...

STM32---外部32.768K晶振(LSE)无法起振问题

晶振是否起振主要就检查两个1、晶振与MCU是否兼容&#xff1b;2、晶振的负载电容是否匹配 目录 一、判断晶振与MCU是否兼容 二、判断负载电容是否匹配 1. 晶振负载电容&#xff08;CL&#xff09;与匹配电容&#xff08;CL1、CL2&#xff09;的关系 2. 如何选择 CL1 和 CL…...