diff --git a/worldwind/src/androidMain/kotlin/earth/worldwind/shape/milstd2525/MilStd2525TacticalGraphic.kt b/worldwind/src/androidMain/kotlin/earth/worldwind/shape/milstd2525/MilStd2525TacticalGraphic.kt index d1282467d..cf74ae97f 100644 --- a/worldwind/src/androidMain/kotlin/earth/worldwind/shape/milstd2525/MilStd2525TacticalGraphic.kt +++ b/worldwind/src/androidMain/kotlin/earth/worldwind/shape/milstd2525/MilStd2525TacticalGraphic.kt @@ -138,7 +138,7 @@ actual open class MilStd2525TacticalGraphic @JvmOverloads actual constructor( Polygon(positions, if (i == 0) shapeAttributes else outlineAttributes) } else { Path(positions, if (i == 0) shapeAttributes else outlineAttributes) - } + }.apply { allowBatching = !hasOutline } applyShapeAttributes(shape) if (i == 0) shapes += shape else outlines += shape if (!hasOutline) break diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/WorldWind.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/WorldWind.kt index 893f207ef..022f2e807 100644 --- a/worldwind/src/commonMain/kotlin/earth/worldwind/WorldWind.kt +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/WorldWind.kt @@ -114,6 +114,7 @@ open class WorldWind @JvmOverloads constructor( * The number of bits in the depth buffer associated with this WorldWind. */ protected var depthBits = 0 + protected var frameIndex = 0L private val scratchModelview = Matrix4() private val scratchProjection = Matrix4() private val scratchPoint = Vec3() @@ -444,6 +445,7 @@ open class WorldWind @JvmOverloads constructor( ) rc.renderResourceCache = renderResourceCache rc.verticalExaggeration = verticalExaggeration + rc.frameIndex = frameIndex++ rc.densityFactor = densityFactor rc.atmosphereAltitude = atmosphereAltitude rc.globeState = globe.state diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawShapeState.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawShapeState.kt index be97544e8..606c99096 100644 --- a/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawShapeState.kt +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawShapeState.kt @@ -5,8 +5,8 @@ import earth.worldwind.geom.Vec3 import earth.worldwind.render.Color import earth.worldwind.render.Texture import earth.worldwind.render.buffer.AbstractBufferObject -import earth.worldwind.render.buffer.FloatBufferObject import earth.worldwind.render.program.TriangleShaderProgram +import earth.worldwind.render.VertexState open class DrawShapeState internal constructor() { companion object { @@ -14,7 +14,6 @@ open class DrawShapeState internal constructor() { } var program: TriangleShaderProgram? = null - var vertexBuffer: FloatBufferObject? = null var elementBuffer: AbstractBufferObject? = null val vertexOrigin = Vec3() var vertexStride = 0 @@ -23,32 +22,34 @@ open class DrawShapeState internal constructor() { var enableDepthWrite = true var depthOffset = 0.0 var isLine = false + var isStatic = false + val vertexState = VertexState() + var pickIdOffset = 0 protected val color = Color() protected var opacity = 1.0f protected var lineWidth = 1f protected var texture: Texture? = null protected val texCoordMatrix = Matrix3() - private val texCoordAttrib = VertexAttrib() internal var primCount = 0 internal val prims = Array(MAX_DRAW_ELEMENTS) { DrawElements() } open fun reset() { program = null - vertexBuffer = null + vertexState.reset() elementBuffer = null vertexOrigin.set(0.0, 0.0, 0.0) vertexStride = 0 enableCullFace = true enableDepthTest = true isLine = false + isStatic = false depthOffset = 0.0 color.set(1f, 1f, 1f, 1f) + pickIdOffset = 0 opacity = 1.0f lineWidth = 1f texture = null texCoordMatrix.setToIdentity() - texCoordAttrib.size = 0 - texCoordAttrib.offset = 0 primCount = 0 for (idx in 0 until MAX_DRAW_ELEMENTS) prims[idx].texture = null } @@ -63,11 +64,6 @@ open class DrawShapeState internal constructor() { fun texCoordMatrix(matrix: Matrix3) = apply { texCoordMatrix.copy(matrix) } - fun texCoordAttrib(size: Int, offset: Int) = apply { - texCoordAttrib.size = size - texCoordAttrib.offset = offset - } - open fun drawElements(mode: Int, count: Int, type: Int, offset: Int) { val prim = prims[primCount++] prim.mode = mode @@ -79,8 +75,6 @@ open class DrawShapeState internal constructor() { prim.lineWidth = lineWidth prim.texture = texture prim.texCoordMatrix.copy(texCoordMatrix) - prim.texCoordAttrib.size = texCoordAttrib.size - prim.texCoordAttrib.offset = texCoordAttrib.offset } internal open class DrawElements { @@ -93,11 +87,5 @@ open class DrawShapeState internal constructor() { var lineWidth = 0f var texture: Texture? = null val texCoordMatrix = Matrix3() - val texCoordAttrib = VertexAttrib() - } - - internal open class VertexAttrib { - var size = 0 - var offset = 0 } } \ No newline at end of file diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawableLines.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawableLines.kt index 65bebcded..2e05dc837 100644 --- a/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawableLines.kt +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawableLines.kt @@ -53,8 +53,9 @@ open class DrawableLines protected constructor(): Drawable { // Disable texturing. program.enableTexture(false) - // Ensure program is in triangles mode - program.enableOneVertexMode(false) + // Ensure program is in lines mode + program.enableLinesMode(true) + program.enableVertexColorAndWidth(false) // Use the leader's color. program.loadColor(color) @@ -75,8 +76,8 @@ open class DrawableLines protected constructor(): Drawable { if (!enableDepthTest) dc.gl.disable(GL_DEPTH_TEST) // Use the leader line as the vertex point attribute. - dc.gl.enableVertexAttribArray(1 /*value*/) - dc.gl.enableVertexAttribArray(2 /*value*/) + dc.gl.enableVertexAttribArray(1 /*pointB*/) + dc.gl.enableVertexAttribArray(2 /*pointC*/) // Use the shape's vertex point attribute and vertex texture coordinate attribute. dc.gl.vertexAttribPointer(0 /*pointA*/, 4, GL_FLOAT, false, 20, offset + 0) @@ -89,7 +90,7 @@ open class DrawableLines protected constructor(): Drawable { // Restore the default WorldWind OpenGL state. if (!enableDepthTest) dc.gl.enable(GL_DEPTH_TEST) - dc.gl.disableVertexAttribArray(1 /*value*/) - dc.gl.disableVertexAttribArray(2 /*value*/) + dc.gl.disableVertexAttribArray(1 /*pointB*/) + dc.gl.disableVertexAttribArray(2 /*pointC*/) } } \ No newline at end of file diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawableShape.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawableShape.kt index 824b20c88..0fe4aca24 100644 --- a/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawableShape.kt +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawableShape.kt @@ -4,7 +4,9 @@ import earth.worldwind.geom.Matrix4 import earth.worldwind.util.Pool import earth.worldwind.util.kgl.GL_CULL_FACE import earth.worldwind.util.kgl.GL_DEPTH_TEST -import earth.worldwind.util.kgl.GL_FLOAT +import earth.worldwind.util.kgl.GL_LINES +import earth.worldwind.util.kgl.GL_LINE_LOOP +import earth.worldwind.util.kgl.GL_LINE_STRIP import earth.worldwind.util.kgl.GL_TEXTURE0 import kotlin.jvm.JvmStatic @@ -32,7 +34,7 @@ open class DrawableShape protected constructor(): Drawable { // TODO shape batching val program = drawState.program ?: return // program unspecified if (!program.useProgram(dc)) return // program failed to build - if (drawState.vertexBuffer?.bindBuffer(dc) != true) return // vertex buffer unspecified or failed to bind + if (!drawState.vertexState.bind(dc)) return if (drawState.elementBuffer?.bindBuffer(dc) != true) return // element buffer unspecified or failed to bind // Use the draw context's pick mode. @@ -51,6 +53,7 @@ open class DrawableShape protected constructor(): Drawable { drawState.vertexOrigin.z ) program.loadModelviewProjection(mvpMatrix) + program.loadPickIdOffset(drawState.pickIdOffset) // Disable triangle back face culling if requested. if (!drawState.enableCullFace) dc.gl.disable(GL_CULL_FACE) @@ -64,48 +67,30 @@ open class DrawableShape protected constructor(): Drawable { // Make multi-texture unit 0 active. dc.activeTextureUnit(GL_TEXTURE0) - // Use the shape's vertex point attribute and vertex texture coordinate attribute. - dc.gl.enableVertexAttribArray(1 /*vertexTexCoord*/) - dc.gl.enableVertexAttribArray(2 /*vertexTexCoord*/) - dc.gl.enableVertexAttribArray(3 /*vertexTexCoord*/) + program.enableLinesMode(drawState.isLine) + program.enableVertexColorAndWidth(drawState.isStatic) + if (drawState.isLine) program.loadScreen( + dc.viewport.width.toFloat(), + dc.viewport.height.toFloat() + ) - if (drawState.isLine) { - program.enableOneVertexMode(false) - program.loadScreen(dc.viewport.width.toFloat(), dc.viewport.height.toFloat()) - dc.gl.vertexAttribPointer(0 /*pointA*/, 4, GL_FLOAT, false, 20, 0) - dc.gl.vertexAttribPointer(1 /*pointB*/, 4, GL_FLOAT, false, 20, 40) - dc.gl.vertexAttribPointer(2 /*pointC*/, 4, GL_FLOAT, false, 20, 80) - dc.gl.vertexAttribPointer(3 /*texCoord*/, 1, GL_FLOAT, false, 20, 56) - } else { - program.enableOneVertexMode(true) - dc.gl.vertexAttribPointer(0 /*vertexPoint*/, 3, GL_FLOAT, false, drawState.vertexStride, 0) - dc.gl.vertexAttribPointer(1 /*vertexPoint*/, 3, GL_FLOAT, false, drawState.vertexStride, 0) - dc.gl.vertexAttribPointer(2 /*vertexPoint*/, 3, GL_FLOAT, false, drawState.vertexStride, 0) - } // Draw the specified primitives. for (idx in 0 until drawState.primCount) { val prim = drawState.prims[idx] - program.loadColor(prim.color) program.loadOpacity(prim.opacity) + if (!drawState.isStatic) { + program.loadColor(prim.color) + program.loadLineWidth(prim.lineWidth) + } if (prim.texture?.bindTexture(dc) == true) { program.loadTexCoordMatrix(prim.texCoordMatrix) program.enableTexture(true) } else { program.enableTexture(false) } - if (drawState.isLine) { - program.loadLineWidth(prim.lineWidth) - } else { - dc.gl.vertexAttribPointer( - 3 /*vertexTexCoord*/, - prim.texCoordAttrib.size, - GL_FLOAT, - false, - drawState.vertexStride, - prim.texCoordAttrib.offset - ) - dc.gl.lineWidth(prim.lineWidth) - } + if (prim.mode == GL_LINES || prim.mode == GL_LINE_STRIP || prim.mode == GL_LINE_LOOP) dc.gl.lineWidth( + prim.lineWidth + ) dc.gl.drawElements(prim.mode, prim.count, prim.type, prim.offset) } @@ -115,8 +100,6 @@ open class DrawableShape protected constructor(): Drawable { if (!drawState.enableDepthWrite) dc.gl.depthMask(true) dc.gl.lineWidth(1f) dc.gl.enable(GL_CULL_FACE) - dc.gl.disableVertexAttribArray(1 /*vertexTexCoord*/) - dc.gl.disableVertexAttribArray(2 /*vertexTexCoord*/) - dc.gl.disableVertexAttribArray(3 /*vertexTexCoord*/) + drawState.vertexState.unbind(dc) } } \ No newline at end of file diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawableSurfaceShape.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawableSurfaceShape.kt index 0022407bd..808371ff0 100644 --- a/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawableSurfaceShape.kt +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawableSurfaceShape.kt @@ -42,11 +42,6 @@ open class DrawableSurfaceShape protected constructor(): Drawable { // Make multi-texture unit 0 active. dc.activeTextureUnit(GL_TEXTURE0) - // Set up to use vertex tex coord attributes. - dc.gl.enableVertexAttribArray(1 /*vertexTexCoord*/) - dc.gl.enableVertexAttribArray(2 /*vertexTexCoord*/) - dc.gl.enableVertexAttribArray(3 /*vertexTexCoord*/) - // Accumulate shapes in the draw context's scratch list. // TODO accumulate in a geospatial quadtree val scratchList = dc.scratchList @@ -75,10 +70,6 @@ open class DrawableSurfaceShape protected constructor(): Drawable { } finally { // Clear the accumulated shapes. scratchList.clear() - // Restore the default WorldWind OpenGL state. - dc.gl.disableVertexAttribArray(1 /*vertexTexCoord*/) - dc.gl.disableVertexAttribArray(2 /*vertexTexCoord*/) - dc.gl.disableVertexAttribArray(3 /*vertexTexCoord*/) } } @@ -124,9 +115,16 @@ open class DrawableSurfaceShape protected constructor(): Drawable { // Get the shape. val shape = element as DrawableSurfaceShape if (shape.offset != terrain.offset || !shape.sector.intersectsOrNextTo(terrainSector)) continue - if (shape.drawState.vertexBuffer?.bindBuffer(dc) != true) continue // vertex buffer unspecified or failed to bind + if (!shape.drawState.vertexState.bind(dc)) continue if (shape.drawState.elementBuffer?.bindBuffer(dc) != true) continue // element buffer unspecified or failed to bind + program.enableLinesMode(shape.drawState.isLine) + program.enableVertexColorAndWidth(shape.drawState.isStatic) + if (shape.drawState.isLine) program.loadScreen( + colorAttachment.width.toFloat(), + colorAttachment.height.toFloat() + ) + // Transform local shape coordinates to texture fragments appropriate for the terrain sector. mvpMatrix.copy(textureMvpMatrix) mvpMatrix.multiplyByTranslation( @@ -135,51 +133,32 @@ open class DrawableSurfaceShape protected constructor(): Drawable { shape.drawState.vertexOrigin.z ) program.loadModelviewProjection(mvpMatrix) - if (shape.drawState.isLine) { - program.enableOneVertexMode(false) - program.loadScreen(colorAttachment.width.toFloat(), colorAttachment.height.toFloat()) - - dc.gl.vertexAttribPointer(0 /*pointA*/, 4, GL_FLOAT, false, 20, 0) - dc.gl.vertexAttribPointer(1 /*pointB*/, 4, GL_FLOAT, false, 20, 40) - dc.gl.vertexAttribPointer(2 /*pointC*/, 4, GL_FLOAT, false, 20, 80) - dc.gl.vertexAttribPointer(3 /*vertexTexCoord*/, 1, GL_FLOAT, false, 20, 56) - } else { - program.enableOneVertexMode(true) - - // Use the shape's vertex point attribute. - dc.gl.vertexAttribPointer(0 /*vertexPoint*/, 3, GL_FLOAT, false, shape.drawState.vertexStride, 0) - dc.gl.vertexAttribPointer(1 /*vertexPoint*/, 3, GL_FLOAT, false, shape.drawState.vertexStride, 0) - dc.gl.vertexAttribPointer(2 /*vertexPoint*/, 3, GL_FLOAT, false, shape.drawState.vertexStride, 0) - } + program.loadPickIdOffset(shape.drawState.pickIdOffset) + // Draw the specified primitives to the framebuffer texture. for (primIdx in 0 until shape.drawState.primCount) { val prim = shape.drawState.prims[primIdx] - program.loadColor(prim.color) program.loadOpacity(prim.opacity) + if (!drawState.isStatic) { + program.loadColor(prim.color) + program.loadLineWidth(prim.lineWidth) + } if (prim.texture?.bindTexture(dc) == true) { program.loadTexCoordMatrix(prim.texCoordMatrix) program.enableTexture(true) } else { program.enableTexture(false) } - if (shape.drawState.isLine) { - program.loadLineWidth(prim.lineWidth) - } else { - dc.gl.vertexAttribPointer( - 3 /*vertexTexCoord*/, - prim.texCoordAttrib.size, - GL_FLOAT, - false, - shape.drawState.vertexStride, - prim.texCoordAttrib.offset - ) - dc.gl.lineWidth(prim.lineWidth) - } + if (prim.mode == GL_LINES || prim.mode == GL_LINE_STRIP || prim.mode == GL_LINE_LOOP) dc.gl.lineWidth( + prim.lineWidth + ) dc.gl.drawElements(prim.mode, prim.count, prim.type, prim.offset) } // Accumulate the number of shapes drawn into the texture. shapeCount++ + + shape.drawState.vertexState.unbind(dc) } } finally { // Restore the default WorldWind OpenGL state. @@ -194,22 +173,24 @@ open class DrawableSurfaceShape protected constructor(): Drawable { protected open fun drawTextureToTerrain(dc: DrawContext, terrain: DrawableTerrain) { val program = drawState.program ?: return try { + dc.gl.enableVertexAttribArray(3 /*vertexTexCoord*/) if (!terrain.useVertexPointAttrib(dc, 0 /*vertexPoint*/)) return // terrain vertex attribute failed to bind - if (!terrain.useVertexPointAttrib(dc, 1 /*vertexPoint*/)) return // terrain vertex attribute failed to bind - if (!terrain.useVertexPointAttrib(dc, 2 /*vertexPoint*/)) return // terrain vertex attribute failed to bind if (!terrain.useVertexTexCoordAttrib(dc, 3 /*vertexTexCoord*/)) return // terrain vertex attribute failed to bind + val colorAttachment = dc.scratchFramebuffer.getAttachedTexture(GL_COLOR_ATTACHMENT0) if (!colorAttachment.bindTexture(dc)) return // framebuffer texture failed to bind // Configure the program to draw texture fragments unmodified and aligned with the terrain. // TODO consolidate pickMode and enableTexture into a single textureMode // TODO it's confusing that pickMode must be disabled during surface shape render-to-texture - program.enableOneVertexMode(true) + program.enableLinesMode(false) + program.enableVertexColorAndWidth(false) program.enablePickMode(false) program.enableTexture(true) program.loadTexCoordMatrix(identityMatrix3) program.loadColor(color) program.loadOpacity(opacity) + program.loadPickIdOffset(0) // Use the draw context's modelview projection matrix, transformed to terrain local coordinates. val terrainOrigin = terrain.vertexOrigin @@ -220,6 +201,7 @@ open class DrawableSurfaceShape protected constructor(): Drawable { // Draw the terrain as triangles. terrain.drawTriangles(dc) } finally { + dc.gl.disableVertexAttribArray(3 /*vertexTexCoord*/) // Unbind color attachment texture to avoid feedback loop dc.bindTexture(KglTexture.NONE) } diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/layer/RenderableLayer.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/layer/RenderableLayer.kt index 07f9a8fa9..23176620a 100644 --- a/worldwind/src/commonMain/kotlin/earth/worldwind/layer/RenderableLayer.kt +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/layer/RenderableLayer.kt @@ -2,14 +2,18 @@ package earth.worldwind.layer import earth.worldwind.render.RenderContext import earth.worldwind.render.Renderable +import earth.worldwind.render.BatchRenderer import earth.worldwind.util.Logger.ERROR import earth.worldwind.util.Logger.logMessage import kotlin.jvm.JvmOverloads +import kotlin.reflect.KClass open class RenderableLayer @JvmOverloads constructor(displayName: String? = null): AbstractLayer(displayName), Iterable { protected val renderables = mutableListOf() val count get() = renderables.size + val batchRenderers = mutableMapOf, BatchRenderer>() + constructor(layer: RenderableLayer): this(layer.displayName) { addAllRenderables(layer) } constructor(renderables: Iterable): this() { addAllRenderables(renderables) } @@ -23,11 +27,12 @@ open class RenderableLayer @JvmOverloads constructor(displayName: String? = null return renderables[index] } + // TODO: Make use of automatic removal from batches via frameIndex comparison and stop removing renderables from batches here fun setRenderable(index: Int, renderable: Renderable): Renderable { require(index in renderables.indices) { logMessage(ERROR, "RenderableLayer", "setRenderable", "invalidIndex") } - return renderables.set(index, renderable) + return renderables.set(index, renderable).also { batchRenderers[it::class]?.removeRenderable(it) } } fun indexOfRenderable(renderable: Renderable) = renderables.indexOf(renderable) @@ -61,22 +66,39 @@ open class RenderableLayer @JvmOverloads constructor(displayName: String? = null fun addAllRenderables(iterable: Iterable) { for (renderable in iterable) renderables.add(renderable) } - fun removeRenderable(renderable: Renderable) = renderables.remove(renderable) + // TODO: Make use of automatic removal from batches via frameIndex comparison and stop removing renderables from batches here + fun removeRenderable(renderable: Renderable) : Boolean { + if (renderables.remove(renderable)) { + batchRenderers[renderable::class]?.removeRenderable(renderable) + return true + } + return false + } + // TODO: Make use of automatic removal from batches via frameIndex comparison and stop removing renderables from batches here fun removeRenderable(index: Int): Renderable { require(index in renderables.indices) { logMessage(ERROR, "RenderableLayer", "removeRenderable", "invalidIndex") } - return renderables.removeAt(index) + return renderables.removeAt(index).also { batchRenderers[it::class]?.removeRenderable(it) } } + // TODO: Make use of automatic removal from batches via frameIndex comparison and stop removing renderables from batches here fun removeAllRenderables(renderables: Iterable): Boolean { var removed = false - for (renderable in renderables) removed = removed or this.renderables.remove(renderable) + for (renderable in renderables) { + removed = removed or this.renderables.remove(renderable) + batchRenderers[renderable::class]?.removeRenderable(renderable) + } return removed } - fun clearRenderables() { renderables.clear() } + fun clearRenderables() { + renderables.clear() + for(batchRenderer in batchRenderers.values) { + batchRenderer.clear() + } + } override fun iterator() = renderables.iterator() @@ -93,5 +115,15 @@ open class RenderableLayer @JvmOverloads constructor(displayName: String? = null // Keep going. Draw the remaining renderables. } } + for(batchRenderer in batchRenderers.values) { + try { + batchRenderer.render(rc) + } catch (e: Exception) { + logMessage( + ERROR, "RenderableLayer", "doRender", + "Exception while rendering batches", e + ) + } + } } } \ No newline at end of file diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/render/BatchRenderer.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/render/BatchRenderer.kt new file mode 100644 index 000000000..28807dbac --- /dev/null +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/render/BatchRenderer.kt @@ -0,0 +1,13 @@ +package earth.worldwind.render + +interface BatchRenderer { + + /* return true if shape was batched */ + fun addOrUpdateRenderable(renderable: Renderable) : Boolean + + fun removeRenderable(renderable : Renderable) + + fun render(rc : RenderContext) + + fun clear() +} \ No newline at end of file diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/render/BatchedPaths.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/render/BatchedPaths.kt new file mode 100644 index 000000000..59fb66c05 --- /dev/null +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/render/BatchedPaths.kt @@ -0,0 +1,45 @@ +package earth.worldwind.render + +import earth.worldwind.shape.PathSet +import earth.worldwind.shape.LineSetAttributes +import earth.worldwind.shape.Path + +class BatchedPaths(private val attributes: LineSetAttributes) { + private val batches = mutableListOf() + private val freeBatches = + mutableListOf() // duplicate batches that aren't full here + private val pathToBatch = mutableMapOf() + + fun addPath(path: Path) { + if (freeBatches.isEmpty()) { + val newBatch = PathSet(attributes) + newBatch.addPath(path) + pathToBatch[path] = newBatch + + batches.add(newBatch) + freeBatches.add(newBatch) + } else { + val freeBatch = freeBatches[0] + freeBatch.addPath(path) + pathToBatch[path] = freeBatch + + if (freeBatch.isFull()) freeBatches.remove(freeBatch) + } + } + + fun removePath(path: Path) { + val batch = pathToBatch[path] ?: return + if (batch.removePath(path) && !freeBatches.contains(batch)) freeBatches.add(batch) + pathToBatch.remove(path) + } + + fun render(rc: RenderContext) { + for (batch in batches) { + batch.render(rc) + } + } + + fun isAttributesEqual(other : LineSetAttributes) : Boolean{ + return attributes == other + } +} \ No newline at end of file diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/render/Color.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/render/Color.kt index afd0d8843..633986b21 100644 --- a/worldwind/src/commonMain/kotlin/earth/worldwind/render/Color.kt +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/render/Color.kt @@ -151,6 +151,21 @@ open class Color @JvmOverloads constructor( return argb(a8, r8, g8, b8) } + /** + * Returns this color's components as a color int. Color ints are stored as packed ints as follows: `(red << + * 0) | (green << 8) | (blue << 16) | (alpha << 24)`. Each component is an 8 bit number between 0 and 255 with 0 + * indicating the component's intensity. + * + * @return this color converted to a color int + */ + fun toColorIntRGBA(): Int { + val r8 = (red * 0xFF).roundToInt() + val g8 = (green * 0xFF).roundToInt() + val b8 = (blue * 0xFF).roundToInt() + val a8 = (alpha * 0xFF).roundToInt() + return rgba(a8, r8, g8, b8) + } + /** * Convert the argb color to its HSV components. * hsv[0] is Hue [0 .. 360) @@ -369,5 +384,17 @@ open class Color @JvmOverloads constructor( * @param blue Blue component \([0..255]\) of the color */ private fun argb(alpha: Int, red: Int, green: Int, blue: Int) = alpha shl 24 or (red shl 16) or (green shl 8) or blue + + /** + * Return a color-int from alpha, red, green, blue components. + * These component values should be \([0..255]\), but there is no + * range check performed, so if they are out of range, the + * returned color is undefined. + * @param alpha Alpha component \([0..255]\) of the color + * @param red Red component \([0..255]\) of the color + * @param green Green component \([0..255]\) of the color + * @param blue Blue component \([0..255]\) of the color + */ + private fun rgba(alpha: Int, red: Int, green: Int, blue: Int) = red or (green shl 8) or (blue shl 16) or (alpha shl 24) } } \ No newline at end of file diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/render/PathBatchRenderer.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/render/PathBatchRenderer.kt new file mode 100644 index 000000000..a51260d46 --- /dev/null +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/render/PathBatchRenderer.kt @@ -0,0 +1,81 @@ +package earth.worldwind.render + +import earth.worldwind.shape.LineSetAttributes +import earth.worldwind.shape.Path + +class PathBatchRenderer : BatchRenderer { + + private val batchedLines = mutableListOf() + private val attributesToBatchedPaths = mutableMapOf() + private val pathToBatchedPaths = mutableMapOf() + + private fun addPathToBatch(path: Path) { + val pathAttributes = LineSetAttributes(path) + var batch = attributesToBatchedPaths[pathAttributes] + if(batch == null) { + batch = BatchedPaths(pathAttributes) + batchedLines.add(batch) + attributesToBatchedPaths[pathAttributes] = batch + } + batch.addPath(path) + pathToBatchedPaths[path] = batch + } + + private fun removePathFromBatch(path : Path) { + val currentBatch = pathToBatchedPaths[path] ?: return + currentBatch.removePath(path) + pathToBatchedPaths.remove(path) + } + + private fun updatePath(path: Path) { + val pathAttributes = LineSetAttributes(path) + val currentBatch = pathToBatchedPaths[path] ?: return + if (!currentBatch.isAttributesEqual(pathAttributes)) { + removePathFromBatch(path) + addPathToBatch(path) + } + } + + /* return true if shape was batched */ + override fun addOrUpdateRenderable(renderable: Renderable) : Boolean { + if(renderable !is Path) + return false + + val pathWasBatched = pathToBatchedPaths[renderable] != null + if (!pathWasBatched) { + renderable.reset() + addPathToBatch(renderable) + } else if (pathWasBatched) { + updatePath(renderable) + } + + return true + } + + override fun removeRenderable(renderable : Renderable) { + if(renderable !is Path) + return + + removePathFromBatch(renderable) + } + + override fun render(rc : RenderContext) { + // Remove shapes that wasn't requested to be drawn this frame + val pathsToRemove = pathToBatchedPaths.keys.filter { it.lastRequestedFrameIndex != rc.frameIndex } + for(path in pathsToRemove) { + if(path.lastRequestedFrameIndex != rc.frameIndex) { + removePathFromBatch(path) + } + } + + for (batch in batchedLines) { + batch.render(rc) + } + } + + override fun clear() { + batchedLines.clear() + attributesToBatchedPaths.clear() + pathToBatchedPaths.clear() + } +} \ No newline at end of file diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/render/RenderContext.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/render/RenderContext.kt index aaa0b8198..dcaa98562 100644 --- a/worldwind/src/commonMain/kotlin/earth/worldwind/render/RenderContext.kt +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/render/RenderContext.kt @@ -37,6 +37,7 @@ open class RenderContext { lateinit var currentLayer: Layer lateinit var camera: Camera lateinit var renderResourceCache: RenderResourceCache + var frameIndex = 0L var densityFactor = 1f var verticalExaggeration = 1.0 var horizonDistance = 0.0 @@ -73,6 +74,7 @@ open class RenderContext { val tessellator: GLUtessellator by lazy { GLU.gluNewTess() } open fun reset() { + frameIndex = 0 densityFactor = 1f verticalExaggeration = 1.0 horizonDistance = 0.0 @@ -370,6 +372,12 @@ open class RenderContext { return pickedObjectId } + fun reservePickedObjectIdRange(range : Int = 1): Int { + pickedObjectId += range + if (pickedObjectId > MAX_PICKED_OBJECT_ID) pickedObjectId = range + return pickedObjectId - range + 1 + } + @Suppress("UNCHECKED_CAST") fun getUserProperty(key: Any) = userProperties[key] as? T diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/render/VertexState.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/render/VertexState.kt new file mode 100644 index 000000000..c2853d4cd --- /dev/null +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/render/VertexState.kt @@ -0,0 +1,63 @@ +package earth.worldwind.render + +import earth.worldwind.draw.DrawContext +import earth.worldwind.render.buffer.AbstractBufferObject +import earth.worldwind.util.kgl.GL_FLOAT + +open class VertexState { + internal class VertexAttrib constructor( + val index: Int = 0, + val vertexBuffer: AbstractBufferObject, + val size: Int = 0, + val type: Int = GL_FLOAT, + val normalized: Boolean = false, + val stride: Int = 0, + val offset: Int = 0 + ) + + private var attributes = mutableListOf() + + fun reset() { + attributes.clear() + } + + fun addAttribute( + index: Int, + vertexBuffer: AbstractBufferObject, + size: Int, + type: Int, + normalized: Boolean, + stride: Int, + offset: Int + ) { + attributes.add(VertexAttrib(index, vertexBuffer, size, type, normalized, stride, offset)) + } + + fun bind(dc: DrawContext): Boolean { + var bindSuccessful = true + for (vertexAttrib in attributes) { + bindSuccessful = vertexAttrib.vertexBuffer.bindBuffer(dc) + if (bindSuccessful) { + dc.gl.enableVertexAttribArray(vertexAttrib.index) + dc.gl.vertexAttribPointer( + vertexAttrib.index, + vertexAttrib.size, + vertexAttrib.type, + vertexAttrib.normalized, + vertexAttrib.stride, + vertexAttrib.offset + ) + } else { + break + } + } + return bindSuccessful + } + + fun unbind(dc: DrawContext) { + for (vertexAttrib in attributes) { + if (vertexAttrib.index == 0) continue // skip 0 for now as it's always enabled and disabling it here will cause lots of headache + dc.gl.disableVertexAttribArray(vertexAttrib.index) + } + } +} \ No newline at end of file diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/render/program/TriangleShaderProgram.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/render/program/TriangleShaderProgram.kt index 692ef5da8..eb5e1a693 100644 --- a/worldwind/src/commonMain/kotlin/earth/worldwind/render/program/TriangleShaderProgram.kt +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/render/program/TriangleShaderProgram.kt @@ -9,31 +9,46 @@ import earth.worldwind.util.kgl.KglUniformLocation open class TriangleShaderProgram : AbstractShaderProgram() { override var programSources = arrayOf( """ + #version 300 es + uniform mat4 mvpMatrix; uniform float lineWidth; uniform float invMiterLengthCutoff; uniform vec2 screen; uniform bool enableTexture; - uniform bool enableOneVertexMode; + uniform bool enableLinesMode; + uniform bool enableVertexColorAndWidth; uniform mat3 texCoordMatrix; + uniform vec4 color; + uniform int pickIdOffset; + uniform bool enablePickMode; - attribute vec4 pointA; - attribute vec4 pointB; - attribute vec4 pointC; - attribute vec2 vertexTexCoord; + in vec4 pointA; + in vec4 pointB; + in vec4 pointC; + in vec2 vertexTexCoord; + in vec4 vertexColor; + in float vertexLineWidth; - varying vec2 texCoord; + out vec2 texCoord; + out vec4 outVertexColor; void main() { - if (enableOneVertexMode) { + if (!enableLinesMode) { /* Transform the vertex position by the modelview-projection matrix. */ gl_Position = mvpMatrix * vec4(pointA.xyz, 1.0); + + /* Transform the vertex tex coord by the tex coord matrix. */ + if (enableTexture) { + texCoord = (texCoordMatrix * vec3(vertexTexCoord, 1.0)).st; + } } else { /* Transform the vertex position by the modelview-projection matrix. */ vec4 pointAScreen = mvpMatrix * vec4(pointA.xyz, 1); vec4 pointBScreen = mvpMatrix * vec4(pointB.xyz, 1); vec4 pointCScreen = mvpMatrix * vec4(pointC.xyz, 1); float corner = pointB.w; + float useLineWidth = enableVertexColorAndWidth ? vertexLineWidth : lineWidth; pointAScreen = pointAScreen / pointAScreen.w; pointBScreen = pointBScreen / pointBScreen.w; @@ -61,49 +76,67 @@ open class TriangleShaderProgram : AbstractShaderProgram() { float miterLength = 1.0 / max(dot(miter, normalA), invMiterLengthCutoff); gl_Position = pointBScreen; - gl_Position.xy = gl_Position.xy + (corner * miter * lineWidth * miterLength) / screen.xy; + gl_Position.xy = gl_Position.xy + (corner * miter * useLineWidth * miterLength) / screen.xy; + + /* Transform the vertex tex coord by the tex coord matrix. */ + if (enableTexture) { + texCoord = (texCoordMatrix * vec3(vertexTexCoord.x, 0.0, 1.0)).st; + } } - - /* Transform the vertex tex coord by the tex coord matrix. */ - if (enableTexture) { - texCoord = (texCoordMatrix * vec3(vertexTexCoord, 1.0)).st; + + outVertexColor = enableVertexColorAndWidth ? vertexColor : color; + if(enablePickMode && pickIdOffset != 0) { + ivec3 colorAsInt = ivec3(int(outVertexColor.r * 255.0), int(outVertexColor.g * 255.0), int(outVertexColor.b * 255.0)); + int colorId = (colorAsInt.r << 16 | colorAsInt.g << 8 | colorAsInt.b); + int resultId = pickIdOffset + colorId; + + outVertexColor.r = float((resultId >> 16) & 0xFF) / 255.0; + outVertexColor.g = float((resultId >> 8) & 0xFF) / 255.0; + outVertexColor.b = float(resultId & 0xFF) / 255.0; + outVertexColor.a = 1.0; } } """.trimIndent(), """ + #version 300 es precision mediump float; uniform bool enablePickMode; uniform bool enableTexture; - uniform vec4 color; + uniform bool enableLinesMode; uniform float opacity; uniform sampler2D texSampler; - varying vec2 texCoord; + in vec2 texCoord; + in vec4 outVertexColor; + + out vec4 fragColor; void main() { if (enablePickMode && enableTexture) { /* Modulate the RGBA color with the 2D texture's Alpha component (rounded to 0.0 or 1.0). */ - float texMask = floor(texture2D(texSampler, texCoord).a + 0.5); - gl_FragColor = color * texMask; + float texMask = floor(texture(texSampler, texCoord).a + 0.5); + fragColor = outVertexColor * texMask; } else if (!enablePickMode && enableTexture) { /* Modulate the RGBA color with the 2D texture's RGBA color. */ - gl_FragColor = color * texture2D(texSampler, texCoord) * opacity; + fragColor = outVertexColor * texture(texSampler, texCoord) * opacity; } else { /* Return the RGBA color as-is. */ - gl_FragColor = color * opacity; + fragColor = outVertexColor * opacity; } } """.trimIndent() ) - override val attribBindings = arrayOf("pointA", "pointB", "pointC", "vertexTexCoord") + override val attribBindings = arrayOf("pointA", "pointB", "pointC", "vertexTexCoord", "vertexColor", "vertexLineWidth") protected var enablePickMode = false protected var enableTexture = false - protected var enableOneVertexMode = false + protected var enableLinesMode = false + protected var enableVertexColorAndWidth = false protected val mvpMatrix = Matrix4() protected val texCoordMatrix = Matrix3() protected val color = Color() + protected var pickIdOffset = 0 protected var opacity = 1.0f protected var lineWidth = 1.0f protected var invMiterLengthCutoff = 1.0f @@ -112,13 +145,15 @@ open class TriangleShaderProgram : AbstractShaderProgram() { protected var mvpMatrixId = KglUniformLocation.NONE protected var colorId = KglUniformLocation.NONE + protected var pickIdOffsetId = KglUniformLocation.NONE protected var opacityId = KglUniformLocation.NONE protected var lineWidthId = KglUniformLocation.NONE protected var invMiterLengthCutoffId = KglUniformLocation.NONE protected var screenId = KglUniformLocation.NONE protected var enablePickModeId = KglUniformLocation.NONE protected var enableTextureId = KglUniformLocation.NONE - protected var enableOneVertexModeId = KglUniformLocation.NONE + protected var enableLinesModeId = KglUniformLocation.NONE + protected var enableVertexColorAndWidthId = KglUniformLocation.NONE protected var texCoordMatrixId = KglUniformLocation.NONE protected var texSamplerId = KglUniformLocation.NONE private val array = FloatArray(16) @@ -131,6 +166,8 @@ open class TriangleShaderProgram : AbstractShaderProgram() { colorId = gl.getUniformLocation(program, "color") val alpha = color.alpha gl.uniform4f(colorId, color.red * alpha, color.green * alpha, color.blue * alpha, alpha) + pickIdOffsetId = gl.getUniformLocation(program, "pickIdOffset") + gl.uniform1i(pickIdOffsetId, pickIdOffset) opacityId = gl.getUniformLocation(program, "opacity") gl.uniform1f(opacityId, opacity) @@ -145,8 +182,10 @@ open class TriangleShaderProgram : AbstractShaderProgram() { gl.uniform1i(enablePickModeId, if (enablePickMode) 1 else 0) enableTextureId = gl.getUniformLocation(program, "enableTexture") gl.uniform1i(enableTextureId, if (enableTexture) 1 else 0) - enableOneVertexModeId = gl.getUniformLocation(program, "enableOneVertexMode") - gl.uniform1i(enableOneVertexModeId, if (enableOneVertexMode) 1 else 0) + enableLinesModeId = gl.getUniformLocation(program, "enableLinesMode") + gl.uniform1i(enableLinesModeId, if (enableLinesMode) 1 else 0) + enableVertexColorAndWidthId = gl.getUniformLocation(program, "enableVertexColorAndWidth") + gl.uniform1i(enableVertexColorAndWidthId, if (enableVertexColorAndWidth) 1 else 0) texCoordMatrixId = gl.getUniformLocation(program, "texCoordMatrix") texCoordMatrix.transposeToArray(array, 0) // 3 x 3 identity matrix @@ -167,10 +206,16 @@ open class TriangleShaderProgram : AbstractShaderProgram() { gl.uniform1i(enableTextureId, if (enable) 1 else 0) } } - fun enableOneVertexMode(enable: Boolean) { - if (enableOneVertexMode != enable) { - enableOneVertexMode = enable - gl.uniform1i(enableOneVertexModeId, if (enable) 1 else 0) + fun enableLinesMode(enable: Boolean) { + if (enableLinesMode != enable) { + enableLinesMode = enable + gl.uniform1i(enableLinesModeId, if (enable) 1 else 0) + } + } + fun enableVertexColorAndWidth(enable: Boolean) { + if (enableVertexColorAndWidth != enable) { + enableVertexColorAndWidth = enable + gl.uniform1i(enableVertexColorAndWidthId, if (enable) 1 else 0) } } fun loadTexCoordMatrix(matrix: Matrix3) { @@ -194,6 +239,13 @@ open class TriangleShaderProgram : AbstractShaderProgram() { } } + fun loadPickIdOffset(pickIdOffset: Int) { + if (this.pickIdOffset != pickIdOffset) { + this.pickIdOffset = pickIdOffset + gl.uniform1i(pickIdOffsetId, pickIdOffset) + } + } + fun loadOpacity(opacity: Float) { if (this.opacity != opacity) { this.opacity = opacity diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/shape/AbstractShape.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/shape/AbstractShape.kt index a9a943c8a..519028164 100644 --- a/worldwind/src/commonMain/kotlin/earth/worldwind/shape/AbstractShape.kt +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/shape/AbstractShape.kt @@ -4,6 +4,7 @@ import earth.worldwind.PickedObject import earth.worldwind.geom.* import earth.worldwind.geom.Angle.Companion.degrees import earth.worldwind.globe.Globe +import earth.worldwind.layer.RenderableLayer import earth.worldwind.render.AbstractRenderable import earth.worldwind.render.Color import earth.worldwind.render.RenderContext @@ -44,14 +45,37 @@ abstract class AbstractShape(override var attributes: ShapeAttributes): Abstract override var highlightAttributes: ShapeAttributes? = null override var isHighlighted = false var maximumIntermediatePoints = 10 - protected lateinit var activeAttributes: ShapeAttributes - protected var isSurfaceShape = false + lateinit var activeAttributes: ShapeAttributes + protected set + var isSurfaceShape = false + protected set protected var lastGlobeState: Globe.State? = null protected var pickedObjectId = 0 protected val pickColor = Color() protected val boundingSector = Sector() protected val boundingBox = BoundingBox() private val scratchPoint = Vec3() + private var activeAttributesHash = 0 + open var allowBatching = false + var forceRecreateBatch = false + var lastRequestedFrameIndex = 0L + + open fun addToBatch(rc : RenderContext) : Boolean { + // TODO: Remove RenderableLayer check via extending batching across any layer + return rc.currentLayer is RenderableLayer && allowBatching && !isHighlighted + } + + private fun updateAttributes(rc: RenderContext) { + // Select the currently active attributes. Don't render anything if the attributes are unspecified. + determineActiveAttributes(rc) + + forceRecreateBatch = forceRecreateBatch || (activeAttributesHash != activeAttributes.hashCode()) + + activeAttributesHash = activeAttributes.hashCode() + + // Determine whether the shape geometry must be assembled as Cartesian geometry or as geographic geometry. + isSurfaceShape = rc.globe.is2D || altitudeMode == AltitudeMode.CLAMP_TO_GROUND && isFollowTerrain + } override fun doRender(rc: RenderContext) { checkGlobeState(rc) @@ -60,8 +84,15 @@ abstract class AbstractShape(override var attributes: ShapeAttributes): Abstract // Don't render anything if the shape is not visible. if (!intersectsFrustum(rc)) return - // Select the currently active attributes. Don't render anything if the attributes are unspecified. - determineActiveAttributes(rc) + lastRequestedFrameIndex = rc.frameIndex + + // Update attributes that are dependent on render context. + updateAttributes(rc) + + // Call function that implements batching and skip next steps, because they're only relevant for single object rendering + if(addToBatch(rc)) { + return + } // Keep track of the drawable count to determine whether this shape has enqueued drawables. val drawableCount = rc.drawableCount @@ -70,9 +101,6 @@ abstract class AbstractShape(override var attributes: ShapeAttributes): Abstract PickedObject.identifierToUniqueColor(pickedObjectId, pickColor) } - // Determine whether the shape geometry must be assembled as Cartesian geometry or as geographic geometry. - isSurfaceShape = rc.globe.is2D || altitudeMode == AltitudeMode.CLAMP_TO_GROUND && isFollowTerrain - // Enqueue drawables for processing on the OpenGL thread. makeDrawable(rc) @@ -80,6 +108,9 @@ abstract class AbstractShape(override var attributes: ShapeAttributes): Abstract if (rc.isPickMode && rc.drawableCount != drawableCount) { rc.offerPickedObject(PickedObject.fromRenderable(pickedObjectId, this, rc.currentLayer)) } + + // Set to false if it was set to true as doRender is only called for nonBatched shapes. + forceRecreateBatch = false } /** @@ -145,9 +176,10 @@ abstract class AbstractShape(override var attributes: ShapeAttributes): Abstract } } - protected open fun reset() { + open fun reset() { boundingBox.setToUnitBox() boundingSector.setEmpty() + forceRecreateBatch = true } protected abstract fun makeDrawable(rc: RenderContext) diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/shape/Boundable.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/shape/Boundable.kt new file mode 100644 index 000000000..854c939ac --- /dev/null +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/shape/Boundable.kt @@ -0,0 +1,61 @@ +package earth.worldwind.shape + +import earth.worldwind.geom.AltitudeMode +import earth.worldwind.geom.Angle.Companion.degrees +import earth.worldwind.geom.BoundingBox +import earth.worldwind.geom.Sector +import earth.worldwind.geom.Vec3 +import earth.worldwind.render.RenderContext +import kotlin.math.sqrt + +// Taken from AbstractShape +// TODO use class magic to refactor and use this interface in AbstractShape +interface Boundable { + val boundingSector : Sector + val boundingBox : BoundingBox + val scratchPoint : Vec3 + + /** + * Indicates whether this shape is within the current globe's projection limits. Subclasses may implement + * this method to perform the test. The default implementation returns true. + * @param rc The current render context. + * @returns true if this shape is within or intersects the current globe's projection limits, otherwise false. + */ + fun isWithinProjectionLimits(rc: RenderContext) = true + + fun intersectsFrustum(rc: RenderContext) = + (boundingBox.isUnitBox || boundingBox.intersectsFrustum(rc.frustum)) && + // This is a temporary solution. Surface shapes should also use bounding box. + (boundingSector.isEmpty || boundingSector.intersects(rc.terrain.sector)) + + fun cameraDistanceGeographic(rc: RenderContext, boundingSector: Sector): Double { + val lat = rc.camera.position.latitude.inDegrees.coerceIn( + boundingSector.minLatitude.inDegrees, + boundingSector.maxLatitude.inDegrees + ) + val lon = rc.camera.position.longitude.inDegrees.coerceIn( + boundingSector.minLongitude.inDegrees, + boundingSector.maxLongitude.inDegrees + ) + val point = rc.geographicToCartesian(lat.degrees, lon.degrees, 0.0, AltitudeMode.CLAMP_TO_GROUND, scratchPoint) + return point.distanceTo(rc.cameraPoint) + } + + fun cameraDistanceCartesian(rc: RenderContext, array: FloatArray, count: Int, stride: Int, offset: Vec3): Double { + val cx = rc.cameraPoint.x - offset.x + val cy = rc.cameraPoint.y - offset.y + val cz = rc.cameraPoint.z - offset.z + var minDistance2 = Double.POSITIVE_INFINITY + for (idx in 0 until count step stride) { + val px = array[idx] + val py = array[idx + 1] + val pz = array[idx + 2] + val dx = px - cx + val dy = py - cy + val dz = pz - cz + val distance2 = dx * dx + dy * dy + dz * dz + if (minDistance2 > distance2) minDistance2 = distance2 + } + return sqrt(minDistance2) + } +} diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/shape/Ellipse.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/shape/Ellipse.kt index f0448ca32..20506c617 100644 --- a/worldwind/src/commonMain/kotlin/earth/worldwind/shape/Ellipse.kt +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/shape/Ellipse.kt @@ -288,7 +288,9 @@ open class Ellipse @JvmOverloads constructor( drawState.program = rc.getShaderProgram { TriangleShaderProgram() } // Assemble the drawable's OpenGL vertex buffer object. - drawState.vertexBuffer = rc.getBufferObject(vertexBufferKey) { FloatBufferObject(GL_ARRAY_BUFFER, vertexArray) } + val vertexBuffer = rc.getBufferObject(vertexBufferKey) { FloatBufferObject(GL_ARRAY_BUFFER, vertexArray) } + drawState.vertexState.addAttribute(0, vertexBuffer, 4, GL_FLOAT, false, VERTEX_STRIDE * 4, 0) + drawState.vertexState.addAttribute(3, vertexBuffer, 2, GL_FLOAT, false, VERTEX_STRIDE * 4,12) // Get the attributes of the element buffer val elementBufferKey = elementBufferKeys[activeIntervals] ?: Any().also { elementBufferKeys[activeIntervals] = it } @@ -300,7 +302,11 @@ open class Ellipse @JvmOverloads constructor( drawStateLines.program = rc.getShaderProgram { TriangleShaderProgram() } // Assemble the drawable's OpenGL vertex buffer object. - drawStateLines.vertexBuffer = rc.getBufferObject(lineVertexBufferKey) { FloatBufferObject(GL_ARRAY_BUFFER, lineVertexArray) } + val lineVertexBuffer = rc.getBufferObject(lineVertexBufferKey) { FloatBufferObject(GL_ARRAY_BUFFER, lineVertexArray) } + drawStateLines.vertexState.addAttribute(0, lineVertexBuffer, 4, GL_FLOAT, false, 20, 0) + drawStateLines.vertexState.addAttribute(1, lineVertexBuffer, 4, GL_FLOAT, false, 20, 40) + drawStateLines.vertexState.addAttribute(2, lineVertexBuffer, 4, GL_FLOAT, false, 20, 80) + drawStateLines.vertexState.addAttribute(3, lineVertexBuffer, 1, GL_FLOAT, false, 20, 56) // Assemble the drawable's OpenGL element buffer object. drawStateLines.elementBuffer = rc.getBufferObject(lineElementBufferKey) { @@ -353,7 +359,6 @@ open class Ellipse @JvmOverloads constructor( // Configure the drawable to display the shape's interior. drawState.color(if (rc.isPickMode) pickColor else activeAttributes.interiorColor) drawState.opacity(if (rc.isPickMode) 1f else rc.currentLayer.opacity) - drawState.texCoordAttrib(2 /*size*/, 12 /*offset in bytes*/) val top = drawState.elementBuffer!!.ranges[TOP_RANGE]!! drawState.drawElements(GL_TRIANGLE_STRIP, top.length, GL_UNSIGNED_SHORT, top.lower * 2 /*offset*/) if (isExtrude) { @@ -516,11 +521,13 @@ open class Ellipse @JvmOverloads constructor( lineVertexArray[lineVertexIndex++] = (altitude - vertexOrigin.z).toFloat() lineVertexArray[lineVertexIndex++] = 1.0f lineVertexArray[lineVertexIndex++] = texCoord1d.toFloat() + lineVertexArray[lineVertexIndex++] = (longitude.inDegrees - vertexOrigin.x).toFloat() lineVertexArray[lineVertexIndex++] = (latitude.inDegrees - vertexOrigin.y).toFloat() lineVertexArray[lineVertexIndex++] = (altitude - vertexOrigin.z).toFloat() lineVertexArray[lineVertexIndex++] = -1.0f lineVertexArray[lineVertexIndex++] = texCoord1d.toFloat() + if (!firstOrLast) { outlineElements.add(vertex) outlineElements.add(vertex.inc()) @@ -531,11 +538,13 @@ open class Ellipse @JvmOverloads constructor( lineVertexArray[lineVertexIndex++] = (point.z - vertexOrigin.z).toFloat() lineVertexArray[lineVertexIndex++] = 1.0f lineVertexArray[lineVertexIndex++] = texCoord1d.toFloat() + lineVertexArray[lineVertexIndex++] = (point.x - vertexOrigin.x).toFloat() lineVertexArray[lineVertexIndex++] = (point.y - vertexOrigin.y).toFloat() lineVertexArray[lineVertexIndex++] = (point.z - vertexOrigin.z).toFloat() lineVertexArray[lineVertexIndex++] = -1.0f lineVertexArray[lineVertexIndex++] = texCoord1d.toFloat() + if (!firstOrLast) { outlineElements.add(vertex) outlineElements.add(vertex.inc()) diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/shape/Path.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/shape/Path.kt index aff5c9ed8..db87d6127 100644 --- a/worldwind/src/commonMain/kotlin/earth/worldwind/shape/Path.kt +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/shape/Path.kt @@ -5,6 +5,7 @@ import earth.worldwind.draw.Drawable import earth.worldwind.draw.DrawableShape import earth.worldwind.draw.DrawableSurfaceShape import earth.worldwind.geom.* +import earth.worldwind.layer.RenderableLayer import earth.worldwind.render.* import earth.worldwind.render.buffer.FloatBufferObject import earth.worldwind.render.buffer.IntBufferObject @@ -41,6 +42,7 @@ open class Path @JvmOverloads constructor( private val prevPoint = Vec3() private val texCoordMatrix = Matrix3() private val intermediateLocation = Location() + override var allowBatching = true companion object { protected const val VERTEX_STRIDE = 10 @@ -52,6 +54,25 @@ open class Path @JvmOverloads constructor( protected fun nextCacheKey() = Any() } + override fun addToBatch(rc : RenderContext) : Boolean { + // Can't batch extrude as it uses different shader and geometry + val canBeBatched = super.addToBatch(rc) && (!isExtrude || isSurfaceShape) + + val layer = rc.currentLayer + if(layer is RenderableLayer) { + val renderer = layer.batchRenderers.getOrPut(Path::class) { PathBatchRenderer() } + + if (!canBeBatched) { + renderer.removeRenderable(this) + return false + } + + return renderer.addOrUpdateRenderable(this) + } + + return false + } + override fun reset() { super.reset() vertexArray = FloatArray(0) @@ -94,9 +115,11 @@ open class Path @JvmOverloads constructor( drawState.program = rc.getShaderProgram { TriangleShaderProgram() } // Assemble the drawable's OpenGL vertex buffer object. - drawState.vertexBuffer = rc.getBufferObject(vertexBufferKey) { - FloatBufferObject(GL_ARRAY_BUFFER, vertexArray, vertexArray.size) - } + val vertexBuffer = rc.getBufferObject(vertexBufferKey) { FloatBufferObject(GL_ARRAY_BUFFER, vertexArray) } + drawState.vertexState.addAttribute(0, vertexBuffer, 4, GL_FLOAT, false, 20, 0) + drawState.vertexState.addAttribute(1, vertexBuffer, 4, GL_FLOAT, false, 20, 40) + drawState.vertexState.addAttribute(2, vertexBuffer, 4, GL_FLOAT, false, 20, 80) + drawState.vertexState.addAttribute(3, vertexBuffer, 1, GL_FLOAT, false, 20, 56) // Assemble the drawable's OpenGL element buffer object. drawState.elementBuffer = rc.getBufferObject(elementBufferKey) { @@ -259,6 +282,7 @@ open class Path @JvmOverloads constructor( texCoord1d += point.distanceTo(prevPoint) } prevPoint.copy(point) + if (isSurfaceShape) { vertexArray[vertexIndex++] = (longitude.inDegrees - vertexOrigin.x).toFloat() vertexArray[vertexIndex++] = (latitude.inDegrees - vertexOrigin.y).toFloat() @@ -271,6 +295,7 @@ open class Path @JvmOverloads constructor( vertexArray[vertexIndex++] = (altitude - vertexOrigin.z).toFloat() vertexArray[vertexIndex++] = -1.0f vertexArray[vertexIndex++] = texCoord1d.toFloat() + if (!firstOrLast) { outlineElements.add(vertex) outlineElements.add(vertex.inc()) @@ -281,6 +306,7 @@ open class Path @JvmOverloads constructor( vertexArray[vertexIndex++] = (point.z - vertexOrigin.z).toFloat() vertexArray[vertexIndex++] = 1.0f vertexArray[vertexIndex++] = texCoord1d.toFloat() + vertexArray[vertexIndex++] = (point.x - vertexOrigin.x).toFloat() vertexArray[vertexIndex++] = (point.y - vertexOrigin.y).toFloat() vertexArray[vertexIndex++] = (point.z - vertexOrigin.z).toFloat() diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/shape/PathSet.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/shape/PathSet.kt new file mode 100644 index 000000000..45f20ca3c --- /dev/null +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/shape/PathSet.kt @@ -0,0 +1,415 @@ +package earth.worldwind.shape + +import earth.worldwind.PickedObject +import earth.worldwind.draw.DrawShapeState +import earth.worldwind.draw.Drawable +import earth.worldwind.draw.DrawableShape +import earth.worldwind.draw.DrawableSurfaceShape +import earth.worldwind.geom.* +import earth.worldwind.render.* +import earth.worldwind.render.buffer.FloatBufferObject +import earth.worldwind.render.buffer.IntBufferObject +import earth.worldwind.render.image.ImageOptions +import earth.worldwind.render.image.ImageSource +import earth.worldwind.render.image.ResamplingMode +import earth.worldwind.render.image.WrapMode +import earth.worldwind.render.program.TriangleShaderProgram +import earth.worldwind.shape.PathType.* +import earth.worldwind.util.kgl.* + +open class LineSetAttributes(path : Path) { + val isSurfaceShape: Boolean = path.isSurfaceShape + val enableDepthTest: Boolean = path.activeAttributes.isDepthTest + val enableDepthWrite: Boolean = path.activeAttributes.isDepthWrite + val outlineImageSource: ImageSource? = path.activeAttributes.outlineImageSource + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is LineSetAttributes) return false + return this.isSurfaceShape == other.isSurfaceShape && this.enableDepthTest == other.enableDepthTest && this.enableDepthWrite == other.enableDepthWrite + } + + override fun hashCode(): Int { + var result = isSurfaceShape.hashCode() + result = 31 * result + enableDepthTest.hashCode() + result = 31 * result + enableDepthWrite.hashCode() + result = 31 * result + (outlineImageSource?.hashCode() ?: 0) + return result + } +} + +open class PathSet(private val attributes: LineSetAttributes): Boundable { + + protected var vertexArray = FloatArray(0) + protected var colorArray = IntArray(0) + protected var pickColorArray = IntArray(0) + protected var widthArray = FloatArray(0) + protected var vertexIndex = 0 + protected var colorWidthIndex = 0 + protected val outlineElements = mutableListOf() + protected val paths: Array = arrayOfNulls(MAX_PATHS) + protected var pathCount: Int = 0 + protected lateinit var vertexBufferKey: Any + protected lateinit var colorBufferKey: Any + protected lateinit var pickColorBufferKey: Any + protected lateinit var widthBufferKey: Any + protected lateinit var elementBufferKey: Any + protected val vertexOrigin = Vec3() + protected var texCoord1d = 0.0 + private val point = Vec3() + private val prevPoint = Vec3() + private val intermediateLocation = Location() + private val texCoordMatrix = Matrix3() + + override val boundingSector = Sector() + override val boundingBox = BoundingBox() + override val scratchPoint = Vec3() + + companion object { + protected const val MAX_PATHS = 256 + protected const val VERTEX_STRIDE = 10 + const val NEAR_ZERO_THRESHOLD = 1.0e-10 + protected val defaultOutlineImageOptions = ImageOptions().apply { + resamplingMode = ResamplingMode.NEAREST_NEIGHBOR + wrapMode = WrapMode.REPEAT + } + + protected fun nextCacheKey() = Any() + } + + fun isFull() : Boolean + { + return pathCount == MAX_PATHS + } + + fun addPath(path : Path) : Boolean { + if (isFull()) return false + paths[pathCount++] = path + reset() + return true + } + + fun removePath(path : Path) : Boolean { + if (pathCount == 0) return false + val index = paths.indexOf(path) + if (index == -1) return false + paths[index] = paths[--pathCount] + paths[pathCount] = null + reset() + return true + } + + // Override doRender here to remove pick related logic from AbstractShape, it's handled by individual lines + fun render(rc: RenderContext) { + if (!isWithinProjectionLimits(rc)) return + + // Don't render anything if the shape is not visible. + if (!intersectsFrustum(rc)) return + + // Enqueue drawables for processing on the OpenGL thread. + makeDrawable(rc) + } + + fun reset() { + boundingBox.setToUnitBox() + boundingSector.setEmpty() + vertexArray = FloatArray(0) + colorArray = IntArray(0) + pickColorArray = IntArray(0) + widthArray = FloatArray(0) + outlineElements.clear() + } + + protected open fun computeRepeatingTexCoordTransform(texture: Texture, metersPerPixel: Double, result: Matrix3): Matrix3 { + val texCoordMatrix = result.setToIdentity() + texCoordMatrix.setScale(1.0 / (texture.width * metersPerPixel), 1.0 / (texture.height * metersPerPixel)) + texCoordMatrix.multiplyByMatrix(texture.coordTransform) + return texCoordMatrix + } + + private fun makeDrawable(rc: RenderContext) { + if (pathCount == 0) return // nothing to draw + + var assembleBuffers = vertexArray.isEmpty() + val pickColorOffset = if(rc.isPickMode) rc.reservePickedObjectIdRange(pathCount) else 0 + for (idx in 0 until pathCount ) { + val path = paths[idx] ?: break + if (path.positions.isEmpty()) continue + + assembleBuffers = assembleBuffers || path.forceRecreateBatch + path.forceRecreateBatch = false + + if(rc.isPickMode) { + rc.offerPickedObject(PickedObject.fromRenderable(pickColorOffset + idx, path, rc.currentLayer)) + } + } + + // reset caches depending on flags + if (assembleBuffers) { + assembleBuffers(rc) + vertexBufferKey = nextCacheKey() + elementBufferKey = nextCacheKey() + colorBufferKey = nextCacheKey() + pickColorBufferKey = nextCacheKey() + widthBufferKey = nextCacheKey() + } + + // Obtain a drawable form the render context pool, and compute distance to the render camera. + val drawable: Drawable + val drawState: DrawShapeState + val cameraDistance: Double + if (attributes.isSurfaceShape) { + val pool = rc.getDrawablePool() + drawable = DrawableSurfaceShape.obtain(pool) + drawState = drawable.drawState + cameraDistance = cameraDistanceGeographic(rc, boundingSector) + drawable.offset = rc.globe.offset + drawable.sector.copy(boundingSector) + } else { + val pool = rc.getDrawablePool() + drawable = DrawableShape.obtain(pool) + drawState = drawable.drawState + cameraDistance = cameraDistanceCartesian( + rc, vertexArray, vertexArray.size, + VERTEX_STRIDE, vertexOrigin + ) + } + + // Convert pickColorOffset to colorOffset + if(rc.isPickMode) { + drawState.pickIdOffset = pickColorOffset + } + + // Use the basic GLSL program to draw the shape. + drawState.program = rc.getShaderProgram { TriangleShaderProgram() } + + // Assemble the drawable's OpenGL vertex buffer object. + val vertexBuffer = rc.getBufferObject(vertexBufferKey) { FloatBufferObject(GL_ARRAY_BUFFER, vertexArray) } + drawState.vertexState.addAttribute(0, vertexBuffer, 4, GL_FLOAT, false, 20, 0) // pointA + drawState.vertexState.addAttribute(1, vertexBuffer, 4, GL_FLOAT, false, 20, 40) // pointB + drawState.vertexState.addAttribute(2, vertexBuffer, 4, GL_FLOAT, false, 20, 80) // pointC + drawState.vertexState.addAttribute(3, vertexBuffer, 1, GL_FLOAT, false, 20,56) // texCoord + + val colorBuffer = rc.getBufferObject(colorBufferKey) { IntBufferObject(GL_ARRAY_BUFFER, colorArray) } + val pickColorBuffer = rc.getBufferObject(pickColorBufferKey) { IntBufferObject(GL_ARRAY_BUFFER, pickColorArray) } + drawState.vertexState.addAttribute(4, if(rc.isPickMode) pickColorBuffer else colorBuffer, 4, GL_UNSIGNED_BYTE, true, 0, 0) // color + + val widthBuffer = rc.getBufferObject(widthBufferKey) { FloatBufferObject(GL_ARRAY_BUFFER, widthArray) } + drawState.vertexState.addAttribute(5, widthBuffer, 1, GL_FLOAT, false, 0,0) // lineWidth + + // Assemble the drawable's OpenGL element buffer object. + drawState.elementBuffer = rc.getBufferObject(elementBufferKey) { + val array = IntArray(outlineElements.size) + var index = 0 + for (element in outlineElements) array[index++] = element + IntBufferObject(GL_ELEMENT_ARRAY_BUFFER, array) + } + + // Configure the drawable to use the outline texture when drawing the outline. + attributes.outlineImageSource?.let { outlineImageSource -> + rc.getTexture(outlineImageSource, defaultOutlineImageOptions)?.let { texture -> + val metersPerPixel = rc.pixelSizeAtDistance(cameraDistance) + computeRepeatingTexCoordTransform(texture, metersPerPixel, texCoordMatrix) + drawState.texture(texture) + drawState.texCoordMatrix(texCoordMatrix) + } + } + + // Configure the drawable to display the shape's extruded verticals. + drawState.opacity(if (rc.isPickMode) 1f else rc.currentLayer.opacity) + drawState.drawElements( + GL_TRIANGLES, outlineElements.size, + GL_UNSIGNED_INT, 0 + ) + + // Configure the drawable according to the shape's attributes. + drawState.isLine = true + drawState.isStatic = true + drawState.vertexOrigin.copy(vertexOrigin) + drawState.enableCullFace = false + drawState.enableDepthTest = attributes.enableDepthTest + drawState.enableDepthWrite = attributes.enableDepthWrite + + // Enqueue the drawable for processing on the OpenGL thread. + if (attributes.isSurfaceShape) rc.offerSurfaceDrawable(drawable, 0.0 /*zOrder*/) + else rc.offerShapeDrawable(drawable, cameraDistance) + } + + protected open fun assembleBuffers(rc: RenderContext) { + // Determine the number of vertexes + var vertexCount = 0 + for (idx in 0 until pathCount ) { + val path = paths[idx] ?: break // break never gonna be hit, need something better + val p = path.positions + + if (p.isEmpty()) continue + + val noIntermediatePoints = path.maximumIntermediatePoints <= 0 || path.pathType == LINEAR + + vertexCount += if (noIntermediatePoints) { + p.size + 2 + } else { + 2 + p.size + (p.size - 1) * path.maximumIntermediatePoints + } + } + + // Clear the shape's vertex array and element arrays. These arrays will accumulate values as the shapes's + // geometry is assembled. + vertexIndex = 0 + colorWidthIndex = 0 + vertexArray = FloatArray(vertexCount * VERTEX_STRIDE) + outlineElements.clear() + colorArray = IntArray(vertexCount * 2) + pickColorArray = IntArray(vertexCount * 2) + widthArray = FloatArray(vertexCount * 2) + + for (idx in 0 until pathCount ) { + val path = paths[idx] ?: break + val positions = path.positions + if (positions.isEmpty()) continue // no boundary positions to assemble + + // Get colors and width + val colorInt = path.activeAttributes.outlineColor.toColorIntRGBA() + val lineWidth = path.activeAttributes.outlineWidth + if(attributes.isSurfaceShape) 0.5f else 0f + val pickColor = Color(0.0f,0.0f,0.0f,0.0f) + PickedObject.identifierToUniqueColor(idx, pickColor) + val pickColorInt = pickColor.toColorIntRGBA() + + // Reset texCoord per path + texCoord1d = 0.0 + + // Add the first vertex. + var begin = positions[0] + addVertices(rc, begin.latitude, begin.longitude, begin.altitude, path.altitudeMode, lineWidth, colorInt, pickColorInt, addIndices = false, isFirstVertex = true) + addVertices(rc, begin.latitude, begin.longitude, begin.altitude, path.altitudeMode, lineWidth, colorInt, pickColorInt, addIndices = false, isFirstVertex = false) + // Add the remaining vertices, inserting vertices along each edge as indicated by the path's properties. + for (vertexIdx in 1 until positions.size) { + val end = positions[vertexIdx] + addIntermediateVertices(rc, begin, end, path.maximumIntermediatePoints, path.pathType, path.altitudeMode, lineWidth, colorInt, pickColorInt) + addVertices(rc, end.latitude, end.longitude, end.altitude, path.altitudeMode, lineWidth, colorInt, pickColorInt,addIndices = true, isFirstVertex = false) + begin = end + } + addVertices(rc, begin.latitude, begin.longitude, begin.altitude, path.altitudeMode, lineWidth, colorInt, pickColorInt,addIndices = false, isFirstVertex = false) + } + + // Compute the shape's bounding box or bounding sector from its assembled coordinates. + if (attributes.isSurfaceShape) { + boundingSector.setEmpty() + boundingSector.union(vertexArray, vertexIndex, VERTEX_STRIDE) + boundingSector.translate(vertexOrigin.y /*latitude*/, vertexOrigin.x /*longitude*/) + boundingBox.setToUnitBox() // Surface/geographic shape bounding box is unused + } else { + boundingBox.setToPoints(vertexArray, vertexIndex, VERTEX_STRIDE) + boundingBox.translate(vertexOrigin.x, vertexOrigin.y, vertexOrigin.z) + boundingSector.setEmpty() // Cartesian shape bounding sector is unused + } + } + + protected open fun addIntermediateVertices(rc: RenderContext, begin: Position, end: Position, maximumIntermediatePoints : Int, pathType: PathType, + altitudeMode: AltitudeMode, lineWidth : Float, colorInt : Int, pickColorInt: Int) { + if (maximumIntermediatePoints <= 0) return // suppress intermediate vertices when configured to do so + val azimuth: Angle + val length: Double + when (pathType) { + GREAT_CIRCLE -> { + azimuth = begin.greatCircleAzimuth(end) + length = begin.greatCircleDistance(end) + } + RHUMB_LINE -> { + azimuth = begin.rhumbAzimuth(end) + length = begin.rhumbDistance(end) + } + else -> return // suppress intermediate vertices when the path type is linear + } + if (length < NEAR_ZERO_THRESHOLD) return // suppress intermediate vertices when the edge length less than a millimeter (on Earth) + val numSubsegments = maximumIntermediatePoints + 1 + val deltaDist = length / numSubsegments + val deltaAlt = (end.altitude - begin.altitude) / numSubsegments + var dist = deltaDist + var alt = begin.altitude + deltaAlt + for (idx in 1 until numSubsegments) { + val loc = intermediateLocation + when (pathType) { + GREAT_CIRCLE -> begin.greatCircleLocation(azimuth, dist, loc) + RHUMB_LINE -> begin.rhumbLocation(azimuth, dist, loc) + else -> {} + } + addVertices(rc, loc.latitude, loc.longitude, alt, altitudeMode, lineWidth, colorInt, pickColorInt,true, false) + dist += deltaDist + alt += deltaAlt + } + } + + protected open fun addVertices( + rc: RenderContext, latitude: Angle, longitude: Angle, altitude: Double, altitudeMode: AltitudeMode, width : Float, colorInt : Int, pickColorInt: Int, addIndices : Boolean, isFirstVertex : Boolean + ) { + val vertex = (vertexIndex / VERTEX_STRIDE - 1) * 2 + val point = rc.geographicToCartesian(latitude, longitude, altitude, altitudeMode, point) + if (vertexIndex == 0) { + if (attributes.isSurfaceShape) vertexOrigin.set( + longitude.inDegrees, + latitude.inDegrees, + altitude + ) + else vertexOrigin.copy(point) + texCoord1d = 0.0 + } + + if(!isFirstVertex) { + texCoord1d += point.distanceTo(prevPoint) + } + prevPoint.copy(point) + + // Duplicate width and color because this function adds 2 vertices + widthArray[colorWidthIndex] = width + colorArray[colorWidthIndex] = colorInt + pickColorArray[colorWidthIndex] = pickColorInt + ++colorWidthIndex + widthArray[colorWidthIndex] = width + colorArray[colorWidthIndex] = colorInt + pickColorArray[colorWidthIndex] = pickColorInt + ++colorWidthIndex + + if (attributes.isSurfaceShape) { + vertexArray[vertexIndex++] = (longitude.inDegrees - vertexOrigin.x).toFloat() + vertexArray[vertexIndex++] = (latitude.inDegrees - vertexOrigin.y).toFloat() + vertexArray[vertexIndex++] = (altitude - vertexOrigin.z).toFloat() + vertexArray[vertexIndex++] = 1.0f + vertexArray[vertexIndex++] = texCoord1d.toFloat() + + vertexArray[vertexIndex++] = (longitude.inDegrees - vertexOrigin.x).toFloat() + vertexArray[vertexIndex++] = (latitude.inDegrees - vertexOrigin.y).toFloat() + vertexArray[vertexIndex++] = (altitude - vertexOrigin.z).toFloat() + vertexArray[vertexIndex++] = -1.0f + vertexArray[vertexIndex++] = texCoord1d.toFloat() + + if (addIndices) { + outlineElements.add(vertex - 2) // 0 0 -- 2 + outlineElements.add(vertex - 1) // 1 | / | ----> line goes this way + outlineElements.add(vertex) // 2 | / | ----> line goes this way + outlineElements.add(vertex) // 2 1 -- 3 + outlineElements.add(vertex - 1) // 1 + outlineElements.add(vertex + 1) // 3 + } + } else { + vertexArray[vertexIndex++] = (point.x - vertexOrigin.x).toFloat() + vertexArray[vertexIndex++] = (point.y - vertexOrigin.y).toFloat() + vertexArray[vertexIndex++] = (point.z - vertexOrigin.z).toFloat() + vertexArray[vertexIndex++] = 1.0f + vertexArray[vertexIndex++] = texCoord1d.toFloat() + vertexArray[vertexIndex++] = (point.x - vertexOrigin.x).toFloat() + vertexArray[vertexIndex++] = (point.y - vertexOrigin.y).toFloat() + vertexArray[vertexIndex++] = (point.z - vertexOrigin.z).toFloat() + vertexArray[vertexIndex++] = -1.0f + vertexArray[vertexIndex++] = texCoord1d.toFloat() + if (addIndices) { + outlineElements.add(vertex - 2) // 0 0 -- 2 + outlineElements.add(vertex - 1) // 1 | / | ----> line goes this way + outlineElements.add(vertex) // 2 | / | ----> line goes this way + outlineElements.add(vertex) // 2 1 -- 3 + outlineElements.add(vertex - 1) // 1 + outlineElements.add(vertex + 1) // 3 + } + } + } +} \ No newline at end of file diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/shape/Polygon.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/shape/Polygon.kt index a494f5355..148eaf798 100644 --- a/worldwind/src/commonMain/kotlin/earth/worldwind/shape/Polygon.kt +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/shape/Polygon.kt @@ -179,9 +179,9 @@ open class Polygon @JvmOverloads constructor( drawState.program = rc.getShaderProgram { TriangleShaderProgram() } // Assemble the drawable's OpenGL vertex buffer object. - drawState.vertexBuffer = rc.getBufferObject(vertexBufferKey) { - FloatBufferObject(GL_ARRAY_BUFFER, vertexArray, vertexIndex) - } + val vertexBuffer = rc.getBufferObject(vertexBufferKey) { FloatBufferObject(GL_ARRAY_BUFFER, vertexArray) } + drawState.vertexState.addAttribute(0, vertexBuffer, 4, GL_FLOAT, false, VERTEX_STRIDE * 4, 0) + drawState.vertexState.addAttribute(3, vertexBuffer, 2, GL_FLOAT, false, VERTEX_STRIDE * 4,12) // Assemble the drawable's OpenGL element buffer object. drawState.elementBuffer = rc.getBufferObject(elementBufferKey) { @@ -199,9 +199,11 @@ open class Polygon @JvmOverloads constructor( drawStateLines.program = rc.getShaderProgram { TriangleShaderProgram() } // Assemble the drawable's OpenGL vertex buffer object. - drawStateLines.vertexBuffer = rc.getBufferObject(vertexLinesBufferKey) { - FloatBufferObject(GL_ARRAY_BUFFER, lineVertexArray) - } + val lineVertexBuffer = rc.getBufferObject(vertexLinesBufferKey) { FloatBufferObject(GL_ARRAY_BUFFER, lineVertexArray) } + drawStateLines.vertexState.addAttribute(0, lineVertexBuffer, 4, GL_FLOAT, false, 20, 0) + drawStateLines.vertexState.addAttribute(1, lineVertexBuffer, 4, GL_FLOAT, false, 20, 40) + drawStateLines.vertexState.addAttribute(2, lineVertexBuffer, 4, GL_FLOAT, false, 20, 80) + drawStateLines.vertexState.addAttribute(3, lineVertexBuffer, 1, GL_FLOAT, false, 20, 56) // Assemble the drawable's OpenGL element buffer object. drawStateLines.elementBuffer = rc.getBufferObject(elementLinesBufferKey) { @@ -255,7 +257,6 @@ open class Polygon @JvmOverloads constructor( // Configure the drawable to display the shape's interior top. drawState.color(if (rc.isPickMode) pickColor else activeAttributes.interiorColor) drawState.opacity(if (rc.isPickMode) 1f else rc.currentLayer.opacity) - drawState.texCoordAttrib(2 /*size*/, 12 /*offset in bytes*/) drawState.drawElements(GL_TRIANGLES, topElements.size, GL_UNSIGNED_INT, 0 /*offset*/) // Configure the drawable to display the shape's interior sides. @@ -490,11 +491,13 @@ open class Polygon @JvmOverloads constructor( lineVertexArray[lineVertexIndex++] = (altitude - vertexOrigin.z).toFloat() lineVertexArray[lineVertexIndex++] = 1.0f lineVertexArray[lineVertexIndex++] = texCoord1d.toFloat() + lineVertexArray[lineVertexIndex++] = (longitude.inDegrees - vertexOrigin.x).toFloat() lineVertexArray[lineVertexIndex++] = (latitude.inDegrees - vertexOrigin.y).toFloat() lineVertexArray[lineVertexIndex++] = (altitude - vertexOrigin.z).toFloat() lineVertexArray[lineVertexIndex++] = -1.0f lineVertexArray[lineVertexIndex++] = texCoord1d.toFloat() + if (addIndices) { outlineElements.add(vertex - 2) outlineElements.add(vertex - 1) @@ -509,6 +512,7 @@ open class Polygon @JvmOverloads constructor( lineVertexArray[lineVertexIndex++] = (point.z - vertexOrigin.z).toFloat() lineVertexArray[lineVertexIndex++] = 1.0f lineVertexArray[lineVertexIndex++] = texCoord1d.toFloat() + lineVertexArray[lineVertexIndex++] = (point.x - vertexOrigin.x).toFloat() lineVertexArray[lineVertexIndex++] = (point.y - vertexOrigin.y).toFloat() lineVertexArray[lineVertexIndex++] = (point.z - vertexOrigin.z).toFloat()