JJONG`sFACTORY
반응형

 

Canvas를 Grid로 나눠보자.

지난번에는 사각형을 그리는 파틀르 진행했었다.

이번에는 Uniform 이란걸 이용하여, canvas를 그리드로 나눠 볼건데, 유니폼은 모든 호출에서 동일한 버퍼의 값 이라고 한다.

기하학적 요소나 애니메이션 처리 등 공통된 값을 전달한느데 유용하다고 하는데, 코드를 통해 알아보자.

 

일단, 기존 drawRect 함수를 일부 수정하였고, 아래는 그 전문이다.

javascript
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

javascript
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 부분이 수정되었다.

튜토리얼 단계를 밟아가다 보면 많은 단계별로 설명이 잘 되어있는데, 일단 최종 데이터를 확인해보면 다음과 같다.

javascript
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

javascript
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) 에 해당한다.

 

javascript
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 수정 내용

javascript
@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개의 좌표값을 전부 계산해 보면 아래와 같은 그림이 나오게 된다.

그 다음으로 최종코드를 확인해 보자.

javascript
@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의 결과물은 아래와 같다.

반응형