Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Canvas Rotating Star Field

I'm taking the following approach to animate a star field across the screen, but I'm stuck for the next part.

JS

var c = document.getElementById('stars'),
    ctx = c.getContext("2d"),
    t = 0; // time

c.width = 300;
c.height = 300;

var w = c.width,
    h = c.height,
    z = c.height,
    v = Math.PI; // angle of vision

(function animate() {

    Math.seedrandom('bg');
    ctx.globalAlpha = 1;

    for (var i = 0; i <= 100; i++) {

        var x = Math.floor(Math.random() * w), // pos x
            y = Math.floor(Math.random() * h), // pos y
            r = Math.random()*2 + 1, // radius
            a = Math.random()*0.5 + 0.5, // alpha

            // linear
            d = (r*a),       // depth
            p = t*d;         // pixels per t

        x = x - p;       // movement
        x = x - w * Math.floor(x / w); // go around when x < 0

        (function draw(x,y) {
            var gradient = ctx.createRadialGradient(x, y, 0, x + r, y + r, r * 2);
            gradient.addColorStop(0, 'rgba(255, 255, 255, ' + a + ')');
            gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');

            ctx.beginPath();
            ctx.arc(x, y, r, 0, 2*Math.PI);
            ctx.fillStyle = gradient;
            ctx.fill();

            return draw;

        })(x, y);

    }

    ctx.restore();
    t += 1;

    requestAnimationFrame(function() {
        ctx.clearRect(0, 0, c.width, c.height);
        animate();
    });
})();

HTML

<canvas id="stars"></canvas>

CSS

canvas {
    background: black;
}

JSFiddle

What it does right now is animate each star with a delta X that considers the opacity and size of the star, so the smallest ones appear to move slower.

Use p = t; to have all the stars moving at the same speed.

QUESTION

I'm looking for a clearly defined model where the velocities give the illusion of the stars rotating around the expectator, defined in terms of the center of the rotation cX, cY, and the angle of vision v which is what fraction of 2π can be seen (if the center of the circle is not the center of the screen, the radius should be at least the largest portion). I'm struggling to find a way that applies this cosine to the speed of star movements, even for a centered circle with a rotation of π.

These diagrams might further explain what I'm after:

Centered circle:

center of vision in x,y

Non-centered:

shifted center

Different angle of vision:

different angle of vision

I'm really lost as to how to move forwards. I already stretched myself a bit to get here. Can you please help me with some first steps?

Thanks


UPDATE

I have made some progress with this code:

        // linear
        d = (r*a)*z,   // depth
        v = (2*Math.PI)/w,
        p = Math.floor( d * Math.cos( t * v ) );     // pixels per t

    x = x + p;       // movement
    x = x - w * Math.floor(x / w); // go around when x < 0

JSFiddle

Where p is the x coordinate of a particle in uniform circular motion and v is the angular velocity, but this generates a pendulum effect. I am not sure how to change these equations to create the illusion that the observer is turning instead.


UPDATE 2:

Almost there. One user at the ##Math freenode channel was kind enough to suggest the following calculation:

        // linear
        d = (r*a),       // depth
        p = t*d;         // pixels per t

    x = x - p;       // movement
    x = x - w * Math.floor(x / w); // go around when x < 0

    x = (x / w) - 0.5;
    y = (y / h) - 0.5;

    y /= Math.cos(x);

    x = (x + 0.5) * w;
    y = (y + 0.5) * h;

JSFiddle

This achieves the effect visually, but does not follow a clearly defined model in terms of the variables (it just "hacks" the effect) so I cannot see a straightforward way to do different implementations (change the center, angle of vision). The real model might be very similar to this one.


UPDATE 3

Following from Iftah's response, I was able to use Sylvester to apply a rotation matrix to the stars, which need to be saved in an array first. Also each star's z coordinate is now determined and the radius r and opacity a are derived from it instead. The code is substantially different and lenghthier so I am not posting it, but it might be a step in the right direction. I cannot get this to rotate continuously yet. Using matrix operations on each frame seems costly in terms of performance.

JSFiddle

like image 876
Alain Jacomet Forte Avatar asked Aug 08 '15 04:08

Alain Jacomet Forte


2 Answers

Here's some pseudocode that does what you're talking about.

Make a bunch of stars not too far but not too close (via rejection sampling) Set up a projection matrix (defines the camera frustum) Each frame     Compute our camera rotation angle     Make a "view" matrix (repositions the stars to be relative to our view)     Compose the view and projection matrix into the view-projection matrix     For each star         Apply the view-projection matrix to give screen star coordinates         If the star is behind the camera skip it         Do some math to give the star a nice seeming 'size'         Scale the star coordinate to the canvas         Draw the star with its canvas coordinate and size 

I've made an implementation of the above. It uses the gl-matrix Javascript library to handle some of the matrix math. It's good stuff. (Fiddle for this is here, or see below.)

var c = document.getElementById('c');  var n = c.getContext('2d');    // View matrix, defines where you're looking  var viewMtx = mat4.create();    // Projection matrix, defines how the view maps onto the screen  var projMtx = mat4.create();    // Adapted from http://stackoverflow.com/questions/18404890/how-to-build-perspective-projection-matrix-no-api  function ComputeProjMtx(field_of_view, aspect_ratio, near_dist, far_dist, left_handed) {      // We'll assume input parameters are sane.      field_of_view = field_of_view * Math.PI / 180.0; // Convert degrees to radians      var frustum_depth = far_dist - near_dist;      var one_over_depth = 1 / frustum_depth;      var e11 = 1.0 / Math.tan(0.5 * field_of_view);      var e00 = (left_handed ? 1 : -1) * e11 / aspect_ratio;      var e22 = far_dist * one_over_depth;      var e32 = (-far_dist * near_dist) * one_over_depth;      return [          e00, 0, 0, 0,          0, e11, 0, 0,          0, 0, e22, e32,          0, 0, 1, 0      ];  }    // Make a view matrix with a simple rotation about the Y axis (up-down axis)  function ComputeViewMtx(angle) {      angle = angle * Math.PI / 180.0; // Convert degrees to radians      return [          Math.cos(angle), 0, Math.sin(angle), 0,          0, 1, 0, 0,          -Math.sin(angle), 0, Math.cos(angle), 0,          0, 0, 0, 1      ];  }    projMtx = ComputeProjMtx(70, c.width / c.height, 1, 200, true);    var angle = 0;    var viewProjMtx = mat4.create();    var minDist = 100;  var maxDist = 1000;    function Star() {      var d = 0;      do {          // Create random points in a cube.. but not too close.          this.x = Math.random() * maxDist - (maxDist / 2);          this.y = Math.random() * maxDist - (maxDist / 2);          this.z = Math.random() * maxDist - (maxDist / 2);          var d = this.x * this.x +                  this.y * this.y +                  this.z * this.z;      } while (           d > maxDist * maxDist / 4 || d < minDist * minDist      );      this.dist = Math.sqrt(d);  }    Star.prototype.AsVector = function() {      return [this.x, this.y, this.z, 1];  }    var stars = [];  for (var i = 0; i < 5000; i++) stars.push(new Star());    var lastLoop = Date.now();    function loop() {            var now = Date.now();      var dt = (now - lastLoop) / 1000.0;      lastLoop = now;            angle += 30.0 * dt;        viewMtx = ComputeViewMtx(angle);            //console.log('---');      //console.log(projMtx);      //console.log(viewMtx);            mat4.multiply(viewProjMtx, projMtx, viewMtx);      //console.log(viewProjMtx);            n.beginPath();      n.rect(0, 0, c.width, c.height);      n.closePath();      n.fillStyle = '#000';      n.fill();            n.fillStyle = '#fff';            var v = vec4.create();      for (var i = 0; i < stars.length; i++) {          var star = stars[i];          vec4.transformMat4(v, star.AsVector(), viewProjMtx);          v[0] /= v[3];          v[1] /= v[3];          v[2] /= v[3];          //v[3] /= v[3];                    if (v[3] < 0) continue;            var x = (v[0] * 0.5 + 0.5) * c.width;          var y = (v[1] * 0.5 + 0.5) * c.height;                    // Compute a visual size...          // This assumes all stars are the same size.          // It also doesn't scale with canvas size well -- we'd have to take more into account.          var s = 300 / star.dist;                              n.beginPath();          n.arc(x, y, s, 0, Math.PI * 2);          //n.rect(x, y, s, s);          n.closePath();          n.fill();      }            window.requestAnimationFrame(loop);  }    loop();
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.3.1/gl-matrix-min.js"></script>  <canvas id="c" width="500" height="500"></canvas>

Some links:

  • More on projection matrices
  • gl-matrix
  • Using view/projection matrices

Update

Here's another version that has keyboard controls. Kinda fun. You can see the difference between rotating and parallax from strafing. Works best full page. (Fiddle for this is here or see below.)

var c = document.getElementById('c');  var n = c.getContext('2d');    // View matrix, defines where you're looking  var viewMtx = mat4.create();    // Projection matrix, defines how the view maps onto the screen  var projMtx = mat4.create();    // Adapted from http://stackoverflow.com/questions/18404890/how-to-build-perspective-projection-matrix-no-api  function ComputeProjMtx(field_of_view, aspect_ratio, near_dist, far_dist, left_handed) {      // We'll assume input parameters are sane.      field_of_view = field_of_view * Math.PI / 180.0; // Convert degrees to radians      var frustum_depth = far_dist - near_dist;      var one_over_depth = 1 / frustum_depth;      var e11 = 1.0 / Math.tan(0.5 * field_of_view);      var e00 = (left_handed ? 1 : -1) * e11 / aspect_ratio;      var e22 = far_dist * one_over_depth;      var e32 = (-far_dist * near_dist) * one_over_depth;      return [          e00, 0, 0, 0,          0, e11, 0, 0,          0, 0, e22, e32,          0, 0, 1, 0      ];  }    // Make a view matrix with a simple rotation about the Y axis (up-down axis)  function ComputeViewMtx(angle) {      angle = angle * Math.PI / 180.0; // Convert degrees to radians      return [          Math.cos(angle), 0, Math.sin(angle), 0,          0, 1, 0, 0,          -Math.sin(angle), 0, Math.cos(angle), 0,          0, 0, -250, 1      ];  }    projMtx = ComputeProjMtx(70, c.width / c.height, 1, 200, true);    var angle = 0;    var viewProjMtx = mat4.create();    var minDist = 100;  var maxDist = 1000;    function Star() {      var d = 0;      do {          // Create random points in a cube.. but not too close.          this.x = Math.random() * maxDist - (maxDist / 2);          this.y = Math.random() * maxDist - (maxDist / 2);          this.z = Math.random() * maxDist - (maxDist / 2);          var d = this.x * this.x +                  this.y * this.y +                  this.z * this.z;      } while (           d > maxDist * maxDist / 4 || d < minDist * minDist      );      this.dist = 100;  }    Star.prototype.AsVector = function() {      return [this.x, this.y, this.z, 1];  }    var stars = [];  for (var i = 0; i < 5000; i++) stars.push(new Star());    var lastLoop = Date.now();      var dir = {      up: 0,      down: 1,      left: 2,      right: 3  };    var dirStates = [false, false, false, false];  var shiftKey = false;    var moveSpeed = 100.0;  var turnSpeed = 1.0;    function loop() {      var now = Date.now();      var dt = (now - lastLoop) / 1000.0;      lastLoop = now;            angle += 30.0 * dt;        //viewMtx = ComputeViewMtx(angle);      var tf = mat4.create();      if (dirStates[dir.up]) mat4.translate(tf, tf, [0, 0, moveSpeed * dt]);      if (dirStates[dir.down]) mat4.translate(tf, tf, [0, 0, -moveSpeed * dt]);      if (dirStates[dir.left])          if (shiftKey) mat4.rotate(tf, tf, -turnSpeed * dt, [0, 1, 0]);          else mat4.translate(tf, tf, [moveSpeed * dt, 0, 0]);      if (dirStates[dir.right])          if (shiftKey) mat4.rotate(tf, tf, turnSpeed * dt, [0, 1, 0]);          else mat4.translate(tf, tf, [-moveSpeed * dt, 0, 0]);      mat4.multiply(viewMtx, tf, viewMtx);            //console.log('---');      //console.log(projMtx);      //console.log(viewMtx);            mat4.multiply(viewProjMtx, projMtx, viewMtx);      //console.log(viewProjMtx);            n.beginPath();      n.rect(0, 0, c.width, c.height);      n.closePath();      n.fillStyle = '#000';      n.fill();            n.fillStyle = '#fff';            var v = vec4.create();      for (var i = 0; i < stars.length; i++) {          var star = stars[i];          vec4.transformMat4(v, star.AsVector(), viewProjMtx);                    if (v[3] < 0) continue;                    var d = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);                    v[0] /= v[3];          v[1] /= v[3];          v[2] /= v[3];          //v[3] /= v[3];                      var x = (v[0] * 0.5 + 0.5) * c.width;          var y = (v[1] * 0.5 + 0.5) * c.height;                    // Compute a visual size...          // This assumes all stars are the same size.          // It also doesn't scale with canvas size well -- we'd have to take more into account.          var s = 300 / d;                              n.beginPath();          n.arc(x, y, s, 0, Math.PI * 2);          //n.rect(x, y, s, s);          n.closePath();          n.fill();      }            window.requestAnimationFrame(loop);  }    loop();    function keyToDir(evt) {      var d = -1;      if (evt.keyCode === 38) d = dir.up      else if (evt.keyCode === 37) d = dir.left;      else if (evt.keyCode === 39) d = dir.right;      else if (evt.keyCode === 40) d = dir.down;      return d;  }    window.onkeydown = function(evt) {      var d = keyToDir(evt);      if (d >= 0) dirStates[d] = true;      if (evt.keyCode === 16) shiftKey = true;  }    window.onkeyup = function(evt) {      var d = keyToDir(evt);      if (d >= 0) dirStates[d] = false;      if (evt.keyCode === 16) shiftKey = false;  }
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.3.1/gl-matrix-min.js"></script>  <div>Click in this pane. Use up/down/left/right, hold shift + left/right to rotate.</div>  <canvas id="c" width="500" height="500"></canvas>

Update 2

Alain Jacomet Forte asked:

What is your recommended method of creating general purpose 3d and if you would recommend working at the matrices level or not, specifically perhaps to this particular scenario.

Regarding matrices: If you're writing an engine from scratch on any platform, then you're unavoidably going to end up working with matrices since they help generalize the basic 3D mathematics. Even if you use OpenGL/WebGL or Direct3D you're still going to end up making a view and projection matrix and additional matrices for more sophisticated purposes. (Handling normal maps, aligning world objects, skinning, etc...)

Regarding a method of creating general purpose 3d... Don't. It will run slow, and it won't be performant without a lot of work. Rely on a hardware-accelerated library to do the heavy lifting. Creating limited 3D engines for specific projects is fun and instructive (e.g. I want a cool animation on my webpage), but when it comes to putting the pixels on the screen for anything serious, you want hardware to handle that as much as you can for performance purposes.

Sadly, the web has no great standard for that yet, but it is coming in WebGL -- learn WebGL, use WebGL. It runs great and works well when it's supported. (You can, however, get away with an awful lot just using CSS 3D transforms and Javascript.)

If you're doing desktop programming, I highly recommend OpenGL via SDL (I'm not sold on SFML yet) -- it's cross-platform and well supported.

If you're programming mobile phones, OpenGL ES is pretty much your only choice (other than a dog-slow software renderer).

If you want to get stuff done rather than writing your own engine from scratch, the defacto for the web is Three.js (which I find effective but mediocre). If you want a full game engine, there's some free options these days, the main commercial ones being Unity and Unreal. Irrlicht has been around a long time -- never had a chance to use it, though, but I hear it's good.

But if you want to make all the 3D stuff from scratch... I always found how the software renderer in Quake was made a pretty good case study. Some of that can be found here.

like image 147
Kaganar Avatar answered Sep 22 '22 11:09

Kaganar


You are resetting the stars 2d position each frame, then moving the stars (depending on how much time and speed of each star) - this is a bad way to achieve your goal. As you discovered, it gets very complex when you try to extend this solution to more scenarios.

A better way would be to set the stars 3d location only once (at initialization) then move a "camera" each frame (depending on time). When you want to render the 2d image you then calculate the stars location on screen. The location on screen depends on the stars 3d location and the current camera location. This will allow you to move the camera (in any direction), rotate the camera (to any angle) and render the correct stars position AND keep your sanity.

like image 27
Iftah Avatar answered Sep 19 '22 11:09

Iftah