鼠标位置到等距图块,包括高度

Posted

技术标签:

【中文标题】鼠标位置到等距图块,包括高度【英文标题】:mouse position to isometric tile including height 【发布时间】:2014-03-17 13:25:59 【问题描述】:

努力将鼠标的位置转换为我的网格中瓷砖的位置。当一切都平坦时,数学看起来像这样:

this.position.x = Math.floor(((pos.y - 240) / 24) + ((pos.x - 320) / 48));
this.position.y = Math.floor(((pos.y - 240) / 24) - ((pos.x - 320) / 48));

其中pos.xpos.y 是鼠标的位置,240 和320 是偏移量,24 和48 是图块的大小。位置然后包含我悬停的瓷砖的网格坐标。这在平坦的表面上工作得相当好。

现在我正在添加高度,数学没有考虑到这一点。

这个网格是一个包含噪声的 2D 网格,它被转换为高度和图块类型。高度实际上只是对瓷砖“Y”位置的调整,因此可以在同一个位置绘制两个瓷砖。

我不知道如何确定我悬停在哪个图块上。

编辑:

取得了一些进展... 之前,我依靠鼠标悬停事件来计算网格位置。我只是将其更改为在绘制循环本身中进行计算,并检查坐标是否在当前绘制的图块的范围内。虽然会产生一些开销,但不确定我是否对此非常满意,但我会确认它是否有效。

编辑 2018:

我没有答案,但既然这有[sd] 一个开放的赏金,请帮助自己一些code and a demo

网格本身是简化的;

let grid = [[10,15],[12,23]];

导致如下图:

for (var i = 0; i < grid.length; i++) 
    for (var j = 0; j < grid[0].length; j++) 
        let x = (j - i) * resourceWidth;
        let y = ((i + j) * resourceHeight) + (grid[i][j] * -resourceHeight); 
        // the "+" bit is the adjustment for height according to perlin noise values
    

编辑赏金后:

参见 GIF。接受的答案有效。延迟是我的错,鼠标移动时屏幕没有更新(还),帧速率很低。它显然带回了正确的瓷砖。

Source

【问题讨论】:

我很好奇你最终是如何解决这个问题的。我自己的想法是从最高层开始(地图可以存储使用的最高层),然后检查单击的位置是否存在图块,然后沿着层向下工作,直到到达该位置的现有图块点击。 嗨@NeilRoy,我实际上还没有:/问题仍然存在。我真的没有这样的层,它只是一个包含高度值的网格 IE; [ [ 0.01, 0.0015, 0.02...] [ ... ] ]。如果你有兴趣,我在 github 上有源代码。 它仍然可以工作。您将计算最大高度处的正常瓷砖位置并查看该高度是否存在瓷砖,如果不存在,则降低高度,根据该高度的偏移重新计算并再次检查瓷砖。重复直到你在那个位置和高度有一个瓷砖。我想到的另一种方法是在地图上“走”你的路,检查你点击的位置以南的基础图块,看看它的高度是否与你的鼠标位置匹配(你的鼠标以南的图块,可能位于你的鼠标给定高度),然后向北移动一个图块,再次检查 在这种情况下降低高度或多或少是无限的。 0 到 1 之间的任何值都是有效值。我试着让所有接触的瓷砖,并检查哪一个更“正面”。我在更新循环中做到了这一点,而且速度非常慢。我会考虑地图步行评论。 我看到其他人使用的另一个想法是他们使用了高度图。这会占用更多内存,因为您需要一个与您的关卡大小相同的单独地图。他们使用灰度位图作为高度图,阴影越深,该部分越高。所以你点击了地图,然后他们检查了高度图并将其上的每个灰色阴影映射到一个高度。我对这个解决方案并不疯狂,因为它似乎是对内存的可怕浪费。但是现在,大多数人都有足够的记忆力,所以我想这取决于你的目标受众。 【参考方案1】:

有趣的任务。

让我们尝试简化它 - 让我们解决这个具体案例

解决方案

工作版本在这里:https://github.com/amuzalevskiy/perlin-landscape(更改为https://github.com/jorgt/perlin-landscape/pull/1)

说明

首先想到的是:

只需两步:

找到与某些图块匹配的垂直列 从下到上迭代集合中的图块,检查光标是否低于顶线

第一步

这里需要两个函数:

检测列:

function getColumn(mouseX, firstTileXShiftAtScreen, columnWidth) 
  return (mouseX - firstTileXShiftAtScreen) / columnWidth;

提取与该列对应的图块数组的函数。

记住将图像旋转 45 度。红色数字是 columnNo。 3 列突出显示。 X轴是水平的

function tileExists(x, y, width, height) 
  return x >= 0 & y >= 0 & x < width & y < height; 


function getTilesInColumn(columnNo, width, height) 
  let startTileX = 0, startTileY = 0;
  let xShift = true;
  for (let i = 0; i < columnNo; i++) 
    if (tileExists(startTileX + 1, startTileY, width, height)) 
      startTileX++;
     else 
      if (xShift) 
        xShift = false;
       else 
        startTileY++;
      
    
  
  let tilesInColumn = [];
  while(tileExists(startTileX, startTileY, width, height)) 
    tilesInColumn.push(x: startTileX, y: startTileY, isLeft: xShift);
    if (xShift) 
      startTileX--;
     else 
      startTileY++;
    
    xShift = !xShift;
  
  return tilesInColumn;

第 2 步

要检查的图块列表已准备就绪。现在,对于每个图块,我们需要找到一条顶线。我们也有两种类型的瓷砖:左和右。我们已经在构建匹配图块集时存储了这些信息。

function getTileYIncrementByTileZ(tileZ) 
    // implement here
    return 0;


function findExactTile(mouseX, mouseY, tilesInColumn, tiles2d,
                       firstTileXShiftAtScreen, firstTileYShiftAtScreenAt0Height,
                       tileWidth, tileHeight) 
    // we built a set of tiles where bottom ones come first
    // iterate tiles from bottom to top
    for(var i = 0; i < tilesInColumn; i++) 
        let tileInfo = tilesInColumn[i];
        let lineAB = findABForTopLineOfTile(tileInfo.x, tileInfo.y, tiles2d[tileInfo.x][tileInfo.y], 
                                            tileInfo.isLeft, tileWidth, tileHeight);
        if ((mouseY - firstTileYShiftAtScreenAt0Height) >
            (mouseX - firstTileXShiftAtScreen)*lineAB.a + lineAB.b) 
            // WOHOO !!!
            return tileInfo;
        
    


function findABForTopLineOfTile(tileX, tileY, tileZ, isLeftTopLine, tileWidth, tileHeight) 
    // find a top line ~~~ a,b
    // y = a * x + b;
    let a = tileWidth / tileHeight; 
    if (isLeftTopLine) 
      a = -a;
    
    let b = isLeftTopLine ? 
       tileY * 2 * tileHeight :
       - (tileX + 1) * 2 * tileHeight;
    b -= getTileYIncrementByTileZ(tileZ);
    return a: a, b: b;

【讨论】:

var topCornerPositionX = 4 /*wtf*/ + ...。我也不知道那是从哪里来的 :') 是的,我喜欢它。 这种方法的额外好处是 - 至少乍一看 - 对性能的影响非常小。 @Jorg 因为“缩放”,花了将近一半的时间来调试代码......所以提交的代码不是 100% 相同...... @Jorg Complexity 是min(n,m),其中nm 是网格的维度。如果有高度限制,复杂性也可能会降低 @NeilRoy,它正在按照您的描述工作,从最近的图块开始搜索。谢谢!【参考方案2】:

请不要评判我,因为我没有发布任何代码。我只是建议一种算法,可以在不占用大量内存的情况下解决它。

算法:

实际上,要确定鼠标悬停在哪个图块上,我们不需要检查所有图块。起初,我们认为表面是 2D 的,并使用 OP 发布的公式找到鼠标指针越过哪个图块。这是鼠标光标可以指向此光标位置的最可能的图块

这个图块在0高度时可以接收鼠标指针,通过检查它的当前高度我们可以验证它是否真的在接收指针的高度,我们标记它并向前移动。

然后我们通过根据光标位置增加或减少 x,y 网格值来找到下一个可能更靠近屏幕的图块。

然后我们继续以曲折的方式前进,直到我们到达一个即使它处于最大高度也无法接收指针的图块。

当我们到达这一点时,找到的最后一个图块处于接收指针的高度,这就是我们正在寻找的图块。

在这种情况下,我们只检查了 8 个图块来确定哪个图块当前正在接收指针。与检查网格中存在的所有图块相比,这非常节省内存并产生更快的结果。

【讨论】:

这不是假设瓷砖的高度不会有太大的变化吗?如果其中一块瓷砖比其他瓷砖高 20 倍怎么办? 如果我错了,请原谅我,但相邻的瓷砖会有很大差异吗?看起来差别不大。该算法实际上并没有假设很少或很大的差异,如果公式正确,它将适用于两者,稍后我将使用工作代码更新我的答案。 (Please help me on this) -> 这正是我的代码正在做的事情:) 这可以工作,除非您沿着地图向上而不是向下走。这就像当您向上移动瓷砖时将光线追踪到地图中,您测试哪个瓷砖顶部与您的位置相匹配,这样无论瓷砖高度是多少,或者它们有多大变化,您都会碰到可见的瓷砖首先(从前到后之类的东西)。【参考方案3】:

解决此问题的一种方法是跟踪从屏幕上点击的像素进入地图的光线。为此,只需确定相机相对于地图的位置以及它所查看的方向:

 const camPos = x: -5, y: -5, z: -5
 const camDirection =  x: 1, y:1, z:1

下一步是在 3D 世界中获取触摸位置。从某种角度来看,这很简单:

 const touchPos = 
   x: camPos.x + touch.x / Math.sqrt(2),
   y: camPos.y - touch.x / Math.sqrt(2),
   z: camPos.z - touch.y / Math.sqrt(2)
 ;

现在您只需要跟随光线进入图层(缩放方向,使其小于您的瓷砖尺寸之一):

 for(let delta = 0; delta < 100; delta++)
   const x = touchPos.x + camDirection.x * delta;
   const y = touchPos.y + camDirection.y * delta;
   const z = touchPos.z + camDirection.z * delta;

现在只需在xz 处取瓷砖并检查y 是否小于其高度;

 const absX = ~~( x / 24 );
 const absZ = ~~( z / 24 );

   if(tiles[absX][absZ].height >= y)
    // hanfle the over event
   

【讨论】:

我看到你添加了一个z,我正在努力看看它是如何工作的,因为我不确定我的图层在这里是什么,但我会试一试 @jorg 这基本上是我编写的类似 java 代码的转译版本。所以我真的没有时间将它应用到等轴测视角。如果您遇到困难请联系我,以便我可以扩展答案...【参考方案4】:

我在游戏中遇到了同样的情况。一开始我尝试了数学,但是当我发现客户每天都想改变地图类型时,我用一些图形解决方案改变了解决方案并将其传递给团队的设计师。我通过监听 SVG 元素的点击来捕获鼠标位置。

直接用于捕获鼠标位置并将鼠标位置转换为我所需像素的主图形。

https://blog.lavrton.com/hit-region-detection-for-html5-canvas-and-how-to-listen-to-click-events-on-canvas-shapes-815034d7e9f8 https://code.sololearn.com/Wq2bwzSxSnjl/#html

【讨论】:

【参考方案5】:

这是我为了讨论而定义的网格输入。根据鼠标用户屏幕上的可见性,输出应该是某个图块(coordinate_1,coordinate_2):

我可以从不同的角度提供两种解决方案,但您需要将其转换回您的问题域。第一种方法基于着色图块,如果地图动态变化,则可能更有用。第二种解决方案基于绘制坐标边界框,基于这样一个事实,即 (0, 0) 等靠近观察者的图块永远不会被其后面的图块 (1,1) 遮挡。

方法 1:透明色块

第一种方法是基于绘图,并在here上进行了阐述。我必须感谢@haldagan 提供了一个特别漂亮的解决方案。总之,它依赖于在原始画布上绘制一个完全不透明的图层,并用不同的颜色为每个图块着色。该顶层应与底层进行相同的高度变换。当鼠标悬停在特定图层上时,您可以通过画布检测颜色,从而检测磁贴本身。这是我可能会采用的解决方案,这在计算机可视化和图形(在 3d 等距世界中查找位置)中似乎并不罕见。

方法 2:查找边界块

这是基于“前”行永远不会被其后面的“后”行遮挡的猜想。此外,“离屏幕较近”的图块不能被“离屏幕较远”的图块遮挡。要准确理解“前”、“后”、“靠近屏幕”和“远离屏幕”的含义,请看以下内容:

.

基于此原理,该方法是为每个图块构建一组多边形。因此,首先我们确定高度缩放后框 (0, 0) 在画布上的坐标。请注意,高度缩放操作只是一个基于高度垂直拉伸的梯形。

然后我们确定高度缩放后框 (1, 0), (0, 1), (1, 1) 在画布上的坐标(我们需要从与多边形重叠的那些多边形中减去任何东西 (0 , 0))。

继续通过从靠近屏幕的多边形中减去任何遮挡来构建每个框的边界坐标,最终得到所有框的多边形坐标。

借助这些坐标并稍加注意,您最终可以通过向上搜索底部行来确定通过重叠多边形的二分搜索样式指向哪个图块。

【讨论】:

我喜欢“第二幅画布”解决方案的简单性,但它不能很好地扩展。不过,这对我的目的来说可能已经足够了。让我们先看看能不能找到更数学的方法 另一种方法可能依赖于光线追踪,但我无法正式确定这种投影的工作原理以及网格中需要哪些信息。 后排绘图加1。 @Jorg,是的,正如我在旧答案中提到的那样,这不适用于具有大量移动物体的复杂情况,但是当你有很多东西时,这种方法可以帮助节省一些神经场景中不规则形状的物体。它基本上是没有数学的光线追踪。【参考方案6】:

屏幕上的其他内容也很重要。如果您的瓷砖非常均匀,则数学尝试会起作用。但是,如果您要显示各种对象并希望用户选择它们,那么使用画布大小的标识符地图要容易得多。

function poly(ctx)var a=arguments;ctx.beginPath();ctx.moveTo(a[1],a[2]);
    for(var i=3;i<a.length;i+=2)ctx.lineTo(a[i],a[i+1]);ctx.closePath();ctx.fill();ctx.stroke();
function circle(ctx,x,y,r)ctx.beginPath();ctx.arc(x,y,r,0,2*Math.PI);ctx.fill();ctx.stroke();
function Tile(h,c,f)
    var cnv=document.createElement("canvas");cnv.width=100;cnv.height=h;
    var ctx=cnv.getContext("2d");ctx.lineWidth=3;ctx.lineStyle="black";
    ctx.fillStyle=c;poly(ctx,2,h-50,50,h-75,98,h-50,50,h-25);
    poly(ctx,50,h-25,2,h-50,2,h-25,50,h-2);
    poly(ctx,50,h-25,98,h-50,98,h-25,50,h-2);
    f(ctx);return ctx.getImageData(0,0,100,h);

function put(x,y,tile,image,id,map)
    var iw=image.width,tw=tile.width,th=tile.height,bdat=image.data,fdat=tile.data;
    for(var i=0;i<tw;i++)
        for(var j=0;j<th;j++)
            var ijtw4=(i+j*tw)*4,a=fdat[ijtw4+3];
            if(a!==0)
                var xiyjiw=x+i+(y+j)*iw;
                for(var k=0;k<3;k++)bdat[xiyjiw*4+k]=(bdat[xiyjiw*4+k]*(255-a)+fdat[ijtw4+k]*a)/255;
                bdat[xiyjiw*4+3]=255;
                map[xiyjiw]=id;
            
        

var cleanimage;
var pickmap;
function startup()
    var water=Tile(77,"blue",function());
    var field=Tile(77,"lime",function());
    var tree=Tile(200,"lime",function(ctx)
        ctx.fillStyle="brown";poly(ctx,50,50,70,150,30,150);
        ctx.fillStyle="forestgreen";circle(ctx,60,40,30);circle(ctx,68,70,30);circle(ctx,32,60,30);
    );
    var sheep=Tile(200,"lime",function(ctx)
        ctx.fillStyle="white";poly(ctx,25,155,25,100);poly(ctx,75,155,75,100);
        circle(ctx,50,100,45);circle(ctx,50,80,30);
        poly(ctx,40,70,35,80);poly(ctx,60,70,65,80);
    );
    var cnv=document.getElementById("scape");
    cnv.width=500;cnv.height=400;
    var ctx=cnv.getContext("2d");
    cleanimage=ctx.getImageData(0,0,500,400);
    pickmap=new Uint8Array(500*400);
    var tiles=[water,field,tree,sheep];
    var map=[[[0,0],[1,1],[1,1],[1,1],[1,1]],
             [[0,0],[1,1],[1,2],[3,2],[1,1]],
             [[0,0],[1,1],[2,2],[3,2],[1,1]],
             [[0,0],[1,1],[1,1],[1,1],[1,1]],
             [[0,0],[0,0],[0,0],[0,0],[0,0]]];
    for(var x=0;x<5;x++)
        for(var y=0;y<5;y++)
            var desc=map[y][x],tile=tiles[desc[0]];
            put(200+x*50-y*50,200+x*25+y*25-tile.height-desc[1]*20,
            tile,cleanimage,x+1+(y+1)*10,pickmap);
        
    ctx.putImageData(cleanimage,0,0);

var mx,my,pick;
function mmove(event)
    mx=Math.round(event.offsetX);
    my=Math.round(event.offsetY);
    if(mx>=0 && my>=0 && mx<cleanimage.width && my<cleanimage.height && pick!==pickmap[mx+my*cleanimage.width])
        requestAnimationFrame(redraw);

function redraw()
    pick=pickmap[mx+my*cleanimage.width];
    document.getElementById("pick").innerHTML=pick;
    var ctx=document.getElementById("scape").getContext("2d");
    ctx.putImageData(cleanimage,0,0);
    if(pick!==0)
        var temp=ctx.getImageData(0,0,cleanimage.width,cleanimage.height);
        for(var i=0;i<pickmap.length;i++)
            if(pickmap[i]===pick)
                temp.data[i*4]=255;
        ctx.putImageData(temp,0,0);
    

startup(); // in place of body.onload
<div id="pick">Move around</div>
<canvas id="scape" onmousemove="mmove(event)"></canvas>

这里的“id”是一个简单的x+1+(y+1)*10(因此显示时很好)并适合一个字节(Uint8Array),它已经可以达到15x15的显示网格,并且还有更广泛的类型可用。

(试着把它画得很小,在 sn-p 编辑器屏幕上看起来还可以,但这里显然还是太大了)

【讨论】:

这有点难以阅读,但是您是否在此处保留了屏幕上对象的每像素图? @Jorg 是的,这就是这里发生的事情。 function put() 几乎可以做所有事情:因为 CanvasRenderingContext2D.putImageData() 不能正确处理“alpha”,所以无论如何都必须创建一个替代方案,然后它也是更新 id-map 的理想场所,无论何时渲染一个不透明的像素当前对象。 虽然我很欣赏您的解决方案的简单性 - 而且我认为它有优点 - 我不确定它是否完全适合像这样的场景,它只是将一个图像的一部分绘制到另一个图像上。虽然希望被证明是错误的 你的涂鸦很有趣,我很喜欢它可以识别形状 @Jorg 我只能重复描述:如果你有统一的瓷砖,只是颜色/纹理不同,更数学的方法效果很好。如果您需要形状,则不需要。老实说,我发现问题中的风景有点空洞,这就是我提出这个方面的原因。确切需要什么也很重要,在我的示例中,用户根本无法选择 23,选择 12 和 13 也不是很方便。【参考方案7】:

计算机图形很有趣,对吧?

这是更标准的计算几何“point location problem”的特例。您也可以将其表示为nearest neighbour search。

要使这看起来像一个点定位问题,您只需要将您的图块表示为 2D 平面中的非重叠多边形。如果您想将形状保持在 3D 空间中(例如,使用 z 缓冲区),这将成为相关的“光线投射问题”。

良好几何算法的一个来源是W. Randolf Franklin's website,turf.js 包含一个implementation of his PNPOLY algorithm。

对于这种特殊情况,通过将我们关于图块形状的先验知识视为粗略的R-tree(一种空间索引),我们甚至可以比一般算法更快。

【讨论】:

以上是关于鼠标位置到等距图块,包括高度的主要内容,如果未能解决你的问题,请参考以下文章

应用数学计算锯齿状等距正方形的面积

用js实现页面图片,以鼠标位置为中心,滚轮缩放图片

有没有办法告诉 Chrome 网络调试器在页面坐标中显示当前鼠标位置?

JS——事件详情(鼠标事件)

100个 Unity实用技能| 游戏中获取鼠标点击的坐标,并将游戏对象移动到鼠标的点击位置

在鼠标位置(鼠标左上角)显示 WPF 窗口的最佳方式是啥?