Kleines "Transparenz experiment"

Hey Leuts.

Ich hab ein kleines Dirty-One-Class Experiment mit Java2D gebastelt.
Ziel war es quasi durch ein „beleuchtetes Sniper Visier“ auf eine dunkle szene zu schauen.
Dazu hab ich das Hintergrundbild und ein Overlay verwendet. Auf das overlay male ich nur einen
nur schwach transparenten schwarzen kasten, und eben das je nach sightFog transparente „Visier“,
also der Kreis durch den man auf den Hintergrund schaut. (Alles auch aus dem Code entnehmbar.)

Nun. Ich habe 2 Fragen.

Da ich momentan nur auf einem Laptop von… (1990?) „arbeiten“ kann, bin ich nicht wirklich in der lage
das objektiv zu testen. Wären vielleicht ein paar leute so freundlich das einzufügen, die beiden Bilder reinzupacken,
(blood.png und scene.jpg) und es mal zu testen, also zu sagen ob es lagt? (bei mir lagt es nur.)

Ich habe ja das java2d AlphaComposite benutzt. Und in jedem Frame, in jedem pixel des „Visierkreises“ mache ich
AlphaComposite.getInstance(). Geht das nicht auch irgendwie anders?
Gibt es weitere Dinge die man java2d performance mäßig verändern könnte?

Controls:
F11 - Vollbild an, aus
Mauslinksklick - „schiessen“
bewegen - visier bewegen
mausrad - visier größe
mausrad + strg - viesier transparenz

Die zweite Frage. Wie könnte man das in opengl implementieren? Wie könnte ich es schaffen,
(hier ja mit alpha composite und 2 bufferedimages) den hintergrund und das overlay zu trennen,
und das overlay quasi „separat zu verändern“, abgesehen davon das ich da ja nicht jedesmal die textur
ändern würde sondern es irgendwie im shader berechnen müsste… Jemand ne idee? ^^

Danke fürs lesen und eventuell ausprobieren! :smiley:

Der code:

[SPOILER]


import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferedImage;

import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class Main extends JPanel implements Runnable {
	
	private BufferedImage rawSceneImage, sceneImage, overlay, blood;
	private float imageAspect1, imageAspect2;
	private float sniperx, snipery;
	private float hiddenSceneAlpha = 0.99f;
	private float sightFog = 0.0f;
	private float sightValueChangeSpeed = 5.5f;
	private int sightRadius = 300;
	private int bloodx, bloody;
	private int lastFrameWidth, lastFrameHeight, lastFramex, lastFramey;
	private boolean drawBlood, ctrlPressed, fullscreen;
	private Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
	
	public Main() throws Exception {
		rawSceneImage = ImageIO.read(getClass().getClassLoader().getResourceAsStream("scene.jpg"));
		blood = ImageIO.read(getClass().getClassLoader().getResourceAsStream("blood.png"));
		imageAspect1 = (float) rawSceneImage.getWidth() / (float) rawSceneImage.getHeight();
		imageAspect2 = (float) rawSceneImage.getHeight() / (float) rawSceneImage.getWidth();
		sceneImage = new BufferedImage(800, 600, BufferedImage.TYPE_INT_ARGB);
		overlay = new BufferedImage(800, 600, BufferedImage.TYPE_INT_ARGB);
		final JFrame f = new JFrame();
		f.setSize(800, 600);
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		f.setLocationRelativeTo(null);
		addMouseMotionListener(new MouseMotionListener(){
			public void mouseMoved(MouseEvent e){
				sniperx = e.getX();
				snipery = e.getY();
			}
			public void mouseDragged(MouseEvent e){
				mouseMoved(e);
			}
		});
		addMouseListener(new MouseAdapter(){
			public void mousePressed(MouseEvent e){
				if(e.getButton() == MouseEvent.BUTTON1){
					bloodx = e.getX();
					bloody = e.getY();
					drawBlood = true;
				}
			}
		});
		addKeyListener(new KeyAdapter(){
			public void keyPressed(KeyEvent e){
				switch(e.getKeyCode()){
				case KeyEvent.VK_CONTROL:
					ctrlPressed = true;
					break;
				case KeyEvent.VK_F11:
					if((fullscreen = !fullscreen)){
						lastFrameWidth = f.getWidth();
						lastFrameHeight = f.getHeight();
						lastFramex = f.getX();
						lastFramey = f.getY();
						f.dispose();
						f.setLocation(0, 0);
						f.setUndecorated(true);
						f.setSize(screenSize.width, screenSize.height);
						f.setVisible(true);
						f.requestFocus();
						requestFocus();
					}
					else{
						f.dispose();
						f.setUndecorated(false);
						f.setSize(lastFrameWidth, lastFrameHeight);
						f.setLocation(lastFramex, lastFramey);
						f.setVisible(true);
						System.out.println("reset");
					}
					break;
				}
			}
			public void keyReleased(KeyEvent e){
				switch(e.getKeyCode()){
				case KeyEvent.VK_CONTROL:
					ctrlPressed = false;
					break;
				}
			}
		});
		addMouseWheelListener(new MouseWheelListener(){
			public void mouseWheelMoved(MouseWheelEvent e){
				if(ctrlPressed){
					if((sightFog = sightFog -= (float) e.getWheelRotation() * sightValueChangeSpeed * 10) < 0){
						sightFog = 0;
					}
				}
				else{
					if((sightRadius = sightRadius -= (float) e.getWheelRotation() * sightValueChangeSpeed) < 50){
						sightRadius = 50;
					}
				}
			}
		});
		f.addComponentListener(new ComponentAdapter(){
			public void componentResized(ComponentEvent e){
				overlay = new BufferedImage(e.getComponent().getWidth(), e.getComponent().getHeight(), BufferedImage.TYPE_INT_ARGB);
				sceneImage = new BufferedImage(e.getComponent().getWidth(), e.getComponent().getHeight(), BufferedImage.TYPE_INT_ARGB);
				createSceneImage();
			}
		});
		setFocusable(true);
		f.add(this);
		f.setVisible(true);
		requestFocus();
		new Thread(this).start();
	}
	
	private void createSceneImage(){
		int drawWidth = (int) ((float) getHeight() * imageAspect1);
		int drawHeight = getHeight();
		if(drawWidth < getWidth()){
			drawWidth = getWidth();
			drawHeight = (int) ((float) getWidth() * imageAspect2);
		}
		Graphics sceneImageGraphics = sceneImage.getGraphics();
		sceneImageGraphics.drawImage(rawSceneImage, 0, 0, drawWidth, drawHeight, null);
	}
	
	private void drawBlood(){
		Graphics g = sceneImage.getGraphics();
		g.drawImage(blood, bloodx - blood.getWidth() / 2, bloody - blood.getHeight() / 2, null);
	}
	
	public void run(){
		long lastFrame = System.nanoTime();
		double delta;
		long thisFrame; 
		while(true){
			thisFrame = System.nanoTime();
			delta = thisFrame - lastFrame;
			lastFrame = thisFrame;
			repaint();
			try{Thread.sleep(1);}catch(Exception e){}
		}
	}
	
	protected void paintComponent(Graphics g){
		super.paintComponent(g);
		Graphics2D g2d = (Graphics2D) g;
		if(drawBlood){
			drawBlood = false;
			drawBlood();
		}
		Graphics2D overlayGraphics = (Graphics2D) overlay.getGraphics();
		overlayGraphics.setComposite(AlphaComposite.Src);
		overlayGraphics.setColor(new Color(0, 0, 0, 0));
		overlayGraphics.fillRect(0, 0, overlay.getWidth(), overlay.getHeight());
		overlayGraphics.setColor(new Color(0, 0, 0, hiddenSceneAlpha));
		overlayGraphics.fillRect(0, 0, overlay.getWidth(), overlay.getHeight());
		for(int i = (int) sniperx - sightRadius; i < (int) sniperx + sightRadius; i++){
			for(int j = (int) snipery - sightRadius; j < (int) snipery + sightRadius; j++){
				float distance = (float) new Point(i, j).distance(sniperx, snipery);
				if(distance <= sightRadius){
					float alpha = hiddenSceneAlpha / (sightFog + sightRadius) * (distance + sightFog);
					overlayGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, alpha));
					overlayGraphics.setColor(new Color(0, 0, 0, 255));
					overlayGraphics.drawLine(i, j, i, j);
				}
			}
		}
		overlayGraphics.setColor(new Color(100, 0, 0, 255));
		int sniperxi = (int) sniperx;
		int sniperyi = (int) snipery;
		overlayGraphics.drawLine(sniperxi, sniperyi - 20, sniperxi, sniperyi - 50);
		overlayGraphics.drawLine(sniperxi, sniperyi + 20, sniperxi, sniperyi + 50);
		overlayGraphics.drawLine(sniperxi - 20, sniperyi, sniperxi - 50, sniperyi);
		overlayGraphics.drawLine(sniperxi + 20, sniperyi, sniperxi + 50, sniperyi);
		for(int i = 20; i <= 50; i += 5){
			int diff = (int) ((float) i * (float) i * 0.01f);
			overlayGraphics.drawLine(sniperxi - diff, sniperyi - i, sniperxi + diff, sniperyi - i);
			overlayGraphics.drawLine(sniperxi - diff, sniperyi + i, sniperxi + diff, sniperyi + i);
			
			overlayGraphics.drawLine(sniperxi - i, sniperyi - diff, sniperxi - i, sniperyi + diff);
			overlayGraphics.drawLine(sniperxi + i, sniperyi - diff, sniperxi + i, sniperyi + diff);
		}
		
		
		g2d.drawImage(sceneImage, 0, 0, null);
		g2d.drawImage(overlay, 0, 0, null);
	}
	
	public static void main(String[] args) throws Exception {
		new Main();
	}
}

[/SPOILER]

Die Bilder die ich benutzt hab im Anhang.

Hab’ nur kurz geschaut und werde es morgen ggf mal testen, aber bis dahin: Das

                    overlayGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, alpha));
                    overlayGraphics.setColor(new Color(0, 0, 0, 255));

in der innersten Schleife ist sicher ein Killer. Da kann man Das AlphaComposite EINmal zu setzen, und in der Schleife dann nur noch

                    overlayGraphics.setColor(new Color(0, 0, 0, (int)(alpha*255)));

aufrufen.

Aaaaber… das “new Color” und das “drawLine” sind auch Dinge, die man klassich NICHT machen sollte, wenn es schnell gehen soll. Das “overlay” ist ja schon ein BufferedImage. Da sollte man doch direkt mit setRGB was machen können? (Dafür muss ich aber erstmal schauen, WAS genau da gemacht wird…)

Stimmt, setRGB gibts ja auch noch… und new color mach ich auch die ganze zeit… jo das sind auf jedenfall schwere fehler. New color vergess ich deshalb immer “auszulagern”, weil ich mich frueher an die statischen color objekte gewoehnt hatte… naja. Aendere ich heute mal.

Aber nein ich kann das composite eben nicht nur einmal setzen. Weil die transparenz pro pixel sich immer aendert… es ist ein kreis der von innen nach aussen dunkler wird…

Ahja, hab’s gerade nochmal getestet. Sowas wird häufiger mal gesucht. Ich hatte da mal irgendwann was gebastelt. Das ist jetzt nur schnell grob an das angepasst, was du gepostet hattest…

package bytewelt;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.MultipleGradientPaint.CycleMethod;
import java.awt.Point;
import java.awt.RadialGradientPaint;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class LightEffectTestNew
{
    public static void main(String args[])
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            public void run()
            {
                new LightEffectTestNew();
            }
        });
    }


    public LightEffectTestNew()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().add(new LightEffectPanelNew());
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

}


class LightEffectPanelNew extends JPanel implements MouseMotionListener
{  
    private Point point = new Point(0,0);
    private BufferedImage image;

    public LightEffectPanelNew()
    {
        image = loadImage("scene.jpg");
        addMouseMotionListener(this);
    }
    
    private static BufferedImage loadImage(String fileName)
    {
        try
        {
            return convertToARGB(ImageIO.read(new File(fileName)));
        }
        catch (IOException e)
        {
            e.printStackTrace();
            return new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
        }
    }
    
    private static BufferedImage convertToARGB(BufferedImage image)
    {
        BufferedImage result = null;
        result = new BufferedImage(image.getWidth(), image.getHeight(),
            BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = result.createGraphics();
        g.drawImage(image,0,0,null);
        g.dispose();
        return result;
    }
    
    @Override
    public Dimension getPreferredSize()
    {
        return new Dimension(image.getWidth(), image.getHeight());
    }

    @Override
    protected void paintComponent(Graphics gr)
    {
        super.paintComponent(gr);
        Graphics2D g = (Graphics2D)gr;
        g.drawImage(image, 0,0,null);

        drawLightEffect(g);
        
        drawCrossheirs(g);
    }

    private void drawCrossheirs(Graphics2D g)
    {
        g.setColor(new Color(100, 0, 0, 255));
        int sniperxi = point.x;
        int sniperyi = point.y;
        g.drawLine(sniperxi, sniperyi - 20, sniperxi, sniperyi - 50);
        g.drawLine(sniperxi, sniperyi + 20, sniperxi, sniperyi + 50);
        g.drawLine(sniperxi - 20, sniperyi, sniperxi - 50, sniperyi);
        g.drawLine(sniperxi + 20, sniperyi, sniperxi + 50, sniperyi);
        for(int i = 20; i <= 50; i += 5){
            int diff = (int) ((float) i * (float) i * 0.01f);
            g.drawLine(sniperxi - diff, sniperyi - i, sniperxi + diff, sniperyi - i);
            g.drawLine(sniperxi - diff, sniperyi + i, sniperxi + diff, sniperyi + i);
           
            g.drawLine(sniperxi - i, sniperyi - diff, sniperxi - i, sniperyi + diff);
            g.drawLine(sniperxi + i, sniperyi - diff, sniperxi + i, sniperyi + diff);
        }
    }

    private void drawLightEffect(Graphics2D g)
    {
        g.setComposite(AlphaComposite.SrcOver);
        Point2D center = new Point2D.Float(point.x, point.y);
        float radius = 300;
        float[] dist = {0.0f, 0.1f, 1.0f};
        Color[] colors = {
            new Color(0,0,0,0), 
            new Color(0,0,0,0), 
            new Color(0,0,0,240) 
        };
        RadialGradientPaint p =
             new RadialGradientPaint(
                center, radius, dist, colors, CycleMethod.NO_CYCLE);
        g.setPaint(p);
        g.fillRect(0,0,getWidth(),getHeight());
        g.setComposite(AlphaComposite.Src);
    }

    @Override
    public void mouseDragged(MouseEvent e) {
    }

    @Override
    public void mouseMoved(MouseEvent e) {
        point = e.getPoint();
        repaint();
    }
}

Den alten Rechner hier bringt das auch noch ins Schwitzen, aber schon deutlich weniger als das mit der Pixelbearbeitung.

Die Idee hier ist eigentlich genau das, was du beschrieben hast, nur mit Java-Bordmitteln umgesetzt: Man malt ganz pauschal einen schwarzen Kasten über das Bild, aber mit einem RadialGradientPaint, der aus diesem schwarzen Kasten einen runden, durchsichtigen, aber nach außen hin weniger durchsichtig werdenden Bereich „ausstanzt“. Mit dem RadialGradientPaint hat man da ein paar Tuning-Parameter. Man kann z.B. die Verteilung nach

        float[] dist = {0.0f, 0.7f, 1.0f};

ändern, um einen Bereich der Größe 0.7*radius zu haben, der komplett „hell“ ist, und dann fließend dunkler wird. Oder ihm mit

        Color[] colors = {
            new Color(0,128,0,0), 
            new Color(0,128,0,0), 
            new Color(0,32,0,240) 
        };

zumindest andeutungsweise diesen typischen „Nachtsichtgerät-Grünstich“ zu geben (aber wirklich nur andeutungsweise!).

Für manche Effekte wird man sicher doch auf händische Pixel-Manipulation zurückgreifen müssen, und das wird dann prinzipbedingt auf der GPU (d.h. mit einem OpenGL-shader) schneller sein, als wenn die CPU da so sequentiell drüberrödelt, aber … das ist dann auch (im Gegensatz zu jetzt) nicht mit 10 Zeilen getan :wink:

Eben das ist das Problem, letzendlich machen diese ganzen XXXPaint klassen auch nix anderes als eben jeden pixel entsprechend einer formel zu verändern, oder nicht?
aber ich hatte gar nicht daran gedacht das Java wenigstens die basics von sowas bereits hat… danke. Ich probier dein Programm später mal aus.

Und wenn man das mit einem Shader macht: Wie genau bekommt man es nun hin tatsächlich jeden pixel zu bearbeiten?
wenn man zB ein rechteck hat, hat man 4 verticies. wenn ich nun jedem von denen ne farbe gebe, dann werden die eben “zu einander hin interpoliert”…

*** Edit ***

Ahhh ich habs ausprobiert, und gecheckt wie genau das jetzt funktioniert, cooles prinzip ^^
Okaayy. aber das lagt bei mir jetzt so gut wie gar nicht. Wie kann das sein? Ich bin leider zu dumm um die entsprechenden
stellen bei grepcode zu finden, denke aber auch dass das zu umfangreich ist dort… aber danke!

Für die Shader-Frage könnte ich spontan nur irgendwas händewedelndes von “Fragment-Shader” usw. erzählen, aber ein @Fancy könnte hier schon helfen.

Grundsätzlich macht ein RadialGradientPaint natürlich konzeptuell etwas ähnliches, wie das, was du da händisch hingeschrieben hast. Aber wie du bei der Suche in GrepCode vielleicht schon gesehen hast: Das ist ziemlich ausgeklügelt, und da geht’s schnell ans Eingemachte (“die entsprechenden Stellen” hatte ich auch mal gesucht - einfach rauszufinden, WO denn letztendlich die Pixel gesetzt werden … aber wenn’s darum geht, bin ich auch “zu dumm”). Im speziellen landen die Zeichenbefehle da ja hinter der Abstraktionsschicht, die so ein Graphics2D von einem BufferedImage darstellt, bei irgendwelchen nativen Methoden. (Und das hat jetzt nicht primär mit der Mär zu tun “Java ist langsam, nativ ist schnell”, sondern vielmehr damit, dass man, wenn man weiß, auf welcher Maschine das läuft, man ganz andere Möglichkeiten hat).

Genau das hatte ich befürchtet… es ist echt nicht einfach „diese Stellen“ mal zu finden… aber ich werd noch ein bisschen schauen ^^
Dachte da auch so wie du, (zum Thema „java ist langsam, blabla…“) cool das da einer die gleiche (wahrscheinlich fachlich unterschriebene? ^^) Meinung vertritt :smiley:

Btw: bei grepcode bekomm ich ja den code vom ojdk. Die Versionen sollten aber doch mehr oder weniger identisch sein, oder nicht?

Ja, im Prinzip schon (man kann auch direkt in http://hg.openjdk.java.net/jdk8/ schauen, aber durch die Verlinkungen sind GropCode/DocJar etc. teilweise sogar praktischer…)