From d61c6b91a7159fb0cde4a3909ed86c666eb6ef9e Mon Sep 17 00:00:00 2001 From: zeruth Date: Mon, 25 Jan 2021 13:06:20 -0500 Subject: [PATCH] api: develop --- .../client/graphics/ModelOutlineRenderer.java | 1113 +++++++++++++++++ .../client/ui/overlay/OverlayUtil.java | 54 +- .../com/openosrs/client/util/ImageUtil.java | 134 ++ .../runelite/client/config/ConfigItem.java | 8 + .../runelite/client/config/ConfigSection.java | 7 + .../net/runelite/client/config/Units.java | 4 + 6 files changed, 1314 insertions(+), 6 deletions(-) create mode 100644 runelite-client/src/main/java/com/openosrs/client/graphics/ModelOutlineRenderer.java diff --git a/runelite-client/src/main/java/com/openosrs/client/graphics/ModelOutlineRenderer.java b/runelite-client/src/main/java/com/openosrs/client/graphics/ModelOutlineRenderer.java new file mode 100644 index 0000000000..5e2f10445f --- /dev/null +++ b/runelite-client/src/main/java/com/openosrs/client/graphics/ModelOutlineRenderer.java @@ -0,0 +1,1113 @@ +/* + * Copyright (c) 2018, 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 com.openosrs.client.graphics; + +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.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import lombok.RequiredArgsConstructor; +import lombok.Value; +import net.runelite.api.Client; +import net.runelite.api.DecorativeObject; +import net.runelite.api.GameObject; +import net.runelite.api.GroundObject; +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.ItemLayer; +import net.runelite.api.TileObject; +import net.runelite.api.WallObject; +import net.runelite.api.coords.LocalPoint; +import net.runelite.client.task.Schedule; + +@Singleton +public class ModelOutlineRenderer +{ + /* + * This class doesn't really "need" static variables, but they are + * static for performance reasons. Arrays are kept outside methods + * to avoid frequent big allocations. Arrays should mostly be seen + * as ArrayLists. The size of them is increased whenever they need + * to become bigger. + */ + + private final Client client; + + private boolean isReset; + private boolean usedSinceLastCheck; + + // Dimensions of the underlying image + private int imageWidth; + private int imageHeight; + + // Boundaries for the current rasterization + private int clipX1; + private int clipY1; + private int clipX2; + private int clipY2; + + // Pixel points that would be rendered to + private int[] visited; + private int currentVisitedNumber = 0; + + // Transformed vertex positions + private int[] projectedVerticesX; + private int[] projectedVerticesY; + private boolean[] projectedVerticesRenderable; + + // An array of pixel points to raster onto the image. These are checked against + // clip boundaries and the visited array to prevent drawing on top of the model + // and outside the scene area. They are grouped per distance to the closest pixel + // drawn on the model. + private int[][] outlinePixels; + private int[] outlinePixelsLengths; // outlinePixelsLength[i] is the used length of outlinePixels[i] + private int outlineArrayWidth; + + // A list of pixel distances ordered from shortest to longest distance for + // each outline width. These are calculated once upon first usage and then + // stored here to prevent reevaluation. + private List> precomputedDistancePriorities; + + @Inject + private ModelOutlineRenderer(Client client) + { + this.client = client; + + reset(); + } + + @Schedule(period = 5, unit = ChronoUnit.SECONDS) + public void checkUsage() + { + if (!isReset && !usedSinceLastCheck) + { + // Reset memory allocated when the rasterizer becomes inactive + reset(); + } + usedSinceLastCheck = false; + } + + /** + * Reset memory used by the rasterizer + */ + private void reset() + { + visited = new int[0]; + projectedVerticesX = new int[0]; + projectedVerticesY = new int[0]; + projectedVerticesRenderable = new boolean[0]; + outlinePixels = new int[0][]; + outlinePixelsLengths = new int[0]; + precomputedDistancePriorities = new ArrayList<>(0); + isReset = true; + } + + /** + * 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; + } + + /** + * Gets the list of pixel distances ordered by distance from closest pixel for a specific outline width. + * + * @param outlineWidth The outline width + * @return Returns the list of pixel distances + */ + private List getPriorityList(int outlineWidth) + { + while (precomputedDistancePriorities.size() <= outlineWidth) + { + precomputedDistancePriorities.add(null); + } + + // Grab the cached outline width if we have one + if (precomputedDistancePriorities.get(outlineWidth) != null) + { + return precomputedDistancePriorities.get(outlineWidth); + } + + 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.sqrt(x * x + y * y); + if (dist > outlineWidth) + { + continue; + } + + int outerAlpha = outlineWidth == 1 ? 255 // For preventing division by 0 + : (int) (255 * (dist - 1) / (outlineWidth - 1)); + ps.add(new PixelDistanceAlpha(outerAlpha, x + y * outlineArrayWidth)); + } + } + ps.sort(Comparator.comparingDouble(PixelDistanceAlpha::getOuterAlpha)); + precomputedDistancePriorities.set(outlineWidth, ps); + + return ps; + } + + /** + * Checks that the size of outlinePixels is big enough to hold a specific + * amount of elements. This is used to reduce the amount of if checks needed + * when adding elements to outlinePixels. + * + * @param distArrayPos The position in the array + * @param additionalMinimumSize The additional minimum size required + */ + private void ensureMinimumOutlineQueueSize(int distArrayPos, int additionalMinimumSize) + { + int minimumSize = outlinePixelsLengths[distArrayPos] + additionalMinimumSize; + while (outlinePixels[distArrayPos].length < minimumSize) + { + int[] newArr = new int[nextPowerOfTwo(minimumSize)]; + System.arraycopy(outlinePixels[distArrayPos], 0, newArr, 0, + outlinePixels[distArrayPos].length); + outlinePixels[distArrayPos] = newArr; + } + } + + /** + * Resets the visited flag for a specific amount of pixels + * + * @param pixelAmount The amount of pixels to reset + */ + private void resetVisited(int pixelAmount) + { + // The visited array is essentially a boolean array, but by + // making it an int array and checking if visited[i] == currentVisitedNumber + // and changing currentVisitedNumber for every new outline, we can essentially + // reset the whole array without having to iterate over every element + + if (visited.length < pixelAmount) + { + visited = new int[nextPowerOfTwo(pixelAmount)]; + currentVisitedNumber = 0; + } + + currentVisitedNumber++; + } + + /** + * Resets the pixels that are queued for outlining + * + * @param outlineWidth The width of the outline to reset pixels for + */ + private void resetOutline(int outlineWidth) + { + outlineArrayWidth = outlineWidth + 2; + + int arraySizes = outlineArrayWidth * outlineArrayWidth; + if (outlinePixels.length < arraySizes) + { + outlinePixels = new int[arraySizes][]; + outlinePixelsLengths = new int[arraySizes]; + for (int i = 0; i < arraySizes; i++) + { + outlinePixels[i] = new int[4]; + } + } + else + { + for (int i = 0; i < arraySizes; i++) + { + outlinePixelsLengths[i] = 0; + } + } + } + + /** + * Simulates a horizontal line rasterization and adds the pixels to the left + * and to the right to the outline queue if they are within the clip area. + * + * @param pixelPos The pixel position in the line where x == 0 + * @param x1 The starting x position + * @param x2 The ending x position + */ + private void simulateHorizontalLineRasterizationForOutline(int pixelPos, int x1, int x2) + { + if (x2 > clipX2) + { + x2 = clipX2; + } + if (x1 < clipX1) + { + x1 = clipX1; + } + if (x1 >= x2) + { + return; + } + + // Queue the pixel positions to the left and to the right of the line + ensureMinimumOutlineQueueSize(1, 2); + if (x2 < clipX2) + { + outlinePixels[1][outlinePixelsLengths[1]++] = pixelPos + x2; + } + if (x1 > clipX1) + { + outlinePixels[1][outlinePixelsLengths[1]++] = pixelPos + x1 - 1; + } + + // Divide by 4 to account for loop unrolling + int xDist = x2 - x1 >> 2; + pixelPos += x1; + + // This loop could run over 100m times per second without loop unrolling in some cases, + // so unrolling it can give a noticeable performance boost. + while (xDist-- > 0) + { + visited[pixelPos++] = currentVisitedNumber; + visited[pixelPos++] = currentVisitedNumber; + visited[pixelPos++] = currentVisitedNumber; + visited[pixelPos++] = currentVisitedNumber; + } + + // Draw up to 3 more pixels if there were any left + xDist = (x2 - x1) & 3; + while (xDist-- > 0) + { + visited[pixelPos++] = currentVisitedNumber; + } + } + + /** + * Queues the pixel positions above and below two horizontal lines, excluding those + * where the x positions of the lines intersect. + * + * @param pixelPos The pixel position at x == 0 of the second line + * @param x1 The starting x position of the first line + * @param x2 The ending x position of the first line + * @param x3 The starting x position of the second line + * @param x4 The ending x position of the second line + */ + private void outlineAroundHorizontalLine(int pixelPos, int x1, int x2, int x3, int x4) + { + if (x1 < clipX1) + { + x1 = clipX1; + } + if (x2 < clipX1) + { + x2 = clipX1; + } + if (x3 < clipX1) + { + x3 = clipX1; + } + if (x4 < clipX1) + { + x4 = clipX1; + } + + if (x1 > clipX2) + { + x1 = clipX2; + } + if (x2 > clipX2) + { + x2 = clipX2; + } + if (x3 > clipX2) + { + x3 = clipX2; + } + if (x4 > clipX2) + { + x4 = clipX2; + } + + if (x1 < x3) + { + ensureMinimumOutlineQueueSize(outlineArrayWidth, x3 - x1); + for (int x = x1; x < x3; x++) + { + outlinePixels[outlineArrayWidth][outlinePixelsLengths[outlineArrayWidth]++] = pixelPos - imageWidth + x; + } + } + else + { + ensureMinimumOutlineQueueSize(outlineArrayWidth, x1 - x3); + for (int x = x3; x < x1; x++) + { + outlinePixels[outlineArrayWidth][outlinePixelsLengths[outlineArrayWidth]++] = pixelPos + x; + } + } + + if (x2 < x4) + { + ensureMinimumOutlineQueueSize(outlineArrayWidth, x4 - x2); + for (int x = x2; x < x4; x++) + { + outlinePixels[outlineArrayWidth][outlinePixelsLengths[outlineArrayWidth]++] = pixelPos + x; + } + } + else + { + ensureMinimumOutlineQueueSize(outlineArrayWidth, x2 - x4); + for (int x = x4; x < x2; x++) + { + outlinePixels[outlineArrayWidth][outlinePixelsLengths[outlineArrayWidth]++] = pixelPos - imageWidth + x; + } + } + } + + /** + * Simulates rasterization of a triangle and adds every pixel outside the triangle + * to the outline queue. + * + * @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 pixelPos = y1 * imageWidth; + int currX1; + int currX2; + if (y1 != y2 && slope3 < slope1 || y1 == y2 && slope3 > slope2) + { + int height1 = y2 - y1; + int height2 = y3 - y2; + + int prevX1; + int prevX2; + if (height1 <= 0) + { + prevX1 = x3 >> 14; + prevX2 = x2 >> 14; + } + else + { + prevX1 = x3 >> 14; + prevX2 = x1 >> 14; + } + + outlineAroundHorizontalLine(pixelPos, prevX1, prevX2, prevX2, prevX2); + + while (height1-- > 0) + { + currX1 = x3 >> 14; + currX2 = x1 >> 14; + outlineAroundHorizontalLine(pixelPos, currX1, currX2, prevX1, prevX2); + simulateHorizontalLineRasterizationForOutline(pixelPos, currX1, currX2); + x3 += slope3; + x1 += slope1; + pixelPos += imageWidth; + prevX1 = currX1; + prevX2 = currX2; + } + + while (height2-- > 0) + { + currX1 = x3 >> 14; + currX2 = x2 >> 14; + outlineAroundHorizontalLine(pixelPos, currX1, currX2, prevX1, prevX2); + simulateHorizontalLineRasterizationForOutline(pixelPos, currX1, currX2); + x3 += slope3; + x2 += slope2; + pixelPos += imageWidth; + prevX1 = currX1; + prevX2 = currX2; + } + + outlineAroundHorizontalLine(pixelPos, prevX1, prevX1, prevX1, prevX2); + } + else + { + int height1 = y2 - y1; + int height2 = y3 - y2; + + int prevX1; + int prevX2; + if (height1 <= 0) + { + prevX1 = x2 >> 14; + prevX2 = x3 >> 14; + } + else + { + prevX1 = x1 >> 14; + prevX2 = x3 >> 14; + } + + outlineAroundHorizontalLine(pixelPos, prevX1, prevX2, prevX2, prevX2); + + while (height1-- > 0) + { + currX1 = x1 >> 14; + currX2 = x3 >> 14; + outlineAroundHorizontalLine(pixelPos, currX1, currX2, prevX1, prevX2); + simulateHorizontalLineRasterizationForOutline(pixelPos, currX1, currX2); + x1 += slope1; + x3 += slope3; + pixelPos += imageWidth; + prevX1 = currX1; + prevX2 = currX2; + } + + while (height2-- > 0) + { + currX1 = x2 >> 14; + currX2 = x3 >> 14; + outlineAroundHorizontalLine(pixelPos, currX1, currX2, prevX1, prevX2); + simulateHorizontalLineRasterizationForOutline(pixelPos, currX1, currX2); + x3 += slope3; + x2 += slope2; + pixelPos += imageWidth; + prevX1 = currX1; + prevX2 = currX2; + } + + outlineAroundHorizontalLine(pixelPos, prevX1, prevX1, prevX1, prevX2); + } + } + + /** + * 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, final int localX, final int localY, final int localZ, final int vertexOrientation) + { + final int cameraX = client.getCameraX(); + final int cameraY = client.getCameraY(); + final int cameraZ = client.getCameraZ(); + final int cameraYaw = client.getCameraYaw(); + final int cameraPitch = client.getCameraPitch(); + final int scale = client.getScale(); + final int orientationSin = Perspective.SINE[vertexOrientation]; + final int orientationCos = Perspective.COSINE[vertexOrientation]; + final int pitchSin = Perspective.SINE[cameraPitch]; + final int pitchCos = Perspective.COSINE[cameraPitch]; + final int yawSin = Perspective.SINE[cameraYaw]; + final int yawCos = Perspective.COSINE[cameraYaw]; + final int vertexCount = model.getVerticesCount(); + final int[] verticesX = model.getVerticesX(); + final int[] verticesY = model.getVerticesY(); + final int[] verticesZ = model.getVerticesZ(); + + boolean anyVisible = false; + + // Make sure the arrays are big enough + while (projectedVerticesX.length < vertexCount) + { + int newSize = nextPowerOfTwo(vertexCount); + projectedVerticesX = new int[newSize]; + projectedVerticesY = new int[newSize]; + projectedVerticesRenderable = new boolean[newSize]; + } + + for (int i = 0; i < vertexCount; i++) + { + int vx = verticesX[i]; + int vy = verticesZ[i]; + int vz = verticesY[i]; + int vh; // Value holder + + // Rotate based on orientation + vh = vx * orientationCos + vy * orientationSin >> 16; + vy = vy * orientationCos - vx * orientationSin >> 16; + vx = vh; + + // Translate to local coords + vx += localX; + vy += localY; + vz += localZ; + + // Translate to camera + vx -= cameraX; + vy -= cameraY; + vz -= cameraZ; + + // Transform to canvas + vh = vx * yawCos + vy * yawSin >> 16; + vy = vy * yawCos - vx * yawSin >> 16; + vx = vh; + vh = vz * pitchCos - vy * pitchSin >> 16; + vz = vz * pitchSin + vy * pitchCos >> 16; + vy = vh; + + if (vz >= 50) + { + projectedVerticesX[i] = (clipX1 + clipX2) / 2 + vx * scale / vz; + projectedVerticesY[i] = (clipY1 + clipY2) / 2 + vy * scale / vz; + + projectedVerticesRenderable[i] = true; + anyVisible |= + projectedVerticesX[i] >= clipX1 && projectedVerticesX[i] < clipX2 && + projectedVerticesY[i] >= clipY1 && projectedVerticesY[i] < clipY2; + } + else + { + projectedVerticesRenderable[i] = false; + } + } + + return anyVisible; + } + + /** + * Simulate rendering of the model and puts every pixel of the wireframe of + * the non-culled and non-transparent faces into the outline pixel queue. + */ + 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 (projectedVerticesRenderable[indices1[i]] && + projectedVerticesRenderable[indices2[i]] && + projectedVerticesRenderable[indices3[i]] && + // 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 an outline of the pixels in the outline queue to an image + * + * @param image The image to draw the outline to + * @param outlineWidth The width of the outline + * @param innerColor The color of the pixels of the outline closest to the model + * @param outerColor The color of the pixels of the outline furthest away from the model + */ + private void renderOutline(BufferedImage image, int outlineWidth, Color innerColor, Color outerColor) + { + int[] imageData = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + List ps = getPriorityList(outlineWidth); + + for (PixelDistanceAlpha p : ps) + { + int color; + int alpha; + if (outlineWidth == 1) + { + color = + ((innerColor.getRed() + outerColor.getRed()) << 15) | + ((innerColor.getGreen() + outerColor.getGreen() << 7)) | + ((innerColor.getBlue() + outerColor.getBlue() >> 1)); + alpha = (innerColor.getAlpha() + outerColor.getAlpha()) >> 1; + } + else + { + int outerAlpha = p.getOuterAlpha(); + int innerAlpha = 255 - outerAlpha; + int innerAlphaFraction = (innerAlpha * innerColor.getAlpha()) / 255; + int outerAlphaFraction = (outerAlpha * outerColor.getAlpha()) / 255; + alpha = innerAlphaFraction + outerAlphaFraction; + if (alpha != 0) + { + color = + ((innerColor.getRed() * innerAlphaFraction + + outerColor.getRed() * outerAlphaFraction) / alpha << 16) | + ((innerColor.getGreen() * innerAlphaFraction + + outerColor.getGreen() * outerAlphaFraction) / alpha << 8) | + ((innerColor.getBlue() * innerAlphaFraction + + outerColor.getBlue() * outerAlphaFraction) / alpha); + } + else + { + color = 0; + } + } + + final int distArrayPos = p.getDistArrayPos(); + final int nextDistArrayPosY = distArrayPos + outlineArrayWidth; + final int nextDistArrayPosX = distArrayPos + 1; + ensureMinimumOutlineQueueSize(nextDistArrayPosX, outlinePixelsLengths[distArrayPos] * 2); + ensureMinimumOutlineQueueSize(nextDistArrayPosY, outlinePixelsLengths[distArrayPos] * 2); + + // The following 3 branches do the same thing, but when the requirements are simple, + // there are less checks needed which can give a performance boost. + if (alpha == 255) + { + if (outlineWidth == 1) + { + for (int i2 = 0; i2 < outlinePixelsLengths[distArrayPos]; i2++) + { + int pixelPos = outlinePixels[distArrayPos][i2]; + int x = pixelPos % imageWidth; + int y = pixelPos / imageWidth; + if (x < clipX1 || x >= clipX2 || + y < clipY1 || y >= clipY2 || + visited[pixelPos] == currentVisitedNumber) + { + continue; + } + + imageData[pixelPos] = color; + } + } + else + { + for (int i2 = 0; i2 < outlinePixelsLengths[distArrayPos]; i2++) + { + int pixelPos = outlinePixels[distArrayPos][i2]; + int x = pixelPos % imageWidth; + int y = pixelPos / imageWidth; + if (x < clipX1 || x >= clipX2 || + y < clipY1 || y >= clipY2 || + visited[pixelPos] == currentVisitedNumber) + { + continue; + } + visited[pixelPos] = currentVisitedNumber; + + imageData[pixelPos] = color; + + if (pixelPos % imageWidth != 0) + { + outlinePixels[nextDistArrayPosX][outlinePixelsLengths[nextDistArrayPosX]++] = pixelPos - 1; + } + if ((pixelPos + 1) % imageWidth != 0) + { + outlinePixels[nextDistArrayPosX][outlinePixelsLengths[nextDistArrayPosX]++] = pixelPos + 1; + } + outlinePixels[nextDistArrayPosY][outlinePixelsLengths[nextDistArrayPosY]++] = pixelPos - imageWidth; + outlinePixels[nextDistArrayPosY][outlinePixelsLengths[nextDistArrayPosY]++] = pixelPos + imageWidth; + } + } + } + else + { + for (int i2 = 0; i2 < outlinePixelsLengths[distArrayPos]; i2++) + { + int pixelPos = outlinePixels[distArrayPos][i2]; + int x = pixelPos % imageWidth; + int y = pixelPos / imageWidth; + if (x < clipX1 || x >= clipX2 || + y < clipY1 || y >= clipY2 || + visited[pixelPos] == currentVisitedNumber) + { + continue; + } + visited[pixelPos] = currentVisitedNumber; + + imageData[pixelPos] = + ((((color & 0xFF0000) * alpha + (imageData[pixelPos] & 0xFF0000) * (255 - alpha)) / 255) & 0xFF0000) + + ((((color & 0xFF00) * alpha + (imageData[pixelPos] & 0xFF00) * (255 - alpha)) / 255) & 0xFF00) + + ((((color & 0xFF) * alpha + (imageData[pixelPos] & 0xFF) * (255 - alpha)) / 255) & 0xFF); + + if (pixelPos % imageWidth != 0) + { + outlinePixels[nextDistArrayPosX][outlinePixelsLengths[nextDistArrayPosX]++] = pixelPos - 1; + } + if ((pixelPos + 1) % imageWidth != 0) + { + outlinePixels[nextDistArrayPosX][outlinePixelsLengths[nextDistArrayPosX]++] = pixelPos + 1; + } + outlinePixels[nextDistArrayPosY][outlinePixelsLengths[nextDistArrayPosY]++] = pixelPos - imageWidth; + outlinePixels[nextDistArrayPosY][outlinePixelsLengths[nextDistArrayPosY]++] = pixelPos + imageWidth; + } + } + } + } + + /** + * 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 innerColor The color of the pixels of the outline closest to the model + * @param outerColor The color of the pixels of the outline furthest away from the model + */ + private void drawModelOutline(Model model, int localX, int localY, int localZ, int orientation, int outlineWidth, Color innerColor, Color outerColor) + { + if (outlineWidth <= 0) + { + return; + } + + isReset = false; + usedSinceLastCheck = true; + + MainBufferProvider bufferProvider = (MainBufferProvider) client.getBufferProvider(); + BufferedImage image = (BufferedImage) bufferProvider.getImage(); + + clipX1 = client.getViewportXOffset(); + clipY1 = client.getViewportYOffset(); + clipX2 = client.getViewportWidth() + clipX1; + clipY2 = client.getViewportHeight() + clipY1; + imageWidth = image.getWidth(); + imageHeight = image.getHeight(); + final int pixelAmount = imageWidth * imageHeight; + + resetVisited(pixelAmount); + resetOutline(outlineWidth); + + 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; + } + + simulateModelRasterizationForOutline(model); + + renderOutline(image, outlineWidth, innerColor, outerColor); + } + + public void drawOutline(NPC npc, int outlineWidth, Color color) + { + drawOutline(npc, outlineWidth, color, color); + } + + public void drawOutline(NPC npc, int outlineWidth, Color innerColor, Color outerColor) + { + 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, innerColor, outerColor); + } + } + + public void drawOutline(Player player, int outlineWidth, Color color) + { + drawOutline(player, outlineWidth, color, color); + } + + public void drawOutline(Player player, int outlineWidth, Color innerColor, Color outerColor) + { + LocalPoint lp = player.getLocalLocation(); + if (lp != null) + { + drawModelOutline(player.getModel(), lp.getX(), lp.getY(), + Perspective.getTileHeight(client, lp, client.getPlane()), + player.getCurrentOrientation(), outlineWidth, innerColor, outerColor); + } + } + + public void drawOutline(GameObject gameObject, int outlineWidth, Color innerColor, Color outerColor) + { + LocalPoint lp = gameObject.getLocalLocation(); + if (lp != null) + { + drawModelOutline(gameObject.getModel(), lp.getX(), lp.getY(), + Perspective.getTileHeight(client, lp, gameObject.getPlane()), + gameObject.getRsOrientation(), outlineWidth, innerColor, outerColor); + } + } + + public void drawOutline(GroundObject groundObject, int outlineWidth, Color innerColor, Color outerColor) + { + LocalPoint lp = groundObject.getLocalLocation(); + if (lp != null) + { + drawModelOutline(groundObject.getModel(), lp.getX(), lp.getY(), + Perspective.getTileHeight(client, lp, client.getPlane()), + 0, outlineWidth, innerColor, outerColor); + } + } + + private void drawOutline(ItemLayer tileItemPile, int outlineWidth, Color innerColor, Color outerColor) + { + LocalPoint lp = tileItemPile.getLocalLocation(); + if (lp != null) + { + Model model = tileItemPile.getModelBottom(); + if (model != null) + { + drawModelOutline(model, lp.getX(), lp.getY(), + Perspective.getTileHeight(client, lp, tileItemPile.getPlane()), + 0, outlineWidth, innerColor, outerColor); + } + + model = tileItemPile.getModelMiddle(); + if (model != null) + { + drawModelOutline(model, lp.getX(), lp.getY(), + Perspective.getTileHeight(client, lp, tileItemPile.getPlane()), + 0, outlineWidth, innerColor, outerColor); + } + + model = tileItemPile.getModelTop(); + if (model != null) + { + drawModelOutline(model, lp.getX(), lp.getY(), + Perspective.getTileHeight(client, lp, tileItemPile.getPlane()), + 0, outlineWidth, innerColor, outerColor); + } + } + } + + private void drawOutline(DecorativeObject decorativeObject, int outlineWidth, Color innerColor, Color outerColor) + { + LocalPoint lp = decorativeObject.getLocalLocation(); + if (lp != null) + { + Model model = decorativeObject.getModel1(); + if (model != null) + { + drawModelOutline(model, + lp.getX() + decorativeObject.getXOffset(), + lp.getY() + decorativeObject.getYOffset(), + Perspective.getTileHeight(client, lp, decorativeObject.getPlane()), + decorativeObject.getOrientation(), outlineWidth, innerColor, outerColor); + } + + model = decorativeObject.getModel2(); + if (model != null) + { + // Offset is not used for the second model + drawModelOutline(model, lp.getX(), lp.getY(), + Perspective.getTileHeight(client, lp, decorativeObject.getPlane()), + decorativeObject.getOrientation(), outlineWidth, innerColor, outerColor); + } + } + } + + private void drawOutline(WallObject wallObject, int outlineWidth, Color innerColor, Color outerColor) + { + LocalPoint lp = wallObject.getLocalLocation(); + if (lp != null) + { + Model model = wallObject.getModelA(); + if (model != null) + { + drawModelOutline(model, lp.getX(), lp.getY(), + Perspective.getTileHeight(client, lp, wallObject.getPlane()), + wallObject.getOrientationA(), outlineWidth, innerColor, outerColor); + } + + model = wallObject.getModelB(); + if (model != null) + { + drawModelOutline(model, lp.getX(), lp.getY(), + Perspective.getTileHeight(client, lp, wallObject.getPlane()), + wallObject.getOrientationB(), outlineWidth, innerColor, outerColor); + } + } + } + + public void drawOutline(TileObject tileObject, int outlineWidth, Color color) + { + drawOutline(tileObject, outlineWidth, color, color); + } + + public void drawOutline(TileObject tileObject, + int outlineWidth, Color innerColor, Color outerColor) + { + if (tileObject instanceof GameObject) + { + drawOutline((GameObject) tileObject, outlineWidth, innerColor, outerColor); + } + else if (tileObject instanceof GroundObject) + { + drawOutline((GroundObject) tileObject, outlineWidth, innerColor, outerColor); + } + else if (tileObject instanceof ItemLayer) + { + drawOutline((ItemLayer) tileObject, outlineWidth, innerColor, outerColor); + } + else if (tileObject instanceof DecorativeObject) + { + drawOutline((DecorativeObject) tileObject, outlineWidth, innerColor, outerColor); + } + else if (tileObject instanceof WallObject) + { + drawOutline((WallObject) tileObject, outlineWidth, innerColor, outerColor); + } + } + + @Value + @RequiredArgsConstructor + class PixelDistanceAlpha + { + private final int outerAlpha; + private final int distArrayPos; + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/com/openosrs/client/ui/overlay/OverlayUtil.java b/runelite-client/src/main/java/com/openosrs/client/ui/overlay/OverlayUtil.java index 06fd875d6d..0b21683877 100644 --- a/runelite-client/src/main/java/com/openosrs/client/ui/overlay/OverlayUtil.java +++ b/runelite-client/src/main/java/com/openosrs/client/ui/overlay/OverlayUtil.java @@ -1,13 +1,14 @@ package com.openosrs.client.ui.overlay; -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Graphics2D; -import java.awt.Polygon; -import net.runelite.api.Client; -import net.runelite.api.Perspective; + +import net.runelite.api.*; +import net.runelite.api.Point; import net.runelite.api.coords.LocalPoint; import net.runelite.api.coords.WorldPoint; +import net.runelite.api.vars.InterfaceTab; +import net.runelite.api.widgets.Widget; + +import java.awt.*; public class OverlayUtil extends net.runelite.client.ui.overlay.OverlayUtil { @@ -39,4 +40,45 @@ public class OverlayUtil extends net.runelite.client.ui.overlay.OverlayUtil graphics.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), fillAlpha)); graphics.fill(poly); } + + public static Rectangle renderPrayerOverlay(Graphics2D graphics, Client client, Prayer prayer, Color color) + { + Widget widget = client.getWidget(prayer.getWidgetInfo()); + + if (widget == null || client.getVar(VarClientInt.INVENTORY_TAB) != InterfaceTab.PRAYER.getId()) + { + return null; + } + + Rectangle bounds = widget.getBounds(); + renderPolygon(graphics, rectangleToPolygon(bounds), color); + return bounds; + } + + private static Polygon rectangleToPolygon(Rectangle rect) + { + int[] xpoints = {rect.x, rect.x + rect.width, rect.x + rect.width, rect.x}; + int[] ypoints = {rect.y, rect.y, rect.y + rect.height, rect.y + rect.height}; + + return new Polygon(xpoints, ypoints, 4); + } + + public static void renderTextLocation(Graphics2D graphics, String txtString, int fontSize, int fontStyle, Color fontColor, Point canvasPoint, boolean shadows, int yOffset) + { + graphics.setFont(new Font("Arial", fontStyle, fontSize)); + if (canvasPoint != null) + { + final Point canvasCenterPoint = new Point( + canvasPoint.getX(), + canvasPoint.getY() + yOffset); + final Point canvasCenterPoint_shadow = new Point( + canvasPoint.getX() + 1, + canvasPoint.getY() + 1 + yOffset); + if (shadows) + { + renderTextLocation(graphics, canvasCenterPoint_shadow, txtString, Color.BLACK); + } + renderTextLocation(graphics, canvasCenterPoint, txtString, fontColor); + } + } } diff --git a/runelite-client/src/main/java/com/openosrs/client/util/ImageUtil.java b/runelite-client/src/main/java/com/openosrs/client/util/ImageUtil.java index 3282e61c98..599eff1ac4 100644 --- a/runelite-client/src/main/java/com/openosrs/client/util/ImageUtil.java +++ b/runelite-client/src/main/java/com/openosrs/client/util/ImageUtil.java @@ -1,8 +1,12 @@ package com.openosrs.client.util; +import net.runelite.api.Client; +import net.runelite.api.SpritePixels; + import java.awt.Color; import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; +import java.util.Arrays; import java.util.function.Predicate; public class ImageUtil extends net.runelite.client.util.ImageUtil @@ -56,4 +60,134 @@ public class ImageUtil extends net.runelite.client.util.ImageUtil } return image; } + + /** + * Draw fg centered on top of bg + */ + public static SpritePixels mergeSprites(final Client client, final SpritePixels bg, final SpritePixels fg) + { + assert fg.getHeight() <= bg.getHeight() && fg.getWidth() <= bg.getWidth() : "Background has to be larger than foreground"; + + final int[] canvas = Arrays.copyOf(bg.getPixels(), bg.getWidth() * bg.getHeight()); + final SpritePixels result = client.createSpritePixels(canvas, bg.getWidth(), bg.getHeight()); + + final int bgWid = bg.getWidth(); + final int fgHgt = fg.getHeight(); + final int fgWid = fg.getWidth(); + + final int xOffset = (bgWid - fgWid) / 2; + final int yOffset = (bg.getHeight() - fgHgt) / 2; + + final int[] fgPixels = fg.getPixels(); + + for (int y1 = yOffset, y2 = 0; y2 < fgHgt; y1++, y2++) + { + int i1 = y1 * bgWid + xOffset; + int i2 = y2 * fgWid; + + for (int x = 0; x < fgWid; x++, i1++, i2++) + { + if (fgPixels[i2] > 0) + { + canvas[i1] = fgPixels[i2]; + } + } + } + + return result; + } + + /** + * Resize Sprite sprite to given width (newW) and height (newH) + */ + public static SpritePixels resizeSprite(final Client client, final SpritePixels sprite, int newW, int newH) + { + assert newW > 0 && newH > 0; + + final int oldW = sprite.getWidth(); + final int oldH = sprite.getHeight(); + + if (oldW == newW && oldH == newH) + { + return sprite; + } + + final int[] canvas = new int[newW * newH]; + final int[] pixels = sprite.getPixels(); + + final SpritePixels result = client.createSpritePixels(canvas, newW, newH); + + int pixelX = 0; + int pixelY = 0; + + final int oldMaxW = sprite.getMaxWidth(); + final int oldMaxH = sprite.getMaxHeight(); + + final int pixelW = (oldMaxW << 16) / newW; + final int pixelH = (oldMaxH << 16) / newH; + + int xOffset = 0; + int yOffset = 0; + + int canvasIdx; + if (sprite.getOffsetX() > 0) + { + canvasIdx = (pixelW + (sprite.getOffsetX() << 16) - 1) / pixelW; + xOffset += canvasIdx; + pixelX += canvasIdx * pixelW - (sprite.getOffsetX() << 16); + } + + if (sprite.getOffsetY() > 0) + { + canvasIdx = (pixelH + (sprite.getOffsetY() << 16) - 1) / pixelH; + yOffset += canvasIdx; + pixelY += canvasIdx * pixelH - (sprite.getOffsetY() << 16); + } + + if (oldW < oldMaxW) + { + newW = (pixelW + ((oldW << 16) - pixelX) - 1) / pixelW; + } + + if (oldH < oldMaxH) + { + newH = (pixelH + ((oldH << 16) - pixelY) - 1) / pixelH; + } + + canvasIdx = xOffset + yOffset * newW; + int canvasOffset = 0; + if (yOffset + newH > newH) + { + newH -= yOffset + newH - newH; + } + + int tmp; + if (yOffset < 0) + { + tmp = -yOffset; + newH -= tmp; + canvasIdx += tmp * newW; + pixelY += pixelH * tmp; + } + + if (newW + xOffset > newW) + { + tmp = newW + xOffset - newW; + newW -= tmp; + canvasOffset += tmp; + } + + if (xOffset < 0) + { + tmp = -xOffset; + newW -= tmp; + canvasIdx += tmp; + pixelX += pixelW * tmp; + canvasOffset += tmp; + } + + client.scaleSprite(canvas, pixels, 0, pixelX, pixelY, canvasIdx, canvasOffset, newW, newH, pixelW, pixelH, oldW); + + return result; + } } diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigItem.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigItem.java index a9a511cc08..dfebdb618c 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigItem.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigItem.java @@ -24,6 +24,8 @@ */ package net.runelite.client.config; +import com.openosrs.client.OpenOSRS; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -48,4 +50,10 @@ public @interface ConfigItem boolean secret() default false; String section() default ""; + + /* + OpenOSRS Lazy Helpers tm + */ + Class enumClass() default OpenOSRS.class; + String unhide() default ""; } diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigSection.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigSection.java index 4a0f722d3f..6e7b905379 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigSection.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigSection.java @@ -24,6 +24,8 @@ */ package net.runelite.client.config; +import com.openosrs.client.OpenOSRS; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -40,4 +42,9 @@ public @interface ConfigSection int position(); boolean closedByDefault() default false; + + /* + OpenOSRS Lazy Helpers tm + */ + String keyName() default ""; } diff --git a/runelite-client/src/main/java/net/runelite/client/config/Units.java b/runelite-client/src/main/java/net/runelite/client/config/Units.java index 45d54df164..ca872bfa80 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/Units.java +++ b/runelite-client/src/main/java/net/runelite/client/config/Units.java @@ -42,8 +42,12 @@ public @interface Units String MINUTES = " mins"; String PERCENT = "%"; String PIXELS = "px"; + String POINTS = "pt"; String SECONDS = "s"; String TICKS = " ticks"; + String LEVELS = " lvls"; + String FPS = " fps"; + String GP = " GP"; String value(); }