diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsConfig.java index 6c86a373d0..ba45c310ef 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsConfig.java @@ -30,6 +30,7 @@ import net.runelite.client.config.Config; import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigItem; import net.runelite.client.config.ConfigSection; +import net.runelite.client.config.Range; @ConfigGroup("npcindicators") public interface NpcIndicatorsConfig extends Config @@ -79,6 +80,59 @@ public interface NpcIndicatorsConfig extends Config @ConfigItem( position = 3, + keyName = "highlightOutline", + name = "Highlight outline", + description = "Configures whether or not the model of the NPC should be highlighted by outline", + section = renderStyleSection + ) + default boolean highlightOutline() + { + return false; + } + + @Alpha + @ConfigItem( + position = 4, + keyName = "npcColor", + name = "Highlight Color", + description = "Color of the NPC highlight", + section = renderStyleSection + ) + default Color getHighlightColor() + { + return Color.CYAN; + } + + @ConfigItem( + position = 5, + keyName = "borderWidth", + name = "Border Width", + description = "Width of the highlighted NPC border", + section = renderStyleSection + ) + default double borderWidth() + { + return 2; + } + + @ConfigItem( + position = 6, + keyName = "outlineFeather", + name = "Outline feather", + description = "Specify between 0-4 how much of the model outline should be faded", + section = renderStyleSection + ) + @Range( + min = 0, + max = 4 + ) + default int outlineFeather() + { + return 0; + } + + @ConfigItem( + position = 7, keyName = "npcToHighlight", name = "NPCs to Highlight", description = "List of NPC names to highlight" @@ -95,31 +149,8 @@ public interface NpcIndicatorsConfig extends Config ) void setNpcToHighlight(String npcsToHighlight); - @Alpha @ConfigItem( - position = 4, - keyName = "npcColor", - name = "Highlight Color", - description = "Color of the NPC highlight" - ) - default Color getHighlightColor() - { - return Color.CYAN; - } - - @ConfigItem( - position = 5, - keyName = "borderWidth", - name = "Border Width", - description = "Width of the highlighted NPC border" - ) - default double borderWidth() - { - return 2; - } - - @ConfigItem( - position = 6, + position = 8, keyName = "drawNames", name = "Draw names above NPC", description = "Configures whether or not NPC names should be drawn above the NPC" @@ -130,7 +161,7 @@ public interface NpcIndicatorsConfig extends Config } @ConfigItem( - position = 7, + position = 9, keyName = "drawMinimapNames", name = "Draw names on minimap", description = "Configures whether or not NPC names should be drawn on the minimap" @@ -141,7 +172,7 @@ public interface NpcIndicatorsConfig extends Config } @ConfigItem( - position = 8, + position = 10, keyName = "highlightMenuNames", name = "Highlight menu names", description = "Highlight NPC names in right click menu" @@ -152,7 +183,7 @@ public interface NpcIndicatorsConfig extends Config } @ConfigItem( - position = 9, + position = 11, keyName = "ignoreDeadNpcs", name = "Ignore dead NPCs", description = "Prevents highlighting NPCs after they are dead" @@ -163,7 +194,7 @@ public interface NpcIndicatorsConfig extends Config } @ConfigItem( - position = 10, + position = 12, keyName = "deadNpcMenuColor", name = "Dead NPC menu color", description = "Color of the NPC menus for dead NPCs" @@ -171,7 +202,7 @@ public interface NpcIndicatorsConfig extends Config Color deadNpcMenuColor(); @ConfigItem( - position = 11, + position = 13, keyName = "showRespawnTimer", name = "Show respawn timer", description = "Show respawn timer of tagged NPCs") diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcSceneOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcSceneOverlay.java index d07f53c2bf..7b7bf9b461 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcSceneOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcSceneOverlay.java @@ -48,6 +48,7 @@ import net.runelite.client.ui.overlay.Overlay; import net.runelite.client.ui.overlay.OverlayLayer; import net.runelite.client.ui.overlay.OverlayPosition; import net.runelite.client.ui.overlay.OverlayUtil; +import net.runelite.client.ui.overlay.outline.ModelOutlineRenderer; import net.runelite.client.util.ColorUtil; import net.runelite.client.util.Text; @@ -67,13 +68,16 @@ public class NpcSceneOverlay extends Overlay private final Client client; private final NpcIndicatorsConfig config; private final NpcIndicatorsPlugin plugin; + private final ModelOutlineRenderer modelOutlineRenderer; @Inject - NpcSceneOverlay(Client client, NpcIndicatorsConfig config, NpcIndicatorsPlugin plugin) + NpcSceneOverlay(Client client, NpcIndicatorsConfig config, NpcIndicatorsPlugin plugin, + ModelOutlineRenderer modelOutlineRenderer) { this.client = client; this.config = config; this.plugin = plugin; + this.modelOutlineRenderer = modelOutlineRenderer; setPosition(OverlayPosition.DYNAMIC); setLayer(OverlayLayer.ABOVE_SCENE); } @@ -181,6 +185,11 @@ public class NpcSceneOverlay extends Overlay renderPoly(graphics, color, southWestTilePoly); } + if (config.highlightOutline()) + { + modelOutlineRenderer.drawOutline(actor, (int)config.borderWidth(), color, config.outlineFeather()); + } + if (config.drawNames() && actor.getName() != null) { String npcName = Text.removeTags(actor.getName()); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsConfig.java index eacde6f23f..bc5d0e7751 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsConfig.java @@ -30,15 +30,50 @@ import net.runelite.client.config.Alpha; import net.runelite.client.config.Config; import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.ConfigSection; +import net.runelite.client.config.Range; @ConfigGroup("objectindicators") public interface ObjectIndicatorsConfig extends Config { + @ConfigSection( + name = "Render style", + description = "The render style of object highlighting", + position = 0 + ) + String renderStyleSection = "renderStyleSection"; + + @ConfigItem( + position = 0, + keyName = "highlightHull", + name = "Highlight hull", + description = "Configures whether or not object should be highlighted by hull", + section = renderStyleSection + ) + default boolean highlightHull() + { + return true; + } + + @ConfigItem( + position = 1, + keyName = "highlightOutline", + name = "Highlight outline", + description = "Configures whether or not the model of the object should be highlighted by outline", + section = renderStyleSection + ) + default boolean highlightOutline() + { + return false; + } + @Alpha @ConfigItem( + position = 2, keyName = "markerColor", name = "Marker color", - description = "Configures the color of object marker" + description = "Configures the color of object marker", + section = renderStyleSection ) default Color markerColor() { @@ -46,6 +81,35 @@ public interface ObjectIndicatorsConfig extends Config } @ConfigItem( + position = 3, + keyName = "borderWidth", + name = "Border Width", + description = "Width of the marked object border", + section = renderStyleSection + ) + default double borderWidth() + { + return 2; + } + + @ConfigItem( + position = 4, + keyName = "outlineFeather", + name = "Outline feather", + description = "Specify between 0-4 how much of the model outline should be faded", + section = renderStyleSection + ) + @Range( + min = 0, + max = 4 + ) + default int outlineFeather() + { + return 0; + } + + @ConfigItem( + position = 5, keyName = "rememberObjectColors", name = "Remember color per object", description = "Color objects using the color from time of marking" @@ -54,14 +118,4 @@ public interface ObjectIndicatorsConfig extends Config { return false; } - - @ConfigItem( - keyName = "borderWidth", - name = "Border Width", - description = "Width of the marked object border" - ) - default double borderWidth() - { - return 2; - } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsOverlay.java index a174e92335..087eff54f6 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsOverlay.java @@ -43,19 +43,23 @@ import net.runelite.client.ui.overlay.OverlayLayer; import net.runelite.client.ui.overlay.OverlayPosition; import net.runelite.client.ui.overlay.OverlayPriority; import net.runelite.client.ui.overlay.OverlayUtil; +import net.runelite.client.ui.overlay.outline.ModelOutlineRenderer; class ObjectIndicatorsOverlay extends Overlay { private final Client client; private final ObjectIndicatorsConfig config; private final ObjectIndicatorsPlugin plugin; + private final ModelOutlineRenderer modelOutlineRenderer; @Inject - private ObjectIndicatorsOverlay(Client client, ObjectIndicatorsConfig config, ObjectIndicatorsPlugin plugin) + private ObjectIndicatorsOverlay(Client client, ObjectIndicatorsConfig config, ObjectIndicatorsPlugin plugin, + ModelOutlineRenderer modelOutlineRenderer) { this.client = client; this.config = config; this.plugin = plugin; + this.modelOutlineRenderer = modelOutlineRenderer; setPosition(OverlayPosition.DYNAMIC); setPriority(OverlayPriority.LOW); setLayer(OverlayLayer.ABOVE_SCENE); @@ -93,43 +97,56 @@ class ObjectIndicatorsOverlay extends Overlay color = config.markerColor(); } - final Shape polygon; - Shape polygon2 = null; - - if (object instanceof GameObject) + if (config.highlightHull()) { - polygon = ((GameObject) object).getConvexHull(); - } - else if (object instanceof WallObject) - { - polygon = ((WallObject) object).getConvexHull(); - polygon2 = ((WallObject) object).getConvexHull2(); - } - else if (object instanceof DecorativeObject) - { - polygon = ((DecorativeObject) object).getConvexHull(); - polygon2 = ((DecorativeObject) object).getConvexHull2(); - } - else if (object instanceof GroundObject) - { - polygon = ((GroundObject) object).getConvexHull(); - } - else - { - polygon = object.getCanvasTilePoly(); + renderConvexHull(graphics, object, color, stroke); } - if (polygon != null) + if (config.highlightOutline()) { - OverlayUtil.renderPolygon(graphics, polygon, color, stroke); - } - - if (polygon2 != null) - { - OverlayUtil.renderPolygon(graphics, polygon2, color, stroke); + modelOutlineRenderer.drawOutline(object, (int)config.borderWidth(), color, config.outlineFeather()); } } return null; } + + private void renderConvexHull(Graphics2D graphics, TileObject object, Color color, Stroke stroke) + { + final Shape polygon; + Shape polygon2 = null; + + if (object instanceof GameObject) + { + polygon = ((GameObject) object).getConvexHull(); + } + else if (object instanceof WallObject) + { + polygon = ((WallObject) object).getConvexHull(); + polygon2 = ((WallObject) object).getConvexHull2(); + } + else if (object instanceof DecorativeObject) + { + polygon = ((DecorativeObject) object).getConvexHull(); + polygon2 = ((DecorativeObject) object).getConvexHull2(); + } + else if (object instanceof GroundObject) + { + polygon = ((GroundObject) object).getConvexHull(); + } + else + { + polygon = object.getCanvasTilePoly(); + } + + if (polygon != null) + { + OverlayUtil.renderPolygon(graphics, polygon, color, stroke); + } + + if (polygon2 != null) + { + OverlayUtil.renderPolygon(graphics, polygon2, color, stroke); + } + } } \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/outline/IntBlockBuffer.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/outline/IntBlockBuffer.java new file mode 100644 index 0000000000..500a297985 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/outline/IntBlockBuffer.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2021, Woox + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.ui.overlay.outline; + +import java.util.Arrays; + +/** + * A class which manages 1024-sized blocks of memory. + */ +class IntBlockBuffer +{ + public static final int BLOCK_BITS = 10; + public static final int BLOCK_SIZE = 1 << BLOCK_BITS; + + private int[] memory = new int[0]; + private int[] unusedBlockIndices = new int[0]; + private int unusedBlockIndicesLength; + + private void increaseBlockCount() + { + int currBlockCount = memory.length >> BLOCK_BITS; + int newBlockCount = Math.max(1, currBlockCount * 2); + memory = Arrays.copyOf(memory, newBlockCount * BLOCK_SIZE); + unusedBlockIndices = Arrays.copyOf(unusedBlockIndices, newBlockCount); + for (int i = currBlockCount; i < newBlockCount; i++) + { + unusedBlockIndices[unusedBlockIndicesLength++] = i; + } + } + + /** + * Retrieves the whole memory buffer. + */ + public int[] getMemory() + { + return memory; + } + + /** + * Marks a new block as used. + * + * @return The index of the block. + */ + public int useNewBlock() + { + if (unusedBlockIndicesLength == 0) + { + increaseBlockCount(); + } + + return unusedBlockIndices[--unusedBlockIndicesLength]; + } + + /** + * Marks a block as unused. + * + * @param index The index of the block. The block should be in use before calling this method. + */ + public void freeBlock(int index) + { + unusedBlockIndices[unusedBlockIndicesLength++] = index; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/outline/ModelOutlineRenderer.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/outline/ModelOutlineRenderer.java new file mode 100644 index 0000000000..8fd9daa4ae --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/outline/ModelOutlineRenderer.java @@ -0,0 +1,1188 @@ +/* + * Copyright (c) 2018-2021, Woox + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.ui.overlay.outline; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import net.runelite.api.Client; +import net.runelite.api.DecorativeObject; +import net.runelite.api.GameObject; +import net.runelite.api.GraphicsObject; +import net.runelite.api.GroundObject; +import net.runelite.api.ItemLayer; +import net.runelite.api.MainBufferProvider; +import net.runelite.api.Model; +import net.runelite.api.NPC; +import net.runelite.api.NPCComposition; +import net.runelite.api.Perspective; +import net.runelite.api.Player; +import net.runelite.api.Renderable; +import net.runelite.api.TileObject; +import net.runelite.api.WallObject; +import net.runelite.api.coords.LocalPoint; + +@Singleton +public class ModelOutlineRenderer +{ + @AllArgsConstructor + private static class PixelDistanceDelta + { + private final int dx; + private final int dy; + } + + @AllArgsConstructor + private static class PixelDistanceGroupIndex + { + @Getter(AccessLevel.PRIVATE) + private final double distance; + private final int distanceGroupIndex; + private final double alphaMultiply; + } + + private static final int MAX_OUTLINE_WIDTH = 50; + private static final int MAX_FEATHER = 4; + private static final int DIRECT_WRITE_OUTLINE_WIDTH_THRESHOLD = 10; + + private final Client client; + + // Vertex positions projected on the screen. + private final int[] projectedVerticesX = new int[6500]; + private final int[] projectedVerticesY = new int[6500]; + + // Window boundaries for the ingame world + private int clipX1; + private int clipY1; + private int clipX2; + private int clipY2; + + // Boundaries for the outline + private int croppedX1; + private int croppedY1; + private int croppedX2; + private int croppedY2; + private int croppedWidth; + private int croppedHeight; + + // Bitset with pixel positions that would be rendered to within the cropped area by the model. + private int[] visited = new int[0]; + + // Memory used for queueing the pixels for the outline of the model. + // Pixels are grouped by x and y distance to the closest pixel drawn on the model. + // A block buffer is used so memory can be reused after a group has been processed + // without using the JVM garbage collector. + private final IntBlockBuffer outlinePixelsBlockBuffer = new IntBlockBuffer(); + private int[][] outlinePixelsBlockIndices = new int[0][]; + private int[] outlinePixelsBlockIndicesLengths = new int[0]; + private int[] outlinePixelsLastBlockLength; + private int outlineArrayWidth; + + // An array of pixel group indices ordered by distance for each outline width and feather. + // These are calculated once upon first usage and then stored here to skip reevaluation. + private PixelDistanceGroupIndex[][][] precomputedGroupIndices = new PixelDistanceGroupIndex[0][][]; + + // An array of pixel distance deltas for each outline width and direction (right/up/left/down). + // These are calculated once upon first usage and then stored here to skip reevaluation. + private PixelDistanceDelta[][][] precomputedDistanceDeltas = new PixelDistanceDelta[0][][]; + + @Inject + private ModelOutlineRenderer(Client client) + { + this.client = client; + } + + /** + * Calculate the next power of two of a value. + * + * @param value The value to find the next power of two of. + * @return Returns the next power of two. + */ + private static int nextPowerOfTwo(int value) + { + value--; + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + value++; + return value; + } + + /** + * Determine if a triangle goes counter clockwise + * + * @return Returns true if the triangle goes counter clockwise and should be culled, otherwise false + */ + private static boolean cullFace(int x1, int y1, int x2, int y2, int x3, int y3) + { + return (y2 - y1) * (x3 - x2) - (x2 - x1) * (y3 - y2) <= 0; + } + + /** + * Get an array of pixel outline group indices ordered by distance for a specific outline width. + * + * @param outlineWidth The outline width. + * @param feather The feather of the outline. + * @return Returns the list of pixel distances. + */ + private PixelDistanceGroupIndex[] getPriorityList(int outlineWidth, int feather) + { + if (precomputedGroupIndices.length <= outlineWidth) + { + precomputedGroupIndices = Arrays.copyOf(precomputedGroupIndices, outlineWidth + 1); + } + if (precomputedGroupIndices[outlineWidth] == null) + { + precomputedGroupIndices[outlineWidth] = new PixelDistanceGroupIndex[feather + 1][]; + } + else if (precomputedGroupIndices[outlineWidth].length <= feather) + { + precomputedGroupIndices[outlineWidth] = Arrays.copyOf(precomputedGroupIndices[outlineWidth], feather + 1); + } + + if (precomputedGroupIndices[outlineWidth][feather] == null) + { + double fadedDistance = (double) feather / MAX_FEATHER * (outlineWidth - 0.5); + List ps = new ArrayList<>(); + for (int x = 0; x <= outlineWidth; x++) + { + for (int y = 0; y <= outlineWidth; y++) + { + if (x == 0 && y == 0) + { + continue; + } + + double dist = Math.hypot(x, y); + if (dist > outlineWidth) + { + continue; + } + + double outerDist = outlineWidth - dist + 0.5; + double multipliedAlpha = outerDist < fadedDistance ? outerDist / fadedDistance : 1.0; + ps.add(new PixelDistanceGroupIndex(dist, x + y * outlineArrayWidth, multipliedAlpha)); + } + } + ps.sort(Comparator.comparingDouble(PixelDistanceGroupIndex::getDistance)); + precomputedGroupIndices[outlineWidth][feather] = ps.toArray(new PixelDistanceGroupIndex[0]); + } + return precomputedGroupIndices[outlineWidth][feather]; + } + + private void ensureDistanceDeltasCreated(int outlineWidth) + { + if (precomputedDistanceDeltas.length <= outlineWidth) + { + precomputedDistanceDeltas = Arrays.copyOf(precomputedDistanceDeltas, outlineWidth + 1); + } + if (precomputedDistanceDeltas[outlineWidth] == null) + { + precomputedDistanceDeltas[outlineWidth] = new PixelDistanceDelta[4][]; + } + if (precomputedDistanceDeltas[outlineWidth][0] != null) + { + return; + } + + List distances = new ArrayList<>(); + for (int dy = -outlineWidth; dy <= outlineWidth; dy++) + { + for (int dx = 1; dx <= outlineWidth; dx++) + { + if (Math.abs(dy) > dx) + { + continue; + } + + double dist = Math.hypot(dx, dy); + if (dist > outlineWidth) + { + continue; + } + + distances.add(new PixelDistanceDelta(dx, dy)); + } + } + + for (int direction = 0; direction < 4; direction++) + { + precomputedDistanceDeltas[outlineWidth][direction] = distances.toArray(new PixelDistanceDelta[0]); + + // Turn 90 deg ccw + for (int i = 0; i < distances.size(); i++) + { + PixelDistanceDelta pdd = distances.get(i); + distances.set(i, new PixelDistanceDelta(pdd.dy, -pdd.dx)); + } + } + } + + /** + * Enqueues a pixel for outlining. + * + * @param distanceGroupIndex The group index to enqueue the pixel into. + * @param x The x position of the pixel. + * @param y The y position of the pixel. + */ + private void enqueueOutlinePixel(int distanceGroupIndex, int x, int y) + { + if (outlinePixelsLastBlockLength[distanceGroupIndex] == IntBlockBuffer.BLOCK_SIZE) + { + int minimumBlockIndicesSize = outlinePixelsBlockIndicesLengths[distanceGroupIndex] + 1; + if (minimumBlockIndicesSize > outlinePixelsBlockIndices[distanceGroupIndex].length) + { + outlinePixelsBlockIndices[distanceGroupIndex] = Arrays.copyOf( + outlinePixelsBlockIndices[distanceGroupIndex], + nextPowerOfTwo(minimumBlockIndicesSize)); + } + outlinePixelsBlockIndices[distanceGroupIndex][outlinePixelsBlockIndicesLengths[distanceGroupIndex]] = + outlinePixelsBlockBuffer.useNewBlock(); + outlinePixelsBlockIndicesLengths[distanceGroupIndex]++; + outlinePixelsLastBlockLength[distanceGroupIndex] = 0; + } + + int[] memory = outlinePixelsBlockBuffer.getMemory(); + int block = outlinePixelsBlockIndices[distanceGroupIndex][outlinePixelsBlockIndicesLengths[distanceGroupIndex] - 1]; + int blockPos = outlinePixelsLastBlockLength[distanceGroupIndex]++; + memory[(block << IntBlockBuffer.BLOCK_BITS) + blockPos] = (y << 16) | x; + } + + /** + * Checks that the visited bitset is big enough to hold a certain amount of pixels and sets them to 0. + * + * @param pixelAmount The amount of pixels needed. + */ + private void resetVisited(int pixelAmount) + { + int size = (pixelAmount >>> 5); + if (visited.length < size) + { + visited = new int[nextPowerOfTwo(size)]; + } + Arrays.fill(visited, 0, size, 0); + } + + /** + * Ensures that the outline buffer arrays are large enough to fit the current outline. + */ + private void initializeOutlineBuffers() + { + int arraySizes = outlineArrayWidth * outlineArrayWidth; + if (outlinePixelsBlockIndicesLengths.length < arraySizes) + { + outlinePixelsBlockIndices = new int[arraySizes][]; + outlinePixelsBlockIndicesLengths = new int[arraySizes]; + outlinePixelsLastBlockLength = new int[arraySizes]; + for (int i = 0; i < arraySizes; i++) + { + outlinePixelsBlockIndices[i] = new int[0]; + } + } + + for (int i = 0; i < arraySizes; i++) + { + outlinePixelsLastBlockLength[i] = IntBlockBuffer.BLOCK_SIZE; + } + } + + /** + * Frees all blocks currently in use by the outline block buffer. + */ + private void freeAllBlockMemory() + { + for (int i = 0; i < outlineArrayWidth * outlineArrayWidth; i++) + { + while (outlinePixelsBlockIndicesLengths[i] > 0) + { + outlinePixelsBlockIndicesLengths[i]--; + outlinePixelsBlockBuffer.freeBlock(outlinePixelsBlockIndices[i][outlinePixelsBlockIndicesLengths[i]]); + } + outlinePixelsLastBlockLength[i] = IntBlockBuffer.BLOCK_SIZE; + } + } + + /** + * Simulates a horizontal line rasterization and marks pixels visited. + * + * @param pixelY The y position of the line + * @param x1 The starting x position + * @param x2 The ending x position + */ + private void simulateHorizontalLineRasterizationForOutline(int pixelY, int x1, int x2) + { + if (x2 > clipX2) + { + x2 = clipX2; + } + if (x1 < clipX1) + { + x1 = clipX1; + } + if (x1 >= x2) + { + return; + } + + int pixelPos1 = (pixelY - croppedY1) * croppedWidth + (x1 - croppedX1); + int pixelPos2 = pixelPos1 + x2 - x1; + int pixelPosIndex1 = pixelPos1 >> 5; + int pixelPosIndex2 = pixelPos2 >> 5; + if (pixelPosIndex1 == pixelPosIndex2) + { + visited[pixelPosIndex1] |= ((1 << (pixelPos2 & 31)) - 1) ^ ((1 << (pixelPos1 & 31)) - 1); + } + else + { + visited[pixelPosIndex1] |= -(1 << (pixelPos1 & 31)); + visited[pixelPosIndex2] |= (1 << (pixelPos2 & 31)) - 1; + for (int i = pixelPosIndex1 + 1; i < pixelPosIndex2; i++) + { + visited[i] = 0xFFFFFFFF; + } + } + } + + /** + * Simulates rasterization of a triangle and marks pixels visited. + * + * @param x1 The x position of the first vertex in the triangle + * @param y1 The y position of the first vertex in the triangle + * @param x2 The x position of the second vertex in the triangle + * @param y2 The y position of the second vertex in the triangle + * @param x3 The x position of the third vertex in the triangle + * @param y3 The y position of the third vertex in the triangle + */ + private void simulateTriangleRasterizationForOutline( + int x1, int y1, int x2, int y2, int x3, int y3) + { + // Swap vertices so y1 <= y2 <= y3 using bubble sort + if (y1 > y2) + { + int yp = y1; + int xp = x1; + y1 = y2; + y2 = yp; + x1 = x2; + x2 = xp; + } + if (y2 > y3) + { + int yp = y2; + int xp = x2; + y2 = y3; + y3 = yp; + x2 = x3; + x3 = xp; + } + if (y1 > y2) + { + int yp = y1; + int xp = x1; + y1 = y2; + y2 = yp; + x1 = x2; + x2 = xp; + } + + if (y1 > clipY2) + { + // All points are outside clip boundaries + return; + } + + int slope1 = 0; + if (y1 != y2) + { + slope1 = (x2 - x1 << 14) / (y2 - y1); + } + + int slope2 = 0; + if (y3 != y2) + { + slope2 = (x3 - x2 << 14) / (y3 - y2); + } + + int slope3 = 0; + if (y1 != y3) + { + slope3 = (x1 - x3 << 14) / (y1 - y3); + } + + if (y2 > clipY2) + { + y2 = clipY2; + } + if (y3 > clipY2) + { + y3 = clipY2; + } + if (y1 == y3 || y3 < 0) + { + return; + } + + x1 <<= 14; + x2 <<= 14; + x3 = x1; + + if (y1 < 0) + { + x3 -= y1 * slope3; + x1 -= y1 * slope1; + y1 = 0; + } + if (y2 < 0) + { + x2 -= slope2 * y2; + y2 = 0; + } + + int pixelY = y1; + int height1 = y2 - y1; + int height2 = y3 - y2; + if (y1 != y2 && slope3 < slope1 || y1 == y2 && slope3 > slope2) + { + while (height1-- > 0) + { + simulateHorizontalLineRasterizationForOutline(pixelY, x3 >> 14, x1 >> 14); + x3 += slope3; + x1 += slope1; + pixelY++; + } + + while (height2-- > 0) + { + simulateHorizontalLineRasterizationForOutline(pixelY, x3 >> 14, x2 >> 14); + x3 += slope3; + x2 += slope2; + pixelY++; + } + } + else + { + while (height1-- > 0) + { + simulateHorizontalLineRasterizationForOutline(pixelY, x1 >> 14, x3 >> 14); + x1 += slope1; + x3 += slope3; + pixelY++; + } + + while (height2-- > 0) + { + simulateHorizontalLineRasterizationForOutline(pixelY, x2 >> 14, x3 >> 14); + x3 += slope3; + x2 += slope2; + pixelY++; + } + } + } + + /** + * Translates the vertices 3D points to the screen canvas 2D points. + * + * @param localX The local x position of the vertices. + * @param localY The local y position of the vertices. + * @param localZ The local z position of the vertices. + * @param vertexOrientation The orientation of the vertices. + * @return Returns true if any of them are inside the clip area, otherwise false. + */ + private boolean projectVertices(Model model, int localX, int localY, int localZ, final int vertexOrientation) + { + final int vertexCount = model.getVerticesCount(); + Perspective.modelToCanvas(client, + vertexCount, + localX, localY, localZ, + vertexOrientation, + model.getVerticesX(), model.getVerticesZ(), model.getVerticesY(), + projectedVerticesX, projectedVerticesY); + + boolean anyVisible = false; + + for (int i = 0; i < vertexCount; i++) + { + int x = projectedVerticesX[i]; + int y = projectedVerticesY[i]; + + if (y != Integer.MIN_VALUE) + { + boolean visibleX = x >= clipX1 && x < clipX2; + boolean visibleY = y >= clipY1 && y < clipY2; + anyVisible |= visibleX && visibleY; + + croppedX1 = Math.min(croppedX1, x); + croppedX2 = Math.max(croppedX2, x + 1); + croppedY1 = Math.min(croppedY1, y); + croppedY2 = Math.max(croppedY2, y + 1); + } + else + { + // Vertex is too close or behind camera and isn't rendered + projectedVerticesY[i] = Integer.MIN_VALUE; + } + } + + return anyVisible; + } + + /** + * Simulates rendering of the model and marks every pixel visited. + */ + private void simulateModelRasterizationForOutline(Model model) + { + final int triangleCount = model.getTrianglesCount(); + final int[] indices1 = model.getTrianglesX(); + final int[] indices2 = model.getTrianglesY(); + final int[] indices3 = model.getTrianglesZ(); + final byte[] triangleTransparencies = model.getTriangleTransparencies(); + + for (int i = 0; i < triangleCount; i++) + { + if (projectedVerticesY[indices1[i]] != Integer.MIN_VALUE && + projectedVerticesY[indices2[i]] != Integer.MIN_VALUE && + projectedVerticesY[indices3[i]] != Integer.MIN_VALUE && + // 254 and 255 counts as fully transparent + (triangleTransparencies == null || (triangleTransparencies[i] & 255) < 254)) + { + final int index1 = indices1[i]; + final int index2 = indices2[i]; + final int index3 = indices3[i]; + final int v1x = projectedVerticesX[index1]; + final int v1y = projectedVerticesY[index1]; + final int v2x = projectedVerticesX[index2]; + final int v2y = projectedVerticesY[index2]; + final int v3x = projectedVerticesX[index3]; + final int v3y = projectedVerticesY[index3]; + + if (!cullFace(v1x, v1y, v2x, v2y, v3x, v3y)) + { + simulateTriangleRasterizationForOutline(v1x, v1y, v2x, v2y, v3x, v3y); + } + } + } + } + + /** + * Draws the outline of a pixel according to the distance deltas of an outline. + * + * @param imageData The image data to draw to. + * @param imageWidth The width of the image to draw to. + * @param x The x position of the pixel. + * @param y The y position of the pixel. + * @param distanceDeltas The distance deltas of the outline width. + * @param color The color to draw the outline in. + */ + private void rasterDistanceDeltas(int[] imageData, int imageWidth, int x, int y, + PixelDistanceDelta[] distanceDeltas, int color) + { + for (PixelDistanceDelta delta : distanceDeltas) + { + int cx = x + delta.dx; + int cy = y + delta.dy; + int visitedPixelPos = (cy - croppedY1) * croppedWidth + (cx - croppedX1); + if (cx >= clipX1 && cx < clipX2 && cy >= clipY1 && cy < clipY2 && + (visited[visitedPixelPos >> 5] & (1 << (visitedPixelPos & 31))) == 0) + { + imageData[cy * imageWidth + cx] = color; + } + } + } + + /** + * Enqueues pixels that are adjacent above or below the model + * or draws them directly to the clients image buffer. + * + * @param directWrite If true the pixels are drawn to the image buffer, otherwise they are enqueued for drawing. + * @param color The color to draw if directWrite == true + * @param outlineWidth The outline width to draw if directWrite == true + */ + private void processInitialOutlinePixels(boolean directWrite, Color color, int outlineWidth) + { + MainBufferProvider bufferProvider = (MainBufferProvider) client.getBufferProvider(); + BufferedImage image = (BufferedImage) bufferProvider.getImage(); + int imageWidth = image.getWidth(); + int[] imageData = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + int colorRGB = color.getRGB(); + + // Up and down + for (int x = 0; x < croppedWidth; x += 32) + { + int v1 = visited[x >> 5]; + for (int y = 1; y < croppedHeight; y++) + { + int v2 = visited[(y * croppedWidth + x) >> 5]; + if (v1 != v2) + { + if (directWrite) + { + // Special case handling outlineWidth == 1 gives a + // small but noticeable performance improvement. + if (outlineWidth == 1) + { + for (int bit = 0; bit < 32; bit++) + { + int bv1 = (v1 >>> bit) & 1; + int bv2 = (v2 >>> bit) & 1; + if (bv1 != bv2) + { + imageData[(croppedY1 + y - bv2) * imageWidth + (croppedX1 + x + bit)] = colorRGB; + } + } + } + else + { + PixelDistanceDelta[] distancesDown = precomputedDistanceDeltas[outlineWidth][3]; + PixelDistanceDelta[] distancesUp = precomputedDistanceDeltas[outlineWidth][1]; + for (int bit = 0; bit < 32; bit++) + { + int bv1 = (v1 >>> bit) & 1; + int bv2 = (v2 >>> bit) & 1; + if (bv1 == 1 && bv2 == 0) + { + rasterDistanceDeltas(imageData, imageWidth, croppedX1 + x + bit, croppedY1 + y - 1, + distancesDown, colorRGB); + } + else if (bv1 == 0 && bv2 == 1) + { + rasterDistanceDeltas(imageData, imageWidth, croppedX1 + x + bit, croppedY1 + y, + distancesUp, colorRGB); + } + } + } + } + else + { + for (int bit = 0; bit < 32; bit++) + { + int bv1 = (v1 >>> bit) & 1; + int bv2 = (v2 >>> bit) & 1; + if (bv1 != bv2) + { + enqueueOutlinePixel(outlineArrayWidth, croppedX1 + x + bit, croppedY1 + y - bv2); + } + } + } + } + + v1 = v2; + } + } + + // Left and right + for (int y = 0; y < croppedHeight; y++) + { + int rowPosition = y * croppedWidth; + int lastV = 0; + for (int x = 0; x < croppedWidth; x += 32) + { + int v = visited[(rowPosition + x) >> 5]; + + // Test adjacent pixels in the same 32-bit segment + if (v != 0 && v != 0xFFFFFFFF) + { + int end = Math.min(32, clipX2 - croppedX1 - x); + int lastBv = v & 1; + if (directWrite) + { + // Special case handling outlineWidth == 1 gives a + // small but noticeable performance improvement. + if (outlineWidth == 1) + { + for (int bit = 1; bit < end; bit++) + { + int bv = (v >>> bit) & 1; + if (bv != lastBv) + { + imageData[(croppedY1 + y) * imageWidth + (croppedX1 + x + bit - bv)] = colorRGB; + } + lastBv = bv; + } + } + else + { + PixelDistanceDelta[] distancesRight = precomputedDistanceDeltas[outlineWidth][0]; + PixelDistanceDelta[] distancesLeft = precomputedDistanceDeltas[outlineWidth][2]; + for (int bit = 1; bit < end; bit++) + { + int bv = (v >>> bit) & 1; + if (bv == 1 && lastBv == 0) + { + rasterDistanceDeltas(imageData, imageWidth, croppedX1 + x + bit, croppedY1 + y, + distancesLeft, colorRGB); + } + else if (bv == 0 && lastBv == 1) + { + rasterDistanceDeltas(imageData, imageWidth, croppedX1 + x + bit - 1, croppedY1 + y, + distancesRight, colorRGB); + } + lastBv = bv; + } + } + } + else + { + for (int bit = 1; bit < end; bit++) + { + int bv = (v >>> bit) & 1; + if (bv != lastBv) + { + enqueueOutlinePixel(1, croppedX1 + x + bit - bv, croppedY1 + y); + } + lastBv = bv; + } + } + } + + // Test adjacent pixels in different 32-bit segments + if ((lastV >>> 31) != (v & 1) && x > 0) + { + if (directWrite) + { + if (outlineWidth == 1) + { + imageData[(croppedY1 + y) * imageWidth + (croppedX1 + x - (v & 1))] = colorRGB; + } + else + { + if ((v & 1) == 1) + { + PixelDistanceDelta[] distancesLeft = precomputedDistanceDeltas[outlineWidth][2]; + rasterDistanceDeltas(imageData, imageWidth, croppedX1 + x, croppedY1 + y, + distancesLeft, colorRGB); + } + else + { + PixelDistanceDelta[] distancesRight = precomputedDistanceDeltas[outlineWidth][0]; + rasterDistanceDeltas(imageData, imageWidth, croppedX1 + x - 1, croppedY1 + y, + distancesRight, colorRGB); + } + } + } + else + { + enqueueOutlinePixel(1, croppedX1 + x - (v & 1), croppedY1 + y); + } + } + + lastV = v; + } + } + } + + /** + * Process the outline queue and draw an outline of the pixels + * in the queue to the client image buffer. + * + * @param outlineWidth The width of the outline. + * @param color The color of the outline. + */ + private void processOutlinePixelQueue(int outlineWidth, Color color, int feather) + { + MainBufferProvider bufferProvider = (MainBufferProvider) client.getBufferProvider(); + BufferedImage image = (BufferedImage) bufferProvider.getImage(); + int imageWidth = image.getWidth(); + int[] imageData = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + PixelDistanceGroupIndex[] ps = getPriorityList(outlineWidth, feather); + + for (PixelDistanceGroupIndex p : ps) + { + final int[] blockMemory = outlinePixelsBlockBuffer.getMemory(); + + final int colorARGB; + final int inverseAlpha; + { + int alpha = (int) Math.round(color.getAlpha() * p.alphaMultiply); + inverseAlpha = 256 - alpha; + colorARGB = (alpha << 24) + | ((color.getRed() * alpha) / 255) << 16 + | ((color.getGreen() * alpha) / 255) << 8 + | ((color.getBlue() * alpha) / 255); + } + + final int groupIndex = p.distanceGroupIndex; + final int nextGroupIndexY = groupIndex + outlineArrayWidth; + final int nextGroupIndexX = groupIndex + 1; + + while (outlinePixelsBlockIndicesLengths[groupIndex] > 0) + { + final int block = outlinePixelsBlockIndices[groupIndex][outlinePixelsBlockIndicesLengths[groupIndex] - 1]; + final int blockStart = block << IntBlockBuffer.BLOCK_BITS; + final int blockEnd = blockStart + outlinePixelsLastBlockLength[groupIndex]; + for (int i = blockStart; i < blockEnd; i++) + { + int x = blockMemory[i] & 0xFFFF; + int y = blockMemory[i] >>> 16; + int visitedPixelPos = (y - croppedY1) * croppedWidth + (x - croppedX1); + if ((visited[visitedPixelPos >> 5] & (1 << (visitedPixelPos & 31))) != 0) + { + continue; + } + visited[visitedPixelPos >> 5] |= 1 << (visitedPixelPos & 31); + + int pixelPos = y * imageWidth + x; + int dst = imageData[pixelPos]; + imageData[pixelPos] + = (colorARGB & 0xFF00FF00) + (((dst & 0xFF00FF00) * inverseAlpha) >>> 8) & 0xFF00FF00 + | (colorARGB & 0x00FF00FF) + (((dst & 0x00FF00FF) * inverseAlpha) >>> 8) & 0x00FF00FF; + + if (x - 1 >= clipX1) + { + enqueueOutlinePixel(nextGroupIndexX, x - 1, y); + } + if (x + 1 < clipX2) + { + enqueueOutlinePixel(nextGroupIndexX, x + 1, y); + } + if (y - 1 >= clipY1) + { + enqueueOutlinePixel(nextGroupIndexY, x, y - 1); + } + if (y + 1 < clipY2) + { + enqueueOutlinePixel(nextGroupIndexY, x, y + 1); + } + } + + outlinePixelsBlockBuffer.freeBlock(block); + outlinePixelsBlockIndicesLengths[groupIndex]--; + outlinePixelsLastBlockLength[groupIndex] = IntBlockBuffer.BLOCK_SIZE; + } + } + } + + /** + * Draws an outline around a model to an image + * + * @param localX The local x position of the model + * @param localY The local y position of the model + * @param localZ The local z position of the model + * @param orientation The orientation of the model + * @param outlineWidth The width of the outline + * @param color The color of the outline + */ + private void drawModelOutline(Model model, + int localX, int localY, int localZ, int orientation, + int outlineWidth, Color color, int feather) + { + if (outlineWidth <= 0 || color.getAlpha() == 0 || model == null) + { + return; + } + + if (outlineWidth > MAX_OUTLINE_WIDTH) + { + outlineWidth = MAX_OUTLINE_WIDTH; + } + + if (feather < 0) + { + feather = 0; + } + else if (feather > MAX_FEATHER) + { + feather = MAX_FEATHER; + } + + croppedX1 = Integer.MAX_VALUE; + croppedX2 = Integer.MIN_VALUE; + croppedY1 = Integer.MAX_VALUE; + croppedY2 = Integer.MIN_VALUE; + + clipX1 = client.getViewportXOffset(); + clipY1 = client.getViewportYOffset(); + clipX2 = client.getViewportWidth() + clipX1; + clipY2 = client.getViewportHeight() + clipY1; + + if (!projectVertices(model, localX, localY, localZ, orientation)) + { + // No vertex of the model is visible on the screen, so we can + // assume there are no parts of the model to outline. + return; + } + + croppedX1 = Math.max(croppedX1 - outlineWidth, clipX1); + croppedX2 = Math.min(croppedX2 + outlineWidth, clipX2); + croppedX2 += ~(croppedX2 - croppedX1 - 1) & 31; // Increases width to next multiple of 32 so bitset segments align + croppedY1 = Math.max(croppedY1 - outlineWidth, clipY1); + croppedY2 = Math.min(croppedY2 + outlineWidth, clipY2); + croppedWidth = croppedX2 - croppedX1; + croppedHeight = croppedY2 - croppedY1; + + resetVisited(croppedWidth * croppedHeight); + + simulateModelRasterizationForOutline(model); + + // We can improve performance and reduce memory needed when drawing + // only a small outline around the model by skipping the pixel queueing + // and instead raster pixels directly. This only looks right for opaque + // outlines since some pixels of the outline can get drawn more than once. + // Performance becomes worse than queueing when using larger outline widths, + // usually around 10 px outline width according to some basic testing. + boolean directWrite = color.getAlpha() == 255 && outlineWidth <= DIRECT_WRITE_OUTLINE_WIDTH_THRESHOLD && + (feather == 0 || outlineWidth == 1); // Feather has no effect on outlineWidth == 1 + + if (directWrite) + { + ensureDistanceDeltasCreated(outlineWidth); + } + else + { + outlineArrayWidth = outlineWidth + 2; + initializeOutlineBuffers(); + } + + try + { + processInitialOutlinePixels(directWrite, color, outlineWidth); + + if (!directWrite) + { + processOutlinePixelQueue(outlineWidth, color, feather); + } + } + finally + { + freeAllBlockMemory(); + } + } + + public void drawOutline(NPC npc, int outlineWidth, Color color, int feather) + { + int size = 1; + NPCComposition composition = npc.getTransformedComposition(); + if (composition != null) + { + size = composition.getSize(); + } + + LocalPoint lp = npc.getLocalLocation(); + if (lp != null) + { + // NPCs z position are calculated based on the tile height of the northeastern tile + final int northEastX = lp.getX() + Perspective.LOCAL_TILE_SIZE * (size - 1) / 2; + final int northEastY = lp.getY() + Perspective.LOCAL_TILE_SIZE * (size - 1) / 2; + final LocalPoint northEastLp = new LocalPoint(northEastX, northEastY); + + drawModelOutline(npc.getModel(), lp.getX(), lp.getY(), + Perspective.getTileHeight(client, northEastLp, client.getPlane()), + npc.getCurrentOrientation(), outlineWidth, color, feather); + } + } + + public void drawOutline(Player player, int outlineWidth, Color color, int feather) + { + LocalPoint lp = player.getLocalLocation(); + if (lp != null) + { + drawModelOutline(player.getModel(), lp.getX(), lp.getY(), + Perspective.getTileHeight(client, lp, client.getPlane()), + player.getCurrentOrientation(), outlineWidth, color, feather); + } + } + + private void drawOutline(GameObject gameObject, int outlineWidth, Color color, int feather) + { + LocalPoint lp = gameObject.getLocalLocation(); + Renderable renderable = gameObject.getRenderable(); + if (renderable != null) + { + Model model = renderable instanceof Model ? (Model) renderable : renderable.getModel(); + if (model != null) + { + drawModelOutline(model, lp.getX(), lp.getY(), + Perspective.getTileHeight(client, lp, gameObject.getPlane()), + gameObject.getModelOrientation(), outlineWidth, color, feather); + } + } + } + + private void drawOutline(GroundObject groundObject, int outlineWidth, Color color, int feather) + { + LocalPoint lp = groundObject.getLocalLocation(); + Renderable renderable = groundObject.getRenderable(); + if (renderable != null) + { + Model model = renderable instanceof Model ? (Model) renderable : renderable.getModel(); + if (model != null) + { + drawModelOutline(model, lp.getX(), lp.getY(), + Perspective.getTileHeight(client, lp, client.getPlane()), + 0, outlineWidth, color, feather); + } + } + } + + private void drawOutline(ItemLayer itemLayer, int outlineWidth, Color color, int feather) + { + LocalPoint lp = itemLayer.getLocalLocation(); + + Renderable bottomRenderable = itemLayer.getBottom(); + if (bottomRenderable != null) + { + Model model = bottomRenderable instanceof Model ? (Model) bottomRenderable : bottomRenderable.getModel(); + if (model != null) + { + drawModelOutline(model, lp.getX(), lp.getY(), + Perspective.getTileHeight(client, lp, itemLayer.getPlane()) - itemLayer.getHeight(), + 0, outlineWidth, color, feather); + } + } + + Renderable middleRenderable = itemLayer.getMiddle(); + if (middleRenderable != null) + { + Model model = middleRenderable instanceof Model ? (Model) middleRenderable : middleRenderable.getModel(); + if (model != null) + { + drawModelOutline(model, lp.getX(), lp.getY(), + Perspective.getTileHeight(client, lp, itemLayer.getPlane()) - itemLayer.getHeight(), + 0, outlineWidth, color, feather); + } + } + + Renderable topRenderable = itemLayer.getTop(); + if (topRenderable != null) + { + Model model = topRenderable instanceof Model ? (Model) topRenderable : topRenderable.getModel(); + if (model != null) + { + drawModelOutline(model, lp.getX(), lp.getY(), + Perspective.getTileHeight(client, lp, itemLayer.getPlane()) - itemLayer.getHeight(), + 0, outlineWidth, color, feather); + } + } + } + + private void drawOutline(DecorativeObject decorativeObject, int outlineWidth, Color color, int feather) + { + LocalPoint lp = decorativeObject.getLocalLocation(); + + Renderable renderable1 = decorativeObject.getRenderable(); + if (renderable1 != null) + { + Model model = renderable1 instanceof Model ? (Model) renderable1 : renderable1.getModel(); + if (model != null) + { + drawModelOutline(model, + lp.getX() + decorativeObject.getXOffset(), + lp.getY() + decorativeObject.getYOffset(), + Perspective.getTileHeight(client, lp, decorativeObject.getPlane()), + 0, outlineWidth, color, feather); + } + } + + Renderable renderable2 = decorativeObject.getRenderable2(); + if (renderable2 != null) + { + Model model = renderable2 instanceof Model ? (Model) renderable2 : renderable2.getModel(); + if (model != null) + { + // Offset is not used for the second model + drawModelOutline(model, lp.getX(), lp.getY(), + Perspective.getTileHeight(client, lp, decorativeObject.getPlane()), + 0, outlineWidth, color, feather); + } + } + } + + private void drawOutline(WallObject wallObject, int outlineWidth, Color color, int feather) + { + LocalPoint lp = wallObject.getLocalLocation(); + + Renderable renderable1 = wallObject.getRenderable1(); + if (renderable1 != null) + { + Model model = renderable1 instanceof Model ? (Model) renderable1 : renderable1.getModel(); + if (model != null) + { + drawModelOutline(model, lp.getX(), lp.getY(), + Perspective.getTileHeight(client, lp, wallObject.getPlane()), + wallObject.getOrientationA(), outlineWidth, color, feather); + } + } + + Renderable renderable2 = wallObject.getRenderable2(); + if (renderable2 != null) + { + Model model = renderable2 instanceof Model ? (Model) renderable2 : renderable2.getModel(); + if (model != null) + { + drawModelOutline(model, lp.getX(), lp.getY(), + Perspective.getTileHeight(client, lp, wallObject.getPlane()), + wallObject.getOrientationB(), outlineWidth, color, feather); + } + } + } + + public void drawOutline(TileObject tileObject, int outlineWidth, Color color, int feather) + { + if (tileObject instanceof GameObject) + { + drawOutline((GameObject) tileObject, outlineWidth, color, feather); + } + else if (tileObject instanceof GroundObject) + { + drawOutline((GroundObject) tileObject, outlineWidth, color, feather); + } + else if (tileObject instanceof ItemLayer) + { + drawOutline((ItemLayer) tileObject, outlineWidth, color, feather); + } + else if (tileObject instanceof DecorativeObject) + { + drawOutline((DecorativeObject) tileObject, outlineWidth, color, feather); + } + else if (tileObject instanceof WallObject) + { + drawOutline((WallObject) tileObject, outlineWidth, color, feather); + } + } + + public void drawOutline(GraphicsObject graphicsObject, int outlineWidth, Color color, int feather) + { + LocalPoint lp = graphicsObject.getLocation(); + if (lp != null) + { + Model model = graphicsObject.getModel(); + if (model != null) + { + drawModelOutline(model, lp.getX(), lp.getY(), graphicsObject.getHeight(), + 0, outlineWidth, color, feather); + } + } + } +}