Updated ParticleEngineDMapImplementation.
[desert.git] / src / org / sourceforge / desert / ParticleEngineDMapImplementation.java
blob12d3942f15e3eafbfb056b42a4c761a22811c4dc
1 /*
2 * Copyright (c) 2010 The Desert team
4 * Permission is hereby granted, free of charge, to any person
5 * obtaining a copy of this software and associated documentation
6 * files (the "Software"), to deal in the Software without
7 * restriction, including without limitation the rights to use,
8 * copy, modify, merge, publish, distribute, sublicense, and/or sell
9 * copies of the Software, and to permit persons to whom the
10 * Software is furnished to do so, subject to the following
11 * conditions:
13 * The above copyright notice and this permission notice shall be
14 * included in all copies or substantial portions of the Software.
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23 * OTHER DEALINGS IN THE SOFTWARE.
26 package org.sourceforge.desert;
28 import java.awt.Dimension;
29 import java.util.ArrayList;
30 import java.util.Iterator;
31 import java.util.List;
33 import static java.lang.Math.*;
34 import static org.sourceforge.desert.Utilities.*;
36 /**
38 * @author codistmonk (creation 2010-04-25)
40 public class ParticleEngineDMapImplementation implements ParticleEngine {
42 private final DensityMap densityMap;
44 private final List<Particle> particles;
46 private float gravity;
48 private Dimension boardSize;
50 public ParticleEngineDMapImplementation() {
51 this.densityMap = new DensityMap();
52 this.particles = new ArrayList<Particle>();
54 this.setGravity(DEFAULT_GRAVITY);
57 @Override
58 public final Dimension getBoardSize() {
59 return this.boardSize;
62 @Override
63 public final void setBoardSize(final Dimension size) {
64 this.boardSize = size;
66 if (size != null) {
67 this.densityMap.setSize(size.width, size.height);
69 for (final Particle particle : this) {
70 this.densityMap.addParticle(particle);
73 else {
74 this.densityMap.setSize(0, 0);
78 @Override
79 public final float getGravity() {
80 return this.gravity;
83 @Override
84 public final void setGravity(final float gravity) {
85 this.gravity = gravity;
88 @Override
89 public final void removeAllParticles() {
90 this.particles.clear();
93 @Override
94 public final void addParticle(final Particle.Type type, final float x, final float y, final float speedX, final float speedY) {
95 final ParticleDefaultImplementation particle = new ParticleDefaultImplementation(type, x, y, speedX, speedY);
97 this.particles.add(particle);
98 this.densityMap.addParticle(particle);
101 @Override
102 public final int getParticleCount() {
103 return this.particles.size();
106 @Override
107 public final void update(final float deltaTime) {
108 for (final Particle particle : this) {
109 if (particle.getType().isMobile()) {
110 this.applyForcesAndUpdatePosition((ParticleDefaultImplementation) particle, deltaTime);
115 @Override
116 public final Iterator<Particle> iterator() {
117 return this.particles.iterator();
122 * @param particle A mobile particle
123 * <br>Should not be null
124 * <br>Input-output parameter
125 * @param deltaTime
126 * <br>Range: <code>[0F .. Float.POSITIVE_INFINITY[</code>
128 private final void applyForcesAndUpdatePosition(final ParticleDefaultImplementation particle, final float deltaTime) {
129 final float pixelCount = max(1F, max(abs(particle.getSpeedX()), particle.getSpeedY()));
130 final float step = deltaTime / pixelCount;
132 for (float time = 0F; time < deltaTime; time += step) {
133 this.densityMap.removeParticle(particle);
135 this.applyForces(particle, step);
136 constrainSpeed(particle);
138 this.updatePosition(particle, step);
139 this.constrainPosition(particle);
141 this.densityMap.addParticle(particle);
147 * @param particle A mobile particle
148 * <br>Should not be null
149 * <br>Input-output parameter
150 * @param deltaTime
151 * <br>Range: <code>[0F .. Float.POSITIVE_INFINITY[</code>
153 private final void updatePosition(final ParticleDefaultImplementation particle, final float deltaTime) {
154 particle.setX(particle.getX() + particle.getSpeedX() * deltaTime);
155 particle.setY(particle.getY() + particle.getSpeedY() * deltaTime);
160 * @param particle
161 * <br>Should not be null
162 * <br>Input-output parameter
164 private final void constrainPosition(final ParticleDefaultImplementation particle) {
165 if (this.getBoardSize() != null) {
166 if (particle.getX() < 0F) {
167 particle.setX(0F);
168 particle.setSpeedX(abs(particle.getSpeedX()));
170 else if (particle.getX() >= this.getBoardSize().width) {
171 particle.setX(this.getBoardSize().width - 1F);
172 particle.setSpeedX(-abs(particle.getSpeedX()));
174 if (particle.getY() < 0F) {
175 particle.setY(0F);
176 particle.setSpeedY(abs(particle.getSpeedY()));
178 else if (particle.getY() >= this.getBoardSize().height) {
179 particle.setY(this.getBoardSize().height - 1F);
180 particle.setSpeedY(-abs(particle.getSpeedY()));
187 * @param particle
188 * <br>Should not be null
189 * <br>Input-output parameter
190 * @param deltaTime
191 * <br>Range: <code>[0F .. Float.POSITIVE_INFINITY[</code>
193 private final void applyForces(final ParticleDefaultImplementation particle, final float deltaTime) {
194 this.applyGravity(particle, deltaTime);
195 this.applyBuyoancy(particle, deltaTime);
196 this.applyPressure(particle, deltaTime);
201 * @param particle
202 * <br>Should not be null
203 * <br>Input-output parameter
204 * @param deltaTime
205 * <br>Range: <code>[0F .. Float.POSITIVE_INFINITY[</code>
207 private final void applyGravity(final ParticleDefaultImplementation particle, final float deltaTime) {
208 particle.setSpeedY(particle.getSpeedY() + this.getGravity() * deltaTime);
213 * @param particle
214 * <br>Should not be null
215 * <br>Input-output parameter
216 * @param deltaTime
217 * <br>Range: <code>[0F .. Float.POSITIVE_INFINITY[</code>
219 private final void applyBuyoancy(final ParticleDefaultImplementation particle, final float deltaTime) {
220 // TODO consider the particle size
221 float localDensity = this.densityMap.getDensity(particle.getX(), particle.getY());
223 if (Float.isInfinite(localDensity)) {
224 localDensity = signum(localDensity) * ARBIRARY_MAXIMUM_DENSITY_DIFFERENCE;
227 final float accelerationY = localDensity * -this.getGravity() / particle.getType().getMass();
229 particle.setSpeedY(particle.getSpeedY() + accelerationY * deltaTime);
234 * @param particle
235 * <br>Should not be null
236 * <br>Input-output parameter
237 * @param deltaTime
238 * <br>Range: <code>[0F .. Float.POSITIVE_INFINITY[</code>
240 private final void applyPressure(final ParticleDefaultImplementation particle, final float deltaTime) {
241 // If we see the density map as a 3D height field, then we can compute an average normal at each point
242 // This normal can then be projected on a horizontal plane to get the direction of the pressure force
243 // To compute the normal, we sum the normals of the eight triangles that can be formed with the center
244 // and each consecutive pair of surrounding points
245 // Here is an illustration of how to get the average normal at some point A:
246 // E-D-C
247 // |\|/|
248 // F-A-B
249 // |/|\|
250 // G-H-I
251 // For the first triangle (ABC), u = B - A, v = C - A and normal(ABC) = u x v (3D cross product)
252 // For the next triangle (ACD), u = C - A, v = D - A and normal(ACD) = u x v
253 // etc. until the last triangle AIB
254 // The sum of all these normals will give us the direction of the normal at the point A
255 // Note that in each pair (u, v) on vector is of length 1 and the other is of length sqrt(2)
256 // This means that the computation does not favor the contribution of any particular triangle
257 // TODO if more precision is needed, then replace C with (A+B+C+D)/4, E with (A+D+E+F)/4, etc.
258 float normalX = 0F;
259 float normalY = 0F;
260 // We don't need normalZ be cause we only care about the projection on a horizontal plane
261 final float centerDensity = this.densityMap.getDensity(particle.getX(), particle.getY());
262 float uX = DENSITY_CELL_OFFSETS[0][0];
263 float uY = DENSITY_CELL_OFFSETS[0][1];
264 float uZ = difference(this.densityMap.getDensity(particle.getX() + uX, particle.getY() + uY), centerDensity);
266 for (int i = 1; i < DENSITY_CELL_OFFSETS.length; ++i) {
267 final float vX = DENSITY_CELL_OFFSETS[i][0];
268 final float vY = DENSITY_CELL_OFFSETS[i][1];
269 final float vZ = difference(this.densityMap.getDensity(particle.getX() + vX, particle.getY() + vY), centerDensity);
271 normalX += uY * vZ - uZ * vY;
272 normalY -= uX * vZ - uZ * vX;
274 uX = vX;
275 uY = vY;
276 uZ = vZ;
279 particle.setSpeedX(particle.getSpeedX() + normalX * ARBITRARY_PRESSURE_COEFFICIENT * deltaTime);
280 particle.setSpeedY(particle.getSpeedY() + normalY * ARBITRARY_PRESSURE_COEFFICIENT * deltaTime);
283 private static final float ARBIRARY_MAXIMUM_DENSITY_DIFFERENCE = 1000F;
285 private static final float ARBITRARY_PRESSURE_COEFFICIENT = 1F;
287 private static final int[][] DENSITY_CELL_OFFSETS = new int[][] {
288 { 1, 0 },
289 { 1, 1 },
290 { 0, 1 },
291 { -1, 1 },
292 { -1, 0 },
293 { -1, -1 },
294 { 0, -1 },
295 { 1, -1 },
296 { 1, 0 },
299 private static final float ARBITRARY_MAXIMUM_SPEED = 64F;
303 * @param density1
304 * <br>Range: any float
305 * @param density2
306 * <br>Range: any float
307 * @return
308 * <br>Range: <code>[-ARBIRARY_MAXIMUM_DENSITY_DIFFERENCE .. ARBIRARY_MAXIMUM_DENSITY_DIFFERENCE]</code>
310 private static final float difference(final float density1, final float density2) {
311 final float result = density1 - density2;
313 if (Float.isNaN(result)) {
314 return 0F;
317 if (Float.isInfinite(result)) {
318 return signum(result) * ARBIRARY_MAXIMUM_DENSITY_DIFFERENCE;
321 return result;
326 * @param particle
327 * <br>Should not be null
328 * <br>Input-output parameter
330 private static final void constrainSpeed(final ParticleDefaultImplementation particle) {
331 final float speed = length(particle.getSpeedX(), particle.getSpeedY());
333 if (speed > ARBITRARY_MAXIMUM_SPEED) {
334 particle.setSpeedX(particle.getSpeedX() / speed * ARBITRARY_MAXIMUM_SPEED);
335 particle.setSpeedY(particle.getSpeedY() / speed * ARBITRARY_MAXIMUM_SPEED);
337 else {
338 particle.setSpeedX(particle.getSpeedX() * ARBITRARY_SPEED_DECAY);
339 particle.setSpeedY(particle.getSpeedY() * ARBITRARY_SPEED_DECAY);
343 private static final float ARBITRARY_SPEED_DECAY = 0.99F;
347 * @author codistmonk (2010-04-26)
349 private static final class DensityMap {
351 private float[][] data;
353 public DensityMap() {
354 this.setSize(0, 0);
359 * @param data
360 * <br>Should not be null
361 * <br>Reference parameter
363 private DensityMap(final float[][] data) {
364 this.data = data;
369 * @param particle
370 * <br>Should not be null
372 public final void addParticle(final Particle particle) {
373 for (int i = 0; i < PARTICLE_MASS_DISTRIBUTION.getWidth(); ++i) {
374 for (int j = 0; j < PARTICLE_MASS_DISTRIBUTION.getHeight(); ++j) {
375 final float x = particle.getX() - PARTICLE_MASS_DISTRIBUTION_HALF_WIDTH + i;
376 final float y = particle.getY() - PARTICLE_MASS_DISTRIBUTION_HALF_HEIGHT + j;
378 this.setDensity(x, y, this.getDensity(x, y) + particle.getType().getMass() * PARTICLE_MASS_DISTRIBUTION.getDensity(i, j));
385 * @param particle
386 * <br>Should not be null
388 public final void removeParticle(final Particle particle) {
389 for (int i = 0; i < PARTICLE_MASS_DISTRIBUTION.getWidth(); ++i) {
390 for (int j = 0; j < PARTICLE_MASS_DISTRIBUTION.getHeight(); ++j) {
391 final float x = particle.getX() - PARTICLE_MASS_DISTRIBUTION_HALF_WIDTH + i;
392 final float y = particle.getY() - PARTICLE_MASS_DISTRIBUTION_HALF_HEIGHT + j;
394 this.setDensity(x, y, this.getDensity(x, y) - particle.getType().getMass() * PARTICLE_MASS_DISTRIBUTION.getDensity(i, j));
401 * @param width
402 * <br>Range: <code>[0 .. Integer.MAX_VALUE]</code>
403 * @param height
404 * <br>Range: <code>[0 .. Integer.MAX_VALUE]</code>
406 public final void setSize(final int width, final int height) {
407 this.data = new float[width][height];
412 * @param x
413 * <br>Range: <code>[Float.NEGATIVE_INFINITY .. Float.POSITIVE_INFINITY]</code>
414 * @param y
415 * <br>Range: <code>[Float.NEGATIVE_INFINITY .. Float.POSITIVE_INFINITY]</code>
416 * @return
417 * <br>Range: <code>[Float.NEGATIVE_INFINITY .. Float.POSITIVE_INFINITY]</code>
419 public final float getDensity(final float x, final float y) {
420 return this.isSingular() ? 0F : (this.isInside(x, y) ? this.data[(int) x][(int) y] : Float.POSITIVE_INFINITY);
425 * @param x
426 * <br>Range: <code>[Float.NEGATIVE_INFINITY .. Float.POSITIVE_INFINITY]</code>
427 * @param y
428 * <br>Range: <code>[Float.NEGATIVE_INFINITY .. Float.POSITIVE_INFINITY]</code>
429 * @param value
430 * <br>Range: <code>[Float.NEGATIVE_INFINITY .. Float.POSITIVE_INFINITY]</code>
432 public final void setDensity(final float x, final float y, final float value) {
433 if (this.isInside(x, y)) {
434 this.data[(int) x][(int) y] = value;
440 * @param x
441 * <br>Range: <code>[Float.NEGATIVE_INFINITY .. Float.POSITIVE_INFINITY]</code>
442 * @param y
443 * <br>Range: <code>[Float.NEGATIVE_INFINITY .. Float.POSITIVE_INFINITY]</code>
444 * @return
446 public final boolean isInside(final float x, final float y) {
447 return 0 <= x && x < this.getWidth() && 0 <= y && y < this.getHeight();
452 * @return
453 * Range: <code>[0 .. Integer.MAX_VALUE]</code>
455 public final int getWidth() {
456 return this.data.length;
461 * @return
462 * Range: <code>[0 .. Integer.MAX_VALUE]</code>
464 public final int getHeight() {
465 return this.data.length == 0 ? 0 : this.data[0].length;
470 * @return <code>true</code> if <code>this</code> width or height is 0
472 private final boolean isSingular() {
473 return this.getWidth() == 0 || this.getHeight() == 0;
476 private static final float SQRT2 = (float) sqrt(2.0);
478 private static final DensityMap PARTICLE_MASS_DISTRIBUTION;
480 private static final int PARTICLE_MASS_DISTRIBUTION_HALF_WIDTH;
482 private static final int PARTICLE_MASS_DISTRIBUTION_HALF_HEIGHT;
484 static {
485 final float corner = 1F / (8F * (1F + SQRT2));
486 final float border = SQRT2 * corner;
487 final float center = 0.5F;
489 assert 4F * (corner + border) + center == 1F;
491 PARTICLE_MASS_DISTRIBUTION = new DensityMap(new float[][] {
492 { corner, border, corner },
493 { border, center, border },
494 { corner, border, corner },
496 PARTICLE_MASS_DISTRIBUTION_HALF_WIDTH = PARTICLE_MASS_DISTRIBUTION.getWidth() / 2;
497 PARTICLE_MASS_DISTRIBUTION_HALF_HEIGHT = PARTICLE_MASS_DISTRIBUTION.getHeight() / 2;