通用 GPU

在3D扩展中,除了后期处理通道外,我们还可以给layer加通用GPU通道,这样可以更灵活地利用GPU的高性能实现各种效果。

  1. const vertex = /* glsl */ `
  2. precision highp float;
  3. attribute vec2 coords;
  4. attribute vec4 random;
  5. uniform float uTime;
  6. uniform sampler2D tPosition;
  7. uniform sampler2D tVelocity;
  8. varying vec4 vRandom;
  9. varying vec4 vVelocity;
  10. void main() {
  11. vRandom = random;
  12. // Get position from texture, rather than attribute
  13. vec4 position = texture2D(tPosition, coords);
  14. vVelocity = texture2D(tVelocity, coords);
  15. // Add some subtle random oscillating so it never fully stops
  16. position.xy += sin(vec2(uTime) * vRandom.wy + vRandom.xz * 6.28) * vRandom.zy * 0.1;
  17. gl_Position = vec4(position.xy, 0, 1);
  18. gl_PointSize = mix(2.0, 15.0, vRandom.x);
  19. // Make bigger while moving
  20. gl_PointSize *= 1.0 + min(1.0, length(vVelocity.xy));
  21. }
  22. `;
  23. const fragment = /* glsl */ `
  24. precision highp float;
  25. varying vec4 vRandom;
  26. varying vec4 vVelocity;
  27. void main() {
  28. // Circle shape
  29. if (step(0.5, length(gl_PointCoord.xy - 0.5)) > 0.0) discard;
  30. // Random colour
  31. vec3 color = vec3(vRandom.zy, 1.0) * mix(0.7, 2.0, vRandom.w);
  32. // Fade to white when not moving, with an ease off curve
  33. gl_FragColor.rgb = mix(vec3(1), color, 1.0 - pow(1.0 - smoothstep(0.0, 0.7, length(vVelocity.xy)), 2.0));
  34. gl_FragColor.a = 1.0;
  35. }
  36. `;
  37. const positionFragment = /* glsl */ `
  38. precision highp float;
  39. uniform float uTime;
  40. uniform sampler2D tVelocity;
  41. // Default texture uniform for GPGPU pass is 'tMap'.
  42. // Can use the textureUniform parameter to update.
  43. uniform sampler2D tMap;
  44. varying vec2 vUv;
  45. void main() {
  46. vec4 position = texture2D(tMap, vUv);
  47. vec4 velocity = texture2D(tVelocity, vUv);
  48. position.xy += velocity.xy * 0.01;
  49. // Keep in bounds
  50. vec2 limits = vec2(1);
  51. position.xy += (1.0 - step(-limits.xy, position.xy)) * limits.xy * 2.0;
  52. position.xy -= step(limits.xy, position.xy) * limits.xy * 2.0;
  53. gl_FragColor = position;
  54. }
  55. `;
  56. const velocityFragment = /* glsl */ `
  57. precision highp float;
  58. uniform float uTime;
  59. uniform sampler2D tPosition;
  60. uniform sampler2D tMap;
  61. uniform vec2 uMouse;
  62. varying vec2 vUv;
  63. void main() {
  64. vec4 position = texture2D(tPosition, vUv);
  65. vec4 velocity = texture2D(tMap, vUv);
  66. // Repulsion from mouse
  67. vec2 toMouse = position.xy - uMouse;
  68. float strength = smoothstep(0.3, 0.0, length(toMouse));
  69. velocity.xy += strength * normalize(toMouse) * 0.5;
  70. // Friction
  71. velocity.xy *= 0.98;
  72. gl_FragColor = velocity;
  73. }
  74. `;
  75. const {Scene} = spritejs;
  76. const {Vec2, GPGPU, Geometry, Mesh3d} = spritejs.ext3d;
  77. const container = document.getElementById('container');
  78. const scene = new Scene({
  79. container,
  80. displayRatio: 2,
  81. });
  82. const layer = scene.layer3d('fglayer', {
  83. camera: {
  84. fov: 35,
  85. },
  86. ambientColor: 'white',
  87. });
  88. layer.camera.attributes.pos = [0, 0, 5];
  89. // Common uniforms
  90. const time = {value: 0};
  91. const mouse = {value: new Vec2()};
  92. // The number of particles will determine how large the GPGPU textures are,
  93. // and therefore how expensive the GPU calculations will be.
  94. // Below I'm using 65536 to use every pixel of a 256x256 texture. If I used one more (65537),
  95. // it would need to use a 512x512 texture - the GPU would then perform calculations for each pixel,
  96. // meaning that nearly 3/4 of the texture (196607 pixels) would be redundant.
  97. const numParticles = 65536;
  98. // Create the initial data arrays for position and velocity. 4 values for RGBA channels in texture.
  99. const initialPositionData = new Float32Array(numParticles * 4);
  100. const initialVelocityData = new Float32Array(numParticles * 4);
  101. // Random to be used as regular static attribute
  102. const random = new Float32Array(numParticles * 4);
  103. for(let i = 0; i < numParticles; i++) {
  104. initialPositionData.set([
  105. (Math.random() - 0.5) * 2.0,
  106. (Math.random() - 0.5) * 2.0,
  107. 0, // the Green and Alpha channels go unused in this example, however I set
  108. 1, // unused Alpha to 1 so that texture is visible in WebGL debuggers
  109. ], i * 4);
  110. initialVelocityData.set([0, 0, 0, 1], i * 4);
  111. random.set([
  112. Math.random(),
  113. Math.random(),
  114. Math.random(),
  115. Math.random(),
  116. ], i * 4);
  117. }
  118. const gl = layer.gl;
  119. // Initialise the GPGPU classes, creating the FBOs and corresponding texture coordinates
  120. const position = new GPGPU(gl, {data: initialPositionData});
  121. const velocity = new GPGPU(gl, {data: initialVelocityData});
  122. // Add the simulation shaders as passes to each GPGPU class
  123. position.addPass({
  124. fragment: positionFragment,
  125. uniforms: {
  126. uTime: time,
  127. tVelocity: velocity.uniform,
  128. },
  129. });
  130. velocity.addPass({
  131. fragment: velocityFragment,
  132. uniforms: {
  133. uTime: time,
  134. uMouse: mouse,
  135. tPosition: position.uniform,
  136. },
  137. });
  138. // Now we can create our geometry, using the coordinates from above.
  139. // We don't use the velocity or position data as attributes,
  140. // instead we will get this from the FBO textures in the shader.
  141. const model = new Geometry(gl, {
  142. random: {size: 4, data: random},
  143. // Could use either position or velocity coords, as they are the same
  144. coords: {size: 2, data: position.coords},
  145. });
  146. const program = layer.createProgram({
  147. vertex,
  148. fragment,
  149. uniforms: {
  150. uTime: time,
  151. tPosition: position.uniform,
  152. tVelocity: velocity.uniform,
  153. },
  154. });
  155. const points = new Mesh3d(program, {model, mode: gl.POINTS});
  156. // Add handlers to get mouse position
  157. const isTouchCapable = 'ontouchstart' in window;
  158. if(isTouchCapable) {
  159. window.addEventListener('touchstart', updateMouse, false);
  160. window.addEventListener('touchmove', updateMouse, false);
  161. } else {
  162. window.addEventListener('mousemove', updateMouse, false);
  163. }
  164. function updateMouse(e) {
  165. if(e.changedTouches && e.changedTouches.length) {
  166. e.x = e.changedTouches[0].pageX;
  167. e.y = e.changedTouches[0].pageY;
  168. }
  169. if(e.x === undefined) {
  170. e.x = e.pageX;
  171. e.y = e.pageY;
  172. }
  173. // Get mouse value in -1 to 1 range, with y flipped
  174. mouse.value.set(
  175. (e.x / gl.renderer.width) * 2 - 1,
  176. (1.0 - e.y / gl.renderer.height) * 2 - 1
  177. );
  178. }
  179. layer.append(points);
  180. layer.tick((t) => {
  181. time.value = t * 0.001;
  182. // Update the GPGPU classes
  183. velocity.render();
  184. position.render();
  185. });