offscreencanvas.html 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974
  1. <!DOCTYPE html><html lang="ko"><head>
  2. <meta charset="utf-8">
  3. <title>OffscreenCanvas</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 – OffscreenCanvas">
  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. <link rel="stylesheet" href="/manual/ko/lang.css">
  24. </head>
  25. <body>
  26. <div class="container">
  27. <div class="lesson-title">
  28. <h1>OffscreenCanvas</h1>
  29. </div>
  30. <div class="lesson">
  31. <div class="lesson-main">
  32. <p><a href="https://developer.mozilla.org/ko/docs/Web/API/OffscreenCanvas"><code class="notranslate" translate="no">OffscreenCanvas</code></a>는 비교적 최근 도입된 브라우저 API로 아직 크로미움 기반 브라우저에서만 사용가능하지만, 갈수록 대부분의 브라우저에서 이 API를 사용할 수 있을 겁니다. <code class="notranslate" translate="no">OffscreenCanvas</code>를 이용하면 <a href="https://developer.mozilla.org/ko/docs/Web/API/Web_Workers_API">웹 워커(Web Worker)</a>에서 캔버스를 렌더링해 복잡한 3D 장면 등의 무거운 작업을 별도 프로세스에서 처리할 수 있습니다. 이러면 무거운 작업을 처리할 때 브라우저가 덜 버벅이도록 할 수 있죠. 또한 데이터도 워커에서 불러와 처리하므로 페이지 초기 로드 시 버벅임을 훨씬 줄일 수 있습니다.</p>
  33. <p>사용법은 꽤나 직관적입니다. 먼저 <a href="responsive.html">반응형 디자인에 관한 글</a>에서 썼던 예제를 가져오도록 하죠.</p>
  34. <p>이 사이트 대부분의 예제는 스크립트를 해당 HTML 파일에 인라인으로 작성했습니다. 반면에 워커는 일반적으로 별도의 스크립트 파일로 분리해 작성합니다.</p>
  35. <p>이 글에서는 <code class="notranslate" translate="no">offscreencanvas-cubes.js</code>라는 별도 파일을 만들어 <a href="responsive.html">반응형 디자인에서 가져온 예제</a>의 자바스크립트 코드를 전부 복사해 넣을 겁니다. 그런 다음 바꿔야할 부분을 바꿔보도록 하죠.</p>
  36. <p>하지만 여전히 HTML 파일에 약간의 자바스크립트 코드가 필요합니다. 캔버스 요소를 참조하고 <code class="notranslate" translate="no">canvas.transferControlToOffscreen</code> 메서드를 호출해 캔버스의 제어권을 <code class="notranslate" translate="no">offscreen</code>에 넘겨줍니다.</p>
  37. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
  38. const canvas = document.querySelector('#c');
  39. const offscreen = canvas.transferControlToOffscreen();
  40. ...
  41. </pre>
  42. <p>그리고 <code class="notranslate" translate="no">new Worker(워커 스크립트 경로, { type: 'module' })</code>로 워커를 생성한 뒤, 워커에 <code class="notranslate" translate="no">offscreen</code> 객체를 넘깁니다.</p>
  43. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
  44. const canvas = document.querySelector('#c');
  45. const offscreen = canvas.transferControlToOffscreen();
  46. const worker = new Worker('offscreencanvas-cubes.js', { type: 'module' });
  47. worker.postMessage({ type: 'main', canvas: offscreen }, [ offscreen ]);
  48. }
  49. main();
  50. </pre>
  51. <p>스크립트를 따로 써야 하는 이유는 워커 안에서는 <code class="notranslate" translate="no">DOM</code>에 접근할 수 없기 때문입니다. HTML 요소를 참조하거나 DOM 요소의 이벤트를 받을 수도 없죠. 일반적으로 메시지 이벤트를 통해서만 다른 스크립트와 통신할 수 있습니다.</p>
  52. <p>워커에 메시지를 보내려면 <a href="https://developer.mozilla.org/ko/docs/Web/API/Worker/postMessage"><code class="notranslate" translate="no">worker.postMessage</code></a>에 하나 또는 두 개의 인자를 넘겨주어 호출하면 됩니다. 첫 번째 인자는 워커에 전달할 객체로, 이 객체는 그대로 전달되지 않고 <a href="https://developer.mozilla.org/ko/docs/Web/API/Web_Workers_API/Structured_clone_algorithm">복사됩니다</a>. 두 번째 인자는 옵션으로 첫 번째 인자 중 그대로 <em>전달하기</em> 원하는 객체를 배열로 지정합니다. 여기에 지정한 객체는 복사되지 않죠. 그대로 워커에 <em>전달되지만</em> 객체를 보낸 스크립트에서는 더 이상 사용이 불가능해집니다. 이것도 모든 객체를 전달할 수 있는 게 아니라 특정 타입의 객체만을 전달할 수 있죠. 당연하게도 이 중에는 <code class="notranslate" translate="no">OffscreenCanvas</code>도 있습니다. 정리하자면 <code class="notranslate" translate="no">offscreen</code> 객체를 전달하고 나면 이 객체는 이 스크립트에서 더 이상 쓸모가 없어집니다.</p>
  53. <p>워커의 <code class="notranslate" translate="no">message</code> 이벤트를 이용하면 메시지를 받을 수 있습니다. <code class="notranslate" translate="no">postMessage</code>에서 넘긴 객체는 <code class="notranslate" translate="no">event.data</code>에 담겨 리스너에 전달되죠. 아까 위 코드에서는 <code class="notranslate" translate="no">type: 'main'</code> 속성을 객체에 선언해 워커에 넘겨줬습니다. 이 <code class="notranslate" translate="no">type</code> 속성은 브라우저의 메인 스레드에서는 쓸 일이 없는 값으로, 워커 내에 다른 함수를 호출하는 키값으로 사용할 겁니다. 이러면 메인 스크립트에서 워커 내의 함수를 호출하기가 훨씬 쉬워지겠죠.</p>
  54. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const handlers = {
  55. main,
  56. };
  57. self.onmessage = function(e) {
  58. const fn = handlers[e.data.type];
  59. if (typeof fn !== 'function') {
  60. throw new Error('no handler for type: ' + e.data.type);
  61. }
  62. fn(e.data);
  63. };
  64. </pre>
  65. <p><code class="notranslate" translate="no">type</code> 값을 통해 호출할 함수를 찾고, 함수가 있다면 메인 스크립트에서 넘어온 <code class="notranslate" translate="no">data</code>를 인자로 넘겨 호출하도록 했습니다.</p>
  66. <p>이제 <a href="responsive.html">반응형 디자인에 관한 글</a>에서 가져온 예제의 <code class="notranslate" translate="no">main</code> 함수를 수정해야 합니다.</p>
  67. <p>DOM에서 캔버스에 접근하는 대신 이벤트의 <code class="notranslate" translate="no">data</code> 속성에서 캔버스 요소를 받도록 합니다.</p>
  68. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function main() {
  69. - const canvas = document.querySelector('#c');
  70. +function main(data) {
  71. + const { canvas } = data;
  72. const renderer = new THREE.WebGLRenderer({ canvas });
  73. ...
  74. </pre>
  75. <p>워커에서는 DOM에 접근할 수 없다고 했었죠. 마찬가지로 DOM 속성인 <code class="notranslate" translate="no">canvas.clientWidth</code>나 <code class="notranslate" translate="no">canvas.clientHeight</code>에도 접근할 수 없습니다. <code class="notranslate" translate="no">resizeRendererToDisplaySize</code>를 그대로 사용할 수 없는 것이죠.</p>
  76. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function resizeRendererToDisplaySize(renderer) {
  77. const canvas = renderer.domElement;
  78. const width = canvas.clientWidth;
  79. const height = canvas.clientHeight;
  80. const needResize = canvas.width !== width || canvas.height !== height;
  81. if (needResize) {
  82. renderer.setSize(width, height, false);
  83. }
  84. return needResize;
  85. }
  86. </pre>
  87. <p>대신 캔버스 크기가 변경될 때마다 워커에 메시지를 보낼 겁니다. 워커에 전역 변수를 하나 생성해 여기에 width와 height 값을 지정하도록 하죠.</p>
  88. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const state = {
  89. width: 300, // 캔버스 기본값
  90. height: 150, // 캔버스 기본값
  91. };
  92. </pre>
  93. <p>그리고 <code class="notranslate" translate="no">size</code>라는 함수를 만들어 해당 값을 업데이트하도록 합니다.</p>
  94. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function size(data) {
  95. + state.width = data.width;
  96. + state.height = data.height;
  97. +}
  98. const handlers = {
  99. main,
  100. + size,
  101. };
  102. </pre>
  103. <p><code class="notranslate" translate="no">resizeRendererToDisplaySize</code>가 <code class="notranslate" translate="no">state.width</code>와 <code class="notranslate" translate="no">state.height</code>를 쓰도록 변경합니다.</p>
  104. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function resizeRendererToDisplaySize(renderer) {
  105. const canvas = renderer.domElement;
  106. - const width = canvas.clientWidth;
  107. - const height = canvas.clientHeight;
  108. + const width = state.width;
  109. + const height = state.height;
  110. const needResize = canvas.width !== width || canvas.height !== height;
  111. if (needResize) {
  112. renderer.setSize(width, height, false);
  113. }
  114. return needResize;
  115. }
  116. </pre>
  117. <p>마찬가지로 종횡비를 계산하는 코드도 DOM 속성 대신 <code class="notranslate" translate="no">state</code>를 쓰도록 변경합니다.</p>
  118. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
  119. time *= 0.001;
  120. if (resizeRendererToDisplaySize(renderer)) {
  121. - camera.aspect = canvas.clientWidth / canvas.clientHeight;
  122. + camera.aspect = state.width / state.height;
  123. camera.updateProjectionMatrix();
  124. }
  125. ...
  126. </pre>
  127. <p>메인 스크립트로 돌아와 페이지 크기가 바뀔 때마다 워커의 <code class="notranslate" translate="no">size</code> 함수를 실행하도록 합니다.</p>
  128. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const worker = new Worker('offscreencanvas-picking.js', { type: 'module' });
  129. worker.postMessage({ type: 'main', canvas: offscreen }, [ offscreen ]);
  130. +function sendSize() {
  131. + worker.postMessage({
  132. + type: 'size',
  133. + width: canvas.clientWidth,
  134. + height: canvas.clientHeight,
  135. + });
  136. +}
  137. +
  138. +window.addEventListener('resize', sendSize);
  139. +sendSize();
  140. </pre>
  141. <p>또한 직접 호출해 최초에 한 번 값을 보내도록 합니다.</p>
  142. <p>여러분의 브라우저가 <code class="notranslate" translate="no">OffscreenCanvas</code>를 완벽히 지원한다면 이제 문제 없이 작동할 겁니다. 그렇게 많이 바꾼 것이 없는데도 말이죠. 혹시 모르니 브라우저가 <code class="notranslate" translate="no">OffscreenCanvas</code>를 지원하지 않을 경우 에러 메시지를 보여주도록 하겠습니다. 먼저 에러 메시지를 표시할 HTML을 작성합니다.</p>
  143. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;body&gt;
  144. &lt;canvas id="c"&gt;&lt;/canvas&gt;
  145. + &lt;div id="noOffscreenCanvas" style="display:none;"&gt;
  146. + &lt;div&gt;no OffscreenCanvas support&lt;/div&gt;
  147. + &lt;/div&gt;
  148. &lt;/body&gt;
  149. </pre>
  150. <p>간단한 스타일도 넣어주도록 하죠.</p>
  151. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#noOffscreenCanvas {
  152. display: flex;
  153. width: 100%;
  154. height: 100%;
  155. align-items: center;
  156. justify-content: center;
  157. background: red;
  158. color: white;
  159. }
  160. </pre>
  161. <p>그리고 캔버스 요소에 <code class="notranslate" translate="no">transferControlToOffscreen</code> 메서드가 있는지 확인해 <code class="notranslate" translate="no">OffscreenCanvas</code>의 지원 여부를 확인합니다.</p>
  162. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
  163. const canvas = document.querySelector('#c');
  164. + if (!canvas.transferControlToOffscreen) {
  165. + canvas.style.display = 'none';
  166. + document.querySelector('#noOffscreenCanvas').style.display = '';
  167. + return;
  168. + }
  169. const offscreen = canvas.transferControlToOffscreen();
  170. const worker = new Worker('offscreencanvas-picking.js'. { type: 'module' });
  171. worker.postMessage({ type: 'main', canvas: offscreen }, [ offscreen ]);
  172. ...
  173. </pre>
  174. <p>브라우저가 <code class="notranslate" translate="no">OffscreenCanvas</code>를 지원한다면 문제 없이 작동할 겁니다.</p>
  175. <p></p><div translate="no" class="threejs_example_container notranslate">
  176. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas.html"></iframe></div>
  177. <a class="threejs_center" href="/manual/examples/offscreencanvas.html" target="_blank">새 탭에서 보기</a>
  178. </div>
  179. <p></p>
  180. <p>하지만 현재 모든 브라우저가 <code class="notranslate" translate="no">OffscreenCanvas</code>를 지원하는 것은 아닙니다. <code class="notranslate" translate="no">OffscreenCanvas</code>를 지원할 경우에만 워커를 사용하도록 하고, 그렇지 않을 경우에는 기존처럼 메인 스크립트에서 렌더링을 처리하도록 하겠습니다.</p>
  181. <blockquote>
  182. <p>OffscreenCanvas를 단순히 페이지를 반응형으로 만드는 데 사용하는 건 의미없어 보일 수 있습니다. 메인 스크립트에서 반응형을 처리할 때보다 워커에서 처리할 때 오히려 작업이 더 많이 들 수 있거든요. 하지만 메인 스크립트만 사용할 때보다 워커를 사용할 때 자원을 더 넉넉하게 활용할 수 있다는 건 분명합니다. 전적으로 여러분이 상황에 따라 결정할 문제라는 것이죠.</p>
  183. </blockquote>
  184. <p>먼저 Three.js 관련 코드를 분리해 워커 관련 코드와 그렇지 않은 코드로 나눠야 합니다. 같은 코드를 메인 스크립트와 워커에서 모두 쓸 수 있도록 말이죠. 아래와 같이 3개의 파일로 나뉠 겁니다.</p>
  185. <ol>
  186. <li><p>html 파일.</p>
  187. <p><code class="notranslate" translate="no">threejs-offscreencanvas-w-fallback.html</code></p>
  188. </li>
  189. <li><p>three.js 관련 자바스크립트 파일.</p>
  190. <p><code class="notranslate" translate="no">shared-cubes.js</code></p>
  191. </li>
  192. <li><p>워커용 스크립트</p>
  193. <p><code class="notranslate" translate="no">offscreencanvas-worker-cubes.js</code></p>
  194. </li>
  195. </ol>
  196. <p><code class="notranslate" translate="no">shared-cubes.js</code>와 <code class="notranslate" translate="no">offscreencanvas-worker-cubes.js</code>는 단순히 이전 <code class="notranslate" translate="no">offscreencanvas-cubes.js</code> 파일을 쪼갠 것입니다. 먼저 <code class="notranslate" translate="no">offscreencanvas-cube.js</code>를 <code class="notranslate" translate="no">shared-cube.js</code>로 옮긴 뒤, 메인 HTML 파일에 이미 <code class="notranslate" translate="no">main</code> 함수가 있어 <code class="notranslate" translate="no">main</code> 함수의 이름만 <code class="notranslate" translate="no">init</code>으로 바꿔야 하죠. 여기에 추가로 <code class="notranslate" translate="no">init</code>과 <code class="notranslate" translate="no">state</code> 함수를 export 시켜줘야 합니다.</p>
  197. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
  198. -const state = {
  199. +export const state = {
  200. width: 300, // 캔버스 기본값
  201. height: 150, // 캔버스 기본값
  202. };
  203. -function main(data) {
  204. +export function init(data) {
  205. const { canvas } = data;
  206. const renderer = new THREE.WebGLRenderer({ canvas });
  207. </pre>
  208. <p>그리고 Three.js와 관련 없는 부분을 잘라냅니다.</p>
  209. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function size(data) {
  210. - state.width = data.width;
  211. - state.height = data.height;
  212. -}
  213. -
  214. -const handlers = {
  215. - main,
  216. - size,
  217. -};
  218. -
  219. -self.onmessage = function(e) {
  220. - const fn = handlers[e.data.type];
  221. - if (typeof fn !== 'function') {
  222. - throw new Error('no handler for type: ' + e.data.type);
  223. - }
  224. - fn(e.data);
  225. -};
  226. </pre>
  227. <p>방금 잘라낸 부분을 <code class="notranslate" translate="no">offscreencanvas-worker-cubes.js</code>에 붙여넣고, <code class="notranslate" translate="no">shared-cubes.js</code>를 import 합니다. 또한 <code class="notranslate" translate="no">main</code> 대신 <code class="notranslate" translate="no">init</code>을 호출하도록 합니다.</p>
  228. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import { init, state } from './shared-cubes.js';
  229. function size(data) {
  230. state.width = data.width;
  231. state.height = data.height;
  232. }
  233. const handlers = {
  234. - main,
  235. + init,
  236. size,
  237. };
  238. self.onmessage = function(e) {
  239. const fn = handlers[e.data.type];
  240. if (typeof fn !== 'function') {
  241. throw new Error('no handler for type: ' + e.data.type);
  242. }
  243. fn(e.data);
  244. };
  245. </pre>
  246. <p>메인 페이지에서도 마찬가지로 Three.js와 <code class="notranslate" translate="no">shared-cubes.js</code>를 추가합니다.</p>
  247. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;script type="module"&gt;&lt;/script&gt;
  248. +import { init, state } from './shared-cubes.js';
  249. </pre>
  250. <p>이전에 추가했던 에러 메시지용 HTML과 CSS를 제거합니다.</p>
  251. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;body&gt;
  252. &lt;canvas id="c"&gt;&lt;/canvas&gt;
  253. - &lt;div id="noOffscreenCanvas" style="display:none;"&gt;
  254. - &lt;div&gt;no OffscreenCanvas support&lt;/div&gt;
  255. - &lt;/div&gt;
  256. &lt;/body&gt;
  257. </pre>
  258. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">-#noOffscreenCanvas {
  259. - display: flex;
  260. - width: 100%;
  261. - height: 100%;
  262. - align-items: center;
  263. - justify-content: center;
  264. - background: red;
  265. - color: white;
  266. -}
  267. </pre>
  268. <p>그리고 <code class="notranslate" translate="no">OffscreenCanvas</code>의 지원 여부에 따라 다른 함수를 실행하도록 합니다.</p>
  269. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
  270. const canvas = document.querySelector('#c');
  271. - if (!canvas.transferControlToOffscreen) {
  272. - canvas.style.display = 'none';
  273. - document.querySelector('#noOffscreenCanvas').style.display = '';
  274. - return;
  275. - }
  276. - const offscreen = canvas.transferControlToOffscreen();
  277. - const worker = new Worker('offscreencanvas-picking.js', { type: 'module' });
  278. - worker.postMessage({ type: 'main', canvas: offscreen }, [ offscreen ]);
  279. + if (canvas.transferControlToOffscreen) {
  280. + startWorker(canvas);
  281. + } else {
  282. + startMainPage(canvas);
  283. + }
  284. ...
  285. </pre>
  286. <p>워커를 만들기 위해 사용했던 코드를 전부 <code class="notranslate" translate="no">startWorker</code> 함수로 옮깁니다.</p>
  287. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startWorker(canvas) {
  288. const offscreen = canvas.transferControlToOffscreen();
  289. const worker = new Worker('offscreencanvas-worker-cubes.js', { type: 'module' });
  290. worker.postMessage({ type: 'main', canvas: offscreen }, [ offscreen ]);
  291. function sendSize() {
  292. worker.postMessage({
  293. type: 'size',
  294. width: canvas.clientWidth,
  295. height: canvas.clientHeight,
  296. });
  297. }
  298. window.addEventListener('resize', sendSize);
  299. sendSize();
  300. console.log('using OffscreenCanvas');
  301. }
  302. </pre>
  303. <p>메시지에 <code class="notranslate" translate="no">'main'</code> 대신 <code class="notranslate" translate="no">'init'</code>을 보냅니다.</p>
  304. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">- worker.postMessage({ type: 'main', canvas: offscreen }, [ offscreen ]);
  305. + worker.postMessage({ type: 'init', canvas: offscreen }, [ offscreen ]);
  306. </pre>
  307. <p>워커를 사용할 수 없는 경우 다음과 같이 실행합니다.</p>
  308. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startMainPage(canvas) {
  309. init({ canvas });
  310. function sendSize() {
  311. state.width = canvas.clientWidth;
  312. state.height = canvas.clientHeight;
  313. }
  314. window.addEventListener('resize', sendSize);
  315. sendSize();
  316. console.log('using regular canvas');
  317. }
  318. </pre>
  319. <p>이제 <code class="notranslate" translate="no">OffscreenCanvas를</code> 지원하는 경우에만 <code class="notranslate" translate="no">OffscreenCanvas</code>를 사용하고, 지원하지 않는 경우에는 메인 스레드에서 직접 렌더링합니다.</p>
  320. <p></p><div translate="no" class="threejs_example_container notranslate">
  321. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas-w-fallback.html"></iframe></div>
  322. <a class="threejs_center" href="/manual/examples/offscreencanvas-w-fallback.html" target="_blank">새 탭에서 보기</a>
  323. </div>
  324. <p></p>
  325. <p>어떤가요? 생각했던 것보다 쉽지 않나요? 여기에 피킹(picking)을 추가해봅시다. <a href="picking.html">피킹에 관한 글</a>의 <code class="notranslate" translate="no">RayCaster</code> 예제에서 코드 일부를 가져오도록 하겠습니다.</p>
  326. <p>먼저 <code class="notranslate" translate="no">shared-cube.js</code>의 코드를 <code class="notranslate" translate="no">shared-picking.js</code>로 복사한 뒤, 피킹 예제에서 <code class="notranslate" translate="no">PickHelper</code>를 가져옵니다.</p>
  327. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class PickHelper {
  328. constructor() {
  329. this.raycaster = new THREE.Raycaster();
  330. this.pickedObject = null;
  331. this.pickedObjectSavedColor = 0;
  332. }
  333. pick(normalizedPosition, scene, camera, time) {
  334. // 이미 다른 물체를 피킹했다면 색을 복원합니다
  335. if (this.pickedObject) {
  336. this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
  337. this.pickedObject = undefined;
  338. }
  339. // 절두체 안에 광선을 쏩니다
  340. this.raycaster.setFromCamera(normalizedPosition, camera);
  341. // 광선과 교차하는 물체들을 배열로 만듭니다
  342. const intersectedObjects = this.raycaster.intersectObjects(scene.children);
  343. if (intersectedObjects.length) {
  344. // 첫 번째 물체가 제일 가까우므로 해당 물체를 고릅니다
  345. this.pickedObject = intersectedObjects[0].object;
  346. // 기존 색을 저장해둡니다
  347. this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
  348. // emissive 색을 빨강/노랑으로 빛나게 만듭니다
  349. this.pickedObject.material.emissive.setHex((time * 8) % 2 &gt; 1 ? 0xFFFF00 : 0xFF0000);
  350. }
  351. }
  352. }
  353. const pickPosition = { x: 0, y: 0 };
  354. const pickHelper = new PickHelper();
  355. </pre>
  356. <p><code class="notranslate" translate="no">pickPosition</code>은 마우스 포인터의 좌표를 기록하는 역할입니다. 이벤트를 통해 해당 속성을 업데이트하도록 했었죠.</p>
  357. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function getCanvasRelativePosition(event) {
  358. const rect = canvas.getBoundingClientRect();
  359. return {
  360. x: (event.clientX - rect.left) * canvas.width / rect.width,
  361. y: (event.clientY - rect.top ) * canvas.height / rect.height,
  362. };
  363. }
  364. function setPickPosition(event) {
  365. const pos = getCanvasRelativePosition(event);
  366. pickPosition.x = (pos.x / canvas.width ) * 2 - 1;
  367. pickPosition.y = (pos.y / canvas.height) * -2 + 1; // Y축을 뒤집었음
  368. }
  369. window.addEventListener('mousemove', setPickPosition);
  370. </pre>
  371. <p>워커는 포인터 좌표에 직접 접근할 수 없기에, 반응형 처리에 사용했던 코드처럼 포인터 좌표를 메시지로 보내야 합니다. 먼저 <code class="notranslate" translate="no">size</code> 함수와 마찬가지로 <code class="notranslate" translate="no">mouse</code> 함수를 만들어 <code class="notranslate" translate="no">pickPosition</code>을 업데이트하도록 합니다.</p>
  372. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function size(data) {
  373. state.width = data.width;
  374. state.height = data.height;
  375. }
  376. +function mouse(data) {
  377. + pickPosition.x = data.x;
  378. + pickPosition.y = data.y;
  379. +}
  380. const handlers = {
  381. init,
  382. + mouse,
  383. size,
  384. };
  385. self.onmessage = function(e) {
  386. const fn = handlers[e.data.type];
  387. if (typeof fn !== 'function') {
  388. throw new Error('no handler for type: ' + e.data.type);
  389. }
  390. fn(e.data);
  391. };
  392. </pre>
  393. <p>그리고 메인 페이지에 분기 함수를 만들어 워커 또는 메인 페이지로 좌표 데이터를 보내도록 합니다.</p>
  394. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+let sendMouse;
  395. function startWorker(canvas) {
  396. const offscreen = canvas.transferControlToOffscreen();
  397. const worker = new Worker('offscreencanvas-worker-picking.js', { type: 'module' });
  398. worker.postMessage({ type: 'init', canvas: offscreen }, [ offscreen ]);
  399. + sendMouse = (x, y) =&gt; {
  400. + worker.postMessage({
  401. + type: 'mouse',
  402. + x,
  403. + y,
  404. + });
  405. + };
  406. function sendSize() {
  407. worker.postMessage({
  408. type: 'size',
  409. width: canvas.clientWidth,
  410. height: canvas.clientHeight,
  411. });
  412. }
  413. window.addEventListener('resize', sendSize);
  414. sendSize();
  415. console.log('using OffscreenCanvas'); /* eslint-disable-line no-console */
  416. }
  417. function startMainPage(canvas) {
  418. init({ canvas });
  419. + sendMouse = (x, y) =&gt; {
  420. + pickPosition.x = x;
  421. + pickPosition.y = y;
  422. + };
  423. function sendSize() {
  424. state.width = canvas.clientWidth;
  425. state.height = canvas.clientHeight;
  426. }
  427. window.addEventListener('resize', sendSize);
  428. sendSize();
  429. console.log('using regular canvas'); /* eslint-disable-line no-console */
  430. }
  431. </pre>
  432. <p>다음으로 마우스 이벤트 관련 코드를 메인 페이지로 옮긴 뒤 <code class="notranslate" translate="no">sendMouse</code> 함수를 쓰도록 수정합니다.</p>
  433. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function setPickPosition(event) {
  434. const pos = getCanvasRelativePosition(event);
  435. - pickPosition.x = (pos.x / canvas.clientWidth ) * 2 - 1;
  436. - pickPosition.y = (pos.y / canvas.clientHeight) * -2 + 1; // Y축을 뒤집었음
  437. + sendMouse(
  438. + (pos.x / canvas.clientWidth ) * 2 - 1,
  439. + (pos.y / canvas.clientHeight) * -2 + 1); // Y축을 뒤집었음
  440. }
  441. function clearPickPosition() {
  442. /**
  443. * 마우스의 경우는 항상 위치가 있어 그다지 큰
  444. * 상관이 없지만, 터치 같은 경우 사용자가 손가락을
  445. * 떼면 피킹을 멈춰야 합니다. 지금은 일단 어떤 것도
  446. * 선택할 수 없는 값으로 지정해두었습니다
  447. **/
  448. - pickPosition.x = -100000;
  449. - pickPosition.y = -100000;
  450. + sendMouse(-100000, -100000);
  451. }
  452. window.addEventListener('mousemove', setPickPosition);
  453. window.addEventListener('mouseout', clearPickPosition);
  454. window.addEventListener('mouseleave', clearPickPosition);
  455. window.addEventListener('touchstart', (event) =&gt; {
  456. event.preventDefault(); // 스크롤 이벤트 방지
  457. setPickPosition(event.touches[0]);
  458. }, { passive: false });
  459. window.addEventListener('touchmove', (event) =&gt; {
  460. setPickPosition(event.touches[0]);
  461. });
  462. window.addEventListener('touchend', clearPickPosition);
  463. </pre>
  464. <p>이제 <code class="notranslate" translate="no">OffscreenCanvas</code>에서도 피킹이 정상적으로 작동할 겁니다.</p>
  465. <p></p><div translate="no" class="threejs_example_container notranslate">
  466. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas-w-picking.html"></iframe></div>
  467. <a class="threejs_center" href="/manual/examples/offscreencanvas-w-picking.html" target="_blank">새 탭에서 보기</a>
  468. </div>
  469. <p></p>
  470. <p>좀 더 욕심을 내 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>까지 추가해봅시다. <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>는 DOM에 꽤 다양하게 접근하기에 처리해줘야 할 것이 좀 많습니다. 제대로 작동하려면 마우스 이벤트, 터치 이벤트, 키보드 이벤트를 모두 처리해줘야 하죠.</p>
  471. <p>여태까지는 전역 <code class="notranslate" translate="no">state</code> 객체를 사용했지만, <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>의 경우는 객체 속성이 너무 많아 그걸 전부 다 하드 코딩하는 건 너무 번거롭습니다. <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>는 필요한 DOM 이벤트의 대부분을 인자로 받는 <code class="notranslate" translate="no">HTMLElement</code>에 바인딩합니다. 이를 이용해 DOM 요소와 같은 구조의 객체를 넘겨준다면 어떨까요? <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>에 필요한 기능만 살려서 말이죠.</p>
  472. <p><a href="https://github.com/mrdoob/three.js/blob/master/examples/jsm/controls/OrbitControls.js"><code class="notranslate" translate="no">OrbitControls</code>의 소스 코드</a>를 분석해보니 아래의 이벤트만 지원하면 될 듯합니다.</p>
  473. <ul>
  474. <li>contextmenu</li>
  475. <li>pointerdown</li>
  476. <li>pointermove</li>
  477. <li>pointerup</li>
  478. <li>touchstart</li>
  479. <li>touchmove</li>
  480. <li>touchend</li>
  481. <li>wheel</li>
  482. <li>keydown</li>
  483. </ul>
  484. <p>마우스 이벤트 중 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>가 사용하는 속성은 <code class="notranslate" translate="no">ctrlKey</code>, <code class="notranslate" translate="no">metaKey</code>, <code class="notranslate" translate="no">shiftKey</code>, <code class="notranslate" translate="no">button</code>, <code class="notranslate" translate="no">pointerType</code>, <code class="notranslate" translate="no">clientX</code>, <code class="notranslate" translate="no">clientY</code>, <code class="notranslate" translate="no">pageX</code>, <code class="notranslate" translate="no">pageY</code>이고,</p>
  485. <p>keydown 이벤트의 경우는 <code class="notranslate" translate="no">ctrlKey</code>, <code class="notranslate" translate="no">metaKey</code>, <code class="notranslate" translate="no">shiftKey</code>, <code class="notranslate" translate="no">keyCode</code> 속성,</p>
  486. <p>wheel 이벤트는 <code class="notranslate" translate="no">deltaY</code> 속성만,</p>
  487. <p>터치 이벤트의 경우는 <code class="notranslate" translate="no">touches</code> 속성의 <code class="notranslate" translate="no">pageX</code>, <code class="notranslate" translate="no">pageY</code> 속성이 필요하네요.</p>
  488. <p>이를 처리할 경유(proxy) 객체를 한 쌍 만들어봅시다. 한쪽은 메인 페이지에서 위 이벤트를 받아 필요한 속성을 워커에 넘겨주는 역할을 할 겁니다. 그리고 다른 한쪽은 워커 안에서 이 이벤트를 받아 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>에 넘겨줄 겁니다. 이벤트 객체가 DOM 이벤트와 같은 구조이기에 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>는 이 이벤트가 DOM 이벤트가 아니란 걸 눈치채지 못하겠죠.</p>
  489. <p>아래는 워커 안의 코드입니다.</p>
  490. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import { EventDispatcher } from 'three';
  491. class ElementProxyReceiver extends EventDispatcher {
  492. constructor() {
  493. super();
  494. }
  495. handleEvent(data) {
  496. this.dispatchEvent(data);
  497. }
  498. }
  499. </pre>
  500. <p>위 코드는 단순히 메시지를 받았을 때 그걸 다시 내보내는(dispatch) 역할을 합니다. 부모 클래스인 <a href="/docs/#api/ko/core/EventDispatcher"><code class="notranslate" translate="no">EventDispatcher</code></a>는 DOM 요소처럼 <code class="notranslate" translate="no">addEventListener</code>나 <code class="notranslate" translate="no">removeEventListener</code> 메서드를 제공하기에 HTML 요소 대신 이 클래스의 인스턴스를 넘겨줘도 문제없이 작동할 겁니다.</p>
  501. <p><code class="notranslate" translate="no">ElementProxyReceiver</code>는 하나의 요소만 대신할 수 있습니다. 예제의 경우 하나만 필요하기는 하나 나중에 캔버스를 여러 개 사용할 수도 있으니 여러 <code class="notranslate" translate="no">ElementProxyReceiver</code>를 관리하는 클래스를 만들겠습니다.</p>
  502. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ProxyManager {
  503. constructor() {
  504. this.targets = {};
  505. this.handleEvent = this.handleEvent.bind(this);
  506. }
  507. makeProxy(data) {
  508. const { id } = data;
  509. const proxy = new ElementProxyReceiver();
  510. this.targets[id] = proxy;
  511. }
  512. getProxy(id) {
  513. return this.targets[id];
  514. }
  515. handleEvent(data) {
  516. this.targets[data.id].handleEvent(data.data);
  517. }
  518. }
  519. </pre>
  520. <p><code class="notranslate" translate="no">ProxyManager</code>의 인스턴스를 만들고 id값과 함께 <code class="notranslate" translate="no">makeProxy</code> 메서드를 호출하면 해당 id에만 응답하는 <code class="notranslate" translate="no">ElementProxyReceiver</code>가 생성됩니다.</p>
  521. <p>이제 이 클래스를 기존 워커 코드와 연동해봅시다.</p>
  522. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const proxyManager = new ProxyManager();
  523. function start(data) {
  524. const proxy = proxyManager.getProxy(data.canvasId);
  525. init({
  526. canvas: data.canvas,
  527. inputElement: proxy,
  528. });
  529. }
  530. function makeProxy(data) {
  531. proxyManager.makeProxy(data);
  532. }
  533. ...
  534. const handlers = {
  535. - init,
  536. - mouse,
  537. + start,
  538. + makeProxy,
  539. + event: proxyManager.handleEvent,
  540. size,
  541. };
  542. self.onmessage = function(e) {
  543. const fn = handlers[e.data.type];
  544. if (typeof fn !== 'function') {
  545. throw new Error('no handler for type: ' + e.data.type);
  546. }
  547. fn(e.data);
  548. };
  549. </pre>
  550. <p>Three.js의 공통 코드에 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> 모듈도 불러와 설정해야 합니다.</p>
  551. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
  552. +import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
  553. export function init(data) {
  554. - const { canvas } = data;
  555. + const { canvas, inputElement } = data;
  556. const renderer = new THREE.WebGLRenderer({ canvas });
  557. + const controls = new OrbitControls(camera, inputElement);
  558. + controls.target.set(0, 0, 0);
  559. + controls.update();
  560. </pre>
  561. <p>위 코드에서는 이전과 달리 <code class="notranslate" translate="no">inputElement</code>로 경유 객체를 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>에 넘겨줬습니다.</p>
  562. <p>하는 김에 피킹 이벤트도 경유 객체를 사용하도록 바꿉니다.</p>
  563. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function getCanvasRelativePosition(event) {
  564. - const rect = canvas.getBoundingClientRect();
  565. + const rect = inputElement.getBoundingClientRect();
  566. return {
  567. x: event.clientX - rect.left,
  568. y: event.clientY - rect.top,
  569. };
  570. }
  571. function setPickPosition(event) {
  572. const pos = getCanvasRelativePosition(event);
  573. - sendMouse(
  574. - (pos.x / canvas.clientWidth ) * 2 - 1,
  575. - (pos.y / canvas.clientHeight) * -2 + 1); // Y축을 뒤집었음
  576. + pickPosition.x = (pos.x / inputElement.clientWidth ) * 2 - 1;
  577. + pickPosition.y = (pos.y / inputElement.clientHeight) * -2 + 1; // Y축을 뒤집었음
  578. }
  579. function clearPickPosition() {
  580. /**
  581. * 마우스의 경우는 항상 위치가 있어 그다지 큰
  582. * 상관이 없지만, 터치 같은 경우 사용자가 손가락을
  583. * 떼면 피킹을 멈춰야 합니다. 지금은 일단 어떤 것도
  584. * 선택할 수 없는 값으로 지정해두었습니다
  585. **/
  586. - sendMouse(-100000, -100000);
  587. + pickPosition.x = -100000;
  588. + pickPosition.y = -100000;
  589. }
  590. *inputElement.addEventListener('mousemove', setPickPosition);
  591. *inputElement.addEventListener('mouseout', clearPickPosition);
  592. *inputElement.addEventListener('mouseleave', clearPickPosition);
  593. *inputElement.addEventListener('touchstart', (event) =&gt; {
  594. event.preventDefault(); // 스크롤 이벤트 방지
  595. setPickPosition(event.touches[0]);
  596. }, { passive: false });
  597. *inputElement.addEventListener('touchmove', (event) =&gt; {
  598. setPickPosition(event.touches[0]);
  599. });
  600. *inputElement.addEventListener('touchend', clearPickPosition);
  601. </pre>
  602. <p>메인 페이지에서 위에 열거한 모든 이벤트가 워커로 메시지를 보내도록 합니다.</p>
  603. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">let nextProxyId = 0;
  604. class ElementProxy {
  605. constructor(element, worker, eventHandlers) {
  606. this.id = nextProxyId++;
  607. this.worker = worker;
  608. const sendEvent = (data) =&gt; {
  609. this.worker.postMessage({
  610. type: 'event',
  611. id: this.id,
  612. data,
  613. });
  614. };
  615. // id를 등록합니다.
  616. worker.postMessage({
  617. type: 'makeProxy',
  618. id: this.id,
  619. });
  620. for (const [eventName, handler] of Object.entries(eventHandlers)) {
  621. element.addEventListener(eventName, function(event) {
  622. handler(event, sendEvent);
  623. });
  624. }
  625. }
  626. }
  627. </pre>
  628. <p><code class="notranslate" translate="no">ElementProxy</code>는 이벤트를 우회할 요소(element)를 인자로 받습니다. 그리고 고유 id를 생성해 워커에 <code class="notranslate" translate="no">makeProxy</code> 메시지로 id를 등록합니다. 그러면 아까 만들었듯 워커는 이 id에 새로운 <code class="notranslate" translate="no">ElementProxyReceiver</code>를 생성하겠죠.</p>
  629. <p>다음으로 이벤트를 처리할 핸들러 맵(<code class="notranslate" translate="no">eventHandlers</code>)을 만듭니다. 이러면 해당 이벤트가 발생했을 때만 워커에 메시지를 보낼 수 있죠.</p>
  630. <p>워커를 생성할 때 <code class="notranslate" translate="no">ElementProxy</code>에 이 핸들러 맵을 넘겨 새 우회 요소를 생성합니다.</p>
  631. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startWorker(canvas) {
  632. const offscreen = canvas.transferControlToOffscreen();
  633. const worker = new Worker('offscreencanvas-worker-orbitcontrols.js', { type: 'module' });
  634. + const eventHandlers = {
  635. + contextmenu: preventDefaultHandler,
  636. + mousedown: mouseEventHandler,
  637. + mousemove: mouseEventHandler,
  638. + mouseup: mouseEventHandler,
  639. + pointerdown: mouseEventHandler,
  640. + pointermove: mouseEventHandler,
  641. + pointerup: mouseEventHandler,
  642. + touchstart: touchEventHandler,
  643. + touchmove: touchEventHandler,
  644. + touchend: touchEventHandler,
  645. + wheel: wheelEventHandler,
  646. + keydown: filteredKeydownEventHandler,
  647. + };
  648. + const proxy = new ElementProxy(canvas, worker, eventHandlers);
  649. worker.postMessage({
  650. type: 'start',
  651. canvas: offscreen,
  652. + canvasId: proxy.id,
  653. }, [ offscreen ]);
  654. console.log('using OffscreenCanvas'); /* eslint-disable-line no-console */
  655. }
  656. </pre>
  657. <p>핸들러 맵의 핸들러는 넘겨 받은 이벤트의 속성 중 넘겨 받은 키 배열에 해당하는 속성만 복사합니다. 그리고 <code class="notranslate" translate="no">ElementProxy</code>에서 넘겨 받은 <code class="notranslate" translate="no">sendEvent</code> 함수를 복사한 데이터와 함께 호출하죠. 그러면 <code class="notranslate" translate="no">sendEvent</code> 함수는 해당하는 id와 데이터를 워커에 보냅니다.</p>
  658. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const mouseEventHandler = makeSendPropertiesHandler([
  659. 'ctrlKey',
  660. 'metaKey',
  661. 'shiftKey',
  662. 'button',
  663. 'pointerType',
  664. 'clientX',
  665. 'clientY',
  666. 'pageX',
  667. 'pageY',
  668. ]);
  669. const wheelEventHandlerImpl = makeSendPropertiesHandler([
  670. 'deltaX',
  671. 'deltaY',
  672. ]);
  673. const keydownEventHandler = makeSendPropertiesHandler([
  674. 'ctrlKey',
  675. 'metaKey',
  676. 'shiftKey',
  677. 'keyCode',
  678. ]);
  679. function wheelEventHandler(event, sendFn) {
  680. event.preventDefault();
  681. wheelEventHandlerImpl(event, sendFn);
  682. }
  683. function preventDefaultHandler(event) {
  684. event.preventDefault();
  685. }
  686. function copyProperties(src, properties, dst) {
  687. for (const name of properties) {
  688. dst[name] = src[name];
  689. }
  690. }
  691. function makeSendPropertiesHandler(properties) {
  692. return function sendProperties(event, sendFn) {
  693. const data = { type: event.type };
  694. copyProperties(event, properties, data);
  695. sendFn(data);
  696. };
  697. }
  698. function touchEventHandler(event, sendFn) {
  699. const touches = [];
  700. const data = { type: event.type, touches };
  701. for (let i = 0; i &lt; event.touches.length; ++i) {
  702. const touch = event.touches[i];
  703. touches.push({
  704. pageX: touch.pageX,
  705. pageY: touch.pageY,
  706. });
  707. }
  708. sendFn(data);
  709. }
  710. // 키보드의 화살표 키
  711. const orbitKeys = {
  712. '37': true, // 왼쪽
  713. '38': true, // 위쪽
  714. '39': true, // 오른쪽
  715. '40': true, // 아래쪽
  716. };
  717. function filteredKeydownEventHandler(event, sendFn) {
  718. const { keyCode } = event;
  719. if (orbitKeys[keyCode]) {
  720. event.preventDefault();
  721. keydownEventHandler(event, sendFn);
  722. }
  723. }
  724. </pre>
  725. <p>거의 다 된 듯합니다. 하지만 실제로 예제를 실행해보니 아직 처리해줘야 할 것들이 몇 개 더 있네요.</p>
  726. <p><a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>는 <code class="notranslate" translate="no">element.focus</code> 메서드를 호출합니다. 이는 워커에서 그다지 쓸모가 없으니 빈 함수로 대체하겠습니다.</p>
  727. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ElementProxyReceiver extends THREE.EventDispatcher {
  728. constructor() {
  729. super();
  730. }
  731. handleEvent(data) {
  732. this.dispatchEvent(data);
  733. }
  734. + focus() {
  735. + // 빈 함수(no-operation)
  736. + }
  737. }
  738. </pre>
  739. <p><code class="notranslate" translate="no">event.preventDefault</code>와 <code class="notranslate" translate="no">event.stopPropagation</code>도 사용합니다. 이는 이미 메인 페이지에서 처리했으니 이 역시 빈 함수로 대체합니다.</p>
  740. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function noop() {
  741. +}
  742. class ElementProxyReceiver extends THREE.EventDispatcher {
  743. constructor() {
  744. super();
  745. }
  746. handleEvent(data) {
  747. + data.preventDefault = noop;
  748. + data.stopPropagation = noop;
  749. this.dispatchEvent(data);
  750. }
  751. focus() {
  752. // 빈 함수(no-operation)
  753. }
  754. }
  755. </pre>
  756. <p>또 <code class="notranslate" translate="no">clientWidth</code>와 <code class="notranslate" translate="no">clientHeight</code>도 사용합니다. 이전에는 캔버스의 크기값을 따로 넘겨줬었는데, 경유 객체들이 이 값도 주고받도록 수정하겠습니다.</p>
  757. <p>워커의 경우 다음처럼 코드를 추가합니다.</p>
  758. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ElementProxyReceiver extends THREE.EventDispatcher {
  759. constructor() {
  760. super();
  761. }
  762. + get clientWidth() {
  763. + return this.width;
  764. + }
  765. + get clientHeight() {
  766. + return this.height;
  767. + }
  768. + getBoundingClientRect() {
  769. + return {
  770. + left: this.left,
  771. + top: this.top,
  772. + width: this.width,
  773. + height: this.height,
  774. + right: this.left + this.width,
  775. + bottom: this.top + this.height,
  776. + };
  777. + }
  778. handleEvent(data) {
  779. + if (data.type === 'size') {
  780. + this.left = data.left;
  781. + this.top = data.top;
  782. + this.width = data.width;
  783. + this.height = data.height;
  784. + return;
  785. + }
  786. data.preventDefault = noop;
  787. data.stopPropagation = noop;
  788. this.dispatchEvent(data);
  789. }
  790. focus() {
  791. // 빈 함수(no-operation)
  792. }
  793. }
  794. </pre>
  795. <p>이제 메인 페이지에서 캔버스의 크기와 위치 좌표를 넘겨줘야 합니다. 하나 언급하고 싶은 건 예제에서는 캔버스의 크기가 바뀌는 경우만 가정했지, 캔버스가 움직이는 경우는 가정하지 않았다는 점입니다. 캔버스가 움직이는 경우를 처리하려면 캔버스가 움직였을 때 <code class="notranslate" translate="no">sendSize</code>를 호출하면 됩니다.</p>
  796. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ElementProxy {
  797. constructor(element, worker, eventHandlers) {
  798. this.id = nextProxyId++;
  799. this.worker = worker;
  800. const sendEvent = (data) =&gt; {
  801. this.worker.postMessage({
  802. type: 'event',
  803. id: this.id,
  804. data,
  805. });
  806. };
  807. // id를 등록합니다.
  808. worker.postMessage({
  809. type: 'makeProxy',
  810. id: this.id,
  811. });
  812. + sendSize();
  813. for (const [eventName, handler] of Object.entries(eventHandlers)) {
  814. element.addEventListener(eventName, function(event) {
  815. handler(event, sendEvent);
  816. });
  817. }
  818. + function sendSize() {
  819. + const rect = element.getBoundingClientRect();
  820. + sendEvent({
  821. + type: 'size',
  822. + left: rect.left,
  823. + top: rect.top,
  824. + width: element.clientWidth,
  825. + height: element.clientHeight,
  826. + });
  827. + }
  828. +
  829. + window.addEventListener('resize', sendSize);
  830. }
  831. }
  832. </pre>
  833. <p>이제 공통 Three.js 코드에서 <code class="notranslate" translate="no">state</code> 전역 변수를 쓰지 않으니 삭제합니다.</p>
  834. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-export const state = {
  835. - width: 300, // 캔버스 기본값
  836. - height: 150, // 캔버스 기본값
  837. -};
  838. ...
  839. function resizeRendererToDisplaySize(renderer) {
  840. const canvas = renderer.domElement;
  841. - const width = state.width;
  842. - const height = state.height;
  843. + const width = inputElement.clientWidth;
  844. + const height = inputElement.clientHeight;
  845. const needResize = canvas.width !== width || canvas.height !== height;
  846. if (needResize) {
  847. renderer.setSize(width, height, false);
  848. }
  849. return needResize;
  850. }
  851. function render(time) {
  852. time *= 0.001;
  853. if (resizeRendererToDisplaySize(renderer)) {
  854. - camera.aspect = state.width / state.height;
  855. + camera.aspect = inputElement.clientWidth / inputElement.clientHeight;
  856. camera.updateProjectionMatrix();
  857. }
  858. ...
  859. </pre>
  860. <p><a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>는 마우스 이벤트를 감지하기 위해 해당 요소의 <code class="notranslate" translate="no">ownerDocument</code>에 <code class="notranslate" translate="no">pointermove</code>와 <code class="notranslate" translate="no">pointerup</code> 리스너를 추가합니다(마우스가 창 밖으로 나갔을 경우를 위해).</p>
  861. <p>또한 코드는 전역 <code class="notranslate" translate="no">document</code> 객체를 참조하지만 워커에는 전역 <code class="notranslate" translate="no">document</code> 객체가 없습니다.</p>
  862. <p>이 문제는 간단한 편법(hack)을 써 해결할 수 있습니다. 다시 한 번 워커의 경유 객체를 이용하도록 하죠.</p>
  863. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function start(data) {
  864. const proxy = proxyManager.getProxy(data.canvasId);
  865. + proxy.ownerDocument = proxy; // HACK!
  866. + self.document = {} // HACK!
  867. init({
  868. canvas: data.canvas,
  869. inputElement: proxy,
  870. });
  871. }
  872. </pre>
  873. <p>이러면 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>가 에러를 던지지 않을 겁니다.</p>
  874. <p>예제가 복잡해 이해하기 어려웠을 수 있습니다. 동작을 요약하자면 <code class="notranslate" translate="no">ElementProxy</code>가 메인 페이지의 DOM 이벤트를 워커의 <code class="notranslate" translate="no">ElementProxyReceiver</code>에 넘기고, <code class="notranslate" translate="no">ElementProxyReceiver</code>는 <code class="notranslate" translate="no">HTMLElement</code>를 가장해 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>와 공통 코드에서 쓸 수 있는 대체 DOM 요소로 기능합니다.</p>
  875. <p>마지막으로 <code class="notranslate" translate="no">OffscreenCanvas</code>를 지원하지 않는 경우의 예외 코드만 수정해주면 끝입니다. 간단히 <code class="notranslate" translate="no">inputElement</code>에 캔버스 요소자체를 넘겨주기만 하면 되죠.</p>
  876. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startMainPage(canvas) {
  877. - init({ canvas });
  878. + init({ canvas, inputElement: canvas });
  879. console.log('using regular canvas');
  880. }
  881. </pre>
  882. <p>이제 <code class="notranslate" translate="no">OffscreenCanvas</code>에서도 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>가 잘 작동합니다.</p>
  883. <p></p><div translate="no" class="threejs_example_container notranslate">
  884. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas-w-orbitcontrols.html"></iframe></div>
  885. <a class="threejs_center" href="/manual/examples/offscreencanvas-w-orbitcontrols.html" target="_blank">새 탭에서 보기</a>
  886. </div>
  887. <p></p>
  888. <p>아마 이 예제가 이 시리즈를 통틀어 가장 복잡한 예제일 겁니다. 각 예제마다 HTML 파일, 워커 파일, 공통 Three.js 코드 파일, 이렇게 파일 3개가 서로 연동되니 그럴만 하죠.</p>
  889. <p>이 글이 너무 어렵게 느껴지지 않았다면 좋겠습니다. 또한 Three.js에서 OffscreenCanvas와 웹 워커를 활용하는 좋은 예가 되었길 바랍니다.</p>
  890. </div>
  891. </div>
  892. </div>
  893. <script src="../resources/prettify.js"></script>
  894. <script src="../resources/lesson.js"></script>
  895. </body></html>