I need to plot animated frames in which each pixel is calculated on the fly as the result of an algorithm. Full screen animations may thus require many millions of operations per frame. I would like to achieve the highest refresh rate possible, preferably 20 to 30+ frames per second, if possible.
Can someone show me how to design/write a highly optimized architecture for extremely fast frame refreshment in java?
This needs to be platform independent, so I cannot take advantage of hardware acceleration. The code will be executed on each individual user's computer, NOT on a central server. Of course, I am separately approaching this from the standpoint of simplifying the algorithms for generating the pixel values within-each-frame, but this question is about architecture for high speed frame-by-frame refreshment between-frames, independent of the algorithm used to generate pixel values within each frame. For example, in answers to this posting, I am looking for methods such as using: BufferedImage, double-buffering, multi-threading, accelerated off-screen images, other between-frames methods, etc.
I wrote some sample code below to simulate the problem. At full screen on my notebook computer, the code below individually refreshes 1,300,000+ pixels per frame with unique values. This takes 500 milliseconds per frame on a machine with four processors and 8 gigabytes of memory. I suspect that I am not using BufferedImage correctly below, and I would really like to learn about other between-frame, architecture-level techniques for optimizing the code below, independent of the algorithms I will end up using to calculate pixel values within-each-frame. Your code samples and links to articles would be much appreciated.
How can I improve the code below, from an frame-by-frame (between-frames) architecture standpoint, not from a within-frame standpoint?
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
public class TestBuffer {
private static void createAndShowUI() {
TestPanel fastGraphicsPanel = new TestPanel();
JFrame frame = new JFrame("This Needs A Faster Architecture!");
frame.getContentPane().add(fastGraphicsPanel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setPreferredSize(new Dimension(800,600));
frame.setResizable(true);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {createAndShowUI();}});}
}
#SuppressWarnings("serial")
class TestPanel extends JPanel {
int w, h;
private static int WIDTH = 700;
private static int HEIGHT = 500;
private static final Color BACKGROUND_COLOR = Color.white;
private BufferedImage bImg;
private Color color = Color.black;
public TestPanel() {
bImg = new BufferedImage(WIDTH, HEIGHT,BufferedImage.TYPE_INT_RGB);
Graphics g = bImg.getGraphics();
g.setColor(BACKGROUND_COLOR);
g.fillRect(0, 0, WIDTH, HEIGHT);
Timer myTimer = new Timer(10, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if(w!=0&&h!=0){
if(WIDTH!=w&&HEIGHT!=h){
WIDTH = w; HEIGHT = h;
bImg = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
}
}
repaint();
}
});
myTimer.setInitialDelay(0);
myTimer.setRepeats(true);
myTimer.setCoalesce(true);
myTimer.start();
g.dispose();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
w = getWidth();
h = getHeight();
// System.out.println("w, h are: "+w+", "+h);
long startTime = System.currentTimeMillis();
g.drawImage(bImg, 0, 0, null);
long endDrawImageTime = System.currentTimeMillis();
Graphics2D g2 = (Graphics2D) g;
drawRandomScreen(g2);
long endDrawScreenTime = System.currentTimeMillis();
long stopTime = System.currentTimeMillis();
long drawImageTime = endDrawImageTime - startTime;
long drawScreenTime = endDrawScreenTime - endDrawImageTime;
long elapsedTime = stopTime - startTime;
System.out.println(drawImageTime+", "+drawScreenTime+", "+elapsedTime);
}
private void drawRandomScreen(Graphics2D g2) {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
for(int i=0;i<WIDTH;i++){
for(int j=0;j<HEIGHT;j++){
color = new Color((int) (Math.random() * 255),(int) (Math.random() * 255),(int) (Math.random() * 255));
g2.setColor(color);
g2.drawLine(i, j, i, j);
}
}
}
}
In the example given, drawRandomScreen() executes on the event dispatch thread. As an alternative, let the model evolve on a separate thread (or threads) and sample the resulting image at a sustainable rate, say 25 Hz, using the observer pattern. Synchronize access to the shared image buffer. A complete example is examined here. Profile to verify ongoing optimization efforts.
instead of interacting through Graphics2D you should interact direct with image data. This is my code, with my laptop can run with 20 frame/second (fullscreen)
#SuppressWarnings("serial")
class TestPanel extends JPanel {
int w, h;
private static int WIDTH = 700;
private static int HEIGHT = 500;
private static final Color BACKGROUND_COLOR = Color.white;
private BufferedImage bImg;
private Color color = Color.black;
public TestPanel() {
bImg = new BufferedImage(WIDTH, HEIGHT,BufferedImage.TYPE_INT_RGB);
Graphics g = bImg.getGraphics();
g.setColor(BACKGROUND_COLOR);
g.fillRect(0, 0, WIDTH, HEIGHT);
Timer myTimer = new Timer(10, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if(w!=0&&h!=0){
if(WIDTH!=w&&HEIGHT!=h){
WIDTH = w; HEIGHT = h;
System.out.println("create");
bImg = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
}
}
repaint();
}
});
myTimer.setInitialDelay(0);
myTimer.setRepeats(true);
myTimer.setCoalesce(true);
myTimer.start();
g.dispose();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
w = getWidth();
h = getHeight();
// System.out.println("w, h are: "+w+", "+h);
long startTime = System.currentTimeMillis();
long endDrawImageTime = System.currentTimeMillis();
// Graphics2D g2 = (Graphics2D) g;
drawRandomScreen(bImg);
g.drawImage(bImg, 0, 0, null);
long endDrawScreenTime = System.currentTimeMillis();
long stopTime = System.currentTimeMillis();
long drawImageTime = endDrawImageTime - startTime;
long drawScreenTime = endDrawScreenTime - endDrawImageTime;
long elapsedTime = stopTime - startTime;
System.out.println(drawImageTime+", "+drawScreenTime+", "+elapsedTime);
}
private void drawRandomScreen(BufferedImage image) {
final int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
final int width = image.getWidth();
final int height = image.getHeight();
long startTime = System.currentTimeMillis();
// g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
// RenderingHints.VALUE_ANTIALIAS_ON);
Random r = new Random();
for(int i=0;i<width;i++){
for(int j=0;j<height;j++){
color = new Color(r.nextInt(255),r.nextInt(255),r.nextInt(255));
int pos = j*width+i;
pixels[pos] = color.hashCode();
}
}
long stopTime = System.currentTimeMillis();
System.out.println("time "+(stopTime-startTime));
}
}
This is your new code. In my laptop, fullscreen can run at 150 frames/ second. At you can see, time execution of function drawRandomScreen only 1/2 time of drawImageTime.
private void drawRandomScreen(BufferedImage image) {
final int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
final int width = image.getWidth();
final int height = image.getHeight();
long startTime = System.currentTimeMillis();
Random r = new Random();
int size = pixels.length;
//swap 100 times
int maxPixelsSwap = 1000;
size-=maxPixelsSwap;
for (int i = 0; i < 100; i++) {
int src = r.nextInt(size);
int des = src+r.nextInt(size-src);
int swapsize = r.nextInt(maxPixelsSwap); //maximium
int[] temp = new int[swapsize];
System.arraycopy(pixels, des, temp, 0, swapsize);
System.arraycopy(pixels, src, pixels, des, swapsize);
System.arraycopy(temp, 0, pixels, src, swapsize);
}
size = pixels.length;
int randomTimes = size/10; //only change 10% of pixels
size--;
for (int i = 0; i < randomTimes; i++) {
pixels[r.nextInt(size)]=r.nextInt();
}
long stopTime = System.currentTimeMillis();
System.out.println("time "+(stopTime-startTime));
}
Related
I am trying to create a little game for a project in university. Since I need to update the screen permanently I am looking for a good render loop implementation. It should be full screen. I found this on gamedev.net - Java Games: Active Rendering:
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.util.Random;
import javax.swing.JFrame;
public class SimpleFullScreenGame {
static boolean running;
public static void main( String[] args ) {
// Create game window...
JFrame app = new JFrame();
app.setIgnoreRepaint( true );
app.setUndecorated( true );
// Add ESC listener to quit...
app.addKeyListener( new KeyAdapter() {
public void keyPressed( KeyEvent e ) {
if( e.getKeyCode() == KeyEvent.VK_ESCAPE )
running = false;
}
});
// Get graphics configuration...
GraphicsEnvironment ge =
GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice gd = ge.getDefaultScreenDevice();
GraphicsConfiguration gc = gd.getDefaultConfiguration();
// Change to full screen
gd.setFullScreenWindow( app );
if( gd.isDisplayChangeSupported() ) {
gd.setDisplayMode(
new DisplayMode( 640, 480, 32, DisplayMode.REFRESH_RATE_UNKNOWN )
);
}
// Create BackBuffer...
app.createBufferStrategy( 2 );
BufferStrategy buffer = app.getBufferStrategy();
// Create off-screen drawing surface
BufferedImage bi = gc.createCompatibleImage( 640, 480 );
// Objects needed for rendering...
Graphics graphics = null;
Graphics2D g2d = null;
Color background = Color.BLACK;
Random rand = new Random();
// Variables for counting frames per seconds
int fps = 0;
int frames = 0;
long totalTime = 0;
long curTime = System.currentTimeMillis();
long lastTime = curTime;
running = true;
while( running ) {
try {
// count Frames per second...
lastTime = curTime;
curTime = System.currentTimeMillis();
totalTime += curTime - lastTime;
if( totalTime > 1000 ) {
totalTime -= 1000;
fps = frames;
frames = 0;
}
++frames;
// clear back buffer...
g2d = bi.createGraphics();
g2d.setColor( background );
g2d.fillRect( 0, 0, 639, 479 );
// draw some rectangles...
for( int i = 0; i < 20; ++i ) {
int r = rand.nextInt(256);
int g = rand.nextInt(256);
int b = rand.nextInt(256);
g2d.setColor( new Color(r,g,b) );
int x = rand.nextInt( 640/2 );
int y = rand.nextInt( 480/2 );
int w = rand.nextInt( 640/2 );
int h = rand.nextInt( 480/2 );
g2d.fillRect( x, y, w, h );
}
// display frames per second...
g2d.setFont( new Font( "Courier New", Font.PLAIN, 12 ) );
g2d.setColor( Color.GREEN );
g2d.drawString( String.format( "FPS: %s", fps ), 20, 20 );
// Blit image and flip...
graphics = buffer.getDrawGraphics();
graphics.drawImage( bi, 0, 0, null );
if( !buffer.contentsLost() )
buffer.show();
} finally {
// release resources
if( graphics != null )
graphics.dispose();
if( g2d != null )
g2d.dispose();
}
}
gd.setFullScreenWindow( null );
System.exit(0);
}
}
I executed the code and there are two things I am wondering about:
My display resolution is 1920x1080. The code is using 640x480. Therefore the rendered rectangles look like a 640x480 picture upscaled to 1920x1080 meaning you have huge pixels and the code doesn't take avantage of the higher posible resolution of the display. How can I adapt the code to actually use the native display resolution of the user?
Second, usually what you see when you take a look on games code is a rendering loop living in its own thread to not block the main thread. However thats not the case here. Why? I assume because the JFrame already lives in its own thread created by swing?
Thanks for the help.
There are lots of issues with this code.
First of all, when you are checking (polling) a flag variable in the main thread, that will be updated by a key listener, which will be called in the event dispatch thread, the minimum you have to do, is to declare that variable volatile.
Then, there is no point in using JFrame when you don’t use the Swing framework at all. Further, it’s nonsensical to request double buffering from the AWT and then, use a BufferedImage for another buffering atop the already buffered operation.
Using the native resolution is as easy as removing the setDisplayMode(…) call. After turning the window to full screen, you can simply use getWidth() and getHeight() on it to get the actual dimensions for the operations (it’s not needed for the buffered image, as that was obsolete anyway).
public class SimpleFullScreenGame {
static volatile boolean running = true;
public static void main(String[] args) {
// Create game window...
Frame app = new Frame();
app.setIgnoreRepaint(true);
app.setUndecorated(true);
// Add ESC listener to quit...
app.addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
if(e.getKeyCode() == KeyEvent.VK_ESCAPE) running = false;
}
});
GraphicsDevice dev = app.getGraphicsConfiguration().getDevice();
// Change to full screen
dev.setFullScreenWindow(app);
int width = app.getWidth(), height = app.getHeight();
// Create BackBuffer...
app.createBufferStrategy(2);
BufferStrategy buffer = app.getBufferStrategy();
// Objects needed for rendering...
Color background = Color.BLACK, textColor = Color.GREEN;
Font font = new Font("Courier New", Font.PLAIN, 12);
ThreadLocalRandom rand = ThreadLocalRandom.current();
// Variables for counting frames per seconds
int fps = 0, frames = 0, totalTime = 0;
long currTime = System.nanoTime(), lastTime;
while(running) {
// count Frames per second...
lastTime = currTime;
currTime = System.nanoTime();
totalTime += currTime - lastTime;
if(totalTime > 1_000_000_000) {
totalTime -= 1_000_000_000;
fps = frames;
frames = 0;
}
frames++;
Graphics gfx = buffer.getDrawGraphics();
gfx.setColor(background);
gfx.fillRect(0, 0, width, height);
// draw some rectangles...
for(int i = 0; i < 20; ++i) {
gfx.setColor(new Color(rand.nextInt(0x1000000)));
int x = rand.nextInt(width/2), y = rand.nextInt(height/2);
int w = rand.nextInt(width/2), h = rand.nextInt(height/2);
gfx.fillRect(x, y, w, h);
}
// display frames per second...
gfx.setFont(font);
gfx.setColor(textColor);
gfx.drawString("FPS: " + fps, 20, 20);
gfx.dispose();
if(!buffer.contentsLost()) buffer.show();
}
dev.setFullScreenWindow(null);
System.exit(0);
}
}
I made some other small improvements.
So I have a black image that acts as darkness (In my game). I want to show a small portion around the character only. Like so
The red square is the player.
The green bit is the ground in the game (grass).
The black is the shadow/darkness.
What I want to do is cut a Ellipse/hole out of the blank, black image. I want this Ellipse to be centered around the players (The red square) x and y position.
Currently I am using this to get the effect:
for(int x = 0; x < width; x++) {
for(int y = 0; y < height; y++) {
//draw mask
g.setColor(Color.black);
if(!nearPlayer(x, y)) {
g.drawLine(x, y, x, y);
}
}
}
But, this processes extremely slow and laggs the players movement drastically.
Is this possible?
..the Player is the red square. Basically what i want to do is cut a circle out of the blank, black image around the coordinates of the player relative to said black image.
What DYM by 'black image' - exactly? To me that just looks like the BG is painted black, which would make more sense for any solid color. In that case, just create the red thing using an Area, fill it, then for the border set a stroke & the color to black, and draw it. This example shows how.
The relevant part of that short code is..
public void paintDaisyPart(Graphics2D g, Area daisyArea) {
g.setClip(daisyArea);
g.setColor(Color.YELLOW);
g.fillRect(0, 0, 200, 200);
g.setColor(Color.YELLOW.darker());
g.setClip(null);
g.setStroke(new BasicStroke(3));
g.draw(daisyArea);
}
I must be bored. This is an animated SSCCE version of the code that drew the image above. It is typically showing >130 FPS. And that is on a clunky machine for which I told the guy my spec. was 'cheap' & reminded him twice that I don't play (heavy rendering) games.
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
public class DaisyDisplay {
DaisyDisplay() {
JPanel gui = new JPanel(new BorderLayout(2,2));
final BufferedImage daisy = new BufferedImage(
200,200,BufferedImage.TYPE_INT_RGB);
final JLabel daisyLabel = new JLabel(new ImageIcon(daisy));
gui.add(daisyLabel,BorderLayout.CENTER);
final Daisy daisyPainter = new Daisy();
daisyPainter.setSize(200);
final JLabel fps = new JLabel("FPS: ");
gui.add(fps,BorderLayout.SOUTH);
ActionListener animator = new ActionListener() {
int counter = 0;
long timeLast = 0;
long timeNow = 0;
public void actionPerformed(ActionEvent ae) {
Graphics2D g = daisy.createGraphics();
g.setColor(Color.GREEN.darker());
g.fillRect(0, 0, 200, 200);
daisyPainter.paint(g);
g.dispose();
daisyLabel.repaint();
counter++;
timeNow = System.currentTimeMillis();
if (timeLast<timeNow-1000) {
fps.setText("FPS: " + counter);
counter = 0;
timeLast = timeNow;
}
}
};
Timer timer = new Timer(1,animator);
timer.start();
JOptionPane.showMessageDialog(null, gui);
timer.stop();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new DaisyDisplay();
}
});
}
}
class Daisy {
double size = 200;
Point location;
double offset = 0.0;
public void paint(Graphics2D g) {
Area daisyArea = getDaisyShape();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
offset += .02d;
AffineTransform plain = g.getTransform();
g.setTransform(AffineTransform.getRotateInstance(
offset + (Math.PI*1/8),
100,100));
paintDaisyPart(g,daisyArea);
g.setTransform(AffineTransform.getRotateInstance(
offset + (Math.PI*3/8),
100,100));
paintDaisyPart(g,daisyArea);
g.setTransform(AffineTransform.getRotateInstance(
offset,
100,100));
paintDaisyPart(g,daisyArea);
g.setTransform(AffineTransform.getRotateInstance(
offset + (Math.PI*2/8),
100,100));
paintDaisyPart(g,daisyArea);
g.setTransform(plain);
}
public void setLocation(Point location) {
this.location = location;
}
public void paintDaisyPart(Graphics2D g, Area daisyArea) {
g.setClip(daisyArea);
g.setColor(Color.YELLOW);
g.fillRect(0, 0, 200, 200);
g.setColor(Color.YELLOW.darker());
g.setClip(null);
g.setStroke(new BasicStroke(3));
g.draw(daisyArea);
}
public void setSize(double size) {
this.size = size;
}
public Area getDaisyShape() {
int diameter = (int)size*6/20;
Ellipse2D.Double core = new Ellipse2D.Double(
(size-diameter)/2,(size-diameter)/2,diameter,diameter);
int pad = 10;
int petalWidth = 50;
int petalLength = 75;
Area area = new Area(core);
// left petal
area.add(new Area(new Ellipse2D.Double(
pad,(size-petalWidth)/2,petalLength,petalWidth)));
// right petal
area.add(new Area(new Ellipse2D.Double(
(size-petalLength-pad),(size-petalWidth)/2,petalLength,petalWidth)));
// top petal
area.add(new Area(new Ellipse2D.Double(
(size-petalWidth)/2,pad,petalWidth,petalLength)));
// bottom petal
area.add(new Area(new Ellipse2D.Double(
(size-petalWidth)/2,(size-petalLength-pad),petalWidth,petalLength)));
return area;
}
}
Trying to figure out the best way to do this (And without crossing any specifics DO NOTs that I don't know about).
I'm working on visually displaying a graph (Various nodes, with edges connecting them) with circles and lines to represent such. Each node will be added during runtime and I can't hardcode this. From what I understand, all painting needs to be done in the paint(Graphics g) method - which isn't that helpful, since I can't be change the parameters and it seems this is only called during the initial creation?
Right now I was thinking about having it call various other methods, passing the Graphics object, and depending on other variables - I'll decide whether that's what I even want to call (Since the paint() method is the only one I can call).
Am I going about this completely wrong? Never bothered with this before.
To give you a better idea of what I want to end up with: I want to be able to pass the coordinates of the shape I want to add for the node, and then add it to whatever I have on the graph so far. And then same with the edges, I want to be able to pass the beginning and end point of the line to repaint on top of whatever is existing at that time.
Not exactly what I want right now - but you'll get the idea from what I patched together so far:
import java.awt.*;
import javax.swing.*;
public class MyCanvas extends Canvas
{
public MyCanvas()
{
}
public void paint(Graphics graphics)
{
// Keep this until I figured out if it's painted on load or not.
graphics.drawLine(10, 20, 350, 380);
}
public static void main(String[] args)
{
MyCanvas canvas = new MyCanvas();
JFrame frame = new JFrame();
int vertexes = 0;
// Change this next part later to be dynamic.
vertexes = 10;
int canvasSize = vertexes * vertexes;
frame.setSize(canvasSize, canvasSize);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(canvas);
frame.setVisible(true);
}
public void drawNode(int x, int y, Graphics g)
{
// Treat each location as a 10x10 block. If position 1,1 then go to (5,5) - If position 3,5 then go to (25, 45) eg: (x*10)-5, (y*10)-5
int xLoc = (x*10) - 5;
int yLoc = (y*10) - 5;
g.setColor(Color.white);
g.fillOval(xLoc, yLoc, 8, 8);
g.drawOval(xLoc, yLoc, 8, 8);
}
public void drawArc(int x, int y, int xx, int yy, Graphics g)
{
int xLoc = (x*10) - 5;
int yLoc = (y*10) - 5;
int xxLoc = (xx*10) - 5;
int yyLoc = (yy*10) - 5;
g.drawLine(xLoc, yLoc, xxLoc, yyLoc);
}
}
Edit: (Response for Andrew)
import java.awt.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
public class MyCanvas extends JPanel
{
public MyCanvas() {
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
}
public static void main(String[] args)
{
int vertexes = 0;
// Change this next part later to be dynamic.
vertexes = 10;
int canvasSize = vertexes * vertexes;
JFrame frame = new JFrame();
JLabel label = new JLabel();
BufferedImage bImage = new BufferedImage(canvasSize, canvasSize, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = bImage.createGraphics();
g2d.drawLine(50, 50, 300, 300);
ImageIcon iIcon = new ImageIcon(bImage);
label.setIcon(iIcon);
frame.add(label);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
g2d = drawNode(1,1,g2d);
label.repaint();
}
public static Graphics2D drawNode(int x, int y,Graphics2D g2d)
{
// Treat each location as a 10x10 block. If position 1,1 then go to (5,5) - If position 3,5 then go to (25, 45) eg: (x*10)-5, (y*10)-5
int xLoc = (x*10) - 5;
int yLoc = (y*10) - 5;
g2d.setColor(Color.white);
g2d.fillOval(xLoc, yLoc, 8, 8);
g2d.drawOval(xLoc, yLoc, 8, 8);
return g2d;
}
public static void drawArc(int x, int y, int xx, int yy)
{
int xLoc = (x*10) - 5;
int yLoc = (y*10) - 5;
int xxLoc = (xx*10) - 5;
int yyLoc = (yy*10) - 5;
// g.drawLine(xLoc, yLoc, xxLoc, yyLoc);
}
}
There are various strategies you might pursue for this.
If the objects are never removed from the drawing once done, use a BufferedImage, put it in a (ImageIcon in a) JLabel. When it comes time to update:
Get the graphics instance of the image and draw the new element.
Dispose of the graphics object.
Call repaint() on the label.
Keep a list of the drawn elements. In the paint method, paint them all. When a new element is added, call repaint() on the rendering component.
Here is an example of the 1st technique:
import java.awt.image.BufferedImage;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.Random;
public class MyCanvas
{
JLabel view;
BufferedImage surface;
Random random = new Random();
public MyCanvas()
{
surface = new BufferedImage(600,400,BufferedImage.TYPE_INT_RGB);
view = new JLabel(new ImageIcon(surface));
Graphics g = surface.getGraphics();
g.setColor(Color.ORANGE);
g.fillRect(0,0,600,400);
g.setColor(Color.BLACK);
// Keep this until I figured out if it's painted on load or not.
g.drawLine(10, 20, 350, 380);
g.dispose();
ActionListener listener = new ActionListener() {
public void actionPerformed(ActionEvent ae) {
addNewElement();
}
};
Timer timer = new Timer(200, listener);
timer.start();
}
public void addNewElement() {
boolean drawArc = random.nextBoolean();
int x = random.nextInt(60);
int y = random.nextInt(40);
Graphics g = surface.getGraphics();
if (drawArc) {
g.setColor(Color.BLUE);
int xx = random.nextInt(60);
int yy = random.nextInt(40);
drawArc(x,y,xx,yy,g);
} else {
drawNode(x,y,g);
}
g.dispose();
view.repaint();
}
public static void main(String[] args)
{
MyCanvas canvas = new MyCanvas();
JFrame frame = new JFrame();
int vertexes = 0;
// Change this next part later to be dynamic.
vertexes = 10;
int canvasSize = vertexes * vertexes;
frame.setSize(canvasSize, canvasSize);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setContentPane(canvas.view);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
public void drawNode(int x, int y, Graphics g)
{
// Treat each location as a 10x10 block. If position 1,1 then go to (5,5) - If position 3,5 then go to (25, 45) eg: (x*10)-5, (y*10)-5
int xLoc = (x*10) - 5;
int yLoc = (y*10) - 5;
g.setColor(Color.white);
g.fillOval(xLoc, yLoc, 8, 8);
g.drawOval(xLoc, yLoc, 8, 8);
}
public void drawArc(int x, int y, int xx, int yy, Graphics g)
{
int xLoc = (x*10) - 5;
int yLoc = (y*10) - 5;
int xxLoc = (xx*10) - 5;
int yyLoc = (yy*10) - 5;
g.drawLine(xLoc, yLoc, xxLoc, yyLoc);
}
}
Further tip
You might notice that the lines look quite 'jagged' & ugly. Both the BufferedImage or a JComponent has access to the more useful Graphics2D object (for the JComponent it is necessary to cast it in paintComponent()). A Graphics2D instance accepts rendering hints that can be used to smooth (dither) the elements drawn.
I'd like to implement a simple bitmap font drawing in Java AWT-based application. Application draws on a Graphics object, where I'd like to implement a simple algorithm:
1) Load a file (probably using ImageIO.read(new File(fileName))), which is 1-bit PNG that looks something like that:
I.e. it's 16*16 (or 16*many, if I'd like to support Unicode) matrix of 8*8 characters. Black corresponds to background color, white corresponds to foreground.
2) Draw strings character-by-character, blitting relevant parts of this bitmap to target Graphics. So far I've only succeeded with something like that:
int posX = ch % 16;
int posY = ch / 16;
int fontX = posX * CHAR_WIDTH;
int fontY = posY * CHAR_HEIGHT;
g.drawImage(
font,
dx, dy, dx + CHAR_WIDTH, dy + CHAR_HEIGHT,
fontX, fontY, fontX + CHAR_WIDTH, fontY + CHAR_HEIGHT,
null
);
It works, but, alas, it blits the text as is, i.e. I can't substitute black and white with desired foreground and background colors, and I can't even make background transparent.
So, the question is: is there a simple (and fast!) way in Java to blit part of one 1-bit bitmap to another, colorizing it in process of blitting (i.e. replacing all 0 pixels with one given color and all 1 pixels with another)?
I've researched into a couple of solutions, all of them look suboptimal to me:
Using a custom colorizing BufferedImageOp, as outlined in this solution - it should work, but it seems that it would be very inefficient to recolorize a bitmap before every blit operation.
Using multiple 32-bit RGBA PNG, with alpha channel set to 0 for black pixels and to maximum for foreground. Every desired foreground color should get its own pre-rendered bitmap. This way I can make background transparent and draw it as a rectangle separately before blitting and then select one bitmap with my font, pre-colorized with desired color and draw a portion of it over that rectangle. Seems like a huge overkill to me - and what makes this option even worse - it limits number of foreground colors to a relatively small amount (i.e. I can realistically load up and hold like hundreds or thousands of bitmaps, not millions)
Bundling and loading a custom font, as outlined in this solution could work, but as far as I see in Font#createFont documentation, AWT's Font seems to work only with vector-based fonts, not with bitmap-based.
May be there's already any libraries that implement such functionality? Or it's time for me to switch to some sort of more advanced graphics library, something like lwjgl?
Benchmarking results
I've tested a couple of algorithms in a simple test: I have 2 strings, 71 characters each, and draw them continuously one after another, right on the same place:
for (int i = 0; i < N; i++) {
cv.putString(5, 5, STR, Color.RED, Color.BLUE);
cv.putString(5, 5, STR2, Color.RED, Color.BLUE);
}
Then I measure time taken and calculate speed: string per second and characters per second. So far, various implementation I've tested yield the following results:
bitmap font, 16*16 characters bitmap: 10991 strings / sec, 780391 chars / sec
bitmap font, pre-split images: 11048 strings / sec, 784443 chars / sec
g.drawString(): 8952 strings / sec, 635631 chars / sec
colored bitmap font, colorized using LookupOp and ByteLookupTable: 404 strings / sec, 28741 chars / sec
You might turn each bitmap into a Shape (or many of them) and draw the Shape. See Smoothing a jagged path for the process of gaining the Shape.
E.G.
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.awt.geom.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import java.util.Random;
/* Gain the outline of an image for further processing. */
class ImageShape {
private BufferedImage image;
private BufferedImage ImageShape;
private Area areaOutline = null;
private JLabel labelOutline;
private JLabel output;
private BufferedImage anim;
private Random random = new Random();
private int count = 0;
private long time = System.currentTimeMillis();
private String rate = "";
public ImageShape(BufferedImage image) {
this.image = image;
}
public void drawOutline() {
if (areaOutline!=null) {
Graphics2D g = ImageShape.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0,0,ImageShape.getWidth(),ImageShape.getHeight());
g.setColor(Color.RED);
g.setClip(areaOutline);
g.fillRect(0,0,ImageShape.getWidth(),ImageShape.getHeight());
g.setColor(Color.BLACK);
g.setClip(null);
g.draw(areaOutline);
g.dispose();
}
}
public Area getOutline(Color target, BufferedImage bi) {
// construct the GeneralPath
GeneralPath gp = new GeneralPath();
boolean cont = false;
int targetRGB = target.getRGB();
for (int xx=0; xx<bi.getWidth(); xx++) {
for (int yy=0; yy<bi.getHeight(); yy++) {
if (bi.getRGB(xx,yy)==targetRGB) {
if (cont) {
gp.lineTo(xx,yy);
gp.lineTo(xx,yy+1);
gp.lineTo(xx+1,yy+1);
gp.lineTo(xx+1,yy);
gp.lineTo(xx,yy);
} else {
gp.moveTo(xx,yy);
}
cont = true;
} else {
cont = false;
}
}
cont = false;
}
gp.closePath();
// construct the Area from the GP & return it
return new Area(gp);
}
public JPanel getGui() {
JPanel images = new JPanel(new GridLayout(1,2,2,2));
JPanel gui = new JPanel(new BorderLayout(3,3));
JPanel originalImage = new JPanel(new BorderLayout(2,2));
final JLabel originalLabel = new JLabel(new ImageIcon(image));
originalImage.add(originalLabel);
images.add(originalImage);
ImageShape = new BufferedImage(
image.getWidth(),
image.getHeight(),
BufferedImage.TYPE_INT_RGB
);
labelOutline = new JLabel(new ImageIcon(ImageShape));
images.add(labelOutline);
anim = new BufferedImage(
image.getWidth()*2,
image.getHeight()*2,
BufferedImage.TYPE_INT_RGB);
output = new JLabel(new ImageIcon(anim));
gui.add(output, BorderLayout.CENTER);
updateImages();
gui.add(images, BorderLayout.NORTH);
animate();
ActionListener al = new ActionListener() {
public void actionPerformed(ActionEvent ae) {
animate();
}
};
Timer timer = new Timer(1,al);
timer.start();
return gui;
}
private void updateImages() {
areaOutline = getOutline(Color.BLACK, image);
drawOutline();
}
private void animate() {
Graphics2D gr = anim.createGraphics();
gr.setColor(Color.BLUE);
gr.fillRect(0,0,anim.getWidth(),anim.getHeight());
count++;
if (count%100==0) {
long now = System.currentTimeMillis();
long duration = now-time;
double fraction = (double)duration/1000;
rate = "" + (double)100/fraction;
time = now;
}
gr.setColor(Color.WHITE);
gr.translate(0,0);
gr.drawString(rate, 20, 20);
int x = random.nextInt(image.getWidth());
int y = random.nextInt(image.getHeight());
gr.translate(x,y);
int r = 128+random.nextInt(127);
int g = 128+random.nextInt(127);
int b = 128+random.nextInt(127);
gr.setColor(new Color(r,g,b));
gr.draw(areaOutline);
gr.dispose();
output.repaint();
}
public static void main(String[] args) throws Exception {
int size = 150;
final BufferedImage outline = javax.imageio.ImageIO.read(new java.io.File("img.gif"));
ImageShape io = new ImageShape(outline);
JFrame f = new JFrame("Image Outline");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(io.getGui());
f.pack();
f.setResizable(false);
f.setLocationByPlatform(true);
f.setVisible(true);
}
}
I have to figure there is a factor of ten error in the FPS count on the top left of the blue image though. 50 FPS I could believe, but 500 FPS seems ..wrong.
Okay, looks like I've found the best solution. The key to success was accessing raw pixel arrays in underlying AWT structures. Initialization goes something like that:
public class ConsoleCanvas extends Canvas {
protected BufferedImage buffer;
protected int w;
protected int h;
protected int[] data;
public ConsoleCanvas(int w, int h) {
super();
this.w = w;
this.h = h;
}
public void initialize() {
data = new int[h * w];
// Fill data array with pure solid black
Arrays.fill(data, 0xff000000);
// Java's endless black magic to get it working
DataBufferInt db = new DataBufferInt(data, h * w);
ColorModel cm = ColorModel.getRGBdefault();
SampleModel sm = cm.createCompatibleSampleModel(w, h);
WritableRaster wr = Raster.createWritableRaster(sm, db, null);
buffer = new BufferedImage(cm, wr, false, null);
}
#Override
public void paint(Graphics g) {
update(g);
}
#Override
public void update(Graphics g) {
g.drawImage(buffer, 0, 0, null);
}
}
After this one, you've got both a buffer that you can blit on canvas updates and underlying array of ARGB 4-byte ints - data.
Single character can be drawn like that:
private void putChar(int dx, int dy, char ch, int fore, int back) {
int charIdx = 0;
int canvasIdx = dy * canvas.w + dx;
for (int i = 0; i < CHAR_HEIGHT; i++) {
for (int j = 0; j < CHAR_WIDTH; j++) {
canvas.data[canvasIdx] = font[ch][charIdx] ? fore : back;
charIdx++;
canvasIdx++;
}
canvasIdx += canvas.w - CHAR_WIDTH;
}
}
This one uses a simple boolean[][] array, where first index chooses character and second index iterates over raw 1-bit character pixel data (true => foreground, false => background).
I'll try to publish a complete solution as a part of my Java terminal emulation class set soon.
This solution benchmarks for impressive 26007 strings / sec or 1846553 chars / sec - that's 2.3x times faster than previous best non-colorized drawImage().
I would like to know how I would handle pixel operations best in Java. I'm using swing and drawing a rectangle of 1 by 1 pixel is terribly slow. I need to get 60 fps at least, without using too much resources. Would blitting to an image first mean this could be archieved succesfully? Or is it a bad idea in general to do this with Java and do I need to stick to C or another alternative?
I'm in the beginning of writing a raycaster and since openCL, which I'm using, has a wrapper for Java, I prefer working in Java.
Adding to #camickr's suggestion:
Create a BufferedImage (BI), wrap it in a IconImage, set it as the icon for a JLabel. Paint changes onto the BI and call JLabel's repaint() to flush those. changes to the screen. Caching the partial image in a BI lessens the amount of work for the paint routine. It just needs to blit the image, not render the image. Use SwingWorkers to deploy background threads to run the calculations and pass the results back to the EDT for painting.
As long as you're talking about a static image, that will work fine. If you are considering something more like a video game (some fixed images and other moving images) take a look at VolatileImage. There is a good description here: http://gpwiki.org/index.php/Java:Tutorials:VolatileImage
Update:
The following gives me a bit over 80 fps:
public class Demo extends javax.swing.JPanel {
private Image src = null;
public Demo() {
new Worker().execute();
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (src != null) g.drawImage(src, 0, 0, this);
}
private class Worker extends SwingWorker<Void, Image>{
private final Color[] colors = { Color.red, Color.green, Color.blue };
protected void process(List<Image> chunks){
for (Image bufferedImage : chunks){
src = bufferedImage;
repaint();
}
}
protected Void doInBackground() throws Exception{
int frames = 0;
int[] mem = new int[1024 * 768];
long start = System.currentTimeMillis();
long end = start + 15000;
long last = start;
while (last < end){
int col = colors[frames % colors.length].getRGB();
for (int y = 0; y < 768; y++)
for (int x = 0; x < 1024; x++)
mem[x + y * 1024] = col;
Image img = createImage(new MemoryImageSource(1024, 768, mem, 0, 1024));
BufferedImage bi = new BufferedImage(1024, 768, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = bi.createGraphics();
g2.drawImage(img, 0, 0, null);
g2.dispose();
publish(bi);
last = System.currentTimeMillis();
frames++;
}
System.err.println("Frames = " + frames + ", fps = " + ((double) frames / (last - start) * 1000));
return null;
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run(){
JFrame jf = new JFrame();
jf.getContentPane().add(new Demo(), BorderLayout.CENTER);
jf.setSize(1024, 768);
jf.setVisible(true);
}
});
}
}
Use a BufferedImage and the setRGB(...) method. Then you draw the entire image in your paint routine.