You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
516 lines
25 KiB
516 lines
25 KiB
2 years ago
|
<!DOCTYPE html><html lang="en"><head>
|
||
|
<meta charset="utf-8">
|
||
|
<title>Optimize Lots of Objects</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 – Optimize Lots of Objects">
|
||
|
<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="/manual/resources/lesson.css">
|
||
|
<link rel="stylesheet" href="/manual/resources/lang.css">
|
||
|
<!-- Import maps polyfill -->
|
||
|
<!-- Remove this when import maps will be widely supported -->
|
||
|
<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
|
||
|
|
||
|
<script type="importmap">
|
||
|
{
|
||
|
"imports": {
|
||
|
"three": "../../build/three.module.js"
|
||
|
}
|
||
|
}
|
||
|
</script>
|
||
|
</head>
|
||
|
<body>
|
||
|
<div class="container">
|
||
|
<div class="lesson-title">
|
||
|
<h1>Optimize Lots of Objects</h1>
|
||
|
</div>
|
||
|
<div class="lesson">
|
||
|
<div class="lesson-main">
|
||
|
<p>This article is part of a series of articles about three.js. The first article
|
||
|
is <a href="fundamentals.html">three.js fundamentals</a>. If you haven't read that
|
||
|
yet and you're new to three.js you might want to consider starting there. </p>
|
||
|
<p>There are many ways to optimize things for three.js. One way is often referred
|
||
|
to as <em>merging geometry</em>. Every <a href="/docs/#api/en/objects/Mesh"><code class="notranslate" translate="no">Mesh</code></a> you create and three.js represents 1 or
|
||
|
more requests by the system to draw something. Drawing 2 things has more
|
||
|
overhead than drawing 1 even if the results are the same so one way to optimize
|
||
|
is to merge meshes.</p>
|
||
|
<p>Let's show an example of when this is a good solution for an issue. Let's
|
||
|
re-create the <a href="https://globe.chromeexperiments.com/">WebGL Globe</a>.</p>
|
||
|
<p>The first thing we need to do is get some data. The WebGL Globe said the data
|
||
|
they use comes from <a href="http://sedac.ciesin.columbia.edu/gpw/">SEDAC</a>. Checking out
|
||
|
the site I saw there was <a href="https://beta.sedac.ciesin.columbia.edu/data/set/gpw-v4-basic-demographic-characteristics-rev10">demographic data in a grid
|
||
|
format</a>.
|
||
|
I downloaded the data at 60 minute resolution. Then I took a look at the data</p>
|
||
|
<p>It looks like this</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>There's a few lines that are like key/value pairs followed by lines with a value
|
||
|
per grid point, one line for each row of data points.</p>
|
||
|
<p>To make sure we understand the data let's try to plot it in 2D.</p>
|
||
|
<p>First some code to load the text file</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">async function loadFile(url) {
|
||
|
const res = await fetch(url);
|
||
|
return res.text();
|
||
|
}
|
||
|
</pre>
|
||
|
<p>The code above returns a <code class="notranslate" translate="no">Promise</code> with the contents of the file at <code class="notranslate" translate="no">url</code>;</p>
|
||
|
<p>Then we need some code to parse the file</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function parseData(text) {
|
||
|
const data = [];
|
||
|
const settings = {data};
|
||
|
let max;
|
||
|
let min;
|
||
|
// split into lines
|
||
|
text.split('\n').forEach((line) => {
|
||
|
// split the line by whitespace
|
||
|
const parts = line.trim().split(/\s+/);
|
||
|
if (parts.length === 2) {
|
||
|
// only 2 parts, must be a key/value pair
|
||
|
settings[parts[0]] = parseFloat(parts[1]);
|
||
|
} else if (parts.length > 2) {
|
||
|
// more than 2 parts, must be data
|
||
|
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>The code above returns an object with all the key/value pairs from the file as
|
||
|
well as a <code class="notranslate" translate="no">data</code> property with all the data in one large array and the <code class="notranslate" translate="no">min</code> and
|
||
|
<code class="notranslate" translate="no">max</code> values found in the data.</p>
|
||
|
<p>Then we need some code to draw that data</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');
|
||
|
// make the canvas the same size as the data
|
||
|
ctx.canvas.width = ncols;
|
||
|
ctx.canvas.height = nrows;
|
||
|
// but display it double size so it's not too small
|
||
|
ctx.canvas.style.width = px(ncols * 2);
|
||
|
ctx.canvas.style.height = px(nrows * 2);
|
||
|
// fill the canvas to dark gray
|
||
|
ctx.fillStyle = '#444';
|
||
|
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||
|
// draw each data point
|
||
|
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>And finally gluing it all together</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>Gives us this result</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">click here to open in a separate window</a>
|
||
|
</div>
|
||
|
|
||
|
<p></p>
|
||
|
<p>So that seems to work. </p>
|
||
|
<p>Let's try it in 3D. Starting with the code from <a href="rendering-on-demand.html">rendering on
|
||
|
demand</a> We'll make one box per data in the
|
||
|
file.</p>
|
||
|
<p>First let's make a simple sphere with a texture of the world. Here's the texture</p>
|
||
|
<div class="threejs_center"><img src="../examples/resources/images/world.jpg" style="width: 600px"></div>
|
||
|
|
||
|
<p>And the code to set it up.</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>Notice the call to <code class="notranslate" translate="no">render</code> when the texture has finished loading. We need this
|
||
|
because we're <a href="rendering-on-demand.html">rendering on demand</a> instead of
|
||
|
continuously so we need to render once when the texture is loaded.</p>
|
||
|
<p>Then we need to change the code that drew a dot per data point above to instead
|
||
|
make a box per data point.</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function addBoxes(file) {
|
||
|
const {min, max, data} = file;
|
||
|
const range = max - min;
|
||
|
|
||
|
// make one box geometry
|
||
|
const boxWidth = 1;
|
||
|
const boxHeight = 1;
|
||
|
const boxDepth = 1;
|
||
|
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
|
||
|
// make it so it scales away from the positive Z axis
|
||
|
geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(0, 0, 0.5));
|
||
|
|
||
|
// these helpers will make it easy to position the boxes
|
||
|
// We can rotate the lon helper on its Y axis to the longitude
|
||
|
const lonHelper = new THREE.Object3D();
|
||
|
scene.add(lonHelper);
|
||
|
// We rotate the latHelper on its X axis to the latitude
|
||
|
const latHelper = new THREE.Object3D();
|
||
|
lonHelper.add(latHelper);
|
||
|
// The position helper moves the object to the edge of the sphere
|
||
|
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);
|
||
|
|
||
|
// adjust the helpers to point to the latitude and longitude
|
||
|
lonHelper.rotation.y = THREE.MathUtils.degToRad(lonNdx + file.xllcorner) + lonFudge;
|
||
|
latHelper.rotation.x = THREE.MathUtils.degToRad(latNdx + file.yllcorner) + latFudge;
|
||
|
|
||
|
// use the world matrix of the position helper to
|
||
|
// position this mesh.
|
||
|
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>The code is mostly straight forward from our test drawing code. </p>
|
||
|
<p>We make one box and adjust its center so it scales away from positive Z. If we
|
||
|
didn't do this it would scale from the center but we want them to grow away from the origin.</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>Of course we could also solve that by parenting the box to more <a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">THREE.Object3D</code></a>
|
||
|
objects like we covered in <a href="scenegraph.html">scene graphs</a> but the more
|
||
|
nodes we add to a scene graph the slower it gets.</p>
|
||
|
<p>We also setup this small hierarchy of nodes of <code class="notranslate" translate="no">lonHelper</code>, <code class="notranslate" translate="no">latHelper</code>, and
|
||
|
<code class="notranslate" translate="no">positionHelper</code>. We use these objects to compute a position around the sphere
|
||
|
were to place the box. </p>
|
||
|
<div class="spread">
|
||
|
<div data-diagram="lonLatPos" style="width: 600px; height: 400px;"></div>
|
||
|
</div>
|
||
|
|
||
|
<p>Above the <span style="color: green;">green bar</span> represents <code class="notranslate" translate="no">lonHelper</code> and
|
||
|
is used to rotate toward longitude on the equator. The <span style="color: blue;">
|
||
|
blue bar</span> represents <code class="notranslate" translate="no">latHelper</code> which is used to rotate to a
|
||
|
latitude above or below the equator. The <span style="color: red;">red
|
||
|
sphere</span> represents the offset that that <code class="notranslate" translate="no">positionHelper</code> provides.</p>
|
||
|
<p>We could do all of the math manually to figure out positions on the globe but
|
||
|
doing it this way leaves most of the math to the library itself so we don't need
|
||
|
to deal with.</p>
|
||
|
<p>For each data point we create a <a href="/docs/#api/en/materials/MeshBasicMaterial"><code class="notranslate" translate="no">MeshBasicMaterial</code></a> and a <a href="/docs/#api/en/objects/Mesh"><code class="notranslate" translate="no">Mesh</code></a> and then we ask
|
||
|
for the world matrix of the <code class="notranslate" translate="no">positionHelper</code> and apply that to the new <a href="/docs/#api/en/objects/Mesh"><code class="notranslate" translate="no">Mesh</code></a>.
|
||
|
Finally we scale the mesh at its new position.</p>
|
||
|
<p>Like above, we could also have created a <code class="notranslate" translate="no">latHelper</code>, <code class="notranslate" translate="no">lonHelper</code>, and
|
||
|
<code class="notranslate" translate="no">positionHelper</code> for every new box but that would be even slower.</p>
|
||
|
<p>There are up to 360x145 boxes we're going to create. That's up to 52000 boxes.
|
||
|
Because some data points are marked as "NO_DATA" the actual number of boxes
|
||
|
we're going to create is around 19000. If we added 3 extra helper objects per
|
||
|
box that would be nearly 80000 scene graph nodes that THREE.js would have to
|
||
|
compute positions for. By instead using one set of helpers to just position the
|
||
|
meshes we save around 60000 operations.</p>
|
||
|
<p>A note about <code class="notranslate" translate="no">lonFudge</code> and <code class="notranslate" translate="no">latFudge</code>. <code class="notranslate" translate="no">lonFudge</code> is π/2 which is a quarter of a turn.
|
||
|
That makes sense. It just means the texture or texture coordinates start at a
|
||
|
different offset around the globe. <code class="notranslate" translate="no">latFudge</code> on the other hand I have no idea
|
||
|
why it needs to be π * -0.135, that's just an amount that made the boxes line up
|
||
|
with the texture.</p>
|
||
|
<p>The last thing we need to do is call our 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>Once the data has finished loading and parsing then we need to render at least
|
||
|
once since we're <a href="rendering-on-demand.html">rendering on demand</a>.</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">click here to open in a separate window</a>
|
||
|
</div>
|
||
|
|
||
|
<p></p>
|
||
|
<p>If you try to rotate the example above by dragging on the sample you'll likely
|
||
|
notice it's slow.</p>
|
||
|
<p>We can check the framerate by <a href="debugging-javascript.html">opening the
|
||
|
devtools</a> and turning on the browser's frame
|
||
|
rate meter.</p>
|
||
|
<div class="threejs_center"><img src="../resources/images/bring-up-fps-meter.gif"></div>
|
||
|
|
||
|
<p>On my machine I see a framerate under 20fps.</p>
|
||
|
<div class="threejs_center"><img src="../resources/images/fps-meter.gif"></div>
|
||
|
|
||
|
<p>That doesn't feel very good to me and I suspect many people have slower machines
|
||
|
which would make it even worse. We'd better look into optimizing.</p>
|
||
|
<p>For this particular problem we can merge all the boxes into a single geometry.
|
||
|
We're currently drawing around 19000 boxes. By merging them into a single
|
||
|
geometry we'd remove 18999 operations.</p>
|
||
|
<p>Here's the new code to merge the boxes into a single geometry.</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function addBoxes(file) {
|
||
|
const {min, max, data} = file;
|
||
|
const range = max - min;
|
||
|
|
||
|
- // make one box geometry
|
||
|
- const boxWidth = 1;
|
||
|
- const boxHeight = 1;
|
||
|
- const boxDepth = 1;
|
||
|
- const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
|
||
|
- // make it so it scales away from the positive Z axis
|
||
|
- geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(0, 0, 0.5));
|
||
|
|
||
|
// these helpers will make it easy to position the boxes
|
||
|
// We can rotate the lon helper on its Y axis to the longitude
|
||
|
const lonHelper = new THREE.Object3D();
|
||
|
scene.add(lonHelper);
|
||
|
// We rotate the latHelper on its X axis to the latitude
|
||
|
const latHelper = new THREE.Object3D();
|
||
|
lonHelper.add(latHelper);
|
||
|
// The position helper moves the object to the edge of the sphere
|
||
|
const positionHelper = new THREE.Object3D();
|
||
|
positionHelper.position.z = 1;
|
||
|
latHelper.add(positionHelper);
|
||
|
+ // Used to move the center of the box so it scales from the position Z axis
|
||
|
+ 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);
|
||
|
|
||
|
// adjust the helpers to point to the latitude and longitude
|
||
|
lonHelper.rotation.y = THREE.MathUtils.degToRad(lonNdx + file.xllcorner) + lonFudge;
|
||
|
latHelper.rotation.x = THREE.MathUtils.degToRad(latNdx + file.yllcorner) + latFudge;
|
||
|
|
||
|
- // use the world matrix of the position helper to
|
||
|
- // position this mesh.
|
||
|
- positionHelper.updateWorldMatrix(true, false);
|
||
|
- mesh.applyMatrix4(positionHelper.matrixWorld);
|
||
|
-
|
||
|
- mesh.scale.set(0.005, 0.005, THREE.MathUtils.lerp(0.01, 0.5, amount));
|
||
|
|
||
|
+ // use the world matrix of the origin helper to
|
||
|
+ // position this geometry
|
||
|
+ 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.mergeBufferGeometries(
|
||
|
+ geometries, false);
|
||
|
+ const material = new THREE.MeshBasicMaterial({color:'red'});
|
||
|
+ const mesh = new THREE.Mesh(mergedGeometry, material);
|
||
|
+ scene.add(mesh);
|
||
|
|
||
|
}
|
||
|
</pre>
|
||
|
<p>Above we removed the code that was changing the box geometry's center point and
|
||
|
are instead doing it by adding an <code class="notranslate" translate="no">originHelper</code>. Before we were using the same
|
||
|
geometry 19000 times. This time we are creating new geometry for every single
|
||
|
box and since we are going to use <code class="notranslate" translate="no">applyMatrix</code> to move the vertices of each box
|
||
|
geometry we might as well do it once instead of twice.</p>
|
||
|
<p>At the end we pass an array of all the geometries to
|
||
|
<code class="notranslate" translate="no">BufferGeometryUtils.mergeBufferGeometries</code> which will combined all of
|
||
|
them into a single mesh.</p>
|
||
|
<p>We also need to include the <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>And now, at least on my machine, I get 60 frames per second</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">click here to open in a separate window</a>
|
||
|
</div>
|
||
|
|
||
|
<p></p>
|
||
|
<p>So that worked but because it's one mesh we only get one material which means we
|
||
|
only get one color where as before we had a different color on each box. We can
|
||
|
fix that by using vertex colors.</p>
|
||
|
<p>Vertex colors add a color per vertex. By setting all the colors of each vertex
|
||
|
of each box to specific colors every box will have a different color.</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);
|
||
|
|
||
|
// adjust the helpers to point to the latitude and longitude
|
||
|
lonHelper.rotation.y = THREE.MathUtils.degToRad(lonNdx + file.xllcorner) + lonFudge;
|
||
|
latHelper.rotation.x = THREE.MathUtils.degToRad(latNdx + file.yllcorner) + latFudge;
|
||
|
|
||
|
// use the world matrix of the origin helper to
|
||
|
// position this geometry
|
||
|
positionHelper.scale.set(0.005, 0.005, THREE.MathUtils.lerp(0.01, 0.5, amount));
|
||
|
originHelper.updateWorldMatrix(true, false);
|
||
|
geometry.applyMatrix4(originHelper.matrixWorld);
|
||
|
|
||
|
+ // compute a color
|
||
|
+ 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);
|
||
|
+ // get the colors as an array of values from 0 to 255
|
||
|
+ const rgb = color.toArray().map(v => v * 255);
|
||
|
+
|
||
|
+ // make an array to store colors for each vertex
|
||
|
+ const numVerts = geometry.getAttribute('position').count;
|
||
|
+ const itemSize = 3; // r, g, b
|
||
|
+ const colors = new Uint8Array(itemSize * numVerts);
|
||
|
+
|
||
|
+ // copy the color into the colors array for each vertex
|
||
|
+ 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>The code above looks up the number or vertices needed by getting the <code class="notranslate" translate="no">position</code>
|
||
|
attribute from the geometry. We then create a <code class="notranslate" translate="no">Uint8Array</code> to put the colors in.
|
||
|
It then adds that as an attribute by calling <code class="notranslate" translate="no">geometry.setAttribute</code>.</p>
|
||
|
<p>Lastly we need to tell three.js to use the vertex colors. </p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(
|
||
|
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>And with that we get our colors back</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">click here to open in a separate window</a>
|
||
|
</div>
|
||
|
|
||
|
<p></p>
|
||
|
<p>Merging geometry is a common optimization technique. For example rather than
|
||
|
100 trees you might merge the trees into 1 geometry, a pile of individual rocks
|
||
|
into a single geometry of rocks, a picket fence from individual pickets into
|
||
|
one fence mesh. Another example in Minecraft it doesn't likely draw each cube
|
||
|
individually but rather creates groups of merged cubes and also selectively removing
|
||
|
faces that are never visible.</p>
|
||
|
<p>The problem with making everything one mesh though is it's no longer easy
|
||
|
to move any part that was previously separate. Depending on our use case
|
||
|
though there are creative solutions. We'll explore one in
|
||
|
<a href="optimize-lots-of-objects-animated.html">another article</a>.</p>
|
||
|
<p><canvas id="c"></canvas></p>
|
||
|
<script type="module" src="../resources/threejs-lots-of-objects.js"></script>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<script src="/manual/resources/prettify.js"></script>
|
||
|
<script src="/manual/resources/lesson.js"></script>
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
</body></html>
|