Canvas를 사용하여 3D 그래픽을 만드는 방법

.NET Framework 3.0

이 항목에서는 HTML5의 canvas 요소를 사용하여 3D 개체를 조작하는 방법에 대해 설명합니다.

참고  이 항목에서 제공하는 샘플을 보려면 canvas 요소를 지원하는 브라우저(예: Windows Internet Explorer 9 이상)가 필요합니다.

컴퓨터 모니터 등의 디스플레이 장치에 다양한 보기를 표시하여 3D 개체를 시각화하고자 한다고 가정합니다. 또한 표시할 개체는 완전히 (x, y, z) 점으로 구성된다고 가정합니다.

표면의 점 기반 표현.

그림 1

원점이 모니터의 중심에 있고 xy 평면이 모니터의 평면과 일치하는 xyz 좌표계에 개체를 포함하는 경우, 관찰자는 2D xy 평면 또는 모니터에 투영된 3D 개체의 모습을 보게 됩니다.

xy 평면/모니터에 투영된 표면.

그림 2

그림 1의 개체는 수학적 표면을 점 기반으로 표현한 것입니다.

표면 수식 z = f(x, y).

그림 2의 이미지를 만들기 위해 xy 평면에 고르게 분산된 8100개의 (x, y) 점이 사용되었고, 제공된 수식 ƒ(x, y)를 사용해 xy 평면 위 표면의 z 높이를 계산했습니다. 다시 말해, xyz 좌표계의 각 표면 점(x, y, z)은 (x, y, ƒ(x, y))로 제공됩니다.

이와는 별도로, 위의 표면을 위에서 내려다보면 "평평하게" 보입니다. 즉, z축을 위에서 직접 내려다보면 표면의 윤곽 그리기가 보입니다.

z-축을 내려다보는 표면의 보기

그림 3

이 표면의 윤곽 그리기는 표면의 z 좌표를 계산하는 데 사용된 8100개의 고르게 분산된 xy 점을 보여 줍니다. 윤곽 그리기는 그 자체로도 매우 유용할 수 있지만(예: 지형도), 그림 1과 2에서 설명한대로 x, yz축 주위로 3D 점을 회전할 경우에만 3D 개체의 진정한 속성을 볼 수 있습니다. 여기에서, 본질적으로 2D인 디스플레이 장치에서 어떻게 3D 점을 표시하고 회전하는가 하는 질문이 생깁니다.

이 질문에 답하기 위해 canvas 요소를 사용합니다. 이 요소는 HTML5 규격 브라우저(예: Internet Explorer 9 이상)에서 2D 점을 빠르게 그리는 기능을 제공합니다. canvas 요소는 이 글을 쓰는 시점에 2D 개체의 조작만 지원합니다. 예를 들면 canvas rotate 메서드는 평평한 2D 개체만 회전하도록 설계되었습니다. 따라서 위의 표면 z = f(x, y) 등의 3D 개체를 그리고 회전하려면 고유한 메서드를 작성해야 합니다. 다행히 처음 두 작업은 간단합니다.

3D 점 그리기

그림 2와 같은 특정 표면 보기의 경우, xy 평면에 대한 표면의 투영만 표시되므로 디스플레이 장치에서 보기를 그리는 데 점의 xy 좌표만 필요합니다. 그러나 특정 변형을 수행하려면 z 좌표를 계속 추적해야 합니다. 이 내용은 다음에 설명합니다.

3D 점 회전

시작하기 위해 먼저 n개의 모든 표면 점(n = 8100)을 3 x n 행렬 P(보기의 좌표 행렬이라고 함)에 배치합니다. P에는 n개의 열이 있으며, 각 열은 표면 점을 나타냅니다.

점 행렬 P.

x, y 또는 z축 주위로 이 표면을 회전하려면 P를 적절한 3 x 3 회전 행렬 R곱합니다.

P' = RP

여기서 P는 표면의 원래 보기이고 P’R에 의해 정의된 좌표축 중 하나 주위로 회전한 후의 보기입니다. 좌표축이 세 개이므로 R에 대한 회전 행렬도 세 개입니다.

세 개의 회전 행렬.

그림 3에 있는 표면을 x축 주위로 π/12라디안(15°) 회전하려는 경우 Rx는 대략 다음과 같습니다.

x-축 회전 행렬.

표면의 새로운 회전된 보기 P’를 만들려면 다음과 같이 행렬 곱셈 RxP를 수행하기만 하면 됩니다.

행렬 Rx 곱하기 P.

그러면 표면을 그리기 위해 P'의 결과 수치(x, y) 점이 디스플레이 장치에 그려집니다. 다음 섹션에서는 이 방법의 구체적인 예를 제공합니다.

3D Canvas 샘플

이 섹션에서는 canvas3dRotation.html 샘플에 대해 설명합니다. 이 샘플과 연결된 소스 코드를 보려면 브라우저의 소스 보기 기능을 사용하세요. 예를 들어 Windows Internet Explorer에서는 보려는 소스 코드의 웹 페이지를 마우스 오른쪽 단추로 클릭하고 소스 보기를 클릭하세요. 이 문서의 나머지 내용을 읽는 동안 사용 가능한 소스 코드가 있는지 확인하세요.

첫 번째 단계는 관심이 있는 표면 ƒ(x, y) 및 xy의 적절한 범위를 선택하는 것입니다. 그림 1과 같이 다음으로 설정합니다.

표면 수식.

ƒ에 대해 사용할 수 있는 범위:

x와 y에 대해 허용되는 값.

다음 단계는 이 18 x 18 xy 평면 영역 위에 그릴 적절한 3D 점의 개수를 결정하는 것입니다. 단위당 5개의 점을 허용하면 (x, y, z) = (x, y, ƒ(x, y)) 형식의 총 5·18 x 5·18 = 8100개 표면 점을 갖게 됩니다.

지정된 xy 범위에 표면의 윤곽 그리기.

그림 4

이 범위 관련 정보는 다음 JavaScript 명령에 보이는 것처럼 전역 constants 변수에 포함됩니다.


var constants = {
  canvasWidth: 600, 
  canvasHeight: 600,
  leftArrow: 37,
  upArrow: 38,
  rightArrow: 39,
  downArrow: 40,
  xMin: -9, // RANGE RELATED
  xMax: 9, // RANGE RELATED
  yMin: -9, // RANGE RELATED
  yMax: 9, // RANGE RELATED
  xDelta: 0.2, // RANGE RELATED
  yDelta: 0.2, // RANGE RELATED
  colorMap: ["#060", "#090", "#0C0", "#0F0", "#9F0", "#9C0", "#990", "#960", "#930", "#900", "#C00"], 
  pointWidth: 2,
  dTheta: 0.05, 
  surfaceScale: 24 
};

8100개의 점이 [ [x0, y0, z0], [x1, y1, z1], …, [x8099, y8099, z8099] ] 형식의 JavaScript 배열에 저장됩니다. 배열은 다음과 같이 초기화됩니다.


Surface.prototype.generate = function()
{
  var i = 0;
  
  for (var x = constants.xMin; x <= constants.xMax; x += constants.xDelta)
  {
    for (var y = constants.yMin; y <= constants.yMax; y += constants.yDelta)
    {
      this.points[i] = point(x, y, this.equation(x, y));
      ++i;
    }
  }
}

여기서 this.equation(x, y)는 다음과 같습니다.

표면 수식.

this.points 배열은 위의 행렬 P와 비슷합니다(즉, 각각 3D 표면 점을 나타내는 3 x 1 열 벡터의 목록). 배열에서 this.points[0][0]는 첫 번째 표면 점의 x 좌표에 액세스하고, this.points[2][2]는 세 번째 점의 z 좌표에 액세스합니다.

모든 표면 점이 생성되었으면 점의 z 좌표를 기반으로 점의 색이 선택됩니다. 즉, 점의 색은 xy 평면 "위"에 있는 점의 "높이"를 기반으로 합니다. 모두 11개의 그러한 "높이" 색(constants.colorMap 배열에 포함됨)이 있으며, 다음과 같이 할당됩니다.


Surface.prototype.color = function()
{
  var z; 
  
  this.zMin = this.zMax = this.points[0][Z];
  for (var i = 0; i < this.points.length; i++)
  {            
    z = this.points[i][Z];
    if (z < this.zMin) { this.zMin = z; }
    if (z > this.zMax) { this.zMax = z; }
  }   
        
  var zDelta = Math.abs(this.zMax - this.zMin) / constants.colorMap.length; 

  for (var i = 0; i < this.points.length; i++)
  {
    this.points[i].color = constants.colorMap[ Math.floor( (this.points[i][Z]-this.zMin)/zDelta ) ];
  }
}

이 메서드의 가장 복잡한 부분은 다음의 for 루프입니다.


for (var i = 0; i < this.points.length; i++)
{
  this.points[i].color = constants.colorMap[ Math.floor( (this.points[i][Z]-this.zMin)/zDelta ) ];
}

constants.colorMap에 다음과 같은 배열 리터럴이 포함되어 있음을 기억하면 이 루프를 이해하는 데 도움이 됩니다.


["#060", "#090", "#0C0", "#0F0", "#9F0", "#9C0", "#990", "#960", "#930", "#900", "#C00"]

다시 말해, 이 for 루프는 기능적으로 다음의(훨씬 덜 멋진) 루프와 같습니다.


for (var i = 0; i < this.points.length; i++)
{
  if (this.points[i][Z] <= this.zMin + zDelta) {this.points[i].color = "#060";}
  else if (this.points[i][Z] <= this.zMin + 2*zDelta) {this.points[i].color = "#090";}
  else if (this.points[i][Z] <= this.zMin + 3*zDelta) {this.points[i].color = "#0C0";}
  else if (this.points[i][Z] <= this.zMin + 4*zDelta) {this.points[i].color = "#0F0";}
  else if (this.points[i][Z] <= this.zMin + 5*zDelta) {this.points[i].color = "#9F0";}
  else if (this.points[i][Z] <= this.zMin + 6*zDelta) {this.points[i].color = "#9C0";}
  else if (this.points[i][Z] <= this.zMin + 7*zDelta) {this.points[i].color = "#990";}
  else if (this.points[i][Z] <= this.zMin + 8*zDelta) {this.points[i].color = "#960";}
  else if (this.points[i][Z] <= this.zMin + 9*zDelta) {this.points[i].color = "#930";}
  else if (this.points[i][Z] <= this.zMin + 10*zDelta) {this.points[i].color = "#900";}          
  else {this.points[i].color = "#C00";}
}

위에서 볼 수 있듯이 최저 z 좌표를 나타내는 색은 #060(어두운 녹색)입니다. #060은 최고 z 좌표를 나타내는 #C00(중간 빨강)으로 "점점" 바뀝니다. 다시 말하면, 각 점의 z 좌표를 기반으로 점들 사이에 11개의 "높이" 색을 선형으로 배포(수직의 개념)하는 데 최소 및 최대 z 좌표(this.zMinthis.zMax)가 사용됩니다.

이제 points 배열에는 표면의 보기를 그리는 데 필요한 모든 정보가 포함되어 있습니다. 이를 위해 다음과 같이 canvas 요소를 프로그래밍 방식으로 만듭니다.


function appendCanvasElement()
{
  var canvasElement = document.createElement('canvas');
  
  canvasElement.width = constants.canvasWidth;
  canvasElement.height = constants.canvasHeight;
  canvasElement.id = "myCanvas";

  canvasElement.getContext('2d').translate(constants.canvasWidth/2, constants.canvasHeight/2); 
  
  document.body.appendChild(canvasElement);
}

translate(constants.canvasWidth/2, constants.canvasHeight/2) 메서드를 사용해 canvas의 가운데에 xyz 좌표계를 중앙 배치합니다.

이제 적절한 canvas 요소를 만들었으므로 여기에 각 점을 그릴 수 있습니다.


Surface.prototype.draw = function()
{
  var myCanvas = document.getElementById("myCanvas");
  var ctx = myCanvas.getContext("2d");

  this.points = surface.points.sort(surface.sortByZIndex);

  for (var i = 0; i < this.points.length; i++)
  {
    ctx.fillStyle = this.points[i].color; 
    ctx.fillRect(this.points[i][X] * constants.surfaceScale, this.points[i][Y] * constants.surfaceScale, constants.pointWidth, constants.pointWidth);  
  }    
}

즉, points 배열에 있는 각각의 3 x 1 점에 대해 작은 색 직사각형을 만들고(작은 원을 렌더링하는 것보다 훨씬 빠름), 경험적으로 파생된 상수와 곱한 점의 xy 좌표를 사용해 배치합니다. 중요한 줄은 다음과 같습니다.


ctx.fillRect(this.points[i][X] * constants.surfaceScale, this.points[i][Y] * constants.surfaceScale, constants.pointWidth, constants.pointWidth);

모든 가능한 보기에 대해 표면을 canvas에 시각적으로 맞추기 위해 표면의 크기를 조정하는 데 경험적으로 파생된 상수 constants.surfaceScale이 사용됩니다.

디스플레이 장치에 점(직사각형)을 그리기 시작하기 전에 상대적인 z-축 위치를 기준으로 점을 정렬합니다. 이렇게 하면 보는 이의 눈과 가장 먼 점이 첫 번째로 그려지고, 가장 가까운 점이 마지막으로 그려집니다. 점의 너비(pointWidth)가 작으면(1-2픽셀) 이러한 정렬 효과가 눈에 띄지 않습니다. 그러나 this.points = surface.points.sort(surface.sortByZIndex);를 주석으로 처리하고 pointWidth를 5로 높이면 표면을 회전할 때 뜻밖의 시각적 효과가 나타납니다. 이제 주석을 제거하고 페이지를 새로 고치면 표면이 예상대로 회전됩니다.

이제 표면의 보기를 그릴 수 있으므로 회전을 통해 다음과 같이 보기를 변경할 수 있습니다.


Surface.prototype.multi = function(R)
{
  var Px = 0, Py = 0, Pz = 0; 
  var P = this.points; 
  var sum; 

  for (var V = 0; V < P.length; V++) 
  {
    Px = P[V][X], Py = P[V][Y], Pz = P[V][Z];
    for (var Rrow = 0; Rrow < 3; Rrow++) 
    {
      sum = (R[Rrow][X] * Px) + (R[Rrow][Y] * Py) + (R[Rrow][Z] * Pz);
      P[V][Rrow] = sum;
    }
  }     
}

이 메서드는 행렬 곱셈 RP를 수행하여 새로운 보기 P’를 만듭니다(앞서 설명함). 좀 더 정확히 말하면, 이 메서드는 P = RP을 수행하며, 여기서 "="는 JavaScript 할당 연산자를 나타냅니다.

Rx, y 또는 z축 주위의 회전을 설명합니다. 즉, multi 메서드로 전달되는 RRx, Ry 또는 Rz입니다(앞서 설명함). Rx, RyRz는 각각 xRotate, yRotatezRotate 메서드를 통해 구현됩니다. 예를 들어 Ry는 다음과 같이 구현됩니다.


Surface.prototype.yRotate = function(sign) 
{
  var Ry = [ [0, 0, 0],
             [0, 0, 0],
             [0, 0, 0] ];
                     
  Ry[0][0] = Math.cos( sign*constants.dTheta );
  Ry[0][1] = 0; 
  Ry[0][2] = Math.sin( sign*constants.dTheta );
  Ry[1][0] = 0; 
  Ry[1][1] = 1;
  Ry[1][2] = 0; 
  Ry[2][0] = -Math.sin( sign*constants.dTheta );
  Ry[2][1] = 0; 
  Ry[2][2] = Math.cos( sign*constants.dTheta );
  
  this.multi(Ry); 
  this.erase(); 
  this.draw();
}

호출하면 yRotate는 표면을 y축 주위로 작은 각도 양인 constants.dTheta만큼 회전합니다. 즉, yRotate는 먼저 Ry를 만든 다음 this.multi(Ry)를 호출하여 RyP를 수행합니다. 따라서 표면을 y축 주위로 sign*constants.dTheta 라디안 회전합니다. y축 주위로 시계 방향 또는 시계 반대 방향으로 회전하는 데 sign 매개 변수가 사용됩니다.

예상할 수 있듯이 x, y 또는 z축 주위의 시계 방향 또는 시계 반대 방향으로 여섯 가지 가능한 회전이 있습니다. 따라서 instructions.html 페이지에서 설명했듯이, 원하는 회전을 표시하는 데 여섯 개의 화살표 키 조합이 사용됩니다.


function processKeyDown(evt)
{                    
  if (evt.ctrlKey)
  {
    switch (evt.keyCode)
    {
      case constants.upArrow: 
        // No operation other than preventing the default behavior of the arrow key.
        evt.preventDefault();
        break;
      case constants.downArrow:
        // No operation other than preventing the default behavior of the arrow key.
        evt.preventDefault();
        break;
      case constants.leftArrow:
        surface.zRotate(-1); 
        evt.preventDefault(); 
        break;
      case constants.rightArrow:
        surface.zRotate(1);
        evt.preventDefault(); 
        break;
    }
    return; 
  }

  switch (evt.keyCode)
  {
    case constants.upArrow:
      surface.xRotate(1);
      evt.preventDefault(); 
      break;
    case constants.downArrow:
      surface.xRotate(-1); 
      evt.preventDefault(); 
      break;
    case constants.leftArrow:
      surface.yRotate(-1);  
      evt.preventDefault(); 
      break;
    case constants.rightArrow:
      surface.yRotate(1);   
      evt.preventDefault(); 
      break;
  }
}

예를 들어 Ctrl 키와 왼쪽 화살표 키를 동시에 누르면 다음 코드가 실행됩니다.


case constants.leftArrow:
  surface.zRotate(-1);  
  evt.preventDefault();  
  break;


surface.zRotate(-1)는 표면을 z축 주위의 시계 반대 방향으로 회전합니다. evt.preventDefault()는 스크롤 막대가 있을 때 브라우저의 창을 스크롤하는 화살표 키의 기본 동작을 차단합니다. 사용자는 여전히 마우스로 창을 스크롤할 수 있습니다.

나머지 구현 세부 정보도 sample 내에 포함된 다수의 자세한 설명과 함께 제공됩니다.

연습

이 섹션에서는 두 가지 핵심 연습을 제안합니다. 첫 번째는 표면에 있는 점의 크기와 수를 줄이는 것이고, 두 번째는 추가 표면 변형을 구현하는 것입니다.

표면 축소

이 샘플에 있는 표면 점의 개수는 8100개로 다소 많습니다. 성능이 떨어지는 하드웨어에서 표면을 렌더링할 경우 느리거나 고르지 못하거나 이 두 가지 모두 발생할 수 있습니다. 그런 경우 전체 점의 개수를 줄이면 성능 향상에 도움이 됩니다. 이렇게 하려면 다음의 JavaScript 설명에 나와 있듯이 다음의 상수를 조정할 수 있습니다.


var constants = {
  canvasWidth: 600, // ADJUST
  canvasHeight: 600, // ADJUST
  leftArrow: 37,
  upArrow: 38,
  rightArrow: 39,
  downArrow: 40,
  xMin: -9,
  xMax: 9,
  yMin: -9,
  yMax: 9,
  xDelta: 0.2, // ADJUST
  yDelta: 0.2, // ADJUST
  colorMap: ["#060", "#090", "#0C0", "#0F0", "#9F0", "#9C0", "#990", "#960", "#930", "#900", "#C00"], 
  pointWidth: 2,
  dTheta: 0.05, 
  surfaceScale: 24 // ADJUST
};

canvas의 크기를 줄이려면 canvasWidthcanvasHeight를 줄입니다. 전체 점의 개수를 줄이려면 xDeltayDelta를 늘립니다. 이렇게 조정한 후, 렌더링된 표면이 canvas에 잘 맞도록 경험적으로 surfaceScale을 줄입니다. 예를 들면 다음 상수 집합은 총 3721개의 점으로 400 x 400 canvas에 잘 맞는 표면을 만듭니다(JavaScript 설명 참조).


var constants = {
  canvasWidth: 400, // 600 TO 400
  canvasHeight: 400, // 600 TO 400
  leftArrow: 37,
  upArrow: 38,
  rightArrow: 39,
  downArrow: 40,
  xMin: -9,
  xMax: 9,
  yMin: -9,
  yMax: 9,
  xDelta: 0.3, // 0.2 TO 0.3
  yDelta: 0.3, // 0.2 TO 0.3
  colorMap: ["#060", "#090", "#0C0", "#0F0", "#9F0", "#9C0", "#990", "#960", "#930", "#900", "#C00"], 
  pointWidth: 2,
  dTheta: 0.05, 
  surfaceScale: 16 // 24 TO 16
};

이러한 상수로 계속 실험해보세요. pointWidthdTheta를 포함할 수도 있습니다. dTheta는 키를 누를 때마다 특정 축 주위로 표면을 회전하는 양입니다. 표면의 "확대/축소 배율"을 조정하려면 xMin, xMax, yMinyMax로 실험합니다.

크기 조정 및 변환

앞서 설명했듯이, 축 주위로 표면을 회전하려면 다음을 수행합니다.

P’ = RP

마찬가지로, 표면의 크기 조정에도 동일한 행렬 곱셈을 수행할 수 있지만, 이번에는 다른 3 x 3 변형 행렬 S를 사용합니다.

크기 조정 행렬 곱하기 P 행렬.

여기서 S는 각각 α, βγ 요소로 x, yz 방향에 따라 표면 보기의 크기를 조정합니다. 예를 들어 x 방향에서 표면 크기를 두 배로 만들려면 다음을 사용합니다.

크기 조정 행렬 S.

마지막으로 고려해볼 변형은 변환(또는 변위)입니다. 즉, 디스플레이 장치의 새 위치로 표면을 이동합니다. 표면의 보기를 변환하려면 각 점 좌표에 상수(하나 이상은 0일 수 있음)를 추가하여 각 표면 점(xi, yi, zi)을 새 점으로 대체합니다.

(xi + dx, yi + dy, zi + dz)

여기서 dx, dy 및 dz는 각각 x, yz 방향의 원하는 변위를 나타냅니다. 예를 들어 표면을 양의 x 방향으로 3 단위 이동하려면 dx = 3, dy = 0 및 dz = 0을 사용합니다. 즉, 각 표면 점의 x좌표에 3을 추가하기만 하면 됩니다.

행렬 관점에서 보면 행렬 더하기 및 3 x n 변환 행렬 T를 사용해 변환을 수행할 수 있습니다.

P 더하기 T, 변환 행렬.

마지막으로 제안하는 연습은 샘플에 크기 조정 및 변환 옵션을 추가해보는 것입니다. 예를 들면, 표면의 크기 조정 및 변환을 위해 다양한 단추를 클릭할 수 있습니다.

관련 항목

HTML5 그래픽
HTML5 Canvas

 

 

표시:
© 2014 Microsoft. All rights reserved.