【问题标题】:HTML5 Canvas generate isometric tiles [duplicate]HTML5 Canvas 生成等距图块 [重复]
【发布时间】:2017-06-29 21:32:03
【问题描述】:

我正在尝试在不使用图像的情况下在 HTML5 Canvas 中生成基本的瓷砖和楼梯。

这是我到目前为止所做的:

但我正在尝试重现:

我不知道该怎么做。



这是我当前的代码:

class IsometricGraphics {
    constructor(canvas, thickness) {
        this.Canvas = canvas;
        this.Context = canvas.getContext("2d");

        if(thickness) {
            this.thickness = thickness;
        } else {
            this.thickness = 2;
        }
    }

    LeftPanelWide(x, y, fillStyle) {
        this.Context.fillStyle = fillStyle;

        for(var i = 0; i < 16; i++) {
            this.Context.fillRect(x + i * 2, y + i * 1, 2, this.thickness * 4);
        }
    }

    RightPanelWide(x, y, fillStyle) {
        this.Context.fillStyle = fillStyle;

        for(var i = 0; i < 16; i++) {
            this.Context.fillRect(x + (i * 2), y + 15 - (i * 1), 2, this.thickness * 4);
        }
    }

    UpperPanelWide(x, y, fillStyle) {
        this.Context.fillStyle = fillStyle;

        for(var i = 0; i < 17; i++) {
            this.Context.fillRect(x + 16 + 16 - (i * 2), y + i - 2, i * 4, 1);
        }

        for(var i = 0; i < 16; i++) {
            this.Context.fillRect(x + i * 2, y + (32 / 2) - 1 + i, ((32 / 2) - i) * 4, 1);
        }
    }

    UpperPanelWideBorder(x, y, fillStyle) {
        this.Context.fillStyle = fillStyle;

        var y = y + 2;

        for(var i = 0; i < 17; i++) {
            this.Context.fillRect(x + 17 + 16 - (i * 2) - 2, y + i - 2, (i == 17) ? 1 : 2, 1);
            this.Context.fillRect(x + 17 + 16 + (i * 2) - 2, y + i - 2, (i == 17) ? 1 : 2, 1);
        }

        for(var i = 0; i < 32 / 2; i++) {
            this.Context.fillRect(x + i * 2, y + 16 - 1 + i, 2, 1);
            this.Context.fillRect(x + 62 - i * 2, y + 16 - 1 + i, 2, 1);
        }
    }

    RightUpperPanelSmall(x, y, fillStyle) {
        this.Context.fillStyle = fillStyle;

        for(var i = 0; i < 32 / 2 + 4; i++) {
            this.Context.fillRect(x + (i * 2), (i >= 4) ? (i - 1) + y : 3 - i + 3 + y, 2, (i >= 4) ? (i <= 20 - 5) ? 8 : (20 - i) * 2 - 1 : 1 + (i * 2));
        }
    }

    LeftUpperPanelSmall(x, y, fillStyle) {
        this.Context.fillStyle = fillStyle;

        for(var i = 0; i < 32 / 2 + 4; i++) {
            this.Context.fillRect(x + (i * 2), (i >= 16) ? y + (i - 16) : 16 + y - (i * 1) - 1, 2, (i >= 4) ? (i >= 16) ? 8 - (i - 16) - (i - 16) - 1 : 8 : 8 * i - (i * 6) + 1);
        }
    }

    LeftPanelSmall(x, y, fillStyle) {
        this.Context.fillStyle = fillStyle;

        for(var i = 0; i < 8 / 2; i++) {
            this.Context.fillRect(x + i * 2, y + i * 1, 2, this.thickness * 4);
        }
    }

    RightPanelSmall(x, y, fillStyle) {
        this.Context.fillStyle = fillStyle;

        for(var i = 0; i < 8 / 2; i++) {
            this.Context.fillRect(x + (i * 2), y + 3 - (i * 1), 2, this.thickness * 4);
        }
    }
}


class IsoGenerator {
    constructor() {
        var Canvas = document.querySelector("canvas");
        var Context = Canvas.getContext("2d");

        //Context.scale(5, 5);
        this.Context = Context;
        this.IsometricGraphics = new IsometricGraphics(Canvas, 2);
    }

    StairLeft(x, y, Color1, Color2, Color3) {
        for(var i = 0; i < 4; i++) {
            this.IsometricGraphics.RightPanelWide((x + 8) + (i * 8), (y + 4) + (i * 12), Color1);
            this.IsometricGraphics.LeftUpperPanelSmall(x + (i * 8), y + (i * 12), Color2);
            this.IsometricGraphics.LeftPanelSmall((i * 8) + x, (16 + (i * 12)) + y, Color3);
        }
    }

    StairRight(x, y, Color1, Color2, Color3) {
        for(var i = 0; i < 4; i++) {
            this.IsometricGraphics.LeftPanelWide(x + 24 - (i * 8), (4 + (i * 12)) + y, Color1);
            this.IsometricGraphics.RightUpperPanelSmall(x + 24 - (i * 8), y + (i * 12) - 3, Color2);
            this.IsometricGraphics.RightPanelSmall(x + 56 - (i * 8), (16 + (i * 12)) + y, Color3);
        }
    }

    Tile(x, y, Color1, Color2, Color3, Border) {
        this.IsometricGraphics.LeftPanelWide(x, 18 + y, Color1);
        this.IsometricGraphics.RightPanelWide(x + 32, 18 + y, Color2);
        this.IsometricGraphics.UpperPanelWide(x, 2 + y, Color3);

        if(Border) {
            this.IsometricGraphics.UpperPanelWideBorder(x, y, Border);
        }
    }
}

var Canvas = document.querySelector("canvas");
var Context = Canvas.getContext("2d");

Context.scale(3, 3);

new IsoGenerator().Tile(0, 0, "#B3E5FC", "#2196F3", "#03A9F4")
new IsoGenerator().StairLeft(70, 0, "#B3E5FC", "#2196F3", "#03A9F4")
new IsoGenerator().StairRight(70 * 2, 0, "#B3E5FC", "#2196F3", "#03A9F4")

// What I'm trying to reproduce: http://i.imgur.com/YF4xyz9.png
&lt;canvas width="1000" height="1000"&gt;&lt;/canvas&gt;

小提琴:https://jsfiddle.net/xvak0jh1/2/

【问题讨论】:

  • 首先,整洁!我的第一个想法是把它想象成一堆堆叠的第一个平台,每个平台都变小了。我现在正在根据您的代码寻找答案。
  • 如果在我今晚有时间继续工作之前你没有得到答案,这就是我要去的方向。 new IsoGenerator().Stack(0, 0, 4, "#B3E5FC", "#2196F3", "#03A9F4") 然后 Tile 需要多取 x, y。 stack的z index是Tiles的个数。
  • @mkaatman 谢谢!为什么需要 z 索引?只需使用像for(var i = 4; i &gt;= 0; i--) 这样的反向循环,然后最后会绘制最小的图块/平台
  • @mkaatman 我做了一些实验,如果这可以帮助你jsfiddle.net/xvak0jh1/1
  • 我想我在想你会从顶部开始,你可以随心所欲地堆高。如果您只想要 4,那么您可能可以对其进行硬编码。

标签: javascript html canvas isometric axonometric


【解决方案1】:

轴测图

处理轴测(通常称为等轴测)渲染的最佳方法是在 3D 中对对象进行建模,然后在您想要的特定轴测投影中渲染模型。

作为网格的 3D 对象

最简单的对象(在这种情况下)是一个盒子。盒子有 6 个边和 8 个顶点,可以通过它的顶点和将边表示为顶点索引的多边形来描述。

例如 3D 盒子,x 从左到右,y 从上到下,z 向上。

首先创建组成盒子的顶点

UPDATE 按照 cmets 中的要求,我已将框更改为其 x、y、z 尺寸。

// 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 boxSizeX = 10;   // size of the box x axis
const boxSizeY = 50;   // size of the box y axis
const boxSizeZ = 8;   // size of the box z axis
const hx = boxSizeX / 2; // half size shorthand for easier typing
const hy = boxSizeY / 2; 
const hz = boxSizeZ / 2; 

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

然后为盒子上的每个面创建多边形

const colours = {
    dark : "#444",
    shade : "#666",
    light : "#aaa",
    bright : "#eee",
}
function createPoly(indexes,colour){ return { indexes, colour} }
const polygons = [];
// always make the polygon vertices indexes in a clockwise direction
// when looking at the polygon from the outside of the object
polygons.push(createPoly([3,2,1,0],colours.dark)); // bottom face
polygons.push(createPoly([0,1,5,4],colours.dark)); // back face
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([3,0,4,7],colours.dark)); // left face
polygons.push(createPoly([4,5,6,7],colours.bright)); // top face

现在您有了一个带有 6 个多边形的盒子的 3D 模型。

投影

投影描述了如何将 3D 对象转换为 2D 投影。这是通过为每个 3D 坐标提供一个 2D 轴来完成的。

在这种情况下,您使用的是双特征投影的修改

因此,让我们为 3 个 3D 坐标中的每一个定义该 2D 轴。

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

  // an object to handle the projection
  const isoProjMat = {
      xAxis : P2(1 , 0.5) ,  // 3D x axis for every 1 pixel in x go down half a pixel in y
      yAxis :  P2(-1 , 0.5) , // 3D y axis for every -1 pixel in x go down half a pixel in y
      zAxis :  P2(0 , -1) , // 3D z axis go up 1 pixels
      origin : P2(100,100),  // where on the screen 3D coordinate (0,0,0) will be

现在通过将 x,y,z (3d) 坐标转换为 x,y (2d) 来定义进行投影的函数

      project (p, retP = P2()) {
          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;
          return retP;
      }
  }

渲染

现在您可以渲染模型了。首先,您必须将每个顶点投影到 2D 屏幕坐标中。

// create a new array of 2D projected verts
const projVerts = vertices.map(vert => isoProjMat.project(vert));

然后只需通过索引将每个多边形渲染到projVerts数组中

polygons.forEach(poly => {
    ctx.fillStyle = poly.colour;
    ctx.beginPath();
    poly.indexs.forEach(index => ctx.lineTo(projVerts[index].x, projVerts[index].y) );
    ctx.fill();
});

作为一个sn-p

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 boxSizeX = 10 * 4;   // size of the box x axis
const boxSizeY = 50 * 4;   // size of the box y axis
const boxSizeZ = 8 * 4;   // size of the box z axis
const hx = boxSizeX / 2; // half size shorthand for easier typing
const hy = boxSizeY / 2; 
const hz = boxSizeZ / 2; 

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



const colours = {
  dark: "#444",
  shade: "#666",
  light: "#aaa",
  bright: "#eee",
}

function createPoly(indexes, colour) {
  return {
    indexes,
    colour
  }
}
const polygons = [];
// always make the polygon vertices indexes in a clockwise direction
// when looking at the polygon from the outside of the object
polygons.push(createPoly([3, 2, 1, 0], colours.dark)); // bottom face
polygons.push(createPoly([0, 1, 5, 4], colours.dark)); // back face
polygons.push(createPoly([3, 0, 4, 7], colours.dark)); // left face
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});

// an object to handle the projection
const isoProjMat = {
  xAxis: P2(1, 0.5), // 3D x axis for every 1 pixel in x go down half a pixel in y
  yAxis: P2(-1, 0.5), // 3D y axis for every -1 pixel in x go down half a pixel in y
  zAxis: P2(0, -1), // 3D z axis go up 1 pixels
  origin: P2(150, 75), // where on the screen 3D coordinate (0,0,0) will be
  project(p, retP = P2()) {
    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;
    return retP;
  }
}

// create a new array of 2D projected verts
const projVerts = vertices.map(vert => isoProjMat.project(vert));
// and render
polygons.forEach(poly => {
  ctx.fillStyle = poly.colour;
  ctx.beginPath();
  poly.indexes.forEach(index => ctx.lineTo(projVerts[index].x, projVerts[index].y));
  ctx.fill();
});
canvas {
  border: 2px solid black;
}
&lt;canvas id="canvas"&gt;&lt;/canvas&gt;

更多

这是基础,但绝不是全部。我通过确保多边形的顺序在与观察者的距离方面是正确的而作弊。确保不绘制更近的多边形。对于更复杂的形状,您需要添加深度排序。您还希望通过不绘制远离观察者的面(多边形)来优化渲染。这称为背面剔除。

您还需要添加照明模型等等。

像素双度量投影。

上面的内容其实不是你想要的。在游戏中,您使用的投影通常被称为像素艺术投影,它不适合漂亮的数学投影。有很多关于抗锯齿的规则,其中顶点的渲染取决于面的方向。

例如,顶点在像素顶部、左侧或顶部、右侧或底部、右侧或底部、左侧绘制,具体取决于面部方向,并在奇数和偶数 x 坐标之间交替,仅举几例规则

这支笔Axonometric Text Render (AKA Isometric) 是一个稍微复杂的轴测渲染示例,它具有 8 种常见轴测投影选项,并包括简单的深度排序,但不是为速度而构建的。 This answer 是我写这支笔的灵感来源。

你的形状。

毕竟,下一个 sn-p 通过将基本框移动到每个位置并按从后到前的顺序渲染它来绘制您想要的形状。

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: "#004",
  shade: "#036",
  light: "#0ad",
  bright: "#0ee",
}

function createPoly(indexes, colour) {
  return {
    indexes,
    colour
  }
}
const polygons = [];
// always make the polygon vertices indexes in a clockwise direction
// when looking at the polygon from the outside of the object
//polygons.push(createPoly([3, 2, 1, 0], colours.dark)); // bottom face
//polygons.push(createPoly([0, 1, 5, 4], colours.dark)); // back face
//polygons.push(createPoly([3, 0, 4, 7], colours.dark)); // left face
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});

// an object to handle the projection
const isoProjMat = {
  xAxis: P2(1, 0.5), // 3D x axis for every 1 pixel in x go down half a pixel in y
  yAxis: P2(-1, 0.5), // 3D y axis for every -1 pixel in x go down half a pixel in y
  zAxis: P2(0, -1), // 3D z axis go up 1 pixels
  origin: P2(150, 55), // where on the screen 3D coordinate (0,0,0) will be
  project(p, retP = P2()) {
    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;
    return retP;
  }
}
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 => isoProjMat.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;
}
&lt;canvas id="canvas"&gt;&lt;/canvas&gt;

【讨论】:

  • 干得真好。这是来自其他地方还是您为这个问题创建的?
  • @mkaatman 我最近回答了一个关于等距深度排序的问题,这是对代码的重写以适应这个问题。
  • 太棒了!我创建了一个新的“轴测图”标签。您认为以这种方式标记这些问题是否公平? (或者等距是否足够好?)另外,你有什么想法可以实现他通过堆叠矩形创建的像素化效果?
  • @mkaatman Axonometric 一点也不为人所知,所以我认为它不会对问题产生太大影响。对于像素艺术版本,画布太慢了,最好通过 webGL 中的着色器来完成。我已经授权了代码,不能公开发布
  • @Blindman67 我是 OP,因为我无法再访问我的帐户“JeePing”,因为我的 chrome 配置文件有问题,所以我丢失了密码(即使是分配给该帐户的电子邮件) )。我已发送支持请求以验证您的回答!不管怎样,你做得非常好!我已经从你的代码中创建了一个类 - 如果它可以帮助任何人,请在此处查看 jsfiddle.net/6hwggtc2/2。只是一个问题,我真的没有看到一种将立方体类转换为长方体的方法,我可以在其中设置 x、y、z 尺寸而不是一个参数Size。你能提供一个小提琴之类的吗?
猜你喜欢
  • 2016-07-11
  • 1970-01-01
  • 1970-01-01
  • 2011-10-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多