webgpu_compute_sort_bitonic.html 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. <html lang="en">
  2. <head>
  3. <title>three.js webgpu - storage pbo external element</title>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  6. <meta property="og:title" content="three.js webgpu - storage pbo external element">
  7. <meta property="og:type" content="website">
  8. <meta property="og:url" content="https://threejs.org/examples/webgpu_compute_sort_bitonic.html">
  9. <meta property="og:image" content="https://threejs.org/examples/screenshots/webgpu_compute_sort_bitonic.jpg">
  10. <link type="text/css" rel="stylesheet" href="main.css">
  11. </head>
  12. <body>
  13. <style>
  14. .swap_area {
  15. position: absolute;
  16. top: 150px;
  17. padding: 10px;
  18. background: rgba( 0, 0, 0, 0.5 );
  19. color: #fff;
  20. font-family: monospace;
  21. font-size: 12px;
  22. line-height: 1.5;
  23. pointer-events: none;
  24. text-align: left;
  25. }
  26. </style>
  27. <div id="info">
  28. <a href="https://threejs.org" target="_blank" rel="noopener">three.js</a>
  29. <br /> This example demonstrates a bitonic sort running step by step in a compute shader.
  30. <br /> The left canvas swaps values within workgroup local arrays. The right swaps values within storage buffers.
  31. <br /> Reference implementation by <a href="https://poniesandlight.co.uk/reflect/bitonic_merge_sort/">Tim Gfrerer</a>
  32. <br />
  33. <div id="local_swap" class="swap_area" style="left: 0;"></div>
  34. <div id="global_swap" class="swap_area" style="right: 0;"></div>
  35. </div>
  36. <script type="importmap">
  37. {
  38. "imports": {
  39. "three": "../build/three.webgpu.js",
  40. "three/webgpu": "../build/three.webgpu.js",
  41. "three/tsl": "../build/three.tsl.js",
  42. "three/addons/": "./jsm/"
  43. }
  44. }
  45. </script>
  46. <script type="module">
  47. import * as THREE from 'three/webgpu';
  48. import { storage, If, vec3, not, uniform, uv, uint, Fn, vec2, abs, int, uvec2, floor, instanceIndex } from 'three/tsl';
  49. import { BitonicSort, getBitonicDisperseIndices, getBitonicFlipIndices } from 'three/addons/gpgpu/BitonicSort.js';
  50. import WebGPU from 'three/addons/capabilities/WebGPU.js';
  51. import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
  52. const StepType = {
  53. NONE: 0,
  54. // Swap values within workgroup local values
  55. SWAP_LOCAL: 1,
  56. DISPERSE_LOCAL: 2,
  57. // Swap values within global data buffer.
  58. FLIP_GLOBAL: 3,
  59. DISPERSE_GLOBAL: 4,
  60. };
  61. const timestamps = {
  62. local_swap: document.getElementById( 'local_swap' ),
  63. global_swap: document.getElementById( 'global_swap' )
  64. };
  65. const localColors = [ 'rgb(203, 64, 203)', 'rgb(0, 215, 215)' ];
  66. const globalColors = [ 'rgb(1, 150, 1)', 'red' ];
  67. // Total number of elements and the dimensions of the display grid.
  68. const size = 16384;
  69. const gridDim = Math.sqrt( size );
  70. const getNumSteps = () => {
  71. const n = Math.log2( size );
  72. return ( n * ( n + 1 ) ) / 2;
  73. };
  74. // Total number of steps in a bitonic sort with 'size' elements.
  75. const MAX_STEPS = getNumSteps();
  76. const effectController = {
  77. // Sqr root of 16834
  78. gridWidth: uniform( gridDim ),
  79. gridHeight: uniform( gridDim ),
  80. highlight: uniform( 1 ),
  81. stepBitonic: true,
  82. 'Display Mode': 'Swap Zone Highlight'
  83. };
  84. const gui = new GUI();
  85. gui.add( effectController, 'Display Mode', [ 'Elements', 'Swap Zone Highlight' ] ).onChange( () => {
  86. if ( effectController[ 'Display Mode' ] === 'Elements' ) {
  87. effectController.highlight.value = 0;
  88. } else {
  89. effectController.highlight.value = 1;
  90. }
  91. } );
  92. if ( WebGPU.isAvailable() === false ) {
  93. document.body.appendChild( WebGPU.getErrorMessage() );
  94. throw new Error( 'No WebGPU support' );
  95. }
  96. // Display utilities
  97. const getElementIndex = Fn( ( [ uvNode, gridWidth, gridHeight ] ) => {
  98. const newUV = uvNode.mul( vec2( gridWidth, gridHeight ) );
  99. const pixel = uvec2( uint( floor( newUV.x ) ), uint( floor( newUV.y ) ) );
  100. const elementIndex = uint( gridWidth ).mul( pixel.y ).add( pixel.x );
  101. return elementIndex;
  102. }, {
  103. uvNode: 'vec2',
  104. gridWidth: 'uint',
  105. gridHeight: 'uint',
  106. return: 'uint'
  107. } );
  108. const getColor = Fn( ( [ colorChanger, gridWidth, gridHeight ] ) => {
  109. const subtracter = colorChanger.div( gridWidth.mul( gridHeight ) );
  110. return vec3( subtracter.oneMinus() ).toVar();
  111. }, {
  112. colorChanger: 'float',
  113. gridWidth: 'float',
  114. gridHeight: 'float',
  115. return: 'vec3'
  116. } );
  117. const randomizeDataArray = ( array ) => {
  118. let currentIndex = array.length;
  119. while ( currentIndex !== 0 ) {
  120. const randomIndex = Math.floor( Math.random() * currentIndex );
  121. currentIndex -= 1;
  122. [ array[ currentIndex ], array[ randomIndex ] ] = [
  123. array[ randomIndex ],
  124. array[ currentIndex ],
  125. ];
  126. }
  127. };
  128. const windowResizeCallback = ( renderer, scene, camera ) => {
  129. renderer.setSize( window.innerWidth / 2, window.innerHeight );
  130. const aspect = ( window.innerWidth / 2 ) / window.innerHeight;
  131. const frustumHeight = camera.top - camera.bottom;
  132. camera.left = - frustumHeight * aspect / 2;
  133. camera.right = frustumHeight * aspect / 2;
  134. camera.updateProjectionMatrix();
  135. renderer.render( scene, camera );
  136. };
  137. const constructInnerHTML = ( isGlobal, colorsArr ) => {
  138. return `
  139. Compute ${isGlobal ? 'Global' : 'Local'}:
  140. <div style="display: flex; flex-direction:row; justify-content: center; align-items: center;">
  141. ${isGlobal ? 'Global Swaps' : 'Local Swaps'} Compare Region&nbsp;
  142. <div style="background-color: ${ colorsArr[ 0 ]}; width:12.5px; height: 1em; border-radius: 20%;"></div>
  143. &nbsp;to Region&nbsp;
  144. <div style="background-color: ${ colorsArr[ 1 ] }; width:12.5px; height: 1em; border-radius: 20%;"></div>
  145. </div>`;
  146. };
  147. const createDisplayMesh = ( elementsStorage, algoStorage = null, blockHeightStorage = null ) => {
  148. const material = new THREE.MeshBasicNodeMaterial( { color: 0x00ff00 } );
  149. const display = Fn( () => {
  150. const { gridWidth, gridHeight, highlight } = effectController;
  151. const elementIndex = getElementIndex( uv(), gridWidth, gridHeight );
  152. const color = getColor( elementsStorage.element( elementIndex ), gridWidth, gridHeight ).toVar();
  153. if ( algoStorage !== null && blockHeightStorage !== null ) {
  154. If( highlight.equal( 1 ).and( not( algoStorage.element( 0 ).equal( StepType.NONE ) ) ), () => {
  155. const boolCheck = int( elementIndex.mod( blockHeightStorage.element( 0 ) ).lessThan( blockHeightStorage.element( 0 ).div( 2 ) ) );
  156. color.z.assign( algoStorage.element( 0 ).lessThanEqual( StepType.DISPERSE_LOCAL ) );
  157. color.x.mulAssign( boolCheck );
  158. color.y.mulAssign( abs( boolCheck.sub( 1 ) ) );
  159. } );
  160. }
  161. return color;
  162. } );
  163. material.colorNode = display();
  164. const plane = new THREE.Mesh( new THREE.PlaneGeometry( 1, 1 ), material );
  165. return plane;
  166. };
  167. const createDisplayMesh2 = ( elementsStorage, infoStorage ) => {
  168. const material = new THREE.MeshBasicNodeMaterial( { color: 0x00ff00 } );
  169. const display = Fn( () => {
  170. const { gridWidth, gridHeight, highlight } = effectController;
  171. const elementIndex = getElementIndex( uv(), gridWidth, gridHeight );
  172. const color = getColor( elementsStorage.element( elementIndex ), gridWidth, gridHeight ).toVar();
  173. If( highlight.equal( 1 ).and( not( infoStorage.element( 0 ).equal( StepType.SWAP_LOCAL ) ) ), () => {
  174. const boolCheck = int( elementIndex.mod( infoStorage.element( 1 ) ).lessThan( infoStorage.element( 1 ).div( 2 ) ) );
  175. color.z.assign( infoStorage.element( 0 ).lessThanEqual( StepType.DISPERSE_LOCAL ) );
  176. color.x.mulAssign( boolCheck );
  177. color.y.mulAssign( abs( boolCheck.sub( 1 ) ) );
  178. } );
  179. return color;
  180. } );
  181. material.colorNode = display();
  182. const plane = new THREE.Mesh( new THREE.PlaneGeometry( 1, 1 ), material );
  183. return plane;
  184. };
  185. const setupDomElement = ( renderer ) => {
  186. document.body.appendChild( renderer.domElement );
  187. renderer.domElement.style.position = 'absolute';
  188. renderer.domElement.style.top = '0';
  189. renderer.domElement.style.left = '0';
  190. renderer.domElement.style.width = '50%';
  191. renderer.domElement.style.height = '100%';
  192. };
  193. async function initBitonicSort() {
  194. let currentStep = 0;
  195. const aspect = ( window.innerWidth / 2 ) / window.innerHeight;
  196. const camera = new THREE.OrthographicCamera( - aspect, aspect, 1, - 1, 0, 2 );
  197. camera.position.z = 1;
  198. const scene = new THREE.Scene();
  199. const array = new Uint32Array( Array.from( { length: size }, ( _, i ) => {
  200. return i;
  201. } ) );
  202. randomizeDataArray( array );
  203. const currentElementsBuffer = new THREE.StorageInstancedBufferAttribute( array, 1 );
  204. const currentElementsStorage = storage( currentElementsBuffer, 'uint', size ).setPBO( true ).setName( 'Elements' );
  205. const randomizedElementsBuffer = new THREE.StorageInstancedBufferAttribute( size, 1 );
  206. const randomizedElementsStorage = storage( randomizedElementsBuffer, 'uint', size ).setPBO( true ).setName( 'RandomizedElements' );
  207. const computeInitFn = Fn( () => {
  208. randomizedElementsStorage.element( instanceIndex ).assign( currentElementsStorage.element( instanceIndex ) );
  209. } );
  210. const computeResetBuffersFn = Fn( () => {
  211. currentElementsStorage.element( instanceIndex ).assign( randomizedElementsStorage.element( instanceIndex ) );
  212. } );
  213. const renderer = new THREE.WebGPURenderer( { antialias: false } );
  214. renderer.setPixelRatio( window.devicePixelRatio );
  215. renderer.setSize( window.innerWidth / 2, window.innerHeight );
  216. await renderer.init();
  217. const animate = () => {
  218. renderer.render( scene, camera );
  219. };
  220. renderer.setAnimationLoop( animate );
  221. setupDomElement( renderer );
  222. scene.background = new THREE.Color( 0x313131 );
  223. const bitonicSortModule = new BitonicSort(
  224. renderer,
  225. currentElementsStorage,
  226. {
  227. workgroupSize: 64,
  228. }
  229. );
  230. scene.add( createDisplayMesh2( currentElementsStorage, bitonicSortModule.infoStorage ) );
  231. // Initialize each value in the elements buffer.
  232. const computeInit = computeInitFn().compute( size );
  233. const computeReset = computeResetBuffersFn().compute( size );
  234. renderer.compute( computeInit );
  235. const stepAnimation = async function () {
  236. renderer.info.reset();
  237. if ( currentStep < bitonicSortModule.stepCount ) {
  238. bitonicSortModule.computeStep( renderer );
  239. currentStep ++;
  240. } else {
  241. renderer.compute( computeReset );
  242. currentStep = 0;
  243. }
  244. timestamps[ 'local_swap' ].innerHTML = constructInnerHTML( false, localColors );
  245. if ( currentStep === bitonicSortModule.stepCount ) {
  246. setTimeout( stepAnimation, 1000 );
  247. } else {
  248. setTimeout( stepAnimation, 100 );
  249. }
  250. };
  251. stepAnimation();
  252. window.addEventListener( 'resize', onWindowResize );
  253. function onWindowResize() {
  254. windowResizeCallback( renderer, scene, camera );
  255. }
  256. }
  257. initBitonicSort();
  258. // Global Swaps Only
  259. initGlobalSwapOnly();
  260. // When forceGlobalSwap is true, force all valid local swaps to be global swaps.
  261. async function initGlobalSwapOnly() {
  262. let currentStep = 0;
  263. const aspect = ( window.innerWidth / 2 ) / window.innerHeight;
  264. const camera = new THREE.OrthographicCamera( - aspect, aspect, 1, - 1, 0, 2 );
  265. camera.position.z = 1;
  266. const scene = new THREE.Scene();
  267. const infoArray = new Uint32Array( [ 3, 2, 2 ] );
  268. const infoBuffer = new THREE.StorageInstancedBufferAttribute( infoArray, 1 );
  269. const infoStorage = storage( infoBuffer, 'uint', infoBuffer.count ).setPBO( true ).setName( 'TheInfo' );
  270. const nextBlockHeightBuffer = new THREE.StorageInstancedBufferAttribute( new Uint32Array( 1 ).fill( 2 ), 1 );
  271. const nextBlockHeightStorage = storage( nextBlockHeightBuffer, 'uint', nextBlockHeightBuffer.count ).setPBO( true ).setName( 'NextBlockHeight' );
  272. const nextBlockHeightRead = storage( nextBlockHeightBuffer, 'uint', nextBlockHeightBuffer.count ).setPBO( true ).setName( 'NextBlockHeight' ).toReadOnly();
  273. const array = new Uint32Array( Array.from( { length: size }, ( _, i ) => {
  274. return i;
  275. } ) );
  276. randomizeDataArray( array );
  277. const currentElementsBuffer = new THREE.StorageInstancedBufferAttribute( array, 1 );
  278. const currentElementsStorage = storage( currentElementsBuffer, 'uint', size ).setPBO( true ).setName( 'Elements' );
  279. const tempBuffer = new THREE.StorageInstancedBufferAttribute( array, 1 );
  280. const tempStorage = storage( tempBuffer, 'uint', size ).setPBO( true ).setName( 'Temp' );
  281. const randomizedElementsBuffer = new THREE.StorageInstancedBufferAttribute( size, 1 );
  282. const randomizedElementsStorage = storage( randomizedElementsBuffer, 'uint', size ).setPBO( true ).setName( 'RandomizedElements' );
  283. // Swap the elements in local storage
  284. const globalCompareAndSwap = ( idxBefore, idxAfter ) => {
  285. // If the later element is less than the current element
  286. If( currentElementsStorage.element( idxAfter ).lessThan( currentElementsStorage.element( idxBefore ) ), () => {
  287. // Apply the swapped values to temporary storage.
  288. tempStorage.element( idxBefore ).assign( currentElementsStorage.element( idxAfter ) );
  289. tempStorage.element( idxAfter ).assign( currentElementsStorage.element( idxBefore ) );
  290. } ).Else( () => {
  291. // Otherwise apply the existing values to temporary storage.
  292. tempStorage.element( idxBefore ).assign( currentElementsStorage.element( idxBefore ) );
  293. tempStorage.element( idxAfter ).assign( currentElementsStorage.element( idxAfter ) );
  294. } );
  295. };
  296. const computeInitFn = Fn( () => {
  297. randomizedElementsStorage.element( instanceIndex ).assign( currentElementsStorage.element( instanceIndex ) );
  298. } );
  299. const computeBitonicStepFn = Fn( () => {
  300. const nextBlockHeight = nextBlockHeightStorage.element( 0 ).toVar();
  301. const nextAlgo = infoStorage.element( 0 ).toVar();
  302. // TODO: Convert to switch block.
  303. If( nextAlgo.equal( uint( StepType.FLIP_GLOBAL ) ), () => {
  304. const idx = getBitonicFlipIndices( instanceIndex, nextBlockHeight );
  305. globalCompareAndSwap( idx.x, idx.y );
  306. } ).ElseIf( nextAlgo.equal( uint( StepType.DISPERSE_GLOBAL ) ), () => {
  307. const idx = getBitonicDisperseIndices( instanceIndex, nextBlockHeight );
  308. globalCompareAndSwap( idx.x, idx.y );
  309. } );
  310. // Since this algorithm is global only, we execute an additional compute step to sync the current buffer with the output buffer.
  311. } );
  312. const computeSetAlgoFn = Fn( () => {
  313. const nextBlockHeight = nextBlockHeightStorage.element( 0 ).toVar();
  314. const nextAlgo = infoStorage.element( 0 );
  315. const highestBlockHeight = infoStorage.element( 2 ).toVar();
  316. nextBlockHeight.divAssign( 2 );
  317. If( nextBlockHeight.equal( 1 ), () => {
  318. highestBlockHeight.mulAssign( 2 );
  319. If( highestBlockHeight.equal( size * 2 ), () => {
  320. nextAlgo.assign( StepType.NONE );
  321. nextBlockHeight.assign( 0 );
  322. } ).Else( () => {
  323. nextAlgo.assign( StepType.FLIP_GLOBAL );
  324. nextBlockHeight.assign( highestBlockHeight );
  325. } );
  326. } ).Else( () => {
  327. nextAlgo.assign( StepType.DISPERSE_GLOBAL );
  328. } );
  329. nextBlockHeightStorage.element( 0 ).assign( nextBlockHeight );
  330. infoStorage.element( 2 ).assign( highestBlockHeight );
  331. } );
  332. const computeAlignCurrentFn = Fn( () => {
  333. currentElementsStorage.element( instanceIndex ).assign( tempStorage.element( instanceIndex ) );
  334. } );
  335. const computeResetBuffersFn = Fn( () => {
  336. currentElementsStorage.element( instanceIndex ).assign( randomizedElementsStorage.element( instanceIndex ) );
  337. } );
  338. const computeResetAlgoFn = Fn( () => {
  339. infoStorage.element( 0 ).assign( StepType.FLIP_GLOBAL );
  340. nextBlockHeightStorage.element( 0 ).assign( 2 );
  341. infoStorage.element( 2 ).assign( 2 );
  342. } );
  343. // Initialize each value in the elements buffer.
  344. const computeInit = computeInitFn().compute( size );
  345. // Swap a pair of elements in the elements buffer.
  346. const computeBitonicStep = computeBitonicStepFn().compute( size / 2 );
  347. // Set the conditions for the next swap.
  348. const computeSetAlgo = computeSetAlgoFn().compute( 1 );
  349. // Align the current buffer with the temp buffer if the previous sort was executed in a global scope.
  350. const computeAlignCurrent = computeAlignCurrentFn().compute( size );
  351. // Reset the buffers and algorithm information after a full bitonic sort has been completed.
  352. const computeResetBuffers = computeResetBuffersFn().compute( size );
  353. const computeResetAlgo = computeResetAlgoFn().compute( 1 );
  354. scene.add( createDisplayMesh( currentElementsStorage, infoStorage, nextBlockHeightRead ) );
  355. const renderer = new THREE.WebGPURenderer( { antialias: false } );
  356. renderer.setPixelRatio( window.devicePixelRatio );
  357. renderer.setSize( window.innerWidth / 2, window.innerHeight );
  358. await renderer.init();
  359. const animate = () => {
  360. renderer.render( scene, camera );
  361. };
  362. renderer.setAnimationLoop( animate );
  363. setupDomElement( renderer );
  364. renderer.domElement.style.left = '50%';
  365. scene.background = new THREE.Color( 0x212121 );
  366. renderer.compute( computeInit );
  367. const stepAnimation = async function () {
  368. if ( currentStep !== MAX_STEPS ) {
  369. renderer.compute( computeBitonicStep );
  370. renderer.compute( computeAlignCurrent );
  371. renderer.compute( computeSetAlgo );
  372. currentStep ++;
  373. } else {
  374. renderer.compute( computeResetBuffers );
  375. renderer.compute( computeResetAlgo );
  376. currentStep = 0;
  377. }
  378. timestamps[ 'global_swap' ].innerHTML = constructInnerHTML( true, globalColors );
  379. if ( currentStep === MAX_STEPS ) {
  380. setTimeout( stepAnimation, 1000 );
  381. } else {
  382. setTimeout( stepAnimation, 100 );
  383. }
  384. };
  385. stepAnimation();
  386. window.addEventListener( 'resize', onWindowResize );
  387. function onWindowResize() {
  388. windowResizeCallback( renderer, scene, camera );
  389. }
  390. }
  391. </script>
  392. </body>
  393. </html>
粤ICP备19079148号