Реалистичная симуляция вращения сферы на полу
Я пытаюсь смоделировать сферу, катящуюся по полу. Для симуляции я использую старую добрую библиотеку Papervision3D Flash AS3, но на самом деле это не имеет значения, это вопрос чистой геометрии.
Предполагая, что у меня есть объект Sphere3D, для которого я могу установить свойства вращение X, вращение Y и вращение Z, как я могу рассчитать вращение по каждой оси, где эта сфера катится по полу?
Например, давайте предположим, что сфера находится в покое. Теперь он катится 1 метр вправо. Если я смотрю на эту сферу сверху - я хочу повернуть ее вокруг оси Z на 90 градусов. Тогда сфера должна катиться "вниз" вдоль пола, поэтому я хочу вращать ее вокруг оси X, но эта проблема заключается в том, что в то же время ось X вращалась сама по себе, когда я вращал сферу вдоль оси Z.
Как я могу решить эту проблему?
Спасибо
1 ответ
Если скольжения нет, тогда:
ось вращения
будет параллельно вашему полу и перпендикулярно вашему движению. Таким образом, вы можете использовать кросс-продукт, чтобы получить его. Позволять:
n
- пол нормальный векторt
- направление движения параллельно полу (касательное)b
- наша ось вращения (бинормальная)поэтому мы можем вычислить это как:
b = cross(t,n) // cross product create perpendicular vector to both operands t /= |t| // normalize to unit vector b /= |b| // normalize to unit vector n /= |n| // normalize to unit vector
скорость вращения
это может быть получено из длины волны и скорости
vel [unit/s]
, Так что, если наша сфера имеет радиусr
затем:ang*r = vel*t ang = vel*t/r // t=1.0 sec omg = vel/r // [rad/sec]
поэтому нам нужно вращать нашу сферу
omg
каждую секунду.математика вращения
Углы Эйлера (ваши последовательные повороты X,Y,Z) - наихудшая вещь для этого, о которой я могу думать, поскольку они приведут к особенностям и странным вещам, превращая этот простой пример в ужасный кошмар для реализации. Вы видели в игре или каком-либо 3D-движке, что вдруг вы не можете выглядеть так, как ожидаете, или случайным образом вращаетесь, пока не начнете двигаться / вращаться по-другому или внезапно не повернетесь на 180 градусов...? Это особенности углов Эйлера на работе без надлежащей обработки...
Кватернионы несколько чужды большинству людей (включая меня), поскольку они не работают так, как мы думаем. IIRC Вы можете рассматривать их как эффективный способ вычисления трехмерной матрицы вращения 3x3 с меньшим количеством гониометрических функций. Поскольку сейчас у нас сильно отличается вычислительная мощность, чем 20 лет назад, нет особого смысла выбирать их, если вы их совсем не знаете. В любом случае, у них есть и другие преимущества, которые по-прежнему актуальны, например, вы можете интерполировать вращения и т. Д.
Матрицы однородного преобразования 4x4 - ваш лучший выбор. Поскольку их геометрическое представление совместимо с абстрактным мышлением человека (вы можете представить, что и как это делается, поэтому вы можете создавать свои собственные матрицы вместо того, чтобы иметь их в виде набора бессмысленных чисел).
Я настоятельно рекомендую начать с трехмерных матриц однородного преобразования 4x4. Так что все остальное в этом ответе будет нацелено на них.
вращающийся
Теперь есть 2 способа, которыми я знаю, как добиться вашего вращения. Либо используйте https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula и закодируйте его как матрицу преобразования, либо просто создайте свою собственную матрицу вращения, которая будет представлять вашу сферу, выровненную по полу. направление движения и ось вращения.
Последнее намного проще, и мы можем сделать это напрямую, так как мы уже знаем 3 необходимых базисных вектора (
t,b,n
). Остается только сферическая позиция, которая также должна быть известна.Итак, в начале создайте матрицу преобразования (при условии обозначения OpenGL):
| tx bx nx x0 | | ty by ny y0 | | tz bz nz z0 | | 0 0 0 1 |
куда
x0,y0,z0
это начальная позиция вашей сферы, выровненная по вашей сетке. Так что если центральная точка вашей сетки(0,0,0)
тогда поместите свою сферуr
над полом...Теперь просто каждый прошедший раз
dt [sec]
(как таймер) умножить эту матрицу на матрицу инкрементного вращения вокругy
ось (какb
наша ось вращения) и уголomg*dt [rad]
,Нам также нужно перевести нашу сферу
t*vel*dt
просто добавьте этот вектор в положение матрицы или умножьте нашу матрицу на:| 1 0 0 tx*vel*dt | | 0 1 0 ty*vel*dt | | 0 0 1 tz*vel*dt | | 0 0 0 1 |
А также визуализируйте сцену снова, используя нашу результирующую матрицу... Этот подход хорош тем, что вы можете в любое время изменить направление движения (вы просто запомните положение и измените внутреннюю часть вращения матрицы 3x3 с помощью новой
t,b,n
векторы.Однако есть один недостаток: такая кумулятивная матрица со временем будет снижать точность (так как мы выполняем умножение на числа с плавающей запятой снова и снова без сброса), поэтому матрица со временем может деформироваться. Чтобы этого избежать, достаточно пересчитать и установить
t,b,n
часть матрицы время от времени. Я привык делать это каждые 128 вращений на 64-битнойdouble
точность переменных. Это может быть сделано также автоматически (когда у вас нет предварительной информации об осях), я делаю так:
Также использование матриц имеет разные обозначения (основной порядок строк / столбцов, порядок умножения), которые могут немного повлиять на уравнения (либо обратный порядок умножения и / или использование вместо этого обратных матриц).
Теперь, если ваш 3D-движок не поддерживает матрицы (что крайне маловероятно), вам необходимо преобразовать нашу полученную матрицу обратно в углы Эйлера. Это можно сделать с помощью гониометрии, но для этого вам нужно знать порядок углов.
В случае скольжения вам нужно идти в обратном порядке. Поэтому сначала вычислите повороты, а затем вычислите направление перемещения от сил захвата с полом и инерцией. Что немного более сложная и чистая физика...
[Edit1] простой стиль OpenGL/C++/VCL в стиле ротундус
Вот простой пример управления с использованием накопительной матрицы (без сохранения точности):
//---------------------------------------------------------------------------
#include <vcl.h> // VCL stuff (ignore)
#include <math.h> // sin,cos,M_PI
#pragma hdrstop // VCL stuff (ignore)
#include "Unit1.h" // VCL stuff (header of this window)
#include "gl_simple.h" // my GL init (source included)
//---------------------------------------------------------------------------
#pragma package(smart_init) // VCL stuff (ignore)
#pragma resource "*.dfm" // VCL stuff (ignore)
TForm1 *Form1; // VCL stuff (this window)
//---------------------------------------------------------------------------
// vector/matrix math
//---------------------------------------------------------------------------
void vector_mul(double *c,double *a,double *b) // c[3] = a[3] x b[3] (cross product)
{
double q[3];
q[0]=(a[1]*b[2])-(a[2]*b[1]);
q[1]=(a[2]*b[0])-(a[0]*b[2]);
q[2]=(a[0]*b[1])-(a[1]*b[0]);
for(int i=0;i<3;i++) c[i]=q[i];
}
//---------------------------------------------------------------------------
void matrix_mul_vector(double *c,double *a,double *b) // c[3] = a[16]*b[3] (w=1)
{
double q[3];
q[0]=(a[ 0]*b[0])+(a[ 4]*b[1])+(a[ 8]*b[2])+(a[12]);
q[1]=(a[ 1]*b[0])+(a[ 5]*b[1])+(a[ 9]*b[2])+(a[13]);
q[2]=(a[ 2]*b[0])+(a[ 6]*b[1])+(a[10]*b[2])+(a[14]);
for(int i=0;i<3;i++) c[i]=q[i];
}
//---------------------------------------------------------------------------
void matrix_inv(double *a,double *b) // a[16] = (Pseudo)Inverse(b[16])
{
double x,y,z;
// transpose of rotation matrix
a[ 0]=b[ 0];
a[ 5]=b[ 5];
a[10]=b[10];
x=b[1]; a[1]=b[4]; a[4]=x;
x=b[2]; a[2]=b[8]; a[8]=x;
x=b[6]; a[6]=b[9]; a[9]=x;
// copy projection part
a[ 3]=b[ 3];
a[ 7]=b[ 7];
a[11]=b[11];
a[15]=b[15];
// convert origin: new_pos = - new_rotation_matrix * old_pos
x=(a[ 0]*b[12])+(a[ 4]*b[13])+(a[ 8]*b[14]);
y=(a[ 1]*b[12])+(a[ 5]*b[13])+(a[ 9]*b[14]);
z=(a[ 2]*b[12])+(a[ 6]*b[13])+(a[10]*b[14]);
a[12]=-x;
a[13]=-y;
a[14]=-z;
}
//---------------------------------------------------------------------------
double* matrix_ld (double *p,double a0,double a1,double a2,double a3,double a4,double a5,double a6,double a7,double a8,double a9,double a10,double a11,double a12,double a13,double a14,double a15) { p[0]=a0; p[1]=a1; p[2]=a2; p[3]=a3; p[4]=a4; p[5]=a5; p[6]=a6; p[7]=a7; p[8]=a8; p[9]=a9; p[10]=a10; p[11]=a11; p[12]=a12; p[13]=a13; p[14]=a14; p[15]=a15; return p; }
//---------------------------------------------------------------------------
void matrix_mul (double *c,double *a,double *b) // c[16] = a[16] * b[16]
{
double q[16];
q[ 0]=(a[ 0]*b[ 0])+(a[ 1]*b[ 4])+(a[ 2]*b[ 8])+(a[ 3]*b[12]);
q[ 1]=(a[ 0]*b[ 1])+(a[ 1]*b[ 5])+(a[ 2]*b[ 9])+(a[ 3]*b[13]);
q[ 2]=(a[ 0]*b[ 2])+(a[ 1]*b[ 6])+(a[ 2]*b[10])+(a[ 3]*b[14]);
q[ 3]=(a[ 0]*b[ 3])+(a[ 1]*b[ 7])+(a[ 2]*b[11])+(a[ 3]*b[15]);
q[ 4]=(a[ 4]*b[ 0])+(a[ 5]*b[ 4])+(a[ 6]*b[ 8])+(a[ 7]*b[12]);
q[ 5]=(a[ 4]*b[ 1])+(a[ 5]*b[ 5])+(a[ 6]*b[ 9])+(a[ 7]*b[13]);
q[ 6]=(a[ 4]*b[ 2])+(a[ 5]*b[ 6])+(a[ 6]*b[10])+(a[ 7]*b[14]);
q[ 7]=(a[ 4]*b[ 3])+(a[ 5]*b[ 7])+(a[ 6]*b[11])+(a[ 7]*b[15]);
q[ 8]=(a[ 8]*b[ 0])+(a[ 9]*b[ 4])+(a[10]*b[ 8])+(a[11]*b[12]);
q[ 9]=(a[ 8]*b[ 1])+(a[ 9]*b[ 5])+(a[10]*b[ 9])+(a[11]*b[13]);
q[10]=(a[ 8]*b[ 2])+(a[ 9]*b[ 6])+(a[10]*b[10])+(a[11]*b[14]);
q[11]=(a[ 8]*b[ 3])+(a[ 9]*b[ 7])+(a[10]*b[11])+(a[11]*b[15]);
q[12]=(a[12]*b[ 0])+(a[13]*b[ 4])+(a[14]*b[ 8])+(a[15]*b[12]);
q[13]=(a[12]*b[ 1])+(a[13]*b[ 5])+(a[14]*b[ 9])+(a[15]*b[13]);
q[14]=(a[12]*b[ 2])+(a[13]*b[ 6])+(a[14]*b[10])+(a[15]*b[14]);
q[15]=(a[12]*b[ 3])+(a[13]*b[ 7])+(a[14]*b[11])+(a[15]*b[15]);
for(int i=0;i<16;i++) c[i]=q[i];
}
//---------------------------------------------------------------------------
// old style GL sphere mesh
//---------------------------------------------------------------------------
const int nb=15; // slices
const int na=nb<<1; // points per equator
class sphere
{
public:
// movement
double r; // sphere radius [units]
double m[16]; // sphere direct matrix
double vel; // actual velocity [unit/sec] in forward direction
void turn(double da) // turn left/right by angle [deg]
{
// rotate m around global Z axis
da*=M_PI/180.0; // [deg] -> [rad]
double c=cos(da),s=sin(da),xyz[16];
matrix_ld(xyz, c,-s, 0, 0, // incremental rotation around Z
s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
matrix_mul_vector(m+0,xyz,m+0); // transform all basis vectors of m from xyz [LCS] into world [GCS]
matrix_mul_vector(m+4,xyz,m+4);
matrix_mul_vector(m+8,xyz,m+8);
}
void update(double dt) // simulate dt [sec] time is elapsed
{
if (fabs(vel)<1e-6) return; // ignore stopped case
// compute unit tangent (both vectors are unit so no normalization needed)
double t[3]={ 0.0,0.0,1.0 }; // tangent is perpendiculr to global Z (turning axis)
vector_mul(t,t,m+0); // and perpendicular to local X (movement rotation axis)
// update position
for (int i=0;i<3;i++) m[12+i]+=vel*dt*t[i];
// update rotation
double da=vel*dt/r,c=cos(da),s=sin(da);
double xyz[16];
matrix_ld(xyz, 1, 0, 0, 0,
0, c,-s, 0,
0, s, c, 0,
0, 0, 0, 1);
matrix_mul(m,xyz,m);
}
// mesh and rendering
bool _init; // has been initiated ?
GLfloat pos[na][nb][3]; // vertex
GLfloat nor[na][nb][3]; // normal
GLfloat txr[na][nb][2]; // texcoord
GLuint txrid; // texture id
sphere() { _init=false; txrid=0; }
~sphere() { if (_init) glDeleteTextures(1,&txrid); }
void init(GLfloat r,AnsiString texture); // call after OpenGL is already working !!!
void draw();
};
void sphere::init(GLfloat _r,AnsiString texture)
{
GLfloat x,y,z,a,b,da,db;
GLfloat tx0,tdx,ty0,tdy;// just correction if CLAMP_TO_EDGE is not available
int ia,ib;
// varables
r=_r; vel=0.0;
for (ia=0;ia<16;ia++ ) m[ia]=0.0;
for (ia=0;ia<16;ia+=5) m[ia]=1.0;
// mesh
if (!_init) { _init=true; glGenTextures(1,&txrid); }
// a,b to texture coordinate system
tx0=0.0;
ty0=0.5;
tdx=0.5/M_PI;
tdy=1.0/M_PI;
// load texture to GPU memory
if (texture!="")
{
Byte q;
unsigned int *pp;
int xs,ys,x,y,adr,*txr;
union { unsigned int c32; Byte db[4]; } c;
Graphics::TBitmap *bmp=new Graphics::TBitmap; // new bmp
bmp->LoadFromFile(texture); // load from file
bmp->HandleType=bmDIB; // allow direct access to pixels
bmp->PixelFormat=pf32bit; // set pixel to 32bit so int is the same size as pixel
xs=bmp->Width; // resolution should be power of 2
ys=bmp->Height;
txr=new int[xs*ys];
for(adr=0,y=0;y<ys;y++)
{
pp=(unsigned int*)bmp->ScanLine[y];
for(x=0;x<xs;x++,adr++)
{
// rgb2bgr and copy bmp -> txr[]
c.c32=pp[x];
q =c.db[2];
c.db[2]=c.db[0];
c.db[0]=q;
txr[adr]=c.c32;
}
}
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D,txrid);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE,GL_MODULATE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, xs, ys, 0, GL_RGBA, GL_UNSIGNED_BYTE, txr);
glDisable(GL_TEXTURE_2D);
delete bmp;
delete[] txr;
// texture coordinates by 1 pixel from each edge (GL_CLAMP_TO_EDGE)
tx0+=1.0/GLfloat(xs);
ty0+=1.0/GLfloat(ys);
tdx*=GLfloat(xs-2)/GLfloat(xs);
tdy*=GLfloat(ys-2)/GLfloat(ys);
}
// correct texture coordinate system (invert x)
tx0=1.0-tx0; tdx=-tdx;
da=(2.0*M_PI)/GLfloat(na-1);
db= M_PI /GLfloat(nb-1);
for (ib=0,b=-0.5*M_PI;ib<nb;ib++,b+=db)
for (ia=0,a= 0.0 ;ia<na;ia++,a+=da)
{
x=cos(b)*cos(a);
y=cos(b)*sin(a);
z=sin(b);
nor[ia][ib][0]=x;
nor[ia][ib][1]=y;
nor[ia][ib][2]=z;
pos[ia][ib][0]=r*x;
pos[ia][ib][1]=r*y;
pos[ia][ib][2]=r*z;
txr[ia][ib][0]=tx0+(a*tdx);
txr[ia][ib][1]=ty0+(b*tdy);
}
}
void sphere::draw()
{
if (!_init) return;
int ia,ib0,ib1;
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glMultMatrixd(m);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D,txrid);
glEnable(GL_CULL_FACE);
glFrontFace(GL_CW);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glColor3f(1.0,1.0,1.0);
for (ib0=0,ib1=1;ib1<nb;ib0=ib1,ib1++)
{
glBegin(GL_QUAD_STRIP);
for (ia=0;ia<na;ia++)
{
glNormal3fv (nor[ia][ib0]);
glTexCoord2fv(txr[ia][ib0]);
glVertex3fv (pos[ia][ib0]);
glNormal3fv (nor[ia][ib1]);
glTexCoord2fv(txr[ia][ib1]);
glVertex3fv (pos[ia][ib1]);
}
glEnd();
}
glDisable(GL_TEXTURE_2D);
glDisable(GL_CULL_FACE);
glDisable(GL_LIGHTING);
glDisable(GL_LIGHT0);
/*
// local axises
double q=1.5*r;
glBegin(GL_LINES);
glColor3f(1.0,0.0,0.0); glVertex3d(0.0,0.0,0.0); glVertex3d(q,0.0,0.0);
glColor3f(0.0,1.0,0.0); glVertex3d(0.0,0.0,0.0); glVertex3d(0.0,q,0.0);
glColor3f(0.0,0.0,1.0); glVertex3d(0.0,0.0,0.0); glVertex3d(0.0,0.0,q);
glEnd();
*/
glMatrixMode(GL_MODELVIEW);
glPopMatrix();
}
//---------------------------------------------------------------------------
// rendring
bool _redraw=false;
double ieye[16]; // camera inverse matrix
sphere obj;
// key codes for controling (Arrows + Space)
WORD key_left =37;
WORD key_right=39;
WORD key_up =38;
WORD key_down =40;
// key pressed state
bool _left =false;
bool _right=false;
bool _up =false;
bool _down =false;
//---------------------------------------------------------------------------
void draw_map()
{
int i,j;
double u,v,p[3],dp[3];
// here 3D view must be already set (modelview,projection)
glDisable(GL_CULL_FACE);
// [draw 3D map]
const int n=30; // map size
double p0[3]={0.0,0.0,0.0}; // map start point
double du[3]={1.0,0.0,0.0}; // map u step (size of grid = 1.0 )
double dv[3]={0.0,1.0,0.0}; // map v step (size of grid = 1.0 )
glColor3f(0.5,0.7,1.0);
glBegin(GL_LINES);
for (j=0;j<=n;j++)
{
for (i=0;i<3;i++) p[i]=p0[i]+(double(j)*du[i])+(double(0)*dv[i]); glVertex3dv(p);
for (i=0;i<3;i++) p[i]=p0[i]+(double(j)*du[i])+(double(n)*dv[i]); glVertex3dv(p);
for (i=0;i<3;i++) p[i]=p0[i]+(double(0)*du[i])+(double(j)*dv[i]); glVertex3dv(p);
for (i=0;i<3;i++) p[i]=p0[i]+(double(n)*du[i])+(double(j)*dv[i]); glVertex3dv(p);
}
glEnd();
}
//---------------------------------------------------------------------------
void gl_draw()
{
_redraw=false;
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glMatrixMode(GL_MODELVIEW);
glLoadMatrixd(ieye); // inverse camera matrix
obj.draw();
draw_map();
glFlush();
SwapBuffers(hdc);
}
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner):TForm(Owner)
{
// this is called on window startup
gl_init(Handle); // init OpenGL 1.0
glMatrixMode(GL_MODELVIEW); // set camera to vew our map
glLoadIdentity;
glTranslatef(-15.0,-5.0,-10.5); // "centered" position above the map
glRotatef(-60.0,1.0,0.0,0.0); // rotate view to be more parallel to plane
glGetDoublev(GL_MODELVIEW_MATRIX,ieye); // store result
// ini obj
obj.init(1.0,"ball.bmp"); // radius texture and mesh
obj.m[12]=10.0; // position (x,y,z)
obj.m[13]=10.0;
obj.m[14]=+obj.r;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
// this is called before window exits
gl_exit(); // exit OpenGL
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormResize(TObject *Sender)
{
// this is called on each window resize (and also after startup)
gl_resize(ClientWidth,ClientHeight);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormPaint(TObject *Sender)
{
// this is called whnewer app needs repaint
gl_draw();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift)
{
// on key down event
if (Key==key_left ) _left =true;
if (Key==key_right) _right=true;
if (Key==key_up ) _up =true;
if (Key==key_down ) _down =true;
Key=0; // key is handled
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormKeyUp(TObject *Sender, WORD &Key, TShiftState Shift)
{
// on key release event
if (Key==key_left ) _left =false;
if (Key==key_right) _right=false;
if (Key==key_up ) _up =false;
if (Key==key_down ) _down =false;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormMouseActivate(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y, int HitTest, TMouseActivate &MouseActivate)
{
_left =false; // clear key flags after focus change
_right=false; // just to avoid constantly "pressed" keys
_up =false; // after window focus swaping during key press
_down =false; // many games are ignoring this and you need to
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
// here movement and repaint timer handler (I have 20ms interval)
double dt=0.001*double(Timer1->Interval); // timer period [sec]
double da=90.0*dt; // angular turn speed in [deg/sec]
double dp=10.0*dt; // camera movement speed in [units/sec]
double dv=10.0*dt; // sphere acceleration [units/sec^2]
// control object
if (_left ) { _redraw=true; obj.turn(-da); }
if (_right) { _redraw=true; obj.turn(+da); }
if (_up ) { _redraw=true; obj.vel+=dv; }
if (_down ) { _redraw=true; obj.vel-=dv; }
// simulate the ball movement
obj.update(dt); _redraw=true;
// render if needed
if (_redraw) gl_draw();
}
//---------------------------------------------------------------------------
Это пустое приложение VCL для одной формы с одним таймером 20 мс. Чтобы портировать в вашу среду, просто игнорируйте материал VCL, имитируйте соответствующие события приложения и рендеринга порта для ваших компонентов /style/api. Единственная важная вещь, это просто sphere
класс отмечен как // movement
и таймер события Timer1Timer(TObject *Sender)
, Все остальное - просто рендеринг и обработка клавиатуры... Я подозреваю, что вы уже справились самостоятельно...
Предварительный просмотр показывает движение, пока я управляю мячом со стрелками:
up/down - accelerate/decelerate
left/right - turn left/right in respect to forward direction around normal to surface
Вот текстура, которую я использовал (нарисованная вручную в mspaint, поэтому она не может быть идеально симметричной по пикселям...)
gl_simple.h
мой можно найти здесь: