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.
602 lines
37 KiB
602 lines
37 KiB
<!DOCTYPE html><html lang="ko"><head>
|
|
<meta charset="utf-8">
|
|
<title>에서 .OBJ 파일 불러오기</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 – 에서 .OBJ 파일 불러오기">
|
|
<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>에서 .OBJ 파일 불러오기</h1>
|
|
</div>
|
|
<div class="lesson">
|
|
<div class="lesson-main">
|
|
<p>Three.js로 프로젝트를 진행할 때, 3D 모델 파일을 불러와 사용하는 것은
|
|
아주 흔한 일입니다. 오늘은 주로 사용하는 파일 형식인 .OBJ 파일을
|
|
불러오는 법에 대해 알아보겠습니다.</p>
|
|
<p>인터넷을 검색해 <a href="https://www.blendswap.com/blends/view/69174">CC-BY-NC 3.0 풍자 3D 모델</a>을
|
|
하나 가져왔습니다(작가: <a href="https://www.blendswap.com/user/ahedov">ahedov</a>).</p>
|
|
<div class="threejs_center"><img src="../resources/images/windmill-obj.jpg"></div>
|
|
|
|
<p>다운받은 파일 형식이 .blend네요. <a href="https://blender.org">블렌더(Blender)</a>로
|
|
파일을 열어 .OBJ 형식으로 변환하겠습니다.</p>
|
|
<div class="threejs_center"><img style="width: 827px;" src="../resources/images/windmill-export-as-obj.jpg"></div>
|
|
|
|
<blockquote>
|
|
<p>블렌더는 다른 프로그램과 다른 점이 많아 낯설게 느껴질 수 있습니다.
|
|
블렌더를 처음 접한다면, 글 읽기를 잠시 멈추고 블렌더의 기본 UI 가이드를
|
|
먼저 읽어보길 권장합니다.</p>
|
|
<p>추가로 보통 3D 프로그램은 수천 가지 기능을 지원하는 거대 함선과 같습니다.
|
|
프로그램들 중에서도 복잡하기로 유명하죠. 1996년, 제가 3D Studio Max를 처음
|
|
배우기 시작했을 때 저는 하루에 몇 시간씩 3주를 들여 공식 매뉴얼의 70% 정도를
|
|
정독했습니다. 그리고 그게 몇 년 뒤 마야(Maya)를 배울 때 도움이 많이 됐죠. 3D
|
|
모델을 만들든, 기존 모델을 수정하든, 3D 프로그램으로 무언가를 하고 싶다면 강의나
|
|
튜토리얼에 따로 시간을 투자하기 바랍니다.</p>
|
|
</blockquote>
|
|
<p>특별한 일이 없다면 저는 파일을 내보낼 때 아래의 옵션을 사용합니다.</p>
|
|
<div class="threejs_center"><img style="width: 239px;" src="../resources/images/windmill-export-options.jpg"></div>
|
|
|
|
<p>자 이제 한 번 화면에 띄워보죠!</p>
|
|
<p><a href="lights.html">조명에 관한 글</a>에서 썼던 예제를 가져와 이 예제를
|
|
반구광(hemisphere light) 예제와 합칩니다. 그러면 장면에는 <a href="/docs/#api/ko/lights/HemisphereLight"><code class="notranslate" translate="no">HemisphereLight</code></a>
|
|
하나, <a href="/docs/#api/ko/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a> 하나가 있는 셈입니다. 또 GUI 관련 코드와 정육면체,
|
|
구체 관련 코드도 지웁니다.</p>
|
|
<p>다음으로 먼저 <a href="/docs/#examples/loaders/OBJLoader"><code class="notranslate" translate="no">OBJLoader</code></a> 모듈을 스크립트에 로드합니다.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
|
|
</pre>
|
|
<p><a href="/docs/#examples/loaders/OBJLoader"><code class="notranslate" translate="no">OBJLoader</code></a>의 인스턴스를 생성한 뒤 .OBJ 파일의 경로와 콜백 함수를 넘겨
|
|
<code class="notranslate" translate="no">load</code> 메서드를 실행합니다. 그리고 콜백 함수에서 불러온 모델을 장면에
|
|
추가합니다.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
|
|
const objLoader = new OBJLoader();
|
|
objLoader.load('resources/models/windmill/windmill.obj', (root) => {
|
|
scene.add(root);
|
|
});
|
|
}
|
|
</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/load-obj-no-materials.html"></iframe></div>
|
|
<a class="threejs_center" href="/manual/examples/load-obj-no-materials.html" target="_blank">새 탭에서 보기</a>
|
|
</div>
|
|
|
|
<p></p>
|
|
<p>뭔가 성공한 듯하지만 재질(materials)이 없어 오류가 납니다. .OBJ 파일에도
|
|
재질이 없고 따로 재질을 지정하지도 않았기 때문이죠.</p>
|
|
<p>위에서 생성한 .OBJ 로더에는 이름 : 재질 쌍을 객체로 지정할 수 있습니다.
|
|
.OBJ 파일을 불러올 때, 이름이 지정되었다면 로더에 지정한 재질 중에 이름(키)과
|
|
일치하는 재질을 찾아 사용하고, 재질을 찾지 못했다면 기본 재질을 사용하죠.</p>
|
|
<p>.OBJ 파일을 생성할 때 재질에 대한 데이터를 담은 .MTL 파일이 같이 생성되기도
|
|
합니다. 방금의 경우에도 .MTL 파일이 같이 생성되었죠. MTL 파일은 ASCII 인코딩이므로
|
|
일반 텍스트 파일처럼 열어볼 수 있습니다.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-mtl" translate="no"># Blender MTL File: 'windmill_001.blend'
|
|
# Material Count: 2
|
|
|
|
newmtl Material
|
|
Ns 0.000000
|
|
Ka 1.000000 1.000000 1.000000
|
|
Kd 0.800000 0.800000 0.800000
|
|
Ks 0.000000 0.000000 0.000000
|
|
Ke 0.000000 0.000000 0.000000
|
|
Ni 1.000000
|
|
d 1.000000
|
|
illum 1
|
|
map_Kd windmill_001_lopatky_COL.jpg
|
|
map_Bump windmill_001_lopatky_NOR.jpg
|
|
|
|
newmtl windmill
|
|
Ns 0.000000
|
|
Ka 1.000000 1.000000 1.000000
|
|
Kd 0.800000 0.800000 0.800000
|
|
Ks 0.000000 0.000000 0.000000
|
|
Ke 0.000000 0.000000 0.000000
|
|
Ni 1.000000
|
|
d 1.000000
|
|
illum 1
|
|
map_Kd windmill_001_base_COL.jpg
|
|
map_Bump windmill_001_base_NOR.jpg
|
|
map_Ns windmill_001_base_SPEC.jpg
|
|
</pre>
|
|
<p>파일을 살펴보면 2개의 재질과 5개의 jpg 텍스처가 보이는데, 텍스처 파일은
|
|
디렉토리 내에 보이지 않습니다. 대체 어디에 있는 걸까요?</p>
|
|
<div class="threejs_center"><img style="width: 757px;" src="../resources/images/windmill-exported-files.png"></div>
|
|
|
|
<p>생성된 거라고는 .OBJ 파일 하나와 .MTL 파일 하나 뿐입니다.</p>
|
|
<p>사실 방금 사용한 모델의 텍스처는 .blend 파일에 포함되어 있습니다.
|
|
<strong>File->External Data->Unpack All Into Files</strong>를 선택하고</p>
|
|
<div class="threejs_center"><img style="width: 828px;" src="../resources/images/windmill-export-textures.jpg"></div>
|
|
|
|
<p><strong>Write Files to Current Directory</strong>를 선택해 텍스처를 별도 파일로
|
|
내보낼 수 있습니다.</p>
|
|
<div class="threejs_center"><img style="width: 828px;" src="../resources/images/windmill-overwrite.jpg"></div>
|
|
|
|
<p>이러면 .blend 파일과 같은 경로의 <strong>textures</strong> 폴더 안에 텍스처 파일이
|
|
생성됩니다.</p>
|
|
<div class="threejs_center"><img style="width: 758px;" src="../resources/images/windmill-exported-texture-files.png"></div>
|
|
|
|
<p>내보낸 텍스처를 복사해 .OBJ 파일과 같은 경로에 두겠습니다.</p>
|
|
<div class="threejs_center"><img style="width: 757px;" src="../resources/images/windmill-exported-files-with-textures.png"></div>
|
|
|
|
<p>이제 .MTL 파일에서 사용할 텍스처를 생성했으니 .MTL 파일을 불러오도록 합시다.</p>
|
|
<p><a href="/docs/#examples/loaders/MTLLoader"><code class="notranslate" translate="no">MTLLoader</code></a> 모듈을 불러옵니다.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
|
|
+import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';
|
|
</pre>
|
|
<p></p>
|
|
<p>우선 .MTL 파일을 불러와 <code class="notranslate" translate="no">MtlObjBridge</code>로 재질을 만듭니다. 그리고 <a href="/docs/#examples/loaders/OBJLoader"><code class="notranslate" translate="no">OBJLoader</code></a>
|
|
인스턴스에 방금 만든 재질을 추가한 뒤 .OBJ 파일을 불러옵니다.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
|
|
+ const mtlLoader = new MTLLoader();
|
|
+ mtlLoader.load('resources/models/windmill/windmill.mtl', (mtl) => {
|
|
+ mtl.preload();
|
|
+ objLoader.setMaterials(mtl);
|
|
objLoader.load('resources/models/windmill/windmill.obj', (root) => {
|
|
scene.add(root);
|
|
});
|
|
+ });
|
|
}
|
|
</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/load-obj-materials.html"></iframe></div>
|
|
<a class="threejs_center" href="/manual/examples/load-obj-materials.html" target="_blank">새 탭에서 보기</a>
|
|
</div>
|
|
|
|
<p></p>
|
|
<p>얼핏 제대로 불러온 것 같지만 아직 부족한 점이 있습니다. 모델을 이리저리
|
|
회전시켜 보면 풍차의 날개 뒷면이 사라지는 것을 볼 수 있을 겁니다.</p>
|
|
<div class="threejs_center"><img style="width: 528px;" src="../resources/images/windmill-missing-cloth.jpg"></div>
|
|
|
|
<p><a href="materials.html">재질에 관한 글</a>을 읽었다면 원인이 무엇인지 알
|
|
겁니다. 일단 풍차의 날개 양면을 모두 렌더링하도록 설정해야 겠네요. .MTL
|
|
파일을 직접 수정하기는 어렵습니다. 그렇다면 쉽게 떠올릴 수 있는 방법은
|
|
3가지 정도죠.</p>
|
|
<ol>
|
|
<li><p>모든 재질을 불러온 뒤 반복문으로 처리한다.</p>
|
|
<pre class="prettyprint showlinemods notranslate notranslate" translate="no"> const mtlLoader = new MTLLoader();
|
|
mtlLoader.load('resources/models/windmill/windmill.mtl', (mtl) => {
|
|
mtl.preload();
|
|
for (const material of Object.values(mtl.materials)) {
|
|
material.side = THREE.DoubleSide;
|
|
}
|
|
...
|
|
</pre><p>문제가 해결되긴 하겠지만, 양면 렌더링은 단면 렌더링에 비해 성능이 느립니다.
|
|
양면일 필요가 있는 재질만 양면으로 렌더링하는 게 이상적이겠죠.</p>
|
|
</li>
|
|
<li><p>특정 재질을 골라 설정한다.</p>
|
|
<p>.MTL 파일에는 <code class="notranslate" translate="no">"windmill"</code>, <code class="notranslate" translate="no">"Material"</code> 2개의 재질이 있습니다. 여러 번의 시도와
|
|
에러 끝에 날개가 <code class="notranslate" translate="no">"Material"</code>이라는 이름의 재질을 쓴다는 것을 알아낸 뒤, 이 재질에만
|
|
양면 속성을 설정할 수도 있을 겁니다.</p>
|
|
<pre class="prettyprint showlinemods notranslate notranslate" translate="no"> const mtlLoader = new MTLLoader();
|
|
mtlLoader.load('resources/models/windmill/windmill.mtl', (mtl) => {
|
|
mtl.preload();
|
|
mtl.materials.Material.side = THREE.DoubleSide;
|
|
...
|
|
</pre></li>
|
|
<li><p>.MTL 파일의 한계에 굴복하고 직접 재질을 만든다.</p>
|
|
<pre class="prettyprint showlinemods notranslate notranslate" translate="no"> objLoader.load('resources/models/windmill/windmill.obj', (root) => {
|
|
const materials = {
|
|
Material: new THREE.MeshPhongMaterial({...}),
|
|
windmill: new THREE.MeshPhongMaterial({...}),
|
|
};
|
|
root.traverse(node => {
|
|
const material = materials[node.material?.name];
|
|
if (material) {
|
|
node.material = material;
|
|
}
|
|
})
|
|
scene.add(root);
|
|
});
|
|
</pre></li>
|
|
</ol>
|
|
<p>뭘 선택하든 그건 여러분의 선택입니다. 1번이 가장 간단하고, 3번이 가장
|
|
확장성이 좋죠. 2번은 그 중간입니다. 지금은 2번 해결책을 사용하도록 하죠.</p>
|
|
<p>해결책을 적용하면 날개가 제대로 보일 겁니다. 하지만 문제가 하나 더 남았습니다.
|
|
모델을 확대해보면 텍스처가 굉장히 각져 보일 거예요.</p>
|
|
<div class="threejs_center"><img style="width: 700px;" src="../resources/images/windmill-blocky.jpg"></div>
|
|
|
|
<p>뭐가 문제일까요?</p>
|
|
<p>텍스처 파일 중에는 NOR, 법선 맵(NORmal map)이라는 이름이 붙은 파일이 있습니다.
|
|
이 파일이 바로 법선 맵이죠. 범프 맵(bump map)이 흑백이라면 법선 맵은 보통
|
|
자주색을 띱니다. 범프 맵이 표면의 높이를 나타낸다면 법선 맵은 표면의 방향을
|
|
나타내죠.</p>
|
|
<div class="threejs_center"><img style="width: 256px;" src="../examples/resources/models/windmill/windmill_001_base_NOR.jpg"></div>
|
|
|
|
<p><a href="https://github.com/mrdoob/three.js/blob/1a560a3426e24bbfc9ca1f5fb0dfb4c727d59046/examples/js/loaders/MTLLoader.js#L432">MTLLoader의 소스 코드</a>를
|
|
살펴보면 법선 맵의 키(key)가 <code class="notranslate" translate="no">norm</code>이어야 한다고 합니다. 간단히 .MTL 파일을
|
|
수정해보죠.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-mtl" translate="no"># Blender MTL File: 'windmill_001.blend'
|
|
# Material Count: 2
|
|
|
|
newmtl Material
|
|
Ns 0.000000
|
|
Ka 1.000000 1.000000 1.000000
|
|
Kd 0.800000 0.800000 0.800000
|
|
Ks 0.000000 0.000000 0.000000
|
|
Ke 0.000000 0.000000 0.000000
|
|
Ni 1.000000
|
|
d 1.000000
|
|
illum 1
|
|
map_Kd windmill_001_lopatky_COL.jpg
|
|
-map_Bump windmill_001_lopatky_NOR.jpg
|
|
+norm windmill_001_lopatky_NOR.jpg
|
|
|
|
newmtl windmill
|
|
Ns 0.000000
|
|
Ka 1.000000 1.000000 1.000000
|
|
Kd 0.800000 0.800000 0.800000
|
|
Ks 0.000000 0.000000 0.000000
|
|
Ke 0.000000 0.000000 0.000000
|
|
Ni 1.000000
|
|
d 1.000000
|
|
illum 1
|
|
map_Kd windmill_001_base_COL.jpg
|
|
-map_Bump windmill_001_base_NOR.jpg
|
|
+norm windmill_001_base_NOR.jpg
|
|
map_Ns windmill_001_base_SPEC.jpg
|
|
</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/load-obj-materials-fixed.html"></iframe></div>
|
|
<a class="threejs_center" href="/manual/examples/load-obj-materials-fixed.html" target="_blank">새 탭에서 보기</a>
|
|
</div>
|
|
|
|
<p></p>
|
|
<p>다른 파일도 불러와봅시다.</p>
|
|
<p>인터넷을 뒤져 <a href="https://creativecommons.org/licenses/by-nc/4.0/">CC-BY-NC</a>
|
|
풍차 3D 모델을 발견했습니다(작가: <a href="http://www.gerzi.ch/">Roger Gerzner / GERIZ.3D Art</a>).</p>
|
|
<div class="threejs_center"><img src="../resources/images/windmill-obj-2.jpg"></div>
|
|
|
|
<p>.OBJ 형식으로 다운 받을 수 있으므로, 해당 형식으로 받아 불러오겠습니다(잠깐
|
|
.MTL 로더를 제거했습니다).</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">- objLoader.load('resources/models/windmill/windmill.obj', ...
|
|
+ objLoader.load('resources/models/windmill-2/windmill.obj', ...
|
|
</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/load-obj-wat.html"></iframe></div>
|
|
<a class="threejs_center" href="/manual/examples/load-obj-wat.html" target="_blank">새 탭에서 보기</a>
|
|
</div>
|
|
|
|
<p></p>
|
|
<p>음, 아무것도 나타나지 않습니다. 뭐가 문제일까요? 모델의 원래 크기 때문일까요?
|
|
Three.js로부터 모델 사이즈를 구해 카메라를 한 번 업데이트해보겠습니다.</p>
|
|
<p>먼저 Three.js가 방금 불러온 모델을 감싸는 육면체를 계산해 모델의 크기와 중심점을
|
|
구하는 코드를 작성합니다.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">objLoader.load('resources/models/windmill_2/windmill.obj', (root) => {
|
|
scene.add(root);
|
|
|
|
+ const box = new THREE.Box3().setFromObject(root);
|
|
+ const boxSize = box.getSize(new THREE.Vector3()).length();
|
|
+ const boxCenter = box.getCenter(new THREE.Vector3());
|
|
+ console.log(boxSize);
|
|
+ console.log(boxCenter);
|
|
</pre>
|
|
<p><a href="debugging-javascript.html">자바스크립트 콘솔</a>을 확인해보면 아래와
|
|
같은 결과가 보일 겁니다.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">size 2123.6499788469982
|
|
center p { x: -0.00006103515625, y: 770.0909731090069, z: -3.313507080078125 }
|
|
</pre>
|
|
<p>이 카메라는 현재 <code class="notranslate" translate="no">near</code> 0.1, <code class="notranslate" translate="no">far</code>가 100 이므로 약 100칸 정도를 투사합니다.
|
|
땅도 40x40칸인데 이 모델은 2000칸이죠. 카메라의 시야보다 훨씬 크니 절두체 영역
|
|
밖에 있는 게 당연합니다.</p>
|
|
<div class="threejs_center"><img style="width: 280px;" src="../resources/images/camera-inside-windmill.svg"></div>
|
|
|
|
<p>수작업으로 고칠 수도 있지만, 카메라가 장면의 크기를 자동으로 감지하도록 만들어보겠습니다.
|
|
방금 모델의 크기를 구할 때 썼던 육면체를 이용하면 되겠네요. 카메라의 위치를 정하는 데
|
|
<em>정해진</em> 방법은 없습니다. 경우에 따라 카메라의 방향과 위치가 다르니 그때 그때 상황에
|
|
맞춰 방법을 찾아야 하죠.</p>
|
|
<p><a href="cameras.html">카메라에 관해 배운 내용</a>을 떠올려봅시다. 카메라를 만들려면
|
|
절두체를 정의해야 하죠. 절두체는 <code class="notranslate" translate="no">fov(시야각, field of view)</code>, <code class="notranslate" translate="no">near</code>, <code class="notranslate" translate="no">far</code> 속성을
|
|
지정해 정의합니다. 시야각이 얼마이든, 절두체가 무한히 늘어난다고 가정할 때, 장면을
|
|
둘러싼 육면체가 절두체 안에 들어오게 하려면 카메라를 얼마나 멀리 보내야 할까요? 그러니까
|
|
<code class="notranslate" translate="no">near</code>가 0.00000001이고 <code class="notranslate" translate="no">far</code>가 무한대라 가정했을 때 말이죠.</p>
|
|
<p>다행히 시야각과 육면체의 크기를 아니 다음 그림과 같은 삼각형을 사용할 수 있습니다.</p>
|
|
<div class="threejs_center"><img style="width: 600px;" src="../resources/images/camera-fit-scene.svg"></div>
|
|
|
|
<p>그림에서 왼쪽은 카메라이고, 카메라에서 뻗어나온 파란 절두체가 풍차를 투사합니다.
|
|
방금 풍차를 둘러싼 육면체의 위치값을 계산했죠. 이제 얼마나 카메라를 멀리 보내야
|
|
육면체가 절두체 안에 들어올지 계산해야 합니다.</p>
|
|
<p>절두체의 시야각과 육면체의 크기를 구했으니, 기본 삼각함수와 <a href="https://www.google.com/search?q=SOHCAHTOA">*SOHCAHTOA</a>를
|
|
이용해 카메라와 육면체의 <em>거리(distance)</em>를 구할 수 있습니다.</p>
|
|
<p>※ SOH-CAH-TOA: 한국에서 삼각함수를 배울 때 얼싸안코와 비슷한 식으로 외우듯,
|
|
영미권에도 삼각함수를 배울 때 Sin = Opposite(대변) 나누기 Hypotenuse(빗변),
|
|
Cos = Adjacent(밑변) 나누기 Hypotenuse, Tan = Oppsite 나누기 Adjacent와 같은
|
|
식으로 외웁니다. 이를 줄여서 SOH-CAH-TOA(소-카-토아)라고 부릅니다. 역주.</p>
|
|
<div class="threejs_center"><img style="width: 600px;" src="../resources/images/field-of-view-camera.svg"></div>
|
|
|
|
<p>그림을 기반으로 계산식을 짜보겠습니다.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">distance = halfSizeToFitOnScreen / tangent(halfFovY) // 거리 = 화면 크기의 반 / 탄젠트(시야각의 절반)
|
|
</pre>
|
|
<p>이제 위 계산식을 코드로 옮겨야 합니다. 먼저 <code class="notranslate" translate="no">distance(거리)</code>를 구한 뒤
|
|
카메라를 육면체의 중심에서 <code class="notranslate" translate="no">distance</code>값만큼 옮깁니다. 그리고 카메라가 육면체의
|
|
<code class="notranslate" translate="no">center(중심)</code>을 바라보게 설정합니다.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
|
|
const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
|
|
const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
|
|
const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
|
|
|
|
// 육면체의 중심에서 카메라가 있는 곳으로 향하는 방향 벡터를 계산합니다
|
|
const direction = (new THREE.Vector3()).subVectors(camera.position, boxCenter).normalize();
|
|
|
|
// 방향 벡터에 따라 카메라를 육면체로부터 일정 거리에 위치시킵니다
|
|
camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
|
|
|
|
// 육면체를 투사할 절두체를 near와 far값으로 정의합니다
|
|
camera.near = boxSize / 100;
|
|
camera.far = boxSize * 100;
|
|
|
|
camera.updateProjectionMatrix();
|
|
|
|
// 카메라가 육면체의 중심을 바라보게 합니다
|
|
camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
|
|
}
|
|
</pre>
|
|
<p>이 함수는 <code class="notranslate" translate="no">boxSize</code>와 <code class="notranslate" translate="no">sizeToFitOnScreen</code>, 두 개의 크기값을 매개변수로 받습니다.
|
|
<code class="notranslate" translate="no">boxSize</code> 값으로 <code class="notranslate" translate="no">sizeToFitOnScreen</code> 값을 대체할 수도 있지만, 이러면 육면체가
|
|
화면에 꽉 차게 됩니다. 조금 여유가 있는 편이 보기 편하므로 조금 더 큰 값을 넘겨주도록
|
|
하겠습니다.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
|
|
const objLoader = new OBJLoader();
|
|
objLoader.load('resources/models/windmill_2/windmill.obj', (root) => {
|
|
scene.add(root);
|
|
+ // 모든 요소를 포함하는 육면체를 계산합니다
|
|
+ const box = new THREE.Box3().setFromObject(root);
|
|
+
|
|
+ const boxSize = box.getSize(new THREE.Vector3()).length();
|
|
+ const boxCenter = box.getCenter(new THREE.Vector3());
|
|
+
|
|
+ // 카메라가 육면체를 완전히 감싸도록 설정합니다
|
|
+ frameArea(boxSize * 1.2, boxSize, boxCenter, camera);
|
|
+
|
|
+ // 마우스 휠 이벤트가 큰 크기에 대응하도록 업데이트합니다
|
|
+ controls.maxDistance = boxSize * 10;
|
|
+ controls.target.copy(boxCenter);
|
|
+ controls.update();
|
|
});
|
|
}
|
|
</pre>
|
|
<p>위 예제에서는 <code class="notranslate" translate="no">boxSize * 1.2</code> 값을 넘겨주어 20% 정도 빈 공간을 더 만들었습니다.
|
|
또 카메라가 장면의 중심을 기준으로 회전하도록 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>도 업데이트했죠.</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/load-obj-auto-camera.html"></iframe></div>
|
|
<a class="threejs_center" href="/manual/examples/load-obj-auto-camera.html" target="_blank">새 탭에서 보기</a>
|
|
</div>
|
|
|
|
<p></p>
|
|
<p>성공했습니다. 마우스로 장면을 드래그하면 풍차가 보일 거예요. 하지만 카메라가 풍차의
|
|
정면이 아닌 아래쪽을 먼저 보여줍니다. 이는 풍차가 너무 커서 육면체의 중심이 약
|
|
(0, 770, 0)인데, 카메라를 육면체의 중심에서 기존 위치 (0, 10, 20) 방향으로 <code class="notranslate" translate="no">distance</code>만큼
|
|
옮겼기에 풍차의 아래쪽에 카메라가 위치하게 된 것입니다.</p>
|
|
<div class="threejs_center"><img style="width: 360px;" src="../resources/images/computed-camera-position.svg"></div>
|
|
|
|
<p>카메라의 기존 위치에 상관없이 육면체의 중심을 기준으로 카메라를 배치해보겠습니다.
|
|
단순히 카메라와 육면체 간 벡터의 <code class="notranslate" translate="no">y</code> 요소를 0으로 만들면 됩니다. <code class="notranslate" translate="no">y</code> 요소를 0으로
|
|
만든 뒤 벡터를 정규화(normalize)하면, XZ 면에 평행한 벡터, 그러니까 바닥에 평행한
|
|
벡터가 되겠죠.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-// 육면체의 중심에서 카메라가 있는 곳으로 향하는 방향 벡터를 계산합니다
|
|
-const direction = (new THREE.Vector3()).subVectors(camera.position, boxCenter).normalize();
|
|
+// 카메라와 육면체 사이의 방향 벡터를 항상 XZ 면에 평행하게 만듭니다
|
|
+const direction = (new THREE.Vector3())
|
|
+ .subVectors(camera.position, boxCenter)
|
|
+ .multiply(new THREE.Vector3(1, 0, 1))
|
|
+ .normalize();
|
|
</pre>
|
|
<p>풍차의 아랫면을 보면 작은 정사각형이 하나 보일 겁니다. 원래 땅으로 썼던 평면이죠.</p>
|
|
<div class="threejs_center"><img style="width: 365px;" src="../resources/images/tiny-ground-plane.jpg"></div>
|
|
|
|
<p>원래 땅은 40x40칸이었으니 풍차에 비해 훨씬 작은 것이 당연합니다. 풍차의 크기는
|
|
2000칸이 넘습니다. 땅을 풍차에 맞게 키워야 겠네요. 또 크기만 키우면 체크무늬가
|
|
너무 작아 확대하지 않는 한 보기가 어려울 테니 체스무늬 한 칸의 크기도 키우겠습니다.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const planeSize = 40;
|
|
+const planeSize = 4000;
|
|
|
|
const loader = new THREE.TextureLoader();
|
|
const texture = loader.load('resources/images/checker.png');
|
|
texture.wrapS = THREE.RepeatWrapping;
|
|
texture.wrapT = THREE.RepeatWrapping;
|
|
texture.magFilter = THREE.NearestFilter;
|
|
-const repeats = planeSize / 2;
|
|
+const repeats = planeSize / 200;
|
|
texture.repeat.set(repeats, repeats);
|
|
</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/load-obj-auto-camera-xz.html"></iframe></div>
|
|
<a class="threejs_center" href="/manual/examples/load-obj-auto-camera-xz.html" target="_blank">새 탭에서 보기</a>
|
|
</div>
|
|
|
|
<p></p>
|
|
<p>이제 재질을 다시 붙여봅시다. 이전 모델과 마찬가지로 텍스처에 대한 데이터를 담은
|
|
.MTL 파일이 보입니다. 하지만 동시에 다른 문제도 보이네요.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-shell" translate="no"> $ ls -l windmill
|
|
-rw-r--r--@ 1 gregg staff 299 May 20 2009 windmill.mtl
|
|
-rw-r--r--@ 1 gregg staff 142989 May 20 2009 windmill.obj
|
|
-rw-r--r--@ 1 gregg staff 12582956 Apr 19 2009 windmill_diffuse.tga
|
|
-rw-r--r--@ 1 gregg staff 12582956 Apr 20 2009 windmill_normal.tga
|
|
-rw-r--r--@ 1 gregg staff 12582956 Apr 19 2009 windmill_spec.tga
|
|
</pre>
|
|
<p>어마어마하게 큰 TARGA (.tga) 파일이 있습니다.</p>
|
|
<p>THREE.js에 TGA 로더가 있기는 하나 대부분의 경우 이를 사용하는 건 좋지 않습니다.
|
|
아주 소수의 경우, 예를 들어 사용자가 임의의 3D 모델 파일을 불러와 확인할 수 있는
|
|
뷰어를 만든다거나 하는 경우라면 TGA 파일을 사용할 수도 있죠.(<a href="#loading-scenes">*</a>)</p>
|
|
<p>TGA 파일의 문제점 중 하나는 압축을 거의 하지 않는다는 점입니다. TGA는 아주 간단한
|
|
압축만 지원하죠. 파일의 크기가 모두 같을 확률은 매우 희박하니, 위 파일들은 아예
|
|
압축이 되지 않았다고 볼 수 있습니다. 게다가 파일 하나당 무려 12 메가바이트!! 저
|
|
파일을 그대로 사용한다면 사용자는 풍차 하나를 보기 위해 36MB의 데이터를 다운받아야
|
|
하는 셈이 됩니다.</p>
|
|
<p>또한 브라우저가 TGA를 지원하지 않기에, .JPG나 .PNG 파일보다 로딩 시간이 훨씬 느릴
|
|
겁니다.</p>
|
|
<p>확신하건데, 이 경우 .JPG 파일로 변환하는 게 가장 좋은 선택입니다. TGA 파일은 알파값이
|
|
없는 RGB 3개의 채널로 구성되죠. JPG도 채널 3개만 사용하니 딱 적당합니다. 또 JPG는 손실
|
|
압축을 사용하기에 파일 용량을 훨씬 많이 줄일 수 있습니다.</p>
|
|
<p>파일을 열어보니 각각 해상도가 2048x2048입니다. 쓰기에 따라 다르겠지만, 저는 이게 다소
|
|
낭비라는 생각에 해상도를 1024x1024로 낯추고 포토샵의 퀄리티 설정을 50%로 지정했습니다.
|
|
다시 파일 구조를 살펴보죠.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-shell" translate="no"> $ ls -l ../threejs.org/manual/examples/resources/models/windmill
|
|
-rw-r--r--@ 1 gregg staff 299 May 20 2009 windmill.mtl
|
|
-rw-r--r--@ 1 gregg staff 142989 May 20 2009 windmill.obj
|
|
-rw-r--r--@ 1 gregg staff 259927 Nov 7 18:37 windmill_diffuse.jpg
|
|
-rw-r--r--@ 1 gregg staff 98013 Nov 7 18:38 windmill_normal.jpg
|
|
-rw-r--r--@ 1 gregg staff 191864 Nov 7 18:39 windmill_spec.jpg
|
|
</pre>
|
|
<p>36MB에서 0.55MB가 되었네요! 물론 디자너이너의 생각은 다를 수 있으니 절충안을
|
|
찾기 전에 상의를 하는 것이 좋습니다.</p>
|
|
<p>이제 .MTL 파일을 열어 .TGA 파일 경로를 .JPG 파일로 바꿉니다. 다행히 .MTL 파일은
|
|
텍스트라 수정이 어렵지 않습니다.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-mtl" translate="no">newmtl blinn1SG
|
|
Ka 0.10 0.10 0.10
|
|
|
|
Kd 0.00 0.00 0.00
|
|
Ks 0.00 0.00 0.00
|
|
Ke 0.00 0.00 0.00
|
|
Ns 0.060000
|
|
Ni 1.500000
|
|
d 1.000000
|
|
Tr 0.000000
|
|
Tf 1.000000 1.000000 1.000000
|
|
illum 2
|
|
-map_Kd windmill_diffuse.tga
|
|
+map_Kd windmill_diffuse.jpg
|
|
|
|
-map_Ks windmill_spec.tga
|
|
+map_Ks windmill_spec.jpg
|
|
|
|
-map_bump windmill_normal.tga
|
|
-bump windmill_normal.tga
|
|
+map_bump windmill_normal.jpg
|
|
+bump windmill_normal.jpg
|
|
</pre>
|
|
<p>텍스처의 용량을 최적화했으니 이제 불러올 일만 남았습니다. 먼저 아까 했던 것처럼
|
|
재질을 불러와 <a href="/docs/#examples/loaders/OBJLoader"><code class="notranslate" translate="no">OBJLoader</code></a>에 지정합니다.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
|
|
+ const mtlLoader = new MTLLoader();
|
|
+ mtlLoader.load('resources/models/windmill_2/windmill-fixed.mtl', (mtl) => {
|
|
+ mtl.preload();
|
|
+ const objLoader = new OBJLoader();
|
|
+ objLoader.setMaterials(mtl);
|
|
objLoader.load('resources/models/windmill/windmill.obj', (root) => {
|
|
root.updateMatrixWorld();
|
|
scene.add(root);
|
|
// 모든 요소를 포함하는 육면체를 계산합니다
|
|
const box = new THREE.Box3().setFromObject(root);
|
|
|
|
const boxSize = box.getSize(new THREE.Vector3()).length();
|
|
const boxCenter = box.getCenter(new THREE.Vector3());
|
|
|
|
// 카메라가 육면체를 완전히 감싸도록 설정합니다
|
|
frameArea(boxSize * 1.2, boxSize, boxCenter, camera);
|
|
|
|
// 마우스 휠 이벤트가 큰 크기에 대응하도록 업데이트합니다
|
|
controls.maxDistance = boxSize * 10;
|
|
controls.target.copy(boxCenter);
|
|
controls.update();
|
|
});
|
|
+ });
|
|
}
|
|
</pre>
|
|
<p>결과를 확인했는데 문제가 발생했습니다. 여러분에게 직접 보여주기보다 하나하나
|
|
짚어보도록 하죠.</p>
|
|
<p>문제 #1: 3개의 <a href="/docs/#examples/loaders/MTLLoader"><code class="notranslate" translate="no">MTLLoader</code></a>가 각각 디퓨즈(diffuse) 색과 디퓨즈 텍스처 맵으로
|
|
혼합하는 재질을 만듬.</p>
|
|
<p>이는 유용한 기능이지만, .MTL 파일의 디퓨즈 색상은 0입니다.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-mtl" translate="no">Kd 0.00 0.00 0.00
|
|
</pre>
|
|
<p>(텍스처 맵 * 0 = 검정)이죠. 모델링 프로그램에서는 디퓨즈 텍스처 맵과 디퓨즈 색을 혼합하지
|
|
않아도 모델이 제대로 보입니다. 이 풍차를 만든 디자이너 입장에서는 이 파일이 문제가
|
|
없다고 생각하는 것이 당연하죠.</p>
|
|
<p>.MTL 파일을 다음과 같이 수정해 문제를 해결할 수 있습니다.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-mtl" translate="no">Kd 1.00 1.00 1.00
|
|
</pre>
|
|
<p>(텍스처 맵 * 1 = 텍스처 맵)이니까요.</p>
|
|
<p>문제 #2: 스페큘러(specular) 색이 검정임.</p>
|
|
<p><code class="notranslate" translate="no">Ks</code>로 시작하는 줄은 스페큘러 색을 나타냅니다. 이 역시 디자이너가 사용한 모델링
|
|
프로그램이 디퓨즈 맵처럼 뭔가 다른 처리를 해주었을 겁니다. Three.js는 스페큘러
|
|
색을 얼마나 많이 반사할지 결정할 때 스페큘러 맵의 빨강(red) 채널만 사용하기는
|
|
하나, 3가지 색상 채널을 모두 지정하긴 해야 합니다.</p>
|
|
<p>디퓨즈 색과 마찬가지로 .MTL 파일을 다음과 같이 수정하겠습니다.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-mtl" translate="no">-Ks 0.00 0.00 0.00
|
|
+Ks 1.00 1.00 1.00
|
|
</pre>
|
|
<p>문제 #3: <code class="notranslate" translate="no">windmill_normal.jpg</code>가 법선 맵이 아닌 범프 맵임.</p>
|
|
<p>마찬가지로 .MTL 파일을 수정해줍니다.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-mtl" translate="no">-map_bump windmill_normal.jpg
|
|
-bump windmill_normal.jpg
|
|
+norm windmill_normal.jpg
|
|
</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/load-obj-materials-windmill2.html"></iframe></div>
|
|
<a class="threejs_center" href="/manual/examples/load-obj-materials-windmill2.html" target="_blank">새 탭에서 보기</a>
|
|
</div>
|
|
|
|
<p></p>
|
|
<p>모델을 불러올 때 주의해야 하는 점을 몇 가지만 적어보겠습니다.</p>
|
|
<ul>
|
|
<li><p>크기를 알아야 한다</p>
|
|
<p>예제에서는 카메라가 장면 전체를 감싸도록 했지만, 이게 항상 최적의 해결책이 될 수는 없습니다.
|
|
직접 모델을 만들거나, 모델을 다운받아 3D 프로그램으로 크기를 조절하는 것이 더 이상적인 방법입니다.</p>
|
|
</li>
|
|
<li><p>잘못된 방향축</p>
|
|
<p>Three.js에서는 보통 Y축이 위쪽입니다. 모델링 프로그램에서는 Z축이 위쪽인 경우, Y축이 위쪽인 경우, 직접
|
|
설정할 수 있는 경우 등 경우가 다양하죠. 모델을 불러왔는데 방향이 잘못되었다면, 모델을 불러온 후 방향을
|
|
바꾸거나(권장하지 않음), 3D 프로그램이나 커맨드 라인 프로그램으로 모델을 원하는 방향으로 맞출 수 있습니다.
|
|
브라우저에서 이미지를 쓸 때와 마찬가지로, 이미지를 수정하는 코드를 넣는 것보다는 이미지를 다운받아 이미지
|
|
자체를 편집하는 게 더 나을 겁니다. 블렌더에서는 아예 파일을 내보낼 때 방향을 바꿀 수 있습니다.</p>
|
|
</li>
|
|
<li><p>.MTL 파일이 없거나 재질 또는 지원하지 않는 값이 있는 경우</p>
|
|
<p>위 예제를 만들 때 .MTL 파일 덕에 재질을 만드는 수고는 덜었지만, 몇 가지 문제가 있었습니다. 문제를 해결하기
|
|
위해 직접 .MTL 파일을 수정했고요. 파일을 열어 .OBJ 파일 안에 어떤 재질이 있는지 확인하거나, Three.js로
|
|
.OBJ 파일을 불러와 재질을 전부 출력하도록 하는 것은 꽤 자주 있는 일입니다. 그런 후에 .MTL 파일 대신 직접
|
|
재질을 만들어 적절한 이름/재질 쌍의 객체로 로더에 넘겨주거나, 장면을 렌더링한 뒤 테스트하면서 문제를 수정하는
|
|
것이죠.</p>
|
|
</li>
|
|
<li><p>고용량 텍스처</p>
|
|
<p>3D 모델은 주로 건축, 영화나 광고, 게임 등에서 사용합니다. 건축이나 영화 같은 분야라면 텍스처의 용량을
|
|
신경 쓸 필요는 없죠. 반면 게임의 경우는 메모리도 제한적이고 로컬 환경에서 구동되기에 신경을 꽤 써야 합니다.
|
|
웹 페이지의 경우는 빠르게 불러와야 하니 용량이 퀄리티가 너무 떨어지지 않는 선에서 최대한 작은 게 좋죠.
|
|
첫 번째로 쓴 풍차의 경우, 실제로 사용하려면 텍스처를 손볼 필요가 있습니다. 지금은 총 용량이 무려 10MB가
|
|
넘거든요!!!</p>
|
|
<p>또한 <a href="textures.html">텍스처에 관한 글</a>에서 말했듯, 텍스처의 해상도도 고려해야 합니다. 50KB짜리
|
|
4096x4096 JPG 이미지는 불러오는 속도는 빠를지 몰라도 굉장히 많은 메모리를 차지할 테니까요.</p>
|
|
</li>
|
|
</ul>
|
|
<p>마지막으로 풍차가 돌아가는 것을 보여주고 싶지만, .OBJ 파일에는 계층 구조가 없습니다. 다시 말해 풍차의 모든
|
|
요소를 기본적으로 1개의 mesh로 취급한다는 것이죠. 풍차의 날개를 건물에서 분리할 수 없기에 날개를 회전시킬
|
|
수가 없습니다.</p>
|
|
<p>이런 이유로 .OBJ는 그다지 좋은 파일 형식이라고 하기 어렵습니다. 추측하건데 .OBJ 형식을 자주 사용하는 이유는
|
|
사용법이 간단하고, 복잡한 기능이 필요 없는 경우가 많기 때문일 겁니다. 예를 들어 건축 디자인을 하는 경우,
|
|
대부분 애니메이션이 필요 없기에 장면에 정적 요소를 추가하는 게 더 좋을 수 있죠.</p>
|
|
<p>.gLTF는 .OBJ보다 더 많은 기능을 지원합니다. 다음 글에서는 이 gLTF 장면을 불러오는 법에 대해 알아보겠습니다.</p>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/manual/resources/prettify.js"></script>
|
|
<script src="/manual/resources/lesson.js"></script>
|
|
|
|
|
|
|
|
|
|
</body></html>
|