voxel-geometry.html 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136
  1. <!DOCTYPE html><html lang="en"><head>
  2. <meta charset="utf-8">
  3. <title>Voxel(Minecraft Like) Geometry</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 – Voxel(Minecraft Like) Geometry">
  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>Voxel(Minecraft Like) Geometry</h1>
  28. </div>
  29. <div class="lesson">
  30. <div class="lesson-main">
  31. <p>I've seen this topic come up more than once in various places.
  32. That is basically, "How do I make a voxel display like Minecraft".</p>
  33. <p>Most people first attempt this by making a cube geometry and then
  34. making a mesh at each voxel position. Just for fun I tried
  35. this. I made a 16777216 element <code class="notranslate" translate="no">Uint8Array</code> to represent
  36. a 256x256x256 cube of voxels.</p>
  37. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cellSize = 256;
  38. const cell = new Uint8Array(cellSize * cellSize * cellSize);
  39. </pre>
  40. <p>I then made a single layer with a kind of hills of
  41. sine waves like this</p>
  42. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">for (let y = 0; y &lt; cellSize; ++y) {
  43. for (let z = 0; z &lt; cellSize; ++z) {
  44. for (let x = 0; x &lt; cellSize; ++x) {
  45. const height = (Math.sin(x / cellSize * Math.PI * 4) + Math.sin(z / cellSize * Math.PI * 6)) * 20 + cellSize / 2;
  46. if (height &gt; y &amp;&amp; height &lt; y + 1) {
  47. const offset = y * cellSize * cellSize +
  48. z * cellSize +
  49. x;
  50. cell[offset] = 1;
  51. }
  52. }
  53. }
  54. }
  55. </pre>
  56. <p>I then walked through all the cells and if they were not
  57. 0 I created a mesh with a cube.</p>
  58. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const geometry = new THREE.BoxGeometry(1, 1, 1);
  59. const material = new THREE.MeshPhongMaterial({color: 'green'});
  60. for (let y = 0; y &lt; cellSize; ++y) {
  61. for (let z = 0; z &lt; cellSize; ++z) {
  62. for (let x = 0; x &lt; cellSize; ++x) {
  63. const offset = y * cellSize * cellSize +
  64. z * cellSize +
  65. x;
  66. const block = cell[offset];
  67. const mesh = new THREE.Mesh(geometry, material);
  68. mesh.position.set(x, y, z);
  69. scene.add(mesh);
  70. }
  71. }
  72. }
  73. </pre>
  74. <p>The rest of the code is based on the example from
  75. <a href="rendering-on-demand.html">the article on rendering on demand</a>.</p>
  76. <p></p><div translate="no" class="threejs_example_container notranslate">
  77. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/voxel-geometry-separate-cubes.html"></iframe></div>
  78. <a class="threejs_center" href="/manual/examples/voxel-geometry-separate-cubes.html" target="_blank">click here to open in a separate window</a>
  79. </div>
  80. <p></p>
  81. <p>It takes a while to start and if you try to move the camera
  82. it's likely too slow. Like <a href="optimize-lots-of-objects.html">the article on how to optimize lots of objects</a>
  83. the problem is there are just way too many objects. 256x256
  84. is 65536 boxes!</p>
  85. <p>Using <a href="rendering-on-demand.html">the technique of merging the geometry</a>
  86. will fix the issue for this example but what if instead of just making
  87. a single layer we filled in everything below the ground with voxel.
  88. In other words change the loop filling in the voxels to this</p>
  89. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">for (let y = 0; y &lt; cellSize; ++y) {
  90. for (let z = 0; z &lt; cellSize; ++z) {
  91. for (let x = 0; x &lt; cellSize; ++x) {
  92. const height = (Math.sin(x / cellSize * Math.PI * 4) + Math.sin(z / cellSize * Math.PI * 6)) * 20 + cellSize / 2;
  93. - if (height &gt; y &amp;&amp; height &lt; y + 1) {
  94. + if (height &lt; y + 1) {
  95. const offset = y * cellSize * cellSize +
  96. z * cellSize +
  97. x;
  98. cell[offset] = 1;
  99. }
  100. }
  101. }
  102. }
  103. </pre>
  104. <p>I tried it once just to see the results. It churned for
  105. about a minute and then crashed with <em>out of memory</em> 😅</p>
  106. <p>There are several issues but the biggest issue is
  107. we're making all these faces inside the cubes that
  108. we can actually never see.</p>
  109. <p>In other words lets say we have a box of voxels
  110. 3x2x2. By merging cubes we're getting this</p>
  111. <div class="spread">
  112. <div data-diagram="mergedCubes" style="height: 300px;"></div>
  113. </div>
  114. <p>but we really want this</p>
  115. <div class="spread">
  116. <div data-diagram="culledCubes" style="height: 300px;"></div>
  117. </div>
  118. <p>In the top box there are faces between the voxels. Faces
  119. that are a waste since they can't be seen. It's not just
  120. one face between each voxel, there are 2 faces, one for
  121. each voxel facing its neighbor that are a waste. All these extra faces,
  122. especially for a large volume of voxels will kill performance.</p>
  123. <p>It should be clear that we can't just merge geometry.
  124. We need to build it ourselves, taking into account that
  125. if a voxel has an adjacent neighbor it doesn't need the
  126. face facing that neighbor.</p>
  127. <p>The next issue is that 256x256x256 is just too big. 16meg is a lot of memory and
  128. if nothing else in much of the space nothing is there so that's a lot of wasted
  129. memory. It's also a huge number of voxels, 16 million! That's too much to
  130. consider at once.</p>
  131. <p>A solution is to divide the area into smaller areas.
  132. Any area that has nothing in it needs no storage. Let's use
  133. 32x32x32 areas (that's 32k) and only create an area if something is in it.
  134. We'll call one of these larger 32x32x32 areas a "cell".</p>
  135. <p>Let's break this into pieces. First let's make a class to manage the voxel data.</p>
  136. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
  137. constructor(cellSize) {
  138. this.cellSize = cellSize;
  139. }
  140. }
  141. </pre>
  142. <p>Let's make the function that makes geometry for a cell.
  143. Let's assume you pass in a cell position.
  144. In other words if you want the geometry for the cell that covers voxels (0-31x, 0-31y, 0-31z)
  145. then you'd pass in 0,0,0. For the cell that covers voxels (32-63x, 0-31y, 0-31z) you'd
  146. pass in 1,0,0.</p>
  147. <p>We need to be able to check the neighboring voxels so let's assume our class
  148. has a function <code class="notranslate" translate="no">getVoxel</code> that given a voxel position returns the value of
  149. the voxel there. In other words if you pass it 35,0,0 and the cellSize is 32
  150. it's going to look at cell 1,0,0 and in that cell it will look at voxel 3,0,0.
  151. Using this function we can look at a voxel's neighboring voxels even if they
  152. happen to be in neighboring cells.</p>
  153. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
  154. constructor(cellSize) {
  155. this.cellSize = cellSize;
  156. }
  157. + generateGeometryDataForCell(cellX, cellY, cellZ) {
  158. + const {cellSize} = this;
  159. + const startX = cellX * cellSize;
  160. + const startY = cellY * cellSize;
  161. + const startZ = cellZ * cellSize;
  162. +
  163. + for (let y = 0; y &lt; cellSize; ++y) {
  164. + const voxelY = startY + y;
  165. + for (let z = 0; z &lt; cellSize; ++z) {
  166. + const voxelZ = startZ + z;
  167. + for (let x = 0; x &lt; cellSize; ++x) {
  168. + const voxelX = startX + x;
  169. + const voxel = this.getVoxel(voxelX, voxelY, voxelZ);
  170. + if (voxel) {
  171. + for (const {dir} of VoxelWorld.faces) {
  172. + const neighbor = this.getVoxel(
  173. + voxelX + dir[0],
  174. + voxelY + dir[1],
  175. + voxelZ + dir[2]);
  176. + if (!neighbor) {
  177. + // this voxel has no neighbor in this direction so we need a face
  178. + // here.
  179. + }
  180. + }
  181. + }
  182. + }
  183. + }
  184. + }
  185. + }
  186. }
  187. +VoxelWorld.faces = [
  188. + { // left
  189. + dir: [ -1, 0, 0, ],
  190. + },
  191. + { // right
  192. + dir: [ 1, 0, 0, ],
  193. + },
  194. + { // bottom
  195. + dir: [ 0, -1, 0, ],
  196. + },
  197. + { // top
  198. + dir: [ 0, 1, 0, ],
  199. + },
  200. + { // back
  201. + dir: [ 0, 0, -1, ],
  202. + },
  203. + { // front
  204. + dir: [ 0, 0, 1, ],
  205. + },
  206. +];
  207. </pre>
  208. <p>So using the code above we know when we need a face. Let's generate the faces.</p>
  209. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
  210. constructor(cellSize) {
  211. this.cellSize = cellSize;
  212. }
  213. generateGeometryDataForCell(cellX, cellY, cellZ) {
  214. const {cellSize} = this;
  215. + const positions = [];
  216. + const normals = [];
  217. + const indices = [];
  218. const startX = cellX * cellSize;
  219. const startY = cellY * cellSize;
  220. const startZ = cellZ * cellSize;
  221. for (let y = 0; y &lt; cellSize; ++y) {
  222. const voxelY = startY + y;
  223. for (let z = 0; z &lt; cellSize; ++z) {
  224. const voxelZ = startZ + z;
  225. for (let x = 0; x &lt; cellSize; ++x) {
  226. const voxelX = startX + x;
  227. const voxel = this.getVoxel(voxelX, voxelY, voxelZ);
  228. if (voxel) {
  229. - for (const {dir} of VoxelWorld.faces) {
  230. + for (const {dir, corners} of VoxelWorld.faces) {
  231. const neighbor = this.getVoxel(
  232. voxelX + dir[0],
  233. voxelY + dir[1],
  234. voxelZ + dir[2]);
  235. if (!neighbor) {
  236. // this voxel has no neighbor in this direction so we need a face.
  237. + const ndx = positions.length / 3;
  238. + for (const pos of corners) {
  239. + positions.push(pos[0] + x, pos[1] + y, pos[2] + z);
  240. + normals.push(...dir);
  241. + }
  242. + indices.push(
  243. + ndx, ndx + 1, ndx + 2,
  244. + ndx + 2, ndx + 1, ndx + 3,
  245. + );
  246. }
  247. }
  248. }
  249. }
  250. }
  251. }
  252. + return {
  253. + positions,
  254. + normals,
  255. + indices,
  256. };
  257. }
  258. }
  259. VoxelWorld.faces = [
  260. { // left
  261. dir: [ -1, 0, 0, ],
  262. + corners: [
  263. + [ 0, 1, 0 ],
  264. + [ 0, 0, 0 ],
  265. + [ 0, 1, 1 ],
  266. + [ 0, 0, 1 ],
  267. + ],
  268. },
  269. { // right
  270. dir: [ 1, 0, 0, ],
  271. + corners: [
  272. + [ 1, 1, 1 ],
  273. + [ 1, 0, 1 ],
  274. + [ 1, 1, 0 ],
  275. + [ 1, 0, 0 ],
  276. + ],
  277. },
  278. { // bottom
  279. dir: [ 0, -1, 0, ],
  280. + corners: [
  281. + [ 1, 0, 1 ],
  282. + [ 0, 0, 1 ],
  283. + [ 1, 0, 0 ],
  284. + [ 0, 0, 0 ],
  285. + ],
  286. },
  287. { // top
  288. dir: [ 0, 1, 0, ],
  289. + corners: [
  290. + [ 0, 1, 1 ],
  291. + [ 1, 1, 1 ],
  292. + [ 0, 1, 0 ],
  293. + [ 1, 1, 0 ],
  294. + ],
  295. },
  296. { // back
  297. dir: [ 0, 0, -1, ],
  298. + corners: [
  299. + [ 1, 0, 0 ],
  300. + [ 0, 0, 0 ],
  301. + [ 1, 1, 0 ],
  302. + [ 0, 1, 0 ],
  303. + ],
  304. },
  305. { // front
  306. dir: [ 0, 0, 1, ],
  307. + corners: [
  308. + [ 0, 0, 1 ],
  309. + [ 1, 0, 1 ],
  310. + [ 0, 1, 1 ],
  311. + [ 1, 1, 1 ],
  312. + ],
  313. },
  314. ];
  315. </pre>
  316. <p>The code above would make basic geometry data for us. We just need to supply
  317. the <code class="notranslate" translate="no">getVoxel</code> function. Let's start with just one hard coded cell.</p>
  318. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
  319. constructor(cellSize) {
  320. this.cellSize = cellSize;
  321. + this.cell = new Uint8Array(cellSize * cellSize * cellSize);
  322. }
  323. + getCellForVoxel(x, y, z) {
  324. + const {cellSize} = this;
  325. + const cellX = Math.floor(x / cellSize);
  326. + const cellY = Math.floor(y / cellSize);
  327. + const cellZ = Math.floor(z / cellSize);
  328. + if (cellX !== 0 || cellY !== 0 || cellZ !== 0) {
  329. + return null
  330. + }
  331. + return this.cell;
  332. + }
  333. + getVoxel(x, y, z) {
  334. + const cell = this.getCellForVoxel(x, y, z);
  335. + if (!cell) {
  336. + return 0;
  337. + }
  338. + const {cellSize} = this;
  339. + const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
  340. + const voxelY = THREE.MathUtils.euclideanModulo(y, cellSize) | 0;
  341. + const voxelZ = THREE.MathUtils.euclideanModulo(z, cellSize) | 0;
  342. + const voxelOffset = voxelY * cellSize * cellSize +
  343. + voxelZ * cellSize +
  344. + voxelX;
  345. + return cell[voxelOffset];
  346. + }
  347. generateGeometryDataForCell(cellX, cellY, cellZ) {
  348. ...
  349. }
  350. </pre>
  351. <p>This seems like it would work. Let's make a <code class="notranslate" translate="no">setVoxel</code> function
  352. so we can set some data.</p>
  353. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
  354. constructor(cellSize) {
  355. this.cellSize = cellSize;
  356. this.cell = new Uint8Array(cellSize * cellSize * cellSize);
  357. }
  358. getCellForVoxel(x, y, z) {
  359. const {cellSize} = this;
  360. const cellX = Math.floor(x / cellSize);
  361. const cellY = Math.floor(y / cellSize);
  362. const cellZ = Math.floor(z / cellSize);
  363. if (cellX !== 0 || cellY !== 0 || cellZ !== 0) {
  364. return null
  365. }
  366. return this.cell;
  367. }
  368. + setVoxel(x, y, z, v) {
  369. + let cell = this.getCellForVoxel(x, y, z);
  370. + if (!cell) {
  371. + return; // TODO: add a new cell?
  372. + }
  373. + const {cellSize} = this;
  374. + const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
  375. + const voxelY = THREE.MathUtils.euclideanModulo(y, cellSize) | 0;
  376. + const voxelZ = THREE.MathUtils.euclideanModulo(z, cellSize) | 0;
  377. + const voxelOffset = voxelY * cellSize * cellSize +
  378. + voxelZ * cellSize +
  379. + voxelX;
  380. + cell[voxelOffset] = v;
  381. + }
  382. getVoxel(x, y, z) {
  383. const cell = this.getCellForVoxel(x, y, z);
  384. if (!cell) {
  385. return 0;
  386. }
  387. const {cellSize} = this;
  388. const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
  389. const voxelY = THREE.MathUtils.euclideanModulo(y, cellSize) | 0;
  390. const voxelZ = THREE.MathUtils.euclideanModulo(z, cellSize) | 0;
  391. const voxelOffset = voxelY * cellSize * cellSize +
  392. voxelZ * cellSize +
  393. voxelX;
  394. return cell[voxelOffset];
  395. }
  396. generateGeometryDataForCell(cellX, cellY, cellZ) {
  397. ...
  398. }
  399. </pre>
  400. <p>Hmmm, I see a lot of repeated code. Let's fix that up</p>
  401. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
  402. constructor(cellSize) {
  403. this.cellSize = cellSize;
  404. + this.cellSliceSize = cellSize * cellSize;
  405. this.cell = new Uint8Array(cellSize * cellSize * cellSize);
  406. }
  407. getCellForVoxel(x, y, z) {
  408. const {cellSize} = this;
  409. const cellX = Math.floor(x / cellSize);
  410. const cellY = Math.floor(y / cellSize);
  411. const cellZ = Math.floor(z / cellSize);
  412. if (cellX !== 0 || cellY !== 0 || cellZ !== 0) {
  413. return null;
  414. }
  415. return this.cell;
  416. }
  417. + computeVoxelOffset(x, y, z) {
  418. + const {cellSize, cellSliceSize} = this;
  419. + const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
  420. + const voxelY = THREE.MathUtils.euclideanModulo(y, cellSize) | 0;
  421. + const voxelZ = THREE.MathUtils.euclideanModulo(z, cellSize) | 0;
  422. + return voxelY * cellSliceSize +
  423. + voxelZ * cellSize +
  424. + voxelX;
  425. + }
  426. setVoxel(x, y, z, v) {
  427. const cell = this.getCellForVoxel(x, y, z);
  428. if (!cell) {
  429. return; // TODO: add a new cell?
  430. }
  431. - const {cellSize} = this;
  432. - const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
  433. - const voxelY = THREE.MathUtils.euclideanModulo(y, cellSize) | 0;
  434. - const voxelZ = THREE.MathUtils.euclideanModulo(z, cellSize) | 0;
  435. - const voxelOffset = voxelY * cellSize * cellSize +
  436. - voxelZ * cellSize +
  437. - voxelX;
  438. + const voxelOffset = this.computeVoxelOffset(x, y, z);
  439. cell[voxelOffset] = v;
  440. }
  441. getVoxel(x, y, z) {
  442. const cell = this.getCellForVoxel(x, y, z);
  443. if (!cell) {
  444. return 0;
  445. }
  446. - const {cellSize} = this;
  447. - const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
  448. - const voxelY = THREE.MathUtils.euclideanModulo(y, cellSize) | 0;
  449. - const voxelZ = THREE.MathUtils.euclideanModulo(z, cellSize) | 0;
  450. - const voxelOffset = voxelY * cellSize * cellSize +
  451. - voxelZ * cellSize +
  452. - voxelX;
  453. + const voxelOffset = this.computeVoxelOffset(x, y, z);
  454. return cell[voxelOffset];
  455. }
  456. generateGeometryDataForCell(cellX, cellY, cellZ) {
  457. ...
  458. }
  459. </pre>
  460. <p>Now let's make some code to fill out the first cell with voxels.</p>
  461. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cellSize = 32;
  462. const world = new VoxelWorld(cellSize);
  463. for (let y = 0; y &lt; cellSize; ++y) {
  464. for (let z = 0; z &lt; cellSize; ++z) {
  465. for (let x = 0; x &lt; cellSize; ++x) {
  466. const height = (Math.sin(x / cellSize * Math.PI * 2) + Math.sin(z / cellSize * Math.PI * 3)) * (cellSize / 6) + (cellSize / 2);
  467. if (y &lt; height) {
  468. world.setVoxel(x, y, z, 1);
  469. }
  470. }
  471. }
  472. }
  473. </pre>
  474. <p>and some code to actually generate geometry like we covered in
  475. <a href="custom-buffergeometry.html">the article on custom BufferGeometry</a>.</p>
  476. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const {positions, normals, indices} = world.generateGeometryDataForCell(0, 0, 0);
  477. const geometry = new THREE.BufferGeometry();
  478. const material = new THREE.MeshLambertMaterial({color: 'green'});
  479. const positionNumComponents = 3;
  480. const normalNumComponents = 3;
  481. geometry.setAttribute(
  482. 'position',
  483. new THREE.BufferAttribute(new Float32Array(positions), positionNumComponents));
  484. geometry.setAttribute(
  485. 'normal',
  486. new THREE.BufferAttribute(new Float32Array(normals), normalNumComponents));
  487. geometry.setIndex(indices);
  488. const mesh = new THREE.Mesh(geometry, material);
  489. scene.add(mesh);
  490. </pre>
  491. <p>let's try it</p>
  492. <p></p><div translate="no" class="threejs_example_container notranslate">
  493. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/voxel-geometry-culled-faces.html"></iframe></div>
  494. <a class="threejs_center" href="/manual/examples/voxel-geometry-culled-faces.html" target="_blank">click here to open in a separate window</a>
  495. </div>
  496. <p></p>
  497. <p>That seems to be working! Okay, let's add in textures.</p>
  498. <p>Searching on the net I found <a href="https://www.minecraftforum.net/forums/mapping-and-modding-java-edition/resource-packs/1245961-16x-1-7-4-wip-flourish">this set</a>
  499. of <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC-BY-NC-SA</a> licensed minecraft textures
  500. by <a href="https://www.minecraftforum.net/members/Joshtimus">Joshtimus</a>.
  501. I picked a few at random and built this <a href="https://www.google.com/search?q=texture+atlas">texture atlas</a>.</p>
  502. <div class="threejs_center"><img class="checkerboard" src="../examples/resources/images/minecraft/flourish-cc-by-nc-sa.png" style="width: 512px; image-rendering: pixelated;"></div>
  503. <p>To make things simple they are arranged a voxel type per column
  504. where the top row is the side of a voxel. The 2nd row is
  505. the top of voxel, and the 3rd row is the bottom of the voxel.</p>
  506. <p>Knowing that we can add info to our <code class="notranslate" translate="no">VoxelWorld.faces</code> data
  507. to specify for each face which row to use and the UVs to use
  508. for that face.</p>
  509. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">VoxelWorld.faces = [
  510. { // left
  511. + uvRow: 0,
  512. dir: [ -1, 0, 0, ],
  513. corners: [
  514. - [ 0, 1, 0 ],
  515. - [ 0, 0, 0 ],
  516. - [ 0, 1, 1 ],
  517. - [ 0, 0, 1 ],
  518. + { pos: [ 0, 1, 0 ], uv: [ 0, 1 ], },
  519. + { pos: [ 0, 0, 0 ], uv: [ 0, 0 ], },
  520. + { pos: [ 0, 1, 1 ], uv: [ 1, 1 ], },
  521. + { pos: [ 0, 0, 1 ], uv: [ 1, 0 ], },
  522. ],
  523. },
  524. { // right
  525. + uvRow: 0,
  526. dir: [ 1, 0, 0, ],
  527. corners: [
  528. - [ 1, 1, 1 ],
  529. - [ 1, 0, 1 ],
  530. - [ 1, 1, 0 ],
  531. - [ 1, 0, 0 ],
  532. + { pos: [ 1, 1, 1 ], uv: [ 0, 1 ], },
  533. + { pos: [ 1, 0, 1 ], uv: [ 0, 0 ], },
  534. + { pos: [ 1, 1, 0 ], uv: [ 1, 1 ], },
  535. + { pos: [ 1, 0, 0 ], uv: [ 1, 0 ], },
  536. ],
  537. },
  538. { // bottom
  539. + uvRow: 1,
  540. dir: [ 0, -1, 0, ],
  541. corners: [
  542. - [ 1, 0, 1 ],
  543. - [ 0, 0, 1 ],
  544. - [ 1, 0, 0 ],
  545. - [ 0, 0, 0 ],
  546. + { pos: [ 1, 0, 1 ], uv: [ 1, 0 ], },
  547. + { pos: [ 0, 0, 1 ], uv: [ 0, 0 ], },
  548. + { pos: [ 1, 0, 0 ], uv: [ 1, 1 ], },
  549. + { pos: [ 0, 0, 0 ], uv: [ 0, 1 ], },
  550. ],
  551. },
  552. { // top
  553. + uvRow: 2,
  554. dir: [ 0, 1, 0, ],
  555. corners: [
  556. - [ 0, 1, 1 ],
  557. - [ 1, 1, 1 ],
  558. - [ 0, 1, 0 ],
  559. - [ 1, 1, 0 ],
  560. + { pos: [ 0, 1, 1 ], uv: [ 1, 1 ], },
  561. + { pos: [ 1, 1, 1 ], uv: [ 0, 1 ], },
  562. + { pos: [ 0, 1, 0 ], uv: [ 1, 0 ], },
  563. + { pos: [ 1, 1, 0 ], uv: [ 0, 0 ], },
  564. ],
  565. },
  566. { // back
  567. + uvRow: 0,
  568. dir: [ 0, 0, -1, ],
  569. corners: [
  570. - [ 1, 0, 0 ],
  571. - [ 0, 0, 0 ],
  572. - [ 1, 1, 0 ],
  573. - [ 0, 1, 0 ],
  574. + { pos: [ 1, 0, 0 ], uv: [ 0, 0 ], },
  575. + { pos: [ 0, 0, 0 ], uv: [ 1, 0 ], },
  576. + { pos: [ 1, 1, 0 ], uv: [ 0, 1 ], },
  577. + { pos: [ 0, 1, 0 ], uv: [ 1, 1 ], },
  578. ],
  579. },
  580. { // front
  581. + uvRow: 0,
  582. dir: [ 0, 0, 1, ],
  583. corners: [
  584. - [ 0, 0, 1 ],
  585. - [ 1, 0, 1 ],
  586. - [ 0, 1, 1 ],
  587. - [ 1, 1, 1 ],
  588. + { pos: [ 0, 0, 1 ], uv: [ 0, 0 ], },
  589. + { pos: [ 1, 0, 1 ], uv: [ 1, 0 ], },
  590. + { pos: [ 0, 1, 1 ], uv: [ 0, 1 ], },
  591. + { pos: [ 1, 1, 1 ], uv: [ 1, 1 ], },
  592. ],
  593. },
  594. ];
  595. </pre>
  596. <p>And we can update the code to use that data. We need to
  597. know the size of a tile in the texture atlas and the dimensions
  598. of the texture.</p>
  599. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
  600. - constructor(cellSize) {
  601. - this.cellSize = cellSize;
  602. + constructor(options) {
  603. + this.cellSize = options.cellSize;
  604. + this.tileSize = options.tileSize;
  605. + this.tileTextureWidth = options.tileTextureWidth;
  606. + this.tileTextureHeight = options.tileTextureHeight;
  607. + const {cellSize} = this;
  608. + this.cellSliceSize = cellSize * cellSize;
  609. + this.cell = new Uint8Array(cellSize * cellSize * cellSize);
  610. }
  611. ...
  612. generateGeometryDataForCell(cellX, cellY, cellZ) {
  613. - const {cellSize} = this;
  614. + const {cellSize, tileSize, tileTextureWidth, tileTextureHeight} = this;
  615. const positions = [];
  616. const normals = [];
  617. + const uvs = [];
  618. const indices = [];
  619. const startX = cellX * cellSize;
  620. const startY = cellY * cellSize;
  621. const startZ = cellZ * cellSize;
  622. for (let y = 0; y &lt; cellSize; ++y) {
  623. const voxelY = startY + y;
  624. for (let z = 0; z &lt; cellSize; ++z) {
  625. const voxelZ = startZ + z;
  626. for (let x = 0; x &lt; cellSize; ++x) {
  627. const voxelX = startX + x;
  628. const voxel = this.getVoxel(voxelX, voxelY, voxelZ);
  629. if (voxel) {
  630. const uvVoxel = voxel - 1; // voxel 0 is sky so for UVs we start at 0
  631. // There is a voxel here but do we need faces for it?
  632. - for (const {dir, corners} of VoxelWorld.faces) {
  633. + for (const {dir, corners, uvRow} of VoxelWorld.faces) {
  634. const neighbor = this.getVoxel(
  635. voxelX + dir[0],
  636. voxelY + dir[1],
  637. voxelZ + dir[2]);
  638. if (!neighbor) {
  639. // this voxel has no neighbor in this direction so we need a face.
  640. const ndx = positions.length / 3;
  641. - for (const pos of corners) {
  642. + for (const {pos, uv} of corners) {
  643. positions.push(pos[0] + x, pos[1] + y, pos[2] + z);
  644. normals.push(...dir);
  645. + uvs.push(
  646. + (uvVoxel + uv[0]) * tileSize / tileTextureWidth,
  647. + 1 - (uvRow + 1 - uv[1]) * tileSize / tileTextureHeight);
  648. }
  649. indices.push(
  650. ndx, ndx + 1, ndx + 2,
  651. ndx + 2, ndx + 1, ndx + 3,
  652. );
  653. }
  654. }
  655. }
  656. }
  657. }
  658. }
  659. return {
  660. positions,
  661. normals,
  662. uvs,
  663. indices,
  664. };
  665. }
  666. }
  667. </pre>
  668. <p>We then need to <a href="textures.html">load the texture</a></p>
  669. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const loader = new THREE.TextureLoader();
  670. const texture = loader.load('resources/images/minecraft/flourish-cc-by-nc-sa.png', render);
  671. texture.magFilter = THREE.NearestFilter;
  672. texture.minFilter = THREE.NearestFilter;
  673. </pre>
  674. <p>and pass the settings to the <code class="notranslate" translate="no">VoxelWorld</code> class</p>
  675. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const tileSize = 16;
  676. +const tileTextureWidth = 256;
  677. +const tileTextureHeight = 64;
  678. -const world = new VoxelWorld(cellSize);
  679. +const world = new VoxelWorld({
  680. + cellSize,
  681. + tileSize,
  682. + tileTextureWidth,
  683. + tileTextureHeight,
  684. +});
  685. </pre>
  686. <p>Let's actually use the UVs when we create the geometry
  687. and the texture when we make the material</p>
  688. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const {positions, normals, indices} = world.generateGeometryDataForCell(0, 0, 0);
  689. +const {positions, normals, uvs, indices} = world.generateGeometryDataForCell(0, 0, 0);
  690. const geometry = new THREE.BufferGeometry();
  691. -const material = new THREE.MeshLambertMaterial({color: 'green'});
  692. +const material = new THREE.MeshLambertMaterial({
  693. + map: texture,
  694. + side: THREE.DoubleSide,
  695. + alphaTest: 0.1,
  696. + transparent: true,
  697. +});
  698. const positionNumComponents = 3;
  699. const normalNumComponents = 3;
  700. +const uvNumComponents = 2;
  701. geometry.setAttribute(
  702. 'position',
  703. new THREE.BufferAttribute(new Float32Array(positions), positionNumComponents));
  704. geometry.setAttribute(
  705. 'normal',
  706. new THREE.BufferAttribute(new Float32Array(normals), normalNumComponents));
  707. +geometry.setAttribute(
  708. + 'uv',
  709. + new THREE.BufferAttribute(new Float32Array(uvs), uvNumComponents));
  710. geometry.setIndex(indices);
  711. const mesh = new THREE.Mesh(geometry, material);
  712. scene.add(mesh);
  713. </pre>
  714. <p>One last thing, we actually need to set some voxels
  715. to use different textures.</p>
  716. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">for (let y = 0; y &lt; cellSize; ++y) {
  717. for (let z = 0; z &lt; cellSize; ++z) {
  718. for (let x = 0; x &lt; cellSize; ++x) {
  719. const height = (Math.sin(x / cellSize * Math.PI * 2) + Math.sin(z / cellSize * Math.PI * 3)) * (cellSize / 6) + (cellSize / 2);
  720. if (y &lt; height) {
  721. - world.setVoxel(x, y, z, 1);
  722. + world.setVoxel(x, y, z, randInt(1, 17));
  723. }
  724. }
  725. }
  726. }
  727. +function randInt(min, max) {
  728. + return Math.floor(Math.random() * (max - min) + min);
  729. +}
  730. </pre>
  731. <p>and with that we get textures!</p>
  732. <p></p><div translate="no" class="threejs_example_container notranslate">
  733. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/voxel-geometry-culled-faces-with-textures.html"></iframe></div>
  734. <a class="threejs_center" href="/manual/examples/voxel-geometry-culled-faces-with-textures.html" target="_blank">click here to open in a separate window</a>
  735. </div>
  736. <p></p>
  737. <p>Let's make it support more than one cell.</p>
  738. <p>To do this lets store cells in an object using cell ids.
  739. A cell id will just be a cell's coordinates separated by
  740. a comma. In other words if we ask for voxel 35,0,0
  741. that is in cell 1,0,0 so its id is <code class="notranslate" translate="no">"1,0,0"</code>.</p>
  742. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class VoxelWorld {
  743. constructor(options) {
  744. this.cellSize = options.cellSize;
  745. this.tileSize = options.tileSize;
  746. this.tileTextureWidth = options.tileTextureWidth;
  747. this.tileTextureHeight = options.tileTextureHeight;
  748. const {cellSize} = this;
  749. this.cellSliceSize = cellSize * cellSize;
  750. - this.cell = new Uint8Array(cellSize * cellSize * cellSize);
  751. + this.cells = {};
  752. }
  753. + computeCellId(x, y, z) {
  754. + const {cellSize} = this;
  755. + const cellX = Math.floor(x / cellSize);
  756. + const cellY = Math.floor(y / cellSize);
  757. + const cellZ = Math.floor(z / cellSize);
  758. + return `${cellX},${cellY},${cellZ}`;
  759. + }
  760. + getCellForVoxel(x, y, z) {
  761. - const cellX = Math.floor(x / cellSize);
  762. - const cellY = Math.floor(y / cellSize);
  763. - const cellZ = Math.floor(z / cellSize);
  764. - if (cellX !== 0 || cellY !== 0 || cellZ !== 0) {
  765. - return null;
  766. - }
  767. - return this.cell;
  768. + return this.cells[this.computeCellId(x, y, z)];
  769. }
  770. ...
  771. }
  772. </pre>
  773. <p>and now we can make <code class="notranslate" translate="no">setVoxel</code> add new cells if
  774. we try to set a voxel in a cell that does not yet exist</p>
  775. <pre class="prettyprint showlinemods notranslate lang-js" translate="no"> setVoxel(x, y, z, v) {
  776. - const cell = this.getCellForVoxel(x, y, z);
  777. + let cell = this.getCellForVoxel(x, y, z);
  778. if (!cell) {
  779. - return 0;
  780. + cell = this.addCellForVoxel(x, y, z);
  781. }
  782. const voxelOffset = this.computeVoxelOffset(x, y, z);
  783. cell[voxelOffset] = v;
  784. }
  785. + addCellForVoxel(x, y, z) {
  786. + const cellId = this.computeCellId(x, y, z);
  787. + let cell = this.cells[cellId];
  788. + if (!cell) {
  789. + const {cellSize} = this;
  790. + cell = new Uint8Array(cellSize * cellSize * cellSize);
  791. + this.cells[cellId] = cell;
  792. + }
  793. + return cell;
  794. + }
  795. </pre>
  796. <p>Let's make this editable.</p>
  797. <p>First we`ll add a UI. Using radio buttons we can make an 8x2
  798. array of tiles</p>
  799. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;body&gt;
  800. &lt;canvas id="c"&gt;&lt;/canvas&gt;
  801. + &lt;div id="ui"&gt;
  802. + &lt;div class="tiles"&gt;
  803. + &lt;input type="radio" name="voxel" id="voxel1" value="1"&gt;&lt;label for="voxel1" style="background-position: -0% -0%"&gt;&lt;/label&gt;
  804. + &lt;input type="radio" name="voxel" id="voxel2" value="2"&gt;&lt;label for="voxel2" style="background-position: -100% -0%"&gt;&lt;/label&gt;
  805. + &lt;input type="radio" name="voxel" id="voxel3" value="3"&gt;&lt;label for="voxel3" style="background-position: -200% -0%"&gt;&lt;/label&gt;
  806. + &lt;input type="radio" name="voxel" id="voxel4" value="4"&gt;&lt;label for="voxel4" style="background-position: -300% -0%"&gt;&lt;/label&gt;
  807. + &lt;input type="radio" name="voxel" id="voxel5" value="5"&gt;&lt;label for="voxel5" style="background-position: -400% -0%"&gt;&lt;/label&gt;
  808. + &lt;input type="radio" name="voxel" id="voxel6" value="6"&gt;&lt;label for="voxel6" style="background-position: -500% -0%"&gt;&lt;/label&gt;
  809. + &lt;input type="radio" name="voxel" id="voxel7" value="7"&gt;&lt;label for="voxel7" style="background-position: -600% -0%"&gt;&lt;/label&gt;
  810. + &lt;input type="radio" name="voxel" id="voxel8" value="8"&gt;&lt;label for="voxel8" style="background-position: -700% -0%"&gt;&lt;/label&gt;
  811. + &lt;/div&gt;
  812. + &lt;div class="tiles"&gt;
  813. + &lt;input type="radio" name="voxel" id="voxel9" value="9" &gt;&lt;label for="voxel9" style="background-position: -800% -0%"&gt;&lt;/label&gt;
  814. + &lt;input type="radio" name="voxel" id="voxel10" value="10"&gt;&lt;label for="voxel10" style="background-position: -900% -0%"&gt;&lt;/label&gt;
  815. + &lt;input type="radio" name="voxel" id="voxel11" value="11"&gt;&lt;label for="voxel11" style="background-position: -1000% -0%"&gt;&lt;/label&gt;
  816. + &lt;input type="radio" name="voxel" id="voxel12" value="12"&gt;&lt;label for="voxel12" style="background-position: -1100% -0%"&gt;&lt;/label&gt;
  817. + &lt;input type="radio" name="voxel" id="voxel13" value="13"&gt;&lt;label for="voxel13" style="background-position: -1200% -0%"&gt;&lt;/label&gt;
  818. + &lt;input type="radio" name="voxel" id="voxel14" value="14"&gt;&lt;label for="voxel14" style="background-position: -1300% -0%"&gt;&lt;/label&gt;
  819. + &lt;input type="radio" name="voxel" id="voxel15" value="15"&gt;&lt;label for="voxel15" style="background-position: -1400% -0%"&gt;&lt;/label&gt;
  820. + &lt;input type="radio" name="voxel" id="voxel16" value="16"&gt;&lt;label for="voxel16" style="background-position: -1500% -0%"&gt;&lt;/label&gt;
  821. + &lt;/div&gt;
  822. + &lt;/div&gt;
  823. &lt;/body&gt;
  824. </pre>
  825. <p>And add some CSS to style it, display the tiles and highlight
  826. the current selection</p>
  827. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">body {
  828. margin: 0;
  829. }
  830. #c {
  831. width: 100%;
  832. height: 100%;
  833. display: block;
  834. }
  835. +#ui {
  836. + position: absolute;
  837. + left: 10px;
  838. + top: 10px;
  839. + background: rgba(0, 0, 0, 0.8);
  840. + padding: 5px;
  841. +}
  842. +#ui input[type=radio] {
  843. + width: 0;
  844. + height: 0;
  845. + display: none;
  846. +}
  847. +#ui input[type=radio] + label {
  848. + background-image: url('resources/images/minecraft/flourish-cc-by-nc-sa.png');
  849. + background-size: 1600% 400%;
  850. + image-rendering: pixelated;
  851. + width: 64px;
  852. + height: 64px;
  853. + display: inline-block;
  854. +}
  855. +#ui input[type=radio]:checked + label {
  856. + outline: 3px solid red;
  857. +}
  858. +@media (max-width: 600px), (max-height: 600px) {
  859. + #ui input[type=radio] + label {
  860. + width: 32px;
  861. + height: 32px;
  862. + }
  863. +}
  864. </pre>
  865. <p>The UX will be as follows. If no tile is selected and you click a voxel that
  866. voxel will be erased or if you click a voxel and are holding the shift key it
  867. will be erased. Otherwise if a tiles is selected it will be added. You can
  868. deselect the selected tile type by clicking it again.</p>
  869. <p>This code will let the user unselect the highlighted
  870. radio button.</p>
  871. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">let currentVoxel = 0;
  872. let currentId;
  873. document.querySelectorAll('#ui .tiles input[type=radio][name=voxel]').forEach((elem) =&gt; {
  874. elem.addEventListener('click', allowUncheck);
  875. });
  876. function allowUncheck() {
  877. if (this.id === currentId) {
  878. this.checked = false;
  879. currentId = undefined;
  880. currentVoxel = 0;
  881. } else {
  882. currentId = this.id;
  883. currentVoxel = parseInt(this.value);
  884. }
  885. }
  886. </pre>
  887. <p>And this below code will let us set a voxel based on where
  888. the user clicks. It uses code similar to the code we
  889. made in <a href="picking.html">the article on picking</a>
  890. but it's not using the built in <code class="notranslate" translate="no">RayCaster</code>. Instead
  891. it's using <code class="notranslate" translate="no">VoxelWorld.intersectRay</code> which returns
  892. the position of intersection and the normal of the face
  893. hit.</p>
  894. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function getCanvasRelativePosition(event) {
  895. const rect = canvas.getBoundingClientRect();
  896. return {
  897. x: (event.clientX - rect.left) * canvas.width / rect.width,
  898. y: (event.clientY - rect.top ) * canvas.height / rect.height,
  899. };
  900. }
  901. function placeVoxel(event) {
  902. const pos = getCanvasRelativePosition(event);
  903. const x = (pos.x / canvas.width ) * 2 - 1;
  904. const y = (pos.y / canvas.height) * -2 + 1; // note we flip Y
  905. const start = new THREE.Vector3();
  906. const end = new THREE.Vector3();
  907. start.setFromMatrixPosition(camera.matrixWorld);
  908. end.set(x, y, 1).unproject(camera);
  909. const intersection = world.intersectRay(start, end);
  910. if (intersection) {
  911. const voxelId = event.shiftKey ? 0 : currentVoxel;
  912. // the intersection point is on the face. That means
  913. // the math imprecision could put us on either side of the face.
  914. // so go half a normal into the voxel if removing (currentVoxel = 0)
  915. // our out of the voxel if adding (currentVoxel &gt; 0)
  916. const pos = intersection.position.map((v, ndx) =&gt; {
  917. return v + intersection.normal[ndx] * (voxelId &gt; 0 ? 0.5 : -0.5);
  918. });
  919. world.setVoxel(...pos, voxelId);
  920. updateVoxelGeometry(...pos);
  921. requestRenderIfNotRequested();
  922. }
  923. }
  924. const mouse = {
  925. x: 0,
  926. y: 0,
  927. };
  928. function recordStartPosition(event) {
  929. mouse.x = event.clientX;
  930. mouse.y = event.clientY;
  931. mouse.moveX = 0;
  932. mouse.moveY = 0;
  933. }
  934. function recordMovement(event) {
  935. mouse.moveX += Math.abs(mouse.x - event.clientX);
  936. mouse.moveY += Math.abs(mouse.y - event.clientY);
  937. }
  938. function placeVoxelIfNoMovement(event) {
  939. if (mouse.moveX &lt; 5 &amp;&amp; mouse.moveY &lt; 5) {
  940. placeVoxel(event);
  941. }
  942. window.removeEventListener('pointermove', recordMovement);
  943. window.removeEventListener('pointerup', placeVoxelIfNoMovement);
  944. }
  945. canvas.addEventListener('pointerdown', (event) =&gt; {
  946. event.preventDefault();
  947. recordStartPosition(event);
  948. window.addEventListener('pointermove', recordMovement);
  949. window.addEventListener('pointerup', placeVoxelIfNoMovement);
  950. }, {passive: false});
  951. canvas.addEventListener('touchstart', (event) =&gt; {
  952. // stop scrolling
  953. event.preventDefault();
  954. }, {passive: false});
  955. </pre>
  956. <p>There's a lot going on in the code above. Basically the mouse
  957. has a dual purpose. One is to move the camera. The other is to
  958. edit the world. Placing/Erasing a voxel happen when you let off the mouse
  959. but only if you have not moved the mouse since you first pressed down.
  960. This is just a guess that if you did move the mouse you were trying
  961. to move the camera, not place a block. <code class="notranslate" translate="no">moveX</code> and <code class="notranslate" translate="no">moveY</code> are
  962. in absolute movement so if you move to the left 10 and then back to
  963. the right 10 you'll have moved 20 units. In that case the user likely
  964. was just rotating the model back and forth and does not want to
  965. place a block. I didn't do any testing to see if <code class="notranslate" translate="no">5</code> is a good range or not. </p>
  966. <p>In the code we call <code class="notranslate" translate="no">world.setVoxel</code> to set a voxel and
  967. then <code class="notranslate" translate="no">updateVoxelGeometry</code> to update the three.js geometry
  968. based on what's changed.</p>
  969. <p>Let's make that now. If the user clicks a
  970. voxel on the edge of a cell then the geometry for the voxel
  971. in the adjacent cell might need new geometry. This means
  972. we need to check the cell for the voxel we just edited
  973. as well as in all 6 directions from that cell.</p>
  974. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const neighborOffsets = [
  975. [ 0, 0, 0], // self
  976. [-1, 0, 0], // left
  977. [ 1, 0, 0], // right
  978. [ 0, -1, 0], // down
  979. [ 0, 1, 0], // up
  980. [ 0, 0, -1], // back
  981. [ 0, 0, 1], // front
  982. ];
  983. function updateVoxelGeometry(x, y, z) {
  984. const updatedCellIds = {};
  985. for (const offset of neighborOffsets) {
  986. const ox = x + offset[0];
  987. const oy = y + offset[1];
  988. const oz = z + offset[2];
  989. const cellId = world.computeCellId(ox, oy, oz);
  990. if (!updatedCellIds[cellId]) {
  991. updatedCellIds[cellId] = true;
  992. updateCellGeometry(ox, oy, oz);
  993. }
  994. }
  995. }
  996. </pre>
  997. <p>I thought about checking for adjacent cells like </p>
  998. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const voxelX = THREE.MathUtils.euclideanModulo(x, cellSize) | 0;
  999. if (voxelX === 0) {
  1000. // update cell to the left
  1001. } else if (voxelX === cellSize - 1) {
  1002. // update cell to the right
  1003. }
  1004. </pre>
  1005. <p>and there would be 4 more checks for the other 4 directions
  1006. but it occurred to me the code would be much simpler with
  1007. just an array of offsets and saving off the cell ids of
  1008. the cells we already updated. If the updated voxel is not
  1009. on the edge of a cell then the test will quickly reject updating
  1010. the same cell.</p>
  1011. <p>For <code class="notranslate" translate="no">updateCellGeometry</code> we're just going to take the code we
  1012. had before that was generating the geometry for one cell
  1013. and make it handle multiple cells.</p>
  1014. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cellIdToMesh = {};
  1015. function updateCellGeometry(x, y, z) {
  1016. const cellX = Math.floor(x / cellSize);
  1017. const cellY = Math.floor(y / cellSize);
  1018. const cellZ = Math.floor(z / cellSize);
  1019. const cellId = world.computeCellId(x, y, z);
  1020. let mesh = cellIdToMesh[cellId];
  1021. const geometry = mesh ? mesh.geometry : new THREE.BufferGeometry();
  1022. const {positions, normals, uvs, indices} = world.generateGeometryDataForCell(cellX, cellY, cellZ);
  1023. const positionNumComponents = 3;
  1024. geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), positionNumComponents));
  1025. const normalNumComponents = 3;
  1026. geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(normals), normalNumComponents));
  1027. const uvNumComponents = 2;
  1028. geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(uvs), uvNumComponents));
  1029. geometry.setIndex(indices);
  1030. geometry.computeBoundingSphere();
  1031. if (!mesh) {
  1032. mesh = new THREE.Mesh(geometry, material);
  1033. mesh.name = cellId;
  1034. cellIdToMesh[cellId] = mesh;
  1035. scene.add(mesh);
  1036. mesh.position.set(cellX * cellSize, cellY * cellSize, cellZ * cellSize);
  1037. }
  1038. }
  1039. </pre>
  1040. <p>The code above checks a map of cell ids to meshes. If
  1041. we ask for a cell that doesn't exist a new <a href="/docs/#api/en/objects/Mesh"><code class="notranslate" translate="no">Mesh</code></a> is made
  1042. and added to the correct place in world space.
  1043. At the end we update the attributes and indices with the new data.</p>
  1044. <p></p><div translate="no" class="threejs_example_container notranslate">
  1045. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/voxel-geometry-culled-faces-ui.html"></iframe></div>
  1046. <a class="threejs_center" href="/manual/examples/voxel-geometry-culled-faces-ui.html" target="_blank">click here to open in a separate window</a>
  1047. </div>
  1048. <p></p>
  1049. <p>Some notes:</p>
  1050. <p><code class="notranslate" translate="no">RayCaster</code> might have worked just fine. I didn't try it.
  1051. Instead I found <a href="https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.42.3443&rep=rep1&type=pdf">a voxel specific raycaster</a>.
  1052. that is optimized for voxels.</p>
  1053. <p>I made <code class="notranslate" translate="no">intersectRay</code> part of VoxelWorld because it seemed
  1054. like if it gets too slow we could raycast against cells
  1055. before raycasting on voxels as a simple speed up if it becomes
  1056. too slow.</p>
  1057. <p>You might want to change the length of the raycast
  1058. as currently it's all the way to Z-far. I expect if the
  1059. user clicks something too far way they don't really want
  1060. to be placing blocks on the other side of the world that
  1061. are 1 or 2 pixel large.</p>
  1062. <p>Calling <code class="notranslate" translate="no">geometry.computeBoundingSphere</code> might be slow.
  1063. We could just manually set the bounding sphere to the fit
  1064. the entire cell.</p>
  1065. <p>Do we want remove cells if all voxels in that cell are 0?
  1066. That would probably be reasonable change if we wanted to ship this.</p>
  1067. <p>Thinking about how this works it's clear the absolute
  1068. worst case is a checkerboard of on and off voxels. I don't
  1069. know off the top of my head what other strategies to use
  1070. if things get too slow. Maybe getting too slow would just
  1071. encourage the user not to make giant checkerboard areas.</p>
  1072. <p>To keep it simple the texture atlas is just 1 column
  1073. per voxel type. It would be better to make something more
  1074. flexible where we have a table of voxel types and each
  1075. type can specify where its face textures are in the atlas.
  1076. As it is lots of space is wasted.</p>
  1077. <p>Looking at real minecraft there are tiles that are not
  1078. voxels, not cubes. Like a fence tile or flowers. To do that
  1079. we'd again need some table of voxel types and for each
  1080. voxel whether it's a cube or some other geometry. If it's
  1081. not a cube the neighbor check when generating the geometry
  1082. would also need to change. A flower voxel next to another
  1083. voxel should not remove the faces between them.</p>
  1084. <p>If you want to make some minecraft like thing using three.js
  1085. I hope this has given you some ideas where to start and how
  1086. to generate some what efficient geometry.</p>
  1087. <p><canvas id="c"></canvas></p>
  1088. <script type="module" src="../resources/threejs-voxel-geometry.js"></script>
  1089. </div>
  1090. </div>
  1091. </div>
  1092. <script src="/manual/resources/prettify.js"></script>
  1093. <script src="/manual/resources/lesson.js"></script>
  1094. </body></html>