Настоящая изометрическая проекция с использованием HTML5 Canvas - PullRequest
4 голосов
/ 12 июля 2011

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

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

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

Ответы [ 2 ]

11 голосов
/ 10 октября 2011

Во-первых, я бы рекомендовал думать об игровом мире как о регулярной сетке квадратных плиток 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 по диагональным осям ).Затем просто разделите мировые координаты на ширину и высоту плитки, чтобы получить «координаты карты».

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

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

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

5 голосов
/ 30 июня 2017

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

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

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

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

Для проецирования точкивызовите функцию axoProjMat({x=10,y=10,z=10}), и она вернет трехмерную точку с 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), где 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 как подробно описано в вики-ссылке в вопросе ОП и с использованием вышеуказанных функций и объектов.

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>
...