1

I have a 2D unit vector containing a world coordinate (some direction), and I want to convert that to screen coordinates (classic isometric tiles).

I'm aware I can achieve this by rotating around the relevant axis but I want to see and understand how to do this using a purely matrix approach? Partly because I'm learning 'modern OpenGL' (v2+) and partly because I will want to use this same technique for other things so need a solid understanding and my math ability is a little lacking.

If needed my screen's coordinate system has it's origin at top left with +x & +y pointing right and down respectively. Also, my vertex positions are converted to the NDC range in my vertex shader if that's relevant.

Language is C++ with no supporting libraries.

Adam Naylor
  • 5,745
  • 9
  • 46
  • 65

1 Answers1

2

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:

Snapshot of test-QIsoView

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.

  1. The actual projection is provided to the constructor of WireframeView.

  2. 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):

  • upgrade from 3×3 matrix to 4×4 matrix. (needed to involve 3D translations also)

  • templates instead of functions and classes (can be used for float and double).

Scheff's Cat
  • 16,517
  • 5
  • 25
  • 45
  • 1
    @AdamNaylor Remembering, that you mentioned OpenGL, I made a minor update (added the OpenGL 4x4 matrix also). – Scheff's Cat Jun 12 '17 at 16:00
  • @AdamNaylor Just realized that my Pi was wrong (last 3 digits) - fixed. – Scheff's Cat Jun 12 '17 at 16:10
  • This is has worked thank you, although for some reason I am having to rotate around `x` with `60°`. I must be missing something somewhere like a flipped sign. – Adam Naylor Jun 18 '17 at 17:07
  • Hmm. These things drive me crazy always: putting the matrices into the right multiplication order as well as to remember whether a matrix is needed or it's inverse. At least, I never forget that matrix multiplication is _not_ commutative (except in the special cases for translation and identity)... – Scheff's Cat Jun 18 '17 at 17:16
  • I seriously was considering to make an OpenGL visualization but I couldn't imagine how to dot it as _small_ [MCVE](http://stackoverflow.com/help/mcve). May be in Qt with OpenGL 1.1 it could become as short as possible. I will try this next week... – Scheff's Cat Jun 18 '17 at 17:20
  • 2
    After having the answer posted, I was thinking a little bit about it. Actually, the isometric projection (as described in Wikipedia) is nothing else than an orthographic view (in opposition to perspective view) with specific rotation angles. May be, it could be worth to add a translation for fitting the 3D models into view. I'm sure these things are mentioned in the common OpenGL tutorials (more or less) you find by google. (I learnt it from the "Red Book" at a time where the "Red Book" was still up to date.) – Scheff's Cat Jun 18 '17 at 17:28
  • @AdamNaylor Finally, I made a Qt application without OpenGL (though it uses OpenGL like matrices). Actually, I build the isometric view matrix by `Mat4x4f(InitRotX, degToRad(30.0f)) * Mat4x4f(InitRotY, degToRad(45.0))`. (similar to the original sample. I hadn't to change any value though it hadn't surprised me.) A cube is not the best sample model as it is quite symmetric. I used a pyramid (with the unit axes as edges) for debugging which I left in. – Scheff's Cat Jun 20 '17 at 19:26
  • Thanks for all your help on this, I've accepted your answer. One further question I had is how to invert this projection? I've tried reversing the sign of each of the rotations and applying them in the opposite order but I'm getting strange results. I've tried a lot of variations to try and understand it. – Adam Naylor Jun 28 '17 at 15:27
  • I'm guessing it's because I'm zeroing out the z after each projection (as I'm working in 2D) Once that info is lost I'm guessing the rotations will be different? – Adam Naylor Jun 28 '17 at 15:28
  • 1
    @AdamNaylor Inverse matrix in general, you find by google or in your math book. There are some interesting special cases: Inverse translation matrix is trivial (as translation vector is the last column). Inverse rotation matrix is the transposed matrix (i.e. flipped at main diagonale). Based on this: A matrix with translation and rotation, you can decompose into translation and rotation matrix, invert them separately, and multiplying them together. M^-1 = (R*T)^-1 = T^-1 * R^-1. (Please, consider, the reversed order in "T^-1 * R^-1".) – Scheff's Cat Jun 28 '17 at 16:57
  • 1
    @AdamNaylor I consider decomposition of translation/rotation matrix as trivial because: Rotation matrix is the upper left 3x3 matrix. Translation matrix is the identity matrix with last column. Thus, decomposition can be done with just storing (overwriting) the resp. elements in an identity matrix. – Scheff's Cat Jun 28 '17 at 17:02