Data텍스쳐를 활용한 GPGPU 연구
threejs 에는 GPGPU 구현을 위해 GPUComputationRenderer 라는 애드온을 제공하고 있습니다. 하지만 GPGPU로 구현하다보면 숨겨진 원리에 대해 궁금할때가 매번 찾아 오게 됩니다. 그래서 GPGPU 구현을 위해서 datatexture 와 핑퐁버퍼로 GPGPU를 구현하였습니다. 좀 더 명시적으로 코드확인이 가능한 장점이 있습니다.
FBO (Frame Buffer Object)
FBO는 화면에 직접 렌더링하지 않고 메모리에 렌더링할 수 있는 기능을 제공합니다. 이를 통해 중간 결과를 저장하고 후속 계산에 사용할 수 있습니다.
Ping-Pong Buffer
Ping-Pong Buffer는 두 개의 프레임 버퍼를 번갈아 사용하여 계산 결과를 저장하고 다음 계산의 입력으로 사용하는 기법입니다. 그래서 핑퐁버퍼라고 이름지어 졌으며, 이를 통해 반복적인 계산 작업을 효율적으로 처리할 수 있습니다.
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()