早在没当程序员之前其实就接触过格式工厂了,当时经常把玩它的图片区域,因为水波纹效果实在太酷炫了。然后也一直想做出这种效果,但是一点思路都没有。趁着过节有时间,就想了却这个心愿,就去网上找教程。发现了https://download.csdn.net/download/jilijelon/4506888正好是我想要的效果,虽然没有去比对过是否跟格式工厂的一样,但这种其实就是我想要的效果。
我看完之后自己照着实现了下,由于代码比较多,作者做了3个特效,有些代码写了也没有用上,所以提炼波纹主要的实现代码还是费点事的。我个人也比较懒,纯读代码,也没有去建工程搭环境去调试,但总归还是啃下来了。
一开始我以为是用水波纹图片与底图执行一些神奇的操作之后进行混合的,学完发现这个效果竟然是利用原图纯计算得出来的。学这个之前要有心理准备,它的复杂度跟图片像素个数挂钩,我800x600的图,debug下只有十几帧。虽然性能不咋的,但遭不住它好看啊!总之先来说说源码主要要关心的地方
首先OnInitDialog方法会对图片区域进行初始化,并且会创建m_bmpRenderSource和m_bmpRenderTarget,Source是原图,初始化完之后就不会再改。而Target则是输出,每帧波纹特效那边会根据原图以及它那边维护的数据结构,将结果输出到Target。还要初始化CWaterRoutine,也就是调用m_myWater.Create。让CWaterRoutine维护的数据结构跟图片大小一样大。
OnTimer,表示每帧Tick,主要看m_myWater.Render
OnMouseMove,处理鼠标移动事件,鼠标移动的时候会泛起小水花,所以执行了m_myWater.HeightBlob
OnLButtonDown,处理鼠标点击事件,鼠标点击的时候会泛起大水花,所以HeightBlob的两个权值要比MouseMove要大
主要关心的就这3个地方,其中有个地方需要注意一下,OnTimer和OnMouseMove都走了Render方法,其实没必要OnTimer一处调用Render就行了,不然两个处理的时候会出现鼠标移动的时候播放速度变快的情况。然后FlattenWater是清空水波纹效果,如果不需要清除效果的话,可以不关心它。
先了解下一要关心哪些成员变量:
m_iHeightField1、m_iHeightField2:数组长度跟图片像素个数一样的数组,用来记录图片每个像素的权重高度。会交替使用,例如某一帧用m_iHeightField1,然后输出结果到m_iHeightField2上。下一帧用m_iHeightField2,输出结果到m_iHeightField1。一直这么持续下去
m_iWidth、m_iHeight:图片大小
m_bDrawWithLight:用于调用DrawWaterWithLight,不太清楚具体表示的含义。属于预设好的配置,值不会变
m_iHpage:与m_iHeightField1和m_iHeightField2搭配使用,起到类似滚动数组一样的作用
m_density:影响波纹扩散的密度
然后来关心用到的方法:
构造函数:初始化配置
Create:根据图片大小,创建m_iHeightField1、m_iHeightField2两个数组
Render:调用DrawWaterWithLight和CalcWater,并切换当前使用的数组
CalcWater:遍历数组中边框内的点(边框内的像素指的是第二行第二个到倒数第二行的倒数第二个),计算新的权重高度存储到下一个数组。每个点会去采样周围8个像素取平均,然后与当前点的权重做差,算出新的权重。然后对权重做下衰减存到新数组
HeightBlob:在数组x,y坐标半径radius的范围内的点都累加上权重height
DrawWaterWithLight:遍历边框内的点,计算每个点相对它右边以及下边点的两个权重高度差,根据两个权重找到新的一个点,将新的点在Source对应的像素颜色的RGB值减去 当前点相对右边的高度差,将新的颜色赋值到Target。注意这里代码中作者for循环里一个操作执行了两遍,我自己读下来感觉可以统一成一个,然后实现完之后也没发现有啥地方有问题。
GetShiftedColor:颜色值的RGB通道都减去一个权重高度
图片是网上随便找的壁纸,测试用的,侵删
void WaterEffect::Init(int width, int height)
{
width_ = width;
height_ = height;
current_ = 0;
for (unsigned i = 0; i < 2; ++i)
{
maps[i].Resize(width_*height_, 0);
}
}
void WaterEffect::Clear()
{
for (unsigned i = 0; i < 2; ++i)
{
maps[i].Resize(width_*height_, 0);
}
}
void WaterEffect::HeightBlob(int x, int y, int radius, int height)
{
int rquad;
int left, top, right, bottom;
Vector& oldMap = maps[current_];
Vector& newMap = maps[current_ ^ 1];
rquad = radius * radius;
// 保证点击的位置在画布内
// if (x < 0) x = 1 + radius + rand() % (width_ - 2 * radius - 1);
// if (y < 0) y = 1 + radius + rand() % (height_ - 2 * radius - 1);
x = Clamp(x, 0, width_ - 1);
y = Clamp(y, 0, height_ - 1);
left = -radius; right = radius;
top = -radius; bottom = radius;
// 保证上下左右在画布内
if (x - radius < 1) left -= (x - radius - 1);
if (y - radius < 1) top -= (y - radius - 1);
if (x + radius > width_ - 1) right -= (x + radius - width_ + 1);
if (y + radius > height_ - 1) bottom -= (y + radius - height_ + 1);
// 遍历圆圈范围内的所有点,让它加上一个权重
for (int cy = top; cy < bottom; ++cy)
{
int cyq = cy * cy;
for (int cx = left; cx < right; ++cx)
{
if (cx*cx + cyq < rquad)
{
oldMap[width_*(cy + y) + (cx + x)] += height;
}
}
}
}
void WaterEffect::Render(const Vector& src, Vector& dest)
{
DrawWater(src, dest);
CalcWater();
current_ ^= 1;
}
void WaterEffect::DrawWater(const Vector& src, Vector& dest)
{
Vector& curMap = maps[current_];
int index = 1 * width_ + 1; // 从第二行第二个开始遍历
int indexMax = (height_ - 1)*width_;
for (; index < indexMax; index += width_) // 忽略掉上一行的最后一个和这一行的第一个
{
int tIndexMax = index + width_ - 2;
for (int tIndex = index; tIndex < tIndexMax; ++tIndex)
{
// dx和dy对应右边和下边的高度差
int dx = curMap[tIndex] - curMap[tIndex + 1];
int dy = curMap[tIndex] - curMap[tIndex + width_];
int lIndex = tIndex + width_ * (dy >> 3) + (dx >> 3);
if (lIndex < width_ * height_&&lIndex>0)
{
Color c = src[lIndex];
dx = (dx + dy) / 2;
c.r_ = Clamp(c.r_ - dx * 1.0f / 255.f, 0.f, 1.f);
c.g_ = Clamp(c.g_ - dx * 1.0f / 255.f, 0.f, 1.f);
c.b_ = Clamp(c.b_ - dx * 1.0f / 255.f, 0.f, 1.f);
dest[tIndex] = c;
}
}
}
}
void WaterEffect::CalcWater()
{
Vector& oldMap = maps[current_];
Vector& newMap = maps[current_ ^ 1];
int index = width_ + 1;
int indexMax = (height_ - 1)*width_;
// 从第二行第二个,遍历到倒数第二行的倒数第二个
for (; index < indexMax; index += width_) // 忽略掉上一行的最后一个和这一行的第一个
{
int tIndexMax = index + width_ - 2;
for (int tIndex = index; tIndex < tIndexMax; ++tIndex)
{
// 采样下周围8个点取平均,与当前点高度做差
int newh = ((oldMap[tIndex + width_]
+ oldMap[tIndex - width_]
+ oldMap[tIndex + 1]
+ oldMap[tIndex - 1]
+ oldMap[tIndex - width_ - 1]
+ oldMap[tIndex - width_ + 1]
+ oldMap[tIndex + width_ - 1]
+ oldMap[tIndex + width_ + 1]
) >> 2) - newMap[tIndex];
newMap[tIndex] = newh - (newh >> density_);
}
}
}
虽然搞清楚了每个部分代码的实现,大概也能理解折射效果是通过采样另一个像素,以及RGB通道整体增强或者减弱来表现光泽的,还有扩散是采样周围像素来实现的。但是即使知道这些,感觉我是没法理解,如果不是运行起来,根本想象不出来是这个效果。看来,还有很长的路要走