앞서 말한대로, 이 문서는 번역/의역 문서이기 때문에 해당 코드에 대한 예제를 굳이 이 글에 넣지 않았다. 사실 넣어볼까 하다가 three.js 로 만든 3D 모델들이 보이는 화면을 어떻게 네이버 블로그에 삽입할수 있는지 방법을 못찾아서 금방 포기했다. 예제에 나온 이 3D 모델들이 어떻게 보이는지에 대해서는 원문이 있는 사이트를 방문해서 확인하기 바란다.
const material = new THREE.MeshBasicMatreial({vertexColors: THREE.FaceColors})
위의 명령은 Material 을 만들 때 THREE.FaceColors를 사용하고 싶다는 의미이고, 다시 말해 이미 Face에 지정되어 있는 색을 사용하는 Material을 만들고 싶다는 뜻이다. 좀 더 구체적으로는, 이미 Face에 지정된 색을 Face를 구성하는 Vertex에 지정하여 Material 을 만들어 달라는 의미이다.
그런데 삼각형의 면을 구성하는 세 점에 위와 같이 Face의 색깔을 세 점 모두에 똑같이 지정하는 것이 아니라, 각 점마다 다른 색을 지정할 수도 있다. vertexColors 속성에 세 점에 지정할 색을 각각 넣는 아래의 코드를 보자.
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),
];
});
geometry의 모든 face마다 괄호안에 든 함수를 실행하는 명령인데, 이 함수는 해당 face 와 ndx(index의 줄임말)를 파라미터로 받고 해당 face의 vertexColors 속성에 세 가지 색을 지닌 Array를 넣어준다. 이때 각 점에 들어갈 이 세 점의 색을 위와 같이 HSL 형식으로 만들되, HSL 각각의 색상(Hue), 채도(Saturation), 명도(Lightness)의 값을 자동으로 계산해서 넣어주게끔 한 것이다.
참고로 HSL 형식에서 색상은 0-360 사이의 값으로 색상환의 각도를 나타내고, 색상 값이 0 또는 360이면 빨간색, 120이면 녹색, 240이면 파란색이 된다. 일단 이 코드에서 채도와 명도는 모두 동일하게 하였다.
다만 three.js에서는 0-360도 사이의 값이 아닌 0 에서 1 사이의 값을 setHSL이라는 함수인에 파라미터로 넣어줘야 하므로 2번째 파라미터로 들어오는 0 에서 11 사이의 index(ndx) 값들을 이용해서 모두 다른 색을 만들려고 한다.
즉 0부터 11까지 총 12개의 face index 값을 12로 나눈 다음 여기에 0.1씩 더함으로써 조금씩 다른 색을 자동으로 만들고, 이렇게 만든 색을 넣어주게 한 것이다.
조명 (lighting) 을 사용하기 위해서는 각 face마다 normal 이 지정되어 있거나 각 face의 점 (Vertex)들마다 normal 이 지정되어 있어야 한다. 여기서 normal 이란 방향을 말해주는 벡터Vector를 말한다. 즉, face에 normal 을 지정하려면 face의 normal이라는 속성에 Vector3 클래스를 사용해서 face의 x축, y축, z축 방향을 아래처럼 지정해 준다.
face.normal = new THREE.Vector3(x, y, z);
그리고 각각의 face를 이루는 Vertex마다 normal을 지정하기 위해서는 아래와 같이 face의 속성 vertexNormals에 Vector3 클래스를 이용해서 normal을 지정할 수도 있다.
face.vertexNormals = [
new THREE.Vertex3(x1, y1, z1),
new THREE.Vertex3(x2, y2, z2),
new THREE.Vertex3(x3, y3, z3),
]
하지만 이렇게 직접 넣는 방법 말고 three.js로 하여금 주어진 위치값을 기반으로 직접 normals를 계산하도록 하는 것도 쉬운 방법 중 하나이다.
face normal 을 계산 시키려면 아래와 같이 Geometry.computeFaceNormals 함수를 호출하면 된다.
geometry.computeFaceNormals();
이렇게 하면, 이제 normal 이 존재하므로 우리는 light를 사용할 수 있다.
light를 사용할 수 없어서 아예 light 가 필요 없는 Material 을 만들었던 아래의 코드 대신
const material = new THREE.MeshBasicMaterial({vertexColors: THREE.VertexColors});
light를 사용해 표현할 수 있는 Material 을 생성해 주는 다음의 코드를 사용해 보자.
const material = new THREE.MeshPhongMaterial({color});
face normal 을 사용하게 되면 faceted look (모델을 이루는 각각의 삼각형 면이 잘 드러나 보이는) 을 얻게 된다. 좀 더 부드럽게 보이게 하려면 vertex normal 을 사용하면 되고, vertex normal 도 각 점으로 주어진 위치 점들의 위치좌표를 기반으로 three.js 가 알아서 계산해 주는 함수가 있다. 아래와 같이 호출해 주면 된다.
geometry.computeVertexNormals();
불행하게도 우리가 지금까지 예로 들어왔던 큐브는 자동으로 vertex normal 을 계산하기에 좋은 geometry는 아니다. 왜냐하면 vertex normal 은 각 vertex를 공유하는 모든 face 들의 normal 들을 참조해서 계산하는데, 큐브의 경우 각 점을 공유하는 면들이 90도씩 극단적으로 꺾여있어서 이걸 통해 계산한 vertex normal 은 큐브 모퉁이의 모양을 오히려 조금 애매모호하게 보이도록 만들어 버리기 때문이다.
텍스처를 입히려면 UV라고 불리는 텍스처 좌표를 추가해줘야 하는데,
Geometry.faceVertexUvs 라는 프로퍼티를 통해 이 정보가 입력되고,
내부적으로는 텍스처가 입혀질 face와 평행한 face 배열 레이어를 통해 수행된다.
우리의 예제인 큐브를 위해 아래와 같은 코드를 보자.
geometry.faceVertexUvs[0].push(
// 3D 큐브 모델을 만들때 총 8개의 점으로 각 면당 2개의 삼각형, 총 12개의 삼각형을 만들었던 그 순서와 똑같이 만든다.
// front :
[ 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) ],
// right
[ 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) ],
// back
[ 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) ],
// left
[ 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) ],
// top
[ 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) ],
// bottom
[ 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) ],
);
3D를 이루는 각 Vertex에 대해 3개의 Vector2 클래스 객체로 이루어진 Array 하나씩이 지정된다. 즉 Vertex 하나당 Array 하나다.
이런 코드는 찬찬히 충분한 시간을 두고 살펴보는 것이 좋다.
다시 설명하자면, 삼각형 하나를 기준으로 각 삼각형의 꼭지점에 UV 좌표점 하나씩을 지정하는 것이다. 그리고 Vertex 점 8개로 정육면체의 6개 face 를 지정할때 각 면마다 2개의 삼각형을 사용해서 만들었는데, 이때의 순서와 동일하게 원하는 UV 좌표값을 넣은것이다.
이제 UV 좌표값이 넣어졌으므로 텍스쳐Texture를 입힐 수 있다.
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, map: texture});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
...
}
이런식이다.
이쯤에서 전체 코드를 한번 살펴보자.
/* global THREE */
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
const fov = 75;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 100;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 5;
const scene = new THREE.Scene();
// light
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);
}
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
);
/*
6----7
/| /|
2----3 |
| | | |
| 4--|-5
|/ |/
0----1
*/
// 이 순서대로 나중에 texture 를 위한 uv 좌표도 넣어줘야 한다.
geometry.faces.push(
// front
new THREE.Face3(0, 3, 2),
new THREE.Face3(0, 1, 3),
// right
new THREE.Face3(1, 7, 3),
new THREE.Face3(1, 5, 7),
// back
new THREE.Face3(5, 6, 7),
new THREE.Face3(5, 4, 6),
// left
new THREE.Face3(4, 2, 6),
new THREE.Face3(4, 0, 2),
// top
new THREE.Face3(2, 7, 6),
new THREE.Face3(2, 3, 7),
// bottom
new THREE.Face3(4, 1, 0),
new THREE.Face3(4, 5, 1),
);
geometry.faceVertexUvs[0].push(
// front
[ 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) ],
// right
[ 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) ],
// back
[ 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) ],
// left
[ 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) ],
// top
[ 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) ],
// bottom
[ 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) ],
);
geometry.computeFaceNormals();
const loader = new THREE.TextureLoader();
const texture = loader.load('https://r105.threejsfundamentals.org/threejs/resources/images/star.png');
// 원문에서는 3개의 큐브를 만들기 위해 이렇게 큐브를 만드는 함수를 따로 만들었다.
function makeInstance(geometry, color, x) {
const material = new THREE.MeshPhongMaterial({color, map: texture});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
cube.position.x = x;
return cube;
}
// 편의상 여기서는 큐브를 하나만 만든다.
const cubes = [
makeInstance(geometry, 0x88FF88, 0),
];
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
cubes.forEach((cube, ndx) => {
const speed = 1 + ndx * .1;
const rot = time * speed;
cube.rotation.x = rot;
cube.rotation.y = rot;
});
renderer.render(scene, camera);
requestAnimationFrame(render);
}// function render(time)
requestAnimationFrame(render);
}
main();