LearnGL - 学习笔记目录
前一篇:
- LearnGL - 13 - PointLight - 点光源 实现了 点光源 的最简单的着色方式
这篇:我们尝试给 “聚光灯” 类型的光源
本人才疏学浅,如有什么错误,望不吝指出。
其实 聚光灯 与 点光源 很类似,以为区别比较大的是: 聚光灯 有光照的张角范围,而不像 点光源 的张角是全方位的范围。
所以相比上一篇来说,代码的添加与调整也是很少的:
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
,如果 val
在 min_val
与 max_val
之间,将会是 0~1
之间的值,val
比 min_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;
这部分就的 fol
或 light->spot_light_FOL
就是编辑器中的 Spot Light FOL 参数,就是聚光灯的 张角 参数。