1 /* 2 * Copyright (c) 2009-2012 jMonkeyEngine 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are 7 * met: 8 * 9 * * Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 12 * * Redistributions in binary form must reproduce the above copyright 13 * notice, this list of conditions and the following disclaimer in the 14 * documentation and/or other materials provided with the distribution. 15 * 16 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 22 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 23 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 package com.jme3.effect; 33 34 import com.jme3.bounding.BoundingBox; 35 import com.jme3.effect.ParticleMesh.Type; 36 import com.jme3.effect.influencers.DefaultParticleInfluencer; 37 import com.jme3.effect.influencers.ParticleInfluencer; 38 import com.jme3.effect.shapes.EmitterPointShape; 39 import com.jme3.effect.shapes.EmitterShape; 40 import com.jme3.export.InputCapsule; 41 import com.jme3.export.JmeExporter; 42 import com.jme3.export.JmeImporter; 43 import com.jme3.export.OutputCapsule; 44 import com.jme3.math.ColorRGBA; 45 import com.jme3.math.FastMath; 46 import com.jme3.math.Matrix3f; 47 import com.jme3.math.Vector3f; 48 import com.jme3.renderer.Camera; 49 import com.jme3.renderer.RenderManager; 50 import com.jme3.renderer.ViewPort; 51 import com.jme3.renderer.queue.RenderQueue.Bucket; 52 import com.jme3.renderer.queue.RenderQueue.ShadowMode; 53 import com.jme3.scene.Geometry; 54 import com.jme3.scene.Spatial; 55 import com.jme3.scene.control.Control; 56 import com.jme3.util.TempVars; 57 import java.io.IOException; 58 59 /** 60 * <code>ParticleEmitter</code> is a special kind of geometry which simulates 61 * a particle system. 62 * <p> 63 * Particle emitters can be used to simulate various kinds of phenomena, 64 * such as fire, smoke, explosions and much more. 65 * <p> 66 * Particle emitters have many properties which are used to control the 67 * simulation. The interpretation of these properties depends on the 68 * {@link ParticleInfluencer} that has been assigned to the emitter via 69 * {@link ParticleEmitter#setParticleInfluencer(com.jme3.effect.influencers.ParticleInfluencer) }. 70 * By default the implementation {@link DefaultParticleInfluencer} is used. 71 * 72 * @author Kirill Vainer 73 */ 74 public class ParticleEmitter extends Geometry { 75 76 private boolean enabled = true; 77 private static final EmitterShape DEFAULT_SHAPE = new EmitterPointShape(Vector3f.ZERO); 78 private static final ParticleInfluencer DEFAULT_INFLUENCER = new DefaultParticleInfluencer(); 79 private ParticleEmitterControl control; 80 private EmitterShape shape = DEFAULT_SHAPE; 81 private ParticleMesh particleMesh; 82 private ParticleInfluencer particleInfluencer = DEFAULT_INFLUENCER; 83 private ParticleMesh.Type meshType; 84 private Particle[] particles; 85 private int firstUnUsed; 86 private int lastUsed; 87 // private int next = 0; 88 // private ArrayList<Integer> unusedIndices = new ArrayList<Integer>(); 89 private boolean randomAngle; 90 private boolean selectRandomImage; 91 private boolean facingVelocity; 92 private float particlesPerSec = 20; 93 private float timeDifference = 0; 94 private float lowLife = 3f; 95 private float highLife = 7f; 96 private Vector3f gravity = new Vector3f(0.0f, 0.1f, 0.0f); 97 private float rotateSpeed; 98 private Vector3f faceNormal = new Vector3f(Vector3f.NAN); 99 private int imagesX = 1; 100 private int imagesY = 1; 101 102 private ColorRGBA startColor = new ColorRGBA(0.4f, 0.4f, 0.4f, 0.5f); 103 private ColorRGBA endColor = new ColorRGBA(0.1f, 0.1f, 0.1f, 0.0f); 104 private float startSize = 0.2f; 105 private float endSize = 2f; 106 private boolean worldSpace = true; 107 //variable that helps with computations 108 private transient Vector3f temp = new Vector3f(); 109 110 public static class ParticleEmitterControl implements Control { 111 112 ParticleEmitter parentEmitter; 113 ParticleEmitterControl()114 public ParticleEmitterControl() { 115 } 116 ParticleEmitterControl(ParticleEmitter parentEmitter)117 public ParticleEmitterControl(ParticleEmitter parentEmitter) { 118 this.parentEmitter = parentEmitter; 119 } 120 cloneForSpatial(Spatial spatial)121 public Control cloneForSpatial(Spatial spatial) { 122 return this; // WARNING: Sets wrong control on spatial. Will be 123 // fixed automatically by ParticleEmitter.clone() method. 124 } 125 setSpatial(Spatial spatial)126 public void setSpatial(Spatial spatial) { 127 } 128 setEnabled(boolean enabled)129 public void setEnabled(boolean enabled) { 130 parentEmitter.setEnabled(enabled); 131 } 132 isEnabled()133 public boolean isEnabled() { 134 return parentEmitter.isEnabled(); 135 } 136 update(float tpf)137 public void update(float tpf) { 138 parentEmitter.updateFromControl(tpf); 139 } 140 render(RenderManager rm, ViewPort vp)141 public void render(RenderManager rm, ViewPort vp) { 142 parentEmitter.renderFromControl(rm, vp); 143 } 144 write(JmeExporter ex)145 public void write(JmeExporter ex) throws IOException { 146 } 147 read(JmeImporter im)148 public void read(JmeImporter im) throws IOException { 149 } 150 } 151 152 @Override clone()153 public ParticleEmitter clone() { 154 return clone(true); 155 } 156 157 @Override clone(boolean cloneMaterial)158 public ParticleEmitter clone(boolean cloneMaterial) { 159 ParticleEmitter clone = (ParticleEmitter) super.clone(cloneMaterial); 160 clone.shape = shape.deepClone(); 161 162 // Reinitialize particle list 163 clone.setNumParticles(particles.length); 164 165 clone.faceNormal = faceNormal.clone(); 166 clone.startColor = startColor.clone(); 167 clone.endColor = endColor.clone(); 168 clone.particleInfluencer = particleInfluencer.clone(); 169 170 // remove wrong control 171 clone.controls.remove(control); 172 173 // put correct control 174 clone.controls.add(new ParticleEmitterControl(clone)); 175 176 // Reinitialize particle mesh 177 switch (meshType) { 178 case Point: 179 clone.particleMesh = new ParticlePointMesh(); 180 clone.setMesh(clone.particleMesh); 181 break; 182 case Triangle: 183 clone.particleMesh = new ParticleTriMesh(); 184 clone.setMesh(clone.particleMesh); 185 break; 186 default: 187 throw new IllegalStateException("Unrecognized particle type: " + meshType); 188 } 189 clone.particleMesh.initParticleData(clone, clone.particles.length); 190 clone.particleMesh.setImagesXY(clone.imagesX, clone.imagesY); 191 192 return clone; 193 } 194 ParticleEmitter(String name, Type type, int numParticles)195 public ParticleEmitter(String name, Type type, int numParticles) { 196 super(name); 197 198 // ignore world transform, unless user sets inLocalSpace 199 this.setIgnoreTransform(true); 200 201 // particles neither receive nor cast shadows 202 this.setShadowMode(ShadowMode.Off); 203 204 // particles are usually transparent 205 this.setQueueBucket(Bucket.Transparent); 206 207 meshType = type; 208 209 // Must create clone of shape/influencer so that a reference to a static is 210 // not maintained 211 shape = shape.deepClone(); 212 particleInfluencer = particleInfluencer.clone(); 213 214 control = new ParticleEmitterControl(this); 215 controls.add(control); 216 217 switch (meshType) { 218 case Point: 219 particleMesh = new ParticlePointMesh(); 220 this.setMesh(particleMesh); 221 break; 222 case Triangle: 223 particleMesh = new ParticleTriMesh(); 224 this.setMesh(particleMesh); 225 break; 226 default: 227 throw new IllegalStateException("Unrecognized particle type: " + meshType); 228 } 229 this.setNumParticles(numParticles); 230 // particleMesh.initParticleData(this, particles.length); 231 } 232 233 /** 234 * For serialization only. Do not use. 235 */ ParticleEmitter()236 public ParticleEmitter() { 237 super(); 238 } 239 setShape(EmitterShape shape)240 public void setShape(EmitterShape shape) { 241 this.shape = shape; 242 } 243 getShape()244 public EmitterShape getShape() { 245 return shape; 246 } 247 248 /** 249 * Set the {@link ParticleInfluencer} to influence this particle emitter. 250 * 251 * @param particleInfluencer the {@link ParticleInfluencer} to influence 252 * this particle emitter. 253 * 254 * @see ParticleInfluencer 255 */ setParticleInfluencer(ParticleInfluencer particleInfluencer)256 public void setParticleInfluencer(ParticleInfluencer particleInfluencer) { 257 this.particleInfluencer = particleInfluencer; 258 } 259 260 /** 261 * Returns the {@link ParticleInfluencer} that influences this 262 * particle emitter. 263 * 264 * @return the {@link ParticleInfluencer} that influences this 265 * particle emitter. 266 * 267 * @see ParticleInfluencer 268 */ getParticleInfluencer()269 public ParticleInfluencer getParticleInfluencer() { 270 return particleInfluencer; 271 } 272 273 /** 274 * Returns the mesh type used by the particle emitter. 275 * 276 * 277 * @return the mesh type used by the particle emitter. 278 * 279 * @see #setMeshType(com.jme3.effect.ParticleMesh.Type) 280 * @see ParticleEmitter#ParticleEmitter(java.lang.String, com.jme3.effect.ParticleMesh.Type, int) 281 */ getMeshType()282 public ParticleMesh.Type getMeshType() { 283 return meshType; 284 } 285 286 /** 287 * Sets the type of mesh used by the particle emitter. 288 * @param meshType The mesh type to use 289 */ setMeshType(ParticleMesh.Type meshType)290 public void setMeshType(ParticleMesh.Type meshType) { 291 this.meshType = meshType; 292 switch (meshType) { 293 case Point: 294 particleMesh = new ParticlePointMesh(); 295 this.setMesh(particleMesh); 296 break; 297 case Triangle: 298 particleMesh = new ParticleTriMesh(); 299 this.setMesh(particleMesh); 300 break; 301 default: 302 throw new IllegalStateException("Unrecognized particle type: " + meshType); 303 } 304 this.setNumParticles(particles.length); 305 } 306 307 /** 308 * Returns true if particles should spawn in world space. 309 * 310 * @return true if particles should spawn in world space. 311 * 312 * @see ParticleEmitter#setInWorldSpace(boolean) 313 */ isInWorldSpace()314 public boolean isInWorldSpace() { 315 return worldSpace; 316 } 317 318 /** 319 * Set to true if particles should spawn in world space. 320 * 321 * <p>If set to true and the particle emitter is moved in the scene, 322 * then particles that have already spawned won't be effected by this 323 * motion. If set to false, the particles will emit in local space 324 * and when the emitter is moved, so are all the particles that 325 * were emitted previously. 326 * 327 * @param worldSpace true if particles should spawn in world space. 328 */ setInWorldSpace(boolean worldSpace)329 public void setInWorldSpace(boolean worldSpace) { 330 this.setIgnoreTransform(worldSpace); 331 this.worldSpace = worldSpace; 332 } 333 334 /** 335 * Returns the number of visible particles (spawned but not dead). 336 * 337 * @return the number of visible particles 338 */ getNumVisibleParticles()339 public int getNumVisibleParticles() { 340 // return unusedIndices.size() + next; 341 return lastUsed + 1; 342 } 343 344 /** 345 * Set the maximum amount of particles that 346 * can exist at the same time with this emitter. 347 * Calling this method many times is not recommended. 348 * 349 * @param numParticles the maximum amount of particles that 350 * can exist at the same time with this emitter. 351 */ setNumParticles(int numParticles)352 public final void setNumParticles(int numParticles) { 353 particles = new Particle[numParticles]; 354 for (int i = 0; i < numParticles; i++) { 355 particles[i] = new Particle(); 356 } 357 //We have to reinit the mesh's buffers with the new size 358 particleMesh.initParticleData(this, particles.length); 359 particleMesh.setImagesXY(this.imagesX, this.imagesY); 360 firstUnUsed = 0; 361 lastUsed = -1; 362 } 363 getMaxNumParticles()364 public int getMaxNumParticles() { 365 return particles.length; 366 } 367 368 /** 369 * Returns a list of all particles (shouldn't be used in most cases). 370 * 371 * <p> 372 * This includes both existing and non-existing particles. 373 * The size of the array is set to the <code>numParticles</code> value 374 * specified in the constructor or {@link ParticleEmitter#setNumParticles(int) } 375 * method. 376 * 377 * @return a list of all particles. 378 */ getParticles()379 public Particle[] getParticles() { 380 return particles; 381 } 382 383 /** 384 * Get the normal which particles are facing. 385 * 386 * @return the normal which particles are facing. 387 * 388 * @see ParticleEmitter#setFaceNormal(com.jme3.math.Vector3f) 389 */ getFaceNormal()390 public Vector3f getFaceNormal() { 391 if (Vector3f.isValidVector(faceNormal)) { 392 return faceNormal; 393 } else { 394 return null; 395 } 396 } 397 398 /** 399 * Sets the normal which particles are facing. 400 * 401 * <p>By default, particles 402 * will face the camera, but for some effects (e.g shockwave) it may 403 * be necessary to face a specific direction instead. To restore 404 * normal functionality, provide <code>null</code> as the argument for 405 * <code>faceNormal</code>. 406 * 407 * @param faceNormal The normals particles should face, or <code>null</code> 408 * if particles should face the camera. 409 */ setFaceNormal(Vector3f faceNormal)410 public void setFaceNormal(Vector3f faceNormal) { 411 if (faceNormal == null || !Vector3f.isValidVector(faceNormal)) { 412 this.faceNormal.set(Vector3f.NAN); 413 } else { 414 this.faceNormal = faceNormal; 415 } 416 } 417 418 /** 419 * Returns the rotation speed in radians/sec for particles. 420 * 421 * @return the rotation speed in radians/sec for particles. 422 * 423 * @see ParticleEmitter#setRotateSpeed(float) 424 */ getRotateSpeed()425 public float getRotateSpeed() { 426 return rotateSpeed; 427 } 428 429 /** 430 * Set the rotation speed in radians/sec for particles 431 * spawned after the invocation of this method. 432 * 433 * @param rotateSpeed the rotation speed in radians/sec for particles 434 * spawned after the invocation of this method. 435 */ setRotateSpeed(float rotateSpeed)436 public void setRotateSpeed(float rotateSpeed) { 437 this.rotateSpeed = rotateSpeed; 438 } 439 440 /** 441 * Returns true if every particle spawned 442 * should have a random facing angle. 443 * 444 * @return true if every particle spawned 445 * should have a random facing angle. 446 * 447 * @see ParticleEmitter#setRandomAngle(boolean) 448 */ isRandomAngle()449 public boolean isRandomAngle() { 450 return randomAngle; 451 } 452 453 /** 454 * Set to true if every particle spawned 455 * should have a random facing angle. 456 * 457 * @param randomAngle if every particle spawned 458 * should have a random facing angle. 459 */ setRandomAngle(boolean randomAngle)460 public void setRandomAngle(boolean randomAngle) { 461 this.randomAngle = randomAngle; 462 } 463 464 /** 465 * Returns true if every particle spawned should get a random 466 * image. 467 * 468 * @return True if every particle spawned should get a random 469 * image. 470 * 471 * @see ParticleEmitter#setSelectRandomImage(boolean) 472 */ isSelectRandomImage()473 public boolean isSelectRandomImage() { 474 return selectRandomImage; 475 } 476 477 /** 478 * Set to true if every particle spawned 479 * should get a random image from a pool of images constructed from 480 * the texture, with X by Y possible images. 481 * 482 * <p>By default, X and Y are equal 483 * to 1, thus allowing only 1 possible image to be selected, but if the 484 * particle is configured with multiple images by using {@link ParticleEmitter#setImagesX(int) } 485 * and {#link ParticleEmitter#setImagesY(int) } methods, then multiple images 486 * can be selected. Setting to false will cause each particle to have an animation 487 * of images displayed, starting at image 1, and going until image X*Y when 488 * the particle reaches its end of life. 489 * 490 * @param selectRandomImage True if every particle spawned should get a random 491 * image. 492 */ setSelectRandomImage(boolean selectRandomImage)493 public void setSelectRandomImage(boolean selectRandomImage) { 494 this.selectRandomImage = selectRandomImage; 495 } 496 497 /** 498 * Check if particles spawned should face their velocity. 499 * 500 * @return True if particles spawned should face their velocity. 501 * 502 * @see ParticleEmitter#setFacingVelocity(boolean) 503 */ isFacingVelocity()504 public boolean isFacingVelocity() { 505 return facingVelocity; 506 } 507 508 /** 509 * Set to true if particles spawned should face 510 * their velocity (or direction to which they are moving towards). 511 * 512 * <p>This is typically used for e.g spark effects. 513 * 514 * @param followVelocity True if particles spawned should face their velocity. 515 * 516 */ setFacingVelocity(boolean followVelocity)517 public void setFacingVelocity(boolean followVelocity) { 518 this.facingVelocity = followVelocity; 519 } 520 521 /** 522 * Get the end color of the particles spawned. 523 * 524 * @return the end color of the particles spawned. 525 * 526 * @see ParticleEmitter#setEndColor(com.jme3.math.ColorRGBA) 527 */ getEndColor()528 public ColorRGBA getEndColor() { 529 return endColor; 530 } 531 532 /** 533 * Set the end color of the particles spawned. 534 * 535 * <p>The 536 * particle color at any time is determined by blending the start color 537 * and end color based on the particle's current time of life relative 538 * to its end of life. 539 * 540 * @param endColor the end color of the particles spawned. 541 */ setEndColor(ColorRGBA endColor)542 public void setEndColor(ColorRGBA endColor) { 543 this.endColor.set(endColor); 544 } 545 546 /** 547 * Get the end size of the particles spawned. 548 * 549 * @return the end size of the particles spawned. 550 * 551 * @see ParticleEmitter#setEndSize(float) 552 */ getEndSize()553 public float getEndSize() { 554 return endSize; 555 } 556 557 /** 558 * Set the end size of the particles spawned. 559 * 560 * <p>The 561 * particle size at any time is determined by blending the start size 562 * and end size based on the particle's current time of life relative 563 * to its end of life. 564 * 565 * @param endSize the end size of the particles spawned. 566 */ setEndSize(float endSize)567 public void setEndSize(float endSize) { 568 this.endSize = endSize; 569 } 570 571 /** 572 * Get the gravity vector. 573 * 574 * @return the gravity vector. 575 * 576 * @see ParticleEmitter#setGravity(com.jme3.math.Vector3f) 577 */ getGravity()578 public Vector3f getGravity() { 579 return gravity; 580 } 581 582 /** 583 * This method sets the gravity vector. 584 * 585 * @param gravity the gravity vector 586 */ setGravity(Vector3f gravity)587 public void setGravity(Vector3f gravity) { 588 this.gravity.set(gravity); 589 } 590 591 /** 592 * Sets the gravity vector. 593 * 594 * @param x the x component of the gravity vector 595 * @param y the y component of the gravity vector 596 * @param z the z component of the gravity vector 597 */ setGravity(float x, float y, float z)598 public void setGravity(float x, float y, float z) { 599 this.gravity.x = x; 600 this.gravity.y = y; 601 this.gravity.z = z; 602 } 603 604 /** 605 * Get the high value of life. 606 * 607 * @return the high value of life. 608 * 609 * @see ParticleEmitter#setHighLife(float) 610 */ getHighLife()611 public float getHighLife() { 612 return highLife; 613 } 614 615 /** 616 * Set the high value of life. 617 * 618 * <p>The particle's lifetime/expiration 619 * is determined by randomly selecting a time between low life and high life. 620 * 621 * @param highLife the high value of life. 622 */ setHighLife(float highLife)623 public void setHighLife(float highLife) { 624 this.highLife = highLife; 625 } 626 627 /** 628 * Get the number of images along the X axis (width). 629 * 630 * @return the number of images along the X axis (width). 631 * 632 * @see ParticleEmitter#setImagesX(int) 633 */ getImagesX()634 public int getImagesX() { 635 return imagesX; 636 } 637 638 /** 639 * Set the number of images along the X axis (width). 640 * 641 * <p>To determine 642 * how multiple particle images are selected and used, see the 643 * {@link ParticleEmitter#setSelectRandomImage(boolean) } method. 644 * 645 * @param imagesX the number of images along the X axis (width). 646 */ setImagesX(int imagesX)647 public void setImagesX(int imagesX) { 648 this.imagesX = imagesX; 649 particleMesh.setImagesXY(this.imagesX, this.imagesY); 650 } 651 652 /** 653 * Get the number of images along the Y axis (height). 654 * 655 * @return the number of images along the Y axis (height). 656 * 657 * @see ParticleEmitter#setImagesY(int) 658 */ getImagesY()659 public int getImagesY() { 660 return imagesY; 661 } 662 663 /** 664 * Set the number of images along the Y axis (height). 665 * 666 * <p>To determine how multiple particle images are selected and used, see the 667 * {@link ParticleEmitter#setSelectRandomImage(boolean) } method. 668 * 669 * @param imagesY the number of images along the Y axis (height). 670 */ setImagesY(int imagesY)671 public void setImagesY(int imagesY) { 672 this.imagesY = imagesY; 673 particleMesh.setImagesXY(this.imagesX, this.imagesY); 674 } 675 676 /** 677 * Get the low value of life. 678 * 679 * @return the low value of life. 680 * 681 * @see ParticleEmitter#setLowLife(float) 682 */ getLowLife()683 public float getLowLife() { 684 return lowLife; 685 } 686 687 /** 688 * Set the low value of life. 689 * 690 * <p>The particle's lifetime/expiration 691 * is determined by randomly selecting a time between low life and high life. 692 * 693 * @param lowLife the low value of life. 694 */ setLowLife(float lowLife)695 public void setLowLife(float lowLife) { 696 this.lowLife = lowLife; 697 } 698 699 /** 700 * Get the number of particles to spawn per 701 * second. 702 * 703 * @return the number of particles to spawn per 704 * second. 705 * 706 * @see ParticleEmitter#setParticlesPerSec(float) 707 */ getParticlesPerSec()708 public float getParticlesPerSec() { 709 return particlesPerSec; 710 } 711 712 /** 713 * Set the number of particles to spawn per 714 * second. 715 * 716 * @param particlesPerSec the number of particles to spawn per 717 * second. 718 */ setParticlesPerSec(float particlesPerSec)719 public void setParticlesPerSec(float particlesPerSec) { 720 this.particlesPerSec = particlesPerSec; 721 } 722 723 /** 724 * Get the start color of the particles spawned. 725 * 726 * @return the start color of the particles spawned. 727 * 728 * @see ParticleEmitter#setStartColor(com.jme3.math.ColorRGBA) 729 */ getStartColor()730 public ColorRGBA getStartColor() { 731 return startColor; 732 } 733 734 /** 735 * Set the start color of the particles spawned. 736 * 737 * <p>The particle color at any time is determined by blending the start color 738 * and end color based on the particle's current time of life relative 739 * to its end of life. 740 * 741 * @param startColor the start color of the particles spawned 742 */ setStartColor(ColorRGBA startColor)743 public void setStartColor(ColorRGBA startColor) { 744 this.startColor.set(startColor); 745 } 746 747 /** 748 * Get the start color of the particles spawned. 749 * 750 * @return the start color of the particles spawned. 751 * 752 * @see ParticleEmitter#setStartSize(float) 753 */ getStartSize()754 public float getStartSize() { 755 return startSize; 756 } 757 758 /** 759 * Set the start size of the particles spawned. 760 * 761 * <p>The particle size at any time is determined by blending the start size 762 * and end size based on the particle's current time of life relative 763 * to its end of life. 764 * 765 * @param startSize the start size of the particles spawned. 766 */ setStartSize(float startSize)767 public void setStartSize(float startSize) { 768 this.startSize = startSize; 769 } 770 771 /** 772 * @deprecated Use ParticleEmitter.getParticleInfluencer().getInitialVelocity() instead. 773 */ 774 @Deprecated getInitialVelocity()775 public Vector3f getInitialVelocity() { 776 return particleInfluencer.getInitialVelocity(); 777 } 778 779 /** 780 * @param initialVelocity Set the initial velocity a particle is spawned with, 781 * the initial velocity given in the parameter will be varied according 782 * to the velocity variation set in {@link ParticleEmitter#setVelocityVariation(float) }. 783 * A particle will move toward its velocity unless it is effected by the 784 * gravity. 785 * 786 * @deprecated 787 * This method is deprecated. 788 * Use ParticleEmitter.getParticleInfluencer().setInitialVelocity(initialVelocity); instead. 789 * 790 * @see ParticleEmitter#setVelocityVariation(float) 791 * @see ParticleEmitter#setGravity(float) 792 */ 793 @Deprecated setInitialVelocity(Vector3f initialVelocity)794 public void setInitialVelocity(Vector3f initialVelocity) { 795 this.particleInfluencer.setInitialVelocity(initialVelocity); 796 } 797 798 /** 799 * @deprecated 800 * This method is deprecated. 801 * Use ParticleEmitter.getParticleInfluencer().getVelocityVariation(); instead. 802 * @return the initial velocity variation factor 803 */ 804 @Deprecated getVelocityVariation()805 public float getVelocityVariation() { 806 return particleInfluencer.getVelocityVariation(); 807 } 808 809 /** 810 * @param variation Set the variation by which the initial velocity 811 * of the particle is determined. <code>variation</code> should be a value 812 * from 0 to 1, where 0 means particles are to spawn with exactly 813 * the velocity given in {@link ParticleEmitter#setStartVel(com.jme3.math.Vector3f) }, 814 * and 1 means particles are to spawn with a completely random velocity. 815 * 816 * @deprecated 817 * This method is deprecated. 818 * Use ParticleEmitter.getParticleInfluencer().setVelocityVariation(variation); instead. 819 */ 820 @Deprecated setVelocityVariation(float variation)821 public void setVelocityVariation(float variation) { 822 this.particleInfluencer.setVelocityVariation(variation); 823 } 824 emitParticle(Vector3f min, Vector3f max)825 private Particle emitParticle(Vector3f min, Vector3f max) { 826 int idx = lastUsed + 1; 827 if (idx >= particles.length) { 828 return null; 829 } 830 831 Particle p = particles[idx]; 832 if (selectRandomImage) { 833 p.imageIndex = FastMath.nextRandomInt(0, imagesY - 1) * imagesX + FastMath.nextRandomInt(0, imagesX - 1); 834 } 835 836 p.startlife = lowLife + FastMath.nextRandomFloat() * (highLife - lowLife); 837 p.life = p.startlife; 838 p.color.set(startColor); 839 p.size = startSize; 840 //shape.getRandomPoint(p.position); 841 particleInfluencer.influenceParticle(p, shape); 842 if (worldSpace) { 843 worldTransform.transformVector(p.position, p.position); 844 worldTransform.getRotation().mult(p.velocity, p.velocity); 845 // TODO: Make scale relevant somehow?? 846 } 847 if (randomAngle) { 848 p.angle = FastMath.nextRandomFloat() * FastMath.TWO_PI; 849 } 850 if (rotateSpeed != 0) { 851 p.rotateSpeed = rotateSpeed * (0.2f + (FastMath.nextRandomFloat() * 2f - 1f) * .8f); 852 } 853 854 temp.set(p.position).addLocal(p.size, p.size, p.size); 855 max.maxLocal(temp); 856 temp.set(p.position).subtractLocal(p.size, p.size, p.size); 857 min.minLocal(temp); 858 859 ++lastUsed; 860 firstUnUsed = idx + 1; 861 return p; 862 } 863 864 /** 865 * Instantly emits all the particles possible to be emitted. Any particles 866 * which are currently inactive will be spawned immediately. 867 */ emitAllParticles()868 public void emitAllParticles() { 869 // Force world transform to update 870 this.getWorldTransform(); 871 872 TempVars vars = TempVars.get(); 873 874 BoundingBox bbox = (BoundingBox) this.getMesh().getBound(); 875 876 Vector3f min = vars.vect1; 877 Vector3f max = vars.vect2; 878 879 bbox.getMin(min); 880 bbox.getMax(max); 881 882 if (!Vector3f.isValidVector(min)) { 883 min.set(Vector3f.POSITIVE_INFINITY); 884 } 885 if (!Vector3f.isValidVector(max)) { 886 max.set(Vector3f.NEGATIVE_INFINITY); 887 } 888 889 while (emitParticle(min, max) != null); 890 891 bbox.setMinMax(min, max); 892 this.setBoundRefresh(); 893 894 vars.release(); 895 } 896 897 /** 898 * Instantly kills all active particles, after this method is called, all 899 * particles will be dead and no longer visible. 900 */ killAllParticles()901 public void killAllParticles() { 902 for (int i = 0; i < particles.length; ++i) { 903 if (particles[i].life > 0) { 904 this.freeParticle(i); 905 } 906 } 907 } 908 909 /** 910 * Kills the particle at the given index. 911 * 912 * @param index The index of the particle to kill 913 * @see #getParticles() 914 */ killParticle(int index)915 public void killParticle(int index){ 916 freeParticle(index); 917 } 918 freeParticle(int idx)919 private void freeParticle(int idx) { 920 Particle p = particles[idx]; 921 p.life = 0; 922 p.size = 0f; 923 p.color.set(0, 0, 0, 0); 924 p.imageIndex = 0; 925 p.angle = 0; 926 p.rotateSpeed = 0; 927 928 if (idx == lastUsed) { 929 while (lastUsed >= 0 && particles[lastUsed].life == 0) { 930 lastUsed--; 931 } 932 } 933 if (idx < firstUnUsed) { 934 firstUnUsed = idx; 935 } 936 } 937 swap(int idx1, int idx2)938 private void swap(int idx1, int idx2) { 939 Particle p1 = particles[idx1]; 940 particles[idx1] = particles[idx2]; 941 particles[idx2] = p1; 942 } 943 updateParticle(Particle p, float tpf, Vector3f min, Vector3f max)944 private void updateParticle(Particle p, float tpf, Vector3f min, Vector3f max){ 945 // applying gravity 946 p.velocity.x -= gravity.x * tpf; 947 p.velocity.y -= gravity.y * tpf; 948 p.velocity.z -= gravity.z * tpf; 949 temp.set(p.velocity).multLocal(tpf); 950 p.position.addLocal(temp); 951 952 // affecting color, size and angle 953 float b = (p.startlife - p.life) / p.startlife; 954 p.color.interpolate(startColor, endColor, b); 955 p.size = FastMath.interpolateLinear(b, startSize, endSize); 956 p.angle += p.rotateSpeed * tpf; 957 958 // Computing bounding volume 959 temp.set(p.position).addLocal(p.size, p.size, p.size); 960 max.maxLocal(temp); 961 temp.set(p.position).subtractLocal(p.size, p.size, p.size); 962 min.minLocal(temp); 963 964 if (!selectRandomImage) { 965 p.imageIndex = (int) (b * imagesX * imagesY); 966 } 967 } 968 updateParticleState(float tpf)969 private void updateParticleState(float tpf) { 970 // Force world transform to update 971 this.getWorldTransform(); 972 973 TempVars vars = TempVars.get(); 974 975 Vector3f min = vars.vect1.set(Vector3f.POSITIVE_INFINITY); 976 Vector3f max = vars.vect2.set(Vector3f.NEGATIVE_INFINITY); 977 978 for (int i = 0; i < particles.length; ++i) { 979 Particle p = particles[i]; 980 if (p.life == 0) { // particle is dead 981 // assert i <= firstUnUsed; 982 continue; 983 } 984 985 p.life -= tpf; 986 if (p.life <= 0) { 987 this.freeParticle(i); 988 continue; 989 } 990 991 updateParticle(p, tpf, min, max); 992 993 if (firstUnUsed < i) { 994 this.swap(firstUnUsed, i); 995 if (i == lastUsed) { 996 lastUsed = firstUnUsed; 997 } 998 firstUnUsed++; 999 } 1000 } 1001 1002 // Spawns particles within the tpf timeslot with proper age 1003 float interval = 1f / particlesPerSec; 1004 tpf += timeDifference; 1005 while (tpf > interval){ 1006 tpf -= interval; 1007 Particle p = emitParticle(min, max); 1008 if (p != null){ 1009 p.life -= tpf; 1010 if (p.life <= 0){ 1011 freeParticle(lastUsed); 1012 }else{ 1013 updateParticle(p, tpf, min, max); 1014 } 1015 } 1016 } 1017 timeDifference = tpf; 1018 1019 BoundingBox bbox = (BoundingBox) this.getMesh().getBound(); 1020 bbox.setMinMax(min, max); 1021 this.setBoundRefresh(); 1022 1023 vars.release(); 1024 } 1025 1026 /** 1027 * Set to enable or disable the particle emitter 1028 * 1029 * <p>When a particle is 1030 * disabled, it will be "frozen in time" and not update. 1031 * 1032 * @param enabled True to enable the particle emitter 1033 */ setEnabled(boolean enabled)1034 public void setEnabled(boolean enabled) { 1035 this.enabled = enabled; 1036 } 1037 1038 /** 1039 * Check if a particle emitter is enabled for update. 1040 * 1041 * @return True if a particle emitter is enabled for update. 1042 * 1043 * @see ParticleEmitter#setEnabled(boolean) 1044 */ isEnabled()1045 public boolean isEnabled() { 1046 return enabled; 1047 } 1048 1049 /** 1050 * Callback from Control.update(), do not use. 1051 * @param tpf 1052 */ updateFromControl(float tpf)1053 public void updateFromControl(float tpf) { 1054 if (enabled) { 1055 this.updateParticleState(tpf); 1056 } 1057 } 1058 1059 /** 1060 * Callback from Control.render(), do not use. 1061 * 1062 * @param rm 1063 * @param vp 1064 */ renderFromControl(RenderManager rm, ViewPort vp)1065 private void renderFromControl(RenderManager rm, ViewPort vp) { 1066 Camera cam = vp.getCamera(); 1067 1068 if (meshType == ParticleMesh.Type.Point) { 1069 float C = cam.getProjectionMatrix().m00; 1070 C *= cam.getWidth() * 0.5f; 1071 1072 // send attenuation params 1073 this.getMaterial().setFloat("Quadratic", C); 1074 } 1075 1076 Matrix3f inverseRotation = Matrix3f.IDENTITY; 1077 TempVars vars = null; 1078 if (!worldSpace) { 1079 vars = TempVars.get(); 1080 1081 inverseRotation = this.getWorldRotation().toRotationMatrix(vars.tempMat3).invertLocal(); 1082 } 1083 particleMesh.updateParticleData(particles, cam, inverseRotation); 1084 if (!worldSpace) { 1085 vars.release(); 1086 } 1087 } 1088 preload(RenderManager rm, ViewPort vp)1089 public void preload(RenderManager rm, ViewPort vp) { 1090 this.updateParticleState(0); 1091 particleMesh.updateParticleData(particles, vp.getCamera(), Matrix3f.IDENTITY); 1092 } 1093 1094 @Override write(JmeExporter ex)1095 public void write(JmeExporter ex) throws IOException { 1096 super.write(ex); 1097 OutputCapsule oc = ex.getCapsule(this); 1098 oc.write(shape, "shape", DEFAULT_SHAPE); 1099 oc.write(meshType, "meshType", ParticleMesh.Type.Triangle); 1100 oc.write(enabled, "enabled", true); 1101 oc.write(particles.length, "numParticles", 0); 1102 oc.write(particlesPerSec, "particlesPerSec", 0); 1103 oc.write(lowLife, "lowLife", 0); 1104 oc.write(highLife, "highLife", 0); 1105 oc.write(gravity, "gravity", null); 1106 oc.write(imagesX, "imagesX", 1); 1107 oc.write(imagesY, "imagesY", 1); 1108 1109 oc.write(startColor, "startColor", null); 1110 oc.write(endColor, "endColor", null); 1111 oc.write(startSize, "startSize", 0); 1112 oc.write(endSize, "endSize", 0); 1113 oc.write(worldSpace, "worldSpace", false); 1114 oc.write(facingVelocity, "facingVelocity", false); 1115 oc.write(faceNormal, "faceNormal", new Vector3f(Vector3f.NAN)); 1116 oc.write(selectRandomImage, "selectRandomImage", false); 1117 oc.write(randomAngle, "randomAngle", false); 1118 oc.write(rotateSpeed, "rotateSpeed", 0); 1119 1120 oc.write(particleInfluencer, "influencer", DEFAULT_INFLUENCER); 1121 } 1122 1123 @Override read(JmeImporter im)1124 public void read(JmeImporter im) throws IOException { 1125 super.read(im); 1126 InputCapsule ic = im.getCapsule(this); 1127 shape = (EmitterShape) ic.readSavable("shape", DEFAULT_SHAPE); 1128 1129 if (shape == DEFAULT_SHAPE) { 1130 // Prevent reference to static 1131 shape = shape.deepClone(); 1132 } 1133 1134 meshType = ic.readEnum("meshType", ParticleMesh.Type.class, ParticleMesh.Type.Triangle); 1135 int numParticles = ic.readInt("numParticles", 0); 1136 1137 1138 enabled = ic.readBoolean("enabled", true); 1139 particlesPerSec = ic.readFloat("particlesPerSec", 0); 1140 lowLife = ic.readFloat("lowLife", 0); 1141 highLife = ic.readFloat("highLife", 0); 1142 gravity = (Vector3f) ic.readSavable("gravity", null); 1143 imagesX = ic.readInt("imagesX", 1); 1144 imagesY = ic.readInt("imagesY", 1); 1145 1146 startColor = (ColorRGBA) ic.readSavable("startColor", null); 1147 endColor = (ColorRGBA) ic.readSavable("endColor", null); 1148 startSize = ic.readFloat("startSize", 0); 1149 endSize = ic.readFloat("endSize", 0); 1150 worldSpace = ic.readBoolean("worldSpace", false); 1151 this.setIgnoreTransform(worldSpace); 1152 facingVelocity = ic.readBoolean("facingVelocity", false); 1153 faceNormal = (Vector3f)ic.readSavable("faceNormal", new Vector3f(Vector3f.NAN)); 1154 selectRandomImage = ic.readBoolean("selectRandomImage", false); 1155 randomAngle = ic.readBoolean("randomAngle", false); 1156 rotateSpeed = ic.readFloat("rotateSpeed", 0); 1157 1158 switch (meshType) { 1159 case Point: 1160 particleMesh = new ParticlePointMesh(); 1161 this.setMesh(particleMesh); 1162 break; 1163 case Triangle: 1164 particleMesh = new ParticleTriMesh(); 1165 this.setMesh(particleMesh); 1166 break; 1167 default: 1168 throw new IllegalStateException("Unrecognized particle type: " + meshType); 1169 } 1170 this.setNumParticles(numParticles); 1171 // particleMesh.initParticleData(this, particles.length); 1172 // particleMesh.setImagesXY(imagesX, imagesY); 1173 1174 particleInfluencer = (ParticleInfluencer) ic.readSavable("influencer", DEFAULT_INFLUENCER); 1175 if (particleInfluencer == DEFAULT_INFLUENCER) { 1176 particleInfluencer = particleInfluencer.clone(); 1177 } 1178 1179 if (im.getFormatVersion() == 0) { 1180 // compatibility before the control inside particle emitter 1181 // was changed: 1182 // find it in the controls and take it out, then add the proper one in 1183 for (int i = 0; i < controls.size(); i++) { 1184 Object obj = controls.get(i); 1185 if (obj instanceof ParticleEmitter) { 1186 controls.remove(i); 1187 // now add the proper one in 1188 controls.add(new ParticleEmitterControl(this)); 1189 break; 1190 } 1191 } 1192 1193 // compatability before gravity was not a vector but a float 1194 if (gravity == null) { 1195 gravity = new Vector3f(); 1196 gravity.y = ic.readFloat("gravity", 0); 1197 } 1198 } else { 1199 // since the parentEmitter is not loaded, it must be 1200 // loaded separately 1201 control = getControl(ParticleEmitterControl.class); 1202 control.parentEmitter = this; 1203 1204 } 1205 } 1206 } 1207