멀티 카메라의 렌더링 결과를 하나의 Scene에 표시 (in Three.js)

여러 개의 카메라의 렌더링 결과를 하나의 장면에 표시해야할 필요가 생긴다. 예를들어 스타크래프트 게임을 보면 화면에 2개의 요소로 구분되어 있다. 첫째는 미니맵과 명령 아이콘들이 표시되는 커멘트 요소와 두번째는 실제 게임 플레이 요소이다. 이를 구현하는 방식은 여러가지가 있겠으나 멀티 카메라를 이용하는 방식이 있다. 게임 플레이 요소는 원근감 있게 PerspectiveCamera를 이용하고 커멘드 요소는 OrthographicCamera로 원근감 없이 렌더링하는 것이다. 이렇게 각 카메라로 렌더링되는 요소를 하나의 장면에 중첩해서 표시하면 된다.

마우스에 대한 상호작용은 Camera를 입력값을 개별 요소로 받으니 어떤 객체든 마우스 상호작용을 쉽게 얻을 수 있다. 이미 알고 있겠지만 Raycast를 이용해서 말이다. 여러개의 카메라를 사용할때 생각할 것은 어떤 요소를 어떤 카메라에 할당해 렌더링할 것인지에 대한 지정이다. 물론 하나의 요소를 여러개의 카메라에서 동시에 렌더링하는 것도 가능하다. 이러한 구분은 Layers라는 기능을 이용하면 매우 직관적으로 처리할 수 있다. three.js에서 Layers는 총 32개로 구성된다. 기본적으로 모든 카메라, 광원, 매시 등은 0번째 레이어에 소속된다. 어떤 카메라가 소속된 레이어와 동일한 레이어에 소속된 것들은 모두 카메라를 통해 렌더링된다.

이를 코드로 작성해 보자. 먼저 카메라 2개를 만들자. 쉽게 설명하기 위해 둘다 PerpectiveCamera 객체다.

_setupCamera() {
  ...
  
  const aspect = width / height;

  const camera1 = new THREE.PerspectiveCamera(60, aspect, 0.1, 10);
  camera1.layers.enable(1);
  camera1.position.z = 3;
  this._camera1 = camera1;

  const camera2 = new THREE.PerspectiveCamera(60, aspect, 0.1, 10);
  camera2.layers.enable(2);
  camera2.position.z = 2;
  this._camera2 = camera2;
}

camera1과 camera2는 각각 0,1 레이어와 0,2 레이어에 소속되어 있다. 만약 camera1을 1번 레이어에만 소속시키려면 camera1.layers.set(1) 코드면 된다.

다음은 렌더링할 매시를 다음처럼 구성한다.

const geometryC1 = new THREE.BoxGeometry();
const materialC1 = new THREE.MeshStandardMaterial();
const meshC1 = new THREE.Mesh(geometryC1, materialC1);
meshC1.layers.set(1);
this._scene.add(meshC1);

const geometryC2 = new THREE.BoxGeometry();
const materialC2 = new THREE.MeshStandardMaterial({ wireframe: true });
const meshC2 = new THREE.Mesh(geometryC2, materialC2)
meshC2.layers.set(2);
this._scene.add(meshC2);

meshC1은 1 레이어에만 소속되어 있다. meshC2는 2 레이어에만 소속되어 있다. 만약 layers의 set 대신 enable를 사용하면 기본적으로 소속된 0번 레이어에 대한 소속은 그대로 유지되므로 명확히 1 번 레이어에만 소속되도록 set를 사용했다. 광원에 대한 layers 설정은 하지 않았으므로 0 레이어에 소속되어 있다. camera1과 camera2의 layers를 enable로 해서 설정했으므로 각 카메라는 기본적으로 소속된 0번 레이어에도 소속되어 있고 0 레이어에 소속된 광원의 영향을 2개 카메라 모두 사용하게 된다.

이제 렌더링과 관련된 코드를 작성해야 한다. 그전에 Renderer 객체에 대해 다음 코드가 필요하다.

renderer.autoClearColor = false;

기본적으로 장면이 렌더링 되기 직전에 자동으로 정해진 색상으로 프레임버퍼가 설정된다. 위의 코드는 이처럼 자동으로 이뤄지는 것을 방지하고자 함이다. 자, 이제 렌더링 코드를 보자.

render() {
  this.update();

  this._renderer.clearColor();
  this._renderer.render(this._scene, this._camera2);
  this._renderer.render(this._scene, this._camera1);

  requestAnimationFrame(this.render.bind(this));
}

Renderer의 autoClearColor를 false로 지정했으므로 이제 직접 프레임버퍼를 지우기 위해 Renderer의 clearColor 매서드를 호출해야 한다. 그런 후에 각 카메라를 이용해 장면을 렌더링한다. 끝이다.

굳히 언급하지 않아도 이미 알겠지만 창 크기가 변경되면 카메라의 기저 인자값들을 재설정해줘야 한다. 카메라가 여러개이므로 이에 대한 코드까지 살펴보고 마무리 한다.

resize() {
  ...

  const aspect = width / height;

  this._camera1.aspect = aspect;
  this._camera1.updateProjectionMatrix();

  this._camera2.aspect = aspect;
  this._camera2.updateProjectionMatrix();

  ...
}

voro-noise

노이즈는 렌덤을 기반으로 하며 FBM(Fractal Brownian Motion)을 위한 한 옥타브를 구성합니다. 몇가지 노이즈 중 하나로 보로노이(Voronoi)를 기반으로 하는 voro-noise가 있는데, Shader의 대가인 Inigo님이 2014년에 만든 알고리즘입니다. 내부 코드를 보면 퍼퍼먼스에 다소 부정적인 부분(일반적인 9개의 격자 그리드가 아닌 25개의 격자 그리드를 사용)이 보이지만 그 결과는 여타 다른 노이즈보다 훨씬 뛰어납니다.

아래의 코드는 voro-noise에 대한 구현 코드입니다.

vec3 hash3(vec2 p) {
    vec3 q = vec3(
        dot(p, vec2(127.1, 311.7)), 
        dot(p, vec2(269.5, 183.3)), 
        dot(p, vec2(419.2, 371.9))
    );
    return fract(sin(q) * 43758.5453);
}

float voronoise(in vec2 p, float u, float v) {
    float k = 1.0 + 63.0 * pow(1.0 - v, 6.0);

    vec2 i = floor(p);
    vec2 f = fract(p);

    vec2 a = vec2(0.0, 0.0);
    for(int y = -2; y <= 2; y++) {
        for(int x = -2; x <= 2; x++) {
            vec2 g = vec2(x, y);
            vec3 o = hash3(i + g) * vec3(u, u, 1.0);
            vec2 d = g - f + o.xy;
            float w = pow(1.0 - smoothstep(0.0, 1.414, length(d)), k);
            a += vec2(o.z * w, w);
        }
    }

    return a.x / a.y;
}

이 함수에 대한 가장 흔한 코드 예시는 다음과 같습니다.

void main() {
    vec2 st = gl_FragCoord.xy / uResolution.xy;
    st.x *= uResolution.x / uResolution.y;
    st *= 10.0;

    float f = voronoise(st, 1., 1.);
    gl_FragColor = vec4(f, f, f, 1.0);
}

위의 코드의 결과는 다음과 같구요.

voronoise 함수는 3개의 인자를 받는데, 첫번째 인자는 일반적으로 노이즈 함수가 받는 인자입니다. 2,3번째 인자인 u와 v는 voronoise에 특화된 인자인데 u는 노이즈 생성을 격자 그리드(Grid)를 얼마나 보로노이스럽게 표현할지에 대한 강도값으로 0에서 1까지의 값을 갖습니다. v는 격자 그리드 내부를 채우는 각 프레그먼트에 대한 보간 정도에 대한 강도 값인데 0부터 1의 값이며 0일때 전혀 보간이 이루어지지 않고 1일때 최대의 보간이 이뤄집니다. 아래의 결과는 u와 v를 모두 0으로 했을 때의 결과입니다.

아래는 u를 1로 v는 0으로 했을때의 결과입니다.

마지막으로 아래는 u를 0으로 v를 1로 했을때의 결과입니다.

아래는 노이즈 구현에 대한 유용한 사이트입니다.

https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83

경로 Mesh를 따라 흐르는 화살표 시각화 (three.js)

공장 생산 설비 시각화나 도로 진행 방향 시각화 등에서 자주 사용되는 기능 중 하나입니다. 아래처럼 간단한 경로를 Mesh로 구성하고 화살표 이미지를 맵핑한 후 원하는 방향으로 화살표가 흘러가도록 합니다.

three.js, 뒤에서 보여지는 물체 감추기

실내와 같은 장면을 3D로 살펴볼때 벽처럼 막힌 부분이 장면의 시인성을 방해하는 경우가 있습니다. 아래가 그러한 경우입니다.

위의 영상에서 보이는 것처럼 실내의 물체를 벽이 가리는 문제가 있습니다. 이런 문제점을 해결하기 위해 뒤에서 보여지는 물체(벽 등)를 잠시 보이지 않도록 해주는 기능이 필요한데요. 아래는 그에 대한 결과입니다.

이에 대한 기능을 컴포넌트로 만든 것이 BackViewHiderControls 입니다. 관련 API는 다음과 같습니다.

먼저 컨트롤러 객체를 생성합니다.

_setupControls() {
  ...

  const backViewHider = new BackViewHiderControls(this._camera);
  this._backViewHider = backViewHider;

  ...
}

그리고 매 프레임마다 호출되는 update 매서드에서 다음 코드를 입력합니다.

update() {
  ....

  this._backViewHider.updateOnFrame();
}

three.js, 선택된 모델에 대한 줌(Zoom)

3D 그래픽 기능 개발에 있어서 선택된 모델을 화면에서 확대하는 기능입니다.

ZoomControls 라는 클래스로 컴포넌트화 해서 재사용성을 높였습니다. 이 기능은 제 유튜브 채널의 강좌에서 설명하고 있는 코드를 거의 그대로 사용하고 있습니다.

ZoomControls에 대한 API 사용은 다음과 같습니다.

먼저 ZoomControls 객체를 생성합니다.

_setupControls() {
  this._orbitControls = new OrbitControls(this._camera, this._divContainer);
 
  ...

  const zoomControls = new ZoomControls(this._orbitControls);
  this._zoomControls = zoomControls;
}

그리고 확대하고자 하는 Object3D에 대해 다음처럼 코드를 수행해주면 됩니다.

zoom(zoomTarget, view) {
  if (view === "좌측 뷰") this._zoomControls.zoomLeft(zoomTarget);
  else if (view === "우측 뷰") this._zoomControls.zoomRight(zoomTarget);
  else if (view === "정면 뷰") this._zoomControls.zoomFront(zoomTarget);
  else if (view === "후면 뷰") this._zoomControls.zoomBack(zoomTarget);
  else if (view === "상단 뷰") this._zoomControls.zoomTop(zoomTarget);
  else if (view === "하단 뷰") this._zoomControls.zoomBottom(zoomTarget);
  else if (view === "뷰 유지") this._zoomControls.zoom(zoomTarget);
}

선택된 모델에 대한 아웃라인 표시는 SelectionPassWrapper API를 사용하였고 이와 관련된 내용은 다음과 같습니다.

three.js, 선택된 3D 모델에 대한 하이라이팅