Если скольжения нет, то:
ось вращения
будет параллельна вашему полу и перпендикулярна вашему движению.Таким образом, вы можете использовать кросс-продукт, чтобы получить его.Пусть:
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 способа, которыми я знаю, как добиться вашего вращения.Либо используйте Rodrigues_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
изМой может быть найден здесь: