计算机图形学:虚拟服装设计

要求:

1)读取给定的模型文件并能够分别使用线模式、点模式、填充模式进行渲染。

2)对模型进行平移、缩放和旋转。

3)改变视点,对模型进行三维漫游。

4)为模型中的衣裙添加物理模型,要求能够拖动某个节点或面片展示局部形变效果。

5)为模型中的衣裙添加碰撞检测,使得衣裙能在重力作用下悬挂在衣架上,以及模拟风力作用下的衣裙飘动效果。


一、实验环境

1. 操作系统:windows 8.1

2. 编程软件:visual studio 2013

二、实验内容



关键代码分析

(1)读取obj模型

void ReadPIC()

 {

int dianshu;

dianshu = 0;

ifstream ifs("dress.obj"); //用文件输入流读入文件

string s;

Mian *f;

POINT3 *v;

FaXiangLiang *vn;

WenLi *vt;

while (getline(ifs, s))  //从标准输入流中读取一个字符串,存储到字符串string(对象)中

{

if (s.length()<2)continue;

if (s[0] == 'v')

{

if (s[1] == 't') //vt 纹理 0.001992 0.001992

{

istringstream in(s); //定义一个字符串输入流的对象in,将s中所包含的字符串放入in 对象中

    vt = new WenLi();

string head;

in >> head >> vt->TU >> vt->TV;

m_pic.VT.push_back(*vt);

}

else if (s[1] == 'n') //vn 法向量 0.000000 -1.000000 0.000000

{

istringstream in(s);

vn = new FaXiangLiang();

string head;

in >> head >> vn->NX >> vn->NY >> vn->NZ;

m_pic.VN.push_back(*vn);

}

else //v 点 - 0.500000 - 0.500000 0.500000

{

istringstream in(s);

v = new POINT3();

string head;

in >> head >> v->X >> v->Y >> v->Z;

m_pic.V.push_back(*v);

dianshu++;

}

//printf("dianshu=%d",dianshu);

}

else if (s[0] == 'f')//f 面 1/1/1 2/2/2 3/3/3  这个面的顶点、纹理坐标、法向量的索引  //f 2443//2656 2442//2656 2444//2656 面

{

for (int k = s.size() - 1; k >= 0; k--)

{

if (s[k] == '/')s[k] = ' ';

}

istringstream in(s);

f = new Mian();

string head;

in >> head;

int i = 0;

while (i<3)

{

if (m_pic.V.size() != 0)

{

in >> f->V[i];

f->V[i] -= 1;

}

if (m_pic.VT.size() != 0)

{

in >> f->T[i];

f->T[i] -= 1;

}

if (m_pic.VN.size() != 0)

{

in >> f->N[i];

f->N[i] -= 1;

}

i++;

}

m_pic.F.push_back(*f);

}

}

}

读取文件dress.obj,依据文件中的索引将数据分别存入法向量m_pic.VN纹理坐标m_pic.VT,顶点m_pic.V依据面的索引,法向量纹理坐标顶点按所在三角形面分别存入m_pic.F中,方便后面绘制以及变换操作。由于数据量大,读取文件函数ReadPIC()设置了标志位,只在文件启动时执行1次。

(2)绘制模型

void InitScene()

{

static GLint flag = 1;//设置标志 位,第一次读取obj文件到数组,后面直接显示

if (flag == 1)

{

flag = 0;

ReadPIC();

}

glClearColor(1.000f, 1.000f, 1.000f, 1.0f); //Background color

// TODO: Replace the following sample code with your initialization code.

// Activate lighting and a light source

//用于启用各种功能。具体功能由参数决定。与glDisable相对应。glDisable用以关闭各项功能。

glEnable(GL_LIGHT0);//启用0号灯到7号灯(光源)  光源要求由函数glLight函数来完成

glEnable(GL_LIGHTING);//启用灯源

glEnable(GL_DEPTH_TEST);//启用深度测试。  根据坐标的远近自动隐藏被遮住的图形(材料)

glEnable(GL_TEXTURE_2D);   // 启用二维纹理

// Define material parameters

static GLfloat glfMatAmbient[] = { 0.000f, 0.450f, 1.000f, 1.0f };

static GLfloat glfMatDiffuse[] = { 0.000f, 0.000f, 0.580f, 1.0f };

static GLfloat glfMatSpecular[] = { 1.000f, 1.000f, 1.000f, 1.0f };

static GLfloat glfMatEmission[] = { 0.000f, 0.000f, 0.000f, 1.0f };

static GLfloat fShininess = 128.000f;

// Set material parameters

//指定用于光照计算的当前材质属性。参数face的取值可以是GL_FRONT、GL_BACK或GL_FRONT_AND_BACK,指出材质属性将应用于物体的哪面。

//void glMaterial{if}(GLenum face, GLenum pname, TYPE param);

glMaterialfv(GL_FRONT, GL_AMBIENT, glfMatAmbient);

glMaterialfv(GL_FRONT, GL_DIFFUSE, glfMatDiffuse);

glMaterialfv(GL_FRONT, GL_SPECULAR, glfMatSpecular);

glMaterialfv(GL_FRONT, GL_EMISSION, glfMatEmission);

glMaterialf(GL_FRONT, GL_SHININESS, fShininess);

}

void DrawScene()

 {

// TODO: Replace the following sample code with your code to draw the scene.

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  // 清除屏幕及深度缓存

glLoadIdentity(); // 重置模型观察矩阵

gluLookAt(viewer[0], viewer[1], viewer[2], 0.0f, -4.0f, -9.0f, 0.0, 1.0, 0.0);

glTranslatef(0.0f, -13.0f, -8.0f); // 移入屏幕 5.0

if (trackballMove&&translating&&!scaling&&!rotating)

{

glTranslatef(tran[0], tran[1], tran[2]);

//printf("tran[0]=%f, tran[1]=%f, tran[2]=%f\n", tran[0], tran[1], tran[2]);

}

if (trackballMove&&!translating&&scaling&&!rotating)

{

glScalef(scale[0], scale[1], scale[2]);

}

if (trackballMove&&!translating&&!scaling&&rotating)

{

glRotatef(angle0, axis[0], axis[1], axis[2]);//旋转(角度,轴)

}

GLCube();// Draw a cube

glFlush();

}

void GLCube()

{

if (dian_model&&!xian_model&&!mian_model)

//if (0)

{

for (int i = 0; i < (m_pic.F.size()); i++)

//for (int i = 0; i<7959; i++)

{

glBegin(GL_POINTS);//点模式

if (qizhi0 == 1)

{

printf("dianmoshi");

qizhi0 = 0;

}

/*glVertex3f(m_pic.V[m_pic.F[3 * i].V[0]].X / YU, m_pic.V[m_pic.F[3 * i].V[0]].Y / YU, m_pic.V[m_pic.F[3 * i].V[0]].Z / YU);

glVertex3f(m_pic.V[m_pic.F[3 * i].V[1]].X / YU, m_pic.V[m_pic.F[3 * i].V[1]].Y / YU, m_pic.V[m_pic.F[3 * i].V[1]].Z / YU);

glVertex3f(m_pic.V[m_pic.F[3 * i].V[2]].X / YU, m_pic.V[m_pic.F[3 * i].V[2]].Y / YU, m_pic.V[m_pic.F[3 * i].V[2]].Z / YU);

*/

glVertex3f(m_pic.V[m_pic.F[i].V[0]].X / YU, m_pic.V[m_pic.F[i].V[0]].Y / YU, m_pic.V[m_pic.F[i].V[0]].Z / YU);

glVertex3f(m_pic.V[m_pic.F[i].V[1]].X / YU, m_pic.V[m_pic.F[i].V[1]].Y / YU, m_pic.V[m_pic.F[i].V[1]].Z / YU);

glVertex3f(m_pic.V[m_pic.F[i].V[2]].X / YU, m_pic.V[m_pic.F[i].V[2]].Y / YU, m_pic.V[m_pic.F[i].V[2]].Z / YU);

//glVertex3f(10*(model.vertices[3 * i]), 10*(model.vertices[3 * i + 1]), 10*(model.vertices[3 * i + 2]));

glEnd();

}

}

if (!dian_model&&xian_model&&!mian_model)

//if (0)

{

for (int i = 0; i<(m_pic.F.size()); i++)

{

glBegin(GL_LINES);//线模式

if (qizhi1 == 1)

{

printf("xianmoshi");

qizhi1 = 0;

}

glVertex3f(m_pic.V[m_pic.F[i].V[0]].X / YU, m_pic.V[m_pic.F[i].V[0]].Y / YU, m_pic.V[m_pic.F[i].V[0]].Z / YU);

glVertex3f(m_pic.V[m_pic.F[i].V[1]].X / YU, m_pic.V[m_pic.F[i].V[1]].Y / YU, m_pic.V[m_pic.F[i].V[1]].Z / YU);

//glVertex3f(m_pic.V[m_pic.F[i].V[2]].X / YU, m_pic.V[m_pic.F[i].V[2]].Y / YU, m_pic.V[m_pic.F[i].V[2]].Z / YU);

glEnd();

glBegin(GL_LINES);

//glVertex3f(m_pic.V[m_pic.F[i].V[0]].X / YU, m_pic.V[m_pic.F[i].V[0]].Y / YU, m_pic.V[m_pic.F[i].V[0]].Z / YU);

glVertex3f(m_pic.V[m_pic.F[i].V[1]].X / YU, m_pic.V[m_pic.F[i].V[1]].Y / YU, m_pic.V[m_pic.F[i].V[1]].Z / YU);

glVertex3f(m_pic.V[m_pic.F[i].V[2]].X / YU, m_pic.V[m_pic.F[i].V[2]].Y / YU, m_pic.V[m_pic.F[i].V[2]].Z / YU);

glEnd();

glBegin(GL_LINES);

glVertex3f(m_pic.V[m_pic.F[i].V[0]].X / YU, m_pic.V[m_pic.F[i].V[0]].Y / YU, m_pic.V[m_pic.F[i].V[0]].Z / YU);

//glVertex3f(m_pic.V[m_pic.F[i].V[1]].X / YU, m_pic.V[m_pic.F[i].V[1]].Y / YU, m_pic.V[m_pic.F[i].V[1]].Z / YU);

glVertex3f(m_pic.V[m_pic.F[i].V[2]].X / YU, m_pic.V[m_pic.F[i].V[2]].Y / YU, m_pic.V[m_pic.F[i].V[2]].Z / YU);

glEnd();

}

}

if (!dian_model&&!xian_model&&mian_model)

//if (1)

{

for (int i = 0; i<(m_pic.F.size()); i++)

{

//printf("m_pic.F.size()=%d", m_pic.F.size());//16213

glBegin(GL_TRIANGLES);//面模式// 绘制三角形

if (qizhi2 == 1)

{

printf("mianmoshi");

qizhi2 = 0;

}

if (m_pic.VT.size() != 0)glTexCoord2f(m_pic.VT[m_pic.F[i].T[0]].TU, m_pic.VT[m_pic.F[i].T[0]].TV);  //纹理

if (m_pic.VN.size() != 0)glNormal3f(m_pic.VN[m_pic.F[i].N[0]].NX, m_pic.VN[m_pic.F[i].N[0]].NY, m_pic.VN[m_pic.F[i].N[0]].NZ);//法向量

glVertex3f(m_pic.V[m_pic.F[i].V[0]].X / YU, m_pic.V[m_pic.F[i].V[0]].Y / YU, m_pic.V[m_pic.F[i].V[0]].Z / YU); // 上顶点

//printf("dian:%f,%f,%f\n",m_pic.V[m_pic.F[i].V[0]].X / YU, m_pic.V[m_pic.F[i].V[0]].Y / YU, m_pic.V[m_pic.F[i].V[0]].Z / YU);

if (m_pic.VT.size() != 0)glTexCoord2f(m_pic.VT[m_pic.F[i].T[1]].TU, m_pic.VT[m_pic.F[i].T[1]].TV);  //纹理

if (m_pic.VN.size() != 0)glNormal3f(m_pic.VN[m_pic.F[i].N[1]].NX, m_pic.VN[m_pic.F[i].N[1]].NY, m_pic.VN[m_pic.F[i].N[1]].NZ);//法向量

glVertex3f(m_pic.V[m_pic.F[i].V[1]].X / YU, m_pic.V[m_pic.F[i].V[1]].Y / YU, m_pic.V[m_pic.F[i].V[1]].Z / YU); // 左下

if (m_pic.VT.size() != 0)glTexCoord2f(m_pic.VT[m_pic.F[i].T[2]].TU, m_pic.VT[m_pic.F[i].T[2]].TV);  //纹理

if (m_pic.VN.size() != 0)glNormal3f(m_pic.VN[m_pic.F[i].N[2]].NX, m_pic.VN[m_pic.F[i].N[2]].NY, m_pic.VN[m_pic.F[i].N[2]].NZ);//法向量

glVertex3f(m_pic.V[m_pic.F[i].V[2]].X / YU, m_pic.V[m_pic.F[i].V[2]].Y / YU, m_pic.V[m_pic.F[i].V[2]].Z / YU); // 右下

glEnd(); // 三角形绘制结束

}

}

}

InitScene()函数中设置了场景的光源和模型的材质。DrawScene()中绘制模型,调用函数GLCube()实现具体绘制,并由标志位设置绘制模式:点模式,线模式和面模式;同时使用库函数实现模型旋转缩放平移变换操作,具体参数由虚拟球设定;同时gluLookAt(viewer[0], viewer[1], viewer[2], 0.0f, -4.0f, -9.0f, 0.0, 1.0, 0.0);函数实现三维漫游,viewer[0], viewer[1], viewer[2]数值由键盘设置,后面的视点方向等用于使模型位于世界坐标系正中间。

(3)物理模型

Cloth::Cloth(GLMmodel *model)

{

//

SpringConstant = 1000;//弹力

DampingFactor = 10;//阻尼

density = 1.2;//密度

drag = 0.4;//阻力

//set vert

Myvertex vert;

verts_.push_back(vert);

for (int i = 1; i <= 7959; i++) // m_pic.V[m_pic.F[i].V[2]].X

{

//printf("%d\n", m_pic.V.size());//7959

vert.id_ = i;

vert.position_ = point(model->vertices[3 * i], model->vertices[3 * i + 1], model->vertices[3 * i + 2]);//point: vec

//printf("[1]=%lf\n", model->vertices[3 * i]);

//printf("[2]=%lf\n", model->vertices[3 * i+1]);

//printf("[3]=%lf\n", model->vertices[3 * i+2]);

//vert.texCoord_ = Vec2f(model->)

verts_.push_back(vert);

}

//calculate hanger_aabb 衣架aabb包围盒

for (int i = 3381; i <= model->numvertices; i++)

{

if (model->vertices[3 * i] > hanger_aabb.max_x)

{

hanger_aabb.max_x = model->vertices[3 * i];

}

if (model->vertices[3 * i] < hanger_aabb.min_x)

{

hanger_aabb.min_x = model->vertices[3 * i];

}

if (model->vertices[3 * i + 1] > hanger_aabb.max_y)

{

hanger_aabb.max_y = model->vertices[3 * i + 1];

}

if (model->vertices[3 * i + 1] < hanger_aabb.min_y)

{

hanger_aabb.min_y = model->vertices[3 * i + 1];

}

if (model->vertices[3 * i + 2] > hanger_aabb.max_z)

{

hanger_aabb.max_z = model->vertices[3 * i + 2];

}

if (model->vertices[3 * i + 2] < hanger_aabb.min_z)

{

hanger_aabb.min_z = model->vertices[3 * i + 2];

}

}

update_aabb(); //calculate_cloth_aabb

for (int i = 1; i <= model->numvertices; i++) //if points in hanger_aabb,point is static

{

if (p_in_aabb(verts_[i].position_))

{

verts_[i].is_fixed = true;

}

else

verts_[i].is_fixed = false;

}

//set face

Myface face;

for (int i = 0; i < model->numtriangles; i++)

{

face.id_ = i;

int nid = model->triangles[i].findex;

face.verId = ivec3(model->triangles[i].vindices[0], model->triangles[i].vindices[1], model->triangles[i].vindices[2]);

face.normal_ = Vec3f(model->facetnorms[nid * 3], model->facetnorms[nid * 3 + 1], model->facetnorms[nid * 3 + 2]);

for (int j = 0; j < 3; j++)

{

int tid = model->triangles[i].tindices[j];

face.texCoord_[j] = Vec2f(model->texcoords[tid * 2], model->texcoords[2 * tid + 1]);

}

int id[3];

for (int j = 0; j < 3; j++)

{

id[j] = face.verId[j];

face.vertex3[j] = &verts_[id[j]];

}

face.s = fabs(((verts_[id[1]].position_ - verts_[id[0]].position_)CROSS(verts_[id[2]].position_ - verts_[id[0]].position_)).length() / 2.0);

faces_.push_back(face);

}

//delete some bad faces

for (int i = 0; i < 6546; i++)

{

if ((faces_[i].normal_[0] * faces_[i].vertex3[0]->position_[0] + faces_[i].normal_[2] * faces_[i].vertex3[0]->position_[2]) < -0.05)

{

faces_[i].badface = true;

}

}

//delete some bad verts,and find bound

for (int i = 0; i < 6546; i++)

{

if (faces_[i].badface)

{

for (int j = 0; j < 3; j++)

{

faces_[i].vertex3[j]->kengdie = true;

faces_[i].vertex3[j]->is_bound = true;

}

}

}

for (int i = 0; i < 6546; i++)

{

if (!faces_[i].badface)

{

for (int j = 0; j < 3; j++)

{

faces_[i].vertex3[j]->kengdie = false;

}

}

}

set_neighbor();

set_weight();

//Update(float deltatime);

}

}

服装模型类,设置了服装的物理模型,为服装添加了SpringConstant弹力DampingFactor阻尼density密度drag 阻力属性;分别找到衣架和服装的顶点取值范围,确定衣架和服装的包围盒;形变是针对服装模型的三角形面片作变化,将模型的基本数据存储到face中;由于原模型中裙摆双层布料,碰撞检测的效果不好,故删去裙摆内衬,即badface。

4)碰撞检测

static void idle() {

if (phy_){ if (loop < 16){ Update(); loop++; } else loop = 0; }

}

void keys(unsigned char key, int x, int y)

{

/* Use x, X, y, Y, z, and Z keys to move viewer */

if (key == 'x') viewer[0] -= 1.0;

if (key == 'X') viewer[0] += 1.0;

if (key == 'y') viewer[1] -= 1.0;

if (key == 'Y') viewer[1] += 1.0;

if (key == 'z') viewer[2] -= 1.0;

if (key == 'Z') viewer[2] += 1.0;

//display();

if (!phy_){

DrawScene();

}

if (key == 'p'){

phy_ = true; printf("phy_=true\n");

}

if (key == 'o'){

wind.x += 0.5;

wind.Print("New Wind:");

}

if (key == 'i'){

wind.y += 0.5;

wind.Print("New Wind:");

}

if (key == 'u'){

wind.z += 0.5;

wind.Print("New Wind:");

}

}

void Cloth::Update(float deltatime)

{

//set gravity

for (int i = 1; i < verts_.size(); i++)

{

verts_[i].applyForce(Vec3f(0, -0.1, 0)*verts_[i].weight);

//verts_[i].applyForce(Vec3f(0, 0, 0.01));

//verts_[i].Update(deltatime);

}

change_wind();

//set wind

for (int i = 0; i < faces_.size(); i++)

{

faces_[i].updateface();

}

//set  spring

for (int i = 0; i < springDampers.size(); i++)

{

springDampers[i].CalculateForces();

}

collision_detection();//碰撞检测

//update verts

for (int i = 1; i < verts_.size(); i++)

{

verts_[i].Update(deltatime);

}

//update normals

update_normal();

}

void Cloth::change_wind()

{

for (int i = 0; i < 6546; i++)

{

faces_[i].Changewind(wind);

}

}

void Cloth::update_normal()

{

for (int i = 0; i < faces_.size(); i++)

{

Vec3f p = verts_[faces_[i].verId[0]].position_;

Vec3f q = verts_[faces_[i].verId[1]].position_;

Vec3f r = verts_[faces_[i].verId[2]].position_;

Vec3f normal_update = ((p - r) CROSS(q - r));

normal_update.normalize();

//normal is  continuous variable

if (normal_update DOT faces_[i].normal_>0)

{

faces_[i].normal_ = normal_update;

}

else

{

faces_[i].normal_ = -normal_update;

}

}

}

void Cloth::set_neighbor()

{

for (int i = 0; i < faces_.size(); i++)

{

if (!faces_[i].badface)

{

int vid1;

int vid2;

for (int j = 0; j < 3; j++)

{

vid1 = faces_[i].verId[j];

vid2 = faces_[i].verId[(j + 1) % 3];

verts_[vid1].neighborIdx.push_back(vid2);

float dis;

dis = (verts_[vid1].position_ - verts_[vid2].position_).length();

verts_[vid1].nei_dis.push_back(dis);

verts_[vid1].neighborfaceId.push_back(i);

if (verts_[vid1].is_bound&&verts_[vid2].is_bound)

{

verts_[vid2].neighborIdx.push_back(vid1);

verts_[vid2].nei_dis.push_back(dis);

}

}

}

}

for (int i = 1; i < verts_.size(); i++)

{

verts_[i].degree_ = verts_[i].neighborIdx.size();

}

for (int i = 0; i < faces_.size(); i++)

{

if (!faces_[i].badface)

{

int vid1;

int vid2;

for (int j = 0; j < 3; j++)

{

vid1 = faces_[i].verId[j];

vid2 = faces_[i].verId[(j + 1) % 3];

springDampers.push_back(springDamper(&verts_[vid1], &verts_[vid2], SpringConstant, DampingFactor));//弹簧阻尼

}

}

}

}

void Cloth::collision_detection()

{

update_aabb();

for (int i = 0; i < 40; i++)

for (int j = 0; j < 40; j++)

for (int k = 0; k < 40; k++)

{

voxels[i][j][k].clear();

}

for (int i = 0; i < 6546; i++)

{

faces_[i].voxel_pos.clear();

}

for (int i = 0; i < 6546; i++)

{

if (!faces_[i].badface)

{

aabb ab_ = faces_[i].get_aabb();

int l = floor((ab_.min_x - cloth_aabb.min_x) / (cloth_aabb.max_x - cloth_aabb.min_x)*40.f);

int m = floor((ab_.min_y - cloth_aabb.min_y) / (cloth_aabb.max_y - cloth_aabb.min_y)*40.f);

int n = floor((ab_.min_z - cloth_aabb.min_z) / (cloth_aabb.max_z - cloth_aabb.min_z)*40.f);

if (l < 0)

l = 0;

if (m < 0)

m = 0;

if (n < 0)

n = 0;

voxels[l][m][n].push_back(i);

faces_[i].voxel_pos = ivec3(l, m, n);

}

}

for (int i = 0; i < 6546; i++)

{

ivec3 voxel = faces_[i].voxel_pos;

for (int j = 0; j < voxels[voxel[0]][voxel[1]][voxel[2]].size(); j++)

{

int face_id = voxels[voxel[0]][voxel[1]][voxel[2]][j];

bool b = tri_collision_detection(i, face_id);

}

}

/*for (int i = 0; i < 6546; i++)

for (int j = 0; j < 6546; j++)

{

tri_collision_detection(i, j);

}*/

}

bool Cloth::tri_collision_detection(int id1, int id2)

{

if ((faces_[id1].normal_ DOT faces_[id2].normal_)>0)

return false;

if (faces_[id1].badface || faces_[id2].badface)

return false;

Vec3f dis(0, 0, 0);

for (int i = 0; i < 3; i++)

{

dis += (faces_[id1].vertex3[i]->position_ - faces_[id2].vertex3[i]->position_) / 3.0f;

}

if (dis.length() > 0.001)

return false;

else

{

Vec3f n = Vec3f(0, 0, 0);

for (int i = 0; i < 3; i++)

{

n = (faces_[id2].normal_ DOT(faces_[id1].vertex3[i]->position_ - faces_[id2].vertex3[1]->position_))*faces_[id2].normal_;

if (n.length() < 0.01)

{

n.normalize();

if (n DOT faces_[id1].vertex3[i]->velocity < 0)

{

Vec3f vv = Vec3f(0, 0, 0);

vv = (n DOT faces_[id1].vertex3[i]->velocity)*-1.1f*n;

faces_[id1].vertex3[i]->velocity += vv;

/*for (int j = 0; j < 3; j++)

{

faces_[id2].vertex3[j]->velocity += vv / -1.1f  / 3.0f;

}*/

//faces_[id1].vertex3[i]->velocity = Vec3f(0, faces_[id1].vertex3[i]->velocity[1], 0);

}

}

}

return true;

}

}

由键盘设置是否开启碰撞检测,按下’p’键即开启碰撞检测,风力大小也由键盘设置。碰撞检测具体实现流程见上述代码。



猜你喜欢

转载自blog.csdn.net/crystalb13/article/details/60961630