├── cover.jpg ├── src ├── app │ ├── is-ios.js │ ├── shader │ │ ├── sph │ │ │ ├── indices.vert.glsl │ │ │ ├── offset.vert.glsl │ │ │ ├── sort.vert.glsl │ │ │ ├── force.vert.glsl │ │ │ ├── integrate.vert.glsl │ │ │ ├── pressure.vert.glsl │ │ │ ├── draw.frag.glsl │ │ │ ├── indices.frag.glsl │ │ │ ├── offset.frag.glsl │ │ │ ├── pressure.frag.glsl │ │ │ ├── sort.frag.glsl │ │ │ ├── draw.vert.glsl │ │ │ ├── integrate.frag.glsl │ │ │ ├── force.frag.glsl │ │ │ └── utils │ │ │ │ └── particle-utils.glsl │ │ ├── light-depth.frag.glsl │ │ ├── blur.vert.glsl │ │ ├── test.vert.glsl │ │ ├── composite.vert.glsl │ │ ├── highpass.vert.glsl │ │ ├── test.frag.glsl │ │ ├── composite.frag.glsl │ │ ├── blur.frag.glsl │ │ ├── highpass.frag.glsl │ │ ├── bead.vert.glsl │ │ └── bead.frag.glsl │ ├── _sketch-template.js │ ├── app.js │ ├── utils │ │ ├── modernizr.js │ │ └── glb-builder.js │ ├── audio-repeater.js │ └── sketch.js ├── assets │ ├── bead.glb │ ├── normal.png │ ├── env-map-01.jpg │ └── env-map-02.jpg ├── index.html └── styles.css ├── dist ├── assets │ ├── bead.glb │ └── normal.png ├── index-6ca6b2e6.css └── index.html ├── package.json ├── README.md ├── LICENSE ├── vite.config.js └── .gitignore /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-leitl/liquid-geo/HEAD/cover.jpg -------------------------------------------------------------------------------- /src/app/is-ios.js: -------------------------------------------------------------------------------- 1 | export const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); -------------------------------------------------------------------------------- /src/assets/bead.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-leitl/liquid-geo/HEAD/src/assets/bead.glb -------------------------------------------------------------------------------- /dist/assets/bead.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-leitl/liquid-geo/HEAD/dist/assets/bead.glb -------------------------------------------------------------------------------- /dist/assets/normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-leitl/liquid-geo/HEAD/dist/assets/normal.png -------------------------------------------------------------------------------- /src/assets/normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-leitl/liquid-geo/HEAD/src/assets/normal.png -------------------------------------------------------------------------------- /src/assets/env-map-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-leitl/liquid-geo/HEAD/src/assets/env-map-01.jpg -------------------------------------------------------------------------------- /src/assets/env-map-02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-leitl/liquid-geo/HEAD/src/assets/env-map-02.jpg -------------------------------------------------------------------------------- /src/app/shader/sph/indices.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec2 a_position; 4 | 5 | void main() { 6 | gl_Position = vec4(a_position, 0., 1.); 7 | } -------------------------------------------------------------------------------- /src/app/shader/sph/offset.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec2 a_position; 4 | 5 | void main() { 6 | gl_Position = vec4(a_position, 0., 1.); 7 | } -------------------------------------------------------------------------------- /src/app/shader/sph/sort.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec2 a_position; 4 | 5 | void main() { 6 | gl_Position = vec4(a_position, 0., 1.); 7 | } -------------------------------------------------------------------------------- /src/app/shader/light-depth.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision highp float; 3 | 4 | out vec4 outColor; 5 | 6 | void main() { 7 | outColor = vec4(0.); 8 | } -------------------------------------------------------------------------------- /src/app/shader/blur.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec2 a_position; 4 | 5 | out vec2 v_uv; 6 | 7 | void main() { 8 | v_uv = 0.5 * a_position + 0.5; 9 | gl_Position = vec4(a_position, 0., 1.); 10 | } -------------------------------------------------------------------------------- /src/app/shader/test.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec2 a_position; 4 | 5 | out vec2 v_uv; 6 | 7 | void main() { 8 | v_uv = 0.5 * a_position + 0.5; 9 | gl_Position = vec4(a_position, 0., 1.); 10 | } -------------------------------------------------------------------------------- /src/app/shader/composite.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec2 a_position; 4 | 5 | out vec2 v_uv; 6 | 7 | void main() { 8 | v_uv = 0.5 * a_position + 0.5; 9 | gl_Position = vec4(a_position, 0., 1.); 10 | } -------------------------------------------------------------------------------- /src/app/shader/highpass.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec2 a_position; 4 | 5 | out vec2 v_uv; 6 | 7 | void main() { 8 | v_uv = 0.5 * a_position + 0.5; 9 | gl_Position = vec4(a_position, 0., 1.); 10 | } -------------------------------------------------------------------------------- /src/app/shader/sph/force.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec2 a_position; 4 | 5 | out vec2 v_uv; 6 | 7 | void main() { 8 | v_uv = 0.5 * a_position + 0.5; 9 | gl_Position = vec4(a_position, 0., 1.); 10 | } -------------------------------------------------------------------------------- /src/app/shader/sph/integrate.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec2 a_position; 4 | 5 | out vec2 v_uv; 6 | 7 | void main() { 8 | v_uv = 0.5 * a_position + 0.5; 9 | gl_Position = vec4(a_position, 0., 1.); 10 | } -------------------------------------------------------------------------------- /src/app/shader/sph/pressure.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec2 a_position; 4 | 5 | out vec2 v_uv; 6 | 7 | void main() { 8 | v_uv = 0.5 * a_position + 0.5; 9 | gl_Position = vec4(a_position, 0., 1.); 10 | } -------------------------------------------------------------------------------- /src/app/shader/test.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | uniform sampler2D u_texture; 6 | 7 | out vec4 outColor; 8 | 9 | in vec2 v_uv; 10 | 11 | void main() { 12 | outColor = vec4(texture(u_texture, v_uv)); 13 | } -------------------------------------------------------------------------------- /src/app/shader/sph/draw.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | in float v_velocity; 6 | flat in vec4 v_color; 7 | 8 | out vec4 outColor; 9 | 10 | void main() { 11 | vec2 c = gl_PointCoord * 2. - 1.; 12 | float mask = 1. - smoothstep(0.7, 0.9, length(c)); 13 | outColor = vec4(0.4 * v_velocity, 0.9, 1., 1.) * mask; 14 | outColor = v_color * mask; 15 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "liquid-geo", 3 | "private": true, 4 | "version": "2.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite serve", 8 | "build": "vite build --emptyOutDir --base=/liquid-geo/dist/", 9 | "preview": "vite preview" 10 | }, 11 | "devDependencies": { 12 | "terser": "^5.14.2", 13 | "vite": "^3.0.0", 14 | "vite-plugin-glsl": "^0.3.0" 15 | }, 16 | "dependencies": { 17 | "@loaders.gl/core": "^3.2.3", 18 | "@loaders.gl/gltf": "^3.2.3", 19 | "gl-matrix": "^3.4.3", 20 | "rxjs": "^7.5.6", 21 | "tone": "^14.7.77", 22 | "tweakpane": "^3.1.0", 23 | "twgl.js": "^5.0.4" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/shader/sph/indices.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | uniform sampler2D u_positionTexture; 6 | uniform vec2 u_domainScale; 7 | uniform ivec2 u_cellTexSize; 8 | uniform float u_cellSize; 9 | 10 | #include ./utils/particle-utils.glsl; 11 | 12 | out uvec2 outIndices; 13 | 14 | void main() { 15 | ivec2 texSize = textureSize(u_positionTexture, 0); 16 | vec2 uv = gl_FragCoord.xy / vec2(texSize); 17 | 18 | int particleId = tex2ndx(texSize, ivec2(gl_FragCoord.xy)); 19 | 20 | ivec2 pi_tex = ndx2tex(texSize, particleId); 21 | vec4 pi = texelFetch(u_positionTexture, pi_tex, 0); 22 | int cellId = pos2CellId(pi.xy, u_cellTexSize, u_domainScale, u_cellSize); 23 | 24 | outIndices = uvec2(cellId, particleId); 25 | } -------------------------------------------------------------------------------- /src/app/shader/composite.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | uniform sampler2D u_colorTexture; 6 | uniform sampler2D u_bloomTexture; 7 | 8 | out vec4 outColor; 9 | 10 | in vec2 v_uv; 11 | 12 | float blendScreen(float base, float blend) { 13 | return 1.0-((1.0-base)*(1.0-blend)); 14 | } 15 | 16 | vec3 blendScreen(vec3 base, vec3 blend) { 17 | return vec3(blendScreen(base.r,blend.r),blendScreen(base.g,blend.g),blendScreen(base.b,blend.b)); 18 | } 19 | 20 | vec3 blendScreen(vec3 base, vec3 blend, float opacity) { 21 | return (blendScreen(base, blend) * opacity + base * (1.0 - opacity)); 22 | } 23 | 24 | void main() { 25 | vec4 color = texture(u_colorTexture, v_uv); 26 | vec4 bloom = texture(u_bloomTexture, v_uv); 27 | 28 | vec3 comp = blendScreen(color.rgb, bloom.rgb, 0.4); 29 | 30 | outColor = vec4(comp, 0.); 31 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Liquid-Geo 2 | 3 | ![Liquid-Geo Screenshot](https://github.com/robert-leitl/liquid-geo/blob/main/cover.jpg?raw=true) 4 | 5 | This web experiment is inspired by the liquid-geo interface designs form the movie [Man of Steel](https://en.wikipedia.org/wiki/Man_of_Steel_(film)). It allows the user to record a short audio snippet. This snippet can then be played back in a distorted version and the sphere responds to the audio signal. In addition, the beads on the surface of the sphere can be manipulated by touch or mouse movement. 6 | 7 | [DEMO](https://robert-leitl.github.io/liquid-geo/dist/?debug=true) 8 | 9 | ### Features 10 | - SPH fluid simulation on sphere surface [Github Repo](https://robert-leitl.github.io/gpgpu-2d-sph-fluid-simulation) 11 | - Screen-space halo and bloom effect inspired by this [article](https://john-chapman.github.io/2017/11/05/pseudo-lens-flare.html) from John Chapman. 12 | - Audio recording and distorted playback using web audio API and tone.js. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Robert Leitl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/app/shader/sph/offset.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | precision highp usampler2D; 5 | 6 | uniform usampler2D u_indicesTexture; 7 | uniform ivec2 u_texSize; 8 | uniform ivec2 u_particleTexSize; 9 | 10 | out uint outIndices; 11 | 12 | #include ./utils/particle-utils.glsl; 13 | 14 | void main() { 15 | ivec2 texSize = u_texSize; 16 | ivec2 particleTexSize = u_particleTexSize; 17 | vec2 uv = gl_FragCoord.xy / vec2(texSize); 18 | float width = float(texSize.x); 19 | float height = float(texSize.y); 20 | float particleCount = float(particleTexSize * particleTexSize); 21 | 22 | uvec4 self = texture(u_indicesTexture, uv); 23 | uint selfCellId = self.x; 24 | float i = floor(uv.x * width) + floor(uv.y * height) * width; 25 | uint offsetCellId = uint(i); 26 | 27 | for(int n=0; n { 26 | let extType = path.extname(assetInfo.name); 27 | if (!/js|css/i.test(extType)) { 28 | return `assets/[name][extname]`; 29 | } 30 | return '[name]-[hash][extname]'; 31 | } 32 | } 33 | } 34 | } 35 | }); -------------------------------------------------------------------------------- /src/app/_sketch-template.js: -------------------------------------------------------------------------------- 1 | import { vec2 } from "gl-matrix"; 2 | 3 | export class Sketch { 4 | 5 | TARGET_FRAME_DURATION = 16; 6 | #time = 0; // total time 7 | #deltaTime = 0; // duration betweent the previous and the current animation frame 8 | #frames = 0; // total framecount according to the target frame duration 9 | // relative frames according to the target frame duration (1 = 60 fps) 10 | // gets smaller with higher framerates --> use to adapt animation timing 11 | #deltaFrames = 0; 12 | 13 | constructor(canvasElm, onInit = null) { 14 | this.canvas = canvasElm; 15 | this.onInit = onInit; 16 | 17 | this.#init(); 18 | } 19 | 20 | run(time = 0) { 21 | this.#deltaTime = Math.min(32, time - this.#time); 22 | this.#time = time; 23 | this.#deltaFrames = this.#deltaTime / this.TARGET_FRAME_DURATION; 24 | this.#frames += this.#deltaFrames 25 | 26 | this.#animate(this.#deltaTime); 27 | this.#render(); 28 | 29 | requestAnimationFrame((t) => this.run(t)); 30 | } 31 | 32 | resize() { 33 | this.viewportSize = vec2.set( 34 | this.viewportSize, 35 | this.canvas.clientWidth, 36 | this.canvas.clientHeight 37 | ); 38 | } 39 | 40 | #init() { 41 | this.viewportSize = vec2.fromValues( 42 | this.canvas.clientWidth, 43 | this.canvas.clientHeight 44 | ); 45 | 46 | if (this.onInit) this.onInit(this); 47 | } 48 | 49 | #animate(deltaTime) { 50 | 51 | } 52 | 53 | #render() { 54 | 55 | } 56 | } -------------------------------------------------------------------------------- /src/app/shader/sph/pressure.frag.glsl: -------------------------------------------------------------------------------- 1 | 2 | #version 300 es 3 | 4 | precision highp float; 5 | precision highp int; 6 | precision highp usampler2D; 7 | 8 | uniform sampler2D u_positionTexture; 9 | 10 | layout(std140) uniform u_SimulationParams { 11 | float H; 12 | float HSQ; 13 | float MASS; 14 | float REST_DENS; 15 | float GAS_CONST; 16 | float VISC; 17 | float POLY6; 18 | float SPIKY_GRAD; 19 | float VISC_LAP; 20 | float POINTER_RADIUS; 21 | float POINTER_STRENGTH; 22 | int PARTICLE_COUNT; 23 | vec4 DOMAIN_SCALE; 24 | }; 25 | 26 | in vec2 v_uv; 27 | 28 | out vec2 outDensityPressure; 29 | 30 | #include ./utils/particle-utils.glsl; 31 | 32 | float poly6Weight(float r2) { 33 | float temp = max(0., HSQ - r2); 34 | return POLY6 * temp * temp * temp; 35 | } 36 | 37 | void main() { 38 | ivec2 particleTexDimensions = textureSize(u_positionTexture, 0); 39 | vec4 domainScale = DOMAIN_SCALE; 40 | 41 | vec4 p = texture(u_positionTexture, v_uv); 42 | vec4 pi = p * domainScale; 43 | float rho = MASS * poly6Weight(0.); 44 | 45 | // loop over all other particles 46 | for(int i=0; i 2 | 3 | 4 | 5 | 6 | liquid-geo 7 | 8 | 9 | 10 | 11 | github 12 | 13 |
14 | 20 | 25 |
26 | 27 | -------------------------------------------------------------------------------- /src/app/shader/sph/sort.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | precision highp usampler2D; 5 | 6 | uniform float u_twoStage; 7 | uniform float u_passModStage; 8 | uniform float u_twoStagePmS1; 9 | uniform ivec2 u_texSize; 10 | uniform float u_ppass; 11 | uniform usampler2D u_indicesTexture; 12 | 13 | out uvec4 outIndices; 14 | 15 | #include ./utils/particle-utils.glsl; 16 | 17 | void main() { 18 | ivec2 texSize = u_texSize; 19 | vec2 uv = gl_FragCoord.xy / vec2(texSize); 20 | float particleCount = float(texSize * texSize); 21 | float width = float(texSize.x); 22 | float height = float(texSize.y); 23 | 24 | // get self 25 | uvec4 self = texture(u_indicesTexture, uv); 26 | float i = floor(uv.x * width) + floor(uv.y * height) * width; 27 | 28 | // my position within the range to merge 29 | float j = floor(mod(i, u_twoStage)); 30 | float compare; 31 | 32 | if ( (j < u_passModStage) || (j > u_twoStagePmS1) ) 33 | // must copy -> compare with self 34 | compare = 0.0; 35 | else 36 | // must sort 37 | if ( mod((j + u_passModStage) / u_ppass, 2.0) < 1.0) 38 | // we are on the left side -> compare with partner on the right 39 | compare = 1.0; 40 | else 41 | // we are on the right side -> compare with partner on the left 42 | compare = -1.0; 43 | 44 | // get the partner 45 | float adr = i + compare * u_ppass; 46 | uvec4 partner = texture(u_indicesTexture, vec2(floor(mod(adr, width)) / width, floor(adr / width) / height)); 47 | 48 | // on the left it's a < operation; on the right it's a >= operation 49 | outIndices = (float(self.x) * compare < float(partner.x) * compare) ? self : partner; 50 | } 51 | -------------------------------------------------------------------------------- /src/app/shader/highpass.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | uniform sampler2D u_colorTexture; 6 | 7 | out vec4 outColor; 8 | 9 | in vec2 v_uv; 10 | 11 | // credits: https://john-chapman.github.io/2017/11/05/pseudo-lens-flare.html 12 | 13 | vec4 applyThreshold(in vec4 rgb, in float threshold) { 14 | return max(rgb - vec4(threshold), vec4(0.0)); 15 | } 16 | 17 | // Cubic window; map [0, _radius] in [1, 0] as a cubic falloff from _center. 18 | float windowCubic(float x, float center, float radius) { 19 | x = min(abs(x - center) / radius, 1.0); 20 | return 1.0 - x * x * (3.0 - 2.0 * x); 21 | } 22 | 23 | vec4 sampleHalo(in vec2 uv, in float radius, in vec2 aspect, in float threshold) { 24 | vec2 off = .5 - uv; 25 | off *= aspect; 26 | off = normalize(off * .5); 27 | off /= aspect; 28 | off *= radius; 29 | vec2 st = uv + off; 30 | float mask = windowCubic(length((2. * uv - 1.) * aspect), radius * 2., 0.1); 31 | return applyThreshold(texture(u_colorTexture, st), threshold) * mask; 32 | } 33 | 34 | void main() { 35 | vec2 texSize = vec2(textureSize(u_colorTexture, 0)); 36 | vec2 texel = 1. / texSize; 37 | 38 | float haloThreshold = 0.3; 39 | float haloRadius = .7; 40 | vec2 aspect = texSize / min(texSize.y, texSize.x); 41 | float shift = min(texel.x, texel.y) * 30.; 42 | float haloR = sampleHalo(v_uv, haloRadius - shift * 3., aspect, haloThreshold).r; 43 | float haloG = sampleHalo(v_uv, haloRadius - shift * 2., aspect, haloThreshold).g; 44 | float haloB = sampleHalo(v_uv, haloRadius - shift, aspect, haloThreshold).b; 45 | vec4 halo = vec4(haloR, haloG, haloB, 0.); 46 | 47 | outColor = applyThreshold(texture(u_colorTexture, v_uv), 0.4) * 200. + halo * 5.; 48 | } -------------------------------------------------------------------------------- /dist/index-6ca6b2e6.css: -------------------------------------------------------------------------------- 1 | *{box-sizing:border-box}html,body{padding:0;margin:0;width:100%;height:100%;width:100dvw;height:100dvh;font-family:Arial,Helvetica Neue,Helvetica,sans-serif;font-size:18px;overflow:hidden;touch-action:none;background-color:#000}body{position:relative}@media screen and (max-width: 640px){body,html{font-size:14px}}canvas{width:100%;height:100%}.github-link{position:absolute;right:0;bottom:0;color:#aaa;mix-blend-mode:difference;text-transform:uppercase;text-decoration:none;padding:.7em 1em;font-size:.8em}.audio-controls{position:absolute;bottom:0;left:0;width:100%;display:flex;align-items:center;justify-content:center;padding:3em 0;pointer-events:none}.audio-controls button{pointer-events:all;cursor:pointer;background:none;border:2px solid #eee;border-radius:10rem;display:flex;align-items:center;min-height:4.25em;width:16em;justify-content:center}@media screen and (min-width: 641px){.audio-controls button{min-height:6em}}.audio-controls button:hover{background:#333}.audio-controls button:disabled{opacity:.4;pointer-events:none}.audio-controls button label{color:#eee;font-size:1.25rem;pointer-events:none}.audio-controls svg{height:2rem;width:2rem;margin-right:.5rem;pointer-events:none}@media (min-aspect-ratio: 7/5){.audio-controls{flex-direction:column;height:100%;padding:0 2em;align-items:flex-end}.audio-controls button+button{margin-top:1em}}@media (max-aspect-ratio: 5/8){.audio-controls{flex-direction:column}.audio-controls button+button{margin-top:1em;margin-left:0!important}}.audio-controls button+button{margin-left:1em}#playback-button svg{width:1.7rem;height:1.7rem}#record-button.is-recording:before{content:"";display:block;width:1.5rem;height:1.5rem;margin-right:.5rem;background:#f22;border-radius:100%}#record-button.is-recording svg{display:none} 2 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | liquid-geo 7 | 8 | 9 | 10 | 11 | 12 | 13 | github 14 | 15 |
16 | 22 | 27 |
28 | 29 | -------------------------------------------------------------------------------- /src/app/shader/sph/draw.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | uniform mat4 u_worldMatrix; 4 | uniform mat4 u_viewMatrix; 5 | uniform mat4 u_projectionMatrix; 6 | uniform sampler2D u_positionTexture; 7 | uniform sampler2D u_velocityTexture; 8 | uniform vec2 u_resolution; 9 | uniform vec2 u_domainScale; 10 | uniform ivec2 u_cellTexSize; 11 | uniform float u_cellSize; 12 | 13 | out float v_velocity; 14 | flat out vec4 v_color; 15 | 16 | #include ./utils/particle-utils.glsl; 17 | 18 | vec3 hsv2rgb(vec3 c) { 19 | vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); 20 | vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); 21 | return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); 22 | } 23 | 24 | // @see https://iquilezles.org/articles/palettes/ 25 | vec3 palette( in float t, in vec3 a, in vec3 b, in vec3 c, in vec3 d ) { 26 | return a + b*cos( 6.28318*(c*t+d) ); 27 | } 28 | 29 | void main() { 30 | ivec2 poisitionTexDimensions = textureSize(u_positionTexture, 0); 31 | 32 | ivec2 pi_tex = ndx2tex(poisitionTexDimensions, gl_VertexID); 33 | vec4 pi = texelFetch(u_positionTexture, pi_tex, 0); 34 | vec4 vi = texelFetch(u_velocityTexture, pi_tex, 0); 35 | v_velocity = length(vi); 36 | float pointSize = max(u_resolution.x, u_resolution.y) * 0.01; 37 | 38 | gl_PointSize = pointSize * (pi.z * 0.5 + 1.)* (pi.z * 0.5 + 1.); 39 | vec4 worldPosition = u_worldMatrix * vec4(pi.xyz, 1.); 40 | gl_Position = u_projectionMatrix * u_viewMatrix * worldPosition; 41 | 42 | vec3 a = vec3(0.5, 0.5, 0.5); 43 | vec3 b = vec3(0.5, 0.5, 0.5); 44 | vec3 c = vec3(2.0, 1.0, 0.0); 45 | vec3 d = vec3(0.50, 0.20, 0.25); 46 | 47 | float t = length(vi) * 0.1; 48 | 49 | v_color = vec4(palette(t, a, b, c, d) * 1.2, 1.); 50 | //v_color.a = (pi.z * 0.5 + 1.) * 0.5; 51 | } -------------------------------------------------------------------------------- /src/app/app.js: -------------------------------------------------------------------------------- 1 | import { concatAll, take, count, debounceTime, delay, filter, forkJoin, from, fromEvent, map, scan, withLatestFrom, of, switchMap, tap, distinctUntilChanged } from 'rxjs'; 2 | import { Sketch } from './sketch'; 3 | import { Pane } from 'tweakpane'; 4 | import * as modernizr from './utils/modernizr'; 5 | import { AudioRepeater } from './audio-repeater'; 6 | 7 | const queryString = window.location.search; 8 | const urlParams = new URLSearchParams(queryString); 9 | const hasDebugParam = urlParams.get('debug'); 10 | const isDev = import.meta.env.MODE === 'development'; 11 | let sketch; 12 | let audioRepeater; 13 | let pane; 14 | 15 | if (isDev) { 16 | import('https://greggman.github.io/webgl-lint/webgl-lint.js'); 17 | } 18 | 19 | if (hasDebugParam || isDev) { 20 | pane = new Pane({ title: 'Settings', expanded: isDev }); 21 | } 22 | 23 | const resize = () => { 24 | // explicitly set the width and height to compensate for missing dvh and dvw support 25 | document.body.style.width = `${document.documentElement.clientWidth}px`; 26 | document.body.style.height = `${document.documentElement.clientHeight}px`; 27 | 28 | if (sketch) { 29 | sketch.resize(); 30 | } 31 | } 32 | 33 | // add a debounced resize listener 34 | fromEvent(window, 'resize').pipe(debounceTime(100)).subscribe(() => resize()); 35 | 36 | // resize initially on load 37 | fromEvent(window, 'load').pipe(take(1)).subscribe(() => resize()); 38 | 39 | // INIT APP 40 | const canvasElm = document.querySelector('canvas'); 41 | const recordBtnElm = document.querySelector('#record-button'); 42 | const playbackBtnElm = document.querySelector('#playback-button'); 43 | audioRepeater = new AudioRepeater(recordBtnElm, playbackBtnElm, isDev, pane); 44 | sketch = new Sketch(canvasElm, audioRepeater, (instance) => instance.run(), isDev, pane); 45 | resize(); 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | 84 | # Gatsby files 85 | .cache/ 86 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 87 | # https://nextjs.org/blog/next-9-1#public-directory-support 88 | # public 89 | 90 | # vuepress build output 91 | .vuepress/dist 92 | 93 | # Serverless directories 94 | .serverless/ 95 | 96 | # FuseBox cache 97 | .fusebox/ 98 | 99 | # DynamoDB Local files 100 | .dynamodb/ 101 | 102 | # TernJS port file 103 | .tern-port 104 | 105 | src/libs/** 106 | .DS_Store -------------------------------------------------------------------------------- /src/app/shader/sph/integrate.frag.glsl: -------------------------------------------------------------------------------- 1 | 2 | #version 300 es 3 | 4 | precision highp float; 5 | 6 | uniform sampler2D u_forceTexture; 7 | uniform sampler2D u_positionTexture; 8 | uniform sampler2D u_velocityTexture; 9 | uniform sampler2D u_densityPressureTexture; 10 | uniform float u_dt; 11 | uniform float u_time; 12 | uniform vec4 u_domainScale; 13 | 14 | layout(std140) uniform u_PointerParams { 15 | vec3 pointerPos; 16 | vec3 pointerVelocity; 17 | float pointerRadius; 18 | float pointerStrength; 19 | }; 20 | 21 | in vec2 v_uv; 22 | 23 | layout(location = 0) out vec4 outPosition; 24 | layout(location = 1) out vec4 outVelocity; 25 | 26 | #include ./utils/particle-utils.glsl; 27 | 28 | void main() { 29 | ivec2 particleTexDimensions = textureSize(u_positionTexture, 0); 30 | vec4 domainScale = u_domainScale; 31 | 32 | vec4 pi = texture(u_positionTexture, v_uv); 33 | vec4 vi = texture(u_velocityTexture, v_uv); 34 | vec4 fi = texture(u_forceTexture, v_uv); 35 | vec4 ri = texture(u_densityPressureTexture, v_uv); 36 | 37 | // integrate to update the velocity 38 | float dt = (u_dt * 0.001); 39 | float rho = ri.x + 0.000000001; 40 | vec4 ai = fi / rho; 41 | vi += ai * dt; 42 | 43 | // apply the pointer force 44 | vec4 pointerPos = vec4(pointerPos, 0.); 45 | float prFront = length(pointerPos * domainScale - pi * domainScale); 46 | float prBack = length(pointerPos * domainScale + pi * domainScale); 47 | if (prFront < pointerRadius) { 48 | vi += vec4(pointerVelocity, 0.) * pointerStrength * (1. - prFront / pointerRadius); 49 | } else if (prBack < pointerRadius) { 50 | // flip the pointer force on the back and lessen its strength, because 51 | // the back particles are used to generate a second layer in the front of the sphere 52 | vi -= vec4(pointerVelocity, 0.) * pointerStrength * 0.8 * (1. - prBack / pointerRadius); 53 | } 54 | 55 | // update the position 56 | pi += (vi + 0.5 * ai * dt) * dt; 57 | pi = normalize(pi); 58 | 59 | // damp the velocity a bit 60 | vi *= 0.99; 61 | 62 | // add noise 63 | vec3 n = curlNoise(pi.xyz * 2.5 + sin(u_time * 0.0001) * 2.) * 2. - 1.; 64 | vi.xyz += n * 0.008; 65 | 66 | outPosition = pi; 67 | outVelocity = vi; 68 | } -------------------------------------------------------------------------------- /src/app/utils/modernizr.js: -------------------------------------------------------------------------------- 1 | /*! modernizr 3.6.0 (Custom Build) | MIT * 2 | * https://modernizr.com/download/?-touchevents-setclasses !*/ 3 | !function(e,n,t){function o(e){var n=c.className,t=Modernizr._config.classPrefix||"";if(d&&(n=n.baseVal),Modernizr._config.enableJSClass){var o=new RegExp("(^|\\s)"+t+"no-js(\\s|$)");n=n.replace(o,"$1"+t+"js$2")}Modernizr._config.enableClasses&&(n+=" "+t+e.join(" "+t),d?c.className.baseVal=n:c.className=n)}function s(e,n){return typeof e===n}function a(){var e,n,t,o,a,i,r;for(var l in u)if(u.hasOwnProperty(l)){if(e=[],n=u[l],n.name&&(e.push(n.name.toLowerCase()),n.options&&n.options.aliases&&n.options.aliases.length))for(t=0;t this.onRecordButtonClicked()); 31 | this.playbackBtnElm.addEventListener('click', () => this.onPlaybackButtonClicked()); 32 | } 33 | 34 | initAudio() { 35 | this.audioContext = new AudioContext(); 36 | Tone.setContext(this.audioContext); 37 | 38 | this.gain = this.audioContext.createGain(); 39 | 40 | this.analyser = this.audioContext.createAnalyser(); 41 | this.analyser.fftSize = this.FFT_BUFFER_SIZE; 42 | this.analyser.minDecibels = -90; 43 | this.bufferLength = this.analyser.frequencyBinCount; 44 | this.buffer = new Uint8Array(this.bufferLength); 45 | this.smoothedBuffer1 = new Float32Array(this.bufferLength); 46 | this.smoothedBuffer2 = new Float32Array(this.bufferLength); 47 | this.buffer.fill(0); 48 | this.smoothedBuffer1.fill(0); 49 | this.smoothedBuffer2.fill(0); 50 | // calculate the frequency bin bandwidth 51 | this.freqBandwidth = (this.audioContext.sampleRate / 2) / this.bufferLength; 52 | 53 | this.dist = new Tone.Distortion(0.3); 54 | this.pitchShift = new Tone.PitchShift(-2); 55 | this.reverb = new Tone.Reverb(3); 56 | 57 | Tone.connect(this.dist, this.pitchShift); 58 | Tone.connect(this.pitchShift, this.reverb); 59 | this.reverb.toDestination(); 60 | } 61 | 62 | startPlayback() { 63 | this.audio.currentTime = 0; 64 | this.audio.play(); 65 | } 66 | 67 | pausePlayback() { 68 | this.audio.pause(); 69 | } 70 | 71 | onPlaybackButtonClicked() { 72 | this.startPlayback(); 73 | } 74 | 75 | async onRecordButtonClicked() { 76 | if (!this.audioContext) this.initAudio(); 77 | 78 | if (this.isRecording) { 79 | this.stopRecording(); 80 | } else { 81 | await this.startRecording(); 82 | } 83 | } 84 | 85 | async startRecording() { 86 | if (this.audio) { 87 | this.audio.pause(); 88 | } 89 | 90 | this.recordBtnElm.classList.add('is-recording'); 91 | this.playbackBtnElm.setAttribute('disabled', true); 92 | this.recordBtnLabelElm.innerHTML = 'STOP'; 93 | this.isRecording = true; 94 | this.audioChunks = []; 95 | 96 | if (!this.mediaRecorder) { 97 | const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); 98 | this.mediaRecorder = new MediaRecorder(stream); 99 | 100 | this.mediaRecorder.addEventListener("dataavailable", event => { 101 | this.audioChunks.push(event.data); 102 | }); 103 | 104 | this.mediaRecorder.addEventListener("stop", () => { 105 | this.audioBlob = new Blob(this.audioChunks, { type: "audio/mpeg" }); 106 | this.audioUrl = URL.createObjectURL(this.audioBlob); 107 | this.audio = new Audio(this.audioUrl); 108 | 109 | this.source = this.audioContext.createMediaElementSource(this.audio); 110 | Tone.connect(this.source, this.dist); 111 | Tone.connect(this.source, this.analyser); 112 | }); 113 | } 114 | this.mediaRecorder.start(); 115 | 116 | this.isRecordingTimeoutId = setTimeout(() => this.stopRecording(), this.MAX_RECORD_LENGTH * 1000); 117 | } 118 | 119 | stopRecording() { 120 | this.recordBtnElm.classList.remove('is-recording'); 121 | this.playbackBtnElm.removeAttribute('disabled'); 122 | this.isRecording = false; 123 | this.recordBtnLabelElm.innerHTML = 'RECORD'; 124 | clearTimeout(this.isRecordingTimeoutId); 125 | this.mediaRecorder.stop(); 126 | } 127 | 128 | getSpectrum(){ 129 | if (this.audioContext) { 130 | if (this.audio && !this.audio.paused) { 131 | this.analyser.getByteFrequencyData( this.buffer ); 132 | } else { 133 | this.buffer.fill(0); 134 | } 135 | 136 | if (this.buffer) { 137 | for(let i=0; i item.meshName == meshName); 31 | } 32 | 33 | async loadPrimitive(glb, meshName, primitiveDef) { 34 | /** @type {WebGLRenderingContext} */ 35 | const gl = this.gl; 36 | 37 | const indices = GLBBuilder.getAccessorData(glb, primitiveDef.indices); 38 | const vertices = GLBBuilder.getAccessorData(glb, primitiveDef.attributes.POSITION); 39 | const normals = GLBBuilder.getAccessorData(glb, primitiveDef.attributes.NORMAL); 40 | const texcoords = GLBBuilder.getAccessorData(glb, primitiveDef.attributes.TEXCOORD_0); 41 | const tangents = GLBBuilder.getAccessorData(glb, primitiveDef.attributes.TANGENT); 42 | 43 | if (!indices || !vertices || !normals || !texcoords || !tangents) return null; 44 | 45 | // Create buffers: 46 | const indicesBuffer = gl.createBuffer(); 47 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indicesBuffer); 48 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW); 49 | 50 | const verticesBuffer = gl.createBuffer(); 51 | gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer); 52 | gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); 53 | 54 | const normalsBuffer = gl.createBuffer(); 55 | gl.bindBuffer(gl.ARRAY_BUFFER, normalsBuffer); 56 | gl.bufferData(gl.ARRAY_BUFFER, normals, gl.STATIC_DRAW); 57 | 58 | const texcoordsBuffer = gl.createBuffer(); 59 | gl.bindBuffer(gl.ARRAY_BUFFER, texcoordsBuffer); 60 | gl.bufferData(gl.ARRAY_BUFFER, texcoords, gl.STATIC_DRAW); 61 | 62 | const tangentsBuffer = gl.createBuffer(); 63 | gl.bindBuffer(gl.ARRAY_BUFFER, tangentsBuffer); 64 | gl.bufferData(gl.ARRAY_BUFFER, tangents, gl.STATIC_DRAW); 65 | 66 | const buffers = { 67 | 68 | indices: { 69 | data: indices, 70 | webglBuffer: indicesBuffer, 71 | length: indices.length, 72 | dataType: GLBBuilder.getAccessorDataType(gl, glb, primitiveDef.indices), 73 | numberOfComponents: GLBBuilder.getAccessorNumberOfComponents(glb, primitiveDef.indices) 74 | }, 75 | 76 | vertices: { 77 | data: vertices, 78 | webglBuffer: verticesBuffer, 79 | length: vertices.length, 80 | dataType: GLBBuilder.getAccessorDataType(gl, glb, primitiveDef.attributes.POSITION), 81 | numberOfComponents: GLBBuilder.getAccessorNumberOfComponents(glb, primitiveDef.attributes.POSITION), 82 | stride: glb.json.bufferViews[glb.json.accessors[primitiveDef.attributes.POSITION].bufferView].byteStride || 0 83 | }, 84 | 85 | normals: { 86 | data: normals, 87 | webglBuffer: normalsBuffer, 88 | length: normals.length, 89 | dataType: GLBBuilder.getAccessorDataType(gl, glb, primitiveDef.attributes.NORMAL), 90 | numberOfComponents: GLBBuilder.getAccessorNumberOfComponents(glb, primitiveDef.attributes.NORMAL), 91 | stride: glb.json.bufferViews[glb.json.accessors[primitiveDef.attributes.NORMAL].bufferView].byteStride || 0 92 | }, 93 | 94 | texcoords: { 95 | data: texcoords, 96 | webglBuffer: texcoordsBuffer, 97 | length: texcoords.length, 98 | dataType: GLBBuilder.getAccessorDataType(gl, glb, primitiveDef.attributes.TEXCOORD_0), 99 | numberOfComponents: GLBBuilder.getAccessorNumberOfComponents(glb, primitiveDef.attributes.TEXCOORD_0), 100 | stride: glb.json.bufferViews[glb.json.accessors[primitiveDef.attributes.TEXCOORD_0].bufferView].byteStride || 0 101 | }, 102 | 103 | tangents: { 104 | data: tangents, 105 | webglBuffer: tangentsBuffer, 106 | length: tangents.length, 107 | dataType: GLBBuilder.getAccessorDataType(gl, glb, primitiveDef.attributes.TANGENT), 108 | numberOfComponents: GLBBuilder.getAccessorNumberOfComponents(glb, primitiveDef.attributes.TANGENT), 109 | stride: glb.json.bufferViews[glb.json.accessors[primitiveDef.attributes.TANGENT].bufferView].byteStride || 0 110 | } 111 | }; 112 | 113 | return { 114 | meshName, 115 | buffers: buffers 116 | } 117 | } 118 | 119 | static getAccessorData(glb, accessorIndex) { 120 | 121 | const accessorDef = glb.json.accessors[accessorIndex]; 122 | 123 | if (accessorDef) { 124 | 125 | const binChunk = glb.binChunks[0]; 126 | 127 | const bufferViewDef = glb.json.bufferViews[accessorDef.bufferView]; 128 | const componentType = accessorDef.componentType; 129 | const count = accessorDef.count; 130 | 131 | const byteOffset = binChunk.byteOffset + (accessorDef.byteOffset || 0) + bufferViewDef.byteOffset; 132 | 133 | let numberOfComponents = GLBBuilder.getAccessorNumberOfComponents(glb, accessorIndex); 134 | 135 | switch (componentType) { 136 | case 5120: { return new Int8Array(binChunk.arrayBuffer, byteOffset, count * numberOfComponents); } 137 | case 5121: { return new Uint8Array(binChunk.arrayBuffer, byteOffset, count * numberOfComponents); } 138 | case 5122: { return new Int16Array(binChunk.arrayBuffer, byteOffset, count * numberOfComponents); } 139 | case 5123: { return new Uint16Array(binChunk.arrayBuffer, byteOffset, count * numberOfComponents); } 140 | case 5125: { return new Uint32Array(binChunk.arrayBuffer, byteOffset, count * numberOfComponents); } 141 | case 5126: { return new Float32Array(binChunk.arrayBuffer, byteOffset, count * numberOfComponents); } 142 | } 143 | } 144 | 145 | return null; 146 | } 147 | 148 | static getAccessorNumberOfComponents(glb, accessorIndex) { 149 | 150 | const accessorDef = glb.json.accessors[accessorIndex]; 151 | 152 | switch (accessorDef.type) { 153 | case "SCALAR": return 1; 154 | case "VEC2": return 2; 155 | case "VEC3": return 3; 156 | case "VEC4": return 4; 157 | case "MAT2": return 4; 158 | case "MAT3": return 9; 159 | case "MAT4": return 16; 160 | } 161 | 162 | return null; 163 | } 164 | 165 | static getAccessorDataType(gl, glb, accessorIndex) { 166 | 167 | const accessorDef = glb.json.accessors[accessorIndex]; 168 | const componentType = accessorDef.componentType; 169 | 170 | switch (componentType) { 171 | case 5120: { return gl.BYTE; } 172 | case 5121: { return gl.UNSIGNED_BYTE; } 173 | case 5122: { return gl.SHORT; } 174 | case 5123: { return gl.UNSIGNED_SHORT; } 175 | case 5125: { return gl.UNSIGNED_INT; } 176 | case 5126: { return gl.FLOAT; } 177 | } 178 | } 179 | } -------------------------------------------------------------------------------- /src/app/sketch.js: -------------------------------------------------------------------------------- 1 | import { mat4, vec2, vec3, vec4 } from "gl-matrix"; 2 | import { filter, fromEvent, merge, throwIfEmpty } from "rxjs"; 3 | import * as twgl from "twgl.js"; 4 | import { GLBBuilder } from "./utils/glb-builder"; 5 | 6 | import drawVert from './shader/sph/draw.vert.glsl'; 7 | import drawFrag from './shader/sph/draw.frag.glsl'; 8 | import integrateVert from './shader/sph/integrate.vert.glsl'; 9 | import integrateFrag from './shader/sph/integrate.frag.glsl'; 10 | import pressureVert from './shader/sph/pressure.vert.glsl'; 11 | import pressureFrag from './shader/sph/pressure.frag.glsl'; 12 | import forceVert from './shader/sph/force.vert.glsl'; 13 | import forceFrag from './shader/sph/force.frag.glsl'; 14 | import testVert from './shader/test.vert.glsl'; 15 | import testFrag from './shader/test.frag.glsl'; 16 | import beadVert from './shader/bead.vert.glsl'; 17 | import beadFrag from './shader/bead.frag.glsl'; 18 | import lightDepthFrag from './shader/light-depth.frag.glsl'; 19 | import highpassVert from './shader/highpass.vert.glsl'; 20 | import highpassFrag from './shader/highpass.frag.glsl'; 21 | import blurVert from './shader/blur.vert.glsl'; 22 | import blurFrag from './shader/blur.frag.glsl'; 23 | import compositeVert from './shader/composite.vert.glsl'; 24 | import compositeFrag from './shader/composite.frag.glsl'; 25 | import {isIOS} from './is-ios.js'; 26 | 27 | export class Sketch { 28 | 29 | TARGET_FRAME_DURATION = 16; 30 | #time = 0; // total time 31 | #deltaTime = 0; // duration betweent the previous and the current animation frame 32 | #frames = 0; // total framecount according to the target frame duration 33 | // relative frames according to the target frame duration (1 = 60 fps) 34 | // gets smaller with higher framerates --> use to adapt animation timing 35 | #deltaFrames = 0; 36 | 37 | // particle constants 38 | NUM_PARTICLES = 500; 39 | 40 | // the scale factor for the bloom and lensflare highpass texture 41 | SS_FX_SCALE = 0.2; 42 | 43 | simulationParams = { 44 | H: 1, // kernel radius 45 | MASS: 1, // particle mass 46 | REST_DENS: 1.5, // rest density 47 | GAS_CONST: 400, // gas constant 48 | VISC: 18.5, // viscosity constant 49 | 50 | // these are calculated from the above constants 51 | POLY6: 0, 52 | HSQ: 0, 53 | SPIKY_GRAD: 0, 54 | VISC_LAP: 0, 55 | 56 | PARTICLE_COUNT: 0, 57 | DOMAIN_SCALE: vec4.fromValues(1, 1, 1, 1), 58 | 59 | STEPS: 0 60 | }; 61 | 62 | pointerParams = { 63 | RADIUS: .5, 64 | STRENGTH: 20, 65 | } 66 | 67 | camera = { 68 | matrix: mat4.create(), 69 | near: 4, 70 | far: 6, 71 | fov: Math.PI / 3, 72 | aspect: 1, 73 | position: vec3.fromValues(0, 0, 6), 74 | up: vec3.fromValues(0, 1, 0), 75 | matrices: { 76 | view: mat4.create(), 77 | projection: mat4.create(), 78 | inversProjection: mat4.create(), 79 | inversViewProjection: mat4.create() 80 | } 81 | }; 82 | 83 | light = { 84 | matrix: mat4.create(), 85 | position: vec3.scale(vec3.create(), vec3.normalize(vec3.create(), vec3.fromValues(1, 1, 1)), 6), 86 | up: vec3.fromValues(0, 1, 0), 87 | size: 2.4, 88 | near: 4, 89 | far: 7, 90 | textureSize: 1024, 91 | matrices: { 92 | view: mat4.create(), 93 | projection: mat4.create(), 94 | viewProjection: mat4.create() 95 | } 96 | } 97 | 98 | constructor(canvasElm, audioRepeater, onInit = null, isDev = false, pane = null) { 99 | this.canvas = canvasElm; 100 | this.onInit = onInit; 101 | this.isDev = isDev; 102 | this.pane = pane; 103 | this.audioRepeater = audioRepeater; 104 | 105 | this.#init().then(() => { 106 | if (this.onInit) this.onInit(this) 107 | }); 108 | } 109 | 110 | run(time = 0) { 111 | this.#deltaTime = Math.min(16, time - this.#time); 112 | this.#time = time; 113 | this.#deltaFrames = this.#deltaTime / this.TARGET_FRAME_DURATION; 114 | this.#frames += this.#deltaFrames; 115 | 116 | this.#animate(this.#deltaTime); 117 | this.#render(); 118 | 119 | requestAnimationFrame((t) => this.run(t)); 120 | } 121 | 122 | resize() { 123 | /** @type {WebGLRenderingContext} */ 124 | const gl = this.gl; 125 | 126 | this.viewportSize = vec2.set( 127 | this.viewportSize, 128 | this.canvas.clientWidth, 129 | this.canvas.clientHeight 130 | ); 131 | 132 | const needsResize = twgl.resizeCanvasToDisplaySize(this.canvas); 133 | 134 | const maxViewportSide = Math.max(this.viewportSize[0], this.viewportSize[1]); 135 | this.SS_FX_SCALE = Math.min(1, 256 / maxViewportSide); 136 | 137 | if (needsResize) { 138 | gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); 139 | 140 | if (this.highpassFBO) { 141 | twgl.resizeFramebufferInfo(gl, this.highpassFBO, [{attachmentPoint: gl.COLOR_ATTACHMENT0}], 142 | this.viewportSize[0] * this.SS_FX_SCALE, this.viewportSize[1] * this.SS_FX_SCALE); 143 | } 144 | 145 | if (this.blurFBO) { 146 | twgl.resizeFramebufferInfo(gl, this.blurFBO, [{attachmentPoint: gl.COLOR_ATTACHMENT0}], 147 | this.viewportSize[0] * this.SS_FX_SCALE, this.viewportSize[1] * this.SS_FX_SCALE); 148 | } 149 | 150 | if (this.drawFBO) { 151 | twgl.resizeFramebufferInfo(gl, this.drawFBO, this.drawFBOAttachements, this.viewportSize[0], this.viewportSize[1]); 152 | } 153 | } 154 | 155 | this.#updateProjectionMatrix(gl); 156 | } 157 | 158 | async #init() { 159 | this.gl = this.canvas.getContext('webgl2', { antialias: false, alpha: false }); 160 | 161 | this.touchevents = Modernizr.touchevents; 162 | 163 | /** @type {WebGLRenderingContext} */ 164 | const gl = this.gl; 165 | 166 | twgl.addExtensionsToContext(gl); 167 | 168 | this.viewportSize = vec2.fromValues( 169 | this.canvas.clientWidth, 170 | this.canvas.clientHeight 171 | ); 172 | 173 | this.#initTextures(); 174 | this.#initLight(); 175 | await this.#initEnvMap(); 176 | await this.#initNormalMap(); 177 | 178 | // Setup Programs 179 | this.drawPrg = twgl.createProgramInfo(gl, [drawVert, drawFrag]); 180 | this.integratePrg = twgl.createProgramInfo(gl, [integrateVert, integrateFrag]); 181 | this.pressurePrg = twgl.createProgramInfo(gl, [pressureVert, pressureFrag]); 182 | this.forcePrg = twgl.createProgramInfo(gl, [forceVert, forceFrag]); 183 | this.beadPrg = twgl.createProgramInfo(gl, [beadVert, beadFrag]); 184 | this.testPrg = twgl.createProgramInfo(gl, [testVert, testFrag]); 185 | this.lightDepthPrg = twgl.createProgramInfo(gl, [beadVert, lightDepthFrag]); 186 | this.highpassPrg = twgl.createProgramInfo(gl, [highpassVert, highpassFrag]); 187 | this.blurPrg = twgl.createProgramInfo(gl, [blurVert, blurFrag]); 188 | this.compositePrg = twgl.createProgramInfo(gl, [compositeVert, compositeFrag]); 189 | 190 | // Setup uinform blocks 191 | this.simulationParamsUBO = twgl.createUniformBlockInfo(gl, this.pressurePrg, 'u_SimulationParams'); 192 | this.pointerParamsUBO = twgl.createUniformBlockInfo(gl, this.integratePrg, 'u_PointerParams'); 193 | this.simulationParamsNeedUpdate = true; 194 | 195 | // Setup Meshes 196 | this.quadBufferInfo = twgl.createBufferInfoFromArrays(gl, { a_position: { numComponents: 2, data: [-1, -1, 3, -1, -1, 3] }}); 197 | this.quadVAO = twgl.createVAOAndSetAttributes(gl, this.pressurePrg.attribSetters, this.quadBufferInfo.attribs, this.quadBufferInfo.indices); 198 | 199 | // load the bead model 200 | this.glbBuilder = new GLBBuilder(gl); 201 | await this.glbBuilder.load(new URL('../assets/bead.glb', import.meta.url)); 202 | this.beadPrimitive = this.glbBuilder.getPrimitiveDataByMeshName('bead'); 203 | this.beadBuffers = this.beadPrimitive.buffers; 204 | this.beadBufferInfo = twgl.createBufferInfoFromArrays(gl, { 205 | a_position: {...this.beadBuffers.vertices, numComponents: this.beadBuffers.vertices.numberOfComponents}, 206 | a_normal: {...this.beadBuffers.normals, numComponents: this.beadBuffers.normals.numberOfComponents}, 207 | a_texcoord: {...this.beadBuffers.texcoords, numComponents: this.beadBuffers.texcoords.numberOfComponents}, 208 | a_tangent: {...this.beadBuffers.tangents, numComponents: this.beadBuffers.tangents.numberOfComponents}, 209 | indices: {...this.beadBuffers.indices, numComponents: this.beadBuffers.indices.numberOfComponents} 210 | }); 211 | this.beadVAO = twgl.createVAOAndSetAttributes(gl, this.beadPrg.attribSetters, this.beadBufferInfo.attribs, this.beadBufferInfo.indices); 212 | 213 | // Setup Framebuffers 214 | this.pressureFBO = twgl.createFramebufferInfo(gl, [{attachment: this.textures.densityPressure}], this.textureSize, this.textureSize); 215 | this.forceFBO = twgl.createFramebufferInfo(gl, [{attachment: this.textures.force}], this.textureSize, this.textureSize); 216 | this.inFBO = twgl.createFramebufferInfo(gl, [{attachment: this.textures.position1},{attachment: this.textures.velocity1}], this.textureSize, this.textureSize); 217 | this.outFBO = twgl.createFramebufferInfo(gl, [{attachment: this.textures.position2},{attachment: this.textures.velocity2}], this.textureSize, this.textureSize); 218 | this.lightDepthFBO = twgl.createFramebufferInfo(gl, [{ 219 | attachmentPoint: gl.DEPTH_ATTACHMENT, 220 | attachment: this.lightDepthTexture 221 | }], this.light.textureSize, this.light.textureSize); 222 | this.drawFBOAttachements = [ 223 | {format: gl.RGBA, internalFormat: gl.RGBA32F, min: isIOS ? gl.NEAREST : gl.LINEAR, mag: isIOS ? gl.NEAREST : gl.LINEAR}, 224 | {attachmentPoint: gl.DEPTH_ATTACHMENT, format: gl.DEPTH_COMPONENT, internalFormat: gl.DEPTH_COMPONENT32F} 225 | ]; 226 | this.drawFBO = twgl.createFramebufferInfo(gl, this.drawFBOAttachements, this.viewportSize[0], this.viewportSize[1]); 227 | this.colorTexture = this.drawFBO.attachments[0]; 228 | this.highpassFBO = twgl.createFramebufferInfo( 229 | gl, 230 | [{attachmentPoint: gl.COLOR_ATTACHMENT0}], 231 | this.viewportSize[0] * this.SS_FX_SCALE, 232 | this.viewportSize[1] * this.SS_FX_SCALE 233 | ); 234 | this.highpassTexture = this.highpassFBO.attachments[0]; 235 | this.blurFBO = twgl.createFramebufferInfo( 236 | gl, 237 | [{attachmentPoint: gl.COLOR_ATTACHMENT0}], 238 | this.viewportSize[0] * this.SS_FX_SCALE, 239 | this.viewportSize[1] * this.SS_FX_SCALE 240 | ); 241 | this.blurTexture = this.blurFBO.attachments[0]; 242 | 243 | this.worldMatrix = mat4.create(); 244 | 245 | this.#initEvents(); 246 | this.#updateSimulationParams(); 247 | this.#initTweakpane(); 248 | this.#updateCameraMatrix(); 249 | this.#updateProjectionMatrix(gl); 250 | 251 | this.resize(); 252 | } 253 | 254 | #initEvents() { 255 | this.isPointerDown = false; 256 | this.pointerLeft = true; 257 | this.pointer = vec2.create(); 258 | this.pointerLerp = vec2.create(); 259 | this.pointerLerpPrev = vec2.create(); 260 | this.pointerLerpDelta = vec2.create(); 261 | this.arcPointer = vec3.create(); 262 | this.arcPointerPrev = vec3.create(); 263 | this.arcPointerDelta = vec3.create(); 264 | 265 | fromEvent(this.canvas, 'pointerdown').subscribe((e) => { 266 | this.isPointerDown = true; 267 | this.pointerLeft = false; 268 | this.pointer = vec2.fromValues(e.clientX, e.clientY); 269 | vec2.copy(this.pointerLerp, this.pointer); 270 | vec2.copy(this.pointerLerpPrev, this.pointerLerp); 271 | }); 272 | merge( 273 | fromEvent(this.canvas, 'pointerup'), 274 | fromEvent(this.canvas, 'pointerleave') 275 | ).subscribe(() => { 276 | this.isPointerDown = false; 277 | this.leftSphere = true; 278 | this.pointerLeft = true; 279 | }); 280 | 281 | fromEvent(this.canvas, 'pointermove').subscribe((e) => { 282 | this.pointer = vec2.fromValues(e.clientX, e.clientY); 283 | if (this.pointerLeft) { 284 | this.pointerLerp = vec2.clone(this.pointer); 285 | this.pointerLerpPrev = vec2.clone(this.pointer); 286 | } 287 | this.pointerLeft = false; 288 | }); 289 | 290 | fromEvent(window.document, 'keyup').subscribe(() => this.debugKey = true); 291 | } 292 | 293 | #updateSimulationParams() { 294 | const sim = this.simulationParams 295 | sim.HSQ = sim.H * sim.H; 296 | sim.POLY6 = 315.0 / (64. * Math.PI * Math.pow(sim.H, 9.)); 297 | sim.SPIKY_GRAD = -45.0 / (Math.PI * Math.pow(sim.H, 6.)); 298 | sim.VISC_LAP = 45.0 / (Math.PI * Math.pow(sim.H, 5.)); 299 | 300 | this.simulationParamsNeedUpdate = true; 301 | } 302 | 303 | #initTextures() { 304 | /** @type {WebGLRenderingContext} */ 305 | const gl = this.gl; 306 | 307 | // get a power of two texture size 308 | this.textureSize = 2**Math.ceil(Math.log2(Math.sqrt(this.NUM_PARTICLES))); 309 | 310 | // update the particle size to fill the texture space 311 | this.NUM_PARTICLES = this.textureSize * this.textureSize; 312 | this.simulationParams.PARTICLE_COUNT = this.NUM_PARTICLES; 313 | this.simulationParamsNeedUpdate = true; 314 | 315 | console.log('number of particles:', this.NUM_PARTICLES); 316 | 317 | this.spectrumTextureSize = Math.sqrt(this.audioRepeater.bufferLength); 318 | 319 | const initVelocities = new Float32Array(this.NUM_PARTICLES * 4); 320 | const initForces = new Float32Array(this.NUM_PARTICLES * 4); 321 | const initPositions = new Float32Array(this.NUM_PARTICLES * 4); 322 | 323 | for(let i=0; i { 394 | this.envMapTexture = twgl.createTexture(gl, { 395 | src: new URL('../assets/env-map-02.jpg', import.meta.url).toString(), 396 | }, () => resolve()); 397 | }); 398 | } 399 | 400 | #initNormalMap() { 401 | /** @type {WebGLRenderingContext} */ 402 | const gl = this.gl; 403 | 404 | return new Promise((resolve) => { 405 | this.normalMapTexture = twgl.createTexture(gl, { 406 | src: new URL('../assets/normal.png', import.meta.url).toString(), 407 | }, () => resolve()); 408 | }); 409 | } 410 | 411 | #initTweakpane() { 412 | if (!this.pane) return; 413 | 414 | const sim = this.pane.addFolder({ title: 'Simulation' }); 415 | sim.addInput(this.simulationParams, 'MASS', { min: 0.01, max: 5, }); 416 | sim.addInput(this.simulationParams, 'REST_DENS', { min: 0.1, max: 5, }); 417 | sim.addInput(this.simulationParams, 'GAS_CONST', { min: 10, max: 500, }); 418 | sim.addInput(this.simulationParams, 'VISC', { min: 1, max: 20, }); 419 | sim.addInput(this.simulationParams, 'STEPS', { min: 0, max: 6, step: 1 }); 420 | 421 | const pointer = this.pane.addFolder({ title: 'Pointer' }); 422 | pointer.addInput(this.pointerParams, 'RADIUS', { min: 0.1, max: 5, }); 423 | pointer.addInput(this.pointerParams, 'STRENGTH', { min: 1, max: 35, }); 424 | 425 | sim.on('change', () => this.#updateSimulationParams()); 426 | pointer.on('change', () => this.pointerParamsNeedUpdate = true); 427 | } 428 | 429 | #initLight() { 430 | mat4.targetTo(this.light.matrix, this.light.position, [0, 0, 0], this.light.up); 431 | mat4.invert(this.light.matrices.view, this.light.matrix); 432 | mat4.ortho( 433 | this.light.matrices.projection, 434 | -this.light.size / 2, 435 | this.light.size / 2, 436 | -this.light.size / 2, 437 | this.light.size / 2, 438 | this.light.near, 439 | this.light.far 440 | ); 441 | mat4.multiply(this.light.matrices.viewProjection, this.light.matrices.projection, this.light.matrices.view); 442 | } 443 | 444 | #updatePointer() { 445 | this.pointerLerp[0] += (this.pointer[0] - this.pointerLerp[0]) / 5; 446 | this.pointerLerp[1] += (this.pointer[1] - this.pointerLerp[1]) / 5; 447 | 448 | let newArcPointer = null; 449 | if (!this.touchevents || this.isPointerDown) 450 | newArcPointer = this.#screenToSpherePos(this.pointerLerp); 451 | 452 | if (newArcPointer !== null) { 453 | this.arcPointer = newArcPointer; 454 | if (this.leftSphere) { 455 | vec3.copy(this.arcPointerPrev, this.arcPointer); 456 | this.leftSphere = false; 457 | } 458 | } else { 459 | this.leftSphere = true; 460 | } 461 | 462 | 463 | this.arcPointerDelta = vec3.subtract(this.arcPointerDelta, this.arcPointer, this.arcPointerPrev); 464 | vec3.copy(this.arcPointerPrev, this.arcPointer); 465 | 466 | vec2.subtract(this.pointerLerpDelta, this.pointerLerp, this.pointerLerpPrev); 467 | vec2.copy(this.pointerLerpPrev, this.pointerLerp); 468 | } 469 | 470 | #simulate(deltaTime) { 471 | /** @type {WebGLRenderingContext} */ 472 | const gl = this.gl; 473 | 474 | if (this.simulationParamsNeedUpdate) { 475 | twgl.setBlockUniforms( 476 | this.simulationParamsUBO, 477 | { 478 | ...this.simulationParams, 479 | } 480 | ); 481 | twgl.setUniformBlock(gl, this.pressurePrg, this.simulationParamsUBO); 482 | this.simulationParamsNeedUpdate = false; 483 | } else { 484 | twgl.bindUniformBlock(gl, this.pressurePrg, this.simulationParamsUBO); 485 | } 486 | 487 | 488 | // calculate density and pressure for every particle 489 | gl.useProgram(this.pressurePrg.program); 490 | twgl.bindFramebufferInfo(gl, this.pressureFBO); 491 | gl.bindVertexArray(this.quadVAO); 492 | twgl.setUniforms(this.pressurePrg, { 493 | u_positionTexture: this.inFBO.attachments[0] 494 | }); 495 | twgl.drawBufferInfo(gl, this.quadBufferInfo); 496 | 497 | 498 | // calculate pressure-, viscosity- and boundary forces for every particle 499 | gl.useProgram(this.forcePrg.program); 500 | twgl.bindFramebufferInfo(gl, this.forceFBO); 501 | twgl.setUniforms(this.forcePrg, { 502 | u_densityPressureTexture: this.pressureFBO.attachments[0], 503 | u_positionTexture: this.inFBO.attachments[0], 504 | u_velocityTexture: this.inFBO.attachments[1] 505 | }); 506 | twgl.drawBufferInfo(gl, this.quadBufferInfo); 507 | 508 | // perform the integration to update the particles position and velocity 509 | gl.useProgram(this.integratePrg.program); 510 | twgl.bindFramebufferInfo(gl, this.outFBO); 511 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 512 | twgl.setUniforms(this.integratePrg, { 513 | u_positionTexture: this.inFBO.attachments[0], 514 | u_velocityTexture: this.inFBO.attachments[1], 515 | u_forceTexture: this.forceFBO.attachments[0], 516 | u_densityPressureTexture: this.pressureFBO.attachments[0], 517 | u_dt: deltaTime, 518 | u_time: this.#time, 519 | u_domainScale: this.simulationParams.DOMAIN_SCALE 520 | }); 521 | twgl.setBlockUniforms( 522 | this.pointerParamsUBO, 523 | { 524 | pointerRadius: this.pointerParams.RADIUS, 525 | pointerStrength: this.pointerParams.STRENGTH, 526 | pointerPos: this.arcPointer, 527 | pointerVelocity: this.arcPointerDelta 528 | } 529 | ); 530 | twgl.setUniformBlock(gl, this.integratePrg, this.pointerParamsUBO); 531 | twgl.drawBufferInfo(gl, this.quadBufferInfo); 532 | 533 | // update the current result textures 534 | this.currentPositionTexture = this.outFBO.attachments[0]; 535 | this.currentVelocityTexture = this.outFBO.attachments[1]; 536 | 537 | // swap the integrate FBOs 538 | const tmp = this.inFBO; 539 | this.inFBO = this.outFBO; 540 | this.outFBO = tmp; 541 | } 542 | 543 | #animate(deltaTime) { 544 | /** @type {WebGLRenderingContext} */ 545 | const gl = this.gl; 546 | 547 | this.#updatePointer(); 548 | 549 | // use a fixed deltaTime of 10 ms adapted to 550 | // device frame rate 551 | deltaTime = 16 * this.#deltaFrames; 552 | 553 | // simulate at least once 554 | this.#simulate(deltaTime); 555 | 556 | // clear the pointer force so that it wont add up during 557 | // subsequent simulation steps 558 | vec2.set(this.pointerLerpDelta, 0, 0); 559 | 560 | // additional simulation steps 561 | for(let i=0; i 1) { 692 | this.camera.fov = 2 * Math.atan( height / distance ); 693 | } else { 694 | this.camera.fov = 2 * Math.atan( (height / this.camera.aspect) / distance ); 695 | } 696 | 697 | mat4.perspective(this.camera.matrices.projection, this.camera.fov, this.camera.aspect, this.camera.near, this.camera.far); 698 | mat4.invert(this.camera.matrices.inversProjection, this.camera.matrices.projection); 699 | mat4.multiply(this.camera.matrices.inversViewProjection, this.camera.matrix, this.camera.matrices.inversProjection) 700 | } 701 | 702 | #screenToSpherePos(screenPos) { 703 | // map to -1 to 1 704 | const x = (screenPos[0] / this.viewportSize[0]) * 2. - 1; 705 | const y = (1 - (screenPos[1] / this.viewportSize[1])) * 2. - 1; 706 | 707 | // l(t) = p + t * u 708 | const p = this.#screenToWorldPosition(x, y, 0); 709 | const u = vec3.subtract(vec3.create(), p, this.camera.position); 710 | vec3.normalize(u, u); 711 | 712 | // sphere at origin intersection 713 | const radius = 1.05; 714 | const c = vec3.dot(p, p) - radius * radius; 715 | const b = vec3.dot(u, p) * 2; 716 | const a = 1; 717 | const d = b * b - 4 * a * c; 718 | 719 | if (d < 0) { 720 | // No solution 721 | return null; 722 | } else { 723 | const sd = Math.sqrt(d); 724 | const t1 = (-b + sd) / (2 * a); 725 | const t2 = (-b - sd) / (2 * a); 726 | const t = Math.min(t1, t2); 727 | 728 | vec3.scale(u, u, t); 729 | const i = vec3.add(vec3.create(), p, u); 730 | 731 | return i; 732 | } 733 | } 734 | 735 | #screenToWorldPosition(x, y, z) { 736 | const ndcPos = vec3.fromValues(x, y, z); 737 | const worldPos = vec4.transformMat4(vec4.create(), vec4.fromValues(ndcPos[0], ndcPos[1], ndcPos[2], 1), this.camera.matrices.inversViewProjection); 738 | if (worldPos[3] !== 0){ 739 | vec4.scale(worldPos, worldPos, 1 / worldPos[3]); 740 | } 741 | 742 | return worldPos; 743 | } 744 | } --------------------------------------------------------------------------------