'use strict' import { createDoubleFBO, createFBO, downloadURI, framebufferToTexture, generateColor, getResolution, getWebGLContext, isMobile, normalizeColor, normalizeTexture, resizeCanvas, scaleByPixelRatio, textureToCanvas, updatePointerDownData, updatePointerMoveData, updatePointerUpData, wrap } from "./util" import factory from './factory' import config from './config' import Pointer from './Pointer' const WebGLFluid = canvas => { resizeCanvas(canvas) let pointers = [] let splatStack = [] let bloomFrameBuffers = [] let lastUpdateTime = Date.now() let colorUpdateTimer = 0.0 pointers.push(new Pointer()) const {gl, ext} = getWebGLContext(canvas) if (isMobile()) { config.DYE_RESOLUTION = 512 } if (!ext.supportLinearFiltering) { config.DYE_RESOLUTION = 512 config.SHADING = false config.BLOOM = false config.SUNRAYS = false } let dye let velocity let divergence let curl let pressure let bloom let sunrays let sunraysTemp const blit = (() => { gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()) gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), gl.STATIC_DRAW) gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer()) gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW) gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0) gl.enableVertexAttribArray(0) return destination => { gl.bindFramebuffer(gl.FRAMEBUFFER, destination) gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0) } })() const { blurProgram, copyProgram, clearProgram, colorProgram, checkerboardProgram, bloomPrefilterProgram, bloomBlurProgram, bloomFinalProgram, sunraysMaskProgram, sunraysProgram, splatProgram, advectionProgram, divergenceProgram, curlProgram, vorticityProgram, pressureProgram, gradienSubtractProgram, displayMaterial } = factory(gl, ext) function startGUI() { if (isMobile()) return let gui = new dat.GUI({width: 300}) gui .add(config, 'DYE_RESOLUTION', {high: 1024, medium: 512, low: 256, 'very low': 128}) .name('quality') .onFinishChange(initFrameBuffers()) gui .add(config, 'SIM_RESOLUTION', {'32': 32, '64': 64, '128': 128, '256': 256}) .name('sim resolution') .onFinishChange(initFrameBuffers()) gui.add(config, 'DENSITY_DISSIPATION', 0, 4.0).name('density diffusion') gui.add(config, 'VELOCITY_DISSIPATION', 0, 4.0).name('velocity diffusion') gui.add(config, 'PRESSURE', 0.0, 1.0).name('pressure') 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, 'SHADING') .name('shading') .onFinishChange(updateKeywords) gui.add(config, 'COLORFUL').name('colorful') gui .add(config, 'PAUSED') .name('paused') .listen() gui .add({fun: () => splatStack.push(parseInt(Math.random() * 20) + 5)}, 'fun') .name('Random splats') let bloomFolder = gui.addFolder('Bloom') bloomFolder .add(config, 'BLOOM') .name('enabled') .onFinishChange(updateKeywords) bloomFolder.add(config, 'BLOOM_INTENSITY', 0.1, 2.0).name('intensity') bloomFolder.add(config, 'BLOOM_THRESHOLD', 0.0, 1.0).name('threshold') let sunraysFolder = gui.addFolder('Sunrays') sunraysFolder .add(config, 'SUNRAYS') .name('enabled') .onFinishChange(updateKeywords) sunraysFolder.add(config, 'SUNRAYS_WEIGHT', 0.3, 1.0).name('weight') 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') } function initFrameBuffers() { let simRes = getResolution(gl, config.SIM_RESOLUTION) let dyeRes = getResolution(gl, config.DYE_RESOLUTION) const texType = ext.halfFloatTexType const rgba = ext.formatRGBA const rg = ext.formatRG const r = ext.formatR const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST if (dye == null) dye = createDoubleFBO(gl, dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering) else dye = resizeDoubleFBO(dye, dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering) if (velocity == null) velocity = createDoubleFBO(gl, simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering) else velocity = resizeDoubleFBO(velocity, simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering) divergence = createFBO(gl, simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST) curl = createFBO(gl, simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST) pressure = createDoubleFBO(gl, simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST) initBloomFrameBuffers() initSunraysFrameBuffers() } function initBloomFrameBuffers() { let res = getResolution(gl, config.BLOOM_RESOLUTION) const texType = ext.halfFloatTexType const rgba = ext.formatRGBA const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST bloom = createFBO(gl, res.width, res.height, rgba.internalFormat, rgba.format, texType, filtering) bloomFrameBuffers.length = 0 for (let i = 0; i < config.BLOOM_ITERATIONS; i++) { let width = res.width >> (i + 1) let height = res.height >> (i + 1) if (width < 2 || height < 2) break let fbo = createFBO(gl, width, height, rgba.internalFormat, rgba.format, texType, filtering) bloomFrameBuffers.push(fbo) } } function initSunraysFrameBuffers() { let res = getResolution(gl, config.SUNRAYS_RESOLUTION) const texType = ext.halfFloatTexType const r = ext.formatR const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST sunrays = createFBO(gl, res.width, res.height, r.internalFormat, r.format, texType, filtering) sunraysTemp = createFBO(gl, res.width, res.height, r.internalFormat, r.format, texType, filtering) } function resizeFBO(target, w, h, internalFormat, format, type, param) { let newFBO = createFBO(gl, w, h, internalFormat, format, type, param) copyProgram.bind(gl) gl.uniform1i(copyProgram.uniforms.uTexture, target.attach(0)) blit(newFBO.fbo) return newFBO } function resizeDoubleFBO(target, w, h, internalFormat, format, type, param) { if (target.width === w && target.height === h) return target target.read = resizeFBO(target.read, w, h, internalFormat, format, type, param) target.write = createFBO(gl, w, h, internalFormat, format, type, param) target.width = w target.height = h target.texelSizeX = 1.0 / w target.texelSizeY = 1.0 / h return target } function updateKeywords() { let displayKeywords = [] if (config.SHADING) displayKeywords.push('SHADING') if (config.BLOOM) displayKeywords.push('BLOOM') if (config.SUNRAYS) displayKeywords.push('SUNRAYS') displayMaterial.setKeywords(gl, displayKeywords) } function update() { const dt = calcDeltaTime() if (resizeCanvas(canvas)) initFrameBuffers() updateColors(dt) applyInputs() if (!config.PAUSED) step(dt) render(null) window.requestAnimationFrame(update) } function calcDeltaTime() { let now = Date.now() let dt = (now - lastUpdateTime) / 1000 dt = Math.min(dt, 0.016666) lastUpdateTime = now return dt } function updateColors(dt) { if (!config.COLORFUL) return colorUpdateTimer += dt * config.COLOR_UPDATE_SPEED if (colorUpdateTimer >= 1) { colorUpdateTimer = wrap(colorUpdateTimer, 0, 1) pointers.forEach(p => { p.color = generateColor() }) } } function applyInputs() { if (splatStack.length > 0) multipleSplats(splatStack.pop()) pointers.forEach(p => { if (p.moved) { p.moved = false splatPointer(p) } }) } function step(dt) { gl.disable(gl.BLEND) gl.viewport(0, 0, velocity.width, velocity.height) curlProgram.bind(gl) gl.uniform2f(curlProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY) gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.read.attach(0)) blit(curl.fbo) vorticityProgram.bind(gl) gl.uniform2f(vorticityProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY) gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.read.attach(0)) gl.uniform1i(vorticityProgram.uniforms.uCurl, curl.attach(1)) gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL) gl.uniform1f(vorticityProgram.uniforms.dt, dt) blit(velocity.write.fbo) velocity.swap() divergenceProgram.bind(gl) gl.uniform2f(divergenceProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY) gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0)) blit(divergence.fbo) clearProgram.bind(gl) gl.uniform1i(clearProgram.uniforms.uTexture, pressure.read.attach(0)) gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE) blit(pressure.write.fbo) pressure.swap() pressureProgram.bind(gl) gl.uniform2f(pressureProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY) gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence.attach(0)) for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) { gl.uniform1i(pressureProgram.uniforms.uPressure, pressure.read.attach(1)) blit(pressure.write.fbo) pressure.swap() } gradienSubtractProgram.bind(gl) gl.uniform2f(gradienSubtractProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY) gl.uniform1i(gradienSubtractProgram.uniforms.uPressure, pressure.read.attach(0)) gl.uniform1i(gradienSubtractProgram.uniforms.uVelocity, velocity.read.attach(1)) blit(velocity.write.fbo) velocity.swap() advectionProgram.bind(gl) gl.uniform2f(advectionProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY) if (!ext.supportLinearFiltering) gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, velocity.texelSizeX, velocity.texelSizeY) let velocityId = velocity.read.attach(0) gl.uniform1i(advectionProgram.uniforms.uVelocity, velocityId) gl.uniform1i(advectionProgram.uniforms.uSource, velocityId) gl.uniform1f(advectionProgram.uniforms.dt, dt) gl.uniform1f(advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION) blit(velocity.write.fbo) velocity.swap() gl.viewport(0, 0, dye.width, dye.height) if (!ext.supportLinearFiltering) gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, dye.texelSizeX, dye.texelSizeY) gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0)) gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1)) gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION) blit(dye.write.fbo) dye.swap() } function render(target) { if (config.BLOOM) { applyBloom(dye.read, bloom) } if (config.SUNRAYS) { applySunrays(dye.read, dye.write, sunrays) blur(sunrays, sunraysTemp, 1) } if (target == null || !config.TRANSPARENT) { gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA) gl.enable(gl.BLEND) } else { gl.disable(gl.BLEND) } let width = target == null ? gl.drawingBufferWidth : target.width let height = target == null ? gl.drawingBufferHeight : target.height gl.viewport(0, 0, width, height) let fbo = target == null ? null : target.fbo if (!config.TRANSPARENT) drawColor(fbo, normalizeColor(config.BACK_COLOR)) if (target == null && config.TRANSPARENT) drawCheckerboard(fbo) drawDisplay(fbo, width, height) } function drawColor(fbo, color) { colorProgram.bind(gl) gl.uniform4f(colorProgram.uniforms.color, color.r, color.g, color.b, 1) blit(fbo) } function drawCheckerboard(fbo) { checkerboardProgram.bind(gl) gl.uniform1f(checkerboardProgram.uniforms.aspectRatio, canvas.width / canvas.height) blit(fbo) } function drawDisplay(fbo, width, height) { displayMaterial.bind(gl) if (config.SHADING) gl.uniform2f(displayMaterial.uniforms.texelSize, 1.0 / width, 1.0 / height) gl.uniform1i(displayMaterial.uniforms.uTexture, dye.read.attach(0)) if (config.BLOOM) { gl.uniform1i(displayMaterial.uniforms.uBloom, bloom.attach(1)) } if (config.SUNRAYS) gl.uniform1i(displayMaterial.uniforms.uSunrays, sunrays.attach(3)) blit(fbo) } function applyBloom(source, destination) { if (bloomFrameBuffers.length < 2) return let last = destination gl.disable(gl.BLEND) bloomPrefilterProgram.bind(gl) let knee = config.BLOOM_THRESHOLD * config.BLOOM_SOFT_KNEE + 0.0001 let curve0 = config.BLOOM_THRESHOLD - knee let curve1 = knee * 2 let curve2 = 0.25 / knee gl.uniform3f(bloomPrefilterProgram.uniforms.curve, curve0, curve1, curve2) gl.uniform1f(bloomPrefilterProgram.uniforms.threshold, config.BLOOM_THRESHOLD) gl.uniform1i(bloomPrefilterProgram.uniforms.uTexture, source.attach(0)) gl.viewport(0, 0, last.width, last.height) blit(last.fbo) bloomBlurProgram.bind(gl) for (let i = 0; i < bloomFrameBuffers.length; i++) { let dest = bloomFrameBuffers[i] gl.uniform2f(bloomBlurProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY) gl.uniform1i(bloomBlurProgram.uniforms.uTexture, last.attach(0)) gl.viewport(0, 0, dest.width, dest.height) blit(dest.fbo) last = dest } gl.blendFunc(gl.ONE, gl.ONE) gl.enable(gl.BLEND) for (let i = bloomFrameBuffers.length - 2; i >= 0; i--) { let baseTex = bloomFrameBuffers[i] gl.uniform2f(bloomBlurProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY) gl.uniform1i(bloomBlurProgram.uniforms.uTexture, last.attach(0)) gl.viewport(0, 0, baseTex.width, baseTex.height) blit(baseTex.fbo) last = baseTex } gl.disable(gl.BLEND) bloomFinalProgram.bind(gl) gl.uniform2f(bloomFinalProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY) gl.uniform1i(bloomFinalProgram.uniforms.uTexture, last.attach(0)) gl.uniform1f(bloomFinalProgram.uniforms.intensity, config.BLOOM_INTENSITY) gl.viewport(0, 0, destination.width, destination.height) blit(destination.fbo) } function applySunrays(source, mask, destination) { gl.disable(gl.BLEND) sunraysMaskProgram.bind(gl) gl.uniform1i(sunraysMaskProgram.uniforms.uTexture, source.attach(0)) gl.viewport(0, 0, mask.width, mask.height) blit(mask.fbo) sunraysProgram.bind(gl) gl.uniform1f(sunraysProgram.uniforms.weight, config.SUNRAYS_WEIGHT) gl.uniform1i(sunraysProgram.uniforms.uTexture, mask.attach(0)) gl.viewport(0, 0, destination.width, destination.height) blit(destination.fbo) } function blur(target, temp, iterations) { blurProgram.bind(gl) for (let i = 0; i < iterations; i++) { gl.uniform2f(blurProgram.uniforms.texelSize, target.texelSizeX, 0.0) gl.uniform1i(blurProgram.uniforms.uTexture, target.attach(0)) blit(temp.fbo) gl.uniform2f(blurProgram.uniforms.texelSize, 0.0, target.texelSizeY) gl.uniform1i(blurProgram.uniforms.uTexture, temp.attach(0)) blit(target.fbo) } } function splatPointer(pointer) { let dx = pointer.deltaX * config.SPLAT_FORCE let dy = pointer.deltaY * config.SPLAT_FORCE splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color) } function multipleSplats(amount) { for (let i = 0; i < amount; i++) { const color = generateColor() color.r *= 10.0 color.g *= 10.0 color.b *= 10.0 const x = Math.random() const y = Math.random() const dx = 1000 * (Math.random() - 0.5) const dy = 1000 * (Math.random() - 0.5) splat(x, y, dx, dy, color) } } function splat(x, y, dx, dy, color) { gl.viewport(0, 0, velocity.width, velocity.height) splatProgram.bind(gl) gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0)) gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height) gl.uniform2f(splatProgram.uniforms.point, x, y) gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0.0) gl.uniform1f(splatProgram.uniforms.radius, correctRadius(config.SPLAT_RADIUS / 100.0)) blit(velocity.write.fbo) velocity.swap() gl.viewport(0, 0, dye.width, dye.height) gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0)) gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b) blit(dye.write.fbo) dye.swap() } function correctRadius(radius) { let aspectRatio = canvas.width / canvas.height if (aspectRatio > 1) radius *= aspectRatio return radius } function captureScreenShot() { let res = getResolution(gl, config.CAPTURE_RESOLUTION) let target = createFBO(gl, res.width, res.height, ext.formatRGBA.internalFormat, ext.formatRGBA.format, ext.halfFloatTexType, gl.NEAREST) render(target) let texture = framebufferToTexture(gl, target) texture = normalizeTexture(texture, target.width, target.height) let captureCanvas = textureToCanvas(texture, target.width, target.height) let dataUri = captureCanvas.toDataURL() downloadURI('fluid.png', dataUri) URL.revokeObjectURL(dataUri) } //startGUI(); updateKeywords() initFrameBuffers() multipleSplats(parseInt(Math.random() * 20) + 5) update() canvas.addEventListener('mousedown', e => { let posX = scaleByPixelRatio(e.offsetX) let posY = scaleByPixelRatio(e.offsetY) let pointer = pointers.find(p => p.id === -1) if (pointer == null) pointer = new Pointer() updatePointerDownData(canvas, pointer, -1, posX, posY) }) canvas.addEventListener('mousemove', e => { let posX = scaleByPixelRatio(e.offsetX) let posY = scaleByPixelRatio(e.offsetY) updatePointerMoveData(canvas, pointers[0], posX, posY) }) canvas.addEventListener('touchstart', e => { e.preventDefault() const touches = e.targetTouches while (touches.length >= pointers.length) pointers.push(new Pointer()) for (let i = 0; i < touches.length; i++) { let posX = scaleByPixelRatio(touches[i].pageX) let posY = scaleByPixelRatio(touches[i].pageY) updatePointerDownData(canvas, pointers[i + 1], touches[i].identifier, posX, posY) } }) canvas.addEventListener('touchmove', e => { e.preventDefault() const touches = e.targetTouches for (let i = 0; i < touches.length; i++) { let posX = scaleByPixelRatio(touches[i].pageX) let posY = scaleByPixelRatio(touches[i].pageY) updatePointerMoveData(canvas, pointers[i + 1], posX, posY) } }) window.addEventListener('mouseup', () => { updatePointerUpData(pointers[0]) }) window.addEventListener('touchend', e => { const touches = e.changedTouches for (let i = 0; i < touches.length; i++) { let pointer = pointers.find(p => p.id === touches[i].identifier) updatePointerUpData(pointer) } }) window.addEventListener('keydown', e => { if (e.code === 'KeyP') config.PAUSED = !config.PAUSED if (e.key === ' ') splatStack.push(parseInt(Math.random() * 20) + 5) if (e.key === 's') { captureScreenShot() } }) } export default WebGLFluid