billboards.html 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. <!DOCTYPE html><html lang="en"><head>
  2. <meta charset="utf-8">
  3. <title>Billboards</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 – Billboards">
  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="../resources/lesson.css">
  12. <link rel="stylesheet" href="../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>Billboards</h1>
  28. </div>
  29. <div class="lesson">
  30. <div class="lesson-main">
  31. <p>In <a href="canvas-textures.html">a previous article</a> we used a <a href="/docs/#api/en/textures/CanvasTexture"><code class="notranslate" translate="no">CanvasTexture</code></a>
  32. to make labels / badges on characters. Sometimes we'd like to make labels or
  33. other things that always face the camera. Three.js provides the <a href="/docs/#api/en/objects/Sprite"><code class="notranslate" translate="no">Sprite</code></a> and
  34. <a href="/docs/#api/en/materials/SpriteMaterial"><code class="notranslate" translate="no">SpriteMaterial</code></a> to make this happen.</p>
  35. <p>Let's change the badge example from <a href="canvas-textures.html">the article on canvas textures</a>
  36. to use <a href="/docs/#api/en/objects/Sprite"><code class="notranslate" translate="no">Sprite</code></a> and <a href="/docs/#api/en/materials/SpriteMaterial"><code class="notranslate" translate="no">SpriteMaterial</code></a></p>
  37. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function makePerson(x, labelWidth, size, name, color) {
  38. const canvas = makeLabelCanvas(labelWidth, size, name);
  39. const texture = new THREE.CanvasTexture(canvas);
  40. // because our canvas is likely not a power of 2
  41. // in both dimensions set the filtering appropriately.
  42. texture.minFilter = THREE.LinearFilter;
  43. texture.wrapS = THREE.ClampToEdgeWrapping;
  44. texture.wrapT = THREE.ClampToEdgeWrapping;
  45. - const labelMaterial = new THREE.MeshBasicMaterial({
  46. + const labelMaterial = new THREE.SpriteMaterial({
  47. map: texture,
  48. - side: THREE.DoubleSide,
  49. transparent: true,
  50. });
  51. const root = new THREE.Object3D();
  52. root.position.x = x;
  53. const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
  54. root.add(body);
  55. body.position.y = bodyHeight / 2;
  56. const head = new THREE.Mesh(headGeometry, bodyMaterial);
  57. root.add(head);
  58. head.position.y = bodyHeight + headRadius * 1.1;
  59. - const label = new THREE.Mesh(labelGeometry, labelMaterial);
  60. + const label = new THREE.Sprite(labelMaterial);
  61. root.add(label);
  62. label.position.y = bodyHeight * 4 / 5;
  63. label.position.z = bodyRadiusTop * 1.01;
  64. </pre>
  65. <p>and the labels now always face the camera</p>
  66. <p></p><div translate="no" class="threejs_example_container notranslate">
  67. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/billboard-labels-w-sprites.html"></iframe></div>
  68. <a class="threejs_center" href="/manual/examples/billboard-labels-w-sprites.html" target="_blank">click here to open in a separate window</a>
  69. </div>
  70. <p></p>
  71. <p>One problem is from certain angles the labels now intersect the
  72. characters. </p>
  73. <div class="threejs_center"><img src="../resources/images/billboard-label-z-issue.png" style="width: 455px;"></div>
  74. <p>We can move the position of the labels to fix.</p>
  75. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+// if units are meters then 0.01 here makes size
  76. +// of the label into centimeters.
  77. +const labelBaseScale = 0.01;
  78. const label = new THREE.Sprite(labelMaterial);
  79. root.add(label);
  80. -label.position.y = bodyHeight * 4 / 5;
  81. -label.position.z = bodyRadiusTop * 1.01;
  82. +label.position.y = head.position.y + headRadius + size * labelBaseScale;
  83. -// if units are meters then 0.01 here makes size
  84. -// of the label into centimeters.
  85. -const labelBaseScale = 0.01;
  86. label.scale.x = canvas.width * labelBaseScale;
  87. label.scale.y = canvas.height * labelBaseScale;
  88. </pre>
  89. <p></p><div translate="no" class="threejs_example_container notranslate">
  90. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/billboard-labels-w-sprites-adjust-height.html"></iframe></div>
  91. <a class="threejs_center" href="/manual/examples/billboard-labels-w-sprites-adjust-height.html" target="_blank">click here to open in a separate window</a>
  92. </div>
  93. <p></p>
  94. <p>Another thing we can do with billboards is draw facades.</p>
  95. <p>Instead of drawing 3D objects we draw 2D planes with an image
  96. of 3D objects. This is often faster than drawing 3D objects.</p>
  97. <p>For example let's make a scene with grid of trees. We'll make each
  98. tree from a cylinder for the base and a cone for the top.</p>
  99. <p>First we make the cone and cylinder geometry and materials that
  100. all the trees will share</p>
  101. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const trunkRadius = .2;
  102. const trunkHeight = 1;
  103. const trunkRadialSegments = 12;
  104. const trunkGeometry = new THREE.CylinderGeometry(
  105. trunkRadius, trunkRadius, trunkHeight, trunkRadialSegments);
  106. const topRadius = trunkRadius * 4;
  107. const topHeight = trunkHeight * 2;
  108. const topSegments = 12;
  109. const topGeometry = new THREE.ConeGeometry(
  110. topRadius, topHeight, topSegments);
  111. const trunkMaterial = new THREE.MeshPhongMaterial({color: 'brown'});
  112. const topMaterial = new THREE.MeshPhongMaterial({color: 'green'});
  113. </pre>
  114. <p>Then we'll make a function that makes a <a href="/docs/#api/en/objects/Mesh"><code class="notranslate" translate="no">Mesh</code></a> each for the trunk and top
  115. of a tree and parents both to an <a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>.</p>
  116. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function makeTree(x, z) {
  117. const root = new THREE.Object3D();
  118. const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial);
  119. trunk.position.y = trunkHeight / 2;
  120. root.add(trunk);
  121. const top = new THREE.Mesh(topGeometry, topMaterial);
  122. top.position.y = trunkHeight + topHeight / 2;
  123. root.add(top);
  124. root.position.set(x, 0, z);
  125. scene.add(root);
  126. return root;
  127. }
  128. </pre>
  129. <p>Then we'll make a loop to place a grid of trees.</p>
  130. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">for (let z = -50; z &lt;= 50; z += 10) {
  131. for (let x = -50; x &lt;= 50; x += 10) {
  132. makeTree(x, z);
  133. }
  134. }
  135. </pre>
  136. <p>Let's also add a ground plane while we're at it</p>
  137. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">// add ground
  138. {
  139. const size = 400;
  140. const geometry = new THREE.PlaneGeometry(size, size);
  141. const material = new THREE.MeshPhongMaterial({color: 'gray'});
  142. const mesh = new THREE.Mesh(geometry, material);
  143. mesh.rotation.x = Math.PI * -0.5;
  144. scene.add(mesh);
  145. }
  146. </pre>
  147. <p>and change the background to light blue</p>
  148. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const scene = new THREE.Scene();
  149. -scene.background = new THREE.Color('white');
  150. +scene.background = new THREE.Color('lightblue');
  151. </pre>
  152. <p>and we get a grid of trees</p>
  153. <p></p><div translate="no" class="threejs_example_container notranslate">
  154. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/billboard-trees-no-billboards.html"></iframe></div>
  155. <a class="threejs_center" href="/manual/examples/billboard-trees-no-billboards.html" target="_blank">click here to open in a separate window</a>
  156. </div>
  157. <p></p>
  158. <p>There are 11x11 or 121 trees. Each tree is made from a 12 polygon
  159. cone and a 48 polygon trunk so each tree is 60 polygons. 121 * 60
  160. is 7260 polygons. That's not that many but of course a more detailed
  161. 3D tree might be 1000-3000 polygons. If they were 3000 polygons each
  162. then 121 trees would be 363000 polygons to draw.</p>
  163. <p>Using facades we can bring that number down.</p>
  164. <p>We could manually create a facade in some painting program but let's write
  165. some code to try to generate one.</p>
  166. <p>Let's write some code to render an object to a texture
  167. using a <code class="notranslate" translate="no">RenderTarget</code>. We covered rendering to a <code class="notranslate" translate="no">RenderTarget</code>
  168. in <a href="rendertargets.html">the article on render targets</a>.</p>
  169. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
  170. const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
  171. const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
  172. const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
  173. camera.position.copy(boxCenter);
  174. camera.position.z += distance;
  175. // pick some near and far values for the frustum that
  176. // will contain the box.
  177. camera.near = boxSize / 100;
  178. camera.far = boxSize * 100;
  179. camera.updateProjectionMatrix();
  180. }
  181. function makeSpriteTexture(textureSize, obj) {
  182. const rt = new THREE.WebGLRenderTarget(textureSize, textureSize);
  183. const aspect = 1; // because the render target is square
  184. const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  185. scene.add(obj);
  186. // compute the box that contains obj
  187. const box = new THREE.Box3().setFromObject(obj);
  188. const boxSize = box.getSize(new THREE.Vector3());
  189. const boxCenter = box.getCenter(new THREE.Vector3());
  190. // set the camera to frame the box
  191. const fudge = 1.1;
  192. const size = Math.max(...boxSize.toArray()) * fudge;
  193. frameArea(size, size, boxCenter, camera);
  194. renderer.autoClear = false;
  195. renderer.setRenderTarget(rt);
  196. renderer.render(scene, camera);
  197. renderer.setRenderTarget(null);
  198. renderer.autoClear = true;
  199. scene.remove(obj);
  200. return {
  201. position: boxCenter.multiplyScalar(fudge),
  202. scale: size,
  203. texture: rt.texture,
  204. };
  205. }
  206. </pre>
  207. <p>Some things to note about the code above:</p>
  208. <p>We're using the field of view (<code class="notranslate" translate="no">fov</code>) defined above this code.</p>
  209. <p>We're computing a box that contains the tree the same way
  210. we did in <a href="load-obj.html">the article on loading a .obj file</a>
  211. with a few minor changes.</p>
  212. <p>We call <code class="notranslate" translate="no">frameArea</code> again adapted <a href="load-obj.html">the article on loading a .obj file</a>.
  213. In this case we compute how far the camera needs to be away from the object
  214. given its field of view to contain the object. We then position the camera -z that distance
  215. from the center of the box that contains the object.</p>
  216. <p>We multiply the size we want to fit by 1.1 (<code class="notranslate" translate="no">fudge</code>) to make sure the tree fits
  217. completely in the render target. The issue here is the size we're using to
  218. calculate if the object fits in the camera's view is not taking into account
  219. that the very edges of the object will end up dipping outside area we
  220. calculated. We could compute how to make 100% of the box fit but that would
  221. waste space as well so instead we just <em>fudge</em> it.</p>
  222. <p>Then we render to the render target and remove the object from
  223. the scene. </p>
  224. <p>It's important to note we need the lights in the scene but we
  225. need to make sure nothing else is in the scene.</p>
  226. <p>We also need to not set a background color on the scene</p>
  227. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const scene = new THREE.Scene();
  228. -scene.background = new THREE.Color('lightblue');
  229. </pre>
  230. <p>Finally we've made the texture we return it and the position and scale we
  231. need to make the facade so that it will appear to be in the same place.</p>
  232. <p>We then make a tree and call this code and pass it in</p>
  233. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">// make billboard texture
  234. const tree = makeTree(0, 0);
  235. const facadeSize = 64;
  236. const treeSpriteInfo = makeSpriteTexture(facadeSize, tree);
  237. </pre>
  238. <p>We can then make a grid of facades instead of a grid of tree models</p>
  239. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function makeSprite(spriteInfo, x, z) {
  240. + const {texture, offset, scale} = spriteInfo;
  241. + const mat = new THREE.SpriteMaterial({
  242. + map: texture,
  243. + transparent: true,
  244. + });
  245. + const sprite = new THREE.Sprite(mat);
  246. + scene.add(sprite);
  247. + sprite.position.set(
  248. + offset.x + x,
  249. + offset.y,
  250. + offset.z + z);
  251. + sprite.scale.set(scale, scale, scale);
  252. +}
  253. for (let z = -50; z &lt;= 50; z += 10) {
  254. for (let x = -50; x &lt;= 50; x += 10) {
  255. - makeTree(x, z);
  256. + makeSprite(treeSpriteInfo, x, z);
  257. }
  258. }
  259. </pre>
  260. <p>In the code above we apply the offset and scale needed to position the facade so it
  261. appears the same place the original tree would have appeared.</p>
  262. <p>Now that we're done making the tree facade texture we can set the background again</p>
  263. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">scene.background = new THREE.Color('lightblue');
  264. </pre>
  265. <p>and now we get a scene of tree facades</p>
  266. <p></p><div translate="no" class="threejs_example_container notranslate">
  267. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/billboard-trees-static-billboards.html"></iframe></div>
  268. <a class="threejs_center" href="/manual/examples/billboard-trees-static-billboards.html" target="_blank">click here to open in a separate window</a>
  269. </div>
  270. <p></p>
  271. <p>Compare to the trees models above and you can see it looks fairly similar.
  272. We used a low-res texture, just 64x64 pixels so the facades are blocky.
  273. You could increase the resolution. Often facades are used only in the far
  274. distance when they are fairly small so a low-res texture is enough and
  275. it saves on drawing detailed trees that are only a few pixels big when
  276. far away.</p>
  277. <p>Another issue is we are only viewing the tree from one side. This is often
  278. solved by rendering more facades, say from 8 directions around the object
  279. and then setting which facade to show based on which direction the camera
  280. is looking at the facade.</p>
  281. <p>Whether or not you use facades is up to you but hopefully this article
  282. gave you some ideas and suggested some solutions if you decide to use them.</p>
  283. </div>
  284. </div>
  285. </div>
  286. <script src="../resources/prettify.js"></script>
  287. <script src="../resources/lesson.js"></script>
  288. </body></html>