Skip to content

Instantly share code, notes, and snippets.

@mattdesl
Last active March 24, 2020 04:13
Show Gist options
  • Save mattdesl/4383372 to your computer and use it in GitHub Desktop.
Save mattdesl/4383372 to your computer and use it in GitHub Desktop.
import java.nio.ByteBuffer;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Pixmap.Blending;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.utils.BufferUtils;
import com.badlogic.gdx.utils.GdxRuntimeException;
/**
* A simple set of software blur utilities for mobile applications.
*
* @author davedes, blur algorithm by Romain Guy
*/
public class BlurUtils {
/*
* Copyright (c) 2007, Romain Guy All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer. * 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. * Neither the name of the
* TimingFramework project nor the names of its contributors may be used to
* endorse or promote products derived from this software without specific
* prior written permission.
*
* 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.
*/
/**
* <p>
* Blurs the source pixels into the destination pixels. The force of the
* blur is specified by the radius which must be greater than 0.
* </p>
* <p>
* The source and destination pixels arrays are expected to be in the RGBA
* format.
* </p>
*
* @param srcPixels
* the source pixels
* @param dstPixels
* the destination pixels
* @param width
* the width of the source picture
* @param height
* the height of the source picture
* @param radius
* the radius of the blur effect
* @author Romain Guy <[email protected]>
*/
public static void blurPass(int[] srcPixels, int[] dstPixels, int width,
int height, int radius) {
final int windowSize = radius * 2 + 1;
final int radiusPlusOne = radius + 1;
int sumRed;
int sumGreen;
int sumBlue;
int sumAlpha;
int srcIndex = 0;
int dstIndex;
int pixel;
int[] sumLookupTable = new int[256 * windowSize];
for (int i = 0; i < sumLookupTable.length; i++) {
sumLookupTable[i] = i / windowSize;
}
int[] indexLookupTable = new int[radiusPlusOne];
if (radius < width) {
for (int i = 0; i < indexLookupTable.length; i++) {
indexLookupTable[i] = i;
}
} else {
for (int i = 0; i < width; i++) {
indexLookupTable[i] = i;
}
for (int i = width; i < indexLookupTable.length; i++) {
indexLookupTable[i] = width - 1;
}
}
for (int y = 0; y < height; y++) {
sumAlpha = sumRed = sumGreen = sumBlue = 0;
dstIndex = y;
pixel = srcPixels[srcIndex];
sumRed += radiusPlusOne * ((pixel >> 24) & 0xFF);
sumGreen += radiusPlusOne * ((pixel >> 16) & 0xFF);
sumBlue += radiusPlusOne * ((pixel >> 8) & 0xFF);
sumAlpha += radiusPlusOne * (pixel & 0xFF);
for (int i = 1; i <= radius; i++) {
pixel = srcPixels[srcIndex + indexLookupTable[i]];
sumRed += (pixel >> 24) & 0xFF;
sumGreen += (pixel >> 16) & 0xFF;
sumBlue += (pixel >> 8) & 0xFF;
sumAlpha += pixel & 0xFF;
}
for (int x = 0; x < width; x++) {
dstPixels[dstIndex] = sumLookupTable[sumRed] << 24
| sumLookupTable[sumGreen] << 16
| sumLookupTable[sumBlue] << 8
| sumLookupTable[sumAlpha];
dstIndex += height;
int nextPixelIndex = x + radiusPlusOne;
if (nextPixelIndex >= width) {
nextPixelIndex = width - 1;
}
int previousPixelIndex = x - radius;
if (previousPixelIndex < 0) {
previousPixelIndex = 0;
}
int nextPixel = srcPixels[srcIndex + nextPixelIndex];
int previousPixel = srcPixels[srcIndex + previousPixelIndex];
sumRed += (nextPixel >> 24) & 0xFF;
sumRed -= (previousPixel >> 24) & 0xFF;
sumGreen += (nextPixel >> 16) & 0xFF;
sumGreen -= (previousPixel >> 16) & 0xFF;
sumBlue += (nextPixel >> 8) & 0xFF;
sumBlue -= (previousPixel >> 8) & 0xFF;
sumAlpha += nextPixel & 0xFF;
sumAlpha -= previousPixel & 0xFF;
}
srcIndex += width;
}
}
/**
* Blurs (in both horizontal and vertical directions) the specified RGBA
* image with the given radius and iterations.
*
* @param inputRGBA
* the image pixels, in RGBA format
* @param width
* the width of the image in pixels
* @param height
* the height of the image in pixels
* @param radius
* the radius of the blur effect
* @param iterations
* the number of times to perform the blur; i.e. to increase
* quality
* @return the blurred pixels
*/
public static int[] blur(int[] inputRGBA, int width, int height,
int radius, int iterations) {
int[] srcPixels = new int[width * height];
int[] dstPixels = new int[width * height];
// copy input into srcPixels
System.arraycopy(inputRGBA, 0, srcPixels, 0, srcPixels.length);
for (int i = 0; i < iterations; i++) {
// horizontal pass
blurPass(srcPixels, dstPixels, width, height, radius);
// vertical pass
blurPass(dstPixels, srcPixels, height, width, radius);
}
// the result is now stored in srcPixels due to the 2nd pass
return srcPixels;
}
/**
* Convenience method to blur using ByteBuffers instead of arrays.
* Note that this requires unnecessary copies of data and is only
* for convenience; a proper solution would be
* to re-write the blur algorithm using a ByteBuffer.
*
* @param inputRGBA
* @param width
* @param height
* @param radius
* @param iterations
* @return
*/
public static ByteBuffer blur(ByteBuffer inputRGBA, int width,
int height, int radius, int iterations) {
if (inputRGBA.limit() != (width * height * 4))
throw new IllegalArgumentException(
"inputRGBA must be in RGBA format");
int[] pixels = pack(inputRGBA);
int[] out = blur(pixels, width, height, radius, iterations);
return unpack(out);
}
/**
* Converts an RGBA byte buffer into an array of RGBA packed ints.
*
* @param rgba
* @return
*/
public static int[] pack(ByteBuffer rgba) {
int[] pixels = new int[rgba.limit() / 4];
for (int i = 0; i < pixels.length; i++) {
int r = rgba.get() & 0xFF;
int g = rgba.get() & 0xFF;
int b = rgba.get() & 0xFF;
int a = rgba.get() & 0xFF;
pixels[i] = (r << 24) | (g << 16) | (b << 8) | a;
}
return pixels;
}
/**
* Unpacks the RGBA pixels array into a ByteBuffer with red, green, blue,
* and alpha bytes in order; it is then flipped to "read mode" before being
* returned.
*
* @param pixels
* the pixels to use
* @return the new byte buffer using RGBA bytes
*/
public static ByteBuffer unpack(int[] pixels) {
ByteBuffer buf = BufferUtils.newByteBuffer(pixels.length * 4);
for (int src = 0; src < pixels.length; src++) {
int value = pixels[src];
buf.put((byte) ((value & 0xff000000) >>> 24))
.put((byte) ((value & 0x00ff0000) >>> 16))
.put((byte) ((value & 0x0000ff00) >>> 8))
.put((byte) ((value & 0x000000ff)));
}
buf.flip();
return buf;
}
/**
* A convenience method to apply the blur to the entire Pixmap.
*
* @param pixmap
* the pixmap to blur
* @param radius
* the radius of the blur effect
* @param iterations
* the number of iterations to blur
* @param disposePixmap
* whether to dispose the given pixmap after blurring
* @return a new Pixmap containing the blurred image
*/
public static Pixmap blur(Pixmap pixmap, int radius, int iterations,
boolean disposePixmap) {
return blur(pixmap, 0, 0, pixmap.getWidth(), pixmap.getHeight(), 0, 0,
pixmap.getWidth(), pixmap.getHeight(), radius, iterations,
disposePixmap);
}
/**
* Blurs the specified pixmap with the given source and destination regions.
*
* The pixmap does not need to be in RGBA8888 format, however, it is
* recommended for better performance.
*
* A new pixmap will be returned containing the blurred image. The old
* pixmap will only be disposed of if <tt>disposePixmap</tt> returns true.
*
* @param pixmap
* the pixmap to blur
* @param srcx
* the x of the pixmap region to blur
* @param srcy
* the y of the pixmap region to blur
* @param srcwidth
* the width of the pixmap region to blur
* @param srcheight
* the height of the pixmap region to blur
* @param dstx
* the destination x to place the blurred image on the resulting
* pixmap
* @param dsty
* the destination y to place the blurred image on the resulting
* pixmap
* @param dstwidth
* the desired width of the resulting pixmap
* @param dstheight
* the desired height of the resulting pixmap
* @param radius
* the radius of the blur effect, in pixels
* @param iterations
* the number of iterations to apply the blur
* @param disposePixmap
* whether to dispose the specified pixmap after applying the
* blur
* @return a new RGBA8888 Pixmap containing the blurred image
*/
public static Pixmap blur(Pixmap pixmap, int srcx, int srcy, int srcwidth,
int srcheight, int dstx, int dsty, int dstwidth, int dstheight,
int radius, int iterations, boolean disposePixmap) {
boolean srcEq = srcx == 0 && srcy == 0 && srcwidth == pixmap.getWidth()
&& srcheight == pixmap.getHeight();
boolean dstEq = dstx == 0 && dsty == 0 && dstwidth == pixmap.getWidth()
&& dstheight == pixmap.getHeight();
// we may need to re-draw the pixmap if a different region or format is
// passed
if (pixmap.getFormat() != Format.RGBA8888 || !srcEq || !dstEq) {
Pixmap tmp = new Pixmap(dstwidth, dstheight, Format.RGBA8888);
tmp.drawPixmap(pixmap, srcx, srcy, srcwidth, srcheight, dstx, dsty,
dstwidth, dstheight);
if (disposePixmap) {
pixmap.dispose(); // discard old pixmap
disposePixmap = false;
}
pixmap = tmp;
}
// blur the pixmap
ByteBuffer blurred = BlurUtils.blur(pixmap.getPixels(), dstwidth,
dstheight, radius, iterations);
Pixmap newPixmap = new Pixmap(dstwidth, dstheight, Format.RGBA8888);
ByteBuffer newRGBA = newPixmap.getPixels();
newRGBA.clear();
newRGBA.put(blurred);
newRGBA.flip();
if (disposePixmap)
pixmap.dispose();
return newPixmap;
}
/**
* Blurs the mipmaps of the currently bound texture with the given settings.
*
* For each mipmap level, the image will be scaled to half (using
* nearest-neighbour scaling) and then blurred in software, before sending
* the bytes to GL.
*
* The first mipmap level should already be uploaded to GL, i.e. through the
* Texture constructor. No blur will be applied to it.
*
* The texture needs to have been created with format RGBA8888 to work
* correctly on all devices.
*
* @param pixmap
* the original pixmap to work with
* @param textureWidth
* the width of the texture
* @param textureHeight
* the height of the texture
* @param radius
* the radius of the blur to use at each level
* @param iterations
* the number of iterations to blur at each level
* @param disposePixmap
* whether to dispose the specified pixmap after building the
* mipmaps
*/
public static void generateBlurredMipmaps(Pixmap pixmap, int textureWidth,
int textureHeight, int radius, int iterations,
boolean disposePixmap) {
if (textureWidth != textureHeight)
throw new GdxRuntimeException(
"texture width and height must be square when using mipmapping.");
Pixmap origPixmap = pixmap;
int width = pixmap.getWidth() / 2;
int height = pixmap.getHeight() / 2;
int level = 1;
Blending blending = Pixmap.getBlending();
Pixmap.setBlending(Blending.None);
// for each mipmap level > 0 ...
while (width > 0 && height > 0) {
// apply blur
pixmap = blur(origPixmap, 0, 0, origPixmap.getWidth(), origPixmap.getHeight(),
0, 0, width, height, radius, iterations, false);
// upload pixels
Gdx.gl.glTexImage2D(GL10.GL_TEXTURE_2D, level,
pixmap.getGLInternalFormat(), pixmap.getWidth(),
pixmap.getHeight(), 0, pixmap.getGLFormat(),
pixmap.getGLType(), pixmap.getPixels());
// reduce size for next level
width = pixmap.getWidth() / 2;
height = pixmap.getHeight() / 2;
level++;
//dispose pixmap at this level
pixmap.dispose();
// NOTE: We can play with the radius and iterations here, e.g.
// increment them for
// each level.
// radius++;
}
Pixmap.setBlending(blending);
if (disposePixmap) {
origPixmap.dispose();
}
}
}
@ahmed-ais
Copy link

Great utility. It needs fixes, however!

  • LibGDX does not support GL10 now I guess so change it into GL20.

  • Wrong calls for Pixmap.getBlending() and Pixmap.setBlending(...) that should be fixed by using pixmap instance instead of class.

@amutov
Copy link

amutov commented Sep 19, 2018

Great utility. But still cannot compile with GWT.

           [ERROR] Line 336: The method blur(int[], int, int, int, int) in the type BlurUtils is not applicable for the arguments (Buffer, int, int, int, int)
           [ERROR] Line 340: Type mismatch: cannot convert from Buffer to ByteBuffer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment