LYGIA Shader Library

repository·main·Indexed 25 days ago

https://github.com/patriciogonzalezvivo/lygia

A granular, cross-platform, and multi-language shader library providing a vast collection of reusable functions. It supports GLSL, HLSL, WGSL, Metal, and CUDA, and integrates with various environments via #include directives or the WESL (WebGPU Shading Language) superset. The library is organized into categories such as math, color, generative, SDF, and lighting, and can be installed via Git, NPM, or Cargo.

Tokens
3.4K
Snippets
9
Records
21
Agent score
86%

What's inside LYGIA

  1. Overview of LYGIA Shader Library

    main
    LYGIA is a large-scale, cross-platform, and multi-language shader library composed of reusable functions. It is designed to be granular, flexible, and efficient, allowing developers to prototype and port shader projects quickly. The library supports multiple shading languages and integrates with a wide variety of environments, engines, and frameworks.
  2. Overview of WESL (WebGPU Shading Language)

    main

    WESL is a superset of WGSL designed to improve shader development. Key features include:

    • import statements: Allows splitting shader code across files and loading libraries (npm or cargo).
    • @if, @else, @elseif directives: Enables conditional compilation to assemble specialized shaders at build time or runtime.

    Lygia provides its WebGPU-compatible shaders using the .wesl extension. While older .wgsl versions exist, the .wesl versions are more complete and recommended for new users.

  3. Understand Lygia shader design patterns

    main

    Lygia follows specific design patterns for shader functions to ensure modularity and multi-language support:

    • Granularity: Each function typically resides in its own file (e.g., myFunc.glsl contains myFunc()).
    • Multi-language: Functions are provided across multiple shading languages including GLSL (*.glsl), HLSL (*.hlsl), WGSL (*.wgsl), Metal (*.msl), and CUDA (*.cuh).
    • Self-documentation: Files contain a YAML-structured comment at the top detailing contributors, description, use (function signature), notes, and options (#define flags).
    • Collision Prevention: Functions are wrapped in #ifndef FNC_NAME guards to prevent name collisions.
    • Templating: Reusability is achieved via #define options. Common patterns include templating the return type (e.g., MYFUNC_TYPE) or the sampling function (e.g., MYFUNC_SAMPLER_FNC).
    • Argument Ordering: Optional arguments are placed at the end. Arguments should be sorted by memory footprint: SAMPLER_TYPE, mat4, mat3, mat2, vec4, vec3, vec2, float, ivec4, ivec3, ivec2, int, bool.
  4. Add a shader to Lygia WESL

    main

    To contribute a new shader to the Lygia WESL library, follow these steps:

    1. Create a .wesl file: Place the file alongside existing shader files. Follow the Lygia convention of having one user-facing function per file to optimize application bundle sizes. You may include multiple type variants of the same function (e.g., one for f32 and one for vec3f) within a single file.
    2. Add tests: Create corresponding tests in the test/wesl directory using the appropriate testing method:
      • Use testCompute() for pure math functions.
      • Use testFragment() for derivative functions (like fwidth, dpdx, dpdy) or texture sampling.
      • Use toMatchImage() for visual regression tests such as filters, generative patterns, or complex rendering.
  5. Resolve LYGIA dependencies with build tools (Vite, esbuild, Webpack)

    main

    If you are working with local .glsl files in a modern build pipeline, you can use existing plugins to handle #include directives automatically:

    • Vite: Use vite-plugin-glsl to import local dependencies or load inline shaders.
    • esbuild: Use esbuild-plugin-glsl-include to import local .glsl dependencies.
    • Webpack: Use webpack-glsl-loader to import local dependencies.
  6. Implement WGSL function naming conventions

    main

    Because WGSL does not support function overloading, you must use unique function names that encode the size of the return type and the parameter types using suffixes.

    Naming Rules:

    1. Consistent scalar types: No suffix needed (e.g., fn random(p: f32) -> f32).
    2. Consistent return type, varying parameter size: Suffix the parameter size (e.g., random2 for vec2f parameters, random3 for vec3f).
    3. Inconsistent return and parameter types: Use two suffixes: [Return Size][Parameter Size].

    Suffix Mapping:

    • vec2<T> or f2 $\rightarrow$ 2
    • vec3<T> or f3 $\rightarrow$ 3
    • vec4<T> or f4 $\rightarrow$ 4
    • Scalar $\rightarrow$ 1

    Example Suffix Logic:

    • random21: Returns vec2, accepts f32 (scalar).
    • random32: Returns vec3, accepts vec2.
    • random44: Returns vec4, accepts vec4.
    // Consistent scalar
    fn random(p: f32) -> f32 { ... }
    
    // Return type consistent, parameter size varies
    fn random2(p: vec2f) -> f32 { ... }
    fn random3(p: vec3f) -> f32 { ... }
    
    // Both return and parameter types vary (ReturnSize + ParamSize)
    fn random21(p: f32) -> vec2f { ... }
    fn random22(p: vec2f) -> vec2f { ... }
    fn random32(p: vec2f) -> vec3f { ... }
    fn random43(p: vec3f) -> vec4f { ... }
  7. Install and use Lygia in Rust

    main

    To use Lygia in a Rust project, add the lygia crate. WESL tools are also available for Rust developers.

    Installation

    cargo add lygia

    Linking at build time

    Add wesl to your build dependencies and use a build.rs file to generate artifacts.

    cargo add --build wesl
    /// build.rs
    fn main() {
        wesl::Wesl::new("src/shaders").build_artifact("main.wesl", "my_shader");
    }

    Linking at run-time

    Add wesl to your dependencies to compile shaders during application execution.

    cargo add wesl
    let shader_string = Wesl::new("src/shaders")
        .compile("main.wesl")
        .inspect_err(|e| eprintln!("WESL error: {e}"))
        .unwrap()
        .to_string();

    Using the Rust CLI tool

    Install the CLI to compile shaders directly from the terminal.

    cargo install wesl-cli
    wesl compile <path/to/shader.wesl>
  8. Configure Lygia with a JavaScript/TypeScript Bundler

    main

    For applications using vite, webpack, or rollup, install wesl and wesl-plugin. You can choose between runtime or static linking:

    Runtime Linking

    Use the ?link suffix on your import. This allows you to use WESL's conditional compilation features (@if directives) to adapt shaders dynamically at runtime based on GPU capabilities or user settings. This requires the wesl linker in your runtime bundle.

    Static Linking

    Use the ?static suffix. This bundles shaders into a single transpiled WGSL string at build time. This is more efficient (~15KB smaller bundle) but prevents dynamic adaptation via @if directives.

    // Runtime Linking
    import appWesl from "../shaders/app.wesl?link";
    import { link } from "wesl";
    
    const linked = await link(appWesl);
    linked.createShaderModule(gpuDevice);
    
    // Static Linking
    import appWgsl from "../shaders/app.wesl?static";
  9. Resolve LYGIA dependencies via LYGIA server

    main

    For cloud platforms (like CodePen or Observable) where you cannot host local files, use the LYGIA server to resolve #include dependencies online.

    1. Include the resolver script in your HTML:

      • JavaScript: <script src="https://lygia.xyz/resolve.js"></script>
      • ES6 Module: <script type="module">import resolveLygia from "https://lygia.xyz/resolve.esm.js"</script>
    2. Call the resolver in your JavaScript code to transform your shader source code strings.

  10. Install LYGIA locally

    main

    You can install LYGIA locally to bundle it with your project. Choose one of the following methods:

    • Git Clone: git clone https://github.com/patriciogonzalezvivo/lygia.git
    • Git Submodule: git submodule add https://github.com/patriciogonzalezvivo/lygia.git
    • Degit (No history): npx degit https://github.com/patriciogonzalezvivo/lygia.git lygia
    • NPM: Use the @lygia/lygia package.

    If you want to reduce the library size by only keeping the files for your specific language (e.g., GLSL), use the prune.py script.

  11. Resolve LYGIA shader dependencies using JavaScript

    main

    To use LYGIA functions in a web environment, you must resolve the #include "path/to/file.glsl" dependencies into a single shader string. You can use the resolveLygia() or resolveLygiaAsync() functions provided by the resolve-lygia npm module or the vanilla JS resolver at lygia.xyz/resolve.js.

    These functions take a string or string[] (representing your shader code or a list of shader files) and parse it, solving all #include dependencies. To support specific versions of LYGIA, use the path pattern lygia/vX.X.X/... within your dependency paths.

  12. Use LYGIA functions in shaders via #include

    main

    To use LYGIA in your shaders, use the #include directive to import specific functions. Ensure your environment is configured to resolve these include paths. Each function is typically contained in its own file.

    #include "lygia/space/ratio.glsl"
    #include "lygia/math/decimate.glsl"
    #include "lygia/draw/circle.glsl"
    
    void main(void) {
        // ... use functions like ratio(), decimate(), or circle()
    }