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
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
.*;
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
);
58 public final Dimension
getBoardSize() {
59 return this.boardSize
;
63 public final void setBoardSize(final Dimension size
) {
64 this.boardSize
= size
;
67 this.densityMap
.setSize(size
.width
, size
.height
);
69 for (final Particle particle
: this) {
70 this.densityMap
.addParticle(particle
);
74 this.densityMap
.setSize(0, 0);
79 public final float getGravity() {
84 public final void setGravity(final float gravity
) {
85 this.gravity
= gravity
;
89 public final void removeAllParticles() {
90 this.particles
.clear();
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
);
102 public final int getParticleCount() {
103 return this.particles
.size();
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
);
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
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
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
);
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
) {
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
) {
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()));
188 * <br>Should not be null
189 * <br>Input-output parameter
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
);
202 * <br>Should not be null
203 * <br>Input-output parameter
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
);
214 * <br>Should not be null
215 * <br>Input-output parameter
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
);
235 * <br>Should not be null
236 * <br>Input-output parameter
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:
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.
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
;
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[][] {
299 private static final float ARBITRARY_MAXIMUM_SPEED
= 64F
;
304 * <br>Range: any float
306 * <br>Range: any float
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
)) {
317 if (Float
.isInfinite(result
)) {
318 return signum(result
) * ARBIRARY_MAXIMUM_DENSITY_DIFFERENCE
;
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
);
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() {
360 * <br>Should not be null
361 * <br>Reference parameter
363 private DensityMap(final float[][] data
) {
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
));
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
));
402 * <br>Range: <code>[0 .. Integer.MAX_VALUE]</code>
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
];
413 * <br>Range: <code>[Float.NEGATIVE_INFINITY .. Float.POSITIVE_INFINITY]</code>
415 * <br>Range: <code>[Float.NEGATIVE_INFINITY .. Float.POSITIVE_INFINITY]</code>
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
);
426 * <br>Range: <code>[Float.NEGATIVE_INFINITY .. Float.POSITIVE_INFINITY]</code>
428 * <br>Range: <code>[Float.NEGATIVE_INFINITY .. Float.POSITIVE_INFINITY]</code>
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
;
441 * <br>Range: <code>[Float.NEGATIVE_INFINITY .. Float.POSITIVE_INFINITY]</code>
443 * <br>Range: <code>[Float.NEGATIVE_INFINITY .. Float.POSITIVE_INFINITY]</code>
446 public final boolean isInside(final float x
, final float y
) {
447 return 0 <= x
&& x
< this.getWidth() && 0 <= y
&& y
< this.getHeight();
453 * Range: <code>[0 .. Integer.MAX_VALUE]</code>
455 public final int getWidth() {
456 return this.data
.length
;
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
;
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;