Nov 05

To już ostatnia część mojej wypowiedzi na temat krzywych Béziera. Pokażę sposób wyliczania położenia punktów na krzywych za pomocą algorytmu de Casteljau i jego implementację w AS 3.0.

Przyjmijmy, że naszą krzywą określa n+1 punktów P0, P1, P2 … Pn. Każdy z odcinków |P0P1| , |P1P2| , … , |Pn-1Pn| dzielimy w stosunku t : (1-t), otrzymując n punktów A0, A1, A2, … , An-1. Po raz kolejny odcinki |A0A1| , |A1A2| , … , |An-2An-1| dzielimy w stosunku t : (1-t), wyznaczając n – 1 punktów B0, B1, B2, … , Bn-2. Operację powtarzamy do momentu, w którym ilość wygenerowanych punktów jest równa 1. W ten sposób otrzymujemy punkt krzywej dla parametru t.

Jeśli wykonamy tę operację dla wszystkich t z przedziału <1 , 0> wyznaczymy wszystkie punkty krzywej Béziera.

Działanie algorytmu, który wyznacza punkt dla t = 0.5 krzywej określonej punktami P0, P1, P2, P3, P4 ilustruje rysunek
deCasteljau

Implementacja funkcji, które wyznaczają punkty krzywej Béziera

package
{
    public class DeCasteljauBezier
    {
        import flash.geom.Point;
       
        /**
            Funkcja zwraca wspolrzedne punktu X,
            ktory dzieli odcinek |p0p1| tak, ze |p0X|/|Xp1| = t.
        */

        public static function calculatePoint   (
                                                    p0 : Point,
                                                    p1 : Point ,
                                                    t : Number
                                                ) : Point
        {
            return new Point(p0.x + (p1.x-p0.x) * t , p0.y + (p1.y - p0.y) * t);
        }
       
        /**
            Funkcja zwraca wspolrzedne punktu krzywej Beziera dla parametru t.
        */

        public static function deCasteljauAlgorithm (
                                                        points : Array,
                                                        t : Number
                                                    ) : Point
        {
            if(points.length == 1) return points[0];
            else
            {
                var outputPoints : Array = new Array(points.length - 1);
           
                for( var i : int = 0 ; i < outputPoints.length ; i++)
                {
                    outputPoints[i] = calculatePoint(points[i] , points[i+1] , t);
                }
               
                return deCasteljauAlgorithm(outputPoints , t);
            }
        }
       
        /**
            Funkcja wylicza tablice punktow krzywej Beziera
            o punktach kontrolnych controlPoints z dokladnoscia do parametru deltaT.
        */

        public static function calculateCurvePoints (  
                                                        controlPoints : Array ,
                                                        deltaT : Number = 0.05
                                                    ) : Array
        {
            var outputPoints : Array = new Array();
           
            for ( var t : Number = 0.00 ; t <= 1.00 ; t += deltaT )
            {
                outputPoints.push(deCasteljauAlgorithm(controlPoints , t));
            }
           
            return outputPoints;
        }
    }
}

DeCasteljauBezier.as

oraz Document Class, który jest zmodyfikowaną wersją DC z części drugiej mojego artykułu na temat krzywych Béziera.

package
{
    import flash.display.Sprite;
    import flash.geom.Point;
    import flash.events.MouseEvent;
    import flash.text.TextField;
   
    public class DeCasteljauTestDC extends Sprite
    {
        const       n       : int       = 8;
        const       deltaT  : Number    = 0.02;
       
        private var _points : Array     = new Array(n + 1);
       
       
        public function DeCasteljauTestDC()
        {          
            for( var j : int = 0 ; j < (n + 1) ; j++ )
            {
                var point : Sprite = new Sprite();
                   
                    point.graphics.beginFill(0xad1f47 , 1);
                    point.graphics.drawCircle(0,0,3);
                    point.graphics.endFill();
                   
                    point.addEventListener  (
                                                MouseEvent.MOUSE_DOWN,
                                                mouseDownEventHandler
                                            );
                   
                    point.addEventListener  (
                                                MouseEvent.MOUSE_UP,
                                                mouseUpEventHandler
                                            );
                   
                    point.mouseChildren = false;
                    point.buttonMode    = true;
                   
                    var tf : TextField  = new TextField();
                   
                        tf.autoSize     = 'left';
                        tf.selectable   = false;
                        tf.textColor    = 0xad1f47;
                        tf.text         = 'P_'+ j;
                   
                    point.addChild(tf);
                   
                    point.x = Math.random() * 400;
                    point.y = Math.random() * 400;
                   
                _points[j]  = addChild(point);
            }
        }
       
        /**
            Events handlers
        */

       
        private function mouseDownEventHandler(event : MouseEvent) : void
        {
            (event.target as Sprite).addEventListener   (
                                                            MouseEvent.MOUSE_MOVE,
                                                            mouseMoveEventHandler
                                                        );
            (event.target as Sprite).startDrag(false);
        }
       
        private function mouseUpEventHandler(event : MouseEvent) : void
        {
            (event.target as Sprite).stopDrag();
        }
       
        private function mouseMoveEventHandler(event : MouseEvent) : void
        {
            /**
                Funkcja przy kazdej zmianie pozycji dowolnego z punktow kontrolnych,
                czysci okno i rysuje nowa krzywa.
            */

           
            graphics.clear();
            graphics.lineStyle(1,0x999999 , 1);
            graphics.moveTo(_points[0].x,_points[0].y);

            var controlPoints : Array = new Array();
           
            for( var j : int = 0 ; j < _points.length; j++ )
            {
                controlPoints.push(new Point( _points[j].x , _points[j].y ));
            }

            var points : Array =
            DeCasteljauBezier.calculateCurvePoints(controlPoints,0.01);

            for (var i : int = 0; i < points.length; i++)
            {
                graphics.lineTo (points[i].x,points[i].y);
            }
        }
    }
}

DeCasteljauTestDC.as

Nie zamieszczam obrazka okna, wyświetlającego krzywą, ponieważ wygląda ono identycznie jak w części drugiej – zmienił się tylko sposób implementacji metody rysującej.

Kody źródłowe oraz pliki fla i swf do pobrania stąd.

  • Nov 04

    W części pierwszej pokazałem jak w prosty sposób można narysować krzywą Beziera stopnia trzeciego.

    W tej części pokażę implementację funkcji, które pozwolą rysować krzywe wyższych stopni.

    Wracam do wzoru (w1)

    Widzę, że potrzebuję metody, która zwróci mi wartość wielomianu b.

    Wielomian ten opisuje wzór (w2)

    Aby wyznaczyć jego wartość w zależności od parametrów  i , n , t potrzebuję jeszcze funkcji, która zwróci mi wartość dwumianu Newtona.

    Intuicyjne implementacje tych funkcji zamieszczam poniżej

    package
    {
        import flash.errors.IllegalOperationError;
       
        public class MathUtils
        {
            /**
                Funkcja zwraca wartosc n!
            */

            public static function factor(n : int) : Number
            {
                if(n < 0 )
                {
                    throw new IllegalOperationError
                    ('Wartosc n musi byc dodatnia liczba calkowita!');
                }
                else
                {
                    var result : Number = n;
                   
                    if(n == 0 || n == 1)
                    {
                        result = 1;
                    }
                    else
                    {
                        for(var k : int = n-1 ; k > 0 ; k--) result *= k;
                    }
                   
                    return result;
                }
            }
           
            /**
                Funkcja zwraca wartosc dwumianu Newtona 'n po k'
            */

            public static function binomialCoefficient(n : int, k : int) : Number
            {
                if(n < 0 )
                {
                    throw new IllegalOperationError
                    ('Wartosc n musi byc nieujemna liczba calkowita ( n >= 0 )!');
                }
                else if(k < 0 )
                {
                    throw new IllegalOperationError
                    ('Wartosc k musi byc nieujemna liczba calkowita ( k >= 0 )!');
                }
                else if(k > n)
                {
                    throw new IllegalOperationError
                    ('Wartosc k nie moze byc wieksza od n ( k <= n )');
                }
                else
                {
                    return factor(n) / ( factor(n - k) * factor(k) );
                }
            }
           
            /**
                Funkcja zwraca wartosc wielomianu Bernsteina B_i_n(t).
            */

            public static function bernstainPolynomial(i : int, n : int, t : Number):Number
            {          
                if(i > n || i < 0)
                {
                    return 0;
                }
                else
                {
                    return  binomialCoefficient(n , i)
                            * Math.pow(t , i)
                            * Math.pow((1-t),n-i);
                }
            }
        }
    }

    MathUtils.as

    Stworzyłem także nowy Document Class, w którym krzywa jest rysowana przy zmianie położenia kursora myszki w czasie przeciągania dowolnego punktu kontrolnego.

    package
    {
        import flash.display.Sprite;
        import flash.geom.Point;
        import flash.events.MouseEvent;
        import flash.text.TextField;
       
        public class NBezierTestDC extends Sprite
        {
            const       n       : int       = 8;
            const       deltaT  : Number    = 0.02;
           
            private var _points : Array     = new Array(n + 1);
           
           
            public function NBezierTestDC()
            {
                trace(MathUtils.factor(21));
               
                for( var j : int = 0 ; j < (n + 1) ; j++ )
                {
                    var point : Sprite = new Sprite();
                       
                        point.graphics.beginFill(0xad1f47 , 1);
                        point.graphics.drawCircle(0,0,3);
                        point.graphics.endFill();
                       
                        point.addEventListener  (
                                                    MouseEvent.MOUSE_DOWN,
                                                    mouseDownEventHandler
                                                );
                       
                        point.addEventListener  (
                                                    MouseEvent.MOUSE_UP,
                                                    mouseUpEventHandler
                                                );
                       
                        point.mouseChildren = false;
                        point.buttonMode    = true;
                       
                        var tf : TextField  = new TextField();
                       
                            tf.autoSize     = 'left';
                            tf.selectable   = false;
                            tf.textColor    = 0xad1f47;
                            tf.text         = 'P_'+ j;
                       
                        point.addChild(tf);
                       
                        point.x = Math.random() * 400;
                        point.y = Math.random() * 400;
                       
                    _points[j]  = addChild(point);
                }
            }
           
            /**
                Events handlers
            */

           
            private function mouseDownEventHandler(event : MouseEvent) : void
            {
                (event.target as Sprite).addEventListener(
                                                              MouseEvent.MOUSE_MOVE ,
                                                              mouseMoveEventHandler
                                                          );
               
                (event.target as Sprite).startDrag(false);
            }
           
            private function mouseUpEventHandler(event : MouseEvent) : void
            {
                (event.target as Sprite).stopDrag();
            }
           
            private function mouseMoveEventHandler(event : MouseEvent) : void
            {
                /**
                    Funkcja przy kazdej zmianie pozycji dowolnego z punktow kontrolnych,
                    czysci okno i rysuje nowa krzywa.
                */

               
                graphics.clear();
                graphics.lineStyle(1,0x999999 , 1);
                graphics.moveTo(_points[0].x,_points[0].y);
               
                for ( var t : Number = 0.0 ; t <= 1.0 ; t+= deltaT )
                {
                    var curvePoint : Point = new Point();
                   
                    for( var i : int = 0 ; i < (n + 1) ; i++ )
                    {
                        var P_i : Sprite    = _points[i] as Sprite;
                       
                        var B   : Number    = MathUtils.bernstainPolynomial(i , n , t);
                       
                        curvePoint.x += B * P_i.x;
                        curvePoint.y += B * P_i.y;
                    }
                   
                    graphics.lineTo(curvePoint.x , curvePoint.y);
                }
            }
        }
    }

    NBezierTestDC.as

    Efekt działania programuNBezierTest
    NBezierTest.swf
    Kody źródłowe oraz pliki fla i swf do pobrania stąd.

    Krzywe Béziera – część trzecia

    Nov 04

    Postanowiłem napisać co nieco na temat krzywych Béziera – temat wg mnie ciekawy, choć na pierwszy rzut oka wydający się niełatwym. Przedstawię także przykładową implementację w Action Script 3.0 programu rysującego takie krzywe.

    bezierCurveSample

    Przykładowa krzywa trzeciego stopnia o czterech punktach kontrolnych P1=(50,300) , P2=(100,100) , P3=(300,50) , P4=(350,250), nakreślona we Flash’u.

    1. Wstęp

    Krzywe Béziera, to parametryczne krzywe opracowane niezależnie przez Pierre’a Béziera oraz Paula de Casteljau. Pierwszy z nich rozpowszechnił je poprzez swoje publikacje we Francji w latach 60. i wykorzystywanie ich przy projektowaniu karoserii samochodowych. Drugi natomiast stworzył algorytm rekurencyjny pozwalający obliczać wartości wielomianów Bernsteina, co jest równoważne obliczaniu punktów krzywej Béziera.

    Flash, Illustrator, Photoshop, programy graficzne 3D czy CAD posiadają gotowe narzędzia, które pozwalają tworzyć krzywe B. w sposób o wiele bardziej wydajny niż przedstawiony przeze mnie, jednak myślę, że używanie gotowego narzędzia nie jest tak ciekawe jak napisanie go własnoręcznie.

    2. Matematyczne podstawy

    Dowolna krzywa Béziera n-tego stopnia wyrażona jest wzorem (w1)

    , w którym składowa

    nazywana wielomianami bazowymi Bernsteina ma postać (w2)

    , natomiast

    jest i-tym punktem kontrolnym krzywej.

    Po podstawieniu wyrażenia (w2) do (w1) otrzymujemy wzór ogólny (w3)


    3. Wyprowadzenie wzoru na krzywe Béziera stopnia trzeciego (n = 3)

    Po podstawieniu pod wzór (w3) n=3 i rozwinięciu wyrażenia pod znakiem sigma, otrzymujemy

    Biorąc pod uwagę fakt, że

    oraz

    otrzymujemy ostateczny wzór na krzywą Béziera stopnia trzeciego (w4)

    Aby wyznaczyć współrzędne punktów tej krzywej, wzór (w4) rozbijamy na części, pozwalające obliczyć poszczególne składowe ( x oraz y ).

    Kolejne punkty wyliczamy zmieniając wartość parametru t w zakresie <0 , 1>.

    Przykładowo, chcąc obliczyć składowe punktów leżących na krzywej opisanej wzorem (w4), przedział <0,1> dzielę na n równych części, otrzymując n+1 wartości parametru t.

    Wartości te tworzą zbiór T = { 0/n , 1/n , 2/n, 3/n, … , n/n }. Dla każdego elementu tego zbioru wyliczam wartość wyrażenia B(t), otrzymując w efekcie zbiór n+1 punktów krzywej P = { B(0/n) , B(1/n) , B(2/n) , … , B(n/n) }.

    4. Implementacja funkcji do wyliczania współrzędnych punktów krzywych Béziera stopnia trzeciego na płaszczyźnie

    Aby wyliczyć poszczególne współrzędne punktów leżących na krzywej potrzebuję funkcji, która w zależności od parametru t oraz punktów kontrolnych zwróci współrzędne punktu krzywej.

    package
    {
        import flash.geom.Point;
       
        public class Bezier
        {
           
            /**
            Funkcja zwraca wspolrzedne punktu krzywej Beziera
            trzeciego stopnia, w zaleznosci od paramteru t oraz
            czterech punktow kontrolnych p0,p1,p2,p3.
            */

           
            public static function bezierCurvePoint (
           
                                                        t  : Number,
                                                        p0 : Point,
                                                        p1 : Point,
                                                        p2 : Point,
                                                        p3 : Point
                                                       
                                                    ) : Point
            {
                /**
                Implementacja wzoru (w4)
                */

                const a : Number = 1 * (1-t) * (1-t) * (1-t);
                const b : Number = 3 * t * (1-t) * (1-t);
                const c : Number = 3 * t * t * (1-t);
                const d : Number = 1 * t * t * t;
               
                var px : Number = a * p0.x + b * p1.x + c * p2.x + d * p3.x;
                var py : Number = a * p0.y + b * p1.y + c * p2.y + d * p3.y;
               
                return new Point(px , py);
            }
        }
    }

    Bezier.as

    Aby sprawdzić działanie funkcji bezierCurvePoint tworzę Document Class BezierTestDC.

    package
    {
        import flash.geom.Point;
        import flash.display.Sprite;

        /**
        Testowy Document Class wyswietlajacy  krzywa Beziera.
        */

       
        public class BezierTestDC extends Sprite
        {
            public function BezierTestDC()
            {
                graphics.clear();
                graphics.lineStyle (1,0x000000,1);
               
                const p0:Point  = new Point(0,0);
                const p1:Point  = new Point(100,50);
                const p2:Point  = new Point(200,-50);
                const p3:Point  = new Point(350,350);
               
                const N : int   = 50;
               
                for (var i : int = 0; i&lt;=N; i++)
                {
                    var p : Point = Bezier.bezierCurvePoint(i/N,p0,p1,p2,p3);
                   
                    if(i == 0)
                    {
                        graphics.moveTo (p.x,p.y);
                    }
                   
                    graphics.lineTo (p.x , p.y);
                }
            }
        }
    }

    BezierTestDC.as

    Okno działającego programu. Krzywa narysowana została poprzez połączenie odcinkami 51 punktów wyznaczonych funkcją Bezier.bezierCurvePoint().BezierTest.swfOkno, w którym widać krzywą, narysowaną poprzez połączenie kolejnych punktów wyznaczonych dla t = { 0.00 , 0.02 , 0.04 , … , 1.00 }.

    Kody źródłowe oraz pliki fla i swf do pobrania stąd.

    Krzywe Béziera – część druga