SeemsPyo 5 years ago
parent
commit
75a68e2d05

+ 475 - 0
threejs/lessons/kr/threejs-custom-geometry.md

@@ -0,0 +1,475 @@
+Title: Three.js 사용자 지정 Geometry
+Description: 사용자 지정 geometry를 만드는 법에 대해 알아봅니다
+TOC: 사용자 지정 Geometry
+
+[이전 글](threejs-primitives.html)에서는 Three.js의 내장 원시 모델에
+대해 살펴보았죠. 이 글에서는 이런 모델, geometry를 직접 만들어 볼 것입니다.
+
+거듭 이야기하지만, 정말 진지하게 3D 컨텐츠를 만들 생각이라면
+[블렌더(Blender)](https://blender.org),
+[마야(Maya)](https://www.autodesk.com/products/maya/overview),
+[3D Studio Max](https://www.autodesk.com/products/3ds-max/overview),
+[시네마4D(Cinema4D)](https://www.maxon.net/en-us/) 등의 3D 모델링
+프로그램을 사용하는 것이 좋습니다. 모델을 만들고 [gLTF](threejs-load-gltf.html)나
+[.obj](threejs-load-obj.html) 포멧으로 저장하여 프로젝트에서 불러오는
+것이죠. 어떤 프로그램을 선택하든 튜토리얼에는 유용한 내용이 많으니, 2주에서
+3주 정도는 해당 프로그램의 튜토리얼을 익히는 데 투자하기 바랍니다.
+
+하지만 때로는 모델링 프로그램을 쓰는 것보다 직접 3D geometry를
+만드는 게 유리할 수 있을 겁니다.
+
+먼저 정육면체를 하나 만들어보겠습니다. 이미 원시 모델에 `BoxGeometry`와
+`BoxBufferGeometry`가 있긴 하지만, 기본 개념을 이해하는 데는 간단한 게
+훨씬 효과적일 테니까요.
+
+Three.js에서 사용자 지정 geometry는 `Geometry` 또는 `BufferGeometry`,
+2가지 클래스를 이용해 만들 수 있습니다. `Geometry`는 분명 사용하기에는
+편하지만 느리고 메모리도 많이 차지합니다. 삼각형 천 개 정도까지야 그냥
+써도 나쁠 것이 없지만, 만 개가 넘어간다면 `BufferGeometry`를 고려하는
+것이 좋죠.
+
+당연하게도 `BufferGeometry`는 훨씬 쓰기 어렵지만, 비교적 메모리도 덜 차지하고
+훨씬 빠릅니다. 대략 구상하기에 삼각형을 10000개 이상 쓰겠다 싶으면 `BufferGeometry`를
+고려하기 바랍니다.
+
+아까 `Geometry`가 느리다고 했는데, 이는 처음 로드할 때와 수정할 때 느리다는
+것이지, 렌더링 속도가 느리다는 말이 아닙니다. geometry를 수정하지 않고 geometry가
+무지막지하게 크지 않은 한, `Geometry`는 `BufferGeometry`에 비해 프로그램 초기화
+단계에서만 약간 더 느릴 뿐입니다. 결국에는 둘 다 살펴보겠지만, 일단-제 생각에-훨씬
+간단하고 이해하기 쉬운 `Geometry`를 먼저 써봅시다.
+
+먼저 정육면체를 만들겠습니다. [반응형 디자인에 관한 글](threejs-responsive.html)에서
+썼던 예제를 일부 가져오도록 하죠.
+
+먼저 `BoxGeometry`를 `Geometry`로 교체합니다.
+
+```js
+-const boxWidth = 1;
+-const boxHeight = 1;
+-const boxDepth = 1;
+-const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
++const geometry = new THREE.Geometry();
+```
+
+이제 정육면체의 꼭지점를 추가합니다. 정육면체의 모서리는 총 8개이죠.
+
+<div class="threejs_center"><img src="resources/cube-vertex-positions.svg" style="width: 500px"></div>
+
+중점을 중심으로 각 꼭지점을 추가합니다.
+
+```js
+const geometry = new THREE.Geometry();
++geometry.vertices.push(
++  new THREE.Vector3(-1, -1,  1),  // 0
++  new THREE.Vector3( 1, -1,  1),  // 1
++  new THREE.Vector3(-1,  1,  1),  // 2
++  new THREE.Vector3( 1,  1,  1),  // 3
++  new THREE.Vector3(-1, -1, -1),  // 4
++  new THREE.Vector3( 1, -1, -1),  // 5
++  new THREE.Vector3(-1,  1, -1),  // 6
++  new THREE.Vector3( 1,  1, -1),  // 7
++);
+```
+
+다음으로 정육면체 한 면에 2개씩, 총 삼각형 12개를 추가해야 합니다.
+
+<div class="threejs_center"><img src="resources/cube-triangles.svg" style="width: 500px"></div>
+
+`Face3` 인스턴스를 만들어 3 꼭지점의 인덱스(index)를 넘겨주면 삼각형
+면(face)을 만들 수 있습니다.
+
+꼭지점의 인덱스를 넘겨줄 때는 순서에 유의해야 합니다. 삼각형이 카메라를
+바라볼 때, 면이 정육면체의 바깥쪽을 향하려면 시계 반대 방향 순으로 인덱스를
+넘겨줘야 합니다.
+
+<div class="threejs_center"><img src="resources/cube-vertex-winding-order.svg" style="width: 500px"></div>
+
+이 패턴대로 정육면체를 구성할 삼각형 12개를 만들어봅시다.
+
+```js
+geometry.faces.push(
+  // 앞쪽
+  new THREE.Face3(0, 3, 2),
+  new THREE.Face3(0, 1, 3),
+  // 오른쪽
+  new THREE.Face3(1, 7, 3),
+  new THREE.Face3(1, 5, 7),
+  // 뒷쪽
+  new THREE.Face3(5, 6, 7),
+  new THREE.Face3(5, 4, 6),
+  // 왼쪽
+  new THREE.Face3(4, 2, 6),
+  new THREE.Face3(4, 0, 2),
+  // 상단
+  new THREE.Face3(2, 7, 6),
+  new THREE.Face3(2, 3, 7),
+  // 하단
+  new THREE.Face3(4, 1, 0),
+  new THREE.Face3(4, 5, 1),
+);
+```
+
+이제 몇 가지만 더 고치면 됩니다.
+
+이 정육면체들은 `BoxGeometry`보다 두 배 더 크므로, 카메라를 약간
+더 뒤로 옮겨줍니다.
+
+```js
+const fov = 75;
+const aspect = 2;  // canvas 기본값
+const near = 0.1;
+-const far = 5;
++const far = 100;
+const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+-camera.position.z = 2;
++camera.position.z = 5;
+```
+
+커진 만큼 간격을 띄우고, 바꾸는 김에 색상도 바꿔주겠습니다.
+
+```js
+const cubes = [
+-  makeInstance(geometry, 0x44aa88,  0),
+-  makeInstance(geometry, 0x8844aa, -2),
+-  makeInstance(geometry, 0xaa8844,  2),
++  makeInstance(geometry, 0x44FF44,  0),
++  makeInstance(geometry, 0x4444FF, -4),
++  makeInstance(geometry, 0xFF4444,  4),
+];
+```
+
+아직 법선(normal)을 넣지 않았는데, 법선이 없으면 빛은 소용이 없으니 마지막으로
+재질(material)을 `MeshBasicMaterial`로 바꿔줍니다.
+
+```js
+function makeInstance(geometry, color, x) {
+-  const material = new THREE.MeshPhongMaterial({ color });
++  const material = new THREE.MeshBasicMaterial({ color });
+
+  const cube = new THREE.Mesh(geometry, material);
+  scene.add(cube);
+
+  ...
+```
+
+자, 이제 실행해보죠.
+
+{{{example url="../threejs-custom-geometry-cube.html" }}}
+
+각 삼각형 면의 `color` 속성을 바꿔 색을 따로 지정할 수 있습니다.
+
+```js
+geometry.faces[ 0].color = geometry.faces[ 1].color = new THREE.Color('red');
+geometry.faces[ 2].color = geometry.faces[ 3].color = new THREE.Color('yellow');
+geometry.faces[ 4].color = geometry.faces[ 5].color = new THREE.Color('green');
+geometry.faces[ 6].color = geometry.faces[ 7].color = new THREE.Color('cyan');
+geometry.faces[ 8].color = geometry.faces[ 9].color = new THREE.Color('blue');
+geometry.faces[10].color = geometry.faces[11].color = new THREE.Color('magenta');
+```
+
+추가로 재질을 생성할 때 `FaceColors`를 사용한다고 명시해야 하죠.
+
+```js
+-const material = new THREE.MeshBasicMaterial({ color });
++const material = new THREE.MeshBasicMaterial({ vertexColors: THREE.FaceColors });
+```
+
+{{{example url="../threejs-custom-geometry-cube-face-colors.html" }}}
+
+또는 삼각형 면의 `vertextColors` 속성에 각 꼭지점의 색상을 지정할 수도 있습니다.
+
+```js
+geometry.faces.forEach((face, ndx) => {
+  face.vertexColors = [
+    (new THREE.Color()).setHSL(ndx / 12      , 1, 0.5),
+    (new THREE.Color()).setHSL(ndx / 12 + 0.1, 1, 0.5),
+    (new THREE.Color()).setHSL(ndx / 12 + 0.2, 1, 0.5),
+  ];
+});
+```
+
+이 또한 꼭지점 색을 사용한다고 명시해야 하죠.
+
+```js
+-const material = new THREE.MeshBasicMaterial({vertexColors: THREE.FaceColors});
++const material = new THREE.MeshBasicMaterial({vertexColors: THREE.VertexColors});
+```
+
+{{{example url="../threejs-custom-geometry-cube-vertex-colors.html" }}}
+
+빛을 사용하려면 법선을 추가해야 합니다. 법선이란 특정 방향을 나타내는 벡터값으로,
+색과 마찬가지로 법선도 각 삼각형 면의 `normal` 속성을 지정해 추가할 수 있습니다.
+
+```js
+face.normal = new THREE.Vector3(...)
+```
+
+또는 `vertexNormals` 속성에 각 꼭지점의 법선을 배열로 지정할 수도 있죠.
+
+```js
+face.vertexNormals = [
+  new THREE.Vector3(...),
+  new THREE.Vector3(...),
+  new THREE.Vector3(...),
+]
+```
+
+하지만 대게 우리가 지정한 좌표에 따라 알아서 법선을 계산해달라고 하는 게
+훨씬 편합니다.
+
+삼각형 면 법선의 경우 `Geometry.computeFaceNormals`를 호출하면 되죠.
+
+```js
+geometry.computeFaceNormals();
+```
+
+꼭지점 색을 제거하고 다시 재질을 `MeshPhongMaterial`로 바꾸겠습니다.
+
+```js
+-const material = new THREE.MeshBasicMaterial({ vertexColors: THREE.VertexColors });
++const material = new THREE.MeshPhongMaterial({ color });
+```
+
+이제 정육면체들이 빛의 영향을 받습니다.
+
+{{{example url="../threejs-custom-geometry-cube-face-normals.html" }}}
+
+삼각형 면 법선을 사용하면 물체가 각진 느낌을 줍니다. 꼭지점 법선을 사용하면
+훨씬 부드러워 보일 수 있죠. 꼭지점 법선은 `Geometry.computeVertexNormals`를
+호출해 사용합니다.
+
+```js
+-geometry.computeFaceNormals();
++geometry.computeVertexNormals();
+```
+
+아쉽게도 정육면체는 꼭지점 법선의 예제로 적당하지 않습니다. 각 꼭지점이 같은
+꼭지점을 쓰는 모든 삼각형 면에서 법선을 가져오기 때문이죠.
+
+{{{example url="../threejs-custom-geometry-cube-vertex-normals.html" }}}
+
+UV라고도 불리는, 텍스처 좌표는 `Geometry.faceVertexUvs` 속성에 삼각형 면들의
+층(layer)을 배열로 지정해 추가할 수 있습니다. 정육면체의 경우 다음처럼 지정할
+수 있죠.
+
+(※ 참고: [UV 매핑](https://ko.wikipedia.org/wiki/UV_%EB%A7%A4%ED%95%91). 역주)
+
+```js
+geometry.faceVertexUvs[0].push(
+  // 앞쪽
+  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
+  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
+  // 오른쪽
+  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
+  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
+  // 뒤쪽
+  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
+  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
+  // 왼쪽
+  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
+  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
+  // 상단
+  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
+  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
+  // 하단
+  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
+  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
+);
+```
+
+중요한 건 `faceVertexUvs`는 층의 배열이라는 점입니다. 하나의 층은 별도의 UV 좌표이죠.
+기본적으로 하나의 UV 층, 층 0이 있어, 예제에서는 그냥 그 층에 UV를 추가했습니다.
+
+다시 삼각형 면 법선을 계산하도록 코드를 바꾸고, 이번에는 재질에 [텍스처를 추가](threejs-textures.html)하겠습니다.
+
+```js
+-geometry.computeVertexNormals();
++geometry.computeFaceNormals();
+
++const loader = new THREE.TextureLoader();
++const texture = loader.load('resources/images/star.png');
+
+function makeInstance(geometry, color, x) {
+-  const material = new THREE.MeshPhongMaterial({color});
++  const material = new THREE.MeshPhongMaterial({color, map: texture});
+
+  const cube = new THREE.Mesh(geometry, material);
+  scene.add(cube);
+
+  ...
+```
+
+{{{example url="../threejs-custom-geometry-cube-texcoords.html" }}}
+
+다음으로는 이 글에서 배운 것을 총 동원해 지형 mesh를 기반으로 높이 맵(heightmap)을
+만들어보겠습니다.
+
+높이 맵을 기반으로 한 지형이란, 이차원 높이 배열을 격자 형태로 만든 것을
+말합니다. 이차원 높이 배열을 만드는 가장 쉬운 방법은 이미지 편집 프로그램을
+사용하는 것이죠. 아래는 제가 만든 96x64 픽셀의 이미지입니다.
+
+<div class="threejs_center"><img src="../resources/images/heightmap-96x64.png" style="width: 512px; image-rendering: pixelated;"></div>
+
+이 이미지의 데이터를 불러와 높이 맵 mesh를 만들겠습니다. 이미지 데이터를
+불러올 때는 `ImageLoader`를 활용합니다.
+
+```js
+const imgLoader = new THREE.ImageLoader();
+imgLoader.load('resources/images/heightmap-96x64.png', createHeightmap);
+
+function createHeightmap(image) {
+  // canvas에 이미지를 렌더링한 후, getImageData 메서드를 호출해 픽셀 데이터를 추출합니다
+  const ctx = document.createElement('canvas').getContext('2d');
+  const { width, height } = image;
+  ctx.canvas.width = width;
+  ctx.canvas.height = height;
+  ctx.drawImage(image, 0, 0);
+  const { data } = ctx.getImageData(0, 0, width, height);
+
+  const geometry = new THREE.Geometry();
+```
+
+이미지에서 데이터를 추출했으니, 이제 격자를 만들어야 합니다. 이미지의 픽셀
+하나당 정사각형 격자 한 칸을 만듭니다.
+
+<div class="threejs_center"><img src="resources/heightmap-points.svg" style="width: 500px"></div>
+
+격자 한 칸당 꼭지점 5개를 만듭니다. 정사각형의 각 꼭지점 당 하나씩 총 4개를 두고,
+네 꼭지점의 높이를 평균내 중앙에 하나를 둡니다.
+
+```js
+const cellsAcross = width - 1;
+const cellsDeep = height - 1;
+for (let z = 0; z < cellsDeep; ++z) {
+  for (let x = 0; x < cellsAcross; ++x) {
+    /**
+     * 열의 위치를 높이 데이터로 계산합니다
+     * 데이터가 RGBA이므로 4를 곱하지만, R 값만 사용합니다
+     **/
+    const base0 = (z * width + x) * 4;
+    const base1 = base0 + (width * 4);
+
+    // 격자 칸 각 꼭지점의 높이를 참조합니다
+    const h00 = data[base0] / 32;
+    const h01 = data[base0 + 4] / 32;
+    const h10 = data[base1] / 32;
+    const h11 = data[base1 + 4] / 32;
+    // 높이의 평균값을 구합니다
+    const hm = (h00 + h01 + h10 + h11) / 4;
+
+    // 꼭지점의 위치
+    const x0 = x;
+    const x1 = x + 1;
+    const z0 = z;
+    const z1 = z + 1;
+
+    // 각 꼭지점의 첫 번째 인덱스를 기록합니다
+    const ndx = geometry.vertices.length;
+
+    // 격자에 모퉁이 꼭지점 4개와 중앙 꼭지점 하나를 배치합니다
+    geometry.vertices.push(
+      new THREE.Vector3(x0, h00, z0),
+      new THREE.Vector3(x1, h01, z0),
+      new THREE.Vector3(x0, h10, z1),
+      new THREE.Vector3(x1, h11, z1),
+      new THREE.Vector3((x0 + x1) / 2, hm, (z0 + z1) / 2),
+    );
+```
+
+다음으로 방금 만든 5개의 정점을 모아 4개의 삼각형을 만들어야 합니다.
+
+<div class="threejs_center"><img src="resources/heightmap-triangles.svg" style="width: 500px"></div>
+
+```js
+    // 삼각형 4개를 만듭니다
+    geometry.faces.push(
+      new THREE.Face3(ndx + 0, ndx + 4, ndx + 1),
+      new THREE.Face3(ndx + 1, ndx + 4, ndx + 3),
+      new THREE.Face3(ndx + 3, ndx + 4, ndx + 2),
+      new THREE.Face3(ndx + 2, ndx + 4, ndx + 0),
+    );
+
+    // 각 삼각형 면의 각 꼭지점에 텍스처 좌표를 추가합니다
+    const u0 = x / cellsAcross;
+    const v0 = z / cellsDeep;
+    const u1 = (x + 1) / cellsAcross;
+    const v1 = (z + 1) / cellsDeep;
+    const um = (u0 + u1) / 2;
+    const vm = (v0 + v1) / 2;
+    geometry.faceVertexUvs[0].push(
+      [ new THREE.Vector2(u0, v0), new THREE.Vector2(um, vm), new THREE.Vector2(u1, v0) ],
+      [ new THREE.Vector2(u1, v0), new THREE.Vector2(um, vm), new THREE.Vector2(u1, v1) ],
+      [ new THREE.Vector2(u1, v1), new THREE.Vector2(um, vm), new THREE.Vector2(u0, v1) ],
+      [ new THREE.Vector2(u0, v1), new THREE.Vector2(um, vm), new THREE.Vector2(u0, v0) ],
+    );
+  }
+}
+```
+
+이제 마무리 지어보죠.
+
+```js
+  geometry.computeFaceNormals();
+
+  // geometry를 중점에 배치
+  geometry.translate(width / -2, 0, height / -2);
+
+  const loader = new THREE.TextureLoader();
+  const texture = loader.load('resources/images/star.png');
+
+  const material = new THREE.MeshPhongMaterial({ color: 'green', map: texture });
+
+  const cube = new THREE.Mesh(geometry, material);
+  scene.add(cube);
+}
+```
+
+장면을 보기 쉽도록 몇 가지 요소를 추가하겠습니다.
+
+`OrbitControls`를 추가하고,
+
+```js
+import * as THREE from './resources/three/r115/build/three.module.js';
++import { OrbitControls } from './resources/threejs/r115/examples/jsm/controls/OrbitControls.js';
+```
+
+```js
+const fov = 75;
+const aspect = 2;  // canvas 기본 비율
+const near = 0.1;
+-const far = 100;
++const far = 200;
+const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+-camera.position.z = 5;
++camera.position.set(20, 20, 20);
+
++const controls = new OrbitControls(camera, canvas);
++controls.target.set(0, 0, 0);
++controls.update();
+```
+
+조명도 두 개 추가합니다.
+
+```js
+-{
++function addLight(...pos) {
+  const color = 0xFFFFFF;
+  const intensity = 1;
+  const light = new THREE.DirectionalLight(color, intensity);
+-  light.position.set(-1, 2, 4\);
++  light.position.set(...pos);
+  scene.add(light);
+}
+
++addLight(-1, 2, 4);
++addLight(1, 2, -2);
+```
+
+정육면체를 회전시키는 코드는 필요없으니 삭제하도록 하죠.
+
+{{{example url="../threejs-custom-geometry-heightmap.html" }}}
+
+이 글이 `Geometry`를 활용하는 데 도움이 되었으면 합니다.
+
+글이 길어졌으니 `BufferGeometry`는 [다음 글](threejs-custom-buffergeometry.html)에서
+살펴보도록 하겠습니다.

+ 58 - 64
threejs/lessons/kr/threejs-fog.md

@@ -12,7 +12,7 @@ TOC: 안개(Fog)
 
 3D 엔진에서 안개란, 일반적으로 카메라로부터의 거리에 따라 특정 색상으로
 점차 변화하는 것을 말합니다. Three.js에서는 `Fog`나 `FogExp2` 객체를
-생성한 뒤 장면(scene)의 [`fog`](Scene.fog) 속성에 지정해 안개를 사용합니다.
+생성한 뒤, 장면(scene)의 [`fog`](Scene.fog) 속성에 지정해 안개를 사용합니다.
 
 `Fog`는 인자로 `near`와 `far`값을 받는데, 이는 카메라로부터의 거리값입니다.
 `near`값보다 가까운 공간은 안개의 영향이 전혀 없고, `far`값보다 먼 공간은
@@ -70,11 +70,11 @@ scene.background = new THREE.Color('#F00');  // 빨강
 
 <div class="spread">
   <div>
-    <div data-diagram="fogBlueBackgroundRed" class="border"></div>
+    <div data-diagram="fogBlueBackgroundRed" style="height:300px" class="border"></div>
     <div class="code">파란 안개, 빨간 배경</div>
   </div>
   <div>
-    <div data-diagram="fogBlueBackgroundBlue" class="border"></div>
+    <div data-diagram="fogBlueBackgroundBlue" style="height:300px" class="border"></div>
     <div class="code">파란 안개, 파란 배경</div>
   </div>
 </div>
@@ -101,19 +101,18 @@ const scene = new THREE.Scene();
 
 {{{example url="../threejs-fog.html" }}}
 
-Let's add an interface so we can adjust the fog. Again we'll use
-[dat.GUI](https://github.com/dataarts/dat.gui). dat.GUI takes
-an object and a property and automagically makes an interface
-for that type of property. We could just simply let it manipulate
-the fog's `near` and `far` properties but it's invalid to have
-`near` be greater than `far` so let's make a helper so dat.GUI
-can manipulate a `near` and `far` property but we'll make sure `near`
-is less than or equal to `far` and `far` is greater than or equal `near`.
+인터페이스를 추가해 안개를 조정할 수 있도록 하겠습니다. 이번에도 [dat.GUI](https://github.com/dataarts/dat.gui)를
+사용할 거예요. dat.GUI는 객체와 객체의 속성 키값을 받아 자동으로 인터페이스를
+생성합니다. 단순히 안개의 `near`와 `far` 제어하도록 설정할 수도 있지만, `near`값이
+`far`값보다 큰 경우는 없기에 헬퍼를 만들어 `near`값을 항상 `far`보다 같거나
+작게, `far`값을 항상 `near`보다 같거나 크게 설정하도록 하겠습니다.
 
 ```js
-// We use this class to pass to dat.gui
-// so when it manipulates near or far
-// near is never > far and far is never < near
+/**
+ * 이 클래스의 인스턴스를 dat.GUI에 넘겨
+ * near나 far 속성을 조정할 때 항상
+ * near는 never >= far, far는 never <= near가 되도록 합니다
+ **/
 class FogGUIHelper {
   constructor(fog) {
     this.fog = fog;
@@ -135,7 +134,7 @@ class FogGUIHelper {
 }
 ```
 
-We can then add it like this
+방금 만든 클래스를 아래처럼 활용합니다.
 
 ```js
 {
@@ -151,33 +150,33 @@ We can then add it like this
 }
 ```
 
-The `near` and `far` parameters set the minimum and maximum values
-for adjusting the fog. They are set when we setup the camera.
+`near`와 `far` 인자는 각 안개 속성의 최솟값과 최댓값입니다.
 
-The `.listen()` at the end of the last 2 lines tells dat.GUI to *listen*
-for changes. That way when we change `near` because of an edit to `far`
-or we change `far` in response to an edit to `near` dat.GUI will update
-the other property's UI for us.
+마지막 2줄의 `.listen()` 메서드를 호출하면 dat.GUI가 변화를 *감지*합니다.
+`near` 속성을 바꿀 때 동시에 `far` 속성을 재할당하고, `far` 속성을 바꿀 때도
+동시에 `near`를 재할당하는데, 이 메서드를 호출하면 조작한 속성 외의 다른
+속성의 변화도 UI에 업데이트됩니다.
 
-It might also be nice to be able to change the fog color but like was
-mentioned above we need to keep both the fog color and the background
-color in sync. So, let's add another *virtual* property to our helper
-that will set both colors when dat.GUI manipulates it.
+여기에 안개의 색까지 조정할 수 있으면 금상첨화겠네요. 하지만 아까 설명했듯
+안개의 색을 바꾸려면 배경색도 같이 바꿔야 합니다. 헬퍼 클래스에 *가상* 속성을
+하나 만들어 dat.GUI가 이 속성을 변경할 때 배경색과 안개색을 같은 값으로
+바꿔주면 어떨까요?
 
-dat.GUI can manipulate colors in 4 ways, as a CSS 6 digit hex string (eg: `#112233`). As an hue, saturation, value, object (eg: `{h: 60, s: 1, v: }`).
-As an RGB array (eg: `[255, 128, 64]`). Or, as an RGBA array (eg: `[127, 200, 75, 0.3]`).
+dat.GUI의 색상 타입은 4가지입니다. 하나는 CSS의 6자리 16진수 문자열(hex string, 예: `#f8f8f8`)이고,
+하나는 hue(색상), saturation(채도), value 객체(예: `{ h: 60, s: 1, v: 0 }`),
+하나는 RGB 배열(예: `[ 255, 128, 64 ]`) 또는 RGBA 색상 배열(예: `[ 127, 200, 75, 0.3 ]`)이죠.
 
-It's easiest for our purpose to use the hex string version since that way
-dat.GUI is only manipulating a single value. Fortunately `THREE.Color`
-as a [`getHexString`](Color.getHexString) method
-we get use to easily get such a string, we just have to prepend a '#' to the front.
+`dat.GUI`가 하나의 값만 조작하도록 하는 게 제일 간단하니, 16진수 문자열을 사용하겠습니다.
+다행히 `THREE.Color`에는 [`getHexString`](Color.getHexString) 메서드가 있어 색상을
+문자열로 쉽게 바꿀 수 있죠. 앞에 '#'만 덧붙이면 됩니다.
 
 ```js
-// We use this class to pass to dat.gui
-// so when it manipulates near or far
-// near is never > far and far is never < near
-+// Also when dat.gui manipulates color we'll
-+// update both the fog and background colors.
+/**
+ * 이 클래스의 인스턴스를 dat.GUI에 넘겨
+ * near나 far 속성을 조정할 때 항상
+ * near는 never >= far, far는 never <= near가 되도록 합니다
+ **/
++// 또 dat.GUI가 color 속성을 조작할 때 안개와 배경색을 동시에 변경합니다
 class FogGUIHelper {
 *  constructor(fog, backgroundColor) {
     this.fog = fog;
@@ -207,7 +206,8 @@ class FogGUIHelper {
 }
 ```
 
-We then call `gui.addColor` to add a color UI for our helper's virtual property.
+이번에는 `gui.addColor` 메서드를 호출합니다. 색상 UI를 생성하는 메서드로,
+방금 추가한 가상 속성을 조작하도록 설정합니다.
 
 ```js
 {
@@ -226,45 +226,39 @@ We then call `gui.addColor` to add a color UI for our helper's virtual property.
 
 {{{example url="../threejs-fog-gui.html" }}}
 
-You can see setting `near` to like 1.9 and `far` to 2.0 gives
-a very sharp transition between un-fogged and completely fogged.
-where as `near` = 1.1 and `far` = 2.9 should just about be
-the smoothest given our cubes are spinning 2 units away from the camera.
-
-One last thing, there is a boolean [`fog`](Material.fog)
-property on a material for whether or not objects rendered
-with that material are affected by fog. It defaults to `true`
-for most materials. As an example of why you might want
-to turn the fog off, imagine you're making a 3D vehicle
-simulator with a view from the driver's seat or cockpit.
-You probably want the fog off for everything inside the vehicle when
-viewing from inside the vehicle.
-
-A better example might be a house
-and thick fog outside house. Let's say the fog is set to start
-2 meters away (near = 2) and completely fogged out at 4 meters (far = 4).
-Rooms are longer than 2 meters and the house is probably longer
-than 4 meters so you need to set the materials for the inside
-of the house to not apply fog otherwise when standing inside the
-house looking outside the wall at the far end of the room will look
-like it's in the fog.
+`near`를 1.9 정도, `far`를 2.0 정도로 설정하면 안개의 경계가 굉장히
+선명해질 겁니다. 정육면체들이 카메라에서 2칸 떨어져 있으므로 `near`를
+1.1, `far`를 2.9 정도로 설정하면 경계가 가장 부드러운 것이라고 할 수
+있죠.
+
+추가로 재질(material)에는 불린 타입의 [`fog`](Material.fog) 속성이 있습니다.
+해당 재질로 렌더링되는 물체가 안개의 영향을 받을지의 여부를 결정하는 속성이죠.
+"안개 효과를 없애버리면 그만 아닌가?" 생각할 수 있지만, 3D 운전 시뮬레이터를
+만드는 경우를 상상해봅시다. 차 밖은 안개가 자욱하더라도 차 안에서 볼 때 차 내부는
+깔끔해야 할 수도 있죠.
+
+안개가 짙은 날, 집 안에서 창 밖을 바라보는 장면이 더 와닿을지도 모르겠네요.
+안개가 카메라로부터 2미터 이후부터 끼기 시작하고(near = 2), 4미터 이후에는
+완전히 안개에 덮히도록(far = 4) 설정합니다. 방은 2미터이고, 집은 최소 4미터입니다.
+여기서 집 안의 재질이 안개의 영향을 받도록 놔둔다면 방 끝에서 창 밖을 바라볼
+때 방 안도 안개가 낀 것처럼 보이겠죠.
 
 <div class="spread">
   <div>
     <div data-diagram="fogHouseAll" style="height: 300px;" class="border"></div>
-    <div class="code">fog: true, all</div>
+    <div class="code">모든 재질의 fog: true</div>
   </div>
 </div>
 
-Notice the walls and ceiling at the far end of the room are getting fog applied.
-By turning fog off on the materials for the house we can fix that issue.
+방 끝 쪽 천장과 벽에 안개가 낀 것이 보일 겁니다. 집 내부 재질의 fog 옵션을 끄면
+안개를 없앨 수 있죠.
 
 <div class="spread">
   <div>
     <div data-diagram="fogHouseInsideNoFog" style="height: 300px;" class="border"></div>
-    <div class="code">fog: true, only outside materials</div>
+    <div class="code">집 밖 물체의 재질만 fog: true</div>
   </div>
 </div>
 
 <canvas id="c"></canvas>
-<script type="module" src="resources/threejs-fog.js"></script>
+<script type="module" src="../resources/threejs-fog.js"></script>

+ 159 - 0
threejs/lessons/kr/threejs-rendertargets.md

@@ -0,0 +1,159 @@
+Title: Three.js 렌더 타겟
+Description: Three.js에서 장면을 텍스처로 만드는 방법을 알아봅니다
+TOC: 렌더 타겟(Render Targets)
+
+Three.js의 렌더 타겟이란, 직접 렌더링할 수 있는 텍스처(texture)를 말합니다.
+한 번 텍스처로 렌더링한 뒤에는 다른 텍스처처럼 사용할 수 있죠.
+
+간단한 예제를 만들어보겠습니다. [반응형 디자인에 관한 글](threejs-responsive.html)에서
+썼던 예제를 가져오도록 하죠.
+
+렌더 타겟을 만드는 방법은 기존 렌더링 방법과 유사합니다. 먼저 `WebGLRenderTarget` 인스턴스를
+생성합니다.
+
+```js
+const rtWidth = 512;
+const rtHeight = 512;
+const renderTarget = new THREE.WebGLRenderTarget(rtWidth, rtHeight);
+```
+
+그리고 `Camera`와 `Scene`(장면)을 추가합니다.
+
+```js
+const rtFov = 75;
+const rtAspect = rtWidth / rtHeight;
+const rtNear = 0.1;
+const rtFar = 5;
+const rtCamera = new THREE.PerspectiveCamera(rtFov, rtAspect, rtNear, rtFar);
+rtCamera.position.z = 2;
+
+const rtScene = new THREE.Scene();
+rtScene.background = new THREE.Color('red');
+```
+
+위 예제에서는 렌더 타겟의 가로세로비(aspect, 종횡비)를 canvas가 아닌 렌더 타겟
+자체의 사이즈로 구했습니다. 렌더 타켓의 가로세로비는 텍스처를 사용할 물체에 맞춰
+정해야 하기 때문이죠. 예제의 경우 렌더 타겟을 정육면체의 텍스처로 사용할 것이고,
+정육면체의 모든 면은 정사각형이므로 가로세로비는 1.0입니다.
+
+그리고 [이전 글](threejs-responsive.html)에서 썼던 조명과 정육면체 3개를 추가하겠습니다.
+
+```js
+{
+  const color = 0xFFFFFF;
+  const intensity = 1;
+  const light = new THREE.DirectionalLight(color, intensity);
+  light.position.set(-1, 2, 4);
+*  rtScene.add(light);
+}
+
+const boxWidth = 1;
+const boxHeight = 1;
+const boxDepth = 1;
+const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
+
+function makeInstance(geometry, color, x) {
+  const material = new THREE.MeshPhongMaterial({color});
+
+  const cube = new THREE.Mesh(geometry, material);
+*  rtScene.add(cube);
+
+  cube.position.x = x;
+
+  return cube;
+}
+
+*const rtCubes = [
+  makeInstance(geometry, 0x44aa88,  0),
+  makeInstance(geometry, 0x8844aa, -2),
+  makeInstance(geometry, 0xaa8844,  2),
+];
+```
+
+이전 글의 `Scene`과 `Camera`는 그대로 둡니다. 이 둘은 canvas를 렌더링하는
+데 사용할 거예요.
+
+먼저 렌더 타겟의 텍스처를 사용하는 정육면체를 추가합니다.
+
+```js
+const material = new THREE.MeshPhongMaterial({
+  map: renderTarget.texture,
+});
+const cube = new THREE.Mesh(geometry, material);
+scene.add(cube);
+```
+
+그리고 `render` 함수 안에서 렌더 타겟의 장면을 먼저 렌더링한 뒤,
+
+```js
+function render(time) {
+  time *= 0.001;
+
+  ...
+
+  // 렌더 타겟의 장면 안에서 정육면체를 각각 회전시킵니다
+  rtCubes.forEach((cube, ndx) => {
+    const speed = 1 + ndx * .1;
+    const rot = time * speed;
+    cube.rotation.x = rot;
+    cube.rotation.y = rot;
+  });
+
+  // 렌더 타겟의 장면을 렌더 타겟에 렌더링합니다
+  renderer.setRenderTarget(renderTarget);
+  renderer.render(rtScene, rtCamera);
+  renderer.setRenderTarget(null);
+```
+
+canvas에 렌더 타겟의 텍스처를 사용하는 정육면체를 렌더링합니다.
+
+```js
+  // 장면 중앙의 정육면체를 회전시킵니다
+  cube.rotation.x = time;
+  cube.rotation.y = time * 1.1;
+
+  // 장면은 canvas에 렌더링합니다
+  renderer.render(scene, camera);
+```
+
+붐!
+
+{{{example url="../threejs-render-target.html" }}}
+
+정육면체가 빨간 건 정육면체를 잘 보이도록 하기 위해 `rtScene`의 `background`
+속성을 빨강으로 설정했기 때문입니다.
+
+렌더 타겟의 용도는 무궁무진합니다. [그림자](threejs-shadows.html)가 렌더 타겟을
+사용하고, [피킹(picking)도 렌더 타겟을 사용할 수 있죠](threejs-picking.html).
+많은 [후처리 효과](threejs-post-processing.html)를 사용할 때 렌더 타겟이 필수
+요소인 경우도 있고, 차의 후사경(rear view mirror, 백미러)이나 모니터 화면 등에도
+렌더 타겟을 활용할 수 있습니다.
+
+이번 글은 여기까지입니다. 마지막으로 `WebGLRenderTarget`을 사용할 때의 주의해야
+할 점 몇 가지만 살펴보고 끝내도록 하죠.
+
+* 기본적으로 `WebGLRenderTarget`은 2개의 텍스처를 생성합니다. 하나는 색상 텍스처이고, 다른 하나는 깊이/스텐실(depth/stencil) 텍스처이죠. 깊이 텍스처나 스텐실 텍스처를 사용하지 않을 거라면 인스턴스 생성 시 옵션을 지정해 텍스처를 아예 생성하지 않도록 할 수 있습니다.
+
+    ```js
+    const rt = new THREE.WebGLRenderTarget(width, height, {
+      depthBuffer: false,
+      stencilBuffer: false,
+    });
+    ```
+
+* 렌더 타겟의 크기를 바꿔야 한다면
+
+  앞선 예제에서는 렌더 타겟을 생성할 때 고정 사이즈, 512x512를 사용했습니다. 하지만 후처리 등에서 렌더 타겟을 사용할 경우, canvas 크기와 렌더 타겟의 크기를 똑같이 설정하는 것이 일반적입니다. 예제를 바탕으로 이를 구현하려면 canvas의 사이즈가 변경되었을 때 카메라와 렌더 타겟의 사이즈를 변경해주어야 하죠.
+
+      function render(time) {
+        time *= 0.001;
+
+        if (resizeRendererToDisplaySize(renderer)) {
+          const canvas = renderer.domElement;
+          camera.aspect = canvas.clientWidth / canvas.clientHeight;
+          camera.updateProjectionMatrix();
+
+      +    renderTarget.setSize(canvas.width, canvas.height);
+      +    rtCamera.aspect = camera.aspect;
+      +    rtCamera.updateProjectionMatrix();
+      }