Language2 min read

Modules

Reusable code organization with modules for sharing functions, structs, and constants across shaders.

Reading Time
2 min
Word Count
192
Sections
13
Try It Live

Pressure-test the syntax

Take the concept from this page into the playground and deliberately break a pass, binding, or type signature to see how the compiler responds.

Try a Live Edit

Modules in BWSL provide a way to organize and reuse code across multiple shaders. They allow you to define functions, structs, and constants that can be imported into pipelines or other modules.

Defining a Module

A module is declared with the module keyword followed by the module name:

bwsl
module Math {
const float PI = 3.14159265358979323846;
const float TAU = 6.28318530717958647692;
square :: (float x) -> float {
return x * x;
}
}

Importing Modules

To use a module in a pipeline or another module, use the import statement:

bwsl
pipeline MyPipeline {
import Math
pass "MainPass" {
vertex {
output.position = float4(0.0, 0.0, 0.0, 1.0);
}
fragment {
float x = 0.5;
float result = Math::square(x);
output.color = float4(result, result, result, 1.0);
}
}
}

Accessing Module Members

Module members are accessed using the :: (scope resolution) operator:

bwsl
float angle = Math::PI * 0.5;
float doubled = Math::square(value);

Module Contents

Modules can contain:

  • Constants: Compile-time constant values
  • Functions: Reusable shader functions
  • Structs: Custom data types

Constants

bwsl
module Math {
const float PI = 3.14159265358979323846;
const float TAU = 6.28318530717958647692;
const float E = 2.71828182845904523536;
const float PHI = 1.61803398874989484820;
const float EPSILON = 1e-6;
const float SQRT_2 = 1.41421356237309504880;
}

Functions

Functions use the :: syntax for declaration:

bwsl
module Math {
square :: (float x) -> float {
return x * x;
}
cube :: (float x) -> float {
return x * x * x;
}
// Function overloading is supported
square :: (float3 x) -> float3 {
return x * x;
}
}

Structs

bwsl
module PBR {
struct PBRMaterial {
float3 albedo;
float roughness;
float metallic;
float ao;
};
}

Module Dependencies

Modules can import other modules:

bwsl
module PBR {
import Math
// Fresnel-Schlick Approximation
fresnelSchlick :: (float cosTheta, float3 F0) -> float3 {
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
// Lambertian Diffuse uses Math::PI
lambertianDiffuse :: (float3 albedo) -> float3 {
return albedo / Math::PI;
}
}

Example: Math Module

Here's a comprehensive Math module with common utilities:

bwsl
module Math {
// Constants
const float PI = 3.14159265358979323846;
const float TAU = 6.28318530717958647692;
const float E = 2.71828182845904523536;
const float EPSILON = 1e-6;
const float SQRT_2 = 1.41421356237309504880;
const float INV_PI = 0.31830988618379067154;
// Range Mapping
inverse_lerp :: (float a, float b, float value) -> float {
return (value - a) / (b - a);
}
remap :: (float value, float in_min, float in_max, float out_min, float out_max) -> float {
float t = (value - in_min) / (in_max - in_min);
return out_min + t * (out_max - out_min);
}
// Safe Operations
safe_divide :: (float a, float b) -> float {
if (abs(b) > EPSILON) {
return a / b;
}
return 0.0;
}
safe_normalize :: (float3 v) -> float3 {
float len_sq = dot(v, v);
if (len_sq > EPSILON * EPSILON) {
return v * rsqrt(len_sq);
}
return float3(0.0, 1.0, 0.0);
}
// Power Shortcuts
square :: (float x) -> float {
return x * x;
}
cube :: (float x) -> float {
return x * x * x;
}
// 2D Transformations
rotate_2d :: (float2 p, float angle) -> float2 {
float c = cos(angle);
float s = sin(angle);
return float2(p.x * c - p.y * s, p.x * s + p.y * c);
}
// Step Functions
linear_step :: (float edge0, float edge1, float x) -> float {
return saturate((x - edge0) / (edge1 - edge0));
}
// Perlin's improved smoothstep (quintic)
smootherstep :: (float edge0, float edge1, float x) -> float {
float t = saturate((x - edge0) / (edge1 - edge0));
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}
// Easing Functions
ease_in_quad :: (float t) -> float {
return t * t;
}
ease_out_quad :: (float t) -> float {
return t * (2.0 - t);
}
ease_in_out_quad :: (float t) -> float {
if (t < 0.5) {
return 2.0 * t * t;
}
return 1.0 - 2.0 * (1.0 - t) * (1.0 - t);
}
// Wave Functions
triangle_wave :: (float x) -> float {
return abs(fract(x) * 2.0 - 1.0);
}
}

Example: PBR Module

A module for physically-based rendering calculations:

bwsl
module PBR {
import Math
struct PBRMaterial {
float3 albedo;
float roughness;
float metallic;
float ao;
};
// Fresnel-Schlick Approximation
fresnelSchlick :: (float cosTheta, float3 F0) -> float3 {
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
// GGX/Trowbridge-Reitz Normal Distribution Function
distributionGGX :: (float NdotH, float roughness) -> float {
float alpha = roughness * roughness;
float alphaSqr = alpha * alpha;
float denom = NdotH * NdotH * (alphaSqr - 1.0) + 1.0;
return alphaSqr / (Math::PI * denom * denom);
}
// Smith's Geometry Function (Schlick-GGX)
geometrySchlickGGX :: (float NdotV, float roughness) -> float {
float k = (roughness + 1.0) * (roughness + 1.0) / 8.0;
return NdotV / (NdotV * (1.0 - k) + k);
}
geometrySmith :: (float NdotV, float NdotL, float roughness) -> float {
float ggx1 = geometrySchlickGGX(NdotV, roughness);
float ggx2 = geometrySchlickGGX(NdotL, roughness);
return ggx1 * ggx2;
}
// Lambertian Diffuse
lambertianDiffuse :: (float3 albedo) -> float3 {
return albedo / Math::PI;
}
// Complete Cook-Torrance Specular BRDF
cookTorranceSpecular :: (float3 N, float3 V, float3 L, float3 F, float roughness) -> float3 {
float3 H = normalize(V + L);
float NdotH = max(dot(N, H), 0.0);
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
float D = distributionGGX(NdotH, roughness);
float G = geometrySmith(NdotV, NdotL, roughness);
float3 numerator = D * F * G;
float denominator = 4.0 * NdotV * NdotL + 0.001;
return numerator / denominator;
}
// Complete Direct Lighting Calculation
calculateDirectLighting :: (PBRMaterial mat, float3 N, float3 V, float3 L, float3 radiance) -> float3 {
float NdotL = max(dot(N, L), 0.0);
float3 H = normalize(V + L);
float VdotH = max(dot(V, H), 0.0);
float3 F0 = lerp(float3(0.04), mat.albedo, mat.metallic);
float3 F = fresnelSchlick(VdotH, F0);
float3 kD = (1.0 - F) * (1.0 - mat.metallic);
float3 diffuse = kD * lambertianDiffuse(mat.albedo);
float3 specular = cookTorranceSpecular(N, V, L, F, mat.roughness);
return (diffuse + specular) * radiance * NdotL;
}
}

Using PBR in a Pipeline

bwsl
pipeline PBRShader {
import PBR
import Math
resources {
mvp: mat4
model: mat4
normalMatrix: mat3
cameraPos: float3
lightDir: float3
lightColor: float3
albedoMap: texture2D
roughnessMap: texture2D
metallicMap: texture2D
aoMap: texture2D
materialSampler: sampler
}
pass "Lighting" {
use resources {
mvp, model, normalMatrix, cameraPos, lightDir, lightColor,
albedoMap, roughnessMap, metallicMap, aoMap, materialSampler
}
vertex {
output.position = resources.mvp * float4(attributes.position, 1.0);
output.worldPos = (resources.model * float4(attributes.position, 1.0)).xyz;
output.normal = normalize(resources.normalMatrix * attributes.normal);
}
fragment {
PBR::PBRMaterial mat;
mat.albedo = sample(resources.albedoMap, resources.materialSampler, input.uv).rgb;
mat.roughness = sample(resources.roughnessMap, resources.materialSampler, input.uv).r;
mat.metallic = sample(resources.metallicMap, resources.materialSampler, input.uv).r;
mat.ao = sample(resources.aoMap, resources.materialSampler, input.uv).r;
float3 N = normalize(input.normal);
float3 V = normalize(resources.cameraPos - input.worldPos);
float3 L = normalize(resources.lightDir);
float3 color = PBR::calculateDirectLighting(mat, N, V, L, resources.lightColor);
color *= mat.ao;
output.color = float4(color, 1.0);
}
}
}

Performance

Modules are parsed once and cached to avoid redundant work. When a module is imported, only the functions and types that are actually used are folded into the final shader. This keeps compiled shaders lean and avoids code bloat.

See Also