From 177f83c4be6c6e03c7fe620b0486081626503067 Mon Sep 17 00:00:00 2001 From: Andrew Adamson Date: Tue, 27 Sep 2022 00:14:49 +0700 Subject: [PATCH] Adding episode 24 Transform Feedback code --- 24.transformfeedback.example.js | 246 ++++++++++++++++++++++++++++++++ 24.transformfeedback.js | 179 +++++++++++++++++++++++ 2 files changed, 425 insertions(+) create mode 100644 24.transformfeedback.example.js create mode 100644 24.transformfeedback.js diff --git a/24.transformfeedback.example.js b/24.transformfeedback.example.js new file mode 100644 index 0000000..bce5980 --- /dev/null +++ b/24.transformfeedback.example.js @@ -0,0 +1,246 @@ +/* + This is an example of a very basic particle system + using transform feedback. + + It's important to remember that not all particle + systems *need* transform feedback. It's just a tool + you can use when the number of particles grow very + large. This animation could have been done entirely + in JavaScript, but you would run out of CPU capcity + and saturate your bandwidth on most hardware. +*/ + +const vertexShaderSource = `#version 300 es +#pragma vscode_glsllint_stage: vert + +uniform float uRandom; + +layout(location=0) in float aAge; +layout(location=1) in float aLifespan; +layout(location=2) in vec2 aPosition; +layout(location=3) in vec2 aVelocity; + +out float vAge; +out float vLifespan; +out vec2 vPosition; +out vec2 vVelocity; +out float vHealth; + +/* From TheBookOfShaders, chapter 10. This is a slightly upscaled implementation + of the algorithm: + r = Math.cos(aReallyHugeNumber); + except it attempts to avoid the concentration of values around 1 and 0 by + multiplying by a very large irrational number and then discarding the result's + integer component. Acceptable results. Other deterministic pseudo-random number + algorithms are available (including random textures). +*/ +float rand2(vec2 source) +{ + return fract(sin(dot(source.xy, vec2(1.9898,1.2313))) * 42758.5453123); +} + +void main() +{ + if (aAge == aLifespan) + { + float s = float(gl_VertexID); + float r1 = rand2(vec2(s, uRandom)); + float r2 = rand2(vec2(r1, uRandom)); + float r3 = rand2(vec2(uRandom, r1 * uRandom)); + + vec2 direction = vec2(cos(r1 * 2.0 + .57), sin(r1 * 2.0 + .57)); // Unit vector, mostly pointing upward + float energy = .2 + r2; // particles with very little energy will never be visible, so always give them something. + vec2 scale = vec2(.05, .3); // direction*energy gives too strong a value, so we scale this to fit the screen better. + + // use values above to calculate velocity + vVelocity = direction * energy * scale; + + // Particles will be emitted from below the frame + vPosition = vec2(.5 - r1, -1.1); + + vAge = -r3 * .01; + vLifespan = aLifespan; + } + else + { + // Note that even values you **arn't** updating + // must be assigned to the varying or else the + // value will be 0 in the next draw call. + vec2 gravity = vec2(0.0, -0.02); + + vVelocity = aVelocity + gravity; + vPosition = aPosition + vVelocity; + vAge = min(aLifespan, aAge + .05); + vLifespan = aLifespan; + } + + vHealth = 1.0 - (vAge / vLifespan); + + gl_Position = vec4(vPosition, 0.0, 1.0); + gl_PointSize = 5.0 * (1.0 - vAge); +}`; + +const fragmentShaderSource = `#version 300 es +#pragma vscode_glsllint_stage: frag + +precision mediump float; + +in float vHealth; + +out vec4 fragColor; + +void main() +{ + // Point primitives are considered to have a width and + // height of 1 and the center is at (.5, .5). So if we + // discard fragments beyond this distance, we get a + // point primitive shaped like a disc. + + float distanceFromPointCenter = distance(gl_PointCoord.xy, vec2(0.5)); + if (distanceFromPointCenter > .5) discard; + + fragColor = vec4(.3, 0.4, 0.8, vHealth); +}`; + +const canvas = document.querySelector('canvas'); +const gl = canvas.getContext('webgl2'); +const program = gl.createProgram(); + +const vertexShader = gl.createShader(gl.VERTEX_SHADER); +gl.shaderSource(vertexShader, vertexShaderSource); +gl.compileShader(vertexShader); +gl.attachShader(program, vertexShader); + +const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); +gl.shaderSource(fragmentShader, fragmentShaderSource); +gl.compileShader(fragmentShader); +gl.attachShader(program, fragmentShader); + +// This line tells WebGL that these four output varyings should +// be recorded by transform feedback and that we're using a single +// buffer to record them. +gl.transformFeedbackVaryings(program, ['vAge', 'vLifespan', 'vPosition', 'vVelocity'], gl.INTERLEAVED_ATTRIBS); + +gl.linkProgram(program); + +if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.log(gl.getShaderInfoLog(vertexShader)); + console.log(gl.getShaderInfoLog(fragmentShader)); + console.log(gl.getProgramInfoLog(program)); +} + +gl.useProgram(program); + +// This is the number of primitives we will draw +const COUNT = 1000000; + +// Initial state of the input data. This "seeds" the +// particle system for its first draw. +let initialData = new Float32Array(COUNT * 6); +for (let i = 0; i < COUNT * 6; i += 6) { + const px = Math.random() * 2 - 1; + const age = Math.random() * -3 + .75; + const lifespan = Math.random() * 3 + 1; + + initialData.set([ + age, // vAge + lifespan, // vLifespan + px, -1.1, // vPosition + 0,0, // vVelocity + ], i); + +} + + +// Describe our first buffer for when it is used a vertex buffer +const buffer1 = gl.createBuffer(); +const vao1 = gl.createVertexArray(); +gl.bindVertexArray(vao1); +gl.bindBuffer(gl.ARRAY_BUFFER, buffer1); +gl.bufferData(gl.ARRAY_BUFFER, 6 * COUNT * 4, gl.DYNAMIC_COPY); +gl.bufferSubData(gl.ARRAY_BUFFER, 0, initialData); +gl.vertexAttribPointer(0, 1, gl.FLOAT, false, 24, 0); +gl.vertexAttribPointer(1, 1, gl.FLOAT, false, 24, 4); +gl.vertexAttribPointer(2, 2, gl.FLOAT, false, 24, 8); +gl.vertexAttribPointer(3, 2, gl.FLOAT, false, 24, 16); +gl.enableVertexAttribArray(0); +gl.enableVertexAttribArray(1); +gl.enableVertexAttribArray(2); +gl.enableVertexAttribArray(3); + +// Initial data is no longer needed, so we can clear it now. +initialData = null; + +// Buffer2 is identical but does not need initial data +const buffer2 = gl.createBuffer(); +const vao2 = gl.createVertexArray(); +gl.bindVertexArray(vao2); +gl.bindBuffer(gl.ARRAY_BUFFER, buffer2); +gl.bufferData(gl.ARRAY_BUFFER, 6 * COUNT * 4, gl.DYNAMIC_COPY); +gl.vertexAttribPointer(0, 1, gl.FLOAT, false, 24, 0); +gl.vertexAttribPointer(1, 1, gl.FLOAT, false, 24, 4); +gl.vertexAttribPointer(2, 2, gl.FLOAT, false, 24, 8); +gl.vertexAttribPointer(3, 2, gl.FLOAT, false, 24, 16); +gl.enableVertexAttribArray(0); +gl.enableVertexAttribArray(1); +gl.enableVertexAttribArray(2); +gl.enableVertexAttribArray(3); + +// Clean up after yourself +gl.bindVertexArray(null); +gl.bindBuffer(gl.ARRAY_BUFFER, null); + +// This code should NOT be used, since we are using a single +// draw call to both UPDATE our particle system and DRAW it. +// gl.enable(gl.RASTERIZER_DISCARD); + + +// We have two VAOs and two buffers, but one of each is +// ever active at a time. These variables will make sure +// of that. +let vao = vao1; +let buffer = buffer1; +let time = 0; + +const uRandomLocation = gl.getUniformLocation(program, 'uRandom'); + +// When we call `gl.clear(gl.COLOR_BUFFER_BIT)` WebGL will +// use this color (100% black) as the background color. +gl.clearColor(0,0,0,1); + +const draw = () => { + // schedule the next draw call + requestAnimationFrame(draw); + + // It often helps to send a single (or multiple) random + // numbers into the vertex shader as a uniform. + gl.uniform1f(uRandomLocation, Math.random()); + gl.clear(gl.COLOR_BUFFER_BIT); + + // Bind one buffer to ARRAY_BUFFER and the other to TFB + gl.bindVertexArray(vao); + gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buffer); + + // Perform transform feedback and the draw call + gl.beginTransformFeedback(gl.POINTS); + gl.drawArrays(gl.POINTS, 0, COUNT); + gl.endTransformFeedback(); + + // Clean up after ourselves to avoid errors. + gl.bindVertexArray(null); + gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null); + + // If we HAD skipped the rasterizer, we would have turned it + // back on here too. + // gl.disable(gl.RASTERIZER_DISCARD); + + // Swap the VAOs and buffers + if (vao === vao1) { + vao = vao2; + buffer = buffer1; + } else { + vao = vao1; + buffer = buffer2; + } +}; +draw(); diff --git a/24.transformfeedback.js b/24.transformfeedback.js new file mode 100644 index 0000000..fdc853e --- /dev/null +++ b/24.transformfeedback.js @@ -0,0 +1,179 @@ +const vertexShaderSource = `#version 300 es +#pragma vscode_glsllint_stage: vert + +layout(location=0) in float input1; +layout(location=1) in float input2; + +out float output1; +out float output2; + +void main() +{ + output1 = input1 + .1; + output2 = input2 + .2; +}`; + +const fragmentShaderSource = `#version 300 es +#pragma vscode_glsllint_stage: frag + +void main() +{ +}`; + +const canvas = document.querySelector('canvas'); +const gl = canvas.getContext('webgl2'); +const program = gl.createProgram(); + +const vertexShader = gl.createShader(gl.VERTEX_SHADER); +gl.shaderSource(vertexShader, vertexShaderSource); +gl.compileShader(vertexShader); +gl.attachShader(program, vertexShader); + +const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); +gl.shaderSource(fragmentShader, fragmentShaderSource); +gl.compileShader(fragmentShader); +gl.attachShader(program, fragmentShader); + +// This line tells WebGL that these two output varyings should +// be recorded by transform feedback. +gl.transformFeedbackVaryings(program, ['output1', 'output2'], gl.INTERLEAVED_ATTRIBS); + +gl.linkProgram(program); + +if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.log(gl.getShaderInfoLog(vertexShader)); + console.log(gl.getShaderInfoLog(fragmentShader)); +} + +gl.useProgram(program); + +// This is the number of primitives we will draw +const COUNT = 1000; + +// Initial state of the input data -- just the numbers 0-1999 +const initialData = new Float32Array(COUNT * 2).map((v, i) => i); + +// In this version of the code, we are using vertex array objects +// since they encapsulate the vertex array buffer binding automatically +// and simplify our draw call. But we don't have to. We could just +// upload our initial data here to buffer1, allocate memory for buffer2 +// and call `gl.vertexAttribPointer()` in our draw call. +const buffer1 = gl.createBuffer(); +const vao1 = gl.createVertexArray(); +gl.bindVertexArray(vao1); +gl.bindBuffer(gl.ARRAY_BUFFER, buffer1); +gl.bufferData(gl.ARRAY_BUFFER, 2 * COUNT * 4, gl.DYNAMIC_COPY); +gl.bufferSubData(gl.ARRAY_BUFFER, 0, initialData); +gl.vertexAttribPointer(0, 1, gl.FLOAT, false, 8, 0); +gl.vertexAttribPointer(1, 1, gl.FLOAT, false, 8, 4); +gl.enableVertexAttribArray(0); +gl.enableVertexAttribArray(1); + +// Buffer2 is identical but does not need initial data +const buffer2 = gl.createBuffer(); +const vao2 = gl.createVertexArray(); +gl.bindVertexArray(vao2); +gl.bindBuffer(gl.ARRAY_BUFFER, buffer2); +gl.bufferData(gl.ARRAY_BUFFER, 2 * COUNT * 4, gl.DYNAMIC_COPY); +gl.vertexAttribPointer(0, 1, gl.FLOAT, false, 8, 0); +gl.vertexAttribPointer(1, 1, gl.FLOAT, false, 8, 4); +gl.enableVertexAttribArray(0); +gl.enableVertexAttribArray(1); + +// If you clean up after yourself, you may experience much +// fewer errors +gl.bindVertexArray(null); +gl.bindBuffer(gl.ARRAY_BUFFER, null); + +// Our fragment shader is empty, so this will silence any +// warnings WebGL gives us. +gl.enable(gl.RASTERIZER_DISCARD); + + +// We have two VAOs and two buffers, but one of each is +// ever active at a time. These variables will make sure +// of that. +let vao = vao1; +let buffer = buffer2; + +for (let i = 0; i < 100; i++) { + // Binding the VAO also will bind the vertex array buffer + // to `ARRAY_BUFFER`. So this will be the input buffer for + // our vertex shader. + gl.bindVertexArray(vao); + + // Binding the buffer to T_F_B will designate that buffer + // for output. Because we're not using `SEPARATE_ATTRIBS`, + // and thus there's only one output buffer, we use the + // index value `0`. + gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buffer); + + // Begin transform feedback. + gl.beginTransformFeedback(gl.POINTS); + + // Issue the draw call, which will also send the outputs to + // the transform feedback buffer. + gl.drawArrays(gl.POINTS, 0, 1000); + + // Ending transform feedback means the buffer can now be + // used for other purposes... such as using it as the + // vertex array (input) buffer next time. + gl.endTransformFeedback(); + + // Clean up after ourselves to avoid errors. + gl.bindVertexArray(null); + gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null); + + // Swap the VAOs and buffers + if (vao === vao1) { + vao = vao2; + buffer = buffer1; + } else { + vao = vao1; + buffer = buffer2; + } +} + +// In this case, we don't have any other working fragment shader +// but if we did, we would have to re-enable output by disabling +// RASTERIZER_DISCARD +gl.disable(gl.RASTERIZER_DISCARD); + + +// This code has nothing to do with transform feedback. It's +// just to confirm our buffers contain the data we think they +// do. +const getBufferContents = (buffer) => { + // Consider this `sync` object as a flag. It will be dropped + // into WebGL's instruction pipeline. When WebGL reaches + // this sync object, it will set its status two one of FOUR + // values. + const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0); + + const checkStatus = () => { + // Get the status + const status = gl.clientWaitSync(sync, gl.SYNC_FLUSH_COMMANDS_BIT, 0); + + if (status === gl.TIMEOUT_EXPIRED) { + console.log('GPU is still busy. Let\'s wait some more.'); + setTimeout(checkStatus); + } else if (status === gl.WAIT_FAILED) { + console.error('Something bad happened and we won\'t get any response.'); + } else { + // This code will be reached if the status is either + // CONDITION_SATISFIED or SIGNALED_ALREADY. We don't + // really care which status it is as long as one of + // these was found. So we can safely read the buffer data + // (assuming another draw call hasn't initiated more + // changes....) + const view = new Float32Array(2 * COUNT); + gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, buffer); + gl.getBufferSubData(gl.TRANSFORM_FEEDBACK_BUFFER, 0, view); + gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, null); + console.log(view); + } + }; + + setTimeout(checkStatus); +}; +getBufferContents(buffer1);