2026.08.09 · 5분 읽기
import * as THREE from "three/webgpu";
export class App {
private renderer!: THREE.WebGPURenderer;
private domApp!: HTMLElement;
private scene!: THREE.Scene;
private camera!: THREE.PerspectiveCamera;
private constructor() {
console.log("Hello, Three.js");
}
static async create(): Promise<App> {
const app = new App();
await app.setupThreeJs();
app.setupCamera();
app.setupLight();
app.setupModel();
app.setupEvent();
return app;
}
private async setupThreeJs() {
this.domApp = document.querySelector("#app") as HTMLElement;
if (!this.domApp) {
throw new Error("Could not find element with id '#app'");
}
const renderer = new THREE.WebGPURenderer({ antialias: true });
this.domApp.appendChild(renderer.domElement);
await renderer.init();
this.renderer = renderer;
this.scene = new THREE.Scene();
}
private setupCamera() {
const width = this.domApp.clientWidth;
const height = this.domApp.clientHeight;
this.camera = new THREE.PerspectiveCamera(60, width / height);
this.camera.position.set(0, 0, 3);
}
private setupLight() {
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(-1, 2, 4);
this.scene.add(light);
}
private setupModel() {
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshStandardMaterial();
const mesh = new THREE.Mesh(geometry, material);
this.scene.add(mesh);
}
private setupEvent() {
window.addEventListener("resize", this.resize.bind(this));
this.resize();
this.renderer.setAnimationLoop(this.render.bind(this));
}
render() {
this.renderer.render(this.scene, this.camera);
}
resize() {
const width = this.domApp.clientWidth;
const height = this.domApp.clientHeight;
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(width, height);
}
}지난 번에 작성했던 코드에서 계속 진행한다.

현재는 회색 네모박스 하나 뿐이다. 살을 덧붙여보자
const renderer = new THREE.WebGPURenderer({ antialias: true });webGPURenderer 객체를 생성할 때, 생성자의 파라미터로 생성 옵션을 넣을 수 있다.
현재 있는 antialias 값을 true로 설정해주면, 안티에일리어싱이 된다.
어려울 것은 없고, 렌더링 되는 모델의 가장자리에 계단현상이 없어지도록 해준다.
그림으로 보면 더 쉽다.
이것 외에도 다양한 생성 옵션들은 three.js 웹사이트의 api 문서를 통해 확인 가능하다.
document 에 들어가서 webGPU를 검색하면
이렇게 자세한 내용을 볼 수 있다.
antialias 의 정보를 보면,
Whether MSAA as the default anti-aliasing should be enabled or not.
Default is false.
라고 되어있다.
기본 값은 false이며, MSAA방식을 사용한다고 한다. 쉽게 말하면, 렌더링(샘플링)을 여러번 해서 안티엘리어싱을 한다는 뜻이다.
samples 옵션을 통해 샘플링할 횟수를 정할 수 있다.
샘플링을 많이 하면품질은 더 좋아지겠지만 당연하게도 렌더링 속도가 느려진다. antialias가 작동할 때 이것의 기본값은 4이다.
두 가지만 더 살펴보자
If set to true, the renderer uses a WebGL 2 backend no matter if WebGPU is supported or not.
Default is false.
기본값은 'false'이며, 'true'로 지정하면, WebGPU 를 사용할 수 있는 지 여부와 상관없이, WebGL 기반의 렌더러를 사용하도록 만든다.
이것을 사용하는 이유는, 개발한 3D 웹앱이 WebGPU 와 WebGL 모두에서 잘 실행되는 지 테스트하기 위함이다.
기본 값은 false이다. 이것을 true로 설정하면, 깊이 버퍼에 대한 값을 로그함수를 이용해 결정한다.
이게 무슨 말이지??
이건 Material 에 대한 이해가 수반되어야 하니, 일단은 이런게 있구나. 정도로만 알고 있으면 되겠다.
이것을 켰을 때와 껐을 때가 어떻게 다른지만 예시를 통해 알아보자.
three.js 사이트의 examples 에서, logarithm을 검색하면 예시가 나온다.

WebGPU 부분을 클릭해서 살펴보자.

왼쪽은 logarithmicDepthBuffer 가 적용되지 않은 것이고, 오른쪽은 적용된 화면이다.
왼쪽은 반짝거림이 심하고, 부정확한 렌더링이 나타난다.
GPU는 기본적으로 가까운 물체는 정밀도를 몰아주고, 먼 물체는 정밀도를 깎아버린다.
그런데 그 깊이 차이가 너무 많이 나면, z-fighting 이라는 현상이 나타나고, 그것이 바로 이 위의 왼쪽화면이다.
logarithmicDepthBuffer: true를 켜면, 깊이 정밀도를 가까운 곳에 몰빵하는 대신 로그 스케일로 좀 더 고르게 분산시켜줘서 이 문제를 완화해준다.
하지만 성능에 영향을 줄 수 있으므로, 무조건 사용하지 말고 경우에 따라 차용하면 된다.
다음은 toneMapping에 대해 알아보자.
const renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.toneMapping = THREE.ACESFilmicToneMapping;코드는 이렇게 추가해주면 된다.
이것은 toneMapping을 추가하기 전이고,
이건 toneMapping을 추가한 후이다. (뭐가 다른거지...?)
조금 더 밝아진 느낌이 있다.
toneMapping에 지정할 수 있는 값은 이렇게 8개이다.
따로 지정하지 않으면 NoToneMapping 값이 지정된다.
toneMapping 이 그래서 뭐냐면
권장되는 방식은 ACESFilmicToneMapping이다. 오랫동안 three.js에서 가장 무난하게 "영화같이 자연스러운" 결과를 주던 옵션이다.
아무것도 안 하면, 밝은 부분이 그냥 뭉텅이로 하얗게 날아갈 수 있으니 주의가 필요하다.
이와 같이 많이 쓰는 것이 toneMappingExposure 이다.
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 2;이런 식으로 2로 설정하면
이렇게 밝아진다.
카메라의 노출값처럼, 높이면 빛을 많이 받아 밝아지고, 낮추면 어두워진다.
다음은 outputColorSpace이다. 이것을 이해하기 위해선 우선 ColorSpace가 무엇인지 부터 알아야 한다.
colorSpace는 색상을 어떻게 왜곡해서 해석할지를 정한다.
Q: 왜곡을 왜 함?
A: 모니터가 밝기를 저장하는 방식이랑, 조명 계산이 요구하는 방식이 서로 달라서, 그 사이를 안 맞춰주면 색이 왜곡되니까.
라고 한다.
THREE.NoColorSpace는 어떠한 색왜곡도 하지 않는다.
THREE.SRGBColorSpace는 어두운 값을 좀 더 밝게 해서, 사람 눈에 더 자연스러운 느낌을 준다. 이것이 default 값이다.
그러므로 따로 지정할 필요 없다. 굳이 지정하자면
renderer.outputColorSpace = THREE.SRGBColorSpace로 지정하면 된다. NoColorSpace를 하면, 아무런 왜곡이 없겠다.
private async setupThreeJs() {
this.domApp = document.querySelector("#app") as HTMLElement;
if (!this.domApp) {
throw new Error("Could not find element with id '#app'");
}
const renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 2;
renderer.outputColorSpace = THREE.SRGBColorSpace;
this.domApp.appendChild(renderer.domElement);
await renderer.init();
this.renderer = renderer;
this.scene = new THREE.Scene();
}React, Next.js, React Native로 만들고 기록합니다.
giscus로 동작하며 GitHub Discussions에 저장됩니다.