물고기 Simulator로 구현된 Procedural Animation(실시간으로 자동 생성되는 애니메이션)

By Josh Marinacci, September 24, 2008

이 예제는 간단한 procedural 애니메이션을 어떻게 만드는가를 보여줍니다. 여러분이 화면 어디든 클릭한뒤 붙들고 있으면 잔물결들이 나타나고 물고기가 잔물결의 중앙을 향해서 꼬리를 흔들면서 움직입니다. 잔물결들과 물고기 모두 procedural 애니메이션의 예제입니다..

코드 이해하기

이 예제에는 두개의 기본적인 살아있는 물체가 있습니다.: 물고기와 잔물결들 Figure 1의 코드는 물고기가 어떻게 만들어 졌는지를 보여줍니다. 이 물고기는 꼬리는 만드는 마지막 한개를 제외하고 각각 앞의 ImageView를 일정비율로 줄이고 약간씩 차감 계산하는 ImageView들의 집합입니다. 여기에는 실제 이미지는 단지 한개 뿐이지만 크기를 조절하여 물고기 몸체의 환영을 만듭니다.

Source Code
public class Fish extends CustomNode {
    // controls the angle of each segment, used for the wag animation
    var ang:Number = 0.0;
    // the heading of the entire fish
    var heading:Number = 0.0;
    
    override public function create():Node {
        var g:Group = Group {
            transforms: bind Transform.rotate(heading+90,0,0)
        };

        var orig = g;

        for(i in [0..4]) {
            var scale = 0.6 * (5 - i) / 3;
            if(i==4) {
                scale = 0.7;
            }
            var newg = Group {
                transforms: bind [
                    Transform.translate(20,0),
                    Transform.rotate(ang,20,0)
                ]
                content: [
                    ImageView {
                        scaleX: scale
                        scaleY: scale
                        translateY: -50;
                        image: Image { url: "{__DIR__}scale2.png" }
                    },
                ]
            }
            insert newg into g.content;
            // add eyeballs to the first one
            if(i == 0) {
                var eyeSpacing = 10;
                var eyeOffset = 20;
                insert Circle { centerX: eyeOffset centerY: eyeSpacing radius: 10 fill: Color.WHITE } into newg.content;
                insert Circle { centerX: eyeOffset centerY: -eyeSpacing radius: 10 fill: Color.WHITE } into newg.content;
                insert Circle { centerX: eyeOffset-4 centerY: eyeSpacing radius: 5 fill: Color.BLACK } into newg.content;
                insert Circle { centerX: eyeOffset-4 centerY: -eyeSpacing radius: 5 fill: Color.BLACK } into newg.content;
            }
            g = newg;
        }

        return orig;
    }

Figure 1: 물고기 생성 코드

이 물고기는 두가지 다른 애니메이션을 제공합니다. 첫번째는 일정한 꼬리 움직임 입니다. 물고기의 각 부분은 단 하나의 ang 변수에 회전 변환 반경을 가지고 있습니다. 이 변수를 변경함으로서 여러분은 물고기 전체가 흔들거리는 움직임을 만들수 있습니다. 흔들거리는 애니메이션은 Timeline을 따라 구현되어 집니다. 흔드는 애니메이션은 ang 변수를 +15도에서 -15까지 사이를 왕복하는 다음의 Timeline에 의해서 구현됩니다. autoReverse는 그것이 왕복하기 위해서 true가 됩니다.repeatCount는 계속 실행되게 하기 위해서 Timeline.INDEFINITE를 설정합니다.

Source Code
    //make angle loop from -10 to +10 degrees
    public var wag = Timeline {
        keyFrames: [
            at(0s) { ang=> -10.0 tween Interpolator.EASEBOTH },
            at(1s) { ang=>  10.0 tween Interpolator.EASEBOTH },
        ]
        autoReverse: true
        repeatCount: Timeline.INDEFINITE
    };

Figure 2: 흔들거리는 애니메이션

이동 애니메이션은 goTo에 넘겨진 좌표에 기초한 마지막 위치와 새로운 각도를 계산함으로서 구현됩니다. 때문에 이 함수는 어떻게 이동하는지에 대한 모든 정보를 숨기기 때문에 코드 밖에서는 이것에 대해 어떠한 것도 알수 없습니다.. 그것은 'go to here'라고 부르는 좀더 높은 단계와 관계가 있는데 물고기는 그것을 처리할 것입니다.이 캡슐화는 좀더 높은 수준의 프로그래밍이 가능하게 해줍니다.

Source Code
    var move:Timeline = Timeline { };
    public function goTo(x:Number, y:Number):Void {
        move.stop();
        var xoff = x - translateX;
        var yoff = y - translateY;
        var angg = calcAngle(translateX, translateY, x, y);
        var dist = Math.sqrt(xoff * xoff + yoff * yoff);
        
        var t = 1s * dist / 100.0; //speed = 100px / sec
        move = Timeline {
            keyFrames: [
                KeyFrame {
                    time: t
                    values: [
                        heading => angg tween Interpolator.LINEAR,
                        translateX => x tween Interpolator.LINEAR,
                        translateY => y tween Interpolator.LINEAR,
                    ]
                }
            ]
        }
        move.play();
    }

    function calcAngle(x1:Number, y1:Number, x2:Number, y2:Number):Number {
        var xoff = x2-x1;
        var yoff = y2-y1;
        var angle = Math.atan(yoff/xoff);
        if (xoff < 0) {
            angle = angle + Math.PI;
        }

        if (angle < 0) {
            angle += 2 * Math.PI;
        }
        if (angle > 2 * Math.PI) {
            angle -= 2 * Math.PI;
        }
        return Math.toDegrees(angle);
    }

Figure 3: goTo function

잔물결들 만들기

물고기는 내부 애니메이션에서 하나의 캐릭터 이기때문에 잔물결들은 입자 시뮬레이션과 같습니다. The RippleGenerator 클래스는 createRipple 함수가 호출될때마다 매번 새로운 잔물결을 만들어 냅니다. 이 함수는 Ripple의 새로운 인스턴스를 생성하고 그것을 화면에 놓고 또 애니메이션을 시작합니다. 또한 3초후에 잔물결을 없애는 두번째 타임라인을 생성합니다.

Source Code
public class RippleGenerator extends CustomNode {
    var ripples:Node[];
    public var generatorCenterX = 100.0;
    public var generatorCenterY = 100.0;
    
    override public function create():Node {
        return Group{
            content: bind ripples;
        }
    }
    
    public function createRipple():Void {
        var rip = Ripple { 
            centerX: generatorCenterX
            centerY: generatorCenterY
        };
        insert rip into ripples;
        rip.anim.start();
        var remover = Timeline {
            keyFrames: [
                KeyFrame { 
                    time: 3s 
                    action:function() { 
                        delete rip from ripples; 
                        rip.anim.stop(); 
                    } 
                },
            ]
        };
        remover.start();
    }
    
    public var generate = Timeline {
        keyFrames: KeyFrame {
            time: 0.5s
            action: createRipple
        }
        repeatCount: Timeline.INDEFINITE
    }
}

Figure 4: RippleGenerator.fx File

잔물결은 특정 색상들을 갖기위해 override 된 circle의 하위 클래스 입니다. 또한 Figure 5에서 보여지는 것처럼 실제적으로 커지고 페이딩 되는 행동의 애니메이션을 추가합니다.

Source Code
class Ripple extends Circle {
    override var stroke = Color.rgb(200,200,255);
    override var fill = null;
    override var centerX = 100;
    override var centerY = 100;
    var anim = Timeline {
        keyFrames: [
            at(0s) { radius => 0 tween Interpolator.LINEAR },
            at(1s) { opacity => 1.0 tween Interpolator.LINEAR },
            at(3s) { radius => 100 tween Interpolator.LINEAR }
            at(3s) { opacity => 0.0 tween Interpolator.LINEAR }
        ]
    }
}

Figure 5: the Ripple class

Pulling it Together

모든 조각들을 서로 통합하기 위해서 Figure 6의 Main 클래스는 물고기를 생성하고 흔들어 움직이는 동작을 시작하고 잔물결 생성기(제너레이터)를 만들고 나서 마우스 클릭을 감지하기 위해서 투명하게 겹쳐진 사각형의 Stage안으로 모두 합치게 됩니다.

Source Code
var fish = Fish { 
    translateX: 0
    translateY: 0
};
fish.wag.start();

var ripper = RippleGenerator { };

var w = 800;
var h = 500;

Stage {
    //closeAction: function() { java.lang.System.exit(0); }
    //visible: true
    scene: Scene {
        content :
            Group { content: [
                // a colorful background
                Rectangle { width: w height: h
                    fill: RadialGradient {
                        radius:500
                        focusX: 0
                        focusY: 0
                        centerX: 300
                        centerY: 300
//                        cycleMethod:
                        proportional: false
                        stops: [
                            Stop {offset: 0 color: Color.BLACK },
                            Stop {offset: 1 color: Color.BLUE  },
                        ]
                    }
                },
                
                fish, // the fish
                ripper, // the ripple generator
                
                // an overlay to capture mouse events
                Rectangle {
                    fill: Color.rgb(255,255,255,0.0)
                    width: w
                    height: h
                    onMousePressed: function(e:MouseEvent) {
                        ripper.generatorCenterX= e.x;
                        ripper.generatorCenterY= e.y;
                        ripper.createRipple();
                        ripper.generate.start();
                        fish.goTo(e.x,e.y);
                    }
                    onMouseDragged: function(e:MouseEvent) {
                        ripper.generatorCenterX= e.x;
                        ripper.generatorCenterY= e.y;
                        fish.goTo(e.x,e.y);
                    }
                    onMouseReleased: function(e:MouseEvent) {
                        ripper.generate.stop();
                    }
                },

                ] 
            }
    }
}

Figure 6: Main.fx

예견

훗날 이 어플리케이션의 가장 큰 발전은 좀더 많은 물고기들이 추가될것이라는 겁니다. 지금 물고기는 어떻게 움직이고 어떻게 흔들지를 알지만 여러분은 헤엄쳐서 자신보다 작은 작은 물고기들을 추격하거나 서로 그룹을 형성하거나 잠시동안 길을 잃는 "영리한" 믈고기를 추가할수 있을 것입니다. 새로운 움직임이 Fish 클래스 안에 캡슐화만 되어 있다면 여러분은 쉽게 다른 프로그램들의 물고기에 재사용할수 있습니다.