Dienstag, 20. Juni 2017

DIY: Emulator (Grafik-)Demo 1: Plasma Effekt

Am Ende des Artikels solltest du das hier generieren können:

Bitte warten, ich lade..
(Du solltest ein animiertes waberndes Bild sehen. Wenn nicht, aktiviere bitte JavaScript.)


Nachdem wir den Emulator Bildschirm aufgebaut und alle benötigten Zeichnungsfunktionen eingebaut haben, können wir nun endlich einmal etwas auf dem Bildschirm anzeigen.

(Letzter relevanter Artikel: Teil 2.1_3, ab dort kannst du dich durchklicken. ;) )

Der Plasma-Effekt bekommt eine eigene Klasse, an welche die EmuGraphicsAdapter-Instanz übergeben wird. Diese Klasse hat zwei Funktionen: Eine, welche das Basis-Bild und die Palette generiert und  eine zweite welche mit jedem Frame aufgerufen wird. Diese verschiebt die Paletten-Farben und generiert dann das neue Endbild daraus.

js/Plasma.js:


var Plasma = function(emuGraphicsAdapter)
{
    var GFX = null;
    if(emuGraphicsAdapter)
        GFX = emuGraphicsAdapter;

    var plasmaImage = null;
    var plasmaPalette = null;
    var plasmaPaletteMultiplier = 4;


  • GFX ist eine Referenz auf die EmuGraphicsAdapter-Instanz.
  • plasmaImage ist eine Kopie des Bildschirm-Arrays, in welchem das statische Bild für den Effekt generiert wird.
  • plasmaPalette ist die Palette, welche auf das mit dem plasmaImage verrechnet wird.
  • plasmaPaletteMultiplier zeigt an, wie oft die Palette auf dem Bild wiederholt werden soll. Bei einem niedrigen Wert (1,2) sieht es sehr einfarbig aus, bei einem zu hohen Wert (10) ist das Bild zu sehr fragmentiert (bei einem 40x40 Screen). Der Wert wird so berechnet: Pixel = random * paletteSize * plasmaPaletteMultiplier und dann so ausgelesen: paletteIndex = Pixel % paletteSize (% heisst Modulo).

Nun generieren wir erst die Palette und dann das Bild basierend auf der Grösse der Palette.

Create-Funktion:


    this.createPlasmaImage = function()
    {
        if(GFX == null)
        {
            console.log("Plasma Error: No EmuGraphicsAdapter given!");
            return;
        }

        plasmaPalette = [];
        var red = 0;
        var green = 0;
        var blue = 0;
        var p = 0;


Ich generiere hier eine Gradient-Palette mit einer Grösse von 256 Einträgen, mit je 32 Abstufungen pro Gradient. Der maximale Wert einer Farbe ist 256 (0xFF). Somit ist die Abstufung "8 farb breit" (256 / 32 = 8). Du kannst deine eigene Palette generieren mit beliebiger Grösse und beliebigen Farben. Meine geht einfach durch alle Farben durch.


// 0-31 + red to yellow
for(p=0;p<32;p++)
{
red = 0xFF;
green = p * 8;
blue = 0;
plasmaPalette.push(RGB(red,green,blue));
}

// 32-63 + yellow to white
for(p=0;p<32;p++)
{
red = 0xFF;
green = 0xFF;
blue = p * 8;
plasmaPalette.push(RGB(red,green,blue));
}

// 64-95 + white to turkis
for(p=0;p<32;p++)
{
red = (31-p)*8;
blue = 0xFF;
green = 0xFF;
plasmaPalette.push(RGB(red,green,blue));
}

// 96-127 + turkis to green
for(p=0;p<32;p++)
{
red = 0;
blue = (31 - p) * 8;
green = 0xFF;
plasmaPalette.push(RGB(red,green,blue));
}
// 128-159 + green to black
for(p=0;p<32;p++)
{
red = 0;
blue = 0;
green = (31-p)*8;
plasmaPalette.push(RGB(red,green,blue));
}

// 160-191 + black to blue
for(p=0;p<32;p++)
{
red = 0;
blue = p * 8;
green=0;
plasmaPalette.push(RGB(red,green,blue));
}

// 192-223 + blue to magenta
for(p=0;p<32;p++)
{
red = p * 8;
blue = 0xFF;
green = 0; 
plasmaPalette.push(RGB(red,green,blue));
}
// 224-255 + magenta to red
for(p=0;p<32;p++)
{
red = 0xFF;
blue = (31 - p) * 8;
green = 0;  
plasmaPalette.push(RGB(red,green,blue));
}


Hier werden einfach 8*32 Werte generiert und in die Plasmapalette hinzugefügt. Wie du siehst, ist die Formel ganz einfach: Farbe rauf mit p * FarbBreite, Farbe runter mit ((gradientSize-1) - p) * FarbBreite.

Nun wird das Referenzbild generiert:


// now generate the image itself.
plasmaImage = GFX.screenToArray(); // get screen image
var arraySize = plasmaImage.length; // length of screen image array.
var palSize = plasmaPalette.length;
var maxValue = (palSize * plasmaPaletteMultiplier)-1;

// just generate a random noise image
for(var z = 0; z < arraySize; z++)
{
var color = parseInt(Math.random()*maxValue);

// little more black in the image.
if(color < maxValue * 0.25)
color = 0;
plasmaImage[z] = color;
}


Erst wird das Array vom EmuGraphicsAdapter "gezogen". Somit ist es automatisch in der richtigen Grösse. maxValue ist die maximale "Höhe" der folgenden Heightmap. Dann wird das gezogene Array mit Zufallswerten gefüllt.

Nun werden wir dieses Zufallspunkte-Bild "smooth" machen, so dass es "Hügel" und "Täler" hat. Dazu nehmen wir einfach alle umliegenden Werte eines Pixels, und berechnen den Durchschnitt daraus. Das machen wir mehrere male.


// refine the image, create "height map"
for(var steps =0;steps < 4;steps++)
{
var emuScreenHeight = GFX.screenHeight();
var emuScreenWidth = GFX.screenWidth();

for(var y=0;y<emuScreenHeight;y++)
{
for(var x=0;x<emuScreenWidth;x++)
{
var myIndex = y*emuScreenWidth+x;
var color = 0;
var dividor = 0;

// get all colors around that pixel
for(difx = -1; difx<=1; difx++)
{
for(dify = -1; dify<=1; dify++)
{
var index = (y+dify)*emuScreenWidth+x+difx;
if(index>=0 && index<arraySize)
{
color+=plasmaImage[index];
dividor++;
}
}
}

if(color>0 && dividor>0)
color = parseInt(color/dividor);

if(color>maxValue)
color=maxValue;

plasmaImage[myIndex] = color;
}
}
}
console.log("Plasma Effect image and palette created.");

   }


Die letzte Klammer schliesst die Funktion ab. Wir haben nun ein "Bild", welches nicht mit Farben sondern mit Indexen für die Palette gefüllt ist.

Pixelfarbe = Palette[Pixel % paletteSize]

Nun können wir die update-Funktion schreiben.

Update-Funktion:


// update the plasma image.
this.update = function()
{
if(plasmaImage==null || plasmaPalette==null || !GFX)
return;

// cycle palette colors
// warning: FROM 1, not from 0!!
var first = plasmaPalette[0];
for(var pz=1;pz<plasmaPalette.length;pz++)
{
plasmaPalette[pz-1] = plasmaPalette[pz];
}
plasmaPalette[plasmaPalette.length-1]=first;

GFX.arrayFromPaletteToScreen(plasmaImage, plasmaPalette);
}


Hier wird einfach durch die Palette rotiert und diese dann auf dem EmuGraphicsAdapter mit dem Referenzbild verknüpft und gerendert. first ist der erste Paletteneintrag, welcher überschrieben wird. Dieser wird am Ende der Palette wieder angehängt.

Dann rufen wir noch schnell die create-Funktion auf und schliessen die Klasse ab:


   this.createPlasmaImage();

}


Nun muss nur noch die index.html angepasst werden. In der $(document).ready()-Funktion wird eine globale Plasma-Instanz erstellt, welche in der mainLoop-Funktion aufgefrischt und gezeichnet wird:

index.html:

...
<script src="js/EmuGraphicsAdapter.js"></script>

<script src="js/Plasma.js"></script>

<script>
   var emuGraphics = null;
   var plasmaEffect = null;

   function mainLoop() 
   {

      if(emuGraphics==null || plasmaEffect==null)

         return;

      plasmaEffect.update();
      emuGraphics.switchBuffers();
   }


   $(document).ready(function() 
      {
      // ... siehe vorige Artikel...

         emuGraphics = new EmuGraphicsAdapter(emuWidth, emuHeight,0x000000,0x3333AA);

         // NEU
         plasmaEffect = new Plasma(emuGraphics);

         console.log("Ready.");
      });
   </script>

Das sollte nun alles funktionieren. Mit den Anpassungen wird die Bildschirmgrösse noch richtig skaliert. Hier ist der Source-Code für diesen Artikel und die Anpassung:

https://github.com/ben0bi/EmulatroniX/releases/tag/Blog_Series_Demo_1_Plasma

Viel Spass dabei.


Keine Kommentare:

Kommentar veröffentlichen