Welcome, Guest. Please login or register. Did you miss your activation email?

Author Topic: Drawing a 3d world using SFML and a perspective tranformation.  (Read 5845 times)

0 Members and 1 Guest are viewing this topic.

fun2code

  • Newbie
  • *
  • Posts: 14
    • View Profile
Drawing a 3d world using SFML and a perspective tranformation.
« on: February 06, 2024, 04:13:21 am »
I took this on just to see what I would get graphically, and boy did it take off! I now have my own little 3d world to do physics stuff in!
Here's a bandicam recording of play.
https://youtu.be/7WrP15e1wHo
 It shows most of the good stuff in rapid fashion, but there's much more. I added a help menu for myself as I was losing track of keyboard shortcuts to the various features.

I'll get into some coding details here soon. Briefly, the (abstract) base class persPt (perspective point) for all 3d drawable objects has static members which build in a camera coordinate basis which are rotated to achieve yaw, pitch and roll. I've really been discovering the utility of static class members in recent projects. Here, the camera position and orientation are available in the member functions of all derived drawable types, which makes it straightforward to have each type fully manage its own state. I've achieved some optimizing in my z-ordering, eg. to sort a container of pointers to type persPt which is a shortlist of only those to be drawn this frame, etc.
I'll post some actual code shortly. I do post rarely but I've been quietly chugging away at projects using SFML. I have a solid appreciation for such an easy to use API (for the basics). For comparison, I did my 1st animated space shooter (2d) about 20 years ago.
I used Windows GDI. The OLD stuff, pre GDI+. I hated having to spend 80% of my time coding for the graphics when it's the other 20% that's the important part!
I have a memory leak bug story from that project for another time, except to say that ever since my programs allow the 'esc' key for quitting.

Edit: for further content. I'm quite lost here. I hope this is an intended means to continue. Here's a try at posting code. This is the static persPt function which transforms a 3d position to the required (x,y) position in the window.
// get the window position from the vec3f position in the scene
sf::Vector2f persPt::get_xyw( vec3f Pos )
{
    vec3f Rcp = Pos - camPos;// from camera to Pos
    float U = Rcp.dot( camDir );// projection along camDir = "z - Zcam"
    vec3f Rcp_perp = Rcp - U*camDir;// component of Rcp in plane perpendicular to camDir
    vec3f L = ( Z0/U )*Rcp_perp;// perspective transform applied
    if( U < 0.0f ) L *= -1.0f;
//    vec3f L = ( Z0/Rcp.mag() )*Rcp_perp;// perspective transform applied
    return sf::Vector2f( X0 + L.dot( xu ), Yh - L.dot( yu ) );
}
The offset is to the crosshair position = center of window. Also, the '-' sign on the y component is the only place in the project where my choice of y axis up has to resolve to graphics reality. The vec3f type is just a math vector type of mine. This project is very heavy with vector based math problems. It's been a great exercise for those skills!
I think I'd better go study a wiki here now.

edit: I've attached my solution to the math problem. The code for get_xyw() is based on it.
« Last Edit: February 10, 2024, 12:19:50 am by fun2code »
-fun2code

eXpl0it3r

  • SFML Team
  • Hero Member
  • *****
  • Posts: 11028
    • View Profile
    • development blog
    • Email
Re: Drawing a 3d world using SFML and a perspective tranformation.
« Reply #1 on: February 06, 2024, 10:28:50 am »
This is really cool! :)
Always good when project spiral out of control into something great! ;D

Do you have any specific goal with it, or just continue playing around?

Btw. for screen recording I highly recommend using OBS Studio, it's free and it's not just for streaming, but also great to just capture some video and doesn't add any watermark
Official FAQ: https://www.sfml-dev.org/faq.php
Official Discord Server: https://discord.gg/nr4X7Fh
——————————————————————
Dev Blog: https://duerrenberger.dev/blog/

fun2code

  • Newbie
  • *
  • Posts: 14
    • View Profile
Re: Drawing a 3d world using SFML and a perspective tranformation.
« Reply #2 on: February 06, 2024, 01:22:41 pm »
Hi. Thank you for the encouraging reply.
No, I have no plans for this aside from continuing to tinker with it.
Incidentally, according to output in the terminal, that drone was hit by the homing shot.
The fun part seems to be working the mechanics out.

I would appreciate tips re. better image sources. If the images all look like something that a 5 minute Google image search would turn up for free, it's because they all are.

In this project I dealt with the problem of how to draw a large object when a portion gas fallen behind the camera yet a portion is still visible ahead? My perspective transform gives no usefull position so I don't draw at all. This works out fine in the level posted since it takes a quite close pass to see an object wink out before you've passed it.
But what about rendering a scene within a room? Half of the room is always behind the camera! I struggled with a couple of models before I realized that I'd already solved the essential problem in the 1st perspective level. The red and green lines in the video are a special type which will move an end to keep it in front of the camera so the line can keep being drawn. A wall with 2 of these as the top and bottom edges can have sf::Vertex pinned to these variable positions and the wall surfaces are drawn. The floor and ceiling are cheats, just what wasn't drawn over. I just got a bandicam for this up too:
https://youtu.be/8tqba-nb6nI

Note the 2 table and chair sets. One is 3 chairs (one looks like a table) plus 2 persQuad for the vases. Z ordering is failing because the 5 separate objects are too close together. The other table and chairs + vases is a single composite object which takes care of z ordering its parts properly. The 3 chairs against the wall are also owned by this table.

edit: about the see through doorways. I've rarely used sf::RenderTexture so I struggled to get images in the doorways. Only the indoor or outdoor objects are being drawn at once. I've got split update and draw functions for these 2 groups of objects and the door calls one for each side. I've got it close, but it's not quite right.

The roller coaster rails are also all type persLine. Also, the coaster is ridable. Key 'P' places the camera a bit above the track. It was time to generalize my existing 2d types for programmatic motion to 3d and the coaster is a great test bed for this.
The 3 trails in the latter video are also persLine, with the corners of the trail surface sf::Quad pinned to the magic line ends. These lines are all over now.

edit: I've attached my solutions to several upcoming problems here because there's space: These are all ray based collision tests. They depend on the position of the collider "now" ( point B) and last frame (point A).
For a fixed target:
ball vs persQuad (rectangular area): planarHitTest_sm.jpg
moving ball vs fixed ball: collideBallFixed_sm

ball vs ball both free. Velocity and position assignments are made to conserve momentum and center of mass position. If anyone knows a physics person, please check my solution. 2 pages: ballVballFree1_sm  and freeBall2_sm
« Last Edit: February 10, 2024, 03:50:18 am by fun2code »
-fun2code

fun2code

  • Newbie
  • *
  • Posts: 14
    • View Profile
Thanks also for the links, I'll be following them.
I'd like to make this thread a bit more solid with some more code.
I'm going to just drop the declaration for class persPt. It's well commented.
The class includes both the "global" coordinate basis ( xHat, yHat, zHat ) and the "camera" coordinate basis ( xu, yu, camDir ) and the methods for rotating the view, to support z-ordering, etc. All code is in C++

#include <SFML/Graphics.hpp>
#include <SFML/Window.hpp>

#include <vector>
#include <fstream>
#include <functional>
#include "../vec2f.h"
#include "../spriteSheet.h"

class persPt
{
    public:
    static float X0, Yh;// view point in window
    static float Z0;// for perspective scaling of x and y coordinates
    static vec3f camPos;// camera position in scene
    static vec3f camDir;// direction pointed
    static vec3f yu;    // unit vector "up" (camera orientation)
    static vec3f xu;    // unit vector "to right" : xu = camDir.cross( yu ) maintain in update()
    static const vec3f xHat, yHat, zHat;// these form the "world" or "global" coordinate vector basis
    static float angle;// yaw
    static float pitchAngle;
    static float rollAngle;

    static sf::Vector2f get_xyw( vec3f Pos );// return window position for Pos
    static float changeCamDir( float dAy, float dAp, float dAr );// returns compass heading
    static float changeCamDir_mat( float dAy, float dAp, float dAr );// uses a matrix
    static void calibrateCameraAxes();// maintenance
    static void sortByDistance( std::vector<persPt*>& pPtVec );// this hand rolled sort function is no longer used
    static bool init_stat( std::istream& is );// initialize static members
    static bool compare( persPt* pPtA, persPt* pPtB );// for std::sort() on a container of pointers. This is operator <

    vec3f pos;// position in scene
    float Rbound = 1.0f;// boundary in front of camera. Closer = don&#39;t draw
    bool doDraw = true;// calculate in update() and use in zSort() and draw()
    bool isMoving = false;// no need to update position of object while off screen or behind camera

    void init( vec3f Pos ){ pos = Pos; }
    sf::Vector2f get_xyw()const { return persPt::get_xyw( pos ); }
    float getDistance()const{ return ( pos - persPt::camPos ).mag(); }
    virtual void setPosition( vec3f Pos ){ pos = Pos; }
    void setPosition( float X, float Y, float Z ) { setPosition( vec3f(X,Y,Z) ); }
    virtual void update( float dt ) = 0;
    virtual void draw( sf::RenderTarget& RT ) const = 0;
    virtual void update_doDraw();

    bool operator<( const persPt& rPt ){ return getDistance() < rPt.getDistance(); }

    persPt(){}
    virtual ~persPt(){}
};

The "flight" control allows input for yaw and pitch. The roll amount is calculated when auto-roll is on (default), otherwise the mouse scrollwheel allows enough roll to be applied to counter the roll induced by pitching and yawing around. The camera vector basis (xu,yu,camDir) is well suited to these
changes since:
A "yaw" motion   = rotation about the yu axis.
A "pitch" motion = rotation about the xu axis.
A "roll" motion  = rotation about the camDir axis.
The small rotations of the vector basis are handled by a function where the new basis is constructed from  the existing one.

This has gone through 2 stages.
Initially I updated each base vector for each of the 3 small (frame to frame) angular changes, thusly:

float persPt::changeCamDir( float dAy, float dAp, float dAr )// returns compass heading
{
    return changeCamDir_mat( dAy, dAp, dAr );// matrix based method now in use

    // this code works fine too
    camDir = camDir*cosf(dAy) + xu*sinf(dAy);// yaw
    xu = yu.cross( camDir );

    camDir = camDir*cosf(dAp) - yu*sinf(dAp);// pitch
    yu = camDir.cross( xu );

    yu = yu*cosf(dAr) + xu*sinf(dAr);// roll
    yu /= yu.mag();// renormalize: this has proven necessary - NEW
    xu = yu.cross( camDir );

    // compass heading
    vec3f Rh = camDir - camDir.y*yHat;// component of camDir in x,z plane
    Rh /= Rh.mag();// normalize
    float a = acosf( Rh.dot( zHat ) );
    if( Rh.dot( xHat ) < 0.0f ) a *= -1.0f;
    return a*180.0f/3.14159f;
}

And here's the matrix based method which follows straight from the above literal update assignments:

float persPt::changeCamDir_mat( float dAy, float dAp, float dAr )// returns compass heading
{
    float M[3][3];
    float SnAy = sinf(dAy), CsAy = cosf(dAy);
    float SnAp = sinf(dAp), CsAp = cosf(dAp);
    float SnAr = sinf(dAr), CsAr = cosf(dAr);

    M[0][0] = CsAr*CsAy - SnAr*SnAp*SnAy;    M[0][1] = -SnAr*CsAp;   M[0][2] = -CsAr*SnAy - SnAy*SnAp*SnAr;
    M[1][0] = SnAr*CsAy + SnAy*SnAp*CsAr;    M[1][1] = CsAr*CsAp;    M[1][2] = -SnAr*SnAy + SnAy*SnAp*CsAr;
    M[2][0] = CsAp*SnAy;                     M[2][1] = -SnAp;        M[2][2] = CsAp*CsAy;
    vec3f xcOld = xu, ycOld = yu, zcOld = camDir;

    xu     = M[0][0]*xcOld + M[0][1]*ycOld + M[0][2]*zcOld;
    yu     = M[1][0]*xcOld + M[1][1]*ycOld + M[1][2]*zcOld;
    camDir = M[2][0]*xcOld + M[2][1]*ycOld + M[2][2]*zcOld;

    // compass heading
    vec3f Rh( camDir.x, 0.0f, camDir.z );// = camDir - camDir.y*yHat;// component of camDir in x,z plane
    Rh /= Rh.mag();// normalize
    float a = acosf( Rh.dot( zHat ) );
    if( Rh.dot( xHat ) < 0.0f ) a *= -1.0f;
    return a*180.0f/3.14159f;
}

The following is being called each frame to keep the camera basis from becoming corrupted by the endless sequence of rotations.
The global basis doesn't need this. It's vectors are never varied.

void persPt::calibrateCameraAxes()
{
    persPt::camDir /= persPt::camDir.mag();
    persPt::xu = persPt::yu.cross( persPt::camDir );
    persPt::xu /= persPt::xu.mag();
    persPt::yu = persPt::camDir.cross( persPt::xu );
}

This isn't actually necessary to do every frame. It takes 10 seconds or more for visible aberrations to start creeping in without any correction.
At an early dev stage I wanted to observe these aberrations so I had a button setup to click when it got too bad, and the above code would instantly restore normalcy. A first sign is that objects near the horizon line appear on the wrong side of it.

This seems like plenty for one post, so I'll stop here for now.

edit: I've attached my solutions to the camera vector basis rotation problem.
filename: mathRotateVec_sm.jpg
The matrix based solution is based on the vector problem above. It's 2 pages.
rotateMatrix1_sm.jpg and rotateMatrix2_sm.jpg

edit2: Also attaching solution for calculating firing angles and velocities for aimed shots.
Problem is coming at end but space to attach is here: planarHitTest_sm.jpg
« Last Edit: February 10, 2024, 03:30:07 am by fun2code »
-fun2code

Meerjel01

  • Newbie
  • *
  • Posts: 28
    • View Profile
Re: Drawing a 3d world using SFML and a perspective tranformation.
« Reply #4 on: February 06, 2024, 06:45:02 pm »
I could've sworn that SFML couldn't do 3D?

fun2code

  • Newbie
  • *
  • Posts: 14
    • View Profile
Re: Drawing a 3d world using SFML and a perspective tranformation.
« Reply #5 on: February 06, 2024, 09:58:17 pm »
It isn't doing the 3d part.
This (extremely busy) function is replacing the z coordinate with a
scaling factor = Z0/U.
// get the window position from the vec3f position in the scene
sf::Vector2f persPt::get_xyw( vec3f Pos )
{
    vec3f Rcp = Pos - camPos;// from camera to Pos
    float U = Rcp.dot( camDir );// projection along camDir = "z - Zcam"
    vec3f Rcp_perp = Rcp - U*camDir;// component of Rcp in plane perpendicular to camDir
    vec3f L = ( Z0/U )*Rcp_perp;// perspective transform applied
    if( U < 0.0f ) L *= -1.0f;
    return sf::Vector2f( X0 + L.dot( xu ), Yh - L.dot( yu ) );
}
SFML gets involved at the return. It returns sf::Vector2f so an assignment can be made directly to the sf::Vertex position member.
Here's an example of use in the persQuad class ( 4 vec3f paired with 4 sf::Vertex )
void persQuad::setPosition( vec3f Pos )
{
    vec3f dPos = Pos - pos;
    pos = Pos;
    for( size_t i = 0; i < 4; ++i )
    {
        pt[i] += dPos;// the pt[i] are the 4 positions in 3d space
        vtx[i].position = persPt::get_xyw( pt[i] );// which the 4 sf::Vertex are mapped to
    }
    update_doDraw();// am I in the viewable window area?
}
I hope this squares the mystery.

Here's the persQuad::update function. It supports a "billboarding" method whereby it always faces the camera. This makes a flat image (eg. of a tree) look much more 3d.
The setNu() sets the surface normal vector and all of the vtx[] positions.
void persQuad::update( float dt )
{
    update_doDraw();
    if( !doDraw ) return;

    if( facingCamera )
    {
        vec3f N = persPt::camPos - pos;
        N /= N.mag();
        setNu( N );
    }
    else
    {
        for( size_t i = 0; i < 4; ++i )
            vtx[i].position = persPt::get_xyw( pt[i] );
    }
}

The vases on the tables in the latter video have facingCamera = true and are "billboarding" a flat image. The eye doesn't notice that the view of the flowers is always the same.
« Last Edit: February 06, 2024, 11:19:01 pm by fun2code »
-fun2code

Hapax

  • Hero Member
  • *****
  • Posts: 3379
  • My number of posts is shown in hexadecimal.
    • View Profile
    • Links
Re: Drawing a 3d world using SFML and a perspective tranformation.
« Reply #6 on: February 07, 2024, 05:07:17 pm »
This is pretty impressive. Nice work! :o

I see you're also encountering the age-old SFML issue of textured quads rotating in 3D space. ;D
Because they're treated as triangles and not as quads, the textures have no knowledge of the point from the other triangle so cannot interpolate as if perspective. You can see this (it's subtle) in your videos. I've managed to find a couple of solutions to this (subdivision or shader per quad) but I've not ventured into large-scale applications of this so I wish you luck with that! Obviously shader per quad could be excessive (one draw call per quad and can't easily be drawn behind everything else) and subdivision can stack the number of those triangles up very quickly!
My examples (as part of Selba Ward) of those two methods are here: Sprite 3D (subdivision) and Elastic Sprite (perspective shader) and you may find them useful while looking for your own solution
It genuinely might be better to get better looking coloured objects than using textured quads but it depends on your intent (or just your interest of looking for problems to solve!)
Selba Ward -SFML drawables
Cheese Map -Drawable Layered Tile Map
Kairos -Timing Library
Grambol
 *Hapaxia Links*

fun2code

  • Newbie
  • *
  • Posts: 14
    • View Profile
Re: Drawing a 3d world using SFML and a perspective tranformation.
« Reply #7 on: February 07, 2024, 07:57:26 pm »
Hi. Thank you for that information Hapax. I'm very interested in that.
I followed your Selba Ward link. I'll be back...
I've come here today to continue with presenting this so that someone could run with it, design their own derived persPt types and continue. You may note the wireframe boxes in the 1st video for instance: A type with 24 sf::Vertex (for drawing 12x sf:Lines)  mapped to 8 vec3f points in 3d space.
The following will only become more evident the  more I show...
My focus on the mechanics...I've learned only enough about how to use SFML to accomplish the immediate need. Links to tutorials will be followed.

OK. On to my planned post, which begins with a closer look at how z ordering is supported in the persPt class. I've changed 2 lines in the previous declaration to make a relationship clear:
bool operator<( const persPt& rPt ){ return getDistance() < rPt.getDistance(); }
static bool compare( persPt* pPtA, persPt* pPtB ) { return *pPtB < *pPtA; }// for std::sort() on a container of pointers.
 
The compare() is for use as the 3rd argument in a call to std::sort() on a container of pointers to type persPt. The definition of operator < determines the "z order" = sorted by distance in front of the camera.
Which is almost all I got to say about that.
This line which is commented out in the 1st definition of get_xyw
vec3f L = ( Z0/Rcp.mag() )*Rcp_perp;
would base the scaling on the distance from the camera, which may produce a better peripheral scaling effect. I tried and found 2 things:
1. No noticeable improvement
2. It breaks type persLine. I haven't explored why
So the current option is chosen.

As before I'l just drop the declaration.
class persQuad : public persPt
{
    public:// zero encapsulation discipline on display

    // data members
    vec3f pt[4];
    vec3f Nu;// unit vector perpendicular to Quad
    sf::Vertex vtx[4];
    const sf::Texture* pTxt = nullptr;
    float w, h;// width and height of the quad
    bool facingCamera = false;// enables "billboarding"

    // function members
    vec3f getPosition()const{ return ( pt[0] + pt[2] )/2.0f; }
    virtual void update( float dt );
    virtual void draw( sf::RenderTarget& RT ) const;
    virtual void setPosition( vec3f Pos );
    void setNu( vec3f nu );// to default orientation: w side is horizontal, quad is tilted upwards
    void setOrientation( vec3f nu, vec3f Tu );// general orientation: pt[0], pt[1] toward Tu

    // Ta: &#39;R&#39; = rotate 90 degrees, Tb = 0, 1, 2, 3 = # times
    // Ta = &#39;F&#39; = flip image around Tb = &#39;X&#39; or &#39;Y&#39; axis
    void setTxtRect( sf::IntRect srcRect, char Ta, char Tb );// assigns texCoords with some mapping variability
    void flip( char XorY );// sometimes the required orientation needs a flip only

    // returns true when hit and writes collision point = P and unit vec3f = vu in reflected direction
    bool hit( vec3f posA, vec3f posB, vec3f& P, vec3f& vu )const;// also persBox_quad
    bool isSighted( float& dist )const;// true if aimed at. Writes distance to aim point on persQuad
    void setColor( sf::Color color );

    persQuad(){}
    virtual ~persQuad(){}

    void init( std::istream& is, sf::Texture* p_Txt = nullptr );
    persQuad( std::istream& is, sf::Texture* p_Txt = nullptr ){ init( is, p_Txt ); }

    void init( vec3f Pos, float W, float H, vec3f nu, sf::Color color, const sf::Texture* p_Txt = nullptr );
    persQuad( vec3f Pos, float W, float H, vec3f nu, sf::Color color, sf::Texture* p_Txt = nullptr )
    { init( Pos, W, H, nu, color, p_Txt ); }
};
 
Most of the data members serve an obvious purpose. The setPosition and update functions are covered. Here's the draw function:
void persQuad::draw( sf::RenderTarget& RT ) const
{
    if( !doDraw ) return;
    if( pTxt ) RT.draw( vtx, 4, sf::Quads, pTxt );
    else RT.draw( vtx, 4, sf::Quads );
}
 
In the setNu and setOrientation functions a vector basis is constructed for assigning the 4 pt[] positions. SetNu uses yHat as a 2nd vector whereas setOrientation uses the 2nd argument. This was added to make the drone over the coaster work.
void persQuad::setNu( vec3f nu )
{
    // from init() code
    Nu = nu;
    vec3f vw = nu.cross( persPt::yHat );
    if( vw.mag() < 0.2f )// nu too close to yh
        vw = nu.cross( persPt::zHat ); // go off of zh instead
    vw /= vw.mag();
    vec3f vh = vw.cross( nu );
    vec3f Pos = pos;
    Pos -= (w/2.0f)*vw;
    Pos -= (h/2.0f)*vh;
    pt[0].x = pt[0].y = pt[0].z = 0.0f;// lower left
    pt[1] = h*vh;// up left
    pt[3] = w*vw;// lower right
    pt[2] = pt[1] + pt[3];// up right
    for( size_t i = 0; i < 4; ++i )
    {
        pt[i] += Pos;
        vtx[i].position = persPt::get_xyw( pt[i] );
    }
}

// unit length of nu and Tu is assumed here
void persQuad::setOrientation( vec3f nu, vec3f Tu )
{
    Nu = nu;
    vec3f T1 = Nu.cross( Tu );// in W direction
    float H = 0.5f*h, W = 0.5f*w;
    pt[0] = pos + H*Tu - W*T1;
    pt[1] = pos + H*Tu + W*T1;
    pt[2] = pos - H*Tu + W*T1;
    pt[3] = pos - H*Tu - W*T1;
    for( size_t i = 0; i < 4; ++i )
        vtx[i].position = persPt::get_xyw( pt[i] );
}
 
The vector Nu is most usefull for determining which quads to draw. For example:
The windows in a room all have Nu facing outward. All windows are to be drawn when inside the room, but when outside I want to draw only the windows facing the camera.
Let PQ be an instance of a persQuad:
if( ( persPt::camPos - PQ.pos ).dot( PQ.Nu ) > 0.0f )
then draw the window.

Let's see how it also helps with collision testing, range finding, target acquisition, etc...
I've prepared another video. The method bool isSighted( float& distance ) is used to highlight the persQuad before the crosshair. distance*persPt::camDir = vector range to target. The velocities to fire are calculated (for both the high and low firing angles).
In the video I adjust the shot speed and gravity so that our position is a bit within the maximum range. The key 'D' will fire the high shot immediately then fire the low shot after a delay = calculated difference in the flight times so the shots hit simultaneously.
Both velocities are found and saved so I can change aim once the 1st shot is off. I walk double hits back and forth across a row of targets...
https://youtu.be/eo7LYYbmq1A
I'll be back to present these
bool hit( vec3f posA, vec3f posB, vec3f& P, vec3f& vu )const;// writes point hit and unit vector in reflected direction
bool isSighted( float& dist )const;// true if aimed at. Writes distance to aim point on persQuad
 
I've attached vec2f.h and vec2f.cpp, which also define type vec3f.
To eXpl0it3r: I'll be checking out those replacements for bandicam soon!

I'm back. On reflection. Presenting and explaining the above would go well beyond "introducing a project", so I've also attached persPt.h and persPt.cpp.
All of my types in use are there. I'm sorry I don't have a GitHub or similar.
I'll also be attaching pictures of my solutions on paper to some of the vector math problems involved here. Some are involved. eg.
bool persBall::hitFree( persBall& collider );
is written to conserve momentum correctly and to assign the positions so that the position of the center of mass is unchanged, which matters if a force field is applied a spontaneous jump in potential energy may matter.
etc...

edit: I've attached my collision test solutions here (earlier post w/room)
https://en.sfml-dev.org/forums/index.php?topic=29412.msg181198#msg181198
« Last Edit: February 10, 2024, 04:07:32 am by fun2code »
-fun2code

fun2code

  • Newbie
  • *
  • Posts: 14
    • View Profile
Re: Drawing a 3d world using SFML and a perspective tranformation.
« Reply #8 on: February 08, 2024, 08:10:28 pm »
Hi again all. This seems like the right time to attach any further files for those interested. This thread is still on top so I won't be bumping it, which I won't want to do without good reason once it's no longer on top.
I'm attaching files for some of the composite types, in order of dependencies which I will remark on as needed.
This order:

Typename    required #includes
-----------     --------------------
persLine         persPt (and all below), vec3d
persTrail         persLine
persWall         persLine

persChair        persPt only
persDoor         persPt only
persTable        persChair
persRoom       persTable, persWall and persDoor

I'm not sure why vec3d, but it includes vec2d, where vec3 is also defined
You get the picture. It's not spectacularly well organized. There are signs of rushed development throughout.
I was hesitant to present persLine because honestly, I can't understand how it's working by looking at the code. I must have had a certain highly tuned picture in mind when I wrote it. It works.
Hopefully someone finds them interesting.
-fun2code

fun2code

  • Newbie
  • *
  • Posts: 14
    • View Profile
Re: Drawing a 3d world using SFML and a perspective tranformation.
« Reply #9 on: February 08, 2024, 08:13:00 pm »
Continuing with persTrail and persWall to finish with those having direct dependence on persLine
-fun2code

fun2code

  • Newbie
  • *
  • Posts: 14
    • View Profile
Re: Drawing a 3d world using SFML and a perspective tranformation.
« Reply #10 on: February 08, 2024, 08:15:03 pm »
persChair and persDoor #include persPt only.
One more post to follow.
-fun2code

fun2code

  • Newbie
  • *
  • Posts: 14
    • View Profile
Re: Drawing a 3d world using SFML and a perspective tranformation.
« Reply #11 on: February 08, 2024, 08:19:42 pm »
Lastly, persTable requires persChair. This is the composite type mentioned earlier.
and persRoom #include wall, door and table.
-fun2code

fun2code

  • Newbie
  • *
  • Posts: 14
    • View Profile
Re: Drawing a 3d world using SFML and a perspective tranformation.
« Reply #12 on: February 08, 2024, 08:48:22 pm »
Uh oh. Type shotBurst might also be interesting. It's the fragmenting shot type from the 1st video. It inherits from persBall, so it's a persBall type and can be handled like any other shot.
shotBurst doesn't contain the shots released when it "goes off". It calls a getShot function which draws from the 100 shots in the pool in use in the level. I've only 1 instance in use, but once it bursts it's done and can be fired again. In the 1st video you may see an active shot count: shots xxx. Notice this jump by 22 or so when the shell bursts. I've tested by firing into the sky until the shot pool is exhausted. Nothing bad happens. You get just the fireball if all 100 shots are in use.

edit: I've gone back and attached some math solutions which my code is based on:

perspective transformation. Basis for get_xyw() here:
https://en.sfml-dev.org/forums/index.php?topic=29412.msg181195#msg181195

The camera basis rotations both vector and matrix based.
and Firing angle and velocities for aimed shots. Here:
https://en.sfml-dev.org/forums/index.php?topic=29412.msg181199#msg181199

Collision tests: ball vs rectangle, moving ball vs fixed ball and both balls free. Here:
https://en.sfml-dev.org/forums/index.php?topic=29412.msg181198#msg181198
« Last Edit: February 10, 2024, 03:47:57 am by fun2code »
-fun2code

fun2code

  • Newbie
  • *
  • Posts: 14
    • View Profile
Re: Drawing a 3d world using SFML and a perspective tranformation.
« Reply #13 on: February 10, 2024, 08:34:43 pm »
Hi all!
I'm adding this post for those who are curious about how I handled the motion of the pendulum in the 1st video. It goes to the limits of my abilities in several ways.
I had an opportunity while working 3rd shift for a couple of years to finally
take on that long overdue review of classical mechanics. I had (typically) 6 hours per shift of free time, and it was nice being paid $22/hour to do it.
I obtained and studied from: Introduction to Classical Mechanics by David Morin. 1st edition (in case there's a 2nd by now).
My educational background: BS in physics long ago.
I'm using skills which would have faded to nothing by now if not for my usage in this hobby.
My goal was to make it through chapter 6 on the Lagrangian method. It's an alternative formulation (to vector based Newtonian mechanics) based on scalar quantities (just a number) such as kinetic and potential energy and is well suited to problems where motion is limited by a constraint, such as being limited to motion on the surface of a sphere in 3d space like this pendulum.

I highly recommend the textbook. It is as hardcore as anyone could want and it's loaded with fully worked solutions for a portion of the very full range of problems in each chapter. It's also loaded with clever little physics limerics. I'll post 1 or 2 here later.
This text has a place of honor in my work here. It can be seen floating in midair in the room in the 2nd video.

Please find attached type persPendulum. It's virtual update() has a bit more than usual going on, which gets right away to an issue with the enormous time step between frames, and to why type vec3d became involved.
My simplistic numerical integration scheme is this:

step 1: obtain the acceleration to be applied this frame.
In the case of the shots the acceleration is constant = (0,g,0).
In the case of the pendulum it's far from that.

step 2:
update the position:
position += velocity*dt + 0.5f*acceleration*dt*dt;
This is from Taylors series to 2 terms.
Then update the velocity
velocity += acceleration*dt;

The analysis will be in spherical coordinates.
So far these methods are persPendulum methods only, but I think they also
should be static methods available in vec3d?
// base vectors in spherical coordinates
vec3d get_rHat()const{ return sin(th)*( cos(phi)*Xu + sin(phi)*Yu ) - cos(th)*Zu; }
vec3d get_thHat()const{ return cos(th)*( cos(phi)*Xu + sin(phi)*Yu ) + sin(th)*Zu; }
vec3d get_phiHat()const{ return cos(phi)*Yu - sin(phi)*Xu; }
 
Where the vectors Xu, Yu, Zu are the cartesian basis.
I'll also attach my solution on paper soon, but I want it to be much cleaner than the working version I have.

The identifiers for the two angles theta (polar) and phi (azimuthal) and their derivatives are:
// polar angle and 1st and 2nd time derivatives
double th, th_1, th_2;
// azimuthal angle and 1st and 2nd time derivatives
double phi, phi_1, phi_2;

The 2 equations of motion obtained by the Lagrangian method are:
// step 1: find acceleration
th_2 = phi_1*phi_1*sinTh*cosTh - GravY*sinTh/L;// for theta
phi_2 = -2.0*th_1*phi_1*cosTh/sinTh;// for phi
 
Here, sinTh, cosTh are saved values of sin(th), cos(th), GravY = gravity.y and L = length of support. Note that the mass isn't a factor.

An additional condition may also be applied, perhaps to stabilize the integration?
Because the Lagrangian = kinetic - potential energy doesn't depend explicitly on the angle phi, there is a constant of the motion. Here it's the z component of the angular momentum so:
phi_1 = Lz/( sin(th)*sin(th) );// based on Lz conservation
where the value of Lz is determined by initial conditions. The quantity here has been scaled from the actual Lz: Scaled this is actually Lz /= m*L*L.
So the units of measure ought to balance above.

Integration at the frame rate ( dt = 20ms ) does not give a stable result.
It became stable at 10 updates per frame, so I tripled that and called it good.
The pendulum in the video is being updated 30 times per frame.

Once all of these micro updates are done I assign bobs position and velocity.
I'm still struggling with the float/double interface here.
// bobs new velocity
        vec3d thHat = cosTh*( cosPhi*Xu + sinPhi*Yu ) + sinTh*Zu;// current thHat
        vec3d phiHat = cosPhi*Yu - sinPhi*Xu;// current phiHat
        Vel = L*( th_1*thHat + sinTh*phi_1*phiHat );
        bob.vel = Vel.to_vec3f();// bob.vel update
        vec3d rHat = sinTh*( cosPhi*Xu + sinPhi*Yu ) - cosTh*Zu;// updated rHat
        // and position
        vec3d Pos = L*rHat;
        Pos.x += static_cast<double>(topQ.pos.x);
        Pos.y += static_cast<double>(topQ.pos.y);
        Pos.z += static_cast<double>(topQ.pos.z);
        bob.setPosition( Pos.to_vec3f() );// bob update
 

I'll leave off here for now. But I must deal with the sudden change in th_1 and phi_1 which occur when poor bob gets shot.

I'm back to wrap up.
As for handling bobs hits:
I'm cheating a bit. I call hitFree() and do not account for the constraint in the collision,
so the momentum transferred isn't accurate, but it seems to do fine for effect. This serves the present purpose well enough for me. Next problem please.

Update the values for th_1 and phi_1
bobs velocity in polar coordinates is:
vec3d persPendulum::get_vel()const
{ return vec3d( L*( th_1*get_thHat() + sin(th)*phi_1*get_phiHat() ) ); }
Let vec3d Vel = bob.vel following the collision, then
Taking the dot product (of the return value above) with thHat (unit vector in theta direction) gives
Vel.dot( thHat ) = L*th_1
solving for th_1 permits the update
th_1 = Vel.dot(thHat)/L;
Take the dot product with phiHat instead to obtain
phi_1 = Vel.dot( phiHat )/( L*sin(th) );
following this I update Vel and then bob.vel
Vel = get_vel();
bob.vel = Vel.to_vec3f();

Just call
bool hit( persBall& PB, float cR, float dt );
where Cr = coefficient of restitution allows partially elastic collisions.
Why dt? It won't be moved here, just in update(). The collision test is ray based so dt is used to obtain the position in the previous frame = 2 points for a ray.

Finally, I'll admit that the integration details are well beyond my knowledge.
I think I should be using th_1 = Lz/( sin(th)*sin(th) ); instead of the 2nd acceleration equation, which actually follows from above. I'm presently experimenting with using
phi_1 = Lz/( sinTh*sinTh );
phi += phi_1*dT;
in place of
phi_2 = -2.0*th_1*phi_1*cosTh/sinTh;
phi += phi_1*dT + 0.5*phi_2*dT*dT;
phi_1 += phi_2*dT;
and it's giving a stable result for lower # of micro updates.
However, I'm once again getting runaway cases...
It's a fight to maintain stability and I don't know enough right here.
Perhaps someone who knows this part better may advise? Please?

I've also now attached my "on paper" solution.
And that's all I've got so I'll call it a wrap.
Any questions and/or suggestions are welcome.
« Last Edit: February 12, 2024, 02:44:52 pm by fun2code »
-fun2code

 

anything