javascript 使用 HTML5 Canvas 进行真正的等距投影
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/6668834/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
True Isometric Projection with HTML5 Canvas
提问by gma
I am a newbie with HTML5 Canvas and JavaScript, but is there a simple way to have Isometric projection in HTML5 Canvas element?
我是 HTML5 Canvas 和 JavaScript 的新手,但是有没有一种简单的方法可以在 HTML5 Canvas 元素中进行等距投影?
I am mean the true isometric projection - http://en.wikipedia.org/wiki/Isometric_projection
我的意思是真正的等距投影 - http://en.wikipedia.org/wiki/Isometric_projection
Thanks all for reply's.
谢谢大家的回复。
回答by alekop
First, I would recommend thinking of the game world as a regular X by Y grid of square tiles. This makes everything from collision detection, pathfinding, and even rendering much easier.
首先,我建议将游戏世界视为一个由 X 乘 Y 方格组成的常规网格。这使得从碰撞检测、寻路甚至渲染的一切变得更加容易。
To render the map in an isometric projection simply modify the projection matrix:
要在等距投影中渲染地图,只需修改投影矩阵:
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... :)
}
This is exciting the first time you get it work, but you'll quickly realize that it's not that useful for drawing actual isometric maps... you can't just rotate your tile images and see what's around the corner. The transformations are not so much for drawing, as they are for converting between screen space and world space.
当您第一次使用它时,这是令人兴奋的,但您很快就会意识到它对于绘制实际的等轴测图并没有多大用处……您不能只是旋转平铺图像并查看拐角处有什么。变换与其说是为了绘图,不如说是为了在屏幕空间和世界空间之间进行转换。
Bonus: figuring out which tile the mouse is over
奖励:找出鼠标在哪个瓷砖上
What you want to do is convert from "view coordinates" (pixel offsets from the canvas origin) to "world coordinates" (pixel offsets from tile 0,0 along the diagonal axes). Then simply divide the world coordinates by the tile width and height to get the "map coordinates".
您想要做的是从“视图坐标”(从画布原点的像素偏移)转换为“世界坐标”(沿对角轴从 tile 0,0 的像素偏移)。然后只需将世界坐标除以瓦片宽度和高度即可获得“地图坐标”。
In theory, all you need to do is project the "view position" vector by the inverseof the projection matrix above to get the "world position". I say in theory, because for some reason the canvas doesn't provide a way of returning the current projection matrix. There is a setTransform()
method, but no getTransform()
, so this is where you'd have to roll your own 3x3 transformation matrix.
理论上,您需要做的就是通过上面的投影矩阵的逆投影“视图位置”向量以获得“世界位置”。我说的是理论上,因为出于某种原因,画布没有提供返回当前投影矩阵的方法。有一种setTransform()
方法,但没有getTransform()
,所以这是您必须滚动自己的 3x3 变换矩阵的地方。
It's not actually that hard, and you will need this for converting between world and view coordinates when drawing objects.
这实际上并不难,在绘制对象时,您将需要它来在世界坐标和视图坐标之间进行转换。
Hope this helps.
希望这可以帮助。
回答by Blindman67
Axonometric rendering
轴测图
The best way to handle axonometric (commonly called isometric) rendering is via a projection matrix.
处理轴测图(通常称为等轴测图)渲染的最佳方法是通过投影矩阵。
A projection object as follows can describe all you need to do any form of axonometric projection
下面的投影对象可以描述你需要做的任何形式的轴测投影
The object has 3 transforms for the x,y and z axis with each describing the scale and direction in the 2D projection for the x,y,z coordinates. A transform for the depth calculation and a origin that is in canvas pixels (if setTransform(1,0,0,1,0,0) or whatever the current transform for the canvas is)
该对象有 3 个针对 x、y 和 z 轴的变换,每个变换都描述了 x、y、z 坐标的 2D 投影中的比例和方向。深度计算的转换和以画布像素为单位的原点(如果 setTransform(1,0,0,1,0,0) 或画布的当前转换是什么)
To project a point call the function axoProjMat({x=10,y=10,z=10})
and it will return a 3D point with x,y being 2D coordinates of the vertex and z being the depth (with depth values positive approaching the view (opposite to 3D perspective projection));
要投影一个点,调用该函数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;
}
}
With the above object you can use the function axoProjMat.setProjection(name)
to select the projection type.
通过上述对象,您可以使用该功能axoProjMat.setProjection(name)
来选择投影类型。
Below is the associated projection types as outlined on the wiki Axonometric projectionsplus two modifications commonly used in pixel art and games (prefixed with Pixel). Use axoProjMat.setProjection(name)
where name is one of the projTypes
property names.
以下是 wiki轴测投影中概述的相关投影类型 以及像素艺术和游戏中常用的两种修改(以 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) ,
}
}
Example of True Isometric Projection.
真实等距投影的示例。
The snippet is an simple example with the projection set to Isometric
as detailed on the wiki link in the OP's question and using the above functions and objects.
该片段是一个简单的示例,其中投影设置Isometric
为 OP 问题中的 wiki 链接上的详细说明,并使用上述函数和对象。
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>