2
\$\begingroup\$

I'm currently working on a WebGL project to realise an idea I've had for some years. On my desk is book on making a space sim written 15 years ago.

There I found some algorithm for particles in space. The idea is simple: A cloud of particles that stay in place so they fly past the player, but reset to be in front of the player when they move past the ship. This shows the player they're moving while there's nothing else around. For this, I reworked the algorithm found in the book a little:

ParticleField.prototype.updateCloud = function(frustum, position, direction) {
  var s = this.size, s2 = s * 2;

  for (var i = 0; i < this.vertices.length; i++) {
    if (!frustum.containsPoint(this.vertices[i])) {
      this.vertices[i].set(
        (position.x + s - Math.floor(Math.random() * s2)) + (s * direction.x),
        (position.y + s - Math.floor(Math.random() * s2)) + (s * direction.y),
        (position.z + s - Math.floor(Math.random() * s2)) + 
                         ((s + Math.floor(Math.random() * s2)) * direction.z)
      );
    }
  }
};

Where s is the size of cloud. I played around with the sizes, but this simple function has several weaknesses.

  • The particles 'pop up' in front of the player when they're moving.
  • If the player backs up and moves backwards, the particles won't reset, leaving the cloud visible popping the illusion.
  • Particles appear to be flying through the player.

Without regard of how the particles look (they're simple points right now), how can I make this look more natural? I've seen the effect in games like Elite: Dangerous and Freelancer, and I'd like to be closer to them, and I'd appreciate any hint on the topic.

\$\endgroup\$
  • \$\begingroup\$ I got curious: may I ask you the reference of the book you mentioned? \$\endgroup\$ – Andy Astro Dec 5 '15 at 3:44
  • \$\begingroup\$ @AndyAstro I have this one since I learned programming, and return to it now and then. It's horribly outdated and probably obsolete, but still a fun read. ISBN: 978-3831105939. \$\endgroup\$ – Lanbo Dec 5 '15 at 8:48
  • \$\begingroup\$ Thanks for the reference. If think this is the book: amazon.co.uk/3D-Spieleprogrammierung-mit-DirectX/dp/3831105936 Is that correct? \$\endgroup\$ – Andy Astro Dec 11 '15 at 2:59
  • \$\begingroup\$ @AndyAstro Yes, that's the one. \$\endgroup\$ – Lanbo Dec 11 '15 at 7:10
2
\$\begingroup\$

"The particles 'pop up' in front of the player when they're moving."

Your calculations aren't generating points outside the view frustum, and it seems they're going to be re-generated until they happen to be placed either inside the frustum (getting you the 'pop up' issue), or right outside the frustum, in the direction of travel.

I'm assuming throughout this that you're allowing the player to move arbitrarily in all 3 dimensions.

To fix this, you need two changes:

  1. Generate your particles farther ahead. Without changing that you're generating in a uniform cube, a hack solution would mean changing the final (s * direction.x) into something like (s * 5 * direction.x), where 5 is some number big enough to place things outside the frustum. Ideally you would calculate it based on the frustum.

  2. Points generated outside the frustum need to be left alone until they appear on-screen, or they're behind the frustum, too old, or what-have-you. Lots of ways to do this. In your case, perhaps change the initial check to also verify the particle has appeared and isn't too old, and set those variables right after resetting the point. (Below, I use a max age of 5 seconds, which may be too much or too little, depending on player velocity.)


"If the player backs up and moves backwards, the particles won't reset, leaving the cloud visible popping the illusion."

I'm assuming here that you're describing the player facing one way, but moving the opposite direction. (If this is wrong, let me know. :P)

Without changing the meaning of "direction" anywhere else, you could fix this by passing the function a fourth parameter which is essentially a "facing" multiplier that's 1 when moving forwards, -1 when moving backwards. Then (s * 5 * direction.x) becomes (s * 5 * direction.x * facing).

This should change particles so they generate behind the player instead.


"Particles appear to be flying through the player."

I'll take this literally and assume you're saying you don't want particles passing through your player's model. To fix this, you need to prevent particles from being placed in column your player's model will sweep as it moves forward.

You could maybe change the initial point generation to enforce this from the get-go, but I think in this case I'd recommend just re-generating any points that don't satisfy that constraint.

Given your direction vector and player's position, you can calculate two points (position, and position+direction) and build off of the javascript code in this answer.

Removing the two lines if (t < 0) ... and if (t > 1) ... - should yield the distance from some point to some arbitrary infinite line defined by those two points.

Armed with this, you can check each generated point to ensure its result is far enough away from center to not hit the player's model. If it's too close, just generate it again. Unless your cube cross-section is similar in size to the sweep cross-section, this shouldn't usually take more than a couple of attempts.

I'm assuming the model is centered on the player's position and roughly symmetric. So then we want to calculate half the longest dimension, perpendicular to the model's "forward" direction. So for a humanoid model, this would be height. For a typical airplane it would be half the wingspan, etc. In the code below I'm calling this modelWidth and just hard-coding it, but you should get this value in whatever way fits your game.


Lastly, here's the code. Deliciously untested!

ParticleField.prototype.updateCloud = function(frustum, position, direction, facing) {
  var s = this.size, s2 = s * 2;
  var modelWidth = 11; // arbitrary magic number representing width of your model
  var d = new Date();

  for (var i = 0; i < this.vertices.length; i++) {
    if (!frustum.containsPoint(this.vertices[i])) {
      // you'll need to initialize the variables elsewhere of course
      if(this.vertices[i].dieAfter <= d.getTime() || this.vertices[i].hasAppeared)
      {
        do {
          this.vertices[i].set(
            (position.x + s - Math.floor(Math.random() * s2)) + (s * 5 * facing * direction.x),
            (position.y + s - Math.floor(Math.random() * s2)) + (s * 5 * facing * direction.y),
            (position.z + s - Math.floor(Math.random() * s2)) + 
                ((s + Math.floor(Math.random() * s2)) * 5 * facing * direction.z)
                // I'm unsure why z has this extra factor in it. :P
          );
        } while( distToSegment(this.vertices[i], position, position+direction) <= modelWidth);
        // may want to cap how many times this can run

        this.vertices[i].dieAfter = d.getTime() + 5000; // 5s in milliseconds
        this.vertices[i].hasAppeared = false;
      }
    }
    else
    {
      this.vertices[i].hasAppeared = true;
    }
  }
};

...

// swiped from https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
function sqr(x) { return x * x }
function dist2(v, w) { return sqr(v.x - w.x) + sqr(v.y - w.y) }
function distToSegmentSquared(p, v, w) {
  var l2 = dist2(v, w);
  if (l2 == 0) return dist2(p, v);
  var t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
  return dist2(p, { x: v.x + t * (w.x - v.x),
                    y: v.y + t * (w.y - v.y) });
}
function distToSegment(p, v, w) { return Math.sqrt(distToSegmentSquared(p, v, w)); }
\$\endgroup\$
  • \$\begingroup\$ I have tried to apply the code you provided, but it makes the particles, strangely, disappear after a certain distrance from the origin of the world. I will investigate further. Thank you for your big and insightful answer, in any case. \$\endgroup\$ – Lanbo Dec 5 '15 at 18:13
  • \$\begingroup\$ Interesting. I noticed I had the comparison backwards in (this.vertices[i].dieAfter <= d.getTime() || ... (it was a >=), so if you flip that it might work. I think what you might be seeing is that particles aren't aging, so if they get generated but don't happen to pass into the view frustum, they never get regenerated. Eventually that would happen to every particle, preventing you from generating any more. \$\endgroup\$ – Chris Mills-Price Dec 5 '15 at 18:35
  • \$\begingroup\$ I actually saw that and fixed it, the problem seems to remain. They seem to be re-generated in one place and thus disappear when I fly further. \$\endgroup\$ – Lanbo Dec 5 '15 at 20:00
  • \$\begingroup\$ position is the player's position, correct? If it's being updated and passed to this function as the player moves, I don't see any reason the particles would be generated in a static location... Hmm. \$\endgroup\$ – Chris Mills-Price Dec 5 '15 at 20:08
  • \$\begingroup\$ It even happens when I remove all of your code and just reset the particles. I have played with the values for hours now, I can't figure out why they disappear after exacly 120 from the origin. I suspect it's a problem with rendering, actually, as if they're culled away at that moment. \$\endgroup\$ – Lanbo Dec 5 '15 at 20:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.