From 90cdd051b3c2d5a99590a2b2996bd8038647ccea Mon Sep 17 00:00:00 2001 From: Zach Glueckert Date: Thu, 1 Feb 2018 14:35:53 -0600 Subject: [PATCH] Add Ellipse interior/outline image support (#211) * Add texture capabilities * Add Ellipse texture fill to Shapes Dash and Fill tutorial * Refactor disparate texture coordinate matrix computation to AbstractShape --- .../src/main/assets/shapes_dash_and_fill.html | 19 ++-- .../worldwindx/ShapesDashAndFillFragment.java | 18 ++-- .../nasa/worldwind/shape/AbstractShape.java | 12 +++ .../gov/nasa/worldwind/shape/Ellipse.java | 102 +++++++++++++++--- .../java/gov/nasa/worldwind/shape/Path.java | 4 +- .../gov/nasa/worldwind/shape/Polygon.java | 10 +- 6 files changed, 117 insertions(+), 48 deletions(-) diff --git a/worldwind-tutorials/src/main/assets/shapes_dash_and_fill.html b/worldwind-tutorials/src/main/assets/shapes_dash_and_fill.html index 1d0649ed7..7e06cc79a 100644 --- a/worldwind-tutorials/src/main/assets/shapes_dash_and_fill.html +++ b/worldwind-tutorials/src/main/assets/shapes_dash_and_fill.html @@ -29,7 +29,7 @@

Shapes Dash and Fill

  • The westernmost Path specifies a dash pattern and factor.
  • The middle Path modifies the dash factor from the western example.
  • The easternmost Path follows terrain and uses a different pattern.
  • -
  • The northern Polygon specifies a repeating fill using an image.
  • +
  • The Ellipse specifies a repeating fill using an image.
  • The southern Polygon uses the same repeating fill, but follows terrain and specifies a dash pattern for the outline.
  • @@ -107,18 +107,13 @@

    ShapesDashAndFillFragment.java

    path.setFollowTerrain(true); layer.addRenderable(path); - // Create a polygon using an image as a repeating fill pattern. - positions = Arrays.asList( - Position.fromDegrees(50.0, -70.0, 1e5), - Position.fromDegrees(35.0, -85.0, 1e5), - Position.fromDegrees(35.0, -55.0, 1e5) - ); - Polygon polygon = new Polygon(positions); + // Create an Ellipse using an image as a repeating fill pattern + Position ellipseCenter = new Position(40, -70.0, 1e5); + Ellipse ellipse = new Ellipse(ellipseCenter, 1.5e6, 800e3); sa = new ShapeAttributes(thickenLine); sa.setInteriorImageSource(ImageSource.fromResource(R.drawable.pattern_sample_houndstooth)); - sa.setInteriorColor(new gov.nasa.worldwind.render.Color(1f, 1f, 1f, 1f)); - polygon.setAttributes(sa); - layer.addRenderable(polygon); + ellipse.setAttributes(sa); + layer.addRenderable(ellipse); // Create a surface polygon using an image as a repeating fill pattern and a dash pattern for the outline // of the polygon. @@ -128,7 +123,7 @@

    ShapesDashAndFillFragment.java

    Position.fromDegrees(10.0, -60.0, 0.0), Position.fromDegrees(25.0, -55.0, 0.0) ); - polygon = new Polygon(positions); + Polygon polygon = new Polygon(positions); sa = new ShapeAttributes(thickenLine); sa.setInteriorImageSource(ImageSource.fromResource(R.drawable.pattern_sample_houndstooth)); sa.setOutlineImageSource(ImageSource.fromLineStipple(8, (short) 0xDFF6)); diff --git a/worldwind-tutorials/src/main/java/gov/nasa/worldwindx/ShapesDashAndFillFragment.java b/worldwind-tutorials/src/main/java/gov/nasa/worldwindx/ShapesDashAndFillFragment.java index e9a3a3a1d..d5568ca90 100644 --- a/worldwind-tutorials/src/main/java/gov/nasa/worldwindx/ShapesDashAndFillFragment.java +++ b/worldwind-tutorials/src/main/java/gov/nasa/worldwindx/ShapesDashAndFillFragment.java @@ -13,6 +13,7 @@ import gov.nasa.worldwind.geom.Position; import gov.nasa.worldwind.layer.RenderableLayer; import gov.nasa.worldwind.render.ImageSource; +import gov.nasa.worldwind.shape.Ellipse; import gov.nasa.worldwind.shape.Path; import gov.nasa.worldwind.shape.Polygon; import gov.nasa.worldwind.shape.ShapeAttributes; @@ -79,18 +80,13 @@ public WorldWindow createWorldWindow() { path.setFollowTerrain(true); layer.addRenderable(path); - // Create a polygon using an image as a repeating fill pattern. - positions = Arrays.asList( - Position.fromDegrees(50.0, -70.0, 1e5), - Position.fromDegrees(35.0, -85.0, 1e5), - Position.fromDegrees(35.0, -55.0, 1e5) - ); - Polygon polygon = new Polygon(positions); + // Create an Ellipse using an image as a repeating fill pattern + Position ellipseCenter = new Position(40, -70.0, 1e5); + Ellipse ellipse = new Ellipse(ellipseCenter, 1.5e6, 800e3); sa = new ShapeAttributes(thickenLine); sa.setInteriorImageSource(ImageSource.fromResource(R.drawable.pattern_sample_houndstooth)); - sa.setInteriorColor(new gov.nasa.worldwind.render.Color(1f, 1f, 1f, 1f)); - polygon.setAttributes(sa); - layer.addRenderable(polygon); + ellipse.setAttributes(sa); + layer.addRenderable(ellipse); // Create a surface polygon using an image as a repeating fill pattern and a dash pattern for the outline // of the polygon. @@ -100,7 +96,7 @@ public WorldWindow createWorldWindow() { Position.fromDegrees(10.0, -60.0, 0.0), Position.fromDegrees(25.0, -55.0, 0.0) ); - polygon = new Polygon(positions); + Polygon polygon = new Polygon(positions); sa = new ShapeAttributes(thickenLine); sa.setInteriorImageSource(ImageSource.fromResource(R.drawable.pattern_sample_houndstooth)); sa.setOutlineImageSource(ImageSource.fromLineStipple(8, (short) 0xDFF6)); diff --git a/worldwind/src/main/java/gov/nasa/worldwind/shape/AbstractShape.java b/worldwind/src/main/java/gov/nasa/worldwind/shape/AbstractShape.java index a03495ded..b6ed53020 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/shape/AbstractShape.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/shape/AbstractShape.java @@ -8,11 +8,13 @@ import gov.nasa.worldwind.PickedObject; import gov.nasa.worldwind.WorldWind; import gov.nasa.worldwind.geom.BoundingBox; +import gov.nasa.worldwind.geom.Matrix3; import gov.nasa.worldwind.geom.Sector; import gov.nasa.worldwind.geom.Vec3; import gov.nasa.worldwind.render.AbstractRenderable; import gov.nasa.worldwind.render.Color; import gov.nasa.worldwind.render.RenderContext; +import gov.nasa.worldwind.render.Texture; import gov.nasa.worldwind.util.WWMath; public abstract class AbstractShape extends AbstractRenderable implements Attributable, Highlightable { @@ -183,6 +185,16 @@ protected double cameraDistanceCartesian(RenderContext rc, float[] array, int co return Math.sqrt(minDistance2); } + protected Matrix3 computeRepeatingTexCoordTransform(Texture texture, double metersPerPixel, Matrix3 result) { + Matrix3 texCoordMatrix = result.setToIdentity(); + texCoordMatrix.setScale( + 1.0 / (texture.getWidth() * metersPerPixel), + 1.0 / (texture.getHeight() * metersPerPixel)); + texCoordMatrix.multiplyByMatrix(texture.getTexCoordTransform()); + + return texCoordMatrix; + } + protected abstract void reset(); protected abstract void makeDrawable(RenderContext rc); diff --git a/worldwind/src/main/java/gov/nasa/worldwind/shape/Ellipse.java b/worldwind/src/main/java/gov/nasa/worldwind/shape/Ellipse.java index 13275e175..c81422da7 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/shape/Ellipse.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/shape/Ellipse.java @@ -19,12 +19,16 @@ import gov.nasa.worldwind.draw.DrawableShape; import gov.nasa.worldwind.draw.DrawableSurfaceShape; import gov.nasa.worldwind.geom.Location; +import gov.nasa.worldwind.geom.Matrix3; +import gov.nasa.worldwind.geom.Matrix4; import gov.nasa.worldwind.geom.Position; import gov.nasa.worldwind.geom.Range; import gov.nasa.worldwind.geom.Vec3; import gov.nasa.worldwind.render.BasicShaderProgram; import gov.nasa.worldwind.render.BufferObject; +import gov.nasa.worldwind.render.ImageOptions; import gov.nasa.worldwind.render.RenderContext; +import gov.nasa.worldwind.render.Texture; import gov.nasa.worldwind.util.Logger; import gov.nasa.worldwind.util.Pool; import gov.nasa.worldwind.util.ShortArray; @@ -81,6 +85,10 @@ public class Ellipse extends AbstractShape { */ protected static final int SIDE_RANGE = 2; + protected static final ImageOptions defaultInteriorImageOptions = new ImageOptions(); + + protected static final ImageOptions defaultOutlineImageOptions = new ImageOptions(); + /** * Simple interval count based cache of the keys for element buffers. Element buffers are dependent only on the * number of intervals so the keys are cached here. The element buffer object itself is in the @@ -145,12 +153,28 @@ public class Ellipse extends AbstractShape { protected boolean isSurfaceShape; + protected double texCoord1d; + + protected Vec3 texCoord2d = new Vec3(); + + protected Matrix3 texCoordMatrix = new Matrix3(); + + protected Matrix4 modelToTexCoord = new Matrix4(); + protected double cameraDistance; + protected Vec3 prevPoint = new Vec3(); + private static Position scratchPosition = new Position(); private static Vec3 scratchPoint = new Vec3(); + static { + defaultInteriorImageOptions.wrapMode = WorldWind.REPEAT; + defaultOutlineImageOptions.resamplingMode = WorldWind.NEAREST_NEIGHBOR; + defaultOutlineImageOptions.wrapMode = WorldWind.REPEAT; + } + /** * Constructs an ellipse with a null center position, and with major- and minor-radius both 0.0. This ellipse does * not display until the center position is defined and the radii are both greater than 0.0. @@ -378,7 +402,7 @@ public Ellipse setFollowTerrain(boolean followTerrain) { * Indicates the maximum number of angular intervals that may be used to approximate this ellipse's geometry on * screen. * - * @return the maximum number of angular intervals + * @return the number of angular intervals */ public int getMaximumIntervals() { return this.maximumIntervals; @@ -435,6 +459,7 @@ protected void makeDrawable(RenderContext rc) { drawable = DrawableSurfaceShape.obtain(pool); drawState = ((DrawableSurfaceShape) drawable).drawState; ((DrawableSurfaceShape) drawable).sector.set(this.boundingSector); + this.cameraDistance = this.cameraDistanceGeographic(rc, this.boundingSector); } else { Pool pool = rc.getDrawablePool(DrawableShape.class); drawable = DrawableShape.obtain(pool); @@ -498,7 +523,21 @@ protected void drawInterior(RenderContext rc, DrawShapeState drawState) { return; } - drawState.texture(null); + // Configure the drawable to use the interior texture when drawing the interior. + if (this.activeAttributes.interiorImageSource != null) { + Texture texture = rc.getTexture(this.activeAttributes.interiorImageSource); + if (texture == null) { + texture = rc.retrieveTexture(this.activeAttributes.interiorImageSource, defaultInteriorImageOptions); + } + if (texture != null) { + double metersPerPixel = rc.pixelSizeAtDistance(this.cameraDistance); + this.computeRepeatingTexCoordTransform(texture, metersPerPixel, this.texCoordMatrix); + drawState.texture(texture); + drawState.texCoordMatrix(this.texCoordMatrix); + } + } else { + drawState.texture(null); + } // Configure the drawable to display the shape's interior. drawState.color(rc.pickMode ? this.pickColor : this.activeAttributes.interiorColor); @@ -509,6 +548,7 @@ protected void drawInterior(RenderContext rc, DrawShapeState drawState) { if (this.extrude) { Range side = drawState.elementBuffer.ranges.get(SIDE_RANGE); + drawState.texture(null); drawState.drawElements(GLES20.GL_TRIANGLE_STRIP, side.length(), GLES20.GL_UNSIGNED_SHORT, side.lower * 2); } @@ -519,7 +559,21 @@ protected void drawOutline(RenderContext rc, DrawShapeState drawState) { return; } - drawState.texture(null); + // Configure the drawable to use the outline texture when drawing the outline. + if (this.activeAttributes.outlineImageSource != null) { + Texture texture = rc.getTexture(this.activeAttributes.outlineImageSource); + if (texture == null) { + texture = rc.retrieveTexture(this.activeAttributes.outlineImageSource, defaultOutlineImageOptions); + } + if (texture != null) { + double metersPerPixel = rc.pixelSizeAtDistance(this.cameraDistance); + this.computeRepeatingTexCoordTransform(texture, metersPerPixel, this.texCoordMatrix); + drawState.texture(texture); + drawState.texCoordMatrix(this.texCoordMatrix); + } + } else { + drawState.texture(null); + } // Configure the drawable to display the shape's outline. drawState.color(rc.pickMode ? this.pickColor : this.activeAttributes.outlineColor); @@ -531,6 +585,9 @@ protected void drawOutline(RenderContext rc, DrawShapeState drawState) { if (this.activeAttributes.drawVerticals && this.extrude) { Range side = drawState.elementBuffer.ranges.get(SIDE_RANGE); + drawState.color(rc.pickMode ? this.pickColor : this.activeAttributes.outlineColor); + drawState.lineWidth(this.activeAttributes.outlineWidth); + drawState.texture(null); drawState.drawElements(GLES20.GL_LINES, side.length(), GLES20.GL_UNSIGNED_SHORT, side.lower * 2); } @@ -551,6 +608,9 @@ protected void assembleGeometry(RenderContext rc) { // Determine whether the shape geometry must be assembled as Cartesian geometry or as goegraphic geometry. this.isSurfaceShape = (this.altitudeMode == WorldWind.CLAMP_TO_GROUND) && this.followTerrain; + // Compute a matrix that transforms from Cartesian coordinates to shape texture coordinates. + this.determineModelToTexCoord(rc); + // Use the ellipse's center position as the local origin for vertex positions. if (this.isSurfaceShape) { this.vertexOrigin.set(this.center.longitude, this.center.latitude, this.center.altitude); @@ -563,9 +623,7 @@ protected void assembleGeometry(RenderContext rc) { int spineCount = computeNumberSpinePoints(this.activeIntervals); // activeIntervals must be even // Clear the shape's vertex array. The array will accumulate values as the shapes's geometry is assembled. - // Determine the offset from the top and extruded vertices this.vertexIndex = 0; - int arrayOffset = computeIndexOffset(this.activeIntervals) * VERTEX_STRIDE; if (this.extrude && !this.isSurfaceShape) { this.vertexArray = new float[(this.activeIntervals * 2 + spineCount) * VERTEX_STRIDE]; } else { @@ -589,6 +647,8 @@ protected void assembleGeometry(RenderContext rc) { minorArcRadians = this.majorRadius / globeRadius; } + // Determine the offset from the top and extruded vertices + int arrayOffset = computeIndexOffset(this.activeIntervals) * VERTEX_STRIDE; // Setup spine radius values int spineIdx = 0; double[] spineRadius = new double[spineCount]; @@ -637,7 +697,6 @@ protected static BufferObject assembleElements(int intervals) { // Generate the top element buffer with spine int interiorIdx = intervals; - int spinePoints = computeNumberSpinePoints(intervals); int offset = computeIndexOffset(intervals); // Add the anchor leg @@ -697,23 +756,32 @@ protected static BufferObject assembleElements(int intervals) { protected void addVertex(RenderContext rc, double latitude, double longitude, double altitude, int offset, boolean isExtrudedSkirt) { int offsetVertexIndex = this.vertexIndex + offset; + Vec3 point = rc.geographicToCartesian(latitude, longitude, altitude, this.altitudeMode, scratchPoint); + Vec3 texCoord2d = this.texCoord2d.set(point).multiplyByMatrix(this.modelToTexCoord); + + if (this.vertexIndex == 0) { + this.texCoord1d = 0; + this.prevPoint.set(point); + } else { + this.texCoord1d += point.distanceTo(this.prevPoint); + this.prevPoint.set(point); + } + if (this.isSurfaceShape) { this.vertexArray[this.vertexIndex++] = (float) (longitude - this.vertexOrigin.x); this.vertexArray[this.vertexIndex++] = (float) (latitude - this.vertexOrigin.y); this.vertexArray[this.vertexIndex++] = (float) (altitude - this.vertexOrigin.z); // reserved for future texture coordinate use - this.vertexArray[this.vertexIndex++] = 0; - this.vertexArray[this.vertexIndex++] = 0; - this.vertexArray[this.vertexIndex++] = 0; + this.vertexArray[this.vertexIndex++] = (float) texCoord2d.x; + this.vertexArray[this.vertexIndex++] = (float) texCoord2d.y; + this.vertexArray[this.vertexIndex++] = (float) this.texCoord1d; } else { - Vec3 point = rc.geographicToCartesian(latitude, longitude, altitude, this.altitudeMode, scratchPoint); this.vertexArray[this.vertexIndex++] = (float) (point.x - this.vertexOrigin.x); this.vertexArray[this.vertexIndex++] = (float) (point.y - this.vertexOrigin.y); this.vertexArray[this.vertexIndex++] = (float) (point.z - this.vertexOrigin.z); - // reserved for future texture coordinate use - this.vertexArray[this.vertexIndex++] = 0; - this.vertexArray[this.vertexIndex++] = 0; - this.vertexArray[this.vertexIndex++] = 0; + this.vertexArray[this.vertexIndex++] = (float) texCoord2d.x; + this.vertexArray[this.vertexIndex++] = (float) texCoord2d.y; + this.vertexArray[this.vertexIndex++] = (float) this.texCoord1d; if (isExtrudedSkirt) { point = rc.geographicToCartesian(latitude, longitude, 0, WorldWind.CLAMP_TO_GROUND, scratchPoint); @@ -727,6 +795,12 @@ protected void addVertex(RenderContext rc, double latitude, double longitude, do } } + protected void determineModelToTexCoord(RenderContext rc) { + Vec3 point = rc.geographicToCartesian(this.center.latitude, this.center.longitude, this.center.altitude, this.altitudeMode, scratchPoint); + this.modelToTexCoord = rc.globe.cartesianToLocalTransform(point.x, point.y, point.z, this.modelToTexCoord); + this.modelToTexCoord.invertOrthonormal(); + } + /** * Calculate the number of times to split the edges of the shape for geometry assembly. * diff --git a/worldwind/src/main/java/gov/nasa/worldwind/shape/Path.java b/worldwind/src/main/java/gov/nasa/worldwind/shape/Path.java index 12bcc614d..91eadadfd 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/shape/Path.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/shape/Path.java @@ -214,9 +214,7 @@ protected void makeDrawable(RenderContext rc) { } if (texture != null) { double metersPerPixel = rc.pixelSizeAtDistance(cameraDistance); - Matrix3 texCoordMatrix = this.texCoordMatrix.setToIdentity(); - texCoordMatrix.setScale(1.0 / (texture.getWidth() * metersPerPixel), 1.0); - texCoordMatrix.multiplyByMatrix(texture.getTexCoordTransform()); + this.computeRepeatingTexCoordTransform(texture, metersPerPixel, this.texCoordMatrix); drawState.texture(texture); drawState.texCoordMatrix(texCoordMatrix); } diff --git a/worldwind/src/main/java/gov/nasa/worldwind/shape/Polygon.java b/worldwind/src/main/java/gov/nasa/worldwind/shape/Polygon.java index 9255a7790..2b3db9669 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/shape/Polygon.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/shape/Polygon.java @@ -349,11 +349,7 @@ protected void drawInterior(RenderContext rc, DrawShapeState drawState) { } if (texture != null) { double metersPerPixel = rc.pixelSizeAtDistance(this.cameraDistance); - Matrix3 texCoordMatrix = this.texCoordMatrix.setToIdentity(); - texCoordMatrix.setScale( - 1.0 / (texture.getWidth() * metersPerPixel), - 1.0 / (texture.getHeight() * metersPerPixel)); - texCoordMatrix.multiplyByMatrix(texture.getTexCoordTransform()); + this.computeRepeatingTexCoordTransform(texture, metersPerPixel, this.texCoordMatrix); drawState.texture(texture); drawState.texCoordMatrix(texCoordMatrix); } @@ -388,9 +384,7 @@ protected void drawOutline(RenderContext rc, DrawShapeState drawState) { } if (texture != null) { double metersPerPixel = rc.pixelSizeAtDistance(this.cameraDistance); - Matrix3 texCoordMatrix = this.texCoordMatrix.setToIdentity(); - texCoordMatrix.setScale(1.0 / (texture.getWidth() * metersPerPixel), 1.0); - texCoordMatrix.multiplyByMatrix(texture.getTexCoordTransform()); + this.computeRepeatingTexCoordTransform(texture, metersPerPixel, this.texCoordMatrix); drawState.texture(texture); drawState.texCoordMatrix(texCoordMatrix); }