# Schnelle 2D darstellung



## Anton2k (22. Feb 2010)

Hallo,
Ich teste gerade einiges aus was die Grafische darstellung angeht. Ziel wird letztendlich eine Anwendung mit einem zentralen Zeicheneditor.
Ok Zeichnen klappt problemlos soweit. Nur die Performance ist nicht so wie ich sie gerne hätte. Nicht das man mich jetzt falsch versteht es ist keine zeitkritische Anwendung wo es auf kleinste FPS ankommt. Das hat ehr was damit zu tun das ich es absolut nicht mag wenn mir etwas träger vorkommt als es sein muss.
Der Zeichenbereich soll 2000x2000 Pixel groß sein. Sprich nicht gerade wenig was neu gezeichnet werden muss vom Programm. Darauf werden sich X Grafikelemente befinden letztendlich.
Aktuell ist die Zeichenfläche ein JPanel. Es gibt ein BufferedImage als Backbuffer auf dem vorher meine Objekte gezeichnet werden, welcher dann im paintComponent vom Panel aufgetragen wird.
So möchte ich jetzt einen Kreis zeichnen wähle ich diesen aus und Ziehe ihn auf der Zeichenfläche auf. Hier merkt man auch die verzögerung beim Zeichnen. Der "Vorschau" Kreis der Anzeigt wie es beim loslassen der Maustaste aussieht läuft der Maus nur sehr träge nach. Was mir sagt das ist noch nicht so wie ich es haben möchte um mein Programm weiter zu erstellen. 
Um noch etwas auszuhohlen.
Ich hab das selbe Programm schon einmal in Delphi für Windows geschrieben. Das war auch träge beim Zeichnen wegen der enormen größe von 2000x2000 pixeln. Dort bin ich dann zu einer 2D Engine gewechselt welche das ganze über OpenGL von der Grafikkarte berechnen lassen hat. 
Das ganze hatte dann aber zwei Ärgernisse für mich. Zum einen war es nur noch möglich das Programm zu nutzen wenn eine 3D Grafikkarte im PC verbaut war. Das zweite war mit der Grund warum mir Java ins Auge gefallen ist das es auf einmal einen Mac user gab der es nutzen wollte.

Laaaaange rede kurzer sinn. Nachdem ich gemerkt hab das die Grafik träge ist hab ich angefangen mich einzulesen. Aber mittlerweile bin ich ehr verwirrt weshalb ich hier frage.
1. Wenn ich richtig gelesen hab nutzt Graphics2D OpenGL Funktionen für die Grafikfrunktionen oder nicht?
1.1 Mein Programm etwas umgestellt mit Graphics zu Graphics2D Casts hat es nicht wirklich beschleunigt. Vermute also es nutzt so oder so ein Software OpenGL Rendering was mir keine Geschwindigkeit gibt.
1.2 Oder ich bin wirklich so durcheinander und es passt gar nix von meinen annahmen.
2. Wenn ich wegen der 2D Beschleunigung und Java suche finde ich auf der Sun Seite immer nur Hinweise das es auf Windows, Linux und Solaris funktioniert. MacOS wird nie erwähnt.
3. Gibt es eine möglichkeit Grafikoperationen sowohl in Software als auch in Hardware halbwegs schnell zu ermögichen?
4. Eine fertige Java Grafikengine möchte ich nicht wirklcih nutzen da ich es gar nicht auf absolute geschwindigkeit abgesehen habe. Sprich ich möchte es vom Programmaufwand Simpel halten.

Hoffe das es nicht zu viel Text war und das man mir helfen kann.


----------



## Marco13 (22. Feb 2010)

Zumindest zu den ersten Punkten: 

_1. Wenn ich richtig gelesen hab nutzt Graphics2D OpenGL Funktionen für die Grafikfrunktionen oder nicht?_

Standardmäßig nicht, aber unter New Features in Java 2D(TM) Technology steht, wie man sie einschalten kann. Zu MacOS steht dort nichts explizit, aber einen Versuch ist's wert.

_1.1 Mein Programm etwas umgestellt mit Graphics zu Graphics2D Casts hat es nicht wirklich beschleunigt. Vermute also es nutzt so oder so ein Software OpenGL Rendering was mir keine Geschwindigkeit gibt._

ALLE Graphics-Objekte, die man bekommt, sind in Wirklichkeit Graphics2D-Objekte - das sind sie auch schon vor dem Casten - Casten bringt dort also erstmal gar nichts...

_3. Gibt es eine möglichkeit Grafikoperationen sowohl in Software als auch in Hardware halbwegs schnell zu ermögichen?_

Die Entscheidung, was in Hardware und was in Software gemacht wird, wird einem von Sun idR abgenommen. Mit einem BufferedImage kann man eigentlich nicht viel falsch machen. Der Type sollte TYPE_INT_RGB (oder ARGB) sein, aber sonst... :bahnhof:


----------



## Anton2k (22. Feb 2010)

Das waren schon mal gute Hinweise welche wieder einiges an Neben entfernt haben.

Hab es mit dem -Dsun.java2d.opengl=True schalter mal Probiert. Leider wird dann nur noch das Fenster erstellt und der ganze Inhalt nicht mehr. Ist also einlLeeres Weißes Fenster


----------



## Steev (22. Feb 2010)

Probiere mal ein VolatileImage zu verwenden, das wird direkt über die Grafikkarte (wenn vorhanden) gezeichnet.
Wird eigendlich immer die gesamte Zeichenfläche gezeichnet? Das kann ich mir gar nicht vorstellen ;-) Probiere doch mal die Zeichenfläche in mehrere Tiles aufzuteilen, die nur dann gezeichnet werden, wenn es wirklich sein muss (=>Viewport).
Der OpenGL-Schalter macht leider nicht immer das, was er eigendlich tun sollte, ich probiere ihn, wenn eben möglich, nicht zu verwenden.
Für wirklich zeitkritische Zeichenoperationen sollte man allerdings LWJGL oder JOGL verwenden...


----------



## Anton2k (23. Feb 2010)

Ich hab mal etwas mit dem VolatileImage rumgespielt, aber denke das es doch in die Falsche richtung geht. Das mit dem Viewport hat mich angeregt es etwas anders zu testen.

Aktuell habe ich mehrere Schichten.
1 Hintergrundschicht mit den Gezeichneten Sachen. Diese wird nur dann neu erstellt wenn auch wirklich was neu dazu kommt oder geändert wird.
1 Schicht welche die aktuelle Zeichenoperation anzeigt (Wie das aufziehen eines Kreises)
Beide sind 2000x2000 pixel groß und ich schätze das hier kostet mich die meiste Performance
g2d.drawImage(offScreenImage, 0, 0, this);

Ein Versuch meine Zeichenschicht mal zu verkleinern auf 400x400 entfernt diese verzögerung beim Aufziehen des Kreises soweit das es bei nem Aktuellen PC nicht mehr störend ist.
Meine Idee wieso sollte das ganze immer 2000x2000 groß sein. Wenn es ausreicht das sie die angezeigt Fläche als größe hat.
Selbst bei einem 24" TFT wird die fläche vermutlich nur 1200x800 pixel größ wenn man das Fenster Maximiert.

Oder hab ich das mit dem Viewport falsch verstanden und was war etwas anderes simpleres gemeint als ich gerade denke?


----------



## Steev (23. Feb 2010)

Die Idee mit dem Viewport ging schon in dieselbe Richtung. Nur das ich halt gedacht hatte, das die Größe von 2000x2000 schon so ihre Richtigkeit hätte. Ich habe mir einfach gedacht, dass man diese große Fläche in mehrere kleine Flächen aufteilt und dann nur das zeichnet, was auch gezeichnet werden muss.


----------



## Empire Phoenix (24. Feb 2010)

Mach das zwetie image nur so groß wie den Kreis, und render es über den Background, wozu x tausend nicht benutzte pixel rendern?


----------



## Marco13 (24. Feb 2010)

Wenn man den Kreis größer zieht müssen dann aber ständig neue, größere Images angelegt werden. Und spätestens, wenn der Kreis den ganzen Bildschim füllt... ja ....

Für mich klingt in erster Linie die Tatsache... "ungünstig", dass auch das aktuelle Objekt als Bild gezeichnet wird. Das Bild ist dann ja praktisch komplett transparent (bis auf so ein paar hundert Kreispixel), und transparente Bilder malen ist u.U. SEHR langsam. Kannst du nicht das, was du da in das obere Bild zeichnest, direkt das Graphics reinzeichnen, in das du auch das untere Bild zeichnest?


----------



## Steev (24. Feb 2010)

Es ist halt die Frage, wie das Zeichenprogramm realisiert werden soll. Wenn es nur mit Primitiven gezeichnet werden soll, dann kann man im Grunde genommen zu jeder Ebene einfach nur die entsprechenden Zeichenobjekte verwalten und alles in ein VolatileImage zeichnen. Wenn allerdings noch effekte und sonstige Zeichnereichen verwendet werden sollen, dann macht ein oder mehrere BufferedImages schon Sinn. Nur sollte man halt nur das Zeichnen, was auch wirklich benötigt wird. Deshalb würde ich die BufferedImages der Ebenen in mehrere kleinere Bilder aufteilen.


----------



## Anton2k (24. Feb 2010)

Im Prinzip sind die Ebenen gar nicht nötig da es vermutlich nicht mehr als 100 gleichzeitige Grafikobjekte werden. Welche jedes mal gezeichnet werden müssten.

Viel interessanter finde ich diese Sache mit dem in Bereiche aufteilen. Gibt es irgendwo was dazu wie ein Code der mir das etwas klar macht. Hätte keine idee wie ich das machen sollte auser:
Ich schaue wie Groß mein Zeichenbereich ist. Schaue wo meine Scrollbars sind. Und lasse einfach alles Zeichnen in abhängikeit der Position. Srich das meiste wird vermutlich dadruch im Nichts gezeichnet, dafür ist die Reine DrawImage Funktion nicht mehr so eine riesen Fläche


----------



## Steev (24. Feb 2010)

Das zeichnen von mehreren hundert Grafikobjekten bringt mittels der standard-java-Zeichenfunktionen enorme Performanceeinstürze, das würde ich entweder mit LWJGL oder gar nicht machen ;-)

Bei dem zerlegen der Bitmap arbeitest du im Grunde genommen genau wie bei einer Tile-Map. Dasselbe Prinziep habe ich mal bei einer Minimap gebraucht, die etwas größer geworden ist. Hier ist mal der Quellcode dazu, ich hoffe du kannst etwas damit anfangen:

[Java]package com.steevcitiy.components;

// Imports
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferedImage;

import javax.swing.JPanel;

import com.easy.gameengine.renderengine.Camera;
import com.easy.gameengine.util.Constants;
import com.steevcitiy.map.Map;
import com.steevcitiy.map.Sprite;
import com.steevcitiy.map.Tile;

/**
 * Diese Klasse stellt eine Mini-Map-Komponente dar, die beliebige Tile-Maps
 * anzeigen kann.
 * 
 * @author Steev
 * @version 1.0
 */
public class MiniMap extends JPanel implements Runnable, MouseListener, MouseMotionListener {
    // private konstante Klassenattribute
    private static final long           serialVersionUID = -4199345235422422604L;

    // private Objektattribute
    private transient BufferedImage[][] tiles            = null;
    private int                         mmapWidth        = 0;
    private int                         mmapHeight       = 0;
    private int                         tileSizeX        = 0;
    private int                         tileSizeY        = 0;
    private int                         noOfXTiles       = 0;
    private int                         noOfYTiles       = 0;
    private int                         tileSize         = 200;
    private Camera                      cam              = null;
    private Map                         tileMap          = null;

    // Konstruktoren
    public MiniMap(Camera cam, Map tileMap) {
        init(cam, tileMap);
        addMouseListener(this);
        addMouseMotionListener(this);
        new Thread(this).start();
    }

    // private Methoden
    private void proceedMouseEvent(int x, int y) {
        /*
         * Berechne die Skalierungsfaktoren die berücksichtigt werden müssen.
         */
        int max = Math.max(mmapWidth, mmapHeight);
        double scfx = getWidth() / (double) max;
        double scfy = getHeight() / (double) max;
        int mw = (int) (mmapWidth * scfx);
        int mh = (int) (mmapHeight * scfy);
        int xm = (getWidth() - mw) / 2;
        int ym = (getHeight() - mh) / 2;
        int mx = xm;
        int my = ym;
        scfx = max / (double) getWidth();
        scfy = max / (double) getHeight();

        /*
         * Wenn sich die Position innerhalb der angezeigten Minimap befindet,
         * dann verschiebe die Kamera an die ausgewählte Position.
         */
        if ((x >= mx && x <= (getWidth() - xm)) && (y >= my && y <= (getHeight() - ym))) {
            int tx = x - mx;
            int ty = y - my;
            tx *= scfx;
            ty *= scfy;

            /*
             * Zentrieren der Ansicht auf den ausgewählten Punkt
             */
            int cw = (int) (cam.getWidth() / tileMap.getTileHeight());
            tx -= cw / 2.;

            double txp = (tx - ty) * (tileMap.getTileWidth() / 2.);
            double typ = (tx + ty) * (tileMap.getTileHeight() / 2.);
            cam.moveTo(txp, typ, cam.getZ());
        }
    }

    // implementierte Methoden
    public void run() {
        while (true) {
            repaint();
            try {
                Thread.sleep(60);
            } catch (Exception e) {}
        }
    }

    public void mousePressed(MouseEvent e) {
        proceedMouseEvent(e.getX(), e.getY());
    }

    public void mouseDragged(MouseEvent e) {
        proceedMouseEvent(e.getX(), e.getY());
    }

    public void mouseReleased(MouseEvent e) {}
    public void mouseMoved(MouseEvent e) {}
    public void mouseClicked(MouseEvent e) {}
    public void mouseEntered(MouseEvent e) {}
    public void mouseExited(MouseEvent e) {}

    // überschriebene Methoden
    public void paint(Graphics g) {
        Graphics2D g2 = (Graphics2D) g;

        /*
         * Zeichne einen schwarzen Hintergrund für die Minimap.
         */
        g2.setColor(Color.BLACK);
        g2.fillRect(0, 0, mmapWidth, mmapHeight);

        /*
         * Berechne den Skalierungsfaktor der Minimap.
         */
        double max = Math.max(mmapWidth, mmapHeight);
        double scfx = mmapWidth / (double) max;
        double scfy = mmapHeight / (double) max;
        int mw = (int) (mmapWidth * scfx);
        int mh = (int) (mmapHeight * scfy);
        int mx = (mmapWidth - mw) / 2;
        int my = (mmapHeight - mh) / 2;

        /*
         * Zeichne die Minimap aus den einzelnen Tiles.
         */
        int tx, ty;
        int tw = (int) Math.round(tileSize * scfx);
        int th = (int) Math.round(tileSize * scfy);
        for (int xx = 0; xx < tiles.length; xx++)
            for (int yy = 0; yy < tiles[0].length; yy++)
                try {
                    tx = mx + (int) (xx * tileSizeX * scfx);
                    ty = my + (int) (yy * tileSizeY * scfy);
                    g2.drawImage(tiles[xx][yy], tx, ty, tw, th, null);
                } catch (Exception e) {
                    e.printStackTrace();
                }

        /*
         * Zeichne die Kameraposition
         */
        // Berechne, an welchem Tile die Startposition der Kamera liegt
        double ex = cam.getX() - tileMap.getX();
        double ey = cam.getY() - tileMap.getY();

        double _y = ((2 * ey - ex) / 2.);
        double _x = (ex + _y);

        int tsYpos = (int) (_y / tileMap.getTileHeight());
        int tsXpos = (int) (_x / tileMap.getTileHeight()) - 1;

        // Berechne, an welchem Tile die Endposition der Kamera liegt
        int cw = (int) (cam.getWidth() / tileMap.getTileHeight());
        int ch = (int) (cam.getHeight() / tileMap.getTileHeight());

        // Umrechnen der Koordinaten auf die Mini-Map
        tsXpos *= scfx;
        tsYpos *= scfy;
        cw *= scfx;
        ch *= scfy;

        tsXpos += mx;
        tsYpos += my;

        // Zeichnen des sichtbaren Bereiches
        g2.setClip(mx, my, mw, mh);
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.translate(tsXpos, tsYpos);
        g2.rotate(25.9);
        g2.setColor(Color.RED);
        g2.drawLine(0, 0, 0, -ch);
        g2.drawLine(0, -ch, cw, -ch);
        g2.drawLine(cw, -ch, cw, 0);
        g2.drawLine(cw, 0, 0, 0);
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);

        g2.setColor(Color.BLACK);
        g2.setTransform(Constants.EMPTY_TRANSFORM);
        g2.drawRect(0, 0, mmapWidth, mmapHeight);
    }

    public void setSize(int width, int height) {
        super.setSize(width, height);
    }

    // öffentliche Methoden
    public void init(Camera cam, Map tileMap) {
        if (cam == null)
            throw new IllegalArgumentException("Error, the argument \"cam\" could not be \"null\"!");
        if (tileMap == null)
            throw new IllegalArgumentException("Error, the argument \"tileMap\" could not be \"null\"!");
        if (tileMap.getNoOfXTiles() <= 0)
            throw new IllegalArgumentException("Error, the argument \"tileMap.getNoOfXTiles()\" must be larger than \"0\"!");
        if (tileMap.getNoOfYTiles() <= 0)
            throw new IllegalArgumentException("Error, the argument \"tileMap.getNoOfYTiles()\" must be larger than \"0\"!");

        this.cam = cam;
        this.tileMap = tileMap;
        this.mmapWidth = tileMap.getNoOfXTiles();
        this.mmapHeight = tileMap.getNoOfYTiles();
        setPreferredSize(new Dimension(mmapWidth, mmapWidth));

        noOfXTiles = (int) Math.round(mmapWidth / (double) tileSize);
        noOfYTiles = (int) Math.round(mmapHeight / (double) tileSize);

        if (noOfXTiles <= 0)
            noOfXTiles = 1;
        if (noOfYTiles <= 0)
            noOfYTiles = 1;

        tileSizeX = mmapWidth / noOfXTiles;
        tileSizeY = mmapHeight / noOfYTiles;
        if (tileSizeX % tileSize != 0)
            tileSizeX = tileSize;
        if (tileSizeY % tileSize != 0)
            tileSizeY = tileSize;

        tiles = new BufferedImage[noOfXTiles + 1][noOfYTiles + 1];
        for (int x = 0; x < tiles.length; x++)
            for (int y = 0; y < tiles[0].length; y++)
                tiles[x][y] = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_INT_ARGB);
    }

    public void setRGB(int x, int y, int rgb) {
        if (x < 0 || x >= mmapWidth)
            throw new IllegalArgumentException("Error, the argument \"x\" could not be less than \"0\" and could not be larger or equal \"width\"!");
        if (y < 0 || y >= mmapHeight)
            throw new IllegalArgumentException("Error, the argument \"y\" could not be less than \"0\" and could not be larger or equal \"height\"!");
        int tx = x / tileSizeX;
        int ty = y / tileSizeY;
        int px = x - (tx * tileSizeX);
        int py = y - (ty * tileSizeY);
        tiles[tx][ty].setRGB(px, py, rgb);
    }

    public int getRGB(int x, int y) {
        if (x < 0 || x >= mmapWidth)
            throw new IllegalArgumentException("Error, the argument \"x\" could not be less than \"0\" and could not be larger or equal \"width\"!");
        if (y < 0 || y >= mmapHeight)
            throw new IllegalArgumentException("Error, the argument \"y\" could not be less than \"0\" and could not be larger or equal \"height\"!");
        try {
            int tx = x / tileSizeX;
            int ty = y / tileSizeY;
            int px = x - (tx * tileSizeX);
            int py = y - (ty * tileSizeY);
            return tiles[tx][ty].getRGB(px, py);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return -1;
    }

    public void update() {
        if (tileMap != null) {
            for (int x = 0; x < mmapWidth; x++)
                for (int y = 0; y < mmapHeight; y++)
                    try {
                        Tile tile = (Tile) tileMap.getTiles()[x][y];
                        if (tile != null && tile.getImage() != null)
                            setRGB(x, y, tile.getAvgRGB());
                        try {
                            tile = (Tile) tileMap.getOverlays()[x][y];
                            if (tile != null && tile.getImage() != null)
                                setRGB(x, y, tile.getAvgRGB());
                        } catch (Exception e) {}
                        try {
                            Sprite sprite = tileMap.getSprites()[x][y];
                            if (sprite != null)
                                setRGB(x, y, sprite.getAvgRGB());
                        } catch (Exception e) {}
                    } catch (Exception e) {
                        setRGB(x, y, 0);
                    }
        }
    }

    public BufferedImage[][] getTiles() {
        return tiles;
    }

    public int getMmapWidth() {
        return mmapWidth;
    }

    public int getMmapHeight() {
        return mmapHeight;
    }

    public int getTileSizeX() {
        return tileSizeX;
    }

    public int getTileSizeY() {
        return tileSizeY;
    }

    public int getNoOfXTiles() {
        return noOfXTiles;
    }

    public int getNoOfYTiles() {
        return noOfYTiles;
    }

    public Map getTileMap() {
        return tileMap;
    }

    public void setTileMap(Map tileMap) {
        this.tileMap = tileMap;
        this.mmapWidth = tileMap.getNoOfXTiles();
        this.mmapHeight = tileMap.getNoOfYTiles();
        setPreferredSize(new Dimension(mmapWidth, mmapWidth));

        noOfXTiles = (int) Math.round(mmapWidth / (double) tileSize);
        noOfYTiles = (int) Math.round(mmapHeight / (double) tileSize);

        if (noOfXTiles <= 0)
            noOfXTiles = 1;
        if (noOfYTiles <= 0)
            noOfYTiles = 1;

        tileSizeX = mmapWidth / noOfXTiles;
        tileSizeY = mmapHeight / noOfYTiles;
        if (tileSizeX % tileSize != 0)
            tileSizeX = tileSize;
        if (tileSizeY % tileSize != 0)
            tileSizeY = tileSize;

        tiles = new BufferedImage[noOfXTiles + 1][noOfYTiles + 1];
        for (int x = 0; x < tiles.length; x++)
            for (int y = 0; y < tiles[0].length; y++)
                tiles[x][y] = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_INT_ARGB);

        update();
    }
}[/Java]

Ich hoffe du findest ein paar brauchbare Stellen

Gruß
Steev


----------



## Anton2k (26. Feb 2010)

Danke sieht so aus als wenn ich mir das aus dem Code erarbeiten kann.
Ein ist mir jetzt aber unschön aufgefallen. Beim Versuch ein Bild in eine Image Variable zu laden bekam ich einen Out of Memory error. Gut also war der Speicher der der JVM zugewiesen war erschöpft. 
Ein Blick in den Tastkmanager von Windows zeigt mir auch das die Anwendung lächerliche 120MB direkt nach dem Start verbrauchte.
Probieren brachte mich dann darauf das meine BufferedImage dafür verantwortlich sind. Alleine meine Zeichenfläche mit den 2000x2000x32bit Farbtiefe ergibt ~16MB Speicherverbrauch. Jedes Bild was ich nur einmal laden möchte um es schneller zu verwenden belegt auch den Speicher eine Unkomprimierten Bildes entsprechender Auflösung. 
Denke es muss einen sinnvolleren Weg geben der weit aus weniger Arbeitsspeicher hungrig daher kommt.


----------



## Steev (26. Feb 2010)

Man muss sich halt entscheiden:
Performat = speicherhungrig
speicherarm = unperformant

Der goldene Weg liegt meist irgendwo dazwischen ;-)


----------

