/** Draws a texture wherein are spots of "heat" where a pixel's spotColor is of highest alpha, fading as distance from heat
* spots increases.
* @param width
* @param height
* @param spotColor The color of the spots, ignores alpha.
* @param density
* @return */
public static Pixmap drawHeatSpots (int width, int height, Color spotColor, Magnitude density) {
long time = TimeUtils.millis();
Pixmap p = new Pixmap(width, height, Format.RGBA8888);
int spotCount;
int divisor;
if (density == Magnitude.high)
divisor = 5000;
else if (density == Magnitude.medium)
divisor = 6500;
else if (density == Magnitude.low)
divisor = 8000;
else
throw new IllegalArgumentException(density.name() + " is an invalid Magnitude");
spotCount = width * height / divisor;
ObjectSet<Vector2> spots = new ObjectSet<>();
for (int i = 0; i < spotCount;) {
Vector2 v = new Vector2(MathUtils.random(width), MathUtils.random(height));
if (spots.add(v)) i++;
}
float[][] alphas = new float[width][height];
Color c = new Color(spotColor);
for (Vector2 v : spots) {
affectNearbyAlphas(v, alphas, 425);
}
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
c.a = alphas[x][y];
if (c.a > 1)
c.a = 1;
else if (c.a < 0) c.a = 0;
p.drawPixel(x, y, Color.rgba8888(c));
}
}
System.out.println(TimeUtils.timeSinceMillis(time));
return p;
}
private static double calcAlpha (Vector2 v1, int x, int y, int bound) {
float dx = v1.x - x;
float dy = v1.y - y;
double z = Math.sqrt(dx * dx + dy * dy);
return 1.25f / (z);
}
private static void affectNearbyAlphas (Vector2 spot, float[][] alphas, int radius) {
int minX = Math.max((int)spot.x - radius, 0);
int maxX = Math.min((int)spot.x + radius + 1, alphas.length);
int minY = Math.max((int)spot.y - radius, 0);
int maxY = Math.min((int)spot.y + radius + 1, alphas[0].length);
for (int x = minX; x < maxX; x++) {
for (int y = minY; y < maxY; y++) {
float val = (x - spot.x) * (x - spot.x) / (radius * radius) + (y - spot.y) * (y - spot.y) / (radius * radius);
if (val > 1) continue;
alphas[x][y] += calcAlpha(spot, x, y, radius);
}
}
}
This is how I have improved my method. It now uses an ObjectSet, which uses hashes for searching, making it faster than an array search. It also calculates the alphas differently. Instead of each Spot affecting the entire image, it only affects pixels within a certain radius (currently at 425, but this can be changed).
I also made it so that the radius truly works like a radius (i.e. only pixels in the circle of that radius are checked) to prevent square outlines around Spots.
The new method creates an image like this at high
density in about 1.3
seconds.
The final thing I had to do was change the return class from Texture
to Pixmap
because I am using multi-threading to load the image while showing something else, and there is no GL context on threads other than the main one (in LibGdx), which is required to create a Texture.
Any further critique is welcome, but I think this is the best visual-to-performance trade-off I could get.
EDIT
I implemented Emily's suggestion and this is what it looks like:
/** Draws a texture wherein are spots of "heat" where a pixel's spotColor is of highest alpha, fading as distance from heat
* spots increases. Takes ~.3 seconds for 720p.
* @param width
* @param height
* @param spotColor The color of the spots, ignores alpha.
* @param density
* @return */
public static Pixmap drawHeatSpots2 (int width, int height, Color spotColor, Magnitude density) {
long time = TimeUtils.millis();
Pixmap p = new Pixmap(width, height, Format.RGBA8888);
int spotCount;
int divisor;
if (density == Magnitude.high)
divisor = 5000;
else if (density == Magnitude.medium)
divisor = 6500;
else if (density == Magnitude.low)
divisor = 8000;
else
throw new IllegalArgumentException(density.name() + " is an invalid Magnitude");
spotCount = width * height / divisor;
final int radius = 425;
final int radius2 = radius * radius;
float[] alphaMap = new float[(radius + 1) * (radius + 1)];
// calculate alphas for quadrant 1 (includes column and row of spot)
for (int y = 0; y < radius + 1; y++) {
// y == dy
float y2 = y * y;
for (int x = 0; x < radius + 1; x++) {
// x == dx
float x2 = x * x;
if (x2 + y2 > radius2) continue;
float val = (float)(1f / Math.sqrt(x2 + y2));
alphaMap[x + y * (radius + 1)] = val;
}
}
float[] alphaBuffer = new float[width * height];
for (int i = 0; i < spotCount; i++) {
affectNearbyAlphas2(MathUtils.random(width), MathUtils.random(height), alphaMap, alphaBuffer, width, height, radius);
}
Color c = new Color(spotColor);
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
c.a = alphaBuffer[x + y * width];
if (c.a > 1) c.a = 1;
p.drawPixel(x, y, Color.rgba8888(c));
}
}
System.out.println(TimeUtils.timeSinceMillis(time));
return p;
}
private static void affectNearbyAlphas2 (int spotX, int spotY, float[] alphas, float[] buffer, int width, int height,
int radius) {
int minX = Math.max(spotX - radius, 0);
int maxX = Math.min(spotX + radius + 1, width);
int minY = Math.max(spotY - radius, 0);
int maxY = Math.min(spotY + radius + 1, height);
for (int y = minY; y < maxY; y++) {
float dy = y - spotY;
if (dy < 0) dy *= -1;
for (int x = minX; x < maxX; x++) {
float dx = x - spotX;
if (dx < 0) dx *= -1;
buffer[x + y * width] += alphas[(int)(dx + dy * (radius + 1))];
}
}
}
This also uses the quicker variant by only calculating the alphas for 1 quadrant, which the symmetry of the spots allows. It only takes .3
seconds as opposed to the 1.3
seconds the above variant takes, for the same radius.
I still need to try out @maaartinus's solution. Seeing as how I brought the execution time down to ~.3
seconds, which is what he had, I don't think it's necessary.