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.

432 lines
29 KiB

2 years ago
<!DOCTYPE html><html lang="ko"><head>
<meta charset="utf-8">
<title>그림자<(Shadows)</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 – 그림자(Shadows)">
<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>그림자(Shadows)</h1>
</div>
<div class="lesson">
<div class="lesson-main">
<p>※ 이 글은 Three.js의 튜토리얼 시리즈로서,
먼저 <a href="fundamentals.html">Three.js의 기본 구조에 관한 글</a>
읽고 오길 권장합니다.</p>
<p>※ 이전 글인 <a href="cameras.html">카메라에 관한 글</a>
<a href="lights.html">조명에 관한 글</a>에서 이 장을 읽는 꼭 필요한 내용을
다루었으니 꼭 먼저 읽고 오시기 바랍니다.</p>
<p>3D 그래픽에서 그림자란 그리 간단한 주제가 아닙니다. 그림자를 구현하는 방법은
아주 많지만 모두 단점이 있기에 어떤 것이 가장 효율적이라고 말하기 어렵습니다.
이는 Three.js에서 제공하는 방법도 마찬가지이죠.</p>
<p>Three.js는 기본적으로 <em>그림자 맵(shadow maps)</em>을 사용합니다. 그림자 맵이란
<em>그림자를 만드는 빛의 영향을 받는, 그림자를 드리우는 모든 물체를 빛의 시점에서
렌더링</em>하는 기법을 말합니다. 중요하니 <strong>한 번 더 읽어보세요!</strong></p>
<p>다시 말해, 공간 안에 20개의 물체와 5개의 조명이 있고, 20개의 물체 모두
그림자를 드리우며 5개의 조명 모두 그림자를 지게 한다면, 한 장면을 만들기
위해 총 6번 화면을 렌더링할 것이라는 이야기입니다. 먼저 조명 1번에 대해
20개의 물체를 전부 렌더링하고, 다음에는 2번 조명, 그 다음에는 3번...
이렇게 처음 5번 렌더링한 결과물을 합쳐 최종 결과물을 만드는 것이죠.</p>
<p>만약 여기에 포인트(point) 조명을 하나 추가하면 조명 하나 때문에 6번을 다시
렌더링해야 합니다.</p>
<p>이 때문에 그림자를 지게 하는 조명을 여러개 만들기보다 다른 방법을 찾는
경우가 보통입니다. 주로 사용하는 방법은 조명이 여러개 있어도 하나의 조명만
그림자를 지게끔 설정하는 것이죠.</p>
<p>물론 라이트맵(lightmaps)이나 앰비언트 오클루전(ambient occlusion)을 이용해
빛의 영향을 미리 계산할 수도 있습니다. 이러면 정적 조명이나 정적 빛 반사를
사용하는 것이기에 수정하기가 어렵지만, 적어도 성능은 빠릅니다. 이 두 가지
모두 나중에 별도로 다룰 것입니다.</p>
<p>가짜 그림자를 사용하는 방법도 있습니다. 평면을 만들고, 흑백 텍스처를 입혀
땅 위에 그림자가 있을 만한 위치에 가져다 놓는 것이죠.</p>
<p>예를 들어 아래 텍스처를 사용해 가짜 그림자를 만들어보겠습니다.</p>
<div class="threejs_center"><img src="../examples/resources/images/roundshadow.png"></div>
<p><a href="cameras.html">이전 글</a>에서 작성했던 코드를 일부 활용하겠습니다.</p>
<p>먼저 배경을 흰색으로 칠합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const scene = new THREE.Scene();
+scene.background = new THREE.Color('white');
</pre>
<p>같은 체크판 무늬 땅을 사용하되, 땅이 조명의 영향을 받을 필요는 없으니
<a href="/docs/#api/ko/materials/MeshBasicMaterial"><code class="notranslate" translate="no">MeshBasicMaterial</code></a>을 사용하겠습니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const loader = new THREE.TextureLoader();
{
const planeSize = 40;
- 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;
texture.repeat.set(repeats, repeats);
const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
const planeMat = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide,
});
+ planeMat.color.setRGB(1.5, 1.5, 1.5);
const mesh = new THREE.Mesh(planeGeo, planeMat);
mesh.rotation.x = Math.PI * -.5;
scene.add(mesh);
}
</pre>
<p>평면의 색상을 <code class="notranslate" translate="no">1.5, 1.5, 1.5</code>로 설정했습니다. 체크판 텍스처의 색상을 1.5, 1.5, 1.5 만큼
곱해준 것이죠. 체크판 원본 텍스처의 색상이 0x808080(회색), 0xC0C0C0(옅은 회색)이므로,
여기에 1.5를 곱해주면 흰색, 옅은 회색 체크판이 됩니다.</p>
<p>이제 그림자 텍스처를 로드해보죠.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const shadowTexture = loader.load('resources/images/roundshadow.png');
</pre>
<p>구체와 관련된 객체를 분류하기 위해 배열을 만들겠습니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const sphereShadowBases = [];
</pre>
<p>다음으로 구체 geometry를 만듭니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const sphereRadius = 1;
const sphereWidthDivisions = 32;
const sphereHeightDivisions = 16;
const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
</pre>
<p>가짜 그림자를 위한 평면 <code class="notranslate" translate="no">geometry</code>도 만듭니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const planeSize = 1;
const shadowGeo = new THREE.PlaneGeometry(planeSize, planeSize);
</pre>
<p>이제 구체를 아주 많이 만들겠습니다. 각각 구체마다 <code class="notranslate" translate="no">컨테이너</code> 역할을 할
<a href="/docs/#api/ko/core/Object3D"><code class="notranslate" translate="no">THREE.Object3D</code></a>를 만들고, 그림자 평면 mesh, 구체 mesh를 이
컨테이너의 자식으로 만듭니다. 이러면 구체와 그림자를 동시에 움직일 수
있죠. z-파이팅 현상을 막기 위해 그림자는 땅보다 약간 위에 둡니다.
<code class="notranslate" translate="no">depthWrite</code> 속성을 false로 설정해 그림자끼리 충돌하는 현상을
막습니다. 이 충돌 현상은 <a href="transparency.html">다른 글</a>에서
더 자세히 이야기할 거예요. 그림자는 빛을 반사하지 않으니 <a href="/docs/#api/ko/materials/MeshBasicMaterial"><code class="notranslate" translate="no">MeshBasicMaterial</code></a>
사용합니다.</p>
<p>구체의 색상을 각각 다르게 지정하고, 컨테이너, 구체 mesh, 그림자 mesh와
구체의 처음 y축 좌표를 배열에 기록합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const numSpheres = 15;
for (let i = 0; i &lt; numSpheres; ++i) {
// 구체와 그림자가 같이 움직이도록 컨테이너(base)를 만듭니다
const base = new THREE.Object3D();
scene.add(base);
/**
* 그림자를 컨테이너에 추가합니다
* 주의: 여기서는 각 구체의 투명도를 따로 설정할 수 있도록
* 재질을 각각 따로 만듬
*/
const shadowMat = new THREE.MeshBasicMaterial({
map: shadowTexture,
transparent: true, // 땅이 보이도록
depthWrite: false, // 그림자를 따로 정렬하지 않도록
});
const shadowMesh = new THREE.Mesh(shadowGeo, shadowMat);
shadowMesh.position.y = 0.001; // 그림자를 땅에서 살짝 위에 배치
shadowMesh.rotation.x = Math.PI * -.5;
const shadowSize = sphereRadius * 4;
shadowMesh.scale.set(shadowSize, shadowSize, shadowSize);
base.add(shadowMesh);
// 구체를 컨테이너에 추가
const u = i / numSpheres; // 반복문이 진행됨에 따라 0에서 1사이 값을 지정
const sphereMat = new THREE.MeshPhongMaterial();
sphereMat.color.setHSL(u, 1, .75);
const sphereMesh = new THREE.Mesh(sphereGeo, sphereMat);
sphereMesh.position.set(0, sphereRadius + 2, 0);
base.add(sphereMesh);
// y축 좌표를 포함해 나머지 요소를 기록
sphereShadowBases.push({ base, sphereMesh, shadowMesh, y: sphereMesh.position.y });
}
</pre>
<p>조명은 2개를 만들겠습니다. 하나는 <a href="/docs/#api/ko/lights/HemisphereLight"><code class="notranslate" translate="no">HemisphereLight</code></a>, 강도를 2로 설정해 화면을 아주
밝게 설정할 겁니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
const skyColor = 0xB1E1FF; // 하늘색
const groundColor = 0xB97A20; // 오렌지 브라운
const intensity = 2;
const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
scene.add(light);
}
</pre>
<p>다른 하나는 구체의 윤곽을 좀 더 분명하게 해 줄 <a href="/docs/#api/ko/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>입니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(0, 10, 5);
light.target.position.set(-5, 0, 0);
scene.add(light);
scene.add(light.target);
}
</pre>
<p>이대로도 렌더링해도 좋지만, 구체들에 애니메이션을 한 번 줘봅시다.
컨테이너를 움직여 구체, 그림자가 xz축 평면을 따라 움직이게 하고,
<a href="/docs/#api/ko/math/Math.abs(Math.sin(time))"><code class="notranslate" translate="no">Math.abs(Math.sin(time))</code></a>를 사용해 구체에 공처럼 통통 튀는
애니메이션을 넣어줍니다. 또 그림자 재질의 투명도를 조절해 구체가
높을수록 그림자가 옅어지도록 합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
time *= 0.001; // 초 단위로 변환
...
sphereShadowBases.forEach((sphereShadowBase, ndx) =&gt; {
const { base, sphereMesh, shadowMesh, y } = sphereShadowBase;
// u는 구체의 반복문을 실행하면서 인덱스에 따라 0 이상, 1 이하로 지정됩니다
const u = ndx / sphereShadowBases.length;
/**
* 컨테이너의 위치를 계산합니다. 구체와 그림자가
* 컨테이너에 종속적이므로 위치가 같이 변합니다
*/
const speed = time * .2;
const angle = speed + u * Math.PI * 2 * (ndx % 1 ? 1 : -1);
const radius = Math.sin(speed - ndx) * 10;
base.position.set(Math.cos(angle) * radius, 0, Math.sin(angle) * radius);
// yOff 값은 0 이상 1 이하입니다
const yOff = Math.abs(Math.sin(time * 2 + ndx));
// 구체를 위아래로 튕김
sphereMesh.position.y = y + THREE.MathUtils.lerp(-2, 2, yOff);
// 구체가 위로 올라갈수록 그림자가 옅어짐
shadowMesh.material.opacity = THREE.MathUtils.lerp(1, .25, yOff);
});
...
</pre>
<p>15가지 색상의 탱탱볼을 완성했습니다.</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/shadows-fake.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/shadows-fake.html" target="_blank">새 탭에서 보기</a>
</div>
<p></p>
<p>물론 다른 모양의 그림자를 사용해야 하는 경우도 있습니다. 그림자의 경계를 분명하게
하고 싶을 수도 있죠. 하지만 모든 물체의 그림자를 둥글게 표현하는 것이 좋은 경우도
분명 있습니다. 모든 그림자를 둥글게 표현한 예 중 하나는 <a href="https://www.google.com/search?tbm=isch&amp;q=animal+crossing+pocket+camp+screenshots">동물의 숲 포켓 캠프</a>입니다.
자연스럽고 성능면에서도 이득이죠. <a href="https://www.google.com/search?q=monument+valley+screenshots&amp;tbm=isch">Monument Valley</a>
메인 캐릭터에 이런 그림자를 사용한 것으로 보입니다.</p>
<p>이제 그림자 맵을 살펴보겠습니다. 그림자를 드리울 수 있는 조명은 3가지, <a href="/docs/#api/ko/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>,
<a href="/docs/#api/ko/lights/PointLight"><code class="notranslate" translate="no">PointLight</code></a>, <a href="/docs/#api/ko/lights/SpotLight"><code class="notranslate" translate="no">SpotLight</code></a>입니다.</p>
<p><a href="lights.html">조명에 관한 글</a>에서 썼던 예제로 먼저 <a href="/docs/#api/ko/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>부터
살펴보죠.</p>
<p>먼저 renderer의 그림자 맵 옵션을 켜야 합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const renderer = new THREE.WebGLRenderer({canvas});
+renderer.shadowMap.enabled = true;
</pre>
<p>조명도 그림자를 드리우도록 옵션을 활성화합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const light = new THREE.DirectionalLight(color, intensity);
+light.castShadow = true;
</pre>
<p>또한 장면(scene) 안 각 mesh에 그림자를 드리울지, 그림자의 영향을 받을지 설정해줘야 합니다.</p>
<p>바닥 아래는 굳이 신경 쓸 필요가 없으니 평면(바닥)은 그림자의 영향만 받게 하겠습니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const mesh = new THREE.Mesh(planeGeo, planeMat);
mesh.receiveShadow = true;
</pre>
<p>정육면체와 구체는 그림자도 드리우고, 영향도 받도록 설정합니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const mesh = new THREE.Mesh(cubeGeo, cubeMat);
mesh.castShadow = true;
mesh.receiveShadow = true;
...
const mesh = new THREE.Mesh(sphereGeo, sphereMat);
mesh.castShadow = true;
mesh.receiveShadow = true;
</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/shadows-directional-light.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/shadows-directional-light.html" target="_blank">새 탭에서 보기</a>
</div>
<p></p>
<p>이런, 그림자 일부가 잘려나간 것이 보이나요?</p>
<p>이는 빛의 시점에서 장면을 렌더링해 그림자 맵을 만들기 때문입니다. 위 예제를 예로 들면
<a href="/docs/#api/ko/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>의 위치에 카메라가 있고, 해당 조명의 목표를 바라보는 것이죠. 조명의
그림자에는 별도의 카메라가 있고, 이전에 <a href="cameras.html">카메라에 관한 글</a>에서
설명한 것처럼 일정 공간 안의 그림자만 렌더링합니다. 위 예제에서는 그 공간이 너무 좁은
것이죠.</p>
<p>그림자용 카메라를 시각화하기 위해 조명의 그림자 속성에서 카메라를 가져와 <a href="/docs/#api/ko/helpers/CameraHelper"><code class="notranslate" translate="no">CameraHelper</code></a>
생성한 뒤, 장면에 추가하겠습니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cameraHelper = new THREE.CameraHelper(light.shadow.camera);
scene.add(cameraHelper);
</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/shadows-directional-light-with-camera-helper.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/shadows-directional-light-with-camera-helper.html" target="_blank">새 탭에서 보기</a>
</div>
<p></p>
<p>target의 x 값을 조정해보면 그림자용 카메라 범위 안에 있는 곳에만 그림자가 보이는
것을 확인할 수 있을 겁니다.</p>
<p>이 공간의 크기는 이 카메라의 속성을 수정해 바꿀 수 있습니다.</p>
<p>그림자용 카메라의 속성을 수정하는 GUI를 추가해보죠. <a href="/docs/#api/ko/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>는 빛이 평행으로
나아가므로, <a href="/docs/#api/ko/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>는 그림자용 카메라로 <a href="/docs/#api/ko/cameras/OrthographicCamera"><code class="notranslate" translate="no">OrthographicCamera</code></a>(정사영 카메라)를
사용합니다. <a href="/docs/#api/ko/cameras/OrthographicCamera"><code class="notranslate" translate="no">OrthographicCamera</code></a>가 뭔지 잘 기억나지 않는다면, <a href="cameras.html">카메라에 관한 이전 글</a>
참고하세요.</p>
<p><a href="/docs/#api/ko/cameras/OrthographicCamera"><code class="notranslate" translate="no">OrthographicCamera</code></a>의 시야는 육면체나 <em>절두체(frustum)</em>로 정의한다고 했었죠. <code class="notranslate" translate="no">left</code>,
<code class="notranslate" translate="no">right</code>, <code class="notranslate" translate="no">top</code>, <code class="notranslate" translate="no">bottom</code>, <code class="notranslate" translate="no">near</code>, <code class="notranslate" translate="no">far</code>, <code class="notranslate" translate="no">zoom</code> 속성을 지정해서요.</p>
<p>lil-gui가 쓸 간단한 헬퍼 클래스를 하나 더 만들겠습니다. 이 <code class="notranslate" translate="no">DimensionGUIHelper</code>
객체와 속성 이름 2개를 인자로 받아, GUI가 하나의 값을 조정할 때 하나의 값은 양수로,
다른 값은 음수로 지정합니다. 이렇게 하면 <code class="notranslate" translate="no">left</code><code class="notranslate" translate="no">right</code>값을 <code class="notranslate" translate="no">width</code>로, <code class="notranslate" translate="no">up</code>
<code class="notranslate" translate="no">down</code>값을 <code class="notranslate" translate="no">height</code>로 바꾸어 조작할 수 있죠.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">class DimensionGUIHelper {
constructor(obj, minProp, maxProp) {
this.obj = obj;
this.minProp = minProp;
this.maxProp = maxProp;
}
get value() {
return this.obj[this.maxProp] * 2;
}
set value(v) {
this.obj[this.maxProp] = v / 2;
this.obj[this.minProp] = v / -2;
}
}
</pre>
<p>또한 <a href="cameras.html">이전 글</a>에서 썼던 <code class="notranslate" translate="no">MinMaxGUIHelper</code>를 가져와 <code class="notranslate" translate="no">near</code>
<code class="notranslate" translate="no">far</code> 속성을 조작하는 데 사용하겠습니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
gui.add(light, 'intensity', 0, 2, 0.01);
+{
+ const folder = gui.addFolder('Shadow Camera');
+ folder.open();
+ folder.add(new DimensionGUIHelper(light.shadow.camera, 'left', 'right'), 'value', 1, 100)
+ .name('width')
+ .onChange(updateCamera);
+ folder.add(new DimensionGUIHelper(light.shadow.camera, 'bottom', 'top'), 'value', 1, 100)
+ .name('height')
+ .onChange(updateCamera);
+ const minMaxGUIHelper = new MinMaxGUIHelper(light.shadow.camera, 'near', 'far', 0.1);
+ folder.add(minMaxGUIHelper, 'min', 0.1, 50, 0.1).name('near').onChange(updateCamera);
+ folder.add(minMaxGUIHelper, 'max', 0.1, 50, 0.1).name('far').onChange(updateCamera);
+ folder.add(light.shadow.camera, 'zoom', 0.01, 1.5, 0.01).onChange(updateCamera);
+}
</pre>
<p>그리고 값이 바뀔 때마다 <code class="notranslate" translate="no">updateCamera</code> 함수를 호출하도록 합니다. 이 함수 안에서는
조명, 조명 헬퍼, 조명의 그림자용 카메라, 그림자용 카메라의 헬퍼를 업데이트할 거예요.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function updateCamera() {
// 헬퍼가 가이드라인을 그릴 때 필요한 조명 목표(target)의 matrixWorld를 업데이트 합니다
light.target.updateMatrixWorld();
helper.update();
// 그림자용 카메라의 투영 행렬(projection matrix)를 업데이트합니다
light.shadow.camera.updateProjectionMatrix();
// 그림자용 카메라를 보기 위해 설치한 카메라의 헬퍼를 업데이트합니다
cameraHelper.update();
}
updateCamera();
</pre>
<p>이제 그림자용 카메라에 GUI가 생겼으니, 값들을 조정하며 놀아봅시다.</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/shadows-directional-light-with-camera-gui.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/shadows-directional-light-with-camera-gui.html" target="_blank">새 탭에서 보기</a>
</div>
<p></p>
<p><code class="notranslate" translate="no">width</code><code class="notranslate" translate="no">height</code> 속성을 30 정도로 조정하면 그림자가 있어야 할만한 공간은
대부분 그림자용 카메라 안에 속할 겁니다.</p>
<p>하지만 여기서 의문이 하나 생깁니다. 어째서 <code class="notranslate" translate="no">width</code><code class="notranslate" translate="no">height</code>를 완전 큰 값으로
설정해 모든 요소를 다 포함하도록 하지 않는 걸까요? <code class="notranslate" translate="no">width</code><code class="notranslate" translate="no">height</code>를 100 정도로
설정해보세요. 아래와 같은 현상이 나타날 겁니다.</p>
<div class="threejs_center"><img src="../resources/images/low-res-shadow-map.png" style="width: 369px"></div>
<p>왜 그림자의 해상도가 낮아졌을까요?</p>
<p>이는 그림자 관련 설정을 할 때 항상 주의해야하는 부분입니다. 사실 그림자 맵은 그림자가
포함된 하나의 텍스처입니다. 이 텍스처는 크기가 정해져 있죠. 위 예제에서 카메라의 공간을
늘리면, 이 텍스처 또한 늘어납니다. 다시 말해 공간을 크게 설정할수록 그림자가 더 각져
보일 거라는 얘기죠.</p>
<p>그림자 맵의 해상도는 <code class="notranslate" translate="no">light.shadow.mapSize</code> 속성의 <code class="notranslate" translate="no">width</code><code class="notranslate" translate="no">height</code> 속성으로 설정합니다(기본값은
512x512). 그림자 맵은 크게 설정할수록 메모리를 많이 차지하고, 연산이 더 복잡해지므로
가능한 작게 설정하는 것이 좋습니다. 이는 그림자용 카메라의 공간도 마찬가지죠. 작을 수록
그림자의 퀄리티가 좋아질 테니 가능한 공간을 작게 설정하는 것이 좋습니다. 또한 기기마다
렌더링할 수 있는 텍스처의 용량이 정해져 있으니 주의해야 합니다. Three.js에서 이 용량은
<a href="/docs/#api/ko/renderers/WebGLRenderer#capabilities"><code class="notranslate" translate="no">renderer.capabilities.maxTextureSize</code></a>로 확인할 수 있습니다.</p>
<p><a href="/docs/#api/ko/lights/SpotLight"><code class="notranslate" translate="no">SpotLight</code></a>는 그림자용 카메라로 <a href="/docs/#api/ko/cameras/PerspectiveCamera"><code class="notranslate" translate="no">PerspectiveCamera</code></a>(원근 카메라)를 사용합니다. <a href="/docs/#api/ko/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a>
그림자용 카메라는 거의 모든 속성을 직접 변경할 수 있었지만, <a href="/docs/#api/ko/lights/SpotLight"><code class="notranslate" translate="no">SpotLight</code></a>의 그림자용 카메라는
조명 속성의 영향을 받습니다. 카메라의 <code class="notranslate" translate="no">fov</code> 속성은 <a href="/docs/#api/ko/lights/SpotLight"><code class="notranslate" translate="no">SpotLight</code></a><code class="notranslate" translate="no">angle</code> 속성과 직접 연결되어
있죠. <code class="notranslate" translate="no">aspect</code>는 그림자 맵의 크기에 따라 자동으로 정해집니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const light = new THREE.DirectionalLight(color, intensity);
+const light = new THREE.SpotLight(color, intensity);
</pre>
<p>추가로 <a href="lights.html">이전 글</a>에서 썼던 <code class="notranslate" translate="no">penumbra(반음영)</code>, <code class="notranslate" translate="no">angle</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/shadows-spot-light-with-camera-gui.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/shadows-spot-light-with-camera-gui.html" target="_blank">새 탭에서 보기</a>
</div>
<p></p>
<p>마지막으로 <a href="/docs/#api/ko/lights/PointLight"><code class="notranslate" translate="no">PointLight</code></a>를 살펴보죠. <a href="/docs/#api/ko/lights/PointLight"><code class="notranslate" translate="no">PointLight</code></a>는 모든 방향으로 빛을 발산하기에
관련 설정은 <code class="notranslate" translate="no">near</code><code class="notranslate" translate="no">far</code> 정도입니다. 그리고 사실 <a href="/docs/#api/ko/lights/PointLight"><code class="notranslate" translate="no">PointLight</code></a>의 그림자는 정육면체의
각 면에 <a href="/docs/#api/ko/lights/SpotLight"><code class="notranslate" translate="no">SpotLight</code></a>를 하나씩, 총 6개의 그림자를 놓은 것과 같습니다. 한 방향에 한
번씩, 총 6번을 렌더링해야 하니 렌더링 속도가 훨씬 느리겠죠.</p>
<p>이번에는 장면 주위에 상자를 두어 벽과 천장에도 그림자가 생길 수 있도록 해보겠습니다.
먼저 재질의 <code class="notranslate" translate="no">side</code> 속성을 <code class="notranslate" translate="no">THREE.BackSide</code>로 설정해 외부 상자의 밖이 아닌 안을 렌더링
하도록 합니다. 바닥과 마찬가지로 그림자를 드리우지 않도록 설정하고, z-파이팅 현상을
피하기 위해 외부 상자를 바닥보다 살짝 아래에 둡니다.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
const cubeSize = 30;
const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
const cubeMat = new THREE.MeshPhongMaterial({
color: '#CCC',
side: THREE.BackSide,
});
const mesh = new THREE.Mesh(cubeGeo, cubeMat);
mesh.receiveShadow = true;
mesh.position.set(0, cubeSize / 2 - 0.1, 0);
scene.add(mesh);
}
</pre>
<p>물론 조명도 <a href="/docs/#api/ko/lights/PointLight"><code class="notranslate" translate="no">PointLight</code></a>로 바꿔야죠.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const light = new THREE.SpotLight(color, intensity);
+const light = new THREE.PointLight(color, intensity);
....
// 조명이 위치를 확인하기 쉽도록 헬퍼 추가
+const helper = new THREE.PointLightHelper(light);
+scene.add(helper);
</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/shadows-point-light.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/shadows-point-light.html" target="_blank">새 탭에서 보기</a>
</div>
<p></p>
<p>GUI의 <code class="notranslate" translate="no">position</code> 속성을 조정해 조명을 움직이면 벽에 그림자가 지는 걸
확인할 수 있을 겁니다. 다른 그림자와 마찬가지로 <code class="notranslate" translate="no">near</code> 값보다 가까운
곳은 그림자가 지지 않고, <code class="notranslate" translate="no">far</code> 값보다 먼 곳은 항상 그림자가 지죠.</p>
</div>
</div>
</div>
<script src="/manual/resources/prettify.js"></script>
<script src="/manual/resources/lesson.js"></script>
</body></html>