-
-
Notifications
You must be signed in to change notification settings - Fork 35.7k
/
Copy pathoptimize-lots-of-objects.html
438 lines (399 loc) · 22.8 KB
/
optimize-lots-of-objects.html
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
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
<!DOCTYPE html><html lang="zh"><head>
<meta charset="utf-8">
<title>大量对象的优化</title>
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@threejs">
<meta name="twitter:title" content="Three.js – 大量对象的优化">
<meta property="og:image" content="https://threejs.org/files/share.png">
<link rel="shortcut icon" href="../../files/favicon_white.ico" media="(prefers-color-scheme: dark)">
<link rel="shortcut icon" href="../../files/favicon.ico" media="(prefers-color-scheme: light)">
<link rel="stylesheet" href="../resources/lesson.css">
<link rel="stylesheet" href="../resources/lang.css">
<script type="importmap">
{
"imports": {
"three": "../../build/three.module.js"
}
}
</script>
<link rel="stylesheet" href="/manual/zh/lang.css">
</head>
<body>
<div class="container">
<div class="lesson-title">
<h1>大量对象的优化</h1>
</div>
<div class="lesson">
<div class="lesson-main">
<p>本文是关于 three.js 系列文章的一部分. 第一篇文章是 <a href="fundamentals.html">three.js 基础</a>. 如果你还没看过而且对three.js 还不熟悉,那应该从那里开始.</p>
<p>three.js的优化有很多种方式. 常见的一种叫做<em>合并几何体</em>. 每一个你创建的<a href="/docs/#api/zh/objects/Mesh"><code class="notranslate" translate="no">Mesh</code></a>代表一个(或多个)请求系统渲染的命令. 即便是画出来的结果一样, 画两个几何体总是比画一个要费时费力. 所以最好的方式就是将这些mesh合并起来. </p>
<p>让我们来展示一个应用这种优化方式的优秀范例. 让我们来重新创建一个<a href="https://globe.chromeexperiments.com/">WebGL Globe</a>.</p>
<p>第一件事是获取一些数据. WebGL Globe说他们的数据是来自<a href="http://sedac.ciesin.columbia.edu/gpw/">SEDAC</a>. 点开这个网站我们可以看到<a href="https://beta.sedac.ciesin.columbia.edu/data/set/gpw-v4-basic-demographic-characteristics-rev10">网格化的人口统计学数据</a>. 我这里下载的是以60分为解析度的数据. 打开可以看到</p>
<pre class="prettyprint showlinemods notranslate lang-txt" translate="no"> ncols 360
nrows 145
xllcorner -180
yllcorner -60
cellsize 0.99999999999994
NODATA_value -9999
-9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
-9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
-9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
-9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
-9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
-9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
-9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
-9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
-9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
-9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
-9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
-9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
-9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
9.241768 8.790958 2.095345 -9999 0.05114867 -9999 -9999 -9999 -9999 -999...
1.287993 0.4395509 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999...
-9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
</pre>
<p>上面的数据首先是几行键值对, 然后是网格化的数据. </p>
<p>为了保证我们的理解没有偏差, 我们先做个2D图</p>
<p>先用这么几行代码载入数据</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">async function loadFile(url) {
const res = await fetch(url);
return res.text();
}
</pre>
<p>上面的代码返回了一个带有指定<code class="notranslate" translate="no">url</code>下文件内容的<code class="notranslate" translate="no">Promise</code></p>
<p>然后写几行数据来解析文件内容</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function parseData(text) {
const data = [];
const settings = {data};
let max;
let min;
// 对每一行进行切分
text.split('\n').forEach((line) => {
// split the line by whitespace
const parts = line.trim().split(/\s+/);
if (parts.length === 2) {
// 长度为2的必定是键值对
settings[parts[0]] = parseFloat(parts[1]);
} else if (parts.length > 2) {
// 长度超过2的肯定是网格数据
const values = parts.map((v) => {
const value = parseFloat(v);
if (value === settings.NODATA_value) {
return undefined;
}
max = Math.max(max === undefined ? value : max, value);
min = Math.min(min === undefined ? value : min, value);
return value;
});
data.push(values);
}
});
return Object.assign(settings, {min, max});
}
</pre>
<p>上面的代码返回了一个有着全部键值对的对象, 然后<code class="notranslate" translate="no">data</code>属性是网格化的数据. <code class="notranslate" translate="no">min</code> 和 <code class="notranslate" translate="no">max</code> 中是 <code class="notranslate" translate="no">data</code> 中的极值</p>
<p>下面是绘图函数</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function drawData(file) {
const {min, max, data} = file;
const range = max - min;
const ctx = document.querySelector('canvas').getContext('2d');
// 新建一个和网格数据尺寸相等的canvas
ctx.canvas.width = ncols;
ctx.canvas.height = nrows;
// 但是以两倍大小绘制防止太小
ctx.canvas.style.width = px(ncols * 2);
ctx.canvas.style.height = px(nrows * 2);
// 用黑灰色填充
ctx.fillStyle = '#444';
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// 绘制数据点
data.forEach((row, latNdx) => {
row.forEach((value, lonNdx) => {
if (value === undefined) {
return;
}
const amount = (value - min) / range;
const hue = 1;
const saturation = 1;
const lightness = amount;
ctx.fillStyle = hsl(hue, saturation, lightness);
ctx.fillRect(lonNdx, latNdx, 1, 1);
});
});
}
function px(v) {
return `${v | 0}px`;
}
function hsl(h, s, l) {
return `hsl(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%)`;
}
</pre>
<p>然后把上面的代码都合并起来</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">loadFile('resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc')
.then(parseData)
.then(drawData);
</pre>
<p>得到了下面的结果</p>
<p></p><div translate="no" class="threejs_example_container notranslate">
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/gpw-data-viewer.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/gpw-data-viewer.html" target="_blank">点击此处在新标签页中打开</a>
</div>
<p></p>
<p>嗯... 看起来没什么问题</p>
<p>试试3D效果. 从<a href="rendering-on-demand.html">按需渲染</a>出发, 我们让每一个数据都画成一个box</p>
<p>首先先画一个地球, 这是sphere表面的贴图</p>
<div class="threejs_center"><img src="../examples/resources/images/world.jpg" style="width: 600px"></div>
<p>用这些代码生成地球</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
const loader = new THREE.TextureLoader();
const texture = loader.load('resources/images/world.jpg', render);
const geometry = new THREE.SphereGeometry(1, 64, 32);
const material = new THREE.MeshBasicMaterial({map: texture});
scene.add(new THREE.Mesh(geometry, material));
}
</pre>
<p>看过来, 当材质加载完成后才调用<code class="notranslate" translate="no">render</code>方法. 我们这么做是因为使用了<a href="rendering-on-demand.html">按需渲染</a>中的方法, 而不是连续渲染. 这样我们仅仅需要在材质加载后渲染一遍就好. </p>
<p>然后我们需要修改上面每个数据点画一个点的代码, 改为每个数据点画一个框.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function addBoxes(file) {
const {min, max, data} = file;
const range = max - min;
// 新建一个box geometry
const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
// 沿着z轴缩放
geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(0, 0, 0.5));
// 位置辅助器可以方便地在球面上定位
// 经度辅助器可以在XZ平面的法向旋转
const lonHelper = new THREE.Object3D();
scene.add(lonHelper);
// 纬度辅助器可以在XZ平面旋转
const latHelper = new THREE.Object3D();
lonHelper.add(latHelper);
// 组合起来得到的位置辅助器可以在球面上定位
const positionHelper = new THREE.Object3D();
positionHelper.position.z = 1;
latHelper.add(positionHelper);
const lonFudge = Math.PI * .5;
const latFudge = Math.PI * -0.135;
data.forEach((row, latNdx) => {
row.forEach((value, lonNdx) => {
if (value === undefined) {
return;
}
const amount = (value - min) / range;
const material = new THREE.MeshBasicMaterial();
const hue = THREE.MathUtils.lerp(0.7, 0.3, amount);
const saturation = 1;
const lightness = THREE.MathUtils.lerp(0.1, 1.0, amount);
material.color.setHSL(hue, saturation, lightness);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 调整辅助器使其指向经纬度
lonHelper.rotation.y = THREE.MathUtils.degToRad(lonNdx + file.xllcorner) + lonFudge;
latHelper.rotation.x = THREE.MathUtils.degToRad(latNdx + file.yllcorner) + latFudge;
// 使用world matrix来操作辅助器
positionHelper.updateWorldMatrix(true, false);
mesh.applyMatrix4(positionHelper.matrixWorld);
mesh.scale.set(0.005, 0.005, THREE.MathUtils.lerp(0.01, 0.5, amount));
});
});
}
</pre>
<p>上面的代码直截了当得从2D测试方法中改动过来</p>
<p>我们新建一个长方体, 然后沿着Z轴缩放. 如果我们不这么做, 它就会以中心为参照放大, 使得根部不在球面上. 我们这么做之后, 就可以达到从球面上长出来的效果. </p>
<div class="spread">
<div>
<div data-diagram="scaleCenter" style="height: 250px"></div>
<div class="code">default</div>
</div>
<div>
<div data-diagram="scalePositiveZ" style="height: 250px"></div>
<div class="code">adjusted</div>
</div>
</div>
<p>当然, 我们可以像<a href="scenegraph.html">场景图</a>一章中讲得, 通过添加到一个父对象来解决上面的问题. 但是要考虑到我们体系几何体非常得多, 所以会大大拖累运行的速度. </p>
<p>上面的位置辅助器<code class="notranslate" translate="no">positionHelper</code>是由<code class="notranslate" translate="no">lonHelper</code>, <code class="notranslate" translate="no">latHelper</code>逐级组合而来. 这个小东西可以帮助我们计算球面上的经纬度来放置几何体. </p>
<div class="spread">
<div data-diagram="lonLatPos" style="width: 600px; height: 400px;"></div>
</div>
<p>上面的<span style="color: green;">绿条条</span>代表<code class="notranslate" translate="no">lonHelper</code>, 在赤道上以经度的变化旋转. The <span style="color: blue;">
蓝条条</span>代表 <code class="notranslate" translate="no">latHelper</code>, 在赤道上下以纬度的变化旋转. <span style="color: red;">红球球</span> 就是位置辅助器实际指向的位置. </p>
<p>我们倒是可以计算所有的球面位置, 但是需要涉及到很多数学和库的调用, 所以就...可以但没必要.</p>
<p>每一个数据我们都创建了一个<a href="/docs/#api/zh/materials/MeshBasicMaterial"><code class="notranslate" translate="no">MeshBasicMaterial</code></a>和一个<a href="/docs/#api/zh/objects/Mesh"><code class="notranslate" translate="no">Mesh</code></a>, 然后我们从位置辅助器中取得world matrix并应用到新的<a href="/docs/#api/zh/objects/Mesh"><code class="notranslate" translate="no">Mesh</code></a>上. 最后, 我们在它的新位置上缩放. </p>
<p>上面, 我们给每一个新box都创建了一个位置辅助器, 但是这将会使运行速度大大下降. </p>
<p>这最多有360x145=52000个盒子需要被创建. 有些点数据被标为 “NO_DATA” 所以实际的盒子数大概是19000左右. 如果我们每个盒子加上三个辅助器, 全局就大概80000个节点. 使用一组辅助器来调整mesh的位置我们可以节约60000个节点的计算. </p>
<p>注意<code class="notranslate" translate="no">lonFudge</code>是π/2也就是四分之一圈, 也就是说在在一周上是以不同的偏移开始. 也能说得通. 但是我不知道为什么<code class="notranslate" translate="no">latFudge</code>要乘以个 π * -0.135, 似乎就是一个能让盒子和材质对齐的数.</p>
<p>最后一步是调用loader</p>
<pre class="prettyprint showlinemods notranslate notranslate" translate="no">loadFile('resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc')
.then(parseData)
- .then(drawData)
+ .then(addBoxes)
+ .then(render);
</pre><p>当数据载入和解析完成, 我们再进行渲染</p>
<p></p><div translate="no" class="threejs_example_container notranslate">
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lots-of-objects-slow.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/lots-of-objects-slow.html" target="_blank">点击此处在新标签页中打开</a>
</div>
<p></p>
<p>拖拽一下这个球你就会发现很卡</p>
<p>我们在<a href="debugging-javascript.html">开启调试工具</a>中提到过怎么打开帧率监视器</p>
<div class="threejs_center"><img src="../resources/images/bring-up-fps-meter.gif"></div>
<p>在我机器上大概是20帧每秒</p>
<div class="threejs_center"><img src="../resources/images/fps-meter.gif"></div>
<p>这不太行, 我寻思很多人机器上会更慢. 我们得想办法优化它一下子.</p>
<p>此时此景, 我们可以通过合并所有的盒子到一个geometry来实现, 一下子就可以省下18999个操作</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function addBoxes(file) {
const {min, max, data} = file;
const range = max - min;
- // 新建一个几何体
- const boxWidth = 1;
- const boxHeight = 1;
- const boxDepth = 1;
- const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
- // 沿着Z轴缩放
- geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(0, 0, 0.5));
// 位置辅助器可以方便地在球面上定位
// 经度辅助器可以在XZ平面的法向旋转
const lonHelper = new THREE.Object3D();
scene.add(lonHelper);
// 纬度辅助器可以在XZ平面旋转
const latHelper = new THREE.Object3D();
lonHelper.add(latHelper);
// 组合起来得到的位置辅助器可以在球面上定位
const positionHelper = new THREE.Object3D();
positionHelper.position.z = 1;
latHelper.add(positionHelper);
+ // 用来定位盒子的中心, 以便接下来沿着Z轴缩放
+ const originHelper = new THREE.Object3D();
+ originHelper.position.z = 0.5;
+ positionHelper.add(originHelper);
const lonFudge = Math.PI * .5;
const latFudge = Math.PI * -0.135;
+ const geometries = [];
data.forEach((row, latNdx) => {
row.forEach((value, lonNdx) => {
if (value === undefined) {
return;
}
const amount = (value - min) / range;
- const material = new THREE.MeshBasicMaterial();
- const hue = THREE.MathUtils.lerp(0.7, 0.3, amount);
- const saturation = 1;
- const lightness = THREE.MathUtils.lerp(0.1, 1.0, amount);
- material.color.setHSL(hue, saturation, lightness);
- const mesh = new THREE.Mesh(geometry, material);
- scene.add(mesh);
+ const boxWidth = 1;
+ const boxHeight = 1;
+ const boxDepth = 1;
+ const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
// 调整位置辅助器的指向
lonHelper.rotation.y = THREE.MathUtils.degToRad(lonNdx + file.xllcorner) + lonFudge;
latHelper.rotation.x = THREE.MathUtils.degToRad(latNdx + file.yllcorner) + latFudge;
// 使用world matrix来操作辅助器
positionHelper.updateWorldMatrix(true, false);
mesh.applyMatrix4(positionHelper.matrixWorld);
mesh.scale.set(0.005, 0.005, THREE.MathUtils.lerp(0.01, 0.5, amount));
+ // 使用位置辅助器和world matrix 来定位
+ positionHelper.scale.set(0.005, 0.005, THREE.MathUtils.lerp(0.01, 0.5, amount));
+ originHelper.updateWorldMatrix(true, false);
+ geometry.applyMatrix4(originHelper.matrixWorld);
+
+ geometries.push(geometry);
});
});
+ const mergedGeometry = BufferGeometryUtils.mergeGeometries(
+ geometries, false);
+ const material = new THREE.MeshBasicMaterial({color:'red'});
+ const mesh = new THREE.Mesh(mergedGeometry, material);
+ scene.add(mesh);
}
</pre>
<p>我们移除了之前用来改变盒子几何中心的代码, 取而代之的是<code class="notranslate" translate="no">originHelper</code>. 这次我们要为每个长方体创建新的几何体, 因为我们要使用“applyMatrix”来移动每个长方体几何体的顶点, 所以我们最好只移动一次, 而不是两次.</p>
<p>最后, 我们将所有几何体的数组传入<code class="notranslate" translate="no">BufferGeometryUtils.mergeGeometries</code>, 这个方法将会将其合并到一个mesh中</p>
<p>别忘了引入<code class="notranslate" translate="no">BufferGeometryUtils</code></p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
</pre>
<p>现在, 至少在我的机器上, 可以跑到60帧每秒了</p>
<p></p><div translate="no" class="threejs_example_container notranslate">
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lots-of-objects-merged.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/lots-of-objects-merged.html" target="_blank">点击此处在新标签页中打开</a>
</div>
<p></p>
<p>虽然可以了, 但是我们这是一整个mesh, 所以我们只能应用一个材质, 意味着我们只能有一种颜色的盒子. 我们之前可是能有不同颜色的盒子. 我们可以通过使用顶点着色法来解决. </p>
<p>顶点着色向每个顶点添加一种颜色. 通过设定每个盒子的每个顶点的所有颜色来指定每个盒子的颜色. </p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const color = new THREE.Color();
const lonFudge = Math.PI * .5;
const latFudge = Math.PI * -0.135;
const geometries = [];
data.forEach((row, latNdx) => {
row.forEach((value, lonNdx) => {
if (value === undefined) {
return;
}
const amount = (value - min) / range;
const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
lonHelper.rotation.y = THREE.MathUtils.degToRad(lonNdx + file.xllcorner) + lonFudge;
latHelper.rotation.x = THREE.MathUtils.degToRad(latNdx + file.yllcorner) + latFudge;
positionHelper.scale.set(0.005, 0.005, THREE.MathUtils.lerp(0.01, 0.5, amount));
originHelper.updateWorldMatrix(true, false);
geometry.applyMatrix4(originHelper.matrixWorld);
+ // 计算颜色
+ const hue = THREE.MathUtils.lerp(0.7, 0.3, amount);
+ const saturation = 1;
+ const lightness = THREE.MathUtils.lerp(0.4, 1.0, amount);
+ color.setHSL(hue, saturation, lightness);
+ // 以0到255之间的值数组形式获取颜色
+ const rgb = color.toArray().map(v => v * 255);
+
+ // 创建一个数组来存储每个顶点的颜色
+ const numVerts = geometry.getAttribute('position').count;
+ const itemSize = 3; // r, g, b
+ const colors = new Uint8Array(itemSize * numVerts);
+
+ // 将颜色复制到每个顶点的颜色数组中
+ colors.forEach((v, ndx) => {
+ colors[ndx] = rgb[ndx % 3];
+ });
+
+ const normalized = true;
+ const colorAttrib = new THREE.BufferAttribute(colors, itemSize, normalized);
+ geometry.setAttribute('color', colorAttrib);
geometries.push(geometry);
});
});
</pre>
<p>上面的代码中, 我们查找几何体中的<code class="notranslate" translate="no">position</code>属性来获取所需的数量和顶点. 然后创建一个<code class="notranslate" translate="no">Uint8Array</code>来输入颜色. 接下来通过调用<code class="notranslate" translate="no">geometry.setAttribute</code>来将其设定为一个属性. </p>
<p>最后告诉three.js使用顶点上色. </p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const mergedGeometry = BufferGeometryUtils.mergeGeometries(
geometries, false);
-const material = new THREE.MeshBasicMaterial({color:'red'});
+const material = new THREE.MeshBasicMaterial({
+ vertexColors: true,
+});
const mesh = new THREE.Mesh(mergedGeometry, material);
scene.add(mesh);
</pre>
<p>我们的彩色世界回来啦!</p>
<p></p><div translate="no" class="threejs_example_container notranslate">
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lots-of-objects-merged-vertexcolors.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/lots-of-objects-merged-vertexcolors.html" target="_blank">点击此处在新标签页中打开</a>
</div>
<p></p>
<p>合并几何体是一个常见的优化手段. 比如, 可以将一百多棵树合并成一个几何体, 一堆石头合并成一块石头, 零零碎碎的栅栏合并成一个栅栏的mesh. 另一个在 Minecraft 中的例子是, 它不太可能单独绘制每个立方体, 而是创建一组合并的立方体, 并且选择性地删除那些永远不可见的面. </p>
<p>这么做带来的问题是, 合并起来简单, 分离难. 接下来我们再引入一种优化方案
<a href="optimize-lots-of-objects-animated.html">优化大量动画对象</a>.</p>
<p><canvas id="c"></canvas></p>
<script type="module" src="../resources/threejs-lots-of-objects.js"></script>
</div>
</div>
</div>
<script src="../resources/prettify.js"></script>
<script src="../resources/lesson.js"></script>
</body></html>