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

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

이제 드디어 FBO와 핑퐁버퍼를 적용해 보겠습니다.
기존의 버텍스쉐이더에서 newPos.x += 0.1; 이라고 적용해도 아마 newPos.x 의 전체 값이 0.1만큼만 이동하고 더이상의 변화는 없을 것입니다. 그것은 데이터가 업데이트 되고 있지 않다는 뜻이지요. 그래서 아래와 같이 FBO환경을 구성합니다.

const setupFBO = () => {
//create FBO Scene
sceneFBO = new THREE.Scene()
cameraFBO = new THREE.OrthographicCamera(-1,1,1,-1,0,1)
cameraFBO.position.z = 1
cameraFBO.lookAt(new THREE.Vector3(0,0,0))

let geo = new THREE.PlaneGeometry(2,2,2,2)
simMaterial = new THREE.MeshBasicMaterial({
color:0xff0000
})
// simMaterial = new THREE.ShaderMaterial({
// uniforms:{
// time:{value:0},
// uTexture:{value:positions}
// },
// vertexShader : simVertexShader,
// fragmentShader : simFragmentShader
// })

const simMesh = new THREE.Mesh(geo,simMaterial)
sceneFBO.add(simMesh)
}

......
setupFBO();
// renderer.render(scene, camera)
renderer.render(sceneFBO, cameraFBO)
//렌더 쪽에서 scene, camera대신 sceneFBO, cameraFBO 로 적용해서 테스트한다.

이렇게 구현한 후 renderer.render(sceneFBO, cameraFBO)이렇게 테스트해보면 화면 전체가 레드로 보이게 됩니다.

레드로 보이면 잘되고 있는 중…
const setupFBO = () => {

const size = 32
const number = size * size

//create dataTexture
const data = new Float32Array( 4 * number) // 각 픽셀에 r,g,b,a 혹은 x,y,z,w 만큰 4개의 데이터가 담길수 있다는 것을 의미합니다.

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

//create FBO Scene
sceneFBO = new THREE.Scene()
cameraFBO = new THREE.OrthographicCamera(-1,1,1,-1,0,1)
cameraFBO.position.z = 1
cameraFBO.lookAt(new THREE.Vector3(0,0,0))

let geo = new THREE.PlaneGeometry(2,2,2,2)
simMaterial = new THREE.ShaderMaterial({
uniforms:{
time:{value:0},
uTexture:{value:positions}
},
vertexShader : simVertexShader,
fragmentShader : simFragmentShader
})

const simMesh = new THREE.Mesh(geo,simMaterial)
sceneFBO.add(simMesh)
}


const addobject = () => {

const size = 512
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)
bufferUv[ index * 2 + 1 ] = i / (size-1)
}
}

geometry.setAttribute('position', new THREE.BufferAttribute(bufferPositions, 3))
geometry.setAttribute('uv', new THREE.BufferAttribute(bufferUv, 2))


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)
}
...........
//simFragmentShader 코드
varying vec2 vUv;
uniform sampler2D uTexture;

void main(){

vec4 position = texture2D(uTexture, vUv);
gl_FragColor = position;
}

그리고 이전에 addobject()에서 만들었던 데이터 텍스쳐를 setupFBO()로 옮기고
simFragmentShader 에 uTexture(데이터 텍스쳐)를 적용시키면 기대한것과 같이 랜덤 값이 아래와 같이 표현됩니다.

이제 FBO에 데이터가 잘 전달되는지 확인하였고, 아래와 같이 두개의 렌더 타겟을 만듭니다. 그리고 렌더 부분에서 두개의 렌더 타겟을 번갈아 가면서(핑퐁 해 가면서)렌더 하도록 설정해줍니다.(이 부분이 핑퐁 버퍼입니다.)

const setupFBO = () => {

const size = 32
const number = size * size

//create dataTexture
const data = new Float32Array( 4 * number) // 각 픽셀에 r,g,b,a 혹은 x,y,z,w 만큰 4개의 데이터가 담길수 있다는 것을 의미합니다.

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

//create FBO Scene
sceneFBO = new THREE.Scene()
cameraFBO = new THREE.OrthographicCamera(-1,1,1,-1,0,1)
cameraFBO.position.z = 1
cameraFBO.lookAt(new THREE.Vector3(0,0,0))

let geo = new THREE.PlaneGeometry(2,2,2,2)
simMaterial = new THREE.ShaderMaterial({
uniforms:{
time:{value:0},
uTexture:{value:positions}
},
vertexShader : simVertexShader,
fragmentShader : simFragmentShader
})

const simMesh = new THREE.Mesh(geo,simMaterial)
sceneFBO.add(simMesh)

//create rendertarget, 핑퐁 버퍼를 활용할 렌더 타겟을 2개 만들어줌
renderTarget = new THREE.WebGLRenderTarget(size, size, {
minFilter: THREE.NearestFilter,
magFilter: THREE.NearestFilter,
format: THREE.RGBAFormat,
type: THREE.FloatType
})

renderTarget1 = new THREE.WebGLRenderTarget(size, size, {
minFilter: THREE.NearestFilter,
magFilter: THREE.NearestFilter,
format: THREE.RGBAFormat,
type: THREE.FloatType
})

}

const tick = () => {
const elapsedTime = clock.getElapsedTime()
const deltaTime = elapsedTime - previousTime
previousTime = elapsedTime
controls.update()

// renderer.render(scene, camera)
renderer.setRenderTarget(renderTarget)
renderer.render(sceneFBO, cameraFBO)
//sceneFBO와 cameraFBO를 사용하여 renderTarget에 렌더링합니다.
//이 과정에서 sceneFBO의 결과가 renderTarget 텍스처에 저장됩니다.


renderer.setRenderTarget(null)//렌더 타겟을 기본 화면으로 돌려놓습니다.
renderer.render(scene, camera)//scene을 화면에 렌더링합니다.


//renderTarget과 renderTarget1을 스왑하여,
//다음 프레임에서 사용할 렌더 타겟을 교체합니다.
//이렇게 하면 두 개의 렌더 타겟을 번갈아 사용하게 됩니다.

const temp = renderTarget
renderTarget = renderTarget1
renderTarget1 = temp

//material과 simMaterial의 uTexture 유니폼을 각각 최신 렌더 타겟의 텍스처로
//업데이트합니다. 이를 통해 다음 렌더링 단계에서 최신 텍스처 데이터를 사용할 수 있습니다.
material.uniforms.uTexture.value = renderTarget.texture
simMaterial.uniforms.uTexture.value = renderTarget1.texture

window.requestAnimationFrame(tick)
}

setupFBO()
addobject()
tick()

이렇게 설정하면 일단 기본적인 FBO와 핑퐁 버퍼가 구현이 됩니다.
그후 simFragmentShader 에서 position.x += 0.001;을 아래와 같이 적용해 주면

varying vec2 vUv;
uniform sampler2D uTexture;

void main(){

vec4 position = texture2D(uTexture, vUv);
position.x += 0.001;//업데이트 되는 부분
gl_FragColor = position;
}

좀 복잡하긴 했지만 일단 GPGPU를 이용한 간단한 움직임 구현이 완료 되었습니다.
참조를 위하여 전체 코드를 공유합니다.

import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
import vertexShader from '../src/shaders/pingpong/vertex.glsl'
import fragmentShader from '../src/shaders/pingpong/fragment.glsl'
import simVertexShader from '../src/shaders/pingpong/simvertex.glsl'
import simFragmentShader from '../src/shaders/pingpong/simfragment.glsl'
import GUI from 'lil-gui'


let material;
let time = 0;
let texLoader = new THREE.TextureLoader
let tex = texLoader.load('./test.jpg')
let sceneFBO;
let cameraFBO;
let positions;
let renderTarget, renderTarget1, simMaterial;
// console.log(tex)
/**
* Base
*/
// Debug
const gui = new GUI({ width: 340 })
const debugObject = {}

//canvas
const canvas = window.document.querySelector('canvas.webgl')

//scene
const scene = new THREE.Scene()

//loader
// Loaders
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/draco/')

const gltfLoader = new GLTFLoader()
gltfLoader.setDRACOLoader(dracoLoader)

//sizes
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
pixelRatio: Math.min(window.devicePixelRatio, 2)
}

window.addEventListener('resize',()=>{
// Update sizes
sizes.width = window.innerWidth
sizes.height = window.innerHeight
sizes.pixelRatio = Math.min(window.devicePixelRatio, 2)

// Update camera
camera.aspect = sizes.width / sizes.height
camera.updateProjectionMatrix()

// Update renderer
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(sizes.pixelRatio)
})


/**
* camera
*/
//basecamera
const camera = new THREE.PerspectiveCamera(35, sizes.width / sizes.height, 0.1, 100)
camera.position.set(0, 0, 3)
scene.add(camera)

// Controls
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true

const setupFBO = () => {

const size = 32
const number = size * size

//create dataTexture
const data = new Float32Array( 4 * number) // 각 픽셀에 r,g,b,a 혹은 x,y,z,w 만큰 4개의 데이터가 담길수 있다는 것을 의미합니다.

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

const index = i * size + j

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

}
}

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

//create FBO Scene
sceneFBO = new THREE.Scene()
cameraFBO = new THREE.OrthographicCamera(-1,1,1,-1,0,1)
cameraFBO.position.z = 1
cameraFBO.lookAt(new THREE.Vector3(0,0,0))

let geo = new THREE.PlaneGeometry(2,2,2,2)
simMaterial = new THREE.ShaderMaterial({
uniforms:{
time:{value:0},
uTexture:{value:positions}
},
vertexShader : simVertexShader,
fragmentShader : simFragmentShader
})

const simMesh = new THREE.Mesh(geo,simMaterial)
sceneFBO.add(simMesh)

//create rendertarget
renderTarget = new THREE.WebGLRenderTarget(size, size, {
minFilter: THREE.NearestFilter,
magFilter: THREE.NearestFilter,
format: THREE.RGBAFormat,
type: THREE.FloatType
})

renderTarget1 = new THREE.WebGLRenderTarget(size, size, {
minFilter: THREE.NearestFilter,
magFilter: THREE.NearestFilter,
format: THREE.RGBAFormat,
type: THREE.FloatType
})

}


const addobject = () => {

const size = 512
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)
bufferUv[ index * 2 + 1 ] = i / (size-1)
}
}

geometry.setAttribute('position', new THREE.BufferAttribute(bufferPositions, 3))
geometry.setAttribute('uv', new THREE.BufferAttribute(bufferUv, 2))


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)
}

/**
* Renderer
*/
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(sizes.pixelRatio)

debugObject.clearColor = '#29191f'
renderer.setClearColor(debugObject.clearColor)

const clock = new THREE.Clock()
let previousTime = 0

const tick = () => {
const elapsedTime = clock.getElapsedTime()
const deltaTime = elapsedTime - previousTime
previousTime = elapsedTime
controls.update()

// renderer.render(scene, camera)
renderer.setRenderTarget(renderTarget)
renderer.render(sceneFBO, cameraFBO)

renderer.setRenderTarget(null)
renderer.render(scene, camera)


//swap render target
const temp = renderTarget
renderTarget = renderTarget1
renderTarget1 = temp

material.uniforms.uTexture.value = renderTarget.texture
simMaterial.uniforms.uTexture.value = renderTarget1.texture

window.requestAnimationFrame(tick)
}

setupFBO()
addobject()
tick()
//vertexShader
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;
// newPos.z += sin(time + position.x * 10.0) * 0.5;

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

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


//fragmentShader
varying vec2 vUv;
uniform sampler2D uTexture;

void main(){

vec4 tex = texture2D(uTexture, vUv);
gl_FragColor = vec4(tex.rgb,1.0);
}



//simVertexShader
varying vec2 vUv;
uniform float time;
void main(){
vUv = uv;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = 30.0 / -mvPosition.z;
gl_Position = projectionMatrix * mvPosition;
}

//simFragmentShader
varying vec2 vUv;
uniform sampler2D uTexture;

void main(){

vec4 position = texture2D(uTexture, vUv);
position.x += 0.001;
gl_FragColor = position;
}

--

--