Slick Forums

Discuss the Slick 2D Library
It is currently Thu May 23, 2013 10:10 am

All times are UTC




Post new topic Reply to topic  [ 8 posts ] 
Author Message
PostPosted: Thu Sep 27, 2012 11:32 pm 
Offline
Oldbie

Joined: Thu Mar 15, 2012 12:38 am
Posts: 260
Hello!

I am trying to draw (and fill) arbitrary shapes using the Path class. I can draw their outlines just fine. Whenever I try to fill them, something bad happens and they do not seem to get properly filled up.

I have an image and a standalone test case that demonstrates this:

Image:
Everything in the white border should be filled, but as you can see it isn't:
Image



Here is the test case:
Code:
import org.newdawn.slick.AppGameContainer;
import org.newdawn.slick.BasicGame;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.geom.Path;

public class FillPathTest extends BasicGame
{
   private Path path;
   private Color alphaWhiteColor = new Color(255, 255, 255, 50);
   
   public static void main ( String[] args ) throws SlickException
   {
      new AppGameContainer(new FillPathTest(), 800, 600, false).start();
   }

   public FillPathTest ()
   {
      super("Fill Path");
   }

   @Override
   public void init ( GameContainer container ) throws SlickException
   {
      path = new Path(125.0f, 0.0f);
      path.curveTo(0.0f, 125.0f,55.964405f, 0.0f, 0.0f, 55.964405f);
      path.curveTo(102.09405f, 247.90594f,0.0f, 186.21158f, 43.99799f, 237.14687f);
      path.lineTo(102.09405f, 247.90594f);
      path.curveTo(225.0f, 350.0f,112.853134f, 306.002f, 163.78842f, 350.0f);
      path.curveTo(348.05453f, 247.08841f,286.49838f, 350.0f, 337.62393f, 305.58875f);
      path.lineTo(348.05453f, 247.08841f);
      path.curveTo(375.0f, 250.0f,356.73285f, 248.99507f, 365.74905f, 250.0f);
      path.curveTo(500.0f, 125.0f,444.03558f, 250.0f, 500.0f, 194.0356f);
      path.curveTo(375.0f, 0.0f,500.0f, 55.964405f, 444.03558f, 0.0f);
      path.curveTo(251.94547f, 102.91159f,313.50162f, 0.0f, 262.37607f, 44.41126f);
      path.lineTo(251.94548f, 102.91159f);
      path.curveTo(247.906f, 102.09406f,250.6069f, 102.6175f, 249.26028f, 102.344864f);
      path.lineTo(247.90596f, 102.09406f);
      path.curveTo(125.0f, 0.0f,237.14687f, 43.997997f, 186.21158f, 0.0f);
      path.lineTo(125.0f, 0.0f);
   }

   @Override
   public void render ( GameContainer container, Graphics g ) throws SlickException
   {
      g.setColor(alphaWhiteColor);
      g.fill(path);
      
      g.setColor(Color.white);
      g.draw(path);
   }

   @Override
   public void update ( GameContainer container, int delta ) throws SlickException
   {

   }

}


Anyone have any ideas on this? I tried to look at the code that does this, and I'm afraid to admit it is beyond me. :oops:


A related question is...
My ultimate goal is to use these paths to represent the owned territory an empire has in my game. Dave says using fill and draw on basic shapes is generally bad due to the video cards drawing slightly different. My question is: how can I achieve the same sort of drawing by using images (or some other way)? I'm not really sure how to achieve that. So I suppose that would solve my problem too.

Here is an example image from Civ 5. You can see the red/white border. I'm trying to do the same thing:
Image


Thanks.


Top
 Profile  
 
PostPosted: Fri Sep 28, 2012 8:11 pm 
Offline
Slick Zombie

Joined: Sat Jan 27, 2007 7:10 pm
Posts: 1469
It looks like Slick's triangulator can't keep up with the complex polygon you've created. I wish I could debug this, but I've never worked with triangulation, and it's a rather complex topic. MatthiasM might be able to provide more insight, since Slick uses one of his triangulators.

Here are a few options:

1. Create the image of the highlight area in Photoshop. This will result in a very efficient, smooth, and reliable output, but it will also take some setup. You will need to "mix and match" images (e.g. corner piece, straight piece, diagonal piece) to get them looking right, and they will never be as dynamic as a path. They also won't be infinitely scalable, if you need zooming. This is really only viable if your paths are predictable and static, like straight lines or rounded rectangles.

2. Don't use Polygons and Path for rendering, but instead just draw the shapes/lines directly to the screen; e.g. with g.drawArc.

3. Use a simpler polygon that Slick can handle. Or try using boolean operations to see if that results in a working triangulation.

4. Use a different triangulator; for example, poly2tri. This either involves hacking around Slick's codebase, or easier, copying the Slick polygon to a poly2tri polygon, then triangulating, then rendering the resulting triangles with OpenGL.

5. Use Java2D to rasterize the shapes for you. This is a good alternative if the shapes aren't changing every frame. You can also use Java2D's geometry classes for collision/logic, as they may be more robust and more thoroughly tested than Slick's. The nice thing about this is that Java2D (being a software renderer) will appear visually consistent across all platforms (e.g. anti-aliasing will look the same regardless of driver). Plus it includes things like gradients, pattern strokes, etc.

Here's some code for that:

Code:
package slicktests;

import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.GeneralPath;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.List;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.newdawn.slick.AppGameContainer;
import org.newdawn.slick.BasicGame;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.geom.Path;

public class TestRasterize extends BasicGame {
   
   public static void main(String[] args) throws SlickException {
      new AppGameContainer(new TestRasterize(), 800, 600, false).start();
   }

   public TestRasterize() {
      super("Fill Path");
   }

   private BufferedImage bufferImg;
   private Image imageFull;
   private final int SIZE = 1024;
   private IntBuffer buffer;
   
   private Image shapeImage;
   private List<java.awt.Shape> shapes = new ArrayList<java.awt.Shape>();
   private int pointer = 0;
   
   @Override
   public void init(GameContainer container) throws SlickException {
      //It's assumed that our shape can fit inside the texture "SIZE" 1024x1024
      
      // 1. --- Create an empty slick image
      //We will use POWER OF TWO sizes to be safe
      imageFull = new Image(SIZE, SIZE);
      
      // 2. --- Create an empty BufferedImage
      //Java2D's software rasterizer will utilize this
      bufferImg = new BufferedImage(SIZE, SIZE, BufferedImage.TYPE_INT_ARGB);
      
      // 3. --- Setup a few shapes for example
      GeneralPath path = new GeneralPath();
      path.moveTo(50, 120);
      path.lineTo(70, 180);
      path.lineTo(20, 140);
      path.lineTo(80, 140);
      path.lineTo(30, 180);
      path.closePath();
      shapes.add(path);
      
      path = new GeneralPath();
      path.moveTo(120, 180);
      path.quadTo(150, 120, 180, 180);
      path.closePath();
      shapes.add(path);
      
      path = new GeneralPath();
      path.moveTo(220, 150);
      path.curveTo(240, 130, 280, 160, 300, 140);
      path.lineTo(300, 180);
      path.quadTo(260, 160, 220, 180);
      path.closePath();
      shapes.add(path);
      
      path = new GeneralPath();
      path.moveTo(360, 100);
      path.lineTo(360, 200);
      path.lineTo(400, 140);
      path.lineTo(320, 120);
      path.lineTo(400, 180);
      path.lineTo(320, 180);
      path.closePath();
      shapes.add(path);
      
      // 4. --- create our first shape
      createShape(shapes.get(pointer));
   }
   
   /**
    * Uses Java2D to rasterize the given shape, and then passes the resulting pixels to OpenGL.
    * @param shape the shape to rasterize
    */
   protected void createShape(java.awt.Shape shape) {
      //the bounds which we'll use for rendering...
      Rectangle bounds = shape.getBounds();
      
      Graphics2D g2d = (Graphics2D)bufferImg.getGraphics();
      g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      g2d.clearRect(0, 0, bufferImg.getWidth(), bufferImg.getHeight());
      g2d.setColor(java.awt.Color.white);
      g2d.fill(shape);
      
      //now we pass the BufferedImage pixel data to the Slick image...
      upload(bufferImg, imageFull);
      
      //now grab the correct portion of our power-of-two texture which we can use for rendering
      //NOTE: Java2D's stroke will draw on the OUTSIDE edge, so you may need to use bounds + 1 px
      shapeImage = imageFull.getSubImage(bounds.x, bounds.y, bounds.width, bounds.height);
   }
   
   /**
    * Uploads the source (Java2D) data to OpenGL (Slick). The image sizes must be the same.
    * @param source the source image
    * @param destination the destination image
    */
   public void upload(BufferedImage source, Image destination) {
      int width = source.getWidth();
      int height = source.getHeight();
      if (destination.getWidth() != width || destination.getHeight() != height)
         throw new IllegalArgumentException("Slick image size must be == Java2D image");
      int size = width * height;
      if (buffer == null || buffer.capacity() < size)
         buffer = BufferUtils.createIntBuffer(size);
      int[] pixels = ((DataBufferInt)source.getRaster().getDataBuffer()).getData();
      destination.bind();
      buffer.rewind();
      buffer.put(pixels, 0, size);
      buffer.flip();
      GL11.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, width, height,
            GL12.GL_BGRA, GL12.GL_UNSIGNED_INT_8_8_8_8_REV, buffer);
   }

   public void render(GameContainer container, Graphics g)
         throws SlickException {
      shapeImage.draw(50, 50, Color.red);
   }

   @Override
   public void update(GameContainer container, int delta)
         throws SlickException {
      if (container.getInput().isKeyPressed(Input.KEY_SPACE)) {
         pointer++;
         if (pointer>=shapes.size())
            pointer = 0;
         createShape(shapes.get(pointer));
      }
   }
}


Top
 Profile  
 
PostPosted: Tue Oct 02, 2012 11:35 pm 
Offline
Oldbie

Joined: Thu Mar 15, 2012 12:38 am
Posts: 260
davedes wrote:
It looks like Slick's triangulator can't keep up with the complex polygon you've created. I wish I could debug this, but I've never worked with triangulation, and it's a rather complex topic. MatthiasM might be able to provide more insight, since Slick uses one of his triangulators.

Here are a few options:


Thanks for your reply. I haven't encountered triangulation before so this is all new to me. I did notice that Slick uses a few different types of triangulators, and I think the one that was being used was a more "basic" one, but not really sure. I will look into it more and see if I can find one that already exists to work for me.

davedes wrote:
1. Create the image of the highlight area in Photoshop. This will result in a very efficient, smooth, and reliable output, but it will also take some setup. You will need to "mix and match" images (e.g. corner piece, straight piece, diagonal piece) to get them looking right, and they will never be as dynamic as a path. They also won't be infinitely scalable, if you need zooming. This is really only viable if your paths are predictable and static, like straight lines or rounded rectangles.


I may be able to use this....but it requires a question: If I overlay multiple semi-transparent images at the same location, it will look different than just having one? Because I could in theory make a semi-transparent image that represents the size of the area of influence around my system ("city") and just have overlaps (I can draw the border with g.draw(path)). But if the areas that are overlapped multiple times will look different, then I can't do that (unless I find a way to remove that aspect). I'm 99.9999999% sure they will look different. I had this problem in Java2D. Hopefully this makes some sense...



davedes wrote:
2. Don't use Polygons and Path for rendering, but instead just draw the shapes/lines directly to the screen; e.g. with g.drawArc.

3. Use a simpler polygon that Slick can handle. Or try using boolean operations to see if that results in a working triangulation.

4. Use a different triangulator; for example, poly2tri. This either involves hacking around Slick's codebase, or easier, copying the Slick polygon to a poly2tri polygon, then triangulating, then rendering the resulting triangles with OpenGL.


Thanks! I'll see what I can do with these items.

davedes wrote:
5. Use Java2D to rasterize the shapes for you. This is a good alternative if the shapes aren't changing every frame. You can also use Java2D's geometry classes for collision/logic, as they may be more robust and more thoroughly tested than Slick's. The nice thing about this is that Java2D (being a software renderer) will appear visually consistent across all platforms (e.g. anti-aliasing will look the same regardless of driver). Plus it includes things like gradients, pattern strokes, etc.

Here's some code for that:
....snippet....


I was actually thinking of doing this but I wasn't sure how feasible it would be in terms of performance and whatnot. Generally it won't be very often the these borders change, so I could use this method. Though, I would need to have no upper limit on the image size, as I need to be able support any size territories.

Thanks for your thoughts!


Last edited by dayrinni on Wed Oct 03, 2012 9:40 pm, edited 1 time in total.

Top
 Profile  
 
PostPosted: Wed Oct 03, 2012 6:12 am 
Offline
Slick Zombie

Joined: Sat Jan 27, 2007 7:10 pm
Posts: 1469
Quote:
I may be able to use this....but it requires a question: If I overlay multiple semi-transparent images at the same location, it will look different than just having one?

I can't really visualize what you mean without pictures. If the images are semi-transparent and overlapping, and you draw them multiple times, they will add up.

But maybe there is some OpenGL blend mode trickery that would allow you to work around that problem.

Quote:
I was actually thinking of doing this but I wasn't sure how feasible it would be in terms of performance and whatnot. Generally it won't be very often the these borders change, so I could use this method. Though, I would need to have no upper limit on the image size, as I need to be able support any size territories.

Even if the Java2D path is changing every frame, the performance will likely still be sufficient.

Although you are limited by texture size (which, honestly, is going to be at least 4096x4096 in most modern GPUs), you could work around that by only rendering what's visible on-screen, or by breaking it up into multiple large images. Chances are your user's resolution isn't going to be larger than 1920x1080, so 2K textures will work fine.

I would definitely try the Java2D method; if the performance is sufficient for your needs, it will look the best across all platforms and be easy to implement.


Top
 Profile  
 
PostPosted: Wed Oct 03, 2012 2:16 pm 
Offline

Joined: Mon Sep 10, 2012 10:47 am
Posts: 16
Hi,

davedes wrote:
Quote:
I may be able to use this....but it requires a question: If I overlay multiple semi-transparent images at the same location, it will look different than just having one?

I can't really visualize what you mean without pictures. If the images are semi-transparent and overlapping, and you draw them multiple times, they will add up.


I'm not sure what dayrinni means exactly either, but if I do understand him correctly, I'm having a similar problem right now as well. If not, then sorry for dragging this thread off-topic! :wink:

Simplified, I'd like to draw two transparent circles that overlap. The naive solution of course doesn't work:

Code:
      Color green_100 = new Color(0, 255, 0, 100);
      g.setDrawMode(Graphics.MODE_NORMAL);
      g.setColor(green_100);
      g.fillOval(100, 100, 200, 200);
      g.fillOval(200, 100, 200, 200);

This will lead to this:
Image
In the overlapping region, the alpha values get added up making it less transparent, which is not what I want.

So I came up with this method: I create two offscreen images, one for the circles, and one for the alpha map, and draw the circles in there; then, I render both offscreen images to the screen:

Code:
      offImg = new Image(1024, 1024);
      offG = offImg.getGraphics();
      alphaImg = new Image(1024, 1024);
      alphaG = alphaImg.getGraphics();

      Color green = new Color(0, 255, 0);
      Color green_100 = new Color(0, 0, 0, 100);

      alphaG.setDrawMode(Graphics.MODE_ALPHA_MAP);
      alphaG.setColor(green_100);
      alphaG.fillOval(100, 100, 200, 200);
      alphaG.fillOval(200, 100, 200, 200);
      
      offG.setDrawMode(Graphics.MODE_NORMAL);
      offG.setColor(green);
      offG.fillOval(100, 100, 200, 200);
      offG.fillOval(200, 100, 200, 200);
      
      g.setDrawMode(Graphics.MODE_ALPHA_MAP);
      g.drawImage(alphaImg, 0, 0);
      g.setDrawMode(Graphics.MODE_ALPHA_BLEND);
      g.drawImage(offImg, 0 ,0);

The result looks like I intended, i.e. the overlapping region has the same transparency as the rest of the circles. (Of course the Image variables won't be created every frame; this is just testing code. :) ). But the problem here is that I have to do a lot of extra rendering: Two times for every circle, plus rendering the two offscreen images (which will have the same size as the screen resolution) to the screen.

Since I'm ignorant of how OpenGL works, here's my question: Does anyone know a better, more clever way of doing this with fewer offscreen images and/or less pixels to render?

Thanks,

-Kylearan


Top
 Profile  
 
PostPosted: Wed Oct 03, 2012 6:51 pm 
Offline
Slick Zombie

Joined: Sat Jan 27, 2007 7:10 pm
Posts: 1469
kylearan - The increase in opacity is the natural result of rendering two overlapping transparent images.

The solution is to render each sprite at 100% opacity to a single offscreen image, then render that offscreen image to the screen at 50% opacity.

There are two ways of achieving this:

1. Create a new Image like you've done with new Image(width, height). Clear the screen with transparent black (0f,0f,0f,0f), draw the overlapping sprites with 100% opacity, then use g.copyArea to copy that area to an image. Clear the screen again. You can then render the image to the screen using lower opacity. This is an easier solution, but it may not be performant if you are doing it many times per frame, as it requires clearing the screen and transferring pixel data from GPU to CPU.

2. Create an offscreen image with an associated Graphics context. Draw the overlapping sprites with 100% opacity. Then render that offscreen image to the screen using lower opacity. This will be more performant (does not require a pixel transfer or an extra screen clear) but it has a couple of limitations due to Slick's architecture. Firstly, it's advised to use Image.createOffscreenGraphics instead of the (width, height) Image constructor (see this bug). Secondly, if your sprites have transparent areas (such as an outer glow), you may lose some of that information (see this bug and workarounds).


Top
 Profile  
 
PostPosted: Wed Oct 03, 2012 9:46 pm 
Offline
Oldbie

Joined: Thu Mar 15, 2012 12:38 am
Posts: 260
Yes to both(Dave + kylearan), that was exactly what I was trying to describe.

I had a feeling there was a way to make it not do that, but I never really investigated it because I found the Area class in Java2D (I was trying to use rectangles and overlapping them before the Area class).


It seems with option 2 would be best for me, since I I'm just going to have a circle with a transparent color and no fancy borders.

Thanks a lot guys! I'll give this a try and see what happens. I may return to this thread with my results. I'm still going to try and look into some of the other methods you(Dave) mentioned.


Top
 Profile  
 
PostPosted: Sun Oct 07, 2012 2:38 am 
Offline
Oldbie

Joined: Thu Mar 15, 2012 12:38 am
Posts: 260
I've been checking out poly2tri and it looks really cool. I found a couple cool videos on YouTube:

http://www.youtube.com/watch?v=Bt1TYzzr ... ure=relmfu

and

http://www.youtube.com/watch?v=Gdceq4fOIMY

Pretty neat stuff!

Edit: I should of probably added this, but I am working on a conversion from slick to poly2tri and back again. I have a simple example that breaks up a rectangle into two triangles. I would like to somehow incorporate this into Slick and I'll send you a PM when I'm done Dave (assuming I am able to get it all working :) ). I have a feeling it'll be a utility feature for people who want to use this.


Top
 Profile  
 
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 8 posts ] 

All times are UTC


Who is online

Users browsing this forum: Force and 2 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to:  
Powered by phpBB® Forum Software © phpBB Group