OpenGL super classic notes - depth texture and shadow

Previously, we introduced the simple method of flattening objects to the projection plane to create shadows. However, this shadow method has its limitations (for example, the projection plane must be a plane). In opengl1 4 introduces a new method shadow map to generate shadows.

The principle behind shadow mapping is simple. We first take the position of the light source as the position of the camera. When we observe the object from this position, we will know which surfaces of the object are illuminated (seen by the light source) and which are not illuminated (blocked) (the surface closest to the light source in a certain direction is illuminated, and the rear surface is not illuminated). We start the depth test so that we can get a useful depth buffer data (the result of each pixel in the depth buffer). Then we read the data from the depth buffer as a shadow texture and project it back to the scene. Then we use the camera's perspective to render the object.

Light angle

First, we move the viewing angle to the position of the light source. We can use the auxiliary functions of the glu Library:

gluLookAt(lightPos[0], lightPos[1], lightPos[2], 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);

Set the position of the light source to the observation position.

In order to make the best use of space to produce shadow maps. From the perspective of the light source, the past perspective visual area should adapt to the proportion of the window, and the nearest plane position of the perspective is the plane of the object closest to the light source, and the farthest plane position is the plane of the object farthest from the light source. In this way, we can make full use of the scene information to fill the depth buffer and make shadow maps. We estimate that the field of view of the whole scene is just wrapped.

//Radius size of the scene
GLfloat sceneBoundingRadius = 95.0f;

//Distance of light
lightToSceneDistance = sqrt(lightPos[0] * lightPos[0] +
												lightPos[1] * lightPos[1] +
												lightPos[2] * lightPos[2]);

//near clipping plane 
nearPlane = lightToSceneDistance - sceneBoundingRadius;
//Fill the scene with the entire depth texture
fieldOfView = (GLfloat)m3dRadToDeg(2.0f * atan(sceneBoundingRadius/lightToSceneDistance));

glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(fieldOfView, 1.0f, nearPlane, nearPlane + (2.0f * sceneBoundingRadius));

In the above code, the center of the scene is at the origin, and all objects in the scene are in a circle with the origin as the center and the radius of sceneBoundingRadius. This is our rough estimate of the scene. It is roughly shown in the figure below:

Because we only need to get the result of depth buffer after pixel depth test. Therefore, we can remove all unnecessary details and do not write data to the color buffer, because there is no need to display.

glShadeModel(GL_FLAT);
glDisable(GL_LIGHTING);
glDisable(GL_COLOR_MATERIAL);
glDisable(GL_NORMALIZE);
glColorMask(0,0,0,0);
...

If we can see the depth buffer, the grayscale image of the depth buffer is probably like this.

New texture

We need to copy the depth data into the texture as a shadow map. In opengl1 After 4, glCopyTexImage2D allows us to copy data from the depth buffer. Texture data has a type of depth texture, and its internal format includes GL_DEPTH_COMPONENT16,GL_DEPTH_COMPONENT24,GL_DEPTH_COMPONENT32, the number represents the number of bits contained in each texture unit. In general, we want its internal format to match the precision of the depth buffer. OpenGL allows you to specify a generic GL_DEPTH_COMPONENT format to match your depth buffer. After drawing from the perspective of light source, we copy the data of depth buffer as depth texture:

glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, 0, 0, shadowWidth, shadowHeight, 0);

The depth texture needs to be regenerated only when the object or light source moves. If only the camera moves, we do not need to regenerate the depth texture, because the depth texture does not change from the perspective of the light source. When the size of the window changes, we also need to produce a larger depth texture.

The size of the depth texture

In opengl2 Before 0, we need to adjust the size of the depth texture so that it is exactly the second power when we do not support the extension of the non second power texture (GL_ARB_texture_non_power_of_two). For example, at a resolution of 1024x768, the maximum quadratic texture size is 1024x512

void ChangeSize(int w, int h)
{
	windowWidth = shadowWidth = w;
	windowHeight = shadowHeight  = h;
	//Non quadratic power texture size is not supported
	if(!nptTextureAvailable)
	{
		int i = 0;
		int j = 0;
		//Get the width of the second power
		while((1 << i) <= shadowWidth )
			i++;

		shadowWidth = (1 << (i-1));
		//Height of quadratic power
		while((1 << j) <= shadowHeight )
			j++;

		shadowHeight = (1 << (j-1));
	}

}

First draw shadows

If a shadow is defined as having no light at all, we don't need to draw it. For example, if only a single spotlight is used as the light source, it is enough to make the shadow all black to meet our requirements. If we don't want the shadow to be all black and need some details in the shadow area, we need to simulate some ambient light in the scene. At the same time, we also add some scattered light to help transfer shape information.

GLfloat lowAmbient[4] = {0.1f, 0.1f, 0.1f, 1.0f};
GLfloat lowDiffuse[4] = {0.35f, 0.35f, 0.35f, 1.0f};

glLightfv(GL_LIGHT0, GL_AMBIENT, lowAmbient);
glLightfv(GL_LIGHT0, GL_DIFFUSE, lowDiffuse);

//Draw objects in the scene
DrawModels()

PS: we don't need swap buffers at this time

If it shows up like this.

Some OpenGL implementations support one GL_ARB_shadow_ambient extension, which can eliminate the need for the first shadow drawing.

Then light

At present, we have a very dark scene. To create shadows, I need a bright lighting area to contrast with the shadow area. How to determine the area that receives more light is the key to shadow mapping. In this bright area, we draw with twice the light intensity of the shadow.

GLfloat ambientLight[] = { 0.2f, 0.2f, 0.2f, 1.0f};
GLfloat diffuseLight[] = { 0.7f, 0.7f, 0.7f, 1.0f};
...
glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight);
glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight);

 

The resulting shadows are not all black.

 

If you remove the previous drawn shadow, the result is:

 

Shadow map

Our goal is to project the shadow map into the scene (from the position of the camera). These projections represent the depth values of the distance from the light source to the first object illuminated by the light. Redirecting texture coordinates to the correct coordinate space requires some mathematical knowledge. Previously, we explained the process of transforming vertices from object space to visual space, then to clipping space, then to normalized device coordinates, and finally to window space. Here, there are two different sets of transformation matrices, one for transformation to the visual space of the camera and the other for transformation to the visual space of the light source. Through these two sets of matrix transformation, two scenes observed from different angles are obtained.

The arrow above indicates the transformation process we need to apply to the visual linear texture coordinates. Texture projection usually starts from the generation of visual linear coordinates. This process automatically generates texture coordinates. Different from the generation of object linear texture coordinates, the generation of visual linear coordinates is not fixed to any geometry. Instead, it's like a projector projecting textures into the scene. Imagine that when you walk in front of the projector, irregular body shapes will appear on the screen.

Projection texture mapping:

Now we get the texture coordinates corresponding to the vertices in the camera's visual space. Then we need to make some transformations to get the texture coordinates of the vertices. At present, we are in the visual space of the camera. First, we return to the world coordinate system through the inverse transformation of the view matrix, and then transform to the visual space of the light source, and then to the clipping space of the light source. This series of transformations can be obtained by multiplying the following matrices:

M = Plight * MVlight * MVcamara-1

The coordinate range of X, y and Z after clipping space normalization is between [- 1,1], but our texture coordinate range is [0,1], so we still need to transform [- 1,1] to the range of [0,1]. This transformation is very simple. We only need to scale [- 1,1] by half (S), and then offset 0.5 to get [0,1] (B).

M = B * S * Plight * MVlight * MVcamara-1

So we can get the texture coordinates of the vertices after transformation. T1 = M * T;

The illustration process is as follows:

PS: the multiplication of the inverse matrix of the current model view matrix has been included in the visual plane equation.

That is, in the texture automatic generation mode of OpenGL_ EYE_ In linear, each eye plane equation is automatically multiplied by MVcamara-1

One way to implement the above steps is to manually implement them step by step through gltranslate, glscale and glmultmatrixf. Another way is that in the automatic texture generation, we can set a texture matrix to realize the above transformation, and take the texture matrix as the visual plane equation GL of visual linear coordinates_ EYE_ Just plan.

M = B * S * Plight * MVlight the approximate code is as follows:

M3DMatrix44f tempMatrix;
m3dLoadIdentity44(tempMatrix);
//Offset 0.5
m3dTranslateMatrix44(tempMatrix, 0.5f, 0.5f, 0.5f);
//Zoom 0.5
m3dScaleMatrix44(tempMatrix, 0.5f, 0.5f, 0.5f);
//Multiply by the light source projection matrix
m3dMatrixMultiply44(textureMatrix, tempMatrix, lightProjection);
//Multiply by the light source view matrix
m3dMatrixMultiply44(tempMatrix, textureMatrix, lightModelView);
//Transpose the matrix to obtain the s,t,r and q rows of the plane equation
m3dTransposeMatrix44(textureMatrix, tempMatrix);

Apply to visual plane:

//Because in the current model, the multiplication of the inverse matrix of the view matrix has been included in the visual plane equation
//Make sure that the model view matrix of the camera has been set up before glTexGenfv.
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(cameraPos[0], cameraPos[1], cameraPos[2],
0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
...

//Sets the visual plane for shadow map projection
glEnable(GL_TEXTURE_GEN_S);
glEnable(GL_TEXTURE_GEN_T);
glEnable(GL_TEXTURE_GEN_R);
glEnable(GL_TEXTURE_GEN_Q);
glTexGenfv(GL_S, GL_EYE_PLANE, &textureMatrix[0]);
glTexGenfv(GL_T, GL_EYE_PLANE, &textureMatrix[4]);
glTexGenfv(GL_R, GL_EYE_PLANE, &textureMatrix[8]);
glTexGenfv(GL_Q, GL_EYE_PLANE, &textureMatrix[12]);
...
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);

Shadow comparison

Now how do we know if the point seen from the camera's perspective is in the shadow. From the above steps, we know the depth texture coordinates of the vertex, so we can know the value of the depth texture corresponding to this depth texture coordinate, that is, texture[s/q, t/q]. This depth texture records the depth value of the point closest to the light source from the Perspective of light. The depth comparison function we set is glDepthFunc(GL_LEQUAL);., At the same time, we know that (r/q) is the depth value of the vertex in the real light source, which has been transformed to the range of [0,1] through scaling and offset. Then we compare texture[s/q, t/q] and (r/q). If texture[s/q, t/q] < R / Q, it means that the point is in the shadow. As shown below:

The depth texture contains only one value for depth. However, in the texture query of texture environment, we need to return the values of four components (RGBA). OpenGL provides several ways to extend this single depth value to other channels, including GL_ALPHA(0,0,0,D),GL_LUMINANCE(D,D,D,1) and GL_INTENSITY(D,D,D,D). Here we extend the depth value to all depth channels.

glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_INSTENSITY);

Turn on shadow comparison in OpenGL to produce shadow effect. We compare the depth value with the R component of the texture coordinates.

//Set shadow comparison

glEnable(GL_TEXTURE_2D);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);

effect:

Some code examples in the book:

// Called to regenerate the shadow map
void RegenerateShadowMap(void)
{
  GLfloat lightToSceneDistance, nearPlane, fieldOfView;
  GLfloat lightModelview[16], lightProjection[16];
  GLfloat sceneBoundingRadius = 95.0f; // based on objects in scene

  // Save the depth precision for where it's useful
  lightToSceneDistance = sqrt(lightPos[0] * lightPos[0] + 
    lightPos[1] * lightPos[1] + 
    lightPos[2] * lightPos[2]);
  nearPlane = lightToSceneDistance - sceneBoundingRadius;
  // Keep the scene filling the depth texture
  fieldOfView = (GLfloat)m3dRadToDeg(2.0f * atan(sceneBoundingRadius / lightToSceneDistance));

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(fieldOfView, 1.0f, nearPlane, nearPlane + (2.0f * sceneBoundingRadius));
  glGetFloatv(GL_PROJECTION_MATRIX, lightProjection);
  // Switch to light's point of view
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  gluLookAt(lightPos[0], lightPos[1], lightPos[2], 
    0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
  glGetFloatv(GL_MODELVIEW_MATRIX, lightModelview);
  glViewport(0, 0, shadowWidth, shadowHeight);

  // Clear the depth buffer only
  glClear(GL_DEPTH_BUFFER_BIT);

  // All we care about here is resulting depth values
  glShadeModel(GL_FLAT);
  glDisable(GL_LIGHTING);
  glDisable(GL_COLOR_MATERIAL);
  glDisable(GL_NORMALIZE);
  glColorMask(0, 0, 0, 0);

  // Overcome imprecision
  glEnable(GL_POLYGON_OFFSET_FILL);

  // Draw objects in the scene except base plane
  // which never shadows anything
  DrawModels(GL_FALSE);

  // Copy depth values into depth texture
  glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, 
    0, 0, shadowWidth, shadowHeight, 0);

  // Restore normal drawing state
  glShadeModel(GL_SMOOTH);
  glEnable(GL_LIGHTING);
  glEnable(GL_COLOR_MATERIAL);
  glEnable(GL_NORMALIZE);
  glColorMask(1, 1, 1, 1);
  glDisable(GL_POLYGON_OFFSET_FILL);

  // Set up texture matrix for shadow map projection,
  // which will be rolled into the eye linear
  // texture coordinate generation plane equations
  M3DMatrix44f tempMatrix;
  m3dLoadIdentity44(tempMatrix);
  m3dTranslateMatrix44(tempMatrix, 0.5f, 0.5f, 0.5f);
  m3dScaleMatrix44(tempMatrix, 0.5f, 0.5f, 0.5f);
  m3dMatrixMultiply44(textureMatrix, tempMatrix, lightProjection);
  m3dMatrixMultiply44(tempMatrix, textureMatrix, lightModelview);
  // transpose to get the s, t, r, and q rows for plane equations
  m3dTransposeMatrix44(textureMatrix, tempMatrix);
}

// Called to draw scene
void RenderScene(void)
{
  // Track camera angle
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  if (windowWidth > windowHeight)
  {
    GLdouble ar = (GLdouble)windowWidth / (GLdouble)windowHeight;
    glFrustum(-ar * cameraZoom, ar * cameraZoom, -cameraZoom, cameraZoom, 1.0, 1000.0);
  }
  else
  {
    GLdouble ar = (GLdouble)windowHeight / (GLdouble)windowWidth;
    glFrustum(-cameraZoom, cameraZoom, -ar * cameraZoom, ar * cameraZoom, 1.0, 1000.0);
  }

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  gluLookAt(cameraPos[0], cameraPos[1], cameraPos[2], 
    0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);

  glViewport(0, 0, windowWidth, windowHeight);

  // Track light position
  glLightfv(GL_LIGHT0, GL_POSITION, lightPos);

  // Clear the window with current clearing color
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  if (showShadowMap)
  {
    // Display shadow map for educational purposes
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glMatrixMode(GL_TEXTURE);
    glPushMatrix();
    glLoadIdentity();
    glEnable(GL_TEXTURE_2D);
    glDisable(GL_LIGHTING);
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE);
    // Show the shadowMap at its actual size relative to window
    glBegin(GL_QUADS);
    glTexCoord2f(0.0f, 0.0f);
    glVertex2f(-1.0f, -1.0f);
    glTexCoord2f(1.0f, 0.0f);
    glVertex2f(((GLfloat)shadowWidth/(GLfloat)windowWidth)*2.0f-1.0f, 
      -1.0f);
    glTexCoord2f(1.0f, 1.0f);
    glVertex2f(((GLfloat)shadowWidth/(GLfloat)windowWidth)*2.0f-1.0f, 
      ((GLfloat)shadowHeight/(GLfloat)windowHeight)*2.0f-1.0f);
    glTexCoord2f(0.0f, 1.0f);
    glVertex2f(-1.0f, 
      ((GLfloat)shadowHeight/(GLfloat)windowHeight)*2.0f-1.0f);
    glEnd();
    glDisable(GL_TEXTURE_2D);
    glEnable(GL_LIGHTING);
    glPopMatrix();
    glMatrixMode(GL_PROJECTION);
    gluPerspective(45.0f, 1.0f, 1.0f, 1000.0f);
    glMatrixMode(GL_MODELVIEW);
  }
  else if (noShadows)
  {
    // Set up some simple lighting
    glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight);
    glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight);

    // Draw objects in the scene including base plane
    DrawModels(GL_TRUE);
  }
  else
  {
    if (!ambientShadowAvailable)
    {
      GLfloat lowAmbient[4] = {0.1f, 0.1f, 0.1f, 1.0f};
      GLfloat lowDiffuse[4] = {0.35f, 0.35f, 0.35f, 1.0f};

      // Because there is no support for an "ambient"
      // shadow compare fail value, we'll have to
      // draw an ambient pass first...
      glLightfv(GL_LIGHT0, GL_AMBIENT, lowAmbient);
      glLightfv(GL_LIGHT0, GL_DIFFUSE, lowDiffuse);

      // Draw objects in the scene, including base plane
      DrawModels(GL_TRUE);

      // Enable alpha test so that shadowed fragments are discarded
      glAlphaFunc(GL_GREATER, 0.9f);
      glEnable(GL_ALPHA_TEST);
    }

    glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight);
    glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight);

    // Set up shadow comparison
    glEnable(GL_TEXTURE_2D);
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, 
      GL_COMPARE_R_TO_TEXTURE);

    // Set up the eye plane for projecting the shadow map on the scene
    glEnable(GL_TEXTURE_GEN_S);
    glEnable(GL_TEXTURE_GEN_T);
    glEnable(GL_TEXTURE_GEN_R);
    glEnable(GL_TEXTURE_GEN_Q);
    glTexGenfv(GL_S, GL_EYE_PLANE, &textureMatrix[0]);
    glTexGenfv(GL_T, GL_EYE_PLANE, &textureMatrix[4]);
    glTexGenfv(GL_R, GL_EYE_PLANE, &textureMatrix[8]);
    glTexGenfv(GL_Q, GL_EYE_PLANE, &textureMatrix[12]);

    // Draw objects in the scene, including base plane
    DrawModels(GL_TRUE);

    glDisable(GL_ALPHA_TEST);
    glDisable(GL_TEXTURE_2D);
    glDisable(GL_TEXTURE_GEN_S);
    glDisable(GL_TEXTURE_GEN_T);
    glDisable(GL_TEXTURE_GEN_R);
    glDisable(GL_TEXTURE_GEN_Q);
  }

  if (glGetError() != GL_NO_ERROR)
    fprintf(stderr, "GL Error!\n");

  // Flush drawing commands
  glutSwapBuffers();
}

Full code address https://github.com/sweetdark/openglex/tree/master/shadowmap

Limited presentation ability. If you are wrong, please correct it. Thank you very much. Please refer to the link below for details.

Projection mapping texture GL_ EYE_ Reference to linear:

http://blog.csdn.net/liu_lin_xm/article/details/4850526

http://blog.csdn.net/xukunn1226/article/details/775644

english http://www.nvidia.com/object/Projective_Texture_Mapping.html

Reference of shadow map:

http://www.eng.utah.edu/~cs5610/lectures/ShadowMapping%20OpenGL%202009.pdf

ftp://download.nvidia.com/developer/presentations/2004/GPU_Jackpot/Shadow_Mapping.pdf

Keywords: C++ OpenGL osg

Added by bestrong on Sat, 22 Jan 2022 07:31:58 +0200