responsive.html 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. <!DOCTYPE html><html lang="en"><head>
  2. <meta charset="utf-8">
  3. <title>Responsive Design</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 – Responsive Design">
  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>Responsive Design</h1>
  28. </div>
  29. <div class="lesson">
  30. <div class="lesson-main">
  31. <p>This is the second article in a series of articles about three.js.
  32. The first article was <a href="fundamentals.html">about fundamentals</a>.
  33. If you haven't read that yet you might want to start there.</p>
  34. <p>This article is about how to make your three.js app be responsive
  35. to any situation. Making a webpage responsive generally refers
  36. to the page displaying well on different sized displays from
  37. desktops to tablets to phones.</p>
  38. <p>For three.js there are even more situations to consider. For
  39. example, a 3D editor with controls on the left, right, top, or
  40. bottom is something we might want to handle. A live diagram
  41. in the middle of a document is another example.</p>
  42. <p>The last sample we had used a plain canvas with no CSS and
  43. no size</p>
  44. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;canvas id="c"&gt;&lt;/canvas&gt;
  45. </pre>
  46. <p>That canvas defaults to 300x150 CSS pixels in size.</p>
  47. <p>In the web platform the recommended way to set the size
  48. of something is to use CSS.</p>
  49. <p>Let's make the canvas fill the page by adding CSS</p>
  50. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;style&gt;
  51. html, body {
  52. margin: 0;
  53. height: 100%;
  54. }
  55. #c {
  56. width: 100%;
  57. height: 100%;
  58. display: block;
  59. }
  60. &lt;/style&gt;
  61. </pre>
  62. <p>In HTML the body has a margin of 5 pixels by default so setting the
  63. margin to 0 removes the margin. Setting the html and body height to 100%
  64. makes them fill the window. Otherwise they are only as large
  65. as the content that fills them.</p>
  66. <p>Next we tell the <code class="notranslate" translate="no">id=c</code> element to be
  67. 100% the size of its container which in this case is the body of
  68. the document.</p>
  69. <p>Finally we set its <code class="notranslate" translate="no">display</code> mode to <code class="notranslate" translate="no">block</code>. A canvas's
  70. default display mode is <code class="notranslate" translate="no">inline</code>. Inline
  71. elements can end up adding whitespace to what is displayed. By
  72. setting the canvas to <code class="notranslate" translate="no">block</code> that issue goes away.</p>
  73. <p>Here's the result</p>
  74. <p></p><div translate="no" class="threejs_example_container notranslate">
  75. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/responsive-no-resize.html"></iframe></div>
  76. <a class="threejs_center" href="/manual/examples/responsive-no-resize.html" target="_blank">click here to open in a separate window</a>
  77. </div>
  78. <p></p>
  79. <p>You can see the canvas is now filling the page but there are 2
  80. problems. One our cubes are stretched. They are not cubes they
  81. are more like boxes. Too tall or too wide. Open the
  82. example in its own window and resize it. You'll see how
  83. the cubes get stretched wide and tall.</p>
  84. <p><img src="../resources/images/resize-incorrect-aspect.png" width="407" class="threejs_center nobg"></p>
  85. <p>The second problem is they look low resolution or blocky and
  86. blurry. Stretch the window really large and you'll really see
  87. the issue.</p>
  88. <p><img src="../resources/images/resize-low-res.png" class="threejs_center nobg"></p>
  89. <p>Let's fix the stretchy problem first. To do that we need
  90. to set the aspect of the camera to the aspect of the canvas's
  91. display size. We can do that by looking at the canvas's
  92. <code class="notranslate" translate="no">clientWidth</code> and <code class="notranslate" translate="no">clientHeight</code> properties.</p>
  93. <p>We'll update our render loop like this</p>
  94. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
  95. time *= 0.001;
  96. + const canvas = renderer.domElement;
  97. + camera.aspect = canvas.clientWidth / canvas.clientHeight;
  98. + camera.updateProjectionMatrix();
  99. ...
  100. </pre>
  101. <p>Now the cubes should stop being distorted.</p>
  102. <p></p><div translate="no" class="threejs_example_container notranslate">
  103. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/responsive-update-camera.html"></iframe></div>
  104. <a class="threejs_center" href="/manual/examples/responsive-update-camera.html" target="_blank">click here to open in a separate window</a>
  105. </div>
  106. <p></p>
  107. <p>Open the example in a separate window and resize the window
  108. and you should see the cubes are no longer stretched tall or wide.
  109. They stay the correct aspect regardless of window size.</p>
  110. <p><img src="../resources/images/resize-correct-aspect.png" width="407" class="threejs_center nobg"></p>
  111. <p>Now let's fix the blockiness.</p>
  112. <p>Canvas elements have 2 sizes. One size is the size the canvas is displayed
  113. on the page. That's what we set with CSS. The other size is the
  114. number of pixels in the canvas itself. This is no different than an image.
  115. For example we might have a 128x64 pixel image and using
  116. CSS we might display as 400x200 pixels.</p>
  117. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;img src="some128x64image.jpg" style="width:400px; height:200px"&gt;
  118. </pre>
  119. <p>A canvas's internal size, its resolution, is often called its drawingbuffer size.
  120. In three.js we can set the canvas's drawingbuffer size by calling <code class="notranslate" translate="no">renderer.setSize</code>.
  121. What size should we pick? The most obvious answer is "the same size the canvas is displayed".
  122. Again, to do that we can look at the canvas's <code class="notranslate" translate="no">clientWidth</code> and <code class="notranslate" translate="no">clientHeight</code>
  123. properties.</p>
  124. <p>Let's write a function that checks if the renderer's canvas is not
  125. already the size it is being displayed as and if so set its size.</p>
  126. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function resizeRendererToDisplaySize(renderer) {
  127. const canvas = renderer.domElement;
  128. const width = canvas.clientWidth;
  129. const height = canvas.clientHeight;
  130. const needResize = canvas.width !== width || canvas.height !== height;
  131. if (needResize) {
  132. renderer.setSize(width, height, false);
  133. }
  134. return needResize;
  135. }
  136. </pre>
  137. <p>Notice we check if the canvas actually needs to be resized. Resizing the canvas
  138. is an interesting part of the canvas spec and it's best not to set the same
  139. size if it's already the size we want.</p>
  140. <p>Once we know if we need to resize or not we then call <code class="notranslate" translate="no">renderer.setSize</code> and
  141. pass in the new width and height. It's important to pass <code class="notranslate" translate="no">false</code> at the end.
  142. <code class="notranslate" translate="no">render.setSize</code> by default sets the canvas's CSS size but doing so is not
  143. what we want. We want the browser to continue to work how it does for all other
  144. elements which is to use CSS to determine the display size of the element. We don't
  145. want canvases used by three to be different than other elements.</p>
  146. <p>Note that our function returns true if the canvas was resized. We can use
  147. this to check if there are other things we should update. Let's modify
  148. our render loop to use the new function</p>
  149. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
  150. time *= 0.001;
  151. + if (resizeRendererToDisplaySize(renderer)) {
  152. + const canvas = renderer.domElement;
  153. + camera.aspect = canvas.clientWidth / canvas.clientHeight;
  154. + camera.updateProjectionMatrix();
  155. + }
  156. ...
  157. </pre>
  158. <p>Since the aspect is only going to change if the canvas's display size
  159. changed we only set the camera's aspect if <code class="notranslate" translate="no">resizeRendererToDisplaySize</code>
  160. returns <code class="notranslate" translate="no">true</code>.</p>
  161. <p></p><div translate="no" class="threejs_example_container notranslate">
  162. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/responsive.html"></iframe></div>
  163. <a class="threejs_center" href="/manual/examples/responsive.html" target="_blank">click here to open in a separate window</a>
  164. </div>
  165. <p></p>
  166. <p>It should now render with a resolution that matches the display
  167. size of the canvas.</p>
  168. <p>To make the point about letting CSS handle the resizing let's take
  169. our code and put it in a <a href="../examples/threejs-responsive.js">separate <code class="notranslate" translate="no">.js</code> file</a>.
  170. Here then are a few more examples where we let CSS choose the size and notice we had
  171. to change zero code for them to work.</p>
  172. <p>Let's put our cubes in the middle of a paragraph of text.</p>
  173. <p></p><div translate="no" class="threejs_example_container notranslate">
  174. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/responsive-paragraph.html&amp;startPane=html"></iframe></div>
  175. <a class="threejs_center" href="/manual/examples/responsive-paragraph.html" target="_blank">click here to open in a separate window</a>
  176. </div>
  177. <p></p>
  178. <p>and here's our same code used in an editor style layout
  179. where the control area on the right can be resized.</p>
  180. <p></p><div translate="no" class="threejs_example_container notranslate">
  181. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/responsive-editor.html&amp;startPane=html"></iframe></div>
  182. <a class="threejs_center" href="/manual/examples/responsive-editor.html" target="_blank">click here to open in a separate window</a>
  183. </div>
  184. <p></p>
  185. <p>The important part to notice is no code changed. Only our HTML and CSS
  186. changed.</p>
  187. <h2 id="handling-hd-dpi-displays">Handling HD-DPI displays</h2>
  188. <p>HD-DPI stands for high-density dot per inch displays.
  189. That's most Macs nowadays and many Windows machines
  190. as well as pretty much all smartphones.</p>
  191. <p>The way this works in the browser is they use
  192. CSS pixels to set the sizes which are supposed to be the same
  193. regardless of how high res the display is. The browser
  194. will just render text with more detail but the
  195. same physical size.</p>
  196. <p>There are various ways to handle HD-DPI with three.js.</p>
  197. <p>The first one is just not to do anything special. This
  198. is arguably the most common. Rendering 3D graphics
  199. takes a lot of GPU processing power. Mobile GPUs have
  200. less power than desktops, at least as of 2018, and yet
  201. mobile phones often have very high resolution displays.
  202. The current top of the line phones have an HD-DPI ratio
  203. of 3x meaning for every one pixel from a non-HD-DPI display
  204. those phones have 9 pixels. That means they have to do 9x
  205. the rendering.</p>
  206. <p>Computing 9x the pixels is a lot of work so if we just
  207. leave the code as it is we'll compute 1x the pixels and the
  208. browser will just draw it at 3x the size (3x by 3x = 9x pixels).</p>
  209. <p>For any heavy three.js app that's probably what you want
  210. otherwise you're likely to get a slow framerate.</p>
  211. <p>That said if you actually do want to render at the resolution
  212. of the device there are a couple of ways to do this in three.js.</p>
  213. <p>One is to tell three.js a resolution multiplier using <code class="notranslate" translate="no">renderer.setPixelRatio</code>.
  214. You ask the browser what the multiplier is from CSS pixels to device pixels
  215. and pass that to three.js</p>
  216. <pre class="prettyprint showlinemods notranslate notranslate" translate="no"> renderer.setPixelRatio(window.devicePixelRatio);
  217. </pre><p>After that any calls to <code class="notranslate" translate="no">renderer.setSize</code> will magically
  218. use the size you request multiplied by whatever pixel ratio
  219. you passed in. <strong>This is strongly NOT RECOMMENDED</strong>. See below</p>
  220. <p>The other way is to do it yourself when you resize the canvas.</p>
  221. <pre class="prettyprint showlinemods notranslate lang-js" translate="no"> function resizeRendererToDisplaySize(renderer) {
  222. const canvas = renderer.domElement;
  223. const pixelRatio = window.devicePixelRatio;
  224. const width = canvas.clientWidth * pixelRatio | 0;
  225. const height = canvas.clientHeight * pixelRatio | 0;
  226. const needResize = canvas.width !== width || canvas.height !== height;
  227. if (needResize) {
  228. renderer.setSize(width, height, false);
  229. }
  230. return needResize;
  231. }
  232. </pre>
  233. <p>This second way is objectively better. Why? Because it means I get what I ask for.
  234. There are many cases when using three.js where we need to know the actual
  235. size of the canvas's drawingBuffer. For example when making a post processing filter,
  236. or if we are making a shader that accesses <code class="notranslate" translate="no">gl_FragCoord</code>, if we are making
  237. a screenshot, or reading pixels for GPU picking, for drawing into a 2D canvas,
  238. etc... There many many cases where if we use <code class="notranslate" translate="no">setPixelRatio</code> then our actual size will be different
  239. than the size we requested and we'll have to guess when to use the size
  240. we asked for and when to use the size three.js is actually using.
  241. By doing it ourselves we always know the size being used is the size we requested.
  242. There is no special case where magic is happening behind the scenes.</p>
  243. <p>Here's an example using the code above.</p>
  244. <p></p><div translate="no" class="threejs_example_container notranslate">
  245. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/responsive-hd-dpi.html"></iframe></div>
  246. <a class="threejs_center" href="/manual/examples/responsive-hd-dpi.html" target="_blank">click here to open in a separate window</a>
  247. </div>
  248. <p></p>
  249. <p>It might be hard to see the difference but if you have an HD-DPI
  250. display and you compare this sample to those above you should
  251. notice the edges are more crisp.</p>
  252. <p>This article covered a very basic but fundamental topic. Next up lets quickly
  253. <a href="primitives.html">go over the basic primitives that three.js provides</a>.</p>
  254. </div>
  255. </div>
  256. </div>
  257. <script src="/manual/resources/prettify.js"></script>
  258. <script src="/manual/resources/lesson.js"></script>
  259. </body></html>