0
\$\begingroup\$

I am trying to setup a virtual ping pong table, and am getting a little stuck in trying to understand how to do 3D vector math and render it in 2D.

So far, my plan is for the ball: X = up/down, Y = left/right, and Z = forward/backwards

Image of the desired perspective

So as a simple start, I am just trying to move the ball forward:

ball.vel.x = 0;
ball.vel.y = 0;
ball.vel.z = 1;

in my update function:

ball.position.x += (ball.vel.x / ball.vel.z) * delta;
ball.position.y += (ball.vel.y / ball.vel.z) * delta;

Which obviously makes no sense since 0 / anything is 0.. my ball will never move forward... So I am fundamentally missing something here. What do I need to do so that my Z velocity will actually move the ball's position?

\$\endgroup\$
  • \$\begingroup\$ Generally, you do the whole simulation in 3d then you "flatten" the result to represent it in 2d. It's unclear now what you want to show in 2d. \$\endgroup\$ – Vaillancourt Mar 10 '18 at 1:48
  • \$\begingroup\$ well as I said, my goal would be, ball.vel.x = -1 or 1 would make the ball go up or down over time, ball.vel.y = -1 or 1 would make the ball move left or right over time, and ball.vel.z = -1 or 1 would make the ball move forward or backward... Can you explain how I should be doing this with a code example? \$\endgroup\$ – patrick Mar 10 '18 at 1:56
  • \$\begingroup\$ In 2d there is only up/down/left/right. You have to decide how you convey the fact that the ball is moving in 3d on a 2d screen. Typically, you'll use a projection, either parallel (orthographic) or perspective. That's basically what 3d engines are for :P \$\endgroup\$ – Vaillancourt Mar 10 '18 at 2:00
  • \$\begingroup\$ Unfortunately, I don't have time right now to write a proper answer. \$\endgroup\$ – Vaillancourt Mar 10 '18 at 2:08
  • \$\begingroup\$ Can you give us an image showing us how you want to display the ball on screen? \$\endgroup\$ – Leo Mar 10 '18 at 9:36
0
\$\begingroup\$

First, I'd change up your coordinate system, because it doesn't match common game conventions and that's likely to cause friction when you try to learn from other answers/tutorials out there.

So, I'd propose the following left-handed scheme:

  • x+ is the lateral axis, pointing rightward on your screen
  • y+ is the vertical axis, pointing upward on your screen
  • z+ is the depth axis, pointing diagonally "into" your screen, away from the viewer

Next, we'll track the ball's position AND velocity in 3D (in what we call worldspace), using all 3 axes.

eg.

ball.position.x += ball.velocty.x * deltaTime;
ball.position.y += ball.velocty.y * deltaTime;
ball.position.z += ball.velocty.z * deltaTime;

or if we define vector addition and scalar multiplication on our vector types, we can write this more concisely as ball.position += ball.velocity * deltaTime

This makes it much easier to check things like "did the ball hit the table?" - let's say our table spans -10 to +10 on the x axis, and -5 to 5 on the z axis, at a height of y = 0. Then our bounce check can look like...

if(ball.position.y < table.height /*0*/ ) {
    if(Abs(ball.position.x) <= table.length /*10*/
        && Abs(ball.position.z) <= table.width /*5*/) {
        // In bounds! Let's bounce the ball, 
        // reflecting its position & velocity across the table's plane.
        ball.position.y = 2 * table.height - ball.position.y;
        ball.velocity.y *= -1;
    } else {
        // Ball fell out of bounds. Score a point for someone.
        OutOfBounds();
    }
}

A more accurate collision method would check the line between the previous & current position of the ball, to check if we clipped the table on our way past the edge, but this shows how simple the math can be in worldspace rather than trying to infer depth & test against a parallellogram in screenspace.

When we want to convert a 3D worldspace position into 2D screenspace for display, we use a projection. Here's an example of a simple projection you could use:

Vector2 ProjectToScreenPixels(Vector3 worldspace) {
    Vector2 screenSpace = worldSpace.x * screenX
                        + worldSpace.y * screenY
                        + worldSpace.z * screenZ;

    screenSpace += originOffset;

    /* equivalent to:
    screenSpace.x = worldSpace.x * screenX.x
                  + worldSpace.y * screenY.x
                  + worldSpace.z * screenZ.x
                  + originOffset.x;

    screenSpace.y = worldSpace.x * screenX.y
                  + worldSpace.y * screenY.y
                  + worldSpace.z * screenZ.y
                  + originOffset.y;
    */

    return screenSpace;
}

We use a few configuration parameters to control the projection. These three decide which way on the screen each axis of our 3D world should point, and how it should be scaled or foreshortened:

Vector2 screenX = new Vector2(5, 0);  // +1 unit of x = 5 pixels right on-screen
Vector2 screenY = new Vector2(0, 5);  // +1 unit of y = 5 pixels up on-screen
Vector2 screenZ = new Vector2(2, 2);  // +1 unit of z = ~3 pixels diagonally

This is roughly a Cabinet projection: we keep the x & y axes perpendicular & scaled equally, and foreshorten the depth axis that lays diagonal to them. I chose it as a roughly eyeballed match for the image included in your original question.

You can use an additional originOffset vector to control where the position (0,0,0) should get drawn on your screen, which effectively pans the whole scene around to help you frame it the way you want.

\$\endgroup\$
  • \$\begingroup\$ So, if I am not using a math library, how do I turn "ws.x * sX + ws.y * sY + ws.z * sZ", into something that I can get and X and Y value from independently? I hope I am making sense.. I am having a hard time finding the right words to ask this question. I feel like that function, in my case should be something like: projectTo2D(x,y,z) { x = // math to figure X out..; y = // math to figure Y out; return [x,y]; } what I am asking is how do I break "ws.x * sX + ws.y * sY + ws.z * sZ" into two things that will tell me what x and y are? \$\endgroup\$ – patrick Mar 10 '18 at 22:43
  • 1
    \$\begingroup\$ Just work it out componentwise like the examples I've shown. If I sum up a bunch of multiples of 2D vectors, the x component of the result is just the sum of the multiples of the x components. There's nothing hidden or fancy happening here. See the commend above. \$\endgroup\$ – DMGregory Mar 10 '18 at 22:49
  • \$\begingroup\$ ohhh.. ok now I see.. thank you! makes much more sense now. \$\endgroup\$ – patrick Mar 10 '18 at 23:03
  • \$\begingroup\$ Thank you again for all your help! I have the table and ball setup and everything seems be working well, but now my next problem is: if I want the to be able to choose a velocity that will result in the ball always landing on the opponent's side of the board, I assume I want to calculate the distance to that point? Calculate the XYZ deltas between the ball's current position (player's paddle) and the target position (opponent's side of the table).. once I have take the sqrt of that x^2 + y^2 + z^2, don't I just want to multiply that by each xyz velocity? \$\endgroup\$ – patrick Mar 11 '18 at 1:53
  • 1
    \$\begingroup\$ That sounds like it might be a new question worth asking separately. If your ball arcs under gravity, you might find some previous answers in the projectile-physics tag useful. \$\endgroup\$ – DMGregory Mar 11 '18 at 2:04

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.