Java2D shape to NanoVG [CODE]

Started by MikOfClassX, August 26, 2017, 08:12:58

Previous topic - Next topic

MikOfClassX

Hello all,

I want to share the code I developed to convert any Java2D shape to a NanoVG path.
It is robust enogh to handle any kind of Shape, GeneralPath, Glyph outline. CW/CCW of paths sections is auto-detected in order to emulate 1:1 the Java2D renderer.
I hope someone finds it useful.

Mik
--


import java.awt.Shape;
import java.awt.geom.PathIterator;
import java.util.ArrayList;

import org.lwjgl.nanovg.NanoVG;

import it.classx.obj.ObjText;
import it.classx.util.javafx.JavaFXTextUtils;

/**
 * @author Mik of ClassX
 */
public class SGLNanoVGUtil
{
	/**
	 * @param vertices a float array of {x,y} coords
	 * @return the area, signed (polygon is CW if area>0, else it's CCW)
	 */
	private static double areaSigned(double[] vertices)
	{
		double area = 0;
		final int nverts = vertices.length / 2;

		// compute area
		for (int v = 0; v < nverts; v++)
		{
			final int i = v * 2;
			final int j = ((v + 1) % nverts) * 2;

			area += (vertices[j] - vertices[i]) * (vertices[j + 1] + vertices[i + 1]);
		}

		return area / 2;
	}

	/**
	 * Convert a java2d shape to a NVG path ready to draw
	 * 
	 * @param nvgContext the context
	 * @param shape the shape
	 */
	public static void toNVGPath(long nvgContext, Shape shape)
	{
		if (shape != null)
		{
			// vertex/op storage
			ArrayList<Float> vert = new ArrayList<>();
			ArrayList<Integer> op = new ArrayList<>();

			// allocate the path iterator
			final float[] segment = new float[6];
			final PathIterator pi = shape.getPathIterator(null);

			// starting path here
			NanoVG.nvgBeginPath(nvgContext);
			NanoVG.nvgPathWinding(nvgContext, NanoVG.NVG_SOLID);

			// scan the shape and accumulate points
			while (!pi.isDone())
			{
				int segType = pi.currentSegment(segment);

				// always add segtype
				op.add(segType);

				switch (segType)
				{
					case PathIterator.SEG_MOVETO:
						vert.add(segment[0]);
						vert.add(segment[1]);
					break;

					case PathIterator.SEG_LINETO:
						vert.add(segment[0]);
						vert.add(segment[1]);
					break;

					case PathIterator.SEG_QUADTO:
						vert.add(segment[0]);
						vert.add(segment[1]);
						vert.add(segment[2]);
						vert.add(segment[3]);
					break;

					case PathIterator.SEG_CUBICTO:
						vert.add(segment[0]);
						vert.add(segment[1]);
						vert.add(segment[2]);
						vert.add(segment[3]);
						vert.add(segment[4]);
						vert.add(segment[5]);
					break;

					case PathIterator.SEG_CLOSE:

						// the path is closed: we transform the vertex storage into a NanoVG path

						// here we get the array
						final double[] vertArray = vert.parallelStream().mapToDouble(f -> f != null ? f : Float.NaN).toArray();

						// we compute the winding
						final int winding = areaSigned(vertArray) > 0 ? NanoVG.NVG_SOLID : NanoVG.NVG_HOLE;

						int id = 0;
						final int sz = op.size();
						for (int i = 0; i < sz; i++)
						{
							segType = op.get(i);

							switch (segType)
							{
								case PathIterator.SEG_MOVETO:
									NanoVG.nvgMoveTo(nvgContext, (float) vertArray[id++], (float) vertArray[id++]);
								break;

								case PathIterator.SEG_LINETO:
									NanoVG.nvgLineTo(nvgContext, (float) vertArray[id++], (float) vertArray[id++]);
								break;

								case PathIterator.SEG_QUADTO:
									NanoVG.nvgQuadTo(nvgContext, (float) vertArray[id++], (float) vertArray[id++], (float) vertArray[id++], (float) vertArray[id++]);
								break;

								case PathIterator.SEG_CUBICTO:
									NanoVG.nvgBezierTo(nvgContext, (float) vertArray[id++], (float) vertArray[id++], (float) vertArray[id++], (float) vertArray[id++], (float) vertArray[id++], (float) vertArray[id++]);
								break;

								case PathIterator.SEG_CLOSE:
									NanoVG.nvgClosePath(nvgContext);
									NanoVG.nvgPathWinding(nvgContext, winding);
								break;
							}
						}

						// done with this path section
						vert.clear();
						op.clear();

					break;
				}
				pi.next();
			}
		}
	}
}

MikOfClassX

Just out of interest: did anyone try/change/improve the code above ?

spasi

There are several performance issues:

- It's doing a ton of allocations when boxing the segments into Floats and Integers.
- A new vertex array is allocated for each segment.
- The segment floats are converted to double (because Stream::mapToFloat() doesn't exist), then to float again when calling the nvg functions.
- Using .parallelStream() is almost always a bad idea. You need a lot of data to benefit from concurrency. This is by far the biggest performance hit.

I would do things differently:

public class SGLNanoVGUtil {

    private static float cross(float x0, float y0, float x1, float y1) {
        return (x0 * y1) - (x1 * y0);
    }

    private static float cross0(float x, float y, float[] segment) {
        return cross(x, y, segment[0], segment[1]);
    }

    private static float cross1(float x, float y, float[] segment) {
        return cross0(x, y, segment) + cross(segment[0], segment[1], segment[2], segment[3]);
    }

    private static float cross2(float x, float y, float[] segment) {
        return cross1(x, y, segment) + cross(segment[2], segment[3], segment[4], segment[5]);
    }

    public static void toNVGPath(long nvgContext, Shape shape) {
        nvgBeginPath(nvgContext);

        PathIterator pi = shape.getPathIterator(null);

        float area = 0.0f;
        float x0   = 0.0f, y0 = 0.0f;
        float x    = 0.0f, y = 0.0f;

        float[] segment = new float[6];
        while (!pi.isDone()) {
            switch (pi.currentSegment(segment)) {
                case PathIterator.SEG_MOVETO:
                    nvgMoveTo(nvgContext, segment[0], segment[1]);
                    x0 = x = segment[0];
                    y0 = y = segment[1];
                    break;

                case PathIterator.SEG_LINETO:
                    area += cross0(x, y, segment);
                    nvgLineTo(nvgContext, segment[0], segment[1]);
                    x = segment[0];
                    y = segment[1];
                    break;

                case PathIterator.SEG_QUADTO:
                    area += cross1(x, y, segment);
                    nvgQuadTo(nvgContext, segment[0], segment[1], segment[2], segment[3]);
                    x = segment[2];
                    y = segment[3];
                    break;

                case PathIterator.SEG_CUBICTO:
                    area += cross2(x, y, segment);
                    nvgBezierTo(nvgContext, segment[0], segment[1], segment[2], segment[3], segment[4], segment[5]);
                    x = segment[4];
                    y = segment[5];
                    break;

                case PathIterator.SEG_CLOSE:
                    area += cross(x, y, x0, y0);

                    nvgClosePath(nvgContext);
                    nvgPathWinding(nvgContext, area < 0 ? NVG_SOLID : NVG_HOLE);

                    area = 0.0f;
                    break;
            }
            pi.next();
        }
    }
}

MikOfClassX

Hello Spasi and thanks for sharing.

My snippet was a first attempt to use NanoVG in my beta code (while slowly moving my code base to LWJGL3).
I was investigating the NanoVG API. I don't think it will be of help for arbitrary-shape clipping or other stuff (path caching) we can achieve with the proprietary nvPathRendering api.

Anyway, I have little to add about your comments, as they are absolutely true and welcome.
Building the area step-by-step is absolutely cool and avoids a double-pass approach (and unneeded allocations, streams, etc).
Your code gives exactly the same results of mine. I think it may be worthly included in some future release of LWJGL.



spasi

Quote from: MikOfClassX on September 22, 2017, 07:45:00I think it may be worthly included in some future release of LWJGL.

LWJGL must not have any dependency to AWT, so this is not possible. But for static shapes, you can decouple the above code from the nvg calls. Store the segments and path windings offline (with Java2D), render the raw data at runtime (with NanoVG).

MikOfClassX

Java2D cache, then render. Yes it's something I was thinking of, as I noticed that NanoVG is quite fast even in direct mode.

One possible problem is that NanoVG uses to modify the GL status when drawing. I suppose I'll need some push/pop to restore it.

What about draw-to-stencil ? May be useful for shape clipping.

spasi

Quote from: MikOfClassX on September 22, 2017, 09:11:55One possible problem is that NanoVG uses to modify the GL status when drawing. I suppose I'll need some push/pop to restore it.

State touched by NanoVG:

glUseProgram(prog);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	glEnable(GL_CULL_FACE);
	glCullFace(GL_BACK);
	glFrontFace(GL_CCW);
	glEnable(GL_BLEND);
	glDisable(GL_DEPTH_TEST);
	glDisable(GL_SCISSOR_TEST);
	glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
	glStencilMask(0xffffffff);
	glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
	glStencilFunc(GL_ALWAYS, 0, 0xffffffff);
	glActiveTexture(GL_TEXTURE0);
	glBindBuffer(GL_UNIFORM_BUFFER, buf);
	glBindVertexArray(arr);
	glBindBuffer(GL_ARRAY_BUFFER, buf);
	glBindTexture(GL_TEXTURE_2D, tex);
	glUniformBlockBinding(... , GLNVG_FRAG_BINDING);


Quote from: MikOfClassX on September 22, 2017, 09:11:55What about draw-to-stencil ? May be useful for shape clipping.

Not sure. NanoVG doesn't support generic shape-based clipping, but there are some ideas you could try, here.

MikOfClassX

Cool!
Someone is generally talking about/suggesting stencil buffers, which is more or less the same NVidia approach and my preferred one.