Chapter 6. OpenGL rendering

6.1. VRML lights rendering

6.1.1. Lighting model

When rendering using the OpenGL we try to get results as close as possible to the VRML 2.0 lighting equations and X3D lighting equations. We set OpenGL lights and materials properties to achieve the required look.

Note that there are bits when it is not possible to exactly match VRML 2.0 / X3D requirements with fixed-function rendering:

  1. VRML 2.0 / X3D specify the spot light falloff by a beamWidth field. This cannot be precisely translated to a standard OpenGL spotlight exponent.

    Let α be the angle between the spot light's direction and the ray from spot light's position to the considered geometry point.

    • OpenGL spot light uses cosinus drop-off, which means that the light intensity within the spot cutOffAngle is calculated as a Cos(α)spotExponent.

    • VRML 2.0 / X3D have a beamWidth. When α < beamWidth, the light intensity is constant (1.0). For larger angles, the intensity is linearly interpolated (down to 0.0) until angle reaches cutOffAngle.

    There is just no sensible translation from beamWidth idea to OpenGL spotExponent.

    An exception is the case when beamWidth >= cutOffAngle. Then spot has constant intensity, which has be accurately expressed with GL_SPOT_EXPONENT = 0. Fortunately, this is the default situation for all spot lights.

    We have considered an extension to define SpotLight.dropOffRate as an extension for VRML >= 2.0 lights. With definition like default value of dropOffRate = -1 means to try to approximate beamWidth, otherwise dropOffRate is used as an exponent. But it didn't prove useful enough, especially since it would be our own extension.

    Looking at how other VRML/X3D implementations handle this:

    • Seems that most of them ignore the issue, leaving spot exponent always 0 and ignoring beamWidth entirely.

    • One implementation http://arteclab.artec.uni-bremen.de/courses/mixed-reality/material/ARToolkit/ARToolKit2.52vrml/lib/libvrml/libvrml97gl/src/vrml97gl/old_ViewerOpenGL.cpp checks beamWidth < cutOffAngle and sets spot_exponent to 0 or 1. This is what we were doing in engine versions <= 3.0.0.

    • Xj3D (see src/java/org/web3d/vrml/renderer/ogl/nodes/lighting/OGLSpotLight.java) sets GL_SPOT_EXPONENT to 0.5 / beamWidth.

      It's not more precise in any way, the value 0.5 is just a "rule of thumb" as far as we know. But at least it allows to control exponent by beamWidth. This is an important advantage, as you can at least change the drop off rate by changing the beamWidth. Even if beamWidth is not interpreted following the specification, at least it's interpreted somehow, and allows to achieve a range of different effects.

    • FreeWRL (see http://search.cpan.org/src/LUKKA/FreeWRL-0.14/VRMLRend.pm, freewrl-1.22.13/src/lib/scenegraph/Component_Lighting.c in later version) uses approach similar to Xj3D, setting GL_SPOT_EXPONENT to 0.5 / (beamWidth + 0.1).

      For example, this results in

      • beamWidth = 0 => GL_SPOT_EXPONENT = 5

      • beamWidth = Pi/4 => GL_SPOT_EXPONENT =~ 0.5 / 0.9 =~ 1/2

      • beamWidth = Pi/2 => GL_SPOT_EXPONENT =~ 0.5 / 1.67 =~ 1/3

      It's similar to Xj3D, and the +0.1 seems to be just to prevent division by (something close to) zero in case beamWidth is very very small. Unfortunately, this addition also limits the possible values of GL_SPOT_EXPONENT: it's at most 5 (0.5 / 0.1 = 5, as beamWidth must be > 0), and sometimes larger values would be useful.

    • In our engine current version, we do it like this:

      • If beamWidth >= cutOffAngle, then GL_SPOT_EXPONENT is 0.

      • Otherwise we follow Xj3D version: GL_SPOT_EXPONENT is 0.5 / max(beamWidth, epsilon)

      If you want to convert VRML 1.0 dropOffRate to VRML 2.0 / X3D beamWidth precisely:

      • If dropOffRate = 0, then leave beamWidth at default Pi/2. This makes beamWidth >= cutOffAngle (because cutOffAngle must be <= Pi/2 according to spec), which means no smooth falloff.

      • Otherwise beamWidth := 0.5 / (128 * dropOffRate) = 1 / (256 * dropOffRate).

  2. The exponential fog of VRML 2.0 also uses different equations than OpenGL exponential fog and cannot be matched perfectly. See VRML and OpenGL specifications for details.

  3. Fixed-function renderer uses Gouraud shading, with it's limitations.

Shader pipeline overcomes above problems. We program spot falloff ourselves in GLSL, honoring beamWidth correctly. We also do per-pixel lighting calculation (Phong shading). See lighting.

You can also use classic ray-tracer of our engine to see the correct lighting.

6.1.2. Rendering lights

VRML/X3D lights are translated to the appropriate OpenGL calls using the TGLRendererLights class. This is used internally by the TGLRenderer discussed in next sections. For now if you implement custom OpenGL rendering of 3D stuff, for have to also implement custom handling of OpenGL lights. (This is scheduled to be improved in engine 2.6.0, by making an instance of TGLRendererLights more widely available.)

When you render 3D models using our engine classes, like TCastleScene, everything related to lights is automatically taken care of. All lights (including the headlight, see https://castle-engine.io/x3d_extensions.php#section_ext_headlight) can be described and animated inside the VRML/X3D model. Programmers can also control lights by code. Some useful things are TCastleSceneCore.HeadlightOn and TCastleSceneCore.CustomHeadlight to control the headlight of given scene. You can also control headlight globally by overriding the viewport and scene manager method TCastleAbstractViewport.Headlight. TCastleSceneCore.Attributes.UseSceneLights controls normal scene lights.

To use main scene lights on other 3D objects as well, you have a comfortable TCastleAbstractViewport.UseGlobalLights. You can also override TCastleAbstractViewport.InitializeLights. For example, in games you may want to render various 3D things: for example you have one mostly static 3D model for level and various creature models. And it may be desirable to use level lights for everything. Using TCastleAbstractViewport.UseGlobalLights = true does this for you.

I use this technique in my games. For example see The Castle levels.