Margoo

...?

【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. 略

添加评论