I started with recalling Isometric Projection in Wikipedia. It provides rather the whole cooking guide (in the section Mathematics).
So, I'm a little bit puzzled what still could be missing. Then, I remembered that I learnt the "matrix stuff" actually not in the math course at university (where I should have) but much later from a patient colleage (with Math diploma).
Thus, I will try to provide a most minimal demonstration in C++:
First, some includes:
#include <iostream>
#include <iomanip>
#include <cmath>
using namespace std;
... a convenience function:
inline float degToRad(float angle) { return 3.141593f * angle / 180.f; }
... a 2d vector "class":
struct Vec2 {
float x, y;
Vec2(float x, float y): x(x), y(y) { }
};
ostream& operator<<(ostream &out, const Vec2 &v)
{
return out << "( " << v.x << ", " << v.y << " )";
}
... a 3d vector "class":
struct Vec3 {
float x, y, z;
Vec3(float x, float y, float z): x(x), y(y), z(z) { }
Vec3(const Vec2 &xy, float z): x(xy.x), y(xy.y), z(z) { }
};
ostream& operator<<(ostream &out, const Vec3 &v)
{
return out << "( " << v.x << ", " << v.y << ", " << v.z << " )";
}
... a 3×3 matrix class:
enum ArgInitRotX { InitRotX };
enum ArgInitRotY { InitRotY };
struct Mat3x3 {
float _00, _01, _02, _10, _11, _12, _20, _21, _22;
// constructor to build a matrix by elements
Mat3x3(
float _00, float _01, float _02,
float _10, float _11, float _12,
float _20, float _21, float _22)
{
this->_00 = _00; this->_01 = _01; this->_02 = _02;
this->_10 = _10; this->_11 = _11; this->_12 = _12;
this->_20 = _20; this->_21 = _21; this->_22 = _22;
}
// constructor to build a matrix for rotation about x axis
Mat3x3(ArgInitRotX, float angle)
{
this->_00 = 1.0f; this->_01 = 0.0f; this->_02 = 0.0f;
this->_10 = 0.0f; this->_11 = cos(angle); this->_12 = sin(angle);
this->_20 = 0.0f; this->_21 = -sin(angle); this->_22 = cos(angle);
}
// constructor to build a matrix for rotation about y axis
Mat3x3(ArgInitRotY, float angle)
{
this->_00 = cos(angle); this->_01 = 0.0f; this->_02 = -sin(angle);
this->_10 = 0.0f; this->_11 = 1.0f; this->_12 = 0.0f;
this->_20 = sin(angle); this->_21 = 0.0f; this->_22 = cos(angle);
}
// multiply matrix with matrix -> matrix
Mat3x3 operator * (const Mat3x3 &mat) const
{
return Mat3x3(
_00 * mat._00 + _01 * mat._10 + _02 * mat._20,
_00 * mat._01 + _01 * mat._11 + _02 * mat._21,
_00 * mat._02 + _01 * mat._12 + _02 * mat._22,
_10 * mat._00 + _11 * mat._10 + _12 * mat._20,
_10 * mat._01 + _11 * mat._11 + _12 * mat._21,
_10 * mat._02 + _11 * mat._12 + _12 * mat._22,
_20 * mat._00 + _21 * mat._10 + _22 * mat._20,
_20 * mat._01 + _21 * mat._11 + _22 * mat._21,
_20 * mat._02 + _21 * mat._12 + _22 * mat._22);
}
// multiply matrix with vector -> vector
Vec3 operator * (const Vec3 &vec) const
{
return Vec3(
_00 * vec.x + _01 * vec.y + _02 * vec.z,
_10 * vec.x + _11 * vec.y + _12 * vec.z,
_20 * vec.x + _21 * vec.y + _22 * vec.z);
}
};
ostream& operator<<(ostream &out, const Mat3x3 &mat)
{
return out
<< mat._20 << ", " << mat._21 << ", " << mat._22 << endl
<< mat._10 << ", " << mat._11 << ", " << mat._12 << endl
<< mat._20 << ", " << mat._21 << ", " << mat._22;
}
... and the main()
function to put all together to the actual demonstration:
int main()
{
// some 2D vector samples (for a quad)
Vec2 quad[] = {
{ 0.0f, 0.0f }, { 0.0f, 1.0f }, { 1.0f, 1.0f }, { 1.0f, 0.0f }
};
/* Something like this:
* ^ y
* |
* v[3] ---- v[2]
* | |
* | |
* | |
* v[0] ---- v[1] --> x
*/
// the rotation matrix for isometric view build by multiplying the rotations
Mat3x3 matIso = Mat3x3(InitRotX, degToRad(30.0)) * Mat3x3(InitRotY, degToRad(45.0));
// prepare output formatting
cout << fixed << setprecision(5);
// the rotation matrix for isometric view:
cout << "The matrix for isometric projection:" << endl
<< matIso << endl;
// prepare output formatting
cout << fixed << setprecision(3);
// do it for all sample 2D vectors:
cout << "Isometric projection of the 2d quad:" << endl;
for (const Vec2 &v : quad) {
// 2D vector -> 3D vector
Vec3 v_(v, 0.0f);
// project v_ to iso view
v_ = matIso * v_;
// print the result:
cout << v << " -> " << v_ << endl;
}
// doing it again with a 3d cube (centered)
Vec3 cube[] = {
{ -0.5f, -0.5f, -0.5f }, { +0.5f, -0.5f, -0.5f }, { +0.5f, +0.5f, -0.5f }, { -0.5f, +0.5f, -0.5f },
{ -0.5f, -0.5f, +0.5f }, { +0.5f, -0.5f, +0.5f }, { +0.5f, +0.5f, +0.5f }, { -0.5f, +0.5f, +0.5f }
};
cout << "Isometric projection of the centered 3d cube:" << endl;
for (const Vec3 &v : cube) {
// project v to iso view
Vec3 v_ = matIso * v;
// print the result:
cout << v << " -> " << v_ << endl;
}
// done
return 0;
}
This is what I got in my test:
The matrix for isometric projection:
0.61237, -0.50000, 0.61237
0.35355, 0.86603, 0.35355
0.61237, -0.50000, 0.61237
Isometric projection of the 2d quad:
( 0.000, 0.000 ) -> ( 0.000, 0.000, 0.000 )
( 0.000, 1.000 ) -> ( 0.000, 0.866, -0.500 )
( 1.000, 1.000 ) -> ( 0.707, 1.220, 0.112 )
( 1.000, 0.000 ) -> ( 0.707, 0.354, 0.612 )
Isometric projection of the centered 3d cube:
( -0.500, -0.500, -0.500 ) -> ( -0.707, -0.787, -0.362 )
( 0.500, -0.500, -0.500 ) -> ( 0.000, -0.433, 0.250 )
( 0.500, 0.500, -0.500 ) -> ( 0.000, 0.433, -0.250 )
( -0.500, 0.500, -0.500 ) -> ( -0.707, 0.079, -0.862 )
( -0.500, -0.500, 0.500 ) -> ( -0.000, -0.433, 0.250 )
( 0.500, -0.500, 0.500 ) -> ( 0.707, -0.079, 0.862 )
( 0.500, 0.500, 0.500 ) -> ( 0.707, 0.787, 0.362 )
( -0.500, 0.500, 0.500 ) -> ( -0.000, 0.433, -0.250 )
I uploaded the whole sample on ideone.
The Mathematics section of the above Wikipedia link mentions also a projection to xy plane. IMHO, it is even simpler to just ignore the z coordinates of the result vectors. However, as OpenGL was mentioned in the question, it can be worth to keep the z-coordinates (e.g. for depth buffering).
In OpenGL, 4×4 matrices are used. These, are introduced for the support of Homogeneous Coordinates. Simplified: Homogeneous coordinates are used to "force points and directions into the same space" or to involve 3d translation into 3d transformations (which is not possible with 3×3 matrices). Homogeneous coordinates are a little bit more complicated (and hence worth another question).
For my luck, the isometric projection is built from rotations only (and, may be, the projection to xy plane which I left out to keep the depth buffer values). Thus, 3×3 matrices are sufficient.
However, I want to mention at least how the matrix would look in OpenGL (as 4×4 matrix):
float matIso[] = {
0.61237f, -0.50000f, 0.61237f, 0.0f,
0.35355f, 0.86603f, 0.35355f, 0.0f,
0.61237f, -0.50000f, 0.61237f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
};
The last column denotes the translation which is (0, 0, 0) in this case.
There are some open source libraries available to do the math stuff on CPU side. Among others I want to mention:
On GPU side (I know, at least, for GLSL), it's already built in.
Update
After having some conversation, I revived my plan for a graphical visualization. My intention was to keep it short and simple without hiding the mathematical details in an API like OpenGL. Thus, I decided to do it as Qt only sample. This is, how it looks:

The source code for the Qt application test-QIsoView.cc
:
#include <QtWidgets>
#include "linmath.h"
typedef unsigned int uint; // for the convenience
struct Wireframe {
Vec3f *points; // coordinates
uint nPoints; // number of points (i.e. values in indices)
uint *indices;
Vec3f color;
};
class WireframeView: public QWidget {
public:
const size_t nWireframes;
const Wireframe *wireframes;
const Mat4x4f matProj;
private:
Mat4x4f _matView;
public:
WireframeView(
size_t nWireframes = 0, const Wireframe *wireframes = nullptr,
const Mat4x4f &matProj = Mat4x4f(InitIdent),
QWidget *pQParent = nullptr):
QWidget(pQParent),
nWireframes(nWireframes), wireframes(wireframes),
matProj(matProj), _matView(InitIdent)
{ }
protected:
virtual void resizeEvent(QResizeEvent *pQEvent) override;
virtual void paintEvent(QPaintEvent *pQEvent) override;
};
void WireframeView::resizeEvent(QResizeEvent*)
{
float w_2 = 0.5f * width(), h_2 = 0.5f * height();
float s = w_2 < h_2 ? w_2 : h_2;
_matView
= Mat4x4f(InitTrans, Vec3f(w_2, h_2, 0.0f))
* Mat4x4f(InitScale, s, -s, 1.0f);
}
void WireframeView::paintEvent(QPaintEvent *pQEvent)
{
const int w = width(), w_2 = w / 2, h = height(), h_2 = h / 2;
int m = w_2 < h_2 ? w_2 : h_2;
QPainter qPainter(this);
// clear background
QPalette::ColorGroup colGrp = isEnabled()
? QPalette::Active : QPalette::Disabled;
qPainter.setBrush(QApplication::palette().brush(colGrp, QPalette::Base));
qPainter.drawRect(0, 0, width(), height());
// draw grid
const QBrush &mid = QApplication::palette().brush(colGrp, QPalette::Mid);
qPainter.setPen(QPen(mid.color(), 1));
qPainter.drawRect(w_2 - m, h_2 - m, 2 * m, 2 * m);
qPainter.drawLine(0, h_2, w, h_2);
qPainter.drawLine(w_2, 0, w_2, h);
// draw wireframes
Mat4x4f matView = _matView * matProj;
for (size_t i = 0; i < nWireframes; ++i) {
const Wireframe &wireframe = wireframes[i];
QColor qColor(
wireframe.color.x * 255, wireframe.color.y * 255,
wireframe.color.z * 255);
qPainter.setPen(QPen(qColor, 2));
for (uint i = 1; i < wireframe.nPoints; i += 2) {
Vec4f p0(wireframe.points[wireframe.indices[i - 1]], 1.0f);
Vec4f p1(wireframe.points[wireframe.indices[i]], 1.0f);
Vec2f p0V = Vec2f(matView * p0);
Vec2f p1V = Vec2f(matView * p1);
qPainter.drawLine((int)p0V.x, (int)p0V.y, (int)p1V.x, (int)p1V.y);
}
}
}
int main(int argc, char **argv)
{
QApplication app(argc, argv);
// build models
Vec3f pointsPyramid[] = {
Vec3f(0.0f, 0.0f, 0.0f),
Vec3f(1.0f, 0.0f, 0.0f),
Vec3f(0.0f, 1.0f, 0.0f),
Vec3f(0.0f, 0.0f, 1.0f)
};
uint indicesPyramid[] = {
0, 1, 0, 2, 0, 3, 1, 2, 2, 3, 3, 1
};
enum {
nPointsPyramid = sizeof indicesPyramid / sizeof *indicesPyramid
};
Vec3f pointsCube[] = {
Vec3f(-0.5f, -0.5f, -0.5f), Vec3f(+0.5f, -0.5f, -0.5f),
Vec3f(-0.5f, +0.5f, -0.5f), Vec3f(+0.5f, +0.5f, -0.5f),
Vec3f(-0.5f, -0.5f, +0.5f), Vec3f(+0.5f, -0.5f, +0.5f),
Vec3f(-0.5f, +0.5f, +0.5f), Vec3f(+0.5f, +0.5f, +0.5f)
};
uint indicesCube[] = {
0, 1, 1, 3, 3, 2, 2, 0, // front
4, 5, 5, 7, 7, 6, 6, 4, // back
0, 4, 1, 5, 3, 7, 2, 6 // sides
};
enum {
nPointsCube = sizeof indicesCube / sizeof *indicesCube
};
Wireframe wireframes[] = {
{ pointsPyramid, nPointsPyramid, indicesPyramid,
Vec3f(0.8f, 0.0f, 0.0f)
},
{ pointsCube, nPointsCube, indicesCube,
Vec3f(0.0f, 0.8f, 0.0f)
}
};
enum { nWireframes = sizeof wireframes / sizeof *wireframes };
// the view projection matrices
Mat4x4f matViewFront(InitIdent);
Mat4x4f matViewTop(InitRotX, degToRad(90.0f));
Mat4x4f matViewLeft(InitRotY, degToRad(-90.0f));
Mat4x4f matViewIso
= Mat4x4f(InitRotX, degToRad(30.0f))
* Mat4x4f(InitRotY, degToRad(45.0));
// build GUI
QWidget win;
QGridLayout qGrid;
QLabel qLblTop(QString::fromUtf8("<b>Top View</b>"));
qLblTop.setTextFormat(Qt::RichText);
qLblTop.setAlignment(Qt::AlignCenter);
qGrid.addWidget(&qLblTop, 0, 0);
WireframeView viewTop(nWireframes, wireframes, matViewTop);
qGrid.addWidget(&viewTop, 1, 0);
QLabel qLblFront(QString::fromUtf8("<b>Front View</b>"));
qLblFront.setTextFormat(Qt::RichText);
qLblFront.setAlignment(Qt::AlignCenter);
qGrid.addWidget(&qLblFront, 2, 0);
WireframeView viewFront(nWireframes, wireframes, matViewFront);
qGrid.addWidget(&viewFront, 3, 0);
QLabel qLblIso(QString::fromUtf8("<b>Isometric View</b>"));
qLblIso.setTextFormat(Qt::RichText);
qLblIso.setAlignment(Qt::AlignCenter);
qGrid.addWidget(&qLblIso, 0, 1);
WireframeView viewIso(nWireframes, wireframes, matViewIso);
qGrid.addWidget(&viewIso, 1, 1);
QLabel qLblLeft(QString::fromUtf8("<b>Left View</b>"));
qLblLeft.setTextFormat(Qt::RichText);
qLblLeft.setAlignment(Qt::AlignCenter);
qGrid.addWidget(&qLblLeft, 2, 1);
WireframeView viewLeft(nWireframes, wireframes, matViewLeft);
qGrid.addWidget(&viewLeft, 3, 1);
qGrid.setRowStretch(1, 1); qGrid.setRowStretch(3, 1);
win.setLayout(&qGrid);
win.show();
// exec. application
return app.exec();
}
For each view, the projection is separated into two matrices.
The actual projection is provided to the constructor of WireframeView
.
The class WireframeView
manages internally a second transformation from NDC (Normalized Device Coordinates) to screen space. This includes the scaling (under consideration of the current aspect ratio), the mirroring of y coordinates, and the translation of origin (0, 0, 0) to center of view.
These two matrices are multiplied before the actual rendering starts. In the rendering loop, each point is multiplied with the combined view matrix to transform it from (model) world coordinates to screen coordinates.
I moved the mathematical stuff to a separate header linmath.h
:
#ifndef LIN_MATH_H
#define LIN_MATH_H
#include <iostream>
#include <cmath>
template <typename VALUE>
inline VALUE degToRad(VALUE angle)
{
return (VALUE)3.1415926535897932384626433832795 * angle / (VALUE)180;
}
template <typename VALUE>
struct Vec2T {
VALUE x, y;
Vec2T(VALUE x, VALUE y): x(x), y(y) { }
};
template <typename VALUE>
std::ostream& operator<<(std::ostream &out, const Vec2T<VALUE> &v)
{
return out << "( " << v.x << ", " << v.y << " )";
}
typedef Vec2T<float> Vec2f;
typedef Vec2T<double> Vec2;
template <typename VALUE>
struct Vec3T {
VALUE x, y, z;
Vec3T(VALUE x, VALUE y, VALUE z): x(x), y(y), z(z) { }
Vec3T(const Vec2T<VALUE> &xy, VALUE z): x(xy.x), y(xy.y), z(z) { }
explicit operator Vec2T<VALUE>() const { return Vec2T<VALUE>(x, y); }
};
typedef Vec3T<float> Vec3f;
typedef Vec3T<double> Vec3;
template <typename VALUE>
struct Vec4T {
VALUE x, y, z, w;
Vec4T(VALUE x, VALUE y, VALUE z, VALUE w): x(x), y(y), z(z), w(w) { }
Vec4T(const Vec2T<VALUE> &xy, VALUE z, VALUE w):
x(xy.x), y(xy.y), z(z), w(w)
{ }
Vec4T(const Vec3T<VALUE> &xyz, VALUE w):
x(xyz.x), y(xyz.y), z(xyz.z), w(w)
{ }
explicit operator Vec2T<VALUE>() const { return Vec2T<VALUE>(x, y); }
explicit operator Vec3T<VALUE>() const { return Vec3T<VALUE>(x, y, z); }
};
typedef Vec4T<float> Vec4f;
typedef Vec4T<double> Vec4;
enum ArgInitIdent { InitIdent };
enum ArgInitTrans { InitTrans };
enum ArgInitRotX { InitRotX };
enum ArgInitRotY { InitRotY };
enum ArgInitRotZ { InitRotZ };
enum ArgInitScale { InitScale };
template <typename VALUE>
struct Mat4x4T {
union {
VALUE comp[4 * 4];
struct {
VALUE _00, _01, _02, _03;
VALUE _10, _11, _12, _13;
VALUE _20, _21, _22, _23;
VALUE _30, _31, _32, _33;
};
};
// constructor to build a matrix by elements
Mat4x4T(
VALUE _00, VALUE _01, VALUE _02, VALUE _03,
VALUE _10, VALUE _11, VALUE _12, VALUE _13,
VALUE _20, VALUE _21, VALUE _22, VALUE _23,
VALUE _30, VALUE _31, VALUE _32, VALUE _33)
{
this->_00 = _00; this->_01 = _01; this->_02 = _02; this->_03 = _03;
this->_10 = _10; this->_11 = _11; this->_12 = _12; this->_13 = _13;
this->_20 = _20; this->_21 = _21; this->_22 = _22; this->_23 = _23;
this->_30 = _30; this->_31 = _31; this->_32 = _32; this->_33 = _33;
}
// constructor to build an identity matrix
Mat4x4T(ArgInitIdent)
{
_00 = (VALUE)1; _01 = (VALUE)0; _02 = (VALUE)0; _03 = (VALUE)0;
_10 = (VALUE)0; _11 = (VALUE)1; _12 = (VALUE)0; _13 = (VALUE)0;
_20 = (VALUE)0; _21 = (VALUE)0; _22 = (VALUE)1; _23 = (VALUE)0;
_30 = (VALUE)0; _31 = (VALUE)0; _32 = (VALUE)0; _33 = (VALUE)1;
}
// constructor to build a matrix for translation
Mat4x4T(ArgInitTrans, const Vec3T<VALUE> &t)
{
_00 = (VALUE)1; _01 = (VALUE)0; _02 = (VALUE)0; _03 = (VALUE)t.x;
_10 = (VALUE)0; _11 = (VALUE)1; _12 = (VALUE)0; _13 = (VALUE)t.y;
_20 = (VALUE)0; _21 = (VALUE)0; _22 = (VALUE)1; _23 = (VALUE)t.z;
_30 = (VALUE)0; _31 = (VALUE)0; _32 = (VALUE)0; _33 = (VALUE)1;
}
// constructor to build a matrix for rotation about x axis
Mat4x4T(ArgInitRotX, VALUE angle)
{
_00 = (VALUE)1; _01 = (VALUE)0; _02 = (VALUE)0; _03 = (VALUE)0;
_10 = (VALUE)0; _11 = cos(angle); _12 = sin(angle); _13 = (VALUE)0;
_20 = (VALUE)0; _21 = -sin(angle); _22 = cos(angle); _23 = (VALUE)0;
_30 = (VALUE)0; _31 = (VALUE)0; _32 = (VALUE)0; _33 = (VALUE)1;
}
// constructor to build a matrix for rotation about y axis
Mat4x4T(ArgInitRotY, VALUE angle)
{
_00 = cos(angle); _01 = (VALUE)0; _02 = -sin(angle); _03 = (VALUE)0;
_10 = (VALUE)0; _11 = (VALUE)1; _12 = (VALUE)0; _13 = (VALUE)0;
_20 = sin(angle); _21 = (VALUE)0; _22 = cos(angle); _23 = (VALUE)0;
_30 = (VALUE)0; _31 = (VALUE)0; _32 = (VALUE)0; _33 = (VALUE)1;
}
// constructor to build a matrix for rotation about z axis
Mat4x4T(ArgInitRotZ, VALUE angle)
{
_00 = cos(angle); _01 = sin(angle); _02 = (VALUE)0; _03 = (VALUE)0;
_10 = -sin(angle); _11 = cos(angle); _12 = (VALUE)0; _13 = (VALUE)0;
_20 = (VALUE)0; _21 = (VALUE)0; _22 = (VALUE)1; _23 = (VALUE)0;
_30 = (VALUE)0; _31 = (VALUE)0; _32 = (VALUE)0; _33 = (VALUE)1;
}
// constructor to build a matrix for scaling
Mat4x4T(ArgInitScale, VALUE sx, VALUE sy, VALUE sz)
{
_00 = (VALUE)sx; _01 = (VALUE)0; _02 = (VALUE)0; _03 = (VALUE)0;
_10 = (VALUE)0; _11 = (VALUE)sy; _12 = (VALUE)0; _13 = (VALUE)0;
_20 = (VALUE)0; _21 = (VALUE)0; _22 = (VALUE)sz; _23 = (VALUE)0;
_30 = (VALUE)0; _31 = (VALUE)0; _32 = (VALUE)0; _33 = (VALUE)1;
}
// multiply matrix with matrix -> matrix
Mat4x4T operator * (const Mat4x4T &mat) const
{
return Mat4x4T(
_00 * mat._00 + _01 * mat._10 + _02 * mat._20 + _03 * mat._30,
_00 * mat._01 + _01 * mat._11 + _02 * mat._21 + _03 * mat._31,
_00 * mat._02 + _01 * mat._12 + _02 * mat._22 + _03 * mat._32,
_00 * mat._03 + _01 * mat._13 + _02 * mat._23 + _03 * mat._33,
_10 * mat._00 + _11 * mat._10 + _12 * mat._20 + _13 * mat._30,
_10 * mat._01 + _11 * mat._11 + _12 * mat._21 + _13 * mat._31,
_10 * mat._02 + _11 * mat._12 + _12 * mat._22 + _13 * mat._32,
_10 * mat._03 + _11 * mat._13 + _12 * mat._23 + _13 * mat._33,
_20 * mat._00 + _21 * mat._10 + _22 * mat._20 + _23 * mat._30,
_20 * mat._01 + _21 * mat._11 + _22 * mat._21 + _23 * mat._31,
_20 * mat._02 + _21 * mat._12 + _22 * mat._22 + _23 * mat._32,
_20 * mat._03 + _21 * mat._13 + _22 * mat._23 + _23 * mat._33,
_30 * mat._00 + _31 * mat._10 + _32 * mat._20 + _33 * mat._30,
_30 * mat._01 + _31 * mat._11 + _32 * mat._21 + _33 * mat._31,
_30 * mat._02 + _31 * mat._12 + _32 * mat._22 + _33 * mat._32,
_30 * mat._03 + _31 * mat._13 + _32 * mat._23 + _33 * mat._33);
}
// multiply matrix with vector -> vector
Vec4T<VALUE> operator * (const Vec4T<VALUE> &vec) const
{
return Vec4T<VALUE>(
_00 * vec.x + _01 * vec.y + _02 * vec.z + _03 * vec.w,
_10 * vec.x + _11 * vec.y + _12 * vec.z + _13 * vec.w,
_20 * vec.x + _21 * vec.y + _22 * vec.z + _23 * vec.w,
_30 * vec.x + _31 * vec.y + _32 * vec.z + _33 * vec.w);
}
};
typedef Mat4x4T<float> Mat4x4f;
typedef Mat4x4T<double> Mat4x4;
#endif // LIN_MATH_H
The most significant changes (compared to the original version):