0%

3dTiles 格式简介

3dTiles生产介绍

由上图可知:

3D Tiles 是由,瓦片集数据和瓦片数据组成。

瓦片集数据:一个 JSON 文件存储,通常为 tileset.json。

瓦片数据:最终在 cesium 中展示的数据都保存在瓦片数据中。

一、瓦片集数据

一个简单的例子:

通过这个例子可以直观的了解 3D Tiles 格式数据。

文件目录:

3dTiles生产介绍

tileset.json 文件:

3dTiles生产介绍

该切片集切片组织方式最为简单,其只有 3 个瓦片数据。根节点为最简化版本,其 content 指向精细程度最低的瓦片 dragon_low.b3dm。其 children 包含次精细程度瓦片,依次到最精细层。运行时根据 geometricError 参数进行瓦片的选择渲染。

3dTiles生产介绍

3dTiles生产介绍

3dTiles生产介绍

asset(必需):

瓦片集的元数据对象。其中 asset.version(必需)属性定义了 3D Tiles 版本。

geometricError(必需):

根据运行时引入的最大屏幕空间误差(SSE),决定哪个层次的细节应该呈现。。每个 tileset 和每个 tile 都有一个几何误差属性。为非负数,以米为单位,为切片的原始几何的简化来表示的误差值。如:根瓦片是最简化的版本,有最大的几何误差,而源几何数据(叶子瓦片)则具有接近 0 的几何误差。一般可以直接用瓦片集外包区域体对角线表示。

root(必需):

根瓦片。每个 Tile 中的属性如下:

boundingVolume(必需):

切片内容包围框。只有 box,sphere,region 属性。

3dTiles生产介绍

3dTiles生产介绍)3dTiles生产介绍)3dTiles生产介绍

box:定义右手笛卡尔坐标系(x,y,z),z 向上,第一行(前三个)元素为边界框中心坐标。第二行为 x 轴方向的半长。第三行为 y 轴方向半长。第四行为 z 轴方向半长;

sphere:前三个值为球体中心坐标值,最后一个为球体半径(米);

region:定义了在 84 坐标系下具有纬度、经度、和高度坐标的边界区域,[西、南、东、北、最小高度、最大高度。

refine:

根瓦片必需,子瓦片默认继承。可以选择子切片是以”ADD”的方式还是”REPLACE”的方式进行渲染。

3dTiles生产介绍

3dTiles生产介绍

content:

通过 content 属性中的 URI 引用与 tile 关联的实际可呈现的内容。如果是相对路径则是相对 tilesetJSON 文件。除此之外 content 也包含一个 boundingVolume(非必须)属性,其与外层 tile. boundingVolume 不同处在于 content. boundingVolume 是一个紧密贴合的边界框。

children:

子瓦片数组,对于叶子瓦片,此属性可能未定义或者数组长度为 0;子切片内容由其父切片边界范围完全包围。并有更小的 geometricError。

一个复杂例子:

文件目录:

3dTiles生产介绍

tileset.json 文件

3dTiles生产介绍

这里引用外部瓦片集”lab_b_0.json”,当指向外部切片集时 tile.children 必须为空或者未定义。

引用不能形成循环。

transform:支持局部坐标系统时可定义。默认为单位矩阵。( 前端操作瓦片可利用该属性,如炸开效果。)我们展示需求不需要支持地理坐标系,应该不太需要这个参数。

lab_b_0.json 文件:

3dTiles生产介绍

这是一个由多个瓦片构成的瓦片集,可以看到该瓦片集根节点无 content,包含了 11 个 children。这与前面一个简单的例子不同。这是因为其生成瓦片方式的不同,这里是通过无层次结构的树状结构来组织。由此可知 3dtile 可以使用多种不同类型的空间数据结构组织瓦片。而其运行时引擎是通用的,可以呈现 tileset 定义的任何树。下图是常用树状结构:

3dTiles生产介绍

对比超图生成切片所支持的有,四叉树和八叉树结构。我们先尝试简单的四叉树结构来组织切片。

简单的瓦片生成方法

想象我们拿到了一个模型,里面有若干个图元(一个顶点集组成)构成了多个对象。我们要做的是使用四叉树的结构将空间划分下去。起始空为整个模型的外包围框,一生四,四生八…… 这样空间被我们分成了一个个小的瓦片,这里我们要定义瓦片边长来控制每个瓦片的大小,根据瓦片的范围去选择模型中得到图元,最后瓦片空间里存在的若干个模型图元即是瓦片的内容。如果每个瓦片过大,里面包含的对象就过多,在加载这一个瓦片时则会发生卡顿。如果瓦片过小,则瓦片文件则会过于碎片,会增加前端请求量和计算量。通过这个步骤,我们可以得到一个 tile 的索引树。同时也将模型按照空间分成了多个部分(瓦片内容),同时经过计算得出每个瓦片的包围盒其父瓦片包围盒必须包围子瓦片的包围盒。

通过上面的处理,我们已经把 tile 组织了起来,必需的参数还差一个 geometricError,上文写到,使用对角线长作为参数,计算简单。如果显示效果有问题,这个参数有待调整。

这样对于切片集,所必需的的 asset、geometricError、refine、root.boundingVolume、root. geometricError 属性都可以计算出来,生成对应的 json 文件。

LOD 数据生成

到此,还有一个问题,就是我们常说的 LOD,经过上面处理我们仅仅是将一个模型分块(瓦片),如果不做 LOD 即代表原始模型被分成小块传输并加载显示。所以在瓦片化后还需要在叶子瓦片下挂接多级精细程度的模型,就像第一个简单的例子那样,同一个模型分为三个精细层,通过 geometricError 去控制选择。所以,geometricError 如果选择对角线长作为参数,会出现一个问题,比如,简化 10%的对象和原始对象对角线长几乎一样,这样参数很可能会失效,初步考虑需要在简化面的过程中去提取出这个参数。我们可以先搁置这个问题,先采用简单的对角线长来计算,最后根据展示效果再来调整。

对于瓦片 LOD 的处理,这里相对复杂,寻找相关图形算法库自带实现对比效果应该是快速的方法。如果要自己实现减面算法,单纯的实现并不难,但是如果考虑到效率以及简化限度的控制比较麻烦比如,一个正方体他的 lod 不应该变形成其他的形状,对于 BIM 数据而言许多对象应该是存在类似问题,如果远看一个立方体变成一个四面体则很影响效果。

CGAL 中应该是存在解决相关问题的程序包,不过还需要尝试。

3dTiles生产介绍

3dTiles生产介绍

瓦片格式转换

最后需要将每个瓦片内容生成 3dtile 的瓦片的格式,即 b3dm、i3dm 等等。我们应该是使用常用的 b3dm 格式。对于该格式生成开源已经有少量工具(官方 obj2gltf 工具)可以借助,这一步涉及到属性数据及对象区分(比如,后续点击展示信息需要用到)。总体来说将模型在瓦片化之后,将每个瓦片内容转换成 b3dm 导出,存储路径即为 tile.content.uri 的值。具体格式内容这里不多描述,后续可以参考标准做转换。

简单生产流程实现

  1. 完成 obj->gltf->glb->b3dm 格式转换。

  2. 拆分 obj 中的对象并将每个对象生成对应的 b3dm 瓦片文件。同时需要按照所期望的加载顺序对瓦片进行排序。如:先加载外壳瓦片展示模型整体情况,再加载内部瓦片。

  3. 根据排序,组织 b3dm 瓦片计算生成对应瓦片集 tileset.json 文件。

  4. 将不同 LOD 的 OBJ 合成至同一瓦片集生成 tileset.json 文件

  5. 渲染加载效果:可以看到每个瓦片对象,根据第 3 步,定义的 tileset.json,按顺序渲染出来。

3dTiles生产介绍

一、算法相关资料

算法来自论文:
Surface Simplification Using Quadric Error Metrics

通过计算网格图形上的每一条边的权重,每次移除最小权重的边。重复这个过程达到简化效果。

二、算法步骤

QEM算法步骤

  1. 初始化所有顶点的 Q 矩阵。
  2. 选择所有有效边。 (所有联通边 (v1, v2) 、或者长度小于某一个阈值的边。)
  3. 计算所有有效边的误差 ¯vT(Q1+ Q2)¯v 作为这条边的cost。
  4. 将所有的边按照cost的权值放到一个最小堆里。
  5. 每次移除最小的边,并且更新包含着v1的所有有效边的代价。

Q矩阵计算方法

基础知识平面方程(Plane Equation)

定义顶点的误差为顶点到该顶点相交的三角形的平面的距离平方和:

QEM距离平方和

  • 其中P为平面方程 [a,b,c,d]T , v为顶点[x,y,z,1],法向量 n = [a,b,c]

  • (P·v)/|n| 为点v到平面P的距离

  • n模长等于1时,P·v即点到平面距离。

QEM距离平方和

QEM矩阵Kp

这个基本二次误差Kp可以用来求空间中任意点到平面P的平方距离。我们可以把这些基本二次曲面加起来,用一个矩阵Q表示整个平面集合。

新顶点位置计算

  1. 选择v1v2(v1+v2)/2中选择一个;

  2. 对二次项式Δ(v)求导,当求导等于0时;

    QEM求导

    当左边矩阵可逆时,可求解:

    QEM可逆

    否则,根据第一条策略。

实现

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
function calculatorEdgeDelta(geometry, edge) {
let mat = new THREE.Matrix4()
let deltaV = 0;
//计算Q1+Q2
let q1 = calculatorVertexDelta(geometry, edge.a).elements
let q2 = calculatorVertexDelta(geometry, edge.b).elements
mat.elements = q1.map((e, i) => {
return e + q2[i]
})
edge.midv = calculatorVertexPos(geometry, edge)
//计算vT (Q1+Q2) v 得到边的代价(cost)deltaV
for (let j = 0; j < 4; j++) {
let t = 0;
for (let k = 0; k < 4; k++) {
// console.log(edge.v.getComponent(k))
t += edge.midv.getComponent(k) * mat.elements[j * 4 + k];
}
deltaV += t * edge.midv.getComponent(j)
}
edge.deltaV = deltaV
return edge
}


//计算顶点Q矩阵
//传入一个顶点 找到与其关联的边 再找到 面 求Kp二次基本误差矩阵PTP之和Q 并返回
function calculatorVertexDelta(geometry, vIndex) {
let result = new THREE.Matrix4()
result.elements[0] = 0
result.elements[5] = 0
result.elements[10] = 0
result.elements[15] = 0
let tmp = new THREE.Vector4()
let f = findFaceWithVindex(geometry, vIndex)
//找出与顶点vIndex关联的面 得到其平面方程[a,b,c,d] 计算Kp矩阵之和Q
f.map(face => {
//计算平面方程系数abcd 即 法向量(a,b,c)
//d = -(ax+by+cz)
let { x, y, z } = face.normal;
let d = -geometry.vertices[face.a].dot(face.normal)
tmp.set(x, y, z, d)
for (let j = 0; j < 4; j++) {
for (let k = 0; k < 4; k++) {
result.elements[j * 4 + k] += tmp.getComponent(j) * tmp.getComponent(k);
}
}
})
return result;
}

效果

下图是使用three.js 实现QEM算法。对OBJ模型动态计算过程。可以看到对于简单模型,简化效果基本满足。经过多个模型测试,对于特殊形状模型简化容易出现破面现象。

QEM求导

一、了解模型矩阵、视图矩阵、投影矩阵

计算机在显示一个三维物体时,本质都是对相应的 顶点 坐标进行计算,进而渲染出图元、片元,得到显示屏上看到的结果。

而最原本的模型的各个顶点坐标并不是在世界坐标系中建立的(世界坐标系即下一节的球心坐标(ECEF))。以下称原本的模型坐标系为模型本体坐标系

那么我们需要将模型在世界坐标系下进行展示和运动,就得将模型所有顶点都转换到世界坐标系下。 这个转换过程可以用44的矩阵表示,称为*模型矩阵(Model Matrix)**。

现在模型通过模型矩阵转换成了世界坐标。需要用一个相机(Camera)来模拟人眼观测位置,因此,需要根据相机的位置和观察方位,将世界坐标系中的所有顶点(不只是模型)坐标转换为视图坐标系 (也称相机坐标系) 中的坐标,这个转换过程也可用44的矩阵表示,称为*视图矩阵(View Matrix)**

在视图坐标系中,所有的点都是三维的,最终要在显示器上展示。要将三维坐标投影到二维平面上去,这个44的矩阵表示,称为 *(透视)投影矩阵(Perspective projection Matrix)**

将模型顶点坐标与三个矩阵相乘后即得到屏幕(裁剪)坐标。

二、了解球心坐标(ECEF)与本地坐标(NEU)

本地坐标系(NEU): 一种地方空间直角系,原点为O,其北向坐标轴(N 坐标)为过O点的子午线的切线,指北为正。其东向坐标轴(E坐标)为过O点的椭球的平行圈的切线,指东为正。天顶向坐标轴(U坐标),为过O点垂直于N轴与E轴,指向天顶为正。

球心坐标系(ECEF,Earth-Centered,Earth-Fixed): 以地球为中心,坐标轴以International Reference Pole(IRM)和International Reference Meridian(IRM)为准,国际参考极点和国际参考子午线,遵照地表所确定。(earth-fixed)

See docs.inertialsense.com

三、平移、旋转、缩放

要用变换矩阵用来表示平移,需要将原本为(x,y,z)的点加上一维,即(x,y,z,w)这种四元组的方式来表达坐标,等同于三维坐标(x/w,y/w,z/w),称为 (齐次坐标)

齐次坐标系使得我们可以在一中特殊的方程组中求出解,这个方程组中每一个方程都表示一个与系统中其他直线平行的直线。我们知道在欧几里得空间中,对这种方程组是无解的,因为他们没有交点。然而在现实世界中我们是可以看到两条平行线相交的。

规定(x, y, z, 0)表示一个向量,(x, y, z, 1)或(x’, y’, z’, 2)等w不为0时来表示点。

对于平移变换矩阵:
平移变换矩阵

对于X旋转变换矩阵:

X旋转变换矩阵

对于Y旋转变换矩阵:

Y旋转变换矩阵

对于Z旋转变换矩阵:

Z旋转变换矩

对于缩放变换矩阵:

缩放变换矩阵

对于任意轴旋转:

首先将旋转轴平移至与坐标轴重合,然后进行旋转,最后再平移回去。

第一步平移

很简单,坐标做减法就可以得出。

第二步旋转

将向量旋转至与Z轴重合。即先绕X轴转角度为α;再绕Y轴旋转至与Z轴重合,旋转的角度为-β。

绕Z轴旋转角度θ

第三步

反向旋转平移回去,得到结果。即绕Y轴旋转角度为β;绕X轴转角度为-α;反向平移。

如果不需要平移,最终结果公式为:

Javascript 读取 Shapefile 教程

一、Shapefile 简介

​ Shapefile 是美国环境系统研究所公司(ESRI)开发的一种空间数据开放格式,属于一种矢量图形格式,它能够保存几何图形的位置及相关属性,实际上该种文件格式是由多个文件组成的,有三个必须的文件:
​ .shp— 图形格式,用于保存元素的几何实体。
​ .shx— 图形索引格式。几何体位置索引,记录每一个几何体在 shp 文件之中的位置,能够加快向前或向后搜索一个几何体的效率。
​ .dbf— 属性数据格式,以 dBase IV 的数据表格式存储每个几何形状的属性数据。

二、shp 文件

​ shp 文件存储了坐标位置信息,该文件由一个定长的文件头和一个或若干个变长的记录数据组成。每一条变长数据记录包含一个记录头和一些记录内容。主文件头包含 17 个字段,共 100 个字节,其中包含九个 4 字节(32 位有符号整数,int32)整数字段,紧接着是八个 8 字节(双精度浮点数)有符号浮点数字段。

字节 类型 字节序 用途
0–3 int32 大端序 文件编号 (永远是十六进制数 0x0000270a)
4–23 int32 大端序 五个没有被使用的 32 位整数
24–27 int32 大端序 文件长度,包括文件头。(用 16 位整数表示)
28–31 int32 小端序 版本
32–35 int32 小端序 图形类型(参见下面)
36–67 double 小端序 最小外接矩形(MBR),也就是一个包含 shapefile 之中所有图形的矩形。以四个浮点数表示,分别是 X 坐标最小值,Y 坐标最小值,X 坐标最大值,Y 坐标最大值。
68–83 double 小端序 Z 坐标值的范围。以两个浮点数表示,分别是 Z 坐标的最小值与 Z 坐标的最大值。
84–99 double 小端序 M 坐标值的范围。以两个浮点数表示,分别是 M 坐标的最小值与 M 坐标的最大值。

​ 然后这个文件包含不定数目的变长数据记录,每个数据记录以一个 8 字节记录头开始:

字节 类型 字节序 用途
0–3 int32 大端序 记录编号 (从 1 开始)
4–7 int32 大端序 记录长度(以 16 位整数表示)

​ 在记录头的后面就是实际的记录:

字节 类型 字节序 用途
0–3 int32 小端序 图形类型(参见下面)
4– - - 图形内容

​ 变长记录的内容由图形的类型决定。Shapefile 支持以下的图形类型:

图形类型 字段
0 空图形
1 Point(点) X, Y
3 Polyline(折线) (最小包围矩形)MBR,组成部分数目,点的数目,所有组成部分,所有点
5 Polygon(多边形) (最小包围矩形)MBR,组成部分数目,点的数目,所有组成部分,所有点
8 MultiPoint(多点) (最小包围矩形)MBR,点的数目,所有点
11 PointZ(带 Z 与 M 坐标的点) X, Y, Z, M
13 PolylineZ(带 Z 或 M 坐标的折线) 必须的: (最小包围矩形)MBR,组成部分数目,点的数目,所有组成部分,所有点,Z 坐标范围, Z 坐标数组 可选的: M 坐标范围, M 坐标数组
15 PolygonZ(带 Z 或 M 坐标的多边形) 必须的: (最小包围矩形)MBR,组成部分数目,点的数目,所有组成部分,所有点,Z 坐标范围, Z 坐标数组 可选的: M 坐标范围, M 坐标数组
18 MultiPointZ(带 Z 或 M 坐标的多点) 必须的: (最小包围矩形)MBR,点的数目,所有点, Z 坐标范围, Z 坐标数组 可选的: M 坐标范围, M 坐标数组
21 PointM(带 M 坐标的点) X, Y, M
23 PolylineM(带 M 坐标的折线) 必须的: (最小包围矩形)MBR,组成部分数目,点的数目,所有组成部分,所有点 可选的: M 坐标范围, M 坐标数组
25 PolygonM(带 M 坐标的多边形) 必须的: (最小包围矩形)MBR,组成部分数目,点的数目,所有组成部分,所有点 可选的: M 坐标范围, M 坐标数组
28 MultiPointM(带 M 坐标的多点) 必须的: (最小包围矩形)MBR,点的数目,所有点 可选的: M 坐标范围, M 坐标数组
31 MultiPatch 必须的: (最小包围矩形)MBR,组成部分数目,点的数目,所有组成部分,所有点,Z 坐标范围, Z 坐标数组 可选的: M 坐标范围, M 坐标数组

三、shp 文件读取

​ 首先新建 Html 的 input 元素和 canvas 元素,当 shp 文件上传成功后,通过FileReader读取 shp 文件,然后将对应的图形绘制到 Canvas 上。

html 页面部分:

1
2
<input type="file" id="file" onchange="readAsArrayBuffer()" />
<canvas id="canvas" width="800" height="800"></canvas>

parseHeader 函数解析 shp 的头文件
parseShape 行数解析 shp 文件的坐标信息
darwShape 函数将坐标信息绘制到 Canvas 上

Javascript 代码:

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
var file = document.getElementById("file");
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
context.strokeStyle = "rgba(0,0,0,1)";
var width = canvas.width;
var height = canvas.height;

function readAsArrayBuffer(e) {
context.clearRect(0, 0, width, height);
var shp = file.files[0];
var reader = new FileReader();
reader.readAsArrayBuffer(shp);
reader.onload = function (f) {
var dataView = new DataView(this.result);
var pos = 0;
var header = parseHeader(dataView, pos);
pos = header.pos;
while (pos < header.byteLength) {
try {
var shape = parseShape(dataView, pos);
pos = shape.pos;
darwShape(header, shape);
} catch (e) {
console.log(e);
break;
}
}
};
}

function darwShape(header, shape) {
var xWidth = header.maxX - header.minX;
var yHeight = header.maxY - header.minY;
var i = 0;
var x, y;
context.fillStyle = "rgba(255,0,0,0.6)";
switch (shape.type) {
case 1: // Point
case 11: // PointZ
case 21: // PointM
var point = shape.coordinates;
if (header.maxX == header.minX || header.maxY == header.minY) {
context.fillRect(width / 2, height / 2, 10, 10);
} else {
x = ((point[0] - header.minX) / xWidth) * width;
y = ((yHeight - (point[1] - header.minY)) / yHeight) * height;
context.fillRect(x - 5, y - 5, 10, 10);
}
break;
case 8: // MultiPoint
case 18: // MultiPointZ
case 28: // MultiPointM
var points = shape.coordinates;
for (; i < points.length; i++) {
x = ((points[i][0] - header.minX) / xWidth) * width;
y = ((yHeight - (points[i][1] - header.minY)) / yHeight) * height;
context.fillRect(x - 5, y - 5, 10, 10);
}
break;
case 3: // Polyline
case 13: // PolylineZ
case 23: // PolylineM
var paths = shape.coordinates;
for (; i < paths.length; i++) {
context.beginPath();
for (var j = 0; j < paths[i].length; j++) {
x = ((paths[i][j][0] - header.minX) / xWidth) * width;
y = ((yHeight - (paths[i][j][1] - header.minY)) / yHeight) * height;
if (j == 0) {
context.moveTo(x, y);
} else {
context.lineTo(x, y);
}
}
context.stroke();
}
break;
case 5: // Polygon
case 15: // PolygonZ
case 25: // PolygonM
var rings = shape.coordinates.slice(0);
// Judging that the previous polygon has been drawed
for (; i < rings.length; i++) {
var clockwise = isClockwise(rings[i]);
if (!clockwise) {
var gCenter = calcGravityCenter(rings[i]);
x = ((gCenter[0] - header.minX) / xWidth) * width;
y = ((yHeight - (gCenter[1] - header.minY)) / yHeight) * height;
var pickData = context.getImageData(x, y, 1, 1).data;
if (
pickData[0] != 0 ||
pickData[1] != 0 ||
pickData[2] != 0 ||
pickData[3] != 0
) {
rings.splice(i, 1);
i--;
}
}
}

i = 0;
for (; i < rings.length; i++) {
var clockwise = isClockwise(rings[i]);
if (clockwise) {
context.beginPath();
for (var j = 0; j < rings[i].length; j++) {
x = ((rings[i][j][0] - header.minX) / xWidth) * width;
y = ((yHeight - (rings[i][j][1] - header.minY)) / yHeight) * height;
if (j == 0) {
context.moveTo(x, y);
} else {
context.lineTo(x, y);
}
}
context.closePath();
context.stroke();
context.fill();
}
}
context.globalCompositeOperation = "destination-out";
context.fillStyle = "rgba(255,255,255,1)";
i = 0;
for (; i < rings.length; i++) {
var clockwise = isClockwise(rings[i]);
if (!clockwise) {
context.beginPath();
for (var j = 0; j < rings[i].length; j++) {
x = ((rings[i][j][0] - header.minX) / xWidth) * width;
y = ((yHeight - (rings[i][j][1] - header.minY)) / yHeight) * height;
if (j == 0) {
context.moveTo(x, y);
} else {
context.lineTo(x, y);
}
}
context.closePath();
context.stroke();
context.fill();
}
}
context.globalCompositeOperation = "source-over";
break;
}
}

function isClockwise(ring) {
if (ring.length < 3) {
return -1;
}
var i = 0,
j = 0;
var area = 0;
for (; i < ring.length; i++) {
j = (i + 1) % ring.length;
area += ring[i][0] * ring[j][1];
area -= ring[j][0] * ring[i][1];
}
return area < 0;
}

function calcGravityCenter(ring) {
var xmin = Number.MAX_VALUE;
var ymin = Number.MAX_VALUE;
var i = 0;
for (; i < ring.length; i++) {
if (ring[i][0] < xmin) {
xmin = ring[i][0];
}
if (ring[i][1] < ymin) {
ymin = ring[i][1];
}
}
var momentX = 0;
var momentY = 0;
var weight = 0;
i = 0;
for (; i < ring.length; i++) {
var p1 = ring[i];
var p2;
if (i == ring.length - 1) {
p2 = ring[0];
} else {
p2 = ring[i + 1];
}
var dWeight =
(p1[0] - xmin) * (p2[1] - p1[1]) -
((p1[0] - xmin) * (ymin - p1[1])) / 2 -
((p2[0] - xmin) * (p2[1] - ymin)) / 2 -
((p1[0] - p2[0]) * (p2[1] - p1[1])) / 2;
weight += dWeight;
var pTmp = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2];
var gravityX = xmin + ((pTmp[0] - xmin) * 2) / 3;
var gravityY = ymin + ((pTmp[1] - ymin) * 2) / 3;
momentX += gravityX * dWeight;
momentY += gravityY * dWeight;
}
return [momentX / weight, momentY / weight];
}

/**
* read the shp header info .
*
* @param dataView The DataView containing the main shapefile.
* @param pos The start position of DataView.
*/
function parseHeader(dataView, pos) {
var header = {};
header.fileCode = dataView.getInt32(pos, false);
if (header.fileCode != 0x0000270a) {
throw new Error("Unknown file code: " + header.fileCode);
}
pos += 4;
pos += 5 * 4; //unused
var wordLength = dataView.getInt32(pos, false); //bigEndian the total length of the file in 16-bit words
header.byteLength = wordLength * 2;
pos += 4;
header.version = dataView.getInt32(pos, true); //littleEndian
pos += 4;
header.shapeType = dataView.getInt32(pos, true);
pos += 4;
header.minX = dataView.getFloat64(pos, true);
header.minY = dataView.getFloat64(pos + 8, true);
header.maxX = dataView.getFloat64(pos + 16, true);
header.maxY = dataView.getFloat64(pos + 24, true);
header.minZ = dataView.getFloat64(pos + 32, true);
header.maxZ = dataView.getFloat64(pos + 40, true);
header.minM = dataView.getFloat64(pos + 48, true);
header.maxM = dataView.getFloat64(pos + 56, true);
pos += 8 * 8;
header.pos = pos;
return header;
}

/**
* read the shp content.
*
* @param dataView The DataView containing the main shapefile.
* @param pos The start position of DataView.
*/
function parseShape(dataView, pos) {
var shape = {};
shape.number = dataView.getInt32(pos, false); //bigEndian
pos += 4;
shape.length = dataView.getInt32(pos, false) * 2; //bigEndian
pos += 4;
shape.type = dataView.getInt32(pos, true); //littleEndian
pos += 4;

var i = 0;
var coordinates = [];
switch (shape.type) {
case 0: // Null
break;
case 1: // Point (x,y)
case 11: // PointZ (X, Y, Z, M)
case 21: // PointM (X, Y, M)
var point = [
dataView.getFloat64(pos, true),
dataView.getFloat64(pos + 8, true),
];
pos += 16;
coordinates = point;
if (shape.type == 11) {
pos += 8; //read z value float64
pos += 8; //read m value float64
}
if (shape.type == 21) {
pos += 8; //read m value float64
}
shape.coordinates = coordinates;
break;
case 8: // MultiPoint (MBR, pointCount, points)
case 18: // MultiPointZ
case 28: // MultiPointM
var minX = dataView.getFloat64(pos, true);
var minY = dataView.getFloat64(pos + 8, true);
var maxX = dataView.getFloat64(pos + 16, true);
var maxY = dataView.getFloat64(pos + 24, true);
var pointCount = dataView.getInt32(pos + 32, true); //x y
pos += 36; //8*4+4
for (i = 0; i < pointCount; i++) {
var point = [
dataView.getFloat64(pos, true),
dataView.getFloat64(pos + 8, true),
];
pos += 16;
coordinates.push(point);
}
if (shape.type == 18) {
pos += 8; //zmin
pos += 8; //zmax
pos += pointCount * 8; //read z values float64

pos += 8; //mmin
pos += 8; //mmax
pos += pointCount * 8; //read m values float64
}
if (shape.type == 28) {
pos += 8; //mmin
pos += 8; //mmax
pos += pointCount * 8; //read m values float64
}
shape.extent = {
minX: minX,
minY: minY,
maxX: maxX,
maxY: maxY,
};
shape.coordinates = coordinates;
break;
case 3: // Polyline
case 13: // PolylineZ
case 23: // PolylineM
case 5: // Polygon
case 15: // PolygonZ
case 25: // PolygonM
var minX = dataView.getFloat64(pos, true);
var minY = dataView.getFloat64(pos + 8, true);
var maxX = dataView.getFloat64(pos + 16, true);
var maxY = dataView.getFloat64(pos + 24, true);
var parts = new Int32Array(dataView.getInt32(pos + 32, true));
var pointCount = dataView.getInt32(pos + 36, true); //x y
pos += 40; //8*4+4+4

for (i = 0; i < parts.length; i++) {
parts[i] = dataView.getInt32(pos, true);
pos += 4;
coordinates.push([]);
}
var p = 1;
for (i = 0; i < pointCount; i++) {
var point = [
dataView.getFloat64(pos, true),
dataView.getFloat64(pos + 8, true),
];
pos += 16;
if (p >= parts.length) {
coordinates[p - 1].push(point);
} else {
if (i < parts[p]) {
coordinates[p - 1].push(point);
} else {
p++;
}
}
}
if (shape.type == 15 || shape.type == 13) {
pos += 8; //zmin
pos += 8; //zmax
pos += pointCount * 8; //read z values float64

pos += 8; //mmin
pos += 8; //mmax
pos += pointCount * 8; //read m values float64
}
if (shape.type == 25 || shape.type == 23) {
pos += 8; //mmin
pos += 8; //mmax
pos += pointCount * 8; //read m values float64
}
shape.extent = {
minX: minX,
minY: minY,
maxX: maxX,
maxY: maxY,
};
shape.coordinates = coordinates;
break;
case 31: // MultiPatch
throw new Error("Shape type not supported: " + shape.type);
default:
throw new Error("Unknown shape type at " + (pos - 4) + ": " + shape.type);
}
shape.pos = pos;
return shape;
}

四、结果

JS加载shp

资料

shapefile 白皮书

Vue响应式原理 Reactivity System

1.介绍

​ 根据尤雨溪在FrontEnd Master上面的一个WorkShop,通过讲解一些基础来更好的理解Vue。自己对着这个仓库里的练习和搜索引擎大法完成。

2.Reactivity System

​ 响应式指的就是当我们改变了某个状态时候,会自动的更新系统中的相关连的变化。这里我们通常指改变状态后怎么变更DOM。Vue是怎么跟踪变化,做到响应式的。

3.页面开发中的同步问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//页面可以自动同步state
//一个state的追踪函数 onStateChanged()
//当state改变时更新页面
onStateChanged(() => {
view = render(state)
})
//它内部大概是实现
let update, state
const onStateChanged = _update => {
update = _update
}
//接受一个newState,更新oldState,然后调用update()
//React中每次去setState就好
const setState = newState => {
state = newState
update()
}
setState({ a: 5 })

//Vue or Angular 直接访问state.a
state.a = 5
//Ang使用的是脏检查,而Vue中每一个state变得reactive(使用object.defineProperty)

​ 我们也要实现一个类似onStateChanged()这样的函数。我们下面把这个函数改名为autorun(),我们传入一个update()函数给这个autorun(),它则自动去更新视图。在Vue中则是直接改变state.a,就会自动触发更新DOM。

4.Vue-WorkShop-Basic

1-1 Getters and Setters

> expected usage:
>
>
1
2
3
4
5
6
7
8
9
10
>function observe (obj) {
// Implement this!
>}

>const obj = { foo: 123 }
>observe(obj)

>obj.foo // should log: 'getting key "foo": 123'
>obj.foo = 234 // should log: 'setting key "foo" to 234'
>obj.foo // should log: 'getting key "foo": 234'

​ 答案:

​ 这个函数目的就是,使传入的state对象的读写都会被监测到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function observe(obj){
Object.keys(obj).forEach((key)=>{
let temp = obj[key]
Object.defineProperty(obj,key,{
get:function(){
console.log('get count is :', temp)
return temp;
},
set:function(newvalue){
temp = newvalue;
console.log('set count is: ',temp)
}
})
})
}

1-2 Dependency Tracking

​ 实现一个Dep类,里面有dependnotify两个方法。

​ 实现autorun()。接受update()函数。

​ update()函数中可以调用dep.depend(),来显式的添加依赖到dep对象

​ 然后,可以调用dep.notify()来再次运行

> The full usage should look like this:
>
> 
1
2
3
4
5
6
7
8
9
10
const dep = new Dep()

autorun(() => {
dep.depend()
console.log('updated')
})
// should log: "updated"

dep.notify()
// should log: "updated"

​ 分析下:

​ 这个dep类里实现发布订阅模式。

​ update()需要被注册到dep对象中,这样dep.notify()就可以调用里面的注册了的函数

​ dep.depend()目的是将需要执行的update()函数注册到dep对象中。

​ autorun(),负责调用这个update()函数,比如更新视图

​ 答案:

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
window.Dep = class Dep {
constructor(){
this.subscribers = new Set()
}
depend(){
if(activeUpdate){
this.subscribers.add(activeUpdate)
}
console.log('is depended')
}
notify(){
this.subscribers.forEach((subscriber)=>{subscriber()})
}
}
let activeUpdate;
function autorun (update) {
//这里使用一个WrappedUpdate包裹住update,运行时update中调用的depend可以
//拿到activeUpdate,而且这样做我们可以知道当前正在执行的是哪一个update()
function WrappedUpdate() {
activeUpdate = WrappedUpdate
update()
activeUpdate = null
}
WrappedUpdate()
}

1-3 Mini Observer

​ observe()接收对象中的属性并使其具有响应式。对于每个转换后的属性,它都被分配一个Dep实例,Dep实例跟踪订阅更新函数的列表,并在调用其setter时触发它们重新运行。

​ autorun()接受一个更新函数,并在更新函数订阅的属性发生变化时重新运行它。如果更新函数在计算期间依赖于某个属性,则该函数被称为“订阅”该属性。

They should support the following usage:

1
2
3
4
5
6
7
8
9
10
11
12
13
const state = {
count: 0
}

observe(state)

autorun(() => {
console.log(state.count)
})
// should immediately log "count is: 0"

state.count++
// should log "count is: 1"

答案:

​ 也就是吧之前的结合起来,具体看codepen↓

在线(codepen)

光源类型:平行光、点光光源、环境光。

反射类型:漫反射、环境反射。

颜色计算

漫反射光颜色 = 入射光颜色 * 表面基底色 * cos θ

θ为入射角 =>

漫反射光颜色 = 入射光颜色 * 表面基底色 * (光线方向 · 法线方向)

光线方向为入射方向反方向的单位向量,两单位向量点乘等于向量夹角的cos值。

环境反射光颜色 = 入射光颜色 * 表面基底色

表面的反射颜色 = 漫反射光颜色 + 环境反射光颜色

法向量计算

正面观察表面,顶点顺序为顺时针。右手法则确定法向量。

变换后的法向量 = 法向量x模型矩阵的逆转置矩阵

逐片元计算

点光源下产生的效果,与通过顶点颜色内插出表面上每个片元的颜色的效果并不完全相同。某些情况下甚至很不一样。需要对表面上每个片元计算光照效果。

  1. 片元在世界坐标系下的坐标
  2. 片元处法向量。将顶点的世界坐标和法向量以varying变量的形式传给片元着色器,片元着色器中就已经是内插后的逐片元值了。

视点、观察目标点、上方向

视点: 观察者所处位置。

观察目标点: 被观察的点,确定视线。

上方向: 最终绘制在屏幕上的影像中的向上的方向。

WebGL中观察者默认状态

  • 视点位于坐标系统(0,0,0)
  • 视线为Z负方向。观察点为(0,0,-1),上方向为Y轴负方向(0,1,0)

可视范围(正射类型)

WebGL只有物体在可视范围内才会绘制。水平视角、垂直视角、可视深度定义了可视空间(view volume)

可视空间

  1. 长方体可视空间,盒装空间,正射投影(orthographic projection)产生。
  2. 四棱锥/金字塔可视空间,由透视投影(perspective projection)产生。
盒状空间

近裁剪面远裁剪面两个矩形表面确定。
正射投影矩阵(orthographic projection matrix)

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
Matrix4.prototype.setOrtho = function(left, right, bottom, top, near, far) {
var e, rw, rh, rd;

if (left === right || bottom === top || near === far) {
throw 'null frustum';
}
rw = 1 / (right - left);
rh = 1 / (top - bottom);
rd = 1 / (far - near);

e = this.elements;

e[0] = 2 * rw;
e[1] = 0;
e[2] = 0;
e[3] = 0;

e[4] = 0;
e[5] = 2 * rh;
e[6] = 0;
e[7] = 0;

e[8] = 0;
e[9] = 0;
e[10] = -2 * rd;
e[11] = 0;

e[12] = -(right + left) * rw;
e[13] = -(top + bottom) * rh;
e[14] = -(far + near) * rd;
e[15] = 1;

return this;
};

盒装空间

可视空间

透视投影可视空间
透视投影矩阵(perspective projection matrix):

  1. 根据顶点与视点的距离,按比例进行了缩小变换

  2. 进行了平移变换,贴近视线。

    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
     Matrix4.prototype.setPerspective = function(fovy, aspect, near, far) {
    var e, rd, s, ct;

    if (near === far || aspect === 0) {
    throw 'null frustum';
    }
    if (near <= 0) {
    throw 'near <= 0';
    }
    if (far <= 0) {
    throw 'far <= 0';
    }

    fovy = Math.PI * fovy / 180 / 2;
    s = Math.sin(fovy);
    if (s === 0) {
    throw 'null frustum';
    }

    rd = 1 / (far - near);
    ct = Math.cos(fovy) / s;

    e = this.elements;

    e[0] = ct / aspect;
    e[1] = 0;
    e[2] = 0;
    e[3] = 0;

    e[4] = 0;
    e[5] = ct;
    e[6] = 0;
    e[7] = 0;

    e[8] = 0;
    e[9] = 0;
    e[10] = -(far + near) * rd;
    e[11] = -1;

    e[12] = 0;
    e[13] = 0;
    e[14] = -2 * near * far * rd;
    e[15] = 0;

    return this;
    };

    模型视图投影矩阵(model view projection matrix)

顶点在规范立方体中的坐标 = <投影矩阵>x<视图矩阵>x<模型矩阵>x<顶点坐标>
*<投影矩阵>x<视图矩阵>x<模型矩阵>* 这个式子和顶点没关系,可以再js中计算出结果,即模型视图投影矩阵传入给顶点着色器。

处理复杂的三维模型

多个简单模型组成的复杂模型,其之间存在连接关系,例如“A部件带动B部件运动”。

三维模型之间并没有真正连接在一起。上层的变化时需要施加其下层模型同样的模型矩阵,这样,上层模型就能跟随下层模型变换。

通过索引绘制物体

将三角形顶点索引写入缓冲区,并绑定到gl.ELEMENT_ARRAY_BUFFER

使用gl.drawElements()进行绘制。其对每个索引值,从绑定到gl.ARRAY_BUFFER的缓冲区中获取顶点信息,并传递给attribute变量,执行一次顶点着色器。

  1. 可以重复利用顶点信息,控制内存开销。

  2. 索引值无法将颜色区分开,颜色依赖于顶点,所以如果单个点在不同表面上时,需要创建多个具有相同坐标不同颜色的顶点。

着色器与着色器程序对象

着色器对象:管理一个顶点着色器或者一个片元着色器。每个着色器都是一个着色器对象。

程序对象:管理着色器对象的容器。一个程序对象必须包含一个顶点着色器与一个片元着色器。

创建和初始化着色器

1
2
3
4
5
6
7
8
gl.createShader(type) //1.创建着色器对象
gl.shaderSource(shader, source) //2.向着色器对象中填充着色器程序源代码
gl.compileShader(shader) //3.编译着色器
---------
gl.createProgram() //4.创建程序对象
gl.attachShader(program, shader) //5.为程序对象分配着色器
gl.linkProgram(program) //6.连接程序对象
gl.useProgram(program) //7.使用程序对象