// luma.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors // Attribution: // MIT license, Copyright (c) 2016-2017 Mohamad Moneimne and Contributors // This fragment shader defines a reference implementation for Physically Based Shading of // a microfacet surface material defined by a glTF model. // TODO - better do the checks outside of shader export const pbrMaterialUniforms = /* wgsl */ `\ uniform Projection { // Projection vec3 u_Camera; }; uniform pbrMaterialUniforms { // Material is unlit bool unlit; // Base color map bool baseColorMapEnabled; vec4 baseColorFactor; bool normalMapEnabled; float normalScale; // #ifdef HAS_NORMALMAP bool emissiveMapEnabled; vec3 emissiveFactor; // #ifdef HAS_EMISSIVEMAP vec2 metallicRoughnessValues; bool metallicRoughnessMapEnabled; bool occlusionMapEnabled; float occlusionStrength; // #ifdef HAS_OCCLUSIONMAP bool alphaCutoffEnabled; float alphaCutoff; // #ifdef ALPHA_CUTOFF vec3 specularColorFactor; float specularIntensityFactor; bool specularColorMapEnabled; bool specularIntensityMapEnabled; float ior; float transmissionFactor; bool transmissionMapEnabled; float thicknessFactor; float attenuationDistance; vec3 attenuationColor; float clearcoatFactor; float clearcoatRoughnessFactor; bool clearcoatMapEnabled; bool clearcoatRoughnessMapEnabled; vec3 sheenColorFactor; float sheenRoughnessFactor; bool sheenColorMapEnabled; bool sheenRoughnessMapEnabled; float iridescenceFactor; float iridescenceIor; vec2 iridescenceThicknessRange; bool iridescenceMapEnabled; float anisotropyStrength; float anisotropyRotation; vec2 anisotropyDirection; bool anisotropyMapEnabled; float emissiveStrength; // IBL bool IBLenabled; vec2 scaleIBLAmbient; // #ifdef USE_IBL // debugging flags used for shader output of intermediate PBR variables // #ifdef PBR_DEBUG vec4 scaleDiffBaseMR; vec4 scaleFGDSpec; // #endif }; // Samplers #ifdef HAS_BASECOLORMAP uniform sampler2D u_BaseColorSampler; #endif #ifdef HAS_NORMALMAP uniform sampler2D u_NormalSampler; #endif #ifdef HAS_EMISSIVEMAP uniform sampler2D u_EmissiveSampler; #endif #ifdef HAS_METALROUGHNESSMAP uniform sampler2D u_MetallicRoughnessSampler; #endif #ifdef HAS_OCCLUSIONMAP uniform sampler2D u_OcclusionSampler; #endif #ifdef HAS_SPECULARCOLORMAP uniform sampler2D u_SpecularColorSampler; #endif #ifdef HAS_SPECULARINTENSITYMAP uniform sampler2D u_SpecularIntensitySampler; #endif #ifdef HAS_TRANSMISSIONMAP uniform sampler2D u_TransmissionSampler; #endif #ifdef HAS_THICKNESSMAP uniform sampler2D u_ThicknessSampler; #endif #ifdef HAS_CLEARCOATMAP uniform sampler2D u_ClearcoatSampler; #endif #ifdef HAS_CLEARCOATROUGHNESSMAP uniform sampler2D u_ClearcoatRoughnessSampler; #endif #ifdef HAS_CLEARCOATNORMALMAP uniform sampler2D u_ClearcoatNormalSampler; #endif #ifdef HAS_SHEENCOLORMAP uniform sampler2D u_SheenColorSampler; #endif #ifdef HAS_SHEENROUGHNESSMAP uniform sampler2D u_SheenRoughnessSampler; #endif #ifdef HAS_IRIDESCENCEMAP uniform sampler2D u_IridescenceSampler; #endif #ifdef HAS_IRIDESCENCETHICKNESSMAP uniform sampler2D u_IridescenceThicknessSampler; #endif #ifdef HAS_ANISOTROPYMAP uniform sampler2D u_AnisotropySampler; #endif #ifdef USE_IBL uniform samplerCube u_DiffuseEnvSampler; uniform samplerCube u_SpecularEnvSampler; uniform sampler2D u_brdfLUT; #endif `; export const source = /* wgsl */ `\ struct PBRFragmentInputs { pbr_vPosition: vec3f, pbr_vUV0: vec2f, pbr_vUV1: vec2f, pbr_vTBN: mat3x3f, pbr_vNormal: vec3f }; var fragmentInputs: PBRFragmentInputs; fn pbr_setPositionNormalTangentUV( position: vec4f, normal: vec4f, tangent: vec4f, uv0: vec2f, uv1: vec2f ) { var pos: vec4f = pbrProjection.modelMatrix * position; fragmentInputs.pbr_vPosition = pos.xyz / pos.w; fragmentInputs.pbr_vNormal = vec3f(0.0, 0.0, 1.0); fragmentInputs.pbr_vTBN = mat3x3f( vec3f(1.0, 0.0, 0.0), vec3f(0.0, 1.0, 0.0), vec3f(0.0, 0.0, 1.0) ); fragmentInputs.pbr_vUV0 = vec2f(0.0, 0.0); fragmentInputs.pbr_vUV1 = uv1; #ifdef HAS_NORMALS let normalW: vec3f = normalize((pbrProjection.normalMatrix * vec4f(normal.xyz, 0.0)).xyz); fragmentInputs.pbr_vNormal = normalW; #ifdef HAS_TANGENTS let tangentW: vec3f = normalize((pbrProjection.modelMatrix * vec4f(tangent.xyz, 0.0)).xyz); let bitangentW: vec3f = cross(normalW, tangentW) * tangent.w; fragmentInputs.pbr_vTBN = mat3x3f(tangentW, bitangentW, normalW); #endif #endif #ifdef HAS_UV fragmentInputs.pbr_vUV0 = uv0; #endif } struct pbrMaterialUniforms { // Material is unlit unlit: u32, // Base color map baseColorMapEnabled: u32, baseColorFactor: vec4f, normalMapEnabled : u32, normalScale: f32, // #ifdef HAS_NORMALMAP emissiveMapEnabled: u32, emissiveFactor: vec3f, // #ifdef HAS_EMISSIVEMAP metallicRoughnessValues: vec2f, metallicRoughnessMapEnabled: u32, occlusionMapEnabled: i32, occlusionStrength: f32, // #ifdef HAS_OCCLUSIONMAP alphaCutoffEnabled: i32, alphaCutoff: f32, // #ifdef ALPHA_CUTOFF specularColorFactor: vec3f, specularIntensityFactor: f32, specularColorMapEnabled: i32, specularIntensityMapEnabled: i32, ior: f32, transmissionFactor: f32, transmissionMapEnabled: i32, thicknessFactor: f32, attenuationDistance: f32, attenuationColor: vec3f, clearcoatFactor: f32, clearcoatRoughnessFactor: f32, clearcoatMapEnabled: i32, clearcoatRoughnessMapEnabled: i32, sheenColorFactor: vec3f, sheenRoughnessFactor: f32, sheenColorMapEnabled: i32, sheenRoughnessMapEnabled: i32, iridescenceFactor: f32, iridescenceIor: f32, iridescenceThicknessRange: vec2f, iridescenceMapEnabled: i32, anisotropyStrength: f32, anisotropyRotation: f32, anisotropyDirection: vec2f, anisotropyMapEnabled: i32, emissiveStrength: f32, // IBL IBLenabled: i32, scaleIBLAmbient: vec2f, // #ifdef USE_IBL // debugging flags used for shader output of intermediate PBR variables // #ifdef PBR_DEBUG scaleDiffBaseMR: vec4f, scaleFGDSpec: vec4f, // #endif baseColorUVSet: i32, baseColorUVTransform: mat3x3f, metallicRoughnessUVSet: i32, metallicRoughnessUVTransform: mat3x3f, normalUVSet: i32, normalUVTransform: mat3x3f, occlusionUVSet: i32, occlusionUVTransform: mat3x3f, emissiveUVSet: i32, emissiveUVTransform: mat3x3f, specularColorUVSet: i32, specularColorUVTransform: mat3x3f, specularIntensityUVSet: i32, specularIntensityUVTransform: mat3x3f, transmissionUVSet: i32, transmissionUVTransform: mat3x3f, thicknessUVSet: i32, thicknessUVTransform: mat3x3f, clearcoatUVSet: i32, clearcoatUVTransform: mat3x3f, clearcoatRoughnessUVSet: i32, clearcoatRoughnessUVTransform: mat3x3f, clearcoatNormalUVSet: i32, clearcoatNormalUVTransform: mat3x3f, sheenColorUVSet: i32, sheenColorUVTransform: mat3x3f, sheenRoughnessUVSet: i32, sheenRoughnessUVTransform: mat3x3f, iridescenceUVSet: i32, iridescenceUVTransform: mat3x3f, iridescenceThicknessUVSet: i32, iridescenceThicknessUVTransform: mat3x3f, anisotropyUVSet: i32, anisotropyUVTransform: mat3x3f, } @group(3) @binding(auto) var pbrMaterial : pbrMaterialUniforms; // Samplers #ifdef HAS_BASECOLORMAP @group(3) @binding(auto) var pbr_baseColorSampler: texture_2d; @group(3) @binding(auto) var pbr_baseColorSamplerSampler: sampler; #endif #ifdef HAS_NORMALMAP @group(3) @binding(auto) var pbr_normalSampler: texture_2d; @group(3) @binding(auto) var pbr_normalSamplerSampler: sampler; #endif #ifdef HAS_EMISSIVEMAP @group(3) @binding(auto) var pbr_emissiveSampler: texture_2d; @group(3) @binding(auto) var pbr_emissiveSamplerSampler: sampler; #endif #ifdef HAS_METALROUGHNESSMAP @group(3) @binding(auto) var pbr_metallicRoughnessSampler: texture_2d; @group(3) @binding(auto) var pbr_metallicRoughnessSamplerSampler: sampler; #endif #ifdef HAS_OCCLUSIONMAP @group(3) @binding(auto) var pbr_occlusionSampler: texture_2d; @group(3) @binding(auto) var pbr_occlusionSamplerSampler: sampler; #endif #ifdef HAS_SPECULARCOLORMAP @group(3) @binding(auto) var pbr_specularColorSampler: texture_2d; @group(3) @binding(auto) var pbr_specularColorSamplerSampler: sampler; #endif #ifdef HAS_SPECULARINTENSITYMAP @group(3) @binding(auto) var pbr_specularIntensitySampler: texture_2d; @group(3) @binding(auto) var pbr_specularIntensitySamplerSampler: sampler; #endif #ifdef HAS_TRANSMISSIONMAP @group(3) @binding(auto) var pbr_transmissionSampler: texture_2d; @group(3) @binding(auto) var pbr_transmissionSamplerSampler: sampler; #endif #ifdef HAS_THICKNESSMAP @group(3) @binding(auto) var pbr_thicknessSampler: texture_2d; @group(3) @binding(auto) var pbr_thicknessSamplerSampler: sampler; #endif #ifdef HAS_CLEARCOATMAP @group(3) @binding(auto) var pbr_clearcoatSampler: texture_2d; @group(3) @binding(auto) var pbr_clearcoatSamplerSampler: sampler; #endif #ifdef HAS_CLEARCOATROUGHNESSMAP @group(3) @binding(auto) var pbr_clearcoatRoughnessSampler: texture_2d; @group(3) @binding(auto) var pbr_clearcoatRoughnessSamplerSampler: sampler; #endif #ifdef HAS_CLEARCOATNORMALMAP @group(3) @binding(auto) var pbr_clearcoatNormalSampler: texture_2d; @group(3) @binding(auto) var pbr_clearcoatNormalSamplerSampler: sampler; #endif #ifdef HAS_SHEENCOLORMAP @group(3) @binding(auto) var pbr_sheenColorSampler: texture_2d; @group(3) @binding(auto) var pbr_sheenColorSamplerSampler: sampler; #endif #ifdef HAS_SHEENROUGHNESSMAP @group(3) @binding(auto) var pbr_sheenRoughnessSampler: texture_2d; @group(3) @binding(auto) var pbr_sheenRoughnessSamplerSampler: sampler; #endif #ifdef HAS_IRIDESCENCEMAP @group(3) @binding(auto) var pbr_iridescenceSampler: texture_2d; @group(3) @binding(auto) var pbr_iridescenceSamplerSampler: sampler; #endif #ifdef HAS_IRIDESCENCETHICKNESSMAP @group(3) @binding(auto) var pbr_iridescenceThicknessSampler: texture_2d; @group(3) @binding(auto) var pbr_iridescenceThicknessSamplerSampler: sampler; #endif #ifdef HAS_ANISOTROPYMAP @group(3) @binding(auto) var pbr_anisotropySampler: texture_2d; @group(3) @binding(auto) var pbr_anisotropySamplerSampler: sampler; #endif // Encapsulate the various inputs used by the various functions in the shading equation // We store values in this struct to simplify the integration of alternative implementations // of the shading terms, outlined in the Readme.MD Appendix. struct PBRInfo { NdotL: f32, // cos angle between normal and light direction NdotV: f32, // cos angle between normal and view direction NdotH: f32, // cos angle between normal and half vector LdotH: f32, // cos angle between light direction and half vector VdotH: f32, // cos angle between view direction and half vector perceptualRoughness: f32, // roughness value, as authored by the model creator (input to shader) metalness: f32, // metallic value at the surface reflectance0: vec3f, // full reflectance color (normal incidence angle) reflectance90: vec3f, // reflectance color at grazing angle alphaRoughness: f32, // roughness mapped to a more linear change in the roughness (proposed by [2]) diffuseColor: vec3f, // color contribution from diffuse lighting specularColor: vec3f, // color contribution from specular lighting n: vec3f, // normal at surface point v: vec3f, // vector from surface point to camera }; const M_PI = 3.141592653589793; const c_MinRoughness = 0.04; fn SRGBtoLINEAR(srgbIn: vec4f ) -> vec4f { var linOut: vec3f = srgbIn.xyz; #ifdef MANUAL_SRGB let bLess: vec3f = step(vec3f(0.04045), srgbIn.xyz); linOut = mix( srgbIn.xyz / vec3f(12.92), pow((srgbIn.xyz + vec3f(0.055)) / vec3f(1.055), vec3f(2.4)), bLess ); #ifdef SRGB_FAST_APPROXIMATION linOut = pow(srgbIn.xyz, vec3f(2.2)); #endif #endif return vec4f(linOut, srgbIn.w); } fn getMaterialUV(uvSet: i32, uvTransform: mat3x3f) -> vec2f { var baseUV = fragmentInputs.pbr_vUV0; if (uvSet == 1) { baseUV = fragmentInputs.pbr_vUV1; } return (uvTransform * vec3f(baseUV, 1.0)).xy; } // Build the tangent basis from interpolated attributes or screen-space derivatives. fn getTBN(uv: vec2f) -> mat3x3f { let pos_dx: vec3f = dpdx(fragmentInputs.pbr_vPosition); let pos_dy: vec3f = dpdy(fragmentInputs.pbr_vPosition); let tex_dx: vec3f = dpdx(vec3f(uv, 0.0)); let tex_dy: vec3f = dpdy(vec3f(uv, 0.0)); var t: vec3f = (tex_dy.y * pos_dx - tex_dx.y * pos_dy) / (tex_dx.x * tex_dy.y - tex_dy.x * tex_dx.y); var ng: vec3f = cross(pos_dx, pos_dy); #ifdef HAS_NORMALS ng = normalize(fragmentInputs.pbr_vNormal); #endif t = normalize(t - ng * dot(ng, t)); var b: vec3f = normalize(cross(ng, t)); var tbn: mat3x3f = mat3x3f(t, b, ng); #ifdef HAS_TANGENTS tbn = fragmentInputs.pbr_vTBN; #endif return tbn; } // Find the normal for this fragment, pulling either from a predefined normal map // or from the interpolated mesh normal and tangent attributes. fn getMappedNormal( normalSampler: texture_2d, normalSamplerBinding: sampler, tbn: mat3x3f, normalScale: f32, uv: vec2f ) -> vec3f { let n = textureSample(normalSampler, normalSamplerBinding, uv).rgb; return normalize(tbn * ((2.0 * n - 1.0) * vec3f(normalScale, normalScale, 1.0))); } fn getNormal(tbn: mat3x3f, uv: vec2f) -> vec3f { // The tbn matrix is linearly interpolated, so we need to re-normalize var n: vec3f = normalize(tbn[2].xyz); #ifdef HAS_NORMALMAP n = getMappedNormal( pbr_normalSampler, pbr_normalSamplerSampler, tbn, pbrMaterial.normalScale, uv ); #endif return n; } fn getClearcoatNormal(tbn: mat3x3f, baseNormal: vec3f, uv: vec2f) -> vec3f { #ifdef HAS_CLEARCOATNORMALMAP return getMappedNormal( pbr_clearcoatNormalSampler, pbr_clearcoatNormalSamplerSampler, tbn, 1.0, uv ); #else return baseNormal; #endif } // Calculation of the lighting contribution from an optional Image Based Light source. // Precomputed Environment Maps are required uniform inputs and are computed as outlined in [1]. // See our README.md on Environment Maps [3] for additional discussion. #ifdef USE_IBL fn getIBLContribution(pbrInfo: PBRInfo, n: vec3f, reflection: vec3f) -> vec3f { let mipCount: f32 = 9.0; // resolution of 512x512 let lod: f32 = pbrInfo.perceptualRoughness * mipCount; // retrieve a scale and bias to F0. See [1], Figure 3 let brdf = SRGBtoLINEAR( textureSampleLevel( pbr_brdfLUT, pbr_brdfLUTSampler, vec2f(pbrInfo.NdotV, 1.0 - pbrInfo.perceptualRoughness), 0.0 ) ).rgb; let diffuseLight = SRGBtoLINEAR( textureSampleLevel(pbr_diffuseEnvSampler, pbr_diffuseEnvSamplerSampler, n, 0.0) ).rgb; var specularLight = SRGBtoLINEAR( textureSampleLevel( pbr_specularEnvSampler, pbr_specularEnvSamplerSampler, reflection, 0.0 ) ).rgb; #ifdef USE_TEX_LOD specularLight = SRGBtoLINEAR( textureSampleLevel( pbr_specularEnvSampler, pbr_specularEnvSamplerSampler, reflection, lod ) ).rgb; #endif let diffuse = diffuseLight * pbrInfo.diffuseColor * pbrMaterial.scaleIBLAmbient.x; let specular = specularLight * (pbrInfo.specularColor * brdf.x + brdf.y) * pbrMaterial.scaleIBLAmbient.y; return diffuse + specular; } #endif // Basic Lambertian diffuse // Implementation from Lambert's Photometria https://archive.org/details/lambertsphotome00lambgoog // See also [1], Equation 1 fn diffuse(pbrInfo: PBRInfo) -> vec3 { return pbrInfo.diffuseColor / M_PI; } // The following equation models the Fresnel reflectance term of the spec equation (aka F()) // Implementation of fresnel from [4], Equation 15 fn specularReflection(pbrInfo: PBRInfo) -> vec3 { return pbrInfo.reflectance0 + (pbrInfo.reflectance90 - pbrInfo.reflectance0) * pow(clamp(1.0 - pbrInfo.VdotH, 0.0, 1.0), 5.0); } // This calculates the specular geometric attenuation (aka G()), // where rougher material will reflect less light back to the viewer. // This implementation is based on [1] Equation 4, and we adopt their modifications to // alphaRoughness as input as originally proposed in [2]. fn geometricOcclusion(pbrInfo: PBRInfo) -> f32 { let NdotL: f32 = pbrInfo.NdotL; let NdotV: f32 = pbrInfo.NdotV; let r: f32 = pbrInfo.alphaRoughness; let attenuationL = 2.0 * NdotL / (NdotL + sqrt(r * r + (1.0 - r * r) * (NdotL * NdotL))); let attenuationV = 2.0 * NdotV / (NdotV + sqrt(r * r + (1.0 - r * r) * (NdotV * NdotV))); return attenuationL * attenuationV; } // The following equation(s) model the distribution of microfacet normals across // the area being drawn (aka D()) // Implementation from "Average Irregularity Representation of a Roughened Surface // for Ray Reflection" by T. S. Trowbridge, and K. P. Reitz // Follows the distribution function recommended in the SIGGRAPH 2013 course notes // from EPIC Games [1], Equation 3. fn microfacetDistribution(pbrInfo: PBRInfo) -> f32 { let roughnessSq = pbrInfo.alphaRoughness * pbrInfo.alphaRoughness; let f = (pbrInfo.NdotH * roughnessSq - pbrInfo.NdotH) * pbrInfo.NdotH + 1.0; return roughnessSq / (M_PI * f * f); } fn maxComponent(value: vec3f) -> f32 { return max(max(value.r, value.g), value.b); } fn getDielectricF0(ior: f32) -> f32 { let clampedIor = max(ior, 1.0); let ratio = (clampedIor - 1.0) / (clampedIor + 1.0); return ratio * ratio; } fn normalizeDirection(direction: vec2f) -> vec2f { let directionLength = length(direction); if (directionLength > 0.0001) { return direction / directionLength; } return vec2f(1.0, 0.0); } fn rotateDirection(direction: vec2f, rotation: f32) -> vec2f { let s = sin(rotation); let c = cos(rotation); return vec2f(direction.x * c - direction.y * s, direction.x * s + direction.y * c); } fn getIridescenceTint(iridescence: f32, thickness: f32, NdotV: f32) -> vec3f { if (iridescence <= 0.0) { return vec3f(1.0); } let phase = 0.015 * thickness * pbrMaterial.iridescenceIor + (1.0 - NdotV) * 6.0; let thinFilmTint = 0.5 + 0.5 * cos(vec3f(phase, phase + 2.0943951, phase + 4.1887902)); return mix(vec3f(1.0), thinFilmTint, iridescence); } fn getVolumeAttenuation(thickness: f32) -> vec3f { if (thickness <= 0.0) { return vec3f(1.0); } let attenuationCoefficient = -log(max(pbrMaterial.attenuationColor, vec3f(0.0001))) / max(pbrMaterial.attenuationDistance, 0.0001); return exp(-attenuationCoefficient * thickness); } fn createClearcoatPBRInfo( basePBRInfo: PBRInfo, clearcoatNormal: vec3f, clearcoatRoughness: f32 ) -> PBRInfo { let perceptualRoughness = clamp(clearcoatRoughness, c_MinRoughness, 1.0); let alphaRoughness = perceptualRoughness * perceptualRoughness; let NdotV = clamp(abs(dot(clearcoatNormal, basePBRInfo.v)), 0.001, 1.0); return PBRInfo( basePBRInfo.NdotL, NdotV, basePBRInfo.NdotH, basePBRInfo.LdotH, basePBRInfo.VdotH, perceptualRoughness, 0.0, vec3f(0.04), vec3f(1.0), alphaRoughness, vec3f(0.0), vec3f(0.04), clearcoatNormal, basePBRInfo.v ); } fn calculateClearcoatContribution( pbrInfo: PBRInfo, lightColor: vec3f, clearcoatNormal: vec3f, clearcoatFactor: f32, clearcoatRoughness: f32 ) -> vec3f { if (clearcoatFactor <= 0.0) { return vec3f(0.0); } let clearcoatPBRInfo = createClearcoatPBRInfo(pbrInfo, clearcoatNormal, clearcoatRoughness); return calculateFinalColor(clearcoatPBRInfo, lightColor) * clearcoatFactor; } #ifdef USE_IBL fn calculateClearcoatIBLContribution( pbrInfo: PBRInfo, clearcoatNormal: vec3f, reflection: vec3f, clearcoatFactor: f32, clearcoatRoughness: f32 ) -> vec3f { if (clearcoatFactor <= 0.0) { return vec3f(0.0); } let clearcoatPBRInfo = createClearcoatPBRInfo(pbrInfo, clearcoatNormal, clearcoatRoughness); return getIBLContribution(clearcoatPBRInfo, clearcoatNormal, reflection) * clearcoatFactor; } #endif fn calculateSheenContribution( pbrInfo: PBRInfo, lightColor: vec3f, sheenColor: vec3f, sheenRoughness: f32 ) -> vec3f { if (maxComponent(sheenColor) <= 0.0) { return vec3f(0.0); } let sheenFresnel = pow(clamp(1.0 - pbrInfo.VdotH, 0.0, 1.0), 5.0); let sheenVisibility = mix(1.0, pbrInfo.NdotL * pbrInfo.NdotV, sheenRoughness); return pbrInfo.NdotL * lightColor * sheenColor * (0.25 + 0.75 * sheenFresnel) * sheenVisibility * (1.0 - pbrInfo.metalness); } fn calculateAnisotropyBoost( pbrInfo: PBRInfo, anisotropyTangent: vec3f, anisotropyStrength: f32 ) -> f32 { if (anisotropyStrength <= 0.0) { return 1.0; } let anisotropyBitangent = normalize(cross(pbrInfo.n, anisotropyTangent)); let bitangentViewAlignment = abs(dot(pbrInfo.v, anisotropyBitangent)); return mix(1.0, 0.65 + 0.7 * bitangentViewAlignment, anisotropyStrength); } fn calculateMaterialLightColor( pbrInfo: PBRInfo, lightColor: vec3f, clearcoatNormal: vec3f, clearcoatFactor: f32, clearcoatRoughness: f32, sheenColor: vec3f, sheenRoughness: f32, anisotropyTangent: vec3f, anisotropyStrength: f32 ) -> vec3f { let anisotropyBoost = calculateAnisotropyBoost(pbrInfo, anisotropyTangent, anisotropyStrength); var color = calculateFinalColor(pbrInfo, lightColor) * anisotropyBoost; color += calculateClearcoatContribution( pbrInfo, lightColor, clearcoatNormal, clearcoatFactor, clearcoatRoughness ); color += calculateSheenContribution(pbrInfo, lightColor, sheenColor, sheenRoughness); return color; } fn PBRInfo_setAmbientLight(pbrInfo: ptr) { (*pbrInfo).NdotL = 1.0; (*pbrInfo).NdotH = 0.0; (*pbrInfo).LdotH = 0.0; (*pbrInfo).VdotH = 1.0; } fn PBRInfo_setDirectionalLight(pbrInfo: ptr, lightDirection: vec3) { let n = (*pbrInfo).n; let v = (*pbrInfo).v; let l = normalize(lightDirection); // Vector from surface point to light let h = normalize(l + v); // Half vector between both l and v (*pbrInfo).NdotL = clamp(dot(n, l), 0.001, 1.0); (*pbrInfo).NdotH = clamp(dot(n, h), 0.0, 1.0); (*pbrInfo).LdotH = clamp(dot(l, h), 0.0, 1.0); (*pbrInfo).VdotH = clamp(dot(v, h), 0.0, 1.0); } fn PBRInfo_setPointLight(pbrInfo: ptr, pointLight: PointLight) { let light_direction = normalize(pointLight.position - fragmentInputs.pbr_vPosition); PBRInfo_setDirectionalLight(pbrInfo, light_direction); } fn PBRInfo_setSpotLight(pbrInfo: ptr, spotLight: SpotLight) { let light_direction = normalize(spotLight.position - fragmentInputs.pbr_vPosition); PBRInfo_setDirectionalLight(pbrInfo, light_direction); } fn calculateFinalColor(pbrInfo: PBRInfo, lightColor: vec3) -> vec3 { // Calculate the shading terms for the microfacet specular shading model let F = specularReflection(pbrInfo); let G = geometricOcclusion(pbrInfo); let D = microfacetDistribution(pbrInfo); // Calculation of analytical lighting contribution let diffuseContrib = (1.0 - F) * diffuse(pbrInfo); let specContrib = F * G * D / (4.0 * pbrInfo.NdotL * pbrInfo.NdotV); // Obtain final intensity as reflectance (BRDF) scaled by the energy of the light (cosine law) return pbrInfo.NdotL * lightColor * (diffuseContrib + specContrib); } fn pbr_filterColor(colorUnused: vec4) -> vec4 { let baseColorUV = getMaterialUV(pbrMaterial.baseColorUVSet, pbrMaterial.baseColorUVTransform); let metallicRoughnessUV = getMaterialUV( pbrMaterial.metallicRoughnessUVSet, pbrMaterial.metallicRoughnessUVTransform ); let normalUV = getMaterialUV(pbrMaterial.normalUVSet, pbrMaterial.normalUVTransform); let occlusionUV = getMaterialUV(pbrMaterial.occlusionUVSet, pbrMaterial.occlusionUVTransform); let emissiveUV = getMaterialUV(pbrMaterial.emissiveUVSet, pbrMaterial.emissiveUVTransform); let specularColorUV = getMaterialUV( pbrMaterial.specularColorUVSet, pbrMaterial.specularColorUVTransform ); let specularIntensityUV = getMaterialUV( pbrMaterial.specularIntensityUVSet, pbrMaterial.specularIntensityUVTransform ); let transmissionUV = getMaterialUV( pbrMaterial.transmissionUVSet, pbrMaterial.transmissionUVTransform ); let thicknessUV = getMaterialUV(pbrMaterial.thicknessUVSet, pbrMaterial.thicknessUVTransform); let clearcoatUV = getMaterialUV(pbrMaterial.clearcoatUVSet, pbrMaterial.clearcoatUVTransform); let clearcoatRoughnessUV = getMaterialUV( pbrMaterial.clearcoatRoughnessUVSet, pbrMaterial.clearcoatRoughnessUVTransform ); let clearcoatNormalUV = getMaterialUV( pbrMaterial.clearcoatNormalUVSet, pbrMaterial.clearcoatNormalUVTransform ); let sheenColorUV = getMaterialUV( pbrMaterial.sheenColorUVSet, pbrMaterial.sheenColorUVTransform ); let sheenRoughnessUV = getMaterialUV( pbrMaterial.sheenRoughnessUVSet, pbrMaterial.sheenRoughnessUVTransform ); let iridescenceUV = getMaterialUV( pbrMaterial.iridescenceUVSet, pbrMaterial.iridescenceUVTransform ); let iridescenceThicknessUV = getMaterialUV( pbrMaterial.iridescenceThicknessUVSet, pbrMaterial.iridescenceThicknessUVTransform ); let anisotropyUV = getMaterialUV( pbrMaterial.anisotropyUVSet, pbrMaterial.anisotropyUVTransform ); // The albedo may be defined from a base texture or a flat color var baseColor: vec4 = pbrMaterial.baseColorFactor; #ifdef HAS_BASECOLORMAP baseColor = SRGBtoLINEAR( textureSample(pbr_baseColorSampler, pbr_baseColorSamplerSampler, baseColorUV) ) * pbrMaterial.baseColorFactor; #endif #ifdef ALPHA_CUTOFF if (baseColor.a < pbrMaterial.alphaCutoff) { discard; } #endif var color = vec3(0.0, 0.0, 0.0); var transmission = 0.0; if (pbrMaterial.unlit != 0u) { color = baseColor.rgb; } else { // Metallic and Roughness material properties are packed together // In glTF, these factors can be specified by fixed scalar values // or from a metallic-roughness map var perceptualRoughness = pbrMaterial.metallicRoughnessValues.y; var metallic = pbrMaterial.metallicRoughnessValues.x; #ifdef HAS_METALROUGHNESSMAP // Roughness is stored in the 'g' channel, metallic is stored in the 'b' channel. // This layout intentionally reserves the 'r' channel for (optional) occlusion map data let mrSample = textureSample( pbr_metallicRoughnessSampler, pbr_metallicRoughnessSamplerSampler, metallicRoughnessUV ); perceptualRoughness = mrSample.g * perceptualRoughness; metallic = mrSample.b * metallic; #endif perceptualRoughness = clamp(perceptualRoughness, c_MinRoughness, 1.0); metallic = clamp(metallic, 0.0, 1.0); let tbn = getTBN(normalUV); let n = getNormal(tbn, normalUV); // normal at surface point let v = normalize(pbrProjection.camera - fragmentInputs.pbr_vPosition); // Vector from surface point to camera let NdotV = clamp(abs(dot(n, v)), 0.001, 1.0); var useExtendedPBR = false; #ifdef USE_MATERIAL_EXTENSIONS useExtendedPBR = pbrMaterial.specularColorMapEnabled != 0 || pbrMaterial.specularIntensityMapEnabled != 0 || abs(pbrMaterial.specularIntensityFactor - 1.0) > 0.0001 || maxComponent(abs(pbrMaterial.specularColorFactor - vec3f(1.0))) > 0.0001 || abs(pbrMaterial.ior - 1.5) > 0.0001 || pbrMaterial.transmissionMapEnabled != 0 || pbrMaterial.transmissionFactor > 0.0001 || pbrMaterial.clearcoatMapEnabled != 0 || pbrMaterial.clearcoatRoughnessMapEnabled != 0 || pbrMaterial.clearcoatFactor > 0.0001 || pbrMaterial.clearcoatRoughnessFactor > 0.0001 || pbrMaterial.sheenColorMapEnabled != 0 || pbrMaterial.sheenRoughnessMapEnabled != 0 || maxComponent(pbrMaterial.sheenColorFactor) > 0.0001 || pbrMaterial.sheenRoughnessFactor > 0.0001 || pbrMaterial.iridescenceMapEnabled != 0 || pbrMaterial.iridescenceFactor > 0.0001 || abs(pbrMaterial.iridescenceIor - 1.3) > 0.0001 || abs(pbrMaterial.iridescenceThicknessRange.x - 100.0) > 0.0001 || abs(pbrMaterial.iridescenceThicknessRange.y - 400.0) > 0.0001 || pbrMaterial.anisotropyMapEnabled != 0 || pbrMaterial.anisotropyStrength > 0.0001 || abs(pbrMaterial.anisotropyRotation) > 0.0001 || length(pbrMaterial.anisotropyDirection - vec2f(1.0, 0.0)) > 0.0001; #endif if (!useExtendedPBR) { let alphaRoughness = perceptualRoughness * perceptualRoughness; let f0 = vec3(0.04); var diffuseColor = baseColor.rgb * (vec3(1.0) - f0); diffuseColor *= 1.0 - metallic; let specularColor = mix(f0, baseColor.rgb, metallic); let reflectance = max(max(specularColor.r, specularColor.g), specularColor.b); let reflectance90 = clamp(reflectance * 25.0, 0.0, 1.0); let specularEnvironmentR0 = specularColor; let specularEnvironmentR90 = vec3(1.0, 1.0, 1.0) * reflectance90; let reflection = -normalize(reflect(v, n)); var pbrInfo = PBRInfo( 0.0, // NdotL NdotV, 0.0, // NdotH 0.0, // LdotH 0.0, // VdotH perceptualRoughness, metallic, specularEnvironmentR0, specularEnvironmentR90, alphaRoughness, diffuseColor, specularColor, n, v ); #ifdef USE_LIGHTS PBRInfo_setAmbientLight(&pbrInfo); color += calculateFinalColor(pbrInfo, lighting.ambientColor); for (var i = 0; i < lighting.directionalLightCount; i++) { if (i < lighting.directionalLightCount) { PBRInfo_setDirectionalLight(&pbrInfo, lighting_getDirectionalLight(i).direction); color += calculateFinalColor(pbrInfo, lighting_getDirectionalLight(i).color); } } for (var i = 0; i < lighting.pointLightCount; i++) { if (i < lighting.pointLightCount) { PBRInfo_setPointLight(&pbrInfo, lighting_getPointLight(i)); let attenuation = getPointLightAttenuation( lighting_getPointLight(i), distance(lighting_getPointLight(i).position, fragmentInputs.pbr_vPosition) ); color += calculateFinalColor(pbrInfo, lighting_getPointLight(i).color / attenuation); } } for (var i = 0; i < lighting.spotLightCount; i++) { if (i < lighting.spotLightCount) { PBRInfo_setSpotLight(&pbrInfo, lighting_getSpotLight(i)); let attenuation = getSpotLightAttenuation( lighting_getSpotLight(i), fragmentInputs.pbr_vPosition ); color += calculateFinalColor(pbrInfo, lighting_getSpotLight(i).color / attenuation); } } #endif #ifdef USE_IBL if (pbrMaterial.IBLenabled != 0) { color += getIBLContribution(pbrInfo, n, reflection); } #endif #ifdef HAS_OCCLUSIONMAP if (pbrMaterial.occlusionMapEnabled != 0) { let ao = textureSample(pbr_occlusionSampler, pbr_occlusionSamplerSampler, occlusionUV).r; color = mix(color, color * ao, pbrMaterial.occlusionStrength); } #endif var emissive = pbrMaterial.emissiveFactor; #ifdef HAS_EMISSIVEMAP if (pbrMaterial.emissiveMapEnabled != 0u) { emissive *= SRGBtoLINEAR( textureSample(pbr_emissiveSampler, pbr_emissiveSamplerSampler, emissiveUV) ).rgb; } #endif color += emissive * pbrMaterial.emissiveStrength; #ifdef PBR_DEBUG color = mix(color, baseColor.rgb, pbrMaterial.scaleDiffBaseMR.y); color = mix(color, vec3(metallic), pbrMaterial.scaleDiffBaseMR.z); color = mix(color, vec3(perceptualRoughness), pbrMaterial.scaleDiffBaseMR.w); #endif return vec4(pow(color, vec3(1.0 / 2.2)), baseColor.a); } var specularIntensity = pbrMaterial.specularIntensityFactor; #ifdef HAS_SPECULARINTENSITYMAP if (pbrMaterial.specularIntensityMapEnabled != 0) { specularIntensity *= textureSample( pbr_specularIntensitySampler, pbr_specularIntensitySamplerSampler, specularIntensityUV ).a; } #endif var specularFactor = pbrMaterial.specularColorFactor; #ifdef HAS_SPECULARCOLORMAP if (pbrMaterial.specularColorMapEnabled != 0) { specularFactor *= SRGBtoLINEAR( textureSample( pbr_specularColorSampler, pbr_specularColorSamplerSampler, specularColorUV ) ).rgb; } #endif transmission = pbrMaterial.transmissionFactor; #ifdef HAS_TRANSMISSIONMAP if (pbrMaterial.transmissionMapEnabled != 0) { transmission *= textureSample( pbr_transmissionSampler, pbr_transmissionSamplerSampler, transmissionUV ).r; } #endif transmission = clamp(transmission * (1.0 - metallic), 0.0, 1.0); var thickness = max(pbrMaterial.thicknessFactor, 0.0); #ifdef HAS_THICKNESSMAP thickness *= textureSample( pbr_thicknessSampler, pbr_thicknessSamplerSampler, thicknessUV ).g; #endif var clearcoatFactor = pbrMaterial.clearcoatFactor; var clearcoatRoughness = pbrMaterial.clearcoatRoughnessFactor; #ifdef HAS_CLEARCOATMAP if (pbrMaterial.clearcoatMapEnabled != 0) { clearcoatFactor *= textureSample( pbr_clearcoatSampler, pbr_clearcoatSamplerSampler, clearcoatUV ).r; } #endif #ifdef HAS_CLEARCOATROUGHNESSMAP if (pbrMaterial.clearcoatRoughnessMapEnabled != 0) { clearcoatRoughness *= textureSample( pbr_clearcoatRoughnessSampler, pbr_clearcoatRoughnessSamplerSampler, clearcoatRoughnessUV ).g; } #endif clearcoatFactor = clamp(clearcoatFactor, 0.0, 1.0); clearcoatRoughness = clamp(clearcoatRoughness, c_MinRoughness, 1.0); let clearcoatNormal = getClearcoatNormal(getTBN(clearcoatNormalUV), n, clearcoatNormalUV); var sheenColor = pbrMaterial.sheenColorFactor; var sheenRoughness = pbrMaterial.sheenRoughnessFactor; #ifdef HAS_SHEENCOLORMAP if (pbrMaterial.sheenColorMapEnabled != 0) { sheenColor *= SRGBtoLINEAR( textureSample( pbr_sheenColorSampler, pbr_sheenColorSamplerSampler, sheenColorUV ) ).rgb; } #endif #ifdef HAS_SHEENROUGHNESSMAP if (pbrMaterial.sheenRoughnessMapEnabled != 0) { sheenRoughness *= textureSample( pbr_sheenRoughnessSampler, pbr_sheenRoughnessSamplerSampler, sheenRoughnessUV ).a; } #endif sheenRoughness = clamp(sheenRoughness, c_MinRoughness, 1.0); var iridescence = pbrMaterial.iridescenceFactor; #ifdef HAS_IRIDESCENCEMAP if (pbrMaterial.iridescenceMapEnabled != 0) { iridescence *= textureSample( pbr_iridescenceSampler, pbr_iridescenceSamplerSampler, iridescenceUV ).r; } #endif iridescence = clamp(iridescence, 0.0, 1.0); var iridescenceThickness = mix( pbrMaterial.iridescenceThicknessRange.x, pbrMaterial.iridescenceThicknessRange.y, 0.5 ); #ifdef HAS_IRIDESCENCETHICKNESSMAP iridescenceThickness = mix( pbrMaterial.iridescenceThicknessRange.x, pbrMaterial.iridescenceThicknessRange.y, textureSample( pbr_iridescenceThicknessSampler, pbr_iridescenceThicknessSamplerSampler, iridescenceThicknessUV ).g ); #endif var anisotropyStrength = clamp(pbrMaterial.anisotropyStrength, 0.0, 1.0); var anisotropyDirection = normalizeDirection(pbrMaterial.anisotropyDirection); #ifdef HAS_ANISOTROPYMAP if (pbrMaterial.anisotropyMapEnabled != 0) { let anisotropySample = textureSample( pbr_anisotropySampler, pbr_anisotropySamplerSampler, anisotropyUV ).rgb; anisotropyStrength *= anisotropySample.b; let mappedDirection = anisotropySample.rg * 2.0 - 1.0; if (length(mappedDirection) > 0.0001) { anisotropyDirection = normalize(mappedDirection); } } #endif anisotropyDirection = rotateDirection(anisotropyDirection, pbrMaterial.anisotropyRotation); var anisotropyTangent = normalize(tbn[0] * anisotropyDirection.x + tbn[1] * anisotropyDirection.y); if (length(anisotropyTangent) < 0.0001) { anisotropyTangent = normalize(tbn[0]); } let anisotropyViewAlignment = abs(dot(v, anisotropyTangent)); perceptualRoughness = mix( perceptualRoughness, clamp(perceptualRoughness * (1.0 - 0.6 * anisotropyViewAlignment), c_MinRoughness, 1.0), anisotropyStrength ); // Roughness is authored as perceptual roughness; as is convention, // convert to material roughness by squaring the perceptual roughness [2]. let alphaRoughness = perceptualRoughness * perceptualRoughness; let dielectricF0 = getDielectricF0(pbrMaterial.ior); var dielectricSpecularF0 = min( vec3f(dielectricF0) * specularFactor * specularIntensity, vec3f(1.0) ); let iridescenceTint = getIridescenceTint(iridescence, iridescenceThickness, NdotV); dielectricSpecularF0 = mix( dielectricSpecularF0, dielectricSpecularF0 * iridescenceTint, iridescence ); var diffuseColor = baseColor.rgb * (vec3f(1.0) - dielectricSpecularF0); diffuseColor *= (1.0 - metallic) * (1.0 - transmission); var specularColor = mix(dielectricSpecularF0, baseColor.rgb, metallic); let baseLayerEnergy = 1.0 - clearcoatFactor * 0.25; diffuseColor *= baseLayerEnergy; specularColor *= baseLayerEnergy; // Compute reflectance. let reflectance = max(max(specularColor.r, specularColor.g), specularColor.b); // For typical incident reflectance range (between 4% to 100%) set the grazing // reflectance to 100% for typical fresnel effect. // For very low reflectance range on highly diffuse objects (below 4%), // incrementally reduce grazing reflectance to 0%. let reflectance90 = clamp(reflectance * 25.0, 0.0, 1.0); let specularEnvironmentR0 = specularColor; let specularEnvironmentR90 = vec3(1.0, 1.0, 1.0) * reflectance90; let reflection = -normalize(reflect(v, n)); var pbrInfo = PBRInfo( 0.0, // NdotL NdotV, 0.0, // NdotH 0.0, // LdotH 0.0, // VdotH perceptualRoughness, metallic, specularEnvironmentR0, specularEnvironmentR90, alphaRoughness, diffuseColor, specularColor, n, v ); #ifdef USE_LIGHTS // Apply ambient light PBRInfo_setAmbientLight(&pbrInfo); color += calculateMaterialLightColor( pbrInfo, lighting.ambientColor, clearcoatNormal, clearcoatFactor, clearcoatRoughness, sheenColor, sheenRoughness, anisotropyTangent, anisotropyStrength ); // Apply directional light for (var i = 0; i < lighting.directionalLightCount; i++) { if (i < lighting.directionalLightCount) { PBRInfo_setDirectionalLight(&pbrInfo, lighting_getDirectionalLight(i).direction); color += calculateMaterialLightColor( pbrInfo, lighting_getDirectionalLight(i).color, clearcoatNormal, clearcoatFactor, clearcoatRoughness, sheenColor, sheenRoughness, anisotropyTangent, anisotropyStrength ); } } // Apply point light for (var i = 0; i < lighting.pointLightCount; i++) { if (i < lighting.pointLightCount) { PBRInfo_setPointLight(&pbrInfo, lighting_getPointLight(i)); let attenuation = getPointLightAttenuation( lighting_getPointLight(i), distance(lighting_getPointLight(i).position, fragmentInputs.pbr_vPosition) ); color += calculateMaterialLightColor( pbrInfo, lighting_getPointLight(i).color / attenuation, clearcoatNormal, clearcoatFactor, clearcoatRoughness, sheenColor, sheenRoughness, anisotropyTangent, anisotropyStrength ); } } for (var i = 0; i < lighting.spotLightCount; i++) { if (i < lighting.spotLightCount) { PBRInfo_setSpotLight(&pbrInfo, lighting_getSpotLight(i)); let attenuation = getSpotLightAttenuation(lighting_getSpotLight(i), fragmentInputs.pbr_vPosition); color += calculateMaterialLightColor( pbrInfo, lighting_getSpotLight(i).color / attenuation, clearcoatNormal, clearcoatFactor, clearcoatRoughness, sheenColor, sheenRoughness, anisotropyTangent, anisotropyStrength ); } } #endif // Calculate lighting contribution from image based lighting source (IBL) #ifdef USE_IBL if (pbrMaterial.IBLenabled != 0) { color += getIBLContribution(pbrInfo, n, reflection) * calculateAnisotropyBoost(pbrInfo, anisotropyTangent, anisotropyStrength); color += calculateClearcoatIBLContribution( pbrInfo, clearcoatNormal, -normalize(reflect(v, clearcoatNormal)), clearcoatFactor, clearcoatRoughness ); color += sheenColor * pbrMaterial.scaleIBLAmbient.x * (1.0 - sheenRoughness) * 0.25; } #endif // Apply optional PBR terms for additional (optional) shading #ifdef HAS_OCCLUSIONMAP if (pbrMaterial.occlusionMapEnabled != 0) { let ao = textureSample(pbr_occlusionSampler, pbr_occlusionSamplerSampler, occlusionUV).r; color = mix(color, color * ao, pbrMaterial.occlusionStrength); } #endif var emissive = pbrMaterial.emissiveFactor; #ifdef HAS_EMISSIVEMAP if (pbrMaterial.emissiveMapEnabled != 0u) { emissive *= SRGBtoLINEAR( textureSample(pbr_emissiveSampler, pbr_emissiveSamplerSampler, emissiveUV) ).rgb; } #endif color += emissive * pbrMaterial.emissiveStrength; if (transmission > 0.0) { color = mix(color, color * getVolumeAttenuation(thickness), transmission); } // This section uses mix to override final color for reference app visualization // of various parameters in the lighting equation. #ifdef PBR_DEBUG // TODO: Figure out how to debug multiple lights // color = mix(color, F, pbr_scaleFGDSpec.x); // color = mix(color, vec3(G), pbr_scaleFGDSpec.y); // color = mix(color, vec3(D), pbr_scaleFGDSpec.z); // color = mix(color, specContrib, pbr_scaleFGDSpec.w); // color = mix(color, diffuseContrib, pbr_scaleDiffBaseMR.x); color = mix(color, baseColor.rgb, pbrMaterial.scaleDiffBaseMR.y); color = mix(color, vec3(metallic), pbrMaterial.scaleDiffBaseMR.z); color = mix(color, vec3(perceptualRoughness), pbrMaterial.scaleDiffBaseMR.w); #endif } let alpha = clamp(baseColor.a * (1.0 - transmission), 0.0, 1.0); return vec4(pow(color, vec3(1.0 / 2.2)), alpha); } `;