OpenGL 中的光源特性和相关材质属性





5.00/5 (3投票s)
OpenGL 的基本光源方法和相关材质属性处理
引言
我想编写一个简单的基于 OpenGL 的 X3DOM 查看器,并在网上搜索了有关如何应用光源和材质属性的信息。我找到很多很好的论坛帖子,但很少有“总体原则”的描述——其中一个比较好的就是 OpenGL Wiki 的文章 光照如何工作。在这篇技巧中,我想与所有正在寻找光照和材质主题入门知识的人分享我的发现。所以请不要期望以下内容是“高深莫测”的,而仅仅是“总体原则”的描述和一些“即用型”的代码片段,它们考虑了光源和材质属性之间的关系。
我写的另一篇技巧文章 "OpenGL 的几何图元" 也与简单的 X3DOM 查看器有关。这篇技巧文章使用了其中介绍的算法来创建几何图元。
背景
X3DOM 规范支持 Material
对象,因此我需要一种设置光照的方式,以便能够成功应用大部分 Material
属性到对象上。
我对于 OpenGL 还不算非常熟悉,因此对我来说,“总体原则”和“即用型”的代码片段易于理解和遵循非常重要。(我的一个来源是 OpenGL Wiki 的文章 光照如何工作,第二个是 OpenGL 编程指南。)
我对于光照特性和材质属性主题最重要的要求之一是结果的可追溯性。所以我决定
- 在场景中包含一个代表光源的点(我使用定向光源,它完美地模拟了平行光束),
- 让光源在场景中非轴平行地旋转(这会让所有对象都经历一次明亮的照明和一次黑暗的照明),
- 使空间坐标轴可见(这大大有助于纠正错误),以及
- 采用非轴平行的观察位置(这增强了场景的空间感)。
以下图片显示了非轴平行旋转的光源,以及对象照明的相应变化。如果您比较第一张图片和第二张图片,您会清楚地看到第二张图片中空间光引入的反射——尤其是在圆锥体上。如果您比较第一张图片和第三张图片,您会清楚地看到被照亮的表面和处于阴影中的表面之间的区别。
对于非轴平行的观察位置,GL_PROJECTION
矩阵绕 X 轴和 Y 轴各倾斜了 15 度,导致 X 轴(红色)和 Z 轴(蓝色)的空间坐标轴与屏幕坐标不平行。
Using the Code
世界准备
通过 fLightSourceRotationAngle
实现的旋转光源需要某种场景动画,我将其实现为一种空闲时的进程 WindowProcOnIdle()
void X3DomLightMainFrame::WindowProcOnIdle(const HWND hWnd)
{
static float fLightSourceRotationAngle = 0.0F;
static Vector3f oSceneRotationVector = { 1.0F, 1.0F, 0.0F };
RECT rc = _pweakMainContent->GetClientRect();
LONG width = rc.right - rc.left;
LONG height = rc.bottom - rc.top;
if (_viewportSize.cx != width ||
_viewportSize.cy != height ||
_oWorld.GetProjectionDirty() == true)
{
OpenGL::ResetViewport((GLsizei)width, (GLsizei)height,
_oWorld.GetOrthographicProjection(), 10.0F / _oWorld.GetZoom(),
10.0F / _oWorld.GetZoom(), -100.0F, 100.0F);
_viewportSize.cx = width;
_viewportSize.cy = height;
_oWorld.SetProjectionDirty(false);
OpenGL::ShadeModel(GL_SMOOTH); // alternative is GL_FLAT
OpenGL::Enable(GL_CULL_FACE); // enable default hidden face culling: GL_BACK
OpenGL::Enable(GL_DEPTH_TEST); // perform depth comparisons and update the depth buffer
OpenGL::DepthMask(GL_TRUE); // ensure depth buffer can be written to
}
OpenGL::ClearColor(0.0F, 0.0F, 0.0F, 0.0F);
OpenGL::Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// --- BEGIN: Provide unchanged start conditions from frame to frame.
OpenGL::PushMatrix();
// --- END: Provide unchanged start conditions from frame to frame.
...
ResetViewport()
中隐藏了一些魔法——有关详细信息,请参阅本技巧文章中的最后一个代码片段。此方法提供了一个统一的调用,用于创建正交投影和透视投影(由 _oWorld.GetOrthographicProjection()
决定),以及缩放功能(由 _oWorld.GetZoom()
决定)。
光照
我简单的 X3DOM 查看器应该能够像上面四张图片中所示的那样,在单独的光照(应用光源)和基本/默认光照之间切换,后者如图所示
光照模型的确定由 _oWorld.GetUseIndividualLighting()
实现。现在我们可以引入存储在 _oWorld.CountLightSources()
中的光源。OpenGL 默认支持八个光源。
---
if (_oWorld.GetUseIndividualLighting() == true)
{
OpenGL::Enable(GL_LIGHTING);
for (int iLigthSourceCnt = 0;
iLigthSourceCnt < _oWorld.CountLightSources() && iLigthSourceCnt < 8;
iLigthSourceCnt++)
{
switch (iLigthSourceCnt)
{
case 0:
OpenGL::Enable(GL_LIGHT0);
break;
case 1:
OpenGL::Enable(GL_LIGHT1);
break;
case 2:
OpenGL::Enable(GL_LIGHT2);
break;
case 3:
OpenGL::Enable(GL_LIGHT3);
break;
case 4:
OpenGL::Enable(GL_LIGHT4);
break;
case 5:
OpenGL::Enable(GL_LIGHT5);
break;
case 6:
OpenGL::Enable(GL_LIGHT6);
break;
case 7:
OpenGL::Enable(GL_LIGHT7);
break;
default:
break;
}
LightSource* pLightSource = _oWorld.GetLightSource(iLigthSourceCnt);
OpenGL::PushMatrix();
OpenGL::Rotatef(+fLightSourceRotationAngle, oSceneRotationVector.x,
oSceneRotationVector.y, oSceneRotationVector.z);
Vector4f oLightVector = Vector4f{ pLightSource->GetLightSourcePosition(), 0.001F };
OpenGL::Lightfv(GL_LIGHT0, GL_POSITION, oLightVector.Get());
OpenGL::PopMatrix();
// Light ambient reflection.
SFColor oAmbientLightColor = pLightSource->GetAmbientLightColor();
GLfloat pfAmbientLightColor[] = { oAmbientLightColor.r, oAmbientLightColor.g,
oAmbientLightColor.b, 1.0F };
OpenGL::Lightfv(GL_LIGHT0, GL_AMBIENT, pfAmbientLightColor);
// Light diffuse reflection.
SFColor oDiffuseLightColor = pLightSource->GetDiffuseLightColor();
GLfloat pfDiffuseLightColor[] = { oDiffuseLightColor.r, oDiffuseLightColor.g,
oDiffuseLightColor.b, 1.0F };
OpenGL::Lightfv(GL_LIGHT0, GL_DIFFUSE, pfDiffuseLightColor);
// Light mirror reflection.
SFColor oSpecularLightColor = pLightSource->GetSpecularLightColor();
GLfloat pfSpecularLightColor[] = { oSpecularLightColor.r, oSpecularLightColor.g,
oSpecularLightColor.b, 1.0F };
OpenGL::Lightfv(GL_LIGHT0, GL_SPECULAR, pfSpecularLightColor);
}
// Set affected face (front, back) and the relevant subset (ambient, diffuse,
// emission, specular) of lights to enable a material to track the current color.
OpenGL::ColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
// Switch on the capability of a material to track the current color.
OpenGL::Enable(GL_COLOR_MATERIAL);
}
else
{
OpenGL::Disable(GL_LIGHT0);
OpenGL::Disable(GL_LIGHT1);
OpenGL::Disable(GL_LIGHT2);
OpenGL::Disable(GL_LIGHT3);
OpenGL::Disable(GL_LIGHT4);
OpenGL::Disable(GL_LIGHT5);
OpenGL::Disable(GL_LIGHT6);
OpenGL::Disable(GL_LIGHT7);
OpenGL::Disable(GL_LIGHTING);
}
...
可视化坐标系
接下来是坐标系的可视化。为了方便区分,坐标的颜色分配(x=红色,y=绿色,z=蓝色)根据颜色向量和坐标向量的顺序进行。我已经在我关于 "OpenGL 的几何图元" 的技巧文章中介绍了 OpenGL::DrawCylinder()
和 OpenGL::DrawCone()
方法——其余都是 OpenGL 标准。
...
// --- Mark the coordinate system.
OpenGL::PushMatrix();
OpenGL::Color3f(1.0F, 0.5F, 0.5F);
OpenGL::Rotatef(-90, 0.0F, 0.0F, 1.0F);
OpenGL::DrawCylinder(0.1F, 6.0F, 0.1F, M_PI / 90.0);
OpenGL::Translatef(0.0F, 6.0F, 0.0F);
OpenGL::DrawCone(0.25F, 0.4F, 0.25F, M_PI / 90);
OpenGL::PopMatrix();
OpenGL::PushMatrix();
OpenGL::Color3f(0.5F, 1.0F, 0.5F);
OpenGL::DrawCylinder(0.1F, 6.0F, 0.1F, M_PI / 90.0);
OpenGL::Translatef(0.0F, 6.0F, 0.0F);
OpenGL::DrawCone(0.25F, 0.4F, 0.25F, M_PI / 90);
OpenGL::PopMatrix();
OpenGL::PushMatrix();
OpenGL::Color3f(0.5F, 0.5F, 1.0F);
OpenGL::Rotatef(90, 1.0F, 0.0F, 0.0F);
OpenGL::DrawCylinder(0.1F, 6.0F, 0.1F, M_PI / 90.0);
OpenGL::Translatef(0.0F, 6.0F, 0.0F);
OpenGL::DrawCone(0.25F, 0.4F, 0.25F, M_PI / 90);
OpenGL::PopMatrix();
...
可视化旋转光源
光源由一个抽象的聚光灯表示,由一个圆柱体和一个圆锥体组成。圆柱体使用不同的颜色(顶部、盖子和底部)COLORREF oCylinderColors[] = { 0xFF444444, 0xFFAAAAAA, 0xFF444444 };
。为了让圆锥体看起来像一个光锥,它必须自己发光。为此,圆锥体的材质属性被设置为自发光 GLfloat oEmissionColors1[] = { 0.8F, 0.8F, 0.6F, 1.0F };
,然后重置 GLfloat oEmissionColors2[] = { 0.0F, 0.0F, 0.0F, 1.0F };
。为了获得逼真的光锥效果,为盖子和底部使用了不同的颜色 COLORREF oConeColors[] = { 0xFF668888, 0xFFEEFFFF };
。
...
// --- Mark the light source.
if (_oWorld.GetUseIndividualLighting() == true)
{
for (int iLigthSourceCnt = 0;
iLigthSourceCnt < _oWorld.CountLightSources() && iLigthSourceCnt < 8;
iLigthSourceCnt++)
{
LightSource* pLightSource = _oWorld.GetLightSource(iLigthSourceCnt);
OpenGL::PushMatrix();
OpenGL::Rotatef(+fLightSourceRotationAngle, oSceneRotationVector.x,
oSceneRotationVector.y, oSceneRotationVector.z);
OpenGL::Translatef(pLightSource->GetLightSourcePosition());
// Direct to coordinate center.
OpenGL::Rotatef(+45.0F, 0.0F, 0.0F, 1.0F);
OpenGL::Rotatef(-45.0F, 1.0F, 0.0F, 0.0F);
COLORREF oCylinderColors[] = { 0xFF444444, 0xFFAAAAAA, 0xFF444444 };
OpenGL::DrawCylinder(0.2F, 0.3F, 0.2F, M_2PI / 20.0, oCylinderColors, false);
OpenGL::Translatef(0.0F, -0.1F, 0.0F);
OpenGL::Color3f(0.9F, 0.9F, 0.8F);
GLfloat oEmissionColors1[] = { 0.8F, 0.8F, 0.6F, 1.0F };
OpenGL::Materialfv(GL_FRONT_AND_BACK, GL_EMISSION, oEmissionColors1);
COLORREF oConeColors[] = { 0xFF668888, 0xFFEEFFFF };
OpenGL::DrawCone(0.4F, 0.4F, 0.4F, M_PI / 20.0, oConeColors);
GLfloat oEmissionColors2[] = { 0.0F, 0.0F, 0.0F, 1.0F };
OpenGL::Materialfv(GL_FRONT_AND_BACK, GL_EMISSION, oEmissionColors2);
OpenGL::PopMatrix();
}
}
渲染场景
显示的物体,圆锥体、盒子和球体,都在场景中,并通过调用 _pScene->RenderToOpenGL()
进行渲染。这背后没有魔术——任何场景都可以在此处渲染。动画通过递增 fLightSourceRotationAngle
和短暂的暂停 MainFrame::Sleep(30)
来完成,对于 ReactOS,这个暂停时间稍短。
...
// --- Scene rendering code goes here.
_pScene->RenderToOpenGL(NULL, _pweakMainContent->GetOpenGlHDC());
// --- Begin: Restore unchanged start conditions from frame to frame.
OpenGL::PopMatrix();
// --- END: Restore unchanged start conditions from frame to frame.
::SwapBuffers(_pweakMainContent->GetOpenGlHDC());
fLightSourceRotationAngle += 1.0F;
#if defined(__GNUC__) || defined(__MINGW32__)
MainFrame::Sleep(30);
#else
MainFrame::Sleep(15);
#endif
// Keep the idle part of the message loop running.
::PostMessage(GetHWnd(), WM_NULL, (WPARAM)0, (LPARAM)0);
}
视口准备
从第一个代码片段中,我还有 OpenGL::ResetViewport()
方法的源代码——在这里
/// <summary>
/// Prepares an orthogonal projection.
/// </summary>
/// <param name="nWidth">The view-port width in pixels.</param>
/// <param name="nHeight">The view-port height in pixels.</param>
/// <param name="bInitOrthoProjection">Determine whether to fall back to
/// perspective projection or to initialize orthographic projection.</param>
/// <param name="dHorzClip">The left/right (we are symmetric) distance of
/// the clipping plane from the camera vector.</param>
/// <param name="dVertClip">The top/bottom (we are symmetric) distance of
/// the clipping plane from the camera vector.</param>
/// <param name="dNearZClip">The orthogonal view near clip plane. Can be negative.
/// Typically too big, if nothing is visible. Can realize a view 'into' the objects.</param>
/// <param name="dFarZClip">The orthogonal view far clip plane.
/// Can cut off the far background (for speed or focus on important things).</param>
/// <remarks>Recommended reading: https://sjbaker.org/steve/omniv/projection_abuse.html
static inline void ResetViewport(GLsizei nWidth, GLsizei nHeight, bool bInitOrthoProjection,
GLdouble dHorzClip, GLdouble dVertClip, GLdouble dNearZClip,
GLdouble dFarZClip)
{
GLdouble fAspect = ((GLdouble)nWidth) / (nHeight != 0 ? nHeight : 1);
::glViewport(0, 0, nWidth, nHeight);
if (bInitOrthoProjection)
{
::glMatrixMode(GL_PROJECTION);
::glLoadIdentity();
// Set up an orthographic projection.
::glOrtho(-dHorzClip, dHorzClip, -dVertClip, dVertClip, dNearZClip, dFarZClip);
// Tilt scene for 3D/volume effect.
::glRotatef(+15.0F, 1.0F, 0.0F, 0.0F);
::glRotatef(-15.0F, 0.0F, 1.0F, 0.0F);
::glMatrixMode(GL_MODELVIEW);
}
else
{
::glMatrixMode(GL_PROJECTION);
::glLoadIdentity();
// EITHER THIS WAY: Set up an perspective projection with the appropriate aspect ratio.
//::gluPerspective(45.0, fAspect, 0.1, 100.0);
// OR THIS WAY: Set up an perspective projection with clip planes.
::glFrustum(-dHorzClip * 0.01, dHorzClip * 0.01, -dVertClip * 0.01, dVertClip * 0.01,
1.0F, 1.0 + dFarZClip);
::glTranslatef(0.0F, 0.0F, (float)dNearZClip);
// Tilt scene for 3D/volume effect.
::glRotatef(+15.0F, 1.0F, 0.0F, 0.0F);
::glRotatef(-15.0F, 0.0F, 1.0F, 0.0F);
::glMatrixMode(GL_MODELVIEW);
}
}
就是这样。祝您在 OpenGL 中玩得开心!
关注点
尽管每个单独的 OpenGL 光照和材质函数都描述得很清楚——组织它们的正确交互需要一些反复试验。
历史
- 2021 年 4 月 16 日:初始技巧