Three.js 추상화?

Three.js를 공부하게 된 배경
(참고)THREE.JS 강좌 : 2. WebGPU에 대한 THREE.JS 추상화
- 실제로 추상화된
Three.js만을 이용해서 개발하면, 상용 개발에 있어서 벽에 부딪힌다. - 그래서, 근간이 되는
WebGPU에 대한 어느정도의 내용을 알아야한다.
WebGPU API만을 이용한 간단한 3D 그래픽 코딩
- 프로젝트 생성하기
npm create vite@latest .로vite프로젝트 생성vanilla,JavaScript,no(기본 패키지 설치)로 진행npm install->npm run dev로 실행
index.html수정
<div id="app"></div>삭제<div id="gpuCanvas"></div>추가- 해당 캔버스에서 3D 그래픽이 렌더링 될 예정
style.css수정- 기존 코드 모두 삭제
- 아래 코드 입력
html, body{ margin:0; overflow:hidden; } #gpuCanvas{ width:100%; height:100%; }```main.js- WebGPU 지원 여부 검사
WebGPU는 CPU에서 GPU가 실행할 명령들을 만들어 GPU에게 전달해주는데,
CPU와 GPU간의 통신에 병목현상을 최소화하기 위해서 비동기 API를 호출한다.
이를 위해서async로main함수를 정의한다.
main.js- adapter와 device 얻기
device.lost~~코드는 절전모드와 같이 디바이스가 소실되었을 때, 이에 대한 에러를 처리하기 위한 코드이다.Adapter: 실제 물리적 GPU 장치 (type :GPUAdapter)Device: 논리적 GPU 인터페이스로 실제 GPU와 통신을 위해 사용 (type :GPUDevice)
-
-1 WebGPU context 얻기
const context = canvas.getContext("webgpu");로WebGPU컨텍스트를 얻는다.
-
-2 context에 WebGPU와 호환되는 구성을 세팅
alphaMode: 'opaque'란?WebGPU로 렌더링된 결과가 완전히 불투명하다는 것을 의미한다.- 이렇게 설정하면, GPU가 렌더링 결과를 최적화할 수 있다.
- 만약 투명한 요소가 포함된 장면을 렌더링하려면,
alphaMode: 'premultiplied'로 설정해야한다.
-
-1 삼각형의 3개의 정점 정의
- 삼각형을 렌더링하기 위해서는 삼각형을 구성하는 3개의 정점이 필요하다.
-
-2 GPU 메모리 생성
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST: 용도를 명확히 지정한다.
- -3. GPU 메모리에 복사.
중간 과정 : 개요
- 삼각형을 움직이고, 뷰 전환 등이 가능하게 하려면 아래의 과정이 필요하다
- 로컬 정점 ->
Model변환 ->View변환 ->Perspective변환 -> 뷰포트 변환(WebGPU에서 처리) -> 화면 2D 좌표(WebGPU에서 처리) - 로컬 정점을
Model변환,View변환 그리고Perspective변환을 하는 것은 행렬을 이용한다.
중간 과정 : 행렬
Model변환 4X4 행렬 M,View변환 4X4 행렬 V,Perspective변환 4X4 행렬 P, 각각 1개씩 총 3개의 행렬이 필요하다.- 3개의 행렬을 GPU에게 전달해서 행렬 연산을 GPU를 이용해 진행해야한다.
- -1. 행렬 구성
- 하나의 행렬은 16개의 실수로 구성된다.
- 따라서, 한 개의 행렬 데이터 크기는
16 * 4(실수값 1개가4byte이기 때문) - 총 3개의 행렬 데이터 크기는
16 * 4 * 3
- -2. GPU 메모리 할당
- 삼각형은 용도에
VERTEX이라 지정했다면, 행렬은UNIFORM이라 지정함에 유의
- -1.
Model행렬 정의하기 위한 함수 정의
function mat4Identity() {
return new Float32Array([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]);
}- 반환된 행렬은 삼각형을 이동하고 크기 조정하는데 쓰이는데, 해당 행렬은 단위 행렬로 회전도, 이동도, 크기 조정도 없는 기본 행렬이다.
- 단위 행렬이란?
- -2.
View행렬을 정의하기 위한 함수 정의
eye는 카메라의 위치,center는 카메라가 바라보는 위치,up은 카메라의 회전 방향을 나타낸다.- 3개의 인자값을 수학적 연산을 통해 4X4 행렬을 반환한다.
- -3.
Perspective행렬을 정의하기 위한 함수 정의
fovY는 카메라의 화각을 나타낸다.aspect는 카메라 필름에 대한 비율을 나타낸다.near,far은 거리값으로 카메라가 볼 수 있는 범위를 지정한다.- 해당 행렬을 통해서 원근감을 나타낼 수 있다.
-
-4 정의된 함수로 행렬 정의하기
-
정의된 행렬을 GPU에 복사하기
- 3개의 정점과, 3개의 행렬 데이터를 GPU에 전달 완료한다.
- 셰이더
- 앞서 전달했던 데이터를 실행하기 위한
WGSL이라는 셰이더가 필요하다. - 쉐이더는
VERTEX_SHADER와FRAGMENT_SHADER가 필요하다. VERTEX_SHADER는 GPU가 삼각형의 정점 데이터를 처리하는 방법을 정의한다.- 즉, 삼각형을 구성하는 로컬 정점을 순차적으로
Model변환 ->View변환 ->Projection변환까지 실행하는 과정,
즉 삼각형의 계산을 실행하는 코드가VERTEX_SHADER에 작성된다.
- 즉, 삼각형을 구성하는 로컬 정점을 순차적으로
FRAGMENT_SHADER는VERTEX_SHADER를 이용해서 화면의 픽셀을 결정할 수 있고, 그 픽셀의 색상값을 정의한다.
- GPU 메모리에 어떤 종류의 데이터가 있고, 어디서 읽어야 하는지 판단 (
Bind Group Layout정의)
- 이러한 레이아웃 설정은 유니폼에 대한 버퍼 데이터에 대해 꼭 필요
- 앞서 정의한 세 개의 행렬을 저장하기 위한 유니폼 버퍼에 대한 것입니다.
-
쉐이더와
Bind Group Layout을 이용해서 렌더링 파이프라인 객체를 생성하면 렌더링 준비는 끝 -
앞선 쉐이더 코드를 아직 컴파일 하지 않았는데, 실제로 GPU에서 실행하려면 컴파일 되어야한다.
const vertexModule = device.createShaderModule({ code: VERTEX_SHADER });
const fragmentModule = device.createShaderModule({ code: FRAGMENT_SHADER });- 근데 이러면, 컴파일 할 때 문제가 있는지 파악할 수 없다.
- 이를 위해 아래 코드를 추가한다.
const vertInfo = await vertexModule.getCompilationInfo();
vertInfo.messages.forEach((m) =>
console[m.type === "error" ? "error" : "warn"](`[Vertex shader] ${m.message}`),
);
const fragInfo = await fragmentModule.getCompilationInfo();
fragInfo.messages.forEach((m) =>
console[m.type === "error" ? "error" : "warn"](`[Fragment shader] ${m.message}`),
);- 컴파일된
ShaderModule과Bind Group Layout을 이용해서 렌더링 파이프라인 객체를 생성해야 한다.
- 파이프라인이란 어떤 쉐이더를 이용하고, 사용할 데이터는 어떤 형식이며, 정점들은 어떻게 렌더링할 것인지에 대한
실행 규칙 흐름을 정의한 객체
| 요소 | 설명 | 타입 |
|---|---|---|
| Adapter | 실제 물리적 GPU 장치 | GPUAdapter |
| Device | 논리적 GPU 인터페이스로 실제 GPU와 통신을 위해 사용 | GPUDevice |
| Shader | WGSL(WebGPU Shading Language)로 정점의 위치 계산 및 픽셀의 색상 결정 등 | GPUShaderModule |
| Pipeline | 어떤 쉐이더를 사용하고 데이터는 어떤 형식이며 어떻게 렌더링할 것인가와 같은 규칙의 흐름을 정의한 객체 | GPURenderPipeline, GPUComputePipeline |
| Command Encoder | GPU에 보낼 작업 내역들(Render Pass, Compute Pass)을 기록함 | GPUCommandEncoder |
| Command Buffer | Command Encoder에 기록된 작업들을 GPU가 실제로 실행될 수 있는 형태로 만든 데이터 | GPUCommandBuffer |
| Pass | Render Pass와 Compute Pass가 있으며 Command Encoder가 Command Buffer를 만들 때 저장하는 것 | GPURenderPassEncoder, GPUComputePassEncoder |
| Queue | Command Buffer에 저장된 실행 명령(Pass)을 GPU(정확히는 실행 Pipeline)로 전달할 때 사용하는 통로 | GPUQueue |
- 위 표를 보면, 삼각형을 렌더링하기 위해서는 GPU 렌더 파이프라인 타입의 객체, 즉
GPURenderPipeline이 필요하다는 것을 알 수 있다. GPURenderPipeline: GPU가 3D 그래픽 렌더링을 수행하기 위해 필요한 전체 절차가 정의된 객체GPUComputePipeline: GPU가 범용적인 계산 작업을 수행하기 위해 필요한 전체 절차가 정의된 객체
- 렌더링
- 렌더링을 위해 실행할 명령을 수집해서 기록을 해두어야한다.
- 이를 위해
Command Encoder객체가 필요하다. - 렌더링을 위해 렌더 패스 객체를
Command Encoder객체를 통해 생성한다. - 화면에 그려질 프레임 버퍼 텍스처를 뷰에 지정한다.
loadOp은clearValue로 지정된 색상으로 프레임 버퍼를 초기화한다는 것을 의미하며,storeOp은 렌더패스가 끝나면 그 결과를 텍스쳐에 저장하라는 의미.- 렌더 패스가 사용할 파이프라인을 지정하고,
Model,View,Perspective행렬이 저장된 유니폼 데이터를 해당 렌더패스에 연결하고,
삼각형의 정점 데이터에 대한 데이터도 렌더패스에 연결한다. renderPass.end();까지가 GPU가 실행할 명령어를 수집하는 것- 수집된 커맨드 버퍼를 디바이스에 큐를 통하여 전달하면 삼각형이 등장한다.
이슈 : 삼각형이 안나오는 문제
- **상황** : 위의 과정을 모두 마쳤는데, **삼각형이 화면에 나타나지 않는 문제 발생**
- **원인**
- `HTML`에 `canvas`가 아닌 `div` 요소로 정의되어 있었음
- `const context = document.getElementById("gpuCanavs");`이 **`gpuCanavs`로 오타**가 있었음
- `const context = document.getElementById("gpuCanvas");` <br/>`const canvas = canvas.getContext("webgpu");` -> **`canvas`가 `context`로 오타**, **`canvas`와 `context`가 서로 바뀌어 있는 오타**.
- `@group(0) @binding(0) var<uniform> matrices : Matrices;` 인데 **`matrices`가 아니라 `mvp`로 오타**
- `struct VertexInput { @location(0) position : vec4<f32>, };` -> **버퍼는 `float32x3` 이므로 `position`이 `vec3<f32>`여야함**- 해결! 삼각형이 화면에 나타남

(참고) WebGPU의 구성요소
Command Encoder: GPU에 보낼 작업 내역들(Render Pass,Compute Pass)을 기록함Command Buffer:Command Encoder에 기록된 작업들을 GPU가 실제로 실행될 수 있는 형태로 만든 데이터Queue:Command Buffer에 저장된 실행 명령(Pass)을 GPU(정확히는 실행Pipeline)로 전달할 때 사용하는 통로
전체 코드
import "./style.css";
// Model 행렬을 정의하기 위한 함수
function mat4Identity() {
return new Float32Array([
1,
0,
0,
0, //
0,
1,
0,
0, //
0,
0,
1,
0, //
0,
0,
0,
1,
]);
}
// View 행렬을 정의하기 위한 함수
function mat4LookAt(eye, center, up) {
const ex = eye[0],
ey = eye[1],
ez = eye[2];
const cx = center[0],
cy = center[1],
cz = center[2];
let ux = up[0],
uy = up[1],
uz = up[2];
let fx = ex - cx,
fy = ey - cy,
fz = ez - cz;
let len = Math.hypot(fx, fy, fz);
fx /= len;
fy /= len;
fz /= len;
let rx = uy * fz - uz * fy,
ry = uz * fx - ux * fz,
rz = ux * fy - uy * fx;
len = Math.hypot(rx, ry, rz);
rx /= len;
ry /= len;
rz /= len;
ux = fy * rz - fz * ry;
uy = fz * rx - fx * rz;
uz = fx * ry - fy * rx;
return new Float32Array([
rx,
ux,
fx,
0, //
ry,
uy,
fy,
0, //
rz,
uz,
fz,
0, //
-(rx * ex + ry * ey + rz * ez),
-(ux * ex + uy * ey + uz * ez),
-(fx * ex + fy * ey + fz * ez),
1,
]);
}
// Perspective 행렬을 정의하기 위한 함수
function mat4Perspective(fovY, aspect, near, far) {
const f = 1.0 / Math.tan(fovY / 2);
const nf = 1 / (near - far);
return new Float32Array([
f / aspect,
0,
0,
0, //
0,
f,
0,
0, //
0,
0,
(far + near) * nf,
-1, //
0,
0,
2 * far * near * nf,
0,
]);
}
async function main() {
//1.WebGPU 지원 여부 검사
if (!navigator.gpu) {
console.error("WebGPU를 지원하지 않습니다.");
return;
}
//2. adapter와 device 얻기
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
console.error("적절한 GPU 어댑터를 찾을 수 없습니다.");
return;
}
const device = await adapter.requestDevice();
device.lost.then((info) => {
console.error("Device 소실 :", info.message, "| 원인 :", info.reason);
});
//3-1. Canvas와 context 설정
const canvas = document.getElementById("gpuCanvas");
const context = canvas.getContext("webgpu");
if (!context) {
console.error("캔버스로부터 WebGPU 컨텍스트를 얻을 수 없습니다.");
return;
}
//3-2 WebGPU와 호환되는 구성 세팅
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format, alphaMode: "opaque" });
//4-1. 삼각형의 3개의 정점 정의
const vertices = new Float32Array([
0.0,
0.6,
0.0, // 위쪽 정점
-0.5,
-0.4,
0.0, // 왼쪽 정점
0.5,
-0.4,
0.0, // 오른쪽 정점
]);
//4-2. GPU 메모리 생성
const vertexBuffer = device.createBuffer({
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
//4-2. CPU가 읽을 수 있는 메모리에서 GPU 메모리로 복사
device.queue.writeBuffer(vertexBuffer, 0, vertices);
const MATRIX_SIZE = 16 * 4;
const UNIFORN_SIZE = 3 * MATRIX_SIZE;
const uniformBuffer = device.createBuffer({
size: UNIFORN_SIZE,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
//5. 정의된 함수로 행렬 정의하기
const model = mat4Identity();
const view = mat4LookAt(
[0, 0, 2], // eye
[0, 0, 0], // center
[0, 1, 0], // up
);
const aspect = canvas.width / canvas.height;
const projection = mat4Perspective((60 * Math.PI) / 180, aspect, 0.1, 100);
// 6. 정의된 행렬을 GPU에 복사하기
device.queue.writeBuffer(uniformBuffer, 0, model);
device.queue.writeBuffer(uniformBuffer, MATRIX_SIZE, view);
device.queue.writeBuffer(uniformBuffer, 2 * MATRIX_SIZE, projection);
// 7. 셰이더 코드 정의하기
const VERTEX_SHADER = /*wgsl*/ `
struct Matrices { //GPU로 전달할 행렬들을 정의하는 구조체
model : mat4x4<f32>, // GPU로 전달한 Model 행렬
view : mat4x4<f32>, // GPU로 전달한 View 행렬
projection : mat4x4<f32>, // GPU로 전달한 Projection 행렬
};
// 이 구조체에 실제 데이터를 채우고 읽기 위해서 group과 binding을 설정
// 0번 그룹과 0번 바인딩을 통해서 행렬 데이터를 읽기 위해서 별도의 코드가 필요 -> 추후 다시 설명
@group(0) @binding(0) var<uniform> mvp : Matrices;
// 삼각형의 정점 데이터를 정의하는 구조체
// 0번 location을 사용하여 정점의 위치 데이터를 읽기 위해서 별도의 코드가 필요 -> 추후 다시 설명
struct VertexInput {
@location(0) position : vec3<f32>,
};
// 정점 셰이더의 최종 결과값에 대한 구조체
struct VertexOutput {
@builtin(position) clip_position : vec4<f32>,
};
@vertex
// 정점 셰이더의 메인 함수 인자로 정점 데이터를 읽기 위한 구조체를 받음
fn vs_main(in: VertexInput) -> VertexOutput {
var out : VertexOutput;
let mvp_matrix = mvp.projection * mvp.view * mvp.model; // 모델 변환, 뷰 변환, 프로젝션 변환에 대한 행렬을 만들고,
out.clip_position = mvp_matrix * vec4<f32>(in.position, 1.0); // 이 행렬에 정점의 로컬 좌표를 곱한 결과를 반환하고 있다.
return out;
}
`;
const FRAGMENT_SHADER = /*wgsl*/ `
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 1.0, 0.0, 1.0); // R: 1.0, G: 1.0, B: 0.0, A: 1.0 -> 노란색으로 출력
}
`;
// 8. Bind Group Layout (어떤 위치에 어떤 종류의 데이터가 들어오는지에 대한 설계도) 설정
const bindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0, // VERTEX_SHADER에서 bindding(0)에 대한 값과 일치해야함, 0번 바인딩에 대한 설정
visibility: GPUShaderStage.VERTEX, // 이렇게 지정하면 -> 여기서 바인딩된 데이터는 VERTEX_SHADER에서만 읽을 수 있다는 것을 의미.
buffer: { type: "uniform" },
// 이 데이터는 uniform이라는 의미,
// uniform 버퍼는 생성될 때 uniform 타입으로 생성되고 있으니까 , 타입을 맞춰야한다.
// -> usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
},
],
});
// 위 bindGroupLayout을 기반으로 실제 데이터를 바인딩하는 Bind Group 설정
const bindGroup = device.createBindGroup({
layout: bindGroupLayout, // 사용할 레이아웃은 위에서 정의한 bindGroupLayout
entries: [
{
binding: 0, // bindGroupLayout 의 entries의 첫번째 요소에 바인딩 되라는 의미
resource: { buffer: uniformBuffer }, // 실제 바인딩 되는 데이터는 uniformBuffer, 이 버퍼는 위에서 생성한 uniform 버퍼
},
],
});
// 9. Shader 컴파일
const vertexModule = device.createShaderModule({ code: VERTEX_SHADER });
const fragmentModule = device.createShaderModule({ code: FRAGMENT_SHADER });
const vertInfo = await vertexModule.getCompilationInfo();
vertInfo.messages.forEach((m) =>
console[m.type === "error" ? "error" : "warn"](`[Vertex shader] ${m.message}`),
);
const fragInfo = await fragmentModule.getCompilationInfo();
fragInfo.messages.forEach((m) =>
console[m.type === "error" ? "error" : "warn"](`[Fragment shader] ${m.message}`),
);
// 10. Render Pipeline (렌더링 과정에 대한 설계도) 설정
const pipeline = device.createRenderPipeline({
layout: device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }), // 사용할 Bind Group Layout은 위에서 정의한 bindGroupLayout
vertex: {
module: vertexModule, // 사용할 Vertex Shader는 위에서 컴파일한 vertexModule
entryPoint: "vs_main", // Vertex Shader의 진입점은 vs_main 함수
buffers: [
{
arrayStride: 3 * 4, // 정점 데이터의 한 요소가 차지하는 바이트 수, 여기서는 position이 vec3<f32>이니까 3개의 float, 각 float는 4바이트 -> 3 * 4
attributes: [
{
shaderLocation: 0, // 쉐이더 location(0)과 연결
offset: 0, // position 데이터는 정점 데이터의 시작 부분에 위치하니까 offset은 0
format: "float32x3", // position 데이터는 vec3<f32>이니까 float 3개짜리 데이터
},
],
},
],
},
fragment: {
module: fragmentModule, // 사용할 Fragment Shader는 위에서 컴파일한 fragmentModule
entryPoint: "fs_main", // Fragment Shader의 진입점은 fs_main 함수
targets: [
// 렌더링 결과가 출력될 대상에 대한 설정
{
format, // 출력 대상의 포맷은 위에서 설정한 format, 여기서는 navigator.gpu.getPreferredCanvasFormat()로 얻은 값
},
],
},
primitive: {
topology: "triangle-list", // 렌더링할 도형의 유형, 여기서는 정점 3개를 묶어 삼각형 1개를 그리는 방식
},
});
// 11. Command Encoder와 Render Pass Encoder를 사용하여 실제 렌더링 명령어 작성
function render() {
const commandEncoder = device.createCommandEncoder(); // 명령어를 기록하기 위한 Command Encoder 생성
const renderPass = commandEncoder.beginRenderPass({
colorAttachments: [
{
view: context.getCurrentTexture().createView(), // 렌더링 결과가 출력될 대상, 여기서는 캔버스의 현재 텍스처 뷰
clearValue: { r: 0.05, g: 0.05, b: 0.08, a: 1.0 }, // 초기화할 색상, 여기서는 검은색
loadOp: "clear", // 렌더링 시작 전에 이 대상에 대한 작업, 여기서는 clear -> 렌더링 시작 전에 이 대상이 지정된 색상으로 초기화됨
storeOp: "store", // 렌더링이 끝난 후 이 대상에 대한 작업, 여기서는 store -> 렌더링 결과가 이 대상에 저장됨
},
],
});
// 위에서 설정한 Render Pipeline을 사용하겠다는 의미
renderPass.setPipeline(pipeline);
// 위에서 설정한 Bind Group을 0번 그룹으로 바인딩,
// 이로써 셰이더에서 @group(0) @binding(0)으로 정의된 uniform 데이터가 이 bindGroup의 uniformBuffer를 참조하게 됨
renderPass.setBindGroup(0, bindGroup);
renderPass.setVertexBuffer(0, vertexBuffer);
renderPass.draw(3); // 정점 3개를 사용해서 1개의 인스턴스를 그리겠다는 의미
renderPass.end(); // Render Pass 종료
const commandBuffer = commandEncoder.finish(); // 명령어 기록 종료, Command Buffer 생성
device.queue.submit([commandBuffer]); // Command Buffer를 GPU에 제출하여 실행
}
render();
}
main();WebGPU에서 중요한 장점
Pipeline을 통한 사전 준비 가능
const pipeline = device.createRenderPipeline({
// 렌더링 과정에 대한 설계도
});WebGL은 상태 관리 방식 : 렌더링 과정에서 필요한 설정들을 렌더링 명령어를 실행하기 전에 여러가지 상태를 매번 새롭게 지정하여 렌더링하는 방식- 지정된 상태가 올바른지 매 프레임마다 CPU가 검사해야함.
WebGPU는 파이프라인 방식 : 렌더링 과정에 필요한 설정들을 미리 무엇을 렌더링할지를 정의하고,
즉 파이프라인 객체로 만들어서 GPU에게 전달하는 방식, 미리 정의해 검증된 상태로 CPU에 추가적인 부담을 주지 않는다.
- 멀티스레드(
Worker)를 통한 최적화 작업이 가능하다.
const commandBuffer = commandEncoder.finish();
device.queue.submit([commandBuffer]);- 코드를 보면 메인 쓰레드에서
commandBuffer를 한 개만 만들었지만,commandBuffer를 별도의 쓰레드에서 만들어서 메인 쓰레드로 전달할 수 있다.
메인 쓰레드에서 디바이스의 큐를 통해 워커에서 전달한commandBuffer를 배열 형태로 전달해서 실행할 수 있다.WebGPU는 배열로 전달된 커맨드 버퍼를 이 배열의 순서에 맞게 실행할 수 있도록 보장한다.
- 3D 그래픽 뿐만 아니라
GPGPU이용
WebGL도 가능했지만 코드 작성이 매우 비효율적이고, 직관적이지 못한 방식이었다.WebGPU는Compute Shader를 이용해서GPGPU를 정식 지원한다.
WebGPU에 대한 three.js 추상화
WebGPU를 추상화한three.js의 기본 구성 요소 및 APIthree.js의WebGPURenderer는WebGPU API를 추상화하여 개발자가 더 쉽게 3D 그래픽을 렌더링할 수 있도록 도와준다.WebGPURenderer는WebGPU의 복잡한 설정과 관리를 내부적으로 처리하여,
개발자는three.js의 친숙한 API를 사용하여 3D 씬을 구성하고 렌더링할 수 있다.THREE.WebGPURendererWebGPU와canvas를 초기화 해주는 부분이며 렌더 파이프라인을 생성해주는 코드,
그리고command encoder를 생성하고command buffer를 관리하고 실제 렌더링까지 해주는 부분을 담당한다.
THREE.Geometry- 정점 데이터를 정의하고 GPU에 버퍼를 생성해서 구성해주는 부분,
즉 3D 모델의 정점, 법선, 텍스처 좌표 등의 데이터를 관리하는 부분
- 정점 데이터를 정의하고 GPU에 버퍼를 생성해서 구성해주는 부분,
THREE.Material- 쉐이더 코드에 대한 부분, 그리고 **유니폼 데이터(바인드 데이터)**에 대한 코드도 해당된다.
THREE.Mesh- 전체 코드에서 정확히 짚을 수는 없지만,
Geometry와Material을 조합해서 실제로 효과적으로 관리할 수 있도록 한다.
- 전체 코드에서 정확히 짚을 수는 없지만,
THREE.Scene- 여러 개의 매시를 효과적으로 관리할 수 있는 클래스
THREE.CameraView변환과Projection변환에 대한 부분을 담당한다.
그리고 해당 행렬을 생성하기 위한mat4LookAt과mat4Perspective와 같은 함수도Camera클래스에서 제공한다.
THREE.Light- 빛의 위치, 색상, 세기 등을 정의하는 클래스, 그리고 쉐이더에서 빛의 영향을 계산하기 위한 코드도 포함한다.
(해당 코드에선 명확하지 않으나 작성된다면FRAGMENT_SHADER에서 빛의 영향을 계산하는 코드가 추가될 것이다.)
- 빛의 위치, 색상, 세기 등을 정의하는 클래스, 그리고 쉐이더에서 빛의 영향을 계산하기 위한 코드도 포함한다.
THREE.MathUtils- 3차원 좌표와 행렬을 효과적으로 표현할 때 사용, 수학적 연산에 대한 다양한 함수를 제공,
코드에서는 단위 행렬을 얻는mat4Identity와 같은 함수가 이에 해당한다.
- 3차원 좌표와 행렬을 효과적으로 표현할 때 사용, 수학적 연산에 대한 다양한 함수를 제공,
결론
WebGPU에 대한 Three.js의 추상화는 복잡한 WebGPU API를 간단하고 직관적인 인터페이스로 감싸서 개발자가 3D 그래픽을 쉽게 렌더링할 수 있도록 도와준다.
하지만 WebGPU를 통해 기초부터 만들어보면서 내부를 이해한다면, 수학적인 내용과 쉐이더에 대해 더 알면 알수록,더 멋진 효과와 장면을 만들 수 있다고 합니다!