LearnGL - 13.1 - SpotLight - 聚光灯

本文详细讲解了如何在Shader中实现聚光灯的光照效果,并通过C++代码生成圆锥体网格用于Gizmos绘制,展示聚光灯的张角范围。文章深入探讨了光照模型中的聚光灯特性,包括光照角度限制和边缘过渡平滑处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


LearnGL - 学习笔记目录

前一篇:

这篇:我们尝试给 “聚光灯” 类型的光源

本人才疏学浅,如有什么错误,望不吝指出。


其实 聚光灯点光源 很类似,以为区别比较大的是: 聚光灯 有光照的张角范围,而不像 点光源 的张角是全方位的范围。

所以相比上一篇来说,代码的添加与调整也是很少的:

void phong_illumination(
    in vec3 worldNormal,
    in vec3 viewDir,
    in vec3 worldPos,
    out vec3 diffuse,
    out vec3 specular,
	out float atten
    ) {

	vec3 lightDir;
	atten = 1;
    if (LightPos.w == 0) {
		// 下面使用的是Phong 光照模型
		// 如果是方向光,那么 LightPos.xyz 是灯光方向的反方向
		lightDir = LightPos.xyz;
	} else {
		// 点光源 或是 聚光灯 都需要处理的
		lightDir = LightPos.xyz - worldPos; // 片段到光源的方向,简称:灯光方向
		float dist = length(lightDir);		// 片段到光源的距离
		lightDir *= dist == 0 ? 1 : 1.0 / dist; // 归一化
		atten = getDistanceAtten(dist);		// 获取距离衰减

		// 聚光灯
		if (LightPos.w != 1) {
			float LdotSD = dot(-lightDir, SpotLightDir); // LightDir dot Spot Light Dir
			float angle = acos(LdotSD);
			if (angle < SpotLightFOL) {
				atten = 1;
			} else {
				atten = 0;
			}
		}
	}
	float D = max(0, dot(lightDir, worldNormal));
	diffuse = LightColor.rgb * LightColor.a * D * DiffuseK * atten;
	vec3 H = normalize(lightDir + viewDir);
	float S = 0;
	if (D > 0) S = pow(max(0, dot(H, worldNormal)), Glossy);
	specular = LightColor.rgb * LightColor.a * S * SpecularK * atten;
}

主要看:

		// 聚光灯
		if (LightPos.w != 1) {
			float LdotSD = dot(-lightDir, SpotLightDir); // LightDir dot Spot Light Dir
			float angle = acos(LdotSD);
			if (angle < SpotLightFOL) {
				atten = 1;
			} else {
				atten = 0;
			}
		}

主要判断的是 -lightDir 光源位置到片段位置的方向,与 SpotLightDir 聚光灯的照射方向 的角度的 SpotLightFOL 张角 范围内就算是照亮范围内:

即:SpotLightDir 与 -lightDir 之间的夹角 小于 (FOL / 2),那就说明是在 聚光灯照射范围内
在这里插入图片描述

我们直接将 atten 输出,可以查看到效果:

gl_FragColor = vec4(atten);

在这里插入图片描述
(上面的效果中,可以发现 气球猫 的模型部分被 球体 挡住的部分,还是有光照,这部分先不用纠结,以后学习到 阴影 部分就可以处理这部分内容,如果需要,你也可以查看我之前 在 Unity 中实现自定义的方向光阴影 ShadowMap 的方式来实现)

但是这个张角的照射边缘的过渡很生硬,我们提供另一个:

		// 聚光灯
		if (LightPos.w != 1) {
			float LdotSD = dot(-lightDir, SpotLightDir); // LightDir dot Spot Light Dir
			float angle = acos(LdotSD);
			if (angle < SpotLightFOL) {
				if (angle < SpotLightFOL_FadeOut) {
					atten = 1;
				} else {
					atten = 1 - smoothstep(SpotLightFOL_FadeOut, SpotLightFOL, angle);
				}
				// atten *= pow(LdotSD, SpotLightExp);
			} else {
				atten = 0;
			}
		}

在代码中,可以看到我们使用了 atten = 1 - smoothstep(SpotLightFOL_FadeOut, SpotLightFOL, angle); 这么一句代码来平滑

其中:

  • SpotLightFOL 还是之前那个 张角
  • SpotLightFOL_FadeOut 就是我们的 开始淡出的张角角度
  • angle 是当前的光源指向片段的角度

这里头,smoothstep 函数用法是:smoothstep(min_val, max_val, val); 指定 最小范围 min_val 和最大范围 max_val,如果 valmin_valmax_val 之间,将会是 0~1 之间的值,valmin_val 更小,则返回 0,比 max_val 大, 则返回 1,想 查看 smoothstep 曲线函数 的可以查看我之前的一篇:CG cosh, sinh, smoothstep, tanh, perlin_easeCurve1/2 曲线 - smoothstep

在这里插入图片描述


演示

在这里插入图片描述

调整张角效果
在这里插入图片描述

还可以对 Spot Light Gizmos - Cone 圆锥体调整渲染状态
在这里插入图片描述


完整 Shader

shader 也是相当简单

// jave.lin - my_lighting.glsl - 光照模型处理

#include "/Include/my_global.glsl"

#ifndef _MY_LIGHTING__GLSL__
#define _MY_LIGHTING__GLSL__

// scene uniform
uniform vec4 _Ambient;		// .xyz 环境光颜色, .w 环境光系数
uniform int AmbientType;	// 环境光类别,[测试用]

// object uniform
uniform float Glossy;		// 光滑度
uniform vec3 DiffuseK;		// 漫反射系数
uniform vec3 SpecularK;		// 高光系数

// light uniform
uniform vec4 LightPos;		// 灯光世界坐标位置,w==0,或名是方向光,w==1说明是点光源,w == 0.5 是聚光灯
uniform vec4 LightColor;	// 灯光颜色,.xyz 顔色,.w 强度
uniform vec3 SpotLightDir;	// 聚光灯照射方向
uniform float SpotLightFOL; // 聚光灯的张角量,Field Of Light(弧度)
uniform float SpotLightFOL_FadeOut; // 这个边缘淡出的角度,必须小于 SpotLightFOL,否则没有效果

// point or spot light
uniform float ATTEN_Kc;		// 点光源 常数项系数
uniform float ATTEN_Kl;		// 点光源 一次项系数
uniform float ATTEN_Kq;		// 点光源 二次项系数
uniform vec2 ATTEN_Range;	// 点光源 有效范围, .x == range, .y == 1.0 / range

// TODO : 实现多光源时使用,现在单光源先不写
// struct LightData_t {
// };

// const uint MaxLightNum = 10;
// uniform LightData_t Lights[MaxLightNum];

// ambient
vec3 getAmbient(vec3 albedo) {
	if (AmbientType == 0) {
		return _Ambient.rgb * _Ambient.a;
	} else {
		return mix(_Ambient.rgb * _Ambient.a, albedo, _Ambient.a);
	}
}

// point light
float getDistanceAtten(float dist) {		// 获取距离衰减
	// method1:
	// return (1.0 / (ATTEN_Kc + ATTEN_Kl * dist + ATTEN_Kq * (dist * dist)))
	// // ;
	//  * clamp(dist * ATTEN_Range.y == 0 ? 0 : 1 - dist * ATTEN_Range.y, 0, 1);

	// method2:
	return 1 - smoothstep(0, ATTEN_Range.x, dist);
}

void phong_illumination(
    in vec3 worldNormal,
    in vec3 viewDir,
    in vec3 worldPos,
    out vec3 diffuse,
    out vec3 specular
	// ,out float atten
    ) {

	vec3 lightDir;
	float atten = 1;
    if (LightPos.w == 0) {
		// 下面使用的是Phong 光照模型
		// 如果是方向光,那么 LightPos.xyz 是灯光方向的反方向
		lightDir = LightPos.xyz;
	} else {
		// 点光源 或是 聚光灯 都需要处理的
		lightDir = LightPos.xyz - worldPos; // 片段到光源的方向,简称:灯光方向
		float dist = length(lightDir);		// 片段到光源的距离
		lightDir *= dist == 0 ? 1 : 1.0 / dist; // 归一化
		atten = getDistanceAtten(dist);		// 获取距离衰减

		// 聚光灯
		if (LightPos.w != 1) {
			float LdotSD = dot(-lightDir, SpotLightDir); // LightDir dot Spot Light Dir
			float angle = acos(LdotSD);
			if (angle < SpotLightFOL) {
				if (angle > SpotLightFOL_FadeOut) {
					atten *= 1 - smoothstep(SpotLightFOL_FadeOut, SpotLightFOL, angle);
				}
			} else {
				atten = 0;
			}
		}
	}
	float D = max(0, dot(lightDir, worldNormal));
	diffuse = LightColor.rgb * LightColor.a * D * DiffuseK * atten;
	vec3 H = normalize(lightDir + viewDir);
	float S = 0;
	if (D > 0) S = pow(max(0, dot(H, worldNormal)), Glossy);
	specular = LightColor.rgb * LightColor.a * S * SpecularK * atten;
}

#endif

Spot Light Gizmos - 聚光灯的 Gizmos 绘制

如果你好奇我的 Spot Light 的调试网格是怎么来的,可以看一下,否则可以不看这部分内容

前面我们有对 方向光(一个 Cube),点光源(一个 Sphere)来绘制 Gizmos

这次我们的 聚光灯 使用的是一个 Cone,圆锥体

这个圆锥体是实时程序控制的顶点坐标


C++ 生成网格代码

初始化网格:

				const size_t segment = 36;
				const float startDeg = 0;
				const float intervalDeg = 360.0f / segment;
				const float fol = D2R(15.0f); // field of light,聚光灯的张角

				/*
				tan(a)=D/L
				a=15 degs
				D=?
				L=1
				tan(15 degs)=D/1
				tan(15 degs)=D
				*/

				// 第一个圆锥点
				// 顶点
				
				std::vector<vec3> vec_vertices;
				// 第一个锥顶点
				vec_vertices.push_back(vec3(0));

				// 聚光灯的张角作为半径
				GLfloat radius = tanf(fol);

				// 画一个原型,从x轴正方向 开始 顺时针 生成
				for (size_t i = 0; i < segment; i++) {
					vec3 v = vec3(
						cosf(D2R(startDeg + intervalDeg * i)) * radius,
						sinf(D2R(startDeg + intervalDeg * i)) * radius,
						-1.0f
					);
					vec_vertices.push_back(v);
					//std::cout << v.x << "," << v.y << "," << v.z << "\n";
				}
				// 最后一个闭合点,与起点重合
				vec_vertices.push_back(vec3(
					cosf(D2R(startDeg))* radius,
					sinf(D2R(startDeg))* radius,
					-1.0f
				));

				size_t vertices_count = vec_vertices.size() * 3;
				//std::cout << "vec_vertices.size() : " << vec_vertices.size() << "\n";
				GLfloat* vertices = new GLfloat[vertices_count];

				for (size_t i = 0; i < vec_vertices.size(); i ++) {
					vec3 v = vec_vertices[i];
					vertices[i * 3 + 0] = v.x;
					vertices[i * 3 + 1] = v.y;
					vertices[i * 3 + 2] = v.z;
				}

				// 颜色
				size_t colors_count = vec_vertices.size() * 4;
				//std::cout << "colors_count : " << colors_count << "\n";
				GLfloat* colors = new GLfloat[colors_count];
				colors[0] = 1.0f;
				colors[1] = 1.0f;
				colors[2] = 1.0f;
				colors[3] = 1.0f;

				for (size_t i = 4; i < colors_count; i += 4) {
					colors[i + 0] = 1.0f;
					colors[i + 1] = 1.0f;
					colors[i + 2] = 1.0f;
					colors[i + 3] = 0.0f;
				}

				// 索引
				size_t size1 = (vec_vertices.size() - 2) * 3;
				size_t size2 = (vec_vertices.size() - 1 - 2) * 3;
				size_t indices_count = size1 + size2;
				GLuint* indices = new GLuint[indices_count];
				//std::cout << "indices_count : " << indices_count << "\n";
				size_t idx = 0;
				size_t v_idx = 0;
				for (size_t i = 0; i < size1 - 2; i += 3) {
					indices[idx++] = 0;
					indices[idx++] = v_idx + 1;
					indices[idx++] = v_idx + 2;
					std::cout << 0 << "," << (v_idx + 1) << "," << (v_idx + 2) << "\n";
					v_idx++;
				}
				v_idx = 0;
				for (size_t i = 0; i < size2 - 2; i += 3) {
					indices[idx++] = 1;
					indices[idx++] = v_idx + 3;
					indices[idx++] = v_idx + 2;
					//std::cout << 1 << "," << (v_idx + 2) << "," << (v_idx + 3) << "\n";
					v_idx++;
				}

				//std::cout << "idx : " << idx << "\n";

				SL_mesh = new Mesh("Cone Mesh");

				SL_mesh->pos_copy_from(vertices, vertices_count);
				SL_mesh->color_copy_from(colors, colors_count);
				SL_mesh->indices_copy_from(indices, indices_count);

				delete[] vertices;
				delete[] colors;
				delete[] indices;

				rawPos = SL_mesh->getRawPos();

				//SL_mesh->primiveType = DrawState_PrimitiveType::TRIANGLE_FAN; // FAN 要两个网格
				SL_mesh->primiveType = DrawState_PrimitiveType::TRIANGLES; // 所以还是用回 TRIANGLES

				SL_mesh->makeDynamic();

然后是更新网格:

			// 更新 Spot Light 的张角,也就是更新顶点的位置
			const size_t segment = 36;
			const float startDeg = 0;
			const float intervalDeg = 360.0f / segment;

			GLfloat* pos = (GLfloat*)rawPos->ptr();
			pos += 3; // 跳过第一个点,因为始终保持为0

			// 聚光灯的张角作为半径
			GLfloat radius = tanf(D2R(light->spot_light_FOL * 0.5f)) * light->point_or_spot_light_range;

			// 画一个圆形,从x轴正方向 开始 顺时针 生成
			for (size_t i = 0; i < segment; i++) {
				*(pos + (i * 3 + 0)) = cosf(D2R(startDeg + intervalDeg * i)) * radius;
				*(pos + (i * 3 + 1)) = sinf(D2R(startDeg + intervalDeg * i)) * radius;
				*(pos + (i * 3 + 2)) = -light->point_or_spot_light_range;
			}
			// 最后一个闭合点,与起点重合
			*(pos + (segment * 3 + 0)) = cosf(D2R(startDeg)) * radius;
			*(pos + (segment * 3 + 1)) = sinf(D2R(startDeg)) * radius;
			*(pos + (segment * 3 + 2)) = -light->point_or_spot_light_range;

			SL_mesh->uploadPos(rawPos);

我的圆锥共有38个顶点,第一个顶点都是0点,其他的点都是一个圆形上分36分段,加上最后一个闭合点,所以共38个。

其中 索引 的部分有两个大小 size1, size2 这两部分我是先画了一下图形,总结了一个规律算出来的索引数量,具体可以参考下图:
在这里插入图片描述

本来我是想用 TRIANGLE_FAN 的图元类型来设计这个圆锥的,因为 TRIANGLE_FAN 比较直观,但是发现用 TRIANGLE_FAN 只能与第一个顶点开始连续链接的索引,所以需要使用两个网格来分离,为何只用一个网格实现,所以还是用回了 TRIANGLES 的图元类型

然后你可以看到有一句:

// 聚光灯的张角作为半径
GLfloat radius = tanf(fol);
...
// 或是
GLfloat radius = tanf(D2R(light->spot_light_FOL * 0.5f)) * light->point_or_spot_light_range;

这部分就的 follight->spot_light_FOL 就是编辑器中的 Spot Light FOL 参数,就是聚光灯的 张角 参数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值