ThreeJS WebGPU & Neighbor Search ( 近傍探索 )
前回の WebGPU に続いて、ThreeJS WebGPU で、Neighbor Search を
行ってみました。
(粒子の表示は、前回同様です。)
プログラム
NeighborSearch-ThreeJS-WebGPU.html
<!DOCTYPE html> <html> <head> <title>Neighbor Search ThreeJS</title> <meta charset="utf-8"> </head> <body> <script type="module"> import * as THREE from '../build/three.module.js'; import WebGPURenderer from './jsm/renderers/webgpu/WebGPURenderer.js'; import WebGPU from './jsm/renderers/webgpu/WebGPU.js'; import { Matrix3Uniform, Matrix4Uniform, Vector2Uniform } from './jsm/renderers/webgpu/WebGPUUniform.js'; import WebGPUStorageBuffer from './jsm/renderers/webgpu/WebGPUStorageBuffer.js'; import WebGPUUniformsGroup from './jsm/renderers/webgpu/WebGPUUniformsGroup.js'; let camera, scene, renderer; let pointer; let stepPQUniform; const computeParams_initialize_buckets = []; const computeParams_sort_buckets = []; const computeParams_work = []; const computeParams_bucketRef = []; const computeParams_findNeighbors = []; const computeParams_clearNeighbors = []; const computeParams_integrate = []; const powerN = 4; const particleNum = 2 ** powerN init().then( animate ).catch( error ); async function init() { if ( WebGPU.isAvailable() === false ) { document.body.appendChild( WebGPU.getErrorMessage() ); throw 'No WebGPU support'; } camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 1000 ); camera.position.z = 1; camera.position.x = 0.5; camera.position.y = 0.5; // particle moving area // x, y: 0 ~ 1 scene = new THREE.Scene(); scene.background = new THREE.Color( 0x000000 ); const viewRadius = 0.2; const bucketSize = 2.0 * viewRadius; const bucketNum = Math.ceil(1.0 / bucketSize); const bucketIndexNum = bucketNum * bucketNum; // 2d console.log("powerN: ", powerN); const particleSize = 3; // find neighbors of particle with this particleIndex const particleIndex = 0; const particleArray = new Float32Array( particleNum * particleSize ); const velocityArray = new Float32Array( particleNum * particleSize ); const colorArray = new Float32Array( particleNum * 3 ); const bucketsArray = new Uint32Array( particleNum * 2) ; const workBucketsArray = new Uint32Array( particleNum * 2) ; const bucketRefArray = new Uint32Array( bucketIndexNum * 2 ); const neighborsArray = new Uint32Array( particleNum ); // test bucketIndex var buckets = []; for ( let i = 0; i < particleArray.length / particleSize; i ++ ) { const r = Math.random() * 0.02 + 0.02; const rad = Math.random() * Math.PI; const rad2 = Math.random() * Math.PI * 2.0; particleArray[ i * particleSize + 0 ] = Math.random(); particleArray[ i * particleSize + 1 ] = Math.random(); particleArray[ i * particleSize + 2 ] = 0.0; velocityArray[ i * particleSize + 0 ] = r * Math.sin( rad ) * Math.cos( rad2 ) * 0.06; velocityArray[ i * particleSize + 1 ] = r * Math.sin( rad ) * Math.sin( rad2 ) * 0.06; velocityArray[ i * particleSize + 2 ] = 0.0; if (i == 0) { colorArray[ i * 3 + 0 ] = 1.0; colorArray[ i * 3 + 1 ] = 1.0; colorArray[ i * 3 + 2 ] = 0.0; } else { colorArray[ i * 3 + 0 ] = 1.0; colorArray[ i * 3 + 1 ] = 0.0; colorArray[ i * 3 + 2 ] = 0.0; } // test bucketIndex ( cpu buckets ) var bucketX = Math.floor(particleArray[i * particleSize + 0] / bucketSize); var bucketY = Math.floor(particleArray[i * particleSize + 1] / bucketSize); var bucketIndex = bucketX + bucketY * bucketNum; buckets.push([bucketIndex, i]); } console.log("neighborsArray: ", neighborsArray); // test buckets (cpu) // initial buckets data var buckets_initial = Array.from(buckets); console.log("initial buckets (cpu): ", buckets_initial); // sort (cpu) for (let p = 0; p < powerN; p++) { for(let q = 0; q <=p; q++) { const d = 1 << (p - q); for (let i = 0; i < buckets.length; i++) { const up = ((i >> p) & 2) === 0; if ((i & d) == 0 && (buckets[i][0] > buckets[i | d][0]) === up) { const tmp = buckets[i]; buckets[i] = buckets[i | d]; buckets[i | d] = tmp; } } } } console.log("buckets soerted (cpu): ", buckets); // initialize bucketRef data for (let i = 0 ; i < bucketIndexNum ; i++){ bucketRefArray[i * 2 + 0] = 65535; bucketRefArray[i * 2 + 1] = 65535; } console.log("bucketRef initialized: ", bucketRefArray); // cpu bucketRef var cpu_bucketRef = new Array(bucketIndexNum); for(let i = 0; i < bucketIndexNum; i++) { cpu_bucketRef[i] = new Array(2).fill(65535); } console.log("cpu bucketRef: ", cpu_bucketRef); for (let i = 0; i <= particleNum; i++) { // dispatch PARTICLE_NUM + 1 ( 0 to PARTICLE_NUM) var refIndex_start; var refIndex_end; if (i == 0) { refIndex_start = buckets[i][0]; cpu_bucketRef[refIndex_start][0] = 0; } else if (i < particleNum) { refIndex_start = buckets[i][0]; refIndex_end = buckets[i - 1][0]; if (refIndex_start != refIndex_end) { cpu_bucketRef[refIndex_start][0] = i; cpu_bucketRef[refIndex_end][1] = i - 1; } } else { refIndex_end = buckets[(i - 1)][0]; cpu_bucketRef[refIndex_end][1] = i - 1; } } const particleAttribute = new THREE.InstancedBufferAttribute( particleArray, particleSize ); const velocityAttribute = new THREE.BufferAttribute( velocityArray, particleSize ); const colorAttribute = new THREE.InstancedBufferAttribute( colorArray, 3 ); const bucketsAttribute = new THREE.BufferAttribute( bucketsArray, 2 ); const workBucketsAttribute = new THREE.BufferAttribute( workBucketsArray, 2 ); const bucketRefAttribute = new THREE.BufferAttribute( bucketRefArray, 2 ); const neighborsAttribute = new THREE.InstancedBufferAttribute( neighborsArray, 1 ); const particleBuffer = new WebGPUStorageBuffer( 'particle', particleAttribute ); const velocityBuffer = new WebGPUStorageBuffer( 'velocity', velocityAttribute ); const bucketsBuffer = new WebGPUStorageBuffer( 'buckets', bucketsAttribute ); const workBucketsBuffer = new WebGPUStorageBuffer( 'workData', workBucketsAttribute ); const bucketRefBuffer = new WebGPUStorageBuffer( 'bucketRef', bucketRefAttribute ); const neighborsBuffer = new WebGPUStorageBuffer( 'neighbors', neighborsAttribute ); // compute shader uniform stepPQUniform = new Vector2Uniform( 'pq' ); const stepPQGroup = new WebGPUUniformsGroup( 'stepPQ' ); stepPQGroup.addUniform( stepPQUniform ); const computeBindings_initialize_buckets = [ particleBuffer, bucketsBuffer ]; const computeBindings_sort_buckets = [ bucketsBuffer, workBucketsBuffer, stepPQGroup ]; const computeBindings_work = [ bucketsBuffer, workBucketsBuffer ]; const computeBindings_bucketRef = [ bucketsBuffer, bucketRefBuffer ]; const computeBindings_findNeighbors = [ particleBuffer, bucketsBuffer, bucketRefBuffer, neighborsBuffer ]; const computeBindings_clearNeighbors = [ neighborsBuffer ]; const computeBindings_integrate = [ particleBuffer, velocityBuffer, bucketsBuffer, workBucketsBuffer, bucketRefBuffer, neighborsBuffer ]; const computeShader_initialize_buckets = ` #version 450 // constants #define PARTICLE_NUM ${particleNum} #define PARTICLE_SIZE ${particleSize} #define BUCKET_NUM ${bucketNum} #define BUCKET_SIZE ${bucketSize} layout(set = 0, binding = 0) buffer Particle { float particle[ PARTICLE_NUM * PARTICLE_SIZE ]; } particle; layout(set = 0, binding = 1) buffer Buckets { uint buckets[ PARTICLE_NUM * 2 ]; }; void main() { uint i = gl_GlobalInvocationID.x; if ( i >= PARTICLE_NUM ) { return; } // buckets index vec3 position = vec3( particle.particle[ i * PARTICLE_SIZE + 0 ], particle.particle[ i * PARTICLE_SIZE + 1 ], particle.particle[ i * PARTICLE_SIZE + 2 ] ); uint bucketX = uint( floor(position.x / BUCKET_SIZE) ); uint bucketY = uint( floor(position.y / BUCKET_SIZE) ); uint bucketIndex = bucketX + bucketY * BUCKET_NUM; buckets[ i * 2 + 0 ] = bucketIndex; buckets[ i * 2 + 1 ] = i; } `; const computeShader_sort_buckets = ` #version 450 // constants #define PARTICLE_NUM ${particleNum} layout(set = 0, binding = 0) buffer Buckets { uint buckets[ PARTICLE_NUM * 2 ]; }; layout(set = 0, binding = 1) buffer WorkData { uint numbers[ PARTICLE_NUM * 2 ]; } workData; layout(set = 0, binding = 2) uniform StepUniform { vec2 pq; } stepPQ; void main() { uint index = gl_GlobalInvocationID.x; if ( index >= PARTICLE_NUM ) { return; } uint p = uint(stepPQ.pq.x); uint q = uint(stepPQ.pq.y); uint d = 1u << (p - q); bool up = ((index >> p) & 2u) == 0u; if ((index & d) == 0 && (buckets[index * 2 + 0] > buckets[(index + d) * 2 + 0]) == up) { workData.numbers[index * 2 + 0] = buckets[(index + d) * 2 + 0]; workData.numbers[index * 2 + 1] = buckets[(index + d) * 2 + 1]; } else if ((index & d) == d && (buckets[(index - d) * 2 + 0] > buckets[index * 2 + 0]) == up) { workData.numbers[index * 2 + 0] = buckets[(index - d) * 2 + 0]; workData.numbers[index * 2 + 1] = buckets[(index - d) * 2 + 1]; } else { workData.numbers[index * 2 + 0] = buckets[index * 2 + 0]; workData.numbers[index * 2 + 1] = buckets[index * 2 + 1]; } } `; const computeShader_work = ` #version 450 #define PARTICLE_NUM ${particleNum} layout(set = 0, binding = 0) buffer Buckets { uint buckets[ PARTICLE_NUM * 2 ]; }; layout(set = 0, binding = 1) buffer WorkData { uint numbers[ PARTICLE_NUM * 2 ]; } workData; void main() { uint index = gl_GlobalInvocationID.x; buckets[index * 2 + 0] = workData.numbers[index * 2 + 0]; buckets[index * 2 + 1] = workData.numbers[index * 2 + 1]; } `; const computeShader_bucketRef = ` #version 450 #define PARTICLE_NUM ${particleNum} #define BUCKETIDX_NUM ${bucketIndexNum} layout(set = 0, binding = 0) buffer BucketsData { uint buckets[ PARTICLE_NUM * 2 ]; }; layout(set = 0, binding = 1) buffer BucketRef { uint bucketRef[ BUCKETIDX_NUM * 2 ]; }; void main() { // dispatch PARTICLE_NUM + 1 ( 0 to PARTICLE_NUM) uint index = gl_GlobalInvocationID.x; if (index == 0) { uint refIndex_start = buckets[index * 2 + 0]; bucketRef[refIndex_start * 2 + 0] = 0; } else if (index < PARTICLE_NUM) { uint refIndex_start = buckets[index * 2 + 0]; uint refIndex_end = buckets[(index - 1) * 2 + 0]; if (refIndex_start != refIndex_end) { bucketRef[refIndex_start * 2 + 0] = index; bucketRef[refIndex_end * 2 + 1] = index - 1; } } else { uint refIndex_end = buckets[(index - 1) * 2 + 0]; bucketRef[refIndex_end * 2 + 1] = index - 1; } } `; const computeShader_findNeighbors = ` #version 450 #define PARTICLE_NUM ${particleNum} #define PARTICLE_SIZE ${particleSize} #define BUCKET_NUM ${bucketNum} #define BUCKET_SIZE ${bucketSize} #define BUCKETIDX_NUM ${bucketIndexNum} #define PARTICLE_INDEX ${particleIndex} layout(set = 0, binding = 0) buffer Particle { float particle[ PARTICLE_NUM * PARTICLE_SIZE ]; } particle; layout(set = 0, binding = 1) buffer BucketsData { uint buckets[ PARTICLE_NUM * 2 ]; }; layout(set = 0, binding = 2) buffer BucketRef { uint bucketRef[ BUCKETIDX_NUM * 2 ]; }; layout(set = 0, binding = 3) buffer Neighbors { uint neighbors[ PARTICLE_NUM ]; }; void findNeighbors(uint index, ivec2 bucketPosition, uint neighborFlag) { if (bucketPosition.x < 0 || bucketPosition.x >= BUCKET_NUM || bucketPosition.y < 0 || bucketPosition.y >= BUCKET_NUM) { return; } uint bucketIndex = bucketPosition.x + BUCKET_NUM * bucketPosition.y; uint cellStart = bucketRef[bucketIndex * 2 + 0]; uint cellEnd = bucketRef[bucketIndex * 2 + 1]; if (cellStart == 65535 || cellEnd == 65535) { return; } for (uint i = cellStart; i <= cellEnd; i++) { if (buckets[i * 2 + 1] == index) { neighbors[index] = neighborFlag; } } } void main() { uint index = gl_GlobalInvocationID.x; uint pIndex = PARTICLE_INDEX; vec2 position = vec2( particle.particle[ pIndex * PARTICLE_SIZE + 0 ], particle.particle[ pIndex * PARTICLE_SIZE + 1 ] ); vec2 bucketPosition = position / BUCKET_SIZE; int xOffset = fract(bucketPosition.x) < 0.5 ? -1: 1; int yOffset = fract(bucketPosition.y) < 0.5 ? -1: 1; ivec2 bucketPosition00 = ivec2(bucketPosition); ivec2 bucketPosition10 = bucketPosition00 + ivec2(xOffset, 0); ivec2 bucketPosition01 = bucketPosition00 + ivec2(0, yOffset); ivec2 bucketPosition11 = bucketPosition00 + ivec2(xOffset, yOffset); findNeighbors(index, bucketPosition00, 0); findNeighbors(index, bucketPosition10, 100); findNeighbors(index, bucketPosition01, 100); findNeighbors(index, bucketPosition11, 100); } `; const computeShader_clearNeighbors = ` #version 450 #define PARTICLE_NUM ${particleNum} layout(set = 0, binding = 0) buffer Neighbors { uint neighbors[ PARTICLE_NUM ]; }; void main() { uint index = gl_GlobalInvocationID.x; neighbors[index] = 65535; } `; const computeShader_integrate = ` #version 450 #define PARTICLE_NUM ${particleNum} #define PARTICLE_SIZE ${particleSize} #define BUCKETIDX_NUM ${bucketIndexNum} // Limitation for now: the order should be the same as bindings order layout(set = 0, binding = 0) buffer Particle { float particle[ PARTICLE_NUM * PARTICLE_SIZE ]; } particle; layout(set = 0, binding = 1) buffer Velocity { float velocity[ PARTICLE_NUM * PARTICLE_SIZE ]; } velocity; layout(set = 0, binding = 2) buffer Buckets { uint buckets[ PARTICLE_NUM * 2 ]; }; layout(set = 0, binding = 3) buffer WorkData { uint numbers[ PARTICLE_NUM * 2 ]; } workData; layout(set = 0, binding = 4) buffer BucketRef { uint bucketRef[ BUCKETIDX_NUM * 2 ]; }; layout(set = 0, binding = 5) buffer Neighbors { uint neighbors[ PARTICLE_NUM ]; }; void main() { uint index = gl_GlobalInvocationID.x; if ( index >= PARTICLE_NUM ) { return; } vec3 position = vec3( particle.particle[ index * PARTICLE_SIZE + 0 ] + velocity.velocity[ index * PARTICLE_SIZE + 0 ], particle.particle[ index * PARTICLE_SIZE + 1 ] + velocity.velocity[ index * PARTICLE_SIZE + 1 ], particle.particle[ index * PARTICLE_SIZE + 2 ] + velocity.velocity[ index * PARTICLE_SIZE + 2 ] ); if ( position.x <= 0.001 || position.x >= 0.999 ) { velocity.velocity[ index * PARTICLE_SIZE + 0 ] = - velocity.velocity[ index * PARTICLE_SIZE + 0 ]; } if ( position.y <= 0.001 || position.y >= 0.999 ) { velocity.velocity[ index * PARTICLE_SIZE + 1 ] = - velocity.velocity[ index * PARTICLE_SIZE + 1 ]; } if ( position.z <= 0.001 || position.z >= 0.999 ) { velocity.velocity[ index * PARTICLE_SIZE + 2 ] = - velocity.velocity[ index * PARTICLE_SIZE + 2 ]; } particle.particle[ index * PARTICLE_SIZE + 0 ] = position.x; particle.particle[ index * PARTICLE_SIZE + 1 ] = position.y; particle.particle[ index * PARTICLE_SIZE + 2 ] = position.z; } `; computeParams_initialize_buckets.push( { num: particleNum, shader: computeShader_initialize_buckets, bindings: computeBindings_initialize_buckets }); computeParams_sort_buckets.push( { num: particleNum, shader: computeShader_sort_buckets, bindings: computeBindings_sort_buckets }); computeParams_work.push( { num: particleNum, shader: computeShader_work, bindings: computeBindings_work }); computeParams_bucketRef.push( { num: particleNum + 1, shader: computeShader_bucketRef, bindings: computeBindings_bucketRef }); computeParams_findNeighbors.push( { num: particleNum, shader: computeShader_findNeighbors, bindings: computeBindings_findNeighbors }); computeParams_clearNeighbors.push( { num: particleNum, shader: computeShader_clearNeighbors, bindings: computeBindings_clearNeighbors }); computeParams_integrate.push( { num: particleNum, shader: computeShader_integrate, bindings: computeBindings_integrate }); const boxSize = 0.02; const boxGeometry = new THREE.BoxBufferGeometry( boxSize, boxSize, boxSize ); const geometry = new THREE.InstancedBufferGeometry() .setAttribute( 'position', boxGeometry.getAttribute( 'position' ) ) .setAttribute( 'instancePosition', particleAttribute ) .setAttribute( 'instanceColor', colorAttribute ) .setAttribute( 'neighbor', neighborsAttribute ); geometry.setIndex( boxGeometry.getIndex() ); geometry.instanceCount = particleNum; const material = new THREE.RawShaderMaterial( { vertexShader: `#version 450 #define PARTICLE_NUM ${particleNum} #define PARTICLE_SIZE ${particleSize} layout(location = 0) in vec3 position; layout(location = 1) in vec3 instancePosition; layout(location = 2) in vec3 instanceColor; layout(location = 3) in int neighbor; // need int (not uint) layout(location = 0) out vec3 vColor; layout(set = 0, binding = 0) uniform ModelUniforms { mat4 modelMatrix; mat4 modelViewMatrix; mat3 normalMatrix; } modelUniforms; layout(set = 0, binding = 1) uniform CameraUniforms { mat4 projectionMatrix; mat4 viewMatrix; } cameraUniforms; void main(){ uint id = gl_InstanceIndex; if (id == 0) { vColor = vec3(1.0, 0.0, 0.0); } else if (neighbor == 0) { vColor = vec3(1.0, 1.0, 0.0); } else if (neighbor == 100) { vColor = vec3(0.0, 1.0, 0.0); } else { vColor = vec3(0.7, 0.7, 0.7); } gl_Position = cameraUniforms.projectionMatrix * modelUniforms.modelViewMatrix * vec4( position + instancePosition, 1.0 ); } `, fragmentShader: `#version 450 #define PARTICLE_NUM ${particleNum} layout(location = 0) in vec3 vColor; layout(location = 0) out vec4 outColor; void main() { outColor = vec4( vColor, 1.0 ); } ` } ); const bindings = []; const modelViewUniform = new Matrix4Uniform( 'modelMatrix' ); const modelViewMatrixUniform = new Matrix4Uniform( 'modelViewMatrix' ); const normalMatrixUniform = new Matrix3Uniform( 'normalMatrix' ); const modelGroup = new WebGPUUniformsGroup( 'modelUniforms' ); modelGroup.addUniform( modelViewUniform ); modelGroup.addUniform( modelViewMatrixUniform ); modelGroup.addUniform( normalMatrixUniform ); modelGroup.setOnBeforeUpdate( function ( object/*, camera */ ) { modelViewUniform.setValue( object.matrixWorld ); modelViewMatrixUniform.setValue( object.modelViewMatrix ); normalMatrixUniform.setValue( object.normalMatrix ); } ); const projectionMatrixUniform = new Matrix4Uniform( 'projectionMatrix' ); const viewMatrixUniform = new Matrix4Uniform( 'viewMatrix' ); const cameraGroup = new WebGPUUniformsGroup( 'cameraUniforms' ); cameraGroup.addUniform( projectionMatrixUniform ); cameraGroup.addUniform( viewMatrixUniform ); cameraGroup.setOnBeforeUpdate( function ( object, camera ) { projectionMatrixUniform.setValue( camera.projectionMatrix ); viewMatrixUniform.setValue( camera.matrixWorldInverse ); } ); bindings.push( modelGroup ); bindings.push( cameraGroup ); material.bindings = bindings; const mesh = new THREE.Mesh( geometry, material ); scene.add( mesh ); renderer = new WebGPURenderer(); renderer.setPixelRatio( window.devicePixelRatio ); renderer.setSize( window.innerWidth, window.innerHeight ); document.body.appendChild( renderer.domElement ); window.addEventListener( 'resize', onWindowResize, false ); return renderer.init(); } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize( window.innerWidth, window.innerHeight ); } function animate() { // requestAnimationFrame( animate ); // clear neighbors renderer.compute( computeParams_clearNeighbors ); renderer.compute( computeParams_initialize_buckets ); for (let p = 0; p < powerN; p++) { for (let q = 0; q <= p; q++) { stepPQUniform.setValue( new THREE.Vector2(p, q) ); renderer.compute( computeParams_sort_buckets ); renderer.compute( computeParams_work ); } } renderer.compute( computeParams_bucketRef ); renderer.compute( computeParams_findNeighbors ); renderer.compute( computeParams_integrate ); renderer.render( scene, camera ); } function error( error ) { console.error( error ); } </script> </body> </html>