/* * Copyright 2013 Hannes Janetzek * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * * This program is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU Lesser General License for more details. * * You should have received a copy of the GNU Lesser General License along with * this program. If not, see <http://www.gnu.org/licenses/>. */ package org.oscim.renderer.bucket; import static org.oscim.backend.GLAdapter.gl; import org.oscim.backend.GL; import org.oscim.backend.GLAdapter; import org.oscim.backend.canvas.Paint.Cap; import org.oscim.core.GeometryBuffer; import org.oscim.core.MercatorProjection; import org.oscim.renderer.GLShader; import org.oscim.renderer.GLState; import org.oscim.renderer.GLUtils; import org.oscim.renderer.GLViewport; import org.oscim.renderer.MapRenderer; import org.oscim.theme.styles.LineStyle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Note: * Coordinates must be in range [-4096..4096] and the maximum * resolution for coordinates is 0.25 as points will be converted * to fixed point values. */ public final class LineBucket extends RenderBucket { static final Logger log = LoggerFactory.getLogger(LineBucket.class); private static final float COORD_SCALE = MapRenderer.COORD_SCALE; /** scale factor mapping extrusion vector to short values */ public static final float DIR_SCALE = 2048; /** maximal resolution */ private static final float MIN_DIST = 1 / 8f; /** * not quite right.. need to go back so that additional * bevel vertices are at least MIN_DIST apart */ private static final float BEVEL_MIN = MIN_DIST * 4; /** * mask for packing last two bits of extrusion vector with texture * coordinates */ private static final int DIR_MASK = 0xFFFFFFFC; /* lines referenced by this outline layer */ public LineBucket outlines; public LineStyle line; public float scale = 1; public boolean roundCap; private float mMinDist = MIN_DIST; public float heightOffset; private int tmin = Integer.MIN_VALUE, tmax = Integer.MAX_VALUE; public LineBucket(int layer) { super(RenderBucket.LINE, false, false); this.level = layer; } public void addOutline(LineBucket link) { for (LineBucket l = outlines; l != null; l = l.outlines) if (link == l) return; link.outlines = outlines; outlines = link; } public void setExtents(int min, int max) { tmin = min; tmax = max; } /** * For point reduction by minimal distance. Default is 1/8. */ public void setDropDistance(float minDist) { mMinDist = Math.max(minDist, MIN_DIST); } public void addLine(GeometryBuffer geom) { if (geom.isPoly()) addLine(geom.points, geom.index, -1, true); else if (geom.isLine()) addLine(geom.points, geom.index, -1, false); else log.debug("geometry must be LINE or POLYGON"); } public void addLine(float[] points, int numPoints, boolean closed) { if (numPoints >= 4) addLine(points, null, numPoints, closed); } private void addLine(float[] points, int[] index, int numPoints, boolean closed) { boolean rounded = false; boolean squared = false; if (line.cap == Cap.ROUND) rounded = true; else if (line.cap == Cap.SQUARE) squared = true; /* Note: just a hack to save some vertices, when there are * more than 200 lines per type. FIXME make optional! */ if (rounded && index != null) { int cnt = 0; for (int i = 0, n = index.length; i < n; i++, cnt++) { if (index[i] < 0) break; if (cnt > 400) { rounded = false; break; } } } roundCap = rounded; int n; int length = 0; if (index == null) { n = 1; if (numPoints > 0) { length = numPoints; } else { length = points.length; } } else { n = index.length; } for (int i = 0, pos = 0; i < n; i++) { if (index != null) length = index[i]; /* check end-marker in indices */ if (length < 0) break; int ipos = pos; pos += length; /* need at least two points */ if (length < 4) continue; /* start an enpoint are equal */ if (length == 4 && points[ipos] == points[ipos + 2] && points[ipos + 1] == points[ipos + 3]) continue; /* avoid simple 180 degree angles */ if (length == 6 && points[ipos] == points[ipos + 4] && points[ipos + 1] == points[ipos + 5]) length -= 2; addLine(vertexItems, points, ipos, length, rounded, squared, closed); } } private void addVertex(VertexData vi, float x, float y, float vNextX, float vNextY, float vPrevX, float vPrevY) { float ux = vNextX + vPrevX; float uy = vNextY + vPrevY; /* vPrev times perpendicular of sum(vNext, vPrev) */ double a = uy * vPrevX - ux * vPrevY; if (a < 0.01 && a > -0.01) { ux = -vPrevY; uy = vPrevX; } else { ux /= a; uy /= a; } short ox = (short) (x * COORD_SCALE); short oy = (short) (y * COORD_SCALE); int ddx = (int) (ux * DIR_SCALE); int ddy = (int) (uy * DIR_SCALE); vi.add(ox, oy, (short) (0 | ddx & DIR_MASK), (short) (1 | ddy & DIR_MASK)); vi.add(ox, oy, (short) (2 | -ddx & DIR_MASK), (short) (1 | -ddy & DIR_MASK)); } private void addLine(VertexData vertices, float[] points, int start, int length, boolean rounded, boolean squared, boolean closed) { float ux, uy; float vPrevX, vPrevY; float vNextX, vNextY; float curX, curY; float nextX, nextY; double a; /* amount of vertices used * + 2 for drawing triangle-strip * + 4 for round caps * + 2 for closing polygons */ numVertices += length + (rounded ? 6 : 2) + (closed ? 2 : 0); int ipos = start; curX = points[ipos++]; curY = points[ipos++]; nextX = points[ipos++]; nextY = points[ipos++]; /* Unit vector to next node */ vPrevX = nextX - curX; vPrevY = nextY - curY; a = (float) Math.sqrt(vPrevX * vPrevX + vPrevY * vPrevY); vPrevX /= a; vPrevY /= a; /* perpendicular on the first segment */ ux = -vPrevY; uy = vPrevX; int ddx, ddy; /* vertex point coordinate */ short ox = (short) (curX * COORD_SCALE); short oy = (short) (curY * COORD_SCALE); /* vertex extrusion vector, last two bit * encode texture coord. */ short dx, dy; /* when the endpoint is outside the tile region omit round caps. */ boolean outside = (curX < tmin || curX > tmax || curY < tmin || curY > tmax); if (rounded && !outside) { ddx = (int) ((ux - vPrevX) * DIR_SCALE); ddy = (int) ((uy - vPrevY) * DIR_SCALE); dx = (short) (0 | ddx & DIR_MASK); dy = (short) (2 | ddy & DIR_MASK); vertices.add(ox, oy, (short) dx, (short) dy); vertices.add(ox, oy, (short) dx, (short) dy); ddx = (int) (-(ux + vPrevX) * DIR_SCALE); ddy = (int) (-(uy + vPrevY) * DIR_SCALE); vertices.add(ox, oy, (short) (2 | ddx & DIR_MASK), (short) (2 | ddy & DIR_MASK)); /* Start of line */ ddx = (int) (ux * DIR_SCALE); ddy = (int) (uy * DIR_SCALE); vertices.add(ox, oy, (short) (0 | ddx & DIR_MASK), (short) (1 | ddy & DIR_MASK)); vertices.add(ox, oy, (short) (2 | -ddx & DIR_MASK), (short) (1 | -ddy & DIR_MASK)); } else { /* outside means line is probably clipped * TODO should align ending with tile boundary * for now, just extend the line a little */ float tx = vPrevX; float ty = vPrevY; if (squared) { tx = 0; ty = 0; } else if (!outside) { tx *= 0.5; ty *= 0.5; } if (rounded) numVertices -= 2; /* add first vertex twice */ ddx = (int) ((ux - tx) * DIR_SCALE); ddy = (int) ((uy - ty) * DIR_SCALE); dx = (short) (0 | ddx & DIR_MASK); dy = (short) (1 | ddy & DIR_MASK); vertices.add(ox, oy, (short) dx, (short) dy); vertices.add(ox, oy, (short) dx, (short) dy); ddx = (int) (-(ux + tx) * DIR_SCALE); ddy = (int) (-(uy + ty) * DIR_SCALE); vertices.add(ox, oy, (short) (2 | ddx & DIR_MASK), (short) (1 | ddy & DIR_MASK)); } curX = nextX; curY = nextY; /* Unit vector pointing back to previous node */ vPrevX *= -1; vPrevY *= -1; // vertexItem.used = opos + 4; for (int end = start + length;;) { if (ipos < end) { nextX = points[ipos++]; nextY = points[ipos++]; } else if (closed && ipos < end + 2) { /* add startpoint == endpoint */ nextX = points[start]; nextY = points[start + 1]; ipos += 2; } else break; /* unit vector pointing forward to next node */ vNextX = nextX - curX; vNextY = nextY - curY; a = Math.sqrt(vNextX * vNextX + vNextY * vNextY); /* skip too short segmets */ if (a < mMinDist) { numVertices -= 2; continue; } vNextX /= a; vNextY /= a; double dotp = (vNextX * vPrevX + vNextY * vPrevY); //log.debug("acos " + dotp); if (dotp > 0.65) { /* add bevel join to avoid miter going to infinity */ numVertices += 2; //dotp = FastMath.clamp(dotp, -1, 1); //double cos = Math.acos(dotp); //log.debug("cos " + Math.toDegrees(cos)); //log.debug("back " + (mMinDist * 2 / Math.sin(cos + Math.PI / 2))); float px, py; if (dotp > 0.999) { /* 360 degree angle, set points aside */ ux = vPrevX + vNextX; uy = vPrevY + vNextY; a = vNextX * uy - vNextY * ux; if (a < 0.1 && a > -0.1) { /* Almost straight */ ux = -vNextY; uy = vNextX; } else { ux /= a; uy /= a; } //log.debug("aside " + a + " " + ux + " " + uy); px = curX - ux * BEVEL_MIN; py = curY - uy * BEVEL_MIN; curX = curX + ux * BEVEL_MIN; curY = curY + uy * BEVEL_MIN; } else { //log.debug("back"); /* go back by min dist */ px = curX + vPrevX * BEVEL_MIN; py = curY + vPrevY * BEVEL_MIN; /* go forward by min dist */ curX = curX + vNextX * BEVEL_MIN; curY = curY + vNextY * BEVEL_MIN; } /* unit vector pointing forward to next node */ vNextX = curX - px; vNextY = curY - py; a = Math.sqrt(vNextX * vNextX + vNextY * vNextY); vNextX /= a; vNextY /= a; addVertex(vertices, px, py, vPrevX, vPrevY, vNextX, vNextY); /* flip unit vector to point back */ vPrevX = -vNextX; vPrevY = -vNextY; /* unit vector pointing forward to next node */ vNextX = nextX - curX; vNextY = nextY - curY; a = Math.sqrt(vNextX * vNextX + vNextY * vNextY); vNextX /= a; vNextY /= a; } addVertex(vertices, curX, curY, vPrevX, vPrevY, vNextX, vNextY); curX = nextX; curY = nextY; /* flip vector to point back */ vPrevX = -vNextX; vPrevY = -vNextY; } ux = vPrevY; uy = -vPrevX; outside = (curX < tmin || curX > tmax || curY < tmin || curY > tmax); ox = (short) (curX * COORD_SCALE); oy = (short) (curY * COORD_SCALE); if (rounded && !outside) { ddx = (int) (ux * DIR_SCALE); ddy = (int) (uy * DIR_SCALE); vertices.add(ox, oy, (short) (0 | ddx & DIR_MASK), (short) (1 | ddy & DIR_MASK)); vertices.add(ox, oy, (short) (2 | -ddx & DIR_MASK), (short) (1 | -ddy & DIR_MASK)); /* For rounded line edges */ ddx = (int) ((ux - vPrevX) * DIR_SCALE); ddy = (int) ((uy - vPrevY) * DIR_SCALE); vertices.add(ox, oy, (short) (0 | ddx & DIR_MASK), (short) (0 | ddy & DIR_MASK)); /* last vertex */ ddx = (int) (-(ux + vPrevX) * DIR_SCALE); ddy = (int) (-(uy + vPrevY) * DIR_SCALE); dx = (short) (2 | ddx & DIR_MASK); dy = (short) (0 | ddy & DIR_MASK); } else { if (squared) { vPrevX = 0; vPrevY = 0; } else if (!outside) { vPrevX *= 0.5; vPrevY *= 0.5; } if (rounded) numVertices -= 2; ddx = (int) ((ux - vPrevX) * DIR_SCALE); ddy = (int) ((uy - vPrevY) * DIR_SCALE); vertices.add(ox, oy, (short) (0 | ddx & DIR_MASK), (short) (1 | ddy & DIR_MASK)); /* last vertex */ ddx = (int) (-(ux + vPrevX) * DIR_SCALE); ddy = (int) (-(uy + vPrevY) * DIR_SCALE); dx = (short) (2 | ddx & DIR_MASK); dy = (short) (1 | ddy & DIR_MASK); } /* add last vertex twice */ vertices.add(ox, oy, (short) dx, (short) dy); vertices.add(ox, oy, (short) dx, (short) dy); } static class Shader extends GLShader { int uMVP, uFade, uWidth, uColor, uMode, uHeight, aPos; Shader(String shaderFile) { if (!create(shaderFile)) return; uMVP = getUniform("u_mvp"); uFade = getUniform("u_fade"); uWidth = getUniform("u_width"); uColor = getUniform("u_color"); uMode = getUniform("u_mode"); uHeight = getUniform("u_height"); aPos = getAttrib("a_pos"); } @Override public boolean useProgram() { if (super.useProgram()) { GLState.enableVertexArrays(aPos, -1); return true; } return false; } } public static final class Renderer { /* TODO: * http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter22.html */ /* factor to normalize extrusion vector and scale to coord scale */ private final static float COORD_SCALE_BY_DIR_SCALE = MapRenderer.COORD_SCALE / LineBucket.DIR_SCALE; private final static int CAP_THIN = 0; private final static int CAP_BUTT = 1; private final static int CAP_ROUND = 2; private final static int SHADER_FLAT = 1; private final static int SHADER_PROJ = 0; public static int mTexID; private static Shader[] shaders = { null, null }; static boolean init() { shaders[0] = new Shader("line_aa_proj"); shaders[1] = new Shader("line_aa"); /* create lookup table as texture for 'length(0..1,0..1)' * using mirrored wrap mode for 'length(-1..1,-1..1)' */ byte[] pixel = new byte[128 * 128]; for (int x = 0; x < 128; x++) { float xx = x * x; for (int y = 0; y < 128; y++) { float yy = y * y; int color = (int) (Math.sqrt(xx + yy) * 2); if (color > 255) color = 255; pixel[x + y * 128] = (byte) color; } } mTexID = GLUtils.loadTexture(pixel, 128, 128, GL.ALPHA, GL.NEAREST, GL.NEAREST, GL.MIRRORED_REPEAT, GL.MIRRORED_REPEAT); return true; } public static RenderBucket draw(RenderBucket b, GLViewport v, float scale, RenderBuckets buckets) { /* simple line shader does not take forward shortening into * account. only used when tilt is 0. */ int mode = v.pos.tilt < 1 ? 1 : 0; Shader s = shaders[mode]; s.useProgram(); GLState.blend(true); /* Somehow we loose the texture after an indefinite * time, when label/symbol textures are used. * Debugging gl on Desktop is most fun imaginable, * so for now: */ if (!GLAdapter.GDX_DESKTOP_QUIRKS) GLState.bindTex2D(mTexID); int uLineFade = s.uFade; int uLineMode = s.uMode; int uLineColor = s.uColor; int uLineWidth = s.uWidth; int uLineHeight = s.uHeight; gl.vertexAttribPointer(s.aPos, 4, GL.SHORT, false, 0, buckets.offset[LINE]); v.mvp.setAsUniform(s.uMVP); /* Line scale factor for non fixed lines: Within a zoom- * level lines would be scaled by the factor 2 by view-matrix. * Though lines should only scale by sqrt(2). This is achieved * by inverting scaling of extrusion vector with: width/sqrt(s). */ double variableScale = Math.sqrt(scale); /* scale factor to map one pixel on tile to one pixel on screen: * used with orthographic projection, (shader mode == 1) */ double pixel = (mode == SHADER_PROJ) ? 0.0001 : 1.5 / scale; gl.uniform1f(uLineFade, (float) pixel); int capMode = 0; gl.uniform1f(uLineMode, capMode); boolean blur = false; double width; float heightOffset = 0; gl.uniform1f(uLineHeight, heightOffset); for (; b != null && b.type == RenderBucket.LINE; b = b.next) { LineBucket lb = (LineBucket) b; LineStyle line = lb.line.current(); if (lb.heightOffset != heightOffset) { heightOffset = lb.heightOffset; gl.uniform1f(uLineHeight, heightOffset / MercatorProjection.groundResolution(v.pos)); } if (line.fadeScale < v.pos.zoomLevel) { GLUtils.setColor(uLineColor, line.color, 1); } else if (line.fadeScale > v.pos.zoomLevel) { continue; } else { float alpha = (float) (scale > 1.2 ? scale : 1.2) - 1; GLUtils.setColor(uLineColor, line.color, alpha); } if (mode == SHADER_PROJ && blur && line.blur == 0) { gl.uniform1f(uLineFade, (float) pixel); blur = false; } /* draw LineLayer */ if (!line.outline) { /* invert scaling of extrusion vectors so that line * width stays the same. */ if (line.fixed) { width = Math.max(line.width, 1) / scale; } else { width = lb.scale * line.width / variableScale; } gl.uniform1f(uLineWidth, (float) (width * COORD_SCALE_BY_DIR_SCALE)); /* Line-edge fade */ if (line.blur > 0) { gl.uniform1f(uLineFade, line.blur); blur = true; } else if (mode == SHADER_FLAT) { gl.uniform1f(uLineFade, (float) (pixel / width)); //GL.uniform1f(uLineScale, (float)(pixel / (ll.width / s))); } /* Cap mode */ if (lb.scale < 1.5 /* || ll.line.fixed */) { if (capMode != CAP_THIN) { capMode = CAP_THIN; gl.uniform1f(uLineMode, capMode); } } else if (lb.roundCap) { if (capMode != CAP_ROUND) { capMode = CAP_ROUND; gl.uniform1f(uLineMode, capMode); } } else if (capMode != CAP_BUTT) { capMode = CAP_BUTT; gl.uniform1f(uLineMode, capMode); } gl.drawArrays(GL.TRIANGLE_STRIP, b.vertexOffset, b.numVertices); continue; } /* draw LineLayers references by this outline */ for (LineBucket ref = lb.outlines; ref != null; ref = ref.outlines) { LineStyle core = ref.line.current(); // core width if (core.fixed) { width = Math.max(core.width, 1) / scale; } else { width = ref.scale * core.width / variableScale; } // add outline width if (line.fixed) { width += line.width / scale; } else { width += lb.scale * line.width / variableScale; } gl.uniform1f(uLineWidth, (float) (width * COORD_SCALE_BY_DIR_SCALE)); /* Line-edge fade */ if (line.blur > 0) { gl.uniform1f(uLineFade, line.blur); blur = true; } else if (mode == SHADER_FLAT) { gl.uniform1f(uLineFade, (float) (pixel / width)); } /* Cap mode */ if (ref.roundCap) { if (capMode != CAP_ROUND) { capMode = CAP_ROUND; gl.uniform1f(uLineMode, capMode); } } else if (capMode != CAP_BUTT) { capMode = CAP_BUTT; gl.uniform1f(uLineMode, capMode); } gl.drawArrays(GL.TRIANGLE_STRIP, ref.vertexOffset, ref.numVertices); } } return b; } } }