웹으로 구현해본 3D 소프트웨어 렌더러
목차
웹 개발을 하다 보면 이미지를 다루어야 하는 경우가 생기고, 그 과정에서 Canvas를 사용하는 상황을 한 번쯤은 마주하게 된다.
단순한 도형이나 이미지를 그리는 것을 넘어서, 조금 더 나아가면 3D 모델을 화면에 표현해야 하는 경우도 생긴다. 보통은 WebGL이나 라이브러리를 사용해 이를 해결하지만, 그 내부에서 어떤 과정이 일어나는지, 이미지가 실제로 어떻게 그려지는지, 그리고 3D 모델이 2D 이미지로 변환되는 과정이 어떻게 이루어지는지까지 깊이 고민하는 경우는 많지 않다.
그래서 이 과정을 하드웨어(GPU) 도움 없이, 렌더링 과정(Rendering Pipeline)을 코드로 직접 구현하는 소프트웨어 렌더러(Software Renderer)를 웹 기반으로 만들어보았으며, 그 과정을 간략하게 정리해보고자 한다.
Rendering Pipeline #
렌더링(Rendering)은 컴퓨터 화면에 우리가 보는 UI나 웹 페이지 등을 출력하는 과정으로, 쉽게 말해 화면에 그림을 그리는 것을 의미한다.
3차원 모델을 2차원 화면에 출력하기 위해서는 차원 1개를 줄여 3D 좌표를 2D 좌표로 변환해야 한다. 이 과정은 수학적으로 정의되어 있으며, 정해진 절차에 따라 계산된 결과가 픽셀 단위의 이미지로 변환되어 화면에 출력된다.
이 일련의 과정을 렌더링 파이프라인(Rendering Pipeline) 이라고 한다. 이 과정은 오래전부터 하드웨어(GPU)로 구현되어 왔으며, 개발자는 일반적으로 OpenGL이나 DirectX와 같은 그래픽 API를 통해 이를 활용한다.
렌더링 파이프라인은 일반적으로 다음과 같은 단계로 구성되며, 이번 구현한 소프트웨어 렌더러는 아래 과정을 구현하였다 (모든 과정은 오른손 좌표계(Right-hand Coordinates) 기준으로 함).
Culling); B-->C(Projection) C-->D(Lighting) D-->E(Clipping) E-->F(Viewport) F-->G(Rasterization)
Transformation #
변환(Transformation)은 3차원 공간에 존재하는 정점(Vertex)을 화면에 출력하기 위한 좌표로 변환하는 과정이다.
각 정점은 모델 변환(Model Transform), 뷰 변환(View Transform), 투영 변환(Projection Transform)을 거치며, 이후 Perspective Divide를 통해 3차원 좌표계에서 [-1, 1] 범위를 가지는 NDC(Normalized Device Coordinates)로 변환된다.
이후 NDC 범위를 벗어난 정점이나 도형은 클리핑(Clipping) 과정을 통해 제거되거나 잘려나가며, Rasterization 단계에서 실제 화면에 출력될 픽셀로 변환된다.
또한, 삼각형이나 폴리곤 수가 많을 경우 렌더링 비용이 증가하기 때문에 보이지 않는 면을 제거하는 Back-face Culling을 뷰 변환 이후 단계에서 수행하였다.
- 폴리곤 법선 벡터와 카메라 방향 벡터의 내적을 통해, 카메라를 향하지 않는 면을 제거하는 방식으로 구현
- 내적 값이 0보다 크면 카메라를 향하는 면(Front-face), 0보다 작으면 카메라를 등지고 있는 면(Back-face)으로 판단
Lighting & Shading #
Lighting과 Shading 단계에서는 물체가 빛을 어떻게 반사하고 표현되는지를 계산한다.
Lighting은 광원과 물체의 관계를 기반으로 밝기를 계산하는 과정이며, Ambient, Diffuse, Specular, Emission 요소의 조합을 통해 물체의 입체감과 재질감을 표현한다.
- Ambient (주변광): 방향성과 관계없이 물체 전체에 균일하게 적용되는 기본 조명
- Diffuse (난반사광): 빛이 물체 표면에 닿아 여러 방향으로 퍼지며, 표면 각도에 따라 밝기가 달라짐
- Specular (정반사광): 특정 각도에서 반사되어 하이라이트를 생성하는 빛
- Emission (방출광): 외부 광원과 관계없이 물체 자체가 발광하는 효과
Shading은 계산된 조명 정보를 실제 픽셀에 적용하는 과정으로, Flat Shading, Smooth Shading (Gouraud, Phong) 방식이 있으며 각 방식에 따라 표면의 부드러움과 디테일이 달라진다.
이번 구현에서는 세 가지 Shading 방식을 모두 구현하여, 사용자가 화면에서 직접 선택하고 비교할 수 있도록 하였다.
- Flat Shading: 폴리곤 단위로 하나의 법선을 사용하여 색상을 계산
- Gouraud Shading: 각 정점 색상을 계산한 후, 보간(Interpolation)으로 폴리곤 내부 색상을 결정
- Phong Shading: 정점 법선 보간 후, 픽셀 단위로 Lighting을 계산하여 자연스러운 결과를 생성
Texture Mapping #
Texture Mapping은 물체 표면에 이미지를 입혀 실제와 같은 재질감과 디테일을 표현하는 방법이다.
각 정점에는 UV 좌표가 정의되어 있으며, 이를 기반으로 텍스처 이미지의 특정 위치를 참조하여 최종 픽셀 색상을 결정한다.
이번 구현에서는 별도의 이미지 처리 모듈을 사용하지 않고, Canvas를 통해 이미지를 로드한 뒤 픽셀 데이터를 배열로 변환하여 사용하였다.
텍스처 매핑 과정에서는 인접한 픽셀 값을 사용하는 Nearest Neighbor Sampling 방식을 적용하였으며, 소수점 단위 보간 없이 정수 좌표 기반으로 샘플링을 수행하였다.
Rasterization #
Rasterization 단계에서는 출력할 화면 크기(Viewport)에 맞는 이미지 버퍼를 생성하고, 3D 공간에서 정의된 도형을 픽셀 단위로 변환하여 화면에 그린다.
이 단계에서는 삼각형 단위로 스캔라인 렌더링(Scanline Rendering)을 수행하였으며, 텍스처 매핑과 조명 계산 결과를 최종 픽셀 색상에 반영하였다. 픽셀 단위 색상 계산 과정에서 무게 중심 좌표계 (barycentric)를 활용하여 보간을 수행하였다.
또한, Depth Buffer(Z-buffer)를 활용하여 겹쳐지는 물체의 앞뒤 관계를 처리하였고, 부드러운 경계를 표현하기 위해 2x2 MSAA(Multi-Sample Anti-Aliasing)를 적용하였다.
최종적으로 생성된 이미지 버퍼를 Canvas에 전달하여 브라우저에 결과를 출력하였다.
Implementation #
본 프로젝트는 Vite와 TypeScript를 기반으로 구현하였다.
일반적으로 사용하는 프론트엔드 프레임워크인 React나 Vue를 사용하지 않고, HTML과 JavaScript만으로 동작하도록 구성하였다. 단, 이벤트 처리를 위해 Alpine.js를 사용했고, Viewport 크기를 고정했기 때문에 (1024x768) 반응형이 아닌 고정형 페이지로 구현했다.
별도의 렌더 루프를 사용하지 않고 Canvas에 렌더링된 이미지 버퍼를 전달하여 출력하는 형태로 구현했고, 이벤트(숫자 변경, frame/depth buffer 변경 시)에만 업데이트 후 다시 렌더링 하는 방식으로 동작하도록 했다.
Models #
구현 시 아래 5개 모델을 이용하였다.
- Texture 있는 모델: Tiger, Bunny, Buddha, Zebra: Texturemontage Textured Models
- Texture 없는 모델: Teapot: Utah University Graphics Lab
각 모델의 정점, 폴리곤 개수는 아래와 같다.
| Model | # of Vertices | # of Polygons | 비고 |
|---|---|---|---|
| Tiger | 6,089 | 10,788 | |
| Bunny | 16,323 | 30,338 | |
| Buddha | 29,609 | 51,414 | |
| Zebra | 21,305 | 40,310 | |
| Teapot | 17,456 | 33,462 | 텍스쳐 없음 |
Performance #
모델 렌더링 후 이벤트가 발생했을 시에만 업데이트하도록 하여 단순한 뷰어 용으로 볼땐 약간의 딜레이가 있지만 감안한다면 문제 없이 사용 가능했다.
그러나 일반적인 그래픽스 프로그램 처럼 렌더 루프(Render Loop)를 이용해 매번 업데이트 해야 하는 경우 M4Pro@24GB, 크롬 브라우저 결과 평균 10fps 정도를 보여주어 실시간 렌더링에는 부적합했다.
구현 결과는 아래와 같다.
- Demo: https://inium.github.io/web-canvas-software-renderer/dist/
- Repository: https://github.com/inium/web-canvas-software-renderer
Conclusion #
약 15년 전, C++로 소프트웨어 렌더러를 구현해본 적이 있었다. 그때의 기억과 경험을 바탕으로, 이번에는 웹 환경에서 동작하는 소프트웨어 렌더러를 다시 구현해보았다. 그 과정을 통해 렌더링 파이프라인 전반을 다시 정리하고, 그동안 익숙하게 사용해왔던 그래픽 처리 과정을 한 단계 더 깊이 이해할 수 있는 계기가 되었다.
컴퓨터 그래픽스(Computer Graphics)는 보통 전문 분야로 여겨지며, OpenGL이나 DirectX와 같은 그래픽 API, 혹은 Unreal, Unity3D, Godot과 같은 엔진을 기반으로 데스크탑이나 모바일 환경에서 구현되는 경우가 많다.
그에 비해 웹 환경에서의 3D 렌더링은 상대적으로 익숙하지 않은 영역일 수 있다. 하지만 실제로는 교육, 산업, 시각화 등 다양한 분야에서 웹 기반 3D 렌더링이 활용되고 있다. 예를 들어, 교육용 서비스에서는 3D 모델을 웹에서 확인하고 수정해야 할 수 있으며, 산업 현장에서는 스캔된 데이터를 웹에서 검토한 뒤 다음 프로세스로 전달하는 과정이 필요할 수도 있다.
이번 구현을 통해 느낀 점은, 기술을 사용하는 것과 그 원리를 이해하는 것은 전혀 다른 경험이라는 것이었다.
소프트웨어 렌더러를 직접 구현해보면 3D 모델이 화면에 그려지기까지의 모든 과정을 한 번에 연결해서 이해할 수 있고, 각 단계가 왜 필요한지 자연스럽게 체감할 수 있다.
개인적으로 그래픽스에 대한 이해를 깊게 가져가고 싶다면, 소프트웨어 렌더러를 한 번쯤 직접 구현해보는 것을 추천한다.