艺术效果:水纹
与火焰效果一样,借鉴粒子系统的思想,我们也可以在图像中实现类似水纹的艺术效果。
粒子系统基本上都要通过循环迭代实现,下面先看看本文方法实现的水纹效果。
第一张为原始图像,第二张为循环迭代50次后的水纹效果,第三张为通过该算法生成的动态GIF图的显示效果。
cat.jpg
cat-water.jpg
cat-water.gif
算法实现
我们通过一个单独的类来保存粒子的状态,这里直接给出代码,感兴趣的朋友可直接通过阅读代码了解实现细节。
import java.awt.image.*;
public class WaterRoutine {
java.util.Random r = new java.util.Random(System.currentTimeMillis());
int m_iWidth;
int m_iHeight;
boolean m_bDrawWithLight;
int m_iLightModifier;
public int m_iHpage;// The current heightfield
int m_density;// The water density - can change the density...
// the height fields
int[] m_iHeightField1;
int[] m_iHeightField2;
public int getrandom(int min, int max) {
return r.nextInt(max - min) + min;
}
public int getRGB(int r, int g, int b) {
return (0x00ff & r) << 16 | (0x00ff & g) << 8 | (0x00ff & b);
}
public int getBValue(int color) {
return color & 0x00ff;
}
public int getGValue(int color) {
return (color >> 8) & 0x00ff;
}
public int getRValue(int color) {
return (color >> 16) & 0x00ff;
}
public int getRGB(BufferedImage img, int offset) {
int x = offset % img.getWidth();
int y = offset / img.getWidth();
if (y >= img.getHeight())
y = img.getHeight() - 1;
return img.getRGB(x, y);
}
public void setRGB(BufferedImage img, int offset, int rgb) {
int x = offset % img.getWidth();
int y = offset / img.getWidth();
if (y >= img.getHeight())
y = img.getHeight() - 1;
img.setRGB(x, y, rgb);
}
public WaterRoutine() {
m_iHeightField1 = null;
m_iHeightField2 = null;
m_iWidth = 0;
m_iHeight = 0;
m_bDrawWithLight = true;
m_iLightModifier = 1;
m_iHpage = 0;
m_density = 5;
}
public void create(int iWidth, int iHeight) {
// Create our height fields
m_iHeightField1 = new int[(iWidth * iHeight)];
m_iHeightField2 = new int[(iWidth * iHeight)];
// Clear our height fields
for (int i = 0; i < iWidth * iHeight; i++) {
m_iHeightField1[i] = 0;
m_iHeightField2[i] = 0;
}
m_iWidth = iWidth;
m_iHeight = iHeight;
// Set our page to 0
m_iHpage = 0;
}
public void flattenWater() {
// Clear our height fields
for (int i = 0; i < m_iWidth * m_iHeight; i++) {
m_iHeightField1[i] = 0;
m_iHeightField2[i] = 0;
}
}
public void nullPos() {
calcWater(m_iHpage, m_density);
m_iHpage ^= 1;
}
public void render(BufferedImage pSrcImage, BufferedImage pTargetImage) {
// Yes they have to be the same size...(for now)
if (m_bDrawWithLight == false) {
drawWaterNoLight(m_iHpage, pSrcImage, pTargetImage);
} else {
drawWaterWithLight(m_iHpage, m_iLightModifier, pSrcImage, pTargetImage);
}
calcWater(m_iHpage, m_density);
m_iHpage ^= 1;
}
public void calcWater(int npage, int density) {
int newh;
int count = m_iWidth + 1;
int[] newptr;
int[] oldptr;
// Set up the pointers
if (npage == 0) {
newptr = m_iHeightField1;
oldptr = m_iHeightField2;
} else {
newptr = m_iHeightField2;
oldptr = m_iHeightField1;
}
int x, y;
// Sorry, this function might not be as readable as I'd like, because
// I optimized it somewhat. (enough to make me feel satisfied with it)
for (y = (m_iHeight - 1) * m_iWidth; count < y; count += 2) {
for (x = count + m_iWidth - 2; count < x; count++) {
// This does the eight-pixel method. It looks much better.
newh = ((oldptr[count + m_iWidth] + oldptr[count - m_iWidth] + oldptr[count + 1] + oldptr[count - 1]
+ oldptr[count - m_iWidth - 1] + oldptr[count - m_iWidth + 1] + oldptr[count + m_iWidth - 1]
+ oldptr[count + m_iWidth + 1]) >> 2) - newptr[count];
newptr[count] = newh - (newh >> density);
}
}
}
public void smoothWater(int npage) {
int newh;
int count = m_iWidth + 1;
int[] newptr;
int[] oldptr;
// Set up the pointers
if (npage == 0) {
newptr = m_iHeightField1;
oldptr = m_iHeightField2;
} else {
newptr = m_iHeightField2;
oldptr = m_iHeightField1;
}
int x, y;
// Sorry, this function might not be as readable as I'd like, because
// I optimized it somewhat. (enough to make me feel satisfied with it)
for (y = 1; y < m_iHeight - 1; y++) {
for (x = 1; x < m_iWidth - 1; x++) {
// This does the eight-pixel method. It looks much better.
newh = ((oldptr[count + m_iWidth] + oldptr[count - m_iWidth] + oldptr[count + 1] + oldptr[count - 1]
+ oldptr[count - m_iWidth - 1] + oldptr[count - m_iWidth + 1] + oldptr[count + m_iWidth - 1]
+ oldptr[count + m_iWidth + 1]) >> 3) + newptr[count];
newptr[count] = newh >> 1;
count++;
}
count += 2;
}
}
public void calcWaterBigFilter(int npage, int density) {
int newh;
int count = (2 * m_iWidth) + 2;
int[] newptr;
int[] oldptr;
// Set up the pointers
if (npage == 0) {
newptr = m_iHeightField1;
oldptr = m_iHeightField2;
} else {
newptr = m_iHeightField2;
oldptr = m_iHeightField1;
}
int x, y;
// Sorry, this function might not be as readable as I'd like, because
// I optimized it somewhat. (enough to make me feel satisfied with it)
for (y = 2; y < m_iHeight - 2; y++) {
for (x = 2; x < m_iWidth - 2; x++) {
// This does the 25-pixel method. It looks much okay.
newh = ((((oldptr[count + m_iWidth] + oldptr[count - m_iWidth] + oldptr[count + 1]
+ oldptr[count - 1]) << 1)
+ ((oldptr[count - m_iWidth - 1] + oldptr[count - m_iWidth + 1] + oldptr[count + m_iWidth - 1]
+ oldptr[count + m_iWidth + 1]))
+ ((oldptr[count - (m_iWidth * 2)] + oldptr[count + (m_iWidth * 2)] + oldptr[count - 2]
+ oldptr[count + 2]) >> 1)
+ ((oldptr[count - (m_iWidth * 2) - 1] + oldptr[count - (m_iWidth * 2) + 1]
+ oldptr[count + (m_iWidth * 2) - 1] + oldptr[count + (m_iWidth * 2) + 1]
+ oldptr[count - 2 - m_iWidth] + oldptr[count - 2 + m_iWidth]
+ oldptr[count + 2 - m_iWidth] + oldptr[count + 2 + m_iWidth]) >> 2)) >> 3)
- (newptr[count]);
newptr[count] = newh - (newh >> density);
count++;
}
count += 4;
}
}
public void HeightBlob(int x, int y, int radius, int height, int page) {
int rquad;
int cx, cy, cyq;
int left, top, right, bottom;
int[] newptr;
int[] oldptr;
// Set up the pointers
if (page == 0) {
newptr = m_iHeightField1;
oldptr = m_iHeightField2;
} else {
newptr = m_iHeightField2;
oldptr = m_iHeightField1;
}
rquad = radius * radius;
// Make a randomly-placed blob...
if (x < 0)
x = 1 + radius + r.nextInt(65536) % (m_iWidth - 2 * radius - 1);
if (y < 0)
y = 1 + radius + r.nextInt(65536) % (m_iHeight - 2 * radius - 1);
left = -radius;
right = radius;
top = -radius;
bottom = radius;
// Perform edge clipping...
if (x - radius < 1)
left -= (x - radius - 1);
if (y - radius < 1)
top -= (y - radius - 1);
if (x + radius > m_iWidth - 1)
right -= (x + radius - m_iWidth + 1);
if (y + radius > m_iHeight - 1)
bottom -= (y + radius - m_iHeight + 1);
for (cy = top; cy < bottom; cy++) {
cyq = cy * cy;
for (cx = left; cx < right; cx++) {
if (cx * cx + cyq < rquad)
newptr[m_iWidth * (cy + y) + (cx + x)] += height;
}
}
}
public void HeightBox(int x, int y, int radius, int height, int page) {
int cx, cy;
int left, top, right, bottom;
int[] newptr;
int[] oldptr;
// Set up the pointers
if (page == 0) {
newptr = m_iHeightField1;
oldptr = m_iHeightField2;
} else {
newptr = m_iHeightField2;
oldptr = m_iHeightField1;
}
if (x < 0)
x = 1 + radius + r.nextInt(65536) % (m_iWidth - 2 * radius - 1);
if (y < 0)
y = 1 + radius + r.nextInt(65536) % (m_iHeight - 2 * radius - 1);
left = -radius;
right = radius;
top = -radius;
bottom = radius;
// Perform edge clipping...
if (x - radius < 1)
left -= (x - radius - 1);
if (y - radius < 1)
top -= (y - radius - 1);
if (x + radius > m_iWidth - 1)
right -= (x + radius - m_iWidth + 1);
if (y + radius > m_iHeight - 1)
bottom -= (y + radius - m_iHeight + 1);
for (cy = top; cy < bottom; cy++) {
for (cx = left; cx < right; cx++) {
newptr[m_iWidth * (cy + y) + (cx + x)] = height;
}
}
}
public void WarpBlob(int x, int y, int radius, int height, int page) {
int cx, cy;
int left, top, right, bottom;
int square;
int radsquare = radius * radius;
int[] newptr;
int[] oldptr;
// Set up the pointers
if (page == 0) {
newptr = m_iHeightField1;
oldptr = m_iHeightField2;
} else {
newptr = m_iHeightField2;
oldptr = m_iHeightField1;
}
// radsquare = (radius*radius) << 8;
radsquare = (radius * radius);
height /= 64;
left = -radius;
right = radius;
top = -radius;
bottom = radius;
// Perform edge clipping...
if (x - radius < 1)
left -= (x - radius - 1);
if (y - radius < 1)
top -= (y - radius - 1);
if (x + radius > m_iWidth - 1)
right -= (x + radius - m_iWidth + 1);
if (y + radius > m_iHeight - 1)
bottom -= (y + radius - m_iHeight + 1);
for (cy = top; cy < bottom; cy++) {
for (cx = left; cx < right; cx++) {
square = cy * cy + cx * cx;
// square <<= 8;
if (square < radsquare) {
// Height[page][WATERWID*(cy+y) + cx+x]
// += (sqrt(radsquare)-sqrt(square))*height;
newptr[m_iWidth * (cy + y) + cx + x] += (int) ((radius - Math.sqrt(square)) * (float) (height));
}
}
}
}
public void sineBlob(int x, int y, int radius, int height, int page) {
int cx, cy;
int left, top, right, bottom;
int square, dist;
int radsquare = radius * radius;
float length = (float) ((1024.0 / (float) radius) * (1024.0 / (float) radius));
int[] newptr;
int[] oldptr;
// Set up the pointers
if (page == 0) {
newptr = m_iHeightField1;
oldptr = m_iHeightField2;
} else {
newptr = m_iHeightField2;
oldptr = m_iHeightField1;
}
if (x < 0)
x = 1 + radius + r.nextInt(65536) % (m_iWidth - 2 * radius - 1);
if (y < 0)
y = 1 + radius + r.nextInt(65536) % (m_iHeight - 2 * radius - 1);
// radsquare = (radius*radius) << 8;
radsquare = (radius * radius);
// height /= 8;
left = -radius;
right = radius;
top = -radius;
bottom = radius;
// Perform edge clipping...
if (x - radius < 1)
left -= (x - radius - 1);
if (y - radius < 1)
top -= (y - radius - 1);
if (x + radius > m_iWidth - 1)
right -= (x + radius - m_iWidth + 1);
if (y + radius > m_iHeight - 1)
bottom -= (y + radius - m_iHeight + 1);
for (cy = top; cy < bottom; cy++) {
for (cx = left; cx < right; cx++) {
square = cy * cy + cx * cx;
if (square < radsquare) {
dist = (int) (Math.sqrt(square * length));
newptr[m_iWidth * (cy + y) + cx + x] += (int) ((Math.cos(dist) + 0xffff) * (height)) >> 19;
}
}
}
}
public void drawWaterNoLight(int page, BufferedImage pSrcImage, BufferedImage pTargetImage) {
int dx, dy;
int x, y;
int c;
int offset = m_iWidth + 1;
int[] ptr = m_iHeightField1;
for (y = (m_iHeight - 1) * m_iWidth; offset < y; offset += 2) {
for (x = offset + m_iWidth - 2; offset < x; offset++) {
if (offset + m_iWidth >= m_iWidth * m_iHeight)
continue;
dx = ptr[offset] - ptr[offset + 1];
dy = ptr[offset] - ptr[offset + m_iWidth];
// c = pSrcImage[offset + m_iWidth*(dy>>3) + (dx>>3)];
c = getRGB(pSrcImage, offset + m_iWidth * (dy >> 3) + (dx >> 3));
// pTargetImage[offset] = c;
setRGB(pTargetImage, offset, c);
offset++;
if (offset + m_iWidth >= m_iWidth * m_iHeight)
continue;
dx = ptr[offset] - ptr[offset + 1];
dy = ptr[offset] - ptr[offset + m_iWidth];
// c = pSrcImage[offset + m_iWidth*(dy>>3) + (dx>>3)];
// pTargetImage[offset] = c;
c = getRGB(pSrcImage, offset + m_iWidth * (dy >> 3) + (dx >> 3));
setRGB(pTargetImage, offset, c);
}
}
}
public void drawWaterWithLight(int page, int LightModifier, BufferedImage pSrcImage, BufferedImage pTargetImage) {
int dx, dy;
int x, y;
int c;
int offset = m_iWidth + 1;
int lIndex;
int lBreak = m_iWidth * m_iHeight;
int[] ptr = m_iHeightField1;
for (y = (m_iHeight - 1) * m_iWidth; offset < y; offset += 2) {
for (x = offset + m_iWidth - 2; offset < x; offset++) {
if (offset + m_iWidth >= m_iWidth * m_iHeight)
continue;
dx = ptr[offset] - ptr[offset + 1];
dy = ptr[offset] - ptr[offset + m_iWidth];
lIndex = offset + m_iWidth * (dy >> 3) + (dx >> 3);
if (lIndex < lBreak && lIndex > 0) {
// c = pSrcImage[lIndex];// - (dx>>LightModifier);
c = getRGB(pSrcImage, lIndex);
// Now we shift it by the dx component...
//
c = getShiftedColor(c, dx);
// pTargetImage[offset] = c;
setRGB(pTargetImage, offset, c);
}
offset++;
if (offset + m_iWidth >= m_iWidth * m_iHeight)
continue;
dx = ptr[offset] - ptr[offset + 1];
dy = ptr[offset] - ptr[offset + m_iWidth];
lIndex = offset + m_iWidth * (dy >> 3) + (dx >> 3);
if (lIndex < lBreak && lIndex > 0) {
// c = pSrcImage[lIndex];// - (dx>>LightModifier);
c = getRGB(pSrcImage, lIndex);
c = getShiftedColor(c, dx);
// temp[offset] = (c < 0) ? 0 : (c > 255) ? 255 : c;
// pTargetImage[offset] = c;
setRGB(pTargetImage, offset, c);
}
}
}
}
public int getShiftedColor(int color, int shift) {
int R;
int G;
int B;
int ir;
int ig;
int ib;
R = getRValue(color) - shift;
G = getGValue(color) - shift;
B = getBValue(color) - shift;
ir = (R < 0) ? 0 : (R > 255) ? 255 : R;
ig = (G < 0) ? 0 : (G > 255) ? 255 : G;
ib = (B < 0) ? 0 : (B > 255) ? 255 : B;
return getRGB(ir, ig, ib);
}
}
测试代码
下面是测试代码:
public class GifEffectWaterTest {
public static void waterImage(int xpos, int ypos) throws IOException {
BufferedImage img = ImageIO.read(new File("D:\\eclipse-workspace\\image-toolkit\\images\\cat.jpg"));
WaterRoutine wr = new WaterRoutine();
wr.create(img.getWidth(), img.getHeight());
wr.HeightBlob(xpos, ypos, 50, 500, wr.m_iHpage);
for (int i = 0; i < 50; i++) {
wr.nullPos();
}
BufferedImage resultImage = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB);
wr.render(img, resultImage);
ImageIO.write(resultImage, "jpeg", new File("D:\\eclipse-workspace\\image-toolkit\\images\\cat-water.jpg"));
}
public static Vector<BufferedImage> waterEffect(BufferedImage img, int xpos, int ypos) {
Vector<BufferedImage> images = new Vector<>();
WaterRoutine wr = new WaterRoutine();
wr.create(img.getWidth(), img.getHeight());
wr.HeightBlob(xpos, ypos, 50, 500, wr.m_iHpage);
for (int i = 0; i < 200; i++) {
if (i % 10 == 0) {
BufferedImage tmpImg = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB);
wr.render(img, tmpImg);
images.add(tmpImg);
} else {
wr.nullPos();
}
}
return images;
}
public static void waterImageGif(int xpos, int ypos) throws IOException {
BufferedImage img = ImageIO.read(new File("D:\\eclipse-workspace\\image-toolkit\\images\\cat.jpg"));
OutputStream out = new FileOutputStream("D:\\eclipse-workspace\\image-toolkit\\images\\cat-water.gif");
GifEncoder gif = new GifEncoder();
if (!gif.isInit()) {
gif.init(img.getWidth(), img.getHeight(), out, true);
gif.setDelay(10);
}
Vector<BufferedImage> images = waterEffect(img, xpos, ypos);
for (BufferedImage item : images) {
gif.addImage(item);
}
gif.setCount(0);
gif.encodeMultiple();
out.close();
}
public static void main(String[] argv) throws IOException {
waterImage(100, 300);
waterImageGif(100, 300);
}
}
其中waterEffect实现迭代50次后的水纹效果,waterImageGif实现每迭代10次生成一张图片,叠加或生成动态GIF图片。GIF图片生成类GifEncoder请参考文章《JAVA实现GIF动画》