HTML5 Canvas + JavaScript线框球体转换问题

时间:2017-03-12 11:23:37

标签: javascript html5 3d

我编写了一些JS代码,用于将3D线框球体绘制到HTML5画布中。

我从this post开始,并使用Qt3D vertices generation对球形网格进行了改进。 JS代码在顶点上执行2次传递:第一次显示环,第二次显示切片。通常,OpenGL会自动将所有顶点与三角形连接起来。

我保持切片/环可配置,但我对转换代码有疑问,例如当我沿X轴旋转球体时。

所以,从基础开始。这是一个1遍,4个环,4个切片,没有变换:

enter image description here

似乎一切都很好。现在2遍,10环,10片,没有转变:

enter image description here

还是不错的,但如果我在X轴上旋转30°,顶部和底部顶点(显然只是Y位置)会搞砸。

enter image description here

我怀疑旋转功能或投影功能有问题。

有人可以帮我弄清楚这里发生了什么吗?

注意我不想使用Three.js导致我的目标是在QML应用程序中移植它)

这是完整的代码。

var sphere = new Sphere3D();
var rotation = new Point3D();
var distance = 1000;
var lastX = -1;
var lastY = -1;

function Point3D() {
  this.x = 0;
  this.y = 0;
  this.z = 0;
}

function Sphere3D(radius) {
  this.vertices = new Array();
  this.radius = (typeof(radius) == "undefined" || typeof(radius) != "number") ? 20.0 : radius;
  this.rings = 10;
  this.slices = 10;
  this.numberOfVertices = 0;

  var M_PI_2 = Math.PI / 2;
  var dTheta = (Math.PI * 2) / this.slices;
  var dPhi = Math.PI / this.rings;

  // Iterate over latitudes (rings)
  for (var lat = 0; lat < this.rings + 1; ++lat) {
    var phi = M_PI_2 - lat * dPhi;
    var cosPhi = Math.cos(phi);
    var sinPhi = Math.sin(phi);

    // Iterate over longitudes (slices)
    for (var lon = 0; lon < this.slices + 1; ++lon) {
      var theta = lon * dTheta;
      var cosTheta = Math.cos(theta);
      var sinTheta = Math.sin(theta);
      p = this.vertices[this.numberOfVertices] = new Point3D();

      p.x = this.radius * cosTheta * cosPhi;
      p.y = this.radius * sinPhi;
      p.z = this.radius * sinTheta * cosPhi;
      this.numberOfVertices++;
    }
  }
}

function rotateX(point, radians) {
  var y = point.y;
  point.y = (y * Math.cos(radians)) + (point.z * Math.sin(radians) * -1.0);
  point.z = (y * Math.sin(radians)) + (point.z * Math.cos(radians));
}

function rotateY(point, radians) {
  var x = point.x;
  point.x = (x * Math.cos(radians)) + (point.z * Math.sin(radians) * -1.0);
  point.z = (x * Math.sin(radians)) + (point.z * Math.cos(radians));
}

function rotateZ(point, radians) {
  var x = point.x;
  point.x = (x * Math.cos(radians)) + (point.y * Math.sin(radians) * -1.0);
  point.y = (x * Math.sin(radians)) + (point.y * Math.cos(radians));
}

function projection(xy, z, xyOffset, zOffset, distance) {
  return ((distance * xy) / (z - zOffset)) + xyOffset;
}

function strokeSegment(index, ctx, width, height) {
  var x, y;
  var p = sphere.vertices[index];

  rotateX(p, rotation.x);
  rotateY(p, rotation.y);
  rotateZ(p, rotation.z);

  x = projection(p.x, p.z, width / 2.0, 100.0, distance);
  y = projection(p.y, p.z, height / 2.0, 100.0, distance);

  if (lastX == -1 && lastY == -1) {
    lastX = x;
    lastY = y;
    return;
  }

  if (x >= 0 && x < width && y >= 0 && y < height) {
    if (p.z < 0) {
      ctx.strokeStyle = "gray";
    } else {
      ctx.strokeStyle = "white";
    }
    ctx.beginPath();
    ctx.moveTo(lastX, lastY);
    ctx.lineTo(x, y);
    ctx.stroke();
    ctx.closePath();
    lastX = x;
    lastY = y;
  }
}

function render() {
  var canvas = document.getElementById("sphere3d");
  var width = canvas.getAttribute("width");
  var height = canvas.getAttribute("height");
  var ctx = canvas.getContext('2d');

  var p = new Point3D();
  ctx.fillStyle = "black";

  ctx.clearRect(0, 0, width, height);
  ctx.fillRect(0, 0, width, height);

  // draw each vertex to get the first sphere skeleton
  for (i = 0; i < sphere.numberOfVertices; i++) {
    strokeSegment(i, ctx, width, height);
  }

  // now walk through rings to draw the slices
  for (i = 0; i < sphere.slices + 1; i++) {
    for (var j = 0; j < sphere.rings + 1; j++) {
      strokeSegment(i + (j * (sphere.slices + 1)), ctx, width, height);
    }
  }
}

function init() {
  rotation.x = Math.PI / 6;
  render();
}
canvas {
  background: black;
  display: block;
}
<body onLoad="init();">
  <canvas id="sphere3d" width="500" height="500">
    Your browser does not support HTML5 canvas.
  </canvas>
</body>

2 个答案:

答案 0 :(得分:1)

您的问题是您的stroke.vertices []数组的内容正在您的strokeSegment()调用中被修改,因此当您在每个点上第二次调用时,旋转将被应用两次。因此,在strokeSegment()中,您需要替换:

var p = sphere.vertices[index];

使用:

var p = new Point3D();

p.x = sphere.vertices[index].x;
p.y = sphere.vertices[index].y;
p.z = sphere.vertices[index].z;

然后它完美地工作如下所示:

var sphere = new Sphere3D();
var rotation = new Point3D();
var distance = 1000;
var lastX = -1;
var lastY = -1;

function Point3D() {
  this.x = 0;
  this.y = 0;
  this.z = 0;
}

function Sphere3D(radius) {
  this.vertices = new Array();
  this.radius = (typeof(radius) == "undefined" || typeof(radius) != "number") ? 20.0 : radius;
  this.rings = 10;
  this.slices = 10;
  this.numberOfVertices = 0;

  var M_PI_2 = Math.PI / 2;
  var dTheta = (Math.PI * 2) / this.slices;
  var dPhi = Math.PI / this.rings;

  // Iterate over latitudes (rings)
  for (var lat = 0; lat < this.rings + 1; ++lat) {
    var phi = M_PI_2 - lat * dPhi;
    var cosPhi = Math.cos(phi);
    var sinPhi = Math.sin(phi);

    // Iterate over longitudes (slices)
    for (var lon = 0; lon < this.slices + 1; ++lon) {
      var theta = lon * dTheta;
      var cosTheta = Math.cos(theta);
      var sinTheta = Math.sin(theta);
      p = this.vertices[this.numberOfVertices] = new Point3D();

      p.x = this.radius * cosTheta * cosPhi;
      p.y = this.radius * sinPhi;
      p.z = this.radius * sinTheta * cosPhi;
      this.numberOfVertices++;
    }
  }
}

function rotateX(point, radians) {
  var y = point.y;
  point.y = (y * Math.cos(radians)) + (point.z * Math.sin(radians) * -1.0);
  point.z = (y * Math.sin(radians)) + (point.z * Math.cos(radians));
}

function rotateY(point, radians) {
  var x = point.x;
  point.x = (x * Math.cos(radians)) + (point.z * Math.sin(radians) * -1.0);
  point.z = (x * Math.sin(radians)) + (point.z * Math.cos(radians));
}

function rotateZ(point, radians) {
  var x = point.x;
  point.x = (x * Math.cos(radians)) + (point.y * Math.sin(radians) * -1.0);
  point.y = (x * Math.sin(radians)) + (point.y * Math.cos(radians));
}

function projection(xy, z, xyOffset, zOffset, distance) {
  return ((distance * xy) / (z - zOffset)) + xyOffset;
}

function strokeSegment(index, ctx, width, height) {
  var x, y;
  var p = new Point3D();

  p.x = sphere.vertices[index].x;
  p.y = sphere.vertices[index].y;
  p.z = sphere.vertices[index].z;

  rotateX(p, rotation.x);
  rotateY(p, rotation.y);
  rotateZ(p, rotation.z);

  x = projection(p.x, p.z, width / 2.0, 100.0, distance);
  y = projection(p.y, p.z, height / 2.0, 100.0, distance);

  if (lastX == -1 && lastY == -1) {
    lastX = x;
    lastY = y;
    return;
  }

  if (x >= 0 && x < width && y >= 0 && y < height) {
    if (p.z < 0) {
      ctx.strokeStyle = "gray";
    } else {
      ctx.strokeStyle = "white";
    }
    ctx.beginPath();
    ctx.moveTo(lastX, lastY);
    ctx.lineTo(x, y);
    ctx.stroke();
    ctx.closePath();
    lastX = x;
    lastY = y;
  }
}

function render() {
  var canvas = document.getElementById("sphere3d");
  var width = canvas.getAttribute("width");
  var height = canvas.getAttribute("height");
  var ctx = canvas.getContext('2d');

  var p = new Point3D();
  ctx.fillStyle = "black";

  ctx.clearRect(0, 0, width, height);
  ctx.fillRect(0, 0, width, height);

  // draw each vertex to get the first sphere skeleton
  for (i = 0; i < sphere.numberOfVertices; i++) {
    strokeSegment(i, ctx, width, height);
  }

  // now walk through rings to draw the slices
  for (i = 0; i < sphere.slices + 1; i++) {
    for (var j = 0; j < sphere.rings + 1; j++) {
      strokeSegment(i + (j * (sphere.slices + 1)), ctx, width, height);
    }
  }
}

function init() {
  rotation.x = Math.PI / 3;
  render();
}
canvas {
  background: black;
  display: block;
}
<body onLoad="init();">
  <canvas id="sphere3d" width="500" height="500">
    Your browser does not support HTML5 canvas.
  </canvas>
</body>

答案 1 :(得分:1)

简短回答

该错误发生在strokeSegment函数

function strokeSegment(index, ctx, width, height) {
  var x, y;
  var p = sphere.vertices[index];

  rotateX(p, rotation.x);
  rotateY(p, rotation.y);
  rotateZ(p, rotation.z);
  ...

错误是所有rotate函数都在原位修改p,因此修改了sphere.vertices中存储的值!所以修复它的方法就是克隆这一点:

function strokeSegment(index, ctx, width, height) {
  var x, y;
  var p0 = sphere.vertices[index];
  var p = new Point3D();
  p.x = p0.x;
  p.y = p0.y;
  p.z = p0.z;

  rotateX(p, rotation.x);
  rotateY(p, rotation.y);
  rotateZ(p, rotation.z);
  ...

您可以在https://plnkr.co/edit/zs5ZxbglFxo9cbwA6MI5?p=preview

找到包含固定代码的演示

更长时间

在我发现这个问题之前,我使用了你的代码,我觉得有所改进。 https://plnkr.co/edit/tpTZ8GH9eByVARUIYZBi?p=preview

提供了改进版本
var sphere = new Sphere3D();
var rotation = new Point3D(0, 0, 0);
var distance = 1000;

var EMPTY_VALUE = Number.MIN_VALUE;

function Point3D(x, y, z) {
    if (arguments.length == 3) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
    else if (arguments.length == 1) {
        fillPointFromPoint(this, x); // 1 argument means point
    }
    else {
        clearPoint(this); // no arguments mean creat empty
    }
}

function fillPointFromPoint(target, src) {
    target.x = src.x;
    target.y = src.y;
    target.z = src.z;
}

function clearPoint(p) {
    p.x = EMPTY_VALUE;
    p.y = EMPTY_VALUE;
    p.z = EMPTY_VALUE;
}

function Sphere3D(radius) {
    this.radius = (typeof(radius) == "undefined" || typeof(radius) != "number") ? 20.0 : radius;
    this.innerRingsCount = 9; // better be odd so we have explicit Equator
    this.slicesCount = 8;


    var M_PI_2 = Math.PI / 2;
    var dTheta = (Math.PI * 2) / this.slicesCount;
    var dPhi = Math.PI / this.innerRingsCount;


    this.rings = [];
    // always add both poles
    this.rings.push([new Point3D(0, this.radius, 0)]);

    // Iterate over latitudes (rings)
    for (var lat = 0; lat < this.innerRingsCount; ++lat) {
        var phi = M_PI_2 - lat * dPhi - dPhi / 2;
        var cosPhi = Math.cos(phi);
        var sinPhi = Math.sin(phi);
        console.log("lat = " + lat + " phi = " + (phi / Math.PI) + " sinPhi = " + sinPhi);

        var vertices = [];
        // Iterate over longitudes (slices)
        for (var lon = 0; lon < this.slicesCount; ++lon) {
            var theta = lon * dTheta;
            var cosTheta = Math.cos(theta);
            var sinTheta = Math.sin(theta);
            var p = new Point3D();
            p.x = this.radius * cosTheta * cosPhi;
            p.y = this.radius * sinPhi;
            p.z = this.radius * sinTheta * cosPhi;
            vertices.push(p);
        }
        this.rings.push(vertices);
    }

    // always add both poles
    this.rings.push([new Point3D(0, -this.radius, 0)]);
}

function rotateX(point, radians) {
    var y = point.y;
    point.y = (y * Math.cos(radians)) + (point.z * Math.sin(radians) * -1.0);
    point.z = (y * Math.sin(radians)) + (point.z * Math.cos(radians));
}

function rotateY(point, radians) {
    var x = point.x;
    point.x = (x * Math.cos(radians)) + (point.z * Math.sin(radians) * -1.0);
    point.z = (x * Math.sin(radians)) + (point.z * Math.cos(radians));
}

function rotateZ(point, radians) {
    var x = point.x;
    point.x = (x * Math.cos(radians)) + (point.y * Math.sin(radians) * -1.0);
    point.y = (x * Math.sin(radians)) + (point.y * Math.cos(radians));
}

function projection(xy, z, xyOffset, zOffset, distance) {
    return ((distance * xy) / (z - zOffset)) + xyOffset;
}


var lastP = new Point3D();
var firstP = new Point3D();

function startRenderingPortion() {
    clearPoint(lastP);
    clearPoint(firstP);
}

function closeRenderingPortion(ctx, width, height) {
    strokeSegmentImpl(ctx, firstP.x, firstP.y, firstP.z, width, height);
    clearPoint(lastP);
    clearPoint(firstP);
}

function strokeSegmentImpl(ctx, x, y, z, width, height) {
    if (x >= 0 && x < width && y >= 0 && y < height) {
        // as we work with floating point numbers, there might near zero that != 0
        // choose gray if one of two points is definitely (z < 0) and other has (z <= 0)
        // Note also that in term of visibility this is a wrong logic! Line is invisible
        // only if it is shadowed by another polygon and this depends on relative "Z" not
        // absolute values
        var eps = 0.01;
        if (((z < -eps) && (lastP.z < eps))
            || ((z < eps) && (lastP.z < -eps))) {
            ctx.strokeStyle = "gray";
        } else {
            ctx.strokeStyle = "white";
        }

        if ((x === lastP.x) && (y == lastP.y)) {
            ctx.beginPath();
            // draw single point
            ctx.moveTo(x, y);
            ctx.lineTo(x + 1, y + 1);
            ctx.stroke();
            ctx.closePath();
        } else {
            ctx.beginPath();
            ctx.moveTo(lastP.x, lastP.y);
            ctx.lineTo(x, y);
            ctx.stroke();
            ctx.closePath();
        }
        lastP.x = x;
        lastP.y = y;
        lastP.z = z;
    }
}

function strokeSegment(p0, ctx, width, height) {
    var p = new Point3D(p0); // clone original point to not mess it up with rotation!
    rotateX(p, rotation.x);
    rotateY(p, rotation.y);
    rotateZ(p, rotation.z);

    var x, y;
    x = projection(p.x, p.z, width / 2.0, 100.0, distance);
    y = projection(p.y, p.z, height / 2.0, 100.0, distance);

    if (lastP.x === EMPTY_VALUE && lastP.y === EMPTY_VALUE) {
        lastP = new Point3D(x, y, p.z);
        fillPointFromPoint(firstP, lastP);
        return;
    }
    strokeSegmentImpl(ctx, x, y, p.z, width, height);
}


function renderSphere(ctx, width, height, sphere) {
    var i, j;
    var vertices;
    // draw each vertex to get the first sphere skeleton
    for (i = 0; i < sphere.rings.length; i++) {
        startRenderingPortion();
        vertices = sphere.rings[i];
        for (j = 0; j < vertices.length; j++) {
            strokeSegment(vertices[j], ctx, width, height);
        }
        closeRenderingPortion(ctx, width, height);
    }

    // now walk through rings to draw the slices

    for (i = 0; i < sphere.slicesCount; i++) {
        startRenderingPortion();
        for (j = 0; j < sphere.rings.length; j++) {
            vertices = sphere.rings[j];
            var p = vertices[i % vertices.length];// for top and bottom vertices.length = 1
            strokeSegment(p, ctx, width, height);
        }
        //closeRenderingPortion(ctx, width, height); // don't close back!
    }
}

function render() {
    var canvas = document.getElementById("sphere3d");
    var width = canvas.getAttribute("width");
    var height = canvas.getAttribute("height");
    var ctx = canvas.getContext('2d');

    ctx.fillStyle = "black";

    ctx.clearRect(0, 0, width, height);
    ctx.fillRect(0, 0, width, height);

    renderSphere(ctx, width, height, sphere);
}

function init() {
    rotation.x = Math.PI / 6;
    //rotation.y = Math.PI / 6;
    rotation.z = Math.PI / 6;
    render();
}

主要变化是:

  • 我在数组vertices数组中明确地分离了普通数组rings,并明确地将两个极点添加到它中。
  • rings的分离让我更频繁地清除lastX / Y,以通过引入startRenderingPortion来避免一些虚假的行。
  • 我还介绍了与closeRenderingPortion逻辑上相似的closePath。使用这种方法,我能够删除所需的重复点。
  • 一般情况下,我试图避免使用更多OOP-ish样式(请参阅renderSphereclearPoint),但我更改了Point3D构造函数以支持3种模式:(x ,y,z),指向,空。
  • 为空的lastX / Y var EMPTY_VALUE = Number.MIN_VALUE;使用更明确的标记值。 -1是可能的值

另请注意,您的灰色/白色选择存在潜在的错误,我没有修复。我认为你的颜色应该反映出&#34;隐形&#34; Z > 0 vs Z < 0的行和简单逻辑无法正确解决此问题。实际上,如果场景中的其他东西遮挡了单行,则它可能只是部分可见。