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 TestPlacemark Select and Drag
+ Texture quad exampleKML DemonstrationGeoJSON DemonstrationPlacemarks 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 GraticuleSurface Image
+ Texture QuadWMS LayerWMTS LayerWCS 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