diff --git a/worldwind-examples-android/src/main/AndroidManifest.xml b/worldwind-examples-android/src/main/AndroidManifest.xml index 5cc7a642d..a8293d81c 100644 --- a/worldwind-examples-android/src/main/AndroidManifest.xml +++ b/worldwind-examples-android/src/main/AndroidManifest.xml @@ -118,6 +118,14 @@ android:noHistory="true" android:theme="@style/AppTheme.NoActionBar"> + + startActivity(Intent(applicationContext, PlacemarksMilStd2525DemoActivity::class.java)) R.id.nav_placemarks_milstd2525_stress_activity -> startActivity(Intent(applicationContext, PlacemarksMilStd2525StressActivity::class.java)) R.id.nav_placemarks_select_drag_activity -> startActivity(Intent(applicationContext, PlacemarksSelectDragActivity::class.java)) + R.id.nav_texture_quad_example_activity -> startActivity(Intent(applicationContext, TextureQuadExampleActivity::class.java)) R.id.nav_placemarks_stress_activity -> startActivity(Intent(applicationContext, PlacemarksStressTestActivity::class.java)) R.id.nav_texture_stress_test_activity -> startActivity(Intent(applicationContext, TextureStressTestActivity::class.java)) R.id.nav_kml_demo_activity -> startActivity(Intent(applicationContext, KmlDemoActivity::class.java)) diff --git a/worldwind-examples-android/src/main/kotlin/earth/worldwind/examples/TextureQuadExampleActivity.kt b/worldwind-examples-android/src/main/kotlin/earth/worldwind/examples/TextureQuadExampleActivity.kt new file mode 100644 index 000000000..7ba85ebb0 --- /dev/null +++ b/worldwind-examples-android/src/main/kotlin/earth/worldwind/examples/TextureQuadExampleActivity.kt @@ -0,0 +1,197 @@ +package earth.worldwind.examples + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import earth.worldwind.WorldWind +import earth.worldwind.geom.AltitudeMode +import earth.worldwind.geom.Angle +import earth.worldwind.geom.Angle.Companion.degrees +import earth.worldwind.geom.Location +import earth.worldwind.geom.LookAt +import earth.worldwind.geom.Offset.Companion.bottomCenter +import earth.worldwind.geom.Sector +import earth.worldwind.layer.RenderableLayer +import earth.worldwind.render.image.ImageSource +import earth.worldwind.shape.SurfaceImage +import earth.worldwind.geom.Position +import earth.worldwind.geom.Position.Companion.fromDegrees +import earth.worldwind.gesture.SelectDragCallback +import earth.worldwind.render.Color +import earth.worldwind.render.Renderable +import earth.worldwind.render.image.ImageSource.Companion.fromResource +import earth.worldwind.shape.Highlightable +import earth.worldwind.shape.Placemark +import earth.worldwind.shape.Placemark.Companion.createWithImage +import earth.worldwind.shape.PlacemarkAttributes +import earth.worldwind.shape.TextureQuad +import earth.worldwind.shape.ShapeAttributes + + +class TextureQuadExampleActivity: GeneralGlobeActivity() { + private var selectedObject: Renderable? = null // Last "selected" object from single tap or double tap + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + aboutBoxTitle = "About the " + resources.getText(R.string.title_texture_quad_example) + aboutBoxText = """ + Demonstrates how to stretch texture quad + """.trimIndent() + + // Add dragging callback + wwd.selectDragDetector.callback = object : SelectDragCallback { + override fun canPickRenderable(renderable: Renderable) = true + override fun canMoveRenderable(renderable: Renderable) = renderable === selectedObject && renderable.hasUserProperty(MOVABLE) + override fun onRenderablePicked(renderable: Renderable, position: Position) = toggleSelection(renderable) + override fun onRenderableContext(renderable: Renderable, position: Position) = contextMenu(renderable) + override fun onTerrainContext(position: Position) = contextMenu() + override fun onRenderableDoubleTap(renderable: Renderable, position: Position) { + // Note that double-tapping should not toggle a "selected" object's selected state + if (renderable !== selectedObject) toggleSelection(renderable) // deselects a previously selected item + } + override fun onRenderableMoved(renderable: Renderable, fromPosition: Position, toPosition: Position) + { + val placemark = (renderable as? Placemark) + placemark?.moveTo(wwd.engine.globe, toPosition) + var textureQuad = renderable.getUserProperty(TEXTURE_QUAD_REF) + val vertexIndex = renderable.getUserProperty(VERTEX_INDEX) + vertexIndex?.let { textureQuad?.setLocation(it, Location(toPosition.latitude, toPosition.longitude)) } + } + + /** + * Toggles the selected state of the picked renderable. + */ + private fun toggleSelection(renderable: Renderable) { + // Test if last picked object is "selectable". If not, retain the + // currently selected object. To discard the current selection, + // the user must pick another selectable object or the current object. + if (renderable.hasUserProperty(SELECTABLE)) { + val isNewSelection = renderable !== selectedObject + + // Only one object can be selected at time, deselect any previously selected object + if (isNewSelection) (selectedObject as? Highlightable)?.isHighlighted = false + + // Display the highlight or normal attributes to indicate the + // selected or unselected state respectively. + (renderable as? Highlightable)?.isHighlighted = isNewSelection + + // Track the selected object + selectedObject = if (isNewSelection) renderable else null + } else { + val textureQuad = (renderable as? TextureQuad) + textureQuad?.let { + val arr = it.getAllLocations() + + Toast.makeText(applicationContext, "BL:{%.6f, %.6f}\nBR:{%.6f, %.6f}\nTR:{%.6f, %.6f}\nTL:{%.6f, %.6f}".format( + arr[0].latitude.inDegrees, arr[0].longitude.inDegrees, + arr[1].latitude.inDegrees, arr[1].longitude.inDegrees, + arr[2].latitude.inDegrees, arr[2].longitude.inDegrees, + arr[3].latitude.inDegrees, arr[3].longitude.inDegrees), Toast.LENGTH_SHORT).show() + } + } + } + + /** + * Shows the context information for the WorldWindow. + */ + private fun contextMenu(renderable: Renderable? = null) { + val so = selectedObject + Toast.makeText( + applicationContext, + "${renderable?.displayName ?: "Nothing"} picked and ${so?.displayName ?: "nothing"} selected.", + Toast.LENGTH_LONG + ).show() + } + } + // Add a layer for texture quads to the WorldWindow + wwd.engine.layers.addLayer( + RenderableLayer("Texture quad example").apply { + + addAllRenderables( + createStretchableTextureQuad( + Location(51.26891660167462.degrees, 30.0096671570868.degrees), + Location(51.27062637593827.degrees, 30.01240117718469.degrees), + Location(51.271804026840556.degrees, 30.01029779181104.degrees), + Location(51.270125460836965.degrees, 30.00779959572076.degrees), + ImageSource.fromResource(R.drawable.korogode_image), true + ) + ) + } + ) + + // And finally, for this demo, position the viewer to look at the placemarks + val lookAt = LookAt( + position = Position(51.270125460836965.degrees, 30.00989959572076.degrees, 9.0e2), altitudeMode = AltitudeMode.ABSOLUTE, + range = 0.0, heading = Angle.ZERO, tilt = Angle.ZERO, roll = Angle.ZERO + ) + wwd.engine.cameraFromLookAt(lookAt) + } + + companion object { + /** + * The MOVABLE capability, if it exists in a Placemark's user properties, allows dragging (after being selected) + */ + const val MOVABLE = "movable" + /** + * The SELECTABLE capability, if it exists in a Placemark's user properties, allows selection with single-tap + */ + const val SELECTABLE = "selectable" + + const val VERTEX_INDEX = "vertex_index" + const val TEXTURE_QUAD_REF = "texture_quad_ref" + private const val NORMAL_IMAGE_SCALE = 3.0 + private const val HIGHLIGHTED_IMAGE_SCALE = 4.0 + + + /** + * Helper method to create vehicle placemarks. + */ + private fun createTextureQuadVertexPlacemark(location: Location, textureQuad : TextureQuad, vertexIndex : Int) = + createWithImage( + Position(location.latitude, location.longitude, 0.0), + fromResource(R.drawable.vehicle_suv) + ).apply { + attributes.apply { + imageOffset = bottomCenter() + imageScale = NORMAL_IMAGE_SCALE + } + highlightAttributes = PlacemarkAttributes(attributes).apply { + imageScale = HIGHLIGHTED_IMAGE_SCALE + imageColor = Color(android.graphics.Color.YELLOW) + } + + altitudeMode = AltitudeMode.CLAMP_TO_GROUND + // The select/drag controller will examine a placemark's "capabilities" to determine what operations are applicable: + putUserProperty(SELECTABLE, true) + putUserProperty(MOVABLE, true) + putUserProperty(TEXTURE_QUAD_REF, textureQuad) + putUserProperty(VERTEX_INDEX, vertexIndex) + } + + private fun createStretchableTextureQuad(bottomLeft : Location, + bottomRight: Location, + topRight : Location, + topLeft : Location, + imageSource: ImageSource, + drawOutline: Boolean = true) : List + { + val textureQuad = TextureQuad(bottomLeft, bottomRight, topRight, topLeft, + ShapeAttributes().apply { + interiorImageSource = imageSource + isDrawOutline = drawOutline + outlineWidth = 3.0f + outlineColor = Color(1.0f, 0.0f, 0.0f, 1f) + }) + return listOf( + createTextureQuadVertexPlacemark(bottomLeft, textureQuad, 0), + createTextureQuadVertexPlacemark(bottomRight, textureQuad, 1), + createTextureQuadVertexPlacemark(topRight, textureQuad, 2), + createTextureQuadVertexPlacemark(topLeft, textureQuad, 3), + textureQuad + ) + } + } +} \ No newline at end of file diff --git a/worldwind-examples-android/src/main/res/drawable/korogode_image.jpg b/worldwind-examples-android/src/main/res/drawable/korogode_image.jpg new file mode 100644 index 000000000..2b953a014 Binary files /dev/null and b/worldwind-examples-android/src/main/res/drawable/korogode_image.jpg differ diff --git a/worldwind-examples-android/src/main/res/menu/activity_drawer.xml b/worldwind-examples-android/src/main/res/menu/activity_drawer.xml index d34e029ff..85eaa2be1 100644 --- a/worldwind-examples-android/src/main/res/menu/activity_drawer.xml +++ b/worldwind-examples-android/src/main/res/menu/activity_drawer.xml @@ -26,6 +26,10 @@ android:id="@+id/nav_placemarks_select_drag_activity" android:icon="@android:drawable/ic_menu_myplaces" android:title="@string/title_placemarks_select_drag"/> + MIL-STD-2525 Demo MIL-STD-2525 Stress Test Placemark Select and Drag + Texture quad example KML Demonstration GeoJSON Demonstration Placemarks Stress Test diff --git a/worldwind-tutorials/src/androidMain/assets/texture_quad_tutorial.html b/worldwind-tutorials/src/androidMain/assets/texture_quad_tutorial.html new file mode 100644 index 000000000..92d5cdc36 --- /dev/null +++ b/worldwind-tutorials/src/androidMain/assets/texture_quad_tutorial.html @@ -0,0 +1,86 @@ + + + + + Texture Quad Tutorial + + + + + + + +

Texture Quad Tutorial

+

+ Demonstrates how to add TextureQuads to a RenderableLayer +

+

+ This example adds one texture quad to the basic globe: +

+
    +
  • A remote image showing novij korogod
  • +
+ +

Example

+

TextureQuadFragment.kt

+

+ The TextureQuadFragment class extends the BasicGlobeFragment and overrides the createWorldWindow method. + Here we create one TextureQuad object and add it to a RenderableLayer, and then we add the layer to the globe. +

+
+ +
+ + + + \ No newline at end of file diff --git a/worldwind-tutorials/src/androidMain/kotlin/earth/worldwind/tutorials/MainActivity.kt b/worldwind-tutorials/src/androidMain/kotlin/earth/worldwind/tutorials/MainActivity.kt index bf2d0ae1d..e50036133 100644 --- a/worldwind-tutorials/src/androidMain/kotlin/earth/worldwind/tutorials/MainActivity.kt +++ b/worldwind-tutorials/src/androidMain/kotlin/earth/worldwind/tutorials/MainActivity.kt @@ -321,6 +321,11 @@ class MainActivity: AppCompatActivity(), NavigationView.OnNavigationItemSelected "file:///android_asset/surface_image_tutorial.html", R.string.title_surface_image ) + R.id.nav_texture_quad_activity -> loadTutorial( + TextureQuadFragment::class.java, + "file:///android_asset/texture_quad_tutorial.html", + R.string.title_texture_quad + ) R.id.nav_wms_layer_activity -> loadTutorial( WmsLayerFragment::class.java, "file:///android_asset/wms_layer_tutorial.html", diff --git a/worldwind-tutorials/src/androidMain/kotlin/earth/worldwind/tutorials/TextureQuadFragment.kt b/worldwind-tutorials/src/androidMain/kotlin/earth/worldwind/tutorials/TextureQuadFragment.kt new file mode 100644 index 000000000..61f50d8f6 --- /dev/null +++ b/worldwind-tutorials/src/androidMain/kotlin/earth/worldwind/tutorials/TextureQuadFragment.kt @@ -0,0 +1,8 @@ +package earth.worldwind.tutorials + +class TextureQuadFragment : BasicGlobeFragment() { + /** + * Creates a new WorldWindow with an additional RenderableLayer containing one TextureQuad. + */ + override fun createWorldWindow() = super.createWorldWindow().also { TextureQuadTutorial(it.engine).start() } +} \ No newline at end of file diff --git a/worldwind-tutorials/src/androidMain/res/menu/activity_drawer.xml b/worldwind-tutorials/src/androidMain/res/menu/activity_drawer.xml index de0007629..5ae45e302 100644 --- a/worldwind-tutorials/src/androidMain/res/menu/activity_drawer.xml +++ b/worldwind-tutorials/src/androidMain/res/menu/activity_drawer.xml @@ -29,6 +29,10 @@ android:id="@+id/nav_surface_image_activity" android:icon="@android:drawable/ic_menu_mapmode" android:title="@string/title_surface_image"/> + MGRS Graticule Gauss-Kruger Graticule Surface Image + Texture Quad WMS Layer WMTS Layer WCS Elevation Coverage diff --git a/worldwind-tutorials/src/commonMain/kotlin/earth.worldwind.tutorials/TextureQuadTutorial.kt b/worldwind-tutorials/src/commonMain/kotlin/earth.worldwind.tutorials/TextureQuadTutorial.kt new file mode 100644 index 000000000..d56b875e6 --- /dev/null +++ b/worldwind-tutorials/src/commonMain/kotlin/earth.worldwind.tutorials/TextureQuadTutorial.kt @@ -0,0 +1,48 @@ +package earth.worldwind.tutorials + +import earth.worldwind.WorldWind +import earth.worldwind.geom.AltitudeMode +import earth.worldwind.geom.Angle +import earth.worldwind.geom.Angle.Companion.degrees +import earth.worldwind.geom.Location +import earth.worldwind.geom.Sector +import earth.worldwind.layer.RenderableLayer +import earth.worldwind.render.Color +import earth.worldwind.render.image.ImageSource +import earth.worldwind.shape.ShapeAttributes +import earth.worldwind.shape.SurfaceImage +import earth.worldwind.shape.TextureQuad + +class TextureQuadTutorial(private val engine: WorldWind) : AbstractTutorial() { + private val layer = RenderableLayer("Texture quad").apply { + // Configure a Texture quad to display an Android resource showing the novij korogod. + addRenderable( + TextureQuad( + Location(51.26891660167462.degrees, 30.0096671570868.degrees), + Location(51.27062637593827.degrees, 30.01240117718469.degrees), + Location(51.271804026840556.degrees, 30.01029779181104.degrees), + Location(51.270125460836965.degrees, 30.00779959572076.degrees), + ShapeAttributes().apply { + interiorImageSource = ImageSource.fromResource(MR.images.korogode_image) + isDrawOutline = true + outlineWidth = 3.0f + outlineColor = Color(1.0f, 0.0f, 0.0f, 1f) + } + ) + ) + } + + override fun start() { + super.start() + engine.layers.addLayer(layer) + engine.camera.set( + 51.270125460836965.degrees, 30.00989959572076.degrees, 9.0e2, + AltitudeMode.ABSOLUTE, heading = Angle.ZERO, tilt = Angle.ZERO, roll = Angle.ZERO + ) + } + + override fun stop() { + super.stop() + engine.layers.removeLayer(layer) + } +} \ No newline at end of file diff --git a/worldwind-tutorials/src/commonMain/moko-resources/images/korogode_image@1x.jpg b/worldwind-tutorials/src/commonMain/moko-resources/images/korogode_image@1x.jpg new file mode 100644 index 000000000..2b953a014 Binary files /dev/null and b/worldwind-tutorials/src/commonMain/moko-resources/images/korogode_image@1x.jpg differ diff --git a/worldwind-tutorials/src/jsMain/kotlin/earth/worldwind/tutorials/Main.kt b/worldwind-tutorials/src/jsMain/kotlin/earth/worldwind/tutorials/Main.kt index 53d99dd83..688d37e49 100644 --- a/worldwind-tutorials/src/jsMain/kotlin/earth/worldwind/tutorials/Main.kt +++ b/worldwind-tutorials/src/jsMain/kotlin/earth/worldwind/tutorials/Main.kt @@ -65,6 +65,7 @@ fun main() { "Labels" to LabelsTutorial(wwd.engine), "Sight line" to SightlineTutorial(wwd.engine), "Surface image" to SurfaceImageTutorial(wwd.engine), + "Texture quad" to TextureQuadTutorial(wwd.engine), "MilStd2525 graphics" to MilStd2525Tutorial(wwd.engine), "Show tessellation" to ShowTessellationTutorial(wwd.engine), "MGRS Graticule" to MGRSGraticuleTutorial(wwd.engine), diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawQuadState.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawQuadState.kt new file mode 100644 index 000000000..e17d4cc4e --- /dev/null +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawQuadState.kt @@ -0,0 +1,121 @@ +package earth.worldwind.draw + +import earth.worldwind.geom.Matrix3 +import earth.worldwind.geom.Matrix4 +import earth.worldwind.geom.Vec2 +import earth.worldwind.geom.Vec3 +import earth.worldwind.render.Color +import earth.worldwind.render.Texture +import earth.worldwind.render.buffer.BufferObject +import earth.worldwind.render.program.AbstractShaderProgram + +open class DrawQuadState internal constructor() { + companion object { + const val MAX_DRAW_ELEMENTS = 5 + } + + var programDrawToTexture: AbstractShaderProgram? = null + var programDrawTextureToTerrain: AbstractShaderProgram? = null + var vertexBuffer: BufferObject? = null + var elementBuffer: BufferObject? = null + val vertexOrigin = Vec3() + + var A = Vec2() + var B = Vec2() + var C = Vec2() + var D = Vec2() + var vertexStride = 0 + var enableCullFace = true + var enableDepthTest = true + var enableLighting = false + var depthOffset = 0.0 + var isLine = false + val color = Color() + var opacity = 1.0f + var lineWidth = 1f + var texture: Texture? = null + var textureLod = 0 + val texCoordMatrix = Matrix3() + val texCoordAttrib = VertexAttrib() + internal var primCount = 0 + internal val prims = Array(MAX_DRAW_ELEMENTS) { DrawElements() } + + open fun reset() { + programDrawToTexture = null + programDrawTextureToTerrain = null + vertexBuffer = null + elementBuffer = null + vertexOrigin.set(0.0, 0.0, 0.0) + vertexStride = 0 + enableCullFace = true + enableDepthTest = true + enableLighting = false + depthOffset = 0.0 + isLine = false + color.set(1f, 1f, 1f, 1f) + opacity = 1.0f + lineWidth = 1f + texture = null + textureLod = 0 + texCoordMatrix.setToIdentity() + texCoordAttrib.size = 0 + texCoordAttrib.offset = 0 + primCount = 0 + A.set(0.0, 0.0) + B.set(0.0, 0.0) + C.set(0.0, 0.0) + D.set(0.0, 0.0) + + for (idx in 0 until MAX_DRAW_ELEMENTS) prims[idx].texture = null + } + + open fun drawElements(mode: Int, count: Int, type: Int, offset: Int) { + val prim = prims[primCount++] + prim.mode = mode + prim.count = count + prim.type = type + prim.offset = offset + prim.color.copy(color) + prim.opacity = opacity + prim.lineWidth = lineWidth + prim.depthOffset = depthOffset + prim.texture = texture + prim.textureLod = textureLod + prim.texCoordMatrix.copy(texCoordMatrix) + prim.texCoordAttrib.copy(texCoordAttrib) + prim.A.copy(A) + prim.B.copy(B) + prim.C.copy(C) + prim.D.copy(D) + } + + internal open class DrawElements { + var mode = 0 + var count = 0 + var type = 0 + var offset = 0 + val color = Color() + var opacity = 1.0f + var lineWidth = 0f + var depthOffset = 0.0 + var texture: Texture? = null + var textureLod = 0 + val texCoordMatrix = Matrix3() + val texCoordAttrib = VertexAttrib() + + var A = Vec2() + var B = Vec2() + var C = Vec2() + var D = Vec2() + } + + open class VertexAttrib { + var size = 0 + var offset = 0 + + fun copy(attrib: VertexAttrib) { + size = attrib.size + offset = attrib.offset + } + } +} \ No newline at end of file diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawableSurfaceQuad.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawableSurfaceQuad.kt new file mode 100644 index 000000000..5a5c1fe98 --- /dev/null +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/draw/DrawableSurfaceQuad.kt @@ -0,0 +1,315 @@ +package earth.worldwind.draw + +import earth.worldwind.geom.Matrix3 +import earth.worldwind.geom.Matrix4 +import earth.worldwind.geom.Sector +import earth.worldwind.geom.Vec2 +import earth.worldwind.globe.Globe +import earth.worldwind.render.Color +import earth.worldwind.render.Texture +import earth.worldwind.render.program.SurfaceQuadShaderProgram +import earth.worldwind.render.program.TriangleShaderProgram +import earth.worldwind.util.Pool +import earth.worldwind.util.kgl.* +import kotlin.jvm.JvmStatic +import kotlin.math.cos + +open class DrawableSurfaceQuad protected constructor(): Drawable { + var offset = Globe.Offset.Center + val sector = Sector() + val drawState = DrawQuadState() + var version = 0 + var isDynamic = false + private var hash = 0 + private var pool: Pool? = null + private val mvpMatrix = Matrix4() + private val textureMvpMatrix = Matrix4() + + companion object { + val KEY = DrawableSurfaceQuad::class + private val identityMatrix3 = Matrix3() + private val color = Color() + private var opacity = 1.0f + + @JvmStatic + fun obtain(pool: Pool): DrawableSurfaceQuad { + val instance = pool.acquire() ?: DrawableSurfaceQuad() + instance.pool = pool + return instance + } + } + + override fun recycle() { + drawState.reset() + pool?.release(this) + pool = null + hash = 0 + } + + override fun draw(dc: DrawContext) { + // 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 + try { + // Add this shape. + scratchList.add(this) + + // Add all shapes that are contiguous in the drawable queue. + while (true) { + val next = dc.peekDrawable() ?: break + // check if the drawable at the front of the queue can be batched + if (next !is DrawableSurfaceQuad || next.isDynamic != isDynamic) break + dc.pollDrawable() // take it off the queue + scratchList.add(next) + } + + // Draw the accumulated shapes on each drawable terrain. + for (idx in 0 until dc.drawableTerrainCount) { + // Get the drawable terrain associated with the draw context. + val terrain = dc.getDrawableTerrain(idx) + // Draw the accumulated surface shapes to a texture representing the terrain's sector. + drawShapesToTexture(dc, terrain)?.let { texture -> + // Draw the texture containing the rasterized shapes onto the terrain geometry. + drawTextureToTerrain(dc, terrain, texture) + } + } + } 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*/) + } + } + + protected open fun textureHash(): Int { + if (hash == 0) { + hash = 31 * version + drawState.isLine.hashCode() + for (i in 0 until drawState.primCount) { + val prim = drawState.prims[i] + hash = 31 * hash + prim.color.hashCode() + hash = 31 * hash + prim.opacity.hashCode() + hash = 31 * hash + prim.texture.hashCode() + hash = 31 * hash + prim.textureLod + } + } + return hash + } + + protected open fun drawShapesToTexture(dc: DrawContext, terrain: DrawableTerrain): Texture? { + // The terrain's sector defines the geographic region in which to draw. + val terrainSector = terrain.sector + + // Filter shapes that intersect current terrain tile and globe offset + val scratchList = mutableListOf() + for (idx in dc.scratchList.indices) { + val shape = dc.scratchList[idx] as DrawableSurfaceQuad + if (shape.offset == terrain.offset && shape.sector.intersectsOrNextTo(terrainSector)) scratchList.add(shape) + } + if (scratchList.isEmpty()) return null // Nothing to draw + + var hash = 0 + val useCache = !dc.isPickMode && !isDynamic // Use single color attachment in pick mode and for dynamic shapes + if (useCache) { + // Calculate a texture cache key for this terrain tile and shapes batch + hash = terrainSector.hashCode() + for (idx in scratchList.indices) hash = 31 * hash + scratchList[idx].textureHash() + + // Use cached texture + val cachedTexture = dc.texturesCache[hash] + if (cachedTexture != null) return cachedTexture + } + + // Redraw shapes to texture and put in cache if required + val framebuffer = dc.scratchFramebuffer + val colorAttachment = framebuffer.getAttachedTexture(GL_COLOR_ATTACHMENT0) + val texture = if (!useCache) colorAttachment + else Texture(colorAttachment.width, colorAttachment.height, GL_RGBA, GL_UNSIGNED_BYTE, true) + try { + if (!framebuffer.bindFramebuffer(dc)) return null // framebuffer failed to bind + if (useCache) framebuffer.attachTexture(dc, texture, GL_COLOR_ATTACHMENT0) + + // Clear the framebuffer and disable the depth test. + dc.gl.viewport(0, 0, colorAttachment.width, colorAttachment.height) + dc.gl.clear(GL_COLOR_BUFFER_BIT) + dc.gl.disable(GL_DEPTH_TEST) + + // Compute the tile common matrix that transforms geographic coordinates to texture fragments appropriate + // for the terrain sector. + // TODO capture this in a method on Matrix4 + textureMvpMatrix.setToIdentity() + textureMvpMatrix.multiplyByTranslation(-1.0, -1.0, 0.0) + textureMvpMatrix.multiplyByScale( + 2.0 / terrainSector.deltaLongitude.inDegrees, + 2.0 / terrainSector.deltaLatitude.inDegrees, + 0.0 + ) + textureMvpMatrix.multiplyByTranslation( + -terrainSector.minLongitude.inDegrees, + -terrainSector.minLatitude.inDegrees, + 0.0 + ) + for (idx in scratchList.indices) { + val shape = scratchList[idx] + if(shape.drawState.isLine) + { + if(!drawShapesToTextureOutline(dc, terrain, shape, + colorAttachment.width.toFloat(), colorAttachment.height.toFloat())) + continue + } + else + { + if(!drawShapesToTextureInterior(dc, terrain, shape)) + continue + } + } + if (useCache) dc.texturesCache.put(hash, texture, 1) + } finally { + if (useCache) framebuffer.attachTexture(dc, colorAttachment, GL_COLOR_ATTACHMENT0) + // Restore the default WorldWind OpenGL state. + dc.bindFramebuffer(KglFramebuffer.NONE) + dc.gl.viewport(dc.viewport.x, dc.viewport.y, dc.viewport.width, dc.viewport.height) + dc.gl.enable(GL_DEPTH_TEST) + dc.gl.lineWidth(1f) + } + return texture + } + + protected open fun drawTextureToTerrain(dc: DrawContext, terrain: DrawableTerrain, texture: Texture) { + val program = drawState.programDrawTextureToTerrain as? TriangleShaderProgram ?: return + if (!program.useProgram(dc)) return // program failed to build + + try { + 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 + if (!texture.bindTexture(dc)) return // 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.enablePickMode(false) + program.enableTexture(true) + program.loadTexCoordMatrix(identityMatrix3) + program.loadColor(color) + program.loadOpacity(opacity) + program.loadScreen(dc.viewport.width.toFloat(), dc.viewport.height.toFloat()) + + // Use the draw context's modelview projection matrix, transformed to terrain local coordinates. + val terrainOrigin = terrain.vertexOrigin + mvpMatrix.copy(dc.modelviewProjection) + mvpMatrix.multiplyByTranslation(terrainOrigin.x, terrainOrigin.y, terrainOrigin.z) + program.loadModelviewProjection(mvpMatrix) + program.loadClipDistance(0.0f) + + // Draw the terrain as triangles. + terrain.drawTriangles(dc) + } finally { + // Unbind color attachment texture to avoid feedback loop + dc.defaultTexture.bindTexture(dc) + } + } + + protected open fun drawShapesToTextureInterior(dc: DrawContext, terrain: DrawableTerrain, shape : DrawableSurfaceQuad) : Boolean { + val program = shape.drawState.programDrawToTexture as? SurfaceQuadShaderProgram ?: return false + if (!program.useProgram(dc)) return false + + // Use the draw context's pick mode. + program.enablePickMode(dc.isPickMode) + + if (shape.drawState.vertexBuffer?.bindBuffer(dc) != true) return false // vertex buffer unspecified or failed to bind + if (shape.drawState.elementBuffer?.bindBuffer(dc) != true) return false // element buffer unspecified or failed to bind + + // Transform local shape coordinates to texture fragments appropriate for the terrain sector. + mvpMatrix.copy(textureMvpMatrix) + mvpMatrix.multiplyByTranslation( + shape.drawState.vertexOrigin.x, + shape.drawState.vertexOrigin.y, + shape.drawState.vertexOrigin.z + ) + program.loadModelviewProjection(mvpMatrix) + + // Use the shape's vertex point attribute. + dc.gl.vertexAttribPointer(0 /*vertexPoint*/, 3, GL_FLOAT, false, shape.drawState.vertexStride, 0) + dc.gl.disable(GL_CULL_FACE); + // 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 (prim.texture?.bindTexture(dc) == true) { + program.loadTexCoordMatrix(prim.texCoordMatrix) + program.loadABCD(prim.A, prim.B, prim.C, prim.D) + program.enableTexture(true) + } else { + program.enableTexture(false) + // prevent "RENDER WARNING: there is no texture bound to unit 0" + dc.defaultTexture.bindTexture(dc) + } + + dc.gl.drawElements(prim.mode, prim.count, prim.type, prim.offset) + } + dc.gl.enable(GL_CULL_FACE); + return true + } + + protected open fun drawShapesToTextureOutline(dc: DrawContext, terrain: DrawableTerrain, shape : DrawableSurfaceQuad, colorAttachmentWidth : Float, colorAttachmentHeight : Float) : Boolean { + val program = shape.drawState.programDrawToTexture as? TriangleShaderProgram ?: return false + if (!program.useProgram(dc)) return false + + if (shape.drawState.vertexBuffer?.bindBuffer(dc) != true) return false // vertex buffer unspecified or failed to bind + if (shape.drawState.elementBuffer?.bindBuffer(dc) != true) return false // element buffer unspecified or failed to bind + + // Use the draw context's pick mode. + program.enablePickMode(dc.isPickMode) + + program.enableOneVertexMode(false) + program.loadClipDistance((textureMvpMatrix.m[11] / (textureMvpMatrix.m[10] - 1.0)).toFloat() / 2.0f) // set value here, but matrix is orthographic and shader clipping won't work as vertices projected orthographically always have .w == 1 + program.loadScreen(colorAttachmentWidth, colorAttachmentHeight) + + // Transform local shape coordinates to texture fragments appropriate for the terrain sector. + mvpMatrix.copy(textureMvpMatrix) + mvpMatrix.multiplyByTranslation( + shape.drawState.vertexOrigin.x, + shape.drawState.vertexOrigin.y, + shape.drawState.vertexOrigin.z + ) + program.loadModelviewProjection(mvpMatrix) + + // Use the shape's vertex point attribute. + dc.gl.vertexAttribPointer(0 /*pointA*/, 4, GL_FLOAT, false, 20, 0) + dc.gl.vertexAttribPointer(1 /*pointB*/, 4, GL_FLOAT, false, 20, 80) + dc.gl.vertexAttribPointer(2 /*pointC*/, 4, GL_FLOAT, false, 20, 160) + dc.gl.vertexAttribPointer(3 /*vertexTexCoord*/, 1, GL_FLOAT, false, 20, 96) + + // 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) + program.loadLineWidth(prim.lineWidth) + if (prim.texture?.bindTexture(dc) == true) { + program.loadTexCoordMatrix(prim.texCoordMatrix) + program.enableTexture(true) + } else { + program.enableTexture(false) + // prevent "RENDER WARNING: there is no texture bound to unit 0" + dc.defaultTexture.bindTexture(dc) + } + + dc.gl.drawElements(prim.mode, prim.count, prim.type, prim.offset) + } + return true + } +} \ No newline at end of file diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/render/program/SurfaceQuadShaderProgram.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/render/program/SurfaceQuadShaderProgram.kt new file mode 100644 index 000000000..baf8f7492 --- /dev/null +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/render/program/SurfaceQuadShaderProgram.kt @@ -0,0 +1,226 @@ +package earth.worldwind.render.program + +import earth.worldwind.draw.DrawContext +import earth.worldwind.geom.Matrix3 +import earth.worldwind.geom.Matrix4 +import earth.worldwind.geom.Vec2 +import earth.worldwind.geom.Vec3 +import earth.worldwind.render.Color +import earth.worldwind.util.kgl.KglUniformLocation + +class SurfaceQuadShaderProgram : AbstractShaderProgram() { + override var programSources = arrayOf( + """ + uniform mat4 mvpMatrix; + uniform bool enableTexture; + uniform vec2 A; + uniform vec2 B; + uniform vec2 C; + uniform vec2 D; + + attribute vec4 pointA; + + varying vec2 q; + varying vec2 b1; + varying vec2 b2; + varying vec2 b3; + + void main() { + /* 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) { + // Set up inverse bilinear interpolation + vec2 P = pointA.xy; + q = P - A; + b1 = B - A; + b2 = D - A; + b3 = A - B - D + C; + } + } + """.trimIndent(), + """ + precision highp float; + + uniform mat3 texCoordMatrix; + uniform bool enablePickMode; + uniform bool enableTexture; + uniform vec4 color; + uniform float opacity; + uniform sampler2D texSampler; + + varying vec2 q; + varying vec2 b1; + varying vec2 b2; + varying vec2 b3; + + float Wedge2D(vec2 v, vec2 w) + { + return v.x*w.y - v.y*w.x; + } + + void main() { + vec2 uv = vec2(0.0, 0.0); + float eps = 1e-9; + if (dot(b3, b3) < eps) + { + float denom = Wedge2D(b1, b2); + + uv.x = Wedge2D(q, b2) / denom; + uv.y = Wedge2D(b1, q) / denom; + } + else + { + // Set up quadratic formula + float A = Wedge2D(b2, b3); + float B = Wedge2D(b3, q) - Wedge2D(b1, b2); + float C = Wedge2D(b1, q); + // Solve for v + + if (abs(A) < eps) + { + // Linear form + uv.y = -C/B; + } + else + { + // Quadratic form. Take positive root for CCW winding with V-up + float discrim = max(B * B - 4.0 * A * C, 0.0); + float sqrtD = sqrt(discrim); + + float v1 = (-B + sqrtD) / (2.0 * A); + float v2 = (-B - sqrtD) / (2.0 * A); + uv.y = (v1 >= 0.0 && v1 <= 1.0) ? v1 : v2; + } + + // Solve for u, using largest-magnitude component + vec2 denom = b1 + uv.y * b3; + if (abs(denom.x) > abs(denom.y)) + uv.x = (q.x - b2.x * uv.y) / max(abs(denom.x), eps) * sign(denom.x); + else + uv.x = (q.y - b2.y * uv.y) / max(abs(denom.y), eps) * sign(denom.y); + } + + uv = (texCoordMatrix * vec3(uv, 1.0)).xy; + + 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, uv).a + 0.5); + gl_FragColor = color * texMask; + } else if (!enablePickMode && enableTexture) { + /* Modulate the RGBA color with the 2D texture's RGBA color. */ + gl_FragColor = color * texture2D(texSampler, uv) * opacity; + } else { + /* Return the RGBA color as-is. */ + gl_FragColor = color * opacity; + } + } + """.trimIndent() + ) + override val attribBindings = arrayOf("pointA") + + private var enablePickMode = false + private var enableTexture = false + private val mvpMatrix = Matrix4() + private val texCoordMatrix = Matrix3() + private val color = Color() + private var opacity = 1.0f + private var mvpMatrixId = KglUniformLocation.NONE + private var colorId = KglUniformLocation.NONE + private var opacityId = KglUniformLocation.NONE + private var enablePickModeId = KglUniformLocation.NONE + private var enableTextureId = KglUniformLocation.NONE + private var texCoordMatrixId = KglUniformLocation.NONE + private var texSamplerId = KglUniformLocation.NONE + + private var AId = KglUniformLocation.NONE + private var BId = KglUniformLocation.NONE + private var CId = KglUniformLocation.NONE + private var DId = KglUniformLocation.NONE + + private val array = FloatArray(16) + + override fun initProgram(dc: DrawContext) { + super.initProgram(dc) + mvpMatrixId = gl.getUniformLocation(program, "mvpMatrix") + mvpMatrix.transposeToArray(array, 0) // 4 x 4 identity matrix + gl.uniformMatrix4fv(mvpMatrixId, 1, false, array, 0) + colorId = gl.getUniformLocation(program, "color") + val alpha = color.alpha + gl.uniform4f(colorId, color.red * alpha, color.green * alpha, color.blue * alpha, alpha) + + opacityId = gl.getUniformLocation(program, "opacity") + gl.uniform1f(opacityId, opacity) + + enablePickModeId = gl.getUniformLocation(program, "enablePickMode") + gl.uniform1i(enablePickModeId, if (enablePickMode) 1 else 0) + enableTextureId = gl.getUniformLocation(program, "enableTexture") + gl.uniform1i(enableTextureId, if (enableTexture) 1 else 0) + + texCoordMatrixId = gl.getUniformLocation(program, "texCoordMatrix") + texCoordMatrix.transposeToArray(array, 0) // 3 x 3 identity matrix + gl.uniformMatrix3fv(texCoordMatrixId, 1, false, array, 0) + texSamplerId = gl.getUniformLocation(program, "texSampler") + gl.uniform1i(texSamplerId, 0) // GL_TEXTURE0 + + + AId = gl.getUniformLocation(program, "A") + BId = gl.getUniformLocation(program, "B") + CId = gl.getUniformLocation(program, "C") + DId = gl.getUniformLocation(program, "D") + } + + fun enablePickMode(enable: Boolean) { + if (enablePickMode != enable) { + enablePickMode = enable + gl.uniform1i(enablePickModeId, if (enable) 1 else 0) + } + } + fun enableTexture(enable: Boolean) { + if (enableTexture != enable) { + enableTexture = enable + gl.uniform1i(enableTextureId, if (enable) 1 else 0) + } + } + + fun loadABCD(a : Vec2, b : Vec2, c : Vec2, d : Vec2) + { + gl.uniform2f(AId, a.x.toFloat(), a.y.toFloat()); + gl.uniform2f(BId, b.x.toFloat(), b.y.toFloat()); + gl.uniform2f(CId, c.x.toFloat(), c.y.toFloat()); + gl.uniform2f(DId, d.x.toFloat(), d.y.toFloat()); + } + + fun loadTexCoordMatrix(matrix: Matrix3) { + if (texCoordMatrix != matrix) { + texCoordMatrix.copy(matrix) + matrix.transposeToArray(array, 0) + gl.uniformMatrix3fv(texCoordMatrixId, 1, false, array, 0) + } + } + fun loadModelviewProjection(matrix: Matrix4) { + // Don't bother testing whether mvpMatrix has changed, the common case is to load a different matrix. + matrix.transposeToArray(array, 0) + gl.uniformMatrix4fv(mvpMatrixId, 1, false, array, 0) + } + + fun loadColor(color: Color) { + if (this.color != color) { + this.color.copy(color) + val alpha = color.alpha + gl.uniform4f(colorId, color.red * alpha, color.green * alpha, color.blue * alpha, alpha) + } + } + + fun loadOpacity(opacity: Float) { + if (this.opacity != opacity) { + this.opacity = opacity + gl.uniform1f(opacityId, opacity) + } + } + + companion object { + val KEY = SurfaceQuadShaderProgram::class + } +} \ No newline at end of file diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/shape/TextureQuad.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/shape/TextureQuad.kt new file mode 100644 index 000000000..cc0366f09 --- /dev/null +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/shape/TextureQuad.kt @@ -0,0 +1,466 @@ +package earth.worldwind.shape + +import earth.worldwind.draw.DrawQuadState +import earth.worldwind.draw.DrawShapeState +import earth.worldwind.draw.Drawable +import earth.worldwind.draw.DrawableSurfaceQuad +import earth.worldwind.draw.DrawableSurfaceShape +import earth.worldwind.geom.* +import earth.worldwind.globe.Globe +import earth.worldwind.render.RenderContext +import earth.worldwind.render.buffer.BufferObject +import earth.worldwind.render.program.SurfaceQuadShaderProgram +import earth.worldwind.render.program.TriangleShaderProgram +import earth.worldwind.util.Logger.ERROR +import earth.worldwind.util.Logger.logMessage +import earth.worldwind.util.NumericArray +import earth.worldwind.util.kgl.GL_ARRAY_BUFFER +import earth.worldwind.util.kgl.GL_ELEMENT_ARRAY_BUFFER +import earth.worldwind.util.kgl.GL_TRIANGLES +import earth.worldwind.util.kgl.GL_UNSIGNED_INT +import kotlin.jvm.JvmOverloads +import earth.worldwind.render.image.ImageSource +import earth.worldwind.shape.Polygon.Companion.VERTEX_ORIGINAL +import earth.worldwind.shape.Polygon.Companion.refAlt +import earth.worldwind.util.math.encodeOrientationVector + +open class TextureQuad @JvmOverloads constructor( + bottomLeft : Location, + bottomRight: Location, + topRight : Location, + topLeft : Location, + attributes: ShapeAttributes = ShapeAttributes() +): AbstractShape(attributes) +{ + constructor(bottomLeft : Location, + bottomRight: Location, + topRight : Location, + topLeft : Location, + imageSource: ImageSource) : this(bottomLeft, bottomRight, topRight, topLeft, + ShapeAttributes().apply { + interiorImageSource = imageSource + isDrawOutline = false + } + ) + + constructor(sector: Sector, imageSource: ImageSource) : this( + Location(sector.minLatitude, sector.minLongitude), + Location(sector.minLatitude, sector.maxLongitude), + Location(sector.maxLatitude, sector.maxLongitude), + Location(sector.maxLatitude, sector.minLongitude), + ShapeAttributes().apply { + interiorImageSource = imageSource + isDrawOutline = false + } + ) + + constructor(sector: Sector, attributes: ShapeAttributes = ShapeAttributes()) : this( + Location(sector.minLatitude, sector.minLongitude), + Location(sector.minLatitude, sector.maxLongitude), + Location(sector.maxLatitude, sector.maxLongitude), + Location(sector.maxLatitude, sector.minLongitude), + attributes + ) + + override val referencePosition: Position get() { + val sector = Sector() + for (position in locations) sector.union(position) + return Position(sector.centroidLatitude, sector.centroidLongitude, 0.0) + } + protected val locations = arrayOf(bottomLeft, bottomRight, topRight, topLeft) + protected val data = mutableMapOf() + + open class TextureQuadData { + val vertexOrigin = Vec3() + var vertexArray = FloatArray(0) + val vertexBufferKey = Any() + var refreshVertexArray = true + + var A = Vec2() + var B = Vec2() + var C = Vec2() + var D = Vec2() + + var lineVertexArray = FloatArray(0) + val outlineElements = mutableListOf() + val vertexLinesBufferKey = Any() + val elementLinesBufferKey = Any() + var refreshLineVertexArray = true + } + + companion object { + protected const val VERTEX_STRIDE = 3 + protected const val VERTEX_LINE_STRIDE = 5 + protected const val OUTLINE_LINE_SEGMENT_STRIDE = 4 * VERTEX_LINE_STRIDE + protected val SHARED_INDEX_ARRAY = intArrayOf(0, 1, 2, 2, 3, 0) + private var sharedIndexBufferKey = Any() + private var sharedIndexBuffer: BufferObject? = null + + protected lateinit var currentData: TextureQuadData + + protected var vertexIndex = 0 + + protected var lineVertexIndex = 0 + protected val prevPoint = Vec3() + protected var texCoord1d = 0.0 + + fun getSharedIndexBuffer(rc: RenderContext): BufferObject? + { + sharedIndexBuffer = rc.getBufferObject(sharedIndexBufferKey) { + BufferObject(GL_ELEMENT_ARRAY_BUFFER, 0) + } + + rc.offerGLBufferUpload(sharedIndexBufferKey, 0) { + NumericArray.Ints(SHARED_INDEX_ARRAY) + } + + return sharedIndexBuffer + } + } + + init { + altitudeMode = AltitudeMode.CLAMP_TO_GROUND + isFollowTerrain = true + } + + fun getAllLocations(): Array { + return this.locations + } + + fun getLocation(index: Int): Location { + require(index in locations.indices) { + logMessage(ERROR, "TextureQuad", "getLocation", "invalidIndex") + } + return locations[index] + } + + fun setLocation(index: Int, location: Location) { + require(index in locations.indices) { + logMessage(ERROR, "TextureQuad", "setLocation", "invalidIndex") + } + reset() + + locations[index] = location + } + + fun setLocations(bottomLeft : Location, + bottomRight: Location, + topRight : Location, + topLeft : Location) { + reset() + locations[0] = bottomLeft + locations[1] = bottomRight + locations[2] = topRight + locations[3] = topLeft + } + + override fun resetGlobeState(globeState: Globe.State?) { + super.resetGlobeState(globeState) + data[globeState]?.let { + it.refreshVertexArray = true + } + } + + override fun reset() { + super.reset() + data.values.forEach { it.refreshVertexArray = true } + } + + override fun moveTo(globe: Globe, position: Position) { + val refPos = referencePosition + for (pos in locations) { + val distance = refPos.greatCircleDistance(pos) + val azimuth = refPos.greatCircleAzimuth(pos) + position.greatCircleLocation(azimuth, distance, pos) + } + reset() + } + + override fun makeDrawable(rc: RenderContext) { + if (locations.isEmpty()) return // nothing to draw + + if (mustAssembleGeometry(rc)) assembleGeometry(rc) + + // Obtain a drawable form the render context pool. + val drawable: Drawable + val drawState: DrawQuadState + val drawableLines: Drawable + val drawStateLines: DrawQuadState + val cameraDistance: Double + if (isSurfaceShape) { + val pool = rc.getDrawablePool(DrawableSurfaceQuad.KEY) + drawable = DrawableSurfaceQuad.obtain(pool) + drawState = drawable.drawState + drawable.offset = rc.globe.offset + drawable.sector.copy(currentBoundindData.boundingSector) + drawable.version = computeVersion() + drawable.isDynamic = isDynamic || rc.currentLayer.isDynamic + } else { + error("TextureQuad must be surface shape") + } + + drawInterior(rc, drawState) + rc.offerSurfaceDrawable(drawable, zOrder) + + if(activeAttributes.isDrawOutline) { + drawableLines = DrawableSurfaceQuad.obtain(rc.getDrawablePool(DrawableSurfaceQuad.KEY)) + drawableLines.offset = rc.globe.offset + drawableLines.sector.copy(currentBoundindData.boundingSector) + drawableLines.version = computeVersion() + drawableLines.isDynamic = isDynamic || rc.currentLayer.isDynamic + + cameraDistance = cameraDistanceGeographic(rc, currentBoundindData.boundingSector) + drawStateLines = drawableLines.drawState + + drawOutline(rc, drawStateLines, cameraDistance) + rc.offerSurfaceDrawable(drawableLines, zOrder) + } + } + + protected open fun drawInterior(rc: RenderContext, drawState: DrawQuadState) { + if (!activeAttributes.isDrawInterior || rc.isPickMode && !activeAttributes.isPickInterior) return + + drawState.isLine = false + + // Use the basic GLSL program to draw the shape. + drawState.programDrawToTexture = rc.getShaderProgram(SurfaceQuadShaderProgram.KEY) { SurfaceQuadShaderProgram() } + drawState.programDrawTextureToTerrain = rc.getShaderProgram(TriangleShaderProgram.KEY) { TriangleShaderProgram() } + + // Assemble the drawable's OpenGL vertex buffer object. + drawState.vertexBuffer = rc.getBufferObject(currentData.vertexBufferKey) { + BufferObject(GL_ARRAY_BUFFER, 0) + } + rc.offerGLBufferUpload(currentData.vertexBufferKey, bufferDataVersion) { + NumericArray.Floats(currentData.vertexArray) + } + + // Use shared index(element) buffer for all TextureQuads + drawState.elementBuffer = getSharedIndexBuffer(rc) + + // Configure the drawable to use the interior texture when drawing the interior. + activeAttributes.interiorImageSource?.let { interiorImageSource -> + rc.getTexture(interiorImageSource)?.let { texture -> + drawState.texture = texture + drawState.textureLod = 0; + drawState.texCoordMatrix.copy(texture.coordTransform) + } + } ?: run { drawState.texture = null } + + // Configure the drawable to display the shape's interior top. + drawState.A.copy(currentData.A) + drawState.B.copy(currentData.B) + drawState.C.copy(currentData.C) + drawState.D.copy(currentData.D) + drawState.color.copy(if (rc.isPickMode) pickColor else activeAttributes.interiorColor) + drawState.opacity = if (rc.isPickMode) 1f else rc.currentLayer.opacity + drawState.vertexOrigin.copy(currentData.vertexOrigin) + drawState.vertexStride = VERTEX_STRIDE * 4 // stride in bytes + drawState.enableCullFace = isExtrude + drawState.enableDepthTest = activeAttributes.isDepthTest + drawState.enableLighting = activeAttributes.isLightingEnabled + drawState.drawElements(GL_TRIANGLES, SHARED_INDEX_ARRAY.size, GL_UNSIGNED_INT, offset = 0) + } + + protected open fun drawOutline(rc: RenderContext, drawState: DrawQuadState, cameraDistance: Double) { + if (!activeAttributes.isDrawOutline || rc.isPickMode && !activeAttributes.isPickOutline) return + + // Use triangles mode to draw lines + drawState.isLine = true + + // Use the geom lines GLSL program to draw the shape. + drawState.programDrawToTexture = rc.getShaderProgram(TriangleShaderProgram.KEY) { TriangleShaderProgram() } + drawState.programDrawTextureToTerrain = rc.getShaderProgram(TriangleShaderProgram.KEY) { TriangleShaderProgram() } + + // Assemble the drawable's OpenGL vertex buffer object. + drawState.vertexBuffer = rc.getBufferObject(currentData.vertexLinesBufferKey) { + BufferObject(GL_ARRAY_BUFFER, 0) + } + rc.offerGLBufferUpload(currentData.vertexLinesBufferKey, bufferDataVersion) { + NumericArray.Floats(currentData.lineVertexArray) + } + + // Assemble the drawable's OpenGL element buffer object. + drawState.elementBuffer = rc.getBufferObject(currentData.elementLinesBufferKey) { + BufferObject(GL_ELEMENT_ARRAY_BUFFER, 0) + } + rc.offerGLBufferUpload(currentData.elementLinesBufferKey, bufferDataVersion) { + val array = IntArray(currentData.outlineElements.size) + var index = 0 + for (element in currentData.outlineElements) array[index++] = element + NumericArray.Ints(array) + } + + // Configure the drawable to use the outline texture when drawing the outline. + activeAttributes.outlineImageSource?.let { outlineImageSource -> + rc.getTexture(outlineImageSource, defaultOutlineImageOptions)?.let { texture -> + drawState.texture = texture + drawState.textureLod = computeRepeatingTexCoordTransform(rc, texture, cameraDistance, drawState.texCoordMatrix) + } + } ?: run { drawState.texture = null } + + // Configure the drawable to display the shape's outline. + drawState.color.copy(if (rc.isPickMode) pickColor else activeAttributes.outlineColor) + drawState.opacity = if (rc.isPickMode) 1f else rc.currentLayer.opacity + drawState.lineWidth = activeAttributes.outlineWidth + drawState.vertexOrigin.copy(currentData.vertexOrigin) + drawState.enableCullFace = false + drawState.enableDepthTest = activeAttributes.isDepthTest + drawState.enableLighting = activeAttributes.isLightingEnabled + drawState.drawElements(GL_TRIANGLES, currentData.outlineElements.size, GL_UNSIGNED_INT, offset = 0) + } + + protected open fun mustAssembleGeometry(rc: RenderContext): Boolean { + currentData = data[rc.globeState] ?: TextureQuadData().also { data[rc.globeState] = it } + return currentData.refreshVertexArray || isExtrude && !isSurfaceShape + } + + protected open fun assembleGeometry(rc: RenderContext) { + // Clear the shape's vertex array and element arrays. These arrays will accumulate values as the shapes's + // geometry is assembled. + vertexIndex = 0 + currentData.vertexArray = FloatArray(locations.size * VERTEX_STRIDE) // Reserve boundaries.size for combined vertexes + + computeQuadLocalCorners(rc) + + // Add the remaining boundary vertices, tessellating each edge as indicated by the polygon's properties. + for (pos in locations) { + addVertex(rc, pos.latitude, pos.longitude) + } + + if(activeAttributes.isDrawOutline) { + lineVertexIndex = 0 + val lastEqualsFirst = locations.first() == locations.last() + val lineVertexCount = locations.size + if (lastEqualsFirst) 2 else 3 + currentData.lineVertexArray = FloatArray(lineVertexCount * OUTLINE_LINE_SEGMENT_STRIDE) + currentData.outlineElements.clear() + + var pos0 = locations[0] + var begin = pos0 + addLineVertex(rc, begin.latitude, begin.longitude, isIntermediate = true, addIndices = true) + addLineVertex(rc, begin.latitude, begin.longitude, isIntermediate = false, addIndices = true) + for (idx in 1 until locations.size) { + val end = locations[idx] + val addIndices = idx != locations.size - 1 || end != pos0 // check if there is implicit closing edge + calcPoint(rc, end.latitude, end.longitude, 0.0, isAbsolute = false, isExtrudedSkirt = false) + addLineVertex(rc, end.latitude, end.longitude, isIntermediate = false, addIndices) + begin = end + } + + if (begin != pos0) { + // Add additional dummy vertex with the same data after the last vertex. + calcPoint(rc, pos0.latitude, pos0.longitude, 0.0, isAbsolute = false, isExtrudedSkirt = false) + addLineVertex(rc, pos0.latitude, pos0.longitude, isIntermediate = true, addIndices = false) + addLineVertex(rc, pos0.latitude, pos0.longitude, isIntermediate = true, addIndices = false) + } else { + calcPoint(rc, begin.latitude, begin.longitude, 0.0, isAbsolute = false, isExtrudedSkirt = false) + addLineVertex(rc, begin.latitude, begin.longitude, isIntermediate = true, addIndices = false) + } + // Drop last six indices as they are used for connecting segments and there's no next segment for last vertices (check addLineVertex) + currentData.outlineElements.subList(currentData.outlineElements.size - 6, currentData.outlineElements.size).clear() + + } + + // Reset update flags + currentData.refreshVertexArray = false + currentData.refreshLineVertexArray = false + + // Compute the shape's bounding box or bounding sector from its assembled coordinates. + with(currentBoundindData) { + if (isSurfaceShape) { + boundingSector.setEmpty() + boundingSector.union(currentData.vertexArray, vertexIndex, VERTEX_STRIDE) + boundingSector.translate(currentData.vertexOrigin.y, currentData.vertexOrigin.x) + boundingBox.setToUnitBox() // Surface/geographic shape bounding box is unused + } + else + { + error("Texture quad must be surface shape") + } + } + + // Adjust final vertex array size to save memory (and fix cameraDistanceCartesian calculation) + currentData.vertexArray = currentData.vertexArray.copyOf(vertexIndex) + } + + protected open fun addVertex( + rc: RenderContext, latitude: Angle, longitude: Angle + ): Int = with(currentData) { + val vertex = vertexIndex / VERTEX_STRIDE + + if (vertex == 0) { + vertexOrigin.set(longitude.inDegrees, latitude.inDegrees, 0.0) + } + vertexArray[vertexIndex++] = (longitude.inDegrees - vertexOrigin.x).toFloat() + vertexArray[vertexIndex++] = (latitude.inDegrees - vertexOrigin.y).toFloat() + vertexArray[vertexIndex++] = 0.0f + vertex + } + + protected open fun addLineVertex( + rc: RenderContext, latitude: Angle, longitude: Angle, isIntermediate : Boolean, addIndices : Boolean + ) = with(currentData) { + val vertex = lineVertexIndex / VERTEX_LINE_STRIDE + if (lineVertexIndex == 0) texCoord1d = 0.0 else texCoord1d += point.distanceTo(prevPoint) + prevPoint.copy(point) + val upperLeftCorner = encodeOrientationVector(-1f, 1f) + val lowerLeftCorner = encodeOrientationVector(-1f, -1f) + val upperRightCorner = encodeOrientationVector(1f, 1f) + val lowerRightCorner = encodeOrientationVector(1f, -1f) + if (isSurfaceShape) { + lineVertexArray[lineVertexIndex++] = (longitude.inDegrees - vertexOrigin.x).toFloat() + lineVertexArray[lineVertexIndex++] = (latitude.inDegrees - vertexOrigin.y).toFloat() + lineVertexArray[lineVertexIndex++] = 0.0f + lineVertexArray[lineVertexIndex++] = upperLeftCorner + lineVertexArray[lineVertexIndex++] = texCoord1d.toFloat() + + lineVertexArray[lineVertexIndex++] = (longitude.inDegrees - vertexOrigin.x).toFloat() + lineVertexArray[lineVertexIndex++] = (latitude.inDegrees - vertexOrigin.y).toFloat() + lineVertexArray[lineVertexIndex++] = 0.0f + lineVertexArray[lineVertexIndex++] = lowerLeftCorner + lineVertexArray[lineVertexIndex++] = texCoord1d.toFloat() + + lineVertexArray[lineVertexIndex++] = (longitude.inDegrees - vertexOrigin.x).toFloat() + lineVertexArray[lineVertexIndex++] = (latitude.inDegrees - vertexOrigin.y).toFloat() + lineVertexArray[lineVertexIndex++] = 0.0f + lineVertexArray[lineVertexIndex++] = upperRightCorner + lineVertexArray[lineVertexIndex++] = texCoord1d.toFloat() + + lineVertexArray[lineVertexIndex++] = (longitude.inDegrees - vertexOrigin.x).toFloat() + lineVertexArray[lineVertexIndex++] = (latitude.inDegrees - vertexOrigin.y).toFloat() + lineVertexArray[lineVertexIndex++] = 0.0f + lineVertexArray[lineVertexIndex++] = lowerRightCorner + lineVertexArray[lineVertexIndex++] = texCoord1d.toFloat() + if (addIndices) { + // indices for triangles made from this segment vertices + outlineElements.add(vertex) + outlineElements.add(vertex + 1) + outlineElements.add(vertex + 2) + outlineElements.add(vertex + 2) + outlineElements.add(vertex + 1) + outlineElements.add(vertex + 3) + // indices for triangles made from last vertices of this segment and first vertices of next segment + outlineElements.add(vertex + 2) + outlineElements.add(vertex + 3) + outlineElements.add(vertex + 4) + outlineElements.add(vertex + 4) + outlineElements.add(vertex + 3) + outlineElements.add(vertex + 5) + } + } + } + fun computeQuadLocalCorners(rc: RenderContext) { + currentData.vertexOrigin.set(locations[0].longitude.inDegrees, locations[0].latitude.inDegrees, 1.0) + currentData.A = Vec2( + locations[0].longitude.inDegrees - currentData.vertexOrigin.x, + locations[0].latitude.inDegrees - currentData.vertexOrigin.y) + currentData.B = Vec2( + locations[1].longitude.inDegrees - currentData.vertexOrigin.x, + locations[1].latitude.inDegrees - currentData.vertexOrigin.y) + currentData.C = Vec2( + locations[2].longitude.inDegrees - currentData.vertexOrigin.x, + locations[2].latitude.inDegrees - currentData.vertexOrigin.y) + currentData.D = Vec2( + locations[3].longitude.inDegrees - currentData.vertexOrigin.x, + locations[3].latitude.inDegrees - currentData.vertexOrigin.y) + } +} \ No newline at end of file