optimize-lots-of-objects-animated.html 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692
  1. <!DOCTYPE html><html lang="en"><head>
  2. <meta charset="utf-8">
  3. <title>Optimize Lots of Objects Animated</title>
  4. <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  5. <meta name="twitter:card" content="summary_large_image">
  6. <meta name="twitter:site" content="@threejs">
  7. <meta name="twitter:title" content="Three.js – Optimize Lots of Objects Animated">
  8. <meta property="og:image" content="https://threejs.org/files/share.png">
  9. <link rel="shortcut icon" href="/files/favicon_white.ico" media="(prefers-color-scheme: dark)">
  10. <link rel="shortcut icon" href="/files/favicon.ico" media="(prefers-color-scheme: light)">
  11. <link rel="stylesheet" href="/manual/resources/lesson.css">
  12. <link rel="stylesheet" href="/manual/resources/lang.css">
  13. <!-- Import maps polyfill -->
  14. <!-- Remove this when import maps will be widely supported -->
  15. <script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
  16. <script type="importmap">
  17. {
  18. "imports": {
  19. "three": "../../build/three.module.js"
  20. }
  21. }
  22. </script>
  23. </head>
  24. <body>
  25. <div class="container">
  26. <div class="lesson-title">
  27. <h1>Optimize Lots of Objects Animated</h1>
  28. </div>
  29. <div class="lesson">
  30. <div class="lesson-main">
  31. <p>This article is a continuation of <a href="optimize-lots-of-objects.html">an article about optimizing lots of objects
  32. </a>. If you haven't read that
  33. yet please read it before proceeding. </p>
  34. <p>In the previous article we merged around 19000 cubes into a
  35. single geometry. This had the advantage that it optimized our drawing
  36. of 19000 cubes but it had the disadvantage of make it harder to
  37. move any individual cube.</p>
  38. <p>Depending on what we are trying to accomplish there are different solutions.
  39. In this case let's graph multiple sets of data and animate between the sets.</p>
  40. <p>The first thing we need to do is get multiple sets of data. Ideally we'd
  41. probably pre-process data offline but in this case let's load 2 sets of
  42. data and generate 2 more</p>
  43. <p>Here's our old loading code</p>
  44. <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')
  45. .then(parseData)
  46. .then(addBoxes)
  47. .then(render);
  48. </pre>
  49. <p>Let's change it to something like this</p>
  50. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">async function loadData(info) {
  51. const text = await loadFile(info.url);
  52. info.file = parseData(text);
  53. }
  54. async function loadAll() {
  55. const fileInfos = [
  56. {name: 'men', hueRange: [0.7, 0.3], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc' },
  57. {name: 'women', hueRange: [0.9, 1.1], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014ft_2010_cntm_1_deg.asc' },
  58. ];
  59. await Promise.all(fileInfos.map(loadData));
  60. ...
  61. }
  62. loadAll();
  63. </pre>
  64. <p>The code above will load all the files in <code class="notranslate" translate="no">fileInfos</code> and when done each object
  65. in <code class="notranslate" translate="no">fileInfos</code> will have a <code class="notranslate" translate="no">file</code> property with the loaded file. <code class="notranslate" translate="no">name</code> and <code class="notranslate" translate="no">hueRange</code>
  66. we'll use later. <code class="notranslate" translate="no">name</code> will be for a UI field. <code class="notranslate" translate="no">hueRange</code> will be used to
  67. choose a range of hues to map over.</p>
  68. <p>The two files above are apparently the number of men per area and the number of
  69. women per area as of 2010. Note, I have no idea if this data is correct but
  70. it's not important really. The important part is showing different sets
  71. of data.</p>
  72. <p>Let's generate 2 more sets of data. One being the places where the number
  73. men are greater than the number of women and visa versa, the places where
  74. the number of women are greater than the number of men. </p>
  75. <p>The first thing let's write a function that given a 2 dimensional array of
  76. of arrays like we had before will map over it to generate a new 2 dimensional
  77. array of arrays</p>
  78. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function mapValues(data, fn) {
  79. return data.map((row, rowNdx) =&gt; {
  80. return row.map((value, colNdx) =&gt; {
  81. return fn(value, rowNdx, colNdx);
  82. });
  83. });
  84. }
  85. </pre>
  86. <p>Like the normal <code class="notranslate" translate="no">Array.map</code> function the <code class="notranslate" translate="no">mapValues</code> function calls a function
  87. <code class="notranslate" translate="no">fn</code> for each value in the array of arrays. It passes it the value and both the
  88. row and column indices.</p>
  89. <p>Now let's make some code to generate a new file that is a comparison between 2
  90. files</p>
  91. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function makeDiffFile(baseFile, otherFile, compareFn) {
  92. let min;
  93. let max;
  94. const baseData = baseFile.data;
  95. const otherData = otherFile.data;
  96. const data = mapValues(baseData, (base, rowNdx, colNdx) =&gt; {
  97. const other = otherData[rowNdx][colNdx];
  98. if (base === undefined || other === undefined) {
  99. return undefined;
  100. }
  101. const value = compareFn(base, other);
  102. min = Math.min(min === undefined ? value : min, value);
  103. max = Math.max(max === undefined ? value : max, value);
  104. return value;
  105. });
  106. // make a copy of baseFile and replace min, max, and data
  107. // with the new data
  108. return {...baseFile, min, max, data};
  109. }
  110. </pre>
  111. <p>The code above uses <code class="notranslate" translate="no">mapValues</code> to generate a new set of data that is
  112. a comparison based on the <code class="notranslate" translate="no">compareFn</code> function passed in. It also tracks
  113. the <code class="notranslate" translate="no">min</code> and <code class="notranslate" translate="no">max</code> comparison results. Finally it makes a new file with
  114. all the same properties as <code class="notranslate" translate="no">baseFile</code> except with a new <code class="notranslate" translate="no">min</code>, <code class="notranslate" translate="no">max</code> and <code class="notranslate" translate="no">data</code>.</p>
  115. <p>Then let's use that to make 2 new sets of data</p>
  116. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  117. const menInfo = fileInfos[0];
  118. const womenInfo = fileInfos[1];
  119. const menFile = menInfo.file;
  120. const womenFile = womenInfo.file;
  121. function amountGreaterThan(a, b) {
  122. return Math.max(a - b, 0);
  123. }
  124. fileInfos.push({
  125. name: '&gt;50%men',
  126. hueRange: [0.6, 1.1],
  127. file: makeDiffFile(menFile, womenFile, (men, women) =&gt; {
  128. return amountGreaterThan(men, women);
  129. }),
  130. });
  131. fileInfos.push({
  132. name: '&gt;50% women',
  133. hueRange: [0.0, 0.4],
  134. file: makeDiffFile(womenFile, menFile, (women, men) =&gt; {
  135. return amountGreaterThan(women, men);
  136. }),
  137. });
  138. }
  139. </pre>
  140. <p>Now let's generate a UI to select between these sets of data. First we need
  141. some UI html</p>
  142. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;body&gt;
  143. &lt;canvas id="c"&gt;&lt;/canvas&gt;
  144. + &lt;div id="ui"&gt;&lt;/div&gt;
  145. &lt;/body&gt;
  146. </pre>
  147. <p>and some CSS to make it appear in the top left area</p>
  148. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#ui {
  149. position: absolute;
  150. left: 1em;
  151. top: 1em;
  152. }
  153. #ui&gt;div {
  154. font-size: 20pt;
  155. padding: 1em;
  156. display: inline-block;
  157. }
  158. #ui&gt;div.selected {
  159. color: red;
  160. }
  161. </pre>
  162. <p>Then we can go over each file and generate a set of merged boxes per
  163. set of data and an element which when hovered over will show that set
  164. and hide all others.</p>
  165. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">// show the selected data, hide the rest
  166. function showFileInfo(fileInfos, fileInfo) {
  167. fileInfos.forEach((info) =&gt; {
  168. const visible = fileInfo === info;
  169. info.root.visible = visible;
  170. info.elem.className = visible ? 'selected' : '';
  171. });
  172. requestRenderIfNotRequested();
  173. }
  174. const uiElem = document.querySelector('#ui');
  175. fileInfos.forEach((info) =&gt; {
  176. const boxes = addBoxes(info.file, info.hueRange);
  177. info.root = boxes;
  178. const div = document.createElement('div');
  179. info.elem = div;
  180. div.textContent = info.name;
  181. uiElem.appendChild(div);
  182. div.addEventListener('mouseover', () =&gt; {
  183. showFileInfo(fileInfos, info);
  184. });
  185. });
  186. // show the first set of data
  187. showFileInfo(fileInfos, fileInfos[0]);
  188. </pre>
  189. <p>The one more change we need from the previous example is we need to make
  190. <code class="notranslate" translate="no">addBoxes</code> take a <code class="notranslate" translate="no">hueRange</code></p>
  191. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function addBoxes(file) {
  192. +function addBoxes(file, hueRange) {
  193. ...
  194. // compute a color
  195. - const hue = THREE.MathUtils.lerp(0.7, 0.3, amount);
  196. + const hue = THREE.MathUtils.lerp(...hueRange, amount);
  197. ...
  198. </pre>
  199. <p>and with that we should be able to show 4 sets of data. Hover the mouse over the labels
  200. or touch them to switch sets</p>
  201. <p></p><div translate="no" class="threejs_example_container notranslate">
  202. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lots-of-objects-multiple-data-sets.html"></iframe></div>
  203. <a class="threejs_center" href="/manual/examples/lots-of-objects-multiple-data-sets.html" target="_blank">click here to open in a separate window</a>
  204. </div>
  205. <p></p>
  206. <p>Note, there are a few strange data points that really stick out. I wonder what's up
  207. with those!??! In any case how do we animate between these 4 sets of data.</p>
  208. <p>Lots of ideas.</p>
  209. <ul>
  210. <li><p>Just fade between them using <a href="/docs/#api/en/materials/Material.opacity"><code class="notranslate" translate="no">Material.opacity</code></a></p>
  211. <p>The problem with this solution is the cubes perfectly overlap which
  212. means there will be z-fighting issues. It's possible we could fix
  213. that by changing the depth function and using blending. We should
  214. probably look into it.</p>
  215. </li>
  216. <li><p>Scale up the set we want to see and scale down the other sets</p>
  217. <p>Because all the boxes have their origin at the center of the planet
  218. if we scale them below 1.0 they will sink into the planet. At first that
  219. sounds like a good idea but the issue is all the low height boxes
  220. will disappear almost immediately and not be replaced until the new
  221. data set scales up to 1.0. This makes the transition not very pleasant.
  222. We could maybe fix that with a fancy custom shader.</p>
  223. </li>
  224. <li><p>Use Morphtargets</p>
  225. <p>Morphtargets are a way were we supply multiple values for each vertex
  226. in the geometry and <em>morph</em> or lerp (linear interpolate) between them.
  227. Morphtargets are most commonly used for facial animation of 3D characters
  228. but that's not their only use.</p>
  229. </li>
  230. </ul>
  231. <p>Let's try morphtargets.</p>
  232. <p>We'll still make a geometry for each set of data but we'll then extract
  233. the <code class="notranslate" translate="no">position</code> attribute from each one and use them as morphtargets.</p>
  234. <p>First let's change <code class="notranslate" translate="no">addBoxes</code> to just make and return the merged geometry.</p>
  235. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function addBoxes(file, hueRange) {
  236. +function makeBoxes(file, hueRange) {
  237. const {min, max, data} = file;
  238. const range = max - min;
  239. ...
  240. - const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(
  241. - geometries, false);
  242. - const material = new THREE.MeshBasicMaterial({
  243. - vertexColors: true,
  244. - });
  245. - const mesh = new THREE.Mesh(mergedGeometry, material);
  246. - scene.add(mesh);
  247. - return mesh;
  248. + return BufferGeometryUtils.mergeBufferGeometries(
  249. + geometries, false);
  250. }
  251. </pre>
  252. <p>There's one more thing we need to do here though. Morphtargets are required to
  253. all have exactly the same number of vertices. Vertex #123 in one target needs
  254. have a corresponding Vertex #123 in all other targets. But, as it is now
  255. different data sets might have some data points with no data so no box will be
  256. generated for that point which would mean no corresponding vertices for another
  257. set. So, we need to check across all data sets and either always generate
  258. something if there is data in any set or, generate nothing if there is data
  259. missing in any set. Let's do the latter.</p>
  260. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function dataMissingInAnySet(fileInfos, latNdx, lonNdx) {
  261. + for (const fileInfo of fileInfos) {
  262. + if (fileInfo.file.data[latNdx][lonNdx] === undefined) {
  263. + return true;
  264. + }
  265. + }
  266. + return false;
  267. +}
  268. -function makeBoxes(file, hueRange) {
  269. +function makeBoxes(file, hueRange, fileInfos) {
  270. const {min, max, data} = file;
  271. const range = max - min;
  272. ...
  273. const geometries = [];
  274. data.forEach((row, latNdx) =&gt; {
  275. row.forEach((value, lonNdx) =&gt; {
  276. + if (dataMissingInAnySet(fileInfos, latNdx, lonNdx)) {
  277. + return;
  278. + }
  279. const amount = (value - min) / range;
  280. ...
  281. </pre>
  282. <p>Now we'll change the code that was calling <code class="notranslate" translate="no">addBoxes</code> to use <code class="notranslate" translate="no">makeBoxes</code>
  283. and setup morphtargets</p>
  284. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+// make geometry for each data set
  285. +const geometries = fileInfos.map((info) =&gt; {
  286. + return makeBoxes(info.file, info.hueRange, fileInfos);
  287. +});
  288. +
  289. +// use the first geometry as the base
  290. +// and add all the geometries as morphtargets
  291. +const baseGeometry = geometries[0];
  292. +baseGeometry.morphAttributes.position = geometries.map((geometry, ndx) =&gt; {
  293. + const attribute = geometry.getAttribute('position');
  294. + const name = `target${ndx}`;
  295. + attribute.name = name;
  296. + return attribute;
  297. +});
  298. +const material = new THREE.MeshBasicMaterial({
  299. + vertexColors: true,
  300. +});
  301. +const mesh = new THREE.Mesh(baseGeometry, material);
  302. +scene.add(mesh);
  303. const uiElem = document.querySelector('#ui');
  304. fileInfos.forEach((info) =&gt; {
  305. - const boxes = addBoxes(info.file, info.hueRange);
  306. - info.root = boxes;
  307. const div = document.createElement('div');
  308. info.elem = div;
  309. div.textContent = info.name;
  310. uiElem.appendChild(div);
  311. function show() {
  312. showFileInfo(fileInfos, info);
  313. }
  314. div.addEventListener('mouseover', show);
  315. div.addEventListener('touchstart', show);
  316. });
  317. // show the first set of data
  318. showFileInfo(fileInfos, fileInfos[0]);
  319. </pre>
  320. <p>Above we make geometry for each data set, use the first one as the base,
  321. then get a <code class="notranslate" translate="no">position</code> attribute from each geometry and add it as
  322. a morphtarget to the base geometry for <code class="notranslate" translate="no">position</code>.</p>
  323. <p>Now we need to change how we're showing and hiding the various data sets.
  324. Instead of showing or hiding a mesh we need to change the influence of the
  325. morphtargets. For the data set we want to see we need to have an influence of 1
  326. and for all the ones we don't want to see to we need to have an influence of 0.</p>
  327. <p>We could just set them to 0 or 1 directly but if we did that we wouldn't see any
  328. animation, it would just snap which would be no different than what we already
  329. have. We could also write some custom animation code which would be easy but
  330. because the original webgl globe uses
  331. <a href="https://github.com/tweenjs/tween.js/">an animation library</a> let's use the same one here.</p>
  332. <p>We need to include the library</p>
  333. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from '/build/three.module.js';
  334. import * as BufferGeometryUtils from '/examples/jsm/utils/BufferGeometryUtils.js';
  335. import {OrbitControls} from '/examples/jsm/controls/OrbitControls.js';
  336. +import {TWEEN} from '/examples/jsm/libs/tween.min.js';
  337. </pre>
  338. <p>And then create a <code class="notranslate" translate="no">Tween</code> to animate the influences.</p>
  339. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">// show the selected data, hide the rest
  340. function showFileInfo(fileInfos, fileInfo) {
  341. + const targets = {};
  342. - fileInfos.forEach((info) =&gt; {
  343. + fileInfos.forEach((info, i) =&gt; {
  344. const visible = fileInfo === info;
  345. - info.root.visible = visible;
  346. info.elem.className = visible ? 'selected' : '';
  347. + targets[i] = visible ? 1 : 0;
  348. });
  349. + const durationInMs = 1000;
  350. + new TWEEN.Tween(mesh.morphTargetInfluences)
  351. + .to(targets, durationInMs)
  352. + .start();
  353. requestRenderIfNotRequested();
  354. }
  355. </pre>
  356. <p>We're also suppose to call <code class="notranslate" translate="no">TWEEN.update</code> every frame inside our render loop
  357. but that points out a problem. "tween.js" is designed for continuous rendering
  358. but we are <a href="rendering-on-demand.html">rendering on demand</a>. We could
  359. switch to continuous rendering but it's sometimes nice to only render on demand
  360. as it well stop using the user's power when nothing is happening
  361. so let's see if we can make it animate on demand.</p>
  362. <p>We'll make a <code class="notranslate" translate="no">TweenManager</code> to help. We'll use it to create the <code class="notranslate" translate="no">Tween</code>s and
  363. track them. It will have an <code class="notranslate" translate="no">update</code> method that will return <code class="notranslate" translate="no">true</code>
  364. if we need to call it again and <code class="notranslate" translate="no">false</code> if all the animations are finished.</p>
  365. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class TweenManger {
  366. constructor() {
  367. this.numTweensRunning = 0;
  368. }
  369. _handleComplete() {
  370. --this.numTweensRunning;
  371. console.assert(this.numTweensRunning &gt;= 0);
  372. }
  373. createTween(targetObject) {
  374. const self = this;
  375. ++this.numTweensRunning;
  376. let userCompleteFn = () =&gt; {};
  377. // create a new tween and install our own onComplete callback
  378. const tween = new TWEEN.Tween(targetObject).onComplete(function(...args) {
  379. self._handleComplete();
  380. userCompleteFn.call(this, ...args);
  381. });
  382. // replace the tween's onComplete function with our own
  383. // so we can call the user's callback if they supply one.
  384. tween.onComplete = (fn) =&gt; {
  385. userCompleteFn = fn;
  386. return tween;
  387. };
  388. return tween;
  389. }
  390. update() {
  391. TWEEN.update();
  392. return this.numTweensRunning &gt; 0;
  393. }
  394. }
  395. </pre>
  396. <p>To use it we'll create one </p>
  397. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
  398. const canvas = document.querySelector('#c');
  399. const renderer = new THREE.WebGLRenderer({canvas});
  400. + const tweenManager = new TweenManger();
  401. ...
  402. </pre>
  403. <p>We'll use it to create our <code class="notranslate" translate="no">Tween</code>s.</p>
  404. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">// show the selected data, hide the rest
  405. function showFileInfo(fileInfos, fileInfo) {
  406. const targets = {};
  407. fileInfos.forEach((info, i) =&gt; {
  408. const visible = fileInfo === info;
  409. info.elem.className = visible ? 'selected' : '';
  410. targets[i] = visible ? 1 : 0;
  411. });
  412. const durationInMs = 1000;
  413. - new TWEEN.Tween(mesh.morphTargetInfluences)
  414. + tweenManager.createTween(mesh.morphTargetInfluences)
  415. .to(targets, durationInMs)
  416. .start();
  417. requestRenderIfNotRequested();
  418. }
  419. </pre>
  420. <p>Then we'll update our render loop to update the tweens and keep rendering
  421. if there are still animations running.</p>
  422. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render() {
  423. renderRequested = false;
  424. if (resizeRendererToDisplaySize(renderer)) {
  425. const canvas = renderer.domElement;
  426. camera.aspect = canvas.clientWidth / canvas.clientHeight;
  427. camera.updateProjectionMatrix();
  428. }
  429. + if (tweenManager.update()) {
  430. + requestRenderIfNotRequested();
  431. + }
  432. controls.update();
  433. renderer.render(scene, camera);
  434. }
  435. render();
  436. </pre>
  437. <p>And with that we should be animating between data sets.</p>
  438. <p></p><div translate="no" class="threejs_example_container notranslate">
  439. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lots-of-objects-morphtargets.html"></iframe></div>
  440. <a class="threejs_center" href="/manual/examples/lots-of-objects-morphtargets.html" target="_blank">click here to open in a separate window</a>
  441. </div>
  442. <p></p>
  443. <p>That seems to work but unfortunately we lost the colors.</p>
  444. <p>Three.js does not support morphtarget colors and in fact this is an issue
  445. with the original <a href="https://github.com/dataarts/webgl-globe">webgl globe</a>.
  446. Basically it just makes colors for the first data set. Any other datasets
  447. use the same colors even if they are vastly different.</p>
  448. <p>Let's see if we can add support for morphing the colors. This might
  449. be brittle. The least brittle way would probably be to 100% write our own
  450. shaders but I think it would be useful to see how to modify the built
  451. in shaders.</p>
  452. <p>The first thing we need to do is make the code extract color a <a href="/docs/#api/en/core/BufferAttribute"><code class="notranslate" translate="no">BufferAttribute</code></a> from
  453. each data set's geometry.</p>
  454. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">// use the first geometry as the base
  455. // and add all the geometries as morphtargets
  456. const baseGeometry = geometries[0];
  457. baseGeometry.morphAttributes.position = geometries.map((geometry, ndx) =&gt; {
  458. const attribute = geometry.getAttribute('position');
  459. const name = `target${ndx}`;
  460. attribute.name = name;
  461. return attribute;
  462. });
  463. +const colorAttributes = geometries.map((geometry, ndx) =&gt; {
  464. + const attribute = geometry.getAttribute('color');
  465. + const name = `morphColor${ndx}`;
  466. + attribute.name = `color${ndx}`; // just for debugging
  467. + return {name, attribute};
  468. +});
  469. const material = new THREE.MeshBasicMaterial({
  470. vertexColors: true,
  471. });
  472. </pre>
  473. <p>We then need to modify the three.js shader. Three.js materials have an
  474. <a href="/docs/#api/en/materials/Material.onBeforeCompile"><code class="notranslate" translate="no">Material.onBeforeCompile</code></a> property we can assign a function. It gives us a
  475. chance to modify the material's shader before it is passed to WebGL. In fact the
  476. shader that is provided is actually a special three.js only syntax of shader
  477. that lists a bunch of shader <em>chunks</em> that three.js will substitute with the
  478. actual GLSL code for each chunk. Here is what the unmodified vertex shader code
  479. looks like as passed to <code class="notranslate" translate="no">onBeforeCompile</code>.</p>
  480. <pre class="prettyprint showlinemods notranslate lang-glsl" translate="no">#include &lt;common&gt;
  481. #include &lt;uv_pars_vertex&gt;
  482. #include &lt;uv2_pars_vertex&gt;
  483. #include &lt;envmap_pars_vertex&gt;
  484. #include &lt;color_pars_vertex&gt;
  485. #include &lt;fog_pars_vertex&gt;
  486. #include &lt;morphtarget_pars_vertex&gt;
  487. #include &lt;skinning_pars_vertex&gt;
  488. #include &lt;logdepthbuf_pars_vertex&gt;
  489. #include &lt;clipping_planes_pars_vertex&gt;
  490. void main() {
  491. #include &lt;uv_vertex&gt;
  492. #include &lt;uv2_vertex&gt;
  493. #include &lt;color_vertex&gt;
  494. #include &lt;skinbase_vertex&gt;
  495. #ifdef USE_ENVMAP
  496. #include &lt;beginnormal_vertex&gt;
  497. #include &lt;morphnormal_vertex&gt;
  498. #include &lt;skinnormal_vertex&gt;
  499. #include &lt;defaultnormal_vertex&gt;
  500. #endif
  501. #include &lt;begin_vertex&gt;
  502. #include &lt;morphtarget_vertex&gt;
  503. #include &lt;skinning_vertex&gt;
  504. #include &lt;project_vertex&gt;
  505. #include &lt;logdepthbuf_vertex&gt;
  506. #include &lt;worldpos_vertex&gt;
  507. #include &lt;clipping_planes_vertex&gt;
  508. #include &lt;envmap_vertex&gt;
  509. #include &lt;fog_vertex&gt;
  510. }
  511. </pre>
  512. <p>Digging through the various chunks we want to replace
  513. the <a href="https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/morphtarget_pars_vertex.glsl.js"><code class="notranslate" translate="no">morphtarget_pars_vertex</code> chunk</a>
  514. the <a href="https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/morphnormal_vertex.glsl.js"><code class="notranslate" translate="no">morphnormal_vertex</code> chunk</a>
  515. the <a href="https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/morphtarget_vertex.glsl.js"><code class="notranslate" translate="no">morphtarget_vertex</code> chunk</a>
  516. the <a href="https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/color_pars_vertex.glsl.js"><code class="notranslate" translate="no">color_pars_vertex</code> chunk</a>
  517. and the <a href="https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/color_vertex.glsl.js"><code class="notranslate" translate="no">color_vertex</code> chunk</a></p>
  518. <p>To do that we'll make a simple array of replacements and apply them in <a href="/docs/#api/en/materials/Material.onBeforeCompile"><code class="notranslate" translate="no">Material.onBeforeCompile</code></a></p>
  519. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const material = new THREE.MeshBasicMaterial({
  520. vertexColors: true,
  521. });
  522. +const vertexShaderReplacements = [
  523. + {
  524. + from: '#include &lt;morphtarget_pars_vertex&gt;',
  525. + to: `
  526. + uniform float morphTargetInfluences[8];
  527. + `,
  528. + },
  529. + {
  530. + from: '#include &lt;morphnormal_vertex&gt;',
  531. + to: `
  532. + `,
  533. + },
  534. + {
  535. + from: '#include &lt;morphtarget_vertex&gt;',
  536. + to: `
  537. + transformed += (morphTarget0 - position) * morphTargetInfluences[0];
  538. + transformed += (morphTarget1 - position) * morphTargetInfluences[1];
  539. + transformed += (morphTarget2 - position) * morphTargetInfluences[2];
  540. + transformed += (morphTarget3 - position) * morphTargetInfluences[3];
  541. + `,
  542. + },
  543. + {
  544. + from: '#include &lt;color_pars_vertex&gt;',
  545. + to: `
  546. + varying vec3 vColor;
  547. + attribute vec3 morphColor0;
  548. + attribute vec3 morphColor1;
  549. + attribute vec3 morphColor2;
  550. + attribute vec3 morphColor3;
  551. + `,
  552. + },
  553. + {
  554. + from: '#include &lt;color_vertex&gt;',
  555. + to: `
  556. + vColor.xyz = morphColor0 * morphTargetInfluences[0] +
  557. + morphColor1 * morphTargetInfluences[1] +
  558. + morphColor2 * morphTargetInfluences[2] +
  559. + morphColor3 * morphTargetInfluences[3];
  560. + `,
  561. + },
  562. +];
  563. +material.onBeforeCompile = (shader) =&gt; {
  564. + vertexShaderReplacements.forEach((rep) =&gt; {
  565. + shader.vertexShader = shader.vertexShader.replace(rep.from, rep.to);
  566. + });
  567. +};
  568. </pre>
  569. <p>Three.js also sorts morphtargets and applies only the highest influences. This
  570. lets it allow many more morphtargets as long as only a few are used at a time.
  571. Unfortunately three.js does not provide any way to know how many morph targets
  572. will be used nor which attributes the morph targets will be assigned to. So,
  573. we'll have to look into the code and reproduce what it does here. If that
  574. algorithm changes in three.js we'll need to refactor this code.</p>
  575. <p>First we remove all the color attributes. It doesn't matter if we did not add
  576. them before as it's safe to remove an attribute that was not previously added.
  577. Then we'll compute which targets we think three.js will use and finally assign
  578. those targets to the attributes we think three.js would assign them to.</p>
  579. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const mesh = new THREE.Mesh(baseGeometry, material);
  580. scene.add(mesh);
  581. +function updateMorphTargets() {
  582. + // remove all the color attributes
  583. + for (const {name} of colorAttributes) {
  584. + baseGeometry.deleteAttribute(name);
  585. + }
  586. +
  587. + // three.js provides no way to query this so we have to guess and hope it doesn't change.
  588. + const maxInfluences = 8;
  589. +
  590. + // three provides no way to query which morph targets it will use
  591. + // nor which attributes it will assign them to so we'll guess.
  592. + // If the algorithm in three.js changes we'll need to refactor this.
  593. + mesh.morphTargetInfluences
  594. + .map((influence, i) =&gt; [i, influence]) // map indices to influence
  595. + .sort((a, b) =&gt; Math.abs(b[1]) - Math.abs(a[1])) // sort by highest influence first
  596. + .slice(0, maxInfluences) // keep only top influences
  597. + .sort((a, b) =&gt; a[0] - b[0]) // sort by index
  598. + .filter(a =&gt; !!a[1]) // remove no influence entries
  599. + .forEach(([ndx], i) =&gt; { // assign the attributes
  600. + const name = `morphColor${i}`;
  601. + baseGeometry.setAttribute(name, colorAttributes[ndx].attribute);
  602. + });
  603. +}
  604. </pre>
  605. <p>We'll return this function from our <code class="notranslate" translate="no">loadAll</code> function. This way we don't
  606. need to leak any variables.</p>
  607. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">async function loadAll() {
  608. ...
  609. + return updateMorphTargets;
  610. }
  611. +// use a no-op update function until the data is ready
  612. +let updateMorphTargets = () =&gt; {};
  613. -loadAll();
  614. +loadAll().then(fn =&gt; {
  615. + updateMorphTargets = fn;
  616. +});
  617. </pre>
  618. <p>And finally we need to call <code class="notranslate" translate="no">updateMorphTargets</code> after we've let the values
  619. be updated by the tween manager and before rendering.</p>
  620. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render() {
  621. ...
  622. if (tweenManager.update()) {
  623. requestRenderIfNotRequested();
  624. }
  625. + updateMorphTargets();
  626. controls.update();
  627. renderer.render(scene, camera);
  628. }
  629. </pre>
  630. <p>And with that we should have the colors animating as well as the boxes.</p>
  631. <p></p><div translate="no" class="threejs_example_container notranslate">
  632. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lots-of-objects-morphtargets-w-colors.html"></iframe></div>
  633. <a class="threejs_center" href="/manual/examples/lots-of-objects-morphtargets-w-colors.html" target="_blank">click here to open in a separate window</a>
  634. </div>
  635. <p></p>
  636. <p>I hope going through this was helpful. Using morphtargets either through the
  637. services three.js provides or by writing custom shaders is a common technique to
  638. move lots of objects. As an example we could give every cube a random place in
  639. another target and morph from that to their first positions on the globe. That
  640. might be a cool way to introduce the globe.</p>
  641. <p>Next you might be interested in adding labels to a globe which is covered
  642. in <a href="align-html-elements-to-3d.html">Aligning HTML Elements to 3D</a>.</p>
  643. <p>Note: We could try to just graph percent of men or percent of women or the raw
  644. difference but based on how we are displaying the info, cubes that grow from the
  645. surface of the earth, we'd prefer most cubes to be low. If we used one of these
  646. other comparisons most cubes would be about 1/2 their maximum height which would
  647. not make a good visualization. Feel free to change the <code class="notranslate" translate="no">amountGreaterThan</code> from
  648. <a href="/docs/#api/en/math/Math.max(a - b, 0)"><code class="notranslate" translate="no">Math.max(a - b, 0)</code></a> to something like <code class="notranslate" translate="no">(a - b)</code> "raw difference" or <code class="notranslate" translate="no">a / (a +
  649. b)</code> "percent" and you'll see what I mean.</p>
  650. </div>
  651. </div>
  652. </div>
  653. <script src="/manual/resources/prettify.js"></script>
  654. <script src="/manual/resources/lesson.js"></script>
  655. </body></html>