├── Metaballs ├── pass_through.vert ├── metaball_shader_3d.vert ├── images │ └── example.png ├── metaball_shader_2d.frag ├── metaball_shader_3d.frag └── main.cpp ├── images └── example.png ├── .gitmodules ├── .gitignore └── README.md /Metaballs/pass_through.vert: -------------------------------------------------------------------------------- 1 | void main() 2 | { 3 | gl_Position = gl_Vertex; 4 | } -------------------------------------------------------------------------------- /Metaballs/metaball_shader_3d.vert: -------------------------------------------------------------------------------- 1 | void main() 2 | { 3 | gl_Position = gl_Vertex; 4 | } -------------------------------------------------------------------------------- /images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orglofch/metaballs/HEAD/images/example.png -------------------------------------------------------------------------------- /Metaballs/images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orglofch/metaballs/HEAD/Metaballs/images/example.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Metaballs/Utility"] 2 | path = Metaballs/Utility 3 | url = https://github.com/orglofch/Utility.git 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.opensdf 2 | *.sln 3 | *.sdf 4 | *.suo 5 | *Debug* 6 | *Release* 7 | *.vcxproj 8 | *.filters 9 | *.user 10 | *.resx 11 | Release 12 | Debug 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Metaballs 2 | ============ 3 | 4 | Realtime 2D and 3D Metaball rendering. 5 | 6 | Language: C++ 7 | 8 | Libraries: Freeglut, GLEW 9 | 10 | --- 11 | 12 | https://www.youtube.com/watch?v=mKZIjw9c6dk 13 | ![example](https://github.com/orglofch/Metaballs/blob/master/images/example.png) 14 | -------------------------------------------------------------------------------- /Metaballs/metaball_shader_2d.frag: -------------------------------------------------------------------------------- 1 | # version 120 2 | 3 | /** The maximum number of charges that can be passed to this shader. */ 4 | #define MAX_CHARGES 75 5 | 6 | /** Container for charges, may not be full. */ 7 | uniform vec4 charges[MAX_CHARGES]; 8 | 9 | /** The actual number of charges. */ 10 | uniform int charge_count; 11 | 12 | /** The treshold charge. */ 13 | uniform float threshold; 14 | 15 | void main() 16 | { 17 | float charge = 0.0; 18 | for (int i = 0; i < charge_count; ++i) { 19 | float dist = distance(charges[i].xy, gl_FragCoord.xy); 20 | if (dist == 0.0) { 21 | charge = threshold; 22 | break; 23 | } 24 | float r = dist / charges[i].w; 25 | charge += 1 / (r*r); 26 | } 27 | 28 | charge /= 1000.0; 29 | if (charge > 0.9) { 30 | charge = pow(charge, 3); 31 | gl_FragColor = vec4(charge / 2, charge / 2, charge, 1); 32 | } else { 33 | gl_FragColor = vec4(charge / 2, charge / 2, charge, 1); 34 | } 35 | } -------------------------------------------------------------------------------- /Metaballs/metaball_shader_3d.frag: -------------------------------------------------------------------------------- 1 | # version 120 2 | 3 | /** The maximum number of charges that can be passed to this shader. */ 4 | #define MAX_CHARGES 30 5 | 6 | #define INF 1.0 / 0.0 7 | #define EPSILON 0.01 8 | #define MAX_REFLECTIONS 2 9 | 10 | /** The intersection types. */ 11 | #define INTERSECTION_ENTRY 12 | #define INTERSECTION_ENTRANCE 13 | 14 | /** Container for charges, may not be full. */ 15 | uniform vec4 charges[MAX_CHARGES]; 16 | 17 | /** The actual number of charges. */ 18 | uniform int charge_count; 19 | 20 | /** The threshold charge for a meta surface. */ 21 | uniform float threshold; 22 | 23 | uniform mat4 camera_matrix; 24 | uniform vec3 camera_origin; 25 | 26 | /** 27 | * Calculates the intersections with the given charge sphere. 28 | * 29 | * @param ro The origin of the cast ray. 30 | * @param dir The direction of the cast ray. 31 | * @param index The index of the charge sphere to intersect. 32 | * @param intersections The structures for storing the intersection results. 33 | * @return The number of intersections found. 34 | */ 35 | int findSphereIntersection( 36 | in vec3 ro, 37 | in vec3 dir, 38 | in int index, 39 | out float roots[2]) { 40 | vec3 fc = ro - charges[index].xyz; 41 | 42 | float a = dot(dir, dir); 43 | float b = 2.0 * dot(fc, dir); 44 | float c = dot(fc, fc) - charges[index].w*charges[index].w; 45 | 46 | if (a == 0) { 47 | if (b == 0) { 48 | return 0; 49 | } else { 50 | roots[0] = -c / b; 51 | return 1; 52 | } 53 | } else { 54 | float det = b*b - 4.0 * a*c; 55 | if (det <= 0.0) { 56 | return 0; 57 | } else { 58 | float q = -(b + sign(b)*sqrt(det)) / 2.0; 59 | roots[0] = q / a; 60 | 61 | if (q != 0) { 62 | roots[1] = c / q; 63 | } else { 64 | roots[1] = roots[0]; 65 | } 66 | return 2; 67 | } 68 | } 69 | return 0; 70 | } 71 | 72 | float calculateCharge(in float distance, in float charge_radius) { 73 | float r = distance / charge_radius; 74 | if (r > 1) { 75 | return 0; 76 | } 77 | 78 | return 1.0 - r*r*r * (r * (r * 6.0 - 15.0) + 10.0); 79 | } 80 | 81 | bool findIntersection(in vec3 ro, in vec3 rd, out vec3 pos, out vec3 normal) { 82 | // Find intersection with the ray and the metaball spheres. 83 | // The charge function we use = 0 for dist > charge radius 84 | // so we can ignore charges which fall outside this range. 85 | int active_charges[MAX_CHARGES]; 86 | int intersection_count = 0; 87 | 88 | // Keep track of the min and max t values so we only have to 89 | // iterate between the two points these define. 90 | // TODO(orglofch): Store all intersections and their type 91 | // E.g. ENTER, EXIT, so we can skip gaps between influence spheres. 92 | float min_t = INF; 93 | float max_t = 0.0; 94 | 95 | for (int i = 0; i < charge_count; ++i) { 96 | float roots[2]; 97 | int num_roots = findSphereIntersection(ro, rd, i, roots); 98 | if (num_roots > 0) { 99 | active_charges[intersection_count++] = i; 100 | if (num_roots >= 1 && roots[0] > 0) { 101 | max_t = max(max_t, roots[0]); 102 | min_t = min(min_t, roots[0]); 103 | } 104 | if (num_roots == 2 && roots[1] > 0) { 105 | max_t = max(max_t, roots[1]); 106 | min_t = min(min_t, roots[1]); 107 | } 108 | } 109 | } 110 | 111 | if (intersection_count == 0) { 112 | return false; 113 | } 114 | 115 | // Ray march between [min_t, max_t] to approximate if this 116 | // ray ever intersects a meta surface. 117 | float cur_t = min_t; 118 | while (cur_t < max_t) { 119 | pos = ro + rd * cur_t; 120 | float step_charge = 0.0; 121 | for (int i = 0; i < intersection_count; ++i) { 122 | vec4 metaball = charges[active_charges[i]]; 123 | float dist = distance(metaball.xyz, pos); 124 | step_charge += calculateCharge(dist, metaball.w); 125 | } 126 | // TODO(orglofch): This can early exit in the loop, but it messes 127 | // up the color function due to not having full charge sum. 128 | if (step_charge >= 0.4) { 129 | normal = vec3(0, 0, 0); 130 | // Add normals for each metaball based on their contribution. 131 | for (int i = 0; i < intersection_count; ++i) { 132 | vec4 metaball = charges[active_charges[i]]; 133 | vec3 from_center = pos - metaball.xyz; 134 | float dist = length(from_center); 135 | float charge = calculateCharge(dist, metaball.w); 136 | if (charge > 0) { 137 | normal += normalize(from_center) * (charge / step_charge); 138 | } 139 | } 140 | normal = normalize(normal); 141 | return true; 142 | } 143 | // Take larger steps the lower our step charge 144 | // (the farther we are from the meta surface) 145 | float step = step_charge; 146 | cur_t += 0.5;//1.0 - 0.9 * step*step*step * (step * (step * 6.0 - 15.0) + 10.0); 147 | } 148 | return false; 149 | } 150 | 151 | vec3 ambient = vec3(0.025, 0.025, 0.05); 152 | 153 | vec3 colourForIntersection(in vec3 rd, in vec3 pos, in vec3 normal) { 154 | vec3 light_pos = vec3(0, 0, -600); 155 | vec3 light_dir = normalize(light_pos - pos); 156 | 157 | vec3 colour = ambient; 158 | 159 | vec3 l_pos, l_normal; 160 | if (!findIntersection(light_pos, -1 * light_dir, l_pos, l_normal) 161 | || distance(light_pos, l_pos) >= distance(light_pos, pos) - 6) { 162 | float shininess = 200.0; 163 | 164 | float lambertian = max(dot(light_dir, normal), 0.0); 165 | float specular = 0.0; 166 | if (lambertian > 0.0) { 167 | vec3 refl_dir = reflect(-light_dir, normal); 168 | float spec_angle = max(dot(refl_dir, -rd), 0.0); 169 | specular = pow(spec_angle, shininess/4.0); 170 | } 171 | 172 | colour += lambertian * vec3(0.25, 0.25, 0.5) 173 | + specular * vec3(1.0, 1.0, 1.0); 174 | } 175 | return colour; 176 | } 177 | 178 | void main() 179 | { 180 | vec3 pixel_in_world = (vec4(gl_FragCoord.xy, 0, 1) * camera_matrix).xyz; 181 | 182 | vec3 ro = camera_origin; 183 | vec3 rd = normalize(pixel_in_world - camera_origin); 184 | 185 | vec3 final_colour = vec3(0.0); 186 | float colour_frac = 1.0; 187 | for (int i = 0; i < MAX_REFLECTIONS + 1; ++i) { 188 | vec3 pos, normal; 189 | if (!findIntersection(ro, rd, pos, normal)) { 190 | break; 191 | } 192 | 193 | vec3 colour = colourForIntersection(rd, pos, normal); 194 | final_colour += colour * colour_frac; 195 | 196 | // Set parameters for next iteration. 197 | colour_frac = colour_frac * 0.2; 198 | ro = pos + normal; 199 | rd = normalize(reflect(rd, normal)); 200 | } 201 | gl_FragColor = vec4(final_colour, 1); 202 | } -------------------------------------------------------------------------------- /Metaballs/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 Owen Glofcheski 3 | * 4 | * This software is provided 'as-is', without any express or implied 5 | * warranty. In no event will the authors be held liable for any damages 6 | * arising from the use of this software. 7 | * 8 | * Permission is granted to anyone to use this software for any purpose, 9 | * including commercial applications, and to alter it and redistribute it 10 | * freely, subject to the following restrictions: 11 | * 12 | * 1. The origin of this software must not be misrepresented; you must not 13 | * claim that you wrote the original software. If you use this software 14 | * in a product, an acknowledgment in the product documentation would be 15 | * appreciated but is not required. 16 | * 17 | * 2. Altered source versions must be plainly marked as such, and must not 18 | * be misrepresented as being the original software. 19 | * 20 | * 3. This notice may not be removed or altered from any source 21 | * distribution. 22 | */ 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #include "Utility\algebra.hpp" 30 | #include "Utility\gl.hpp" 31 | #include "Utility\quaternion.hpp" 32 | 33 | /** If you change this value, change it in the shaders as well. **/ 34 | const static int MAX_CHARGES = 30; 35 | 36 | enum RenderMode 37 | { 38 | RENDER_MODE_2D, 39 | RENDER_MODE_3D 40 | }; 41 | 42 | struct Charge 43 | { 44 | Point3 center; 45 | Vector3 velocity; 46 | 47 | int radius = 1; 48 | }; 49 | 50 | struct MetaballShader2D : public Shader 51 | { 52 | Uniform charges_uniform = -1; 53 | Uniform charge_count_uniform = -1; 54 | 55 | Uniform threshold_uniform = -1; 56 | }; 57 | 58 | struct MetaballShader3D : public Shader 59 | { 60 | Uniform charges_uniform = -1; 61 | Uniform charge_count_uniform = -1; 62 | 63 | Uniform threshold_uniform = -1; 64 | 65 | Uniform camera_origin_uniform = -1; 66 | Uniform camera_matrix_uniform = -1; 67 | }; 68 | 69 | struct State 70 | { 71 | int window = 0; 72 | 73 | bool paused = false; 74 | 75 | RenderMode render_mode = RENDER_MODE_3D; 76 | MetaballShader2D shader_2d; 77 | MetaballShader3D shader_3d; 78 | 79 | Charge charges[MAX_CHARGES]; 80 | int active_charges = 0; 81 | 82 | float threshold = 1000; 83 | 84 | Quaternion rotation; 85 | 86 | Size3 bounding_box = Size3(250, 150, 250); 87 | }; 88 | 89 | State state; 90 | 91 | void update() { 92 | // TODO(orglofch): Sort the charges in some fashion so the shader 93 | // can shortcut some work. 94 | 95 | // Move the charges. 96 | for (int i = 0; i < state.active_charges; ++i) { 97 | Charge &charge = state.charges[i]; 98 | charge.center += charge.velocity; 99 | if (charge.center.x < -state.bounding_box.width / 2) { 100 | charge.center.x = -state.bounding_box.width / 2; 101 | charge.velocity.x *= -0.99; 102 | } else if (charge.center.x > state.bounding_box.width / 2) { 103 | charge.center.x = state.bounding_box.width / 2; 104 | charge.velocity.x *= -0.99; 105 | } 106 | if (charge.center.y < -state.bounding_box.height / 2) { 107 | charge.center.y = -state.bounding_box.height / 2; 108 | charge.velocity.y *= -0.99; 109 | } else if (charge.center.y > state.bounding_box.height / 2) { 110 | charge.center.y = state.bounding_box.height / 2; 111 | charge.velocity.y *= -0.99; 112 | } 113 | if (charge.center.z < -state.bounding_box.depth / 2) { 114 | charge.center.z = -state.bounding_box.depth / 2; 115 | charge.velocity.z *= -0.99; 116 | } else if (charge.center.z > state.bounding_box.depth / 2) { 117 | charge.center.z = state.bounding_box.depth / 2; 118 | charge.velocity.z *= -0.99; 119 | } 120 | } 121 | state.rotation *= Quaternion(0.0, 0.002, 0.0, 1.0); 122 | } 123 | 124 | void render() { 125 | glClear(GL_COLOR_BUFFER_BIT); 126 | 127 | // Serialize the charges. 128 | GLfloat *charge_data = new GLfloat[state.active_charges * 4]; 129 | for (int i = 0; i < state.active_charges; ++i) { 130 | const Charge &charge = state.charges[i]; 131 | int index = i * 4; 132 | charge_data[index + 0] = charge.center.x; 133 | charge_data[index + 1] = charge.center.y; 134 | charge_data[index + 2] = charge.center.z; 135 | charge_data[index + 3] = charge.radius; 136 | } 137 | 138 | if (state.render_mode == RENDER_MODE_2D) { 139 | glUseProgram(state.shader_2d.program); 140 | glUniform4fv(state.shader_2d.charges_uniform, state.active_charges, charge_data); 141 | glUniform1i(state.shader_2d.charge_count_uniform, state.active_charges); 142 | glUniform1f(state.shader_2d.threshold_uniform, state.threshold); 143 | } else if (state.render_mode == RENDER_MODE_3D) { 144 | glUseProgram(state.shader_3d.program); 145 | glUniform4fv(state.shader_3d.charges_uniform, state.active_charges, charge_data); 146 | glUniform1i(state.shader_3d.charge_count_uniform, state.active_charges); 147 | glUniform1f(state.shader_3d.threshold_uniform, state.threshold); 148 | 149 | Point3 eye = state.rotation.matrix() * Point3(0, 0, -400); 150 | 151 | GLfloat origin[3]; 152 | for (int i = 0; i < 3; ++i) { 153 | origin[i] = eye.d[i]; 154 | } 155 | glUniform3fv(state.shader_3d.camera_origin_uniform, 1, origin); 156 | 157 | Vector3 view = Point3(0, 0, 0) - eye; 158 | view.normalize(); 159 | Vector3 up(0, 1, 0); 160 | 161 | double d = view.length(); 162 | double h = 2.0 * d * tan(toRad(60 /* fov */) / 2.0); 163 | Matrix4x4 t1 = Matrix4x4::translation(-640, -360, d); 164 | Matrix4x4 s2 = Matrix4x4::scaling(-h / 720, -h / 720, 1.0); 165 | Matrix4x4 r3 = Matrix4x4::rotation(eye, view, up); 166 | Matrix4x4 t4 = Matrix4x4::translation(eye - Point3(0, 0, 0)); 167 | Matrix4x4 camera_matrix = t4 * r3 * s2 * t1; 168 | GLfloat camera_data[16]; 169 | for (int i = 0; i < 16; ++i) { 170 | camera_data[i] = camera_matrix.d[i]; 171 | } 172 | glUniformMatrix4fv(state.shader_3d.camera_matrix_uniform, 1, false, camera_data); 173 | } 174 | 175 | delete[] charge_data; 176 | glDrawRect(-1, 1, -1, 1, 0); 177 | 178 | glutSwapBuffers(); 179 | glutPostRedisplay(); 180 | } 181 | 182 | void tick() { 183 | if (!state.paused) { 184 | update(); 185 | } 186 | render(); 187 | } 188 | 189 | void handleMouseButton(int button, int button_state, int x, int y) { 190 | switch (button) { 191 | case GLUT_LEFT_BUTTON: 192 | break; 193 | case GLUT_RIGHT_BUTTON: 194 | state.paused = !state.paused; 195 | break; 196 | } 197 | } 198 | 199 | void handlePressNormalKeys(unsigned char key, int x, int y) { 200 | switch (key) { 201 | case 'e': 202 | case 'E': 203 | state.render_mode = static_cast(!state.render_mode); 204 | break; 205 | case 'q': 206 | case 'Q': 207 | case 27: 208 | exit(EXIT_SUCCESS); 209 | } 210 | } 211 | 212 | void handleReleaseNormalKeys(unsigned char key, int x, int y) { 213 | } 214 | 215 | void handlePressSpecialKey(int key, int x, int y) { 216 | } 217 | 218 | void handleReleaseSpecialKey(int key, int x, int y) { 219 | } 220 | 221 | 222 | void init() { 223 | glBlendFunc(GL_SRC_ALPHA, GL_ONE); 224 | glDisable(GL_NORMALIZE); 225 | 226 | glClearColor(0.0, 0.0, 0.0, 0.0); 227 | glClear(GL_COLOR_BUFFER_BIT); 228 | 229 | glutDisplayFunc(tick); 230 | 231 | glutIgnoreKeyRepeat(1); 232 | glutMouseFunc(handleMouseButton); 233 | glutKeyboardFunc(handlePressNormalKeys); 234 | glutKeyboardUpFunc(handleReleaseNormalKeys); 235 | glutSpecialFunc(handlePressSpecialKey); 236 | glutSpecialUpFunc(handleReleaseSpecialKey); 237 | 238 | state.shader_2d.program = glLoadShader("pass_through.vert", "metaball_shader_2d.frag"); 239 | state.shader_2d.charges_uniform = glGetUniform(state.shader_2d, "charges"); 240 | state.shader_2d.charge_count_uniform = glGetUniform(state.shader_2d, "charge_count"); 241 | state.shader_2d.threshold_uniform = glGetUniform(state.shader_2d, "threshold"); 242 | 243 | state.shader_3d.program = glLoadShader("metaball_shader_3d.vert", "metaball_shader_3d.frag"); 244 | state.shader_3d.charges_uniform = glGetUniform(state.shader_3d, "charges"); 245 | state.shader_3d.charge_count_uniform = glGetUniform(state.shader_3d, "charge_count"); 246 | state.shader_3d.threshold_uniform = glGetUniform(state.shader_3d, "threshold"); 247 | state.shader_3d.camera_origin_uniform = glGetUniform(state.shader_3d, "camera_origin"); 248 | state.shader_3d.camera_matrix_uniform = glGetUniform(state.shader_3d, "camera_matrix"); 249 | } 250 | 251 | void cleanup() { 252 | } 253 | 254 | int main(int argc, char **argv) { 255 | srand((unsigned int)time(NULL)); 256 | 257 | for (int i = 0; i < MAX_CHARGES; ++i) { 258 | state.charges[i].center.x = Randf(-state.bounding_box.width / 2, state.bounding_box.width / 2); 259 | state.charges[i].center.y = Randf(-state.bounding_box.height / 2, state.bounding_box.height / 2); 260 | state.charges[i].center.z = Randf(-state.bounding_box.depth / 2, state.bounding_box.depth / 2); 261 | state.charges[i].velocity.x = Randf(-3, 3); 262 | state.charges[i].velocity.y = Randf(-3, 3); 263 | state.charges[i].velocity.z = Randf(-3, 3); 264 | state.charges[i].radius = Randf(10, 90); 265 | } 266 | state.active_charges = MAX_CHARGES; 267 | 268 | glutInit(&argc, argv); 269 | glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA); 270 | glutInitWindowPosition(100, 100); 271 | glutInitWindowSize(1280, 720); 272 | state.window = glutCreateWindow("Metaballs"); 273 | glewInit(); 274 | 275 | init(); 276 | 277 | glutMainLoop(); 278 | 279 | cleanup(); 280 | } --------------------------------------------------------------------------------