Canvas를 Grid로 나눠보자.
지난번에는 사각형을 그리는 파틀르 진행했었다.
이번에는 Uniform 이란걸 이용하여, canvas를 그리드로 나눠 볼건데, 유니폼은 모든 호출에서 동일한 버퍼의 값 이라고 한다.
기하학적 요소나 애니메이션 처리 등 공통된 값을 전달한느데 유용하다고 하는데, 코드를 통해 알아보자.
일단, 기존 drawRect 함수를 일부 수정하였고, 아래는 그 전문이다.
drawRect() {
// 삼각형 두개로, 정사각형의 좌표 지정.
const vertices = new Float32Array([
// X, Y,
-0.8, -0.8, // Triangle 1 (Blue)
0.8, -0.8,
0.8, 0.8,
-0.8, -0.8, // Triangle 2 (Red)
0.8, 0.8,
-0.8, 0.8,
])
// GPU 측 메모리는 GPUBuffer를 통해 관리 됨.
const vertexBuffer = this.device.createBuffer({
label: "Cell vertices",
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
})
this.device.queue.writeBuffer(vertexBuffer, 0, vertices);
const vertexBufferLayout: GPUVertexBufferLayout = {
arrayStride: 8, // byte 단위. GPU에서 다음 꼭짓점을 찾을 때 버퍼에서 앞으로 건너뛰어야 하는 바이트 수. Float32Array를 이용하여, 각 꼭지점은 32bit + 32bit, 8byte로 구현
// 따라서 다음 꼭지점을 찾으려면 8byte 뒤의 버퍼를 찾아야 하기 때문에 8으로 초기화 해준다.
attributes: [{
format: "float32x2", //float32, 2개의 값을 이용한다는 정의
offset: 0, // 어디서 부터 버퍼를 읽을지에 대한 설정. 지금은 사각형 하나만 그릴 것이기 때문에 0이 입력된다. 두개 이상이 있을 때는 이전 버퍼의 합을 넣어주면 된다.
shaderLocation: 0, // Position, see vertex shader
}],
};
// GRID 부분 변경
const cellShaderModule = this.device.createShaderModule({
label: 'Cell shader',
code: `
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f,
@builtin(instance_index) instance: u32) ->
@builtin(position) vec4f {
let i = f32(instance);
// Compute the cell coordinate from the instance_index
let cell = vec2f(i % grid.x, floor(i / grid.x));
let cellOffset = cell / grid * 2;
let gridPos = (pos + 1) / grid - 1 + cellOffset;
return vec4f(gridPos, 0, 1);
}
@fragment
fn fragmentMain() -> @location(0) vec4f {
return vec4f(1, 0, 0, 1);
}
`
});
const cellPipeline = this.device.createRenderPipeline({
label: "Cell pipeline",
layout: "auto",
vertex: {
module: cellShaderModule,
entryPoint: "vertexMain",
buffers: [vertexBufferLayout]
},
fragment: {
module: cellShaderModule,
entryPoint: "fragmentMain",
targets: [{
format: this.canvasFormat
}]
}
});
// 뭔가 새로 그리려면, beginRenderPass가 시작되고, pass.end가 호출 되어야 함
this.pass = this.encoder.beginRenderPass({
colorAttachments: [{
view: this.context.getCurrentTexture().createView(),
loadOp: "clear",
clearValue: [0, 0, 0.4, 1],
storeOp: "store",
}]
})
// Grid 관련 처리
const GRID_SIZE = 32
const uniformArary = new Float32Array([GRID_SIZE, GRID_SIZE])
const uniformBuffer = this.device.createBuffer({
label: "Grid Uniforms",
size: uniformArary.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
})
this.device.queue.writeBuffer(uniformBuffer, 0, uniformArary)
// 바인드 그룹 만들기
const bindGroup = this.device.createBindGroup({
label: "Cell renderer bind group",
layout: cellPipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}]
})
// 그리는 작업을 이 안에서 처리한다.
this.pass.setPipeline(cellPipeline)
this.pass.setVertexBuffer(0, vertexBuffer)
this.pass.setBindGroup(0, bindGroup) // Grid 추가
this.pass.draw(vertices.length / 2, GRID_SIZE * GRID_SIZE);
this.pass.end()
// 명령 버퍼 제출
this.device.queue.submit([this.encoder.finish()])
}
조금씩 뜯어보면서 알아보도록 하자.
uniformBuffer
const GRID_SIZE = 32
const uniformArary = new Float32Array([GRID_SIZE, GRID_SIZE])
const uniformBuffer = this.device.createBuffer({
label: "Grid Uniforms",
size: uniformArary.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
})
this.device.queue.writeBuffer(uniformBuffer, 0, uniformArary)
먼저 위 코드를 살펴보면,
Float32Array 형태로 32*32 짜리 배열을 생성해 주었고, 버퍼를 만들고, 디바이스에 버퍼를 넣어주었다.
버퍼를 생성하고 넣어주는 부분은 지난번에 정사각형을 그리는 파트와 동일하기 때문에 설명은 넘어가고, usage 부분을 보면
GPUBufferUsage.VERTEX 가 GPUBufferUsage.UNIFORM로 대체 되었다는걸 알 수 있다.
해당 부분만 변경해주면 vertex buffer 대신 uniformBuffer가 생성된다.
createShaderModule -> code 수정
createShaderModule에서 code 부분이 수정되었다.
튜토리얼 단계를 밟아가다 보면 많은 단계별로 설명이 잘 되어있는데, 일단 최종 데이터를 확인해보면 다음과 같다.
const cellShaderModule = this.device.createShaderModule({
label: 'Cell shader',
code: `
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f,
@builtin(instance_index) instance: u32) ->
@builtin(position) vec4f {
let i = f32(instance);
// Compute the cell coordinate from the instance_index
let cell = vec2f(i % grid.x, floor(i / grid.x));
let cellOffset = cell / grid * 2;
let gridPos = (pos + 1) / grid - 1 + cellOffset;
return vec4f(gridPos, 0, 1);
}
@fragment
fn fragmentMain() -> @location(0) vec4f {
return vec4f(1, 0, 0, 1);
}
`
});
@group(0) @binding(0) var<uniform> grid: vec2f 부분이 추가되었고,
@vertex 안쪽이 조금 수정되었다.
유니폼이 @group(0)과 @binding(0)에 바인딩 되도록 지정하고 grid 라고 하는 균일성을 정의한다고 나와있는데, 해당부분은 아래 createBindGroup 쪽에서 조금 더 알아보도록 하고 넘어가자.
- @group(0)은 그룹 인덱스를 지정하는데, 이 경우에는 0번 그룹을 나타냅니다.
- @binding(0)은 바인딩 인덱스를 지정하는데, 이 경우에는 0번 바인딩을 나타냅니다. 바인딩 인덱스는 Bind Group에 할당된 Uniform 변수와 매핑됩니다.
@vertex 부분은 로직적으로 수정된 부분들이 많이 보인다.
이부분의 분석 전에 먼저 바인드 그룹을 만들고, draw 부분에 그리는 부분을 추가해보자.
createBindGroup & draw
const bindGroup = this.device.createBindGroup({
label: "Cell renderer bind group",
layout: cellPipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}],
});
먼저, 셰이더에서 유니폼을 선언하더라도 바인딩 그룹을 만들고 연결을 해줘야 한다.
바인딩 그룹은 세이더에 동시에 엑세스할 수 있게 하는 리소스의 모음이다.
layout 부분은 보면 cellPipeline.getbindGroupLayout(0)를 넘겨준다.
파이프라인의 바인딩 그룹 레이아웃은 가져오는 부분이고,
아까 @group(0)이 getBindGroupLayout(0) 에 해당한다.
this.pass.setPipeline(cellPipeline)
this.pass.setVertexBuffer(0, vertexBuffer)
this.pass.setBindGroup(0, bindGroup) // Grid 추가
this.pass.draw(vertices.length / 2, GRID_SIZE * GRID_SIZE);
this.pass.end()
setBindGroup은 pass에 bindgroup를 set 해 주는 것이고, 첫번째 0은 bindGroup의 index값을 의미한다.
@vertex 수정 내용
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
let cell = vec2f(1, 1);
let cellOffset = cell / grid * 2; // Updated
let gridPos = (pos + 1) / grid - 1 + cellOffset;
return vec4f(gridPos, 0, 1);
}
먼저 최종 코드 전에 위 코드를 분석해 보자
vec2f 형태에 +1를 넣게 되면, 다음과 같은 연산이 이루어 진다.
(x + 1, y + 1)
cell의 좌표값을 (1,1)로 놓고, cellOffset = cell / grid * 2 로 지정되어있는데
이 때 grid는 (위에서는 32로 지정해 두었지만) 4라고 가정한다면
cellOffset 값은 (1/4*2, 1/4*2) 이므로 (0.5, 0.5)가 되겠다.
아래 gridPos의 값은 pos값을 계산하기 쉽게, 1,1 이라고 가정한다면,
(1+1, 1+1) / grid(4) - 1 + (0.5, 0.5) 형태가 된다. 이는 계산해 보면,
(0,0) 값이 나오게 된다.
기존 pos 값을 0.8 ~ -0.8 로 처리했었는데, 이에 따른 6개의 좌표값을 전부 계산해 보면 아래와 같은 그림이 나오게 된다.
그 다음으로 최종코드를 확인해 보자.
@vertex
fn vertexMain(@location(0) pos: vec2f,
@builtin(instance_index) instance: u32) ->
@builtin(position) vec4f {
let i = f32(instance);
// Compute the cell coordinate from the instance_index
let cell = vec2f(i % grid.x, floor(i / grid.x));
let cellOffset = cell / grid * 2;
let gridPos = (pos + 1) / grid - 1 + cellOffset;
return vec4f(gridPos, 0, 1);
}
grid가 4*4 라는 가정하에, instance_index는 0부터 15까지의 값을 가지게 된다. (최종 코드에서는 32*32 이므로 0부터 1023 까지의 값을 가짐)
grid.x의 값은 4 고정값 이므로, cell의 값은 다음과 같이 계산되겠다.
(1%4, floor(1/4))
따라서, (0,0) ~ (3,3) 까지의 값을 가지게 되므로 각 셀에 해당하는 offset값을 가진다.
예를 들어 현재의 cell 값이 (0,0) 이라고 가정한다면
gridPos의 값은 다음과 같이 연산될 수 있겠다.
((0.8 + 1)/4 - 1, (0.8 + 1)/4 -1) + (0, 0 ) (각 pos 에 따른 계산, 6번 반복)
즉 각 셀마다 사각형의 여섯 꼭지점 값을 반환하게 된다.
32*32의 결과물은 아래와 같다.
'JAVASCRIPT > vanilla' 카테고리의 다른 글
[javascript] DOM 변화를 관측하여 observer 패턴 사용 (0) | 2023.11.02 |
---|---|
WebGPU(4) - cell 색 지정 (1) | 2023.05.17 |
WebGPU(2) - draw rect (0) | 2023.05.16 |
WebGPU(1) - 개념과 캔버스 초기화 (1) | 2023.05.16 |