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()
Previous
Previous

WebGL에서 그림자를 포함한 ToonShader 연구