Last active
March 1, 2022 21:40
-
-
Save DylanJones/5b927d66418017a810b1c946b7b36846 to your computer and use it in GitHub Desktop.
GLSL Fluid Simulation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// #version 300 es | |
#define SHADERTOY | |
// In order to get this to work on Shader Editor, comment out the SHADERTOY define | |
// and comment in the #version tag. | |
/********** SHADERTOY COMPAT ***********/ | |
// Shadertoy uses different input names from Shader Editor. | |
#ifdef SHADERTOY | |
#define resolution iResolution | |
#define backbuffer iChannel0 | |
#define noise iChannel1 | |
#define touch iMouse | |
#define time iTime | |
#define gl_FragCoord fragCoord | |
#define pointerCount 1 | |
#define NOISE_CUTOFF 0.7 | |
// Shadertoy provides far higher resolution for color storage, so we can get away with a much | |
// higher value for FLOAT_MAX there. | |
#define FLOAT_MAX pow(2.0, 15.0) | |
#else | |
uniform sampler2D backbuffer; | |
uniform sampler2D noise; | |
uniform vec2 resolution; | |
uniform vec2 touch; | |
uniform float time; | |
uniform int pointerCount; | |
out vec4 fragColor; | |
#define NOISE_CUTOFF 0.5 | |
// Shader Editor clamps our color values, so we're limited to 7 bits of color resolution. | |
#define FLOAT_MAX pow(2.0, 7.0) | |
#endif | |
// We only need 5 bits, so set the steganography parameters to hold | |
// 6 bits total. | |
#define BITS_PER_CHANNEL 2 | |
// Shader Editor absolutely destroys the precision of any colors in the output buffer | |
// (potentially as bad as 7 bits worth of color precision), so there's no point in | |
// using highp floats. | |
precision mediump float; | |
/*********** STEGANOGRAPHIC STUFF ************/ | |
// Convenience function to pull out a single "bit" from a large float (just modulo by 2^bit). | |
int getBit(float v, int bit) { | |
return int(mod(v / pow(2.0, float(bit)), 2.0)); | |
} | |
// Pulls out 3 * BITS_PER_CHANNEL bits of data from the "LSB" of the R, G, B channels | |
int decode(vec3 color) { | |
vec3 ex = round(color * FLOAT_MAX); | |
int data = 0; | |
for (int i = 0; i < BITS_PER_CHANNEL; i++) { | |
data |= getBit(ex.r, i) << (i); | |
data |= getBit(ex.g, i) << (i + BITS_PER_CHANNEL); | |
data |= getBit(ex.b, i) << (i + 2 * BITS_PER_CHANNEL); | |
} | |
return data; | |
} | |
// Encode up to 3 * BITS_PER_CHANNEL bits of data in the given vec3's "LSBs". | |
vec3 encode(vec3 color, int data) { | |
vec3 ex = color * FLOAT_MAX; | |
ex = min(ex, FLOAT_MAX); | |
ex /= pow(2.0, float(BITS_PER_CHANNEL)); | |
// This is a *hack* that makes it work with an initial value of 1.0, but seems to | |
// kind of break encoding of 0.0 | |
ex -= vec3(1.0, 1.0, 1.0) / FLOAT_MAX; | |
ex = floor(ex); | |
ex *= pow(2.0, float(BITS_PER_CHANNEL)); | |
ex.r += float(data & int(pow(2.0, float(BITS_PER_CHANNEL)) - 1.0)); | |
ex.g += float((data >> BITS_PER_CHANNEL) & int(pow(2.0, float(BITS_PER_CHANNEL)) - 1.0)); | |
ex.b += float((data >> (BITS_PER_CHANNEL * 2)) & int(pow(2.0, float(BITS_PER_CHANNEL)) - 1.0)); | |
ex /= FLOAT_MAX; | |
return ex; | |
} | |
// Get the RGB value of the pixel at the given offset from pix | |
vec4 get(vec2 pix, vec2 offset) { | |
return texture(backbuffer, (pix + offset) / resolution.xy); | |
} | |
// Do the movement step for a single vector. This essentially just looks in the given | |
// direction for a vector that should move into this cell, and returns the vector if it | |
// exists in the adjacent cell. It also handles reflections off of the black border at the edge. | |
int getNeighborOrReflect(vec2 pix, vec2 offset, int data, int bit) { | |
const int reflections[4] = int[4](0x2, 0x1, 0x8, 0x4); | |
vec2 neighbor = (pix + offset); | |
// Can't really figure out why, but we need to leave a 1px black border. | |
if ((pix.x < 1.0 || pix.y < 1.0 || pix.x > resolution.x - 1.0 || pix.y > resolution.y - 1.0)) { | |
return 0; | |
} | |
if ((neighbor.x < 1.0 || neighbor.y < 1.0 || neighbor.x > resolution.x - 1.0 || neighbor.y > resolution.y - 1.0) | |
&& (data & reflections[bit]) != 0) { | |
return 1 << bit; | |
} | |
return decode(get(pix, offset).rgb) & (1 << bit); | |
} | |
/********************* SIMULATION *******************/ | |
/* | |
* Bit 0 indicates the prescence of an up vector. | |
* Bit 1 is for a down vector. | |
* Bit 2 is for a left vector. | |
* Bit 3 is for a right vector. | |
* Bit 4 is for the time information - 0 if a collision step | |
* is next, 1 if a movement step is next. | |
*/ | |
int collide(int data) { | |
// This is just implemented as a lookup table. | |
// D U DU | |
const int collisions[16] = int[16](0x0, 0x1, 0x2, 0xC, | |
/* L */ 0x4, 0x5, 0x6, 0x7, | |
/* R */ 0x8, 0x9, 0xA, 0xB, | |
/* LR*/ 0x3, 0xD, 0xE, 0xF); | |
return collisions[data & 0xF]; | |
} | |
int move(vec2 coord, int data) { | |
return getNeighborOrReflect(coord, vec2(0, 1), data, 0) | | |
getNeighborOrReflect(coord, vec2(0, -1), data, 1) | | |
getNeighborOrReflect(coord, vec2(-1, 0), data, 2) | | |
getNeighborOrReflect(coord, vec2(1, 0), data, 3); | |
} | |
// For the Gaussian blur, a 5x5 kernel defined as a constant | |
#define SIGMA 2.0 | |
#define GB(x, y) (2.0/(2.0 * 3.1415926 * SIGMA * SIGMA) * exp(-1.0 * (float(x) * float(x) + float(y) * float(y)) / (2.0 * SIGMA * SIGMA))) | |
const float kernel[25] = float[25](GB(2,2), GB(1,2), GB(0,2), GB(1,2), GB(2,2), | |
GB(2,1), GB(1,1), GB(0,1), GB(1,1), GB(2,1), | |
GB(2,0), GB(1,0), GB(0,0), GB(1,0), GB(2,0), | |
GB(2,1), GB(1,1), GB(0,1), GB(1,1), GB(2,1), | |
GB(2,2), GB(1,2), GB(0,2), GB(1,2), GB(2,2)); | |
// Apply the Gaussian kernel to the current pixel. | |
float gaussian(vec2 coord) { | |
float sum = 0.0; | |
for (int x = -2; x <= 2; x++) { | |
for(int y = -2; y <= 2; y++) { | |
int data = decode(get(coord, vec2(x, y)).rgb); | |
if ((data & 0xF) != 0) { | |
sum += kernel[x * 5 + y]; | |
} | |
} | |
} | |
return sum; | |
} | |
#ifdef SHADERTOY | |
void mainImage( out vec4 fragColor, in vec2 fragCoord ) | |
#else | |
void main() | |
#endif | |
{ | |
vec2 uv = gl_FragCoord.xy / resolution.xy; | |
vec2 pix = gl_FragCoord.xy; | |
vec3 color = texture(backbuffer, uv).rgb; | |
if (time < 0.06) { | |
color = vec3(uv, abs(sin(uv.x * uv.y * 2.0))); | |
// if (uv.x > 0.4 && uv.x < 0.7 && uv.y > 0.4 && uv.y < 0.7) { | |
// We can't allow pixels in the border region to be set, as they'll just spawn particles indefinitely without moving | |
// this is also a massive hack since it just... doesn't work sometimes? | |
if (texture(noise, uv).r > NOISE_CUTOFF && | |
pix.y > 1.0) { | |
color = encode(color, 0xf); | |
} else { | |
color = encode(color, 0); | |
} | |
} | |
// Grab the vector data from the previous frame | |
int data = decode(color); | |
// mouse can spawn particles | |
if (distance(touch.xy, pix) < 5.0 && pointerCount > 0) { | |
data |= 0xF; | |
} | |
// Do the actual collision / movement step | |
if ((data & 0x10) != 0) { | |
data = move(pix.xy, data); | |
} else { | |
data = collide(data) | 0x10; | |
} | |
// Color the output based on whether or not there's at least | |
// one particle there | |
//if ((data & 0xF) != 0) { | |
// color = vec3(1.0, 1.0, 1.0); | |
float strength = gaussian(pix); | |
color = vec3(uv.x, 1.0 - uv.y, uv.y) * strength; | |
//} else { | |
// color = vec3(0.1, 0.0, 0.1); | |
//} | |
// Finally, encode the new vector data back into the newly selected color | |
color = encode(color, data); | |
fragColor = vec4(color, 1.0); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment