COOKIES

This site may be using cookies to melk you with your own data. I, ben0bi, am not the owner of this web service and I also do not maintain their servers. But the EU and the owner of this service think, that the user (me) has the responsibility to inform the consumer (you), that this website uses cookies. Again: I, ben0bi, NEVER use cookies. I am not responsible for the setup of this web service. I just present some information here and do not intend to spy on you for whatever reason ever. But (also again), I do not host this website nor do I maintain any servers related to this website nor do I benefit from using the cookies maintained from this service. I hereby give the responsibility for using cookies on blogspot back to the owners of blogspot.

Mittwoch, 16. Oktober 2019

DIY Emulator Teil 2.1: Der Placeholder-Emulator

DIY Emulator Teil 2.1: Der Placeholder-Emulator

in Unity


(Interlude - Dieser Artikel ist für den weiteren Verlauf der Tutorials nicht relevant.)

Du solltest das Framework des vorigen Artikels nun aufgesetzt haben...

Nachdem wir in den vorigen zwei Artikeln ein kleines 2D-Framework für Emulatoren in Unity3D aufgesetzt haben, soll dieses Framework nun auch einmal etwas anzeigen.

Wie schon beschrieben ist der Placeholder-Emulator kein wirklicher Emulator sondern hat nur die selbe Klasse als Basis. Der Placeholder-Emulator läuft im Hauptmenü, wenn kein anderer Emulator geladen ist. Dies dient der Vereinfachung: Im System ist immer ein Emulator am laufen und somit muss kein Extracode für den Fall geschrieben werden, wenn eben kein Emulator geladen ist. Desweiteren kann man hier verschiedenste Sachen austesten, ohne gross etwas kaputt zu machen.

In diesem Artikel werden wir einen Plasma-Effekt generieren.

Dazu wird eine Farbpalette erstellt.
Dann wird das Display des Emulators mit zufälligen Indexen dieser Palette gefüllt.
Danach wird das Display "refined", also alle Pixel werden vom Wert her an die benachbarten angeglichen. Das gibt schöne "Hügel und Täler".

Dieses Bild aus Paletten-Indexen dient nun als Ausgangsbasis.

In Jedem Frame werden die Paletten-Indexe des Displays als Farbe (Color) aus der Palette geholt und auf die Textur "gemalt". Dabei wird ein "plasmaIndex" dazu gerechnet, welcher das Plasma...plasmieren lässt.

Alles was man pro Frame ändern muss ist der plasmaIndex, und dann natürlich noch das Display in Colors umrechnen.

Wir nehmen den Placeholder-Emulator und erweitern diesen (mehrere Abschnitte):

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Placeholder_Emulator : EmulatorBase
{
    protected int[] m_display; // pos = y * disp_width + x
    protected Color[] m_palette;
    protected int m_palSize = 0xFF; // palsize is one byte. :)

    // this index is added to the actual map palette index to "move" the colors.
    protected float m_plasmaIndex = 0.0f;

    // we need to slow down the things a little.
    protected int colorsPerSecond = 20;

    public Placeholder_Emulator(GameObject g) : base(g)
    {
        Debug.Log("EPlaceholder_Constructor");
        disp_width = 320;
        disp_height = 240;
        m_display = new int[disp_width * disp_height];

        createPalette();
        createPlasmaField();
    }

    public override void Update()
    {
        copyDisplay();

        m_plasmaIndex+=Time.deltaTime*colorsPerSecond;
        if ((int)m_plasmaIndex >= m_palSize)
            m_plasmaIndex = (float)m_palSize-m_plasmaIndex;
    }

Der Placeholder_Emulator ist von der vorher erstellten EmulatorBase abgeleitet.
  • m_display ist das der Textur entsprechende Array mit den Palette-Indexen für das Plasma-Bild.
  • m_palette ist das Array mit den Colors für die Indexe in m_display.
  • m_palSize ist die Grösse der Palette.
  • m_plasmaIndex ist der aktuelle Index, welcher auf jeden Index in m_display aufgerechnet wird.
  • colorsPerSecond zeigt an, wieviele Farbwechsel (Änderung von m_plasmaIndex) es pro Sekunde geben soll.
  • Im Konstruktor wird das GameObject (von EmuCreator) an den Basis-Konstruktor übergeben.
    Die Grösse des Displays wird auf 320x240 (PC-SCREEN 13) eingestellt und m_display wird initialisiert. Dann werden die Palette und das Plasmabild erstellt mit createPalette und createPlasmaField.
  • Schliesslich wird in jedem Update das Display per copyDisplay auf die Textur "gerendert". Dann wird der plasmaIndex um frameTime * colorsPerSecond erhöht.

createPalette()

Erst mal die createPalette-Funktion:

    // create a palette.
    protected void createPalette()
    {
        m_palette = new Color[m_palSize];

        float r = 0.0f;
        float g = 0.0f;
        float b = 0.0f;
        float eight = m_palSize / 8.0f;
        for(int i=0;i < m_palSize;i++)
        {
            // first red to yellow
            if (i<=eight)
            {
                r = 1.0f;
                g = 1.0f / eight * i;
                b = 0.0f;
            }

            // not to white because it's to bright.
            // then yellow to black
            if(i>eight && i<=eight*2)
            {
                r = 1.0f - (1.0f / eight * (i - (eight * 1))); // reverse;
                g = 1.0f - (1.0f / eight * (i - (eight * 1))); // reverse;
                b = 0.0f;
            }

            // then black to turkis
            if (i > eight*2 && i <= eight * 3)
            {
                r = 0.0f;
                g = 1.0f / eight * (i - (eight * 2));
                b = 1.0f/eight*(i-(eight*2));
            }

            // then turkis to green
            if (i > eight * 3 && i <= eight * 4)
            {
                r = 0.0f;
                g = 1.0f;
                b = 1.0f - (1.0f / eight * (i - (eight * 3)));  
            }

            // then green to black
            if (i > eight * 4 && i <= eight * 5)
            {
                r = 0.0f;
                g = 1.0f - (1.0f / eight * (i - (eight * 4))); // reverse
                b = 0.0f;
            }

            // then black to blue
            if (i > eight * 5 && i <= eight * 6)
            {
                r = 0.0f;
                g = 0.0f;
                b = 1.0f / eight * (i - (eight * 5));
            }

            // then blue to magenta
            if (i > eight * 6 && i <= eight * 7)
            {
                r = 1.0f / eight * (i - (eight * 6));
                g = 0.0f;
                b = 1.0f;
            }

            // then magenta to red
            if (i > eight * 7)
            {
                r = 1.0f;
                g = 0.0f;
                b = 1.0f - (1.0f / eight * (i - (eight * 7))); // reverse
            }

            m_palette[i] = new Color(r, g, b);
        }
    }


Diese Palette geht durch alle Farben ausser Weiss (das war zu hell, ich habe es durch Schwarz ersetzt. Rate, wo. ;) ). Darum braucht es acht Abschnitte. Die Variable eight ist ein Achtel der Gesamtpalette. So kann man in jedem Unterabschnitt mit eight und dem Index von 0.0f bis 1.0f oder umgekehrt gehen.

Zunehmende Farbe ist: 1.0f/eight * i
Abnehmende Farbe ist: 1.0f-(1.0f/eight * i)

Dazu muss man von i jeweils noch ein paar Achtel abziehen, damit i immer im Bereich von 0 bis eight ist.

Die Reihenfolge der Farben ist die folgende:  Rot, Gelb, Schwarz, Türkis, Grün, Schwarz, Blau, Magenta, Rot

createPlasmaField()

Nun die createPlasmaField-Funktion:
Erst wird das gesamte Display mit Zufallswerten von 0 bis zur Palettengrösse gefüllt.
Dann wird vier mal durch das Display durchgegangen. Die umliegenden und der aktuelle Pixel werden zusammengezählt und der Durchschnitt des Ergebnisses ausgerechnet. Der Dividor für den Durchschnitt wird jedesmal neu berechnet, da es am Rand weniger Pixel hat.

    protected void createPlasmaField()
    {
         // initialize the display array.
         int m_display = new int[disp_width * disp_height];
         // fill the display with random values.
         for(int i=0;i<m_display.Length;i++)
              m_display[i] = (int)Random.Range(0, m_palSize);

         // go several times through the whole display and
         // smoothen the pixel color(-indexes)
         // 4 steps are appropriate: less do it carvy, more flatten it out to one color.
         int dividor = 0;
 
         int newcol = 0; // color is a palette index, not a color.
         for(int steps=0;steps < 4;steps++)
         {
             // go through x and y instead of mapIndexes because...
             for(int y = 0;y < disp_height; y++)
             {
                for(int x = 0;x < disp_width; x++)
                {
                    dividor = 0;
                    // .. we need to get the right position here.
                    for(int yp= -1;yp<=1;yp++)
                    {
                       for(int xp= -1;xp<=1;xp++)
                       {
                          // get x and y of the pixel to add
                          int newx = x + xp;
                          int newy = y + yp;
                          // check if it is in bounds.
                          if(newx >= 0 && newx < disp_width &&
                             newy >= 0 && newy < disp_height)
                           {
                              dividor++;
                              int idx = newy*disp_height + newx;
                              newcol += m_display[idx];
                           }   
                       }
                    }
                    // divide by dividor.
                    newcol = (int)newcol / dividor;
                    // set new, "smooth" palette index.
                    m_display[y*disp_width +x]=newcol;
                }
             }
         }
    }

In dieser Funktion hat es ziemlich viele for-s, doch das macht nichts, da sie nur einmal aufgerufen wird. Bei Update allerdings musste ich mehrmals "drüber", da schon kleinste Multiplikationen (zB. index aus x,y ausrechnen pro Pixel) einen enormen Einfluss auf den Verarbeitungs-Speed haben (bei einem Array in der Grösse eines Displays). Im EmuGraphicsAdapter hat es dafür eine neue Funktion MarkPixelToChangeByIndex. Eventuell kommt später noch eine "CopyPaletteToTexture(map, palette)" in den EmuGraphicsAdapter, um nur einen Funktions-Call zu machen statt xTausend, doch bis jetzt reicht das.

Was genau gemacht wird, ist oben schon erklärt. Mit den innersten Schleifen werden die Pixel um x und y herum ausgelesen.

copyDisplay():

Schliesslich noch die copyDisplay-Funktion. Da das Display hier gleich gross ist wie die Textur, kann man direkt mit dem Index arbeiten.

    protected void copyDisplay()
    {
       // speed up the things a little with direct indexing.
       for(int i=0; i &lt m_display.Length; i++)
       {
          gfx.MarkPixelToChangeByIndex(i, m_palette[m_display[i]]);
       }
    }
} // end the class here.

Die vielen MarkPixelToChange-Aufrufe pro Frame sind nicht so der Bringer, das wird noch refined.

Desweiteren muss ja eigentlich nicht in jedem Frame das Bild neu gelöscht, aufgebaut und angezeigt werden, sondern nur wenn sich der plasmaIndex ändert (also alle 3 Frames, zur Zeit: 20 Änderungen auf 60 Frames pro Sekunde verteilt). Dazu müsste man switchBuffers aus dem EmuGraphicsAdapter-Update herausnehmen und im Emulator selbst aufrufen.

Ich lerne das Zeug auch alles gleich neu, da wird eventuell noch einiges geändert in älteren Artikeln.
[änderungen folgen] ;)

Jetzt musst du noch.....Play drücken. :)

Das Projekt findest du auch auf github, und zwar hier:
https://github.com/ben0bi/EmulatroniX
im Ordner "JUMPEE".

JUMPEE heisst "JavaScript & Unity Multi Purpose Emulator Environment"
naja, J müsste man rausnehmen, es ist ja jetzt C#. Aber dann tönts nicht mehr so gut....

Den Plasmaeffekt in JavaScript findest du zum angucken hier:
https://ben0bi.github.io/EmulatroniX

Keine Kommentare:

Kommentar veröffentlichen