3

I'm developing a kind of perspective based 2d/3d game in javascript.

I've got an X- and an Y-axis like I've displayed in the image below.


To my question: I've got a bunch of objects (marked with "1" and "2") on my map with properties like:

  • positionX / positionY
  • sizeX / sizeY

In the image Object "1" does get the coordinates x:3, y:2, and the Object "2" does get the coordinates x:5, y:4. SizeX and sizeY is w:1, h:1 for both objects.

What I'd like to do with this info is to sort all of the objects in ascending order (based on the objects position and size) to know in 3d which objects comes for another object to lateron draw all of my objects sorted into my canvas (objects in foreground/background = "layers").


img

Note: The Camera has to fixed position - lets say the camera has the identical X and Y value so that the camera position must not be used while calculation CameraX = CameraY.

What I've tried so far:

let objects = [
  {
    name: "objectA",
    x: 8,
    y: 12,
    w: 2,
    h: 2
  }, 
  {
    name: "objectB",
    x: 3,
    y: 5,
    w: 2,
    h: 2
  },
  {
    name: "objectC",
    x: 6,
    y: 2,
    w: 1,
    h: 3
  }
]


let sortObjects = (objects) => {
  return objects.sort((a, b)=> {
    let distanceA = Math.sqrt(a.x**2 + a.y**2);
    let distanceB = Math.sqrt(b.x**2 + b.y**2);
    return distanceA - distanceB;
  });
}


let sortedObjects = sortObjects(objects);
console.log(sortedObjects);

// NOTE in 3d: first Object drawn first, second Object drawn second and so on...

Edit to the snippet above:

I've tried to sort the objects based on their x/y coordinate but it seems like the width and height parameter must be used also while calculation to avoid errors.

How do I have to make use of width/height? Tbh I've got no clue so any help would be really appreciated.

  • If all you want to do is sort your objects in back-to-front order for rendering purposes (the so-called painter's algorithm) they why not implement a BSP-tree. BSP-tree guarantee perfect sorting, but you would need to trade-off some things... – Mauricio Cele Lopez Belon Jan 28 '19 at 16:13

5 Answers5

1

I'm not sure what you meant by:

Note: The Camera has to fixed position - lets say the camera has the identical X and Y value so that the camera position must not be used while calculation CameraX = CameraY.

So here's a general-case solution.

You must sort the objects by their closest distance to the camera. This depends on an object's dimensions as well as its relative position.

The algorithm can be implemented in JS as follows:

// If e.g. horizontal distance > width / 2, subtract width / 2; same for vertical
let distClamp = (dim, diff) => {
    let dist = Math.abs(diff);
    return (dist > 0.5 * dim) ? (dist - 0.5 * dim) : dist;
}

// Closest distance to the camera
let closestDistance = (obj, cam) => {
    let dx = distClamp(obj.width, obj.x - cam.x);
    let dy = distClamp(obj.height, obj.y - cam.y);
    return Math.sqrt(dx * dx + dy * dy);
}

// Sort using this as the metric
let sortObject = (objects, camera) => {
    return objects.sort((a, b) => {
        return closestDistance(a, camera) - closestDistance(b, camera);
    });
}

EDIT this solution also does not work because it makes naive assumptions, will update soon or delete.

meowgoesthedog
  • 14,670
  • 4
  • 27
  • 40
  • I think this solution looks pretty good. Especially because you use the width and height for the calculation. I have built your algorithm into my code... long story short: This approach doesn't seem to work either. In many cases the layer function works perfectly (even if I had to swap closestDistance(a, camera) - closestDistance(b, camera)), but there are many exceptions where it doesn't work as it should... –  Jan 19 '19 at 22:22
  • ... If I'm standing in front of an object (sizeX: 1, sizeY: 2) on the right side I'm painted as a player behind the object instead of in front of it. On the other sides it seems to work without any problems. –  Jan 19 '19 at 22:22
  • @jonas00 Can you give the coordinates and dimensions of the player and object, and the coordinates of the camera? I don't see how this problem should arise. – meowgoesthedog Jan 19 '19 at 22:29
  • Sure! **Player**: `{x:7.16, y: 10.25, width: 0.2, height: 0.2 }`, **Object**: `{x: 6.66, y: 11.44, width: 0.3, height: 1.6}`. I located my camera at `x: 100, y: 100`. It will make no difference where my camera is because x and y are identical at all time. So `x: 200, y: 200` would be valid also. –  Jan 19 '19 at 22:35
  • @jonas00 ah I realise what is going wrong now, apologies. I totally forgot that a simple approach such as this won't work because it doesn't take the orientation of object planes into account. I'll re-post or delete. – meowgoesthedog Jan 19 '19 at 22:43
  • Ok. Thanks so much in advance. –  Jan 20 '19 at 09:10
  • @jonas00 just another question - are you rendering in perspective or orthogonal view? I have a possible algorithm in mind – meowgoesthedog Jan 21 '19 at 10:57
  • Hey @meowgoesthedog. Sorry for asking so stupidly - but do you have any idea how I can move on? I still have no clue how to continue and you wrote that you've had another approach. Greetings –  Mar 07 '19 at 22:15
1

Alright, let's have a go at this! We'll have to order these objects not by distance, but by obstruction. By that, I mean for two objects A and B, A can visibly obstruct B, B can visibly obstruct A, or neither obstructs the other. If A obstructs B, we'll want to draw B first, and vice-versa. To solve this, we need to be able to say whether A obstructs B, or the other way around.

Here's what I've come up with. I only had a limited ability to test this, so there may still be flaws, but the thought process is sound.

Step 1. Map each object to its bounds, saving the original object for later:

let step1 = objects.map(o => ({
  original: o,
  xmin: o.x,
  xmax: o.x + o.w,
  ymin: o.y,
  ymax: o.y + o.h
}));

Step 2. Map each object to the two corners that, when a line is drawn between them, form the largest obstruction to the camera's field of view:

let step2 = step1.map(o => {
  const [closestX, farthestX] = [o.xmin, o.xmax].sort((a, b) => Math.abs(camera.x - a) - Math.abs(camera.x - b));
  const [closestY, farthestY] = [o.ymin, o.ymax].sort((a, b) => Math.abs(camera.y - a) - Math.abs(camera.y - b));

  return {
    original: o.original,
    x1: closestX,
    y1: o.xmin <= camera.x && camera.x <= o.xmax ? closestY : farthestY,
    x2: o.ymin <= camera.y && camera.y <= o.ymax ? closestX : farthestX,
    y2: closestY
  };
});

Step 3. Sort the objects. Draw a line segment from the camera to each endpoint of one object. If the line segment between the endpoints of the other object intersects, the other object is closer and must be drawn after.

let step3 = step2.sort((a, b) => {
  const camSegmentA1 = {
    x1: camera.x,
    y1: camera.y,
    x2: a.x1,
    y2: a.y1
  };
  const camSegmentA2 = {
    x1: camera.x,
    y1: camera.y,
    x2: a.x2,
    y2: a.y2
  };
  const camSegmentB1 = {
    x1: camera.x,
    y1: camera.y,
    x2: b.x1,
    y2: b.y1
  };
  const camSegmentB2 = {
    x1: camera.x,
    y1: camera.y,
    x2: b.x2,
    y2: b.y2
  };

  // Intersection function taken from here: https://stackoverflow.com/a/24392281
  function intersects(seg1, seg2) {
    const a = seg1.x1, b = seg1.y1, c = seg1.x2, d = seg1.y2,
          p = seg2.x1, q = seg2.y1, r = seg2.x2, s = seg2.y2;
    const det = (c - a) * (s - q) - (r - p) * (d - b);
    if (det === 0) {
      return false;
    } else {
      lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / det;
      gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / det;
      return (0 < lambda && lambda < 1) && (0 < gamma && gamma < 1);
    }
  }

  function squaredDistance(pointA, pointB) {
    return Math.pow(pointB.x - pointA.x, 2) + Math.pow(pointB.y - pointA.y, 2);
  }

  if (intersects(camSegmentA1, b) || intersects(camSegmentA2, b)) {
    return -1;
  } else if (intersects(camSegmentB1, a) || intersects(camSegmentB2, a)) {
    return 1;
  } else {
    return Math.max(squaredDistance(camera, {x: b.x1, y: b.y1}), squaredDistance(camera, {x: b.x2, y: b.y2})) - Math.max(squaredDistance(camera, {x: a.x1, y: a.y1}), squaredDistance(camera, {x: a.x2, y: a.y2}));
  }
});

Step 4. Final step -- get the original objects back, sorted in order of farthest to closest:

let results = step3.map(o => o.original);

Now, to put it all together:

results = objects.map(o => {
  const xmin = o.x,
        xmax = o.x + o.w,
        ymin = o.y,
        ymax = o.y + o.h;

  const [closestX, farthestX] = [xmin, xmax].sort((a, b) => Math.abs(camera.x - a) - Math.abs(camera.x - b));
  const [closestY, farthestY] = [ymin, ymax].sort((a, b) => Math.abs(camera.y - a) - Math.abs(camera.y - b));

  return {
    original: o,
    x1: closestX,
    y1: xmin <= camera.x && camera.x <= xmax ? closestY : farthestY,
    x2: ymin <= camera.y && camera.y <= ymax ? closestX : farthestX,
    y2: closestY
  };
}).sort((a, b) => {
  const camSegmentA1 = {
    x1: camera.x,
    y1: camera.y,
    x2: a.x1,
    y2: a.y1
  };
  const camSegmentA2 = {
    x1: camera.x,
    y1: camera.y,
    x2: a.x2,
    y2: a.y2
  };
  const camSegmentB1 = {
    x1: camera.x,
    y1: camera.y,
    x2: b.x1,
    y2: b.y1
  };
  const camSegmentB2 = {
    x1: camera.x,
    y1: camera.y,
    x2: b.x2,
    y2: b.y2
  };

  // Intersection function taken from here: https://stackoverflow.com/a/24392281
  function intersects(seg1, seg2) {
    const a = seg1.x1, b = seg1.y1, c = seg1.x2, d = seg1.y2,
          p = seg2.x1, q = seg2.y1, r = seg2.x2, s = seg2.y2;
    const det = (c - a) * (s - q) - (r - p) * (d - b);
    if (det === 0) {
      return false;
    } else {
      lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / det;
      gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / det;
      return (0 < lambda && lambda < 1) && (0 < gamma && gamma < 1);
    }
  }

  function squaredDistance(pointA, pointB) {
    return Math.pow(pointB.x - pointA.x, 2) + Math.pow(pointB.y - pointA.y, 2);
  }

  if (intersects(camSegmentA1, b) || intersects(camSegmentA2, b)) {
    return -1;
  } else if (intersects(camSegmentB1, a) || intersects(camSegmentB2, a)) {
    return 1;
  }
  return Math.max(squaredDistance(camera, {x: b.x1, y: b.y1}), squaredDistance(camera, {x: b.x2, y: b.y2})) - Math.max(squaredDistance(camera, {x: a.x1, y: a.y1}), squaredDistance(camera, {x: a.x2, y: a.y2}));
}).map(o => o.original);

Let me know if that works!

JasonR
  • 401
  • 3
  • 11
  • @jonas00 I see the problem: I was testing if the camera line segments intersected with the other line SEGMENT, when I should have been testing if it intersects with the other LINE. I edited my code. Does it work now? – JasonR Feb 02 '19 at 13:18
  • @jonas00 Fixed! There was a bug in my new approach to finding the intersections, so I went back to the previous way using segment intersections and added checks for the case that was handled incorrectly. I also realized by playing with your JSFiddle that the objects aren't centered on their coordinates. The coordinates are the upper left corner. I edited my code in my answer, and I also edited your JSFiddle: https://jsfiddle.net/4z9bcu7o/ The lines from the camera to the objects are shown for testing purposes. Note also that you may have to adjust the position of your camera. – JasonR Feb 02 '19 at 15:58
  • @jonas00 Thanks! Glad I could help! – JasonR Feb 02 '19 at 16:47
  • Jupyy. The example works perfectly - good work bro. But now I have tested your code in my game - it does not work as it should. A thousand thanks for your help anyways - I grant you the bounty even if there are some bugs left I've just noticed now. Mhm. :) –  Feb 03 '19 at 16:39
  • @jonas00 What are the bugs now? What arrangement of objects is drawn incorrectly? I’m curious about what scenarios I didn’t anticipate and how my code can be fixed. – JasonR Feb 03 '19 at 22:06
0

The issue here is that you are using euclidean distance to measure how far away an object is from (0, 0), to try to measure the distance from the y = -x line. This will not work, however Manhattan distance will.

let sortObjects = (objects) => {
  return objects.sort((a, b)=> {
    let distanceA = a.x + a.y;
    let distanceB = b.x + b.y;
    return distanceA - distanceB;
  });
}

This will order your objects vertically in your rotated coordinate system.

Dillon Davis
  • 6,679
  • 2
  • 15
  • 37
  • Thanks for your answer. But I have to be completely honest: That answer doesn't seem to be true. When I calculate the Manhattan distance, it works partially, just as I calculated it with the euclidean distance. However, it does not work in every case. I therefore added that the reason for the error is that I did not calculate with the width and height of the objects. And I still think so. I am glad about your answer. Thank you in advance. –  Jan 19 '19 at 10:47
  • I must be misunderstanding the question. Can the objects have different sizes? If so, where are the images anchored (center, top-left, etc.)? – Dillon Davis Jan 19 '19 at 19:27
  • @jonas00 I'm just having a hard time seeing where this wouldn't work, provided the `cameraX == cameraY`, and (assuming) all objects have a fixed height/width – Dillon Davis Jan 19 '19 at 19:28
  • Yeah. But in my rotated coordinate system the layers are somehow connected to the objects width and height. All Objects could have different sizes what will break your/my sorting algorithm. And I'm pretty sure this solution is not working exactly as it should :( –  Jan 19 '19 at 19:30
  • @jonas00 do objects "shift" over in their rows to accommodate space for larger objects? Are your sprites not drawn with their centers at `(x, y)`? I really don't understand how the sprite widths/heights are changing the relative positioning. To me, it seems like a smaller sprite would still land in the same horizontal row as a normal sized one. – Dillon Davis Jan 19 '19 at 20:51
  • @jonas00 I might understand the issue now. You are saying an object may span the rectangle `(x: 0, y: 0) to (x: 1, y: 2)`, and so it'd be positioned according to `(x: (0+1)/2, y: (0+2)/2) = (0.5, 1)`, but this may cause objects just barely under it (and off to either the left or right) to be sorted above it instead. – Dillon Davis Jan 19 '19 at 23:14
  • If this is the case, it is not possible to solve using your current methodology. There exists no comparison function that will allow you to do a comparison-based sort to order these elements, because it would violate the triangle inequality. Example: square (x:0, y:5), rectangle (x:1, y:5) to (x:2, y:5), square (x:3, y:0). The first is above the second, the second above the third, but yet the third is above the first. You'll need to completely rethink your algorithm. – Dillon Davis Jan 19 '19 at 23:30
0

In your diagram consider the value of x+y for each cell

diagram with X+Y for each cell

To sort top-to-bottom the cells you can simply sort on the value of x+y.

6502
  • 112,025
  • 15
  • 165
  • 265
  • This is exactly what my code does, yet OP claims it does not work. – Dillon Davis Jan 19 '19 at 20:43
  • 1
    @DillonDavis: I know... hoped showing a diagram would make that clear – 6502 Jan 19 '19 at 20:45
  • But an object could go from (x: 0, y: 0) to (x: 1, y: 2) so I can not sort the objects by their x/y position like @DillonDavis does, because this is absolutely wrong. The X/Y parameter stands for the center point of the area. –  Jan 19 '19 at 22:23
  • @jonas00: what you should ask yourself is... "can anything built on a square marked 3 hide something on a square marked 4 ?"... (or another one marked 3) – 6502 Jan 19 '19 at 23:10
0

Maybe you can find something useful here (use Firefox & check the DEMO)

In my case depth is basically pos.x + pos.y [assetHelper.js -> get depth() {...] exactly as the first answer describes it. Then the sorting is a simple compare [canvasRenderer -> depthSortAssets() {...]

Tamás Katona
  • 683
  • 7
  • 13