OpenGL中如何实现通过鼠标点击选取对象(正交投影)

最近在用OpenGL写一个魔方程序,考虑到需要选取其中的小方块,于是我研究了半天,发现了这个方法

这种方法暂时只能在正交投影下使用,不需要OpenGL自带的什么glpick之类的方法,说实话,我现在并不想学习那种方法


接下来,我要介绍方法的大概思路:

该方法的核心是对凸包算法的使用以及OpenGL里的矩阵运算()即仿射变换)但是凸包算法已经是一个经典算法,大家尝试自己解决,如果实在不行,我下面会发一下我的土包代码。

首先建立cube对象,cube指的是魔方之中每个小的立方体方块。每个cube对象都维护:


一个相对矩阵(MATRIX[16]),


一个绝对矩阵(ABSOLUTE_MATRIX[16]),


一个初始位置数组origion_position[3],固定不变


一个绝对当前位置数组cur_abs_position[3],可以改变,值由origon_position与MATRIX相乘得出

这组数据中实际上用到的只是z坐标值(因此尚有改进之处),用于判断深度,如果忽略深度条件,鼠标点击可能会选取到多个对象,但是一般来说我们想选的是我们可以看到的,即离我们最近的哪一个对象,因此需要进行深度比较


(其实还有一个相对的当前位置current_position[3],但是这与今天要讲的选取没有关系,所以姑且不多提),


八个顶点的初始位置origion_vertex[24],固定不变


八个顶点的当前位置urrent_vertex[24],可以改变,值由origon_vertex与ABSOLUTE_MATRIX相乘得出

这八组数据主要用到每组数据的x坐标与y坐标,相当于八个二维点,通过这八个点来求出他们的凸包,进而当鼠标点击窗口某个位置时用于判断在哪些凸包之内

因为实际上只需要16个信息,因此尚有改进之处


整个程序还要维护一个矩阵M[16],用于进行整个魔方的旋转与通过与MATRIX结合来求ABSOLUTE_MATRIX;


接下来下介绍各种矩阵的获取方法:

MATRIX的获取方法:

因为魔方有6个面可以先把每个面作为一个组,然后每次旋转魔方的一个面时,对各个组的成员进行重新分配,对每个小方块(cube)的MATRIX重新获取

代码如下:

void rotate(int right, int angle) {

//当time为9时,说明旋转了90度,此刻才会形成完整的一步旋转

for (int i = 0;i < 9;i++) {


glPushMatrix();


//每做一次小的旋转,都要对小方块的当前位置更新一下
glLoadIdentity();


//当coord为零时,说明group所在的面与x轴垂直,则绕x轴旋转
//         为1时,                  y           y
//           2                     z           z
if (coord == 0)
glRotatef(angle, right, 0, 0);
else if (coord == 1)
glRotatef(angle, 0, right, 0);
else if (coord == 2)
glRotatef(angle, 0, 0, right);


glMultMatrixf(cubes[i]->MATRIX);
glGetFloatv(GL_MODELVIEW_MATRIX, cubes[i]->MATRIX);
glPopMatrix();

//每次小的旋转都要改变当前位置
//这样才可以正确画出方块
cubes[i]->changePosition();
mc->firstNine[i] = cubes[i]->index;
cubes[i]->changeAbsData(M);

}

这个不是今天的重点,详情请研究原代码及注释

通过该代码主要了解到,MATRIX的获取是对每个小方块进行如下操作(伪代码):

for(i=0:n)

第一步,保存当前矩阵: glpushMatrix();

第二步,当前矩阵变为单位阵: glloadIdentity();

第三步: 调用一系列变换函数;

第四步,右乘当前cube的MATRIX: glmultMatrix(cube[i]->MATRIX);

第五步,获取新的MATRIX: glGetFloat(GL_MODEL_MATRIX,cube[i]->MATRIX);

第六步:还原之前保存的矩阵: glPopMatrix();



M的获取方法(例如要对图像进行总体的旋转),与MAYTRIX的获取有些类似,可以采用如下代码,改代码时glMotionFunc的一个回掉函数:

void motion(int x, int y) {

//变换一下坐标系
y = 600 - y;
float dx = -stx +x;
float dy = -sty + y;
glLoadIdentity();
glRotatef(2, -dy, dx, 0);
glMultMatrixf(M);
glGetFloatv(GL_MODELVIEW_MATRIX, M);
glutPostRedisplay();
stx = x;
sty = y;
}

其中:

stx,sty是鼠标按下左键时的坐标(已经经过变换,现在以左下角原点),

x,y是鼠标拖动的坐标,以左上角为原点,需要变换使其以左下角为原点

dx,dy是鼠标拖动的位移,通过他的方向来确定物体的旋转方向

有时可能会出现旋转方向与鼠标拖动方向下给你返的情况,则把一下两行代码的正负号变换一下:

float dx = -stx +x;
float dy = -sty + y;

改为:

float dx = stx -x;
float dy = sty - y;

尤其注意,每当拖动一次鼠标之后,在函数最后要更新stx,sty的位置


ABSOLUTE_MATRIX的方法很简单,调用一下下列代码段就可以:

glPushMatrix();//保存当前矩阵
glLoadIdentity();//是当前矩阵变为单位阵
glMultMatrixf(M);//右乘M
glMultMatrixf(MATRIX);//右乘MATRIX
glGetFloatv(GL_MODELVIEW_MATRIX, ABSOLUTE_MATRIX);//获得当前矩阵,即ABSOLUTE_MATRIX
glPopMatrix();//恢复之前保存的矩阵


获得了各种矩阵,就可以求各种绝对位置,相对位置,求取方法就是简单向量与矩阵相称的原理,采用一种简单的函数就可以轻而易举地实现,下面是一种函数:

void changeVector(float *MAT, float* o_v, float *n_v) {
float tm[16];
//矩阵转置
for (int i = 0;i < 4;i++) {
for (int j = 0;j < 4;j++) {
tm[i * 4 + j] = MAT[j * 4 + i];
// printf("%f ", tm[i * 4 + j]);
}
// printf("\n");
}


for (int i = 0;i < 3;i++) {
n_v[i] = 0;
for (int j = 0;j < 3;j++) {
n_v[i] += tm[i * 4 + j] * o_v[j];
}
}
}

函数中,MAT表示要乘的矩阵,o_v表示变换之前的向量,n_v表示变换之后的向量


这样,我们就可以当每次相改变一次图形各部分位置时(例如,我的模仿可以旋转)就可以通过一下步骤来对各个顶点的信息进行更改:

第一步:获取M

第二步:获取每个cube对象的MATRIX

第三步:获取每个cube对象的ABSLOOLUTE_MATRIX

第四步:调用changeVector方法获取新的current_vertex和cur_abs_position

第五步:改变各个cube对象所维护的凸包的信息

这样,这一次图像变换完成(以上五步记作Change)


接下来又是一个新周期

一个变换周期可以如下概括:

一、鼠标选中{

检查那个对象被选中

}

二、判断是否改变(即是否拖动鼠标或旋转魔方){

若改变,则调用Change;

否则,不调用Change,即跳过

}

三、画图


当然,这并不是绝对的严格遵守的三步,有可能会直接从第二步开始,但是只有通过鼠标才能表现出对对象的选取,详细请见程序

我写的这段程序,有一些扩展功能,当然,还有一些没有实现的功能,这只是实验产品,希望大家有兴趣的加以完善

#pragma once
#ifndef CONVEX_ROC

#define CONVEX_ROC

#include<stack>
using namespace std;

template<class T>
class Convex {
private:
	struct point2 {
		int x, y;
		double ATAN;
		point2() {};
		~point2() {}
		point2(const point2&p) {
			x = p.x;
			y = p.y;
			ATAN = p.ATAN;
		}

		point2& operator=(const point2&p) {
			x = p.x;
			y = p.y;
			ATAN = p.ATAN;
			return *this;
		}

		point2(int _x, int _y) {
			x = _x;
			y = _y;
		};
		int &operator[](int i) {
			if (i == 0)return x;
			if (i == 1)return y;
			return x;
		};
	};

	//交换两个点的数据
	void Swap(point2& p1, point2& p2) {
		swap(p1.x, p2.x);
		swap(p1.y, p2.y);
		swap(p1.ATAN, p2.ATAN);
	}


	//检测是否比子树的值大,如果大于子树则交换
	//比较顺序,先与左子树比较,再与右子树比较
	//先按y值比较,再按x值比较
	void  check_xchange(const int& a, point2*ps, const int & allPointNum) {
		if (ps[a].y > ps[a * 2].y) {
			Swap(ps[a], ps[a * 2]);
		}
		else if (ps[a].y == ps[a * 2].y) {
			if (ps[a].x > ps[a * 2].x) {
				Swap(ps[a], ps[a * 2]);
			}
		}
		if (a * 2 + 1 <= allPointNum)
			if (ps[a].y > ps[a * 2 + 1].y) {
				Swap(ps[a], ps[a * 2 + 1]);
			}
			else if (ps[a].y == ps[a * 2 + 1].y) {
				if (ps[a].x > ps[a * 2 + 1].x) {
					Swap(ps[a], ps[a * 2 + 1]);
				}
			}
	}

	//使用堆排序算法,求出y最小的点.当有多个y最小的点时,再从中选取x最小的
	void HEAPresort(point2*ps, const int& point_num) {
		for (int i = point_num / 2;i > 0;i--) {
			check_xchange(i, ps, point_num);
		}
	}

	//获得每个点的极角,
	//通过反三角函数acos来确定角的大小
	//改进后通过函数的单调性来确定ATAN的大小
	void getJiJiao(point2*points, const int& point_num) {

		for (int i = 2;i <= point_num;i++) {

			if (points[i].x == points[1].x) {
				points[i].ATAN = 0;
				continue;
			}
			double t = points[i].x - points[1].x;
			//if (points[i].x != points[1].x)
			points[i].ATAN = -(t) / pow((pow(t, 2) + pow(points[i].y - points[1].y, 2)), 0.5);
			//else points[i].ATAN = PI / 2;
		}
	}

	//按照极角的大小,有小到大排列,从第二个开始排
	void MERGESORT(point2*points, const int &point_num) {
		for (int j = point_num - 1;j > 0;j--)
			for (int i = 2;i <= j;i++) {
				if (points[i].ATAN > points[i + 1].ATAN) {
					Swap(points[i], points[i + 1]);
				}
				else if (points[i].ATAN == points[i + 1].ATAN) {
					if (points[i].y > points[i + 1].y) {
						Swap(points[i], points[i + 1]);
					}
					else if (points[i].x > points[i + 1].x) {
						Swap(points[i], points[i + 1]);
					}
				}
			}

	}

	//当返回值小于0时 说明是向左转(即p3在p1->p2左面),等于零则三点共线
	int LeftTurn_CHA(point2 &p1, point2 &p2, point2 &p3) {
		return (p1.x - p2.x)*(p3.y - p2.y) - (p1.y - p2.y)*(p3.x - p2.x);
	}

	bool inPolygon(point2 &p, point2*ps, int p_size) {
		if (p_size < 3)return 0;
		else if (p_size == 3) {
			return (LeftTurn_CHA(ps[0], ps[1], p)) <= 0 && (LeftTurn_CHA(ps[1], ps[2], p)) <= 0 && (LeftTurn_CHA(ps[2], ps[0], p)) <= 0;
		}
		else {
			int t = LeftTurn_CHA(ps[0], ps[p_size / 2], p);
			if (t == 0) {
				return LeftTurn_CHA(ps[p_size / 2], ps[p_size / 2 + 1], p) <= 0;
			}
			else if (t > 0) {
				return inPolygon(p, ps, p_size / 2 + 1);
			}
			else
			{
				point2 *tps = new point2[p_size - p_size / 2 + 1];
				tps[0] = ps[0];
				for (int i = 1;i < p_size - p_size / 2 + 1;i++)
					tps[i] = (ps + (p_size / 2))[i - 1];
				bool in = inPolygon(p, tps, p_size - p_size / 2 + 1);
				delete[]tps;
				return in;
			}

		}
	}

	point2*vertex = 0;//顶点数组
	int level = 1;//精确级别,精确度越高级别越大,都是10的整数幂
	int tp_size = 0;//顶点数目

public:

	Convex() {}
	~Convex() {
		if (vertex)
			delete[]vertex;
	}
	//T为一维数组,可以用作二维数组
	bool creatHull(T *data, const int& point_num, const int& lev) {
		level = lev;
		point2*points = new point2[point_num + 1];
		for (int i = 1;i < point_num + 1;i++) {
			points[i] = point2(data[i * 2 - 2] * level, data[i * 2 - 1] * level);
		}
		HEAPresort(points, point_num);
		getJiJiao(points, point_num);
		MERGESORT(points, point_num);

		//取得顶点
		stack<point2>tp;
		tp.push(points[1]);
		tp.push(points[2]);
		for (int i = 3;i <= point_num;i++) {
			point2 *p2 = &tp.top();
			tp.pop();
			point2 *p1 = &tp.top();
			tp.pop();
			int t;
			while ((t = LeftTurn_CHA(*p1, *p2, points[i])) >= 0) {
				if (tp.size() == 0) {
					p2 = &points[i];
					break;
				}
				p2 = p1;
				p1 = &tp.top();
				tp.pop();

			}
			tp.push(*p1);
			tp.push(*p2);
			tp.push(points[i]);
		}

		//将栈中的数据转移到数组中来
		vertex = new point2[(tp_size = tp.size())];
		for (int i = tp_size - 1;i >= 0;i--) {
			vertex[i] = tp.top();
			tp.pop();
			//printf("TP:%d,%d %f\n", vertex[i].x, vertex[i].y, vertex[i].ATAN);


		}

		delete[]points;
		return true;
	}

	bool testIn(const T *point) {
		point2 p(point[0] * level, point[1] * level);
		return inPolygon(p, vertex, tp_size);
	}

	//需要用户自行释放
	T* getNewVertx()const {
		T*v = new T[tp_size * 2];
		for (int i = 0;i < tp_size;i++) {
			v[i * 2] = (T)(vertex[i].x) / level;
			v[i * 2 + 1] = (T)(vertex[i].y) / level;
		}
		return v;
	}
	int getVertexSize() const {
		return tp_size;
	}
};



#endif // !CONVEX_ROC

#include<iostream>
#include<vector>
#include<gl\glut.h>
using namespace std;

Convex<float> c;

float PROJECT_MATRIX[16];
float LOOKAT_MATRIX[16];
/**
结构体steps用于记录模仿的拧动历史,便于还原模仿
*/

//根据矩阵mat,将o_position转换为n_position
void setPosition(float*mat, float*o_position, float *n_position) {
	float tm[16];
	for (int i = 0;i < 4;i++) {
		for (int j = 0;j < 4;j++) {
			tm[i * 4 + j] = mat[j * 4 + i];
			//printf("%f ", tm[i * 4 + j]);
		}
		//printf("\n");
	}
	//printf("\n");
	for (int i = 0;i < 3;i++) {
		n_position[i] = 0;
		//printf("np[%d]=", i);
		for (int j = 0;j < 3;j++) {
			n_position[i] += tm[i * 4 + j] * o_position[j];
			//	printf("%d*%d+", (int)tm[i * 4 + j], p[j]);
		}
		//	printf("  = %d\n", np[i]);
	}
	//cout << np[0] << " " << np[1] << " " << np[2] << endl;
}

struct step {
	int drawPlane;//旋转的面的序号
	int rotate_direction;//旋转的方向

	//在控制台输出魔方的历史数据
	void PrintOut() {
		cout << "PLANE: " << drawPlane << " DER: " << rotate_direction;
	}
};

//创建step向量,每当旋转一次魔方,都会产生一个新的step对象并存入steps
vector<step>steps;

//魔方各个面的颜色,可随意更该
float color[7][3] = {
	1,1,0,
	0,1,0,
	1,0.5,0,
	0,0,1,
	1,0,1,
	0,1,1,
	0.5,0.5,0.5
};

//画小方块cube的一个面,在cube方法中调用,即可将一个小方格画出
void drawPlane(int a) {
	glColor3fv(color[a]);
	glBegin(GL_QUADS);
	glVertex3f(1, 1, 0);
	glVertex3f(-1, 1, 0);
	glVertex3f(-1, -1, 0);
	glVertex3f(1, -1, 0);
	glEnd();
}

float CM[16] = { 1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1 };
float p[3] = { 1,1,1 };
float np[3];
int absulueP[3];
float AM[16];
float o_range[18] = {
	1,1,0.5,
	1,1,1.5,
	0.5,1,1,
	1.5,1,1,
	1,0.5,1,
	1,1.5,1
};
float c_range[18];



//画一个小方块,边长为2,中心在原点
void cube() {

	glPushMatrix();

	//glTranslatef(1, 1, 0);
	for (int i = 0;i < 3;i++) {
		glPushMatrix();
		if (i == 0)
			glRotatef(90, 1, 0, 0);
		if (i == 1)
			glRotatef(90, 0, 1, 0);
		glTranslatef(0, 0, 1);
		drawPlane(i * 2);
		glTranslatef(0, 0, -2);
		drawPlane(1 + 2 * i);
		glPopMatrix();
	}
	glPopMatrix();

}
void bigcube() {
	//glTranslatef(2, 2, 0);
	glPushMatrix();
	glScalef(0.2, 0.2, 0.2);
	glRotatef(90, 0, 1, 0);
	cube();
	glPopMatrix();
}

//八个顶点,最初的位置
float ori_poinVer[24] = {
	0.5,0.5,0.5,
	0.5,0.5,1.5,
	0.5,1.5,1.5,
	0.5,1.5,0.5,

	1.5,0.5,0.5,
	1.5,0.5,1.5,
	1.5,1.5,1.5,
	1.5,1.5,0.5,

};

//八个顶点当前位置
float cur_poinVer[24];

float *convexVertex;//图报的顶点
int conVertNum;//凸包的顶点数

//求出每个顶点的当前坐标
void changeVertex() {
	float ver[16];
	for (int i = 0;i < 8;i++) {
		setPosition(AM, ori_poinVer + i * 3, cur_poinVer + i * 3);

		//得到二维坐标,去掉深度
		ver[i * 2] = cur_poinVer[i * 3];
		ver[i * 2 + 1] = cur_poinVer[i * 3 + 1];
	}

	//求八个顶点的凸包
	c.creatHull(ver, 8, 1000);
	conVertNum = c.getVertexSize();
	convexVertex = c.getNewVertx();
	for (int i = 0;i < conVertNum;i++) {
		//printf("%f,%f\n", convexVertex[i * 2], convexVertex[i * 2 + 1]);
	}
	delete[]convexVertex;

}



void smallcube() {
	glPushMatrix();
	glScalef(0.1, 0.1, 0.1);
	glTranslatef(2, 2, 2);
	cube();
	glPopMatrix();
	setPosition(CM, p, np);
}
void changeCM() {
	glPushMatrix();
	glLoadIdentity();
	glRotatef(90, 1, 0, 0);
	glMultMatrixf(CM);
	glGetFloatv(GL_MODELVIEW_MATRIX, CM);
	glPopMatrix();
}
float M[16] = { 1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1 };
void changeAM() {
	glPushMatrix();
	glLoadIdentity();
	glMultMatrixf(M);
	glMultMatrixf(CM);
	glGetFloatv(GL_MODELVIEW_MATRIX, AM);
	glPopMatrix();
	//printf("M:\n");
	for (int i = 0;i < 4;i++) {
		for (int j = 0;j < 4;j++) {
			//		printf("%f ", M[j * 4 + i]);
		}
		//	printf("\n");
	}
	//printf("CM:\n");
	for (int i = 0;i < 4;i++) {
		for (int j = 0;j < 4;j++) {
			//		printf("%f ", CM[j * 4 + i]);
		}
		//	printf("\n");
	}
	//printf("AM:\n");
	for (int i = 0;i < 4;i++) {
		for (int j = 0;j < 4;j++) {
			//		printf("%f ", AM[j * 4 + i]);
		}
		//	printf("\n");
	}
	setPosition(AM, p, np);
	//	printf("np:\n");
	for (int i = 0;i < 3;i++) {
		//	printf("%f ", np[i]);
	}
	//	printf("\n");
	//	printf("6面:\n");
	for (int i = 0;i < 6;i++) {
		setPosition(AM, o_range + i * 3, c_range + i * 3);

		for (int j = 0;j < 3;j++) {
			//		printf("%f ", (c_range+i*3)[j]);
		}
		//		printf("\n");
	}
	//	printf("\n");
	//	printf("CONVEX:\n");
}

//检查是否被选中
bool picked(int x, int y) {
	float p[2];
	p[0] = (float)x / 60 - 5;
	p[1] = (float)y / 60 - 5;
	printf("%f %f\nIn: %d\n", p[0], p[1]);
	return c.testIn(p);
}

bool rotae = 1;

#define CUBE_SCALE_TIME 0.96
#define MAGIC_CUBE_SCALE_TIME 0.2


template<class T>
T whatangle(const T *a, const T *b, const  T *c) {
	T t = 0;
	for (int i = 0;i < 3;i++) {
		t += (a[i] - b[i])*(c[i] - b[i]);
	}
	return t;
}


//改变向量,通过对应矩阵改变相应点的坐标,通法
void changeVector(float *MAT, float* o_v, float *n_v) {
	float tm[16];
	//矩阵转置
	for (int i = 0;i < 4;i++) {
		for (int j = 0;j < 4;j++) {
			tm[i * 4 + j] = MAT[j * 4 + i];
			//	printf("%f ", tm[i * 4 + j]);
		}
		//	printf("\n");
	}
	//printf("\n");

	for (int i = 0;i < 3;i++) {
		n_v[i] = 0;
		//printf("np[%d]=", i);
		for (int j = 0;j < 3;j++) {
			n_v[i] += tm[i * 4 + j] * o_v[j];
			//		printf("%d*%d+", (int)tm[i * 4 + j], p[j]);
		}
		//	printf("  = %d\n", np[i]);
	}
}

/*
小方块,一共需要27个,算上中间的
*/

struct Cube
{
	//画一个小方块,边长为2,中心在原点

	int validcolor[3] = { -1,-1,-1 };
	int validColorNum;
	void setValideColor() {
		validColorNum = 0;
		int a[3];

		for (int i = 0;i < 6;i++) {
			for (int j = 0;j < 3;j++)
				a[j] = (int)origion_position[j];
			switch (i)
			{
			case 0:
				a[1] -= 1;
				break;
			case 1:
				a[1] += 1;
				break;
			case 2:
				a[0] += 1;
				break;
			case 3:
				a[0] -= 1;
				break;
			case 4:
				a[2] += 1;
				break;
			case 5:
				a[2] -= 1;
				break;
			}

			int p[3] = { 0,0,0 };
			int d[3];
			for (int j = 0;j < 3;j++) {
				d[j] = (int)origion_position[j];
			}

			if (whatangle<int>(p, d, a) < 0) {
				validcolor[validColorNum++] = i;
			}
		}
	}

	void drawcube() {

		glPushMatrix();

		//glTranslatef(1, 1, 0);
		int k = 0;
		int able = validcolor[k++];
		for (int i = 0;i < 3;i++) {

			glPushMatrix();
			if (i == 0)
				glRotatef(90, 1, 0, 0);
			if (i == 1)
				glRotatef(90, 0, 1, 0);
			glTranslatef(0, 0, 1);
			if (i * 2 == able) {
				able = validcolor[k++];
				drawPlane(i * 2);
			}
			else drawPlane(6);
			//glFlush();
			glTranslatef(0, 0, -2);
			if (i * 2 + 1 == able) {
				able = validcolor[k++];
				drawPlane(1 + 2 * i);
			}
			else drawPlane(6);
			//	glFlush();
			glPopMatrix();
		}
		glPopMatrix();

	}


	bool ispicked = 0;;//是否被选中

	int index;//序号

	bool drew = 0;//是否已经画过

	float origion_position[3];//原始位置

	float current_position[3];//旋转之后的当前位置,相对位置

	float cur_abs_position[3];//绝对位置,由绝对矩阵与原始位置相乘而得

	 //原始顶点位置
	float origion_vertex[24];

	//当前顶点位置,绝对位置,由原始顶点位置与绝对矩阵相乘得出
	float current_vertex[24];

	Convex<float> c;//维护一个凸包,用于选择

	//旋转之后的当前仿射变换矩阵,相对矩阵
	float MATRIX[16] = { 1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1 };

	//绝对矩阵,由相对矩阵与总体变化的矩阵相乘而得
	float ABSOLUTE_MATRIX[16] = { 1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1 };



	//改变8个顶点坐标,绝对改变
	void changeVertix() {
		for (int i = 0;i < 8;i++) {
			changeVector(ABSOLUTE_MATRIX, origion_vertex + 3 * i, current_vertex + 3 * i);
		}
	}

	//更新凸包
	void refreshConvex() {
		float v[16];
		for (int i = 0;i < 8;i++) {
			//setPosition(AM, ori_poinVer + i * 3, cur_poinVer + i * 3);

			//得到二维坐标,去掉深度
			v[i * 2] = current_vertex[i * 3];
			v[i * 2 + 1] = current_vertex[i * 3 + 1];
		}

		//求八个顶点的凸包
		c.creatHull(v, 8, 1000);

	}
	bool picked(const float x, const  float y) {
		float p[2] = { x,y };
		return c.testIn(p);
	}


	//改变当前的位置current_position,改变相对位置
	void changePosition() {
		changeVector(MATRIX, origion_position, current_position);
	}

	void changeAbsPosition() {
		changeVector(ABSOLUTE_MATRIX, origion_position, cur_abs_position);
	}

	//画出小方块
	void draw() {
		changeAbsData(M);
		if (!drew) {

			glPushMatrix();

			//右乘当前矩阵
			glMultMatrixf(MATRIX);
			glPushMatrix();

			//平移小方块,使各个小方块按照序号放在相应的位置
			glTranslatef(index % 3 * 2 - 2, index / 3 % 3 * 2 - 2, index / 9 % 3 * 2 - 2);

			//缩小一下,使得各个方块之间有缝隙
			glScalef(CUBE_SCALE_TIME, CUBE_SCALE_TIME, CUBE_SCALE_TIME);
			drawcube();
			glPopMatrix();
			glPopMatrix();
			drew = 1;
			if (ispicked) {
				glColor3f(1, 1, 1);
				glLineWidth(3);
				glPushMatrix();
				glLoadIdentity();
				glScalef(MAGIC_CUBE_SCALE_TIME, MAGIC_CUBE_SCALE_TIME, MAGIC_CUBE_SCALE_TIME);
				//glMultMatrixf(M);
				glTranslatef(0, 0, -5);
				glPushMatrix();
				glBegin(GL_LINE_LOOP);
				for (int i = 0;i < 4;i++)
					glVertex2f((current_vertex + 3 * i)[0], (current_vertex + 3 * i)[1]);
				for (int i = 7;i >= 4;i--)
					glVertex2f((current_vertex + 3 * i)[0], (current_vertex + 3 * i)[1]);

				glEnd();
				glBegin(GL_LINES);
				glVertex2f((current_vertex)[0], (current_vertex)[1]);
				glVertex2f((current_vertex + 9)[0], (current_vertex + 9)[1]);
				glVertex2f((current_vertex + 3)[0], (current_vertex + 3)[1]);
				glVertex2f((current_vertex + 15)[0], (current_vertex + 15)[1]);
				glVertex2f((current_vertex + 6)[0], (current_vertex + 6)[1]);
				glVertex2f((current_vertex + 18)[0], (current_vertex + 18)[1]);
				glVertex2f((current_vertex + 12)[0], (current_vertex + 12)[1]);
				glVertex2f((current_vertex + 21)[0], (current_vertex + 21)[1]);

				glEnd();
				glPopMatrix();
				glPopMatrix();
			}

		}
	}
	void changeAbsData(float*M) {
		glMatrixMode(GL_MODELVIEW);
		glPushMatrix();
		glLoadIdentity();
	//	glMultMatrixf(PROJECT_MATRIX);
		//glMultMatrixf(LOOKAT_MATRIX);
		glMultMatrixf(M);
		glMultMatrixf(MATRIX);
		glGetFloatv(GL_MODELVIEW_MATRIX, ABSOLUTE_MATRIX);
		glPopMatrix();
		refreshConvex();
		changeVertix();
		changeAbsPosition();

	}
};

float verts[24] = {
	-1,-1,-1,
	-1,1,-1,
	-1,1,1,
	-1,-1,1,
	1,-1,-1,
	1,1,-1,
	1,1,1,
	1,-1,1
};
//魔方
struct MagicCube {
	//每次画的时候,首先画的9个方块的序号,初始化如下
	int firstNine[9] = { 0,1,2,3,4,5,6,7,8 };

	//27的小方块
	Cube cubes[27];
	MagicCube() {

		//初始化位置信息
		for (int i = 0;i < 27;i++) {
			cubes[i].index = i;
			cubes[i].current_position[0] = cubes[i].origion_position[0] = i % 3 - 1;
			cubes[i].current_position[1] = cubes[i].origion_position[1] = i / 3 % 3 - 1;
			cubes[i].current_position[2] = cubes[i].origion_position[2] = i / 9 % 3 - 1;

			for (int j = 0;j < 8;j++) {
				cubes[i].current_vertex[j * 3] = cubes[i].origion_vertex[j * 3] = cubes[i].origion_position[0] * 2 + verts[j * 3] * CUBE_SCALE_TIME;
				cubes[i].current_vertex[j * 3 + 1] = cubes[i].origion_vertex[j * 3 + 1] = cubes[i].origion_position[1] * 2 + verts[j * 3 + 1] * CUBE_SCALE_TIME;
				cubes[i].current_vertex[j * 3 + 2] = cubes[i].origion_vertex[j * 3 + 2] = cubes[i].origion_position[2] * 2 + verts[j * 3 + 2] * CUBE_SCALE_TIME;
			}
			cubes[i].setValideColor();
			//cubes[i].changeAbsData(M);

		//	printf("%d :%d %d %d\n", i, cubes[i].current_position[0], cubes[i].current_position[1], cubes[i].current_position[2]);
		}

	}
	//画出整个魔方
	void draw() {

		//令每个小方块变为未画的状态
		for (int i = 0;i < 27;i++) {
			cubes[i].drew = 0;
		}

		//先画前九个方块
		for (int i = 0;i < 9;i++) {
			glPushMatrix();
			//glMultMatrixf(cubes[firstNine[i]].MATRIX);
			cubes[firstNine[i]].draw();
			glPopMatrix();
		}

		//再画后18个方块,因为不能确定那几个先画,因此用27次循环,
		//若遇到之前已经画过的,因为drew为true所以直接跳过
		for (int i = 0;i < 27;i++)
		{
			//cubes[i].drew = 1;
			glPushMatrix();
			//glMultMatrixf(cubes[i].MATRIX);
			cubes[i].draw();
			glPopMatrix();
		}
	}
};
#define POSITIVE 1
#define NEGTIVE -1
#define COORD_X 0
#define COORD_Y 1;
#define COORD_Z 2

//每次旋转时,旋转了第几次,
//当time为0时,没有旋转,
//当time为9时,本次大的旋转结束,time归零
//每次大的旋转包含9次小的旋转,
//每次小的旋转会旋转10度,每次大的旋转旋转90度
int time = 0;

//方块的组.有9个成员,每次旋转一次魔方,成员有可能需要变
struct group {
	int coord;//坐标不为零的坐标轴x:0,y:1,z:2
	float center;//中心的坐标,只有1与-1两种可能
	Cube *cubes[9];//成员的指针数组
	MagicCube *mc;//魔方对象的指针
	group() {}
	group(MagicCube*m_c, int cen, int coor) {
		mc = m_c;
		center = cen;
		coord = coor;
	}


	//初始化成员,
	//或者
	//每当有任何一个group旋转一次,需要改变其他group的成员信息,调用该方法即可
	//该方法的作用是获得或改变该group的成员对象,使得满足一定要求的cube属于该group
	void getMember() {
		int k = 0;
		for (int i = 0;i < 27;i++) {

			//当cneter与当前方块的currentposition相差小于0.1时,
			//说明该方块属于该group
			if (abs(center - mc->cubes[i].current_position[coord]) < 0.1) {
				cubes[k] = &mc->cubes[i];
				//	printf("%d ", cubes[k]->index);
				mc->firstNine[k++] = i;

			}
		}

		//printf("\n");

	}

	//对group进行一次小的旋转,连续调用9次则构成一次大的旋转
	//连续调用的目的是增强动画效果
	void rotate(int right, int angle) {

		//当time为9时,说明旋转了90度,此刻才会形成完整的一步旋转


		for (int i = 0;i < 9;i++) {

			glPushMatrix();

			//每做一次小的旋转,都要对小方块的当前位置更新一下
			glLoadIdentity();

			//当coord为零时,说明group所在的面与x轴垂直,则绕x轴旋转
			//         为1时,                  y           y
			//           2                     z           z
			if (coord == 0)
				glRotatef(angle, right, 0, 0);
			else if (coord == 1)
				glRotatef(angle, 0, right, 0);
			else if (coord == 2)
				glRotatef(angle, 0, 0, right);

			glMultMatrixf(cubes[i]->MATRIX);
			glGetFloatv(GL_MODELVIEW_MATRIX, cubes[i]->MATRIX);
			glPopMatrix();

			//每次小的旋转都要改变当前位置
			//这样才可以正确画出方块
			cubes[i]->changePosition();
			mc->firstNine[i] = cubes[i]->index;
			cubes[i]->changeAbsData(M);

		}
	}


};


MagicCube mc;
group gc[6];

bool rotaing = 0;//是否正在转动
int plane_rotating = -1;//正在转动的group序号,没有转动时一直为-1
int rotat_der = -1;//转动的方向,顺时针or逆时针
int rotate_angle = 10;//每一次小旋转的角度,四种为10,修改该值的同时也要修改time的值

//计时器,用于实现转动时的动画效果
void timer(int);

int planeToDraw = -1;


struct plane {
	bool ispicked = 0;
	Cube* of;
	int color;
	float vertex[8];
	float original_center_time[3];
	float current_center_time[3];
	float *cur_vertex = 0;
	int hulVerNum = 0;
	Convex <float>c;
	plane() {}
	~plane() {
		if (cur_vertex) {
			delete[]cur_vertex;
			cur_vertex = 0;
		};

	}
	plane(const plane&p) {
		of = p.of;
		color = p.color;
		for (int i = 0;i < 3;i++) {
			original_center_time[i] = p.original_center_time[i];
		}
		for (int i = 0;i < 8;i++) {
			vertex[i] = p.vertex[i];
		}
		//changeVector(of->ABSOLUTE_MATRIX, vertex, new_vertex);
		
		refrushHull();
	}

	void refrushCurrentCenter() {
		changeVector(of->ABSOLUTE_MATRIX, original_center_time, current_center_time);
	}

	plane& operator =(const plane& p) {
		of = p.of;
		color = p.color;

		for (int i = 0;i < 3;i++) {
			original_center_time[i] = p.original_center_time[i];

		}
		//changeVector(of->ABSOLUTE_MATRIX, vertex, new_vertex);

		
		refrushHull();
		return *this;
	}
	plane(Cube* o, int c) {

		of = o;
		color = c;

		for (int i = 0;i < 3;i++) {
			original_center_time[i] = of->origion_position[i];
		}
		switch (c)
		{
		case 0:
			original_center_time[1] -= 1;
			break;
		case 1:
			original_center_time[1] += 1;
			break;
		case 2:
			original_center_time[0] += 1;
			break;
		case 3:
			original_center_time[0] -= 1;
			break;
		case 4:
			original_center_time[2] += 1;
			break;
		case 5:
			original_center_time[2] -= 1;
			break;
		}



		
		refrushHull();
		/**
			case 0:
				a[1] -= 1;
				break;
			case 1:
				a[1] += 1;
				break;
			case 2:
				a[0] += 1;
				break;
			case 3:
				a[0] -= 1;
				break;
			case 4:
				a[2] += 1;
				break;
			case 5:
				a[2] -= 1;
				*/

	}

	void refrushHull() {
		refrushCurrentCenter();
		switch (color) {
		case 3:
			for (int i = 0;i < 4;i++) {
				vertex[i * 2] = of->current_vertex[i * 3];
				vertex[i * 2 + 1] = of->current_vertex[i * 3 + 1];
			}
			break;
		case 2:
			for (int i = 0;i < 4;i++) {
				vertex[i * 2] = of->current_vertex[(i + 4) * 3];
				vertex[i * 2 + 1] = of->current_vertex[(i + 4) * 3 + 1];
			}
			break;
		case 0:
			for (int i = 0;i < 2;i++) {
				vertex[0 + i] = of->current_vertex[i];
				vertex[2 + i] = of->current_vertex[9 + i];
				vertex[4 + i] = of->current_vertex[12 + i];
				vertex[6 + i] = of->current_vertex[21 + i];
			}
			break;
		case 1:
			for (int i = 0;i < 2;i++) {
				vertex[0 + i] = of->current_vertex[3 + i];
				vertex[2 + i] = of->current_vertex[6 + i];
				vertex[4 + i] = of->current_vertex[15 + i];
				vertex[6 + i] = of->current_vertex[18 + i];
			}
			break;

		case 4:
			for (int i = 0;i < 2;i++) {
				vertex[0 + i] = of->current_vertex[9 + i];
				vertex[2 + i] = of->current_vertex[6 + i];
				vertex[4 + i] = of->current_vertex[21 + i];
				vertex[6 + i] = of->current_vertex[18 + i];
			}
			break;
		case 5:
			for (int i = 0;i < 2;i++) {
				vertex[0 + i] = of->current_vertex[3 + i];
				vertex[2 + i] = of->current_vertex[12 + i];
				vertex[4 + i] = of->current_vertex[15 + i];
				vertex[6 + i] = of->current_vertex[i];
			}
			break;
		}
		c.creatHull(vertex, 4, 1000);
		if (cur_vertex) {
			delete[]cur_vertex;
			//cur_vertex = 0;
		}
		cur_vertex = c.getNewVertx();
		hulVerNum = c.getVertexSize();

	}

	bool picked(float x, float y) {
		refrushHull();
		bool t;
		float p[2] = { x,y };
		return t = c.testIn(p);
	}
	void drawLine() {
		glPushMatrix();
		glScalef(MAGIC_CUBE_SCALE_TIME, MAGIC_CUBE_SCALE_TIME, MAGIC_CUBE_SCALE_TIME);
		glTranslatef(0, 0, -5);
		//glMultMatrixf(M);
	//	glMultMatrixf(of->MATRIX);
		glLineWidth(10);
		glColor3f(1, 0, 0);
		glBegin(GL_LINE_LOOP);
		for (int i = 0;i < hulVerNum;i++)
			glVertex2fv(cur_vertex + 2 * i);
		glEnd();
		glPopMatrix();
	}
};

plane planes[54];

void display() {

	for (int i = 0;i < 54;i++) {
		planes[i].refrushHull();
	}
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glEnable(GL_DEPTH_TEST);
	//glEnable(GL_POLYGON_SMOOTH);
	glLoadIdentity();
	//glScalef(30, 1, 1, 0);
	glScalef(MAGIC_CUBE_SCALE_TIME, MAGIC_CUBE_SCALE_TIME, MAGIC_CUBE_SCALE_TIME);
	//gluLookAt(0,  0,30, 0, 0, 0, 0, 1, 0);
	glPushMatrix();

	glMultMatrixf(M);
	/*
	//glPolygonMode()
	//bigcube();
	glPopMatrix();
	glMultMatrixf(M);

	glPushMatrix();
	glMultMatrixf(CM);
	smallcube();
	*/
	mc.draw();

	glPopMatrix();
	glPushMatrix();
	glLoadIdentity();
	if (planeToDraw >= 0) {
		//for (int i = 0;i < 54;i++) {{
		planes[planeToDraw].drawLine();
	}
	glPopMatrix();
	//}

	glFlush();
	//changeAM();
}
float stx, sty;
void mouse(int b, int s, int x, int y) {
	if (b == GLUT_LEFT_BUTTON&&s == GLUT_DOWN) {
		stx = x;
		sty = 600 - y;
	}
	if (b == GLUT_RIGHT_BUTTON&&s == GLUT_DOWN) {
		y = 600 - y;
		float X = x, Y = y;
		X = (X - 300) / 300 / MAGIC_CUBE_SCALE_TIME;
		Y = (Y - 300) / 300 / MAGIC_CUBE_SCALE_TIME;
		//printf("\n%f %f : ", X, Y);
		int theNrearest;
		bool first = 1;
		for (int i = 0;i < 27;i++) {
			mc.cubes[i].ispicked = 0;
			if (mc.cubes[i].picked(X, Y)) {
				//planes[i].refrushHull();
			//	printf("%d\n", i);
				if (first) {
					mc.cubes[i].ispicked = 1;
					theNrearest = i;
					first = 0;
				}
				else {
					if (mc.cubes[i].cur_abs_position[2] < mc.cubes[theNrearest].cur_abs_position[2]) {
						mc.cubes[i].ispicked = 1;
						mc.cubes[theNrearest].ispicked = 0;
						theNrearest = i;
					};
				}
			}
			else mc.cubes[i].ispicked = 0;

		}

		bool firstPlane = 1;
		planeToDraw = -1;
		for (int i = 0;i < 54;i++) {
			//planes[i].refrushHull();
			if (planes[i].picked(X, Y)) {
				if (firstPlane) {
					planeToDraw = i;
					firstPlane = 0;
				}
				else {
					if (planes[i].picked(X, Y) && planes[i].current_center_time[2] < planes[planeToDraw].current_center_time[2]) {
						planeToDraw = i;
					};
				}
			}
		}
		glutPostRedisplay();
		//printf("\n");
	}

}

void getPlanes() {
	int k = 0;
	for (int i = 0;i < 27;i++) {
		for (int j = 0;j < mc.cubes[i].validColorNum;j++) {
			int t = mc.cubes[i].validcolor[j];
			switch (t) {
			case 3:
				planes[k++] = plane(&mc.cubes[i], mc.cubes[i].validcolor[j]);
				break;
			case 2:
				planes[k++] = plane(&mc.cubes[i], mc.cubes[i].validcolor[j]);
				break;
			case 4:
				planes[k++] = plane(&mc.cubes[i], mc.cubes[i].validcolor[j]);
				break;
			case 5:
				planes[k++] = plane(&mc.cubes[i], mc.cubes[i].validcolor[j]);
				break;
			case 0:
				planes[k++] = plane(&mc.cubes[i], mc.cubes[i].validcolor[j]);
				break;
			case 1:
				planes[k++] = plane(&mc.cubes[i], mc.cubes[i].validcolor[j]);
				break;
			}
		}
	}
}
void motion(int x, int y) {

	//变换一下坐标系
	y = 600 - y;
	float dx = stx -x;
	float dy = sty - y;
	glLoadIdentity();
	glRotatef(2, -dy, dx, 0);
	glMultMatrixf(M);
	glGetFloatv(GL_MODELVIEW_MATRIX, M);
	glutPostRedisplay();
	stx = x;
	sty = y;
}
int cotl = 0;

//一次大的旋转,输入group的序号,与旋转的方向即可
void bigRotate(int groupIndex, int degree_direction) {


	plane_rotating = groupIndex;
	rotat_der = degree_direction;
	glutTimerFunc(20, timer, 0);
}

int stepIndex;
int step_index;
bool recover_started = 0;
void recover_timer(int t) {
	if (!recover_started) {
		recover_started = 1;
		step_index = steps.size();
	}
	if (step_index > 0) {
		step_index--;
		bigRotate(steps[step_index].drawPlane, -steps[step_index].rotate_direction);
		glutTimerFunc(1000, recover_timer, 0);
	}
	else {
		steps.clear();
		recover_started = 0;

	}
}




void key(int k, int x, int y) {
	y = 600 - y;

	if (plane_rotating == -1)
		switch (k) {
		case GLUT_KEY_F4:
			glutTimerFunc(200,recover_timer, 0);
			return;
		case GLUT_KEY_UP:
		{
			step s;
			s.drawPlane = 0;
			s.rotate_direction = -1;
			steps.push_back(s);
		}
			bigRotate(0, -1);
			break;
		case GLUT_KEY_DOWN:
			//printf("%d\n", picked(x, y));
			break;
		case GLUT_KEY_LEFT:
			bigRotate(1, -1);
			{
				step s;
				s.drawPlane = 1;
				s.rotate_direction = -1;
				steps.push_back(s);
			}
			break;
		case GLUT_KEY_RIGHT:
			bigRotate(1, 1);
			{
				step s;
				s.drawPlane = 1;
				s.rotate_direction = 1;
				steps.push_back(s);
			}
			break;
		case GLUT_KEY_F1:
			bigRotate(2, -1);
			break;

		case GLUT_KEY_F2:
			bigRotate(2, 1);
			break;
		}

	//glutPostRedisplay();
}



void timer(int n) {

	time++;//小旋转次数+1,加到9归零

		   //调用小旋转
	gc[plane_rotating].rotate(rotat_der, rotate_angle);

	glutPostRedisplay();

	//当time为9时,表示已经完成一次大的旋转,这停止旋转,而且要time归零
	//而且要调整旋转之后各个group的成员改变
	if (time == 9) {

		plane_rotating = -1;
		time = 0;

		//改变各个group的成员cube
		for (int i = 0;i < 6;i++) {
			gc[i].getMember();
		}
		//不再注册计时器,即停止旋转,直接返回
		return;
	}
	glutTimerFunc(50, timer, 0);
}
void passMotion(int x, int y) {
	y = 600 - y;
}
void main() {

	//初始化group
	for (int i = 0;i < 6;i++) {
		gc[i].mc = &mc;
		gc[i].coord = i % 3;
		if (i < 3)gc[i].center = -1;
		else gc[i].center = 1;
		gc[i].getMember();
	}
	getPlanes();

	glutInitWindowSize(600, 600);
	glutCreateWindow("TESTCUBE");
	glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE | GLUT_DEPTH);
	glClearColor(0, 0, 0, 1);
	glutDisplayFunc(display);
	glutMouseFunc(mouse);
	glutMotionFunc(motion);
	//glutPassiveMotionFunc(passMotion);
	glutSpecialFunc(key);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glGetFloatv(GL_PROJECTION_MATRIX, PROJECT_MATRIX);
//	glOrtho(-1, 1, -1, 1, 100, -100);
	//gluPerspective(30, 1, 1, 100);
	
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	//gluLookAt(0, 0, 30, 0, 0, 0, 0, 1, 0);
	glGetFloatv(GL_MODELVIEW_MATRIX, LOOKAT_MATRIX);

	//glLoadIdentity();
	//glScalef(MAGIC_CUBE_SCALE_TIME, MAGIC_CUBE_SCALE_TIME, MAGIC_CUBE_SCALE_TIME);
	//glGetFloatv(GL_MODELVIEW_MATRIX, M);
	glutMainLoop();
}


感谢阅览,若有什么好的建议,指教,想法,请留言,谢谢!


  • 1
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于OpenGL正交投影和透视投影的C++实现源码(含使用说明+exe可执行文件).zip基于OpenGL正交投影和透视投影的C++实现源码(含使用说明+exe可执行文件).zip基于OpenGL正交投影和透视投影的C++实现源码(含使用说明+exe可执行文件).zip基于OpenGL正交投影和透视投影的C++实现源码(含使用说明+exe可执行文件).zip基于OpenGL正交投影和透视投影的C++实现源码(含使用说明+exe可执行文件).zip基于OpenGL正交投影和透视投影的C++实现源码(含使用说明+exe可执行文件).zip基于OpenGL正交投影和透视投影的C++实现源码(含使用说明+exe可执行文件).zip基于OpenGL正交投影和透视投影的C++实现源码(含使用说明+exe可执行文件).zip基于OpenGL正交投影和透视投影的C++实现源码(含使用说明+exe可执行文件).zip基于OpenGL正交投影和透视投影的C++实现源码(含使用说明+exe可执行文件).zip基于OpenGL正交投影和透视投影的C++实现源码(含使用说明+exe可执行文件).zip 基于OpenGL正交投影和透视投影的C++实现源码(含使用说明+exe可执行文件).zip 基于OpenGL正交投影和透视投影的C++实现源码(含使用说明+exe可执行文件).zip 【备注】 1.项目代码均经过功能验证ok,确保稳定可靠运行。欢迎下载使用体验! 2.主要针对各个计算机相关专业,包括计算机科学、信息安全、数据科学与大数据技术、人工智能、通信、物联网等领域的在校学生、专业教师、企业员工。 3.项目具有丰富的拓展空间,不仅可作为入门进阶,也可直接作为毕设、课程设计、大作业、初期项目立项演示等用途。 4.当然也鼓励大家基于此进行二次开发。在使用过程,如有问题或建议,请及时沟通。 5.期待你能在项目找到乐趣和灵感,也欢迎你的分享和反馈!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值