three.js에서 HDR 데이터를 이용한 배경 및 광원으로 사용하기

HDR은 High Dynamic Range의 약자로 밝은 부분과 어두운 부분의 차이를 효과적으로 보정하여 전반적으로 균형있게 출력할 수 있는 기술이라고 합니다. 다르게 설명하면 보다 더 사실적인 명암(밝기와 어두움)를 표현하기 위해 사용되는 기술이라고도 합니다. 아래의 연속된 3개의 이미지 중 가장 오른쪽이 HDR 기술이 적용된 이미지입니다. 왼쪽 이미지는땅 부분이 소실되었고 가운데 이미지는 하늘 부분이 소실되었습니다. 오른쪽 이미지는 HDR 기술을 적용해 만든 것으로 전반적으로 군형있게 표현되고 있습니다.

원래 일반적인 이미지 데이터는 가장 어두운 색의 값을 0, 가장 밝은 색의 값을 1이라는 범위로 정하고 모든 색상을 0~1사이로 맞춥니다. 하지만 HDR이 적용되어 만들어진 이미지 데이터는 0~1 사이의 한정된 범위가 아닌 훨씬 더 넓은 범위를 갖습니다. 즉 사용하는 데이터 크기가 더 큽니다. HDR 데이터의 크기만 봐도 수십에서 수백 MB가 되는 것을 보면 쉽게 이해할 수 있습니다.

three.js에서도 HDR 데이터를 사용할 수 있는데, 그 용도는 크게 두 가지입니다. 첫번째는 3차원 배경으로써의 사용과 두번째는 HDR 데이터 자체를 광원으로써 사용하는 것입니다. HDR 데이터는 인터넷에서 검색을 통해 쉽게 다운로드 받을 수 있습니다. 저 같은 경우 polyhaven이라는 사이트에서 데이터를 받았습니다.

three.js에서 HDR 데이터를 사용하는 주요 코드만을 정리하면 다음과 같습니다. 먼저 필요한 모듈과 HDR 데이터를 로딩하는 함수는 다음과 같습니다.

import { RGBELoader } from "../examples/jsm/loaders/RGBELoader.js"

class App {
    ...

    _setupBackground() {
        new RGBELoader()
            .load("./data/brown_photostudio_03_8k.hdr", (texture) => {
                texture.mapping = THREE.EquirectangularReflectionMapping;
                this._scene.background = texture; // 3차원 배경으로 사용
                this._scene.environment = texture; // 광원으로 사용

                //texture.dispose();
            }
        );
    }

    _setupModel() {
        const geometry = new THREE.TorusKnotGeometry(1, 0.3, 256, 64, 2, 3);
        // HDR을 광원으로 사용하기 위해서는 재질을 MeshStandardMaterial 또는 파생 객체를 사용해야 가능함
        const material = new THREE.MeshStandardMaterial({color: 0xffffff}); 

        const cube = new THREE.Mesh(geometry, material);
        this._scene.add(cube);
    }

    ...
}

위 코드에 대한 결과는 다음과 같습니다.

위의 이미지는 three.js에 어떠한 광원(Light) 객체도 장면에 추가하지 않은 결과입니다. 보시면 TorusKnot 모델이 너무 밝게 표현되어 백색으로 뭉게져 보입니다. 이는 톤맵핑 설정이 바르지 않기 때문입니다. 톤맵핑(Tone Mapping)은 HDR 영역의 색상 표현을 모니터의 색상 표현(SDR)으로 맵핑시켜준다는 개념입니다. 이를 위해 다음 코드가 필요합니다.

renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;

결과는 다음과 같습니다.

toneMappingExposure 값을 적절하게 조정하면 밝기값이 조정됩니다.

HDR은 3차원 배경으로써 사용될 수도 있지만 광원에 대한 매우 효과적인 해결책입니다. 3차원 배경으로는 사용하지 않고 광원으로써 HDR를 사용하고자 한다면 this._scene.background = texture 코드를 제거하기만 하면 됩니다.

Node.js에서 독립적인 프로세스를 통한 연산 및 그 결과 얻기

독립적인 프로세스를 통해 얻는 장점은 여러 개의 CPU를 활용하여 보다 더 빠른 결과를 얻을 수 있다는 점인데, 단점은 CPU 간의 데이터 공유가 참조(Reference)가 아닌 복사(Copy)이므로 메모리 사용량이 2배가 된다는 점이다. 하지만 이러한 단점은 특정한 상황에서는 아무런 문제가 되지 않는 경우가 있으므로 Node.js에서 독립적인 프로세스를 이용한 연산은 매우 유용한 경우가 많다.

먼저 독립적인 프로세스에서 수행할 코드는 worker.js 파일에 다음처럼 작성한다.

const process = require("process");

function getSum(arr) {
    let sum = 0;
    arr.forEach(i => {  sum += i; });
    return sum;
}

process.on("message", (message) => {
    if(message.cmd === "sum") {
        process.send({ result: getSum(message.data) });
    }
});

getSum이라는 함수를 처리하며 다른 프로세스로부터 전달 받은 데이터(배열 형태) 안의 숫자 값들의 합계를 전달해 준다.

이 worker.js를 사용하는 소스는 다음과 같다.

const cp = require("child_process");
const worker = cp.fork("./worker.js");

function task(arr) {
    return new Promise((resolve, reject) => {
        function onMessage(message) {
            resolve(message.result);
            worker.removeListener("message", onMessage);
        }

        worker.addListener("message", onMessage);

        worker.send({ 
            cmd: "sum",
            data: arr
        });
    })
}

task([1,2,3,4,5,6,7,8,9,10])
    .then((result) => {
        console.log(result);
        worker.kill();
    })
    .catch(err => {
        console.log(err);
    });

4번 코드에서 독립적인 프로세스에서 수행된 결과를 얻기 위해 Promise API를 사용했다는 점. 20번 코드에서 Prmoise의 일반적인 처리를 위해 then, catch를 사용하여 정상적인 경우와 예외처리를 하고 있다는 점이 코드의 완결성을 느끼게 해준다.

Node.js에서 fork를 사용해 독립적인 프로세스를 통한 연산

이전에 아래의 포스트를 통해 Node.js에서 스레드를 사용해 연산을 처리한 글을 올렸습니다.

Node.js에서 Thread 사용

이번에는 별도의 독립적인 프로세스를 통한 연산 수행해 그 결과를 얻는 코드에 대해 정리합니다. 위의 글과 동일한 기능을 구현하며 Thread 대신 Process를 이용한다는 차이점만 있습니다./p>

서버에 대한 코드는 다음과 같습니다.

const express = require("express");
const app = express();

const { fork } = require("child_process");

app.get("/", (req, res) => {
    const child = fork("./task.js");
    
    child.send("start");

    child.on("message", (sum) => {
        res.send({sum: sum});
    })
});

app.listen(3000);

외부 프로세스에서 수행할 연산에 대한 코드는 task.js 파일에 정의되는데.. 아래와 같습니다.

process.on("message", (message) => {
    if(message === "start") {
        let total = 0;

        for(let i=0; i<10000000000; i++) {
            total++;
        }
        
        process.send(total);
    }
})