Hello Guest

Java2D shape to NanoVG [CODE]

  • 7 Replies
  • 5907 Views
Java2D shape to NanoVG [CODE]
« on: August 26, 2017, 08:12:58 »
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
--


Code: [Select]
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();
}
}
}
}

Re: Java2D shape to NanoVG [CODE]
« Reply #1 on: September 21, 2017, 18:09:41 »
Just out of interest: did anyone try/change/improve the code above ?

*

Offline spasi

  • *****
  • 2261
    • WebHotelier
Re: Java2D shape to NanoVG [CODE]
« Reply #2 on: September 21, 2017, 21:29:53 »
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:

Code: [Select]
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();
        }
    }
}

Re: Java2D shape to NanoVG [CODE]
« Reply #3 on: September 22, 2017, 07:45:00 »
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.



*

Offline spasi

  • *****
  • 2261
    • WebHotelier
Re: Java2D shape to NanoVG [CODE]
« Reply #4 on: September 22, 2017, 08:36:24 »
I 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).

Re: Java2D shape to NanoVG [CODE]
« Reply #5 on: September 22, 2017, 09:11:55 »
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.

*

Offline spasi

  • *****
  • 2261
    • WebHotelier
Re: Java2D shape to NanoVG [CODE]
« Reply #6 on: September 22, 2017, 09:26:29 »
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.

State touched by NanoVG:

Code: [Select]
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);

What 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.

Re: Java2D shape to NanoVG [CODE]
« Reply #7 on: September 22, 2017, 09:39:18 »
Cool!
Someone is generally talking about/suggesting stencil buffers, which is more or less the same NVidia approach and my preferred one.