我是 HTML5 Canvas 和 JavaScript 的新手,但是有没有一种简单的方法可以在 HTML5 Canvas 元素中进行等距投影?
我的意思是真正的等距投影 - http://en.wikipedia.org/wiki/Isometric_projection
谢谢大家的回复。
我是 HTML5 Canvas 和 JavaScript 的新手,但是有没有一种简单的方法可以在 HTML5 Canvas 元素中进行等距投影?
我的意思是真正的等距投影 - http://en.wikipedia.org/wiki/Isometric_projection
谢谢大家的回复。
首先,我建议将游戏世界想象成一个规则的 X 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 变换矩阵的地方。
这实际上并不难,在绘制对象时,您将需要它在世界坐标和视图坐标之间进行转换。
希望这可以帮助。
处理轴测(通常称为等轴测)渲染的最佳方法是通过投影矩阵。
如下的投影对象可以描述您进行任何形式的轴测投影所需的一切
该对象对 x、y 和 z 轴有 3 个变换,每个变换都描述了 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)
选择投影类型。
以下是 wiki轴测投影中概述的相关投影类型 以及像素艺术和游戏中常用的两个修改(以像素为前缀)。使用axoProjMat.setProjection(name)
where 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 问题中的 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>
我为我的等距应用程序创建了一些东西
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;
}
}