使用HTML5 Canvas实现真正的等轴测投影

8
3个回答

15

等轴投影渲染

处理等轴(通常称为等角)投影渲染的最佳方法是使用投影矩阵。

下面这个对象可以描述进行任何形式的等轴投影所需的所有内容。

该对象有三个变换,分别对应x、y和z轴,每个变换都描述了x、y和z坐标在2D投影中的比例和方向。还有一个深度计算的变换和一个原点,它位于画布像素上(如果设置setTransform(1,0,0,1,0,0)或画布的当前变换)。

要投影一个点,调用函数axoProjMat({x=10,y=10,z=10}),它将返回一个3D点,其中x、y是顶点的2D坐标,z是深度(具有正值的深度接近视角,与3D透视投影相反);

  // 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为前缀)。在name参数中使用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) ,
  }
}

真等轴投影的示例。

这个片段是一个简单的示例,将投影设置为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>


13

首先,我建议将游戏世界想象成一个由方块瓦片组成的正常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变换矩阵的地方。

这实际上并不难,当绘制对象时需要在世界坐标和视图坐标之间进行转换,因此您将需要这个。

希望这可以帮助到您。


我该如何增加深度(z)?并在等角视图上绘制其他对象。 - redigaffi
假设您的世界坐标已经在3D空间中,例如x,y,z,您需要通过投影矩阵将每个对象的位置乘以,以转换为2D空间。我不记得确切的矩阵,但应该很容易查找。 - alekop

-1

我为我的等距应用程序创建了这个东西

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;
    }
}

1
如果你能把它分开,并描述一下正在发生的事情以解释它。 - Liam

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接