Skip to content

Instantly share code, notes, and snippets.

@DylanJones
Last active March 1, 2022 21:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DylanJones/5b927d66418017a810b1c946b7b36846 to your computer and use it in GitHub Desktop.
Save DylanJones/5b927d66418017a810b1c946b7b36846 to your computer and use it in GitHub Desktop.
GLSL Fluid Simulation
// #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