마지막 강좌에서 점의 위치를 인수로 받아서 그 주변의 8개의 점을 생성하여 반환하는 calDotData 함수의 코드까지 설명했다.


          다음은 24개의 꼭지점에서 6개의 8각형 주사위 면과  8개의 잘라나간 귀퉁이 3각형 면을 정의하는 인덱스 배열을 코딩하는 문제가 남아 있다.  이들은 앞마디(주사위9)의 <표1>의 줄16, 17에 정의된 cornsIndex8 과 cornsIndex3라는 배열이다.  <표1>에 올려 놓았다.



1

cornsIndex8 = new Array( [0, 1, 4, 3, 6, 7, 10, 9], [1, 2, 14, 13, 16, 17,  5, 4], [3, 5, 17, 15, 18, 20, 8, 6], [0, 9, 11, 23, 21, 12, 14, 2], [7, 8, 20, 19, 22, 23, 11,10], [12, 21, 22, 19, 18, 15, 16, 13])

2

cornsIndex3 = new Array([2, 1, 0], [3, 4, 5], [8, 7, 6], [9, 10, 11], [12, 13, 14], [17, 16, 15], [18, 19, 20], [23, 22, 21]);


표1  두개의 인덱스 배열 정의


           이 배열을 코딩하기 위해서  앞마디(주사위9)의 <그림1>왼쪽과  같은 모형이 필요했던 것이다. <그림1>의 제일 앞으로 보이는 귀퉁이 3각형은 cornsIndex3의 6 번째 요소 cornsIndex3[5] = [17,16,15]로 그 순서가 바로 오른쪽나사법칙으로 [17,16,15]->[16,15,17] 방향으로 회전하면 나사의 전진 방향이 3각형의 표면을 표시하고 있다. 이러한 순서를 잘 매기지 않으면 면의 앞뒷면을 가리는데 일관성을 유지할 수 없게 된다. 

 

1

 public  function showDice(dist:Number, isIso:Boolean):Void  {

2

 distZ = dist;

3

 isIsometric = isIso;

4

 var imax = aCube.length;

5

 var zz :Number ;

6

for (var i = 0;  i <imax;  i++)

7

{ currentCube[i]=  rotMat.transform3DBody(aCube[i]);

8

 if (isIsometric)  currentCube[i] = Isom.mTSB(currentCube[i]);}

9

 for (var i = 0;  i <imax;  i++)

10

aCube2D[i] = GraUtil.convScreen(currentCube[i], distZ);

11

 mc.clear();

12

 for (var s=0; s<6; s++) {

13

 for (var k = 0; k< 8; k++)

14

 corns2D[k] =  aCube2D[cornsIndex8[s][k]];

15

 zz = GraUtil.calZZ(corns2D);

16

  if(zz>0)

17

 {drawFilledPoly(corns2D,  colA[s], 100);

18

 var jb = s*(s+1)/2;

19

var je =  (s+1)*(s+2)/2;

20

for(var j = jb;  j<je; j++)

21

drawDot(dotData[j]);

22

 }

23

}

24

//will cut  8 corners

25

for (var s=0; s<8; s++) {

26

for (var ii = 0;  ii <3; ii++)

27

corns2D3[ii] =  aCube2D[cornsIndex3[s][ii]];

28

 zz = GraUtil.calZZ(corns2D3);

29

 if(zz>0)

30

drawFilledPoly(corns2D3,  colCorner[s], 100);

31

}      

32

}//showDice()  


표2 createDice 클래스의 퍼브릭 메쏘드 함수 showDice.

 

           CreateDice클래스의 생성자함수를 제외한 유일한 퍼블릭 메쏘드 함수는 showDice로 이것으로 주사위를 그린다.  아래의 <표2>에 코드를 올려 놓았다.  CreateDice의 내부 메쏘드 함수들을 써서 주사위를 그리는데 인수로 dist 라는 원근법 여부를 결정하는 거리와 아이소메트릭공간에 투영할 것인가를 묻는 isIso 변수를 인수로 받아 들인다.   이 인수는  줄10의 GraUtil.convScreen함수에 전달되는데 이 static class 유틸리티 함수는 아래의 <표3>에 올려 놓았다.  이 유틸리티 클래스에는  두 메쏘드 함수만 있는데 calZZ는 그리려는 면의 꼭지점의 배열을 인수로 받으면 벡터  (0→1)   와 (1→2) 를 구성하여 그 벡터 곱, (0→1)x(1→2)    의 Z성분을 셈하여 반환한다. 


          GraUtil.convScreen 함수는 거리를 인수로  받아서 그 거리가 1000보다 크면 원근법을 사용하지 않고 그 보다 작으면 그 거리를 카메라 거리로 삼아서 원근법 변환을 적용한다.  자주 쓰는 함수는 static 메쏘드 함수로 정의해 두면 아무 때나 Math 클래스 함수를 불러 쓰듯 손쉽게 사용할 수 있다.  

 

1

class GraUtil{

2

 

3

static function calZZ(vecArr:Array):Number {

4

        var x01 = vecArr[0][0]- vecArr[1][0];

5

        var y01 =vecArr[0][1]-vecArr[1][1];

6

        var x21 =vecArr[2][0]-vecArr[1][0];

7

        var y21 =vecArr[2][1]-vecArr[1][1];

8

        return (x01*y21-y01*x21);

9

}

10

static function convScreen(pt3D:Array, distance:Number):Array{

11

                var scale :Number ;

12

                if (distance <1000)

13

                scale = distance/(distance + pt3D[2]);

14

                else  scale = 1;

15

var pt2D = [];

16

pt2D[0]  =pt3D[0]*scale;

17

pt2D[1]  =pt3D[1]*scale;

18

return pt2D;}

19

}


표3 GraUtil static 유틸리티 클래스

 


          <표2> 의 showDice 메쏘드는 먼저 줄6,7,8에서 24개의 꼭지점을 현재 변환행렬을 가지고 변환시킨다.  아이소메트릭 공간에 투영하고 싶으면 한번 더 변환을 수행한다. 배경 자리표를 그린다든가 공간에서의 지향 관계를 보이고 설명을 할 필요가 없을 때엔 이 변환은 생략하는 것이 CPU에 짐을 덜어 줄 것이다.        

          줄10은 원근법여부를 정하기 위하여 넣은 코드인데 이것 역시 원근법이 필요가 없으면 showDice 의 인수 dist에 1000 보다 큰 수를 넣으면 된다.   줄12에서 줄22까지가 주사위의 6면을 그리는 부분인데 s가 면을 나타내는 변수이고 k는 8각형의 꼭지점를  나타낸다.  aCube2D는 회전된 24개의 꼭지점으로 Z성분을 제거한 2차원 스크린 자리표를 나타낸다. 이 때 줄14의 인덱스가 어떻게 쓰이는가를 잘 음미하게 바란다. s면의 k번째 꼭지점인 corns2D[k](2차원 점) 는 2차원화된 일반적인 꼭지점 aCube2D 중에서 s면 인덱스(8개의 8각형 꼭지점을 담은 배열)의 k번째 성분으로 결정하는 것이다. 

          줄15는 이렇게 정해진 8각형의 2차원 꼭지점배열을 인수로 하는 GraUtil.calZZ(corns2D)함수로 꼭지점 "0", "1", "2"를 가지고 벡타   (01)   와 (12) 를 구성하여 그 벡터 곱, (0→1)x(1→2)    의 Z성분을 계산한다. 이 때 배열은 크기가 3요소이상(3각형이상) 이고 성분은 2이상(2차원 이상)이면 된다. <표15>의 줄3~9를 음미하기 바란다. 이렇게 하여 16줄에서 s면이 앞면이면 면과 점을 그리라는 명령을 수행한다.  

            앞마디(주사위9)의 <표3>에서 21개의 주사위의 점에  0, 1, 2, ....20까지  일련번호를 매긴 것을 기억할 것이다.  <표2>의 줄18과 19는 s 면의 점의 시작과 끝 번호를 계산한 것이다. 마지막으로 줄24~31은 잘려 나간 주사위 귀퉁이 3각형을 그리는 명령부분이고 주사위의 큰 면을 그릴 때 쓴 방법과 같은 방법을 쓴다. 되풀이 설명이 필요 없을 것이다.    

            이렇게 하여 마침내 주사위를 그릴 수 있게 되었다. 그러면 이 주사위를 어떻게 제어할 것인가?  <표4>의 코드는 주사위를 제어하기 위해 만든 것이다.



1

var dpt:Number = -1000;

2

createEmptyMovieClip("dice_mc",  dpt++);

3

var colArr:Array =  new Array(ColorUtil.violet, ColorUtil.pink, ColorUtil.lightgreen,  ColorUtil.cyan, ColorUtil.magenta, ColorUtil.beige);

4

var colorCorn:Array =  new Array( ColorUtil.darkgray,  ColorUtil.darkgray,  ColorUtil.darkgray,  ColorUtil.darkgray,  ColorUtil.darkgray,  ColorUtil.darkgray,  ColorUtil.darkgray,  ColorUtil.darkgray);

5

var dotColor:Number = 0x333333;

6

var dice:CreateDice  = new CreateDice(dice_mc , 80,  0.3 ,  0.1,  colArr,  colorCorn, dotColor)

7

dice.mc._x= 290;

8

dice.mc._y= 300;       

9

        var ranNum = Math.floor (6*Math.random ()); 

10

var eulerSet:Array = new Array([180 ,180 ,270], [180, 270, 270], [270, 90, 180], [270, 270, 180], [270, 90, 270], [180, 360, 270]);

11

var phi = eulerSet[ranNum][0];

12

        var theta = eulerSet[ranNum][1];

13

        var psi = eulerSet[ranNum][2];

14

        dice.rotMat.eulerAngles(phi,theta,psi);

15

        dice.showDice(999, true);

16

         var tt = 0;

17

function runthis(){

18

                if (  tt<400)

19

{tt += 10;     

20

dice.mc._y =  0.005*(tt - 200)*(tt - 200) + 100;

21

theta  -=Math.log(6*Math.abs(theta - eulerSet[ranNum][1])+1);

22

psi  -=Math.log(7*Math.abs(psi - eulerSet[ranNum][2])+1);

23

dice.rotMat.eulerAngles(phi,theta,psi);

24

         dice.showDice(1000,true);

25

          updateAfterEvent();

26

                 }//if (  tt<400)       

27

                else{

28

                clearInterval(id1); delete  id1;

29

                }//else

30

                }//function runthis(){

31

dice.mc.onRelease = function(){ ranNum = Math.floor (6*Math.random ());  phi = eulerSet[ranNum][0]; theta =  eulerSet[ranNum][1]+250; psi  = eulerSet[ranNum][2]+250;

32

tt= 0;

33

if (id1 == undefined)

34

id1= setInterval(runthis, 25);

35

else{clearInterval(id1); delete  id1; id1= setInterval(runthis, 25);}

36

} //function


표4 주사위 제어하기

 
             제일 먼저 주사위를 담을 무비클립을 줄2에 만든다. 이 무비 클립의 이름은 dice_mc이다. 이제 주사위의 면, 귀퉁이의 3각형과 면위의 점들의 색깔을 정한다. 이 색깔 배열과 기타 주사위의 크기와 자세한 설계 데이터를  인수로 넣고 줄6에서 createDice 클래스의 인스턴스를 만든다.  그 이름은 dice 이다.         이제 이 dice의 속성만 가지고 주사위를 제어한다. 주사위의 병진 운동은 dice 인스턴스의 속성 mc, 즉, dice.mc의 위치로 제어 한다. 이 무비클립 은 처음에 만든 dice_mc와 동등하다.


          그러나 일단 이 인스턴스 dice 만 가지고 모든 풀그림을 짜는 것을 원칙으로 하기 때문에 dice.mc._x, dice.mc._y 로서 운동을 제어한다.  다음은 dice 의 지향인데 이 dice의 속성, 즉 주사위 속성인 rotMat를 써서 제어한다. rotMat 는 dice를 생성할 때에 초기화 과정에서 만들어진 Rotation3D 의 인스턴스다. 그러므로  주사위5 (박스 그리기)의 <표2>에 나열했던 모든 속성과 메쏘드함수를 불러 쓸 수 있다.   

        줄10에는 주사위의 6개의 면이 위로 향하는 지향에 해당하는 오일러각 세트를 6개 구해서 배열에 담아 놓는다.  처음에 0과 5사이의 막수를 생성하여 (줄9) 그 지향으로 주사위를 놓는다.  (줄11~15 참조) 


        주사위를 클릭하면 주사위는 병진운동과 회전운동을 수행하다 바닥에 떨어지면 새로운 숫자가 위를 향해서 앉게 한다.  그 부분이 줄17에서 줄30까지의 코드이다. 이 부분은 물리와 약간의 수학을 가미하여 운동이 자연스레 보이도록 하였다. 이러한 것이 일종의 물리모델인 것이다.  예를 들어 줄20은 고등학교 물리교과서에 나오는 내용으로 지구위에서와 같이 일정한 중력가속도   [g]가 작용할 때 물체의 속도는 [v=-gt +v0]  가 된다는 사실을 쓰고 있다.  그래서 줄31에서 주사위를 클릭하면 주사위가 던져지고 자연스런 운동을 하면서 내려 앉게 된다.  일단 내려 앉으면 더 이상 CPU는 아무 일도 않는다. 






무비 1 완성된 주사위
주사위를 던지려면 주사위에 마우스를 대고 클릭한다. 
 


맺음말
             여기  우리는 순전히 액션스크립트 프로그래밍만으로 주사위를 그렸다.  그래서 swf 파일의 크기가  4144 바이트, 약 4KB 에 불과하다.  바이트란 무엇인가?  1 바이트는 8비트다. 1 비트는 1 binary digit 즉 2진수의 1자리를 뜻한다. 4144 bytes = 4144 x8 = 33152 bits 이다. 나는 주사위를 그린 것이 아니라 33152자리의 2진수를 하나 적은 것이다.  

            주사위를 그리기 위해 플래시를 배웠고 플래시를 배우고 처음 그린 것이 주사위였다.  그래서 이 주사위에 애착이 가기에 이 작품집의 주제로 내 놓은 것이다. 그러나 과연 이러한 방법으로 플래시를 사용하는 것이 권장할 만한 일인가?   생산성이란 면으로는 결코 권장할 만한 일이 못 된다고 생각한다. 그러나 나는 이것을 즐기며 할 수 있었기에 작업할  수가 있었던 것이다.

           Donald Knuth 교수는 프로그래밍은 시를 쓰거나 작곡을 하는 것과 같은 심미적 체험을 안겨 준다고 말했다.  나는 여기에 덧붙여 플래시는 조각가가 돌을 깎는 작업과 같다고 말하고 싶다.  힘들고 고된 작업이라도 조금씩 조금씩 형상이 보이고 마침내 머릿속에 그렸던 그림이 화면에  나타 날 때의 기쁨은 길고 긴 여정의 피로를 씻겨 준다.  
 
                                        2004. 5. 8  샛솔 이구철 
 
 


 

Posted by 샛솔 :