Skip to content

Instantly share code, notes, and snippets.

@salt-die
Last active January 2, 2022 23:20
Show Gist options
  • Save salt-die/1e4a9d58934868ffafa87a672662f7fe to your computer and use it in GitHub Desktop.
Save salt-die/1e4a9d58934868ffafa87a672662f7fe to your computer and use it in GitHub Desktop.
poke metaballs with this metaball shader written for kivy
"""Scroll with the mouse to enable auto-poking."""
from random import random
from kivy.app import App
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.lang import Builder
from kivy.uix.effectwidget import AdvancedEffectBase
FRICTION = .97
MAX_V = .02
POKE = 100
SHADER = AdvancedEffectBase()
SHADER.glsl = """
uniform vec2 ball_pos[16];
vec4 new_color;
float current_falloff;
float total_falloff;
float r;
float rr;
float rrr;
const float threshold = .0016; // tuning this up will give more well-defined balls, try .5
const float radius = .4; // size of metaballs
// falloff function can be any piecewise continuous function that decreases as distance from ball increases
float falloff(int ball, vec2 pos){
r = distance(ball_pos[ball], pos);
rr = ((radius / 5.0) * sin(time) * sin(time)) + 4.0 * radius / 5.0; // change the radius of the balls over time
rrr = r / rr;
if (r < rr / 3.0) current_falloff = 1.0 - 3.0 * rrr * rrr;
else {
if (r < rr) current_falloff = 1.5 * (1.0 - rrr) * (1.0 - rrr);
else current_falloff = 0.0;}
return current_falloff;}
vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords){
total_falloff = 0.0;
for (int i = 0; i < 16; i++){
total_falloff += falloff(i, tex_coords);}
if (total_falloff > threshold){
// colorings are just random functions until i found something that i liked
new_color.r = clamp(total_falloff * total_falloff + sin(time), 0.0, 1.0);
new_color.g = clamp(sin(1.414213 * time) / total_falloff, 0.0, 1.0);
new_color.b = clamp(sin(1.732050 * time) / total_falloff, 0.0, 1.0);
new_color.a = clamp(total_falloff - .1, 0.0, 1.0);}
else new_color = vec4(0.0, 0.0, 0.0, 0.0);
return new_color;}
"""
SHADER.uniforms = {}
KV = """
#:import SHADER __main__.SHADER
EffectWidget:
effects: SHADER,
"""
class Ball:
vel = 0j
_auto = False
def __init__(self):
self.xy = random() + random() * 1j
@property
def auto(self):
return self._auto
@auto.setter
def auto(self, is_auto):
self._auto = is_auto
if is_auto:
self.vel = complex(random(), random()) * 2 - 1 - 1j
def update(self):
vel_mag = abs(self.vel)
cond, reverse, friction = (vel_mag != MAX_V, -random(), 1) if self.auto else (vel_mag > MAX_V, -1, FRICTION)
if cond: # Normalize velocity if self.auto else clip it
normal = vel_mag / MAX_V
self.vel /= normal
# Apply velocity, bouncing off walls
self.xy += self.vel
if not 0 < self.xy.real < 1:
self.vel = complex(reverse * self.vel.real, self.vel.imag)
self.xy += self.vel.real
if not 0 < self.xy.imag < 1:
self.vel = complex(self.vel.real, reverse * self.vel.imag)
self.xy += self.vel.imag * 1j
# Apply friction
self.vel *= friction
class MeatBalls(App):
auto = False
def build(self):
self.meatballs = [Ball() for _ in range(16)]
Window.bind(on_touch_down=self._on_touch_down, on_touch_move=self._on_touch_down)
Clock.schedule_interval(self.update, 0)
return Builder.load_string(KV)
def update(self, dt):
for ball in self.meatballs:
ball.update()
SHADER.uniforms['ball_pos'] = [[ball.xy.real, ball.xy.imag] for ball in self.meatballs]
def poke(self, tx, ty):
for ball in self.meatballs:
dxy = ball.xy - complex(tx, ty)
distance = POKE * abs(dxy)**2
if distance:
ball.vel += dxy / distance
def _on_touch_down(self, _, touch):
if touch.is_mouse_scrolling:
self.auto = not self.auto
for ball in self.meatballs:
ball.auto = self.auto
else:
self.poke(touch.sx, touch.sy)
if __name__ == '__main__':
MeatBalls().run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment