Three.js/Intro

[threejs-journey 1-3] : Scene 구성(깊게 통찰해보자!)

SL123 2025. 5. 30. 11:31

 

개요

안녕하세요. 이번에는 3D 웹을 살펴보게 되었습니다. 3D 웹은 webGL만 있는 줄 알았는데, Three.js라는 라이브러리가 있더군요. 해당 라이브러리를 이용해서 3D 웹을 공부하게 되었습니다.

 

저는 위 제목처럼 강의를 구매했고, 해당 강의에서 설명하는 것을 기반으로 더 깊게파서 포스팅하려 합니다. 저는 이 코드의 근거가 무엇이며, 왜 라이브러리에서는 이렇게 사용해야 할까? 를 원론적으로 파고들 생각입니다. 그래서 포스팅이 상당 부분 이해가 안될 수 도 있으며, 저 또한 공부중이기 때문에 설명에 대한 실수가 나올 수 있습니다. 이 점 참고해서 더블 체킹 부탁드리고, 문제가 있는 부분을 지적해주시면, 바로 수정해보도록 하겠습니다. 

 

본론 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Transform objects</title>
    <link rel="stylesheet" href="./style.css">
</head>
<body>
    <canvas class="webgl"></canvas>
    <script type="module" src="./script.js"></script>
</body>
</html>

- canvas class='webgl' 

=> 실제로 html로 화면을 그릴 수 있게 만들어주는 DOM

 

 

import * as THREE from 'three'

// Canvas
const canvas = document.querySelector('canvas.webgl');

// Scene
const scene = new THREE.Scene();

/**
 * Objects
 */
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

/**
 * Sizes
 */
const sizes = {
    width: 800,
    height: 600
};

/**
 * Camera
 */
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height);
camera.position.z = 3;
scene.add(camera);

/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
    canvas: canvas
});
renderer.setSize(sizes.width, sizes.height);
renderer.render(scene, camera);

 

- import * as THREE from 'three'

'three' 모듈에 포함된 모든 클래스와 함수를 THREE라는 네임스페이스에 묶어서 가져옵니다.

THREE는 이제 Three.js의 모든 도구가 들어있는 도구 상자처럼 쓰입니다.
ex) THREE.Scene, THREE.Mesh, THREE.BoxGeometry 

 

- canvas

Three.js는 3D 그래픽을 브라우저에 출력하기 위해 <canvas> 요소를 사용합니다. canvas.webgl 클래스를 가진 DOM 요소를 가져와 렌더러(THREE.WebGLRenderer)에 연결함으로써 이 영역에 3D 씬을 그릴 수 있게 됩니다.

 

 

- Scene

Three.js에서 Scene은 3D 오브젝트, 카메라, 조명 등 모든 요소를 담는 컨테이너 역할입니다.

https://threejs.org/docs/index.html#api/en/scenes/Scene

 

three.js docs

 

threejs.org

설명을 보면 Scene을 사용해 렌더링할 대상과 위치를 정할 수 있습니다. 해당 구문을 통해 Scene은 말 그대로 Object를 묶어놓은 Level이라고 생각하면 될 것 같습니다. 즉 모든 3D 요소들이 배치되는 무대(stage)라고도 볼 수 있죠.

 

밑에 프로퍼티에 대한 설명들이 자세히 나와있는게 이 부분들은 하면서 알아보고, 러프하게 보자면 환경 변수, 배경, 머터리얼 등을 Scene에 대해 전역으로 세팅할 수 있어보입니다. 하지만, Scene에 전역으로 사용하는 것은 Object나 light가 많으면 많을수록 물리적 연산이 많아져 상당한 부하를 일으키기 때문에 Scene에 대한 변수를 사용할 때는 주의할 필요가 있어보입니다.(추후에 다시 공부해서 해당 페이지를 수정해보겠습니다.)

 

- BoxGeometry

직육면체 형태의 기본 3D 객체를 생성하는 기하학 컨테이너입니다.

https://threejs.org/docs/index.html#api/en/geometries/BoxGeometry

 

three.js docs

 

threejs.org

설명을 보면 뭔가 많이 빠져있습니다. 기본적인 width, height, depth가 존재한다고 하는데, 이는 기존에 스케일 x, y, z를 말합니다. 즉, BoxGeometry(Vertex), (Face) 등의 기하학적 데이터만 담고 있는 객체입니다. 이것만으로는 화면에 보이지 않으며, 색상, 텍스처, 광원 등의 시각적 정보가 추가되어야 합니다. 결국 기하학적인 데이터만이 존재하는 것이죠.

 

이렇게 되면 Scene에 그릴 수 없습니다. 왜냐? 데이터를 가공하지 않았기 때문에 데이터만 가지고는 렌더링을 할 수 없습니다. 렌더링을 하기 위해서는 해당 데이터를 가공해야하죠. 이 기하 데이터를 렌더링 가능한 상태로 만들기 위해서는 Material이 필요합니다. 예를 들어 MeshBasicMaterial은 색상을 입히는 재질로, 이 둘을 Mesh 정보로 합쳐야 실제 Scene에 그릴 수 있는 3D 객체가 됩니다. 재질과 기하 데이터가 합쳐져 가공되어야지 렌더링을 할 수 있고, Scene에 배치되면 그려질 수 있는거죠.

 

- MeshBasicMaterial

https://threejs.org/docs/index.html#api/en/materials/MeshBasicMaterial

 

three.js docs

 

threejs.org

MeshBasicMaterial은 가장 단순한 재질(Material) 중 하나로, 광원(Light)의 영향을 받지 않고 고정된 색으로 표현됩니다(프로퍼티에 따라 달라짐). 복잡한 광원 효과가 필요하지 않은 경우에 자주 사용됩니다.

 

 

기본적으로 Material을 상속받은 컨테이너이고, 환경 변수를 간단히 확인해보니, 굴절, 반사, 환경 매핑 등 다양한 기능들을 MeshMaterial에서 제공하고 있습니다. 즉, 왠만한 Mesh로 할 수 있는 기능들은 다 제공되는 셈이죠.

 

- Mesh

https://threejs.org/docs/index.html#api/en/objects/Mesh

 

three.js docs

 

threejs.org

이제 Mesh의 데이터와, Material의 데이터를 합쳐 가공된 정보를 Scene에 넘겨주면, Scene에서 Box가 그려집니다. 쉐이딩은 기본적으로 해주기 때문에 우리가 따로 Mesh에 재질을 입혀주지 않아도 알아서 배치되어 그려지게 됩니다. 따라서, 박스를 Scene에 구성한다고 하면 BoxGeometry를 만들고, MeshBasicMaterial로 재질을 추가해 Scene에 담아놔야 하죠. 이 부분은 쉐이더를 잘 알고 있다면 충분히 이해가 되실겁니다. 이후에 바로 mesh를 넣어줍니다. mesh는 0, 0, 0 위치에 생성됩니다.

 

 

내용이 어렵다고 느껴지면 한 없이 어려울 수 있습니다. 비유를 들어 천천히 설명드리겠습니다.

-출저 pngtree 다운로드

Geometry(기하학적 데이터)는 마치 조각되지 않은 나무 블록과 같습니다. Material(재질 데이터)은 그 위에 입히는 광택, 색상, 질감이라 볼 수 있습니다. 이 둘이 결합되면 비로소 하나의 조각상(Mesh) 이 완성되는 것이죠.

 

다시 말해, Geometry만 있다면 형태만 존재할 뿐 색도 질감도 없습니다. Material만 있다면 칠할 대상이 없어 아무런 의미가 없죠. 따라서 3D Scene에서 오브젝트를 표현하려면 GemoetryMaterial이 반드시 함께 존재해야 하며, 이 둘이 합쳐져야 Mesh가 실제로 화면에 렌더링됩니다.

 

- PerspectiveCamera

https://threejs.org/docs/index.html#api/en/cameras/PerspectiveCamera

 

three.js docs

 

threejs.org

만약 우리가 방송 무대를 구성한다고 가정해봅시다. 무대(Scene)을 구성하고, Mesh(Props, env)을 배치했습니다. 근데 방송에서 Camera가 없다면 어떨까요? 방송을 보는 시청자 입장에서는 아무것도 전달이 안될 겁니다. 그래서 카메라는 필수 요소이며, 3D 웹과는 뗄레야 뗄 수 없습니다.

 

Perspective는 말 그대로 원근카메라, 즉, 입체감을 주기 위한 카메라 설정이죠. 반대로는 Orthographic 3D를 2D로 그리기 위한 카메라가 있습니다. 이 둘의 차이는 바로 w값의 차이입니다. w값을 이용해 3차원 공간을 4차원 동차 좌표계(Homogeneous Coordinate)로 확장하고, 이걸 통해 투영 행렬(Perspective Projection Matrix)을 구성해서 원근감을 만드는 것입니다.

 

Perspective의 프로퍼티는 fov, aspect, near, far 등 기본적인 카메라 기능들을 가진 것을 확인할 수 있습니다. 해당 프로퍼티를 간단하게 설명하면 fov는 field of view의 약자이며, 카메라의 시야각을 뜻합니다. 대부분 인간의 시야각인 75도를 사용합니다. aspect는 종횡비이며, 종횡비는 쉽게 설명하면 가로x세로 비율입니다. 대부분의 모니터는 가로가 길기 때문에 sizes를 가로를 넓게 지정해서 사용했습니다. near, far는 Culling에 대한 최적화 기능입니다. near는 Default 0.1 far는 2000입니다. 0.1 ~ 2000 사이만 그리고 나머지는 Culling을 수행하죠. 더 궁금하신 부분들은 Culling, aspect ratio, FOV 를 검색해보시기 바랍니다. 카메라는 기본적으로 0, 0, 0에 배치됩니다.

 

여기서 질문하나 드리겠습니다.

현재 Mesh가 그려지지 않을 겁니다.

왜 안 그려질까요? 

위에서 설명한 Culling 때문에 안그려질 겁니다. 0.1 이상, 2000미만 위치에 화면만 그리기 때문에 0, 0, 0위치에 존재하는 Mesh는 그려지지 않기 때문이죠. 그래서 z값을 뒤로 땡겨줍니다.(이 라이브러리는 신기하게 backward가 positive값이다) 그리고, 위에서 설명한 것 처럼 무대에 카메라를 설치해야죠. scene에 카메라를 add 시켜줍니다.

 

- WebGLRenderer

https://threejs.org/docs/index.html#api/en/renderers/WebGLRenderer

 

three.js docs

 

threejs.org

WebGLRenderer는 Three.js에서 WebGL API를 직접적으로 추상화한 핵심 렌더러 클래스입니다. 생성 시 넘겨주는 canvas는 HTML에서 미리 만든 <canvas> 요소를 의미하며, 해당 캔버스는 렌더링 결과가 출력될 렌더 타겟(Frame Buffer)으로 사용됩니다.내부적으로 WebGL context를 생성하고 초기화합니다. (gl = canvas.getContext('webgl2') 등) 이후 GPU와 통신하는 기반이 됩니다.

 

 

렌더 타겟은 향후 쉐이더를 공부할 때 핵심이 되는 개념으로, 예를 들어, Diffuse, Normal, Specular, Ambient, Shadow, Occlusion, Metalness, Roughness 등 다양한 정보를 저장하는 G-Buffer 로 구성될 수 있습니다. 실제로 복잡한 렌더링 파이프라인에서는 20개 이상의 렌더 타겟(depth 포함)이 존재할 수 있으며, 디버깅 시 각각의 버퍼를 캡처해 쉐이더의 결과를 분석하게 됩니다.(Visual studio 20xx 버전 사용해서 HLSL 캡처를 통해 Vertex, Pixel Shader 등을 캡처한 경험이 있지만, Three.js(WebGL)에 경우 아직 디버깅 방식에 익숙하지 않아 이 부분은 추후 정리할 예정입니다)

 

- SetSize

렌더러가 출력할 해상도를 정의합니다. 이건 단순히 캔버스의 CSS 크기가 아니라 실제 픽셀 단위의 크기를 지정하는 것입니다. 내부적으로는 다음과 같이 처리됩니다.

canvas.width = sizes.width;
canvas.height = sizes.height;
canvas.style.width = sizes.width + 'px';
canvas.style.height = sizes.height + 'px';
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();

 

 

renderer.render(scene, camera);

위에 코드로 render를 마무리합니다.

 

내부 동작을 요약해보겠습니다.

1. 카메라의 View-Projection 행렬을 계산 (camera.projectionMatrix * camera.viewMatrix)

    - view 행렬 * projection 행렬 곱으로 View-Projection 변환

2. 모든 Mesh/Object를 Scene Graph 트리 순회한다

    - 각 객체의 World Matrix 계산

    - Shader에 해당 변환 행렬 전달

3. 렌더 타겟(=canvas)에 픽셀 단위로 그리기 시작

    - z-buffer 사용

    - depth test, culling, blending 등 GPU 단계 적용

4. 실제 CPU 명령으로 받은 draw call을 GPU 명령으로 실행

 

 

2번에서  Scene Graph 트리 순회가 이해가 안될 수도 있습니다. 트리 순회를 하려면 Scene에 자식이 있어야 하는데, 어디에 자식이 있는거지? 물론 제가 그렇게 생각했습니다. 그래서 찾아봤더니, Scene은 내가 그려야할 대상을 모두 '자식'으로 들고 있습니다. 이후 그 자식들을 트리로 순회하면서 렌더링 하는 구조죠. 

Scene (root)
├── Camera
├── Light
├── Mesh1
│   └── ChildMesh1
│       └── GrandChildMesh
├── GroupA
│   ├── Mesh2
│   └── Mesh3
└── ...

이 정도로 기술 스택을 마무리하겠습니다. 

 

그리고 이렇게 설정하고 실행하면!!!!

짜잔~ 사각형 하나 그릴 수 있습니다! 

 

결론

 

고작 사각형 하나 그린다고 이렇게 많은 작업을 내부에서 수행하고 그 고생을 한거라고? 라고 하실 수 있지만, 항상 시작이 반이라고 말합니다. 그래픽스는 이렇게 세팅은 기준을 무조건 따르게 됩니다. 말하자면, 고집불통이죠. 그래서 처음만 조금 고생하고, 이렇게 꼼꼼하게 원리부터 안다면 나중에는 해당 코드들과 연계되어 코드가 술술 읽히게 될 것입니다. 

 

이상 Three.js Scene 기본 세팅을 마치겠습니다.

감사합니다.