3d 中三角形的 z 排序问题

z sorting issue with triangles in 3d

我们用 js 编写了一个旋转的 3d 形状。顶部三角形的渲染出现闪烁,我们认为这是因为 z 排序没有正常工作。我们如何解决这个问题?

这是一个jsfiddle

这是 z 排序代码:

// z sorting

// dots_for_rendering.sort((a,b) => Math.sqrt((b.x)**2 + (b.y)**2) - Math.sqrt((a.x)**2 + (a.y)**2))

for (var i = 0; i < polygons.length; i++) {
  polygons[i].maxz = -Infinity;
  polygons[i].minz = Infinity;
  polygons[i].midz = 0;


for (var j = 0; j < polygons[i].verticies.length; j++) {
  var z = rotated_verticies[polygons[i].verticies[j]].vector[2];
  if (z > polygons[i].maxz) {
    polygons[i].maxz = z;
  }
  if (z < polygons[i].minz) {
    polygons[i].minz = z;
  }
  polygons[i].midz += z;
}
polygons[i].midz /= polygons[i].verticies.length;

}

polygons.sort((a, b) => b.midz - a.midz)
// polygons.sort((a,b) => Math.max(b.maxz - a.minz, b.minz - a.maxz))


// polygons.sort((a,b) => {
//   if (a.minz < b.maxz) {
//     return 0;
//   }
//   if (b.minz < a.maxz) {
//     return -1;
//   }
//   return 0;
// })

这是一个代码片段:

class Tensor {
  constructor(){
    var input = this.takeInput(...arguments);
    this.vector = input;
  }

  takeInput() {
    var a = true;
    for (var arg of arguments) {
      if (typeof arg !== "number"){
        a = false
      }
    }

     if (a && arguments[2] !== true){
       return new Array(...arguments);
     }
     else {
       if (arguments[0] instanceof Tensor){
         return arguments[0].vector;
       }
       else {
          if (typeof arguments[0] === "number" && typeof arguments[1] === "number" && arguments[2] === true) {
            var res = [];
            for (var i = 0; i < arguments[0]; i++) {
              res.push(arguments[1]);
            }
            return res;
          }
       }
     }
  }

  // used for + - * /
  change(f, input){
    for (var i in this.vector) {
      this.vector[i] = f(this.vector[i], input[i]);
    }
    return this;
  }

  copy() {
    return new Tensor(...this.vector);
  }

  dimentions() {
    return this.vector.length;
  }

  //-----------

  len() {
    var s = 0;
    for (var dim of this.vector) {
      s += dim ** 2;
    }
    return Math.sqrt(s);
  }

  norm() {
    return this.div(this.dimentions(), this.len(), true)
  }

  add() {
    var input = this.takeInput(...arguments);
    return this.change((x, y) => x + y, input);
  }

  sub() {
    var input = this.takeInput(...arguments);
    return this.change((x, y) => x - y, input);
  }

  mult() {
    var input = this.takeInput(...arguments);
    return this.change((x, y) => x * y, input);
  }

  div() {
    var input = this.takeInput(...arguments);
    return this.change((x, y) => x / y, input);
  }

  dot() {
    var input = this.takeInput(...arguments);
    var res = 0;
    for (var i in this.vector) {
      res += this.vector[i] * input[i]
    }
    return res;
  }

  rotate() {
    // WARNING: only for 3D currently!!!
    var input = this.takeInput(...arguments);

    var [x, y, z] = this.vector;

    // rotate Z
    var t_x = x * Math.cos(input[2]) - y * Math.sin(input[2])
    y = y * Math.cos(input[2]) + x * Math.sin(input[2])
    x = t_x

    // rotate X
    var t_y = y * Math.cos(input[0]) - z * Math.sin(input[0])
    z = z * Math.cos(input[0]) + y * Math.sin(input[0])
    y = t_y

    // rotate Y
    t_x = x * Math.cos(input[1]) + z * Math.sin(input[1])
    z = z * Math.cos(input[1]) - x * Math.sin(input[1])
    x = t_x

    this.vector = [x, y, z];

    return this;
  }
}



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


w = 300
h = 286

fov = 0.1
scale = 65;
offset = new Tensor(w / 2 - 5, h / 2 - 92, 0.1);
light = new Tensor(3.5, 0.5, 1).norm();

canvas.width = w;
canvas.height = h;

var verticies = [];
verticies.push(new Tensor(0.5, 1, 0))
verticies.push(new Tensor(0.5, -1, 0))
verticies.push(new Tensor(-1, 0, 0))
verticies.push(new Tensor(0, 0, 2))


var polygons = [];
polygons.push({
  verticies: [0, 3, 1],
  color: 'red',
  nf: 1
});
polygons.push({
  verticies: [2, 3, 0],
  color: 'blue',
  nf: 1
});
polygons.push({
  verticies: [2, 3, 1],
  color: 'green',
  nf: -1
});
polygons.push({
  verticies: [0, 1, 2],
  color: 'yellow',
  nf: -1
});


for (var i = 0; i < polygons.length; i++) {
  polygons[i].id = i;
}

theta = new Tensor(1.5 * Math.PI, 0, 1.5 * Math.PI);

function loop() {
  ctx.clearRect(0, 0, w, h);


  rotated_verticies = [];

  for (var i = 0; i < verticies.length; i++) {
    rotated_verticies.push(verticies[i].copy().rotate(theta));
  }

  // z sorting

  // dots_for_rendering.sort((a,b) => Math.sqrt((b.x)**2 + (b.y)**2) - Math.sqrt((a.x)**2 + (a.y)**2))

  for (var i = 0; i < polygons.length; i++) {
    polygons[i].maxz = -Infinity;
    polygons[i].minz = Infinity;
    polygons[i].midz = 0;


    for (var j = 0; j < polygons[i].verticies.length; j++) {
      var z = rotated_verticies[polygons[i].verticies[j]].vector[2];
      // z += 1 * (Math.random() * 2 - 1)
      if (z > polygons[i].maxz) {
        polygons[i].maxz = z;
      }
      if (z < polygons[i].minz) {
        polygons[i].minz = z;
      }
      polygons[i].midz += z;
    }
    polygons[i].midz /= polygons[i].verticies.length;

  }

  polygons.sort((a, b) => b.midz - a.midz)
  // polygons.sort((a,b) => Math.max(b.maxz - a.minz, b.minz - a.maxz))


  // polygons.sort((a,b) => {
  //   if (a.minz < b.maxz) {
  //     return 0;
  //   }
  //   if (b.minz < a.maxz) {
  //     return -1;
  //   }
  //   return 0;
  // })




  for (var i = 0; i < polygons.length; i++) {
    var polygon_2 = [];

    for (var j = 0; j < polygons[i].verticies.length; j++) {
      var v = rotated_verticies[polygons[i].verticies[j]]
      polygon_2.push(v.vector);
    }

    var norm = getNormal(polygon_2, polygons[i].nf);
    // var rotated_light = light.copy().rotate(theta);
    var brightness = Math.max(0, norm.dot(light))

    //ctx.fillStyle = "hsl(31, "+100+"%, "+(Math.min(9.0*brightness + 40, 100))+"%)";
    ctx.fillStyle = "hsl(190, "+100+"%, "+(Math.min(9.0*brightness + 40, 100))+"%)";
    // ctx.fillStyle = polygons[i].color
    ctx.beginPath();

    for (var j = 0; j < polygons[i].verticies.length; j++) {
      var vertex = rotated_verticies[polygons[i].verticies[j]].copy();
      vertex.mult(scale, scale, 1);
      vertex.add(offset);
      var n = 1 + vertex.vector[2] * fov;
      vertex.div(n, n, 1)

      // console.log(vertex.vector)
      if (j == 0) {
        ctx.moveTo(vertex.vector[0], vertex.vector[1]);
      } else {
        ctx.lineTo(vertex.vector[0], vertex.vector[1]);
      }
    }
    ctx.closePath();
    ctx.fill()
    // ctx.stroke()

    polygons[i].mid = new Tensor(3, 0, true);

    for (var k = 0; k < polygons[i].verticies.length; k++) {
      var vertex = rotated_verticies[polygons[i].verticies[k]].copy();

      vertex.mult(scale, scale, 1);
      vertex.add(offset);


      var n = 1 + vertex.vector[2] * fov;
      vertex.div(n, n, 1)

      polygons[i].mid.add(vertex);
    }

    polygons[i].mid.div(3, polygons[i].verticies.length, true);



    ctx.fillStyle = "red"
    ctx.font = '50px serif';

    // ctx.fillText(polygons[i].id + ", " + polygons[i].nf, polygons[i].mid.vector[0], polygons[i].mid.vector[1])
  }



  // theta.add(theta.vector[0] + (0.01*mouseY - theta.vector[0]) * 0.1, 0, theta.vector[2] + (-0.01*mouseX - theta.vector[2]) * 0.1)

  theta.add(0, -0.0375, 0);

  // fov = (mouseX - w/2) * 0.001
  requestAnimationFrame(loop);
}

loop();

// setInterval(loop, 1000 / 60)


function getNormal(polygon, nf) {

  var Ax = polygon[1][0] - polygon[0][0];
  var Ay = polygon[1][1] - polygon[0][1];
  var Az = polygon[1][2] - polygon[0][2];

  var Bx = polygon[2][0] - polygon[0][0];
  var By = polygon[2][1] - polygon[0][1];
  var Bz = polygon[2][2] - polygon[0][2];


  var Nx = Ay * Bz - Az * By
  var Ny = Az * Bx - Ax * Bz
  var Nz = Ax * By - Ay * Bx

  return new Tensor(nf * Nx, nf * Ny, nf * Nz);
}

function len(p1, p2) {
  return Math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2 + (p2[2] - p1[2]) ** 2);
}



mouseX = 0
mouseY = 0

onmousemove = (e) => {
  mouseX = e.clientX;
  mouseY = e.clientY;
}
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta name=”ad.size” content=”width=300,height=600”>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>...</title>

</head>
<body>

   <canvas id="canvas" width="300" height="238"></canvas>



</body>

</html>

** 编辑:

好的,我们已经对代码进行了大量编辑,请参阅此 fiddle 以及下面的代码片段。还是不能正常工作,我们认为这与这段代码的第一行有关,有什么想法吗?

    if (polygons[i].mid.copy().sub(camera).dot(norm) < 0) {
  var pathelem = document.createElementNS("http://www.w3.org/2000/svg", "path");
  pathelem.setAttribute("d", path);
  pathelem.setAttribute("fill", "hsl(31, "+100+"%, "+(Math.min(9.0*brightness + 40, 100))+"%)");
  svg.appendChild(pathelem);
}

class Tensor {
  constructor(){
    var input = this.takeInput(...arguments);
    this.vector = input;
  }

  takeInput() {
    var a = true;
    for (var arg of arguments) {
      if (typeof arg !== "number"){
        a = false
      }
    }

     if (a && arguments[2] !== true){
       return new Array(...arguments);
     }
     else {
       if (arguments[0] instanceof Tensor){
         return arguments[0].vector;
       }
       else {
          if (typeof arguments[0] === "number" && typeof arguments[1] === "number" && arguments[2] === true) {
            var res = [];
            for (var i = 0; i < arguments[0]; i++) {
              res.push(arguments[1]);
            }
            return res;
          }
       }
     }
  }

  // used for + - * /
  change(f, input){
    for (var i in this.vector) {
      this.vector[i] = f(this.vector[i], input[i]);
    }
    return this;
  }

  copy() {
    return new Tensor(...this.vector);
  }

  dimentions() {
    return this.vector.length;
  }

  //-----------

  len() {
    var s = 0;
    for (var dim of this.vector) {
      s += dim ** 2;
    }
    return Math.sqrt(s);
  }

  norm() {
    return this.div(this.dimentions(), this.len(), true)
  }

  add() {
    var input = this.takeInput(...arguments);
    return this.change((x, y) => x + y, input);
  }

  sub() {
    var input = this.takeInput(...arguments);
    return this.change((x, y) => x - y, input);
  }

  mult() {
    var input = this.takeInput(...arguments);
    return this.change((x, y) => x * y, input);
  }

  div() {
    var input = this.takeInput(...arguments);
    return this.change((x, y) => x / y, input);
  }

  dot() {
    var input = this.takeInput(...arguments);
    var res = 0;
    for (var i in this.vector) {
      res += this.vector[i] * input[i]
    }
    return res;
  }

  rotate() {
    // WARNING: only for 3D currently!!!
    var input = this.takeInput(...arguments);

    var [x, y, z] = this.vector;

    // rotate Z
    var t_x = x * Math.cos(input[2]) - y * Math.sin(input[2])
    y = y * Math.cos(input[2]) + x * Math.sin(input[2])
    x = t_x

    // rotate X
    var t_y = y * Math.cos(input[0]) - z * Math.sin(input[0])
    z = z * Math.cos(input[0]) + y * Math.sin(input[0])
    y = t_y

    // rotate Y
    t_x = x * Math.cos(input[1]) + z * Math.sin(input[1])
    z = z * Math.cos(input[1]) - x * Math.sin(input[1])
    x = t_x

    this.vector = [x, y, z];

    return this;
  }
}


var svg = document.getElementById('svg')


w = 300
h = 286

fov = 0.1
scale = 65;
camera = new Tensor(-w / 2 + 5, -h / 2 + 92, 0.1);
light = new Tensor(3.5, 0.5, 1).norm();

svg.setAttribute('width', w);
svg.setAttribute('height', h);

var vertices = [
  new Tensor(0.5, 1, 0),
  new Tensor(0.5, -1, 0),
  new Tensor(-1, 0, 0),
  new Tensor(0, 0, 2)
];

var polygons = [];
polygons.push({
  vertices: [0, 3, 1],
  color: 'red',
  nf: 1
});
polygons.push({
  vertices: [2, 3, 0],
  color: 'blue',
  nf: 1
});
polygons.push({
  vertices: [2, 3, 1],
  color: 'green',
  nf: -1
});
polygons.push({
  vertices: [0, 1, 2],
  color: 'yellow',
  nf: 1
});


for (var i = 0; i < polygons.length; i++) {
  polygons[i].id = i;
}

theta = new Tensor(1.5 * Math.PI, 0, 1.5 * Math.PI);

function loop() {
  // ctx.clearRect(0, 0, w, h);
  svg.innerHTML = "";


  rotated_vertices = [];

  for (var i = 0; i < vertices.length; i++) {
    rotated_vertices.push(vertices[i].copy().rotate(theta));
  }

  // z sorting

  // dots_for_rendering.sort((a,b) => Math.sqrt((b.x)**2 + (b.y)**2) - Math.sqrt((a.x)**2 + (a.y)**2))

  for (var i = 0; i < polygons.length; i++) {
    polygons[i].maxz = -Infinity;
    polygons[i].minz = Infinity;
    polygons[i].midz = 0;


    for (var j = 0; j < polygons[i].vertices.length; j++) {
      var z = rotated_vertices[polygons[i].vertices[j]].vector[2];
      // z += 1 * (Math.random() * 2 - 1)
      if (z > polygons[i].maxz) {
        polygons[i].maxz = z;
      }
      if (z < polygons[i].minz) {
        polygons[i].minz = z;
      }
      polygons[i].midz += z;
    }
    polygons[i].midz /= polygons[i].vertices.length;


    polygons[i].mid = new Tensor(3, 0, true);

    for (var k = 0; k < polygons[i].vertices.length; k++) {
      var vertex = rotated_vertices[polygons[i].vertices[k]].copy();

      vertex.mult(scale, scale, 1);
      vertex.sub(camera);


      var n = 1 + vertex.vector[2] * fov;
      vertex.div(n, n, 1)

      polygons[i].mid.add(vertex);
    }

    polygons[i].mid.div(3, polygons[i].vertices.length, true);

  }

  polygons.sort((a, b) => b.midz - a.midz)
  // polygons.sort((a,b) => Math.max(b.maxz - a.minz, b.minz - a.maxz))


  // polygons.sort((a,b) => {
  //   if (a.minz < b.maxz) {
  //     return 0;
  //   }
  //   if (b.minz < a.maxz) {
  //     return -1;
  //   }
  //   return 0;
  // })


  for (var i = 0; i < polygons.length; i++) {
    var polygons_embedded_point_coords = [];

    for (var j = 0; j < polygons[i].vertices.length; j++) {
      var v = rotated_vertices[polygons[i].vertices[j]]
      polygons_embedded_point_coords.push(v.vector);
    }

    var norm = getNormal(polygons_embedded_point_coords, polygons[i].nf);
    // var rotated_light = light.copy().rotate(theta);
    var brightness = Math.max(0, norm.dot(light))

    // ctx.fillStyle = "hsl(31, "+100+"%, "+(Math.min(9.0*brightness + 40, 100))+"%)";
    // ctx.fillStyle = polygons[i].color
    // ctx.beginPath();

    var path = [];

    for (var j = 0; j < polygons[i].vertices.length; j++) {
      var vertex = rotated_vertices[polygons[i].vertices[j]].copy();
      vertex.mult(scale, scale, 1);
      vertex.sub(camera);
      var n = 1 + vertex.vector[2] * fov;
      vertex.div(n, n, 1)

      // console.log(vertex.vector)
      if (j == 0) {
        // ctx.moveTo(vertex.vector[0], vertex.vector[1]);
        path.push("M "+vertex.vector[0]+" "+vertex.vector[1]);
      } else {
        path.push("L "+vertex.vector[0]+" "+vertex.vector[1]);
        // ctx.lineTo(vertex.vector[0], vertex.vector[1]);
      }
    }


    // that should work
    if (polygons[i].mid.copy().sub(camera).dot(norm) < 0) {
      var pathelem = document.createElementNS("http://www.w3.org/2000/svg", "path");
      pathelem.setAttribute("d", path);
      pathelem.setAttribute("fill", "hsl(31, "+100+"%, "+(Math.min(9.0*brightness + 40, 100))+"%)");
      svg.appendChild(pathelem);
    }


    // ctx.fillStyle = "red"
    // ctx.font = '15px serif';
    //
    // ctx.fillText(polygons[i].id + ", " + polygons[i].nf, polygons[i].mid.vector[0], polygons[i].mid.vector[1])
  }


  // theta.add(theta.vector[0] + (0.01*mouseY - theta.vector[0]) * 0.1, 0, theta.vector[2] + (-0.01*mouseX - theta.vector[2]) * 0.1)

  theta.add(0, -0.0375, 0);

  // fov = (mouseX - w/2) * 0.001
  requestAnimationFrame(loop);
}

loop();

// setInterval(loop, 1000 / 60)


function getNormal(vertices, nf) {

  var Ax = vertices[1][0] - vertices[0][0];
  var Ay = vertices[1][1] - vertices[0][1];
  var Az = vertices[1][2] - vertices[0][2];

  var Bx = vertices[2][0] - vertices[0][0];
  var By = vertices[2][1] - vertices[0][1];
  var Bz = vertices[2][2] - vertices[0][2];

  var Nx = Ay * Bz - Az * By
  var Ny = Az * Bx - Ax * Bz
  var Nz = Ax * By - Ay * Bx

  return new Tensor(nf * Nx, nf * Ny, nf * Nz);
}

function len(p1, p2) {
  return Math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2 + (p2[2] - p1[2]) ** 2);
}


mouseX = 0
mouseY = 0

onmousemove = (e) => {
  mouseX = e.clientX;
  mouseY = e.clientY;
}
<!DOCTYPE html>
<html lang="en" dir="ltr">

<head>
  <meta charset="utf-8">
  <title></title>
  <script src="Tensor.js"></script>
  <script src="script-tensors-svg.js" async defer></script>
</head>

<body>
  <!-- <canvas id="canvas"></canvas> -->
  <svg id="svg" xmlns="http://www.w3.org/2000/svg"></svg>

</body>

</html>

按平均 Z 排序并不能为您提供可靠的渲染顺序。但是,由于您的形状是凸形的,所以您根本不需要排序。

确保对每个三角形的顶点进行排序,以便始终获得指向外的表面法线。然后,不要渲染任何法线指向相机远离的三角形,即:

if (vector_from_camera_to_poly_midpoint \dot poly_normal < 0) {
   //render the poly
}

现在您将只渲染对象面向相机的一侧 -- none 多边形将重叠,因此您可以按任何顺序渲染它们。