OpenGL阴影,Shadow Mapping

OpenGL基础54:点光源阴影 前置:OpenGL基础53:阴影映射(下) 一、万象阴影贴图 之前成功实现了平行光阴影,生成阴影贴图时使用的矩阵是正交矩阵,若是想要实现点光源的阴影效果,那么理论上只需要修改投影矩阵为透视矩阵就好了,看上去并不是很难 但是要得到点光源阴影贴图要考虑到的是:如何渲染出周围所有的场景,像摄像机它是有一定视角的,而点光源确是360°全面照射,因此对于点光源的阴影贴图一张肯定是不够的,它只能展示出一个方向 下图是90°透视投影的视觉范围,朝向为x轴正方向,那么只需要再对剩下的5个方向各来一次投影理论... 阅读详情
原文

  http://www.cnblogs.com/liangliangh/p/4131103.htm


实验平台:Win7,VS2010

先上结果截图(文章最后下载程序,解压后直接运行BIN文件夹下的EXE程序):

本文描述图形学的两个最常用的阴影技术之一,Shadow Mapping方法(另一种是Shadow Volumes方法)。在讲解Shadow Mapping基本原理及其基本算法的OpenGL实现之后,将继续深入分析解决几个实际问题,包括如何处理全方向点光源、多个光源、平行光。最近还有可能写一篇Shadow Volumes的博文(目前已经将基本理论弄清楚了),在那里,将对Shadow Mapping和Shadow Volumes方法做简要的分析对比。

本文的程序实现用到了很多开源程序库,列举如下:

  1. GLEW ,用于处理OpenGL本地扩展;
  2. GLFW ,用于处理窗口,以及创建OpenGL Context;
  3. Freeglut ,处理窗口,但本文只用其绘制基本几何体,如茶壶;
  4. GLM ,OpenGL数学库,向量及矩阵代数计算;
  5. DevIL ,读写图片,支持很多格式,如JPG、PNG;
  6. FTGL ( 作者网站 ),在OpenGL中显示字体,支持TrueType字体文件读取,支持抗锯齿字体、拉伸实体字形等,需要 FreeType 库支持;
  7. Bullet ,物理引擎,可以进行刚体可变形体的模拟,本文暂未使用;
  8. VCG ,读写.obj等网格数据,高效表示网格,并有大量如网格修复算法实现,本文暂未使用。
1. 数学原理

抛开复杂的现实世界中对“阴影”难以定义的问题(见文献[1]第1章),直接来看图形学实际采用的阴影的数学定义,如下图(摘自文献[1]):

lit(lighted)是直接接受光照的区域,umbra中文为“本影”,是某个光源完全被遮挡的局域,penumbra中文为“半影”,是仅能接受到有限大光源部分光照的区域。有限大光源产生半影,使得阴影的边沿柔和化,也称作Soft Shadow,理想点光源的半影将消失,也称为Hard Shadow。本文中,我将只考虑Hard Shadow,并主要讨论点光源,可以想见,有限大光源可以用无穷多个点光源逼近。

有了阴影的定义,用OpenGL实现阴影的问题就归结为:对摄像机看到的每个表面上的点,确定其和光源之间是否有遮挡,如果有则该点位于阴影中,如果没有则该点直接接受光照(不考虑半影)。Shadow Mapping方法将这个问题等价转换为:对于每个表面上的点P,过该点做一条从光源射出的射线(再次,我们主要说点光源),这条射线和场景中物体的表面有多个交点,设这些交点中离光源最近的为A,如果P点离光源距离大于A,则P点位于阴影中,否则接受光照;如果对从光源发出的每条射线,均找到这样的A,并将A到光源距离计算出来做成“表”,这样对于P点只需要“查表”找到其所在射线的那个表项就可以了;当然,计算机处理不了“每条射线”这种无穷问题,需要将光源照射的方向离散化,转化为有限问题,这将用到现代图形硬件的“光栅化”功能。

下图说明了Shadow Mapping的基本原理,先不用看图中文字,请看下面解释(摘自文献[1]):

左图中,黄色光源下面那个蓝线框矩形图即“类似”于上面说的,对于光源发出的每条射线,找一个最近距离,称为Shadow Map(阴影图),在实际渲染中,对于每个表面点P,只需找到和P在同一条光源发出射线上的Shadow Map中的表项,比较P点到光源距离和表项值的大小,即可判断阴影。这里之所以说“类似”是因为,Shadow Map中存储的并不是最近点A到光源的距离(设这个距离为d),而是d的函数,设为f(d),可以看到只要f严格单调递增,比较d的大小和比较f(d)的大小是等价的(好在只需要比较大小,而不需要知道具体大多少)。这个f即齐次坐标变换,f(d)即深度值,说的具体一点,就是模型视图变换和投影变换,模型变换将物体坐标变换到世界坐标,再经过视图变换到视觉坐标,再经过投影变换到裁剪坐标(视景体被变为xyz为±1的边长为2的正方体),详见我前两篇博文:文献[6][7]。这里来说明一下投影变换具有所需要的性质:将过光源点(摄像机位置)的射线变换为射线,且射线上的点顺序不发生变化(严格单调增加)。

Shadow Mapping方法概述如下:

  • 定义一个变换生成Shadow Map,记为表S,其中保存了最近点深度值,即视图矩阵V为摄像机在光源点对准物体,投影矩阵P为开口和聚光灯开角相等或足以包括场景物体的透视投影矩阵,记M=PV为视图矩阵和投影矩阵变换的叠加;
  • 在渲染场景时,对每个片断,设其世界坐标为 p ,则其到光源的深度值可如下计算, q =M p =(x q ,y q ,z q ,w q ),d=z q /w q ,用d和S的表项S(x q /w q ,y q /w q)比较,若结果为等于则 p 接受光照,若大于则位于阴影中。

这里再注意一个细节,上面算法对每个片断进行,对每个片断的坐标进行齐次变换求得其到光源深度值,其实这是没有必要的,对于每个图元:点、线、多边形,其片断的到光源深度值可由其顶点到光源的深度值插值得到,毕竟,同一图元必定落于某平面上。这里的“插值”是在光栅化阶段进行的,就像我之前博文说的,其实它并不简单(文献[6]),但我们不用管,即使在着色器程序中,光栅化也由固定管线功能实现。

2.基本算法的OpenGL实现

我们先来看一个最简单的程序,从光源绘制一个深度图,将其拷贝到纹理,我们先手动计算纹理坐标,以直观表达计算过程。程序的全局变量,纹理初始化代码如下(请见文献[6]最后的OpenGL函数总结):

GLuint tex_shadow; // 纹理名字
glm::vec4 light_pos; // 光源在世界坐标中的位置
glm::mat4 shadow_mat_p; // 光源视角的投影矩阵
glm::mat4 shadow_mat_v; // 光源视角的视图矩阵
void tex_init() // 纹理初始化
{
  // 纹理如何影响颜色,和光照计算结果相乘
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
  // 分配纹理对象,并绑定为当前纹理
  glGenTextures(1, &tex_shadow);
  glBindTexture(GL_TEXTURE_2D, tex_shadow);
  // 纹理坐标超出[0,1]时如何处理
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  // 非整数纹理坐标处理方式,线性插值
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  // 深度纹理,深度值对应亮度
  glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_LUMINANCE);
} 

绘制函数里,先将摄像机放置在光源位置,渲染后将深度缓冲拷贝到纹理,代码如下:

//---------------------------------------第1次绘制,生成深度纹理--------
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 将摄像机放置在光源位置,投影矩阵和视图矩阵
shadow_mat_p = glm::perspective(glm::radians(90.0f), 1.0f, 1.0f, 1.0e10f);
shadow_mat_v = glm::lookAt(glm::vec3(light_pos), glm::vec3(0), glm::vec3(0,1,0));
glMatrixMode(GL_PROJECTION); glPushMatrix();
glLoadMatrixf(&shadow_mat_p[0][0]); // 加载投影矩阵
glMatrixMode(GL_MODELVIEW); glPushMatrix();
glLoadMatrixf(&shadow_mat_v[0][0]); // 加载视图矩阵
  draw_world();
glMultMatrixf(&mat_model[0][0]);
  draw_model();
glMatrixMode(GL_PROJECTION); glPopMatrix();
glMatrixMode(GL_MODELVIEW); glPopMatrix();
// 拷贝深度缓冲到纹理
glBindTexture(GL_TEXTURE_2D, tex_shadow);
glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT,
  0, 0, glStaff::get_frame_width(), glStaff::get_frame_height(), 0);
glEnable(GL_TEXTURE_2D); // 使能纹理
void draw_model() // 绘制模型,一个茶壶
{    
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glTranslatef(0, 1, 0);
        glutSolidTeapot(1);
    glPopMatrix();
}

我们的draw_world就绘制一个平面,draw_model就绘制一个茶壶,场景如下(黄色为光源位置):

可以用如下代码获取纹理像素,并用DevIL保存(il_saveImgDep是我写的函数,字符串前加L是wchar_t字符串):

GLfloat* data = new GLfloat[glStaff::get_frame_width()*glStaff::get_frame_height()];
glGetTexImage(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, GL_FLOAT, data); // 获取纹理数据
il_saveImgDep(L"d0.png", data, glStaff::get_frame_width(), glStaff::get_frame_height());
delete[] data;

深度图如下,距离摄像机近的点深度值小,所以颜色为黑色,距离越远颜色越白:

我们手动将这个纹理贴到那个正方形地板上:

void draw_world() // 绘制世界,一个地板
{
  glm::vec4 v1(-3, 0,-3, 1), v2(-3, 0, 3, 1), v3( 3, 0, 3, 1), v4( 3, 0,-3, 1);//四个顶点
  glm::mat4 m = glm::translate(glm::vec3(0.5f,0.5f,0.5f))
    * glm::scale(glm::vec3(0.5f,0.5f,0.5f)); // 需要将裁剪坐标的[-1,+1]缩放到[0,1]
  glm::vec4 t;
  glBegin(GL_POLYGON);
    glNormal3f(0, 1, 0);
    t = m*shadow_mat_p*shadow_mat_v*v1; // 按和生成纹理相同的变换计算纹理坐标
    glTexCoord4fv(&t[0]); glVertex3fv(&v1[0]);
    t = m*shadow_mat_p*shadow_mat_v*v2;
    glTexCoord4fv(&t[0]); glVertex3fv(&v2[0]);
    t = m*shadow_mat_p*shadow_mat_v*v3;
    glTexCoord4fv(&t[0]); glVertex3fv(&v3[0]);
    t = m*shadow_mat_p*shadow_mat_v*v4;
    glTexCoord4fv(&t[0]); glVertex3fv(&v4[0]);
  glEnd();
}
//-------------------------------------------第2次绘制,绘制场景------------
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glMatrixMode(GL_MODELVIEW);
glLoadMatrixf(&mat_view[0][0]);
    draw_world();
glMultMatrixf(&mat_model[0][0]);
    draw_model();

注意一个细节,经过模型视图和投影变换得到的裁剪坐标xyz坐标位于[-1,+1],而纹理坐标以及纹理像素值也就是深度值位于[0,1]需要将[-1,+1]缩放到[0,1],也即先缩放0.5倍,再平移0.5(OpenGL管线中这一变换在视口变换时进行,见文献[6])。绘制结果如下:

请对照深度图,因为计算纹理坐标的变换和生成纹理的变换相同,所以,深度纹理中的地板的四个角正好被贴图到了场景地板的四个角。由于茶壶函数是 glut 内置,其内部可能指定了纹理坐标,所以纹理也被贴到了茶壶上。上述所有代码请见所附程序中的 mapping_basic0.cpp。

上面程序的结果,地板上看着挺像阴影的,因为恰好在遮挡的地方纹理的颜色又偏黑(深度值小)。现在还需将计算出的纹理坐标的z值和纹理像素值也即深度值进行比较,并根据结果选择进行光照还是没有光照,因为纹理的影响模式为乘积,完全的光照也就是纹理值为1,完全没有光照也就是纹理值为0,OpenGL提供了纹理比较机制:用纹理坐标的r(纹理坐标四个分量为strq)值和纹理像素值比较,比较的结果是0和1(相等时为1),用比较的结果替换原来纹理值。只需在上面代码的初始化纹理函数 tex_init 中加入如下两行代码便启用此机制:

// 纹理比较模式,用纹理坐标r和纹理值(深度值)比较,若小于等于纹理值改为1,否则改为0
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);

你可能已经想到了,对两种计算方式下计算出的浮点数进行相等比较(程序中用小于等于,理论上用等于就可以)结果是不确定的,如下图的斑纹:

可以对计算的纹理坐标r坐标进行少许偏移,让其偏小:

glm::mat4 m = glm::translate(glm::vec3(0.5f,0.5f,0.49f))
        * glm::scale(glm::vec3(0.5f,0.5f,0.5f)); // 需要将裁剪坐标的[-1,+1]缩放到[0,1]

直接对r坐标进行偏移或者直接对深度纹理的深度值进行偏移并不是一个好方法,因为透视投影下深度值和裁剪坐标的z值之间并不是线性关系:在离摄像机很远的地方,两个z值差别很大的点其深度值可能差别非常小(都接近1)。合理的做法是:1.多边形偏移,2.在生成深度纹理时剔除正面,更多方法请见文献[1]。

除了手动计算纹理坐标,我们可以将变换放到纹理变换矩阵中,上面绘制世界函数的等价版本如下:

void draw_world() // 绘制世界,一个地板
{
  glm::vec4 v1(-3, 0,-3, 1), v2(-3, 0, 3, 1), v3( 3, 0, 3, 1), v4( 3, 0,-3, 1);
  glm::mat4 m = glm::translate(glm::vec3(0.5f,0.5f,0.5f))
    * glm::scale(glm::vec3(0.5f,0.5f,0.5f)); // 需要将裁剪坐标的[-1,+1]缩放到[0,1]
  m = m*shadow_mat_p*shadow_mat_v;
  glMatrixMode(GL_TEXTURE); glLoadMatrixf(&m[0][0]); glMatrixMode(GL_MODELVIEW);
  glBegin(GL_POLYGON);
    glNormal3f(0, 1, 0);
    glTexCoord4fv(&v1[0]); glVertex3fv(&v1[0]);
    glTexCoord4fv(&v2[0]); glVertex3fv(&v2[0]);
    glTexCoord4fv(&v3[0]); glVertex3fv(&v3[0]);
    glTexCoord4fv(&v4[0]); glVertex3fv(&v4[0]);
  glEnd();
}

其实目前还有一个问题,就是我们的影子只投到了地板上,茶壶上并没有,这是因为茶壶函数是封装好的,我们不能到茶壶函数内部去指定纹理坐标。OpenGL提供了纹理坐标自动生成机制,可以从顶点物体坐标或顶点视觉坐标自动生成纹理坐标,我们先来看看从顶点物体坐标自动生成纹理坐标。需要在纹理初始化时加入如下代码:

// 纹理坐标自动生成,从顶点物体坐标生成
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
glEnable(GL_TEXTURE_GEN_S);
glEnable(GL_TEXTURE_GEN_T);
glEnable(GL_TEXTURE_GEN_R);
glEnable(GL_TEXTURE_GEN_Q);

并在使用纹理前,也就是渲染深度纹理后将纹理坐标变换矩阵分行传递到纹理坐标自动生成的参数,如下:

// 将纹理坐标变换矩阵分行传递到纹理坐标自动生成的参数
glm::mat4 mat =
  glm::translate(glm::vec3(0.5f,0.5f,0.5f))*glm::scale(glm::vec3(0.5f,0.5f,0.5f))
  * shadow_mat_p * shadow_mat_v;
mat = glm::transpose(mat);
glTexGenfv(GL_S, GL_OBJECT_PLANE, &mat[0][0]);
glTexGenfv(GL_T, GL_OBJECT_PLANE, &mat[1][0]);
glTexGenfv(GL_R, GL_OBJECT_PLANE, &mat[2][0]);
glTexGenfv(GL_Q, GL_OBJECT_PLANE, &mat[3][0]);

还记得 OpenGL 和 GLM 的矩阵都是列优先,所以按行加载前要转置。其实,所谓纹理坐标自动生成就是,管线在遇到一个顶点时自动计算一个纹理坐标,这和之前手动计算或加载到纹理矩阵的计算方式是完全相同的,只不过现在自动计算而已,这里看到这些 GL_OBJECT_PLANE 参数合起来就是纹理矩阵,但 OpenGL 支持对 strq 坐标指定不同变换的行。看看结果,有点惊讶:

可以看到,地板上的阴影是正确的,但茶壶上不正确,原因是我们使用顶点的物体坐标,也就是直接传递给 glVertex3f() 等函数的值,这样茶壶函数指定的顶点物体坐标可能经过模型视图矩阵的变换,而我们没有跟踪到这些变换,毕竟那是封装的函数。其实我们想用的是顶点的世界坐标,不过OpenGL纹理坐标自动生成除了用顶点物体坐标外,另只支持从顶点视觉坐标生成纹理坐标,因为OpenGL将视图和模型变换矩阵合二为一了,不过,视觉坐标到世界坐标的转换可以通过摄像机定义的视图变换矩阵的逆做到。下面是从顶点视觉坐标自动生成纹理坐标的代码,请对照前面的物体坐标代码:

// 纹理坐标自动生成,从顶点视觉坐标
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);
glEnable(GL_TEXTURE_GEN_S);
glEnable(GL_TEXTURE_GEN_T);
glEnable(GL_TEXTURE_GEN_R);
glEnable(GL_TEXTURE_GEN_Q);
// When the eye planes are specified, the GL will automatically post-multiply them
// with the inverse of the current modelview matrix.
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glm::mat4 mat =
  glm::translate(glm::vec3(0.5f,0.5f,0.5f)) * glm::scale(glm::vec3(0.5f,0.5f,0.5f))
  * shadow_mat_p * shadow_mat_v * glm::affineInverse(mat_view);
mat = glm::transpose(mat);
glTexGenfv(GL_S, GL_EYE_PLANE, &mat[0][0]);
glTexGenfv(GL_T, GL_EYE_PLANE, &mat[1][0]);
glTexGenfv(GL_R, GL_EYE_PLANE, &mat[2][0]);
glTexGenfv(GL_Q, GL_EYE_PLANE, &mat[3][0]);

指定从顶点视觉坐标自动生成纹理坐标的参数时,OpenGL会自动将参数代表的矩阵和当前模型视图矩阵的逆相乘,这本来是要给我们带来方便的,但很多时候这种额外的耦合会被忽略从而得到莫名其妙的结果。上面代码等价于:

// When the eye planes are specified, the GL will automatically post-multiply them
// with the inverse of the current modelview matrix.
glMatrixMode(GL_MODELVIEW);
glLoadMatrixf(&mat_view[0][0]); // glLoadIdentity();
glm::mat4 mat =
  glm::translate(glm::vec3(0.5f,0.5f,0.5f)) * glm::scale(glm::vec3(0.5f,0.5f,0.5f))
  * shadow_mat_p * shadow_mat_v/* * glm::affineInverse(mat_view)*/;
mat = glm::transpose(mat);
glTexGenfv(GL_S, GL_EYE_PLANE, &mat[0][0]);
glTexGenfv(GL_T, GL_EYE_PLANE, &mat[1][0]);
glTexGenfv(GL_R, GL_EYE_PLANE, &mat[2][0]);
glTexGenfv(GL_Q, GL_EYE_PLANE, &mat[3][0]);

来看结果,抛开浮点数相等比较带来的斑纹问题,都是正确的,茶壶把手和壶盖那里也有了阴影:

下面来看用多边形偏移和剔除正面方法解决斑纹问题的代码:

//---------------------------------------第1次绘制,生成深度纹理--------
// ...
glEnable(GL_POLYGON_OFFSET_FILL); // 多边形偏移
glPolygonOffset(0, 20000);
// draw_world() ...
glPolygonOffset(0, 0); // 别忘了恢复原来的值
glDisable(GL_POLYGON_OFFSET_FILL);
//---------------------------------------第1次绘制,生成深度纹理--------
// ...
glEnable(GL_CULL_FACE); // 剔除正面
glCullFace(GL_FRONT);
// draw_world() ...
glCullFace(GL_BACK); // 别忘了恢复原来的值
glDisable(GL_CULL_FACE);

就像我前一篇博文文献[6]说的,多边形顶点的环绕方向(右手法则)要和多边形的法向量一直,glut茶壶函数就是个反例,这使得我们不得不在绘制茶壶前临时剔除背面。

多边形偏移结果如下:

剔除正面的结果以及剔除前后的深度图如下:

结果差不多,注意茶壶相对光照的背面还有斑纹,那无关紧要,因为那里是不受光源照射(法向量和到光源向量乘积小于0)的地方,后面将对环境光和光源光分开两遍渲染,背面斑纹将自然消失。这里再次强调上图结果之所以对每个片断都正确,得益于光栅化对纹理坐标进行了正确插值(文献[6])。后面将采用剔除正面的做法,因为:多边形偏移方法的偏移值不好确定,剔除正面可以减少片断数量提高效率。但剔除正面方法也有问题:几何体(除了只接收阴影的物体)必须是封闭的,几何体的多边形顶点环绕方向必须和多边形法向量一致。这部分所有代码请见所附程序中的 mapping_basic1.cpp。

在进入下节之前,我们先来看一个 Shadow Mapping 方法给我们带来的附加产品:投影贴图,将上面深度纹理改成普通纹理,继续使用纹理坐标自动生成,代码如下:

void tex_init() // 纹理初始化
{
  // 纹理如何影响颜色,和光照计算结果相乘
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
  // 分配纹理对象,并绑定为当前纹理
  glGenTextures(1, &tex_lena);
  glBindTexture(GL_TEXTURE_2D, tex_lena);
  // 纹理坐标超出[0,1]时如何处理
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
  // 边框颜色
  GLfloat c[4] = {1,1,1, 1};
  glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, c);
  // 非整数纹理坐标处理方式,线性插值
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  // 纹理坐标自动生成
  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);
  glEnable(GL_TEXTURE_GEN_S);
  glEnable(GL_TEXTURE_GEN_T);
  glEnable(GL_TEXTURE_GEN_R);
  glEnable(GL_TEXTURE_GEN_Q);
  // 纹理数据
  void* data; int w, h;
  il_readImg(L"Lena Soderberg.jpg", &data, &w, &h);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
  delete data;
}
// 将摄像机放置在光源位置,投影矩阵和视图矩阵
shadow_mat_p = glm::perspective(glm::radians(45.0f), 1.0f, 1.0f, 1.0e10f);
shadow_mat_v = glm::lookAt(glm::vec3(light_pos), glm::vec3(0), glm::vec3(0,1,0));
// When the eye planes are specified, the GL will automatically post-multiply them
// with the inverse of the current modelview matrix.
glMatrixMode(GL_MODELVIEW);
glLoadMatrixf(&mat_view[0][0]); // glLoadIdentity();
glm::mat4 mat =
  glm::translate(glm::vec3(0.5f,0.5f,0.5f)) * glm::scale(glm::vec3(0.5f,0.5f,0.5f))
  * shadow_mat_p * shadow_mat_v/* * glm::affineInverse(mat_view)*/;
mat = glm::transpose(mat);
glTexGenfv(GL_S, GL_EYE_PLANE, &mat[0][0]);
glTexGenfv(GL_T, GL_EYE_PLANE, &mat[1][0]);
glTexGenfv(GL_R, GL_EYE_PLANE, &mat[2][0]);
glTexGenfv(GL_Q, GL_EYE_PLANE, &mat[3][0]);
glEnable(GL_TEXTURE_2D);
//-------------------------------------------绘制场景------------
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// ...

结果如下,左上角为纹理原图(已打马赛克):

结果就好像在光源处有一个投影仪(没处理遮挡问题,可以用下一节方法)将图片投影下来,如果将上图中著名的 Lena 换成窗户,结果像光透过窗户洒在地上:

这部分代码请见所附程序中的 mapping_tex_map.cpp。

后面我们将在基础 Shadow Mapping 方法上进行改进以解决如下问题:

  • 目前深度图渲染使用默认帧缓冲区(Default Frame Buffer,请见文献[6]),这个缓冲区的宽和高跟随窗口,另外从默认帧缓冲中将深度值拷贝到纹理效率也不高,为了提高效率,也为了渲染大尺寸深度纹理来减轻阴影锯齿,将使用帧缓冲对象(Framebuffer Objects),并将纹理绑定到帧缓冲对象的深度缓冲,这样将能够直接将深度值渲染到纹理;
  • 在渲染深度图时,由于只需要深度值,把光照、纹理关闭以及屏蔽颜色缓冲写操作可以提高效率;
  • Shadow Mapping方法占用纹理通道,如果还想用普通的纹理贴图,需要使用多重纹理;
  • 目前阴影部分是纯黑色的,我们希望阴影部分不接受对应光源的照射,但接受环境光和其他光源的照射,这需要在渲染场景时进行多遍渲染,并将结果累加,这时后续渲染不需要清除深度缓冲和颜色缓冲,并需要修改深度测试函数和混合函数;
  • 目前渲染深度图时只有一个视角,如果点光源的四周都有物体将不能正确处理,最简单的方法是用6个视角为90度的光源视角将光源的全方向都渲染到深度纹理(想象光源位于某正方体中心),并在应用时将结果累加;
  • 多个光源的处理也需要多遍渲染,这和环境光光源光分离以及全方向点光源的处理类似;
  • 另外还有平行光问题,将光源视角的投影矩阵从透视投影换成平行投影即可,另外需要合理设置视景体以将场景全部包括进来,这时不存在全方向的问题。

下一节将逐个解决这些问题。

3.解决实际问题

3.1多重纹理,渲染到纹理,环境光

OpenGL多重纹理很简单,用 glActiveTexture(GL_TEXTURE0[1,2,...]) 函数指定当前纹理单元(纹理单元是个术语,就是一个纹理组,不同纹理组可以同时应用纹理功能),这里要分清纹理单元和纹理的参数,纹理单元的参数包括 glTexEnvi[f]() 指定的纹理影响模式以及 glTexGeni[f]() 指定的纹理坐标自动生成参数,纹理的参数包括纹理像素和 glTexParameteri[f]() 指定的参数。如下例子:

// 纹理单元0为当前纹理单元
glActiveTexture(GL_TEXTURE0);
  // 纹理单元0的影响模式
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
  glGenTextures(1, &tex1);
  glBindTexture(GL_TEXTURE_2D, tex1);
  // 纹理单元0中的一个纹理tex1,其像素和参数
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  // 纹理单元0中的一个纹理tex2,其参数
  glGenTextures(1, &tex2);
  glBindTexture(GL_TEXTURE_2D, tex2);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);

// 纹理单元1为当前纹理单元
glActiveTexture(GL_TEXTURE1); // shadow texture
  // 纹理单元1的环境函数,以及纹理坐标自动生成
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
  glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
  glEnable(GL_TEXTURE_GEN_S);
  glTexGenfv(GL_S, GL_EYE_PLANE, v1);
  // 纹理单元1的一个纹理,其参数
  glGenTextures(1, &tex3);
  glBindTexture(GL_TEXTURE_2D, tex3);
  glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_LUMINANCE);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, //指定像素数据且传入0指针,预分配存储
    shadow_w, shadow_h, 0, GL_DEPTH_COMPONENT, GL_FLOAT, 0);

// 纹理单元1,禁用纹理
glActiveTexture(GL_TEXTURE1);
  glDisable(GL_TEXTURE_2D);
// 纹理单元0,启用纹理
glActiveTexture(GL_TEXTURE0);
  glEnable(GL_TEXTURE_2D);

// -------------------------------------- 绘制函数 -------------------------------------
// 因为设置纹理单元0为当前纹理单元,且绑定tex1,纹理坐标t1,t2,t3将索引纹理tex1
// 另可用glMultiTexCoord指定多重纹理中特定纹理单元的纹理坐标,将索引那个纹理单元中最后绑定的纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, tex1);
glBegin(GL_POLYGON);
  glNormal3f(0, 1, 0);
  glTexCoord4fv(&t1[0]); glVertex3fv(&v1[0]);
  glTexCoord4fv(&t2[0]); glVertex3fv(&v2[0]);
  glTexCoord4fv(&t3[0]); glVertex3fv(&v3[0]);
glEnd();

帧缓冲对象的使用例子如下:

// 分配一个帧缓冲对象,并绑定为当前写缓冲对象
glGenFramebuffers(1, &frame_buffer_s);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frame_buffer_s);
// 分配一个渲染缓冲,绑定,分配存储
glGenRenderbuffers(1, &render_buff_rgba);
glBindRenderbuffer(GL_RENDERBUFFER, render_buff_rgba);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA, shadow_w, shadow_h);
// 将渲染缓冲设定为帧缓冲对象的颜色缓冲,帧缓冲可以有颜色、深度、模板缓冲
glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
  GL_RENDERBUFFER, render_buff_rgba);
// 将深度纹理设定为帧缓冲对象的深度缓冲
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
  GL_TEXTURE_2D, tex_shadow, 0);

// -------------------------------------- 绘制函数 -------------------------------------
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frame_buffer_s);
// 以下绘制将绘制到帧缓冲对象frame_buffer_s,即render_buff_rgba和tex_shadow
glViewport(0, 0, shadow_w, shadow_h); // 将视口设置为和frame_buffer_s相同
glClear(GL_DEPTH_BUFFER_BIT); // 清除tex_shadow
// ...

glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 以下绘制将绘制到帧默认缓冲对象,即窗口的附属帧缓冲
glViewport(0, 0, get_frame_width(), get_frame_height()); // 将视口设置为和窗口大小相同
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清除屏幕
// ....

上面代码中,帧缓冲对象用于存放渲染目标,并用纹理或渲染对象作为帧缓冲对象这个“壳子”的具体存储。除此之外帧缓冲对象还可以用于指定 glReadPixels() 函数的读目标。

为减轻 Shadow Mapping 阴影的锯齿问题,需要增加纹理的分辨率,现在,应用绘制到纹理之后纹理的大小将可以自由设置,可以用 glGetIntegerv(GL_MAX_TEXTURE_SIZE, GLint*) 获取系统支持的最大纹理,我的机器(GT240 1GB GDDR5 OpenGL 3.3)最大为 8192x8192,下面是128x128 和 8192x8192 分辨率深度纹理的对比:

可以看到,现在阴影不再是全黑色了,这用到了多遍渲染,并将结果累加,代码如下:

//-------------------------------- 第2次绘制,绘制场景 ----------------------------
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 1 环境光
glDisable(GL_LIGHT0);
glActiveTexture(GL_TEXTURE1); glDisable(GL_TEXTURE_2D);
glActiveTexture(GL_TEXTURE0); glEnable(GL_TEXTURE_2D);
//float gac2[4]={0,0,0,1}; glLightModelfv(GL_LIGHT_MODEL_AMBIENT, gac2); // black
glMatrixMode(GL_MODELVIEW);
glLoadMatrixf(&mat_view[0][0]);
  draw_world();
glMultMatrixf(&mat_model[0][0]);
  draw_model();
// 2 点光源
GLfloat la[4]; glGetFloatv(GL_LIGHT_MODEL_AMBIENT, la);
float gac[4]={0,0,0,1}; glLightModelfv(GL_LIGHT_MODEL_AMBIENT, gac); // black
glEnable(GL_LIGHT0);
glActiveTexture(GL_TEXTURE1); glEnable(GL_TEXTURE_2D);
glActiveTexture(GL_TEXTURE0); glEnable(GL_TEXTURE_2D);
glDepthFunc(GL_EQUAL); glBlendFunc(GL_ONE, GL_ONE);
glMatrixMode(GL_MODELVIEW);
glLoadMatrixf(&mat_view[0][0]);
glLightfv(GL_LIGHT0, GL_POSITION, &light_pos[0]); // 位置式光源
  draw_world();
glMultMatrixf(&mat_model[0][0]);
  draw_model();
glLightModelfv(GL_LIGHT_MODEL_AMBIENT, la); // 恢复环境光
glDepthFunc(GL_LESS); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

要点是,第二次不清除颜色和深度缓冲,并将深度测试函数设为相等(这里怎么又可以对浮点数进行相等比较了呢,因为第二遍渲染和第一遍的深度值计算过程完全相同),将混合设为直接相加(源,即片断,和目标,即之前颜色缓冲的值,的因子均为1)。第一遍打开环境光,关闭点光源,第二遍关闭环境光,打开点光源。

叠加示意图如下:

注意一个细节,OpenGL光照为逐顶点光照,上图中底板的明暗变化是用多个小方块才产生的,如果简单的将底板用四个顶点绘制,底板内部的颜色将是从顶点光照颜色插值而来(光栅化的结果),这样就不会有明暗变化,对比下图的左右边:

本小节代码见所附程序中的 mapping_render_to_tex.cpp。

3.2 全方向点光源

可以渲染6个深度纹理,每个代表点光源全方向的6分之1,如下图所示:

全方向点光源的实现和上一小节的环境和点光源分离类似,都是采用“1+1”叠加的混合实现的,具体实现代码见所附程序中的 mapping_omni_directional.cpp。下面是程序结果:

下面是这幅图的6个深度图,以及环境、点光源6个方向的贡献图,1、2行为光源视角深度图(剔除正面),3、4行为对应点光源贡献,5行为环境光贡献、最后结果、摄像机视角深度图:

一个细节,为了让点光源每个方向的贡献,在超出纹理坐标[0,1]之后全是黑色,把深度纹理的边框设为黑色,将纹理坐标环绕模式(纹理坐标超出[0,1]时处理方式)设置为 GL_CLAMP_TO_BORDER,并将纹理比较函数从 GL_LEQUAL 改为 GL_LESS(影响可以忽略不计,浮点数比较),代码如下:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
GLfloat c[4]={0,0,0,1}; glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, c);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LESS);

同理,当使用单个视角的 Shadow Mapping 时,为防止超出纹理坐标范围[0,1]的部分变为黑色,可以将纹理的边框颜色设置为白色,并将纹理坐标环绕模式设置为GL_CLAMP_TO_BORDER。

3.2 多个光源

多个光源的处理和全方向光源非常类似,也是进行多遍渲染,请见所附程序中的 mapping_multi_lights.cpp,程序利用了这样的性质 GL_LIGHTi=GL_LIGHT0+i,程序结果见最前面彩色图(gif图片颜色有损失,小黑点是光源位置)。

再看各个光源以及环境光的贡献:

上图中第1行从左到右依次为光源1、2、3的深度图,第2行从左到右依次为光源1、2、3的贡献,第3行为环境光贡献,下面是最后结果:

3.3 平行光 

平行光的处理非常简单,只需将前面的从光源视角的透视投影矩阵改为平行投影矩阵,并设置视景体使得场景全部落在裁剪体内,另外,平行投影的深度值和视觉坐标的z值是线性关系(有平移),所以深度比较的精度也会高些,具体代码加所附程序中的 mapping_parallel.cpp。下面是结果截图:

深度图如下(剔除正面,光源视角摄像机沿y轴向上):

4.进一步研究

Shadow Mapping 方法虽然提出很早,但直到现在仍有许多前沿研究,这可能是因为 Shadow Mapping 方法的简洁性(不需要几何信息,只需要将场景额外的从光源渲染),研究内容主要位于从阴影图过滤产生柔和阴影,详见文献[1]。

下载链接,因为程序将所有的库都打包了,这样的好处是程序不依赖系统,另外将微软雅黑字体也拷贝了进去,还有几张贴图,所以程序压缩后仍有25MB大小,见谅。

链接: http://pan.baidu.com/s/1qWPWC7i 密码: nwdo

参考文献
  1. Eisemann, E., Assarsson, U., Schwarz, M. and Wimmer, M., Shadow algorithms for real-time rendering. in Eurographics 2010-Tutorials, (2010), The Eurographics Association( 进入作者给的下载链接 ,另该作者在ACM SIGGRAPH 2012,2013 Course “Efficient real-time shadows”,ACM SIGGRAPH Asia 2009 Course “Casting Shadows in Real Time”,2011 Book “Real-Time Shadows”);
  2. 《OpenGL Specification Version 3.3 (Compatibility Profile) 2010》, 2.12.3 Generating Texture Coordinates( 到官网下载 );
  3. http://en.wikipedia.org/wiki/Shadow_mapping ;
  4. C. Everitt, "Projective texture mapping," White paper, NVidia Corporation, vol. 4, 2001( 进入下载 );
  5. Paul's Projects, Shadow Mapping ( 这里进入网页 );
  6. OpenGL管线(用经典管线代说着色器内部) ;
  7. OpenGL坐标变换及其数学原理,两种摄像机交互模型(附源程序) 。
l
使用OpenGL实现点阴影的示例(C/C++) 在本文中,我将向您展示如何使用OpenGL和C/C++编程语言来实现点阴影效果。点阴影是一种用于在三维场景中呈现阴影效果的技术。我们将使用OpenGL提供的着色器和渲染技术来创建点阴影效果。在本示例中,我们使用了两个着色器程序,一个用于生成阴影贴图,另一个用于渲染场景。我们还使用了投影矩阵、视图矩阵和模型矩阵来对场景进行变换和投影。这就是使用OpenGL和C/C++实现点阴影的示例。您可以根据自己的需求进行修改和扩展。现在,让我们开始编写代码。使用OpenGL实现点阴影的示例(C/C++) 阅读详情

相关推荐

OpenGL 阴影图之二

OpenGL阴影图OpenGL阴影图简介源代码剖析主要源代码 OpenGL阴影图简介 在上一课中,我们学习了阴影显示方法的基本原理,并了解了如何将深度渲染到纹理中,然后通过深度缓冲区的采样器将其显示到屏幕上。在本课中,我们将看到如何使用它来显示阴影。 我们已经知道阴影显示分两个阶段进行:第一个场景从光源位置渲染。让我们回想一下 Z 顶点坐标在其中发生的情况: 落入顶点着色器的顶点位置在本地空间中指定。 着色器将位置从本地空间转换为剪辑,然后通过管道转换(如果需要刷新内存,请查看第 12 课)。 栅格生成透视

希望我的博客,能帮上你解决学习中工作中所遇到的问题 720

OpenGL - Shadows

OpenGL - 阴影

qq_42403042的博客 650

OpenGL Shadow Mapping阴影贴图的实例

OpenGL shadowmapping先上图,再解答。完整主要的源代码源代码剖析 先上图,再解答。 完整主要的源代码 源代码剖析

希望我的博客,能帮上你解决学习中工作中所遇到的问题 638

openGL阴影实现(硬阴影)

openGL阴影贴图,硬阴影,会产生伪影

aoxuestudy的专栏 5638

OpenGL Shadow Mapping阴影贴图的实例

OpenGL Shadow Mapping阴影贴图先上图,再解答。完整主要的源代码源代码剖析 先上图,再解答。 完整主要的源代码 #include "vapp.h" #include "vutils.h" #include "vmath.h" #include "vbm.h" #include "LoadShaders.h" #include <stdio.h> using namespace vmath; #define FRUSTUM_DEPTH 800.0f #define

希望我的博客,能帮上你解决学习中工作中所遇到的问题 573

OpenGL shadow mapping 阴影贴图的实例

OpenGL shadow mapping 阴影贴图先上图,再解答。完整主要的源代码源代码剖析 先上图,再解答。 完整主要的源代码 #include <glad/glad.h> #include <GLFW/glfw3.h> #include <stb_image.h> #include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/typ

希望我的博客,能帮上你解决学习中工作中所遇到的问题 702

OpenGL es 2.0使用shadow mapping方法制作阴影时,阴影穿透实体现象的形成原因及初步应对思路

在本人开发手机游戏《KF坦克》时,发现了一个问题,具体如下: 一、开发环境描述: window7+adt-bundle-windows-x86_64-20131030 纯java编程,OpenGL es 版本为2.0 二、问题描述: 使用shadow mapping方法产生阴影,阴影会穿透实体。具体见阴影穿透实体效果图。 此时,使用glsl部分代码为代码为(该代码为fragment代码

kuafulaile的专栏 1752

双轮差动机器人SLAM实战:轮速里程计从建模到精度验证(附Matlab代码)

本文详细介绍了双轮差动机器人SLAM系统中轮速里程计的实现全流程,包括核心原理、建模方法、Matlab代码实现及精度验证。通过轮速仪数据建模和连续积分算法,提升定位精度,并附有完整的误差分析和工程优化技巧,为开发者提供实用参考。

weixin_29050271的博客 223

[OpenGL] shadow mapping(实时阴影映射)

1978年,Lance Williams在其发表的论文《Casting curved shadows on curved surfaces》中提出了Shadow mapping算法,从那以后,该算法在离线渲染和实时渲染两个领域都得到了广泛的应用。皮尔斯动画工作室的Renderman渲染器、以及一些知名电影如《玩具总动员》都使用了shadow mapping技术。

perseverancep的博客 2386

逆向实战:喜马拉雅新版xm-sign算法解析与本地化生成方案

本文深入解析了喜马拉雅新版xm-sign算法的逆向过程与本地化生成方案。通过详细拆解browserid和sessionid的生成机制,提供了基于Node.js的核心代码实现,帮助开发者在不依赖官方环境的情况下生成有效签名。文章还涵盖了参数获取技巧、常见问题排查及性能优化建议,为爬虫开发者提供了实用的技术参考。

weixin_29256771的博客 299

用Origin快速拟合荧光寿命、PL Decay (TRPL)数据分析处理

用Origin快速拟合荧光寿命、PL Decay (TRPL)数据分析处理

A20802013的博客 2万+

openGL实现阴影映射(Shadow Mapping)

openGL阴影原理

aoxuestudy的专栏 2884

ERA5气象数据下载与解压全攻略:Python脚本一键搞定(附错误处理技巧)

本文提供了一套完整的ERA5气象数据自动化处理方案,涵盖从CDS API配置、智能批量下载到高效解压与数据验证的全流程。重点介绍了如何通过Python脚本实现一键式数据下载与批量解压,并分享了实用的错误处理技巧与性能优化策略,帮助科研人员高效获取和处理大规模气象数据。

k8l9m0n的博客 337

OpenGL ES 2.0 shadow mapping阴影

最近一直在做阴影算法的研究,其实阴影算法目前主流就三四种,有shadow volume还有就是shadow mapping,由于shadow mapping比较简单容易实现,但是锯齿比较严重,之前我是在opengl 下的实现,但这次我是在opengl es 2.0下实现shadow mapping阴影算法, 其实算法无外乎两个步骤, 首先将摄像机移动到光源处绘制场景,得到深度图; 其次就是移

smsmn的专栏 3064

【深度学习|语义分割之UNet】如何使用ResNet50 作为 U-Net 网络的骨干网络(backbone)并使用预训练的 ResNet50 模型。附代码及解读。

【深度学习|语义分割之UNet】如何使用ResNet50 作为 U-Net 网络的骨干网络(backbone)并使用预训练的 ResNet50 模型。附代码及解读。

985小水博的摸鱼日常 3090

ESP32与DS18B20温度传感器的数据采集与告警系统

并且,通过自定义的微信告警功能,我们可以及时收到温度超过阈值的告警消息。随着物联网的快速发展,传感器在各种应用场景中发挥着重要的作用。本文将介绍如何使用ESP32微控制器与DS18B20温度传感器结合,通过串口输出和Web服务器实现温度数据的采集和监控,并使用自定义微信告警功能。上传完成后,打开串口监视器,您将看到ESP32连接到Wi-Fi网络,并开始监测温度。在您的计算机或移动设备上,打开一个Web浏览器,并输入ESP32的IP地址。在浏览器中输入ESP32的IP地址后,您将看到显示当前温度的页面。

loop_class349的博客 2242

OpenGL - 阴影映射 - Tutorial 16 : Shadow mapping

文章目录Basic shadowmap - shadow map的基础知识Rendering the shadow map - 渲染shadow mapSetting up the rendertarget and the MVP matrix - 设置渲染目标和MVP矩阵The shaders - Shader着色器Result - 渲染结果Using the shadow map - 使用sh...

Jave.Lin 的学习笔记 2072

最新Python爬虫实战(入门爬虫篇)——案例15:微博热搜数据采集(详细爬虫思路截图+抓包动图演示+完整爬虫代码+详细注释)

本次案例通过 Python 爬虫技术,成功实现了对微博热搜 API 接口数据的批量爬取与 Excel 保存。代码采用模块化设计,包含接口请求、JSON 解析、数据提取、Excel 持久化等完整流程;通过模拟真实浏览器的请求头和 cookies,有效规避基础反爬策略,保证请求的稳定性。整个爬虫逻辑遵循“API请求 → JSON解析 → 数据提取 → 保存到Excel”的标准流程,与异步加载类网站的爬取范式高度一致。AJAX 接口抓包与分析。

努力让自己发光,对的人才能迎着光而来 7万+

OpenGL 阴影之Shadow Mapping和Shadow Volumes

  先说下开发环境.VS2013,C++空项目,引用glut,glew.glut包含基本窗口操作,免去我们自己新建win32窗口一些操作.glew使我们能使用最新opengl的API,因winodw本身只包含opengl 1.1版本的API,根本是不能用的.   其中矩阵计算采用gitHub项目openvr中的三份文件, Vectors.h ,Matrices.h, Matrices.cpp,...

weixin_33755847的博客 417

使用 Simulink + Simscape Electrical 对 级联H桥多电平整流器并网功率分配 进行建模与仿真

摘要:本文详细介绍了基于Simulink对级联H桥(CHB)多电平整流器并网功率分配进行建模的方法。内容包括系统组成、功率分配意义、Simulink建模流程、PWM控制策略实现、功率分配算法设计以及仿真分析。重点阐述了主电路搭建、双环控制结构(外环功率分配+内环电流控制)和载波层叠PWM调制技术,并提供了功率分配算法示例。通过优化参数和控制算法,可提升系统性能。该建模方法为电力电子变换器和新能源并网系统研究提供了有效工具。

amy_mhd的博客 251

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

[转载] [OpenGL] shadow mapping(实时阴影映射)

参考链接: Java中静态函数的阴影(方法隐藏) 转载原创:ZJU_fish1996 http://blog.youkuaiyun.com/zju_fish1996/article/details/51932954 source:原文地址 code:点击可以直接下载源代码 1978年,Lance Williams在其发表的论文《Casting curved shadows on curved surfaces》中提出了Shadow mapping算法,从那以后,该...

ld909的博客 428

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

Learn OpenGL 笔记6.3 Shadow Mapping(阴影贴图)

阴影为照明场景增添了大量真实感,并使观看者更容易观察对象之间的空间关系。 它们为我们的场景和物体提供了更大的深度感。 在当前的实时(光栅化图形)研究中,还没有开发出完美的阴影算法。 有几种很好的阴影近似技术,但它们都有我们必须考虑的缺点。 基础知识: 1.shadow mapping阴影映射 大多数游戏使用的一种技术可以提供不错的结果并且相对容易实现,那就是shadow mapping阴影映射。 shadow mapping不太难理解,不会在性能上花费太多,并且很容易扩展到更高级的算法(如Omn

u013617851的博客 2329

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

OpenGL阴影,Shadow Mapping(附源程序)

OpenGL阴影,Shadow Mapping(附源程序)   实验平台:Win7,VS2010   先上结果截图(文章最后下载程序,解压后直接运行BIN文件夹下的EXE程序):   本文描述图形学的两个最常用的阴影技术之一,Shadow Mapping方法(另一种是Shadow Volumes方法)。在讲解Shadow Mapping基本原理及其基本算法的OpenGL实现之后...

linuxheik的专栏 797
上一篇: NGUI所见即所得之UICamera
下一篇: 不进则退|进取,可以解决一切问题.
CCCStudyer
博客等级 码龄16年 14粉丝 9原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值