You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
			
				
					653 lines
				
				29 KiB
			
		
		
			
		
	
	
					653 lines
				
				29 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								<!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="/manual/resources/lesson.css">
							 | 
						||
| 
								 | 
							
								    <link rel="stylesheet" href="/manual/resources/lang.css">
							 | 
						||
| 
								 | 
							
								<!-- Import maps polyfill -->
							 | 
						||
| 
								 | 
							
								<!-- Remove this when import maps will be widely supported -->
							 | 
						||
| 
								 | 
							
								<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								<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({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="/manual/resources/prettify.js"></script>
							 | 
						||
| 
								 | 
							
								  <script src="/manual/resources/lesson.js"></script>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								</body></html>
							 |