Hazel学习笔记:游戏功能完善
由于Hazel剩下的内容非常多,我以前也没有接触过游戏、渲染部分,所以学起来比较吃力。这部分我将作为OpenGL、游戏相关知识的积累,沉淀一些个人理解,并持续更新。
ECS
ECS是游戏中常用的架构,他之所以流行,是因为:他把对象的存储和使用分开,在不影响易用性的前提下,保证了性能。
如果没有ECS,我们应该如何处理游戏对象?
假如我们现在有一个路灯和一个玩家,他们作为游戏世界的一员,首先需要有自己的位置信息,现在要让这个路灯和这个玩家都具备发光的能力,那么我们很容易能想到:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
class Transform
{
public:
void SetPos(vec3 pos) {}
vec3 GetPos() {}
private:
vec3 pos;
};
class Light
{
public:
void StartLight();
void StopLight();
};
class StreetLight : Light, Transform
{
public:
void StartLight() { /* ... */ }
};
class Player : Light, Transform
{
public:
void StartLight() { /* ... */ }
}
|
后来游戏逐渐更新,玩家的灯光逻辑变得越来越复杂…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
class Light
{
public:
void StartLight();
void StopLight();
void SetLightingDirection(int dir) {}
void SetLightingIntensity(float intensity) {}
void SetLightingColor() {}
// ...
};
class StreetLight : Light, Transform
{
public:
void StartLight() { /* ... */ }
};
class Player : Light, Transform
{
public:
void StartLight() { /* ... */ }
}
|
很容易就会让原本的路灯得到一些他根本用不上的属性,更不用说玩家可能还会有:声音、生命值、攻击能力…,玩家就会变得越来越臃肿,很不利于维护。
如果我们把“继承”变成“组合”呢?
组合的思路是把能力拆成组件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
class StreetLight
{
public:
void StartLight() { /* ... */ }
private:
Transform transform;
Light light;
// ...
};
class Player
{
public:
void StartLight() { /* ... */ }
private:
Transform transform;
Light light;
// ...
}
|
好处显而易见:我们不用维护越来越复杂的继承树了,现在我们的属性能够随意拆解组装。Player 和 StreetLight 可以共享组件。
但是对于我的游戏循环来说,这种方式仍然不直观,你可能需要在你的游戏循环中写一大堆if-else来维护你玩家的各种各样的能力:
1
2
3
4
5
6
7
8
9
10
11
| void Update(Player& o, float dt) {
if (o.velocity) {
o.transform.x += o.velocity->vx * dt;
o.transform.y += o.velocity->vy * dt;
}
if (o.sprite) {
// render ...
}
// if (has AI)
// if (has Physics) ...
}
|
也许你会说,我不怕麻烦,我愿意写成百上千行的条件判断来处理我玩家越来越复杂的能力。
是的,我们当然可以这样,但是游戏其实对性能的要求很高,我们选择C++来做游戏底层开发就是因为他可以直接和操作系统硬件交互,中间层很少,那么对于这种越来越多的条件判断,尤其是对象越来越多的时候,我们每一帧都要对每一个对象的每一个功能做条件判断,这就对CPU的缓存越来越不友好了。
之前看到过有说,想让你的程序跑的更快,可以从以下三个方面入手优化:
- 磁盘IO(或者网络带宽)
- 算法优化
- CPU缓存
显然我们现在正在游戏开发的初期,算法和磁盘我们都还不涉及,那么我们在设计之初就应该考虑考虑CPU缓存的问题。
现在我们存在的问题是:
- Update 里全是条件分支。
- 每个对象每帧都要跑一遍 if。
- 很难并行。很难做批处理。
现代 CPU 快不快,关键看数据是不是连续。
普通 OOP 组合一般是“对象数组”,你可能会想到将所有的相同对象都放在一起,对象内部又指向一堆组件或资源。访问路径是跳来跳去的指针追踪。
当我访问其中某一个对象时,CPU会假设我未来会访问这个对象旁边的其他一些对象,就会顺手将他们全部加载进 CPU Cache 中,但是当我访问对象内部信息时,仍然会出现大量缓存未命中的情况。
ECS的核心:所有的数据当作组件存储,以逻辑为单位进行运行
ECS(Entity/Component/System):
- E:一个游戏实体(可能是一个玩家、一个路灯),一般只是一个ID,不携带数据
- C:纯数据,与实体无关
- S:整合E和C,做逻辑处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// Entity
using Entity = uint32_t;
// Component
struct Transform { float x, y; };
struct Velocity { float vx, vy; };
// System
struct MovementSystem {
void Update(float dt,
std::vector<Transform>& transforms,
std::vector<Velocity>& velocities,
const std::vector<Entity>& entities) {
for (Entity e : entities) {
// 这里假设能通过 e 定位到对应的 Transform/Velocity
}
}
};
|
通常ECS实现会将各个组件都放在一段连续的内存中,这样就能够:
- 在更新帧时只遍历“有 Velocity 的实体”。没有无意义 if
- 数据之间内存连续。CPU缓存命中率高
- 同一系统的更新循环非常适合并行和 SIMD
- 渲染系统可以自然做批处理。比如按材质、功能分组
简单实现可能像这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| using Entity = uint32_t;
struct Transform { float x = 0, y = 0; };
struct Velocity { float vx = 0, vy = 0; };
struct World {
Entity nextId = 1;
std::vector<Entity> entities;
// 组件存储:按类型分开
std::unordered_map<Entity, Transform> transforms;
std::unordered_map<Entity, Velocity> velocities;
Entity CreateEntity() {
Entity e = nextId++;
entities.push_back(e);
return e;
}
bool HasTransform(Entity e) const { return transforms.find(e) != transforms.end(); }
bool HasVelocity(Entity e) const { return velocities.find(e) != velocities.end(); }
};
struct MovementSystem {
void Update(World& w, float dt) {
// 只遍历有 Velocity 的实体
for (auto& [e, v] : w.velocities) {
auto it = w.transforms.find(e);
if (it == w.transforms.end()) continue;
Transform& t = it->second;
t.x += v.vx * dt;
t.y += v.vy * dt;
}
}
};
|
这个仍然有很多优化的地方,比如 unordered_map 可能不连续(桶与桶之间)。但 ECS 的思路大致如上。
Shader
Shader实现屏幕水滴效果
以下为我代码中的雨天和屏幕水滴效果的开发记录,作为着色器的知识积累。
效果展示
想象你站在窗边,看着外面的雨。玻璃上布满了水滴,透过水滴看到的景象会有轻微的扭曲——这就是我们要用代码实现的效果。
实现过程
在图形渲染领域,许多实现方式都与我们的直觉相悖。例如:光线追踪中的"光线"实际上是从摄像机发出、逆向追踪到光源的;3D 游戏中那些看起来丝滑飘逸的布料,本质上是由成千上万的离散顶点模拟出来的;实时阴影并非通过物理遮挡计算,而是借助"从灯光视角拍摄的深度照片"来判断。
屏幕水滴效果同样如此。如果直接在画面上叠加一张水滴贴图,得到的只是"贴纸般的圆点"——缺乏真实感,效果单一。我们真正想要的,是那种站在雨天玻璃窗前的视觉体验:透过水滴看到的背景会产生微妙的扭曲和偏移。
这种效果源于折射——光线穿过水滴时会发生弯曲。然而在 GPU 着色器中,我们并没有真正模拟光线的物理路径,而是采用了一种巧妙的近似:直接偏移纹理的采样坐标(UV)。当一个像素位于"水滴区域"内时,我们让它去采样稍微偏移后的背景位置,视觉上就产生了"透过水滴看东西"的折射效果。
这正是渲染吸引我的魅力所在:很多的特效背后的实现逻辑总是那么的出乎意料。
UV 扭曲,偏移纹理坐标
在着色器中,我们用 UV 坐标 来决定"从背景图的哪个位置取颜色"。但是,从背景图的哪个位置取?取多少?如何确定需要扭曲的范围?这都是我们需要计算的。
网格法确定水滴位置
2.1 如何确定水滴位置?
假设我们想在屏幕上放 100 个水滴,最直接的想法是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 存储每个水滴的位置
vec2 dropPositions[100] = {
vec2(0.1, 0.2),
vec2(0.3, 0.7),
...
};
// 在着色器中,对每个像素都要检查所有水滴
for (int i = 0; i < 100; i++) {
if (distance(currentPixel, dropPositions[i]) < 0.05) {
// 这个像素在水滴范围内
}
}
// 在CPU端控制水滴位置动态变化...
|
如果此时水滴数量增多,那么每次就需要向GPU中传送大量的数据,并且是每一帧都传!效率大大降低了,那有没有办法将水滴的位置控制放在计算性能更高的GPU端呢?
2.2 解决方案:网格 + 伪随机
核心:通过把屏幕分成很多个格子,让每个格子内"自动生成“一个“位置随机”水滴。
我们可以将uv坐标按照坐标划分网格,有一种快速划分网格的方法:
- 将原本在
[0, 1]范围内的uv坐标转化成[10, 10]的坐标(即:放大原始坐标值) - 将新坐标的整数部分作为格子的
ID,将小数部分作为新坐标在该格子内的坐标
代码实现:
1
2
3
| uv *= vec2(10.0, 2.5); // 放大,创建 10×2.5 的网格
vec2 cellId = floor(uv); // 格子 ID(floor,取整数部分)
vec2 localPos = fract(uv) - 0.5; // 格子内坐标(-0.5 ~ 0.5,fract,取小数部分)
|
比如说:一个原始uv坐标为(0.23, 0.4)的像素,在经过换算后会得到一个在格子[2, 1],格子内偏移量(0.3, -0.2)的结果。
2.3 伪随机函数
现在每个格子需要一个"独特的水滴位置”,我们用 伪随机函数:
1
2
3
4
5
6
| float Hash21(vec2 coord) // 返回 [0, 1)
{
coord = fract(coord * vec2(123.34, 345.45));
coord += dot(coord, coord + 34.345); // dot: 返回向量点积的结果
return fract(coord.x * coord.y);
}
|
1
2
| float cellRandom = Hash21(cellId + seed); // 用格子 ID 查询
float dropX = cellRandom - 0.5; // 水滴在格子内的 X 偏移 (-0.5 ~ 0.5)
|
这样做的好处是:对于同一个格子,相同的种子来说,得到的随机数一定相同,可以作为水滴下滑的初始位置,并保证水滴在相同格子滑落时能够竖直向下(格子ID不变时,x偏移量不变)。
水滴动画
3.1 垂直滚动
最简单的动画:让整个 UV 向上滚动,视觉上水滴就会向下流。
1
| uv.y += u_Time * 0.05; // 随时间向上偏移
|
原理:让同一时刻的同一位置的uv能够处在不同的“方格”,从而达到“滚动”的错觉。
3.2 锯齿动画
但只是匀速下落太单调了。真实的水滴会:
- 积累一会儿
- 突然滑落
- 再积累…
我们用一个特殊的 锯齿波函数:
1
2
3
4
| float SawtoothWave(float x)
{
return (cos(x) + cos(x * 3.0) * 0.5 + sin(x * 5.0) * 0.1) * 0.4 + 0.5;
}
|

代码中的使用:
1
2
| float animPhase = fract(time + cellRandom); // 每个水滴的动画起始位置不同
float dropY = (SawtoothWave(animPhase) - 0.5) * 0.9; // 垂直位置随时间变化
|
通过在animPhase中添加cellRandom变量来控制同一时刻,不同格子内的水滴位置不会相同,形成“错落”的感觉。
水滴形状绘制
4.1 主水滴
一个水滴首先要有"形状"。我们用 距离场 (SDF)来定义:
1
2
3
| vec2 dropCenter = vec2(dropX, dropY); // 水滴中心位置
float distToDrop = length(localPos - dropCenter); // 当前像素到中心的距离
float mainDrop = smoothstep(0.2, 0.0, distToDrop); // 距离 < 0.2 的区域是水滴
|
**smoothstep(0.2, 0.0, d) **能够在像素距离中心[0, 0.2]的时候返回[0, 1]。
4.2 水滴尾迹
真实的雨滴不只是一个圆点,下落时会拖出一条"尾巴"
尾迹的实现分为三步:
第一步:限制在水滴下方
1
| float trailMask = smoothstep(0.0, 0.2, localPos.y - dropCenter.y);
|
第二步:限制尾迹长度
1
| trailMask *= smoothstep(0.5, 0.0, localPos.y - dropCenter.y);
|
第三步:限制尾迹宽度
1
| trailMask *= smoothstep(0.05, 0.03, abs(localPos.x - dropCenter.x));
|
你可能会问:为什么三个条件之间是用 *= 而不是 += 或别的进行连接?
因为乘法是实现逻辑“或”的一种方式,每个 smoothstep 返回的是 0 ~ 1 之间的值:
- 返回 1 = 这个条件完全满足
- 返回 0 = 这个条件完全不满足
- 返回 0.5 = 这个条件部分满足
最终就能实现:只要有一个条件不满足,则整体条件都不满足的效果。
第四步:绘制尾迹
1
2
3
| float trailDist = abs(localPos.x - dropCenter.x); // 到尾迹中心线的距离
float dropTrail = smoothstep(0.1, 0.02, trailDist);
dropTrail *= trailMask; // 应用三个限制条件
|
折射!
这是整个效果的最关键一步:
1
2
3
| vec2 offsetDirection = localPos - dropCenter;
float offsetStrength = mainDrop + dropTrail * 0.5;
uvOffset = offsetStrength * offsetDirection;
|
这行代码到底在做什么?为什么一行就能实现折射?
让我们把它拆成三个部分:
offsetDirection — 偏移方向
1
| vec2 offsetDirection = localPos - dropCenter; // 从水滴中心指向当前像素的向量
|
真实水滴像一个凸透镜,光线会向中心聚焦。我们用 localPos - dropCenter 作为偏移方向,最终就会将远处相应方向的像素偏移至当前像素位置,就是在模拟"从边缘向中心弯曲"的折射效果。
mainDrop + dropTrail * 0.5 — 偏移强度
1
| float offsetStrength = mainDrop + dropTrail * 0.5; // 0 ~ 1 之间的遮罩
|
这是一个遮罩值,决定了"这个像素需要多大的偏移",偏移强度随当前像素到水滴中心和尾部的距离来决定,尾部占比会小一点,因为水滴滑落时尾部水量更少。
**A × B **
1
| uvOffset = offsetStrength * offsetDirection;
|
向量乘以标量 = 保持方向,改变长度。
多层折射叠加
只用一层水滴会很单调——所有水滴大小相同、间距均匀。
1
2
3
4
5
6
7
8
9
| vec2 CalculateRaindropsOffset(vec2 uv, float intensity)
{
vec2 totalOffset = vec2(0.0);
totalOffset += GetDrops(uv, 1.0, intensity);
totalOffset += GetDrops(uv * 1.4 + 7.23, 1.25, intensity);
totalOffset += GetDrops(uv * 2.1 + 1.17, 1.5, intensity) * 0.5;
return totalOffset * 0.03 * intensity;
}
|
我们对每一次偏移设置不同的seed值,不同的位置,从而最终得到不同大小,不同位置的“水滴”,让水滴变得更密集,更有随机性。
随后再进行纹理采样,安全性检查,就得到了开头的水滴效果:
1
2
3
4
5
| vec2 offset = CalculateRaindropsOffset(uv, u_RaindropsIntensity);
uv = clamp(uv + offset, vec2(0.0), vec2(1.0));
vec4 color = texture(u_ScreenTexture, uv);
o_Color = color; // 最终颜色
|
总结
现在的效果还比较简陋,后面可以继续进行拓展,同时我的效果和原始的效果相比还有很大差距。
参考代码:https://www.shadertoy.com/view/MdfBRX
完整GLSL:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
| #version 450 core
in vec2 v_TexCoord;
out vec4 o_Color;
uniform sampler2D u_ScreenTexture;
uniform int u_RaindropsEnabled;
uniform float u_RaindropsIntensity;
uniform float u_RaindropsTime;
float Hash21(vec2 coord)
{
coord = fract(coord * vec2(123.34, 345.45));
coord += dot(coord, coord + 34.345);
return fract(coord.x * coord.y);
}
float SawtoothWave(float x)
{
return (cos(x) + cos(x * 3.0) * 0.5 + sin(x * 5.0) * 0.1) * 0.4 + 0.5;
}
vec2 GetDrops(vec2 uv, float seed, float intensity)
{
float time = u_RaindropsTime + intensity * 30.0;
vec2 uvOffset = vec2(0.0);
uv.y += time * 0.05;
uv *= vec2(10.0, 2.5) * seed;
vec2 cellId = floor(uv);
vec2 localPos = fract(uv) - 0.5;
float cellRandom = Hash21(cellId + seed);
float dropX = cellRandom - 0.5;
float animPhase = fract(time + cellRandom);
float dropY = (SawtoothWave(animPhase) - 0.5) * 0.9;
vec2 dropCenter = vec2(dropX, dropY);
float distToDrop = length(localPos - dropCenter);
float mainDrop = smoothstep(0.2, 0.0, distToDrop);
float trailMask = smoothstep(0.0, 0.2, localPos.y - dropCenter.y);
trailMask *= smoothstep(0.5, 0.0, localPos.y - dropCenter.y);
trailMask *= smoothstep(0.05, 0.03, abs(localPos.x - dropCenter.x));
float trailDist = abs(localPos.x - dropCenter.x);
float dropTrail = smoothstep(0.1, 0.02, trailDist);
dropTrail *= trailMask;conditions (AND logic via multiplication);
vec2 offsetDirection = localPos - dropCenter;
float offsetStrength = mainDrop + dropTrail * 0.5;
uvOffset = offsetStrength * offsetDirection;
return uvOffset;
}
vec2 CalculateRaindropsOffset(vec2 uv, float intensity)
{
vec2 totalOffset = vec2(0.0);
totalOffset += GetDrops(uv, 1.0, intensity);
totalOffset += GetDrops(uv * 1.4 + 7.23, 1.25, intensity);
totalOffset += GetDrops(uv * 2.1 + 1.17, 1.5, intensity) * 0.5;
return totalOffset * 0.03 * intensity;
}
void main()
{
vec2 uv = v_TexCoord;
if (u_RaindropsEnabled != 0 && u_RaindropsIntensity > 0.0)
{
vec2 offset = CalculateRaindropsOffset(uv, u_RaindropsIntensity);
uv = clamp(uv + offset, vec2(0.0), vec2(1.0));
}
vec4 color = texture(u_ScreenTexture, uv);
o_Color = color;
}
|
2D锥形光照实现
效果
实现思路
都需要哪些数据?
如果想要实现一个类似于手电筒的效果,如果只渲染一个简单的三角形那是远远不够的,观察日常生活中的手电筒效果,随着照射距离增加,光照强度会逐渐降低,对于手电筒光照中心,应该是最亮的,随后向锥体两边逐渐变暗,所以我们至少需要:
- 两个锥体,也就是两个角度
- 光照强度(最远能照射到的范围)
- 光线的衰减速率
所以我们可以先确定光照需要的所有属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| enum class Light2DType : uint8_t
{
Spot
};
struct Light2D
{
Light2DType type = Light2DType::Spot;
glm::vec2 position = { 0.0f, 0.0f }; // 世界坐标位置
glm::vec3 color = { 1.0f, 1.0f, 1.0f }; // RGB 颜色
float intensity = 1.0f; // 强度 0~∞
float radius = 5.0f; // 光照半径(光照最远距离)
float falloff = 2.0f; // 控制光线渐变速度
float direction = 0.0f; // 朝向角度(弧度)
float innerAngle = 0.3f; // 内锥角(完全亮)
float outerAngle = 0.6f; // 外锥角(渐变到 0)
bool enabled = true; // 是否启用
};
|
距离衰减实现
1
2
3
4
| float dist = length(currentPos - lightPos); // 计算当前像素到光源的距离
float normalizedDist = dist / lightRadius; // 归一化
float attenuation = 1.0 - pow(clamp(normalizedDist, 0.0, 1.0), falloff); // 衰减公式
|
这里使用1-pow(normalizedDist, falloff)来做到中心值最大,越靠外强度越小:
锥形区域
3.1 角度计算
1
2
3
4
5
6
7
8
9
| // 从光源指向当前像素的向量
vec2 toLight = currentPos - lightPos;
// 计算角度(弧度,-π 到 +π)
float angle = atan(toLight.y, toLight.x);
// 计算与光源朝向的角度差
float angleDiff = abs(angle - lightDirection);
// 处理角度回绕(例如 -170° 和 +170° 实际只差 20°)
if (angleDiff > PI)
angleDiff = 2.0 * PI - angleDiff;
|
3.2 锥形衰减
1
2
3
4
5
| // 内锥角内:完全明亮
// 外锥角外:完全黑暗
// 内外之间:平滑过渡
float spotFactor = 1.0 - smoothstep(innerAngle, outerAngle, angleDiff);
attenuation *= spotFactor; // 结合之前的距离衰减
|
完整的聚光灯 Shader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| #version 450 core
in vec2 v_TexCoord;
out vec4 o_Color;
uniform vec2 u_LightPosition; // 屏幕空间 [0, 1]
uniform vec3 u_LightColor;
uniform float u_LightRadius; // 屏幕空间半径
uniform float u_LightIntensity;
uniform float u_LightFalloff;
uniform float u_AspectRatio;
// 聚光灯参数
uniform int u_IsSpotLight;
uniform float u_LightDirection; // 弧度
uniform float u_InnerAngle;
uniform float u_OuterAngle;
const float PI = 3.14159265359;
void main()
{
vec2 uv = v_TexCoord;
// 修正宽高比
vec2 correctedUV = uv;
correctedUV.x *= u_AspectRatio;
vec2 correctedLightPos = u_LightPosition;
correctedLightPos.x *= u_AspectRatio;
// 计算到光源的距离
vec2 toLight = correctedUV - correctedLightPos;
float dist = length(toLight);
// 距离衰减
float normalizedDist = dist / (u_LightRadius * u_AspectRatio);
float attenuation = 1.0 - pow(clamp(normalizedDist, 0.0, 1.0), u_LightFalloff);
attenuation = clamp(attenuation, 0.0, 1.0);
// 聚光灯角度衰减
float angle = atan(toLight.y, toLight.x);
float angleDiff = abs(angle - u_LightDirection);
// 处理角度回绕
if (angleDiff > PI)
angleDiff = 2.0 * PI - angleDiff;
float spotFactor = 1.0 - smoothstep(u_InnerAngle, u_OuterAngle, angleDiff);
attenuation *= spotFactor;
vec3 finalColor = u_LightColor * u_LightIntensity * attenuation;
o_Color = vec4(finalColor, 1.0);
}
|
其他问题
如何处理光源叠加?
场景中可能有多个光源——主角的手电筒、墙上的火把、地上的蜡烛…
最直接的想法是把所有光照效果相加:
在 OpenGL 中实现:
1
2
| glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE); // 源 × 1 + 目标 × 1
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| void OpenGLLightingPass::BeginLightMap(const LightingConfig& config)
{
m_lightMapFBO->Bind();
// 用环境光颜色清空光照图
glm::vec3 ambient = config.ambientColor * config.ambientIntensity;
glClearColor(ambient.r, ambient.g, ambient.b, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 启用加法混合
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE);
}
void OpenGLLightingPass::EndLightMap()
{
// 恢复标准 Alpha 混合
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
m_lightMapFBO->Unbind();
}
|
Framebuffer 渲染管线
渲染代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
| void LightLayer::OnUpdate(Yuicy::Timestep ts)
{
// 获取鼠标位置(屏幕坐标)
auto [mouseX, mouseY] = Yuicy::Input::GetMousePosition();
// 归一化、Y轴翻转
glm::vec2 mouseUV = {mouseX / viewportSize.x, 1.0f - (mouseY / viewportSize.y)};
// 假设灯光射出位置为屏幕中心
glm::vec2 lightPos = { 0.5f, 0.5f };
// 灯光照射角度
glm::vec2 dir = mouseUV - lightPos;
lightDirection = std::atan2(dir.y, dir.x);
// 场景渲染
sceneFbo->Bind();
Yuicy::RenderCommand::SetClearColor({ 0.1f, 0.1f, 0.1f, 1.0f });
Yuicy::RenderCommand::Clear();
// Render...
sceneFbo->Unbind();
// 灯光渲染
lightFbo->Bind();
// 清屏
glm::vec3 ambient = ambientColor * ambientIntensity;
Yuicy::RenderCommand::SetClearColor({ ambient.r, ambient.g, ambient.b, 1.0f });
Yuicy::RenderCommand::Clear();
if (lightEnabled)
{
// 混合模式处理灯光叠加
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE);
lightShader->Bind();
lightShader->SetFloat2("u_LightPosition", { 0.5f, 0.5f });
lightShader->SetFloat3("u_LightColor", lightColor);
lightShader->SetFloat("u_LightRadius", lightRadius);
lightShader->SetFloat("u_LightIntensity", lightIntensity);
lightShader->SetFloat("u_LightFalloff", lightFalloff);
lightShader->SetFloat("u_AspectRatio", viewportSize.x / viewportSize.y);
lightShader->SetFloat("u_LightDirection", lightDirection);
lightShader->SetFloat("u_InnerAngle", innerAngle);
lightShader->SetFloat("u_OuterAngle", outerAngle);
quadVao->Bind();
Yuicy::RenderCommand::DrawIndexed(quadVao);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}
lightFbo->Unbind();
// 场景混合着色器
Yuicy::RenderCommand::SetClearColor({ 0.0f, 0.0f, 0.0f, 1.0f });
Yuicy::RenderCommand::Clear();
compositeShader->Bind();
// 绑定场景纹理
uint32_t sceneTexId = sceneFbo->GetColorAttachmentRendererID();
// glBindTextureUnit(0, sceneTexId);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, sceneTexId);
compositeShader->SetInt("u_SceneTexture", 0);
// 绑定灯光纹理
uint32_t lightTexId = lightFbo->GetColorAttachmentRendererID();
// glBindTextureUnit(1, lightTexId);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, lightTexId);
compositeShader->SetInt("u_LightMap", 1);
compositeShader->SetInt("u_LightingEnabled", lightEnabled ? 1 : 0);
quadVao->Bind();
Yuicy::RenderCommand::DrawIndexed(quadVao);
}
|
要在后处理中加入光照,只需要将光照计算的结果与其他处理效果相乘:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| uniform sampler2D u_ScreenTexture; // 场景纹理
uniform sampler2D u_LightMap; // 光照图
uniform int u_LightingEnabled;
void main()
{
vec4 sceneColor = texture(u_ScreenTexture, v_TexCoord);
if (u_LightingEnabled != 0)
{
vec3 lightColor = texture(u_LightMap, v_TexCoord).rgb;
sceneColor.rgb *= lightColor;
}
o_Color = sceneColor;
}
|