代码之家  ›  专栏  ›  技术社区  ›  Mevia

利用transform api实现三维幻觉效果

  •  1
  • Mevia  · 技术社区  · 6 年前

    我在做一个简单的太空船二维游戏,我想通过利用 transform api . 通过实验我最终调整了 a (水平缩放)和 d (垂直缩放)。

    所以在默认情况下(没有转换),我们可以使用 ctx.transform(1, 0, 0, 1, 0, 0);

    我现在的设置是这个 ctx.transform(1.72, 0, 0, 0.65, 0, 0);

    有了这个设置船看起来体面,但只有在一些角度下。我在想是否有办法用适当的数学来应用公式来动态调整这些值(可能是基于船的旋转角度)。不幸的是,我的数学知识很差,所以我请求更有经验的成员帮助我解决这个问题。

    下面是小原型(通过单击单选按钮,您可以在默认值和我的硬编码值之间切换):

    var mode = "3d";
    var toggle = function(ev, item) {
      mode = item.value;
    };
    
    var tools = new function() {
      this.rad = a => (Math.PI / 180) * a;
      this.deg = rad => (rad * 180) / Math.PI;
      this.distance = p =>
        Math.sqrt((p.x - p.dx) * (p.x - p.dx) + (p.y - p.dy) * (p.y - p.dy));
      this.rftv = p => Math.atan2(p.dy - p.y, p.dx - p.x);
      this.pfa = function(l, x, y, a) {
        return {
          x: Math.cos(this.rad(a)) * l + x,
          y: Math.sin(this.rad(a)) * l + y
        };
      };
    }();
    
    let design = {
      c: [
        { size: 0.7166666666666667, deg: -90 },
        { size: 0.5060742150229658, deg: -107.24145939893998 },
        { size: 0.42196629670573876, deg: -99.09027692082233 },
        { size: 0.08975274678557507, deg: -158.19859051364818 },
        { size: 0.08975274678557507, deg: -21.80140948635181 },
        { size: 0.42196629670573876, deg: -80.90972307917767 },
        { size: 0.5060742150229658, deg: -72.75854060106003 }
      ],
      l1: [
        { size: 0.4552166761249221, deg: -113.74949449286676 },
        { size: 0.3901566636906542, deg: -109.98310652189998 },
        { size: 0.18408935028645435, deg: -174.8055710922652 },
        { size: 0.6324555320336759, deg: 161.565051177078 }
      ],
      r1: [
        { size: 0.3901566636906542, deg: -70.01689347810003 },
        { size: 0.4552166761249221, deg: -66.25050550713325 },
        { size: 0.6324555320336759, deg: 18.43494882292201 },
        { size: 0.18408935028645435, deg: -5.194428907734806 }
      ],
      l2: [
        { size: 0.2608745973749754, deg: 153.434948822922 },
        { size: 0.6262764742685312, deg: 154.79887635452494 },
        { size: 0.6616477747093069, deg: 130.91438322002512 }
      ],
      r2: [
        { size: 0.2608745973749754, deg: 26.56505117707799 },
        { size: 0.6262764742685312, deg: 25.20112364547507 },
        { size: 0.6616477747093069, deg: 49.08561677997487 }
      ]
    };
    
    let circle = (x, y, r, fs, ss) => {
      ctx.save();
      ctx.beginPath();
      ctx.arc(x, y, r, 0, Math.PI * 2);
      if (fs !== false) {
        ctx.fillStyle = fs;
        ctx.fill();
      }
      if (ss !== false) {
        ctx.lineWidth = 1;
        ctx.strokeStyle = ss;
        ctx.stroke();
      }
      ctx.restore();
    };
    
    var transform = function(zAxis, tilt, scale, x, y) {
    	var cs = Math.cos(zAxis), sn = Math.sin(zAxis);
    	var h = Math.cos(tilt);
    	var a = scale*cs, b = -scale*sn, c = x;
    	var d = h*scale*sn, e = h*scale*cs, f = y;
      return { a, d, b, e, c, f };
    };
    
    let ship = (x, y, size, a, fs) => {
      ctx.save();
      ctx.beginPath();
    
      ctx.translate(x, y);
      if (mode === "2d") {
        ctx.transform(1, 0, 0, 1, 0, 0);
      }
      if (mode === "3d") {
        var { a, d, b, e, c, f } = transform(tools.rad(a), 45, 1, x, y);
        ctx.setTransform(a, d, b, e, c, f);
      }
      ctx.translate(-x, -y);
    
      for (let type in design) {
        for (let i = 0; i < design[type].length; i++) {
          let c = design[type][i],
            p = tools.pfa(size * c.size, x, y, c.deg + a + 90);
          if (i === 0) {
            ctx.moveTo(p.x, p.y);
          } else {
            ctx.lineTo(p.x, p.y);
          }
        }
        if (design[type].length > 0) {
          let c = design[type][0],
            p = tools.pfa(size * c.size, x, y, c.deg + a + 90);
          ctx.lineTo(p.x, p.y);
        }
      }
    
      ctx.fillStyle = fs;
      ctx.fill();
      ctx.restore();
    
      circle(x, y, size, false, "blue");
    };
    
    let cvs = document.createElement("canvas"),
      ctx = cvs.getContext("2d"),
      w = (cvs.width = 400),
      h = (cvs.height = 400),
      cx = w / 2,
      cy = h / 2;
    
    let points = [
      { x: cx - 40, y: 3 },
      { x: cx + 40, y: h - 3 },
      { x: 3, y: cy + 40 },
      { x: w - 3, y: cy - 40 }
    ];
    
    let shipData = {
      x: cx,
      y: cy,
      r: 40,
      a: 0,
      c: 0,
      dx: points[0].x,
      dy: points[0].y,
      run: function() {
        let d = tools.distance(this);
        if (d < 1) {
          this.c += 1;
          if (this.c > points.length - 1) {
            this.c = 0;
          }
          this.dx = points[this.c].x;
          this.dy = points[this.c].y;
        }
        let rad = tools.rftv(this);
        this.a = tools.deg(rad);
        this.x += Math.cos(rad);
        this.y += Math.sin(rad);
      }
    };
    
    let render = () => {
      ctx.clearRect(0, 0, w, h);
      ctx.fillStyle = "#ccc";
      ctx.fillRect(0, 0, w, h);
    
      /* debug */ circle(points[0].x, points[0].y, 3, "red", false);
      /* debug */ circle(points[1].x, points[1].y, 3, "red", false);
      /* debug */ circle(points[2].x, points[2].y, 3, "red", false);
      /* debug */ circle(points[3].x, points[3].y, 3, "red", false);
    
      ship(shipData.x, shipData.y, shipData.r, shipData.a, "blue");
      shipData.run();
    
      requestAnimationFrame(render);
    };
    
    document.body.appendChild(cvs);
    render();
    <div>
    	2d <input type="radio" onclick="toggle(event, this);" name="display" value="2d">
    	3d <input type="radio" onclick="toggle(event, this);" name="display" value="3d" checked>
    </div>

    ---编辑

    我根据这个做了一个编辑 topic 在答案中,有人给出了计算矩阵的公式,但在我的设置下,它似乎不起作用。

    我是这样实现的:

    var transform = function(angle1, angle2, size1, size2) {
      var cs = Math.cos(angle1), sn = Math.sin(angle1);
      var h = Math.cos(angle2);
      var a = size1*cs, b = -size1*sn, c = size2;
      var d = h*size1*sn, e = h*size1*cs, f = size2;
      return { a, d, b, e, c, f };
    };
    
    if (mode === "3d") {
        var { a, d, b, e, c, f } = transform(a, a, size * 0.5, size);
        ctx.setTransform(a, d, b, e, c, f);
    }
    

    ---编辑2

    在评论的帮助下,我假设:

    var transform = function(zAxis, tilt, scale, x, y) {
        var cs = Math.cos(zAxis), sn = Math.sin(zAxis);
        var h = Math.cos(tilt);
        var a = scale*cs, b = -scale*sn, c = x;
        var d = h*scale*sn, e = h*scale*cs, f = y;
        return { a, d, b, e, c, f };
    };
    
    if(mode === '3d') {
        var {a, d, b, e, c, f} = transform(tools.rad(a), 45, 1, x, y);
        ctx.setTransform(a, d, b, e, c, f);
    }
    

    所以我想 scale 1 应该没事的,既然我 x, y 是船的中心所以我可以用它们。最后一块拼图将是第二个参数 global tilt ,我确实设置为 45 deg 它看起来很不错,但是我不知道它是否100%正确,我更新了代码片段中的代码,以便有人查看。

    1 回复  |  直到 6 年前
        1
  •  1
  •   6502    6 年前

    您可以使用画布轻松呈现ismoetric(非透视)图形,同样的转换也可以用于CSS。矩阵系数取决于倾斜角度(通常固定在等距游戏)和围绕Z(高度)轴的旋转。

    对于完整的公式,您可以看到以下答案: https://stackoverflow.com/a/5186153/320726