Jack Huang's Blog


  • 首页

  • 标签

  • 归档

  • 搜索

JSBSim源代码分析

发表于 2020-01-13

JSBSim是一个开源跨平台的飞行动力学模型(FDM)软件库,用于模拟航空航天飞行器的飞行动力学。 该库已被纳入飞行模拟软件包FlightGear和OpenEaagles。JSBSim可以独立运行,通过命令行参数指定飞行器和初始状态,进行简单情境下的飞行动力学仿真,也可以将JSBSim作为代码库,编程实现飞行器模型加载,设置输入,获得输出。下面将通过分析JSBSim源代码,研究其实现通用飞行动力学模型的方法。

入口分析

下面是JSBSim参考手册中的最简单实例,因JSBSim的不断开发,JSBSim参考手册中该编程实例有点过时,因此进行了少量修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <FGFDMExec.h>
#include <sg_path.hxx>

using namespace std;

int main(int argc, char **argv)
{
JSBSim::FGFDMExec FDMExec;
bool result = true;

FDMExec.LoadScript(SGPath::fromUtf8(argv[1]));

while (result) result = FDMExec.Run();
}

从上述代码可知,调用JSBSim的主要方法是利用FGFDMExec类,通过实例化一个FGFDMExec类,就相当于获得了一个运行JSBSim仿真的工具箱,通过这个工具箱就可以调用JSBSim的大部分功能,实现我们要的仿真目标。同时FGFDMExec类通过加载外部飞机的XML脚本,实现飞行动力学模型的通用性。

JSBSim初始化流程

上述JSBSim最简仿真示例中已包含其初始化流程,采用图示如下:

JSBSim初始化流程

图1 JSBSim初始化流程

FGFDMExec初始化

FGFDMExec类在其构造函数中对各个模型进行初始化,具体代码在Allocate函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
FGFDMExec::FGFDMExec(FGPropertyManager* root, unsigned int* fdmctr)
: Root(root), RandomEngine(new default_random_engine), FDMctr(fdmctr)
{
...
try {
Allocate();
} catch (const string& msg ) {
cout << "Caught error: " << msg << endl;
exit(1);
}

...
}

Allocate函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
bool FGFDMExec::Allocate(void)
{
bool result=true;

Models.resize(eNumStandardModels);

// First build the inertial model since some other models are relying on
// the inertial model and the ground callback to build themselves.
// Note that this does not affect the order in which the models will be
// executed later.
Models[eInertial] = new FGInertial(this);

// See the eModels enum specification in the header file. The order of the
// enums specifies the order of execution. The Models[] vector is the primary
// storage array for the list of models.
Models[ePropagate] = new FGPropagate(this);
Models[eInput] = new FGInput(this);
Models[eAtmosphere] = new FGStandardAtmosphere(this);
...

// Assign the Model shortcuts for internal executive use only.
Propagate = (FGPropagate*)Models[ePropagate];
Inertial = (FGInertial*)Models[eInertial];
Atmosphere = (FGAtmosphere*)Models[eAtmosphere];
...

// Initialize planet (environment) constants
LoadPlanetConstants();

// Initialize models
for (unsigned int i = 0; i < Models.size(); i++) {
// The Input/Output models must not be initialized prior to IC loading
if (i == eInput || i == eOutput) continue;

LoadInputs(i);
Models[i]->InitModel();
}

...
return result;
}

Allocate函数代码中需要注意LoadInputs函数,该函数决定各个子模型的初始化顺序,确定各个子模型的输入输出,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
void FGFDMExec::LoadInputs(unsigned int idx)
{
switch(idx) {
case ePropagate:
Propagate->in.vPQRidot = Accelerations->GetPQRidot();
Propagate->in.vUVWidot = Accelerations->GetUVWidot();
Propagate->in.DeltaT = dT;
break;
case eInput:
break;
case eInertial:
Inertial->in.Position = Propagate->GetLocation();
break;
case eAtmosphere:
Atmosphere->in.altitudeASL = Propagate->GetAltitudeASL();
break;
case eWinds:
Winds->in.AltitudeASL = Propagate->GetAltitudeASL();
Winds->in.DistanceAGL = Propagate->GetDistanceAGL();
...
break;
case eAuxiliary:
Auxiliary->in.Pressure = Atmosphere->GetPressure();
Auxiliary->in.Density = Atmosphere->GetDensity();
...
break;
case eSystems:
// Dynamic inputs come into the components that FCS manages through properties
break;
case ePropulsion:
Propulsion->in.Pressure = Atmosphere->GetPressure();
...

break;
case eAerodynamics:
Aerodynamics->in.Alpha = Auxiliary->Getalpha();
...
break;
case eGroundReactions:
// There are no external inputs to this model.
GroundReactions->in.Vground = Auxiliary->GetVground();
...
break;
case eExternalReactions:
// There are no external inputs to this model.
break;
case eBuoyantForces:
BuoyantForces->in.Density = Atmosphere->GetDensity();
...
break;
case eMassBalance:
MassBalance->in.GasInertia = BuoyantForces->GetGasMassInertia();
MassBalance->in.GasMass = BuoyantForces->GetGasMass();
...
break;
case eAircraft:
Aircraft->in.AeroForce = Aerodynamics->GetForces();
Aircraft->in.PropForce = Propulsion->GetForces();
...
break;
case eAccelerations:
Accelerations->in.J = MassBalance->GetJ();
Accelerations->in.Jinv = MassBalance->GetJinv();
...
break;
default:
break;
}
}

FGFDMExec加载飞机配置

FGFDMExec的LoadScript函数在初始化时负责加载飞机配置,用于初始化各个子模型。

1
2
3
4
5
6
7
8
9
10
bool FGFDMExec::LoadScript(const SGPath& script, double deltaT,
const SGPath& initfile)
{
bool result;

Script = new FGScript(this);
result = Script->LoadScript(GetFullPath(script), deltaT, initfile);

return result;
}

FGFDMExec运行

FGFDMExec的Run函数负责飞行动力学模型的计算,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bool FGFDMExec::Run(void)
{
bool success=true;

...

// returns true if success, false if complete
if (Script != 0 && !IntegrationSuspended()) success = Script->RunScript();

for (unsigned int i = 0; i < Models.size(); i++) {
LoadInputs(i);
Models[i]->Run(holding);
}

...

return success;
}

FGFDMExec的Run函数将依次调用各个子模型的Run函数。

参考链接

  1. JSBSim编程实践之入门,by jackhuang.

排序算法总结

发表于 2020-01-05 | 更新于 2020-05-23

排序算法是计算机科学的基石之一,可从时间复杂度、空间复杂度、稳定性、是否原地排序等维度对排序算法进行分类。下面从时间复杂度方面对排序算法进行分类。

O(n^2)算法

冒泡排序

选择排序

插入排序

O(nlogn)算法

希尔排序

快速排序

归并排序

堆排序

O(n)算法

计数排序

桶排序

基数排序

参考链接

  1. 漫画:“排序算法” 大总结,by 小灰.
  2. 分布式哈希表 (DHT) 和 P2P 技术,by luyuhuang.
  3. Gzip 格式和 DEFLATE 压缩算法,by Luyu Huang.

Pandas入门教程

发表于 2020-01-02 | 更新于 2024-11-18

Pandas是一个开源的,BSD许可的库,为Python编程语言提供高性能,易于使用的数据结构和数据分析工具。

Pandas特色

Pandas 适用于处理以下类型的数据:

  • 与 SQL 或 Excel 表类似的,含异构列的表格数据;
  • 有序和无序(非固定频率)的时间序列数据;
  • 带行列标签的矩阵数据,包括同构或异构型数据;
  • 任意其它形式的观测、统计数据集, 数据转入 Pandas 数据结构时不必事先标记。

Pandas数据结构

Pandas 的主要数据结构是 Series(一维数据)与 DataFrame(二维数据),这两种数据结构足以处理金融、统计、社会科学、工程等领域里的大多数典型用例。对于 R 用户,DataFrame 提供了比 R 语言 data.frame 更丰富的功能。Pandas 基于 NumPy 开发,可以与其它第三方科学计算支持库完美集成。

维数 名称 描述
1 Series 带标签的一维同构数组
2 DataFrame 带标签的,大小可变的,二维异构表格

Pandas用法

Pandas用法与Matlab中矩阵操作很类似,熟悉Matlab操作的同学可以很快上手Pandas。

生成对象

生成Series对象:

1
2
3
4
5
6
7
8
9
10
11
In [3]: s = pd.Series([1, 3, 5, np.nan, 6, 8])

In [4]: s
Out[4]:
0 1.0
1 3.0
2 5.0
3 NaN
4 6.0
5 8.0
dtype: float64

生成DataFrame对象:

1
2
3
4
5
6
7
8
9
10
11
In [7]: df = pd.DataFrame(np.random.randn(6, 4), index=dates, columns=list('ABCD'))

In [8]: df
Out[8]:
A B C D
2013-01-01 0.469112 -0.282863 -1.509059 -1.135632
2013-01-02 1.212112 -0.173215 0.119209 -1.044236
2013-01-03 -0.861849 -2.104569 -0.494929 1.071804
2013-01-04 0.721555 -0.706771 -1.039575 0.271860
2013-01-05 -0.424972 0.567020 0.276232 -1.087401
2013-01-06 -0.673690 0.113648 -1.478427 0.524988

列切片

1
df1 = df[df.columns[0:6]];

重命名列名

1
df1.columns['id','name','sex','age','department','work']

过滤行

1
df1 = df1[df1.iloc[:,1]=='YES']

合并表格

pandas 2.0实现数据的合并与拼接,主要有三种方法:

  • join 最简单,主要用于基于索引的横向合并拼接
  • merge 最常用,主要用于基于指定列的横向合并拼接
  • concat最强大,可用于横向和纵向合并拼接
1
2
# 合并两个表,df1 和 df2 表结构一样
df3 = pd.concat([df1,df2])

毫秒解析

1
2
timeDelta = datetime.datetime(2024,11,11) - datetime.datetime(1900,1,1)
df3['datetime'] = pd.to_datetime(df['datetime'], format='%Y-%m-%d %H:%M:%S:%f') + timeDelta

按时间排序

1
df3 = df3.sort_values(by="datetime")

输出CSV表格

1
df3.to_csv(filePath + 'test.csv',index=0)

参考链接

  1. Pandas 中文,by pypandas.
  2. 十分钟入门 Pandas,by pypandas.
  3. Python读取csv文件的三种方式,by 涛声依旧2019.
  4. Python模块化开发组织代码程序示例,by BabyFish13.
  5. Python最佳实践指南!,by Prodesire.
  6. pandas中DataFrame 数据合并,连接(merge,join,concat) ,by Vincent-yuan.
  7. 【已解决】AttributeError: ‘DataFrame‘ object has no attribute ‘append‘,by zhtstar.
  8. pandas DataFrame数据重命名列名的几种方式,by littleRpl.
  9. Pandas 毫秒级时间解析,by 一定波兮.

jupyter notebook安装与使用

发表于 2019-12-31 | 更新于 2025-06-25

Jupyter Notebook(前身是IPython Notebook)是一个基于Web的交互式计算环境,用于创建Jupyter Notebook文档。Notebook一词可以通俗地引用许多不同的实体,主要是Jupyter Web应用程序、Jupyter Python Web服务器或Jupyter文档格式(取决于上下文)。Jupyter Notebook文档是一个JSON文档,遵循版本化模式,包含一个有序的输入/输出单元格列表,这些单元格可以包含代码、文本(使用Markdown语言)、数学、图表和富媒体,通常以“.ipynb”结尾扩展。

安装过程

安装前提

  • python>3.3 或者python=2.7

安装步骤

1
pip install notebook

启动Jupyter Notebook

1
jupyter notebook

使用指南

  • 单元格执行状态

单元格的执行状态对于复杂度高的代码,往往会意味着更长的执行等待时间。在Jupyter Notebook 中,当一个单元格处于执行状态时,单元格前面会出现 In [*] 符号,只有执行完成的单元格, [] 中的 * 才会变成相应的 序号。

除此之外,你可以通过页面右上角的 Kernel 状态指示器判断内核占用情况。如果 Python 字符右边出现了实心圆圈 ◉,代表内核处于占有状态。而空心圆圈 ◯ 则代表内核处于空闲状态。当然,也可能出现链接断开的符号,那就代表着内核已经断开链接,你可能需要刷新页面或重启实验环境。

参考链接

  1. Jupyter,by wikipedia.
  2. Installing the Jupyter Software,by jupyter.
  3. Matplotlib animation not working in IPython Notebook (blank plot),by stackoverflow.
  4. Jupyter Notebook使用指南,by zhanlang619.
  5. Jupyter中markdown的操作技巧,by 那一年_我九岁.

JavaScript正则表达式入门

发表于 2019-12-30 | 更新于 2019-12-31

最近在学习逐行剖析 Vue.js 源码的时候,发现Vuejs在模板编译时大量使用正则表达式。因此,将正则表达式的知识再温习一下。

基本概念

正则表达式是用于匹配字符串中字符组合的模式。在 JavaScript中,正则表达式也是对象。这些模式被用于 RegExp 的 exec 和 test 方法, 以及 String 的 match、matchAll、replace、search 和 split 方法。

基本语法

特殊字符

正则表达式中的特殊字符:

\

依照下列规则匹配:

在非特殊字符之前的反斜杠表示下一个字符是特殊字符,不能按照字面理解。例如,前面没有 “" 的 “b” 通常匹配小写字母 “b”,即字符会被作为字面理解,无论它出现在哪里。但如果前面加了 “",它将不再匹配任何字符,而是表示一个字符边界。

在特殊字符之前的反斜杠表示下一个字符不是特殊字符,应该按照字面理解。详情请参阅下文中的 “转义(Escaping)” 部分。

如果你想将字符串传递给 RegExp 构造函数,不要忘记在字符串字面量中反斜杠是转义字符。所以为了在模式中添加一个反斜杠,你需要在字符串字面量中转义它。/[a-z]\s/i 和 new RegExp(“[a-z]\s”, “i”) 创建了相同的正则表达式:一个用于搜索后面紧跟着空白字符(\s 可看后文)并且在 a-z 范围内的任意字符的表达式。为了通过字符串字面量给 RegExp 构造函数创建包含反斜杠的表达式,你需要在字符串级别和表达式级别都对它进行转义。例如 /[a-z]:\/i 和 new RegExp(“[a-z]:\\“,”i”) 会创建相同的表达式,即匹配类似 “C:" 字符串。

^

匹配输入的开始。如果多行标志被设置为 true,那么也匹配换行符后紧跟的位置。

例如,/^A/ 并不会匹配 “an A” 中的 ‘A’,但是会匹配 “An E” 中的 ‘A’。

当 ‘^’ 作为第一个字符出现在一个字符集合模式时,它将会有不同的含义。反向字符集合 一节有详细介绍和示例。

$

匹配输入的结束。如果多行标示被设置为 true,那么也匹配换行符前的位置。

例如,/t$/ 并不会匹配 “eater” 中的 ‘t’,但是会匹配 “eat” 中的 ‘t’。

*

匹配前一个表达式 0 次或多次。等价于 {0,}。

例如,/bo*/ 会匹配 “A ghost boooooed” 中的 ‘booooo’ 和 “A bird warbled” 中的 ‘b’,但是在 “A goat grunted” 中不会匹配任何内容。

+

匹配前面一个表达式 1 次或者多次。等价于 {1,}。

例如,/a+/ 会匹配 “candy” 中的 ‘a’ 和 “caaaaaaandy” 中所有的 ‘a’,但是在 “cndy” 中不会匹配任何内容。

?

匹配前面一个表达式 0 次或者 1 次。等价于 {0,1}。

例如,/e?le?/ 匹配 “angel” 中的 ‘el’、”angle” 中的 ‘le’ 以及 “oslo’ 中的 ‘l’。

如果紧跟在任何量词 *、 +、? 或 {} 的后面,将会使量词变为非贪婪(匹配尽量少的字符),和缺省使用的贪婪模式(匹配尽可能多的字符)正好相反。例如,对 “123abc” 使用 /\d+/ 将会匹配 “123”,而使用 /\d+?/ 则只会匹配到 “1”。

还用于先行断言中,如本表的 x(?=y) 和 x(?!y) 条目所述。

.

小数点)默认匹配除换行符之外的任何单个字符。

例如,/.n/ 将会匹配 “nay, an apple is on the tree” 中的 ‘an’ 和 ‘on’,但是不会匹配 ‘nay’。

如果 s (“dotAll”) 标志位被设为 true,它也会匹配换行符。

\n

在正则表达式中,它返回最后的第n个子捕获匹配的子字符串(捕获的数目以左括号计数)。

比如 /apple(,)\sorange\1/ 匹配”apple, orange, cherry, peach.”中的’apple, orange,’ 。

\s

匹配一个空白字符,包括空格、制表符、换页符和换行符。等价于[ \f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]。

例如, /\s\w*/ 匹配”foo bar.”中的’ bar’。

\w

匹配一个单字字符(字母、数字或者下划线)。等价于 [A-Za-z0-9_]。

例如, /\w/ 匹配 “apple,” 中的 ‘a’,”$5.28,”中的 ‘5’ 和 “3D.” 中的 ‘3’。

\W

匹配一个非单字字符。等价于 [^A-Za-z0-9_]。

例如, /\W/ 或者 /[^A-Za-z0-9_]/ 匹配 “50%.” 中的 ‘%’。

(x)

像下面的例子展示的那样,它会匹配 ‘x’ 并且记住匹配项。其中括号被称为捕获括号。

模式 /(foo) (bar) \1 \2/ 中的 ‘(foo)’ 和 ‘(bar)’ 匹配并记住字符串 “foo bar foo bar” 中前两个单词。模式中的 \1 和 \2 表示第一个和第二个被捕获括号匹配的子字符串,即 foo 和 bar,匹配了原字符串中的后两个单词。注意 \1、\2、…、\n 是用在正则表达式的匹配环节,详情可以参阅后文的 \n 条目。而在正则表达式的替换环节,则要使用像 $1、$2、…、$n 这样的语法,例如,’bar foo’.replace(/(…) (…)/, ‘$2 $1’)。$& 表示整个用于匹配的原字符串。

标志

标志 描述
g 全局搜索。
i 不区分大小写搜索。
m 多行搜索。
s 允许 . 匹配换行符。
u 使用unicode码的模式进行匹配。
y 执行“粘性”搜索,匹配从目标字符串的当前位置开始,可以使用y标

使用方法

正则表达式可以被用于 RegExp 的 exec 和 test 方法以及 String 的 match、replace、search 和 split 方法。使用正则表达式的方法如下:

方法

方法 描述
exec 一个在字符串中执行查找匹配的RegExp方法,它返回一个数组(未匹配到则返回 null)。
test 一个在字符串中测试是否匹配的RegExp方法,它返回 true 或 false。
match 一个在字符串中执行查找匹配的String方法,它返回一个数组,在未匹配到时会返回 null。
matchAll 一个在字符串中执行查找所有匹配的String方法,它返回一个迭代器(iterator)。
search 一个在字符串中测试匹配的String方法,它返回匹配到的位置索引,或者在失败时返回-1。
replace 一个在字符串中执行查找匹配的String方法,并且使用替换字符串替换掉匹配到的子字符串。
split 一个使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中的 String 方法。

返回

对象 属性或索引 描述 在例子中对应的值
myArray 匹配到的字符串和所有被记住的子字符串。 [“dbbd”, “bb”]
myArray index 在输入的字符串中匹配到的以0开始的索引值。 1
myArray input 初始字符串。 “cdbbdbsbz”
myArray [0] 匹配到的所有字符串(并不是匹配后记住的字符串)。注:原文”The last matched characters.”,应该是原版错误。匹配到的最终字符。 “dbbd”
myRe lastIndex 下一个匹配的索引值。(这个属性只有在使用g参数时可用在 通过参数进行高级搜索 一节有详细的描述.) 5
myRe source 模式文本。在正则表达式创建时更新,不执行。 “d(b+)d”

示例

1
2
3
4
5
6
var re = /\w+\s/g;
var str = "fee fi fo fum";
var myArray = str.match(re);
console.log(myArray);

// ["fee ", "fi ", "fo "]

参考链接

  1. 逐行剖析 Vue.js 源码,by nlrx.
  2. 正则表达式,by mozilla.

编译原理学习笔记

发表于 2019-12-29

编译原理是计算机专业的一门重要专业课,旨在介绍编译程序构造的一般原理和基本方法。内容包括语言和文法、词法分析、语法分析、语法制导翻译、中间代码生成、存储管理、代码优化和目标代码生成。

基本概念

  • 词法分析

从左到右逐个字符地扫描,从中识别出一个个“单词”符号。“单词”符号是程序设计语言的基本语法单位,如关键字、标识符、常数、运算符和分隔符等。

  • 语法分析

根据语言的语法规则将单词符号序列分解成各类语法单位,比如表达式、语句和程序等。语法规则就是各类语法单位的构成规则。通过语法分析确定整个输入串是否构成一个语法上正确的程序。

  • 语义分析

检查源程序是否包含静态语义错误,并收集类型信息供后面的代码生成阶段使用。只有语法和语义都正确的源程序才能被翻译成正确的目标代码。

语义分析的一个主要工作是进行类型分析和检查。程序语言中的一个数据类型一般包含两个方面的内容:类型的载体及其上的运算。例如:整除取余运算只能对整型数据进行运算,若其运算对象中有浮点数就认为是类型不匹配的错误。静态的语义错误是指编译程序可以发现,动态的语义错误是指源程序虽然能够被编译和执行,但是结果不对,一般是逻辑上的错误。

编译的过程

编译程序的工作过程一般可以分为5个阶段:

  1. 词法分析
  2. 语法分析
  3. 语义分析和中间代码的产生
  4. 优化
  5. 目标代码生成

参考链接

  1. AST 抽象语法树,by Jartto.
  2. 【编译原理】编译原理简单介绍,by cflys.
  3. 编译原理,by junhey.

3D模型动画分类及其使用

发表于 2019-12-28

3DMax、Blender之类的3D建模软件易学难精,其原因在于很多人不了解其背后的计算机图形学原理。因此,掌握相关的计算机图形学原理和知识,对于我们熟练运用3D建模软件是十分必要的。下面简单介绍3D模型的分类及其使用方法。

3D模型动画分类

3D模型动画的基本原理是让模型中各顶点的位置随时间变化。 主要种类有Morph(变形)动画,关节动画和骨骼蒙皮动画(SkinnedMesh)。从动画数据的角度来说,三者一般都采用关键帧技术,即只给出关键帧的数据,其他帧的数据使用插值得到。但由于这三种技术的不同,关键帧的数据是不一样的。

变形动画

Morph(渐变,变形)动画是直接指定动画每一帧的顶点位置,其动画关键中存储的是Mesh所有顶点在关键帧对应时刻的位置。

关节动画

关节动画的模型不是一个整体的Mesh,而是分成很多部分(Mesh),通过一个父子层次结构将这些分散的Mesh组织在一起,父Mesh带动其下子Mesh的运动,各Mesh中的顶点坐标定义在自己的坐标系中,这样各个Mesh是作为一个整体参与运动的。

动画帧中设置各子Mesh相对于其父Mesh的变换(主要是旋转,当然也可包括移动和缩放),通过子到父,一级级的变换累加(当然从技术上,如果是矩阵操作是累乘)得到该Mesh在整个动画模型所在的坐标空间中的变换(从本文的视角来说就是世界坐标系了,下同),从而确定每个Mesh在世界坐标系中的位置和方向,然后以Mesh为单位渲染即可。

关节动画的问题是,各部分Mesh中的顶点是固定在其Mesh坐标系中的,这样在两个Mesh结合处就可能产生裂缝。

骨骼蒙皮动画

骨骼蒙皮动画即SkinnedMesh了,骨骼蒙皮动画的出现解决了关节动画的裂缝问题。骨骼动画的基本原理可概括为:在骨骼控制下,通过顶点混合动态计算蒙皮网格的顶点,而骨骼的运动相对于其父骨骼,并由动画关键帧数据驱动。

一个骨骼动画通常包括骨骼层次结构数据,网格(Mesh)数据,网格蒙皮数据(skin info)和骨骼的动画(关键帧)数据。

SkinnedMesh原理

SkinnedMesh中文一般称作骨骼蒙皮动画,正如其名,这种动画中包含骨骼(Bone)和蒙皮(Skinned Mesh)两个部分,Bone的层次结构和关节动画类似,Mesh则和关节动画不同:

关节动画中是使用多个分散的Mesh,而Skinned Mesh中Mesh是一个整体,也就是说只有一个Mesh,实际上如果没有骨骼让Mesh运动变形,Mesh就和静态模型一样了。

Skinned Mesh技术的精华在于蒙皮,所谓的皮并不是模型的贴图(也许会有人这么想过吧),而是Mesh本身,蒙皮是指将Mesh中的顶点附着(绑定)在骨骼之上,而且每个顶点可以被多个骨骼所控制,这样在关节处的顶点由于同时受到父子骨骼的拉扯而改变位置就消除了裂缝。

Skinned Mesh这个词从字面上理解似乎是有皮的模型,哦,如果贴图是皮,那么普通静态模型不也都有吗?所以我觉得应该理解为具有蒙皮信息的Mesh或可当做皮肤用的Mesh,这个皮肤就是Mesh。而为了有皮肤功能,Mesh还需要蒙皮信息,即Skin数据,没有Skin数据就是一个普通的静态Mesh了。

Skin数据决定顶点如何绑定到骨骼上。顶点的Skin数据包括顶点受哪些骨骼影响以及这些骨骼影响该顶点时的权重(weight),另外对于每块骨骼还需要骨骼偏移矩阵(BoneOffsetMatrix)用来将顶点从Mesh空间变换到骨骼空间。

SkinnedMesh结构

  • 骨骼决定了模型整体在世界坐标系中的位置和朝向。

先看看静态模型吧,静态模型没有骨骼,我们在世界坐标系中放置静态模型时,只要指定模型自身坐标系在世界坐标系中的位置和朝向。在骨骼动画中,不是把Mesh直接放到世界坐标系中,Mesh只是作为Skin使用的,是依附于骨骼的,真正决定模型在世界坐标系中的位置和朝向的是骨骼。

在渲染静态模型时,由于模型的顶点都是定义在模型坐标系中的,所以各顶点只要经过模型坐标系到世界坐标系的变换后就可进行渲染。而对于骨骼动画,我们设置模型的位置和朝向,实际是在设置根骨骼的位置和朝向,然后根据骨骼层次结构中父子骨骼之间的变换关系计算出各个骨骼的位置和朝向,然后根据骨骼对Mesh中顶点的绑定计算出顶点在世界坐标系中的坐标,从而对顶点进行渲染。要记住,在骨骼动画中,骨骼才是模型主体,Mesh不过是一层皮,一件衣服。

  • 骨骼可理解为一个坐标空间。

骨骼只是一个形象的说法,实际上骨骼可理解为一个坐标空间,关节可理解为骨骼坐标空间的原点。关节的位置由它在父骨骼坐标空间中的位置描述。上图中有三块骨骼,分别是上臂,前臂和两个手指。Clavicle(锁骨)是一个关节,它是上臂的原点,同样肘关节(elbow joint)是前臂的原点,腕关节(wrist)是手指骨骼的原点。关节既决定了骨骼空间的位置,又是骨骼空间的旋转和缩放中心。

骨骼就是坐标空间,骨骼层次就是嵌套的坐标空间。关节只是描述骨骼的位置即骨骼自己的坐标空间原点在其父空间中的位置,绕关节旋转是指骨骼坐标空间(包括所有子空间)自身的旋转。

但还有两个可能的疑问,一是骨骼的长度问题,由于骨骼是坐标空间,没有所谓的长度和宽度的限制,我们看到的长度一方面是蒙皮后的结果,另一方面子骨骼的原点(也就是关节)的位置往往决定了视觉上父骨骼的长度,比如这里upper arm线段的长度实际是由elbow joint的位置决定的。

第二个问题,手指的那个端点是啥啊?实际上在我们的例子中手指没有子骨骼,所以那个端点并不存在:)那是为了方便演示画上去的。实际问题中总有最下层的骨骼,他们不能决定其他骨骼了,他们的作用只剩下控制Mesh顶点。对了,那么手指的长度如何确定?我们看到的长度应该是由蒙皮决定的,也就是由Mesh中属于手指的那些点离腕关节的距离决定。

3D模型动画使用

下面给出一段在Unity3D中控制3D模型动画的代码,作为参考。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

using UnityEngine;
using System.Collections;

public class AnimationScript : MonoBehaviour
{
void Start()
{
Animation animation = this.animation;//动画控制器
animation.Play("idle");//上来直接播放idle动画
}
void OnGUI()
{
if (GUI.Button(new Rect(0, 0, 100, 30), "行走"))
{
animation.Play("run");
}
if (GUI.Button(new Rect(100, 0, 100, 30), "停止"))
{
animation.Play("idle");
}
if (GUI.Button(new Rect(200, 0, 100, 30), "攻击"))
{
animation.Play("attack");
animation.PlayQueued("idle");//播放完attack之后再播放idle
}
}
}

参考链接

  1. 骨骼蒙皮动画(SkinnedMesh)的原理解析,by feng.
  2. 【Unity3D】3D模型的使用——FBX的使用与Animation设置,by yongh701.

GitBook入门教程

发表于 2019-12-26 | 更新于 2020-01-28

GitBook是一种制作在线书籍的工具。它基于Git支持多人协作,支持将采用Markdown语法编辑的文档导出成 PDF,EPUB,HTML等多种格式。

GitBook安装

环境要求

  • NodeJS (v4.0.0 and above is recommended)
  • Windows, Linux, Unix, or Mac OS X

NPM安装GitBook

通过NPM工具安装GitBook是最佳的方法:

1
2
$ npm install gitbook-cli -g
$ gitbook init //下载稳定版的gitbook,同时创建在线书籍

gitbook-cli工具可安装多个GitBook版本到系统上。对于Windows平台,gitbook-cli工具安装的多个GitBook版本通常存储在“C:\Users\CurrentLoginUser\.gitbook”。

离线安装GitBook

内网机器上安装GitBook的方法如下:

  • 安装最新Nodejs长期支持版。
  • 使用npm-bundle命令在线打包gitbook-cli
1
2
npm install npm-bundle -g
npm-bundle gitbook-cli
  • 内网机器上安装gitbook-cli
1
npm install ./gitbook-cli.tgz
  • 将“C:\Users\CurrentLoginUser\.gitbook”目录打包拷贝至内网机器对应位置

创建书籍

1
2
3
$ gitbook init    //在当前目录创建书籍
$ gitbook build //构建在线书籍网站
$ gitbook serve //构建在线书籍网站并启动

参考链接

  1. GitBook 从懵逼到入门,by 阿基米东.
  2. 使用 Gitbook 打造你的电子书,by 文艺小青年.
  3. 世上最佳离线markdown编辑工具(gitbook和gitbook editor),by icharm.
  4. 移除GitBook目录下方的“本书使用GitBook发布”字样,by tedxiong.
  5. EbookError: Error during ebook generation: ‘ebook-convert,by 狼爷.
  6. 书籍配置文件(book.json),by wiliam.

glTF2.0格式解析

发表于 2019-12-25 | 更新于 2019-12-27

glTF(GL传输格式的衍生简称)是一种使用JSON标准的3D场景和模型的文件格式。 它是Khronos Group 3D格式工作组开发的一种与API无关的运行时资产交付格式。 它在HTML5DevConf 2016上宣布。此格式旨在成为一种高效,可互操作的格式,具有最小的文件大小和应用程序对运行时的处理。 因此,其创建者将其描述为“3D JPEG”。 glTF还为3D内容工具和服务定义了一种通用的发布格式。本文旨通过对glTF2.0格式的解析,进一步加深对3D建模的理解。

基本概念

在对glTF2.0格式解析之前,应先了解一些3D建模或glTF独有的基本概念:

  • scenes, nodes:场景的基本结构
  • cameras:场景的可视配置
  • meshes:构成3D对象的几何
  • buffers, bufferViews, accessors:数据参考和布局描述
  • materials:定义数据如何被渲染
  • textures, images, samplers:对象表面显示
  • skins:顶点蒙皮信息
  • animations:随时间改变的属性

glTF概念之间的关系

图1 glTF概念之间的关系

参考链接

  1. glTF,by KhronosGroup.
  2. glTF,by wikipedia.
  3. glTF Overview,by KhronosGroup.
  4. 骨骼蒙皮动画(SkinnedMesh)的原理解析,by feng.
  5. 【Unity3D】3D模型的使用——FBX的使用与Animation设置,by yongh701.

jszip使用方法简介

发表于 2019-12-24

当大文件需要在网络中传输时,最好进行压缩传输,然后在终点进行解压。以ZIP压缩为例,压缩后文件大小极具减小,可节约带宽,提高系统并发能力。下面介绍使用jszip在浏览器端的解压方法。

JSZip简介

JSZip是一个用于创建、读取和编辑.zip文件的javascript库,有一个可爱而简单的API。JSZip支持Nodejs和浏览器端的安装使用。具体方法如下:

1
2
3
4
5
6
7
With npm : npm install jszip

With bower : bower install Stuk/jszip

With component : component install Stuk/jszip

Manually : download JSZip and include the file dist/jszip.js or dist/jszip.min.js

浏览器端解压zip文件

后端Nodejs将zip文件以二进制形式存储到数据库中。当前端需要该zip文件时,后端将zip文件以二进制形式传输到前端,前端再解压还原。

Nodejs使用JSZip压缩文件

1
2
3
4
5
6
7
8
9
10
11
12
var JSZip = require("jszip");
var zip = new JSZip();

// create a file
zip.file("hello.txt", "Hello[p my)6cxsw2q");
// oops, cat on keyboard. Fixing !
zip.file("hello.txt", "Hello World\n");

// create a file and a folder
zip.file("nested/hello.txt", "Hello World\n");
// same as
zip.folder("nested").file("hello.txt", "Hello World\n");

浏览器端解压Zip文件

1
2
3
4
5
6
7
8
9
10
import JSZip from 'jszip'

let new_zip = new JSZip();

// Read zip package
new_zip.loadAsync(content)
.then(function(zip) {
// you now have every files contained in the loaded zip
new_zip.file("hello.txt").async("string");
});

参考链接

  1. ZIP格式,by wikipedia.
  2. gzip,bzip2,zip三种格式压缩率对比,by CatDeacon.
  3. JSZip,by stuk.
上一页1…303132…53下一页

Jack Huang

521 日志
67 标签
© 2025 Jack Huang
由 Hexo 强力驱动
|
主题 — NexT.Muse