【EasyGPU】Lesson 4:EasyGPU 中的自定义结构体与 Callable 类
`Callable` 在先前的课程中就有所提及。在 EasyGPU 中,`Callable` 是专门为可复用 GPU 逻辑而设计的类,相当于 GPU 中的函数。本节的示例代码如下:
#include <GPU.h>
#include <graphics.h>
#pragma comment(lib, "opengl32.lib")
EASYGPU_STRUCT(Data,
(float, r1),
(float, r2)
);
Callable<float(Vec2, float)> Hexagram = [&](Float2 P, Float R) {
Float4 k = MakeFloat4(-0.5f, 0.8660254038f, 0.5773502692f, 1.7320508076f);
P = Abs(P);
P -= 2.0f * Min(Dot(MakeFloat2(k.x(), k.y()), P), 0.0f) * MakeFloat2(k.x(), k.y());
P -= 2.0f * Min(Dot(MakeFloat2(k.y(), k.x()), P), 0.0f) * MakeFloat2(k.y(), k.x());
P -= MakeFloat2(Clamp(P.x(), R * k.z(), R * k.w()), R);
Return(Length(P) * Sign(P.y()));
};
Callable<float(Vec2, float)> Hexagon = [&](Float2 P, Float R) {
Float3 k = MakeFloat3(-0.866025404f, 0.5f, 0.577350269f);
P = Abs(P);
P -= 2.0f * Min(Dot(k.xy(), P), 0.0f) * k.xy();
P -= MakeFloat2(Clamp(P.x(), -k.z() * R, k.z() * R), R);
Return(Length(P) * Sign(P.y()));
};
int main() {
initgraph(640, 480);
Uniform<int> time;
Uniform<Data> data;
Texture2D<PixelFormat::RGBA8> texture(getwidth(), getheight(), GetImageBuffer());
Kernel2D kernel("Rendering SDF", [&](Int& CoordX, Int& CoordY) {
Float X = (2.0f * ToFloat(CoordX) - getwidth()) / getheight();
Float Y = (2.0f * ToFloat(CoordY) - getheight()) / getheight();
auto t = ToFloat(time.Load());
auto r = data.Load();
auto tex = texture.Bind();
auto factor = Abs(Sin(t / 1000.0f));
auto d1 = Hexagon(MakeFloat2(X, Y), r.r1());
auto d2 = Hexagram(MakeFloat2(X, Y), r.r2());
auto d = d2 * factor + d1 * (1.0f - factor);
Float3 col;
If(d > 0.0f, [&] {
col = MakeFloat3(0.9f, 0.6f, 0.3f);
}).Else([&] {
col = MakeFloat3(0.65f, 0.85f, 1.0f);
});
col *= 1.0f - Exp(-6.0f * Abs(d));
col *= 0.8f + 0.2f * Cos(140.0f * d);
col = Mix(col, MakeFloat3(1.0f), 1.0f - Smoothstep(0.0f, 0.015f, Abs(d)));
tex.Write(CoordX, CoordY, MakeFloat4(col, 1.0f));
});
BeginBatchDraw();
while (true) {
time = clock();
data = Data{ 0.3f, 0.5f };
kernel.Dispatch((getwidth() + 15) / 16, (getheight() + 15) / 16, true);
texture.Download(GetImageBuffer());
FlushBatchDraw();
}
return 0;
}
编译运行可以得到一个随时间自动平滑变化的动态图像:

在完成了本节的学习后,你便已掌握了 EasyGPU 的所有基础概念。
`Callable` 类
`Callable` 类可以理解为 GPU 上的函数,它允许用户将 GPU 上可复用的逻辑封装起来以便反复使用。基本语法如下:
Callable<ReturnType(ArgType1, ArgType2, ...)> fn = [&](ArgType1 arg1, ArgType2 arg2, ...) {
...
Return(value);
};
值得注意的是,在 `Callable` 的模板参数中,类型应选择 C++ 端提供的标量类型而不是 `Var<T>` 类型。例如 `Float3` 应写为 `Vec3`,`Int2` 应写为 `IVec2`;而 lambda 参数的类型应为对应的 `Var<T>` 类型。以示例代码中的调用为例:
// ↓ 使用 float/Vec2 等 C++ 标量类型 ↓ 使用 Float2 等 GPU Var<T> 类型
Callable<float(Vec2, float)> Hexagram = [&](Float2 P, Float R) {
Float4 k = MakeFloat4(-0.5f, 0.8660254038f, 0.5773502692f, 1.7320508076f);
P = Abs(P);
P -= 2.0f * Min(Dot(MakeFloat2(k.x(), k.y()), P), 0.0f) * MakeFloat2(k.x(), k.y());
P -= 2.0f * Min(Dot(MakeFloat2(k.y(), k.x()), P), 0.0f) * MakeFloat2(k.y(), k.x());
P -= MakeFloat2(Clamp(P.x(), R * k.z(), R * k.w()), R);
Return(Length(P) * Sign(P.y()));
};
Callable 中使用 `Return` 来返回值,这与 Kernel 中的空 `Return()` 不同,后面我们会详细说明。
在 EasyGPU 中自定义结构体
在 EasyGPU 中,我们提供了 `EASYGPU_STRUCT` 接口来方便用户自定义结构体。如示例代码中所示:
EASYGPU_STRUCT(Data,
(float, r1),
(float, r2)
);
其基本语法为:
EASYGPU_STRUCT(Name,
(ScalarType, var1),
(ScalarType, var2),
...,
(ScalarType, varN)
);
`EASYGPU_STRUCT` 支持嵌套。用户在定义结构体后,可以在 C++ 中直接使用对应的类型:
EASYGPU_STRUCT(Material,
(GPU::Math::Vec3, albedo),
(float, roughness),
(float, metallic)
);
EASYGPU_STRUCT(Particle,
(GPU::Math::Vec3, position),
(GPU::Math::Vec3, velocity),
(float, life),
(int, type)
);
EASYGPU_STRUCT(RenderObject,
(Particle, particle),
(Material, material),
(int, objectId)
);
Particle cpuParticle;
cpuParticle.position = GPU::Math::Vec3(10.0f, 20.0f, 30.0f);
cpuParticle.velocity = GPU::Math::Vec3(1.0f, 2.0f, 3.0f);
cpuParticle.life = 5.0f;
cpuParticle.type = 2;
定义了 `EASYGPU_STRUCT` 后,可以在 `Kernel` 中直接使用 `Var<T>` 创建对应的变量:
EASYGPU_STRUCT(Data,
(float, r1),
(float, r2)
);
Kernel2D kernel([&](Int& CoordX, Int& CoordY) {
Var<Data> d;
// 使用 d.r1() 和 d.r2() 访问成员
});
结构体还支持通过 `Buffer<T>` 传入 GPU:
EASYGPU_STRUCT(ColorRGBA,
(float, r),
(float, g),
(float, b),
(float, a)
);
Buffer<ColorRGBA> colorsBuffer(colorsCPU, BufferMode::ReadWrite);
Kernel1D kernel([&](Int& id) {
auto cols = colorsBuffer.Bind();
Var<ColorRGBA> c = cols[id];
c.r() = 1.0f - c.r();
c.g() = 1.0f - c.g();
c.b() = 1.0f - c.b();
cols[id] = c;
});
⚠️ 重要提示:`EASYGPU_STRUCT` 不能定义在任何命名空间下,必须定义在全局作用域。
`Callable` 与 Kernel 中 `Return` 的区别
在前面的课程中我们已经介绍了 Kernel 中的 `Return()`,它能提前结束当前工作项的执行。而在 `Callable` 中,`Return` 用于返回一个值给调用方。
| 使用场景 | 语法 | 作用 |
|---------|------|------|
| Kernel | `Return();` | 提前退出当前工作项 |
| Callable | `Return(value);` | 返回计算结果 |
| 使用场景 | 语法 | 作用 |
| Kernel | Return(); | 提前退出当前工作项 |
| Callable | Return(value); | 返回计算结果 |
在 `Callable` 中,如果返回值类型不是 `void`,则必须使用 `Return` 返回一个值,否则会导致编译错误。
课后作业
作业题目
1. 试阐述以下问题:
- `Callable` 的基本语法是什么?模板参数中的类型与 lambda 参数中的类型有什么区别?为什么要这样设计?
- `EASYGPU_STRUCT` 的作用是什么?如何在 Kernel 中创建和使用自定义结构体变量?
- `Callable` 中的 `Return` 与 Kernel 中的 `Return` 有什么区别?在什么情况下 `Callable` 必须使用 `Return`?
2. 使用 `Callable`、`EASYGPU_STRUCT` 和 `Uniform` 实现一个多形状 SDF 渲染器。
要求:
- 使用 `EASYGPU_STRUCT` 定义一个 `ShapeParams` 结构体,包含至少两个 `float` 类型的参数(如 `radius`、`blendFactor` 等)
- 创建至少两个 `Callable`:
-
- `CircleSDF`:计算圆形的有符号距离,函数签名为 `float(Vec2, float)`(位置、半径)
- `BoxSDF`:计算矩形的有符号距离,函数签名为 `float(Vec2, Vec2)`(位置、半边长)
-
- 使用 `Uniform<int>` 传入时间,使用 `Uniform<ShapeParams>` 传入形状参数
- 在 Kernel 中:
-
- 将屏幕坐标归一化到 [-1, 1] 范围
- 调用 `CircleSDF` 和 `BoxSDF` 计算两个形状的距离
- 使用 `Mix` 函数根据 `blendFactor` 混合两个距离值(实现形状渐变效果)
- 根据最终距离值输出颜色(内部/外部用不同颜色,边界可加亮)
-
- 实现动画效果:让 `blendFactor` 随时间周期性变化(如 0→1→0)
- 使用 `Texture2D` 将结果输出到屏幕
提示:
- 圆形 SDF:
- 矩形 SDF:
3. (可选)使用 `EASYGPU_STRUCT`、`Buffer<T>` 和 `Callable` 实现一个简单的**粒子系统**。
要求:
- 定义 `Particle` 结构体,包含位置(`Vec2`)、速度(`Vec2`)、生命周期(`float`)等属性
- 创建 `Buffer<Particle>` 存储 1000 个粒子
- 创建 `Callable` 封装粒子更新逻辑(位置更新、生命周期衰减)
- 在 Kernel 中遍历所有粒子,调用 Callable 更新状态
- 只绘制生命周期大于 0 的粒子
- (进阶)粒子生命周期结束后重置到屏幕中心并赋予随机速度
答案
1. 略
2.
#include <GPU.h>
#include <graphics.h>
#pragma comment(lib, "opengl32.lib")
EASYGPU_STRUCT(ShapeParams,
(float, radius), // 圆形半径
(float, blendFactor) // 混合因子 0~1
);
// 圆形 SDF:距离 = |P| - R
Callable<float(Vec2, float)> CircleSDF = [&](Float2 P, Float R) {
Return(Length(P) - R);
};
// 矩形 SDF
Callable<float(Vec2, Vec2)> BoxSDF = [&](Float2 P, Float2 B) {
Float2 d = Abs(P) - B;
Return(Length(Max(d, MakeFloat2(0.0f, 0.0f))) + Min(Max(d.x(), d.y()), 0.0f));
};
int main() {
initgraph(640, 480);
Uniform<int> time;
Uniform<ShapeParams> params;
Texture2D<PixelFormat::RGBA8> texture(getwidth(), getheight(), GetImageBuffer());
try {
Kernel2D kernel("MultiShape SDF", [&](Int& X, Int& Y) {
// 归一化坐标到 [-1, 1]
Float2 uv = MakeFloat2(
(2.0f * ToFloat(X) - getwidth()) / getheight(),
(2.0f * ToFloat(Y) - getheight()) / getheight()
);
auto p = params.Load();
auto t = ToFloat(time.Load());
// 动画:圆形位置随时间移动
Float2 circlePos = MakeFloat2(
0.3f * Cos(t / 500.0f),
0.3f * Sin(t / 500.0f)
);
// 计算两个形状的 SDF
Float d1 = CircleSDF(uv - circlePos, p.radius());
Float d2 = BoxSDF(uv, MakeFloat2(0.3f, 0.3f));
// 混合两个距离场
Float d = Mix(d1, d2, p.blendFactor() + 0.3f * Sin(t / 300.0f));
// 根据距离着色
Float3 col;
If(d > 0.0f, [&] {
// 外部:淡蓝色
col = MakeFloat3(0.65f, 0.85f, 1.0f);
}).Else([&] {
// 内部:橙色
col = MakeFloat3(0.9f, 0.6f, 0.3f);
});
// 边界高光
col *= 1.0f - Exp(-4.0f * Abs(d));
col = Mix(col, MakeFloat3(1.0f), 1.0f - Smoothstep(0.0f, 0.02f, Abs(d)));
auto tex = texture.Bind();
tex.Write(X, Y, MakeFloat4(col, 1.0f));
});
BeginBatchDraw();
while (true) {
time = clock();
// 动态修改参数
params = ShapeParams{ 0.25f, 0.5f };
kernel.Dispatch((getwidth() + 15) / 16, (getheight() + 15) / 16, true);
texture.Download(GetImageBuffer());
FlushBatchDraw();
}
EndBatchDraw();
}
catch (ShaderCompileException& e) {
MessageBoxA(nullptr, e.what(), "Shader Compile Error", MB_OK | MB_ICONERROR);
std::cout << e.GetBeautifulOutput() << std::endl;
}
closegraph();
return 0;
}
3. 略
添加评论
取消回复