ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Computer Graphics] 3D 변환 행렬과 행렬 분해
    Computer Graphic 2023. 1. 31. 17:27
    반응형

    1. 3D 변환 행렬의 구성

      3D 공간에서의 변환은 보통 4x4 매트릭스를 통해 이루어집니다.(homogenous coordinate라고 표현하며 하나의 행렬을 통해 여러 종류의 연산을 나타낼 수 있다는 장점을 가집니다.) OpenGL에서는 아래 이미지와 같이 4x4 매트릭스를 이차원 배열이 아닌 16개의 원소를 갖는 1차원 배열을 통해 구현하며, column-primary layout을 취합니다. 즉, 배열의 첫번째 네 원소가 매트릭스의 첫번째 열(A00 - A03)에 대응됩니다. 

     

     

     

      3d 변환 행렬은 네 개의 열벡터로 이루어지며,. 앞 부분의 세 열벡터는 rotation과 scale에 대한 정보를, 마지막 열벡터는 translation에 대한 정보를 담고 있습니다. 매트릭스는 각 변환에 대한 정보를 담고 있습니다. 따라서 각 정점에 대한 벡터에 변환에 대한 정보를 담고 있는 매트릭스를 곱하면 변환이 이루어집니다. 이때, 주의해야할 점은 교환 법칙이 성립하지 않기에 아래의 그림에 알 수 있듯이 rotation 적용 후 translation을 적용한 (a)와 translation 적용 후 rotation을 적용한 (b)는 다른 결과를 가진다는 것입니다.

     

     

     

    2. 3D 변환 종류에 따른 행렬

      각 변환에 대응하는 매트릭스를 확인하기 전에 위치벡터와 방향벡터에 대한 개념을 간단히 소개하겠습니다. 위치벡터란 시점을 원점에 두고 종점을 특정 점에 두어 해당 점의 위치를 나타내는 벡터이고, 방향 벡터는 시점의 위치에 제한이 없으며 고정된 크기와 방향을 가지는 벡터입니다. 전자의 경우 좌표 공간에서 각종 연산을 적용하는데 사용되고 보통의 경우 [x, y, z, 1] 이 네가지로 원소로 구성됩니다. 후자의 경우 물리적 양을 나타내는데 주로 사용되며 [x, y, z] 혹은 [x , y, z, 0]와 같이 구성됩니다. (각 벡터가 4차원으로 표현되는 이유는 연산의 편의성을 위함이고 네 번째 원소에 따라 차이가 있습니다.)

     

    1) translation

     

     

     

      translation에 사용되는 매트릭스는 위와 같으며, tx, ty, tz는 각각 x축, y축, z축 방향 이동 값을 나타냅니다. 변환행렬을 특정 위치벡터 [x, y, z, 1]에 곱해주면 결과는 [x + tx, y + ty, z + tz, 1]으로 비교적 쉽게 의도한 연산을 수행할 수 있습니다. 이 부분이 바로 변환 행렬이 4x4꼴인 이유이며 위치벡터를 [x, y, z, 1]과 같이 표현하게 된 이유입니다. 좌표 (x, y, z)를 (x, y, z, 1)과 같이 표현하는 것을 homogeneous coordinates라 하며 이러한 좌표계를 사용함으로써 각종 변환(affine 변환 등)을 단일행렬로 표현할 수 있는 이점을 가집니다.

     

    2) rotation

    (1) 기본 회전 행렬

     

     

      위의 행렬 Rx, Ry, Rz는 각각 x, y, z축에서 오른손 법칙에 따르는 회전 변환 매트릭스입니다. 여러 축을 복합적으로 회전하는 경우 경우 두 축 혹은 세 축을 거쳐 이루어지는데 이때 연산은 순서가 결과에 영향을 미치므로 회전 순서에 따라 다른 변환 행렬을 갖습니다. 행렬 RzRyRx는 x축 - y축 - z축 순의 회전에 대한 행렬이며 각각의 회전을 나타내는 용어는 roll, pitch, yaw입니다. (순서와 반대로 ZYX 회전이라 합니다.) 

     

    (2) 회전 축과 각으로 표현된 회전행렬

     

     

      벡터 u = (ux, uy, uz) 일 때, 벡터 u를 중심축에 대한 θ 만큼의 회전변환 행렬은 R과 같습니다. 이때 R과 u의 관계는 위 두 번째, 세 번째 예시를 통해 알 수 있습니다. 보다 자세한 유도 과정은 아래 링크에서 확인 가능합니다. 

     

     

    Rotation matrix - Wikipedia

     

    en.wikipedia.org

     

      앞서 위에서 주어진 행렬 R과 벡터 u의 관계를 통해 js에서 쉽게 각 요소의 rotate3d에 대한 데이터를 얻을 수 있습니다. 이는 아래 decomposition에서 확인하시면 됩니다. 이러한 회전변환 행렬은 homogeneous coordinates로 표현함으로써 translation, scaling과 함께 하나의 행렬로 표현 가능합니다.

     

    3) scaling

     

     

      스케일링에 사용되는 변환 행렬을 위와 같으며,  sx, sy, sz는 각각 x, y, z 차원에 대응하는 scaling factor입니다. 각 축의 scaling factor는 각 행의 첫 번째 부터 세 번째 원소를 제곱하여 루트를 씌운 값으로 첫 번째 행은 ,x, 두 번째 행은 y, 세 번째 행은 z에 대응합니다.

     

    3. 3D 변환 행렬의 분해 ( JS )

    (1) translation factor 추출

     

    class Transform {
      constructor(ele) {
        this.ele = ele;
        this.matrix3d = window
          .getComputedStyle(ele)
          .getPropertyValue("transform") // column priority
          .split("(")[1]
          .split(")")[0]
          .split(",")
          .map((ele) => Number(ele));
      }
    
      getTranslateFactors() {
        if (this.matrix3d.length === 6)
          return {
            tx: this.matrix3d[4].toFixed(2),
            ty: this.matrix3d[5].toFixed(2),
          };
        if (this.matrix3d.length === 16)
          return {
            tx: this.matrix3d[12].toFixed(2),
            ty: this.matrix3d[13].toFixed(2),
            tz: this.matrix3d[14].toFixed(2),
          };
      }
    }

     

     

    (2) scale factor 추출

     

    class Transform {
      constructor(ele) {
        this.ele = ele;
        this.matrix3d = window
          .getComputedStyle(ele)
          .getPropertyValue("transform") // column priority
          .split("(")[1]
          .split(")")[0]
          .split(",")
          .map((ele) => Number(ele));
      }
    
      getScaleFactors() {
        if (this.matrix3d.length === 6)
          return {
            sx: Math.pow(
              this.matrix3d[0] ** 2 + this.matrix3d[2] ** 2,
              0.5
            ).toFixed(2),
            sy: Math.pow(
              this.matrix3d[1] ** 2 + this.matrix3d[3] ** 2,
              0.5
            ).toFixed(2),
          };
        if (this.matrix3d.length === 16)
          return {
            sx: Math.pow(
              this.matrix3d[0] ** 2 + this.matrix3d[4] ** 2 + this.matrix3d[8] ** 2,
              0.5
            ).toFixed(2),
            sy: Math.pow(
              this.matrix3d[1] ** 2 + this.matrix3d[5] ** 2 + this.matrix3d[9] ** 2,
              0.5
            ).toFixed(2),
            sz: Math.pow(
              this.matrix3d[2] ** 2 +
                this.matrix3d[6] ** 2 +
                this.matrix3d[10] ** 2,
              0.5
            ).toFixed(2),
          };
      }
    }

     

     

    (3) rotation factor 추출

     

    class Transform {
      constructor(ele) {
        this.ele = ele;
        this.matrix3d = window
          .getComputedStyle(ele)
          .getPropertyValue("transform") // column priority
          .split("(")[1]
          .split(")")[0]
          .split(",")
          .map((ele) => Number(ele));
      }
    
      getScaleFactors() {
        if (this.matrix3d.length === 6)
          return {
            sx: Math.pow(
              this.matrix3d[0] ** 2 + this.matrix3d[2] ** 2,
              0.5
            ).toFixed(2),
            sy: Math.pow(
              this.matrix3d[1] ** 2 + this.matrix3d[3] ** 2,
              0.5
            ).toFixed(2),
          };
        if (this.matrix3d.length === 16)
          return {
            sx: Math.pow(
              this.matrix3d[0] ** 2 + this.matrix3d[4] ** 2 + this.matrix3d[8] ** 2,
              0.5
            ).toFixed(2),
            sy: Math.pow(
              this.matrix3d[1] ** 2 + this.matrix3d[5] ** 2 + this.matrix3d[9] ** 2,
              0.5
            ).toFixed(2),
            sz: Math.pow(
              this.matrix3d[2] ** 2 +
                this.matrix3d[6] ** 2 +
                this.matrix3d[10] ** 2,
              0.5
            ).toFixed(2),
          };
      }
      
      getRotateFactors() {
        let scaleFactors = Object.values(this.getScaleFactors());
        let dimension = scaleFactors.length;
        let tempMatrix = this.matrix3d.slice();
        let deg;
    
        if (dimension === 2) {
          [0, 1].forEach((ele) => {
            for (let i = 0; i < 2; i++) {
              tempMatrix[ele + 2 * i] /= scaleFactors[ele];
            }
          });
    
          deg = (Math.acos(tempMatrix[0]) * 180) / Math.PI;
    
          return { u: undefined, deg: deg.toFixed(2) + "deg" };
        } else if (dimension === 3) {
          [0, 1, 2].forEach((ele) => {
            for (let i = 0; i < 3; i++) {
              tempMatrix[ele + 4 * i] /= scaleFactors[ele];
            }
          });
    
          let u = [
            tempMatrix[6] - tempMatrix[9],
            tempMatrix[8] - tempMatrix[2],
            tempMatrix[1] - tempMatrix[4],
          ];
    
          if (u[0].toFixed(2) != 1) {
            deg =
              (Math.acos((tempMatrix[0] - u[0] ** 2) / (1 - u[0] ** 2)) * 180) /
              Math.PI;
          } else if (u[1].toFixed(2) != 1) {
            deg =
              (Math.acos((tempMatrix[5] - u[1] ** 2) / (1 - u[1] ** 2)) * 180) /
              Math.PI;
          } else if (u[2].toFixed(2) != 1) {
            deg =
              (Math.acos((tempMatrix[10] - u[2] ** 2) / (1 - u[2] ** 2)) * 180) /
              Math.PI;
          }
    
          return {
            u: u.map((ele) => ele.toFixed(2)).join(","),
            deg: deg.toFixed(2) + "deg",
          };
        }
      }
    }

     

     

    (4) 전체 코드

     

    class Transform {
      constructor(ele) {
        this.ele = ele;
        this.matrix3d = window
          .getComputedStyle(ele)
          .getPropertyValue("transform") // column priority
          .split("(")[1]
          .split(")")[0]
          .split(",")
          .map((ele) => Number(ele));
      }
    
      show() {
        let temp = this.matrix3d.slice().map((ele) => Number(ele.toFixed(2)));
    
        if (temp.length === 6) {
          console.log(
            `
            ${temp[0]} / ${temp[2]} / ${temp[4]}
            ${temp[1]} / ${temp[3]} / ${temp[5]}
            0 / 0 / 1
            `
          );
        } else if (temp.length === 16) {
          console.log(
            `
            ${temp[0]} / ${temp[4]} / ${temp[8]} / ${temp[12]}
            ${temp[1]} / ${temp[5]} / ${temp[9]} / ${temp[13]}
            ${temp[2]} / ${temp[6]} / ${temp[10]} / ${temp[14]}
            ${temp[3]} / ${temp[7]} / ${temp[11]} / ${temp[15]}
            `
          );
        }
    
        return;
      }
    
      showAllFactors() {
        let t = this.getTranslateFactors();
        let s = this.getScaleFactors();
        let r = this.getRotateFactors();
    
        console.log("\n--------------translation--------------\n");
        Object.keys(t).forEach((ele) => console.log(`${ele} : ${t[ele]}`));
        console.log("\n---------------scaling-----------------\n");
        Object.keys(s).forEach((ele) => console.log(`${ele} : ${s[ele]}`));
        console.log("\n---------------rotation----------------\n");
        Object.keys(r).forEach((ele) => console.log(`${ele} : ${r[ele]}`));
      }
    
      getTranslateFactors() {
        if (this.matrix3d.length === 6)
          return {
            tx: this.matrix3d[4].toFixed(2),
            ty: this.matrix3d[5].toFixed(2),
          };
        if (this.matrix3d.length === 16)
          return {
            tx: this.matrix3d[12].toFixed(2),
            ty: this.matrix3d[13].toFixed(2),
            tz: this.matrix3d[14].toFixed(2),
          };
      }
    
      getScaleFactors() {
        if (this.matrix3d.length === 6)
          return {
            sx: Math.pow(
              this.matrix3d[0] ** 2 + this.matrix3d[2] ** 2,
              0.5
            ).toFixed(2),
            sy: Math.pow(
              this.matrix3d[1] ** 2 + this.matrix3d[3] ** 2,
              0.5
            ).toFixed(2),
          };
        if (this.matrix3d.length === 16)
          return {
            sx: Math.pow(
              this.matrix3d[0] ** 2 + this.matrix3d[4] ** 2 + this.matrix3d[8] ** 2,
              0.5
            ).toFixed(2),
            sy: Math.pow(
              this.matrix3d[1] ** 2 + this.matrix3d[5] ** 2 + this.matrix3d[9] ** 2,
              0.5
            ).toFixed(2),
            sz: Math.pow(
              this.matrix3d[2] ** 2 +
                this.matrix3d[6] ** 2 +
                this.matrix3d[10] ** 2,
              0.5
            ).toFixed(2),
          };
      }
    
      getRotateFactors() {
        let scaleFactors = Object.values(this.getScaleFactors());
        let dimension = scaleFactors.length;
        let tempMatrix = this.matrix3d.slice();
        let deg;
    
        if (dimension === 2) {
          [0, 1].forEach((ele) => {
            for (let i = 0; i < 2; i++) {
              tempMatrix[ele + 2 * i] /= scaleFactors[ele];
            }
          });
    
          deg = (Math.acos(tempMatrix[0]) * 180) / Math.PI;
    
          return { u: undefined, deg: deg.toFixed(2) + "deg" };
        } else if (dimension === 3) {
          [0, 1, 2].forEach((ele) => {
            for (let i = 0; i < 3; i++) {
              tempMatrix[ele + 4 * i] /= scaleFactors[ele];
            }
          });
    
          let u = [
            tempMatrix[6] - tempMatrix[9],
            tempMatrix[8] - tempMatrix[2],
            tempMatrix[1] - tempMatrix[4],
          ];
    
          if (u[0].toFixed(2) != 1) {
            deg =
              (Math.acos((tempMatrix[0] - u[0] ** 2) / (1 - u[0] ** 2)) * 180) /
              Math.PI;
          } else if (u[1].toFixed(2) != 1) {
            deg =
              (Math.acos((tempMatrix[5] - u[1] ** 2) / (1 - u[1] ** 2)) * 180) /
              Math.PI;
          } else if (u[2].toFixed(2) != 1) {
            deg =
              (Math.acos((tempMatrix[10] - u[2] ** 2) / (1 - u[2] ** 2)) * 180) /
              Math.PI;
          }
    
          return {
            u: u.map((ele) => ele.toFixed(2)).join(","),
            deg: deg.toFixed(2) + "deg",
          };
        }
      }
    
      setSameTransformTo(ele) {
        let t = this.getTranslateFactors();
        let s = this.getScaleFactors();
        let r = this.getRotateFactors();
        console.log(t, s, r);
    
        ele.style.transform = `translate3d(${t["tx"]}px, ${t["ty"]}px, ${
          t["tz"]
        }px) scale3d(${s["sx"] + "," + s["sy"] + "," + s["sz"]}) rotate3d(${
          r["u"]
        }, ${r["deg"]})`;
      }
    }
    반응형

    댓글

Designed by Tistory.