Isometric simple sorting with xdim and ydim

2020-04-26 10:43发布

问题:

I have a simple isometric sorting system with this function (code is in Typescript/Javascript) :

public Sort(a: PIXI.Sprite, b: PIXI.Sprite) {
    return ((a.IsoZ - b.IsoZ) == 0 ? (a.TileZ - b.TileZ == 0 ? (a.Tile2Z ? (a.Tile2Z < b.Tile2Z ? -1 : (a.Tile2Z > b.Tile2Z ? 1 : 0)) : 0) : a.TileZ - b.TileZ) : (a.IsoZ - b.IsoZ));
}

It depends on three parameters:

  • IsoZ: the first sorting variables, used to sort tiles
  • TileZ: the tile sorting variable, used if a.IsoZ == b.IsoZ
  • Tile2Z: used if a.TileZ == b.TileZ

Here's how IsoZ is basically calculated for most objects:

this.Position is an array of x and y coordinates

this.Position[0] + this.Position[1] + 1000;

now I want to support object x and y dimensions, so how can I implement something like this in this expression?

x and y dimensions values are for example (2, 2) for a cube or (2, 4) for a cuboid

this.Position[0] + this.Position[1] + 1000 // + x dimension + y dimension ???

回答1:

Isometric visual occlusion sort (depth sort)

Defining depth: Higher depths values are closer to the screen. Unlike 3D perspective projection where depth is distance from the front plane, this answer uses depth as distance towards the screen.

Iso projection

If you have a iso projection

const P2 = (x = 0,y = 0) => ({x, y});
const isoProjMat = {
    xAxis : P2(1   , 0.5),
    yAxis : P2(-0.5, 1  ),
    zAxis : P2(0   , -1 ),
}

That takes a 3d point and projects to screen space

const P3 = (x = 0, y = 0, z = 0) => ({x, y, z});
isoProjMat.project = function (p, retP = P2()) { // p is 3D point
    retP.x = p.x * this.xAxis.x + p.y * this.yAxis.x + p.z * this.zAxis.x;
    retP.y = p.x * this.xAxis.y + p.y * this.yAxis.y + p.z * this.zAxis.y;
    return retP;
}

You can add the depth of a point as the z value of the 2D projected point. You need to add a transform axis for the depth.

isoProjMat.depth = P3(0.5,1, 1 );

For x move closer by half its size, y * 1 and z * 1.

The modified project now adds z to the returned point.

isoProjMat.project = function (p, retP = P3()) { 
    retP.x = p.x * this.xAxis.x + p.y * this.yAxis.x + p.z * this.zAxis.x;
    retP.y = p.x * this.xAxis.y + p.y * this.yAxis.y + p.z * this.zAxis.y;
    retP.z = p.x * this.depth.x + p.y * this.depth.y + p.z * this.depth.z; 
    return retP;
}

Thus for a set of points in 3D space projected to 2D iso screen space you sort on the z

const points = mySetOfPoints(); // what ever your points come from
const projected = points.map(p => isoProjMat.project(p));
projected.sort((a,b) => a.z - b.z);

All good for points but for sprites which occupy a 3D volume this does not work.

What you need to do is add a bounding volume ie a square. If your projection is static then we can simplify the bounding volume to the nearest point. For the box that is the vertex at the top bottom right eg sprite at (0,0,0) has a size (10,10,20) the nearest point in 3d is at (10,10,20).

I can not work your sort out as there is not enough info in the question but I am guessing sprite.Iso is the base origin of the sprite and sprite.Tile & Tile2 represent bounding box.

Thus to get the nearest point

const depthProj = P3(0.5,1, 1 ); // depth projection matrix
// get the depth of each sprite adding the property depth
sprites.forEach(spr => {
    const p = {
        x : spr.IsoX + Math.max(spr.TileX,spr.Tile2X),
        y : spr.IsoY + Math.max(spr.TileY,spr.Tile2Y),
        z : spr.IsoZ + Math.max(spr.TileZ,spr.Tile2Z)
    };
    spr.depth = p.x * depthProj.x + p.y * depthProj.y + p.z * depthProj.z; 
})
sprites.sort((a,b) => a.depth - b.depth);

Then render from index 0 up.

An example.

The following is not fully applicable as it sorts by polygons and uses the polygons mean depth rather than its max depth (really should use max but cant be bothered ATM)

I add it only to show how the above code for the isoProjMat is used. It draws stacked boxes from pixel alpha and color rendered on a canvas.

Click rendered result to switch projections from bi-morphic to tri-morphic (as you did not specify the type of projection you used this shows how the depth transform changes between two types of parallel projection.

const ctx = canvas.getContext("2d");
var count = 0;
var firstRun = 0;
function doIt(){

  // 3d 2d points
  const P3 = (x=0, y=0, z=0) => ({x,y,z});
  const P2 = (x=0, y=0) => ({x, y});

  // isomorphic projection matrix
  const isoProjMat = {
      xAxis : count ? P2(1 , 0.5) : P2(1 , 0.5) ,
      yAxis : count ? P2(-0.5, 1) : P2(-1 , 0.5) ,
      zAxis : count ? P2(0   , -1) : P2(0 , -1) ,
      depth : count ? P3(0.5,1, 1) : P3(0.5,0.5,1) , // projections have z as depth
      origin : P2(), // (0,0) default 2D point
      project (p, retP = P3()) {
          retP.x = p.x * this.xAxis.x + p.y * this.yAxis.x + p.z * this.zAxis.x + this.origin.x;
          retP.y = p.x * this.xAxis.y + p.y * this.yAxis.y + p.z * this.zAxis.y + this.origin.y;
          retP.z = p.x * this.depth.x + p.y * this.depth.y + p.z * this.depth.z; 
          return retP;
      }
  }

  // isomorphic mesh shape as vertices and polygons
  const isoMesh = (()=>{
      const polygon = {
          inds : null,
          depth : 0,
          fillStyle : "#888",
          lineWidth : 0.5,
          strokeStyle : "#000",
          setStyle(ctx) {
              ctx.fillStyle = this.fillStyle;
              ctx.lineWidth = this.lineWidth;
              ctx.strokeStyle = this.strokeStyle;
          },
      }
      const isoShape = {
          verts : null,
          pVerts : null, // projected verts
          polys : null,
          addVert(p3 = P3()) { this.verts.push(p3); return p3 },
          addPoly(poly = isoShape.createPoly()) { this.polys.push(poly); return poly },
          createPoly(options = {}) { return Object.assign({}, polygon, {inds : []}, options) },
          render(ctx,mat = isoProjMat) {
              var i,j,d;
              const pv = this.pVerts === null ? this.pVerts = [] : this.pVerts;
              const v = this.verts;
              const ps = this.polys;
              for(i = 0; i < v.length; i += 1){  pv[i] = mat.project(v[i], pv[i]) }
              for(i = 0; i < ps.length; i += 1) {
                  const p = ps[i];
                  j = 0; d = 0;
                  while(j < p.inds.length) { d += pv[p.inds[j++]].z }
                  p.depth = d / p.inds.length;
              }
              ps.sort((a,b)=>a.depth - b.depth);
              for(i = 0; i < ps.length; i += 1) {
                  const p = ps[i];
                  p.setStyle(ctx);
                  ctx.beginPath();
                  j = 0;
                  while(j < p.inds.length) { ctx.lineTo(pv[p.inds[j]].x, pv[p.inds[j++]].y) }
                  if (p.fillStyle !== "") { ctx.fill() }
                  if (p.strokeStyle !== "" && p.lineWidth !== 0) {ctx.closePath(); ctx.stroke() }
              }
          }
      }
      return () => Object.assign({},isoShape,{verts : [], polys : []});
  })();

  // Lazy coding I am using Point3 (P3) to hold RGB values
  function createBoxMesh(box = isoMesh(), pos = P3(), size = P3(10,10,10), rgb = P3(128,128,128)){ // x,y,z are sizes in those directions
      const PA3 = (x,y,z) => P3(x + pos.x, y + pos.y, z + pos.z);
      const RGB = (s) => `rgb(${(rgb.x * s) | 0},${(rgb.y * s) | 0},${(rgb.z * s) | 0})`;
      const indA = (inds) => inds.map(ind => ind + i);
      const i = box.verts.length; // get top vert index
      if(typeof size === "number") { size = P3(size,size,size) }
      const x = size.x / 2;
      const y = size.y / 2;
      const z = size.z;
      box.addVert(PA3(-x,-y, 0)); // ind 0
      box.addVert(PA3( x,-y, 0));
      box.addVert(PA3( x, y, 0));
      box.addVert(PA3(-x, y, 0));
      box.addVert(PA3(-x,-y, z)); // ind 4
      box.addVert(PA3( x,-y, z));
      box.addVert(PA3( x, y, z));
      box.addVert(PA3(-x, y, z));
     // box.addPoly(box.createPoly({ inds : indA([0,1,5,4]), fillStyle : RGB(0.5) }));
      box.addPoly(box.createPoly({ inds : indA([1,2,6,5]), fillStyle : RGB(0.7) }));
      box.addPoly(box.createPoly({ inds : indA([2,3,7,6]), fillStyle : RGB(1) }));
     // box.addPoly(box.createPoly({ inds : indA([3,0,4,7]), fillStyle : RGB(0.8) }));
      box.addPoly(box.createPoly({ inds : indA([4,5,6,7]), fillStyle : RGB(1.5) }));
      return box;
  }

  function createDrawable(w,h){
      const c = document.createElement("canvas");
      c.width = w;
      c.height = h;
      c.ctx = c.getContext("2d");
      return c;
  }
  const map = createDrawable(40,30);
  map.ctx.font = "20px arial";
  map.ctx.textAlign = "center";
  map.ctx.textBaseline = "middle";
  map.ctx.fillStyle = "rgba(0,128,0,0.5)";
  map.ctx.strokeStyle = "rgba(255,0,0,0.5)";
  map.ctx.lineWidth = 2;
  map.ctx.fillRect(1,1,map.width - 2, map.height - 2);
  map.ctx.strokeRect(1,1,map.width - 2, map.height - 2);
  map.ctx.fillStyle = "#AAA";
  map.ctx.strokeStyle = "rgba(255,128,0,0.5)";
  map.ctx.strokeText("text",map.width / 2, map.height / 2);
  map.ctx.fillText("text",map.width / 2, map.height / 2);
  var dat = map.ctx.getImageData(0, 0, map.width , map.height).data;

  ctx.setTransform(1,0,0,1,0,0);

  // get total projection area and size canvas so that the iso projection fits
  const boxSize = P3(10,10,5);
  const topLeft = isoProjMat.project(P3(0,0,10 * boxSize.z));
  const botRight = isoProjMat.project(P3(map.width * boxSize.x,map.height * boxSize.y,0));
  const topRight = isoProjMat.project(P3(map.width * boxSize.x,0,0));
  const botLeft = isoProjMat.project(P3(0,map.height * boxSize.y,0));

  canvas.width = ((topRight.x - botLeft.x) + 10)|0;
  canvas.height = ((botRight.y - topLeft.y) + 10)|0;
  ctx.clearRect(0,0,canvas.width,canvas.height);
  ctx.font = "32px arial";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.fillText("Rendering will take a moment.",Math.min(innerWidth,canvas.width)/2,Math.min(innerHeight,canvas.height)/2)
  setTimeout(function(){
    ctx.clearRect(0,0,canvas.width,canvas.height);
    ctx.setTransform(1,0,0,1,-botLeft.x+10,-topLeft.y+10);

    const alphaThresh = 100;
    const boxes = isoMesh();
    for(var y = 0; y < map.height; y ++){
        for(var x = 0; x < map.width; x ++){
            const ind = (x + y * map.width) * 4;
            if(dat[ind + 3] > alphaThresh){
                const h = (((dat[ind + 3]-alphaThresh)/(255-alphaThresh)) * 10) | 0;
                for(var z = 0; z < h; z++){
                    createBoxMesh(
                        boxes,
                        P3(x * boxSize.x,y * boxSize.y, z * boxSize.z),
                        boxSize,
                        P3(dat[ind],dat[ind+1],dat[ind+2])
                    );
                }
            }
        }
    }

    boxes.render(ctx);
    if(firstRun === 0){
        firstRun = 1;
        ctx.setTransform(1,0,0,1,0,0);
        ctx.font = "24px arial";
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";
        ctx.fillStyle = "black";
        ctx.fillText("Bimorphic projection. Click for Trimorphic projection..",canvas.width/2,30)
        canvas.onclick =()=>{
           count += 1;
           count %= 2;
           doIt();
         };
     }
  },0);

};
doIt();
canvas {
   border : 2px solid black;
}
<canvas id="canvas"></canvas>