You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在OpenGL中为非连续形状指定顶点法线以实现阴影效果?

问题描述

我用GL_TRIANGLES创建了带盖圆柱(茶杯),希望它能产生类似glutSolidSphere的阴影效果。我知道需要设置顶点法线才能实现预期的阴影和反射效果,但不确定具体操作方法(代码是大学作业结构,相对复杂)。目前茶杯完全不生成阴影,期望它能像桌上的茶壶和球体一样投射阴影、产生反射。

void drawTeaCup(void) {
    GLfloat matAmbient[] = { 0.8, 0.8, 0.8, 1.0 };
    GLfloat matDiffuse[] = { 0.8, 0.8, 0.8, 1.0 };
    GLfloat matSpecular[] = { 1.0, 1.0, 1.0, 1.0 };
    GLfloat matShininess[] = { 128.0 };
    glMaterialfv( GL_FRONT_AND_BACK, GL_AMBIENT, matAmbient );
    glMaterialfv( GL_FRONT_AND_BACK, GL_DIFFUSE, matDiffuse );
    glMaterialfv( GL_FRONT_AND_BACK, GL_SPECULAR, matSpecular );
    glMaterialfv( GL_FRONT_AND_BACK, GL_SHININESS, matShininess );

    glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE );
    glBindTexture( GL_TEXTURE_2D, spotsTexObj );

    glDisable( GL_CULL_FACE );  // Disable back-face culling.

    glPushMatrix();
    glTranslated(0.5, -0.3, TABLETOP_Z);
    drawCupVertices();
    glPopMatrix();

    glEnable( GL_CULL_FACE );  // Enable back-face culling.
}

void drawCupVertices()
{
    drawCylinderFan(0.35, 0.33, 0.36, 0.36); // Lid side
    drawDisc(0.36, 0.35); // Lid top
    drawCylinderFan(0.35, 0.2, 0.35, 0.25); // Top half of cup
    drawCylinderFan(0.2, 0, 0.25, 0.15); // Bottom half of cup
}

void drawDisc(double size, double height)
{
    std::array<std::array<double, 2>, 19> circle = getCircleTemplate();
    glBegin(GL_TRIANGLE_FAN);
    for (std::array<double, 2> pt: circle) {
        double vertex[] = {pt[0] * size, pt[1] * size, height};
        glVertex3dv(vertex);
    }
    glEnd();
}

void drawCylinderFan(double high, double low, double highFrac, double lowFrac)
{
    std::array<std::array<double, 3>, 38> fan = getCylinderFan(high, low, highFrac, lowFrac);
    
    glBegin(GL_TRIANGLE_STRIP);
    for (int i = fan.size() - 1; i >= 0; i--) {
        std::array<double, 3> pt = fan[i];
        double vertex[] = {pt[0], pt[1], pt[2]};
        glVertex3dv(vertex);
    }
    glEnd();
}

std::array<std::array<double, 3>, 38> getCylinderFan(double high, double low, double highFrac, double lowFrac)
{
    std::array<std::array<double, 2>, 19> circle = getCircleTemplate();
    std::array<std::array<double, 3>, 38> fan;
    
    for (int i = 0; i < 38; i++) {
        if (i % 2) {
            fan[i] = { circle[i / 2][0] * lowFrac, circle[i / 2][1] * lowFrac, low };
        } else {
            fan[i] = { circle[i / 2][0] * highFrac, circle[i / 2][1] * highFrac, high };
        }
    }

    return fan;
}


std::array<std::array<double, 2>, 19> getCircleTemplate( void )
{
    static bool firstTime = true;
    int NUM_OF_SIDES = 18;
    static std::array<std::array<double, 2>, 19> unitDiscVertex;

    if ( firstTime )
    {
        // Pre-compute and store the vertices' positions of a unit-radius disc.
        firstTime = false;
        
        double theta = 0;
        double deltaTheta = (2 * PI / NUM_OF_SIDES);
        
        for (int i = 0; i < NUM_OF_SIDES + 1; i++) {
            double nextX = cos(theta);
            double nextY = sin(theta);

            unitDiscVertex[i][0] = nextX;
            unitDiscVertex[i][1] = nextY;
            
            theta += deltaTheta;
        }

    }

    return unitDiscVertex;
}
解决方案

OpenGL的光照和阴影计算靠顶点法线判断表面朝向,你得在绘制每个顶点前调用glNormal3dv()或glNormal3f()设置对应的法线向量,下面是具体修改步骤:

1. 给圆盘(盖子顶部)添加法线

圆盘是平面,所有顶点的法线方向一致:盖子顶部的法线沿Z轴正方向,要是加杯底的话法线就沿Z轴负方向。修改drawDisc函数:

void drawDisc(double size, double height, bool isTop) { // 添加参数区分顶部/底部
    std::array<std::array<double, 2>, 19> circle = getCircleTemplate();
    // 定义法线:顶部朝上,底部朝下
    double normal[] = {0.0, 0.0, isTop ? 1.0 : -1.0};
    glBegin(GL_TRIANGLE_FAN);
    glNormal3dv(normal); // 所有顶点共用同一法线
    for (std::array<double, 2> pt: circle) {
        double vertex[] = {pt[0] * size, pt[1] * size, height};
        glVertex3dv(vertex);
    }
    glEnd();
}

然后修改drawCupVertices里的调用:

drawDisc(0.36, 0.35, true); // 盖子顶部,法线朝上
// 要是需要画杯底,就这么调用:drawDisc(0.15, 0, false);

2. 给圆柱侧面添加法线

圆柱侧面的法线是径向的(从Z轴指向顶点),得根据每个顶点的X/Y坐标计算并归一化。修改drawCylinderFan函数:

void drawCylinderFan(double high, double low, double highFrac, double lowFrac)
{
    std::array<std::array<double, 3>, 38> fan = getCylinderFan(high, low, highFrac, lowFrac);
    
    glBegin(GL_TRIANGLE_STRIP);
    for (int i = fan.size() - 1; i >= 0; i--) {
        std::array<double, 3> pt = fan[i];
        // 计算径向法线:X/Y和顶点一致,Z为0,然后归一化
        double nx = pt[0];
        double ny = pt[1];
        double nz = 0.0;
        // 归一化避免长度影响光照
        double length = sqrt(nx*nx + ny*ny + nz*nz);
        if (length > 0.0001) { // 防止除以0
            nx /= length;
            ny /= length;
        }
        double normal[] = {nx, ny, nz};
        glNormal3dv(normal);
        
        double vertex[] = {pt[0], pt[1], pt[2]};
        glVertex3dv(vertex);
    }
    glEnd();
}

额外提醒

  • 得确保你已经启用了光照:glEnable(GL_LIGHTING)和对应的光源(比如glEnable(GL_LIGHT0)),没开光照的话法线等于白加。
  • 如果是用OpenGL的阴影体积或纹理阴影,还要保证茶杯被纳入阴影计算的几何体内,但先把法线弄对让光照正常,阴影才会显示出来。

内容的提问来源于stack exchange,提问作者dragonoider

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 15:19:50