├── README.md └── voxel_watersim.pde /README.md: -------------------------------------------------------------------------------- 1 | # Voxel Watersim 2 | ## A voxel fluid simulator implemented in Processing 3 | 4 | A body of water is dropped on top of a randomly-generated landscape and cellular automaton rules are used to simulate fluid dynamics. 5 | 6 | Credit goes to [YankeeMinistrel](https://www.reddit.com/r/cellular_automata/comments/6jhdfw/i_used_1dimensional_cellular_automata_to_make_a/) for the 1D version on which my code is heavily inspired. 7 | 8 | ### Some gifs generated with this tool: 9 | 10 | ![](http://i.imgur.com/fYbfG4g.gif) 11 | ![](http://i.imgur.com/po87vpt.gif) 12 | ![](http://i.imgur.com/M1h6Gg8.gif) 13 | ![](http://i.imgur.com/0ibrloJ.gif) 14 | ![](http://i.imgur.com/UdYl1DR.gif) 15 | ![Here you can see the effects of turbulence](http://i.imgur.com/w8uosxK.gif) 16 | ![Turbulence, again](http://i.imgur.com/3zkLXre.gif) 17 | ![](http://i.imgur.com/hLzB4pb.gif) 18 | -------------------------------------------------------------------------------- /voxel_watersim.pde: -------------------------------------------------------------------------------- 1 | 2 | // Import gifAnimation to export the sketch as gifs 3 | import gifAnimation.*; 4 | GifMaker gif; 5 | 6 | // N: determines the grid size (N² x N²) 7 | // cw: cube width 8 | // terrainNoise, hueNoise, saturationNoise: noise scales 9 | // Increase terrainNoise to generate more rugged terrains 10 | // Increase hueNoise, saturationNoise accordingly to generate more rugged color landscapes 11 | int N; 12 | float cw; 13 | float terrainNoise,hueNoise,saturationNoise; 14 | 15 | // terrain: earth level at each (i,j) coordinate 16 | int[][] terrain; 17 | 18 | // water, energy: water levels and kinetic energy levels 19 | float[][] water; 20 | float[][] energy; 21 | 22 | // t: simulation time time 23 | float t = 0; 24 | 25 | void setup() 26 | { 27 | // Initialize screen dimensions and 3D mode 28 | size(400,400,P3D); 29 | 30 | // We will work in the hue/saturation/brightness color space 31 | colorMode(HSB); 32 | 33 | // Initialize parameters 34 | N = 50; 35 | cw = width/N; 36 | terrainNoise = 5; 37 | hueNoise = 3; 38 | saturationNoise = 3; 39 | 40 | // Initialize terrain 41 | init_terrain(); 42 | 43 | // Initialize water levels and kinetic energy levels 44 | water = new float[N][N]; 45 | energy = new float[N][N]; 46 | for(int i = 0; i < N; i++) 47 | for(int j = 0; j < N; j++) 48 | { 49 | water[i][j] = 0; 50 | energy[i][j] = 0; 51 | } 52 | 53 | // Initialize water levels placing a square prism of water of base length L and height H on the center of the terrain 54 | int L = 10; 55 | int H = 30; 56 | for(int i = N/2 -L/2; i < N/2 + L/2; i++) 57 | for(int j = N/2 -L/2; j < N/2 + L/2; j++) 58 | water[i][j] = H; 59 | 60 | // Initialize gif maker 61 | gif = new GifMaker(this, "export.gif"); 62 | gif.setRepeat(0); // make it an "endless" animation 63 | gif.setQuality(20); // set a higher compression quality (the default is 10) to preserve the blocks' colors 64 | } 65 | 66 | void draw() 67 | { 68 | // Clear the canvas 69 | background(50); 70 | 71 | // Check and increment time counter (the exported gif will contain 50 frames) 72 | if(t >= TWO_PI) 73 | { 74 | gif.finish(); 75 | exit(); 76 | } 77 | else 78 | { 79 | t += TWO_PI/50; 80 | } 81 | 82 | // Set isometric projection 83 | setOrtho(); 84 | 85 | // Draw terrain and water 86 | drawTerrain(); 87 | drawWater(); 88 | 89 | // Run 10 iterations of the fluid simulation procedure 90 | for(int i=0; i < 10; i++) updateWater(); 91 | 92 | // Add this frame to the gif maker 93 | gif.addFrame(); 94 | } 95 | 96 | // This function generates the terrain 97 | void init_terrain() 98 | { 99 | terrain = new int[N][N]; 100 | 101 | for(int i = 0; i < N; i++) 102 | for(int j = 0; j < N; j++) 103 | { 104 | // Create walls protecting the limits of the terrain 105 | // These walls will be invisible (the drawTerrain() procedure won't draw them) 106 | if(i==0||i==N-1||j==0||j==N-1) 107 | { 108 | terrain[i][j] = 100*N; 109 | } 110 | else 111 | { 112 | // H: the (i,j) height determined by a 2D parabolic equation 113 | // P: a random perturbation generated by perlin noise to be added to H 114 | float H = 0.4*N - 0.02*(pow(i-N/2,2)+pow(j-N/2,2)); 115 | float P = 0.5*N*(-0.5 + noise(terrainNoise*i/N,terrainNoise*j/N,100)); 116 | terrain[i][j] = max(0,int(H+P)); 117 | } 118 | } 119 | } 120 | 121 | void updateWater() 122 | { 123 | float[][] new_water = water; 124 | float[][] new_energy = energy; 125 | 126 | // Friction: determines fluid viscosity 127 | float friction = 0.125; 128 | 129 | for(int i = 0; i < N; i++) 130 | for(int j = 0; j < N; j++) 131 | { 132 | // Lp: left water pressure | Rp: right water pressure | Bp: back water pressure | Fp: front water pressure 133 | float Lp,Rp,Bp,Fp; 134 | Lp = Rp = Bp = Fp = 0; 135 | 136 | // Avoid border cases 137 | if(i > 0) Lp = terrain[i-1][j]+water[i-1][j]+energy[i-1][j]; 138 | if(i < N-1) Rp = terrain[i+1][j]+water[i+1][j]-energy[i+1][j]; 139 | if(j > 0) Bp = terrain[i][j-1]+water[i][j-1]+energy[i][j-1]; 140 | if(j < N-1) Fp = terrain[i][j+1]+water[i][j+1]-energy[i][j+1]; 141 | 142 | if(i > 0 && terrain[i][j]+water[i][j]-energy[i][j] > Lp) 143 | { 144 | float flow = min(water[i][j], terrain[i][j]+water[i][j]-energy[i][j] - Lp)/16; 145 | new_water[i-1][j] += flow; 146 | new_water[i][j] += -flow; 147 | new_energy[i-1][j] *= (1-friction); 148 | new_energy[i-1][j] += -flow; 149 | } 150 | 151 | if(i < N-1 && terrain[i][j]+water[i][j]+energy[i][j] > Rp) 152 | { 153 | float flow = min(water[i][j], terrain[i][j]+water[i][j]+energy[i][j] - Rp)/16; 154 | new_water[i+1][j] += flow; 155 | new_water[i][j] += -flow; 156 | new_energy[i+1][j] *= (1-friction); 157 | new_energy[i+1][j] += flow; 158 | } 159 | 160 | if(j > 0 && terrain[i][j]+water[i][j]-energy[i][j] > Bp) 161 | { 162 | float flow = min(water[i][j], terrain[i][j]+water[i][j]-energy[i][j] - Bp)/16; 163 | new_water[i][j-1] += flow; 164 | new_water[i][j] += -flow; 165 | new_energy[i][j-1] *= (1-friction); 166 | new_energy[i][j-1] += -flow; 167 | } 168 | 169 | if(j < N-1 && terrain[i][j]+water[i][j]+energy[i][j] > Fp) 170 | { 171 | float flow = min(water[i][j], terrain[i][j]+water[i][j]+energy[i][j] - Fp)/16; 172 | new_water[i][j+1] += flow; 173 | new_water[i][j] += -flow; 174 | new_energy[i][j+1] *= (1-friction); 175 | new_energy[i][j+1] += flow; 176 | } 177 | } 178 | 179 | water = new_water; 180 | energy = new_energy; 181 | } 182 | 183 | void setOrtho() 184 | { 185 | float a = 0.7; 186 | ortho(-a*width,a*width,-a*height,a*height); 187 | rotateX(-PI/5); 188 | rotateY(PI/3); 189 | } 190 | 191 | void drawTerrain() 192 | { 193 | for(int i = 1; i < N-1; i++) 194 | for(int j = 1; j < N-1; j++) 195 | for(int k = 0; k < terrain[i][j]; k++) 196 | { 197 | // Adjust color configurations as you will (this configuration generates "reddish clay" terrains) 198 | int hue = 10*(int(0 + 50*noise(hueNoise*i/N,hueNoise*j/N,hueNoise*k/N))/10); 199 | int saturation = 10*(int(50 + 100*noise(1000+saturationNoise*i/N,saturationNoise*j/N,saturationNoise*k/N))/10); 200 | int brightness = 150; 201 | 202 | // Drawing the edges of blocks with similar colors to their faces' colors creates an aesthetically pleasing effect 203 | stroke(hue,0.8*saturation,0.8*brightness); 204 | 205 | pushMatrix(); 206 | fill(hue,saturation,brightness); 207 | translate(-width/2 + i*cw, 1.3*width - k*cw, -width/2 + j*cw); 208 | box(cw); 209 | popMatrix(); 210 | } 211 | } 212 | 213 | void drawWater() 214 | { 215 | stroke(150,200,250); 216 | for(int i = 0; i < N; i++) 217 | for(int j = 0; j < N; j++) 218 | { 219 | for(int k = terrain[i][j]; k < terrain[i][j]+int(round(water[i][j])); k++) 220 | { 221 | pushMatrix(); 222 | fill(150,200,200); 223 | translate(-width/2 + i*cw, 1.3*width - k*cw, -width/2 + j*cw); 224 | box(cw); 225 | popMatrix(); 226 | } 227 | } 228 | } 229 | --------------------------------------------------------------------------------