박스 그리기
주사위를 그리기 위한 기초 작업으로 3차원 풀그림의 기본을 보기를 들어 가며 이야기를 전개하기로 한다. 먼저 보통 박스로 불리는 직6면체(cuboid)를 그려 본다. 직6면체는 8개의 꼭지점과 6개의 면이 있다. 직6면체의 바닥을 1로 잡았을 때 높이와 깊이를 각각 rh(height), rd(depth)로 표시한다. 그러면 직6면체의 한 가운데에 원점을 둔 자리표계에서 8개의 꼭지점의 자리표는 [rh,1, -rd], [rh, -1, -rd], [-rh, -1, -rd], [-rh, 1, -rd], [rh, 1, rd], [rh, -1, rd], [-rh, -1, rd], [-rh, 1, rd] 가 된다. 이것을 aCuboid 라는 배열로 만들면 aCuboid[0] = [rh,1, -rd], aCuboid[1] = [rh, -1, -d],....aCuboid[7] = [-rh, 1, rd]가 된다. 아래의 코드리스팅<표6>은 박스를 하나 만들어 그리는 CreateCuboidWire라는 클래스이다.
1 |
class CreateCuboidWire{ |
2 |
var mc:MovieClip; |
3 |
var size:Number; |
4 |
var colA:Array; |
5 |
var rh:Number; |
6 |
var rd:Number; |
7 |
var rotMat:Rotation3D; |
8 |
private var aCuboid:Array; |
9 |
private var aCuboidRot:Array; |
10 |
private var cornsIndex:Array = new Array([0, 1, 2, 3], [0, 4, 5, 1], [1, 5, 6, 2], [0, 3, 7, 4], [2, 6, 7, 3], [4, 7, 6, 5]); |
11 |
private var surface:Array = new Array([[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]); |
12 |
|
13 |
// constructor 속성 변수를 초기화한다. |
14 |
function CreateCuboidWire(cuboidMc:MovieClip, cuboidSize:Number, rh1:Number, rd1:Number , colorArray:Array) { |
15 |
mc = cuboidMc; |
16 |
size = cuboidSize; |
17 |
colA = colorArray; |
18 |
rh =rh1; |
19 |
rd = rd1; |
20 |
aCuboid = new Array([rh,1, -rd], [rh, -1, -rd], [-rh, -1, -rd], [-rh, 1, -rd], [rh, 1, rd], [rh, -1, rd], [-rh, -1, rd], [-rh, 1, rd]); |
21 |
aCuboidRot = new Array([rh,1, -rd], [rh, -1, -rd], [-rh, -1, -rd], [-rh, 1, -rd], [rh, 1, rd], [rh, -1, rd], [-rh, -1, rd], [-rh, 1, rd]); |
22 |
rotMat = new Rotation3D(); |
23 |
for (var i = 0; i<8; i++) { |
24 |
for (var j = 0; j<3; j++) { |
25 |
aCuboid[i][j] = aCuboid[i][j]*cuboidSize/2; |
26 |
} |
27 |
} |
28 |
} |
29 |
// function CreateCuboid |
30 |
function showCuboid(alp:Number , IsIso:Boolean) { |
31 |
for (var i = 0; i<8; i++) { |
32 |
aCuboidRot[i] = rotMat.transform3D(aCuboid[i]); |
33 |
if (IsIso) { |
34 |
aCuboidRot[i] = Isom.mTSA(aCuboidRot[i]); |
35 |
} |
36 |
} |
37 |
for (var i = 0; i<6; i++) { |
38 |
for (var j = 0; j<4; j++) { |
39 |
for (var k = 0; k<3; k++) { |
40 |
surface[i][j][k] = aCuboidRot[cornsIndex[i][j]][k]; |
41 |
} |
42 |
} |
43 |
} |
44 |
mc.clear(); |
45 |
var x01, x21, y01, y21, zz:Number; |
46 |
for (var i = 0; i<6; i++) { |
47 |
mc.lineStyle(0, colA[i], alp); |
48 |
mc.beginFill(colA[i], alp); |
49 |
mc.moveTo(surface[i][0][0], surface[i][0][1]); |
50 |
for (var j = 1; j<4; j++) { |
51 |
mc.lineTo(surface[i][j][0], surface[i][j][1]); |
52 |
} |
53 |
mc.lineTo(surface[i][0][0], surface[i][0][1]); |
54 |
mc.endFill(); |
55 |
} |
56 |
//for (var i = 0; i< 6; i++) draw surface |
57 |
} |
58 |
// functoin showCuboid() |
59 |
} |
60 |
// class CreateCuboid |
표1 class CreateCuboid
이 코드를 여기에 나열한 것은 이 코드가 3차원 강체를 생성하는 가장 간단한 형태로서 그 뼈대역할을 하기 때문이다. 이 코드를 이해하여야 보다 복잡한 주사위와 같은 강체를 그릴 수 있게 된다. 직6면체를 그리려면 이 8개의 꼭지점의 데이타만 알면 된다. 이 데이터에서 모서리를 그을 수 있고 모서리 4개가 한 면을 결정한다. 따라서 이 꼭지점의 배열이 직 6면체 클래스의 뼈대가 된다. 위 <표1> 에서 cornsIndex 는 6개의 면을 정의하는 배열이다. 첫째면(cornsIndex[0])은 꼭지점의 데이터에서 꼭지점, [0,1,2,3] 으로 구성된다는 의미이다. 여기서 이 꼭지점을 기술하는 순서가 매우 중요하다. 즉 이 순서는 면의 앞뒷면을 가리는 데에 쓰인다. [0,1,2,3]->[1,2,3,0]로 돌렸을 때 나사의 진행방향이 면의 바깥쪽을 향하게 정한다. 나는 이러한 경우 모형을 만들어 작업한다. 직 6면체의 경우 다행히 꼭지가 뭉툭한 주사위가 있어서 거기에 번호를 매기고 이 인덱스를 코딩했다.
그림1) 정20면체의 인덱스 배열을 코딩하기 위해 필자가 만든 정20면체모형
앞마디 (주사위4 강체와 아이소메트릭 투영법)에 삽입된 <그림1>, <그림2>, <그림3>에 보면 꼭지점에 번호가 적혀 있는 것을 볼 것이다. 그것을 참조하여 면의 인덱스를 적어 넣었다. 정20면체와 같은 것은 <그림 1>과 같은 모형을 만들어 인덱스를 코딩하였다. 12개의 꼭지점에서 3개씩 선택하여 20개의 3각형을 만들어 정20면체를 구성한다. 이 모형의 꼭지점에 번호를 적은 수틱커를 부착하여 인덱스 배열을 코딩하였다.
수학적으로는 다면체의 꼭지점으로부터 면을 구성하는 꼭지점을 구하는 공식이 있을지 모른다. 그러나 일반적인 다면체를 그리는 무른모를 만드는 경우가 아니라면 그런 거창한 수학적 지식을 동원하기 보다는 이런 모형을 만들어 보는 것도 재미가 있다. 이제는 사라졌지만 옛날 초등학교 시절, 수수깡으로 공작하던 추억을 되 삭이면서.
30줄의 메쏘드함수 showCuboid는 직6면체를 그리라는 명령인데 여기서 보듯, 8개의 꼭지점만 회전 행렬로 회전
시키고 그 회전된 꼭지점의 데이터를 써서 면을 그린다. 회전을 두 번 거치는데 교육적 목적으로 분리했을 뿐 아이소메트릭 투영변환을 일반 회전행렬 속에 포함 시킬 수도 있다.
40줄의 surface 는 직4각형이므로 4개의 3차원 점으로 구성된다. 다시 말하면 첫 번째 면, surface[0]는 surface[0][0], surface[0][1], surface[0][2], surface[0][3]이라는 4개의 꼭지점으로 구성되는데 각 꼭지점은 3차원 공간의 1점을 나타내는 3요소(x,y,z) 배열이다. 이 점들을 위에 얘기한 cornsIndex를 통해서 구한다. 이렇게 해서 6개면을 다 그리면 우리가 원하는 정6면체가 그려진다. 사실 주사위도 그 원리는 마찬가지이므로 이 직6면체를 플래시로 그릴 수 있으면 어떤 다른 다면체도 그릴 수 있다. 다만 이 직6면체는 반투명 유리상자로 상자의 안쪽면도 보인다. 앞으로 이 문제도 풀어 나갈 것이다.
직교 변환 행렬
주사위와 같은 3차원 입체의 회전을 다루기 위해서는 강체운동학의 기초를 이해할 필요가 있다.
아래의 플래시 무비는 직교회전행렬과 강체의 회전과의 관계를 보이기 위하여 만든 것이다. 버튼을 눌러 직6면체를 회전시키면서 실험해 보기 바란다.
플래시 무비
왼쪽 버튼들은 한번 클릭하면 X,Y,Z 축에 대해 5˚ 씩 회전하도록 풀그림됬다.
아래의 그림과 같은 직6면체의 지향을 직접 만들어 보는
실험을 하면 강체의 운동학을 배우는데 도움이 될 것이다.
그림2) 직6면체의 초기지향과 회전행렬은 단위행렬이다.
<그림2>는 이 직6면체의 초기 지향(orientation, 강체의 놓임새)이다. 이 지향의 회전 직교행렬을 단위 행렬로 잡
는다. <그림 3>은 초기 지향에서 직6면체를 X축에 90° 회전시킨 후의 지향과 행렬을 나타 낸 것이다. <그림4>는 <그림3>의 지향에서 Y축에 90° 회전시킨 후 지향과 회전 행렬을 나타낸 것이다. <그림5>는 다시 초기 지향, <그림2>에서 먼저 Y 축에 90° 회전시킨 후의 결과를 나타 낸 것이고 <그림6>은 이어서 X축에 90° 회전시킨 후 결과를 보인다. 순서가 다른 회전의 결과 서로 다른 지향을 갖는다는 것은 유한한 각도의 회전은 비가환적이라는 사실을 말해 준다. 따라서 직교축에 대한 회전을 가지고 강체의 지향을 기술하는 방법은 적당하지 않다는 것을 알 수 있다. 즉 각 직교축에 대한 회전각 뿐만 아니라 회전을 실행한 순서도 함께 기술하지 않으면 서로 다른 행렬과 그에 해당하는 강체의 지향을 얻게 되기 때문이다. 이 것은 행렬 곱의 비가환성(noncommutability)이 바로 실세계에서 쓰이는 좋은 보기가 된다.
그림3) X축에 90° 회전한 후 지향과 행렬
그림4) 다시 Y축에 90° 회전한 후 지향과 행렬
그림5) 초기상태에서 먼저 Y축에 90° 회전한 지향과 행렬
그림6) 이어서 X축에 90° 회전한 후 지향과 행렬
<그림3>, <그림4>, <그림5>, <그림6>을 그리기 위하여 쓰인 행렬은 <표1>의 22줄에서 인스턴스를 만들어 쓴 Rotation3D라는 클래스 속에 들 어 있다. 이 클래스는 모든 3차원 강체의 운동을 제어하는 핵심 클래스라고 할 수 있다. 이 클래스는 매우 중요하므로 <표2>에 나열해 놓았다.
1 |
class Rotation3D { |
2 |
var mat:Array; |
3 |
function Rotation3D() { |
4 |
mat = new Array([1, 0, 0], [0, 1, 0], [0, 0, 1]); |
5 |
} |
6 |
function unit() { |
7 |
mat[0] = [1, 0, 0]; |
8 |
mat[1] = [0, 1, 0]; |
9 |
mat[2] = [0, 0, 1]; |
10 |
} |
11 |
function concat(m:Array) { |
12 |
var temp:Array = new Array([1, 0, 0], [0, 1, 0], [0, 0, 1]); |
13 |
for (var i = 0; i<3; i++) { |
14 |
for (var j = 0; j<3; j++) { |
15 |
temp[i][j] = mat[i][j]; |
16 |
} |
17 |
} |
18 |
for (var i = 0; i<3; i++) { |
19 |
for (var j = 0; j<3; j++) { |
20 |
mat[i][j] = temp[i][0]*m[0][j]+temp[i][1]*m[1][j]+temp[i][2]*m[2][j]; |
21 |
} |
22 |
} |
23 |
} |
24 |
function rotateX(r:Number) { |
25 |
// r is in degrees - must convert to radians |
26 |
var rad = (r/180)*Math.PI; |
27 |
var cosA = Math.cos(rad); |
28 |
var sinA = Math.sin(rad); |
29 |
var m1:Array = new Array([1,0,0],[0,cosA,sinA],[0,-sinA,cosA]); |
30 |
concat(m1); |
31 |
} |
32 |
function rotateY(r:Number) { |
33 |
// r is in degrees - must convert to radians |
34 |
var rad = (r/180)*Math.PI; |
35 |
var cosA = Math.cos(rad); |
36 |
var sinA = Math.sin(rad); |
37 |
var m1:Array = new Array([cosA,0,-sinA], [0,1,0],[sinA,0, cosA]); |
38 |
concat(m1); |
39 |
} |
40 |
function rotateZ(r:Number) { |
41 |
// r is in degrees - must convert to radians |
42 |
var rad = (r/180)*Math.PI; |
43 |
var cosA = Math.cos(rad); |
44 |
var sinA = Math.sin(rad); |
45 |
var m1:Array = new Array([cosA,sinA,0],[-sinA,cosA,0],[0,0,1]); |
46 |
concat(m1); |
47 |
} |
48 |
function eulerAngles(phi, theta, psi) { |
49 |
var ph = (phi/180)*Math.PI; |
50 |
var th = (theta/180)*Math.PI; |
51 |
var ps = (psi/180)*Math.PI; |
52 |
var sinph = Math.sin(ph); |
53 |
var cosph = Math.cos(ph); |
54 |
var sinth = Math.sin(th); |
55 |
var costh = Math.cos(th); |
56 |
var sinps = Math.sin(ps); |
57 |
var cosps = Math.cos(ps); |
58 |
mat[0][0] = cosps*cosph-costh*sinph*sinps; |
59 |
mat[0][1] = -sinps*cosph-costh*sinph*cosps; |
60 |
mat[0][2] = sinth*sinph; |
61 |
mat[1][0] = cosps*sinph+costh*cosph*sinps; |
62 |
mat[1][1] = -sinps*sinph+costh*cosph*cosps; |
63 |
mat[1][2] = -sinth*cosph; |
64 |
mat[2][0] = sinth*sinps; |
65 |
mat[2][1] = sinth*cosps; |
66 |
mat[2][2] = costh; |
67 |
} |
68 |
function transform3D(pt3D:Array):Array { |
69 |
var result:Array = [0.0, 0.0, 0.0]; |
70 |
for (var i = 0; i<3; i++) { |
71 |
result[i] = mat[0][i]*pt3D[0]+mat[1][i]*pt3D[1]+mat[2][i]*pt3D[2]; |
72 |
} |
73 |
return result; |
74 |
} |
75 |
function transform3DBody(pt3D:Array):Array { |
76 |
var result:Array = [0.0, 0.0, 0.0]; |
77 |
for (var i = 0; i<3; i++) { |
78 |
result[i] = mat[i][0]*pt3D[0]+mat[i][1]*pt3D[1]+mat[i][2]*pt3D[2]; |
79 |
} |
80 |
return result; |
81 |
} |
82 |
} |
표2 Rotation3D 클래스 코드리스팅
이 클래스의 속성인 줄2의 mat가 바로 직교 변환행렬로 이 클래스의 메쏘드 함수들은 모두 이 mat를 조작하거나
이 mat를 써서 원하는 효과를 얻는다. 줄3은 생성자 함수로 이 mat를 단위 행렬로 초기화한다. 줄6의 메쏘드함수
unit는 이 행렬을 단위행렬로 reset 하는 함수이고 줄11의 concat함수는 인수로 받은 행렬 m을 mat 행렬에 오른 쪽에
서 곱하는 함수이다. 즉
를 나타낸다.
줄24, 32, 40 은 기존 행렬 mat에 인수로 받은 각도 r(° 로)만큼 X, Y, Z 축에 대해 회전 시키는 행렬을 오른쪽에
서 곱하라는 메쏘드 함수를 말한다.
줄75는 이 회전 행렬을 써서 3차원 공간의 한 점 pt3D (3요소 배열)를 회전시키라는 명령이다. 이 때 회전축은
강체의 몸통에 붙어 있는 자리표 축이다. 반면 줄68은 공간 자리표 축에 대한 회전을 뜻하는데 이 경우에는 같은 행렬
을 쓰되 그 실제는 -r(°) 회전하는 결과를 가져온다. 공간 자리표와 몸통 자리표의 구분은 지구가 태양 주위를 공전할
때 지구의 몸통과는 상관 없는 공간의 관성자리표에 대한 회전이고 지구가 자전하는 경우 지구의 회전은 몸통에 붙어
있는 자리표, 남북극을 꿰뚫는 회전축이다. 몸통자리표축 회전이란 몸통에 불어 있는 자리표를 축으로 삼아서 돌린
회전을 말한다.
줄 75의 변환 메쏘드함수는 인수를 받은 3차원 벡터 pt3D(3요소 배열)에 변환행렬 mat를 오른쪽
에서 곱해 준다. 즉,
를 코딩한 것이다.