#shader #ir #naga #import #composition #manipulating #combining

naga_oil

一个用于使用 naga IR 组合和操作着色器的 crate

20 个版本 (破坏性)

0.15.0 2024 年 8 月 5 日
0.14.0 2024 年 5 月 28 日
0.13.0 2024 年 2 月 2 日
0.11.0 2023 年 11 月 17 日
0.3.0 2022 年 11 月 23 日

#8图形 API

Download history 15154/week @ 2024-05-03 14757/week @ 2024-05-10 14099/week @ 2024-05-17 16053/week @ 2024-05-24 15909/week @ 2024-05-31 14865/week @ 2024-06-07 16322/week @ 2024-06-14 14571/week @ 2024-06-21 14976/week @ 2024-06-28 19534/week @ 2024-07-05 17488/week @ 2024-07-12 18131/week @ 2024-07-19 21207/week @ 2024-07-26 16358/week @ 2024-08-02 20527/week @ 2024-08-09 15007/week @ 2024-08-16

每月 76,023 次下载
555 个 crate 中使用 (5 个直接使用)

MIT/Apache

355KB
9K SLoC

Naga 组织集成库 (naga-oil) 是一个用于组合和操作着色器的 crate。

  • compose 提供了一个模块化着色器组合框架
  • prune 将着色器精简到所需的部分

以及可能对外部不太有用的部分

  • derive 允许将多个着色器中的项目导入到单个着色器中
  • redirect 通过替换函数调用和修改绑定来修改着色器

组合

组合模块允许从模块(它们自身也是着色器)构建着色器。

它通过将着色器视为模块,并

  • 独立地将每个模块构建为 naga IR
  • 为每种支持的语言创建 "头文件",这些文件用于构建依赖的模块/着色器
  • 通过将着色器 IR 与导入模块的 IR 相结合来制作最终着色器

对于多个具有大型公共导入的小型着色器,这可能比解析每个着色器的完整源代码更快,并且允许以更干净、更模块化的方式构建着色器,同时更好地控制作用域。

导入

着色器可以作为模块添加到组合器中。这使得它们的类型、常量、变量和函数可用于导入它们的模块/着色器。请注意,如果模块定义具有绑定的全局变量,则导入模块将影响最终着色器的全局状态。

模块可以包含一个 #define_import_path 指令,用于命名模块

#define_import_path my_module

fn my_func() -> f32 {
	return 1.0;
}

模块名称还可以作为参数指定给 Composer::add_composable_module

着色器可以使用 #import 指令(可选带有 as 名称)导入模块

#import my_module;
#import my_other_module as mod2;

fn main() -> f32 {
    let x = my_module::my_func();
    let y = mod2::my_other_func();
    return x*y;
}

或导入逗号分隔的各个项目列表

#import my_module::{my_func, my_const}

fn main() -> f32 {
    return my_func(my_const);
}

支持一些 Rust 风格的导入语法,可以直接使用完全限定的项目名称导入

#import my_package::{
    first_module::{item_one as item, item_two}, 
    second_module::submodule,
}

fn main() -> f32 {
    return item + item_two + submodule::subitem + my_package::third_module::item;
}

module::selfmodule::* 目前不支持。

导入可以嵌套 - 模块可以导入其他模块,但不能递归地导入。当添加新模块时,所有其 #import 必须已添加。同一模块可以被导入树中的不同模块多次导入。命名空间没有重叠,因此可以在不同的模块中使用相同的函数名(或类型、常量或变量名)。

注意:最终的着色器将包括使用到的任何导入项目的所需依赖项(绑定、全局变量、常量、其他函数),但不会包括导入模块的其余部分。

覆盖函数

可以使用 virtual 关键字声明虚函数

    virtual fn point_light(world_position: vec3<f32>) -> vec3<f32> { ... }

导入模块中定义的虚函数可以使用 override 关键字覆盖

#import bevy_pbr::lighting as Lighting

override fn Lighting::point_light (world_position: vec3<f32>) -> vec3<f32> {
    let original = Lighting::point_light(world_position);
    let quantized = vec3<u32>(original * 3.0);
    return vec3<f32>(quantized) / 3.0;
}

覆盖必须在顶层着色器中声明,或者包含覆盖的模块必须作为 additional_import 导入到 Composer::add_composable_moduleComposer::make_naga_module 调用中。由于树摇,使用 #import 导入带有覆盖的模块将不会工作。

覆盖函数定义会导致整个着色器作用域中原始函数的所有调用都被替换为对新函数的调用,但覆盖函数内部的调用除外。

覆盖函数的签名必须与基函数匹配。

覆盖可以在最终着色器导入树的任何位置指定。

可以对同一函数应用多个覆盖。例如,给定

  • 包含函数 f 的模块 a
  • 导入 a 并包含覆盖函数 override a::f 的模块 b
  • 导入 ab 并包含覆盖函数 override a::f 的模块 c

那么 bc 都指定了对 a::f 的覆盖。

模块 b 中声明的 override fn a::f 可能在其体内部调用 a::f

模块 c 中声明的 override fn a::f 可能在其体内部调用 a::f,但调用将被重定向到 b::f

a::f(在模块 ab 内,或任何其他地方)的任何其他调用都将重定向到 c::f

通过这种方式,可以应用一系列或堆栈形式的覆盖。

同一函数的不同覆盖可以在不同的导入分支中指定。最终堆栈将根据导入树中覆盖首次出现的位置进行排序(使用深度优先搜索)。

请注意,模块/着色器的导入按顺序处理,但它们在当前着色器/模块的主体之前进行处理,无论它们在该模块中的位置如何,因此无法导入包含覆盖的模块并将调用注入到导入覆盖之前。相反,您可以创建两个每个都包含覆盖的模块,并将它们导入到父模块/着色器中,以按所需的方式排序。

覆盖函数目前只能定义在 wgsl 中。

如果启用了 override_any 铁砧功能,则不需要对被覆盖的函数使用 virtual 关键字。

语言

模块可以用 GLSL 或 WGSL 编写。具有入口点的着色器可以作为模块导入(前提是它们具有 #define_import_path 指令)。入口点可以通过其名称(对于 WGSL)或通过 module::main(对于 GLSL)从导入的模块中调用。

最终着色器也可以用 GLSL 或 WGSL 编写。对于 GLSL 用户,必须通过 ShaderType 参数指定着色器是顶点着色器还是片段着色器(GLSL 计算着色器不受支持)。

预处理

生成最终着色器或添加可组合的模块时,必须提供一组 shader_def 字符串/值对。值可以是 bool(ShaderDefValue::Bool)、i32(ShaderDefValue::Int)或 u32(ShaderDefValue::UInt)。

这些允许对模块和最终着色器的一部分进行条件编译。条件编译使用 #if / #ifdef / #ifndef#else#endif 预处理器指令执行。

fn get_number() -> f32 {
    #ifdef BIG_NUMBER
        return 999.0;
    #else
        return 0.999;
    #endif
}

#ifdef 指令在定义名称存在于输入绑定集时匹配(无论值如何)。#ifndef 指令正好相反。

#if 指令需要一个定义名称、一个运算符和一个比较值。

  • 定义名称必须是提供的 shader_def 名称。
  • 运算符必须是以下之一:==!=>=><<=
  • 当与ShaderDefValue::IntShaderDefValue::Uint进行比较时,值必须是一个整数字面量;当与ShaderDef::Bool进行比较时,值为truefalse

着色器定义还可以在着色器源中使用#SHADER_DEF#{SHADER_DEF},并将用它们的值进行替换。

预处理器的分支指令(ifdefifndefif)可以与#else一起使用,以创建更复杂的控制流。

fn get_number() -> f32 {
    #ifdef BIG_NUMBER
        return 999.0;
    #else if USER_NUMBER > 1
        return f32(#USER_NUMBER)
    #else
        return 0.999;
    #endif
}

着色器定义可以通过使用#define指令在顶级着色器开始处创建或覆盖。

#define USER_NUMBER 42

如果未指定,创建的值默认为true

错误报告

使用错误emit_to_string方法可以提供错误报告的codespan。这需要启用验证,默认情况下是启用的。Composer::non_validating()产生一个非验证的作曲家,不能提供准确的错误报告。

修剪

  • 根据指定的所需输出从着色器中删除死代码和绑定。旨在用于从任意顶点/片段着色器构建减少深度和/或法线着色器。

待定文档

重定向

  • 重定向函数调用
  • 待定:重新绑定全局绑定
  • 将来某天:在均匀、纹理和缓冲区访问之间进行转换,以便可以在间接传递中使用为直接传递编写的着色器

待定文档

推导

  • 从一个或多个现有模块的部分构建一个单一的自我包含的naga模块

待定文档

依赖项

~8–17MB
~212K SLoC