简体   繁体   中英

Simulate a physical 3d ball throw on a 2d js canvas from mouse click into the scene

I'd like to throw a ball (with an image) into a 2d scene and check it for a collision when it reached some distance. But I can't make it "fly" correctly. It seems like this has been asked like a million times, but with the more I find, the more confused I get.. Now I followed this answer but it seems, like the ball behaves very different than I expect. In fact, its moving to the top left of my canvas and becoming too little way too fast - ofcouse I could adjust this by setting vz to 0.01 or similar, but then I dont't see a ball at all...

This is my object (simplyfied) / Link to full source who is interested. Important parts are update() and render()

var ball = function(x,y) {

  this.x        = x;
  this.y        = y;
  this.z        = 0;
  this.r        = 0;
  this.src      = 'img/ball.png';
  this.gravity  = -0.097;

  this.scaleX   = 1;
  this.scaleY   = 1;

  this.vx       = 0;
  this.vy       = 3.0;
  this.vz       = 5.0;

  this.isLoaded = false;

  // update is called inside window.requestAnimationFrame game loop
  this.update = function() {
    if(this.isLoaded) {
      // ball should fly 'into' the scene
      this.x += this.vx;
      this.y += this.vy;
      this.z += this.vz;

      // do more stuff like removing it when hit the ground or check for collision
      //this.r += ?

      this.vz += this.gravity;
    }
  };

  // render is called inside window.requestAnimationFrame game loop after this.update()
  this.render = function() {
    if(this.isLoaded) {

      var x       = this.x / this.z;
      var y       = this.y / this.z;

      this.scaleX = this.scaleX / this.z;
      this.scaleY = this.scaleY / this.z;

      var width   = this.img.width * this.scaleX;
      var height  = this.img.height * this.scaleY;

      canvasContext.drawImage(this.img, x, y, width, height);

    }
  };

  // load image
  var self      = this;
  this.img      = new Image();
  this.img.onLoad = function() {
    self.isLoaded = true;
    // update offset to spawn the ball in the middle of the click
    self.x        = this.width/2;
    self.y        = this.height/2;
    // set radius for collision detection because the ball is round
    self.r        = this.x;
  };
  this.img.src = this.src;

} 

I'm also wondering, which parametes for velocity should be apropriate when rendering the canvas with ~ 60fps using requestAnimationFrame, to have a "natural" flying animation

I'd appreciate it very much, if anyone could point me to the right direction (also with pseudocode explaining the logic ofcourse).

Thanks

I think the best way is to simulate the situation first within metric system.

speed = 30; // 30 meters per second or 108 km/hour -- quite fast ...
angle = 30 * pi/180;  // 30 degree angle, moved to radians.

speed_x = speed * cos(angle);
speed_y = speed * sin(angle);  // now you have initial direction vector

x_coord = 0;
y_coord = 0;  // assuming quadrant 1 of traditional cartesian coordinate system

time_step = 1.0/60.0;    // every frame...

// at most 100 meters and while not below ground
while (y_coord > 0 && x_coord < 100) {

   x_coord += speed_x * time_step;
   y_coord += speed_y * time_step;

   speed_y -= 9.81 * time_step;   // in one second the speed has changed 9.81m/s

   // Final stage: ball shape, mass and viscosity of air causes a counter force
   // that is proportional to the speed of the object. This is a funny part:
   // just multiply each speed component separately by a factor (< 1.0)
   // (You can calculate the actual factor by noticing that there is a limit for speed
   //  speed == (speed - 9.81 * time_step)*0.99, called _terminal velocity_
   // if you know or guesstimate that, you don't need to remember _rho_,
   // projected Area or any other terms for the counter force.

   speed_x *= 0.99; speed_y *=0.99;
}

Now you'll have a time / position series, which start at 0,0 (you can calculate this with Excel or OpenOffice Calc)

speed_x        speed_y       position_x     position_y    time 
25,9807687475  14,9999885096 0              0             0 
25,72096106    14,6881236245 0,4286826843   0,2448020604  1 / 60
25,4637514494  14,3793773883 0,8530785418   0,4844583502  2 / 60
25,2091139349  14,0737186144 1,2732304407   0,7190203271
...
5,9296028059   -9,0687933774 33,0844238036  0,0565651137  147 / 60
5,8703067779   -9,1399704437 33,1822622499 -0,0957677271  148 / 60

From that sheet one can first estimate the distance of ball hitting ground and time. They are 33,08 meters and 2.45 seconds (or 148 frames). By continuing the simulation in excel, one also notices that the terminal velocity will be ~58 km/h, which is not much.

Deciding that terminal velocity of 60 m/s or 216 km/h is suitable, a correct decay factor would be 0,9972824054451614.

Now the only remaining task is to decide how long (in meters) the screen will be and multiply the pos_x, pos_y with correct scaling factor. If screen of 1024 pixels would be 32 meters, then each pixel would correspond to 3.125 centimeters. Depending on the application, one may wish to "improve" the reality and make the ball much larger.

EDIT : Another thing is how to project this on 3D. I suggest you make the path generated by the former algorithm (or excel) as a visible object (consisting of line segments), which you will able to rotate & translate.

The origin of the bad behaviour you're seeing is the projection that you use, centered on (0,0), and more generally too simple to look nice.
You need a more complete projection with center, scale, ...
i use that one for adding a little 3d :

     projectOnScreen : function(wx,wy,wz) {
            var screenX =   ... real X size of your canvas here ... ;
            var screenY =  ... real Y size of your canvas here ... ;
            var scale   = ... the scale you use between world / screen coordinates ...;
            var ZOffset=3000; // the bigger, the less z has effet
            var k =ZOffset;  // coeficient to have projected point = point for z=0
            var zScale =2.0; // the bigger, the more a change in Z will have effect

            var worldCenterX=screenX/(2*scale);
            var worldCenterY=screenY/(2*scale);


            var sizeAt = ig.system.scale*k/(ZOffset+zScale*wz);
            return {
                     x: screenX/2  +  sizeAt * (wx-worldCenterX) ,
                     y: screenY/2  +  sizeAt * (wy-worldCenterY) ,
                     sizeAt : sizeAt
                  }
        }

Obviously you can optimize depending on your game. For instance if resolution and scale don't change you can compute some parameters once, out of that function.
sizeAt is the zoom factor (canvas.scale) you will have to apply to your images.

Edit : for your update/render code, as pointed out in the post of Aki Suihkonen, you need to use a 'dt', the time in between two updates. so if you change later the frame per second (fps) OR if you have a temporary slowdown in the game, you can change the dt and everything still behaves the same.
Equation becomes x+=vx*dt / ... / vx+=gravity*dt;
you should have the speed, and gravity computed relative to screen height, to have same behaviour whatever the screen size.
i would also use a negative z to start with. to have a bigger ball first. Also i would separate concerns :
- handle loading of the image separatly. Your game should start after all necessary assets are loaded. Some free and tiny frameworks can do a lot for you. just one example : crafty.js, but there are a lot of good ones.
- adjustment relative to the click position and the image size should be done in the render, and x,y are just the mouse coordinates.

var currWidth = this.width *scaleAt, currHeight= this.height*scaleAt;
canvasContext.drawImage(this.img, x-currWidth/2, y-currHeight/2, currWidth, currHeight);

Or you can have the canvas to do the scale. bonus is that you can easily rotate this way :

 ctx.save();
 ctx.translate(x,y);
 ctx.scale(scaleAt, scaleAt);  // or scaleAt * worldToScreenScale if you have 
                               // a scaling factor
 // ctx.rotate(someAngle);   // if you want...
 ctx.drawImage(this.img, x-this.width/2, x-this.height/2);
 ctx.restore();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM