Tutorial #08: Animated Images

Version 1: Texturatlas

Animierte Grafiken bzw. Bildsequenzen können mit dem Objekt PlaneSequenceGeometry erstellt werden. Dafür wird ein Texturatlas und eine Atlas-XML-Datei benötigt:

  • texture_explosion.png
  • atlas_explosion.xml

Der Texturatlas fasst viele Einzelbilder in einem großen Gesamtbild zusammen, wobei die Atlas-XML-Datei die Positionen der Einzelbilder speichert. Beide Dateien müssen in der Datei package.xml bekannt gemacht werden.

    <!-- Graphic resources -->
    <Resource id="gfx_explosion" fileName="gfx/gfx_explosion.png"/>
    
    <!-- Atlas resources -->
    <Resource id="atlas_explosion"   fileName="atlas_explosion.xml"/>

Der Texturatlas zeigt eine Explosionsanimation, welche aus 32 Einzelbildern besteht. Jedes Einzelbild hat eine Größe von 64x64 Pixeln. Die einheitliche Größe der Einzelbilder wurde nur für dieses Beispiel gewählt und ist nicht zwingend vorgeschrieben.

tut0108_explosion_texture.png
Atlastextur einer Explosion

Im Atlas-XML-File wird für jedes Einzelbild ein rechteckiger Ausschnitt definiert. Mit den Attributen texCoordX1 und texCoordX2 werden die X-Koordinaten und mit texCoordY1 und texCoordY2 die Y-Koordinaten des Rechtecks festgelegt. Die Angaben sind relativ zur später in der PlaneSequenceGeometry angegebenen textureSizeX bzw. textureSizeY.

Für unser Beispiel verwenden wir die Pixel-Koordinaten und geben für textureSizeX und textureSizeY die Pixelauflösung des Gesamtbildes an. Alternativ werden oft normierte Werte im Bereich von 0 bis 1 für die Koordinaten sowie der Standardwert 1 für textureSizeX bzw. textureSizeY verwendet.

<?xml version="1.0" ?>

<Atlas>
    
    <Rectangle texCoordX1="0"   texCoordY1="0" texCoordX2="64"  texCoordY2="64"/>
    <Rectangle texCoordX1="64"  texCoordY1="0" texCoordX2="128" texCoordY2="64"/>
    <Rectangle texCoordX1="128" texCoordY1="0" texCoordX2="192" texCoordY2="64"/>
    <Rectangle texCoordX1="192" texCoordY1="0" texCoordX2="256" texCoordY2="64"/>
    <Rectangle texCoordX1="256" texCoordY1="0" texCoordX2="320" texCoordY2="64"/>
    <Rectangle texCoordX1="320" texCoordY1="0" texCoordX2="384" texCoordY2="64"/>
    <Rectangle texCoordX1="384" texCoordY1="0" texCoordX2="448" texCoordY2="64"/>
    <Rectangle texCoordX1="448" texCoordY1="0" texCoordX2="512" texCoordY2="64"/>
    
    <Rectangle texCoordX1="0"   texCoordY1="64" texCoordX2="64"  texCoordY2="128"/>
    <Rectangle texCoordX1="64"  texCoordY1="64" texCoordX2="128" texCoordY2="128"/>
    <Rectangle texCoordX1="128" texCoordY1="64" texCoordX2="192" texCoordY2="128"/>
    <Rectangle texCoordX1="192" texCoordY1="64" texCoordX2="256" texCoordY2="128"/>
    <Rectangle texCoordX1="256" texCoordY1="64" texCoordX2="320" texCoordY2="128"/>
    <Rectangle texCoordX1="320" texCoordY1="64" texCoordX2="384" texCoordY2="128"/>
    <Rectangle texCoordX1="384" texCoordY1="64" texCoordX2="448" texCoordY2="128"/>
    <Rectangle texCoordX1="448" texCoordY1="64" texCoordX2="512" texCoordY2="128"/>
    
    <Rectangle texCoordX1="0"   texCoordY1="128" texCoordX2="64"  texCoordY2="192"/>
    <Rectangle texCoordX1="64"  texCoordY1="128" texCoordX2="128" texCoordY2="192"/>
    <Rectangle texCoordX1="128" texCoordY1="128" texCoordX2="192" texCoordY2="192"/>
    <Rectangle texCoordX1="192" texCoordY1="128" texCoordX2="256" texCoordY2="192"/>
    <Rectangle texCoordX1="256" texCoordY1="128" texCoordX2="320" texCoordY2="192"/>
    <Rectangle texCoordX1="320" texCoordY1="128" texCoordX2="384" texCoordY2="192"/>
    <Rectangle texCoordX1="384" texCoordY1="128" texCoordX2="448" texCoordY2="192"/>
    <Rectangle texCoordX1="448" texCoordY1="128" texCoordX2="512" texCoordY2="192"/>
    
    <Rectangle texCoordX1="0"   texCoordY1="192" texCoordX2="64"  texCoordY2="256"/>
    <Rectangle texCoordX1="64"  texCoordY1="192" texCoordX2="128" texCoordY2="256"/>
    <Rectangle texCoordX1="128" texCoordY1="192" texCoordX2="192" texCoordY2="256"/>
    <Rectangle texCoordX1="192" texCoordY1="192" texCoordX2="256" texCoordY2="256"/>
    <Rectangle texCoordX1="256" texCoordY1="192" texCoordX2="320" texCoordY2="256"/>
    <Rectangle texCoordX1="320" texCoordY1="192" texCoordX2="384" texCoordY2="256"/>
    <Rectangle texCoordX1="384" texCoordY1="192" texCoordX2="448" texCoordY2="256"/>
    <Rectangle texCoordX1="448" texCoordY1="192" texCoordX2="512" texCoordY2="256"/>
    
</Atlas>

Plane Sequence Geometry

Im Graphen setzen wir den Texturatlas mit TextureState und wählen ein geeignetes Material zum Zeichnen aus.

Danach definieren wir eine PlaneSequenceGeometry, um die Einzelbilder anzuzeigen. Zum Vergleich erstellen wir auch einen PlaneGeometry Knoten, der uns den gesamten Texturatlas anzeigt.

Mit dem Attribut atlasResourceId definieren wir die zu verwendende Atlas-XML-Datei mit den Positionen für die Einzelbilder. Die Attribute textureSizeX und textureSizeY legen die Größe des gesamten Texturatlas-Bildes in Bezug auf die angegebenen Koordinatenwerte in der Atlas-XML-Datei fest.

Jedes Rechteck in der Atlas-XML-Datei definiert ein Einzelbild mit eindeutigem, fortlaufendem Index. Die Indizierung erfolgt aufsteigend und beginnt mit 0. Die Auswahl des Einzelbildes erfolgt mit dem Attribut index.

    <TextureState textureId="material/texture_explosion"/>
    <MaterialState materialId="material/mat_alpha_color_texture"/>

    <PlaneGeometry
        id="plane5"
        scaleFactorX="512" scaleFactorY="256"
        posX="0" posY="100"
    />
    
    <PlaneSequenceGeometry
        id="myPlaneSequence"
        atlasResourceId="package_main:atlas_explosion"
        textureSizeX="512" textureSizeY="256"
        scaleFactorX="64" scaleFactorY="64"
        index="5"
        posX="0" posY="-100"
    />

Um die Indexnummer im Programm verändern zu können, erstellen wir noch ein PlaneSequenceGeometryNode-Objekt

            Logic::PlaneSequenceGeometryNode mPlaneSequence;

und verknüpfen es mit dem Graphenknoten:

    AddGraphNode(mPlaneSequence.GetReference(root, "myPlaneSequence"));

Mit der Methode SetIndex() kann ein Einzelbild ausgewählt werden. Wir verwenden für die Steuerung die linke bzw. rechte Maustaste sowie die Pfeiltasten links bzw. rechts.

void App::AnimatedImagesLogic::OnProcessTick(const Logic::IState* state)
{
    Logic::IDeviceHandler* deviceHandler = state->GetDeviceHandler();
    
    if (deviceHandler->WasMouseButtonPressed(IEnums::MOUSE_BUTTON_LEFT) ||
        deviceHandler->WasRawKeyPressed(RAWKEY_RIGHT_ARROW))
    {
        UInt32 index = mPlaneSequence->GetIndex() + 1;
        index = index < 32 ? index : 0;
        mPlaneSequence->SetIndex(index);
        state->SetUserDebugMessage("Index " + Util::UInt32ToString(index));
    }

    if (deviceHandler->WasMouseButtonPressed(IEnums::MOUSE_BUTTON_RIGHT)||
        deviceHandler->WasRawKeyPressed(RAWKEY_LEFT_ARROW))
    {
        UInt32 index = mPlaneSequence->GetIndex() - 1;
        index = index > 31 ? 31 : index;
        mPlaneSequence->SetIndex(index);
        state->SetUserDebugMessage("Index " + Util::UInt32ToString(index));
    }

    if (deviceHandler->WasRawKeyPressed(RAWKEY_ESCAPE) ||
        deviceHandler->WasRawButtonPressed(RAWBUTTON_BACK))
    {
        deviceHandler->TerminateApp();
    }
}
tut0108_v1.png
V1-Ausgabefenster

Version 2: Animation

Anstatt mit SetIndex() gezielt ein Einzelbild auszuwählen, kann alternativ die Animation auch mit einer zusätzlichen Animationsdatei automatisiert und zeitgesteuert abgespielt werden.

Die neue Datei muss wiederum mit einer eindeutigen id im Package bekannt gemacht werden.

    <!-- Animation resources -->
    <Resource id="anim_explosion"    fileName="anim_explosion.xml"/>

In der Animationsdatei erstellen wir eine Animation mit definierter Startzeit (0,0 Sekunden) und Endzeit (1,5 Sekunden). Mit dem Element IndexKey erstellen wir eine Sequenz von Indexwerten mit zugehörigen Zeitwerten. In unserem Beispiel definieren wir diese Sequenz mit nur zwei Stützwerten – einem zum Zeitpunkt 0,0 Sekunden mit dem Indexwert 0 und einem zum Zeitpunkt 1,5 Sekunden mit dem Indexwert 31. Die Zwischenwerte werden automatisch gemäß einer vorgegebenen Interpolationsfunktion berechnet.

<?xml version="1.0" ?>

<Animation startTime="0.0" endTime="1.5">
    
    <IndexKey time="0.0" value="0"/>
    <IndexKey time="1.5" value="31"/>
    
</Animation>

Im Graphen erzeugen wir einen neuen PlaneSequenceGeometry-Knoten und verknüpfen diesen mit dem Attribut controller.animationResourceId mit der Animationsdatei. Zusätzlich verpacken wir den Knoten in eine Graph::Timeline, um die Animation steuern zu können.

    <Timeline
        id="explosion_timeline"
        startTime="0.0" endTime="1.5" autoRewind="yes"
    >
        <PlaneSequenceGeometry
            id="myAnimationPlaneSequence"
            atlasResourceId="package_main:atlas_explosion"
            textureSizeX="512" textureSizeY="256"
            scaleFactorX="64" scaleFactorY="64"
            posX="100" posY="-100"
            controller.animationResourceId="package_main:anim_explosion"
        />
    </Timeline>

In der Methode OnProcessTick() können wir mit einem passenden Logic::TimelineNode die Animation steuern. Mit der linken Maustaste kann die Animation neu gestartet und mit der rechten Maustaste pausiert werden.

void App::AnimatedImagesLogic::OnProcessTick(const Logic::IState* state)
{
    Logic::IDeviceHandler* deviceHandler = state->GetDeviceHandler();
    
    if (deviceHandler->WasMouseButtonPressed(IEnums::MOUSE_BUTTON_LEFT) ||
        deviceHandler->WasRawKeyPressed(RAWKEY_RIGHT_ARROW))
    {
        UInt32 index = mPlaneSequence->GetIndex() + 1;
        index = index < 32 ? index : 0;
        mPlaneSequence->SetIndex(index);
        state->SetUserDebugMessage("Index " + Util::UInt32ToString(index));
        mTimeLine->Rewind();
        mTimeLine->Start();
    }

    if (deviceHandler->WasMouseButtonPressed(IEnums::MOUSE_BUTTON_RIGHT)||
        deviceHandler->WasRawKeyPressed(RAWKEY_LEFT_ARROW))
    {
        UInt32 index = mPlaneSequence->GetIndex() - 1;
        index = index > 31 ? 31 : index;
        mPlaneSequence->SetIndex(index);
        state->SetUserDebugMessage("Index " + Util::UInt32ToString(index));
        if (mTimeLine->IsPaused())
            mTimeLine->Start();
        else
            mTimeLine->Pause();
    }

    if (deviceHandler->WasRawKeyPressed(RAWKEY_ESCAPE) ||
        deviceHandler->WasRawButtonPressed(RAWBUTTON_BACK))
    {
        deviceHandler->TerminateApp();
    }
}
tut0108_v2.png
V2-Ausgabefenster

Version 3: Interpolation

Eine Animationsdatei kann nicht nur den IndexKey steuern, sondern auch viele andere Parameter. Beispielsweise kann mit dem AlphaKey ein Bild ein- oder ausgeblendet oder mit dem PositionKey ein Bild bewegt werden. Folgende Key-Elemente stehen zur Verfügung:

Siehe auch Abschnitt Animationsressourcen in der User's Guide Dokumentation.

Mit dem Attribut interpolation kann für jedes Key-Element die zu verwendende Interpolationsfunktion vorgegeben werden (Standardwert ist LINEAR). Alle erlaubten Werte sind in der Enumeration IEnums::Interpolation gelistet.

Interpolationsfunktionen

Your browser does not support the HTML5 canvas tag.

Im folgenden Beispiel werden die Sichtbarkeit und der Index über die Animation gesteuert.

<?xml version="1.0" ?>

<Animation startTime="0.0" endTime="5">
    
    <VisibleKey time="0.0"  value="0" interpolation="CONSTANT"/>
    <VisibleKey time="0.01" value="1" interpolation="CONSTANT"/>
    <VisibleKey time="5"    value="0"/>
    
    <IndexKey time="0.0" value="0"   interpolation="LINEAR"/>
    <IndexKey time="5"   value="31"/>
    
</Animation>

Der VisibleKey wird beim Starten der Animation auf sichtbar (1) gesetzt (Zeitpunkt 0,01 Sekunden). Nach dem Ende der Animation wird der VisibleKey wieder auf nicht sichtbar geschaltet (Zeitpunkt 5 Sekunden).

tut0108_keys.png
Index- und Visible-Keys

Mit den Attributen controller.timeScale und controller.timeShift kann zusätzlich die Zeitangabe in der Animationsdatei skaliert und verschoben werden. Die tatsächliche Zeit in der Animationsdatei ergibt sich aus der Formel:

(time – animationTimeShift) * animationTimeScale 

In folgendem Beispiel wird durch die Angabe von controller.timeScale="2" die Animation doppelt so schnell abgespielt (also in 2,5 Sekunden anstatt der im Animation File definierten 5 Sekunden).

    <Timeline
        id="explosion_timeline"
        startTime="0.0" endTime="3" autoRewind="yes"
    >
        
        <PlaneSequenceGeometry
            id="myAnimationPlaneSequence"
            atlasResourceId="package_main:atlas_explosion"
            textureSizeX="512" textureSizeY="256"
            scaleFactorX="64" scaleFactorY="64"
            posX="300" posY="-100"
            controller.animationResourceId="package_main:anim_explosion"
            controller.timeScale="2"
        />

Zum Vergleich der unterschiedlichen Interpolationsfunktionen, erstellen wir für einige Interpolationsfunktionen einen PlaneGeometry-Graphenknoten mit eigener Animationsdatei.

<?xml version="1.0" ?>

<Animation startTime="0.0" endTime="1">

  <PositionKey time="0.0" posX="-50" posY="-250" interpolation="SMOOTHSTEP_IN"/>
  <PositionKey time="1"   posX="-50" posY="250"/>

</Animation>
        <PlaneGeometry
            id="plane6"
            scaleFactorX="64" scaleFactorY="64"
            controller.animationResourceId="package_main:anim_smoothstep_in"
            depthOrder="1"
        />
        <PlaneGeometry
            id="plane7"
            scaleFactorX="64" scaleFactorY="64"
            controller.animationResourceId="package_main:anim_smoothstep_out"
            depthOrder="1"
        />
        <PlaneGeometry
            id="plane8"
            scaleFactorX="64" scaleFactorY="64"
            controller.animationResourceId="package_main:anim_smoothstep_in_out"
            depthOrder="1"
        />

Als Ergebnis erhalten wir eine Reihe von Smiley-Grafiken, die sich beim Drücken der linken Maustaste mit unterschiedlichen Geschwindigkeitsverläufen von unten nach oben bewegen.

tut0108_v3.png
V3-Ausgabefenster


Copyright © 2011-2018 Spraylight GmbH.