Ответ 1
Таким образом, вопрос на самом деле заключается в понимании однородных матриц преобразования 4x4
хорошо без математики позади единственное, что осталось, это геометрическое представление/значение, которое намного лучше для человеческой абстракции/понимания.
1. Так что же такое матрица 4х4?
Это представление некоторой декартовой системы координат, и оно состоит из:
-
3
базисных вектора (по одному на каждую ось) красный, зеленый, синийТаким образом, если красный, зеленый и синий векторы перпендикулярны друг другу, то система координат ортогональна. Если они также являются единичными векторами, то они ортонормированы (как, например, единичная матрица).
-
точка начала серая
-
проекция и однородная сторона (немаркированный нижний остаток матрицы)
Эта часть предназначена только для одновременного вращения и перемещения, поэтому используемая точка должна быть однородной, что означает форму
(x,y,z,w=1)
. Если бы это было просто(x,y,z)
то матрица была бы3x3
и этого недостаточно для перевода. Я не буду использовать какие-либо прогнозы, которые их нелегко объяснить геометрически.
Этот макет из нотации OpenGL, там также есть транспонированное представление (векторы - это строки, а не столбцы)
Теперь, как преобразовать любую точку в/из этой системы координат:
g=M*l;
l=Inverse(M)*g;
где:
-
M
- матрица преобразования -
l
- точкаM
локальной системы координат (LCS) -
g
- точка глобальной системы координат (GCS)
для транспонированной версии (DirectX) это:
l=M*g;
g=Inverse(M)*l;
Это связано с тем, что транспонированная матрица ортогонального вращения также является обратной
- для получения дополнительной информации см анатомии матрицы преобразования и 3D графический конвейер
2. как это визуализировать
Да, вы можете нарисовать матричные числа, но они не имеют смысла на первый взгляд, особенно если числа меняются, поэтому нарисуйте векторы осей, как на изображении выше. Где каждая ось является линией от origin
до origin + line_size*axis_vector
3. как его построить
Просто рассчитайте векторы оси и начало координат и поместите их в матрицу. Для обеспечения ортогональности использовать перекрестное произведение (но будьте осторожны с порядком мультипликаторов, чтобы использовать правильное направление)
4. эффекты
- вращение осуществляется путем вращения осей, чтобы вы могли вычислить каждую ось с помощью параметрического уравнения окружности...
- масштабирование выполняется путем умножения осей на коэффициент масштабирования
- перекос просто с использованием неперпендикулярных осей
5. вращение
В большинстве случаев используется пошаговое вращение. Есть два типа
-
локальное вращение
M'=M*rotation_matrix
вращается вокруг локальных координатных осей, как если бы вы управляли самолетом, автомобилем или игроком... Большинство движков/игр не используют их и вместо этого имитируют углы Эйлера, что является дешевым решением (имеет много причуд и проблемы), потому что большинство людей, которые используют OpenGL, даже не знают, что это возможно, и, скорее, составляют списокglRotate/glTranslate
... -
глобальное вращение
M'=Inverse(Inverse(M)*rotation_matrix)
вращается вокруг осей глобальной системы координат.
где rotation_matrix
- любая стандартная матрица преобразования вращения.
Если у вас разная матрица (транспонированная), то локальные и глобальные повороты вычисляются наоборот...
Вы также можете вычислить ваш rotation_matrix
из 3
углов, например:
rotation_matrix=rotation_around_x(ax)*rotation_around_y(ay)*rotation_around_z(az);
см. вики-матрицы поворотов. 3D Rx,Ry,Rz
из Basic rotations
- это то, что вам нужно. Как вы можете видеть, они на самом деле являются параметрическим уравнением единичного круга. Порядок умножения меняет то, как углы сходятся к заданному положению. Это называется углами Эйлера, и я не использую его (вместо этого я интегрирую пошаговые изменения, которые не имеют ограничений, если все сделано правильно, не говоря уже о том, что это проще).
В любом случае, если вам нужно, вы можете относительно легко преобразовать матрицу преобразования в углы Эйлера:
6. glRotate
Если вы хотите glRotate
вам следует использовать кватернионы, потому что это вращение вокруг оси, а не на 3 угла! Есть обходной путь:
- создать матрицу преобразования
N
для этой оси - затем преобразуйте свою матрицу
M
в нее - повернуть
N
на угол - затем преобразовать
M
обратно изN
в глобальные координаты
Или вместо этого вы можете использовать Rodrigues_rotation_formula
Чтобы преобразовать Матрицу в/из Матрицы в этом случае, просто преобразуйте оси как точки и оставьте начало координат как есть, но начало N
должно быть (0,0,0) !!!
7. использование
Преобразования являются кумулятивными, что означает:
-
p'=M1*M2*M3*M4*p;
такой же, какM=M1*M2*M3*M4; p'=M*p
M=M1*M2*M3*M4; p'=M*p
Так что если у вас есть много точек для преобразования, то вы предварительно вычисляете все преобразования в одну матрицу и используете только ее. Не нужно умножать точки на все последующие матрицы. Хорошо, теперь концепция:
у вас должно быть 3
системы координат:
- камера
C
- мир (обычно единичная матрица)
- объект
O
(каждый объект имеет свою матрицу)
так что если у вас есть куб с 8
вершинами p0,...,p7
то вы должны выполнить преобразование в каждой точке от локальных координат объекта до локальных координат камеры. Некоторые gfx api делают некоторые из них, поэтому вы применяете только то, что вам нужно, так что вам действительно нужно:
-
p(i)'=inverse(C)*unit*M*p(i);
преобразования являются кумулятивными, и единичная матрица ничего не меняет, поэтому:
-
Q=inverse(C)*M; p(i)'=Q*p(i);
поэтому перед рисованием вычислите Q
для нарисованного объекта, затем возьмите каждую точку p(i)
объекта и вычислите преобразованную p(i)'
и нарисуйте/используйте преобразованную точку... p(i)'
находится в локальной системе координат камеры (x, y экрана), но там нет перспективы, поэтому перед рисованием вы также можете добавить любую из матриц проекции и разделить на z
оси в конце... Проекция также является кумулятивной, поэтому она также может быть внутри Q
[edit1] C++ пример
//$$---- Form CPP ----
//---------------------------------------------------------------------------
// apart from math.h include you can ignore this machine generated VCL related code
#include <vcl.h>
#pragma hdrstop
#include "win_main.h"
#include <math.h>
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TMain *Main; // pointer to main window ...
//---------------------------------------------------------------------------
// Here is the important stuff some math first
//---------------------------------------------------------------------------
const double deg=M_PI/180.0;
double divide(double x,double y);
void matrix_mul (double *c,double *a,double *b); // c[16] = a[16] * b[16]
void matrix_mul_vector(double *c,double *a,double *b); // c[ 4] = a[16] * b[ 4]
void matrix_subdet (double *c,double *a); // c[16] = all subdets of a[16]
double matrix_subdet ( double *a,int r,int s);// = subdet(r,s) of a[16]
double matrix_det ( double *a); // = det of a[16]
double matrix_det ( double *a,double *b); // = det of a[16] and subdets b[16]
void matrix_inv (double *c,double *a); // c[16] = a[16] ^ -1
//---------------------------------------------------------------------------
double divide(double x,double y)
{
if (!y) return 0.0;
return x/y;
}
void matrix_mul (double *c,double *a,double *b)
{
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];
}
void matrix_mul_vector(double *c,double *a,double *b)
{
double q[3];
q[0]=(a[ 0]*b[0])+(a[ 1]*b[1])+(a[ 2]*b[2])+(a[ 3]);
q[1]=(a[ 4]*b[0])+(a[ 5]*b[1])+(a[ 6]*b[2])+(a[ 7]);
q[2]=(a[ 8]*b[0])+(a[ 9]*b[1])+(a[10]*b[2])+(a[11]);
for(int i=0;i<3;i++) c[i]=q[i];
}
void matrix_subdet (double *c,double *a)
{
double q[16];
int i,j;
for (i=0;i<4;i++)
for (j=0;j<4;j++)
q[j+(i<<2)]=matrix_subdet(a,i,j);
for (i=0;i<16;i++) c[i]=q[i];
}
double matrix_subdet ( double *a,int r,int s)
{
double c,q[9];
int i,j,k;
k=0; // q = sub matrix
for (j=0;j<4;j++)
if (j!=s)
for (i=0;i<4;i++)
if (i!=r)
{
q[k]=a[i+(j<<2)];
k++;
}
c=0;
c+=q[0]*q[4]*q[8];
c+=q[1]*q[5]*q[6];
c+=q[2]*q[3]*q[7];
c-=q[0]*q[5]*q[7];
c-=q[1]*q[3]*q[8];
c-=q[2]*q[4]*q[6];
if (int((r+s)&1)) c=-c; // add signum
return c;
}
double matrix_det ( double *a)
{
double c=0;
c+=a[ 0]*matrix_subdet(a,0,0);
c+=a[ 4]*matrix_subdet(a,0,1);
c+=a[ 8]*matrix_subdet(a,0,2);
c+=a[12]*matrix_subdet(a,0,3);
return c;
}
double matrix_det ( double *a,double *b)
{
double c=0;
c+=a[ 0]*b[ 0];
c+=a[ 4]*b[ 1];
c+=a[ 8]*b[ 2];
c+=a[12]*b[ 3];
return c;
}
void matrix_inv (double *c,double *a)
{
double d[16],D;
matrix_subdet(d,a);
D=matrix_det(a,d);
if (D) D=1.0/D;
for (int i=0;i<16;i++) c[i]=d[i]*D;
}
//---------------------------------------------------------------------------
// now the object representation
//---------------------------------------------------------------------------
const int pnts=8;
double pnt[pnts*3]= // Vertexes for 100x100x100 cube centered at (0,0,0)
{
-100.0,-100.0,-100.0,
-100.0,+100.0,-100.0,
+100.0,+100.0,-100.0,
+100.0,-100.0,-100.0,
-100.0,-100.0,+100.0,
-100.0,+100.0,+100.0,
+100.0,+100.0,+100.0,
+100.0,-100.0,+100.0,
};
const int facs=6;
int fac[facs*4]= // faces (index of point used) no winding rule
{
0,1,2,3,
4,5,6,7,
0,1,5,4,
1,2,6,5,
2,3,7,6,
3,0,4,7,
};
double rep[16]= // 4x4 transform matrix of object (unit from start) at (0,0,+100)
{
1.0,0.0,0.0, 0.0,
0.0,1.0,0.0, 0.0,
0.0,0.0,1.0,100.0,
0.0,0.0,0.0,1.0,
};
double eye[16]= // 4x4 transform matrix of camera at (0,0,-150)
{
1.0,0.0,0.0, 0.0,
0.0,1.0,0.0, 0.0,
0.0,0.0,1.0,-150.0,
0.0,0.0,0.0,1.0,
};
//---------------------------------------------------------------------------
// this is how to draw it
//---------------------------------------------------------------------------
void obj(double *pnt,int pnts,int *fac,int facs,double *rep,double *ieye)
{
// variables for drawing
int i;
double p0[3],p1[3],p2[3],p3[3],m[16],d;
// gfx api variables (change to your stuff) Main is the main form of this application
TCanvas *scr=Main->bmp->Canvas;
double xs2=Main->ClientWidth/2,ys2=Main->ClientHeight/2;
double v=xs2*tan(30.0*deg); // 60 degree viewing angle perspective projection
matrix_mul(m,ieye,rep); // cumulate all needed transforms
for (i=0;i<facs*4;) // go through all faces
{
// convert all points of face
matrix_mul_vector(p0,m,&pnt[fac[i]*3]); i++;
matrix_mul_vector(p1,m,&pnt[fac[i]*3]); i++;
matrix_mul_vector(p2,m,&pnt[fac[i]*3]); i++;
matrix_mul_vector(p3,m,&pnt[fac[i]*3]); i++;
// here goes perspective divide by z coordinate if needed
d=divide(v,p0[2]); p0[0]*=d; p0[1]*=d;
d=divide(v,p1[2]); p1[0]*=d; p1[1]*=d;
d=divide(v,p2[2]); p2[0]*=d; p2[1]*=d;
d=divide(v,p3[2]); p3[0]*=d; p3[1]*=d;
// here is viewport transform (just translate (0,0) to middle of screen in this case
p0[0]+=xs2; p0[1]+=ys2;
p1[0]+=xs2; p1[1]+=ys2;
p2[0]+=xs2; p2[1]+=ys2;
p3[0]+=xs2; p3[1]+=ys2;
// draw quad
// I use VCL GDI TCanvas you use what you have ...
// and wireframe only to keep this simple (no Z buffer,winding culling,...)
scr->Pen->Color=clAqua; // perimeter wireframe
scr->MoveTo(p0[0],p0[1]);
scr->LineTo(p1[0],p1[1]);
scr->LineTo(p2[0],p2[1]);
scr->LineTo(p3[0],p3[1]);
scr->LineTo(p0[0],p0[1]);
// scr->Pen->Color=clBlue; // face cross to visualy check if I correctly generate the fac[]
// scr->MoveTo(p0[0],p0[1]);
// scr->LineTo(p2[0],p2[1]);
// scr->MoveTo(p1[0],p1[1]);
// scr->LineTo(p3[0],p3[1]);
}
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
void TMain::draw()
{
if (!_redraw) return;
bmp->Canvas->Brush->Color=clBlack;
bmp->Canvas->FillRect(TRect(0,0,xs,ys));
// compute inverse of camera need to compute just once for all objects
double ieye[16];
matrix_inv(ieye,eye);
// draw all objects
obj(pnt,pnts,fac,facs,rep,ieye);
Main->Canvas->Draw(0,0,bmp);
_redraw=false;
}
//---------------------------------------------------------------------------
__fastcall TMain::TMain(TComponent* Owner) : TForm(Owner)
{
// window constructor you can ignore this ... (just create a backbuffer bitmap here)
bmp=new Graphics::TBitmap;
bmp->HandleType=bmDIB;
bmp->PixelFormat=pf32bit;
pyx=NULL;
}
//---------------------------------------------------------------------------
void __fastcall TMain::FormDestroy(TObject *Sender)
{
// window destructor release memory ... also ignoe this
if (pyx) delete pyx;
delete bmp;
}
//---------------------------------------------------------------------------
void __fastcall TMain::FormResize(TObject *Sender)
{
// on resize event ... just resize/redraw backbuffer also can ignore this
xs=ClientWidth; xs2=xs>>1;
ys=ClientHeight; ys2=ys>>1;
bmp->Width=xs;
bmp->Height=ys;
if (pyx) delete pyx;
pyx=new int*[ys];
for (int y=0;y<ys;y++) pyx[y]=(int*) bmp->ScanLine[y];
_redraw=true;
}
//---------------------------------------------------------------------------
void __fastcall TMain::FormPaint(TObject *Sender)
{
// repaint event can ignore
_redraw=true;
}
//---------------------------------------------------------------------------
void __fastcall TMain::tim_redrawTimer(TObject *Sender)
{
// timer event to animate the cube ...
_redraw=true;
// rotate the object to see it in motion
double ang,c,s;
ang=5.0*deg; c=cos(ang); s=sin(ang); // rotate baround z by 5 degrees per timer step
double rz[16]= { c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1 };
ang=1.0*deg; c=cos(ang); s=sin(ang); // rotate baround x by 1 degrees per timer step
double rx[16]= { 1, 0, 0, 0,
0, c, s, 0,
0,-s, c, 0,
0, 0, 0, 1 };
matrix_mul(rep,rep,rz);
matrix_mul(rep,rep,rx);
draw();
}
//---------------------------------------------------------------------------
вот как это выглядит:
И GIF анимация с отбором задних лиц:
[заметки]
Если у вас есть вопросы, прокомментируйте меня...
[Edit2] часто нужны базовые трехмерные векторные операции
Если вы не знаете, как вычислять векторные операции, такие как скрещенные/точечные произведения или абсолютное значение, смотрите:
// cross product: W = U x V
W.x=(U.y*V.z)-(U.z*V.y)
W.y=(U.z*V.x)-(U.x*V.z)
W.z=(U.x*V.y)-(U.y*V.x)
// dot product: a = (U.V)
a=U.x*V.x+U.y*V.y+U.z*V.z
// abs of vector a = |U|
a=sqrt((U.x*U.x)+(U.y*U.y)+(U.z*U.z))
вот моя C++ векторная математика:
static double vector_tmp[3];
double divide(double x,double y) { if ((y>=-1e-30)&&(y<=+1e-30)) return 0.0; return x/y; }
double* vector_ld(double x,double y,double z) { double *p=vector_tmp; p[0]=x; p[1]=y; p[2]=z; return p;}
double* vector_ld(double *p,double x,double y,double z) { p[0]=x; p[1]=y; p[2]=z; return p;}
void vector_copy(double *c,double *a) { for(int i=0;i<3;i++) c[i]=a[i]; }
void vector_abs(double *c,double *a) { for(int i=0;i<3;i++) c[i]=fabs(a[i]); }
void vector_one(double *c,double *a)
{
double l=divide(1.0,sqrt((a[0]*a[0])+(a[1]*a[1])+(a[2]*a[2])));
c[0]=a[0]*l;
c[1]=a[1]*l;
c[2]=a[2]*l;
}
void vector_len(double *c,double *a,double l)
{
l=divide(l,sqrt((a[0]*a[0])+(a[1]*a[1])+(a[2]*a[2])));
c[0]=a[0]*l;
c[1]=a[1]*l;
c[2]=a[2]*l;
}
void vector_neg(double *c,double *a) { for(int i=0;i<3;i++) c[i]=-a[i]; }
void vector_add(double *c,double *a,double *b) { for(int i=0;i<3;i++) c[i]=a[i]+b[i]; }
void vector_sub(double *c,double *a,double *b) { for(int i=0;i<3;i++) c[i]=a[i]-b[i]; }
void vector_mul(double *c,double *a,double *b) // cross
{
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 vector_mul(double *c,double *a,double b) { for(int i=0;i<3;i++) c[i]=a[i]*b; }
void vector_mul(double *c,double a,double *b) { for(int i=0;i<3;i++) c[i]=a*b[i]; }
double vector_mul( double *a,double *b) { double c=0; for(int i=0;i<3;i++) c+=a[i]*b[i]; return c; } // dot
double vector_len(double *a) { return sqrt((a[0]*a[0])+(a[1]*a[1])+(a[2]*a[2])); }
double vector_len2(double *a) { return (a[0]*a[0])+(a[1]*a[1])+(a[2]*a[2]); }
[Edit3] локальные повороты для управления камерой и объектом с клавиатуры
Так как в последнее время это часто спрашивают, вот несколько примеров моих ответов с демонстрациями: