My website

sign my guestbook!!!

thank you!!


Todays song!!!

Live Again - Mori Calliope - spotify


Site pages

please press ctrl + shift + r if the page isn't loading correctly

9/23/24 - 3d in CanvasRenderingContext2d


3d is possible without webgl or webgpu

before i explain how i, a better question in my opinion is why?

i am doing this because i like 3d and i like backwards compatibility. for me, consistency is most important. browsers aren't exactly great for that but browsers are available on basically everything so that makes it worth it imo. and webgl is not supported by old browsers, like chrome 1.0 which is what i use for testing backwards compatibility. webgl might also not be supported on some devices even with modern browsers.

how?

it's just a single function. here it is:

  function mapTriangle(x0, y0, x1, y1, x2, y2, x3, y3, x4, y4, x5, y5) {
    var t = !!(y1 - y0) || !!(x2 - x0) ?
      [
        -9/(x0-x1),
        -((y1-y0)/(x1-x0))*(-9/(y0-y2)),
        -((x2-x0)/(y2-y0))*(-9/(x0-x1)),
        -9/(y0-y2)
      ] : [
        9/(x0-x2),
        -((y2-y0)/(x2-x0))*(9/(y0-y1)),
        -((x1-x0)/(y1-y0))*(9/(x0-x2)),
        9/(y0-y1)
      ],
    i = t[0]*x1+t[2]*y1-(t[0]*x0+t[2]*y0),
    j = t[1]*x1+t[3]*y1-(t[1]*x0+t[3]*y0),
    s = Math.abs(i) > Math.abs(j) ? i : j,
    r = [
      -(x3-x4)/s,
      -(y3-y4)/s,
      -(x3-x5)/s,
      -(y3-y5)/s
    ];
    return [
      r[0]*t[0]+r[2]*t[1],
      r[1]*t[0]+r[3]*t[1],
      r[0]*t[2]+r[2]*t[3],
      r[1]*t[2]+r[3]*t[3],
      (r[0]*t[0]+r[2]*t[1])*-x0+(r[0]*t[2]+r[2]*t[3])*-y0+x3,
      (r[1]*t[0]+r[3]*t[1])*-x0+(r[1]*t[2]+r[3]*t[3])*-y0+y3
    ];
  }

what the fuck?

first, what is this function for? the main problem with doing 3d with 2d canvas context is textures and speed. If you have only single-color triangles you can use Path2d and fill with newell's algorithm. However, since newell's algorithm paints in triangles we can't really add textures. The other method for rendering 3d is using a depth buffer, and this always textures and handling intersections HOWEVER, it's done pixel by pixel - and i've tried it, javascript is not fast enough for this, especially if you're trying to make it chrome 1 compatible since it doesn't support putImageData and the best way to do this is ImageData/a pixel array.

so, what does this function do?

this function inputs the coordinates of 2 triangles, and outputs an array of arguments you can provide to CanvasRenderingContext2d.transform so that when you draw the first triangle, it's coordinates will be at the location of the second triangle.

why do we need this??

this allows drawing an image (clipping to the shape of a triangle) and using it as the texture for a 3d triangle! wow! now we can use newell's algorithm (OKOK I HAVENT ACTUALLY TESTED THIS YET BUT HOPEFULLY ITLL GO WELL)

ok, but how does it work?

here's a more readable version of the function:

  function mapTriangle(x0, y0, x1, y1, x2, y2, x3, y3, x4, y4, x5, y5) {
    var transform;
    if (y1 - y0 === 0 || x2 - x0 === 0) {
      transform = [
        -9 / (x0 - x1),
        -((y1 - y0) / (x1 - x0)) * (-9 / (y0 - y2)),
        -((x2 - x0) / (y2 - y0)) * (-9 / (x0 - x1)),
        -9 / (y0 - y2)
      ]
    } else {
      transform = [
        9 / (x0 - x2),
        -((y2 - y0) / (x2 - x0)) * (9 / (y0 - y1)),
        -((x1 - x0) / (y1 - y0)) * (9 / (x0 - x2)),
        9 / (y0 - y1)
      ]
    }
    var width = transform[0] * x1 + transform[2] * y1 - (transform[0] * x0 + transform[2] * y0);
    var height = transform[1] * x1 + transform[3] * y1 - (transform[1] * x0 + transform[3] * y0);
    var size = Math.abs(width) > Math.abs(height) ? width : height;
    var transform2 = [
      -(x3 - x4) / size,
      -(y3 - y4) / size,
      -(x3 - x5) / size,
      -(y3 - y5) / size
    ];
    return [
      transform2[0] * transform[0] + transform2[2] * transform[1],
      transform2[1] * transform[0] + transform2[3] * transform[1],
      transform2[0] * transform[2] + transform2[2] * transform[3],
      transform2[1] * transform[2] + transform2[3] * transform[3],
      (transform2[0] * transform[0] + transform2[2] * transform[1]) * -x0 + (transform2[0] * transform[2] + transform2[2] * transform[3]) * -y0 + x3,
      (transform2[1] * transform[0] + transform2[3] * transform[1]) * -x0 + (transform2[1] * transform[2] + transform2[3] * transform[3]) * -y0 + y3
    ];
  }

..err, yeah. not much better.

this function consists of 3 main steps:

  1. offset the triangle's first coordinate to 0
  2. transform the triangle to an isosceles right triangle
  3. transform the right triangle to the output triangle

each of these steps represents a separate transform

ok, so what is the transform variable?

lets see. by default, transform will be set to

  [
    9 / (x0 - x2),
    -((y2 - y0) / (x2 - x0)) * (9 / (y0 - y1)),
    -((x1 - x0) / (y1 - y0)) * (9 / (x0 - x2)),
    9 / (y0 - y1)
  ]

transform represents the second step - offsetting the triangle does need to be done first, but since in the end all 3 transforms are combined into one.

if you dont know how CanvasRenderingContext2d.transform works, read that here.

ok, now back to the transform variable. transform is the first 4 arguments to canvas2d transform - the last 2 are skipped since that is part of the 1st step. it's not included here because the last two arguments go after the first four, while in this case we need the offset before all the other transforms

notice how all 4 indices are being multiplied by 9 / (y0 - y1) or 9 / (x0 - x2). This scales the triangle, but more importantly since the scale factor of 9 is the same between x and y, it makes it isosceles which helps a lot later. the remaining part is -((y2 - y0) / (x2 - x0)) and -((x1 - x0) / (y1 - y0))

the 2nd and 3rd argument to canvas transform determine how much of the opposite axis the coordinates are affected by - for example, if the coordinates of a point are (x, y), changing the first and 4th argument to transform will just add a multiplier to x or y, effectively scaling the shape. but changing the 2nd or 3rd will make it so (x, y) becomes (x + y * 3rd arg, x * 2nd arg + y). This means we can adjust the coordinates however the hell we want if the first point is 0, since at 0 0 it is not affected by any multipliers at all

ok, so both -((y2 - y0) / (x2 - x0)) and -((x1 - x0) / (y1 - y0)) are basically the same thing, just one for x and one for y. really, y2 - y0 is just the y position of the 3rd coordinate (everything starts at x0 and y0, so y2 is the 3rd point). y0 has to be subtracted because we haven't actually shifted the coordinate of (x0, y0) to (0, 0) yet, so this makes up for that.

ok, and now the most interesting part - why do we multiply it by the ratio of the 3rd y coordinate to the 3rd x coordinate? unfortunately, i have no clue. this is just a pattern i noticed while messing around and it worked. maybe someone smart can explain.

the if statement just checks that we won't get a denominator that is equal to 0, and just swaps using y1 and y2, and x1 and x2.

ok, so now we have a right triangle with a point at (0, 0) such that it has one leg on the x axis and one on the y.

this means it's super easy to transform to any shape now - since 0, 0, isnt changed, and since the legs intercept the axes, they can be all adjusted individually

the next part is

  var width = transform[0] * x1 + transform[2] * y1 - (transform[0] * x0 + transform[2] * y0);
  var height = transform[1] * x1 + transform[3] * y1 - (transform[1] * x0 + transform[3] * y0);
  var size = Math.abs(width) > Math.abs(height) ? width : height;

this gets the size of the legs. since the triangle is isosceles, we only need the length of one side. to do that, it uses the description of transform on mdn to find the new coordinates of one of the points. it then checks which absval is lower - the x or the y coordinate. this is effectively checking which is 0, to choose the coordinate which is the length; it instead checks which absolute value is lower though, because due to rounding errors, it might not be exactly a right triangle and the point might not lie on the x or y axis. checking which one is closer to zero works instead.

it then calculates the second transform. I actually wrote this a while ago, so i don't remember how it works, and honestly i am too lazy to figure out. it's a lot simpler though so shouldn't be as complicated

finally, it returns all the 3 matrices, multiplied together, i think. maybe it does something wrong. and all 3 is transform * transform2 * [1, 0, 0, 1, -x0, -y0], because the last one is the offset i mentioned in step one.

if you read all this i am sorry for you. i probably explained this so terribly it was not useful at all but if you are genuinely interested whatever didnt make sense i could try to actually figure out instead of just saying "idk" if you want to message me abt it :3

you can use this however you want just pls credit enty.nekoweb.org. this could be by adding a comment like //! https://enty.nekoweb.org/blog/2024/9/23.htm

thank you for reading!!!


Tested on Firefox 130.0.1 Windows 64-bit

Cool websites!!!

morikei max's apartment remblanc trademarkization company of 2003 axel starry lilly mars

webrings




random

What Dere Type Are You? Valid HTML 4.01 Strict Valid CSS! nekoweb