pause, transparency, background color, take screenshot

This commit is contained in:
Pavel Dobryakov
2019-01-06 19:29:26 +03:00
parent ba1c07e009
commit e41ed80ea0

344
script.js
View File

@@ -13,23 +13,41 @@ let config = {
PRESSURE_ITERATIONS: 20, PRESSURE_ITERATIONS: 20,
CURL: 30, CURL: 30,
SPLAT_RADIUS: 0.5, SPLAT_RADIUS: 0.5,
SHADING: true SHADING: true,
COLORFUL: true,
PAUSED: false,
BACK_COLOR: { r: 0, g: 0, b: 0 },
TRANSPARENT: false
}
function pointerPrototype () {
this.id = -1;
this.x = 0;
this.y = 0;
this.dx = 0;
this.dy = 0;
this.down = false;
this.moved = false;
this.color = [30, 0, 300];
} }
let pointers = []; let pointers = [];
let splatStack = []; let splatStack = [];
pointers.push(new pointerPrototype());
const { gl, ext } = getWebGLContext(canvas); const { gl, ext } = getWebGLContext(canvas);
if (isMobile()) if (isMobile()) {
config.DYE_RESOLUTION = 256; config.DYE_RESOLUTION = 256;
config.SHADING = false;
}
if (!ext.supportLinearFiltering) if (!ext.supportLinearFiltering)
config.SHADING = false; config.SHADING = false;
startGUI(); startGUI();
function getWebGLContext (canvas) { function getWebGLContext (canvas) {
const params = { alpha: false, depth: false, stencil: false, antialias: false }; const params = { alpha: true, depth: false, stencil: false, antialias: false, preserveDrawingBuffer: false };
let gl = canvas.getContext('webgl2', params); let gl = canvas.getContext('webgl2', params);
const isWebGL2 = !!gl; const isWebGL2 = !!gl;
@@ -134,11 +152,18 @@ function startGUI () {
gui.add(config, 'CURL', 0, 50).name('vorticity').step(1); gui.add(config, 'CURL', 0, 50).name('vorticity').step(1);
gui.add(config, 'SPLAT_RADIUS', 0.01, 1.0).name('splat radius'); gui.add(config, 'SPLAT_RADIUS', 0.01, 1.0).name('splat radius');
gui.add(config, 'SHADING').name('shading'); gui.add(config, 'SHADING').name('shading');
gui.add(config, 'COLORFUL').name('colorful');
gui.add(config, 'PAUSED').name('paused').listen();
gui.add({ fun: () => { gui.add({ fun: () => {
splatStack.push(parseInt(Math.random() * 20) + 5); splatStack.push(parseInt(Math.random() * 20) + 5);
} }, 'fun').name('Random splats'); } }, 'fun').name('Random splats');
let captureFolder = gui.addFolder('Capture');
captureFolder.addColor(config, 'BACK_COLOR').name('background color');
captureFolder.add(config, 'TRANSPARENT').name('transparent');
captureFolder.add({ fun: CaptureScreenshot }, 'fun').name('take screenshot');
let github = gui.add({ fun : () => { let github = gui.add({ fun : () => {
window.open('https://github.com/PavelDoGreat/WebGL-Fluid-Simulation'); window.open('https://github.com/PavelDoGreat/WebGL-Fluid-Simulation');
ga('send', 'event', 'link button', 'github'); ga('send', 'event', 'link button', 'github');
@@ -173,23 +198,64 @@ function startGUI () {
gui.close(); gui.close();
} }
function CaptureScreenshot () {
colorProgram.bind();
gl.uniform4f(colorProgram.uniforms.color, 0, 0, 0, 1);
blit(density.write.fbo);
render(density.write.fbo);
gl.bindFramebuffer(gl.FRAMEBUFFER, density.write.fbo);
let length = dyeWidth * dyeHeight * 4;
let pixels = new Float32Array(length);
gl.readPixels(0, 0, dyeWidth, dyeHeight, gl.RGBA, gl.FLOAT, pixels);
let newPixels = new Uint8Array(length);
let id = 0;
for (let i = dyeHeight - 1; i >= 0; i--) {
for (let j = 0; j < dyeWidth; j++) {
let nid = i * dyeWidth * 4 + j * 4;
newPixels[nid + 0] = clamp01(pixels[id + 0]) * 255;
newPixels[nid + 1] = clamp01(pixels[id + 1]) * 255;
newPixels[nid + 2] = clamp01(pixels[id + 2]) * 255;
newPixels[nid + 3] = clamp01(pixels[id + 3]) * 255;
id += 4;
}
}
let captureCanvas = document.createElement('canvas');
let ctx = captureCanvas.getContext('2d');
captureCanvas.width = dyeWidth;
captureCanvas.height = dyeHeight;
let imageData = ctx.createImageData(dyeWidth, dyeHeight);
imageData.data.set(newPixels);
ctx.putImageData(imageData, 0, 0);
let datauri = captureCanvas.toDataURL();
downloadURI("fluid.png", datauri);
URL.revokeObjectURL(datauri);
}
function clamp01 (input) {
return Math.min(Math.max(input, 0), 1);
}
function downloadURI (filename, uri) {
let link = document.createElement("a");
link.download = filename;
link.href = uri;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function isMobile () { function isMobile () {
return /Mobi|Android/i.test(navigator.userAgent); return /Mobi|Android/i.test(navigator.userAgent);
} }
function pointerPrototype () {
this.id = -1;
this.x = 0;
this.y = 0;
this.dx = 0;
this.dy = 0;
this.down = false;
this.moved = false;
this.color = [30, 0, 300];
}
pointers.push(new pointerPrototype());
class GLProgram { class GLProgram {
constructor (vertexShader, fragmentShader) { constructor (vertexShader, fragmentShader) {
this.uniforms = {}; this.uniforms = {};
@@ -227,7 +293,6 @@ function compileShader (type, source) {
const baseVertexShader = compileShader(gl.VERTEX_SHADER, ` const baseVertexShader = compileShader(gl.VERTEX_SHADER, `
precision highp float; precision highp float;
precision mediump sampler2D;
attribute vec2 aPosition; attribute vec2 aPosition;
varying vec2 vUv; varying vec2 vUv;
@@ -248,10 +313,10 @@ const baseVertexShader = compileShader(gl.VERTEX_SHADER, `
`); `);
const clearShader = compileShader(gl.FRAGMENT_SHADER, ` const clearShader = compileShader(gl.FRAGMENT_SHADER, `
precision highp float; precision mediump float;
precision mediump sampler2D; precision mediump sampler2D;
varying vec2 vUv; varying highp vec2 vUv;
uniform sampler2D uTexture; uniform sampler2D uTexture;
uniform float value; uniform float value;
@@ -260,21 +325,51 @@ const clearShader = compileShader(gl.FRAGMENT_SHADER, `
} }
`); `);
const colorShader = compileShader(gl.FRAGMENT_SHADER, `
precision mediump float;
uniform vec4 color;
void main () {
gl_FragColor = color;
}
`);
const backgroundShader = compileShader(gl.FRAGMENT_SHADER, `
precision highp float;
precision highp sampler2D;
varying vec2 vUv;
uniform sampler2D uTexture;
uniform float aspectRatio;
#define SCALE 25.0
void main () {
vec2 uv = floor(vUv * SCALE * vec2(aspectRatio, 1.0));
float v = mod(uv.x + uv.y, 2.0);
v = v * 0.1 + 0.8;
gl_FragColor = vec4(vec3(v), 1.0);
}
`);
const displayShader = compileShader(gl.FRAGMENT_SHADER, ` const displayShader = compileShader(gl.FRAGMENT_SHADER, `
precision highp float; precision highp float;
precision mediump sampler2D; precision highp sampler2D;
varying vec2 vUv; varying vec2 vUv;
uniform sampler2D uTexture; uniform sampler2D uTexture;
void main () { void main () {
gl_FragColor = texture2D(uTexture, vUv); vec3 C = texture2D(uTexture, vUv).rgb;
float a = max(C.r, max(C.g, C.b));
gl_FragColor = vec4(C, a);
} }
`); `);
const displayShadingShader = compileShader(gl.FRAGMENT_SHADER, ` const displayShadingShader = compileShader(gl.FRAGMENT_SHADER, `
precision highp float; precision highp float;
precision mediump sampler2D; precision highp sampler2D;
varying vec2 vUv; varying vec2 vUv;
varying vec2 vL; varying vec2 vL;
@@ -300,13 +395,14 @@ const displayShadingShader = compileShader(gl.FRAGMENT_SHADER, `
float diffuse = clamp(dot(n, l) + 0.7, 0.7, 1.0); float diffuse = clamp(dot(n, l) + 0.7, 0.7, 1.0);
C.rgb *= diffuse; C.rgb *= diffuse;
gl_FragColor = vec4(C, 1.0); float a = max(C.r, max(C.g, C.b));
gl_FragColor = vec4(C, a);
} }
`); `);
const splatShader = compileShader(gl.FRAGMENT_SHADER, ` const splatShader = compileShader(gl.FRAGMENT_SHADER, `
precision highp float; precision highp float;
precision mediump sampler2D; precision highp sampler2D;
varying vec2 vUv; varying vec2 vUv;
uniform sampler2D uTarget; uniform sampler2D uTarget;
@@ -326,7 +422,7 @@ const splatShader = compileShader(gl.FRAGMENT_SHADER, `
const advectionManualFilteringShader = compileShader(gl.FRAGMENT_SHADER, ` const advectionManualFilteringShader = compileShader(gl.FRAGMENT_SHADER, `
precision highp float; precision highp float;
precision mediump sampler2D; precision highp sampler2D;
varying vec2 vUv; varying vec2 vUv;
uniform sampler2D uVelocity; uniform sampler2D uVelocity;
@@ -336,7 +432,7 @@ const advectionManualFilteringShader = compileShader(gl.FRAGMENT_SHADER, `
uniform float dt; uniform float dt;
uniform float dissipation; uniform float dissipation;
vec4 bilerp (in sampler2D sam, in vec2 uv, in vec2 tsize) { vec4 bilerp (sampler2D sam, vec2 uv, vec2 tsize) {
vec2 st = uv / tsize - 0.5; vec2 st = uv / tsize - 0.5;
vec2 iuv = floor(st); vec2 iuv = floor(st);
@@ -359,7 +455,7 @@ const advectionManualFilteringShader = compileShader(gl.FRAGMENT_SHADER, `
const advectionShader = compileShader(gl.FRAGMENT_SHADER, ` const advectionShader = compileShader(gl.FRAGMENT_SHADER, `
precision highp float; precision highp float;
precision mediump sampler2D; precision highp sampler2D;
varying vec2 vUv; varying vec2 vUv;
uniform sampler2D uVelocity; uniform sampler2D uVelocity;
@@ -376,44 +472,42 @@ const advectionShader = compileShader(gl.FRAGMENT_SHADER, `
`); `);
const divergenceShader = compileShader(gl.FRAGMENT_SHADER, ` const divergenceShader = compileShader(gl.FRAGMENT_SHADER, `
precision highp float; precision mediump float;
precision mediump sampler2D; precision mediump sampler2D;
varying vec2 vUv; varying highp vec2 vUv;
varying vec2 vL; varying highp vec2 vL;
varying vec2 vR; varying highp vec2 vR;
varying vec2 vT; varying highp vec2 vT;
varying vec2 vB; varying highp vec2 vB;
uniform sampler2D uVelocity; uniform sampler2D uVelocity;
vec2 sampleVelocity (in vec2 uv) {
vec2 multiplier = vec2(1.0, 1.0);
if (uv.x < 0.0) { uv.x = 0.0; multiplier.x = -1.0; }
if (uv.x > 1.0) { uv.x = 1.0; multiplier.x = -1.0; }
if (uv.y < 0.0) { uv.y = 0.0; multiplier.y = -1.0; }
if (uv.y > 1.0) { uv.y = 1.0; multiplier.y = -1.0; }
return multiplier * texture2D(uVelocity, uv).xy;
}
void main () { void main () {
float L = sampleVelocity(vL).x; float L = texture2D(uVelocity, vL).x;
float R = sampleVelocity(vR).x; float R = texture2D(uVelocity, vR).x;
float T = sampleVelocity(vT).y; float T = texture2D(uVelocity, vT).y;
float B = sampleVelocity(vB).y; float B = texture2D(uVelocity, vB).y;
vec2 C = texture2D(uVelocity, vUv).xy;
if (vL.x < 0.0) { L = -C.x; }
if (vR.x > 1.0) { R = -C.x; }
if (vT.y > 1.0) { T = -C.y; }
if (vB.y < 0.0) { B = -C.y; }
float div = 0.5 * (R - L + T - B); float div = 0.5 * (R - L + T - B);
gl_FragColor = vec4(div, 0.0, 0.0, 1.0); gl_FragColor = vec4(div, 0.0, 0.0, 1.0);
} }
`); `);
const curlShader = compileShader(gl.FRAGMENT_SHADER, ` const curlShader = compileShader(gl.FRAGMENT_SHADER, `
precision highp float; precision mediump float;
precision mediump sampler2D; precision mediump sampler2D;
varying vec2 vUv; varying highp vec2 vUv;
varying vec2 vL; varying highp vec2 vL;
varying vec2 vR; varying highp vec2 vR;
varying vec2 vT; varying highp vec2 vT;
varying vec2 vB; varying highp vec2 vB;
uniform sampler2D uVelocity; uniform sampler2D uVelocity;
void main () { void main () {
@@ -428,7 +522,7 @@ const curlShader = compileShader(gl.FRAGMENT_SHADER, `
const vorticityShader = compileShader(gl.FRAGMENT_SHADER, ` const vorticityShader = compileShader(gl.FRAGMENT_SHADER, `
precision highp float; precision highp float;
precision mediump sampler2D; precision highp sampler2D;
varying vec2 vUv; varying vec2 vUv;
varying vec2 vL; varying vec2 vL;
@@ -458,20 +552,22 @@ const vorticityShader = compileShader(gl.FRAGMENT_SHADER, `
`); `);
const pressureShader = compileShader(gl.FRAGMENT_SHADER, ` const pressureShader = compileShader(gl.FRAGMENT_SHADER, `
precision highp float; precision mediump float;
precision mediump sampler2D; precision mediump sampler2D;
varying vec2 vUv; varying highp vec2 vUv;
varying vec2 vL; varying highp vec2 vL;
varying vec2 vR; varying highp vec2 vR;
varying vec2 vT; varying highp vec2 vT;
varying vec2 vB; varying highp vec2 vB;
uniform sampler2D uPressure; uniform sampler2D uPressure;
uniform sampler2D uDivergence; uniform sampler2D uDivergence;
vec2 boundary (in vec2 uv) { vec2 boundary (vec2 uv) {
uv = min(max(uv, 0.0), 1.0);
return uv; return uv;
// uncomment if you use wrap or repeat texture mode
// uv = min(max(uv, 0.0), 1.0);
// return uv;
} }
void main () { void main () {
@@ -487,20 +583,21 @@ const pressureShader = compileShader(gl.FRAGMENT_SHADER, `
`); `);
const gradientSubtractShader = compileShader(gl.FRAGMENT_SHADER, ` const gradientSubtractShader = compileShader(gl.FRAGMENT_SHADER, `
precision highp float; precision mediump float;
precision mediump sampler2D; precision mediump sampler2D;
varying vec2 vUv; varying highp vec2 vUv;
varying vec2 vL; varying highp vec2 vL;
varying vec2 vR; varying highp vec2 vR;
varying vec2 vT; varying highp vec2 vT;
varying vec2 vB; varying highp vec2 vB;
uniform sampler2D uPressure; uniform sampler2D uPressure;
uniform sampler2D uVelocity; uniform sampler2D uVelocity;
vec2 boundary (in vec2 uv) { vec2 boundary (vec2 uv) {
uv = min(max(uv, 0.0), 1.0);
return uv; return uv;
// uv = min(max(uv, 0.0), 1.0);
// return uv;
} }
void main () { void main () {
@@ -539,6 +636,8 @@ let curl;
let pressure; let pressure;
const clearProgram = new GLProgram(baseVertexShader, clearShader); const clearProgram = new GLProgram(baseVertexShader, clearShader);
const colorProgram = new GLProgram(baseVertexShader, colorShader);
const backgroundProgram = new GLProgram(baseVertexShader, backgroundShader);
const displayProgram = new GLProgram(baseVertexShader, displayShader); const displayProgram = new GLProgram(baseVertexShader, displayShader);
const displayShadingProgram = new GLProgram(baseVertexShader, displayShadingShader); const displayShadingProgram = new GLProgram(baseVertexShader, displayShadingShader);
const splatProgram = new GLProgram(baseVertexShader, splatShader); const splatProgram = new GLProgram(baseVertexShader, splatShader);
@@ -575,8 +674,8 @@ function getResolution (resolution) {
if (aspectRatio < 1) if (aspectRatio < 1)
aspectRatio = 1.0 / aspectRatio; aspectRatio = 1.0 / aspectRatio;
let max = resolution * aspectRatio; let max = Math.round(resolution * aspectRatio);
let min = resolution; let min = Math.round(resolution);
if (gl.drawingBufferWidth > gl.drawingBufferHeight) if (gl.drawingBufferWidth > gl.drawingBufferHeight)
return { width: max, height: min }; return { width: max, height: min };
@@ -629,13 +728,16 @@ function createDoubleFBO (texId, w, h, internalFormat, format, type, param) {
initFramebuffers(); initFramebuffers();
multipleSplats(parseInt(Math.random() * 20) + 5); multipleSplats(parseInt(Math.random() * 20) + 5);
let lastColorChangeTime = Date.now();
update(); update();
function update () { function update () {
resizeCanvas(); resizeCanvas();
input(); input();
step(0.016); if (!config.PAUSED)
render(); step(0.016);
render(null);
requestAnimationFrame(update); requestAnimationFrame(update);
} }
@@ -644,15 +746,28 @@ function input () {
multipleSplats(splatStack.pop()); multipleSplats(splatStack.pop());
for (let i = 0; i < pointers.length; i++) { for (let i = 0; i < pointers.length; i++) {
const pointer = pointers[i]; const p = pointers[i];
if (pointer.moved) { if (p.moved) {
splat(pointer.x, pointer.y, pointer.dx, pointer.dy, pointer.color); splat(p.x, p.y, p.dx, p.dy, p.color);
pointer.moved = false; p.moved = false;
}
}
if (!config.COLORFUL)
return;
if (lastColorChangeTime + 100 < Date.now())
{
lastColorChangeTime = Date.now();
for (let i = 0; i < pointers.length; i++) {
const p = pointers[i];
p.color = generateColor();
} }
} }
} }
function step (dt) { function step (dt) {
gl.disable(gl.BLEND);
gl.viewport(0, 0, simWidth, simHeight); gl.viewport(0, 0, simWidth, simHeight);
curlProgram.bind(); curlProgram.bind();
@@ -714,8 +829,9 @@ function step (dt) {
velocity.swap(); velocity.swap();
gl.viewport(0, 0, dyeWidth, dyeHeight); gl.viewport(0, 0, dyeWidth, dyeHeight);
if (!ext.supportLinearFiltering) if (!ext.supportLinearFiltering)
gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, 1.0 / dyeWidth, 1.0 / dyeWidth); gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, 1.0 / dyeWidth, 1.0 / dyeHeight);
gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.texId); gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.texId);
gl.uniform1i(advectionProgram.uniforms.uSource, density.read.texId); gl.uniform1i(advectionProgram.uniforms.uSource, density.read.texId);
gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION); gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION);
@@ -723,12 +839,33 @@ function step (dt) {
density.swap(); density.swap();
} }
function render () { function render (target) {
let width = gl.drawingBufferWidth; if (target == null || !config.TRANSPARENT) {
let height = gl.drawingBufferHeight; gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.enable(gl.BLEND);
}
else {
gl.disable(gl.BLEND);
}
let width = target == null ? gl.drawingBufferWidth : dyeWidth;
let height = target == null ? gl.drawingBufferHeight : dyeHeight;
gl.viewport(0, 0, width, height); gl.viewport(0, 0, width, height);
if (!config.TRANSPARENT) {
colorProgram.bind();
let bc = config.BACK_COLOR;
gl.uniform4f(colorProgram.uniforms.color, bc.r / 255, bc.g / 255, bc.b / 255, 1);
blit(target);
}
if (target == null && config.TRANSPARENT) {
backgroundProgram.bind();
gl.uniform1f(backgroundProgram.uniforms.aspectRatio, canvas.width / canvas.height);
blit(null);
}
if (config.SHADING) { if (config.SHADING) {
displayShadingProgram.bind(); displayShadingProgram.bind();
gl.uniform2f(displayShadingProgram.uniforms.texelSize, 1.0 / width, 1.0 / height); gl.uniform2f(displayShadingProgram.uniforms.texelSize, 1.0 / width, 1.0 / height);
@@ -739,7 +876,7 @@ function render () {
gl.uniform1i(displayProgram.uniforms.uTexture, density.read.texId); gl.uniform1i(displayProgram.uniforms.uTexture, density.read.texId);
} }
blit(null); blit(target);
} }
function splat (x, y, dx, dy, color) { function splat (x, y, dx, dy, color) {
@@ -782,7 +919,7 @@ function resizeCanvas () {
} }
} }
canvas.addEventListener('mousemove', (e) => { canvas.addEventListener('mousemove', e => {
pointers[0].moved = pointers[0].down; pointers[0].moved = pointers[0].down;
pointers[0].dx = (e.offsetX - pointers[0].x) * 5.0; pointers[0].dx = (e.offsetX - pointers[0].x) * 5.0;
pointers[0].dy = (e.offsetY - pointers[0].y) * 5.0; pointers[0].dy = (e.offsetY - pointers[0].y) * 5.0;
@@ -790,7 +927,7 @@ canvas.addEventListener('mousemove', (e) => {
pointers[0].y = e.offsetY; pointers[0].y = e.offsetY;
}); });
canvas.addEventListener('touchmove', (e) => { canvas.addEventListener('touchmove', e => {
e.preventDefault(); e.preventDefault();
const touches = e.targetTouches; const touches = e.targetTouches;
for (let i = 0; i < touches.length; i++) { for (let i = 0; i < touches.length; i++) {
@@ -808,7 +945,7 @@ canvas.addEventListener('mousedown', () => {
pointers[0].color = generateColor(); pointers[0].color = generateColor();
}); });
canvas.addEventListener('touchstart', (e) => { canvas.addEventListener('touchstart', e => {
e.preventDefault(); e.preventDefault();
const touches = e.targetTouches; const touches = e.targetTouches;
for (let i = 0; i < touches.length; i++) { for (let i = 0; i < touches.length; i++) {
@@ -827,7 +964,7 @@ window.addEventListener('mouseup', () => {
pointers[0].down = false; pointers[0].down = false;
}); });
window.addEventListener('touchend', (e) => { window.addEventListener('touchend', e => {
const touches = e.changedTouches; const touches = e.changedTouches;
for (let i = 0; i < touches.length; i++) for (let i = 0; i < touches.length; i++)
for (let j = 0; j < pointers.length; j++) for (let j = 0; j < pointers.length; j++)
@@ -835,10 +972,39 @@ window.addEventListener('touchend', (e) => {
pointers[j].down = false; pointers[j].down = false;
}); });
window.addEventListener('keydown', e => {
if (e.key === 'p')
config.PAUSED = !config.PAUSED;
});
function generateColor () { function generateColor () {
let c = HSVtoRGB(Math.random(), 1.0, 1.0);
c.r *= 0.15;
c.g *= 0.15;
c.b *= 0.15;
return c;
}
function HSVtoRGB (h, s, v) {
let r, g, b, i, f, p, q, t;
i = Math.floor(h * 6);
f = h * 6 - i;
p = v * (1 - s);
q = v * (1 - f * s);
t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
return { return {
r: Math.random() * 0.15 + 0.05, r,
g: Math.random() * 0.15 + 0.05, g,
b: Math.random() * 0.15 + 0.05 b
}; };
} }