부록 - 주사위 컴포넌트

 


        원고를 마치고 내친 김에 주사위컴포넌트를 만들었기에 여기에 부록으로 첨부한다.  이 글을 쓰기 전에 나는 이 주사위를 이 작품집의 주제로 삼는데 회의를 가졌다. 그래서 저자들의 모임 클럽의 자유 게시판에 아래와 같은 글을 올렸다. 



 안녕하세요.

        내가 플래시를 배우고 처음 제작한 것이 주사위였습니다.  상당히 시간과 공을 들여 제작했기에 actionscript.org. 에올렸습니다.지금도가끔아래와같은 e-mail 을받습니다.

Dear Koo Chul Lee

I am not a technical guy but just wanted to say how impressed I am at the size efficiency of your dice etc on actionscript.org.

Really cool stuff.

Do you do flash design work?

Regards,

William ****

        그래서 주사위에 애착이 갑니다. 그래서 이 주사위를 이 프로젝트의 sample로 삼아서 해설을 할까 생각했습니다.  V6로 그린 것이라 V7(FlashMX2004)로 update 하려다 V6 코드를 다시 보니 세상에 나처럼 주사위를 그릴 사람은 나 빼고는 없을 것이라는 생각이 들어서 이 글을 씁니다.

        플래시에는 3D 엔진이 달려 있지 않기 때문에 플래시의 3D 애니메이션을 하는 경우 Swift3D, Vector3D 같은 제3자 소프트웨어 것을 연동해서 쓰라는 것이 전문가들의 권고입니다. 아마 이것이 정수일 것입니다.

        플래시만 써서 3D 애니메이션을 한다는 것은 엄청난 기술적 지식과 노동이 요구되는 작업입니다.  설혹 기술적 지식이 있다 하더라도 이 방법으로 3D를 하는 이는 없을 것입니다. 풀그림을 생업으로 하는 사람은 생산성이 떨어져 밥 먹기가 힘들 테니까요. 나처럼 시간이 많아 풀그림을 “놀이”로 하는 이나 할 일이라는 생각이 듭니다. 또 내가 그렇게 풀그림을 할 수 있는 것은 내가 마우스가 발명되기 훨씬 이전의 "원시시대"부터 풀그림을 해 왔기 때문에 가능할 것입니다.

        나는 응용 프로그램을 좋아 하지 않습니다.Versatility 가 없기 때문입니다.90년도 초에 Mathematica를 내 개인용 워크스테이션에 거금 2500불을 지불하고 single user license로 설치한 일이 있었습니다.몇 번 쓰고는 버렸습니다.Flexibility 가 없어서 내가 원하는 데로 output를 낼 수가 없어서 였습니다.Mathematica가 할 수 있는 것은 나는 Pascal 이나 C로 풀그림을 할 수 있기 때문이었습니다. 물론 엄청난 노동이 필요했지만 말입니다.

        요즘처럼 Productivity 위주의 세상에서 나 같은 방법을 전수한다는 것이 무슨 의미가 있는가 하는 의구심이 들어서 이 글을 씁니다.  여러분은 어떻게 생각하시는 지요?

 




        그런데 이 게시글에 다른 저자들의 덧글은 한결 같이 이 주제가 적합하다는 격려였다.  그래서 이 원고를 쓰게 된 것이다.  그러나 아직도 이 의구심은 남아 있다. 그래서 마지막으로 생각한 것은 이 주사위를 컴포넌트로 만들어 3d 풀그림에 익숙하지 않는 프로그래머나 디자이너에게 프로그램없이 사용하게 해 주는 것이었다.

        컴포넌트의 일반적 제작이나 사용법은 이 책의 다른 저자들이 상세히 다루었으므로 생략하고 이 주사위 컴포넌트의 사용법만 간단히 다루겠다.  먼저 컴포넌트를 컴포넌트 패널에 보이게 설치하려면 매크로미디어사가 무료 배포하는 extension manger를 내려 받아 사용하기 바란다.  아래의 사이트에서 무료 다운 받을 수 있다.

http://www.macromedia.com/exchange/em_download/





그림 18) 주사위 컴포넌트가 설치된 components panel


 

        이 이책의 CD에 포함된 DiceComp.mxp (Macromedia Exchange Package)를 Extension Manager 로 설치하면 자동적으로 component 패널에 <그림39>와 갈이 주사위 컴포넌트가  설치된다.


 



 

그림 19) 주사위를 stage에 끌어다 놓으면 그 밑에는 속성창이 뜬다.

        


         이컴포넌트가 일단 설치되면 그 사용법은 여느 다른 UI 컴포넌트와 같다. 즉 컴포넌트를 마우스로 잡아서 stage 에 가져 오면 <그림40>과 같이 주사위를 stage 에 생성한다.  그리고는 그 property 창에서 주사위의 속성을 편집한다. 속성창에서는 주사위의 6면의 색깔을 편집할 수 있다.   colArr 가 바로 그것이다.  다음은 귀퉁이의 색을 편집할 수 있다.  cornerColor가 그것이다. 다음은 초기 주사위의 놓임새를 편집할 수 있다. DiceNum 이 그것이다.  6을 입력하면 주사위의 초기 지향은 6이 위로 향하여 앉아 있게 된다.  또 주사위의 점 색깔을 편집할 수 있다.  dotColor 가 바로 그것이다. 끝으로 주사위를 클릭했을 때 뛰어 오르게 하거나 정지 상태에서 회전하거나를 선택할 수 있다.  그림<그림41>은 속성창을 편집했을 때의 보기를 하나 스크린 셧으로 보이고 있다. 

        마지막으로 주사위를 클릭하였을 때 나오는 점수를 액션스크립트에서 사용하기 위해서는 <그림42>의 오른쪽 위에 있는 스크립트와 같은 이벤트 핸들링을 쓴다.  이  스크립트는 여느 다른 UI컴포넌트의 이벤트 핸들링 메쏘드와 다를 바  없으므로 자세한 설명은 생략한다. 


 

그림 20) 속성창을 편집한 예

          여기서 myDice 는 이 컴포넌트의 인스턴스 이름이다.  아래 무비는 이 컴포넌트의 미리보기 무비다. 이 무비에서는 세 번째 제일 큰 주사위는 뛰지 않고 제 자리에서 회전하여 점수를 제공한다.  CD에 수록된 <DiceTeste.fla> 파일을 열어 액션을 살펴 보기 바란다.  이 컴포넌트는 주사위를 던져서  나오는 점수로 게임을 하는 풀그림에 사용할 수 있을 것이다.  

         




무비1 컴포넌트 미리보기 무비
주사위를 던지려면 주사위에 마우스를
대고 클릭한다.
3번째 주사위는 앉은 자리에서 그대로 회전한다.

       
이것이 내가 독자 여러분에 해 줄 수 있는 모든 것이다.  

CD.zip

 


---------------------
[후기]

첫번째 게재물(주사위1)의 글 첫머리에 밝혔 듯이 이 글은 원래 종이책을 위해 썼던 원고를 내키지 않아 기고를 철회하고 묻어 두었던 것을 거의 그대로 마디(section) 별로 나누어 웹 문서로 고쳐 게재한 것이다. 

거의 원고 그대로이지만 플래시 무비가 있는 경우는 무비를 그대로 올려 글을 읽으며 스크린위에서 실험해 볼 수 있게 했다.

이 게재를 마치면서 adobe 사의  컴포넌트 download 사이트에 가서 내가 만든 주사위 컴포넌트의 다운로드 횟수를 보니 오늘 날자(2009-02-20)로 54,684 Downloads 가 있었다.

그 정도 면 빚은 조금 갚고 가는 것 같다.  

 

 

Posted by 샛솔 :


 

          마지막 강좌에서 점의 위치를 인수로 받아서 그 주변의 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 샛솔 :


 

마침내 주사위

          이제 주사위를 그릴 모든 준비가 끝났다. 정6면체의 8개의 꼭지점을 3각형으로 자른  주사위는  24개의 꼭지점을 가진 볼록 14면체이다.   6개의 8각형 큰 면과 8개의 3각형 작은 면을 가진 14면체다. 따라서 주사위를 그리려면 14개의 면을 그려야 한다.  24개의 꼭지점에 번호를 매기고 그 번호로 면을 정의하는 인덱스를 코딩하여야 한다. 나는 프라톤 입체를 그릴 때와 마찬가지로 모형을 만들어 사용하였다.  찰흙과 이쑤시개로 만든 정6면체 모형의 8 귀퉁이에 생긴 3각형 꼭지점에 번호를 붙였다.


          아래 그림 <그림1>의 왼쪽 것이 바로 그것이다.  이 경우 번호 매김은 어떤 순서로 하거나 상관이 없다.  나중에 면을 정의하는 인덱스 배열을 만들 때 그 순서를 일관성 있게만 해 주면 된다. 

 



그림 1) 주사위를 그리기 위하여 만든 모형과 플래시로 만든 주사위 스크린 셧


           위 그림 <그림1>의 오른 쪽 것이 왼쪽 모형에 따라 만든 주사위 최종 결과물 스크린 셧이다.   이제 이 주사위를 던질 수 있게 만드는 법을 설명한다. 

먼저 이 주사위를 생성하는 CreateDice 클래스의에 대해 설명한다.  그 클래스의  빼대는 <표1>에 올려 놓았다.  20개의 속성과  6개 메쏘드 함수로 구성되었다. 20개의 속성 가운데 2개만 public 으로 밖에서 불러 내어 조종할 수 있고 나머지 18개는 private 속성으로 클래스 내부에서만 사용할 수 있다. 메쏘드 함수도 두개만 public이고 나머지 4개는 private로 클래스 안에서만 사용할 수 있다.  public 함수중에서 하나는 생성자(constructor) 함수로 클래스 인스턴스를 만들 때 한번만 쓰이는 것이므로 아무때나 밖에서 실행시킬 수 있는 public 함수는 하나뿐이다. 


 

1

class CreateDice{

2

        public var mc:MovieClip;

3

        private var sizeCube:Number;

4

        private var cornerSizeRatio:Number;

5

        private var sd :Number ;

6

        private var sizeDot:Number;

7

        private var colA:Array ;

8

        private var colCorner:Array;

9

        private var colDot:Number;

10

        public var rotMat : Rotation3D ;

11

        private var aCube:Array;

12

        private var currentCube:Array ;

13

        private var aCube2D:Array ;

14

        private var corns2D:Array;

15

        private var corns2D3:Array;

16

        private var cornsIndex8:Array ;

17

        private var cornsIndex3:Array ;

18

        private var dotsC:Array ;

19

        private var dotData:Array;

20

        private var distZ:Number;

21

        private var isIsometric:Boolean;

22

 

23

 

public function CreateDice(diceMc:MovieClip , diceSize:Number, cornSize:Number , dotSizeR:Number,  colorArray:Array,          colorCorner:Array,dotCol:Number){//***//}

24

private function init():Void {//***//}

25

private function calDotData(pt3D1:Array):Array {//***//}

26

private function drawFilledPoly(corners,  fillC,  alp):Void {//***//}

27

private function drawDot (oneDot):Void{//***//}

28

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

29

}//end class def.


표1) class CreateDice 의 코드의 골자



          줄2의 MovieClip 속성 mc 는 주사위가 생성되는 무비클립이다. 따라서 주사위의 병진 운동은 mc._x, mc._y를 변동시켜 스크린 위에서 움직이게 한다.  줄3의 sizeCube는 주사위의 크기로서 초기화할 때 한번 생성자 함수의 인수로 받아 정하면 주사위를 없앨 때까지 바꿀 수 없다. 줄4의 cornerSizeRatio는 주사위의 모서리의 잘린 부분의 크기를 정한다. <그림37>에서 cornerSizeRatio=0.3이다. 줄5의 내부 속성변수 sd=1-cornerSizeRatio 로<그림37>에서 보면 0.7이 된다.  나머지 속성변수를 모두  여기서  설명하지 않겠다.  클래스 안에서 이들 속성이 어떻게 쓰였는가를 보면 그 속성의 의미를 이해할 수 있다.  



그림 2) 무비1에 그린 주사위 설계도



 

           가장 중요한 속성은 aCube 라는 배열로 전에 말했던 24개의 꼭지점을 정의하는 데이터 배열이다.  이 x,y,z 성분으로 표시되는 24개의 3차원 점의 배열임에 주의하기 바란다.  주사위의 중심을 3차원공간 자리표의 원점(0,0,0) 에 놓고  이 원점에서 주사위의 변까지의 거리를 1로 잡았을 때의 꼭지점 자리표를 <그림36>의 모형의 번호순으로 기입한 것이다.  생성자 함수를 실행하면 그 일부에서 아래 <표10>의 코드로 기술 되는 aCube 배열이 생성된다.

 

 aCube = new Array( [sd, 1, -1],  [1, sd, -1], [1, 1, -sd], [sd, -1, -1], [1, -sd, -1], [1, -1, -sd], [-sd, -1, -1], [-1, -sd, -1], [-1, -1, -sd], [-sd, 1, -1], [-1, sd, -1], [-1, 1, -sd], [sd, 1, 1], [1, sd, 1], [1, 1, sd], [sd, -1, 1], [1, -sd, 1], [1, -1, sd], [-sd, -1, 1], [-1, -sd, 1], [-1, -1, sd], [-sd, 1, 1], [-1, sd, 1], [-1, 1, sd] );

표2 주사위의 24개의 꼭지점의 3차원 자리표로 된 배열  (표10)

 


          다음 중요 속성 변수는 주사위의 텍스쳐 데이터인 점들의 위치를 담은 배열 18줄의 dotsC와 그리기 데이터를 담은 19줄의 otData 다.  dotsC 는 주사위의 점들의 중심의 자리표를 모은 배열이다.  주사위에는  점이 1+2+3+4+5+6 = 21 개가 있다. 이들의 위치를 다음과 같이 배열에 담았다.

        <그림2>의 설계도에 점들의 위치를 표시했는데 는 "2" 와 "4" 만 실선으로 표시된 위치에 점을 그리고 나머지 "1", "3", "5", "6" 은 점선으로 그린 원들의 위치에 그렸다. 그리고 21개의 점들은 ·"1", "2", "3", "4", "5", "6" 면의 점들의 순서대로 배열에 담는다. 즉 "1" 면의 점은 dotsC[0] 에, "2" 면의 점은 dotsC[1], dotsC[2] 에, "3" 면의 점은 dotsC[3], dotsC[4], dotsC[5]에 담는다. 나머지도 마찬가지이다.  줄4~6은 점의 크기를 주사위의 크기에 비례하여 키운다. 줄7~11은 이제 이 주사위의 점을 그리기 위한 텍스쳐 데이터를 만드는 코드다.


 

그림 3) 주사위의 한점을 그리는데 필요한 8개의 데이터 점.

 

 

          점(근사원) 하나마다  8개의 3차원점을 사용하기로 한다. <그림3>의 가운데 점이 주사위의 점의 위치를 나타내고 정4각형의 변과 꼭지점에 놓인 8개의 점이 근사원을 그리는데 쓰인다. 원 위의 점을 양끝으로 하고 꼭지점점을 조절점(control point)으로 해서 curveTo를 사용한 곡선 4개로 근사원을 그린다.




1

dotsC =  new Array([0, 0, -1], [1, 0.4, -0.4], [1, -0.4, 0.4], [-0.5, -1, -0.5], [0, -1, 0], [0.5, -1, 0.5], [-0.5, 1, -0.5], [0.5, 1, -0.5], [0.5, 1, 0.5], [-0.5, 1, 0.5], [-1, 0, 0], [-1, 0.5, -0.5], [-1, -0.5, -0.5], [-1, -0.5, 0.5], [-1, 0.5, 0.5], [-0.5, -0.5, 1], [0, -0.5, 1], [0.5, -0.5, 1], [0.5, 0.5, 1], [0, 0.5, 1], [-0.5, 0.5, 1]);

2

for (var i=0; i<21; i++) {

3

 for (var j=0; j<3; j++) {

4

dotsC[i][j] = dotsC[i][j]*sizeCube/2;

5

}

6

}

7

dotData = new Array( 21);     

8

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

9

dotData[i] = 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]];

10

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

11

dotData[i] = calDotData(dotsC[i]);

표3 주사위의 점들을 그리기 위한 코드



 

          <표3>의 줄9에 정의된  8요소 배열에 이 8개의 3차원 점들을 담는다. 그리고 모두 21개의 점에 대해서 이 데이터점을 생성한다. (<표3> 줄10, 11 참조)  이것을 손으로 일일이 코딩하는 것은 힘들 뿐 아니라 자칫 에러를 유발할 수 있으므로 이것을 피하기 위하여 아래의 코드와 같은 생성함수를 만들어 <표3> 줄11에서 시행한다. 

           아래의 <표4>에 올려 놓은 CalDotData 함수는 인수로 받은 근사원의 중심이 어느 평면에 놓여 있나를 판별한 다음 이 평면에 근사원 중심점 주변에 8개의 점을 생성하여 반환하는 함수다.  

 


1

private function calDotData(pt3D1:Array):Array {

2

var pt3DA = [[0,0,0], [0,0,0], [0,0,0], [0,0,0], [0,0,0], [0,0,0], [0,0,0], [0,0,0]];

3

if (Math.abs (pt3D1[0])==(sizeCube/2))

4

{pt3DA[0] =[pt3D1[0], pt3D1[1] + sizeDot,  pt3D1[2]];

5

pt3DA[1] = [pt3D1[0], pt3D1[1] + sizeDot,  pt3D1[2]+sizeDot];

6

pt3DA[2] = [pt3D1[0], pt3D1[1],  pt3D1[2]+sizeDot];

7

pt3DA[3] = [pt3D1[0], pt3D1[1] -sizeDot,  pt3D1[2]+sizeDot];

8

pt3DA[4] = [pt3D1[0], pt3D1[1] -sizeDot,  pt3D1[2]];

9

pt3DA[5] = [pt3D1[0], pt3D1[1] -sizeDot,  pt3D1[2]-sizeDot];

10

pt3DA[6] = [pt3D1[0], pt3D1[1],  pt3D1[2]-sizeDot];

11

pt3DA[7] = [pt3D1[0], pt3D1[1] +sizeDot,  pt3D1[2]-sizeDot];

12

        }

13

else

14

{if( Math.abs (pt3D1[1])==(sizeCube/2))

15

{pt3DA[0] = [pt3D1[0] + sizeDot,  pt3D1[1],  pt3D1[2]];

16

pt3DA[1] = [pt3D1[0] + sizeDot,  pt3D1[1],  pt3D1[2]+ sizeDot];

17

pt3DA[2] = [pt3D1[0],  pt3D1[1], pt3D1[2]+sizeDot];

18

pt3DA[3] = [pt3D1[0] -sizeDot,  pt3D1[1], pt3D1[2]+sizeDot];

19

pt3DA[4] = [pt3D1[0] -sizeDot,  pt3D1[1], pt3D1[2]];

20

pt3DA[5] = [pt3D1[0] -sizeDot,  pt3D1[1], pt3D1[2]-sizeDot];

21

pt3DA[6] = [pt3D1[0],  pt3D1[1], pt3D1[2]-sizeDot];

22

pt3DA[7] = [pt3D1[0] +sizeDot,  pt3D1[1], pt3D1[2]-sizeDot];

23

        }

24

          else

25

{pt3DA[0] =[pt3D1[0],  pt3D1[1] + sizeDot,  pt3D1[2]];

26

pt3DA[1] = [pt3D1[0]+sizeDot , pt3D1[1] + sizeDot,  pt3D1[2]];

27

pt3DA[2] = [pt3D1[0]+sizeDot , pt3D1[1],  pt3D1[2]];

28

pt3DA[3] = [pt3D1[0]+sizeDot , pt3D1[1] - sizeDot,  pt3D1[2]];

29

pt3DA[4] = [pt3D1[0],  pt3D1[1] - sizeDot,  pt3D1[2]];

30

pt3DA[5] = [pt3D1[0]-sizeDot , pt3D1[1] - sizeDot,  pt3D1[2]];

31

pt3DA[6] = [pt3D1[0]-sizeDot , pt3D1[1],  pt3D1[2]];

32

pt3DA[7] = [pt3D1[0]-sizeDot , pt3D1[1] + sizeDot,  pt3D1[2]];

33

         }

34

        }

35

        return pt3DA;

36

}

표4 calDotData 함수의 코드.  점의 위치를 인수로 받아서 그 주변의 8개의
점을 생성하여 반환한다.(12)

 

 

          이 마디는 너무 길어지므로 다음 강좌에서 이어서 설명한다. 

Posted by 샛솔 :


 

물체의 3차원 회전을 풀그림 할 때 텍스쳐 맵핑도 고려해야 한다. 면에 그려진 텍스쳐도 회전과 함께 같이 돌아야 한다. 여기에 특별한 기법이 따로 있는 것이 아니다. 모든 텍스쳐를 벡터화해서 그 데이터를 함께 회전 행렬을 써서 변환시키면 그 뿐이다. 그러나 <그림1>과 같이 정교한 텍스쳐는 CPU 에 매우 큰 짐을 안겨 줌으로 최적화를 위하여 노력해야 한다. 

 

        <그림1>의 원테의 그라데이션을 회전시키기 위해서는 360개의 3차원 점을 회전시켜야 한다. 따라서 이런 경우에는 그림을 그릴 필요가 있는지 없는지를 먼저 판별한 다음 회전시켜야 한다. 미리 모든 데이터점을 회전시킬 필요가 없다. 한편 <그림3>과 같은 간단한 영자 Z 또는 N자 텍스쳐는 단 10 개의 3차원 점을 회전시키면 된다. CPU 에 큰 부담을 주지 않는다.  

 





무비 1  텍스쳐 맵핑 데모 무비



          오일러각 제어 스라디더를 움직이면서 실험해 보기 바란다.



 



그림 1 면이 회전하면 면위의 그림도 따라 돈다.




그림 2 정교한 맵핑일수록 CPU에 부담을 준다.




그림3 글자와 같은 것은 작은 벡터데이타로 그릴수 있다.

 



그림4 영자 Z(N?)는 10개의 3차원 점으로 처리했다.





______________________________________________


(각주)  2006년 Actionscript3.0 이 나오면서 텍스쳐매핑은 아주 빨라졌다.  2006년 내가 플래시 클럽강좌에 AS3.0 으로 텍스쳐매핑하는 법을 올려 놨다.   그때 만든 데모 플래시 무비를 아래에 첨부한다.



 


 

 



 

Posted by 샛솔 :


         박스 그리기(주사위5)에서 보여 준 정6면체는 반투명 유리 박스로 6면이 모두 보였다.  불투명 박스를 그리기 위해서는 박스의 안쪽 면은 그리지 말아야 한다.  다면체의 안쪽 면은 항상 가려져 보이지 않는다. 


         사실 3차원 입체를 그리기 위해서는 가려진 면을 감추는 것이 매우 중요한 과제가 된다. 그런데 뽈록 입체의 경우 입체의 안쪽면만 제거하면  보이는 모든 것을 그린 셈이 된다.  뽈록 하기 때문에 겉면이 입체의 다른 부분에 의해서 가려지는 경우는 없기 때문이다. 오목 입체의 경우에는 보다 복잡한 알고리즘을 사용하여야 한다. 다행이 주사위는 뽈록 입체이므로 앞뒷면 가리기만 해 주어 겉면만 그리게 되며 그것으로 그리기는 끝난다. 


          3차원 공간에서 평면의 앞뒤면 가리기에는 두 가지 방법이 있다.  하나는 강체를 이루는 표면 다각형의 법선을 먼저 구해 두고 그 법선도 강체와 함께 변환 행렬을 적용한다. 그런 다음 그 법선의 Z성분이 양수냐 음수냐를 가려서 다각형 면이 앞면을 보이고 있나 뒷면을 보이고 있는지를 판별한다. 이 방법은 입체를 변환시킬 때 입체를 구성하는 꼭지점 이외에 각 구성면의 법선까지 함께 변환시켜 주어야 하는 계산상 짐이 생긴다. 

 
          두 번째 방법은 다각형의 꼭지점으로부터 법선의 Z성분을 직접 셈하는 방법이다.  <무비1>은 두가지 방법을 보이기 위하여 만든 무른모 무비이다.   아래에 기술하는 설명을 직접 실험허 보기 바란다.






무비 1




 

           먼저 다각형의 꼭지점의 인덱스는 항상 일관성 있게 정의한다. 박스 그리기 인덱스 배열을 만들 때 이미 언급하였다. 첫 번째 방법으로 미리 셈한 법선을 청회색으로 표시 하였다. 두 번째 방법은 법선을 미리 구하지 않고  다각형의 두 모서리(변)를  벡터로 잡고 이 벡터의 벡터곱을 셈하고 이 벡터곱의 Z성분으로 면의 앞뒷면 가리기로 사용하는 것이다. 

         <무비1> 초기 화면에서 평면(세모꼴)은 Z-Y면에 있는데 인덱스 0,1,2 가 붙어 있다.   미리 구한 법선은 인덱스 0에서 공간 X축을 따라 길이 side인 벡터이다.  이 무른모의 코드 일부를 <표1>에  올려  놓았다.  



1

var  dpt:Number = -1000;

2

var side :Number = 100;

3

 

4

//....

5

 

6

var triDat:Array = new Array([0,0,0],[0,side,0],[0,side/2,side*Math.sqrt(3)/2]);

7

var normal :Array =new Array(side, 0, 0);

8

var rot:Rotation3D = new Rotation3D();

9

var triCur:Array = new Array([0,0,0],[0,side,0],[0,side/2,side*Math.sqrt(3)/2]);

10

var normalCur:Array = new Array(side,0,0);

11

 

12

//.....

13

 

14

for (var i = 0; i <triCur.length ;  i++)

15

triCur[i] = rot.transform3DBody(triDat[i]);

16

normalCur = rot.transform3DBody(normal);

17

//*****

18

 

19

triB.mcmc.lineStyle(1, ColorName.magenta, 100);

20

triB.mcmc.moveTo(0, 0);

21

triB.mcmc.lineTo(Isom.mTSB(normal)[0], Isom.mTSB(normal)[1]);

22

triB.mcmc.moveTo(0, 0);

23

triB.mcmc.lineStyle(3, ColorName.cyan, 100);

24

triB.mcmc.lineTo(Isom.mTSB(normalCur)[0], Isom.mTSB(normalCur)[1]);

25

x01 = Isom.mTSB(triCur[0])[0] - Isom.mTSB(triCur[1])[0] ;

26

        y01 = Isom.mTSB(triCur[0])[1] - Isom.mTSB(triCur[1])[1] ;

27

        z01 = Isom.mTSB(triCur[0])[2] - Isom.mTSB(triCur[1])[2] ;

28

        x21 = Isom.mTSB(triCur[2])[0] - Isom.mTSB(triCur[1])[0] ;

29

        y21 =  Isom.mTSB(triCur[2])[1] - Isom.mTSB(triCur[1])[1] ;

30

        z21 =  Isom.mTSB(triCur[2])[2] - Isom.mTSB(triCur[1])[2] ;

31

        xx = (y21*z01-z21*y01)/side;         

32

        yy = (z21*x01-x21*z01)/side;         

33

        zz = (x21*y01-y21*x01)/side;

34

triB.mcmc.lineStyle(3, colArr[2], 100);

35

triB.mcmc.moveTo(Isom.mTSB(triCur[1])[0], Isom.mTSB(triCur[1])[1]);

36

triB.mcmc.lineTo(Isom.mTSB(triCur[1])[0]+xx, Isom.mTSB(triCur[1])[1]+yy);

37

triB.mcmc.lineStyle(1, ColorUtil.cyan, 100);

38

triB.mcmc.moveTo(0, 0);

39

triB.mcmc.lineTo(Isom.mTSB(triCur)[0], Isom.mTSB(triCur)[1]);

40

 

41

        nz =   Isom.mTSB(normalCur)[2];

42

txtt.write(dpt4, " nz = "+ NumU.doubleFormat(nz, 3),  40, 160, 170, 30,  ColorUtil.cyan, "_sans",  18, true);

43

txtt.write(dpt5," zz = "+ NumU.doubleFormat(zz, 3), 40, 180, 170, 30,

colArr[2], "_sans",  18, true);

44

                        if (zz>0)  colCur = colArr[0];

45

                        else colCur = colArr[1]; 

46

                        triB.mcmc.lineStyle(0, colCur, 85);

47

                        triB.mcmc.beginFill(colCur, 85);

48

        triB.mcmc.moveTo(Isom.mTSB(triCur[0])[0], Isom.mTSB(triCur[0])[1]);

49

for (var i = 1; i<triCur.length; i++)

50

triB.mcmc.lineTo(Isom.mTSB(triCur[i])[0], Isom.mTSB(triCur[i])[1]);

51

triB.mcmc.lineTo(Isom.mTSB(triCur[0])[0], Isom.mTSB(triCur[0])[1]);

52

triB.mcmc.endFill();             

53

        if(zz<0){

54

                                triB.mcmc.lineStyle(3, colArr[2], 100);

55

triB.mcmc.moveTo(Isom.mTSB(triCur[1])[0], Isom.mTSB(triCur[1])[1]);

56

triB.mcmc.lineTo(Isom.mTSB(triCur[1])[0]+xx, Isom.mTSB(triCur[1])[1]+yy);

57

triB.mcmc.lineStyle(3, ColorName.cyan, 100);

58

triB.mcmc.moveTo(0, 0);

59

triB.mcmc.lineTo(Isom.mTSB(normalCur)[0], Isom.mTSB(normalCur)[1]);

60

        }



표1 평면의 앞뒷면 가리기 코드의 일부 리스팅

 


            줄7에 법선을 미리 정의하고  줄16에서 변환된 법선벡타를 줄10의 배열에 담는다.  이 변환된 벡터를 아이소메트릭 공간에 투영했을 때 그 3번째 성분인 Z성분을 줄41에서 nz라 정의하고 화면에 출력한다.  또 다른 법선 산출방법은 다각형의 첫 3꼭지점으로 부터 두개의 벡터,  (1→0)  와 (1→2)  를 구성하여 그 벡터곱을 셈한다. 그런 다음 그 곱의 Z성분을 줄25에서 줄30까지 셈하여 이것을 zz 로 정의하여 화면에 출력한다.   평면이 스크린에 수직이 되면 평면의 법선은 스크린과 평행이 되므로 이 nz 또는 zz 값은 0이 된다. 따라서 이 nz 또는 zz 값을 가지고 평면의 앞뒷면을 가릴 수가 있다. 


 


그림1



그림2



그림3

              <그림1>, <그림2>, <그림3>은 평면이 앞면을 보일 때 nz 또는 zz 값이 음수를 보이다가 평면이 스크린과 수직이 되면 이들 값은 정확히 0을 갖는다.  다시 평면이 뒷면을 보이면 이들 값은 양수가 되는 것을 보이고 있다. 

무비 1을 직접 실험해 보기 바란다.

Posted by 샛솔 :


오일러각
 
        직교축에 대한 회전각으로 강체의 지향을 정하는 것이 부적당하다면 그 대안은 무엇인가?  18세기의 수학의 거장이며 물리학에도 혁혁한 공적을 남긴 오일러(Leonard Euler)가 제안한 오일러각이라는 것이 있다. 

       
        이 것은 특별한 3축에 대한 회전각으로 정의되는데 서로 직교하는 축은 아니다.  이 오일러각으로 강체의 지향을  정하는 것이 물리적으로 가장 좋은 방법인데 이 회전각 묶음을 주면 유일하게 강체의 지향이 결정된다.  뿐만 아니라 이 변수는 강체의 운동방정식을 간단하게 세울 수 있다는 이점이 있다.

        오일러각은 Φ(phi),   θ(theta),   Ψ(psi)로 부르는 세 각의 세트로 표시한다. 

 

       앞 강좌 (박스 그리기)에 보여 준 <표2>의 Rotation3D 클래스의 메쏘드 함수 48줄의 eulerAngles는 이 3개의 각, 즉 Φ(파이),   θ(쎄타),   Ψ(싸이, 또는 프싸이)를 인수로 주면  강체의 지향을 유일하게 결정해 주는 직교 회전 행렬 값을 돌려주는 함수이다. 


        이 튜토리얼에서는 이 함수를 주로 써서 강체의 지향과 회전운동을 기술할 것이다. 또 물리 모델이 도입되어 강체의 회전을 자연스레 기술하자면 역학 방정식을 풀어야 하고 그러기 위해서는 오일러 각을 쓸 수밖에 없다. 트위닝을 위하여는 쿼터니온이  쓰이기도 하지만 운동방정식을 풀기에는 부적당한 변수다.  오일러각이 유일한 대안이다.   앞 강좌 <표2>의 48줄의 수식은 복잡하므로 여기서는 생략하고 관심 있는 독자는 대학 2년차 이상의 역학 교과서를 참조할 것을 권한다. 

       아래의 플래시 무비는 오일러각을 설명하기 위해 만든 것이다.   스라이더를 움직이며 오일러각의 변동과 원테의 지향을 비교해 보기 바란다. 






무비1  오일러각을 설명하기 위한 플래시 무비 
스라이더를 움직이며 오일러각들이
강체의 지향을 어떻게 기술하나 이해할 수 있다.

  

       이제 색채를 띤 원테를 강체로 잡고 오일러각을  설명하기로 한다.  이 원테에 붙어 있는 몸통 직교 자리표는  X축을 노랑색, Z축을 청회색(cyan)으로 표시해 보였다.  몸통 Y축은 그림이 혼잡해지는 것을 피하기 위해 생략한다.  X-Z축이 결정되면  Y축은 오른손 나사법칙에 따라 유일하게 결정되기 때문이다. 








그림1 오일러각  Φ은 공간 Z축에 대한 회전각이다.




그림2 마지막으로 몸통Z축(청회색)을 축으로 Ψ 만큼회전시킨다.


 

     첫째 회전각은 공간 Z축에 대한 회전 각이다. 이 때  공간 X축과 겹쳐 있던 몸통 X축은 공간 X축과 Φ만큼 회전한다. <그림 1>이 이를 보여 주고 있다. 이 때 몸통 X축을 마디선이라 한다. 현재는 몸통 X축과 겹쳐 있지만 일반적으로는 몸통 X축과는 따로 떨어져  항상 공간 X-Y면에 놓이게 된다.(<그림3>참조)  여기서는 녹색으로 표시한다.  다음은 이 마디선을 축으로 하여 θ큼 회전시킨다.  <그림2>가 이 결과를 보여 주고 있다. <그림2>가 보여 주듯 이젠 몸통 Z축은 공간 Z축과 분리되어 θ각을 이룬다. 



 



그림3) 마디선을 축으로 하여 θ 각 회전시킨 결과.

        마지막으로 Ψ는 이젠 겹침이 풀린 몸통 Z축을 중심으로 회전한 각이다. <그림3>이 이것을 보여 준다. 이때 마디선은  몸통 X축과 겹침이 풀려 그 모습을 드러내었다.     다시 한번 종합해 보면 마디선은 공간 X-Y평면과 몸통 X-Y평면이 교차하는 선이고 Φ는 이 마디선이 공간 X축과 이루는 각이다. 그리고 θ는 이 몸통 X-Y면이 공간 X-Y면과 이루는 각으로 몸통 Z축이 공간 Z축이 만드는 각이다.  Ψ는 몸통 X축이 마디선과 이루는 각이다.        

        오일러각은 어떤 각을 먼저 변화시키건 상관없이 같은 직교변환행렬을 만들고 따라서 강체의 지향이 일의적으로
정해진다.  이 무비는 앞 강좌 <표2>의 Rotation3D 클래스의 줄 48에 정의된 메쏘드 함수 eulerAngles를 써서 제어한 것이다.
      

      앞으로는 강체의 지향은 모두 이 오일러각을 써서 제어하기로 한다. 이 방법만이 강체의 지향을 일의적으로 정하
고 또 강체의 회전 운동을 가장 잘 기술할 수 있기 때문이다.
 
 
 
 

Posted by 샛솔 :


박스 그리기 
 

        주사위를 그리기 위한 기초 작업으로 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를 오른쪽
에서 곱해 준다.  즉,   


  를 코딩한 것이다.
 


 

Posted by 샛솔 :


                           
          물리에서는 움직여도 물체의 모양이 변형하지 않는 입체를 강체(rigid body) 라 부른다.  위의 프라톤 입체도 강체로 볼 수 있다.  강체를 그리려면 어떻게 할 것인가?  여기에는 수학과 물리를 피해 갈 도리가 없다.  강체 운동학 그리고 강체동력학이 들어오게 된다.  더욱이 3차원 시늉내기를 자연스럽게 하자면 물리 모델을 불러 쓸 수밖에 없고 따라서  수학과 물리로 이어질 수밖에 없다.  

          앞서 언급한 Digipen 대학의 교과과정을 보면 게임프로그래머를 양성하는 RTIS(실시간 대화형 시뮤레이션)학과의 교과 과정은 전산과학 25과목, 수학과 물리 21과목, 드로잉기초, 미술 감상, 신화, 창작법등은 단 5~6 과목에 불과했다.  그만큼 수학과 물리가  중요하다는 것이다. 

          나는 수학이나 물리도 가르치기에 따라 수학이나 물리에 소양이 없는 사람이라 할지라도 상당히 고급 내용을 소화하고 사용할 수 있게 가르칠 수 있다고 본다. 그것은 앞으로 수학자 물리학자, 수학교육학자, 물리교육학자가 도전해야  할 과제라고 본다.



아이소메트릭 투영법


          컴퓨터 스크린과 같이 2차원 평면에 3차원 영상을 투영한다는 것은 하나의 눈속임이다.   결코 두눈이 보는 입체감을 스크린위에 재현할 수는 없다.  다만 여러 가지 기법을 동원하여 조금 더 시각적 정보를 늘리려는 트릭이라 할 수 있다.




그림 1 주사위를 정면에서


그림 2 주사위를 45˚ 돌아서


그림3 다시 37˚ 올라서서



          <그림1>은  주사위 한 면을 정면에서 본 것으로 주사위의 입체적 정보는 전혀 없다.  오직 한면만 보고 있기 때문이다.  두 째 그림, <그림2>는  45° 도 돌아선 각도에서 본 경우로 두면이 보이고 이 물체가 최소한 구조를 가지고 있다는 정보를 준다.  <그림3>은  다시 약 37° 올라서서   본 것으로  주사위의 3면이 모두 보이고 주사위의 입체감은 최고조에 달한다.   이러한 시각이 아이소메트릭 투영법(Isometric projection)이다.  즉 직각을 이루는 입체의 3면이 모두 같은 비율로 보인다는 뜻이다.  

          이와 같은 투영법은 요지음 게임프로그램에  아주 많이 쓰인다.  아래의 <그림4>는 이러한 투영법을 사용한 게임의 스크린 셧을 몇 컷 옮겨 놓은 것이다.  
 

그림4  게임프로그램의 스크린셧 2개



           아이소메트릭 투영법은 한국화에도 아주 많이 나타나는데 한국화 중에서도 일반 회화가 아닌 경우가 그렇다.  주로 삽화 또는 기록화로 정보를 전달하려는 목적이 강한  경우에 이러한 기법이 자주 쓰인다.  <그림5>는  김홍도의 오륜행실도인데 이는 문자를 모르는 백성들에게 도덕을 가르치려고 편찬된 일종의 그림책이기 때문에 가장 정보를 잘 전 전달할 수 있는 아이소메트릭 투영법이 사용된 것이다.  

그림 5  김홍도의 오륜행실도중 하나


          아래의 플래시 무비는 아이소메트릭 투영법을 얻는 방법을 데모로 보인 것이다.  풀그림을 가르치기 위해 풀그림을 사용하는 방법, 이것이 내가 주장하는 새로운 교육 방법이다.  이 무비는 스크린자리표를 모서리로 하는  정6면체를 어떻게 회전시키면  원하는 각도로 자리표를 나타 낼 수 있는지를 여러 자리표에 대해 시범을 보인 무른모이다. 원하는 투영법 자리표를 래디오 버튼으로 선택하면 정6면체가 회전하여 최종 목표의 자리표(coordinate)로 바뀐다.  

          2 차원 평면에 3차원 물체를 나타 낼 때 쓰는 자리표는 실제로 어떤 각도로 투영하여 보이게 하는가를 이해 할 수 있을 것이다.  

          초기의 스크린 자리표는 오른쪽 수평방향이  +X이고 수직 아래 방향이 +Y 축이 된다.  오른손 나사법칙에 따라 오른손 나사방향 자리표(앞으로 우리는 이 자리표계를 계속 사용한다.)는 스크린 안쪽으로 들어가는 수평방향이 +Z 축이 된다.  오른손 나사법칙방향이란 나사를 +X 축에서 +Y 축방향으로 회전시키면 오른손 나사가 진행하는 방향이 +Z 축이 된다는 뜻이다.  
 

          IsometricA 는 처음에 공간의 X축에 대해 90° 회전시키고 다음에 공간 Y축에 45° 회전시킨다. 그런 다음 다시 공간 X축에 대해 37° 회전시키면 생기는 자리표다.  독자가 직접 이 과정을 실험해 보기 바란다. 직접 해 보면 가장 잘 이해 할 수 있다.

 



플래시 무비 1
이이소메트릭 투영법은 보여 주는 데모 플래시 무비
독자가 위의 선택 버튼을 눌러 실험해 보면 이해가 빠를 것이다.  


            아이소메트릭 자리표를 사용하는 것은 간단한 변환만 하여 주면 된다.  이를 위한 변환 함수를 메쏘드 함수로 포함하고 있는 static class Isom을 만들었다.  <표1>



1

class Isom {

2

static var oneBySqrt2:Number = 1/Math.sqrt (2);

3

static var oneBySqrt6:Number  = 1/Math.sqrt (6);

4

static var oneBySqrt3:Number  = 1/Math.sqrt (3);

5

 

6

static function mTSA (pt3D:Array ):Array {

7

        var x:Number = oneBySqrt2*(pt3D[0]+pt3D[1]);

8

        var y:Number = oneBySqrt6*(pt3D[0]-pt3D[1]-2*pt3D[2]);

9

var z:Number = oneBySqrt3*(pt3D[1]-pt3D[0]-pt3D[2]);

10

        return [x, y, z];

11

};//function mTSA

12

 

13

static function mTSB (pt3D:Array):Array {

14

        var x:Number = oneBySqrt2*(-pt3D[0]+pt3D[1]);

15

        var y:Number = oneBySqrt6*(pt3D[0]+pt3D[1]-2*pt3D[2]);

16

var z:Number = oneBySqrt3*(-pt3D[1]-pt3D[0]-pt3D[2]);

17

        return [x, y, z];

18

};//function mTSB

19

}//class Isom


코드 리스팅 표 1
아이소메트릭 변환 static class


          코드 리스팅  <표1>이 바로 그것인데 여기에는 두개의 메쏘드 함수 mTSA(map to screen A)와 mTSB가 있다.    임의의 3차원 자리표를 2차원 컴퓨터 스크린에 나타내려면  3차원 자리표를 [x, y, z]와 같은 배열로 만들어 이를 이 함수의 인자로 전달하면 되돌려 받는 함수 값이 스크린 자리표이다.  이 배열 역시 3차원 자리표인데 x, y만 2차원 스크린 자리표로 쓰고 z값은 여러 물체가 겹쳐 있을 때에는 깊이 변수로 쓸 수 있다.  z값이 크면 스크린 깊게 있으므로 먼저 그려 안쪽 깊숙히 있게 하고  z값이 작으면 나중에 그려 스크린 앞쪽으로  나오게 한다.  

           아이소메트릭 자리표가 3차원 강체를 그리는데 반드시 필요한 것은 아니다. 그런데도 이 이야기를 꺼낸 것은 3차원 강체의 운동을 기술하는 방법을 설명하기 위한 것이다.  앞으로의 이 튜토리얼에서는 모두 아이소메트릭 투영법B, 즉  static 함수 Isom.mTSB( pt3D:Array)를 불러 변환을 한 다음 스크린에 나타내게 한다.
 
Posted by 샛솔 :


 

         
        나는 어렸을 때 수학을 무척 좋아 했다.  중학교 3학년 때 625 전쟁이 났는데 피난 가서 단칸방에 가족들이 모두 함께 살 때 사과 궤짝을 책상 대신 놓고 "그랜빌"과 "러브"의 미적분학 책을 혼자 공부하면서 수학에 매료되었던 생각이 난다.  고등학교 때에는 미분방정식이나 함수론 책을 헌 책방에서 구해서 잘 이해를 못하면서도 높은 수준의 수학을 혼자 할 수 있다는 자긍심으로 만족해 했던 생각이 아직도 기억에 남는다.  

        수학은 인류가 만들어 낸 가장 위대한 건축물이고 교향악이다.  수학을 아름다운 지적 건축물로 비유한 사람은 많았지만 교향악이라 부른 이는 많지 않았다. 내가 대학 시절에 읽은 일본 수학자들이 쓴 응용수학 총서의  어떤 저자가 그렇게 표현했다.  “프리에 급수와  편미분방정식이 짜는 교향악에 심취했다”고 그는 회고했다.  나는 그 표현에 아주 공감했다.  내가 물리를 전공하게 된 계기는  그렇게 아름다운 수학이 물리를 기술하는데 쓰인다는 데에 감탄하고는 물리에 흠뻑 반해 버렸기 때문이다. 

        사실 내게는 수학과 물리의 구별이 없다.  그것은 물리학의 역사와 무관하지 않다. 물리학의 창시자 뉴턴은 물리학자인 동시에 수학자이다. 그리고 18세기와 19세기의 이론 물리학의 거장들은 동시에 해석학의 거장들이었다. 그들에게도 역시 수학과 물리학은 같은 것이었다.   

        이처럼 나는 수학과 물리의 아름다움에 이끌려 물리학을 하게 된 것이다.  내가 가장 좋아하는 글귀는 카오스 이론의 원조인  프랑스의 대 수학자, 물리학자인 앙리 푸앙카레의 말이다.  그는 "과학과 방법"이라는 책에서 다음과 같은 말을 하였다.  “과학자는 유용성때문에 자연을 연구하지 않는다.  그는 연구가 즐겁기 때문에 하는 것이다. 왜 즐거운가 하면 자연은 아름답기 때문이다.  물론 내가 여기서  말하는 “미”란  우리의 감각을 자극하는 질감이나 형상의 감성적 “미”를 말하지 않는다.  물론 나는 그런 “미”를 깎아 내리려는 의도는 추호도 없다.  그러나 그런 “미”는 과학과는 무관한 것이다. 내가 말하는 “미”란 자연의 각 부분이 만드는 조화로운 질서에서 오는 그런 “아름다움”, 오직 순수한 지성만이 포착할 수 있는 그런 “미”를 말한다.”   내가 이 말에 작은 꼬리를 달아 설명하자면  "아름다운 수학으로 기술할 수 있는 자연이기에  자연을 연구하는  과학자는 미를 추구한다고 할 수 있다." 라는 뜻이라 할 수 있겠다. 

        그런데 대학에 몸담고 물리학을 전공할 때 컴퓨터가 등장했다.  수학이나 물리나 그 궁극적 목표는 계산에 있다.  수치를 답으로 얻어야 한다. 헨리치(Peter Henrici)같은 수학자는 계산할 수 없는 수학은 수학이 아니다 라고까지 극언을 했다. 나는 항상 계산하는데 흥미가 있었다. 그래서 수치계산을 많이 했다.  통계물리를 전공했고 그 중에서도 고비현상(critical phenomena) 과 상전이(phase transition) 문제에 많은 관심을 가졌다. 그런데 상전이는 통계물리학에서도 이른바 함수의 특이점(singular point) 을 연구하는 것이다. 상전이라는 것은  물이 언다든가 물이 끓어서 기체가 된다든가 해서 그 성질이 아주 다른 상으로 바뀌는 현상이다. 이렇게 급작스럽게 물질의 성질이 극단적으로 바뀌는 현상은 그 상태를 기술하는 어떤 함수의 특이점을 연구함으로서 이론적으로 이해가 된다.
 
        특이점은 계산이라는 관점에서 보면 아주 어려운 주제가 된다. 무엇인가 비정상적이 된다는 뜻이니까.  불연속이 된다든가 미분이 안 된다든가 하는 따위를 말한다. 그런데 이런 수학적 문제를 컴퓨터를 써서 풀 수 있는 방법이 50년대 이후 컴퓨터의 발달과 더불어 크게 발달하였다. 그래서 나는 이 연구를 하는데 이제까지의 내 생애를 바쳤다고 해도 지나친 말이 아니다.  자연히 컴퓨터와 친숙하게 되고 컴퓨팅에는 일가견을 갖게 되었다.  



 그림1) HP-65, programmbable calculator


          처음에는 프로그래머블 포켓 캘큐레이터(programmable pocket calculator)로 계산을 했다. 이제는 그런 제품은 골동품이 되었다. 휴렛 패커드와 텍사스 인스트르먼트에서 이런 휴대용 계산기를 생산했었다. 그러다 PC 가 나오면서 세상에는 컴퓨터 혁명이라는 것이 일어났다. 이 혁명과 더불어 물리학의 연구 방법도 가히 혁명적이라고 할 수 있는 변화가 일어났다. 전산물리라는 분야가 새로 생긴 것이다. 그리고 과거에는 꿈도 꿀 수 없던 어려운 문제들이 이 연구 방법에 의해서 풀리게 된다. 


         그중에서도 가장 잘 알려진 예가 카오스의 문제다.  카오스는 물리학에 오래 전부터 존재하여 왔다. 그러나 도대체 어떻게 그 문제를 다룰지 카오스 현상이란 무엇인지 아무도 쉽에 대답할 수 없었다.   19세기 말 수학자, 물리학자인, 앞서 말한  푸앙카레만이 카오스의 성격을 어렴풋이 드려다 보고 그 누구도 이해할 수 없는 기막힌 세계의 존재를 예언했을 뿐이다. 그런데 이제  컴퓨터의 발전이 푸앙카레가 들여다 본 무시무시하게 복잡한 세계를 모니터위에 그려 볼 수 있게 해 준 것이다.


         이런 연유로 나는 PC 가 출현하기 전부터 프로그래밍을 하였다. 그리고는 베이직, 파스칼, C, Assembler, C++, Java 로 과학계산을 하는 모든 언어들을 따라 배웠다. 그래서  앞서 말한 대로 1997 년 처음 서울대학교 물리학부에 전산물리라는 과목이 신설되었을 때 그 과목을 개발하고 강의하는 교수가 되었던 것이다.

      

        첫해는 C++ 언어로 이 과목을 운영했으나 Java 가 나오면서 Platform independent 라 하는 장점을 고려해 그 이듬해부터는 이 과목의 공식언어를 Java 로 정했다.   퇴임한 후 Java 도 많이 발전했고 먼저 만든 소프트웨어들도 손 볼 때가 있어 다시 이 시늉내기 프로그램들을 들여다 보다 Flash MX 를 대하게 되었다.  그리고 Actionscript 라는 것을 알게 되었다.  그래서 이 새로운 동영상 제작 소프트웨어를 만나게 된 것이다.

        
         내가 Flash programming을 하게 된 직접적인 동기는 전에 Java 로 만든 대학 일학년 일반 물리중  열물리의 보조교재 “주사위만 던져도 열 물리의 기본을 이해할 수 있어요.”(http://phya.snu.ac.kr/~kclee/dice/)를 업데이트 할 가 하던 중 주사위의 동영상을 그릴 필요가 생긴 데에 있었다. 주사위를 그리기 위해 정6면체를 그리다 보니 주사위보다 Flash에 더 매료되어  Java를 제쳐놓게 되었다. 그러다 보니 엉뚱하게 프라톤 입체 몇 개를 더 그리게 되었고 마침내 Flasher 가 되었다. 아래 동영상은 2002년 가을에 플래시를 배우고 Flash MX Actionscript 만으로 제작한 프라톤 입체들이다. 아래의 주소에 가면 이 그림들을 볼 수 있다.

       
   아래의 그림에 마우스를 클릭하면 프라톤 입체들이 마우스 방향으로 회전한다.  마우스가 입체에서 멀 수록 빨리 돈다.




 










프라톤 입체들 위로부터
정4면체, 정6면체,정8면체,정12면체,정20면체들이다.
프라톤 입체는 5개만 존재한다.
 
 
나는 이 글에서 내가 처음 그린 주사위를 Actionscript 2.0 으로 업데이트하여 설명하고자 한다. 

 

Posted by 샛솔 :


 
        

         플래시는 과학과 예술의 만남이라 할 수 있다.  사실 모든 컴퓨터 풀그림은 수학과 물리에 기반을 둔 컴퓨터 과학의 산물이다.  아무리 아름다운 영상도 따지고 보면 하나의 이진수(bianry number)의 행진일 뿐이다. CRT 모니터엔 전자빔이 형광물질을 때려서 빛을 내고 TFT(Thin Film Transister) 스크린에서는 컴퓨터의 그래픽카드에서 보내는 신호가 발광트랜지스터로 하여금 빛을 내게 한다. 그래서 아름다운 영상을 생성한다.  우리는 그러한 그래픽 카드의 작동 원리나 모니터의 발광원리를 다 잊고 플래시의 무비 클립만 제작하면 되지만 그 뒤에는 과학과 기술이 뒷받침해 주고 있다.

        사실 플래시의 작동원리도 따지고 아주 고급 수학의 결정체라 할 수 있다.  우리는 그것을 모르고도 마우스 몇 번 클릭하여  원하는 효과를 얻고 있을 뿐이다.   

        아래의 그림은 그라데이션 효과를 낸 간단한 도형이다.  그림 1은 원과 직사각형에 그라데이션 효과를 내어 채우기를 한 것이다.  플래시에는 선형(linear) 과 원형(radial) 두 가지만 있다.  




그림1


그림2


그림3



        <그림1> 은 저작 툴을 써서 그린 것이고 <그림2>의 아래 부분은 아래와 같은 코드를 써서 그린 것이다.


1

matrix = {matrixType:"box", x:10, y:250, w:500, h:100, r:0};

2

var colors = [0xFF0000, 0x00FFFF];

3

var alphas = [100, 100];

4

var ratios = [0, 255];

5

beginGradientFill("linear", colors, alphas, ratios, matrix);

6

moveTo(10, 251);

7

lineTo(510, 251);

8

lineTo(510, 350);

9

lineTo(10, 350);

10

lineTo(10, 251);

11

endFill();

표1 간단한 그라데이션 코드



 <그림3>은 어떻게 그렸나?  이 채우기는  저작툴로는 할 수 없다.  그렇다고  beginGradientFill 로도 할 수 없다.  이런 Fill 타입은  beginGradientFill 에는 없기 때문이다. 

           그렇다면 그라데이션 채우기(GradientFill)란 어떻게 해야 하는가?  이것은 다름 아닌 색깔의 트위닝이다.  한 색깔에서 다른 색깔로 점차 바뀌게 하는 것은 아래와 같은 간단한 기법으로 트위닝을 하면 된다.  먼저 양끝 색깔을 3원색 성분 RGB(Red, Green, Blue)으로 분해한 다음 성분별로 양끝사이를 균등하게 사이채움하는 그라데이션을 만든다. 마지막으로 이들 성불을 다시 합성하여 그라데이션 스펙트럼을 만드는 것이다.  (<표2> 참조)


1

class ColorUtil {

2

        static function makeCG(colorB, colorE, nn):Array {

3

                var result = [];

4

                var blueB = colorB%256;

5

                var blueE = colorE%256;

6

                var greenB = ((colorB-blueB)/256)%256;

7

                var greenE = ((colorE-blueE)/256)%256;

8

                var redB = (colorB-blueB-greenB*256)/65536;

9

                var redE = (colorE-blueE-greenE*256)/65536;

10

                for (var i = 0; i<nn; i++) {

11

result[i] = blueB+(blueE-blueB)*i/nn +Math.round((greenB+(greenE-greenB)*i/nn))*256

12

+ Math.round((redB+(redE-redB)*i/nn))*65536;

13

                }

14

                return result;

 

        }

 

}

표2 그라데이션을 주는 색깔 배열을 만드는 static class 함수


 
         이 static class의 static method 함수 makeCG(colorB, colorE, nn)을 첫 색깔  colorB 와 끝 색깔 colorE 사이의 nn 단계의 그라데이션 색깔의 배열을 만들라는 명령이다. 이렇게 만든 nn개의 색깔을 채우기하려는 영역을 nn 개로 나누어 이 배열에 들어 있는 색깔로 차래대로 채우면 그라데이션 효과가 생기는 것이다.  그림2의 윗부분은 아래와 같은 코드로 그려진 것이다. 



1

var n = 100;

2

var wdth = Math.round(500/n);

3

var colorAB = ColorUtil.makeCG(0xFF0000, 0x00FFFF, n);

4

for (var i = 0; i<n; i++) {

5

        lineStyle();

6

        beginFill(colorAB[i], 100);

7

        moveTo(10+wdth*i, 150);

8

        lineTo(10+wdth*(i+1), 150);

9

        lineTo(10+wdth*(i+1), 250);

10

        lineTo(10+wdth*i, 250);

11

        lineTo(10+wdth*i, 150);

12

        endFill();

13

}

표3 그라데이션 주기


<그림2>의 beginGradientFill 로 채운 아래 부분과 이 <표3>의 코드로 채운 윗부분은 같은 효과를 주고 있다는 것을 확인 할 수 있다.

 
        이러한 그라데이션 사이채움 색깔 배열을 만들면 여러 다른 타입의 사이채움을 할 수 있다.  사실 나는 <그림3>과 같은 사이채움이 하고 싶어 이 ColorUtil.makeCG 함수를 만들었던 것이다. <그림3>은 아래와 같은 코드로 그린 것이다.


 

1

_x = 225;

2

_y = 200;

3

var radius = 100;

4

var n = 60;

5

var step = 2*Math.PI/3/n;

6

var ang1 = step*n;

7

var ang2 = step*n*2;

8

var colorA = ColorUtil.makeCG(0xFFFF00, 0xFF00FF, n);

9

var colorB = ColorUtil.makeCG(0xFF00FF, 0x00FFFF, n);

10

var colorC = ColorUtil.makeCG(0x00FFFF, 0xFFFF00, n);

11

lineStyle();

12

for (var i = 0; i<n; i++) {

13

        beginFill(colorA[i], 100);

14

        moveTo(0, 0);

15

        lineTo(radius*Math.cos(step*i), radius*Math.sin(step*i));

16

        lineTo(radius*Math.cos(step*i+step), radius*Math.sin(step*i+step));

17

        lineTo(0, 0);

18

        endFill();

19

        beginFill(colorB[i], 100);

20

        moveTo(0, 0);

21

        lineTo(radius*Math.cos(step*i+ang1), radius*Math.sin(step*i+ang1));

22

        lineTo(radius*Math.cos(step*i+step+ang1), radius*Math.sin(step*i+step+ang1));

23

        lineTo(0, 0);

24

        endFill();

25

        beginFill(colorC[i], 100);

26

        moveTo(0, 0);

27

        lineTo(radius*Math.cos(step*i+ang2), radius*Math.sin(step*i+ang2));

28

        lineTo(radius*Math.cos(step*i+step+ang2), radius*Math.sin(step*i+step+ang2));

29

        lineTo(0, 0);

30

        endFill();

31

}

표4 <그림3>의 방사형 그라데이션 효과주기

 

 

       내가 여기서 이 보기를 든 것은 플래시라는 풀그림속에는 곳곳에 물리와 수학이 숨어있다는 것을 보이기 위해서였다.  그런 의미에서 플래시는 과학과 예술의 만남이라 불러도 지나친 말이 아닐 것이다. 
 

Posted by 샛솔 :