1115 lines
32 KiB
Java
1115 lines
32 KiB
Java
/*
|
|
* Copyright (c) 2018, Woox <https://github.com/wooxsolo>
|
|
* 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;
|
|
import net.runelite.client.task.Scheduler;
|
|
|
|
@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<List<PixelDistanceAlpha>> precomputedDistancePriorities;
|
|
|
|
@Inject
|
|
private ModelOutlineRenderer(Client client, Scheduler scheduler)
|
|
{
|
|
this.client = client;
|
|
scheduler.registerObject(this);
|
|
|
|
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<PixelDistanceAlpha> 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<PixelDistanceAlpha> 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<PixelDistanceAlpha> 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;
|
|
}
|
|
} |