渲染填充凹凸多边形 OpenGL(总结)

OpenGL中认为合法的多边形必须是凸多边形,凹多边形、自交多边形、带孔的多边形等非凸的多边形在OpenGL中绘制会出现出乎意料的结果。例如,在大多数系统中,只有多边形的凸包被填充,而在有些系统中,并非所有的凸包都被填充。OpenGL之所以对合法多边形类型做出限制,是为了更方便地提供能够对符合条件的多边形进行快速渲染的硬件。简单多边形可被快速地渲染,而复杂多边形难以快速检测出来。为了最大限度的提高性能,OpenGL假定多边形是简单的。

polygon

解决凹多边形渲染的方法有以下几种:

第一种解决方案:多边形网格化法 - 已实现并测试

对于非简单多边形、非凸多边形或有洞的多边形,OpenGL在GLU库中提供了一个多边形网格化对象GLUtesselator,对多边形进行网格化————将它们分解成一组简单的、能够进行渲染的OpenGL多边形。

经测试这种方法对凹凸多边形和自交、带孔多边形都能正确的渲染。

 

第二种解决方案:模板缓冲法stencil Buffer - 以实现并测试

// 具体流程阐述:

//    1.申请模板缓存区    为了使用OpenGL的模板功能,首先必须使用平台特定的OpenGL设置过程请求一个模板缓存区。在以VC++.NET为基础的OpenGL平台中,是在设置像素格式时在PIXELFORMATDESCRIPTOR结构中指定的模板缓存,并且需要指定模板缓存的位数。如果使用GLUT,在初始化显示模式时请求一个模板缓存区,下面的代码设置了带模板缓存的双缓存RGB颜色缓存区:    glutInitDisplayMode(GLUT_RGB|GLUT_DOUBLE|GLUT_STENCIL)。如果使用了全屏反走样功能,wglChoosePixelFormatARB 使用的参数 中

int iAttributes[] =
{
    WGL_DRAW_TO_WINDOW_ARB,GL_TRUE,
    WGL_SUPPORT_OPENGL_ARB,GL_TRUE,
    WGL_ACCELERATION_ARB,WGL_FULL_ACCELERATION_ARB,
    WGL_COLOR_BITS_ARB,24,
    WGL_ALPHA_BITS_ARB,8,
    WGL_DEPTH_BITS_ARB,16,
    WGL_STENCIL_BITS_ARB,8,
    WGL_DOUBLE_BUFFER_ARB,GL_TRUE,
    WGL_SAMPLE_BUFFERS_ARB,GL_TRUE,
    WGL_SAMPLES_ARB,4,
    0,0
};

WGL_STENCIL_BITS_ARB后面的参数决不能为0,用8就可以。
//    2. 首先清除模板缓存,并禁用颜色缓存的写入状态glColorMask(GL_FALSE,GL_FALSE,GL_FALSE,GL_FALSE),将模板缓存操作函数设置为GL_INVERT,glStencilFunc(GL_ALWAYS, 0x1, 0x1);    glStencilOp(GL_KEEP, GL_KEEP, GL_INVERT);
//    3. 任取一个点P(这里取所有点的平均坐标,也可以是非第一个点的),绘制三角扇,注意首末点要是同一个点,这样绘制所有三角形后,像素被覆盖偶数次相应的模板缓存值为零,否则非零
//    4. 恢复状态glEnable(GL_DEPTH_TEST);    设置模板缓存函数glStencilFunc(GL_NOTEQUAL,0,0x1);    glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);    glColorMask(GL_TRUE,GL_TRUE,GL_TRUE,GL_TRUE);   
//    绘制一个大的覆盖所有区域的多边形(我用的是四边形),只绘制模板缓存为非零的像素,OK效果达到
//    5. 使用显示列表加快渲染速度

经测试这种方法对凹凸多边形和自交、带孔多边形都能正确的渲染。

 

第三种解决方案: 凹多边形凸分解法

思路: 使用算法将凹多边形分解为多个凸多边形或一系列的三角形,然后进行渲染。

这种思路对于由一条边组成的凹多边形还是可行的,但对于自交和带孔的多边形复杂度会很大,很难解决所有问题,所以就没有再深入写下去了,把我找到的一个算法附在下面,有兴趣的同志可以继续做下去,有成果的话记得发给我一份呀,O(∩_∩)O~

一个三角化多边形的算法

    在使用OpenGL画图的过程中,由于OpenGL不支持直接绘制凹多边形,所以我们通常需要先将凹多边形转化为一组三角形下面就是一个三角化多边形的算法
1) 用单向循环链表保存多边形顶点,并计算这个链表中每一个顶点的凸凹性。
2) 在循环链表中顺序取三个结点P、Q、R ,如果Q 为凸点,并且由P、Q、R 所构成的三角形PQR不包含多边形上其他顶点,则计算△PQR 的特征角(三角形内最小的角)。求出所有这样的三角形,从中选择特征角最大的△PQR ,保存该三角形,并从链表中删去结点Q。
3) 如果链表中不存在三个以上顶点,则转步骤2)。
4) 由链表中的最后三个顶点构成一个三角形。

 

实现代码贡献给大家吧:

……

// 设置像素格式

static    PIXELFORMATDESCRIPTOR pfd=                // pfd Tells Windows How We Want Things To Be
{
    sizeof(PIXELFORMATDESCRIPTOR),                // Size Of This Pixel Format Descriptor
    1,                                            // Version Number
    PFD_DRAW_TO_WINDOW |                        // Format Must Support Window
    PFD_SUPPORT_OPENGL |                        // Format Must Support OpenGL
    PFD_DOUBLEBUFFER,                            // Must Support Double Buffering
    PFD_TYPE_RGBA,                                // Request An RGBA Format
    bits,                                        // Select Our Color Depth
    0, 0, 0, 0, 0, 0,                            // Color Bits Ignored
    0,                                            // No Alpha Buffer
    0,                                            // Shift Bit Ignored
    0,                                            // No Accumulation Buffer
    0, 0, 0, 0,                                 // Accumulation Bits Ignored
    16,                                           // 16Bit Z-Buffer (Depth Buffer) 
    1,                                            // Use Stencil Buffer ( * Important * )   -------此处必须为1!!!
    0,                                            // No Auxiliary Buffer
    PFD_MAIN_PLANE,                       // Main Drawing Layer
    0,                                            // Reserved
    0, 0, 0                                      // Layer Masks Ignored
};

……

GLuint nTesselatorList = 0;

void CALLBACK beginCallback(GLenum which)
{
    glBegin(which);
}
void CALLBACK errorCallback(GLenum errorCode)
{
    const GLubyte *estring;
    estring = gluErrorString(errorCode);
    fprintf(stderr, "Tessellation Error: %s\n", estring);
    exit(0);
}
void CALLBACK endCallback(void)
{
    glEnd();
}
void CALLBACK vertexCallback(GLvoid *vertex)
{
    const GLdouble *pointer;
    pointer = (GLdouble *) vertex;
    glColor3dv(pointer+3);
    glVertex3dv(pointer);
}
void CALLBACK combineCallback(GLdouble coords[3],
                              GLdouble *vertex_data[4],
                              GLfloat weight[4], GLdouble **dataOut )
{
    GLdouble *vertex;
    int i;
    vertex = (GLdouble *) malloc(6 * sizeof(GLdouble));
    vertex[0] = coords[0];
    vertex[1] = coords[1];
    vertex[2] = coords[2];
    for (i = 3; i < 7; i++)
        vertex[i] = weight[0] * vertex_data[0][i]
    + weight[1] * vertex_data[1][i]
    + weight[2] * vertex_data[2][i]
    + weight[3] * vertex_data[3][i];
    *dataOut = vertex;
}

void _polygonRender(int mode)
{// 多边形渲染器
    // define concave quad data (vertices only)
    //  0    2
    //  \ \/ /
    //   \3 /
    //    \/
    //    1
    //GLdouble quad1[4][3] = { {-1,3,0}, {0,0,0}, {1,3,0}, {0,2,0} };

    GLdouble quad1[4][3] = { {-1,1,2}, {0,0,2}, {1,1,2}, {0,0.7,2} };

    // define concave quad with a hole
    //  0--------3
    //  | 4----7 |
    //  | |    | |
    //  | 5----6 |
    //  1--------2
    GLdouble quad2[12][3] = { {-2,3,0}, {-2,0,0}, {2,0,0}, { 2,3,0},
    {-1,2,0}, {-1,1,0}, {1,1,0}, { 1,2,0}
    , {-0.5,1,0}, {-0.5,2,0}, {0.5,2,0}, { 0.5,1,0} };

if (mode == 0)
{// 多边形网格化对象渲染
    if (nTesselatorList == 0)
    {
        nTesselatorList = glGenLists(1);
        // 方法1: 多边形网格化 (测试通过)
        // 检测glu版本
        const GLubyte * pgluVersion = gluGetString(GLU_VERSION);

        GLUtesselator* tobj = gluNewTess();
        if (!tobj) return;

        gluTessCallback(tobj, GLU_TESS_VERTEX, (void (CALLBACK *)())vertexCallback);
        gluTessCallback(tobj, GLU_TESS_BEGIN, (void (CALLBACK *)())beginCallback);
        gluTessCallback(tobj, GLU_TESS_END, (void (CALLBACK *)())endCallback);
        gluTessCallback(tobj, GLU_TESS_ERROR, (void (CALLBACK *)())errorCallback);
        gluTessCallback(tobj, GLU_TESS_COMBINE, (void (CALLBACK *)())combineCallback);

        glNewList(nTesselatorList,GL_COMPILE);
        //glShadeModel(GL_FLAT);

        //gluTessProperty(tobj,GLU_TESS_WINDING_RULE,GLU_TESS_WINDING_POSITIVE); //GLU_TESS_WINDING_ODD
        gluTessBeginPolygon(tobj,NULL);

        gluTessBeginContour(tobj);
        gluTessVertex(tobj, quad2[0], quad2[0]);
        gluTessVertex(tobj, quad2[1], quad2[1]);
        gluTessVertex(tobj, quad2[2], quad2[2]);
        gluTessVertex(tobj, quad2[3], quad2[3]);
        gluTessEndContour(tobj);

        gluTessBeginContour(tobj);                      // inner quad (hole)
        gluTessVertex(tobj, quad2[4], quad2[4]);
        gluTessVertex(tobj, quad2[5], quad2[5]);
        gluTessVertex(tobj, quad2[6], quad2[6]);
        gluTessVertex(tobj, quad2[7], quad2[7]);
        gluTessEndContour(tobj);

        //gluTessBeginContour(tobj);                      // inner quad (hole)
        //gluTessVertex(tobj, quad2[8], quad2[8]);
        //gluTessVertex(tobj, quad2[9], quad2[9]);
        //gluTessVertex(tobj, quad2[10], quad2[10]);
        //gluTessVertex(tobj, quad2[11], quad2[11]);
        //gluTessEndContour(tobj);

        //gluTessBeginContour(tobj);
        //gluTessVertex(tobj, quad1[0], quad1[0]);
        //gluTessVertex(tobj, quad1[1], quad1[1]);
        //gluTessVertex(tobj, quad1[2], quad1[2]);
        //gluTessVertex(tobj, quad1[3], quad1[3]);
        //gluTessEndContour(tobj);

        gluTessEndPolygon(tobj);

        gluDeleteTess(tobj);

        glEndList();
    }
    else
    {
        glCallList(nTesselatorList);
    }

}
else if (mode == 1)
{// 蒙板缓冲   
    glClear(GL_STENCIL_BUFFER_BIT);

    glClearStencil(0x0);
    glEnable(GL_STENCIL_TEST);
    glColorMask(GL_FALSE,GL_FALSE,GL_FALSE,GL_FALSE);    // 禁用颜色缓存写入 重要!!

    // 设置模板缓存操作函数为GL_INVERT
    glStencilFunc(GL_ALWAYS, 0x1, 0x1);
    glStencilOp(GL_KEEP, GL_KEEP, GL_INVERT);

      glDisable(GL_DEPTH_TEST);    // 禁用深度缓存 重要!!
        GLdouble center[3];
        center[0] = (quad1[0][0] + quad1[1][0] + quad1[2][0] + quad1[3][0]) / 4.0;
        center[1] = (quad1[0][1] + quad1[1][1] + quad1[2][1] + quad1[3][1]) / 4.0;
        center[2] = (quad1[0][2] + quad1[1][2] + quad1[2][2] + quad1[3][2]) / 4.0;

        //center[0] = quad2[1][0];
        //center[1] = quad2[1][1];
        center[2] = quad2[1][2];

        // 绘制多边形
        glBegin(GL_TRIANGLE_FAN);
        glVertex3dv(center);       
        //glVertex3dv(quad1[0]);
        //glVertex3dv(quad1[1]);
        //glVertex3dv(quad1[2]);
        //glVertex3dv(quad1[3]);
        //glVertex3dv(quad1[0]);

        glVertex3dv(quad2[0]);
        glVertex3dv(quad2[1]);
        glVertex3dv(quad2[2]);
        glVertex3dv(quad2[3]);
        glVertex3dv(quad2[0]);
        glEnd();

        glBegin(GL_TRIANGLE_FAN);
        glVertex3dv(center);   
        glVertex3dv(quad2[4]);
        glVertex3dv(quad2[5]);
        glVertex3dv(quad2[6]);
        glVertex3dv(quad2[7]);
        glVertex3dv(quad2[4]);
        glEnd();

        GLdouble quad3[4][3] = { {-2,6,0}, {0,0,0}, {2,6,0}, {0,4,0} };

        glEnable(GL_DEPTH_TEST);   
        // 重绘多边形,只绘制模板缓存值非0的像素
        glStencilFunc(GL_NOTEQUAL,0,0x1);
        glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
        //glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE);
        glColorMask(GL_TRUE,GL_TRUE,GL_TRUE,GL_TRUE);        // 重要!!

        // 再绘制一次所有的三角面片
        glBegin(GL_TRIANGLE_FAN);
        glVertex3dv(center);       
        //glVertex3dv(quad1[0]);
        //glVertex3dv(quad1[1]);
        //glVertex3dv(quad1[2]);
        //glVertex3dv(quad1[3]);
        //glVertex3dv(quad1[0]);

        glVertex3dv(quad2[0]);
        glVertex3dv(quad2[1]);
        glVertex3dv(quad2[2]);
        glVertex3dv(quad2[3]);
        glVertex3dv(quad2[0]);
        glEnd();

        glDisable(GL_STENCIL_TEST);
        glEndList();
}
else if(mode == 2)
{// 算法实现 凹多边形凸分解算法

}

}

 

另附 : 判断多边形凹凸性的函数以供参考

// 返回值为true,表示该多边形为凹多边形,否则为凸多边形。  

BOOL  IsConcavePolygon(IGPolygon * poly)
{    //  思路:
    //    把两个边作为向量:进行差积  
    //    然后按下列规则判断:  
    //    如果全部大于等于零则是凸多边形。  
    //    如果全为零,则是所有边共线;  
    //    小于零则表明是凹多边形;  
    if (!poly)
        return FALSE;

    long pathcount;
    GLdouble coord[3];
    poly->get_PathCount(&pathcount);
    IGPath *path = NULL;
    if (pathcount < 1)
    {// 至少要有一条曲线
        return FALSE;
    }
    else if (pathcount < 2)
    {// 一条曲线围成的多边形
        std::vector<GLdouble> nodeArray;
        for (long i = 0; i < pathcount; i++)
        {// 闭合曲线数量==1
            poly->GetPath(i,&path);           

            long nodecount;
            path->get_NodeCount(&nodecount);
            if (nodecount < 3)// 不足3个点无法组成面
                continue;

            IGNode *node = NULL;           
            for (long j = 0; j < nodecount; j++)
            {// 曲线上节点数
                path->GetNode(j,&node);
                if (!node) continue;

                node->get_X(&coord[0]);
                node->get_Y(&coord[1]);
                node->get_Z(&coord[2]);               

                nodeArray.push_back(coord[0]);
                nodeArray.push_back(coord[1]);
                nodeArray.push_back(coord[2]);

                node->Release();
            }       
        }

        // 起点和终点要重合(系统做多边形时已经将其首尾坐标重合了)
        //nodeArray.push_back(nodeArray.at(0));
        //nodeArray.push_back(nodeArray.at(1));
        //nodeArray.push_back(nodeArray.at(2));

        // 判断凹凸性
        int node_size = nodeArray.size() / 3;
        for (int i = 0; i < node_size-1; i++)
        {
            GLdouble res = Dot(nodeArray[i*3],nodeArray[i*3+1],nodeArray[i*3+2],
                nodeArray[(i+1)*3],nodeArray[(i+1)*3]+1,nodeArray[(i+1)*3]+2);
            if (res < 0) // 凹多边形
            {
                nodeArray.clear();
                return TRUE;
            }
        }
        nodeArray.clear();
    }
    else
    {// 多条曲线围成的多边形 - 还有问题           
        // 思路: 第一重判断 : 组成多边形的的闭合曲线中任意一条围成的是凹多边形,则为凹多边形
        // 第二重判断 : (简单判断,并不准确,但速度快) 判断每条曲线的范围矩形是否相交或包含,是则为凹多边形,否则为凸多边形
        // 多条闭合曲线组成凹多边形两两不想交这种情况存在吗? 也许只有飞地适合这种情况如:本土和飞地不直接相连
   

        // 暂时认为凡是由多条曲线组成的多边形都是凹多边形 2010.5.18 ml
        return TRUE;
    }
    // 凸多边形   
    return FALSE;
}

GLdouble Dot(GLdouble x1,GLdouble y1,GLdouble z1,GLdouble x2,GLdouble y2,GLdouble z2)
{
    GLdouble res = x1*x2 + y1*y2 + z1*z2;
    return res;
}

 

源代码二:(跟上面的有些不同)

///////////////////////////////////////////////////////////////////////////////
// draw a simple concave quad
///////////////////////////////////////////////////////////////////////////////
void draw1()
{
    // define concave quad data (vertices only)
    //  0    2
    //  \ \/ /
    //   \3 /
    //    \/
    //    1
    GLdouble quad1[4][3] = { {-1,3,0}, {0,0,0}, {1,3,0}, {0,2,0} };

    // We are going to do 2-pass draw: draw to stencil buffer first,
    // then draw to color buffer.
    glEnable(GL_STENCIL_TEST);          // enable stencil test

    // PASS 1: draw to stencil buffer only
    // The reference value will be written to the stencil buffer plane if test passed
    // The stencil buffer is initially set to all 0s.
    glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); // disable writing to color buffer
    glStencilFunc(GL_ALWAYS, 0x1, 0x1);
    glStencilOp(GL_KEEP, GL_INVERT, GL_INVERT);   // 重要

    glBegin(GL_TRIANGLE_FAN);
        glVertex3dv(quad1[0]);
        glVertex3dv(quad1[1]);
        glVertex3dv(quad1[2]);
        glVertex3dv(quad1[3]);
    glEnd();

    // PASS 2: draw color buffer
    // Draw again the exact same polygon to color buffer where the stencil
    // value is only odd number(1). The even(0) area will be descarded.
    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);    // enable writing to color buffer
    glStencilFunc(GL_EQUAL, 0x1, 0x1);                  // test if it is odd(1) 重要
    glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
    glColor3f(1,1,1);
    glBegin(GL_TRIANGLE_FAN);
        glVertex3dv(quad1[0]);
        glVertex3dv(quad1[1]);
        glVertex3dv(quad1[2]);
        glVertex3dv(quad1[3]);
    glEnd();

    glDisable(GL_STENCIL_TEST);
}

///////////////////////////////////////////////////////////////////////////////
// draw a polygon with a hole
///////////////////////////////////////////////////////////////////////////////
void draw2()
{
    // define concave quad with a hole
    //  0--------3
    //  | 4----7 |
    //  | |    | |
    //  | 5----6 |
    //  1--------2
    GLdouble quad2[8][3] = { {-2,3,0}, {-2,0,0}, {2,0,0}, { 2,3,0},
                             {-1,2,0}, {-1,1,0}, {1,1,0}, { 1,2,0} };

    glEnable(GL_STENCIL_TEST);          // enable stencil test

    // PASS 1: draw to stencil buffer only
    glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); // disable writing to color buffer
    glStencilFunc(GL_ALWAYS, 0x1, 0x1);
    glStencilOp(GL_KEEP, GL_INVERT, GL_INVERT);

    // outer contour
    glBegin(GL_TRIANGLE_FAN);
        glVertex3dv(quad2[0]);
        glVertex3dv(quad2[1]);
        glVertex3dv(quad2[2]);
        glVertex3dv(quad2[3]);
    glEnd();
    //inner contour
    glBegin(GL_TRIANGLE_FAN);
        glVertex3dv(quad2[4]);
        glVertex3dv(quad2[5]);
        glVertex3dv(quad2[6]);
        glVertex3dv(quad2[7]);
    glEnd();

    // PASS 2: draw color buffer
    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);    // enable writing to color buffer
    glStencilFunc(GL_EQUAL, 0x1, 0x1);                  // test if it is odd(1)
    glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
    glColor3f(1,1,1);

    // outer contour
    glBegin(GL_TRIANGLE_FAN);
        glVertex3dv(quad2[0]);
        glVertex3dv(quad2[1]);
        glVertex3dv(quad2[2]);
        glVertex3dv(quad2[3]);
    glEnd();
    //inner contour
    glBegin(GL_TRIANGLE_FAN);
        glVertex3dv(quad2[4]);
        glVertex3dv(quad2[5]);
        glVertex3dv(quad2[6]);
        glVertex3dv(quad2[7]);
    glEnd();
}

///////////////////////////////////////////////////////////////////////////////
// draw a self-intersecting polygon (star)
///////////////////////////////////////////////////////////////////////////////
void draw3()
{
    // define self-intersecting star shape (with color)
    //      0
    //     / \
    //3---+---+---2
    //  \ |   | /
    //   \|   |/
    //    +   +
    //    |\ /|
    //    | + |
    //    |/ \|
    //    1   4
    GLdouble star[5][6] = { { 0.0, 3.0, 0,  1, 0, 0},       // 0: x,y,z,r,g,b
                            {-1.0, 0.0, 0,  0, 1, 0},       // 1:
                            { 1.6, 1.9, 0,  1, 0, 1},       // 2:
                            {-1.6, 1.9, 0,  1, 1, 0},       // 3:
                            { 1.0, 0.0, 0,  0, 0, 1} };     // 4:

    glEnable(GL_STENCIL_TEST);          // enable stencil test

    // PASS 1: draw to stencil buffer only
    glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); // disable writing to color buffer
    glStencilFunc(GL_ALWAYS, 0x1, 0x1);
    glStencilOp(GL_KEEP, GL_INVERT, GL_INVERT);

    glBegin(GL_TRIANGLE_FAN);
        glColor3dv(star[0]+3);
        glVertex3dv(star[0]);
        glColor3dv(star[1]+3);
        glVertex3dv(star[1]);
        glColor3dv(star[2]+3);
        glVertex3dv(star[2]);
        glColor3dv(star[3]+3);
        glVertex3dv(star[3]);
        glColor3dv(star[4]+3);
        glVertex3dv(star[4]);
    glEnd();

    // PASS 2: draw color buffer
    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);    // enable writing to color buffer
    glStencilFunc(GL_EQUAL, 0x1, 0x1);                  // test if it is odd(1)
    glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
    glColor3f(1,1,1);
    glDisable(GL_DEPTH_TEST);

    glBegin(GL_TRIANGLE_FAN);
        glColor3dv(star[0]+3);
        glVertex3dv(star[0]);
        glColor3dv(star[1]+3);
        glVertex3dv(star[1]);
        glColor3dv(star[2]+3);
        glVertex3dv(star[2]);
        glColor3dv(star[3]+3);
        glVertex3dv(star[3]);
        glColor3dv(star[4]+3);
        glVertex3dv(star[4]);
    glEnd();

    glEnable(GL_DEPTH_TEST);

}

posted on 2010-05-18 17:49  3D入魔  阅读(15029)  评论(1编辑  收藏  举报