├── .gitattributes
├── .gitignore
├── README.md
├── docs
├── css
│ └── page.css
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon-96x96.png
├── images
│ ├── gear.svg
│ ├── noise-dark.png
│ ├── noise-light.png
│ └── resize.svg
├── index.html
├── readme
│ ├── css
│ │ └── page.css
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon-96x96.png
│ ├── images
│ │ ├── noise-dark.png
│ │ └── noise-light.png
│ └── index.html
└── script
│ ├── main.min.js
│ ├── main.min.js.map
│ ├── page.js
│ └── page.min.js
├── package-lock.json
├── package.json
└── src
├── config
├── tsconfig.json
└── webpack.config.js
├── generate-page.ts
└── ts
├── brush.ts
├── fluid.ts
├── gl-utils
├── fbo.ts
├── gl-resource.ts
├── shader.ts
├── utils.ts
└── vbo.ts
├── main.ts
├── obstacle-map.ts
├── parameters.ts
├── requirements.ts
└── shaders
├── brush-shaders.ts
├── build-shaders.ts
├── fluid-shaders.ts
└── obstacle-map-shaders.ts
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *generated.*
3 | debug.log
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # navier-stokes-webgl
2 | Stable fluid simulation on GPU using WebGL.
3 |
4 | Live version [here](https://piellardj.github.io/navier-stokes-webgl).
5 |
6 | This is an implementation of the Stable Fluid described by Jos Stam.
7 |
8 | [](https://www.paypal.com/donate/?hosted_button_id=AF7H7GEJTL95E)
9 |
10 | # Simulation
11 |
12 | The simulation is implemented on GPU with the method provided in GPU Gems.
13 | The diffusion term was dropped since it didn't have much visual influence.
14 |
15 | # Data storage
16 |
17 | This simulation can run in two modes for storing the velocities:
18 | * velocity stored in float textures: each component (x, y) is stored as a 32bit float.
19 | To do so the following extensions must be available: `OES_texture_float`, `WEBGL_color_buffer_float`, `OES_texture_float_linear`.
20 | * velocity stored in normal textures with four 8bit channels.
21 | In this mode each component is stored as a 16bit fixed point value, encoded in two 8bit texture channels.
22 | This mode provides less precision for the computing, and you can see artifacts if you push the display color intensity to the maximum.
23 |
--------------------------------------------------------------------------------
/docs/css/page.css:
--------------------------------------------------------------------------------
1 | body{text-align:center}#error-messages{margin:32px 8px;color:red;font-weight:bold}.demo{display:flex;flex-flow:row wrap;align-items:flex-start;justify-content:center;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}
2 | .logo{display:block;position:relative;width:64px;height:64px;margin:8px auto 16px;border-radius:50%;user-select:none;box-sizing:border-box}.logo,.logo:hover,.logo:focus,.logo:active{border-width:1px;border-style:solid;border-color:#009688;border-color:var(--var-color-control-accent, #009688)}.logo::before,.logo svg.logo-icon,.logo::after{position:absolute;top:-1px;left:-1px;width:64px;height:64px;border-radius:50%;pointer-events:none}.logo svg.logo-icon{stroke:#009688;stroke:var(--var-color-control-accent, #009688);fill:#009688;fill:var(--var-color-control-accent, #009688)}.logo::before{content:"";transform:scale(0);-webkit-transform:scale(0);-ms-transform:scale(0);transition:.1s ease;-webkit-transition:.1s ease}.logo.logo-animate-fill .logo::before{content:"";transform:scale(0);-webkit-transform:scale(0);-ms-transform:scale(0);transition:.1s ease;-webkit-transition:.1s ease}.logo:hover::before{transform:scale(1);-webkit-transform:scale(1);-ms-transform:scale(1)}.logo.logo-animate-fill{background:#eeeeee;background:var(--var-color-block-background, #eeeeee)}.logo.logo-animate-fill::before{background:#009688;background:var(--var-color-control-accent, #009688)}.logo.logo-animate-fill:hover svg.logo-icon{fill:#fff;stroke:#fff}.logo.logo-animate-empty{background:#009688;background:var(--var-color-control-accent, #009688)}.logo.logo-animate-empty::before{top:0;left:0;width:62px;height:62px;background:#eeeeee;background:var(--var-color-block-background, #eeeeee)}
3 | .intro{margin:auto;padding:16px;border-radius:8px;border:1px solid #c9c9c9;border:var(--var-color-block-border, 1px solid #c9c9c9);background:#eeeeee;background:var(--var-color-block-background, #eeeeee)}.intro h1{margin-top:0;text-align:center}@media only screen and (min-width: 560px){.intro{max-width:512px;border-width:1px}}.description{justify-content:center;line-height:125%;text-align:justify;text-indent:1em}.project-links{display:flex;flex-flow:row;justify-content:space-between;text-indent:0}
4 | a{color:#009688;color:var(--var-color-control-accent, #009688);font-weight:bold;text-decoration:none;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;border-width:0 0 2px;border-style:solid;border-color:rgba(0,0,0,0)}a:focus,a:hover{border-color:#009688;border-color:var(--var-color-control-accent, #009688)}
5 | .canvas-button{width:32px;height:32px;cursor:pointer}#canvas-container{position:relative;margin-bottom:16px;background:#000;overflow:hidden}@media only screen and (min-width: 540px){#canvas-container{margin:16px}}#canvas-container>canvas{width:100%;height:100%;z-index:10}#canvas-container>.loader{display:none}#indicators{display:flex;position:absolute;top:1px;left:1px;flex-direction:column;align-items:flex-start;color:#fff;font-family:"Lucida Console",Monaco,monospace;text-align:left;z-index:20}#indicators>div{flex:0 0 1em;margin:1px;padding:1px 4px;background:#000}#canvas-buttons-column{position:absolute;top:0;right:0;width:32px;z-index:30}#fullscreen-toggle-id{display:block;background-image:url("../images/resize.svg");background-position:0 0;background-size:200%}#fullscreen-toggle-id:hover{background-position-x:100%}#side-pane-toggle-id{display:none;background-image:url("../images/gear.svg");transition:transform .1s ease-in-out;-webkit-transition:transform .1s ease-in-out}#side-pane-toggle-id:hover{transform:rotate(-30deg);-webkit-transform:rotate(-30deg);-ms-transform:rotate(-30deg)}#side-pane-checkbox-id:checked+#canvas-container #side-pane-toggle-id:hover{transform:rotate(30deg);-webkit-transform:rotate(30deg);-ms-transform:rotate(30deg)}.hidden{display:none}#fullscreen-checkbox-id:checked+.demo{position:fixed;overflow:hidden}#fullscreen-checkbox-id:checked+.demo #canvas-container{position:fixed;top:0;left:0;width:100vw;height:100vh;margin:0;overflow:hidden;z-index:5}#fullscreen-checkbox-id:checked+.demo #canvas-container #canvas-buttons-column{transition:transform .2s ease-in-out;-webkit-transition:transform .2s ease-in-out}#fullscreen-checkbox-id:checked+.demo #canvas-container #fullscreen-toggle-id{background-position-y:100%}@media only screen and (min-width: 500px){#fullscreen-checkbox-id:checked+.demo #canvas-container #side-pane-toggle-id{display:block}}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id:checked+#canvas-container #canvas-buttons-column{transform:translateX(-400px)}
6 | .loader{position:absolute;top:0;right:0;bottom:0;left:0;width:120px;height:120px;margin:auto}.loader>span{color:#fff;font-size:32px;line-height:120px;text-shadow:1px 1px #000,-1px 1px #000,1px -1px #000,-1px -1px #000,1px 0 #000,-1px 0 #000,0 1px #000,0 -1px #000}.loader-animation{position:absolute;top:0;left:0;width:120px;height:120px;animation:spin 1.1s linear infinite}.loader-animation:before{position:absolute;top:-1px;left:-1px;width:122px;height:122px;border:6px solid rgba(0,0,0,0);border-top:6px solid #000;border-radius:50%;content:"";z-index:50;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}.loader-animation:after{position:absolute;top:0;left:0;width:120px;height:120px;border:4px solid rgba(0,0,0,0);border-top:4px solid #fff;border-radius:50%;content:"";z-index:51;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}
7 | .canvas-button{width:32px;height:32px;cursor:pointer}.controls-block{flex:1 0 0;max-width:36em;margin:16px 0;padding:12px 0;border-radius:8px;border:1px solid #c9c9c9;border:var(--var-color-block-border, 1px solid #c9c9c9);background:#eeeeee;background:var(--var-color-block-background, #eeeeee);z-index:0}@media only screen and (min-width: 540px){.controls-block{margin:16px}}.controls-block>hr{margin:12px 0;clear:both;border:none;border-top:1px solid #c9c9c9;border-top:var(--var-color-block-border, 1px solid #c9c9c9)}.controls-section{display:flex;flex-flow:row wrap;align-items:baseline;margin:0 16px}.controls-section>h2{width:7em;margin:0;font-size:medium;font-weight:bold;line-height:2em;text-align:left}.controls-section>.controls-list{display:flex;flex-direction:column;flex-grow:1}.controls-list>.control{display:flex;flex-flow:row wrap;align-items:center;min-width:300px;padding:3px 0}.control>label{min-width:8em;font-size:95%;line-height:95%;text-align:left}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id~.controls-block{position:fixed;top:0;left:100%;width:400px;max-height:calc(100% - 48px);margin:0;border-width:0 0 1px 1px;border-radius:0 0 0 8px;z-index:50;overflow-x:hidden;overflow-y:auto;transition:transform .2s ease-in-out;-webkit-transition:transform .2s ease-in-out}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id~.controls-block::-webkit-scrollbar{width:16px}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id~.controls-block::-webkit-scrollbar-track{border-radius:8px;background-color:#eeeeee;background-color:var(--var-color-block-background, #eeeeee)}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id~.controls-block::-webkit-scrollbar-thumb{border-width:3px 5px;border-style:solid;border-radius:8px;border-color:#eeeeee;border-color:var(--var-color-block-background, #eeeeee);background-color:#a5a5a5;background-color:var(--var-color-scrollbar, #a5a5a5)}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id~.controls-block::-webkit-scrollbar-thumb:focus,#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id~.controls-block::-webkit-scrollbar-thumb:hover{background-color:#b2b2b2;background-color:var(--var-color-scrollbar-hover, #b2b2b2)}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id~.controls-block::-webkit-scrollbar-thumb:active{background-color:#959595;background-color:var(--var-color-scrollbar-active, #959595)}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id~.controls-block:hover::-webkit-scrollbar-thumb{border-width:3px}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id:checked~.controls-block{transform:translateX(-100%);-webkit-transform:translateX(-100%);-ms-transform:translateX(-100%)}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id:checked~.controls-block .tooltip{transform:translateX(-100vw) translateX(400px);-webkit-transform:translateX(-100vw) translateX(400px);-ms-transform:translateX(-100vw) translateX(400px)}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id:checked~.controls-block>#side-pane-close-toggle-id{display:block}#side-pane-close-toggle-id{display:none;position:absolute;top:0;right:0}#side-pane-close-toggle-id svg{stroke:#5e5e5e;stroke:var(--var-color-block-actionitem, #5e5e5e)}#side-pane-close-toggle-id svg:focus,#side-pane-close-toggle-id svg:hover{stroke:#7e7e7e;stroke:var(--var-color-block-actionitem-hover, #7e7e7e)}#side-pane-close-toggle-id svg:active{stroke:#535353;stroke:var(--var-color-block-actionitem-active, #535353)}
8 | .tabs{display:flex;position:relative;flex-flow:row wrap;flex-grow:1;width:auto;border-radius:4px;background:none;overflow:hidden}.tabs::after{position:absolute;top:0;left:0;width:100%;height:100%;border-width:2px;border-style:solid;border-color:#c9c9c9;border-color:var(--var-color-control-neutral, #c9c9c9);border-radius:4px;content:"";z-index:1;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}.tabs.compact>input+label{padding:6px 14px;font-size:75%}.tabs>input{position:absolute;top:0;left:0;width:1px;height:1px;opacity:0}.tabs>input+label{flex:1;padding:8px 14px;font-size:87.5%;font-weight:bold;text-align:center;white-space:nowrap;cursor:pointer;z-index:2}.tabs>input:disabled+label,.tabs>input[type=radio]:checked+label{cursor:default}.tabs>input+label{background:none;color:#009688;color:var(--var-color-control-accent, #009688)}.tabs>input:checked+label{background:#009688;background:var(--var-color-control-accent, #009688);color:#fff}.tabs>input:disabled+label{background:none;color:#a5a5a5}.tabs>input:disabled:checked+label{background:#a5a5a5;color:#fff}.tabs>input[type=checkbox]:not(:disabled):hover+label,.tabs>input[type=checkbox]:not(:disabled):focus+label{background:rgba(0,150,136,.05)}.tabs>input[type=checkbox]:not(:disabled):hover:checked+label,.tabs>input[type=checkbox]:not(:disabled):focus:checked+label{background:#26a69a;background:var(--var-color-control-accent-hover, #26a69a)}.tabs>input[type=checkbox]:not(:disabled):active+label{background:rgba(0,150,136,.1)}.tabs>input[type=checkbox]:not(:disabled):active:checked+label{background:#00897b;background:var(--var-color-control-accent-active, #00897b)}.tabs>input[type=radio]:not(:disabled):not(:checked):hover+label,.tabs>input[type=radio]:not(:disabled):not(:checked):focus+label{background:rgba(0,150,136,.05)}.tabs>input[type=radio]:not(:disabled):not(:checked):active+label{background:rgba(0,150,136,.1)}
9 | .checkbox{display:block;position:relative;text-align:left;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.checkbox>input[type=checkbox]{width:1px;height:1px;opacity:0}.checkbox>input[type=checkbox]+label.checkmark,.checkbox>input[type=checkbox]+label.checkmark-line{margin-left:24px;line-height:26px;cursor:pointer}.checkbox>input[type=checkbox]:disabled+label.checkmark,.checkbox>input[type=checkbox]:disabled+label.checkmark-line{cursor:default}.checkbox>input[type=checkbox]+label.checkmark::before{position:absolute;top:calc(.5*(100% - 20px));left:0;width:20px;height:20px;border-width:2px;border-style:solid;border-radius:2px;background:none;content:"";box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}.checkbox>input[type=checkbox]+label.checkmark::after{position:absolute;top:calc(.5*(100% - 20px) + .5*(20px - 14px));right:0;bottom:0;left:6.5px;width:7px;height:14px;border:solid #fff;border-width:0 3px 3px 0;background:none;content:"";box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;transform:translate(0, -1px) scale(0) rotate(45deg);-webkit-transform:translate(0, -1px) scale(0) rotate(45deg);-ms-transform:translate(0, -1px) scale(0) rotate(45deg)}.checkbox>input[type=checkbox]:checked+label.checkmark::after{transform:translate(0, -1px) scale(1) rotate(45deg);-webkit-transform:translate(0, -1px) scale(1) rotate(45deg);-ms-transform:translate(0, -1px) scale(1) rotate(45deg)}.checkbox>input[type=checkbox]+label.checkmark::before{border-color:#009688;border-color:var(--var-color-control-accent, #009688)}.checkbox>input[type=checkbox]:checked+label.checkmark::before{background:#009688;background:var(--var-color-control-accent, #009688)}.checkbox>input[type=checkbox]:hover+label.checkmark::before,.checkbox>input[type=checkbox]:focus+label.checkmark::before{border-color:#26a69a;border-color:var(--var-color-control-accent-hover, #26a69a)}.checkbox>input[type=checkbox]:hover:checked+label.checkmark::before,.checkbox>input[type=checkbox]:focus:checked+label.checkmark::before{background:#26a69a;background:var(--var-color-control-accent-hover, #26a69a)}.checkbox>input[type=checkbox]:active+label.checkmark::before{border-color:#00897b;border-color:var(--var-color-control-accent-active, #00897b)}.checkbox>input[type=checkbox]:active:checked+label.checkmark::before{background:#00897b;background:var(--var-color-control-accent-active, #00897b)}.checkbox>input[type=checkbox]:disabled+label.checkmark::before{border-color:#a5a5a5}.checkbox>input[type=checkbox]:disabled:checked+label.checkmark::before{background:#a5a5a5}
10 | .range-container{display:inline-block;position:relative;flex:1 1 0%;width:100%;min-width:15px;height:26px}.range-container input[type=range]{width:100%;min-width:128px;height:100%;margin:0;padding:0;opacity:0}.range-container input[type=range]:not(:disabled){cursor:pointer}.range-container .range-skin-container{display:flex;position:absolute;top:0;left:0;flex-flow:nowrap;width:100%;height:100%;pointer-events:none;user-select:none}.range-container .range-stub{position:relative;flex-grow:0;flex-shrink:0;width:7px}.range-container .range-progress{display:flex;flex:1;flex-flow:row nowrap}.range-container .range-progress-left{position:relative;flex-grow:0;flex-shrink:0;width:85%}.range-container .range-progress-right{position:relative;flex-grow:1}.range-container .range-bar{position:absolute;left:0;width:100%;z-index:0}.range-container .range-bar.range-bar-left{top:12px;height:3px}.range-container .range-bar.range-bar-right{top:12px;height:3px;background:#c9c9c9;background:var(--var-color-control-neutral, #c9c9c9)}.range-container .range-bar.range-stub-left{border-radius:3px 0 0 3px}.range-container .range-bar.range-stub-right{border-radius:0 3px 3px 0}.range-container .range-handle{position:absolute;top:5.5px;right:-7.5px;width:15px;height:15px;border-radius:50%;z-index:1}.range-container .range-bar-left,.range-container .range-handle{background:#009688;background:var(--var-color-control-accent, #009688)}.range-container input[type=range]:not(:disabled):hover+.range-skin-container .range-handle,.range-container input[type=range]:not(:disabled):focus+.range-skin-container .range-handle{background:#26a69a;background:var(--var-color-control-accent-hover, #26a69a)}.range-container input[type=range]:not(:disabled):active+.range-skin-container .range-handle{background:#00897b;background:var(--var-color-control-accent-active, #00897b)}.range-container input[type=range]:disabled+.range-skin-container .range-bar-left,.range-container input[type=range]:disabled+.range-skin-container .range-handle{background:#a5a5a5}.range-container .range-tooltip{position:absolute;top:-28px;right:0;min-width:24px;padding:4px;transform:translateX(50%);transition:opacity .1s ease-in-out;border-radius:4px;background:#535353;color:#eee;font-size:87.5%;text-align:center;opacity:0;z-index:2}.range-container input[type=range]:hover+.range-skin-container .range-tooltip,.range-container input[type=range]:active+.range-skin-container .range-tooltip,.range-container input[type=range]:focus+.range-skin-container .range-tooltip{opacity:1}.range-container .range-tooltip::after{position:absolute;top:100%;left:50%;width:0px;height:12px;margin-left:-6px;border-width:6px;border-style:solid;border-color:#535353 rgba(0,0,0,0) rgba(0,0,0,0);content:""}
11 | :root{--var-color-theme:white;--var-color-page-background:#ededed;--var-page-background-image:url("../images/noise-light.png");--var-color-block-background:#eeeeee;--var-color-block-border:1px solid #c9c9c9;--var-color-title:#535353;--var-color-text:#676767;--var-color-block-actionitem:#5e5e5e;--var-color-block-actionitem-hover:#7e7e7e;--var-color-block-actionitem-active:#535353;--var-color-scrollbar:#a5a5a5;--var-color-scrollbar-hover:#b2b2b2;--var-color-scrollbar-active:#959595;--var-color-control-neutral:#c9c9c9;--var-color-control-accent:#009688;--var-color-control-accent-hover:#26a69a;--var-color-control-accent-active:#00897b}@media(prefers-color-scheme: dark){:root{--var-color-theme:black;--var-color-page-background:#232323;--var-page-background-image:url("../images/noise-dark.png");--var-color-block-background:#202020;--var-color-block-border:1px solid #535353;--var-color-title:#eeeeee;--var-color-text:#dbdbdb;--var-color-block-actionitem:#dbdbdb;--var-color-block-actionitem-hover:#eeeeee;--var-color-block-actionitem-active:#c9c9c9;--var-color-scrollbar:#7e7e7e;--var-color-scrollbar-hover:#959595;--var-color-scrollbar-active:#676767;--var-color-control-neutral:#5e5e5e;--var-color-control-accent:#26a69a;--var-color-control-accent-hover:#4db6ac;--var-color-control-accent-active:#009688}}:root{color-scheme:light dark}html{display:flex;min-height:100%;font-family:Arial,Helvetica,sans-serif}body{display:flex;flex:1;flex-direction:column;min-height:100vh;margin:0px;background-attachment:fixed;background:#ededed;background:var(--var-color-page-background, #ededed);background-image:url("../images/noise-light.png");background-image:var(--var-page-background-image, url("../images/noise-light.png"));color:#676767;color:var(--var-color-text, #676767)}main{display:block;flex-grow:1;padding-bottom:32px}h1,h2,h3{color:#535353;color:var(--var-color-title, #535353)}
12 | .badge{width:32px;height:32px;margin:8px 12px;border:none}.badge>svg{width:32px;height:32px}.badge,.badge:hover,.badge:focus,.badge:active{border:none}.badge svg{fill:#5e5e5e;fill:var(--var-color-block-actionitem, #5e5e5e)}.badge svg:focus,.badge svg:hover{fill:#7e7e7e;fill:var(--var-color-block-actionitem-hover, #7e7e7e)}.badge svg:active{fill:#535353;fill:var(--var-color-block-actionitem-active, #535353)}.badge-shelf{display:flex;flex-flow:row;justify-content:center}footer{align-items:center;padding:8px;text-align:center;border-top:1px solid #c9c9c9;border-top:var(--var-color-block-border, 1px solid #c9c9c9);background:#eeeeee;background:var(--var-color-block-background, #eeeeee)}
13 |
--------------------------------------------------------------------------------
/docs/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piellardj/navier-stokes-webgl/8327673365cf22262dac10596dc1251449c1d12c/docs/favicon-16x16.png
--------------------------------------------------------------------------------
/docs/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piellardj/navier-stokes-webgl/8327673365cf22262dac10596dc1251449c1d12c/docs/favicon-32x32.png
--------------------------------------------------------------------------------
/docs/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piellardj/navier-stokes-webgl/8327673365cf22262dac10596dc1251449c1d12c/docs/favicon-96x96.png
--------------------------------------------------------------------------------
/docs/images/gear.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/docs/images/noise-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piellardj/navier-stokes-webgl/8327673365cf22262dac10596dc1251449c1d12c/docs/images/noise-dark.png
--------------------------------------------------------------------------------
/docs/images/noise-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piellardj/navier-stokes-webgl/8327673365cf22262dac10596dc1251449c1d12c/docs/images/noise-light.png
--------------------------------------------------------------------------------
/docs/images/resize.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Navier-Stokes
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
26 |
27 |
28 |
Navier-Stokes
29 |
30 |
31 |
This project is a WebGL incompressible fluid simulation running entirely on your GPU. You can interact with the fluid with the left mouse button and visualize both the velocity and the pressure of the fluid.
32 |
This is an implementation of the Stable Fluid described by J. Stam.
33 |
34 |
35 |
36 |
39 |
40 |
41 |
42 |
43 | You need to enable Javascript to run this experiment.
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
56 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | Simulation
75 |
76 |
77 |
88 |
89 |
Float texture:
90 |
91 |
92 |
93 |
94 |
95 |
96 |
Solver steps:
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
Time step:
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
Stream:
145 |
146 |
147 |
148 |
149 |
150 |
161 |
162 |
163 |
164 |
165 | Brush
166 |
167 |
168 |
169 |
Radius:
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
Strength:
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 | Display
221 |
222 |
223 |
232 |
233 |
Intensity:
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
Color:
258 |
259 |
260 |
261 |
262 |
263 |
264 |
Obstacles:
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
299 |
306 |
307 |
308 |
309 |
310 |
--------------------------------------------------------------------------------
/docs/readme/css/page.css:
--------------------------------------------------------------------------------
1 | a{color:#009688;color:var(--var-color-control-accent, #009688);font-weight:bold;text-decoration:none;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;border-width:0 0 2px;border-style:solid;border-color:rgba(0,0,0,0)}a:focus,a:hover{border-color:#009688;border-color:var(--var-color-control-accent, #009688)}:root{--color-code: #e0e0e0}@media(prefers-color-scheme: dark){:root{--color-code: #343434}}body{max-width:100%}.contents{line-height:1.5em;max-width:900px;margin:auto;padding:16px 32px;border-radius:8px;border:1px solid #c9c9c9;border:var(--var-color-block-border, 1px solid #c9c9c9);background:#eeeeee;background:var(--var-color-block-background, #eeeeee)}h1{text-align:center;margin-bottom:1em}pre{overflow-x:auto;background:var(--color-code);padding:4px 16px;border-radius:8px;line-height:1.45}pre::-webkit-scrollbar{width:16px}pre::-webkit-scrollbar-track{background-color:rgba(0,0,0,0)}pre::-webkit-scrollbar-thumb{border-width:6px;border-style:solid;border-radius:8px;border-color:var(--color-code);background-color:#a5a5a5;background-color:var(--var-color-scrollbar, #a5a5a5)}pre::-webkit-scrollbar-thumb:focus,pre::-webkit-scrollbar-thumb:hover{background-color:#b2b2b2;background-color:var(--var-color-scrollbar-hover, #b2b2b2)}pre::-webkit-scrollbar-thumb:active{background-color:#959595;background-color:var(--var-color-scrollbar-active, #959595)}pre:hover::-webkit-scrollbar-thumb{border-width:5px}pre code{padding:0}code{background:var(--color-code);padding:2px 4px;border-radius:3px;font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;line-height:1.5em}video,img{max-width:100%;border-radius:8px}
2 | .logo{display:block;position:relative;width:64px;height:64px;margin:8px auto 16px;border-radius:50%;user-select:none;box-sizing:border-box}.logo,.logo:hover,.logo:focus,.logo:active{border-width:1px;border-style:solid;border-color:#009688;border-color:var(--var-color-control-accent, #009688)}.logo::before,.logo svg.logo-icon,.logo::after{position:absolute;top:-1px;left:-1px;width:64px;height:64px;border-radius:50%;pointer-events:none}.logo svg.logo-icon{stroke:#009688;stroke:var(--var-color-control-accent, #009688);fill:#009688;fill:var(--var-color-control-accent, #009688)}.logo::before{content:"";transform:scale(0);-webkit-transform:scale(0);-ms-transform:scale(0);transition:.1s ease;-webkit-transition:.1s ease}.logo.logo-animate-fill .logo::before{content:"";transform:scale(0);-webkit-transform:scale(0);-ms-transform:scale(0);transition:.1s ease;-webkit-transition:.1s ease}.logo:hover::before{transform:scale(1);-webkit-transform:scale(1);-ms-transform:scale(1)}.logo.logo-animate-fill{background:#eeeeee;background:var(--var-color-block-background, #eeeeee)}.logo.logo-animate-fill::before{background:#009688;background:var(--var-color-control-accent, #009688)}.logo.logo-animate-fill:hover svg.logo-icon{fill:#fff;stroke:#fff}.logo.logo-animate-empty{background:#009688;background:var(--var-color-control-accent, #009688)}.logo.logo-animate-empty::before{top:0;left:0;width:62px;height:62px;background:#eeeeee;background:var(--var-color-block-background, #eeeeee)}
3 | :root{--var-color-theme:white;--var-color-page-background:#ededed;--var-page-background-image:url("../images/noise-light.png");--var-color-block-background:#eeeeee;--var-color-block-border:1px solid #c9c9c9;--var-color-title:#535353;--var-color-text:#676767;--var-color-block-actionitem:#5e5e5e;--var-color-block-actionitem-hover:#7e7e7e;--var-color-block-actionitem-active:#535353;--var-color-scrollbar:#a5a5a5;--var-color-scrollbar-hover:#b2b2b2;--var-color-scrollbar-active:#959595;--var-color-control-neutral:#c9c9c9;--var-color-control-accent:#009688;--var-color-control-accent-hover:#26a69a;--var-color-control-accent-active:#00897b}@media(prefers-color-scheme: dark){:root{--var-color-theme:black;--var-color-page-background:#232323;--var-page-background-image:url("../images/noise-dark.png");--var-color-block-background:#202020;--var-color-block-border:1px solid #535353;--var-color-title:#eeeeee;--var-color-text:#dbdbdb;--var-color-block-actionitem:#dbdbdb;--var-color-block-actionitem-hover:#eeeeee;--var-color-block-actionitem-active:#c9c9c9;--var-color-scrollbar:#7e7e7e;--var-color-scrollbar-hover:#959595;--var-color-scrollbar-active:#676767;--var-color-control-neutral:#5e5e5e;--var-color-control-accent:#26a69a;--var-color-control-accent-hover:#4db6ac;--var-color-control-accent-active:#009688}}:root{color-scheme:light dark}html{display:flex;min-height:100%;font-family:Arial,Helvetica,sans-serif}body{display:flex;flex:1;flex-direction:column;min-height:100vh;margin:0px;background-attachment:fixed;background:#ededed;background:var(--var-color-page-background, #ededed);background-image:url("../images/noise-light.png");background-image:var(--var-page-background-image, url("../images/noise-light.png"));color:#676767;color:var(--var-color-text, #676767)}main{display:block;flex-grow:1;padding-bottom:32px}h1,h2,h3{color:#535353;color:var(--var-color-title, #535353)}
4 | .badge{width:32px;height:32px;margin:8px 12px;border:none}.badge>svg{width:32px;height:32px}.badge,.badge:hover,.badge:focus,.badge:active{border:none}.badge svg{fill:#5e5e5e;fill:var(--var-color-block-actionitem, #5e5e5e)}.badge svg:focus,.badge svg:hover{fill:#7e7e7e;fill:var(--var-color-block-actionitem-hover, #7e7e7e)}.badge svg:active{fill:#535353;fill:var(--var-color-block-actionitem-active, #535353)}.badge-shelf{display:flex;flex-flow:row;justify-content:center}footer{align-items:center;padding:8px;text-align:center;border-top:1px solid #c9c9c9;border-top:var(--var-color-block-border, 1px solid #c9c9c9);background:#eeeeee;background:var(--var-color-block-background, #eeeeee)}
5 |
--------------------------------------------------------------------------------
/docs/readme/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piellardj/navier-stokes-webgl/8327673365cf22262dac10596dc1251449c1d12c/docs/readme/favicon-16x16.png
--------------------------------------------------------------------------------
/docs/readme/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piellardj/navier-stokes-webgl/8327673365cf22262dac10596dc1251449c1d12c/docs/readme/favicon-32x32.png
--------------------------------------------------------------------------------
/docs/readme/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piellardj/navier-stokes-webgl/8327673365cf22262dac10596dc1251449c1d12c/docs/readme/favicon-96x96.png
--------------------------------------------------------------------------------
/docs/readme/images/noise-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piellardj/navier-stokes-webgl/8327673365cf22262dac10596dc1251449c1d12c/docs/readme/images/noise-dark.png
--------------------------------------------------------------------------------
/docs/readme/images/noise-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piellardj/navier-stokes-webgl/8327673365cf22262dac10596dc1251449c1d12c/docs/readme/images/noise-light.png
--------------------------------------------------------------------------------
/docs/readme/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Navier-Stokes - Explanations
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
26 |
27 |
28 |
navier-stokes-webgl
29 |
Stable fluid simulation on GPU using WebGL.
30 |
Live version here .
31 |
This is an implementation of the Stable Fluid described by Jos Stam.
32 |
33 |
Simulation
34 |
The simulation is implemented on GPU with the method provided in GPU Gems.
35 | The diffusion term was dropped since it didn't have much visual influence.
36 |
Data storage
37 |
This simulation can run in two modes for storing the velocities:
38 |
39 | velocity stored in float textures: each component (x, y) is stored as a 32bit float.
40 | To do so the following extensions must be available: OES_texture_float
, WEBGL_color_buffer_float
, OES_texture_float_linear
.
41 | velocity stored in normal textures with four 8bit channels.
42 | In this mode each component is stored as a 16bit fixed point value, encoded in two 8bit texture channels.
43 | This mode provides less precision for the computing, and you can see artifacts if you push the display color intensity to the maximum.
44 |
45 |
46 |
47 |
48 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/docs/script/main.min.js:
--------------------------------------------------------------------------------
1 | !function(){"use strict";var e={737:function(e,t,r){var n,o=this&&this.__extends||(n=function(e,t){return n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(e[r]=t[r])},n(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function r(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}),i=this&&this.__createBinding||(Object.create?function(e,t,r,n){void 0===n&&(n=r);var o=Object.getOwnPropertyDescriptor(t,r);o&&!("get"in o?!t.__esModule:o.writable||o.configurable)||(o={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,n,o)}:function(e,t,r,n){void 0===n&&(n=r),e[n]=t[r]}),a=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),s=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)"default"!==r&&Object.prototype.hasOwnProperty.call(e,r)&&i(t,e,r);return a(t,e),t},u=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var l=u(r(771)),c=s(r(61)),d=s(r(363)),f=function(e){function t(t){var r=e.call(this,t)||this;return r._drawShader=d.buildDrawShader(t),r.thickness=2,r}return o(t,e),t.prototype.freeGLResources=function(){this._drawShader.freeGLResources()},t.prototype.draw=function(){var t=e.prototype.gl.call(this),r=t.canvas,n=[r.clientWidth,r.clientHeight],o=this._drawShader;o.use();var i=[c.brush.radius/n[0],c.brush.radius/n[1]];o.u.uBrushSize.value=i,o.u.uBrushPos.value=c.mouse.pos,o.u.uThickness.value=this.thickness/c.brush.radius,o.bindUniformsAndAttributes(),t.drawArrays(t.TRIANGLE_STRIP,0,4)},t}(l.default);t.default=f},850:function(e,t,r){var n,o=this&&this.__extends||(n=function(e,t){return n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(e[r]=t[r])},n(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function r(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}),i=this&&this.__createBinding||(Object.create?function(e,t,r,n){void 0===n&&(n=r);var o=Object.getOwnPropertyDescriptor(t,r);o&&!("get"in o?!t.__esModule:o.writable||o.configurable)||(o={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,n,o)}:function(e,t,r,n){void 0===n&&(n=r),e[n]=t[r]}),a=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),s=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)"default"!==r&&Object.prototype.hasOwnProperty.call(e,r)&&i(t,e,r);return a(t,e),t},u=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var l=u(r(771)),c=u(r(72)),d=s(r(61)),f=s(r(916));r(457);var v=function(e){function t(t,r,n){var o=e.call(this,t)||this;return o._useFloatTextures=!1,o.viscosity=2e-4,o.colorIntensity=.033,o.color=!0,o.reset(r,n),o}return o(t,e),t.prototype.freeGLResources=function(){this._FBO&&this._FBO.freeGLResources(),this.freeTextures(),this.freeShaders()},t.prototype.freeTextures=function(){var t=e.prototype.gl.call(this);this._velTextures&&(t.deleteTexture(this._velTextures[0]),t.deleteTexture(this._velTextures[1])),t.deleteTexture(this._tmpTexture),t.deleteTexture(this._pressureTexture),t.deleteTexture(this._divergenceTexture)},t.prototype.freeShaders=function(){function e(e){e&&e.freeGLResources()}e(this._drawVelocityShader),e(this._drawPressureShader),e(this._addVelShader),e(this._advectShader),e(this._jacobiPressureShader),e(this._divergenceShader),e(this._substractGradientShader),e(this._obstaclesVelocityShader)},t.prototype.reset=function(t,r){this.freeGLResources(),this._width=t,this._height=r,this.dx=1/Math.min(t,r),this._FBO=new c.default(e.prototype.gl.call(this),t,r),this.initTextures(),this.buildShaders(),this._currIndex=0},Object.defineProperty(t.prototype,"useFloatTextures",{set:function(e){e!==this._useFloatTextures&&(this._useFloatTextures=e,this.reset(this._width,this._height))},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"minNbIterations",{set:function(e){this._nbIterations=2*Math.ceil(e/2)+1},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"texelSize",{get:function(){return[1/this._width,1/this._height]},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"velTexture",{get:function(){return this._velTextures[this.currIndex]},enumerable:!1,configurable:!0}),t.prototype.update=function(t){var r=e.prototype.gl.call(this),n=this.timestep;if(r.clearColor(.5,0,.5,0),Page.Canvas.isMouseDown()){var o=r.canvas,i=[o.clientWidth,o.clientHeight],a=[d.brush.radius/i[0],d.brush.radius/i[1]],s=d.mouse.pos,u=[d.mouse.movement[0]*d.brush.strength,d.mouse.movement[1]*d.brush.strength];this.addVel(s,a,u)}this.advect(n),this.obstaclesVelocity(t),this.project(t),this.obstaclesVelocity(t)},t.prototype.addVel=function(e,t,r){var n=this.gl(),o=this._addVelShader;o.u.uVel.value=this._velTextures[this.currIndex],o.u.uBrushPos.value=e,o.u.uBrushSize.value=t,o.u.uAddVel.value=r,o.use(),this._FBO.bind([this._velTextures[this.nextIndex]]),o.bindUniformsAndAttributes(),n.drawArrays(n.TRIANGLE_STRIP,0,4),this.switchBuffers()},t.prototype.drawVelocity=function(){var e=this.gl(),t=this._drawVelocityShader;t.u.uVel.value=this._velTextures[this.currIndex],t.u.uColorIntensity.value=this.colorIntensity,t.u.uBlacknWhite.value=!this.color,t.use(),t.bindUniformsAndAttributes(),e.drawArrays(e.TRIANGLE_STRIP,0,4)},t.prototype.drawPressure=function(){var e=this.gl(),t=this._drawPressureShader;t.u.uPressure.value=this._pressureTexture,t.u.uColorIntensity.value=this.colorIntensity,t.u.uBlacknWhite.value=!this.color,t.use(),t.bindUniformsAndAttributes(),e.drawArrays(e.TRIANGLE_STRIP,0,4)},Object.defineProperty(t.prototype,"currIndex",{get:function(){return this._currIndex},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"nextIndex",{get:function(){return(this._currIndex+1)%2},enumerable:!1,configurable:!0}),t.prototype.switchBuffers=function(){this._currIndex=this.nextIndex},t.prototype.obstaclesVelocity=function(e){var t=this.gl(),r=this._obstaclesVelocityShader;r.u.uVelocities.value=this._velTextures[this.currIndex],r.u.uObstacles.value=e.texture,r.u.uTexelSize.value=this.texelSize,this._FBO.bind([this._velTextures[this.nextIndex]]),r.use(),r.bindUniformsAndAttributes(),t.drawArrays(t.TRIANGLE_STRIP,0,4),this.switchBuffers()},t.prototype.advect=function(e){var t=this.gl(),r=this._advectShader;r.u.uVelUnit.value=[128/this._width,128/this._height],r.u.uDt.value=e,r.u.uQuantity.value=this._velTextures[this.currIndex],r.u.uVel.value=this._velTextures[this.currIndex],this._FBO.bind([this._velTextures[this.nextIndex]]),r.use(),r.bindUniformsAndAttributes(),t.drawArrays(t.TRIANGLE_STRIP,0,4),this.switchBuffers()},t.prototype.computeDivergence=function(){var e=this.gl(),t=this._divergenceShader;t.u.uTexelSize.value=this.texelSize,t.u.uVelocity.value=this._velTextures[this.currIndex],this._FBO.bind([this._divergenceTexture]),e.clear(e.COLOR_BUFFER_BIT),t.use(),t.bindUniformsAndAttributes(),e.drawArrays(e.TRIANGLE_STRIP,0,4)},t.prototype.computePressure=function(e){var t=this.gl(),r=this._jacobiPressureShader,n=-.5*this.dx,o=this._pressureTexture,i=this._divergenceTexture;r.u.uTexelSize.value=this.texelSize,r.u.uAlpha.value=n,r.u.uInvBeta.value=1/4,r.u.uConstantTerm.value=i,r.u.uObstacles.value=e.texture;var a=0,s=[this._tmpTexture,o];this._FBO.bind([this._tmpTexture]),t.clear(t.COLOR_BUFFER_BIT),r.use(),r.bindAttributes();for(var u=0;u16&&(t[0]*=16/r,t[1]*=16/r,this._pivotInPx[0]=e[0]+t[0],this._pivotInPx[1]=e[1]+t[1]);var n=[-t[0]/16,-t[1]/16];this.setMovementInPx(n),this._posInPx=e,this._pos=this.setRelative(e)},e.prototype.setMovementInPx=function(e){this._movementInPx=e,this._movement=this.setRelative(e)},e.prototype.setRelative=function(e){var t=Page.Canvas.getSize();return[e[0]/t[0],e[1]/t[1]]},e}(),u=new s;t.mouse=u;var l={radius:10,strength:100};t.brush=l;var c,d={stream:!0};t.fluid=d,function(e){e.NONE="none",e.ONE="one",e.MANY="many"}(c||(c={}));var f=c.NONE;t.obstacles=f;var v={velocity:!0,pressure:!0,brush:!0,obstacles:!0};t.display=v,t.bind=function(e){!function(e){var r="resolution",n=function(t){var r=+t[0];e.reset(r,r)};Page.Tabs.addObserver(r,n),n(Page.Tabs.getValues(r));var o="float-texture-checkbox-id";a.allExtensionsLoaded||(Page.Controls.setVisibility(o,!1),Page.Checkbox.setChecked(o,!1));var i=function(t){e.useFloatTextures=t};Page.Checkbox.addObserver(o,i),i(Page.Checkbox.isChecked(o));var s="solver-steps-range-id",u=function(t){e.minNbIterations=t};Page.Range.addObserver(s,u),u(Page.Range.getValue(s));var c="timestep-range-id",h=function(t){e.timestep=t};Page.Range.addObserver(c,h),h(Page.Range.getValue(c));var _="stream-checkbox-id",p=function(e){d.stream=e};Page.Checkbox.addObserver(_,p),p(Page.Checkbox.isChecked(_));var b="obstacles",m=function(e){t.obstacles=f=e[0]};Page.Tabs.addObserver(b,m),m(Page.Tabs.getValues(b));var g="brush-radius-range-id",y=function(e){l.radius=e};Page.Range.addObserver(g,y),y(Page.Range.getValue(g)),Page.Canvas.Observers.mouseWheel.push((function(e){Page.Range.setValue(g,l.radius+5*e),y(Page.Range.getValue(g))}));var x="brush-strength-range-id",E=function(e){l.strength=e};Page.Range.addObserver(x,E),E(Page.Range.getValue(x));var T="displayed-fields",P=function(e){v.velocity="velocity"===e[0]||"velocity"===e[1],v.pressure="pressure"===e[0]||"pressure"===e[1]};Page.Tabs.addObserver(T,P),P(Page.Tabs.getValues(T));var S="intensity-range-id",O=function(t){e.colorIntensity=t};Page.Range.addObserver(S,O),O(Page.Range.getValue(S));var D="display-color-checkbox-id",R=function(t){e.color=t};Page.Checkbox.addObserver(D,R),R(Page.Checkbox.isChecked(D));var C="display-obstacles-checkbox-id",A=function(e){v.obstacles=e};Page.Checkbox.addObserver(C,A),A(Page.Checkbox.isChecked(C))}(e),t.mouse=u=new s}},761:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.allExtensionsLoaded=t.loadExtensions=t.check=void 0,r(457),t.check=function(e){return!(e.getShaderPrecisionFormat(e.FRAGMENT_SHADER,e.MEDIUM_FLOAT).precision<23&&(Page.Demopage.setErrorMessage("webgl-requirements","Your device only supports low precision float in fragment shader.\nThe simulation will not run."),1))};var n=!1;t.allExtensionsLoaded=n,t.loadExtensions=function(e,r){t.allExtensionsLoaded=n=!0;for(var o=0,i=0,a=r;i 1.0)\n discard;\n\n const vec3 color = vec3(1);\n\n gl_FragColor = vec4(color, 1.0);\n}");t.buildDrawShader=function(e){var t=new o.default(e,a.vert,a.frag);return t.a.aCorner.VBO=i.default.createQuad(e,-1,-1,1,1),t}},445:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.ShaderSrc=t.fetch=void 0,t.fetch=function(e){var t=new XMLHttpRequest;return t.open("GET",e,!1),t.send(),t.responseText};var r=function(){function e(e,t){this.vert=e,this.frag=t}return e.prototype.batchReplace=function(e){for(var t=0,r=e;t 1.0)\n discard;\n\n vec2 normal = -toCenter / dist;\n gl_FragColor = encodeObstacle(normal);\n}");c.batchReplace(u),t.buildDrawShader=function(e){var t=new o.default(e,l.vert,l.frag);return t.a.aCorner.VBO=i.default.createQuad(e,0,0,1,1),t},t.buildAddShader=function(e){var t=new o.default(e,c.vert,c.frag);return t.a.aCorner.VBO=i.default.createQuad(e,-1,-1,1,1),t}}},t={};!function r(n){var o=t[n];if(void 0!==o)return o.exports;var i=t[n]={exports:{}};return e[n].call(i.exports,i,i.exports,r),i.exports}(633)}();
2 | //# sourceMappingURL=main.min.js.map
--------------------------------------------------------------------------------
/docs/script/page.min.js:
--------------------------------------------------------------------------------
1 | var Page;!function(e){var e=e.Demopage||(e.Demopage={}),r="error-messages",t=document.getElementById(r);if(!t)throw new Error("Cannot find element '"+r+"'.");function a(e){return t?t.querySelector("span[id=error-message-"+e+"]"):null}e.setErrorMessage=function(e,r){var n;t&&((n=a(e))?n.innerHTML=r:((n=document.createElement("span")).id="error-message-"+e,n.innerText=r,t.appendChild(n),t.appendChild(document.createElement("br"))))},e.removeErrorMessage=function(e){var r;t&&(e=a(e))&&((r=e.nextElementSibling)&&t.removeChild(r),t.removeChild(e))}}(Page=Page||{});
2 | var Page;!function(n){var e,t,i,a;function s(e){this.queryParameters={};var t=e.indexOf(s.queryDelimiter);if(t<0)this.baseUrl=e;else{this.baseUrl=e.substring(0,t);for(var r=0,n=e.substring(t+s.queryDelimiter.length).split(s.parameterDelimiter);re.length&&(i=this.queryParameters[a],t(a.substring(e.length),i))}},s.prototype.buildUrl=function(){for(var e=[],t=0,r=Object.keys(this.queryParameters);t input[type=checkbox][id]").map(function(e){return new r(e)})}),n=new e.Helpers.Storage("checkbox",function(e){return e.checked?"true":"false"},function(e,t){e=c.getByIdSafe(e);return!(!e||"true"!==t&&"false"!==t||(e.checked="true"===t,e.callObservers(),0))}),e.Helpers.Events.callAfterDOMLoaded(function(){c.load(),n.applyStoredState()}),t.addObserver=function(e,t){c.getById(e).observers.push(t)},t.setChecked=function(e,t){c.getById(e).checked=t},t.isChecked=function(e){return c.getById(e).checked},t.storeState=function(e){e=c.getById(e),n.storeState(e)},t.clearStoredState=function(e){e=c.getById(e),n.clearStoredState(e)}}(Page=Page||{});
6 | var Page;!function(r){var e,t,n,s,i;function l(e){var t=this,e=(this.onInputObservers=[],this.onChangeObservers=[],this.inputElement=r.Helpers.Utils.selector(e,"input[type='range']"),this.progressLeftElement=r.Helpers.Utils.selector(e,".range-progress-left"),this.tooltipElement=r.Helpers.Utils.selector(e,"output.range-tooltip"),this.id=this.inputElement.id,+this.inputElement.min),n=+this.inputElement.max,i=+this.inputElement.step;this.nbDecimalsToDisplay=l.getMaxNbDecimals(e,n,i),this.inputElement.addEventListener("input",function(e){e.stopPropagation(),t.reloadValue(),t.callSpecificObservers(t.onInputObservers)}),this.inputElement.addEventListener("change",function(e){e.stopPropagation(),t.reloadValue(),s.storeState(t),t.callSpecificObservers(t.onChangeObservers)}),this.reloadValue()}e=r.Range||(r.Range={}),Object.defineProperty(l.prototype,"value",{get:function(){return this._value},set:function(e){this.inputElement.value=""+e,this.reloadValue()},enumerable:!1,configurable:!0}),l.prototype.callObservers=function(){this.callSpecificObservers(this.onInputObservers),this.callSpecificObservers(this.onChangeObservers)},l.prototype.callSpecificObservers=function(e){for(var t=0,n=e;t input[type='range']").map(function(e){e=e.parentElement;return new t(e)})}),s=new r.Helpers.Storage("range",function(e){return""+e.value},function(e,t){e=n.getByIdSafe(e);return!!e&&(e.value=+t,e.callObservers(),!0)}),r.Helpers.Events.callAfterDOMLoaded(function(){n.load(),s.applyStoredState()}),i=!!window.MSInputMethodContext&&!!document.documentMode,e.addObserver=function(e,t){e=n.getById(e),(i?e.onChangeObservers:e.onInputObservers).push(t)},e.addLazyObserver=function(e,t){n.getById(e).onChangeObservers.push(t)},e.getValue=function(e){return n.getById(e).value},e.setValue=function(e,t){n.getById(e).value=t},e.storeState=function(e){e=n.getById(e),s.storeState(e)},e.clearStoredState=function(e){e=n.getById(e),s.clearStoredState(e)}}(Page=Page||{});
7 |
8 | var Page;!function(h){var n,o,i,T,r,t,c,a,s,u,l,v,d,f,g,p,m,y,E,w,e,L,k,M,C,H,U,b,Y,x,S,X,z,A,D;function O(e){var n=document.querySelector(e);return n||console.error("Cannot find element '"+e+"'."),n}function q(e){return h.Helpers.Utils.selector(document,"input[type=checkbox][id="+e+"]")}function B(e){document.body.style.overflow=e?"hidden":"auto"}function P(){var e=i.getBoundingClientRect();return[Math.floor(e.width),Math.floor(e.height)]}function F(e){return e+"px"}function R(){o.style.width="100vw";var e=P();if(r.checked?(o.style.height="100%",o.style.maxWidth="",o.style.maxHeight=""):(e[1]=e[0]*s/a,o.style.height=F(e[1]),o.style.maxWidth=F(a),o.style.maxHeight=F(s)),e[0]!==u[0]||e[1]!==u[1]){u=P();for(var n=0,t=l;n (https://github.com/piellardj)",
6 | "repository": "github:piellardj/navier-stokes-webgl",
7 | "private": true,
8 | "license": "ISC",
9 | "scripts": {
10 | "pre-commit": "npm run rebuild",
11 | "build-page": "ts-node-script src/generate-page.ts",
12 | "build": "npm run build-page && npm run webpack",
13 | "clean": "shx rm -rf docs/* **/*generated.*",
14 | "rebuild": "npm run clean && npm run build",
15 | "webpack": "webpack --config src/config/webpack.config.js"
16 | },
17 | "engines": {
18 | "node": ">=18.16.0"
19 | },
20 | "devDependencies": {
21 | "@types/node": "^20.3.0",
22 | "shx": "^0.3.4",
23 | "ts-loader": "^9.4.3",
24 | "ts-node": "^10.9.1",
25 | "typescript": "^5.1.3",
26 | "webpack": "^5.86.0",
27 | "webpack-cli": "^5.1.4",
28 | "webpage-templates": "github:piellardj/webpage-templates"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/config/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "../ts/main.ts"
4 | ],
5 | "compilerOptions": {
6 | /* Basic Options */
7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
9 | // "lib": [], /* Specify library files to be included in the compilation. */
10 | // "allowJs": true, /* Allow javascript files to be compiled. */
11 | // "checkJs": true, /* Report errors in .js files. */
12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
15 | // "sourceMap": true, /* Generates corresponding '.map' file. */
16 | // "outFile": "../../tmp/script/skybox-editor.js", /* Concatenate and emit output to single file. */
17 | "outDir": "../../tmp/script", /* Redirect output structure to the directory. */
18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
19 | // "composite": true, /* Enable project compilation */
20 | "removeComments": true, /* Do not emit comments to output. */
21 | // "noEmit": true, /* Do not emit outputs. */
22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
25 | /* Strict Type-Checking Options */
26 | "strict": true, /* Enable all strict type-checking options. */
27 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */
28 | "strictNullChecks": false, /* Enable strict null checks. */
29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
34 | /* Additional Checks */
35 | // "noUnusedLocals": true, /* Report errors on unused locals. */
36 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
37 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
38 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
39 | /* Module Resolution Options */
40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
44 | "typeRoots": [
45 | "../@types",
46 | "../../node_modules/@types"
47 | ], /* List of folders to include type definitions from. */
48 | // "types": [], /* Type declaration files to be included in compilation. */
49 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
50 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
51 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
52 | /* Source Map Options */
53 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
55 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
56 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
57 | /* Experimental Options */
58 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
59 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
60 | }
61 | }
--------------------------------------------------------------------------------
/src/config/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | const PROJECT_DIR = path.resolve(__dirname, "..", "..");
4 |
5 | module.exports = {
6 | devtool: "source-map",
7 | mode: "production",
8 | entry: path.join(PROJECT_DIR, "src", "ts", "main.ts"),
9 | output: {
10 | path: path.join(PROJECT_DIR, "docs", "script"),
11 | filename: "[name].min.js"
12 | },
13 | target: ["web", "es5"],
14 | resolve: {
15 | extensions: [".ts"]
16 | },
17 | module: {
18 | rules: [
19 | {
20 | test: /\.ts?$/,
21 | exclude: /node_modules/,
22 | use: [
23 | {
24 | loader: "ts-loader",
25 | options: {
26 | // transpileOnly: true,
27 | compilerOptions: {
28 | rootDir: path.join(PROJECT_DIR, "src", "ts")
29 | },
30 | configFile: path.join(PROJECT_DIR, "src", "config", 'tsconfig.json')
31 | }
32 | }
33 | ],
34 | }
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/generate-page.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import * as path from "path";
3 | import { Demopage } from "webpage-templates";
4 |
5 | const data = {
6 | title: "Navier-Stokes",
7 | description: "Stable fluid simulation running on GPU",
8 | introduction: [
9 | "This project is a WebGL incompressible fluid simulation running entirely on your GPU. You can interact with the fluid with the left mouse button and visualize both the velocity and the pressure of the fluid.",
10 | "This is an implementation of the Stable Fluid described by J. Stam."
11 | ],
12 | githubProjectName: "navier-stokes-webgl",
13 | readme: {
14 | filepath: path.join(__dirname, "..", "README.md"),
15 | branchName: "master"
16 | },
17 | additionalLinks: [],
18 | scriptFiles: [
19 | "script/main.min.js"
20 | ],
21 | indicators: [
22 | {
23 | id: "fps",
24 | label: "FPS"
25 | }
26 | ],
27 | canvas: {
28 | width: 512,
29 | height: 512,
30 | enableFullscreen: false
31 | },
32 | controlsSections: [
33 | {
34 | title: "Simulation",
35 | controls: [
36 | {
37 | type: Demopage.supportedControls.Tabs,
38 | title: "Resolution",
39 | id: "resolution",
40 | unique: true,
41 | options: [
42 | {
43 | value: "128",
44 | label: "128"
45 | },
46 | {
47 | value: "256",
48 | label: "256",
49 | checked: true
50 | },
51 | {
52 | value: "512",
53 | label: "512"
54 | }
55 | ]
56 | },
57 | {
58 | type: Demopage.supportedControls.Checkbox,
59 | title: "Float texture",
60 | id: "float-texture-checkbox-id",
61 | checked: true
62 | },
63 | {
64 | type: Demopage.supportedControls.Range,
65 | title: "Solver steps",
66 | id: "solver-steps-range-id",
67 | min: 1,
68 | max: 99,
69 | value: 49,
70 | step: 2
71 | },
72 | {
73 | type: Demopage.supportedControls.Range,
74 | title: "Time step",
75 | id: "timestep-range-id",
76 | min: 0.01,
77 | max: 0.1,
78 | value: 0.033,
79 | step: 0.001
80 | },
81 | {
82 | type: Demopage.supportedControls.Checkbox,
83 | title: "Stream",
84 | id: "stream-checkbox-id",
85 | checked: true
86 | },
87 | {
88 | type: Demopage.supportedControls.Tabs,
89 | title: "Obstacles",
90 | id: "obstacles",
91 | unique: true,
92 | options: [
93 | {
94 | value: "none",
95 | label: "None"
96 | },
97 | {
98 | value: "one",
99 | label: "One"
100 | },
101 | {
102 | value: "many",
103 | label: "Many",
104 | checked: true
105 | }
106 | ]
107 | }
108 | ]
109 | },
110 | {
111 | title: "Brush",
112 | controls: [
113 | {
114 | type: Demopage.supportedControls.Range,
115 | title: "Radius",
116 | id: "brush-radius-range-id",
117 | min: 20,
118 | max: 100,
119 | value: 40,
120 | step: 1
121 | },
122 | {
123 | type: Demopage.supportedControls.Range,
124 | title: "Strength",
125 | id: "brush-strength-range-id",
126 | min: 20,
127 | max: 200,
128 | value: 100,
129 | step: 1
130 | }
131 | ]
132 | },
133 | {
134 | title: "Display",
135 | controls: [
136 | {
137 | type: Demopage.supportedControls.Tabs,
138 | title: "Fields",
139 | id: "displayed-fields",
140 | unique: false,
141 | options: [
142 | {
143 | value: "velocity",
144 | label: "Velocity",
145 | checked: true
146 | },
147 | {
148 | value: "pressure",
149 | label: "Pressure"
150 | }
151 | ]
152 | },
153 | {
154 | type: Demopage.supportedControls.Range,
155 | title: "Intensity",
156 | id: "intensity-range-id",
157 | min: 0.1,
158 | max: 10,
159 | value: 1,
160 | step: 0.1
161 | },
162 | {
163 | type: Demopage.supportedControls.Checkbox,
164 | title: "Color",
165 | id: "display-color-checkbox-id",
166 | checked: true
167 | },
168 | {
169 | type: Demopage.supportedControls.Checkbox,
170 | title: "Obstacles",
171 | id: "display-obstacles-checkbox-id",
172 | checked: true
173 | }
174 | ]
175 | }
176 | ]
177 | };
178 |
179 | const DEST_DIR = path.resolve(__dirname, "..", "docs");
180 | const minified = true;
181 |
182 | const buildResult = Demopage.build(data, DEST_DIR, {
183 | debug: !minified,
184 | });
185 |
186 | // disable linting on this file because it is generated
187 | buildResult.pageScriptDeclaration = "/* tslint:disable */\n" + buildResult.pageScriptDeclaration;
188 |
189 | const SCRIPT_DECLARATION_FILEPATH = path.resolve(__dirname, ".", "ts", "page-interface-generated.ts");
190 | fs.writeFileSync(SCRIPT_DECLARATION_FILEPATH, buildResult.pageScriptDeclaration);
191 |
--------------------------------------------------------------------------------
/src/ts/brush.ts:
--------------------------------------------------------------------------------
1 | import GLResource from "./gl-utils/gl-resource";
2 | import Shader from "./gl-utils/shader";
3 | import * as Parameters from "./parameters";
4 | import * as BrushShaders from "./shaders/brush-shaders";
5 |
6 | class Brush extends GLResource {
7 | public thickness: number;
8 |
9 | private _drawShader: Shader;
10 |
11 | constructor(gl: WebGLRenderingContext) {
12 | super(gl);
13 | this._drawShader = BrushShaders.buildDrawShader(gl);
14 |
15 | this.thickness = 2;
16 | }
17 |
18 | public freeGLResources(): void {
19 | this._drawShader.freeGLResources();
20 | }
21 |
22 | public draw(): void {
23 | const gl = super.gl();
24 | const canvas = gl.canvas as HTMLCanvasElement;
25 | const canvasSize = [canvas.clientWidth, canvas.clientHeight];
26 | const drawShader = this._drawShader;
27 | drawShader.use();
28 |
29 | const brushSize = [
30 | Parameters.brush.radius / canvasSize[0],
31 | Parameters.brush.radius / canvasSize[1]
32 | ];
33 | drawShader.u["uBrushSize"].value = brushSize;
34 | drawShader.u["uBrushPos"].value = Parameters.mouse.pos;
35 | drawShader.u["uThickness"].value = this.thickness / Parameters.brush.radius;
36 | drawShader.bindUniformsAndAttributes();
37 |
38 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
39 | }
40 | }
41 |
42 | export default Brush;
--------------------------------------------------------------------------------
/src/ts/fluid.ts:
--------------------------------------------------------------------------------
1 | import GLResource from "./gl-utils/gl-resource";
2 | import Shader from "./gl-utils/shader";
3 | import FBO from "./gl-utils/fbo";
4 | import ObstacleMap from "./obstacle-map";
5 | import * as Parameters from "./parameters";
6 | import * as FluidShaders from "./shaders/fluid-shaders";
7 |
8 | import "./page-interface-generated";
9 |
10 | class Fluid extends GLResource {
11 | private _width: number;
12 | private _height: number;
13 | private _FBO: FBO;
14 | private _velTextures: WebGLTexture[];
15 | private _tmpTexture: WebGLTexture;
16 | private _pressureTexture: WebGLTexture;
17 | private _divergenceTexture: WebGLTexture;
18 | private _currIndex: number;
19 | private _nbIterations: number;
20 | private _useFloatTextures: boolean;
21 |
22 | private _drawVelocityShader: Shader;
23 | private _drawPressureShader: Shader;
24 | private _addVelShader: Shader;
25 | private _advectShader: Shader;
26 | private _jacobiPressureShader: Shader;
27 | private _divergenceShader: Shader;
28 | private _substractGradientShader: Shader;
29 | private _obstaclesVelocityShader: Shader;
30 |
31 | public viscosity: number;
32 | public dx: number;
33 | public timestep: number;
34 | public colorIntensity: number;
35 | public color: boolean;
36 |
37 | constructor(gl: WebGLRenderingContext, width: number, height: number) {
38 | super(gl);
39 |
40 | this._useFloatTextures = false;
41 | this.viscosity = 0.0002;
42 | this.colorIntensity = 0.033;
43 | this.color = true;
44 |
45 | this.reset(width, height);
46 | }
47 |
48 | public freeGLResources(): void {
49 | if (this._FBO) {
50 | this._FBO.freeGLResources();
51 | }
52 |
53 | this.freeTextures();
54 | this.freeShaders();
55 | }
56 |
57 | private freeTextures(): void {
58 | const gl = super.gl();
59 |
60 | if (this._velTextures) {
61 | gl.deleteTexture(this._velTextures[0]);
62 | gl.deleteTexture(this._velTextures[1]);
63 | }
64 | gl.deleteTexture(this._tmpTexture);
65 | gl.deleteTexture(this._pressureTexture);
66 | gl.deleteTexture(this._divergenceTexture);
67 | }
68 |
69 | private freeShaders(): void {
70 | function freeShader(shader: Shader): void {
71 | if (shader) {
72 | shader.freeGLResources();
73 | }
74 | }
75 |
76 | freeShader(this._drawVelocityShader);
77 | freeShader(this._drawPressureShader);
78 | freeShader(this._addVelShader);
79 | freeShader(this._advectShader);
80 | freeShader(this._jacobiPressureShader);
81 | freeShader(this._divergenceShader);
82 | freeShader(this._substractGradientShader);
83 | freeShader(this._obstaclesVelocityShader);
84 | }
85 |
86 | public reset(width: number, height: number): void {
87 | this.freeGLResources();
88 |
89 | this._width = width;
90 | this._height = height;
91 | this.dx = 1 / Math.min(width, height);
92 |
93 | this._FBO = new FBO(super.gl(), width, height);
94 |
95 | this.initTextures();
96 | this.buildShaders();
97 |
98 | this._currIndex = 0;
99 | }
100 |
101 | public set useFloatTextures(bool: boolean) {
102 | if (bool !== this._useFloatTextures) {
103 | this._useFloatTextures = bool;
104 | this.reset(this._width, this._height);
105 | }
106 | }
107 |
108 | public set minNbIterations(value: number) {
109 | this._nbIterations = 2 * Math.ceil(value / 2) + 1;
110 | }
111 |
112 | public get texelSize(): number[] {
113 | return [1 / this._width, 1 / this._height];
114 | }
115 |
116 | public get velTexture(): WebGLTexture {
117 | return this._velTextures[this.currIndex];
118 | }
119 |
120 | public update(obstacleMap: ObstacleMap): void {
121 | const gl = super.gl();
122 | const dt = this.timestep;
123 |
124 | gl.clearColor(0.5, 0, 0.5, 0);
125 |
126 | if (Page.Canvas.isMouseDown()) {
127 | const canvas = gl.canvas as HTMLCanvasElement;
128 | const canvasSize = [canvas.clientWidth, canvas.clientHeight];
129 | const brushSize = [
130 | Parameters.brush.radius / canvasSize[0],
131 | Parameters.brush.radius / canvasSize[1]
132 | ];
133 | const pos = Parameters.mouse.pos;
134 | const vel = [
135 | Parameters.mouse.movement[0] * Parameters.brush.strength,
136 | Parameters.mouse.movement[1] * Parameters.brush.strength,
137 | ];
138 | this.addVel(pos, brushSize, vel);
139 | }
140 |
141 | this.advect(dt);
142 |
143 | this.obstaclesVelocity(obstacleMap);
144 |
145 | this.project(obstacleMap);
146 |
147 | this.obstaclesVelocity(obstacleMap);
148 | }
149 |
150 | public addVel(pos: number[], size: number[], vel: number[]): void {
151 | const gl = this.gl();
152 | const addVelShader = this._addVelShader;
153 | addVelShader.u["uVel"].value = this._velTextures[this.currIndex];
154 | addVelShader.u["uBrushPos"].value = pos;
155 | addVelShader.u["uBrushSize"].value = size;
156 | addVelShader.u["uAddVel"].value = vel;
157 |
158 | addVelShader.use();
159 |
160 | this._FBO.bind([this._velTextures[this.nextIndex]]);
161 | addVelShader.bindUniformsAndAttributes();
162 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
163 |
164 | this.switchBuffers();
165 | }
166 |
167 | public drawVelocity(): void {
168 | const gl = this.gl();
169 | const drawShader = this._drawVelocityShader;
170 | drawShader.u["uVel"].value = this._velTextures[this.currIndex];
171 | drawShader.u["uColorIntensity"].value = this.colorIntensity;
172 | drawShader.u["uBlacknWhite"].value = !this.color;
173 |
174 | drawShader.use();
175 | drawShader.bindUniformsAndAttributes();
176 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
177 | }
178 |
179 | public drawPressure(): void {
180 | const gl = this.gl();
181 | const drawPressureShader = this._drawPressureShader;
182 | drawPressureShader.u["uPressure"].value = this._pressureTexture;
183 | drawPressureShader.u["uColorIntensity"].value = this.colorIntensity;
184 | drawPressureShader.u["uBlacknWhite"].value = !this.color;
185 |
186 | drawPressureShader.use();
187 | drawPressureShader.bindUniformsAndAttributes();
188 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
189 | }
190 |
191 | private get currIndex(): number {
192 | return this._currIndex;
193 | }
194 |
195 | private get nextIndex(): number {
196 | return (this._currIndex + 1) % 2;
197 | }
198 |
199 | private switchBuffers(): void {
200 | this._currIndex = this.nextIndex;
201 | }
202 |
203 | private obstaclesVelocity(obstacleMap: ObstacleMap): void {
204 | const gl = this.gl();
205 | const obstacleVelocityShader = this._obstaclesVelocityShader;
206 |
207 | obstacleVelocityShader.u["uVelocities"].value = this._velTextures[this.currIndex];
208 | obstacleVelocityShader.u["uObstacles"].value = obstacleMap.texture;
209 | obstacleVelocityShader.u["uTexelSize"].value = this.texelSize;
210 |
211 | this._FBO.bind([this._velTextures[this.nextIndex]]);
212 |
213 | obstacleVelocityShader.use();
214 | obstacleVelocityShader.bindUniformsAndAttributes();
215 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
216 |
217 | this.switchBuffers();
218 | }
219 |
220 | private advect(dt: number): void {
221 | const gl = this.gl();
222 | const advectShader = this._advectShader;
223 |
224 | advectShader.u["uVelUnit"].value = [128 / this._width, 128 / this._height];
225 | advectShader.u["uDt"].value = dt;
226 | advectShader.u["uQuantity"].value = this._velTextures[this.currIndex];
227 | advectShader.u["uVel"].value = this._velTextures[this.currIndex];
228 |
229 | this._FBO.bind([this._velTextures[this.nextIndex]]);
230 |
231 | advectShader.use();
232 | advectShader.bindUniformsAndAttributes();
233 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
234 |
235 | this.switchBuffers();
236 | }
237 |
238 | private computeDivergence(): void {
239 | const gl = this.gl();
240 | const divergenceShader = this._divergenceShader
241 |
242 | divergenceShader.u["uTexelSize"].value = this.texelSize;
243 | divergenceShader.u["uVelocity"].value = this._velTextures[this.currIndex];
244 |
245 | this._FBO.bind([this._divergenceTexture]);
246 | gl.clear(gl.COLOR_BUFFER_BIT);
247 |
248 | divergenceShader.use();
249 | divergenceShader.bindUniformsAndAttributes();
250 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
251 | }
252 |
253 | private computePressure(obstacleMap: ObstacleMap): void {
254 | const gl = this.gl();
255 | const jacobiPressureShader = this._jacobiPressureShader
256 | const alpha = -.5 * this.dx;
257 | const beta = 4;
258 | const dst = this._pressureTexture;
259 | const cstTerm = this._divergenceTexture;
260 |
261 | jacobiPressureShader.u["uTexelSize"].value = this.texelSize;
262 | jacobiPressureShader.u["uAlpha"].value = alpha;
263 | jacobiPressureShader.u["uInvBeta"].value = 1 / beta;
264 | jacobiPressureShader.u["uConstantTerm"].value = cstTerm;
265 | jacobiPressureShader.u["uObstacles"].value = obstacleMap.texture;
266 |
267 | let index = 0;
268 | let textures = [this._tmpTexture, dst];
269 | this._FBO.bind([this._tmpTexture]);
270 | gl.clear(gl.COLOR_BUFFER_BIT);
271 |
272 | jacobiPressureShader.use();
273 | jacobiPressureShader.bindAttributes();
274 | for (let i = 0; i < this._nbIterations; ++i) { //nb iterations must be odd
275 | jacobiPressureShader.u["uPrevIter"].value = textures[index];
276 |
277 | this._FBO.bind([textures[(index + 1) % 2]]);
278 | jacobiPressureShader.bindUniforms();
279 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
280 |
281 | index = (index + 1) % 2;
282 | }
283 | }
284 |
285 | private substractPressureGradient(): void {
286 | const gl = this.gl();
287 | const substractGradientShader = this._substractGradientShader;
288 | substractGradientShader.u["uVelocities"].value = this._velTextures[this.currIndex];
289 | substractGradientShader.u["uPressure"].value = this._pressureTexture;
290 | substractGradientShader.u["uTexelSize"].value = this.texelSize;
291 | substractGradientShader.u["uHalfInvDx"].value = 0.5 / this.dx;
292 |
293 | this._FBO.bind([this._velTextures[this.nextIndex]]);
294 | substractGradientShader.use();
295 | substractGradientShader.bindUniformsAndAttributes();
296 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
297 |
298 | this.switchBuffers();
299 | }
300 |
301 | private project(obstacleMap: ObstacleMap): void {
302 | this.computeDivergence();
303 | this.computePressure(obstacleMap);
304 | this.substractPressureGradient();
305 | }
306 |
307 | private initTextures(): void {
308 | this.freeTextures();
309 | const gl = super.gl();
310 | const width = this._width;
311 | const height = this._height;
312 |
313 | let floatTexels: number[] = [];
314 | for (let i = 0; i < 4 * width * height; ++i) {
315 | floatTexels.push(0);
316 | }
317 | const floatData = new Float32Array(floatTexels);
318 |
319 | let uintTexels: number[] = [];
320 | for (let i = 0; i < 4 * width * height; ++i) {
321 | uintTexels.push(127);
322 | }
323 | const uintData = new Uint8Array(uintTexels);
324 |
325 | const velFormat = (this._useFloatTextures) ? gl.FLOAT : gl.UNSIGNED_BYTE;
326 | const velData = (this._useFloatTextures) ? floatData : uintData;
327 |
328 | let textures: WebGLTexture[] = [];
329 | for (let i = 0; i < 2; ++i) {
330 | let texture = gl.createTexture();
331 | gl.bindTexture(gl.TEXTURE_2D, texture);
332 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0,
333 | gl.RGBA, velFormat, velData);
334 | textures.push(texture);
335 | }
336 |
337 | for (let i = 0; i < 3; ++i) {
338 | let texture = gl.createTexture();
339 | gl.bindTexture(gl.TEXTURE_2D, texture);
340 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0,
341 | gl.RGBA, gl.UNSIGNED_BYTE, uintData);
342 | textures.push(texture);
343 | }
344 |
345 | for (let texture of textures) {
346 | gl.bindTexture(gl.TEXTURE_2D, texture);
347 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
348 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
349 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
350 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
351 | }
352 | gl.bindTexture(gl.TEXTURE_2D, null);
353 |
354 | this._velTextures = [textures[0], textures[1]];
355 | this._tmpTexture = textures[2];
356 | this._pressureTexture = textures[3];
357 | this._divergenceTexture = textures[4];
358 | }
359 |
360 | private buildShaders(): void {
361 | this.freeShaders();
362 | FluidShaders.setUseFloatTextures(this._useFloatTextures);
363 | const gl = super.gl();
364 |
365 | this._drawVelocityShader = FluidShaders.buildDrawVelocityShader(gl);
366 | this._drawPressureShader = FluidShaders.buildDrawPressureShader(gl);
367 | this._addVelShader = FluidShaders.buildAddVelShader(gl);
368 | this._advectShader = FluidShaders.buildAdvectShader(gl);
369 | this._jacobiPressureShader = FluidShaders.buildJacobiPressureShader(gl);
370 | this._divergenceShader = FluidShaders.buildDivergenceShader(gl);
371 | this._substractGradientShader = FluidShaders.buildSubstractGradientShader(gl);
372 | this._obstaclesVelocityShader = FluidShaders.buildObstaclesVelocityShader(gl);
373 | }
374 | }
375 |
376 | export default Fluid;
--------------------------------------------------------------------------------
/src/ts/gl-utils/fbo.ts:
--------------------------------------------------------------------------------
1 | import GLResource from "./gl-resource";
2 |
3 |
4 | class FBO extends GLResource {
5 | id: WebGLFramebuffer;
6 | width: number;
7 | height: number;
8 |
9 | constructor(gl: WebGLRenderingContext, width: number, height: number) {
10 | super(gl);
11 |
12 | this.id = gl.createFramebuffer();
13 | this.width = width;
14 | this.height = height;
15 | }
16 |
17 | public bind(colorBuffers: WebGLTexture[], depthBuffer: WebGLRenderbuffer = null): void {
18 | const gl = super.gl();
19 |
20 | gl.bindFramebuffer(gl.FRAMEBUFFER, this.id);
21 | gl.viewport(0, 0, this.width, this.height);
22 |
23 | for (let i = 0; i < colorBuffers.length; ++i) {
24 | gl.framebufferTexture2D(
25 | gl.FRAMEBUFFER, gl['COLOR_ATTACHMENT' + i], gl.TEXTURE_2D, colorBuffers[i], 0);
26 | }
27 |
28 | if (depthBuffer) {
29 | gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
30 | gl.framebufferRenderbuffer(
31 | gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);
32 | }
33 | }
34 |
35 | public static bindDefault(gl: WebGLRenderingContext): void {
36 | gl.bindFramebuffer(gl.FRAMEBUFFER, null);
37 | gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
38 | }
39 |
40 | public freeGLResources(): void {
41 | super.gl().deleteFramebuffer(this.id);
42 | this.id = null;
43 | }
44 | }
45 |
46 | export default FBO;
--------------------------------------------------------------------------------
/src/ts/gl-utils/gl-resource.ts:
--------------------------------------------------------------------------------
1 | abstract class GLResource {
2 | private _gl: WebGLRenderingContext;
3 |
4 | constructor(gl: WebGLRenderingContext) {
5 | this._gl = gl;
6 | }
7 |
8 | public gl(): WebGLRenderingContext {
9 | return this._gl;
10 | }
11 | public abstract freeGLResources(): void;
12 | }
13 |
14 | export default GLResource;
--------------------------------------------------------------------------------
/src/ts/gl-utils/shader.ts:
--------------------------------------------------------------------------------
1 | import GLResource from "./gl-resource";
2 | import VBO from "./vbo";
3 |
4 | function notImplemented(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: any): void {
5 | alert("NOT IMPLEMENTED YET");
6 | }
7 |
8 | function bindUniformFloat(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number): void;
9 | function bindUniformFloat(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void;
10 | function bindUniformFloat(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: any): void {
11 | if (Array.isArray(value)) {
12 | gl.uniform1fv(location, value);
13 | } else {
14 | gl.uniform1f(location, value);
15 | }
16 | }
17 |
18 | function bindUniformFloat2v(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void {
19 | gl.uniform2fv(location, value);
20 | }
21 |
22 | function bindUniformFloat3v(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void {
23 | gl.uniform3fv(location, value);
24 | }
25 |
26 | function bindUniformFloat4v(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void {
27 | gl.uniform4fv(location, value);
28 | }
29 |
30 | function bindUniformInt(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number): void;
31 | function bindUniformInt(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void;
32 | function bindUniformInt(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: any): void {
33 | if (Array.isArray(value)) {
34 | gl.uniform1iv(location, value);
35 | } else {
36 | gl.uniform1iv(location, value);
37 | }
38 | }
39 |
40 | function bindUniformInt2v(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void {
41 | gl.uniform2iv(location, value);
42 | }
43 |
44 | function bindUniformInt3v(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void {
45 | gl.uniform3iv(location, value);
46 | }
47 |
48 | function bindUniformInt4v(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void {
49 | gl.uniform4iv(location, value);
50 | }
51 |
52 | function bindUniformBool(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: boolean | number): void {
53 | gl.uniform1i(location, +value);
54 | }
55 |
56 | function bindUniformBool2v(gl: WebGLRenderingContext, location: WebGLUniformLocation, value): void {
57 | gl.uniform2iv(location, value);
58 | }
59 |
60 | function bindUniformBool3v(gl: WebGLRenderingContext, location: WebGLUniformLocation, value): void {
61 | gl.uniform3iv(location, value);
62 | }
63 |
64 | function bindUniformBool4v(gl: WebGLRenderingContext, location: WebGLUniformLocation, value): void {
65 | gl.uniform4iv(location, value);
66 | }
67 |
68 | function bindUniformFloatMat2(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void {
69 | gl.uniformMatrix2fv(location, false, value);
70 | }
71 |
72 | function bindUniformFloatMat3(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void {
73 | gl.uniformMatrix3fv(location, false, value);
74 | }
75 |
76 | function bindUniformFloatMat4(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void {
77 | gl.uniformMatrix4fv(location, false, value);
78 | }
79 |
80 | function bindSampler2D(gl: WebGLRenderingContext, location: WebGLUniformLocation, unitNb: number, value: WebGLTexture): void {
81 | gl.uniform1i(location, unitNb);
82 | gl.activeTexture(gl['TEXTURE' + unitNb]);
83 | gl.bindTexture(gl.TEXTURE_2D, value);
84 | }
85 |
86 | function bindSamplerCube(gl: WebGLRenderingContext, location: WebGLUniformLocation, unitNb: number, value: WebGLTexture): void {
87 | gl.uniform1i(location, unitNb);
88 | gl.activeTexture(gl['TEXTURE' + unitNb]);
89 | gl.bindTexture(gl.TEXTURE_CUBE_MAP, value);
90 | }
91 |
92 | interface Type {
93 | str: string;
94 | binder;//: (gl: WebGLRenderingContext, location: WebGLUniformLocation, value: any) => void;// |
95 | //((gl: WebGLRenderingContext, location: WebGLUniformLocation, unitNb: number, value: WebGLTexture) => void);
96 | };
97 |
98 | /* From WebGL spec:
99 | * http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.14 */
100 | const types = {
101 | 0x8B50: { str: 'FLOAT_VEC2', binder: bindUniformFloat2v },
102 | 0x8B51: { str: 'FLOAT_VEC3', binder: bindUniformFloat3v },
103 | 0x8B52: { str: 'FLOAT_VEC4', binder: bindUniformFloat4v },
104 | 0x8B53: { str: 'INT_VEC2', binder: bindUniformInt2v },
105 | 0x8B54: { str: 'INT_VEC3', binder: bindUniformInt3v },
106 | 0x8B55: { str: 'INT_VEC4', binder: bindUniformInt4v },
107 | 0x8B56: { str: 'BOOL', binder: bindUniformBool },
108 | 0x8B57: { str: 'BOOL_VEC2', binder: bindUniformBool2v },
109 | 0x8B58: { str: 'BOOL_VEC3', binder: bindUniformBool3v },
110 | 0x8B59: { str: 'BOOL_VEC4', binder: bindUniformBool4v },
111 | 0x8B5A: { str: 'FLOAT_MAT2', binder: bindUniformFloatMat2 },
112 | 0x8B5B: { str: 'FLOAT_MAT3', binder: bindUniformFloatMat3 },
113 | 0x8B5C: { str: 'FLOAT_MAT4', binder: bindUniformFloatMat4 },
114 | 0x8B5E: { str: 'SAMPLER_2D', binder: bindSampler2D },
115 | 0x8B60: { str: 'SAMPLER_CUBE', binder: bindSamplerCube },
116 | 0x1400: { str: 'BYTE', binder: notImplemented },
117 | 0x1401: { str: 'UNSIGNED_BYTE', binder: notImplemented },
118 | 0x1402: { str: 'SHORT', binder: notImplemented },
119 | 0x1403: { str: 'UNSIGNED_SHORT', binder: notImplemented },
120 | 0x1404: { str: 'INT', binder: bindUniformInt },
121 | 0x1405: { str: 'UNSIGNED_INT', binder: notImplemented },
122 | 0x1406: { str: 'FLOAT', binder: bindUniformFloat }
123 | };
124 |
125 | interface ShaderUniform {
126 | value: boolean | boolean[] | number | number[] | WebGLTexture | WebGLTexture[];
127 | loc: WebGLUniformLocation;
128 | size: number;
129 | type: number;
130 | }
131 |
132 | interface ShaderAttribute {
133 | VBO: VBO;
134 | loc: GLint;
135 | size: number;
136 | type: number;
137 | }
138 |
139 | class ShaderProgram extends GLResource {
140 | id: WebGLProgram;
141 | uCount: number;
142 | u: ShaderUniform[];
143 |
144 | aCount: number;
145 | a: ShaderAttribute[];
146 |
147 | constructor(gl: WebGLRenderingContext, vertexSource: string, fragmentSource: string) {
148 | function createShader(type: GLenum, source: string): WebGLShader {
149 | const shader = gl.createShader(type);
150 | gl.shaderSource(shader, source);
151 | gl.compileShader(shader);
152 |
153 | const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
154 | if (!success) {
155 | console.log(gl.getShaderInfoLog(shader));
156 | gl.deleteShader(shader);
157 | return null;
158 | }
159 |
160 | return shader;
161 | }
162 |
163 | super(gl);
164 |
165 | this.id = null;
166 | this.uCount = 0;
167 | this.aCount = 0;
168 |
169 | const vertexShader = createShader(gl.VERTEX_SHADER, vertexSource);
170 | const fragmentShader = createShader(gl.FRAGMENT_SHADER, fragmentSource);
171 |
172 | const id = gl.createProgram();
173 | gl.attachShader(id, vertexShader);
174 | gl.attachShader(id, fragmentShader);
175 | gl.linkProgram(id);
176 |
177 | const success = gl.getProgramParameter(id, gl.LINK_STATUS);
178 | if (!success) {
179 | console.log(gl.getProgramInfoLog(id));
180 | gl.deleteProgram(id);
181 | } else {
182 | this.id = id;
183 |
184 | this.introspection();
185 | }
186 | }
187 |
188 | public freeGLResources(): void {
189 | super.gl().deleteProgram(this.id);
190 | this.id = null;
191 | }
192 |
193 | private introspection(): void {
194 | const gl = super.gl();
195 |
196 | this.uCount = gl.getProgramParameter(this.id, gl.ACTIVE_UNIFORMS);
197 | this.u = [];
198 | for (let i = 0; i < this.uCount; ++i) {
199 | const uniform = gl.getActiveUniform(this.id, i);
200 | const name = uniform.name;
201 |
202 | this.u[name] = {
203 | value: null,
204 | loc: gl.getUniformLocation(this.id, name),
205 | size: uniform.size,
206 | type: uniform.type,
207 | };
208 | }
209 |
210 | this.aCount = gl.getProgramParameter(this.id, gl.ACTIVE_ATTRIBUTES);
211 | this.a = [];
212 | for (let i = 0; i < this.aCount; ++i) {
213 | const attribute = gl.getActiveAttrib(this.id, i);
214 | const name = attribute.name;
215 |
216 | this.a[name] = {
217 | VBO: null,
218 | loc: gl.getAttribLocation(this.id, name),
219 | size: attribute.size,
220 | type: attribute.type,
221 | };
222 | }
223 | }
224 |
225 | public use(): void {
226 | super.gl().useProgram(this.id);
227 | }
228 |
229 | public bindUniforms(): void {
230 | const gl: WebGLRenderingContext = super.gl();
231 | let currTextureUnitNb: number = 0;
232 |
233 | for (let uName in this.u) {
234 | const uniform = this.u[uName];
235 | if (uniform.value !== null) {
236 | if (uniform.type === 0x8B5E || uniform.type === 0x8B60) {
237 | const unitNb: number = currTextureUnitNb;
238 | types[uniform.type].binder(gl, uniform.loc, unitNb, uniform.value);
239 | currTextureUnitNb++;
240 | } else {
241 | types[uniform.type].binder(gl, uniform.loc, uniform.value);
242 | }
243 | }
244 | }
245 | }
246 |
247 | public bindAttributes(): void {
248 | for (let aName in this.a) {
249 | const attribute = this.a[aName];
250 | if (attribute.VBO !== null) {
251 | attribute.VBO.bind(attribute.loc);
252 | }
253 | }
254 | }
255 |
256 | public bindUniformsAndAttributes(): void {
257 | this.bindUniforms();
258 | this.bindAttributes();
259 | }
260 | }
261 |
262 | export default ShaderProgram;
--------------------------------------------------------------------------------
/src/ts/gl-utils/utils.ts:
--------------------------------------------------------------------------------
1 | function resizeCanvas(gl: WebGLRenderingContext, hidpi: boolean = false): void {
2 | const cssPixel: number = (hidpi) ? window.devicePixelRatio : 1;
3 | const canvas = gl.canvas as HTMLCanvasElement;
4 |
5 | const width: number = Math.floor(canvas.clientWidth * cssPixel);
6 | const height: number = Math.floor(canvas.clientHeight * cssPixel);
7 | if (canvas.width != width || canvas.height != height) {
8 | canvas.width = width;
9 | canvas.height = height;
10 | }
11 | }
12 |
13 | export { resizeCanvas };
--------------------------------------------------------------------------------
/src/ts/gl-utils/vbo.ts:
--------------------------------------------------------------------------------
1 | import GLResource from "./gl-resource";
2 |
3 | class VBO extends GLResource {
4 | private id: WebGLBuffer;
5 | private size: number;
6 | private type: GLenum;
7 | private normalize: GLboolean;
8 | private stride: GLsizei;
9 | private offset: GLintptr;
10 |
11 | constructor(gl: WebGLRenderingContext, array: ArrayBufferView, size: number, type: GLenum) {
12 | super(gl);
13 |
14 | this.id = gl.createBuffer();
15 | gl.bindBuffer(gl.ARRAY_BUFFER, this.id);
16 | gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
17 | gl.bindBuffer(gl.ARRAY_BUFFER, null);
18 |
19 | this.size = size;
20 | this.type = type;
21 | this.normalize = false;
22 | this.stride = 0;
23 | this.offset = 0;
24 | }
25 |
26 | public freeGLResources(): void {
27 | this.gl().deleteBuffer(this.id);
28 | this.id = null;
29 | }
30 |
31 | public static createQuad(gl: WebGLRenderingContext, minX: number, minY: number, maxX: number, maxY: number): VBO {
32 | let vert = [
33 | minX, minY,
34 | maxX, minY,
35 | minX, maxY,
36 | maxX, maxY,
37 | ];
38 |
39 | return new VBO(gl, new Float32Array(vert), 2, gl.FLOAT);
40 | }
41 |
42 | public bind(location: GLuint): void {
43 | const gl = super.gl();
44 |
45 | gl.enableVertexAttribArray(location);
46 | gl.bindBuffer(gl.ARRAY_BUFFER, this.id);
47 | gl.vertexAttribPointer(location, this.size, this.type, this.normalize, this.stride, this.offset);
48 | }
49 | };
50 |
51 | export default VBO;
--------------------------------------------------------------------------------
/src/ts/main.ts:
--------------------------------------------------------------------------------
1 | import * as Utils from "./gl-utils/utils";
2 | import FBO from "./gl-utils/fbo";
3 | import * as Parameters from "./parameters";
4 | import Brush from "./brush";
5 | import ObstacleMap from "./obstacle-map";
6 | import Fluid from "./fluid";
7 | import * as Requirements from "./requirements";
8 |
9 | import "./page-interface-generated";
10 |
11 | /** Initializes a WebGL context */
12 | function initGL(canvas: HTMLCanvasElement, flags: any): WebGLRenderingContext {
13 | function setError(message: string) {
14 | Page.Demopage.setErrorMessage("webgl-support", message);
15 | }
16 |
17 | let gl: WebGLRenderingContext = canvas.getContext("webgl", flags) as WebGLRenderingContext;
18 | if (!gl) {
19 | gl = canvas.getContext("experimental-webgl", flags) as WebGLRenderingContext;
20 | if (!gl) {
21 | setError("Your browser or device does not seem to support WebGL.");
22 | return null;
23 | }
24 | setError("Your browser or device only supports experimental WebGL.\n" +
25 | "The simulation may not run as expected.");
26 | }
27 |
28 | if (gl) {
29 | canvas.style.cursor = "none";
30 | gl.disable(gl.CULL_FACE);
31 | gl.disable(gl.DEPTH_TEST);
32 | gl.disable(gl.BLEND);
33 | gl.clearColor(0.0, 0.0, 0.0, 1.0);
34 |
35 | Utils.resizeCanvas(gl, false);
36 | }
37 |
38 | return gl;
39 | }
40 |
41 | function main() {
42 | const canvas: HTMLCanvasElement = Page.Canvas.getCanvas();
43 | const gl: WebGLRenderingContext = initGL(canvas, { alpha: false });
44 | if (!gl || !Requirements.check(gl))
45 | return;
46 |
47 | const extensions: string[] = [
48 | "OES_texture_float",
49 | "WEBGL_color_buffer_float",
50 | "OES_texture_float_linear",
51 | ];
52 | Requirements.loadExtensions(gl, extensions);
53 |
54 | const size = 256;
55 |
56 | const fluid = new Fluid(gl, size, size);
57 | const brush = new Brush(gl);
58 | const obstacleMaps: ObstacleMap[] = [];
59 | obstacleMaps["none"] = new ObstacleMap(gl, size, size);
60 | obstacleMaps["one"] = new ObstacleMap(gl, size, size);
61 | {
62 | obstacleMaps["one"].addObstacle([0.015, 0.015], [0.3, 0.5]);
63 | }
64 | obstacleMaps["many"] = new ObstacleMap(gl, size, size);
65 | {
66 | let size = [0.012, 0.012];
67 | for (let iX = 0; iX < 5; ++iX) {
68 | for (let iY = -iX / 2; iY <= iX / 2; ++iY) {
69 | size = [size[0] + 0.0005, size[1] + 0.0005];
70 | const pos = [0.3 + iX * 0.07, 0.5 + iY * 0.08];
71 | obstacleMaps["many"].addObstacle(size, pos);
72 | }
73 | }
74 | }
75 |
76 | Parameters.bind(fluid);
77 |
78 | /* Update the FPS indicator every second. */
79 | let instantFPS: number = 0;
80 | const updateFpsText = function () {
81 | Page.Canvas.setIndicatorText("fps", instantFPS.toFixed(0));
82 | };
83 | setInterval(updateFpsText, 1000);
84 |
85 | let lastUpdate = 0;
86 | function mainLoop(time: number) {
87 | time *= 0.001; //dt is now in seconds
88 | let dt = time - lastUpdate;
89 | instantFPS = 1 / dt;
90 | lastUpdate = time;
91 |
92 | /* If the javascript was paused (tab lost focus), the dt may be too big.
93 | * In that case we adjust it so the simulation resumes correctly. */
94 | dt = Math.min(dt, 1 / 10);
95 |
96 | const obstacleMap: ObstacleMap = obstacleMaps[Parameters.obstacles];
97 |
98 | /* Updating */
99 | if (Parameters.fluid.stream) {
100 | fluid.addVel([0.1, 0.5], [0.05, 0.2], [0.4, 0]);
101 | }
102 | fluid.update(obstacleMap);
103 |
104 | /* Drawing */
105 | FBO.bindDefault(gl);
106 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
107 |
108 | if (Parameters.display.velocity) {
109 | fluid.drawVelocity();
110 | } else if (Parameters.display.pressure) {
111 | fluid.drawPressure();
112 | }
113 |
114 | if (Parameters.display.brush) {
115 | brush.draw();
116 | }
117 |
118 | if (Parameters.display.obstacles) {
119 | obstacleMap.draw();
120 | }
121 |
122 | if (Parameters.display.velocity && Parameters.display.pressure) {
123 | gl.viewport(10, 10, 128, 128);
124 | fluid.drawPressure();
125 | }
126 |
127 | requestAnimationFrame(mainLoop);
128 | }
129 |
130 | requestAnimationFrame(mainLoop);
131 | }
132 |
133 | main();
--------------------------------------------------------------------------------
/src/ts/obstacle-map.ts:
--------------------------------------------------------------------------------
1 | import GLResource from "./gl-utils/gl-resource";
2 | import Shader from "./gl-utils/shader";
3 | import FBO from "./gl-utils/fbo";
4 | import * as ObstacleMapShaders from "./shaders/obstacle-map-shaders";
5 |
6 | class ObstacleMap extends GLResource {
7 | private _width: number;
8 | private _height: number;
9 |
10 | private _fbo: FBO;
11 | private _texture: WebGLTexture;
12 | private _initTexture: WebGLTexture;
13 |
14 | private _drawShader: Shader;
15 | private _addShader: Shader;
16 |
17 | constructor(gl: WebGLRenderingContext, width: number, height: number) {
18 | super(gl);
19 |
20 | this._width = width;
21 | this._height = height;
22 |
23 | this._fbo = new FBO(gl, width, height);
24 |
25 | this._drawShader = ObstacleMapShaders.buildDrawShader(gl);
26 | this._addShader = ObstacleMapShaders.buildAddShader(gl);
27 |
28 | this.initObstaclesMap();
29 | }
30 |
31 | public freeGLResources(): void {
32 | const gl = super.gl();
33 |
34 | this._fbo.freeGLResources();
35 | this._fbo = null;
36 |
37 | gl.deleteTexture(this._texture);
38 | gl.deleteTexture(this._initTexture);
39 |
40 | this._drawShader.freeGLResources();
41 | this._addShader.freeGLResources();
42 | this._drawShader = null;
43 | this._addShader = null;
44 | }
45 |
46 | public get texture(): WebGLTexture {
47 | return this._texture;
48 | }
49 |
50 | public draw(): void {
51 | const gl = super.gl();
52 | const drawShader = this._drawShader;
53 |
54 | drawShader.u["uObstacles"].value = this.texture;
55 | drawShader.use();
56 | drawShader.bindUniformsAndAttributes();
57 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
58 | }
59 |
60 | public addObstacle(pos, size): void {
61 | const gl = super.gl();
62 | const addShader = this._addShader;
63 | addShader.u["uSize"].value = pos;
64 | addShader.u["uPos"].value = size;
65 |
66 | this._fbo.bind([this._texture]);
67 | addShader.use();
68 | addShader.bindUniformsAndAttributes();
69 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
70 | }
71 |
72 | private initObstaclesMap(): void {
73 | const gl = super.gl();
74 | const width = this._width;
75 | const height = this._height;
76 |
77 | let texels: number[] = [];
78 | for (let iY = 0; iY < height; ++iY) {
79 | for (let iX = 0; iX < width; ++iX) {
80 | if (iY === 0) {
81 | texels.push.apply(texels, [127, 255, 0, 255]);
82 | } else if (iY === height - 1) {
83 | texels.push.apply(texels, [127, 0, 0, 255]);
84 | } else if (iX === 0) {
85 | texels.push.apply(texels, [255, 127, 0, 255]);
86 | } else if (iX === width - 1) {
87 | texels.push.apply(texels, [0, 127, 0, 255]);
88 | } else {
89 | texels.push.apply(texels, [127, 127, 0, 255]);
90 | }
91 | }
92 | }
93 | const data = new Uint8Array(texels);
94 |
95 | const textures: WebGLTexture[] = [];
96 | for (let i = 0; i < 2; ++i) {
97 | const tex = gl.createTexture();
98 | gl.bindTexture(gl.TEXTURE_2D, tex);
99 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0,
100 | gl.RGBA, gl.UNSIGNED_BYTE, data);
101 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
102 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
103 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
104 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
105 | textures.push(tex);
106 | }
107 | gl.bindTexture(gl.TEXTURE_2D, null);
108 |
109 | this._texture = textures[0];
110 | this._initTexture = textures[1];
111 | }
112 | }
113 |
114 | export default ObstacleMap;
--------------------------------------------------------------------------------
/src/ts/parameters.ts:
--------------------------------------------------------------------------------
1 | import Fluid from "./fluid";
2 | import * as Requirements from "./requirements";
3 |
4 | import "./page-interface-generated";
5 |
6 | class Mouse {
7 | private _posInPx: number[];
8 | private _pos: number[];
9 |
10 | private _movementInPx: number[];
11 | private _movement: number[];
12 |
13 | private _pivotInPx: number[];
14 |
15 | constructor() {
16 | this._posInPx = [0, 0];
17 | this._pivotInPx = [0, 0];
18 | this.setPosInPx([0, 0]);
19 | this.setMovementInPx([0, 0]);
20 |
21 | Page.Canvas.Observers.mouseMove.push((relX: number, relY: number) => {
22 | const canvasSize = Page.Canvas.getSize();
23 | this.setPosInPx([canvasSize[0] * relX, canvasSize[1] * (1 - relY)]);
24 | });
25 |
26 | Page.Canvas.Observers.mouseDown.push(() => {
27 | this.setMovementInPx([0, 0]);
28 | this._pivotInPx = this._posInPx;
29 | });
30 | }
31 |
32 | public get posInPx(): number[] {
33 | return this._posInPx;
34 | }
35 |
36 | public get pos(): number[] {
37 | return this._pos;
38 | }
39 |
40 | public get movementInPx(): number[] {
41 | return this._movementInPx;
42 | }
43 |
44 | public get movement(): number[] {
45 | return this._movement;
46 | }
47 |
48 | private setPosInPx(pos: number[]): void {
49 | const toPivot: number[] = [
50 | this._pivotInPx[0] - pos[0],
51 | this._pivotInPx[1] - pos[1]
52 | ];
53 | const distToPivot = Math.sqrt(toPivot[0] * toPivot[0] + toPivot[1] * toPivot[1]);
54 | const maxDist = 16;
55 |
56 | if (distToPivot > maxDist) {
57 | toPivot[0] *= maxDist / distToPivot;
58 | toPivot[1] *= maxDist / distToPivot;
59 |
60 | this._pivotInPx[0] = pos[0] + toPivot[0];
61 | this._pivotInPx[1] = pos[1] + toPivot[1];
62 | }
63 | const movementInPx = [-toPivot[0] / maxDist, -toPivot[1] / maxDist];
64 | this.setMovementInPx(movementInPx);
65 | this._posInPx = pos;
66 | this._pos = this.setRelative(pos);
67 | }
68 |
69 | private setMovementInPx(movement: number[]): void {
70 | this._movementInPx = movement;
71 | this._movement = this.setRelative(movement);
72 | }
73 |
74 | private setRelative(pos: number[]): number[] {
75 | const canvasSize = Page.Canvas.getSize();
76 | return [
77 | pos[0] / canvasSize[0],
78 | pos[1] / canvasSize[1],
79 | ];
80 | }
81 | }
82 |
83 | let mouse: Mouse = new Mouse();
84 |
85 | function bindMouse(): void {
86 | mouse = new Mouse();
87 | }
88 |
89 | interface BrushInfo {
90 | radius: number,
91 | strength: number,
92 | }
93 | const brushInfo: BrushInfo = {
94 | radius: 10,
95 | strength: 100,
96 | }
97 |
98 | interface FluidInfo {
99 | stream: boolean;
100 | }
101 | const fluidInfo: FluidInfo = {
102 | stream: true,
103 | }
104 |
105 | enum ObstaclesInfo {
106 | NONE = "none",
107 | ONE = "one",
108 | MANY = "many",
109 | }
110 | let obstaclesInfo: ObstaclesInfo = ObstaclesInfo.NONE;
111 |
112 | interface DisplayInfo {
113 | velocity: boolean,
114 | pressure: boolean,
115 | brush: boolean,
116 | obstacles: boolean,
117 | }
118 | const displayInfo: DisplayInfo = {
119 | velocity: true,
120 | pressure: true,
121 | brush: true,
122 | obstacles: true,
123 | }
124 |
125 | function bindControls(fluid: Fluid): void {
126 | {
127 | const RESOLUTIONS_CONTROL_ID = "resolution";
128 | const updateResolution = (values: string[]) => {
129 | const size: number = +values[0];
130 | fluid.reset(size, size);
131 | };
132 | Page.Tabs.addObserver(RESOLUTIONS_CONTROL_ID, updateResolution);
133 | updateResolution(Page.Tabs.getValues(RESOLUTIONS_CONTROL_ID));
134 | }
135 | {
136 | const FLOAT_CONTROL_ID = "float-texture-checkbox-id";
137 | if (!Requirements.allExtensionsLoaded) {
138 | Page.Controls.setVisibility(FLOAT_CONTROL_ID, false);
139 | Page.Checkbox.setChecked(FLOAT_CONTROL_ID, false);
140 | }
141 | const updateFloat = (use: boolean) => { fluid.useFloatTextures = use; };
142 | Page.Checkbox.addObserver(FLOAT_CONTROL_ID, updateFloat);
143 | updateFloat(Page.Checkbox.isChecked(FLOAT_CONTROL_ID));
144 | }
145 | {
146 | const ITERATIONS_CONTROL_ID = "solver-steps-range-id";
147 | const updateIterations = (iterations: number) => { fluid.minNbIterations = iterations; };
148 | Page.Range.addObserver(ITERATIONS_CONTROL_ID, updateIterations);
149 | updateIterations(Page.Range.getValue(ITERATIONS_CONTROL_ID));
150 | }
151 | {
152 | const TIMESTEP_CONTROL_ID = "timestep-range-id";
153 | const updateTimestep = (timestep: number) => { fluid.timestep = timestep; };
154 | Page.Range.addObserver(TIMESTEP_CONTROL_ID, updateTimestep);
155 | updateTimestep(Page.Range.getValue(TIMESTEP_CONTROL_ID));
156 | }
157 | {
158 | const STREAM_CONTROL_ID = "stream-checkbox-id";
159 | const updateStream = (doStream: boolean) => { fluidInfo.stream = doStream; };
160 | Page.Checkbox.addObserver(STREAM_CONTROL_ID, updateStream);
161 | updateStream(Page.Checkbox.isChecked(STREAM_CONTROL_ID));
162 | }
163 | {
164 | const OBSTACLES_CONTROL_ID = "obstacles";
165 | const updateObstacles = (values: string[]) => {
166 | obstaclesInfo = values[0] as ObstaclesInfo;
167 |
168 | };
169 | Page.Tabs.addObserver(OBSTACLES_CONTROL_ID, updateObstacles);
170 | updateObstacles(Page.Tabs.getValues(OBSTACLES_CONTROL_ID));
171 | }
172 |
173 | {
174 | const BRUSH_RADIUS_CONTROL_ID = "brush-radius-range-id";
175 | const updateBrushRadius = (radius: number) => { brushInfo.radius = radius; };
176 | Page.Range.addObserver(BRUSH_RADIUS_CONTROL_ID, updateBrushRadius);
177 | updateBrushRadius(Page.Range.getValue(BRUSH_RADIUS_CONTROL_ID));
178 |
179 | Page.Canvas.Observers.mouseWheel.push((delta: number) => {
180 | Page.Range.setValue(BRUSH_RADIUS_CONTROL_ID, brushInfo.radius + 5 * delta);
181 | updateBrushRadius(Page.Range.getValue(BRUSH_RADIUS_CONTROL_ID));
182 | });
183 | }
184 | {
185 | const BRUSH_STRENGTH_CONTROL_ID = "brush-strength-range-id";
186 | const updateBrushStrength = (strength: number) => { brushInfo.strength = strength; };
187 | Page.Range.addObserver(BRUSH_STRENGTH_CONTROL_ID, updateBrushStrength);
188 | updateBrushStrength(Page.Range.getValue(BRUSH_STRENGTH_CONTROL_ID));
189 | }
190 |
191 | {
192 | const DISPLAY_MODE_CONTROL_ID = "displayed-fields";
193 | const updateDisplayMode = (modes: string[]) => {
194 | displayInfo.velocity = modes[0] === "velocity" || modes[1] === "velocity";
195 | displayInfo.pressure = modes[0] === "pressure" || modes[1] === "pressure";
196 | };
197 | Page.Tabs.addObserver(DISPLAY_MODE_CONTROL_ID, updateDisplayMode);
198 | updateDisplayMode(Page.Tabs.getValues(DISPLAY_MODE_CONTROL_ID));
199 | }
200 | {
201 | const COLOR_INTENSITY_CONTROL_ID = "intensity-range-id";
202 | const updateColorIntensity = (intensity: number) => { fluid.colorIntensity = intensity; };
203 | Page.Range.addObserver(COLOR_INTENSITY_CONTROL_ID, updateColorIntensity);
204 | updateColorIntensity(Page.Range.getValue(COLOR_INTENSITY_CONTROL_ID));
205 | }
206 | {
207 | const DISPLAY_COLOR_CONTROL_ID = "display-color-checkbox-id";
208 | const updateColor = (display: boolean) => { fluid.color = display; };
209 | Page.Checkbox.addObserver(DISPLAY_COLOR_CONTROL_ID, updateColor);
210 | updateColor(Page.Checkbox.isChecked(DISPLAY_COLOR_CONTROL_ID));
211 | }
212 | {
213 | const DISPLAY_OBSTACLES_CONTROL_ID = "display-obstacles-checkbox-id";
214 | const updateDisplayObstacles = (display: boolean) => { displayInfo.obstacles = display; };
215 | Page.Checkbox.addObserver(DISPLAY_OBSTACLES_CONTROL_ID, updateDisplayObstacles);
216 | updateDisplayObstacles(Page.Checkbox.isChecked(DISPLAY_OBSTACLES_CONTROL_ID));
217 | }
218 | }
219 |
220 | function bind(fluid: Fluid): void {
221 | bindControls(fluid);
222 | bindMouse();
223 | }
224 |
225 | export {
226 | mouse,
227 | bind,
228 | brushInfo as brush,
229 | displayInfo as display,
230 | obstaclesInfo as obstacles,
231 | fluidInfo as fluid
232 | };
--------------------------------------------------------------------------------
/src/ts/requirements.ts:
--------------------------------------------------------------------------------
1 | import "./page-interface-generated";
2 |
3 | function check(gl: WebGLRenderingContext): boolean {
4 | // const vertexUnits = gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS);
5 | // const fragmentUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
6 | // if (vertexUnits < 2 || fragmentUnits < 3) {
7 | // alert("Your device does not meet the requirements for this simulation.");
8 | // return false;
9 | // }
10 |
11 | function setError(message: string) {
12 | Page.Demopage.setErrorMessage("webgl-requirements", message);
13 | }
14 |
15 | const mediump = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT);
16 | if (mediump.precision < 23) {
17 | Page.Demopage.setErrorMessage("webgl-requirements", "Your device only supports low precision float in fragment shader.\n" +
18 | "The simulation will not run.");
19 | return false;
20 | }
21 |
22 | return true;
23 | }
24 |
25 | let allExtensionsLoaded: boolean = false;
26 |
27 | function loadExtensions(gl: WebGLRenderingContext, extensions: string[]) {
28 | allExtensionsLoaded = true;
29 |
30 | let i = 0;
31 | for (let ext of extensions) {
32 | if (!gl.getExtension(ext)) {
33 | Page.Demopage.setErrorMessage("no-ext" + i, "Cannot load WebGL extension '" + ext + "'.");
34 | allExtensionsLoaded = false;
35 | }
36 | ++i;
37 | }
38 | }
39 |
40 |
41 | export { check, loadExtensions, allExtensionsLoaded };
--------------------------------------------------------------------------------
/src/ts/shaders/brush-shaders.ts:
--------------------------------------------------------------------------------
1 | import Shader from "../gl-utils/shader";
2 | import VBO from "../gl-utils/vbo";
3 | import { ShaderSrc } from "./build-shaders";
4 |
5 | const drawVert =
6 | `uniform vec2 uBrushSize; //relative, in [0,1]x[0,1]
7 | uniform vec2 uBrushPos; //relative, in [0,1]x[0,1]
8 |
9 | attribute vec2 aCorner; //in {-1,+1}x{-1,+1}
10 |
11 | varying vec2 toCenter;
12 |
13 | void main(void) {
14 | toCenter = -aCorner;
15 |
16 | vec2 pos = uBrushPos + aCorner * uBrushSize;
17 |
18 | gl_Position = vec4(2.0 * pos - 1.0, 0, 1);
19 | }`;
20 |
21 | const drawFrag =
22 | `precision mediump float;
23 |
24 | uniform float uThickness; //relative to brushRadius
25 |
26 | varying vec2 toCenter;
27 |
28 | void main(void) {
29 | float dist = length(toCenter);
30 | if (dist < 1.0-uThickness || dist > 1.0)
31 | discard;
32 |
33 | const vec3 color = vec3(1);
34 |
35 | gl_FragColor = vec4(color, 1.0);
36 | }`;
37 |
38 |
39 | const drawSrc = new ShaderSrc(drawVert, drawFrag);
40 |
41 | function buildDrawShader(gl: WebGLRenderingContext): Shader {
42 | const shader = new Shader(gl, drawSrc.vert, drawSrc.frag);
43 |
44 | shader.a["aCorner"].VBO = VBO.createQuad(gl, -1, -1, +1, +1);
45 | return shader;
46 | }
47 |
48 | export { buildDrawShader };
--------------------------------------------------------------------------------
/src/ts/shaders/build-shaders.ts:
--------------------------------------------------------------------------------
1 | import Shader from "../gl-utils/shader";
2 |
3 | function fetch(filepath: string): string {
4 | const request = new XMLHttpRequest();
5 | request.open('GET', filepath, false);
6 | request.send();
7 | return request.responseText;
8 | }
9 |
10 | interface Replace {
11 | toReplace: string;
12 | replacement: string;
13 | };
14 |
15 | class ShaderSrc {
16 | public vert: string;
17 | public frag: string;
18 |
19 | constructor(vert: string, frag: string) {
20 | this.vert = vert;
21 | this.frag = frag;
22 | }
23 |
24 | public batchReplace(includes: Replace[]): void {
25 | for (let include of includes) {
26 | this.vert = this.vert.replace(new RegExp(include.toReplace), include.replacement);
27 | this.frag = this.frag.replace(new RegExp(include.toReplace), include.replacement);
28 | }
29 | }
30 |
31 | static fromScript(vertId: string, fragId: string): ShaderSrc {
32 | const vert = (document.getElementById(vertId) as HTMLScriptElement).text;
33 | const frag = (document.getElementById(fragId) as HTMLScriptElement).text;
34 | return new ShaderSrc(vert, frag);
35 | }
36 | }
37 |
38 |
39 | export { fetch, Replace, ShaderSrc};
--------------------------------------------------------------------------------
/src/ts/shaders/fluid-shaders.ts:
--------------------------------------------------------------------------------
1 | import Shader from "../gl-utils/shader";
2 | import VBO from "../gl-utils/vbo";
3 | import { ShaderSrc } from "./build-shaders";
4 | import { encodingStr as encodingObstaclesStr } from "./obstacle-map-shaders";
5 |
6 | const rawEncodingStr =
7 | `const float MAX_SPEED = 1.0;
8 | const float SPEED_BANDWIDTH = 2.01 * MAX_SPEED;
9 |
10 | const float MIN_DIVERGENCE = -4.0 * MAX_SPEED;
11 | const float MAX_DIVERGENCE = 4.0 * MAX_SPEED;
12 | const float DIVERGENCE_BANDWIDTH = MAX_DIVERGENCE - MIN_DIVERGENCE;
13 |
14 | const float MIN_PRESSURE = 0.5 * MIN_DIVERGENCE;
15 | const float MAX_PRESSURE = 0.5 * MAX_DIVERGENCE;
16 | const float PRESSURE_BANDWIDTH = MAX_PRESSURE - MIN_PRESSURE;
17 |
18 | /* Decodes a float value (32 bits in [0,1])
19 | * from a 4D value (4x8bits in [0,1]x[0,1]x[0,1]x[0,1]) */
20 | float decode32bit(vec4 v)
21 | {
22 | const vec4 weights = 255.0 * vec4(256.0*256.0*256.0, 256.0*256.0, 256.0, 1.0) / (256.0*256.0*256.0*256.0 - 1.0);
23 | return dot(weights, v);
24 | }
25 |
26 | /* Encodes a float value (32 bits in [0,1])
27 | * into a 4D value (4x8bits in [0,1]x[0,1]x[0,1]x[0,1]) */
28 | vec4 encode32bit(float f)
29 | {
30 | const vec4 base = (256.0*256.0*256.0*256.0 - 1.0) / vec4(256.0*256.0*256.0, 256.0*256.0, 256.0, 1.0);
31 | return floor(mod(f * base, 256.0)) / 255.0;
32 | }
33 |
34 | /* Decodes a float value (16 bits in [0,1])
35 | * from a 2D value (2x8bits in [0,1]x[0,1]) */
36 | float decode16bit(vec2 v)
37 | {
38 | const vec2 weights = 255.0 * vec2(256.0, 1.0) / (256.0*256.0 - 1.0);
39 | return dot(weights, v);
40 | }
41 |
42 | /* Encodes a float value (16 bits in [0,1])
43 | * into a 2D value (2x8bits in [0,1]x[0,1]) */
44 | vec2 encode16bit(float f)
45 | {
46 | const vec2 base = (256.0*256.0 - 1.0) / vec2(256.0, 1.0);
47 | return floor(mod(f * base, 256.0)) / 255.0;
48 | }
49 |
50 | float decodeDivergence(vec4 texel) {
51 | float div = decode32bit(texel);
52 | return div * DIVERGENCE_BANDWIDTH + MIN_DIVERGENCE;
53 | }
54 | vec4 encodeDivergence(float div) {
55 | div = (div - MIN_DIVERGENCE) / DIVERGENCE_BANDWIDTH;
56 | return encode32bit(div);
57 | }
58 |
59 | float decodePressure(vec4 texel) {
60 | float p = decode32bit(texel);
61 | return p * PRESSURE_BANDWIDTH + MIN_PRESSURE;
62 | }
63 | vec4 encodePressure(float p) {
64 | p = (p - MIN_PRESSURE) / PRESSURE_BANDWIDTH;
65 | return encode32bit(p);
66 | }
67 |
68 | ___ENCODING_VELOCITY___
69 |
70 | ___ENCODING_OBSTACLES___`;
71 |
72 | const encodingVelocityFloatStr =
73 | `vec4 encodeVelocity(vec2 vel) {
74 | return vec4(vel, 0, 0);
75 | }
76 | vec2 decodeVelocity(vec4 texel) {
77 | return texel.rg;
78 | }`;
79 |
80 | const encodingVelocityNoFloatStr =
81 | `vec4 encodeVelocity(vec2 vel) {
82 | vel = 0.5 * (vel / MAX_SPEED + 1.0);
83 | return vec4(encode16bit(vel.x), encode16bit(vel.y));
84 | }
85 | vec2 decodeVelocity(vec4 texel) {
86 | vec2 vel = vec2(decode16bit(texel.rg), decode16bit(texel.ba));
87 | return (2.0 * vel - 1.0) * MAX_SPEED;
88 | }`;
89 |
90 | const addVelVert =
91 | `uniform vec2 uBrushSize; //relative, in [0,1]x[0,1]
92 | uniform vec2 uBrushPos; //relative, in [0,1]x[0,1]
93 |
94 | attribute vec2 aCorner; //{0,1}x{0,1}
95 |
96 | varying vec2 sampleCoords;
97 | varying vec2 toBrush;
98 |
99 | void main(void) {
100 | sampleCoords = aCorner;
101 | toBrush = (uBrushPos - aCorner) / uBrushSize;
102 |
103 | gl_Position = vec4(2.0*aCorner - 1.0, 0.0, 1.0);
104 | }`;
105 |
106 | const addVelFrag =
107 | `precision mediump float;
108 |
109 | uniform sampler2D uVel;
110 |
111 | uniform vec2 uAddVel;
112 |
113 | varying vec2 sampleCoords;
114 | varying vec2 toBrush;
115 |
116 | ___ENCODING___
117 |
118 | void main(void) {
119 | vec2 vel = decodeVelocity(texture2D(uVel, sampleCoords));
120 |
121 | float influence = 1.0 - clamp(length(toBrush), 0.0, 1.0);
122 | vec2 toAdd = influence * uAddVel;
123 |
124 | vel += toAdd;
125 | vel *= min(1.0, MAX_SPEED / length(vel));
126 |
127 | gl_FragColor = encodeVelocity(vel);
128 | }`;
129 |
130 | const fullscreenVert =
131 | `attribute vec2 aCorner; //{0,1}x{0,1}
132 |
133 | varying vec2 sampleCoords;
134 |
135 | void main(void) {
136 | sampleCoords = aCorner;
137 | gl_Position = vec4(2.0*aCorner - 1.0, 0.0, 1.0);
138 | }`;
139 |
140 | const advectFrag =
141 | `precision mediump float;
142 |
143 | uniform sampler2D uQuantity; //thing to advect
144 | uniform sampler2D uVel;
145 |
146 | uniform vec2 uVelUnit;
147 | uniform float uDt;
148 |
149 | varying vec2 sampleCoords;
150 |
151 | ___ENCODING___
152 |
153 | void main(void) {
154 | vec2 vel = decodeVelocity(texture2D(uVel, sampleCoords));
155 | vec2 pos = sampleCoords - uDt * vel * uVelUnit;
156 | gl_FragColor = texture2D(uQuantity, pos);
157 | }`;
158 |
159 | const jacobiPressureFrag =
160 | `precision mediump float;
161 |
162 | uniform sampler2D uPrevIter; //x
163 | uniform sampler2D uConstantTerm; //b
164 | uniform sampler2D uObstacles;
165 |
166 | uniform float uAlpha;
167 | uniform float uInvBeta;
168 |
169 | uniform vec2 uTexelSize;
170 |
171 | varying vec2 sampleCoords;
172 |
173 | ___ENCODING___
174 |
175 | void main(void) {
176 | vec2 obstacle = decodeObstacle(texture2D(uObstacles, sampleCoords));
177 | vec2 coords = sampleCoords + uTexelSize * obstacle;
178 |
179 | float result = decodePressure(texture2D(uPrevIter, coords - vec2(uTexelSize.x,0))) +
180 | decodePressure(texture2D(uPrevIter, coords + vec2(uTexelSize.x,0))) +
181 | decodePressure(texture2D(uPrevIter, coords - vec2(0,uTexelSize.y))) +
182 | decodePressure(texture2D(uPrevIter, coords + vec2(0,uTexelSize.y)));
183 |
184 | result += uAlpha * decodeDivergence(texture2D(uConstantTerm, coords));
185 | result *= uInvBeta;
186 | result = clamp(result, MIN_PRESSURE, MAX_PRESSURE);
187 |
188 | gl_FragColor = encodePressure(result);
189 | }`;
190 |
191 | const divergenceFrag =
192 | `precision mediump float;
193 |
194 | uniform sampler2D uVelocity;
195 |
196 | uniform vec2 uTexelSize;
197 |
198 | varying vec2 sampleCoords;
199 |
200 | ___ENCODING___
201 |
202 | void main(void) {
203 | vec2 top = decodeVelocity(texture2D(uVelocity, sampleCoords + vec2(0,uTexelSize.y)));
204 | vec2 bottom = decodeVelocity(texture2D(uVelocity, sampleCoords - vec2(0,uTexelSize.y)));
205 | vec2 left = decodeVelocity(texture2D(uVelocity, sampleCoords - vec2(uTexelSize.x,0)));
206 | vec2 right = decodeVelocity(texture2D(uVelocity, sampleCoords + vec2(uTexelSize.x,0)));
207 |
208 | float div = ((right.x - left.x) + (top.y - bottom.y));
209 | div *= min(1.0, MAX_DIVERGENCE / length(div));
210 |
211 | gl_FragColor = encodeDivergence(div);
212 | }`;
213 |
214 | const substractGradientFrag =
215 | `precision mediump float;
216 |
217 | uniform sampler2D uVelocities;
218 | uniform sampler2D uPressure;
219 |
220 | uniform vec2 uTexelSize;
221 | uniform float uHalfInvDx;
222 |
223 | varying vec2 sampleCoords;
224 |
225 | ___ENCODING___
226 |
227 | void main(void) {
228 | float top = decodePressure(texture2D(uPressure, sampleCoords + vec2(0,uTexelSize.y)));
229 | float bottom = decodePressure(texture2D(uPressure, sampleCoords - vec2(0,uTexelSize.y)));
230 | float left = decodePressure(texture2D(uPressure, sampleCoords - vec2(uTexelSize.x,0)));
231 | float right = decodePressure(texture2D(uPressure, sampleCoords + vec2(uTexelSize.x,0)));
232 |
233 | vec2 grad = uHalfInvDx * vec2(right - left, top - bottom);
234 |
235 | vec2 partialVel = decodeVelocity(texture2D(uVelocities, sampleCoords));
236 | vec2 divFreeVel = partialVel - grad;
237 | divFreeVel *= min(1.0, MAX_SPEED / length(divFreeVel));
238 |
239 | gl_FragColor = encodeVelocity(divFreeVel);
240 | }`;
241 |
242 | const obstacleVelocityFrag =
243 | `precision mediump float;
244 |
245 | uniform sampler2D uVelocities;
246 | uniform sampler2D uObstacles;
247 |
248 | uniform vec2 uTexelSize;
249 |
250 | varying vec2 sampleCoords;
251 |
252 | ___ENCODING___
253 |
254 | void main(void) {
255 | vec2 obstacle = decodeObstacle(texture2D(uObstacles, sampleCoords));
256 | vec2 coords = sampleCoords + obstacle * uTexelSize;
257 |
258 | vec2 vel = decodeVelocity(texture2D(uVelocities, coords));
259 | vel *= sign(0.5 - dot(obstacle,obstacle));
260 |
261 | gl_FragColor = encodeVelocity(vel);
262 | }`;
263 |
264 | const drawVelocityFrag =
265 | `precision mediump float;
266 |
267 | uniform sampler2D uVel;
268 | uniform float uColorIntensity;
269 | uniform bool uBlacknWhite;
270 |
271 | varying vec2 sampleCoords;
272 |
273 | ___ENCODING___
274 |
275 | /*
276 | /---\
277 | __/ \__
278 | 0 1 2 3 4
279 | */
280 | float bump(float x) {
281 | return min(mix(0.0, 1.0, clamp(x, 0.0, 1.0)),
282 | mix(1.0, 0.0, clamp(x-3.0, 0.0, 1.0)));
283 | }
284 |
285 | /* Every hue, periodic with a period of 1 */
286 | vec3 color(float value) {
287 | value *= 6.0;
288 | float r = bump(mod(value-4.0, 6.0));
289 | float g = bump(mod(value-0.0, 6.0));
290 | float b = bump(mod(value-2.0, 6.0));
291 | return vec3(r,g,b);
292 | }
293 |
294 | void main(void) {
295 | vec2 vel = decodeVelocity(texture2D(uVel, sampleCoords)) / MAX_SPEED;
296 |
297 | vec3 c = color(atan(vel.y, vel.x) / (2.0 * 3.14159));
298 | c = mix(c, vec3(1), float(uBlacknWhite));
299 |
300 | float intensity = smoothstep(0.0, 1.0, uColorIntensity*length(vel));
301 |
302 | gl_FragColor = vec4(intensity * c, 1);
303 | }`;
304 |
305 | const drawPressureFrag =
306 | `precision mediump float;
307 |
308 | uniform sampler2D uPressure;
309 | uniform float uColorIntensity;
310 | uniform bool uBlacknWhite;
311 |
312 | varying vec2 sampleCoords;
313 |
314 | ___ENCODING___
315 |
316 | vec3 color(float value) {
317 | value = smoothstep(0.0, 1.0, clamp(value, 0.0, 1.0));
318 | value = smoothstep(0.0, 1.0, value);
319 |
320 | float r = smoothstep(.5, .75, value);
321 | float g = min(smoothstep(.0, .25, value),
322 | 1.0 - smoothstep(.75, 1.0, value));
323 | float b = 1.0 - smoothstep(.25, .5, value);
324 | return vec3(r,g,b);
325 | }
326 |
327 | void main(void) {
328 | float pressure = decodePressure(texture2D(uPressure, sampleCoords));
329 | pressure = pressure / MAX_PRESSURE;
330 | pressure = clamp(256.0*uColorIntensity*pressure, -.5, .5) + 0.5;
331 |
332 | vec3 c = color(pressure);
333 | c = mix(c, vec3(pressure), float(uBlacknWhite));
334 |
335 | gl_FragColor = vec4(c, 1);
336 | }`;
337 |
338 | let encodingStr: string = rawEncodingStr;
339 | setUseFloatTextures(false);
340 |
341 | function setUseFloatTextures(useFloat: boolean): void {
342 | const replace = (useFloat) ? encodingVelocityFloatStr : encodingVelocityNoFloatStr;
343 | encodingStr = rawEncodingStr.replace(/___ENCODING_VELOCITY___/g, replace);
344 | encodingStr = encodingStr.replace(/___ENCODING_OBSTACLES___/g, encodingObstaclesStr);
345 | }
346 |
347 | const drawVelocitySrc = new ShaderSrc(fullscreenVert, drawVelocityFrag);
348 | const drawPressureSrc = new ShaderSrc(fullscreenVert, drawPressureFrag);
349 | const addVelSrc = new ShaderSrc(addVelVert, addVelFrag);
350 | const advectSrc = new ShaderSrc(fullscreenVert, advectFrag);
351 | const jacobiPressureSrc = new ShaderSrc(fullscreenVert, jacobiPressureFrag);
352 | const divergenceSrc = new ShaderSrc(fullscreenVert, divergenceFrag);
353 | const substractGradientSrc = new ShaderSrc(fullscreenVert, substractGradientFrag);
354 | const obstaclesVelocitySrc = new ShaderSrc(fullscreenVert, obstacleVelocityFrag);
355 |
356 | function buildFullscreenShader(gl: WebGLRenderingContext, src: ShaderSrc): Shader {
357 | const vertSrc = src.vert;
358 | let fragSrc = src.frag.replace(/___ENCODING___/g, encodingStr);
359 |
360 | const shader = new Shader(gl, vertSrc, fragSrc);
361 | shader.a["aCorner"].VBO = VBO.createQuad(gl, 0, 0, 1, 1);
362 | return shader;
363 | }
364 |
365 | function buildDrawVelocityShader(gl: WebGLRenderingContext): Shader {
366 | return buildFullscreenShader(gl, drawVelocitySrc);
367 | }
368 |
369 | function buildDrawPressureShader(gl: WebGLRenderingContext): Shader {
370 | return buildFullscreenShader(gl, drawPressureSrc);
371 | }
372 |
373 | function buildAddVelShader(gl: WebGLRenderingContext): Shader {
374 | return buildFullscreenShader(gl, addVelSrc);
375 | }
376 |
377 | function buildAdvectShader(gl: WebGLRenderingContext): Shader {
378 | return buildFullscreenShader(gl, advectSrc);
379 | }
380 |
381 | function buildJacobiPressureShader(gl: WebGLRenderingContext): Shader {
382 | return buildFullscreenShader(gl, jacobiPressureSrc);
383 | }
384 |
385 | function buildDivergenceShader(gl: WebGLRenderingContext): Shader {
386 | return buildFullscreenShader(gl, divergenceSrc);
387 | }
388 |
389 | function buildSubstractGradientShader(gl: WebGLRenderingContext): Shader {
390 | return buildFullscreenShader(gl, substractGradientSrc);
391 | }
392 |
393 | function buildObstaclesVelocityShader(gl: WebGLRenderingContext): Shader {
394 | return buildFullscreenShader(gl, obstaclesVelocitySrc);
395 | }
396 |
397 | export {
398 | buildDrawVelocityShader,
399 | buildDrawPressureShader,
400 | buildAddVelShader,
401 | buildAdvectShader,
402 | buildJacobiPressureShader,
403 | buildDivergenceShader,
404 | buildSubstractGradientShader,
405 | buildObstaclesVelocityShader,
406 | setUseFloatTextures,
407 | };
--------------------------------------------------------------------------------
/src/ts/shaders/obstacle-map-shaders.ts:
--------------------------------------------------------------------------------
1 | import Shader from "../gl-utils/shader";
2 | import VBO from "../gl-utils/vbo";
3 | import { ShaderSrc, Replace } from "./build-shaders";
4 |
5 | const encodingStr =
6 | `vec4 encodeObstacle(vec2 normal) {
7 | normal = clamp(normalize(normal), -1.0, 1.0);
8 | return vec4(0.5 * normal + 0.5, 0, 0);
9 | }
10 | vec2 decodeObstacle(vec4 texel) {
11 | return 2.0 * texel.rg - 1.0;
12 | }`;
13 |
14 | const drawVert =
15 | `attribute vec2 aCorner; //{0,1}x{0,1}
16 |
17 | varying vec2 sampleCoords;
18 |
19 | void main(void) {
20 | sampleCoords = aCorner;
21 | gl_Position = vec4(2.0*aCorner - 1.0, 0.0, 1.0);
22 | }`;
23 |
24 | const drawFrag =
25 | `precision mediump float;
26 |
27 | uniform sampler2D uObstacles;
28 |
29 | varying vec2 sampleCoords;
30 |
31 | ___ENCODING___
32 |
33 | void main(void) {
34 | vec2 obstacle = decodeObstacle(texture2D(uObstacles, sampleCoords));
35 | if (dot(obstacle, obstacle) < 0.5)
36 | discard;
37 |
38 | gl_FragColor = vec4(0.5*obstacle + 0.5, 0, 0);
39 | }`;
40 |
41 | const addObstacleVert =
42 | `uniform vec2 uSize; //relative, in [0,1]x[0,1]
43 | uniform vec2 uPos; //relative, in [0,1]x[0,1]
44 |
45 | attribute vec2 aCorner; //in {-1,+1}x{-1,+1}
46 |
47 | varying vec2 toCenter;
48 |
49 | void main(void) {
50 | toCenter = -aCorner;
51 |
52 | vec2 pos = uPos + aCorner * uSize;
53 |
54 | gl_Position = vec4(2.0 * pos - 1.0, 0, 1);
55 | }`;
56 |
57 | const addObstacleFrag =
58 | `precision mediump float;
59 |
60 | varying vec2 toCenter;
61 |
62 | ___ENCODING___
63 |
64 | void main(void) {
65 | float dist = length(toCenter);
66 | if (dist > 1.0)
67 | discard;
68 |
69 | vec2 normal = -toCenter / dist;
70 | gl_FragColor = encodeObstacle(normal);
71 | }`;
72 |
73 |
74 | const includes: Replace[] = [
75 | { toReplace: "___ENCODING___", replacement: encodingStr },
76 | ];
77 |
78 | const drawSrc = new ShaderSrc(drawVert, drawFrag);
79 | drawSrc.batchReplace(includes);
80 |
81 | const addSrc = new ShaderSrc(addObstacleVert, addObstacleFrag);
82 | addSrc.batchReplace(includes);
83 |
84 |
85 | function buildDrawShader(gl: WebGLRenderingContext): Shader {
86 | const shader = new Shader(gl, drawSrc.vert, drawSrc.frag);
87 | shader.a["aCorner"].VBO = VBO.createQuad(gl, 0, 0, 1, 1);
88 | return shader;
89 | }
90 |
91 | function buildAddShader(gl: WebGLRenderingContext): Shader {
92 | const shader = new Shader(gl, addSrc.vert, addSrc.frag);
93 | shader.a["aCorner"].VBO = VBO.createQuad(gl, -1, -1, 1, 1);
94 | return shader;
95 | }
96 |
97 | export { buildDrawShader, buildAddShader, encodingStr };
--------------------------------------------------------------------------------