three.js Custom Geometry 2/3

Kleinstein·2022년 12월 19일
0

Three.js 관련

목록 보기
4/5

앞서 말한대로, 이 문서는 번역/의역 문서이기 때문에 해당 코드에 대한 예제를 굳이 이 글에 넣지 않았다. 사실 넣어볼까 하다가 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();
profile
developer in germany

0개의 댓글