如何在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
相关产品推荐
相关产品推荐

