멀티 카메라의 렌더링 결과를 하나의 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로 했을때의 결과입니다.