Настоящая изометрическая проекция с холстом HTML5

Я новичок в HTML5 Canvas и JavaScript, но есть ли простой способ получить изометрическую проекцию в элементе HTML5 Canvas?

Я имею в виду истинную изометрическую проекцию - http://en.wikipedia.org/wiki/Isometric_projection

Спасибо всем за ответы.


person gma    schedule 12.07.2011    source источник


Ответы (3)


Во-первых, я бы рекомендовал думать об игровом мире как об обычной сетке X на Y квадратных плиток. Это значительно упрощает все, от обнаружения столкновений, поиска пути и даже рендеринга.

Чтобы визуализировать карту в изометрической проекции, просто измените матрицу проекции:

var ctx = canvas.getContext('2d');

function render(ctx) {
    var dx = 0, dy = 0;
    ctx.save();

    // change projection to isometric view
    ctx.translate(view.x, view.y);
    ctx.scale(1, 0.5);
    ctx.rotate(45 * Math.PI /180);

    for (var y = 0; i < 10; y++) {
        for (var x = 0; x < 10; x++) {
            ctx.strokeRect(dx, dy, 40, 40);
            dx += 40;
        }
        dx = 0;
        dy += 40;
    }

    ctx.restore(); // back to orthogonal projection

    // Now, figure out which tile is under the mouse cursor... :)
}

Это захватывающе в первый раз, когда вы начнете работать, но вы быстро поймете, что это не так уж полезно для рисования настоящих изометрических карт ... вы не можете просто вращать изображения плитки и смотреть, что за углом. Преобразования предназначены не столько для рисования, сколько для преобразования между экранным пространством и мировым пространством.

Бонус: выяснить, над какой плиткой находится мышь

Что вам нужно сделать, так это преобразовать «координаты вида» (смещения пикселей от начала координат холста) в «мировые координаты» (смещения пикселей от плитки 0,0 вдоль диагональных осей). Затем просто разделите мировые координаты на ширину и высоту плитки, чтобы получить «координаты карты».

Теоретически все, что вам нужно сделать, это спроецировать вектор "позиция просмотра" на обратную матрицу проекции выше, чтобы получить "мировую позицию". Я говорю в теории, потому что по какой-то причине холст не предоставляет способ вернуть текущую матрицу проекции. Есть метод setTransform(), но нет getTransform(), поэтому здесь вам придется свернуть собственную матрицу преобразования 3x3.

На самом деле это не так сложно, и вам понадобится это для преобразования между мировыми и видовыми координатами при рисовании объектов.

Надеюсь это поможет.

person alekop    schedule 10.10.2011
comment
Как я могу добавить глубину (z)? И нарисуйте другие объекты поверх этого изометрического вида. - person redigaffi; 13.05.2017
comment
@redigaffi Предполагая, что ваши мировые координаты уже находятся в трехмерном пространстве, например. x, y, z, вам нужно будет умножить положение каждого объекта на матрицу проекции, чтобы преобразовать его в 2D-пространство. Я не помню точную матрицу, но ее должно быть довольно легко найти. - person alekop; 31.05.2017

Аксонометрический рендеринг

Лучший способ обработки аксонометрического (обычно называемого изометрическим) рендеринга — через матрицу проекции.

Следующий объект проекции может описать все, что вам нужно для любой формы аксонометрической проекции.

Объект имеет 3 преобразования для осей x, y и z, каждое из которых описывает масштаб и направление в 2D-проекции для координат x, y, z. Преобразование для вычисления глубины и начало координат в пикселях холста (если setTransform(1,0,0,1,0,0) или любое другое текущее преобразование для холста)

Чтобы спроецировать точку, вызовите функцию axoProjMat({x=10,y=10,z=10}), и она вернет 3D-точку, где x, y — 2D-координаты вершины, а z — глубина (с положительными значениями глубины, приближающимися к виду (в отличие от трехмерной проекции перспективы));

  // 3d 2d points
  const P3 = (x=0, y=0, z=0) => ({x,y,z});
  const P2 = (x=0, y=0) => ({x, y});
  // projection object
  const axoProjMat = {
      xAxis : P2(1 , 0.5) ,
      yAxis :  P2(-1 , 0.5) ,
      zAxis :  P2(0 , -1) ,
      depth :  P3(0.5,0.5,1) , // projections have z as depth
      origin : P2(), // (0,0) default 2D point
      setProjection(name){
        if(projTypes[name]){
          Object.keys(projTypes[name]).forEach(key => {
            this[key]=projTypes[name][key];
          })
          if(!projTypes[name].depth){
            this.depth = P3(
              this.xAxis.y,
              this.yAxis.y,
              -this.zAxis.y
            );
          }
        }
      },
      project (p, retP = P3()) {
          retP.x = p.x * this.xAxis.x + p.y * this.yAxis.x + p.z * this.zAxis.x + this.origin.x;
          retP.y = p.x * this.xAxis.y + p.y * this.yAxis.y + p.z * this.zAxis.y + this.origin.y;
          retP.z = p.x * this.depth.x + p.y * this.depth.y + p.z * this.depth.z; 
          return retP;
      }
  }

С вышеуказанным объектом вы можете использовать функцию axoProjMat.setProjection(name) для выбора типа проекции.

Ниже приведены связанные типы проекций, описанные в вики аксонометрические проекции, а также две модификации, обычно используемые в пиксельной графике. и игры (с префиксом Pixel). Используйте axoProjMat.setProjection(name), где имя является одним из projTypes имен свойств.

const D2R = (ang) => (ang-90) * (Math.PI/180 );
const Ang2Vec = (ang,len = 1) => P2(Math.cos(D2R(ang)) * len,Math.sin(D2R(ang)) * len);
const projTypes = {
  PixelBimetric : {
    xAxis : P2(1 , 0.5) ,
    yAxis :  P2(-1 , 0.5) ,
    zAxis :  P2(0 , -1) ,
    depth :  P3(0.5,0.5,1) , // projections have z as depth      
  },
  PixelTrimetric : {
    xAxis : P2(1 , 0.5) ,
    yAxis :  P2(-0.5 , 1) ,
    zAxis :  P2(0 , -1) ,
    depth :  P3(0.5,1,1) ,
  },
  Isometric : {
    xAxis : Ang2Vec(120) ,
    yAxis : Ang2Vec(-120) ,
    zAxis : Ang2Vec(0) ,
  },
  Bimetric : {
    xAxis : Ang2Vec(116.57) ,
    yAxis : Ang2Vec(-116.57) ,
    zAxis : Ang2Vec(0) ,
  },
  Trimetric : {
    xAxis : Ang2Vec(126.87,2/3) ,
    yAxis : Ang2Vec(-104.04) ,
    zAxis : Ang2Vec(0) ,
  },
  Military : {
    xAxis : Ang2Vec(135) ,
    yAxis : Ang2Vec(-135) ,
    zAxis : Ang2Vec(0) ,
  },
  Cavalier : {
    xAxis : Ang2Vec(135) ,
    yAxis : Ang2Vec(-90) ,
    zAxis : Ang2Vec(0) ,
  },
  TopDown : {
    xAxis : Ang2Vec(180) ,
    yAxis : Ang2Vec(-90) ,
    zAxis : Ang2Vec(0) ,
  }
}

Пример истинной изометрической проекции.

Фрагмент представляет собой простой пример с проекцией, установленной на Isometric, как подробно описано в вики-ссылке в вопросе OP, и с использованием вышеуказанных функций и объектов.

const ctx = canvas.getContext("2d");

// function creates a 3D point (vertex)
function vertex(x, y, z) { return { x, y, z}};
// an array of vertices
const vertices = []; // an array of vertices

// create the 8 vertices that make up a box
const boxSize = 20; // size of the box
const hs = boxSize / 2; // half size shorthand for easier typing

vertices.push(vertex(-hs, -hs, -hs)); // lower top left  index 0
vertices.push(vertex(hs, -hs, -hs)); // lower top right
vertices.push(vertex(hs, hs, -hs)); // lower bottom right
vertices.push(vertex(-hs, hs, -hs)); // lower bottom left
vertices.push(vertex(-hs, -hs, hs)); // upper top left  index 4
vertices.push(vertex(hs, -hs, hs)); // upper top right
vertices.push(vertex(hs, hs, hs)); // upper bottom right
vertices.push(vertex(-hs, hs, hs)); // upper  bottom left index 7



const colours = {
  dark: "#040",
  shade: "#360",
  light: "#ad0",
  bright: "#ee0",
}

function createPoly(indexes, colour) {
  return {
    indexes,
    colour
  }
}
const polygons = [];

polygons.push(createPoly([1, 2, 6, 5], colours.shade)); // right face
polygons.push(createPoly([2, 3, 7, 6], colours.light)); // front face
polygons.push(createPoly([4, 5, 6, 7], colours.bright)); // top face



// From here in I use P2,P3 to create 2D and 3D points
const P3 = (x = 0, y = 0, z = 0) => ({x,y,z});
const P2 = (x = 0, y = 0) => ({ x, y});
const D2R = (ang) => (ang-90) * (Math.PI/180 );
const Ang2Vec = (ang,len = 1) => P2(Math.cos(D2R(ang)) * len,Math.sin(D2R(ang)) * len);
const projTypes = {
  PixelBimetric : {
    xAxis : P2(1 , 0.5) ,
    yAxis :  P2(-1 , 0.5) ,
    zAxis :  P2(0 , -1) ,
    depth :  P3(0.5,0.5,1) , // projections have z as depth      
  },
  PixelTrimetric : {
    xAxis : P2(1 , 0.5) ,
    yAxis :  P2(-0.5 , 1) ,
    zAxis :  P2(0 , -1) ,
    depth :  P3(0.5,1,1) ,
  },
  Isometric : {
    xAxis : Ang2Vec(120) ,
    yAxis : Ang2Vec(-120) ,
    zAxis : Ang2Vec(0) ,
  },
  Bimetric : {
    xAxis : Ang2Vec(116.57) ,
    yAxis : Ang2Vec(-116.57) ,
    zAxis : Ang2Vec(0) ,
  },
  Trimetric : {
    xAxis : Ang2Vec(126.87,2/3) ,
    yAxis : Ang2Vec(-104.04) ,
    zAxis : Ang2Vec(0) ,
  },
  Military : {
    xAxis : Ang2Vec(135) ,
    yAxis : Ang2Vec(-135) ,
    zAxis : Ang2Vec(0) ,
  },
  Cavalier : {
    xAxis : Ang2Vec(135) ,
    yAxis : Ang2Vec(-90) ,
    zAxis : Ang2Vec(0) ,
  },
  TopDown : {
    xAxis : Ang2Vec(180) ,
    yAxis : Ang2Vec(-90) ,
    zAxis : Ang2Vec(0) ,
  }
}

const axoProjMat = {
  xAxis : P2(1 , 0.5) ,
  yAxis :  P2(-1 , 0.5) ,
  zAxis :  P2(0 , -1) ,
  depth :  P3(0.5,0.5,1) , // projections have z as depth
  origin : P2(150,65), // (0,0) default 2D point
  setProjection(name){
    if(projTypes[name]){
      Object.keys(projTypes[name]).forEach(key => {
        this[key]=projTypes[name][key];
      })
      if(!projTypes[name].depth){
        this.depth = P3(
          this.xAxis.y,
          this.yAxis.y,
          -this.zAxis.y
        );
      }
    }
  },
  project (p, retP = P3()) {
      retP.x = p.x * this.xAxis.x + p.y * this.yAxis.x + p.z * this.zAxis.x + this.origin.x;
      retP.y = p.x * this.xAxis.y + p.y * this.yAxis.y + p.z * this.zAxis.y + this.origin.y;
      retP.z = p.x * this.depth.x + p.y * this.depth.y + p.z * this.depth.z; 
      return retP;
  }
}
axoProjMat.setProjection("Isometric");

var x,y,z;
for(z = 0; z < 4; z++){
   const hz = z/2;
   for(y = hz; y < 4-hz; y++){
       for(x = hz; x < 4-hz; x++){
          // move the box
          const translated = vertices.map(vert => {
               return P3(
                   vert.x + x * boxSize, 
                   vert.y + y * boxSize, 
                   vert.z + z * boxSize, 
               );
          });
                   
          // create a new array of 2D projected verts
          const projVerts = translated.map(vert => axoProjMat.project(vert));
          // and render
          polygons.forEach(poly => {
            ctx.fillStyle = poly.colour;
            ctx.strokeStyle = poly.colour;
            ctx.lineWidth = 1;
            ctx.beginPath();
            poly.indexes.forEach(index => ctx.lineTo(projVerts[index].x , projVerts[index].y));
            ctx.stroke();
            ctx.fill();
            
          });
      }
   }
}
canvas {
  border: 2px solid black;
}
body { font-family: arial; }
True Isometric projection. With x at 120deg, and y at -120deg from up.<br>
<canvas id="canvas"></canvas>

person Blindman67    schedule 30.06.2017

Я создаю нечто подобное для своего изометрического приложения.

class IsoProjection {
    constructor() {
        this.matP = [1, 0, 0, 1, 0, 0];
        this.matI = [1, 0, 0, 1, 0, 0];
        this.mapRatio = 1;
        this.mapRatioI = 1;
    }
    isoToTilePos(a, ao) {
        let m = this.matI,
        b = ao || [],
        i = 0,
        j = 1;
        do {
            j = i + 1;
            b[i] = a[i] * m[0] + a[j] * m[2] + m[4];
            b[j] = a[i] * m[1] + a[j] * m[3] + m[5];
            i += 2;
        } while (i < a.length);
        return b;
    }
    tileToIsoPos(a, ao) {
        let m = this.matP,
        b = ao || [],
        i = 0,
        j = 1;
        do {
            j = i + 1;
            b[i] = a[i] * m[0] + a[j] * m[2] + m[4];
            b[j] = a[i] * m[1] + a[j] * m[3] + m[5];
            i += 2;
        } while (i < a.length);
        return b;
    }
    reset(numC, numR, cellW, cellH) {
        /*
            Math.sqrt(2 * isoW * isoW) = cellW
            isoW = Math.sqrt(cellW * cellW / 2);
            while map's tileW = 1
        */
        let isoW = Math.sqrt(cellW * cellW / 2);
        this.mapRatio = isoW;
        this.mapRatioI = 1 / isoW;
        // translation
        let ctr = Math.max(numC, numR) / 2;
        //rotation
        let rot = -Math.PI / 4;
        let cos = Math.cos(rot);
        let sin = Math.sin(rot);
        // scale
        let sx = isoW;
        let sy = cellH / cellW * isoW;
        // the matrix
        this.matP[0] = sx * cos;
        this.matP[1] = sy * sin;
        this.matP[2] = sx * -sin;
        this.matP[3] = sy * cos;
        this.matP[4] = 0;
        this.matP[5] = 0;
        // the inverted matrix;
        let a = this.matP[0],
        b = this.matP[1],
        c = this.matP[2],
        d = this.matP[3],
        e = this.matP[4],
        f = this.matP[5];
        let det = a * d - b * c;
        if (det !== 0) {
            det = 1 / det;
            this.matI[0] = d * det;
            this.matI[1] =  - b * det;
            this.matI[2] =  - c * det;
            this.matI[3] = a * det;
            this.matI[4] = (c * f - e * d) * det;
            this.matI[5] = (e * b - a * f) * det;
        } else {
            this.matI[0] = a;
            this.matI[1] = b;
            this.matI[2] = c;
            this.matI[3] = d;
            this.matI[4] = e;
            this.matI[5] = f;
        }
        return this;
    }
}
person Imam Kuncoro    schedule 06.02.2021
comment
Если бы вы могли разделить это и, возможно, описать, что происходит, чтобы объяснить это. - person Liam; 06.02.2021