From e7e58e9bec6e7080466b7c116154b7f3276118eb Mon Sep 17 00:00:00 2001 From: ghsoares Date: Fri, 12 Jun 2020 17:22:17 -0300 Subject: [PATCH 1/4] added compute shader fire code --- playground/compute-shader-fire/README.md | 37 +++ playground/compute-shader-fire/css/style.css | 61 ++++ playground/compute-shader-fire/index.html | 43 +++ playground/compute-shader-fire/js/perlin.js | 310 ++++++++++++++++++ playground/compute-shader-fire/js/script.js | 278 ++++++++++++++++ .../compute-shader-fire/shaders/fire.comp | 128 ++++++++ 6 files changed, 857 insertions(+) create mode 100644 playground/compute-shader-fire/README.md create mode 100644 playground/compute-shader-fire/css/style.css create mode 100644 playground/compute-shader-fire/index.html create mode 100644 playground/compute-shader-fire/js/perlin.js create mode 100644 playground/compute-shader-fire/js/script.js create mode 100644 playground/compute-shader-fire/shaders/fire.comp diff --git a/playground/compute-shader-fire/README.md b/playground/compute-shader-fire/README.md new file mode 100644 index 00000000..35ef9c4d --- /dev/null +++ b/playground/compute-shader-fire/README.md @@ -0,0 +1,37 @@ +# Doom Fire Compute Shader +The classic Doom Fire experiment but with OpenGL Compute Shader! + +## What's a Compute Shader? +Firstly, you need to know what's a shader: +A shader is a type of computer program to create post-processing effects in a 3D or 2D scene. +Basically, in any game have some kind of shader, from color manipulation to very complex effects like water +reflections, scene lightning, ray-tracing, etc. +There's three types of shader: +- Vertex shader (this type of shader processes individually every vertice to form a primitive shape (3D or 2D)); +- Fragment shader (this type of shader samples every pixel of the image and outputs (shows) an processed pixel); +- Compute shader (this type of shader computes arbitrary information into GPU and outputs the data into buffers); +So the Compute Shader makes mathematic calculations in GPU and can output the data into CPU with buffers. + +## Why use Compute Shader? +The main advantage to make mathematic calculations into GPU is that GPU processes more tasks and processes faster than CPU. +Originally the GPU is used only for render a application and throw the result at your monitor, but nowadays the GPU is being +used to make other tasks than render. One example is Neural Networks that inputs data into GPU, makes Matrix calculation +and outputs the result to CPU, this whole process without render anything to screen. Other example being used in this +experiment is to use the GPU to calculate fire propagation and render the result to a texture that latter is rendered +into html5 canvas in CPU. + +## TODO +There's many things that I want to implement like: +- Heat Gradient change option; +- Cooling map noise options; +- Change fire dimensions in runtime; +- Change simulation FPS in runtime; +- Add the capability to output the fire animation to a sprite-sheet so can be used into game engines. +- Fix some graphical bugs: + - Cooling map not being generated seamlesly so that appears a line into screen; + - Wind speed to the left appears a bit faster than wind speed to the right. + +## Author + +| [
@ghsoares](https://github.com/ghsoares) | +| :---: | \ No newline at end of file diff --git a/playground/compute-shader-fire/css/style.css b/playground/compute-shader-fire/css/style.css new file mode 100644 index 00000000..2c850866 --- /dev/null +++ b/playground/compute-shader-fire/css/style.css @@ -0,0 +1,61 @@ +body, html, #contents { + margin: 0; + padding: 0; + width: 100%; + height: 100%; +} + +body { + font-family: sans-serif; + background-color: #54391B; + color: white; +} + +a { + color: white; +} + +#experiment { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +#fps { + margin-top: 8px; +} + +.option { + margin-top: 16px; + display: flex; + width: 256px; + flex-direction: row; + align-items: center; + justify-content: space-between; +} + +table { + border-spacing: 0; +} + +td { + width: 2px; + height: 2px; +} + +#canvas { + image-rendering: pixelated; +} + +#error { + margin: 32px; +} + +.hidden { + display: none !important; +} + +h2 { + margin: 0; +} \ No newline at end of file diff --git a/playground/compute-shader-fire/index.html b/playground/compute-shader-fire/index.html new file mode 100644 index 00000000..62fb5b0d --- /dev/null +++ b/playground/compute-shader-fire/index.html @@ -0,0 +1,43 @@ + + + + + + + + Compute Shaders + + + + + + +
+ + +
+ + + \ No newline at end of file diff --git a/playground/compute-shader-fire/js/perlin.js b/playground/compute-shader-fire/js/perlin.js new file mode 100644 index 00000000..3ab0f482 --- /dev/null +++ b/playground/compute-shader-fire/js/perlin.js @@ -0,0 +1,310 @@ +/* + * A speed-improved perlin and simplex noise algorithms for 2D. + * + * Based on example code by Stefan Gustavson (stegu@itn.liu.se). + * Optimisations by Peter Eastman (peastman@drizzle.stanford.edu). + * Better rank ordering method by Stefan Gustavson in 2012. + * Converted to Javascript by Joseph Gentle. + * + * Version 2012-03-09 + * + * This code was placed in the public domain by its original author, + * Stefan Gustavson. You may use it as you see fit, but + * attribution is appreciated. + * + */ + +(function(global){ + var module = global.noise = {}; + + function Grad(x, y, z) { + this.x = x; this.y = y; this.z = z; + } + + Grad.prototype.dot2 = function(x, y) { + return this.x*x + this.y*y; + }; + + Grad.prototype.dot3 = function(x, y, z) { + return this.x*x + this.y*y + this.z*z; + }; + + var grad3 = [new Grad(1,1,0),new Grad(-1,1,0),new Grad(1,-1,0),new Grad(-1,-1,0), + new Grad(1,0,1),new Grad(-1,0,1),new Grad(1,0,-1),new Grad(-1,0,-1), + new Grad(0,1,1),new Grad(0,-1,1),new Grad(0,1,-1),new Grad(0,-1,-1)]; + + var p = [151,160,137,91,90,15, + 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, + 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, + 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, + 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, + 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, + 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, + 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, + 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, + 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, + 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, + 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, + 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180]; + // To remove the need for index wrapping, double the permutation table length + var perm = new Array(512); + var gradP = new Array(512); + + // This isn't a very good seeding function, but it works ok. It supports 2^16 + // different seed values. Write something better if you need more seeds. + module.seed = function(seed) { + if(seed > 0 && seed < 1) { + // Scale the seed out + seed *= 65536; + } + + seed = Math.floor(seed); + if(seed < 256) { + seed |= seed << 8; + } + + for(var i = 0; i < 256; i++) { + var v; + if (i & 1) { + v = p[i] ^ (seed & 255); + } else { + v = p[i] ^ ((seed>>8) & 255); + } + + perm[i] = perm[i + 256] = v; + gradP[i] = gradP[i + 256] = grad3[v % 12]; + } + }; + + module.seed(0); + + /* + for(var i=0; i<256; i++) { + perm[i] = perm[i + 256] = p[i]; + gradP[i] = gradP[i + 256] = grad3[perm[i] % 12]; + }*/ + + // Skewing and unskewing factors for 2, 3, and 4 dimensions + var F2 = 0.5*(Math.sqrt(3)-1); + var G2 = (3-Math.sqrt(3))/6; + + var F3 = 1/3; + var G3 = 1/6; + + // 2D simplex noise + module.simplex2 = function(xin, yin) { + var n0, n1, n2; // Noise contributions from the three corners + // Skew the input space to determine which simplex cell we're in + var s = (xin+yin)*F2; // Hairy factor for 2D + var i = Math.floor(xin+s); + var j = Math.floor(yin+s); + var t = (i+j)*G2; + var x0 = xin-i+t; // The x,y distances from the cell origin, unskewed. + var y0 = yin-j+t; + // For the 2D case, the simplex shape is an equilateral triangle. + // Determine which simplex we are in. + var i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords + if(x0>y0) { // lower triangle, XY order: (0,0)->(1,0)->(1,1) + i1=1; j1=0; + } else { // upper triangle, YX order: (0,0)->(0,1)->(1,1) + i1=0; j1=1; + } + // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and + // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where + // c = (3-sqrt(3))/6 + var x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords + var y1 = y0 - j1 + G2; + var x2 = x0 - 1 + 2 * G2; // Offsets for last corner in (x,y) unskewed coords + var y2 = y0 - 1 + 2 * G2; + // Work out the hashed gradient indices of the three simplex corners + i &= 255; + j &= 255; + var gi0 = gradP[i+perm[j]]; + var gi1 = gradP[i+i1+perm[j+j1]]; + var gi2 = gradP[i+1+perm[j+1]]; + // Calculate the contribution from the three corners + var t0 = 0.5 - x0*x0-y0*y0; + if(t0<0) { + n0 = 0; + } else { + t0 *= t0; + n0 = t0 * t0 * gi0.dot2(x0, y0); // (x,y) of grad3 used for 2D gradient + } + var t1 = 0.5 - x1*x1-y1*y1; + if(t1<0) { + n1 = 0; + } else { + t1 *= t1; + n1 = t1 * t1 * gi1.dot2(x1, y1); + } + var t2 = 0.5 - x2*x2-y2*y2; + if(t2<0) { + n2 = 0; + } else { + t2 *= t2; + n2 = t2 * t2 * gi2.dot2(x2, y2); + } + // Add contributions from each corner to get the final noise value. + // The result is scaled to return values in the interval [-1,1]. + return 70 * (n0 + n1 + n2); + }; + + // 3D simplex noise + module.simplex3 = function(xin, yin, zin) { + var n0, n1, n2, n3; // Noise contributions from the four corners + + // Skew the input space to determine which simplex cell we're in + var s = (xin+yin+zin)*F3; // Hairy factor for 2D + var i = Math.floor(xin+s); + var j = Math.floor(yin+s); + var k = Math.floor(zin+s); + + var t = (i+j+k)*G3; + var x0 = xin-i+t; // The x,y distances from the cell origin, unskewed. + var y0 = yin-j+t; + var z0 = zin-k+t; + + // For the 3D case, the simplex shape is a slightly irregular tetrahedron. + // Determine which simplex we are in. + var i1, j1, k1; // Offsets for second corner of simplex in (i,j,k) coords + var i2, j2, k2; // Offsets for third corner of simplex in (i,j,k) coords + if(x0 >= y0) { + if(y0 >= z0) { i1=1; j1=0; k1=0; i2=1; j2=1; k2=0; } + else if(x0 >= z0) { i1=1; j1=0; k1=0; i2=1; j2=0; k2=1; } + else { i1=0; j1=0; k1=1; i2=1; j2=0; k2=1; } + } else { + if(y0 < z0) { i1=0; j1=0; k1=1; i2=0; j2=1; k2=1; } + else if(x0 < z0) { i1=0; j1=1; k1=0; i2=0; j2=1; k2=1; } + else { i1=0; j1=1; k1=0; i2=1; j2=1; k2=0; } + } + // A step of (1,0,0) in (i,j,k) means a step of (1-c,-c,-c) in (x,y,z), + // a step of (0,1,0) in (i,j,k) means a step of (-c,1-c,-c) in (x,y,z), and + // a step of (0,0,1) in (i,j,k) means a step of (-c,-c,1-c) in (x,y,z), where + // c = 1/6. + var x1 = x0 - i1 + G3; // Offsets for second corner + var y1 = y0 - j1 + G3; + var z1 = z0 - k1 + G3; + + var x2 = x0 - i2 + 2 * G3; // Offsets for third corner + var y2 = y0 - j2 + 2 * G3; + var z2 = z0 - k2 + 2 * G3; + + var x3 = x0 - 1 + 3 * G3; // Offsets for fourth corner + var y3 = y0 - 1 + 3 * G3; + var z3 = z0 - 1 + 3 * G3; + + // Work out the hashed gradient indices of the four simplex corners + i &= 255; + j &= 255; + k &= 255; + var gi0 = gradP[i+ perm[j+ perm[k ]]]; + var gi1 = gradP[i+i1+perm[j+j1+perm[k+k1]]]; + var gi2 = gradP[i+i2+perm[j+j2+perm[k+k2]]]; + var gi3 = gradP[i+ 1+perm[j+ 1+perm[k+ 1]]]; + + // Calculate the contribution from the four corners + var t0 = 0.6 - x0*x0 - y0*y0 - z0*z0; + if(t0<0) { + n0 = 0; + } else { + t0 *= t0; + n0 = t0 * t0 * gi0.dot3(x0, y0, z0); // (x,y) of grad3 used for 2D gradient + } + var t1 = 0.6 - x1*x1 - y1*y1 - z1*z1; + if(t1<0) { + n1 = 0; + } else { + t1 *= t1; + n1 = t1 * t1 * gi1.dot3(x1, y1, z1); + } + var t2 = 0.6 - x2*x2 - y2*y2 - z2*z2; + if(t2<0) { + n2 = 0; + } else { + t2 *= t2; + n2 = t2 * t2 * gi2.dot3(x2, y2, z2); + } + var t3 = 0.6 - x3*x3 - y3*y3 - z3*z3; + if(t3<0) { + n3 = 0; + } else { + t3 *= t3; + n3 = t3 * t3 * gi3.dot3(x3, y3, z3); + } + // Add contributions from each corner to get the final noise value. + // The result is scaled to return values in the interval [-1,1]. + return 32 * (n0 + n1 + n2 + n3); + + }; + + // ##### Perlin noise stuff + + function fade(t) { + return t*t*t*(t*(t*6-15)+10); + } + + function lerp(a, b, t) { + return (1-t)*a + t*b; + } + + // 2D Perlin Noise + module.perlin2 = function(x, y) { + // Find unit grid cell containing point + var X = Math.floor(x), Y = Math.floor(y); + // Get relative xy coordinates of point within that cell + x = x - X; y = y - Y; + // Wrap the integer cells at 255 (smaller integer period can be introduced here) + X = X & 255; Y = Y & 255; + + // Calculate noise contributions from each of the four corners + var n00 = gradP[X+perm[Y]].dot2(x, y); + var n01 = gradP[X+perm[Y+1]].dot2(x, y-1); + var n10 = gradP[X+1+perm[Y]].dot2(x-1, y); + var n11 = gradP[X+1+perm[Y+1]].dot2(x-1, y-1); + + // Compute the fade curve value for x + var u = fade(x); + + // Interpolate the four results + return lerp( + lerp(n00, n10, u), + lerp(n01, n11, u), + fade(y)); + }; + + // 3D Perlin Noise + module.perlin3 = function(x, y, z) { + // Find unit grid cell containing point + var X = Math.floor(x), Y = Math.floor(y), Z = Math.floor(z); + // Get relative xyz coordinates of point within that cell + x = x - X; y = y - Y; z = z - Z; + // Wrap the integer cells at 255 (smaller integer period can be introduced here) + X = X & 255; Y = Y & 255; Z = Z & 255; + + // Calculate noise contributions from each of the eight corners + var n000 = gradP[X+ perm[Y+ perm[Z ]]].dot3(x, y, z); + var n001 = gradP[X+ perm[Y+ perm[Z+1]]].dot3(x, y, z-1); + var n010 = gradP[X+ perm[Y+1+perm[Z ]]].dot3(x, y-1, z); + var n011 = gradP[X+ perm[Y+1+perm[Z+1]]].dot3(x, y-1, z-1); + var n100 = gradP[X+1+perm[Y+ perm[Z ]]].dot3(x-1, y, z); + var n101 = gradP[X+1+perm[Y+ perm[Z+1]]].dot3(x-1, y, z-1); + var n110 = gradP[X+1+perm[Y+1+perm[Z ]]].dot3(x-1, y-1, z); + var n111 = gradP[X+1+perm[Y+1+perm[Z+1]]].dot3(x-1, y-1, z-1); + + // Compute the fade curve value for x, y, z + var u = fade(x); + var v = fade(y); + var w = fade(z); + + // Interpolate + return lerp( + lerp( + lerp(n000, n100, u), + lerp(n001, n101, u), w), + lerp( + lerp(n010, n110, u), + lerp(n011, n111, u), w), + v); + }; + +})(this); diff --git a/playground/compute-shader-fire/js/script.js b/playground/compute-shader-fire/js/script.js new file mode 100644 index 00000000..91736297 --- /dev/null +++ b/playground/compute-shader-fire/js/script.js @@ -0,0 +1,278 @@ +var glCanvas; +var gl; +var compProgram; +var gridSSBO, heatGradientSSBO, coolingMapSSBO; +var grid, heatGradient, coolingMap; +var coolFactorUniform, timeUniform, windSpeedUniform, baseFireHeatUniform; +var time = 0.0; +var baseHeatSlider, coolFactorSlider, windSpeedSlider; + +// Constants +const FPS = 60; +const GRID_SIZE_X = 256; +const GRID_SIZE_Y = 256; +const SCALE = 2; + +// Clamps value between range +Math.clamp = function(a, b, c) { + return Math.min(Math.max(a, b), c); +} + +// Linear interpolation formula +function lerp(a = 0.0, b = 1.0, t = 0.0) { + return a + t*(b-a); +} + +// Remap a value from [low1, high1] to [low2, high2] +function map(value, low1, high1, low2, high2) { + return low2 + (value - low1) * ((high2 - low2) / (high1 - low1)); +} + +// Gets a color from gradient at offset t [0, 1.0] +function getGradient(colors = [], offsets = [], t = 0.0) { + let idx = 0; + let color_t_from = 0; + let color_t_to = 0; + for (let i = offsets.length - 2; i >= 0; i--) { + let offset = offsets[i]; + if (t >= offset) { + idx = i; + color_t_from = offset; + color_t_to = offsets[i + 1]; + break; + } + } + let color_t = map(t, color_t_from, color_t_to, 0, 1); + let color1 = colors[idx]; + let color2 = colors[idx + 1]; + + let r = lerp(color1.r, color2.r, color_t); + let g = lerp(color1.g, color2.g, color_t); + let b = lerp(color1.b, color2.b, color_t); + //let a = lerp(color1.a, color2.a, color_t); + + return {r, g, b}; +} + +// Generate Float32Array with rgb values from input colors, offset and resolution defined by width +function generateFloat32ArrayGradient(colors = [], offsets = [], width = 64) { + const result = new Float32Array(width * 3); + const step = 1.0 / (width - 1); + for (let i = 0; i < width; i++) { + let color = getGradient(colors, offsets, i * step); + result[i * 3 + 0] = color.r; + result[i * 3 + 1] = color.g; + result[i * 3 + 2] = color.b; + } + return result; +} + +// Gets noise and remap value from [-1, 1] to [0, 1] +function getNoise(x, y) { + return Math.pow((noise.simplex2(x, y) + 1.0) / 2.0, 2.0) +} + +// Generates cooling map using simplex noise +function generateCoolingMap() { + const result = new Float32Array(GRID_SIZE_X * GRID_SIZE_Y); + + noise.seed(Math.random()); + + // Pass for every pixel and generate noise seamless + for (let x = 0; x < GRID_SIZE_X; x++) { + for (let y = 0; y < GRID_SIZE_Y; y++) { + var value = ( + getNoise(x, y) * (GRID_SIZE_X - x) * (GRID_SIZE_Y - y) + + getNoise(x - GRID_SIZE_X, y) * (x) * (GRID_SIZE_Y - y) + + getNoise(x - GRID_SIZE_X, y - GRID_SIZE_Y) * (x) * (y) + + getNoise(x, y - GRID_SIZE_Y) * (GRID_SIZE_X - x) * (y) + ) / (GRID_SIZE_X * GRID_SIZE_Y); + result[x + y * GRID_SIZE_Y] = value; + } + } + + return result; +} + +const execute = async () => { + // Get some elements + baseHeatSlider = document.getElementById("base-heat"); + coolFactorSlider = document.getElementById("cool-factor"); + windSpeedSlider = document.getElementById("wind-speed"); + + // Sliders change + baseHeatSlider.addEventListener('input', function() { + let value = this.value; + gl.uniform1f(baseFireHeatUniform, value); + }); + + coolFactorSlider.addEventListener('input', function() { + let value = this.value; + gl.uniform1f(coolFactorUniform, value); + }); + + windSpeedSlider.addEventListener('input', function() { + let value = this.value; + gl.uniform1f(windSpeedUniform, value); + }); + + // Initialize canvas with WebGL 2.0 + glCanvas = document.getElementById("canvas"); + gl = glCanvas.getContext("webgl2-compute", {antialias: false}); + glCanvas.width = GRID_SIZE_X; + glCanvas.height = GRID_SIZE_Y; + glCanvas.style.width = GRID_SIZE_X * SCALE + "px"; + glCanvas.style.height = GRID_SIZE_Y * SCALE + "px"; + + if (!gl) { + document.getElementById("error").classList.remove("hidden") + return; + } else { + document.getElementById("experiment").classList.remove("hidden") + } + + // Initialize fire grid + grid = new Float32Array(GRID_SIZE_X * GRID_SIZE_Y + 2).fill(0.0); + grid[0] = GRID_SIZE_X; + grid[1] = GRID_SIZE_Y; + + // Initialize heat gradient + heatGradient = generateFloat32ArrayGradient( + [{r: 1.0, g: 1.0, b: 1.0}, {r: 1.0, g: 1.0, b: 0.0}, {r: 1.0, g: 0.0, b: 0.0}, {r: 0.0, g: 0.0, b: 0.0}], + [0.0, 0.25, 0.65, 1.0], 32 + ); + + // Initialize cooling map + coolingMap = generateCoolingMap(); + + for (let x = 0; x < GRID_SIZE_X; x++) { + let idx = 2 + x + (GRID_SIZE_Y - 1.0) * GRID_SIZE_X; + grid[idx] = 1.0; + } + + // Compute shader + const compCodeFetch = await fetch('shaders/fire.comp'); + const compCode = await compCodeFetch.text(); + + const compShader = gl.createShader(gl.COMPUTE_SHADER); + gl.shaderSource(compShader, compCode); + gl.compileShader(compShader); + if (!gl.getShaderParameter(compShader, gl.COMPILE_STATUS)) { + console.warn(gl.getShaderInfoLog(compShader)); + return; + } + + // Compute program + compProgram = gl.createProgram(); + gl.attachShader(compProgram, compShader); + gl.linkProgram(compProgram); + if (!gl.getProgramParameter(compProgram, gl.LINK_STATUS)) { + console.log(gl.getProgramInfoLog(compProgram)); + return; + } + + // Get uniforms locations + coolFactorUniform = gl.getUniformLocation(compProgram, "cool_factor"); + timeUniform = gl.getUniformLocation(compProgram, "time"); + windSpeedUniform = gl.getUniformLocation(compProgram, "wind_speed"); + baseFireHeatUniform = gl.getUniformLocation(compProgram, "base_fire_heat"); + + // Initialize some uniforms + gl.useProgram(compProgram); + gl.uniform1f(coolFactorUniform, 0.05); + gl.uniform1f(windSpeedUniform, 0); + gl.uniform1f(baseFireHeatUniform, 1); + coolFactorSlider.value = 0.05; + windSpeedSlider.value = 0; + baseHeatSlider.value = 1; + + // Create grid buffer + gridSSBO = gl.createBuffer(); + gl.bindBuffer(gl.SHADER_STORAGE_BUFFER, gridSSBO); + gl.bufferData(gl.SHADER_STORAGE_BUFFER, grid, gl.DYNAMIC_COPY); + gl.bindBufferBase(gl.SHADER_STORAGE_BUFFER, 0, gridSSBO); + + // Create heat gradient buffer; + heatGradientSSBO = gl.createBuffer(); + gl.bindBuffer(gl.SHADER_STORAGE_BUFFER, heatGradientSSBO); + gl.bufferData(gl.SHADER_STORAGE_BUFFER, heatGradient, gl.DYNAMIC_COPY); + gl.bindBufferBase(gl.SHADER_STORAGE_BUFFER, 1, heatGradientSSBO); + + // Create cooling map buffer; + coolingMapSSBO = gl.createBuffer(); + gl.bindBuffer(gl.SHADER_STORAGE_BUFFER, coolingMapSSBO); + gl.bufferData(gl.SHADER_STORAGE_BUFFER, coolingMap, gl.DYNAMIC_COPY); + gl.bindBufferBase(gl.SHADER_STORAGE_BUFFER, 2, coolingMapSSBO); + + // Create texture + const texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, GRID_SIZE_X, GRID_SIZE_Y); + gl.bindImageTexture(0, texture, 0, false, 0, gl.WRITE_ONLY, gl.RGBA8); + + // Create frame buffer + const frameBuffer = gl.createFramebuffer(); + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, frameBuffer); + gl.framebufferTexture2D(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); + + setTimeout(frame, 1000 / FPS); +} + +// Executed every frame defined by FPS +function frame() { + // Increase time + time += 1; + + // Used to measure performance in Compute Shader + var t0 = performance.now(); + + // Uses compute shader program + gl.useProgram(compProgram); + + // Defines time uniform + gl.uniform1f(timeUniform, time); + + // Dispatch + gl.dispatchCompute(GRID_SIZE_X, GRID_SIZE_Y, 1); + + // IDK + gl.memoryBarrier(gl.SHADER_IMAGE_ACCESS_BARRIER_BIT); + + // Renders result to canvas + gl.blitFramebuffer( + 0, 0, GRID_SIZE_X, GRID_SIZE_Y, + 0, 0, GRID_SIZE_X, GRID_SIZE_Y, + gl.COLOR_BUFFER_BIT, gl.NEAREST + ); + + // Get the delta time from computation + var delta = performance.now() - t0; + + // Displays delta time + document.getElementById("fps").innerHTML = "elapsed computation time: " + delta.toFixed(2) + " milliseconds" + + setTimeout(frame, 1000 / FPS); +} + +// Initialize program +execute(); + +// Just to debug cooling map to table +/* +let test = generateCoolingMap(); + +let t = document.getElementById("teste-table"); + +let table = ""; + +for (let x = 0; x < GRID_SIZE_X; x++) { + table += "\n"; + for (let y = 0; y < GRID_SIZE_Y; y++) { + let white = Math.round(test[x + y * GRID_SIZE_X] * 255); + let color = `rgb(${white}, ${white}, ${white})` + table += `\t\n`; + } + table += "\n"; +} + +t.innerHTML = table;*/ \ No newline at end of file diff --git a/playground/compute-shader-fire/shaders/fire.comp b/playground/compute-shader-fire/shaders/fire.comp new file mode 100644 index 00000000..7d63450c --- /dev/null +++ b/playground/compute-shader-fire/shaders/fire.comp @@ -0,0 +1,128 @@ +#version 310 es + +/* + OpenGL compute shader created by Gabriel Henrique Pereira Soares (https://github.com/ghsoares). + uses same algorithm logic from Filipe Deschamps's video (https://www.youtube.com/watch?v=fxm8cadCqbs), +The only difference is that the point coordinate is given from the shader itself (gl_GlobalInvocationID.xy). + The main aim to use Compute Shader is to transfer all the algorithm logic to GPU, because the GPU. +processes more tasks than CPU, so it makes faster to process the logic and then render the result on screen, +this means that you can create a high-resolution fire grid without too many performance lose. +*/ + +layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; + +layout(std430, binding = 0) buffer SSBO1 { + float grid[]; +}; + +layout(std430, binding = 1) buffer SSBO2 { + float heat_gradient[]; +}; + +layout(std430, binding = 2) buffer SSBO3 { + float cooling_map[]; +}; + +layout (rgba8, binding = 0) writeonly uniform highp image2D destTex; + +uniform float cool_factor; +uniform float time; +uniform float wind_speed; +uniform float base_fire_heat; + +// Get heat color from heat gradient +vec3 getHeat(float white) { + white = clamp(white, 0.0, 1.0); + white = 1.0 - white; + int size = heat_gradient.length() / 3; + int idx = int( white * float(size) ) * 3; + float r = heat_gradient[idx + 0]; + float g = heat_gradient[idx + 1]; + float b = heat_gradient[idx + 2]; + return vec3(r, g, b); +} + +// Get decay from cooling map at pixel with offset +float getDecay(ivec2 pixel, vec2 offset) { + int size_x = int(grid[0]); + int size_y = int(grid[1]); + + ivec2 dPixel = pixel + ivec2(offset); + + dPixel.x = (dPixel.x + size_x) % size_x; + dPixel.y = (dPixel.y + size_y) % size_y; + + int i = dPixel.x + dPixel.y * size_x; + + return cooling_map[i]; +} + +// Gets grid pixel, offsets two because the first two elements from grid is the grid size +int idx(int x, int y) { + int size_x = int(grid[0]); + + if (x <= 1) { + if (y == 0) x = 2; + } + + return 2 + x + y * size_x; +} + +// Propagates fire +void propagation() { + int size_x = int(grid[0]); + int size_y = int(grid[1]); + + ivec2 pos = ivec2(gl_GlobalInvocationID.xy); + + int i = idx(pos.x, pos.y); + + if (pos.y == size_y - 1) { + grid[i] = base_fire_heat; + } else { + int gX = pos.x; + + int bellow_idx = idx(gX, pos.y + 1); + + float decay = getDecay(pos, vec2(time * -wind_speed, time)); + + int sX = gX + int(wind_speed * decay); + + while (sX < 0) { + sX += size_x; + } + while (sX >= size_x) { + sX -= size_x; + } + + int this_idx = idx(sX, pos.y); + + grid[this_idx] = clamp(grid[bellow_idx] * (1.0 - cool_factor * decay), 0.0, 1.0); + } +} + +// Renders to texture +void render() { + int size_y = int(grid[1]); + + ivec2 storePos = ivec2(gl_GlobalInvocationID.xy); + + int i = idx(storePos.x, storePos.y); + + float white = grid[i]; + + vec3 color = getHeat(white); + + storePos.y = (size_y - 1) - storePos.y; + + imageStore(destTex, storePos, vec4(color, 1.0)); +} + +// Main program +void main() { + int size_x = int(grid[0]); + int size_y = int(grid[1]); + + propagation(); + render(); +} \ No newline at end of file From 06abc8eb97aff9bd864dea65ebd310415ff3fd76 Mon Sep 17 00:00:00 2001 From: ghsoares Date: Sat, 13 Jun 2020 16:10:52 -0300 Subject: [PATCH 2/4] added insert fire from mouse and fire gradient change on page --- playground/compute-shader-fire/css/style.css | 100 +++++++ playground/compute-shader-fire/index.html | 30 +- playground/compute-shader-fire/js/script.js | 258 ++++++++++++++++-- .../compute-shader-fire/shaders/fire.comp | 37 +-- 4 files changed, 383 insertions(+), 42 deletions(-) diff --git a/playground/compute-shader-fire/css/style.css b/playground/compute-shader-fire/css/style.css index 2c850866..59124d4b 100644 --- a/playground/compute-shader-fire/css/style.css +++ b/playground/compute-shader-fire/css/style.css @@ -58,4 +58,104 @@ td { h2 { margin: 0; +} + +#wrapper { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} + +#color-picker { + display: flex; + flex-direction: column; + margin-right: 64px; + background-color: white; + color: black; + padding: 16px; + border-radius: 8px; + justify-content: space-between +} + +#color { + height: 32px; + background-color: black; + border-radius: 6px; + margin: 16px 0; +} + +.color-option { + width: 224px; + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.color-option:not(:last-child) { + margin-bottom: 12px; +} + +#gradient-slider { + display: block; + position: relative; + height: 256px; + width: 8px; + background: white; + margin-right: 16px; +} + +.gradient-step { + outline: none; + position: absolute; + height: 16px; + width: 256px; + margin: 0; + -webkit-appearance: none; + pointer-events: none; + background: transparent; + bottom: 0; + left: 150%; + transform-origin: 0% 100%; + transform: rotate(-90deg) translateY(-1%); + z-index: 2; +} + +.gradient-step::-webkit-slider-thumb { + -webkit-appearance: none; + width: 10px; + height: 16px; + border-radius: 1px; + background-color: white; + pointer-events: all; + cursor: pointer; + border: 1px solid black; +} + +.gradient-step.focused::-webkit-slider-thumb { + width: 14px; + height: 20px; +} + +.gradient-step::-webkit-slider-runnable-track { + background: transparent; +} + +#update { + font-size: 24px; + padding: 4px 8px; + border-radius: 4px; + border: 0; + background-color: rgb(145, 255, 191); + margin-top: 16px; + outline: none; + cursor: pointer; +} + +#update:hover { + background-color: rgb(177, 255, 225); +} + +#update:active { + background-color: rgb(48, 114, 100); } \ No newline at end of file diff --git a/playground/compute-shader-fire/index.html b/playground/compute-shader-fire/index.html index 62fb5b0d..bbc3c007 100644 --- a/playground/compute-shader-fire/index.html +++ b/playground/compute-shader-fire/index.html @@ -17,14 +17,37 @@

WebGL 2.0 Compute not available

Make sure you are on a system with WebGL 2.0 Compute enabled. - Windows Google Chrome Canary or Microsoft Edge Insider Channels with Command Line Switches: - "--enable-webgl2-compute-context", "--use-angle=gl" and "--use-cmd-decoder=passthrough". Be aware that Compute Shader is an experimental feature. + Windows Google Chrome Canary or Microsoft Edge Insider + Channels with Command Line Switches: + "--enable-webgl2-compute-context", "--use-angle=gl" and "--use-cmd-decoder=passthrough". Be aware that + Compute Shader is an experimental feature.

diff --git a/playground/compute-shader-fire/js/script.js b/playground/compute-shader-fire/js/script.js index 91736297..1c080e39 100644 --- a/playground/compute-shader-fire/js/script.js +++ b/playground/compute-shader-fire/js/script.js @@ -3,15 +3,19 @@ var gl; var compProgram; var gridSSBO, heatGradientSSBO, coolingMapSSBO; var grid, heatGradient, coolingMap; -var coolFactorUniform, timeUniform, windSpeedUniform, baseFireHeatUniform; +var coolFactorUniform, timeUniform, windSpeedUniform, baseFireHeatUniform, secondaryFireSource; var time = 0.0; var baseHeatSlider, coolFactorSlider, windSpeedSlider; // Constants -const FPS = 60; +const FPS = 80; const GRID_SIZE_X = 256; const GRID_SIZE_Y = 256; const SCALE = 2; +const INITIAL_GRADIENT = [ + [[1.0, 1.0, 1.0], [1.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 0.0]], + [0.0, 0.25, 0.65, 1.0] +]; // Clamps value between range Math.clamp = function(a, b, c) { @@ -46,12 +50,12 @@ function getGradient(colors = [], offsets = [], t = 0.0) { let color1 = colors[idx]; let color2 = colors[idx + 1]; - let r = lerp(color1.r, color2.r, color_t); - let g = lerp(color1.g, color2.g, color_t); - let b = lerp(color1.b, color2.b, color_t); + let r = lerp(color1[0], color2[0], color_t); + let g = lerp(color1[1], color2[1], color_t); + let b = lerp(color1[2], color2[2], color_t); //let a = lerp(color1.a, color2.a, color_t); - return {r, g, b}; + return [r, g, b]; } // Generate Float32Array with rgb values from input colors, offset and resolution defined by width @@ -60,41 +64,64 @@ function generateFloat32ArrayGradient(colors = [], offsets = [], width = 64) { const step = 1.0 / (width - 1); for (let i = 0; i < width; i++) { let color = getGradient(colors, offsets, i * step); - result[i * 3 + 0] = color.r; - result[i * 3 + 1] = color.g; - result[i * 3 + 2] = color.b; + result[i * 3 + 0] = color[0]; + result[i * 3 + 1] = color[1]; + result[i * 3 + 2] = color[2]; } return result; } // Gets noise and remap value from [-1, 1] to [0, 1] function getNoise(x, y) { - return Math.pow((noise.simplex2(x, y) + 1.0) / 2.0, 2.0) + return (noise.simplex2(x, y) + 1.0) / 2.0 +} + +// Gets seamless noise +function getSeamlessNoise(x, y, w, h) { + return ( + getNoise(x, y) * (w - x) * (h - y) + + getNoise(x - w, y) * (x) * (h - y) + + getNoise(x - w, y - h) * (x) * (y) + + getNoise(x, y - h) * (w - x) * (y) + ) / (w * h) } // Generates cooling map using simplex noise function generateCoolingMap() { const result = new Float32Array(GRID_SIZE_X * GRID_SIZE_Y); + const scale = 0.5; noise.seed(Math.random()); + let size_x = GRID_SIZE_X * scale; + let size_y = GRID_SIZE_Y * scale; + // Pass for every pixel and generate noise seamless for (let x = 0; x < GRID_SIZE_X; x++) { for (let y = 0; y < GRID_SIZE_Y; y++) { - var value = ( - getNoise(x, y) * (GRID_SIZE_X - x) * (GRID_SIZE_Y - y) + - getNoise(x - GRID_SIZE_X, y) * (x) * (GRID_SIZE_Y - y) + - getNoise(x - GRID_SIZE_X, y - GRID_SIZE_Y) * (x) * (y) + - getNoise(x, y - GRID_SIZE_Y) * (GRID_SIZE_X - x) * (y) - ) / (GRID_SIZE_X * GRID_SIZE_Y); - result[x + y * GRID_SIZE_Y] = value; + let nX = x * scale; + let nY = y * scale; + result[x + y * GRID_SIZE_X] = getSeamlessNoise(nX, nY, size_x, size_y); } } return result; } -const execute = async () => { +// Sets fire gradient +function setFireGradient(gradient = [], updateBuffer = false) { + let grad = JSON.parse(JSON.stringify(gradient)); + heatGradient = generateFloat32ArrayGradient(grad[0], grad[1], 32); + if (updateBuffer) { + heatGradientSSBO = gl.createBuffer(); + gl.bindBuffer(gl.SHADER_STORAGE_BUFFER, heatGradientSSBO); + gl.bufferData(gl.SHADER_STORAGE_BUFFER, heatGradient, gl.DYNAMIC_COPY); + gl.bindBufferBase(gl.SHADER_STORAGE_BUFFER, 1, heatGradientSSBO); + } +} + +// Starts simulation execution +async function execute() { // Get some elements baseHeatSlider = document.getElementById("base-heat"); coolFactorSlider = document.getElementById("cool-factor"); @@ -131,16 +158,43 @@ const execute = async () => { document.getElementById("experiment").classList.remove("hidden") } + initGradientSlider(); + + let mouseDown = false; + + glCanvas.addEventListener("mousedown", function(ev) { + mouseDown = true; + }); + + glCanvas.addEventListener("mouseup", function() { + mouseDown = false; + gl.uniform3f(secondaryFireSource, 0, 0, 0); + }); + + glCanvas.addEventListener("mousemove", function(ev) { + if (mouseDown) { + let x = ev.offsetX; + let y = ev.offsetY; + x /= SCALE; + y /= SCALE; + + x = Math.floor(x); + y = Math.floor(y); + + x = Math.clamp(x, 0, GRID_SIZE_X - 1); + y = Math.clamp(y, 0, GRID_SIZE_X - 1); + + gl.uniform3f(secondaryFireSource, x, y, 1); + } + }); + // Initialize fire grid grid = new Float32Array(GRID_SIZE_X * GRID_SIZE_Y + 2).fill(0.0); grid[0] = GRID_SIZE_X; grid[1] = GRID_SIZE_Y; // Initialize heat gradient - heatGradient = generateFloat32ArrayGradient( - [{r: 1.0, g: 1.0, b: 1.0}, {r: 1.0, g: 1.0, b: 0.0}, {r: 1.0, g: 0.0, b: 0.0}, {r: 0.0, g: 0.0, b: 0.0}], - [0.0, 0.25, 0.65, 1.0], 32 - ); + setFireGradient(INITIAL_GRADIENT); // Initialize cooling map coolingMap = generateCoolingMap(); @@ -176,6 +230,7 @@ const execute = async () => { timeUniform = gl.getUniformLocation(compProgram, "time"); windSpeedUniform = gl.getUniformLocation(compProgram, "wind_speed"); baseFireHeatUniform = gl.getUniformLocation(compProgram, "base_fire_heat"); + secondaryFireSource = gl.getUniformLocation(compProgram, "secondary_fire_source"); // Initialize some uniforms gl.useProgram(compProgram); @@ -218,6 +273,165 @@ const execute = async () => { setTimeout(frame, 1000 / FPS); } +// Creates DOM element from code +function createElementFromHTML(htmlString) { + var div = document.createElement('div'); + div.innerHTML = htmlString.trim(); + + // Change this to div.childNodes to support multiple top-level nodes + return div.firstChild; +} + +// Initialize gradient slider +function initGradientSlider() { + var sliders = []; + var currentColorSlider; + + const ROOT_SLIDER = document.getElementById("gradient-slider"); + const COLOR_PICKER_ROOT = document.getElementById("color-picker"); + const COLOR_PICKER_OFFSET = COLOR_PICKER_ROOT.querySelector("#offset"); + const COLOR_PICKER_COLOR_DISPLAY = COLOR_PICKER_ROOT.querySelector("#color"); + const COLOR_PICKER_RED = COLOR_PICKER_ROOT.querySelector("#color-r"); + const COLOR_PICKER_GREEN = COLOR_PICKER_ROOT.querySelector("#color-g"); + const COLOR_PICKER_BLUE = COLOR_PICKER_ROOT.querySelector("#color-b"); + + // Gets all offsets and colors + function getOffsetsAndColors() { + let result = []; + for (let slider of sliders) { + result.push([slider.colorOffset, slider.color]); + } + result.sort((a, b) => a[0] - b[0]); + return result; + } + + // Gets all offsets and colors as separated arrays + function getOffsetAndColorsSeparated() { + let cols = getOffsetsAndColors(); + let offsets = []; + let colors = []; + + for (let col of cols) { + offsets.push(col[0]); + colors.push(col[1]); + } + + return [offsets, colors]; + } + + // Updates slider gradient preview + function updateSliderGradient() { + const offsetsAndColors = getOffsetsAndColors(); + let stops = []; + for (let col of offsetsAndColors) { + let percent = `${col[0] * 100}%`; + let color = `rgb(${col[1][0] * 255}, ${col[1][1] * 255}, ${col[1][2] * 255})`; + stops.push(`${color} ${percent}`); + } + let gradientCSS = `linear-gradient(to top, ${stops.join(", ")})`; + ROOT_SLIDER.style.background = gradientCSS; + + focusColorSlider(); + } + + // Changes current color slider + function changeCurrentColorSlider(slider) { + if (currentColorSlider) { + currentColorSlider.classList.remove("focused"); + } + currentColorSlider = slider; + currentColorSlider.classList.add("focused"); + focusColorSlider(); + } + + // Focus color picker to a slider + function focusColorSlider() { + let color_r = (currentColorSlider.color[0] * 255).toFixed(0); + let color_g = (currentColorSlider.color[1] * 255).toFixed(0); + let color_b = (currentColorSlider.color[2] * 255).toFixed(0); + + COLOR_PICKER_OFFSET.innerHTML = `offset: ${(currentColorSlider.colorOffset * 100).toFixed(2)}%`; + COLOR_PICKER_COLOR_DISPLAY.style.backgroundColor = `rgb(${color_r}, ${color_g}, ${color_b})`; + + COLOR_PICKER_RED.querySelector("input").value = color_r; + COLOR_PICKER_GREEN.querySelector("input").value = color_g; + COLOR_PICKER_BLUE.querySelector("input").value = color_b; + + COLOR_PICKER_RED.querySelector("span").innerHTML = color_r; + COLOR_PICKER_GREEN.querySelector("span").innerHTML = color_g; + COLOR_PICKER_BLUE.querySelector("span").innerHTML = color_b; + } + + // Creates thumb + function createThumb(offset, color) { + var code = ``; + var slider = createElementFromHTML(code); + + slider.colorOffset = offset; + slider.color = color; + + sliders.push(slider); + + ROOT_SLIDER.appendChild(slider); + + slider.addEventListener('mousedown', function(ev) { + if (!ev.shiftKey) { + changeCurrentColorSlider(slider); + } else if (sliders.length > 2) { + let idx = sliders.indexOf(this); + sliders.splice(idx, 1); + if (currentColorSlider == this) { + changeCurrentColorSlider(sliders[0]); + } + this.remove(); + updateSliderGradient(); + } + ev.stopPropagation(); + }); + + slider.addEventListener('input', function(ev) { + this.colorOffset = this.value; + updateSliderGradient(); + ev.stopPropagation(); + }); + + changeCurrentColorSlider(slider); + } + + ROOT_SLIDER.addEventListener('mousedown', function(ev) { + let offset = 1.0 - ev.offsetY / this.offsetHeight; + let colors = getOffsetAndColorsSeparated(); + let color = getGradient(colors[1], colors[0], offset); + createThumb(offset, color); + updateSliderGradient(); + }); + + COLOR_PICKER_RED.querySelector("input").addEventListener('input', function() { + currentColorSlider.color[0] = this.value / 255; + updateSliderGradient(); + }); + + COLOR_PICKER_GREEN.querySelector("input").addEventListener('input', function() { + currentColorSlider.color[1] = this.value / 255; + updateSliderGradient(); + }); + + COLOR_PICKER_BLUE.querySelector("input").addEventListener('input', function() { + currentColorSlider.color[2] = this.value / 255; + updateSliderGradient(); + }); + + for (let i = 0; i < INITIAL_GRADIENT[0].length; i++) { + createThumb(INITIAL_GRADIENT[1][i], INITIAL_GRADIENT[0][i].slice()); + } + + document.getElementById("update").addEventListener('click', function() { + setFireGradient(getOffsetAndColorsSeparated().reverse(), true); + }); + + updateSliderGradient(); +} + // Executed every frame defined by FPS function frame() { // Increase time diff --git a/playground/compute-shader-fire/shaders/fire.comp b/playground/compute-shader-fire/shaders/fire.comp index 7d63450c..d5e46d67 100644 --- a/playground/compute-shader-fire/shaders/fire.comp +++ b/playground/compute-shader-fire/shaders/fire.comp @@ -29,6 +29,7 @@ uniform float cool_factor; uniform float time; uniform float wind_speed; uniform float base_fire_heat; +uniform vec3 secondary_fire_source; // Get heat color from heat gradient vec3 getHeat(float white) { @@ -42,6 +43,11 @@ vec3 getHeat(float white) { return vec3(r, g, b); } +// Wraps value between a range without clamping (loop value) +int wrapi(int x, int x_min, int x_max) { + return (((x - x_min) % (x_max - x_min)) + (x_max - x_min)) % (x_max - x_min) + x_min; +} + // Get decay from cooling map at pixel with offset float getDecay(ivec2 pixel, vec2 offset) { int size_x = int(grid[0]); @@ -49,8 +55,8 @@ float getDecay(ivec2 pixel, vec2 offset) { ivec2 dPixel = pixel + ivec2(offset); - dPixel.x = (dPixel.x + size_x) % size_x; - dPixel.y = (dPixel.y + size_y) % size_y; + dPixel.x = dPixel.x % size_x; + dPixel.y = dPixel.y % size_y; int i = dPixel.x + dPixel.y * size_x; @@ -75,29 +81,26 @@ void propagation() { ivec2 pos = ivec2(gl_GlobalInvocationID.xy); + if (wind_speed < 0.0) { + pos.x = (size_x - 1) - pos.x; + } + int i = idx(pos.x, pos.y); if (pos.y == size_y - 1) { grid[i] = base_fire_heat; } else { - int gX = pos.x; - - int bellow_idx = idx(gX, pos.y + 1); - - float decay = getDecay(pos, vec2(time * -wind_speed, time)); - - int sX = gX + int(wind_speed * decay); + int bellow_idx = idx(pos.x, pos.y + 1); - while (sX < 0) { - sX += size_x; - } - while (sX >= size_x) { - sX -= size_x; - } + float cool = getDecay(pos, vec2(time * abs(wind_speed), time)); - int this_idx = idx(sX, pos.y); + int thisX = pos.x + int(wind_speed * cool); + thisX = (thisX + size_x) % size_x; + int thisI = idx(thisX, pos.y); + grid[thisI] = grid[bellow_idx] * (1.0 - cool_factor * cool); - grid[this_idx] = clamp(grid[bellow_idx] * (1.0 - cool_factor * decay), 0.0, 1.0); + float dist_source = 1.0 - clamp(distance(vec2(pos), vec2(secondary_fire_source.x, secondary_fire_source.y)) / 4.0, 0.0, 1.0); + grid[i] += dist_source * secondary_fire_source.z; } } From b066e5053eb6ea6ab0b38266851f9ca0a2bebe78 Mon Sep 17 00:00:00 2001 From: ghsoares Date: Sat, 13 Jun 2020 17:03:01 -0300 Subject: [PATCH 3/4] added variable length mouse fire using scroll wheel --- playground/compute-shader-fire/css/style.css | 2 + playground/compute-shader-fire/index.html | 3 +- playground/compute-shader-fire/js/script.js | 61 +++++++++++-------- .../compute-shader-fire/shaders/fire.comp | 7 ++- 4 files changed, 43 insertions(+), 30 deletions(-) diff --git a/playground/compute-shader-fire/css/style.css b/playground/compute-shader-fire/css/style.css index 59124d4b..dbd299e9 100644 --- a/playground/compute-shader-fire/css/style.css +++ b/playground/compute-shader-fire/css/style.css @@ -46,6 +46,8 @@ td { #canvas { image-rendering: pixelated; + width: 512px; + height: 512px; } #error { diff --git a/playground/compute-shader-fire/index.html b/playground/compute-shader-fire/index.html index bbc3c007..d2a4b465 100644 --- a/playground/compute-shader-fire/index.html +++ b/playground/compute-shader-fire/index.html @@ -8,7 +8,6 @@ Compute Shaders - @@ -58,10 +57,10 @@

elapsed computation time:

Wind speed: -
+ \ No newline at end of file diff --git a/playground/compute-shader-fire/js/script.js b/playground/compute-shader-fire/js/script.js index 1c080e39..a80aab1e 100644 --- a/playground/compute-shader-fire/js/script.js +++ b/playground/compute-shader-fire/js/script.js @@ -6,12 +6,13 @@ var grid, heatGradient, coolingMap; var coolFactorUniform, timeUniform, windSpeedUniform, baseFireHeatUniform, secondaryFireSource; var time = 0.0; var baseHeatSlider, coolFactorSlider, windSpeedSlider; +var scaleX = 1.0, scaleY = 1.0; +var secondaryFireScale = 8.0; // Constants -const FPS = 80; -const GRID_SIZE_X = 256; -const GRID_SIZE_Y = 256; -const SCALE = 2; +const FPS = 60; +const GRID_SIZE_X = 128; +const GRID_SIZE_Y = 128; const INITIAL_GRADIENT = [ [[1.0, 1.0, 1.0], [1.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 0.0]], [0.0, 0.25, 0.65, 1.0] @@ -89,7 +90,7 @@ function getSeamlessNoise(x, y, w, h) { // Generates cooling map using simplex noise function generateCoolingMap() { const result = new Float32Array(GRID_SIZE_X * GRID_SIZE_Y); - const scale = 0.5; + const scale = 0.25; noise.seed(Math.random()); @@ -111,7 +112,7 @@ function generateCoolingMap() { // Sets fire gradient function setFireGradient(gradient = [], updateBuffer = false) { let grad = JSON.parse(JSON.stringify(gradient)); - heatGradient = generateFloat32ArrayGradient(grad[0], grad[1], 32); + heatGradient = generateFloat32ArrayGradient(grad[0], grad[1], 16); if (updateBuffer) { heatGradientSSBO = gl.createBuffer(); gl.bindBuffer(gl.SHADER_STORAGE_BUFFER, heatGradientSSBO); @@ -148,8 +149,8 @@ async function execute() { gl = glCanvas.getContext("webgl2-compute", {antialias: false}); glCanvas.width = GRID_SIZE_X; glCanvas.height = GRID_SIZE_Y; - glCanvas.style.width = GRID_SIZE_X * SCALE + "px"; - glCanvas.style.height = GRID_SIZE_Y * SCALE + "px"; + scaleX = 512 / GRID_SIZE_X; + scaleY = 512 / GRID_SIZE_Y; if (!gl) { document.getElementById("error").classList.remove("hidden") @@ -161,31 +162,41 @@ async function execute() { initGradientSlider(); let mouseDown = false; + let mouseX = 0, mouseY = 0; + + function mouseHold() { + if (mouseDown) { + let x = mouseX / scaleX; + let y = mouseY / scaleY; + + x = Math.clamp(x, 0, GRID_SIZE_X - 1); + y = Math.clamp(y, 0, GRID_SIZE_X - 1); + + gl.uniform4f(secondaryFireSource, x, y, secondaryFireScale, 1); + + setTimeout(mouseHold, 1); + } else { + gl.uniform4f(secondaryFireSource, 0, 0, 0, 0); + } + } + + glCanvas.addEventListener("wheel", function(ev) { + secondaryFireScale += ev.deltaY * -0.01; + secondaryFireScale = Math.max(secondaryFireScale, 1.0); + }); glCanvas.addEventListener("mousedown", function(ev) { mouseDown = true; + setTimeout(mouseHold, 1); }); - glCanvas.addEventListener("mouseup", function() { + document.addEventListener("mouseup", function() { mouseDown = false; - gl.uniform3f(secondaryFireSource, 0, 0, 0); }); glCanvas.addEventListener("mousemove", function(ev) { - if (mouseDown) { - let x = ev.offsetX; - let y = ev.offsetY; - x /= SCALE; - y /= SCALE; - - x = Math.floor(x); - y = Math.floor(y); - - x = Math.clamp(x, 0, GRID_SIZE_X - 1); - y = Math.clamp(y, 0, GRID_SIZE_X - 1); - - gl.uniform3f(secondaryFireSource, x, y, 1); - } + mouseX = ev.offsetX; + mouseY = ev.offsetY; }); // Initialize fire grid @@ -469,7 +480,7 @@ function frame() { } // Initialize program -execute(); +document.addEventListener('DOMContentLoaded', execute); // Just to debug cooling map to table /* diff --git a/playground/compute-shader-fire/shaders/fire.comp b/playground/compute-shader-fire/shaders/fire.comp index d5e46d67..cebeb8c1 100644 --- a/playground/compute-shader-fire/shaders/fire.comp +++ b/playground/compute-shader-fire/shaders/fire.comp @@ -29,7 +29,7 @@ uniform float cool_factor; uniform float time; uniform float wind_speed; uniform float base_fire_heat; -uniform vec3 secondary_fire_source; +uniform vec4 secondary_fire_source; // Get heat color from heat gradient vec3 getHeat(float white) { @@ -99,8 +99,9 @@ void propagation() { int thisI = idx(thisX, pos.y); grid[thisI] = grid[bellow_idx] * (1.0 - cool_factor * cool); - float dist_source = 1.0 - clamp(distance(vec2(pos), vec2(secondary_fire_source.x, secondary_fire_source.y)) / 4.0, 0.0, 1.0); - grid[i] += dist_source * secondary_fire_source.z; + float dist_source = 1.0 - clamp(distance(vec2(pos), vec2(secondary_fire_source.x, secondary_fire_source.y)) / secondary_fire_source.z, 0.0, 1.0); + dist_source = round(dist_source); + grid[i] += dist_source * secondary_fire_source.w; } } From e94d6a4256a50718489f4798068fbf312f77aba2 Mon Sep 17 00:00:00 2001 From: ghsoares Date: Sat, 13 Jun 2020 22:01:19 -0300 Subject: [PATCH 4/4] added vertex and fragment shader to render fire --- playground/compute-shader-fire/css/style.css | 1 + playground/compute-shader-fire/js/script.js | 141 +++++++++++------- .../compute-shader-fire/shaders/fire.comp | 38 +---- .../compute-shader-fire/shaders/fire.frag | 56 +++++++ .../compute-shader-fire/shaders/fire.vert | 13 ++ 5 files changed, 162 insertions(+), 87 deletions(-) create mode 100644 playground/compute-shader-fire/shaders/fire.frag create mode 100644 playground/compute-shader-fire/shaders/fire.vert diff --git a/playground/compute-shader-fire/css/style.css b/playground/compute-shader-fire/css/style.css index dbd299e9..50a379d1 100644 --- a/playground/compute-shader-fire/css/style.css +++ b/playground/compute-shader-fire/css/style.css @@ -9,6 +9,7 @@ body { font-family: sans-serif; background-color: #54391B; color: white; + user-select: none; } a { diff --git a/playground/compute-shader-fire/js/script.js b/playground/compute-shader-fire/js/script.js index a80aab1e..3be5c31e 100644 --- a/playground/compute-shader-fire/js/script.js +++ b/playground/compute-shader-fire/js/script.js @@ -1,6 +1,6 @@ var glCanvas; var gl; -var compProgram; +var compProgram, renderProgram; var gridSSBO, heatGradientSSBO, coolingMapSSBO; var grid, heatGradient, coolingMap; var coolFactorUniform, timeUniform, windSpeedUniform, baseFireHeatUniform, secondaryFireSource; @@ -11,6 +11,7 @@ var secondaryFireScale = 8.0; // Constants const FPS = 60; +const RENDER_FPS = 30; const GRID_SIZE_X = 128; const GRID_SIZE_Y = 128; const INITIAL_GRADIENT = [ @@ -110,15 +111,15 @@ function generateCoolingMap() { } // Sets fire gradient -function setFireGradient(gradient = [], updateBuffer = false) { +function setFireGradient(gradient = []) { let grad = JSON.parse(JSON.stringify(gradient)); - heatGradient = generateFloat32ArrayGradient(grad[0], grad[1], 16); - if (updateBuffer) { - heatGradientSSBO = gl.createBuffer(); - gl.bindBuffer(gl.SHADER_STORAGE_BUFFER, heatGradientSSBO); - gl.bufferData(gl.SHADER_STORAGE_BUFFER, heatGradient, gl.DYNAMIC_COPY); - gl.bindBufferBase(gl.SHADER_STORAGE_BUFFER, 1, heatGradientSSBO); - } + heatGradient = generateFloat32ArrayGradient(grad[0], grad[1], 128); + + gl.useProgram(renderProgram); + heatGradientSSBO = gl.createBuffer(); + gl.bindBuffer(gl.SHADER_STORAGE_BUFFER, heatGradientSSBO); + gl.bufferData(gl.SHADER_STORAGE_BUFFER, heatGradient, gl.DYNAMIC_COPY); + gl.bindBufferBase(gl.SHADER_STORAGE_BUFFER, 2, heatGradientSSBO); } // Starts simulation execution @@ -131,16 +132,19 @@ async function execute() { // Sliders change baseHeatSlider.addEventListener('input', function() { let value = this.value; + gl.useProgram(compProgram); gl.uniform1f(baseFireHeatUniform, value); }); coolFactorSlider.addEventListener('input', function() { let value = this.value; + gl.useProgram(compProgram); gl.uniform1f(coolFactorUniform, value); }); windSpeedSlider.addEventListener('input', function() { let value = this.value; + gl.useProgram(compProgram); gl.uniform1f(windSpeedUniform, value); }); @@ -159,8 +163,6 @@ async function execute() { document.getElementById("experiment").classList.remove("hidden") } - initGradientSlider(); - let mouseDown = false; let mouseX = 0, mouseY = 0; @@ -172,10 +174,12 @@ async function execute() { x = Math.clamp(x, 0, GRID_SIZE_X - 1); y = Math.clamp(y, 0, GRID_SIZE_X - 1); + gl.useProgram(compProgram); gl.uniform4f(secondaryFireSource, x, y, secondaryFireScale, 1); setTimeout(mouseHold, 1); } else { + gl.useProgram(compProgram); gl.uniform4f(secondaryFireSource, 0, 0, 0, 0); } } @@ -204,17 +208,9 @@ async function execute() { grid[0] = GRID_SIZE_X; grid[1] = GRID_SIZE_Y; - // Initialize heat gradient - setFireGradient(INITIAL_GRADIENT); - // Initialize cooling map coolingMap = generateCoolingMap(); - for (let x = 0; x < GRID_SIZE_X; x++) { - let idx = 2 + x + (GRID_SIZE_Y - 1.0) * GRID_SIZE_X; - grid[idx] = 1.0; - } - // Compute shader const compCodeFetch = await fetch('shaders/fire.comp'); const compCode = await compCodeFetch.text(); @@ -235,6 +231,40 @@ async function execute() { console.log(gl.getProgramInfoLog(compProgram)); return; } + + // Vertex shader + const vertCodeFetch = await fetch('shaders/fire.vert'); + const vertCode = await vertCodeFetch.text(); + + const vertShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vertShader, vertCode); + gl.compileShader(vertShader); + if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS)) { + console.warn(gl.getShaderInfoLog(vertShader)); + return; + } + + // Fragment shader + const fragCodeFetch = await fetch('shaders/fire.frag'); + const fragCode = await fragCodeFetch.text(); + + const fragShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fragShader, fragCode); + gl.compileShader(fragShader); + if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS)) { + console.warn(gl.getShaderInfoLog(fragShader)); + return; + } + + // Render program + renderProgram = gl.createProgram(); + gl.attachShader(renderProgram, vertShader); + gl.attachShader(renderProgram, fragShader); + gl.linkProgram(renderProgram); + if (!gl.getProgramParameter(renderProgram, gl.LINK_STATUS)) { + console.log(gl.getProgramInfoLog(renderProgram)); + return; + } // Get uniforms locations coolFactorUniform = gl.getUniformLocation(compProgram, "cool_factor"); @@ -257,31 +287,42 @@ async function execute() { gl.bindBuffer(gl.SHADER_STORAGE_BUFFER, gridSSBO); gl.bufferData(gl.SHADER_STORAGE_BUFFER, grid, gl.DYNAMIC_COPY); gl.bindBufferBase(gl.SHADER_STORAGE_BUFFER, 0, gridSSBO); - - // Create heat gradient buffer; - heatGradientSSBO = gl.createBuffer(); - gl.bindBuffer(gl.SHADER_STORAGE_BUFFER, heatGradientSSBO); - gl.bufferData(gl.SHADER_STORAGE_BUFFER, heatGradient, gl.DYNAMIC_COPY); - gl.bindBufferBase(gl.SHADER_STORAGE_BUFFER, 1, heatGradientSSBO); - + // Create cooling map buffer; coolingMapSSBO = gl.createBuffer(); gl.bindBuffer(gl.SHADER_STORAGE_BUFFER, coolingMapSSBO); gl.bufferData(gl.SHADER_STORAGE_BUFFER, coolingMap, gl.DYNAMIC_COPY); - gl.bindBufferBase(gl.SHADER_STORAGE_BUFFER, 2, coolingMapSSBO); + gl.bindBufferBase(gl.SHADER_STORAGE_BUFFER, 1, coolingMapSSBO); + + // Create vertex array buffer + const vertBuffer = gl.createBuffer(); + var vertices = [ + -1,-1, + 1,-1, + -1,1, + + 1,-1, + 1,1, + -1,1 + ]; + + // Binds the vertex array buffer + gl.bindBuffer(gl.ARRAY_BUFFER, vertBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.DYNAMIC_COPY); + gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); + gl.enableVertexAttribArray(0); + + // Sets the fire gradient to the initial fire gradient + setFireGradient(INITIAL_GRADIENT); - // Create texture - const texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, GRID_SIZE_X, GRID_SIZE_Y); - gl.bindImageTexture(0, texture, 0, false, 0, gl.WRITE_ONLY, gl.RGBA8); + // Initialize gradient slider + initGradientSlider(); - // Create frame buffer - const frameBuffer = gl.createFramebuffer(); - gl.bindFramebuffer(gl.READ_FRAMEBUFFER, frameBuffer); - gl.framebufferTexture2D(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); + // Adjust the viewport + gl.viewport(0,0,glCanvas.width,glCanvas.height); - setTimeout(frame, 1000 / FPS); + setTimeout(logic, 1000 / FPS); + setTimeout(render, 1000 / RENDER_FPS); } // Creates DOM element from code @@ -443,8 +484,8 @@ function initGradientSlider() { updateSliderGradient(); } -// Executed every frame defined by FPS -function frame() { +// Executed every frame defined by FPS (logic) +function logic() { // Increase time time += 1; @@ -460,25 +501,25 @@ function frame() { // Dispatch gl.dispatchCompute(GRID_SIZE_X, GRID_SIZE_Y, 1); - // IDK - gl.memoryBarrier(gl.SHADER_IMAGE_ACCESS_BARRIER_BIT); - - // Renders result to canvas - gl.blitFramebuffer( - 0, 0, GRID_SIZE_X, GRID_SIZE_Y, - 0, 0, GRID_SIZE_X, GRID_SIZE_Y, - gl.COLOR_BUFFER_BIT, gl.NEAREST - ); - // Get the delta time from computation var delta = performance.now() - t0; // Displays delta time document.getElementById("fps").innerHTML = "elapsed computation time: " + delta.toFixed(2) + " milliseconds" - setTimeout(frame, 1000 / FPS); + setTimeout(logic, 1000 / FPS); } +function render() { + // Renders result to canvas using vertex and fragment shader + gl.useProgram(renderProgram); + gl.drawArrays(gl.TRIANGLES, 0, 6); + + setTimeout(render, 1000 / RENDER_FPS); +} + +// Executed every frame defined by FPS (rendering) + // Initialize program document.addEventListener('DOMContentLoaded', execute); diff --git a/playground/compute-shader-fire/shaders/fire.comp b/playground/compute-shader-fire/shaders/fire.comp index cebeb8c1..2c1142c2 100644 --- a/playground/compute-shader-fire/shaders/fire.comp +++ b/playground/compute-shader-fire/shaders/fire.comp @@ -15,34 +15,16 @@ layout(std430, binding = 0) buffer SSBO1 { float grid[]; }; -layout(std430, binding = 1) buffer SSBO2 { - float heat_gradient[]; -}; - -layout(std430, binding = 2) buffer SSBO3 { +layout(std430, binding = 1) buffer SSBO3 { float cooling_map[]; }; -layout (rgba8, binding = 0) writeonly uniform highp image2D destTex; - uniform float cool_factor; uniform float time; uniform float wind_speed; uniform float base_fire_heat; uniform vec4 secondary_fire_source; -// Get heat color from heat gradient -vec3 getHeat(float white) { - white = clamp(white, 0.0, 1.0); - white = 1.0 - white; - int size = heat_gradient.length() / 3; - int idx = int( white * float(size) ) * 3; - float r = heat_gradient[idx + 0]; - float g = heat_gradient[idx + 1]; - float b = heat_gradient[idx + 2]; - return vec3(r, g, b); -} - // Wraps value between a range without clamping (loop value) int wrapi(int x, int x_min, int x_max) { return (((x - x_min) % (x_max - x_min)) + (x_max - x_min)) % (x_max - x_min) + x_min; @@ -105,28 +87,10 @@ void propagation() { } } -// Renders to texture -void render() { - int size_y = int(grid[1]); - - ivec2 storePos = ivec2(gl_GlobalInvocationID.xy); - - int i = idx(storePos.x, storePos.y); - - float white = grid[i]; - - vec3 color = getHeat(white); - - storePos.y = (size_y - 1) - storePos.y; - - imageStore(destTex, storePos, vec4(color, 1.0)); -} - // Main program void main() { int size_x = int(grid[0]); int size_y = int(grid[1]); propagation(); - render(); } \ No newline at end of file diff --git a/playground/compute-shader-fire/shaders/fire.frag b/playground/compute-shader-fire/shaders/fire.frag new file mode 100644 index 00000000..806284af --- /dev/null +++ b/playground/compute-shader-fire/shaders/fire.frag @@ -0,0 +1,56 @@ +#version 310 es +precision lowp float; + +/* + OpenGL Fragment shader that renders the grid on screen +*/ + +layout(std430, binding = 0) buffer SSBO1 { + float grid[]; +}; + +layout(std430, binding = 2) buffer SSBO2 { + float heat_gradient[]; +}; + +out vec4 out_color; +in vec2 uv; + +// Get heat color from heat gradient +vec3 getHeat(float white) { + white = clamp(white, 0.0, 1.0); + white = 1.0 - white; + int size = heat_gradient.length() / 3; + int idx = int( white * float(size) ) * 3; + float r = heat_gradient[idx + 0]; + float g = heat_gradient[idx + 1]; + float b = heat_gradient[idx + 2]; + return vec3(r, g, b); +} + +// Gets grid pixel, offsets two because the first two elements from grid is the grid size +int idx(int x, int y) { + int size_x = int(grid[0]); + + if (x <= 1) { + if (y == 0) x = 2; + } + + return 2 + x + y * size_x; +} + +void main() { + float size_x = grid[0]; + float size_y = grid[1]; + + float pixel_size_x = 1.0 / size_x; + float pixel_size_y = 1.0 / size_y; + vec2 pixel_size = vec2(pixel_size_x, pixel_size_y); + + ivec2 pixel_pos = ivec2(vec2(uv.x, 1.0 - uv.y) * vec2(size_x, size_y)); + + int i = idx(pixel_pos.x, pixel_pos.y); + float white = grid[i]; + + out_color = vec4(getHeat(white), 1.0); +} \ No newline at end of file diff --git a/playground/compute-shader-fire/shaders/fire.vert b/playground/compute-shader-fire/shaders/fire.vert new file mode 100644 index 00000000..667ab7d7 --- /dev/null +++ b/playground/compute-shader-fire/shaders/fire.vert @@ -0,0 +1,13 @@ +#version 310 es +/* + Simple OpenGL Vertex shader program to render a square on canvas to render the grid on fragment shader latter +*/ + +layout (location = 0) in vec2 position; + +out vec2 uv; + +void main() { + uv = (position + 1.0) / 2.0; + gl_Position = vec4(position, 0.0, 1.0); +} \ No newline at end of file