|
| 1 | +/* |
| 2 | + This is an example of a very basic particle system |
| 3 | + using transform feedback. |
| 4 | +
|
| 5 | + It's important to remember that not all particle |
| 6 | + systems *need* transform feedback. It's just a tool |
| 7 | + you can use when the number of particles grow very |
| 8 | + large. This animation could have been done entirely |
| 9 | + in JavaScript, but you would run out of CPU capcity |
| 10 | + and saturate your bandwidth on most hardware. |
| 11 | +*/ |
| 12 | + |
| 13 | +const vertexShaderSource = `#version 300 es |
| 14 | +#pragma vscode_glsllint_stage: vert |
| 15 | +
|
| 16 | +uniform float uRandom; |
| 17 | +
|
| 18 | +layout(location=0) in float aAge; |
| 19 | +layout(location=1) in float aLifespan; |
| 20 | +layout(location=2) in vec2 aPosition; |
| 21 | +layout(location=3) in vec2 aVelocity; |
| 22 | +
|
| 23 | +out float vAge; |
| 24 | +out float vLifespan; |
| 25 | +out vec2 vPosition; |
| 26 | +out vec2 vVelocity; |
| 27 | +out float vHealth; |
| 28 | +
|
| 29 | +/* From TheBookOfShaders, chapter 10. This is a slightly upscaled implementation |
| 30 | + of the algorithm: |
| 31 | + r = Math.cos(aReallyHugeNumber); |
| 32 | + except it attempts to avoid the concentration of values around 1 and 0 by |
| 33 | + multiplying by a very large irrational number and then discarding the result's |
| 34 | + integer component. Acceptable results. Other deterministic pseudo-random number |
| 35 | + algorithms are available (including random textures). |
| 36 | +*/ |
| 37 | +float rand2(vec2 source) |
| 38 | +{ |
| 39 | + return fract(sin(dot(source.xy, vec2(1.9898,1.2313))) * 42758.5453123); |
| 40 | +} |
| 41 | +
|
| 42 | +void main() |
| 43 | +{ |
| 44 | + if (aAge == aLifespan) |
| 45 | + { |
| 46 | + float s = float(gl_VertexID); |
| 47 | + float r1 = rand2(vec2(s, uRandom)); |
| 48 | + float r2 = rand2(vec2(r1, uRandom)); |
| 49 | + float r3 = rand2(vec2(uRandom, r1 * uRandom)); |
| 50 | +
|
| 51 | + vec2 direction = vec2(cos(r1 * 2.0 + .57), sin(r1 * 2.0 + .57)); // Unit vector, mostly pointing upward |
| 52 | + float energy = .2 + r2; // particles with very little energy will never be visible, so always give them something. |
| 53 | + vec2 scale = vec2(.05, .3); // direction*energy gives too strong a value, so we scale this to fit the screen better. |
| 54 | +
|
| 55 | + // use values above to calculate velocity |
| 56 | + vVelocity = direction * energy * scale; |
| 57 | +
|
| 58 | + // Particles will be emitted from below the frame |
| 59 | + vPosition = vec2(.5 - r1, -1.1); |
| 60 | +
|
| 61 | + vAge = -r3 * .01; |
| 62 | + vLifespan = aLifespan; |
| 63 | + } |
| 64 | + else |
| 65 | + { |
| 66 | + // Note that even values you **arn't** updating |
| 67 | + // must be assigned to the varying or else the |
| 68 | + // value will be 0 in the next draw call. |
| 69 | + vec2 gravity = vec2(0.0, -0.02); |
| 70 | +
|
| 71 | + vVelocity = aVelocity + gravity; |
| 72 | + vPosition = aPosition + vVelocity; |
| 73 | + vAge = min(aLifespan, aAge + .05); |
| 74 | + vLifespan = aLifespan; |
| 75 | + } |
| 76 | +
|
| 77 | + vHealth = 1.0 - (vAge / vLifespan); |
| 78 | + |
| 79 | + gl_Position = vec4(vPosition, 0.0, 1.0); |
| 80 | + gl_PointSize = 5.0 * (1.0 - vAge); |
| 81 | +}`; |
| 82 | + |
| 83 | +const fragmentShaderSource = `#version 300 es |
| 84 | +#pragma vscode_glsllint_stage: frag |
| 85 | +
|
| 86 | +precision mediump float; |
| 87 | +
|
| 88 | +in float vHealth; |
| 89 | +
|
| 90 | +out vec4 fragColor; |
| 91 | +
|
| 92 | +void main() |
| 93 | +{ |
| 94 | + // Point primitives are considered to have a width and |
| 95 | + // height of 1 and the center is at (.5, .5). So if we |
| 96 | + // discard fragments beyond this distance, we get a |
| 97 | + // point primitive shaped like a disc. |
| 98 | +
|
| 99 | + float distanceFromPointCenter = distance(gl_PointCoord.xy, vec2(0.5)); |
| 100 | + if (distanceFromPointCenter > .5) discard; |
| 101 | +
|
| 102 | + fragColor = vec4(.3, 0.4, 0.8, vHealth); |
| 103 | +}`; |
| 104 | + |
| 105 | +const canvas = document.querySelector('canvas'); |
| 106 | +const gl = canvas.getContext('webgl2'); |
| 107 | +const program = gl.createProgram(); |
| 108 | + |
| 109 | +const vertexShader = gl.createShader(gl.VERTEX_SHADER); |
| 110 | +gl.shaderSource(vertexShader, vertexShaderSource); |
| 111 | +gl.compileShader(vertexShader); |
| 112 | +gl.attachShader(program, vertexShader); |
| 113 | + |
| 114 | +const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); |
| 115 | +gl.shaderSource(fragmentShader, fragmentShaderSource); |
| 116 | +gl.compileShader(fragmentShader); |
| 117 | +gl.attachShader(program, fragmentShader); |
| 118 | + |
| 119 | +// This line tells WebGL that these four output varyings should |
| 120 | +// be recorded by transform feedback and that we're using a single |
| 121 | +// buffer to record them. |
| 122 | +gl.transformFeedbackVaryings(program, ['vAge', 'vLifespan', 'vPosition', 'vVelocity'], gl.INTERLEAVED_ATTRIBS); |
| 123 | + |
| 124 | +gl.linkProgram(program); |
| 125 | + |
| 126 | +if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { |
| 127 | + console.log(gl.getShaderInfoLog(vertexShader)); |
| 128 | + console.log(gl.getShaderInfoLog(fragmentShader)); |
| 129 | + console.log(gl.getProgramInfoLog(program)); |
| 130 | +} |
| 131 | + |
| 132 | +gl.useProgram(program); |
| 133 | + |
| 134 | +// This is the number of primitives we will draw |
| 135 | +const COUNT = 1000000; |
| 136 | + |
| 137 | +// Initial state of the input data. This "seeds" the |
| 138 | +// particle system for its first draw. |
| 139 | +let initialData = new Float32Array(COUNT * 6); |
| 140 | +for (let i = 0; i < COUNT * 6; i += 6) { |
| 141 | + const px = Math.random() * 2 - 1; |
| 142 | + const age = Math.random() * -3 + .75; |
| 143 | + const lifespan = Math.random() * 3 + 1; |
| 144 | + |
| 145 | + initialData.set([ |
| 146 | + age, // vAge |
| 147 | + lifespan, // vLifespan |
| 148 | + px, -1.1, // vPosition |
| 149 | + 0,0, // vVelocity |
| 150 | + ], i); |
| 151 | + |
| 152 | +} |
| 153 | + |
| 154 | + |
| 155 | +// Describe our first buffer for when it is used a vertex buffer |
| 156 | +const buffer1 = gl.createBuffer(); |
| 157 | +const vao1 = gl.createVertexArray(); |
| 158 | +gl.bindVertexArray(vao1); |
| 159 | +gl.bindBuffer(gl.ARRAY_BUFFER, buffer1); |
| 160 | +gl.bufferData(gl.ARRAY_BUFFER, 6 * COUNT * 4, gl.DYNAMIC_COPY); |
| 161 | +gl.bufferSubData(gl.ARRAY_BUFFER, 0, initialData); |
| 162 | +gl.vertexAttribPointer(0, 1, gl.FLOAT, false, 24, 0); |
| 163 | +gl.vertexAttribPointer(1, 1, gl.FLOAT, false, 24, 4); |
| 164 | +gl.vertexAttribPointer(2, 2, gl.FLOAT, false, 24, 8); |
| 165 | +gl.vertexAttribPointer(3, 2, gl.FLOAT, false, 24, 16); |
| 166 | +gl.enableVertexAttribArray(0); |
| 167 | +gl.enableVertexAttribArray(1); |
| 168 | +gl.enableVertexAttribArray(2); |
| 169 | +gl.enableVertexAttribArray(3); |
| 170 | + |
| 171 | +// Initial data is no longer needed, so we can clear it now. |
| 172 | +initialData = null; |
| 173 | + |
| 174 | +// Buffer2 is identical but does not need initial data |
| 175 | +const buffer2 = gl.createBuffer(); |
| 176 | +const vao2 = gl.createVertexArray(); |
| 177 | +gl.bindVertexArray(vao2); |
| 178 | +gl.bindBuffer(gl.ARRAY_BUFFER, buffer2); |
| 179 | +gl.bufferData(gl.ARRAY_BUFFER, 6 * COUNT * 4, gl.DYNAMIC_COPY); |
| 180 | +gl.vertexAttribPointer(0, 1, gl.FLOAT, false, 24, 0); |
| 181 | +gl.vertexAttribPointer(1, 1, gl.FLOAT, false, 24, 4); |
| 182 | +gl.vertexAttribPointer(2, 2, gl.FLOAT, false, 24, 8); |
| 183 | +gl.vertexAttribPointer(3, 2, gl.FLOAT, false, 24, 16); |
| 184 | +gl.enableVertexAttribArray(0); |
| 185 | +gl.enableVertexAttribArray(1); |
| 186 | +gl.enableVertexAttribArray(2); |
| 187 | +gl.enableVertexAttribArray(3); |
| 188 | + |
| 189 | +// Clean up after yourself |
| 190 | +gl.bindVertexArray(null); |
| 191 | +gl.bindBuffer(gl.ARRAY_BUFFER, null); |
| 192 | + |
| 193 | +// This code should NOT be used, since we are using a single |
| 194 | +// draw call to both UPDATE our particle system and DRAW it. |
| 195 | +// gl.enable(gl.RASTERIZER_DISCARD); |
| 196 | + |
| 197 | + |
| 198 | +// We have two VAOs and two buffers, but one of each is |
| 199 | +// ever active at a time. These variables will make sure |
| 200 | +// of that. |
| 201 | +let vao = vao1; |
| 202 | +let buffer = buffer1; |
| 203 | +let time = 0; |
| 204 | + |
| 205 | +const uRandomLocation = gl.getUniformLocation(program, 'uRandom'); |
| 206 | + |
| 207 | +// When we call `gl.clear(gl.COLOR_BUFFER_BIT)` WebGL will |
| 208 | +// use this color (100% black) as the background color. |
| 209 | +gl.clearColor(0,0,0,1); |
| 210 | + |
| 211 | +const draw = () => { |
| 212 | + // schedule the next draw call |
| 213 | + requestAnimationFrame(draw); |
| 214 | + |
| 215 | + // It often helps to send a single (or multiple) random |
| 216 | + // numbers into the vertex shader as a uniform. |
| 217 | + gl.uniform1f(uRandomLocation, Math.random()); |
| 218 | + gl.clear(gl.COLOR_BUFFER_BIT); |
| 219 | + |
| 220 | + // Bind one buffer to ARRAY_BUFFER and the other to TFB |
| 221 | + gl.bindVertexArray(vao); |
| 222 | + gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buffer); |
| 223 | + |
| 224 | + // Perform transform feedback and the draw call |
| 225 | + gl.beginTransformFeedback(gl.POINTS); |
| 226 | + gl.drawArrays(gl.POINTS, 0, COUNT); |
| 227 | + gl.endTransformFeedback(); |
| 228 | + |
| 229 | + // Clean up after ourselves to avoid errors. |
| 230 | + gl.bindVertexArray(null); |
| 231 | + gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null); |
| 232 | + |
| 233 | + // If we HAD skipped the rasterizer, we would have turned it |
| 234 | + // back on here too. |
| 235 | + // gl.disable(gl.RASTERIZER_DISCARD); |
| 236 | + |
| 237 | + // Swap the VAOs and buffers |
| 238 | + if (vao === vao1) { |
| 239 | + vao = vao2; |
| 240 | + buffer = buffer1; |
| 241 | + } else { |
| 242 | + vao = vao1; |
| 243 | + buffer = buffer2; |
| 244 | + } |
| 245 | +}; |
| 246 | +draw(); |
0 commit comments