123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561 |
- <!DOCTYPE html><html lang="ko"><head>
- <meta charset="utf-8">
- <title>텍스처(Textures)</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 – 텍스처(Textures)">
- <meta property="og:image" content="https://threejs.org/files/share.png">
- <link rel="shortcut icon" href="../../files/favicon_white.ico" media="(prefers-color-scheme: dark)">
- <link rel="shortcut icon" href="../../files/favicon.ico" media="(prefers-color-scheme: light)">
- <link rel="stylesheet" href="../resources/lesson.css">
- <link rel="stylesheet" href="../resources/lang.css">
- <script type="importmap">
- {
- "imports": {
- "three": "../../build/three.module.js"
- }
- }
- </script>
- <link rel="stylesheet" href="/manual/ko/lang.css">
- </head>
- <body>
- <div class="container">
- <div class="lesson-title">
- <h1>텍스처(Textures)</h1>
- </div>
- <div class="lesson">
- <div class="lesson-main">
- <p>※ 이 글은 Three.js의 튜토리얼 시리즈로서,
- 먼저 <a href="fundamentals.html">Three.js의 기본 구조에 관한 글</a>과
- <a href="setup.html">개발 환경 설정하는 법</a>을 읽고 오길 권장합니다.</p>
- <p>※ 텍스처, Texture는 "질감"으로 번역할 수 있으나, 그대로 표기하는 쪽이
- 직관적이라 판단하여 <strong>텍스처</strong>로 번역하였습니다.</p>
- <p>Three.js에서 텍스처를 이야기하기란 쉽지 않습니다. 텍스처는 워낙 방대한
- 주제이고, 각 주제끼리도 서로 연결되어 있어 한 번에 설명하는 것이 거의
- 불가능에 가깝기 때문이죠. 어떻게 설명해야 잘 설명했다고 할 수 있을지
- 확신은 없지만, 일단 해보기로 합시다. 다음은 이 글의 간략한 목차입니다.</p>
- <ul>
- <li><a href="#hello">하이, 텍스처</a></li>
- <li><a href="#six">육면체 각 면에 다른 텍스처 지정하기</a></li>
- <li><a href="#loading">텍스처 불러오기</a></li>
- <ul>
- <li><a href="#easy">간단한 방법</a></li>
- <li><a href="#wait1">텍스처를 불러온 후 처리하기</a></li>
- <li><a href="#waitmany">다수의 텍스처를 불러온 후 처리하기</a></li>
- <li><a href="#cors">다른 도메인(origin)에서 텍스처 불러오기</a></li>
- </ul>
- <li><a href="#memory">메모리 관리</a></li>
- <li><a href="#format">JPG vs PNG</a></li>
- <li><a href="#filtering-and-mips">필터링과 Mips</a></li>
- <li><a href="#uvmanipulation">텍스처의 반복(repeating), 위치 조절(offseting), 회전(rotating), 래핑(wrapping)</a></li>
- </ul>
- <h2 id="-a-name-hello-a-"><a name="hello"></a> 하이, 텍스처</h2>
- <p>텍스처는 <em>일반적으로</em> 포토샵이나 김프 등의 프로그램으로 만든 이미지입니다.
- 예를 들어 아래 이미지를 정육면체에 씌워보죠.</p>
- <div class="threejs_center">
- <img src="../examples/resources/images/wall.jpg" style="width: 600px;" class="border">
- </div>
- <p>예제는 처음 만들었던 것을 사용하겠습니다. 추가로 <a href="/docs/#api/ko/loaders/TextureLoader"><code class="notranslate" translate="no">TextureLoader</code></a>를 새로 생성한
- 뒤, 인스턴스의 <a href="/docs/#api/ko/loaders/TextureLoader#load"><code class="notranslate" translate="no">load</code></a> 메서드에 이미지의 URL을 넘겨주어 호출하고,
- 반환 받은 값을 재질(material)의 <code class="notranslate" translate="no">map</code> 속성에 지정합니다(<code class="notranslate" translate="no">color</code> 속성은 지정하지
- 않습니다).</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const loader = new THREE.TextureLoader();
- +const texture = loader.load( 'resources/images/wall.jpg' );
- +texture.colorSpace = THREE.SRGBColorSpace;
- const material = new THREE.MeshBasicMaterial({
- - color: 0xFF8844,
- + map: texture,
- });
- </pre>
- <p>※ <a href="/docs/#api/ko/materials/MeshBasicMaterial"><code class="notranslate" translate="no">MeshBasicMaterial</code></a>을 사용했으므로 광원을 사용할 필요가 없습니다.</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/textured-cube.html"></iframe></div>
- <a class="threejs_center" href="/manual/examples/textured-cube.html" target="_blank">새 탭에서 보기</a>
- </div>
- <p></p>
- <h2 id="-a-name-six-a-"><a name="six"></a> 육면체 각 면에 다른 텍스처 지정하기</h2>
- <p>이번에는 육면체의 각 면에 다른 텍스처를 넣어볼까요?</p>
- <div class="threejs_center">
- <div>
- <img src="../examples/resources/images/flower-1.jpg" style="width: 100px;" class="border">
- <img src="../examples/resources/images/flower-2.jpg" style="width: 100px;" class="border">
- <img src="../examples/resources/images/flower-3.jpg" style="width: 100px;" class="border">
- </div>
- <div>
- <img src="../examples/resources/images/flower-4.jpg" style="width: 100px;" class="border">
- <img src="../examples/resources/images/flower-5.jpg" style="width: 100px;" class="border">
- <img src="../examples/resources/images/flower-6.jpg" style="width: 100px;" class="border">
- </div>
- </div>
- <p>단순히 재질을 6개 만들어 <a href="/docs/#api/ko/objects/Mesh"><code class="notranslate" translate="no">Mesh</code></a>를 생성할 때 배열로 넘겨주기만 하면 됩니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const loader = new THREE.TextureLoader();
- -const texture = loader.load( 'resources/images/wall.jpg' );
- -texture.colorSpace = THREE.SRGBColorSpace;
- -const material = new THREE.MeshBasicMaterial({
- - map: texture,
- -});
- +const materials = [
- + new THREE.MeshBasicMaterial({map: loadColorTexture('resources/images/flower-1.jpg')}),
- + new THREE.MeshBasicMaterial({map: loadColorTexture('resources/images/flower-2.jpg')}),
- + new THREE.MeshBasicMaterial({map: loadColorTexture('resources/images/flower-3.jpg')}),
- + new THREE.MeshBasicMaterial({map: loadColorTexture('resources/images/flower-4.jpg')}),
- + new THREE.MeshBasicMaterial({map: loadColorTexture('resources/images/flower-5.jpg')}),
- + new THREE.MeshBasicMaterial({map: loadColorTexture('resources/images/flower-6.jpg')}),
- +];
- -const cube = new THREE.Mesh(geometry, material);
- +const cube = new THREE.Mesh(geometry, materials);
- +function loadColorTexture( path ) {
- + const texture = loader.load( path );
- + texture.colorSpace = THREE.SRGBColorSpace;
- + return texture;
- +}
- </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/textured-cube-6-textures.html"></iframe></div>
- <a class="threejs_center" href="/manual/examples/textured-cube-6-textures.html" target="_blank">새 탭에서 보기</a>
- </div>
- <p></p>
- <p>주의해야할 점은 모든 <code class="notranslate" translate="no">geometry</code>가 재질을 배열로 받진 않는다는 점입니다.
- <a href="/docs/#api/ko/geometries/BoxGeometry"><code class="notranslate" translate="no">BoxGeometry</code></a>나 <a href="/docs/#api/ko/geometries/BoxGeometry"><code class="notranslate" translate="no">BoxGeometry</code></a>는 최대 6개, <a href="/docs/#api/ko/geometries/ConeGeometry"><code class="notranslate" translate="no">ConeGeometry</code></a>와
- <a href="/docs/#api/ko/geometries/ConeGeometry"><code class="notranslate" translate="no">ConeGeometry</code></a>는 밑면과 뿔 부분에 하나씩 최대 2개, <a href="/docs/#api/ko/geometries/CylinderGeometry"><code class="notranslate" translate="no">CylinderGeometry</code></a>와
- <a href="/docs/#api/ko/geometries/CylinderGeometry"><code class="notranslate" translate="no">CylinderGeometry</code></a>는 아래, 위, 옆면 하나씩 최대 3개를 지정할 수 있죠.
- 다른 경우에는 <code class="notranslate" translate="no">geometry</code>를 따로 만들거나, 텍스처의 좌표를 직접 수정해야 합니다.</p>
- <p>다른 3D 엔진에서나 Three.js에서나, 하나의 <code class="notranslate" translate="no">geometry</code>에서 여러 텍스처를 쓰고 싶을 때는
- 보통 <a href="https://en.wikipedia.org/wiki/Texture_atlas">텍스처 아틀라스</a>를 사용합니다.
- 텍스처 아틀라스란 여러 이미지로 구성된 하나의 텍스처로, <code class="notranslate" translate="no">geometry</code>의 정점에 따라 텍스처의
- 좌표를 조절해 <code class="notranslate" translate="no">geometry</code>의 각 삼각형이 텍스처의 일정 부분을 표현하도록 할 수 있습니다.</p>
- <p>그렇다면 텍스처의 좌표란 무엇일까요? 이는 <code class="notranslate" translate="no">geometry</code>의 각 정점에 추가되는 데이터로, 특정
- 정점에 텍스처의 어느 부분을 써야하는지를 나타냅니다. 자세한 사용법은 나중에
- <a href="custom-buffergeometry.html">사용자 지정 geometry 만들기</a>에서 살펴보겠습니다.</p>
- <h2 id="-a-name-loading-a-"><a name="loading"></a> 텍스처 불러오기</h2>
- <h3 id="-a-name-easy-a-"><a name="easy"></a> 간단한 방법</h3>
- <p>이 사이트의 예제는 대부분 텍스처를 로딩할 때 간단한 메서드를 사용했습니다.
- <a href="/docs/#api/ko/loaders/TextureLoader"><code class="notranslate" translate="no">TextureLoader</code></a>를 생성하고, 인스턴스의 <a href="/docs/#api/ko/loaders/TextureLoader#load"><code class="notranslate" translate="no">load</code></a> 메서드를
- 호출하는 거죠. 이 <code class="notranslate" translate="no">load</code> 메서드는 <a href="/docs/#api/ko/textures/Texture"><code class="notranslate" translate="no">Texture</code></a> 객체를 반환합니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const texture = loader.load('resources/images/flower-1.jpg');
- </pre>
- <p>알아둬야 할 건 이 메서드는 비동기로 작동한다는 점입니다. 이미지를 완전히
- 불러온 후 이미지로 텍스처를 업데이트하기 전까지, 텍스처는 투명하게 보일 겁니다.</p>
- <p>텍스처를 전부 불러오지 않아도 브라우저가 페이지 렌더링을 시작할 것이므로 이는
- 속도면에서 꽤 큰 장점입니다. 텍스처를 언제 다 불러왔는지 알아야 하는 경우가
- 아니라면, 대부분 큰 문제가 되지 않겠죠.</p>
- <h3 id="-a-name-wait1-a-"><a name="wait1"></a> 텍스처를 불러온 후 처리하기</h3>
- <p>텍스처를 불러온 후 후처리를 위해 <code class="notranslate" translate="no">load</code> 메서드는 두 번째 인자로 콜백(callback)
- 함수를 받습니다. 이 함수는 텍스처를 전부 불러온 후 호출되죠. 글의 첫 번째 예제를
- 조금 수정해보겠습니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const loader = new THREE.TextureLoader();
- loader.load('resources/images/wall.jpg', (texture) => {
- const material = new THREE.MeshBasicMaterial({
- map: texture,
- });
- const cube = new THREE.Mesh(geometry, material);
- scene.add(cube);
- cubes.push(cube); // 회전 애니메이션을 위해 배열에 추가
- });
- </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/textured-cube-wait-for-texture.html"></iframe></div>
- <a class="threejs_center" href="/manual/examples/textured-cube-wait-for-texture.html" target="_blank">새 탭에서 보기</a>
- </div>
- <p></p>
- <h3 id="-a-name-waitmany-a-"><a name="waitmany"></a> 다수의 텍스처를 불러온 후 처리하기</h3>
- <p>다수의 텍스처를 한 번에 불러와야 할 경우 <a href="/docs/#api/ko/loaders/managers/LoadingManager"><code class="notranslate" translate="no">LoadingManager</code></a>를 사용할 수 있습니다.
- <a href="/docs/#api/ko/loaders/TextureLoader"><code class="notranslate" translate="no">TextureLoader</code></a>를 생성할 때 미리 생성한 <a href="/docs/#api/ko/loaders/managers/LoadingManager"><code class="notranslate" translate="no">LoadingManager</code></a>의 인스턴스를 인자로
- 넘겨주고, <a href="/docs/#api/ko/loaders/managers/LoadingManager"><code class="notranslate" translate="no">LoadingManager</code></a> 인스턴스의 <a href="/docs/#api/ko/loaders/managers/LoadingManager#onLoad"><code class="notranslate" translate="no">onLoad</code></a> 속성에
- 콜백 함수를 설정해주는 거죠.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const loadManager = new THREE.LoadingManager();
- *const loader = new THREE.TextureLoader(loadManager);
- const materials = [
- new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-1.jpg')}),
- new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-2.jpg')}),
- new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-3.jpg')}),
- new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-4.jpg')}),
- new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-5.jpg')}),
- new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-6.jpg')}),
- ];
- +loadManager.onLoad = () => {
- + const cube = new THREE.Mesh(geometry, materials);
- + scene.add(cube);
- + cubes.push(cube); // 회전 애니메이션을 위해 배열에 추가
- +};
- </pre>
- <p><a href="/docs/#api/ko/loaders/managers/LoadingManager"><code class="notranslate" translate="no">LoadingManager</code></a>의 <a href="/docs/#api/ko/loaders/managers/LoadingManager#onProgress"><code class="notranslate" translate="no">onProgress</code></a>에 콜백 함수를 지정하면
- 현재 진행 상태를 추적할 수 있습니다.</p>
- <p>일단 HTML로 프로그래스 바(progress bar)를 만들겠습니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-html" translate="no"><body>
- <canvas id="c"></canvas>
- + <div id="loading">
- + <div class="progress"><div class="progressbar"></div></div>
- + </div>
- </body>
- </pre>
- <p>스타일도 추가하죠.</p>
- <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#loading {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- #loading .progress {
- margin: 1.5em;
- border: 1px solid white;
- width: 50vw;
- }
- #loading .progressbar {
- margin: 2px;
- background: white;
- height: 1em;
- transform-origin: top left;
- transform: scaleX(0);
- }
- </pre>
- <p>다음으로 <code class="notranslate" translate="no">onProgress</code> 콜백에서 <code class="notranslate" translate="no">.progressbar</code>의 X축 크기를 조정하겠습니다.
- 콜백 함수는 마지막으로 불러온 자원의 URL, 현재까지 불러온 자원의 수, 총 지원의
- 수를 매개변수로 받습니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const loadingElem = document.querySelector('#loading');
- +const progressBarElem = loadingElem.querySelector('.progressbar');
- loadManager.onLoad = () => {
- + loadingElem.style.display = 'none';
- const cube = new THREE.Mesh(geometry, materials);
- scene.add(cube);
- cubes.push(cube); // 회전 애니메이션을 위해 배열에 추가
- };
- +loadManager.onProgress = (urlOfLastItemLoaded, itemsLoaded, itemsTotal) => { // 마지막으로 불러온 자원의 URL, 현재까지 불러온 자원의 수, 총 지원의 수
- + const progress = itemsLoaded / itemsTotal;
- + progressBarElem.style.transform = `scaleX(${progress})`;
- +};
- </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/textured-cube-wait-for-all-textures.html"></iframe></div>
- <a class="threejs_center" href="/manual/examples/textured-cube-wait-for-all-textures.html" target="_blank">새 탭에서 보기</a>
- </div>
- <p></p>
- <h2 id="-a-name-cors-a-origin-"><a name="cors"></a> 다른 도메인(origin)에서 텍스처 불러오기</h2>
- <p>다른 서버에서 이미지를 불러오려면 해당 서버가 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">CORS 헤더</a>를
- 보내줘야 합니다. CORS 헤더가 없다면 Three.js가 이미지를 불러오지 않을 것이고,
- 에러가 발생할 겁니다. 만약 이미지 호스팅 서버를 운영한다면 해당 서버가 CORS 헤더를
- 보내는지 확인해보세요.</p>
- <p><a href="https://imgur.com">imgur</a>, <a href="https://flickr.com">flickr</a>, <a href="https://github.com">github</a>
- 등의 사이트는 자신이 호스팅하는 이미지를 사용해도 좋다는 헤더를 보냅니다.
- 대부분의 웹사이트는 이를 허용하지 않죠.</p>
- <h2 id="-a-name-memory-a-"><a name="memory"></a> 메모리 관리</h2>
- <p>텍스처는 Three.js 앱에서 메모리를 가장 많이 사용하는 요소 중 하나입니다.
- <em>대체로</em> 텍스처는 약 <code class="notranslate" translate="no">너비 * 높이 * 4 * 1.33</code> 바이트의 메모리를 사용합니다.</p>
- <p>여기서 압축은 그다지 중요한 요소가 아닙니다. 예를 들어 집이 포함된 장면(scene)을
- 만든다고 해보죠. 집 안에는 탁자가 있고, 탁자의 윗면에 나무 텍스처를 씌우려고
- 합니다.</p>
- <div class="threejs_center"><img class="border" src="../resources/images/compressed-but-large-wood-texture.jpg" align="center" style="width: 300px"></div>
- <p>이 이미지는 매우 고 배율로 압축되어 157kb 밖에 되지 않습니다. 상대적으로
- 다운 속도는 빠를 것이나, 이 <a href="resources/images/compressed-but-large-wood-texture.jpg">이미지의 실제 크기는 3024 x 3761 픽셀입니다</a>.
- 위 공식에 따라 이 이미지를 적용해보면,</p>
- <pre class="prettyprint showlinemods notranslate notranslate" translate="no">3024 * 3761 * 4 * 1.33 = 60505764.5
- </pre><p>무려 <strong>약 60 메가바이트의 메모리</strong>를 사용합니다. 이런 텍스처가 몇 개만 더
- 있어도 메모리 부족으로 앱을 사용하지 못할 수 있죠(OUT_OF_MEMORY).</p>
- <p>극단적인 예제이기는 하나, 이 예제는 텍스처를 사용하는데 숨겨진 비용을 고려해야
- 한다는 것을 잘 알려줍니다. Three.js가 텍스처를 사용하려면 GPU에 텍스처를
- 넘겨주어야 하는데, GPU는 <em>일반적으로</em> 압축하지 않은 데이터를 사용하죠.</p>
- <p>이 예시의 교훈은 파일의 용량이 아니라 파일의 해상도를 줄어야 한다는 것입니다.
- 파일의 용량이 작다면 불러오는 속도가 빠를 것이고, 해상도가 낮다면 메모리를
- 그만큼 적게 사용하겠죠. 얼마나 낮게 만들어야 할까요? 필요한 만큼 퀄리티를
- 유지한 선에서 가능한 낮게 만드는 게 좋습니다.</p>
- <h2 id="-a-name-format-a-jpg-vs-png"><a name="format"></a> JPG vs PNG</h2>
- <p>이는 HTML과 마찬가지입니다. JPG는 손실 압축을 사용하고, PNG는 비손실 압축을
- 사용하는 대신 보통 PNG가 더 용량이 크죠. 하지만 PNG는 투명도를 지원합니다.
- PNG는 비-이미지 데이터인 법선 맵(normal maps), 그리고 나중에 살펴볼 다른
- 비-이미지 데이터를 사용하기에 현재로써는 가장 적당한 파일 형식입니다.</p>
- <p>위에서 말했듯, WebGL에서는 JPG가 용량이 더 작긴 해도
- PNG 형식보다 메모리 점유율이 낮진 않습니다.</p>
- <h2 id="-a-name-filtering-and-mips-a-mips"><a name="filtering-and-mips"></a> 필터링과 Mips</h2>
- <p>이 16x16 텍스처를</p>
- <div class="threejs_center"><img src="../resources/images/mip-low-res-enlarged.png" class="nobg" align="center"></div>
- <p>아래의 정육면체에 적용해보죠.</p>
- <div class="spread"><div data-diagram="filterCube"></div></div>
- <p>그리고 정육면체를 아주 작게 렌더링합니다.</p>
- <div class="spread"><div data-diagram="filterCubeSmall"></div></div>
- <p>음, 보기가 어렵네요. 확대해봅시다.</p>
- <div class="spread"><div data-diagram="filterCubeSmallLowRes"></div></div>
- <p>GPU는 작은 정육면체를 표현할 때 어떻게 각 픽셀의 색상을 결정할까요? 정육면체가
- 작아도 너무 작아서 1, 2 픽셀 정도라면요?</p>
- <p>이게 바로 필터링(filtering)이 있는 이유입니다.</p>
- <p>포토샵이라면 근처 픽셀의 평균을 내 해당 1, 2 픽셀의 형태를 결정할 겁니다.
- 이는 매우 무거운 작업이죠. GPU는 이 문제를 해결하기 위해 <a href="https://ko.wikipedia.org/wiki/%EB%B0%89%EB%A7%B5">밉맵(mipmaps)</a>을
- 사용합니다.</p>
- <p>밉(mips)은 텍스처의 복사본으로, 각 밉은 축소된 이전 밉보다 반만큼 작습니다.
- 밉은 1x1 픽셀 밉을 생성할 때까지 계속 생성되죠. 위 이미지의 경우 밉은 다음처럼
- 생성될 겁니다.</p>
- <div class="threejs_center"><img src="../resources/images/mipmap-low-res-enlarged.png" class="nobg" align="center"></div>
- <p>이제 1, 2 픽셀 정도로 작은 정육면체를 렌더링할 때 GPU는 가장 작거나, 두 번째로
- 작은 밉을 선택해 텍스처를 적용하기만 하면 되죠.</p>
- <p>Three.js에서는 텍스처의 크기가 원본보다 클 때와 작을 때 각각 어떻게 표현할지를
- 설정할 수 있습니다.</p>
- <p>텍스처의 크기가 원본보다 클 때의 필터는 <a href="/docs/#api/ko/textures/Texture#magFilter"><code class="notranslate" translate="no">texture.magFilter</code></a>
- 속성을 <code class="notranslate" translate="no">THREE.NearestFilter</code>나 <code class="notranslate" translate="no">THREE.LinearFilter</code>로 지정해 설정합니다.</p>
- <p><code class="notranslate" translate="no">NearestFilter</code>는 말 그대로 텍스처에서 가장 가까운 픽셀을 고르는 것입니다.
- 낮은 해상도라면 텍스처가 픽셀화되어 마인크래프트 같은 느낌을 주겠죠.</p>
- <p><code class="notranslate" translate="no">LinearFilter</code>는 가장 가까운 4개의 픽셀을 골라 각 픽셀의 실제 거리에 따라 적절한
- 비율로 섞는 것을 말합니다.</p>
- <div class="spread">
- <div>
- <div data-diagram="filterCubeMagNearest" style="height: 250px;"></div>
- <div class="code">Nearest</div>
- </div>
- <div>
- <div data-diagram="filterCubeMagLinear" style="height: 250px;"></div>
- <div class="code">Linear</div>
- </div>
- </div>
- <p>텍스처가 원본 크기보다 작을 때의 필터는 <a href="/docs/#api/ko/textures/Texture#minFilter"><code class="notranslate" translate="no">texture.minFilter</code></a>
- 속성을 다음 6가지 값 중 하나로 지정해 사용합니다.</p>
- <ul>
- <li><p><code class="notranslate" translate="no">THREE.NearestFilter</code></p>
- <p> 원본보다 클 때와 마찬가지로 가장 가까운 픽셀을 선택합니다</p>
- </li>
- <li><p><code class="notranslate" translate="no">THREE.LinearFilter</code></p>
- <p> 원본보다 클 때와 마찬가지로 주변의 가까운 픽셀 4개를 골라 섞습니다</p>
- </li>
- <li><p><code class="notranslate" translate="no">THREE.NearestMipmapNearestFilter</code></p>
- <p> 적절한 밉을 고른 뒤 밉에서 픽셀 하나를 선택합니다</p>
- </li>
- <li><p><code class="notranslate" translate="no">THREE.NearestMipmapLinearFilter</code></p>
- <p> 두 개의 밉을 골라 픽셀을 하나씩 선택한 후, 두 픽셀을 섞습니다</p>
- </li>
- <li><p><code class="notranslate" translate="no">THREE.LinearMipmapNearestFilter</code></p>
- <p> 적절한 밉을 고른 뒤 픽셀 4개를 골라 섞습니다</p>
- </li>
- <li><p><code class="notranslate" translate="no">THREE.LinearMipmapLinearFilter</code></p>
- <p>두 개의 밉을 골라 각각 픽셀을 4개씩 선택하고, 선택한 8개의 픽셀을 하나의 픽셀로 혼합합니다</p>
- </li>
- </ul>
- <p>아래는 6개의 필터를 각각 적용한 예제입니다.</p>
- <div class="spread">
- <div data-diagram="filterModes" style="
- height: 450px;
- position: relative;
- ">
- <div style="
- width: 100%;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: flex-start;
- ">
- <div style="
- background: rgba(255,0,0,.8);
- color: white;
- padding: .5em;
- margin: 1em;
- font-size: small;
- border-radius: .5em;
- line-height: 1.2;
- user-select: none;">클릭해<br>텍스처를<br>변경</div>
- </div>
- <div class="filter-caption" style="left: 0.5em; top: 0.5em;">nearest</div>
- <div class="filter-caption" style="width: 100%; text-align: center; top: 0.5em;">linear</div>
- <div class="filter-caption" style="right: 0.5em; text-align: right; top: 0.5em;">nearest<br>mipmap<br>nearest</div>
- <div class="filter-caption" style="left: 0.5em; text-align: left; bottom: 0.5em;">nearest<br>mipmap<br>linear</div>
- <div class="filter-caption" style="width: 100%; text-align: center; bottom: 0.5em;">linear<br>mipmap<br>nearest</div>
- <div class="filter-caption" style="right: 0.5em; text-align: right; bottom: 0.5em;">linear<br>mipmap<br>linear</div>
- </div>
- </div>
- <p>여기서 주의깊게 봐야할 건 상단 왼쪽 <code class="notranslate" translate="no">NearestFilter</code>와 상단 중앙의 <code class="notranslate" translate="no">LinearFilter</code>는
- 밉을 사용하지 않는다는 점입니다. 두 텍스처를 보면 멀리 떨어질수록 픽셀이 깜빡이는 증상이
- 보이죠. 이는 GPU가 픽셀을 원본 텍스처에서 선택하기 때문입니다. <code class="notranslate" translate="no">NearestFilter</code>는 하나의
- 픽셀을 선택하고, <code class="notranslate" translate="no">LinearFilter</code>는 4개의 픽셀을 선택하기는 하나, 픽셀을 제대로 표현하진
- 못합니다. 다른 4개 예제는 그나마 낫고, 그 중 <code class="notranslate" translate="no">LinearMipmapLinearFilter</code>가 제일 깔끔해
- 보이네요.</p>
- <p>위 캔버스를 클릭해보면 텍스처를 바꿀 수 있습니다. 하나는 여태까지 사용하던 텍스처이고,
- 또 하나는 밉의 각 단계가 다른 색으로 나타나는 텍스처이죠.</p>
- <div class="threejs_center">
- <div data-texture-diagram="differentColoredMips"></div>
- </div>
- <p>이 텍스처는 필터의 동작 원리를 이해하기 쉽도록 해줍니다. 위 예제에서 <code class="notranslate" translate="no">NearestFilter</code>와
- <code class="notranslate" translate="no">LinearFilter</code>는 아주 멀리까지도 첫 번째 밉을 사용합니다. 반면 상단 오른쪽과 하단 중앙을
- 보면 밉의 경계가 뚜렷이 보이죠.</p>
- <p>다시 원래 텍스처로 바꿔보면 하단 오른쪽이 가장 매끄러운 것이 보일 겁니다. 왜 항상 이
- 필터를 쓰지 않는 걸까요? 뭐, 레트로 감성을 표현한다든가 하는 이유로 물체들이 픽셀화된
- 것을 원할 수도 있죠. 하지만 보다 흔한 이유는 성능입니다. 8개의 픽셀 데이터를 처리하는
- 것보다는 당연히 1개의 픽셀 데이터를 처리하는 게 훨씬 빠르겠죠. 하나의 텍스처로 이런
- 성능 차이를 체감하기는 어렵지만, Three.js를 사용하다보면 하나의 물체에 4, 5개의 텍스처가
- 들어가는 경우도 빈번합니다. 4개의 텍스처에서 각각 8개의 픽셀을 처리해야 하니, 이는 한
- 프레임당 32개의 픽셀을 처리해야 함을 의미하죠. 이는 저사양 기기를 고려할 때 특히 중요히
- 여겨야 하는 요소입니다.</p>
- <h2 id="-a-name-uvmanipulation-a-repeating-offseting-rotating-wrapping-"><a name="uvmanipulation"></a> 텍스처의 반복(repeating), 위치 조절(offseting), 회전(rotating), 래핑(wrapping)</h2>
- <p>텍스처에는 반복, 위치, 회전 설정이 있습니다.</p>
- <p>Three.js는 기본적으로 텍스처를 반복하지 않습니다. 반복 여부를 설정하는
- 2가지 속성이 있는데, 하나는 수평 래핑을 설정하는 <a href="/docs/#api/ko/textures/Texture#wrapS"><code class="notranslate" translate="no">wrapS</code></a>이고,
- 또 하나는 수직 래핑을 설정하는 <a href="/docs/#api/ko/textures/Texture#wrapT"><code class="notranslate" translate="no">wrapT</code></a>입니다.</p>
- <p>두 속성은 다음 중 하나로 지정할 수 있습니다.</p>
- <ul>
- <li><p><code class="notranslate" translate="no">THREE.ClampToEdgeWrapping</code></p>
- <p> 텍스처의 가장자리 픽셀을 계속해서 반복합니다</p>
- </li>
- <li><p><code class="notranslate" translate="no">THREE.RepeatWrapping</code></p>
- <p> 텍스처 자체를 반복합니다</p>
- </li>
- <li><p><code class="notranslate" translate="no">THREE.MirroredRepeatWrapping</code></p>
- <p> 텍스처 자체를 반복하되, 매번 뒤집습니다.</p>
- </li>
- </ul>
- <p>양 방향의 래핑을 키려면 다음과 같이 설정할 수 있습니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">someTexture.wrapS = THREE.RepeatWrapping;
- someTexture.wrapT = THREE.RepeatWrapping;
- </pre>
- <p>반복은 <code class="notranslate" translate="no">repeat</code> 속성으로 설정할 수 있죠.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const timesToRepeatHorizontally = 4;
- const timesToRepeatVertically = 2;
- someTexture.repeat.set(timesToRepeatHorizontally, timesToRepeatVertically);
- </pre>
- <p>텍스처의 위치는 <code class="notranslate" translate="no">offset</code> 속성을 설정해 조절할 수 있습니다. 텍스처 위치의 단위는
- 텍스처의 크기와 1:1, 즉 0은 위치가 그대로인 것이고 1은 각 축에서 텍스처 크기만큼
- 이동한 것을 의미하죠.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const xOffset = .5; // 텍스처 너비의 반만큼 이동
- const yOffset = .25; // 텍스처 높이의 1/4만큼 이동
- someTexture.offset.set(xOffset, yOffset);
- </pre>
- <p>텍스처의 회전은 <code class="notranslate" translate="no">rotation</code> 속성을 라디안(radians) 단위로 지정해 조절할 수 있습니다.
- <code class="notranslate" translate="no">center</code> 속성은 회전의 중심을 정하는 데 사용하죠. <code class="notranslate" translate="no">center</code> 속성의 기본값은 <code class="notranslate" translate="no">0, 0</code>으로
- 왼쪽 상단을 기준으로 회전하고, <code class="notranslate" translate="no">offset</code>과 마찬가지로 텍스처의 크기를 기준으로
- 단위가 정해지기에 <code class="notranslate" translate="no">.5, .5</code>로 설정하면 텍스처의 중앙을 기준으로 회전합니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">someTexture.center.set(.5, .5);
- someTexture.rotation = THREE.MathUtils.degToRad(45);
- </pre>
- <p>아까 작성한 예제를 수정해 위 설정을 테스트할 예제를 만들겠습니다.</p>
- <p>먼저 텍스처를 별도 변수에 담아 나중에 수정할 수 있도록 합니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const texture = loader.load('resources/images/wall.jpg');
- const material = new THREE.MeshBasicMaterial({
- - map: loader.load('resources/images/wall.jpg');
- + map: texture,
- });
- </pre>
- <p>간단한 인터페이스를 만들어보죠.
- 다시 한 번 <a href="https://github.com/georgealways/lil-gui">lil-gui</a>가 등장할 때입니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
- </pre>
- <p>이전 예제처럼 간단한 헬퍼 클래스를 만들어 각도(degrees)로 값을 조절하면
- 알아서 호도(radians)로 변환해 지정하게끔 해줍니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class DegRadHelper {
- constructor(obj, prop) {
- this.obj = obj;
- this.prop = prop;
- }
- get value() {
- return THREE.MathUtils.radToDeg(this.obj[this.prop]);
- }
- set value(v) {
- this.obj[this.prop] = THREE.MathUtils.degToRad(v);
- }
- }
- </pre>
- <p>또 문자열을 숫자형으로 변환시켜줄 클래스도 만듭니다. lil-gui는 값을 문자열로
- 넘겨주는데, Three.js는 <code class="notranslate" translate="no">wrapS</code>나 <code class="notranslate" translate="no">wrapT</code> 등 enum 값을 지정할 때 숫자형만
- 받기 때문이죠.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class StringToNumberHelper {
- constructor(obj, prop) {
- this.obj = obj;
- this.prop = prop;
- }
- get value() {
- return this.obj[this.prop];
- }
- set value(v) {
- this.obj[this.prop] = parseFloat(v);
- }
- }
- </pre>
- <p>위에서 만든 클래스를 이용해 설정값을 조절할 GUI를 만듭니다.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const wrapModes = {
- 'ClampToEdgeWrapping': THREE.ClampToEdgeWrapping,
- 'RepeatWrapping': THREE.RepeatWrapping,
- 'MirroredRepeatWrapping': THREE.MirroredRepeatWrapping,
- };
- function updateTexture() {
- texture.needsUpdate = true;
- }
- const gui = new GUI();
- gui.add(new StringToNumberHelper(texture, 'wrapS'), 'value', wrapModes)
- .name('texture.wrapS')
- .onChange(updateTexture);
- gui.add(new StringToNumberHelper(texture, 'wrapT'), 'value', wrapModes)
- .name('texture.wrapT')
- .onChange(updateTexture);
- gui.add(texture.repeat, 'x', 0, 5, .01).name('texture.repeat.x');
- gui.add(texture.repeat, 'y', 0, 5, .01).name('texture.repeat.y');
- gui.add(texture.offset, 'x', -2, 2, .01).name('texture.offset.x');
- gui.add(texture.offset, 'y', -2, 2, .01).name('texture.offset.y');
- gui.add(texture.center, 'x', -.5, 1.5, .01).name('texture.center.x');
- gui.add(texture.center, 'y', -.5, 1.5, .01).name('texture.center.y');
- gui.add(new DegRadHelper(texture, 'rotation'), 'value', -360, 360)
- .name('texture.rotation');
- </pre>
- <p>텍스처의 <code class="notranslate" translate="no">wrapS</code>나 <code class="notranslate" translate="no">wrapT</code> 속성을 변경할 경우 <a href="/docs/#api/ko/textures/Texture#needsUpdate"><code class="notranslate" translate="no">texture.needsUpdate</code></a>를
- <code class="notranslate" translate="no">true</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/textured-cube-adjust.html"></iframe></div>
- <a class="threejs_center" href="/manual/examples/textured-cube-adjust.html" target="_blank">새 탭에서 보기</a>
- </div>
- <p></p>
- <p>뭔가 많은 것을 배운 것 같지만, 이는 맛보기에 불과합니다. 글을 진행하다보면
- 텍스처의 정렬과 재질에 적용할 수 있는 다른 9가지의 텍스처에 대해 다룰 기회가
- 있을 거예요.</p>
- <p>일단 다음 장에서는 <a href="lights.html">조명(lights)</a>에 대해 알아보기로 하죠.</p>
- <!--
- alpha
- ao
- env
- light
- specular
- bumpmap ?
- normalmap ?
- metalness
- roughness
- -->
- <p><link rel="stylesheet" href="../resources/threejs-textures.css"></p>
- <script type="module" src="../resources/threejs-textures.js"></script>
- </div>
- </div>
- </div>
- <script src="../resources/prettify.js"></script>
- <script src="../resources/lesson.js"></script>
- </body></html>
|