使用2D渲染上下文的HTML5 Canvas转换问题

8
我正在尝试使用2D渲染上下文在HTML5 Canvas中制作一种相机。如下图所示,这是我想要实现的目标:
  1. 假设黑色方块是相机的眼睛,我希望它能够随着画布(如图片中的绿色箭头)移动,并且看起来像是环绕物体旅行,就像红色方块一样(我认为这是视差效果)。
  2. 每当我环绕物体旋转相机时,我希望它围绕相机中心旋转(见蓝色旋转)。
我已经完成了让红色方块能够随着相机移动并围绕相机中心旋转的部分。[编辑] 下面是一个简化的示例:
*Within the requestAnimationFrame (game loop)*

...

ctx.canvas.width = window.innerWidth;
ctx.canvas.height = window.innerHeight;
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);

ctx.save();
    // draw camera eye.
    ctx.lineWidth = 3;
    ctx.fillStyle = "black";
    ctx.beginPath();
    ctx.moveTo(ctx.canvas.width / 2 - 50, ctx.canvas.height / 2);
    ctx.lineTo(ctx.canvas.width / 2 + 50, ctx.canvas.height / 2);
    ctx.moveTo(ctx.canvas.width / 2, ctx.canvas.height / 2 - 50);
    ctx.lineTo(ctx.canvas.width / 2, ctx.canvas.height / 2 + 50);
    ctx.stroke();

    // Rotate by camera's center.
    ctx.translate(ctx.canvas.width / 2, ctx.canvas.height / 2);
    ctx.rotate(worldRotate);
    ctx.translate(worldPos.x, worldPos.y);

    //
    // ADD WORLD ENTITIES BELOW to be viewed by the camera.
    //

    ctx.fillStyle = "red";
    ctx.fillRect(-5, -5, 10, 10);
ctx.restore();

...

(编辑:更改了转换对象为ctx以进一步简化代码。)
可通过输入更改变量worldRotate和worldPos进行测试。
编辑:这是一个真实的示例,其中绘制了问题,使我更加清楚地了解到我要实现的内容
尝试将其向右或向左移动,看看会发生什么。然后尝试向右旋转相机,使红色框与x轴对齐,然后移动相机,看它如何完美地像我想要的相机一样工作。
(注:建议使用IE10、最新的Chrome、最新的Firefox、最新的Opera和桌面Safari 5+进行测试)。 问题是,当我移动相机(通过更改worldPos,使红色框远离相机),进行旋转(使用worldRotate),然后再次移动相机时,红色物体始终朝着紫色箭头而不是橙色箭头移动。我希望红色框无论如何旋转相机的中心,都能朝向橙色箭头移动。 编辑:据我所知,很明显旋转会导致翻译出错,因为旋转改变了红色框的坐标系,但我仍然不知道如何处理它,至少让翻译像橙色箭头一样进行,而不管它当前旋转后的坐标系。
有关此问题的解决方案吗?谢谢。

一些代码会更有启发性,但如果没有的话,听起来你需要在进行变换(移动、旋转等)之前使用context.save(),然后在变换完成后使用context.restore()。 - markE
抱歉,markE先生,我已根据您的要求更新了代码以进行考虑。是的,它与那些东西有关。 - user905864
3个回答

4

[在OP进一步澄清后进行了编辑]

好的,我现在对你的问题有更好的理解了。您想要让相机镜头对您世界中的物体具有透视效果。

如果您的相机只在与世界物体相关的X方向上查看,则可以简单地使用视差。 您可以通过在画布上应用两个以不同速度在X方向上滚动的图层来实现这一点。 这就是全景视差。 因此,对于每一帧,绘制2个类似于以下伪代码的图层:

// Move the back layer
backLayerX += 3;
ctx.save();
ctx.translate(backLayerX,backLayerY);
// draw the objects in your back layer now
ctx.translate(-backLayerX,-backLayerY);
ctx.restore();

// Move the front layer faster than the back layer
frontLayerX += 10;
ctx.save();
ctx.translate(frontLayerX,frontLayerY);
// draw the objects in your front layer now
ctx.translate(frontLayerX,frontLayerY);
ctx.restore();

如果您在X方向上移动相机本身,则还必须更改前后图层平移的速度。
如果您的相机还朝Z方向(相机更近)看世界对象,则该过程要复杂得多。
例如,如果您靠近一座雕像,则雕像似乎变大——ctx.scale(2,2)。背景也会变大,但不会像雕像那样快——ctx.scale(1.25,1.25)。
然后,如果您向左走一步,则雕像的左侧略大于雕像的右侧——上下文倾斜。背景也会倾斜,但是非常轻微。
然后,如果您站在椅子上(改变Y方向),则会出现不同类型的倾斜。
您的相机永远无法与雕像保持一致,因为您的雕像是2D的——只是雕像的纸板剪影。从相机的角度来看,雕像实际上会消失!
希望我给您的项目提供了一个开端,但是完整的答案远远超出了Stackoverflow答案的范围——这是关于计算机图形学的大学课程。
我可以为您推荐一份在线教程。Wolfgang Hurst有一堂关于透视的优秀讲座,这是他13部分在线大学课程中的第七讲。您可以以两种方式查看他的讲座。广泛地查看,了解如何给您的相机带来透视概念。或者深入了解数学算法:http://www.youtube.com/watch?v=q_HA-x5AujI&list=PLDFA8FCF0017504DE 祝你好运和学习... :)
[以下内容回答了原问题]
这是一个关于如何在画布中成功旋转任何东西的快速教程。
整个世界都围绕着X/Y“注册点”旋转。
您可以通过在ctx.rotate(angle)之前执行ctx.translate(X,Y)来设置注册点。
在您在旋转空间中绘制“世界”对象之后,必须通过执行translate(-X,-Y)来重新设置注册点。
因此,世界旋转的过程如下:
  • Context.save();
  • Context.translate(registrationX, registrationY);
  • Context.rotate(angle);
  • 绘制内容。
  • Context.translate(-registrationX, -registrationY);
  • Context.restore();

以下是理解旋转和注册点的绝佳方法:

  • 取一张纸,在页面上画一个字母。
  • 取一支铅笔,放在页面上的任何一点。
  • --铅笔点是注册点。
  • --铅笔点是 translate(X,Y)。
  • 围绕铅笔点旋转纸张。
  • --纸张的旋转是 rotate(angle)。
  • --注意当纸张移动时字母的旋转。
  • --你的红色框将像字母一样旋转(围绕相机的注册点)。
  • 将纸张重置为直立的旋转状态。
  • --这种“取消旋转”就像 translate(-X,-Y)。
  • 将铅笔移到纸上的另一点
  • --同样,这就像 translate(newX,newY)。
  • 再次旋转纸张并注意字母以不同的模式旋转。

这里有代码和一个 Fiddle:http://jsfiddle.net/m1erickson/nj29x/

<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>

<style>
    body{ background-color: ivory; padding:30px; }
    canvas{border:1px solid red;}
</style>

<script>
$(function(){

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");

    var camX=100;
    var camY=100;
    var camRadius=40;
    var camDegrees=0;

    draw();

    function draw(){
    ctx.clearRect(0,0,canvas.width,canvas.height);
        drawWorldRect(75,75,30,20,0,"blue","A");
        drawWorldRect(100,100,30,20,15,"red","B");
        drawCamera();
    }

    function drawWorldRect(x,y,width,height,angleDegrees,fillstyle,letter){
        var cx=x+width/2;
        var cy=y+height/2;
        ctx.save();
        ctx.translate(cx, cy);
        ctx.rotate( (Math.PI / 180) * angleDegrees);
        ctx.fillStyle = fillstyle;
        ctx.fillRect(-width/2, -height/2, width, height);
        ctx.fillStyle="white";
        ctx.font = '18px Verdana';
        ctx.fillText(letter, -6, 7);
        ctx.restore();
    }


    function drawCamera(){
        ctx.save();
        ctx.lineWidth = 3;
        ctx.beginPath();
        // draw camera cross-hairs
        ctx.strokeStyle = "#dddddd";
        ctx.moveTo(camX,camY-camRadius);
        ctx.lineTo(camX,camY+camRadius);
        ctx.moveTo(camX-camRadius,camY);
        ctx.lineTo(camX+camRadius,camY);
        ctx.arc(camX, camY, camRadius, 0 , 2 * Math.PI, false);
        ctx.stroke();
        // draw camera site
        ctx.fillStyle = "#222222";
        ctx.beginPath();
        ctx.arc(camX, camY-camRadius, 3, 0 , 2 * Math.PI, false);
        ctx.fill()
        ctx.restore();
    }

    function rotate(){
        ctx.save();
        ctx.translate(camX,camY);
        ctx.rotate(Math.PI/180*camDegrees);
        ctx.translate(-camX,-camY);
        draw();
        ctx.restore();
    }

    $("#rotateCW").click(function(){ camDegrees+=30; rotate(); });
    $("#rotateCCW").click(function(){ camDegrees-=30; rotate(); });
    $("#camUp").click(function(){ camY-=20; draw(); });
    $("#camDown").click(function(){ camY+=20; draw(); });
    $("#camLeft").click(function(){ camX-=20; draw(); });
    $("#camRight").click(function(){ camX+=20; draw(); });

}); // end $(function(){});
</script>

</head>

<body>

    <canvas id="canvas" width=400 height=300></canvas><br/>
    <button id="camLeft">Camera Left</button>
    <button id="camRight">Camera Right</button>
    <button id="camUp">Camera Up.</button>
    <button id="camDown">Camera Down</button><br/>
    <button id="rotateCW">Rotate Clockwise</button>
    <button id="rotateCCW">Rotate CounterClockwise</button>

</body>
</html>

嗨,markE,感谢您的回答,我很欣赏您为此编写的代码。我相信您的答案更接近我想要实现的目标,但我认为我没有用代码表达得更清楚,所以我在这里制作了一个真实的示例片段,以便您可以了解摄像机的情况:https://dl.dropbox.com/u/53141171/samples/stackoverflow/index.html...(我还更新了问题并附上了链接)。尝试将其向右或向左移动,看看会发生什么。然后尝试将相机向右旋转,使红色框与x轴对齐,并查看它如何完美地像我想要的相机一样工作。 - user905864
嗨,markE,我已经重新表述并更新了我的问题中的第一点;相机应该随着画布移动,这意味着它是一种视差效果,世界实体(包含红色框)应该围绕移动,而枢轴点将是相机的中心。我想这就是让每个人感到困惑的原因,对此我感到抱歉。 - user905864
嗨,感谢您理解并努力回答我的问题,我认为您的回答真正深入了解了我一直在苦苦挣扎的问题。您的回答显然对我(以及其他想要解决这个问题的人)来说是一个很好的起点,因此我首先给了您的回答点赞。谢谢markE. :) - user905864
关于缩放的问题,我已经使用了相同的代码在旋转之前实现了它。只是我用的是 transform.scale() 而不是 transform.rotate()。由于缩放不关心角度,所以即使我将镜头移动到其他地方,枢轴仍然停留在镜头中心,这非常有效。当然,这只是一个缩放而不是真正的Z参数,但我现在并不需要它;我需要缩放来实现摄像机效果,而不是3D效果。再次感谢。 - user905864

2
“x”和“y”点用于移动世界中心的位置,它们是通过旋转角度的正切计算得出的。我有一个展示这个概念的例子,它不是最终解决方案,但可以作为你开始工作的起点。把它看作“初步处理”(没有时间完全优化它)。例如,如你所见,旋转的支点点并没有移动到镜头正下方的点,因此旋转并未直接发生在相机下方。

下面是一个说明几何/三角函数运算的图表:

enter image description here

这是代码片段和一个完全可用的jsfiddle链接。键盘命令与您的示例相同。

http://jsfiddle.net/ricksuggs/X8rYF/

    //s (down)
    if (keycode === 83) {

        event.preventDefault();
        canvas.getContext('2d').clearRect(-200,-200,400,400);
        canvas.getContext('2d').translate(2*Math.tan(rotationSum), -2); 
        drawGrid();
        drawCamera();

    }

    // <-- (rotate left)
    if (keycode === 37) {

        event.preventDefault();
        canvas.getContext('2d').clearRect(-200,-200,400,400);
        rotationSum += rotation;
        console.log('rotationSum: ' + rotationSum);
        canvas.getContext('2d').rotate(-rotation);        
        drawGrid();
        drawCamera();

    }

恭喜ricksuggs,你做到了。我从来没有想过tan在这个上面工作,因为我一直在搞这个三角学的东西,但它从来没有成功过(仅仅因为我的数学太差了)。感谢你的解释和实时示例,希望这也能帮助其他人。 - user905864
哦,我对我的判断过早了,但在回答中你仍然是最接近的,所以让我来帮助你整理一下。是否有任何方法可以使您的网格保持旋转,基于十字准心的中心,即使它移动了?因为显示的网格是基于自己的支点而不是相机中心(十字准心)旋转的。这基于问题的第二个要点,谢谢。 - user905864
正如我在第一段中提到的那样,旋转的枢轴点尚未移动到直接位于相机镜头下方的点...... 如果我有时间,我将修改解决方案以纠正此问题。 - Rick Suggs
啊,你提到了这个,我的错。你已经得到我赞成的投票了,因为你已经做得很好了。请慢慢来。 :) - user905864

0

顺序应该是这样的:

Ctx.save()
Ctx.translate(x, y) //to the center (Or top left of object to rotate)
Ctx.rotates(radian)
Ctx.translate(-x, -y) //important to move the context back!
Ctx.draw(...)
Ctx.restore()

我尝试了您发布的代码,但是当我进行动态翻译时,结果与绘制的一样。我已经更新了我的问题,并附上了代码,以使我的操作更加清晰明了。 - user905864
看起来你没有将第一次翻译中产生的负数金额返还,以重置上下文。 - Jarrod
根据我的代码,如果你的意思是我应该在transform.rotate(worldRotate)之后放置transform.translate(-ctx.canvas.width / 2, -ctx.canvas.height / 2);,那么它只会将红色框移动到左上角(当然是画布大小的一半),问题仍然存在;红色框向紫色箭头移动,即使它仍然围绕相机中心旋转。也许你可以给我建议,在我的代码中哪里应该进行负数量的平移作为解决方案?谢谢。 - user905864

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