123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649 |
- <!DOCTYPE html><html lang="ko"><head>
- <meta charset="utf-8">
- <title>로 캔버스, 장면 여러 개 만들기</title>
- <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
- <meta name="twitter:card" content="summary_large_image">
- <meta name="twitter:site" content="@threejs">
- <meta name="twitter:title" content="Three.js – 로 캔버스, 장면 여러 개 만들기">
- <meta property="og:image" content="https://threejs.org/files/share.png">
- <link rel="shortcut icon" href="../../files/favicon_white.ico" media="(prefers-color-scheme: dark)">
- <link rel="shortcut icon" href="../../files/favicon.ico" media="(prefers-color-scheme: light)">
- <link rel="stylesheet" href="../resources/lesson.css">
- <link rel="stylesheet" href="../resources/lang.css">
- <script type="importmap">
- {
- "imports": {
- "three": "../../build/three.module.js"
- }
- }
- </script>
- <link rel="stylesheet" href="/manual/ko/lang.css">
- </head>
- <body>
- <div class="container">
- <div class="lesson-title">
- <h1>로 캔버스, 장면 여러 개 만들기</h1>
- </div>
- <div class="lesson">
- <div class="lesson-main">
- <p>사람들이 자주 하는 질문 중 하나는 Three.js로 여러 개의 캔버스(canvas)를 렌더링하려면
- 어떻게 해야 하나요?"입니다. 쇼핑몰 사이트나 3D 도표가 여러 개 있는 웹 페이지를
- 제작한다고 해봅시다. 얼핏 그리 어려울 건 없어 보입니다. 그냥 도표가 들어갈 곳마다
- 각각 캔버스를 만들고, 각 캔버스마다 <a href="/docs/#api/ko/constants/Renderer"><code class="notranslate" translate="no">Renderer</code></a>를 생성하면 되지 않을까요?</p>
- <p>하지만 이 방법을 적용하자마자 문제가 생깁니다.</p>
- <ol>
- <li><p>브라우저의 WebGL 컨텍스트(context)는 제한적이다.</p>
- <p> 일반적으로 약 8개가 최대입니다. 9번째 컨텍스트를 만들면 제일 처음에 만들었던
- 컨텍스트가 사라지죠.</p>
- </li>
- <li><p>WebGL 자원은 컨텍스트끼리 공유할 수 없다.</p>
- <p> 10MB짜리 모델을 캔버스 두 개에서 사용하려면 모델을 각각 총 두 번 로드해야 하고,
- 원래의 두 배인 20MB의 자원을 사용한다는 의미입니다. 컨텍스트끼리는 어떤 것도 공유할
- 수 없죠. 또한 초기화도 두 번, 쉐이더 컴파일도 두 번, 같은 동작은 모두 두 번씩
- 실행해야 합니다. 캔버스의 개수가 많아질수록 성능에 문제가 생기겠죠.</p>
- </li>
- </ol>
- <p>그렇다면 어떻게 해야 할까요?</p>
- <p>방법 중 하나는 캔버스 하나로 화면 전체를 채우고, 각 "가상" 캔버스를 대신할 HTML 요소(element)를
- 두는 겁니다. <a href="/docs/#api/ko/constants/Renderer"><code class="notranslate" translate="no">Renderer</code></a>는 하나만 만들되 가상 캔버스에 각각 <a href="/docs/#api/ko/scenes/Scene"><code class="notranslate" translate="no">Scene</code></a>을 만드는 거죠. 그리고
- 가상 HTML 요소의 좌표를 계산해 요소가 화면에 보인다면 Three.js가 해당 장면(scene)을 가상
- 요소의 좌표에 맞춰 렌더링하도록 합니다.</p>
- <p>이 방법은 캔버스를 하나만 사용하므로 위 1번과 2번 문제 모두 해결할 수 있습니다. 컨텍스트를
- 하나만 사용하니 WebGL 컨텍스트 제한을 걱정할 일도 없고, 자원을 몇 배씩 더 사용할 일도 없죠.</p>
- <p>2개의 장면만 만들어 간단히 테스트를 해보겠습니다. 먼저 HTML을 작성합니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-html" translate="no"><canvas id="c"></canvas>
- <p>
- <span id="box" class="diagram left"></span>
- I love boxes. Presents come in boxes.
- When I find a new box I'm always excited to find out what's inside.
- </p>
- <p>
- <span id="pyramid" class="diagram right"></span>
- When I was a kid I dreamed of going on an expedition inside a pyramid
- and finding a undiscovered tomb full of mummies and treasure.
- </p>
- </pre>
- <p>다음으로 CSS를 작성합니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#c {
- position: fixed;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- display: block;
- z-index: -1;
- }
- .diagram {
- display: inline-block;
- width: 5em;
- height: 3em;
- border: 1px solid black;
- }
- .left {
- float: left;
- margin-right: .25em;
- }
- .right {
- float: right;
- margin-left: .25em;
- }
- </pre>
- <p>캔버스가 화면 전체를 채우도록 하고 <code class="notranslate" translate="no">z-index</code>를 -1로 설정해 다른 요소 뒤로 가도록 했습니다.
- 가상 요소에 컨텐츠가 없어 크기가 0이니 별도의 width와 height도 지정해줬습니다.</p>
- <p>이제 각각의 카메라와 조명이 있는 장면 2개를 만듭니다. 하나에는 정육면체, 다른 하나에는
- 다이아몬드 모양을 넣을 겁니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function makeScene(elem) {
- const scene = new THREE.Scene();
- const fov = 45;
- const aspect = 2; // 캔버스 기본값
- const near = 0.1;
- const far = 5;
- const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
- camera.position.z = 2;
- camera.position.set(0, 1, 2);
- camera.lookAt(0, 0, 0);
- {
- const color = 0xFFFFFF;
- const intensity = 1;
- const light = new THREE.DirectionalLight(color, intensity);
- light.position.set(-1, 2, 4);
- scene.add(light);
- }
- return { scene, camera, elem };
- }
- function setupScene1() {
- const sceneInfo = makeScene(document.querySelector('#box'));
- const geometry = new THREE.BoxGeometry(1, 1, 1);
- const material = new THREE.MeshPhongMaterial({color: 'red'});
- const mesh = new THREE.Mesh(geometry, material);
- sceneInfo.scene.add(mesh);
- sceneInfo.mesh = mesh;
- return sceneInfo;
- }
- function setupScene2() {
- const sceneInfo = makeScene(document.querySelector('#pyramid'));
- const radius = .8;
- const widthSegments = 4;
- const heightSegments = 2;
- const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
- const material = new THREE.MeshPhongMaterial({
- color: 'blue',
- flatShading: true,
- });
- const mesh = new THREE.Mesh(geometry, material);
- sceneInfo.scene.add(mesh);
- sceneInfo.mesh = mesh;
- return sceneInfo;
- }
- const sceneInfo1 = setupScene1();
- const sceneInfo2 = setupScene2();
- </pre>
- <p>이제 각 요소가 화면에 보일 때만 장면을 렌더링할 함수를 만듭니다. <a href="/docs/#api/ko/constants/Renderer.setScissorTest"><code class="notranslate" translate="no">Renderer.setScissorTest</code></a>를
- 호출해 <em>가위(scissor)</em> 테스트를 활성화하면 Three.js가 캔버스의 특정 부분만 렌더링하도록
- 할 수 있습니다. 그리고 <a href="/docs/#api/ko/constants/Renderer.setScissor"><code class="notranslate" translate="no">Renderer.setScissor</code></a>로 가위를 설정한 뒤 <a href="/docs/#api/ko/constants/Renderer.setViewport"><code class="notranslate" translate="no">Renderer.setViewport</code></a>로
- 장면의 좌표를 설정합니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function renderSceneInfo(sceneInfo) {
- const { scene, camera, elem } = sceneInfo;
- // 해당 요소의 화면 대비 좌표를 가져옵니다
- const { left, right, top, bottom, width, height } =
- elem.getBoundingClientRect();
- const isOffscreen =
- bottom < 0 ||
- top > renderer.domElement.clientHeight ||
- right < 0 ||
- left > renderer.domElement.clientWidth;
- if (isOffscreen) {
- return;
- }
- camera.aspect = width / height;
- camera.updateProjectionMatrix();
- const positiveYUpBottom = canvasRect.height - bottom;
- renderer.setScissor(left, positiveYUpBottom, width, height);
- renderer.setViewport(left, positiveYUpBottom, width, height);
- renderer.render(scene, camera);
- }
- </pre>
- <p>다음으로 <code class="notranslate" translate="no">render</code> 함수 안에서 먼저 캔버스 전체를 비운 뒤 각 장면을 렌더링합니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
- time *= 0.001;
- resizeRendererToDisplaySize(renderer);
- renderer.setScissorTest(false);
- renderer.clear(true, true);
- renderer.setScissorTest(true);
- sceneInfo1.mesh.rotation.y = time * .1;
- sceneInfo2.mesh.rotation.y = time * .1;
- renderSceneInfo(sceneInfo1);
- renderSceneInfo(sceneInfo2);
- requestAnimationFrame(render);
- }
- </pre>
- <p>결과를 확인해볼까요?</p>
- <p></p><div translate="no" class="threejs_example_container notranslate">
- <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/multiple-scenes-v1.html"></iframe></div>
- <a class="threejs_center" href="/manual/examples/multiple-scenes-v1.html" target="_blank">새 탭에서 보기</a>
- </div>
- <p></p>
- <p>첫 번째 <code class="notranslate" translate="no"><span></code> 요소가 있는 곳에는 빨간 정육면체가, 두 번째 <code class="notranslate" translate="no"><span></code> 요소가 있는 곳에는
- 파란 다이아몬드가 보일 겁니다.</p>
- <h2 id="-">동기화하기</h2>
- <p>위 코드는 나쁘지 않지만 작은 문제가 있습니다. 복잡한 장면 등 무슨 이유라도 렌더링하는
- 데 시간이 오래 걸린다면, 장면의 좌표는 페이지의 다른 컨텐츠에 비해 더디게 내려올 겁니다.</p>
- <p>각 가상 요소에 테두리를 넣고</p>
- <pre class="prettyprint showlinemods notranslate lang-css" translate="no">.diagram {
- display: inline-block;
- width: 5em;
- height: 3em;
- + border: 1px solid black;
- }
- </pre>
- <p>각 장면에 배경색도 넣어줍니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const scene = new THREE.Scene();
- +scene.background = new THREE.Color('red');
- </pre>
- <p>그런 다음 <a href="../examples/multiple-scenes-v2.html" target="_blank">빠르게 스크롤을 위아래로 반복해보면</a>
- 문제가 보일겁니다. 아래는 스크롤 애니메이션 캡쳐본의 속도를 10배 낮춘 예시입니다.</p>
- <div class="threejs_center"><img class="border" src="../resources/images/multi-view-skew.gif"></div>
- <p>추가로 처리해줘야 할 것이 있긴 하지만, 캔버스의 CSS를 <code class="notranslate" translate="no">position: fixed</code>에서 <code class="notranslate" translate="no">position: absolute</code>로
- 바꿔 문제를 해결할 수 있습니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#c {
- - position: fixed;
- + position: absolute;
- </pre>
- <p>그리고 페이지 스크롤에 상관 없이 캔버스가 항상 화면의 상단에 위치할 수 있도록 캔버스에
- transform 스타일을 지정해줍니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
- ...
- const transform = `translateY(${ window.scrollY }px)`;
- renderer.domElement.style.transform = transform;
- </pre>
- <p>캔버스에 <code class="notranslate" translate="no">position: fixed</code>를 적용하면 캔버스는 스크롤의 영향을 받지 않습니다. <code class="notranslate" translate="no">position: absolute</code>를
- 적용하면 렌더링하는 데 시간이 걸리더라도 일단 다른 페이지와 같이 스크롤이 되겠죠. 그리고
- 렌더링하기 전에 캔버스를 다시 움직여 화면 전체에 맞춘 뒤 캔버스를 렌더링하는 겁니다. 이러면
- 화면의 가장자리에 살짝 렌더링되지 않은 부분이 보일 수는 있어도 나머지 페이지에 있는 요소는
- 버벅이지 않고 제자리에 있을 겁니다. 아래는 해당 코드를 적용한 화면의 캡쳐본을 아까와 마찬가지로
- 10배 느리게 만든 것입니다.</p>
- <div class="threejs_center"><img class="border" src="../resources/images/multi-view-fixed.gif"></div>
- <h2 id="-">확장하기 쉽게 만들기</h2>
- <p>여러 장면을 구현했으니 이제 이 예제를 좀 더 확장하기 쉽게 만들어보겠습니다.</p>
- <p>먼저 기존처럼 캔버스 전체를 렌더링하는 <code class="notranslate" translate="no">render</code> 함수를 두고, 각 장면에 해당하는 가상 요소,
- 해당 장면을 렌더링하는 함수로 이루어진 객체의 배열을 만듭니다. <code class="notranslate" translate="no">render</code> 함수에서 가상 요소가
- 화면에 보이는지 확인한 뒤, 가상 요소가 화면에 보인다면 상응하는 렌더링 함수를 호출합니다. 이러면
- 확장성은 물론 각 장면의 렌더링 함수를 작성할 때도 전체를 신경쓸 필요가 없죠.</p>
- <p>아래는 전체를 담당하는 <code class="notranslate" translate="no">render</code> 함수입니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const sceneElements = [];
- function addScene(elem, fn) {
- sceneElements.push({ elem, fn });
- }
- function render(time) {
- time *= 0.001;
- resizeRendererToDisplaySize(renderer);
- renderer.setScissorTest(false);
- renderer.setClearColor(clearColor, 0);
- renderer.clear(true, true);
- renderer.setScissorTest(true);
- const transform = `translateY(${ window.scrollY }px)`;
- renderer.domElement.style.transform = transform;
- for (const { elem, fn } of sceneElements) {
- // 해당 요소의 화면 대비 좌표를 가져옵니다
- const rect = elem.getBoundingClientRect();
- const {left, right, top, bottom, width, height} = rect;
- const isOffscreen =
- bottom < 0 ||
- top > renderer.domElement.clientHeight ||
- right < 0 ||
- left > renderer.domElement.clientWidth;
- if (!isOffscreen) {
- const positiveYUpBottom = renderer.domElement.clientHeight - bottom;
- renderer.setScissor(left, positiveYUpBottom, width, height);
- renderer.setViewport(left, positiveYUpBottom, width, height);
- fn(time, rect);
- }
- }
- requestAnimationFrame(render);
- }
- </pre>
- <p><code class="notranslate" translate="no">render</code> 함수는 <code class="notranslate" translate="no">elem</code>과 <code class="notranslate" translate="no">fn</code> 속성의 객체로 이루어진 <code class="notranslate" translate="no">sceneElements</code> 배열을 순회합니다.</p>
- <p>그리고 각 요소가 화면에 보이는지 확인하고, 화면에 보인다면 <code class="notranslate" translate="no">fn</code>에 해당 장면이 들어가야할
- 사각 좌표와 현재 시간값을 넘겨주어 호출합니다.</p>
- <p>이제 각 장면을 만들고 상응하는 요소와 렌더링 함수를 추가합니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
- const elem = document.querySelector('#box');
- const { scene, camera } = makeScene();
- const geometry = new THREE.BoxGeometry(1, 1, 1);
- const material = new THREE.MeshPhongMaterial({ color: 'red' });
- const mesh = new THREE.Mesh(geometry, material);
- scene.add(mesh);
- addScene(elem, (time, rect) => {
- camera.aspect = rect.width / rect.height;
- camera.updateProjectionMatrix();
- mesh.rotation.y = time * .1;
- renderer.render(scene, camera);
- });
- }
- {
- const elem = document.querySelector('#pyramid');
- const { scene, camera } = makeScene();
- const radius = .8;
- const widthSegments = 4;
- const heightSegments = 2;
- const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
- const material = new THREE.MeshPhongMaterial({
- color: 'blue',
- flatShading: true,
- });
- const mesh = new THREE.Mesh(geometry, material);
- scene.add(mesh);
- addScene(elem, (time, rect) => {
- camera.aspect = rect.width / rect.height;
- camera.updateProjectionMatrix();
- mesh.rotation.y = time * .1;
- renderer.render(scene, camera);
- });
- }
- </pre>
- <p><code class="notranslate" translate="no">sceneInfo1</code>, <code class="notranslate" translate="no">sceneInfo2</code>는 더 이상 필요 없으니 제거합니다. 대신 각 mesh의 회전은 해당
- 장면에서 처리해야 합니다.</p>
- <p></p><div translate="no" class="threejs_example_container notranslate">
- <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/multiple-scenes-generic.html"></iframe></div>
- <a class="threejs_center" href="/manual/examples/multiple-scenes-generic.html" target="_blank">새 탭에서 보기</a>
- </div>
- <p></p>
- <h2 id="html-dataset-">HTML Dataset 사용하기</h2>
- <p>HTML의 <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset">dataset</a>을
- 이용하면 좀 더 확장하기 쉬운 환경을 만들 수 있습니다. <code class="notranslate" translate="no">id="..."</code> 대신 <code class="notranslate" translate="no">data-diagram="..."</code>을
- 이용해 데이터를 직접 HTML 요소에 지정하는 거죠.</p>
- <pre class="prettyprint showlinemods notranslate lang-html" translate="no"><canvas id="c"></canvas>
- <p>
- - <span id="box" class="diagram left"></span>
- + <span data-diagram="box" class="left"></span>
- I love boxes. Presents come in boxes.
- When I find a new box I'm always excited to find out what's inside.
- </p>
- <p>
- - <span id="pyramid" class="diagram left"></span>
- + <span data-diagram="pyramid" class="right"></span>
- When I was a kid I dreamed of going on an expedition inside a pyramid
- and finding a undiscovered tomb full of mummies and treasure.
- </p>
- </pre>
- <p>요소의 id를 제거했으니 CSS 셀렉터도 다음처럼 바꾸어야 합니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-css" translate="no">-.diagram
- +*[data-diagram] {
- display: inline-block;
- width: 5em;
- height: 3em;
- }
- </pre>
- <p>또한 각 장면을 만드는 코드를 <em>scene initialization functions</em>라는 맵으로 만듭니다.
- 이 맵은 키값에 대응하는 <em>장면 렌더링 함수</em>를 반환할 겁니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const sceneInitFunctionsByName = {
- 'box': () => {
- const { scene, camera } = makeScene();
- const geometry = new THREE.BoxGeometry(1, 1, 1);
- const material = new THREE.MeshPhongMaterial({color: 'red'});
- const mesh = new THREE.Mesh(geometry, material);
- scene.add(mesh);
- return (time, rect) => {
- mesh.rotation.y = time * .1;
- camera.aspect = rect.width / rect.height;
- camera.updateProjectionMatrix();
- renderer.render(scene, camera);
- };
- },
- 'pyramid': () => {
- const { scene, camera } = makeScene();
- const radius = .8;
- const widthSegments = 4;
- const heightSegments = 2;
- const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
- const material = new THREE.MeshPhongMaterial({
- color: 'blue',
- flatShading: true,
- });
- const mesh = new THREE.Mesh(geometry, material);
- scene.add(mesh);
- return (time, rect) => {
- mesh.rotation.y = time * .1;
- camera.aspect = rect.width / rect.height;
- camera.updateProjectionMatrix();
- renderer.render(scene, camera);
- };
- },
- };
- </pre>
- <p>그리고 <code class="notranslate" translate="no">querySelectorAll</code>로 가상 요소를 전부 불러와 해당 요소에 상응하는 렌더링 함수를
- 실행합니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">document.querySelectorAll('[data-diagram]').forEach((elem) => {
- const sceneName = elem.dataset.diagram;
- const sceneInitFunction = sceneInitFunctionsByName[sceneName];
- const sceneRenderFunction = sceneInitFunction(elem);
- addScene(elem, sceneRenderFunction);
- });
- </pre>
- <p>이제 코드를 확장하기가 한결 편해졌습니다.</p>
- <p></p>
- <h2 id="-">각 요소에 액션 추가하기</h2>
- <p>사용자 액션, 예를 들어 <code class="notranslate" translate="no">TrackballControls</code>를 추가하는 건 아주 간단합니다. 먼저 스크립트를
- 불러옵니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import { TrackballControls } from 'three/addons/controls/TrackballControls.js';
- </pre>
- <p>그리고 각 장면에 대응하는 요소에 <code class="notranslate" translate="no">TrackballControls</code>를 추가합니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function makeScene() {
- +function makeScene(elem) {
- const scene = new THREE.Scene();
- const fov = 45;
- const aspect = 2; // 캔버스 기본값
- const near = 0.1;
- const far = 5;
- const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
- camera.position.set(0, 1, 2);
- camera.lookAt(0, 0, 0);
- + scene.add(camera);
- + const controls = new TrackballControls(camera, elem);
- + controls.noZoom = true;
- + controls.noPan = true;
- {
- const color = 0xFFFFFF;
- const intensity = 1;
- const light = new THREE.DirectionalLight(color, intensity);
- light.position.set(-1, 2, 4);
- - scene.add(light);
- + camera.add(light);
- }
- - return { scene, camera };
- + return { scene, camera, controls };
- }
- </pre>
- <p>위 코드에서는 카메라를 장면에 추가하고, 카메라에 조명을 추가했습니다. 이러면 조명이 카메라를
- 따라다니겠죠. <code class="notranslate" translate="no">TrackballControls</code>는 카메라를 조정하기 때문에 이렇게 해야 빛이 계속 우리가
- 바라보는 방향에서 나갑니다.</p>
- <p>또한 컨트롤을 렌더링 함수에서 업데이트해줘야 합니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const sceneInitFunctionsByName = {
- - 'box': () => {
- - const {scene, camera} = makeScene();
- + 'box': (elem) => {
- + const { scene, camera, controls } = makeScene(elem);
- const geometry = new THREE.BoxGeometry(1, 1, 1);
- const material = new THREE.MeshPhongMaterial({color: 'red'});
- const mesh = new THREE.Mesh(geometry, material);
- scene.add(mesh);
- return (time, rect) => {
- mesh.rotation.y = time * .1;
- camera.aspect = rect.width / rect.height;
- camera.updateProjectionMatrix();
- + controls.handleResize();
- + controls.update();
- renderer.render(scene, camera);
- };
- },
- - 'pyramid': () => {
- - const { scene, camera } = makeScene();
- + 'pyramid': (elem) => {
- + const { scene, camera, controls } = makeScene(elem);
- const radius = .8;
- const widthSegments = 4;
- const heightSegments = 2;
- const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
- const material = new THREE.MeshPhongMaterial({
- color: 'blue',
- flatShading: true,
- });
- const mesh = new THREE.Mesh(geometry, material);
- scene.add(mesh);
- return (time, rect) => {
- mesh.rotation.y = time * .1;
- camera.aspect = rect.width / rect.height;
- camera.updateProjectionMatrix();
- + controls.handleResize();
- + controls.update();
- renderer.render(scene, camera);
- };
- },
- };
- </pre>
- <p>이제 각 물체를 자유롭게 회전시킬 수 있습니다.</p>
- <p></p><div translate="no" class="threejs_example_container notranslate">
- <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/multiple-scenes-controls.html"></iframe></div>
- <a class="threejs_center" href="/manual/examples/multiple-scenes-controls.html" target="_blank">새 탭에서 보기</a>
- </div>
- <p></p>
- <p>이 기법은 이 사이트 전체에 사용한 기법입니다. <a href="primitives.html">원시 모델에 관한 글</a>과
- <a href="materials.html">재질에 관한 글</a>에서 다양한 예시를 보여주기 위해 사용했죠.</p>
- <p>다른 방법으로는 화면 밖의 캔버스에서 장면을 렌더링해 각 요소에 2D 캔버스 형태로 넘겨주는
- 방법이 있습니다. 이 방법의 장점은 각 영역을 어떻게 분리할지 고민하지 않아도 된다는 것이죠.
- 위에서 살펴본 방법은 캔버스를 화면 전체의 배경으로 써야 하지만, 이 방법은 일반 HTML 형태로
- 사용할 수 있습니다.</p>
- <p>하지만 이 방법은 각 영역을 복사하는 것이기에 성능이 더 느립니다. 얼마나 느릴지는 브라우저와
- GPU 성능에 따라 다르죠.</p>
- <p>바꿔야 하는 건 생각보다 많지 않습니다.</p>
- <p>먼저 배경에서 캔버스 요소를 제거합니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-html" translate="no"><body>
- - <canvas id="c"></canvas>
- ...
- </body>
- </pre>
- <p>CSS도 바꿔줍니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-css" translate="no">-#c {
- - position: absolute;
- - left: 0;
- - top: 0;
- - width: 100%;
- - height: 100%;
- - display: block;
- - z-index: -1;
- -}
- canvas {
- width: 100%;
- height: 100%;
- display: block;
- }
- *[data-diagram] {
- display: inline-block;
- width: 5em;
- height: 3em;
- }
- </pre>
- <p>캔버스 요소가 부모에 꽉 차도록 변경했습니다.</p>
- <p>이제 자바스크립트를 변경해봅시다. 먼저 캔버스를 참조할 필요가 없으니 대신 캔버스 요소를
- 새로 만듭니다. 또한 가위 테스트를 처음에 활성화합니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
- - const canvas = document.querySelector('#c');
- + const canvas = document.createElement('canvas');
- const renderer = new THREE.WebGLRenderer({antialias: true, canvas, alpha: true});
- + renderer.setScissorTest(true);
- ...
- </pre>
- <p>다음으로 각 장면에 2D 렌더링 컨텍스트를 생성하고 장면에 대응하는 요소에 캔버스를 추가합니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const sceneElements = [];
- function addScene(elem, fn) {
- + const ctx = document.createElement('canvas').getContext('2d');
- + elem.appendChild(ctx.canvas);
- - sceneElements.push({ elem, fn });
- + sceneElements.push({ elem, ctx, fn });
- }
- </pre>
- <p>만약 렌더링 시 렌더링용 캔버스의 크기가 장면의 크기보다 작을 경우, 렌더링용 캔버스의 크기를
- 키웁니다. 또한 2D 캔버스의 크기가 부모 요소와 다르다면 2D 캔버스의 크기를 조정합니다. 마지막으로
- 가위와 화면을 설정하고, 해당 장면을 렌더링한 뒤, 요소의 캔버스로 렌더링 결과물을 복사합니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
- time *= 0.001;
- - resizeRendererToDisplaySize(renderer);
- -
- - renderer.setScissorTest(false);
- - renderer.setClearColor(clearColor, 0);
- - renderer.clear(true, true);
- - renderer.setScissorTest(true);
- -
- - const transform = `translateY(${ window.scrollY }px)`;
- - renderer.domElement.style.transform = transform;
- - for (const { elem, fn } of sceneElements) {
- + for (const { elem, fn, ctx } of sceneElements) {
- // 해당 요소의 화면 대비 좌표를 가져옵니다
- const rect = elem.getBoundingClientRect();
- const { left, right, top, bottom, width, height } = rect;
- + const rendererCanvas = renderer.domElement;
- const isOffscreen =
- bottom < 0 ||
- - top > renderer.domElement.clientHeight ||
- + top > window.innerHeight ||
- right < 0 ||
- - left > renderer.domElement.clientWidth;
- + left > window.innerWidth;
- if (!isOffscreen) {
- - const positiveYUpBottom = renderer.domElement.clientHeight - bottom;
- - renderer.setScissor(left, positiveYUpBottom, width, height);
- - renderer.setViewport(left, positiveYUpBottom, width, height);
- + // 렌더링용 캔버스 크기 조정
- + if (rendererCanvas.width < width || rendererCanvas.height < height) {
- + renderer.setSize(width, height, false);
- + }
- +
- + // 2D 캔버스의 크기가 요소의 크기와 같도록 조정
- + if (ctx.canvas.width !== width || ctx.canvas.height !== height) {
- + ctx.canvas.width = width;
- + ctx.canvas.height = height;
- + }
- +
- + renderer.setScissor(0, 0, width, height);
- + renderer.setViewport(0, 0, width, height);
- fn(time, rect);
- + // 렌더링된 장면을 2D 캔버스에 복사
- + ctx.globalCompositeOperation = 'copy';
- + ctx.drawImage(
- + rendererCanvas,
- + 0, rendererCanvas.height - height, width, height, // 원본 사각 좌표
- + 0, 0, width, height); // 결과물 사각 좌표
- }
- }
- requestAnimationFrame(render);
- }
- </pre>
- <p>결과물은 위와 다르지 않습니다.</p>
- <p></p><div translate="no" class="threejs_example_container notranslate">
- <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/multiple-scenes-copy-canvas.html"></iframe></div>
- <a class="threejs_center" href="/manual/examples/multiple-scenes-copy-canvas.html" target="_blank">새 탭에서 보기</a>
- </div>
- <p></p>
- <p>이 기법의 다른 장점은 <a href="https://developer.mozilla.org/ko/docs/Web/API/OffscreenCanvas"><code class="notranslate" translate="no">OffscreenCanvas</code></a>
- 웹 워커를 이용해 이 기능을 별도 스레드에서 구현할 수 있다는 겁니다. 하지만 아쉽게도
- 2020년 7월을 기준으로 <code class="notranslate" translate="no">OffscreenCanvas</code>는 아직 크로미움 기반 브라우저에서만 지원합니다.</p>
- </div>
- </div>
- </div>
- <script src="../resources/prettify.js"></script>
- <script src="../resources/lesson.js"></script>
- </body></html>
|