pub const STANDARD_PBR_WGSL: &str = "// Standard PBR Material Shader\n// Implements metallic-roughness workflow for physically-based rendering\n\n// --- Vertex Shader ---\n\n// Camera uniform block containing view and projection matrices\nstruct CameraUniforms {\n view_projection: mat4x4<f32>, // Combined projection * view matrix\n camera_position: vec4<f32>, // Camera position in world space (w is padding)\n};\n\n@group(0) @binding(0)\nvar<uniform> camera: CameraUniforms;\n\n// Vertex input from the vertex buffer\nstruct VertexInput {\n @location(0) position: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) color: vec3<f32>,\n};\n\n// Output from vertex shader to fragment shader\nstruct VertexOutput {\n @builtin(position) clip_position: vec4<f32>,\n @location(0) world_position: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) color: vec3<f32>,\n};\n\n// Model transform uniform\nstruct ModelUniforms {\n model_matrix: mat4x4<f32>,\n normal_matrix: mat4x4<f32>, // For transforming normals\n};\n\n@group(1) @binding(0)\nvar<uniform> model: ModelUniforms;\n\n@vertex\nfn vs_main(input: VertexInput) -> VertexOutput {\n var out: VertexOutput;\n \n // Transform vertex position to world space\n let world_pos = model.model_matrix * vec4<f32>(input.position, 1.0);\n out.world_position = world_pos.xyz;\n \n // Transform to clip space\n out.clip_position = camera.view_projection * world_pos;\n \n // Transform normal to world space (using normal matrix to handle non-uniform scales)\n out.normal = normalize((model.normal_matrix * vec4<f32>(input.normal, 0.0)).xyz);\n \n // Pass through UV and color\n out.uv = input.uv;\n out.color = input.color;\n \n return out;\n}\n\n\n// --- Fragment Shader ---\n\n// Material properties uniform\nstruct MaterialUniforms {\n base_color: vec4<f32>, // RGBA base color\n emissive: vec3<f32>, // RGB emissive color\n metallic: f32, // Metallic factor [0-1]\n roughness: f32, // Roughness factor [0-1]\n alpha_cutoff: f32, // For alpha masking\n _padding: vec2<f32>, // Alignment padding\n};\n\n@group(2) @binding(0)\nvar<uniform> material: MaterialUniforms;\n\n// Simple directional light for basic lighting\nstruct DirectionalLight {\n direction: vec3<f32>,\n _padding1: f32,\n color: vec3<f32>,\n intensity: f32,\n};\n\n@group(3) @binding(0)\nvar<uniform> light: DirectionalLight;\n\n// Constants\nconst PI: f32 = 3.14159265359;\n\n// Simplified PBR calculations\n// This is a basic implementation - will be enhanced with full PBR in issue #48\n\nfn fresnel_schlick(cosTheta: f32, F0: vec3<f32>) -> vec3<f32> {\n return F0 + (vec3<f32>(1.0) - F0) * pow(1.0 - cosTheta, 5.0);\n}\n\nfn distribution_ggx(N: vec3<f32>, H: vec3<f32>, roughness: f32) -> f32 {\n let a = roughness * roughness;\n let a2 = a * a;\n let NdotH = max(dot(N, H), 0.0);\n let NdotH2 = NdotH * NdotH;\n \n let nom = a2;\n var denom = (NdotH2 * (a2 - 1.0) + 1.0);\n denom = PI * denom * denom;\n \n return nom / denom;\n}\n\nfn geometry_schlick_ggx(NdotV: f32, roughness: f32) -> f32 {\n let r = (roughness + 1.0);\n let k = (r * r) / 8.0;\n \n let nom = NdotV;\n let denom = NdotV * (1.0 - k) + k;\n \n return nom / denom;\n}\n\nfn geometry_smith(N: vec3<f32>, V: vec3<f32>, L: vec3<f32>, roughness: f32) -> f32 {\n let NdotV = max(dot(N, V), 0.0);\n let NdotL = max(dot(N, L), 0.0);\n let ggx2 = geometry_schlick_ggx(NdotV, roughness);\n let ggx1 = geometry_schlick_ggx(NdotL, roughness);\n \n return ggx1 * ggx2;\n}\n\n@fragment\nfn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {\n // Base color from material and vertex color\n let albedo = material.base_color.rgb * in.color;\n \n // Normal (already in world space from vertex shader)\n let N = normalize(in.normal);\n \n // View direction\n let V = normalize(camera.camera_position.xyz - in.world_position);\n \n // Light direction (negated because we typically define light direction as \"to light\")\n let L = normalize(-light.direction);\n \n // Half vector\n let H = normalize(V + L);\n \n // Calculate F0 (surface reflection at zero incidence)\n // For dielectrics, F0 is around 0.04, for metals it\'s the albedo color\n var F0 = vec3<f32>(0.04);\n F0 = mix(F0, albedo, material.metallic);\n \n // Cook-Torrance BRDF\n let NDF = distribution_ggx(N, H, material.roughness);\n let G = geometry_smith(N, V, L, material.roughness);\n let F = fresnel_schlick(max(dot(H, V), 0.0), F0);\n \n let NdotL = max(dot(N, L), 0.0);\n \n // Specular component\n let numerator = NDF * G * F;\n let denominator = 4.0 * max(dot(N, V), 0.0) * NdotL + 0.001; // Add epsilon to prevent division by zero\n let specular = numerator / denominator;\n \n // Energy conservation - diffuse component\n let kS = F; // Specular reflection\n var kD = vec3<f32>(1.0) - kS; // Diffuse reflection\n kD *= 1.0 - material.metallic; // Metals have no diffuse reflection\n \n // Lambert diffuse\n let diffuse = kD * albedo / PI;\n \n // Combine diffuse and specular\n let radiance = light.color * light.intensity;\n let Lo = (diffuse + specular) * radiance * NdotL;\n \n // Simple ambient (will be replaced with proper ambient lighting later)\n let ambient = vec3<f32>(0.03) * albedo;\n \n // Final color\n var color = ambient + Lo + material.emissive;\n \n // Simple tone mapping (Reinhard)\n color = color / (color + vec3<f32>(1.0));\n \n // Gamma correction\n color = pow(color, vec3<f32>(1.0 / 2.2));\n \n return vec4<f32>(color, material.base_color.a);\n}\n";Expand description
Standard PBR (Physically-Based Rendering) shader.
Implements the metallic-roughness workflow with Cook-Torrance BRDF:
- GGX/Trowbridge-Reitz normal distribution
- Schlick-GGX geometry function
- Fresnel-Schlick approximation