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.
471 lines
32 KiB
471 lines
32 KiB
2 years ago
|
<!DOCTYPE html><html lang="ko"><head>
|
||
|
<meta charset="utf-8">
|
||
|
<title>씬 그래프(Scene graph)</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 – 씬 그래프(Scene graph)">
|
||
|
<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>씬 그래프(Scene graph)</h1>
|
||
|
</div>
|
||
|
<div class="lesson">
|
||
|
<div class="lesson-main">
|
||
|
<p>※ 이 글은 Three.js의 튜토리얼 시리즈로서,
|
||
|
먼저 <a href="fundamentals.html">Three.js의 기본 구조에 관한 글</a>을
|
||
|
읽고 오길 권장합니다.</p>
|
||
|
<p>Three.js에서 가장 중요한 것은 무엇보다 씬 그래프(Scene graph)입니다.
|
||
|
3D 엔진에서 씬 그래프란 요소(node)의 계층 구조를 그림으로 나타낸 것으로,
|
||
|
여기서 각 요소는 각각의 "지역 공간(local space)"을 가리킵니다.</p>
|
||
|
<p><img src="../resources/images/scenegraph-generic.svg" align="center"></p>
|
||
|
<p>예시가 다소 추상적이니 좀 더 이해하기 쉬운 걸 예로 들어보겠습니다.</p>
|
||
|
<p>태양계, 그 중에서도 태양, 지구, 달이 적당하겠네요.</p>
|
||
|
<p><img src="../resources/images/scenegraph-solarsystem.svg" align="center"></p>
|
||
|
<p>지구는 태양을 중심으로 공전합니다. 달은 지구를 중심으로 공전하죠.
|
||
|
달의 공전 궤도는 원과 유사합니다. 달의 관점에서 달은 지구의 "지역
|
||
|
공간" 안에서 공전하는 셈이죠. 태양이 봤을 때 달은 취한 사람처럼
|
||
|
스피로그래프(spirograph, 용수철 모양의 그래프)를 그리며 돌지만,
|
||
|
달은 그저 지구의 "지역 공간"을 도는 것에만 집중할 뿐입니다.</p>
|
||
|
<p></p><div class="threejs_diagram_container">
|
||
|
<iframe class="threejs_diagram " style="width: 400px; height: 300px;" src="/manual/foo/../resources/moon-orbit.html"></iframe>
|
||
|
</div>
|
||
|
|
||
|
<p></p>
|
||
|
<p>좀 더 가까운 예를 들어보죠. 우리는 지구에서 살지만 지구의 자전이나
|
||
|
자전축, 태양을 공전하는 일은 크게 신경쓰지 않습니다. 이건 지구의
|
||
|
일이니까요. 우리가 걷거나, 뭔가를 타고 이동하거나 수영하거나 달리거나
|
||
|
하는 일들은 지구의 일과는 무관해 보입니다. 그래서 옛날 사람들은 지구가
|
||
|
공전, 자전한다는 사실을 쉽게 받아들이지 못했죠. 우리가 걷든, 헤엄을
|
||
|
치든, 우리의 삶은 지구의 "지역 공간" 안에서 이루어집니다. 태양에서
|
||
|
봤을 때 여러분은 지구를 시속 약 1,600km로 돌고 태양의 주위를 시속 약
|
||
|
107,800km로 도는 셈이지만, 우리는 이렇게 빨리 움직이기 위해 따로
|
||
|
노력할 필요가 없습니다. 달과 마찬가지로 우리가 신경써야 하는 건 지구의
|
||
|
"지역 공간" 뿐이죠.</p>
|
||
|
<p>이제 위 예제를 Three.js로 하나씩 구현해볼 겁니다. 먼저 중점에
|
||
|
태양의 역할을 할 구체를 하나 놓는 것으로 시작하죠.</p>
|
||
|
<p>※ 앞으로 설명할 예제는 씬 그래프를 설명하기 위해 태양, 지구, 달을
|
||
|
활용합니다. 실제 태양, 지구, 달의 운행을 구현하려면 물리를 사용해야
|
||
|
하지만, 목적이 씬 그래프이니 씬 그래프로 실제 운행을 모방할 것입니다.</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">// 회전값을 업데이트할 객체들
|
||
|
const objects = [];
|
||
|
|
||
|
// 하나의 geometry로 모든 태양, 지구, 달을 생성
|
||
|
const radius = 1;
|
||
|
const widthSegments = 6;
|
||
|
const heightSegments = 6;
|
||
|
const sphereGeometry = new THREE.SphereGeometry(
|
||
|
radius, widthSegments, heightSegments);
|
||
|
|
||
|
const sunMaterial = new THREE.MeshPhongMaterial({emissive: 0xFFFF00});
|
||
|
const sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial);
|
||
|
sunMesh.scale.set(5, 5, 5); // 태양의 크기를 키움
|
||
|
scene.add(sunMesh);
|
||
|
objects.push(sunMesh);
|
||
|
</pre>
|
||
|
<p>예제에서는 로우-폴리(low poly) 구체를 사용할 겁니다. 적도를 중심으로
|
||
|
딱 6분할만 한 구체이죠. 이렇게 하면 자전 운동을 쉽게 확인할 수 있습니다.</p>
|
||
|
<p>같은 구체를 재활용할 것이므로 태양의 <code class="notranslate" translate="no">mesh</code>를 5배로 설정해줍니다.</p>
|
||
|
<p>다음으로 <a href="/docs/#api/ko/materials/MeshPhongMaterial"><code class="notranslate" translate="no">MeshPhongMaterial</code></a>의 <code class="notranslate" translate="no">emissive(방사성)</code> 속성(property)을
|
||
|
노랑으로 지정합니다. 퐁-메터리얼의 <code class="notranslate" translate="no">emissive</code> 속성은 빛을 반사하지 않는
|
||
|
표면 색상으로, 대신 광원에 해당 색상이 더해집니다.</p>
|
||
|
<p>씬 가운데에 단방향 조명(single point light)도 하나 넣습니다. 조명에
|
||
|
대해서는 나중에 자세히 다루기로 하고, 지금은 한 점에서 발산하는 광원
|
||
|
정도로 알아둡시다.</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
|
||
|
const color = 0xFFFFFF;
|
||
|
const intensity = 3;
|
||
|
const light = new THREE.PointLight(color, intensity);
|
||
|
scene.add(light);
|
||
|
}
|
||
|
</pre>
|
||
|
<p>예제를 쉽게 확인하기 위해 카메라를 중점 바로 위에서 아래로 내려다보게
|
||
|
설치합니다. 카메라의 시점을 바꾸는 가장 간단한 방법은 <code class="notranslate" translate="no">lookAt</code> 메서드를
|
||
|
활용하는 것으로, 이 메서드는 카메라가 넘겨받은 좌표를 바라보게끔 회전시켜줍니다.
|
||
|
하지만 이전에 먼저 카메라에게 어떤 방향이 위인지 알려줘야 합니다. 대부분의
|
||
|
경우 양의 y(positive y) 방향을 위로 설정하면 되지만, 예제의 경우 위에서
|
||
|
아래를 내려다 볼 것이므로 양의 z 방향이 위가 됩니다.</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
|
||
|
camera.position.set(0, 50, 0);
|
||
|
camera.up.set(0, 0, 1);
|
||
|
camera.lookAt(0, 0, 0);
|
||
|
</pre>
|
||
|
<p>이전 예제처럼 렌더링 루프에서 <code class="notranslate" translate="no">objects</code> 배열의 모든 객체를 회전시키겠습니다.</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">objects.forEach((obj) => {
|
||
|
obj.rotation.y = time;
|
||
|
});
|
||
|
</pre>
|
||
|
<p><code class="notranslate" translate="no">sunMesh</code>를 <code class="notranslate" translate="no">objects</code> 배열 안에 넣어놨으므로 태양 모델이 회전하는 것을
|
||
|
확인할 수 있습니다.</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/scenegraph-sun.html"></iframe></div>
|
||
|
<a class="threejs_center" href="/manual/examples/scenegraph-sun.html" target="_blank">새 탭에서 보기</a>
|
||
|
</div>
|
||
|
|
||
|
<p></p>
|
||
|
<p>다음으로 지구를 추가하겠습니다.</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const earthMaterial = new THREE.MeshPhongMaterial({color: 0x2233FF, emissive: 0x112244});
|
||
|
const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial);
|
||
|
earthMesh.position.x = 10;
|
||
|
scene.add(earthMesh);
|
||
|
objects.push(earthMesh);
|
||
|
</pre>
|
||
|
<p>지구는 푸른색을 사용했으나, 약간의 <em>방사성(emissive)</em> 파랑을 섞어
|
||
|
검은 배경에서 잘 보이도록 만들었습니다.</p>
|
||
|
<p>그리고 이전에 썼던 <code class="notranslate" translate="no">sphereGeometry</code>와 방금 만든 <code class="notranslate" translate="no">earthMaterial</code>을
|
||
|
이용해 <code class="notranslate" translate="no">earthMesh</code>를 만들고, 태양의 10칸 옆에 위치하도록 설정한 뒤
|
||
|
씬에 추가했습니다. 마지막으로 <code class="notranslate" translate="no">objects</code> 배열에 추가했으므로, 지구도
|
||
|
태양과 마찬가지로 자전하게 됩니다.</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/scenegraph-sun-earth.html"></iframe></div>
|
||
|
<a class="threejs_center" href="/manual/examples/scenegraph-sun-earth.html" target="_blank">새 탭에서 보기</a>
|
||
|
</div>
|
||
|
|
||
|
<p></p>
|
||
|
<p>하지만 지구가 태양의 주위를 돌진 않습니다. 지구를 바로 씬에 추가하는
|
||
|
대신, 태양의 자식으로 추가하면...</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-scene.add(earthMesh);
|
||
|
+sunMesh.add(earthMesh);
|
||
|
</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/scenegraph-sun-earth-orbit.html"></iframe></div>
|
||
|
<a class="threejs_center" href="/manual/examples/scenegraph-sun-earth-orbit.html" target="_blank">새 탭에서 보기</a>
|
||
|
</div>
|
||
|
|
||
|
<p></p>
|
||
|
<p>뭔가 이상합니다. 왜 지구의 크기와 태양의 크기가 같고 또 왜 저렇게
|
||
|
멀리 떨어졌을까요? 기존 카메라로는 지구가 보이지 않아 카메라의 위치도
|
||
|
150칸 위로 옮겼습니다.</p>
|
||
|
<p>방금 우리는 <code class="notranslate" translate="no">earthMesh</code>를 <code class="notranslate" translate="no">sunMesh</code>의 자식으로 추가했습니다. 이전에
|
||
|
<code class="notranslate" translate="no">sunMesh</code>를 만들 때 <code class="notranslate" translate="no">sunMesh.scale.set(5, 5, 5)</code>라는 코드로 크기를
|
||
|
5배로 설정했죠. 이는 <code class="notranslate" translate="no">sunMesh</code>의 "지역 공간" 자체를 5배 키우겠다는
|
||
|
의미입니다. 그래서 지구의 크기도 5배가 되었고, 거리(<code class="notranslate" translate="no">earthMesh.position.x = 10</code>)도
|
||
|
5배로 적용된 것이죠.</p>
|
||
|
<p>현재 예제의 씬 그래프는 다음과 같습니다.</p>
|
||
|
<p><img src="../resources/images/scenegraph-sun-earth.svg" align="center"></p>
|
||
|
<p>이를 해결하기 위해 빈 씬 그래프 요소를 하나 추가합니다. 그리고 태양과
|
||
|
지구 둘 다 이 요소의 자식으로 추가할 겁니다.</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const solarSystem = new THREE.Object3D();
|
||
|
+scene.add(solarSystem);
|
||
|
+objects.push(solarSystem);
|
||
|
|
||
|
const sunMaterial = new THREE.MeshPhongMaterial({emissive: 0xFFFF00});
|
||
|
const sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial);
|
||
|
sunMesh.scale.set(5, 5, 5);
|
||
|
-scene.add(sunMesh);
|
||
|
+solarSystem.add(sunMesh);
|
||
|
objects.push(sunMesh);
|
||
|
|
||
|
const earthMaterial = new THREE.MeshPhongMaterial({color: 0x2233FF, emissive: 0x112244});
|
||
|
const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial);
|
||
|
earthMesh.position.x = 10;
|
||
|
-sunMesh.add(earthMesh);
|
||
|
+solarSystem.add(earthMesh);
|
||
|
objects.push(earthMesh);
|
||
|
</pre>
|
||
|
<p>여기서는 <a href="/docs/#api/ko/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>를 생성했습니다. <a href="/docs/#api/ko/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>는 <a href="/docs/#api/ko/objects/Mesh"><code class="notranslate" translate="no">Mesh</code></a>와 마찬가지로
|
||
|
씬 그래프의 한 요소지만, <code class="notranslate" translate="no">material</code>이나 <code class="notranslate" translate="no">geometry</code>가 없다는 점이 다릅니다.
|
||
|
그저 하나의 빈 "지역 공간"인 셈이죠.</p>
|
||
|
<p>이제 씬 그래프는 다음과 같습니다.</p>
|
||
|
<p><img src="../resources/images/scenegraph-sun-earth-fixed.svg" align="center"></p>
|
||
|
<p><code class="notranslate" translate="no">sunMesh</code>와 <code class="notranslate" translate="no">earthMesh</code>는 <code class="notranslate" translate="no">solarSystem</code>의 자식입니다. 이 3 객체는 각각
|
||
|
회전하죠. 이제 <code class="notranslate" translate="no">earthMesh</code>는 <code class="notranslate" translate="no">sunMesh</code>의 자식이 아니므로 5배 커지지도
|
||
|
않았습니다.</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/scenegraph-sun-earth-orbit-fixed.html"></iframe></div>
|
||
|
<a class="threejs_center" href="/manual/examples/scenegraph-sun-earth-orbit-fixed.html" target="_blank">새 탭에서 보기</a>
|
||
|
</div>
|
||
|
|
||
|
<p></p>
|
||
|
<p>훨씬 낫네요. 지구는 태양보다 작고 태양을 공전하는 동시에 자전까지 합니다.</p>
|
||
|
<p>같은 패턴으로 달도 추가해봅시다.</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const earthOrbit = new THREE.Object3D();
|
||
|
+earthOrbit.position.x = 10;
|
||
|
+solarSystem.add(earthOrbit);
|
||
|
+objects.push(earthOrbit);
|
||
|
|
||
|
const earthMaterial = new THREE.MeshPhongMaterial({color: 0x2233FF, emissive: 0x112244});
|
||
|
const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial);
|
||
|
-solarSystem.add(earthMesh);
|
||
|
+earthOrbit.add(earthMesh);
|
||
|
objects.push(earthMesh);
|
||
|
|
||
|
+const moonOrbit = new THREE.Object3D();
|
||
|
+moonOrbit.position.x = 2;
|
||
|
+earthOrbit.add(moonOrbit);
|
||
|
|
||
|
+const moonMaterial = new THREE.MeshPhongMaterial({color: 0x888888, emissive: 0x222222});
|
||
|
+const moonMesh = new THREE.Mesh(sphereGeometry, moonMaterial);
|
||
|
+moonMesh.scale.set(.5, .5, .5);
|
||
|
+moonOrbit.add(moonMesh);
|
||
|
+objects.push(moonMesh);
|
||
|
</pre>
|
||
|
<p>이전처럼 <a href="/docs/#api/ko/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>를 이용해 <code class="notranslate" translate="no">eathOrbit</code> "지역 공간"을 만들고 거기에
|
||
|
<code class="notranslate" translate="no">earthMesh</code>와 <code class="notranslate" translate="no">moonMesh</code>를 추가했습니다. 씬 그래프는 다음과 같죠.</p>
|
||
|
<p><img src="../resources/images/scenegraph-sun-earth-moon.svg" align="center"></p>
|
||
|
<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/scenegraph-sun-earth-moon.html"></iframe></div>
|
||
|
<a class="threejs_center" href="/manual/examples/scenegraph-sun-earth-moon.html" target="_blank">새 탭에서 보기</a>
|
||
|
</div>
|
||
|
|
||
|
<p></p>
|
||
|
<p>처음에 봤던 예제처럼 달이 스피로그래프를 그리며 돌지만, 복잡한 수학적
|
||
|
연산이 하나도 들어가지 않았습니다. 우리가 한 건 씬 그래프에게 그 연산을
|
||
|
대신 맡긴 것 뿐이죠.</p>
|
||
|
<p>때론 씬 그래프의 요소를 시각화하는 것이 도움이 될 때도 있습니다.
|
||
|
Three.js는 유용한.. 음... 그러니까 이 <del>거시기</del>를 도와줄
|
||
|
헬퍼 클래스가 있습니다.</p>
|
||
|
<p>그 중 하나는 <a href="/docs/#api/ko/helpers/AxesHelper"><code class="notranslate" translate="no">AxesHelper</code></a>로, 이 클래스는 지역
|
||
|
<span style="color:red">X</span>,
|
||
|
<span style="color:green">Y</span>,
|
||
|
<span style="color:blue">Z</span> 축을 표시해줍니다.
|
||
|
한 번 여태까지 만든 요소에 모두 추가해보죠.</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">// AxesHelper 클래스를 각 요소에 지정
|
||
|
objects.forEach((node) => {
|
||
|
const axes = new THREE.AxesHelper();
|
||
|
axes.material.depthTest = false;
|
||
|
axes.renderOrder = 1;
|
||
|
node.add(axes);
|
||
|
});
|
||
|
</pre>
|
||
|
<p>우리는 축이 구체 내부에 있더라도 전부 보이길 원하므로, 각 축의 <code class="notranslate" translate="no">depthTest</code>를
|
||
|
<code class="notranslate" translate="no">false</code>로 설정합니다. 이러면 Three.js는 어떤 물체 뒤에 있는 요소를 그릴지
|
||
|
말지 검사하는 과정을 생략하므로, 어떤 방향에서라도 축을 볼 수 있습니다. 그리고
|
||
|
<code class="notranslate" translate="no">renderOrder</code>를 1로 설정(기본값은 0)해 구체를 전부 렌더링한 후 축을 렌더링하도록
|
||
|
합니다. 그렇지 않으면 축을 그린 후 구체가 그려져 보이지 않을 수도 있으니까요.</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/scenegraph-sun-earth-moon-axes.html"></iframe></div>
|
||
|
<a class="threejs_center" href="/manual/examples/scenegraph-sun-earth-moon-axes.html" target="_blank">새 탭에서 보기</a>
|
||
|
</div>
|
||
|
|
||
|
<p></p>
|
||
|
<p><span style="color:red">x축(빨강)</span> 그리고
|
||
|
<span style="color:blue">z축(파랑)</span> 축이 보이나요? 카메라가 바로 위에서
|
||
|
아래를 내려다 보고, 각 물체도 y축을 따라 회전하므로 <span style="color:green">y축(초록)</span>은
|
||
|
보여도 거의 점처럼 보일 겁니다.</p>
|
||
|
<p>몇몇 축은 2개의 축이 겹쳐져 구별이 어려울 수 있습니다. <code class="notranslate" translate="no">sunMesh</code>와 <code class="notranslate" translate="no">solarSystem</code>,
|
||
|
<code class="notranslate" translate="no">earthMesh</code>와 <code class="notranslate" translate="no">earthOrbit</code>이 같은 위치에 있기 때문이죠. 각 노드의 축을 켜고
|
||
|
끌 수 있는 간단한 컨트롤 패널을 한 번 만들어보죠. 동시에 다른 헬퍼 클래스인
|
||
|
<a href="/docs/#api/ko/helpers/GridHelper"><code class="notranslate" translate="no">GridHelper</code></a>도 추가해보겠습니다. <a href="/docs/#api/ko/helpers/GridHelper"><code class="notranslate" translate="no">GridHelper</code></a>는 X, Z축으로 2D 격자(grid)를
|
||
|
만다는 클래스로, 기본값은 10x10 칸입니다.</p>
|
||
|
<p>또 Three.js와 함께 사용하기로 유명한 <a href="https://github.com/georgealways/lil-gui">lil-gui</a>도
|
||
|
사용할 겁니다. lil-gui는 UI 라이브러리로, 객체와 속성 이름을 넘겨받고, 해당 속성의
|
||
|
타입을 기반으로 속성값을 UI로 조정할 수 있게 해줍니다.</p>
|
||
|
<p>각 요소에 <a href="/docs/#api/ko/helpers/GridHelper"><code class="notranslate" translate="no">GridHelper</code></a>와 <a href="/docs/#api/ko/helpers/AxesHelper"><code class="notranslate" translate="no">AxesHelper</code></a>를 추가하겠습니다. 각 노드에 헬퍼를
|
||
|
추가하기 위해 각 노드의 이름이 필요하니, 기존 렌더링 루프를 제거하고 특정
|
||
|
함수를 호출하게 변경하겠습니다.</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-// add an AxesHelper to each node
|
||
|
-objects.forEach((node) => {
|
||
|
- const axes = new THREE.AxesHelper();
|
||
|
- axes.material.depthTest = false;
|
||
|
- axes.renderOrder = 1;
|
||
|
- node.add(axes);
|
||
|
-});
|
||
|
|
||
|
+function makeAxisGrid(node, label, units) {
|
||
|
+ const helper = new AxisGridHelper(node, units);
|
||
|
+ gui.add(helper, 'visible').name(label);
|
||
|
+}
|
||
|
+
|
||
|
+makeAxisGrid(solarSystem, 'solarSystem', 25);
|
||
|
+makeAxisGrid(sunMesh, 'sunMesh');
|
||
|
+makeAxisGrid(earthOrbit, 'earthOrbit');
|
||
|
+makeAxisGrid(earthMesh, 'earthMesh');
|
||
|
+makeAxisGrid(moonMesh, 'moonMesh');
|
||
|
</pre>
|
||
|
<p><code class="notranslate" translate="no">makeAxisGrid</code> 함수는 나중에 만들 <code class="notranslate" translate="no">AxisGridHelper</code>를 생성하여
|
||
|
lil-gui에 붙이는 역할을 합니다. 예제에서는 체크박스를 만들 것이므로,
|
||
|
<code class="notranslate" translate="no">boolean</code> 타입으로 속성을 지정해주겠습니다. 또 하나의 속성이 바뀔 때
|
||
|
축과 격자가 동시에 나타나고 사라지게 할 것이니 getter와 setter가
|
||
|
있는 간단한 클래스를 하나 만들겠습니다. 이러면 lil-gui가 하나의
|
||
|
속성을 바꿀 때 요소의 <a href="/docs/#api/ko/helpers/AxesHelper"><code class="notranslate" translate="no">AxesHelper</code></a>와 <a href="/docs/#api/ko/helpers/GridHelper"><code class="notranslate" translate="no">GridHelper</code></a>의 속성을
|
||
|
동시에 조작할 수 있죠.</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">/*
|
||
|
* 축과 격자를 동시에 켜고 끕니다
|
||
|
* lil-gui가 체크박스를 만들게 하려면 boolean 타입의
|
||
|
* 속성을 지정해줘야 하므로, `visible` 속성에
|
||
|
* getter와 setter를 지정해 lil-gui가 이 속성을
|
||
|
* 바라보도록 합니다
|
||
|
*/
|
||
|
class AxisGridHelper {
|
||
|
constructor(node, units = 10) {
|
||
|
const axes = new THREE.AxesHelper();
|
||
|
axes.material.depthTest = false;
|
||
|
axes.renderOrder = 2; // 격자 다음에 렌더링
|
||
|
node.add(axes);
|
||
|
|
||
|
const grid = new THREE.GridHelper(units, units);
|
||
|
grid.material.depthTest = false;
|
||
|
grid.renderOrder = 1;
|
||
|
node.add(grid);
|
||
|
|
||
|
this.grid = grid;
|
||
|
this.axes = axes;
|
||
|
this.visible = false;
|
||
|
}
|
||
|
get visible() {
|
||
|
return this._visible;
|
||
|
}
|
||
|
set visible(v) {
|
||
|
this._visible = v;
|
||
|
this.grid.visible = v;
|
||
|
this.axes.visible = v;
|
||
|
}
|
||
|
}
|
||
|
</pre>
|
||
|
<p>격자가 축을 가릴 수 있으니, <a href="/docs/#api/ko/helpers/AxesHelper"><code class="notranslate" translate="no">AxesHelper</code></a>의 <code class="notranslate" translate="no">renderOrder</code>를
|
||
|
2로 설정하고 <a href="/docs/#api/ko/helpers/GridHelper"><code class="notranslate" translate="no">GridHelper</code></a>를 2로 설정해 축을 격자 다음에
|
||
|
렌더링하도록 합니다.</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/scenegraph-sun-earth-moon-axes-grids.html"></iframe></div>
|
||
|
<a class="threejs_center" href="/manual/examples/scenegraph-sun-earth-moon-axes-grids.html" target="_blank">새 탭에서 보기</a>
|
||
|
</div>
|
||
|
|
||
|
<p></p>
|
||
|
<p><code class="notranslate" translate="no">solarSystem</code>을 체크하면 위에서 설정했듯 지구가 정확히 중앙으로부터
|
||
|
10칸 떨어진 것을 확인할 수 있습니다. 지구가 <code class="notranslate" translate="no">solarSystem</code> "지역 공간"
|
||
|
안에 있는 것도 확인할 수 있죠. <code class="notranslate" translate="no">earthOrbit</code>을 켜면 달도 마찬가지로
|
||
|
<code class="notranslate" translate="no">earthOrbit</code>의 "지역 공간"의 중심으로부터 정확히 2칸 떨어진 것을
|
||
|
확인할 수 있을 겁니다.</p>
|
||
|
<p>씬 그래프의 다른 예시로 자동차를 들 수 있습니다.</p>
|
||
|
<p><img src="../resources/images/scenegraph-car.svg" align="center"></p>
|
||
|
<p>차체(Car body)를 움직이면 바퀴(wheel)도 같이 움직입니다. 차체가
|
||
|
바퀴와는 별도로 튀게 하려면(서스펜션. 역주) 차체와 바퀴를 하나의
|
||
|
차체의 "프레임" 요소의 자식으로 설정할 수 있죠.</p>
|
||
|
<p>다른 예로 게임 속 인간형 캐릭터를 한 번 봅시다.</p>
|
||
|
<p><img src="../resources/images/scenegraph-human.svg" align="center"></p>
|
||
|
<p>인간형 캐릭터의 씬 그래프는 꽤 복잡하네요. 위 씬 그래프는 상당히 축소된
|
||
|
버젼인데도 말이죠. 좀 더 세세하게 만든다면 손가락 하나하나(최소한 28마디)와
|
||
|
발가락 하나하나(또 다른 28마디), 얼굴과 턱, 눈 등등으로 나눠야 합니다.</p>
|
||
|
<p>약간 복잡한 씬 그래프를 만들어 봅시다. 탱크가 좋겠네요. 바퀴 6개와
|
||
|
포탑으로 이루어진 간단한 탱크입니다. 또 탱크의 주위를 돌아다니는 구체를
|
||
|
하나 만들어 탱크가 그 구체를 조준하도록 해보겠습니다.</p>
|
||
|
<p>아래는 예제를 구현하기 위한 씬 그래프입니다. <code class="notranslate" translate="no">mesh</code>는 녹색으로 칠했고,
|
||
|
<a href="/docs/#api/ko/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>는 청색, 광원은 갈색, 카메라는 보라색으로 칠했습니다. 하나의
|
||
|
카메라는 씬 그래프에 포함하지 않았습니다.</p>
|
||
|
<div class="threejs_center"><img src="../resources/images/scenegraph-tank.svg" style="width: 800px;"></div>
|
||
|
|
||
|
<p>모든 요소를 어떻게 설정했는지 코드를 하나씩 살펴보죠.</p>
|
||
|
<p>탱크가 조준할 목표를 만들기 위해 먼저 위 예제의 <code class="notranslate" translate="no">earthOrbit</code>과 유사한
|
||
|
<code class="notranslate" translate="no">targetOrbit</code>(<a href="/docs/#api/ko/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>)을 만듭니다. 그리고 <code class="notranslate" translate="no">targetOrbit</code>의 상대 좌표를 넘겨줄
|
||
|
<code class="notranslate" translate="no">targetElevation</code>(<a href="/docs/#api/ko/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>)을 만들어 <code class="notranslate" translate="no">targetOrbit</code>의 자식으로 추가한 뒤,
|
||
|
또 다른 <a href="/docs/#api/ko/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>, <code class="notranslate" translate="no">targetBob</code>을 만들어 <code class="notranslate" translate="no">targetElevation</code>의 자식으로 추가합니다.
|
||
|
이 <code class="notranslate" translate="no">targetBob</code>은 위아래로 보빙(bob은 낙시찌, 권투에서 bobbing은 몸을 숙이는 동작을 말함. 역주)하는
|
||
|
역할을 할 겁니다. 마지막으로 색이 색이 바뀌는 동시에 회전할 <code class="notranslate" translate="no">targetMesh</code> 육면체를
|
||
|
만듭니다.</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">// 움직이는 목표
|
||
|
targetOrbit.rotation.y = time * .27;
|
||
|
targetBob.position.y = Math.sin(time * 2) * 4;
|
||
|
targetMesh.rotation.x = time * 7;
|
||
|
targetMesh.rotation.y = time * 13;
|
||
|
targetMaterial.emissive.setHSL(time * 10 % 1, 1, .25);
|
||
|
targetMaterial.color.setHSL(time * 10 % 1, 1, .25);
|
||
|
</pre>
|
||
|
<p>탱크는 먼저 <code class="notranslate" translate="no">tank</code>라는 이름으로 다른 요소를 감쌀 <a href="/docs/#api/ko/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>를 하나 생성합니다.
|
||
|
예제에서는 커브에 따라 위치값을 반환받을 수 있는 <a href="/docs/#api/ko/extras/curves/SplineCurve"><code class="notranslate" translate="no">SplineCurve</code></a>를 이용하겠습니다.
|
||
|
0.0은 커브의 시작점이고, 1.0은 커브의 끝점으로, 먼저 탱크의 위치를 넘겨주어 탱크의
|
||
|
다음 위치를 정한 뒤(아래 <code class="notranslate" translate="no">tankPosition</code>. 역주), 커브의 다음 값을 받아 탱크가 어디를
|
||
|
바라봐야할지 구합니다(아래 <code class="notranslate" translate="no">tankTarget</code>. 역주). 그리고 구한 값을 <a href="/docs/#api/ko/core/Object3D.lookAt"><code class="notranslate" translate="no">Object3D.lookAt</code></a>
|
||
|
메서드에 넘겨주어 탱크가 그 방향을 바라보도록 합니다.</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const tankPosition = new THREE.Vector2();
|
||
|
const tankTarget = new THREE.Vector2();
|
||
|
|
||
|
...
|
||
|
|
||
|
// move tank
|
||
|
const tankTime = time * .05;
|
||
|
curve.getPointAt(tankTime % 1, tankPosition);
|
||
|
curve.getPointAt((tankTime + 0.01) % 1, tankTarget);
|
||
|
tank.position.set(tankPosition.x, 0, tankPosition.y);
|
||
|
tank.lookAt(tankTarget.x, 0, tankTarget.y);
|
||
|
</pre>
|
||
|
<p>그 다음 탱크의 포탑을 탱크의 자식으로 지정해서 탱크를 따라 움직이게 합니다.
|
||
|
그리고 목표물의 전역 위치값(global position)을 구한 뒤 <a href="/docs/#api/ko/core/Object3D.lookAt"><code class="notranslate" translate="no">Object3D.lookAt</code></a>
|
||
|
메서드를 이용, 포탑이 목표물을 조준하게 합니다.</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const targetPosition = new THREE.Vector3();
|
||
|
|
||
|
...
|
||
|
|
||
|
// 목표를 조준하도록
|
||
|
targetMesh.getWorldPosition(targetPosition);
|
||
|
turretPivot.lookAt(targetPosition);
|
||
|
</pre>
|
||
|
<p><code class="notranslate" translate="no">turretCamera</code>를 <code class="notranslate" translate="no">turretMesh</code>의 자식으로 지정해 포탑과 함께 카메라가
|
||
|
움직이도록 설정합니다. 또 카메라도 목표물을 바라보게 변경합니다.</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">// 포탑 카메라가 목표물을 바라보도록
|
||
|
turretCamera.lookAt(targetPosition);
|
||
|
</pre>
|
||
|
<p><code class="notranslate" translate="no">targetCameraPivot</code>은 <code class="notranslate" translate="no">targetBob</code>의 자식으로 지정해 목표물과 함께
|
||
|
돌아다니도록 하고, 탱크의 뒤쪽을 바라보도록 합니다. 이는 <code class="notranslate" translate="no">targetCamera</code>가
|
||
|
목표물의 위치에서 살짝 벗어나게 하기 위함으로, 만약 카메라를 <code class="notranslate" translate="no">targetBob</code>의
|
||
|
자식으로 바로 추가한다면 목표물 안에서 탱크를 보게 될 겁니다.</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">// targetCameraPivot이 탱크를 바라보도록
|
||
|
tank.getWorldPosition(targetPosition);
|
||
|
targetCameraPivot.lookAt(targetPosition);
|
||
|
</pre>
|
||
|
<p>다음으로 바퀴를 회전시킵니다.</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">wheelMeshes.forEach((obj) => {
|
||
|
obj.rotation.x = time * 3;
|
||
|
});
|
||
|
</pre>
|
||
|
<p>그리고 카메라를 간단한 설명과 함께 배열로 묶은 뒤,</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cameras = [
|
||
|
{ cam: camera, desc: 'detached camera', },
|
||
|
{ cam: turretCamera, desc: 'on turret looking at target', },
|
||
|
{ cam: targetCamera, desc: 'near target looking at tank', },
|
||
|
{ cam: tankCamera, desc: 'above back of tank', },
|
||
|
];
|
||
|
|
||
|
const infoElem = document.querySelector('#info');
|
||
|
</pre>
|
||
|
<p>시간에 따라 카메라를 변경하도록 합니다.</p>
|
||
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const camera = cameras[time * .25 % cameras.length | 0];
|
||
|
infoElem.textContent = camera.desc;
|
||
|
</pre>
|
||
|
<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/scenegraph-tank.html"></iframe></div>
|
||
|
<a class="threejs_center" href="/manual/examples/scenegraph-tank.html" target="_blank">새 탭에서 보기</a>
|
||
|
</div>
|
||
|
|
||
|
<p></p>
|
||
|
<p>자, 이번 장은 여기까지입니다. 이 글이 씬 그래프가 어떻게 작동하는지,
|
||
|
어떻게 사용해야할지 감을 잡는 데 도움이 되었으면 좋겠네요. <a href="/docs/#api/ko/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>
|
||
|
요소를 만들어 부모로 만드는 것은 Three.js 뿐만 아니라 다른 3D 엔진을
|
||
|
쓸 때도 중요한 요소입니다. 뭔가를 만들다보면 종종 복잡한 수학이 필요한
|
||
|
것처럼 느껴질 수 있는데, 이때 씬 그래프를 사용하지 않는다면 달의 궤도를
|
||
|
계산하거나 자동차 바퀴의 위치를 계산하는 건 굉장히 복잡할 겁니다. 씬
|
||
|
그래프를 적절히 활용하면 이런 복잡한 동작을 더 쉽게 구현할 수 있죠.</p>
|
||
|
<p><a href="materials.html">다음 장에서는 <code class="notranslate" translate="no">재질(material)</code>에 대해 알아보겠습니다</a>.</p>
|
||
|
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<script src="/manual/resources/prettify.js"></script>
|
||
|
<script src="/manual/resources/lesson.js"></script>
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
</body></html>
|