The glTF format is designed to hold objects which are intended for "physically based rendering", where the parameters of the object map to physical properties such as metallicity, roughness, etc. This function replaces the default rgl shaders with PBR shaders based on the reference implementation in https://github.com/KhronosGroup/glTF-Sample-Viewer/tree/88eda8c5358efe03128b72b6c5f5f6e5b6d023e1/shaders.

setPBRshaders(gltf, gltfMat, id,
              scene = scene3d(minimal = TRUE),
              useIBL = TRUE,
              brdfLUT = system.file("textures/brdfLUT.png", package = "rgl2gltf"),
              IBLspecular = system.file("textures/refmap.png", package = "rgl"),
              IBLdiffuse = system.file("textures/refmapblur.jpeg", package = "rgl2gltf"),
              debugBaseColor = 0,
              debugMetallic = 0,
              debugRoughness = 0,
              debugSpecularReflection = 0,
              debugGeometricOcclusion = 0,
              debugMicrofacetDistribution = 0,
              debugSpecContrib = 0,
              debugDiffuseContrib = 0,
              debugIBLDiffuse = 1,
              debugIBLSpecular = 1,
              defines = list(),
              uniforms = list(),
              attributes = list(),
              textures = list())

Arguments

gltf, gltfMat

A "gltf" object, and a material record from it.

id, scene

The rgl id of the corresponding object and the scene holding it.

useIBL

Whether to use image based lighting.

brdfLUT

The texture to use for the "bidirectional reflectance distribution function" lookup table.

IBLspecular

The texture to use for the "image based specular lighting".

IBLdiffuse

The texture to use for the "image based diffuse lighting".

debugBaseColor, debugMetallic, debugRoughness, debugSpecularReflection, debugGeometricOcclusion, debugMicrofacetDistribution, debugSpecContrib, debugDiffuseContrib

These are flags used for debugging. Setting one of these to 1 will display just that contribution to the rendering.

debugIBLDiffuse, debugIBLSpecular

Two more debugging settings. These should be set to non-negative values to control the contribution from each of those components to the image based lighting.

defines, uniforms, attributes, textures

Values to use in setUserShaders in addition to the ones determined by this function.

Details

rgl is designed to work with WebGL version 1, which doesn't support all of the features used in the reference shaders. In particular, no extensions are assumed, and the IBL textures are single 2D textures rather than cube maps.

Value

This function modifies the id object in scene, and returns the modified scene.

Author

Duncan Murdoch for the adaptation to rgl, various others for the original shaders.

See also

Examples


# This web page has lots of sample files

samples <- "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0"

# Get one of them:  a 2 cylinder engine

gltf <- readGLB(paste0(samples, "/NormalTangentTest/glTF-Binary/NormalTangentTest.glb?raw=true"))
gltfMat <- gltf$getMaterial(0)
scene <- as.rglscene(gltf)
id <- scene$objects[[1]]$id
scene <- setPBRshaders(gltf, gltfMat, id, scene)
cat(scene$objects[[1]]$userFragmentShader)
#> #define HAS_UV 1
#> #define HAS_NORMALS 1
#> #define HAS_BASECOLORMAP 1
#> #define HAS_NORMALMAP 1
#> #define HAS_TANGENTS 1
#> #define HAS_METALROUGHNESSMAP 1
#> #define HAS_OCCLUSIONMAP 1
#> #define USE_IBL 1
#> #define MANUAL_SRGB 1
#> //
#> // This fragment shader defines a reference implementation for Physically Based Shading of
#> // a microfacet surface material defined by a glTF model.
#> //
#> // References:
#> // [1] Real Shading in Unreal Engine 4
#> //     http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf
#> // [2] Physically Based Shading at Disney
#> //     http://blog.selfshadow.com/publications/s2012-shading-course/burley/s2012_pbs_disney_brdf_notes_v3.pdf
#> // [3] README.md - Environment Maps
#> //     https://github.com/KhronosGroup/glTF-Sample-Viewer/#environment-maps
#> // [4] "An Inexpensive BRDF Model for Physically based Rendering" by Christophe Schlick
#> //     https://www.cs.virginia.edu/~jdl/bib/appearance/analytic%20models/schlick94b.pdf
#> 
#> #ifdef GL_ES
#> #ifdef GL_FRAGMENT_PRECISION_HIGH
#>   precision highp float;
#> #else
#>   precision mediump float;
#> #endif
#> #endif
#> 
#> uniform vec3 lightDir0;
#> uniform vec3 u_LightColor;
#> 
#> #ifdef USE_IBL
#> uniform sampler2D u_DiffuseEnvSampler;
#> uniform sampler2D u_SpecularEnvSampler;
#> uniform sampler2D u_brdfLUT;
#> // debugging flag
#> uniform vec2 u_ScaleIBLAmbient;
#> #endif
#> 
#> #ifdef HAS_BASECOLORMAP
#> uniform sampler2D uSampler;
#> #endif
#> #ifdef HAS_NORMALMAP
#> uniform sampler2D normalTexture;
#> uniform float u_NormalScale;
#> #endif
#> #ifdef HAS_EMISSIVEMAP
#> uniform sampler2D u_EmissiveSampler;
#> uniform vec3 u_EmissiveFactor;
#> #endif
#> #ifdef HAS_METALROUGHNESSMAP
#> uniform sampler2D u_MetallicRoughnessSampler;
#> #endif
#> #ifdef HAS_OCCLUSIONMAP
#> uniform sampler2D u_OcclusionSampler;
#> uniform float u_OcclusionStrength;
#> #endif
#> 
#> uniform vec2 u_MetallicRoughnessValues;
#> varying vec4 vCol;
#> 
#> // debugging flags used for shader output of intermediate PBR variables
#> uniform vec4 u_ScaleDiffBaseMR;
#> uniform vec4 u_ScaleFGDSpec;
#> 
#> varying vec4 vPosition;
#> 
#> varying vec2 vTexcoord;
#> 
#> #ifdef HAS_NORMALS
#> #ifdef HAS_TANGENTS
#> varying mat3 vtbnMatrix;
#> #else
#> varying vec3 vNormal;
#> #endif
#> #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
#> {
#>     float NdotL;                  // cos angle between normal and light direction
#>     float NdotV;                  // cos angle between normal and view direction
#>     float NdotH;                  // cos angle between normal and half vector
#>     float LdotH;                  // cos angle between light direction and half vector
#>     float VdotH;                  // cos angle between view direction and half vector
#>     float perceptualRoughness;    // roughness value, as authored by the model creator (input to shader)
#>     float metalness;              // metallic value at the surface
#>     vec3 reflectance0;            // full reflectance color (normal incidence angle)
#>     vec3 reflectance90;           // reflectance color at grazing angle
#>     float alphaRoughness;         // roughness mapped to a more linear change in the roughness (proposed by [2])
#>     vec3 diffuseColor;            // color contribution from diffuse lighting
#>     vec3 specularColor;           // color contribution from specular lighting
#> };
#> 
#> const float M_PI = 3.141592653589793;
#> const float c_MinRoughness = 0.04;
#> 
#> vec4 SRGBtoLINEAR(vec4 srgbIn)
#> {
#>     #ifdef MANUAL_SRGB
#>     #ifdef SRGB_FAST_APPROXIMATION
#>     vec3 linOut = pow(srgbIn.xyz,vec3(2.2));
#>     #else //SRGB_FAST_APPROXIMATION
#>     vec3 bLess = step(vec3(0.04045),srgbIn.xyz);
#>     vec3 linOut = mix( srgbIn.xyz/vec3(12.92), pow((srgbIn.xyz+vec3(0.055))/vec3(1.055),vec3(2.4)), bLess );
#>     #endif //SRGB_FAST_APPROXIMATION
#>     return vec4(linOut,srgbIn.w);;
#>     #else //MANUAL_SRGB
#>     return srgbIn;
#>     #endif //MANUAL_SRGB
#> }
#> 
#> // Find the normal for this fragment, pulling either from a predefined normal map
#> // or from the interpolated mesh normal and tangent attributes.
#> vec3 getNormal()
#> {
#>     // Retrieve the tangent space matrix
#> #ifndef HAS_TANGENTS
#> #ifdef HAS_EXTENSIONS
#>     vec3 pos_dx = dFdx(vPosition.xyz);
#>     vec3 pos_dy = dFdy(vPosition.xyz);
#>     vec3 tex_dx = dFdx(vec3(vTexcoord, 0.0));
#>     vec3 tex_dy = dFdy(vec3(vTexcoord, 0.0));
#>     vec3 t = (tex_dy.t * pos_dx - tex_dx.t * pos_dy) / (tex_dx.s * tex_dy.t - tex_dy.s * tex_dx.t);
#> #else
#>     vec3 pos_dx = vec3(1);
#>     vec3 pos_dy = vec3(1);
#>     vec3 tex_dx = vec3(1);
#>     vec3 tex_dy = vec3(1);
#>     vec3 t = (tex_dy.t * pos_dx - tex_dx.t * pos_dy) / (tex_dx.s * tex_dy.t - tex_dy.s * tex_dx.t);
#> #endif
#> #ifdef HAS_NORMALS
#>     vec3 ng = normalize(vNormal);
#> #else
#>     vec3 ng = cross(pos_dx, pos_dy);
#> #endif
#> 
#>     t = normalize(t - ng * dot(ng, t));
#>     vec3 b = normalize(cross(ng, t));
#>     mat3 tbn = mat3(t, b, ng);
#> #else // HAS_TANGENTS
#>     mat3 tbn = vtbnMatrix;
#> #endif
#> 
#> #ifdef HAS_NORMALMAP
#>     vec3 n = texture2D(normalTexture, vTexcoord).rgb;
#>     n = normalize(tbn * ((2.0 * n - 1.0) * vec3(u_NormalScale, u_NormalScale, 1.0)));
#> #else
#>     // The tbn matrix is linearly interpolated, so we need to re-normalize
#>     vec3 n = normalize(tbn[2].xyz);
#> #endif
#> 
#>     return n;
#> }
#> 
#> // 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
#> vec3 getIBLContribution(PBRInfo pbrInputs, vec3 n, vec3 reflection)
#> {
#>     float mipCount = 9.0; // resolution of 512x512
#>     float lod = (pbrInputs.perceptualRoughness * mipCount);
#>     // retrieve a scale and bias to F0. See [1], Figure 3
#>     vec3 brdf = SRGBtoLINEAR(texture2D(u_brdfLUT, vec2(pbrInputs.NdotV, 1.0 - pbrInputs.perceptualRoughness))).rgb;
#>     vec2 latlong1 = vec2(asin(n.z)/M_PI + 0.5,
#>                          atan(n.y, n.x)/2.0/M_PI + 0.5);
#>     vec3 diffuseLight = SRGBtoLINEAR(texture2D(u_DiffuseEnvSampler, latlong1)).rgb;
#> 
#> #ifdef USE_TEX_LOD
#>     vec3 specularLight = SRGBtoLINEAR(textureCubeLodEXT(u_SpecularEnvSampler, reflection, lod)).rgb;
#> #else
#>     vec2 latlong2 = vec2(asin(reflection.z)/M_PI + 0.5,
#>                          atan(reflection.y, reflection.x)/2.0/M_PI + 0.5);
#>     vec3 specularLight = SRGBtoLINEAR(texture2D(u_SpecularEnvSampler, latlong2.xy)).rgb;
#> #endif
#> 
#>     vec3 diffuse = diffuseLight * pbrInputs.diffuseColor;
#>     vec3 specular = specularLight * (pbrInputs.specularColor * brdf.x + brdf.y);
#> 
#>     // For presentation, this allows us to disable IBL terms
#>     diffuse *= u_ScaleIBLAmbient.x;
#>     specular *= u_ScaleIBLAmbient.y;
#> 
#>     return diffuse + specular;
#> }
#> #endif
#> 
#> // Basic Lambertian diffuse
#> // Implementation from Lambert's Photometria https://archive.org/details/lambertsphotome00lambgoog
#> // See also [1], Equation 1
#> vec3 diffuse(PBRInfo pbrInputs)
#> {
#>     return pbrInputs.diffuseColor / M_PI;
#> }
#> 
#> // The following equation models the Fresnel reflectance term of the spec equation (aka F())
#> // Implementation of fresnel from [4], Equation 15
#> vec3 specularReflection(PBRInfo pbrInputs)
#> {
#>     return pbrInputs.reflectance0 + (pbrInputs.reflectance90 - pbrInputs.reflectance0) * pow(clamp(1.0 - pbrInputs.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].
#> float geometricOcclusion(PBRInfo pbrInputs)
#> {
#>     float NdotL = pbrInputs.NdotL;
#>     float NdotV = pbrInputs.NdotV;
#>     float r = pbrInputs.alphaRoughness;
#> 
#>     float attenuationL = 2.0 * NdotL / (NdotL + sqrt(r * r + (1.0 - r * r) * (NdotL * NdotL)));
#>     float 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.
#> float microfacetDistribution(PBRInfo pbrInputs)
#> {
#>     float roughnessSq = pbrInputs.alphaRoughness * pbrInputs.alphaRoughness;
#>     float f = (pbrInputs.NdotH * roughnessSq - pbrInputs.NdotH) * pbrInputs.NdotH + 1.0;
#>     return roughnessSq / (M_PI * f * f);
#> }
#> 
#> void main()
#> {
#>     // 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
#>     float perceptualRoughness = u_MetallicRoughnessValues.y;
#>     float metallic = u_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
#>     vec4 mrSample = texture2D(u_MetallicRoughnessSampler, vTexcoord);
#>     perceptualRoughness = mrSample.g * perceptualRoughness;
#>     metallic = mrSample.b * metallic;
#> #endif
#>     perceptualRoughness = clamp(perceptualRoughness, c_MinRoughness, 1.0);
#>     metallic = clamp(metallic, 0.0, 1.0);
#>     // Roughness is authored as perceptual roughness; as is convention,
#>     // convert to material roughness by squaring the perceptual roughness [2].
#>     float alphaRoughness = perceptualRoughness * perceptualRoughness;
#> 
#>     // The albedo may be defined from a base texture or a flat color
#> #ifdef HAS_BASECOLORMAP
#>     vec4 baseColor = SRGBtoLINEAR(texture2D(uSampler, vTexcoord)) * vCol;
#> #else
#>     vec4 baseColor = vCol;
#> #endif
#> 
#>     vec3 f0 = vec3(0.04);
#>     vec3 diffuseColor = baseColor.rgb * (vec3(1.0) - f0);
#>     diffuseColor *= 1.0 - metallic;
#>     vec3 specularColor = mix(f0, baseColor.rgb, metallic);
#> 
#>     // Compute reflectance.
#>     float 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 reflecance to 0%.
#>     float reflectance90 = clamp(reflectance * 25.0, 0.0, 1.0);
#>     vec3 specularEnvironmentR0 = specularColor.rgb;
#>     vec3 specularEnvironmentR90 = vec3(1.0, 1.0, 1.0) * reflectance90;
#> 
#>     vec3 n = getNormal();                             // normal at surface point
#>     vec3 v = normalize(- vPosition.xyz);        // Vector from surface point to camera
#>     vec3 l = normalize(lightDir0);                    // Vector from surface point to light
#>     vec3 h = normalize(l+v);                          // Half vector between both l and v
#>     vec3 reflection = -normalize(reflect(v, n));
#> 
#>     float NdotL = clamp(dot(n, l), 0.001, 1.0);
#>     float NdotV = clamp(abs(dot(n, v)), 0.001, 1.0);
#>     float NdotH = clamp(dot(n, h), 0.0, 1.0);
#>     float LdotH = clamp(dot(l, h), 0.0, 1.0);
#>     float VdotH = clamp(dot(v, h), 0.0, 1.0);
#> 
#>     PBRInfo pbrInputs = PBRInfo(
#>         NdotL,
#>         NdotV,
#>         NdotH,
#>         LdotH,
#>         VdotH,
#>         perceptualRoughness,
#>         metallic,
#>         specularEnvironmentR0,
#>         specularEnvironmentR90,
#>         alphaRoughness,
#>         diffuseColor,
#>         specularColor
#>     );
#> 
#>     // Calculate the shading terms for the microfacet specular shading model
#>     vec3 F = specularReflection(pbrInputs);
#>     float G = geometricOcclusion(pbrInputs);
#>     float D = microfacetDistribution(pbrInputs);
#> 
#>     // Calculation of analytical lighting contribution
#>     vec3 diffuseContrib = (1.0 - F) * diffuse(pbrInputs);
#>     vec3 specContrib = F * G * D / (4.0 * NdotL * NdotV);
#>     // Obtain final intensity as reflectance (BRDF) scaled by the energy of the light (cosine law)
#>     vec3 color = NdotL * u_LightColor * (diffuseContrib + specContrib);
#> 
#>     // Calculate lighting contribution from image based lighting source (IBL)
#> #ifdef USE_IBL
#>     color += getIBLContribution(pbrInputs, n, reflection);
#> #endif
#> 
#>     // Apply optional PBR terms for additional (optional) shading
#> #ifdef HAS_OCCLUSIONMAP
#>     float ao = texture2D(u_OcclusionSampler, vTexcoord).r;
#>     color = mix(color, color * ao, u_OcclusionStrength);
#> #endif
#> 
#> #ifdef HAS_EMISSIVEMAP
#>     vec3 emissive = SRGBtoLINEAR(texture2D(u_EmissiveSampler, vTexcoord)).rgb * u_EmissiveFactor;
#>     color += emissive;
#> #endif
#> 
#>     // This section uses mix to override final color for reference app visualization
#>     // of various parameters in the lighting equation.
#>     color = mix(color, F, u_ScaleFGDSpec.x);
#>     color = mix(color, vec3(G), u_ScaleFGDSpec.y);
#>     color = mix(color, vec3(D), u_ScaleFGDSpec.z);
#>     color = mix(color, specContrib, u_ScaleFGDSpec.w);
#> 
#>     color = mix(color, diffuseContrib, u_ScaleDiffBaseMR.x);
#>     color = mix(color, baseColor.rgb, u_ScaleDiffBaseMR.y);
#>     color = mix(color, vec3(metallic), u_ScaleDiffBaseMR.z);
#>     color = mix(color, vec3(perceptualRoughness), u_ScaleDiffBaseMR.w);
#> 
#>     gl_FragColor = vec4(pow(color,vec3(1.0/2.2)), baseColor.a);
#> }