05 高级光照

Posted etherovo

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了05 高级光照相关的知识,希望对你有一定的参考价值。

一、Blinn-Phong model

  • 冯氏模型的镜面反射采取了观察方向与反射方向的夹角,这样会造成当夹角大于九十度时,就会造成镜面反射光为零,这样一般情况下问题不大,但是当镜面反射的glossy程度比较大时,就会出现镜面反射边缘的截断现象。
  • Blinn-Phong model改用半程向量与法线的夹角,解决了问题,其他方面一样。
	//specular
	vec3 viewDir = normalize(viewPos - fragPos);
	vec3 halfwayDir = normalize(viewDir + lightDir);
	float spec = pow(max(dot(halfwayDir, normal), 0.0), material.shininess);
	vec3 specular = light.specular * spec * vec3(texture(material.specular, texCoord));

二、Gamma矫正

  • 在Gamma校正之前,首先需要理解什么是线性颜色空间。我们为什么要强调线性空间呢?顾名思义,线性空间就是指可以对颜色进行线性操作还能得到正确结果的颜色空间。事实上,线性空间只有一个,即自然空间。在自然界中,我们以光子的数量或者光的强度来描述亮度,而这个映射关系是线性的,即亮度与光子数量呈线性关系,那么对亮度的先线性操作就具备充分的物理意义。当然,这里对亮度的概念是一个定义,并非观感上的亮度,即观感亮度与光子数量是非线性的。
  • 了解了线性亮度空间之后,我们就可以知悉,如果要对颜色进行计算,在模拟自然光照时,需要在线性空间进行计算才有直观的意义。
  • 另外一个需要强调的点是,自然界的亮度,即光子数量,经过人的视觉系统的转换后,会呈现出亮度认知,而这一个过程是非线性的,大概会是1/2.2的幂次关系。这样一来,人会对暗区更加敏感,即人的观感亮度是非线性且偏亮的。应该认识的是,人的视觉系统是一个物理系统,将电磁波转换为电磁波或者是说将光子转换为电子的过程,这可以结合到后面的CRT显示器。
  • CRT显示器是将电转换为光的物理设备,是一个与人的视觉系统反向处理的设备。这个设备的转换过程同样不是线性的,大致是2.2的幂次关系,这就意味着线性颜色空间经过CRT之后变成了偏暗的非线性空间。
  • 结合人的视觉系统与CRT之后,一个直观的印象是,二者的非线性不是正好大致抵消,变成了线性了吗,这样一来,计算机中以线性方式计算的亮度值转换到人的视觉感知系统中的表达方式不也恰好是线性空间了吗?事实上,这种思路是错误的,因为在前面提起了,人的视觉系统的颜色表达空间是非线性的,而人类认知颜色亮度是从一个线性的自然空间认知的。因此,偶们要保证投射入人眼的光照形式的亮度空间必须是一个线性空间,恰如自然界。
  • 结合:我们需要在计算机中以线性空间进行颜色计算 + CRT显示器的输出光的颜色空间必须是线性的,那么直观的结论就是,我们需要将计算出来的线性空间的颜色表达经过一步大约是1/2.2次幂的校正,然后送入显示器的光电转换设备,才能恰好得到线性空间,而人眼也可以观察到合理的亮度了,这一步便是Gamma校正。
  • 在OpenGL中,实现Gamma校正有两种方式:
    • glEnable(GL_FRAMEBUFFER_SRGB);这个使得opengl再写入颜色缓冲时会自行进行gamma校正,将线性空间转换为sRGB空间,sRGB这个颜色空间大致对应于gamma2.2。这样一来,我们的shader只需要在线性空间做计算,不需要进行gamma矫正了。显然的是,这样使得我们只有使能权力,没有控制权力。
    • 另一个方法就是不开启GL_FRAMEBUFFER_SRGB,而是在片段着色器中自行进行gamma校正,然后让opengl写入RGB空间,相当于得到了sRGB空间。但是明显的是,我们有了更细粒化的操作权限。
    //gamma
    float gamma = 2.2;
    fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / gamma));

看看,多么亮!

  • 一个明显的问题就是,经过了gamma校正的场景也太亮了吧,这是为什么呢?这是因为,我们观察到的场景中基本上都是基于纹理贴图渲染出来的,而纹理贴图并不是线性空间。纹理贴图是如何绘制出来的呢?显然是,艺术家们对着显示器调节出来的。那么显然的是,在艺术家的眼中,纹理贴图的亮度是正常的,即纹理贴图经过显示器后的亮度是线性空间的,那么显然的结论是,纹理贴图中记录的颜色本身不是线性空间的颜色,而是已经经过了gamma校正的sRGB空间的颜色。那么,我们将纹理映射出来的颜色进行gamma校正,就相当于进行了两次gamma校正,自然亮度会郭亮,反之可以想到,再不惊醒gamma校正时,纹理的颜色自行经过了gamma校正,所以显示的是合理颜色。然而,如果我们要对颜色进行计算,就意味着直接使用纹理贴图的sRGB颜色的计算结果是错误的。因此,对于纹理贴图,我们需要首先将她们由sRGB空间转换到线性空间。
    • 第一种方法就是,我们在片段着色器中使用texture()提取到颜色后,首先经过一个反gamma校正。
float gamma = 2.2;
vec3 diffuseColor = pow(texture(diffuse, texCoords).rgb, vec3(gamma));
  • 另一个比较方便的方法是,我们在使用glTexImage2D读取纹理数据时,通过将内部格式设置为GL_SRGB或者GL_SRGB_ALPHA来告知OpenGL将其由sRGB或者sRGBA转换为RGB或者RGBA
glTexture2D(GL_TEXTURE_2D,0,GL_SRGB,width,height,0GL_RGB,GL_UNSIGNED_BYTE,data);
  • 然而,需要注意的是,只有颜色纹理需要经过反gamma校正,其他纹理像是深度纹理、法线纹理等等不存在类似的需求。同样的,我们在创建帧缓冲时,在为其纹理缓冲分配空间时也不需要进行转换,毕竟我们存入的颜色本身就是线性空间计算出来的结果。
	int width, height, nrChannels;
	unsigned char* data = stbi_load((dir+\'/\' + pat).c_str(), &width, &height, &nrChannels, 0);
	if (data)
	
		GLenum interformat,format;
		if (nrChannels == 1)
		
			interformat = GL_RED;
			format = GL_RED;
		
		else if (nrChannels == 3)
		
			interformat = GL_SRGB;
			format = GL_RGB;
		
		else if (nrChannels == 4)
		
			interformat = GL_SRGB_ALPHA;
			format = GL_RGBA;
		

		glTexImage2D(GL_TEXTURE_2D, 0, interformat, width, height, 0, format, GL_UNSIGNED_BYTE, data);
		glGenerateMipmap(GL_TEXTURE_2D);
	
	else
	
		std::cout << "Failed to load texture." << std::endl;
	
	stbi_image_free(data);
  • 最后一个需要注意的地方是,当不采取gamma校正时,使用距离反比的光照衰减效果会很好,反而平方反比会使得光照衰减很快,这是因为显示器的gamma参数导致的距离反比更接近真实的平方反比,显然的,使用gamma校正之后,距离平方反比的效果更好。
  • 你也许发现了为什么现在的效果还是有点亮呢,这个从天空盒就可以看出来了,我们之前使用的纹理已经经过gamma矫正了,现在的效果应该跟他一样才对,后来发现,这是因为我在渲染到帧缓冲纹理和从纹理渲染到屏幕帧缓冲上都做了一次gamma校正,呵呵我是傻逼,这里就不改了。

三、阴影

1. 阴影映射shadowmap

  • 使用blinn-phong model得到的渲染结果,看起来好像是有明暗区分的,这是通过对三种成分的光照,尤其是diffuse和specular进行了余弦变换以及截断实现的,这样一来,我们就可以看到在同一物体上有着明暗的区分,好像是给出了阴影的效果。但是,当我们将多个物体放在一起观察时,就会发现,他们之间好像是不存在阴影的。这是一个显然的结论,我们使用着色模型达成的阴影效果实则是对法线判断的结果,其本质上是在进行光照强度效果的计算,而非遮挡计算,因此即便是有阴影效果也不是我们期待的物理遮挡造成的,此外,他们只是对单个mesh的着色计算,自然不存在交互。
  • 使用shadow map来实现阴影效果。
  • 首先,我们创建一个用于记录帧缓冲,用于记录对某一个光源而言,其观察到的深度信息。因此,我们会附加一个深度附件,同时因为我们需要在片段着色器中使用这个信息,所以我们使用深度纹理。此外,因为这个深度不是用于渲染到屏幕上的,所以我们不会采取屏幕的宽高,而是以分辨率的需求来设计宽高。最后一个需要注意的地方是,帧缓冲必须要有颜色缓冲,但是我们并不需要,所以明确告知OpenGL我们不对该帧缓冲进行颜色缓冲的读写。
	unsigned int depthMapFBO;
	glGenFramebuffers(1, &depthMapFBO);
	glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);

	const unsigned int SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
	unsigned int depthMap;
	glGenTextures(1, &depthMap);
	glBindTexture(GL_TEXTURE_2D, depthMap);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

	glad_glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0);
	glDrawBuffer(GL_NONE);
	glReadBuffer(GL_NONE);
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
  • 然后,我们实现了向该帧缓冲中渲染的shader。该shader的作用只是深度测试以及深度写入,所以我们只需要进行顶点坐标的计算就可以了。
#version 330 core

layout(location = 0)in vec3 aPos;

uniform mat4 lightSpaceMatrix;
uniform mat4 model;

void main()

	gl_Position = lightSpaceMatrix * model * vec4(aPos, 1.0);

#version 330 core

void main()



  • 有了着色器之后,我们需要在渲染物体之前,首先渲染深度信息。这个着色器是用于对所有需要进行阴影效果的着色物体的,因此都进行一遍渲染。一个非常有趣的地方是,因为我们修改了帧缓冲的深度纹理的大小,不再是屏幕宽高,所以我们需要修改视口glViewport。这是因为,该函数是用于将NDC空间转换到屏幕空间时,进行xy方向坐标映射的宽高参数以及原点参数(偏执参数)。下图是我们将宽高减半的效果,可见,当我们渲染多重采样帧缓冲时,只能渲染左下角1/4部分,然后将该帧缓冲复原到中间帧缓冲,将中间帧缓冲渲染到屏幕上,即默认诊断冲时,又在此基础上只能渲染左下角1/4的部分,总的看来,之渲染了1/16的部分;此外如果宽高过大,比如两倍,就会引起只会将颜色缓冲的一部分(1/16)渲染到屏幕上。
  • 因为深度纹理是在光源处观察到的深度信息,所以该shader输入的MVP矩阵是相对于光源的,首先,M矩阵不受影响,然后,V矩阵是由光源得到的,其中对于点光源而言可以直接看作一个camera,使用摄像机的view矩阵计算方法来得到其view矩阵,但是对于定向光而言不存在位置信息,只存在方向信息,回顾camera的view计算可知,我们是需要一个摄像机位置和一个观察方向(或者观察位置)的,因此,我们只能假定定向光有一个位置,实际上就是给深度纹理一个本来对于定向光而言不存在的中心位置,因此,得到的深度纹理一定是基于这个中心位置有边界的;最后,P矩阵也不需要变动,只是对于定向光使用正交,对于点光使用投影。
//shader
Shader shadowmap_Dir_Shader("res/shader/shadowMap_Dir_Vertex.shader", "res/shader/shadowMap_Dir_Fragment.shader");
//shadowmap
		glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
		glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
		glEnable(GL_DEPTH_TEST);
		glDepthMask(GL_TRUE);
		glClear(GL_DEPTH_BUFFER_BIT);

		shadowmap_Dir_Shader.use();

		float near_plane = 0.1f, far_plane = 30.0f;
		glm::mat4 lightProjection = glm::ortho(-20.0f, 20.0f, -20.0f, 20.0f, near_plane, far_plane);
		glm::mat4 lightView = glm::lookAt(glm::vec3(40.0f, 10.0f, 40.0f), glm::vec3(30.0f,3.0f,30.0f), glm::vec3(0.0f, 1.0f, 0.0f));
		glm::mat4 lightSpaceMatrix = lightProjection * lightView;

		shadowmap_Dir_Shader.setMat4("lightSpaceMatrix", glm::value_ptr(lightSpaceMatrix));
		
		glBindVertexArray(stencilVAO);

		model = glm::mat4(1.0);
		model = glm::translate(model, glm::vec3(24.0, 3.1, 39.0));
		model = glm::scale(model, glm::vec3(4.0));
		model = glm::translate(model, glm::vec3(0.0, 0.5, 0.0));
		shadowmap_Dir_Shader.setMat4("model", glm::value_ptr(model));
		glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);

		model = glm::mat4(1.0);
		model = glm::translate(model, glm::vec3(30.0, 6.0, 33.0));
		model = glm::scale(model, glm::vec3(4.0));
		model = glm::translate(model, glm::vec3(0.0, 0.5, 0.0));
		shadowmap_Dir_Shader.setMat4("model", glm::value_ptr(model));
		glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);

		model = glm::mat4(1.0);
		model = glm::translate(model, glm::vec3(37.0, 4.0, 23.0));
		model = glm::scale(model, glm::vec3(4.0));
		model = glm::translate(model, glm::vec3(0.0, 0.5, 0.0));
		shadowmap_Dir_Shader.setMat4("model", glm::value_ptr(model));
		glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);

		model = glm::mat4(1.0f);
		model = glm::translate(model, glm::vec3(30.0, 3.0, 30.0));
		model = glm::scale(model, glm::vec3(50.0));
		model = glm::translate(model, glm::vec3(0.0, -0.5, 0.0));
		shadowmap_Dir_Shader.setMat4("model", glm::value_ptr(model));
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (const void*)(12 * sizeof(unsigned int)));
  • 得到了深度纹理之后,我们将其渲染到屏幕上,看看效果。方法很简单,只是将深度纹理绘制到屏幕上,当然要注意修改视口。第一幅是正交变换,即定向光,第二幅是投影变换,即点光源。

#version 330 core

in vec2 texCoord;

uniform sampler2D depthmap;
uniform float near_plane;
uniform float far_plane;

out vec4 fragColor;

float LinearizeDepth(float depth)

	float z = depth * 2.0 - 1.0; // Back to NDC 
	return (2.0 * near_plane * far_plane) / (far_plane + near_plane - z * (far_plane - near_plane));


void main()

	float depthValue=texture(depthmap, texCoord).r;
	fragColor = vec4(vec3(depthValue), 1.0);
	//fragColor = vec4(vec3(LinearizeDepth(depthValue) / far_plane), 1.0);
		

		//shadowmap
		glBindFramebuffer(GL_FRAMEBUFFER, 0);
		glDisable(GL_DEPTH_TEST);
		glDisable(GL_STENCIL_TEST);
		//glClearColor(0.0f, 0.1f, 0.0f, 1.0f);
		//glClear(GL_COLOR_BUFFER_BIT);
		glViewport(0, 0, SCR_WIDTH/2, SCR_HEIGHT);

		depthmapShader.use();

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, depthMap);
		depthmapShader.setInt("depthmap", 0);

		depthmapShader.setFloat("near_plane", near_plane);
		depthmapShader.setFloat("far_plane", far_plane);

		glBindVertexArray(frameVAO);
		glDrawArrays(GL_TRIANGLES, 0, 6);

		glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);

  • 在得到了深度纹理之后,我们就需要在原本的blinn-phong model的着色器中,修改着色结果。只需要在顶点着色器中计算出光源的裁剪空间中顶点的位置,并输出给片段着色器,然后在片段着色器中转换到NDC空间,进一步的转换到[0,1]的坐标空间,这样xy轴就是纹理坐标,而z轴就是深度信息,这样一俩就可以查询深度纹理,将二者进行对比,然后利用对比结果判断是否显示该片段的diffuse和specular光照,当然ambient光照必须要显示的。画个图,漂亮!但不完全漂亮...
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
layout(location = 2) in vec2 aTexCoord;
layout(location = 3) in vec3 aNorm;

out VS_OUT
	vec3 fragPos;
	vec3 normal;
	vec2 texCoord;
	vec4 fragPosLightSpace;
 vs_out;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform mat4 lightSpaceMatrix;

void main()

	gl_Position = projection * view * model * vec4(aPos, 1.0f);
	vs_out.fragPos = vec3(model * vec4(aPos, 1.0f));
	vs_out.normal = normalize(mat3(transpose(inverse(model))) * aNorm);
	vs_out.texCoord = aTexCoord;
	vs_out.fragPosLightSpace = lightSpaceMatrix * model * vec4(aPos, 1.0f);

#version 330 core

out vec4 fragColor;

in VS_OUT
	vec3 fragPos;
	vec3 normal;
	vec2 texCoord;
	vec4 fragPosLightSpace;
 fs_in;

uniform vec3 viewPos;

uniform sampler2D shadowMap;

struct Material 
	//vec3 ambient;
	sampler2D diffuse;
	sampler2D specular;
	float shininess;
;
uniform Material material;

struct DirLight 
	vec3 direction;

	vec3 ambient;
	vec3 diffuse;
	vec3 specular;
;
uniform DirLight dirlight;

struct PointLight 
	vec3 position;

	vec3 ambient;
	vec3 diffuse;
	vec3 specular;

	float constant;
	float linear;
	float quadratic;
;
#define NR_POINT_LIGHTS 4
uniform PointLight pointlights[NR_POINT_LIGHTS];

struct SpotLight 
	vec3 direction;
	vec3 position;

	vec3 ambient;
	vec3 diffuse;
	vec3 specular;

	float cutOff;
	float outerCutOff;
	float constant;
	float linear;
	float quadratic;
;
uniform SpotLight spotlight;

vec3 calcDirLight(DirLight light, vec3 normal, vec3 viewPos, vec3 fragPos, float shadow);
vec3 calcSpotLight(SpotLight light, vec3 normal, vec3 viewPos, vec3 fragPos, float shadow);
vec3 calcPointLight(PointLight light, vec3 normal, vec3 viewPos, vec3 fragPos, float shadow);
float ShadowCalculation(vec4 fragPosLightSpace);

void main()

	float shadow = ShadowCalculation(fs_in.fragPosLightSpace);

	vec3 result = calcDirLight(dirlight, fs_in.normal, viewPos, fs_in.fragPos, shadow);
	
	//for (int i = 0; i != NR_POINT_LIGHTS; i++)
	//
	//	result += calcPointLight(pointlights[i], fs_in.normal, viewPos, fs_in.fragPos, shadow);
	//
	//
	//result += calcSpotLight(spotlight, fs_in.normal, viewPos, fs_in.fragPos, shadow);
	
	fragColor = vec4(result, 1.0f);

	//gamma
	float gamma = 2.2;
	fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / gamma));


vec3 calcDirLight(DirLight light, vec3 normal, vec3 viewPos, vec3 fragPos, float shadow)

	//ambient
	vec3 ambient = light.ambient * vec3(texture(material.diffuse, fs_in.texCoord));
	//diffuse
	vec3 lightDir = normalize(-light.direction);
	float diff = max(dot(normal, lightDir), 0.0);
	vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, fs_in.texCoord));
	//specular
	vec3 viewDir = normalize(viewPos - fragPos);
	vec3 halfwayDir = normalize(viewDir + lightDir);
	float spec = pow(max(dot(halfwayDir, normal), 0.0), material.shininess);
	vec3 specular = light.specular * spec * vec3(texture(material.specular, fs_in.texCoord));

	return  ambient + (1.0 - shadow) * (diffuse + specular);


vec3 calcPointLight(PointLight light, vec3 normal, vec3 viewPos, vec3 fragPos, float shadow)

	float distance = length(light.position - fragPos);
	float attenuation = 1.0f / (light.constant + light.linear * distance + light.quadratic * distance * distance);
	//ambient
	vec3 ambient = light.ambient * vec3(texture(material.diffuse, fs_in.texCoord));
	//diffuse
	vec3 lightDir = normalize(light.position - fragPos);
	float diff = max(dot(normal, lightDir), 0.0);
	vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, fs_in.texCoord));
	//specular
	vec3 viewDir = normalize(viewPos - fragPos);
	vec3 halfwayDir = normalize(viewDir + lightDir);
	float spec = pow(max(dot(halfwayDir, normal), 0.0), material.shininess);
	vec3 specular = light.specular * spec * vec3(texture(material.specular, fs_in.texCoord));

	return (ambient + (1.0-shadow)*(diffuse + specular)) * attenuation;


vec3 calcSpotLight(SpotLight light, vec3 normal, vec3 viewPos, vec3 fragPos, float shadow)


	//ambient
	vec3 ambient = light.ambient * vec3(texture(material.diffuse, fs_in.texCoord));
	//diffuse
	vec3 lightDir = normalize(light.position - fragPos);
	float diff = max(dot(normal, lightDir), 0.0);
	vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, fs_in.texCoord));
	//specular
	vec3 viewDir = normalize(viewPos - fragPos);
	vec3 halfwayDir = normalize(viewDir + lightDir);
	float spec = pow(max(dot(halfwayDir, normal), 0.0), material.shininess);
	vec3 specular = light.specular * spec * vec3(texture(material.specular, fs_in.texCoord));

	float theta = dot(-lightDir, normalize(light.direction));
	float epsilon = light.cutOff - light.outerCutOff;
	float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);

	float distance = length(light.position - fragPos);
	float attenuation = 1.0f / (light.constant + light.linear * distance + light.quadratic * distance * distance);

	return (ambient + (1.0 - shadow) * (diffuse + specular)) * intensity * attenuation;


float ShadowCalculation(vec4 fragPosLightSpace)

	vec3 projCoord = fragPosLightSpace.xyz / fragPosLightSpace.w;
	projCoord = projCoord * 0.5 + 0.5;
	float closestDepth = texture(shadowMap, projCoord.xy).r;
	float currentDepth = projCoord.z;

	float shadow = currentDepth > closestDepth ? 1.0 : 0.0;
	return shadow;

2.阴影失真

  • 可以看到,在本应该被光照射到的地方存在着大量的失真,原因很简单:
  • 第一种方法:阴影偏移。我们把物体的深度减小一个bias,这样就可以避免片段深度与采样的深度纹理有交叉失真。注意原文的图画是错误的,他把深度贴图给抬升了。一个技巧是,因为光线的入射方向,或者说观察方向越是平行于平面,则失真越严重,因此可以使用法线方向与光线方向的余弦来动态调整bias。
float ShadowCalculation(vec4 fragPosLightSpace, vec3 normal, vec3 lightDir)

	vec3 projCoord = fragPosLightSpace.xyz / fragPosLightSpace.w;
	projCoord = projCoord * 0.5 + 0.5;
	float closestDepth = texture(shadowMap, projCoord.xy).r;
	float currentDepth = projCoord.z;

	float bias =  max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);

	float shadow = currentDepth-bias > closestDepth ? 1.0 : 0.0;
	return shadow;

  • 但是显然的一个现象是,我们抬升了物体的深度,来避免深度纹理的采样深度与真是深度的差异造成的失真,但是在其他的一些情况下,深度纹理的深度信息比真是深度略小,但却是真的有价值的评比,可以据此来生成阴影,那么此时就会造成阴影缺失,即彼得潘
  • 对于彼得潘问题,我们采取正面剔除,来获得背面的深度贴图,以此来解决阴影失真。这是因为对于闭合物体,使用背面的深度显然的就会有一个天然的深度bias,那么对于不需要阴影的正面来说就会很大程度的改善失真。可以看到,彼得潘问题被解决了,大部分地方的阴影失真也解决了,但是对于地板而言,他是一个单面的物体,或者被正面剔除而大部分地方解决了阴影失真,或者没有被剔除仍然存在阴影失真,但是即便被剔除了,地板与其他物体的交界处记录的深度是其他物体的深度,仍然会存在失真,如下:
	//shadowmap
		glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
		glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
		glEnable(GL_DEPTH_TEST);
		glDepthMask(GL_TRUE);
		glClear(GL_DEPTH_BUFFER_BIT);

		glEnable(GL_CULL_FACE);
		glCullFace(GL_FRONT);
		glFrontFace(GL_CCW);

		shadowmap_Dir_Shader.use();

		float near_plane = 0.1f, far_plane = 30.0f;
		glm::mat4 lightProjection = glm::ortho(-20.0f, 20.0f, -20.0f, 20.0f, near_plane, far_plane);
		glm::mat4 lightView = glm::lookAt(glm::vec3(40.0f, 10.0f, 40.0f), glm::vec3(30.0f,3.0f,30.0f), glm::vec3(0.0f, 1.0f, 0.0f));
		glm::mat4 lightSpaceMatrix = lightProjection * lightView;

		shadowmap_Dir_Shader.setMat4("lightSpaceMatrix", glm::value_ptr(lightSpaceMatrix));
		
		glBindVertexArray(stencilVAO);

		model = glm::mat4(1.0);
		model = glm::translate(model, glm::vec3(24.0, 3.0, 39.0));
		model = glm::scale(model, glm::vec3(4.0));
		model = glm::translate(model, glm::vec3(0.0, 0.5, 0.0));
		shadowmap_Dir_Shader.setMat4("model", glm::value_ptr(model));
		glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);

		model = glm::mat4(1.0);
		model = glm::translate(model, glm::vec3(30.0, 6.0, 33.0));
		model = glm::scale(model, glm::vec3(4.0));
		model = glm::translate(model, glm::vec3(0.0, 0.5, 0.0));
		shadowmap_Dir_Shader.setMat4("model", glm::value_ptr(model));
		glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);

		model = glm::mat4(1.0);
		model = glm::translate(model, glm::vec3(37.0, 4.0, 23.0));
		model = glm::scale(model, glm::vec3(4.0));
		model = glm::translate(model, glm::vec3(0.0, 0.5, 0.0));
		shadowmap_Dir_Shader.setMat4("model", glm::value_ptr(model));
		glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);

		model = glm::mat4(1.0f);
		model = glm::translate(model, glm::vec3(30.0, 3.0, 30.0));
		model = glm::scale(model, glm::vec3(50.0));
		model = glm::translate(model, glm::vec3(0.0, -0.5, 0.0));
		shadowmap_Dir_Shader.setMat4("model", glm::value_ptr(model));
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (const void*)(12 * sizeof(unsigned int)));

		glCullFace(GL_BACK);
		glDisable(GL_CULL_FACE);

3. 深度纹理的范围

  • 可以看到,在这里多了一块不应当存在的阴影,这是因为这个区域不在深度纹理的采样范围之内,而且我们对深度纹理的映射使用了GL_REPEAT,因此这个区域错误的评估了此处的最浅深度,为了解决这个问题,我们使用GL_CLAMP_TO_BREDER,并将边界设置为最深的深度1.0,这样虽然在深度纹理xy区域范围之外的地方不会存在阴影,但是总比出现莫名其妙的阴影要好得多。

    好了,奇怪的阴影不见了。
	const unsigned int SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
	unsigned int depthMap;
	glGenTextures(1, &depthMap);
	glBindTexture(GL_TEXTURE_2D, depthMap);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
  • 但是,可以看到,还有一大块区域是阴影,这是因为这一段区域超出了便准化设备空间的z向范围,即得到的深度都是大于我们的阴影纹理的最大深度值1.0的,所以,对于这部分区域,我们判断它的z值,如果大于1.0直接下结论这里木有阴影,虽然也很暴力,但是同样的,比有阴影要强。完美!
float ShadowCalculation(vec4 fragPosLightSpace, vec3 normal, vec3 lightDir)

	vec3 projCoord = fragPosLightSpace.xyz / fragPosLightSpace.w;
	projCoord = projCoord * 0.5 + 0.5;
	float closestDepth = texture(shadowMap, projCoord.xy).r;
	float currentDepth = projCoord.z;

	//float bias =  max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
	float bias = 0;

	float shadow = currentDepth-bias > closestDepth ? 1.0 : 0.0;
	if (projCoord.z > 1.0f) :
		shadow = 0.0f;

	return shadow;

3.PCF

  • 但还不够玩完美!可以看到,在阴影的边缘有锯齿,这个原因也很简单,这是因为阴影贴图的采样得到的。
  • 最直接的方法是提高阴影贴图的分辨率。
  • 另一个不那么直接的方法是PCF。即不是使用片段映射到的一处深度纹理,而是多处纹理,综合判断。一个最简单的方法就是取周围的纹理取平均值,作为shadow值,这样一来,就会出现0.0与1.0之间的shadow值,即不完全的阴影。在此处的平行光还没有太大的用处,但是在处理面光源时,这个可以用于模拟软阴影
    虽然还是有锯齿,但是好多了。

    这里用到了textureSize(shadowMap,0)用于返回纹理(参数1)的0阶mipmap(参数2)的宽高,是一个vec2。
float ShadowCalculation(vec4 fragPosLightSpace, vec3 normal, vec3 lightDir)

	vec3 projCoord = fragPosLightSpace.xyz / fragPosLightSpace.w;
	projCoord = projCoord * 0.5 + 0.5;
	float closestDepth = texture(shadowMap, projCoord.xy).r;
	float currentDepth = projCoord.z;
	float shadow = 0.0f;

	float bias = 0.0f;// max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);

	//PCF
	vec2 texelSize = 1.0 / textureSize(shadowMap, 0);
	for (int i = 0; i != 3; i++)
	
		for (int j = 0; j != 3; j++)
		
			float pcfDepth = texture(shadowMap, projCoord.xy + vec2(i, j) * texelSize).r;

			shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0;;
		
	
	
	shadow /= 9.0;

	//远处
	if (projCoord.z > 1.0f)
		shadow = 0.0f;

	return shadow;

4.点光源阴影

  • 你的2D深度贴图很不错,但是如果我拿出点光源,你又要怎么办呢?
  • 针对点光源,就不能再使用2D的深度纹理,因为点光源会对所有方向有阴影贡献。因此,使用立方体贴图作为深度纹理,即万向阴影贴图
  • 首先我们创建一个附加了立方体贴图的帧缓冲。创建帧缓冲的过程没有新意,创建立方体贴图也没有新意,但是小的改动在于,我们在为立方体贴图的每一个面分配空间时(之前使用的也是glTexImage2D),与一般的2D的深度纹理缓冲的申请空间的参数大致一样,只是对于第一个参数不再是GL_TEXTURE_2D而是GL_TEXTURE_CUBE_MAP_POSITIVE_X等一系列立方体贴图的编号,这个自然也是有序的可以循环访问。最后,一个比较特殊的地方是,在为帧缓冲附加立方体纹理时,不再使用glFramebufferTexture2D,而是glFramebufferTexture,与glFramebufferTexture2D最大区别在于不再需要纹理的类型(之前用到了GL_TEXTURE_2D,而这里没有要求GL_TEXTUE_CUBE_MAP)。同样的,我们也告知不会使用颜色缓冲。
//立方体阴影贴图
	unsigned int depthCubeMapFBO;
	glGenFramebuffers(1, &depthCubeMapFBO);
	glBindFramebuffer(GL_FRAMEBUFFER, depthCubeMapFBO);

	unsigned int depthCubeMap;
	glGenTextures(1, &depthCubeMap);
	glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubeMap);
	for (unsigned int i = 0; i != 6; i++)
	
		glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_DEPTH_COMPONENT, SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
	
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);

	glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCubeMap,0);
	glDrawBuffer(GL_NONE);
	glReadBuffer(GL_NONE);
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
  • 同样的,我们需要首先对需要进行阴影效果的物体进行shadow map的渲染。要记得,我们的立方体贴图有六个面,这是与2D纹理的最大区别。一个直观的想法是,对帧缓冲分别附加这六个面,然后分别渲染深度图。但是这样实现起来比较繁琐,有一个简洁的方法是,使用几何着色器来实现。在几何着色器中,可以通过设置gl_Layer状态标记,来实现对不同面进行几何处理,类似于状态机。这样做的最大好处在于,不同的面在进行深度图处理的时候,由之前的2D深度图计算过程可知,其区别只在于顶点的View变换是不一样,而几何着色器可以进行顶点属性的计算,因此,通过gl_Layer来实现在不同的面之间的状态转移,然后对每个面按照其自身的View matrix进行顶点坐标的变换,就可以在一个几何着色器中实现对六个面的的深度图功能了。对于其中的View矩阵,我们可以将其作为全局变量数组传入几何着色器。当然,可以在CPU中提前将PV变换计算出来,相应的,我们在顶点着色器中只需要处理model变换。
  • 而在片段着色器中,有着另外一个有趣的地方,那就是我们可以通过gl_FragDepth来写入深度缓冲。额外的用它来自行写入深度,是因为我们不再期望使用屏幕空间的z值来作为深度了,这是因为对于平行光,深度与z值具有天然的对应关系,但是对于点光源,z深度不同于光源距离,甚至经过投影变换z深度是非线性的。因此,我们期待直接计算出世界空间中光源与片段的距离,然后利用远平面深度far_plane来进行归一化。这就要求我们在几何着色器中需要输出顶点的世界坐标。
#version 330 core

layout(location = 0)in vec3 aPos;

uniform mat4 model;

void main()

	gl_Position = model * vec4(aPos, 1.0);

#version 330 core

layout(triangles) in;
layout(triangle_strip, max_vertices = 18)out;

uniform mat4 shadowMatrixs[6];

out vec4 fragPos;

void main()

	for (int face = 0; face != 6; face++)
	
		gl_Layer = face;
		for (int i = 0; i != 3; i++)
		
			gl_Position = shadowMatrixs[face] * gl_in[i].gl_Position;
			fragPos = gl_in[i].gl_Position;
			EmitVertex();
		
		EndPrimitive();
	

#version 330 core

in vec4 fragPos;

uniform vec3 lightPos;
uniform float far_plane;

void main()

	float lightDistance = length(fragPos.xyz - lightPos);

	lightDistance = lightDistance / far_plane;

	gl_FragDepth = lightDistance;

//shader
Shader shadowmap_Dot_Shader("res/shader/shadowMap_Dot_Vertex.shader", "res/shader/shadowMap_Dot_Geometry.shader", "res/shader/shadowMap_Dot_Fragment.shader");
//变化矩阵数组
	float aspect = (float)SHADOW_WIDTH / (float)SHADOW_HEIGHT;
	float near = 0.1f, far = 200.0f;
	glm::mat4 shadowProj = glm::perspective(glm::radians(90.0f), aspect, near, far);
	vector<glm::mat4> shadowTransforms;
	glm::vec3 dotLightPos = glm::vec3(-30.0f, -13.0f, 30.0f);
	shadowTransforms.push_back(shadowProj * glm::lookAt(dotLightPos, dotLightPos + glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)));
	shadowTransforms.push_back(shadowProj * glm::lookAt(dotLightPos, dotLightPos + glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)));
	shadowTransforms.push_back(shadowProj * glm::lookAt(dotLightPos, dotLightPos + glm::vec3(0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)));
	shadowTransforms.push_back(shadowProj * glm::lookAt(dotLightPos, dotLightPos + glm::vec3(0.0f, -1.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f)));
	shadowTransforms.push_back(shadowProj * glm::lookAt(dotLightPos, dotLightPos + glm::vec3(0.0f, 0.0f, 1.0f), glm::vec3(0.0f, -1.0f, 0.0f)));
	shadowTransforms.push_back(shadowProj * glm::lookAt(dotLightPos, dotLightPos + glm::vec3(0.0f, 0.0f, -1.0f), glm::vec3(0.0f, -1.0f, 0.0f)));
		//dot shadowmap
		glBindFramebuffer(GL_FRAMEBUFFER, depthCubeMapFBO);
		glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
		glEnable(GL_DEPTH_TEST);
		glDepthMask(GL_TRUE);
		glClear(GL_DEPTH_BUFFER_BIT);

		shadowmap_Dot_Shader.use();

		shadowmap_Dot_Shader.setFloat("far_plane", far);
		shadowmap_Dot_Shader.setVec3("lightPos", dotLightPos);

		glBindVertexArray(lightVAO);

		for (unsigned int i = 0; i != 6; i++)
		
			shadowmap_Dot_Shader.setMat4(("shadowMatrixs["+std::to_string(i)+"]").c_str(), glm::value_ptr(shadowTransforms[i]));
		
		
		for (unsigned int i = 0; i != 6; i++)
		
			model = glm::mat4(1.0);
			model = glm::translate(model, glm::vec3(-30.0, -20.0, 30.0) + goldPositions[i]);
			model = glm::scale(model, glm::vec3(4.0));
			model = glm::translate(model, glm::vec3(0.0, 0.5, 0.0));
			shadowmap_Dot_Shader.setMat4("model", glm::value_ptr(model));
			glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
		

		model = glm::mat4(1.0f);
		model = glm::translate(model, glm::vec3(-30.0, -5.0, 30.0));
		model = glm::scale(model, glm::vec3(50.0));
		model = glm::translate(model, glm::vec3(0.0, 0.0, -1.0));
		shadowmap_Dot_Shader.setMat4("model", glm::value_ptr(model));
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (const void*)(0 * 6 * sizeof(unsigned int)));

		model = glm::mat4(1.0f);
		model = glm::translate(model, glm::vec3(-30.0, -5.0, 30.0));
		model = glm::scale(model, glm::vec3(50.0));
		model = glm::translate(model, glm::vec3(0.0, 0.0, 1.0));
		shadowmap_Dot_Shader.setMat4("model", glm::value_ptr(model));
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (const void*)(1 * 6 * sizeof(unsigned int)));

		model = glm::mat4(1.0f);
		model = glm::translate(model, glm::vec3(-30.0, -5.0, 30.0));
		model = glm::scale(model, glm::vec3(50.0));
		model = glm::translate(model, glm::vec3(0.0, -1.0, 0.0));
		shadowmap_Dot_Shader.setMat4("model", glm::value_ptr(model));
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (const void*)(2 * 6 * sizeof(unsigned int)));

		model = glm::mat4(1.0f);
		model = glm::translate(model, glm::vec3(-30.0, -5.0, 30.0));
		model = glm::scale(model, glm::vec3(50.0));
		model = glm::translate(model, glm::vec3(0.0, 1.0, 0.0));
		shadowmap_Dot_Shader.setMat4("model", glm::value_ptr(model));
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (const void*)(3 * 6 * sizeof(unsigned int)));

		model = glm::mat4(1.0f);
		model = glm::translate(model, glm::vec3(-30.0, -5.0, 30.0));
		model = glm::scale(model, glm::vec3(50.0));
		model = glm::translate(model, glm::vec3(-1.0, 0.0, 0.0));
		shadowmap_Dot_Shader.setMat4("model", glm::value_ptr(model));
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (const void*)(4 * 6 * sizeof(unsigned int)));

		model = glm::mat4(1.0f);
		model = glm::translate(model, glm::vec3(-30.0, -5.0, 30.0));
		model = glm::scale(model, glm::vec3(50.0));
		model = glm::translate(model, glm::vec3(1.0, 0.0, 0.0));
		shadowmap_Dot_Shader.setMat4("model", glm::value_ptr(model));
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (const void*)(5 * 6 * sizeof(unsigned int)));
  • 有了shader之后,我们就可以对物体进行shadowmap计算,然后将设深度图画出来看看。还不错。
  • 接着,我们要实现利用深度图的阴影效果shader。顶点着色器与一般的布林冯模型没有区别,因为立方体贴图是使用世界坐标的方向矢量来映射,而非观察空间的xy坐标,因此不再需要在顶点着色器中计算观察空间坐标了。
  • 片段着色器的shadow计算也只是修改为对光源和片段之间的矢量进行映射,其他地方没有大的改动。
  • 最后,关于PCF,是采取对该矢量周围的三维空间中进行采样,来调整采样的方向。这里采取了20个方向,即立方体的顶点和边中点。此外,我们对采样的尺度进行了调整,当尺度较大时,阴影更软,尺度更小时,阴影更硬,所以我们可以根据观察到片段的距离来调整,使得远处更软(这样做好像不是很合理,应该按照光源、遮挡物、以及阴影的距离来实现软效果)。
  • 完美!
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
layout(location = 2) in vec2 aTexCoord;
layout(location = 3) in vec3 aNorm;

out VS_OUT
	vec3 fragPos;
	vec3 normal;
	vec2 texCoord;
 vs_out;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()

	gl_Position = projection * view * model * vec4(aPos, 1.0f);
	vs_out.fragPos = vec3(model * vec4(aPos, 1.0f));
	vs_out.normal = normalize(mat3(transpose(inverse(model))) * aNorm);
	vs_out.texCoord = aTexCoord;

#version 330 core

out vec4 fragColor;

in VS_OUT
	vec3 fragPos;
	vec3 normal;
	vec2 texCoord;
 fs_in;

uniform vec3 viewPos;
uniform float far_plane;
uniform samplerCube shadowMap;

struct Material 
	//vec3 ambient;
	sampler2D diffuse;
	sampler2D specular;
	float shininess;
;
uniform Material material;

struct DirLight 
	vec3 direction;

	vec3 ambient;
	vec3 diffuse;
	vec3 specular;
;
uniform DirLight dirlight;

struct PointLight 
	vec3 position;

	vec3 ambient;
	vec3 diffuse;
	vec3 specular;

	float constant;
	float linear;
	float quadratic;
;
uniform PointLight pointlight;

struct SpotLight 
	vec3 direction;
	vec3 position;

	vec3 ambient;
	vec3 diffuse;
	vec3 specular;

	float cutOff;
	float outerCutOff;
	float constant;
	float linear;
	float quadratic;
;
uniform SpotLight spotlight;

vec3 sampleOffsetDirections[20] = 
	vec3(1,  1,  1), vec3(1, -1,  1), vec3(-1, -1,  1), vec3(-1,  1,  1),
	vec3(1,  1, -1), vec3(1, -1, -1), vec3(-1, -1, -1), vec3(-1,  1, -1),
	vec3(1,  1,  0), vec3(1, -1,  0), vec3(-1, -1,  0), vec3(-1,  1,  0),
	vec3(1,  0,  1), vec3(-1,  0,  1), vec3(1,  0, -1), vec3(-1,  0, -1),
	vec3(0,  1,  1), vec3(0, -1,  1), vec3(0, -1, -1), vec3(0,  1, -1)
;

vec3 calcSpotLight(SpotLight light, vec3 normal, vec3 viewPos, vec3 fragPos);
vec3 calcPointLight(PointLight light, vec3 normal, vec3 viewPos, vec3 fragPos);
float ShadowCalculation(vec3 fragPos, vec3 lightPos, vec3 normal, vec3 lightDir);

void main()

	vec3 result = calcPointLight(pointlight, fs_in.normal, viewPos, fs_in.fragPos);

	fragColor = vec4(result, 1.0f);

	//gamma
	float gamma = 2.2;
	fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / gamma));



vec3 calcPointLight(PointLight light, vec3 normal, vec3 viewPos, vec3 fragPos)

	float distance = length(light.position - fragPos);
	float attenuation = 1.0f / (light.constant + light.linear * distance + light.quadratic * distance * distance);
	//ambient
	vec3 ambient = light.ambient * vec3(texture(material.diffuse, fs_in.texCoord));
	//diffuse
	vec3 lightDir = normalize(light.position - fragPos);
	float diff = max(dot(normal, lightDir), 0.0);
	vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, fs_in.texCoord));
	//specular
	vec3 viewDir = normalize(viewPos - fragPos);
	vec3 halfwayDir = normalize(viewDir + lightDir);
	float spec = pow(max(dot(halfwayDir, normal), 0.0), material.shininess);
	vec3 specular = light.specular * spec * vec3(texture(material.specular, fs_in.texCoord));

	float shadow = ShadowCalculation(fragPos, light.position, normal, lightDir);

	return (ambient + (1.0 - shadow) * (diffuse + specular))* attenuation;


vec3 calcSpotLight(SpotLight light, vec3 normal, vec3 viewPos, vec3 fragPos)


	//ambient
	vec3 ambient = light.ambient * vec3(texture(material.diffuse, fs_in.texCoord));
	//diffuse
	vec3 lightDir = normalize(light.position - fragPos);
	float diff = max(dot(normal, lightDir), 0.0);
	vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, fs_in.texCoord));
	//specular
	vec3 viewDir = normalize(viewPos - fragPos);
	vec3 halfwayDir = normalize(viewDir + lightDir);
	float spec = pow(max(dot(halfwayDir, normal), 0.0), material.shininess);
	vec3 specular = light.specular * spec * vec3(texture(material.specular, fs_in.texCoord));

	float theta = dot(-lightDir, normalize(light.direction));
	float epsilon = light.cutOff - light.outerCutOff;
	float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);

	float distance = length(light.position - fragPos);
	float attenuation = 1.0f / (light.constant + light.linear * distance + light.quadratic * distance * distance);

	float shadow = ShadowCalculation(fragPos, light.position, normal, lightDir);

	return (ambient + (1.0 - shadow) * (diffuse + specular)) * intensity * attenuation;


float ShadowCalculation(vec3 fragPos, vec3 lightPos, vec3 normal, vec3 lightDir)


	vec3 fragToLight = fragPos - lightPos;
	float currentDepth = length(fragToLight);

	float shadow = 0.0f;
	float bias =  max(0.5 * (1.0 - dot(normal, lightDir)), 0.05);
	int samples = 20;
	float viewDistance = length(viewPos - fragPos);
	float diskRadius = (1.0 + viewDistance / far_plane) /25.0f;

	for (int i = 0; i != samples; i++)
	
		float closestDepth = texture(shadowMap, fragToLight + sampleOffsetDirections[i] * diskRadius).r;
		closestDepth *= far_plane;
		shadow += currentDepth - bias > closestDepth ? 1.0 : 0.0;
	

	shadow /= float(samples);

	return shadow;

四、法线贴图

1. 法线贴图

  • 使用法线贴图在偏片段着色器中计算光照,代替顶点法线属性的插值,这一步代替很相似于使用反射纹理代替顶点的颜色属性。
  • 但是,可以看出,法线纹理的效果只在三角形面朝向z正向时(还要求一个角度)有效,一旦平面倾斜,就会造成不好的后果。这是因为法线纹理的采样结果是固定的矢量,当我们在世界坐标中计算光照时,发现并没有随着平面的model变换而调整。
	//normal map
	unsigned int normalMap;
	glGenTextures(1, &normalMap);
	glBindTexture(GL_TEXTURE_2D, normalMap);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

	stbi_set_flip_vertically_on_load(true);
	data = stbi_load("res/texture/brickwall_normal.jpg", &width, &height, &nrChannels, 0);
	if (data)
	
		glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
		glGenerateMipmap(GL_TEXTURE_2D);
	
	else
	
		std::cout << "Failed to load brickwall_normal." << std::endl;
	
	stbi_image_free(data);

	unsigned int brickwall;
	glGenTextures(1, &brickwall);
	glBindTexture(GL_TEXTURE_2D, brickwall);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

	stbi_set_flip_vertically_on_load(true);
	data = stbi_load("res/texture/brickwall.jpg", &width, &height, &nrChannels, 0);
	if (data)
	
		glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
		glGenerateMipmap(GL_TEXTURE_2D);
	
	else
	
		std::cout << "Failed to load brickwall." << std::endl;
	
	stbi_image_free(data);
		//normal map
		normalShader.use();

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, normalMap);
		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, brickwall);
		glActiveTexture(GL_TEXTURE2);
		glBindTexture(GL_TEXTURE_2D, brickwall);

		model = glm::mat4(1.0f);
		model = glm::translate(model, glm::vec3(0.0f, 0.0f, -20.0f));
		model = glm::rotate(model, (float)glfwGetTime() * -1, glm::normalize(glm::vec3(1.0, 0.0, 1.0)));
		model = glm::scale(model, glm::vec3(10.0f, 10.0f, 10.0f));
		normalShader.setMat4("model",glm::value_ptr(model));
		normalShader.setMat4("view", glm::value_ptr(view));
		normalShader.setMat4("projection", glm::value_ptr(projection));

		normalShader.setInt("normalMap", 0);
		normalShader.setInt("material.diffuse", 1);
		normalShader.setInt("material.specular", 2);
		normalShader.setFloat("material.shininess", 32.0f);
		normalShader.setVec3("viewPos", camera.Position);
		normalShader.setVec3("dirlight.ambient", 0.05f, 0.05f, 0.05f);
		normalShader.setVec3("dirlight.diffuse", 10.0f, 10.0f, 10.0f);
		normalShader.setVec3("dirlight.specular", 8.0f, 8.0f, 8.0f);
		normalShader.setVec3("dirlight.direction", -0.2f, -1.0f, -0.3f);
		normalShader.setVec3("spotlight.ambient", 0.0f, 0.0f, 0.0f);
		normalShader.setVec3("spotlight.diffuse", 10.0f, 10.0f, 10.0f);
		normalShader.setVec3("spotlight.specular", 5.0f, 5.0f, 5.0f);
		normalShader.setVec3("spotlight.direction", camera.Front);
		normalShader.setVec3("spotlight.position", camera.Position);
		normalShader.setFloat("spotlight.cutOff", glm::cos(glm::radians(12.5f)));
		normalShader.setFloat("spotlight.outerCutOff", glm::cos(glm::radians(15.0f)));
		normalShader.setFloat("spotlight.constant", 1.0f);
		normalShader.setFloat("spotlight.linear", 0.09f);
		normalShader.setFloat("spotlight.quadratic", 0.032f);

		glBindVertexArray(VAO);
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

  • 那么,一个直接的想法就是,为什么不跟之前的顶点法线属性一样,采取normalModel变换呢。这个乍一看是很合理的,但是有一个前提是被忽略了的,当我们设置顶点属性的时候,顶点法线是该组顶点属性在局部坐标系下能够确保正确的,只是当顶点属性中的顶点位置发生变换的时候,为了保持法线与位置的在各种坐标系下,尤其是世界坐标系下的一致性,我们需要用到发现变换矩阵,当我们使用法线纹理的时候,我们同样可以保证定点属性在局部坐标系下是正确的,但是这个保证止步于纹理坐标,不能保证法线纹理的映射结果在局部坐标中也是正确的,所以不能直接使用法线变换矩阵。
  • 因此,我们就需要引入切线空间,这个切线空间就可以解决局部坐标中的发现正确性问题,即将不能保证局部坐标下正确的法线纹理进行坐标变换到局部坐标系下。这样一来,就可以使用法线变换矩阵了。把他们综合起来,就是将法线纹理一步转换到世界坐标系。当然,也可以将世界坐标系下的位置,包括fragPos、lightPos和viewPos等光照计算需要的坐标,反向转换到法线纹理所在的空间,即切线空间。当然,要注意的是,将法线纹理转换到世界空间使用的是法线的model矩阵,而将位置坐标转换到切线空间使用的是基本的modle矩阵,这很容易理解。

2. 切线空间

  • 我们定义切线空间的三个基矢量是T,B,N,对于每一个三角形面而言,切线空间是它的局部坐标空间,但是不同于一般的局部坐标,正如之前所说的,法线纹理是保存在一个颜色纹理之中的RGB通道中的,分别对应空间中的xyz坐标,这个xyz坐标的坐标基矢量既不是顶点的局部坐标更不是之前见到的任何其他坐标如世界坐标、观察坐标等,我们把它定义为切线空间。很显然的,切线空间是平铺在三角形面上的,因此切线空间的N矢量就是面的法线,相应的T和B矢量是面上的两个正交基矢量,那么我们关注的重点就是如何确定T和B矢量在局部坐标系中的表示。
  • T和B矢量是三角形面上的两个基矢量,那么很显然的是,我们要求解出TB实则只要拿到切线空间的坐标以及局部空间的坐标就可以计算出坐标变换矩阵,即拿到TB在局部空间中的坐标了。

\\[P_xyz=TBN \\cdot P_uvn \\]

即:

\\[x=T_x \\cdot u+B_x \\cdot v+N_x \\cdot n \\]

\\[y=T_y \\cdot u+B_y \\cdot v+N_y \\cdot n \\]

\\[z=T_z \\cdot u+B_z \\cdot v+N_z \\cdot n \\]

然而,一个问题在于,我们并不知道在切线空间中的n坐标是什么,但是幸运的是,他们的n坐标都是一样的,因此,我们只需要三个点的位置,并对他们做差就可以得到足以解出TB的方程了。


进一步的,可以得到:

  • 一个需要注意的点是,计算出来的tangent和bitangent向量不是单位向量,需要标准化。
  • 之前说到,我们是将normal、tangent和bitangent作为顶点属性的,因此我们在CPU端计算出tangent和bitangent,并传入顶点着色器中:
void normalSquare(unsigned int &VAO,unsigned int &VBO)

	glm::vec3 pos1(-1.0f, 1.0f, 0.0f);
	glm::vec3 pos2(-1.0f, -1.0f, 0.0f);
	glm::vec3 pos3(1.0f, -1.0f, 0.0f);
	glm::vec3 pos4(1.0f, 1.0f, 0.0f);

	glm::vec2 uv1(0.0f, 1.0f);
	glm::vec2 uv2(0.0f, 0.0f);
	glm::vec2 uv3(1.0f, 0.0f);
	glm::vec2 uv4(1.0f, 1.0f);

	glm::vec3 nm(0.0f, 0.0f, 1.0f);

	glm::vec3 edge1 = pos2 - pos1;
	glm::vec3 edge2 = pos3 - pos1;
	glm::vec2 deltaUV1 = uv2 - uv1;
	glm::vec2 deltaUV2 = uv3 - uv1;

	float f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x);

	glm::vec3 tangent1;
	tangent1.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
	tangent1.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
	tangent1.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
	tangent1=glm::normalize(tangent1);
	glm::vec3 bitangent1;
	bitangent1.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
	bitangent1.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
	bitangent1.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);
	bitangent1 = glm::normalize(bitangent1);

	edge1 = pos3 - pos1;
	edge2 = pos4 - pos1;
	deltaUV1 = uv3 - uv1;
	deltaUV2 = uv4 - uv1;

	f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x);

	glm::vec3 tangent2;
	tangent2.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
	tangent2.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
	tangent2.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
	tangent2 = glm::normalize(tangent2);
	glm::vec3 bitangent2;
	bitangent2.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
	bitangent2.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
	bitangent2.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);
	bitangent2 = glm::normalize(bitangent2);

	float vertices[] =
	
		pos1.x,pos1.y,pos1.z,nm.x,nm.y,nm.z,uv1.x,uv1.y,tangent1.x,tangent1.y,tangent1.z,bitangent1.x,bitangent1.y,bitangent1.z,
		pos2.x,pos2.y,pos2.z,nm.x,nm.y,nm.z,uv2.x,uv2.y,tangent1.x,tangent1.y,tangent1.z,bitangent1.x,bitangent1.y,bitangent1.z,
		pos3.x,pos3.y,pos3.z,nm.x,nm.y,nm.z,uv3.x,uv3.y,tangent1.x,tangent1.y,tangent1.z,bitangent1.x,bitangent1.y,bitangent1.z,

		pos1.x,pos1.y,pos1.z,nm.x,nm.y,nm.z,uv1.x,uv1.y,tangent2.x,tangent2.y,tangent2.z,bitangent2.x,bitangent2.y,bitangent2.z,
		pos3.x,pos3.y,pos3.z,nm.x,nm.y,nm.z,uv3.x,uv3.y,tangent2.x,tangent2.y,tangent2.z,bitangent2.x,bitangent2.y,bitangent2.z,
		pos4.x,pos4.y,pos4.z,nm.x,nm.y,nm.z,uv4.x,uv4.y,tangent2.x,tangent2.y,tangent2.z,bitangent2.x,bitangent2.y,bitangent2.z
	;

	glGenVertexArrays(1, &VAO);
	glBindVertexArray(VAO);
	glGenBuffers(1, &VBO);
	glBindBuffer(GL_ARRAY_BUFFER,VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), &vertices, GL_STATIC_DRAW);
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(1);
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(float), (void*)(3 * sizeof(float)));
	glEnableVertexAttribArray(2);
	glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 14 * sizeof(float), (void*)(6 * sizeof(float)));
	glEnableVertexAttribArray(3);
	glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(float), (void*)(8 * sizeof(float)));
	glEnableVertexAttribArray(4);
	glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(float), (void*)(11 * sizeof(float)));

  • 在这里我们可以看到,一个三角形的每个顶点都有tangent、bitangent和normal属性,对于一个平坦的三角形来说,这三个属性显然是一样的,相应的三角形的片段的插值也是一样的,但是如果三角形之间共享顶点,那么一般的做法是将一个顶点的三个属性都做平均化处理,这样一来相邻三角形的法线会比较平缓的过渡,而非呈现出网格的生硬过度,自然的,中间插值的片段TBN也不再处处相同了。当然,在这里我们只是处理了一个四边形,无论是否平均化结果都是一样的。
  • 我们的着色器拿到这三个属性之后,有两个思路来利用TBN变换:
    • 第一个是,在片段着色器中使用TBN变换,将法线纹理采样的解雇做变换。这个只需要在顶点着色器中对三个适量做方向矢量的model变换,然后得到TBN矩阵输出到片段着色器中就可以了。

#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNorm;
layout(location = 2) in vec2 aTexCoord;
layout(location = 3) in vec3 aTangent;
layout(location = 3) in vec3 aBiTangent;

out VS_OUT
	vec3 fragPos;
	vec2 texCoord;
	vec3 normal;
	mat3 TBN;
vs_out;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()

	gl_Position = projection * view * model * vec4(aPos, 1.0f);
	vs_out.fragPos = vec3(model * vec4(aPos, 1.0f));
	vs_out.normal = normalize(mat3(transpose(inverse(model))) * aNorm);
	vs_out.texCoord = aTexCoord;

	mat4 normModel = transpose(inverse(model));
	vec3 T = normalize(vec3(normModel * vec4(aTangent, 0.0)));
	vec3 B = normalize(vec3(transpose(inverse(model)) * vec4(aBiTangent, 0.0)));
	vec3 N = normalize(vec3(transpose(inverse(model)) * vec4(aNorm, 0.0)));
	vs_out.TBN = mat3(T, B, N);

#version 330 core

out vec4 fragColor;

in VS_OUT
	vec3 fragPos;
	vec2 texCoord;
	vec3 normal;
	mat3 TBN;
fs_in;

uniform vec3 viewPos;

uniform sampler2D normalMap;

struct Material 
	sampler2D diffuse;
	sampler2D specular;
	float shininess;
;
uniform Material material;

struct DirLight 
	vec3 direction;

	vec3 ambient;
	vec3 diffuse;
	vec3 specular;
;
uniform DirLight dirlight;

struct PointLight 
	vec3 position;

	vec3 ambient;
	vec3 diffuse;
	vec3 specular;

	float constant;
	float linear;
	float quadratic;
;
#define NR_POINT_LIGHTS 4
uniform PointLight pointlights[NR_POINT_LIGHTS];

struct SpotLight 
	vec3 direction;
	vec3 position;

	vec3 ambient;
	vec3 diffuse;
	vec3 specular;

	float cutOff;
	float outerCutOff;
	float constant;
	float linear;
	float quadratic;
;
uniform SpotLight spotlight;

vec3 calcDirLight(DirLight light, vec3 normal, vec3 viewPos, vec3 fragPos);
vec3 calcSpotLight(SpotLight light, vec3 normal, vec3 viewPos, vec3 fragPos);
vec3 calcPointLight(PointLight light, vec3 normal, vec3 viewPos, vec3 fragPos);

void main()

	vec3 normal = texture(normalMap, fs_in.texCoord).rgb;
	normal = normalize(normal * 2.0 - 1.0);
	normal = normalize(fs_in.TBN * normal);

	vec3 result = calcDirLight(dirlight, normal, viewPos, fs_in.fragPos);

	//for (int i = 0; i != NR_POINT_LIGHTS; i++)
	//
	//	result += calcPointLight(pointlights[i], normal, viewPos, fragPos);
	//

	result += calcSpotLight(spotlight, normal, viewPos, fs_in.fragPos);

	fragColor = vec4(result, 1.0f);

	//gamma
	float gamma = 2.2;
	fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / gamma));


vec3 calcDirLight(DirLight light, vec3 normal, vec3 viewPos, vec3 fragPos)

	//ambient
	vec3 ambient = light.ambient * vec3(texture(material.diffuse, fs_in.texCoord));
	//diffuse
	vec3 lightDir = normalize(-light.direction);
	float diff = max(dot(normal, lightDir), 0.0);
	vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, fs_in.texCoord));
	//specular
	vec3 viewDir = normalize(viewPos - fragPos);
	vec3 halfwayDir = normalize(viewDir + lightDir);
	float spec = pow(max(dot(halfwayDir, normal), 0.0), material.shininess);
	vec3 specular = light.specular * spec * vec3(texture(material.specular, fs_in.texCoord));

	return  ambient + diffuse + specular;


vec3 calcPointLight(PointLight light, vec3 normal, vec3 viewPos, vec3 fragPos)

	float distance = length(light.position - fragPos);
	float attenuation = 1.0f / (light.constant + light.linear * distance + light.quadratic * distance * distance);
	//ambient
	vec3 ambient = light.ambient * vec3(texture(material.diffuse, fs_in.texCoord));
	//diffuse
	vec3 lightDir = normalize(light.position - fragPos);
	float diff = max(dot(normal, lightDir), 0.0);
	vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, fs_in.texCoord));
	//specular
	vec3 viewDir = normalize(viewPos - fragPos);
	vec3 halfwayDir =

视觉高级篇24 # 如何模拟光照让3D场景更逼真?(下)

说明

【跟月影学可视化】学习笔记。

什么是镜面反射?

如果若干平行光照射在表面光滑的物体上,反射出来的光依然平行,这种反射就是镜面反射。越光滑的材质,它的镜面反射效果也就越强,并且物体表面会有闪耀的光斑,也叫镜面高光

镜面反射的性质:入射光与法线的夹角等于反射光与法线的夹角

如何实现有向光的镜面反射?

实现镜面反射效果的步骤:

  1. 求出反射光线的方向向量
  2. 根据相机位置计算视线与反射光线夹角的余弦
  3. 使用系数和指数函数设置镜面反射强度
  4. 将漫反射和镜面反射结合起来,让距离光源近的物体上形成光斑

下面以点光源为例来实现光斑:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>如何实现点光源的镜面反射</title>
        <style>
            canvas 
                border: 1px dashed #fa8072;
            
        </style>
    </head>
    <body>
        <canvas width="512" height="512"></canvas>
        <script type="module">
            import  Renderer, Camera, Transform, Sphere, Box, Cylinder, Torus, Orbit, Program, Mesh, Color  from './common/lib/ogl/index.mjs';
            // JavaScript Controller Library
            import * as dat from './common/lib/dat.gui.js';
            console.log(dat)

            const canvas = document.querySelector('canvas');
            const renderer = new Renderer(
                canvas,
                width: 512,
                height: 512,
            );

            const gl = renderer.gl;
            gl.clearColor(1, 1, 1, 1);
            const camera = new Camera(gl, fov: 35);
            camera.position.set(0, 0, 10);
            camera.lookAt([0, 0, 0]);

            const scene = new Transform();

            // 在顶点着色器中,将物体变换后的坐标传给片元着色器
            const vertex = `
                precision highp float;

                attribute vec3 position;
                attribute vec3 normal;
                uniform mat4 modelViewMatrix;
                uniform mat4 viewMatrix;
                uniform mat4 projectionMatrix;
                uniform mat3 normalMatrix;
                uniform vec3 cameraPosition;

                varying vec3 vNormal;
                varying vec3 vPos;
                varying vec3 vCameraPos;

                void main() 
                    vec4 pos = modelViewMatrix * vec4(position, 1.0);
                    vPos = pos.xyz;
                    // 求光源与点坐标的方向向量
                    vCameraPos = (viewMatrix * vec4(cameraPosition, 1.0)).xyz;
                    vNormal = normalize(normalMatrix * normal);
                    gl_Position = projectionMatrix * pos;
                
            `;

            // 传入环境光 ambientLight 和材质反射率 materialReflection
            // 片元着色器中计算光线方向与法向量夹角的余弦
            const fragment = `
                precision highp float;

                uniform vec3 ambientLight;
                uniform vec3 materialReflection;
                uniform vec3 pointLightColor;
                uniform vec3 pointLightPosition;
                uniform mat4 viewMatrix;
                uniform vec3 pointLightDecayFactor;

                varying vec3 vNormal;
                varying vec3 vPos;
                varying vec3 vCameraPos;

                void main() 
                    // 光线到点坐标的方向
                    vec3 dir = (viewMatrix * vec4(pointLightPosition, 1.0)).xyz - vPos;

                    // 光线到点坐标的距离,用来计算衰减
                    float dis = length(dir);

                    // 归一化
                    dir = normalize(dir);

                    // 与法线夹角余弦
                    float cos = max(dot(normalize(dir), vNormal), 0.0);

                    // 反射光线:使用 GLSL 的内置函数 reflect,这个函数能够返回一个向量相对于某个法向量的反射向量
                    vec3 reflectionLight = reflect(-dir, vNormal);
                    vec3 eyeDirection = vCameraPos - vPos;
                    eyeDirection = normalize(eyeDirection);

                    // 与视线夹角余弦
                    float eyeCos = max(dot(eyeDirection, reflectionLight), 0.0);

                    // 镜面反射:指数取 20.0,系数取 3.0。
                    // 指数越大,镜面越聚焦,高光的光斑范围就越小。
                    // 系数能改变反射亮度,系数越大,反射的亮度就越高。
                    float specular = 3.0 *  pow(eyeCos, 20.0);

                    // 计算衰减
                    float decay = min(1.0, 1.0 /
                        (pointLightDecayFactor.x * pow(dis, 2.0) + pointLightDecayFactor.y * dis + pointLightDecayFactor.z));

                    // 计算漫反射
                    vec3 diffuse = decay * cos * pointLightColor;
                    
                    // 合成颜色
                    gl_FragColor.rgb = specular + (ambientLight + diffuse) * materialReflection;
                    gl_FragColor.a = 1.0;
                
            `;

            // 创建四个不同的几何体,初始化它们的环境光 ambientLight 以及材质反射率 materialReflection
            const sphereGeometry = new Sphere(gl);
            const cubeGeometry = new Box(gl);
            const cylinderGeometry = new Cylinder(gl);
            const torusGeometry = new Torus(gl);

            // 添加一个水平向右的白色平行光
            const ambientLight =  value: [1, 1, 1] ;

            const directional = 
                pointLightPosition:  value: [3, 3, 0] ,
                pointLightColor:  value: [0.5, 0.5, 0.5] ,
                pointLightDecayFactor:  value: [0, 0, 1] ,
            ;

            const program1 = new Program(gl, 
                vertex,
                fragment,
                uniforms: 
                    ambientLight,
                    materialReflection: value: [250/255, 128/255, 114/255],
                    ...directional
                ,
            );
            const program2 = new Program(gl, 
                vertex,
                fragment,
                uniforms: 
                    ambientLight,
                    materialReflection: value: [218/255, 165/255, 32/255],
                    ...directional
                ,
            );
            const program3 = new Program(gl, 
                vertex,
                fragment,
                uniforms: 
                    ambientLight,
                    materialReflection: value: [46/255, 139/255, 87/255],
                    ...directional
                ,
            );
            const program4 = new Program(gl, 
                vertex,
                fragment,
                uniforms: 
                    ambientLight,
                    materialReflection: value: [106/255, 90/255, 205/255],
                    ...directional
                ,
            );

            const torus = new Mesh(gl, geometry: torusGeometry, program: program1);
            torus.position.set(0, 1.3, 0);
            torus.setParent(scene);

            const sphere = new Mesh(gl, geometry: sphereGeometry, program: program2);
            sphere.position.set(1.3, 0, 0);
            sphere.setParent(scene);

            const cube = new Mesh(gl, geometry: cubeGeometry, program: program3);
            cube.position.set(0, -1.3, 0);
            cube.setParent(scene);

            const cylinder = new Mesh(gl, geometry: cylinderGeometry, program: program4);
            cylinder.position.set(-1.3, 0, 0);
            cylinder.setParent(scene);

            const controls = new Orbit(camera);

            // 添加动画
            requestAnimationFrame(update);
            function update() 
                requestAnimationFrame(update);
                controls.update();

                torus.rotation.y -= 0.02;
                sphere.rotation.y -= 0.03;
                cube.rotation.y -= 0.04;
                cylinder.rotation.y -= 0.02;

                renderer.render(scene, camera);
            

            // 添加控制
            const gui = new dat.GUI();
            const palette = 
                light: '#FFFFFF',
                reflection1: '#fa8072', // salmon rgb(250, 128, 114) [250/255, 128/255, 114/255, 1]
                reflection2: '#daa520', // goldenrod rgb(218, 165, 32) [218/255, 165/255, 32/255, 1]
                reflection3: '#2e8b57', // seagreen rgb(46, 139, 87) [46/255, 139/255, 87/255, 1]
                reflection4: '#6a5acd', // slateblue rgb(106, 90, 205) [106/255, 90/255, 205/255, 1]
            ;
            gui.addColor(palette, 'light').onChange((val) => 
                const color = new Color(val);
                program1.uniforms.ambientLight.value = color;
                program2.uniforms.ambientLight.value = color;
                program3.uniforms.ambientLight.value = color;
                program4.uniforms.ambientLight.value = color;
            );
            gui.addColor(palette, 'reflection1').onChange((val) => 
                program1.uniforms.materialReflection.value = new Color(val);
            );
            gui.addColor(palette, 'reflection2').onChange((val) => 
                program2.uniforms.materialReflection.value = new Color(val);
            );
            gui.addColor(palette, 'reflection3').onChange((val) => 
                program3.uniforms.materialReflection.value = new Color(val);
            );
            gui.addColor(palette, 'reflection4').onChange((val) => 
                program4.uniforms.materialReflection.value = new Color(val);
            );
        </script>
    </body>
</html>

什么是 Phong 反射模型?

冯氏反射模型是由美国犹他大学(University of Utah)的 Bui Tuong Phong 于1975年在他的博士论文中提出的,都用其名字命名。

Phong 光照模型是真实图形学中提出的第一个有影响的光照明模型,该模型只考虑物体对直接光照的反射作用,认为环境光是常量,没有考虑物体之间相互的反射光,物体间的反射光只用环境光表示。Phong光照模型属于简单光照模型。

Phong 模型认为物体表面反射光线由三部分组成:

  • 环境光(Ambient):场景中的其他间接光照
  • 漫反射(Diffuse):散射部分(大但不光亮)
  • 高光反射(Specular):镜面反射部分(小而亮)


在上图中,光线是白色的,环境光和漫反射部分是蓝色的,高光部分是白色的。

高光部分反射的光区域比较小,但强度很大;漫反射部分的强度根据物体表面方向的不同而不同;而环境光部分是跟方向无关的。

Phong 反射模型的完整公式如下:

光源部分:

  • lights:所有光源的集合,对于每盏光,可分为高光和漫反射两部分
  • i s i_s is:光源高光部分的强度(可以理解为就是RGB)
  • i d i_d id:光源漫反射部分的强度(可以理解为就是RGB)
  • i a i_a ia:环境光部分的强度(可以理解为就是RGB)

场景中材质的参数:

  • k s k_s ks:对入射光的高光反射常数(镜面反射系数)
  • k d k_d kd:对入射光的漫反射常数
  • k a k_a ka​:对环境光的反射常数
  • α:是和物体材质有关的常量,决定了镜面高光的范围。光泽度 α 越大,则亮点越小。

几个向量(全部归一化):

  • L m ^ \\hatL_m Lm^:物体表面某点指向光源m的位置的向量
  • N ^ \\hatN N^:物体表面某点的法线
  • R m ^ \\hatR_m Rm^:光源在物体表面某点发生镜面反射的方向
  • V ^ \\hatV V^:物体表面某点指向摄像机位置的向量

如何实现完整的 Phong 反射模型?

Phong 反射模型的实现整个过程分为三步:定义光源模型、定义几何体材质和实现着色器。

1、定义光源模型

属性(作用) /光源点光源平行光聚光灯
direction 方向 (定义光照方向)
position 位置 (定义光源位置)
color 颜色 (定义光的颜色)
decay 衰减 (光强度随着距离而减小)
angle 角度 (光传播的角度范围)

实现定义一个 Phong 类:用于添加和删除光源,并把光源的属性通过 uniforms 访问器属性转换成对应的 uniform 变量。

class Phong 
  constructor(ambientLight = [0.5, 0.5, 0.5]) 
    this.ambientLight = ambientLight;
    this.directionalLights = new Set();
    this.pointLights = new Set();
    this.spotLights = new Set();
  

  addLight(light) 
    const position, direction, color, decay, angle = light;
    if(!position && !direction) throw new TypeError('invalid light');
    light.color = color || [1, 1, 1];
    if(!position) this.directionalLights.add(light);
    else 
      light.decay = decay || [0, 0, 1];
      if(!angle) 
        this.pointLights.add(light);
       else 
        this.spotLights.add(light);
      
    
  

  removeLight(light) 
    if以上是关于05 高级光照的主要内容,如果未能解决你的问题,请参考以下文章

视觉高级篇23 # 如何模拟光照让3D场景更逼真?(上)

Unity高级开发-光照系统(三)-使用Unity的Progressive工具去正确的烘焙光照

视觉高级篇24 # 如何模拟光照让3D场景更逼真?(下)

视觉高级篇24 # 如何模拟光照让3D场景更逼真?(下)

unity 中Deferred Lighting (延迟光照)

Unity 3D 光照贴图快速入门 Lightmapping Quickstart

(c)2006-2024 SYSTEM All Rights Reserved IT常识