DataTexture 를 이용한 GPGPU 구현, THREE.JS_02

P5js
Art & tech / 아트와 테크로 미래를 보다
8 min readJun 23, 2024

데이터 텍스쳐를 만든 과정에 이어서 이제 활용하는 방법을 알아 보겠습니다.
먼저 이전에는 planeGeometry를 사용하였지만 이제 BufferGeometry로 바꾸어 줍니다.

버퍼지오메트리란?

BufferGeometry는 Three.js에서 임의의 형태를 직접 정의하고 생성할 수 있는 클래스입니다. 정점 데이터를 연속적인 버퍼에 저장하여 GPU로 전송해 메모리 효율성과 렌더링 성능이 뛰어납니다. 위치, 법선, 색상, UV 좌표 등의 정점 속성을 관리하며, 복잡한 3D 객체도 유연하게 생성할 수 있습니다.

const addobject = () => {

// 플랜지오메트리에서 버퍼지오메트리로 변환
const size = 256
const number = size * size // 높이와 넓이 만큼의 픽셀 개수
const geometry = new THREE.BufferGeometry()

const bufferPositions = new Float32Array( 3 * number) //버퍼지오메트리의 포지션을 구현하기위한 배열
const bufferUv = new Float32Array( 2 * number) //버퍼지오메트리의 유브이를 구현하기 위한 배열

for(let i=0; i < size; i++){
for(let j=0; j < size; j++){

const index = i * size + j

bufferPositions[ index * 3 ] = j / size - 0.5 //size로 나누어 0에서 1 사이의 값을 만들고, 여기에 -0.5, 이 범위 조정은 격자의 중심을 (0, 0, 0)으로 맞추기 위해 필요
bufferPositions[ index * 3 + 1 ] = i / size - 0.5 //size로 나누어 0에서 1 사이의 값을 만들고, 여기에 -0.5
bufferPositions[ index * 3 + 2 ] = 0

bufferUv[ index * 2 ] = j / (size-1) // size-1을 한 이유는 size가 256이면 인덱스는 0부터 255까지입니다. size - 1로 나누어 정확히 0부터 1까지의 범위를 갖도록 함 j가255 이면 size는 256 -1 = 255 로 딱 떨어짐
bufferUv[ index * 2 + 1 ] = i / (size-1)//UV 좌표를 size - 1로 나누는 이유는 텍스처의 가장자리가 정확히 1에 대응되도록 하여, 텍스처가 3D 모델에 올바르게 매핑되도록 하기 위함입니다.
}

geometry.setAttribute('position', new THREE.BufferAttribute(bufferPositions, 3))
geometry.setAttribute('uv', new THREE.BufferAttribute(bufferUv, 2))
//setAttribute 로 버퍼지오메트리의 속성을 추가하거나 업뎃할때 사용 -> geometry.setAttribute(name, attribute);
//BufferAttribute 이 클래스는 버텍스 데이터(위치, 법선, 색상, UV 좌표 등)를 효율적으로 관리하고 GPU로 전송할 수 있도록 도와줍니다.
//array: 속성 데이터를 포함하는 배열입니다. Float32Array, Uint16Array 등 다양한 타입의 배열을 사용할 수 있습니다.
//itemSize: 각 버텍스 당 데이터의 요소 개수입니다. 예를 들어, 위치는 3 (X, Y, Z), UV 좌표는 2 (U, V)입니다.
//normalized: (선택적) 데이터가 정규화되어 있는지 여부입니다. 기본값은 false입니다.
//여기까지 버퍼지오메트리 구현
//아래는데이터 텍스쳐 구현
const data = new Float32Array( 4 * number) // 각 픽셀에 r,g,b,a 혹은 x,y,z,w 만큰 4개의 데이터가 담길수 있다는 것을 의미합니다.->const attribute = new THREE.BufferAttribute(array, itemSize, normalized);

for(let i=0; i < size; i++){
for(let j=0; j < size; j++){

const index = i * size + j

data[ index * 4 ] = Math.random()
data[ index * 4 + 1 ] = Math.random()
data[ index * 4 + 2 ] = 0
data[ index * 4 + 3 ] = 1

}
}

positions = new THREE.DataTexture(data, size, size, THREE.RGBAFormat, THREE.FloatType)
positions.needsUpdate = true

material = new THREE.ShaderMaterial({
uniforms:{
time:{value:0},
uTexture:{value: positions}
},
vertexShader: vertexShader,
fragmentShader: fragmentShader
})
const mesh = new THREE.Points(geometry, material)
scene.add(mesh)

}

bufferPosition(포지션값) 과 bufferUv(uv값) 을 만들어 버퍼지오메트리를 구성하면 이전 플랜지오메트리로 구현하였던 것과 같은 결과물을 확인할 수 있습니다.

이제 버퍼지오메트리를 만들었고 이것을 바로 GPGPU로 활용하지 않고 버텍스쉐이더에서 확인해보겠습니다.

varying vec2 vUv;
uniform float time;
uniform sampler2D uTexture;//데이터텍스쳐를 버텍스쉐이더에 직접 넣어봄

void main(){

vUv = uv;

vec3 newPos = position;
vec4 color = texture2D(uTexture, vUv);
newPos.xy = color.xy;

vec4 mvPosition = modelViewMatrix * vec4(newPos, 1.0);

gl_PointSize = 40.0 / -mvPosition.z;
gl_Position = projectionMatrix * mvPosition;
}

이렇게 버텍스 쉐이더에서 데이터 텍스쳐값을 포지션에 대입하게 되면
(좀더 결과물을 확실히 보기위하여 const size = 24, 사이즈 값을 24로 줄여서테스트하였습니다.) 아래와 같이 랜덤한 포지션 값이 대입되게 됩니다. 어찌되었든 데이터 텍스쳐 값이 버텍스 쉐이더의 포지션값으로 전달 된 것은 맞습니다.
하지만 GPGPU를 활용한 방식은 아니고 단지 테스트를 해본것이죠

자 이제는 포지션값을 한번 넣는 것이 아닌 계속해서 업데이트 되는 포지션값을 적용해보겠습니다. 그리고 이때 GPGPU의 파워를 느낄수 있는 것 같습니다.
그러기 위해서는 먼저 FBO와 핑퐁버퍼를 이용하여야 합니다.

FBO란?
FBO는 “Frame Buffer Object”의 약자입니다. FBO는 오프스크린 렌더링을 가능하게 하는 도구입니다. 즉, 우리가 일반적으로 화면에 직접 그리는 대신, 메모리 상의 프레임 버퍼에 렌더링할 수 있게 합니다. 이렇게 하면 다양한 후처리 효과나 복잡한 계산을 수행할 수 있습니다.

Ping-Pong Buffer 핑퐁버퍼란?
Ping-Pong Buffer (핑퐁 버퍼)는 두 개의 FBO를 번갈아 가며 사용하는 기술입니다. 이는 반복적인 렌더링 작업에서 주로 사용되며, 특히 각 단계의 결과를 다음 단계의 입력으로 사용하는 경우에 유용합니다.

FBO와 핑퐁버퍼를 사용하는 이유는 업데이트 되는 데이터를 화면에 보이지 않고 메모리상에서만 그린 후 핑퐁버퍼를 통해 계속 해서 업데이트 시키기 위함입니다.
그렇게 된다면 데이터 텍스쳐에 있는 무수한 각각의 데이터들이 계속해서 업데이트 될수 있을 것입니다.

자세히 쓰다 보니 글이 길어졌는데요 다음 글에서 실제 FBO구현을 적용해 보겠습니다

--

--