利用C/C++实现的水波纹特效

2023-12-25 12:26:51

1.项目介绍

? ? ? ? 水波纹特效是生活中常见的一种物理特效,在一些特殊的网络场合中使用水波纹特效不仅可以增加前端界面的美感,吸引流量关注,更能增加观看者对应项目的好感度,带来许多潜在的收益,比如一些5A级旅游景区的宣传网站,在使用绚丽的水波纹特效后能够达到加深印象的作用。本次项目的实现采用的是C/C++语言,外加任意版本的easyX图形库,通过设计较为还原的物理仿真接口,供主函数调用,从而实现对一张固定的图片的水波纹处理。项目最终效果的展示如下图:

2.原理简介和准备工作

? ? ? ? easyX图形库里有专门准对鼠标动作的函数方法,利用这一特点,可以将鼠标的运动轨迹想象为一个可以快速高频无限飞行的水漂石,打过水漂的都知道,水漂石在触碰水平面的时候其实就相当于快速在正下方的位置施加了一个力,而这个力给予了该范围的水平面一个波幅,从而产生了范围性的震荡,明白这一原理后,可以设计一个石头接触水面时的接口,为了降低仿真难度,可以将水漂石接触水平面的瞬间看作是在该位置投入了一颗小石头,该接口的作用就是赋予水面当前位置一个振幅。有了振幅之后,剩下的工作就简单了,只需要设计出一个波动的物理仿真接口,就可以实现整个项目的交互。有了这些理解之后,就可以完成准备工作:一张背景图片,以及基本的全局变量和宏定义:

#include <graphics.h>

#define WIN_WIDTH 512
#define WIN_HEIGHT 512

//二维数组一维化的宏定义方法
#define ELEM(array, row, col) (array[(row) * WIN_WIDTH + (col)]) 

short *wave1;  // 当前时刻的振幅数据
short *wave2;  // 上一时刻的振幅数据

IMAGE imgSrc;  // 原图片
IMAGE imgDest; // 特效图片

3.初始化接口

? ? ? ? 完成了准备工作以后,事不宜迟,马上开始接口的实现。首先是初始化接口的实现,这一步的目的主要是提前准备好要需要交互的图片资源,并为图片的每一个像素开辟动态的存储空间,用来保存某一时刻水平面的整体振幅和该水面上一时刻的整体振幅。初始化的过程中刚好利用easyX来打印窗口和图片的尺寸调整,从而将准备工作做得更完善,初始化接口的代码如下:

void init() {
	/*创建窗口,WIN_WIDTH和WIN_HEIGHT这两个宏分别是窗口的宽度和高度
    可以根据自己的需要定义*/
	initgraph(WIN_WIDTH, WIN_HEIGHT);

    //调整图片尺寸,imgSrc为原图片,imgDest为特效图片
	loadimage(&imgSrc, L"background.jpg", WIN_HEIGHT, WIN_WIDTH);	
	imgDest.Resize(WIN_WIDTH, WIN_HEIGHT);

	//给两个波幅数组分配动态存储空间
	wave1 = (short*)malloc(sizeof(short) * WIN_WIDTH * WIN_HEIGHT);
	wave2 = (short*)malloc(sizeof(short) * WIN_WIDTH * WIN_HEIGHT);

	//把两个振幅数组全部清零
	memset(wave1, 0, sizeof(short) * WIN_WIDTH * WIN_HEIGHT);
	memset(wave2, 0, sizeof(short) * WIN_WIDTH * WIN_HEIGHT);
}

?4.水漂石接口

????????水漂石落水可以简化为,有一颗和水漂石同样大小的石头在当前时刻投入了水中,所以可以顺带将鼠标点击水平面的振幅效果也做出来,经过尝试发现投入矩形的石头后产生的物理效果非常差,所以这里直接使用圆形的石头进行投入,为了方便后期物理波动的仿真接口,石头的尺寸参数不宜过大,接口具体实现代码如下:

void putStone(int x, int y, int stoneSize, int stoneWeight) {
	if (x >= WIN_WIDTH - stoneSize || x < stoneSize ||
		y >= WIN_HEIGHT - stoneSize || y < stoneSize) {
		return;
	}
    //当石头的投放位置有越界风险时,直接舍弃

	for (int posx = x - stoneSize; posx < x + stoneSize; posx++) {
		for (int posy = y - stoneSize; posy < y + stoneSize; posy++) {
			if ((posx - x) * (posx - x) + (posy - y) * (posy - y)
				< stoneSize * stoneSize) {				
				wave1[posy * WIN_WIDTH + posx] += stoneWeight;
			}
		}
	}
    //将石头的影响范围约束在一个圆形中
}

?5.振幅计算接口

????????振幅计算的作用在于计算石头落水的下一时刻,水平面的水波振幅变化,也就是用到了两个振幅数组的交换,那么该怎么得出下一时刻的水波振幅是本接口的一个难点,也是物理仿真的关键点,我们可以假设每个像素之间相隔了一个周期,也就是只有一个波长的距离,那么每个像素点的振幅都是叠加而非减少的,那么一个像素的振幅会受到上下左右四个方向的振幅增益和时间的衰减

据此可以利用for循环仿真计算出下一时刻整个水面的振幅数据?:

void calcNextFrameWave() {
	/*对整个水面进行仿真计算,每个像素受上下左右四个其他像素的
    小幅度增益*/
	for (int i = 1; i < WIN_HEIGHT - 1; i++) {
		for (int j = 1; j < WIN_WIDTH - 1; j++) {
			ELEM(wave2, i, j) =
				(ELEM(wave1, i - 1, j) + ELEM(wave1, i + 1, j) +
					ELEM(wave1, i, j - 1) + ELEM(wave1, i, j + 1)) / 2 -
				ELEM(wave2, i, j);
			//由于时间的流逝,该像素点振幅要进一步衰减
			ELEM(wave2, i, j) -= ELEM(wave2, i, j) >> 5;//左移5位,除以32的快捷写法
		}
	}
	//交换 wave1 和 wave2,实现画面特效传递
	short* tmp = wave1;
	wave1 = wave2;
	wave2 = tmp;
}

6.特效生成接口

? ? ? ? 特效生成接口的作用是利用已存在的波幅数据,来实现特效画面的仿真制作,基本原理还是物理仿真,通过振幅参数,来实现像素点的偏移。像素点的偏移可以直接利用图片的地址操纵数组来实现,而将该像素的振幅参数转化为偏移量正是本接口的重点所在,话不多说,实现代码如下:

void calcImage() {
	//获取图片的像素内存
	DWORD* p1 = GetImageBuffer(&imgSrc);
	DWORD* p2 = GetImageBuffer(&imgDest);

	int i = 0;
	for (int y = 0; y < WIN_HEIGHT; y++) {//逐行计算
		for (int x = 0; x < WIN_WIDTH; x++) {//逐列计算
            
            // 计算像素的偏移位置
			int a = (x - 0.5 * WIN_WIDTH) * (1 - wave1[i] / 1024.0) + 0.5 * WIN_WIDTH;
			int b = (y - 0.5 * WIN_HEIGHT) * (1 - wave1[i] / 1024.0) + 0.5 * WIN_HEIGHT;

            // 防止图片数组越界
			if (a >= WIN_WIDTH) a = WIN_WIDTH - 1;
			if (a < 0) a = 0;
			if (b >= WIN_HEIGHT) b = WIN_WIDTH - 1;
			if (b < 0) b = 0;
			p2[i] = ELEM(p1, b, a);
			i++;

		}
	}
}

7.画面渲染接口、鼠标捕捉接口、主函数调用?

? ? ? ? ?所有的仿真接口做完了以后,就只剩下这三个调用性函数,没有什么特别难理解的地方,但是需要格外注意一些语法的使用规范和技术目的,比如渲染接口的双缓冲、鼠标捕捉接口的内置宏含义,有什么不明白的建议直接查手册,三个函数代码如下:

// 画面渲染接口,双缓冲防止卡顿
void showImage() {
	BeginBatchDraw();

	putimage(0, 0, &imgDest);

	EndBatchDraw();
}
// 鼠标信息捕获
void userClickMove() {
	ExMessage msg; // 用来存储鼠标消息
	
	while (peekmessage(&msg, EM_MOUSE)) {
		if (msg.message == WM_MOUSEMOVE) { //移动鼠标
			putStone(msg.x, msg.y, 3, 256);
		}
		else if (msg.message == WM_LBUTTONDOWN) { //鼠标左键单击
			putStone(msg.x, msg.y, 3, 2560);
		}

	}
}
// 主函数
int main(void) {
	init(); // 初始化

	while (1) {
		
		userClickMove();     // 鼠标信息捕捉接口

		calcNextFrameWave(); // 振幅计算接口

		calcImage();         // 特效计算接口
		
		showImage();         // 画面渲染接口

		//帧等待,防止卡顿
		Sleep(1);
	}
	return 0;
}

8.组装函数和接口?

? ? ? ? ?按照本文章的顺序依次复制粘贴并组装好接口和函数后即可正常启动项目,但要记得自己准备好一张背景图片放在项目的目录下,名字可以重命名为background.jpg,代码组装好后的所有功能如下图所示:

项目结束,感谢观看。?

文章来源:https://blog.csdn.net/qq_63303370/article/details/132794536
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。