├── .gitignore
├── README.md
├── UNLICENSE
├── applet.html
├── build.xml
├── checkstyle.xml
├── ivy.xml
├── src
└── liquid
│ ├── Bottle.java
│ ├── Controls.java
│ ├── Launcher.java
│ ├── LiquidApplet.java
│ ├── Recorder.java
│ ├── Viewer.java
│ └── package-info.java
└── webgl
├── index.html
├── lib
├── box2d.js
└── igloo-0.0.1.js
├── liquid.css
└── src
├── ball.frag
├── ball.vert
├── blur.frag
├── bottle.js
├── color.frag
├── fps.js
├── identity.vert
├── liquid.js
├── threshold.frag
└── utility.js
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | dist
3 | cache.properties
4 | TAGS
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Liquid Simulation
2 |
3 | Simulates a simple 2D liquid using Box2D and a drawing trick.
4 |
5 | 
6 |
7 | The following two articles explain how it works:
8 |
9 | * [Cartoon Liquid Simulation](http://nullprogram.com/blog/2013/06/26/)
10 | * [Liquid Simulation in WebGL](http://nullprogram.com/blog/2012/02/03/)
11 |
12 | There are two versions in this repository: a slow CPU-only Java
13 | implementation and a faster GPU-powered WebGL version (under
14 | `webgl/`).
15 |
16 | * http://skeeto.github.io/fun-liquid/ (Java Applet)
17 | * http://skeeto.github.io/fun-liquid/webgl/ (WebGL)
18 |
--------------------------------------------------------------------------------
/UNLICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
--------------------------------------------------------------------------------
/applet.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Fun Liquid Simulation
5 |
30 |
31 |
32 | Cartoony Liquid Simulator
33 |
34 |
36 |
37 |
38 |
39 | The idea is simple:
40 |
41 |
42 | Do a physics simulation of balls in a container
43 | (JBox2D )
44 | Render the simulation onto a raster.
45 | Blur the image.
46 | Apply a threshold.
47 |
48 |
49 | What you're left with is a cartoon-ish, chunky liquid. Use the
50 | controls to see the components of this process.
51 |
52 |
53 | The most computationally intensive step is blurring the
54 | raster. Convolution is expensive, O(n^2), and it's being done by
55 | the CPU here, rather than the GPU. To run at full speed, you
56 | need a nice computer.
57 |
58 |
59 | See the Git
60 | repository for full source code listing.
61 |
62 |
63 | Original idea:
64 |
65 | How to simulate liquid
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/build.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
42 |
44 |
45 |
46 |
47 |
48 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
58 |
59 |
60 |
62 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
87 |
89 |
90 |
91 |
92 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/checkstyle.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
32 |
33 |
34 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
--------------------------------------------------------------------------------
/ivy.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
13 |
15 |
17 |
18 |
19 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/liquid/Bottle.java:
--------------------------------------------------------------------------------
1 | package liquid;
2 |
3 | import java.awt.geom.Rectangle2D;
4 | import java.util.Observable;
5 | import java.util.Random;
6 | import java.util.concurrent.Executors;
7 | import java.util.concurrent.ScheduledExecutorService;
8 | import java.util.concurrent.TimeUnit;
9 | import lombok.Getter;
10 | import lombok.extern.java.Log;
11 | import org.jbox2d.collision.shapes.CircleShape;
12 | import org.jbox2d.collision.shapes.PolygonShape;
13 | import org.jbox2d.common.Vec2;
14 | import org.jbox2d.dynamics.Body;
15 | import org.jbox2d.dynamics.BodyDef;
16 | import org.jbox2d.dynamics.BodyType;
17 | import org.jbox2d.dynamics.FixtureDef;
18 | import org.jbox2d.dynamics.World;
19 |
20 | /**
21 | * A simulated bottle containing a chunky liquid (large solid particles).
22 | */
23 | @Log
24 | public class Bottle extends Observable {
25 |
26 | /* Solver */
27 | private static final int FPS = 30;
28 | private static final int V_ITERATIONS = 8;
29 | private static final int P_ITERATIONS = 3;
30 | private static final double MILLIS = 1000.0;
31 |
32 | /* World */
33 | private static final float WIDTH = 50f;
34 | private static final float HEIGHT = 70f;
35 | private static final float THICKNESS = 0.1f;
36 | private static final Vec2 GRAVITY = new Vec2(0, -60f);
37 | private static final Rectangle2D VIEW =
38 | new Rectangle2D.Float(-WIDTH / 2, -HEIGHT / 2, WIDTH, HEIGHT);
39 | private static final double FLIP_RATE = 3.5; // seconds
40 |
41 | /* Balls */
42 | private static final int BALLS = 400;
43 | private static final float BALL_RADIUS = 0.5f;
44 | private static final float BALL_DENSITY = 1f;
45 | private static final float BALL_FRICTION = 0f;
46 | private static final float BALL_RESTITUTION = 0.3f;
47 |
48 | private static final float SPIKE_THICKNESS = 12f;
49 | private static final float SPIKE_EXTENT = 20f;
50 |
51 | @Getter private final World world;
52 | @Getter private double time = 0; // World time
53 | private boolean running = false;
54 | private static final ScheduledExecutorService EXEC =
55 | Executors.newSingleThreadScheduledExecutor();
56 |
57 | /**
58 | * Create a new bottle.
59 | */
60 | public Bottle() {
61 | world = new World(GRAVITY, false);
62 | /* Set up the containment box. */
63 | buildContainer();
64 |
65 | /* Add a ball. */
66 | Random rng = new Random();
67 | for (int i = 0; i < BALLS; i++) {
68 | addBall((rng.nextFloat() - 0.5f) * (WIDTH - BALL_RADIUS),
69 | (rng.nextFloat() - 0.5f) * (HEIGHT - BALL_RADIUS));
70 | }
71 | addSpike(SPIKE_EXTENT, 0, 1);
72 | addSpike(-SPIKE_EXTENT, 0, -1);
73 | EXEC.scheduleAtFixedRate(new Runnable() {
74 | public void run() {
75 | if (running) {
76 | world.step(1f / FPS, V_ITERATIONS, P_ITERATIONS);
77 | time += 1.0 / FPS;
78 | setChanged();
79 | notifyObservers();
80 | if (Math.sin(time / FLIP_RATE * Math.PI) < 0) {
81 | world.setGravity(GRAVITY.negate());
82 | } else {
83 | world.setGravity(GRAVITY);
84 | }
85 | }
86 | }
87 | }, 0L, (long) (MILLIS / FPS), TimeUnit.MILLISECONDS);
88 | }
89 |
90 | /**
91 | * Run the simulation.
92 | */
93 | public final void start() {
94 | running = true;
95 | }
96 |
97 | /**
98 | * Stop the simulation, which can be restarted again.
99 | */
100 | public final void stop() {
101 | running = false;
102 | }
103 |
104 | /**
105 | * Return true if the simulation is running.
106 | * @return true if the simulation is running
107 | */
108 | public final boolean isRunning() {
109 | return running;
110 | }
111 |
112 | /**
113 | * Specify the area of interest for this world.
114 | * @return a rectangle specifying where things are happening
115 | */
116 | public final Rectangle2D getView() {
117 | return VIEW;
118 | }
119 |
120 | /**
121 | * Build the world container.
122 | */
123 | private void buildContainer() {
124 | BodyDef def = new BodyDef();
125 | PolygonShape box = new PolygonShape();
126 | Body side;
127 |
128 | def.position = new Vec2(WIDTH / 2, 0);
129 | box.setAsBox(THICKNESS / 2, HEIGHT / 2);
130 | world.createBody(def).createFixture(box, 0f);
131 |
132 | def.position = new Vec2(-WIDTH / 2, 0);
133 | box.setAsBox(THICKNESS / 2, HEIGHT / 2);
134 | world.createBody(def).createFixture(box, 0f);
135 |
136 | def.position = new Vec2(0, HEIGHT / 2);
137 | box.setAsBox(WIDTH / 2, THICKNESS / 2);
138 | world.createBody(def).createFixture(box, 0f);
139 |
140 | def.position = new Vec2(0, -HEIGHT / 2);
141 | box.setAsBox(WIDTH / 2, THICKNESS / 2);
142 | world.createBody(def).createFixture(box, 0f);
143 | }
144 |
145 | /**
146 | * Add a new ball body to the world.
147 | * @param x the x-coordinate of the ball
148 | * @param y the y-coordinate of the ball
149 | */
150 | private void addBall(final float x, final float y) {
151 | BodyDef def = new BodyDef();
152 | def.position = new Vec2(x, y);
153 | def.type = BodyType.DYNAMIC;
154 | CircleShape circle = new CircleShape();
155 | circle.m_radius = BALL_RADIUS;
156 | FixtureDef mass = new FixtureDef();
157 | mass.shape = circle;
158 | mass.density = BALL_DENSITY;
159 | mass.friction = BALL_FRICTION;
160 | mass.restitution = BALL_RESTITUTION;
161 | world.createBody(def).createFixture(mass);
162 | }
163 |
164 | /**
165 | * Add a static spike to the bottle.
166 | * @param x x-position of the point
167 | * @param y y-position of the point
168 | * @param dir direction of the point
169 | */
170 | private void addSpike(final float x, final float y, final int dir) {
171 | BodyDef def = new BodyDef();
172 | def.position = new Vec2(x, y);
173 | PolygonShape shape = new PolygonShape();
174 | Vec2[] vecs = new Vec2[3];
175 | int side = 1;
176 | vecs[0] = new Vec2(dir * WIDTH / 2 - x, dir * SPIKE_THICKNESS / 2f);
177 | vecs[1] = new Vec2(0, 0);
178 | vecs[2] = new Vec2(dir * WIDTH / 2 - x, dir * -SPIKE_THICKNESS / 2f);
179 | shape.set(vecs, vecs.length);
180 | FixtureDef fix = new FixtureDef();
181 | fix.shape = shape;
182 | fix.density = 0f;
183 | fix.friction = 0f;
184 | world.createBody(def).createFixture(fix);
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/src/liquid/Controls.java:
--------------------------------------------------------------------------------
1 | package liquid;
2 |
3 | import java.awt.GridLayout;
4 | import java.awt.event.ActionEvent;
5 | import java.awt.event.ActionListener;
6 | import javax.swing.BorderFactory;
7 | import javax.swing.JButton;
8 | import javax.swing.JCheckBox;
9 | import javax.swing.JPanel;
10 | import lombok.val;
11 |
12 | /**
13 | * Control panel for a bottle and viewer.
14 | */
15 | public final class Controls extends JPanel {
16 |
17 | private static final long serialVersionUID = 1L;
18 | private static final int GAP = 10;
19 |
20 | private final Bottle bottle;
21 | private final Viewer viewer;
22 |
23 | /**
24 | * Create a new control panel for the given bottle and viewer.
25 | * @param bottle the bottle to be controlled
26 | * @param viewer the viewer to be controlled
27 | */
28 | public Controls(final Bottle bottle, final Viewer viewer) {
29 | this.bottle = bottle;
30 | this.viewer = viewer;
31 |
32 | val layout = new GridLayout(2, 2);
33 | layout.setHgap(GAP);
34 | layout.setVgap(GAP);
35 | setLayout(layout);
36 | setBorder(BorderFactory.createEmptyBorder(0, GAP, GAP, GAP));
37 |
38 | val threshold = new JCheckBox("Threshold", true);
39 | threshold.addActionListener(new ActionListener() {
40 | public void actionPerformed(final ActionEvent e) {
41 | viewer.setThreshold(threshold.isSelected());
42 | }
43 | });
44 | add(threshold);
45 |
46 | val blur = new JCheckBox("Blur", true);
47 | blur.addActionListener(new ActionListener() {
48 | public void actionPerformed(final ActionEvent e) {
49 | viewer.setBlur(blur.isSelected());
50 | threshold.setEnabled(blur.isSelected());
51 | }
52 | });
53 | add(blur);
54 |
55 | val pause = new JButton("Pause");
56 | pause.addActionListener(new ActionListener() {
57 | public void actionPerformed(final ActionEvent e) {
58 | if (bottle.isRunning()) {
59 | bottle.stop();
60 | pause.setText("Play");
61 | } else {
62 | bottle.start();
63 | pause.setText("Pause");
64 | }
65 | }
66 | });
67 | add(pause);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/liquid/Launcher.java:
--------------------------------------------------------------------------------
1 | package liquid;
2 |
3 | import com.beust.jcommander.JCommander;
4 | import com.beust.jcommander.Parameter;
5 | import com.beust.jcommander.ParameterException;
6 | import javax.swing.BoxLayout;
7 | import javax.swing.JFrame;
8 | import lombok.extern.java.Log;
9 | import lombok.val;
10 |
11 | /**
12 | * Sets up the environment and drives the simulation forward.
13 | */
14 | @Log
15 | public final class Launcher {
16 |
17 | @Parameter(names = "-record", description = "Record the simulation.")
18 | private boolean record;
19 |
20 | /**
21 | * Private constructor.
22 | */
23 | private Launcher() {
24 | }
25 |
26 | /**
27 | * The main method, application entry point.
28 | * @param args command line arguments
29 | */
30 | public static void main(final String[] args) {
31 | try {
32 | /* Fix for poor OpenJDK performance. */
33 | System.setProperty("sun.java2d.pmoffscreen", "false");
34 | } catch (java.security.AccessControlException e) {
35 | log.info("could not set sun.java2d.pmoffscreen");
36 | }
37 |
38 | /* Check the command line arguments. */
39 | val options = new Launcher();
40 | try {
41 | new JCommander(options, args);
42 | } catch (ParameterException e) {
43 | System.out.println("error: " + e.getMessage());
44 | new JCommander(options).usage();
45 | System.exit(-1);
46 | } catch (java.security.AccessControlException e) {
47 | log.warning("could not process arguments: " + e.getMessage());
48 | }
49 |
50 | /* Set up the frame. */
51 | JFrame frame = new JFrame("Fun Liquid");
52 | frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
53 | val layout = new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS);
54 | frame.setLayout(layout);
55 | val bottle = new Bottle();
56 | val viewer = new Viewer(bottle);
57 | frame.add(viewer);
58 | frame.add(new Controls(bottle, viewer));
59 | frame.setResizable(false);
60 | frame.pack();
61 | frame.setVisible(true);
62 | if (options.record) {
63 | new Recorder(viewer);
64 | }
65 |
66 | /* Begin the simulation. */
67 | bottle.start();
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/liquid/LiquidApplet.java:
--------------------------------------------------------------------------------
1 | package liquid;
2 |
3 | import javax.swing.BoxLayout;
4 | import javax.swing.JApplet;
5 | import lombok.val;
6 |
7 | /**
8 | * Run the simulation as an applet.
9 | */
10 | public class LiquidApplet extends JApplet {
11 | private static final long serialVersionUID = 1L;
12 |
13 | private Bottle bottle;
14 | private Viewer viewer;
15 |
16 | @Override
17 | public final void init() {
18 | bottle = new Bottle();
19 | viewer = new Viewer(bottle);
20 | val layout = new BoxLayout(getContentPane(), BoxLayout.Y_AXIS);
21 | setLayout(layout);
22 | add(viewer);
23 | add(new Controls(bottle, viewer));
24 | }
25 |
26 | @Override
27 | public final void start() {
28 | bottle.start();
29 | }
30 |
31 | @Override
32 | public final void stop() {
33 | bottle.stop();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/liquid/Recorder.java:
--------------------------------------------------------------------------------
1 | package liquid;
2 |
3 | import java.awt.image.BufferedImage;
4 | import java.io.File;
5 | import java.util.Observable;
6 | import java.util.Observer;
7 | import javax.imageio.ImageIO;
8 | import lombok.extern.java.Log;
9 | import lombok.val;
10 |
11 | /**
12 | * Latches onto a Viewer and records each frame of the
13 | * simulation. These frames can be later reassembled into a video
14 | * file.
15 | */
16 | @Log
17 | public class Recorder implements Observer {
18 |
19 | private final Viewer viewer;
20 | private long counter = 0;
21 | private static final String PREFIX = "frame-";
22 |
23 | /**
24 | * Make a new recorder that follows the given viewer.
25 | * @param viewer the viewer to record
26 | */
27 | public Recorder(final Viewer viewer) {
28 | this.viewer = viewer;
29 | viewer.getBottle().addObserver(this);
30 | }
31 |
32 | @Override
33 | public final void update(final Observable o, final Object arg) {
34 | val image = new BufferedImage(viewer.getWidth(), viewer.getHeight(),
35 | BufferedImage.TYPE_INT_RGB);
36 | val g = image.createGraphics();
37 | viewer.paintComponent(g);
38 | g.dispose();
39 | val file = new File(String.format("%s%08d.png", PREFIX, counter++));
40 | try {
41 | ImageIO.write(image, "PNG", file);
42 | } catch (java.io.IOException e) {
43 | log.warning("failed to write " + file + ": " + e.getMessage());
44 | } catch (java.security.AccessControlException e) {
45 | log.warning("failed to write " + file + ": " + e.getMessage());
46 | viewer.getBottle().deleteObserver(this);
47 | log.info("unsubscribed");
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/liquid/Viewer.java:
--------------------------------------------------------------------------------
1 | package liquid;
2 |
3 | import java.awt.Color;
4 | import java.awt.Dimension;
5 | import java.awt.Graphics2D;
6 | import java.awt.Graphics;
7 | import java.awt.RenderingHints;
8 | import java.awt.geom.AffineTransform;
9 | import java.awt.geom.Ellipse2D;
10 | import java.awt.geom.Path2D;
11 | import java.awt.image.BufferedImage;
12 | import java.awt.image.BufferedImageOp;
13 | import java.awt.image.ConvolveOp;
14 | import java.awt.image.Kernel;
15 | import java.util.Observable;
16 | import java.util.Observer;
17 | import javax.swing.JComponent;
18 | import lombok.Getter;
19 | import lombok.val;
20 | import org.jbox2d.collision.shapes.CircleShape;
21 | import org.jbox2d.collision.shapes.PolygonShape;
22 | import org.jbox2d.collision.shapes.Shape;
23 | import org.jbox2d.common.Vec2;
24 | import org.jbox2d.dynamics.Body;
25 | import org.jbox2d.dynamics.BodyType;
26 | import org.jbox2d.dynamics.Fixture;
27 |
28 | /**
29 | * Displays a view of a JBox2D world.
30 | */
31 | public class Viewer extends JComponent implements Observer {
32 |
33 | private static final Color BACKGROUND = Color.BLACK;
34 | private static final Color FOREGROUND = Color.WHITE;
35 | private static final Color STATIC = Color.GRAY;
36 | private static final int KERNEL_SIZE = 12;
37 | private static final int THRESHOLD = 28 * 3;
38 |
39 | private static final long serialVersionUID = 1L;
40 |
41 | private static final float SCALE = 5f;
42 |
43 | @Getter private final Bottle bottle;
44 |
45 | private boolean blur = true;
46 | private boolean threshold = true;
47 |
48 | private final Kernel vkernel;
49 | private final Kernel hkernel;
50 |
51 | /**
52 | * Create a display of a world at a given location.
53 | * @param bottle the bottle to be displayed
54 | */
55 | public Viewer(final Bottle bottle) {
56 | this.bottle = bottle;
57 | val view = bottle.getView();
58 | Dimension size = new Dimension((int) (view.getWidth() * SCALE),
59 | (int) (view.getHeight() * SCALE));
60 | setPreferredSize(size);
61 | vkernel = makeKernel(KERNEL_SIZE, true);
62 | hkernel = makeKernel(KERNEL_SIZE, false);
63 | bottle.addObserver(this);
64 | }
65 |
66 | @Override
67 | public final void update(final Observable o, final Object arg) {
68 | repaint();
69 | }
70 |
71 | /**
72 | * Turn blurring on or off.
73 | * @param set the new value
74 | */
75 | public final void setBlur(final boolean set) {
76 | blur = set;
77 | repaint();
78 | }
79 |
80 | /**
81 | * Turn thresholding on or off.
82 | * @param set the new value
83 | */
84 | public final void setThreshold(final boolean set) {
85 | threshold = set;
86 | repaint();
87 | }
88 |
89 | @Override
90 | public final void paintComponent(final Graphics graphics) {
91 | Graphics2D g = (Graphics2D) graphics;
92 | if (!blur) {
93 | g.setColor(BACKGROUND);
94 | g.fillRect(0, 0, getWidth(), getHeight());
95 | draw((Graphics2D) g.create(), getWidth(), getHeight(), true,
96 | BodyType.DYNAMIC, FOREGROUND);
97 | } else {
98 | Dimension size = getPreferredSize();
99 | BufferedImage work;
100 | work = new BufferedImage(size.width + KERNEL_SIZE * 2,
101 | size.height + KERNEL_SIZE * 2,
102 | BufferedImage.TYPE_INT_RGB);
103 | Graphics2D wg = work.createGraphics();
104 | wg.setColor(BACKGROUND);
105 | wg.fillRect(0, 0, work.getWidth(), work.getHeight());
106 | draw(wg, work.getWidth(), work.getHeight(), false,
107 | BodyType.DYNAMIC, FOREGROUND);
108 | wg.dispose();
109 |
110 | /* Blur. */
111 | BufferedImageOp op = new ConvolveOp(vkernel);
112 | BufferedImage conv = op.filter(work, null);
113 | op = new ConvolveOp(hkernel);
114 | conv = op.filter(conv, null);
115 | /* Threshold. */
116 | if (threshold) {
117 | threshold(conv);
118 | }
119 | /* Draw the result. */
120 | g.drawImage(conv, -KERNEL_SIZE, -KERNEL_SIZE, null);
121 | }
122 | draw(g, getWidth(), getHeight(), true,
123 | BodyType.STATIC, STATIC);
124 | }
125 |
126 | /**
127 | * Draw the world onto the given graphics.
128 | * @param g the graphics context to use
129 | * @param width width of the drawing context
130 | * @param height height of the drawing context
131 | * @param aa enable anti-aliasing
132 | * @param type the type of body to draw
133 | * @param color the color to draw the bodies
134 | */
135 | private void draw(final Graphics2D g, final int width, final int height,
136 | final boolean aa,
137 | final BodyType type, final Color color) {
138 | /* Set up coordinate system. */
139 | g.translate(width / 2, height / 2);
140 | g.scale(SCALE, -SCALE);
141 |
142 | if (aa) {
143 | /* Configure rendering options. */
144 | g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
145 | RenderingHints.VALUE_ANTIALIAS_ON);
146 | }
147 |
148 | /* Draw each body. */
149 | g.setColor(color);
150 | Body body = bottle.getWorld().getBodyList();
151 | while (body != null) {
152 | Vec2 pos = body.getPosition();
153 | float angle = body.getAngle();
154 | Fixture fixture = body.getFixtureList();
155 | while (body.m_type == type && fixture != null) {
156 | Shape shape = fixture.getShape();
157 | if (shape instanceof CircleShape) {
158 | draw(g, pos, (CircleShape) shape);
159 | } else if (shape instanceof PolygonShape) {
160 | draw(g, pos, angle, (PolygonShape) shape);
161 | } else {
162 | System.out.println("Cannot draw shape: " + shape);
163 | }
164 | fixture = fixture.getNext();
165 | }
166 | body = body.getNext();
167 | }
168 | }
169 |
170 | /**
171 | * Draw a circle shape from the world.
172 | * @param g the graphics context to use
173 | * @param pos position of the shape
174 | * @param s the circle to be drawn
175 | */
176 | private void draw(final Graphics2D g, final Vec2 pos, final CircleShape s) {
177 | Ellipse2D circle = new Ellipse2D.Float(pos.x - s.m_radius,
178 | pos.y - s.m_radius,
179 | s.m_radius * 2, s.m_radius * 2);
180 | g.fill(circle);
181 | }
182 |
183 | /**
184 | * Draw a polygon shape from the world.
185 | * @param g the graphics context to use
186 | * @param pos position of the shape
187 | * @param angle the rotation of the shape
188 | * @param s the polygon to be drawn
189 | */
190 | private void draw(final Graphics2D g, final Vec2 pos,
191 | final float angle, final PolygonShape s) {
192 | Path2D path = new Path2D.Float();
193 | Vec2 first = s.getVertex(0);
194 | path.moveTo(first.x, first.y);
195 | for (int i = 1; i < s.getVertexCount(); i++) {
196 | Vec2 v = s.getVertex(i);
197 | path.lineTo(v.x, v.y);
198 | }
199 | path.closePath();
200 | AffineTransform at = new AffineTransform();
201 | at.translate(pos.x, pos.y);
202 | at.rotate(angle);
203 | g.fill(at.createTransformedShape(path));
204 | }
205 |
206 | /**
207 | * Make a blur kernel.
208 | * @param size the size of the kernel
209 | * @param vertical make the kernel vertical or horizontal
210 | * @return the specified kernel
211 | */
212 | private static Kernel makeKernel(final int size, final boolean vertical) {
213 | float radius = size;
214 | int rows = size * 2 + 1;
215 | float[] matrix = new float[rows];
216 | float sigma = radius / 3;
217 | float sigma22 = 2 * sigma * sigma;
218 | float sigmaPi2 = 2 * (float) Math.PI * sigma;
219 | float sqrtSigmaPi2 = (float) Math.sqrt(sigmaPi2);
220 | float radius2 = radius * radius;
221 | float total = 0;
222 | int index = 0;
223 | for (int row = -size; row <= size; row++) {
224 | float distance = row * row;
225 | if (distance > radius2) {
226 | matrix[index] = 0;
227 | } else {
228 | matrix[index] = (float) Math.exp(-(distance) / sigma22)
229 | / sqrtSigmaPi2;
230 | }
231 | total += matrix[index];
232 | index++;
233 | }
234 | for (int i = 0; i < rows; i++) {
235 | matrix[i] /= total;
236 | }
237 |
238 | if (vertical) {
239 | return new Kernel(1, rows, matrix);
240 | } else {
241 | return new Kernel(rows, 1, matrix);
242 | }
243 | }
244 |
245 | /**
246 | * Threshold an image.
247 | * @param im the image to be thresholded
248 | */
249 | private void threshold(final BufferedImage im) {
250 | for (int i = 0; i < im.getWidth(); i++) {
251 | for (int j = 0; j < im.getHeight(); j++) {
252 | Color c = new Color(im.getRGB(i, j));
253 | if (c.getRed() + c.getGreen() + c.getBlue() > THRESHOLD) {
254 | im.setRGB(i, j, FOREGROUND.getRGB());
255 | } else {
256 | im.setRGB(i, j, BACKGROUND.getRGB());
257 | }
258 | }
259 | }
260 | }
261 | }
262 |
--------------------------------------------------------------------------------
/src/liquid/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Simple liquid animation using a phyisics engine (JBox2D).
3 | */
4 | package liquid;
5 |
--------------------------------------------------------------------------------
/webgl/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Fun Liquid
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | WebGL Liquid Simulator
16 |
17 |
18 |
19 |
20 |
21 | Do a physics simulation of balls in a container
22 | (box2d.js )
23 |
24 |
25 | Render the simulation onto a raster
26 |
27 |
28 | Blur the image
29 | (
30 | enable
31 | )
32 |
33 |
34 | Apply a threshold
35 | (
36 | enable
37 | )
38 |
39 |
40 |
41 | The physics simulation is computed on the CPU (Box2D) and the
42 | blurring and thresholding is handled by the GPU using WebGL.
43 |
44 |
45 | See the Git
46 | repository for full source code listing. The original idea
47 | came from here:
48 |
49 | How to simulate liquid
50 | .
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/webgl/lib/igloo-0.0.1.js:
--------------------------------------------------------------------------------
1 | var VecN=VecN||function(){};VecN.FIELDS="xyzwabcdefghijklmnopqrstuv".split("");VecN.make=function(c){if(c>VecN.FIELDS.length){throw new Error("VecN limited to "+VecN.FIELDS.length)}function d(q,i){var p=q.slice(0);p.push(i);p.unshift(null);var n=Function.bind.apply(Function,p);return new n()}var k=VecN.FIELDS.slice(0,c);var e=d(k,k.map(function(i){return"this."+i+" = "+i+";"}).join("\n"));e.prototype=Object.create(VecN.prototype);e.prototype.length=c;e.prototype.VecN=this;e.prototype.toString=d([],'return "[Vec'+c+' (" + '+k.map(function(i){return"this."+i}).join(' + ", " + ')+' + ")]";');function h(n,i){i=i||"this.constructor";return"return new "+i+"("+k.map(n).join(", ")+");"}function l(i){return d(["vec"],h(function(n){return"this."+n+" "+i+" vec."+n}))}e.prototype.add=l("+");e.prototype.subtract=l("-");e.prototype.multiply=l("*");e.prototype.divide=l("/");function j(i){return d(["scalar"],h(function(n){return"this."+n+" "+i+" scalar"}))}e.prototype.fadd=j("+");e.prototype.fsubtract=j("-");e.prototype.fmultiply=j("*");e.prototype.fdivide=j("/");e.prototype.magnitude=d([],"return Math.sqrt("+k.map(function(i){return"this."+i+" * this."+i}).join(" + ")+");");function o(i,n){n=n||[];return d(n,h(function(q){var p=n.slice(0);p.unshift("this."+q);return i+"("+p.join(", ")+")"}))}e.prototype.floor=o("Math.floor");e.prototype.ceil=o("Math.ceil");e.prototype.abs=o("Math.abs");e.prototype.negate=o("-1 * ");e.prototype.pow=o("Math.pow",["expt"]);e.prototype.pow2=d([],h(function(i){return"this."+i+" * this."+i}));e.prototype.pow3=d([],h(function(i){return"this."+i+" * this."+i+" * this."+i}));e.prototype.product=d([],"return "+k.map(function(i){return"this."+i}).join(" * ")+";");e.prototype.sum=d([],"return "+k.map(function(i){return"this."+i}).join(" + ")+";");e.prototype.normalize=function g(){return this.divide(this.magnitude())};e.prototype.dot=function a(i){return this.multiply(i).sum()};e.prototype.toArray=d([],"return ["+k.map(function(i){return"this."+i}).join(", ")+"]");function m(i){var n=i.map(function(p){return"this."+p}).join(", ");Object.defineProperty(e.prototype,i.join(""),{get:new Function("return new this.VecN.Vec"+i.length+"("+n+");")})}function b(i,n){k.forEach(function(p){i.push(p);if(n===1){m(i)}else{b(i,n-1)}i.pop()})}if(c<=6){for(var f=2;f<=c;f++){b([],f)}}e.random=d([],h(function(){return"Math.random()"},"this"));return e};(10);VecN.convenience=function(a){return function(){var d=[];for(var f=0;f= x
82 | */
83 | Bottle.highest2 = function(x) {
84 | return Math.pow(2, Math.ceil(Math.log(x) / Math.LN2));
85 | };
86 |
87 | Bottle.prototype.texScale = function() {
88 | return vec2(Bottle.highest2(this.gl.canvas.width),
89 | Bottle.highest2(this.gl.canvas.height));
90 | };
91 |
92 | /**
93 | * @returns {WebGLTexture} An appropriately initialized intermediate texture
94 | */
95 | Bottle.prototype.createTexture = function() {
96 | var gl = this.gl, tex = gl.createTexture(),
97 | scale = this.texScale();
98 | gl.bindTexture(gl.TEXTURE_2D, tex);
99 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
100 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
101 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
102 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
103 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, scale.x, scale.y,
104 | 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
105 | return tex;
106 | };
107 |
108 | /**
109 | * Swaps the front and back textures and bind the back texture.
110 | */
111 | Bottle.prototype.swap = function() {
112 | var gl = this.gl,
113 | temp = this.textures.front;
114 | this.textures.front = this.textures.back;
115 | this.textures.back = temp;
116 | gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo);
117 | gl.bindTexture(gl.TEXTURE_2D, this.textures.back);
118 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
119 | gl.TEXTURE_2D, this.textures.back, 0);
120 | gl.clearColor(0, 0, 0, 1);
121 | gl.clear(gl.COLOR_BUFFER_BIT);
122 | gl.bindTexture(gl.TEXTURE_2D, this.textures.front);
123 | return this;
124 | };
125 |
126 | Bottle.prototype.buildOuter = function() {
127 | var thickness = 0.1;
128 | var box = new B.PolygonShape(), def = new B.BodyDef();
129 |
130 | def.set_position(new B.Vec2(this.width / 2, 0));
131 | box.SetAsBox(thickness / 2, this.height / 2);
132 | this.world.CreateBody(def).CreateFixture(box, 0);
133 |
134 | def.set_position(new B.Vec2(-this.width / 2, 0));
135 | box.SetAsBox(thickness / 2, this.height / 2);
136 | this.world.CreateBody(def).CreateFixture(box, 0);
137 |
138 | def.set_position(new B.Vec2(0, this.height / 2));
139 | box.SetAsBox(this.width / 2, thickness / 2);
140 | this.world.CreateBody(def).CreateFixture(box, 0);
141 |
142 | def.set_position(new B.Vec2(0, -this.height / 2));
143 | box.SetAsBox(this.width / 2, thickness / 2);
144 | this.world.CreateBody(def).CreateFixture(box, 0);
145 | };
146 |
147 | Bottle.prototype.addSpike = function(pos, dir) {
148 | var thickness = Bottle.SPIKE_THICKNESS;
149 | var def = new B.BodyDef();
150 | def.set_position(pos);
151 | var verts = [
152 | new B.Vec2(dir * this.width / 2 - pos.get_x(), dir * thickness / 2),
153 | new B.Vec2(0, 0),
154 | new B.Vec2(dir * this.width / 2 - pos.get_x(), dir * -thickness / 2)
155 | ];
156 | this.polys.push({pos: pos, verts: verts});
157 | var fix = new B.FixtureDef();
158 | fix.set_shape(createPolygonShape(verts));
159 | fix.set_density(1.0);
160 | fix.set_friction(0);
161 | this.world.CreateBody(def).CreateFixture(fix);
162 | };
163 |
164 | Bottle.prototype.random = function() {
165 | return new B.Vec2(Math.random() * this.width - (this.width / 2),
166 | Math.random() * this.height - (this.height / 2));
167 | };
168 |
169 | Bottle.prototype.addBall = function(pos) {
170 | pos = pos || this.random();
171 | var def = new B.BodyDef();
172 | def.set_position(pos);
173 | def.set_type(B.b2_dynamicBody);
174 | var circle = new B.CircleShape();
175 | circle.set_m_radius(Bottle.BALL_RADIUS);
176 | var mass = new B.FixtureDef();
177 | mass.set_shape(circle);
178 | mass.set_density(Bottle.BALL_DENSITY);
179 | mass.set_friction(Bottle.BALL_FRICTION);
180 | mass.set_restitution(Bottle.BALL_RESTITUTION);
181 | this.balls.push(this.world.CreateBody(def).CreateFixture(mass));
182 | };
183 |
184 | Bottle.prototype.render = function() {
185 | var gl = this.gl;
186 | var w = this.gl.canvas.width, h = this.gl.canvas.height;
187 | var sx = w / this.width * 2, sy = h / this.height * 2;
188 |
189 | /* Update balls vertex attribute. */
190 | var pos = new Float32Array(this.balls.length * 2);
191 | for (var i = 0; i < this.balls.length; i++) {
192 | var p = this.balls[i].GetBody().GetPosition();
193 | pos[i * 2 + 0] = p.get_x() / w * sx;
194 | pos[i * 2 + 1] = p.get_y() / h * sy;
195 | }
196 | this.buffers.balls.update(pos);
197 |
198 | this.swap();
199 | gl.bindTexture(gl.TEXTURE_2D, this.textures.front);
200 | this.programs.balls.use()
201 | .attrib('ball', this.buffers.balls, 2)
202 | .uniform('size', Bottle.BALL_RADIUS * sx)
203 | .draw(gl.POINTS, this.balls.length);
204 | this.swap();
205 |
206 | if (this.doBlur) {
207 | this.programs.blur.use()
208 | .attrib('position', this.buffers.quad, 2)
209 | .uniform('base', 0, true)
210 | .uniform('scale', this.texScale())
211 | .uniform('dir', vec2(0.0, 1.0))
212 | .draw(gl.TRIANGLE_STRIP, 4);
213 | this.swap();
214 |
215 | this.programs.blur
216 | .uniform('dir', vec2(1.0, 0.0))
217 | .draw(gl.TRIANGLE_STRIP, 4);
218 | this.swap();
219 | }
220 |
221 | gl.bindFramebuffer(gl.FRAMEBUFFER, null);
222 | this.programs.threshold.use()
223 | .attrib('position', this.buffers.quad, 2)
224 | .uniform('base', 0, true)
225 | .uniform('scale', this.texScale())
226 | .uniform('copy', !this.doThreshold, true)
227 | .uniform('threshold', this.threshold)
228 | .draw(gl.TRIANGLE_STRIP, 4);
229 |
230 | this.programs.spikes.use()
231 | .attrib('position', this.buffers.spikes, 2)
232 | .uniform('color', vec4(0.5, 0.5, 0.5, 1.0))
233 | .draw(gl.TRIANGLES, this.polys.length * 3);
234 | };
235 |
236 | Bottle.prototype.step = function() {
237 | this.fps.tick();
238 | this.time += 1 / Bottle.FPS;
239 | if (Math.sin(this.time / Bottle.FLIP_RATE * Math.PI) < 0) {
240 | this.world.SetGravity(Bottle.NGRAVITY);
241 | } else {
242 | this.world.SetGravity(Bottle.GRAVITY);
243 | }
244 | this.world.Step(1 / 30, 8, 3);
245 | };
246 |
--------------------------------------------------------------------------------
/webgl/src/color.frag:
--------------------------------------------------------------------------------
1 | #ifdef GL_ES
2 | precision mediump float;
3 | #endif
4 |
5 | uniform vec4 color;
6 |
7 | void main() {
8 | gl_FragColor = color;
9 | }
10 |
--------------------------------------------------------------------------------
/webgl/src/fps.js:
--------------------------------------------------------------------------------
1 | function FPS() {
2 | this.second = null;
3 | this.counter = 0;
4 | this.value = NaN;
5 | this.listeners = [];
6 | }
7 |
8 | FPS.prototype.tick = function() {
9 | var now = ~~(Date.now() / 1000);
10 | if (this.second != now) {
11 | this.value = this.count;
12 | this.second = now;
13 | this.count = 0;
14 | var value = this.value;
15 | this.listeners.forEach(function(callback) {
16 | callback(value);
17 | });
18 | } else {
19 | this.count++;
20 | }
21 | };
22 |
23 | FPS.prototype.listen = function(callback) {
24 | this.listeners.push(callback);
25 | };
26 |
27 | FPS.prototype.valueOf = function() {
28 | return this.value;
29 | };
30 |
31 | FPS.prototype.toString = function() {
32 | return '[FPS ' + this.value + ']';
33 | };
34 |
--------------------------------------------------------------------------------
/webgl/src/identity.vert:
--------------------------------------------------------------------------------
1 | #ifdef GL_ES
2 | precision mediump float;
3 | #endif
4 |
5 | attribute vec2 position;
6 |
7 | void main() {
8 | gl_Position = vec4(position, 0, 1.0);
9 | }
10 |
--------------------------------------------------------------------------------
/webgl/src/liquid.js:
--------------------------------------------------------------------------------
1 | var bottle = null;
2 | window.addEventListener('load', function() {
3 | bottle = new Bottle(document.getElementById('display'));
4 | function step() {
5 | bottle.step();
6 | }
7 | function render() {
8 | bottle.render();
9 | window.requestAnimationFrame(render);
10 | }
11 | window.requestAnimationFrame(render);
12 | setInterval(step, 1000 / Bottle.FPS);
13 |
14 | document.getElementById('doBlur')
15 | .addEventListener('change', function() {
16 | bottle.doBlur = this.checked;
17 | });
18 | document.getElementById('doThreshold')
19 | .addEventListener('change', function() {
20 | bottle.doThreshold = this.checked;
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/webgl/src/threshold.frag:
--------------------------------------------------------------------------------
1 | #ifdef GL_ES
2 | precision mediump float;
3 | #endif
4 |
5 | uniform sampler2D base;
6 | uniform vec2 scale;
7 | uniform float threshold;
8 | uniform int copy;
9 |
10 | void main() {
11 | vec4 value = texture2D(base, gl_FragCoord.xy / scale);
12 | if (copy != 0) {
13 | gl_FragColor = value;
14 | } else if (value.r > threshold) {
15 | gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
16 | } else {
17 | gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/webgl/src/utility.js:
--------------------------------------------------------------------------------
1 | var B = B || {};
2 |
3 | B.Vec2 = Box2D.b2Vec2;
4 | B.BodyDef = Box2D.b2BodyDef;
5 | B.Body = Box2D.b2Body;
6 | B.FixtureDef = Box2D.b2FixtureDef;
7 | B.Fixture = Box2D.b2Fixture;
8 | B.World = Box2D.b2World;
9 | B.MassData = Box2D.b2MassData;
10 | B.PolygonShape = Box2D.b2PolygonShape;
11 | B.CircleShape = Box2D.b2CircleShape;
12 |
13 | B.b2_dynamicBody = Box2D.b2_dynamicBody;
14 |
15 | function createPolygonShape(verts) {
16 | var shape = new Box2D.b2PolygonShape();
17 | var buffer = Box2D.allocate(verts.length * 8, 'float', Box2D.ALLOC_STACK);
18 | var offset = 0;
19 | for (var i = 0; i < verts.length; i++) {
20 | Box2D.setValue(buffer + (offset + 0), verts[i].get_x(), 'float'); // x
21 | Box2D.setValue(buffer + (offset + 4), verts[i].get_y(), 'float'); // y
22 | offset += 8;
23 | }
24 | var ptr_wrapped = Box2D.wrapPointer(buffer, Box2D.b2Vec2);
25 | shape.Set(ptr_wrapped, verts.length);
26 | return shape;
27 | }
28 |
29 | if (window.requestAnimationFrame == null) {
30 | window.requestAnimationFrame =
31 | window.mozRequestAnimationFrame ||
32 | window.webkitRequestAnimationFrame ||
33 | window.msRequestAnimationFrame ||
34 | window.oRequestAnimationFrame ||
35 | function(f) {
36 | setTimeout(f, 16);
37 | };
38 | }
39 |
--------------------------------------------------------------------------------