diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 0000000000..025c3bb0fa --- /dev/null +++ b/docs/404.html @@ -0,0 +1,203 @@ + + + + + + Page not found - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Document not found (404)

+

This URL is invalid, sorry. Please use the navigation bar or search to continue.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/Filament.html b/docs/Filament.html new file mode 100644 index 0000000000..17ecbf4950 --- /dev/null +++ b/docs/Filament.html @@ -0,0 +1,11 @@ + + + + + + Redirecting... + + + + + diff --git a/docs/Filament.md.html b/docs/Filament.md.html new file mode 100644 index 0000000000..167f967bd5 --- /dev/null +++ b/docs/Filament.md.html @@ -0,0 +1,4315 @@ + + + + +**Physically Based Rendering in Filament** + +![](images/filament_logo.png) + +# About + +This document is part of the [Filament project](https://github.com/google/filament). To report errors in this document please use the [project's issue tracker](https://github.com/google/filament/issues). + +## Authors + +- [Romain Guy](https://github.com/romainguy), [@romainguy](https://twitter.com/romainguy) +- [Mathias Agopian](https://github.com/pixelflinger), [@darthmoosious](https://twitter.com/darthmoosious) + +# Overview + +Filament is a physically based rendering (PBR) engine for Android. The goal of Filament is to offer a set of tools and APIs for Android developers that will enable them to create high quality 2D and 3D rendering with ease. + +The goal of this document is to explain the equations and theory behind the material and lighting models used in Filament. This document is intended as a reference for contributors to Filament or developers interested in the inner workings of the engine. We will provide code snippets as needed to make the relationship between theory and practice as clear as possible. + +This document is not intended as a design document. It focuses solely on algorithms and its content could be used to implement PBR in any engine. However, this document explains why we chose specific algorithms/models over others. + +Unless noted otherwise, all the 3D renderings present in this document have been generated in-engine (prototype or production). Many of these 3D renderings were captured during the early stages of development of Filament and do not reflect the final quality. + +## Principles + +Real-time rendering is an active area of research and there is a large number of equations, algorithms and implementation to choose from for every single feature that needs to be implemented (the book *Rendering real-time shadows*, for instance, is a 400 pages summary of dozens of shadows rendering techniques). As such, we must first define our goals (or principles, to follow Brent Burley's seminal paper Physically-based shading at Disney [#Burley12]) before we can make informed decisions. + +Real-time mobile performance +: Our primary goal is to design and implement a rendering system able to perform efficiently on mobile platforms. The primary target will be OpenGL ES 3.x class GPUs. + +Quality +: Our rendering system will emphasize overall picture quality. We will however accept quality compromises to support low and medium performance GPUs. + +Ease of use +: Artists need to be able to iterate often and quickly on their assets and our rendering system must allow them to do so intuitively. We must therefore provide parameters that are easy to understand (for instance, no specular power). + + We also understand that not all developers have the luxury to work with artists. The physically based approach of our system will allow developers to craft visually plausible materials without the need to understand the theory behind our implementation. + + For both artists and developers, our system will rely on as few parameters as possible to reduce trial and error and allow users to quickly master the material model. + + In addition, any combination of parameter values should lead to physically plausible results. Physically implausible materials must be hard to create. + +Familiarity +: Our system should use physical units everywhere possible: distances in meters or centimeters, color temperatures in Kelvin, light units in lumens or candelas, etc. + +Flexibility +: A physically based approach must not preclude non-realistic rendering. User interfaces for instance will need unlit materials. + +Deployment size +: While not directly related to the content of this document, it bears emphasizing our desire to keep the rendering library as small as possible so any application can bundle it without increasing the binary to undesirable sizes. + +## Physically based rendering + +We chose to adopt PBR for its benefits from an artistic and production efficient standpoints, and because it is compatible with our goals. + +Physically based rendering is a rendering method that provides a more accurate representation of materials and how they interact with light when compared to traditional real-time models. The separation of materials and lighting at the core of the PBR method makes it easier to create realistic assets that look accurate in all lighting conditions. + +# Notation + +$$ +\newcommand{NoL}{n \cdot l} +\newcommand{NoV}{n \cdot v} +\newcommand{NoH}{n \cdot h} +\newcommand{VoH}{v \cdot h} +\newcommand{LoH}{l \cdot h} +\newcommand{fNormal}{f_{0}} +\newcommand{fDiffuse}{f_d} +\newcommand{fSpecular}{f_r} +\newcommand{fX}{f_x} +\newcommand{aa}{\alpha^2} +\newcommand{fGrazing}{f_{90}} +\newcommand{schlick}{F_{Schlick}} +\newcommand{nior}{n_{ior}} +\newcommand{Ed}{E_d} +\newcommand{Lt}{L_{\bot}} +\newcommand{Lout}{L_{out}} +\newcommand{cosTheta}{\left< \cos \theta \right> } +$$ + +The equations found throughout this document use the symbols described in table [symbols]. + + + Symbol | Definition +:---------------------------:|:---------------------------| +$v$ | View unit vector +$l$ | Incident light unit vector +$n$ | Surface normal unit vector +$h$ | Half unit vector between $l$ and $v$ +$f$ | BRDF +$\fDiffuse$ | Diffuse component of a BRDF +$\fSpecular$ | Specular component of a BRDF +$\alpha$ | Roughness, remapped from using input `perceptualRoughness` +$\sigma$ | Diffuse reflectance +$\Omega$ | Spherical domain +$\fNormal$ | Reflectance at normal incidence +$\fGrazing$ | Reflectance at grazing angle +$\chi^+(a)$ | Heaviside function (1 if $a > 0$ and 0 otherwise) +$n_{ior}$ | Index of refraction (IOR) of an interface +$\left< \NoL \right>$ | Dot product clamped to [0..1] +$\left< a \right>$ | Saturated value (clamped to [0..1]) +[Table [symbols]: Symbols definitions] + +# Material system + +The sections below describe multiple material models to simplify the description of various surface features such as anisotropy or the clear coat layer. In practice however some of these models are condensed into a single one. For instance, the standard model, the clear coat model and the anisotropic model can be combined to form a single, more flexible and powerful model. Please refer to the [Materials documentation](./Materials.md.html) to get a description of the material models as implemented in Filament. + +## Standard model + +The goal of our model is to represent standard material appearances. A material model is described mathematically by a BSDF (Bidirectional Scattering Distribution Function), which is itself composed of two other functions: the BRDF (Bidirectional Reflectance Distribution Function) and the BTDF (Bidirectional Transmittance Function). + +Since we aim to model commonly encountered surfaces, our standard material model will focus on the BRDF and ignore the BTDF, or approximate it greatly. Our standard model will therefore only be able to correctly mimic reflective, isotropic, dielectric or conductive surfaces with short mean free paths. + +The BRDF describes the surface response of a standard material as a function made of two terms: +- A diffuse component, or $f_d$ +- A specular component, or $f_r$ + +The relationship between a surface, the surface normal, incident light and these terms is shown in figure [frFd] (we ignore subsurface scattering for now): + +![Figure [frFd]: Interaction of the light with a surface using BRDF model with a diffuse term $ f_d $ and a specular term $ f_r $](images/diagram_fr_fd.png) + +The complete surface response can be expressed as such: + +$$\begin{equation}\label{brdf} +f(v,l)=f_d(v,l)+f_r(v,l) +\end{equation}$$ + +This equation characterizes the surface response for incident light from a single direction. The full rendering equation would require to integrate $l$ over the entire hemisphere. + +Commonly encountered surfaces are usually not made of a flat interface so we need a model that can characterize the interaction of light with an irregular interface. + +A microfacet BRDF is a good physically plausible BRDF for that purpose. Such BRDF states that surfaces are not smooth at a micro level, but made of a large number of randomly aligned planar surface fragments, called microfacets. Figure [microfacetVsFlat] shows the difference between a flat interface and an irregular interface at a micro level: + +![Figure [microfacetVsFlat]: Irregular interface as modeled by a microfacet model (left) and flat interface (right)](images/diagram_microfacet.png) + +Only the microfacets whose normal is oriented halfway between the light direction and the view direction will reflect visible light, as shown in figure [microfacets]. + +![Figure [microfacets]: Microfacets](images/diagram_macrosurface.png) + +However, not all microfacets with a properly oriented normal will contribute reflected light as the BRDF takes into account masking and shadowing. This is illustrated in figure [microfacetShadowing]. + +![Figure [microfacetShadowing]: Masking and shadowing of microfacets](images/diagram_shadowing_masking.png) + +A microfacet BRDF is heavily influenced by a _roughness_ parameter which describes how smooth (low roughness) or how rough (high roughness) a surface is at a micro level. The smoother the surface, the more facets are aligned and the more pronounced the reflected light is. The rougher the surface, the fewer facets are oriented towards the camera and incoming light is scattered away from the camera after reflection, giving a blurry aspect to the specular highlights. + +Figure [roughness] shows surfaces of different roughness and how light interacts with them. + +![Figure [roughness]: Varying roughness (from left to right, rough to smooth) and the resulting BRDF specular component lobe](images/diagram_roughness.png) + +!!! Note: About roughness + The roughness parameter as set by the user is called `perceptualRoughness` in the shader snippets throughout this document. The variable called `roughness` is the `perceptualRoughness` with a remapping explained in section [Parameterization]. + +A microfacet model is described by the following equation (where x stands for the specular or diffuse component): + +$$\begin{equation} +\fX(v,l) = \frac{1}{| \NoV | | \NoL |} +\int_\Omega D(m,\alpha) G(v,l,m) f_m(v,l,m) (v \cdot m) (l \cdot m) dm +\end{equation}$$ + +The term $D$ models the distribution of the microfacets (this term is also referred to as the NDF or Normal Distribution Function). This term plays a primordial role in the appearance of surfaces as shown in figure [roughness]. + +The term $G$ models the visibility (or occlusion or shadow-masking) of the microfacets. + +Since this equation is valid for both the specular and diffuse components, the difference lies in the microfacet BRDF $f_m$. + +It is important to note that this equation is used to integrate over the hemisphere at a _micro level_: + +![Figure [microLevel]: Modeling the surface response at a single point requires an integration at the micro level](images/diagram_micro_vs_macro.png) + +The diagram above shows that at a macro level, the surfaces is considered flat. This helps simplify our equations by assuming that a shaded fragment lit from a single direction corresponds to a single point at the surface. + +At a micro level however, the surface is not flat and we cannot assume a single ray of light anymore (we can however assume that the incident rays are parallel). Since the micro facets will scatter the light in different directions given a bundle of parallel incident rays, we must integrate the surface response over a hemisphere, noted m in the above diagram. + +It is obviously not practical to compute the full integration over the microfacets hemisphere for each shaded fragment. We will therefore rely on approximations of the integration for both the specular and diffuse components. + +## Dielectrics and conductors + +To better understand some of the equations and behaviors shown below, we must first clearly understand the difference between metallic (conductor) and non-metallic (dielectric) surfaces. + +We saw earlier that when incident light hits a surface governed by a BRDF, the light is reflected as two separate components: the diffuse reflectance and the specular reflectance. The modelization of this behavior is straightforward as shown in figure [bsdfBrdf]. + +![Figure [bsdfBrdf]: Modelization of the BRDF part of a BSDF](images/diagram_fr_fd.png) + +This modelization is a simplification of how the light actually interacts with the surface. In reality, part of the incident light will penetrate the surface, scatter inside, and exit the surface again as diffuse reflectance. This phenomenon is illustrated in figure [diffuseScattering]. + +![Figure [diffuseScattering]: Scattering of diffuse light](images/diagram_scattering.png) + +Here lies the difference between conductors and dielectrics. There is no subsurface scattering occurring with purely metallic materials, which means there is no diffuse component (and we will see later that this has an influence on the perceived color of the specular component). Scattering happens in dielectrics, which means they have both specular and diffuse components. + +To properly modelize the BRDF we must therefore distinguish between dielectrics and conductors (scattering not shown for clarity), as shown in figure [dielectricConductor]. + +![Figure [dielectricConductor]: BRDF modelization for dielectric and conductor surfaces](images/diagram_brdf_dielectric_conductor.png) + +## Energy conservation + +Energy conservation is one of the key components of a good BRDF for physically based rendering. An energy conservative BRDF states that the total amount of specular and diffuse reflectance energy is less than the total amount of incident energy. Without an energy conservative BRDF, artists must manually ensure that the light reflected off a surface is never more intense than the incident light. + +## Specular BRDF + +For the specular term, $f_r$ is a mirror BRDF that can be modeled with the Fresnel law, noted $F$ in the Cook-Torrance approximation of the microfacet model integration: + +$$\begin{equation} +f_r(v,l) = \frac{D(h, \alpha) G(v, l, \alpha) F(v, h, f0)}{4(\NoV)(\NoL)} +\end{equation}$$ + +Given our real-time constraints, we must use an approximation for the three terms $D$, $G$ and $F$. [#Karis13a] has compiled a great list of formulations for these three terms that can be used with the Cook-Torrance specular BRDF. The sections that follow describe the equations we picked for these terms. + +### Normal distribution function (specular D) + +[#Burley12] observed that long-tailed normal distribution functions (NDF) are a good fit for real-world surfaces. The GGX distribution described in [#Walter07] is a distribution with long-tailed falloff and short peak in the highlights, with a simple formulation suitable for real-time implementations. It is also a popular model, equivalent to the Trowbridge-Reitz distribution, in modern physically based renderers. + +$$\begin{equation} +D_{GGX}(h,\alpha) = \frac{\aa}{\pi ( (\NoH)^2 (\aa - 1) + 1)^2} +\end{equation}$$ + +The GLSL implementation of the NDF, shown in listing [specularD], is simple and efficient. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +float D_GGX(float NoH, float roughness) { + float a = NoH * roughness; + float k = roughness / (1.0 - NoH * NoH + a * a); + return k * k * (1.0 / PI); +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [specularD]: Implementation of the specular D term in GLSL] + +We can improve this implementation by using half precision floats. This optimization requires changes to the original equation as there are two problems when computing $1 - (\NoH)^2$ in half-floats. First, this computation suffers from floating point cancellation when $(\NoH)^2$ is close to 1 (highlights). Secondly $\NoH$ does not have enough precision around 1. + +The solution involves Lagrange's identity: + +$$\begin{equation} +| a \times b |^2 = |a|^2 |b|^2 - (a \cdot b)^2 +\end{equation}$$ + +Since both $n$ and $h$ are unit vectors, $|n \times h|^2 = 1 - (\NoH)^2$. This allows us to compute $1 - (\NoH)^2$ directly with half precision floats by using a simple cross product. Listing [specularDfp16] shows the final optimized implementation. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +#define MEDIUMP_FLT_MAX 65504.0 +#define saturateMediump(x) min(x, MEDIUMP_FLT_MAX) + +float D_GGX(float roughness, float NoH, const vec3 n, const vec3 h) { + vec3 NxH = cross(n, h); + float a = NoH * roughness; + float k = roughness / (dot(NxH, NxH) + a * a); + float d = k * k * (1.0 / PI); + return saturateMediump(d); +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [specularDfp16]: Implementation of the specular D term in GLSL optimized for fp16] + +### Geometric shadowing (specular G) + +Eric Heitz showed in [#Heitz14] that the Smith geometric shadowing function is the correct and exact $G$ term to use. The Smith formulation is the following: + +$$\begin{equation} +G(v,l,\alpha) = G_1(l,\alpha) G_1(v,\alpha) +\end{equation}$$ + +$G_1$ can in turn follow several models, and is commonly set to the GGX formulation: + +$$\begin{equation} +G_1(v,\alpha) = G_{GGX}(v,\alpha) = \frac{2 (\NoV)}{\NoV + \sqrt{\aa + (1 - \aa) (\NoV)^2}} +\end{equation}$$ + +The full Smith-GGX formulation thus becomes: + +$$\begin{equation} +G(v,l,\alpha) = \frac{2 (\NoL)}{\NoL + \sqrt{\aa + (1 - \aa) (\NoL)^2}} \frac{2 (\NoV)}{\NoV + \sqrt{\aa + (1 - \aa) (\NoV)^2}} +\end{equation}$$ + +We can observe that the dividends $2 (\NoL)$ and $2 (n \cdot v)$ allow us to simplify the original function $f_r$ by introducing a visibility function $V$: + +$$\begin{equation} +f_r(v,l) = D(h, \alpha) V(v, l, \alpha) F(v, h, f_0) +\end{equation}$$ + +Where: + +$$\begin{equation} +V(v,l,\alpha) = \frac{G(v, l, \alpha)}{4 (\NoV) (\NoL)} = V_1(l,\alpha) V_1(v,\alpha) +\end{equation}$$ + +And: + +$$\begin{equation} +V_1(v,\alpha) = \frac{1}{\NoV + \sqrt{\aa + (1 - \aa) (\NoV)^2}} +\end{equation}$$ + +Heitz notes however that taking the height of the microfacets into account to correlate masking and shadowing leads to more accurate results. He defines the height-correlated Smith function thusly: + +$$\begin{equation} +G(v,l,h,\alpha) = \frac{\chi^+(\VoH) \chi^+(\LoH)}{1 + \Lambda(v) + \Lambda(l)} +\end{equation}$$ + +$$\begin{equation} +\Lambda(m) = \frac{-1 + \sqrt{1 + \aa tan^2(\theta_m)}}{2} = \frac{-1 + \sqrt{1 + \aa \frac{(1 - cos^2(\theta_m))}{cos^2(\theta_m)}}}{2} +\end{equation}$$ + +Replacing $cos(\theta_m)$ by $\NoV$, we obtain: + +$$\begin{equation} +\Lambda(v) = \frac{1}{2} \left( \frac{\sqrt{\aa + (1 - \aa)(\NoV)^2}}{\NoV} - 1 \right) +\end{equation}$$ + +From which we can derive the visibility function: + +$$\begin{equation} +V(v,l,\alpha) = \frac{0.5}{\NoL \sqrt{(\NoV)^2 (1 - \aa) + \aa} + \NoV \sqrt{(\NoL)^2 (1 - \aa) + \aa}} +\end{equation}$$ + +The GLSL implementation of the visibility term, shown in listing [specularV], is a bit more expensive than we would like since it requires two `sqrt` operations. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +float V_SmithGGXCorrelated(float NoV, float NoL, float roughness) { + float a2 = roughness * roughness; + float GGXV = NoL * sqrt(NoV * NoV * (1.0 - a2) + a2); + float GGXL = NoV * sqrt(NoL * NoL * (1.0 - a2) + a2); + return 0.5 / (GGXV + GGXL); +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [specularV]: Implementation of the specular V term in GLSL] + +We can optimize this visibility function by using an approximation after noticing that all the terms under the square roots are squares and that all the terms are in the $[0..1]$ range: + +$$\begin{equation} +V(v,l,\alpha) = \frac{0.5}{\NoL (\NoV (1 - \alpha) + \alpha) + \NoV (\NoL (1 - \alpha) + \alpha)} +\end{equation}$$ + +This approximation is mathematically wrong but saves two square root operations and is good enough for real-time mobile applications, as shown in listing [approximatedSpecularV]. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +float V_SmithGGXCorrelatedFast(float NoV, float NoL, float roughness) { + float a = roughness; + float GGXV = NoL * (NoV * (1.0 - a) + a); + float GGXL = NoV * (NoL * (1.0 - a) + a); + return 0.5 / (GGXV + GGXL); +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [approximatedSpecularV]: Implementation of the approximated specular V term in GLSL] + +[#Hammon17] proposes the same approximation based on the same observation that the square root can be removed. It does so by rewriting the expressions as _lerps_: + +$$\begin{equation} +V(v,l,\alpha) = \frac{0.5}{lerp(2 (\NoL) (\NoV), \NoL + \NoV, \alpha)} +\end{equation}$$ + +### Fresnel (specular F) + +The Fresnel effect plays an important role in the appearance of physically based materials. This effect models the fact that the amount of light the viewer sees reflected from a surface depends on the viewing angle. Large bodies of water are a perfect way to experience this phenomenon, as shown in figure [fresnelLake]. When looking at the water straight down (at normal incidence) you can see through the water. However, when looking further out in the distance (at grazing angle, where perceived light rays are getting parallel to the surface), you will see the specular reflections on the water become more intense. + +The amount of light reflected depends not only on the viewing angle, but also on the index of refraction (IOR) of the material. At normal incidence (perpendicular to the surface, or 0 degree angle), the amount of light reflected back is noted $\fNormal$ and can be derived from the IOR as we will see in section [Reflectance remapping]. The amount of light reflected back at grazing angle is noted $\fGrazing$ and approaches 100% for smooth materials. + +![Figure [fresnelLake]: The Fresnel effect is particularly evident on large bodies of water](images/photo_fresnel_lake.jpg) + +More formally, the Fresnel term defines how light reflects and refracts at the interface between two different media, or the ratio of reflected and transmitted energy. [#Schlick94] describes an inexpensive approximation of the Fresnel term for the Cook-Torrance specular BRDF: + +$$\begin{equation} +F_{Schlick}(v,h,\fNormal,\fGrazing) = \fNormal + (\fGrazing - \fNormal)(1 - \VoH)^5 +\end{equation}$$ + +The constant $\fNormal$ represents the specular reflectance at normal incidence and is achromatic for dielectrics, and chromatic for metals. The actual value depends on the index of refraction of the interface. The GLSL implementation of this term requires the use of a `pow`, as shown in listing [specularF], which can be replaced by a few multiplications. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +vec3 F_Schlick(float u, vec3 f0, float f90) { + return f0 + (vec3(f90) - f0) * pow(1.0 - u, 5.0); +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [specularF]: Implementation of the specular F term in GLSL] + +This Fresnel function can be seen as interpolating between the incident specular reflectance and the reflectance at grazing angles, represented here by $\fGrazing$. Observation of real world materials show that both dielectrics and conductors exhibit achromatic specular reflectance at grazing angles and that the Fresnel reflectance is 1.0 at 90 degrees. A more correct $\fGrazing$ is discussed in section [Specular occlusion]. + +Using $\fGrazing$ set to 1, the Schlick approximation for the Fresnel term can be optimized for scalar operations by refactoring the code slightly. The result is shown in listing [scalarSpecularF]. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +vec3 F_Schlick(float u, vec3 f0) { + float f = pow(1.0 - u, 5.0); + return f + f0 * (1.0 - f); +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [scalarSpecularF]: Scalar optimization of the specular F term in GLSL] + +## Diffuse BRDF + +In the diffuse term, $f_m$ is a Lambertian function and the diffuse term of the BRDF becomes: + +$$\begin{equation} +\fDiffuse(v,l) = \frac{\sigma}{\pi} \frac{1}{| \NoV | | \NoL |} +\int_\Omega D(m,\alpha) G(v,l,m) (v \cdot m) (l \cdot m) dm +\end{equation}$$ + +Our implementation will instead use a simple Lambertian BRDF that assumes a uniform diffuse response over the microfacets hemisphere: + +$$\begin{equation} +\fDiffuse(v,l) = \frac{\sigma}{\pi} +\end{equation}$$ + +In practice, the diffuse reflectance $\sigma$ is multiplied later, as shown in listing [diffuseBRDF]. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +float Fd_Lambert() { + return 1.0 / PI; +} + +vec3 Fd = diffuseColor * Fd_Lambert(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [diffuseBRDF]: Implementation of the diffuse Lambertian BRDF in GLSL] + +The Lambertian BRDF is obviously extremely efficient and delivers results close enough to more complex models. + +However, the diffuse part would ideally be coherent with the specular term and take into account the surface roughness. Both the Disney diffuse BRDF [#Burley12] and Oren-Nayar model [#Oren94] take the roughness into account and create some retro-reflection at grazing angles. Given our constraints we decided that the extra runtime cost does not justify the slight increase in quality. This sophisticated diffuse model also renders image-based and spherical harmonics more difficult to express and implement. + +For completeness, the Disney diffuse BRDF expressed in [#Burley12] is the following: + +$$\begin{equation} +\fDiffuse(v,l) = \frac{\sigma}{\pi} \schlick(n,l,1,\fGrazing) \schlick(n,v,1,\fGrazing) +\end{equation}$$ + +Where: + +$$\begin{equation} +\fGrazing=0.5 + 2 \cdot \alpha cos^2(\theta_d) +\end{equation}$$ + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +float F_Schlick(float u, float f0, float f90) { + return f0 + (f90 - f0) * pow(1.0 - u, 5.0); +} + +float Fd_Burley(float NoV, float NoL, float LoH, float roughness) { + float f90 = 0.5 + 2.0 * roughness * LoH * LoH; + float lightScatter = F_Schlick(NoL, 1.0, f90); + float viewScatter = F_Schlick(NoV, 1.0, f90); + return lightScatter * viewScatter * (1.0 / PI); +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [diffuseBRDF]: Implementation of the diffuse Disney BRDF in GLSL] + +Figure [lambert_vs_disney] shows a comparison between a simple Lambertian diffuse BRDF and the higher quality Disney diffuse BRDF, using a fully rough dielectric material. For comparison purposes, the right sphere was mirrored. The surface response is very similar with both BRDFs but the Disney one exhibits some nice retro-reflections at grazing angles (look closely at the left edge of the spheres). + +![Figure [lambert_vs_disney]: Comparison between the Lambertian diffuse BRDF (left) and the Disney diffuse BRDF (right)](images/diagram_lambert_vs_disney.png) + +We could allow artists/developers to choose the Disney diffuse BRDF depending on the quality they desire and the performance of the target device. It is important to note however that the Disney diffuse BRDF is not energy conserving as expressed here. + +## Standard model summary + +**Specular term**: a Cook-Torrance specular microfacet model, with a GGX normal distribution function, a Smith-GGX height-correlated visibility function, and a Schlick Fresnel function. + +**Diffuse term**: a Lambertian diffuse model. + +The full GLSL implementation of the standard model is shown in listing [glslBRDF]. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +float D_GGX(float NoH, float a) { + float a2 = a * a; + float f = (NoH * a2 - NoH) * NoH + 1.0; + return a2 / (PI * f * f); +} + +vec3 F_Schlick(float u, vec3 f0) { + return f0 + (vec3(1.0) - f0) * pow(1.0 - u, 5.0); +} + +float V_SmithGGXCorrelated(float NoV, float NoL, float a) { + float a2 = a * a; + float GGXL = NoV * sqrt((-NoL * a2 + NoL) * NoL + a2); + float GGXV = NoL * sqrt((-NoV * a2 + NoV) * NoV + a2); + return 0.5 / (GGXV + GGXL); +} + +float Fd_Lambert() { + return 1.0 / PI; +} + +void BRDF(...) { + vec3 h = normalize(v + l); + + float NoV = abs(dot(n, v)) + 1e-5; + float NoL = clamp(dot(n, l), 0.0, 1.0); + float NoH = clamp(dot(n, h), 0.0, 1.0); + float LoH = clamp(dot(l, h), 0.0, 1.0); + + // perceptually linear roughness to roughness (see parameterization) + float roughness = perceptualRoughness * perceptualRoughness; + + float D = D_GGX(NoH, roughness); + vec3 F = F_Schlick(LoH, f0); + float V = V_SmithGGXCorrelated(NoV, NoL, roughness); + + // specular BRDF + vec3 Fr = (D * V) * F; + + // diffuse BRDF + vec3 Fd = diffuseColor * Fd_Lambert(); + + // apply lighting... +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [glslBRDF]: Evaluation of the BRDF in GLSL] + +## Improving the BRDFs + +We mentioned in section [Energy conservation] that energy conservation is one of the key components of a good BRDF. Unfortunately the BRDFs explored previously suffer from two problems that we will examine below. + +### Energy gain in diffuse reflectance + +The Lambert diffuse BRDF does not account for the light that reflects at the surface and that is therefore not able to participate in the diffuse scattering event. + +[TODO: talk about the issue with fr+fd] + +### Energy loss in specular reflectance + +The Cook-Torrance BRDF we presented earlier attempts to model several events at the microfacet level but does so by accounting for a single bounce of light. This approximation can cause a loss of energy at high roughness, the surface is not energy preserving. Figure [singleVsMultiBounce] shows why this loss of energy occurs. In the single bounce (or single scattering) model, a ray of light hitting the surface can be reflected back onto another microfacet and thus be discarded because of the masking and shadowing term. If we however account for multiple bounces (multiscattering), the same ray of light might escape the microfacet field and be reflected back towards the viewer. + +![Figure [singleVsMultiBounce]: Single scattering (left) vs multiscattering](images/diagram_single_vs_multi_scatter.png) + +Based on this simple explanation, we can intuitively deduce that the rougher a surface is, the higher the chances are that energy gets lost because of the failure to account for multiple scattering events. This loss of energy appears to darken rough materials. Metallic surfaces are particularly affected because all of their reflectance is specular. This darkening effect is illustrated in figure [metallicRoughEnergyLoss]. With multiscattering, energy preservation can be achieved, as shown in figure [metallicRoughEnergyPreservation]. + +![Figure [metallicRoughEnergyLoss]: Darkening increases with roughness due to single scattering](images/material_metallic_energy_loss.png) + +![Figure [metallicRoughEnergyPreservation]: Energy preservation with multiscattering](images/material_metallic_energy_preservation.png) + +We can use a white furnace, a uniform lighting environment set to pure white, to validate the energy preservation property of a BRDF. When energy preservation is achieved, a purely reflective metallic surface ($\fNormal = 1$) should be indistinguishable from the background, no matter the roughness of said surface. Figure [whiteFurnaceLoss] shows what such a surface looks like with the specular BRDF presented in the previous sections. The loss of energy as the roughness increases is obvious. In contrast, figure [whiteFurnacePreservation] shows that accounting for multiscattering events addresses the energy loss. + +![Figure [whiteFurnaceLoss]: Darkening increases with roughness due to single scattering](images/material_furnace_energy_loss.png) + +![Figure [whiteFurnacePreservation]: Energy preservation with multiscattering](images/material_furnace_energy_preservation.png) + +Multiple-scattering microfacet BRDFs are discussed in depth in [#Heitz16]. Unfortunately this paper only presents a stochastic evaluation of the multiscattering BRDF. This solution is therefore not suitable for real-time rendering. Kulla and Conty present a different approach in [#Kulla17]. Their idea is to add an energy compensation term as an additional BRDF lobe shown in equation $\ref{energyCompensationLobe}$: + +$$\begin{equation}\label{energyCompensationLobe} +f_{ms}(l,v) = \frac{(1 - E(l)) (1 - E(v)) F_{avg}^2 E_{avg}}{\pi (1 - E_{avg}) (1 - F_{avg}(1 - E_{avg}))} +\end{equation}$$ + +Where $E$ is the directional albedo of the specular BRDF $f_r$, with $\fNormal$ set to 1: + +$$\begin{equation} +E(l) = \int_{\Omega} f(l,v) (\NoV) dv +\end{equation}$$ + +The term $E_{avg}$ is the cosine-weighted average of $E$: + +$$\begin{equation} +E_{avg} = 2 \int_0^1 E(\mu) \mu d\mu +\end{equation}$$ + +Similarly, $F_{avg}$ is the cosine-weighted average of the Fresnel term: + +$$\begin{equation} +F_{avg} = 2 \int_0^1 F(\mu) \mu d\mu +\end{equation}$$ + +Both terms $E$ and $E_{avg}$ can be precomputed and stored in lookup tables. while $F_{avg}$ can be greatly simplified when the Schlick approximation is used: + +$$\begin{equation}\label{averageFresnel} +F_{avg} = \frac{1 + 20 \fNormal}{21} +\end{equation}$$ + +This new lobe is combined with the original single scattering lobe, previously noted $f_r$: + +$$\begin{equation} +f_{r}(l,v) = f_{ss}(l,v) + f_{ms}(l,v) +\end{equation}$$ + +In [#Lagarde18], with credit to Emmanuel Turquin, Lagarde and Golubev make the observation that equation $\ref{averageFresnel}$ can be simplified to $\fNormal$. They also propose to apply energy compensation by adding a scaled GGX specular lobe: + +$$\begin{equation}\label{energyCompensation} +f_{ms}(l,v) = \fNormal \frac{1 - E(l)}{E(l)} f_{ss}(l,v) +\end{equation}$$ + +The key insight is that $E(l)$ can not only be precomputed but also shared with image-based lighting pre-integration. The multiscattering energy compensation formula thus becomes: + +$$\begin{equation}\label{scaledEnergyCompensationLobe} +f_r(l,v) = f_{ss}(l,v) + \fNormal \left( \frac{1}{r} - 1 \right) f_{ss}(l,v) +\end{equation}$$ + +Where $r$ is defined as: + +$$\begin{equation} +r = \int_{\Omega} D(l,v) V(l,v) \left< \NoL \right> dl +\end{equation}$$ + +We can implement specular energy compensation at a negligible cost if we store $r$ in the DFG lookup table presented in section [Image based lights]. Listing [energyCompensationImpl] shows that the implementation is a direct conversion of equation $\ref{scaledEnergyCompensationLobe}$. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +vec3 energyCompensation = 1.0 + f0 * (1.0 / dfg.y - 1.0); +// Scale the specular lobe to account for multiscattering +Fr *= pixel.energyCompensation; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [energyCompensationImpl]: Implementation of the energy compensation specular lobe] + +Please refer to section [Image based lights] and section [Pre-integration for multiscattering] to learn how the DFG lookup table is derived and computed. + +## Parameterization + +Disney's material model described in [#Burley12] is a good starting point but its numerous parameters makes it impractical for real-time implementations. In addition, we would like our standard material model to be easy to understand and easy to use for both artists and developers. + +### Standard parameters + +Table [standardParameters] describes the list of parameters that satisfy our constraints. + + + Parameter | Definition +---------------------:|:--------------------- +**BaseColor** | Diffuse albedo for non-metallic surfaces, and specular color for metallic surfaces +**Metallic** | Whether a surface appears to be dielectric (0.0) or conductor (1.0). Often used as a binary value (0 or 1) +**Roughness** | Perceived smoothness (0.0) or roughness (1.0) of a surface. Smooth surfaces exhibit sharp reflections +**Reflectance** | Fresnel reflectance at normal incidence for dielectric surfaces. This replaces an explicit index of refraction +**Emissive** | Additional diffuse albedo to simulate emissive surfaces (such as neons, etc.) This parameter is mostly useful in an HDR pipeline with a bloom pass +**Ambient occlusion** | Defines how much of the ambient light is accessible to a surface point. It is a per-pixel shadowing factor between 0.0 and 1.0. This parameter will be discussed in more details in the lighting section +[Table [standardParameters]: Parameters of the standard model] + +Figure [material_parameters] shows how the metallic, roughness and reflectance parameters affect the appearance of a surface. + +![Figure [material_parameters]: From top to bottom: varying metallic, varying dielectric roughness, varying metallic roughness, varying reflectance](images/material_parameters.png) + +### Types and ranges + +It is important to understand the type and range of the different parameters of our material model, described in table [standardParametersTypes]. + + + Parameter | Type and range +---------------------:|:--------------------- +**BaseColor** | Linear RGB [0..1] +**Metallic** | Scalar [0..1] +**Roughness** | Scalar [0..1] +**Reflectance** | Scalar [0..1] +**Emissive** | Linear RGB [0..1] + exposure compensation +**Ambient occlusion** | Scalar [0..1] +[Table [standardParametersTypes]: Range and type of the standard model's parameters] + +Note that the types and ranges described here are what the shader will expect. The API and/or tools UI could and should allow to specify the parameters using other types and ranges when they are more intuitive for artists. + +For instance, the base color could be expressed in sRGB space and converted to linear space before being sent off to the shader. It can also be useful for artists to express the metallic, roughness and reflectance parameters as gray values between 0 and 255 (black to white). + +Another example: the emissive parameter could be expressed as a color temperature and an intensity, to simulate the light emitted by a black body. + +### Remapping + +To make the standard material model easier and more intuitive to use for artists, we must remap the parameters _baseColor_, _roughness_ and _reflectance_. + +#### Base color remapping + +The base color of a material is affected by the "metallicness" of said material. Dielectrics have achromatic specular reflectance but retain their base color as the diffuse color. Conductors on the other hand use their base color as the specular color and do not have a diffuse component. + +The lighting equations must therefore use the diffuse color and $\fNormal$ instead of the base color. The diffuse color can easily be computed from the base color, as show in listing [baseColorToDiffuse]. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +vec3 diffuseColor = (1.0 - metallic) * baseColor.rgb; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [baseColorToDiffuse]: Conversion of base color to diffuse in GLSL] + +#### Reflectance remapping + +**Dielectrics** + +The Fresnel term relies on $\fNormal$, the specular reflectance at normal incidence angle, and is achromatic for dielectrics. We will use the remapping for dielectric surfaces described in [#Lagarde14] : + +$$\begin{equation} +\fNormal = 0.16 \cdot reflectance^2 +\end{equation}$$ + +The goal is to map $\fNormal$ onto a range that can represent the Fresnel values of both common dielectric surfaces (4% reflectance) and gemstones (8% to 16%). The mapping function is chosen to yield a 4% Fresnel reflectance value for an input reflectance of 0.5 (or 128 on a linear RGB gray scale). Figure [reflectance] show those common values and how they relate to the mapping function. + +![Figure [reflectance]: Common reflectance values](images/diagram_reflectance.png) + +If the index of refraction is known (for instance, an air-water interface has an IOR of 1.33), the Fresnel reflectance can be calculated as follows: + +$$\begin{equation}\label{fresnelEquation} +\fNormal(n_{ior}) = \frac{(\nior - 1)^2}{(\nior + 1)^2} +\end{equation}$$ + +And if the reflectance value is known, we can compute the corresponding IOR: + +$$\begin{equation} +n_{ior} = \frac{2}{1 - \sqrt{\fNormal}} - 1 +\end{equation}$$ + +Table [commonMatReflectance] describes acceptable Fresnel reflectance values for various types of materials (no real world material has a value under 2%). + + + Material | Reflectance | IOR | Linear value +--------------------------:|:-----------------|:-----------------|:---------------- +Water | 2% | 1.33 | 0.35 +Fabric | 4% to 5.6% | 1.5 to 1.62 | 0.5 to 0.59 +Common liquids | 2% to 4% | 1.33 to 1.5 | 0.35 to 0.5 +Common gemstones | 5% to 16% | 1.58 to 2.33 | 0.56 to 1.0 +Plastics, glass | 4% to 5% | 1.5 to 1.58 | 0.5 to 0.56 +Other dielectric materials | 2% to 5% | 1.33 to 1.58 | 0.35 to 0.56 +Eyes | 2.5% | 1.38 | 0.39 +Skin | 2.8% | 1.4 | 0.42 +Hair | 4.6% | 1.55 | 0.54 +Teeth | 5.8% | 1.63 | 0.6 +Default value | 4% | 1.5 | 0.5 +[Table [commonMatReflectance]: Reflectance of common materials (source: Real-Time Rendering 4th Edition)] + +Table [fNormalMetals] lists the $\fNormal$ values for a few metals. The values are given in sRGB and must be used as the base color in our material model. Please refer to the annex, section [Specular color], for an explanation of how these sRGB colors are computed from measured data. + + + Metal | $\fNormal$ in sRGB | Hexadecimal | Color +----------:|:-------------------:|:------------:|------------------------------------------------------- +Silver | 0.97, 0.96, 0.91 | #f7f4e8 |
 
+Aluminum | 0.91, 0.92, 0.92 | #e8eaea |
 
+Titanium | 0.76, 0.73, 0.69 | #c1baaf |
 
+Iron | 0.77, 0.78, 0.78 | #c4c6c6 |
 
+Platinum | 0.83, 0.81, 0.78 | #d3cec6 |
 
+Gold | 1.00, 0.85, 0.57 | #ffd891 |
 
+Brass | 0.98, 0.90, 0.59 | #f9e596 |
 
+Copper | 0.97, 0.74, 0.62 | #f7bc9e |
 
+[Table [fNormalMetals]: $\fNormal$ for common metals] + +All materials have a Fresnel reflectance of 100% at grazing angles so we will set $\fGrazing$ in the following way when evaluating the specular BRDF $\fSpecular$: + +$$\begin{equation} +\fGrazing = 1.0 +\end{equation}$$ + +Figure [grazing_reflectance] shows a red plastic ball. If you look closely at the edges of the sphere, you will be able to notice the achromatic specular reflectance at grazing angles. + +![Figure [grazing_reflectance]: The specular reflectance becomes achromatic at grazing angles](images/material_grazing_reflectance.png) + +**Conductors** + +The specular reflectance of metallic surfaces is chromatic: + +$$\begin{equation} +\fNormal = baseColor \cdot metallic +\end{equation}$$ + +Listing [fNormal] shows how $\fNormal$ is computed for both dielectric and metallic materials. It shows that the color of the specular reflectance is derived from the base color in the metallic case. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +vec3 f0 = 0.16 * reflectance * reflectance * (1.0 - metallic) + baseColor * metallic; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [fNormal]: Computing $\fNormal$ for dielectric and metallic materials in GLSL] + +#### Roughness remapping and clamping + +The roughness set by the user, called `perceptualRoughness` here, is remapped to a perceptually linear range using the following formulation: + +$$\begin{equation} +\alpha = perceptualRoughness^2 +\end{equation}$$ + +Figure [roughness_remap] shows a silver metallic surface with increasing roughness (from 0.0 to 1.0), using the unmodified roughness value (bottom) and the remapped value (top). + +![Figure [roughness_remap]: Roughness remapping comparison: perceptually linear roughness (top) and roughness (bottom)](images/material_roughness_remap.png) + +Using this visual comparison, it is obvious that the remapped roughness is easier to understand by artists and developers. Without this remapping, shiny metallic surfaces would have to be confined to a very small range between 0.0 and 0.05. + +Brent Burley made similar observations in his presentation [#Burley12]. After experimenting with other remappings (cubic and quadratic mappings for instance), we have reached the conclusion that this simple square remapping delivers visually pleasing and intuitive results while being cheap for real-time applications. + +Last but not least, it is important to note that the roughness parameters is used in various computations at runtime where limited floating point precision can become an issue. For instance, _mediump_ precision floats are often implemented as half-floats (fp16) on mobile GPUs. + +This cause problems when computing small values like $\frac{1}{perceptualRoughness^4}$ in our lighting equations (roughness squared in the GGX computation). The smallest value that can be represented as a half-float is $2^{-14}$ or $6.1 \times 10^{-5}$. To avoid divisions by 0 on devices that do not support denormals, the result of $\frac{1}{roughness^4}$ must therefore not be lower than $6.1 \times 10^{-5}$. To do so, we must clamp the roughness to 0.089, which gives us $6.274 \times 10^{-5}$. + +Denormals should also be avoided to prevent performance drops. The roughness can also not be set to 0 to avoid obvious divisions by 0. + +Since we also want specular highlights to have a minimum size (a roughness close to 0 creates almost invisible highlights), we should clamp the roughness to a safe range in the shader. This clamping has the added benefit of correcting specular aliasing[^frostbiteRoughnessClamp] that can appear for low roughness values. + +[^frostbiteRoughnessClamp]: The Frostbite engine clamps the roughness of analytical lights to 0.045 to reduce specular aliasing. This is possible when using single precision floats (fp32). + +### Blending and layering + +As noted in [#Burley12] and [#Neubelt13], this model allows for robust blending between different materials by simply interpolating the different parameters. In particular, this allows to layer different materials using simple masks. + +For instance, figure [materialBlending] shows how the studio Ready at Dawn used material blending and layering in _The Order: 1886_ to create complex appearances from a library of simple materials (gold, copper, wood, rust, etc.). + +![Figure [materialBlending]: Material blending and layering. Source: Ready at Dawn Studios](images/material_blending.png) + +The blending and layering of materials is effectively an interpolation of the various parameters of the material model. Figure [material_interpolation] show an interpolation between shiny metallic chrome and rough red plastic. While the intermediate blended materials make little physical sense, they look plausible. + +![Figure [material_interpolation]: Interpolation from shiny chrome (left) to rough red plastic (right)](images/material_interpolation.png) + +### Crafting physically based materials + +Designing physically based materials is fairly easy once you understand the nature of the four main parameters: base color, metallic, roughness and reflectance. + +We provide a [useful chart/reference guide](./Material%20Properties.pdf) to help artists and developers craft their own physically based materials. + +![Crafting physically based materials](images/material_chart.jpg) + +In addition, here is a quick summary of how to use our material model: + +All materials +: **Base color** should be devoid of lighting information, except for micro-occlusion. + + **Metallic** is almost a binary value. Pure conductors have a metallic value of 1 and pure dielectrics have a metallic value of 0. You should try to use values close at or close to 0 and 1. Intermediate values are meant for transitions between surface types (metal to rust for instance). + +Non-metallic materials +: **Base color** represents the reflected color and should be an sRGB value in the range 50-240 (strict range) or 30-240 (tolerant range). + + **Metallic** should be 0 or close to 0. + + **Reflectance** should be set to 127 sRGB (0.5 linear, 4% reflectance) if you cannot find a proper value. Do not use values under 90 sRGB (0.35 linear, 2% reflectance). + +Metallic materials +: **Base color** represents both the specular color and reflectance. Use values with a luminosity of 67% to 100% (170-255 sRGB). Oxidized or dirty metals should use a lower luminosity than clean metals to take into account the non-metallic components. + + **Metallic** should be 1 or close to 1. + + **Reflectance** is ignored (calculated from the base color). + +## Clear coat model + +The standard material model described previously is a good fit for isotropic surfaces made of a single layer. Multi-layer materials are unfortunately fairly common, particularly materials with a thin translucent layer over a standard layer. Real world examples of such materials include car paints, soda cans, lacquered wood, acrylic, etc. + +![Figure [materialClearCoat]: Comparison of a blue metallic surface under the standard material model (left) and the clear coat model (right)](images/material_clear_coat.png) + +A clear coat layer can be simulated as an extension of the standard material model by adding a second specular lobe, which implies evaluating a second specular BRDF. To simplify the implementation and parameterization, the clear coat layer will always be isotropic and dielectric. The base layer can be anything allowed by the standard model (dielectric or conductor). + +Since incoming light will traverse the clear coat layer, we must also take the loss of energy into account as shown in figure [clearCoatModel]. Our model will however not simulate inter reflection and refraction behaviors. + +![Figure [clearCoatModel]: Clear coat surface model](images/diagram_clear_coat.png) + +### Clear coat specular BRDF + +The clear coat layer will be modeled using the same Cook-Torrance microfacet BRDF used in the standard model. Since the clear coat layer is always isotropic and dielectric, with low roughness values (see section [Clear coat parameterization]), we can choose cheaper DFG terms without notably sacrificing visual quality. + +A survey of the terms listed in [#Karis13a] and [#Burley12] shows that the Fresnel and NDF terms we already use in the standard model are not computationally more expensive than other terms. [#Kelemen01] describes a much simpler term that can replace our Smith-GGX visibility term: + +$$\begin{equation} +V(l,h) = \frac{1}{4(\LoH)^2} +\end{equation}$$ + +This masking-shadowing function is not physically based, as shown in [#Heitz14], but its simplicity makes it desirable for real-time rendering. + +In summary, our clear coat BRDF is a Cook-Torrance specular microfacet model, with a GGX normal distribution function, a Kelemen visibility function, and a Schlick Fresnel function. Listing [kelemen] shows how trivial the GLSL implementation is. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +float V_Kelemen(float LoH) { + return 0.25 / (LoH * LoH); +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [kelemen]: Implementation of the Kelemen visibility term in GLSL] + +**Note on the Fresnel term** + +The Fresnel term of the specular BRDF requires $\fNormal$, the specular reflectance at normal incidence angle. This parameter can be computed from an index of refraction of an interface. We will assume that our clear coat layer is made of polyurethane, a common compound [used in coatings and varnishes](https://en.wikipedia.org/wiki/List_of_polyurethane_applications#Varnish), or similar. An air-polyurethane interface [has an IOR of 1.5](http://www.clearpur.com/transparent-polyurethanes/), from which we can deduce $\fNormal$: + +$$\begin{equation} +\fNormal(1.5) = \frac{(1.5 - 1)^2}{(1.5 + 1)^2} = 0.04 +\end{equation}$$ + +This corresponds to a Fresnel reflectance of 4% that we know is associated with common dielectric materials. + +### Integration in the surface response + +Because we must take into account the loss of energy caused by the addition of the clear coat layer, we can reformulate the BRDF from equation $\ref{brdf}$ thusly: + +$$\begin{equation} +f(v,l)=\fDiffuse(v,l) (1 - F_c) + \fSpecular(v,l) (1 - F_c) + f_c(v,l) +\end{equation}$$ + +Where $F_c$ is the Fresnel term of the clear coat BRDF and $f_c$ the clear coat BRDF + +### Clear coat parameterization + +The clear coat material model encompasses all the parameters previously defined for the standard material mode, plus two parameters described in table [clearCoatParameters]. + + + Parameter | Definition +----------------------:|:--------------------- +**ClearCoat** | Strength of the clear coat layer. Scalar between 0 and 1 +**ClearCoatRoughness** | Perceived smoothness or roughness of the clear coat layer. Scalar between 0 and 1 +[Table [clearCoatParameters]: Clear coat model parameters] + +The clear coat roughness parameter is remapped and clamped in a similar way to the roughness parameter of the standard material. + +Figure [clearCoat] and figure [clearCoatRoughness] show how the clear coat parameters affect the appearance of a surface. + +![Figure [clearCoat]: Clear coat varying from 0.0 (left) to 1.0 (right) with metallic set to 1.0 and roughness to 0.8](images/material_clear_coat1.png) + +![Figure [clearCoatRoughness]: Clear coat roughness varying from 0.0 (left) to 1.0 (right) with metallic set to 1.0, roughness to 0.8 and clear coat to 1.0](images/material_clear_coat2.png) + +Listing [clearCoatBRDF] shows the GLSL implementation of the clear coat material model after remapping, parameterization and integration in the standard surface response. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +void BRDF(...) { + // compute Fd and Fr from standard model + + // remapping and linearization of clear coat roughness + clearCoatPerceptualRoughness = clamp(clearCoatPerceptualRoughness, 0.089, 1.0); + clearCoatRoughness = clearCoatPerceptualRoughness * clearCoatPerceptualRoughness; + + // clear coat BRDF + float Dc = D_GGX(clearCoatRoughness, NoH); + float Vc = V_Kelemen(clearCoatRoughness, LoH); + float Fc = F_Schlick(0.04, LoH) * clearCoat; // clear coat strength + float Frc = (Dc * Vc) * Fc; + + // account for energy loss in the base layer + return color * ((Fd + Fr * (1.0 - Fc)) * (1.0 - Fc) + Frc); +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [clearCoatBRDF]: Implementation of the clear coat BRDF in GLSL] + +### Base layer modification + +The presence of a clear coat layer means that we should recompute $\fNormal$, since it is normally based on an air-material interface. The base layer thus requires $\fNormal$ to be computed based on a clear coat-material interface instead. + +This can be achieved by computing the material's index of refraction (IOR) from $\fNormal$, then computing a new $\fNormal$ based on the newly computed IOR and the IOR of the clear coat layer (1.5). + +First, we compute the base layer's IOR: + +$$ +IOR_{base} = \frac{1 + \sqrt{\fNormal}}{1 - \sqrt{\fNormal}} +$$ + +Then we compute the new $\fNormal$ from this new index of refraction: + +$$ +f_{0_{base}} = \left( \frac{IOR_{base} - 1.5}{IOR_{base} + 1.5} \right) ^2 +$$ + +Since the clear coat layer's IOR is fixed, we can combine both steps to simplify: + +$$ +f_{0_{base}} = \frac{\left( 1 - 5 \sqrt{\fNormal} \right) ^2}{\left( 5 - \sqrt{\fNormal} \right) ^2} +$$ + +We should also modify the base layer's apparent roughness based on the IOR of the clear coat layer but this is something we have opted to leave out for now. + +## Anisotropic model + +The standard material model described previously can only describe isotropic surfaces, that is, surfaces whose properties are identical in all directions. Many real-world materials, such as brushed metal, can, however, only be replicated using an anisotropic model. + +![Figure [anisotropic]: Comparison of isotropic material (left) and anisotropic material (right)](images/material_anisotropic.png) + +### Anisotropic specular BRDF + +The isotropic specular BRDF described previously can be modified to handle anisotropic materials. Burley achieves this by using an anisotropic GGX NDF: + +$$\begin{equation} +D_{aniso}(h,\alpha) = \frac{1}{\pi \alpha_t \alpha_b} \frac{1}{((\frac{t \cdot h}{\alpha_t})^2 + (\frac{b \cdot h}{\alpha_b})^2 + (\NoH)^2)^2} +\end{equation}$$ + +This NDF unfortunately relies on two supplemental roughness terms noted $\alpha_b$, the roughness along the bitangent direction, and $\alpha_t$, the roughness along the tangent direction. Neubelt and Pettineo [#Neubelt13] propose a way to derive $\alpha_b$ from $\alpha_t$ by using an _anisotropy_ parameter that describes the relationship between the two roughness values for a material: + +$$ +\begin{align*} + \alpha_t &= \alpha \\ + \alpha_b &= lerp(0, \alpha, 1 - anisotropy) +\end{align*} +$$ + +The relationship defined in [#Burley12] is different, offers more pleasant and intuitive results, but is slightly more expensive: + +$$ +\begin{align*} + \alpha_t &= \frac{\alpha}{\sqrt{1 - 0.9 \times anisotropy}} \\ + \alpha_b &= \alpha \sqrt{1 - 0.9 \times anisotropy} +\end{align*} +$$ + +We instead opted to follow the relationship described in [#Kulla17] as it allows creation of sharp highlights: + +$$ +\begin{align*} + \alpha_t &= \alpha \times (1 + anisotropy) \\ + \alpha_b &= \alpha \times (1 - anisotropy) +\end{align*} +$$ + +Note that this NDF requires the tangent and bitangent directions in addition to the normal direction. Since these directions are already needed for normal mapping, providing them may not be an issue. + +The resulting implementation is described in listing [anisotropicBRDF]. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +float at = max(roughness * (1.0 + anisotropy), 0.001); +float ab = max(roughness * (1.0 - anisotropy), 0.001); + +float D_GGX_Anisotropic(float NoH, const vec3 h, + const vec3 t, const vec3 b, float at, float ab) { + float ToH = dot(t, h); + float BoH = dot(b, h); + float a2 = at * ab; + highp vec3 v = vec3(ab * ToH, at * BoH, a2 * NoH); + highp float v2 = dot(v, v); + float w2 = a2 / v2; + return a2 * w2 * w2 * (1.0 / PI); +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [anisotropicBRDF]: Implementation of Burley's anisotropic NDF in GLSL] + +In addition, [#Heitz14] presents an anisotropic masking-shadowing function to match the height-correlated GGX distribution. The masking-shadowing term can be greatly simplified by using the visibility function instead: + +$$\begin{equation} +G(v,l,h,\alpha) = \frac{\chi^+(\VoH) \chi^+(\LoH)}{1 + \Lambda(v) + \Lambda(l)} +\end{equation}$$ + +$$\begin{equation} +\Lambda(m) = \frac{-1 + \sqrt{1 + \alpha_0^2 tan^2(\theta_m)}}{2} = \frac{-1 + \sqrt{1 + \alpha_0^2 \frac{(1 - cos^2(\theta_m))}{cos^2(\theta_m)}}}{2} +\end{equation}$$ + +Where: + +$$\begin{equation} +\alpha_0 = \sqrt{cos^2(\phi_0)\alpha_x^2 + sin^2(\phi_0)\alpha_y^2} +\end{equation}$$ + +After derivation we obtain: + +$$\begin{equation} +V_{aniso}(\NoL,\NoV,\alpha) = \frac{1}{2((\NoL)\hat{\Lambda}_v+(\NoV)\hat{\Lambda}_l)} \\ +\hat{\Lambda}_v = \sqrt{\alpha^2_t(t \cdot v)^2+\alpha^2_b(b \cdot v)^2+(\NoV)^2} \\ +\hat{\Lambda}_l = \sqrt{\alpha^2_t(t \cdot l)^2+\alpha^2_b(b \cdot l)^2+(\NoL)^2} +\end{equation}$$ + +The term $ \hat{\Lambda}_v $ is the same for every light and can be computed only once if needed. The resulting implementation is described in listing [anisotropicV]. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +float at = max(roughness * (1.0 + anisotropy), 0.001); +float ab = max(roughness * (1.0 - anisotropy), 0.001); + +float V_SmithGGXCorrelated_Anisotropic(float at, float ab, float ToV, float BoV, + float ToL, float BoL, float NoV, float NoL) { + float lambdaV = NoL * length(vec3(at * ToV, ab * BoV, NoV)); + float lambdaL = NoV * length(vec3(at * ToL, ab * BoL, NoL)); + float v = 0.5 / (lambdaV + lambdaL); + return saturateMediump(v); +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [anisotropicV]: Implementation of the anisotropic visibility function in GLSL] + +### Anisotropic parameterization + +The anisotropic material model encompasses all the parameters previously defined for the standard material mode, plus an extra parameter described in table [anisotropicParameters]. + + + Parameter | Definition +----------------------:|:--------------------- +**Anisotropy** | Amount of anisotropy. Scalar between -1 and 1 +[Table [anisotropicParameters]: Anisotropic model parameters] + +No further remapping is required. Note that negative values will align the anisotropy with the bitangent direction instead of the tangent direction. Figure [anisotropyParameter] shows how the anisotropy parameter affect the appearance of a rough metallic surface. + +![Figure [anisotropyParameter]: Anisotropy varying from 0.0 (left) to 1.0 (right)](images/materials/anisotropy.png) + +## Subsurface model + +[TODO] + +### Subsurface specular BRDF + +[TODO] + +### Subsurface parameterization + +[TODO] + +## Cloth model + +All the material models described previously are designed to simulate dense surfaces, both at a macro and at a micro level. Clothes and fabrics are however often made of loosely connected threads that absorb and scatter incident light. The microfacet BRDFs presented earlier do a poor job of recreating the nature of cloth due to their underlying assumption that a surface is made of random grooves that behave as perfect mirrors. When compared to hard surfaces, cloth is characterized by a softer specular lobe with a large falloff and the presence of fuzz lighting, caused by forward/backward scattering. Some fabrics also exhibit two-tone specular colors (velvets for instance). + +Figure [materialCloth] shows how a traditional microfacet BRDF fails to capture the appearance of a sample of denim fabric. The surface appears rigid (almost plastic-like), more similar to a tarp than a piece of clothing. This figure also shows how important the softer specular lobe caused by absorption and scattering is to the faithful recreation of the fabric. + +![Figure [materialCloth]: Comparison of denim fabric rendered using a traditional microfacet BRDF (left) and our cloth BRDF (right)](images/screenshot_cloth.png) + +Velvet is an interesting use case for a cloth material model. As shown in figure [materialVelvet] this type of fabric exhibits strong rim lighting due to forward and backward scattering. These scattering events are caused by fibers standing straight at the surface of the fabric. When the incident light comes from the direction opposite to the view direction, the fibers will forward-scatter the light. Similarly, when the incident light from the same direction as the view direction, the fibers will scatter the light backward. + +![Figure [materialVelvet]: Velvet fabric showcasing forward and backward scattering](images/screenshot_cloth_velvet.png) + +Since fibers are flexible, we should in theory model the ability to groom the surface. While our model does not replicate this characteristic, it does model a visible front facing specular contribution that can be attributed to the random variance in the direction of the fibers. + +It is important to note that there are types of fabrics that are still best modeled by hard surface material models. For instance, leather, silk and satin can be recreated using the standard or anisotropic material models. + +### Cloth specular BRDF + +The cloth specular BRDF we use is a modified microfacet BRDF as described by Ashikhmin and Premoze in [#Ashikhmin07]. In their work, Ashikhmin and Premoze note that the distribution term is what contributes most to a BRDF and that the shadowing/masking term is not necessary for their velvet distribution. The distribution term itself is an inverted Gaussian distribution. This helps achieve fuzz lighting (forward and backward scattering) while an offset is added to simulate the front facing specular contribution. The so-called velvet NDF is defined as follows: + +$$\begin{equation} +D_{velvet}(v,h,\alpha) = c_{norm}(1 + 4 exp\left(\frac{-{cot}^2\theta_{h}}{\alpha^2}\right)) +\end{equation}$$ + +This NDF is a variant of the NDF the same authors describe in [#Ashikhmin00], notably modified to include an offset (set to 1 here) and an amplitude (4). In [#Neubelt13], Neubelt and Pettineo propose a normalized version of this NDF: + +$$\begin{equation} +D_{velvet}(v,h,\alpha) = \frac{1}{\pi(1 + 4\alpha^2)} (1 + 4 \frac{exp\left(\frac{-{cot}^2\theta_{h}}{\alpha^2}\right)}{{sin}^4\theta_{h}}) +\end{equation}$$ + +For the full specular BRDF, we also follow [#Neubelt13] and replace the traditional denominator with a smoother variant: + +$$\begin{equation}\label{clothSpecularBRDF} +f_{r}(v,h,\alpha) = \frac{D_{velvet}(v,h,\alpha)}{4(\NoL + \NoV - (\NoL)(\NoV))} +\end{equation}$$ + +The implementation of the velvet NDF is presented in listing [clothBRDF], optimized to properly fit in half float formats and to avoid computing a costly cotangent, relying instead on trigonometric identities. Note that we removed the Fresnel component from this BRDF. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +float D_Ashikhmin(float roughness, float NoH) { + // Ashikhmin 2007, "Distribution-based BRDFs" + float a2 = roughness * roughness; + float cos2h = NoH * NoH; + float sin2h = max(1.0 - cos2h, 0.0078125); // 2^(-14/2), so sin2h^2 > 0 in fp16 + float sin4h = sin2h * sin2h; + float cot2 = -cos2h / (a2 * sin2h); + return 1.0 / (PI * (4.0 * a2 + 1.0) * sin4h) * (4.0 * exp(cot2) + sin4h); +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [clothBRDF]: Implementation of Ashikhmin's velvet NDF in GLSL] + +In [#Estevez17] Estevez and Kulla propose a different NDF (called the "Charlie" sheen) that is based on an exponentiated sinusoidal instead of an inverted Gaussian. This NDF is appealing for several reasons: its parameterization feels more natural and intuitive, it provides a softer appearance and, as shown in equation $\ref{charlieNDF}$, its implementation is simpler: + +$$\begin{equation}\label{charlieNDF} +D(m) = \frac{(2 + \frac{1}{\alpha}) sin(\theta)^{\frac{1}{\alpha}}}{2 \pi} +\end{equation}$$ + +[#Estevez17] also presents a new shadowing term that we omit here because of its cost. We instead rely on the visibility term from [#Neubelt13] (shown in equation $\ref{clothSpecularBRDF}$ above). +The implementation of this NDF is presented in listing [clothCharlieBRDF], optimized to properly fit in half float formats. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +float D_Charlie(float roughness, float NoH) { + // Estevez and Kulla 2017, "Production Friendly Microfacet Sheen BRDF" + float invAlpha = 1.0 / roughness; + float cos2h = NoH * NoH; + float sin2h = max(1.0 - cos2h, 0.0078125); // 2^(-14/2), so sin2h^2 > 0 in fp16 + return (2.0 + invAlpha) * pow(sin2h, invAlpha * 0.5) / (2.0 * PI); +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [clothCharlieBRDF]: Implementation of the "Charlie" NDF in GLSL] + +#### Sheen color + +To offer better control over the appearance of cloth and to give users the ability to recreate two-tone specular materials, we introduce the ability to directly modify the specular reflectance. Figure [materialClothSheen] shows an example of using the parameter we call "sheen color". + +![Figure [materialClothSheen]: Blue fabric without (left) and with (right) sheen](images/screenshot_cloth_sheen.png) + +### Cloth diffuse BRDF + +Our cloth material model still relies on a Lambertian diffuse BRDF. It is however slightly modified to be energy conservative (akin to the energy conservation of our clear coat material model) and offers an optional subsurface scattering term. This extra term is not physically based and can be used to simulate the scattering, partial absorption and re-emission of light in certain types of fabrics. + +First, here is the diffuse term without the optional subsurface scattering: + +$$\begin{equation} +f_{d}(v,h) = \frac{c_{diff}}{\pi}(1 - F(v,h)) +\end{equation}$$ + +Where $F(v,h)$ is the Fresnel term of the cloth specular BRDF in equation $\ref{clothSpecularBRDF}$. In practice we've opted to leave out the $1 - F(v, h)$ term in the diffuse component. The effect is a bit subtle and we deemed it wasn't worth the added cost. + +Subsurface scattering is implemented using the wrapped diffuse lighting technique, in its energy conservative form: + +$$\begin{equation} +f_{d}(v,h) = \frac{c_{diff}}{\pi}(1 - F(v,h)) \left< \frac{\NoL + w}{(1 + w)^2} \right> \left< c_{subsurface} + \NoL \right> +\end{equation}$$ + +Where $w$ is a value between 0 and 1 defining by how much the diffuse light should wrap around the terminator. To avoid introducing another parameter, we fix $w = 0.5$. Note that with wrap diffuse lighting, the diffuse term must not be multiplied by $\NoL$. The effect of this cheap +subsurface scattering approximation can be seen in figure [materialClothSubsurface]. + +![Figure [materialClothSubsurface]: White cloth (left column) vs white cloth with brown subsurface scattering (right)](images/screenshot_cloth_subsurface.png) + +The complete implementation of our cloth BRDF, including sheen color and optional subsurface scattering, can be found in listing [clothFullBRDF]. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// specular BRDF +float D = distributionCloth(roughness, NoH); +float V = visibilityCloth(NoV, NoL); +vec3 F = sheenColor; +vec3 Fr = (D * V) * F; + +// diffuse BRDF +float diffuse = diffuse(roughness, NoV, NoL, LoH); +#if defined(MATERIAL_HAS_SUBSURFACE_COLOR) +// energy conservative wrap diffuse +diffuse *= saturate((dot(n, light.l) + 0.5) / 2.25); +#endif +vec3 Fd = diffuse * pixel.diffuseColor; + +#if defined(MATERIAL_HAS_SUBSURFACE_COLOR) +// cheap subsurface scatter +Fd *= saturate(subsurfaceColor + NoL); +vec3 color = Fd + Fr * NoL; +color *= (lightIntensity * lightAttenuation) * lightColor; +#else +vec3 color = Fd + Fr; +color *= (lightIntensity * lightAttenuation * NoL) * lightColor; +#endif +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [clothFullBRDF]: Implementation of our cloth BRDF in GLSL] + +### Cloth parameterization + +The cloth material model encompasses all the parameters previously defined for the standard material mode except for _metallic_ and _reflectance_. Two extra parameters described in table [clothParameters] are also available. + + + Parameter | Definition +---------------------:|:--------------------- +**SheenColor** | Specular tint to create two-tone specular fabrics (defaults to 0.04 to match the standard reflectance) +**SubsurfaceColor** | Tint for the diffuse color after scattering and absorption through the material +[Table [clothParameters]: Cloth model parameters] + + +To create a velvet-like material, the base color can be set to black (or a dark color). Chromaticity information should instead be set on the sheen color. To create more common fabrics such as denim, cotton, etc. use the base color for chromaticity and use the default sheen color or set the sheen color to the luminance of the base color. + +# Lighting + +The correctness and coherence of the lighting environment is paramount to achieving plausible visuals. After surveying existing rendering engines (such as Unity or Unreal Engine 4) as well as the traditional real-time rendering literature, it is obvious that coherency is rarely achieved. + +The Unreal Engine, for instance, lets artists specify the "brightness" of a point light in lumens, a unit of luminous power. The brightness of directional lights is however expressed using an arbitrary unnamed unit. To match the brightness of a point light with a luminous power of 5,000 lumens, the artist must use a directional light of brightness 10. This kind of mismatch makes it difficult for artists to maintain the visual integrity of a scene when adding, removing or modifying lights. +Using solely arbitrary units is a coherent solution but it makes reusing lighting rigs a difficult task. For instance, an outdoor scene will use a directional light of brightness 10 as the sun and all other lights will be defined relative to that value. Moving these lights to an indoor environment would make them too bright. + +Our goal is therefore to make all lighting correct by default, while giving artists enough freedom to achieve the desired look. We will support a number of lights, split in two categories, direct and indirect lighting: + +**Direct lighting**: punctual lights, photometric lights, area lights. + +**Indirect lighting**: image based lights (IBLs), for both local[^localProbesMobile] and distant light probes. + +[^localProbesMobile]: Local light probes might be too expensive to support on mobile, we will first focus our efforts on distant light probes set at infinity + +## Units + +The following sections will discuss how to implement various types of lights and the proposed equations make use of different symbols and units summarized in table [lightUnits]. + + + Photometric term | Notation | Unit +-----------------------:|:------------------:|:----------------- +Luminous power | $\Phi$ | Lumen ($lm$) +Luminous intensity | $I$ | Candela ($cd$) or $\frac{lm}{sr}$ +Illuminance | $E$ | Lux ($lx$) or $\frac{lm}{m^2}$ +Luminance | $L$ | Nit ($nt$) or $\frac{cd}{m^2}$ +Radiant power | $\Phi_e$ | Watt ($W$) +Luminous efficacy | $\eta$ | Lumens per watt ($\frac{lm}{W}$) +Luminous efficiency | $V$ | Percentage (%) +[Table [lightUnits]: Photometric units] + +To get properly coherent lighting, we must use light units that respect the ratio between various light intensities found in real-world scenes. These intensities can vary greatly, from around 800 $lm$ for a household light bulb to 120,000 $lx$ for a daylight sky and sun illumination. + +The easiest way to achieve lighting coherency is to adopt physical light units. This will in turn enable full reusability of lighting rigs. Using physical light units also allows us to use a physically based camera. + +Table [lightTypesUnits] shows the light unit associated with each type of light we intend to support. + + + Light type | Unit +------------------------:|:--------------------- +Directional light | Illuminance ($lx$ or $\frac{lm}{m^2}$) +Point light | Luminous power ($lm$) +Spot light | Luminous power ($lm$) +Photometric light | Luminous intensity ($cd$) +Masked photometric light | Luminous power ($lm$) +Area light | Luminous power ($lm$) +Image based light | Luminance ($\frac{cd}{m^2}$) +[Table [lightTypesUnits]: Intensity unity for each light type] + +**Notes about the radiant power unit** + +Even though commercially available light bulbs often display their brightness in lumens on the packaging, it is common to refer to the brightness of a light bulb by using its required energy in watts. The number of watts only indicates how much energy a bulb uses, not how bright it is. It is even more important to understand this difference now that more energy efficient bulbs are readily available (halogens, LEDs, etc.). + +However, since artists might be accustomed to gauging a light's brightness by its power, we should allow users to use the power unit to define the brightness of a light. The conversion is presented in equation $\ref{radiantPowerToLuminousPower}$. + +$$\begin{equation}\label{radiantPowerToLuminousPower} +\Phi = \Phi_e \eta +\end{equation}$$ + +In equation $\ref{radiantPowerToLuminousPower}$, $\eta$ is the luminous efficacy of the light, expressed in lumens per watt. Knowing that the [maximum possible luminous efficacy](http://en.wikipedia.org/wiki/Luminous_efficacy) is 683 $\frac{lm}{W}$ we can also use luminous efficiency $V$ (also called luminous coefficient), as shown in equation $\ref{radiantPowerLuminousEfficiency}$. + +$$\begin{equation}\label{radiantPowerLuminousEfficiency} +\Phi = \Phi_e 683 \times V +\end{equation}$$ + +Table [lightTypesEfficacy] can be used as a reference to convert watts to lumens using either the luminous efficacy or the luminous efficiency of various types of lights. More specific values are available on Wikipedia's [luminous efficacy](http://en.wikipedia.org/wiki/Luminous_efficacy) page. + + + Light type | Efficacy $\eta$ | Efficiency $V$ +-----------------------:|:------------------:|:----------------- +Incandescent | 14-35 | 2-5% +LED | 28-100 | 4-15% +Fluorescent | 60-100 | 9-15% +[Table [lightTypesEfficacy]: Efficacy and efficiency of various light types] + +### Light units validation + +One of the big advantages of using physical light units is the ability to physically validate our equations. We can use specialized devices to measure three light units. + +#### Illuminance + +The illuminance reaching a surface can be measured using an incident light meter. For our tests, we use a [Sekonic L-478D](http://www.sekonic.com/products/l-478d/overview.aspx), shown in figure [sekonic]. + +The incident light meter uses a white diffuse dome to capture the illuminance reaching a surface. It is important to orient the dome properly depending on the desired measurement. For instance, orienting the dome perpendicular to the sun on a bright clear day will give very different results than orienting the dome horizontally. + +![Figure [sekonic]: Sekonic L-478D incident light meter](images/photo_light_meter.jpg) + +#### Luminance + +The luminance at a surface, or the product of the incident light and the surface, can be measured using a luminance meter, also often called a spot meter. While incident light meters use a diffuse hemisphere to capture light from all directions, a spot meter uses a shield to measure incident light from a single direction. For our tests, we use a [Sekonic 5 degree Viewfinder](http://www.sekonic.com/products/l-478dr/accessories/np-finder-5-degree-for-l-478.aspx) that can replace the diffuser on the L-478D to measure luminance in a 5 degree cone. + +![Sekonic L-478D working as a luminance meter using a special viewfinder](images/photo_incident_light_meter.jpg) + +#### Luminous intensity + +The luminous intensity of a light source cannot be measured directly but can be derived from the measured illuminance if we know the distance between the measuring device and the light source. Equation $\ref{derivedLuminousIntensity}$ is a simple application of the inverse square law discussed in section [Punctual lights]. + +$$\begin{equation}\label{derivedLuminousIntensity} +I = E \cdot d^2 +\end{equation}$$ + +## Direct lighting + +We have defined the light units for all the light types supported by the renderer in the section above but we have not defined the light unit for the result of the lighting equations. Choosing physical light units means that we will compute luminance values in our shaders, and therefore that all our light evaluation functions will compute the luminance $L_{out}$ (or outgoing radiance) at any given point. The luminance depends on the illuminance $E$ and the BSDF $f(v,l)$ : + +$$\begin{equation}\label{luminanceEquation} +L_{out} = f(v,l)E +\end{equation}$$ + +### Directional lights + +The main purpose of directional lights is to recreate important light sources for outdoor environment, i.e. the sun and/or the moon. While directional lights do not truly exist in the physical world, any light source sufficiently far from the light receptor can be assumed to be directional (i.e. all the incident light rays are parallel, as shown in figure [directionalLight]). + +![Figure [directionalLight]: Interaction between a directional light and a surface. The light source is a virtual construct that can only be represented by a direction](images/diagram_directional_light.png) + +This approximation proves to work incredibly well for the diffuse response of a surface but the specular response is incorrect. The Frostbite engine solves this problem by treating the "sun" directional light as a disc area light. However, our tests have shown that the quality increase does not justify the added computational costs. + +We earlier stated that we chose an illuminance light unit ($lx$) for directional lights. This is in part due to the fact that we can easily find illuminance values for the sky and the sun (online or with a light meter) but also to simplify the luminance equation described in $\ref{luminanceEquation}$. + +$$\begin{equation}\label{directionalLuminanceEquation} +L_{out} = f(v,l) E_{\bot} \left< \NoL \right> +\end{equation}$$ + +In the simplified luminance equation $\ref{directionalLuminanceEquation}$, $E_{\bot}$ is the illuminance of the light source for a surface perpendicular to said light source. If the directional light source simulates the sun, $E_{\bot}$ is the illuminance of the sun for a surface perpendicular to the sun direction. + +Table [sunSkyIlluminance] provides useful reference values for the sun and sky illumination, measured[^illuminanceMeasures] on a clear day in March, in California. + + + Light | 10am | 12pm | 5:30pm +--------------------------:|---------:|---------:|---------: +$Sky_{\bot} + Sun_{\bot}$ | 120,000 | 130,000 | 90,000 +$Sky_{\bot}$ | 20,000 | 25,000 | 9,000 +$Sun_{\bot}$ | 100,000 | 105,000 | 81,000 +[Table [sunSkyIlluminance]: Illuminance values in $lx$ (a full moon has an illuminance of 1 $lx$)] + +Dynamic directional lights are particularly cheap to evaluate at runtime, as shown in listing [glslDirectionalLight]. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +vec3 l = normalize(-lightDirection); +float NoL = clamp(dot(n, l), 0.0, 1.0); + +// lightIntensity is the illuminance +// at perpendicular incidence in lux +float illuminance = lightIntensity * NoL; +vec3 luminance = BSDF(v, l) * illuminance; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [glslDirectionalLight]: Implementation of directional lights in GLSL] + +Figure [directionalLightTest] shows the effect of lighting a simple scene with a directional light setup to approximate a midday Sun (illuminance set to 110,000 $lx$). For illustration purposes, only direct lighting is shown. + +![Figure [directionalLightTest]: Series of dielectric materials of varying roughness under a directional light](images/screenshot_directional_light.png) + +[^illuminanceMeasures]: Measurements taken with an incident light meter (Sekonic L-478D) + +### Punctual lights + +Our engine will support two types of punctual lights, commonly found in most if not all rendering engines: point lights and spot lights. These types of lights are traditionally physically inaccurate for two reasons: + +1. They are truly punctual and infinitesimally small. +2. They do not follow the [inverse square law](http://en.wikipedia.org/wiki/Inverse-square_law). + +The first issue can be addressed with area lights but, given the cheaper nature of punctual lights it is deemed practical to use infinitesimally small punctual lights whenever possible. + +The second issue is easy to fix. For a given punctual light, the perceived intensity decreases proportionally to the square of the distance from the viewer (more precisely, the light receptor). + +For punctual lights following the inverse square law, the term $E$ of equation $ \ref{luminanceEquation} $ is expressed in equation $\ref{punctualLightEquation}$, where $d$ is the distance from a point at the surface to the light. + +$$\begin{equation}\label{punctualLightEquation} +E = L_{in} \left< \NoL \right> = \frac{I}{d^2} \left< \NoL \right> +\end{equation}$$ + +The difference between point and spot lights lies in how $E$ is computed, and in particular how the luminous intensity $I$ is computed from the luminous power $\Phi$. + +#### Point lights + +A point light is defined only by a position in space, as shown in figure [pointLight]. + +![Figure [pointLight]: Interaction between a point light and a surface. The attenuation only depends on the distance to the light](images/diagram_point_light.png) + +The luminous power of a point light is calculated by integrating the luminous intensity over the light's solid angle, as show in equation $\ref{pointLightLuminousPower}$. The luminous intensity can then be easily derived from the luminous power. + +$$\begin{equation}\label{pointLightLuminousPower} +\Phi = \int_{\Omega} I dl = \int_{0}^{2\pi} \int_{0}^{\pi} I d\theta d\phi = 4 \pi I \\ +I = \frac{\Phi}{4 \pi} +\end{equation}$$ + +By simple substitution of $I$ in $\ref{punctualLightEquation}$ and $E$ in $ \ref{luminanceEquation} $ we can formulate the luminance equation of a point light as a function of the luminous power (see $ \ref{pointLightLuminanceEquation} $). + +$$\begin{equation}\label{pointLightLuminanceEquation} +L_{out} = f(v,l) \frac{\Phi}{4 \pi d^2} \left< \NoL \right> +\end{equation}$$ + +Figure [pointLightTest] shows the effect of lighting a simple scene with a point light subject to distance attenuation. Light falloff is exaggerated for illustration purposes. + +![Figure [pointLightTest]: Inverse square law applied to point lights evaluation](images/screenshot_point_light.png) + +#### Spot lights + +A spot light is defined by a position in space, a direction vector and two cone angles, $ \theta_{inner} $ and $ \theta_{outer} $ (see figure [spotLight]). These two angles are used to define the angular falloff attenuation of the spot light. The light evaluation function of a spot light must therefore take into account both the inverse square law and these two angles to properly evaluate the luminance attenuation. + +![Figure [spotLight]: Interaction between a spot light and a surface. The attenuation depends on the distance to the light and the angle between the surface the spot light's direction vector](images/diagram_spot_light.png) + +Equation $ \ref{spotLightLuminousPower} $ describes how the luminous power of a spot light can be calculated in a similar fashion to point lights, using $ \theta_{outer} $ the outer angle of the spot light's cone in the range [0..$\pi$]. + +$$\begin{equation}\label{spotLightLuminousPower} +\Phi = \int_{\Omega} I dl = \int_{0}^{2\pi} \int_{0}^{\theta_{outer}} I d\theta d\phi = 2 \pi (1 - cos\frac{\theta_{outer}}{2})I \\ +I = \frac{\Phi}{2 \pi (1 - cos\frac{\theta_{outer}}{2})} +\end{equation}$$ + +While this formulation is physically correct, it makes spot lights a little difficult to use: changing the outer angle of the cone changes the illumination levels. Figure [spotLightTestFocused] shows the same scene lit by a spot light, with an outer angle of 55 degrees and an outer angle of 15 degrees. Observes how the illumination level increases as the cone aperture decreases. + +![Figure [spotLightTestFocused]: Comparison of spot light outer angles, 55 degrees (left) and 15 degrees (right)](images/screenshot_spot_light_focused.png) + +The coupling of illumination and the outer cone means that an artist cannot tweak the influence cone of a spot light without also changing the perceived illumination. It therefore makes sense to provide artists with a parameter to disable this coupling. Equations $ \ref{spotLightLuminousPowerB} $ shows how to formulate the luminous power for that purpose. + +$$\begin{equation}\label{spotLightLuminousPowerB} +\Phi = \pi I \\ +I = \frac{\Phi}{\pi} \\ +\end{equation}$$ + +With this new formulation to compute the luminous intensity, the test scene in figure [spotLightTest] exhibits similar illumination levels with both cone apertures. + +![Figure [spotLightTest]: Comparison of spot light outer angles, 55 degrees (left) and 15 degrees (right)](images/screenshot_spot_light.png) + +This new formulation can also be considered physically based if the spot's reflector is replaced with a matte, diffuse mask that absorbs light perfectly. + +The spot light evaluation function can be expressed in two ways: + +- **With a light absorber** + $$\begin{equation}\label{spotAbsorber} + L_{out} = f(v,l) \frac{\Phi}{\pi d^2} \left< \NoL \right> \lambda(l) + \end{equation}$$ +- **With a light reflector** + $$\begin{equation}\label{spotReflector} + L_{out} = f(v,l) \frac{\Phi}{2 \pi (1 - cos\frac{\theta_{outer}}{2}) d^2} \left< \NoL \right> \lambda(l) + \end{equation}$$ + +The term $ \lambda(l) $ in equations $ \ref{spotAbsorber} $ and $ \ref{spotReflector} $ is the spot's angle attenuation factor described in equation + $ \ref{spotAngleAtt} $ below. + +$$\begin{equation}\label{spotAngleAtt} +\lambda(l) = \frac{l \cdot spotDirection - cos\theta_{outer}}{cos\theta_{inner} - cos\theta_{outer}} +\end{equation}$$ + +#### Attenuation function + +A proper evaluation of the inverse square law attenuation factor is mandatory for physically based punctual lights. The simple mathematical formulation is unfortunately impractical for implementation purposes: + +1. The division by the squared distance can lead to divides by 0 when objects intersect or "touch" light sources. + +2. The influence sphere of each light is infinite ($ \frac{I}{d^2} $ is asymptotic, it never reaches 0) which means that to correctly shade a pixel we need to evaluate every light in the world. + + +The first issue can be solved easily by setting the assumption that punctual lights are not truly punctual but instead small area lights. To do this we can simply treat punctual lights as spheres of 1 cm radius, as show in equation $\ref{finitePunctualLight}$. + +$$\begin{equation}\label{finitePunctualLight} +E = \frac{I}{max(d^2, {0.01}^2)} +\end{equation}$$ + +We can solve the second issue by introducing an influence radius for each light. There are several advantages to this solution. Tools can quickly show artists what parts of the world will be influenced by every light (the tool just needs to draw a sphere centered on each light). The rendering engine can cull lights more aggressively using this extra piece of information and artists/developers can assist the engine by manually tweaking the influence radius of a light. + +Mathematically, the illuminance of a light should smoothly reach zero at the limit defined by the influence radius. [#Karis13b] proposes to window the inverse square function in such a way that the majority of the light's influence remains unaffected. The proposed windowing is described in equation $\ref{attenuationWindowing}$, where $r$ is the light's radius of influence. + +$$\begin{equation}\label{attenuationWindowing} +E = \frac{I}{max(d^2, {0.01}^2)} \left< 1 - \frac{d^4}{r^4} \right>^2 +\end{equation}$$ + +Listing [glslPunctualLight] demonstrates how to implement physically based punctual lights in GLSL. Note that the light intensity used in this piece of code is the luminous intensity $I$ in $cd$, converted from the luminous power CPU-side. This snippet is not optimized and some of the computations can be offloaded to the CPU (for instance the square of the light's inverse falloff radius, or the spot scale and angle). + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +float getSquareFalloffAttenuation(vec3 posToLight, float lightInvRadius) { + float distanceSquare = dot(posToLight, posToLight); + float factor = distanceSquare * lightInvRadius * lightInvRadius; + float smoothFactor = max(1.0 - factor * factor, 0.0); + return (smoothFactor * smoothFactor) / max(distanceSquare, 1e-4); +} + +float getSpotAngleAttenuation(vec3 l, vec3 lightDir, + float innerAngle, float outerAngle) { + // the scale and offset computations can be done CPU-side + float cosOuter = cos(outerAngle); + float spotScale = 1.0 / max(cos(innerAngle) - cosOuter, 1e-4) + float spotOffset = -cosOuter * spotScale + + float cd = dot(normalize(-lightDir), l); + float attenuation = clamp(cd * spotScale + spotOffset, 0.0, 1.0); + return attenuation * attenuation; +} + +vec3 evaluatePunctualLight() { + vec3 l = normalize(posToLight); + float NoL = clamp(dot(n, l), 0.0, 1.0); + vec3 posToLight = lightPosition - worldPosition; + + float attenuation; + attenuation = getSquareFalloffAttenuation(posToLight, lightInvRadius); + attenuation *= getSpotAngleAttenuation(l, lightDir, innerAngle, outerAngle); + + vec3 luminance = (BSDF(v, l) * lightIntensity * attenuation * NoL) * lightColor; + return luminance; +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [glslPunctualLight]: Implementation of punctual lights in GLSL] + +### Photometric lights + +Punctual lights are an extremely practical and efficient way to light a scene but do not give artists enough control over the light distribution. The field of architectural lighting design concerns itself with designing lighting systems to serve humans needs by taking into account: + +- The amount of light provided +- The color of the light +- The distribution of light within the space + +The lighting system we have described so far can easily address the first two points but we need a way to define the distribution of light within the space. Light distribution is especially important for indoor scenes or for some types of outdoor scenes or even road lighting. Figure [lightDistributionTest] shows scenes where the light distribution is controlled by the artist. This type of distribution control is widely used when putting objects on display (museums, stores or galleries for instance). + +![Figure [lightDistributionTest]: Controlling the distribution of a point light](images/screenshot_photometric_lights.png) + +Photometric lights use a photometric profile to describe their intensity distribution. There are two commonly used formats, IES (Illuminating Engineering Society) and EULUMDAT (European Lumen Data format) but we will focus on the former. IES profiles are supported by many tools and engines, such as Unreal Engine 4, Frostbite, Renderman, Maya and Killzone. In addition, IES light profiles are commonly made available by bulbs and luminaires manufacturers (Philips offers [an extensive array of IES files](http://www.usa.lighting.philips.com/connect/tools_literature/photometric_data_1.wpd) for download for instance). Photometric profiles are particularly useful when they measure a luminaire or light fixture, in which the light source is partially covered. The luminaire will block the light emitted in certain directions, thus shaping the light distribution. + +![Example of a real world luminaires that can be described by photometric profiles](images/photo_photometric_lights.jpg) + +An IES profile stores luminous intensity for various angles on a sphere around the measured light source. This spherical coordinate system is usually referred to as the photometric web, which can be visualized using specialized tools such as [IESviewer](http://www.photometricviewer.com/). Figure [xarrow] below shows the photometric web of the XArrow IES profile [provided by Pixar](http://renderman.pixar.com/view/DP25764) for use with Renderman. This picture also shows a rendering in 3D space of the XArrow IES profile by our tool `lightgen`. + +![Figure [xarrow]: The XArrow IES profile rendered as a photometric web and as a point light in 3D space](images/screenshot_xarrow.png) + +The IES format is poorly documented and it is not uncommon to find syntax variations between files found on the Internet. The best resource to understand IES profile is Ian Ashdown's "Parsing the IESNA LM-63 photometric data file" document [#Ashdown98]. Succinctly, an IES profiles stores luminous intensities in candela at various angles around the light source. For each measured horizontal angle, a series of luminous intensities at different vertical angles is provided. It is however fairly common for measured light sources to be horizontally symmetrical. The XArrow profile shown above is a good example: intensities vary with vertical angles (vertical axis) but are symmetrical on the horizontal axis. The range of vertical angles in an IES profile is 0 to 180 degrees and the range of horizontal angles is 0 to 360 degrees. + +Figure [lightenSamples] shows the series of IES profiles provided by Pixar for Renderman, rendered using our `lightgen` tool. + +![Figure [lightenSamples]: Series of IES light profiles rendered with lightgen](images/screenshot_lightgen_samples.png) + +IES profiles can be applied directly to any punctual light, point or spot. To do so, we must first process the IES profile and generate a photometric profile as a texture. For performance considerations, the photometric profile we generate is a 1D texture that represents the average luminous intensity for all horizontal angles at a specific vertical angle (i.e., each pixel represents a vertical angle). To truly represent a photometric light, we should use a 2D texture but since most lights are fully, or mostly, symmetrical on the horizontal plane, we can accept this approximation. The values stored in the texture are normalized by the inverse maximum intensity defined in the IES profile. This allows us to easily store the texture in any float format or, at the cost of a bit of precision, in a luminance 8-bit texture (grayscale PNG for instance). Storing normalized values also allows us to treat photometric profiles as a mask: + +Photometric profile as a mask +: The luminous intensity is defined by the artist by setting the luminous power of the light, as with any other punctual light. The artist defined intensity is divided by the intensity of the light computed from the IES profile. IES profiles contain a luminous intensity but it is only valid for a bare light bulb whereas the measured intensity values take into account the light fixture. To measure the intensity of the luminaire, instead of the bulb, we perform a Monte-Carlo integration of the unit sphere using the intensities from the profile[^xarrowIntensity]. + +Photometric profile +: The luminous intensity comes from the profile itself. All the values sampled from the 1D texture are simply multiplied by the maximum intensity. We also provide a multiplier for convenience. + +The photometric profile can be applied at rendering time as a simple attenuation. The luminance equation $ \ref{photometricLightEvaluation} $ describes the photometric point light evaluation function. + +$$\begin{equation}\label{photometricLightEvaluation} +L_{out} = f(v,l) \frac{I}{d^2} \left< \NoL \right> \Psi(l) +\end{equation}$$ + +The term $ \Psi(l) $ is the photometric attenuation function. It depends on the light vector, but also on the direction of the light. Spot lights already possess a direction vector but we need to introduce one for photometric point lights as well. + +The photometric attenuation function can be easily implemented in GLSL by adding a new attenuation factor to the implementation of punctual lights (listing [glslPunctualLight]). The modified implementation is show in listing [glslPhotometricPunctualLight]. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +float getPhotometricAttenuation(vec3 posToLight, vec3 lightDir) { + float cosTheta = dot(-posToLight, lightDir); + float angle = acos(cosTheta) * (1.0 / PI); + return texture2DLodEXT(lightProfileMap, vec2(angle, 0.0), 0.0).r; +} + +vec3 evaluatePunctualLight() { + vec3 l = normalize(posToLight); + float NoL = clamp(dot(n, l), 0.0, 1.0); + vec3 posToLight = lightPosition - worldPosition; + + float attenuation; + attenuation = getSquareFalloffAttenuation(posToLight, lightInvRadius); + attenuation *= getSpotAngleAttenuation(l, lightDirection, innerAngle, outerAngle); + attenuation *= getPhotometricAttenuation(l, lightDirection); + + float luminance = (BSDF(v, l) * lightIntensity * attenuation * NoL) * lightColor; + return luminance; +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [glslPhotometricPunctualLight]: Implementation of attenuation from photometric profiles in GLSL] + +The light intensity is computed CPU-side (listing [photometricLightIntensity]) and depends on whether the photometric profile is used as a mask. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +float multiplier; +// Photometric profile used as a mask +if (photometricLight.isMasked()) { + // The desired intensity is set by the artist + // The integrated intensity comes from a Monte-Carlo + // integration over the unit sphere around the luminaire + multiplier = photometricLight.getDesiredIntensity() / + photometricLight.getIntegratedIntensity(); +} else { + // Multiplier provided for convenience, set to 1.0 by default + multiplier = photometricLight.getMultiplier(); +} + +// The max intensity in cd comes from the IES profile +float lightIntensity = photometricLight.getMaxIntensity() * multiplier; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [photometricLightIntensity]: Computing the intensity of a photometric light on the CPU] + +[^xarrowIntensity]: The XArrow profile declares a luminous intensity of 1,750 lm but a Monte-Carlo integration shows an intensity of only 350 lm. + +### Area lights + +[TODO] + +### Lights parameterization + +Similarly to the parameterization of the standard material model, our goal is to make lights parameterization intuitive and easy to use for artists and developers alike. In that spirit, we decided to separate the light color (or hue) from the light intensity. A light color will therefore be defined as a linear RGB color (or sRGB in the tools UI for convenience). + +The full list of light parameters is presented in table [lightParameters]. + + + Parameter | Definition +--------------------------:|:--------------------- +**Type** | Directional, point, spot or area +**Direction** | Used for directional lights, spot lights, photometric point lights, and linear and tubular area lights (orientation) +**Color** | The color of emitted light, as a linear RGB color. Can be specified as an sRGB color or a color temperature in the tools +**Intensity** | The light's brightness. The unit depends on the type of light +**Falloff radius** | Maximum distance of influence +**Inner angle** | Angle of the inner cone for spot lights, in degrees +**Outer angle** | Angle of the outer cone for spot lights, in degrees +**Length** | Length of the area light, used to create linear or tubular lights +**Radius** | Radius of the area light, used to create spherical or tubular lights +**Photometric profile** | Texture representing a photometric light profile, works only for punctual lights +**Masked profile** | Boolean indicating whether the IES profile is used as a mask or not. When used as a mask, the light's brightness will be multiplied by the ratio between the user specified intensity and the integrated IES profile intensity. When not used as a mask, the user specified intensity is ignored but the IES multiplier is used instead +**Photometric multiplier** | Brightness multiplier for photometric lights (if IES as mask is turned off) +[Table [lightParameters]: Light types parameters] + +**Note**: to simplify the implementation, all luminous powers will converted to luminous intensities ($cd$) before being sent to the shader. The conversion is light dependent and is explained in the previous sections. + +**Note**: the light type can be inferred from other parameters (e.g. a point light has a length, radius, inner angle and outer angle of 0). + +#### Color temperature + +However, real-world artificial lights are often defined by their color temperature, measured in Kelvin (K). The color temperature of a light source is the temperature of an ideal black-body radiator that radiates light of comparable hue to that of the light source. For convenience, the tools should allow the artist to specify the hue of a light source as a color temperature (a meaningful range is 1,000 K to 12,500 K). + +To compute RGB values from a temperature, we can use the Planckian locus, shown in figure [planckianLocus]. This locus is the path that the color of an incandescent black body takes in a chromaticity space as the body's temperature changes. + +![Figure [planckianLocus]: The Planckian locus visualized on a CIE 1931 chromaticity diagram (source: Wikipedia)](images/diagram_planckian_locus.png) + +The easiest way to compute RGB values from this locus is to use the formula described in [#Krystek85]. Krystek's algorithm (equation $\ref{krystek}$) works in the CIE 1960 (UCS) space, using the following formula where $T$ is the desired temperature, and $u$ and $v$ the coordinates in UCS. + +$$\begin{equation}\label{krystek} +u(T) = \frac{0.860117757 + 1.54118254 \times 10^{-4}T + 1.28641212 \times 10^{-7}T^2}{1 + 8.42420235 \times 10^{-4}T + 7.08145163 \times 10^{-7}T^2} \\ +v(T) = \frac{0.317398726 + 4.22806245 + \times 10^{-5}T + 4.20481691 \times 10^{-8}T^2}{1 - 2.89741816 + \times 10^{-5}T + 1.61456053 \times 10^{-7}T^2} +\end{equation}$$ + +This approximation is accurate to roughly $ 9 \times 10^{-5} $ in the range 1,000K to 15,000K. From the CIE 1960 space we can compute the coordinates in xyY space (CIES 1931), using the formula from equation $\ref{cieToxyY}$. + +$$\begin{equation}\label{cieToxyY} +x = \frac{3u}{2u - 8v + 4} \\ +y = \frac{2v}{2u - 8v + 4} +\end{equation}$$ + +The formulas above are valid for black body color temperatures, and therefore correlated color temperatures of standard illuminants. If we wish to compute the precise chromaticity coordinates of standard CIE illuminants in the D series we can use equation $\ref{seriesDtoxyY}$. + +$$\begin{equation}\label{seriesDtoxyY} +x = \begin{cases} 0.244063 + 0.09911 \frac{10^3}{T} + 2.9678 \frac{10^6}{T^2} - 4.6070 \frac{10^9}{T^3} & 4,000K \le T \le 7,000K \\ +0.237040 + 0.24748 \frac{10^3}{T} + 1.9018 \frac{10^6}{T^2} - 2.0064 \frac{10^9}{T^3} & 7,000K \le T \le 25,000K \end{cases} \\ +y = -3x^2 + 2.87 x - 0.275 +\end{equation}$$ + +From the xyY space, we can then convert to the CIE XYZ space (equation $\ref{xyYtoXYZ}$). + +$$\begin{equation}\label{xyYtoXYZ} +X = \frac{xY}{y} \\ +Z = \frac{(1 - x - y)Y}{y} +\end{equation}$$ + +For our needs, we will fix $Y = 1$. This allows us to convert from the XYZ space to linear RGB with a simple 3x3 matrix, as shown in equation $\ref{XYZtoRGB}$. + +$$\begin{equation}\label{XYZtoRGB} +\left[ \begin{matrix} R \\ G \\ B \end{matrix} \right] = M^{-1} \left[ \begin{matrix} X \\ Y \\ Z \end{matrix} \right] +\end{equation}$$ + +The transformation matrix M is calculated from the target RGB color space primaries. Equation $ \ref{XYZtoRGBValues} $ shows the conversion using the inverse matrix for the sRGB color space. + +$$\begin{equation}\label{XYZtoRGBValues} +\left[ \begin{matrix} R \\ G \\ B \end{matrix} \right] = \left[ \begin{matrix} 3.2404542 & -1.5371385 & -0.4985314 \\ -0.9692660 & 1.8760108 & 0.0415560 \\ 0.0556434 & -0.2040259 & 1.0572252 \end{matrix} \right] \left[ \begin{matrix} X \\ Y \\ Z \end{matrix} \right] +\end{equation}$$ + +The result of these operations is a linear RGB triplet in the sRGB color space. Since we care about the chromaticity of the results, we must apply a normalization step to avoid clamping values greater than 1.0 and distort resulting colors: + +$$\begin{equation}\label{normalizedRGB} +\hat{C}_{linear} = \frac{C_{linear}}{max(C_{linear})} +\end{equation}$$ + +We must finally apply the sRGB opto-electronic conversion function (OECF, shown in equation $ \ref{OECFsRGB} $) to obtain a displayable value (the value should remain linear if passed to the renderer for shading). + +$$\begin{equation}\label{OECFsRGB} +C_{sRGB} = \begin{cases} 12.92 \times \hat{C}_{linear} & \hat{C}_{linear} \le 0.0031308 \\ +1.055 \times \hat{C}_{linear}^{\frac{1}{2.4}} - 0.055 & \hat{C}_{linear} \gt 0.0031308 \end{cases} +\end{equation}$$ + +For convenience, figure [colorTemperatureScaleCCT] shows the range of correlated color temperatures from 1,000K to 12,500K. All the colors used below assume CIE $ D_{65} $ as the white point (as is the case in the sRGB color space). + +![Figure [colorTemperatureScaleCCT]: Scale of correlated color temperatures](images/diagram_color_temperature_cct.png) + +Similarly, figure [colorTemperatureScaleCIE] shows the range of CIE standard illuminants series D from 1,000K to 12,500K. + +![Figure [colorTemperatureScaleCIE]: Scale of CIE standard illuminants series D](images/diagram_color_temperature_cie.png) + +For reference, figure [colorTemperatureScaleCCTClamped] shows the range of correlated color temperatures without the normalization step presented in equation $\ref{normalizedRGB}$. + +![Figure [colorTemperatureScaleCCTClamped]: Unnormalized scale of correlated color temperatures](images/diagram_color_temperature_cct_clamped.png) + +Table [colorTemperatureSamples] presents the correlated color temperature of various common light sources as sRGB color swatches. These colors are relative to the $ D_{65} $ white point, so their perceived hue might vary based on your display's white point. See [What colour is the Sun?](http://jila.colorado.edu/~ajsh/colour/Tspectrum.html) for more information. + + + Temperature (K) | Light source | Color +--------------------:|:-----------------------------|------------------------------------------------------- +1,700-1,800 | Match flame |
 
+1,850-1,930 | Candle flame |
 
+2,000-3,000 | Sun at sunrise/sunset |
 
+2,500-2,900 | Household tungsten lightbulb |
 
+3,000 | Tungsten lamp 1K |
 
+3,200-3,500 | Quartz lights |
 
+3,200-3,700 | Fluorescent lights |
 
+3,275 | Tungsten lamp 2K |
 
+3,380 | Tungsten lamp 5K, 10K |
 
+5,000-5,400 | Sun at noon |
 
+5,500-6,500 | Daylight (sun + sky) |
 
+5,500-6,500 | Sun through clouds/haze |
 
+6,000-7,500 | Overcast sky |
 
+6,500 | RGB monitor white point |
 
+7,000-8,000 | Shaded areas outdoors |
 
+8,000-10,000 | Partly cloudy sky |
 
+[Table [colorTemperatureSamples]: Normalized correlated color temperatures for common light sources] + +### Pre-exposed lights + +Physically based rendering and physical light units pose an interesting challenge: how to store and handle the large range of values produced by the lighting code? Assuming computations performed at full precision in the shaders, we still want to be able to store the linear output of the lighting pass in a reasonably sized buffer (`RGB16F` or equivalent). The most obvious and easiest way to achieve this is to simply apply the camera exposure (see the Physically based camera section for more information) before writing out the result of the lighting pass. This simple step is shown in listing [preexposedLighting]: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +fragColor = luminance * camera.exposure; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [preexposedLighting]: The output of the lighting pass is pre-exposed to fit in half-float buffers] + +This solution solves the storage problem but requires intermediate computations to be performed with single precision floats. We would instead prefer to perform all (or at least most) of the lighting work using half precision floats instead. Doing so can greatly improve performance and power usage, particularly on mobile devices. Half precision floats are however ill-suited for this kind of work as common illuminance and luminance values (for the sun for instance) can exceed their range. The solution is to simply pre-expose the lights themselves instead of the result of the lighting pass. This can be done efficiently on the CPU if updating a light's constant buffer is cheap. This can also be done on the GPU, as shown in listing [preexposedLights]. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// The inputs must be highp/single precision, +// both for range (intensity) and precision (exposure) +// The output is mediump/half precision +float computePreExposedIntensity(highp float intensity, highp float exposure) { + return intensity * exposure; +} + +Light getPointLight(uint index) { + Light light; + uint lightIndex = // fetch light index; + + // the intensity must be highp/single precision + highp vec4 colorIntensity = lightsUniforms.lights[lightIndex][1]; + + // pre-expose the light + light.colorIntensity.w = computePreExposedIntensity( + colorIntensity.w, frameUniforms.exposure); + + return light; +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [preexposedLights]: Pre-exposing lights allows the entire shading pipeline to use half precision floats] + +In practice we pre-expose the following lights: +- Punctual lights (point and spot): on the GPU +- Directional light: on the CPU +- IBLs: on the CPU +- Material emissive: on the GPU + +## Image based lights + +In real life, light comes from every direction either directly from light sources or indirectly after bouncing off objects in the environment, being partially absorbed in the process. In a way the whole environment around an object can be seen as a light source. Images, in particular cubemaps, are a great way to encode such an “environment light”. This is called Image Based Lighting (IBL) or sometimes Indirect Lighting. + +![Figure [iblBall]: The object shown here is lit only by image-encoded environment lights. Notice the subtle lighting effects that can be applied using this technique.](images/screenshot_ball_ibl.png) + +There are limitations with image-based lighting. Obviously the environment image must be acquired somehow and as we'll see below it needs to be pre-processed before it can be used for lighting. Typically, the environment image is acquired offline in the real world, or generated by the engine either offline or at run time; either way, local or distant probes are used. + +These probes can be used to acquire the distant or local environment. In this document, we're focusing on distant environment probes, where the light is assumed to come from infinitely far away (which means every point on the object's surface uses the same environment map). + +The whole environment contributes light to a given point on the object's surface; this is called _irradiance_ ($E$). The resulting light bouncing off of the object is called radiance ($L_{out}$). Incident lighting must be applied consistently to the diffuse and specular parts of the BRDF. + +The radiance $L_{out}$ resulting from the interaction between an image based light's (IBL) irradiance and a material model (BRDF) $f(\Theta)$[^ibl1] is computed as follows: + +$$\begin{equation} +L_{out}(n, v, \Theta) = \int_\Omega f(l, v, \Theta) L_{\bot}(l) \left< \NoL \right> dl +\end{equation}$$ + +Note that here we're looking at the behavior of the surface at **macro** level (not to be confused with the micro level equation), which is why it only depends on $\vec n$ and $\vec v$. Essentially, we're applying the BRDF to “point-lights” coming from all directions and encoded in the IBL. + +### IBL Types ### + +There are four common types of IBLs used in modern rendering engines: + +- **Distant light probes**, used to capture lighting information at "infinity", where parallax can be ignored. Distant probes typically contain the sky, distant landscape features or buildings, etc. They are either captured by the engine or acquired from a camera as high dynamic range images (HDRI). + +- **Local light probes**, used to capture a certain area of the world from a specific point of view. The capture is projected on a cube or sphere depending on the surrounding geometry. Local probes are more accurate than distance probes and are particularly useful to add local reflections to materials. + +- **Planar reflections**, used to capture reflections by rendering the scene mirrored by a plane. This technique works only for flat surfaces such as building floors, roads and water. + +- **Screen space reflection**, used to capture reflections based on the rendered scene (using the previous frame for instance) by ray-marching in the depth buffer. SSR gives great result but can be very expensive. + +In addition we must distinguish between static and dynamic IBLs. Implementing a fully dynamic day/night cycle requires for instance to recompute the distant light probes dynamically[^iblTypes1]. Both planar and screen space reflections are inherently dynamic. + +### IBL Unit ### + +As discussed previously in the direct lighting section, all our lights must use physical units. As such our IBLs will use the luminance unit $\frac{cd}{m^2}$, which is also the output unit of all our direct lighting equations. Using the luminance unit is straightforward for light probes captures by the engine (dynamically or statically offline). + +High dynamic range images are a bit more delicate to handle however. Cameras do not record measured luminance but a device-dependent value that is only _related_ to the original scene luminance. As such, we must provide artists with a multiplier that allows them to recover, or at the very least closely approximate, the original absolute luminance. + +To properly reconstruct the luminance of an HDRI for IBL, artists must do more than simply take photos of the environment and record extra information: + +- **Color calibration**: using a gray card or a [MacBeth ColorChecker](http://en.wikipedia.org/wiki/ColorChecker) + +- **Camera settings**: aperture, shutter and ISO + +- **Luminance samples**: using a spot/luminance meter + +[TODO] Measure and list common luminance values (clear sky, interior, etc.) + +### Processing light probes ### + +We saw previously that the radiance of an IBL is computed by integrating over the surface's hemisphere. Since this would obviously be too expensive to do in real-time, we must first pre-process our light probes to convert them into a format better suited for real-time interactions. + +The sections below will discuss the techniques used to accelerate the evaluation of light probes: + +- **Specular reflectance**: pre-filtered importance sampling and split-sum approximation + +- **Diffuse reflectance**: irradiance map and spherical harmonics + +### Distant light probes ### + +#### Diffuse BRDF integration #### + +Using the Lambertian BRDF[^iblDiffuse1], we get the radiance: + +$$ +\begin{align*} + f_d(\sigma) &= \frac{\sigma}{\pi} \\ +L_d(n, \sigma) &= \int_{\Omega} f_d(\sigma) L_{\bot}(l) \left< \NoL \right> dl \\ + &= \frac{\sigma}{\pi} \int_{\Omega} L_{\bot}(l) \left< \NoL \right> dl \\ + &= \frac{\sigma}{\pi} E_d(n) \quad \text{with the irradiance} \; + E_d(n) = \int_{\Omega} L_{\bot}(l) \left< \NoL \right> dl +\end{align*} +$$ + +Or in the discrete domain: + +$$ E_d(n) \equiv \sum_{\forall \, i \in image} L_{\bot}(s_i) \left< n \cdot s_i \right> \Omega_s $$ + +$\Omega_s$ is the solid-angle[^iblDiffuse2] associated to sample $i$. + +The irradiance integral $\Ed$ can be trivially, albeit slowly[^iblDiffuse3], precomputed and stored into a cubemap for efficient access at runtime. Typically, _image_ is a cubemap or an equirectangular image. The term $ \frac{\sigma}{\pi} $ is independent of the IBL and is added at runtime to obtain the _radiance_. + +![Figure [iblOriginal]: Image-based environment](images/ibl/ibl_river_roughness_m0.png style="max-width:100%;") + +![Figure [iblIrradiance]: Image-based irradiance map using the Lambertian BRDF](images/ibl/ibl_irradiance.png style="max-width:100%;") + + +[^ibl1]: $\Theta$ represents the parameters of the material model $f$, i.e.: _roughness_, albedo and so on... + +[^iblTypes1]: This can be done through blending of static probes or by spreading the workload over time + +[^iblDiffuse1]: The Lambertian BRDF doesn't depend on $\vec l$, $\vec v$ or $\theta$, so $L_d(n,v,\theta) \equiv L_d(n,\sigma)$ + +[^iblDiffuse2]: $\Omega_s$ can be approximated by $\frac{2\pi}{6 \cdot width \cdot height}$ for a cubemap + +[^iblDiffuse3]: $O(12\,n^2\,m^2)$, with $n$ and $m$ respectively the dimensions of the environment and the precomputed cubemap + + +However, the irradiance can also be approximated very closely by a decomposition into Spherical Harmonics (SH, described in more details in the Spherical Harmonics section) and calculated at runtime cheaply. It is usually best to avoid texture fetches on mobile and free-up a texture unit. Even if it is stored into a cubemap, it is orders of magnitude faster to pre-compute the integral using SH decomposition followed by a rendering. + +SH decomposition is similar in concept to a Fourier transform, it expresses the signal over an orthonormal base in the frequency domain. The properties that interests us most are: + +- Very few coefficients are needed to encode $\cosTheta$ + +- Convolutions by a kernel that _has a circular symmetry_ are very inexpensive and become products in SH space + +In practice only 4 or 9 coefficients (i.e.: 2 or 3 bands) are enough for $\cosTheta$ meaning we don't need more either for $\Lt$. + +![Figure [iblSH3]: 3 bands (9 coefficients)](images/ibl/ibl_irradiance_sh3.png style="max-width:100%;") + +![Figure [iblSH2]: 2 bands (4 coefficients)](images/ibl/ibl_irradiance_sh2.png style="max-width:100%;") + + +In practice we pre-convolve $\Lt$ with $\cosTheta$ and pre-scale these coefficients by the basis scaling factors $K_l^m$ so that the reconstruction code is as simple as possible in the shader: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +vec3 irradianceSH(vec3 n) { + // uniform vec3 sphericalHarmonics[9] + // We can use only the first 2 bands for better performance + return + sphericalHarmonics[0] + + sphericalHarmonics[1] * (n.y) + + sphericalHarmonics[2] * (n.z) + + sphericalHarmonics[3] * (n.x) + + sphericalHarmonics[4] * (n.y * n.x) + + sphericalHarmonics[5] * (n.y * n.z) + + sphericalHarmonics[6] * (3.0 * n.z * n.z - 1.0) + + sphericalHarmonics[7] * (n.z * n.x) + + sphericalHarmonics[8] * (n.x * n.x - n.y * n.y); +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [irradianceSH]: GLSL code to reconstruct the irradiance from the pre-scaled SH] + +Note that with 2 bands, the computation above becomes a single $4 \times 4$ matrix-by-vector multiply. + +Additionally, because of the pre-scaling by $K_l^m$, the SH coefficients can be thought of as colors, in particular `sphericalHarmonics[0]` is directly the average irradiance. + + +#### Specular BRDF integration #### + +As we've seen above, the radiance $\Lout$ resulting from the interaction between an IBL's irradiance and a BRDF is: + +$$\begin{equation}\label{specularBRDFIntegration} +\Lout(n, v, \Theta) = \int_\Omega f(l, v, \Theta) \Lt(l) \left< \NoL \right> \partial l +\end{equation}$$ + +We recognize the convolution of $\Lt$ by $f(l, v, \Theta) \left< \NoL \right>$, +i.e.: the environment is *filtered* using the BRDF as a kernel. Indeed at higher roughness, +specular reflections look more *blurry*. + +Plugging the expression of $f$ in equation $\ref{specularBRDFIntegration}$, we obtain: + +$$\begin{equation} +\Lout(n,v,\Theta) = \int_\Omega D(l, v, \alpha) F(l, v, f_0, f_{90}) V(l, v, \alpha) \left< \NoL \right> \Lt(l) \partial l +\end{equation}$$ + +This expression depends on $v$, $\alpha$, $f_0$ and $f_{90}$ inside the integral, +which makes its evaluation extremely costly and unsuitable for real-time on mobile +(even using pre-filtered importance sampling). + +##### Simplifying the BRDF integration ##### + +Since there is no closed-form solution or an easy way to compute the $\Lout$ integral, we use a simplified +equation instead: $\hat{I}$, whereby we assume that $v = n$, that is the view direction $v$ is always +equal to the surface normal $n$. Clearly, this assumption will break all view-dependant effects of +the convolution, such as the increased blur in reflections closer to the viewer +(a.k.a. stretchy reflections). + +Such a simplification would also have a severe impact on constant environments, such as the white +furnace, because it would affect the magnitude of the constant (i.e. DC) term of the result. We +can at least correct for that by using a scale factor, $K$, in our simplified integral, which +will make sure the average irradiance stay correct when chosen properly. + + - $I$ is our original integral, i.e.: $I(g) = \int_\Omega g(l) \left< \NoL \right> \partial l$ + - $\hat{I}$ is the simplified integral where $v = n$ + - $K$ is a scale factor that ensures the average irradiance is unchanged by $\hat{I}$ + - $\tilde{I}$ is our final approximation of $I$, $\tilde{I} = \hat{I} \times K$ + + +Because $I$ is an integral multiplications can be distributed over it. i.e.: $I(g()f()) = I(g())I(f())$. + +Armed with that, + +$$\begin{equation} +I( f(\Theta) \Lt ) \approx \tilde{I}( f(\Theta) \Lt ) \\ +\tilde{I}( f(\Theta) \Lt ) = K \times \hat{I}( f(\Theta) \Lt ) \\ +K = \frac{I(f(\Theta))}{\hat{I}(f(\Theta))} +\end{equation}$$ + + +From the equation above we can see that $\tilde{I}$ is equivalent to $I$ when $\Lt$ is a constant, +and yields the correct result: + +$$\begin{align*} +\tilde{I}(f(\Theta)\Lt^{constant}) &= \Lt^{constant} \hat{I}(f(\Theta)) \frac{I(f(\Theta))}{\hat{I}(f(\Theta))} \\ + &= \Lt^{constant} I(f(\Theta)) \\ + &= I(f(\Theta)\Lt^{constant}) +\end{align*}$$ + + +Similarly, we can also demonstrate that the result is correct when $v = n$, since in that case $I = \hat{I}$: + +$$\begin{align*} +\tilde{I}(f(\Theta)\Lt) &= I(f(\Theta)\Lt) \frac{I(f(\Theta))}{I(f(\Theta))} \\ + &= I(f(\Theta)\Lt) +\end{align*}$$ + +Finally, we can show that the scale factor $K$ satisfies our average irradiance ($\bar{\Lt}$) +requirement by plugging $\Lt = \bar{\Lt} + (\Lt - \bar{\Lt}) = \bar{\Lt} + \Delta\Lt$ into $\tilde{I}$: + +$$\begin{align*} +\tilde{I}(f(\Theta)\Lt) &= \tilde{I}\left[f\left(\Theta\right) \left(\bar{\Lt} + \Delta\Lt\right)\right] \\ + &= K \times \hat{I}\left[f\left(\Theta\right) \left(\bar{\Lt} + \Delta\Lt\right)\right] \\ + &= K \times \left[\hat{I}\left(f\left(\Theta\right)\bar{\Lt}\right) + \hat{I}\left(f\left(\Theta\right)\Delta\Lt\right)\right] \\ + &= K \times \hat{I}\left(f\left(\Theta\right)\bar{\Lt}\right) + K \times \hat{I}\left(f\left(\Theta\right) \Delta\Lt\right) \\ + &= \tilde{I}\left(f\left(\Theta\right)\bar{\Lt}\right) + \tilde{I}\left(f\left(\Theta\right) \Delta\Lt\right) \\ + &= I\left(f\left(\Theta\right)\bar{\Lt}\right) + \tilde{I}\left(f\left(\Theta\right) \Delta\Lt\right) +\end{align*}$$ + +The above result shows that the average irradiance is computed correctly, i.e.: $I(f(\Theta)\bar{\Lt})$. + +A way to think about this approximation is that it splits the radiance $\Lt$ in two parts, +the average $\bar{\Lt}$ and the delta from the average $\Delta\Lt$ and computes the correct +integration of the average part then adds the simplified integration of the delta part: + +$$\begin{equation} +approximation(\Lt) = correct(\bar{\Lt}) + simplified(\Lt - \bar{\Lt}) +\end{equation}$$ + + + +Now, let's look at each term: + +$$\begin{equation}\label{iblPartialEquations} +\hat{I}(f(n, \alpha) \Lt) = \int_\Omega f(l, n, \alpha) \Lt(l) \left< \NoL \right> \partial l \\ +\hat{I}(f(n, \alpha)) = \int_\Omega f(l, n, \alpha) \left< \NoL \right> \partial l \\ +I(f(n, v, \alpha)) = \int_\Omega f(l, n, v, \alpha) \left< \NoL \right> \partial l +\end{equation}$$ + + +All three of these equations can be easily pre-calculated and stored in look-up tables, as explained +below. + + +##### Discrete Domain ##### + +In the discrete domain the equations in \ref{iblPartialEquations} become: + +$$\begin{equation} +\hat{I}(f(n, \alpha) \Lt) \equiv \frac{1}{N}\sum_{\forall \, i \in image} f(l_i, n, \alpha) \Lt(l_i) \left<\NoL\right> \\ +\hat{I}(f(n, \alpha)) \equiv \frac{1}{N}\sum_{\forall \, i \in image} f(l_i, n, \alpha) \left<\NoL\right> \\ +I(f(n, v, \alpha)) \equiv \frac{1}{N}\sum_{\forall \, i \in image} f(l_i, n, v, \alpha) \left<\NoL\right> +\end{equation}$$ + +However, in practice we're using _importance sampling_ which needs to take the $pdf$ of the distribution +into account and adds a term $\frac{4\left<\VoH\right>}{D(h_i, \alpha)\left<\NoH\right>}$. +See Importance Sampling For The IBL section: + +$$\begin{equation}\label{iblImportanceSampling} +\hat{I}(f(n, \alpha) \Lt) \equiv \frac{4}{N}\sum_i^N f(l_i, n, \alpha) \frac{\left<\VoH\right>}{D(h_i, \alpha)\left<\NoH\right>} \Lt(l_i) \left<\NoL\right> \\ +\hat{I}(f(n, \alpha)) \equiv \frac{4}{N}\sum_i^N f(l_i, n, \alpha) \frac{\left<\VoH\right>}{D(h_i, \alpha)\left<\NoH\right>} \left<\NoL\right> \\ +I(f(n, v, \alpha)) \equiv \frac{4}{N}\sum_i^N f(l_i, n, v, \alpha) \frac{\left<\VoH\right>}{D(h_i, \alpha)\left<\NoH\right>} \left<\NoL\right> +\end{equation}$$ + + +Recalling that for $\hat{I}$, we assume that $v = n$, equations \ref{iblImportanceSampling}, +simplifies to: + +$$\begin{equation} +\hat{I}(f(n, \alpha) \Lt) \equiv \frac{4}{N}\sum_i^N \frac{f(l_i, n, \alpha)}{D(h_i, \alpha)} \Lt(l_i) \left<\NoL\right> \\ +\hat{I}(f(n, \alpha)) \equiv \frac{4}{N}\sum_i^N \frac{f(l_i, n, \alpha)}{D(h_i, \alpha)} \left<\NoL\right> \\ +I(f(n, v, \alpha)) \equiv \frac{4}{N}\sum_i^N \frac{f(l_i, n, v, \alpha)}{D(h_i, \alpha)} \frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> +\end{equation}$$ + +Then, the first two equations can be merged together such that $LD(n, \alpha) = \frac{\hat{I}(f(n, \alpha) \Lt)}{\hat{I}(f(n, \alpha))}$ + +$$\begin{equation}\label{iblLD} +LD(n, \alpha) \equiv \frac{\sum_i^N \frac{f(l_i, n, \alpha)}{D(h_i, \alpha)} \Lt(l_i) \left<\NoL\right>}{\sum_i^N \frac{f(l_i, n, \alpha)}{D(h_i, \alpha)}\left<\NoL\right>} +\end{equation}$$ +$$\begin{equation}\label{iblDFV} +I(f(n, v, \alpha)) \equiv \frac{4}{N}\sum_i^N \frac{f(l_i, n, v, \alpha)}{D(h_i, \alpha)} \frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> +\end{equation}$$ + +Note that at this point, we could almost compute both remaining equations off-line. The only difficulty +is that we don't know $f_0$ nor $f_{90}$ when we precompute those integrals. We will see below that +we can incorporate these terms at runtime for equation \ref{iblDFV}, alas, this is not possible for +equation \ref{iblLD} and we have to assume $f_0 = f_{90} = 1$ (i.e.: the fresnel term always evaluates to 1). + +We also have to deal with the visibility term of the brdf, in practice keeping it yields to slightly +worst results compared to the ground truth, so we also set $V = 1$. + +Let's substitute $f$ in equations \ref{iblLD} and \ref{iblDFV}: + +$$\begin{equation} +f(l_i, n, \alpha) = D(h_i, \alpha)F(f_0, f_{90}, \left<\VoH\right>)V(l_i, v, \alpha) +\end{equation}$$ + +The first simplification is that the term $D(h_i, \alpha)$ in the brdf cancels out with the +denominator (which came from the $pdf$ due to importance sampling) and F and V disappear since we +assume their value is 1. + +$$\begin{equation} +LD(n, \alpha) \equiv \frac{\sum_i^N V(l_i, v, \alpha)\left<\NoL\right>\Lt(l_i) }{\sum_i^N \left<\NoL\right>} +\end{equation}$$ +$$\begin{equation}\label{iblFV} +I(f(n, v, \alpha)) \equiv \frac{4}{N}\sum_i^N \color{green}{F(f_0, f_{90}, \left<\VoH\right>)} V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> +\end{equation}$$ + +Now, let's substitute the fresnel term into equation \ref{iblFV}: + +$$\begin{equation} +F(f_0, f_{90}, \left<\VoH\right>) = f_0 (1 - F_c(\left<\VoH\right>)) + f_{90} F_c(\left<\VoH\right>) \\ +F_c(\left<\VoH\right>) = (1 - \left<\VoH\right>)^5 +\end{equation}$$ + + +$$\begin{equation} +I(f(n, v, \alpha)) \equiv \frac{4}{N}\sum_i^N \left[\color{green}{f_0 (1 - F_c(\left<\VoH\right>)) + f_{90} F_c(\left<\VoH\right>)}\right] V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> \\ +\end{equation}$$ + +$$ +\begin{align*} +I(f(n, v, \alpha)) \equiv & \color{green}{f_0 } \frac{4}{N}\sum_i^N \color{green}{(1 - F_c(\left<\VoH\right>))} V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> \\ + + & \color{green}{f_{90}} \frac{4}{N}\sum_i^N \color{green}{ F_c(\left<\VoH\right>) } V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> +\end{align*} +$$ + + +And finally, we extract the equations that can be calculated off-line (i.e.: the part that doesn't +depend on the runtime parameters $f_0$ and $f_{90}$): + +$$\begin{equation}\label{iblAllEquations} +DFG_1(\alpha, \left<\NoV\right>) = \frac{4}{N}\sum_i^N \color{green}{(1 - F_c(\left<\VoH\right>))} V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> \\ +DFG_2(\alpha, \left<\NoV\right>) = \frac{4}{N}\sum_i^N \color{green}{ F_c(\left<\VoH\right>) } V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> \\ +I(f(n, v, \alpha)) \equiv \color{green}{f_0} \color{red}{DFG_1(\alpha, \left<\NoV\right>)} + \color{green}{f_{90}} \color{red}{DFG_2(\alpha, \left<\NoV\right>)} +\end{equation}$$ + + +Notice that $DFG_1$ and $DFG_2$ only depend on $\NoV$, that is the angle between the normal $n$ and +the view direction $v$. This is true because the integral is symmetrical with respect to $n$. +When integrating, we can choose any $v$ we please as long as it satisfies $\NoV$ +(e.g.: when calculating $\VoH$). + + +Putting everything back together: + +$$ +\begin{align*} +\Lout(n,v,\alpha,f_0,f_{90}) &\simeq \big[ f_0 \color{red}{DFG_1(\NoV, \alpha)} + f_{90} \color{red}{DFG_2(\NoV, \alpha)} \big] \times LD(n, \alpha) \\ +DFG_1(\alpha, \left<\NoV\right>) &= \frac{4}{N}\sum_i^N \color{green}{(1 - F_c(\left<\VoH\right>))} V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> \\ +DFG_2(\alpha, \left<\NoV\right>) &= \frac{4}{N}\sum_i^N \color{green}{ F_c(\left<\VoH\right>) } V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> \\ +LD(n, \alpha) &= \frac{\sum_i^N V(l_i, n, \alpha)\left<\NoL\right>\Lt(l_i) }{\sum_i^N \left<\NoL\right>} +\end{align*} +$$ + +#### The $DFG_1$ and $DFG_2$ term visualized #### + +Both $DFG_1$ and $DFG_2$ can either be pre-calculated in a regular 2D texture indexed by $(\NoV, \alpha)$ +and sampled bilinearly, or computed at runtime using an analytic approximation of the surfaces. +See sample code in the annex. +The pre-calculated textures are shown in table [textureDFG]. +A C++ implementation of the pre-computation can be found in section [Precomputing L for image-based lighting]. + + +$DFG_1$ | $DFG_2$ | ${ DFG_1, DFG_2, 0 }$ +-------------------------|--------------------------|---------------------- +![](images/ibl/dfg1.png) | ![](images/ibl/dfg2.png) | ![](images/ibl/dfg.png) +[Table [textureDFG]: Y axis: $\alpha$. X axis: $cos \theta$] + + +$DFG_1$ and $DFG_2$ are conveniently within the $[0, 1]$ range, however 8-bits textures don't have +enough precision and will cause problems. +Unfortunately, on mobile, 16-bits or float textures are not ubiquitous and there are a limited +number of samplers. +Despite the attractive simplicity of the shader code using a texture, it might be better to use an +analytic approximation. Note however that since we only need to store two terms, +OpenGL ES 3.0's RG16F texture format is a good candidate. + +Such analytic approximation is described in [#Karis14], itself based on [#Lazarov13]. +[#Narkowicz14] is another interesting approximation. Note that these two approximations are not +compatible with the energy compensation term presented in section [Pre-integration for multiscattering]. +Table [textureApproxDFG] presents a visual representation of these approximations. + +$DFG_1$ | $DFG_2$ | ${ DFG_1, DFG_2, 0 }$ +--------------------------------|---------------------------------|---------------------- +![](images/ibl/dfg1_approx.png) | ![](images/ibl/dfg2_approx.png) | ![](images/ibl/dfg_approx.png) +[Table [textureApproxDFG]: Y axis: $\alpha$. X axis: $cos \theta$] + + +#### The $LD$ term visualized #### + +$LD$ is the convolution of the environment by a function that only depends on the $\alpha$ parameter +(itself related to the roughness, see section [Roughness remapping and clamping]). +$LD$ can conveniently be stored in a mip-mapped cubemap where increasing LODs receive the environment +pre-filtered with increasing roughness. This works well because this convolution is a +powerful low-pass filter. To make good use of each mipmap level, it is necessary to remap +$\alpha$; we find that using a power remapping with $\gamma = 2$ works well and is convenient. + +$$ +\begin{align*} + \alpha &= perceptualRoughness^2 \\ + lod_{\alpha} &= \alpha^{\frac{1}{2}} = perceptualRoughness \\ +\end{align*} +$$ + +See an example below: + + +![$\alpha=0.0$](images/ibl/ibl_river_roughness_m0.png style="max-width:100%;") +![$\alpha=0.2$](images/ibl/ibl_river_roughness_m1.png style="max-width:100%;") +![$\alpha=0.4$](images/ibl/ibl_river_roughness_m2.png style="max-width:100%;") +![$0.6$](images/ibl/ibl_river_roughness_m3.png style="max-width:100%;") +![$0.8$](images/ibl/ibl_river_roughness_m4.png style="max-width:100%;") + +#### Indirect specular and indirect diffuse components visualized #### + +Figure [iblVisualized] shows how indirect lighting interacts with dielectrics and conductors. Direct lighting was removed for illustration purposes. + +![Figure [iblVisualized]: Indirect diffuse and specular decomposition](images/ibl/ibl_visualization.jpg) + +#### IBL evaluation implementation #### + +Listing [iblEvaluation] presents a GLSL implementation to evaluate the IBL, using the various textures described in the previous sections. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +vec3 ibl(vec3 n, vec3 v, vec3 diffuseColor, vec3 f0, vec3 f90, + float perceptualRoughness) { + vec3 r = reflect(n); + vec3 Ld = textureCube(irradianceEnvMap, r) * diffuseColor; + float lod = computeLODFromRoughness(perceptualRoughness); + vec3 Lld = textureCube(prefilteredEnvMap, r, lod); + vec2 Ldfg = textureLod(dfgLut, vec2(dot(n, v), perceptualRoughness), 0.0).xy; + vec3 Lr = (f0 * Ldfg.x + f90 * Ldfg.y) * Lld; + return Ld + Lr; +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [iblEvaluation]: GLSL implementation of image based lighting evaluation] + +We can however save a couple of texture lookups by using Spherical Harmonics instead of an +irradiance cubemap and the analytical approximation of the $DFG$ LUT, as shown in listing [optimizedIblEvaluation]. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +vec3 irradianceSH(vec3 n) { + // uniform vec3 sphericalHarmonics[9] + // We can use only the first 2 bands for better performance + return + sphericalHarmonics[0] + + sphericalHarmonics[1] * (n.y) + + sphericalHarmonics[2] * (n.z) + + sphericalHarmonics[3] * (n.x) + + sphericalHarmonics[4] * (n.y * n.x) + + sphericalHarmonics[5] * (n.y * n.z) + + sphericalHarmonics[6] * (3.0 * n.z * n.z - 1.0) + + sphericalHarmonics[7] * (n.z * n.x) + + sphericalHarmonics[8] * (n.x * n.x - n.y * n.y); +} + +// NOTE: this is the DFG LUT implementation of the function above +vec2 prefilteredDFG_LUT(float coord, float NoV) { + // coord = sqrt(roughness), which is the mapping used by the + // IBL prefiltering code when computing the mipmaps + return textureLod(dfgLut, vec2(NoV, coord), 0.0).rg; +} + +vec3 evaluateSpecularIBL(vec3 r, float perceptualRoughness) { + // This assumes a 256x256 cubemap, with 9 mip levels + float lod = 8.0 * perceptualRoughness; + // decodeEnvironmentMap() either decodes RGBM or is a no-op if the + // cubemap is stored in a float texture + return decodeEnvironmentMap(textureCubeLodEXT(environmentMap, r, lod)); +} + +vec3 evaluateIBL(vec3 n, vec3 v, vec3 diffuseColor, vec3 f0, vec3 f90, float perceptualRoughness) { + float NoV = max(dot(n, v), 0.0); + vec3 r = reflect(-v, n); + + // Specular indirect + vec3 indirectSpecular = evaluateSpecularIBL(r, perceptualRoughness); + vec2 env = prefilteredDFG_LUT(perceptualRoughness, NoV); + vec3 specularColor = f0 * env.x + f90 * env.y; + + // Diffuse indirect + // We multiply by the Lambertian BRDF to compute radiance from irradiance + // With the Disney BRDF we would have to remove the Fresnel term that + // depends on NoL (it would be rolled into the SH). The Lambertian BRDF + // can be baked directly in the SH to save a multiplication here + vec3 indirectDiffuse = max(irradianceSH(n), 0.0) * Fd_Lambert(); + + // Indirect contribution + return diffuseColor * indirectDiffuse + indirectSpecular * specularColor; +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [optimizedIblEvaluation]: GLSL implementation of image based lighting evaluation] + + +#### Pre-integration for multiscattering #### + +In section [Energy loss in specular reflectance] we discussed how to use a second scaled specular lobe +to compensate for the energy loss due to only accounting for a single scattering event in our BRDF. +This energy compensation lobe is scaled by a term that depends on $r$ defined in the following way: + +$$\begin{equation} +r = \int_{\Omega} D(l,v) V(l,v) \left< \NoL \right> \partial l +\end{equation}$$ + +Or, evaluated with importance sampling (See Importance Sampling For The IBL section): + +$$\begin{equation} +r \equiv \frac{4}{N}\sum_i^N V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> +\end{equation}$$ + +This equality is very similar to the terms $DFG_1$ and $DFG_2$ seen in equation $\ref{iblAllEquations}$. +In fact, it's the same, except without the Fresnel term. + +By making the further assumption that $f_{90} = 1$, we can rewrite $DFG_1$ and $DFG_2$ and the +$\Lout$ reconstruction: + +$$ +\begin{align*} +\Lout(n,v,\alpha,f_0) &\simeq \big[ (1 - f_0) \color{red}{DFG_1^{multiscatter}(\NoV, \alpha)} + f_0 \color{red}{DFG_2^{multiscatter}(\NoV, \alpha)} \big] \times LD(n, \alpha) \\ +DFG_1^{multiscatter}(\alpha, \left<\NoV\right>) &= \frac{4}{N}\sum_i^N \color{green}{F_c(\left<\VoH\right>)} V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> \\ +DFG_2^{multiscatter}(\alpha, \left<\NoV\right>) &= \frac{4}{N}\sum_i^N V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> \\ +LD(n, \alpha) &= \frac{\sum_i^N V(l_i, n, \alpha)\left<\NoL\right>\Lt(l_i) }{\sum_i^N V(l_i, n, \alpha)\left<\NoL\right>} +\end{align*} +$$ + +These two new $DFG$ terms simply need to replace the ones used in the implementation shown in section [Precomputing L for image-based lighting]: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +float Fc = pow(1 - VoH, 5.0f); +r.x += Gv * Fc; +r.y += Gv; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [multiscatterIBLPreintegration]: C++ implementation of the $L_{DFG}$ term for multiscattering] + +To perform the reconstruction we need to slightly modify listing [multiscatterIBLEvaluation]: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +vec2 dfg = textureLod(dfgLut, vec2(dot(n, v), perceptualRoughness), 0.0).xy; +// (1 - f0) * dfg.x + f0 * dfg.y +vec3 specularColor = mix(dfg.xxx, dfg.yyy, f0); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [multiscatterIBLEvaluation]: GLSL implementation of image based lighting evaluation, with multiscattering LUT] + + +#### Summary #### + +In order to calculate the specular contribution of distant image-based lights, we had to make a few +approximations and compromises: + + - $v = n$, by far the assumption contributing to the largest error when integrating the + non-constant part of the IBL. This results in the complete loss of roughness anisotropy + with respect to the view point. + + - Roughness contribution for the non-constant part of the IBL is quantized and trilinear filtering + is used to interpolate between these levels. This is most visible at low roughnes (e.g.: around 0.0625 + for a 9 LODs cubemap). + + - Because mipmap levels are used to store the pre-integrated environment, they can't be used for + texture minification, as they ought to. This can causes aliasing or moiré artifacts in high frequency + regions or the environment at low roughness and/or distant or small objects. + This can also impact performance due to the resulting poor cache access pattern. + + - No Fresnel for the non-constant part of the IBL. + + - Visibility = 1 for the non-constant part of the IBL. + + - Schlick's Fresnel + + - $f_{90} = 1$ in the multiscattering case. + + +![Figure [iblPrefilterVsImportanceSampling]: +Comparison between importance-sampled reference (top) and prefiltered IBL (middle).](images/ibl/ibl_prefilter_vs_reference.png) + +![Figure [iblStretchyReflectionLoss]: +Error in reflections due to assuming $v = n$ (bottom) -- loss of "stretchy reflections".](images/ibl/ibl_stretchy_reflections_error.png) + +![Figure [iblRoughnessInLods0]: +Error due to storing the roughness in cubemaps LODs at roughness = 0.0625 (i.e.: sampling exactly between levels). +Notice how instead of bluring we see a "cross-fade" between two blurs.](images/ibl/ibl_trilinear_0.png) + +![Figure [iblRoughnessInLods1]: +Error due to storing the roughness in cubemaps LODs at roughness = 0.125 (i.e.: sampling exactly level 1). +When the roughness closely matches a LOD, the error due to trilinear filtering in the cubemap is +reduced. Notice the errors due to $v = n$ at grazing angles.](images/ibl/ibl_trilinear_1.png) + +![Figure [iblMoirePattern]: +Moiré pattern due to texture minification on a metallic sphere at $\alpha = 0$ +using an environment made of colored vertical stripes (skybox hidden).](images/ibl/ibl_no_mipmaping.png) + + +### Clear coat ### + +When sampling the IBL, the clear coat layer is calculated as a second specular lobe. This specular lobe is oriented along the view direction since we cannot reasonably integrate over the hemisphere. Listing [clearCoatIBL] demonstrates this approximation in practice. It also shows the energy conservation step. It is important to note that this second specular lobe is computed exactly the same way as the main specular lobe, using the same DFG approximation. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// clearCoat_NoV == shading_NoV if the clear coat layer doesn't have its own normal map +float Fc = F_Schlick(0.04, 1.0, clearCoat_NoV) * clearCoat; +// base layer attenuation for energy compensation +iblDiffuse *= 1.0 - Fc; +iblSpecular *= sq(1.0 - Fc); +iblSpecular += specularIBL(r, clearCoatPerceptualRoughness) * Fc; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [clearCoatIBL]: GLSL implementation of the clear coat specular lobe for image-based lighting] + +### Anisotropy ### + +[#McAuley15] describes a technique called “bent reflection vector”, based [#Revie12]. The bent reflection vector is a rough approximation of anisotropic lighting but the alternative is to use importance sampling. This approximation is sufficiently cheap to compute and provides good results, as shown in figure [anisotropicIBL1] and figure [anisotropicIBL2]. + +![Figure [anisotropicIBL1]: Anisotropic indirect specular reflections using bent normals (left: roughness 0.3, right: roughness: 0.0; both: anisotropy 1.0)](images/screenshot_anisotropic_ibl1.jpg) + +![Figure [anisotropicIBL2]: Anisotropic reflections with varying roughness, metallicness, etc.](images/screenshot_anisotropic_ibl2.jpg) + +The implementation of this technique is straightforward, as demonstrated in listing [bentReflectionVector]. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +vec3 anisotropicTangent = cross(bitangent, v); +vec3 anisotropicNormal = cross(anisotropicTangent, bitangent); +vec3 bentNormal = normalize(mix(n, anisotropicNormal, anisotropy)); +vec3 r = reflect(-v, bentNormal); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [bentReflectionVector]: GLSL implementation of the bent reflection vector] + +This technique can be made more useful by accepting negative `anisotropy` values, as shown in listing [bentReflectionVectorDirection]. When the anisotropy is negative, the highlights are not in the direction of the tangent, but in the direction of the bitangent instead. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +vec3 anisotropicDirection = anisotropy >= 0.0 ? bitangent : tangent; +vec3 anisotropicTangent = cross(anisotropicDirection, v); +vec3 anisotropicNormal = cross(anisotropicTangent, anisotropicDirection); +vec3 bentNormal = normalize(mix(n, anisotropicNormal, anisotropy)); +vec3 r = reflect(-v, bentNormal); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [bentReflectionVectorDirection]: GLSL implementation of the bent reflection vector] + +Figure [anisotropicDirection] demonstrates this modified implementation in practice. + +![Figure [anisotropicDirection]: Control of the anisotropy direction using positive (left) and negative (right) values](images/screenshot_anisotropy_direction.png) + +### Subsurface ### + +[TODO] Explain subsurface and IBL + +### Cloth ### + +The IBL implementation for the cloth material model is more complicated than for the other material models. The main difference stems from the use of a different NDF ("Charlie" vs height-correlated Smith GGX). As described in this section, we use the split-sum approximation to compute the DFG term of the BRDF when computing an IBL. This DFG term is designed for a different BRDF and cannot be used for the cloth BRDF. Since we designed our cloth BRDF to not need a Fresnel term, we can generate a single DG term in the 3rd channel of the DFG LUT. The result is shown in figure [dfgClothLUT]. + +The DG term is generated using uniform sampling as recommended in [#Estevez17]. With uniform sampling the $pdf$ is simply $\frac{1}{2\pi}$ and we must still use the Jacobian $\frac{1}{4\left< \VoH \right>}$. + +![Figure [dfgClothLUT]: DFG LUT with a 3rd channel encoding the DG term of the cloth BRDF](images/ibl/dfg_cloth.png) + +The remainder of the image-based lighting implementation follows the same steps as the implementation of regular lights, including the optional subsurface scattering term and its wrap diffuse component. Just as with the clear coat IBL implementation, we cannot integrate over the hemisphere and use the view direction as the dominant light direction to compute the wrap diffuse component. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +float diffuse = Fd_Lambert() * ambientOcclusion; +#if defined(SHADING_MODEL_CLOTH) +#if defined(MATERIAL_HAS_SUBSURFACE_COLOR) +diffuse *= saturate((NoV + 0.5) / 2.25); +#endif +#endif + +vec3 indirectDiffuse = irradianceIBL(n) * diffuse; +#if defined(SHADING_MODEL_CLOTH) && defined(MATERIAL_HAS_SUBSURFACE_COLOR) +indirectDiffuse *= saturate(subsurfaceColor + NoV); +#endif + +vec3 ibl = diffuseColor * indirectDiffuse + indirectSpecular * specularColor; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [clothApprox]: GLSL implementation of the DFG approximation for the cloth NDF] + +It is important to note that this only addresses part of the IBL problem. The pre-filtered specular environment maps described earlier are convolved with the standard shading model's BRDF, which differs from the cloth BRDF. To get accurate result we should in theory provide one set of IBLs per BRDF used in the engine. Providing a second set of IBLs is however not practical for our use case so we decided to rely on the existing IBLs instead. + +## Static lighting + +[TODO] Spherical-harmonics or spherical-gaussian lightmaps, irradiance volumes, PRT?… + +## Transparency and translucency lighting + +Transparent and translucent materials are important to add realism and correctness to scenes. Filament must therefore provide lighting models for both types of materials to allow artists to properly recreate realistic scenes. Translucency can also be used effectively in a number of non-realistic settings. + +### Transparency + +To properly light a transparent surface, we must first understand how the material's opacity is applied. Observe a window and you will see that the diffuse reflectance is transparent. On the other hand, the brighter the specular reflectance, the less opaque the window appears. This effect can be seen in figure [cameraTransparency]: the scene is properly reflected onto the glass surfaces but the specular highlight of the sun is bright enough to appear opaque. + +![Figure [cameraTransparency]: Example of a complex object where lit surface transparency plays an important role](images/screenshot_camera_transparency.jpg) + +![Figure [litCar]: Example of a complex object where lit surface transparency plays an important role](images/screenshot_car.jpg) + +To properly implement opacity, we will use the premultiplied alpha format. Given a desired opacity noted $ \alpha_{opacity} $ and a diffuse color $ \sigma $ (linear, unpremultiplied), we can compute the effective opacity of a fragment. + +$$\begin{align*} +color &= \sigma * \alpha_{opacity} \\ +opacity &= \alpha_{opacity} +\end{align*}$$ + +The physical interpretation is that the RGB components of the source color define how much light is emitted by the pixel, whereas the alpha component defines how much of the light behind the pixel is blocked by said pixel. We must therefore use the following blending functions: + +$$\begin{align*} +Blend_{src} &= 1 \\ +Blend_{dst} &= 1 - src_{\alpha} +\end{align*}$$ + +The GLSL implementation of these equations is presented in listing [surfaceTransparency]. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// baseColor has already been premultiplied +vec4 shadeSurface(vec4 baseColor) { + float alpha = baseColor.a; + + vec3 diffuseColor = evaluateDiffuseLighting(); + vec3 specularColor = evaluateSpecularLighting(); + + return vec4(diffuseColor + specularColor, alpha); +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [surfaceTransparency]: Implementation of lit surface transparency in GLSL] + +### Translucency + +Translucent materials can be divided into two categories: +- Surface translucency +- Volume translucency + +Volume translucency is useful to light particle systems, for instance clouds or smoke. Surface translucency can be used to imitate materials with transmitted scattering such as wax, marble, skin, etc. + +[TODO] Surface translucency (BRDF+BTDF, BSSRDF) + +![Figure [translucency]: Front-lit translucent object (left) and back-lit translucent object (right), using approximated BTDF and BSSRDF. Model: Lucy from the Stanford University Computer Graphics Laboratory](images/screenshot_translucency.png) + +## Occlusion + +Occlusion is an important darkening factor used to recreate shadowing at various scales: + +Small scale +: Micro-occlusion used to handle creases, cracks and cavities. + +Medium scale +: Macro-occlusion used to handle occlusion by an object's own geometry or by geometry baked in normal maps (bricks, etc.). + +Large scale +: Occlusion coming from contact between objects, or from an object's own geometry. + +We currently ignore micro-occlusion, which is often exposed in tools and engines under the form of a "cavity map". Sébastien Lagarde offers an interesting discussion in [#Lagarde14] on how micro-occlusion is handled in Frostbite: diffuse micro-occlusion is pre-baked in diffuse maps and specular micro-occlusion is pre-baked in reflectance textures. +In our system, micro-occlusion can simply be baked in the base color map. This must be done knowing that the specular light will not be affected by micro-occlusion. + +Medium scale ambient occlusion is pre-baked in ambient occlusion maps, exposed as a material parameter, as seen in the material parameterization section earlier. + +Large scale ambient occlusion is often computed using screen-space techniques such as *SSAO* (screen-space ambient occlusion), *HBAO* (horizon based ambient occlusion), etc. Note that these techniques can also contribute to medium scale ambient occlusion when the camera is close enough to surfaces. + +**Note**: to prevent over darkening when using both medium and large scale occlusion, Lagarde recommends to use $min({AO}_{medium}, {AO}_{large})$. + +### Diffuse occlusion + +Morgan McGuire formalizes ambient occlusion in the context of physically based rendering in [#McGuire10]. In his formulation, McGuire defines an ambient illumination function $ L_a $, which in our case is encoded with spherical harmonics. He also defines a visibility function $V$, with $V(l)=1$ if there is an unoccluded line of sight from the surface in direction $l$, and 0 otherwise. + +With these two functions, the ambient term of the rendering equation can be expressed as shown in equation $\ref{diffuseAO}$. + +$$\begin{equation}\label{diffuseAO} +L(l,v) = \int_{\Omega} f(l,v) L_a(l) V(l) \left< \NoL \right> dl +\end{equation}$$ + +This expression can be approximated by separating the visibility term from the illumination function, as shown in equation $\ref{diffuseAOApprox}$. + +$$\begin{equation}\label{diffuseAOApprox} +L(l,v) \approx \left( \pi \int_{\Omega} f(l,v) L_a(l) dl \right) \left( \frac{1}{\pi} \int_{\Omega} V(l) \left< \NoL \right> dl \right) +\end{equation}$$ + +This approximation is only exact when the distant light $ L_a $ is constant and $f$ is a Lambertian term. McGuire states however that this approximation is reasonable if both functions are relatively smooth over most of the sphere. This happens to be the case with a distant light probe (IBL). + +The left term of this approximation is the pre-computed diffuse component of our IBL. The right term is a scalar factor between 0 and 1 that indicates the fractional accessibility of a point. Its opposite is the diffuse ambient occlusion term, show in equation $\ref{diffuseAOTerm}$. + +$$\begin{equation}\label{diffuseAOTerm} +{AO} = 1 - \frac{1}{\pi} \int_{\Omega} V(l) \left< \NoL \right> dl +\end{equation}$$ + +Since we use a pre-computed diffuse term, we cannot compute the exact accessibility of shaded points at runtime. To compensate for this lack of information in our precomputed term, we partially reconstruct incident lighting by applying an ambient occlusion factor specific to the surface's material at the shaded point. + +In practice, baked ambient occlusion is stored as a grayscale texture which can often be lower resolution than other textures (base color or normals for instance). It is important to note that the ambient occlusion property of our material model intends to recreate macro-level diffuse ambient occlusion. While this approximation is not physically correct, it constitutes an acceptable tradeoff of quality vs performance. + +Figure [aoComparison] shows two different materials without and with diffuse ambient occlusion. Notice how the material ambient occlusion is used to recreate the natural shadowing that occurs between the different tiles. Without ambient occlusion, both materials appear too flat. + +![Figure [aoComparison]: Comparison of materials without diffuse ambient occlusion (left) and with (right)](images/screenshot_ao.jpg) + +Applying baked diffuse ambient occlusion in a GLSL shader is straightforward, as shown in listing [bakedDiffuseAO]. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// diffuse indirect +vec3 indirectDiffuse = max(irradianceSH(n), 0.0) * Fd_Lambert(); +// ambient occlusion +indirectDiffuse *= texture2D(aoMap, outUV).r; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [bakedDiffuseAO]: Implementation of baked diffuse ambient occlusion in GLSL] + +Note how the ambient occlusion term is only applied to indirect lighting. + +### Specular occlusion + +Specular micro-occlusion can be derived from $\fNormal$, itself derived from the diffuse color. The derivation is based on the knowledge that no real-world material has a reflectance lower than 2%. Values in the 0-2% range can therefore be treated as pre-baked specular occlusion used to smoothly extinguish the Fresnel term. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +float f90 = clamp(dot(f0, 50.0 * 0.33), 0.0, 1.0); +// cheap luminance approximation +float f90 = clamp(50.0 * f0.g, 0.0, 1.0); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [specularMicroOcclusion]: Pre-baked specular occlusion in GLSL] + +The derivations mentioned earlier for ambient occlusion assume Lambertian surfaces and are only valid for indirect diffuse lighting. The lack of information about surface accessibility is particularly harmful to the reconstruction of indirect specular lighting. It usually manifests itself as light leaks. + +Sébastien Lagarde proposes an empirical approach to derive the specular occlusion term from the diffuse occlusion term in [#Lagarde14]. The result does not have any physical basis but produces visually pleasant results. The goal of his formulation is return the diffuse occlusion term unmodified for rough surfaces. For smooth surfaces, the formulation, implemented in listing [specularOcclusion], reduces the influence of occlusion at normal incidence and increases it at grazing angles. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +float computeSpecularAO(float NoV, float ao, float roughness) { + return clamp(pow(NoV + ao, exp2(-16.0 * roughness - 1.0)) - 1.0 + ao, 0.0, 1.0); +} + +// specular indirect +vec3 indirectSpecular = evaluateSpecularIBL(r, perceptualRoughness); +// ambient occlusion +float ao = texture2D(aoMap, outUV).r; +indirectSpecular *= computeSpecularAO(NoV, ao, roughness); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [specularOcclusion]: Implementation of Lagarde's specular occlusion factor in GLSL] + +Note how the specular occlusion factor is only applied to indirect lighting. + +#### Horizon specular occlusion + +When computing the specular IBL contribution for a surface that uses a normal map, it is possible to end up with a reflection vector pointing towards the surface. If this reflection vector is used for shading directly, the surface will be lit in places where it should not be lit (assuming opaque surfaces). This is another occurrence of light leaking that can easily be minimized using a simple technique described by Jeff Russell [#Russell15]. + +The key idea is to occlude light coming from behind the surface. This can easily be achieved since a negative dot product between the reflected vector and the surface's normal indicates a reflection vector pointing towards the surface. Our implementation shown in listing [horizonOcclusion] is similar to Russell's, albeit without the artist controlled horizon fading factor. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// specular indirect +vec3 indirectSpecular = evaluateSpecularIBL(r, perceptualRoughness); + +// horizon occlusion with falloff, should be computed for direct specular too +float horizon = min(1.0 + dot(r, n), 1.0); +indirectSpecular *= horizon * horizon; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [horizonOcclusion]: Implementation of horizon specular occlusion in GLSL] + +Horizon specular occlusion fading is cheap but can easily be omitted to improve performance as needed. + +## Normal mapping + +There are two common use cases of normal maps: replacing high-poly meshes with low-poly meshes (using a base map) and adding surface details (using a detail map). + +Let's imagine that we want to render a piece of furniture covered in tufted leather. Modeling the geometry to accurately represent the tufted pattern would require too many triangles so we instead bake a high-poly mesh into a normal map. Once the base map is applied to a simplified mesh (in this case, a quad), we get the result in figure [normalMapped]. The base map used to create this effect is shown in figure [baseNormalMap]. + +![Figure [normalMapped]: Low-poly mesh without normal mapping (left) and with (right)](images/screenshot_normal_mapping.jpg) + +![Figure [baseNormalMap]: Normal map used as a base map](images/screenshot_normal_map.jpg) + +A simple problem arises if we now want to combine this base map with a second normal map. For instance, let's use the detail map shown in figure [detailNormalMap] to add cracks in the leather. + +![Figure [detailNormalMap]: Normal map used as a detail map](images/screenshot_normal_map_detail.jpg) + +Given the nature of normal maps (XYZ components stored in tangent space), it is fairly obvious that naive approaches such as linear or overlay blending cannot work. We will use two more advanced techniques: a mathematically correct one and an approximation suitable for real-time shading. + +### Reoriented normal mapping + +Colin Barré-Brisebois and Stephen Hill propose in [#Hill12] a mathematically sound solution called *Reoriented Normal Mapping*, which consists in rotating the basis of the detail map onto the normal from the base map. This technique relies on the shortest arc quaternion to apply the rotation, which greatly simplifies thanks to the properties of the tangent space. + +Following the simplifications described in [#Hill12], we can produce the GLSL implementation shown in listing [reorientedNormalMapping]. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +vec3 t = texture(baseMap, uv).xyz * vec3( 2.0, 2.0, 2.0) + vec3(-1.0, -1.0, 0.0); +vec3 u = texture(detailMap, uv).xyz * vec3(-2.0, -2.0, 2.0) + vec3( 1.0, 1.0, -1.0); +vec3 r = normalize(t * dot(t, u) - u * t.z); +return r; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [reorientedNormalMapping]: Implementation of reoriented normal mapping in GLSL] + +Note that this implementation assumes that the normals are stored uncompressed and in the [0..1] range in the source textures. + +The normalization step is not strictly necessary and can be skipped if the technique is used at runtime. If so, the computation of `r` becomes `t * dot(t, u) / t.z - u`. + +Since this technique is slightly more expensive than the one described below, we will mostly use it offline. We therefore provide a simple offline tool to combine two normal maps. Figure [blendedNormalMaps] presents the output of the tool with the base map and the detail map shown previously. + +![Figure [blendedNormalMaps]: Blended normal and detail map (left) and resulting render when combined with a diffuse map (right)](images/screenshot_normal_map_blended.jpg) + +### UDN blending + +The technique called UDN blending, described in [#Hill12], is a variant of the partial derivative blending technique. Its main advantage is the low number of shader instructions it requires (see listing [udnBlending]). While it leads to a reduction in details over flat areas, UDN blending is interesting if blending must be performed at runtime. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +vec3 t = texture(baseMap, uv).xyz * 2.0 - 1.0; +vec3 u = texture(detailMap, uv).xyz * 2.0 - 1.0; +vec3 r = normalize(t.xy + u.xy, t.z); +return r; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [udnBlending]: Implementation of UDN blending in GLSL] + +The results are visually close to Reoriented Normal Mapping but a careful comparison of the data shows that UDN is indeed less correct. Figure [blendedNormalMapsUDN] presents the result of the UDN blending approach using the same source data as in the previous examples. + +![Figure [blendedNormalMapsUDN]: Blended normal and detail map using the UDN blending technique](images/screenshot_normal_map_blended_udn.jpg) + +# Volumetric effects + +## Exponential height fog + +![Figure [exponentialHeightFog1]: Example of directional in-scattering with exponential height fog](images/screenshot_fog1.jpg) + +![Figure [exponentialHeightFog2]: Example of directional in-scattering with exponential height fog](images/screenshot_fog2.jpg) + +# Anti-aliasing + +[TODO] MSAA, geometric AA (normals and roughness), shader anti-aliasing (object-space shading?) + +# Imaging pipeline + +The lighting section of this document describes how light interacts with surfaces in the scene in a physically based manner. To achieve plausible results, we must go a step further and consider the transformations necessary to convert the scene luminance, as computed by our lighting equations, into displayable pixel values. + +The series of transformations we are going to use form the following imaging pipeline: + +************************************************************************************* +* .-------------. .--------------. .---------------. * +* | Scene | | Normalized | | | * +* | luminance +----->| luminance +----->| White balance | * +* | | | (HDR) | | | * +* '-------------' '--------------' '-------+-------' * +* | * +* v * +* .---------------. * +* | | * +* | Color grading | * +* | | * +* '-------+-------' * +* | * +* v * +* .---------------. * +* | | * +* | Tone mapping | * +* | | * +* '-------+-------' * +* | * +* v * +* .---------------. .-------------. * +* | | | Pixel | * +* | OETF +----->| value | * +* | | | (LDR) | * +* '---------------' '-------------' * +************************************************************************************* + +**Note**: the *OETF* step is the application of the opto-electronic transfer function of the target color space. For clarity this diagram does not include post-processing steps such as vignette, bloom, etc. These effects will be discussed separately. + +[TODO] Color spaces (ACES, sRGB, Rec. 709, Rec. 2020, etc.), gamma/linear, etc. + +## Physically based camera + +The first step in the image transformation process is to use a physically based camera to properly expose the scene's outgoing luminance. + +### Exposure settings + +Because we use photometric units throughout the lighting pipeline, the light reaching the camera is an energy expressed in luminance $L$, in $cd.m^{-2}$. Light incident to the camera sensor can cover a large range of values, from $10^{-5}cd.m^{-2}$ for starlight to $10^{9}cd.m^{-2}$ for the sun. Since we obviously cannot manipulate and even less record such a large range of values, we need to remap them. + +This range remapping is done in a camera by exposing the sensor for a certain time. To maximize the use of the limited range of the sensor, the scene's light range is centered around the "middle gray", a value halfway between black and white. The exposition is therefore achieved by manipulating, either manually or automatically, 3 settings: + +- Aperture +- Shutter speed +- Sensitivity (also called gain) + +Aperture +: Noted $N$ and expressed in f-stops ƒ, this setting controls how open or closed the camera system's aperture is. Since an f-stop indicate the ratio of the lens' focal length to the diameter of the entrance pupil, high-values (ƒ/16) indicate a small aperture and small values (ƒ/1.4) indicate a wide aperture. In addition to the exposition, the aperture setting controls the depth of field. + +Shutter speed +: Noted $t$ and expressed in seconds $s$, this setting controls how long the aperture remains opened (it also controls the timing of the sensor shutter(s), whether electronic or mechanical). In addition to the exposition, the shutter speed controls motion blur. + +Sensitivity +: Noted $S$ and expressed in ISO, this setting controls how the light reaching the sensor is quantized. Because of its unit, this setting is often referred to as simply the "ISO" or "ISO setting". In addition to the exposition, the sensitivity setting controls the amount of noise. + +### Exposure value + +Since referring to these 3 settings in our equations would be unwieldy, we instead summarize the “exposure triangle” by an exposure value, noted EV[^reciprocity]. + +The EV is expressed in a base-2 logarithmic scale, with a difference of 1 EV called a stop. One positive stop (+1 EV) corresponds to a factor of two in luminance and one negative stop (-1 EV) corresponds to a factor of half in luminance. + +Equation $ \ref{ev} $ shows the [formal definition of EV](https://en.wikipedia.org/wiki/Exposure_value). + +$$\begin{equation}\label{ev} +EV = log_2(\frac{N^2}{t}) +\end{equation}$$ + +Note that this definition is only function of the aperture and shutter speed, but not the sensitivity. An exposure value is by convention defined for ISO 100, or $ EV_{100} $, and because we wish to work with this convention, we need to be able to express $ EV_{100} $ as a function of the sensitivity. + +Since we know that EV is a base-2 logarithmic scale in which each stop increases or decreases the brightness by a factor of 2, we can formally define $ EV_{S} $, the exposure value at given sensitivity (equation $\ref{evS}$). + +$$\begin{equation}\label{evS} +{EV}_S = EV_{100} + log_2(\frac{S}{100}) +\end{equation}$$ + +Calculating the $ EV_{100} $ as a function of the 3 camera settings is trivial, as shown in $\ref{ev100}$. + +$$\begin{equation}\label{ev100} +{EV}_{100} = EV_{S} - log_2(\frac{S}{100}) = log_2(\frac{N^2}{t}) - log_2(\frac{S}{100}) +\end{equation}$$ + +Note that the operator (photographer, etc.) can achieve the same exposure (and therefore EV) with several combinations of aperture, shutter speed and sensitivity. This allows some artistic control in the process (depth of field vs motion blur vs grain). + +[^reciprocity]: We assume a digital sensor, which means we don't need to take reciprocity failure into account + +#### Exposure value and luminance + +A camera, similar to a spot meter, is able to measure the average luminance of a scene and convert it into EV to achieve automatic exposure, or at the very least offer the user exposure guidance. + +It is possible to define EV as a function of the scene luminance $L$, given a per-device calibration constant $K$ (equation $ \ref{evK} $). + +$$\begin{equation}\label{evK} +EV = log_2(\frac{L \times S}{K}) +\end{equation}$$ + +That constant $K$ is the reflected-light meter constant, which varies between manufacturers. We could find two common values for this constant: 12.5, used by Canon, Nikon and Sekonic, and 14, used by Pentax and Minolta. Given the wide availability of Canon and Nikon cameras, as well as our own usage of Sekonic light meters, we will choose to use $ K = 12.5 $. + +Since we want to work with $ EV_{100} $, we can substitute $K$ and $S$ in equation $ \ref{evK} $ to obtain equation $ \ref{ev100L} $. + +$$\begin{equation}\label{ev100L} +EV = log_2(L \frac{100}{12.5}) +\end{equation}$$ + +Given this relationship, it would be possible to implement automatic exposure in our engine by first measuring the average luminance of a frame. An easy way to achieve this is to simply downsample a luminance buffer down to 1 pixel and read the remaining value. This technique is unfortunately rarely stable and can easily be affected by extreme values. Many games use a different approach which consists in using a luminance histogram to remove extreme values. + +For validation and testing purposes, the luminance can be computed from a given EV: + +$$\begin{equation} +L = 2^{EV_{100}} \times \frac{12.5}{100} = 2^{EV_{100} - 3} +\end{equation}$$ + +#### Exposure value and illuminance + +It is possible to define EV as a function of the illuminance $E$, given a per-device calibration constant $C$: + +$$\begin{equation}\label{evC} +EV = log_2(\frac{E \times S}{C}) +\end{equation}$$ + +The constant $C$ is the incident-light meter constant, which varies between manufacturers and/or types of sensors. There are two common types of sensors: flat and hemispherical. For flat sensors, a common value is 250. With hemispherical sensors, we could find two common values: 320, used by Minolta, and 340, used by Sekonic. + +Since we want to work with $ EV_{100} $, we can substitute $S$ $ \ref{evC} $ to obtain equation $ \ref{ev100C} $. + +$$\begin{equation}\label{ev100C} +EV = log_2(E \frac{100}{C}) +\end{equation}$$ + +The illuminance can then be computed from a given EV. For a flat sensor with $ C = 250 $ we obtain equation $ \ref{eFlatSensor} $. + +$$\begin{equation}\label{eFlatSensor} +E = 2^{EV_{100}} \times 2.5 +\end{equation}$$ + +For a hemispherical sensor with $ C = 340 $ we obtain equation $ \ref{eHemisphereSensor} $ + +$$\begin{equation}\label{eHemisphereSensor} +E = 2^{EV_{100}} \times 3.4 +\end{equation}$$ + +#### Exposure compensation + +Even though an exposure value actually indicates combinations of camera settings, it is often used by photographers to describe light intensity. This is why cameras let photographers apply an exposure compensation to over or under-expose an image. This setting can be used for artistic control but also to achieve proper exposure (snow for instance will be exposed for as 18% middle-gray). + +Applying an exposure compensation $EC$ is a simple as adding an offset to the exposure value, as shown in equation $ \ref{ec} $. + +$$\begin{equation}\label{ec} +EV_{100}' = EV_{100} - EC +\end{equation}$$ + +This equation uses a negative sign because we are using $EC$ in f-stops to adjust the final exposure. Increasing the EV is akin to closing down the aperture of the lens (or reducing shutter speed or reducing sensitivity). A higher EV will produce darker images. + +### Exposure + +To convert the scene luminance into normalized luminance, we must use the [photometric exposure](https://en.wikipedia.org/wiki/Exposure_value#Camera_settings_vs._photometric_exposure) (or luminous exposure), or amount of scene luminance that reaches the camera sensor. The photometric exposure, expressed in lux seconds and noted $H$, is given by equation $ \ref{photometricExposure} $. + +$$\begin{equation}\label{photometricExposure} +H = \frac{q \cdot t}{N^2} L +\end{equation}$$ + +Where $L$ is the luminance of the scene, $t$ the shutter speed, $N$ the aperture and $q$ the lens and vignetting attenuation (typically $ q = 0.65 $[^lensAttenuation]). This definition does not take the sensor sensitivity into account. To do so, we must use one of the three ways to relate photometric exposure and sensitivity: saturation-based speed, noise-based speed and standard output sensitivity. + +We choose the saturation-based speed relation, which gives us $ H_{sat} $, the maximum possible exposure that does not lead to clipped or bloomed camera output (equation $ \ref{hSat} $). + +$$\begin{equation}\label{hSat} +H_{sat} = \frac{78}{S_{sat}} +\end{equation}$$ + +We combine equations $ \ref{hSat} $ and $ \ref{photometricExposure} $ in equation $ \ref{lmax} $ to compute the maximum luminance $ L_{max} $ that will saturate the sensor given exposure settings $S$, $N$ and $t$. + +$$\begin{equation}\label{lmax} +L_{max} = \frac{N^2}{q \cdot t} \frac{78}{S} +\end{equation}$$ + +This maximum luminance can then be used to normalize incident luminance $L$ as shown in equation $ \ref{normalizedLuminance} $. + +$$\begin{equation}\label{normalizedLuminance} +L' = L \frac{1}{L_{max}} +\end{equation}$$ + +$ L_{max} $ can be simplified using equation $ \ref{ev} $, $ S = 100 $ and $ q = 0.65 $: + +$$\begin{align*} +L_{max} &= \frac{N^2}{t} \frac{78}{q \cdot S} \\ +L_{max} &= 2^{EV_{100}} \frac{78}{q \cdot S} \\ +L_{max} &= 2^{EV_{100}} \times 1.2 +\end{align*}$$ + +Listing [fragmentExposure] shows how the exposure term can be applied directly to the pixel color computed in a fragment shader. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Computes the camera's EV100 from exposure settings +// aperture in f-stops +// shutterSpeed in seconds +// sensitivity in ISO +float exposureSettings(float aperture, float shutterSpeed, float sensitivity) { + return log2((aperture * aperture) / shutterSpeed * 100.0 / sensitivity); +} + +// Computes the exposure normalization factor from +// the camera's EV100 +float exposure(float ev100) { + return 1.0 / (pow(2.0, ev100) * 1.2); +} + +float ev100 = exposureSettings(aperture, shutterSpeed, sensitivity); +float exposure = exposure(ev100); + +vec4 color = evaluateLighting(); +color.rgb *= exposure; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [fragmentExposure]: Implementation of exposure in GLSL] + +In practice the exposure factor can be pre-computed on the CPU to save shader instructions. + +[^lensAttenuation]: See *Film Speed, Measurements and calculations* on Wikipedia (https://en.wikipedia.org/wiki/Film_speed) + +### Automatic exposure + +The process described above relies on artists setting the camera exposure settings manually. This can prove cumbersome in practice since camera movements and/or dynamic effects can greatly affect the scene's luminance. Since we know how to compute the exposure value from a given luminance (see section [Exposure value and luminance]), we can transform our camera into a spot meter. To do so, we need to measure the scene's luminance. + +There are two common techniques used to measure the scene's luminance: + +- **Luminance downsampling**, by downsampling the previous frame successively until obtaining a 1x1 log luminance buffer that can be read on the CPU (this could also be achieved using a compute shader). The result is the average log luminance of the scene. The first downsampling must extract the luminance of each pixel first. This technique can be unstable and its output should be smoothed over time. +- **Using a luminance histogram**, to find the average log luminance. This technique has an advantage over the previous one as it allows to ignore extreme values and offers more stable results. + +Note that both methods will find the average luminance after multiplication by the albedo. This is not entirely correct but the alternative is to keep a luminance buffer that contains the luminance of each pixel before multiplication by the surface albedo. This is expensive both computationally and memory-wise. + +These two techniques also limit the metering system to average metering, where each pixel has the same influence (or weight) over the final exposure. Cameras typically offer 3 modes of metering: + +Spot metering +: In which only a small circle in the center of the image contributes to the final exposure. That circle is usually 1 to 5% of the total image size. + +Center-weighted metering +: Gives more influence to scene luminance values located in the center of the screen. + +Multi-zone or matrix metering +: A metering mode that differs for each manufacturer. The goal of this mode is to prioritize exposure for the most important parts of the scene. This is often achieved by splitting the image into a grid and by classifying each cell (using focus information, min/max luminance, etc.). Advanced implementations attempt to compare the scene to a known dataset to achieve proper exposure (backlit sunset, overcast snowy day, etc.). + +#### Spot metering + +The weight $w$ of each luminance value to use when computing the scene luminance is given by equation $ \ref{spotMetering} $. + +$$\begin{equation}\label{spotMetering} +w(x,y) = \begin{cases} 1 & \left| p_{x,y} - s_{x,y} \right| \le s_r \\ 0 & \left| p_{x,y} - s_{x,y} \right| \gt s_r \end{cases} +\end{equation}$$ + +Where $p$ is the position of the pixel, $s$ the center of the spot and $ s_r $ the radius of the spot. + +#### Center-weighted metering + +$$\begin{equation}\label{centerMetering} +w(x,y) = smooth(\left| p_{x,y} - c \right| \times \frac{2}{width} ) +\end{equation}$$ + +Where $c$ is the center of the time and $ smooth() $ a smoothing function such as GLSL's `smoothstep()`. + +#### Adaptation + +To smooth the result of the metering, we can use equation $ \ref{adaptation} $, an exponential feedback loop as described by Pattanaik et al. in [Pattanaik00]. + +$$\begin{equation}\label{adaptation} +L_{avg} = L_{avg} + (L - L_{avg}) \times (1 - e^{-\Delta t \cdot \tau}) +\end{equation}$$ + +Where $ \Delta t $ is the delta time from the previous frame and $\tau$ a constant that controls the adaptation rate. + +### Bloom + +Because the EV scale is almost perceptually linear, the exposure value is also often used as a light unit. This means we could let artists specify the intensity of lights or emissive surfaces using exposure compensation as a unit. The intensity of emitted light would therefore be relative to the exposure settings. Using exposure compensation as a light unit should be avoided whenever possible but can be useful to force (or cancel) a bloom effect around emissive surfaces independently of the camera settings (for instance, a lightsaber in a game should always bloom). + +![Figure [bloom]: Saturated photosites on a sensor create a blooming effect in the bright parts of the scene](images/screenshot_bloom.jpg) + +With $c$ the bloom color and $ EV_{100} $ the current exposure value, we can easily compute the luminance of the bloom value as show in equation $ \ref{bloomEV} $. + +$$\begin{equation}\label{bloomEV} +EV_{bloom} = EV_{100} + EC \\ +L_{bloom} = c \times 2^{EV_{bloom} - 3} +\end{equation}$$ + +Equation $ \ref{bloomEV} $ can be used in a fragment shader to implement emissive blooms, as shown in listing [fragmentEmissive]. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +vec4 surfaceShading() { + vec4 color = evaluateLights(); + // rgb = color, w = exposure compensation + vec4 emissive = getEmissive(); + color.rgb += emissive.rgb * pow(2.0, ev100 + emissive.w - 3.0); + color.rgb *= exposure; + return color; +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [fragmentEmissive]: Implementation of emissive bloom in GLSL] + +## Optics post-processing + +### Color fringing + +[TODO] + +![Figure [fringing]: Example of color fringing: look at the ear on the left or the chin at the bottom.](images/screenshot_fringing.jpg) + +### Lens flares + +[TODO] Notes: there is a physically based approach to generating lens flares, by tracing rays through the optical assembly of the lens, but we are going to use an image-based approach. This approach is cheaper and has a few welcome benefits such as free emitters occlusion and unlimited light sources support. + +## Filmic post-processing + +[TODO] Perform post-processing on the scene referred data (linear space, before tone-mapping) as much as possible + +It is important to provide color correction tools to give artists greater artistic control over the final image. These tools are found in every photo or video processing application, such as Adobe Photoshop or Adobe After Effects. + +### Contrast + +### Curves + +### Levels + +### Color grading + +## Light path + +The light path, or rendering method, used by the engine can have serious performance implications and may impose strong limitations on how many lights can be used in a scene. There are traditionally two different rendering methods used by 3D engines forward and deferred rendering. + +Our goal is to use a rendering method that obeys the following constraints: + +- Low bandwidth requirements +- Multiple dynamic lights per pixel + +Additionally, we would like to easily support: + +- MSAA +- Transparency +- Multiple material models + +Deferred rendering is used by many modern 3D rendering engines to easily support dozens, hundreds or even thousands of light source (amongst other benefits). This method is unfortunately very expensive in terms of bandwidth. With our default PBR material model, our G-buffer would use between 160 and 192 bits per pixel, which would translate directly to rather high bandwidth requirements. + +Forward rendering methods on the other hand have historically been bad at handling multiple lights. A common implementation is to render the scene multiple times, once per visible light, and to blend (add) the results. Another technique consists in assigning a fixed maximum of lights to each object in the scene. This is however impractical when objects occupy a vast amount of space in the world (building, road, etc.). + +Tiled shading can be applied to both forward and deferred rendering methods. The idea is to split the screen in a grid of tiles and for each tile, find the list of lights that affect the pixels within that tile. This has the advantage of reducing overdraw (in deferred rendering) and shading computations of large objects (in forward rendering). This technique suffers however from depth discontinuities issues that can lead to large amounts of extraneous work. + +The scene displayed in figure [sponza] was rendered using clustered forward rendering. + +![Figure [sponza]: Clustered forward rendering with dozens of dynamic lights and MSAA](images/screenshot_sponza.jpg) + +Figure [sponzaTiles] shows the same scene split in tiles (in this case, a 1280x720 render target with 80x80px tiles). + +![Figure [sponzaTiles]: Tiled shading (16x9 tiles)](images/screenshot_sponza_tiles.jpg) + +### Clustered Forward Rendering + +We decided to explore another method called Clustered Shading, in its forward variant. Clustered shading expands on the idea of tiled rendering but adds a segmentation on the 3rd axis. The “clustering” is done in view space, by splitting the frustum into a 3D grid. + +The frustum is first sliced on the depth axis as show in figure [sponzaSlices]. + +![Figure [sponzaSlices]: Depth slicing (16 slices)](images/screenshot_sponza_slices.jpg) + +And the depth slices are then combined with the screen tiles to "voxelize" the frustum. We call each cluster a froxel as it makes it clear what they represent (a voxel in frustum space). The result of the "froxelization" pass is shown in figure [froxel1] and figure [froxel2]. + +![Figure [froxel1]: Frustum voxelization (5x3 tiles, 8 depth slices)](images/screenshot_sponza_froxels1.jpg) + +![Figure [froxel2]: Frustum voxelization (5x3 tiles, 8 depth slices)](images/screenshot_sponza_froxels2.jpg) + +Before rendering a frame, each light in the scene is assigned to any froxel it intersects with. The result of the lights assignment pass is a list of lights for each froxel. During the rendering pass, we can compute the ID of the froxel a fragment belongs to and therefore the list of lights that can affect that fragment. + +The depth slicing is not linear, but exponential. In a typical scene, there will be more pixels close to the near plane than to the far plane. An exponential grid of froxels will therefore improve the assignment of lights where it matters the most. + +Figure [froxelDistribution] shows how much world space unit each depth slice uses with exponential slicing. + +![Figure [froxelDistribution]: Near: 0.1m, Far: 100m, 16 slices](images/diagram_froxels1.png) + +A simple exponential voxelization is unfortunately not enough. The graphic above clearly illustrates how world space is distributed across slices but it fails to show what happens close to the near plane. If we examine the same distribution in a smaller range (0.1m to 7m) we can see an interesting problem appear as shown in figure [froxelDistributionClose]. + +![Figure [froxelDistributionClose]: Depth distribution in the 0.1-7m range](images/diagram_froxels2.png) + +This graphic shows that a simple exponential distribution uses up half of the slices very close to the camera. In this particular case, we use 8 slices out of 16in the first 5 meters. Since dynamic world lights are either point lights (spheres) or spot lights (cones), such a fine resolution is completely unnecessary so close to the near plane. + +Our solution is to manually tweak the size of the first froxel depending on the scene and the near and far planes. By doing so, we can better distribute the remaining froxels across the frustum. Figure [froxelDistributionExp] shows for instance what happens when we use a special froxel between 0.1m and 5m. + +![Figure [froxelDistributionExp]: Near: 0.1, Far: 100m, 16 slices, Special froxel: 0.1-5m](images/diagram_froxels3.png) + +This new distribution is much more efficient and allows a better assignment of the lights throughout the entire frustum. + +### Implementation notes + +Lights assignment can be done in two different ways, on the GPU or on the CPU. + +#### GPU lights assignment + +This implementation requires OpenGL ES 3.1 and support for compute shaders. The lights are stored in Shader Storage Buffer Objects (SSBO) and passed to a compute shader that assigns each light to the corresponding froxels. + +The frustum voxelization can be executed only once by a first compute shader (as long as the projection matrix does not change), and the lights assignment can be performed each frame by another compute shader. + +The threading model of compute shaders is particularly well suited for this task. We simply invoke as many workgroups as we have froxels (we can directly map the X, Y and Z workgroup counts to our froxel grid resolution). Each workground will in turn be threaded and traverse all the lights to assign. + +Intersection tests imply simple sphere/frustum or cone/frustum tests. + +See the annex for the source code of a GPU implementation (point lights only). + +#### CPU lights assignment + +On non-OpenGL ES 3.1 devices, lights assignment can be performed efficiently on the CPU. The algorithm is different from the GPU implementation. Instead of iterating over every light for each froxel, the engine will “rasterize” each light as froxels. For instance, given a point light’s center and radius, it is trivial to compute the list of froxels it intersects with. + +This technique has the added benefit of providing tighter culling than in the GPU variant. The CPU implementation can also more easily generate a packed list of lights. + +#### Shading + +The list of lights per froxel can be passed to the fragment shader either as an SSBO (OpenGL ES 3.1) or a texture. + +#### From depth to froxel + +Given a near plane $n$, a far plane $f$, a maximum number of depth slices $m$ and a linear depth value $z$ in the range [0..1], equation $\ref{zToCluster}$ can be used to compute the index of the cluster for a given position. + +$$\begin{equation}\label{zToCluster} +zToCluster(z,n,f,m)=floor \left( max \left( log2(z) \frac{m}{-log2(\frac{n}{f})} + m, 0 \right) \right) +\end{equation}$$ + +This formula suffers however from the resolution issue mentioned previously. We can fix it by introducing $sn$, a special near value that defines the extent of the first froxel (the first froxel occupies the range [n..sn], the remaining froxels [sn..f]). + +$$\begin{equation}\label{zToClusterFix} +zToCluster(z,n,sn,f,m)=floor \left( max \left( log2(z) \frac{m-1}{-log2(\frac{sn}{f})} + m, 0 \right) \right) +\end{equation}$$ + +Equation $\ref{linearZ}$ can be used to compute a linear depth value from `gl_FragCoord.z` (assuming a standard OpenGL projection matrix). + +$$\begin{equation}\label{linearZ} +linearZ(z)=\frac{n}{f+z(n-f)} +\end{equation}$$ + +This equation can be simplified by pre-computing two terms $c0$ and $c1$, as shown in equation $\ref{linearZFix}$. + +$$\begin{equation}\label{linearZFix} +c1 = \frac{f}{n} \\ +c0 = 1 - c1 \\ +linearZ(z)=\frac{1}{z \cdot c0 + c1} +\end{equation}$$ + +This simplification is important because we pass the linear z value to a `log2` in $\ref{zToClusterFix}$. Since the division becomes a negation under a logarithmic, we can avoid a division by using $-log2(z \cdot c0 + c1)$ instead. + +All put together, computing the froxel index of a given fragment can be implemented fairly easily as shown in listing [fragCoordToFroxel]. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +#define MAX_LIGHT_COUNT 16 // max number of lights per froxel + +uniform uvec4 froxels; // res x, res y, count y, count y +uniform vec4 zParams; // c0, c1, index scale, index bias + +uint getDepthSlice() { + return uint(max(0.0, log2(zParams.x * gl_FragCoord.z + zParams.y) * + zParams.z + zParams.w)); +} + +uint getFroxelOffset(uint depthSlice) { + uvec2 froxelCoord = uvec2(gl_FragCoord.xy) / froxels.xy; + froxelCoord.y = (froxels.w - 1u) - froxelCoord.y; + + uint index = froxelCoord.x + froxelCoord.y * froxels.z + + depthSlice * froxels.z * froxels.w; + return index * MAX_FROXEL_LIGHT_COUNT; +} + +uint slice = getDepthSlice(); +uint offset = getFroxelOffset(slice); + +// Compute lighting... +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [fragCoordToFroxel]: GLSL implementation to compute a froxel index from a fragment's screen coordinates] + +Several uniforms must be pre-computed for perform the index evaluation efficiently. The code used to pre-compute these uniforms can be found in listing [froxelIndexPrecomputation]. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +froxels[0] = TILE_RESOLUTION_IN_PX; +froxels[1] = TILE_RESOLUTION_IN_PX; +froxels[2] = numberOfTilesInX; +froxels[3] = numberOfTilesInY; + +zParams[0] = 1.0f - Z_FAR / Z_NEAR; +zParams[1] = Z_FAR / Z_NEAR; +zParams[2] = (MAX_DEPTH_SLICES - 1) / log2(Z_SPECIAL_NEAR / Z_FAR); +zParams[3] = MAX_DEPTH_SLICES; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [froxelIndexPrecomputation]] + +#### From froxel to depth + +Given a froxel index $i$, a special near plane $sn$, a far plane $f$ and a maximum number of depth slices $m$, equation $\ref{clusterToZ}$ computes the minimum depth of a given froxel. + +$$\begin{equation}\label{clusterToZ} +clusterToZ(i \ge 1,sn,f,m)=2^{(i-m) \frac{-log2(\frac{sn}{f})}{m-1}} +\end{equation}$$ + +For $i=0$, the z value is 0. The result of this equation is in the [0..1] range and should be multiplied by $f$ to get a distance in world units. + +The compute shader implementation should use `exp2` instead of a `pow`. The division can be precomputed and passed as a uniform. + +## Validation + +Given the complexity of our lighting system, it is important to validate our implementation. We will do so in several ways: using reference renderings, light measurements and data visualization. + +[TODO] Explain light measurement validation (reading EV from the render target and comparing against values measure with light meters/cameras, etc.) + +### Scene referred visualization + +A quick and easy way to validate a scene's lighting is to modify the shader to output colors that provide an intuitive mapping to relevant data. This can easily be done by using a custom debug tone-mapping operator that outputs fake colors. + +#### Luminance stops + +With emissive materials and IBLs, it is fairly easy to obtain a scene in which specular highlights are brighter than their apparent caster. This type of issue can be difficult to observe after tone-mapping and quantization but is fairly obvious in the scene-referred space. Figure [luminanceViz] shows how the custom operator described in listing [tonemapLuminanceViz] is used to show the exposed luminance of a scene. + +![Figure [luminanceViz]: Visualizing luminance by color coding the stops: cyan is middle gray, blue is 1 stop darker, green 1 stop brighter, etc.](images/screenshot_luminance_debug.png) + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +vec3 Tonemap_DisplayRange(const vec3 x) { + // The 5th color in the array (cyan) represents middle gray (18%) + // Every stop above or below middle gray causes a color shift + float v = log2(luminance(x) / 0.18); + v = clamp(v + 5.0, 0.0, 15.0); + int index = int(floor(v)); + return mix(debugColors[index], debugColors[min(15, index + 1)], fract(v)); +} + +const vec3 debugColors[16] = vec3[]( + vec3(0.0, 0.0, 0.0), // black + vec3(0.0, 0.0, 0.1647), // darkest blue + vec3(0.0, 0.0, 0.3647), // darker blue + vec3(0.0, 0.0, 0.6647), // dark blue + vec3(0.0, 0.0, 0.9647), // blue + vec3(0.0, 0.9255, 0.9255), // cyan + vec3(0.0, 0.5647, 0.0), // dark green + vec3(0.0, 0.7843, 0.0), // green + vec3(1.0, 1.0, 0.0), // yellow + vec3(0.90588, 0.75294, 0.0), // yellow-orange + vec3(1.0, 0.5647, 0.0), // orange + vec3(1.0, 0.0, 0.0), // bright red + vec3(0.8392, 0.0, 0.0), // red + vec3(1.0, 0.0, 1.0), // magenta + vec3(0.6, 0.3333, 0.7882), // purple + vec3(1.0, 1.0, 1.0) // white +); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [tonemapLuminanceViz]: GLSL implementation of a custom debug tone-mapping operator for luminance visualization] + +### Reference renderings + +To validate our implementation against reference renderings, we will use a commercial-grade Open Source physically based offline path tracer called Mitsuba. Mitsuba offers many different integrators, samplers and material models, which should allow us to provide fair comparisons with our real-time renderer. This path tracer also relies on a simple XML scene description format that should be easy to automatically generate from our own scene descriptions. + +Figure [mitsubaReference] and figure [filamentReference] show a simple scene, a perfectly smooth dielectric sphere, rendered respectively with Mitsuba and Filament. + +![Figure [mitsubaReference]: Rendered in 2048x1440 in 1 minute and 42 seconds on a 12 core 2013 MacPro](images/screenshot_ref_mitsuba.jpg) + +![Figure [filamentReference]: Rendered in 2048x1440 with MSAA 4x at 60 fps on a Nexus 9 device (Tegra K1 GPU)](images/screenshot_ref_filament.jpg) + +The parameters used to render both scenes are the following: + +**Filament** + +- Material + - Base color: sRGB 0.81, 0, 0 + - Metallic: 0 + - Roughness: 0 + - Reflectance: 0.5 +- Indirect light: IBL + - 256x256 cubemap generated by cmgen from office.exr + - Multiplier: 35,000 +- Direct light: directional light + - Linear color: 1.0, 0.96, 0.95 + - Intensity: 120,000 lux +- Exposure + - Aperture: f/16 + - Shutter speed: 1/125s + - ISO: 100 + +**Mitsuba** + +- BSDF: roughplastic + - Distribution: GGX + - Alpha: 0 + - Diffuse reflectance: sRGB 0.81, 0, 0 +- Emitter: environment map + - Source: office.exr + - Scale: 35,000 +- Emitter: directional + - Irradiance: linear RGB 120,000 115,200 114,000 +- Film: LDR + - Exposure: -15.23, computed from log2(filamentExposure) +- Integrator: path +- Sampler: ldsampler + - Sample count: 256 + +The full Mitsuba scene can be found as an annex. Both scenes were rendered at the same resolution (2048x1440). + +#### Comparison + +The slight differences between the two renderings come from the various approximations used by Filament: RGBM 256x256 reflection probe, RGBM 1024x1024 background map, Lambert diffuse, split-sum approximation, analytical approximation of the DFG term, etc. + +Figure [referenceComparison] shows the luminance gradient of the images produced by both engines. The comparison was performed on LDR images. + +![Figure [referenceComparison]: Luminance gradients from Mitsuba (left) and Filament (right)](images/screenshot_ref_comparison.png) + +The biggest difference is visible at grazing angles, which is most likely explained by Filament's use of a Lambertian diffuse term. The Disney diffuse term and its grazing retro-reflections would move Filament closer to Mitsuba. + +## Coordinates systems + +### World coordinates system + +Filament uses a Y-up, right-handed coordinate system. + +![Figure [coordinates]: Red +X, green +Y, blue +Z (rendered in Marmoset Toolbag).](images/screenshot_coordinates.jpg) + + +### Camera coordinates system + +Filament's Camera looks towards its local -Z axis. That is, when placing a camera in the world +without any transform applied to it, the camera looks down the world's -Z axis. + + +### Cubemaps coordinates system + +All cubemaps used in Filament follow the OpenGL convention for face +alignment shown in figure [cubemapCoordinates]. + +![Figure [cubemapCoordinates]: Horizontal cross representation of a cubemap following the OpenGL faces alignment convention.](images/screenshot_cubemap_coordinates.png) + +Note that environment background and reflection probes are mirrored (see section [Mirroring]). + + +#### Mirroring + +To simplify the rendering of reflections, IBL cubemaps are stored mirrored on the X axis. This is +the default behaviour of the `cmgen` tool. This means that an IBL cubemap used as environment +background needs to be mirrored again at runtime. +An easy way to achieve this for skyboxes is to use textured back faces. Filament does +this by default. + + +#### Equirectangular environment maps + +To convert equirectangular environment maps to horizontal/vertical cross cubemaps we position the ++Z face in the center of the source rectilinear environment map. + + +#### World space orientation of environment maps and Skyboxes + +When specifying a skybox or an IBL in Filament, the specified cubemap is oriented such that its +-Z face points towards the +Z axis of the world (this is because filament assumes mirrored cubemaps, +see section [Mirroring]). However, because environments and skyboxes are expected to be pre-mirrored, +their -Z (back) face points towards the world's -Z axis as expected (and the camera looks toward that +direction by default, see section [Camera coordinates system]). + + +# Annex + +## Specular color + +The specular color of a metallic surface, or $\fNormal$, can be computed directly from measured spectral data. Online databases such as [Refractive Index](https://refractiveindex.info/?shelf=3d&book=metals&page=brass) provide tables of complex IOR measured at different wavelengths for various materials. + +Earlier in this document, we presented equation $\ref{fresnelEquation}$ to compute the Fresnel reflectance at normal incidence for a dielectric surface given its IOR. The same equation can be rewritten for conductors by using complex numbers to represent the surface's IOR: + +$$\begin{equation} +c_{ior} = n_{ior} + ik +\end{equation}$$ + +Equation $\ref{fresnelComplexIOR}$ presents the resulting Fresnel formula, where $c^*$ is the conjugate of the complex number $c$: + +$$\begin{equation}\label{fresnelComplexIOR} +\fNormal(c_{ior}) = \frac{(c_{ior} - 1)(c_{ior}^* - 1)}{(c_{ior} + 1)(c_{ior}^* + 1)} +\end{equation}$$ + +To compute the specular color of a material we need to evaluate the complex Fresnel equation at each spectral sample of complex IOR over the visible spectrum. For each spectral sample, we obtain a spectral reflectance sample. To find the RGB color at normal incidence, we must multiply each sample by the CIE XYZ CMFs (color matching functions) and the spectral power distribution of the desired illuminant. We choose the standard illuminant D65 because we want to compute a color in the sRGB color space. + +We then sum (integrate) and normalize all the samples to obtain $\fNormal$ in the XYZ color space. From there, a simple color space conversion yields a linear sRGB color or a non-linear sRGB color after applying the opto-electronic transfer function (OETF, commonly known as "gamma" curve). Note that for some materials such as gold the final sRGB color might fall out of gamut. We use a simple normalization step as a cheap form of gamut remapping but it would be interesting to consider computing values in a color space with a wider gamut (for instance BT.2020). + +To achieve the desired result we used the ICE 1931 2 degrees CMFs, from 360nm to 830nm at 1nm intervals ([source](http://cvrl.ioo.ucl.ac.uk/cmfs.htm)), and the CIE Standard Illuminant D65 relative spectral power distribution, from 300nm to 830nm, at 5nm intervals ([source](http://files.cie.co.at/204.xls)). + +Our implementation is presented in listing [specularColorImpl], with the actual data omitted for brevity. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// CIE 1931 2-deg color matching functions (CMFs), from 360nm to 830nm, +// at 1nm intervals +// +// Data source: +// http://cvrl.ioo.ucl.ac.uk/cmfs.htm +// http://cvrl.ioo.ucl.ac.uk/database/text/cmfs/ciexyz31.htm +const size_t CIE_XYZ_START = 360; +const size_t CIE_XYZ_COUNT = 471; +const float3 CIE_XYZ[CIE_XYZ_COUNT] = { ... }; + +// CIE Standard Illuminant D65 relative spectral power distribution, +// from 300nm to 830, at 5nm intervals +// +// Data source: +// https://en.wikipedia.org/wiki/Illuminant_D65 +// https://cielab.xyz/pdf/CIE_sel_colorimetric_tables.xls +const size_t CIE_D65_INTERVAL = 5; +const size_t CIE_D65_START = 300; +const size_t CIE_D65_END = 830; +const size_t CIE_D65_COUNT = 107; +const float CIE_D65[CIE_D65_COUNT] = { ... }; + +struct Sample { + float w = 0.0f; // wavelength + std::complex ior; // complex IOR, n + ik +}; + +static float illuminantD65(float w) { + auto i0 = size_t((w - CIE_D65_START) / CIE_D65_INTERVAL); + uint2 indexBounds{i0, std::min(i0 + 1, CIE_D65_END)}; + + float2 wavelengthBounds = CIE_D65_START + float2{indexBounds} * CIE_D65_INTERVAL; + float t = (w - wavelengthBounds.x) / (wavelengthBounds.y - wavelengthBounds.x); + return lerp(CIE_D65[indexBounds.x], CIE_D65[indexBounds.y], t); +} + +// For std::lower_bound +bool operator<(const Sample& lhs, const Sample& rhs) { + return lhs.w < rhs.w; +} + +// The wavelength w must be between 360nm and 830nm +static std::complex findSample(const std::vector& samples, float w) { + auto i1 = std::lower_bound( + samples.begin(), samples.end(), Sample{w, 0.0f + 0.0if}); + auto i0 = i1 - 1; + + // Interpolate the complex IORs + float t = (w - i0->w) / (i1->w - i0->w); + float n = lerp(i0->ior.real(), i1->ior.real(), t); + float k = lerp(i0->ior.imag(), i1->ior.imag(), t); + return { n, k }; +} + +static float fresnel(const std::complex& sample) { + return (((sample - (1.0f + 0if)) * (std::conj(sample) - (1.0f + 0if))) / + ((sample + (1.0f + 0if)) * (std::conj(sample) + (1.0f + 0if)))).real(); +} + +static float3 XYZ_to_sRGB(const float3& v) { + const mat3f XYZ_sRGB{ + 3.2404542f, -0.9692660f, 0.0556434f, + -1.5371385f, 1.8760108f, -0.2040259f, + -0.4985314f, 0.0415560f, 1.0572252f + }; + return XYZ_sRGB * v; +} + +// Outputs a linear sRGB color +static float3 computeColor(const std::vector& samples) { + float3 xyz{0.0f}; + float y = 0.0f; + + for (size_t i = 0; i < CIE_XYZ_COUNT; i++) { + // Current wavelength + float w = CIE_XYZ_START + i; + + // Find most appropriate CIE XYZ sample for the wavelength + auto sample = findSample(samples, w); + // Compute Fresnel reflectance at normal incidence + float f0 = fresnel(sample); + + // We need to multiply by the spectral power distribution of the illuminant + float d65 = illuminantD65(w); + + xyz += f0 * CIE_XYZ[i] * d65; + y += CIE_XYZ[i].y * d65; + } + + // Normalize so that 100% reflectance at every wavelength yields Y=1 + xyz /= y; + + float3 linear = XYZ_to_sRGB(xyz); + + // Normalize out-of-gamut values + if (any(greaterThan(linear, float3{1.0f}))) linear *= 1.0f / max(linear); + + return linear; +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [specularColorImpl]: C++ implementation to compute the base color of a metallic surface from spectral data] + +Special thanks to Naty Hoffman for his valuable help on this topic. + +## Importance sampling for the IBL + +In the discrete domain, the integral can be approximated with sampling as defined in equation $\ref{iblSampling}$. + +$$\begin{equation}\label{iblSampling} +\Lout(n,v,\Theta) \equiv \frac{1}{N} \sum_{i}^{N} f(l_{i}^{uniform},v,\Theta) L_{\perp}(l_i) \left< n \cdot l_i^{uniform} \right> +\end{equation}$$ + +Unfortunately, we would need too many samples to evaluate this integral. A technique commonly used +is to choose samples that are more "important" more often, this is called _importance sampling_. +In our case we'll use the distribution of micro-facets normals, $D_{ggx}$, as the distribution of +important samples. + +The evaluation of $ \Lout(n,v,\Theta) $ with importance sampling is presented in equation $\ref{annexIblImportanceSampling}$. + +$$\begin{equation}\label{annexIblImportanceSampling} +\Lout(n,v,\Theta) \equiv \frac{1}{N} \sum_{i}^{N} \frac{f(l_{i},v,\Theta)}{p(l_i,v,\Theta)} L_{\perp}(l_i) \left< n \cdot l_i \right> +\end{equation}$$ + +In equation $\ref{annexIblImportanceSampling}$, $p$ is the probability density function (PDF) of the +distribution of _important direction samples_ $l_i$. These samples depend on $h_i$, $v$ and $\alpha$. +The definition of the PDF is shown in equation $\ref{iblPDF}$. + +$h_i$ is given by the distribution we chose, see section [Choosing important directions] for more details. + +The _important direction samples_ $l_i$ are calculated as the reflection of $v$ around $h_i$, and therefore +**do not** have the same PDF as $h_i$. The PDF of a transformed distribution is given by: + +$$\begin{equation} +p(T_r(x)) = p(x) |J(T_r)|^{-1} +\end{equation}$$ + +Where $|J(T_r)|$ is the determinant of the Jacobian of the transform. In our case we're considering +the transform from $h_i$ to $l_i$ and the determinant of its Jacobian is given in \ref{iblPDF}. + +$$\begin{equation}\label{iblPDF} +p(l,v,\Theta) = D(h,\alpha) \left< \NoH \right> |J_{h \rightarrow l}|^{-1} \\ +|J_{h \rightarrow l}| = 4 \left< \VoH \right> +\end{equation}$$ + +### Choosing important directions + +Refer to section [Choosing important directions for sampling the BRDF] for more details. Given a uniform distribution $(\zeta_{\phi},\zeta_{\theta})$ the important direction $l$ is defined by equation $\ref{importantDirection}$. + +$$\begin{equation}\label{importantDirection} +\phi = 2 \pi \zeta_{\phi} \\ +\theta = cos^{-1} \sqrt{\frac{1 - \zeta_{\theta}}{(\alpha^2 - 1)\zeta_{\theta}+1}} \\ +l = \{ cos \phi sin \theta, sin \phi sin \theta, cos \theta \} +\end{equation}$$ + +Typically, $ (\zeta_{\phi},\zeta_{\theta}) $ are chosen using the Hammersley uniform distribution algorithm described in section [Hammersley sequence]. + +### Pre-filtered importance sampling + +Importance sampling considers only the PDF to generate important directions; in particular, it is oblivious to the actual content of the IBL. If the latter contains high frequencies in areas without a lot of samples, the integration won’t be accurate. This can be somewhat mitigated by using a technique called _pre-filtered importance sampling_, in addition this allows the integral to converge with many fewer samples. + +Pre-filtered importance sampling uses several images of the environment increasingly low-pass filtered. This is typically implemented very efficiently with mipmaps and a box filter. The LOD is selected based on the sample importance, that is, low probability samples use a higher LOD index (more filtered). + +This technique is described in details in [#Krivanek08]. + +The cubemap LOD is determined in the following way: + +$$\begin{align*} +lod &= log_4 \left( K\frac{\Omega_s}{\Omega_p} \right) \\ +K &= 4.0 \\ +\Omega_s &= \frac{1}{N \cdot p(l_i)} \\ +\Omega_p &\approx \frac{4\pi}{6 \cdot width \cdot height} +\end{align*}$$ + +Where $K$ is a constant determined empirically, $p$ the PDF of the BRDF, $ \Omega_{s} $ the solid angle associated to the sample and $\Omega_p$ the solid angle associated with the texel in the cubemap. + +Cubemap sampling is done using seamless trilinear filtering. It is extremely important to sample the cubemap correctly across faces using OpenGL's seamless sampling feature or any other technique that avoids/reduces seams. + +Table [importanceSamplingViz] shows a comparison between importance sampling and pre-filtered importance sampling when applied to figure [importanceSamplingRef]. + +![Figure [importanceSamplingRef]: Importance sampling image reference](images/image_is_original.png) + + + Samples | Importance sampling | Pre-filtered importance sampling +---------|-------------------------------|--------------------------------------- + 4096 | ![](images/image_is_4096.png) |   + 1024 | ![](images/image_is_1024.png) | ![](images/image_fis_1024.png) + 32 | ![](images/image_is_32.png) | ![](images/image_fis_32.png) +[Table [importanceSamplingViz]: Importance sampling vs pre-filtered importance sampling with $\alpha = 0.4$] + +The reference renderer used in the comparison below performs no approximation. In particular, it does not assume $v = n$ and does not perform the split sum approximation. The pre-filtered renderer uses all the techniques discussed in this section: pre-filtered cubemaps, the analytic formulation of the DFG term, and of course the split sum approximation. + +Left: reference renderer, right: pre-filtered importance sampling. + +![](images/image_is_ref_1.png) ![](images/image_filtered_1.png) +![](images/image_is_ref_2.png) ![](images/image_filtered_2.png) +![](images/image_is_ref_3.png) ![](images/image_filtered_3.png) +![](images/image_is_ref_4.png) ![](images/image_filtered_4.png) + +## Choosing important directions for sampling the BRDF + +For simplicity we use the $ D $ term of the BRDF as the PDF, however the PDF must be normalized such that the integral over the hemisphere is 1: + +$$\begin{equation} +\int_{\Omega}p(m)dm = 1 \\ +\int_{\Omega}D(m)(n \cdot m)dm = 1 \\ +\int_{\phi=0}^{2\pi}\int_{\theta=0}^{\frac{\pi}{2}}D(\theta,\phi) cos \theta sin \theta d\theta d\phi = 1 \\ +\end{equation}$$ + +The PDF of the BRDF can therefore be expressed as in equation $\ref{importantPDF}$: + +$$\begin{equation}\label{importantPDF} +p(\theta,\phi) = \frac{\alpha^2}{\pi(cos^2\theta (\alpha^2-1) + 1)^2} cos\theta sin\theta +\end{equation}$$ + +The term $sin\theta$ comes from the differential solid angle $sin\theta d\phi d\theta$ since we integrate over a sphere. We sample $\theta$ and $\phi$ independently: + +$$\begin{align*} +p(\theta) &= \int_0^{2\pi} p(\theta,\phi) d\phi = \frac{2\alpha^2}{(cos^2\theta (\alpha^2-1) + 1)^2} cos\theta sin\theta \\ +p(\phi) &= \frac{p(\theta,\phi)}{p(\phi)} = \frac{1}{2\pi} +\end{align*}$$ + +The expression of $ p(\phi) $ is true for an isotropic distribution of normals. + +We then calculate the cumulative distribution function (CDF) for each variable: + +$$\begin{align*} +P(s_{\phi}) &= \int_{0}^{s_{\phi}} p(\phi) d\phi = \frac{s_{\phi}}{2\pi} \\ +P(s_{\theta}) &= \int_{0}^{s_{\theta}} p(\theta) d\theta = 2 \alpha^2 \left( \frac{1}{(2\alpha^4-4\alpha^2+2) cos(s_{\theta})^2 + 2\alpha^2 - 2} - \frac{1}{2\alpha^4-2\alpha^2} \right) +\end{align*}$$ + +We set $ P(s_{\phi}) $ and $ P(s_{\theta}) $ to random variables $ \zeta_{\phi} $ and $ \zeta_{\theta} $ and solve for $ s_{\phi} $ and $ s_{\theta} $ respectively: + +$$\begin{align*} +P(s_{\phi}) &= \zeta_{\phi} \rightarrow s_{\phi} = 2\pi\zeta_{\phi} \\ +P(s_{\theta}) &= \zeta_{\theta} \rightarrow s_{\theta} = cos^{-1} \sqrt{\frac{1-\zeta_{\theta}}{(\alpha^2-1)\zeta_{\theta}+1}} +\end{align*}$$ + +So given a uniform distribution $ (\zeta_{\phi},\zeta_{\theta}) $, our important direction $l$ is defined as: + +$$\begin{align*} +\phi &= 2\pi\zeta_{\phi} \\ +\theta &= cos^{-1} \sqrt{\frac{1-\zeta_{\theta}}{(\alpha^2-1)\zeta_{\theta}+1}} \\ +l &= \{ cos\phi sin\theta,sin\phi sin\theta,cos\theta \} +\end{align*}$$ + +## Hammersley sequence + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +vec2f hammersley(uint i, float numSamples) { + uint bits = i; + bits = (bits << 16) | (bits >> 16); + bits = ((bits & 0x55555555) << 1) | ((bits & 0xAAAAAAAA) >> 1); + bits = ((bits & 0x33333333) << 2) | ((bits & 0xCCCCCCCC) >> 2); + bits = ((bits & 0x0F0F0F0F) << 4) | ((bits & 0xF0F0F0F0) >> 4); + bits = ((bits & 0x00FF00FF) << 8) | ((bits & 0xFF00FF00) >> 8); + return vec2f(i / numSamples, bits / exp2(32)); +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[C++ implementation of a Hammersley sequence generator] + +## Precomputing L for image-based lighting + +The term $ L_{DFG} $ is only dependent on $ \NoV $. Below, the normal is arbitrarily set to $ n=\left[0, 0, 1\right] $ and $v$ is chosen to satisfy $ \NoV $. The vector $ h_i $ is the $ D_{GGX}(\alpha) $ important direction sample $i$. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +float GDFG(float NoV, float NoL, float a) { + float a2 = a * a; + float GGXL = NoV * sqrt((-NoL * a2 + NoL) * NoL + a2); + float GGXV = NoL * sqrt((-NoV * a2 + NoV) * NoV + a2); + return (2 * NoL) / (GGXV + GGXL); +} + +float2 DFG(float NoV, float a) { + float3 V; + V.x = sqrt(1.0f - NoV*NoV); + V.y = 0.0f; + V.z = NoV; + + float2 r = 0.0f; + for (uint i = 0; i < sampleCount; i++) { + float2 Xi = hammersley(i, sampleCount); + float3 H = importanceSampleGGX(Xi, a, N); + float3 L = 2.0f * dot(V, H) * H - V; + + float VoH = saturate(dot(V, H)); + float NoL = saturate(L.z); + float NoH = saturate(H.z); + + if (NoL > 0.0f) { + float G = GDFG(NoV, NoL, a); + float Gv = G * VoH / NoH; + float Fc = pow(1 - VoH, 5.0f); + r.x += Gv * (1 - Fc); + r.y += Gv * Fc; + } + } + return r * (1.0f / sampleCount); +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[C++ implementation of the $ L_{DFG} $ term] + +## Spherical Harmonics + + Symbol | Definition +:---------------------------:|:---------------------------| +$K^m_l$ | Normalization factors +$P^m_l(x)$ | Associated Legendre polynomials +$y^m_l$ | Spherical harmonics bases, or SH bases +$L^m_l$ | SH coefficients of the $L(s)$ function defined on the unit sphere +[Table [shSymbols]: Spherical harmonics symbols definitions] + +### Basis functions + +Spherical parameterization of points on the surface of the unit sphere: + +$$\begin{equation} +\{ x, y, z \} = \{ cos \phi sin \theta, sin \phi sin \theta, cos \theta \} +\end{equation}$$ + +The complex spherical harmonics bases are given by: + +$$\begin{equation} +Y^m_l(\theta, \phi) = K^m_l e^{im\theta} P^{|m|}_l(cos \theta), l \in N, -l <= m <= l +\end{equation}$$ + +However we only need the real bases: + +$$\begin{align*} +y^{m > 0}_l &= \sqrt{2} K^m_l cos(m \phi) P^m_l(cos \theta) \\ +y^{m < 0}_l &= \sqrt{2} K^m_l sin(|m| \phi) P^{|m|}_l(cos \theta) \\ +y^0_l &= K^0_l P^0_l(cos \theta) +\end{align*}$$ + +The normalization factors are given by: + +$$\begin{equation} +K^m_l = \sqrt{\frac{(2l + 1)(l - |m|)!}{4 \pi (l + |m|)!}} +\end{equation}$$ + +The associated Legendre polynomials $P^{|m|}_l$ can be calculated from the following recursions: + +$$\begin{equation}\label{shRecursions} +P^0_0(x) = 1 \\ +P^0_1(x) = x \\ +P^l_l(x) = (-1)^l (2l - 1)!! (1 - x^2)^{\frac{l}{2}} \\ +P^m_l(x) = \frac{((2l - 1) x P^m_{l - 1} - (l + m - 1) P^m_{l - 2})}{l - m} \\ +\end{equation}$$ + +Computing $y^{|m|}_l$ requires to compute $P^{|m|}_l(z)$ first. +This can be accomplished fairly easily using the recursions in equation $\ref{shRecursions}$. +The third recursion can be used to "move diagonally" in table [basisFunctions], i.e. calculating $y^0_0$, $y^1_1$, $y^2_2$ etc. +Then, the fourth recursion can be used to move vertically. + + Band index | Basis functions $-l <= m <= l$ +:-----------:|:---------------------------------:| +$l = 0$ | $y^0_0$ +$l = 1$ | $y^{-1}_1$ $y^0_1$ $y^1_1$ +$l = 2$ | $y^{-2}_2$ $y^{-1}_2$ $y^0_2$ $y^1_2$ $y^2_2$ +[Table [basisFunctions]: Basis functions per band] + +It’s also fairly easy to compute the trigonometric terms recursively: + +$$\begin{align*} +C_m &\equiv cos(m \phi)sin(\theta)^m \\ +S_m &\equiv sin(m \phi)sin(\theta)^m \\ +\{ x, y, z \} &= \{ cos \phi sin \theta, sin \phi sin \theta, cos \theta \} +\end{align*}$$ + +Using the angle sum trigonometric identities: + +$$\begin{align*} +cos(m \phi + \phi) &= cos(m \phi) cos(\phi) - sin(m \phi) sin(\phi) \Leftrightarrow C_{m + 1} = x C_m - y S_m \\ +sin(m \phi + \phi) &= sin(m \phi) cos(\phi) + cos(m \phi) sin(\phi) \Leftrightarrow S_{m + 1} = x S_m - y C_m +\end{align*}$$ + + +Listing [nonNormalizedSHBasis] shows the C++ code to compute the non-normalized SH basis $\frac{y^m_l(s)}{\sqrt{2} K^m_l}$: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +static inline size_t SHindex(ssize_t m, size_t l) { + return l * (l + 1) + m; +} + +void computeShBasis( + double* const SHb, + size_t numBands, + const vec3& s) +{ + // handle m=0 separately, since it produces only one coefficient + double Pml_2 = 0; + double Pml_1 = 1; + SHb[0] = Pml_1; + for (ssize_t l = 1; l < numBands; l++) { + double Pml = ((2 * l - 1) * Pml_1 * s.z - (l - 1) * Pml_2) / l; + Pml_2 = Pml_1; + Pml_1 = Pml; + SHb[SHindex(0, l)] = Pml; + } + double Pmm = 1; + for (ssize_t m = 1; m < numBands ; m++) { + Pmm = (1 - 2 * m) * Pmm; + double Pml_2 = Pmm; + double Pml_1 = (2 * m + 1)*Pmm*s.z; + // l == m + SHb[SHindex(-m, m)] = Pml_2; + SHb[SHindex( m, m)] = Pml_2; + if (m + 1 < numBands) { + // l == m+1 + SHb[SHindex(-m, m + 1)] = Pml_1; + SHb[SHindex( m, m + 1)] = Pml_1; + for (ssize_t l = m + 2; l < numBands; l++) { + double Pml = ((2 * l - 1) * Pml_1 * s.z - (l + m - 1) * Pml_2) + / (l - m); + Pml_2 = Pml_1; + Pml_1 = Pml; + SHb[SHindex(-m, l)] = Pml; + SHb[SHindex( m, l)] = Pml; + } + } + } + double Cm = s.x; + double Sm = s.y; + for (ssize_t m = 1; m <= numBands ; m++) { + for (ssize_t l = m; l < numBands ; l++) { + SHb[SHindex(-m, l)] *= Sm; + SHb[SHindex( m, l)] *= Cm; + } + double Cm1 = Cm * s.x - Sm * s.y; + double Sm1 = Sm * s.x + Cm * s.y; + Cm = Cm1; + Sm = Sm1; + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [nonNormalizedSHBasis]: C++ implementation to compute a non-normalized SH basis] + +Normalized SH basis functions $y^m_l(s)$ for the first 3 bands: + + Band | $m = -2$ | $m = -1$ | $m = 0$ | $m = 1$ | $m = 2$ | +:-------:|:------------------------------------:|:-------------------------------------:|:---------------------------------------------------:|:-------------------------------------:|:---------------------------------------------:| +$l = 0$ | | | $\frac{1}{2}\sqrt{\frac{1}{\pi}}$ | | | +$l = 1$ | | $-\frac{1}{2}\sqrt{\frac{3}{\pi}}y$ | $\frac{1}{2}\sqrt{\frac{3}{\pi}}z$ | $-\frac{1}{2}\sqrt{\frac{3}{\pi}}x$ | | +$l = 2$ | $\frac{1}{2}\sqrt{\frac{15}{\pi}}xy$ | $-\frac{1}{2}\sqrt{\frac{15}{\pi}}yz$ | $\frac{1}{4}\sqrt{\frac{5}{\pi}}(2z^2 - x^2 - y^2)$ | $-\frac{1}{2}\sqrt{\frac{15}{\pi}}xz$ | $\frac{1}{4}\sqrt{\frac{15}{\pi}}(x^2 - y^2)$ | +[Table [basisFunctions]: Normalized basis functions per band] + +### Decomposition and reconstruction + +A function $L(s)$ defined on a sphere is projected to the SH basis as follows: + +$$\begin{equation} +L^m_l = \int_\Omega L(s) y^m_l(s) ds \\ +L^m_l = \int_{\theta = 0}^{\pi} \int_{\phi = 0}^{2\pi} L(\theta, \phi) y^m_l(\theta, \phi) sin \theta d\theta d\phi +\end{equation}$$ + +Note that each $L^m_l$ is a vector of 3 values, one for each RGB color channel. + +The inverse transformation, or reconstruction, or rendering, from the SH coefficients is given by: + +$$\begin{equation} +\hat{L}(s) = \sum_l \sum_{m = -l}^l L^m_l y^m_l(s) +\end{equation}$$ + +### Decomposition of $\left< cos \theta \right>$ + +Since $\left< cos \theta \right>$ does not depend on $\phi$ (azimuthal independence), the integral simplifies to: + +$$\begin{align*} +C^0_l &= 2\pi \int_0^{\pi} \left< cos \theta \right> y^0_l(\theta) sin \theta d\theta \\ +C^0_l &= 2\pi K^m_l \int_0^{\frac{\pi}{2}} P^0_l(cos \theta) cos \theta sin \theta d\theta \\ +C^m_l &= 0, m != 0 +\end{align*}$$ + +In [#Ramamoorthi01] an analytical solution to the integral is described: + +$$\begin{align*} +C_1 &= \sqrt{\frac{\pi}{3}} \\ +C_{odd} &= 0 \\ +C_{l, even} &= 2\pi \sqrt{\frac{2l + 1}{4\pi}} \frac{(-1)^{\frac{l}{2} - 1}}{(l + 2)(l - 1)} \frac{l!}{2^l (\frac{l!}{2})^2} +\end{align*}$$ + +The first few coefficients are: + +$$\begin{align*} +C_0 &= +0.88623 \\ +C_1 &= +1.02333 \\ +C_2 &= +0.49542 \\ +C_3 &= +0.00000 \\ +C_4 &= -0.11078 +\end{align*}$$ + +Very few coefficients are needed to reasonably approximate $\left< cos \theta \right>$, as shown in figure [shCosThetaApprox]. + +![Figure [shCosThetaApprox]: Approximation of $cos \theta$ with SH coefficients](images/chart_sh_cos_thera_approx.png) + +### Convolution + +Convolutions by a kernel $h$ that has a circular symmetry can be applied directly and easily in SH space: + +$$\begin{equation} +(h * f)^m_l = \sqrt{\frac{4\pi}{2l + 1}} h^0_l(s) f^m_l(s) +\end{equation}$$ + +Conveniently, $\sqrt{\frac{4\pi}{2l + 1}} = \frac{1}{K^0_l}$, so in practice we pre-multiply $C_l$ by $\frac{1}{K^0_l}$ and we get a simpler expression: + +$$\begin{equation} +\hat{C}_{l, even} = 2\pi \frac{(-1)^{\frac{l}{2} - 1}}{(l + 2)(l - 1)} \frac{l!}{2^l (\frac{l!}{2})^2} \\ +\hat{C}_1 = \frac{2\pi}{3} +\end{equation}$$ + +Here is the C++ code to compute $\hat{C}_l$: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +static double factorial(size_t n, size_t d = 1); + +// < cos(theta) > SH coefficients pre-multiplied by 1 / K(0,l) +double computeTruncatedCosSh(size_t l) { + if (l == 0) { + return M_PI; + } else if (l == 1) { + return 2 * M_PI / 3; + } else if (l & 1) { + return 0; + } + const size_t l_2 = l / 2; + double A0 = ((l_2 & 1) ? 1.0 : -1.0) / ((l + 2) * (l - 1)); + double A1 = factorial(l, l_2) / (factorial(l_2) * (1 << l)); + return 2 * M_PI * A0 * A1; +} + +// returns n! / d! +double factorial(size_t n, size_t d ) { + d = std::max(size_t(1), d); + n = std::max(size_t(1), n); + double r = 1.0; + if (n == d) { + // intentionally left blank + } else if (n > d) { + for ( ; n>d ; n--) { + r *= n; + } + } else { + for ( ; d>n ; d--) { + r *= d; + } + r = 1.0 / r; + } + return r; +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +## Sample validation scene for Mitsuba + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +<scene version="0.5.0"> + <integrator type="path"/> + + <shape type="serialized" id="sphere_mesh"> + <string name="filename" value="plastic_sphere.serialized"/> + <integer name="shapeIndex" value="0"/> + + <bsdf type="roughplastic"> + <string name="distribution" value="ggx"/> + <float name="alpha" value="0.0"/> + <srgb name="diffuseReflectance" value="0.81, 0.0, 0.0"/> + </bsdf> + </shape> + + <emitter type="envmap"> + <string name="filename" value="../../environments/office/office.exr"/> + <float name="scale" value="35000.0" /> + <boolean name="cache" value="false" /> + </emitter> + + <emitter type="directional"> + <vector name="direction" x="-1" y="-1" z="1" /> + <rgb name="irradiance" value="120000.0, 115200.0, 114000.0" /> + </emitter> + + <sensor type="perspective"> + <float name="farClip" value="12.0"/> + <float name="focusDistance" value="4.1"/> + <float name="fov" value="45"/> + <string name="fovAxis" value="y"/> + <float name="nearClip" value="0.01"/> + <transform name="toWorld"> + + <lookat target="0, 0, 0" origin="0, 0, -3.1" up="0, 1, 0"/> + </transform> + + <sampler type="ldsampler"> + <integer name="sampleCount" value="256"/> + </sampler> + + <film type="ldrfilm"> + <integer name="height" value="1440"/> + <integer name="width" value="2048"/> + <float name="exposure" value="-15.23" /> + <rfilter type="gaussian"/> + </film> + </sensor> +</scene> +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +## Light assignment with froxels + +Assigning lights to froxels can be implemented on the GPU using two compute shaders. The first one, shown in listing [froxelGeneration], creates the froxels data (4 planes + a min Z and max Z per froxel) in an SSBO and needs to be run only once. The shader requires the following uniforms: + +Projection matrix +: The projection matrix used to render the scene (view space to clip space transformation). + +Inverse projection matrix +: The inverse of the projection matrix used to render the scene (clip space to view space transformation). + +Depth parameters +: $-log2(\frac{z_{lighnear}}{z_{far}}) \frac{1}{maxSlices-1}$, maximum number of depth slices, Z near and Z far. + +Clip space size +: $\frac{F_x \times F_r}{w} \times 2$, with $F_x$ the number of tiles on the X axis, $F_r$ the resolution in pixels of a tile and w the width in pixels of the render target. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +#version 310 es + +precision highp float; +precision highp int; + + +#define FROXEL_RESOLUTION 80u + +layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; + +layout(location = 0) uniform mat4 projectionMatrix; +layout(location = 1) uniform mat4 projectionInverseMatrix; +layout(location = 2) uniform vec4 depthParams; // index scale, index bias, near, far +layout(location = 3) uniform float clipSpaceSize; + +struct Froxel { + // NOTE: the planes should be stored in vec4[4] but the + // Adreno shader compiler has a bug that causes the data + // to not be read properly inside the loop + vec4 plane0; + vec4 plane1; + vec4 plane2; + vec4 plane3; + vec2 minMaxZ; +}; + +layout(binding = 0, std140) writeonly restrict buffer FroxelBuffer { + Froxel data[]; +} froxels; + +shared vec4 corners[4]; +shared vec2 minMaxZ; + +vec4 projectionToView(vec4 p) { + p = projectionInverseMatrix * p; + return p / p.w; +} + +vec4 createPlane(vec4 b, vec4 c) { + // standard plane equation, with a at (0, 0, 0) + return vec4(normalize(cross(c.xyz, b.xyz)), 1.0); +} + +void main() { + uint index = gl_WorkGroupID.x + gl_WorkGroupID.y * gl_NumWorkGroups.x + + gl_WorkGroupID.z * gl_NumWorkGroups.x * gl_NumWorkGroups.y; + + if (gl_LocalInvocationIndex == 0u) { + // first tile the screen and build the frustum for the current tile + vec2 renderTargetSize = vec2(FROXEL_RESOLUTION * gl_NumWorkGroups.xy); + vec2 frustumMin = vec2(FROXEL_RESOLUTION * gl_WorkGroupID.xy); + vec2 frustumMax = vec2(FROXEL_RESOLUTION * (gl_WorkGroupID.xy + 1u)); + + corners[0] = vec4( + frustumMin.x / renderTargetSize.x * clipSpaceSize - 1.0, + (renderTargetSize.y - frustumMin.y) / renderTargetSize.y + * clipSpaceSize - 1.0, + 1.0, + 1.0 + ); + corners[1] = vec4( + frustumMax.x / renderTargetSize.x * clipSpaceSize - 1.0, + (renderTargetSize.y - frustumMin.y) / renderTargetSize.y + * clipSpaceSize - 1.0, + 1.0, + 1.0 + ); + corners[2] = vec4( + frustumMax.x / renderTargetSize.x * clipSpaceSize - 1.0, + (renderTargetSize.y - frustumMax.y) / renderTargetSize.y + * clipSpaceSize - 1.0, + 1.0, + 1.0 + ); + corners[3] = vec4( + frustumMin.x / renderTargetSize.x * clipSpaceSize - 1.0, + (renderTargetSize.y - frustumMax.y) / renderTargetSize.y + * clipSpaceSize - 1.0, + 1.0, + 1.0 + ); + + uint froxelSlice = gl_WorkGroupID.z; + minMaxZ = vec2(0.0, 0.0); + if (froxelSlice > 0u) { + minMaxZ.x = exp2((float(froxelSlice) - depthParams.y) * depthParams.x) + * depthParams.w; + } + minMaxZ.y = exp2((float(froxelSlice + 1u) - depthParams.y) * depthParams.x) + * depthParams.w; + } + + if (gl_LocalInvocationIndex == 0u) { + vec4 frustum[4]; + frustum[0] = projectionToView(corners[0]); + frustum[1] = projectionToView(corners[1]); + frustum[2] = projectionToView(corners[2]); + frustum[3] = projectionToView(corners[3]); + + froxels.data[index].plane0 = createPlane(frustum[0], frustum[1]); + froxels.data[index].plane1 = createPlane(frustum[1], frustum[2]); + froxels.data[index].plane2 = createPlane(frustum[2], frustum[3]); + froxels.data[index].plane3 = createPlane(frustum[3], frustum[0]); + froxels.data[index].minMaxZ = minMaxZ; + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [froxelGeneration]: GLSL implementation of froxels data generation (compute shader)] + +The second compute shader, shown in listing [froxelEvaluation], runs every frame (if the camera and/or lights have changed) and assigns all the lights to their respective froxels. This shader relies only on a couple of uniforms (the number of point/spot lights and the view matrix) and four SSBOs: + +Light index buffer +: For each froxel, the index of each light that affects said froxel. The indices for point lights are written first and if there is enough space left, the indices for spot lights are written as well. A sentinel of value 0x7fffffffu separates point and spot lights and/or marks the end of the froxel's list of lights. Each froxel has a maximum number of lights (point + spot). + +Point lights buffer +: Array of structures describing the scene's point lights. + +Spot lights buffer +: Array of structures describing the scene's spot lights. + +Froxels buffer +: The list of froxels represented by planes, created by the previous compute shader. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +#version 310 es +precision highp float; +precision highp int; + +#define LIGHT_BUFFER_SENTINEL 0x7fffffffu +#define MAX_FROXEL_LIGHT_COUNT 32u + +#define THREADS_PER_FROXEL_X 8u +#define THREADS_PER_FROXEL_Y 8u +#define THREADS_PER_FROXEL_Z 1u +#define THREADS_PER_FROXEL (THREADS_PER_FROXEL_X * \ + THREADS_PER_FROXEL_Y * THREADS_PER_FROXEL_Z) + +layout(local_size_x = THREADS_PER_FROXEL_X, + local_size_y = THREADS_PER_FROXEL_Y, + local_size_z = THREADS_PER_FROXEL_Z) in; + +// x = point lights, y = spot lights +layout(location = 0) uniform uvec2 totalLightCount; +layout(location = 1) uniform mat4 viewMatrix; + +layout(binding = 0, packed) writeonly restrict buffer LightIndexBuffer { + uint index[]; +} lightIndexBuffer; + +struct PointLight { + vec4 positionFalloff; // x, y, z, falloff + vec4 colorIntensity; // r, g, b, intensity + vec4 directionIES; // dir x, dir y, dir z, IES profile index +}; + +layout(binding = 1, std140) readonly restrict buffer PointLightBuffer { + PointLight lights[]; +} pointLights; + +struct SpotLight { + vec4 positionFalloff; // x, y, z, falloff + vec4 colorIntensity; // r, g, b, intensity + vec4 directionIES; // dir x, dir y, dir z, IES profile index + vec4 angle; // angle scale, angle offset, unused, unused +}; + +layout(binding = 2, std140) readonly restrict buffer SpotLightBuffer { + SpotLight lights[]; +} spotLights; + +struct Froxel { + // NOTE: the planes should be stored in vec4[4] but the + // Adreno shader compiler has a bug that causes the data + // to not be read properly inside the loop + vec4 plane0; + vec4 plane1; + vec4 plane2; + vec4 plane3; + vec2 minMaxZ; +}; + +layout(binding = 3, std140) readonly restrict buffer FroxelBuffer { + Froxel data[]; +} froxels; + +shared uint groupLightCounter; +shared uint groupLightIndexBuffer[MAX_FROXEL_LIGHT_COUNT]; + +float signedDistanceFromPlane(vec4 p, vec4 plane) { + // plane.w == 0.0, simplify computation + return dot(plane.xyz, p.xyz); +} + +void synchronize() { + memoryBarrierShared(); + barrier(); +} + +void main() { + if (gl_LocalInvocationIndex == 0u) { + groupLightCounter = 0u; + } + memoryBarrierShared(); + + uint froxelIndex = gl_WorkGroupID.x + gl_WorkGroupID.y * gl_NumWorkGroups.x + + gl_WorkGroupID.z * gl_NumWorkGroups.x * gl_NumWorkGroups.y; + Froxel current = froxels.data[froxelIndex]; + + uint offset = gl_LocalInvocationID.x + + gl_LocalInvocationID.y * THREADS_PER_FROXEL_X; + for (uint i = 0u; i < totalLightCount.x && + groupLightCounter < MAX_FROXEL_LIGHT_COUNT && + offset + i < totalLightCount.x; i += THREADS_PER_FROXEL) { + + uint currentLight = offset + i; + + vec4 center = pointLights.lights[currentLight].positionFalloff; + center.xyz = (viewMatrix * vec4(center.xyz, 1.0)).xyz; + float r = inversesqrt(center.w); + + if (-center.z + r > current.minMaxZ.x && + -center.z - r <= current.minMaxZ.y) { + if (signedDistanceFromPlane(center, current.plane0) < r && + signedDistanceFromPlane(center, current.plane1) < r && + signedDistanceFromPlane(center, current.plane2) < r && + signedDistanceFromPlane(center, current.plane3) < r) { + + uint index = atomicAdd(groupLightCounter, 1u); + groupLightIndexBuffer[index] = currentLight; + } + } + } + + synchronize(); + + uint pointLightCount = groupLightCounter; + offset = froxelIndex * MAX_FROXEL_LIGHT_COUNT; + + for (uint i = gl_LocalInvocationIndex; i < pointLightCount; + i += THREADS_PER_FROXEL) { + lightIndexBuffer.index[offset + i] = groupLightIndexBuffer[i]; + } + + if (gl_LocalInvocationIndex == 0u) { + if (pointLightCount < MAX_FROXEL_LIGHT_COUNT) { + lightIndexBuffer.index[offset + pointLightCount] = LIGHT_BUFFER_SENTINEL; + } + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[Listing [froxelEvaluation]: GLSL implementation of assigning lights to froxels (compute shader)] + +# Revisions + +February 20, 2019: Cloth shading + - Removed Fresnel term from the cloth BRDF + - Removed cloth DFG approximations, replaced with a new channel in the DFG LUT + +August 21, 2018: Multiscattering + - Added section [Energy loss in specular reflectance] on how to compensate for energy loss in single scattering BRDFs + +August 17, 2018: Specular color + - Added section [Specular color] to explain how the base color of various metals is computed + +August 15, 2018: Fresnel + - Added a description of the Fresnel effect in section [Fresnel (specular F)] + +August 9, 2018: Lighting + - Added explanation about pre-exposed lights + +August 7, 2018: Cloth model + - Added description of the "Charlie" NDF + +August 3, 2018: First public version + +# Bibliography + +[#Ashdown98]: Ian Ashdown. 1998. Parsing the IESNA LM-63 photometric data file. http://lumen.iee.put.poznan.pl/kw/iesna.txt + +[#Ashikhmin00]: Michael Ashikhmin, Simon Premoze and Peter Shirley. A Microfacet-based BRDF Generator. *SIGGRAPH '00 Proceedings*, 65-74. + +[#Ashikhmin07]: Michael Ashikhmin and Simon Premoze. 2007. Distribution-based BRDFs. + +[#Burley12]: Brent Burley. 2012. Physically Based Shading at Disney. *Physically Based Shading in Film and Game Production, ACM SIGGRAPH 2012 Courses*. + +[#Estevez17]: Alejandro Conty Estevez and Christopher Kulla. 2017. Production Friendly Microfacet Sheen BRDF. *ACM SIGGRAPH 2017*. + +[#Hammon17]: Earl Hammon. 217. PBR Diffuse Lighting for GGX+Smith Microsurfaces. *GDC 2017*. + +[#Heitz14]: Eric Heitz. 2014. Understanding the Masking-Shadowing Function +in Microfacet-Based BRDFs. *Journal of Computer Graphics Techniques*, 3 (2). + +[#Heitz16]: Eric Heitz et al. 2016. Multiple-Scattering Microfacet BSDFs with the Smith Model. *ACM SIGGRAPH 2016*. + +[#Hill12]: Colin Barré-Brisebois and Stephen Hill. 2012. Blending in Detail. http://blog.selfshadow.com/publications/blending-in-detail/ + +[#Karis13a]: Brian Karis. 2013. Specular BRDF Reference. http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.html + +[#Karis13b]: Brian Karis, 2013. Real Shading in Unreal Engine 4. https://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf + +[#Karis14]: Brian Karis. 2014. Physically Based Shading on Mobile. https://www.unrealengine.com/blog/physically-based-shading-on-mobile + +[#Kelemen01]: Csaba Kelemen et al. 2001. A Microfacet Based Coupled Specular-Matte BRDF Model with Importance Sampling. *Eurographics Short Presentations*. + +[#Krystek85]: M. Krystek. 1985. An algorithm to calculate correlated color temperature. *Color Research & Application*, 10 (1), 38–40. + +[#Krivanek08]: Jaroslave Krivànek and Mark Colbert. 2008. Real-time Shading with Filtered Importance Sampling. *Eurographics Symposium on Rendering 2008*, Volume 27, Number 4. + +[#Kulla17]: Christopher Kulla and Alejandro Conty. 2017. Revisiting Physically Based Shading at Imageworks. *ACM SIGGRAPH 2017* + +[#Lagarde14]: Sébastien Lagarde and Charles de Rousiers. 2014. Moving Frostbite to PBR. *Physically Based Shading in Theory and Practice, ACM SIGGRAPH 2014 Courses*. + +[#Lagarde18]: Sébastien Lagarde and Evgenii Golubev. 2018. The road toward unified rendering with Unity’s high definition rendering pipeline. *Advances in Real-Time Rendering in Games, ACM SIGGRAPH 2018 Courses*. + +[#Lazarov13]: Dimitar Lazarov. 2013. Physically-Based Shading in Call of Duty: Black Ops. *Physically Based Shading in Theory and Practice, ACM SIGGRAPH 2013 Courses*. + +[#McAuley15]: Stephen McAuley. 2015. Rendering the World of Far Cry 4. *GDC 2015*. + +[#McGuire10]: Morgan McGuire. 2010. Ambient Occlusion Volumes. *High Performance Graphics*. + +[#Narkowicz14]: Krzysztof Narkowicz. 2014. Analytical DFG Term for IBL. https://knarkowicz.wordpress.com/2014/12/27/analytical-dfg-term-for-ibl + +[#Neubelt13]: David Neubelt and Matt Pettineo. 2013. Crafting a Next-Gen Material Pipeline for The Order: 1886. *Physically Based Shading in Theory and Practice, ACM SIGGRAPH 2013 Courses*. + +[#Oren94]: Michael Oren and Shree K. Nayar. 1994. Generalization of lambert's reflectance model. *SIGGRAPH*, 239–246. ACM. + +[#Pattanaik00]: Sumanta Pattanaik00 et al. 2000. Time-Dependent Visual Adaptation +For Fast Realistic Image Display. *SIGGRAPH '00 Proceedings of the 27th annual conference on Computer graphics and interactive techniques*, 47-54. + +[#Ramamoorthi01]: Ravi Ramamoorthi and Pat Hanrahan. 2001. On the relationship between radiance and irradiance: determining the illumination from images of a convex Lambertian object. *Journal of the Optical Society of America*, Volume 18, Number 10, October 2001. + +[#Revie12]: Donald Revie. 2012. Implementing Fur in Deferred Shading. *GPU Pro 2*, Chapter 2. + +[#Russell15]: Jeff Russell. 2015. Horizon Occlusion for Normal Mapped Reflections. http://marmosetco.tumblr.com/post/81245981087 + +[#Schlick94]: Christophe Schlick. 1994. An Inexpensive BRDF Model for Physically-Based Rendering. *Computer Graphics Forum*, 13 (3), 233–246. + +[#Walter07]: Bruce Walter et al. 2007. Microfacet Models for Refraction through Rough Surfaces. *Proceedings of the Eurographics Symposium on Rendering*. + + diff --git a/docs/FontAwesome/css/font-awesome.css b/docs/FontAwesome/css/font-awesome.css new file mode 100644 index 0000000000..540440ce89 --- /dev/null +++ b/docs/FontAwesome/css/font-awesome.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/docs/FontAwesome/fonts/FontAwesome.ttf b/docs/FontAwesome/fonts/FontAwesome.ttf new file mode 100644 index 0000000000..35acda2fa1 Binary files /dev/null and b/docs/FontAwesome/fonts/FontAwesome.ttf differ diff --git a/docs/FontAwesome/fonts/fontawesome-webfont.eot b/docs/FontAwesome/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000000..e9f60ca953 Binary files /dev/null and b/docs/FontAwesome/fonts/fontawesome-webfont.eot differ diff --git a/docs/FontAwesome/fonts/fontawesome-webfont.svg b/docs/FontAwesome/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000000..855c845e53 --- /dev/null +++ b/docs/FontAwesome/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/FontAwesome/fonts/fontawesome-webfont.ttf b/docs/FontAwesome/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000000..35acda2fa1 Binary files /dev/null and b/docs/FontAwesome/fonts/fontawesome-webfont.ttf differ diff --git a/docs/FontAwesome/fonts/fontawesome-webfont.woff b/docs/FontAwesome/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000000..400014a4b0 Binary files /dev/null and b/docs/FontAwesome/fonts/fontawesome-webfont.woff differ diff --git a/docs/FontAwesome/fonts/fontawesome-webfont.woff2 b/docs/FontAwesome/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000000..4d13fc6040 Binary files /dev/null and b/docs/FontAwesome/fonts/fontawesome-webfont.woff2 differ diff --git a/docs/Materials.html b/docs/Materials.html new file mode 100644 index 0000000000..b6ca0057ba --- /dev/null +++ b/docs/Materials.html @@ -0,0 +1,11 @@ + + + + + + Redirecting... + + + + + diff --git a/docs/Materials.md.html b/docs/Materials.md.html new file mode 100644 index 0000000000..1114da3e8b --- /dev/null +++ b/docs/Materials.md.html @@ -0,0 +1,2754 @@ + + + + +**Filament Materials Guide** + +![](images/filament_logo.png) + +# About + +This document is part of the [Filament project](https://github.com/google/filament). To report errors in this document please use the [project's issue tracker](https://github.com/google/filament/issues). + +## Authors + +- [Romain Guy](https://github.com/romainguy), [@romainguy](https://twitter.com/romainguy) +- [Mathias Agopian](https://github.com/pixelflinger), [@pixelflinger](https://bsky.app/profile/pixelflinger.bsky.social) + +# Overview + +Filament is a physically based rendering (PBR) engine for Android. Filament offers a customizable +material system that you can use to create both simple and complex materials. This document +describes all the features available to materials and how to create your own material. + +## Core concepts + +Material +: A material defines the visual appearance of a surface. To completely describe and render a + surface, a material provides the following information: + - Material model + - Set of use-controllable named parameters + - Raster state (blending mode, backface culling, etc.) + - Vertex shader code + - Fragment shader code + +Material model +: Also called _shading model_ or _lighting model_, the material model defines the intrinsic + properties of a surface. These properties have a direct influence on the way lighting is + computed and therefore on the appearance of a surface. + +Material definition +: A text file that describes all the information required by a material. This is the file that you + will directly author to create new materials. + +Material package +: At runtime, materials are loaded from _material packages_ compiled from material definitions + using the `matc` tool. A material package contains all the information required to describe a + material, and shaders generated for the target runtime platforms. This is necessary because + different platforms (Android, macOS, Linux, etc.) use different graphics APIs or different + variants of similar graphics APIs (OpenGL vs OpenGL ES for instance). + +Material instance +: A material instance is a reference to a material and a set of values for the different values of + that material. Material instances are not covered in this document as they are created and + manipulated directly from code using Filament's APIs. + +# Material models + +Filament materials can use one of the following material models: +- Lit (or standard) +- Subsurface +- Cloth +- Unlit +- Specular glossiness (legacy) + +## Lit model + +The lit model is Filament's standard material model. This physically-based shading model was +designed after to offer good interoperability with other common tools and engines such as _Unity 5_, +_Unreal Engine 4_, _Substance Designer_ or _Marmoset Toolbag_. + +This material model can be used to describe many non-metallic surfaces (_dielectrics_) +or metallic surfaces (_conductors_). + +The appearance of a material using the standard model is controlled using the properties described +in table [standardProperties]. + + + Property | Definition +-----------------------:|:--------------------- +**baseColor** | Diffuse albedo for non-metallic surfaces, and specular color for metallic surfaces +**roughness** | Perceived smoothness (1.0) or roughness (0.0) of a surface. Smooth surfaces exhibit sharp reflections +**metallic** | Whether a surface appears to be dielectric (0.0) or conductor (1.0). Often used as a binary value (0 or 1) +**reflectance** | Fresnel reflectance at normal incidence for dielectric surfaces. This directly controls the strength of the reflections +**ambientOcclusion** | Defines how much of the ambient light is accessible to a surface point. It is a per-pixel shadowing factor between 0.0 and 1.0 +**clearCoat** | Strength of the clear coat layer +**clearCoatRoughness** | Perceived smoothness or roughness of the clear coat layer +**clearCoatNormal** | A detail normal used to perturb the clear coat layer using _bump mapping_ (_normal mapping_) +**anisotropy** | Amount of anisotropy in either the tangent or bitangent direction +**anisotropyDirection** | Local surface direction in tangent space +**thickness** | Thickness of the solid volume of refractive objects +**sheenColor** | Strength of the sheen layer +**sheenRoughness** | Perceived smoothness or roughness of the sheen layer +**emissive** | Additional diffuse albedo to simulate emissive surfaces (such as neons, etc.) This property is mostly useful in an HDR pipeline with a bloom pass +**normal** | A detail normal used to perturb the surface using _bump mapping_ (_normal mapping_) +**postLightingColor** | Additional color that can be blended with the result of the lighting computations. See `postLightingBlending` +**absorption** | Absorption factor for refractive objects +**transmission** | Defines how much of the diffuse light of a dielectric is transmitted through the object, in other words this defines how transparent an object is +**ior** | Index of refraction, either for refractive objects or as an alternative to reflectance +**microThickness** | Thickness of the thin layer of refractive objects +**bentNormal** | A normal pointing in the average unoccluded direction. Can be used to improve indirect lighting quality +**shadowStrength** | Strength factor between 0 and 1 for all shadows received by this material +[Table [standardProperties]: Properties of the standard model] + +The type and range of each property is described in table [standardPropertiesTypes]. + + Property | Type | Range | Note +-----------------------:|:--------:|:------------------------:|:------------------------- +**baseColor** | float4 | [0..1] | Pre-multiplied linear RGB +**metallic** | float | [0..1] | Should be 0 or 1 +**roughness** | float | [0..1] | +**reflectance** | float | [0..1] | Prefer values > 0.35 +**sheenColor** | float3 | [0..1] | Linear RGB +**sheenRoughness** | float | [0..1] | +**clearCoat** | float | [0..1] | Should be 0 or 1 +**clearCoatRoughness** | float | [0..1] | +**anisotropy** | float | [-1..1] | Anisotropy is in the tangent direction when this value is positive +**anisotropyDirection** | float3 | [0..1] | Linear RGB, encodes a direction vector in tangent space +**ambientOcclusion** | float | [0..1] | +**normal** | float3 | [0..1] | Linear RGB, encodes a direction vector in tangent space +**bentNormal** | float3 | [0..1] | Linear RGB, encodes a direction vector in tangent space +**clearCoatNormal** | float3 | [0..1] | Linear RGB, encodes a direction vector in tangent space +**emissive** | float4 | rgb=[0..n], a=[0..1] | Linear RGB intensity in nits, alpha encodes the exposure weight +**postLightingColor** | float4 | [0..1] | Pre-multiplied linear RGB +**ior** | float | [1..n] | Optional, usually deduced from the reflectance +**transmission** | float | [0..1] | +**absorption** | float3 | [0..n] | +**microThickness** | float | [0..n] | +**thickness** | float | [0..n] | +[Table [standardPropertiesTypes]: Range and type of the standard model's properties] + + +!!! Note: About linear RGB + Several material model properties expect RGB colors. Filament materials use RGB colors in linear + space and you must take proper care of supplying colors in that space. See the Linear colors + section for more information. + +!!! Note: About pre-multiplied RGB + Filament materials expect colors to use pre-multiplied alpha. See the Pre-multiplied alpha + section for more information. + +!!! Note: About `absorption` + The light attenuation through the material is defined as $e^{-absorption \cdot distance}$, + and the distance depends on the `thickness` parameter. If `thickness` is not provided, then + the `absorption` parameter is used directly and the light attenuation through the material + becomes $1 - absorption$. To obtain a certain color at a desired distance, the above + equation can be inverted such as $absorption = -\frac{ln(color)}{distance}$. + +!!! Note: About `ior` and `reflectance` + The index of refraction (IOR) and the reflectance represent the same physical attribute, + therefore they don't need to be both specified. Typically, only the reflectance is specified, + and the IOR is deduced automatically. When only the IOR is specified, the reflectance is then + deduced automatically. It is possible to specify both, in which case their values are kept + as-is, which can lead to physically impossible materials, however, this might be desirable + for artistic reasons. + +!!! Note: About `thickness` and `microThickness` for refraction + `thickness` represents the thickness of solid objects in the direction of the normal, for + satisfactory results, this should be provided per fragment (e.g.: as a texture) or at least per + vertex. `microThickness` represent the thickness of the thin layer of an object, and can + generally be provided as a constant value. For example, a 1mm thin hollow sphere of radius 1m, + would have a `thickness` of 1 and a `microThickness` of 0.001. Currently `thickness` is not + used when `refractionType` is set to `thin`. + +### Base color + +The `baseColor` property defines the perceived color of an object (sometimes called albedo). The +effect of `baseColor` depends on the nature of the surface, controlled by the `metallic` property +explained in the Metallic section. + +Non-metals (dielectrics) +: Defines the diffuse color of the surface. Real-world values are typically found in the range + $[10..240]$ if the value is encoded between 0 and 255, or in the range $[0.04..0.94]$ between 0 + and 1. Several examples of base colors for non-metallic surfaces can be found in + table [baseColorsDielectrics]. + + Metal | sRGB | Hexadecimal | Color +----------:|:-------------------:|:------------:|------------------------------------------------------- +Coal | 0.19, 0.19, 0.19 | #323232 |
 
+Rubber | 0.21, 0.21, 0.21 | #353535 |
 
+Mud | 0.33, 0.24, 0.19 | #553d31 |
 
+Wood | 0.53, 0.36, 0.24 | #875c3c |
 
+Vegetation | 0.48, 0.51, 0.31 | #7b824e |
 
+Brick | 0.58, 0.49, 0.46 | #947d75 |
 
+Sand | 0.69, 0.66, 0.52 | #b1a884 |
 
+Concrete | 0.75, 0.75, 0.73 | #c0bfbb |
 
+[Table [baseColorsDielectrics]: `baseColor` for common non-metals] + +Metals (conductors) +: Defines the specular color of the surface. Real-world values are typically found in the range + $[170..255]$ if the value is encoded between 0 and 255, or in the range $[0.66..1.0]$ between 0 and + 1. Several examples of base colors for metallic surfaces can be found in table [baseColorsConductors]. + + Metal | sRGB | Hexadecimal | Color +----------:|:-------------------:|:------------:|------------------------------------------------------- +Silver | 0.97, 0.96, 0.91 | #f7f4e8 |
 
+Aluminum | 0.91, 0.92, 0.92 | #e8eaea |
 
+Titanium | 0.76, 0.73, 0.69 | #c1baaf |
 
+Iron | 0.77, 0.78, 0.78 | #c4c6c6 |
 
+Platinum | 0.83, 0.81, 0.78 | #d3cec6 |
 
+Gold | 1.00, 0.85, 0.57 | #ffd891 |
 
+Brass | 0.98, 0.90, 0.59 | #f9e596 |
 
+Copper | 0.97, 0.74, 0.62 | #f7bc9e |
 
+[Table [baseColorsConductors]: `baseColor` for common metals] + +### Metallic + +The `metallic` property defines whether the surface is a metallic (_conductor_) or a non-metallic +(_dielectric_) surface. This property should be used as a binary value, set to either 0 or 1. +Intermediate values are only truly useful to create transitions between different types of surfaces +when using textures. + +This property can dramatically change the appearance of a surface. Non-metallic surfaces have +chromatic diffuse reflection and achromatic specular reflection (reflected light does not change +color). Metallic surfaces do not have any diffuse reflection and chromatic specular reflection +(reflected light takes on the color of the surfaced as defined by `baseColor`). + +The effect of `metallic` is shown in figure [metallicProperty] (click on the image to see a +larger version). + +![Figure [metallicProperty]: `metallic` varying from 0.0 +(left) to 1.0 (right)](images/materials/metallic.png) + +### Roughness + +The `roughness` property controls the perceived smoothness of the surface. When `roughness` is set +to 0, the surface is perfectly smooth and highly glossy. The rougher a surface is, the "blurrier" +the reflections are. This property is often called _glossiness_ in other engines and tools, and is +simply the opposite of the roughness (`roughness = 1 - glossiness`). + +### Non-metals + +The effect of `roughness` on non-metallic surfaces is shown in figure [roughnessProperty] (click +on the image to see a larger version). + +![Figure [roughnessProperty]: Dielectric `roughness` varying from 0.0 +(left) to 1.0 (right)](images/materials/dielectric_roughness.png) + +### Metals + +The effect of `roughness` on metallic surfaces is shown in figure [roughnessConductorProperty] +(click on the image to see a larger version). + +![Figure [roughnessConductorProperty]: Conductor `roughness` varying from 0.0 +(left) to 1.0 (right)](images/materials/conductor_roughness.png) + +### Refraction + +When refraction through an object is enabled (using a `refractonType` of `thin` or `solid`), the +`roughness` property will also affect the refractions, as shown in figure +[roughnessRefractionProperty] (click on the image to see a larger version). + +![Figure [roughnessRefractionProperty]: Refractive sphere with `roughness` varying from 0.0 + (left) to 1.0 (right)](images/materials/refraction_roughness.png) + +### Reflectance + +The `reflectance` property only affects non-metallic surfaces. This property can be used to control +the specular intensity and index of refraction of materials. This value is defined +between 0 and 1 and represents a remapping of a percentage of reflectance. For instance, the +default value of 0.5 corresponds to a reflectance of 4%. Values below 0.35 (2% reflectance) should +be avoided as no real-world materials have such low reflectance. + +The effect of `reflectance` on non-metallic surfaces is shown in figure [reflectanceProperty] +(click on the image to see a larger version). + +![Figure [reflectanceProperty]: `reflectance` varying from 0.0 (left) +to 1.0 (right)](images/materials/reflectance.png) + +Figure [reflectance] shows common values and how they relate to the mapping function. + +![Figure [reflectance]: Common reflectance values](images/diagram_reflectance.png) + +Table [commonMatReflectance] describes acceptable reflectance values for various types of materials +(no real world material has a value under 2%). + + +Material | Reflectance | IOR | Linear value +--------------------------:|:-----------------|:-----------------|:---------------- +Water | 2% | 1.33 | 0.35 +Fabric | 4% to 5.6% | 1.5 to 1.62 | 0.5 to 0.59 +Common liquids | 2% to 4% | 1.33 to 1.5 | 0.35 to 0.5 +Common gemstones | 5% to 16% | 1.58 to 2.33 | 0.56 to 1.0 +Plastics, glass | 4% to 5% | 1.5 to 1.58 | 0.5 to 0.56 +Other dielectric materials | 2% to 5% | 1.33 to 1.58 | 0.35 to 0.56 +Eyes | 2.5% | 1.38 | 0.39 +Skin | 2.8% | 1.4 | 0.42 +Hair | 4.6% | 1.55 | 0.54 +Teeth | 5.8% | 1.63 | 0.6 +Default value | 4% | 1.5 | 0.5 +[Table [commonMatReflectance]: Reflectance of common materials] + +Note that the `reflectance` property also defines the index of refraction of the surface. +When this property is defined it is not necessary to define the `ior` property. Setting +either of these properties will automatically compute the other property. It is possible +to specify both, in which case their values are kept as-is, which can lead to physically +impossible materials, however, this might be desirable for artistic reasons. + +The `reflectance` property is designed as a normalized property in the range 0..1 which makes +it easy to define from a texture. + +See section [Index of refraction] for more information about the `ior` property and refractive +indices. + +### Sheen color + +The sheen color controls the color appearance and strength of an optional sheen layer on top of the +base layer described by the properties above. The sheen layer always sits below the clear coat layer +if such a layer is present. + +The sheen layer can be used to represent cloth and fabric materials. Please refer to +section [Cloth model] for more information about cloth and fabric materials. + +The effect of `sheenColor` is shown in figure [materialSheenColor] +(click on the image to see a larger version). + +![Figure [materialSheenColor]: Different sheen colors](images/screenshot_sheen_color.png) + +!!! Note + If you do not need the other properties offered by the standard lit material model but want to + create a cloth-like or fabric-like appearance, it is more efficient to use the dedicated cloth + model described in section [Cloth model]. + +### Sheen roughness + +The `sheenRoughness` property is similar to the `roughness` property but applies only to the +sheen layer. + +The effect of `sheenRoughness` on a rough metal is shown in figure [sheenRoughnessProperty] +(click on the image to see a larger version). In this picture, the base layer is a dark blue, with +`metallic` set to `0.0` and `roughness` set to `1.0`. + +![Figure [sheenRoughnessProperty]: `sheenRoughness` varying from 0.0 +(left) to 1.0 (right)](images/materials/sheen_roughness.png) + +### Clear coat + +Multi-layer materials are fairly common, particularly materials with a thin translucent +layer over a base layer. Real world examples of such materials include car paints, soda cans, +lacquered wood and acrylic. + +The `clearCoat` property can be used to describe materials with two layers. The clear coat layer +will always be isotropic and dielectric. + +![Figure [clearCoat]: Comparison of a carbon-fiber material under the standard material model +(left) and the clear coat model (right)](images/material_carbon_fiber.png) + +The `clearCoat` property controls the strength of the clear coat layer. This should be treated as a +binary value, set to either 0 or 1. Intermediate values are useful to control transitions between +parts of the surface that have a clear coat layers and parts that don't. + +The effect of `clearCoat` on a rough metal is shown in figure [clearCoatProperty] +(click on the image to see a larger version). + +![Figure [clearCoatProperty]: `clearCoat` varying from 0.0 +(left) to 1.0 (right)](images/materials/clear_coat.png) + +!!! Warning + The clear coat layer effectively doubles the cost of specular computations. Do not assign a + value, even 0.0, to the clear coat property if you don't need this second layer. + +!!! Note + The clear coat layer is added on top of the sheen layer if present. + +### Clear coat roughness + +The `clearCoatRoughness` property is similar to the `roughness` property but applies only to the +clear coat layer. + +The effect of `clearCoatRoughness` on a rough metal is shown in figure [clearCoatRoughnessProperty] +(click on the image to see a larger version). + +![Figure [clearCoatRoughnessProperty]: `clearCoatRoughness` varying from 0.0 +(left) to 1.0 (right)](images/materials/clear_coat_roughness.png) + +### Anisotropy + +Many real-world materials, such as brushed metal, can only be replicated using an anisotropic +reflectance model. A material can be changed from the default isotropic model to an anisotropic +model by using the `anisotropy` property. + +![Figure [anisotropic]: Comparison of isotropic material +(left) and anistropic material (right)](images/material_anisotropic.png) + +The effect of `anisotropy` on a rough metal is shown in figure [anisotropyProperty] +(click on the image to see a larger version). + +![Figure [anisotropyProperty]: `anisotropy` varying from 0.0 +(left) to 1.0 (right)](images/materials/anisotropy.png) + +The figure [anisotropyDir] below shows how the direction of the anisotropic highlights can be +controlled by using either positive or negative values: positive values define anisotropy in the +tangent direction and negative values in the bitangent direction. + +![Figure [anisotropyDir]: Positive (left) vs negative +(right) `anisotropy` values](images/screenshot_anisotropy_direction.png) + +!!! Tip + The anisotropic material model is slightly more expensive than the standard material model. Do + not assign a value (even 0.0) to the `anisotropy` property if you don't need anisotropy. + +### Anisotropy direction + +The `anisotropyDirection` property defines the direction of the surface at a given point and thus +control the shape of the specular highlights. It is specified as vector of 3 values that usually +come from a texture, encoding the directions local to the surface in tangent space. Because the +direction is in tangent space, the Z component should be set to 0. + +The effect of `anisotropyDirection` on a metal is shown in figure [anisotropyDirectionProperty] +(click on the image to see a larger version). + +![Figure [anisotropyDirectionProperty]: Anisotropic metal rendered +with a direction map](images/screenshot_anisotropy.png) + +The result shown in figure [anisotropyDirectionProperty] was obtained using the direction map shown +in figure [anisotropyDirectionProperty]. + +![Figure [anisotropyDirectionProperty]: Example of Lighting: specularAmbientOcclusiona direction map](images/screenshot_anisotropy_map.jpg) + +### Ambient occlusion + +The `ambientOcclusion` property defines how much of the ambient light is accessible to a surface +point. It is a per-pixel shadowing factor between 0.0 (fully shadowed) and 1.0 (fully lit). This +property only affects diffuse indirect lighting (image-based lighting), not direct lights such as +directional, point and spot lights, nor specular lighting. + +![Figure [aoExample]: Comparison of materials without diffuse ambient occlusion +(left) and with (right)](images/screenshot_ao.jpg) + +### Normal + +The `normal` property defines the normal of the surface at a given point. It usually comes from a +_normal map_ texture, which allows to vary the property per-pixel. The normal is supplied in tangent +space, which means that +Z points outside of the surface. + +For example, let's imagine that we want to render a piece of furniture covered in tufted leather. +Modeling the geometry to accurately represent the tufted pattern would require too many triangles +so we instead bake a high-poly mesh into a normal map. Once the base map is applied to a simplified +mesh, we get the result in figure [normalMapped]. + +Note that the `normal` property affects the _base layer_ and not the clear coat layer. + +![Figure [normalMapped]: Low-poly mesh without normal mapping (left) +and with (right)](images/screenshot_normal_mapping.jpg) + +!!! Warning + Using a normal map increases the runtime cost of the material model. + +### Bent normal + +The `bentNormal` property defines the average unoccluded direction at a point on the surface. It is +used to improve the accuracy of indirect lighting. Bent normals can also improve the quality of +specular ambient occlusion (see section [Lighting: specularAmbientOcclusion] about +`specularAmbientOcclusion`). + +Bent normals can greatly increase the visual fidelity of an asset with various cavities and concave +areas, as shown in figure [bentNormalMapped]. See the areas of the ears, nostrils and eyes for +instance. + +![Figure [bentNormalMapped]: Example of a model rendered with and without a bent normal map. Both +versions use the same ambient occlusion map.](images/material_bent_normal.gif) + +### Clear coat normal + +The `clearCoatNormal` property defines the normal of the clear coat layer at a given point. It +behaves otherwise like the `normal` property. + +![Figure [clearCoatNormalMapped]: A material with a clear coat normal +map and a surface normal map](images/screenshot_clear_coat_normal.jpg) + +!!! Warning + Using a clear coat normal map increases the runtime cost of the material model. + +### Emissive + +The `emissive` property can be used to simulate additional light emitted by the surface. It is +defined as a `float4` value that contains an RGB intensity in nits as well as an exposure +weight (in the alpha channel). + +The intensity in nits allows an emissive surface to function as a light and can be used to recreate +real world surfaces. For instance a computer display has an intensity between 200 and 1,000 nits. + +If you prefer to work in EV (or f-stops), you can simplify multiply your emissive color by the +output of the API `filament::Exposure::luminance(ev)`. This API returns the luminance in nits of +the specific EV. You can perform this conversion yourself using the following formula, where $L$ +is the final intensity in nits: $ L = 2^{EV - 3} $. + +The exposure weight carried in the alpha channel can be used to undo the camera exposure, and thus +force an emissive surface to bloom. When the exposure weight is set to 0, the emissive intensity is +not affected by the camera exposure. When the weight is set to 1, the intensity is multiplied by +the camera exposure like with any regular light. + +### Post-lighting color + +The `postLightingColor` can be used to modify the surface color after lighting computations. This +property has no physical meaning and only exists to implement specific effects or to help with +debugging. This property is defined as a `float4` value containing a pre-multiplied RGB color in +linear space. + +The post-lighting color is blended with the result of lighting according to the blending mode +specified by the `postLightingBlending` material option. Please refer to the documentation of +this option for more information. + +!!! Tip + `postLightingColor` can be used as a simpler `emissive` property by setting + `postLightingBlending` to `add` and by providing an RGB color with alpha set to `0.0`. + +### Index of refraction + +The `ior` property only affects non-metallic surfaces. This property can be used to control the +index of refraction and the specular intensity of materials. The `ior` property is intended to +be used with refractive (transmissive) materials, which are enabled when the `refractionMode` is +set to `cubemap` or `screenspace`. It can also be used on non-refractive objects as an alternative +to setting the reflectance. + +The index of refraction (or refractive index) of a material is a dimensionless number that describes +how fast light travels through that material. The higher the number, the slower light travels +through the medium. More importantly for rendering materials, the refractive index determines how +the path light travels is bent when entering the material. Higher indices of refraction will cause +light to bend further away from the initial path. + +Table [commonMatIOR] describes acceptable refractive indices for various types of materials. + +Material | IOR +--------------------------:|:----------------- +Air | 1.0 +Water | 1.33 +Common liquids | 1.33 to 1.5 +Common gemstones | 1.58 to 2.33 +Plastics, glass | 1.5 to 1.58 +Other dielectric materials | 1.33 to 1.58 +[Table [commonMatIOR]: Index of refraction of common materials] + +The appearance of a refractive material will greatly depend on the `refractionType` and +`refractionMode` settings of the material. Refer to section +[Blending and transparency: refractionType] and section [Blending and transparency: refractionMode] +for more information. + +The effect of `ior` when `refractionMode` is set to `cubemap` and `refractionType` is set to `solid` +can be seen in figure [iorProperty2] (click on the image to see a larger version). + +![Figure [iorProperty2]: `transmission` varying from 1.0 +(left) to 1.5 (right)](images/materials/ior.png) + +Figure [iorProperty] shows the comparison of a sphere of `ior` 1.0 with a sphere of `ior` 1.33, with +the `refractionMode` set to `screenspace` and the `refractionType` set to `solid` +(click on the image to see a larger version). + +![Figure [iorProperty]: `ior` of 1.0 (left) and 1.33 (right)](images/material_ior.png) + +Note that the `ior` property also defines the reflectance (or specular intensity) of the surface. +When this property is defined it is not necessary to define the `reflectance` property. Setting +either of these properties will automatically compute the other property. It is possible to specify +both, in which case their values are kept as-is, which can lead to physically impossible materials, +however, this might be desirable for artistic reasons. + +See the Reflectance section for more information on the `reflectance` property. + +!!! Tip + Refractive materials are affected by the `roughness` property. Rough materials will scatter + light, creating a diffusion effect useful to recreate "blurry" appearances such as frosted + glass, certain plastics, etc. + +### Transmission + +The `transmission` property defines what ratio of diffuse light is transmitted through a refractive +material. This property only affects materials with a `refractionMode` set to `cubemap` or +`screenspace`. + +When `transmission` is set to 0, no amount of light is transmitted and the diffuse component of +the surface is 100% visible. When `transmission` is set to 1, all the light is transmitted and the +diffuse component is not visible anymore, only the specular component is. + +The effect of `transmission` on a glossy dielectric (`ior` of 1.5, `refractionMode` set to +`cubemap`, `refractionType` set to `solid`) is shown in figure [transmissionProperty] +(click on the image to see a larger version). + +![Figure [transmissionProperty]: `transmission` varying from 0.0 +(left) to 1.0 (right)](images/materials/transmission.png) + +!!! Tip + The `transmission` property is useful to create decals, paint, etc. at the surface of refractive + materials. + +### Absorption + +The `absorption` property defines the absorption coefficients of light transmitted through the +material. Figure [absorptionExample] shows the effect of `absorption` on a refracting object with +an index of refraction of 1.5 and a base color set to white. + +![Figure [absorptionExample]: Refracting object without (left) +and with (right) absorption](images/material_absorption.png) + +Transmittance through a volume is exponential with respect to the optical depth (defined either +with `microThickness` or `thickness`). The computed color follows the following formula: + +$$color \cdot e^{-absorption \cdot distance}$$ + +Where `distance` is either `microThickness` or `thickness`, that is the distance light will travel +through the material at a given point. If no thickness/distance is specified, the computed color +follows this formula instead: + +$$color \cdot (1 - absorption)$$ + +The effect of varying the `absorption` coefficients is shown in figure [absorptionProperty] +(click on the image to see a larger version). In this picture, the object has a fixed `thickness` +of 4.5 and an index of refraction set to 1.3. + +![Figure [absorptionProperty]: `absorption` varying from (0.0, 0.02, 0.14) +(left) to (0.0, 0.36, 2.3) (right)](images/materials/absorption.png) + +Setting the absorption coefficients directly can be unintuitive which is why we recommend working +with a _transmittance color_ and a _"at distance"_ factor instead. These two parameters allow an +artist to specify the precise color the material should have at a specified distance through the +volume. The value to pass to `absorption` can be computed this way: + +$$absorption = -\frac{ln(transmittanceColor)}{atDistance}$$ + +While this computation can be done in the material itself we recommend doing it offline whenever +possible. Filament provides an API for this purpose, `Color::absorptionAtDistance()`. + +### Micro-thickness and thickness + +The `microThickness` and `thickness` properties define the optical depth of the material of a +refracting object. `microThickness` is used when `refractionType` is set to `thin`, and `thickness` +is used when `refractionType` is set to `volume`. + +`thickness` represents the thickness of solid objects in the direction of the normal, for +satisfactory results, this should be provided per fragment (e.g.: as a texture) or at least per +vertex. + +`microThickness` represent the thickness of the thin layer (shell) of an object, and can generally +be provided as a constant value. For example, a 1mm thin hollow sphere of radius 1m, would have a +`thickness` of 1 and a `microThickness` of 0.001. Currently `thickness` is not used when +`refractionType` is set to `thin`. Both properties are made available for possible future use. + +Both `thickness` and `microThickness` are used to compute the transmitted color of the material +when the `absorption` property is set. In solid volumes, `thickness` will also affect how light +rays are refracted. + +The effect `thickness` in a solid volume with `refractionMode` set to `screenSpace` is shown in +figure [thicknessProperty] (click on the image to see a larger version). Note how the `thickness` +value not only changes the effect of `absorption` but also modifies the direction of the refracted +light. + +![Figure [thicknessProperty]: `thickness` varying from 0.0 +(left) to 2.0 (right)](images/materials/thickness.png) + +Figure [varyingThickness] shows what a prism with spatially varying `thickness` looks like when +the `refractionType` is set to `solid` and `absorption` coefficients are set. + +![Figure [varyingThickness]: `thickness` varying from 0.0 at the top of the prism to 3.0 at the +bottom of the prism](images/material_thickness.png) + +## Subsurface model + +### Thickness + +### Subsurface color + +### Subsurface power + +## Cloth model + +All the material models described previously are designed to simulate dense surfaces, both at a +macro and at a micro level. Clothes and fabrics are however often made of loosely connected threads +that absorb and scatter incident light. When compared to hard surfaces, cloth is characterized by +a softer specular lob with a large falloff and the presence of fuzz lighting, caused by +forward/backward scattering. Some fabrics also exhibit two-tone specular colors +(velvets for instance). + +Figure [materialCloth] shows how the standard material model fails to capture the appearance of a +sample of denim fabric. The surface appears rigid (almost plastic-like), more similar to a tarp +than a piece of clothing. This figure also shows how important the softer specular lobe caused by +absorption and scattering is to the faithful recreation of the fabric. + +![Figure [materialCloth]: Comparison of denim fabric rendered using the standard model +(left) and the cloth model (right)](images/screenshot_cloth.png) + +Velvet is an interesting use case for a cloth material model. As shown in figure [materialVelvet] +this type of fabric exhibits strong rim lighting due to forward and backward scattering. These +scattering events are caused by fibers standing straight at the surface of the fabric. When the +incident light comes from the direction opposite to the view direction, the fibers will forward +scatter the light. Similarly, when the incident light from the same direction as the view +direction, the fibers will scatter the light backward. + +![Figure [materialVelvet]: Velvet fabric showcasing forward and +backward scattering](images/screenshot_cloth_velvet.png) + +It is important to note that there are types of fabrics that are still best modeled by hard surface +material models. For instance, leather, silk and satin can be recreated using the standard or +anisotropic material models. + +The cloth material model encompasses all the parameters previously defined for the standard +material mode except for _metallic_ and _reflectance_. Two extra parameters described in +table [clothProperties] are also available. + + + Parameter | Definition +---------------------:|:--------------------- +**sheenColor** | Specular tint to create two-tone specular fabrics (defaults to $\sqrt{baseColor}$) +**subsurfaceColor** | Tint for the diffuse color after scattering and absorption through the material +[Table [clothProperties]: Cloth model parameters] + +The type and range of each property is described in table [clothPropertiesTypes]. + + Property | Type | Range | Note +---------------------:|:--------:|:------------------------:|:------------------------- +**sheenColor** | float3 | [0..1] | Linear RGB +**subsurfaceColor** | float3 | [0..1] | Linear RGB +[Table [clothPropertiesTypes]: Range and type of the cloth model's properties] + +To create a velvet-like material, the base color can be set to black (or a dark color). +Chromaticity information should instead be set on the sheen color. To create more common fabrics +such as denim, cotton, etc. use the base color for chromaticity and use the default sheen color +or set the sheen color to the luminance of the base color. + +!!! Tip + To see the effect of the `roughness` parameter make sure the `sheenColor` is brighter than + `baseColor`. This can be used to create a fuzz effect. Taking the luminance of `baseColor` + as the `sheenColor` will produce a fairly natural effect that works for common cloth. A dark + `baseColor` combined with a bright/saturated `sheenColor` can be used to create velvet. + +!!! Tip + The `subsurfaceColor` parameter should be used with care. High values can interfere with shadows + in some areas. It is best suited for subtle transmission effects through the material. + +### Sheen color + +The `sheenColor` property can be used to directly modify the specular reflectance. It offers +better control over the appearance of cloth and gives give the ability to create +two-tone specular materials. + +The effect of `sheenColor` is shown in figure [materialClothSheen] +(click on the image to see a larger version). + +![Figure [materialClothSheen]: Blue fabric without (left) and with (right) sheen](images/screenshot_cloth_sheen.png) + +### Subsurface color + +The `subsurfaceColor` property is not physically-based and can be used to simulate the scattering, +partial absorption and re-emission of light in certain types of fabrics. This is particularly +useful to create softer fabrics. + +!!! Warning + The cloth material model is more expensive to compute when the `subsurfaceColor` property is used. + +The effect of `subsurfaceColor` is shown in figure [materialClothSubsurface] +(click on the image to see a larger version). + +![Figure [materialClothSubsurface]: White cloth (left column) vs white cloth with +brown subsurface scatting (right)](images/screenshot_cloth_subsurface.png) + +## Unlit model + +The unlit material model can be used to turn off all lighting computations. Its primary purpose is +to render pre-lit elements such as a cubemap, external content (such as a video or camera stream), +user interfaces, visualization/debugging etc. The unlit model exposes only two properties described +in table [unlitProperties]. + + Property | Definition +---------------------:|:--------------------- +**baseColor** | Surface diffuse color +**emissive** | Additional diffuse color to simulate emissive surfaces. This property is mostly useful in an HDR pipeline with a bloom pass +**postLightingColor** | Additional color to blend with base color and emissive +[Table [unlitProperties]: Properties of the standard model] + +The type and range of each property is described in table [unlitPropertiesTypes]. + + Property | Type | Range | Note +---------------------:|:--------:|:------------------------:|:------------------------- +**baseColor** | float4 | [0..1] | Pre-multiplied linear RGB +**emissive** | float4 | rgb=[0..n], a=[0..1] | Linear RGB intensity in nits, alpha encodes the exposure weight +**postLightingColor** | float4 | [0..1] | Pre-multiplied linear RGB +[Table [unlitPropertiesTypes]: Range and type of the unlit model's properties] + +The value of `postLightingColor` is blended with the sum of `emissive` and `baseColor` according to +the blending mode specified by the `postLightingBlending` material option. + +Figure [materialUnlit] shows an example of the unlit material model +(click on the image to see a larger version). + +![Figure [materialUnlit]: The unlit model is used to render debug information](images/screenshot_unlit.jpg) + +## Specular glossiness + +This alternative lighting model exists to comply with legacy standards. Since it is not a +physically-based formulation, we do not recommend using it except when loading legacy assets. + +This model encompasses the parameters previously defined for the standard lit mode except for +_metallic_, _reflectance_, and _roughness_. It adds parameters for _specularColor_ and _glossiness_. + +Parameter | Definition +---------------------:|:--------------------- +**baseColor** | Surface diffuse color +**specularColor** | Specular tint (defaults to black) +**glossiness** | Glossiness (defaults to 0.0) +[Table [glossinessProperties]: Properties of the specular-glossiness shading model] + +The type and range of each property is described in table [glossinessPropertiesTypes]. + + Property | Type | Range | Note +---------------------:|:--------:|:------------------------:|:------------------------- +**baseColor** | float4 | [0..1] | Pre-multiplied linear RGB +**specularColor** | float3 | [0..1] | Linear RGB +**glossiness** | float | [0..1] | Inverse of roughness +[Table [glossinessPropertiesTypes]: Range and type of the specular-glossiness model's properties] + +# Material definitions + +A material definition is a text file that describes all the information required by a material: + +- Name +- User parameters +- Material model +- Required attributes +- Interpolants (called _variables_) +- Raster state (blending mode, etc.) +- Shader code (fragment shader, optionally vertex shader) + +## Format + +The material definition format is a format loosely based on [JSON](https://www.json.org/) that we +call _JSONish_. At the top level a material definition is composed of 3 different blocks that use +the JSON object notation: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + // material properties +} + +vertex { + // vertex shader, optional +} + +fragment { + // fragment shader +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A minimum viable material definition must contain a `material` preamble and a `fragment` block. The +`vertex` block is optional. + +### Differences with JSON + +In JSON, an object is made of key/value _pairs_. A JSON pair has the following syntax: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +"key" : value +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Where value can be a string, number, object, array or a literal (`true`, `false` or `null`). While +this syntax is perfectly valid in a material definition, a variant without quotes around strings is +also accepted in JSONish: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +key : value +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Quotes remain mandatory when the string contains spaces. + +The `vertex` and `fragment` blocks contain unescaped, unquoted GLSL code, which is not valid in JSON. + +Single-line C++-style comments are allowed. + +The key of a pair is case-sensitive. + +The value of a pair is not case-sensitive. + +### Example + +The following code listing shows an example of a valid material definition. This definition uses +the _lit_ material model (see Lit model section), uses the default opaque blending mode, requires +that a set of UV coordinates be presented in the rendered mesh and defines 3 user parameters. The +following sections of this document describe the `material` and `fragment` blocks in detail. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + name : "Textured material", + parameters : [ + { + type : sampler2d, + name : texture + }, + { + type : float, + name : metallic + }, + { + type : float, + name : roughness + } + ], + requires : [ + uv0 + ], + shadingModel : lit, + blending : opaque +} + +fragment { + void material(inout MaterialInputs material) { + prepareMaterial(material); + material.baseColor = texture(materialParams_texture, getUV0()); + material.metallic = materialParams.metallic; + material.roughness = materialParams.roughness; + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +## Material block + +The material block is mandatory block that contains a list of property pairs to describe all +non-shader data. + +### General: name + +Type +: `string` + +Value +: Any string. Double quotes are required if the name contains spaces. + +Description +: Sets the name of the material. The name is retained at runtime for debugging purpose. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + name : stone +} + +material { + name : "Wet pavement" +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### General: featureLevel + +Type +: `number` + +Value +: An integer value, either 1, 2 or 3. Defaults to 1. + + Feature Level | Guaranteed features +:----------------------|:--------------------------------- +1 | 9 textures per material +2 | 9 textures per material, cubemap arrays, ESSL 3.10 +3 | 12 textures per material, cubemap arrays, ESSL 3.10 +[Table [featureLevels]: Feature levels] + +Description +: Sets the feature level of the material. Each feature level defines a set of features the + material can use. If the material uses a feature not supported by the selected level, `matc` + will generate an error during compilation. A given feature level is guaranteed to support + all features of lower feature levels. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + featureLevel : 2 +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Bugs +: `matc` doesn't verify that a material is not using features above its selected feature level. + + +### General: shadingModel + +Type +: `string` + +Value +: Any of `lit`, `subsurface`, `cloth`, `unlit`, `specularGlossiness`. Defaults to `lit`. + +Description +: Selects the material model as described in the Material models section. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + shadingModel : unlit +} + +material { + shadingModel : "subsurface" +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### General: parameters + +Type +: array of parameter objects + +Value +: Each entry is an object with the properties `name` and `type`, both of `string` type. The + name must be a valid GLSL identifier. Entries have an optional `precision`, which can be + one of `default` (best precision for the platform, typically `high` on desktop, `medium` on + mobile), `low`, `medium`, `high`. The type must be one of the types described in + table [materialParamsTypes]. For Android external textures, entries also have an optional + transformName parameter to specify the name of the material parameter that will be + used to expose the transform matrix associated with the external sampler. In iOS and Vulkan, + this will always be identity. + + Type | Description +:----------------------|:--------------------------------- +bool | Single boolean +bool2 | Vector of 2 booleans +bool3 | Vector of 3 booleans +bool4 | Vector of 4 booleans +float | Single float +float2 | Vector of 2 floats +float3 | Vector of 3 floats +float4 | Vector of 4 floats +int | Single integer +int2 | Vector of 2 integers +int3 | Vector of 3 integers +int4 | Vector of 4 integers +uint | Single unsigned integer +uint2 | Vector of 2 unsigned integers +uint3 | Vector of 3 unsigned integers +uint4 | Vector of 4 unsigned integers +float3x3 | Matrix of 3x3 floats +float4x4 | Matrix of 4x4 floats +sampler2d | 2D texture +sampler2dArray | Array of 2D textures +samplerExternal | External texture (platform-specific) +samplerCubemap | Cubemap texture +[Table [materialParamsTypes]: Material parameter types] + +Samplers +: Sampler types can have the following fields: + - `format` : which can be either `int` or `float` (defaults to `float`). + - `multisample` : a boolean to indicate whether the sampler is meant for multisampling (defaults to `false`) + - `filterable` : a boolean to indicate whether the sampling is filterable + - When the `format` is `int`, `filterable` is assumed to be `false`, and setting this attribute is not allowed. + - When the `format` is `float`, the default of `filterable` is `true`. The client must explicitly + set it to `false` if they wish for unfiltered sampling. + +Arrays +: A parameter can define an array of values by appending `[size]` after the type name, where + `size` is a positive integer. For instance: `float[9]` declares an array of nine `float` + values. This syntax does not apply to samplers as arrays are treated as separate types. + +Description +: Lists the parameters required by your material. These parameters can be set at runtime using + Filament's material API. Accessing parameters from the shaders varies depending on the type of + parameter: + + - **Samplers types**: use the parameter name prefixed with `materialParams_`. For instance, + `materialParams_myTexture`. + - **Other types**: use the parameter name as the field of a structure called `materialParams`. + For instance, `materialParams.myColor`. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + parameters : [ + { + type : float4, + name : albedo + }, + { + type : sampler2d, + format : float, + precision : high, + name : roughness + }, + { + type : float2, + name : metallicReflectance + } + ], + requires : [ + uv0 + ], + shadingModel : lit, +} + +fragment { + void material(inout MaterialInputs material) { + prepareMaterial(material); + material.baseColor = materialParams.albedo; + material.roughness = texture(materialParams_roughness, getUV0()); + material.metallic = materialParams.metallicReflectance.x; + material.reflectance = materialParams.metallicReflectance.y; + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### General: constants + +Type +: array of constant objects + +Value +: Each entry is an object with the properties `name` and `type`, both of `string` type. The name + must be a valid GLSL identifier. Entries also have an optional `default`, which can either be a + `bool` or `number`, depending on the `type` of the constant. The type must be one of the types + described in table [materialConstantsTypes]. + + Type | Description | Default +:----------------------|:-----------------------------------------|:------------------ +int | A signed, 32 bit GLSL int | 0 +float | A single-precision GLSL float | 0.0 +bool | A GLSL bool | false +[Table [materialConstantsTypes]: Material constants types] + +Description +: Lists the constant parameters accepted by your material. These constants can be set, or + "specialized", at runtime when loading a material package. Multiple materials can be loaded from + the same material package with differing constant parameter specializations. Once a material is + loaded from a material package, its constant parameters cannot be changed. Compared to regular + parameters, constant parameters allow the compiler to generate more efficient code. Access + constant parameters from the shader by prefixing the name with `materialConstant_`. For example, + a constant parameter named `myConstant` is accessed in the shader as + `materialConstant_myConstant`. If a constant parameter is not set at runtime, the default is + used. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + constants : [ + { + name : overrideAlpha, + type : bool + }, + { + name : customAlpha, + type : float, + default : 0.5 + } + ], + shadingModel : lit, + blending : transparent, +} + +fragment { + void material(inout MaterialInputs material) { + prepareMaterial(material); + if (materialConstants_overrideAlpha) { + material.baseColor.a = materialConstants_customAlpha; + material.baseColor.rgb *= material.baseColor.a; + } + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### General: variantFilter + +Type +: array of `string` + +Value +: Each entry must be any of `dynamicLighting`, `directionalLighting`, `shadowReceiver`, + `skinning`, `ssr`, or `stereo`. + +Description +: Used to specify a list of shader variants that the application guarantees will never be + needed. These shader variants are skipped during the code generation phase, thus reducing + the overall size of the material. + Note that some variants may automatically be filtered out. For instance, all lighting related + variants (`directionalLighting`, etc.) are filtered out when compiling an `unlit` material. + Use the variant filter with caution, filtering out a variant required at runtime may lead + to crashes. + +Description of the variants: +- `directionalLighting`, used when a directional light is present in the scene +- `dynamicLighting`, used when a non-directional light (point, spot, etc.) is present in the scene +- `shadowReceiver`, used when an object can receive shadows +- `skinning`, used when an object is animated using GPU skinning +- `fog`, used when global fog is applied to the scene +- `vsm`, used when VSM shadows are enabled and the object is a shadow receiver +- `ssr`, used when screen-space reflections are enabled in the View +- `stereo`, used when stereoscopic rendering is enabled in the View + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + name : "Invisible shadow plane", + shadingModel : unlit, + shadowMultiplier : true, + blending : transparent, + variantFilter : [ skinning ] +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### General: flipUV + +Type +: `boolean` + +Value +: `true` or `false`. Defaults to `true`. + +Description +: When set to `true` (default value), the Y coordinate of UV attributes will be flipped when + read by this material's vertex shader. Flipping is equivalent to `y = 1.0 - y`. When set + to `false`, flipping is disabled and the UV attributes are read as is. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + flipUV : false +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### General: linearFog + +Type +: `boolean` + +Value +: `true` or `false`. Defaults to `false`. + +Description +: When set to `true`, a simplified fog equation is used for large-scale fog calculations. In this mode, + in-scattering is ignored as well as height falloff. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + linearFog : true +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### General: shadowFarAttenuation + +Type +: `boolean` + +Value +: `true` or `false`. Defaults to `true`. + +Description +: When set to `false`, the directional light shadow is no longer attenuated at near the far plane. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + shadowFarAttenuation : true +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### General: quality + +Type +: `string` + +Value +: Any of `low`, `normal`, `high`, `default`. Defaults to `default`. + +Description +: Set some global quality parameters of the material. `low` enables optimizations that can + slightly affect correctness and is the default on mobile platforms. `normal` does not affect + correctness and is otherwise similar to `low`. `high` enables quality settings that can + adversely affect performance and is the default on desktop platforms. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + quality : default +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### General: instanced + +Type +: `boolean` + +Value +: `true` or `false`. Defaults to `false`. + +Description +: Allows a material to access the instance index (i.e.: **`gl_InstanceIndex`**) of instanced + primitives using `getInstanceIndex()` in the material's shader code. Never use + **`gl_InstanceIndex`** directly. This is typically used with + `RenderableManager::Builder::instances()`. `getInstanceIndex()` is available in both the + vertex and fragment shader. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + instanced : true +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### General: vertexDomainDeviceJittered + +Type +: `boolean` + +Value +: `true` or `false`. Defaults to `false`. + +Description +: Only meaningful for `vertexDomain:Device` materials, this parameter specifies whether the + filament clip-space transforms need to be applied or not, which affects TAA and guard bands. + Generally it needs to be applied because by definition `vertexDomain:Device` materials + vertices are not transformed and used *as is*. + However, if the vertex shader uses for instance `getViewFromClipMatrix()` (or other + matrices based on the projection), the clip-space transform is already applied. + Setting this parameter incorrectly can prevent TAA or the guard bands to work correctly. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + vertexDomainDeviceJittered : true +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### General: useDefaultDepthVariant + +Type +: `boolean` + +Value +: `true` or `false`. Defaults to `false`. + +Description +: This parameter forces Filament to use its default variant for depth passes, such as those used + in shadow rendering. This provides an optimization for materials with expensive custom vertex + shaders. For example, custom vertex shader computations intended to be consumed by the fragment + stage can be skipped during the depth-only pass. This parameter is only meaningful if the + material has a vertex block. + This parameter should not be set to `true` for vertex blocks that modify geometry (i.e., + modifying `worldPosition`), otherwise shadows may render incorrectly. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + variables : [ + customColor + ], + useDefaultDepthVariant : true +} + +vertex { + void materialVertex(inout MaterialVertexInputs material) { + material.customColor = /* expensive computation that can be skipped for depth-only passes */ + } +} + +fragment { + void material(inout MaterialInputs material) { + prepareMaterial(material); + material.baseColor = variable_customColor; + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Vertex and attributes: requires + +Type +: array of `string` + +Value +: Each entry must be any of `uv0`, `uv1`, `color`, `position`, `tangents`, `custom0` + through `custom7`. + +Description +: Lists the vertex attributes required by the material. The `position` attribute is always + required and does not need to be specified. The `tangents` attribute is automatically required + when selecting any shading model that is not `unlit`. See the shader sections of this document + for more information on how to access these attributes from the shaders. + +!!! Note: Interaction with custom variables + When the `color` attribute is specified, only four custom variables are available instead of five. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + parameters : [ + { + type : sampler2d, + name : texture + }, + ], + requires : [ + uv0, + custom0 + ], + shadingModel : lit, +} + +fragment { + void material(inout MaterialInputs material) { + prepareMaterial(material); + material.baseColor = texture(materialParams_texture, getUV0()); + material.baseColor.rgb *= getCustom0().rgb; + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Vertex and attributes: variables + +Type +: array of `string` + +Value +: Up to 5 strings, each must be a valid GLSL identifier. + +Description +: Defines custom interpolants (or variables) that are output by the material's vertex shader. + Each entry of the array defines the name of an interpolant. The full name in the fragment + shader is the name of the interpolant with the `variable_` prefix. For instance, if you + declare a variable called `eyeDirection` you can access it in the fragment shader using + `variable_eyeDirection`. In the vertex shader, the interpolant name is simply a member of + the `MaterialVertexInputs` structure (`material.eyeDirection` in your example). Each + interpolant is of type `float4` (`vec4`) in the shaders. By default the precision of the + interpolant is `highp` in *both* the vertex and fragment shaders. + An alternate syntax can be used to specify both the name and precision of the interpolant. + In this case the specified precision is used as-is in both fragment and vertex stages, in + particular if `default` is specified the default precision is used is the fragment shader + (`mediump`) and in the vertex shader (`highp`). + +!!! Warning: Interaction with required attributes + If the `color` attribute is specified in the `required` list, then only four variables can be used + instead of five. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + name : Skybox, + parameters : [ + { + type : samplerCubemap, + name : skybox + } + ], + variables : [ + eyeDirection, + { + name : eyeColor, + precision : medium + } + ], + vertexDomain : device, + depthWrite : false, + shadingModel : unlit +} + +fragment { + void material(inout MaterialInputs material) { + prepareMaterial(material); + float3 sky = texture(materialParams_skybox, variable_eyeDirection.xyz).rgb; + material.baseColor = vec4(sky, 1.0); + } +} + +vertex { + void materialVertex(inout MaterialVertexInputs material) { + float3 p = getPosition().xyz; + float3 u = mulMat4x4Float3(getViewFromClipMatrix(), p).xyz; + material.eyeDirection.xyz = mulMat3x3Float3(getWorldFromViewMatrix(), u); + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Vertex and attributes: vertexDomain + +Type +: `string` + +Value +: Any of `object`, `world`, `view`, `device`. Defaults to `object`. + +Description +: Defines the domain (or coordinate space) of the rendered mesh. The domain influences how the + vertices are transformed in the vertex shader. The possible domains are: + + - **Object**: the vertices are defined in the object (or model) coordinate space. The + vertices are transformed using the rendered object's transform matrix + - **World**: the vertices are defined in world coordinate space. The vertices are not + transformed using the rendered object's transform. + - **View**: the vertices are defined in view (or eye or camera) coordinate space. The + vertices are not transformed using the rendered object's transform. + - **Device**: the vertices are defined in normalized device (or clip) coordinate space. + The vertices are not transformed using the rendered object's transform. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + vertexDomain : device +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Vertex and attributes: interpolation + +Type +: `string` + +Value +: Any of `smooth`, `flat`. Defaults to `smooth`. + +Description +: Defines how interpolants (or variables) are interpolated between vertices. When this property + is set to `smooth`, a perspective correct interpolation is performed on each interpolant. + When set to `flat`, no interpolation is performed and all the fragments within a given + triangle will be shaded the same. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + interpolation : flat +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Blending and transparency: blending + +Type +: `string` + +Value +: Any of `opaque`, `transparent`, `fade`, `add`, `masked`, `multiply`, `screen`, `custom`. Defaults to `opaque`. + +Description +: Defines how/if the rendered object is blended with the content of the render target. + The possible blending modes are: + + - **Opaque**: blending is disabled, the alpha channel of the material's output is ignored. + - **Transparent**: blending is enabled. The material's output is alpha composited with the + render target, using Porter-Duff's `source over` rule. This blending mode assumes + pre-multiplied alpha. + - **Fade**: acts as `transparent` but transparency is also applied to specular lighting. In + `transparent` mode, the material's alpha values only applies to diffuse lighting. This + blending mode is useful to fade lit objects in and out. + - **Add**: blending is enabled. The material's output is added to the content of the + render target. + - **Multiply**: blending is enabled. The material's output is multiplied with the content of the + render target, darkening the content. + - **Screen**: blending is enabled. Effectively the opposite of the `multiply`, the content of the + render target is brightened. + - **Masked**: blending is disabled. This blending mode enables alpha masking. The alpha channel + of the material's output defines whether a fragment is discarded or not. Additionally, + ALPHA_TO_COVERAGE is enabled for non-translucent views. See the maskThreshold section for more + information. + - **Custom**: blending is enabled. But the blending function is user specified. See `blendFunction`. + +!!! Note + When `blending` is set to `masked`, alpha to coverage is automatically enabled for the material. + If this behavior is undesirable, refer to the Rasterization: alphaToCoverage section to turn + alpha to coverage off using the `alphaToCoverage` property. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + blending : transparent +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Blending and transparency: blendFunction + +Type +: `object` + +Fields +: `srcRGB`, `srcA`, `dstRGB`, `dstA` + +Description +: - *srcRGB*: source function applied to the RGB channels + - *srcA*: source function applied to the alpha channel + - *srcRGB*: destination function applied to the RGB channels + - *srcRGB*: destination function applied to the alpha channel + The values possible for each functions are one of `zero`, `one`, `srcColor`, `oneMinusSrcColor`, + `dstColor`, `oneMinusDstColor`, `srcAlpha`, `oneMinusSrcAlpha`, `dstAlpha`, + `oneMinusDstAlpha`, `srcAlphaSaturate` + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + blending : custom, + blendFunction : + { + srcRGB: one, + srcA: one, + dstRGB: oneMinusSrcColor, + dstA: oneMinusSrcAlpha + } + } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Blending and transparency: postLightingBlending + +Type +: `string` + +Value +: Any of `opaque`, `transparent`, `add`. Defaults to `transparent`. + +Description +: Defines how the `postLightingColor` material property is blended with the result of the + lighting computations. The possible blending modes are: + + - **Opaque**: blending is disabled, the material will output `postLightingColor` directly. + - **Transparent**: blending is enabled. The material's computed color is alpha composited with + the `postLightingColor`, using Porter-Duff's `source over` rule. This blending mode assumes + pre-multiplied alpha. + - **Add**: blending is enabled. The material's computed color is added to `postLightingColor`. + - **Multiply**: blending is enabled. The material's computed color is multiplied with `postLightingColor`. + - **Screen**: blending is enabled. The material's computed color is inverted and multiplied with `postLightingColor`, + and the result is added to the material's computed color. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + postLightingBlending : add +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Blending and transparency: transparency + +Type +: `string` + +Value +: Any of `default`, `twoPassesOneSide` or `twoPassesTwoSides`. Defaults to `default`. + +Description +: Controls how transparent objects are rendered. It is only valid when the `blending` mode is + not `opaque` and `refractionMode` is `none`. None of these methods can accurately render + concave geometry, but in practice they are often good enough. + +The three possible transparency modes are: +- `default`: the transparent object is rendered normally (as seen in figure [transparencyDefault]), + honoring the `culling` mode, etc. +- `twoPassesOneSide`: the transparent object is first rendered in the depth buffer, then again in + the color buffer, honoring the `culling` mode. This effectively renders only half of the + transparent object as shown in figure [transparencyTwoPassesOneSide]. +- `twoPassesTwoSides`: the transparent object is rendered twice in the color buffer: first with its + back faces, then with its front faces. This mode lets you render both set of faces while reducing + or eliminating sorting issues, as shown in figure [transparencyTwoPassesTwoSides]. + `twoPassesTwoSides` can be combined with `doubleSided` for better effect. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + transparency : twoPassesOneSide +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +![Figure [transparencyDefault]: This double sided model shows the type of sorting issues transparent +objects can be subject to in `default` mode](images/screenshot_transparency_default.png) + +![Figure [transparencyTwoPassesOneSide]: In `twoPassesOneSide` mode, only one set of faces is visible +and correctly sorted](images/screenshot_twopasses_oneside.png) + +![Figure [transparencyTwoPassesTwoSides]: In `twoPassesTwoSides` mode, both set of faces are visible +and sorting issues are minimized or eliminated](images/screenshot_twopasses_twosides.png) + +### Blending and transparency: maskThreshold + +Type +: `number` + +Value +: A value between `0.0` and `1.0`. Defaults to `0.4`. + +Description +: Sets the minimum alpha value a fragment must have to not be discarded when the `blending` mode + is set to `masked`. If the fragment is not discarded, its source alpha is set to 1. When the + blending mode is not `masked`, this value is ignored. This value can be used to controlled the + appearance of alpha-masked objects. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + blending : masked, + maskThreshold : 0.5 +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Blending and transparency: refractionMode + +Type +: `string` + +Value +: Any of `none`, `cubemap`, `screenspace`. Defaults to `none`. + +Description +: Activates refraction when set to anything but `none`. A value of `cubemap` will only use the + IBL cubemap as source of refraction, while this is significantly more efficient, no scene + objects will be refracted, only the distant environment encoded in the cubemap. This mode is + adequate for an object viewer for instance. A value of `screenspace` will employ the more + advanced screen-space refraction algorithm which allows opaque objects in the scene to be + refracted. In `cubemap` mode, refracted rays are assumed to emerge from the center of the + object and the `thickness` parameter is only used for computing the absorption, but has no + impact on the refraction itself. In `screenspace` mode, refracted rays are assumed to travel + parallel to the view direction when they exit the refractive medium. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + refractionMode : cubemap, +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Blending and transparency: refractionType + +Type +: `string` + +Value +: Any of `solid`, `thin`. Defaults to `solid`. + +Description +: This is only meaningful when `refractionMode` is set to anything but `none`. `refractionType` + defines the refraction model used. `solid` is used for thick objects such as a crystal ball, + an ice cube or as sculpture. `thin` is used for thin objects such as a window, an ornament + ball or a soap bubble. In `solid` mode all refracive objects are assumed to be a sphere + tangent to the entry point and of radius `thickness`. In `thin` mode, all refractive objects + are assumed to be flat and thin and of thickness `thickness`. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + refractionMode : cubemap, + refractionType : thin, +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Rasterization: culling + +Type +: `string` + +Value +: Any of `none`, `front`, `back`, `frontAndBack`. Defaults to `back`. + +Description +: Defines which triangles should be culled: none, front-facing triangles, back-facing + triangles or all. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + culling : none +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Rasterization: colorWrite + +Type +: `boolean` + +Value +: `true` or `false`. Defaults to `true`. + +Description +: Enables or disables writes to the color buffer. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + colorWrite : false +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Rasterization: depthWrite + +Type +: `boolean` + +Value +: `true` or `false`. Defaults to `true` for opaque materials, `false` for transparent materials. + +Description +: Enables or disables writes to the depth buffer. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + depthWrite : false +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Rasterization: depthCulling + +Type +: `boolean` + +Value +: `true` or `false`. Defaults to `true`. + +Description +: Enables or disables depth testing. When depth testing is disabled, an object rendered with + this material will always appear on top of other opaque objects. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + depthCulling : false +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Rasterization: doubleSided + +Type +: `boolean` + +Value +: `true` or `false`. Defaults to `false`. + +Description +: Enables two-sided rendering and its capability to be toggled at run time. When set to `true`, + `culling` is automatically set to `none`; if the triangle is back-facing, the triangle's + normal is flipped to become front-facing. When explicitly set to `false`, this allows the + double-sidedness to be toggled at run time. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + name : "Double sided material", + shadingModel : lit, + doubleSided : true +} + +fragment { + void material(inout MaterialInputs material) { + prepareMaterial(material); + material.baseColor = materialParams.albedo; + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Rasterization: alphaToCoverage + +Type +: `boolean` + +Value +: `true` or `false`. Defaults to `false`. + +Description +: Enables or disables alpha to coverage. When alpha to coverage is enabled, the coverage of + fragment is derived from its alpha. This property is only meaningful when MSAA is enabled. + Note: setting `blending` to `masked` automatically enables alpha to coverage. If this is not + desired, you can override this behavior by setting alpha to coverage to false as in the + example below. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + name : "Alpha to coverage", + shadingModel : lit, + blending : masked, + alphaToCoverage : false +} + +fragment { + void material(inout MaterialInputs material) { + prepareMaterial(material); + material.baseColor = materialParams.albedo; + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Lighting: reflections + +Type +: `string` + +Value +: `default` or `screenspace`. Defaults to `default`. + +Description +: Controls the source of specular reflections for this material. When this property is set to + `default`, reflections only come image-based lights. When this property is set to + `screenspace`, reflections come from the screen space's color buffer in addition to + image-based lights. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + name : "Glossy metal", + reflections : screenspace +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Lighting: shadowMultiplier + +Type +: `boolean` + +Value +: `true` or `false`. Defaults to `false`. + +Description +: Only available in the `unlit` shading model. If this property is enabled, the final color + computed by the material is multiplied by the shadowing factor (or visibility). This allows to + create transparent shadow-receiving objects (for instance an invisible ground plane in AR). + This is only supported with shadows from directional lights. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + name : "Invisible shadow plane", + shadingModel : unlit, + shadowMultiplier : true, + blending : transparent +} + +fragment { + void material(inout MaterialInputs material) { + prepareMaterial(material); + // baseColor defines the color and opacity of the final shadow + material.baseColor = vec4(0.0, 0.0, 0.0, 0.7); + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Lighting: transparentShadow + +Type +: `boolean` + +Value +: `true` or `false`. Defaults to `false`. + +Description +: Enables transparent shadows on this material. When this feature is enabled, Filament emulates + transparent shadows using a dithering pattern: they work best with variance shadow maps (VSM) + and blurring enabled. The opacity of the shadow derives directly from the alpha channel of + the material's `baseColor` property. Transparent shadows can be enabled on opaque objects, + making them compatible with refractive/transmissive objects that are otherwise considered + opaque. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + name : "Clear plastic with stickers", + transparentShadow : true, + blending : transparent, + // ... +} + +fragment { + void material(inout MaterialInputs material) { + prepareMaterial(material); + material.baseColor = texture(materialParams_baseColor, getUV0()); + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +![Figure [transparentShadow]: Objects rendered with transparent shadows and blurry VSM with a +radius of 4. Model [Bottle of Water](https://sketchfab.com/3d-models/bottle-of-water-48fd4f6e90d84d89b5740ee78587d0ff) +by [T-Art](https://sketchfab.com/person-x).](images/screenshot_transparent_shadows.jpg) + +### Lighting: clearCoatIorChange + +Type +: `boolean` + +Value +: `true` or `false`. Defaults to `true`. + +Description +: When adding a clear coat layer, the change in index of refraction (IoR) is taken into account + to modify the specular color of the base layer. This appears to darken `baseColor`. When this + effect is disabled, `baseColor` is left unmodified. See figure [clearCoatIorChange] for an + example of how this property can affect a red metallic base layer. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + clearCoatIorChange : false +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +![Figure [clearCoatIorChange]: The same rough metallic ball with a clear coat layer rendered +with `clearCoatIorChange` enabled (left) and disabled +(right).](images/screenshot_clear_coat_ior_change.jpg) + +### Lighting: multiBounceAmbientOcclusion + +Type +: `boolean` + +Value +: `true` or `false`. Defaults to `false` on mobile, `true` on desktop. + +Description +: Multi-bounce ambient occlusion takes into account interreflections when applying ambient + occlusion to image-based lighting. Turning this feature on avoids over-darkening occluded + areas. It also takes the surface color into account to generate colored ambient occlusion. + Figure [multiBounceAO] compares the ambient occlusion term of a surface with and without + multi-bounce ambient occlusion. Notice how multi-bounce ambient occlusion introduces color + in the occluded areas. Figure [multiBounceAOAnimated] toggles between multi-bounce ambient + occlusion on and off on a lit brick material to highlight the effects of this property. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + multiBounceAmbientOcclusion : true +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +![Figure [multiBounceAO]: Brick texture amient occlusion map rendered with multi-bounce ambient +occclusion enabled (left) and disabled (right).](images/screenshot_multi_bounce_ao.jpg) + +![Figure [multiBounceAOAnimated]: Brick texture rendered with multi-bounce ambient +occclusion enabled and disabled.](images/screenshot_multi_bounce_ao.gif) + +### Lighting: specularAmbientOcclusion + +Type +: `string` + +Value +: `none`, `simple` or `bentNormals`. Defaults to `none` on mobile, `simple` on desktop. For + compatibility reasons, `true` and `false` are also accepted and map respectively to `simple` + and `none`. + +Description +: Static ambient occlusion maps and dynamic ambient occlusion (SSAO, etc.) apply to diffuse + indirect lighting. When setting this property to other than `none`, a new ambient occlusion + term is derived from the surface roughness and applied to specular indirect lighting. + This effect helps remove unwanted specular reflections as shown in figure [specularAO]. + When this value is set to `simple`, Filament uses a cheap but approximate method of computing + the specular ambient occlusion term. If this value is set to `bentNormals`, Filament will use + a much more accurate but much more expensive method. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + specularAmbientOcclusion : simple +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +![Figure [specularAO]: Comparison of specular ambient occlusion on and off. The effect is +particularly visible under the hose.](images/screenshot_specular_ao.gif) + +### Anti-aliasing: specularAntiAliasing + +Type +: `boolean` + +Value +: `true` or `false`. Defaults to `false`. + +Description +: Reduces specular aliasing and preserves the shape of specular highlights as an object moves + away from the camera. This anti-aliasing solution is particularly effective on glossy materials + (low roughness) but increases the cost of the material. The strength of the anti-aliasing + effect can be controlled using two other properties: `specularAntiAliasingVariance` and + `specularAntiAliasingThreshold`. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + specularAntiAliasing : true +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Anti-aliasing: specularAntiAliasingVariance + +Type +: `float` + +Value +: A value between 0 and 1, set to 0.15 by default. + +Description +: Sets the screen space variance of the filter kernel used when applying specular anti-aliasing. + Higher values will increase the effect of the filter but may increase roughness in unwanted + areas. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + specularAntiAliasingVariance : 0.2 +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Anti-aliasing: specularAntiAliasingThreshold + +Type +: `float` + +Value +: A value between 0 and 1, set to 0.2 by default. + +Description +: Sets the clamping threshold used to suppress estimation errors when applying specular + anti-aliasing. When set to 0, specular anti-aliasing is disabled. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + specularAntiAliasingThreshold : 0.1 +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Shading: customSurfaceShading + +Type +: `bool` + +Value +: `true` or `false`. Defaults to `false`. + +Description +: Enables custom surface shading when set to true. When surface shading is enabled, the fragment + shader must provide an extra function that will be invoked for every light in the scene that + may influence the current fragment. Please refer to the Custom surface shading section below + for more information. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + customSurfaceShading : true +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +## Vertex block + +The vertex block is optional and can be used to control the vertex shading stage of the material. +The vertex block must contain valid +[ESSL 3.0](https://www.khronos.org/registry/OpenGL/specs/es/3.0/GLSL_ES_Specification_3.00.pdf) code +(the version of GLSL supported in OpenGL ES 3.0). You are free to create multiple functions inside +the vertex block but you **must** declare the `materialVertex` function: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GLSL +vertex { + void materialVertex(inout MaterialVertexInputs material) { + // vertex shading code + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This function will be invoked automatically at runtime by the shading system and gives you the +ability to read and modify material properties using the `MaterialVertexInputs` structure. This full +definition of the structure can be found in the Material vertex inputs section. + +You can use this structure to compute your custom variables/interpolants or to modify the value of +the attributes. For instance, the following vertex blocks modifies both the color and the UV +coordinates of the vertex over time: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GLSL +material { + requires : [uv0, color] +} +vertex { + void materialVertex(inout MaterialVertexInputs material) { + material.color *= sin(getUserTime().x); + material.uv0 *= sin(getUserTime().x); + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In addition to the `MaterialVertexInputs` structure, your vertex shading code can use all the public +APIs listed in the Shader public APIs section. + +### Material vertex inputs + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GLSL +struct MaterialVertexInputs { + float4 color; // if the color attribute is required + float2 uv0; // if the uv0 attribute is required + float2 uv1; // if the uv1 attribute is required + float3 worldNormal; // only if the shading model is not unlit + float4 worldPosition; // always available (see note below about world-space) + + mat4 clipSpaceTransform; // default: identity, transforms the clip-space position, only available for `vertexDomain:device` + + // variable* names are replaced with actual names + float4 variable0; // if 1 or more variables is defined + float4 variable1; // if 2 or more variables is defined + float4 variable2; // if 3 or more variables is defined + float4 variable3; // if 4 or more variables is defined +}; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +!!! TIP: worldPosition + To achieve good precision, the `worldPosition` coordinate in the vertex shader is shifted by the + camera position. To get the true world-space position, users can use + `getUserWorldPosition()`, however be aware that the true world-position might not + be able to fit in a `float` or might be represented with severely reduced precision. + +!!! TIP: UV attributes + By default the vertex shader of a material will flip the Y coordinate of the UV attributes + of the current mesh: `material.uv0 = vec2(mesh_uv0.x, 1.0 - mesh_uv0.y)`. You can control + this behavior using the `flipUV` property and setting it to `false`. + +### Custom vertex attributes + +You can use up to 8 custom vertex attributes, all of type `float4`. These attributes can be accessed +using the vertex block shader functions `getCustom0()` to `getCustom7()`. However, before using +custom attributes, you *must* declare those attributes as required in the `requires` property of +the material: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON +material { + requires : [ + custom0, + custom1, + custom2 + ] +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +## Fragment block + +The fragment block must be used to control the fragment shading stage of the material. The fragment +block must contain valid +[ESSL 3.0](https://www.khronos.org/registry/OpenGL/specs/es/3.0/GLSL_ES_Specification_3.00.pdf) +code (the version of GLSL supported in OpenGL ES 3.0). You are free to create multiple functions +inside the fragment block but you **must** declare the `material` function: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GLSL +fragment { + void material(inout MaterialInputs material) { + prepareMaterial(material); + // fragment shading code + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This function will be invoked automatically at runtime by the shading system and gives you the +ability to read and modify material properties using the `MaterialInputs` structure. This full +definition of the structure can be found in the Material fragment inputs section. The full +definition of the various members of the structure can be found in the Material models section +of this document. + +The goal of the `material()` function is to compute the material properties specific to the selected +shading model. For instance, here is a fragment block that creates a glossy red metal using the +standard lit shading model: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GLSL +fragment { + void material(inout MaterialInputs material) { + prepareMaterial(material); + material.baseColor.rgb = vec3(1.0, 0.0, 0.0); + material.metallic = 1.0; + material.roughness = 0.0; + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### prepareMaterial function + +Note that you **must** call `prepareMaterial(material)` before exiting the `material()` function. +This `prepareMaterial` function sets up the internal state of the material model. Some of the APIs +described in the Fragment APIs section - like `shading_normal` for instance - can only be accessed +_after_ invoking `prepareMaterial()`. + +It is also important to remember that the `normal` property - as described in the Material fragment +inputs section - only has an effect when modified _before_ calling `prepareMaterial()`. Here is an +example of a fragment shader that properly modifies the `normal` property to implement a glossy red +plastic with bump mapping: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GLSL +fragment { + void material(inout MaterialInputs material) { + // fetch the normal in tangent space + vec3 normal = texture(materialParams_normalMap, getUV0()).xyz; + material.normal = normal * 2.0 - 1.0; + + // prepare the material + prepareMaterial(material); + + // from now on, shading_normal, etc. can be accessed + material.baseColor.rgb = vec3(1.0, 0.0, 0.0); + material.metallic = 0.0; + material.roughness = 1.0; + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Material fragment inputs + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GLSL +struct MaterialInputs { + float4 baseColor; // default: float4(1.0) + float4 emissive; // default: float4(0.0, 0.0, 0.0, 1.0) + float4 postLightingColor; // default: float4(0.0) + + // no other field is available with the unlit shading model + float roughness; // default: 1.0 + float metallic; // default: 0.0, not available with cloth or specularGlossiness + float reflectance; // default: 0.5, not available with cloth or specularGlossiness + float ambientOcclusion; // default: 0.0 + + // not available when the shading model is subsurface or cloth + float3 sheenColor; // default: float3(0.0) + float sheenRoughness; // default: 0.0 + float clearCoat; // default: 1.0 + float clearCoatRoughness; // default: 0.0 + float3 clearCoatNormal; // default: float3(0.0, 0.0, 1.0) + float anisotropy; // default: 0.0 + float3 anisotropyDirection; // default: float3(1.0, 0.0, 0.0) + + // only available when the shading model is subsurface or refraction is enabled + float thickness; // default: 0.5 + + // only available when the shading model is subsurface + float subsurfacePower; // default: 12.234 + float3 subsurfaceColor; // default: float3(1.0) + + // only available when the shading model is cloth + float3 sheenColor; // default: sqrt(baseColor) + float3 subsurfaceColor; // default: float3(0.0) + + // only available when the shading model is specularGlossiness + float3 specularColor; // default: float3(0.0) + float glossiness; // default: 0.0 + + // not available when the shading model is unlit + // must be set before calling prepareMaterial() + float3 normal; // default: float3(0.0, 0.0, 1.0) + + // only available when refraction is enabled + float transmission; // default: 1.0 + float3 absorption; // default float3(0.0, 0.0, 0.0) + float ior; // default: 1.5 + float microThickness; // default: 0.0, not available with refractionType "solid" +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Custom surface shading + +When `customSurfaceShading` is set to `true` in the material block, the fragment block **must** +declare and implement the `surfaceShading` function: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GLSL +fragment { + void material(inout MaterialInputs material) { + prepareMaterial(material); + // prepare material inputs + } + + vec3 surfaceShading( + const MaterialInputs materialInputs, + const ShadingData shadingData, + const LightData lightData + ) { + return vec3(1.0); // output of custom lighting + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This function will be invoked for every light (directional, spot or point) in the scene that may +influence the current fragment. The `surfaceShading` is invoked with 3 sets of data: + +- `MaterialInputs`, as described in the Material fragment inputs section and prepared in the + `material` function explained above +- `ShadingData`, a structure containing values derived from `MaterialInputs` (see below) +- `LightData`, a structure containing values specific to the light being currently + evaluated (see below) + +The `surfaceShading` function must return an RGB color in linear sRGB. Alpha blending and alpha +masking are handled outside of this function and must therefore be ignored. + +!!! Note: About shadowed fragments + The `surfaceShading` function is invoked even when a fragment is known to be fully in the shadow + of the current light (`lightData.NdotL <= 0.0` or `lightData.visibility <= 0.0`). This gives + more flexibility to the `surfaceShading` function as it provides a simple way to handle constant + ambient lighting for instance. + +!!! Warning: Shading models + Custom surface shading only works with the `lit` shading model. Attempting to use any other + model will result in an error. + +#### Shading data structure + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GLSL +struct ShadingData { + // The material's diffuse color, as derived from baseColor and metallic. + // This color is pre-multiplied by alpha and in the linear sRGB color space. + vec3 diffuseColor; + + // The material's specular color, as derived from baseColor and metallic. + // This color is pre-multiplied by alpha and in the linear sRGB color space. + vec3 f0; + + // The perceptual roughness is the roughness value set in MaterialInputs, + // with extra processing: + // - Clamped to safe values + // - Filtered if specularAntiAliasing is enabled + // This value is between 0.0 and 1.0. + float perceptualRoughness; + + // The roughness value expected by BRDFs. This value is the square of + // perceptualRoughness. This value is between 0.0 and 1.0. + float roughness; +}; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#### Light data structure + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GLSL +struct LightData { + // The color (.rgb) and pre-exposed intensity (.w) of the light. + // The color is an RGB value in the linear sRGB color space. + // The pre-exposed intensity is the intensity of the light multiplied by + // the camera's exposure value. + vec4 colorIntensity; + + // The normalized light vector, in world space (direction from the + // current fragment's position to the light). + vec3 l; + + // The dot product of the shading normal (with normal mapping applied) + // and the light vector. This value is equal to the result of + // saturate(dot(getWorldSpaceNormal(), lightData.l)). + // This value is always between 0.0 and 1.0. When the value is <= 0.0, + // the current fragment is not visible from the light and lighting + // computations can be skipped. + float NdotL; + + // The position of the light in world space. + vec3 worldPosition; + + // Attenuation of the light based on the distance from the current + // fragment to the light in world space. This value between 0.0 and 1.0 + // is computed differently for each type of light (it's always 1.0 for + // directional lights). + float attenuation; + + // Visibility factor computed from shadow maps or other occlusion data + // specific to the light being evaluated. This value is between 0.0 and + // 1.0. + float visibility; +}; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#### Example + +The material below shows how to use custom surface shading to implement a simplified toon shader: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +material { + name : Toon, + shadingModel : lit, + parameters : [ + { + type : float3, + name : baseColor + } + ], + customSurfaceShading : true +} + +fragment { + void material(inout MaterialInputs material) { + prepareMaterial(material); + material.baseColor.rgb = materialParams.baseColor; + } + + vec3 surfaceShading( + const MaterialInputs materialInputs, + const ShadingData shadingData, + const LightData lightData + ) { + // Number of visible shade transitions + const float shades = 5.0; + // Ambient intensity + const float ambient = 0.1; + + float toon = max(ceil(lightData.NdotL * shades) / shades, ambient); + + // Shadowing and attenuation + toon *= lightData.visibility * lightData.attenuation; + + // Color and intensity + vec3 light = lightData.colorIntensity.rgb * lightData.colorIntensity.w; + + return shadingData.diffuseColor * light * toon; + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The result can be seen in figure [toonShading]. + +![Figure [toonShading]: simple toon shading implemented with custom +surface shading](images/screenshot_toon_shading.png) + + +## Shader public APIs + +### Types + +While GLSL types can be used directly (`vec4` or `mat4`) we recommend the use of the following +type aliases: + + Name | GLSL type | Description +:--------------------------------|:------------:|:------------------------------------ +**bool2** | bvec2 | A vector of 2 booleans +**bool3** | bvec3 | A vector of 3 booleans +**bool4** | bvec4 | A vector of 4 booleans +**int2** | ivec2 | A vector of 2 integers +**int3** | ivec3 | A vector of 3 integers +**int4** | ivec4 | A vector of 4 integers +**uint2** | uvec2 | A vector of 2 unsigned integers +**uint3** | uvec3 | A vector of 3 unsigned integers +**uint4** | uvec4 | A vector of 4 unsigned integers +**float2** | float2 | A vector of 2 floats +**float3** | float3 | A vector of 3 floats +**float4** | float4 | A vector of 4 floats +**float4x4** | mat4 | A 4x4 float matrix +**float3x3** | mat3 | A 3x3 float matrix + +### Math + Name | Type | Description +:-----------------------------------------|:--------:|:------------------------------------ +**PI** | float | A constant that represent $\pi$ +**HALF_PI** | float | A constant that represent $\frac{\pi}{2}$ +**saturate(float x)** | float | Clamps the specified value between 0.0 and 1.0 +**pow5(float x)** | float | Computes $x^5$ +**sq(float x)** | float | Computes $x^2$ +**max3(float3 v)** | float | Returns the maximum value of the specified `float3` +**mulMat4x4Float3(float4x4 m, float3 v)** | float4 | Returns $m * v$ +**mulMat3x3Float3(float4x4 m, float3 v)** | float4 | Returns $m * v$ + +### Matrices + + Name | Type | Description +:-----------------------------------------|:--------:|:------------------------------------ +**getViewFromWorldMatrix()** | float4x4 | Matrix that converts from world space to view/eye space +**getWorldFromViewMatrix()** | float4x4 | Matrix that converts from view/eye space to world space +**getClipFromViewMatrix()** | float4x4 | Matrix that converts from view/eye space to clip (NDC) space +**getViewFromClipMatrix()** | float4x4 | Matrix that converts from clip (NDC) space to view/eye space +**getEyeFromViewMatrix()** | float4x4 | Matrix that converts from view space to eye space +**getEyeFromViewMatrix(int eyeIndex)** | float4x4 | Matrix that converts from view space to eye space for the eye referred to by eyeIndex +**getClipFromWorldMatrix()** | float4x4 | Matrix that converts from world to clip (NDC) space +**getClipFromWorldMatrix(int eyeIndex)** | float4x4 | Matrix that converts from world to clip (NDC) space for the eye referred to by eyeIndex +**getWorldFromClipMatrix()** | float4x4 | Matrix that converts from clip (NDC) space to world space + +### Frame constants + + Name | Type | Description +:-----------------------------------|:--------:|:------------------------------------ +**getResolution()** | float4 | Dimensions of the view's effective (physical) viewport in pixels: `width`, `height`, `1 / width`, `1 / height`. This might be different from `View::getViewport()` for instance because of added rendering guard-bands. +**getWorldCameraPosition()** | float3 | Position of the camera/eye in world space (see note below) +**getWorldOffset()** | float3 | [deprecated] The shift required to obtain API-level world space. Use getUserWorldPosition() instead +**getUserWorldFromWorldMatrix()** | float4x4 | Matrix that converts from world space to API-level (user) world space. +**getTime()** | float | Current time as a remainder of 1 second. Yields a value between 0 and 1 +**getUserTime()** | float4 | Current time in seconds: `time`, `(double)time - time`, `0`, `0` +**getUserTimeMod(float m)** | float | Current time modulo m in seconds +**getExposure()** | float | Photometric exposure of the camera +**getEV100()** | float | [Exposure value at ISO 100](https://en.wikipedia.org/wiki/Exposure_value) of the camera + +!!! TIP: world space + To achieve good precision, the "world space" in Filament's shading system does not necessarily + match the API-level world space. To obtain the position of the API-level camera, custom + materials can use `getUserWorldFromWorldMatrix()` to transform `getWorldCameraPosition()`. + +### Material globals + + Name | Type | Description +:-----------------------------------|:--------:|:------------------------------------ +**getMaterialGlobal0()** | float4 | A vec4 visible by all materials, its value is set by `View::setMaterialGlobal(0, float4)`. Its default value is {0,0,0,1}. +**getMaterialGlobal1()** | float4 | A vec4 visible by all materials, its value is set by `View::setMaterialGlobal(1, float4)`. Its default value is {0,0,0,1}. +**getMaterialGlobal2()** | float4 | A vec4 visible by all materials, its value is set by `View::setMaterialGlobal(2, float4)`. Its default value is {0,0,0,1}. +**getMaterialGlobal3()** | float4 | A vec4 visible by all materials, its value is set by `View::setMaterialGlobal(3, float4)`. Its default value is {0,0,0,1}. + +### Vertex only + +The following APIs are only available from the vertex block: + + Name | Type | Description +:------------------------------------|:--------:|:------------------------------------ +**getPosition()** | float4 | Vertex position in the domain defined by the material (default: object/model space) +**getCustom0()** to **getCustom7()** | float4 | Custom vertex attribute +**getWorldFromModelMatrix()** | float4x4 | Matrix that converts from model (object) space to world space +**getWorldFromModelNormalMatrix()** | float3x3 | Matrix that converts normals from model (object) space to world space +**getVertexIndex()** | int | Index of the current vertex +**getEyeIndex()** | int | Index of the eye being rendered, starting at 0 + +### Fragment only + +The following APIs are only available from the fragment block: + + Name | Type | Description +:---------------------------------------|:--------:|:------------------------------------ +**getWorldTangentFrame()** | float3x3 | Matrix containing in each column the `tangent` (`frame[0]`), `bi-tangent` (`frame[1]`) and `normal` (`frame[2]`) of the vertex in world space. If the material does not compute a tangent space normal for bump mapping or if the shading is not anisotropic, only the `normal` is valid in this matrix. +**getWorldPosition()** | float3 | Position of the fragment in world space (see note below about world-space) +**getUserWorldPosition()** | float3 | Position of the fragment in API-level (user) world-space (see note below about world-space) +**getWorldViewVector()** | float3 | Normalized vector in world space from the fragment position to the eye +**getWorldNormalVector()** | float3 | Normalized normal in world space, after bump mapping (must be used after `prepareMaterial()`) +**getWorldGeometricNormalVector()** | float3 | Normalized normal in world space, before bump mapping (can be used before `prepareMaterial()`) +**getWorldReflectedVector()** | float3 | Reflection of the view vector about the normal (must be used after `prepareMaterial()`) +**getNormalizedViewportCoord()** | float3 | Normalized user viewport position (i.e. NDC coordinates normalized to [0, 1] for the position, [1, 0] for the depth), can be used before `prepareMaterial()`). Because the user viewport is smaller than the actual physical viewport, these coordinates can be negative or superior to 1 in the non-visible area of the physical viewport. +**getNdotV()** | float | The result of `dot(normal, view)`, always strictly greater than 0 (must be used after `prepareMaterial()`) +**getColor()** | float4 | Interpolated color of the fragment, if the color attribute is required +**getUV0()** | float2 | First interpolated set of UV coordinates, only available if the uv0 attribute is required +**getUV1()** | float2 | First interpolated set of UV coordinates, only available if the uv1 attribute is required +**getMaskThreshold()** | float | Returns the mask threshold, only available when `blending` is set to `masked` +**inverseTonemap(float3)** | float3 | Applies the inverse tone mapping operator to the specified linear sRGB color and returns a linear sRGB color. This operation may be an approximation and works best with the "Filmic" tone mapping operator +**inverseTonemapSRGB(float3)** | float3 | Applies the inverse tone mapping operator to the specified non-linear sRGB color and returns a linear sRGB color. This operation may be an approximation and works best with the "Filmic" tone mapping operator +**luminance(float3)** | float | Computes the luminance of the specified linear sRGB color +**ycbcrToRgb(float, float2)** | float3 | Converts a luminance and CbCr pair to a sRGB color +**uvToRenderTargetUV(float2)** | float2 | Transforms a UV coordinate to allow sampling from a `RenderTarget` attachment + +!!! TIP: world-space + To obtain API-level world-space coordinates, custom materials should use `getUserWorldPosition()` + or use `getUserWorldFromWorldMatrix()`. Note that API-level world-space coordinates should + never or rarely be used because they may not fit in a float3 or have severely reduced precision. + +!!! TIP: sampling from render targets + When sampling from a `filament::Texture` that is attached to a `filament::RenderTarget` for + materials in the surface domain, please use `uvToRenderTargetUV` to transform the texture + coordinate. This will flip the coordinate depending on which backend is being used. + +# Compiling materials + +Material packages can be compiled from material definitions using the command line tool called +`matc`. The simplest way to use `matc` is to specify an input material definition (`car_paint.mat` +in the example below) and an output material package (`car_paint.filamat` in the example below): + +```text +$ matc -o ./materials/bin/car_paint.filamat ./materials/src/car_paint.mat +``` + +## Shader validation + +`matc` attempts to validate shaders when compiling a material package. The example below shows an +example of an error message generated when compiling a material definition containing a typo in the +fragment shader (`metalic` instead of `metallic`). The reported line numbers are line numbers in the +source material definition file. + +```text +ERROR: 0:13: 'metalic' : no such field in structure +ERROR: 0:13: '' : compilation terminated +ERROR: 2 compilation errors. No code generated. + +Could not compile material metal.mat +``` + +## Flags + +The command line flags relevant to application development are described in table [matcFlags]. + + Flag | Value | Usage +-------------------------------:|:------------------:|:--------------------- +**-o**, **--output** | [path] | Specify the output file path +**-p**, **--platform** | desktop/mobile/all | Select the target platform(s) +**-a**, **--api** | opengl/vulkan/all | Specify the target graphics API +**-S**, **--optimize-size** | N/A | Optimize compiled material for size instead of just performance +**-r**, **--reflect** | parameters | Outputs the specified metadata as JSON +**-v**, **--variant-filter** | [variant] | Filters out the specified, comma-separated variants +[Table [matcFlags]: List of `matc` flags] + +`matc` offers a few other flags that are irrelevant to application developers and for internal +use only. + +### --platform + +By default, `matc` generates material packages containing shaders for all supported platforms. If +you wish to reduce the size of your material packages, it is recommended to select only the +appropriate target platform. For instance, to compile a material package for Android only, run +the following command: + +```text +$ matc -p mobile -o ./materials/bin/car_paint.filamat ./materials/src/car_paint.mat +``` + +### --api + +By default, `matc` generates material packages containing shaders for the OpenGL API. You can choose +to generate shaders for the Vulkan API in addition to the OpenGL shaders. If you intend on targeting +only Vulkan capable devices, you can reduce the size of the material packages by generating only +the set of Vulkan shaders: + +```text +$ matc -a vulkan -o ./materials/bin/car_paint.filamat ./materials/src/car_paint.mat +``` + +### --optimize-size + +This flag applies fewer optimization techniques to try and keep the final material as small as +possible. If the compiled material is deemed too large by default, using this flag might be +a good compromise between runtime performance and size. + +### --reflect + +This flag was designed to help build tools around `matc`. It allows you to print out specific +metadata in JSON format. The example below prints out the list of parameters defined in Filament's +standard skybox material. It produces a list of 2 parameters, named `showSun` and `skybox`, +respectively a boolean and a cubemap texture. + +```text +$ matc --reflect parameters filament/src/materials/skybox.mat +{ + "parameters": [ + { + "name": "showSun", + "type": "bool", + "size": "1" + }, + { + "name": "skybox", + "type": "samplerCubemap", + "format": "float", + "precision": "default" + } + ] +} +``` + +### --variant-filter + +This flag can be used to further reduce the size of a compiled material. It is used to specify a +list of shader variants that the application guarantees will never be needed. These shader variants +are skipped during the code generation phase of `matc`, thus reducing the overall size of the +material. + +The variants must be specified as a comma-separated list, using one of the following available +variants: + +- `directionalLighting`, used when a directional light is present in the scene +- `dynamicLighting`, used when a non-directional light (point, spot, etc.) is present in the scene +- `shadowReceiver`, used when an object can receive shadows +- `skinning`, used when an object is animated using GPU skinning or vertex morphing +- `fog`, used when global fog is applied to the scene +- `vsm`, used when VSM shadows are enabled and the object is a shadow receiver +- `ssr`, used when screen-space reflections are enabled in the View + +Example: +``` +--variant-filter=skinning,shadowReceiver +``` + +Note that some variants may automatically be filtered out. For instance, all lighting related +variants (`directionalLighting`, etc.) are filtered out when compiling an `unlit` material. + +When this flag is used, the specified variant filters are merged with the variant filters specified +in the material itself. + +Use this flag with caution, filtering out a variant required at runtime may lead to crashes. + +# Handling colors + +## Linear colors + +If the color data comes from a texture, simply make sure you use an sRGB texture to benefit from +automatic hardware conversion from sRGB to linear. If the color data is passed as a parameter to +the material you can convert from sRGB to linear by running the following algorithm on each +color channel: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GLSL +float sRGB_to_linear(float color) { + return color <= 0.04045 ? color / 12.92 : pow((color + 0.055) / 1.055, 2.4); +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Alternatively you can use one of the two cheaper but less accurate versions shown below: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GLSL +// Cheaper +linearColor = pow(color, 2.2); +// Cheapest +linearColor = color * color; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +## Pre-multiplied alpha + +A color uses pre-multiplied alpha if its RGB components are multiplied by the alpha channel: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GLSL +// Compute pre-multiplied color +color.rgb *= color.a; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If the color is sampled from a texture, you can simply ensure that the texture data is +pre-multiplied ahead of time. On Android, any texture uploaded from a +[Bitmap](https://developer.android.com/reference/android/graphics/Bitmap.html) will be +pre-multiplied by default. + +# Sampler usage in Materials + +The number of usable sampler parameters (e.g.: type is `sampler2d`) in materials is limited and +depends on the material properties, shading model, feature level and variant filter. + +## Feature level 1 and 2 + +`unlit` materials can use up to 12 samplers by default. + +`lit` materials can use up to 9 samplers by default, however if `refractionMode` or `reflectionMode` +is set to `screenspace` that number is reduced to 8. + +Finally if `variantFilter` contains the `fog` filter, an extra sampler is made available, such that +`unlit` materials can use up to 13 and `lit` materials up to 10 samplers by default. + +## Feature level 3 + +16 samplers are available. + +!!! TIP: external samplers + Be aware that `external` samplers account for 2 regular samplers. + + diff --git a/docs/ayu-highlight.css b/docs/ayu-highlight.css new file mode 100644 index 0000000000..a20a3ce55a --- /dev/null +++ b/docs/ayu-highlight.css @@ -0,0 +1,78 @@ +/* +Based off of the Ayu theme +Original by Dempfi (https://github.com/dempfi/ayu) +*/ + +.hljs { + display: block; + overflow-x: auto; + background: #d0d0d0; + color: #444; +} + +.hljs-comment, +.hljs-quote { + color: #5c6773; + font-style: italic; +} + +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-attr, +.hljs-regexp, +.hljs-link, +.hljs-selector-id, +.hljs-selector-class { + color: #a72; +} + +.hljs-number, +.hljs-meta, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #a72; +} + +.hljs-string, +.hljs-bullet { + color: #aac042; +} + +.hljs-title, +.hljs-built_in, +.hljs-section { + color: #a52; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-symbol { + color: #a72; +} + +.hljs-name { + color: #36a3d9; +} + +.hljs-tag { + color: #00568d; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-addition { + color: #419322; +} + +.hljs-deletion { + color: #b96c75; +} diff --git a/docs/book.js b/docs/book.js new file mode 100644 index 0000000000..178f1e902d --- /dev/null +++ b/docs/book.js @@ -0,0 +1,690 @@ +"use strict"; + +// Fix back button cache problem +window.onunload = function () { }; + +// Global variable, shared between modules +function playground_text(playground, hidden = true) { + let code_block = playground.querySelector("code"); + + if (window.ace && code_block.classList.contains("editable")) { + let editor = window.ace.edit(code_block); + return editor.getValue(); + } else if (hidden) { + return code_block.textContent; + } else { + return code_block.innerText; + } +} + +(function codeSnippets() { + function fetch_with_timeout(url, options, timeout = 6000) { + return Promise.race([ + fetch(url, options), + new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)) + ]); + } + + var playgrounds = Array.from(document.querySelectorAll(".playground")); + if (playgrounds.length > 0) { + fetch_with_timeout("https://play.rust-lang.org/meta/crates", { + headers: { + 'Content-Type': "application/json", + }, + method: 'POST', + mode: 'cors', + }) + .then(response => response.json()) + .then(response => { + // get list of crates available in the rust playground + let playground_crates = response.crates.map(item => item["id"]); + playgrounds.forEach(block => handle_crate_list_update(block, playground_crates)); + }); + } + + function handle_crate_list_update(playground_block, playground_crates) { + // update the play buttons after receiving the response + update_play_button(playground_block, playground_crates); + + // and install on change listener to dynamically update ACE editors + if (window.ace) { + let code_block = playground_block.querySelector("code"); + if (code_block.classList.contains("editable")) { + let editor = window.ace.edit(code_block); + editor.addEventListener("change", function (e) { + update_play_button(playground_block, playground_crates); + }); + // add Ctrl-Enter command to execute rust code + editor.commands.addCommand({ + name: "run", + bindKey: { + win: "Ctrl-Enter", + mac: "Ctrl-Enter" + }, + exec: _editor => run_rust_code(playground_block) + }); + } + } + } + + // updates the visibility of play button based on `no_run` class and + // used crates vs ones available on https://play.rust-lang.org + function update_play_button(pre_block, playground_crates) { + var play_button = pre_block.querySelector(".play-button"); + + // skip if code is `no_run` + if (pre_block.querySelector('code').classList.contains("no_run")) { + play_button.classList.add("hidden"); + return; + } + + // get list of `extern crate`'s from snippet + var txt = playground_text(pre_block); + var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g; + var snippet_crates = []; + var item; + while (item = re.exec(txt)) { + snippet_crates.push(item[1]); + } + + // check if all used crates are available on play.rust-lang.org + var all_available = snippet_crates.every(function (elem) { + return playground_crates.indexOf(elem) > -1; + }); + + if (all_available) { + play_button.classList.remove("hidden"); + } else { + play_button.classList.add("hidden"); + } + } + + function run_rust_code(code_block) { + var result_block = code_block.querySelector(".result"); + if (!result_block) { + result_block = document.createElement('code'); + result_block.className = 'result hljs language-bash'; + + code_block.append(result_block); + } + + let text = playground_text(code_block); + let classes = code_block.querySelector('code').classList; + let edition = "2015"; + if(classes.contains("edition2018")) { + edition = "2018"; + } else if(classes.contains("edition2021")) { + edition = "2021"; + } + var params = { + version: "stable", + optimize: "0", + code: text, + edition: edition + }; + + if (text.indexOf("#![feature") !== -1) { + params.version = "nightly"; + } + + result_block.innerText = "Running..."; + + fetch_with_timeout("https://play.rust-lang.org/evaluate.json", { + headers: { + 'Content-Type': "application/json", + }, + method: 'POST', + mode: 'cors', + body: JSON.stringify(params) + }) + .then(response => response.json()) + .then(response => { + if (response.result.trim() === '') { + result_block.innerText = "No output"; + result_block.classList.add("result-no-output"); + } else { + result_block.innerText = response.result; + result_block.classList.remove("result-no-output"); + } + }) + .catch(error => result_block.innerText = "Playground Communication: " + error.message); + } + + // Syntax highlighting Configuration + hljs.configure({ + tabReplace: ' ', // 4 spaces + languages: [], // Languages used for auto-detection + }); + + let code_nodes = Array + .from(document.querySelectorAll('code')) + // Don't highlight `inline code` blocks in headers. + .filter(function (node) {return !node.parentElement.classList.contains("header"); }); + + if (window.ace) { + // language-rust class needs to be removed for editable + // blocks or highlightjs will capture events + code_nodes + .filter(function (node) {return node.classList.contains("editable"); }) + .forEach(function (block) { block.classList.remove('language-rust'); }); + + code_nodes + .filter(function (node) {return !node.classList.contains("editable"); }) + .forEach(function (block) { hljs.highlightBlock(block); }); + } else { + code_nodes.forEach(function (block) { hljs.highlightBlock(block); }); + } + + // Adding the hljs class gives code blocks the color css + // even if highlighting doesn't apply + code_nodes.forEach(function (block) { block.classList.add('hljs'); }); + + Array.from(document.querySelectorAll("code.hljs")).forEach(function (block) { + + var lines = Array.from(block.querySelectorAll('.boring')); + // If no lines were hidden, return + if (!lines.length) { return; } + block.classList.add("hide-boring"); + + var buttons = document.createElement('div'); + buttons.className = 'buttons'; + buttons.innerHTML = ""; + + // add expand button + var pre_block = block.parentNode; + pre_block.insertBefore(buttons, pre_block.firstChild); + + pre_block.querySelector('.buttons').addEventListener('click', function (e) { + if (e.target.classList.contains('fa-eye')) { + e.target.classList.remove('fa-eye'); + e.target.classList.add('fa-eye-slash'); + e.target.title = 'Hide lines'; + e.target.setAttribute('aria-label', e.target.title); + + block.classList.remove('hide-boring'); + } else if (e.target.classList.contains('fa-eye-slash')) { + e.target.classList.remove('fa-eye-slash'); + e.target.classList.add('fa-eye'); + e.target.title = 'Show hidden lines'; + e.target.setAttribute('aria-label', e.target.title); + + block.classList.add('hide-boring'); + } + }); + }); + + if (window.playground_copyable) { + Array.from(document.querySelectorAll('pre code')).forEach(function (block) { + var pre_block = block.parentNode; + if (!pre_block.classList.contains('playground')) { + var buttons = pre_block.querySelector(".buttons"); + if (!buttons) { + buttons = document.createElement('div'); + buttons.className = 'buttons'; + pre_block.insertBefore(buttons, pre_block.firstChild); + } + + var clipButton = document.createElement('button'); + clipButton.className = 'clip-button'; + clipButton.title = 'Copy to clipboard'; + clipButton.setAttribute('aria-label', clipButton.title); + clipButton.innerHTML = ''; + + buttons.insertBefore(clipButton, buttons.firstChild); + } + }); + } + + // Process playground code blocks + Array.from(document.querySelectorAll(".playground")).forEach(function (pre_block) { + // Add play button + var buttons = pre_block.querySelector(".buttons"); + if (!buttons) { + buttons = document.createElement('div'); + buttons.className = 'buttons'; + pre_block.insertBefore(buttons, pre_block.firstChild); + } + + var runCodeButton = document.createElement('button'); + runCodeButton.className = 'fa fa-play play-button'; + runCodeButton.hidden = true; + runCodeButton.title = 'Run this code'; + runCodeButton.setAttribute('aria-label', runCodeButton.title); + + buttons.insertBefore(runCodeButton, buttons.firstChild); + runCodeButton.addEventListener('click', function (e) { + run_rust_code(pre_block); + }); + + if (window.playground_copyable) { + var copyCodeClipboardButton = document.createElement('button'); + copyCodeClipboardButton.className = 'clip-button'; + copyCodeClipboardButton.innerHTML = ''; + copyCodeClipboardButton.title = 'Copy to clipboard'; + copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title); + + buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild); + } + + let code_block = pre_block.querySelector("code"); + if (window.ace && code_block.classList.contains("editable")) { + var undoChangesButton = document.createElement('button'); + undoChangesButton.className = 'fa fa-history reset-button'; + undoChangesButton.title = 'Undo changes'; + undoChangesButton.setAttribute('aria-label', undoChangesButton.title); + + buttons.insertBefore(undoChangesButton, buttons.firstChild); + + undoChangesButton.addEventListener('click', function () { + let editor = window.ace.edit(code_block); + editor.setValue(editor.originalCode); + editor.clearSelection(); + }); + } + }); +})(); + +(function themes() { + var html = document.querySelector('html'); + var themeToggleButton = document.getElementById('theme-toggle'); + var themePopup = document.getElementById('theme-list'); + var themeColorMetaTag = document.querySelector('meta[name="theme-color"]'); + var themeIds = []; + themePopup.querySelectorAll('button.theme').forEach(function (el) { + themeIds.push(el.id); + }); + var stylesheets = { + ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"), + tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"), + highlight: document.querySelector("[href$='highlight.css']"), + }; + + function showThemes() { + themePopup.style.display = 'block'; + themeToggleButton.setAttribute('aria-expanded', true); + themePopup.querySelector("button#" + get_theme()).focus(); + } + + function updateThemeSelected() { + themePopup.querySelectorAll('.theme-selected').forEach(function (el) { + el.classList.remove('theme-selected'); + }); + themePopup.querySelector("button#" + get_theme()).classList.add('theme-selected'); + } + + function hideThemes() { + themePopup.style.display = 'none'; + themeToggleButton.setAttribute('aria-expanded', false); + themeToggleButton.focus(); + } + + function get_theme() { + var theme; + try { theme = localStorage.getItem('mdbook-theme'); } catch (e) { } + if (theme === null || theme === undefined || !themeIds.includes(theme)) { + return default_theme; + } else { + return theme; + } + } + + function set_theme(theme, store = true) { + let ace_theme; + + if (theme == 'coal' || theme == 'navy') { + stylesheets.ayuHighlight.disabled = true; + stylesheets.tomorrowNight.disabled = false; + stylesheets.highlight.disabled = true; + + ace_theme = "ace/theme/tomorrow_night"; + } else if (theme == 'ayu') { + stylesheets.ayuHighlight.disabled = false; + stylesheets.tomorrowNight.disabled = true; + stylesheets.highlight.disabled = true; + ace_theme = "ace/theme/tomorrow_night"; + } else { + stylesheets.ayuHighlight.disabled = true; + stylesheets.tomorrowNight.disabled = true; + stylesheets.highlight.disabled = false; + ace_theme = "ace/theme/dawn"; + } + + setTimeout(function () { + themeColorMetaTag.content = getComputedStyle(document.documentElement).backgroundColor; + }, 1); + + if (window.ace && window.editors) { + window.editors.forEach(function (editor) { + editor.setTheme(ace_theme); + }); + } + + var previousTheme = get_theme(); + + if (store) { + try { localStorage.setItem('mdbook-theme', theme); } catch (e) { } + } + + html.classList.remove(previousTheme); + html.classList.add(theme); + updateThemeSelected(); + } + + // Set theme + var theme = get_theme(); + + set_theme(theme, false); + + themeToggleButton.addEventListener('click', function () { + if (themePopup.style.display === 'block') { + hideThemes(); + } else { + showThemes(); + } + }); + + themePopup.addEventListener('click', function (e) { + var theme; + if (e.target.className === "theme") { + theme = e.target.id; + } else if (e.target.parentElement.className === "theme") { + theme = e.target.parentElement.id; + } else { + return; + } + set_theme(theme); + }); + + themePopup.addEventListener('focusout', function(e) { + // e.relatedTarget is null in Safari and Firefox on macOS (see workaround below) + if (!!e.relatedTarget && !themeToggleButton.contains(e.relatedTarget) && !themePopup.contains(e.relatedTarget)) { + hideThemes(); + } + }); + + // Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang/mdBook/issues/628 + document.addEventListener('click', function(e) { + if (themePopup.style.display === 'block' && !themeToggleButton.contains(e.target) && !themePopup.contains(e.target)) { + hideThemes(); + } + }); + + document.addEventListener('keydown', function (e) { + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; } + if (!themePopup.contains(e.target)) { return; } + + switch (e.key) { + case 'Escape': + e.preventDefault(); + hideThemes(); + break; + case 'ArrowUp': + e.preventDefault(); + var li = document.activeElement.parentElement; + if (li && li.previousElementSibling) { + li.previousElementSibling.querySelector('button').focus(); + } + break; + case 'ArrowDown': + e.preventDefault(); + var li = document.activeElement.parentElement; + if (li && li.nextElementSibling) { + li.nextElementSibling.querySelector('button').focus(); + } + break; + case 'Home': + e.preventDefault(); + themePopup.querySelector('li:first-child button').focus(); + break; + case 'End': + e.preventDefault(); + themePopup.querySelector('li:last-child button').focus(); + break; + } + }); +})(); + +(function sidebar() { + var body = document.querySelector("body"); + var sidebar = document.getElementById("sidebar"); + var sidebarLinks = document.querySelectorAll('#sidebar a'); + var sidebarToggleButton = document.getElementById("sidebar-toggle"); + var sidebarResizeHandle = document.getElementById("sidebar-resize-handle"); + var firstContact = null; + + function showSidebar() { + body.classList.remove('sidebar-hidden') + body.classList.add('sidebar-visible'); + Array.from(sidebarLinks).forEach(function (link) { + link.setAttribute('tabIndex', 0); + }); + sidebarToggleButton.setAttribute('aria-expanded', true); + sidebar.setAttribute('aria-hidden', false); + try { localStorage.setItem('mdbook-sidebar', 'visible'); } catch (e) { } + } + + function hideSidebar() { + body.classList.remove('sidebar-visible') + body.classList.add('sidebar-hidden'); + Array.from(sidebarLinks).forEach(function (link) { + link.setAttribute('tabIndex', -1); + }); + sidebarToggleButton.setAttribute('aria-expanded', false); + sidebar.setAttribute('aria-hidden', true); + try { localStorage.setItem('mdbook-sidebar', 'hidden'); } catch (e) { } + } + + // Toggle sidebar + sidebarToggleButton.addEventListener('click', function sidebarToggle() { + if (body.classList.contains("sidebar-hidden")) { + var current_width = parseInt( + document.documentElement.style.getPropertyValue('--sidebar-width'), 10); + if (current_width < 150) { + document.documentElement.style.setProperty('--sidebar-width', '150px'); + } + showSidebar(); + } else if (body.classList.contains("sidebar-visible")) { + hideSidebar(); + } else { + if (getComputedStyle(sidebar)['transform'] === 'none') { + hideSidebar(); + } else { + showSidebar(); + } + } + }); + + sidebarResizeHandle.addEventListener('mousedown', initResize, false); + + function initResize(e) { + window.addEventListener('mousemove', resize, false); + window.addEventListener('mouseup', stopResize, false); + body.classList.add('sidebar-resizing'); + } + function resize(e) { + var pos = (e.clientX - sidebar.offsetLeft); + if (pos < 20) { + hideSidebar(); + } else { + if (body.classList.contains("sidebar-hidden")) { + showSidebar(); + } + pos = Math.min(pos, window.innerWidth - 100); + document.documentElement.style.setProperty('--sidebar-width', pos + 'px'); + } + } + //on mouseup remove windows functions mousemove & mouseup + function stopResize(e) { + body.classList.remove('sidebar-resizing'); + window.removeEventListener('mousemove', resize, false); + window.removeEventListener('mouseup', stopResize, false); + } + + document.addEventListener('touchstart', function (e) { + firstContact = { + x: e.touches[0].clientX, + time: Date.now() + }; + }, { passive: true }); + + document.addEventListener('touchmove', function (e) { + if (!firstContact) + return; + + var curX = e.touches[0].clientX; + var xDiff = curX - firstContact.x, + tDiff = Date.now() - firstContact.time; + + if (tDiff < 250 && Math.abs(xDiff) >= 150) { + if (xDiff >= 0 && firstContact.x < Math.min(document.body.clientWidth * 0.25, 300)) + showSidebar(); + else if (xDiff < 0 && curX < 300) + hideSidebar(); + + firstContact = null; + } + }, { passive: true }); +})(); + +(function chapterNavigation() { + document.addEventListener('keydown', function (e) { + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; } + if (window.search && window.search.hasFocus()) { return; } + var html = document.querySelector('html'); + + function next() { + var nextButton = document.querySelector('.nav-chapters.next'); + if (nextButton) { + window.location.href = nextButton.href; + } + } + function prev() { + var previousButton = document.querySelector('.nav-chapters.previous'); + if (previousButton) { + window.location.href = previousButton.href; + } + } + switch (e.key) { + case 'ArrowRight': + e.preventDefault(); + if (html.dir == 'rtl') { + prev(); + } else { + next(); + } + break; + case 'ArrowLeft': + e.preventDefault(); + if (html.dir == 'rtl') { + next(); + } else { + prev(); + } + break; + } + }); +})(); + +(function clipboard() { + var clipButtons = document.querySelectorAll('.clip-button'); + + function hideTooltip(elem) { + elem.firstChild.innerText = ""; + elem.className = 'clip-button'; + } + + function showTooltip(elem, msg) { + elem.firstChild.innerText = msg; + elem.className = 'clip-button tooltipped'; + } + + var clipboardSnippets = new ClipboardJS('.clip-button', { + text: function (trigger) { + hideTooltip(trigger); + let playground = trigger.closest("pre"); + return playground_text(playground, false); + } + }); + + Array.from(clipButtons).forEach(function (clipButton) { + clipButton.addEventListener('mouseout', function (e) { + hideTooltip(e.currentTarget); + }); + }); + + clipboardSnippets.on('success', function (e) { + e.clearSelection(); + showTooltip(e.trigger, "Copied!"); + }); + + clipboardSnippets.on('error', function (e) { + showTooltip(e.trigger, "Clipboard error!"); + }); +})(); + +(function scrollToTop () { + var menuTitle = document.querySelector('.menu-title'); + + menuTitle.addEventListener('click', function () { + document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' }); + }); +})(); + +(function controllMenu() { + var menu = document.getElementById('menu-bar'); + + (function controllPosition() { + var scrollTop = document.scrollingElement.scrollTop; + var prevScrollTop = scrollTop; + var minMenuY = -menu.clientHeight - 50; + // When the script loads, the page can be at any scroll (e.g. if you reforesh it). + menu.style.top = scrollTop + 'px'; + // Same as parseInt(menu.style.top.slice(0, -2), but faster + var topCache = menu.style.top.slice(0, -2); + menu.classList.remove('sticky'); + var stickyCache = false; // Same as menu.classList.contains('sticky'), but faster + document.addEventListener('scroll', function () { + scrollTop = Math.max(document.scrollingElement.scrollTop, 0); + // `null` means that it doesn't need to be updated + var nextSticky = null; + var nextTop = null; + var scrollDown = scrollTop > prevScrollTop; + var menuPosAbsoluteY = topCache - scrollTop; + if (scrollDown) { + nextSticky = false; + if (menuPosAbsoluteY > 0) { + nextTop = prevScrollTop; + } + } else { + if (menuPosAbsoluteY > 0) { + nextSticky = true; + } else if (menuPosAbsoluteY < minMenuY) { + nextTop = prevScrollTop + minMenuY; + } + } + if (nextSticky === true && stickyCache === false) { + menu.classList.add('sticky'); + stickyCache = true; + } else if (nextSticky === false && stickyCache === true) { + menu.classList.remove('sticky'); + stickyCache = false; + } + if (nextTop !== null) { + menu.style.top = nextTop + 'px'; + topCache = nextTop; + } + prevScrollTop = scrollTop; + }, { passive: true }); + })(); + (function controllBorder() { + function updateBorder() { + if (menu.offsetTop === 0) { + menu.classList.remove('bordered'); + } else { + menu.classList.add('bordered'); + } + } + updateBorder(); + document.addEventListener('scroll', updateBorder, { passive: true }); + })(); +})(); diff --git a/docs/build/maven_release.html b/docs/build/maven_release.html new file mode 100644 index 0000000000..f16709fb3a --- /dev/null +++ b/docs/build/maven_release.html @@ -0,0 +1,267 @@ + + + + + + Maven Release - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Maven Release

+

Register for a Sonatype Account

+

First, you'll need to register for a Sonatype account at the Central +Portal.

+

To publish under the com.google.android namespace, you'll also need to email +central-support@sonatype.com with your account information to request access.

+

Then, generate a user token through the Sonatype website. Navigate to +https://central.sonatype.com/account and select Generate +User Token.

+

Finally, add the generated token credentials to ~/.gradle/gradle.properties. (Note: these are +different from the credentials used to log into your Sonatype account):

+
# Generated Sonatype token
+sonatypeUsername=<username>
+sonatypePassword=<password>
+
+
+

Signing Key

+

All release artifacts must be signed. You'll need to create an OpenPGP key pair. See Gradle's +documentation on Signatory +credentials +for instructions.

+

Update ~/.gradle/gradle.properties with your new key's credentials:

+
signing.keyId=<key id>
+signing.password=<key password>
+signing.secretKeyRingFile=<secret key ring file>
+
+
+

Build the Android Release

+

Make sure JAVA_HOME is set correctly. For example:

+
export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home
+./build.sh -C -i -p android release
+
+
+

Publish to Sonatype

+

A Note on the Legacy Staging Service

+

Previously, Filament was published to Maven via OSSRH (Open Source Software Repository Hosting). In +2025, this service was sunsetted. Now, we use +Sonatype's Central Publisher Portal.

+

The new Central Publisher Portal does not officially support Gradle. However, Sonatype provides a +staging API compatibility service, which works with Filament's Gradle setup.

+
+

1. Upload to the Staging API Compatibility Service

+
cd android
+./gradlew publishToSonatype
+
+

2. Move the Repository to the Central Publisher Portal

+

We have a script to automate this. It reads the sonatypeUsername and sonatypePassword from your +~/.gradle/gradle.properties file.

+
python3 build/common/close-sonatype-staging-repository.py
+
+

3. Publish the Release on Sonatype

+

Navigate to Maven Central Repository Deployments.

+

Here, you should see a new deployment with a Validated status and all your artifacts listed. Click +the Publish button to publish the artifacts. It typically takes around 5 minutes after clicking +Publish for the artifacts to go live.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/build/windows_android.html b/docs/build/windows_android.html new file mode 100644 index 0000000000..2c2ff3af74 --- /dev/null +++ b/docs/build/windows_android.html @@ -0,0 +1,318 @@ + + + + + + Build for Android on Windows - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Building Filament for Android on Windows

+

Prerequisites

+

In addition to the requirements for building Filament on Windows, you'll +need the Android SDK and NDK. See Getting Started with the +NDK for detailed installation instructions.

+

You'll also need Ninja 1.8 (or +more recent) and Git for Windows to clone the repository and run +Bash scripts.

+

Ensure the %ANDROID_HOME% environment variable is set to your Android SDK installation location.

+

On Windows, we require VS2019 for building the host tools. All of the following commands should be +executed in a Visual Studio x64 Native Tools Command Prompt for VS 2019.

+

A Note About Python 3

+

Python 3 is required. If CMake errors because it cannot find Python 3:

+
Could NOT find PythonInterp: Found unsuitable version "1.4", but required is at least "3"
+
+

then add the following flag to the CMake invocations:

+
-DPYTHON_EXECUTABLE:FILEPATH=\path\to\python3
+
+

Desktop Tools

+

First, a few Filament tools need to be compiled for desktop.

+
    +
  1. From Filament's root directory, create a desktop build directory and run CMake.
  2. +
+
mkdir out\cmake-release
+cd out\cmake-release
+cmake ^
+    -G Ninja ^
+    -DCMAKE_INSTALL_PREFIX=..\release\filament ^
+    -DFILAMENT_ENABLE_JAVA=NO ^
+    -DCMAKE_BUILD_TYPE=Release ^
+    ..\..
+
+
    +
  1. Build the required desktop host tools.
  2. +
+
ninja matc resgen cmgen
+
+

The build should succeed and a ImportExecutables-Release.cmake file should automatically be +created at Filament's root directory.

+

If you are going to build Filament samples you should install desktop host tools:

+
ninja install
+
+

Build

+
    +
  1. Create the build directories.
  2. +
+
mkdir out\cmake-android-release-aarch64
+mkdir out\cmake-android-release-arm7
+mkdir out\cmake-android-release-x86_64
+mkdir out\cmake-android-release-x86
+
+
    +
  1. Run CMake for each architecture.
  2. +
+
cd out\cmake-android-release-aarch64
+cmake ^
+    -G Ninja ^
+    -DCMAKE_BUILD_TYPE=Release ^
+    -DCMAKE_INSTALL_PREFIX=..\android-release\filament ^
+    -DCMAKE_TOOLCHAIN_FILE=..\..\build\toolchain-aarch64-linux-android.cmake ^
+    ..\..
+
+cd out\cmake-android-release-arm7
+cmake ^
+    -G Ninja ^
+    -DCMAKE_BUILD_TYPE=Release ^
+    -DCMAKE_INSTALL_PREFIX=..\android-release\filament ^
+    -DCMAKE_TOOLCHAIN_FILE=..\..\build\toolchain-arm7-linux-android.cmake ^
+    ..\..
+
+cd out\cmake-android-release-x86_64
+cmake ^
+    -G Ninja ^
+    -DCMAKE_BUILD_TYPE=Release ^
+    -DCMAKE_INSTALL_PREFIX=..\android-release\filament ^
+    -DCMAKE_TOOLCHAIN_FILE=..\..\build\toolchain-x86_64-linux-android.cmake ^
+    ..\..
+
+cd out\cmake-android-release-x86
+cmake ^
+    -G Ninja ^
+    -DCMAKE_BUILD_TYPE=Release ^
+    -DCMAKE_INSTALL_PREFIX=..\android-release\filament ^
+    -DCMAKE_TOOLCHAIN_FILE=..\..\build\toolchain-x86-linux-android.cmake ^
+    ..\..
+
+
    +
  1. Build.
  2. +
+

Inside of each build directory, run:

+
ninja install
+
+

Generate AAR

+

The Gradle project used to generate the AAR is located at <filament>\android.

+
cd android
+gradlew -Pcom.google.android.filament.dist-dir=..\out\android-release\filament assembleRelease
+copy filament-android\build\outputs\aar\filament-android-release.aar ..\..\out\
+
+

If you're only interested in building for a single ABI, you'll need to pass a com.google.android.filament.abis parameter:

+
gradlew -Pcom.google.android.filament.dist-dir=..\out\android-release\filament assembleRelease -Pcom.google.android.filament.abis=x86
+
+

If you're only interested in building SDK, you may skip samples build by passing a com.google.android.filament.skip-samples flag:

+
gradlew -Pcom.google.android.filament.dist-dir=..\out\android-release\filament assembleRelease -Pcom.google.android.filament.skip-samples
+
+

filament-android-release.aar should now be present at <filament>\out\filament-android-release.aar.

+

See Using Filament's AAR for usage instructions.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/clipboard.min.js b/docs/clipboard.min.js new file mode 100644 index 0000000000..02c549e35c --- /dev/null +++ b/docs/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.4 + * https://zenorocha.github.io/clipboard.js + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return function(n){var o={};function r(t){if(o[t])return o[t].exports;var e=o[t]={i:t,l:!1,exports:{}};return n[t].call(e.exports,e,e.exports,r),e.l=!0,e.exports}return r.m=n,r.c=o,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=0)}([function(t,e,n){"use strict";var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=function(){function o(t,e){for(var n=0;n .hljs { + color: var(--links); +} + +/* + body-container is necessary because mobile browsers don't seem to like + overflow-x on the body tag when there is a tag. +*/ +#body-container { + /* + This is used when the sidebar pushes the body content off the side of + the screen on small screens. Without it, dragging on mobile Safari + will want to reposition the viewport in a weird way. + */ + overflow-x: clip; +} + +/* Menu Bar */ + +#menu-bar, +#menu-bar-hover-placeholder { + z-index: 101; + margin: auto calc(0px - var(--page-padding)); +} +#menu-bar { + position: relative; + display: flex; + flex-wrap: wrap; + background-color: var(--bg); + border-block-end-color: var(--bg); + border-block-end-width: 1px; + border-block-end-style: solid; +} +#menu-bar.sticky, +#menu-bar-hover-placeholder:hover + #menu-bar, +#menu-bar:hover, +html.sidebar-visible #menu-bar { + position: -webkit-sticky; + position: sticky; + top: 0 !important; +} +#menu-bar-hover-placeholder { + position: sticky; + position: -webkit-sticky; + top: 0; + height: var(--menu-bar-height); +} +#menu-bar.bordered { + border-block-end-color: var(--table-border-color); +} +#menu-bar i, #menu-bar .icon-button { + position: relative; + padding: 0 8px; + z-index: 10; + line-height: var(--menu-bar-height); + cursor: pointer; + transition: color 0.5s; +} +@media only screen and (max-width: 420px) { + #menu-bar i, #menu-bar .icon-button { + padding: 0 5px; + } +} + +.icon-button { + border: none; + background: none; + padding: 0; + color: inherit; +} +.icon-button i { + margin: 0; +} + +.right-buttons { + margin: 0 15px; +} +.right-buttons a { + text-decoration: none; +} + +.left-buttons { + display: flex; + margin: 0 5px; +} +html:not(.js) .left-buttons button { + display: none; +} + +.menu-title { + display: inline-block; + font-weight: 200; + font-size: 2.4rem; + line-height: var(--menu-bar-height); + text-align: center; + margin: 0; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.menu-title { + cursor: pointer; +} + +.menu-bar, +.menu-bar:visited, +.nav-chapters, +.nav-chapters:visited, +.mobile-nav-chapters, +.mobile-nav-chapters:visited, +.menu-bar .icon-button, +.menu-bar a i { + color: var(--icons); +} + +.menu-bar i:hover, +.menu-bar .icon-button:hover, +.nav-chapters:hover, +.mobile-nav-chapters i:hover { + color: var(--icons-hover); +} + +/* Nav Icons */ + +.nav-chapters { + font-size: 2.5em; + text-align: center; + text-decoration: none; + + position: fixed; + top: 0; + bottom: 0; + margin: 0; + max-width: 150px; + min-width: 90px; + + display: flex; + justify-content: center; + align-content: center; + flex-direction: column; + + transition: color 0.5s, background-color 0.5s; +} + +.nav-chapters:hover { + text-decoration: none; + background-color: var(--theme-hover); + transition: background-color 0.15s, color 0.15s; +} + +.nav-wrapper { + margin-block-start: 50px; + display: none; +} + +.mobile-nav-chapters { + font-size: 2.5em; + text-align: center; + text-decoration: none; + width: 90px; + border-radius: 5px; + background-color: var(--sidebar-bg); +} + +/* Only Firefox supports flow-relative values */ +.previous { float: left; } +[dir=rtl] .previous { float: right; } + +/* Only Firefox supports flow-relative values */ +.next { + float: right; + right: var(--page-padding); +} +[dir=rtl] .next { + float: left; + right: unset; + left: var(--page-padding); +} + +/* Use the correct buttons for RTL layouts*/ +[dir=rtl] .previous i.fa-angle-left:before {content:"\f105";} +[dir=rtl] .next i.fa-angle-right:before { content:"\f104"; } + +@media only screen and (max-width: 1080px) { + .nav-wide-wrapper { display: none; } + .nav-wrapper { display: block; } +} + +/* sidebar-visible */ +@media only screen and (max-width: 1380px) { + #sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wide-wrapper { display: none; } + #sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wrapper { display: block; } +} + +/* Inline code */ + +:not(pre) > .hljs { + display: inline; + padding: 0.1em 0.3em; + border-radius: 3px; +} + +:not(pre):not(a) > .hljs { + color: var(--inline-code-color); + overflow-x: initial; +} + +a:hover > .hljs { + text-decoration: underline; +} + +pre { + position: relative; +} +pre > .buttons { + position: absolute; + z-index: 100; + right: 0px; + top: 2px; + margin: 0px; + padding: 2px 0px; + + color: var(--sidebar-fg); + cursor: pointer; + visibility: hidden; + opacity: 0; + transition: visibility 0.1s linear, opacity 0.1s linear; +} +pre:hover > .buttons { + visibility: visible; + opacity: 1 +} +pre > .buttons :hover { + color: var(--sidebar-active); + border-color: var(--icons-hover); + background-color: var(--theme-hover); +} +pre > .buttons i { + margin-inline-start: 8px; +} +pre > .buttons button { + cursor: inherit; + margin: 0px 5px; + padding: 4px 4px 3px 5px; + font-size: 23px; + + border-style: solid; + border-width: 1px; + border-radius: 4px; + border-color: var(--icons); + background-color: var(--theme-popup-bg); + transition: 100ms; + transition-property: color,border-color,background-color; + color: var(--icons); +} + +pre > .buttons button.clip-button { + padding: 2px 4px 0px 6px; +} +pre > .buttons button.clip-button::before { + /* clipboard image from octicons (https://github.com/primer/octicons/tree/v2.0.0) MIT license + */ + content: url('data:image/svg+xml,\ +\ +\ +'); + filter: var(--copy-button-filter); +} +pre > .buttons button.clip-button:hover::before { + filter: var(--copy-button-filter-hover); +} + +@media (pointer: coarse) { + pre > .buttons button { + /* On mobile, make it easier to tap buttons. */ + padding: 0.3rem 1rem; + } + + .sidebar-resize-indicator { + /* Hide resize indicator on devices with limited accuracy */ + display: none; + } +} +pre > code { + display: block; + padding: 1rem; +} + +/* FIXME: ACE editors overlap their buttons because ACE does absolute + positioning within the code block which breaks padding. The only solution I + can think of is to move the padding to the outer pre tag (or insert a div + wrapper), but that would require fixing a whole bunch of CSS rules. +*/ +.hljs.ace_editor { + padding: 0rem 0rem; +} + +pre > .result { + margin-block-start: 10px; +} + +/* Search */ + +#searchresults a { + text-decoration: none; +} + +mark { + border-radius: 2px; + padding-block-start: 0; + padding-block-end: 1px; + padding-inline-start: 3px; + padding-inline-end: 3px; + margin-block-start: 0; + margin-block-end: -1px; + margin-inline-start: -3px; + margin-inline-end: -3px; + background-color: var(--search-mark-bg); + transition: background-color 300ms linear; + cursor: pointer; +} + +mark.fade-out { + background-color: rgba(0,0,0,0) !important; + cursor: auto; +} + +.searchbar-outer { + margin-inline-start: auto; + margin-inline-end: auto; + max-width: var(--content-max-width); +} + +#searchbar { + width: 100%; + margin-block-start: 5px; + margin-block-end: 0; + margin-inline-start: auto; + margin-inline-end: auto; + padding: 10px 16px; + transition: box-shadow 300ms ease-in-out; + border: 1px solid var(--searchbar-border-color); + border-radius: 3px; + background-color: var(--searchbar-bg); + color: var(--searchbar-fg); +} +#searchbar:focus, +#searchbar.active { + box-shadow: 0 0 3px var(--searchbar-shadow-color); +} + +.searchresults-header { + font-weight: bold; + font-size: 1em; + padding-block-start: 18px; + padding-block-end: 0; + padding-inline-start: 5px; + padding-inline-end: 0; + color: var(--searchresults-header-fg); +} + +.searchresults-outer { + margin-inline-start: auto; + margin-inline-end: auto; + max-width: var(--content-max-width); + border-block-end: 1px dashed var(--searchresults-border-color); +} + +ul#searchresults { + list-style: none; + padding-inline-start: 20px; +} +ul#searchresults li { + margin: 10px 0px; + padding: 2px; + border-radius: 2px; +} +ul#searchresults li.focus { + background-color: var(--searchresults-li-bg); +} +ul#searchresults span.teaser { + display: block; + clear: both; + margin-block-start: 5px; + margin-block-end: 0; + margin-inline-start: 20px; + margin-inline-end: 0; + font-size: 0.8em; +} +ul#searchresults span.teaser em { + font-weight: bold; + font-style: normal; +} + +/* Sidebar */ + +.sidebar { + position: fixed; + left: 0; + top: 0; + bottom: 0; + width: var(--sidebar-width); + font-size: 0.875em; + box-sizing: border-box; + -webkit-overflow-scrolling: touch; + overscroll-behavior-y: contain; + background-color: var(--sidebar-bg); + color: var(--sidebar-fg); +} +.sidebar-iframe-inner { + background-color: var(--sidebar-bg); + color: var(--sidebar-fg); + padding: 10px 10px; + margin: 0; + font-size: 1.4rem; +} +.sidebar-iframe-outer { + border: none; + height: 100%; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; +} +[dir=rtl] .sidebar { left: unset; right: 0; } +.sidebar-resizing { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} +html:not(.sidebar-resizing) .sidebar { + transition: transform 0.3s; /* Animation: slide away */ +} +.sidebar code { + line-height: 2em; +} +.sidebar .flogo { + max-height: 90px; + padding: 5px; +} +.sidebar .sidebar-scrollbox { + overflow-y: auto; + position: absolute; + top: 100px; + bottom: 0; + left: 0; + right: 0; + padding: 10px 10px; +} +.sidebar .sidebar-resize-handle { + position: absolute; + cursor: col-resize; + width: 0; + right: calc(var(--sidebar-resize-indicator-width) * -1); + top: 0; + bottom: 0; + display: flex; + align-items: center; +} + +.sidebar-resize-handle .sidebar-resize-indicator { + width: 100%; + height: 12px; + background-color: var(--icons); + margin-inline-start: var(--sidebar-resize-indicator-space); +} + +[dir=rtl] .sidebar .sidebar-resize-handle { + left: calc(var(--sidebar-resize-indicator-width) * -1); + right: unset; +} +.js .sidebar .sidebar-resize-handle { + cursor: col-resize; + width: calc(var(--sidebar-resize-indicator-width) - var(--sidebar-resize-indicator-space)); +} +/* sidebar-hidden */ +#sidebar-toggle-anchor:not(:checked) ~ .sidebar { + transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width))); + z-index: -1; +} +[dir=rtl] #sidebar-toggle-anchor:not(:checked) ~ .sidebar { + transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width))); +} +.sidebar::-webkit-scrollbar { + background: var(--sidebar-bg); +} +.sidebar::-webkit-scrollbar-thumb { + background: var(--scrollbar); +} + +/* sidebar-visible */ +#sidebar-toggle-anchor:checked ~ .page-wrapper { + transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width))); +} +[dir=rtl] #sidebar-toggle-anchor:checked ~ .page-wrapper { + transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width))); +} +@media only screen and (min-width: 620px) { + #sidebar-toggle-anchor:checked ~ .page-wrapper { + transform: none; + margin-inline-start: calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)); + } + [dir=rtl] #sidebar-toggle-anchor:checked ~ .page-wrapper { + transform: none; + } +} + +.chapter { + list-style: none outside none; + padding-inline-start: 0; + line-height: 2.2em; +} + +.chapter ol { + width: 100%; +} + +.chapter li { + display: flex; + color: var(--sidebar-non-existant); +} +.chapter li a { + display: block; + padding: 0; + text-decoration: none; + color: var(--sidebar-fg); +} + +.chapter li a:hover { + color: var(--sidebar-active); +} + +.chapter li a.active { + color: var(--sidebar-active); +} + +.chapter li > a.toggle { + cursor: pointer; + display: block; + margin-inline-start: auto; + padding: 0 10px; + user-select: none; + opacity: 0.68; +} + +.chapter li > a.toggle div { + transition: transform 0.5s; +} + +/* collapse the section */ +.chapter li:not(.expanded) + li > ol { + display: none; +} + +.chapter li.chapter-item { + line-height: 1.5em; + margin-block-start: 0.6em; +} + +.chapter li.expanded > a.toggle div { + transform: rotate(90deg); +} + +.spacer { + width: 100%; + height: 3px; + margin: 5px 0px; +} +.chapter .spacer { + background-color: var(--sidebar-spacer); +} + +@media (-moz-touch-enabled: 1), (pointer: coarse) { + .chapter li a { padding: 5px 0; } + .spacer { margin: 10px 0; } +} + +.section { + list-style: none outside none; + padding-inline-start: 20px; + line-height: 1.9em; +} + +/* Theme Menu Popup */ + +.theme-popup { + position: absolute; + left: 10px; + top: var(--menu-bar-height); + z-index: 1000; + border-radius: 4px; + font-size: 0.7em; + color: var(--fg); + background: var(--theme-popup-bg); + border: 1px solid var(--theme-popup-border); + margin: 0; + padding: 0; + list-style: none; + display: none; + /* Don't let the children's background extend past the rounded corners. */ + overflow: hidden; +} +[dir=rtl] .theme-popup { left: unset; right: 10px; } +.theme-popup .default { + color: var(--icons); +} +.theme-popup .theme { + width: 100%; + border: 0; + margin: 0; + padding: 2px 20px; + line-height: 25px; + white-space: nowrap; + text-align: start; + cursor: pointer; + color: inherit; + background: inherit; + font-size: inherit; +} +.theme-popup .theme:hover { + background-color: var(--theme-hover); +} + +.theme-selected::before { + display: inline-block; + content: "✓"; + margin-inline-start: -14px; + width: 14px; +} diff --git a/docs/css/general.css b/docs/css/general.css new file mode 100644 index 0000000000..9946cfc01a --- /dev/null +++ b/docs/css/general.css @@ -0,0 +1,279 @@ +/* Base styles and content styles */ + +:root { + /* Browser default font-size is 16px, this way 1 rem = 10px */ + font-size: 62.5%; + color-scheme: var(--color-scheme); +} + +html { + font-family: "Open Sans", sans-serif; + color: var(--fg); + background-color: var(--bg); + text-size-adjust: none; + -webkit-text-size-adjust: none; +} + +body { + margin: 0; + font-size: 1.6rem; + overflow-x: hidden; +} + +code { + font-family: var(--mono-font) !important; + font-size: var(--code-font-size); + direction: ltr !important; +} + +/* make long words/inline code not x overflow */ +main { + overflow-wrap: break-word; +} + +/* make wide tables scroll if they overflow */ +.table-wrapper { + overflow-x: auto; +} + +/* Don't change font size in headers. */ +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + font-size: unset; +} + +.left { float: left; } +.right { float: right; } +.boring { opacity: 0.6; } +.hide-boring .boring { display: none; } +.hidden { display: none !important; } + +h2, h3 { margin-block-start: 2.5em; } +h4, h5 { margin-block-start: 2em; } + +.header + .header h3, +.header + .header h4, +.header + .header h5 { + margin-block-start: 1em; +} + +h1:target::before, +h2:target::before, +h3:target::before, +h4:target::before, +h5:target::before, +h6:target::before { + display: inline-block; + content: "»"; + margin-inline-start: -30px; + width: 30px; +} + +/* This is broken on Safari as of version 14, but is fixed + in Safari Technology Preview 117 which I think will be Safari 14.2. + https://bugs.webkit.org/show_bug.cgi?id=218076 +*/ +:target { + /* Safari does not support logical properties */ + scroll-margin-top: calc(var(--menu-bar-height) + 0.5em); +} + +.page { + outline: 0; + padding: 0 var(--page-padding); + margin-block-start: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */ +} +.page-wrapper { + box-sizing: border-box; + background-color: var(--bg); +} +.no-js .page-wrapper, +.js:not(.sidebar-resizing) .page-wrapper { + transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */ +} +[dir=rtl] .js:not(.sidebar-resizing) .page-wrapper { + transition: margin-right 0.3s ease, transform 0.3s ease; /* Animation: slide away */ +} + +.content { + overflow-y: auto; + padding: 0 5px 50px 5px; +} +.content main { + margin-inline-start: auto; + margin-inline-end: auto; + max-width: var(--content-max-width); +} +.content p { line-height: 1.45em; } +.content ol { line-height: 1.45em; } +.content ul { line-height: 1.45em; } +.content a { text-decoration: none; } +.content a:hover { text-decoration: underline; } +.content img, .content video { max-width: 100%; } +.content .header:link, +.content .header:visited { + color: var(--fg); +} +.content .header:link, +.content .header:visited:hover { + text-decoration: none; +} + +table { + margin: 0 auto; + border-collapse: collapse; +} +table td { + padding: 3px 20px; + border: 1px var(--table-border-color) solid; +} +table thead { + background: var(--table-header-bg); +} +table thead td { + font-weight: 700; + border: none; +} +table thead th { + padding: 3px 20px; +} +table thead tr { + border: 1px var(--table-header-bg) solid; +} +/* Alternate background colors for rows */ +table tbody tr:nth-child(2n) { + background: var(--table-alternate-bg); +} + + +blockquote { + margin: 20px 0; + padding: 0 20px; + color: var(--fg); + background-color: var(--quote-bg); + border-block-start: .1em solid var(--quote-border); + border-block-end: .1em solid var(--quote-border); +} + +.warning { + margin: 20px; + padding: 0 20px; + border-inline-start: 2px solid var(--warning-border); +} + +.warning:before { + position: absolute; + width: 3rem; + height: 3rem; + margin-inline-start: calc(-1.5rem - 21px); + content: "ⓘ"; + text-align: center; + background-color: var(--bg); + color: var(--warning-border); + font-weight: bold; + font-size: 2rem; +} + +blockquote .warning:before { + background-color: var(--quote-bg); +} + +kbd { + background-color: var(--table-border-color); + border-radius: 4px; + border: solid 1px var(--theme-popup-border); + box-shadow: inset 0 -1px 0 var(--theme-hover); + display: inline-block; + font-size: var(--code-font-size); + font-family: var(--mono-font); + line-height: 10px; + padding: 4px 5px; + vertical-align: middle; +} + +sup { + /* Set the line-height for superscript and footnote references so that there + isn't an awkward space appearing above lines that contain the footnote. + + See https://github.com/rust-lang/mdBook/pull/2443#discussion_r1813773583 + for an explanation. + */ + line-height: 0; +} + +.footnote-definition { + font-size: 0.9em; +} +/* The default spacing for a list is a little too large. */ +.footnote-definition ul, +.footnote-definition ol { + padding-left: 20px; +} +.footnote-definition > li { + /* Required to position the ::before target */ + position: relative; +} +.footnote-definition > li:target { + scroll-margin-top: 50vh; +} +.footnote-reference:target { + scroll-margin-top: 50vh; +} +/* Draws a border around the footnote (including the marker) when it is selected. + TODO: If there are multiple linkbacks, highlight which one you just came + from so you know which one to click. +*/ +.footnote-definition > li:target::before { + border: 2px solid var(--footnote-highlight); + border-radius: 6px; + position: absolute; + top: -8px; + right: -8px; + bottom: -8px; + left: -32px; + pointer-events: none; + content: ""; +} +/* Pulses the footnote reference so you can quickly see where you left off reading. + This could use some improvement. +*/ +@media not (prefers-reduced-motion) { + .footnote-reference:target { + animation: fn-highlight 0.8s; + border-radius: 2px; + } + + @keyframes fn-highlight { + from { + background-color: var(--footnote-highlight); + } + } +} + +.tooltiptext { + position: absolute; + visibility: hidden; + color: #fff; + background-color: #333; + transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */ + left: -8px; /* Half of the width of the icon */ + top: -35px; + font-size: 0.8em; + text-align: center; + border-radius: 6px; + padding: 5px 8px; + margin: 5px; + z-index: 1000; +} +.tooltipped .tooltiptext { + visibility: visible; +} + +.chapter li.part-title { + color: var(--sidebar-fg); + margin: 5px 0px; + font-weight: bold; +} + +.result-no-output { + font-style: italic; +} diff --git a/docs/css/variables.css b/docs/css/variables.css new file mode 100644 index 0000000000..b19fcb4759 --- /dev/null +++ b/docs/css/variables.css @@ -0,0 +1,330 @@ + +/* Globals */ + +:root { + --sidebar-target-width: 300px; + --sidebar-width: min(var(--sidebar-target-width), 80vw); + --sidebar-resize-indicator-width: 8px; + --sidebar-resize-indicator-space: 2px; + --page-padding: 15px; + --content-max-width: 750px; + --menu-bar-height: 50px; + --mono-font: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace; + --code-font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */ +} + +/* Themes */ + +.ayu { + --bg: #f3f1f1; + --fg: #555; + + --sidebar-bg: #f3f1f1; + --sidebar-fg: #555; + --sidebar-non-existant: #5c6773; + --sidebar-active: #ffb454; + --sidebar-spacer: #2d334f; + + --scrollbar: var(--sidebar-fg); + + --icons: #737480; + --icons-hover: #b7b9cc; + + --links: #0096cf; + + --inline-code-color: #444; + + --theme-popup-bg: #14191f; + --theme-popup-border: #5c6773; + --theme-hover: #191f26; + + --quote-bg: hsl(226, 15%, 17%); + --quote-border: hsl(226, 15%, 22%); + + --warning-border: #ff8e00; + + --table-border-color: hsl(210, 25%, 13%); + --table-header-bg: #7fbfff; + --table-alternate-bg: #a5b6c7; + + --searchbar-border-color: #848484; + --searchbar-bg: #424242; + --searchbar-fg: #fff; + --searchbar-shadow-color: #d4c89f; + --searchresults-header-fg: #666; + --searchresults-border-color: #888; + --searchresults-li-bg: #252932; + --search-mark-bg: #e3b171; + + --color-scheme: dark; + + /* Same as `--icons` */ + --copy-button-filter: invert(45%) sepia(6%) saturate(621%) hue-rotate(198deg) brightness(99%) contrast(85%); + /* Same as `--sidebar-active` */ + --copy-button-filter-hover: invert(68%) sepia(55%) saturate(531%) hue-rotate(341deg) brightness(104%) contrast(101%); + + --footnote-highlight: #2668a6; + + --overlay-bg: rgba(33, 40, 48, 0.4); +} + +.coal { + --bg: hsl(200, 7%, 8%); + --fg: #98a3ad; + + --sidebar-bg: #292c2f; + --sidebar-fg: #a1adb8; + --sidebar-non-existant: #505254; + --sidebar-active: #3473ad; + --sidebar-spacer: #393939; + + --scrollbar: var(--sidebar-fg); + + --icons: #43484d; + --icons-hover: #b3c0cc; + + --links: #2b79a2; + + --inline-code-color: #c5c8c6; + + --theme-popup-bg: #141617; + --theme-popup-border: #43484d; + --theme-hover: #1f2124; + + --quote-bg: hsl(234, 21%, 18%); + --quote-border: hsl(234, 21%, 23%); + + --warning-border: #ff8e00; + + --table-border-color: hsl(200, 7%, 13%); + --table-header-bg: hsl(200, 7%, 28%); + --table-alternate-bg: hsl(200, 7%, 11%); + + --searchbar-border-color: #aaa; + --searchbar-bg: #b7b7b7; + --searchbar-fg: #000; + --searchbar-shadow-color: #aaa; + --searchresults-header-fg: #666; + --searchresults-border-color: #98a3ad; + --searchresults-li-bg: #2b2b2f; + --search-mark-bg: #355c7d; + + --color-scheme: dark; + + /* Same as `--icons` */ + --copy-button-filter: invert(26%) sepia(8%) saturate(575%) hue-rotate(169deg) brightness(87%) contrast(82%); + /* Same as `--sidebar-active` */ + --copy-button-filter-hover: invert(36%) sepia(70%) saturate(503%) hue-rotate(167deg) brightness(98%) contrast(89%); + + --footnote-highlight: #4079ae; + + --overlay-bg: rgba(33, 40, 48, 0.4); +} + +.light, html:not(.js) { + --bg: hsl(0, 0%, 100%); + --fg: hsl(0, 0%, 0%); + + --sidebar-bg: #fafafa; + --sidebar-fg: hsl(0, 0%, 0%); + --sidebar-non-existant: #aaaaaa; + --sidebar-active: #1f1fff; + --sidebar-spacer: #f4f4f4; + + --scrollbar: #8F8F8F; + + --icons: #747474; + --icons-hover: #000000; + + --links: #20609f; + + --inline-code-color: #301900; + + --theme-popup-bg: #fafafa; + --theme-popup-border: #cccccc; + --theme-hover: #e6e6e6; + + --quote-bg: hsl(197, 37%, 96%); + --quote-border: hsl(197, 37%, 91%); + + --warning-border: #ff8e00; + + --table-border-color: hsl(0, 0%, 95%); + --table-header-bg: hsl(0, 0%, 80%); + --table-alternate-bg: hsl(0, 0%, 97%); + + --searchbar-border-color: #aaa; + --searchbar-bg: #fafafa; + --searchbar-fg: #000; + --searchbar-shadow-color: #aaa; + --searchresults-header-fg: #666; + --searchresults-border-color: #888; + --searchresults-li-bg: #e4f2fe; + --search-mark-bg: #a2cff5; + + --color-scheme: light; + + /* Same as `--icons` */ + --copy-button-filter: invert(45.49%); + /* Same as `--sidebar-active` */ + --copy-button-filter-hover: invert(14%) sepia(93%) saturate(4250%) hue-rotate(243deg) brightness(99%) contrast(130%); + + --footnote-highlight: #7e7eff; + + --overlay-bg: rgba(200, 200, 205, 0.4); +} + +.navy { + --bg: hsl(226, 23%, 11%); + --fg: #bcbdd0; + + --sidebar-bg: #282d3f; + --sidebar-fg: #c8c9db; + --sidebar-non-existant: #505274; + --sidebar-active: #2b79a2; + --sidebar-spacer: #2d334f; + + --scrollbar: var(--sidebar-fg); + + --icons: #737480; + --icons-hover: #b7b9cc; + + --links: #2b79a2; + + --inline-code-color: #c5c8c6; + + --theme-popup-bg: #161923; + --theme-popup-border: #737480; + --theme-hover: #282e40; + + --quote-bg: hsl(226, 15%, 17%); + --quote-border: hsl(226, 15%, 22%); + + --warning-border: #ff8e00; + + --table-border-color: hsl(226, 23%, 16%); + --table-header-bg: hsl(226, 23%, 31%); + --table-alternate-bg: hsl(226, 23%, 14%); + + --searchbar-border-color: #aaa; + --searchbar-bg: #aeaec6; + --searchbar-fg: #000; + --searchbar-shadow-color: #aaa; + --searchresults-header-fg: #5f5f71; + --searchresults-border-color: #5c5c68; + --searchresults-li-bg: #242430; + --search-mark-bg: #a2cff5; + + --color-scheme: dark; + + /* Same as `--icons` */ + --copy-button-filter: invert(51%) sepia(10%) saturate(393%) hue-rotate(198deg) brightness(86%) contrast(87%); + /* Same as `--sidebar-active` */ + --copy-button-filter-hover: invert(46%) sepia(20%) saturate(1537%) hue-rotate(156deg) brightness(85%) contrast(90%); + + --footnote-highlight: #4079ae; + + --overlay-bg: rgba(33, 40, 48, 0.4); +} + +.rust { + --bg: hsl(60, 9%, 87%); + --fg: #262625; + + --sidebar-bg: #3b2e2a; + --sidebar-fg: #c8c9db; + --sidebar-non-existant: #505254; + --sidebar-active: #e69f67; + --sidebar-spacer: #45373a; + + --scrollbar: var(--sidebar-fg); + + --icons: #737480; + --icons-hover: #262625; + + --links: #2b79a2; + + --inline-code-color: #6e6b5e; + + --theme-popup-bg: #e1e1db; + --theme-popup-border: #b38f6b; + --theme-hover: #99908a; + + --quote-bg: hsl(60, 5%, 75%); + --quote-border: hsl(60, 5%, 70%); + + --warning-border: #ff8e00; + + --table-border-color: hsl(60, 9%, 82%); + --table-header-bg: #b3a497; + --table-alternate-bg: hsl(60, 9%, 84%); + + --searchbar-border-color: #aaa; + --searchbar-bg: #fafafa; + --searchbar-fg: #000; + --searchbar-shadow-color: #aaa; + --searchresults-header-fg: #666; + --searchresults-border-color: #888; + --searchresults-li-bg: #dec2a2; + --search-mark-bg: #e69f67; + + /* Same as `--icons` */ + --copy-button-filter: invert(51%) sepia(10%) saturate(393%) hue-rotate(198deg) brightness(86%) contrast(87%); + /* Same as `--sidebar-active` */ + --copy-button-filter-hover: invert(77%) sepia(16%) saturate(1798%) hue-rotate(328deg) brightness(98%) contrast(83%); + + --footnote-highlight: #d3a17a; + + --overlay-bg: rgba(150, 150, 150, 0.25); +} + +@media (prefers-color-scheme: dark) { + html:not(.js) { + --bg: hsl(200, 7%, 8%); + --fg: #98a3ad; + + --sidebar-bg: #292c2f; + --sidebar-fg: #a1adb8; + --sidebar-non-existant: #505254; + --sidebar-active: #3473ad; + --sidebar-spacer: #393939; + + --scrollbar: var(--sidebar-fg); + + --icons: #43484d; + --icons-hover: #b3c0cc; + + --links: #2b79a2; + + --inline-code-color: #c5c8c6; + + --theme-popup-bg: #141617; + --theme-popup-border: #43484d; + --theme-hover: #1f2124; + + --quote-bg: hsl(234, 21%, 18%); + --quote-border: hsl(234, 21%, 23%); + + --warning-border: #ff8e00; + + --table-border-color: hsl(200, 7%, 13%); + --table-header-bg: hsl(200, 7%, 28%); + --table-alternate-bg: hsl(200, 7%, 11%); + + --searchbar-border-color: #aaa; + --searchbar-bg: #b7b7b7; + --searchbar-fg: #000; + --searchbar-shadow-color: #aaa; + --searchresults-header-fg: #666; + --searchresults-border-color: #98a3ad; + --searchresults-li-bg: #2b2b2f; + --search-mark-bg: #355c7d; + + --color-scheme: dark; + + /* Same as `--icons` */ + --copy-button-filter: invert(26%) sepia(8%) saturate(575%) hue-rotate(169deg) brightness(87%) contrast(82%); + /* Same as `--sidebar-active` */ + --copy-button-filter-hover: invert(36%) sepia(70%) saturate(503%) hue-rotate(167deg) brightness(98%) contrast(89%); + } +} diff --git a/docs/dup/README.txt b/docs/dup/README.txt new file mode 100644 index 0000000000..554222e309 --- /dev/null +++ b/docs/dup/README.txt @@ -0,0 +1,2 @@ +Do not manually edit any file in this folder. They have been autogenerated +by a script. diff --git a/docs/dup/beamsplitter.html b/docs/dup/beamsplitter.html new file mode 100644 index 0000000000..778f0b81f9 --- /dev/null +++ b/docs/dup/beamsplitter.html @@ -0,0 +1,346 @@ + + + + + + beamsplitter - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

beamsplitter

+ +

Description

+

This Go program consumes C++ header file(s) and generates Java bindings, JavaScript bindings, and +C++ code that performs JSON serialization.

+

Instructions

+

To install the Go compiler on macOS, just do:

+
brew install go
+
+

To build and invoke the code generator, do:

+
cd tools/beamsplitter ; go run .
+
+

Emitter Flags

+

Special directives in the form %codegen_foo% are called emitter flags. They are typically +embedded in a comment associated with a particular struct field.

+
+ + + + +
flagdescription
codegen_skip_jsonField is skipped when generating JSON serialization code.
codegen_skip_javascriptField is skipped when generating JavaScript and TypeScript bindings.
codegen_java_flattenField is replaced with constituent sub-fields.
codegen_java_floatField will be forced to have a float representation in Java.
+
+

Source Files

+
    +
  • filament/include/filament/Options.h
  • +
+

Output Files

+

The following files are created:

+
    +
  • libs/viewer/src/Settings_generated.h
  • +
  • libs/viewer/src/Settings_generated.cpp
  • +
  • web/filament-js/jsbindings_generated.cpp
  • +
  • web/filament-js/jsenums_generated.cpp
  • +
  • web/filament-js/extensions_generated.js
  • +
+

Additionally, in-place edits are made to the following files:

+
    +
  • web/filament-js/filament.d.ts
  • +
  • android/filament-android/src/main/java/.../View.java
  • +
+

Input Format

+

There are many ways in which the source file format is more restrictive than the full C++ +language, but here are some of the highlights:

+
    +
  • All enums must be class enums.
  • +
  • External headers pulled in with #include files are ignored.
  • +
  • Expressions in the RHS of default value assignments are not parsed, they are just exposed by +the lexer as blobs.
  • +
  • Struct fields, class fields, and method arguments must have fairly simple types. e.g. they cannot +have parentheses. If a type is C style callback, then it should be specified with an alias.
  • +
  • Multiline strings and macro definitions are not allowed.
  • +
  • Enum values must be sequential and cannot have custom values.
  • +
+

The following formal grammar describes the above limitations in greater detail, but with some +caveats:

+
    +
  • All C preprocessor directives are discarded during lexical analysis; they do not exist in the AST.
  • +
  • Whitespace is similarly discarded, so there is no "space" concept in the AST.
  • +
  • Macro invocations are also removed by the lexer if they are known Filament-specific macros (e.g. +UTILS_PUBLIC and UTILS_DEPRECATED).
  • +
  • Comments are removed by the lexer and are generally not part of the resulting AST. However +the lexer proffers a mapping from line numbers to comments to allow for docstring extraction.
  • +
  • Emitter flags in the form %codegen_foo% are detected in a post-processing phase and removed from +all comments.
  • +
+

Grammar

+
root = namespace ;
+namespace = "namespace" , [ ident ] , "{" , { block } , "}" ;
+block = class | struct | enum | namespace | using | forward_declaration;
+forward_declaration = ("class" | "struct" ) , ident , ";" ;
+template = "template" , "TemplateArgs" ;
+class = [template] , "class" , ident , [ ":" , [ "public" ] , "SimpleType" ]
+    , "{" , struct_body  , "}" , ";" ;
+struct = [template] , "struct" , ident , "{" , struct_body , "}" , ";" ;
+enum = "enum" , "class" , ident , [ ":" , type ]
+    , "{" , , ident , { "," , ident } , [ "," ] , "}" , ";" ;
+using = "using" , ident , "=", type , ";" ;
+struct_body = { access_specifier | field | method | block } ;
+access_specifier = ("public" | "private" | "protected" ) , ":" ;
+method = [template] , { "constexpr" , "friend" } ,
+    , type , ident , "MethodArgs" , specifiers , ( ";" | "MethodBody" ) ;
+specifiers = { "const" | "noexcept" } ;
+field = type , ident , [ array ] , [ "=" , "DefaultValue" ] ";" ;
+array = "[" , "ArrayLength", "]" ;
+type = "SimpleType" ;
+ident = "Identifier" ;
+
+

The above grammar uses the following notation:

+
    +
  • " ... " denotes a terminal
  • +
  • { ... } denotes zero or more repetition
  • +
  • [ ... ] denotes an optional quantity
  • +
  • ( ... ) is used for grouping
  • +
  • a | b denotes a choice
  • +
  • a , b denotes concatenation
  • +
  • ; terminates a production
  • +
+
+ + + + + + + +
Terminal nameDescription
SimpleType (*)examples: Texture* const, uint8_t, BlendMode
MethodBodyunparsed implementation of a function or method, including outer {}
MethodArgssimilar to above; an unparsed blob, but delimited with ()
TemplateArgssimilar to above; an unparsed blob, but delimited with <>
DefaultValue (**)an unparsed expression with certain restrictions
Identifier[A-Za-z_][A-Za-z0-9_]*
ArrayLength[1-9][0-9]*
+
+

(*) SimpleType should not contain parentheses or commas, so C callbacks are not allowed unless +you alias them first.

+

(**) If DefaultValue is a vector, it must be in the form: { x, y, z }.

+

References

+

Initially inspired by the following Rob Pike talk.

+
    +
  • https://www.youtube.com/watch?v=HxaD_trXwRE
  • +
+

Beamsplitter does not use the state machine described in the above prezo, but it does use a channel +for separating the parser from the lexer. The beamsplitter lexer is actually a recursive descent +parser with simple lookahead functionality. This makes it easy for the "real" parser to create a +coarse-grained AST.

+

The companion to the above talk is Go's template lexer, which can be studied here:

+
    +
  • https://cs.opensource.google/go/go/+/master:src/text/template/parse/lex.go
  • +
+

Wikipedia has a good example of recursive descent:

+
    +
  • https://en.wikipedia.org/wiki/Recursive_descent_parser
  • +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/bluegl.html b/docs/dup/bluegl.html new file mode 100644 index 0000000000..df9162e553 --- /dev/null +++ b/docs/dup/bluegl.html @@ -0,0 +1,251 @@ + + + + + + bluegl - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

BlueGL Mechanics

+

So you want to call glClear()?

+

Step 0: Run bluegl-gen.py

+

This step is only required if updating or modifying BlueGL. These artifacts should already be checked into the Filament repository.

+

From the libs/bluegl folder, run:

+
./bluegl-gen.py
+
+

The bluegl-gen.py script generates a set of files:

+
    +
  • assembly (proxy) files: BlueGLCore*.S
  • +
  • header files: include/BlueGLDefines.h and include/bluegl/BlueGL.h
  • +
  • a private header:include/private_BlueGL.h
  • +
+

Step 1: Include the BlueGL defines header:

+
#include <bluegl/BlueGLDefines.h>
+
+

This headers adds a bunch of defines:

+
...
+#define glClear bluegl_glClear
+...
+
+

Step 2: Include the BlueGL header after the defines header:

+
#include <bluegl/BlueGLDefines.h>
+#include <bluegl/BlueGL.h>
+
+

This also includes the GL headers, like <GL/glcorearb.h> for you.

+

Step 3: Call bluegl::bind()

+

Internally, the BlueGL library maintains a list of function pointers:

+
void* __blue_glCore_glClear;
+
+

During bluegl::bind(), each function gets assigned to the appropriate symbol loaded from the OS-specific GL shared library via dlopen, dlsym, and equivalents.

+

Step 4: Call glClear()

+

Because of the prior #define, you'll actually be calling bluegl_glClear(). This is a trampoline function, defined in the BlueGLCore*.S assembly file (the exact implementation varies slightly on each platform):

+
.private_extern _bluegl_glClear
+_bluegl_glClear:
+    mov ___blue_glCore_glClear@GOTPCREL(%rip), %r11
+    jmp *(%r11)
+
+

The invokes the __blue_glCore_glClear function, which was previously assigned to the actual GL function.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/bluevk.html b/docs/dup/bluevk.html new file mode 100644 index 0000000000..4a0766f07f --- /dev/null +++ b/docs/dup/bluevk.html @@ -0,0 +1,225 @@ + + + + + + bluevk - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Updating Vulkan headers

+

To update the Vulkan headers, perform the following steps.

+

First, find the latest version of the Vulkan headers here: +https://github.com/KhronosGroup/Vulkan-Headers/tags

+

Replace v1.3.232 with the latest version of the headers in the following commands.

+
cd libs/bluevk
+curl -OL https://github.com/KhronosGroup/Vulkan-Headers/archive/refs/tags/v1.3.232.zip
+unzip v1.3.232.zip
+rsync -r Vulkan-Headers-1.3.232/include/vulkan/ include/vulkan --delete
+rsync -r Vulkan-Headers-1.3.232/include/vk_video/ include/vk_video --delete
+rm include/vulkan/*.hpp
+rm -r Vulkan-Headers-1.3.232 v1.3.232.zip
+
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/building.html b/docs/dup/building.html new file mode 100644 index 0000000000..c1b4074d14 --- /dev/null +++ b/docs/dup/building.html @@ -0,0 +1,560 @@ + + + + + + Build - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Building Filament

+

Prerequisites

+

To build Filament, you must first install the following tools:

+
    +
  • CMake 3.22.1 (or more recent)
  • +
  • clang 16.0 (or more recent)
  • +
  • ninja 1.10 (or more recent)
  • +
+

Additional dependencies may be required for your operating system. Please refer to the appropriate +section below.

+

To build Filament for Android you must also install the following:

+
    +
  • Android Studio Flamingo or more recent
  • +
  • Android SDK
  • +
  • Android NDK 25.1 or higher
  • +
  • Java 17
  • +
+

Environment variables

+

To build Filament for Android, make sure the environment variable ANDROID_HOME points to the +location of your Android SDK.

+

When building for WebGL, you'll also need to set EMSDK. See WebAssembly.

+

IDE

+

We recommend using CLion to develop for Filament. Simply open the root directory's CMakeLists.txt +in CLion to obtain a usable project.

+

Easy build

+

Once the required OS specific dependencies listed below are installed, you can use the script +located in build.sh to build Filament easily on macOS and Linux.

+

This script can be invoked from anywhere and will produce build artifacts in the out/ directory +inside the Filament source tree.

+

To trigger an incremental debug build:

+
./build.sh debug
+
+

To trigger an incremental release build:

+
./build.sh release
+
+

To trigger both incremental debug and release builds:

+
./build.sh debug release
+
+

If build fails for some reasons, it may leave the out/ directory in a broken state. You can +force a clean build by adding the -c flag in that case.

+

To install the libraries and executables in out/debug/ and out/release/, add the -i flag. +The script offers more features described by executing build.sh -h.

+

Filament-specific CMake Options

+

The following CMake options are boolean options specific to Filament:

+
    +
  • FILAMENT_ENABLE_LTO: Enable link-time optimizations if supported by the compiler
  • +
  • FILAMENT_BUILD_FILAMAT: Build filamat and JNI buildings
  • +
  • FILAMENT_SUPPORTS_OPENGL: Include the OpenGL backend
  • +
  • FILAMENT_SUPPORTS_METAL: Include the Metal backend
  • +
  • FILAMENT_SUPPORTS_VULKAN: Include the Vulkan backend
  • +
  • FILAMENT_INSTALL_BACKEND_TEST: Install the backend test library so it can be consumed on iOS
  • +
  • FILAMENT_USE_EXTERNAL_GLES3: Experimental: Compile Filament against OpenGL ES 3
  • +
  • FILAMENT_SKIP_SAMPLES: Don't build sample apps
  • +
+

To turn an option on or off:

+
cd <cmake-build-directory>
+cmake . -DOPTION=ON       # Replace OPTION with the option name, set to ON / OFF
+
+

Options can also be set with the CMake GUI.

+

Linux

+

Make sure you've installed the following dependencies:

+
    +
  • clang-16 or higher
  • +
  • libglu1-mesa-dev
  • +
  • libc++-16-dev (libcxx-devel and libcxx-static on Fedora) or higher
  • +
  • libc++abi-16-dev (libcxxabi-static on Fedora) or higher
  • +
  • ninja-build
  • +
  • libxi-dev
  • +
  • libxcomposite-dev (libXcomposite-devel on Fedora)
  • +
  • libxxf86vm-dev (libXxf86vm-devel on Fedora)
  • +
+
sudo apt install clang-16 libglu1-mesa-dev libc++-16-dev libc++abi-16-dev ninja-build libxi-dev libxcomposite-dev libxxf86vm-dev -y
+
+

After dependencies have been installed, we highly recommend using the easy build +script.

+

If you'd like to run cmake directly rather than using the build script, it can be invoked as +follows, with some caveats that are explained further down.

+
mkdir out/cmake-release
+cd out/cmake-release
+cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=../release/filament ../..
+
+

Your Linux distribution might default to gcc instead of clang, if that's the case invoke +cmake with the following command:

+
mkdir out/cmake-release
+cd out/cmake-release
+# Or use a specific version of clang, for instance /usr/bin/clang-16
+CC=/usr/bin/clang CXX=/usr/bin/clang++ CXXFLAGS=-stdlib=libc++ \
+  cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=../release/filament ../..
+
+

You can also export the CC and CXX environment variables to always point to clang. Another +solution is to use update-alternatives to both change the default compiler, and point to a +specific version of clang:

+
update-alternatives --install /usr/bin/clang clang /usr/bin/clang-16 100
+update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-16 100
+update-alternatives --install /usr/bin/cc cc /usr/bin/clang 100
+update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++ 100
+
+

Finally, invoke ninja:

+
ninja
+
+

This will build Filament, its tests and samples, and various host tools.

+

macOS

+

To compile Filament you must have the most recent version of Xcode installed and you need to +make sure the command line tools are setup by running:

+
xcode-select --install
+
+

If you wish to run the Vulkan backend instead of the default Metal backend, you must install +the LunarG SDK, enable "System Global Components", and reboot your machine.

+

Then run cmake and ninja to trigger a build:

+
mkdir out/cmake-release
+cd out/cmake-release
+cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=../release/filament ../..
+ninja
+
+

iOS

+

The easiest way to build Filament for iOS is to use build.sh and the +-p ios flag. For instance to build the debug target:

+
./build.sh -p ios debug
+
+

See ios/samples/README.md for more information.

+

Windows

+

Building on Windows with Visual Studio 2019 or later

+

Install the following components:

+ +

The latest Windows SDK can also be installed by opening Visual Studio and selecting Get Tools and +Features... under the Tools menu.

+

By default, Windows treats the file system as case insensitive. Please do not enable case +sensitivity in your repo, since this does not align with CMake expectations. This can be queried +using fsutil.exe file queryCaseSensitiveInfo.

+

Next, open x64 Native Tools Command Prompt for VS 2019, create a working directory, and run +CMake in it:

+
mkdir out
+cd out
+cmake ..
+
+

Open the generated solution file TNT.sln in Visual Studio.

+

To build all targets, run Build Solution from the Build menu. Alternatively, right click on a +target in the Solution Explorer and choose Build to build a specific target.

+

For example, build the material_sandbox sample and run it from the out directory with:

+
samples\Debug\material_sandbox.exe ..\assets\models\monkey\monkey.obj
+
+

You can also use CMake to invoke the build without opening Visual Studio. For example, from the +out folder run the following command.

+
cmake --build . --target gltf_viewer --config Release
+
+

Android

+

Before building Filament for Android, make sure to build Filament for your host. Some of the +host tools are required to successfully build for Android.

+

Filament can be built for the following architectures:

+
    +
  • ARM 64-bit (arm64-v8a)
  • +
  • ARM 32-bit (armeabi-v7a)
  • +
  • Intel 64-bit (x86_64)
  • +
  • Intel 32-bit (x86)
  • +
+

Note that the main target is the ARM 64-bit target. Our implementation is optimized first and +foremost for arm64-v8a.

+

To build Android on Windows machines, see android/Windows.md.

+

Easy Android build

+

The easiest way to build Filament for Android is to use build.sh and the +-p android flag. For instance to build the release target:

+
./build.sh -p android release
+
+

Run build.sh -h for more information.

+

Manual builds

+

Invoke CMake in a build directory of your choice, inside of filament's directory. The commands +below show how to build Filament for ARM 64-bit (aarch64).

+
mkdir out/android-build-release-aarch64
+cd out/android-build-release-aarch64
+cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=../../build/toolchain-aarch64-linux-android.cmake \
+      -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=../android-release/filament ../..
+
+

And then invoke ninja:

+
ninja install
+
+

or

+
ninja install/strip
+
+

This will generate Filament's Android binaries in out/android-release. This location is important +to build the Android Studio projects located in filament/android. After install, the library +binaries should be found in out/android-release/filament/lib/arm64-v8a.

+

AAR

+

Before you attempt to build the AAR, make sure you've compiled and installed the native libraries +as explained in the sections above. You must have the following ABIs built in +out/android-release/filament/lib/:

+
    +
  • arm64-v8a
  • +
  • armeabi-v7a
  • +
  • x86_64
  • +
  • x86
  • +
+

To build Filament's AAR simply open the Android Studio project in android/. The +AAR is a universal AAR that contains all supported build targets:

+
    +
  • arm64-v8a
  • +
  • armeabi-v7a
  • +
  • x86_64
  • +
  • x86
  • +
+

To filter out unneeded ABIs, rely on the abiFilters of the project that links against Filament's +AAR.

+

Alternatively you can build the AAR from the command line by executing the following in the +android/ directory:

+
./gradlew -Pcom.google.android.filament.dist-dir=../../out/android-release/filament assembleRelease
+
+

The -Pcom.google.android.filament.dist-dir can be used to specify a different installation +directory (it must match the CMake install prefix used in the previous steps).

+

Using Filament's AAR

+

Create a new module in your project and select Import .JAR or .AAR Package when prompted. Make +sure to add the newly created module as a dependency to your application.

+

If you do not wish to include all supported ABIs, make sure to create the appropriate flavors in +your Gradle build file. For example:

+
flavorDimensions 'cpuArch'
+productFlavors {
+    arm8 {
+        dimension 'cpuArch'
+        ndk {
+            abiFilters 'arm64-v8a'
+        }
+    }
+    arm7 {
+        dimension 'cpuArch'
+        ndk {
+            abiFilters 'armeabi-v7a'
+        }
+    }
+    x86_64 {
+        dimension 'cpuArch'
+        ndk {
+            abiFilters 'x86_64'.
+        }
+    }
+    x86 {
+        dimension 'cpuArch'
+        ndk {
+            abiFilters 'x86'
+        }
+    }
+    universal {
+        dimension 'cpuArch'
+    }
+}
+
+

WebAssembly

+

The core Filament library can be cross-compiled to WebAssembly from either macOS or Linux. To get +started, follow the instructions for building Filament on your platform (macOS or +linux), which will ensure you have the proper dependencies installed.

+

Next, you need to install the Emscripten SDK. The following instructions show how to install the +same version that our continuous builds use.

+
cd <your chosen parent folder for the emscripten SDK>
+curl -L https://github.com/emscripten-core/emsdk/archive/refs/tags/3.1.60.zip > emsdk.zip
+unzip emsdk.zip ; mv emsdk-* emsdk ; cd emsdk
+python ./emsdk.py install latest
+python ./emsdk.py activate latest
+source ./emsdk_env.sh
+
+

Alternatively, you can try running the script build/common/get-emscripten.sh.

+

After this you can invoke the easy build script as follows:

+
export EMSDK=<your chosen home for the emscripten SDK>
+./build.sh -p webgl release
+
+

The EMSDK variable is required so that the build script can find the Emscripten SDK. The build +creates a samples folder that can be used as the root of a simple static web server. Note that you +cannot open the HTML directly from the filesystem due to CORS. We recommend using the emrun tool +to create a quick localhost server:

+
emrun out/cmake-webgl-release/web/samples --no_browser --port 8000
+
+

You can then open http://localhost:8000/suzanne.html in your web browser.

+

Alternatively, if you have node installed you can use the +live-server package, which automatically refreshes the +web page when it detects a change.

+

Each sample app has its own handwritten html file. Additionally the server folder contains assets +such as meshes, textures, and materials.

+

Running the native samples

+

The samples/ directory contains several examples of how to use Filament with SDL2.

+

Some of the samples accept FBX/OBJ meshes while others rely on the filamesh file format. To +generate a filamesh file from an FBX/OBJ asset, run the filamesh tool +(./tools/filamesh/filamesh in your build directory):

+
filamesh ./assets/models/monkey/monkey.obj monkey.filamesh
+
+

Most samples accept an IBL that must be generated using the cmgen tool (./tools/filamesh/cmgen +in your build directory). These sample apps expect a path to a directory containing the .rgb32f +files for the IBL (which are PNGs containing R11F_G11F_B10F data) or a path to a directory +containing two .ktx files (one for the IBL itself, one for the skybox). To generate an IBL +simply use this command:

+
cmgen -f ktx -x ./ibls/ my_ibl.exr
+
+

The source environment map can be a PNG (8 or 16 bit), a PSD (16 or 32 bit), an HDR or an OpenEXR +file. The environment map can be an equirectangular projection, a horizontal cross, a vertical +cross, or a list of cubemap faces (horizontal or vertical).

+

cmgen will automatically create a directory based on the name of the source environment map. In +the example above, the final directory will be ./ibls/my_ibl/. This directory should contain the +pre-filtered environment map (one file per cubemap face and per mip level), the environment map +texture for the skybox and a text file containing the level harmonics for indirect diffuse +lighting.

+

If you prefer a blurred background, run cmgen with this flag: --extract-blur=0.1. The numerical +value is the desired roughness between 0 and 1.

+

Generating C++ documentation

+

To generate the documentation you must first install doxygen and graphviz, then run the +following commands:

+
cd filament/filament
+doxygen docs/doxygen/filament.doxygen
+
+

Finally simply open docs/html/index.html in your web browser.

+

Software Rasterization

+

We have tested swiftshader and Mesa for software rasterization on the Vulkan/GL backends.

+

To use this for Vulkan, please first make sure that the Vulkan SDK is +installed on your machine. If you are doing a manual installation of the SDK on Linux, you will have +to source setup-env.sh in the SDK's root folder to make sure the Vulkan loader is the first lib loaded.

+

Swiftshader (Vulkan) [tested on macOS and Linux]

+

First, build SwiftShader

+
git clone https://github.com/google/swiftshader.git
+cd swiftshader/build
+cmake .. &&  make -j
+
+

and then set VK_ICD_FILENAMES to the ICD json produced in the build. For example,

+
export VK_ICD_FILENAMES=/Users/user/swiftshader/build/Darwin/vk_swiftshader_icd.json
+
+

Build and run Filament as usual and specify the Vulkan backend when creating the Engine.

+

Mesa's LLVMPipe (GL) and Lavapipe (Vulkan) [tested on Linux]

+

We will only cover steps that build Mesa from source. The official documentation of Mesa mentioned +that in general precompiled libraries are not made available.

+

Download the repo and make sure you have the build depedencies. For example (assuming an Ubuntu/Debian distro),

+
git clone https://gitlab.freedesktop.org/mesa/mesa.git
+sudo apt-get build-dep mesa
+
+

To build both the GL and Vulkan rasterizers,

+
cd mesa
+mkdir -p out
+meson setup builddir/ -Dprefix=$(pwd)/out -Dglx=xlib -Dgallium-drivers=swrast -Dvulkan-drivers=swrast
+meson install -C builddir/
+
+

For GL, we need to ensure that we load the GL lib from the mesa output directory. For example, to run +the debug gltf_viewer, we would execute

+
LD_LIBRARY_PATH=/Users/user/mesa/out/lib/x86_64-linux-gnu \
+    ./out/cmake-debug/samples/gltf_viewer -a opengl
+
+

For Vulkan, we need to set the path to the ICD json, which tells the loader where to find the driver +library. To run gltf_viewer, we would execute

+
VK_ICD_FILENAMES=/Users/user/mesa/out/share/vulkan/icd.d/lvp_icd.x86_64.json \
+    ./out/cmake-debug/samples/gltf_viewer -a vulkan
+
+
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/cmgen.html b/docs/dup/cmgen.html new file mode 100644 index 0000000000..c59b038123 --- /dev/null +++ b/docs/dup/cmgen.html @@ -0,0 +1,269 @@ + + + + + + cmgen - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

cmgen

+

cmgen is a command-line tool for generating SH and mipmap levels from an env map. +Cubemaps and equirectangular formats are both supported, automatically detected according to the aspect ratio of the source image.

+

The tool can consume a HDR environment map in latlong format (equirectilinear) as well as "cross" cubemap formats (vertical and horizontal), and row/column cubemap formats, and can produce a mipmapped IBL(Image Based Lighting) or a blurry skybox or both.

+

Usage

+
cmgen [options] <input-file>
+cmgen [options] <uv[N]>
+
+

Supported input formats

+
    +
  • PNG, 8 and 16 bits
  • +
  • Radiance (.hdr)
  • +
  • Photoshop (.psd), 16 and 32 bits
  • +
  • OpenEXR (.exr)
  • +
+

Options

+
--help, -h
+    Print this message
+--license
+    Print copyright and license information
+--quiet, -q
+    Quiet mode. Suppress all non-error output
+--type=[cubemap|equirect|octahedron|ktx], -t [cubemap|equirect|octahedron|ktx]
+    Specify output type (default: cubemap)
+--format=[exr|hdr|psd|rgbm|rgb32f|png|dds|ktx], -f [format]
+    Specify output file format. ktx implies -type=ktx.
+    KTX files are always KTX1 files, not KTX2.
+    They are encoded with 3-channel RGB_10_11_11_REV data
+--compression=COMPRESSION, -c COMPRESSION
+    Format specific compression:
+        KTX: ignored
+        PNG: Ignored
+        PNG RGBM: Ignored
+        Radiance: Ignored
+        Photoshop: 16 (default), 32
+        OpenEXR: RAW, RLE, ZIPS, ZIP, PIZ (default)
+        DDS: 8, 16 (default), 32
+--size=power-of-two, -s power-of-two
+    Size of the output cubemaps (base level), 256 by default
+    Also applies to DFG LUT
+--deploy=dir, -x dir
+    Generate everything needed for deployment into <dir>
+--extract=dir
+    Extract faces of the cubemap into <dir>
+--extract-blur=roughness
+    Blurs the cubemap before saving the faces using the roughness blur
+--clamp
+    Clamp environment before processing
+--no-mirror
+    Skip mirroring of generated cubemaps (for assets with mirroring already backed in)
+--ibl-samples=numSamples
+    Number of samples to use for IBL integrations (default 1024)
+--ibl-ld=dir
+    Roughness pre-filter into <dir>
+--sh-shader
+    Generate irradiance SH for shader code
+
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/code_style.html b/docs/dup/code_style.html new file mode 100644 index 0000000000..4927ba867a --- /dev/null +++ b/docs/dup/code_style.html @@ -0,0 +1,393 @@ + + + + + + Coding Style - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Filament Code style and Formatting

+

Filament largely uses Android's code style, which is significantly different from the +Google code style and is derived from the Java code style, but not quite.

+

The guiding principles of the filament code style and code formatting can be resumed as:

+
    +
  • no nonsense
  • +
  • use your own judgement
  • +
  • break the rules if it makes sense e.g.: it improves readability substantially
  • +
  • use the formatting of the file you're in, even if it breaks the rules
  • +
  • no nonsense
  • +
+

Formatting

+
    +
  • 4 spaces indent
  • +
  • 8 spaces continuation indent
  • +
  • 100 columns
  • +
  • { at the end of the line
  • +
  • spaces around operators and after ;
  • +
  • class access modifiers are not indented
  • +
  • last line of .cpp or .h file must be an empty line
  • +
+
for (int i = 0; i < max; i++) {
+}
+
+class Foo {
+public:
+protected:
+private:
+};
+
+
+

Naming Conventions

+

Files

+
    +
  • headers use the .h extension
  • +
  • implementation files use the .cpp extension
  • +
  • included files use the .inc extension
  • +
  • class files bear the name of the class they implement
  • +
  • no spaces in file names
  • +
  • file names must be treated as case insensitive, i.e. it is not allowed to have several files +with the same name but a different case
  • +
  • #include must use fully qualified names
  • +
  • use #include < > for all public (exported) headers
  • +
  • use #include " " for private headers
  • +
  • all public include files must reside under the include folder
  • +
  • all source files must reside under the src folder
  • +
  • tests reside under the test folder
  • +
  • public headers of a foo library must live in a folder named foo
  • +
+
libfoo.so
+
+include/foo/FooBar.h
+src/FooBar.cpp
+src/data.inc
+
+#include <foo/FooBar.h>
+#include "FooBarPrivate.h"
+
+

Code

+
    +
  • Everything is camel case except constants
  • +
  • constants are uppercase and don't have a prefix
  • +
  • global variables prefixed with g
  • +
  • static variables prefixed with s
  • +
  • private and protected class attributes prefixed with m
  • +
  • static class attributes prefixed with s
  • +
  • public class attributes are not prefixed
  • +
  • class attributes and methods are lower camelcase
  • +
+
extern int gGlobalWarming;
+
+class FooBar {
+public:
+    void methodName();
+    int sizeInBytes;
+private:
+    int mAttributeName;
+    static int sGlobalAttribute;
+    static constexpr int FOO_COUNT = 10;
+    enum {
+        ONE, TWO, THREE
+    };
+};
+
+

Code Style

+

Files

+
    +
  • always include the copyright notice at the top of every file
  • +
  • make sure the date is correct
  • +
+
/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+

Headers

+
    +
  • always include a class' header first in the .cpp file
  • +
  • other headers are sorted in reverse order of their layering, that is, lower layer headers last
  • +
  • within a layer, headers are sorted alphabetically
  • +
  • strive for implementing one class per file
  • +
  • STL limited in filament public headers to: +
      +
    • array
    • +
    • initializer_list
    • +
    • iterator
    • +
    • limits
    • +
    • optional
    • +
    • type_traits
    • +
    • utility
    • +
    • variant
    • +
    +
  • +
+

For libfilament the rule of thumb is that STL headers that don't generate code are allowed (e.g. type_traits), +conversely containers and algorithms are not allowed. There are exceptions such as array. See above for the full list.

+
    +
  • The following STL headers are banned entirely, from public and private headers as well as implementation files: +
      +
    • iostream
    • +
    +
  • +
+

Sorting the headers is important to help catching missing #include directives.

+
/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Bar.cpp
+
+#include <foo/Bar.h>
+
+#include "PrivateStuff.h"
+
+#include <foo/Alloc.h>
+#include <foo/Bar.h>
+
+#include <utils/compiler.h>
+
+#include <algorithm>
+#include <iostream>
+
+#include <assert.h>
+#include <string.h>
+
+

Strings

+
    +
  • Never use std::string in the Filament core renderer. Prefer utils::CString or std::string_view.
  • +
  • When using std::string in tools, always include the std:: qualifier to disambiguate it +from other string types.
  • +
+

Misc

+
    +
  • Use auto only when the type appears on the same line or with iterators and lambdas.
  • +
+
auto foo = new Foo();
+for (auto& i : collection) { }
+
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/contributing.html b/docs/dup/contributing.html new file mode 100644 index 0000000000..2b02d5a389 --- /dev/null +++ b/docs/dup/contributing.html @@ -0,0 +1,258 @@ + + + + + + Contribute - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

How to become a contributor and submit your own code

+

Contributor License Agreement

+

Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution; +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to https://cla.developers.google.com/ to see +your current agreements on file or to sign a new one.

+

You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again.

+

Contributing A Patch

+
    +
  1. Submit an issue describing your proposed change to the repo in question.
  2. +
  3. The repo owner will respond to your issue promptly.
  4. +
  5. If your proposed change is accepted, and you haven't already done so, sign a +Contributor License Agreement (see details above).
  6. +
  7. Fork the desired repo, develop and test your code changes.
  8. +
  9. Ensure that your code adheres to the existing style in the sample to which +you are contributing. Refer to CodeStyle.md for the recommended coding +standards for this project.
  10. +
  11. Ensure that your code has an appropriate set of unit tests which all pass.
  12. +
  13. Submit a pull request.
  14. +
+

Code Style

+

See CODE_STYLE.md

+

Code reviews

+

All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +GitHub Help for more +information on using pull requests.

+

Community Guidelines

+

This project follows +Google's Open Source Community Guidelines.

+

Dependencies

+

One of our design goals is that Filament itself should have no dependencies or as few dependencies +as possible. The current external dependencies of the runtime library include:

+
    +
  • STL
  • +
  • robin-map (header only library)
  • +
+

When building with Vulkan enabled, we have a few additional small dependencies:

+
    +
  • vkmemalloc
  • +
  • smol-v
  • +
+

Host tools (such as matc or cmgen) can use external dependencies freely.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/cso_lut.html b/docs/dup/cso_lut.html new file mode 100644 index 0000000000..3fd773f327 --- /dev/null +++ b/docs/dup/cso_lut.html @@ -0,0 +1,214 @@ + + + + + + cso-lut - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Cone/Sphere Occlusion LUT Generator

+

cso-lut is a tool that generates a lookup table to cone/sphere occlusions.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/docs.html b/docs/dup/docs.html new file mode 100644 index 0000000000..b16383d6bf --- /dev/null +++ b/docs/dup/docs.html @@ -0,0 +1,302 @@ + + + + + + Documentation - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Documentation

+

Filament's documentation (which you are reading) is a collection of pages created with mdBook.

+

How the book is created and updated

+

Prerequisites

+
    +
  • Install mdBook for your platform +
      +
    • There is a script docs_src/build/install_mdbook.sh that might help.
    • +
    +
  • +
  • It is best practice to install the python dependencies in a virtual python environment. You can +start such an environment by +
    python3 -m venv venv
    +. venv/bin/activate
    +
    +After that you may install deps and run the build script.
  • +
  • selenium package for python +
    python3 -m pip install selenium
    +
    +
  • +
+

Generate

+

We wrote a python script to gather and transform the different documents in the project tree into a +single book. This script can be found in docs_src/build/run.py. In addition, +docs_src/build/duplicates.json is used to describe the markdown files that are copied and +transformed from the source tree. These copies are placed into docs_src/src_mdbook/src/dup.

+

To collect the pages and generate the book, run the following

+
cd docs_src
+python3 build/run.py
+
+

Copy to docs

+

docs is the github-specfic directory for producing a web frontend (i.e. documentation) for a +project.

+

(To be completed)

+

Document sources

+

We list the different document sources and how they are copied and processed into the collection +of markdown files that are then processed with mdBook.

+

Introductory docs

+

The github landing page for Filament displays an extensive introduction to Filament. It +links to BUILDING.md and CONTRIBUTING.md, which are conventional pages for building or +contributing to the project. We copy these pages from their respective locations in the project +tree into docs_src/src_mdbook/src/dup. Moreover, to restore valid linkage between the pages, we need +to perform a number of URL replacements in addition to the copy. These replacements are +described in docs_src/build/duplicates.json.

+

Core concept docs

+

The primary design of Filament as a phyiscally-based renderer and details of its materials +system are described in Filament.md.html and Materials.md.html, respectively. These two +documents are written in markdeep. To embed them into our book, we

+
    +
  1. Convert the markdeep into html
  2. +
  3. Embed the html output in a markdown file
  4. +
  5. Place the markdown file in docs_src/src_mdbook/src/main
  6. +
+

We describe step 1 in detail for the sake of record:

+
    +
  • Start a local-only server to serve the markdeep file (e.g. Filament.md.html)
  • +
  • Start a selenium driver (essentially run chromium in headless mode)
  • +
  • Visit the local page through the driver (i.e. open url http://localhost:xx/Filament.md.html?export)
  • +
  • Parse out the exported output in the retrieved html (note that the output of the markdeep +export is an html with the output captured in a <pre> tag).
  • +
  • Replace css styling in the exported output as needed (so they don't interfere with the book's css.
  • +
  • Replace resource urls to refer to locations relative to the mdbook structure.
  • +
+

Any markdeep doc can be placed in docs_src/src_markdeep/ and they will be parsed to html and included +in the book as above.

+

READMEs

+

Filament depends on a number of libraries, which reside in the directory libs. These individual +libaries often have README.md in their root to describe itself. We collect these descriptions into our +book. In addition, client usage of Filament also requires using a set of binary tools, which are +located in tools. Some of tools also have README.md as description. We also collect them into the book.

+

The process for copying and processing these READMEs is outlined in Introductory docs.

+

Other technical notes

+

These are technical documents that do not fit into a library, tool, or directory of the +Filament source tree. We collect them into the docs_src/src_mdbook/src/notes directory. No additional +processing is needed for these documents.

+

Raw source files

+

These are files that are not part of the mdbook generation, but should be included output in /docs +to point to standalone pages or components (for example, the remote page for Android's gltf_viewer). These +files are stored in docs_src/src_raw.

+

Adding more documents

+

To add any documentation, first consider the type of the document you like to add. If it +belongs to any of the above sources, then simply place the document in the appropriate place, +add a link in SUMMARY.md, and perform the steps outlined in +how-to create section.

+

For example, if you are adding a general technical note, then you would

+ + +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/filamat.html b/docs/dup/filamat.html new file mode 100644 index 0000000000..9cea839cf5 --- /dev/null +++ b/docs/dup/filamat.html @@ -0,0 +1,385 @@ + + + + + + filamat - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Filamat

+

Filamat allows for generating materials programatically on the device as opposed to with the matc +tool on the host machine. The cost is a binary size increase of your app due to the relatively +larger size of the filamat library.

+

For a smaller-sized library, see filamat_lite. It has no dependencies on +glslang, but can only compile materials for OpenGL and does no shader code optimization.

+

The filamat package is included in the releases available on +GitHub.

+

Libraries

+

Filamat is distributed as a set of static libraries you must link against:

+
    +
  • filamat, Filamat main library
  • +
  • filabridge, Support library for Filament / Filamat
  • +
  • shaders, Shader text for material generation
  • +
  • utils, Support library for Filament / Filamat
  • +
  • smol-v, SPIR-V compression library
  • +
+

To use Filamat from Java you must use the following two libraries instead:

+
    +
  • filamat-java.jar, Contains Filamat's Java classes
  • +
  • filamat-jni, Filamat's JNI bindings
  • +
+

Linking against Filamat

+

This walkthrough will get you successfully compiling and linking native code against Filamat with +minimum dependencies.

+

To start, download Filament's latest binary release +and extract into a directory of your choosing. Binary releases are suffixed with the platform name, +for example, filament-20181009-linux.tgz.

+

Create a file, main.cpp, in the same directory with the following contents:

+
#include <filamat/MaterialBuilder.h>
+
+#include <iostream>
+
+using namespace filamat;
+
+int main(int argc, char** argv)
+{
+    // Must be called before any materials can be built.
+    MaterialBuilder::init();
+
+    MaterialBuilder builder;
+    builder
+        .name("My material")
+        .material("void material (inout MaterialInputs material) {"
+                  "  prepareMaterial(material);"
+                  "  material.baseColor.rgb = float3(1.0, 0.0, 0.0);"
+                  "}")
+        .shading(MaterialBuilder::Shading::LIT)
+        .targetApi(MaterialBuilder::TargetApi::ALL)
+        .platform(MaterialBuilder::Platform::ALL);
+
+    Package package = builder.build();
+    if (package.isValid()) {
+        std::cout << "Success!" << std::endl;
+    }
+
+    // Call when finished building all materials to release internal MaterialBuilder resources.
+    MaterialBuilder::shutdown();
+    return 0;
+}
+
+

The directory should look like:

+
|-- README.md
+|-- bin
+|-- docs
+|-- include
+|-- lib
+|-- main.cpp
+
+

We'll use a platform-specific Makefile to compile and link main.cpp with Filamat's libraries. +Copy your platform's Makefile below into a Makefile inside the same directory.

+

Linux

+
FILAMENT_LIBS=-lfilamat -lfilabridge -lshaders -lutils -lsmol-v
+CC=clang++
+
+main: main.o
+	$(CC) -Llib/x86_64/ -stdlib=libc++ main.o $(FILAMENT_LIBS) -lpthread -ldl -o main
+
+main.o: main.cpp
+	$(CC) -Iinclude/ -std=c++20 -stdlib=libc++ -pthread -c main.cpp
+
+clean:
+	rm -f main main.o
+
+.PHONY: clean
+
+

macOS

+
FILAMENT_LIBS=-lfilamat -lfilabridge -lshaders -lutils -lsmol-v
+CC=clang++
+
+main: main.o
+	$(CC) -Llib/x86_64/ main.o $(FILAMENT_LIBS) -o main
+
+main.o: main.cpp
+	$(CC) -Iinclude/ -std=c++20 -c main.cpp
+
+clean:
+	rm -f main main.o
+
+.PHONY: clean
+
+

Windows

+

Note that the static libraries distributed for Windows include several +variants: mt, md, mtd, mdd. These correspond to the run-time library +flags +/MT, /MD, /MTd, and /MDd, respectively. Here we use the mt variant.

+

When building Filamat from source, the USE_STATIC_CRT CMake option can be +used to change the run-time library version.

+
FILAMENT_LIBS=lib/x86_64/mt/filamat.lib lib/x86_64/mt/filabridge.lib lib/x86_64/mt/shaders.lib \
+              lib/x86_64/mt/utils.lib lib/x86_64/mt/smol-v.lib
+CC=clang-cl.exe
+
+main.exe: main.obj
+	$(CC) main.obj $(FILAMENT_LIBS) gdi32.lib user32.lib opengl32.lib
+
+main.obj: main.cpp
+	$(CC) /MT /Iinclude/ /std:c++20 /c main.cpp
+
+clean:
+	del main.exe main.obj
+
+.PHONY: clean
+
+

Compiling

+

You should be able to invoke make and run the executable successfully:

+
$ make
+$ ./main
+Success!
+
+

On Windows, you'll need to open up a Visual Studio Native Tools Command Prompt +and invoke nmake instead of make.

+

Using the Material with Filament

+

For simplicity, this demo doesn't do anything useful with the built material package. To use the +material with Filament, pass the material package's data into a Filament Material builder:

+
    Package package = builder.build();
+    filament::Material* myMaterial = Material::Builder()
+        .package(package.getData(), package.getSize())
+        .build(*engine);
+
+

Note that this will require linking against Filament's libraries in +addition to Filamat's.

+

Filamat Lite

+

The filamat_lite library is interchangeable with filamat, with a few caveats:

+
    +
  1. Material compilation is only supported for the OpenGL backend.
  2. +
  3. No shader-level optimization is performed.
  4. +
  5. GLSL correctness is not checked.
  6. +
+

In addition, filamat_lite only performs a simple text match to determine which properties on the +MaterialInputs structure are set. The material input variable must also always be refered to by +the name material.

+
void anotherFunction(inout MaterialInputs m) {
+    // Incorrect! The MaterialInputs is being referred to by the name "m".
+    m.metallic = 0.0;
+}
+
+void aFunction(inout MaterialInputs material) {
+    // Works, but only because the variable name "material" is used.
+    material.reflectance = 0.5;
+}
+
+// The MaterialInputs variable must be named material.
+void material(inout MaterialInputs material) {
+    prepareMaterial(material);
+
+    // Good.
+    material.roughness = materialParams.roughness;
+    material.baseColor.rgb = vec3(1.0, 0.0, 1.0);
+
+    aFunction(material);
+    anotherFunction(material);
+}
+
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/filamesh.html b/docs/dup/filamesh.html new file mode 100644 index 0000000000..e0d0a831b1 --- /dev/null +++ b/docs/dup/filamesh.html @@ -0,0 +1,464 @@ + + + + + + filamesh - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Filamesh

+

filamesh converts any mesh file supported by assimp (as configured in this source tree) into a +custom binary file format. The goal of this binary file format is to allow test applications to +easily and quickly load meshes.

+

The source mesh must have at least one set of UV coordinates.

+

The destination mesh will contain vertex positions, one set of UV coordinates and per-vertex +tangents, bitangents and normals.

+

The destination mesh is made of a single vertex buffer and a single index buffer. Mesh parts are +identified by an offset and count in the index buffer. Each part can have its own material.

+

Usage

+
filamesh source_mesh destination_mesh
+
+

Format

+

Note: the UV1 attribute cannot be used in interleaved mode

+
+

Note +If you use the hex editor for macOS called Hex Fiend, you can use the +template found in ide/hexfiend/Templates to inspect filamesh files.

+
+ +
char[8] : magic identifier "FILAMESH"
+uint32  : version number
+uint32  : number of parts (sub-meshes or draw calls)
+float3  : center of the total bounding box (AABB)
+float3  : half extent of the total bounding box (AABB)
+uint32  : flags (see below)
+uint32  : offset of the position attribute
+uint32  : stride of the position attribute
+uint32  : offset of the tangents attribute
+uint32  : stride of the tangents attribute
+uint32  : offset of the color attribute
+uint32  : stride of the color attribute
+uint32  : offset of the UV0 attribute
+uint32  : stride of the UV0 attribute
+uint32  : offset of the UV1 attribute (0xffffffff if UV1 is not present)
+uint32  : stride of the UV1 attribute (0xffffffff if UV1 is not present)
+uint32  : total number of vertices
+uint32  : size in bytes occupied by the (compressed) vertices
+uint32  : 0 if indices are stored as uint32, 1 if stored as uint16
+uint32  : total number of indices
+uint32  : size in bytes occupied by the (compressed) indices
+
+

The flags field contains the following bits:

+
    +
  • Bit 0: Specifies that vertex attributes are interleaved.
  • +
  • Bit 1: UV's are 16-bit integers normalized into [-1, +1] rather than half-floats.
  • +
  • Bit 2: Vertex and index data are compressed using zeux/meshoptimizer.
  • +
+

Vertex data

+
char*   : non-interleaved:
+              with n = number of vertices
+              n * half4:  XYZ positions, W set to 1.0
+              n * short4: tangent, bitangent and normal as a quaternion (snorm unsigned short)
+              n * ubyte4: color
+              n * half2:  UV texture coordinates
+              n * half2:  UV texture coordinates (if UV1 offset and stride != 0xffffffff)
+          interleaved:
+              for each vertex:
+                   half4:  XYZ position, W set to 1.0
+                   short4: tangent, bitangent and normal as a quaternion (snorm unsigned short)
+                   ubyte4: color
+                   half2:  UV texture coordinates
+
+

Index data

+
char*   : each index is a uint32 or uint16 (see header)
+
+

Parts

+
for each part:
+    uint32: offset of the first index in the index buffer
+    uint32: number of indices that compose this part
+    uint32: min index referenced by this part (glDrawRangeElements)
+    uint32: max index referenced by this part (glDrawRangeElements)
+    uint32: material ID (index in list of materials)
+    float3: center of the part's bounding box (AABB)
+    float3: half extent of the part's bounding box (AABB)
+
+

Materials

+
uint32  : number of materials
+for each material:
+    uint32: length in bytes of the material name's string (not counting terminating \0)
+    char* : name of the material (null terminated)
+
+

Example

+
struct Mesh {
+    utils::Entity renderable;
+    VertexBuffer* vertexBuffer = nullptr;
+    IndexBuffer* indexBuffer = nullptr;
+};
+
+struct Header {
+    uint32_t version;
+    uint32_t parts;
+    Box      aabb;
+    uint32_t flags;
+    uint32_t offsetPosition;
+    uint32_t stridePosition;
+    uint32_t offsetTangents;
+    uint32_t strideTangents;
+    uint32_t offsetColor;
+    uint32_t strideColor;
+    uint32_t offsetUV0;
+    uint32_t strideUV0;
+    uint32_t offsetUV1;
+    uint32_t strideUV1;
+    uint32_t vertexCount;
+    uint32_t vertexSize;
+    uint32_t indexType;
+    uint32_t indexCount;
+    uint32_t indexSize;
+};
+
+struct Vertex {
+    half4  position;
+    short4 tangents;
+    ubyte4 color;
+    short2 uv0; // either half-float or snorm int16
+};
+
+struct Part {
+    uint32_t offset;
+    uint32_t indexCount;
+    uint32_t minIndex;
+    uint32_t maxIndex;
+    uint32_t materialID;
+    Box      aabb;
+};
+
+static size_t fileSize(int fd) {
+    size_t filesize;
+    filesize = (size_t) lseek(fd, 0, SEEK_END);
+    lseek(fd, 0, SEEK_SET);
+    return filesize;
+}
+
+Mesh loadMeshFromFile(filament::Engine* engine, const utils::Path& path,
+        const std::map<std::string, filament::MaterialInstance*>& materials) {
+
+    Mesh mesh;
+
+    int fd = open(path.c_str(), O_RDONLY);
+
+    size_t size = fileSize(fd);
+    char* data = (char*) mmap(0, size, PROT_READ, MAP_PRIVATE, fd, 0);
+
+    if (data) {
+        char *p = data;
+
+        char magic[9];
+        memcpy(magic, (const char*) p, sizeof(char) * 8);
+        magic[8] = '\0';
+        p += sizeof(char) * 8;
+
+        if (!strcmp("FILAMESH", magic)) {
+            Header* header = (Header*) p;
+            p += sizeof(Header);
+
+            char* vertexData = p;
+            p += header->vertexSize;
+
+            char* indices = p;
+            p += header->indexSize;
+
+            Part* parts = (Part*) p;
+            p += header->parts * sizeof(Part);
+
+            uint32_t materialCount = (uint32_t) *p;
+            p += sizeof(uint32_t);
+
+            std::vector<std::string> partsMaterial;
+            partsMaterial.resize(materialCount);
+
+            for (size_t i = 0; i < materialCount; i++) {
+                uint32_t nameLength = (uint32_t) *p;
+                p += sizeof(uint32_t);
+
+                partsMaterial[i] = p;
+                p += nameLength + 1; // null terminated
+            }
+
+            mesh.indexBuffer = IndexBuffer::Builder()
+                    .indexCount(header->indexCount)
+                    .bufferType(header->indexType ? IndexBuffer::IndexType::USHORT
+                                                  : IndexBuffer::IndexType::UINT)
+                    .build(*engine);
+
+            mesh.indexBuffer->setBuffer(*engine,
+                    IndexBuffer::BufferDescriptor(indices, header->indexSize));
+
+            const uint32_t FLAG_SNORM16_UV = 0x2;
+
+            VertexBuffer::AttributeType::HALF2 uvType = VertexBuffer::AttributeType::HALF2;
+            if (header->flags & FLAG_SNORM16_UV) {
+                uvType = VertexBuffer::AttributeType::SHORT2;
+            }
+            bool uvNormalized = header->flags & FLAG_SNORM16_UV;
+
+            VertexBuffer::Builder vbb;
+            vbb.vertexCount(header->vertexCount)
+                .bufferCount(1)
+                .normalized(VertexAttribute::TANGENTS)
+                .normalized(VertexAttribute::COLOR)
+                .attribute(VertexAttribute::POSITION, 0, VertexBuffer::AttributeType::HALF4,
+                        header->offsetPosition, uint8_t(header->stridePosition))
+                .attribute(VertexAttribute::TANGENTS, 0, VertexBuffer::AttributeType::SHORT4,
+                        header->offsetTangents, uint8_t(header->strideTangents))
+                .attribute(VertexAttribute::COLOR,    0, VertexBuffer::AttributeType::UBYTE4,
+                        header->offsetColor, uint8_t(header->strideColor))
+                .attribute(VertexAttribute::UV0,      0, uvType,
+                        header->offsetUV0, uint8_t(header->strideUV0))
+                .normalized(VertexAttribute::UV0, uvNormalized);
+            }
+
+            if (header->offsetUV1 != std::numeric_limits<uint32_t>::max() &&
+                    header->strideUV1 != std::numeric_limits<uint32_t>::max()) {
+                vbb
+                    .attribute(VertexAttribute::UV1, 0, uvType,
+                            header->offsetUV1, uint8_t(header->strideUV1))
+                   .normalized(VertexAttribute::UV1, uvNormalized);
+            }
+
+            mesh.vertexBuffer = vbb.build(*engine);
+
+            VertexBuffer::BufferDescriptor buffer(vertexData, header->vertexSize);
+            mesh.vertexBuffer->setBufferAt(*engine, 0, std::move(buffer));
+
+            RenderableManager::Builder builder(header->parts);
+            builder.boundingBox(header->aabb);
+
+            for (size_t i = 0; i < header->parts; i++) {
+                builder.geometry(i, RenderableManager::PrimitiveType::TRIANGLES,
+                        mesh.vertexBuffer, mesh.indexBuffer, parts[i].offset,
+                        parts[i].minIndex, parts[i].maxIndex, parts[i].indexCount);
+                auto m = materials.find(partsMaterial[i]);
+                if (m != materials.end()) {
+                    builder.material(i, m->second);
+                } else {
+                    builder.material(i, materials.at("DefaultMaterial"));
+                }
+            }
+
+            mesh.renderable = utils::EntityManager::get().create();
+            builder.build(*engine, mesh.renderable);
+        }
+
+        Fence::waitAndDestroy(engine->createFence());
+        munmap(data, size);
+    }
+    close(fd);
+
+    return mesh;
+}
+
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/gltfio.html b/docs/dup/gltfio.html new file mode 100644 index 0000000000..a9f3dda510 --- /dev/null +++ b/docs/dup/gltfio.html @@ -0,0 +1,239 @@ + + + + + + gltfio - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Description

+

gltfio is a loader library that consumes gltf or glb content and produces Filament +objects. For usage details, see the docstring for AssetLoader.

+

gltfio has two plug-in interfaces, TextureProvider and MaterialProvider. Filament ships with +several ready-to-go implementations described below.

+
    +
  • MaterialProvider creates Filament materials in response to certain glTF requirements. +
      +
    • UbershaderProvider loads pre-built materials.
    • +
    • JitShaderProvider builds materials at run time using the filamat library.
    • +
    +
  • +
  • TextureProvider creates and populates Filament Texture objects. +
      +
    • StbProvider uses the STB library to read PNG and JPEG files.
    • +
    • Ktx2Provider uses the BasisU library to read KTX2 files.
    • +
    +
  • +
+

UbershaderProvider

+

UbershaderProvider is a ready-to-go implementation of the MaterialProvider interface that should +be used in applications that need fast startup times. There is no material compilation that +occurs at run time, but the shaders might be relatively large and complex.

+

At load time, the ubershader loader consumes an ubershader archive which is a precompiled set of +materials bundled with formal descriptions of the glTF features that they support.

+

The uberz command line tool consumes a list of .spec and .filamat files and produces a single +.uberz file. For details on these two file formats, see the README in libs/uberz.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/iblprefilter.html b/docs/dup/iblprefilter.html new file mode 100644 index 0000000000..1ff0f85a7c --- /dev/null +++ b/docs/dup/iblprefilter.html @@ -0,0 +1,246 @@ + + + + + + iblprefilter - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

IBL Prefilter

+

This library can be used to generate the reflections texture used by filament's IndirectLight +class. It is similar to the cmgen tool except that all computations are performed on the GPU and +are therefore significantly faster. cmgen however offers more functionalities.

+

IBL Prefilter is designed entirely as a client of filament, that is, it only uses filament +public APIs.

+

Library and headers

+

The library is called libfilament-iblprefilter.a and its public headers can be found in +<filament-iblprefilter/*.h>.

+

Performance

+

Expect a total processing time of about 100ms to 300ms for a 5-levels 256 x 256 cubemap with 1024 +samples.

+

Example

+
#include <filament/Engine.h>
+#include <filament-iblprefilter/IBLPrefilterContext.h>
+
+using namespace filament;
+
+Engine* engine = Engine::create();
+
+// create an IBLPrefilterContext, keep it around if several cubemap will be processed.
+IBLPrefilterContext context(engine);
+
+// create the specular (reflections) filter. This operation generates the kernel, so it's important
+// to keep it around if it will be reused for several cubemaps.
+IBLPrefilterContext::SpecularFilter filter(context);
+
+// launch the heaver computation. Expect 100-100ms on the GPU.
+Texture* texture = filter(environment_cubemap);
+
+IndirectLight* indirectLight = IndirectLight::Builder()
+    .reflections(texture)
+    .build(engine);
+
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/intro.html b/docs/dup/intro.html new file mode 100644 index 0000000000..6ae276da7b --- /dev/null +++ b/docs/dup/intro.html @@ -0,0 +1,564 @@ + + + + + + Introduction - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Filament

+

Android Build Status +iOS Build Status +Linux Build Status +macOS Build Status +Windows Build Status +Web Build Status

+

Filament is a real-time physically based rendering engine for Android, iOS, Linux, macOS, Windows, +and WebGL. It is designed to be as small as possible and as efficient as possible on Android.

+

Download

+

Download Filament releases to access stable builds. +Filament release archives contains host-side tools that are required to generate assets.

+

Make sure you always use tools from the same release as the runtime library. This is particularly +important for matc (material compiler).

+

If you'd rather build Filament yourself, please refer to our build manual.

+

Android

+

Android projects can simply declare Filament libraries as Maven dependencies:

+
repositories {
+    // ...
+    mavenCentral()
+}
+
+dependencies {
+    implementation 'com.google.android.filament:filament-android:1.65.0'
+}
+
+

Here are all the libraries available in the group com.google.android.filament:

+
+ + + + + + +
ArtifactDescription
filament-androidThe Filament rendering engine itself.
filament-android-debugDebug version of filament-android.
gltfio-androidA glTF 2.0 loader for Filament, depends on filament-android.
filament-utils-androidKTX loading, Kotlin math, and camera utilities, depends on gltfio-android.
filamat-androidA runtime material builder/compiler. This library is large but contains a full shader compiler/validator/optimizer and supports both OpenGL and Vulkan.
filamat-android-liteA much smaller alternative to filamat-android that can only generate OpenGL shaders. It does not provide validation or optimizations.
+
+

iOS

+

iOS projects can use CocoaPods to install the latest release:

+
pod 'Filament', '~> 1.65.0'
+
+

Documentation

+
    +
  • Filament, an in-depth explanation of +real-time physically based rendering, the graphics capabilities and implementation of Filament. +This document explains the math and reasoning behind most of our decisions. This document is a +good introduction to PBR for graphics programmers.
  • +
  • Materials, the full reference +documentation for our material system. This document explains our different material models, how +to use the material compiler matc and how to write custom materials.
  • +
  • Material Properties, a reference +sheet for the standard material model.
  • +
+

Examples

+

Night scene +Night scene +Materials +Materials +Helmet +Screen-space refraction

+

Features

+

APIs

+
    +
  • Native C++ API for Android, iOS, Linux, macOS and Windows
  • +
  • Java/JNI API for Android
  • +
  • JavaScript API
  • +
+

Backends

+
    +
  • OpenGL 4.1+ for Linux, macOS and Windows
  • +
  • OpenGL ES 3.0+ for Android and iOS
  • +
  • Metal for macOS and iOS
  • +
  • Vulkan 1.0 for Android, Linux, macOS, and Windows
  • +
  • WebGL 2.0 for all platforms
  • +
+

Rendering

+
    +
  • Clustered forward renderer
  • +
  • Cook-Torrance microfacet specular BRDF
  • +
  • Lambertian diffuse BRDF
  • +
  • Custom lighting/surface shading
  • +
  • HDR/linear lighting
  • +
  • Metallic workflow
  • +
  • Clear coat
  • +
  • Anisotropic lighting
  • +
  • Approximated translucent (subsurface) materials
  • +
  • Cloth/fabric/sheen shading
  • +
  • Normal mapping & ambient occlusion mapping
  • +
  • Image-based lighting
  • +
  • Physically-based camera (shutter speed, sensitivity and aperture)
  • +
  • Physical light units
  • +
  • Point lights, spot lights, and directional light
  • +
  • Specular anti-aliasing
  • +
  • Point, spot, and directional light shadows
  • +
  • Cascaded shadows
  • +
  • EVSM, PCSS, DPCF, or PCF shadows
  • +
  • Transparent shadows
  • +
  • Contact shadows
  • +
  • Screen-space ambient occlusion
  • +
  • Screen-space reflections
  • +
  • Screen-space refraction
  • +
  • Global fog
  • +
  • Dynamic resolution (with support for AMD FidelityFX FSR)
  • +
+

Post processing

+
    +
  • HDR bloom
  • +
  • Depth of field bokeh
  • +
  • Multiple tone mappers: generic (customizable), ACES, filmic, etc.
  • +
  • Color and tone management: luminance scaling, gamut mapping
  • +
  • Color grading: exposure, night adaptation, white balance, channel mixer, +shadows/mid-tones/highlights, ASC CDL, contrast, saturation, etc.
  • +
  • TAA, FXAA, MSAA
  • +
  • Screen-space lens flares
  • +
+

glTF 2.0

+
    +
  • +

    Encodings

    +
      +
    • +Embeded
    • +
    • +Binary
    • +
    +
  • +
  • +

    Primitive Types

    +
      +
    • +Points
    • +
    • +Lines
    • +
    • +Line Loop
    • +
    • +Line Strip
    • +
    • +Triangles
    • +
    • +Triangle Strip
    • +
    • +Triangle Fan
    • +
    +
  • +
  • +

    Animation

    +
      +
    • +Transform animation
    • +
    • +Linear interpolation
    • +
    • +Morph animation +
        +
      • +Sparse accessor
      • +
      +
    • +
    • +Skin animation
    • +
    • +Joint animation
    • +
    +
  • +
  • +

    Extensions

    +
      +
    • +KHR_draco_mesh_compression
    • +
    • +KHR_lights_punctual
    • +
    • +KHR_materials_clearcoat
    • +
    • +KHR_materials_emissive_strength
    • +
    • +KHR_materials_ior
    • +
    • +KHR_materials_pbrSpecularGlossiness
    • +
    • +KHR_materials_sheen
    • +
    • +KHR_materials_transmission
    • +
    • +KHR_materials_unlit
    • +
    • +KHR_materials_variants
    • +
    • +KHR_materials_volume
    • +
    • +KHR_materials_specular
    • +
    • +KHR_mesh_quantization
    • +
    • +KHR_texture_basisu
    • +
    • +KHR_texture_transform
    • +
    • +EXT_meshopt_compression
    • +
    +
  • +
+

Rendering with Filament

+

Native Linux, macOS and Windows

+

You must create an Engine, a Renderer and a SwapChain. The SwapChain is created from a +native window pointer (an NSView on macOS or a HWND on Windows for instance):

+
Engine* engine = Engine::create();
+SwapChain* swapChain = engine->createSwapChain(nativeWindow);
+Renderer* renderer = engine->createRenderer();
+
+

To render a frame you must then create a View, a Scene and a Camera:

+
Camera* camera = engine->createCamera(EntityManager::get().create());
+View* view = engine->createView();
+Scene* scene = engine->createScene();
+
+view->setCamera(camera);
+view->setScene(scene);
+
+

Renderables are added to the scene:

+
Entity renderable = EntityManager::get().create();
+// build a quad
+RenderableManager::Builder(1)
+        .boundingBox({{ -1, -1, -1 }, { 1, 1, 1 }})
+        .material(0, materialInstance)
+        .geometry(0, RenderableManager::PrimitiveType::TRIANGLES, vertexBuffer, indexBuffer, 0, 6)
+        .culling(false)
+        .build(*engine, renderable);
+scene->addEntity(renderable);
+
+

The material instance is obtained from a material, itself loaded from a binary blob generated +by matc:

+
Material* material = Material::Builder()
+        .package((void*) BAKED_MATERIAL_PACKAGE, sizeof(BAKED_MATERIAL_PACKAGE))
+        .build(*engine);
+MaterialInstance* materialInstance = material->createInstance();
+
+

To learn more about materials and matc, please refer to the +materials documentation.

+

To render, simply pass the View to the Renderer:

+
// beginFrame() returns false if we need to skip a frame
+if (renderer->beginFrame(swapChain)) {
+    // for each View
+    renderer->render(view);
+    renderer->endFrame();
+}
+
+

For complete examples of Linux, macOS and Windows Filament applications, look at the source files +in the samples/ directory. These samples are all based on libs/filamentapp/ which contains the +code that creates a native window with SDL2 and initializes the Filament engine, renderer and views.

+

For more information on how to prepare environment maps for image-based lighting please refer to +BUILDING.md.

+

Android

+

See android/samples for examples of how to use Filament on Android.

+

You must always first initialize Filament by calling Filament.init().

+

Rendering with Filament on Android is similar to rendering from native code (the APIs are largely +the same across languages). You can render into a Surface by passing a Surface to the +createSwapChain method. This allows you to render to a SurfaceTexture, a TextureView or +a SurfaceView. To make things easier we provide an Android specific API called UiHelper in the +package com.google.android.filament.android. All you need to do is set a render callback on the +helper and attach your SurfaceView or TextureView to it. You are still responsible for +creating the swap chain in the onNativeWindowChanged() callback.

+

iOS

+

Filament is supported on iOS 11.0 and above. See ios/samples for examples of using Filament on +iOS.

+

Filament on iOS is largely the same as native rendering with C++. A CAEAGLLayer or CAMetalLayer +is passed to the createSwapChain method. Filament for iOS supports both Metal (preferred) and +OpenGL ES.

+

Assets

+

To get started you can use the textures and environment maps found respectively in +third_party/textures and third_party/environments. These assets are under CC0 license. Please +refer to their respective URL.txt files to know more about the original authors.

+

Environments must be pre-processed using +cmgen or +using the libiblprefilter library.

+

How to make contributions

+

Please read and follow the steps in CONTRIBUTING.md. Make sure you are +familiar with the code style.

+

Directory structure

+

This repository not only contains the core Filament engine, but also its supporting libraries +and tools.

+
    +
  • android: Android libraries and projects +
      +
    • filamat-android: Filament material generation library (AAR) for Android
    • +
    • filament-android: Filament library (AAR) for Android
    • +
    • filament-utils-android: Extra utilities (KTX loader, math types, etc.)
    • +
    • gltfio-android: Filament glTF loading library (AAR) for Android
    • +
    • samples: Android-specific Filament samples
    • +
    +
  • +
  • art: Source for various artworks (logos, PDF manuals, etc.)
  • +
  • assets: 3D assets to use with sample applications
  • +
  • build: CMake build scripts
  • +
  • docs: Documentation +
      +
    • math: Mathematica notebooks used to explore BRDFs, equations, etc.
    • +
    +
  • +
  • filament: Filament rendering engine (minimal dependencies) +
      +
    • backend: Rendering backends/drivers (Vulkan, Metal, OpenGL/ES)
    • +
    +
  • +
  • ide: Configuration files for IDEs (CLion, etc.)
  • +
  • ios: Sample projects for iOS
  • +
  • libs: Libraries +
      +
    • bluegl: OpenGL bindings for macOS, Linux and Windows
    • +
    • bluevk: Vulkan bindings for macOS, Linux, Windows and Android
    • +
    • camutils: Camera manipulation utilities
    • +
    • filabridge: Library shared by the Filament engine and host tools
    • +
    • filaflat: Serialization/deserialization library used for materials
    • +
    • filagui: Helper library for Dear ImGui
    • +
    • filamat: Material generation library
    • +
    • filamentapp: SDL2 skeleton to build sample apps
    • +
    • filameshio: Tiny filamesh parsing library (see also tools/filamesh)
    • +
    • geometry: Mesh-related utilities
    • +
    • gltfio: Loader for glTF 2.0
    • +
    • ibl: IBL generation tools
    • +
    • image: Image filtering and simple transforms
    • +
    • imageio: Image file reading / writing, only intended for internal use
    • +
    • matdbg: DebugServer for inspecting shaders at run-time (debug builds only)
    • +
    • math: Math library
    • +
    • mathio: Math types support for output streams
    • +
    • utils: Utility library (threads, memory, data structures, etc.)
    • +
    • viewer: glTF viewer library (requires gltfio)
    • +
    +
  • +
  • samples: Sample desktop applications
  • +
  • shaders: Shaders used by filamat and matc
  • +
  • third_party: External libraries and assets +
      +
    • environments: Environment maps under CC0 license that can be used with cmgen
    • +
    • models: Models under permissive licenses
    • +
    • textures: Textures under CC0 license
    • +
    +
  • +
  • tools: Host tools +
      +
    • cmgen: Image-based lighting asset generator
    • +
    • filamesh: Mesh converter
    • +
    • glslminifier: Minifies GLSL source code
    • +
    • matc: Material compiler
    • +
    • filament-matp: Material parser
    • +
    • matinfo Displays information about materials compiled with matc
    • +
    • mipgen Generates a series of miplevels from a source image
    • +
    • normal-blending: Tool to blend normal maps
    • +
    • resgen Aggregates binary blobs into embeddable resources
    • +
    • roughness-prefilter: Pre-filters a roughness map from a normal map to reduce aliasing
    • +
    • specular-color: Computes the specular color of conductors based on spectral data
    • +
    +
  • +
  • web: JavaScript bindings, documentation, and samples
  • +
+

License

+

Please see LICENSE.

+

Disclaimer

+

This is not an officially supported Google product.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/matdbg.html b/docs/dup/matdbg.html new file mode 100644 index 0000000000..0845035e77 --- /dev/null +++ b/docs/dup/matdbg.html @@ -0,0 +1,397 @@ + + + + + + matdbg - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

matdbg

+
    +
  1. Capabilities
  2. +
  3. Setup for Desktop
  4. +
  5. Setup for Android
  6. +
  7. Debugger Usage
  8. +
  9. Architecture Overview
  10. +
  11. C++ Server
  12. +
  13. JavaScript Client
  14. +
  15. HTTP Requests
  16. +
  17. WebSocket Messages
  18. +
  19. Wish List
  20. +
  21. Screenshot
  22. +
  23. Material Chunks
  24. +
+

Capabilities

+

matdbg is a library and web application that enables debugging and live-editing of Filament shaders. +At the time of this writing, the following capabilities are supported.

+
    +
  • OpenGL: Editing GLSL
  • +
  • Metal: Editing MSL
  • +
  • Vulkan: Editing transpiled GLSL, displaying disassembled SPIR-V
  • +
  • WebGPU: Editing WGSL
  • +
+

Note that a given material can be built with multiple backends, even though only one backend +is active in a particular session. For example, if the current app is using Vulkan, it is still +possible to inspect the Metal shaders, as long as the material has been built with Metal support +included.

+

Setup for Desktop

+

When using the easy build script, include the -d argument. For example:

+
./build.sh -fd debug gltf_viewer
+
+

The d enables a CMake option called FILAMENT_ENABLE_MATDBG and the f ensures that CMake gets +re-run so that the option is honored.

+

Next, set an environment variable as follows. In Windows, use set instead of export.

+
export FILAMENT_MATDBG_PORT=8080
+
+

Next, launch any app that links against a debug build of a Filament and point your web browser to +http://localhost:8080. Skip ahead to Debugger Usage.

+

Setup for Android

+

Rebuild Filament for Android after enabling a CMake option called FILAMENT_ENABLE_MATDBG. Note that +CMake is invoked from several places for Android (both gradle and our easy build script), so one +pragmatic and reliable way of doing this is to simply hack CMakeLists.txt and +filament-android/CMakeLists.txt by unconditionally setting FILAMENT_ENABLE_MATDBG to ON.

+

After rebuilding Filament with the option enabled, ensure that internet permissions are enabled in +your app by adding the following into your manifest as a child of the <manifest> element.

+
<uses-permission android:name="android.permission.INTERNET" />
+
+

Now launch your app as usual. The Filament Engine sets up a server that is hardcoded to listen to +port 8081. Next, you will need to forward your device's TCP port 8081 to your host port of choice. +For example, to forward the matdbg server on your device to port 8081 on your host machine, do the +following:

+
adb forward tcp:8081 tcp:8081
+
+

This lets you go to http://localhost:8081 in Chrome on your host machine.

+

Note that we generally use a release build of Filament when running on Android, so the shaders +are optimized and very unreadable. This can be avoided by modifying the build such that -g is +passed to matc even in release builds.

+

Debugger Usage

+

After opening the matdbg page in your browser, the usual first step is to select a material in the +upper-left pane. Sometimes you might need force your app to redraw (e.g. by resizing the window) in +order make the materials selectable.

+

The next step is to select an active (boldface) shader variant in the lower-left pane. This allows +you to view the GLSL, MSL, and SPIR-V code that was generated by matc or filamat.

+

In the sidebar, inactive shader variants have a disabled appearance, but they can still be examined +in the shader editor. The active status of each shader program is refreshed every second.

+

You can also make modifications to GLSL or MSL, so long as the shader inputs and uniforms remain +intact. After making an edit, click the [rebuild] button in the header. Note that your edits will +be lost after closing the web page.

+

Keyboard Shortcuts

+

To save an edit, press Cmd+S (Ctrl+S on Linux/Windows) as an alternative to clicking +[rebuild].

+

If the editor has focus, you can navigate between materials by holding Shift+Ctrl while +pressing the up or down arrow. Navigation between variants is similar, just use left / right instead +of up / down.

+

Architecture Overview

+

The matdbg library has two parts: a C++ server and a JavaScript client. The C++ server is +responsible for instancing a civetweb context that handles HTTP and WebSocket requests. The +JavaScript client is a small web app that contains a view into an in-browser database of materials.

+

The WebSocket server receives push-style notifications from the client (such as edits) while +the HTTP server responds to material queries using simple JSON messages.

+

When a new WebSocket connection is established, the client asks the server for a list of materials +in order to populate its in-browser database. If the connection is lost (e.g. if the app crashes), +then the database stays intact and the web app is still functional. If a new Filament app is +launched, the client inserts entries into its database rather than replacing the existing set.

+

The material database is cleared only when the web page is manually refreshed by the user.

+

C++ Server

+

The civetweb server is wrapped by our DebugServer class, whose public interface is comprised of a +couple methods that are called from the Filament engine:

+
    +
  • addMaterial Notifies the debugger that the given material package is being loaded into the +engine.
  • +
  • setEditCallback Sets up a callback that allows the Filament engine to listen for shader edits.
  • +
  • setQueryCallback Sets up a callback that allows the debugger to ask for current information.
  • +
+

JavaScript Client

+

The web app is written in simple, modern JavaScript. It uses third-party libraries +which are fetched from a CDN using <script>. This allows us to avoid adding them to our git repo, +and leads to good caching behavior.

+
    +
  • lit-html A small wrapper around web-components for fast, iterative development.
  • +
  • monaco The engine behind Visual Studio Code. +
      +
    • We've configured this for C++ for somewhat reasonable syntax highlighting.
    • +
    • If desired we could extend the editor to better handle GLSL and SPIR-V.
    • +
    +
  • +
+

All the source code for our web app is contained in a single file (app.js), and there is a +corresponding api.js to handle the protocol between the server (the running filament app) +the client (the browser).

+

The web app basically provides a view over a pseudo-database which is a just a global variable +that holds a dictionary that maps from material id's to objects that conform to the JSON described +below.

+

HTTP requests

+

The server responds to the following GET requests by returning a JSON blob. The {id} in these +requests is a concept specific to matdbg (not Filament) which is an 8-digit hex string that hashes +the entire binary content of the material package.

+
+

/api/matids

+

Returns an array containing the id for each known material. Example:

+
["e4c41141", "44ae2b62", "9dab8a03"]
+
+
+

/api/materials

+

Returns an array with all information (except shader source) for all known materials. Example:

+
[{
+    "matid": "e4c41141",
+    "name": "uiBlit",
+    "version": 4,
+    "shading": { "model": "unlit", "vertex_domain": "object", ... },
+    "raster":  { "blending": "transparent", "color_write": "true", ... },
+    "opengl": [
+        { "index": " 0", "shaderModel": "gl41", "pipelineStage": "vertex  ", "variantString": "", "variant": 0 },
+        { "index": " 1", "shaderModel": "gl41", "pipelineStage": "fragment", "variantString": "", "variant": 0 },
+    ],
+    "vulkan": [],
+    "metal": [],
+    "required_attributes": ["position", "color", "uv0"]
+},
+{
+    "matid": "44ae2b62",
+    ...
+}]
+
+

Some of the returned data may seem redundant (e.g. the index and variantString fields) but +these allow the client to be very simple by passing the raw JSON into [mustache][4] templates. +Moreover it helps prevent duplication of knowledge between C++ and JavaScript.

+

This format of this message is also used for the in-browser "database" of materials.

+
+

/api/material?matid={id}

+

Returns all information (except shader source) for a specific known material. The JSON response +is equivalent to one of the items in the top-level array in /api/materials.

+
+

/api/active

+

Returns an object that maps from material ids to their active shader variants. Example:

+
{"b38d4ad0": ["opengl", 5] , "44ae2b62": ["opengl", 1, 4] }
+
+

Each numeric element in the list is a variant mask. For example, at the time of this writing, +Filament has 7-bit mask, so each number in the list is between 0 and 127.

+
+

/api/shader?matid={id}&type=[glsl|spirv|msl]&[glindex|vkindex|metalindex]={index}

+

Returns the entire shader code for the given variant. This is the only HTTP request that returns +text instead of JSON.

+

The type field in the request selects the desired shading language, not the backend. For example, +for Vulkan it can select between SPIR-V or decompiled GLSL. Note that the original GLSL that was +used to create the SPIR-V is not available.

+
+

Wish List

+
    +
  • Allow editing of the original GLSL, perhaps by enhancing the -g option in matc and adding new chunk types.
  • +
  • Expose the entire engine.debug struct in the web UI.
  • +
  • When shader errors occur, send them back over the wire to the web client.
  • +
  • The sidebar in the web app is not resizeable.
  • +
  • For the material ids, SHA-1 would be better than murmur since the latter can easily have collisions.
  • +
  • It would be easy to add diff decorations to the editor in our onEdit function: +
      +
    1. Examine "changes" (IModelContentChange) to get a set of line numbers.
    2. +
    3. shader.decorations = gEditor.deltaDecorations(shader.decorations, ...)
    4. +
    5. See these monaco docs.
    6. +
    +
  • +
+

Screenshot

+ + +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/matinfo.html b/docs/dup/matinfo.html new file mode 100644 index 0000000000..68438d4f98 --- /dev/null +++ b/docs/dup/matinfo.html @@ -0,0 +1,218 @@ + + + + + + matinfo - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Matinfo

+

matinfo lists the content of a compiled material as output by matc. This tool is meant to be +used for debug purpose only.

+

Usage

+
matinfo [options] <material file>
+
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/mipgen.html b/docs/dup/mipgen.html new file mode 100644 index 0000000000..06e0348759 --- /dev/null +++ b/docs/dup/mipgen.html @@ -0,0 +1,218 @@ + + + + + + mipgen - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

mipgen

+

mipgen generates mipmaps for an image down to the 1x1 level.

+

Usage

+
mipgen [options] <input_file> <output_pattern>
+
+

Run mipgen --help for more information about available options.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/normal_blending.html b/docs/dup/normal_blending.html new file mode 100644 index 0000000000..3ddbaff94d --- /dev/null +++ b/docs/dup/normal_blending.html @@ -0,0 +1,216 @@ + + + + + + normal-blending - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Normal Blending

+

normal_blending is a simple tool that can be used to combine two normal maps in a single texture.

+

This tool uses the blending technique called Reoriented Normal Mapping which offers mathematically +correct results (as opposed to common techniques such as linear or overlay blending).

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/roughness_prefilter.html b/docs/dup/roughness_prefilter.html new file mode 100644 index 0000000000..594534d21f --- /dev/null +++ b/docs/dup/roughness_prefilter.html @@ -0,0 +1,216 @@ + + + + + + roughness-prefilter - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Roughness Prefilter

+

roughness_prefilter is a simple tool that can be used to generate a pre-filtered roughness map +from a normal map. The input roughness can either be a constant or a roughness map. The output can +be used to reduce shading aliasing.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/specular_color.html b/docs/dup/specular_color.html new file mode 100644 index 0000000000..fb1d7be858 --- /dev/null +++ b/docs/dup/specular_color.html @@ -0,0 +1,231 @@ + + + + + + specular-color - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

specular-color

+

specular-color computes the base color of conductors based on spectral data. +The base color is output in linear and sRGB formats. The base color is the reflectance +at normal incidence (0°) and is often noted f0.

+

specular-color can also compute the perceived color of a conductor at another angle, +set to ~82° by default. This value is particularly useful when used in combination with +the Lazanyi-Schlick model to better approximate the behavior of metallic surfaces at +grazing angles. See Hoffman 2019, "Fresnel Equations Considered Harmful".

+

Usage

+
specular-color <spectral data file>
+
+

The spectral data files can be obtained from +Refractive Index.

+

For instance, to compute the base color of gold:

+
specular-color data/gold.txt
+
+

To set the second angle, use -a to specify the angle in degrees:

+
specular-color -a 75 data/gold.txt
+
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/uberz.html b/docs/dup/uberz.html new file mode 100644 index 0000000000..4b2d69f246 --- /dev/null +++ b/docs/dup/uberz.html @@ -0,0 +1,289 @@ + + + + + + uberz - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+ +

Ubershader Archive Files

+

An ubershader archive provides a way to bundle up a set of filamat files along with some metadata +that conveys which glTF features each material can handle. It is a file that has been compressed +with zstd and has an .uberz file extension. In uncompressed form, it has the following layout +(little endian is assumed).

+
[u32] magic identifier: UBER
+[u32] simple (unpartitioned) version number for the archive format
+[u64] number of specs
+[u64] byte offset to SPECS
+SPECS:
+foreach spec {
+    [u8] shading model
+    [u8] blending model
+    [u16] number of flags
+    [u32] size in bytes of the filamat blob
+    [u64] byte offset to FLAGLIST for this spec
+    [u64] byte offset to FILAMAT for this spec
+}
+foreach spec {
+    FLAGLIST:
+    foreach flag {
+        [u64] byte offset to FLAGNAME for this spec/flag pair
+        [u64] flag value: 0 = unsupported, 1 = optional, or 2 = required
+    }
+}
+foreach spec {
+    foreach flag {
+        FLAGNAME:
+        [u8...] flag name, including null terminator
+    }
+}
+foreach spec {
+    FILAMAT:
+    [u8...] filamat blob
+}
+
+

In the above specification, each "offset" is a number of bytes between the top of the file to the +given label. These offsets are 64 bits so that they can be replaced with pointers in a C struct, +which allows the file to be consumed without any parsing. On 32-bit architectures, this still works +because we can simply ignore the unused padding after every pointer.

+

Ubershader Spec Files

+

An ubershader spec file is a simple text file with a .spec extension. It contains a list of +key-value pairs conforming to the following grammar. Each key-value pair is either a feature flag +or a fundamental aspect.

+
    +
  • Each feature flag can be unsupported, required, or optional.
  • +
  • The fundamental aspect of the material cannot be changed, such as the blend mode.
  • +
+
spec = { [ comment | key_value_pair ] , "\n" } ;
+comment = "#" , { any } ;
+key_value_pair = ( fundamental_aspect | feature_flag ) ;
+fundamental_aspect = ( blending | shading ) ;
+feature_flag = identifier , equals , ("unsupported" | "required" | "optional") ;
+blending = "BlendingMode" , equals ,
+    ( "opaque" | "transparent" | "fade" | "add" | "masked" | "multiply" | "screen" ) ;
+shading = "ShadingModel"  , equals ,
+    ( "lit" | "subsurface" | "cloth" | "unlit" | "specularGlossiness") ;
+equals = [ whitespace ] , "=" , [ whitespace ] ;
+any = ? any character other than newline ? ;
+whitespace = ? sequence of tabs and spaces ? ;
+identifier = ? sequence of alphanumeric characters ? ;
+
+

If a fundamental aspect is missing from the spec, then the loader will assume that the spec can +handle all possible values for that aspect. For example, we may wish to override the glTF +blending mode in certain ubershader materials (e.g. materials that support KHR_materials_volume). +These materials should simply omit the BlendingMode line from the spec.

+

If any feature flag is missing from the spec, it implicitly has the value of unsupported. For an +up-to-date list of recognized feature flags, look at the source for UbershaderProvider::getMaterial.

+

If a particular feature flag is set to required for a particular material, then the glTF loader +will bind that material to a given glTF mesh only if that feature is enabled in the mesh.

+

Usually, features are either unsupported or optional. For example, if the ubershader user can +set normalIndex in the material to -1 to signal that they do not have a normal map, then normal +mapping should be specified as an optional feature of the ubershader.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/dup/zbloat.html b/docs/dup/zbloat.html new file mode 100644 index 0000000000..5df3f0dac8 --- /dev/null +++ b/docs/dup/zbloat.html @@ -0,0 +1,230 @@ + + + + + + zbloat - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

zbloat

+

This tool makes it easy to analyze the composition of Android applications that use Filament.

+
    +
  • Filament materials are shown in the treemap if resgen --json was used in the build.
  • +
  • The input path can be a .so or .a file, a folder, or a zip archive (apk or aar).
  • +
  • If the path is a zip or folder, interactively finds the file to analyze.
  • +
  • The generated web report is a self-contained HTML file.
  • +
  • Reports the gzipped size of all Filament materials.
  • +
+

Note that the executable must be built with debugging information.

+

For example, from a mac, you can generate a self-contained HTML file by typing this from the +Filament repo root:

+
./tools/zbloat/zbloat.py ./android/filament-android
+open index.html
+
+

This tool uses Python, nm, and objdump.

+

Linux, macOS, and Docker

+

The nm tool works slightly differently between macOS and Linux, so a Dockerfile is +provided that installs dependencies inside a Linux container. This is also convenient if you +do not have both versions of Python on your system.

+

The easy way to use docker is to invoke the helper bash script. Simply type zbloat.sh [args...] +instead of zbloat.py [args...]. The first time you run it, it will be slow but subsequent times +will be fast.

+

Many thanks to Evan Martin for his interactive treemap widget.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/elasticlunr.min.js b/docs/elasticlunr.min.js new file mode 100644 index 0000000000..94b20dd2ef --- /dev/null +++ b/docs/elasticlunr.min.js @@ -0,0 +1,10 @@ +/** + * elasticlunr - http://weixsong.github.io + * Lightweight full-text search engine in Javascript for browser search and offline search. - 0.9.5 + * + * Copyright (C) 2017 Oliver Nightingale + * Copyright (C) 2017 Wei Song + * MIT Licensed + * @license + */ +!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();o/g,">").replace(/"/g,""").replace(/'/g,"'")}function r(e,...n){var t={};for(const n in e)t[n]=e[n];return n.forEach((function(e){for(const n in e)t[n]=e[n]})),t}function a(e){return e.nodeName.toLowerCase()}var i=Object.freeze({__proto__:null,escapeHTML:t,inherit:r,nodeStream:function(e){var n=[];return function e(t,r){for(var i=t.firstChild;i;i=i.nextSibling)3===i.nodeType?r+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:r,node:i}),r=e(i,r),a(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:r,node:i}));return r}(e,0),n},mergeStreams:function(e,n,r){var i=0,s="",o=[];function l(){return e.length&&n.length?e[0].offset!==n[0].offset?e[0].offset"}function u(e){s+=""}function d(e){("start"===e.event?c:u)(e.node)}for(;e.length||n.length;){var g=l();if(s+=t(r.substring(i,g[0].offset)),i=g[0].offset,g===e){o.reverse().forEach(u);do{d(g.splice(0,1)[0]),g=l()}while(g===e&&g.length&&g[0].offset===i);o.reverse().forEach(c)}else"start"===g[0].event?o.push(g[0].node):o.pop(),d(g.splice(0,1)[0])}return s+t(r.substr(i))}});const s="",o=e=>!!e.kind;class l{constructor(e,n){this.buffer="",this.classPrefix=n.classPrefix,e.walk(this)}addText(e){this.buffer+=t(e)}openNode(e){if(!o(e))return;let n=e.kind;e.sublanguage||(n=`${this.classPrefix}${n}`),this.span(n)}closeNode(e){o(e)&&(this.buffer+=s)}value(){return this.buffer}span(e){this.buffer+=``}}class c{constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){this.top.children.push(e)}openNode(e){const n={kind:e,children:[]};this.add(n),this.stack.push(n)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,n){return"string"==typeof n?e.addText(n):n.children&&(e.openNode(n),n.children.forEach(n=>this._walk(e,n)),e.closeNode(n)),e}static _collapse(e){"string"!=typeof e&&e.children&&(e.children.every(e=>"string"==typeof e)?e.children=[e.children.join("")]:e.children.forEach(e=>{c._collapse(e)}))}}class u extends c{constructor(e){super(),this.options=e}addKeyword(e,n){""!==e&&(this.openNode(n),this.addText(e),this.closeNode())}addText(e){""!==e&&this.add(e)}addSublanguage(e,n){const t=e.root;t.kind=n,t.sublanguage=!0,this.add(t)}toHTML(){return new l(this,this.options).value()}finalize(){return!0}}function d(e){return e?"string"==typeof e?e:e.source:null}const g="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",h={begin:"\\\\[\\s\\S]",relevance:0},f={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[h]},p={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[h]},b={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},m=function(e,n,t={}){var a=r({className:"comment",begin:e,end:n,contains:[]},t);return a.contains.push(b),a.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):",relevance:0}),a},v=m("//","$"),x=m("/\\*","\\*/"),E=m("#","$");var _=Object.freeze({__proto__:null,IDENT_RE:"[a-zA-Z]\\w*",UNDERSCORE_IDENT_RE:"[a-zA-Z_]\\w*",NUMBER_RE:"\\b\\d+(\\.\\d+)?",C_NUMBER_RE:g,BINARY_NUMBER_RE:"\\b(0b[01]+)",RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",SHEBANG:(e={})=>{const n=/^#![ ]*\//;return e.binary&&(e.begin=function(...e){return e.map(e=>d(e)).join("")}(n,/.*\b/,e.binary,/\b.*/)),r({className:"meta",begin:n,end:/$/,relevance:0,"on:begin":(e,n)=>{0!==e.index&&n.ignoreMatch()}},e)},BACKSLASH_ESCAPE:h,APOS_STRING_MODE:f,QUOTE_STRING_MODE:p,PHRASAL_WORDS_MODE:b,COMMENT:m,C_LINE_COMMENT_MODE:v,C_BLOCK_COMMENT_MODE:x,HASH_COMMENT_MODE:E,NUMBER_MODE:{className:"number",begin:"\\b\\d+(\\.\\d+)?",relevance:0},C_NUMBER_MODE:{className:"number",begin:g,relevance:0},BINARY_NUMBER_MODE:{className:"number",begin:"\\b(0b[01]+)",relevance:0},CSS_NUMBER_MODE:{className:"number",begin:"\\b\\d+(\\.\\d+)?(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},REGEXP_MODE:{begin:/(?=\/[^/\n]*\/)/,contains:[{className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[h,{begin:/\[/,end:/\]/,relevance:0,contains:[h]}]}]},TITLE_MODE:{className:"title",begin:"[a-zA-Z]\\w*",relevance:0},UNDERSCORE_TITLE_MODE:{className:"title",begin:"[a-zA-Z_]\\w*",relevance:0},METHOD_GUARD:{begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0},END_SAME_AS_BEGIN:function(e){return Object.assign(e,{"on:begin":(e,n)=>{n.data._beginMatch=e[1]},"on:end":(e,n)=>{n.data._beginMatch!==e[1]&&n.ignoreMatch()}})}}),N="of and for in not or if then".split(" ");function w(e,n){return n?+n:function(e){return N.includes(e.toLowerCase())}(e)?0:1}const R=t,y=r,{nodeStream:k,mergeStreams:O}=i,M=Symbol("nomatch");return function(t){var a=[],i={},s={},o=[],l=!0,c=/(^(<[^>]+>|\t|)+|\n)/gm,g="Could not find the language '{}', did you forget to load/include a language module?";const h={disableAutodetect:!0,name:"Plain text",contains:[]};var f={noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:null,__emitter:u};function p(e){return f.noHighlightRe.test(e)}function b(e,n,t,r){var a={code:n,language:e};S("before:highlight",a);var i=a.result?a.result:m(a.language,a.code,t,r);return i.code=a.code,S("after:highlight",i),i}function m(e,t,a,s){var o=t;function c(e,n){var t=E.case_insensitive?n[0].toLowerCase():n[0];return Object.prototype.hasOwnProperty.call(e.keywords,t)&&e.keywords[t]}function u(){null!=y.subLanguage?function(){if(""!==A){var e=null;if("string"==typeof y.subLanguage){if(!i[y.subLanguage])return void O.addText(A);e=m(y.subLanguage,A,!0,k[y.subLanguage]),k[y.subLanguage]=e.top}else e=v(A,y.subLanguage.length?y.subLanguage:null);y.relevance>0&&(I+=e.relevance),O.addSublanguage(e.emitter,e.language)}}():function(){if(!y.keywords)return void O.addText(A);let e=0;y.keywordPatternRe.lastIndex=0;let n=y.keywordPatternRe.exec(A),t="";for(;n;){t+=A.substring(e,n.index);const r=c(y,n);if(r){const[e,a]=r;O.addText(t),t="",I+=a,O.addKeyword(n[0],e)}else t+=n[0];e=y.keywordPatternRe.lastIndex,n=y.keywordPatternRe.exec(A)}t+=A.substr(e),O.addText(t)}(),A=""}function h(e){return e.className&&O.openNode(e.className),y=Object.create(e,{parent:{value:y}})}function p(e){return 0===y.matcher.regexIndex?(A+=e[0],1):(L=!0,0)}var b={};function x(t,r){var i=r&&r[0];if(A+=t,null==i)return u(),0;if("begin"===b.type&&"end"===r.type&&b.index===r.index&&""===i){if(A+=o.slice(r.index,r.index+1),!l){const n=Error("0 width match regex");throw n.languageName=e,n.badRule=b.rule,n}return 1}if(b=r,"begin"===r.type)return function(e){var t=e[0],r=e.rule;const a=new n(r),i=[r.__beforeBegin,r["on:begin"]];for(const n of i)if(n&&(n(e,a),a.ignore))return p(t);return r&&r.endSameAsBegin&&(r.endRe=RegExp(t.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),"m")),r.skip?A+=t:(r.excludeBegin&&(A+=t),u(),r.returnBegin||r.excludeBegin||(A=t)),h(r),r.returnBegin?0:t.length}(r);if("illegal"===r.type&&!a){const e=Error('Illegal lexeme "'+i+'" for mode "'+(y.className||"")+'"');throw e.mode=y,e}if("end"===r.type){var s=function(e){var t=e[0],r=o.substr(e.index),a=function e(t,r,a){let i=function(e,n){var t=e&&e.exec(n);return t&&0===t.index}(t.endRe,a);if(i){if(t["on:end"]){const e=new n(t);t["on:end"](r,e),e.ignore&&(i=!1)}if(i){for(;t.endsParent&&t.parent;)t=t.parent;return t}}if(t.endsWithParent)return e(t.parent,r,a)}(y,e,r);if(!a)return M;var i=y;i.skip?A+=t:(i.returnEnd||i.excludeEnd||(A+=t),u(),i.excludeEnd&&(A=t));do{y.className&&O.closeNode(),y.skip||y.subLanguage||(I+=y.relevance),y=y.parent}while(y!==a.parent);return a.starts&&(a.endSameAsBegin&&(a.starts.endRe=a.endRe),h(a.starts)),i.returnEnd?0:t.length}(r);if(s!==M)return s}if("illegal"===r.type&&""===i)return 1;if(B>1e5&&B>3*r.index)throw Error("potential infinite loop, way more iterations than matches");return A+=i,i.length}var E=T(e);if(!E)throw console.error(g.replace("{}",e)),Error('Unknown language: "'+e+'"');var _=function(e){function n(n,t){return RegExp(d(n),"m"+(e.case_insensitive?"i":"")+(t?"g":""))}class t{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(e,n){n.position=this.position++,this.matchIndexes[this.matchAt]=n,this.regexes.push([n,e]),this.matchAt+=function(e){return RegExp(e.toString()+"|").exec("").length-1}(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null);const e=this.regexes.map(e=>e[1]);this.matcherRe=n(function(e,n="|"){for(var t=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./,r=0,a="",i=0;i0&&(a+=n),a+="(";o.length>0;){var l=t.exec(o);if(null==l){a+=o;break}a+=o.substring(0,l.index),o=o.substring(l.index+l[0].length),"\\"===l[0][0]&&l[1]?a+="\\"+(+l[1]+s):(a+=l[0],"("===l[0]&&r++)}a+=")"}return a}(e),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex;const n=this.matcherRe.exec(e);if(!n)return null;const t=n.findIndex((e,n)=>n>0&&void 0!==e),r=this.matchIndexes[t];return n.splice(0,t),Object.assign(n,r)}}class a{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){if(this.multiRegexes[e])return this.multiRegexes[e];const n=new t;return this.rules.slice(e).forEach(([e,t])=>n.addRule(e,t)),n.compile(),this.multiRegexes[e]=n,n}considerAll(){this.regexIndex=0}addRule(e,n){this.rules.push([e,n]),"begin"===n.type&&this.count++}exec(e){const n=this.getMatcher(this.regexIndex);n.lastIndex=this.lastIndex;const t=n.exec(e);return t&&(this.regexIndex+=t.position+1,this.regexIndex===this.count&&(this.regexIndex=0)),t}}function i(e,n){const t=e.input[e.index-1],r=e.input[e.index+e[0].length];"."!==t&&"."!==r||n.ignoreMatch()}if(e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return function t(s,o){const l=s;if(s.compiled)return l;s.compiled=!0,s.__beforeBegin=null,s.keywords=s.keywords||s.beginKeywords;let c=null;if("object"==typeof s.keywords&&(c=s.keywords.$pattern,delete s.keywords.$pattern),s.keywords&&(s.keywords=function(e,n){var t={};return"string"==typeof e?r("keyword",e):Object.keys(e).forEach((function(n){r(n,e[n])})),t;function r(e,r){n&&(r=r.toLowerCase()),r.split(" ").forEach((function(n){var r=n.split("|");t[r[0]]=[e,w(r[0],r[1])]}))}}(s.keywords,e.case_insensitive)),s.lexemes&&c)throw Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ");return l.keywordPatternRe=n(s.lexemes||c||/\w+/,!0),o&&(s.beginKeywords&&(s.begin="\\b("+s.beginKeywords.split(" ").join("|")+")(?=\\b|\\s)",s.__beforeBegin=i),s.begin||(s.begin=/\B|\b/),l.beginRe=n(s.begin),s.endSameAsBegin&&(s.end=s.begin),s.end||s.endsWithParent||(s.end=/\B|\b/),s.end&&(l.endRe=n(s.end)),l.terminator_end=d(s.end)||"",s.endsWithParent&&o.terminator_end&&(l.terminator_end+=(s.end?"|":"")+o.terminator_end)),s.illegal&&(l.illegalRe=n(s.illegal)),void 0===s.relevance&&(s.relevance=1),s.contains||(s.contains=[]),s.contains=[].concat(...s.contains.map((function(e){return function(e){return e.variants&&!e.cached_variants&&(e.cached_variants=e.variants.map((function(n){return r(e,{variants:null},n)}))),e.cached_variants?e.cached_variants:function e(n){return!!n&&(n.endsWithParent||e(n.starts))}(e)?r(e,{starts:e.starts?r(e.starts):null}):Object.isFrozen(e)?r(e):e}("self"===e?s:e)}))),s.contains.forEach((function(e){t(e,l)})),s.starts&&t(s.starts,o),l.matcher=function(e){const n=new a;return e.contains.forEach(e=>n.addRule(e.begin,{rule:e,type:"begin"})),e.terminator_end&&n.addRule(e.terminator_end,{type:"end"}),e.illegal&&n.addRule(e.illegal,{type:"illegal"}),n}(l),l}(e)}(E),N="",y=s||_,k={},O=new f.__emitter(f);!function(){for(var e=[],n=y;n!==E;n=n.parent)n.className&&e.unshift(n.className);e.forEach(e=>O.openNode(e))}();var A="",I=0,S=0,B=0,L=!1;try{for(y.matcher.considerAll();;){B++,L?L=!1:(y.matcher.lastIndex=S,y.matcher.considerAll());const e=y.matcher.exec(o);if(!e)break;const n=x(o.substring(S,e.index),e);S=e.index+n}return x(o.substr(S)),O.closeAllNodes(),O.finalize(),N=O.toHTML(),{relevance:I,value:N,language:e,illegal:!1,emitter:O,top:y}}catch(n){if(n.message&&n.message.includes("Illegal"))return{illegal:!0,illegalBy:{msg:n.message,context:o.slice(S-100,S+100),mode:n.mode},sofar:N,relevance:0,value:R(o),emitter:O};if(l)return{illegal:!1,relevance:0,value:R(o),emitter:O,language:e,top:y,errorRaised:n};throw n}}function v(e,n){n=n||f.languages||Object.keys(i);var t=function(e){const n={relevance:0,emitter:new f.__emitter(f),value:R(e),illegal:!1,top:h};return n.emitter.addText(e),n}(e),r=t;return n.filter(T).filter(I).forEach((function(n){var a=m(n,e,!1);a.language=n,a.relevance>r.relevance&&(r=a),a.relevance>t.relevance&&(r=t,t=a)})),r.language&&(t.second_best=r),t}function x(e){return f.tabReplace||f.useBR?e.replace(c,e=>"\n"===e?f.useBR?"
":e:f.tabReplace?e.replace(/\t/g,f.tabReplace):e):e}function E(e){let n=null;const t=function(e){var n=e.className+" ";n+=e.parentNode?e.parentNode.className:"";const t=f.languageDetectRe.exec(n);if(t){var r=T(t[1]);return r||(console.warn(g.replace("{}",t[1])),console.warn("Falling back to no-highlight mode for this block.",e)),r?t[1]:"no-highlight"}return n.split(/\s+/).find(e=>p(e)||T(e))}(e);if(p(t))return;S("before:highlightBlock",{block:e,language:t}),f.useBR?(n=document.createElement("div")).innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n"):n=e;const r=n.textContent,a=t?b(t,r,!0):v(r),i=k(n);if(i.length){const e=document.createElement("div");e.innerHTML=a.value,a.value=O(i,k(e),r)}a.value=x(a.value),S("after:highlightBlock",{block:e,result:a}),e.innerHTML=a.value,e.className=function(e,n,t){var r=n?s[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),e.includes(r)||a.push(r),a.join(" ").trim()}(e.className,t,a.language),e.result={language:a.language,re:a.relevance,relavance:a.relevance},a.second_best&&(e.second_best={language:a.second_best.language,re:a.second_best.relevance,relavance:a.second_best.relevance})}const N=()=>{if(!N.called){N.called=!0;var e=document.querySelectorAll("pre code");a.forEach.call(e,E)}};function T(e){return e=(e||"").toLowerCase(),i[e]||i[s[e]]}function A(e,{languageName:n}){"string"==typeof e&&(e=[e]),e.forEach(e=>{s[e]=n})}function I(e){var n=T(e);return n&&!n.disableAutodetect}function S(e,n){var t=e;o.forEach((function(e){e[t]&&e[t](n)}))}Object.assign(t,{highlight:b,highlightAuto:v,fixMarkup:x,highlightBlock:E,configure:function(e){f=y(f,e)},initHighlighting:N,initHighlightingOnLoad:function(){window.addEventListener("DOMContentLoaded",N,!1)},registerLanguage:function(e,n){var r=null;try{r=n(t)}catch(n){if(console.error("Language definition for '{}' could not be registered.".replace("{}",e)),!l)throw n;console.error(n),r=h}r.name||(r.name=e),i[e]=r,r.rawDefinition=n.bind(null,t),r.aliases&&A(r.aliases,{languageName:e})},listLanguages:function(){return Object.keys(i)},getLanguage:T,registerAliases:A,requireLanguage:function(e){var n=T(e);if(n)return n;throw Error("The '{}' language is required, but not loaded.".replace("{}",e))},autoDetection:I,inherit:y,addPlugin:function(e){o.push(e)}}),t.debugMode=function(){l=!1},t.safeMode=function(){l=!0},t.versionString="10.1.1";for(const n in _)"object"==typeof _[n]&&e(_[n]);return Object.assign(t,_),t}({})}();"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs); +hljs.registerLanguage("apache",function(){"use strict";return function(e){var n={className:"number",begin:"\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?"};return{name:"Apache config",aliases:["apacheconf"],case_insensitive:!0,contains:[e.HASH_COMMENT_MODE,{className:"section",begin:"",contains:[n,{className:"number",begin:":\\d{1,5}"},e.inherit(e.QUOTE_STRING_MODE,{relevance:0})]},{className:"attribute",begin:/\w+/,relevance:0,keywords:{nomarkup:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"},starts:{end:/$/,relevance:0,keywords:{literal:"on off all deny allow"},contains:[{className:"meta",begin:"\\s\\[",end:"\\]$"},{className:"variable",begin:"[\\$%]\\{",end:"\\}",contains:["self",{className:"number",begin:"[\\$%]\\d+"}]},n,{className:"number",begin:"\\d+"},e.QUOTE_STRING_MODE]}}],illegal:/\S/}}}()); +hljs.registerLanguage("bash",function(){"use strict";return function(e){const s={};Object.assign(s,{className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{/,end:/\}/,contains:[{begin:/:-/,contains:[s]}]}]});const t={className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},n={className:"string",begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,s,t]};t.contains.push(n);const a={begin:/\$\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,s]},i=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10}),c={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b-?[a-z\._]+\b/,keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},contains:[i,e.SHEBANG(),c,a,e.HASH_COMMENT_MODE,n,{className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},s]}}}()); +hljs.registerLanguage("c-like",function(){"use strict";return function(e){function t(e){return"(?:"+e+")?"}var n="(decltype\\(auto\\)|"+t("[a-zA-Z_]\\w*::")+"[a-zA-Z_]\\w*"+t("<.*?>")+")",r={className:"keyword",begin:"\\b[a-z\\d_]*_t\\b"},a={className:"string",variants:[{begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",end:"'",illegal:"."},e.END_SAME_AS_BEGIN({begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},i={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},s={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{"meta-keyword":"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"},contains:[{begin:/\\\n/,relevance:0},e.inherit(a,{className:"meta-string"}),{className:"meta-string",begin:/<.*?>/,end:/$/,illegal:"\\n"},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},o={className:"title",begin:t("[a-zA-Z_]\\w*::")+e.IDENT_RE,relevance:0},c=t("[a-zA-Z_]\\w*::")+e.IDENT_RE+"\\s*\\(",l={keyword:"int float while private char char8_t char16_t char32_t catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid wchar_t short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignas alignof constexpr consteval constinit decltype concept co_await co_return co_yield requires noexcept static_assert thread_local restrict final override atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and and_eq bitand bitor compl not not_eq or or_eq xor xor_eq",built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr _Bool complex _Complex imaginary _Imaginary",literal:"true false nullptr NULL"},d=[r,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,i,a],_={variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}],keywords:l,contains:d.concat([{begin:/\(/,end:/\)/,keywords:l,contains:d.concat(["self"]),relevance:0}]),relevance:0},u={className:"function",begin:"("+n+"[\\*&\\s]+)+"+c,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:l,illegal:/[^\w\s\*&:<>]/,contains:[{begin:"decltype\\(auto\\)",keywords:l,relevance:0},{begin:c,returnBegin:!0,contains:[o],relevance:0},{className:"params",begin:/\(/,end:/\)/,keywords:l,relevance:0,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,i,r,{begin:/\(/,end:/\)/,keywords:l,relevance:0,contains:["self",e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,i,r]}]},r,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s]};return{aliases:["c","cc","h","c++","h++","hpp","hh","hxx","cxx"],keywords:l,disableAutodetect:!0,illegal:"",keywords:l,contains:["self",r]},{begin:e.IDENT_RE+"::",keywords:l},{className:"class",beginKeywords:"class struct",end:/[{;:]/,contains:[{begin://,contains:["self"]},e.TITLE_MODE]}]),exports:{preprocessor:s,strings:a,keywords:l}}}}()); +hljs.registerLanguage("c",function(){"use strict";return function(e){var n=e.getLanguage("c-like").rawDefinition();return n.name="C",n.aliases=["c","h"],n}}()); +hljs.registerLanguage("coffeescript",function(){"use strict";const e=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],n=["true","false","null","undefined","NaN","Infinity"],a=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);return function(r){var t={keyword:e.concat(["then","unless","until","loop","by","when","and","or","is","isnt","not"]).filter((e=>n=>!e.includes(n))(["var","const","let","function","static"])).join(" "),literal:n.concat(["yes","no","on","off"]).join(" "),built_in:a.concat(["npm","print"]).join(" ")},i="[A-Za-z$_][0-9A-Za-z$_]*",s={className:"subst",begin:/#\{/,end:/}/,keywords:t},o=[r.BINARY_NUMBER_MODE,r.inherit(r.C_NUMBER_MODE,{starts:{end:"(\\s*/)?",relevance:0}}),{className:"string",variants:[{begin:/'''/,end:/'''/,contains:[r.BACKSLASH_ESCAPE]},{begin:/'/,end:/'/,contains:[r.BACKSLASH_ESCAPE]},{begin:/"""/,end:/"""/,contains:[r.BACKSLASH_ESCAPE,s]},{begin:/"/,end:/"/,contains:[r.BACKSLASH_ESCAPE,s]}]},{className:"regexp",variants:[{begin:"///",end:"///",contains:[s,r.HASH_COMMENT_MODE]},{begin:"//[gim]{0,3}(?=\\W)",relevance:0},{begin:/\/(?![ *]).*?(?![\\]).\/[gim]{0,3}(?=\W)/}]},{begin:"@"+i},{subLanguage:"javascript",excludeBegin:!0,excludeEnd:!0,variants:[{begin:"```",end:"```"},{begin:"`",end:"`"}]}];s.contains=o;var c=r.inherit(r.TITLE_MODE,{begin:i}),l={className:"params",begin:"\\([^\\(]",returnBegin:!0,contains:[{begin:/\(/,end:/\)/,keywords:t,contains:["self"].concat(o)}]};return{name:"CoffeeScript",aliases:["coffee","cson","iced"],keywords:t,illegal:/\/\*/,contains:o.concat([r.COMMENT("###","###"),r.HASH_COMMENT_MODE,{className:"function",begin:"^\\s*"+i+"\\s*=\\s*(\\(.*\\))?\\s*\\B[-=]>",end:"[-=]>",returnBegin:!0,contains:[c,l]},{begin:/[:\(,=]\s*/,relevance:0,contains:[{className:"function",begin:"(\\(.*\\))?\\s*\\B[-=]>",end:"[-=]>",returnBegin:!0,contains:[l]}]},{className:"class",beginKeywords:"class",end:"$",illegal:/[:="\[\]]/,contains:[{beginKeywords:"extends",endsWithParent:!0,illegal:/[:="\[\]]/,contains:[c]},c]},{begin:i+":",end:":",returnBegin:!0,returnEnd:!0,relevance:0}])}}}()); +hljs.registerLanguage("cpp",function(){"use strict";return function(e){var t=e.getLanguage("c-like").rawDefinition();return t.disableAutodetect=!1,t.name="C++",t.aliases=["cc","c++","h++","hpp","hh","hxx","cxx"],t}}()); +hljs.registerLanguage("csharp",function(){"use strict";return function(e){var n={keyword:"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let nameof on orderby partial remove select set value var when where yield",literal:"null false true"},i=e.inherit(e.TITLE_MODE,{begin:"[a-zA-Z](\\.?\\w)*"}),a={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},s={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},t=e.inherit(s,{illegal:/\n/}),l={className:"subst",begin:"{",end:"}",keywords:n},r=e.inherit(l,{illegal:/\n/}),c={className:"string",begin:/\$"/,end:'"',illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},e.BACKSLASH_ESCAPE,r]},o={className:"string",begin:/\$@"/,end:'"',contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},l]},g=e.inherit(o,{illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},r]});l.contains=[o,c,s,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.C_BLOCK_COMMENT_MODE],r.contains=[g,c,t,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.inherit(e.C_BLOCK_COMMENT_MODE,{illegal:/\n/})];var d={variants:[o,c,s,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},E={begin:"<",end:">",contains:[{beginKeywords:"in out"},i]},_=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",b={begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"],keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0,contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{begin:"\x3c!--|--\x3e"},{begin:""}]}]}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},d,a,{beginKeywords:"class interface",end:/[{;=]/,illegal:/[^\s:,]/,contains:[{beginKeywords:"where class"},i,E,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",end:/[{;=]/,illegal:/[^\s:]/,contains:[i,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta",begin:"^\\s*\\[",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{className:"meta-string",begin:/"/,end:/"/}]},{beginKeywords:"new return throw await else",relevance:0},{className:"function",begin:"("+_+"\\s+)+"+e.IDENT_RE+"\\s*(\\<.+\\>)?\\s*\\(",returnBegin:!0,end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{begin:e.IDENT_RE+"\\s*(\\<.+\\>)?\\s*\\(",returnBegin:!0,contains:[e.TITLE_MODE,E],relevance:0},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0,contains:[d,a,e.C_BLOCK_COMMENT_MODE]},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},b]}}}()); +hljs.registerLanguage("css",function(){"use strict";return function(e){var n={begin:/(?:[A-Z\_\.\-]+|--[a-zA-Z0-9_-]+)\s*:/,returnBegin:!0,end:";",endsWithParent:!0,contains:[{className:"attribute",begin:/\S/,end:":",excludeEnd:!0,starts:{endsWithParent:!0,excludeEnd:!0,contains:[{begin:/[\w-]+\(/,returnBegin:!0,contains:[{className:"built_in",begin:/[\w-]+/},{begin:/\(/,end:/\)/,contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.CSS_NUMBER_MODE]}]},e.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,e.C_BLOCK_COMMENT_MODE,{className:"number",begin:"#[0-9A-Fa-f]+"},{className:"meta",begin:"!important"}]}}]};return{name:"CSS",case_insensitive:!0,illegal:/[=\/|'\$]/,contains:[e.C_BLOCK_COMMENT_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/},{className:"selector-class",begin:/\.[A-Za-z0-9_-]+/},{className:"selector-attr",begin:/\[/,end:/\]/,illegal:"$",contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{begin:"@(page|font-face)",lexemes:"@[a-z-]+",keywords:"@page @font-face"},{begin:"@",end:"[{;]",illegal:/:/,returnBegin:!0,contains:[{className:"keyword",begin:/@\-?\w[\w]*(\-\w+)*/},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:"and or not only",contains:[{begin:/[a-z-]+:/,className:"attribute"},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.CSS_NUMBER_MODE]}]},{className:"selector-tag",begin:"[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0},{begin:"{",end:"}",illegal:/\S/,contains:[e.C_BLOCK_COMMENT_MODE,n]}]}}}()); +hljs.registerLanguage("diff",function(){"use strict";return function(e){return{name:"Diff",aliases:["patch"],contains:[{className:"meta",relevance:10,variants:[{begin:/^@@ +\-\d+,\d+ +\+\d+,\d+ +@@$/},{begin:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{begin:/^\-\-\- +\d+,\d+ +\-\-\-\-$/}]},{className:"comment",variants:[{begin:/Index: /,end:/$/},{begin:/={3,}/,end:/$/},{begin:/^\-{3}/,end:/$/},{begin:/^\*{3} /,end:/$/},{begin:/^\+{3}/,end:/$/},{begin:/^\*{15}$/}]},{className:"addition",begin:"^\\+",end:"$"},{className:"deletion",begin:"^\\-",end:"$"},{className:"addition",begin:"^\\!",end:"$"}]}}}()); +hljs.registerLanguage("go",function(){"use strict";return function(e){var n={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune",literal:"true false iota nil",built_in:"append cap close complex copy imag len make new panic print println real recover delete"};return{name:"Go",aliases:["golang"],keywords:n,illegal:"e(n)).join("")}return function(a){var s={className:"number",relevance:0,variants:[{begin:/([\+\-]+)?[\d]+_[\d_]+/},{begin:a.NUMBER_RE}]},i=a.COMMENT();i.variants=[{begin:/;/,end:/$/},{begin:/#/,end:/$/}];var t={className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{begin:/\$\{(.*?)}/}]},r={className:"literal",begin:/\bon|off|true|false|yes|no\b/},l={className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'},{begin:"'",end:"'"}]},c={begin:/\[/,end:/\]/,contains:[i,r,t,l,s,"self"],relevance:0},g="("+[/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/].map(n=>e(n)).join("|")+")";return{name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/,contains:[i,{className:"section",begin:/\[+/,end:/\]+/},{begin:n(g,"(\\s*\\.\\s*",g,")*",n("(?=",/\s*=\s*[^#\s]/,")")),className:"attr",starts:{end:/$/,contains:[i,c,r,t,l,s]}}]}}}()); +hljs.registerLanguage("java",function(){"use strict";function e(e){return e?"string"==typeof e?e:e.source:null}function n(e){return a("(",e,")?")}function a(...n){return n.map(n=>e(n)).join("")}function s(...n){return"("+n.map(n=>e(n)).join("|")+")"}return function(e){var t="false synchronized int abstract float private char boolean var static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",i={className:"meta",begin:"@[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*",contains:[{begin:/\(/,end:/\)/,contains:["self"]}]},r=e=>a("[",e,"]+([",e,"_]*[",e,"]+)?"),c={className:"number",variants:[{begin:`\\b(0[bB]${r("01")})[lL]?`},{begin:`\\b(0${r("0-7")})[dDfFlL]?`},{begin:a(/\b0[xX]/,s(a(r("a-fA-F0-9"),/\./,r("a-fA-F0-9")),a(r("a-fA-F0-9"),/\.?/),a(/\./,r("a-fA-F0-9"))),/([pP][+-]?(\d+))?/,/[fFdDlL]?/)},{begin:a(/\b/,s(a(/\d*\./,r("\\d")),r("\\d")),/[eE][+-]?[\d]+[dDfF]?/)},{begin:a(/\b/,r(/\d/),n(/\.?/),n(r(/\d/)),/[dDfFlL]?/)}],relevance:0};return{name:"Java",aliases:["jsp"],keywords:t,illegal:/<\/|#/,contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"class",beginKeywords:"class interface",end:/[{;=]/,excludeEnd:!0,keywords:"class interface",illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"new throw return else",relevance:0},{className:"function",begin:"([À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(<[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(\\s*,\\s*[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*)*>)?\\s+)+"+e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:t,contains:[{begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,contains:[e.UNDERSCORE_TITLE_MODE]},{className:"params",begin:/\(/,end:/\)/,keywords:t,relevance:0,contains:[i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE]},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},c,i]}}}()); +hljs.registerLanguage("javascript",function(){"use strict";const e=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],n=["true","false","null","undefined","NaN","Infinity"],a=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);function s(e){return r("(?=",e,")")}function r(...e){return e.map(e=>(function(e){return e?"string"==typeof e?e:e.source:null})(e)).join("")}return function(t){var i="[A-Za-z$_][0-9A-Za-z$_]*",c={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/},o={$pattern:"[A-Za-z$_][0-9A-Za-z$_]*",keyword:e.join(" "),literal:n.join(" "),built_in:a.join(" ")},l={className:"number",variants:[{begin:"\\b(0[bB][01]+)n?"},{begin:"\\b(0[oO][0-7]+)n?"},{begin:t.C_NUMBER_RE+"n?"}],relevance:0},E={className:"subst",begin:"\\$\\{",end:"\\}",keywords:o,contains:[]},d={begin:"html`",end:"",starts:{end:"`",returnEnd:!1,contains:[t.BACKSLASH_ESCAPE,E],subLanguage:"xml"}},g={begin:"css`",end:"",starts:{end:"`",returnEnd:!1,contains:[t.BACKSLASH_ESCAPE,E],subLanguage:"css"}},u={className:"string",begin:"`",end:"`",contains:[t.BACKSLASH_ESCAPE,E]};E.contains=[t.APOS_STRING_MODE,t.QUOTE_STRING_MODE,d,g,u,l,t.REGEXP_MODE];var b=E.contains.concat([{begin:/\(/,end:/\)/,contains:["self"].concat(E.contains,[t.C_BLOCK_COMMENT_MODE,t.C_LINE_COMMENT_MODE])},t.C_BLOCK_COMMENT_MODE,t.C_LINE_COMMENT_MODE]),_={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:b};return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:o,contains:[t.SHEBANG({binary:"node",relevance:5}),{className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},t.APOS_STRING_MODE,t.QUOTE_STRING_MODE,d,g,u,t.C_LINE_COMMENT_MODE,t.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+",contains:[{className:"type",begin:"\\{",end:"\\}",relevance:0},{className:"variable",begin:i+"(?=\\s*(-)|$)",endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]}),t.C_BLOCK_COMMENT_MODE,l,{begin:r(/[{,\n]\s*/,s(r(/(((\/\/.*)|(\/\*(.|\n)*\*\/))\s*)*/,i+"\\s*:"))),relevance:0,contains:[{className:"attr",begin:i+s("\\s*:"),relevance:0}]},{begin:"("+t.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[t.C_LINE_COMMENT_MODE,t.C_BLOCK_COMMENT_MODE,t.REGEXP_MODE,{className:"function",begin:"(\\([^(]*(\\([^(]*(\\([^(]*\\))?\\))?\\)|"+t.UNDERSCORE_IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:t.UNDERSCORE_IDENT_RE},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:o,contains:b}]}]},{begin:/,/,relevance:0},{className:"",begin:/\s/,end:/\s*/,skip:!0},{variants:[{begin:"<>",end:""},{begin:c.begin,end:c.end}],subLanguage:"xml",contains:[{begin:c.begin,end:c.end,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[t.inherit(t.TITLE_MODE,{begin:i}),_],illegal:/\[|%/},{begin:/\$[(.]/},t.METHOD_GUARD,{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends"},t.UNDERSCORE_TITLE_MODE]},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0},{begin:"(get|set)\\s+(?="+i+"\\()",end:/{/,keywords:"get set",contains:[t.inherit(t.TITLE_MODE,{begin:i}),{begin:/\(\)/},_]}],illegal:/#(?!!)/}}}()); +hljs.registerLanguage("json",function(){"use strict";return function(n){var e={literal:"true false null"},i=[n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE],t=[n.QUOTE_STRING_MODE,n.C_NUMBER_MODE],a={end:",",endsWithParent:!0,excludeEnd:!0,contains:t,keywords:e},l={begin:"{",end:"}",contains:[{className:"attr",begin:/"/,end:/"/,contains:[n.BACKSLASH_ESCAPE],illegal:"\\n"},n.inherit(a,{begin:/:/})].concat(i),illegal:"\\S"},s={begin:"\\[",end:"\\]",contains:[n.inherit(a)],illegal:"\\S"};return t.push(l,s),i.forEach((function(n){t.push(n)})),{name:"JSON",contains:t,keywords:e,illegal:"\\S"}}}()); +hljs.registerLanguage("kotlin",function(){"use strict";return function(e){var n={keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual trait volatile transient native default",built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing",literal:"true false null"},a={className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"@"},i={className:"subst",begin:"\\${",end:"}",contains:[e.C_NUMBER_MODE]},s={className:"variable",begin:"\\$"+e.UNDERSCORE_IDENT_RE},t={className:"string",variants:[{begin:'"""',end:'"""(?=[^"])',contains:[s,i]},{begin:"'",end:"'",illegal:/\n/,contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/,contains:[e.BACKSLASH_ESCAPE,s,i]}]};i.contains.push(t);var r={className:"meta",begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UNDERSCORE_IDENT_RE+")?"},l={className:"meta",begin:"@"+e.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/,end:/\)/,contains:[e.inherit(t,{className:"meta-string"})]}]},c=e.COMMENT("/\\*","\\*/",{contains:[e.C_BLOCK_COMMENT_MODE]}),o={variants:[{className:"type",begin:e.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/,contains:[]}]},d=o;return d.variants[1].contains=[o],o.variants[1].contains=[d],{name:"Kotlin",aliases:["kt"],keywords:n,contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,c,{className:"keyword",begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol",begin:/@\w+/}]}},a,r,l,{className:"function",beginKeywords:"fun",end:"[(]|$",returnBegin:!0,excludeEnd:!0,keywords:n,illegal:/fun\s+(<.*>)?[^\s\(]+(\s+[^\s\(]+)\s*=/,relevance:5,contains:[{begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,contains:[e.UNDERSCORE_TITLE_MODE]},{className:"type",begin://,keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:n,relevance:0,contains:[{begin:/:/,end:/[=,\/]/,endsWithParent:!0,contains:[o,e.C_LINE_COMMENT_MODE,c],relevance:0},e.C_LINE_COMMENT_MODE,c,r,l,t,e.C_NUMBER_MODE]},c]},{className:"class",beginKeywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0,illegal:"extends implements",contains:[{beginKeywords:"public protected internal private constructor"},e.UNDERSCORE_TITLE_MODE,{className:"type",begin://,excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,]|$/,excludeBegin:!0,returnEnd:!0},r,l]},t,{className:"meta",begin:"^#!/usr/bin/env",end:"$",illegal:"\n"},{className:"number",begin:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",relevance:0}]}}}()); +hljs.registerLanguage("less",function(){"use strict";return function(e){var n="([\\w-]+|@{[\\w-]+})",a=[],s=[],t=function(e){return{className:"string",begin:"~?"+e+".*?"+e}},r=function(e,n,a){return{className:e,begin:n,relevance:a}},i={begin:"\\(",end:"\\)",contains:s,relevance:0};s.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,t("'"),t('"'),e.CSS_NUMBER_MODE,{begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]",excludeEnd:!0}},r("number","#[0-9A-Fa-f]+\\b"),i,r("variable","@@?[\\w-]+",10),r("variable","@{[\\w-]+}"),r("built_in","~?`[^`]*?`"),{className:"attribute",begin:"[\\w-]+\\s*:",end:":",returnBegin:!0,excludeEnd:!0},{className:"meta",begin:"!important"});var c=s.concat({begin:"{",end:"}",contains:a}),l={beginKeywords:"when",endsWithParent:!0,contains:[{beginKeywords:"and not"}].concat(s)},o={begin:n+"\\s*:",returnBegin:!0,end:"[;}]",relevance:0,contains:[{className:"attribute",begin:n,end:":",excludeEnd:!0,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:s}}]},g={className:"keyword",begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b",starts:{end:"[;{}]",returnEnd:!0,contains:s,relevance:0}},d={className:"variable",variants:[{begin:"@[\\w-]+\\s*:",relevance:15},{begin:"@[\\w-]+"}],starts:{end:"[;}]",returnEnd:!0,contains:c}},b={variants:[{begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:n,end:"{"}],returnBegin:!0,returnEnd:!0,illegal:"[<='$\"]",relevance:0,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,l,r("keyword","all\\b"),r("variable","@{[\\w-]+}"),r("selector-tag",n+"%?",0),r("selector-id","#"+n),r("selector-class","\\."+n,0),r("selector-tag","&",0),{className:"selector-attr",begin:"\\[",end:"\\]"},{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{begin:"\\(",end:"\\)",contains:c},{begin:"!important"}]};return a.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,g,d,o,b),{name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:a}}}()); +hljs.registerLanguage("lua",function(){"use strict";return function(e){var t={begin:"\\[=*\\[",end:"\\]=*\\]",contains:["self"]},a=[e.COMMENT("--(?!\\[=*\\[)","$"),e.COMMENT("--\\[=*\\[","\\]=*\\]",{contains:[t],relevance:10})];return{name:"Lua",keywords:{$pattern:e.UNDERSCORE_IDENT_RE,literal:"true false nil",keyword:"and break do else elseif end for goto if in local not or repeat return then until while",built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove"},contains:a.concat([{className:"function",beginKeywords:"function",end:"\\)",contains:[e.inherit(e.TITLE_MODE,{begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params",begin:"\\(",endsWithParent:!0,contains:a}].concat(a)},e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string",begin:"\\[=*\\[",end:"\\]=*\\]",contains:[t],relevance:5}])}}}()); +hljs.registerLanguage("makefile",function(){"use strict";return function(e){var i={className:"variable",variants:[{begin:"\\$\\("+e.UNDERSCORE_IDENT_RE+"\\)",contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin:"",relevance:10,contains:[a,i,t,s,{begin:"\\[",end:"\\]",contains:[{className:"meta",begin:"",contains:[a,s,i,t]}]}]},e.COMMENT("\x3c!--","--\x3e",{relevance:10}),{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10},n,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:")",end:">",keywords:{name:"style"},contains:[c],starts:{end:"",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:")",end:">",keywords:{name:"script"},contains:[c],starts:{end:"<\/script>",returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:"",contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0},c]}]}}}()); +hljs.registerLanguage("markdown",function(){"use strict";return function(n){const e={begin:"<",end:">",subLanguage:"xml",relevance:0},a={begin:"\\[.+?\\][\\(\\[].*?[\\)\\]]",returnBegin:!0,contains:[{className:"string",begin:"\\[",end:"\\]",excludeBegin:!0,returnEnd:!0,relevance:0},{className:"link",begin:"\\]\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0},{className:"symbol",begin:"\\]\\[",end:"\\]",excludeBegin:!0,excludeEnd:!0}],relevance:10},i={className:"strong",contains:[],variants:[{begin:/_{2}/,end:/_{2}/},{begin:/\*{2}/,end:/\*{2}/}]},s={className:"emphasis",contains:[],variants:[{begin:/\*(?!\*)/,end:/\*/},{begin:/_(?!_)/,end:/_/,relevance:0}]};i.contains.push(s),s.contains.push(i);var c=[e,a];return i.contains=i.contains.concat(c),s.contains=s.contains.concat(c),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:c=c.concat(i,s)},{begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n",contains:c}]}]},e,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)",end:"\\s+",excludeEnd:!0},i,s,{className:"quote",begin:"^>\\s+",contains:c,end:"$"},{className:"code",variants:[{begin:"(`{3,})(.|\\n)*?\\1`*[ ]*"},{begin:"(~{3,})(.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))",contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{begin:"^[-\\*]{3,}",end:"$"},a,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}}}()); +hljs.registerLanguage("nginx",function(){"use strict";return function(e){var n={className:"variable",variants:[{begin:/\$\d+/},{begin:/\$\{/,end:/}/},{begin:"[\\$\\@]"+e.UNDERSCORE_IDENT_RE}]},a={endsWithParent:!0,keywords:{$pattern:"[a-z/_]+",literal:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"},relevance:0,illegal:"=>",contains:[e.HASH_COMMENT_MODE,{className:"string",contains:[e.BACKSLASH_ESCAPE,n],variants:[{begin:/"/,end:/"/},{begin:/'/,end:/'/}]},{begin:"([a-z]+):/",end:"\\s",endsWithParent:!0,excludeEnd:!0,contains:[n]},{className:"regexp",contains:[e.BACKSLASH_ESCAPE,n],variants:[{begin:"\\s\\^",end:"\\s|{|;",returnEnd:!0},{begin:"~\\*?\\s+",end:"\\s|{|;",returnEnd:!0},{begin:"\\*(\\.[a-z\\-]+)+"},{begin:"([a-z\\-]+\\.)+\\*"}]},{className:"number",begin:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{className:"number",begin:"\\b\\d+[kKmMgGdshdwy]*\\b",relevance:0},n]};return{name:"Nginx config",aliases:["nginxconf"],contains:[e.HASH_COMMENT_MODE,{begin:e.UNDERSCORE_IDENT_RE+"\\s+{",returnBegin:!0,end:"{",contains:[{className:"section",begin:e.UNDERSCORE_IDENT_RE}],relevance:0},{begin:e.UNDERSCORE_IDENT_RE+"\\s",end:";|{",returnBegin:!0,contains:[{className:"attribute",begin:e.UNDERSCORE_IDENT_RE,starts:a}],relevance:0}],illegal:"[^\\s\\}]"}}}()); +hljs.registerLanguage("objectivec",function(){"use strict";return function(e){var n=/[a-zA-Z@][a-zA-Z0-9_]*/,_={$pattern:n,keyword:"@interface @class @protocol @implementation"};return{name:"Objective-C",aliases:["mm","objc","obj-c"],keywords:{$pattern:n,keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN",literal:"false true FALSE TRUE nil YES NO NULL",built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"},illegal:"/,end:/$/,illegal:"\\n"},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"class",begin:"("+_.keyword.split(" ").join("|")+")\\b",end:"({|$)",excludeEnd:!0,keywords:_,contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"\\."+e.UNDERSCORE_IDENT_RE,relevance:0}]}}}()); +hljs.registerLanguage("perl",function(){"use strict";return function(e){var n={$pattern:/[\w.]+/,keyword:"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qq fileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmget sub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedir ioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when"},t={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:n},s={begin:"->{",end:"}"},r={variants:[{begin:/\$\d/},{begin:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{begin:/[\$%@][^\s\w{]/,relevance:0}]},i=[e.BACKSLASH_ESCAPE,t,r],a=[r,e.HASH_COMMENT_MODE,e.COMMENT("^\\=\\w","\\=cut",{endsWithParent:!0}),s,{className:"string",contains:i,variants:[{begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[",end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*\\<",end:"\\>",relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'",contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`",contains:[e.BACKSLASH_ESCAPE]},{begin:"{\\w+}",contains:[],relevance:0},{begin:"-?\\w+\\s*\\=\\>",contains:[],relevance:0}]},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{begin:"(\\/\\/|"+e.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*",keywords:"split return print reverse grep",relevance:0,contains:[e.HASH_COMMENT_MODE,{className:"regexp",begin:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",relevance:10},{className:"regexp",begin:"(m|qr)?/",end:"/[a-z]*",contains:[e.BACKSLASH_ESCAPE],relevance:0}]},{className:"function",beginKeywords:"sub",end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[e.TITLE_MODE]},{begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$",subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}]}];return t.contains=a,s.contains=a,{name:"Perl",aliases:["pl","pm"],keywords:n,contains:a}}}()); +hljs.registerLanguage("php",function(){"use strict";return function(e){var r={begin:"\\$+[a-zA-Z_-ÿ][a-zA-Z0-9_-ÿ]*"},t={className:"meta",variants:[{begin:/<\?php/,relevance:10},{begin:/<\?[=]?/},{begin:/\?>/}]},a={className:"string",contains:[e.BACKSLASH_ESCAPE,t],variants:[{begin:'b"',end:'"'},{begin:"b'",end:"'"},e.inherit(e.APOS_STRING_MODE,{illegal:null}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null})]},n={variants:[e.BINARY_NUMBER_MODE,e.C_NUMBER_MODE]},i={keyword:"__CLASS__ __DIR__ __FILE__ __FUNCTION__ __LINE__ __METHOD__ __NAMESPACE__ __TRAIT__ die echo exit include include_once print require require_once array abstract and as binary bool boolean break callable case catch class clone const continue declare default do double else elseif empty enddeclare endfor endforeach endif endswitch endwhile eval extends final finally float for foreach from global goto if implements instanceof insteadof int integer interface isset iterable list new object or private protected public real return string switch throw trait try unset use var void while xor yield",literal:"false null true",built_in:"Error|0 AppendIterator ArgumentCountError ArithmeticError ArrayIterator ArrayObject AssertionError BadFunctionCallException BadMethodCallException CachingIterator CallbackFilterIterator CompileError Countable DirectoryIterator DivisionByZeroError DomainException EmptyIterator ErrorException Exception FilesystemIterator FilterIterator GlobIterator InfiniteIterator InvalidArgumentException IteratorIterator LengthException LimitIterator LogicException MultipleIterator NoRewindIterator OutOfBoundsException OutOfRangeException OuterIterator OverflowException ParentIterator ParseError RangeException RecursiveArrayIterator RecursiveCachingIterator RecursiveCallbackFilterIterator RecursiveDirectoryIterator RecursiveFilterIterator RecursiveIterator RecursiveIteratorIterator RecursiveRegexIterator RecursiveTreeIterator RegexIterator RuntimeException SeekableIterator SplDoublyLinkedList SplFileInfo SplFileObject SplFixedArray SplHeap SplMaxHeap SplMinHeap SplObjectStorage SplObserver SplObserver SplPriorityQueue SplQueue SplStack SplSubject SplSubject SplTempFileObject TypeError UnderflowException UnexpectedValueException ArrayAccess Closure Generator Iterator IteratorAggregate Serializable Throwable Traversable WeakReference Directory __PHP_Incomplete_Class parent php_user_filter self static stdClass"};return{aliases:["php","php3","php4","php5","php6","php7"],case_insensitive:!0,keywords:i,contains:[e.HASH_COMMENT_MODE,e.COMMENT("//","$",{contains:[t]}),e.COMMENT("/\\*","\\*/",{contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),e.COMMENT("__halt_compiler.+?;",!1,{endsWithParent:!0,keywords:"__halt_compiler"}),{className:"string",begin:/<<<['"]?\w+['"]?$/,end:/^\w+;?$/,contains:[e.BACKSLASH_ESCAPE,{className:"subst",variants:[{begin:/\$\w+/},{begin:/\{\$/,end:/\}/}]}]},t,{className:"keyword",begin:/\$this\b/},r,{begin:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{className:"function",beginKeywords:"fn function",end:/[;{]/,excludeEnd:!0,illegal:"[$%\\[]",contains:[e.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:i,contains:["self",r,e.C_BLOCK_COMMENT_MODE,a,n]}]},{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,illegal:/[:\(\$"]/,contains:[{beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"namespace",end:";",illegal:/[\.']/,contains:[e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"use",end:";",contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"=>"},a,n]}}}()); +hljs.registerLanguage("php-template",function(){"use strict";return function(n){return{name:"PHP template",subLanguage:"xml",contains:[{begin:/<\?(php|=)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*",end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{begin:"b'",end:"'",skip:!0},n.inherit(n.APOS_STRING_MODE,{illegal:null,className:null,contains:null,skip:!0}),n.inherit(n.QUOTE_STRING_MODE,{illegal:null,className:null,contains:null,skip:!0})]}]}}}()); +hljs.registerLanguage("plaintext",function(){"use strict";return function(t){return{name:"Plain text",aliases:["text","txt"],disableAutodetect:!0}}}()); +hljs.registerLanguage("properties",function(){"use strict";return function(e){var n="[ \\t\\f]*",t="("+n+"[:=]"+n+"|[ \\t\\f]+)",a="([^\\\\:= \\t\\f\\n]|\\\\.)+",s={end:t,relevance:0,starts:{className:"string",end:/$/,relevance:0,contains:[{begin:"\\\\\\n"}]}};return{name:".properties",case_insensitive:!0,illegal:/\S/,contains:[e.COMMENT("^\\s*[!#]","$"),{begin:"([^\\\\\\W:= \\t\\f\\n]|\\\\.)+"+t,returnBegin:!0,contains:[{className:"attr",begin:"([^\\\\\\W:= \\t\\f\\n]|\\\\.)+",endsParent:!0,relevance:0}],starts:s},{begin:a+t,returnBegin:!0,relevance:0,contains:[{className:"meta",begin:a,endsParent:!0,relevance:0}],starts:s},{className:"attr",relevance:0,begin:a+n+"$"}]}}}()); +hljs.registerLanguage("python",function(){"use strict";return function(e){var n={keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10",built_in:"Ellipsis NotImplemented",literal:"False None True"},a={className:"meta",begin:/^(>>>|\.\.\.) /},i={className:"subst",begin:/\{/,end:/\}/,keywords:n,illegal:/#/},s={begin:/\{\{/,relevance:0},r={className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{begin:/(u|b)?r?'''/,end:/'''/,contains:[e.BACKSLASH_ESCAPE,a],relevance:10},{begin:/(u|b)?r?"""/,end:/"""/,contains:[e.BACKSLASH_ESCAPE,a],relevance:10},{begin:/(fr|rf|f)'''/,end:/'''/,contains:[e.BACKSLASH_ESCAPE,a,s,i]},{begin:/(fr|rf|f)"""/,end:/"""/,contains:[e.BACKSLASH_ESCAPE,a,s,i]},{begin:/(u|r|ur)'/,end:/'/,relevance:10},{begin:/(u|r|ur)"/,end:/"/,relevance:10},{begin:/(b|br)'/,end:/'/},{begin:/(b|br)"/,end:/"/},{begin:/(fr|rf|f)'/,end:/'/,contains:[e.BACKSLASH_ESCAPE,s,i]},{begin:/(fr|rf|f)"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,s,i]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},l={className:"number",relevance:0,variants:[{begin:e.BINARY_NUMBER_RE+"[lLjJ]?"},{begin:"\\b(0o[0-7]+)[lLjJ]?"},{begin:e.C_NUMBER_RE+"[lLjJ]?"}]},t={className:"params",variants:[{begin:/\(\s*\)/,skip:!0,className:null},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:["self",a,l,r,e.HASH_COMMENT_MODE]}]};return i.contains=[r,l,a],{name:"Python",aliases:["py","gyp","ipython"],keywords:n,illegal:/(<\/|->|\?)|=>/,contains:[a,l,{beginKeywords:"if",relevance:0},r,e.HASH_COMMENT_MODE,{variants:[{className:"function",beginKeywords:"def"},{className:"class",beginKeywords:"class"}],end:/:/,illegal:/[${=;\n,]/,contains:[e.UNDERSCORE_TITLE_MODE,t,{begin:/->/,endsWithParent:!0,keywords:"None"}]},{className:"meta",begin:/^[\t ]*@/,end:/$/},{begin:/\b(print|exec)\(/}]}}}()); +hljs.registerLanguage("python-repl",function(){"use strict";return function(n){return{aliases:["pycon"],contains:[{className:"meta",starts:{end:/ |$/,starts:{end:"$",subLanguage:"python"}},variants:[{begin:/^>>>(?=[ ]|$)/},{begin:/^\.\.\.(?=[ ]|$)/}]}]}}}()); +hljs.registerLanguage("ruby",function(){"use strict";return function(e){var n="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",a={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},s={className:"doctag",begin:"@[A-Za-z]+"},i={begin:"#<",end:">"},r=[e.COMMENT("#","$",{contains:[s]}),e.COMMENT("^\\=begin","^\\=end",{contains:[s],relevance:10}),e.COMMENT("^__END__","\\n$")],c={className:"subst",begin:"#\\{",end:"}",keywords:a},t={className:"string",contains:[e.BACKSLASH_ESCAPE,c],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:"%[qQwWx]?\\(",end:"\\)"},{begin:"%[qQwWx]?\\[",end:"\\]"},{begin:"%[qQwWx]?{",end:"}"},{begin:"%[qQwWx]?<",end:">"},{begin:"%[qQwWx]?/",end:"/"},{begin:"%[qQwWx]?%",end:"%"},{begin:"%[qQwWx]?-",end:"-"},{begin:"%[qQwWx]?\\|",end:"\\|"},{begin:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},{begin:/<<[-~]?'?(\w+)(?:.|\n)*?\n\s*\1\b/,returnBegin:!0,contains:[{begin:/<<[-~]?'?/},e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,contains:[e.BACKSLASH_ESCAPE,c]})]}]},b={className:"params",begin:"\\(",end:"\\)",endsParent:!0,keywords:a},d=[t,i,{className:"class",beginKeywords:"class module",end:"$|;",illegal:/=/,contains:[e.inherit(e.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{begin:"<\\s*",contains:[{begin:"("+e.IDENT_RE+"::)?"+e.IDENT_RE}]}].concat(r)},{className:"function",beginKeywords:"def",end:"$|;",contains:[e.inherit(e.TITLE_MODE,{begin:n}),b].concat(r)},{begin:e.IDENT_RE+"::"},{className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"(\\!|\\?)?:",relevance:0},{className:"symbol",begin:":(?!\\s)",contains:[t,{begin:n}],relevance:0},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{begin:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{className:"params",begin:/\|/,end:/\|/,keywords:a},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*",keywords:"unless",contains:[i,{className:"regexp",contains:[e.BACKSLASH_ESCAPE,c],illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:"%r{",end:"}[a-z]*"},{begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]}].concat(r),relevance:0}].concat(r);c.contains=d,b.contains=d;var g=[{begin:/^\s*=>/,starts:{end:"$",contains:d}},{className:"meta",begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>)",starts:{end:"$",contains:d}}];return{name:"Ruby",aliases:["rb","gemspec","podspec","thor","irb"],keywords:a,illegal:/\/\*/,contains:r.concat(g).concat(d)}}}()); +hljs.registerLanguage("rust",function(){"use strict";return function(e){var n="([ui](8|16|32|64|128|size)|f(32|64))?",t="drop i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 str char bool Box Option Result String Vec Copy Send Sized Sync Drop Fn FnMut FnOnce ToOwned Clone Debug PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator SliceConcatExt ToString assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules! assert_ne! debug_assert_ne!";return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",keyword:"abstract as async await become box break const continue crate do dyn else enum extern false final fn for if impl in let loop macro match mod move mut override priv pub ref return self Self static struct super trait true try type typeof unsafe unsized use virtual where while yield",literal:"true false Some None Ok Err",built_in:t},illegal:""}]}}}()); +hljs.registerLanguage("scss",function(){"use strict";return function(e){var t={className:"variable",begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b"},i={className:"number",begin:"#[0-9A-Fa-f]+"};return e.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,e.C_BLOCK_COMMENT_MODE,{name:"SCSS",case_insensitive:!0,illegal:"[=/|']",contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"selector-id",begin:"\\#[A-Za-z0-9_-]+",relevance:0},{className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0},{className:"selector-attr",begin:"\\[",end:"\\]",illegal:"$"},{className:"selector-tag",begin:"\\b(a|abbr|acronym|address|area|article|aside|audio|b|base|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|frame|frameset|(h[1-6])|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|samp|script|section|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|ul|var|video)\\b",relevance:0},{className:"selector-pseudo",begin:":(visited|valid|root|right|required|read-write|read-only|out-range|optional|only-of-type|only-child|nth-of-type|nth-last-of-type|nth-last-child|nth-child|not|link|left|last-of-type|last-child|lang|invalid|indeterminate|in-range|hover|focus|first-of-type|first-line|first-letter|first-child|first|enabled|empty|disabled|default|checked|before|after|active)"},{className:"selector-pseudo",begin:"::(after|before|choices|first-letter|first-line|repeat-index|repeat-item|selection|value)"},t,{className:"attribute",begin:"\\b(src|z-index|word-wrap|word-spacing|word-break|width|widows|white-space|visibility|vertical-align|unicode-bidi|transition-timing-function|transition-property|transition-duration|transition-delay|transition|transform-style|transform-origin|transform|top|text-underline-position|text-transform|text-shadow|text-rendering|text-overflow|text-indent|text-decoration-style|text-decoration-line|text-decoration-color|text-decoration|text-align-last|text-align|tab-size|table-layout|right|resize|quotes|position|pointer-events|perspective-origin|perspective|page-break-inside|page-break-before|page-break-after|padding-top|padding-right|padding-left|padding-bottom|padding|overflow-y|overflow-x|overflow-wrap|overflow|outline-width|outline-style|outline-offset|outline-color|outline|orphans|order|opacity|object-position|object-fit|normal|none|nav-up|nav-right|nav-left|nav-index|nav-down|min-width|min-height|max-width|max-height|mask|marks|margin-top|margin-right|margin-left|margin-bottom|margin|list-style-type|list-style-position|list-style-image|list-style|line-height|letter-spacing|left|justify-content|initial|inherit|ime-mode|image-orientation|image-resolution|image-rendering|icon|hyphens|height|font-weight|font-variant-ligatures|font-variant|font-style|font-stretch|font-size-adjust|font-size|font-language-override|font-kerning|font-feature-settings|font-family|font|float|flex-wrap|flex-shrink|flex-grow|flex-flow|flex-direction|flex-basis|flex|filter|empty-cells|display|direction|cursor|counter-reset|counter-increment|content|column-width|column-span|column-rule-width|column-rule-style|column-rule-color|column-rule|column-gap|column-fill|column-count|columns|color|clip-path|clip|clear|caption-side|break-inside|break-before|break-after|box-sizing|box-shadow|box-decoration-break|bottom|border-width|border-top-width|border-top-style|border-top-right-radius|border-top-left-radius|border-top-color|border-top|border-style|border-spacing|border-right-width|border-right-style|border-right-color|border-right|border-radius|border-left-width|border-left-style|border-left-color|border-left|border-image-width|border-image-source|border-image-slice|border-image-repeat|border-image-outset|border-image|border-color|border-collapse|border-bottom-width|border-bottom-style|border-bottom-right-radius|border-bottom-left-radius|border-bottom-color|border-bottom|border|background-size|background-repeat|background-position|background-origin|background-image|background-color|background-clip|background-attachment|background-blend-mode|background|backface-visibility|auto|animation-timing-function|animation-play-state|animation-name|animation-iteration-count|animation-fill-mode|animation-duration|animation-direction|animation-delay|animation|align-self|align-items|align-content)\\b",illegal:"[^\\s]"},{begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b"},{begin:":",end:";",contains:[t,i,e.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,{className:"meta",begin:"!important"}]},{begin:"@(page|font-face)",lexemes:"@[a-z-]+",keywords:"@page @font-face"},{begin:"@",end:"[{;]",returnBegin:!0,keywords:"and or not only",contains:[{begin:"@[a-z-]+",className:"keyword"},t,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,i,e.CSS_NUMBER_MODE]}]}}}()); +hljs.registerLanguage("shell",function(){"use strict";return function(s){return{name:"Shell Session",aliases:["console"],contains:[{className:"meta",begin:"^\\s{0,3}[/\\w\\d\\[\\]()@-]*[>%$#]",starts:{end:"$",subLanguage:"bash"}}]}}}()); +hljs.registerLanguage("sql",function(){"use strict";return function(e){var t=e.COMMENT("--","$");return{name:"SQL",case_insensitive:!0,illegal:/[<>{}*]/,contains:[{beginKeywords:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment values with",end:/;/,endsWithParent:!0,keywords:{$pattern:/[\w\.]+/,keyword:"as abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias all allocate allow alter always analyze ancillary and anti any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound bucket buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain explode export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force foreign form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour hours http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lateral lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minutes minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notnull notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second seconds section securefile security seed segment select self semi sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tablesample tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unnest unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace window with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",literal:"true false null unknown",built_in:"array bigint binary bit blob bool boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text time timestamp tinyint varchar varchar2 varying void"},contains:[{className:"string",begin:"'",end:"'",contains:[{begin:"''"}]},{className:"string",begin:'"',end:'"',contains:[{begin:'""'}]},{className:"string",begin:"`",end:"`"},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,t,e.HASH_COMMENT_MODE]},e.C_BLOCK_COMMENT_MODE,t,e.HASH_COMMENT_MODE]}}}()); +hljs.registerLanguage("swift",function(){"use strict";return function(e){var i={keyword:"#available #colorLiteral #column #else #elseif #endif #file #fileLiteral #function #if #imageLiteral #line #selector #sourceLocation _ __COLUMN__ __FILE__ __FUNCTION__ __LINE__ Any as as! as? associatedtype associativity break case catch class continue convenience default defer deinit didSet do dynamic dynamicType else enum extension fallthrough false fileprivate final for func get guard if import in indirect infix init inout internal is lazy left let mutating nil none nonmutating open operator optional override postfix precedence prefix private protocol Protocol public repeat required rethrows return right self Self set static struct subscript super switch throw throws true try try! try? Type typealias unowned var weak where while willSet",literal:"true false nil",built_in:"abs advance alignof alignofValue anyGenerator assert assertionFailure bridgeFromObjectiveC bridgeFromObjectiveCUnconditional bridgeToObjectiveC bridgeToObjectiveCUnconditional c compactMap contains count countElements countLeadingZeros debugPrint debugPrintln distance dropFirst dropLast dump encodeBitsAsWords enumerate equal fatalError filter find getBridgedObjectiveCType getVaList indices insertionSort isBridgedToObjectiveC isBridgedVerbatimToObjectiveC isUniquelyReferenced isUniquelyReferencedNonObjC join lazy lexicographicalCompare map max maxElement min minElement numericCast overlaps partition posix precondition preconditionFailure print println quickSort readLine reduce reflect reinterpretCast reverse roundUpToAlignment sizeof sizeofValue sort split startsWith stride strideof strideofValue swap toString transcode underestimateCount unsafeAddressOf unsafeBitCast unsafeDowncast unsafeUnwrap unsafeReflect withExtendedLifetime withObjectAtPlusZero withUnsafePointer withUnsafePointerToObject withUnsafeMutablePointer withUnsafeMutablePointers withUnsafePointer withUnsafePointers withVaList zip"},n=e.COMMENT("/\\*","\\*/",{contains:["self"]}),t={className:"subst",begin:/\\\(/,end:"\\)",keywords:i,contains:[]},a={className:"string",contains:[e.BACKSLASH_ESCAPE,t],variants:[{begin:/"""/,end:/"""/},{begin:/"/,end:/"/}]},r={className:"number",begin:"\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b",relevance:0};return t.contains=[r],{name:"Swift",keywords:i,contains:[a,e.C_LINE_COMMENT_MODE,n,{className:"type",begin:"\\b[A-Z][\\wÀ-ʸ']*[!?]"},{className:"type",begin:"\\b[A-Z][\\wÀ-ʸ']*",relevance:0},r,{className:"function",beginKeywords:"func",end:"{",excludeEnd:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{begin://},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:i,contains:["self",r,a,e.C_BLOCK_COMMENT_MODE,{begin:":"}],illegal:/["']/}],illegal:/\[|%/},{className:"class",beginKeywords:"struct protocol class extension enum",keywords:i,end:"\\{",excludeEnd:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/})]},{className:"meta",begin:"(@discardableResult|@warn_unused_result|@exported|@lazy|@noescape|@NSCopying|@NSManaged|@objc|@objcMembers|@convention|@required|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix|@autoclosure|@testable|@available|@nonobjc|@NSApplicationMain|@UIApplicationMain|@dynamicMemberLookup|@propertyWrapper)\\b"},{beginKeywords:"import",end:/$/,contains:[e.C_LINE_COMMENT_MODE,n]}]}}}()); +hljs.registerLanguage("typescript",function(){"use strict";const e=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],n=["true","false","null","undefined","NaN","Infinity"],a=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);return function(r){var t={$pattern:"[A-Za-z$_][0-9A-Za-z$_]*",keyword:e.concat(["type","namespace","typedef","interface","public","private","protected","implements","declare","abstract","readonly"]).join(" "),literal:n.join(" "),built_in:a.concat(["any","void","number","boolean","string","object","never","enum"]).join(" ")},s={className:"meta",begin:"@[A-Za-z$_][0-9A-Za-z$_]*"},i={className:"number",variants:[{begin:"\\b(0[bB][01]+)n?"},{begin:"\\b(0[oO][0-7]+)n?"},{begin:r.C_NUMBER_RE+"n?"}],relevance:0},o={className:"subst",begin:"\\$\\{",end:"\\}",keywords:t,contains:[]},c={begin:"html`",end:"",starts:{end:"`",returnEnd:!1,contains:[r.BACKSLASH_ESCAPE,o],subLanguage:"xml"}},l={begin:"css`",end:"",starts:{end:"`",returnEnd:!1,contains:[r.BACKSLASH_ESCAPE,o],subLanguage:"css"}},E={className:"string",begin:"`",end:"`",contains:[r.BACKSLASH_ESCAPE,o]};o.contains=[r.APOS_STRING_MODE,r.QUOTE_STRING_MODE,c,l,E,i,r.REGEXP_MODE];var d={begin:"\\(",end:/\)/,keywords:t,contains:["self",r.QUOTE_STRING_MODE,r.APOS_STRING_MODE,r.NUMBER_MODE]},u={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:t,contains:[r.C_LINE_COMMENT_MODE,r.C_BLOCK_COMMENT_MODE,s,d]};return{name:"TypeScript",aliases:["ts"],keywords:t,contains:[r.SHEBANG(),{className:"meta",begin:/^\s*['"]use strict['"]/},r.APOS_STRING_MODE,r.QUOTE_STRING_MODE,c,l,E,r.C_LINE_COMMENT_MODE,r.C_BLOCK_COMMENT_MODE,i,{begin:"("+r.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[r.C_LINE_COMMENT_MODE,r.C_BLOCK_COMMENT_MODE,r.REGEXP_MODE,{className:"function",begin:"(\\([^(]*(\\([^(]*(\\([^(]*\\))?\\))?\\)|"+r.UNDERSCORE_IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:r.UNDERSCORE_IDENT_RE},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:t,contains:d.contains}]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/[\{;]/,excludeEnd:!0,keywords:t,contains:["self",r.inherit(r.TITLE_MODE,{begin:"[A-Za-z$_][0-9A-Za-z$_]*"}),u],illegal:/%/,relevance:0},{beginKeywords:"constructor",end:/[\{;]/,excludeEnd:!0,contains:["self",u]},{begin:/module\./,keywords:{built_in:"module"},relevance:0},{beginKeywords:"module",end:/\{/,excludeEnd:!0},{beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:"interface extends"},{begin:/\$[(.]/},{begin:"\\."+r.IDENT_RE,relevance:0},s,d]}}}()); +hljs.registerLanguage("yaml",function(){"use strict";return function(e){var n="true false yes no null",a="[\\w#;/?:@&=+$,.~*\\'()[\\]]+",s={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:"{{",end:"}}"},{begin:"%{",end:"}"}]}]},i=e.inherit(s,{variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),l={end:",",endsWithParent:!0,excludeEnd:!0,contains:[],keywords:n,relevance:0},t={begin:"{",end:"}",contains:[l],illegal:"\\n",relevance:0},g={begin:"\\[",end:"\\]",contains:[l],illegal:"\\n",relevance:0},b=[{className:"attr",variants:[{begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---s*$",relevance:10},{className:"string",begin:"[\\|>]([0-9]?[+-])?[ ]*\\n( *)[\\S ]+\\n(\\2[\\S ]+\\n?)*"},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!\\w+!"+a},{className:"type",begin:"!<"+a+">"},{className:"type",begin:"!"+a},{className:"type",begin:"!!"+a},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"\\-(?=[ ]|$)",relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{className:"number",begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"},{className:"number",begin:e.C_NUMBER_RE+"\\b"},t,g,s],c=[...b];return c.pop(),c.push(i),l.contains=c,{name:"YAML",case_insensitive:!0,aliases:["yml","YAML"],contains:b}}}()); +hljs.registerLanguage("armasm",function(){"use strict";return function(s){const e={variants:[s.COMMENT("^[ \\t]*(?=#)","$",{relevance:0,excludeBegin:!0}),s.COMMENT("[;@]","$",{relevance:0}),s.C_LINE_COMMENT_MODE,s.C_BLOCK_COMMENT_MODE]};return{name:"ARM Assembly",case_insensitive:!0,aliases:["arm"],keywords:{$pattern:"\\.?"+s.IDENT_RE,meta:".2byte .4byte .align .ascii .asciz .balign .byte .code .data .else .end .endif .endm .endr .equ .err .exitm .extern .global .hword .if .ifdef .ifndef .include .irp .long .macro .rept .req .section .set .skip .space .text .word .arm .thumb .code16 .code32 .force_thumb .thumb_func .ltorg ALIAS ALIGN ARM AREA ASSERT ATTR CN CODE CODE16 CODE32 COMMON CP DATA DCB DCD DCDU DCDO DCFD DCFDU DCI DCQ DCQU DCW DCWU DN ELIF ELSE END ENDFUNC ENDIF ENDP ENTRY EQU EXPORT EXPORTAS EXTERN FIELD FILL FUNCTION GBLA GBLL GBLS GET GLOBAL IF IMPORT INCBIN INCLUDE INFO KEEP LCLA LCLL LCLS LTORG MACRO MAP MEND MEXIT NOFP OPT PRESERVE8 PROC QN READONLY RELOC REQUIRE REQUIRE8 RLIST FN ROUT SETA SETL SETS SN SPACE SUBT THUMB THUMBX TTL WHILE WEND ",built_in:"r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 r15 pc lr sp ip sl sb fp a1 a2 a3 a4 v1 v2 v3 v4 v5 v6 v7 v8 f0 f1 f2 f3 f4 f5 f6 f7 p0 p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12 p13 p14 p15 c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12 c13 c14 c15 q0 q1 q2 q3 q4 q5 q6 q7 q8 q9 q10 q11 q12 q13 q14 q15 cpsr_c cpsr_x cpsr_s cpsr_f cpsr_cx cpsr_cxs cpsr_xs cpsr_xsf cpsr_sf cpsr_cxsf spsr_c spsr_x spsr_s spsr_f spsr_cx spsr_cxs spsr_xs spsr_xsf spsr_sf spsr_cxsf s0 s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s11 s12 s13 s14 s15 s16 s17 s18 s19 s20 s21 s22 s23 s24 s25 s26 s27 s28 s29 s30 s31 d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 d10 d11 d12 d13 d14 d15 d16 d17 d18 d19 d20 d21 d22 d23 d24 d25 d26 d27 d28 d29 d30 d31 {PC} {VAR} {TRUE} {FALSE} {OPT} {CONFIG} {ENDIAN} {CODESIZE} {CPU} {FPU} {ARCHITECTURE} {PCSTOREOFFSET} {ARMASM_VERSION} {INTER} {ROPI} {RWPI} {SWST} {NOSWST} . @"},contains:[{className:"keyword",begin:"\\b(adc|(qd?|sh?|u[qh]?)?add(8|16)?|usada?8|(q|sh?|u[qh]?)?(as|sa)x|and|adrl?|sbc|rs[bc]|asr|b[lx]?|blx|bxj|cbn?z|tb[bh]|bic|bfc|bfi|[su]bfx|bkpt|cdp2?|clz|clrex|cmp|cmn|cpsi[ed]|cps|setend|dbg|dmb|dsb|eor|isb|it[te]{0,3}|lsl|lsr|ror|rrx|ldm(([id][ab])|f[ds])?|ldr((s|ex)?[bhd])?|movt?|mvn|mra|mar|mul|[us]mull|smul[bwt][bt]|smu[as]d|smmul|smmla|mla|umlaal|smlal?([wbt][bt]|d)|mls|smlsl?[ds]|smc|svc|sev|mia([bt]{2}|ph)?|mrr?c2?|mcrr2?|mrs|msr|orr|orn|pkh(tb|bt)|rbit|rev(16|sh)?|sel|[su]sat(16)?|nop|pop|push|rfe([id][ab])?|stm([id][ab])?|str(ex)?[bhd]?|(qd?)?sub|(sh?|q|u[qh]?)?sub(8|16)|[su]xt(a?h|a?b(16)?)|srs([id][ab])?|swpb?|swi|smi|tst|teq|wfe|wfi|yield)(eq|ne|cs|cc|mi|pl|vs|vc|hi|ls|ge|lt|gt|le|al|hs|lo)?[sptrx]?(?=\\s)"},e,s.QUOTE_STRING_MODE,{className:"string",begin:"'",end:"[^\\\\]'",relevance:0},{className:"title",begin:"\\|",end:"\\|",illegal:"\\n",relevance:0},{className:"number",variants:[{begin:"[#$=]?0x[0-9a-f]+"},{begin:"[#$=]?0b[01]+"},{begin:"[#$=]\\d+"},{begin:"\\b\\d+"}],relevance:0},{className:"symbol",variants:[{begin:"^[ \\t]*[a-z_\\.\\$][a-z0-9_\\.\\$]+:"},{begin:"^[a-z_\\.\\$][a-z0-9_\\.\\$]+"},{begin:"[=#]\\w+"}],relevance:0}]}}}()); +hljs.registerLanguage("d",function(){"use strict";return function(e){var a={$pattern:e.UNDERSCORE_IDENT_RE,keyword:"abstract alias align asm assert auto body break byte case cast catch class const continue debug default delete deprecated do else enum export extern final finally for foreach foreach_reverse|10 goto if immutable import in inout int interface invariant is lazy macro mixin module new nothrow out override package pragma private protected public pure ref return scope shared static struct super switch synchronized template this throw try typedef typeid typeof union unittest version void volatile while with __FILE__ __LINE__ __gshared|10 __thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ __VERSION__",built_in:"bool cdouble cent cfloat char creal dchar delegate double dstring float function idouble ifloat ireal long real short string ubyte ucent uint ulong ushort wchar wstring",literal:"false null true"},d="((0|[1-9][\\d_]*)|0[bB][01_]+|0[xX]([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))",n="\\\\(['\"\\?\\\\abfnrtv]|u[\\dA-Fa-f]{4}|[0-7]{1,3}|x[\\dA-Fa-f]{2}|U[\\dA-Fa-f]{8})|&[a-zA-Z\\d]{2,};",t={className:"number",begin:"\\b"+d+"(L|u|U|Lu|LU|uL|UL)?",relevance:0},_={className:"number",begin:"\\b(((0[xX](([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)\\.([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)|\\.?([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))[pP][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))|((0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(\\.\\d*|([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)))|\\d+\\.(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)|\\.(0|[1-9][\\d_]*)([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))?))([fF]|L|i|[fF]i|Li)?|"+d+"(i|[fF]i|Li))",relevance:0},r={className:"string",begin:"'("+n+"|.)",end:"'",illegal:"."},i={className:"string",begin:'"',contains:[{begin:n,relevance:0}],end:'"[cwd]?'},s=e.COMMENT("\\/\\+","\\+\\/",{contains:["self"],relevance:10});return{name:"D",keywords:a,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s,{className:"string",begin:'x"[\\da-fA-F\\s\\n\\r]*"[cwd]?',relevance:10},i,{className:"string",begin:'[rq]"',end:'"[cwd]?',relevance:5},{className:"string",begin:"`",end:"`[cwd]?"},{className:"string",begin:'q"\\{',end:'\\}"'},_,t,r,{className:"meta",begin:"^#!",end:"$",relevance:5},{className:"meta",begin:"#(line)",end:"$",relevance:5},{className:"keyword",begin:"@[a-zA-Z_][a-zA-Z_\\d]*"}]}}}()); +hljs.registerLanguage("handlebars",function(){"use strict";function e(...e){return e.map(e=>(function(e){return e?"string"==typeof e?e:e.source:null})(e)).join("")}return function(n){const a={"builtin-name":"action bindattr collection component concat debugger each each-in get hash if in input link-to loc log lookup mut outlet partial query-params render template textarea unbound unless view with yield"},t=/\[.*?\]/,s=/[^\s!"#%&'()*+,.\/;<=>@\[\\\]^`{|}~]+/,i=e("(",/'.*?'/,"|",/".*?"/,"|",t,"|",s,"|",/\.|\//,")+"),r=e("(",t,"|",s,")(?==)"),l={begin:i,lexemes:/[\w.\/]+/},c=n.inherit(l,{keywords:{literal:"true false undefined null"}}),o={begin:/\(/,end:/\)/},m={className:"attr",begin:r,relevance:0,starts:{begin:/=/,end:/=/,starts:{contains:[n.NUMBER_MODE,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,c,o]}}},d={contains:[n.NUMBER_MODE,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,{begin:/as\s+\|/,keywords:{keyword:"as"},end:/\|/,contains:[{begin:/\w+/}]},m,c,o],returnEnd:!0},g=n.inherit(l,{className:"name",keywords:a,starts:n.inherit(d,{end:/\)/})});o.contains=[g];const u=n.inherit(l,{keywords:a,className:"name",starts:n.inherit(d,{end:/}}/})}),b=n.inherit(l,{keywords:a,className:"name"}),h=n.inherit(l,{className:"name",keywords:a,starts:n.inherit(d,{end:/}}/})});return{name:"Handlebars",aliases:["hbs","html.hbs","html.handlebars","htmlbars"],case_insensitive:!0,subLanguage:"xml",contains:[{begin:/\\\{\{/,skip:!0},{begin:/\\\\(?=\{\{)/,skip:!0},n.COMMENT(/\{\{!--/,/--\}\}/),n.COMMENT(/\{\{!/,/\}\}/),{className:"template-tag",begin:/\{\{\{\{(?!\/)/,end:/\}\}\}\}/,contains:[u],starts:{end:/\{\{\{\{\//,returnEnd:!0,subLanguage:"xml"}},{className:"template-tag",begin:/\{\{\{\{\//,end:/\}\}\}\}/,contains:[b]},{className:"template-tag",begin:/\{\{#/,end:/\}\}/,contains:[u]},{className:"template-tag",begin:/\{\{(?=else\}\})/,end:/\}\}/,keywords:"else"},{className:"template-tag",begin:/\{\{\//,end:/\}\}/,contains:[b]},{className:"template-variable",begin:/\{\{\{/,end:/\}\}\}/,contains:[h]},{className:"template-variable",begin:/\{\{/,end:/\}\}/,contains:[h]}]}}}()); +hljs.registerLanguage("haskell",function(){"use strict";return function(e){var n={variants:[e.COMMENT("--","$"),e.COMMENT("{-","-}",{contains:["self"]})]},i={className:"meta",begin:"{-#",end:"#-}"},a={className:"meta",begin:"^#",end:"$"},s={className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},l={begin:"\\(",end:"\\)",illegal:'"',contains:[i,a,{className:"type",begin:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},e.inherit(e.TITLE_MODE,{begin:"[_a-z][\\w']*"}),n]};return{name:"Haskell",aliases:["hs"],keywords:"let in if then else case of where do module import hiding qualified type data newtype deriving class instance as default infix infixl infixr foreign export ccall stdcall cplusplus jvm dotnet safe unsafe family forall mdo proc rec",contains:[{beginKeywords:"module",end:"where",keywords:"module where",contains:[l,n],illegal:"\\W\\.|;"},{begin:"\\bimport\\b",end:"$",keywords:"import qualified as hiding",contains:[l,n],illegal:"\\W\\.|;"},{className:"class",begin:"^(\\s*)?(class|instance)\\b",end:"where",keywords:"class family instance where",contains:[s,l,n]},{className:"class",begin:"\\b(data|(new)?type)\\b",end:"$",keywords:"data family type newtype deriving",contains:[i,s,l,{begin:"{",end:"}",contains:l.contains},n]},{beginKeywords:"default",end:"$",contains:[s,l,n]},{beginKeywords:"infix infixl infixr",end:"$",contains:[e.C_NUMBER_MODE,n]},{begin:"\\bforeign\\b",end:"$",keywords:"foreign import export ccall stdcall cplusplus jvm dotnet safe unsafe",contains:[s,e.QUOTE_STRING_MODE,n]},{className:"meta",begin:"#!\\/usr\\/bin\\/env runhaskell",end:"$"},i,a,e.QUOTE_STRING_MODE,e.C_NUMBER_MODE,s,e.inherit(e.TITLE_MODE,{begin:"^[_a-z][\\w']*"}),n,{begin:"->|<-"}]}}}()); +hljs.registerLanguage("julia",function(){"use strict";return function(e){var r="[A-Za-z_\\u00A1-\\uFFFF][A-Za-z_0-9\\u00A1-\\uFFFF]*",t={$pattern:r,keyword:"in isa where baremodule begin break catch ccall const continue do else elseif end export false finally for function global if import importall let local macro module quote return true try using while type immutable abstract bitstype typealias ",literal:"true false ARGS C_NULL DevNull ENDIAN_BOM ENV I Inf Inf16 Inf32 Inf64 InsertionSort JULIA_HOME LOAD_PATH MergeSort NaN NaN16 NaN32 NaN64 PROGRAM_FILE QuickSort RoundDown RoundFromZero RoundNearest RoundNearestTiesAway RoundNearestTiesUp RoundToZero RoundUp STDERR STDIN STDOUT VERSION catalan e|0 eu|0 eulergamma golden im nothing pi γ π φ ",built_in:"ANY AbstractArray AbstractChannel AbstractFloat AbstractMatrix AbstractRNG AbstractSerializer AbstractSet AbstractSparseArray AbstractSparseMatrix AbstractSparseVector AbstractString AbstractUnitRange AbstractVecOrMat AbstractVector Any ArgumentError Array AssertionError Associative Base64DecodePipe Base64EncodePipe Bidiagonal BigFloat BigInt BitArray BitMatrix BitVector Bool BoundsError BufferStream CachingPool CapturedException CartesianIndex CartesianRange Cchar Cdouble Cfloat Channel Char Cint Cintmax_t Clong Clonglong ClusterManager Cmd CodeInfo Colon Complex Complex128 Complex32 Complex64 CompositeException Condition ConjArray ConjMatrix ConjVector Cptrdiff_t Cshort Csize_t Cssize_t Cstring Cuchar Cuint Cuintmax_t Culong Culonglong Cushort Cwchar_t Cwstring DataType Date DateFormat DateTime DenseArray DenseMatrix DenseVecOrMat DenseVector Diagonal Dict DimensionMismatch Dims DirectIndexString Display DivideError DomainError EOFError EachLine Enum Enumerate ErrorException Exception ExponentialBackOff Expr Factorization FileMonitor Float16 Float32 Float64 Function Future GlobalRef GotoNode HTML Hermitian IO IOBuffer IOContext IOStream IPAddr IPv4 IPv6 IndexCartesian IndexLinear IndexStyle InexactError InitError Int Int128 Int16 Int32 Int64 Int8 IntSet Integer InterruptException InvalidStateException Irrational KeyError LabelNode LinSpace LineNumberNode LoadError LowerTriangular MIME Matrix MersenneTwister Method MethodError MethodTable Module NTuple NewvarNode NullException Nullable Number ObjectIdDict OrdinalRange OutOfMemoryError OverflowError Pair ParseError PartialQuickSort PermutedDimsArray Pipe PollingFileWatcher ProcessExitedException Ptr QuoteNode RandomDevice Range RangeIndex Rational RawFD ReadOnlyMemoryError Real ReentrantLock Ref Regex RegexMatch RemoteChannel RemoteException RevString RoundingMode RowVector SSAValue SegmentationFault SerializationState Set SharedArray SharedMatrix SharedVector Signed SimpleVector Slot SlotNumber SparseMatrixCSC SparseVector StackFrame StackOverflowError StackTrace StepRange StepRangeLen StridedArray StridedMatrix StridedVecOrMat StridedVector String SubArray SubString SymTridiagonal Symbol Symmetric SystemError TCPSocket Task Text TextDisplay Timer Tridiagonal Tuple Type TypeError TypeMapEntry TypeMapLevel TypeName TypeVar TypedSlot UDPSocket UInt UInt128 UInt16 UInt32 UInt64 UInt8 UndefRefError UndefVarError UnicodeError UniformScaling Union UnionAll UnitRange Unsigned UpperTriangular Val Vararg VecElement VecOrMat Vector VersionNumber Void WeakKeyDict WeakRef WorkerConfig WorkerPool "},a={keywords:t,illegal:/<\//},n={className:"subst",begin:/\$\(/,end:/\)/,keywords:t},o={className:"variable",begin:"\\$"+r},i={className:"string",contains:[e.BACKSLASH_ESCAPE,n,o],variants:[{begin:/\w*"""/,end:/"""\w*/,relevance:10},{begin:/\w*"/,end:/"\w*/}]},l={className:"string",contains:[e.BACKSLASH_ESCAPE,n,o],begin:"`",end:"`"},s={className:"meta",begin:"@"+r};return a.name="Julia",a.contains=[{className:"number",begin:/(\b0x[\d_]*(\.[\d_]*)?|0x\.\d[\d_]*)p[-+]?\d+|\b0[box][a-fA-F0-9][a-fA-F0-9_]*|(\b\d[\d_]*(\.[\d_]*)?|\.\d[\d_]*)([eEfF][-+]?\d+)?/,relevance:0},{className:"string",begin:/'(.|\\[xXuU][a-zA-Z0-9]+)'/},i,l,s,{className:"comment",variants:[{begin:"#=",end:"=#",relevance:10},{begin:"#",end:"$"}]},e.HASH_COMMENT_MODE,{className:"keyword",begin:"\\b(((abstract|primitive)\\s+)type|(mutable\\s+)?struct)\\b"},{begin:/<:/}],n.contains=a.contains,a}}()); +hljs.registerLanguage("nim",function(){"use strict";return function(e){return{name:"Nim",aliases:["nim"],keywords:{keyword:"addr and as asm bind block break case cast const continue converter discard distinct div do elif else end enum except export finally for from func generic if import in include interface is isnot iterator let macro method mixin mod nil not notin object of or out proc ptr raise ref return shl shr static template try tuple type using var when while with without xor yield",literal:"shared guarded stdin stdout stderr result true false",built_in:"int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 float float32 float64 bool char string cstring pointer expr stmt void auto any range array openarray varargs seq set clong culong cchar cschar cshort cint csize clonglong cfloat cdouble clongdouble cuchar cushort cuint culonglong cstringarray semistatic"},contains:[{className:"meta",begin:/{\./,end:/\.}/,relevance:10},{className:"string",begin:/[a-zA-Z]\w*"/,end:/"/,contains:[{begin:/""/}]},{className:"string",begin:/([a-zA-Z]\w*)?"""/,end:/"""/},e.QUOTE_STRING_MODE,{className:"type",begin:/\b[A-Z]\w+\b/,relevance:0},{className:"number",relevance:0,variants:[{begin:/\b(0[xX][0-9a-fA-F][_0-9a-fA-F]*)('?[iIuU](8|16|32|64))?/},{begin:/\b(0o[0-7][_0-7]*)('?[iIuUfF](8|16|32|64))?/},{begin:/\b(0(b|B)[01][_01]*)('?[iIuUfF](8|16|32|64))?/},{begin:/\b(\d[_\d]*)('?[iIuUfF](8|16|32|64))?/}]},e.HASH_COMMENT_MODE]}}}()); +hljs.registerLanguage("nix",function(){"use strict";return function(e){var n={keyword:"rec with let in inherit assert if else then",literal:"true false or and null",built_in:"import abort baseNameOf dirOf isNull builtins map removeAttrs throw toString derivation"},i={className:"subst",begin:/\$\{/,end:/}/,keywords:n},t={className:"string",contains:[i],variants:[{begin:"''",end:"''"},{begin:'"',end:'"'}]},s=[e.NUMBER_MODE,e.HASH_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,t,{begin:/[a-zA-Z0-9-_]+(\s*=)/,returnBegin:!0,relevance:0,contains:[{className:"attr",begin:/\S+/}]}];return i.contains=s,{name:"Nix",aliases:["nixos"],keywords:n,contains:s}}}()); +hljs.registerLanguage("r",function(){"use strict";return function(e){var n="([a-zA-Z]|\\.[a-zA-Z.])[a-zA-Z0-9._]*";return{name:"R",contains:[e.HASH_COMMENT_MODE,{begin:n,keywords:{$pattern:n,keyword:"function if in break next repeat else for return switch while try tryCatch stop warning require library attach detach source setMethod setGeneric setGroupGeneric setClass ...",literal:"NULL NA TRUE FALSE T F Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10"},relevance:0},{className:"number",begin:"0[xX][0-9a-fA-F]+[Li]?\\b",relevance:0},{className:"number",begin:"\\d+(?:[eE][+\\-]?\\d*)?L\\b",relevance:0},{className:"number",begin:"\\d+\\.(?!\\d)(?:i\\b)?",relevance:0},{className:"number",begin:"\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d*)?i?\\b",relevance:0},{className:"number",begin:"\\.\\d+(?:[eE][+\\-]?\\d*)?i?\\b",relevance:0},{begin:"`",end:"`",relevance:0},{className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{begin:'"',end:'"'},{begin:"'",end:"'"}]}]}}}()); +hljs.registerLanguage("scala",function(){"use strict";return function(e){var n={className:"subst",variants:[{begin:"\\$[A-Za-z0-9_]+"},{begin:"\\${",end:"}"}]},a={className:"string",variants:[{begin:'"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{begin:'"""',end:'"""',relevance:10},{begin:'[a-z]+"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE,n]},{className:"string",begin:'[a-z]+"""',end:'"""',contains:[n],relevance:10}]},s={className:"type",begin:"\\b[A-Z][A-Za-z0-9_]*",relevance:0},t={className:"title",begin:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/,relevance:0},i={className:"class",beginKeywords:"class object trait type",end:/[:={\[\n;]/,excludeEnd:!0,contains:[{beginKeywords:"extends with",relevance:10},{begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[s]},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[s]},t]},l={className:"function",beginKeywords:"def",end:/[:={\[(\n;]/,excludeEnd:!0,contains:[t]};return{name:"Scala",keywords:{literal:"true false null",keyword:"type yield lazy override def with val var sealed abstract private trait object if forSome for while throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit"},contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,{className:"symbol",begin:"'\\w[\\w\\d_]*(?!')"},s,l,i,e.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"}]}}}()); +hljs.registerLanguage("x86asm",function(){"use strict";return function(s){return{name:"Intel x86 Assembly",case_insensitive:!0,keywords:{$pattern:"[.%]?"+s.IDENT_RE,keyword:"lock rep repe repz repne repnz xaquire xrelease bnd nobnd aaa aad aam aas adc add and arpl bb0_reset bb1_reset bound bsf bsr bswap bt btc btr bts call cbw cdq cdqe clc cld cli clts cmc cmp cmpsb cmpsd cmpsq cmpsw cmpxchg cmpxchg486 cmpxchg8b cmpxchg16b cpuid cpu_read cpu_write cqo cwd cwde daa das dec div dmint emms enter equ f2xm1 fabs fadd faddp fbld fbstp fchs fclex fcmovb fcmovbe fcmove fcmovnb fcmovnbe fcmovne fcmovnu fcmovu fcom fcomi fcomip fcomp fcompp fcos fdecstp fdisi fdiv fdivp fdivr fdivrp femms feni ffree ffreep fiadd ficom ficomp fidiv fidivr fild fimul fincstp finit fist fistp fisttp fisub fisubr fld fld1 fldcw fldenv fldl2e fldl2t fldlg2 fldln2 fldpi fldz fmul fmulp fnclex fndisi fneni fninit fnop fnsave fnstcw fnstenv fnstsw fpatan fprem fprem1 fptan frndint frstor fsave fscale fsetpm fsin fsincos fsqrt fst fstcw fstenv fstp fstsw fsub fsubp fsubr fsubrp ftst fucom fucomi fucomip fucomp fucompp fxam fxch fxtract fyl2x fyl2xp1 hlt ibts icebp idiv imul in inc incbin insb insd insw int int01 int1 int03 int3 into invd invpcid invlpg invlpga iret iretd iretq iretw jcxz jecxz jrcxz jmp jmpe lahf lar lds lea leave les lfence lfs lgdt lgs lidt lldt lmsw loadall loadall286 lodsb lodsd lodsq lodsw loop loope loopne loopnz loopz lsl lss ltr mfence monitor mov movd movq movsb movsd movsq movsw movsx movsxd movzx mul mwait neg nop not or out outsb outsd outsw packssdw packsswb packuswb paddb paddd paddsb paddsiw paddsw paddusb paddusw paddw pand pandn pause paveb pavgusb pcmpeqb pcmpeqd pcmpeqw pcmpgtb pcmpgtd pcmpgtw pdistib pf2id pfacc pfadd pfcmpeq pfcmpge pfcmpgt pfmax pfmin pfmul pfrcp pfrcpit1 pfrcpit2 pfrsqit1 pfrsqrt pfsub pfsubr pi2fd pmachriw pmaddwd pmagw pmulhriw pmulhrwa pmulhrwc pmulhw pmullw pmvgezb pmvlzb pmvnzb pmvzb pop popa popad popaw popf popfd popfq popfw por prefetch prefetchw pslld psllq psllw psrad psraw psrld psrlq psrlw psubb psubd psubsb psubsiw psubsw psubusb psubusw psubw punpckhbw punpckhdq punpckhwd punpcklbw punpckldq punpcklwd push pusha pushad pushaw pushf pushfd pushfq pushfw pxor rcl rcr rdshr rdmsr rdpmc rdtsc rdtscp ret retf retn rol ror rdm rsdc rsldt rsm rsts sahf sal salc sar sbb scasb scasd scasq scasw sfence sgdt shl shld shr shrd sidt sldt skinit smi smint smintold smsw stc std sti stosb stosd stosq stosw str sub svdc svldt svts swapgs syscall sysenter sysexit sysret test ud0 ud1 ud2b ud2 ud2a umov verr verw fwait wbinvd wrshr wrmsr xadd xbts xchg xlatb xlat xor cmove cmovz cmovne cmovnz cmova cmovnbe cmovae cmovnb cmovb cmovnae cmovbe cmovna cmovg cmovnle cmovge cmovnl cmovl cmovnge cmovle cmovng cmovc cmovnc cmovo cmovno cmovs cmovns cmovp cmovpe cmovnp cmovpo je jz jne jnz ja jnbe jae jnb jb jnae jbe jna jg jnle jge jnl jl jnge jle jng jc jnc jo jno js jns jpo jnp jpe jp sete setz setne setnz seta setnbe setae setnb setnc setb setnae setcset setbe setna setg setnle setge setnl setl setnge setle setng sets setns seto setno setpe setp setpo setnp addps addss andnps andps cmpeqps cmpeqss cmpleps cmpless cmpltps cmpltss cmpneqps cmpneqss cmpnleps cmpnless cmpnltps cmpnltss cmpordps cmpordss cmpunordps cmpunordss cmpps cmpss comiss cvtpi2ps cvtps2pi cvtsi2ss cvtss2si cvttps2pi cvttss2si divps divss ldmxcsr maxps maxss minps minss movaps movhps movlhps movlps movhlps movmskps movntps movss movups mulps mulss orps rcpps rcpss rsqrtps rsqrtss shufps sqrtps sqrtss stmxcsr subps subss ucomiss unpckhps unpcklps xorps fxrstor fxrstor64 fxsave fxsave64 xgetbv xsetbv xsave xsave64 xsaveopt xsaveopt64 xrstor xrstor64 prefetchnta prefetcht0 prefetcht1 prefetcht2 maskmovq movntq pavgb pavgw pextrw pinsrw pmaxsw pmaxub pminsw pminub pmovmskb pmulhuw psadbw pshufw pf2iw pfnacc pfpnacc pi2fw pswapd maskmovdqu clflush movntdq movnti movntpd movdqa movdqu movdq2q movq2dq paddq pmuludq pshufd pshufhw pshuflw pslldq psrldq psubq punpckhqdq punpcklqdq addpd addsd andnpd andpd cmpeqpd cmpeqsd cmplepd cmplesd cmpltpd cmpltsd cmpneqpd cmpneqsd cmpnlepd cmpnlesd cmpnltpd cmpnltsd cmpordpd cmpordsd cmpunordpd cmpunordsd cmppd comisd cvtdq2pd cvtdq2ps cvtpd2dq cvtpd2pi cvtpd2ps cvtpi2pd cvtps2dq cvtps2pd cvtsd2si cvtsd2ss cvtsi2sd cvtss2sd cvttpd2pi cvttpd2dq cvttps2dq cvttsd2si divpd divsd maxpd maxsd minpd minsd movapd movhpd movlpd movmskpd movupd mulpd mulsd orpd shufpd sqrtpd sqrtsd subpd subsd ucomisd unpckhpd unpcklpd xorpd addsubpd addsubps haddpd haddps hsubpd hsubps lddqu movddup movshdup movsldup clgi stgi vmcall vmclear vmfunc vmlaunch vmload vmmcall vmptrld vmptrst vmread vmresume vmrun vmsave vmwrite vmxoff vmxon invept invvpid pabsb pabsw pabsd palignr phaddw phaddd phaddsw phsubw phsubd phsubsw pmaddubsw pmulhrsw pshufb psignb psignw psignd extrq insertq movntsd movntss lzcnt blendpd blendps blendvpd blendvps dppd dpps extractps insertps movntdqa mpsadbw packusdw pblendvb pblendw pcmpeqq pextrb pextrd pextrq phminposuw pinsrb pinsrd pinsrq pmaxsb pmaxsd pmaxud pmaxuw pminsb pminsd pminud pminuw pmovsxbw pmovsxbd pmovsxbq pmovsxwd pmovsxwq pmovsxdq pmovzxbw pmovzxbd pmovzxbq pmovzxwd pmovzxwq pmovzxdq pmuldq pmulld ptest roundpd roundps roundsd roundss crc32 pcmpestri pcmpestrm pcmpistri pcmpistrm pcmpgtq popcnt getsec pfrcpv pfrsqrtv movbe aesenc aesenclast aesdec aesdeclast aesimc aeskeygenassist vaesenc vaesenclast vaesdec vaesdeclast vaesimc vaeskeygenassist vaddpd vaddps vaddsd vaddss vaddsubpd vaddsubps vandpd vandps vandnpd vandnps vblendpd vblendps vblendvpd vblendvps vbroadcastss vbroadcastsd vbroadcastf128 vcmpeq_ospd vcmpeqpd vcmplt_ospd vcmpltpd vcmple_ospd vcmplepd vcmpunord_qpd vcmpunordpd vcmpneq_uqpd vcmpneqpd vcmpnlt_uspd vcmpnltpd vcmpnle_uspd vcmpnlepd vcmpord_qpd vcmpordpd vcmpeq_uqpd vcmpnge_uspd vcmpngepd vcmpngt_uspd vcmpngtpd vcmpfalse_oqpd vcmpfalsepd vcmpneq_oqpd vcmpge_ospd vcmpgepd vcmpgt_ospd vcmpgtpd vcmptrue_uqpd vcmptruepd vcmplt_oqpd vcmple_oqpd vcmpunord_spd vcmpneq_uspd vcmpnlt_uqpd vcmpnle_uqpd vcmpord_spd vcmpeq_uspd vcmpnge_uqpd vcmpngt_uqpd vcmpfalse_ospd vcmpneq_ospd vcmpge_oqpd vcmpgt_oqpd vcmptrue_uspd vcmppd vcmpeq_osps vcmpeqps vcmplt_osps vcmpltps vcmple_osps vcmpleps vcmpunord_qps vcmpunordps vcmpneq_uqps vcmpneqps vcmpnlt_usps vcmpnltps vcmpnle_usps vcmpnleps vcmpord_qps vcmpordps vcmpeq_uqps vcmpnge_usps vcmpngeps vcmpngt_usps vcmpngtps vcmpfalse_oqps vcmpfalseps vcmpneq_oqps vcmpge_osps vcmpgeps vcmpgt_osps vcmpgtps vcmptrue_uqps vcmptrueps vcmplt_oqps vcmple_oqps vcmpunord_sps vcmpneq_usps vcmpnlt_uqps vcmpnle_uqps vcmpord_sps vcmpeq_usps vcmpnge_uqps vcmpngt_uqps vcmpfalse_osps vcmpneq_osps vcmpge_oqps vcmpgt_oqps vcmptrue_usps vcmpps vcmpeq_ossd vcmpeqsd vcmplt_ossd vcmpltsd vcmple_ossd vcmplesd vcmpunord_qsd vcmpunordsd vcmpneq_uqsd vcmpneqsd vcmpnlt_ussd vcmpnltsd vcmpnle_ussd vcmpnlesd vcmpord_qsd vcmpordsd vcmpeq_uqsd vcmpnge_ussd vcmpngesd vcmpngt_ussd vcmpngtsd vcmpfalse_oqsd vcmpfalsesd vcmpneq_oqsd vcmpge_ossd vcmpgesd vcmpgt_ossd vcmpgtsd vcmptrue_uqsd vcmptruesd vcmplt_oqsd vcmple_oqsd vcmpunord_ssd vcmpneq_ussd vcmpnlt_uqsd vcmpnle_uqsd vcmpord_ssd vcmpeq_ussd vcmpnge_uqsd vcmpngt_uqsd vcmpfalse_ossd vcmpneq_ossd vcmpge_oqsd vcmpgt_oqsd vcmptrue_ussd vcmpsd vcmpeq_osss vcmpeqss vcmplt_osss vcmpltss vcmple_osss vcmpless vcmpunord_qss vcmpunordss vcmpneq_uqss vcmpneqss vcmpnlt_usss vcmpnltss vcmpnle_usss vcmpnless vcmpord_qss vcmpordss vcmpeq_uqss vcmpnge_usss vcmpngess vcmpngt_usss vcmpngtss vcmpfalse_oqss vcmpfalsess vcmpneq_oqss vcmpge_osss vcmpgess vcmpgt_osss vcmpgtss vcmptrue_uqss vcmptruess vcmplt_oqss vcmple_oqss vcmpunord_sss vcmpneq_usss vcmpnlt_uqss vcmpnle_uqss vcmpord_sss vcmpeq_usss vcmpnge_uqss vcmpngt_uqss vcmpfalse_osss vcmpneq_osss vcmpge_oqss vcmpgt_oqss vcmptrue_usss vcmpss vcomisd vcomiss vcvtdq2pd vcvtdq2ps vcvtpd2dq vcvtpd2ps vcvtps2dq vcvtps2pd vcvtsd2si vcvtsd2ss vcvtsi2sd vcvtsi2ss vcvtss2sd vcvtss2si vcvttpd2dq vcvttps2dq vcvttsd2si vcvttss2si vdivpd vdivps vdivsd vdivss vdppd vdpps vextractf128 vextractps vhaddpd vhaddps vhsubpd vhsubps vinsertf128 vinsertps vlddqu vldqqu vldmxcsr vmaskmovdqu vmaskmovps vmaskmovpd vmaxpd vmaxps vmaxsd vmaxss vminpd vminps vminsd vminss vmovapd vmovaps vmovd vmovq vmovddup vmovdqa vmovqqa vmovdqu vmovqqu vmovhlps vmovhpd vmovhps vmovlhps vmovlpd vmovlps vmovmskpd vmovmskps vmovntdq vmovntqq vmovntdqa vmovntpd vmovntps vmovsd vmovshdup vmovsldup vmovss vmovupd vmovups vmpsadbw vmulpd vmulps vmulsd vmulss vorpd vorps vpabsb vpabsw vpabsd vpacksswb vpackssdw vpackuswb vpackusdw vpaddb vpaddw vpaddd vpaddq vpaddsb vpaddsw vpaddusb vpaddusw vpalignr vpand vpandn vpavgb vpavgw vpblendvb vpblendw vpcmpestri vpcmpestrm vpcmpistri vpcmpistrm vpcmpeqb vpcmpeqw vpcmpeqd vpcmpeqq vpcmpgtb vpcmpgtw vpcmpgtd vpcmpgtq vpermilpd vpermilps vperm2f128 vpextrb vpextrw vpextrd vpextrq vphaddw vphaddd vphaddsw vphminposuw vphsubw vphsubd vphsubsw vpinsrb vpinsrw vpinsrd vpinsrq vpmaddwd vpmaddubsw vpmaxsb vpmaxsw vpmaxsd vpmaxub vpmaxuw vpmaxud vpminsb vpminsw vpminsd vpminub vpminuw vpminud vpmovmskb vpmovsxbw vpmovsxbd vpmovsxbq vpmovsxwd vpmovsxwq vpmovsxdq vpmovzxbw vpmovzxbd vpmovzxbq vpmovzxwd vpmovzxwq vpmovzxdq vpmulhuw vpmulhrsw vpmulhw vpmullw vpmulld vpmuludq vpmuldq vpor vpsadbw vpshufb vpshufd vpshufhw vpshuflw vpsignb vpsignw vpsignd vpslldq vpsrldq vpsllw vpslld vpsllq vpsraw vpsrad vpsrlw vpsrld vpsrlq vptest vpsubb vpsubw vpsubd vpsubq vpsubsb vpsubsw vpsubusb vpsubusw vpunpckhbw vpunpckhwd vpunpckhdq vpunpckhqdq vpunpcklbw vpunpcklwd vpunpckldq vpunpcklqdq vpxor vrcpps vrcpss vrsqrtps vrsqrtss vroundpd vroundps vroundsd vroundss vshufpd vshufps vsqrtpd vsqrtps vsqrtsd vsqrtss vstmxcsr vsubpd vsubps vsubsd vsubss vtestps vtestpd vucomisd vucomiss vunpckhpd vunpckhps vunpcklpd vunpcklps vxorpd vxorps vzeroall vzeroupper pclmullqlqdq pclmulhqlqdq pclmullqhqdq pclmulhqhqdq pclmulqdq vpclmullqlqdq vpclmulhqlqdq vpclmullqhqdq vpclmulhqhqdq vpclmulqdq vfmadd132ps vfmadd132pd vfmadd312ps vfmadd312pd vfmadd213ps vfmadd213pd vfmadd123ps vfmadd123pd vfmadd231ps vfmadd231pd vfmadd321ps vfmadd321pd vfmaddsub132ps vfmaddsub132pd vfmaddsub312ps vfmaddsub312pd vfmaddsub213ps vfmaddsub213pd vfmaddsub123ps vfmaddsub123pd vfmaddsub231ps vfmaddsub231pd vfmaddsub321ps vfmaddsub321pd vfmsub132ps vfmsub132pd vfmsub312ps vfmsub312pd vfmsub213ps vfmsub213pd vfmsub123ps vfmsub123pd vfmsub231ps vfmsub231pd vfmsub321ps vfmsub321pd vfmsubadd132ps vfmsubadd132pd vfmsubadd312ps vfmsubadd312pd vfmsubadd213ps vfmsubadd213pd vfmsubadd123ps vfmsubadd123pd vfmsubadd231ps vfmsubadd231pd vfmsubadd321ps vfmsubadd321pd vfnmadd132ps vfnmadd132pd vfnmadd312ps vfnmadd312pd vfnmadd213ps vfnmadd213pd vfnmadd123ps vfnmadd123pd vfnmadd231ps vfnmadd231pd vfnmadd321ps vfnmadd321pd vfnmsub132ps vfnmsub132pd vfnmsub312ps vfnmsub312pd vfnmsub213ps vfnmsub213pd vfnmsub123ps vfnmsub123pd vfnmsub231ps vfnmsub231pd vfnmsub321ps vfnmsub321pd vfmadd132ss vfmadd132sd vfmadd312ss vfmadd312sd vfmadd213ss vfmadd213sd vfmadd123ss vfmadd123sd vfmadd231ss vfmadd231sd vfmadd321ss vfmadd321sd vfmsub132ss vfmsub132sd vfmsub312ss vfmsub312sd vfmsub213ss vfmsub213sd vfmsub123ss vfmsub123sd vfmsub231ss vfmsub231sd vfmsub321ss vfmsub321sd vfnmadd132ss vfnmadd132sd vfnmadd312ss vfnmadd312sd vfnmadd213ss vfnmadd213sd vfnmadd123ss vfnmadd123sd vfnmadd231ss vfnmadd231sd vfnmadd321ss vfnmadd321sd vfnmsub132ss vfnmsub132sd vfnmsub312ss vfnmsub312sd vfnmsub213ss vfnmsub213sd vfnmsub123ss vfnmsub123sd vfnmsub231ss vfnmsub231sd vfnmsub321ss vfnmsub321sd rdfsbase rdgsbase rdrand wrfsbase wrgsbase vcvtph2ps vcvtps2ph adcx adox rdseed clac stac xstore xcryptecb xcryptcbc xcryptctr xcryptcfb xcryptofb montmul xsha1 xsha256 llwpcb slwpcb lwpval lwpins vfmaddpd vfmaddps vfmaddsd vfmaddss vfmaddsubpd vfmaddsubps vfmsubaddpd vfmsubaddps vfmsubpd vfmsubps vfmsubsd vfmsubss vfnmaddpd vfnmaddps vfnmaddsd vfnmaddss vfnmsubpd vfnmsubps vfnmsubsd vfnmsubss vfrczpd vfrczps vfrczsd vfrczss vpcmov vpcomb vpcomd vpcomq vpcomub vpcomud vpcomuq vpcomuw vpcomw vphaddbd vphaddbq vphaddbw vphadddq vphaddubd vphaddubq vphaddubw vphaddudq vphadduwd vphadduwq vphaddwd vphaddwq vphsubbw vphsubdq vphsubwd vpmacsdd vpmacsdqh vpmacsdql vpmacssdd vpmacssdqh vpmacssdql vpmacsswd vpmacssww vpmacswd vpmacsww vpmadcsswd vpmadcswd vpperm vprotb vprotd vprotq vprotw vpshab vpshad vpshaq vpshaw vpshlb vpshld vpshlq vpshlw vbroadcasti128 vpblendd vpbroadcastb vpbroadcastw vpbroadcastd vpbroadcastq vpermd vpermpd vpermps vpermq vperm2i128 vextracti128 vinserti128 vpmaskmovd vpmaskmovq vpsllvd vpsllvq vpsravd vpsrlvd vpsrlvq vgatherdpd vgatherqpd vgatherdps vgatherqps vpgatherdd vpgatherqd vpgatherdq vpgatherqq xabort xbegin xend xtest andn bextr blci blcic blsi blsic blcfill blsfill blcmsk blsmsk blsr blcs bzhi mulx pdep pext rorx sarx shlx shrx tzcnt tzmsk t1mskc valignd valignq vblendmpd vblendmps vbroadcastf32x4 vbroadcastf64x4 vbroadcasti32x4 vbroadcasti64x4 vcompresspd vcompressps vcvtpd2udq vcvtps2udq vcvtsd2usi vcvtss2usi vcvttpd2udq vcvttps2udq vcvttsd2usi vcvttss2usi vcvtudq2pd vcvtudq2ps vcvtusi2sd vcvtusi2ss vexpandpd vexpandps vextractf32x4 vextractf64x4 vextracti32x4 vextracti64x4 vfixupimmpd vfixupimmps vfixupimmsd vfixupimmss vgetexppd vgetexpps vgetexpsd vgetexpss vgetmantpd vgetmantps vgetmantsd vgetmantss vinsertf32x4 vinsertf64x4 vinserti32x4 vinserti64x4 vmovdqa32 vmovdqa64 vmovdqu32 vmovdqu64 vpabsq vpandd vpandnd vpandnq vpandq vpblendmd vpblendmq vpcmpltd vpcmpled vpcmpneqd vpcmpnltd vpcmpnled vpcmpd vpcmpltq vpcmpleq vpcmpneqq vpcmpnltq vpcmpnleq vpcmpq vpcmpequd vpcmpltud vpcmpleud vpcmpnequd vpcmpnltud vpcmpnleud vpcmpud vpcmpequq vpcmpltuq vpcmpleuq vpcmpnequq vpcmpnltuq vpcmpnleuq vpcmpuq vpcompressd vpcompressq vpermi2d vpermi2pd vpermi2ps vpermi2q vpermt2d vpermt2pd vpermt2ps vpermt2q vpexpandd vpexpandq vpmaxsq vpmaxuq vpminsq vpminuq vpmovdb vpmovdw vpmovqb vpmovqd vpmovqw vpmovsdb vpmovsdw vpmovsqb vpmovsqd vpmovsqw vpmovusdb vpmovusdw vpmovusqb vpmovusqd vpmovusqw vpord vporq vprold vprolq vprolvd vprolvq vprord vprorq vprorvd vprorvq vpscatterdd vpscatterdq vpscatterqd vpscatterqq vpsraq vpsravq vpternlogd vpternlogq vptestmd vptestmq vptestnmd vptestnmq vpxord vpxorq vrcp14pd vrcp14ps vrcp14sd vrcp14ss vrndscalepd vrndscaleps vrndscalesd vrndscaless vrsqrt14pd vrsqrt14ps vrsqrt14sd vrsqrt14ss vscalefpd vscalefps vscalefsd vscalefss vscatterdpd vscatterdps vscatterqpd vscatterqps vshuff32x4 vshuff64x2 vshufi32x4 vshufi64x2 kandnw kandw kmovw knotw kortestw korw kshiftlw kshiftrw kunpckbw kxnorw kxorw vpbroadcastmb2q vpbroadcastmw2d vpconflictd vpconflictq vplzcntd vplzcntq vexp2pd vexp2ps vrcp28pd vrcp28ps vrcp28sd vrcp28ss vrsqrt28pd vrsqrt28ps vrsqrt28sd vrsqrt28ss vgatherpf0dpd vgatherpf0dps vgatherpf0qpd vgatherpf0qps vgatherpf1dpd vgatherpf1dps vgatherpf1qpd vgatherpf1qps vscatterpf0dpd vscatterpf0dps vscatterpf0qpd vscatterpf0qps vscatterpf1dpd vscatterpf1dps vscatterpf1qpd vscatterpf1qps prefetchwt1 bndmk bndcl bndcu bndcn bndmov bndldx bndstx sha1rnds4 sha1nexte sha1msg1 sha1msg2 sha256rnds2 sha256msg1 sha256msg2 hint_nop0 hint_nop1 hint_nop2 hint_nop3 hint_nop4 hint_nop5 hint_nop6 hint_nop7 hint_nop8 hint_nop9 hint_nop10 hint_nop11 hint_nop12 hint_nop13 hint_nop14 hint_nop15 hint_nop16 hint_nop17 hint_nop18 hint_nop19 hint_nop20 hint_nop21 hint_nop22 hint_nop23 hint_nop24 hint_nop25 hint_nop26 hint_nop27 hint_nop28 hint_nop29 hint_nop30 hint_nop31 hint_nop32 hint_nop33 hint_nop34 hint_nop35 hint_nop36 hint_nop37 hint_nop38 hint_nop39 hint_nop40 hint_nop41 hint_nop42 hint_nop43 hint_nop44 hint_nop45 hint_nop46 hint_nop47 hint_nop48 hint_nop49 hint_nop50 hint_nop51 hint_nop52 hint_nop53 hint_nop54 hint_nop55 hint_nop56 hint_nop57 hint_nop58 hint_nop59 hint_nop60 hint_nop61 hint_nop62 hint_nop63",built_in:"ip eip rip al ah bl bh cl ch dl dh sil dil bpl spl r8b r9b r10b r11b r12b r13b r14b r15b ax bx cx dx si di bp sp r8w r9w r10w r11w r12w r13w r14w r15w eax ebx ecx edx esi edi ebp esp eip r8d r9d r10d r11d r12d r13d r14d r15d rax rbx rcx rdx rsi rdi rbp rsp r8 r9 r10 r11 r12 r13 r14 r15 cs ds es fs gs ss st st0 st1 st2 st3 st4 st5 st6 st7 mm0 mm1 mm2 mm3 mm4 mm5 mm6 mm7 xmm0 xmm1 xmm2 xmm3 xmm4 xmm5 xmm6 xmm7 xmm8 xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15 xmm16 xmm17 xmm18 xmm19 xmm20 xmm21 xmm22 xmm23 xmm24 xmm25 xmm26 xmm27 xmm28 xmm29 xmm30 xmm31 ymm0 ymm1 ymm2 ymm3 ymm4 ymm5 ymm6 ymm7 ymm8 ymm9 ymm10 ymm11 ymm12 ymm13 ymm14 ymm15 ymm16 ymm17 ymm18 ymm19 ymm20 ymm21 ymm22 ymm23 ymm24 ymm25 ymm26 ymm27 ymm28 ymm29 ymm30 ymm31 zmm0 zmm1 zmm2 zmm3 zmm4 zmm5 zmm6 zmm7 zmm8 zmm9 zmm10 zmm11 zmm12 zmm13 zmm14 zmm15 zmm16 zmm17 zmm18 zmm19 zmm20 zmm21 zmm22 zmm23 zmm24 zmm25 zmm26 zmm27 zmm28 zmm29 zmm30 zmm31 k0 k1 k2 k3 k4 k5 k6 k7 bnd0 bnd1 bnd2 bnd3 cr0 cr1 cr2 cr3 cr4 cr8 dr0 dr1 dr2 dr3 dr8 tr3 tr4 tr5 tr6 tr7 r0 r1 r2 r3 r4 r5 r6 r7 r0b r1b r2b r3b r4b r5b r6b r7b r0w r1w r2w r3w r4w r5w r6w r7w r0d r1d r2d r3d r4d r5d r6d r7d r0h r1h r2h r3h r0l r1l r2l r3l r4l r5l r6l r7l r8l r9l r10l r11l r12l r13l r14l r15l db dw dd dq dt ddq do dy dz resb resw resd resq rest resdq reso resy resz incbin equ times byte word dword qword nosplit rel abs seg wrt strict near far a32 ptr",meta:"%define %xdefine %+ %undef %defstr %deftok %assign %strcat %strlen %substr %rotate %elif %else %endif %if %ifmacro %ifctx %ifidn %ifidni %ifid %ifnum %ifstr %iftoken %ifempty %ifenv %error %warning %fatal %rep %endrep %include %push %pop %repl %pathsearch %depend %use %arg %stacksize %local %line %comment %endcomment .nolist __FILE__ __LINE__ __SECT__ __BITS__ __OUTPUT_FORMAT__ __DATE__ __TIME__ __DATE_NUM__ __TIME_NUM__ __UTC_DATE__ __UTC_TIME__ __UTC_DATE_NUM__ __UTC_TIME_NUM__ __PASS__ struc endstruc istruc at iend align alignb sectalign daz nodaz up down zero default option assume public bits use16 use32 use64 default section segment absolute extern global common cpu float __utf16__ __utf16le__ __utf16be__ __utf32__ __utf32le__ __utf32be__ __float8__ __float16__ __float32__ __float64__ __float80m__ __float80e__ __float128l__ __float128h__ __Infinity__ __QNaN__ __SNaN__ Inf NaN QNaN SNaN float8 float16 float32 float64 float80m float80e float128l float128h __FLOAT_DAZ__ __FLOAT_ROUND__ __FLOAT__"},contains:[s.COMMENT(";","$",{relevance:0}),{className:"number",variants:[{begin:"\\b(?:([0-9][0-9_]*)?\\.[0-9_]*(?:[eE][+-]?[0-9_]+)?|(0[Xx])?[0-9][0-9_]*\\.?[0-9_]*(?:[pP](?:[+-]?[0-9_]+)?)?)\\b",relevance:0},{begin:"\\$[0-9][0-9A-Fa-f]*",relevance:0},{begin:"\\b(?:[0-9A-Fa-f][0-9A-Fa-f_]*[Hh]|[0-9][0-9_]*[DdTt]?|[0-7][0-7_]*[QqOo]|[0-1][0-1_]*[BbYy])\\b"},{begin:"\\b(?:0[Xx][0-9A-Fa-f_]+|0[DdTt][0-9_]+|0[QqOo][0-7_]+|0[BbYy][0-1_]+)\\b"}]},s.QUOTE_STRING_MODE,{className:"string",variants:[{begin:"'",end:"[^\\\\]'"},{begin:"`",end:"[^\\\\]`"}],relevance:0},{className:"symbol",variants:[{begin:"^\\s*[A-Za-z._?][A-Za-z0-9_$#@~.?]*(:|\\s+label)"},{begin:"^\\s*%%[A-Za-z0-9_$#@~.?]*:"}],relevance:0},{className:"subst",begin:"%[0-9]+",relevance:0},{className:"subst",begin:"%!S+",relevance:0},{className:"meta",begin:/^\s*\.[\w_-]+/}]}}}()); \ No newline at end of file diff --git a/docs/images/anisotropy_00.png b/docs/images/anisotropy_00.png new file mode 100644 index 0000000000..cec213253b Binary files /dev/null and b/docs/images/anisotropy_00.png differ diff --git a/docs/images/anisotropy_01.png b/docs/images/anisotropy_01.png new file mode 100644 index 0000000000..ac64b3836d Binary files /dev/null and b/docs/images/anisotropy_01.png differ diff --git a/docs/images/anisotropy_02.png b/docs/images/anisotropy_02.png new file mode 100644 index 0000000000..31f0515b9c Binary files /dev/null and b/docs/images/anisotropy_02.png differ diff --git a/docs/images/anisotropy_03.png b/docs/images/anisotropy_03.png new file mode 100644 index 0000000000..76a1d283cd Binary files /dev/null and b/docs/images/anisotropy_03.png differ diff --git a/docs/images/anisotropy_04.png b/docs/images/anisotropy_04.png new file mode 100644 index 0000000000..035561a0ff Binary files /dev/null and b/docs/images/anisotropy_04.png differ diff --git a/docs/images/anisotropy_05.png b/docs/images/anisotropy_05.png new file mode 100644 index 0000000000..8e8a92be55 Binary files /dev/null and b/docs/images/anisotropy_05.png differ diff --git a/docs/images/anisotropy_06.png b/docs/images/anisotropy_06.png new file mode 100644 index 0000000000..11b59e8b21 Binary files /dev/null and b/docs/images/anisotropy_06.png differ diff --git a/docs/images/anisotropy_07.png b/docs/images/anisotropy_07.png new file mode 100644 index 0000000000..dceda6ff50 Binary files /dev/null and b/docs/images/anisotropy_07.png differ diff --git a/docs/images/anisotropy_08.png b/docs/images/anisotropy_08.png new file mode 100644 index 0000000000..8d3caba1d0 Binary files /dev/null and b/docs/images/anisotropy_08.png differ diff --git a/docs/images/anisotropy_09.png b/docs/images/anisotropy_09.png new file mode 100644 index 0000000000..9efd263f97 Binary files /dev/null and b/docs/images/anisotropy_09.png differ diff --git a/docs/images/anisotropy_10.png b/docs/images/anisotropy_10.png new file mode 100644 index 0000000000..3aec26fe3c Binary files /dev/null and b/docs/images/anisotropy_10.png differ diff --git a/docs/images/branching.png b/docs/images/branching.png new file mode 100644 index 0000000000..005a7ccf62 Binary files /dev/null and b/docs/images/branching.png differ diff --git a/docs/images/chart_sh_cos_thera_approx.png b/docs/images/chart_sh_cos_thera_approx.png new file mode 100644 index 0000000000..c3bb69c015 Binary files /dev/null and b/docs/images/chart_sh_cos_thera_approx.png differ diff --git a/docs/images/clear_coat_00.png b/docs/images/clear_coat_00.png new file mode 100644 index 0000000000..5499ef4c8e Binary files /dev/null and b/docs/images/clear_coat_00.png differ diff --git a/docs/images/clear_coat_01.png b/docs/images/clear_coat_01.png new file mode 100644 index 0000000000..f50b45bef9 Binary files /dev/null and b/docs/images/clear_coat_01.png differ diff --git a/docs/images/clear_coat_02.png b/docs/images/clear_coat_02.png new file mode 100644 index 0000000000..e887f9752f Binary files /dev/null and b/docs/images/clear_coat_02.png differ diff --git a/docs/images/clear_coat_03.png b/docs/images/clear_coat_03.png new file mode 100644 index 0000000000..a2335e94b4 Binary files /dev/null and b/docs/images/clear_coat_03.png differ diff --git a/docs/images/clear_coat_04.png b/docs/images/clear_coat_04.png new file mode 100644 index 0000000000..98ca7eb172 Binary files /dev/null and b/docs/images/clear_coat_04.png differ diff --git a/docs/images/clear_coat_05.png b/docs/images/clear_coat_05.png new file mode 100644 index 0000000000..48e2e813ee Binary files /dev/null and b/docs/images/clear_coat_05.png differ diff --git a/docs/images/clear_coat_06.png b/docs/images/clear_coat_06.png new file mode 100644 index 0000000000..a10a80d72f Binary files /dev/null and b/docs/images/clear_coat_06.png differ diff --git a/docs/images/clear_coat_07.png b/docs/images/clear_coat_07.png new file mode 100644 index 0000000000..bf24cdb3de Binary files /dev/null and b/docs/images/clear_coat_07.png differ diff --git a/docs/images/clear_coat_08.png b/docs/images/clear_coat_08.png new file mode 100644 index 0000000000..960b03acba Binary files /dev/null and b/docs/images/clear_coat_08.png differ diff --git a/docs/images/clear_coat_09.png b/docs/images/clear_coat_09.png new file mode 100644 index 0000000000..d46cdcb643 Binary files /dev/null and b/docs/images/clear_coat_09.png differ diff --git a/docs/images/clear_coat_10.png b/docs/images/clear_coat_10.png new file mode 100644 index 0000000000..2b57da45cb Binary files /dev/null and b/docs/images/clear_coat_10.png differ diff --git a/docs/images/clear_coat_roughness_00.png b/docs/images/clear_coat_roughness_00.png new file mode 100644 index 0000000000..54a0ff359c Binary files /dev/null and b/docs/images/clear_coat_roughness_00.png differ diff --git a/docs/images/clear_coat_roughness_01.png b/docs/images/clear_coat_roughness_01.png new file mode 100644 index 0000000000..8796bdb82b Binary files /dev/null and b/docs/images/clear_coat_roughness_01.png differ diff --git a/docs/images/clear_coat_roughness_02.png b/docs/images/clear_coat_roughness_02.png new file mode 100644 index 0000000000..c9389e7ef7 Binary files /dev/null and b/docs/images/clear_coat_roughness_02.png differ diff --git a/docs/images/clear_coat_roughness_03.png b/docs/images/clear_coat_roughness_03.png new file mode 100644 index 0000000000..85eb0cd878 Binary files /dev/null and b/docs/images/clear_coat_roughness_03.png differ diff --git a/docs/images/clear_coat_roughness_04.png b/docs/images/clear_coat_roughness_04.png new file mode 100644 index 0000000000..145059a091 Binary files /dev/null and b/docs/images/clear_coat_roughness_04.png differ diff --git a/docs/images/clear_coat_roughness_05.png b/docs/images/clear_coat_roughness_05.png new file mode 100644 index 0000000000..7630af3048 Binary files /dev/null and b/docs/images/clear_coat_roughness_05.png differ diff --git a/docs/images/clear_coat_roughness_06.png b/docs/images/clear_coat_roughness_06.png new file mode 100644 index 0000000000..992aad1134 Binary files /dev/null and b/docs/images/clear_coat_roughness_06.png differ diff --git a/docs/images/clear_coat_roughness_07.png b/docs/images/clear_coat_roughness_07.png new file mode 100644 index 0000000000..2c7c2736a4 Binary files /dev/null and b/docs/images/clear_coat_roughness_07.png differ diff --git a/docs/images/clear_coat_roughness_08.png b/docs/images/clear_coat_roughness_08.png new file mode 100644 index 0000000000..1004176e57 Binary files /dev/null and b/docs/images/clear_coat_roughness_08.png differ diff --git a/docs/images/clear_coat_roughness_09.png b/docs/images/clear_coat_roughness_09.png new file mode 100644 index 0000000000..0ae8675961 Binary files /dev/null and b/docs/images/clear_coat_roughness_09.png differ diff --git a/docs/images/clear_coat_roughness_10.png b/docs/images/clear_coat_roughness_10.png new file mode 100644 index 0000000000..e68af2a06a Binary files /dev/null and b/docs/images/clear_coat_roughness_10.png differ diff --git a/docs/images/diagram_brdf_dielectric_conductor.png b/docs/images/diagram_brdf_dielectric_conductor.png new file mode 100644 index 0000000000..ec45e9ca99 Binary files /dev/null and b/docs/images/diagram_brdf_dielectric_conductor.png differ diff --git a/docs/images/diagram_clear_coat.png b/docs/images/diagram_clear_coat.png new file mode 100644 index 0000000000..fc1c1505a6 Binary files /dev/null and b/docs/images/diagram_clear_coat.png differ diff --git a/docs/images/diagram_color_temperature_cct.png b/docs/images/diagram_color_temperature_cct.png new file mode 100644 index 0000000000..e8f936bdef Binary files /dev/null and b/docs/images/diagram_color_temperature_cct.png differ diff --git a/docs/images/diagram_color_temperature_cct_clamped.png b/docs/images/diagram_color_temperature_cct_clamped.png new file mode 100644 index 0000000000..a2a1ec7b9e Binary files /dev/null and b/docs/images/diagram_color_temperature_cct_clamped.png differ diff --git a/docs/images/diagram_color_temperature_cie.png b/docs/images/diagram_color_temperature_cie.png new file mode 100644 index 0000000000..d661f7b38e Binary files /dev/null and b/docs/images/diagram_color_temperature_cie.png differ diff --git a/docs/images/diagram_directional_light.png b/docs/images/diagram_directional_light.png new file mode 100644 index 0000000000..175772c283 Binary files /dev/null and b/docs/images/diagram_directional_light.png differ diff --git a/docs/images/diagram_fr_fd.png b/docs/images/diagram_fr_fd.png new file mode 100644 index 0000000000..5794760e5b Binary files /dev/null and b/docs/images/diagram_fr_fd.png differ diff --git a/docs/images/diagram_froxels1.png b/docs/images/diagram_froxels1.png new file mode 100644 index 0000000000..33486477bb Binary files /dev/null and b/docs/images/diagram_froxels1.png differ diff --git a/docs/images/diagram_froxels2.png b/docs/images/diagram_froxels2.png new file mode 100644 index 0000000000..7f7bccf5d7 Binary files /dev/null and b/docs/images/diagram_froxels2.png differ diff --git a/docs/images/diagram_froxels3.png b/docs/images/diagram_froxels3.png new file mode 100644 index 0000000000..695a89a3b2 Binary files /dev/null and b/docs/images/diagram_froxels3.png differ diff --git a/docs/images/diagram_lambert_vs_disney.png b/docs/images/diagram_lambert_vs_disney.png new file mode 100644 index 0000000000..2cef9ab17d Binary files /dev/null and b/docs/images/diagram_lambert_vs_disney.png differ diff --git a/docs/images/diagram_macrosurface.png b/docs/images/diagram_macrosurface.png new file mode 100644 index 0000000000..1d39160c46 Binary files /dev/null and b/docs/images/diagram_macrosurface.png differ diff --git a/docs/images/diagram_micro_vs_macro.png b/docs/images/diagram_micro_vs_macro.png new file mode 100644 index 0000000000..13a9629684 Binary files /dev/null and b/docs/images/diagram_micro_vs_macro.png differ diff --git a/docs/images/diagram_microfacet.png b/docs/images/diagram_microfacet.png new file mode 100644 index 0000000000..2311e42981 Binary files /dev/null and b/docs/images/diagram_microfacet.png differ diff --git a/docs/images/diagram_planckian_locus.png b/docs/images/diagram_planckian_locus.png new file mode 100644 index 0000000000..bea8ae6c22 Binary files /dev/null and b/docs/images/diagram_planckian_locus.png differ diff --git a/docs/images/diagram_point_light.png b/docs/images/diagram_point_light.png new file mode 100644 index 0000000000..44ca40189f Binary files /dev/null and b/docs/images/diagram_point_light.png differ diff --git a/docs/images/diagram_reflectance.png b/docs/images/diagram_reflectance.png new file mode 100644 index 0000000000..ca4cf781ba Binary files /dev/null and b/docs/images/diagram_reflectance.png differ diff --git a/docs/images/diagram_roughness.png b/docs/images/diagram_roughness.png new file mode 100644 index 0000000000..a428bff917 Binary files /dev/null and b/docs/images/diagram_roughness.png differ diff --git a/docs/images/diagram_scattering.png b/docs/images/diagram_scattering.png new file mode 100644 index 0000000000..d8dbd0c966 Binary files /dev/null and b/docs/images/diagram_scattering.png differ diff --git a/docs/images/diagram_shadowing_masking.png b/docs/images/diagram_shadowing_masking.png new file mode 100644 index 0000000000..78b37d7118 Binary files /dev/null and b/docs/images/diagram_shadowing_masking.png differ diff --git a/docs/images/diagram_single_vs_multi_scatter.png b/docs/images/diagram_single_vs_multi_scatter.png new file mode 100644 index 0000000000..81bfeb5c93 Binary files /dev/null and b/docs/images/diagram_single_vs_multi_scatter.png differ diff --git a/docs/images/diagram_spot_light.png b/docs/images/diagram_spot_light.png new file mode 100644 index 0000000000..7aa74421d6 Binary files /dev/null and b/docs/images/diagram_spot_light.png differ diff --git a/docs/images/filament_logo.png b/docs/images/filament_logo.png new file mode 100644 index 0000000000..58aacf78ad Binary files /dev/null and b/docs/images/filament_logo.png differ diff --git a/docs/images/filament_logo_small.png b/docs/images/filament_logo_small.png new file mode 100644 index 0000000000..710976c516 Binary files /dev/null and b/docs/images/filament_logo_small.png differ diff --git a/docs/images/framegraph.png b/docs/images/framegraph.png new file mode 100644 index 0000000000..c5da6cb186 Binary files /dev/null and b/docs/images/framegraph.png differ diff --git a/docs/images/ibl/dfg.png b/docs/images/ibl/dfg.png new file mode 100644 index 0000000000..7d1f48aa21 Binary files /dev/null and b/docs/images/ibl/dfg.png differ diff --git a/docs/images/ibl/dfg1.png b/docs/images/ibl/dfg1.png new file mode 100644 index 0000000000..599b0ced0b Binary files /dev/null and b/docs/images/ibl/dfg1.png differ diff --git a/docs/images/ibl/dfg1_approx.png b/docs/images/ibl/dfg1_approx.png new file mode 100644 index 0000000000..59855e199e Binary files /dev/null and b/docs/images/ibl/dfg1_approx.png differ diff --git a/docs/images/ibl/dfg2.png b/docs/images/ibl/dfg2.png new file mode 100644 index 0000000000..2702a94233 Binary files /dev/null and b/docs/images/ibl/dfg2.png differ diff --git a/docs/images/ibl/dfg2_approx.png b/docs/images/ibl/dfg2_approx.png new file mode 100644 index 0000000000..64848da0c1 Binary files /dev/null and b/docs/images/ibl/dfg2_approx.png differ diff --git a/docs/images/ibl/dfg_approx.png b/docs/images/ibl/dfg_approx.png new file mode 100644 index 0000000000..d3f17555c9 Binary files /dev/null and b/docs/images/ibl/dfg_approx.png differ diff --git a/docs/images/ibl/dfg_cloth.png b/docs/images/ibl/dfg_cloth.png new file mode 100644 index 0000000000..1a05807775 Binary files /dev/null and b/docs/images/ibl/dfg_cloth.png differ diff --git a/docs/images/ibl/ibl_irradiance.png b/docs/images/ibl/ibl_irradiance.png new file mode 100644 index 0000000000..3718d42077 Binary files /dev/null and b/docs/images/ibl/ibl_irradiance.png differ diff --git a/docs/images/ibl/ibl_irradiance_sh2.png b/docs/images/ibl/ibl_irradiance_sh2.png new file mode 100644 index 0000000000..8ec7bc15e2 Binary files /dev/null and b/docs/images/ibl/ibl_irradiance_sh2.png differ diff --git a/docs/images/ibl/ibl_irradiance_sh3.png b/docs/images/ibl/ibl_irradiance_sh3.png new file mode 100644 index 0000000000..703a60aadd Binary files /dev/null and b/docs/images/ibl/ibl_irradiance_sh3.png differ diff --git a/docs/images/ibl/ibl_no_mipmaping.png b/docs/images/ibl/ibl_no_mipmaping.png new file mode 100644 index 0000000000..e9f43777a2 Binary files /dev/null and b/docs/images/ibl/ibl_no_mipmaping.png differ diff --git a/docs/images/ibl/ibl_prefilter_vs_reference.png b/docs/images/ibl/ibl_prefilter_vs_reference.png new file mode 100644 index 0000000000..87d7e03d7b Binary files /dev/null and b/docs/images/ibl/ibl_prefilter_vs_reference.png differ diff --git a/docs/images/ibl/ibl_river_roughness_m0.png b/docs/images/ibl/ibl_river_roughness_m0.png new file mode 100644 index 0000000000..f029919a45 Binary files /dev/null and b/docs/images/ibl/ibl_river_roughness_m0.png differ diff --git a/docs/images/ibl/ibl_river_roughness_m1.png b/docs/images/ibl/ibl_river_roughness_m1.png new file mode 100644 index 0000000000..c0156e8a88 Binary files /dev/null and b/docs/images/ibl/ibl_river_roughness_m1.png differ diff --git a/docs/images/ibl/ibl_river_roughness_m2.png b/docs/images/ibl/ibl_river_roughness_m2.png new file mode 100644 index 0000000000..0efd9f579f Binary files /dev/null and b/docs/images/ibl/ibl_river_roughness_m2.png differ diff --git a/docs/images/ibl/ibl_river_roughness_m3.png b/docs/images/ibl/ibl_river_roughness_m3.png new file mode 100644 index 0000000000..f1b42a63cd Binary files /dev/null and b/docs/images/ibl/ibl_river_roughness_m3.png differ diff --git a/docs/images/ibl/ibl_river_roughness_m4.png b/docs/images/ibl/ibl_river_roughness_m4.png new file mode 100644 index 0000000000..0412914013 Binary files /dev/null and b/docs/images/ibl/ibl_river_roughness_m4.png differ diff --git a/docs/images/ibl/ibl_river_roughness_m5.png b/docs/images/ibl/ibl_river_roughness_m5.png new file mode 100644 index 0000000000..57c75c8b08 Binary files /dev/null and b/docs/images/ibl/ibl_river_roughness_m5.png differ diff --git a/docs/images/ibl/ibl_river_roughness_m6.png b/docs/images/ibl/ibl_river_roughness_m6.png new file mode 100644 index 0000000000..0a4d29f597 Binary files /dev/null and b/docs/images/ibl/ibl_river_roughness_m6.png differ diff --git a/docs/images/ibl/ibl_river_roughness_m7.png b/docs/images/ibl/ibl_river_roughness_m7.png new file mode 100644 index 0000000000..d3b7123712 Binary files /dev/null and b/docs/images/ibl/ibl_river_roughness_m7.png differ diff --git a/docs/images/ibl/ibl_stretchy_reflections_error.png b/docs/images/ibl/ibl_stretchy_reflections_error.png new file mode 100644 index 0000000000..c8e2bb3948 Binary files /dev/null and b/docs/images/ibl/ibl_stretchy_reflections_error.png differ diff --git a/docs/images/ibl/ibl_trilinear_0.png b/docs/images/ibl/ibl_trilinear_0.png new file mode 100644 index 0000000000..39ca22e840 Binary files /dev/null and b/docs/images/ibl/ibl_trilinear_0.png differ diff --git a/docs/images/ibl/ibl_trilinear_1.png b/docs/images/ibl/ibl_trilinear_1.png new file mode 100644 index 0000000000..5ce39b0c31 Binary files /dev/null and b/docs/images/ibl/ibl_trilinear_1.png differ diff --git a/docs/images/ibl/ibl_visualization.jpg b/docs/images/ibl/ibl_visualization.jpg new file mode 100644 index 0000000000..7fe141a3cd Binary files /dev/null and b/docs/images/ibl/ibl_visualization.jpg differ diff --git a/docs/images/image_filtered_1.png b/docs/images/image_filtered_1.png new file mode 100644 index 0000000000..f9f1fd82f1 Binary files /dev/null and b/docs/images/image_filtered_1.png differ diff --git a/docs/images/image_filtered_2.png b/docs/images/image_filtered_2.png new file mode 100644 index 0000000000..0e42263432 Binary files /dev/null and b/docs/images/image_filtered_2.png differ diff --git a/docs/images/image_filtered_3.png b/docs/images/image_filtered_3.png new file mode 100644 index 0000000000..d19e3257e7 Binary files /dev/null and b/docs/images/image_filtered_3.png differ diff --git a/docs/images/image_filtered_4.png b/docs/images/image_filtered_4.png new file mode 100644 index 0000000000..1054dfcbef Binary files /dev/null and b/docs/images/image_filtered_4.png differ diff --git a/docs/images/image_fis_1024.png b/docs/images/image_fis_1024.png new file mode 100644 index 0000000000..25bb443601 Binary files /dev/null and b/docs/images/image_fis_1024.png differ diff --git a/docs/images/image_fis_32.png b/docs/images/image_fis_32.png new file mode 100644 index 0000000000..4a73da0465 Binary files /dev/null and b/docs/images/image_fis_32.png differ diff --git a/docs/images/image_is_1024.png b/docs/images/image_is_1024.png new file mode 100644 index 0000000000..b2ba367cb8 Binary files /dev/null and b/docs/images/image_is_1024.png differ diff --git a/docs/images/image_is_32.png b/docs/images/image_is_32.png new file mode 100644 index 0000000000..d4f1b96d19 Binary files /dev/null and b/docs/images/image_is_32.png differ diff --git a/docs/images/image_is_4096.png b/docs/images/image_is_4096.png new file mode 100644 index 0000000000..70fcaf74d5 Binary files /dev/null and b/docs/images/image_is_4096.png differ diff --git a/docs/images/image_is_original.png b/docs/images/image_is_original.png new file mode 100644 index 0000000000..425268719d Binary files /dev/null and b/docs/images/image_is_original.png differ diff --git a/docs/images/image_is_ref_1.png b/docs/images/image_is_ref_1.png new file mode 100644 index 0000000000..fe29a4b293 Binary files /dev/null and b/docs/images/image_is_ref_1.png differ diff --git a/docs/images/image_is_ref_2.png b/docs/images/image_is_ref_2.png new file mode 100644 index 0000000000..61c2d38680 Binary files /dev/null and b/docs/images/image_is_ref_2.png differ diff --git a/docs/images/image_is_ref_3.png b/docs/images/image_is_ref_3.png new file mode 100644 index 0000000000..f11d7400dc Binary files /dev/null and b/docs/images/image_is_ref_3.png differ diff --git a/docs/images/image_is_ref_4.png b/docs/images/image_is_ref_4.png new file mode 100644 index 0000000000..ddc18dbe79 Binary files /dev/null and b/docs/images/image_is_ref_4.png differ diff --git a/docs/images/ios_sample/blue-screen.png b/docs/images/ios_sample/blue-screen.png new file mode 100644 index 0000000000..eda00fa7d5 Binary files /dev/null and b/docs/images/ios_sample/blue-screen.png differ diff --git a/docs/images/ios_sample/colored-triangle.png b/docs/images/ios_sample/colored-triangle.png new file mode 100644 index 0000000000..a32fbe0eed Binary files /dev/null and b/docs/images/ios_sample/colored-triangle.png differ diff --git a/docs/images/ios_sample/default-options.png b/docs/images/ios_sample/default-options.png new file mode 100644 index 0000000000..a2888e7259 Binary files /dev/null and b/docs/images/ios_sample/default-options.png differ diff --git a/docs/images/ios_sample/mtkview.gif b/docs/images/ios_sample/mtkview.gif new file mode 100644 index 0000000000..137d4678cd Binary files /dev/null and b/docs/images/ios_sample/mtkview.gif differ diff --git a/docs/images/ios_sample/obj-cpp.png b/docs/images/ios_sample/obj-cpp.png new file mode 100644 index 0000000000..ba856f177a Binary files /dev/null and b/docs/images/ios_sample/obj-cpp.png differ diff --git a/docs/images/ios_sample/rotating-triangle.gif b/docs/images/ios_sample/rotating-triangle.gif new file mode 100644 index 0000000000..acd460332f Binary files /dev/null and b/docs/images/ios_sample/rotating-triangle.gif differ diff --git a/docs/images/ios_sample/single-view-app.png b/docs/images/ios_sample/single-view-app.png new file mode 100644 index 0000000000..6f3b401c4e Binary files /dev/null and b/docs/images/ios_sample/single-view-app.png differ diff --git a/docs/images/ios_sample/view.png b/docs/images/ios_sample/view.png new file mode 100644 index 0000000000..eb7e66fe43 Binary files /dev/null and b/docs/images/ios_sample/view.png differ diff --git a/docs/images/ios_sample/white-triangle.png b/docs/images/ios_sample/white-triangle.png new file mode 100644 index 0000000000..e95d81b7a0 Binary files /dev/null and b/docs/images/ios_sample/white-triangle.png differ diff --git a/docs/images/material_absorption.png b/docs/images/material_absorption.png new file mode 100644 index 0000000000..ffbca78b10 Binary files /dev/null and b/docs/images/material_absorption.png differ diff --git a/docs/images/material_anisotropic.png b/docs/images/material_anisotropic.png new file mode 100644 index 0000000000..68b812d6dc Binary files /dev/null and b/docs/images/material_anisotropic.png differ diff --git a/docs/images/material_bent_normal.gif b/docs/images/material_bent_normal.gif new file mode 100644 index 0000000000..9cc3ee234b Binary files /dev/null and b/docs/images/material_bent_normal.gif differ diff --git a/docs/images/material_blending.png b/docs/images/material_blending.png new file mode 100644 index 0000000000..f810e9beb1 Binary files /dev/null and b/docs/images/material_blending.png differ diff --git a/docs/images/material_carbon_fiber.png b/docs/images/material_carbon_fiber.png new file mode 100644 index 0000000000..4b5851a9ed Binary files /dev/null and b/docs/images/material_carbon_fiber.png differ diff --git a/docs/images/material_chart.jpg b/docs/images/material_chart.jpg new file mode 100644 index 0000000000..856ebcd8f8 Binary files /dev/null and b/docs/images/material_chart.jpg differ diff --git a/docs/images/material_clear_coat.png b/docs/images/material_clear_coat.png new file mode 100644 index 0000000000..bbf3b2954d Binary files /dev/null and b/docs/images/material_clear_coat.png differ diff --git a/docs/images/material_clear_coat1.png b/docs/images/material_clear_coat1.png new file mode 100644 index 0000000000..65bf963106 Binary files /dev/null and b/docs/images/material_clear_coat1.png differ diff --git a/docs/images/material_clear_coat2.png b/docs/images/material_clear_coat2.png new file mode 100644 index 0000000000..1e50aa5f3e Binary files /dev/null and b/docs/images/material_clear_coat2.png differ diff --git a/docs/images/material_furnace_energy_loss.png b/docs/images/material_furnace_energy_loss.png new file mode 100644 index 0000000000..b025d26bfb Binary files /dev/null and b/docs/images/material_furnace_energy_loss.png differ diff --git a/docs/images/material_furnace_energy_preservation.png b/docs/images/material_furnace_energy_preservation.png new file mode 100644 index 0000000000..e116ce2593 Binary files /dev/null and b/docs/images/material_furnace_energy_preservation.png differ diff --git a/docs/images/material_grazing_reflectance.png b/docs/images/material_grazing_reflectance.png new file mode 100644 index 0000000000..92da8ce493 Binary files /dev/null and b/docs/images/material_grazing_reflectance.png differ diff --git a/docs/images/material_interpolation.png b/docs/images/material_interpolation.png new file mode 100644 index 0000000000..88bbf3d241 Binary files /dev/null and b/docs/images/material_interpolation.png differ diff --git a/docs/images/material_ior.png b/docs/images/material_ior.png new file mode 100644 index 0000000000..9975273821 Binary files /dev/null and b/docs/images/material_ior.png differ diff --git a/docs/images/material_metallic_energy_loss.png b/docs/images/material_metallic_energy_loss.png new file mode 100644 index 0000000000..a4120d18ac Binary files /dev/null and b/docs/images/material_metallic_energy_loss.png differ diff --git a/docs/images/material_metallic_energy_preservation.png b/docs/images/material_metallic_energy_preservation.png new file mode 100644 index 0000000000..ca95e1197b Binary files /dev/null and b/docs/images/material_metallic_energy_preservation.png differ diff --git a/docs/images/material_parameters.png b/docs/images/material_parameters.png new file mode 100644 index 0000000000..4116632c7f Binary files /dev/null and b/docs/images/material_parameters.png differ diff --git a/docs/images/material_roughness_remap.png b/docs/images/material_roughness_remap.png new file mode 100644 index 0000000000..c98dfaaf83 Binary files /dev/null and b/docs/images/material_roughness_remap.png differ diff --git a/docs/images/material_thickness.png b/docs/images/material_thickness.png new file mode 100644 index 0000000000..bada659291 Binary files /dev/null and b/docs/images/material_thickness.png differ diff --git a/docs/images/materials/absorption.png b/docs/images/materials/absorption.png new file mode 100644 index 0000000000..6c1ebf8652 Binary files /dev/null and b/docs/images/materials/absorption.png differ diff --git a/docs/images/materials/anisotropy.png b/docs/images/materials/anisotropy.png new file mode 100644 index 0000000000..c568d48652 Binary files /dev/null and b/docs/images/materials/anisotropy.png differ diff --git a/docs/images/materials/clear_coat.png b/docs/images/materials/clear_coat.png new file mode 100644 index 0000000000..d3d41bc5bd Binary files /dev/null and b/docs/images/materials/clear_coat.png differ diff --git a/docs/images/materials/clear_coat_roughness.png b/docs/images/materials/clear_coat_roughness.png new file mode 100644 index 0000000000..03f169c85b Binary files /dev/null and b/docs/images/materials/clear_coat_roughness.png differ diff --git a/docs/images/materials/conductor_roughness.png b/docs/images/materials/conductor_roughness.png new file mode 100644 index 0000000000..1fe537984f Binary files /dev/null and b/docs/images/materials/conductor_roughness.png differ diff --git a/docs/images/materials/dielectric_roughness.png b/docs/images/materials/dielectric_roughness.png new file mode 100644 index 0000000000..0fe9575840 Binary files /dev/null and b/docs/images/materials/dielectric_roughness.png differ diff --git a/docs/images/materials/ior.png b/docs/images/materials/ior.png new file mode 100644 index 0000000000..e1b8ced8d9 Binary files /dev/null and b/docs/images/materials/ior.png differ diff --git a/docs/images/materials/metallic.png b/docs/images/materials/metallic.png new file mode 100644 index 0000000000..34dbbd83f9 Binary files /dev/null and b/docs/images/materials/metallic.png differ diff --git a/docs/images/materials/reflectance.png b/docs/images/materials/reflectance.png new file mode 100644 index 0000000000..5a1a510d8f Binary files /dev/null and b/docs/images/materials/reflectance.png differ diff --git a/docs/images/materials/refraction_roughness.png b/docs/images/materials/refraction_roughness.png new file mode 100644 index 0000000000..64cf1a4f12 Binary files /dev/null and b/docs/images/materials/refraction_roughness.png differ diff --git a/docs/images/materials/sheen_roughness.png b/docs/images/materials/sheen_roughness.png new file mode 100644 index 0000000000..c1247f46e4 Binary files /dev/null and b/docs/images/materials/sheen_roughness.png differ diff --git a/docs/images/materials/thickness.png b/docs/images/materials/thickness.png new file mode 100644 index 0000000000..d64320f0ae Binary files /dev/null and b/docs/images/materials/thickness.png differ diff --git a/docs/images/materials/transmission.png b/docs/images/materials/transmission.png new file mode 100644 index 0000000000..88d9a036f7 Binary files /dev/null and b/docs/images/materials/transmission.png differ diff --git a/docs/images/metallic_00.png b/docs/images/metallic_00.png new file mode 100644 index 0000000000..529bfd1287 Binary files /dev/null and b/docs/images/metallic_00.png differ diff --git a/docs/images/metallic_01.png b/docs/images/metallic_01.png new file mode 100644 index 0000000000..39f921834d Binary files /dev/null and b/docs/images/metallic_01.png differ diff --git a/docs/images/metallic_02.png b/docs/images/metallic_02.png new file mode 100644 index 0000000000..98977fb18e Binary files /dev/null and b/docs/images/metallic_02.png differ diff --git a/docs/images/metallic_03.png b/docs/images/metallic_03.png new file mode 100644 index 0000000000..5023aa17ba Binary files /dev/null and b/docs/images/metallic_03.png differ diff --git a/docs/images/metallic_04.png b/docs/images/metallic_04.png new file mode 100644 index 0000000000..53c01e1f7c Binary files /dev/null and b/docs/images/metallic_04.png differ diff --git a/docs/images/metallic_05.png b/docs/images/metallic_05.png new file mode 100644 index 0000000000..514d769592 Binary files /dev/null and b/docs/images/metallic_05.png differ diff --git a/docs/images/metallic_06.png b/docs/images/metallic_06.png new file mode 100644 index 0000000000..9a079771d3 Binary files /dev/null and b/docs/images/metallic_06.png differ diff --git a/docs/images/metallic_07.png b/docs/images/metallic_07.png new file mode 100644 index 0000000000..cedfcd9878 Binary files /dev/null and b/docs/images/metallic_07.png differ diff --git a/docs/images/metallic_08.png b/docs/images/metallic_08.png new file mode 100644 index 0000000000..56a9826783 Binary files /dev/null and b/docs/images/metallic_08.png differ diff --git a/docs/images/metallic_09.png b/docs/images/metallic_09.png new file mode 100644 index 0000000000..5c335de438 Binary files /dev/null and b/docs/images/metallic_09.png differ diff --git a/docs/images/metallic_10.png b/docs/images/metallic_10.png new file mode 100644 index 0000000000..67b8452873 Binary files /dev/null and b/docs/images/metallic_10.png differ diff --git a/docs/images/metallic_grayscale_00.png b/docs/images/metallic_grayscale_00.png new file mode 100644 index 0000000000..6a47404da8 Binary files /dev/null and b/docs/images/metallic_grayscale_00.png differ diff --git a/docs/images/metallic_grayscale_01.png b/docs/images/metallic_grayscale_01.png new file mode 100644 index 0000000000..20ee8d6774 Binary files /dev/null and b/docs/images/metallic_grayscale_01.png differ diff --git a/docs/images/metallic_grayscale_02.png b/docs/images/metallic_grayscale_02.png new file mode 100644 index 0000000000..0d3a9719fa Binary files /dev/null and b/docs/images/metallic_grayscale_02.png differ diff --git a/docs/images/metallic_grayscale_03.png b/docs/images/metallic_grayscale_03.png new file mode 100644 index 0000000000..3f29b36238 Binary files /dev/null and b/docs/images/metallic_grayscale_03.png differ diff --git a/docs/images/metallic_grayscale_04.png b/docs/images/metallic_grayscale_04.png new file mode 100644 index 0000000000..64266d09cf Binary files /dev/null and b/docs/images/metallic_grayscale_04.png differ diff --git a/docs/images/metallic_grayscale_05.png b/docs/images/metallic_grayscale_05.png new file mode 100644 index 0000000000..66b350f7bb Binary files /dev/null and b/docs/images/metallic_grayscale_05.png differ diff --git a/docs/images/metallic_grayscale_06.png b/docs/images/metallic_grayscale_06.png new file mode 100644 index 0000000000..513239e1ed Binary files /dev/null and b/docs/images/metallic_grayscale_06.png differ diff --git a/docs/images/metallic_grayscale_07.png b/docs/images/metallic_grayscale_07.png new file mode 100644 index 0000000000..cf1d31067f Binary files /dev/null and b/docs/images/metallic_grayscale_07.png differ diff --git a/docs/images/metallic_grayscale_08.png b/docs/images/metallic_grayscale_08.png new file mode 100644 index 0000000000..ada724f4ee Binary files /dev/null and b/docs/images/metallic_grayscale_08.png differ diff --git a/docs/images/metallic_grayscale_09.png b/docs/images/metallic_grayscale_09.png new file mode 100644 index 0000000000..3344a239ae Binary files /dev/null and b/docs/images/metallic_grayscale_09.png differ diff --git a/docs/images/metallic_grayscale_10.png b/docs/images/metallic_grayscale_10.png new file mode 100644 index 0000000000..33dd4b90e6 Binary files /dev/null and b/docs/images/metallic_grayscale_10.png differ diff --git a/docs/images/non_metallic_00.png b/docs/images/non_metallic_00.png new file mode 100644 index 0000000000..e2756880a4 Binary files /dev/null and b/docs/images/non_metallic_00.png differ diff --git a/docs/images/non_metallic_01.png b/docs/images/non_metallic_01.png new file mode 100644 index 0000000000..6b6333725c Binary files /dev/null and b/docs/images/non_metallic_01.png differ diff --git a/docs/images/non_metallic_02.png b/docs/images/non_metallic_02.png new file mode 100644 index 0000000000..a47d40adac Binary files /dev/null and b/docs/images/non_metallic_02.png differ diff --git a/docs/images/non_metallic_03.png b/docs/images/non_metallic_03.png new file mode 100644 index 0000000000..08b370a15d Binary files /dev/null and b/docs/images/non_metallic_03.png differ diff --git a/docs/images/non_metallic_04.png b/docs/images/non_metallic_04.png new file mode 100644 index 0000000000..913e2b0f91 Binary files /dev/null and b/docs/images/non_metallic_04.png differ diff --git a/docs/images/non_metallic_05.png b/docs/images/non_metallic_05.png new file mode 100644 index 0000000000..eb39d47e31 Binary files /dev/null and b/docs/images/non_metallic_05.png differ diff --git a/docs/images/non_metallic_06.png b/docs/images/non_metallic_06.png new file mode 100644 index 0000000000..bc150a7cb9 Binary files /dev/null and b/docs/images/non_metallic_06.png differ diff --git a/docs/images/non_metallic_07.png b/docs/images/non_metallic_07.png new file mode 100644 index 0000000000..dae9a6a114 Binary files /dev/null and b/docs/images/non_metallic_07.png differ diff --git a/docs/images/non_metallic_08.png b/docs/images/non_metallic_08.png new file mode 100644 index 0000000000..8fc2bbbafc Binary files /dev/null and b/docs/images/non_metallic_08.png differ diff --git a/docs/images/non_metallic_09.png b/docs/images/non_metallic_09.png new file mode 100644 index 0000000000..e097aeaaa6 Binary files /dev/null and b/docs/images/non_metallic_09.png differ diff --git a/docs/images/non_metallic_10.png b/docs/images/non_metallic_10.png new file mode 100644 index 0000000000..10a428cf8d Binary files /dev/null and b/docs/images/non_metallic_10.png differ diff --git a/docs/images/photo_fresnel_lake.jpg b/docs/images/photo_fresnel_lake.jpg new file mode 100644 index 0000000000..0e34475a84 Binary files /dev/null and b/docs/images/photo_fresnel_lake.jpg differ diff --git a/docs/images/photo_incident_light_meter.jpg b/docs/images/photo_incident_light_meter.jpg new file mode 100644 index 0000000000..f7017ad930 Binary files /dev/null and b/docs/images/photo_incident_light_meter.jpg differ diff --git a/docs/images/photo_light_meter.jpg b/docs/images/photo_light_meter.jpg new file mode 100644 index 0000000000..2ca1f94b5f Binary files /dev/null and b/docs/images/photo_light_meter.jpg differ diff --git a/docs/images/photo_photometric_lights.jpg b/docs/images/photo_photometric_lights.jpg new file mode 100644 index 0000000000..f1af42c0fe Binary files /dev/null and b/docs/images/photo_photometric_lights.jpg differ diff --git a/docs/images/reflectance_00.png b/docs/images/reflectance_00.png new file mode 100644 index 0000000000..dd020ff358 Binary files /dev/null and b/docs/images/reflectance_00.png differ diff --git a/docs/images/reflectance_01.png b/docs/images/reflectance_01.png new file mode 100644 index 0000000000..89d78faa17 Binary files /dev/null and b/docs/images/reflectance_01.png differ diff --git a/docs/images/reflectance_02.png b/docs/images/reflectance_02.png new file mode 100644 index 0000000000..d4c0bb7b6d Binary files /dev/null and b/docs/images/reflectance_02.png differ diff --git a/docs/images/reflectance_03.png b/docs/images/reflectance_03.png new file mode 100644 index 0000000000..b24641805b Binary files /dev/null and b/docs/images/reflectance_03.png differ diff --git a/docs/images/reflectance_04.png b/docs/images/reflectance_04.png new file mode 100644 index 0000000000..f377273555 Binary files /dev/null and b/docs/images/reflectance_04.png differ diff --git a/docs/images/reflectance_05.png b/docs/images/reflectance_05.png new file mode 100644 index 0000000000..98262f526c Binary files /dev/null and b/docs/images/reflectance_05.png differ diff --git a/docs/images/reflectance_06.png b/docs/images/reflectance_06.png new file mode 100644 index 0000000000..40e8574aa9 Binary files /dev/null and b/docs/images/reflectance_06.png differ diff --git a/docs/images/reflectance_07.png b/docs/images/reflectance_07.png new file mode 100644 index 0000000000..cbfd7b5e60 Binary files /dev/null and b/docs/images/reflectance_07.png differ diff --git a/docs/images/reflectance_08.png b/docs/images/reflectance_08.png new file mode 100644 index 0000000000..1ee057119d Binary files /dev/null and b/docs/images/reflectance_08.png differ diff --git a/docs/images/reflectance_09.png b/docs/images/reflectance_09.png new file mode 100644 index 0000000000..8b2805d3e4 Binary files /dev/null and b/docs/images/reflectance_09.png differ diff --git a/docs/images/reflectance_10.png b/docs/images/reflectance_10.png new file mode 100644 index 0000000000..bf94e60eac Binary files /dev/null and b/docs/images/reflectance_10.png differ diff --git a/docs/images/samples/app_gmm_ar_nav.jpg b/docs/images/samples/app_gmm_ar_nav.jpg new file mode 100644 index 0000000000..093400ebcb Binary files /dev/null and b/docs/images/samples/app_gmm_ar_nav.jpg differ diff --git a/docs/images/samples/app_google_3d_viewer.jpg b/docs/images/samples/app_google_3d_viewer.jpg new file mode 100644 index 0000000000..446370ef90 Binary files /dev/null and b/docs/images/samples/app_google_3d_viewer.jpg differ diff --git a/docs/images/samples/example_bistro1.jpg b/docs/images/samples/example_bistro1.jpg new file mode 100644 index 0000000000..29d7befdcd Binary files /dev/null and b/docs/images/samples/example_bistro1.jpg differ diff --git a/docs/images/samples/example_bistro2.jpg b/docs/images/samples/example_bistro2.jpg new file mode 100644 index 0000000000..ecd092cd92 Binary files /dev/null and b/docs/images/samples/example_bistro2.jpg differ diff --git a/docs/images/samples/example_helmet.jpg b/docs/images/samples/example_helmet.jpg new file mode 100644 index 0000000000..8f49543503 Binary files /dev/null and b/docs/images/samples/example_helmet.jpg differ diff --git a/docs/images/samples/example_live_wallpaper.jpg b/docs/images/samples/example_live_wallpaper.jpg new file mode 100644 index 0000000000..74ad33f1ae Binary files /dev/null and b/docs/images/samples/example_live_wallpaper.jpg differ diff --git a/docs/images/samples/example_materials1.jpg b/docs/images/samples/example_materials1.jpg new file mode 100644 index 0000000000..54266b97cf Binary files /dev/null and b/docs/images/samples/example_materials1.jpg differ diff --git a/docs/images/samples/example_materials2.jpg b/docs/images/samples/example_materials2.jpg new file mode 100644 index 0000000000..c5745515e9 Binary files /dev/null and b/docs/images/samples/example_materials2.jpg differ diff --git a/docs/images/samples/example_ssr.jpg b/docs/images/samples/example_ssr.jpg new file mode 100644 index 0000000000..46018afffb Binary files /dev/null and b/docs/images/samples/example_ssr.jpg differ diff --git a/docs/images/samples/sample_gltf_viewer.jpg b/docs/images/samples/sample_gltf_viewer.jpg new file mode 100644 index 0000000000..1ecbd3b38f Binary files /dev/null and b/docs/images/samples/sample_gltf_viewer.jpg differ diff --git a/docs/images/samples/sample_hello_camera.jpg b/docs/images/samples/sample_hello_camera.jpg new file mode 100644 index 0000000000..e945e8db06 Binary files /dev/null and b/docs/images/samples/sample_hello_camera.jpg differ diff --git a/docs/images/samples/sample_hello_triangle.jpg b/docs/images/samples/sample_hello_triangle.jpg new file mode 100644 index 0000000000..224ee04366 Binary files /dev/null and b/docs/images/samples/sample_hello_triangle.jpg differ diff --git a/docs/images/samples/sample_image_based_lighting.jpg b/docs/images/samples/sample_image_based_lighting.jpg new file mode 100644 index 0000000000..334e2a2067 Binary files /dev/null and b/docs/images/samples/sample_image_based_lighting.jpg differ diff --git a/docs/images/samples/sample_lit_cube.jpg b/docs/images/samples/sample_lit_cube.jpg new file mode 100644 index 0000000000..b5ef384e08 Binary files /dev/null and b/docs/images/samples/sample_lit_cube.jpg differ diff --git a/docs/images/samples/sample_page_curl.jpg b/docs/images/samples/sample_page_curl.jpg new file mode 100644 index 0000000000..6ea94d1ca4 Binary files /dev/null and b/docs/images/samples/sample_page_curl.jpg differ diff --git a/docs/images/samples/sample_stream_test.jpg b/docs/images/samples/sample_stream_test.jpg new file mode 100644 index 0000000000..d442926477 Binary files /dev/null and b/docs/images/samples/sample_stream_test.jpg differ diff --git a/docs/images/samples/sample_texture_view.jpg b/docs/images/samples/sample_texture_view.jpg new file mode 100644 index 0000000000..76eb15cd7e Binary files /dev/null and b/docs/images/samples/sample_texture_view.jpg differ diff --git a/docs/images/samples/sample_textured_object.jpg b/docs/images/samples/sample_textured_object.jpg new file mode 100644 index 0000000000..2f04e30a60 Binary files /dev/null and b/docs/images/samples/sample_textured_object.jpg differ diff --git a/docs/images/samples/sample_transparent_rendering.jpg b/docs/images/samples/sample_transparent_rendering.jpg new file mode 100644 index 0000000000..8a5d0c0e1e Binary files /dev/null and b/docs/images/samples/sample_transparent_rendering.jpg differ diff --git a/docs/images/screenshot_anisotropic_ibl1.jpg b/docs/images/screenshot_anisotropic_ibl1.jpg new file mode 100644 index 0000000000..0462fdd563 Binary files /dev/null and b/docs/images/screenshot_anisotropic_ibl1.jpg differ diff --git a/docs/images/screenshot_anisotropic_ibl2.jpg b/docs/images/screenshot_anisotropic_ibl2.jpg new file mode 100644 index 0000000000..b7aca5984f Binary files /dev/null and b/docs/images/screenshot_anisotropic_ibl2.jpg differ diff --git a/docs/images/screenshot_anisotropy.png b/docs/images/screenshot_anisotropy.png new file mode 100644 index 0000000000..9eda502fd9 Binary files /dev/null and b/docs/images/screenshot_anisotropy.png differ diff --git a/docs/images/screenshot_anisotropy_direction.png b/docs/images/screenshot_anisotropy_direction.png new file mode 100644 index 0000000000..a0825b36e6 Binary files /dev/null and b/docs/images/screenshot_anisotropy_direction.png differ diff --git a/docs/images/screenshot_anisotropy_map.jpg b/docs/images/screenshot_anisotropy_map.jpg new file mode 100644 index 0000000000..8e5bde1c53 Binary files /dev/null and b/docs/images/screenshot_anisotropy_map.jpg differ diff --git a/docs/images/screenshot_ao.jpg b/docs/images/screenshot_ao.jpg new file mode 100644 index 0000000000..8ed57a6e38 Binary files /dev/null and b/docs/images/screenshot_ao.jpg differ diff --git a/docs/images/screenshot_ball_ibl.png b/docs/images/screenshot_ball_ibl.png new file mode 100644 index 0000000000..a453316093 Binary files /dev/null and b/docs/images/screenshot_ball_ibl.png differ diff --git a/docs/images/screenshot_bloom.jpg b/docs/images/screenshot_bloom.jpg new file mode 100644 index 0000000000..c7f62f54d4 Binary files /dev/null and b/docs/images/screenshot_bloom.jpg differ diff --git a/docs/images/screenshot_camera_transparency.jpg b/docs/images/screenshot_camera_transparency.jpg new file mode 100644 index 0000000000..32336edcd4 Binary files /dev/null and b/docs/images/screenshot_camera_transparency.jpg differ diff --git a/docs/images/screenshot_car.jpg b/docs/images/screenshot_car.jpg new file mode 100644 index 0000000000..3c0970e42e Binary files /dev/null and b/docs/images/screenshot_car.jpg differ diff --git a/docs/images/screenshot_clear_coat_ior_change.jpg b/docs/images/screenshot_clear_coat_ior_change.jpg new file mode 100644 index 0000000000..49d74fd0ca Binary files /dev/null and b/docs/images/screenshot_clear_coat_ior_change.jpg differ diff --git a/docs/images/screenshot_clear_coat_normal.jpg b/docs/images/screenshot_clear_coat_normal.jpg new file mode 100644 index 0000000000..4debc92d31 Binary files /dev/null and b/docs/images/screenshot_clear_coat_normal.jpg differ diff --git a/docs/images/screenshot_cloth.png b/docs/images/screenshot_cloth.png new file mode 100644 index 0000000000..666034c628 Binary files /dev/null and b/docs/images/screenshot_cloth.png differ diff --git a/docs/images/screenshot_cloth_sheen.png b/docs/images/screenshot_cloth_sheen.png new file mode 100644 index 0000000000..9be7ded2cb Binary files /dev/null and b/docs/images/screenshot_cloth_sheen.png differ diff --git a/docs/images/screenshot_cloth_subsurface.png b/docs/images/screenshot_cloth_subsurface.png new file mode 100644 index 0000000000..e78e80f22d Binary files /dev/null and b/docs/images/screenshot_cloth_subsurface.png differ diff --git a/docs/images/screenshot_cloth_velvet.png b/docs/images/screenshot_cloth_velvet.png new file mode 100644 index 0000000000..9d6b04928f Binary files /dev/null and b/docs/images/screenshot_cloth_velvet.png differ diff --git a/docs/images/screenshot_coordinates.jpg b/docs/images/screenshot_coordinates.jpg new file mode 100644 index 0000000000..998b55e1c6 Binary files /dev/null and b/docs/images/screenshot_coordinates.jpg differ diff --git a/docs/images/screenshot_cubemap_coordinates.png b/docs/images/screenshot_cubemap_coordinates.png new file mode 100644 index 0000000000..5a8c58c771 Binary files /dev/null and b/docs/images/screenshot_cubemap_coordinates.png differ diff --git a/docs/images/screenshot_directional_light.png b/docs/images/screenshot_directional_light.png new file mode 100644 index 0000000000..16f32ad924 Binary files /dev/null and b/docs/images/screenshot_directional_light.png differ diff --git a/docs/images/screenshot_fog1.jpg b/docs/images/screenshot_fog1.jpg new file mode 100644 index 0000000000..bf60c6e5a5 Binary files /dev/null and b/docs/images/screenshot_fog1.jpg differ diff --git a/docs/images/screenshot_fog2.jpg b/docs/images/screenshot_fog2.jpg new file mode 100644 index 0000000000..5ddf155fcf Binary files /dev/null and b/docs/images/screenshot_fog2.jpg differ diff --git a/docs/images/screenshot_fringing.jpg b/docs/images/screenshot_fringing.jpg new file mode 100644 index 0000000000..a9b5c22c9e Binary files /dev/null and b/docs/images/screenshot_fringing.jpg differ diff --git a/docs/images/screenshot_lightgen_samples.png b/docs/images/screenshot_lightgen_samples.png new file mode 100644 index 0000000000..80b292806c Binary files /dev/null and b/docs/images/screenshot_lightgen_samples.png differ diff --git a/docs/images/screenshot_luminance_debug.png b/docs/images/screenshot_luminance_debug.png new file mode 100644 index 0000000000..431ebed660 Binary files /dev/null and b/docs/images/screenshot_luminance_debug.png differ diff --git a/docs/images/screenshot_multi_bounce_ao.gif b/docs/images/screenshot_multi_bounce_ao.gif new file mode 100644 index 0000000000..4208dc9905 Binary files /dev/null and b/docs/images/screenshot_multi_bounce_ao.gif differ diff --git a/docs/images/screenshot_multi_bounce_ao.jpg b/docs/images/screenshot_multi_bounce_ao.jpg new file mode 100644 index 0000000000..2f629906d2 Binary files /dev/null and b/docs/images/screenshot_multi_bounce_ao.jpg differ diff --git a/docs/images/screenshot_normal_map.jpg b/docs/images/screenshot_normal_map.jpg new file mode 100644 index 0000000000..dd186561e4 Binary files /dev/null and b/docs/images/screenshot_normal_map.jpg differ diff --git a/docs/images/screenshot_normal_map_blended.jpg b/docs/images/screenshot_normal_map_blended.jpg new file mode 100644 index 0000000000..af8fd0da76 Binary files /dev/null and b/docs/images/screenshot_normal_map_blended.jpg differ diff --git a/docs/images/screenshot_normal_map_blended_udn.jpg b/docs/images/screenshot_normal_map_blended_udn.jpg new file mode 100644 index 0000000000..0453bf0e3b Binary files /dev/null and b/docs/images/screenshot_normal_map_blended_udn.jpg differ diff --git a/docs/images/screenshot_normal_map_detail.jpg b/docs/images/screenshot_normal_map_detail.jpg new file mode 100644 index 0000000000..7372941cf6 Binary files /dev/null and b/docs/images/screenshot_normal_map_detail.jpg differ diff --git a/docs/images/screenshot_normal_mapping.jpg b/docs/images/screenshot_normal_mapping.jpg new file mode 100644 index 0000000000..4c3ff5f566 Binary files /dev/null and b/docs/images/screenshot_normal_mapping.jpg differ diff --git a/docs/images/screenshot_photometric_lights.png b/docs/images/screenshot_photometric_lights.png new file mode 100644 index 0000000000..6a2fe85b97 Binary files /dev/null and b/docs/images/screenshot_photometric_lights.png differ diff --git a/docs/images/screenshot_point_light.png b/docs/images/screenshot_point_light.png new file mode 100644 index 0000000000..bca0cc9457 Binary files /dev/null and b/docs/images/screenshot_point_light.png differ diff --git a/docs/images/screenshot_ref_comparison.png b/docs/images/screenshot_ref_comparison.png new file mode 100644 index 0000000000..b050291896 Binary files /dev/null and b/docs/images/screenshot_ref_comparison.png differ diff --git a/docs/images/screenshot_ref_filament.jpg b/docs/images/screenshot_ref_filament.jpg new file mode 100644 index 0000000000..43a749ef93 Binary files /dev/null and b/docs/images/screenshot_ref_filament.jpg differ diff --git a/docs/images/screenshot_ref_mitsuba.jpg b/docs/images/screenshot_ref_mitsuba.jpg new file mode 100644 index 0000000000..d3c2cccf6d Binary files /dev/null and b/docs/images/screenshot_ref_mitsuba.jpg differ diff --git a/docs/images/screenshot_sheen_color.png b/docs/images/screenshot_sheen_color.png new file mode 100644 index 0000000000..02f27ab820 Binary files /dev/null and b/docs/images/screenshot_sheen_color.png differ diff --git a/docs/images/screenshot_specular_ao.gif b/docs/images/screenshot_specular_ao.gif new file mode 100644 index 0000000000..9d81e7dc4c Binary files /dev/null and b/docs/images/screenshot_specular_ao.gif differ diff --git a/docs/images/screenshot_sponza.jpg b/docs/images/screenshot_sponza.jpg new file mode 100644 index 0000000000..cf8934a3c5 Binary files /dev/null and b/docs/images/screenshot_sponza.jpg differ diff --git a/docs/images/screenshot_sponza_froxels1.jpg b/docs/images/screenshot_sponza_froxels1.jpg new file mode 100644 index 0000000000..1d8702b14f Binary files /dev/null and b/docs/images/screenshot_sponza_froxels1.jpg differ diff --git a/docs/images/screenshot_sponza_froxels2.jpg b/docs/images/screenshot_sponza_froxels2.jpg new file mode 100644 index 0000000000..c297e312d2 Binary files /dev/null and b/docs/images/screenshot_sponza_froxels2.jpg differ diff --git a/docs/images/screenshot_sponza_slices.jpg b/docs/images/screenshot_sponza_slices.jpg new file mode 100644 index 0000000000..7cb48b2771 Binary files /dev/null and b/docs/images/screenshot_sponza_slices.jpg differ diff --git a/docs/images/screenshot_sponza_tiles.jpg b/docs/images/screenshot_sponza_tiles.jpg new file mode 100644 index 0000000000..c3e2aa86af Binary files /dev/null and b/docs/images/screenshot_sponza_tiles.jpg differ diff --git a/docs/images/screenshot_spot_light.png b/docs/images/screenshot_spot_light.png new file mode 100644 index 0000000000..8d0939ba18 Binary files /dev/null and b/docs/images/screenshot_spot_light.png differ diff --git a/docs/images/screenshot_spot_light_focused.png b/docs/images/screenshot_spot_light_focused.png new file mode 100644 index 0000000000..96c4d91f85 Binary files /dev/null and b/docs/images/screenshot_spot_light_focused.png differ diff --git a/docs/images/screenshot_toon_shading.png b/docs/images/screenshot_toon_shading.png new file mode 100644 index 0000000000..059cc5bca9 Binary files /dev/null and b/docs/images/screenshot_toon_shading.png differ diff --git a/docs/images/screenshot_translucency.png b/docs/images/screenshot_translucency.png new file mode 100644 index 0000000000..b27980f91c Binary files /dev/null and b/docs/images/screenshot_translucency.png differ diff --git a/docs/images/screenshot_transparency_default.png b/docs/images/screenshot_transparency_default.png new file mode 100644 index 0000000000..e9174998e9 Binary files /dev/null and b/docs/images/screenshot_transparency_default.png differ diff --git a/docs/images/screenshot_transparent_shadows.jpg b/docs/images/screenshot_transparent_shadows.jpg new file mode 100644 index 0000000000..819cf63d93 Binary files /dev/null and b/docs/images/screenshot_transparent_shadows.jpg differ diff --git a/docs/images/screenshot_twopasses_oneside.png b/docs/images/screenshot_twopasses_oneside.png new file mode 100644 index 0000000000..8c2c366b0c Binary files /dev/null and b/docs/images/screenshot_twopasses_oneside.png differ diff --git a/docs/images/screenshot_twopasses_twosides.png b/docs/images/screenshot_twopasses_twosides.png new file mode 100644 index 0000000000..bb877fdc09 Binary files /dev/null and b/docs/images/screenshot_twopasses_twosides.png differ diff --git a/docs/images/screenshot_unlit.jpg b/docs/images/screenshot_unlit.jpg new file mode 100644 index 0000000000..e03fb31612 Binary files /dev/null and b/docs/images/screenshot_unlit.jpg differ diff --git a/docs/images/screenshot_xarrow.png b/docs/images/screenshot_xarrow.png new file mode 100644 index 0000000000..bdf91d44e5 Binary files /dev/null and b/docs/images/screenshot_xarrow.png differ diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000000..7ec32d2406 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,11 @@ + + + + + + Redirecting... + + + + + diff --git a/docs/main/filament.html b/docs/main/filament.html new file mode 100644 index 0000000000..dce74158e3 --- /dev/null +++ b/docs/main/filament.html @@ -0,0 +1,4449 @@ + + + + + + Filament - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

+ +

$$\newcommand{\n}{\hat{n}}\newcommand{\thetai}{\theta_\mathrm{i}}\newcommand{\thetao}{\theta_\mathrm{o}}\newcommand{\d}[1]{\mathrm{d}#1}\newcommand{\w}{\hat{\omega}}\newcommand{\wi}{\w_\mathrm{i}}\newcommand{\wo}{\w_\mathrm{o}}\newcommand{\wh}{\w_\mathrm{h}}\newcommand{\Li}{L_\mathrm{i}}\newcommand{\Lo}{L_\mathrm{o}}\newcommand{\Le}{L_\mathrm{e}}\newcommand{\Lr}{L_\mathrm{r}}\newcommand{\Lt}{L_\mathrm{t}}\newcommand{\O}{\mathrm{O}}\newcommand{\degrees}{{^{\large\circ}}}\newcommand{\T}{\mathsf{T}}\newcommand{\mathset}[1]{\mathbb{#1}}\newcommand{\Real}{\mathset{R}}\newcommand{\Integer}{\mathset{Z}}\newcommand{\Boolean}{\mathset{B}}\newcommand{\Complex}{\mathset{C}}\newcommand{\un}[1]{,\mathrm{#1}}$$ + +

Physically Based Rendering in Filament

Physically Based Rendering in Filament

+
+

+

+
Contents

(Top)
+About
+  1.1  Authors
+Overview
+  2.1  Principles
+  2.2  Physically based rendering
+Notation
+Material system
+  4.1  Standard model
+  4.2  Dielectrics and conductors
+  4.3  Energy conservation
+  4.4  Specular BRDF
+    4.4.1  Normal distribution function (specular D)
+    4.4.2  Geometric shadowing (specular G)
+    4.4.3  Fresnel (specular F)
+  4.5  Diffuse BRDF
+  4.6  Standard model summary
+  4.7  Improving the BRDFs
+    4.7.1  Energy gain in diffuse reflectance
+    4.7.2  Energy loss in specular reflectance
+  4.8  Parameterization
+    4.8.1  Standard parameters
+    4.8.2  Types and ranges
+    4.8.3  Remapping
+    4.8.4  Blending and layering
+    4.8.5  Crafting physically based materials
+  4.9  Clear coat model
+    4.9.1  Clear coat specular BRDF
+    4.9.2  Integration in the surface response
+    4.9.3  Clear coat parameterization
+    4.9.4  Base layer modification
+  4.10  Anisotropic model
+    4.10.1  Anisotropic specular BRDF
+    4.10.2  Anisotropic parameterization
+  4.11  Subsurface model
+    4.11.1  Subsurface specular BRDF
+    4.11.2  Subsurface parameterization
+  4.12  Cloth model
+    4.12.1  Cloth specular BRDF
+    4.12.2  Cloth diffuse BRDF
+    4.12.3  Cloth parameterization
+Lighting
+  5.1  Units
+    5.1.1  Light units validation
+  5.2  Direct lighting
+    5.2.1  Directional lights
+    5.2.2  Punctual lights
+    5.2.3  Photometric lights
+    5.2.4  Area lights
+    5.2.5  Lights parameterization
+    5.2.6  Pre-exposed lights
+  5.3  Image based lights
+    5.3.1  IBL Types
+    5.3.2  IBL Unit
+    5.3.3  Processing light probes
+    5.3.4  Distant light probes
+    5.3.5  Clear coat
+    5.3.6  Anisotropy
+    5.3.7  Subsurface
+    5.3.8  Cloth
+  5.4  Static lighting
+  5.5  Transparency and translucency lighting
+    5.5.1  Transparency
+    5.5.2  Translucency
+  5.6  Occlusion
+    5.6.1  Diffuse occlusion
+    5.6.2  Specular occlusion
+  5.7  Normal mapping
+    5.7.1  Reoriented normal mapping
+    5.7.2  UDN blending
+Volumetric effects
+  6.1  Exponential height fog
+Anti-aliasing
+Imaging pipeline
+  8.1  Physically based camera
+    8.1.1  Exposure settings
+    8.1.2  Exposure value
+    8.1.3  Exposure
+    8.1.4  Automatic exposure
+    8.1.5  Bloom
+  8.2  Optics post-processing
+    8.2.1  Color fringing
+    8.2.2  Lens flares
+  8.3  Filmic post-processing
+    8.3.1  Contrast
+    8.3.2  Curves
+    8.3.3  Levels
+    8.3.4  Color grading
+  8.4  Light path
+    8.4.1  Clustered Forward Rendering
+    8.4.2  Implementation notes
+  8.5  Validation
+    8.5.1  Scene referred visualization
+    8.5.2  Reference renderings
+  8.6  Coordinates systems
+    8.6.1  World coordinates system
+    8.6.2  Camera coordinates system
+    8.6.3  Cubemaps coordinates system
+Annex
+  9.1  Specular color
+  9.2  Importance sampling for the IBL
+    9.2.1  Choosing important directions
+    9.2.2  Pre-filtered importance sampling
+  9.3  Choosing important directions for sampling the BRDF
+  9.4  Hammersley sequence
+  9.5  Precomputing L for image-based lighting
+  9.6  Spherical Harmonics
+    9.6.1  Basis functions
+    9.6.2  Decomposition and reconstruction
+    9.6.3  Decomposition of \(\left< cos \theta \right>\)
+    9.6.4  Convolution
+  9.7  Sample validation scene for Mitsuba
+  9.8  Light assignment with froxels
+10  Revisions
+11  Bibliography
+

   

About

+

+

This document is part of the Filament project. To report errors in this document please use the project's issue tracker.

+

+   

Authors

+

+

+

+   

Overview

+

+

Filament is a physically based rendering (PBR) engine for Android. The goal of Filament is to offer a set of tools and APIs for Android developers that will enable them to create high quality 2D and 3D rendering with ease.

+

+The goal of this document is to explain the equations and theory behind the material and lighting models used in Filament. This document is intended as a reference for contributors to Filament or developers interested in the inner workings of the engine. We will provide code snippets as needed to make the relationship between theory and practice as clear as possible. +

+This document is not intended as a design document. It focuses solely on algorithms and its content could be used to implement PBR in any engine. However, this document explains why we chose specific algorithms/models over others. +

+Unless noted otherwise, all the 3D renderings present in this document have been generated in-engine (prototype or production). Many of these 3D renderings were captured during the early stages of development of Filament and do not reflect the final quality. +

+   

Principles

+

+

Real-time rendering is an active area of research and there is a large number of equations, algorithms and implementation to choose from for every single feature that needs to be implemented (the book Rendering real-time shadows, for instance, is a 400 pages summary of dozens of shadows rendering techniques). As such, we must first define our goals (or principles, to follow Brent Burley's seminal paper Physically-based shading at Disney [Burley12]) before we can make informed decisions.

+

+

Real-time mobile performance

Our primary goal is to design and implement a rendering system able to perform efficiently on mobile platforms. The primary target will be OpenGL ES 3.x class GPUs. +

Quality

Our rendering system will emphasize overall picture quality. We will however accept quality compromises to support low and medium performance GPUs. +

Ease of use

Artists need to be able to iterate often and quickly on their assets and our rendering system must allow them to do so intuitively. We must therefore provide parameters that are easy to understand (for instance, no specular power). +

+ We also understand that not all developers have the luxury to work with artists. The physically based approach of our system will allow developers to craft visually plausible materials without the need to understand the theory behind our implementation. +

+ For both artists and developers, our system will rely on as few parameters as possible to reduce trial and error and allow users to quickly master the material model. +

+ In addition, any combination of parameter values should lead to physically plausible results. Physically implausible materials must be hard to create. +

Familiarity

Our system should use physical units everywhere possible: distances in meters or centimeters, color temperatures in Kelvin, light units in lumens or candelas, etc. +

Flexibility

A physically based approach must not preclude non-realistic rendering. User interfaces for instance will need unlit materials. +

Deployment size

While not directly related to the content of this document, it bears emphasizing our desire to keep the rendering library as small as possible so any application can bundle it without increasing the binary to undesirable sizes. +

+   

Physically based rendering

+

+

We chose to adopt PBR for its benefits from an artistic and production efficient standpoints, and because it is compatible with our goals.

+

+Physically based rendering is a rendering method that provides a more accurate representation of materials and how they interact with light when compared to traditional real-time models. The separation of materials and lighting at the core of the PBR method makes it easier to create realistic assets that look accurate in all lighting conditions. +

+   

Notation

+

+

$$ +\newcommand{NoL}{n \cdot l} +\newcommand{NoV}{n \cdot v} +\newcommand{NoH}{n \cdot h} +\newcommand{VoH}{v \cdot h} +\newcommand{LoH}{l \cdot h} +\newcommand{fNormal}{f_{0}} +\newcommand{fDiffuse}{f_d} +\newcommand{fSpecular}{f_r} +\newcommand{fX}{f_x} +\newcommand{aa}{\alpha^2} +\newcommand{fGrazing}{f_{90}} +\newcommand{schlick}{F_{Schlick}} +\newcommand{nior}{n_{ior}} +\newcommand{Ed}{E_d} +\newcommand{Lt}{L_{\bot}} +\newcommand{Lout}{L_{out}} +\newcommand{cosTheta}{\left< \cos \theta \right> } +$$

+

+The equations found throughout this document use the symbols described in table 1. +

+

+  + + + + + + + + + + + + + + + + +
Symbol Definition
\(v\) View unit vector
\(l\) Incident light unit vector
\(n\) Surface normal unit vector
\(h\) Half unit vector between \(l\) and \(v\)
\(f\) BRDF
\(\fDiffuse\) Diffuse component of a BRDF
\(\fSpecular\) Specular component of a BRDF
\(\alpha\) Roughness, remapped from using input perceptualRoughness
\(\sigma\) Diffuse reflectance
\(\Omega\) Spherical domain
\(\fNormal\) Reflectance at normal incidence
\(\fGrazing\) Reflectance at grazing angle
\(\chi^+(a)\) Heaviside function (1 if \(a > 0\) and 0 otherwise)
\(n_{ior}\) Index of refraction (IOR) of an interface
\(\left< \NoL \right>\) Dot product clamped to [0..1]
\(\left< a \right>\) Saturated value (clamped to [0..1])
Table 1: Symbols definitions
+

+   

Material system

+

+

The sections below describe multiple material models to simplify the description of various surface features such as anisotropy or the clear coat layer. In practice however some of these models are condensed into a single one. For instance, the standard model, the clear coat model and the anisotropic model can be combined to form a single, more flexible and powerful model. Please refer to the Materials documentation to get a description of the material models as implemented in Filament.

+

+   

Standard model

+

+

The goal of our model is to represent standard material appearances. A material model is described mathematically by a BSDF (Bidirectional Scattering Distribution Function), which is itself composed of two other functions: the BRDF (Bidirectional Reflectance Distribution Function) and the BTDF (Bidirectional Transmittance Function).

+

+Since we aim to model commonly encountered surfaces, our standard material model will focus on the BRDF and ignore the BTDF, or approximate it greatly. Our standard model will therefore only be able to correctly mimic reflective, isotropic, dielectric or conductive surfaces with short mean free paths. +

+The BRDF describes the surface response of a standard material as a function made of two terms: +

+

    +
  • A diffuse component, or \(f_d\) +
  • +
  • A specular component, or \(f_r\)
+

+The relationship between a surface, the surface normal, incident light and these terms is shown in figure 1 (we ignore subsurface scattering for now): +

+

 
Figure 1: Interaction of the light with a surface using BRDF model with a diffuse term \( f_d \) and a specular term \( f_r \)
+

+The complete surface response can be expressed as such: +

+$$\begin{equation}\label{brdf} +f(v,l)=f_d(v,l)+f_r(v,l) +\end{equation}$$ +

+This equation characterizes the surface response for incident light from a single direction. The full rendering equation would require to integrate \(l\) over the entire hemisphere. +

+Commonly encountered surfaces are usually not made of a flat interface so we need a model that can characterize the interaction of light with an irregular interface. +

+A microfacet BRDF is a good physically plausible BRDF for that purpose. Such BRDF states that surfaces are not smooth at a micro level, but made of a large number of randomly aligned planar surface fragments, called microfacets. Figure 2 shows the difference between a flat interface and an irregular interface at a micro level: +

+

 
Figure 2: Irregular interface as modeled by a microfacet model (left) and flat interface (right)
+

+Only the microfacets whose normal is oriented halfway between the light direction and the view direction will reflect visible light, as shown in figure 3. +

+

 
Figure 3: Microfacets
+

+However, not all microfacets with a properly oriented normal will contribute reflected light as the BRDF takes into account masking and shadowing. This is illustrated in figure 4. +

+

 
Figure 4: Masking and shadowing of microfacets
+

+A microfacet BRDF is heavily influenced by a roughness parameter which describes how smooth (low roughness) or how rough (high roughness) a surface is at a micro level. The smoother the surface, the more facets are aligned and the more pronounced the reflected light is. The rougher the surface, the fewer facets are oriented towards the camera and incoming light is scattered away from the camera after reflection, giving a blurry aspect to the specular highlights. +

+Figure 5 shows surfaces of different roughness and how light interacts with them. +

+

 
Figure 5: Varying roughness (from left to right, rough to smooth) and the resulting BRDF specular component lobe
+

+

About roughness
+

+ The roughness parameter as set by the user is called perceptualRoughness in the shader snippets throughout this document. The variable called roughness is the perceptualRoughness with a remapping explained in section 4.8.

+

+A microfacet model is described by the following equation (where x stands for the specular or diffuse component): +

+$$\begin{equation} +\fX(v,l) = \frac{1}{| \NoV | | \NoL |} +\int_\Omega D(m,\alpha) G(v,l,m) f_m(v,l,m) (v \cdot m) (l \cdot m) dm +\end{equation}$$ +

+The term \(D\) models the distribution of the microfacets (this term is also referred to as the NDF or Normal Distribution Function). This term plays a primordial role in the appearance of surfaces as shown in figure 5. +

+The term \(G\) models the visibility (or occlusion or shadow-masking) of the microfacets. +

+Since this equation is valid for both the specular and diffuse components, the difference lies in the microfacet BRDF \(f_m\). +

+It is important to note that this equation is used to integrate over the hemisphere at a micro level: +

+

 
Figure 6: Modeling the surface response at a single point requires an integration at the micro level
+

+The diagram above shows that at a macro level, the surfaces is considered flat. This helps simplify our equations by assuming that a shaded fragment lit from a single direction corresponds to a single point at the surface. +

+At a micro level however, the surface is not flat and we cannot assume a single ray of light anymore (we can however assume that the incident rays are parallel). Since the micro facets will scatter the light in different directions given a bundle of parallel incident rays, we must integrate the surface response over a hemisphere, noted m in the above diagram. +

+It is obviously not practical to compute the full integration over the microfacets hemisphere for each shaded fragment. We will therefore rely on approximations of the integration for both the specular and diffuse components. +

+   

Dielectrics and conductors

+

+

To better understand some of the equations and behaviors shown below, we must first clearly understand the difference between metallic (conductor) and non-metallic (dielectric) surfaces.

+

+We saw earlier that when incident light hits a surface governed by a BRDF, the light is reflected as two separate components: the diffuse reflectance and the specular reflectance. The modelization of this behavior is straightforward as shown in figure 7. +

+

 
Figure 7: Modelization of the BRDF part of a BSDF
+

+This modelization is a simplification of how the light actually interacts with the surface. In reality, part of the incident light will penetrate the surface, scatter inside, and exit the surface again as diffuse reflectance. This phenomenon is illustrated in figure 8. +

+

 
Figure 8: Scattering of diffuse light
+

+Here lies the difference between conductors and dielectrics. There is no subsurface scattering occurring with purely metallic materials, which means there is no diffuse component (and we will see later that this has an influence on the perceived color of the specular component). Scattering happens in dielectrics, which means they have both specular and diffuse components. +

+To properly modelize the BRDF we must therefore distinguish between dielectrics and conductors (scattering not shown for clarity), as shown in figure 9. +

+

 
Figure 9: BRDF modelization for dielectric and conductor surfaces
+

+   

Energy conservation

+

+

Energy conservation is one of the key components of a good BRDF for physically based rendering. An energy conservative BRDF states that the total amount of specular and diffuse reflectance energy is less than the total amount of incident energy. Without an energy conservative BRDF, artists must manually ensure that the light reflected off a surface is never more intense than the incident light.

+

+   

Specular BRDF

+

+

For the specular term, (f_r) is a mirror BRDF that can be modeled with the Fresnel law, noted (F) in the Cook-Torrance approximation of the microfacet model integration:

+

+$$\begin{equation} +f_r(v,l) = \frac{D(h, \alpha) G(v, l, \alpha) F(v, h, f0)}{4(\NoV)(\NoL)} +\end{equation}$$ +

+Given our real-time constraints, we must use an approximation for the three terms \(D\), \(G\) and \(F\). [Karis13a] has compiled a great list of formulations for these three terms that can be used with the Cook-Torrance specular BRDF. The sections that follow describe the equations we picked for these terms. +

+   

Normal distribution function (specular D)

+

+

[Burley12] observed that long-tailed normal distribution functions (NDF) are a good fit for real-world surfaces. The GGX distribution described in [Walter07] is a distribution with long-tailed falloff and short peak in the highlights, with a simple formulation suitable for real-time implementations. It is also a popular model, equivalent to the Trowbridge-Reitz distribution, in modern physically based renderers.

+

+$$\begin{equation} +D_{GGX}(h,\alpha) = \frac{\aa}{\pi ( (\NoH)^2 (\aa - 1) + 1)^2} +\end{equation}$$ +

+The GLSL implementation of the NDF, shown in listing 1, is simple and efficient. +

 
float D_GGX(float NoH, float roughness) {
+    float a = NoH * roughness;
+    float k = roughness / (1.0 - NoH * NoH + a * a);
+    return k * k * (1.0 / PI);
+}
Listing 1: Implementation of the specular D term in GLSL
+

+

We can improve this implementation by using half precision floats. This optimization requires changes to the original equation as there are two problems when computing (1 - (\NoH)^2) in half-floats. First, this computation suffers from floating point cancellation when ((\NoH)^2) is close to 1 (highlights). Secondly (\NoH) does not have enough precision around 1.

+

+The solution involves Lagrange's identity: +

+$$\begin{equation} +| a \times b |^2 = |a|^2 |b|^2 - (a \cdot b)^2 +\end{equation}$$ +

+Since both \(n\) and \(h\) are unit vectors, \(|n \times h|^2 = 1 - (\NoH)^2\). This allows us to compute \(1 - (\NoH)^2\) directly with half precision floats by using a simple cross product. Listing 2 shows the final optimized implementation. +

 
#define MEDIUMP_FLT_MAX    65504.0
+#define saturateMediump(x) min(x, MEDIUMP_FLT_MAX)
+
+float D_GGX(float roughness, float NoH, const vec3 n, const vec3 h) {
+    vec3 NxH = cross(n, h);
+    float a = NoH * roughness;
+    float k = roughness / (dot(NxH, NxH) + a * a);
+    float d = k * k * (1.0 / PI);
+    return saturateMediump(d);
+}
Listing 2: Implementation of the specular D term in GLSL optimized for fp16
+   

Geometric shadowing (specular G)

+

+

Eric Heitz showed in [Heitz14] that the Smith geometric shadowing function is the correct and exact (G) term to use. The Smith formulation is the following:

+

+$$\begin{equation} +G(v,l,\alpha) = G_1(l,\alpha) G_1(v,\alpha) +\end{equation}$$ +

+\(G_1\) can in turn follow several models, and is commonly set to the GGX formulation: +

+$$\begin{equation} +G_1(v,\alpha) = G_{GGX}(v,\alpha) = \frac{2 (\NoV)}{\NoV + \sqrt{\aa + (1 - \aa) (\NoV)^2}} +\end{equation}$$ +

+The full Smith-GGX formulation thus becomes: +

+$$\begin{equation} +G(v,l,\alpha) = \frac{2 (\NoL)}{\NoL + \sqrt{\aa + (1 - \aa) (\NoL)^2}} \frac{2 (\NoV)}{\NoV + \sqrt{\aa + (1 - \aa) (\NoV)^2}} +\end{equation}$$ +

+We can observe that the dividends \(2 (\NoL)\) and \(2 (n \cdot v)\) allow us to simplify the original function \(f_r\) by introducing a visibility function \(V\): +

+$$\begin{equation} +f_r(v,l) = D(h, \alpha) V(v, l, \alpha) F(v, h, f_0) +\end{equation}$$ +

+Where: +

+$$\begin{equation} +V(v,l,\alpha) = \frac{G(v, l, \alpha)}{4 (\NoV) (\NoL)} = V_1(l,\alpha) V_1(v,\alpha) +\end{equation}$$ +

+And: +

+$$\begin{equation} +V_1(v,\alpha) = \frac{1}{\NoV + \sqrt{\aa + (1 - \aa) (\NoV)^2}} +\end{equation}$$ +

+Heitz notes however that taking the height of the microfacets into account to correlate masking and shadowing leads to more accurate results. He defines the height-correlated Smith function thusly: +

+$$\begin{equation} +G(v,l,h,\alpha) = \frac{\chi^+(\VoH) \chi^+(\LoH)}{1 + \Lambda(v) + \Lambda(l)} +\end{equation}$$ +

+$$\begin{equation} +\Lambda(m) = \frac{-1 + \sqrt{1 + \aa tan^2(\theta_m)}}{2} = \frac{-1 + \sqrt{1 + \aa \frac{(1 - cos^2(\theta_m))}{cos^2(\theta_m)}}}{2} +\end{equation}$$ +

+Replacing \(cos(\theta_m)\) by \(\NoV\), we obtain: +

+$$\begin{equation} +\Lambda(v) = \frac{1}{2} \left( \frac{\sqrt{\aa + (1 - \aa)(\NoV)^2}}{\NoV} - 1 \right) +\end{equation}$$ +

+From which we can derive the visibility function: +

+$$\begin{equation} +V(v,l,\alpha) = \frac{0.5}{\NoL \sqrt{(\NoV)^2 (1 - \aa) + \aa} + \NoV \sqrt{(\NoL)^2 (1 - \aa) + \aa}} +\end{equation}$$ +

+The GLSL implementation of the visibility term, shown in listing 3, is a bit more expensive than we would like since it requires two sqrt operations. +

 
float V_SmithGGXCorrelated(float NoV, float NoL, float roughness) {
+    float a2 = roughness * roughness;
+    float GGXV = NoL * sqrt(NoV * NoV * (1.0 - a2) + a2);
+    float GGXL = NoV * sqrt(NoL * NoL * (1.0 - a2) + a2);
+    return 0.5 / (GGXV + GGXL);
+}
Listing 3: Implementation of the specular V term in GLSL
+

+

We can optimize this visibility function by using an approximation after noticing that all the terms under the square roots are squares and that all the terms are in the ([0..1]) range:

+

+$$\begin{equation} +V(v,l,\alpha) = \frac{0.5}{\NoL (\NoV (1 - \alpha) + \alpha) + \NoV (\NoL (1 - \alpha) + \alpha)} +\end{equation}$$ +

+This approximation is mathematically wrong but saves two square root operations and is good enough for real-time mobile applications, as shown in listing 4. +

 
float V_SmithGGXCorrelatedFast(float NoV, float NoL, float roughness) {
+    float a = roughness;
+    float GGXV = NoL * (NoV * (1.0 - a) + a);
+    float GGXL = NoV * (NoL * (1.0 - a) + a);
+    return 0.5 / (GGXV + GGXL);
+}
Listing 4: Implementation of the approximated specular V term in GLSL
+

+

[Hammon17] proposes the same approximation based on the same observation that the square root can be removed. It does so by rewriting the expressions as lerps:

+

+$$\begin{equation} +V(v,l,\alpha) = \frac{0.5}{lerp(2 (\NoL) (\NoV), \NoL + \NoV, \alpha)} +\end{equation}$$ +

+   

Fresnel (specular F)

+

+

The Fresnel effect plays an important role in the appearance of physically based materials. This effect models the fact that the amount of light the viewer sees reflected from a surface depends on the viewing angle. Large bodies of water are a perfect way to experience this phenomenon, as shown in figure 10. When looking at the water straight down (at normal incidence) you can see through the water. However, when looking further out in the distance (at grazing angle, where perceived light rays are getting parallel to the surface), you will see the specular reflections on the water become more intense.

+

+The amount of light reflected depends not only on the viewing angle, but also on the index of refraction (IOR) of the material. At normal incidence (perpendicular to the surface, or 0° angle), the amount of light reflected back is noted \(\fNormal\) and can be derived from the IOR as we will see in section 4.8.3.2. The amount of light reflected back at grazing angle is noted \(\fGrazing\) and approaches 100% for smooth materials. +

+

 
Figure 10: The Fresnel effect is particularly evident on large bodies of water
+

+More formally, the Fresnel term defines how light reflects and refracts at the interface between two different media, or the ratio of reflected and transmitted energy. [Schlick94] describes an inexpensive approximation of the Fresnel term for the Cook-Torrance specular BRDF: +

+$$\begin{equation} +F_{Schlick}(v,h,\fNormal,\fGrazing) = \fNormal + (\fGrazing - \fNormal)(1 - \VoH)^5 +\end{equation}$$ +

+The constant \(\fNormal\) represents the specular reflectance at normal incidence and is achromatic for dielectrics, and chromatic for metals. The actual value depends on the index of refraction of the interface. The GLSL implementation of this term requires the use of a pow, as shown in listing 5, which can be replaced by a few multiplications. +

 
vec3 F_Schlick(float u, vec3 f0, float f90) {
+    return f0 + (vec3(f90) - f0) * pow(1.0 - u, 5.0);
+}
Listing 5: Implementation of the specular F term in GLSL
+

+

This Fresnel function can be seen as interpolating between the incident specular reflectance and the reflectance at grazing angles, represented here by (\fGrazing). Observation of real world materials show that both dielectrics and conductors exhibit achromatic specular reflectance at grazing angles and that the Fresnel reflectance is 1.0 at 90°. A more correct (\fGrazing) is discussed in section 5.6.2.

+

+Using \(\fGrazing\) set to 1, the Schlick approximation for the Fresnel term can be optimized for scalar operations by refactoring the code slightly. The result is shown in listing 6. +

 
vec3 F_Schlick(float u, vec3 f0) {
+    float f = pow(1.0 - u, 5.0);
+    return f + f0 * (1.0 - f);
+}
Listing 6: Scalar optimization of the specular F term in GLSL
+   

Diffuse BRDF

+

+

In the diffuse term, (f_m) is a Lambertian function and the diffuse term of the BRDF becomes:

+

+$$\begin{equation} +\fDiffuse(v,l) = \frac{\sigma}{\pi} \frac{1}{| \NoV | | \NoL |} +\int_\Omega D(m,\alpha) G(v,l,m) (v \cdot m) (l \cdot m) dm +\end{equation}$$ +

+Our implementation will instead use a simple Lambertian BRDF that assumes a uniform diffuse response over the microfacets hemisphere: +

+$$\begin{equation} +\fDiffuse(v,l) = \frac{\sigma}{\pi} +\end{equation}$$ +

+In practice, the diffuse reflectance \(\sigma\) is multiplied later, as shown in listing 8. +

 
float Fd_Lambert() {
+    return 1.0 / PI;
+}
+
+vec3 Fd = diffuseColor * Fd_Lambert();
Listing 7: Implementation of the diffuse Lambertian BRDF in GLSL
+

+

The Lambertian BRDF is obviously extremely efficient and delivers results close enough to more complex models.

+

+However, the diffuse part would ideally be coherent with the specular term and take into account the surface roughness. Both the Disney diffuse BRDF [Burley12] and Oren-Nayar model [Oren94] take the roughness into account and create some retro-reflection at grazing angles. Given our constraints we decided that the extra runtime cost does not justify the slight increase in quality. This sophisticated diffuse model also renders image-based and spherical harmonics more difficult to express and implement. +

+For completeness, the Disney diffuse BRDF expressed in [Burley12] is the following: +

+$$\begin{equation} +\fDiffuse(v,l) = \frac{\sigma}{\pi} \schlick(n,l,1,\fGrazing) \schlick(n,v,1,\fGrazing) +\end{equation}$$ +

+Where: +

+$$\begin{equation} +\fGrazing=0.5 + 2 \cdot \alpha cos^2(\theta_d) +\end{equation}$$ +

 
float F_Schlick(float u, float f0, float f90) {
+    return f0 + (f90 - f0) * pow(1.0 - u, 5.0);
+}
+
+float Fd_Burley(float NoV, float NoL, float LoH, float roughness) {
+    float f90 = 0.5 + 2.0 * roughness * LoH * LoH;
+    float lightScatter = F_Schlick(NoL, 1.0, f90);
+    float viewScatter = F_Schlick(NoV, 1.0, f90);
+    return lightScatter * viewScatter * (1.0 / PI);
+}
Listing 8: Implementation of the diffuse Disney BRDF in GLSL
+

+

Figure 11 shows a comparison between a simple Lambertian diffuse BRDF and the higher quality Disney diffuse BRDF, using a fully rough dielectric material. For comparison purposes, the right sphere was mirrored. The surface response is very similar with both BRDFs but the Disney one exhibits some nice retro-reflections at grazing angles (look closely at the left edge of the spheres).

+

+

 
Figure 11: Comparison between the Lambertian diffuse BRDF (left) and the Disney diffuse BRDF (right)
+

+We could allow artists/developers to choose the Disney diffuse BRDF depending on the quality they desire and the performance of the target device. It is important to note however that the Disney diffuse BRDF is not energy conserving as expressed here. +

+   

Standard model summary

+

+

Specular term: a Cook-Torrance specular microfacet model, with a GGX normal distribution function, a Smith-GGX height-correlated visibility function, and a Schlick Fresnel function.

+

+Diffuse term: a Lambertian diffuse model. +

+The full GLSL implementation of the standard model is shown in listing 9. +

 
float D_GGX(float NoH, float a) {
+    float a2 = a * a;
+    float f = (NoH * a2 - NoH) * NoH + 1.0;
+    return a2 / (PI * f * f);
+}
+
+vec3 F_Schlick(float u, vec3 f0) {
+    return f0 + (vec3(1.0) - f0) * pow(1.0 - u, 5.0);
+}
+
+float V_SmithGGXCorrelated(float NoV, float NoL, float a) {
+    float a2 = a * a;
+    float GGXL = NoV * sqrt((-NoL * a2 + NoL) * NoL + a2);
+    float GGXV = NoL * sqrt((-NoV * a2 + NoV) * NoV + a2);
+    return 0.5 / (GGXV + GGXL);
+}
+
+float Fd_Lambert() {
+    return 1.0 / PI;
+}
+
+void BRDF(...) {
+    vec3 h = normalize(v + l);
+
+    float NoV = abs(dot(n, v)) + 1e-5;
+    float NoL = clamp(dot(n, l), 0.0, 1.0);
+    float NoH = clamp(dot(n, h), 0.0, 1.0);
+    float LoH = clamp(dot(l, h), 0.0, 1.0);
+
+    // perceptually linear roughness to roughness (see parameterization)
+    float roughness = perceptualRoughness * perceptualRoughness;
+
+    float D = D_GGX(NoH, roughness);
+    vec3  F = F_Schlick(LoH, f0);
+    float V = V_SmithGGXCorrelated(NoV, NoL, roughness);
+
+    // specular BRDF
+    vec3 Fr = (D * V) * F;
+
+    // diffuse BRDF
+    vec3 Fd = diffuseColor * Fd_Lambert();
+
+    // apply lighting...
+}
Listing 9: Evaluation of the BRDF in GLSL
+   

Improving the BRDFs

+

+

We mentioned in section 4.3 that energy conservation is one of the key components of a good BRDF. Unfortunately the BRDFs explored previously suffer from two problems that we will examine below.

+

+   

Energy gain in diffuse reflectance

+

+

The Lambert diffuse BRDF does not account for the light that reflects at the surface and that is therefore not able to participate in the diffuse scattering event.

+

+[TODO: talk about the issue with fr+fd] +

+   

Energy loss in specular reflectance

+

+

The Cook-Torrance BRDF we presented earlier attempts to model several events at the microfacet level but does so by accounting for a single bounce of light. This approximation can cause a loss of energy at high roughness, the surface is not energy preserving. Figure 12 shows why this loss of energy occurs. In the single bounce (or single scattering) model, a ray of light hitting the surface can be reflected back onto another microfacet and thus be discarded because of the masking and shadowing term. If we however account for multiple bounces (multiscattering), the same ray of light might escape the microfacet field and be reflected back towards the viewer.

+

+

 
Figure 12: Single scattering (left) vs multiscattering
+

+Based on this simple explanation, we can intuitively deduce that the rougher a surface is, the higher the chances are that energy gets lost because of the failure to account for multiple scattering events. This loss of energy appears to darken rough materials. Metallic surfaces are particularly affected because all of their reflectance is specular. This darkening effect is illustrated in figure 13. With multiscattering, energy preservation can be achieved, as shown in figure 14. +

+

 
Figure 13: Darkening increases with roughness due to single scattering
+

+

 
Figure 14: Energy preservation with multiscattering
+

+We can use a white furnace, a uniform lighting environment set to pure white, to validate the energy preservation property of a BRDF. When energy preservation is achieved, a purely reflective metallic surface (\(\fNormal = 1\)) should be indistinguishable from the background, no matter the roughness of said surface. Figure 15 shows what such a surface looks like with the specular BRDF presented in the previous sections. The loss of energy as the roughness increases is obvious. In contrast, figure 16 shows that accounting for multiscattering events addresses the energy loss. +

+

 
Figure 15: Darkening increases with roughness due to single scattering
+

+

 
Figure 16: Energy preservation with multiscattering
+

+Multiple-scattering microfacet BRDFs are discussed in depth in [Heitz16]. Unfortunately this paper only presents a stochastic evaluation of the multiscattering BRDF. This solution is therefore not suitable for real-time rendering. Kulla and Conty present a different approach in [Kulla17]. Their idea is to add an energy compensation term as an additional BRDF lobe shown in equation \(\ref{energyCompensationLobe}\): +

+$$\begin{equation}\label{energyCompensationLobe} +f_{ms}(l,v) = \frac{(1 - E(l)) (1 - E(v)) F_{avg}^2 E_{avg}}{\pi (1 - E_{avg}) (1 - F_{avg}(1 - E_{avg}))} +\end{equation}$$ +

+Where \(E\) is the directional albedo of the specular BRDF \(f_r\), with \(\fNormal\) set to 1: +

+$$\begin{equation} +E(l) = \int_{\Omega} f(l,v) (\NoV) dv +\end{equation}$$ +

+The term \(E_{avg}\) is the cosine-weighted average of \(E\): +

+$$\begin{equation} +E_{avg} = 2 \int_0^1 E(\mu) \mu d\mu +\end{equation}$$ +

+Similarly, \(F_{avg}\) is the cosine-weighted average of the Fresnel term: +

+$$\begin{equation} +F_{avg} = 2 \int_0^1 F(\mu) \mu d\mu +\end{equation}$$ +

+Both terms \(E\) and \(E_{avg}\) can be precomputed and stored in lookup tables. while \(F_{avg}\) can be greatly simplified when the Schlick approximation is used: +

+$$\begin{equation}\label{averageFresnel} +F_{avg} = \frac{1 + 20 \fNormal}{21} +\end{equation}$$ +

+This new lobe is combined with the original single scattering lobe, previously noted \(f_r\): +

+$$\begin{equation} +f_{r}(l,v) = f_{ss}(l,v) + f_{ms}(l,v) +\end{equation}$$ +

+In [Lagarde18], with credit to Emmanuel Turquin, Lagarde and Golubev make the observation that equation \(\ref{averageFresnel}\) can be simplified to \(\fNormal\). They also propose to apply energy compensation by adding a scaled GGX specular lobe: +

+$$\begin{equation}\label{energyCompensation} +f_{ms}(l,v) = \fNormal \frac{1 - E(l)}{E(l)} f_{ss}(l,v) +\end{equation}$$ +

+The key insight is that \(E(l)\) can not only be precomputed but also shared with image-based lighting pre-integration. The multiscattering energy compensation formula thus becomes: +

+$$\begin{equation}\label{scaledEnergyCompensationLobe} +f_r(l,v) = f_{ss}(l,v) + \fNormal \left( \frac{1}{r} - 1 \right) f_{ss}(l,v) +\end{equation}$$ +

+Where \(r\) is defined as: +

+$$\begin{equation} +r = \int_{\Omega} D(l,v) V(l,v) \left< \NoL \right> dl +\end{equation}$$ +

+We can implement specular energy compensation at a negligible cost if we store \(r\) in the DFG lookup table presented in section 5.3. Listing 10 shows that the implementation is a direct conversion of equation \(\ref{scaledEnergyCompensationLobe}\). +

 
vec3 energyCompensation = 1.0 + f0 * (1.0 / dfg.y - 1.0);
+// Scale the specular lobe to account for multiscattering
+Fr *= pixel.energyCompensation;
Listing 10: Implementation of the energy compensation specular lobe
+

+

Please refer to section 5.3 and section 5.3.4.7 to learn how the DFG lookup table is derived and computed.

+

+   

Parameterization

+

+

Disney's material model described in [Burley12] is a good starting point but its numerous parameters makes it impractical for real-time implementations. In addition, we would like our standard material model to be easy to understand and easy to use for both artists and developers.

+

+   

Standard parameters

+

+

Table 2 describes the list of parameters that satisfy our constraints.

+

+

+  + + + + + + +
Parameter Definition
BaseColor Diffuse albedo for non-metallic surfaces, and specular color for metallic surfaces
Metallic Whether a surface appears to be dielectric (0.0) or conductor (1.0). Often used as a binary value (0 or 1)
Roughness Perceived smoothness (0.0) or roughness (1.0) of a surface. Smooth surfaces exhibit sharp reflections
Reflectance Fresnel reflectance at normal incidence for dielectric surfaces. This replaces an explicit index of refraction
Emissive Additional diffuse albedo to simulate emissive surfaces (such as neons, etc.) This parameter is mostly useful in an HDR pipeline with a bloom pass
Ambient occlusion Defines how much of the ambient light is accessible to a surface point. It is a per-pixel shadowing factor between 0.0 and 1.0. This parameter will be discussed in more details in the lighting section
Table 2: Parameters of the standard model
+

+Figure 17 shows how the metallic, roughness and reflectance parameters affect the appearance of a surface. +

+

 
Figure 17: From top to bottom: varying metallic, varying dielectric roughness, varying metallic roughness, varying reflectance
+

+   

Types and ranges

+

+

It is important to understand the type and range of the different parameters of our material model, described in table 3.

+

+

+  + + + + + + +
Parameter Type and range
BaseColor Linear RGB [0..1]
Metallic Scalar [0..1]
Roughness Scalar [0..1]
Reflectance Scalar [0..1]
Emissive Linear RGB [0..1] + exposure compensation
Ambient occlusion Scalar [0..1]
Table 3: Range and type of the standard model's parameters
+

+Note that the types and ranges described here are what the shader will expect. The API and/or tools UI could and should allow to specify the parameters using other types and ranges when they are more intuitive for artists. +

+For instance, the base color could be expressed in sRGB space and converted to linear space before being sent off to the shader. It can also be useful for artists to express the metallic, roughness and reflectance parameters as gray values between 0 and 255 (black to white). +

+Another example: the emissive parameter could be expressed as a color temperature and an intensity, to simulate the light emitted by a black body. +

+   

Remapping

+

+

To make the standard material model easier and more intuitive to use for artists, we must remap the parameters baseColor, roughness and reflectance.

+

+   

Base color remapping

+

+

The base color of a material is affected by the “metallicness” of said material. Dielectrics have achromatic specular reflectance but retain their base color as the diffuse color. Conductors on the other hand use their base color as the specular color and do not have a diffuse component.

+

+The lighting equations must therefore use the diffuse color and \(\fNormal\) instead of the base color. The diffuse color can easily be computed from the base color, as show in listing 11. +

 
vec3 diffuseColor = (1.0 - metallic) * baseColor.rgb;
Listing 11: Conversion of base color to diffuse in GLSL
+   

Reflectance remapping

+

+

Dielectrics

+

+The Fresnel term relies on \(\fNormal\), the specular reflectance at normal incidence angle, and is achromatic for dielectrics. We will use the remapping for dielectric surfaces described in [Lagarde14] : +

+$$\begin{equation} +\fNormal = 0.16 \cdot reflectance^2 +\end{equation}$$ +

+The goal is to map \(\fNormal\) onto a range that can represent the Fresnel values of both common dielectric surfaces (4% reflectance) and gemstones (8% to 16%). The mapping function is chosen to yield a 4% Fresnel reflectance value for an input reflectance of 0.5 (or 128 on a linear RGB gray scale). Figure 18 show those common values and how they relate to the mapping function. +

+

 
Figure 18: Common reflectance values
+

+If the index of refraction is known (for instance, an air-water interface has an IOR of 1.33), the Fresnel reflectance can be calculated as follows: +

+$$\begin{equation}\label{fresnelEquation} +\fNormal(n_{ior}) = \frac{(\nior - 1)^2}{(\nior + 1)^2} +\end{equation}$$ +

+And if the reflectance value is known, we can compute the corresponding IOR: +

+$$\begin{equation} +n_{ior} = \frac{2}{1 - \sqrt{\fNormal}} - 1 +\end{equation}$$ +

+Table 4 describes acceptable Fresnel reflectance values for various types of materials (no real world material has a value under 2%). +

+

+  + + + + + + + + + + + +
Material Reflectance IOR Linear value
Water 2% 1.33 0.35
Fabric 4% to 5.6% 1.5 to 1.62 0.5 to 0.59
Common liquids 2% to 4% 1.33 to 1.5 0.35 to 0.5
Common gemstones 5% to 16% 1.58 to 2.33 0.56 to 1.0
Plastics, glass 4% to 5% 1.5 to 1.58 0.5 to 0.56
Other dielectric materials 2% to 5% 1.33 to 1.58 0.35 to 0.56
Eyes 2.5% 1.38 0.39
Skin 2.8% 1.4 0.42
Hair 4.6% 1.55 0.54
Teeth 5.8% 1.63 0.6
Default value 4% 1.5 0.5
Table 4: Reflectance of common materials (source: Real-Time Rendering 4th Edition)
+

+Table 5 lists the \(\fNormal\) values for a few metals. The values are given in sRGB and must be used as the base color in our material model. Please refer to the annex, section 9.1, for an explanation of how these sRGB colors are computed from measured data. +

+

+  + + + + + + + + +
Metal \(\fNormal\) in sRGB Hexadecimal Color
Silver 0.97, 0.96, 0.91 #f7f4e8
 
Aluminum 0.91, 0.92, 0.92 #e8eaea
 
Titanium 0.76, 0.73, 0.69 #c1baaf
 
Iron 0.77, 0.78, 0.78 #c4c6c6
 
Platinum 0.83, 0.81, 0.78 #d3cec6
 
Gold 1.00, 0.85, 0.57 #ffd891
 
Brass 0.98, 0.90, 0.59 #f9e596
 
Copper 0.97, 0.74, 0.62 #f7bc9e
 
Table 5: \(\fNormal\) for common metals
+

+All materials have a Fresnel reflectance of 100% at grazing angles so we will set \(\fGrazing\) in the following way when evaluating the specular BRDF \(\fSpecular\): +

+$$\begin{equation} +\fGrazing = 1.0 +\end{equation}$$ +

+Figure 19 shows a red plastic ball. If you look closely at the edges of the sphere, you will be able to notice the achromatic specular reflectance at grazing angles. +

+

 
Figure 19: The specular reflectance becomes achromatic at grazing angles
+

+Conductors +

+The specular reflectance of metallic surfaces is chromatic: +

+$$\begin{equation} +\fNormal = baseColor \cdot metallic +\end{equation}$$ +

+Listing 12 shows how \(\fNormal\) is computed for both dielectric and metallic materials. It shows that the color of the specular reflectance is derived from the base color in the metallic case. +

 
vec3 f0 = 0.16 * reflectance * reflectance * (1.0 - metallic) + baseColor * metallic;
Listing 12: Computing \(\fNormal\) for dielectric and metallic materials in GLSL
+   

Roughness remapping and clamping

+

+

The roughness set by the user, called perceptualRoughness here, is remapped to a perceptually linear range using the following formulation:

+

+$$\begin{equation} +\alpha = perceptualRoughness^2 +\end{equation}$$ +

+Figure 20 shows a silver metallic surface with increasing roughness (from 0.0 to 1.0), using the unmodified roughness value (bottom) and the remapped value (top). +

+

 
Figure 20: Roughness remapping comparison: perceptually linear roughness (top) and roughness (bottom)
+

+Using this visual comparison, it is obvious that the remapped roughness is easier to understand by artists and developers. Without this remapping, shiny metallic surfaces would have to be confined to a very small range between 0.0 and 0.05. +

+Brent Burley made similar observations in his presentation [Burley12]. After experimenting with other remappings (cubic and quadratic mappings for instance), we have reached the conclusion that this simple square remapping delivers visually pleasing and intuitive results while being cheap for real-time applications. +

+Last but not least, it is important to note that the roughness parameters is used in various computations at runtime where limited floating point precision can become an issue. For instance, mediump precision floats are often implemented as half-floats (fp16) on mobile GPUs. +

+This cause problems when computing small values like \(\frac{1}{perceptualRoughness^4}\) in our lighting equations (roughness squared in the GGX computation). The smallest value that can be represented as a half-float is \(2^{-14}\) or \(6.1 \times 10^{-5}\). To avoid divisions by 0 on devices that do not support denormals, the result of \(\frac{1}{roughness^4}\) must therefore not be lower than \(6.1 \times 10^{-5}\). To do so, we must clamp the roughness to 0.089, which gives us \(6.274 \times 10^{-5}\). +

+Denormals should also be avoided to prevent performance drops. The roughness can also not be set to 0 to avoid obvious divisions by 0. +

+Since we also want specular highlights to have a minimum size (a roughness close to 0 creates almost invisible highlights), we should clamp the roughness to a safe range in the shader. This clamping has the added benefit of correcting specular aliasing1 that can appear for low roughness values. +

+

 1 The Frostbite engine clamps the roughness of analytical lights to 0.045 to reduce specular aliasing. This is possible when using single precision floats (fp32). +
+

+   

Blending and layering

+

+

As noted in [Burley12] and [Neubelt13], this model allows for robust blending between different materials by simply interpolating the different parameters. In particular, this allows to layer different materials using simple masks.

+

+For instance, figure 21 shows how the studio Ready at Dawn used material blending and layering in The Order: 1886 to create complex appearances from a library of simple materials (gold, copper, wood, rust, etc.). +

+

 
Figure 21: Material blending and layering. Source: Ready at Dawn Studios
+

+The blending and layering of materials is effectively an interpolation of the various parameters of the material model. Figure 22 show an interpolation between shiny metallic chrome and rough red plastic. While the intermediate blended materials make little physical sense, they look plausible. +

+

 
Figure 22: Interpolation from shiny chrome (left) to rough red plastic (right)
+

+   

Crafting physically based materials

+

+

Designing physically based materials is fairly easy once you understand the nature of the four main parameters: base color, metallic, roughness and reflectance.

+

+We provide a useful chart/reference guide to help artists and developers craft their own physically based materials. +

+

Crafting physically based materials
+

+In addition, here is a quick summary of how to use our material model: +

+

All materials

Base color should be devoid of lighting information, except for micro-occlusion. +

+ Metallic is almost a binary value. Pure conductors have a metallic value of 1 and pure dielectrics have a metallic value of 0. You should try to use values close at or close to 0 and 1. Intermediate values are meant for transitions between surface types (metal to rust for instance). +

Non-metallic materials

Base color represents the reflected color and should be an sRGB value in the range 50-240 (strict range) or 30-240 (tolerant range). +

+ Metallic should be 0 or close to 0. +

+ Reflectance should be set to 127 sRGB (0.5 linear, 4% reflectance) if you cannot find a proper value. Do not use values under 90 sRGB (0.35 linear, 2% reflectance). +

Metallic materials

Base color represents both the specular color and reflectance. Use values with a luminosity of 67% to 100% (170-255 sRGB). Oxidized or dirty metals should use a lower luminosity than clean metals to take into account the non-metallic components. +

+ Metallic should be 1 or close to 1. +

+ Reflectance is ignored (calculated from the base color). +

+   

Clear coat model

+

+

The standard material model described previously is a good fit for isotropic surfaces made of a single layer. Multi-layer materials are unfortunately fairly common, particularly materials with a thin translucent layer over a standard layer. Real world examples of such materials include car paints, soda cans, lacquered wood, acrylic, etc.

+

+

 
Figure 23: Comparison of a blue metallic surface under the standard material model (left) and the clear coat model (right)
+

+A clear coat layer can be simulated as an extension of the standard material model by adding a second specular lobe, which implies evaluating a second specular BRDF. To simplify the implementation and parameterization, the clear coat layer will always be isotropic and dielectric. The base layer can be anything allowed by the standard model (dielectric or conductor). +

+Since incoming light will traverse the clear coat layer, we must also take the loss of energy into account as shown in figure 24. Our model will however not simulate inter reflection and refraction behaviors. +

+

 
Figure 24: Clear coat surface model
+

+   

Clear coat specular BRDF

+

+

The clear coat layer will be modeled using the same Cook-Torrance microfacet BRDF used in the standard model. Since the clear coat layer is always isotropic and dielectric, with low roughness values (see section 4.9.3), we can choose cheaper DFG terms without notably sacrificing visual quality.

+

+A survey of the terms listed in [Karis13a] and [Burley12] shows that the Fresnel and NDF terms we already use in the standard model are not computationally more expensive than other terms. [Kelemen01] describes a much simpler term that can replace our Smith-GGX visibility term: +

+$$\begin{equation} +V(l,h) = \frac{1}{4(\LoH)^2} +\end{equation}$$ +

+This masking-shadowing function is not physically based, as shown in [Heitz14], but its simplicity makes it desirable for real-time rendering. +

+In summary, our clear coat BRDF is a Cook-Torrance specular microfacet model, with a GGX normal distribution function, a Kelemen visibility function, and a Schlick Fresnel function. Listing 13 shows how trivial the GLSL implementation is. +

 
float V_Kelemen(float LoH) {
+    return 0.25 / (LoH * LoH);
+}
Listing 13: Implementation of the Kelemen visibility term in GLSL
+

+

Note on the Fresnel term

+

+The Fresnel term of the specular BRDF requires \(\fNormal\), the specular reflectance at normal incidence angle. This parameter can be computed from an index of refraction of an interface. We will assume that our clear coat layer is made of polyurethane, a common compound used in coatings and varnishes, or similar. An air-polyurethane interface has an IOR of 1.5, from which we can deduce \(\fNormal\): +

+$$\begin{equation} +\fNormal(1.5) = \frac{(1.5 - 1)^2}{(1.5 + 1)^2} = 0.04 +\end{equation}$$ +

+This corresponds to a Fresnel reflectance of 4% that we know is associated with common dielectric materials. +

+   

Integration in the surface response

+

+

Because we must take into account the loss of energy caused by the addition of the clear coat layer, we can reformulate the BRDF from equation (\ref{brdf}) thusly:

+

+$$\begin{equation} +f(v,l)=\fDiffuse(v,l) (1 - F_c) + \fSpecular(v,l) (1 - F_c) + f_c(v,l) +\end{equation}$$ +

+Where \(F_c\) is the Fresnel term of the clear coat BRDF and \(f_c\) the clear coat BRDF +

+   

Clear coat parameterization

+

+

The clear coat material model encompasses all the parameters previously defined for the standard material mode, plus two parameters described in table 6.

+

+

+  + + +
Parameter Definition
ClearCoat Strength of the clear coat layer. Scalar between 0 and 1
ClearCoatRoughness Perceived smoothness or roughness of the clear coat layer. Scalar between 0 and 1
Table 6: Clear coat model parameters
+

+The clear coat roughness parameter is remapped and clamped in a similar way to the roughness parameter of the standard material. +

+Figure 25 and figure 26 show how the clear coat parameters affect the appearance of a surface. +

+

 
Figure 25: Clear coat varying from 0.0 (left) to 1.0 (right) with metallic set to 1.0 and roughness to 0.8
+

+

 
Figure 26: Clear coat roughness varying from 0.0 (left) to 1.0 (right) with metallic set to 1.0, roughness to 0.8 and clear coat to 1.0
+

+Listing 14 shows the GLSL implementation of the clear coat material model after remapping, parameterization and integration in the standard surface response. +

 
void BRDF(...) {
+    // compute Fd and Fr from standard model
+
+    // remapping and linearization of clear coat roughness
+    clearCoatPerceptualRoughness = clamp(clearCoatPerceptualRoughness, 0.089, 1.0);
+    clearCoatRoughness = clearCoatPerceptualRoughness * clearCoatPerceptualRoughness;
+
+    // clear coat BRDF
+    float  Dc = D_GGX(clearCoatRoughness, NoH);
+    float  Vc = V_Kelemen(clearCoatRoughness, LoH);
+    float  Fc = F_Schlick(0.04, LoH) * clearCoat; // clear coat strength
+    float Frc = (Dc * Vc) * Fc;
+
+    // account for energy loss in the base layer
+    return color * ((Fd + Fr * (1.0 - Fc)) * (1.0 - Fc) + Frc);
+}
Listing 14: Implementation of the clear coat BRDF in GLSL
+   

Base layer modification

+

+

The presence of a clear coat layer means that we should recompute (\fNormal), since it is normally based on an air-material interface. The base layer thus requires (\fNormal) to be computed based on a clear coat-material interface instead.

+

+This can be achieved by computing the material's index of refraction (IOR) from \(\fNormal\), then computing a new \(\fNormal\) based on the newly computed IOR and the IOR of the clear coat layer (1.5). +

+First, we compute the base layer's IOR: +

+$$ +IOR_{base} = \frac{1 + \sqrt{\fNormal}}{1 - \sqrt{\fNormal}} +$$ +

+Then we compute the new \(\fNormal\) from this new index of refraction: +

+$$ +f_{0_{base}} = \left( \frac{IOR_{base} - 1.5}{IOR_{base} + 1.5} \right) ^2 +$$ +

+Since the clear coat layer's IOR is fixed, we can combine both steps to simplify: +

+$$ +f_{0_{base}} = \frac{\left( 1 - 5 \sqrt{\fNormal} \right) ^2}{\left( 5 - \sqrt{\fNormal} \right) ^2} +$$ +

+We should also modify the base layer's apparent roughness based on the IOR of the clear coat layer but this is something we have opted to leave out for now. +

+   

Anisotropic model

+

+

The standard material model described previously can only describe isotropic surfaces, that is, surfaces whose properties are identical in all directions. Many real-world materials, such as brushed metal, can, however, only be replicated using an anisotropic model.

+

+

 
Figure 27: Comparison of isotropic material (left) and anisotropic material (right)
+

+   

Anisotropic specular BRDF

+

+

The isotropic specular BRDF described previously can be modified to handle anisotropic materials. Burley achieves this by using an anisotropic GGX NDF:

+

+$$\begin{equation} +D_{aniso}(h,\alpha) = \frac{1}{\pi \alpha_t \alpha_b} \frac{1}{((\frac{t \cdot h}{\alpha_t})^2 + (\frac{b \cdot h}{\alpha_b})^2 + (\NoH)^2)^2} +\end{equation}$$ +

+This NDF unfortunately relies on two supplemental roughness terms noted \(\alpha_b\), the roughness along the bitangent direction, and \(\alpha_t\), the roughness along the tangent direction. Neubelt and Pettineo [Neubelt13] propose a way to derive \(\alpha_b\) from \(\alpha_t\) by using an anisotropy parameter that describes the relationship between the two roughness values for a material: +

+$$ +\begin{align*} + \alpha_t &= \alpha \\ + \alpha_b &= lerp(0, \alpha, 1 - anisotropy) +\end{align*} +$$ +

+The relationship defined in [Burley12] is different, offers more pleasant and intuitive results, but is slightly more expensive: +

+$$ +\begin{align*} + \alpha_t &= \frac{\alpha}{\sqrt{1 - 0.9 \times anisotropy}} \\ + \alpha_b &= \alpha \sqrt{1 - 0.9 \times anisotropy} +\end{align*} +$$ +

+We instead opted to follow the relationship described in [Kulla17] as it allows creation of sharp highlights: +

+$$ +\begin{align*} + \alpha_t &= \alpha \times (1 + anisotropy) \\ + \alpha_b &= \alpha \times (1 - anisotropy) +\end{align*} +$$ +

+Note that this NDF requires the tangent and bitangent directions in addition to the normal direction. Since these directions are already needed for normal mapping, providing them may not be an issue. +

+The resulting implementation is described in listing 15. +

 
float at = max(roughness * (1.0 + anisotropy), 0.001);
+float ab = max(roughness * (1.0 - anisotropy), 0.001);
+
+float D_GGX_Anisotropic(float NoH, const vec3 h,
+        const vec3 t, const vec3 b, float at, float ab) {
+    float ToH = dot(t, h);
+    float BoH = dot(b, h);
+    float a2 = at * ab;
+    highp vec3 v = vec3(ab * ToH, at * BoH, a2 * NoH);
+    highp float v2 = dot(v, v);
+    float w2 = a2 / v2;
+    return a2 * w2 * w2 * (1.0 / PI);
+}
Listing 15: Implementation of Burley's anisotropic NDF in GLSL
+

+

In addition, [Heitz14] presents an anisotropic masking-shadowing function to match the height-correlated GGX distribution. The masking-shadowing term can be greatly simplified by using the visibility function instead:

+

+$$\begin{equation} +G(v,l,h,\alpha) = \frac{\chi^+(\VoH) \chi^+(\LoH)}{1 + \Lambda(v) + \Lambda(l)} +\end{equation}$$ +

+$$\begin{equation} +\Lambda(m) = \frac{-1 + \sqrt{1 + \alpha_0^2 tan^2(\theta_m)}}{2} = \frac{-1 + \sqrt{1 + \alpha_0^2 \frac{(1 - cos^2(\theta_m))}{cos^2(\theta_m)}}}{2} +\end{equation}$$ +

+Where: +

+$$\begin{equation} +\alpha_0 = \sqrt{cos^2(\phi_0)\alpha_x^2 + sin^2(\phi_0)\alpha_y^2} +\end{equation}$$ +

+After derivation we obtain: +

+$$\begin{equation} +V_{aniso}(\NoL,\NoV,\alpha) = \frac{1}{2((\NoL)\hat{\Lambda}_v+(\NoV)\hat{\Lambda}_l)} \\ +\hat{\Lambda}_v = \sqrt{\alpha^2_t(t \cdot v)^2+\alpha^2_b(b \cdot v)^2+(\NoV)^2} \\ +\hat{\Lambda}_l = \sqrt{\alpha^2_t(t \cdot l)^2+\alpha^2_b(b \cdot l)^2+(\NoL)^2} +\end{equation}$$ +

+The term \( \hat{\Lambda}_v \) is the same for every light and can be computed only once if needed. The resulting implementation is described in listing 16. +

 
float at = max(roughness * (1.0 + anisotropy), 0.001);
+float ab = max(roughness * (1.0 - anisotropy), 0.001);
+
+float V_SmithGGXCorrelated_Anisotropic(float at, float ab, float ToV, float BoV,
+        float ToL, float BoL, float NoV, float NoL) {
+    float lambdaV = NoL * length(vec3(at * ToV, ab * BoV, NoV));
+    float lambdaL = NoV * length(vec3(at * ToL, ab * BoL, NoL));
+    float v = 0.5 / (lambdaV + lambdaL);
+    return saturateMediump(v);
+}
Listing 16: Implementation of the anisotropic visibility function in GLSL
+   

Anisotropic parameterization

+

+

The anisotropic material model encompasses all the parameters previously defined for the standard material mode, plus an extra parameter described in table 7.

+

+

+  + +
Parameter Definition
Anisotropy Amount of anisotropy. Scalar between −1 and 1
Table 7: Anisotropic model parameters
+

+No further remapping is required. Note that negative values will align the anisotropy with the bitangent direction instead of the tangent direction. Figure 28 shows how the anisotropy parameter affect the appearance of a rough metallic surface. +

+

 
Figure 28: Anisotropy varying from 0.0 (left) to 1.0 (right)
+

+   

Subsurface model

+

+

[TODO]

+

+   

Subsurface specular BRDF

+

+

[TODO]

+

+   

Subsurface parameterization

+

+

[TODO]

+

+   

Cloth model

+

+

All the material models described previously are designed to simulate dense surfaces, both at a macro and at a micro level. Clothes and fabrics are however often made of loosely connected threads that absorb and scatter incident light. The microfacet BRDFs presented earlier do a poor job of recreating the nature of cloth due to their underlying assumption that a surface is made of random grooves that behave as perfect mirrors. When compared to hard surfaces, cloth is characterized by a softer specular lobe with a large falloff and the presence of fuzz lighting, caused by forward/backward scattering. Some fabrics also exhibit two-tone specular colors (velvets for instance).

+

+Figure 29 shows how a traditional microfacet BRDF fails to capture the appearance of a sample of denim fabric. The surface appears rigid (almost plastic-like), more similar to a tarp than a piece of clothing. This figure also shows how important the softer specular lobe caused by absorption and scattering is to the faithful recreation of the fabric. +

+

 
Figure 29: Comparison of denim fabric rendered using a traditional microfacet BRDF (left) and our cloth BRDF (right)
+

+Velvet is an interesting use case for a cloth material model. As shown in figure 30 this type of fabric exhibits strong rim lighting due to forward and backward scattering. These scattering events are caused by fibers standing straight at the surface of the fabric. When the incident light comes from the direction opposite to the view direction, the fibers will forward-scatter the light. Similarly, when the incident light from the same direction as the view direction, the fibers will scatter the light backward. +

+

 
Figure 30: Velvet fabric showcasing forward and backward scattering
+

+Since fibers are flexible, we should in theory model the ability to groom the surface. While our model does not replicate this characteristic, it does model a visible front facing specular contribution that can be attributed to the random variance in the direction of the fibers. +

+It is important to note that there are types of fabrics that are still best modeled by hard surface material models. For instance, leather, silk and satin can be recreated using the standard or anisotropic material models. +

+   

Cloth specular BRDF

+

+

The cloth specular BRDF we use is a modified microfacet BRDF as described by Ashikhmin and Premoze in [Ashikhmin07]. In their work, Ashikhmin and Premoze note that the distribution term is what contributes most to a BRDF and that the shadowing/masking term is not necessary for their velvet distribution. The distribution term itself is an inverted Gaussian distribution. This helps achieve fuzz lighting (forward and backward scattering) while an offset is added to simulate the front facing specular contribution. The so-called velvet NDF is defined as follows:

+

+$$\begin{equation} +D_{velvet}(v,h,\alpha) = c_{norm}(1 + 4 exp\left(\frac{-{cot}^2\theta_{h}}{\alpha^2}\right)) +\end{equation}$$ +

+This NDF is a variant of the NDF the same authors describe in [Ashikhmin00], notably modified to include an offset (set to 1 here) and an amplitude (4). In [Neubelt13], Neubelt and Pettineo propose a normalized version of this NDF: +

+$$\begin{equation} +D_{velvet}(v,h,\alpha) = \frac{1}{\pi(1 + 4\alpha^2)} (1 + 4 \frac{exp\left(\frac{-{cot}^2\theta_{h}}{\alpha^2}\right)}{{sin}^4\theta_{h}}) +\end{equation}$$ +

+For the full specular BRDF, we also follow [Neubelt13] and replace the traditional denominator with a smoother variant: +

+$$\begin{equation}\label{clothSpecularBRDF} +f_{r}(v,h,\alpha) = \frac{D_{velvet}(v,h,\alpha)}{4(\NoL + \NoV - (\NoL)(\NoV))} +\end{equation}$$ +

+The implementation of the velvet NDF is presented in listing 17, optimized to properly fit in half float formats and to avoid computing a costly cotangent, relying instead on trigonometric identities. Note that we removed the Fresnel component from this BRDF. +

 
float D_Ashikhmin(float roughness, float NoH) {
+    // Ashikhmin 2007, "Distribution-based BRDFs"
+ float a2 = roughness * roughness;
+ float cos2h = NoH * NoH;
+ float sin2h = max(1.0 - cos2h, 0.0078125); // 2^(-14/2), so sin2h^2 > 0 in fp16
+ float sin4h = sin2h * sin2h;
+ float cot2 = -cos2h / (a2 * sin2h);
+ return 1.0 / (PI * (4.0 * a2 + 1.0) * sin4h) * (4.0 * exp(cot2) + sin4h);
+}
Listing 17: Implementation of Ashikhmin's velvet NDF in GLSL
+

+

In [Estevez17] Estevez and Kulla propose a different NDF (called the “Charlie” sheen) that is based on an exponentiated sinusoidal instead of an inverted Gaussian. This NDF is appealing for several reasons: its parameterization feels more natural and intuitive, it provides a softer appearance and, as shown in equation (\ref{charlieNDF}), its implementation is simpler:

+

+$$\begin{equation}\label{charlieNDF} +D(m) = \frac{(2 + \frac{1}{\alpha}) sin(\theta)^{\frac{1}{\alpha}}}{2 \pi} +\end{equation}$$ +

+[Estevez17] also presents a new shadowing term that we omit here because of its cost. We instead rely on the visibility term from [Neubelt13] (shown in equation \(\ref{clothSpecularBRDF}\) above). +The implementation of this NDF is presented in listing 18, optimized to properly fit in half float formats. +

 
float D_Charlie(float roughness, float NoH) {
+    // Estevez and Kulla 2017, "Production Friendly Microfacet Sheen BRDF"
+    float invAlpha  = 1.0 / roughness;
+    float cos2h = NoH * NoH;
+    float sin2h = max(1.0 - cos2h, 0.0078125); // 2^(-14/2), so sin2h^2 > 0 in fp16
+    return (2.0 + invAlpha) * pow(sin2h, invAlpha * 0.5) / (2.0 * PI);
+}
Listing 18: Implementation of the “Charlie” NDF in GLSL
+   

Sheen color

+

+

To offer better control over the appearance of cloth and to give users the ability to recreate two-tone specular materials, we introduce the ability to directly modify the specular reflectance. Figure 31 shows an example of using the parameter we call “sheen color”.

+

+

 
Figure 31: Blue fabric without (left) and with (right) sheen
+

+   

Cloth diffuse BRDF

+

+

Our cloth material model still relies on a Lambertian diffuse BRDF. It is however slightly modified to be energy conservative (akin to the energy conservation of our clear coat material model) and offers an optional subsurface scattering term. This extra term is not physically based and can be used to simulate the scattering, partial absorption and re-emission of light in certain types of fabrics.

+

+First, here is the diffuse term without the optional subsurface scattering: +

+$$\begin{equation} +f_{d}(v,h) = \frac{c_{diff}}{\pi}(1 - F(v,h)) +\end{equation}$$ +

+Where \(F(v,h)\) is the Fresnel term of the cloth specular BRDF in equation \(\ref{clothSpecularBRDF}\). In practice we've opted to leave out the \(1 - F(v, h)\) term in the diffuse component. The effect is a bit subtle and we deemed it wasn't worth the added cost. +

+Subsurface scattering is implemented using the wrapped diffuse lighting technique, in its energy conservative form: +

+$$\begin{equation} +f_{d}(v,h) = \frac{c_{diff}}{\pi}(1 - F(v,h)) \left< \frac{\NoL + w}{(1 + w)^2} \right> \left< c_{subsurface} + \NoL \right> +\end{equation}$$ +

+Where \(w\) is a value between 0 and 1 defining by how much the diffuse light should wrap around the terminator. To avoid introducing another parameter, we fix \(w = 0.5\). Note that with wrap diffuse lighting, the diffuse term must not be multiplied by \(\NoL\). The effect of this cheap +subsurface scattering approximation can be seen in figure 32. +

+

 
Figure 32: White cloth (left column) vs white cloth with brown subsurface scattering (right)
+

+The complete implementation of our cloth BRDF, including sheen color and optional subsurface scattering, can be found in listing 19. +

 
// specular BRDF
+float D = distributionCloth(roughness, NoH);
+float V = visibilityCloth(NoV, NoL);
+vec3  F = sheenColor;
+vec3 Fr = (D * V) * F;
+
+// diffuse BRDF
+float diffuse = diffuse(roughness, NoV, NoL, LoH);
+#if defined(MATERIAL_HAS_SUBSURFACE_COLOR)
+// energy conservative wrap diffuse
+diffuse *= saturate((dot(n, light.l) + 0.5) / 2.25);
+#endif
+vec3 Fd = diffuse * pixel.diffuseColor;
+
+#if defined(MATERIAL_HAS_SUBSURFACE_COLOR)
+// cheap subsurface scatter
+Fd *= saturate(subsurfaceColor + NoL);
+vec3 color = Fd + Fr * NoL;
+color *= (lightIntensity * lightAttenuation) * lightColor;
+#else
+vec3 color = Fd + Fr;
+color *= (lightIntensity * lightAttenuation * NoL) * lightColor;
+#endif
Listing 19: Implementation of our cloth BRDF in GLSL
+   

Cloth parameterization

+

+

The cloth material model encompasses all the parameters previously defined for the standard material mode except for metallic and reflectance. Two extra parameters described in table 8 are also available.

+

+

+  + + +
Parameter Definition
SheenColor Specular tint to create two-tone specular fabrics (defaults to 0.04 to match the standard reflectance)
SubsurfaceColor Tint for the diffuse color after scattering and absorption through the material
Table 8: Cloth model parameters
+

+To create a velvet-like material, the base color can be set to black (or a dark color). Chromaticity information should instead be set on the sheen color. To create more common fabrics such as denim, cotton, etc. use the base color for chromaticity and use the default sheen color or set the sheen color to the luminance of the base color. +

+   

Lighting

+

+

The correctness and coherence of the lighting environment is paramount to achieving plausible visuals. After surveying existing rendering engines (such as Unity or Unreal Engine 4) as well as the traditional real-time rendering literature, it is obvious that coherency is rarely achieved.

+

+The Unreal Engine, for instance, lets artists specify the “brightness” of a point light in lumens, a unit of luminous power. The brightness of directional lights is however expressed using an arbitrary unnamed unit. To match the brightness of a point light with a luminous power of 5,000 lumens, the artist must use a directional light of brightness 10. This kind of mismatch makes it difficult for artists to maintain the visual integrity of a scene when adding, removing or modifying lights. +Using solely arbitrary units is a coherent solution but it makes reusing lighting rigs a difficult task. For instance, an outdoor scene will use a directional light of brightness 10 as the sun and all other lights will be defined relative to that value. Moving these lights to an indoor environment would make them too bright. +

+Our goal is therefore to make all lighting correct by default, while giving artists enough freedom to achieve the desired look. We will support a number of lights, split in two categories, direct and indirect lighting: +

+Direct lighting: punctual lights, photometric lights, area lights. +

+Indirect lighting: image based lights (IBLs), for both local2 and distant light probes. +

+

 2 Local light probes might be too expensive to support on mobile, we will first focus our efforts on distant light probes set at infinity +
+

+   

Units

+

+

The following sections will discuss how to implement various types of lights and the proposed equations make use of different symbols and units summarized in table 9.

+

+

+  + + + + + + + +
Photometric term Notation Unit
Luminous power \(\Phi\) Lumen (\(lm\))
Luminous intensity \(I\) Candela (\(cd\)) or \(\frac{lm}{sr}\)
Illuminance \(E\) Lux (\(lx\)) or \(\frac{lm}{m^2}\)
Luminance \(L\) Nit (\(nt\)) or \(\frac{cd}{m^2}\)
Radiant power \(\Phi_e\) Watt (\(W\))
Luminous efficacy \(\eta\) Lumens per watt (\(\frac{lm}{W}\))
Luminous efficiency \(V\) Percentage (%)
Table 9: Photometric units
+

+To get properly coherent lighting, we must use light units that respect the ratio between various light intensities found in real-world scenes. These intensities can vary greatly, from around 800 \(lm\) for a household light bulb to 120,000 \(lx\) for a daylight sky and sun illumination. +

+The easiest way to achieve lighting coherency is to adopt physical light units. This will in turn enable full reusability of lighting rigs. Using physical light units also allows us to use a physically based camera. +

+Table 10 shows the light unit associated with each type of light we intend to support. +

+

+  + + + + + + + +
Light type Unit
Directional light Illuminance (\(lx\) or \(\frac{lm}{m^2}\))
Point light Luminous power (\(lm\))
Spot light Luminous power (\(lm\))
Photometric light Luminous intensity (\(cd\))
Masked photometric light Luminous power (\(lm\))
Area light Luminous power (\(lm\))
Image based light Luminance (\(\frac{cd}{m^2}\))
Table 10: Intensity unity for each light type
+

+Notes about the radiant power unit +

+Even though commercially available light bulbs often display their brightness in lumens on the packaging, it is common to refer to the brightness of a light bulb by using its required energy in watts. The number of watts only indicates how much energy a bulb uses, not how bright it is. It is even more important to understand this difference now that more energy efficient bulbs are readily available (halogens, LEDs, etc.). +

+However, since artists might be accustomed to gauging a light's brightness by its power, we should allow users to use the power unit to define the brightness of a light. The conversion is presented in equation \(\ref{radiantPowerToLuminousPower}\). +

+$$\begin{equation}\label{radiantPowerToLuminousPower} +\Phi = \Phi_e \eta +\end{equation}$$ +

+In equation \(\ref{radiantPowerToLuminousPower}\), \(\eta\) is the luminous efficacy of the light, expressed in lumens per watt. Knowing that the maximum possible luminous efficacy is 683 \(\frac{lm}{W}\) we can also use luminous efficiency \(V\) (also called luminous coefficient), as shown in equation \(\ref{radiantPowerLuminousEfficiency}\). +

+$$\begin{equation}\label{radiantPowerLuminousEfficiency} +\Phi = \Phi_e 683 \times V +\end{equation}$$ +

+Table 11 can be used as a reference to convert watts to lumens using either the luminous efficacy or the luminous efficiency of various types of lights. More specific values are available on Wikipedia's luminous efficacy page. +

+

+  + + + +
Light type Efficacy \(\eta\) Efficiency \(V\)
Incandescent 14-35 2-5%
LED 28-100 4-15%
Fluorescent 60-100 9-15%
Table 11: Efficacy and efficiency of various light types
+

+   

Light units validation

+

+

One of the big advantages of using physical light units is the ability to physically validate our equations. We can use specialized devices to measure three light units.

+

+   

Illuminance

+

+

The illuminance reaching a surface can be measured using an incident light meter. For our tests, we use a Sekonic L-478D, shown in figure 33.

+

+The incident light meter uses a white diffuse dome to capture the illuminance reaching a surface. It is important to orient the dome properly depending on the desired measurement. For instance, orienting the dome perpendicular to the sun on a bright clear day will give very different results than orienting the dome horizontally. +

+

 
Figure 33: Sekonic L-478D incident light meter
+

+   

Luminance

+

+

The luminance at a surface, or the product of the incident light and the surface, can be measured using a luminance meter, also often called a spot meter. While incident light meters use a diffuse hemisphere to capture light from all directions, a spot meter uses a shield to measure incident light from a single direction. For our tests, we use a Sekonic 5° Viewfinder that can replace the diffuser on the L-478D to measure luminance in a 5° cone.

+

+

Sekonic L-478D working as a luminance meter using a special viewfinder
+

+   

Luminous intensity

+

+

The luminous intensity of a light source cannot be measured directly but can be derived from the measured illuminance if we know the distance between the measuring device and the light source. Equation (\ref{derivedLuminousIntensity}) is a simple application of the inverse square law discussed in section 5.2.2.

+

+$$\begin{equation}\label{derivedLuminousIntensity} +I = E \cdot d^2 +\end{equation}$$ +

+   

Direct lighting

+

+

We have defined the light units for all the light types supported by the renderer in the section above but we have not defined the light unit for the result of the lighting equations. Choosing physical light units means that we will compute luminance values in our shaders, and therefore that all our light evaluation functions will compute the luminance (L_{out}) (or outgoing radiance) at any given point. The luminance depends on the illuminance (E) and the BSDF (f(v,l)) :

+

+$$\begin{equation}\label{luminanceEquation} +L_{out} = f(v,l)E +\end{equation}$$ +

+   

Directional lights

+

+

The main purpose of directional lights is to recreate important light sources for outdoor environment, i.e. the sun and/or the moon. While directional lights do not truly exist in the physical world, any light source sufficiently far from the light receptor can be assumed to be directional (i.e. all the incident light rays are parallel, as shown in figure 34).

+

+

 
Figure 34: Interaction between a directional light and a surface. The light source is a virtual construct that can only be represented by a direction
+

+This approximation proves to work incredibly well for the diffuse response of a surface but the specular response is incorrect. The Frostbite engine solves this problem by treating the “sun” directional light as a disc area light. However, our tests have shown that the quality increase does not justify the added computational costs. +

+We earlier stated that we chose an illuminance light unit (\(lx\)) for directional lights. This is in part due to the fact that we can easily find illuminance values for the sky and the sun (online or with a light meter) but also to simplify the luminance equation described in \(\ref{luminanceEquation}\). +

+$$\begin{equation}\label{directionalLuminanceEquation} +L_{out} = f(v,l) E_{\bot} \left< \NoL \right> +\end{equation}$$ +

+In the simplified luminance equation \(\ref{directionalLuminanceEquation}\), \(E_{\bot}\) is the illuminance of the light source for a surface perpendicular to said light source. If the directional light source simulates the sun, \(E_{\bot}\) is the illuminance of the sun for a surface perpendicular to the sun direction. +

+Table 12 provides useful reference values for the sun and sky illumination, measured3 on a clear day in March, in California. +

+

+  + + + +
Light 10am 12pm 5:30pm
\(Sky_{\bot} + Sun_{\bot}\) 120,000 130,000 90,000
\(Sky_{\bot}\) 20,000 25,000 9,000
\(Sun_{\bot}\) 100,000 105,000 81,000
Table 12: Illuminance values in \(lx\) (a full moon has an illuminance of 1 \(lx\))
+

+Dynamic directional lights are particularly cheap to evaluate at runtime, as shown in listing 20. +

 
vec3 l = normalize(-lightDirection);
+float NoL = clamp(dot(n, l), 0.0, 1.0);
+
+// lightIntensity is the illuminance
+// at perpendicular incidence in lux
+float illuminance = lightIntensity * NoL;
+vec3 luminance = BSDF(v, l) * illuminance;
Listing 20: Implementation of directional lights in GLSL
+

+

Figure 35 shows the effect of lighting a simple scene with a directional light setup to approximate a midday Sun (illuminance set to 110,000 (lx)). For illustration purposes, only direct lighting is shown.

+

+

 
Figure 35: Series of dielectric materials of varying roughness under a directional light
+

+

 3 Measurements taken with an incident light meter (Sekonic L-478D) +
+

+   

Punctual lights

+

+

Our engine will support two types of punctual lights, commonly found in most if not all rendering engines: point lights and spot lights. These types of lights are traditionally physically inaccurate for two reasons:

+

+

    +
  1. They are truly punctual and infinitesimally small. +
  2. +
  3. They do not follow the inverse square law.
+

+The first issue can be addressed with area lights but, given the cheaper nature of punctual lights it is deemed practical to use infinitesimally small punctual lights whenever possible. +

+The second issue is easy to fix. For a given punctual light, the perceived intensity decreases proportionally to the square of the distance from the viewer (more precisely, the light receptor). +

+For punctual lights following the inverse square law, the term \(E\) of equation \( \ref{luminanceEquation} \) is expressed in equation \(\ref{punctualLightEquation}\), where \(d\) is the distance from a point at the surface to the light. +

+$$\begin{equation}\label{punctualLightEquation} +E = L_{in} \left< \NoL \right> = \frac{I}{d^2} \left< \NoL \right> +\end{equation}$$ +

+The difference between point and spot lights lies in how \(E\) is computed, and in particular how the luminous intensity \(I\) is computed from the luminous power \(\Phi\). +

+   

Point lights

+

+

A point light is defined only by a position in space, as shown in figure 36.

+

+

 
Figure 36: Interaction between a point light and a surface. The attenuation only depends on the distance to the light
+

+The luminous power of a point light is calculated by integrating the luminous intensity over the light's solid angle, as show in equation \(\ref{pointLightLuminousPower}\). The luminous intensity can then be easily derived from the luminous power. +

+$$\begin{equation}\label{pointLightLuminousPower} +\Phi = \int_{\Omega} I dl = \int_{0}^{2\pi} \int_{0}^{\pi} I d\theta d\phi = 4 \pi I \\ +I = \frac{\Phi}{4 \pi} +\end{equation}$$ +

+By simple substitution of \(I\) in \(\ref{punctualLightEquation}\) and \(E\) in \( \ref{luminanceEquation} \) we can formulate the luminance equation of a point light as a function of the luminous power (see \( \ref{pointLightLuminanceEquation} \)). +

+$$\begin{equation}\label{pointLightLuminanceEquation} +L_{out} = f(v,l) \frac{\Phi}{4 \pi d^2} \left< \NoL \right> +\end{equation}$$ +

+Figure 37 shows the effect of lighting a simple scene with a point light subject to distance attenuation. Light falloff is exaggerated for illustration purposes. +

+

 
Figure 37: Inverse square law applied to point lights evaluation
+

+   

Spot lights

+

+

A spot light is defined by a position in space, a direction vector and two cone angles, ( \theta_{inner} ) and ( \theta_{outer} ) (see figure 38). These two angles are used to define the angular falloff attenuation of the spot light. The light evaluation function of a spot light must therefore take into account both the inverse square law and these two angles to properly evaluate the luminance attenuation.

+

+

 
Figure 38: Interaction between a spot light and a surface. The attenuation depends on the distance to the light and the angle between the surface the spot light's direction vector
+

+Equation \( \ref{spotLightLuminousPower} \) describes how the luminous power of a spot light can be calculated in a similar fashion to point lights, using \( \theta_{outer} \) the outer angle of the spot light's cone in the range [0..\(\pi\)]. +

+$$\begin{equation}\label{spotLightLuminousPower} +\Phi = \int_{\Omega} I dl = \int_{0}^{2\pi} \int_{0}^{\theta_{outer}} I d\theta d\phi = 2 \pi (1 - cos\frac{\theta_{outer}}{2})I \\ +I = \frac{\Phi}{2 \pi (1 - cos\frac{\theta_{outer}}{2})} +\end{equation}$$ +

+While this formulation is physically correct, it makes spot lights a little difficult to use: changing the outer angle of the cone changes the illumination levels. Figure 39 shows the same scene lit by a spot light, with an outer angle of 55° and an outer angle of 15°. Observes how the illumination level increases as the cone aperture decreases. +

+

 
Figure 39: Comparison of spot light outer angles, 55° (left) and 15° (right)
+

+The coupling of illumination and the outer cone means that an artist cannot tweak the influence cone of a spot light without also changing the perceived illumination. It therefore makes sense to provide artists with a parameter to disable this coupling. Equations \( \ref{spotLightLuminousPowerB} \) shows how to formulate the luminous power for that purpose. +

+$$\begin{equation}\label{spotLightLuminousPowerB} +\Phi = \pi I \\ +I = \frac{\Phi}{\pi} \\ +\end{equation}$$ +

+With this new formulation to compute the luminous intensity, the test scene in figure 40 exhibits similar illumination levels with both cone apertures. +

+

 
Figure 40: Comparison of spot light outer angles, 55° (left) and 15° (right)
+

+This new formulation can also be considered physically based if the spot's reflector is replaced with a matte, diffuse mask that absorbs light perfectly. +

+The spot light evaluation function can be expressed in two ways: +

+

    +
  • With a light absorber + $$\begin{equation}\label{spotAbsorber} + L_{out} = f(v,l) \frac{\Phi}{\pi d^2} \left< \NoL \right> \lambda(l) + \end{equation}$$ +
  • +
  • With a light reflector + $$\begin{equation}\label{spotReflector} + L_{out} = f(v,l) \frac{\Phi}{2 \pi (1 - cos\frac{\theta_{outer}}{2}) d^2} \left< \NoL \right> \lambda(l) + \end{equation}$$
+

+The term \( \lambda(l) \) in equations \( \ref{spotAbsorber} \) and \( \ref{spotReflector} \) is the spot's angle attenuation factor described in equation + \( \ref{spotAngleAtt} \) below. +

+$$\begin{equation}\label{spotAngleAtt} +\lambda(l) = \frac{l \cdot spotDirection - cos\theta_{outer}}{cos\theta_{inner} - cos\theta_{outer}} +\end{equation}$$ +

+   

Attenuation function

+

+

A proper evaluation of the inverse square law attenuation factor is mandatory for physically based punctual lights. The simple mathematical formulation is unfortunately impractical for implementation purposes:

+

+

    +
  1. The division by the squared distance can lead to divides by 0 when objects intersect or “touch” light sources. +

    +

  2. +
  3. The influence sphere of each light is infinite (\( \frac{I}{d^2} \) is asymptotic, it never reaches 0) which means that to correctly shade a pixel we need to evaluate every light in the world.
+

+The first issue can be solved easily by setting the assumption that punctual lights are not truly punctual but instead small area lights. To do this we can simply treat punctual lights as spheres of 1 cm radius, as show in equation \(\ref{finitePunctualLight}\). +

+$$\begin{equation}\label{finitePunctualLight} +E = \frac{I}{max(d^2, {0.01}^2)} +\end{equation}$$ +

+We can solve the second issue by introducing an influence radius for each light. There are several advantages to this solution. Tools can quickly show artists what parts of the world will be influenced by every light (the tool just needs to draw a sphere centered on each light). The rendering engine can cull lights more aggressively using this extra piece of information and artists/developers can assist the engine by manually tweaking the influence radius of a light. +

+Mathematically, the illuminance of a light should smoothly reach zero at the limit defined by the influence radius. [Karis13b] proposes to window the inverse square function in such a way that the majority of the light's influence remains unaffected. The proposed windowing is described in equation \(\ref{attenuationWindowing}\), where \(r\) is the light's radius of influence. +

+$$\begin{equation}\label{attenuationWindowing} +E = \frac{I}{max(d^2, {0.01}^2)} \left< 1 - \frac{d^4}{r^4} \right>^2 +\end{equation}$$ +

+Listing 21 demonstrates how to implement physically based punctual lights in GLSL. Note that the light intensity used in this piece of code is the luminous intensity \(I\) in \(cd\), converted from the luminous power CPU-side. This snippet is not optimized and some of the computations can be offloaded to the CPU (for instance the square of the light's inverse falloff radius, or the spot scale and angle). +

 
float getSquareFalloffAttenuation(vec3 posToLight, float lightInvRadius) {
+    float distanceSquare = dot(posToLight, posToLight);
+    float factor = distanceSquare * lightInvRadius * lightInvRadius;
+    float smoothFactor = max(1.0 - factor * factor, 0.0);
+    return (smoothFactor * smoothFactor) / max(distanceSquare, 1e-4);
+}
+
+float getSpotAngleAttenuation(vec3 l, vec3 lightDir,
+        float innerAngle, float outerAngle) {
+    // the scale and offset computations can be done CPU-side
+    float cosOuter = cos(outerAngle);
+    float spotScale = 1.0 / max(cos(innerAngle) - cosOuter, 1e-4)
+    float spotOffset = -cosOuter * spotScale
+
+    float cd = dot(normalize(-lightDir), l);
+    float attenuation = clamp(cd * spotScale + spotOffset, 0.0, 1.0);
+    return attenuation * attenuation;
+}
+
+vec3 evaluatePunctualLight() {
+    vec3 l = normalize(posToLight);
+    float NoL = clamp(dot(n, l), 0.0, 1.0);
+    vec3 posToLight = lightPosition - worldPosition;
+
+    float attenuation;
+    attenuation  = getSquareFalloffAttenuation(posToLight, lightInvRadius);
+    attenuation *= getSpotAngleAttenuation(l, lightDir, innerAngle, outerAngle);
+
+    vec3 luminance = (BSDF(v, l) * lightIntensity * attenuation * NoL) * lightColor;
+    return luminance;
+}
Listing 21: Implementation of punctual lights in GLSL
+   

Photometric lights

+

+

Punctual lights are an extremely practical and efficient way to light a scene but do not give artists enough control over the light distribution. The field of architectural lighting design concerns itself with designing lighting systems to serve humans needs by taking into account:

+

+

    +
  • The amount of light provided +
  • +
  • The color of the light +
  • +
  • The distribution of light within the space
+

+The lighting system we have described so far can easily address the first two points but we need a way to define the distribution of light within the space. Light distribution is especially important for indoor scenes or for some types of outdoor scenes or even road lighting. Figure 41 shows scenes where the light distribution is controlled by the artist. This type of distribution control is widely used when putting objects on display (museums, stores or galleries for instance). +

+

 
Figure 41: Controlling the distribution of a point light
+

+Photometric lights use a photometric profile to describe their intensity distribution. There are two commonly used formats, IES (Illuminating Engineering Society) and EULUMDAT (European Lumen Data format) but we will focus on the former. IES profiles are supported by many tools and engines, such as Unreal Engine 4, Frostbite, Renderman, Maya and Killzone. In addition, IES light profiles are commonly made available by bulbs and luminaires manufacturers (Philips offers an extensive array of IES files for download for instance). Photometric profiles are particularly useful when they measure a luminaire or light fixture, in which the light source is partially covered. The luminaire will block the light emitted in certain directions, thus shaping the light distribution. +

+

Example of a real world luminaires that can be described by photometric profiles
+

+An IES profile stores luminous intensity for various angles on a sphere around the measured light source. This spherical coordinate system is usually referred to as the photometric web, which can be visualized using specialized tools such as IESviewer. Figure 42 below shows the photometric web of the XArrow IES profile provided by Pixar for use with Renderman. This picture also shows a rendering in 3D space of the XArrow IES profile by our tool lightgen. +

+

 
Figure 42: The XArrow IES profile rendered as a photometric web and as a point light in 3D space
+

+The IES format is poorly documented and it is not uncommon to find syntax variations between files found on the Internet. The best resource to understand IES profile is Ian Ashdown's “Parsing the IESNA LM-63 photometric data file” document [Ashdown98]. Succinctly, an IES profiles stores luminous intensities in candela at various angles around the light source. For each measured horizontal angle, a series of luminous intensities at different vertical angles is provided. It is however fairly common for measured light sources to be horizontally symmetrical. The XArrow profile shown above is a good example: intensities vary with vertical angles (vertical axis) but are symmetrical on the horizontal axis. The range of vertical angles in an IES profile is 0 to 180° and the range of horizontal angles is 0 to 360°. +

+Figure 43 shows the series of IES profiles provided by Pixar for Renderman, rendered using our lightgen tool. +

+

 
Figure 43: Series of IES light profiles rendered with lightgen
+

+IES profiles can be applied directly to any punctual light, point or spot. To do so, we must first process the IES profile and generate a photometric profile as a texture. For performance considerations, the photometric profile we generate is a 1D texture that represents the average luminous intensity for all horizontal angles at a specific vertical angle (i.e., each pixel represents a vertical angle). To truly represent a photometric light, we should use a 2D texture but since most lights are fully, or mostly, symmetrical on the horizontal plane, we can accept this approximation. The values stored in the texture are normalized by the inverse maximum intensity defined in the IES profile. This allows us to easily store the texture in any float format or, at the cost of a bit of precision, in a luminance 8-bit texture (grayscale PNG for instance). Storing normalized values also allows us to treat photometric profiles as a mask: +

+

Photometric profile as a mask

The luminous intensity is defined by the artist by setting the luminous power of the light, as with any other punctual light. The artist defined intensity is divided by the intensity of the light computed from the IES profile. IES profiles contain a luminous intensity but it is only valid for a bare light bulb whereas the measured intensity values take into account the light fixture. To measure the intensity of the luminaire, instead of the bulb, we perform a Monte-Carlo integration of the unit sphere using the intensities from the profile4. +

Photometric profile

The luminous intensity comes from the profile itself. All the values sampled from the 1D texture are simply multiplied by the maximum intensity. We also provide a multiplier for convenience. +

The photometric profile can be applied at rendering time as a simple attenuation. The luminance equation \( \ref{photometricLightEvaluation} \) describes the photometric point light evaluation function. +

+$$\begin{equation}\label{photometricLightEvaluation} +L_{out} = f(v,l) \frac{I}{d^2} \left< \NoL \right> \Psi(l) +\end{equation}$$ +

+The term \( \Psi(l) \) is the photometric attenuation function. It depends on the light vector, but also on the direction of the light. Spot lights already possess a direction vector but we need to introduce one for photometric point lights as well. +

+The photometric attenuation function can be easily implemented in GLSL by adding a new attenuation factor to the implementation of punctual lights (listing 21). The modified implementation is show in listing 22. +

 
float getPhotometricAttenuation(vec3 posToLight, vec3 lightDir) {
+    float cosTheta = dot(-posToLight, lightDir);
+    float angle = acos(cosTheta) * (1.0 / PI);
+    return texture2DLodEXT(lightProfileMap, vec2(angle, 0.0), 0.0).r;
+}
+
+vec3 evaluatePunctualLight() {
+    vec3 l = normalize(posToLight);
+    float NoL = clamp(dot(n, l), 0.0, 1.0);
+    vec3 posToLight = lightPosition - worldPosition;
+
+    float attenuation;
+    attenuation  = getSquareFalloffAttenuation(posToLight, lightInvRadius);
+    attenuation *= getSpotAngleAttenuation(l, lightDirection, innerAngle, outerAngle);
+    attenuation *= getPhotometricAttenuation(l, lightDirection);
+
+    float luminance = (BSDF(v, l) * lightIntensity * attenuation * NoL) * lightColor;
+    return luminance;
+}
Listing 22: Implementation of attenuation from photometric profiles in GLSL
+

+

The light intensity is computed CPU-side (listing 23) and depends on whether the photometric profile is used as a mask.

+

 
float multiplier;
+// Photometric profile used as a mask
+if (photometricLight.isMasked()) {
+    // The desired intensity is set by the artist
+    // The integrated intensity comes from a Monte-Carlo
+    // integration over the unit sphere around the luminaire
+    multiplier = photometricLight.getDesiredIntensity() /
+            photometricLight.getIntegratedIntensity();
+} else {
+    // Multiplier provided for convenience, set to 1.0 by default
+    multiplier = photometricLight.getMultiplier();
+}
+
+// The max intensity in cd comes from the IES profile
+float lightIntensity = photometricLight.getMaxIntensity() * multiplier;
Listing 23: Computing the intensity of a photometric light on the CPU
+

+

 4 The XArrow profile declares a luminous intensity of 1,750 lm but a Monte-Carlo integration shows an intensity of only 350 lm. +
+

+   

Area lights

+

+

[TODO]

+

+   

Lights parameterization

+

+

Similarly to the parameterization of the standard material model, our goal is to make lights parameterization intuitive and easy to use for artists and developers alike. In that spirit, we decided to separate the light color (or hue) from the light intensity. A light color will therefore be defined as a linear RGB color (or sRGB in the tools UI for convenience).

+

+The full list of light parameters is presented in table 13. +

+

+  + + + + + + + + + + + + +
Parameter Definition
Type Directional, point, spot or area
Direction Used for directional lights, spot lights, photometric point lights, and linear and tubular area lights (orientation)
Color The color of emitted light, as a linear RGB color. Can be specified as an sRGB color or a color temperature in the tools
Intensity The light's brightness. The unit depends on the type of light
Falloff radius Maximum distance of influence
Inner angle Angle of the inner cone for spot lights, in degrees
Outer angle Angle of the outer cone for spot lights, in degrees
Length Length of the area light, used to create linear or tubular lights
Radius Radius of the area light, used to create spherical or tubular lights
Photometric profile Texture representing a photometric light profile, works only for punctual lights
Masked profile Boolean indicating whether the IES profile is used as a mask or not. When used as a mask, the light's brightness will be multiplied by the ratio between the user specified intensity and the integrated IES profile intensity. When not used as a mask, the user specified intensity is ignored but the IES multiplier is used instead
Photometric multiplier Brightness multiplier for photometric lights (if IES as mask is turned off)
Table 13: Light types parameters
+

+Note: to simplify the implementation, all luminous powers will converted to luminous intensities (\(cd\)) before being sent to the shader. The conversion is light dependent and is explained in the previous sections. +

+Note: the light type can be inferred from other parameters (e.g. a point light has a length, radius, inner angle and outer angle of 0). +

+   

Color temperature

+

+

However, real-world artificial lights are often defined by their color temperature, measured in Kelvin (K). The color temperature of a light source is the temperature of an ideal black-body radiator that radiates light of comparable hue to that of the light source. For convenience, the tools should allow the artist to specify the hue of a light source as a color temperature (a meaningful range is 1,000 K to 12,500 K).

+

+To compute RGB values from a temperature, we can use the Planckian locus, shown in figure 44. This locus is the path that the color of an incandescent black body takes in a chromaticity space as the body's temperature changes. +

+

 
Figure 44: The Planckian locus visualized on a CIE 1931 chromaticity diagram (source: Wikipedia)
+

+The easiest way to compute RGB values from this locus is to use the formula described in [Krystek85]. Krystek's algorithm (equation \(\ref{krystek}\)) works in the CIE 1960 (UCS) space, using the following formula where \(T\) is the desired temperature, and \(u\) and \(v\) the coordinates in UCS. +

+$$\begin{equation}\label{krystek} +u(T) = \frac{0.860117757 + 1.54118254 \times 10^{-4}T + 1.28641212 \times 10^{-7}T^2}{1 + 8.42420235 \times 10^{-4}T + 7.08145163 \times 10^{-7}T^2} \\ +v(T) = \frac{0.317398726 + 4.22806245 + \times 10^{-5}T + 4.20481691 \times 10^{-8}T^2}{1 - 2.89741816 + \times 10^{-5}T + 1.61456053 \times 10^{-7}T^2} +\end{equation}$$ +

+This approximation is accurate to roughly \( 9 \times 10^{-5} \) in the range 1,000K to 15,000K. From the CIE 1960 space we can compute the coordinates in xyY space (CIES 1931), using the formula from equation \(\ref{cieToxyY}\). +

+$$\begin{equation}\label{cieToxyY} +x = \frac{3u}{2u - 8v + 4} \\ +y = \frac{2v}{2u - 8v + 4} +\end{equation}$$ +

+The formulas above are valid for black body color temperatures, and therefore correlated color temperatures of standard illuminants. If we wish to compute the precise chromaticity coordinates of standard CIE illuminants in the D series we can use equation \(\ref{seriesDtoxyY}\). +

+$$\begin{equation}\label{seriesDtoxyY} +x = \begin{cases} 0.244063 + 0.09911 \frac{10^3}{T} + 2.9678 \frac{10^6}{T^2} - 4.6070 \frac{10^9}{T^3} & 4,000K \le T \le 7,000K \\ +0.237040 + 0.24748 \frac{10^3}{T} + 1.9018 \frac{10^6}{T^2} - 2.0064 \frac{10^9}{T^3} & 7,000K \le T \le 25,000K \end{cases} \\ +y = -3x^2 + 2.87 x - 0.275 +\end{equation}$$ +

+From the xyY space, we can then convert to the CIE XYZ space (equation \(\ref{xyYtoXYZ}\)). +

+$$\begin{equation}\label{xyYtoXYZ} +X = \frac{xY}{y} \\ +Z = \frac{(1 - x - y)Y}{y} +\end{equation}$$ +

+For our needs, we will fix \(Y = 1\). This allows us to convert from the XYZ space to linear RGB with a simple 3×3 matrix, as shown in equation \(\ref{XYZtoRGB}\). +

+$$\begin{equation}\label{XYZtoRGB} +\left[ \begin{matrix} R \\ G \\ B \end{matrix} \right] = M^{-1} \left[ \begin{matrix} X \\ Y \\ Z \end{matrix} \right] +\end{equation}$$ +

+The transformation matrix M is calculated from the target RGB color space primaries. Equation \( \ref{XYZtoRGBValues} \) shows the conversion using the inverse matrix for the sRGB color space. +

+$$\begin{equation}\label{XYZtoRGBValues} +\left[ \begin{matrix} R \\ G \\ B \end{matrix} \right] = \left[ \begin{matrix} 3.2404542 & -1.5371385 & -0.4985314 \\ -0.9692660 & 1.8760108 & 0.0415560 \\ 0.0556434 & -0.2040259 & 1.0572252 \end{matrix} \right] \left[ \begin{matrix} X \\ Y \\ Z \end{matrix} \right] +\end{equation}$$ +

+The result of these operations is a linear RGB triplet in the sRGB color space. Since we care about the chromaticity of the results, we must apply a normalization step to avoid clamping values greater than 1.0 and distort resulting colors: +

+$$\begin{equation}\label{normalizedRGB} +\hat{C}_{linear} = \frac{C_{linear}}{max(C_{linear})} +\end{equation}$$ +

+We must finally apply the sRGB opto-electronic conversion function (OECF, shown in equation \( \ref{OECFsRGB} \)) to obtain a displayable value (the value should remain linear if passed to the renderer for shading). +

+$$\begin{equation}\label{OECFsRGB} +C_{sRGB} = \begin{cases} 12.92 \times \hat{C}_{linear} & \hat{C}_{linear} \le 0.0031308 \\ +1.055 \times \hat{C}_{linear}^{\frac{1}{2.4}} - 0.055 & \hat{C}_{linear} \gt 0.0031308 \end{cases} +\end{equation}$$ +

+For convenience, figure 45 shows the range of correlated color temperatures from 1,000K to 12,500K. All the colors used below assume CIE \( D_{65} \) as the white point (as is the case in the sRGB color space). +

+

 
Figure 45: Scale of correlated color temperatures
+

+Similarly, figure 46 shows the range of CIE standard illuminants series D from 1,000K to 12,500K. +

+

 
Figure 46: Scale of CIE standard illuminants series D
+

+For reference, figure 47 shows the range of correlated color temperatures without the normalization step presented in equation \(\ref{normalizedRGB}\). +

+

 
Figure 47: Unnormalized scale of correlated color temperatures
+

+Table 14 presents the correlated color temperature of various common light sources as sRGB color swatches. These colors are relative to the \( D_{65} \) white point, so their perceived hue might vary based on your display's white point. See What colour is the Sun? for more information. +

+

+  + + + + + + + + + + + + + + + + +
Temperature (K) Light source Color
1,700-1,800 Match flame
 
1,850-1,930 Candle flame
 
2,000-3,000 Sun at sunrise/sunset
 
2,500-2,900 Household tungsten lightbulb
 
3,000 Tungsten lamp 1K
 
3,200-3,500 Quartz lights
 
3,200-3,700 Fluorescent lights
 
3,275 Tungsten lamp 2K
 
3,380 Tungsten lamp 5K, 10K
 
5,000-5,400 Sun at noon
 
5,500-6,500 Daylight (sun + sky)
 
5,500-6,500 Sun through clouds/haze
 
6,000-7,500 Overcast sky
 
6,500 RGB monitor white point
 
7,000-8,000 Shaded areas outdoors
 
8,000-10,000 Partly cloudy sky
 
Table 14: Normalized correlated color temperatures for common light sources
+

+   

Pre-exposed lights

+

+

Physically based rendering and physical light units pose an interesting challenge: how to store and handle the large range of values produced by the lighting code? Assuming computations performed at full precision in the shaders, we still want to be able to store the linear output of the lighting pass in a reasonably sized buffer (RGB16F or equivalent). The most obvious and easiest way to achieve this is to simply apply the camera exposure (see the Physically based camera section for more information) before writing out the result of the lighting pass. This simple step is shown in listing 24:

+

 
fragColor = luminance * camera.exposure;
Listing 24: The output of the lighting pass is pre-exposed to fit in half-float buffers
+

+

This solution solves the storage problem but requires intermediate computations to be performed with single precision floats. We would instead prefer to perform all (or at least most) of the lighting work using half precision floats instead. Doing so can greatly improve performance and power usage, particularly on mobile devices. Half precision floats are however ill-suited for this kind of work as common illuminance and luminance values (for the sun for instance) can exceed their range. The solution is to simply pre-expose the lights themselves instead of the result of the lighting pass. This can be done efficiently on the CPU if updating a light's constant buffer is cheap. This can also be done on the GPU, as shown in listing 25.

+

 
// The inputs must be highp/single precision,
+// both for range (intensity) and precision (exposure)
+// The output is mediump/half precision
+float computePreExposedIntensity(highp float intensity, highp float exposure) {
+    return intensity * exposure;
+}
+
+Light getPointLight(uint index) {
+    Light light;
+    uint lightIndex = // fetch light index;
+
+    // the intensity must be highp/single precision
+    highp vec4 colorIntensity  = lightsUniforms.lights[lightIndex][1];
+
+    // pre-expose the light
+    light.colorIntensity.w = computePreExposedIntensity(
+            colorIntensity.w, frameUniforms.exposure);
+
+    return light;
+}
Listing 25: Pre-exposing lights allows the entire shading pipeline to use half precision floats
+

+

In practice we pre-expose the following lights:

+

+

    +
  • Punctual lights (point and spot): on the GPU +
  • +
  • Directional light: on the CPU +
  • +
  • IBLs: on the CPU +
  • +
  • Material emissive: on the GPU
+

+   

Image based lights

+

+

In real life, light comes from every direction either directly from light sources or indirectly after bouncing off objects in the environment, being partially absorbed in the process. In a way the whole environment around an object can be seen as a light source. Images, in particular cubemaps, are a great way to encode such an “environment light”. This is called Image Based Lighting (IBL) or sometimes Indirect Lighting.

+

+

 
Figure 48: The object shown here is lit only by image-encoded environment lights. Notice the subtle lighting effects that can be applied using this technique.
+

+There are limitations with image-based lighting. Obviously the environment image must be acquired somehow and as we'll see below it needs to be pre-processed before it can be used for lighting. Typically, the environment image is acquired offline in the real world, or generated by the engine either offline or at run time; either way, local or distant probes are used. +

+These probes can be used to acquire the distant or local environment. In this document, we're focusing on distant environment probes, where the light is assumed to come from infinitely far away (which means every point on the object's surface uses the same environment map). +

+The whole environment contributes light to a given point on the object's surface; this is called irradiance (\(E\)). The resulting light bouncing off of the object is called radiance (\(L_{out}\)). Incident lighting must be applied consistently to the diffuse and specular parts of the BRDF. +

+The radiance \(L_{out}\) resulting from the interaction between an image based light's (IBL) irradiance and a material model (BRDF) \(f(\Theta)\)5 is computed as follows: +

+$$\begin{equation} +L_{out}(n, v, \Theta) = \int_\Omega f(l, v, \Theta) L_{\bot}(l) \left< \NoL \right> dl +\end{equation}$$ +

+Note that here we're looking at the behavior of the surface at macro level (not to be confused with the micro level equation), which is why it only depends on \(\vec n\) and \(\vec v\). Essentially, we're applying the BRDF to “point-lights” coming from all directions and encoded in the IBL. +

+   

IBL Types

+

+

There are four common types of IBLs used in modern rendering engines:

+

+

    +
  • Distant light probes, used to capture lighting information at “infinity”, where parallax can be ignored. Distant probes typically contain the sky, distant landscape features or buildings, etc. They are either captured by the engine or acquired from a camera as high dynamic range images (HDRI). +

    +

  • +
  • Local light probes, used to capture a certain area of the world from a specific point of view. The capture is projected on a cube or sphere depending on the surrounding geometry. Local probes are more accurate than distance probes and are particularly useful to add local reflections to materials. +

    +

  • +
  • Planar reflections, used to capture reflections by rendering the scene mirrored by a plane. This technique works only for flat surfaces such as building floors, roads and water. +

    +

  • +
  • Screen space reflection, used to capture reflections based on the rendered scene (using the previous frame for instance) by ray-marching in the depth buffer. SSR gives great result but can be very expensive.
+

+In addition we must distinguish between static and dynamic IBLs. Implementing a fully dynamic day/night cycle requires for instance to recompute the distant light probes dynamically6. Both planar and screen space reflections are inherently dynamic. +

+   

IBL Unit

+

+

As discussed previously in the direct lighting section, all our lights must use physical units. As such our IBLs will use the luminance unit (\frac{cd}{m^2}), which is also the output unit of all our direct lighting equations. Using the luminance unit is straightforward for light probes captures by the engine (dynamically or statically offline).

+

+High dynamic range images are a bit more delicate to handle however. Cameras do not record measured luminance but a device-dependent value that is only related to the original scene luminance. As such, we must provide artists with a multiplier that allows them to recover, or at the very least closely approximate, the original absolute luminance. +

+To properly reconstruct the luminance of an HDRI for IBL, artists must do more than simply take photos of the environment and record extra information: +

+

    +
  • Color calibration: using a gray card or a MacBeth ColorChecker +

    +

  • +
  • Camera settings: aperture, shutter and ISO +

    +

  • +
  • Luminance samples: using a spot/luminance meter
+

+[TODO] Measure and list common luminance values (clear sky, interior, etc.) +

+   

Processing light probes

+

+

We saw previously that the radiance of an IBL is computed by integrating over the surface's hemisphere. Since this would obviously be too expensive to do in real-time, we must first pre-process our light probes to convert them into a format better suited for real-time interactions.

+

+The sections below will discuss the techniques used to accelerate the evaluation of light probes: +

+

    +
  • Specular reflectance: pre-filtered importance sampling and split-sum approximation +

    +

  • +
  • Diffuse reflectance: irradiance map and spherical harmonics
+

+   

Distant light probes

+   

Diffuse BRDF integration

+

+

Using the Lambertian BRDF7, we get the radiance:

+

+$$ +\begin{align*} + f_d(\sigma) &= \frac{\sigma}{\pi} \\ +L_d(n, \sigma) &= \int_{\Omega} f_d(\sigma) L_{\bot}(l) \left< \NoL \right> dl \\ + &= \frac{\sigma}{\pi} \int_{\Omega} L_{\bot}(l) \left< \NoL \right> dl \\ + &= \frac{\sigma}{\pi} E_d(n) \quad \text{with the irradiance} \; + E_d(n) = \int_{\Omega} L_{\bot}(l) \left< \NoL \right> dl +\end{align*} +$$ +

+Or in the discrete domain: +

+$$ E_d(n) \equiv \sum_{\forall \, i \in image} L_{\bot}(s_i) \left< n \cdot s_i \right> \Omega_s $$ +

+\(\Omega_s\) is the solid-angle8 associated to sample \(i\). +

+The irradiance integral \(\Ed\) can be trivially, albeit slowly9, precomputed and stored into a cubemap for efficient access at runtime. Typically, image is a cubemap or an equirectangular image. The term \( \frac{\sigma}{\pi} \) is independent of the IBL and is added at runtime to obtain the radiance. +

+

 
Figure 49: Image-based environment
+

+

 
Figure 50: Image-based irradiance map using the Lambertian BRDF
+

+

 5 \(\Theta\) represents the parameters of the material model \(f\), i.e.: roughness, albedo and so on... +
+

+

 6 This can be done through blending of static probes or by spreading the workload over time +
+

+

 7 The Lambertian BRDF doesn't depend on \(\vec l\), \(\vec v\) or \(\theta\), so \(L_d(n,v,\theta) \equiv L_d(n,\sigma)\) +
+

+

 8 \(\Omega_s\) can be approximated by \(\frac{2\pi}{6 \cdot width \cdot height}\) for a cubemap +
+

+

 9 \(O(12\,n^2\,m^2)\), with \(n\) and \(m\) respectively the dimensions of the environment and the precomputed cubemap +
+

+However, the irradiance can also be approximated very closely by a decomposition into Spherical Harmonics (SH, described in more details in the Spherical Harmonics section) and calculated at runtime cheaply. It is usually best to avoid texture fetches on mobile and free-up a texture unit. Even if it is stored into a cubemap, it is orders of magnitude faster to pre-compute the integral using SH decomposition followed by a rendering. +

+SH decomposition is similar in concept to a Fourier transform, it expresses the signal over an orthonormal base in the frequency domain. The properties that interests us most are: +

+

    +
  • Very few coefficients are needed to encode \(\cosTheta\) +

    +

  • +
  • Convolutions by a kernel that has a circular symmetry are very inexpensive and become products in SH space
+

+In practice only 4 or 9 coefficients (i.e.: 2 or 3 bands) are enough for \(\cosTheta\) meaning we don't need more either for \(\Lt\). +

+

 
Figure 51: 3 bands (9 coefficients)
+

+

 
Figure 52: 2 bands (4 coefficients)
+

+In practice we pre-convolve \(\Lt\) with \(\cosTheta\) and pre-scale these coefficients by the basis scaling factors \(K_l^m\) so that the reconstruction code is as simple as possible in the shader: +

 
vec3 irradianceSH(vec3 n) {
+    // uniform vec3 sphericalHarmonics[9]
+    // We can use only the first 2 bands for better performance
+    return
+          sphericalHarmonics[0]
+        + sphericalHarmonics[1] * (n.y)
+        + sphericalHarmonics[2] * (n.z)
+        + sphericalHarmonics[3] * (n.x)
+        + sphericalHarmonics[4] * (n.y * n.x)
+        + sphericalHarmonics[5] * (n.y * n.z)
+        + sphericalHarmonics[6] * (3.0 * n.z * n.z - 1.0)
+        + sphericalHarmonics[7] * (n.z * n.x)
+        + sphericalHarmonics[8] * (n.x * n.x - n.y * n.y);
+}
Listing 26: GLSL code to reconstruct the irradiance from the pre-scaled SH
+

+

Note that with 2 bands, the computation above becomes a single (4 \times 4) matrix-by-vector multiply.

+

+Additionally, because of the pre-scaling by \(K_l^m\), the SH coefficients can be thought of as colors, in particular sphericalHarmonics[0] is directly the average irradiance. +

+   

Specular BRDF integration

+

+

As we've seen above, the radiance (\Lout) resulting from the interaction between an IBL's irradiance and a BRDF is:

+

+$$\begin{equation}\label{specularBRDFIntegration} +\Lout(n, v, \Theta) = \int_\Omega f(l, v, \Theta) \Lt(l) \left< \NoL \right> \partial l +\end{equation}$$ +

+We recognize the convolution of \(\Lt\) by \(f(l, v, \Theta) \left< \NoL \right>\), +i.e.: the environment is filtered using the BRDF as a kernel. Indeed at higher roughness, +specular reflections look more blurry. +

+Plugging the expression of \(f\) in equation \(\ref{specularBRDFIntegration}\), we obtain: +

+$$\begin{equation} +\Lout(n,v,\Theta) = \int_\Omega D(l, v, \alpha) F(l, v, f_0, f_{90}) V(l, v, \alpha) \left< \NoL \right> \Lt(l) \partial l +\end{equation}$$ +

+This expression depends on \(v\), \(\alpha\), \(f_0\) and \(f_{90}\) inside the integral, +which makes its evaluation extremely costly and unsuitable for real-time on mobile +(even using pre-filtered importance sampling). +

+   
Simplifying the BRDF integration
+

+

Since there is no closed-form solution or an easy way to compute the (\Lout) integral, we use a simplified +equation instead: (\hat{I}), whereby we assume that (v = n), that is the view direction (v) is always +equal to the surface normal (n). Clearly, this assumption will break all view-dependant effects of +the convolution, such as the increased blur in reflections closer to the viewer +(a.k.a. stretchy reflections).

+

+Such a simplification would also have a severe impact on constant environments, such as the white +furnace, because it would affect the magnitude of the constant (i.e. DC) term of the result. We +can at least correct for that by using a scale factor, \(K\), in our simplified integral, which +will make sure the average irradiance stay correct when chosen properly. +

+

    +
  • \(I\) is our original integral, i.e.: \(I(g) = \int_\Omega g(l) \left< \NoL \right> \partial l\) +
  • +
  • \(\hat{I}\) is the simplified integral where \(v = n\) +
  • +
  • \(K\) is a scale factor that ensures the average irradiance is unchanged by \(\hat{I}\) +
  • +
  • \(\tilde{I}\) is our final approximation of \(I\), \(\tilde{I} = \hat{I} \times K\)
+

+Because \(I\) is an integral multiplications can be distributed over it. i.e.: \(I(g()f()) = I(g())I(f())\). +

+Armed with that, +

+$$\begin{equation} +I( f(\Theta) \Lt ) \approx \tilde{I}( f(\Theta) \Lt ) \\ +\tilde{I}( f(\Theta) \Lt ) = K \times \hat{I}( f(\Theta) \Lt ) \\ +K = \frac{I(f(\Theta))}{\hat{I}(f(\Theta))} +\end{equation}$$ +

+From the equation above we can see that \(\tilde{I}\) is equivalent to \(I\) when \(\Lt\) is a constant, +and yields the correct result: +

+$$\begin{align*} +\tilde{I}(f(\Theta)\Lt^{constant}) &= \Lt^{constant} \hat{I}(f(\Theta)) \frac{I(f(\Theta))}{\hat{I}(f(\Theta))} \\ + &= \Lt^{constant} I(f(\Theta)) \\ + &= I(f(\Theta)\Lt^{constant}) +\end{align*}$$ +

+Similarly, we can also demonstrate that the result is correct when \(v = n\), since in that case \(I = \hat{I}\): +

+$$\begin{align*} +\tilde{I}(f(\Theta)\Lt) &= I(f(\Theta)\Lt) \frac{I(f(\Theta))}{I(f(\Theta))} \\ + &= I(f(\Theta)\Lt) +\end{align*}$$ +

+Finally, we can show that the scale factor \(K\) satisfies our average irradiance (\(\bar{\Lt}\)) +requirement by plugging \(\Lt = \bar{\Lt} + (\Lt - \bar{\Lt}) = \bar{\Lt} + \Delta\Lt\) into \(\tilde{I}\): +

+$$\begin{align*} +\tilde{I}(f(\Theta)\Lt) &= \tilde{I}\left[f\left(\Theta\right) \left(\bar{\Lt} + \Delta\Lt\right)\right] \\ + &= K \times \hat{I}\left[f\left(\Theta\right) \left(\bar{\Lt} + \Delta\Lt\right)\right] \\ + &= K \times \left[\hat{I}\left(f\left(\Theta\right)\bar{\Lt}\right) + \hat{I}\left(f\left(\Theta\right)\Delta\Lt\right)\right] \\ + &= K \times \hat{I}\left(f\left(\Theta\right)\bar{\Lt}\right) + K \times \hat{I}\left(f\left(\Theta\right) \Delta\Lt\right) \\ + &= \tilde{I}\left(f\left(\Theta\right)\bar{\Lt}\right) + \tilde{I}\left(f\left(\Theta\right) \Delta\Lt\right) \\ + &= I\left(f\left(\Theta\right)\bar{\Lt}\right) + \tilde{I}\left(f\left(\Theta\right) \Delta\Lt\right) +\end{align*}$$ +

+The above result shows that the average irradiance is computed correctly, i.e.: \(I(f(\Theta)\bar{\Lt})\). +

+A way to think about this approximation is that it splits the radiance \(\Lt\) in two parts, +the average \(\bar{\Lt}\) and the delta from the average \(\Delta\Lt\) and computes the correct +integration of the average part then adds the simplified integration of the delta part: +

+$$\begin{equation} +approximation(\Lt) = correct(\bar{\Lt}) + simplified(\Lt - \bar{\Lt}) +\end{equation}$$ +

+Now, let's look at each term: +

+$$\begin{equation}\label{iblPartialEquations} +\hat{I}(f(n, \alpha) \Lt) = \int_\Omega f(l, n, \alpha) \Lt(l) \left< \NoL \right> \partial l \\ +\hat{I}(f(n, \alpha)) = \int_\Omega f(l, n, \alpha) \left< \NoL \right> \partial l \\ +I(f(n, v, \alpha)) = \int_\Omega f(l, n, v, \alpha) \left< \NoL \right> \partial l +\end{equation}$$ +

+All three of these equations can be easily pre-calculated and stored in look-up tables, as explained +below. +

+   
Discrete Domain
+

+

In the discrete domain the equations in \ref{iblPartialEquations} become:

+

+$$\begin{equation} +\hat{I}(f(n, \alpha) \Lt) \equiv \frac{1}{N}\sum_{\forall \, i \in image} f(l_i, n, \alpha) \Lt(l_i) \left<\NoL\right> \\ +\hat{I}(f(n, \alpha)) \equiv \frac{1}{N}\sum_{\forall \, i \in image} f(l_i, n, \alpha) \left<\NoL\right> \\ +I(f(n, v, \alpha)) \equiv \frac{1}{N}\sum_{\forall \, i \in image} f(l_i, n, v, \alpha) \left<\NoL\right> +\end{equation}$$ +

+However, in practice we're using importance sampling which needs to take the \(pdf\) of the distribution +into account and adds a term \(\frac{4\left<\VoH\right>}{D(h_i, \alpha)\left<\NoH\right>}\). +See Importance Sampling For The IBL section: +

+$$\begin{equation}\label{iblImportanceSampling} +\hat{I}(f(n, \alpha) \Lt) \equiv \frac{4}{N}\sum_i^N f(l_i, n, \alpha) \frac{\left<\VoH\right>}{D(h_i, \alpha)\left<\NoH\right>} \Lt(l_i) \left<\NoL\right> \\ +\hat{I}(f(n, \alpha)) \equiv \frac{4}{N}\sum_i^N f(l_i, n, \alpha) \frac{\left<\VoH\right>}{D(h_i, \alpha)\left<\NoH\right>} \left<\NoL\right> \\ +I(f(n, v, \alpha)) \equiv \frac{4}{N}\sum_i^N f(l_i, n, v, \alpha) \frac{\left<\VoH\right>}{D(h_i, \alpha)\left<\NoH\right>} \left<\NoL\right> +\end{equation}$$ +

+Recalling that for \(\hat{I}\), we assume that \(v = n\), equations \ref{iblImportanceSampling}, +simplifies to: +

+$$\begin{equation} +\hat{I}(f(n, \alpha) \Lt) \equiv \frac{4}{N}\sum_i^N \frac{f(l_i, n, \alpha)}{D(h_i, \alpha)} \Lt(l_i) \left<\NoL\right> \\ +\hat{I}(f(n, \alpha)) \equiv \frac{4}{N}\sum_i^N \frac{f(l_i, n, \alpha)}{D(h_i, \alpha)} \left<\NoL\right> \\ +I(f(n, v, \alpha)) \equiv \frac{4}{N}\sum_i^N \frac{f(l_i, n, v, \alpha)}{D(h_i, \alpha)} \frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> +\end{equation}$$ +

+Then, the first two equations can be merged together such that \(LD(n, \alpha) = \frac{\hat{I}(f(n, \alpha) \Lt)}{\hat{I}(f(n, \alpha))}\) +

+$$\begin{equation}\label{iblLD} +LD(n, \alpha) \equiv \frac{\sum_i^N \frac{f(l_i, n, \alpha)}{D(h_i, \alpha)} \Lt(l_i) \left<\NoL\right>}{\sum_i^N \frac{f(l_i, n, \alpha)}{D(h_i, \alpha)}\left<\NoL\right>} +\end{equation}$$ +$$\begin{equation}\label{iblDFV} +I(f(n, v, \alpha)) \equiv \frac{4}{N}\sum_i^N \frac{f(l_i, n, v, \alpha)}{D(h_i, \alpha)} \frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> +\end{equation}$$ +

+Note that at this point, we could almost compute both remaining equations off-line. The only difficulty +is that we don't know \(f_0\) nor \(f_{90}\) when we precompute those integrals. We will see below that +we can incorporate these terms at runtime for equation \ref{iblDFV}, alas, this is not possible for +equation \ref{iblLD} and we have to assume \(f_0 = f_{90} = 1\) (i.e.: the fresnel term always evaluates to 1). +

+We also have to deal with the visibility term of the brdf, in practice keeping it yields to slightly +worst results compared to the ground truth, so we also set \(V = 1\). +

+Let's substitute \(f\) in equations \ref{iblLD} and \ref{iblDFV}: +

+$$\begin{equation} +f(l_i, n, \alpha) = D(h_i, \alpha)F(f_0, f_{90}, \left<\VoH\right>)V(l_i, v, \alpha) +\end{equation}$$ +

+The first simplification is that the term \(D(h_i, \alpha)\) in the brdf cancels out with the +denominator (which came from the \(pdf\) due to importance sampling) and F and V disappear since we +assume their value is 1. +

+$$\begin{equation} +LD(n, \alpha) \equiv \frac{\sum_i^N V(l_i, v, \alpha)\left<\NoL\right>\Lt(l_i) }{\sum_i^N \left<\NoL\right>} +\end{equation}$$ +$$\begin{equation}\label{iblFV} +I(f(n, v, \alpha)) \equiv \frac{4}{N}\sum_i^N \color{green}{F(f_0, f_{90}, \left<\VoH\right>)} V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> +\end{equation}$$ +

+Now, let's substitute the fresnel term into equation \ref{iblFV}: +

+$$\begin{equation} +F(f_0, f_{90}, \left<\VoH\right>) = f_0 (1 - F_c(\left<\VoH\right>)) + f_{90} F_c(\left<\VoH\right>) \\ +F_c(\left<\VoH\right>) = (1 - \left<\VoH\right>)^5 +\end{equation}$$ +

+$$\begin{equation} +I(f(n, v, \alpha)) \equiv \frac{4}{N}\sum_i^N \left[\color{green}{f_0 (1 - F_c(\left<\VoH\right>)) + f_{90} F_c(\left<\VoH\right>)}\right] V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> \\ +\end{equation}$$ +

+$$ +\begin{align*} +I(f(n, v, \alpha)) \equiv & \color{green}{f_0 } \frac{4}{N}\sum_i^N \color{green}{(1 - F_c(\left<\VoH\right>))} V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> \\ + + & \color{green}{f_{90}} \frac{4}{N}\sum_i^N \color{green}{ F_c(\left<\VoH\right>) } V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> +\end{align*} +$$ +

+And finally, we extract the equations that can be calculated off-line (i.e.: the part that doesn't +depend on the runtime parameters \(f_0\) and \(f_{90}\)): +

+$$\begin{equation}\label{iblAllEquations} +DFG_1(\alpha, \left<\NoV\right>) = \frac{4}{N}\sum_i^N \color{green}{(1 - F_c(\left<\VoH\right>))} V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> \\ +DFG_2(\alpha, \left<\NoV\right>) = \frac{4}{N}\sum_i^N \color{green}{ F_c(\left<\VoH\right>) } V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> \\ +I(f(n, v, \alpha)) \equiv \color{green}{f_0} \color{red}{DFG_1(\alpha, \left<\NoV\right>)} + \color{green}{f_{90}} \color{red}{DFG_2(\alpha, \left<\NoV\right>)} +\end{equation}$$ +

+Notice that \(DFG_1\) and \(DFG_2\) only depend on \(\NoV\), that is the angle between the normal \(n\) and +the view direction \(v\). This is true because the integral is symmetrical with respect to \(n\). +When integrating, we can choose any \(v\) we please as long as it satisfies \(\NoV\) +(e.g.: when calculating \(\VoH\)). +

+Putting everything back together: +

+$$ +\begin{align*} +\Lout(n,v,\alpha,f_0,f_{90}) &\simeq \big[ f_0 \color{red}{DFG_1(\NoV, \alpha)} + f_{90} \color{red}{DFG_2(\NoV, \alpha)} \big] \times LD(n, \alpha) \\ +DFG_1(\alpha, \left<\NoV\right>) &= \frac{4}{N}\sum_i^N \color{green}{(1 - F_c(\left<\VoH\right>))} V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> \\ +DFG_2(\alpha, \left<\NoV\right>) &= \frac{4}{N}\sum_i^N \color{green}{ F_c(\left<\VoH\right>) } V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> \\ +LD(n, \alpha) &= \frac{\sum_i^N V(l_i, n, \alpha)\left<\NoL\right>\Lt(l_i) }{\sum_i^N \left<\NoL\right>} +\end{align*} +$$ +

+   

The \(DFG_1\) and \(DFG_2\) term visualized

+

+

Both (DFG_1) and (DFG_2) can either be pre-calculated in a regular 2D texture indexed by ((\NoV, \alpha)) +and sampled bilinearly, or computed at runtime using an analytic approximation of the surfaces. +See sample code in the annex. +The pre-calculated textures are shown in table 15. +A C++ implementation of the pre-computation can be found in section 9.5.

+

+

+  + +
\(DFG_1\) \(DFG_2\) \({ DFG_1, DFG_2, 0 }\)
Table 15: Y axis: \(\alpha\). X axis: \(cos \theta\)
+

+\(DFG_1\) and \(DFG_2\) are conveniently within the \([0, 1]\) range, however 8-bits textures don't have +enough precision and will cause problems. +Unfortunately, on mobile, 16-bits or float textures are not ubiquitous and there are a limited +number of samplers. +Despite the attractive simplicity of the shader code using a texture, it might be better to use an +analytic approximation. Note however that since we only need to store two terms, +OpenGL ES 3.0's RG16F texture format is a good candidate. +

+Such analytic approximation is described in [Karis14], itself based on [Lazarov13]. +[Narkowicz14] is another interesting approximation. Note that these two approximations are not +compatible with the energy compensation term presented in section 5.3.4.7. +Table 16 presents a visual representation of these approximations. +

+  + +
\(DFG_1\) \(DFG_2\) \({ DFG_1, DFG_2, 0 }\)
Table 16: Y axis: \(\alpha\). X axis: \(cos \theta\)
+

+   

The \(LD\) term visualized

+

+

(LD) is the convolution of the environment by a function that only depends on the (\alpha) parameter +(itself related to the roughness, see section 4.8.3.3). +(LD) can conveniently be stored in a mip-mapped cubemap where increasing LODs receive the environment +pre-filtered with increasing roughness. This works well because this convolution is a +powerful low-pass filter. To make good use of each mipmap level, it is necessary to remap +(\alpha); we find that using a power remapping with (\gamma = 2) works well and is convenient.

+

+$$ +\begin{align*} + \alpha &= perceptualRoughness^2 \\ + lod_{\alpha} &= \alpha^{\frac{1}{2}} = perceptualRoughness \\ +\end{align*} +$$ +

+See an example below: +

+
+

+

\(\alpha=0.0\)
+

+

+

+

\(\alpha=0.2\)
+

+

+

+

\(\alpha=0.4\)
+

+

+

+

\(0.6\)
+

+

+

+

\(0.8\)
+

+

+

+   

Indirect specular and indirect diffuse components visualized

+

+

Figure 53 shows how indirect lighting interacts with dielectrics and conductors. Direct lighting was removed for illustration purposes.

+

+

 
Figure 53: Indirect diffuse and specular decomposition
+

+   

IBL evaluation implementation

+

+

Listing 27 presents a GLSL implementation to evaluate the IBL, using the various textures described in the previous sections.

+

 
vec3 ibl(vec3 n, vec3 v, vec3 diffuseColor, vec3 f0, vec3 f90,
+        float perceptualRoughness) {
+    vec3 r = reflect(n);
+    vec3 Ld = textureCube(irradianceEnvMap, r) * diffuseColor;
+    float lod = computeLODFromRoughness(perceptualRoughness);
+    vec3 Lld = textureCube(prefilteredEnvMap, r, lod);
+    vec2 Ldfg = textureLod(dfgLut, vec2(dot(n, v), perceptualRoughness), 0.0).xy;
+    vec3 Lr =  (f0 * Ldfg.x + f90 * Ldfg.y) * Lld;
+    return Ld + Lr;
+}
Listing 27: GLSL implementation of image based lighting evaluation
+

+

We can however save a couple of texture lookups by using Spherical Harmonics instead of an +irradiance cubemap and the analytical approximation of the (DFG) LUT, as shown in listing 28.

+

 
vec3 irradianceSH(vec3 n) {
+    // uniform vec3 sphericalHarmonics[9]
+    // We can use only the first 2 bands for better performance
+    return
+          sphericalHarmonics[0]
+        + sphericalHarmonics[1] * (n.y)
+        + sphericalHarmonics[2] * (n.z)
+        + sphericalHarmonics[3] * (n.x)
+        + sphericalHarmonics[4] * (n.y * n.x)
+        + sphericalHarmonics[5] * (n.y * n.z)
+        + sphericalHarmonics[6] * (3.0 * n.z * n.z - 1.0)
+        + sphericalHarmonics[7] * (n.z * n.x)
+        + sphericalHarmonics[8] * (n.x * n.x - n.y * n.y);
+}
+
+// NOTE: this is the DFG LUT implementation of the function above
+vec2 prefilteredDFG_LUT(float coord, float NoV) {
+    // coord = sqrt(roughness), which is the mapping used by the
+    // IBL prefiltering code when computing the mipmaps
+    return textureLod(dfgLut, vec2(NoV, coord), 0.0).rg;
+}
+
+vec3 evaluateSpecularIBL(vec3 r, float perceptualRoughness) {
+    // This assumes a 256x256 cubemap, with 9 mip levels
+    float lod = 8.0 * perceptualRoughness;
+    // decodeEnvironmentMap() either decodes RGBM or is a no-op if the
+    // cubemap is stored in a float texture
+    return decodeEnvironmentMap(textureCubeLodEXT(environmentMap, r, lod));
+}
+
+vec3 evaluateIBL(vec3 n, vec3 v, vec3 diffuseColor, vec3 f0, vec3 f90, float perceptualRoughness) {
+    float NoV = max(dot(n, v), 0.0);
+    vec3 r = reflect(-v, n);
+
+    // Specular indirect
+    vec3 indirectSpecular = evaluateSpecularIBL(r, perceptualRoughness);
+    vec2 env = prefilteredDFG_LUT(perceptualRoughness, NoV);
+    vec3 specularColor = f0 * env.x + f90 * env.y;
+
+    // Diffuse indirect
+    // We multiply by the Lambertian BRDF to compute radiance from irradiance
+    // With the Disney BRDF we would have to remove the Fresnel term that
+    // depends on NoL (it would be rolled into the SH). The Lambertian BRDF
+    // can be baked directly in the SH to save a multiplication here
+    vec3 indirectDiffuse = max(irradianceSH(n), 0.0) * Fd_Lambert();
+
+    // Indirect contribution
+    return diffuseColor * indirectDiffuse + indirectSpecular * specularColor;
+}
Listing 28: GLSL implementation of image based lighting evaluation
+   

Pre-integration for multiscattering

+

+

In section 4.7.2 we discussed how to use a second scaled specular lobe +to compensate for the energy loss due to only accounting for a single scattering event in our BRDF. +This energy compensation lobe is scaled by a term that depends on (r) defined in the following way:

+

+$$\begin{equation} +r = \int_{\Omega} D(l,v) V(l,v) \left< \NoL \right> \partial l +\end{equation}$$ +

+Or, evaluated with importance sampling (See Importance Sampling For The IBL section): +

+$$\begin{equation} +r \equiv \frac{4}{N}\sum_i^N V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> +\end{equation}$$ +

+This equality is very similar to the terms \(DFG_1\) and \(DFG_2\) seen in equation \(\ref{iblAllEquations}\). +In fact, it's the same, except without the Fresnel term. +

+By making the further assumption that \(f_{90} = 1\), we can rewrite \(DFG_1\) and \(DFG_2\) and the +\(\Lout\) reconstruction: +

+$$ +\begin{align*} +\Lout(n,v,\alpha,f_0) &\simeq \big[ (1 - f_0) \color{red}{DFG_1^{multiscatter}(\NoV, \alpha)} + f_0 \color{red}{DFG_2^{multiscatter}(\NoV, \alpha)} \big] \times LD(n, \alpha) \\ +DFG_1^{multiscatter}(\alpha, \left<\NoV\right>) &= \frac{4}{N}\sum_i^N \color{green}{F_c(\left<\VoH\right>)} V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> \\ +DFG_2^{multiscatter}(\alpha, \left<\NoV\right>) &= \frac{4}{N}\sum_i^N V(l_i, v, \alpha)\frac{\left<\VoH\right>}{\left<\NoH\right>} \left<\NoL\right> \\ +LD(n, \alpha) &= \frac{\sum_i^N V(l_i, n, \alpha)\left<\NoL\right>\Lt(l_i) }{\sum_i^N V(l_i, n, \alpha)\left<\NoL\right>} +\end{align*} +$$ +

+These two new \(DFG\) terms simply need to replace the ones used in the implementation shown in section 9.5: +

 
float Fc = pow(1 - VoH, 5.0f);
+r.x += Gv * Fc;
+r.y += Gv;
Listing 29: C++ implementation of the \(L_{DFG}\) term for multiscattering
+

+

To perform the reconstruction we need to slightly modify listing 30:

+

 
vec2 dfg = textureLod(dfgLut, vec2(dot(n, v), perceptualRoughness), 0.0).xy;
+// (1 - f0) * dfg.x + f0 * dfg.y
+vec3 specularColor = mix(dfg.xxx, dfg.yyy, f0);
Listing 30: GLSL implementation of image based lighting evaluation, with multiscattering LUT
+   

Summary

+

+

In order to calculate the specular contribution of distant image-based lights, we had to make a few +approximations and compromises:

+

+

    +
  • \(v = n\), by far the assumption contributing to the largest error when integrating the + non-constant part of the IBL. This results in the complete loss of roughness anisotropy + with respect to the view point. +

    +

  • +
  • Roughness contribution for the non-constant part of the IBL is quantized and trilinear filtering + is used to interpolate between these levels. This is most visible at low roughnes (e.g.: around 0.0625 + for a 9 LODs cubemap). +

    +

  • +
  • Because mipmap levels are used to store the pre-integrated environment, they can't be used for + texture minification, as they ought to. This can causes aliasing or moiré artifacts in high frequency + regions or the environment at low roughness and/or distant or small objects. + This can also impact performance due to the resulting poor cache access pattern. +

    +

  • +
  • No Fresnel for the non-constant part of the IBL. +

    +

  • +
  • Visibility = 1 for the non-constant part of the IBL. +

    +

  • +
  • Schlick's Fresnel +

    +

  • +
  • \(f_{90} = 1\) in the multiscattering case.
+

+

 
Figure 54: +
+

+

 
Figure 55: +
+

+

 
Figure 56: +
+

+

 
Figure 57: +
+

+

 
Figure 58: +
+

+   

Clear coat

+

+

When sampling the IBL, the clear coat layer is calculated as a second specular lobe. This specular lobe is oriented along the view direction since we cannot reasonably integrate over the hemisphere. Listing 31 demonstrates this approximation in practice. It also shows the energy conservation step. It is important to note that this second specular lobe is computed exactly the same way as the main specular lobe, using the same DFG approximation.

+

 
// clearCoat_NoV == shading_NoV if the clear coat layer doesn't have its own normal map
+float Fc = F_Schlick(0.04, 1.0, clearCoat_NoV) * clearCoat;
+// base layer attenuation for energy compensation
+iblDiffuse  *= 1.0 - Fc;
+iblSpecular *= sq(1.0 - Fc);
+iblSpecular += specularIBL(r, clearCoatPerceptualRoughness) * Fc;
Listing 31: GLSL implementation of the clear coat specular lobe for image-based lighting
+   

Anisotropy

+

+

[McAuley15] describes a technique called “bent reflection vector”, based [Revie12]. The bent reflection vector is a rough approximation of anisotropic lighting but the alternative is to use importance sampling. This approximation is sufficiently cheap to compute and provides good results, as shown in figure 59 and figure 60.

+

+

 
Figure 59: Anisotropic indirect specular reflections using bent normals (left: roughness 0.3, right: roughness: 0.0; both: anisotropy 1.0)
+

+

 
Figure 60: Anisotropic reflections with varying roughness, metallicness, etc.
+

+The implementation of this technique is straightforward, as demonstrated in listing 32. +

 
vec3 anisotropicTangent = cross(bitangent, v);
+vec3 anisotropicNormal = cross(anisotropicTangent, bitangent);
+vec3 bentNormal = normalize(mix(n, anisotropicNormal, anisotropy));
+vec3 r = reflect(-v, bentNormal);
Listing 32: GLSL implementation of the bent reflection vector
+

+

This technique can be made more useful by accepting negative anisotropy values, as shown in listing 33. When the anisotropy is negative, the highlights are not in the direction of the tangent, but in the direction of the bitangent instead.

+

 
vec3 anisotropicDirection = anisotropy >= 0.0 ? bitangent : tangent;
+vec3 anisotropicTangent = cross(anisotropicDirection, v);
+vec3 anisotropicNormal = cross(anisotropicTangent, anisotropicDirection);
+vec3 bentNormal = normalize(mix(n, anisotropicNormal, anisotropy));
+vec3 r = reflect(-v, bentNormal);
Listing 33: GLSL implementation of the bent reflection vector
+

+

Figure 61 demonstrates this modified implementation in practice.

+

+

 
Figure 61: Control of the anisotropy direction using positive (left) and negative (right) values
+

+   

Subsurface

+

+

[TODO] Explain subsurface and IBL

+

+   

Cloth

+

+

The IBL implementation for the cloth material model is more complicated than for the other material models. The main difference stems from the use of a different NDF (“Charlie” vs height-correlated Smith GGX). As described in this section, we use the split-sum approximation to compute the DFG term of the BRDF when computing an IBL. This DFG term is designed for a different BRDF and cannot be used for the cloth BRDF. Since we designed our cloth BRDF to not need a Fresnel term, we can generate a single DG term in the 3rd channel of the DFG LUT. The result is shown in figure 62.

+

+The DG term is generated using uniform sampling as recommended in [Estevez17]. With uniform sampling the \(pdf\) is simply \(\frac{1}{2\pi}\) and we must still use the Jacobian \(\frac{1}{4\left< \VoH \right>}\). +

+

 
Figure 62: DFG LUT with a 3rd channel encoding the DG term of the cloth BRDF
+

+The remainder of the image-based lighting implementation follows the same steps as the implementation of regular lights, including the optional subsurface scattering term and its wrap diffuse component. Just as with the clear coat IBL implementation, we cannot integrate over the hemisphere and use the view direction as the dominant light direction to compute the wrap diffuse component. +

 
float diffuse = Fd_Lambert() * ambientOcclusion;
+#if defined(SHADING_MODEL_CLOTH)
+#if defined(MATERIAL_HAS_SUBSURFACE_COLOR)
+diffuse *= saturate((NoV + 0.5) / 2.25);
+#endif
+#endif
+
+vec3 indirectDiffuse = irradianceIBL(n) * diffuse;
+#if defined(SHADING_MODEL_CLOTH) && defined(MATERIAL_HAS_SUBSURFACE_COLOR)
+indirectDiffuse *= saturate(subsurfaceColor + NoV);
+#endif
+
+vec3 ibl = diffuseColor * indirectDiffuse + indirectSpecular * specularColor;
Listing 34: GLSL implementation of the DFG approximation for the cloth NDF
+

+

It is important to note that this only addresses part of the IBL problem. The pre-filtered specular environment maps described earlier are convolved with the standard shading model's BRDF, which differs from the cloth BRDF. To get accurate result we should in theory provide one set of IBLs per BRDF used in the engine. Providing a second set of IBLs is however not practical for our use case so we decided to rely on the existing IBLs instead.

+

+   

Static lighting

+

+

[TODO] Spherical-harmonics or spherical-gaussian lightmaps, irradiance volumes, PRT?…

+

+   

Transparency and translucency lighting

+

+

Transparent and translucent materials are important to add realism and correctness to scenes. Filament must therefore provide lighting models for both types of materials to allow artists to properly recreate realistic scenes. Translucency can also be used effectively in a number of non-realistic settings.

+

+   

Transparency

+

+

To properly light a transparent surface, we must first understand how the material's opacity is applied. Observe a window and you will see that the diffuse reflectance is transparent. On the other hand, the brighter the specular reflectance, the less opaque the window appears. This effect can be seen in figure 63: the scene is properly reflected onto the glass surfaces but the specular highlight of the sun is bright enough to appear opaque.

+

+

 
Figure 63: Example of a complex object where lit surface transparency plays an important role
+

+

 
Figure 64: Example of a complex object where lit surface transparency plays an important role
+

+To properly implement opacity, we will use the premultiplied alpha format. Given a desired opacity noted \( \alpha_{opacity} \) and a diffuse color \( \sigma \) (linear, unpremultiplied), we can compute the effective opacity of a fragment. +

+$$\begin{align*} +color &= \sigma * \alpha_{opacity} \\ +opacity &= \alpha_{opacity} +\end{align*}$$ +

+The physical interpretation is that the RGB components of the source color define how much light is emitted by the pixel, whereas the alpha component defines how much of the light behind the pixel is blocked by said pixel. We must therefore use the following blending functions: +

+$$\begin{align*} +Blend_{src} &= 1 \\ +Blend_{dst} &= 1 - src_{\alpha} +\end{align*}$$ +

+The GLSL implementation of these equations is presented in listing 35. +

 
// baseColor has already been premultiplied
+vec4 shadeSurface(vec4 baseColor) {
+    float alpha = baseColor.a;
+
+    vec3 diffuseColor = evaluateDiffuseLighting();
+    vec3 specularColor = evaluateSpecularLighting();    
+
+    return vec4(diffuseColor + specularColor, alpha);
+}
Listing 35: Implementation of lit surface transparency in GLSL
+   

Translucency

+

+

Translucent materials can be divided into two categories:

+

+

    +
  • Surface translucency +
  • +
  • Volume translucency
+

+Volume translucency is useful to light particle systems, for instance clouds or smoke. Surface translucency can be used to imitate materials with transmitted scattering such as wax, marble, skin, etc. +

+[TODO] Surface translucency (BRDF+BTDF, BSSRDF) +

+

 
Figure 65: Front-lit translucent object (left) and back-lit translucent object (right), using approximated BTDF and BSSRDF. Model: Lucy from the Stanford University Computer Graphics Laboratory
+

+   

Occlusion

+

+

Occlusion is an important darkening factor used to recreate shadowing at various scales:

+

+

Small scale

Micro-occlusion used to handle creases, cracks and cavities. +

Medium scale

Macro-occlusion used to handle occlusion by an object's own geometry or by geometry baked in normal maps (bricks, etc.). +

Large scale

Occlusion coming from contact between objects, or from an object's own geometry. +

We currently ignore micro-occlusion, which is often exposed in tools and engines under the form of a “cavity map”. Sébastien Lagarde offers an interesting discussion in [Lagarde14] on how micro-occlusion is handled in Frostbite: diffuse micro-occlusion is pre-baked in diffuse maps and specular micro-occlusion is pre-baked in reflectance textures. +In our system, micro-occlusion can simply be baked in the base color map. This must be done knowing that the specular light will not be affected by micro-occlusion. +

+Medium scale ambient occlusion is pre-baked in ambient occlusion maps, exposed as a material parameter, as seen in the material parameterization section earlier. +

+Large scale ambient occlusion is often computed using screen-space techniques such as SSAO (screen-space ambient occlusion), HBAO (horizon based ambient occlusion), etc. Note that these techniques can also contribute to medium scale ambient occlusion when the camera is close enough to surfaces. +

+Note: to prevent over darkening when using both medium and large scale occlusion, Lagarde recommends to use \(min({AO}_{medium}, {AO}_{large})\). +

+   

Diffuse occlusion

+

+

Morgan McGuire formalizes ambient occlusion in the context of physically based rendering in [McGuire10]. In his formulation, McGuire defines an ambient illumination function ( L_a ), which in our case is encoded with spherical harmonics. He also defines a visibility function (V), with (V(l)=1) if there is an unoccluded line of sight from the surface in direction (l), and 0 otherwise.

+

+With these two functions, the ambient term of the rendering equation can be expressed as shown in equation \(\ref{diffuseAO}\). +

+$$\begin{equation}\label{diffuseAO} +L(l,v) = \int_{\Omega} f(l,v) L_a(l) V(l) \left< \NoL \right> dl +\end{equation}$$ +

+This expression can be approximated by separating the visibility term from the illumination function, as shown in equation \(\ref{diffuseAOApprox}\). +

+$$\begin{equation}\label{diffuseAOApprox} +L(l,v) \approx \left( \pi \int_{\Omega} f(l,v) L_a(l) dl \right) \left( \frac{1}{\pi} \int_{\Omega} V(l) \left< \NoL \right> dl \right) +\end{equation}$$ +

+This approximation is only exact when the distant light \( L_a \) is constant and \(f\) is a Lambertian term. McGuire states however that this approximation is reasonable if both functions are relatively smooth over most of the sphere. This happens to be the case with a distant light probe (IBL). +

+The left term of this approximation is the pre-computed diffuse component of our IBL. The right term is a scalar factor between 0 and 1 that indicates the fractional accessibility of a point. Its opposite is the diffuse ambient occlusion term, show in equation \(\ref{diffuseAOTerm}\). +

+$$\begin{equation}\label{diffuseAOTerm} +{AO} = 1 - \frac{1}{\pi} \int_{\Omega} V(l) \left< \NoL \right> dl +\end{equation}$$ +

+Since we use a pre-computed diffuse term, we cannot compute the exact accessibility of shaded points at runtime. To compensate for this lack of information in our precomputed term, we partially reconstruct incident lighting by applying an ambient occlusion factor specific to the surface's material at the shaded point. +

+In practice, baked ambient occlusion is stored as a grayscale texture which can often be lower resolution than other textures (base color or normals for instance). It is important to note that the ambient occlusion property of our material model intends to recreate macro-level diffuse ambient occlusion. While this approximation is not physically correct, it constitutes an acceptable tradeoff of quality vs performance. +

+Figure 66 shows two different materials without and with diffuse ambient occlusion. Notice how the material ambient occlusion is used to recreate the natural shadowing that occurs between the different tiles. Without ambient occlusion, both materials appear too flat. +

+

 
Figure 66: Comparison of materials without diffuse ambient occlusion (left) and with (right)
+

+Applying baked diffuse ambient occlusion in a GLSL shader is straightforward, as shown in listing 36. +

 
// diffuse indirect
+vec3 indirectDiffuse = max(irradianceSH(n), 0.0) * Fd_Lambert();
+// ambient occlusion
+indirectDiffuse *= texture2D(aoMap, outUV).r;
Listing 36: Implementation of baked diffuse ambient occlusion in GLSL
+

+

Note how the ambient occlusion term is only applied to indirect lighting.

+

+   

Specular occlusion

+

+

Specular micro-occlusion can be derived from (\fNormal), itself derived from the diffuse color. The derivation is based on the knowledge that no real-world material has a reflectance lower than 2%. Values in the 0-2% range can therefore be treated as pre-baked specular occlusion used to smoothly extinguish the Fresnel term.

+

 
float f90 = clamp(dot(f0, 50.0 * 0.33), 0.0, 1.0);
+// cheap luminance approximation
+float f90 = clamp(50.0 * f0.g, 0.0, 1.0);
Listing 37: Pre-baked specular occlusion in GLSL
+

+

The derivations mentioned earlier for ambient occlusion assume Lambertian surfaces and are only valid for indirect diffuse lighting. The lack of information about surface accessibility is particularly harmful to the reconstruction of indirect specular lighting. It usually manifests itself as light leaks.

+

+Sébastien Lagarde proposes an empirical approach to derive the specular occlusion term from the diffuse occlusion term in [Lagarde14]. The result does not have any physical basis but produces visually pleasant results. The goal of his formulation is return the diffuse occlusion term unmodified for rough surfaces. For smooth surfaces, the formulation, implemented in listing 38, reduces the influence of occlusion at normal incidence and increases it at grazing angles. +

 
float computeSpecularAO(float NoV, float ao, float roughness) {
+    return clamp(pow(NoV + ao, exp2(-16.0 * roughness - 1.0)) - 1.0 + ao, 0.0, 1.0);
+}
+
+// specular indirect
+vec3 indirectSpecular = evaluateSpecularIBL(r, perceptualRoughness);
+// ambient occlusion
+float ao = texture2D(aoMap, outUV).r;
+indirectSpecular *= computeSpecularAO(NoV, ao, roughness);
Listing 38: Implementation of Lagarde's specular occlusion factor in GLSL
+

+

Note how the specular occlusion factor is only applied to indirect lighting.

+

+   

Horizon specular occlusion

+

+

When computing the specular IBL contribution for a surface that uses a normal map, it is possible to end up with a reflection vector pointing towards the surface. If this reflection vector is used for shading directly, the surface will be lit in places where it should not be lit (assuming opaque surfaces). This is another occurrence of light leaking that can easily be minimized using a simple technique described by Jeff Russell [Russell15].

+

+The key idea is to occlude light coming from behind the surface. This can easily be achieved since a negative dot product between the reflected vector and the surface's normal indicates a reflection vector pointing towards the surface. Our implementation shown in listing 39 is similar to Russell's, albeit without the artist controlled horizon fading factor. +

 
// specular indirect
+vec3 indirectSpecular = evaluateSpecularIBL(r, perceptualRoughness);
+
+// horizon occlusion with falloff, should be computed for direct specular too
+float horizon = min(1.0 + dot(r, n), 1.0);
+indirectSpecular *= horizon * horizon;
Listing 39: Implementation of horizon specular occlusion in GLSL
+

+

Horizon specular occlusion fading is cheap but can easily be omitted to improve performance as needed.

+

+   

Normal mapping

+

+

There are two common use cases of normal maps: replacing high-poly meshes with low-poly meshes (using a base map) and adding surface details (using a detail map).

+

+Let's imagine that we want to render a piece of furniture covered in tufted leather. Modeling the geometry to accurately represent the tufted pattern would require too many triangles so we instead bake a high-poly mesh into a normal map. Once the base map is applied to a simplified mesh (in this case, a quad), we get the result in figure 67. The base map used to create this effect is shown in figure 68. +

+

 
Figure 67: Low-poly mesh without normal mapping (left) and with (right)
+

+

 
Figure 68: Normal map used as a base map
+

+A simple problem arises if we now want to combine this base map with a second normal map. For instance, let's use the detail map shown in figure 69 to add cracks in the leather. +

+

 
Figure 69: Normal map used as a detail map
+

+Given the nature of normal maps (XYZ components stored in tangent space), it is fairly obvious that naive approaches such as linear or overlay blending cannot work. We will use two more advanced techniques: a mathematically correct one and an approximation suitable for real-time shading. +

+   

Reoriented normal mapping

+

+

Colin Barré-Brisebois and Stephen Hill propose in [Hill12] a mathematically sound solution called Reoriented Normal Mapping, which consists in rotating the basis of the detail map onto the normal from the base map. This technique relies on the shortest arc quaternion to apply the rotation, which greatly simplifies thanks to the properties of the tangent space.

+

+Following the simplifications described in [Hill12], we can produce the GLSL implementation shown in listing 40. +

 
vec3 t = texture(baseMap,   uv).xyz * vec3( 2.0,  2.0, 2.0) + vec3(-1.0, -1.0,  0.0);
+vec3 u = texture(detailMap, uv).xyz * vec3(-2.0, -2.0, 2.0) + vec3( 1.0,  1.0, -1.0);
+vec3 r = normalize(t * dot(t, u) - u * t.z);
+return r;
Listing 40: Implementation of reoriented normal mapping in GLSL
+

+

Note that this implementation assumes that the normals are stored uncompressed and in the [0..1] range in the source textures.

+

+The normalization step is not strictly necessary and can be skipped if the technique is used at runtime. If so, the computation of r becomes t * dot(t, u) / t.z - u. +

+Since this technique is slightly more expensive than the one described below, we will mostly use it offline. We therefore provide a simple offline tool to combine two normal maps. Figure 70 presents the output of the tool with the base map and the detail map shown previously. +

+

 
Figure 70: Blended normal and detail map (left) and resulting render when combined with a diffuse map (right)
+

+   

UDN blending

+

+

The technique called UDN blending, described in [Hill12], is a variant of the partial derivative blending technique. Its main advantage is the low number of shader instructions it requires (see listing 41). While it leads to a reduction in details over flat areas, UDN blending is interesting if blending must be performed at runtime.

+

 
vec3 t = texture(baseMap,   uv).xyz * 2.0 - 1.0;
+vec3 u = texture(detailMap, uv).xyz * 2.0 - 1.0;
+vec3 r = normalize(t.xy + u.xy, t.z);
+return r;
Listing 41: Implementation of UDN blending in GLSL
+

+

The results are visually close to Reoriented Normal Mapping but a careful comparison of the data shows that UDN is indeed less correct. Figure 71 presents the result of the UDN blending approach using the same source data as in the previous examples.

+

+

 
Figure 71: Blended normal and detail map using the UDN blending technique
+

+   

Volumetric effects

+   

Exponential height fog

+

+

 
Figure 72: Example of directional in-scattering with exponential height fog
+

+

 
Figure 73: Example of directional in-scattering with exponential height fog
+

+   

Anti-aliasing

+

+

[TODO] MSAA, geometric AA (normals and roughness), shader anti-aliasing (object-space shading?)

+

+   

Imaging pipeline

+

+

The lighting section of this document describes how light interacts with surfaces in the scene in a physically based manner. To achieve plausible results, we must go a step further and consider the transformations necessary to convert the scene luminance, as computed by our lighting equations, into displayable pixel values.

+

+The series of transformations we are going to use form the following imaging pipeline: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +SceneNormalizedluminanceluminanceWhitebalance(HDR)ColorgradingTonemappingPixelOETFvalue(LDR) +

+Note: the OETF step is the application of the opto-electronic transfer function of the target color space. For clarity this diagram does not include post-processing steps such as vignette, bloom, etc. These effects will be discussed separately. +

+[TODO] Color spaces (ACES, sRGB, Rec. 709, Rec. 2020, etc.), gamma/linear, etc. +

+   

Physically based camera

+

+

The first step in the image transformation process is to use a physically based camera to properly expose the scene's outgoing luminance.

+

+   

Exposure settings

+

+

Because we use photometric units throughout the lighting pipeline, the light reaching the camera is an energy expressed in luminance (L), in (cd.m^{-2}). Light incident to the camera sensor can cover a large range of values, from (10^{-5}cd.m^{-2}) for starlight to (10^{9}cd.m^{-2}) for the sun. Since we obviously cannot manipulate and even less record such a large range of values, we need to remap them.

+

+This range remapping is done in a camera by exposing the sensor for a certain time. To maximize the use of the limited range of the sensor, the scene's light range is centered around the “middle gray”, a value halfway between black and white. The exposition is therefore achieved by manipulating, either manually or automatically, 3 settings: +

+

    +
  • Aperture +
  • +
  • Shutter speed +
  • +
  • Sensitivity (also called gain)
+

+

Aperture

Noted \(N\) and expressed in f-stops ƒ, this setting controls how open or closed the camera system's aperture is. Since an f-stop indicate the ratio of the lens' focal length to the diameter of the entrance pupil, high-values (ƒ/16) indicate a small aperture and small values (ƒ/1.4) indicate a wide aperture. In addition to the exposition, the aperture setting controls the depth of field. +

Shutter speed

Noted \(t\) and expressed in seconds \(s\), this setting controls how long the aperture remains opened (it also controls the timing of the sensor shutter(s), whether electronic or mechanical). In addition to the exposition, the shutter speed controls motion blur. +

Sensitivity

Noted \(S\) and expressed in ISO, this setting controls how the light reaching the sensor is quantized. Because of its unit, this setting is often referred to as simply the “ISO” or “ISO setting”. In addition to the exposition, the sensitivity setting controls the amount of noise. +

+   

Exposure value

+

+

Since referring to these 3 settings in our equations would be unwieldy, we instead summarize the “exposure triangle” by an exposure value, noted EV10.

+

+The EV is expressed in a base-2 logarithmic scale, with a difference of 1 EV called a stop. One positive stop (+1 EV) corresponds to a factor of two in luminance and one negative stop (−1 EV) corresponds to a factor of half in luminance. +

+Equation \( \ref{ev} \) shows the formal definition of EV. +

+$$\begin{equation}\label{ev} +EV = log_2(\frac{N^2}{t}) +\end{equation}$$ +

+Note that this definition is only function of the aperture and shutter speed, but not the sensitivity. An exposure value is by convention defined for ISO 100, or \( EV_{100} \), and because we wish to work with this convention, we need to be able to express \( EV_{100} \) as a function of the sensitivity. +

+Since we know that EV is a base-2 logarithmic scale in which each stop increases or decreases the brightness by a factor of 2, we can formally define \( EV_{S} \), the exposure value at given sensitivity (equation \(\ref{evS}\)). +

+$$\begin{equation}\label{evS} +{EV}_S = EV_{100} + log_2(\frac{S}{100}) +\end{equation}$$ +

+Calculating the \( EV_{100} \) as a function of the 3 camera settings is trivial, as shown in \(\ref{ev100}\). +

+$$\begin{equation}\label{ev100} +{EV}_{100} = EV_{S} - log_2(\frac{S}{100}) = log_2(\frac{N^2}{t}) - log_2(\frac{S}{100}) +\end{equation}$$ +

+Note that the operator (photographer, etc.) can achieve the same exposure (and therefore EV) with several combinations of aperture, shutter speed and sensitivity. This allows some artistic control in the process (depth of field vs motion blur vs grain). +

+

 10 We assume a digital sensor, which means we don't need to take reciprocity failure into account +
+

+   

Exposure value and luminance

+

+

A camera, similar to a spot meter, is able to measure the average luminance of a scene and convert it into EV to achieve automatic exposure, or at the very least offer the user exposure guidance.

+

+It is possible to define EV as a function of the scene luminance \(L\), given a per-device calibration constant \(K\) (equation \( \ref{evK} \)). +

+$$\begin{equation}\label{evK} +EV = log_2(\frac{L \times S}{K}) +\end{equation}$$ +

+That constant \(K\) is the reflected-light meter constant, which varies between manufacturers. We could find two common values for this constant: 12.5, used by Canon, Nikon and Sekonic, and 14, used by Pentax and Minolta. Given the wide availability of Canon and Nikon cameras, as well as our own usage of Sekonic light meters, we will choose to use \( K = 12.5 \). +

+Since we want to work with \( EV_{100} \), we can substitute \(K\) and \(S\) in equation \( \ref{evK} \) to obtain equation \( \ref{ev100L} \). +

+$$\begin{equation}\label{ev100L} +EV = log_2(L \frac{100}{12.5}) +\end{equation}$$ +

+Given this relationship, it would be possible to implement automatic exposure in our engine by first measuring the average luminance of a frame. An easy way to achieve this is to simply downsample a luminance buffer down to 1 pixel and read the remaining value. This technique is unfortunately rarely stable and can easily be affected by extreme values. Many games use a different approach which consists in using a luminance histogram to remove extreme values. +

+For validation and testing purposes, the luminance can be computed from a given EV: +

+$$\begin{equation} +L = 2^{EV_{100}} \times \frac{12.5}{100} = 2^{EV_{100} - 3} +\end{equation}$$ +

+   

Exposure value and illuminance

+

+

It is possible to define EV as a function of the illuminance (E), given a per-device calibration constant (C):

+

+$$\begin{equation}\label{evC} +EV = log_2(\frac{E \times S}{C}) +\end{equation}$$ +

+The constant \(C\) is the incident-light meter constant, which varies between manufacturers and/or types of sensors. There are two common types of sensors: flat and hemispherical. For flat sensors, a common value is 250. With hemispherical sensors, we could find two common values: 320, used by Minolta, and 340, used by Sekonic. +

+Since we want to work with \( EV_{100} \), we can substitute \(S\) \( \ref{evC} \) to obtain equation \( \ref{ev100C} \). +

+$$\begin{equation}\label{ev100C} +EV = log_2(E \frac{100}{C}) +\end{equation}$$ +

+The illuminance can then be computed from a given EV. For a flat sensor with \( C = 250 \) we obtain equation \( \ref{eFlatSensor} \). +

+$$\begin{equation}\label{eFlatSensor} +E = 2^{EV_{100}} \times 2.5 +\end{equation}$$ +

+For a hemispherical sensor with \( C = 340 \) we obtain equation \( \ref{eHemisphereSensor} \) +

+$$\begin{equation}\label{eHemisphereSensor} +E = 2^{EV_{100}} \times 3.4 +\end{equation}$$ +

+   

Exposure compensation

+

+

Even though an exposure value actually indicates combinations of camera settings, it is often used by photographers to describe light intensity. This is why cameras let photographers apply an exposure compensation to over or under-expose an image. This setting can be used for artistic control but also to achieve proper exposure (snow for instance will be exposed for as 18% middle-gray).

+

+Applying an exposure compensation \(EC\) is a simple as adding an offset to the exposure value, as shown in equation \( \ref{ec} \). +

+$$\begin{equation}\label{ec} +EV_{100}' = EV_{100} - EC +\end{equation}$$ +

+This equation uses a negative sign because we are using \(EC\) in f-stops to adjust the final exposure. Increasing the EV is akin to closing down the aperture of the lens (or reducing shutter speed or reducing sensitivity). A higher EV will produce darker images. +

+   

Exposure

+

+

To convert the scene luminance into normalized luminance, we must use the photometric exposure (or luminous exposure), or amount of scene luminance that reaches the camera sensor. The photometric exposure, expressed in lux seconds and noted (H), is given by equation ( \ref{photometricExposure} ).

+

+$$\begin{equation}\label{photometricExposure} +H = \frac{q \cdot t}{N^2} L +\end{equation}$$ +

+Where \(L\) is the luminance of the scene, \(t\) the shutter speed, \(N\) the aperture and \(q\) the lens and vignetting attenuation (typically \( q = 0.65 \)11). This definition does not take the sensor sensitivity into account. To do so, we must use one of the three ways to relate photometric exposure and sensitivity: saturation-based speed, noise-based speed and standard output sensitivity. +

+We choose the saturation-based speed relation, which gives us \( H_{sat} \), the maximum possible exposure that does not lead to clipped or bloomed camera output (equation \( \ref{hSat} \)). +

+$$\begin{equation}\label{hSat} +H_{sat} = \frac{78}{S_{sat}} +\end{equation}$$ +

+We combine equations \( \ref{hSat} \) and \( \ref{photometricExposure} \) in equation \( \ref{lmax} \) to compute the maximum luminance \( L_{max} \) that will saturate the sensor given exposure settings \(S\), \(N\) and \(t\). +

+$$\begin{equation}\label{lmax} +L_{max} = \frac{N^2}{q \cdot t} \frac{78}{S} +\end{equation}$$ +

+This maximum luminance can then be used to normalize incident luminance \(L\) as shown in equation \( \ref{normalizedLuminance} \). +

+$$\begin{equation}\label{normalizedLuminance} +L' = L \frac{1}{L_{max}} +\end{equation}$$ +

+\( L_{max} \) can be simplified using equation \( \ref{ev} \), \( S = 100 \) and \( q = 0.65 \): +

+$$\begin{align*} +L_{max} &= \frac{N^2}{t} \frac{78}{q \cdot S} \\ +L_{max} &= 2^{EV_{100}} \frac{78}{q \cdot S} \\ +L_{max} &= 2^{EV_{100}} \times 1.2 +\end{align*}$$ +

+Listing 42 shows how the exposure term can be applied directly to the pixel color computed in a fragment shader. +

 
// Computes the camera's EV100 from exposure settings
+// aperture in f-stops
+// shutterSpeed in seconds
+// sensitivity in ISO
+float exposureSettings(float aperture, float shutterSpeed, float sensitivity) {
+    return log2((aperture * aperture) / shutterSpeed * 100.0 / sensitivity);
+}
+
+// Computes the exposure normalization factor from
+// the camera's EV100
+float exposure(float ev100) {
+    return 1.0 / (pow(2.0, ev100) * 1.2);
+}
+
+float ev100 = exposureSettings(aperture, shutterSpeed, sensitivity);
+float exposure = exposure(ev100);
+
+vec4 color = evaluateLighting();
+color.rgb *= exposure;
Listing 42: Implementation of exposure in GLSL
+

+

In practice the exposure factor can be pre-computed on the CPU to save shader instructions.

+

+

 11 See Film Speed, Measurements and calculations on Wikipedia (https://en.wikipedia.org/wiki/Film_speed) +
+

+   

Automatic exposure

+

+

The process described above relies on artists setting the camera exposure settings manually. This can prove cumbersome in practice since camera movements and/or dynamic effects can greatly affect the scene's luminance. Since we know how to compute the exposure value from a given luminance (see section 8.1.2.1), we can transform our camera into a spot meter. To do so, we need to measure the scene's luminance.

+

+There are two common techniques used to measure the scene's luminance: +

+

    +
  • Luminance downsampling, by downsampling the previous frame successively until obtaining a 1×1 log luminance buffer that can be read on the CPU (this could also be achieved using a compute shader). The result is the average log luminance of the scene. The first downsampling must extract the luminance of each pixel first. This technique can be unstable and its output should be smoothed over time. +
  • +
  • Using a luminance histogram, to find the average log luminance. This technique has an advantage over the previous one as it allows to ignore extreme values and offers more stable results.
+

+Note that both methods will find the average luminance after multiplication by the albedo. This is not entirely correct but the alternative is to keep a luminance buffer that contains the luminance of each pixel before multiplication by the surface albedo. This is expensive both computationally and memory-wise. +

+These two techniques also limit the metering system to average metering, where each pixel has the same influence (or weight) over the final exposure. Cameras typically offer 3 modes of metering: +

+

Spot metering

In which only a small circle in the center of the image contributes to the final exposure. That circle is usually 1 to 5% of the total image size. +

Center-weighted metering

Gives more influence to scene luminance values located in the center of the screen. +

Multi-zone or matrix metering

A metering mode that differs for each manufacturer. The goal of this mode is to prioritize exposure for the most important parts of the scene. This is often achieved by splitting the image into a grid and by classifying each cell (using focus information, min/max luminance, etc.). Advanced implementations attempt to compare the scene to a known dataset to achieve proper exposure (backlit sunset, overcast snowy day, etc.). +

+   

Spot metering

+

+

The weight (w) of each luminance value to use when computing the scene luminance is given by equation ( \ref{spotMetering} ).

+

+$$\begin{equation}\label{spotMetering} +w(x,y) = \begin{cases} 1 & \left| p_{x,y} - s_{x,y} \right| \le s_r \\ 0 & \left| p_{x,y} - s_{x,y} \right| \gt s_r \end{cases} +\end{equation}$$ +

+Where \(p\) is the position of the pixel, \(s\) the center of the spot and \( s_r \) the radius of the spot. +

+   

Center-weighted metering

+

+

$$\begin{equation}\label{centerMetering} +w(x,y) = smooth(\left| p_{x,y} - c \right| \times \frac{2}{width} ) +\end{equation}$$

+

+Where \(c\) is the center of the time and \( smooth() \) a smoothing function such as GLSL's smoothstep(). +

+   

Adaptation

+

+

To smooth the result of the metering, we can use equation ( \ref{adaptation} ), an exponential feedback loop as described by Pattanaik et al. in [Pattanaik00].

+

+$$\begin{equation}\label{adaptation} +L_{avg} = L_{avg} + (L - L_{avg}) \times (1 - e^{-\Delta t \cdot \tau}) +\end{equation}$$ +

+Where \( \Delta t \) is the delta time from the previous frame and \(\tau\) a constant that controls the adaptation rate. +

+   

Bloom

+

+

Because the EV scale is almost perceptually linear, the exposure value is also often used as a light unit. This means we could let artists specify the intensity of lights or emissive surfaces using exposure compensation as a unit. The intensity of emitted light would therefore be relative to the exposure settings. Using exposure compensation as a light unit should be avoided whenever possible but can be useful to force (or cancel) a bloom effect around emissive surfaces independently of the camera settings (for instance, a lightsaber in a game should always bloom).

+

+

 
Figure 74: Saturated photosites on a sensor create a blooming effect in the bright parts of the scene
+

+With \(c\) the bloom color and \( EV_{100} \) the current exposure value, we can easily compute the luminance of the bloom value as show in equation \( \ref{bloomEV} \). +

+$$\begin{equation}\label{bloomEV} +EV_{bloom} = EV_{100} + EC \\ +L_{bloom} = c \times 2^{EV_{bloom} - 3} +\end{equation}$$ +

+Equation \( \ref{bloomEV} \) can be used in a fragment shader to implement emissive blooms, as shown in listing 43. +

 
vec4 surfaceShading() {
+    vec4 color = evaluateLights();
+    // rgb = color, w = exposure compensation
+    vec4 emissive = getEmissive();
+    color.rgb += emissive.rgb * pow(2.0, ev100 + emissive.w - 3.0);
+    color.rgb *= exposure;
+    return color;
+}
Listing 43: Implementation of emissive bloom in GLSL
+   

Optics post-processing

+   

Color fringing

+

+

[TODO]

+

+

 
Figure 75: Example of color fringing: look at the ear on the left or the chin at the bottom.
+

+   

Lens flares

+

+

[TODO] Notes: there is a physically based approach to generating lens flares, by tracing rays through the optical assembly of the lens, but we are going to use an image-based approach. This approach is cheaper and has a few welcome benefits such as free emitters occlusion and unlimited light sources support.

+

+   

Filmic post-processing

+

+

[TODO] Perform post-processing on the scene referred data (linear space, before tone-mapping) as much as possible

+

+It is important to provide color correction tools to give artists greater artistic control over the final image. These tools are found in every photo or video processing application, such as Adobe Photoshop or Adobe After Effects. +

+   

Contrast

+   

Curves

+   

Levels

+   

Color grading

+   

Light path

+

+

The light path, or rendering method, used by the engine can have serious performance implications and may impose strong limitations on how many lights can be used in a scene. There are traditionally two different rendering methods used by 3D engines forward and deferred rendering.

+

+Our goal is to use a rendering method that obeys the following constraints: +

+

    +
  • Low bandwidth requirements +
  • +
  • Multiple dynamic lights per pixel
+

+Additionally, we would like to easily support: +

+

    +
  • MSAA +
  • +
  • Transparency +
  • +
  • Multiple material models
+

+Deferred rendering is used by many modern 3D rendering engines to easily support dozens, hundreds or even thousands of light source (amongst other benefits). This method is unfortunately very expensive in terms of bandwidth. With our default PBR material model, our G-buffer would use between 160 and 192 bits per pixel, which would translate directly to rather high bandwidth requirements. +

+Forward rendering methods on the other hand have historically been bad at handling multiple lights. A common implementation is to render the scene multiple times, once per visible light, and to blend (add) the results. Another technique consists in assigning a fixed maximum of lights to each object in the scene. This is however impractical when objects occupy a vast amount of space in the world (building, road, etc.). +

+Tiled shading can be applied to both forward and deferred rendering methods. The idea is to split the screen in a grid of tiles and for each tile, find the list of lights that affect the pixels within that tile. This has the advantage of reducing overdraw (in deferred rendering) and shading computations of large objects (in forward rendering). This technique suffers however from depth discontinuities issues that can lead to large amounts of extraneous work. +

+The scene displayed in figure 76 was rendered using clustered forward rendering. +

+

 
Figure 76: Clustered forward rendering with dozens of dynamic lights and MSAA
+

+Figure 77 shows the same scene split in tiles (in this case, a 1280×720 render target with 80×80px tiles). +

+

 
Figure 77: Tiled shading (16×9 tiles)
+

+   

Clustered Forward Rendering

+

+

We decided to explore another method called Clustered Shading, in its forward variant. Clustered shading expands on the idea of tiled rendering but adds a segmentation on the 3rd axis. The “clustering” is done in view space, by splitting the frustum into a 3D grid.

+

+The frustum is first sliced on the depth axis as show in figure 78. +

+

 
Figure 78: Depth slicing (16 slices)
+

+And the depth slices are then combined with the screen tiles to “voxelize” the frustum. We call each cluster a froxel as it makes it clear what they represent (a voxel in frustum space). The result of the “froxelization” pass is shown in figure 79 and figure 80. +

+

 
Figure 79: Frustum voxelization (5×3 tiles, 8 depth slices)
+

+

 
Figure 80: Frustum voxelization (5×3 tiles, 8 depth slices)
+

+Before rendering a frame, each light in the scene is assigned to any froxel it intersects with. The result of the lights assignment pass is a list of lights for each froxel. During the rendering pass, we can compute the ID of the froxel a fragment belongs to and therefore the list of lights that can affect that fragment. +

+The depth slicing is not linear, but exponential. In a typical scene, there will be more pixels close to the near plane than to the far plane. An exponential grid of froxels will therefore improve the assignment of lights where it matters the most. +

+Figure 81 shows how much world space unit each depth slice uses with exponential slicing. +

+

 
Figure 81: Near: 0.1m, Far: 100m, 16 slices
+

+A simple exponential voxelization is unfortunately not enough. The graphic above clearly illustrates how world space is distributed across slices but it fails to show what happens close to the near plane. If we examine the same distribution in a smaller range (0.1m to 7m) we can see an interesting problem appear as shown in figure 82. +

+

 
Figure 82: Depth distribution in the 0.1-7m range
+

+This graphic shows that a simple exponential distribution uses up half of the slices very close to the camera. In this particular case, we use 8 slices out of 16in the first 5 meters. Since dynamic world lights are either point lights (spheres) or spot lights (cones), such a fine resolution is completely unnecessary so close to the near plane. +

+Our solution is to manually tweak the size of the first froxel depending on the scene and the near and far planes. By doing so, we can better distribute the remaining froxels across the frustum. Figure 83 shows for instance what happens when we use a special froxel between 0.1m and 5m. +

+

 
Figure 83: Near: 0.1, Far: 100m, 16 slices, Special froxel: 0.1-5m
+

+This new distribution is much more efficient and allows a better assignment of the lights throughout the entire frustum. +

+   

Implementation notes

+

+

Lights assignment can be done in two different ways, on the GPU or on the CPU.

+

+   

GPU lights assignment

+

+

This implementation requires OpenGL ES 3.1 and support for compute shaders. The lights are stored in Shader Storage Buffer Objects (SSBO) and passed to a compute shader that assigns each light to the corresponding froxels.

+

+The frustum voxelization can be executed only once by a first compute shader (as long as the projection matrix does not change), and the lights assignment can be performed each frame by another compute shader. +

+The threading model of compute shaders is particularly well suited for this task. We simply invoke as many workgroups as we have froxels (we can directly map the X, Y and Z workgroup counts to our froxel grid resolution). Each workground will in turn be threaded and traverse all the lights to assign. +

+Intersection tests imply simple sphere/frustum or cone/frustum tests. +

+See the annex for the source code of a GPU implementation (point lights only). +

+   

CPU lights assignment

+

+

On non-OpenGL ES 3.1 devices, lights assignment can be performed efficiently on the CPU. The algorithm is different from the GPU implementation. Instead of iterating over every light for each froxel, the engine will “rasterize” each light as froxels. For instance, given a point light’s center and radius, it is trivial to compute the list of froxels it intersects with.

+

+This technique has the added benefit of providing tighter culling than in the GPU variant. The CPU implementation can also more easily generate a packed list of lights. +

+   

Shading

+

+

The list of lights per froxel can be passed to the fragment shader either as an SSBO (OpenGL ES 3.1) or a texture.

+

+   

From depth to froxel

+

+

Given a near plane (n), a far plane (f), a maximum number of depth slices (m) and a linear depth value (z) in the range [0..1], equation (\ref{zToCluster}) can be used to compute the index of the cluster for a given position.

+

+$$\begin{equation}\label{zToCluster} +zToCluster(z,n,f,m)=floor \left( max \left( log2(z) \frac{m}{-log2(\frac{n}{f})} + m, 0 \right) \right) +\end{equation}$$ +

+This formula suffers however from the resolution issue mentioned previously. We can fix it by introducing \(sn\), a special near value that defines the extent of the first froxel (the first froxel occupies the range [n..sn], the remaining froxels [sn..f]). +

+$$\begin{equation}\label{zToClusterFix} +zToCluster(z,n,sn,f,m)=floor \left( max \left( log2(z) \frac{m-1}{-log2(\frac{sn}{f})} + m, 0 \right) \right) +\end{equation}$$ +

+Equation \(\ref{linearZ}\) can be used to compute a linear depth value from gl_FragCoord.z (assuming a standard OpenGL projection matrix). +

+$$\begin{equation}\label{linearZ} +linearZ(z)=\frac{n}{f+z(n-f)} +\end{equation}$$ +

+This equation can be simplified by pre-computing two terms \(c0\) and \(c1\), as shown in equation \(\ref{linearZFix}\). +

+$$\begin{equation}\label{linearZFix} +c1 = \frac{f}{n} \\ +c0 = 1 - c1 \\ +linearZ(z)=\frac{1}{z \cdot c0 + c1} +\end{equation}$$ +

+This simplification is important because we pass the linear z value to a log2 in \(\ref{zToClusterFix}\). Since the division becomes a negation under a logarithmic, we can avoid a division by using \(-log2(z \cdot c0 + c1)\) instead. +

+All put together, computing the froxel index of a given fragment can be implemented fairly easily as shown in listing 44. +

 
#define MAX_LIGHT_COUNT 16 // max number of lights per froxel
+
+uniform uvec4 froxels; // res x, res y, count y, count y
+uniform vec4 zParams;  // c0, c1, index scale, index bias
+
+uint getDepthSlice() {
+    return uint(max(0.0, log2(zParams.x * gl_FragCoord.z + zParams.y) *
+            zParams.z + zParams.w));
+}
+
+uint getFroxelOffset(uint depthSlice) {
+    uvec2 froxelCoord = uvec2(gl_FragCoord.xy) / froxels.xy;
+    froxelCoord.y = (froxels.w - 1u) - froxelCoord.y;
+
+    uint index = froxelCoord.x + froxelCoord.y * froxels.z +
+            depthSlice * froxels.z * froxels.w;
+    return index * MAX_FROXEL_LIGHT_COUNT;
+}
+
+uint slice = getDepthSlice();
+uint offset = getFroxelOffset(slice);
+
+// Compute lighting...
Listing 44: GLSL implementation to compute a froxel index from a fragment's screen coordinates
+

+

Several uniforms must be pre-computed for perform the index evaluation efficiently. The code used to pre-compute these uniforms can be found in listing ?.

+

froxels[0] = TILE_RESOLUTION_IN_PX;
+froxels[1] = TILE_RESOLUTION_IN_PX;
+froxels[2] = numberOfTilesInX;
+froxels[3] = numberOfTilesInY;
+
+zParams[0] = 1.0f - Z_FAR / Z_NEAR;
+zParams[1] = Z_FAR / Z_NEAR;
+zParams[2] = (MAX_DEPTH_SLICES - 1) / log2(Z_SPECIAL_NEAR / Z_FAR);
+zParams[3] = MAX_DEPTH_SLICES;
[Listing ?]
+   

From froxel to depth

+

+

Given a froxel index (i), a special near plane (sn), a far plane (f) and a maximum number of depth slices (m), equation (\ref{clusterToZ}) computes the minimum depth of a given froxel.

+

+$$\begin{equation}\label{clusterToZ} +clusterToZ(i \ge 1,sn,f,m)=2^{(i-m) \frac{-log2(\frac{sn}{f})}{m-1}} +\end{equation}$$ +

+For \(i=0\), the z value is 0. The result of this equation is in the [0..1] range and should be multiplied by \(f\) to get a distance in world units. +

+The compute shader implementation should use exp2 instead of a pow. The division can be precomputed and passed as a uniform. +

+   

Validation

+

+

Given the complexity of our lighting system, it is important to validate our implementation. We will do so in several ways: using reference renderings, light measurements and data visualization.

+

+[TODO] Explain light measurement validation (reading EV from the render target and comparing against values measure with light meters/cameras, etc.) +

+   

Scene referred visualization

+

+

A quick and easy way to validate a scene's lighting is to modify the shader to output colors that provide an intuitive mapping to relevant data. This can easily be done by using a custom debug tone-mapping operator that outputs fake colors.

+

+   

Luminance stops

+

+

With emissive materials and IBLs, it is fairly easy to obtain a scene in which specular highlights are brighter than their apparent caster. This type of issue can be difficult to observe after tone-mapping and quantization but is fairly obvious in the scene-referred space. Figure 84 shows how the custom operator described in listing 45 is used to show the exposed luminance of a scene.

+

+

 
Figure 84: Visualizing luminance by color coding the stops: cyan is middle gray, blue is 1 stop darker, green 1 stop brighter, etc.
+

 
vec3 Tonemap_DisplayRange(const vec3 x) {
+    // The 5th color in the array (cyan) represents middle gray (18%)
+    // Every stop above or below middle gray causes a color shift
+    float v = log2(luminance(x) / 0.18);
+    v = clamp(v + 5.0, 0.0, 15.0);
+    int index = int(floor(v));
+    return mix(debugColors[index], debugColors[min(15, index + 1)], fract(v));
+}
+
+const vec3 debugColors[16] = vec3[](
+     vec3(0.0, 0.0, 0.0),         // black
+     vec3(0.0, 0.0, 0.1647),      // darkest blue
+     vec3(0.0, 0.0, 0.3647),      // darker blue
+     vec3(0.0, 0.0, 0.6647),      // dark blue
+     vec3(0.0, 0.0, 0.9647),      // blue
+     vec3(0.0, 0.9255, 0.9255),   // cyan
+     vec3(0.0, 0.5647, 0.0),      // dark green
+     vec3(0.0, 0.7843, 0.0),      // green
+     vec3(1.0, 1.0, 0.0),         // yellow
+     vec3(0.90588, 0.75294, 0.0), // yellow-orange
+     vec3(1.0, 0.5647, 0.0),      // orange
+     vec3(1.0, 0.0, 0.0),         // bright red
+     vec3(0.8392, 0.0, 0.0),      // red
+     vec3(1.0, 0.0, 1.0),         // magenta
+     vec3(0.6, 0.3333, 0.7882),   // purple
+     vec3(1.0, 1.0, 1.0)          // white
+);
Listing 45: GLSL implementation of a custom debug tone-mapping operator for luminance visualization
+   

Reference renderings

+

+

To validate our implementation against reference renderings, we will use a commercial-grade Open Source physically based offline path tracer called Mitsuba. Mitsuba offers many different integrators, samplers and material models, which should allow us to provide fair comparisons with our real-time renderer. This path tracer also relies on a simple XML scene description format that should be easy to automatically generate from our own scene descriptions.

+

+Figure 85 and figure 86 show a simple scene, a perfectly smooth dielectric sphere, rendered respectively with Mitsuba and Filament. +

+

 
Figure 85: Rendered in 2048×1440 in 1 minute and 42 seconds on a 12 core 2013 MacPro
+

+

 
Figure 86: Rendered in 2048×1440 with MSAA 4x at 60 fps on a Nexus 9 device (Tegra K1 GPU)
+

+The parameters used to render both scenes are the following: +

+Filament +

+

    +
  • Material +
      +
    • Base color: sRGB 0.81, 0, 0 +
    • +
    • Metallic: 0 +
    • +
    • Roughness: 0 +
    • +
    • Reflectance: 0.5 +
    +
  • Indirect light: IBL +
      +
    • 256×256 cubemap generated by cmgen from office.exr +
    • +
    • Multiplier: 35,000 +
    +
  • Direct light: directional light +
      +
    • Linear color: 1.0, 0.96, 0.95 +
    • +
    • Intensity: 120,000 lux +
    +
  • Exposure +
      +
    • Aperture: f/16 +
    • +
    • Shutter speed: 1/125s +
    • +
    • ISO: 100
+

+Mitsuba +

+

    +
  • BSDF: roughplastic +
      +
    • Distribution: GGX +
    • +
    • Alpha: 0 +
    • +
    • Diffuse reflectance: sRGB 0.81, 0, 0 +
    +
  • Emitter: environment map +
      +
    • Source: office.exr +
    • +
    • Scale: 35,000 +
    +
  • Emitter: directional +
      +
    • Irradiance: linear RGB 120,000 115,200 114,000 +
    +
  • Film: LDR +
      +
    • Exposure: −15.23, computed from log2(filamentExposure) +
    +
  • Integrator: path +
  • +
  • Sampler: ldsampler +
      +
    • Sample count: 256
+

+The full Mitsuba scene can be found as an annex. Both scenes were rendered at the same resolution (2048×1440). +

+   

Comparison

+

+

The slight differences between the two renderings come from the various approximations used by Filament: RGBM 256×256 reflection probe, RGBM 1024×1024 background map, Lambert diffuse, split-sum approximation, analytical approximation of the DFG term, etc.

+

+Figure 87 shows the luminance gradient of the images produced by both engines. The comparison was performed on LDR images. +

+

 
Figure 87: Luminance gradients from Mitsuba (left) and Filament (right)
+

+The biggest difference is visible at grazing angles, which is most likely explained by Filament's use of a Lambertian diffuse term. The Disney diffuse term and its grazing retro-reflections would move Filament closer to Mitsuba. +

+   

Coordinates systems

+   

World coordinates system

+

+

Filament uses a Y-up, right-handed coordinate system.

+

+

 
Figure 88: Red +X, green +Y, blue +Z (rendered in Marmoset Toolbag).
+

+   

Camera coordinates system

+

+

Filament's Camera looks towards its local -Z axis. That is, when placing a camera in the world +without any transform applied to it, the camera looks down the world's -Z axis.

+

+   

Cubemaps coordinates system

+

+

All cubemaps used in Filament follow the OpenGL convention for face +alignment shown in figure 89.

+

+

 
Figure 89: Horizontal cross representation of a cubemap following the OpenGL faces alignment convention.
+

+Note that environment background and reflection probes are mirrored (see section 8.6.3.1). +

+   

Mirroring

+

+

To simplify the rendering of reflections, IBL cubemaps are stored mirrored on the X axis. This is +the default behaviour of the cmgen tool. This means that an IBL cubemap used as environment +background needs to be mirrored again at runtime. +An easy way to achieve this for skyboxes is to use textured back faces. Filament does +this by default.

+

+   

Equirectangular environment maps

+

+

To convert equirectangular environment maps to horizontal/vertical cross cubemaps we position the ++Z face in the center of the source rectilinear environment map.

+

+   

World space orientation of environment maps and Skyboxes

+

+

When specifying a skybox or an IBL in Filament, the specified cubemap is oriented such that its +-Z face points towards the +Z axis of the world (this is because filament assumes mirrored cubemaps, +see section 8.6.3.1). However, because environments and skyboxes are expected to be pre-mirrored, +their -Z (back) face points towards the world's -Z axis as expected (and the camera looks toward that +direction by default, see section 8.6.2).

+

+   

Annex

+   

Specular color

+

+

The specular color of a metallic surface, or (\fNormal), can be computed directly from measured spectral data. Online databases such as Refractive Index provide tables of complex IOR measured at different wavelengths for various materials.

+

+Earlier in this document, we presented equation \(\ref{fresnelEquation}\) to compute the Fresnel reflectance at normal incidence for a dielectric surface given its IOR. The same equation can be rewritten for conductors by using complex numbers to represent the surface's IOR: +

+$$\begin{equation} +c_{ior} = n_{ior} + ik +\end{equation}$$ +

+Equation \(\ref{fresnelComplexIOR}\) presents the resulting Fresnel formula, where \(c^*\) is the conjugate of the complex number \(c\): +

+$$\begin{equation}\label{fresnelComplexIOR} +\fNormal(c_{ior}) = \frac{(c_{ior} - 1)(c_{ior}^* - 1)}{(c_{ior} + 1)(c_{ior}^* + 1)} +\end{equation}$$ +

+To compute the specular color of a material we need to evaluate the complex Fresnel equation at each spectral sample of complex IOR over the visible spectrum. For each spectral sample, we obtain a spectral reflectance sample. To find the RGB color at normal incidence, we must multiply each sample by the CIE XYZ CMFs (color matching functions) and the spectral power distribution of the desired illuminant. We choose the standard illuminant D65 because we want to compute a color in the sRGB color space. +

+We then sum (integrate) and normalize all the samples to obtain \(\fNormal\) in the XYZ color space. From there, a simple color space conversion yields a linear sRGB color or a non-linear sRGB color after applying the opto-electronic transfer function (OETF, commonly known as “gamma” curve). Note that for some materials such as gold the final sRGB color might fall out of gamut. We use a simple normalization step as a cheap form of gamut remapping but it would be interesting to consider computing values in a color space with a wider gamut (for instance BT.2020). +

+To achieve the desired result we used the ICE 1931 2° CMFs, from 360nm to 830nm at 1nm intervals (source), and the CIE Standard Illuminant D65 relative spectral power distribution, from 300nm to 830nm, at 5nm intervals (source). +

+Our implementation is presented in listing 46, with the actual data omitted for brevity. +

 
// CIE 1931 2-deg color matching functions (CMFs), from 360nm to 830nm,
+// at 1nm intervals
+//
+// Data source:
+//     http://cvrl.ioo.ucl.ac.uk/cmfs.htm
+//     http://cvrl.ioo.ucl.ac.uk/database/text/cmfs/ciexyz31.htm
+const size_t CIE_XYZ_START = 360;
+const size_t CIE_XYZ_COUNT = 471;
+const float3 CIE_XYZ[CIE_XYZ_COUNT] = { ... };
+
+// CIE Standard Illuminant D65 relative spectral power distribution,
+// from 300nm to 830, at 5nm intervals
+//
+// Data source:
+//     https://en.wikipedia.org/wiki/Illuminant_D65
+//     https://cielab.xyz/pdf/CIE_sel_colorimetric_tables.xls
+const size_t CIE_D65_INTERVAL = 5;
+const size_t CIE_D65_START = 300;
+const size_t CIE_D65_END = 830;
+const size_t CIE_D65_COUNT = 107;
+const float CIE_D65[CIE_D65_COUNT] = { ... };
+
+struct Sample {
+    float w = 0.0f; // wavelength
+    std::complex<float> ior; // complex IOR, n + ik
+};
+
+static float illuminantD65(float w) {
+    auto i0 = size_t((w - CIE_D65_START) / CIE_D65_INTERVAL);
+    uint2 indexBounds{i0, std::min(i0 + 1, CIE_D65_END)};
+
+    float2 wavelengthBounds = CIE_D65_START + float2{indexBounds} * CIE_D65_INTERVAL;
+    float t = (w - wavelengthBounds.x) / (wavelengthBounds.y - wavelengthBounds.x);
+    return lerp(CIE_D65[indexBounds.x], CIE_D65[indexBounds.y], t);
+}
+
+// For std::lower_bound
+bool operator<(const Sample& lhs, const Sample& rhs) {
+    return lhs.w < rhs.w;
+}
+
+// The wavelength w must be between 360nm and 830nm
+static std::complex<float> findSample(const std::vector<sample>& samples, float w) {
+    auto i1 = std::lower_bound(
+         samples.begin(), samples.end(), Sample{w, 0.0f + 0.0if});
+    auto i0 = i1 - 1;
+
+    // Interpolate the complex IORs
+    float t = (w - i0->w) / (i1->w - i0->w);
+    float n = lerp(i0->ior.real(), i1->ior.real(), t);
+    float k = lerp(i0->ior.imag(), i1->ior.imag(), t);
+    return { n, k };
+}
+
+static float fresnel(const std::complex<float>& sample) {
+    return (((sample - (1.0f + 0if)) * (std::conj(sample) - (1.0f + 0if))) /
+            ((sample + (1.0f + 0if)) * (std::conj(sample) + (1.0f + 0if)))).real();
+}
+
+static float3 XYZ_to_sRGB(const float3& v) {
+    const mat3f XYZ_sRGB{
+             3.2404542f, -0.9692660f,  0.0556434f,
+            -1.5371385f,  1.8760108f, -0.2040259f,
+            -0.4985314f,  0.0415560f,  1.0572252f
+    };
+    return XYZ_sRGB * v;
+}
+
+// Outputs a linear sRGB color
+static float3 computeColor(const std::vector<sample>& samples) {
+    float3 xyz{0.0f};
+    float y = 0.0f;
+
+    for (size_t i = 0; i < CIE_XYZ_COUNT; i++) {
+        // Current wavelength
+        float w = CIE_XYZ_START + i;
+
+        // Find most appropriate CIE XYZ sample for the wavelength
+        auto sample = findSample(samples, w);
+        // Compute Fresnel reflectance at normal incidence
+        float f0 = fresnel(sample);
+
+        // We need to multiply by the spectral power distribution of the illuminant
+        float d65 = illuminantD65(w);
+
+        xyz += f0 * CIE_XYZ[i] * d65;
+        y += CIE_XYZ[i].y * d65;
+    }
+
+    // Normalize so that 100% reflectance at every wavelength yields Y=1
+    xyz /= y;
+
+    float3 linear = XYZ_to_sRGB(xyz);
+
+    // Normalize out-of-gamut values
+    if (any(greaterThan(linear, float3{1.0f}))) linear *= 1.0f / max(linear);
+
+    return linear;
+}
Listing 46: C++ implementation to compute the base color of a metallic surface from spectral data
+

+

Special thanks to Naty Hoffman for his valuable help on this topic.

+

+   

Importance sampling for the IBL

+

+

In the discrete domain, the integral can be approximated with sampling as defined in equation (\ref{iblSampling}).

+

+$$\begin{equation}\label{iblSampling} +\Lout(n,v,\Theta) \equiv \frac{1}{N} \sum_{i}^{N} f(l_{i}^{uniform},v,\Theta) L_{\perp}(l_i) \left< n \cdot l_i^{uniform} \right> +\end{equation}$$ +

+Unfortunately, we would need too many samples to evaluate this integral. A technique commonly used +is to choose samples that are more “important” more often, this is called importance sampling. +In our case we'll use the distribution of micro-facets normals, \(D_{ggx}\), as the distribution of +important samples. +

+The evaluation of \( \Lout(n,v,\Theta) \) with importance sampling is presented in equation \(\ref{annexIblImportanceSampling}\). +

+$$\begin{equation}\label{annexIblImportanceSampling} +\Lout(n,v,\Theta) \equiv \frac{1}{N} \sum_{i}^{N} \frac{f(l_{i},v,\Theta)}{p(l_i,v,\Theta)} L_{\perp}(l_i) \left< n \cdot l_i \right> +\end{equation}$$ +

+In equation \(\ref{annexIblImportanceSampling}\), \(p\) is the probability density function (PDF) of the +distribution of important direction samples \(l_i\). These samples depend on \(h_i\), \(v\) and \(\alpha\). +The definition of the PDF is shown in equation \(\ref{iblPDF}\). +

+\(h_i\) is given by the distribution we chose, see section 9.2.1 for more details. +

+The important direction samples \(l_i\) are calculated as the reflection of \(v\) around \(h_i\), and therefore +do not have the same PDF as \(h_i\). The PDF of a transformed distribution is given by: +

+$$\begin{equation} +p(T_r(x)) = p(x) |J(T_r)|^{-1} +\end{equation}$$ +

+Where \(|J(T_r)|\) is the determinant of the Jacobian of the transform. In our case we're considering +the transform from \(h_i\) to \(l_i\) and the determinant of its Jacobian is given in \ref{iblPDF}. +

+$$\begin{equation}\label{iblPDF} +p(l,v,\Theta) = D(h,\alpha) \left< \NoH \right> |J_{h \rightarrow l}|^{-1} \\ +|J_{h \rightarrow l}| = 4 \left< \VoH \right> +\end{equation}$$ +

+   

Choosing important directions

+

+

Refer to section 9.3 for more details. Given a uniform distribution ((\zeta_{\phi},\zeta_{\theta})) the important direction (l) is defined by equation (\ref{importantDirection}).

+

+$$\begin{equation}\label{importantDirection} +\phi = 2 \pi \zeta_{\phi} \\ +\theta = cos^{-1} \sqrt{\frac{1 - \zeta_{\theta}}{(\alpha^2 - 1)\zeta_{\theta}+1}} \\ +l = \{ cos \phi sin \theta, sin \phi sin \theta, cos \theta \} +\end{equation}$$ +

+Typically, \( (\zeta_{\phi},\zeta_{\theta}) \) are chosen using the Hammersley uniform distribution algorithm described in section 9.4. +

+   

Pre-filtered importance sampling

+

+

Importance sampling considers only the PDF to generate important directions; in particular, it is oblivious to the actual content of the IBL. If the latter contains high frequencies in areas without a lot of samples, the integration won’t be accurate. This can be somewhat mitigated by using a technique called pre-filtered importance sampling, in addition this allows the integral to converge with many fewer samples.

+

+Pre-filtered importance sampling uses several images of the environment increasingly low-pass filtered. This is typically implemented very efficiently with mipmaps and a box filter. The LOD is selected based on the sample importance, that is, low probability samples use a higher LOD index (more filtered). +

+This technique is described in details in [Krivanek08]. +

+The cubemap LOD is determined in the following way: +

+$$\begin{align*} +lod &= log_4 \left( K\frac{\Omega_s}{\Omega_p} \right) \\ +K &= 4.0 \\ +\Omega_s &= \frac{1}{N \cdot p(l_i)} \\ +\Omega_p &\approx \frac{4\pi}{6 \cdot width \cdot height} +\end{align*}$$ +

+Where \(K\) is a constant determined empirically, \(p\) the PDF of the BRDF, \( \Omega_{s} \) the solid angle associated to the sample and \(\Omega_p\) the solid angle associated with the texel in the cubemap. +

+Cubemap sampling is done using seamless trilinear filtering. It is extremely important to sample the cubemap correctly across faces using OpenGL's seamless sampling feature or any other technique that avoids/reduces seams. +

+Table 17 shows a comparison between importance sampling and pre-filtered importance sampling when applied to figure 90. +

+

 
Figure 90: Importance sampling image reference
+

+

+  + + + +
Samples Importance sampling Pre-filtered importance sampling
4096  
1024
32
Table 17: Importance sampling vs pre-filtered importance sampling with \(\alpha = 0.4\)
+

+The reference renderer used in the comparison below performs no approximation. In particular, it does not assume \(v = n\) and does not perform the split sum approximation. The pre-filtered renderer uses all the techniques discussed in this section: pre-filtered cubemaps, the analytic formulation of the DFG term, and of course the split sum approximation. +

+Left: reference renderer, right: pre-filtered importance sampling. +

+
+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+   

Choosing important directions for sampling the BRDF

+

+

For simplicity we use the ( D ) term of the BRDF as the PDF, however the PDF must be normalized such that the integral over the hemisphere is 1:

+

+$$\begin{equation} +\int_{\Omega}p(m)dm = 1 \\ +\int_{\Omega}D(m)(n \cdot m)dm = 1 \\ +\int_{\phi=0}^{2\pi}\int_{\theta=0}^{\frac{\pi}{2}}D(\theta,\phi) cos \theta sin \theta d\theta d\phi = 1 \\ +\end{equation}$$ +

+The PDF of the BRDF can therefore be expressed as in equation \(\ref{importantPDF}\): +

+$$\begin{equation}\label{importantPDF} +p(\theta,\phi) = \frac{\alpha^2}{\pi(cos^2\theta (\alpha^2-1) + 1)^2} cos\theta sin\theta +\end{equation}$$ +

+The term \(sin\theta\) comes from the differential solid angle \(sin\theta d\phi d\theta\) since we integrate over a sphere. We sample \(\theta\) and \(\phi\) independently: +

+$$\begin{align*} +p(\theta) &= \int_0^{2\pi} p(\theta,\phi) d\phi = \frac{2\alpha^2}{(cos^2\theta (\alpha^2-1) + 1)^2} cos\theta sin\theta \\ +p(\phi) &= \frac{p(\theta,\phi)}{p(\phi)} = \frac{1}{2\pi} +\end{align*}$$ +

+The expression of \( p(\phi) \) is true for an isotropic distribution of normals. +

+We then calculate the cumulative distribution function (CDF) for each variable: +

+$$\begin{align*} +P(s_{\phi}) &= \int_{0}^{s_{\phi}} p(\phi) d\phi = \frac{s_{\phi}}{2\pi} \\ +P(s_{\theta}) &= \int_{0}^{s_{\theta}} p(\theta) d\theta = 2 \alpha^2 \left( \frac{1}{(2\alpha^4-4\alpha^2+2) cos(s_{\theta})^2 + 2\alpha^2 - 2} - \frac{1}{2\alpha^4-2\alpha^2} \right) +\end{align*}$$ +

+We set \( P(s_{\phi}) \) and \( P(s_{\theta}) \) to random variables \( \zeta_{\phi} \) and \( \zeta_{\theta} \) and solve for \( s_{\phi} \) and \( s_{\theta} \) respectively: +

+$$\begin{align*} +P(s_{\phi}) &= \zeta_{\phi} \rightarrow s_{\phi} = 2\pi\zeta_{\phi} \\ +P(s_{\theta}) &= \zeta_{\theta} \rightarrow s_{\theta} = cos^{-1} \sqrt{\frac{1-\zeta_{\theta}}{(\alpha^2-1)\zeta_{\theta}+1}} +\end{align*}$$ +

+So given a uniform distribution \( (\zeta_{\phi},\zeta_{\theta}) \), our important direction \(l\) is defined as: +

+$$\begin{align*} +\phi &= 2\pi\zeta_{\phi} \\ +\theta &= cos^{-1} \sqrt{\frac{1-\zeta_{\theta}}{(\alpha^2-1)\zeta_{\theta}+1}} \\ +l &= \{ cos\phi sin\theta,sin\phi sin\theta,cos\theta \} +\end{align*}$$ +

+   

Hammersley sequence

+
vec2f hammersley(uint i, float numSamples) {
+    uint bits = i;
+    bits = (bits << 16) | (bits >> 16);
+    bits = ((bits & 0x55555555) << 1) | ((bits & 0xAAAAAAAA) >> 1);
+    bits = ((bits & 0x33333333) << 2) | ((bits & 0xCCCCCCCC) >> 2);
+    bits = ((bits & 0x0F0F0F0F) << 4) | ((bits & 0xF0F0F0F0) >> 4);
+    bits = ((bits & 0x00FF00FF) << 8) | ((bits & 0xFF00FF00) >> 8);
+    return vec2f(i / numSamples, bits / exp2(32));
+}
[C++ implementation of a Hammersley sequence generator]
+   

Precomputing L for image-based lighting

+

+

The term ( L_{DFG} ) is only dependent on ( \NoV ). Below, the normal is arbitrarily set to ( n=\left[0, 0, 1\right] ) and (v) is chosen to satisfy ( \NoV ). The vector ( h_i ) is the ( D_{GGX}(\alpha) ) important direction sample (i).

+

float GDFG(float NoV, float NoL, float a) {
+    float a2 = a * a;
+    float GGXL = NoV * sqrt((-NoL * a2 + NoL) * NoL + a2);
+    float GGXV = NoL * sqrt((-NoV * a2 + NoV) * NoV + a2);
+    return (2 * NoL) / (GGXV + GGXL);
+}
+
+float2 DFG(float NoV, float a) {
+    float3 V;
+    V.x = sqrt(1.0f - NoV*NoV);
+    V.y = 0.0f;
+    V.z = NoV;
+
+    float2 r = 0.0f;
+    for (uint i = 0; i < sampleCount; i++) {
+        float2 Xi = hammersley(i, sampleCount);
+        float3 H = importanceSampleGGX(Xi, a, N);
+        float3 L = 2.0f * dot(V, H) * H - V;
+
+        float VoH = saturate(dot(V, H));
+        float NoL = saturate(L.z);
+        float NoH = saturate(H.z);
+
+        if (NoL > 0.0f) {
+            float G = GDFG(NoV, NoL, a);
+            float Gv = G * VoH / NoH;
+            float Fc = pow(1 - VoH, 5.0f);
+            r.x += Gv * (1 - Fc);
+            r.y += Gv * Fc;
+        }
+    }
+    return r * (1.0f / sampleCount);
+}
[C++ implementation of the \( L_{DFG} \) term]
+   

Spherical Harmonics

+

+

+  + + + + +
Symbol Definition
\(K^m_l\) Normalization factors
\(P^m_l(x)\) Associated Legendre polynomials
\(y^m_l\) Spherical harmonics bases, or SH bases
\(L^m_l\) SH coefficients of the \(L(s)\) function defined on the unit sphere
Table 18: Spherical harmonics symbols definitions
+

+   

Basis functions

+

+

Spherical parameterization of points on the surface of the unit sphere:

+

+$$\begin{equation} +\{ x, y, z \} = \{ cos \phi sin \theta, sin \phi sin \theta, cos \theta \} +\end{equation}$$ +

+The complex spherical harmonics bases are given by: +

+$$\begin{equation} +Y^m_l(\theta, \phi) = K^m_l e^{im\theta} P^{|m|}_l(cos \theta), l \in N, -l <= m <= l +\end{equation}$$ +

+However we only need the real bases: +

+$$\begin{align*} +y^{m > 0}_l &= \sqrt{2} K^m_l cos(m \phi) P^m_l(cos \theta) \\ +y^{m < 0}_l &= \sqrt{2} K^m_l sin(|m| \phi) P^{|m|}_l(cos \theta) \\ +y^0_l &= K^0_l P^0_l(cos \theta) +\end{align*}$$ +

+The normalization factors are given by: +

+$$\begin{equation} +K^m_l = \sqrt{\frac{(2l + 1)(l - |m|)!}{4 \pi (l + |m|)!}} +\end{equation}$$ +

+The associated Legendre polynomials \(P^{|m|}_l\) can be calculated from the following recursions: +

+$$\begin{equation}\label{shRecursions} +P^0_0(x) = 1 \\ +P^0_1(x) = x \\ +P^l_l(x) = (-1)^l (2l - 1)!! (1 - x^2)^{\frac{l}{2}} \\ +P^m_l(x) = \frac{((2l - 1) x P^m_{l - 1} - (l + m - 1) P^m_{l - 2})}{l - m} \\ +\end{equation}$$ +

+Computing \(y^{|m|}_l\) requires to compute \(P^{|m|}_l(z)\) first. +This can be accomplished fairly easily using the recursions in equation \(\ref{shRecursions}\). +The third recursion can be used to “move diagonally” in table 20, i.e. calculating \(y^0_0\), \(y^1_1\), \(y^2_2\) etc. +Then, the fourth recursion can be used to move vertically. +

+  + + + +
Band index Basis functions \(-l <= m <= l\)
\(l = 0\) \(y^0_0\)
\(l = 1\) \(y^{-1}_1\) \(y^0_1\) \(y^1_1\)
\(l = 2\) \(y^{-2}_2\) \(y^{-1}_2\) \(y^0_2\) \(y^1_2\) \(y^2_2\)
Table 19: Basis functions per band
+

+It’s also fairly easy to compute the trigonometric terms recursively: +

+$$\begin{align*} +C_m &\equiv cos(m \phi)sin(\theta)^m \\ +S_m &\equiv sin(m \phi)sin(\theta)^m \\ +\{ x, y, z \} &= \{ cos \phi sin \theta, sin \phi sin \theta, cos \theta \} +\end{align*}$$ +

+Using the angle sum trigonometric identities: +

+$$\begin{align*} +cos(m \phi + \phi) &= cos(m \phi) cos(\phi) - sin(m \phi) sin(\phi) \Leftrightarrow C_{m + 1} = x C_m - y S_m \\ +sin(m \phi + \phi) &= sin(m \phi) cos(\phi) + cos(m \phi) sin(\phi) \Leftrightarrow S_{m + 1} = x S_m - y C_m +\end{align*}$$ +

+Listing 47 shows the C++ code to compute the non-normalized SH basis \(\frac{y^m_l(s)}{\sqrt{2} K^m_l}\): +

 
static inline size_t SHindex(ssize_t m, size_t l) {
+    return l * (l + 1) + m;
+}
+
+void computeShBasis(
+        double* const SHb,
+        size_t numBands,
+        const vec3& s)
+{
+    // handle m=0 separately, since it produces only one coefficient
+    double Pml_2 = 0;
+    double Pml_1 = 1;
+    SHb[0] =  Pml_1;
+    for (ssize_t l = 1; l < numBands; l++) {
+        double Pml = ((2 * l - 1) * Pml_1 * s.z - (l - 1) * Pml_2) / l;
+        Pml_2 = Pml_1;
+        Pml_1 = Pml;
+        SHb[SHindex(0, l)] = Pml;
+    }
+    double Pmm = 1;
+    for (ssize_t m = 1; m < numBands ; m++) {
+        Pmm = (1 - 2 * m) * Pmm;
+        double Pml_2 = Pmm;
+        double Pml_1 = (2 * m + 1)*Pmm*s.z;
+        // l == m
+        SHb[SHindex(-m, m)] = Pml_2;
+        SHb[SHindex( m, m)] = Pml_2;
+        if (m + 1 < numBands) {
+            // l == m+1
+            SHb[SHindex(-m, m + 1)] = Pml_1;
+            SHb[SHindex( m, m + 1)] = Pml_1;
+            for (ssize_t l = m + 2; l < numBands; l++) {
+                double Pml = ((2 * l - 1) * Pml_1 * s.z - (l + m - 1) * Pml_2)
+                        / (l - m);
+                Pml_2 = Pml_1;
+                Pml_1 = Pml;
+                SHb[SHindex(-m, l)] = Pml;
+                SHb[SHindex( m, l)] = Pml;
+            }
+        }
+    }
+    double Cm = s.x;
+    double Sm = s.y;
+    for (ssize_t m = 1; m <= numBands ; m++) {
+        for (ssize_t l = m; l < numBands ; l++) {
+            SHb[SHindex(-m, l)] *= Sm;
+            SHb[SHindex( m, l)] *= Cm;
+        }
+        double Cm1 = Cm * s.x - Sm * s.y;
+        double Sm1 = Sm * s.x + Cm * s.y;
+        Cm = Cm1;
+        Sm = Sm1;
+    }
+}
Listing 47: C++ implementation to compute a non-normalized SH basis
+

+

Normalized SH basis functions (y^m_l(s)) for the first 3 bands:

+
+  + + + +
Band \(m = -2\) \(m = -1\) \(m = 0\) \(m = 1\) \(m = 2\)
\(l = 0\) \(\frac{1}{2}\sqrt{\frac{1}{\pi}}\)
\(l = 1\) \(-\frac{1}{2}\sqrt{\frac{3}{\pi}}y\) \(\frac{1}{2}\sqrt{\frac{3}{\pi}}z\) \(-\frac{1}{2}\sqrt{\frac{3}{\pi}}x\)
\(l = 2\) \(\frac{1}{2}\sqrt{\frac{15}{\pi}}xy\) \(-\frac{1}{2}\sqrt{\frac{15}{\pi}}yz\) \(\frac{1}{4}\sqrt{\frac{5}{\pi}}(2z^2 - x^2 - y^2)\) \(-\frac{1}{2}\sqrt{\frac{15}{\pi}}xz\) \(\frac{1}{4}\sqrt{\frac{15}{\pi}}(x^2 - y^2)\)
Table 20: Normalized basis functions per band
+

+   

Decomposition and reconstruction

+

+

A function (L(s)) defined on a sphere is projected to the SH basis as follows:

+

+$$\begin{equation} +L^m_l = \int_\Omega L(s) y^m_l(s) ds \\ +L^m_l = \int_{\theta = 0}^{\pi} \int_{\phi = 0}^{2\pi} L(\theta, \phi) y^m_l(\theta, \phi) sin \theta d\theta d\phi +\end{equation}$$ +

+Note that each \(L^m_l\) is a vector of 3 values, one for each RGB color channel. +

+The inverse transformation, or reconstruction, or rendering, from the SH coefficients is given by: +

+$$\begin{equation} +\hat{L}(s) = \sum_l \sum_{m = -l}^l L^m_l y^m_l(s) +\end{equation}$$ +

+   

Decomposition of \(\left< cos \theta \right>\)

+

+

Since (\left< cos \theta \right>) does not depend on (\phi) (azimuthal independence), the integral simplifies to:

+

+$$\begin{align*} +C^0_l &= 2\pi \int_0^{\pi} \left< cos \theta \right> y^0_l(\theta) sin \theta d\theta \\ +C^0_l &= 2\pi K^m_l \int_0^{\frac{\pi}{2}} P^0_l(cos \theta) cos \theta sin \theta d\theta \\ +C^m_l &= 0, m != 0 +\end{align*}$$ +

+In [Ramamoorthi01] an analytical solution to the integral is described: +

+$$\begin{align*} +C_1 &= \sqrt{\frac{\pi}{3}} \\ +C_{odd} &= 0 \\ +C_{l, even} &= 2\pi \sqrt{\frac{2l + 1}{4\pi}} \frac{(-1)^{\frac{l}{2} - 1}}{(l + 2)(l - 1)} \frac{l!}{2^l (\frac{l!}{2})^2} +\end{align*}$$ +

+The first few coefficients are: +

+$$\begin{align*} +C_0 &= +0.88623 \\ +C_1 &= +1.02333 \\ +C_2 &= +0.49542 \\ +C_3 &= +0.00000 \\ +C_4 &= -0.11078 +\end{align*}$$ +

+Very few coefficients are needed to reasonably approximate \(\left< cos \theta \right>\), as shown in figure 91. +

+

 
Figure 91: Approximation of \(cos \theta\) with SH coefficients
+

+   

Convolution

+

+

Convolutions by a kernel (h) that has a circular symmetry can be applied directly and easily in SH space:

+

+$$\begin{equation} +(h * f)^m_l = \sqrt{\frac{4\pi}{2l + 1}} h^0_l(s) f^m_l(s) +\end{equation}$$ +

+Conveniently, \(\sqrt{\frac{4\pi}{2l + 1}} = \frac{1}{K^0_l}\), so in practice we pre-multiply \(C_l\) by \(\frac{1}{K^0_l}\) and we get a simpler expression: +

+$$\begin{equation} +\hat{C}_{l, even} = 2\pi \frac{(-1)^{\frac{l}{2} - 1}}{(l + 2)(l - 1)} \frac{l!}{2^l (\frac{l!}{2})^2} \\ +\hat{C}_1 = \frac{2\pi}{3} +\end{equation}$$ +

+Here is the C++ code to compute \(\hat{C}_l\): +

static double factorial(size_t n, size_t d = 1);
+
+// < cos(theta) > SH coefficients pre-multiplied by 1 / K(0,l)
+double computeTruncatedCosSh(size_t l) {
+    if (l == 0) {
+        return M_PI;
+    } else if (l == 1) {
+        return 2 * M_PI / 3;
+    } else if (l & 1) {
+        return 0;
+    }
+    const size_t l_2 = l / 2;
+    double A0 = ((l_2 & 1) ? 1.0 : -1.0) / ((l + 2) * (l - 1));
+    double A1 = factorial(l, l_2) / (factorial(l_2) * (1 << l));
+    return 2 * M_PI * A0 * A1;
+}
+
+// returns n! / d!
+double factorial(size_t n, size_t d ) {
+   d = std::max(size_t(1), d);
+   n = std::max(size_t(1), n);
+   double r = 1.0;
+   if (n == d) {
+       // intentionally left blank
+   } else if (n > d) {
+       for ( ; n>d ; n--) {
+           r *= n;
+       }
+   } else {
+       for ( ; d>n ; d--) {
+           r *= d;
+       }
+       r = 1.0 / r;
+   }
+   return r;
+}
+   

Sample validation scene for Mitsuba

+
<scene version="0.5.0">
+    <integrator type="path"/>
+
+    <shape type="serialized" id="sphere_mesh">
+        <string name="filename" value="plastic_sphere.serialized"/>
+        <integer name="shapeIndex" value="0"/>
+
+        <bsdf type="roughplastic">
+            <string name="distribution" value="ggx"/>
+            <float name="alpha" value="0.0"/>
+            <srgb name="diffuseReflectance" value="0.81, 0.0, 0.0"/>
+        </bsdf>
+    </shape>
+
+    <emitter type="envmap">
+        <string name="filename" value="../../environments/office/office.exr"/>
+        <float name="scale" value="35000.0" />
+        <boolean name="cache" value="false" />
+    </emitter>
+
+    <emitter type="directional">
+        <vector name="direction" x="-1" y="-1" z="1" />
+        <rgb name="irradiance" value="120000.0, 115200.0, 114000.0" />
+    </emitter>
+
+    <sensor type="perspective">
+        <float name="farClip" value="12.0"/>
+        <float name="focusDistance" value="4.1"/>
+        <float name="fov" value="45"/>
+        <string name="fovAxis" value="y"/>
+        <float name="nearClip" value="0.01"/>
+        <transform name="toWorld">
+
+            <lookat target="0, 0, 0" origin="0, 0, -3.1" up="0, 1, 0"/>
+        </transform>
+
+        <sampler type="ldsampler">
+            <integer name="sampleCount" value="256"/>
+        </sampler>
+
+        <film type="ldrfilm">
+            <integer name="height" value="1440"/>
+            <integer name="width" value="2048"/>
+            <float name="exposure" value="-15.23" />
+            <rfilter type="gaussian"/>
+        </film>
+    </sensor>
+</scene>
+   

Light assignment with froxels

+

+

Assigning lights to froxels can be implemented on the GPU using two compute shaders. The first one, shown in listing 48, creates the froxels data (4 planes + a min Z and max Z per froxel) in an SSBO and needs to be run only once. The shader requires the following uniforms:

+

+

Projection matrix

The projection matrix used to render the scene (view space to clip space transformation). +

Inverse projection matrix

The inverse of the projection matrix used to render the scene (clip space to view space transformation). +

Depth parameters

\(-log2(\frac{z_{lighnear}}{z_{far}}) \frac{1}{maxSlices-1}\), maximum number of depth slices, Z near and Z far. +

Clip space size

\(\frac{F_x \times F_r}{w} \times 2\), with \(F_x\) the number of tiles on the X axis, \(F_r\) the resolution in pixels of a tile and w the width in pixels of the render target. +

 
#version 310 es
+
+precision highp float;
+precision highp int;
+
+
+#define FROXEL_RESOLUTION 80u
+
+layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
+
+layout(location = 0) uniform mat4 projectionMatrix;
+layout(location = 1) uniform mat4 projectionInverseMatrix;
+layout(location = 2) uniform vec4 depthParams; // index scale, index bias, near, far
+layout(location = 3) uniform float clipSpaceSize;
+
+struct Froxel {
+    // NOTE: the planes should be stored in vec4[4] but the
+    // Adreno shader compiler has a bug that causes the data
+    // to not be read properly inside the loop
+    vec4 plane0;
+    vec4 plane1;
+    vec4 plane2;
+    vec4 plane3;
+    vec2 minMaxZ;
+};
+
+layout(binding = 0, std140) writeonly restrict buffer FroxelBuffer {
+    Froxel data[];
+} froxels;
+
+shared vec4 corners[4];
+shared vec2 minMaxZ;
+
+vec4 projectionToView(vec4 p) {
+    p = projectionInverseMatrix * p;
+    return p / p.w;
+}
+
+vec4 createPlane(vec4 b, vec4 c) {
+    // standard plane equation, with a at (0, 0, 0)
+    return vec4(normalize(cross(c.xyz, b.xyz)), 1.0);
+}
+
+void main() {
+    uint index = gl_WorkGroupID.x + gl_WorkGroupID.y * gl_NumWorkGroups.x +
+            gl_WorkGroupID.z * gl_NumWorkGroups.x * gl_NumWorkGroups.y;
+
+    if (gl_LocalInvocationIndex == 0u) {
+        // first tile the screen and build the frustum for the current tile
+        vec2 renderTargetSize = vec2(FROXEL_RESOLUTION * gl_NumWorkGroups.xy);
+        vec2 frustumMin = vec2(FROXEL_RESOLUTION * gl_WorkGroupID.xy);
+        vec2 frustumMax = vec2(FROXEL_RESOLUTION * (gl_WorkGroupID.xy + 1u));
+
+        corners[0] = vec4(
+            frustumMin.x / renderTargetSize.x * clipSpaceSize - 1.0,
+            (renderTargetSize.y - frustumMin.y) / renderTargetSize.y
+       * clipSpaceSize - 1.0,
+            1.0,
+            1.0
+        );
+        corners[1] = vec4(
+            frustumMax.x / renderTargetSize.x * clipSpaceSize - 1.0,
+            (renderTargetSize.y - frustumMin.y) / renderTargetSize.y
+       * clipSpaceSize - 1.0,
+            1.0,
+            1.0
+        );
+        corners[2] = vec4(
+            frustumMax.x / renderTargetSize.x * clipSpaceSize - 1.0,
+            (renderTargetSize.y - frustumMax.y) / renderTargetSize.y
+       * clipSpaceSize - 1.0,
+            1.0,
+            1.0
+        );
+        corners[3] = vec4(
+            frustumMin.x / renderTargetSize.x * clipSpaceSize - 1.0,
+            (renderTargetSize.y - frustumMax.y) / renderTargetSize.y
+       * clipSpaceSize - 1.0,
+            1.0,
+            1.0
+        );
+
+        uint froxelSlice = gl_WorkGroupID.z;
+        minMaxZ = vec2(0.0, 0.0);
+        if (froxelSlice > 0u) {
+            minMaxZ.x = exp2((float(froxelSlice) - depthParams.y) * depthParams.x)
+                    * depthParams.w;
+        }
+        minMaxZ.y = exp2((float(froxelSlice + 1u) - depthParams.y) * depthParams.x)
+                * depthParams.w;
+    }
+
+    if (gl_LocalInvocationIndex == 0u) {
+        vec4 frustum[4];
+        frustum[0] = projectionToView(corners[0]);
+        frustum[1] = projectionToView(corners[1]);
+        frustum[2] = projectionToView(corners[2]);
+        frustum[3] = projectionToView(corners[3]);
+
+        froxels.data[index].plane0 = createPlane(frustum[0], frustum[1]);
+        froxels.data[index].plane1 = createPlane(frustum[1], frustum[2]);
+        froxels.data[index].plane2 = createPlane(frustum[2], frustum[3]);
+        froxels.data[index].plane3 = createPlane(frustum[3], frustum[0]);
+        froxels.data[index].minMaxZ = minMaxZ;
+    }
+}
Listing 48: GLSL implementation of froxels data generation (compute shader)
+

+

The second compute shader, shown in listing 49, runs every frame (if the camera and/or lights have changed) and assigns all the lights to their respective froxels. This shader relies only on a couple of uniforms (the number of point/spot lights and the view matrix) and four SSBOs:

+

+

Light index buffer

For each froxel, the index of each light that affects said froxel. The indices for point lights are written first and if there is enough space left, the indices for spot lights are written as well. A sentinel of value 0×7fffffffu separates point and spot lights and/or marks the end of the froxel's list of lights. Each froxel has a maximum number of lights (point + spot). +

Point lights buffer

Array of structures describing the scene's point lights. +

Spot lights buffer

Array of structures describing the scene's spot lights. +

Froxels buffer

The list of froxels represented by planes, created by the previous compute shader. +

 
#version 310 es
+precision highp float;
+precision highp int;
+
+#define LIGHT_BUFFER_SENTINEL 0x7fffffffu
+#define MAX_FROXEL_LIGHT_COUNT 32u
+
+#define THREADS_PER_FROXEL_X 8u
+#define THREADS_PER_FROXEL_Y 8u
+#define THREADS_PER_FROXEL_Z 1u
+#define THREADS_PER_FROXEL (THREADS_PER_FROXEL_X * \
+        THREADS_PER_FROXEL_Y * THREADS_PER_FROXEL_Z)
+
+layout(local_size_x = THREADS_PER_FROXEL_X,
+       local_size_y = THREADS_PER_FROXEL_Y,
+       local_size_z = THREADS_PER_FROXEL_Z) in;
+
+// x = point lights, y = spot lights
+layout(location = 0) uniform uvec2 totalLightCount;
+layout(location = 1) uniform mat4 viewMatrix;
+
+layout(binding = 0, packed) writeonly restrict buffer LightIndexBuffer {
+    uint index[];
+} lightIndexBuffer;
+
+struct PointLight {
+    vec4 positionFalloff; // x, y, z, falloff
+    vec4 colorIntensity;  // r, g, b, intensity
+    vec4 directionIES;    // dir x, dir y, dir z, IES profile index
+};
+
+layout(binding = 1, std140) readonly restrict buffer PointLightBuffer {
+    PointLight lights[];
+} pointLights;
+
+struct SpotLight {
+    vec4 positionFalloff; // x, y, z, falloff
+    vec4 colorIntensity;  // r, g, b, intensity
+    vec4 directionIES;    // dir x, dir y, dir z, IES profile index
+    vec4 angle;           // angle scale, angle offset, unused, unused
+};
+
+layout(binding = 2, std140) readonly restrict buffer SpotLightBuffer {
+    SpotLight lights[];
+} spotLights;
+
+struct Froxel {
+    // NOTE: the planes should be stored in vec4[4] but the
+    // Adreno shader compiler has a bug that causes the data
+    // to not be read properly inside the loop
+    vec4 plane0;
+    vec4 plane1;
+    vec4 plane2;
+    vec4 plane3;
+    vec2 minMaxZ;
+};
+
+layout(binding = 3, std140) readonly restrict buffer FroxelBuffer {
+    Froxel data[];
+} froxels;
+
+shared uint groupLightCounter;
+shared uint groupLightIndexBuffer[MAX_FROXEL_LIGHT_COUNT];
+
+float signedDistanceFromPlane(vec4 p, vec4 plane) {
+    // plane.w == 0.0, simplify computation
+    return dot(plane.xyz, p.xyz);
+}
+
+void synchronize() {
+    memoryBarrierShared();
+    barrier();
+}
+
+void main() {
+    if (gl_LocalInvocationIndex == 0u) {
+        groupLightCounter = 0u;
+    }
+    memoryBarrierShared();
+
+    uint froxelIndex = gl_WorkGroupID.x + gl_WorkGroupID.y * gl_NumWorkGroups.x +
+            gl_WorkGroupID.z * gl_NumWorkGroups.x * gl_NumWorkGroups.y;
+    Froxel current = froxels.data[froxelIndex];
+
+    uint offset = gl_LocalInvocationID.x +
+         gl_LocalInvocationID.y * THREADS_PER_FROXEL_X;
+    for (uint i = 0u; i < totalLightCount.x &&
+      groupLightCounter < MAX_FROXEL_LIGHT_COUNT &&
+            offset + i < totalLightCount.x; i += THREADS_PER_FROXEL) {
+
+        uint currentLight = offset + i;
+
+        vec4 center = pointLights.lights[currentLight].positionFalloff;
+        center.xyz = (viewMatrix * vec4(center.xyz, 1.0)).xyz;
+        float r = inversesqrt(center.w);
+
+        if (-center.z + r > current.minMaxZ.x &&
+                -center.z - r <= current.minMaxZ.y) {
+            if (signedDistanceFromPlane(center, current.plane0) < r &&
+                signedDistanceFromPlane(center, current.plane1) < r &&
+                signedDistanceFromPlane(center, current.plane2) < r &&
+                signedDistanceFromPlane(center, current.plane3) < r) {
+
+                uint index = atomicAdd(groupLightCounter, 1u);
+                groupLightIndexBuffer[index] = currentLight;
+            }
+        }
+    }
+
+    synchronize();
+
+    uint pointLightCount = groupLightCounter;
+    offset = froxelIndex * MAX_FROXEL_LIGHT_COUNT;
+
+    for (uint i = gl_LocalInvocationIndex; i < pointLightCount;
+            i += THREADS_PER_FROXEL) {
+        lightIndexBuffer.index[offset + i] = groupLightIndexBuffer[i];
+    }
+
+    if (gl_LocalInvocationIndex == 0u) {
+        if (pointLightCount < MAX_FROXEL_LIGHT_COUNT) {
+            lightIndexBuffer.index[offset + pointLightCount] = LIGHT_BUFFER_SENTINEL;
+        }
+    }
+}
Listing 49: GLSL implementation of assigning lights to froxels (compute shader)
+   

Revisions

+

+
 Friday
3 August 2018
First public version
+

+

 Tuesday
7 August 2018
Cloth model
+

+

    +
  • Added description of the “Charlie” NDF
+

+

 Thursday
9 August 2018
Lighting
+

+

    +
  • Added explanation about pre-exposed lights
+

+

 Wednesday
15 August 2018
Fresnel
+

+

    +
  • Added a description of the Fresnel effect in section 4.4.3
+

+

 Friday
17 August 2018
Specular color
+

+

    +
  • Added section 9.1 to explain how the base color of various metals is computed
+

+

 Tuesday
21 August 2018
Multiscattering
+

+

    +
  • Added section 4.7.2 on how to compensate for energy loss in single scattering BRDFs
+

+

 Wednesday
20 February 2019
Cloth shading
+

+

    +
  • Removed Fresnel term from the cloth BRDF +
  • +
  • Removed cloth DFG approximations, replaced with a new channel in the DFG LUT
+

+

+

+   

Bibliography

+

+

[ Ashdown98] Ian Ashdown. 1998. Parsing the IESNA LM-63 photometric data file. http://lumen.iee.put.poznan.pl/kw/iesna.txt +
[ Ashikhmin00] Michael Ashikhmin, Simon Premoze and Peter Shirley. A Microfacet-based BRDF Generator. SIGGRAPH '00 Proceedings, 65-74. +
[ Ashikhmin07] Michael Ashikhmin and Simon Premoze. 2007. Distribution-based BRDFs. +
[ Burley12] Brent Burley. 2012. Physically Based Shading at Disney. Physically Based Shading in Film and Game Production, ACM SIGGRAPH 2012 Courses. +
[ Estevez17] Alejandro Conty Estevez and Christopher Kulla. 2017. Production Friendly Microfacet Sheen BRDF. ACM SIGGRAPH 2017. +
[ Hammon17] Earl Hammon. 217. PBR Diffuse Lighting for GGX+Smith Microsurfaces. GDC 2017. +
[ Heitz14] Eric Heitz. 2014. Understanding the Masking-Shadowing Function +in Microfacet-Based BRDFs. Journal of Computer Graphics Techniques, 3 (2). +
[ Heitz16] Eric Heitz et al. 2016. Multiple-Scattering Microfacet BSDFs with the Smith Model. ACM SIGGRAPH 2016. +
[ Hill12] Colin Barré-Brisebois and Stephen Hill. 2012. Blending in Detail. http://blog.selfshadow.com/publications/blending-in-detail/ +
[ Karis13a] Brian Karis. 2013. Specular BRDF Reference. http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.html +
[ Karis13b] Brian Karis, 2013. Real Shading in Unreal Engine 4. https://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf +
[ Karis14] Brian Karis. 2014. Physically Based Shading on Mobile. https://www.unrealengine.com/blog/physically-based-shading-on-mobile +
[ Kelemen01] Csaba Kelemen et al. 2001. A Microfacet Based Coupled Specular-Matte BRDF Model with Importance Sampling. Eurographics Short Presentations. +
[ Krystek85] M. Krystek. 1985. An algorithm to calculate correlated color temperature. Color Research & Application, 10 (1), 38–40. +
[ Krivanek08] Jaroslave Krivànek and Mark Colbert. 2008. Real-time Shading with Filtered Importance Sampling. Eurographics Symposium on Rendering 2008, Volume 27, Number 4. +
[ Kulla17] Christopher Kulla and Alejandro Conty. 2017. Revisiting Physically Based Shading at Imageworks. ACM SIGGRAPH 2017 +
[ Lagarde14] Sébastien Lagarde and Charles de Rousiers. 2014. Moving Frostbite to PBR. Physically Based Shading in Theory and Practice, ACM SIGGRAPH 2014 Courses. +
[ Lagarde18] Sébastien Lagarde and Evgenii Golubev. 2018. The road toward unified rendering with Unity’s high definition rendering pipeline. Advances in Real-Time Rendering in Games, ACM SIGGRAPH 2018 Courses. +
[ Lazarov13] Dimitar Lazarov. 2013. Physically-Based Shading in Call of Duty: Black Ops. Physically Based Shading in Theory and Practice, ACM SIGGRAPH 2013 Courses. +
[ McAuley15] Stephen McAuley. 2015. Rendering the World of Far Cry 4. GDC 2015. +
[ McGuire10] Morgan McGuire. 2010. Ambient Occlusion Volumes. High Performance Graphics. +
[ Narkowicz14] Krzysztof Narkowicz. 2014. Analytical DFG Term for IBL. https://knarkowicz.wordpress.com/2014/12/27/analytical-dfg-term-for-ibl +
[ Neubelt13] David Neubelt and Matt Pettineo. 2013. Crafting a Next-Gen Material Pipeline for The Order: 1886. Physically Based Shading in Theory and Practice, ACM SIGGRAPH 2013 Courses. +
[ Oren94] Michael Oren and Shree K. Nayar. 1994. Generalization of lambert's reflectance model. SIGGRAPH, 239–246. ACM. +
[ Pattanaik00] Sumanta Pattanaik00 et al. 2000. Time-Dependent Visual Adaptation +For Fast Realistic Image Display. SIGGRAPH '00 Proceedings of the 27th annual conference on Computer graphics and interactive techniques, 47-54. +
[ Ramamoorthi01] Ravi Ramamoorthi and Pat Hanrahan. 2001. On the relationship between radiance and irradiance: determining the illumination from images of a convex Lambertian object. Journal of the Optical Society of America, Volume 18, Number 10, October 2001. +
[ Revie12] Donald Revie. 2012. Implementing Fur in Deferred Shading. GPU Pro 2, Chapter 2. +
[ Russell15] Jeff Russell. 2015. Horizon Occlusion for Normal Mapped Reflections. http://marmosetco.tumblr.com/post/81245981087 +
[ Schlick94] Christophe Schlick. 1994. An Inexpensive BRDF Model for Physically-Based Rendering. Computer Graphics Forum, 13 (3), 233–246. +
[ Walter07] Bruce Walter et al. 2007. Microfacet Models for Refraction through Rough Surfaces. Proceedings of the Eurographics Symposium on Rendering. +
+

formatted by Markdeep 1.18  
+
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/main/index.html b/docs/main/index.html new file mode 100644 index 0000000000..8e30868f50 --- /dev/null +++ b/docs/main/index.html @@ -0,0 +1,217 @@ + + + + + + Core Concepts - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Core Concepts

+
    +
  • Filament - High-level designs; Filament's PBR/math assumptions; implementation details.
  • +
  • Materials - A guide to Filament's material definition.
  • +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/main/materials.html b/docs/main/materials.html new file mode 100644 index 0000000000..dc82a12409 --- /dev/null +++ b/docs/main/materials.html @@ -0,0 +1,2657 @@ + + + + + + Materials - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

+ +

$$\newcommand{\n}{\hat{n}}\newcommand{\thetai}{\theta_\mathrm{i}}\newcommand{\thetao}{\theta_\mathrm{o}}\newcommand{\d}[1]{\mathrm{d}#1}\newcommand{\w}{\hat{\omega}}\newcommand{\wi}{\w_\mathrm{i}}\newcommand{\wo}{\w_\mathrm{o}}\newcommand{\wh}{\w_\mathrm{h}}\newcommand{\Li}{L_\mathrm{i}}\newcommand{\Lo}{L_\mathrm{o}}\newcommand{\Le}{L_\mathrm{e}}\newcommand{\Lr}{L_\mathrm{r}}\newcommand{\Lt}{L_\mathrm{t}}\newcommand{\O}{\mathrm{O}}\newcommand{\degrees}{{^{\large\circ}}}\newcommand{\T}{\mathsf{T}}\newcommand{\mathset}[1]{\mathbb{#1}}\newcommand{\Real}{\mathset{R}}\newcommand{\Integer}{\mathset{Z}}\newcommand{\Boolean}{\mathset{B}}\newcommand{\Complex}{\mathset{C}}\newcommand{\un}[1]{,\mathrm{#1}}$$ + +

Filament Materials Guide

Filament Materials Guide

+
+

+

+
Contents

(Top)
+About
+  1.1  Authors
+Overview
+  2.1  Core concepts
+Material models
+  3.1  Lit model
+    3.1.1  Base color
+    3.1.2  Metallic
+    3.1.3  Roughness
+    3.1.4  Non-metals
+    3.1.5  Metals
+    3.1.6  Refraction
+    3.1.7  Reflectance
+    3.1.8  Sheen color
+    3.1.9  Sheen roughness
+    3.1.10  Clear coat
+    3.1.11  Clear coat roughness
+    3.1.12  Anisotropy
+    3.1.13  Anisotropy direction
+    3.1.14  Ambient occlusion
+    3.1.15  Normal
+    3.1.16  Bent normal
+    3.1.17  Clear coat normal
+    3.1.18  Emissive
+    3.1.19  Post-lighting color
+    3.1.20  Index of refraction
+    3.1.21  Transmission
+    3.1.22  Absorption
+    3.1.23  Micro-thickness and thickness
+  3.2  Subsurface model
+    3.2.1  Thickness
+    3.2.2  Subsurface color
+    3.2.3  Subsurface power
+  3.3  Cloth model
+    3.3.1  Sheen color
+    3.3.2  Subsurface color
+  3.4  Unlit model
+  3.5  Specular glossiness
+Material definitions
+  4.1  Format
+    4.1.1  Differences with JSON
+    4.1.2  Example
+  4.2  Material block
+    4.2.1  General: name
+    4.2.2  General: featureLevel
+    4.2.3  General: shadingModel
+    4.2.4  General: parameters
+    4.2.5  General: constants
+    4.2.6  General: variantFilter
+    4.2.7  General: flipUV
+    4.2.8  General: linearFog
+    4.2.9  General: shadowFarAttenuation
+    4.2.10  General: quality
+    4.2.11  General: instanced
+    4.2.12  General: vertexDomainDeviceJittered
+    4.2.13  General: useDefaultDepthVariant
+    4.2.14  Vertex and attributes: requires
+    4.2.15  Vertex and attributes: variables
+    4.2.16  Vertex and attributes: vertexDomain
+    4.2.17  Vertex and attributes: interpolation
+    4.2.18  Blending and transparency: blending
+    4.2.19  Blending and transparency: blendFunction
+    4.2.20  Blending and transparency: postLightingBlending
+    4.2.21  Blending and transparency: transparency
+    4.2.22  Blending and transparency: maskThreshold
+    4.2.23  Blending and transparency: refractionMode
+    4.2.24  Blending and transparency: refractionType
+    4.2.25  Rasterization: culling
+    4.2.26  Rasterization: colorWrite
+    4.2.27  Rasterization: depthWrite
+    4.2.28  Rasterization: depthCulling
+    4.2.29  Rasterization: doubleSided
+    4.2.30  Rasterization: alphaToCoverage
+    4.2.31  Lighting: reflections
+    4.2.32  Lighting: shadowMultiplier
+    4.2.33  Lighting: transparentShadow
+    4.2.34  Lighting: clearCoatIorChange
+    4.2.35  Lighting: multiBounceAmbientOcclusion
+    4.2.36  Lighting: specularAmbientOcclusion
+    4.2.37  Anti-aliasing: specularAntiAliasing
+    4.2.38  Anti-aliasing: specularAntiAliasingVariance
+    4.2.39  Anti-aliasing: specularAntiAliasingThreshold
+    4.2.40  Shading: customSurfaceShading
+  4.3  Vertex block
+    4.3.1  Material vertex inputs
+    4.3.2  Custom vertex attributes
+  4.4  Fragment block
+    4.4.1  prepareMaterial function
+    4.4.2  Material fragment inputs
+    4.4.3  Custom surface shading
+  4.5  Shader public APIs
+    4.5.1  Types
+    4.5.2  Math
+    4.5.3  Matrices
+    4.5.4  Frame constants
+    4.5.5  Material globals
+    4.5.6  Vertex only
+    4.5.7  Fragment only
+Compiling materials
+  5.1  Shader validation
+  5.2  Flags
+    5.2.1  —platform
+    5.2.2  —api
+    5.2.3  —optimize-size
+    5.2.4  —reflect
+    5.2.5  —variant-filter
+Handling colors
+  6.1  Linear colors
+  6.2  Pre-multiplied alpha
+Sampler usage in Materials
+  7.1  Feature level 1 and 2
+  7.2  Feature level 3
+

   

About

+

+

This document is part of the Filament project. To report errors in this document please use the project's issue tracker.

+

+   

Authors

+

+

+

+   

Overview

+

+

Filament is a physically based rendering (PBR) engine for Android. Filament offers a customizable +material system that you can use to create both simple and complex materials. This document +describes all the features available to materials and how to create your own material.

+

+   

Core concepts

+

+

Material

A material defines the visual appearance of a surface. To completely describe and render a + surface, a material provides the following information: +

+

    +
  • Material model +
  • +
  • Set of use-controllable named parameters +
  • +
  • Raster state (blending mode, backface culling, etc.) +
  • +
  • Vertex shader code +
  • +
  • Fragment shader code
+

Material model

Also called shading model or lighting model, the material model defines the intrinsic + properties of a surface. These properties have a direct influence on the way lighting is + computed and therefore on the appearance of a surface. +

Material definition

A text file that describes all the information required by a material. This is the file that you + will directly author to create new materials. +

Material package

At runtime, materials are loaded from material packages compiled from material definitions + using the matc tool. A material package contains all the information required to describe a + material, and shaders generated for the target runtime platforms. This is necessary because + different platforms (Android, macOS, Linux, etc.) use different graphics APIs or different + variants of similar graphics APIs (OpenGL vs OpenGL ES for instance). +

Material instance

A material instance is a reference to a material and a set of values for the different values of + that material. Material instances are not covered in this document as they are created and + manipulated directly from code using Filament's APIs. +

+   

Material models

+

+

Filament materials can use one of the following material models:

+

+

    +
  • Lit (or standard) +
  • +
  • Subsurface +
  • +
  • Cloth +
  • +
  • Unlit +
  • +
  • Specular glossiness (legacy)
+

+   

Lit model

+

+

The lit model is Filament's standard material model. This physically-based shading model was +designed after to offer good interoperability with other common tools and engines such as Unity 5, +Unreal Engine 4, Substance Designer or Marmoset Toolbag.

+

+This material model can be used to describe many non-metallic surfaces (dielectrics) +or metallic surfaces (conductors). +

+The appearance of a material using the standard model is controlled using the properties described +in table 1. +

+

+  + + + + + + + + + + + + + + + + + + + + + + +
Property Definition
baseColor Diffuse albedo for non-metallic surfaces, and specular color for metallic surfaces
roughness Perceived smoothness (1.0) or roughness (0.0) of a surface. Smooth surfaces exhibit sharp reflections
metallic Whether a surface appears to be dielectric (0.0) or conductor (1.0). Often used as a binary value (0 or 1)
reflectance Fresnel reflectance at normal incidence for dielectric surfaces. This directly controls the strength of the reflections
ambientOcclusion Defines how much of the ambient light is accessible to a surface point. It is a per-pixel shadowing factor between 0.0 and 1.0
clearCoat Strength of the clear coat layer
clearCoatRoughness Perceived smoothness or roughness of the clear coat layer
clearCoatNormal A detail normal used to perturb the clear coat layer using bump mapping (normal mapping)
anisotropy Amount of anisotropy in either the tangent or bitangent direction
anisotropyDirection Local surface direction in tangent space
thickness Thickness of the solid volume of refractive objects
sheenColor Strength of the sheen layer
sheenRoughness Perceived smoothness or roughness of the sheen layer
emissive Additional diffuse albedo to simulate emissive surfaces (such as neons, etc.) This property is mostly useful in an HDR pipeline with a bloom pass
normal A detail normal used to perturb the surface using bump mapping (normal mapping)
postLightingColor Additional color that can be blended with the result of the lighting computations. See postLightingBlending
absorption Absorption factor for refractive objects
transmission Defines how much of the diffuse light of a dielectric is transmitted through the object, in other words this defines how transparent an object is
ior Index of refraction, either for refractive objects or as an alternative to reflectance
microThickness Thickness of the thin layer of refractive objects
bentNormal A normal pointing in the average unoccluded direction. Can be used to improve indirect lighting quality
shadowStrength Strength factor between 0 and 1 for all shadows received by this material
Table 1: Properties of the standard model
+

+The type and range of each property is described in table 2. +

+  + + + + + + + + + + + + + + + + + + + + + +
Property Type Range Note
baseColor float4 [0..1] Pre-multiplied linear RGB
metallic float [0..1] Should be 0 or 1
roughness float [0..1]  
reflectance float [0..1] Prefer values > 0.35
sheenColor float3 [0..1] Linear RGB
sheenRoughness float [0..1]  
clearCoat float [0..1] Should be 0 or 1
clearCoatRoughness float [0..1]  
anisotropy float [−1..1] Anisotropy is in the tangent direction when this value is positive
anisotropyDirection float3 [0..1] Linear RGB, encodes a direction vector in tangent space
ambientOcclusion float [0..1]  
normal float3 [0..1] Linear RGB, encodes a direction vector in tangent space
bentNormal float3 [0..1] Linear RGB, encodes a direction vector in tangent space
clearCoatNormal float3 [0..1] Linear RGB, encodes a direction vector in tangent space
emissive float4 rgb=[0..n], a=[0..1] Linear RGB intensity in nits, alpha encodes the exposure weight
postLightingColor float4 [0..1] Pre-multiplied linear RGB
ior float [1..n] Optional, usually deduced from the reflectance
transmission float [0..1]  
absorption float3 [0..n]  
microThickness float [0..n]  
thickness float [0..n]  
Table 2: Range and type of the standard model's properties
+

+

About linear RGB
+

+ Several material model properties expect RGB colors. Filament materials use RGB colors in linear + space and you must take proper care of supplying colors in that space. See the Linear colors section for more information.

+

+

About pre-multiplied RGB
+

+ Filament materials expect colors to use pre-multiplied alpha. See the Pre-multiplied alpha section for more information.

+

+

About absorption
+

+ The light attenuation through the material is defined as \(e^{-absorption \cdot distance}\), + and the distance depends on the thickness parameter. If thickness is not provided, then + the absorption parameter is used directly and the light attenuation through the material + becomes \(1 - absorption\). To obtain a certain color at a desired distance, the above + equation can be inverted such as \(absorption = -\frac{ln(color)}{distance}\).

+

+

About ior and reflectance
+

+ The index of refraction (IOR) and the reflectance represent the same physical attribute, + therefore they don't need to be both specified. Typically, only the reflectance is specified, + and the IOR is deduced automatically. When only the IOR is specified, the reflectance is then + deduced automatically. It is possible to specify both, in which case their values are kept + as-is, which can lead to physically impossible materials, however, this might be desirable + for artistic reasons.

+

+

About thickness and microThickness for refraction
+

+ thickness represents the thickness of solid objects in the direction of the normal, for + satisfactory results, this should be provided per fragment (e.g.: as a texture) or at least per + vertex. microThickness represent the thickness of the thin layer of an object, and can + generally be provided as a constant value. For example, a 1mm thin hollow sphere of radius 1m, + would have a thickness of 1 and a microThickness of 0.001. Currently thickness is not + used when refractionType is set to thin.

+

+   

Base color

+

+

The baseColor property defines the perceived color of an object (sometimes called albedo). The +effect of baseColor depends on the nature of the surface, controlled by the metallic property +explained in the Metallic section.

+

+

Non-metals (dielectrics)

Defines the diffuse color of the surface. Real-world values are typically found in the range + \([10..240]\) if the value is encoded between 0 and 255, or in the range \([0.04..0.94]\) between 0 + and 1. Several examples of base colors for non-metallic surfaces can be found in + table 3. +

+  + + + + + + + + +
Metal sRGB Hexadecimal Color
Coal 0.19, 0.19, 0.19 #323232
 
Rubber 0.21, 0.21, 0.21 #353535
 
Mud 0.33, 0.24, 0.19 #553d31
 
Wood 0.53, 0.36, 0.24 #875c3c
 
Vegetation 0.48, 0.51, 0.31 #7b824e
 
Brick 0.58, 0.49, 0.46 #947d75
 
Sand 0.69, 0.66, 0.52 #b1a884
 
Concrete 0.75, 0.75, 0.73 #c0bfbb
 
Table 3: baseColor for common non-metals
+

+

Metals (conductors)

Defines the specular color of the surface. Real-world values are typically found in the range + \([170..255]\) if the value is encoded between 0 and 255, or in the range \([0.66..1.0]\) between 0 and + 1. Several examples of base colors for metallic surfaces can be found in table 4. +

+  + + + + + + + + +
Metal sRGB Hexadecimal Color
Silver 0.97, 0.96, 0.91 #f7f4e8
 
Aluminum 0.91, 0.92, 0.92 #e8eaea
 
Titanium 0.76, 0.73, 0.69 #c1baaf
 
Iron 0.77, 0.78, 0.78 #c4c6c6
 
Platinum 0.83, 0.81, 0.78 #d3cec6
 
Gold 1.00, 0.85, 0.57 #ffd891
 
Brass 0.98, 0.90, 0.59 #f9e596
 
Copper 0.97, 0.74, 0.62 #f7bc9e
 
Table 4: baseColor for common metals
+

+   

Metallic

+

+

The metallic property defines whether the surface is a metallic (conductor) or a non-metallic +(dielectric) surface. This property should be used as a binary value, set to either 0 or 1. +Intermediate values are only truly useful to create transitions between different types of surfaces +when using textures.

+

+This property can dramatically change the appearance of a surface. Non-metallic surfaces have +chromatic diffuse reflection and achromatic specular reflection (reflected light does not change +color). Metallic surfaces do not have any diffuse reflection and chromatic specular reflection +(reflected light takes on the color of the surfaced as defined by baseColor). +

+The effect of metallic is shown in figure 1 (click on the image to see a +larger version). +

+

 
Figure 1: metallic varying from 0.0 +
+

+   

Roughness

+

+

The roughness property controls the perceived smoothness of the surface. When roughness is set +to 0, the surface is perfectly smooth and highly glossy. The rougher a surface is, the “blurrier” +the reflections are. This property is often called glossiness in other engines and tools, and is +simply the opposite of the roughness (roughness = 1 - glossiness).

+

+   

Non-metals

+

+

The effect of roughness on non-metallic surfaces is shown in figure 2 (click +on the image to see a larger version).

+

+

 
Figure 2: Dielectric roughness varying from 0.0 +
+

+   

Metals

+

+

The effect of roughness on metallic surfaces is shown in figure 3 +(click on the image to see a larger version).

+

+

 
Figure 3: Conductor roughness varying from 0.0 +
+

+   

Refraction

+

+

When refraction through an object is enabled (using a refractonType of thin or solid), the +roughness property will also affect the refractions, as shown in figure 4 (click on the image to see a larger version).

+

+

 
Figure 4: Refractive sphere with roughness varying from 0.0 +
+

+   

Reflectance

+

+

The reflectance property only affects non-metallic surfaces. This property can be used to control +the specular intensity and index of refraction of materials. This value is defined +between 0 and 1 and represents a remapping of a percentage of reflectance. For instance, the +default value of 0.5 corresponds to a reflectance of 4%. Values below 0.35 (2% reflectance) should +be avoided as no real-world materials have such low reflectance.

+

+The effect of reflectance on non-metallic surfaces is shown in figure 5 +(click on the image to see a larger version). +

+

 
Figure 5: reflectance varying from 0.0 (left) +
+

+Figure 6 shows common values and how they relate to the mapping function. +

+

 
Figure 6: Common reflectance values
+

+Table 5 describes acceptable reflectance values for various types of materials +(no real world material has a value under 2%). +

+

+  + + + + + + + + + + + +
Material Reflectance IOR Linear value
Water 2% 1.33 0.35
Fabric 4% to 5.6% 1.5 to 1.62 0.5 to 0.59
Common liquids 2% to 4% 1.33 to 1.5 0.35 to 0.5
Common gemstones 5% to 16% 1.58 to 2.33 0.56 to 1.0
Plastics, glass 4% to 5% 1.5 to 1.58 0.5 to 0.56
Other dielectric materials 2% to 5% 1.33 to 1.58 0.35 to 0.56
Eyes 2.5% 1.38 0.39
Skin 2.8% 1.4 0.42
Hair 4.6% 1.55 0.54
Teeth 5.8% 1.63 0.6
Default value 4% 1.5 0.5
Table 5: Reflectance of common materials
+

+Note that the reflectance property also defines the index of refraction of the surface. +When this property is defined it is not necessary to define the ior property. Setting +either of these properties will automatically compute the other property. It is possible +to specify both, in which case their values are kept as-is, which can lead to physically +impossible materials, however, this might be desirable for artistic reasons. +

+The reflectance property is designed as a normalized property in the range 0..1 which makes +it easy to define from a texture. +

+See section 3.1.20 for more information about the ior property and refractive +indices. +

+   

Sheen color

+

+

The sheen color controls the color appearance and strength of an optional sheen layer on top of the +base layer described by the properties above. The sheen layer always sits below the clear coat layer +if such a layer is present.

+

+The sheen layer can be used to represent cloth and fabric materials. Please refer to +section 3.3 for more information about cloth and fabric materials. +

+The effect of sheenColor is shown in figure 7 +(click on the image to see a larger version). +

+

 
Figure 7: Different sheen colors
+

+

If you do not need the other properties offered by the standard lit material model but want to + create a cloth-like or fabric-like appearance, it is more efficient to use the dedicated cloth + model described in section 3.3.
+

+   

Sheen roughness

+

+

The sheenRoughness property is similar to the roughness property but applies only to the +sheen layer.

+

+The effect of sheenRoughness on a rough metal is shown in figure 8 +(click on the image to see a larger version). In this picture, the base layer is a dark blue, with +metallic set to 0.0 and roughness set to 1.0. +

+

 
Figure 8: sheenRoughness varying from 0.0 +
+

+   

Clear coat

+

+

Multi-layer materials are fairly common, particularly materials with a thin translucent +layer over a base layer. Real world examples of such materials include car paints, soda cans, +lacquered wood and acrylic.

+

+The clearCoat property can be used to describe materials with two layers. The clear coat layer +will always be isotropic and dielectric. +

+

 
Figure 9: Comparison of a carbon-fiber material under the standard material model +
+

+The clearCoat property controls the strength of the clear coat layer. This should be treated as a +binary value, set to either 0 or 1. Intermediate values are useful to control transitions between +parts of the surface that have a clear coat layers and parts that don't. +

+The effect of clearCoat on a rough metal is shown in figure 10 +(click on the image to see a larger version). +

+

 
Figure 10: clearCoat varying from 0.0 +
+

+

The clear coat layer effectively doubles the cost of specular computations. Do not assign a + value, even 0.0, to the clear coat property if you don't need this second layer.
+

+

The clear coat layer is added on top of the sheen layer if present.
+

+   

Clear coat roughness

+

+

The clearCoatRoughness property is similar to the roughness property but applies only to the +clear coat layer.

+

+The effect of clearCoatRoughness on a rough metal is shown in figure 11 +(click on the image to see a larger version). +

+

 
Figure 11: clearCoatRoughness varying from 0.0 +
+

+   

Anisotropy

+

+

Many real-world materials, such as brushed metal, can only be replicated using an anisotropic +reflectance model. A material can be changed from the default isotropic model to an anisotropic +model by using the anisotropy property.

+

+

 
Figure 12: Comparison of isotropic material +
+

+The effect of anisotropy on a rough metal is shown in figure 13 +(click on the image to see a larger version). +

+

 
Figure 13: anisotropy varying from 0.0 +
+

+The figure 14 below shows how the direction of the anisotropic highlights can be +controlled by using either positive or negative values: positive values define anisotropy in the +tangent direction and negative values in the bitangent direction. +

+

 
Figure 14: Positive (left) vs negative +
+

+

The anisotropic material model is slightly more expensive than the standard material model. Do + not assign a value (even 0.0) to the anisotropy property if you don't need anisotropy.
+

+   

Anisotropy direction

+

+

The anisotropyDirection property defines the direction of the surface at a given point and thus +control the shape of the specular highlights. It is specified as vector of 3 values that usually +come from a texture, encoding the directions local to the surface in tangent space. Because the +direction is in tangent space, the Z component should be set to 0.

+

+The effect of anisotropyDirection on a metal is shown in figure 16 +(click on the image to see a larger version). +

+

 
Figure 15: Anisotropic metal rendered +
+

+The result shown in figure 16 was obtained using the direction map shown +in figure 16. +

+

 
Figure 16: Example of Lighting: specularAmbientOcclusiona direction map
+

+   

Ambient occlusion

+

+

The ambientOcclusion property defines how much of the ambient light is accessible to a surface +point. It is a per-pixel shadowing factor between 0.0 (fully shadowed) and 1.0 (fully lit). This +property only affects diffuse indirect lighting (image-based lighting), not direct lights such as +directional, point and spot lights, nor specular lighting.

+

+

 
Figure 17: Comparison of materials without diffuse ambient occlusion +
+

+   

Normal

+

+

The normal property defines the normal of the surface at a given point. It usually comes from a +normal map texture, which allows to vary the property per-pixel. The normal is supplied in tangent +space, which means that +Z points outside of the surface.

+

+For example, let's imagine that we want to render a piece of furniture covered in tufted leather. +Modeling the geometry to accurately represent the tufted pattern would require too many triangles +so we instead bake a high-poly mesh into a normal map. Once the base map is applied to a simplified +mesh, we get the result in figure 18. +

+Note that the normal property affects the base layer and not the clear coat layer. +

+

 
Figure 18: Low-poly mesh without normal mapping (left) +
+

+

Using a normal map increases the runtime cost of the material model.
+

+   

Bent normal

+

+

The bentNormal property defines the average unoccluded direction at a point on the surface. It is +used to improve the accuracy of indirect lighting. Bent normals can also improve the quality of +specular ambient occlusion (see section 4.2.36 about +specularAmbientOcclusion).

+

+Bent normals can greatly increase the visual fidelity of an asset with various cavities and concave +areas, as shown in figure 19. See the areas of the ears, nostrils and eyes for +instance. +

+

 
Figure 19: Example of a model rendered with and without a bent normal map. Both +
+

+   

Clear coat normal

+

+

The clearCoatNormal property defines the normal of the clear coat layer at a given point. It +behaves otherwise like the normal property.

+

+

 
Figure 20: A material with a clear coat normal +
+

+

Using a clear coat normal map increases the runtime cost of the material model.
+

+   

Emissive

+

+

The emissive property can be used to simulate additional light emitted by the surface. It is +defined as a float4 value that contains an RGB intensity in nits as well as an exposure +weight (in the alpha channel).

+

+The intensity in nits allows an emissive surface to function as a light and can be used to recreate +real world surfaces. For instance a computer display has an intensity between 200 and 1,000 nits. +

+If you prefer to work in EV (or f-stops), you can simplify multiply your emissive color by the +output of the API filament::Exposure::luminance(ev). This API returns the luminance in nits of +the specific EV. You can perform this conversion yourself using the following formula, where \(L\) +is the final intensity in nits: \( L = 2^{EV - 3} \). +

+The exposure weight carried in the alpha channel can be used to undo the camera exposure, and thus +force an emissive surface to bloom. When the exposure weight is set to 0, the emissive intensity is +not affected by the camera exposure. When the weight is set to 1, the intensity is multiplied by +the camera exposure like with any regular light. +

+   

Post-lighting color

+

+

The postLightingColor can be used to modify the surface color after lighting computations. This +property has no physical meaning and only exists to implement specific effects or to help with +debugging. This property is defined as a float4 value containing a pre-multiplied RGB color in +linear space.

+

+The post-lighting color is blended with the result of lighting according to the blending mode +specified by the postLightingBlending material option. Please refer to the documentation of +this option for more information. +

+

postLightingColor can be used as a simpler emissive property by setting + postLightingBlending to add and by providing an RGB color with alpha set to 0.0.
+

+   

Index of refraction

+

+

The ior property only affects non-metallic surfaces. This property can be used to control the +index of refraction and the specular intensity of materials. The ior property is intended to +be used with refractive (transmissive) materials, which are enabled when the refractionMode is +set to cubemap or screenspace. It can also be used on non-refractive objects as an alternative +to setting the reflectance.

+

+The index of refraction (or refractive index) of a material is a dimensionless number that describes +how fast light travels through that material. The higher the number, the slower light travels +through the medium. More importantly for rendering materials, the refractive index determines how +the path light travels is bent when entering the material. Higher indices of refraction will cause +light to bend further away from the initial path. +

+Table 6 describes acceptable refractive indices for various types of materials. +

+  + + + + + + +
Material IOR
Air 1.0
Water 1.33
Common liquids 1.33 to 1.5
Common gemstones 1.58 to 2.33
Plastics, glass 1.5 to 1.58
Other dielectric materials 1.33 to 1.58
Table 6: Index of refraction of common materials
+

+The appearance of a refractive material will greatly depend on the refractionType and +refractionMode settings of the material. Refer to section 4.2.24 and section 4.2.23 +for more information. +

+The effect of ior when refractionMode is set to cubemap and refractionType is set to solid +can be seen in figure 21 (click on the image to see a larger version). +

+

 
Figure 21: transmission varying from 1.0 +
+

+Figure 22 shows the comparison of a sphere of ior 1.0 with a sphere of ior 1.33, with +the refractionMode set to screenspace and the refractionType set to solid +(click on the image to see a larger version). +

+

 
Figure 22: ior of 1.0 (left) and 1.33 (right)
+

+Note that the ior property also defines the reflectance (or specular intensity) of the surface. +When this property is defined it is not necessary to define the reflectance property. Setting +either of these properties will automatically compute the other property. It is possible to specify +both, in which case their values are kept as-is, which can lead to physically impossible materials, +however, this might be desirable for artistic reasons. +

+See the Reflectance section for more information on the reflectance property. +

+

Refractive materials are affected by the roughness property. Rough materials will scatter + light, creating a diffusion effect useful to recreate “blurry” appearances such as frosted + glass, certain plastics, etc.
+

+   

Transmission

+

+

The transmission property defines what ratio of diffuse light is transmitted through a refractive +material. This property only affects materials with a refractionMode set to cubemap or +screenspace.

+

+When transmission is set to 0, no amount of light is transmitted and the diffuse component of +the surface is 100% visible. When transmission is set to 1, all the light is transmitted and the +diffuse component is not visible anymore, only the specular component is. +

+The effect of transmission on a glossy dielectric (ior of 1.5, refractionMode set to +cubemap, refractionType set to solid) is shown in figure 23 +(click on the image to see a larger version). +

+

 
Figure 23: transmission varying from 0.0 +
+

+

The transmission property is useful to create decals, paint, etc. at the surface of refractive + materials.
+

+   

Absorption

+

+

The absorption property defines the absorption coefficients of light transmitted through the +material. Figure 24 shows the effect of absorption on a refracting object with +an index of refraction of 1.5 and a base color set to white.

+

+

 
Figure 24: Refracting object without (left) +
+

+Transmittance through a volume is exponential with respect to the optical depth (defined either +with microThickness or thickness). The computed color follows the following formula: +

+$$color \cdot e^{-absorption \cdot distance}$$ +

+Where distance is either microThickness or thickness, that is the distance light will travel +through the material at a given point. If no thickness/distance is specified, the computed color +follows this formula instead: +

+$$color \cdot (1 - absorption)$$ +

+The effect of varying the absorption coefficients is shown in figure 25 +(click on the image to see a larger version). In this picture, the object has a fixed thickness +of 4.5 and an index of refraction set to 1.3. +

+

 
Figure 25: absorption varying from (0.0, 0.02, 0.14) +
+

+Setting the absorption coefficients directly can be unintuitive which is why we recommend working +with a transmittance color and a “at distance” factor instead. These two parameters allow an +artist to specify the precise color the material should have at a specified distance through the +volume. The value to pass to absorption can be computed this way: +

+$$absorption = -\frac{ln(transmittanceColor)}{atDistance}$$ +

+While this computation can be done in the material itself we recommend doing it offline whenever +possible. Filament provides an API for this purpose, Color::absorptionAtDistance(). +

+   

Micro-thickness and thickness

+

+

The microThickness and thickness properties define the optical depth of the material of a +refracting object. microThickness is used when refractionType is set to thin, and thickness +is used when refractionType is set to volume.

+

+thickness represents the thickness of solid objects in the direction of the normal, for +satisfactory results, this should be provided per fragment (e.g.: as a texture) or at least per +vertex. +

+microThickness represent the thickness of the thin layer (shell) of an object, and can generally +be provided as a constant value. For example, a 1mm thin hollow sphere of radius 1m, would have a +thickness of 1 and a microThickness of 0.001. Currently thickness is not used when +refractionType is set to thin. Both properties are made available for possible future use. +

+Both thickness and microThickness are used to compute the transmitted color of the material +when the absorption property is set. In solid volumes, thickness will also affect how light +rays are refracted. +

+The effect thickness in a solid volume with refractionMode set to screenSpace is shown in +figure 26 (click on the image to see a larger version). Note how the thickness +value not only changes the effect of absorption but also modifies the direction of the refracted +light. +

+

 
Figure 26: thickness varying from 0.0 +
+

+Figure 27 shows what a prism with spatially varying thickness looks like when +the refractionType is set to solid and absorption coefficients are set. +

+

 
Figure 27: thickness varying from 0.0 at the top of the prism to 3.0 at the +
+

+   

Subsurface model

+   

Thickness

+   

Subsurface color

+   

Subsurface power

+   

Cloth model

+

+

All the material models described previously are designed to simulate dense surfaces, both at a +macro and at a micro level. Clothes and fabrics are however often made of loosely connected threads +that absorb and scatter incident light. When compared to hard surfaces, cloth is characterized by +a softer specular lob with a large falloff and the presence of fuzz lighting, caused by +forward/backward scattering. Some fabrics also exhibit two-tone specular colors +(velvets for instance).

+

+Figure 28 shows how the standard material model fails to capture the appearance of a +sample of denim fabric. The surface appears rigid (almost plastic-like), more similar to a tarp +than a piece of clothing. This figure also shows how important the softer specular lobe caused by +absorption and scattering is to the faithful recreation of the fabric. +

+

 
Figure 28: Comparison of denim fabric rendered using the standard model +
+

+Velvet is an interesting use case for a cloth material model. As shown in figure 29 +this type of fabric exhibits strong rim lighting due to forward and backward scattering. These +scattering events are caused by fibers standing straight at the surface of the fabric. When the +incident light comes from the direction opposite to the view direction, the fibers will forward +scatter the light. Similarly, when the incident light from the same direction as the view +direction, the fibers will scatter the light backward. +

+

 
Figure 29: Velvet fabric showcasing forward and +
+

+It is important to note that there are types of fabrics that are still best modeled by hard surface +material models. For instance, leather, silk and satin can be recreated using the standard or +anisotropic material models. +

+The cloth material model encompasses all the parameters previously defined for the standard +material mode except for metallic and reflectance. Two extra parameters described in +table 7 are also available. +

+

+  + + +
Parameter Definition
sheenColor Specular tint to create two-tone specular fabrics (defaults to \(\sqrt{baseColor}\))
subsurfaceColor Tint for the diffuse color after scattering and absorption through the material
Table 7: Cloth model parameters
+

+The type and range of each property is described in table 8. +

+  + + +
Property Type Range Note
sheenColor float3 [0..1] Linear RGB
subsurfaceColor float3 [0..1] Linear RGB
Table 8: Range and type of the cloth model's properties
+

+To create a velvet-like material, the base color can be set to black (or a dark color). +Chromaticity information should instead be set on the sheen color. To create more common fabrics +such as denim, cotton, etc. use the base color for chromaticity and use the default sheen color +or set the sheen color to the luminance of the base color. +

+

To see the effect of the roughness parameter make sure the sheenColor is brighter than + baseColor. This can be used to create a fuzz effect. Taking the luminance of baseColor + as the sheenColor will produce a fairly natural effect that works for common cloth. A dark + baseColor combined with a bright/saturated sheenColor can be used to create velvet.
+

+

The subsurfaceColor parameter should be used with care. High values can interfere with shadows + in some areas. It is best suited for subtle transmission effects through the material.
+

+   

Sheen color

+

+

The sheenColor property can be used to directly modify the specular reflectance. It offers +better control over the appearance of cloth and gives give the ability to create +two-tone specular materials.

+

+The effect of sheenColor is shown in figure 30 +(click on the image to see a larger version). +

+

 
Figure 30: Blue fabric without (left) and with (right) sheen
+

+   

Subsurface color

+

+

The subsurfaceColor property is not physically-based and can be used to simulate the scattering, +partial absorption and re-emission of light in certain types of fabrics. This is particularly +useful to create softer fabrics.

+

+

The cloth material model is more expensive to compute when the subsurfaceColor property is used.
+

+The effect of subsurfaceColor is shown in figure 31 +(click on the image to see a larger version). +

+

 
Figure 31: White cloth (left column) vs white cloth with +
+

+   

Unlit model

+

+

The unlit material model can be used to turn off all lighting computations. Its primary purpose is +to render pre-lit elements such as a cubemap, external content (such as a video or camera stream), +user interfaces, visualization/debugging etc. The unlit model exposes only two properties described +in table 9.

+
+  + + + +
Property Definition
baseColor Surface diffuse color
emissive Additional diffuse color to simulate emissive surfaces. This property is mostly useful in an HDR pipeline with a bloom pass
postLightingColor Additional color to blend with base color and emissive
Table 9: Properties of the standard model
+

+The type and range of each property is described in table 10. +

+  + + + +
Property Type Range Note
baseColor float4 [0..1] Pre-multiplied linear RGB
emissive float4 rgb=[0..n], a=[0..1] Linear RGB intensity in nits, alpha encodes the exposure weight
postLightingColor float4 [0..1] Pre-multiplied linear RGB
Table 10: Range and type of the unlit model's properties
+

+The value of postLightingColor is blended with the sum of emissive and baseColor according to +the blending mode specified by the postLightingBlending material option. +

+Figure 32 shows an example of the unlit material model +(click on the image to see a larger version). +

+

 
Figure 32: The unlit model is used to render debug information
+

+   

Specular glossiness

+

+

This alternative lighting model exists to comply with legacy standards. Since it is not a +physically-based formulation, we do not recommend using it except when loading legacy assets.

+

+This model encompasses the parameters previously defined for the standard lit mode except for +metallic, reflectance, and roughness. It adds parameters for specularColor and glossiness. +

+  + + + +
Parameter Definition
baseColor Surface diffuse color
specularColor Specular tint (defaults to black)
glossiness Glossiness (defaults to 0.0)
Table 11: Properties of the specular-glossiness shading model
+

+The type and range of each property is described in table 12. +

+  + + + +
Property Type Range Note
baseColor float4 [0..1] Pre-multiplied linear RGB
specularColor float3 [0..1] Linear RGB
glossiness float [0..1] Inverse of roughness
Table 12: Range and type of the specular-glossiness model's properties
+

+   

Material definitions

+

+

A material definition is a text file that describes all the information required by a material:

+

+

    +
  • Name +
  • +
  • User parameters +
  • +
  • Material model +
  • +
  • Required attributes +
  • +
  • Interpolants (called variables) +
  • +
  • Raster state (blending mode, etc.) +
  • +
  • Shader code (fragment shader, optionally vertex shader)
+

+   

Format

+

+

The material definition format is a format loosely based on JSON that we +call JSONish. At the top level a material definition is composed of 3 different blocks that use +the JSON object notation:

+

material {
+    // material properties
+}
+
+vertex {
+    // vertex shader, optional
+}
+
+fragment {
+    // fragment shader
+}

+A minimum viable material definition must contain a material preamble and a fragment block. The +vertex block is optional. +

+   

Differences with JSON

+

+

In JSON, an object is made of key/value pairs. A JSON pair has the following syntax:

+

"key" : value

+Where value can be a string, number, object, array or a literal (true, false or null). While +this syntax is perfectly valid in a material definition, a variant without quotes around strings is +also accepted in JSONish: +

key : value

+Quotes remain mandatory when the string contains spaces. +

+The vertex and fragment blocks contain unescaped, unquoted GLSL code, which is not valid in JSON. +

+Single-line C++-style comments are allowed. +

+The key of a pair is case-sensitive. +

+The value of a pair is not case-sensitive. +

+   

Example

+

+

The following code listing shows an example of a valid material definition. This definition uses +the lit material model (see Lit model section), uses the default opaque blending mode, requires +that a set of UV coordinates be presented in the rendered mesh and defines 3 user parameters. The +following sections of this document describe the material and fragment blocks in detail.

+

material {
+    name : "Textured material",
+    parameters : [
+        {
+           type : sampler2d,
+           name : texture
+        },
+        {
+           type : float,
+           name : metallic
+        },
+        {
+            type : float,
+            name : roughness
+        }
+    ],
+    requires : [
+        uv0
+    ],
+    shadingModel : lit,
+    blending : opaque
+}
+
+fragment {
+    void material(inout MaterialInputs material) {
+        prepareMaterial(material);
+        material.baseColor = texture(materialParams_texture, getUV0());
+        material.metallic = materialParams.metallic;
+        material.roughness = materialParams.roughness;
+    }
+}
+   

Material block

+

+

The material block is mandatory block that contains a list of property pairs to describe all +non-shader data.

+

+   

General: name

+

+

Type

string +

Value

Any string. Double quotes are required if the name contains spaces. +

Description

Sets the name of the material. The name is retained at runtime for debugging purpose. +

material {
+    name : stone
+}
+
+material {
+    name : "Wet pavement"
+}
+   

General: featureLevel

+

+

Type

number +

Value

An integer value, either 1, 2 or 3. Defaults to 1. +

+  + + + +
Feature Level Guaranteed features
1 9 textures per material
2 9 textures per material, cubemap arrays, ESSL 3.10
3 12 textures per material, cubemap arrays, ESSL 3.10
Table 13: Feature levels
+

+

Description

Sets the feature level of the material. Each feature level defines a set of features the + material can use. If the material uses a feature not supported by the selected level, matc + will generate an error during compilation. A given feature level is guaranteed to support + all features of lower feature levels. +

material {
+    featureLevel : 2
+}

+

Bugs

matc doesn't verify that a material is not using features above its selected feature level. +

+   

General: shadingModel

+

+

Type

string +

Value

Any of lit, subsurface, cloth, unlit, specularGlossiness. Defaults to lit. +

Description

Selects the material model as described in the Material models section. +

material {
+    shadingModel : unlit
+}
+
+material {
+    shadingModel : "subsurface"
+}
+   

General: parameters

+

+

Type

array of parameter objects +

Value

Each entry is an object with the properties name and type, both of string type. The + name must be a valid GLSL identifier. Entries have an optional precision, which can be + one of default (best precision for the platform, typically high on desktop, medium on + mobile), low, medium, high. The type must be one of the types described in + table 14. For Android external textures, entries also have an optional + transformName parameter to specify the name of the material parameter that will be + used to expose the transform matrix associated with the external sampler. In iOS and Vulkan, + this will always be identity. +

+  + + + + + + + + + + + + + + + + + + + + + + +
Type Description
bool Single boolean
bool2 Vector of 2 booleans
bool3 Vector of 3 booleans
bool4 Vector of 4 booleans
float Single float
float2 Vector of 2 floats
float3 Vector of 3 floats
float4 Vector of 4 floats
int Single integer
int2 Vector of 2 integers
int3 Vector of 3 integers
int4 Vector of 4 integers
uint Single unsigned integer
uint2 Vector of 2 unsigned integers
uint3 Vector of 3 unsigned integers
uint4 Vector of 4 unsigned integers
float3×3 Matrix of 3×3 floats
float4×4 Matrix of 4×4 floats
sampler2d 2D texture
sampler2dArray Array of 2D textures
samplerExternal External texture (platform-specific)
samplerCubemap Cubemap texture
Table 14: Material parameter types
+

+

Samplers

Sampler types can have the following fields: +

+

    +
  • format : which can be either int or float (defaults to float). +
  • +
  • multisample : a boolean to indicate whether the sampler is meant for multisampling (defaults to false) +
  • +
  • filterable : a boolean to indicate whether the sampling is filterable +
      +
    • When the format is int, filterable is assumed to be false, and setting this attribute is not allowed. +
    • +
    • When the format is float, the default of filterable is true. The client must explicitly + set it to false if they wish for unfiltered sampling.
+

Arrays

A parameter can define an array of values by appending [size] after the type name, where + size is a positive integer. For instance: float[9] declares an array of nine float + values. This syntax does not apply to samplers as arrays are treated as separate types. +

Description

Lists the parameters required by your material. These parameters can be set at runtime using + Filament's material API. Accessing parameters from the shaders varies depending on the type of + parameter: +

+

    +
  • Samplers types: use the parameter name prefixed with materialParams_. For instance, + materialParams_myTexture. +
  • +
  • Other types: use the parameter name as the field of a structure called materialParams. + For instance, materialParams.myColor.
+

material {
+    parameters : [
+        {
+           type : float4,
+           name : albedo
+        },
+        {
+           type      : sampler2d,
+           format    : float,
+           precision : high,
+           name      : roughness
+        },
+        {
+            type : float2,
+            name : metallicReflectance
+        }
+    ],
+    requires : [
+        uv0
+    ],
+    shadingModel : lit,
+}
+
+fragment {
+    void material(inout MaterialInputs material) {
+        prepareMaterial(material);
+        material.baseColor = materialParams.albedo;
+        material.roughness = texture(materialParams_roughness, getUV0());
+        material.metallic = materialParams.metallicReflectance.x;
+        material.reflectance = materialParams.metallicReflectance.y;
+    }
+}
+   

General: constants

+

+

Type

array of constant objects +

Value

Each entry is an object with the properties name and type, both of string type. The name + must be a valid GLSL identifier. Entries also have an optional default, which can either be a + bool or number, depending on the type of the constant. The type must be one of the types + described in table 15. +

+  + + + +
Type Description Default
int A signed, 32 bit GLSL int 0
float A single-precision GLSL float 0.0
bool A GLSL bool false
Table 15: Material constants types
+

+

Description

Lists the constant parameters accepted by your material. These constants can be set, or + “specialized”, at runtime when loading a material package. Multiple materials can be loaded from + the same material package with differing constant parameter specializations. Once a material is + loaded from a material package, its constant parameters cannot be changed. Compared to regular + parameters, constant parameters allow the compiler to generate more efficient code. Access + constant parameters from the shader by prefixing the name with materialConstant_. For example, + a constant parameter named myConstant is accessed in the shader as + materialConstant_myConstant. If a constant parameter is not set at runtime, the default is + used. +

material {
+    constants : [
+        {
+           name : overrideAlpha,
+           type : bool
+        },
+        {
+           name : customAlpha,
+           type : float,
+           default : 0.5
+        }
+    ],
+    shadingModel : lit,
+    blending : transparent,
+}
+
+fragment {
+    void material(inout MaterialInputs material) {
+        prepareMaterial(material);
+        if (materialConstants_overrideAlpha) {
+            material.baseColor.a = materialConstants_customAlpha;
+            material.baseColor.rgb *= material.baseColor.a;
+        }
+    }
+}
+   

General: variantFilter

+

+

Type

array of string +

Value

Each entry must be any of dynamicLighting, directionalLighting, shadowReceiver, + skinning, ssr, or stereo. +

Description

Used to specify a list of shader variants that the application guarantees will never be + needed. These shader variants are skipped during the code generation phase, thus reducing + the overall size of the material. + Note that some variants may automatically be filtered out. For instance, all lighting related + variants (directionalLighting, etc.) are filtered out when compiling an unlit material. + Use the variant filter with caution, filtering out a variant required at runtime may lead + to crashes. +

Description of the variants: +

+

    +
  • directionalLighting, used when a directional light is present in the scene +
  • +
  • dynamicLighting, used when a non-directional light (point, spot, etc.) is present in the scene +
  • +
  • shadowReceiver, used when an object can receive shadows +
  • +
  • skinning, used when an object is animated using GPU skinning +
  • +
  • fog, used when global fog is applied to the scene +
  • +
  • vsm, used when VSM shadows are enabled and the object is a shadow receiver +
  • +
  • ssr, used when screen-space reflections are enabled in the View +
  • +
  • stereo, used when stereoscopic rendering is enabled in the View
+

material {
+    name : "Invisible shadow plane",
+    shadingModel : unlit,
+    shadowMultiplier : true,
+    blending : transparent,
+    variantFilter : [ skinning ]
+}
+   

General: flipUV

+

+

Type

boolean +

Value

true or false. Defaults to true. +

Description

When set to true (default value), the Y coordinate of UV attributes will be flipped when + read by this material's vertex shader. Flipping is equivalent to y = 1.0 - y. When set + to false, flipping is disabled and the UV attributes are read as is. +

material {
+    flipUV : false
+}
+   

General: linearFog

+

+

Type

boolean +

Value

true or false. Defaults to false. +

Description

When set to true, a simplified fog equation is used for large-scale fog calculations. In this mode, + in-scattering is ignored as well as height falloff. +

material {
+    linearFog : true
+}
+   

General: shadowFarAttenuation

+

+

Type

boolean +

Value

true or false. Defaults to true. +

Description

When set to false, the directional light shadow is no longer attenuated at near the far plane. +

material {
+    shadowFarAttenuation : true
+}
+   

General: quality

+

+

Type

string +

Value

Any of low, normal, high, default. Defaults to default. +

Description

Set some global quality parameters of the material. low enables optimizations that can + slightly affect correctness and is the default on mobile platforms. normal does not affect + correctness and is otherwise similar to low. high enables quality settings that can + adversely affect performance and is the default on desktop platforms. +

material {
+    quality : default
+}
+   

General: instanced

+

+

Type

boolean +

Value

true or false. Defaults to false. +

Description

Allows a material to access the instance index (i.e.: gl_InstanceIndex) of instanced + primitives using getInstanceIndex() in the material's shader code. Never use + gl_InstanceIndex directly. This is typically used with + RenderableManager::Builder::instances(). getInstanceIndex() is available in both the + vertex and fragment shader. +

material {
+    instanced : true
+}
+   

General: vertexDomainDeviceJittered

+

+

Type

boolean +

Value

true or false. Defaults to false. +

Description

Only meaningful for vertexDomain:Device materials, this parameter specifies whether the + filament clip-space transforms need to be applied or not, which affects TAA and guard bands. + Generally it needs to be applied because by definition vertexDomain:Device materials + vertices are not transformed and used as is. + However, if the vertex shader uses for instance getViewFromClipMatrix() (or other + matrices based on the projection), the clip-space transform is already applied. + Setting this parameter incorrectly can prevent TAA or the guard bands to work correctly. +

material {
+    vertexDomainDeviceJittered : true
+}
+   

General: useDefaultDepthVariant

+

+

Type

boolean +

Value

true or false. Defaults to false. +

Description

This parameter forces Filament to use its default variant for depth passes, such as those used + in shadow rendering. This provides an optimization for materials with expensive custom vertex + shaders. For example, custom vertex shader computations intended to be consumed by the fragment + stage can be skipped during the depth-only pass. This parameter is only meaningful if the + material has a vertex block. + This parameter should not be set to true for vertex blocks that modify geometry (i.e., + modifying worldPosition), otherwise shadows may render incorrectly. +

material {
+    variables : [
+        customColor
+    ],
+    useDefaultDepthVariant : true
+}
+
+vertex {
+    void materialVertex(inout MaterialVertexInputs material) {
+        material.customColor = /* expensive computation that can be skipped for depth-only passes */
+    }
+}
+
+fragment {
+    void material(inout MaterialInputs material) {
+        prepareMaterial(material);
+        material.baseColor = variable_customColor;
+    }
+}
+   

Vertex and attributes: requires

+

+

Type

array of string +

Value

Each entry must be any of uv0, uv1, color, position, tangents, custom0 + through custom7. +

Description

Lists the vertex attributes required by the material. The position attribute is always + required and does not need to be specified. The tangents attribute is automatically required + when selecting any shading model that is not unlit. See the shader sections of this document + for more information on how to access these attributes from the shaders. +

Interaction with custom variables
+

+ When the color attribute is specified, only four custom variables are available instead of five.

+

material {
+    parameters : [
+        {
+           type : sampler2d,
+           name : texture
+        },
+    ],
+    requires : [
+        uv0,
+        custom0
+    ],
+    shadingModel : lit,
+}
+
+fragment {
+    void material(inout MaterialInputs material) {
+        prepareMaterial(material);
+        material.baseColor = texture(materialParams_texture, getUV0());
+        material.baseColor.rgb *= getCustom0().rgb;
+    }
+}
+   

Vertex and attributes: variables

+

+

Type

array of string +

Value

Up to 5 strings, each must be a valid GLSL identifier. +

Description

Defines custom interpolants (or variables) that are output by the material's vertex shader. + Each entry of the array defines the name of an interpolant. The full name in the fragment + shader is the name of the interpolant with the variable_ prefix. For instance, if you + declare a variable called eyeDirection you can access it in the fragment shader using + variable_eyeDirection. In the vertex shader, the interpolant name is simply a member of + the MaterialVertexInputs structure (material.eyeDirection in your example). Each + interpolant is of type float4 (vec4) in the shaders. By default the precision of the + interpolant is highp in both the vertex and fragment shaders. + An alternate syntax can be used to specify both the name and precision of the interpolant. + In this case the specified precision is used as-is in both fragment and vertex stages, in + particular if default is specified the default precision is used is the fragment shader + (mediump) and in the vertex shader (highp). +

Interaction with required attributes
+

+ If the color attribute is specified in the required list, then only four variables can be used + instead of five.

+

material {
+    name : Skybox,
+    parameters : [
+        {
+           type : samplerCubemap,
+           name : skybox
+        }
+    ],
+    variables : [
+         eyeDirection,
+         {
+            name : eyeColor,
+            precision : medium
+         }
+    ],
+    vertexDomain : device,
+    depthWrite : false,
+    shadingModel : unlit
+}
+
+fragment {
+    void material(inout MaterialInputs material) {
+        prepareMaterial(material);
+        float3 sky = texture(materialParams_skybox, variable_eyeDirection.xyz).rgb;
+        material.baseColor = vec4(sky, 1.0);
+    }
+}
+
+vertex {
+    void materialVertex(inout MaterialVertexInputs material) {
+        float3 p = getPosition().xyz;
+        float3 u = mulMat4x4Float3(getViewFromClipMatrix(), p).xyz;
+        material.eyeDirection.xyz = mulMat3x3Float3(getWorldFromViewMatrix(), u);
+    }
+}
+   

Vertex and attributes: vertexDomain

+

+

Type

string +

Value

Any of object, world, view, device. Defaults to object. +

Description

Defines the domain (or coordinate space) of the rendered mesh. The domain influences how the + vertices are transformed in the vertex shader. The possible domains are: +

+

    +
  • Object: the vertices are defined in the object (or model) coordinate space. The + vertices are transformed using the rendered object's transform matrix +
  • +
  • World: the vertices are defined in world coordinate space. The vertices are not + transformed using the rendered object's transform. +
  • +
  • View: the vertices are defined in view (or eye or camera) coordinate space. The + vertices are not transformed using the rendered object's transform. +
  • +
  • Device: the vertices are defined in normalized device (or clip) coordinate space. + The vertices are not transformed using the rendered object's transform.
+

material {
+    vertexDomain : device
+}
+   

Vertex and attributes: interpolation

+

+

Type

string +

Value

Any of smooth, flat. Defaults to smooth. +

Description

Defines how interpolants (or variables) are interpolated between vertices. When this property + is set to smooth, a perspective correct interpolation is performed on each interpolant. + When set to flat, no interpolation is performed and all the fragments within a given + triangle will be shaded the same. +

material {
+    interpolation : flat
+}
+   

Blending and transparency: blending

+

+

Type

string +

Value

Any of opaque, transparent, fade, add, masked, multiply, screen, custom. Defaults to opaque. +

Description

Defines how/if the rendered object is blended with the content of the render target. + The possible blending modes are: +

+

    +
  • Opaque: blending is disabled, the alpha channel of the material's output is ignored. +
  • +
  • Transparent: blending is enabled. The material's output is alpha composited with the + render target, using Porter-Duff's source over rule. This blending mode assumes + pre-multiplied alpha. +
  • +
  • Fade: acts as transparent but transparency is also applied to specular lighting. In + transparent mode, the material's alpha values only applies to diffuse lighting. This + blending mode is useful to fade lit objects in and out. +
  • +
  • Add: blending is enabled. The material's output is added to the content of the + render target. +
  • +
  • Multiply: blending is enabled. The material's output is multiplied with the content of the + render target, darkening the content. +
  • +
  • Screen: blending is enabled. Effectively the opposite of the multiply, the content of the + render target is brightened. +
  • +
  • Masked: blending is disabled. This blending mode enables alpha masking. The alpha channel + of the material's output defines whether a fragment is discarded or not. Additionally, + ALPHA_TO_COVERAGE is enabled for non-translucent views. See the maskThreshold section for more + information. +
  • +
  • Custom: blending is enabled. But the blending function is user specified. See blendFunction.
+

When blending is set to masked, alpha to coverage is automatically enabled for the material. + If this behavior is undesirable, refer to the Rasterization: alphaToCoverage section to turn + alpha to coverage off using the alphaToCoverage property.
+

material {
+    blending : transparent
+}
+   

Blending and transparency: blendFunction

+

+

Type

object +

Fields

srcRGB, srcA, dstRGB, dstA +

Description

- srcRGB: source function applied to the RGB channels + - srcA: source function applied to the alpha channel + - srcRGB: destination function applied to the RGB channels + - srcRGB: destination function applied to the alpha channel + The values possible for each functions are one of zero, one, srcColor, oneMinusSrcColor, + dstColor, oneMinusDstColor, srcAlpha, oneMinusSrcAlpha, dstAlpha, + oneMinusDstAlpha, srcAlphaSaturate +

material {
+    blending : custom,
+    blendFunction :
+    {
+        srcRGB: one,
+        srcA: one,
+        dstRGB: oneMinusSrcColor,
+        dstA: oneMinusSrcAlpha
+    }
+ }
+   

Blending and transparency: postLightingBlending

+

+

Type

string +

Value

Any of opaque, transparent, add. Defaults to transparent. +

Description

Defines how the postLightingColor material property is blended with the result of the + lighting computations. The possible blending modes are: +

+

    +
  • Opaque: blending is disabled, the material will output postLightingColor directly. +
  • +
  • Transparent: blending is enabled. The material's computed color is alpha composited with + the postLightingColor, using Porter-Duff's source over rule. This blending mode assumes + pre-multiplied alpha. +
  • +
  • Add: blending is enabled. The material's computed color is added to postLightingColor. +
  • +
  • Multiply: blending is enabled. The material's computed color is multiplied with postLightingColor. +
  • +
  • Screen: blending is enabled. The material's computed color is inverted and multiplied with postLightingColor, + and the result is added to the material's computed color.
+

material {
+    postLightingBlending : add
+}
+   

Blending and transparency: transparency

+

+

Type

string +

Value

Any of default, twoPassesOneSide or twoPassesTwoSides. Defaults to default. +

Description

Controls how transparent objects are rendered. It is only valid when the blending mode is + not opaque and refractionMode is none. None of these methods can accurately render + concave geometry, but in practice they are often good enough. +

The three possible transparency modes are: +

+

    +
  • default: the transparent object is rendered normally (as seen in figure 33), + honoring the culling mode, etc. +
  • +
  • twoPassesOneSide: the transparent object is first rendered in the depth buffer, then again in + the color buffer, honoring the culling mode. This effectively renders only half of the + transparent object as shown in figure 34. +
  • +
  • twoPassesTwoSides: the transparent object is rendered twice in the color buffer: first with its + back faces, then with its front faces. This mode lets you render both set of faces while reducing + or eliminating sorting issues, as shown in figure 35. + twoPassesTwoSides can be combined with doubleSided for better effect.
+

material {
+    transparency : twoPassesOneSide
+}

+

 
Figure 33: This double sided model shows the type of sorting issues transparent +
+

+

 
Figure 34: In twoPassesOneSide mode, only one set of faces is visible +
+

+

 
Figure 35: In twoPassesTwoSides mode, both set of faces are visible +
+

+   

Blending and transparency: maskThreshold

+

+

Type

number +

Value

A value between 0.0 and 1.0. Defaults to 0.4. +

Description

Sets the minimum alpha value a fragment must have to not be discarded when the blending mode + is set to masked. If the fragment is not discarded, its source alpha is set to 1. When the + blending mode is not masked, this value is ignored. This value can be used to controlled the + appearance of alpha-masked objects. +

material {
+    blending : masked,
+    maskThreshold : 0.5
+}
+   

Blending and transparency: refractionMode

+

+

Type

string +

Value

Any of none, cubemap, screenspace. Defaults to none. +

Description

Activates refraction when set to anything but none. A value of cubemap will only use the + IBL cubemap as source of refraction, while this is significantly more efficient, no scene + objects will be refracted, only the distant environment encoded in the cubemap. This mode is + adequate for an object viewer for instance. A value of screenspace will employ the more + advanced screen-space refraction algorithm which allows opaque objects in the scene to be + refracted. In cubemap mode, refracted rays are assumed to emerge from the center of the + object and the thickness parameter is only used for computing the absorption, but has no + impact on the refraction itself. In screenspace mode, refracted rays are assumed to travel + parallel to the view direction when they exit the refractive medium. +

material {
+    refractionMode : cubemap,
+}
+   

Blending and transparency: refractionType

+

+

Type

string +

Value

Any of solid, thin. Defaults to solid. +

Description

This is only meaningful when refractionMode is set to anything but none. refractionType + defines the refraction model used. solid is used for thick objects such as a crystal ball, + an ice cube or as sculpture. thin is used for thin objects such as a window, an ornament + ball or a soap bubble. In solid mode all refracive objects are assumed to be a sphere + tangent to the entry point and of radius thickness. In thin mode, all refractive objects + are assumed to be flat and thin and of thickness thickness. +

material {
+    refractionMode : cubemap,
+    refractionType : thin,
+}
+   

Rasterization: culling

+

+

Type

string +

Value

Any of none, front, back, frontAndBack. Defaults to back. +

Description

Defines which triangles should be culled: none, front-facing triangles, back-facing + triangles or all. +

material {
+    culling : none
+}
+   

Rasterization: colorWrite

+

+

Type

boolean +

Value

true or false. Defaults to true. +

Description

Enables or disables writes to the color buffer. +

material {
+    colorWrite : false
+}
+   

Rasterization: depthWrite

+

+

Type

boolean +

Value

true or false. Defaults to true for opaque materials, false for transparent materials. +

Description

Enables or disables writes to the depth buffer. +

material {
+    depthWrite : false
+}
+   

Rasterization: depthCulling

+

+

Type

boolean +

Value

true or false. Defaults to true. +

Description

Enables or disables depth testing. When depth testing is disabled, an object rendered with + this material will always appear on top of other opaque objects. +

material {
+    depthCulling : false
+}
+   

Rasterization: doubleSided

+

+

Type

boolean +

Value

true or false. Defaults to false. +

Description

Enables two-sided rendering and its capability to be toggled at run time. When set to true, + culling is automatically set to none; if the triangle is back-facing, the triangle's + normal is flipped to become front-facing. When explicitly set to false, this allows the + double-sidedness to be toggled at run time. +

material {
+    name : "Double sided material",
+    shadingModel : lit,
+    doubleSided : true
+}
+
+fragment {
+    void material(inout MaterialInputs material) {
+        prepareMaterial(material);
+        material.baseColor = materialParams.albedo;
+    }
+}
+   

Rasterization: alphaToCoverage

+

+

Type

boolean +

Value

true or false. Defaults to false. +

Description

Enables or disables alpha to coverage. When alpha to coverage is enabled, the coverage of + fragment is derived from its alpha. This property is only meaningful when MSAA is enabled. + Note: setting blending to masked automatically enables alpha to coverage. If this is not + desired, you can override this behavior by setting alpha to coverage to false as in the + example below. +

material {
+    name : "Alpha to coverage",
+    shadingModel : lit,
+    blending : masked,
+    alphaToCoverage : false
+}
+
+fragment {
+    void material(inout MaterialInputs material) {
+        prepareMaterial(material);
+        material.baseColor = materialParams.albedo;
+    }
+}
+   

Lighting: reflections

+

+

Type

string +

Value

default or screenspace. Defaults to default. +

Description

Controls the source of specular reflections for this material. When this property is set to + default, reflections only come image-based lights. When this property is set to + screenspace, reflections come from the screen space's color buffer in addition to + image-based lights. +

material {
+    name : "Glossy metal",
+    reflections : screenspace
+}
+   

Lighting: shadowMultiplier

+

+

Type

boolean +

Value

true or false. Defaults to false. +

Description

Only available in the unlit shading model. If this property is enabled, the final color + computed by the material is multiplied by the shadowing factor (or visibility). This allows to + create transparent shadow-receiving objects (for instance an invisible ground plane in AR). + This is only supported with shadows from directional lights. +

material {
+    name : "Invisible shadow plane",
+    shadingModel : unlit,
+    shadowMultiplier : true,
+    blending : transparent
+}
+
+fragment {
+    void material(inout MaterialInputs material) {
+        prepareMaterial(material);
+        // baseColor defines the color and opacity of the final shadow
+        material.baseColor = vec4(0.0, 0.0, 0.0, 0.7);
+    }
+}
+   

Lighting: transparentShadow

+

+

Type

boolean +

Value

true or false. Defaults to false. +

Description

Enables transparent shadows on this material. When this feature is enabled, Filament emulates + transparent shadows using a dithering pattern: they work best with variance shadow maps (VSM) + and blurring enabled. The opacity of the shadow derives directly from the alpha channel of + the material's baseColor property. Transparent shadows can be enabled on opaque objects, + making them compatible with refractive/transmissive objects that are otherwise considered + opaque. +

material {
+    name : "Clear plastic with stickers",
+    transparentShadow : true,
+    blending : transparent,
+    // ...
+}
+
+fragment {
+    void material(inout MaterialInputs material) {
+        prepareMaterial(material);
+        material.baseColor = texture(materialParams_baseColor, getUV0());
+    }
+}

+

 
Figure 36: Objects rendered with transparent shadows and blurry VSM with a +
+

+   

Lighting: clearCoatIorChange

+

+

Type

boolean +

Value

true or false. Defaults to true. +

Description

When adding a clear coat layer, the change in index of refraction (IoR) is taken into account + to modify the specular color of the base layer. This appears to darken baseColor. When this + effect is disabled, baseColor is left unmodified. See figure 37 for an + example of how this property can affect a red metallic base layer. +

material {
+    clearCoatIorChange : false
+}

+

 
Figure 37: The same rough metallic ball with a clear coat layer rendered +
+

+   

Lighting: multiBounceAmbientOcclusion

+

+

Type

boolean +

Value

true or false. Defaults to false on mobile, true on desktop. +

Description

Multi-bounce ambient occlusion takes into account interreflections when applying ambient + occlusion to image-based lighting. Turning this feature on avoids over-darkening occluded + areas. It also takes the surface color into account to generate colored ambient occlusion. + Figure 38 compares the ambient occlusion term of a surface with and without + multi-bounce ambient occlusion. Notice how multi-bounce ambient occlusion introduces color + in the occluded areas. Figure 39 toggles between multi-bounce ambient + occlusion on and off on a lit brick material to highlight the effects of this property. +

material {
+    multiBounceAmbientOcclusion : true
+}

+

 
Figure 38: Brick texture amient occlusion map rendered with multi-bounce ambient +
+

+

 
Figure 39: Brick texture rendered with multi-bounce ambient +
+

+   

Lighting: specularAmbientOcclusion

+

+

Type

string +

Value

none, simple or bentNormals. Defaults to none on mobile, simple on desktop. For + compatibility reasons, true and false are also accepted and map respectively to simple + and none. +

Description

Static ambient occlusion maps and dynamic ambient occlusion (SSAO, etc.) apply to diffuse + indirect lighting. When setting this property to other than none, a new ambient occlusion + term is derived from the surface roughness and applied to specular indirect lighting. + This effect helps remove unwanted specular reflections as shown in figure 40. + When this value is set to simple, Filament uses a cheap but approximate method of computing + the specular ambient occlusion term. If this value is set to bentNormals, Filament will use + a much more accurate but much more expensive method. +

material {
+    specularAmbientOcclusion : simple
+}

+

 
Figure 40: Comparison of specular ambient occlusion on and off. The effect is +
+

+   

Anti-aliasing: specularAntiAliasing

+

+

Type

boolean +

Value

true or false. Defaults to false. +

Description

Reduces specular aliasing and preserves the shape of specular highlights as an object moves + away from the camera. This anti-aliasing solution is particularly effective on glossy materials + (low roughness) but increases the cost of the material. The strength of the anti-aliasing + effect can be controlled using two other properties: specularAntiAliasingVariance and + specularAntiAliasingThreshold. +

material {
+    specularAntiAliasing : true
+}
+   

Anti-aliasing: specularAntiAliasingVariance

+

+

Type

float +

Value

A value between 0 and 1, set to 0.15 by default. +

Description

Sets the screen space variance of the filter kernel used when applying specular anti-aliasing. + Higher values will increase the effect of the filter but may increase roughness in unwanted + areas. +

material {
+    specularAntiAliasingVariance : 0.2
+}
+   

Anti-aliasing: specularAntiAliasingThreshold

+

+

Type

float +

Value

A value between 0 and 1, set to 0.2 by default. +

Description

Sets the clamping threshold used to suppress estimation errors when applying specular + anti-aliasing. When set to 0, specular anti-aliasing is disabled. +

material {
+    specularAntiAliasingThreshold : 0.1
+}
+   

Shading: customSurfaceShading

+

+

Type

bool +

Value

true or false. Defaults to false. +

Description

Enables custom surface shading when set to true. When surface shading is enabled, the fragment + shader must provide an extra function that will be invoked for every light in the scene that + may influence the current fragment. Please refer to the Custom surface shading section below + for more information. +

material {
+    customSurfaceShading : true
+}
+   

Vertex block

+

+

The vertex block is optional and can be used to control the vertex shading stage of the material. +The vertex block must contain valid +ESSL 3.0 code +(the version of GLSL supported in OpenGL ES 3.0). You are free to create multiple functions inside +the vertex block but you must declare the materialVertex function:

+

vertex {
+    void materialVertex(inout MaterialVertexInputs material) {
+        // vertex shading code
+    }
+}

+This function will be invoked automatically at runtime by the shading system and gives you the +ability to read and modify material properties using the MaterialVertexInputs structure. This full +definition of the structure can be found in the Material vertex inputs section. +

+You can use this structure to compute your custom variables/interpolants or to modify the value of +the attributes. For instance, the following vertex blocks modifies both the color and the UV +coordinates of the vertex over time: +

material {
+    requires : [uv0, color]
+}
+vertex {
+    void materialVertex(inout MaterialVertexInputs material) {
+        material.color *= sin(getUserTime().x);
+        material.uv0 *= sin(getUserTime().x);
+    }
+}

+In addition to the MaterialVertexInputs structure, your vertex shading code can use all the public +APIs listed in the Shader public APIs section. +

+   

Material vertex inputs

+
struct MaterialVertexInputs {
+    float4 color;              // if the color attribute is required
+    float2 uv0;                // if the uv0 attribute is required
+    float2 uv1;                // if the uv1 attribute is required
+    float3 worldNormal;        // only if the shading model is not unlit
+    float4 worldPosition;      // always available (see note below about world-space)
+
+    mat4   clipSpaceTransform; // default: identity, transforms the clip-space position, only available for `vertexDomain:device`
+
+    // variable* names are replaced with actual names
+    float4 variable0;          // if 1 or more variables is defined
+    float4 variable1;          // if 2 or more variables is defined
+    float4 variable2;          // if 3 or more variables is defined
+    float4 variable3;          // if 4 or more variables is defined
+};

+

worldPosition
+

+ To achieve good precision, the worldPosition coordinate in the vertex shader is shifted by the + camera position. To get the true world-space position, users can use + getUserWorldPosition(), however be aware that the true world-position might not + be able to fit in a float or might be represented with severely reduced precision.

+

+

UV attributes
+

+ By default the vertex shader of a material will flip the Y coordinate of the UV attributes + of the current mesh: material.uv0 = vec2(mesh_uv0.x, 1.0 - mesh_uv0.y). You can control + this behavior using the flipUV property and setting it to false.

+

+   

Custom vertex attributes

+

+

You can use up to 8 custom vertex attributes, all of type float4. These attributes can be accessed +using the vertex block shader functions getCustom0() to getCustom7(). However, before using +custom attributes, you must declare those attributes as required in the requires property of +the material:

+

material {
+    requires : [
+        custom0,
+        custom1,
+        custom2
+    ]
+}
+   

Fragment block

+

+

The fragment block must be used to control the fragment shading stage of the material. The fragment +block must contain valid +ESSL 3.0 +code (the version of GLSL supported in OpenGL ES 3.0). You are free to create multiple functions +inside the fragment block but you must declare the material function:

+

fragment {
+    void material(inout MaterialInputs material) {
+        prepareMaterial(material);
+        // fragment shading code
+    }
+}

+This function will be invoked automatically at runtime by the shading system and gives you the +ability to read and modify material properties using the MaterialInputs structure. This full +definition of the structure can be found in the Material fragment inputs section. The full +definition of the various members of the structure can be found in the Material models section +of this document. +

+The goal of the material() function is to compute the material properties specific to the selected +shading model. For instance, here is a fragment block that creates a glossy red metal using the +standard lit shading model: +

fragment {
+    void material(inout MaterialInputs material) {
+        prepareMaterial(material);
+        material.baseColor.rgb = vec3(1.0, 0.0, 0.0);
+        material.metallic = 1.0;
+        material.roughness = 0.0;
+    }
+}
+   

prepareMaterial function

+

+

Note that you must call prepareMaterial(material) before exiting the material() function. +This prepareMaterial function sets up the internal state of the material model. Some of the APIs +described in the Fragment APIs section - like shading_normal for instance - can only be accessed +after invoking prepareMaterial().

+

+It is also important to remember that the normal property - as described in the Material fragment +inputs section - only has an effect when modified before calling prepareMaterial(). Here is an +example of a fragment shader that properly modifies the normal property to implement a glossy red +plastic with bump mapping: +

fragment {
+    void material(inout MaterialInputs material) {
+        // fetch the normal in tangent space
+        vec3 normal = texture(materialParams_normalMap, getUV0()).xyz;
+        material.normal = normal * 2.0 - 1.0;
+
+        // prepare the material
+        prepareMaterial(material);
+
+        // from now on, shading_normal, etc. can be accessed
+        material.baseColor.rgb = vec3(1.0, 0.0, 0.0);
+        material.metallic = 0.0;
+        material.roughness = 1.0;
+    }
+}
+   

Material fragment inputs

+
struct MaterialInputs {
+    float4 baseColor;           // default: float4(1.0)
+    float4 emissive;            // default: float4(0.0, 0.0, 0.0, 1.0)
+    float4 postLightingColor;   // default: float4(0.0)
+
+    // no other field is available with the unlit shading model
+    float  roughness;           // default: 1.0
+    float  metallic;            // default: 0.0, not available with cloth or specularGlossiness
+    float  reflectance;         // default: 0.5, not available with cloth or specularGlossiness
+    float  ambientOcclusion;    // default: 0.0
+
+    // not available when the shading model is subsurface or cloth
+    float3 sheenColor;          // default: float3(0.0)
+    float  sheenRoughness;      // default: 0.0
+    float  clearCoat;           // default: 1.0
+    float  clearCoatRoughness;  // default: 0.0
+    float3 clearCoatNormal;     // default: float3(0.0, 0.0, 1.0)
+    float  anisotropy;          // default: 0.0
+    float3 anisotropyDirection; // default: float3(1.0, 0.0, 0.0)
+
+    // only available when the shading model is subsurface or refraction is enabled
+    float  thickness;           // default: 0.5
+
+    // only available when the shading model is subsurface
+    float  subsurfacePower;     // default: 12.234
+    float3 subsurfaceColor;     // default: float3(1.0)
+
+    // only available when the shading model is cloth
+    float3 sheenColor;          // default: sqrt(baseColor)
+    float3 subsurfaceColor;     // default: float3(0.0)
+
+    // only available when the shading model is specularGlossiness
+    float3 specularColor;       // default: float3(0.0)
+    float  glossiness;          // default: 0.0
+
+    // not available when the shading model is unlit
+    // must be set before calling prepareMaterial()
+    float3 normal;              // default: float3(0.0, 0.0, 1.0)
+
+    // only available when refraction is enabled
+    float transmission;         // default: 1.0
+    float3 absorption;          // default float3(0.0, 0.0, 0.0)
+    float ior;                  // default: 1.5
+    float microThickness;       // default: 0.0, not available with refractionType "solid"
+}
+   

Custom surface shading

+

+

When customSurfaceShading is set to true in the material block, the fragment block must +declare and implement the surfaceShading function:

+

fragment {
+    void material(inout MaterialInputs material) {
+        prepareMaterial(material);
+        // prepare material inputs
+    }
+
+    vec3 surfaceShading(
+        const MaterialInputs materialInputs,
+        const ShadingData shadingData,
+        const LightData lightData
+    ) {
+        return vec3(1.0); // output of custom lighting
+    }
+}

+This function will be invoked for every light (directional, spot or point) in the scene that may +influence the current fragment. The surfaceShading is invoked with 3 sets of data: +

+

    +
  • MaterialInputs, as described in the Material fragment inputs section and prepared in the + material function explained above +
  • +
  • ShadingData, a structure containing values derived from MaterialInputs (see below) +
  • +
  • LightData, a structure containing values specific to the light being currently + evaluated (see below)
+

+The surfaceShading function must return an RGB color in linear sRGB. Alpha blending and alpha +masking are handled outside of this function and must therefore be ignored. +

+

About shadowed fragments
+

+ The surfaceShading function is invoked even when a fragment is known to be fully in the shadow + of the current light (lightData.NdotL <= 0.0 or lightData.visibility <= 0.0). This gives + more flexibility to the surfaceShading function as it provides a simple way to handle constant + ambient lighting for instance.

+

+

Shading models
+

+ Custom surface shading only works with the lit shading model. Attempting to use any other + model will result in an error.

+

+   

Shading data structure

+
struct ShadingData {
+    // The material's diffuse color, as derived from baseColor and metallic.
+    // This color is pre-multiplied by alpha and in the linear sRGB color space.
+    vec3  diffuseColor;
+
+    // The material's specular color, as derived from baseColor and metallic.
+    // This color is pre-multiplied by alpha and in the linear sRGB color space.
+    vec3  f0;
+
+    // The perceptual roughness is the roughness value set in MaterialInputs,
+    // with extra processing:
+    // - Clamped to safe values
+    // - Filtered if specularAntiAliasing is enabled
+    // This value is between 0.0 and 1.0.
+    float perceptualRoughness;
+
+    // The roughness value expected by BRDFs. This value is the square of
+    // perceptualRoughness. This value is between 0.0 and 1.0.
+    float roughness;
+};
+   

Light data structure

+
struct LightData {
+    // The color (.rgb) and pre-exposed intensity (.w) of the light.
+    // The color is an RGB value in the linear sRGB color space.
+    // The pre-exposed intensity is the intensity of the light multiplied by
+    // the camera's exposure value.
+    vec4  colorIntensity;
+
+    // The normalized light vector, in world space (direction from the
+    // current fragment's position to the light).
+    vec3  l;
+
+    // The dot product of the shading normal (with normal mapping applied)
+    // and the light vector. This value is equal to the result of
+    // saturate(dot(getWorldSpaceNormal(), lightData.l)).
+    // This value is always between 0.0 and 1.0. When the value is <= 0.0,
+    // the current fragment is not visible from the light and lighting
+    // computations can be skipped.
+    float NdotL;
+
+    // The position of the light in world space.
+    vec3  worldPosition;
+
+    // Attenuation of the light based on the distance from the current
+    // fragment to the light in world space. This value between 0.0 and 1.0
+    // is computed differently for each type of light (it's always 1.0 for
+    // directional lights).
+    float attenuation;
+
+    // Visibility factor computed from shadow maps or other occlusion data
+    // specific to the light being evaluated. This value is between 0.0 and
+    // 1.0.
+    float visibility;
+};
+   

Example

+

+

The material below shows how to use custom surface shading to implement a simplified toon shader:

+

material {
+    name : Toon,
+    shadingModel : lit,
+    parameters : [
+        {
+            type : float3,
+            name : baseColor
+        }
+    ],
+    customSurfaceShading : true
+}
+
+fragment {
+    void material(inout MaterialInputs material) {
+        prepareMaterial(material);
+        material.baseColor.rgb = materialParams.baseColor;
+    }
+
+    vec3 surfaceShading(
+            const MaterialInputs materialInputs,
+            const ShadingData shadingData,
+            const LightData lightData
+    ) {
+        // Number of visible shade transitions
+        const float shades = 5.0;
+        // Ambient intensity
+        const float ambient = 0.1;
+
+        float toon = max(ceil(lightData.NdotL * shades) / shades, ambient);
+
+        // Shadowing and attenuation
+        toon *= lightData.visibility * lightData.attenuation;
+
+        // Color and intensity
+        vec3 light = lightData.colorIntensity.rgb * lightData.colorIntensity.w;
+
+        return shadingData.diffuseColor * light * toon;
+    }
+}

+The result can be seen in figure 41. +

+

 
Figure 41: simple toon shading implemented with custom +
+

+   

Shader public APIs

+   

Types

+

+

While GLSL types can be used directly (vec4 or mat4) we recommend the use of the following +type aliases:

+
+ + + + + + + + + + + + + + +
Name GLSL type Description
bool2 bvec2 A vector of 2 booleans
bool3 bvec3 A vector of 3 booleans
bool4 bvec4 A vector of 4 booleans
int2 ivec2 A vector of 2 integers
int3 ivec3 A vector of 3 integers
int4 ivec4 A vector of 4 integers
uint2 uvec2 A vector of 2 unsigned integers
uint3 uvec3 A vector of 3 unsigned integers
uint4 uvec4 A vector of 4 unsigned integers
float2 float2 A vector of 2 floats
float3 float3 A vector of 3 floats
float4 float4 A vector of 4 floats
float4×4 mat4 A 4×4 float matrix
float3×3 mat3 A 3×3 float matrix
+

+   

Math

+

+

+ + + + + + + + +
Name Type Description
PI float A constant that represent \(\pi\)
HALF_PI float A constant that represent \(\frac{\pi}{2}\)
saturate(float x) float Clamps the specified value between 0.0 and 1.0
pow5(float x) float Computes \(x^5\)
sq(float x) float Computes \(x^2\)
max3(float3 v) float Returns the maximum value of the specified float3
mulMat4×4Float3(float4×4 m, float3 v) float4 Returns \(m * v\)
mulMat3×3Float3(float4×4 m, float3 v) float4 Returns \(m * v\)
+

+   

Matrices

+

+

+ + + + + + + + + +
Name Type Description
getViewFromWorldMatrix() float4×4 Matrix that converts from world space to view/eye space
getWorldFromViewMatrix() float4×4 Matrix that converts from view/eye space to world space
getClipFromViewMatrix() float4×4 Matrix that converts from view/eye space to clip (NDC) space
getViewFromClipMatrix() float4×4 Matrix that converts from clip (NDC) space to view/eye space
getEyeFromViewMatrix() float4×4 Matrix that converts from view space to eye space
getEyeFromViewMatrix(int eyeIndex) float4×4 Matrix that converts from view space to eye space for the eye referred to by eyeIndex
getClipFromWorldMatrix() float4×4 Matrix that converts from world to clip (NDC) space
getClipFromWorldMatrix(int eyeIndex) float4×4 Matrix that converts from world to clip (NDC) space for the eye referred to by eyeIndex
getWorldFromClipMatrix() float4×4 Matrix that converts from clip (NDC) space to world space
+

+   

Frame constants

+

+

+ + + + + + + + + +
Name Type Description
getResolution() float4 Dimensions of the view's effective (physical) viewport in pixels: width, height, 1 / width, 1 / height. This might be different from View::getViewport() for instance because of added rendering guard-bands.
getWorldCameraPosition() float3 Position of the camera/eye in world space (see note below)
getWorldOffset() float3 [deprecated] The shift required to obtain API-level world space. Use getUserWorldPosition() instead
getUserWorldFromWorldMatrix() float4×4 Matrix that converts from world space to API-level (user) world space.
getTime() float Current time as a remainder of 1 second. Yields a value between 0 and 1
getUserTime() float4 Current time in seconds: time, (double)time - time, 0, 0
getUserTimeMod(float m) float Current time modulo m in seconds
getExposure() float Photometric exposure of the camera
getEV100() float Exposure value at ISO 100 of the camera
+

+

world space
+

+ To achieve good precision, the “world space” in Filament's shading system does not necessarily + match the API-level world space. To obtain the position of the API-level camera, custom + materials can use getUserWorldFromWorldMatrix() to transform getWorldCameraPosition().

+

+   

Material globals

+

+

+ + + + +
Name Type Description
getMaterialGlobal0() float4 A vec4 visible by all materials, its value is set by View::setMaterialGlobal(0, float4). Its default value is {0,0,0,1}.
getMaterialGlobal1() float4 A vec4 visible by all materials, its value is set by View::setMaterialGlobal(1, float4). Its default value is {0,0,0,1}.
getMaterialGlobal2() float4 A vec4 visible by all materials, its value is set by View::setMaterialGlobal(2, float4). Its default value is {0,0,0,1}.
getMaterialGlobal3() float4 A vec4 visible by all materials, its value is set by View::setMaterialGlobal(3, float4). Its default value is {0,0,0,1}.
+

+   

Vertex only

+

+

The following APIs are only available from the vertex block:

+
+ + + + + + +
Name Type Description
getPosition() float4 Vertex position in the domain defined by the material (default: object/model space)
getCustom0() to getCustom7() float4 Custom vertex attribute
getWorldFromModelMatrix() float4×4 Matrix that converts from model (object) space to world space
getWorldFromModelNormalMatrix() float3×3 Matrix that converts normals from model (object) space to world space
getVertexIndex() int Index of the current vertex
getEyeIndex() int Index of the eye being rendered, starting at 0
+

+   

Fragment only

+

+

The following APIs are only available from the fragment block:

+
+ + + + + + + + + + + + + + + + + + +
Name Type Description
getWorldTangentFrame() float3×3 Matrix containing in each column the tangent (frame[0]), bi-tangent (frame[1]) and normal (frame[2]) of the vertex in world space. If the material does not compute a tangent space normal for bump mapping or if the shading is not anisotropic, only the normal is valid in this matrix.
getWorldPosition() float3 Position of the fragment in world space (see note below about world-space)
getUserWorldPosition() float3 Position of the fragment in API-level (user) world-space (see note below about world-space)
getWorldViewVector() float3 Normalized vector in world space from the fragment position to the eye
getWorldNormalVector() float3 Normalized normal in world space, after bump mapping (must be used after prepareMaterial())
getWorldGeometricNormalVector() float3 Normalized normal in world space, before bump mapping (can be used before prepareMaterial())
getWorldReflectedVector() float3 Reflection of the view vector about the normal (must be used after prepareMaterial())
getNormalizedViewportCoord() float3 Normalized user viewport position (i.e. NDC coordinates normalized to [0, 1] for the position, [1, 0] for the depth), can be used before prepareMaterial()). Because the user viewport is smaller than the actual physical viewport, these coordinates can be negative or superior to 1 in the non-visible area of the physical viewport.
getNdotV() float The result of dot(normal, view), always strictly greater than 0 (must be used after prepareMaterial())
getColor() float4 Interpolated color of the fragment, if the color attribute is required
getUV0() float2 First interpolated set of UV coordinates, only available if the uv0 attribute is required
getUV1() float2 First interpolated set of UV coordinates, only available if the uv1 attribute is required
getMaskThreshold() float Returns the mask threshold, only available when blending is set to masked
inverseTonemap(float3) float3 Applies the inverse tone mapping operator to the specified linear sRGB color and returns a linear sRGB color. This operation may be an approximation and works best with the “Filmic” tone mapping operator
inverseTonemapSRGB(float3) float3 Applies the inverse tone mapping operator to the specified non-linear sRGB color and returns a linear sRGB color. This operation may be an approximation and works best with the “Filmic” tone mapping operator
luminance(float3) float Computes the luminance of the specified linear sRGB color
ycbcrToRgb(float, float2) float3 Converts a luminance and CbCr pair to a sRGB color
uvToRenderTargetUV(float2) float2 Transforms a UV coordinate to allow sampling from a RenderTarget attachment
+

+

world-space
+

+ To obtain API-level world-space coordinates, custom materials should use getUserWorldPosition() + or use getUserWorldFromWorldMatrix(). Note that API-level world-space coordinates should + never or rarely be used because they may not fit in a float3 or have severely reduced precision.

+

+

sampling from render targets
+

+ When sampling from a filament::Texture that is attached to a filament::RenderTarget for + materials in the surface domain, please use uvToRenderTargetUV to transform the texture + coordinate. This will flip the coordinate depending on which backend is being used.

+

+   

Compiling materials

+

+

Material packages can be compiled from material definitions using the command line tool called +matc. The simplest way to use matc is to specify an input material definition (car_paint.mat +in the example below) and an output material package (car_paint.filamat in the example below):

+

$ matc -o ./materials/bin/car_paint.filamat ./materials/src/car_paint.mat
+   

Shader validation

+

+

matc attempts to validate shaders when compiling a material package. The example below shows an +example of an error message generated when compiling a material definition containing a typo in the +fragment shader (metalic instead of metallic). The reported line numbers are line numbers in the +source material definition file.

+

ERROR: 0:13: 'metalic' : no such field in structure
+ERROR: 0:13: '' : compilation terminated
+ERROR: 2 compilation errors.  No code generated.
+
+Could not compile material metal.mat
+   

Flags

+

+

The command line flags relevant to application development are described in table 16.

+
+  + + + + + + +
Flag Value Usage
-o, —output [path] Specify the output file path
-p, —platform desktop/mobile/all Select the target platform(s)
-a, —api opengl/vulkan/all Specify the target graphics API
-S, —optimize-size N/A Optimize compiled material for size instead of just performance
-r, —reflect parameters Outputs the specified metadata as JSON
-v, —variant-filter [variant] Filters out the specified, comma-separated variants
Table 16: List of matc flags
+

+matc offers a few other flags that are irrelevant to application developers and for internal +use only. +

+   

—platform

+

+

By default, matc generates material packages containing shaders for all supported platforms. If +you wish to reduce the size of your material packages, it is recommended to select only the +appropriate target platform. For instance, to compile a material package for Android only, run +the following command:

+

$ matc -p mobile -o ./materials/bin/car_paint.filamat ./materials/src/car_paint.mat
+   

—api

+

+

By default, matc generates material packages containing shaders for the OpenGL API. You can choose +to generate shaders for the Vulkan API in addition to the OpenGL shaders. If you intend on targeting +only Vulkan capable devices, you can reduce the size of the material packages by generating only +the set of Vulkan shaders:

+

$ matc -a vulkan -o ./materials/bin/car_paint.filamat ./materials/src/car_paint.mat
+   

—optimize-size

+

+

This flag applies fewer optimization techniques to try and keep the final material as small as +possible. If the compiled material is deemed too large by default, using this flag might be +a good compromise between runtime performance and size.

+

+   

—reflect

+

+

This flag was designed to help build tools around matc. It allows you to print out specific +metadata in JSON format. The example below prints out the list of parameters defined in Filament's +standard skybox material. It produces a list of 2 parameters, named showSun and skybox, +respectively a boolean and a cubemap texture.

+

$ matc --reflect parameters filament/src/materials/skybox.mat
+{
+  "parameters": [
+    {
+      "name": "showSun",
+      "type": "bool",
+      "size": "1"
+    },
+    {
+      "name": "skybox",
+      "type": "samplerCubemap",
+      "format": "float",
+      "precision": "default"
+    }
+  ]
+}
+   

—variant-filter

+

+

This flag can be used to further reduce the size of a compiled material. It is used to specify a +list of shader variants that the application guarantees will never be needed. These shader variants +are skipped during the code generation phase of matc, thus reducing the overall size of the +material.

+

+The variants must be specified as a comma-separated list, using one of the following available +variants: +

+

    +
  • directionalLighting, used when a directional light is present in the scene +
  • +
  • dynamicLighting, used when a non-directional light (point, spot, etc.) is present in the scene +
  • +
  • shadowReceiver, used when an object can receive shadows +
  • +
  • skinning, used when an object is animated using GPU skinning or vertex morphing +
  • +
  • fog, used when global fog is applied to the scene +
  • +
  • vsm, used when VSM shadows are enabled and the object is a shadow receiver +
  • +
  • ssr, used when screen-space reflections are enabled in the View
+

+Example: +

--variant-filter=skinning,shadowReceiver

+Note that some variants may automatically be filtered out. For instance, all lighting related +variants (directionalLighting, etc.) are filtered out when compiling an unlit material. +

+When this flag is used, the specified variant filters are merged with the variant filters specified +in the material itself. +

+Use this flag with caution, filtering out a variant required at runtime may lead to crashes. +

+   

Handling colors

+   

Linear colors

+

+

If the color data comes from a texture, simply make sure you use an sRGB texture to benefit from +automatic hardware conversion from sRGB to linear. If the color data is passed as a parameter to +the material you can convert from sRGB to linear by running the following algorithm on each +color channel:

+

float sRGB_to_linear(float color) {
+ return color <= 0.04045 ? color / 12.92 : pow((color + 0.055) / 1.055, 2.4);
+}

+Alternatively you can use one of the two cheaper but less accurate versions shown below: +

// Cheaper
+linearColor = pow(color, 2.2);
+// Cheapest
+linearColor = color * color;
+   

Pre-multiplied alpha

+

+

A color uses pre-multiplied alpha if its RGB components are multiplied by the alpha channel:

+

// Compute pre-multiplied color
+color.rgb *= color.a;

+If the color is sampled from a texture, you can simply ensure that the texture data is +pre-multiplied ahead of time. On Android, any texture uploaded from a +Bitmap will be +pre-multiplied by default. +

+   

Sampler usage in Materials

+

+

The number of usable sampler parameters (e.g.: type is sampler2d) in materials is limited and +depends on the material properties, shading model, feature level and variant filter.

+

+   

Feature level 1 and 2

+

+

unlit materials can use up to 12 samplers by default.

+

+lit materials can use up to 9 samplers by default, however if refractionMode or reflectionMode +is set to screenspace that number is reduced to 8. +

+Finally if variantFilter contains the fog filter, an extra sampler is made available, such that +unlit materials can use up to 13 and lit materials up to 10 samplers by default. +

+   

Feature level 3

+

+

16 samplers are available.

+

+

external samplers
+

+ Be aware that external samplers account for 2 regular samplers.

+

+

formatted by Markdeep 1.18  
+
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/mark.min.js b/docs/mark.min.js new file mode 100644 index 0000000000..1636231883 --- /dev/null +++ b/docs/mark.min.js @@ -0,0 +1,7 @@ +/*!*************************************************** +* mark.js v8.11.1 +* https://markjs.io/ +* Copyright (c) 2014–2018, Julian Kühnel +* Released under the MIT license https://git.io/vwTVl +*****************************************************/ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Mark=t()}(this,function(){"use strict";var e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},t=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},n=function(){function e(e,t){for(var n=0;n1&&void 0!==arguments[1])||arguments[1],i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[],o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:5e3;t(this,e),this.ctx=n,this.iframes=r,this.exclude=i,this.iframesTimeout=o}return n(e,[{key:"getContexts",value:function(){var e=[];return(void 0!==this.ctx&&this.ctx?NodeList.prototype.isPrototypeOf(this.ctx)?Array.prototype.slice.call(this.ctx):Array.isArray(this.ctx)?this.ctx:"string"==typeof this.ctx?Array.prototype.slice.call(document.querySelectorAll(this.ctx)):[this.ctx]:[]).forEach(function(t){var n=e.filter(function(e){return e.contains(t)}).length>0;-1!==e.indexOf(t)||n||e.push(t)}),e}},{key:"getIframeContents",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){},r=void 0;try{var i=e.contentWindow;if(r=i.document,!i||!r)throw new Error("iframe inaccessible")}catch(e){n()}r&&t(r)}},{key:"isIframeBlank",value:function(e){var t="about:blank",n=e.getAttribute("src").trim();return e.contentWindow.location.href===t&&n!==t&&n}},{key:"observeIframeLoad",value:function(e,t,n){var r=this,i=!1,o=null,a=function a(){if(!i){i=!0,clearTimeout(o);try{r.isIframeBlank(e)||(e.removeEventListener("load",a),r.getIframeContents(e,t,n))}catch(e){n()}}};e.addEventListener("load",a),o=setTimeout(a,this.iframesTimeout)}},{key:"onIframeReady",value:function(e,t,n){try{"complete"===e.contentWindow.document.readyState?this.isIframeBlank(e)?this.observeIframeLoad(e,t,n):this.getIframeContents(e,t,n):this.observeIframeLoad(e,t,n)}catch(e){n()}}},{key:"waitForIframes",value:function(e,t){var n=this,r=0;this.forEachIframe(e,function(){return!0},function(e){r++,n.waitForIframes(e.querySelector("html"),function(){--r||t()})},function(e){e||t()})}},{key:"forEachIframe",value:function(t,n,r){var i=this,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){},a=t.querySelectorAll("iframe"),s=a.length,c=0;a=Array.prototype.slice.call(a);var u=function(){--s<=0&&o(c)};s||u(),a.forEach(function(t){e.matches(t,i.exclude)?u():i.onIframeReady(t,function(e){n(t)&&(c++,r(e)),u()},u)})}},{key:"createIterator",value:function(e,t,n){return document.createNodeIterator(e,t,n,!1)}},{key:"createInstanceOnIframe",value:function(t){return new e(t.querySelector("html"),this.iframes)}},{key:"compareNodeIframe",value:function(e,t,n){if(e.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_PRECEDING){if(null===t)return!0;if(t.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_FOLLOWING)return!0}return!1}},{key:"getIteratorNode",value:function(e){var t=e.previousNode();return{prevNode:t,node:null===t?e.nextNode():e.nextNode()&&e.nextNode()}}},{key:"checkIframeFilter",value:function(e,t,n,r){var i=!1,o=!1;return r.forEach(function(e,t){e.val===n&&(i=t,o=e.handled)}),this.compareNodeIframe(e,t,n)?(!1!==i||o?!1===i||o||(r[i].handled=!0):r.push({val:n,handled:!0}),!0):(!1===i&&r.push({val:n,handled:!1}),!1)}},{key:"handleOpenIframes",value:function(e,t,n,r){var i=this;e.forEach(function(e){e.handled||i.getIframeContents(e.val,function(e){i.createInstanceOnIframe(e).forEachNode(t,n,r)})})}},{key:"iterateThroughNodes",value:function(e,t,n,r,i){for(var o,a=this,s=this.createIterator(t,e,r),c=[],u=[],l=void 0,h=void 0;void 0,o=a.getIteratorNode(s),h=o.prevNode,l=o.node;)this.iframes&&this.forEachIframe(t,function(e){return a.checkIframeFilter(l,h,e,c)},function(t){a.createInstanceOnIframe(t).forEachNode(e,function(e){return u.push(e)},r)}),u.push(l);u.forEach(function(e){n(e)}),this.iframes&&this.handleOpenIframes(c,e,n,r),i()}},{key:"forEachNode",value:function(e,t,n){var r=this,i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){},o=this.getContexts(),a=o.length;a||i(),o.forEach(function(o){var s=function(){r.iterateThroughNodes(e,o,t,n,function(){--a<=0&&i()})};r.iframes?r.waitForIframes(o,s):s()})}}],[{key:"matches",value:function(e,t){var n="string"==typeof t?[t]:t,r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.oMatchesSelector||e.webkitMatchesSelector;if(r){var i=!1;return n.every(function(t){return!r.call(e,t)||(i=!0,!1)}),i}return!1}}]),e}(),o=function(){function e(n){t(this,e),this.opt=r({},{diacritics:!0,synonyms:{},accuracy:"partially",caseSensitive:!1,ignoreJoiners:!1,ignorePunctuation:[],wildcards:"disabled"},n)}return n(e,[{key:"create",value:function(e){return"disabled"!==this.opt.wildcards&&(e=this.setupWildcardsRegExp(e)),e=this.escapeStr(e),Object.keys(this.opt.synonyms).length&&(e=this.createSynonymsRegExp(e)),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),this.opt.diacritics&&(e=this.createDiacriticsRegExp(e)),e=this.createMergedBlanksRegExp(e),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.createJoinersRegExp(e)),"disabled"!==this.opt.wildcards&&(e=this.createWildcardsRegExp(e)),e=this.createAccuracyRegExp(e),new RegExp(e,"gm"+(this.opt.caseSensitive?"":"i"))}},{key:"escapeStr",value:function(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}},{key:"createSynonymsRegExp",value:function(e){var t=this.opt.synonyms,n=this.opt.caseSensitive?"":"i",r=this.opt.ignoreJoiners||this.opt.ignorePunctuation.length?"\0":"";for(var i in t)if(t.hasOwnProperty(i)){var o=t[i],a="disabled"!==this.opt.wildcards?this.setupWildcardsRegExp(i):this.escapeStr(i),s="disabled"!==this.opt.wildcards?this.setupWildcardsRegExp(o):this.escapeStr(o);""!==a&&""!==s&&(e=e.replace(new RegExp("("+this.escapeStr(a)+"|"+this.escapeStr(s)+")","gm"+n),r+"("+this.processSynonyms(a)+"|"+this.processSynonyms(s)+")"+r))}return e}},{key:"processSynonyms",value:function(e){return(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),e}},{key:"setupWildcardsRegExp",value:function(e){return(e=e.replace(/(?:\\)*\?/g,function(e){return"\\"===e.charAt(0)?"?":""})).replace(/(?:\\)*\*/g,function(e){return"\\"===e.charAt(0)?"*":""})}},{key:"createWildcardsRegExp",value:function(e){var t="withSpaces"===this.opt.wildcards;return e.replace(/\u0001/g,t?"[\\S\\s]?":"\\S?").replace(/\u0002/g,t?"[\\S\\s]*?":"\\S*")}},{key:"setupIgnoreJoinersRegExp",value:function(e){return e.replace(/[^(|)\\]/g,function(e,t,n){var r=n.charAt(t+1);return/[(|)\\]/.test(r)||""===r?e:e+"\0"})}},{key:"createJoinersRegExp",value:function(e){var t=[],n=this.opt.ignorePunctuation;return Array.isArray(n)&&n.length&&t.push(this.escapeStr(n.join(""))),this.opt.ignoreJoiners&&t.push("\\u00ad\\u200b\\u200c\\u200d"),t.length?e.split(/\u0000+/).join("["+t.join("")+"]*"):e}},{key:"createDiacriticsRegExp",value:function(e){var t=this.opt.caseSensitive?"":"i",n=this.opt.caseSensitive?["aàáảãạăằắẳẵặâầấẩẫậäåāą","AÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ","cçćč","CÇĆČ","dđď","DĐĎ","eèéẻẽẹêềếểễệëěēę","EÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ","iìíỉĩịîïī","IÌÍỈĨỊÎÏĪ","lł","LŁ","nñňń","NÑŇŃ","oòóỏõọôồốổỗộơởỡớờợöøō","OÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ","rř","RŘ","sšśșş","SŠŚȘŞ","tťțţ","TŤȚŢ","uùúủũụưừứửữựûüůū","UÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ","yýỳỷỹỵÿ","YÝỲỶỸỴŸ","zžżź","ZŽŻŹ"]:["aàáảãạăằắẳẵặâầấẩẫậäåāąAÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ","cçćčCÇĆČ","dđďDĐĎ","eèéẻẽẹêềếểễệëěēęEÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ","iìíỉĩịîïīIÌÍỈĨỊÎÏĪ","lłLŁ","nñňńNÑŇŃ","oòóỏõọôồốổỗộơởỡớờợöøōOÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ","rřRŘ","sšśșşSŠŚȘŞ","tťțţTŤȚŢ","uùúủũụưừứửữựûüůūUÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ","yýỳỷỹỵÿYÝỲỶỸỴŸ","zžżźZŽŻŹ"],r=[];return e.split("").forEach(function(i){n.every(function(n){if(-1!==n.indexOf(i)){if(r.indexOf(n)>-1)return!1;e=e.replace(new RegExp("["+n+"]","gm"+t),"["+n+"]"),r.push(n)}return!0})}),e}},{key:"createMergedBlanksRegExp",value:function(e){return e.replace(/[\s]+/gim,"[\\s]+")}},{key:"createAccuracyRegExp",value:function(e){var t=this,n=this.opt.accuracy,r="string"==typeof n?n:n.value,i="";switch(("string"==typeof n?[]:n.limiters).forEach(function(e){i+="|"+t.escapeStr(e)}),r){case"partially":default:return"()("+e+")";case"complementary":return"()([^"+(i="\\s"+(i||this.escapeStr("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~¡¿")))+"]*"+e+"[^"+i+"]*)";case"exactly":return"(^|\\s"+i+")("+e+")(?=$|\\s"+i+")"}}}]),e}(),a=function(){function a(e){t(this,a),this.ctx=e,this.ie=!1;var n=window.navigator.userAgent;(n.indexOf("MSIE")>-1||n.indexOf("Trident")>-1)&&(this.ie=!0)}return n(a,[{key:"log",value:function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"debug",r=this.opt.log;this.opt.debug&&"object"===(void 0===r?"undefined":e(r))&&"function"==typeof r[n]&&r[n]("mark.js: "+t)}},{key:"getSeparatedKeywords",value:function(e){var t=this,n=[];return e.forEach(function(e){t.opt.separateWordSearch?e.split(" ").forEach(function(e){e.trim()&&-1===n.indexOf(e)&&n.push(e)}):e.trim()&&-1===n.indexOf(e)&&n.push(e)}),{keywords:n.sort(function(e,t){return t.length-e.length}),length:n.length}}},{key:"isNumeric",value:function(e){return Number(parseFloat(e))==e}},{key:"checkRanges",value:function(e){var t=this;if(!Array.isArray(e)||"[object Object]"!==Object.prototype.toString.call(e[0]))return this.log("markRanges() will only accept an array of objects"),this.opt.noMatch(e),[];var n=[],r=0;return e.sort(function(e,t){return e.start-t.start}).forEach(function(e){var i=t.callNoMatchOnInvalidRanges(e,r),o=i.start,a=i.end;i.valid&&(e.start=o,e.length=a-o,n.push(e),r=a)}),n}},{key:"callNoMatchOnInvalidRanges",value:function(e,t){var n=void 0,r=void 0,i=!1;return e&&void 0!==e.start?(r=(n=parseInt(e.start,10))+parseInt(e.length,10),this.isNumeric(e.start)&&this.isNumeric(e.length)&&r-t>0&&r-n>0?i=!0:(this.log("Ignoring invalid or overlapping range: "+JSON.stringify(e)),this.opt.noMatch(e))):(this.log("Ignoring invalid range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:n,end:r,valid:i}}},{key:"checkWhitespaceRanges",value:function(e,t,n){var r=void 0,i=!0,o=n.length,a=t-o,s=parseInt(e.start,10)-a;return(r=(s=s>o?o:s)+parseInt(e.length,10))>o&&(r=o,this.log("End range automatically set to the max value of "+o)),s<0||r-s<0||s>o||r>o?(i=!1,this.log("Invalid range: "+JSON.stringify(e)),this.opt.noMatch(e)):""===n.substring(s,r).replace(/\s+/g,"")&&(i=!1,this.log("Skipping whitespace only range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:s,end:r,valid:i}}},{key:"getTextNodes",value:function(e){var t=this,n="",r=[];this.iterator.forEachNode(NodeFilter.SHOW_TEXT,function(e){r.push({start:n.length,end:(n+=e.textContent).length,node:e})},function(e){return t.matchesExclude(e.parentNode)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT},function(){e({value:n,nodes:r})})}},{key:"matchesExclude",value:function(e){return i.matches(e,this.opt.exclude.concat(["script","style","title","head","html"]))}},{key:"wrapRangeInTextNode",value:function(e,t,n){var r=this.opt.element?this.opt.element:"mark",i=e.splitText(t),o=i.splitText(n-t),a=document.createElement(r);return a.setAttribute("data-markjs","true"),this.opt.className&&a.setAttribute("class",this.opt.className),a.textContent=i.textContent,i.parentNode.replaceChild(a,i),o}},{key:"wrapRangeInMappedTextNode",value:function(e,t,n,r,i){var o=this;e.nodes.every(function(a,s){var c=e.nodes[s+1];if(void 0===c||c.start>t){if(!r(a.node))return!1;var u=t-a.start,l=(n>a.end?a.end:n)-a.start,h=e.value.substr(0,a.start),f=e.value.substr(l+a.start);if(a.node=o.wrapRangeInTextNode(a.node,u,l),e.value=h+f,e.nodes.forEach(function(t,n){n>=s&&(e.nodes[n].start>0&&n!==s&&(e.nodes[n].start-=l),e.nodes[n].end-=l)}),n-=l,i(a.node.previousSibling,a.start),!(n>a.end))return!1;t=a.end}return!0})}},{key:"wrapGroups",value:function(e,t,n,r){return r((e=this.wrapRangeInTextNode(e,t,t+n)).previousSibling),e}},{key:"separateGroups",value:function(e,t,n,r,i){for(var o=t.length,a=1;a-1&&r(t[a],e)&&(e=this.wrapGroups(e,s,t[a].length,i))}return e}},{key:"wrapMatches",value:function(e,t,n,r,i){var o=this,a=0===t?0:t+1;this.getTextNodes(function(t){t.nodes.forEach(function(t){t=t.node;for(var i=void 0;null!==(i=e.exec(t.textContent))&&""!==i[a];){if(o.opt.separateGroups)t=o.separateGroups(t,i,a,n,r);else{if(!n(i[a],t))continue;var s=i.index;if(0!==a)for(var c=1;c + + + + + Running with ASAN and UBSAN - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Running with ASAN/UBSAN

+

Enabling

+

When building though build.sh, pass the -b flag. This sets the cmake variable +FILAMENT_ENABLE_ASAN_UBSAN=ON which eventually passes "-fsanitize=address -fsanitize=undefined" +to all compile and link operations.

+

If building through CMake directly, or an IDE like CLion that doesn't use build.sh, instead pass +-DFILAMENT_ENABLE_ASAN_UBSAN=ON to cmake in order to get the same result.

+

Getting memory leak detection on Mac

+

Memory leak detection isn't enabled by default on MacOS. There are two issues to address, first is +using a version of clang that supports memory leak detection and second is enabling it at runtime.

+

The version of clang distributed by Apple (with a version like "Apple clang version 16.0.0") doesn't +currently support leak detection at all. Instead you will need to get or build a different LLVM, +such as the one distributed through homebrew and get CMake to use that instead.

+

Then during runtime you'll need to have the environment variable ASAN_OPTIONS include the option +detect_leaks=1. Multiple ASAN_OPTIONS values are concatenated with :.

+

Getting memory leak output in CLion

+

Setting variables

+

Under Settings | Build, Execution, Deployment | Dynamic Analysis Tools | Sanitizers there is an +ASAN Settings field that overrides whatever other ASAN_OPTIONS you might set elsewhere, so you +must use that instead of setting it through your Run/Debug Configuration.

+

To pass -DFILAMENT_ENABLE_ASAN_UBSAN=ON to CMake you'll want to create a new CMake Profile and +pass it as a CMake argument.

+

Avoiding losing output

+

CMake will consume ASAN output and display it through a separate "Sanitizers" tab. Unfortunately +certain leak detection errors that interrupt the executable seem to not show up in this tab, but are +still removed from the user-visible console output. If this is happening and you need to see the +unfiltered console output you'll need to go to Settings | Build, Execution, Deployment | Dynamic Analysis Tools | Sanitizers and uncheck "Use visual representation for Sanitizer's output".

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/notes/branching.html b/docs/notes/branching.html new file mode 100644 index 0000000000..ff2a32e868 --- /dev/null +++ b/docs/notes/branching.html @@ -0,0 +1,234 @@ + + + + + + Branching - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Branching

+

Filament branching strategy

+

Which branch do I open my PR against?

+

For normal development, open PRs against main. Once they're merged, no further action is necessary.

+

If you discover a bug with the latest release candidate, open a bug fix PR against the release +candidate branch (rc/1.9.0, for example). Once the PR is merged, decide whether it makes sense for +the fix to also go into main. If it was a temporary fix, simply make the correct fix in main as +you would any other change. If the fix is good for main as well, use git cherry-pick <sha> to +cherry-pick it into main.

+

If an immediate hotfix is needed on the release branch, open a PR against the release branch. Once +the PR is merged, decide whether the fix is temporary or permanent. If the fix was temporary, make +the correct fix in both the next release candidate branch and main. If the fix is good, use git cherry-pick <sha> to cherry-pick it into the relase candidate branch and main.

+

What consitutes a bug?

+

Only bug fix PRs should be opened against the release candidate branch.

+

Bugs are defined as one of the following introduced since the prior release:

+
    +
  • crashes
  • +
  • rendering issues
  • +
  • unintentional binary size increases
  • +
  • unintentional public API changes
  • +
+

For example, a long-standing crash just recently discovered would not necessitate a bug fix PR.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/notes/coverage.html b/docs/notes/coverage.html new file mode 100644 index 0000000000..4328658079 --- /dev/null +++ b/docs/notes/coverage.html @@ -0,0 +1,295 @@ + + + + + + Code coverage analysis - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Generating Backend Code Coverage

+

Code coverage analysis helps visualize which parts of the backend are exercised by backend tests. +This guide outlines the process for generating an HTML coverage report for Filament's backend on +macOS.

+

1. Prerequisites: Install Clang and LLVM tools

+

You'll need a recent version of Clang and its corresponding LLVM tools for code coverage. +You can install these using Homebrew or MacPorts.

+

Using Homebrew

+

Install the llvm package:

+
brew install llvm
+
+

This typically installs the tools in a location like /usr/local/opt/llvm/bin. You may need to add +this to your PATH environment variable.

+

Using MacPorts

+

Install a specific version of Clang (e.g., version 18):

+
sudo port install clang-18
+
+

MacPorts often adds version suffixes to the tool names (e.g., llvm-cov-mp-18).

+

Required Tools

+

Ensure you can locate the following tools from your installation:

+
    +
  • clang and clang++ (The C/C++ compilers)
  • +
  • llvm-profdata (For merging coverage data)
  • +
  • llvm-cov (For generating reports)
  • +
+

The rest of this guide assumes your tools are in your PATH. If not, you'll need to use the full +path to each executable.

+

2. Build Filament with Coverage Enabled

+

Compile the backend_test_mac target with coverage instrumentation. This is done by setting the +CC and CXX environment variables to point to your Clang compiler and using the -V flag in the +build script.

+
CC=clang CXX=clang++ ./build.sh -V -p desktop debug backend_test_mac
+
+

If your Clang executables aren't in your PATH or have version suffixes, provide the full name or +path (e.g., CC=/opt/local/bin/clang CXX=/opt/local/bin/clang++).

+

3. Run the Backend Tests

+

Running the test suite will generate the raw coverage data needed for the report.

+
    +
  1. +

    Navigate to the build output directory:

    +
    cd out/cmake-debug/filament/backend
    +
    +
  2. +
  3. +

    Run the tests for a specific backend (e.g., Metal):

    +
    ./backend_test_mac --api metal
    +
    +
  4. +
+

This command creates a default.profraw file in the current directory, which contains the raw +execution profile data.

+

4. Generate the Coverage Report

+

Finally, process the raw data and generate an HTML report.

+
    +
  1. +

    Merge the raw profile data into a single file using llvm-profdata.

    +
    llvm-profdata merge -sparse default.profraw -o filament.profdata
    +
    +

    Remember to use the version-specific tool name if required (e.g., llvm-profdata-mp-18).

    +
  2. +
  3. +

    Generate the HTML report using llvm-cov. This command creates a report for the entire +backend_test_mac executable.

    +
    llvm-cov show ./backend_test_mac \
    +  -instr-profile=filament.profdata \
    +  -format=html \
    +  -show-line-counts-or-regions > coverage.html
    +
    +

    To view coverage for a specific source file, add its path at the end of the command:

    +
    llvm-cov show ./backend_test_mac \
    +  -instr-profile=filament.profdata \
    +  -format=html \
    +  -show-line-counts-or-regions \
    +  -- ../../../../filament/backend/src/metal/MetalDriver.mm > coverage.html
    +
    +
  4. +
  5. +

    Open the report in your browser:

    +
    open coverage.html
    +
    +
  6. +
+

In the report, code paths that were not executed during the test run will be highlighted in red.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/notes/debugging.html b/docs/notes/debugging.html new file mode 100644 index 0000000000..2b3ded2b6b --- /dev/null +++ b/docs/notes/debugging.html @@ -0,0 +1,214 @@ + + + + + + Debugging - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Debugging

+

Helpful documents for specific debugging needs.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/notes/framegraph.html b/docs/notes/framegraph.html new file mode 100644 index 0000000000..bc2fed0cea --- /dev/null +++ b/docs/notes/framegraph.html @@ -0,0 +1,380 @@ + + + + + + Framegraph - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

FrameGraph

+

FrameGraph is a framework within Filament for computing resources needed to +render a frame. The framework enables declaring dependencies between resources.

+

For example, when rendering shadows, we would need to first compute and store the +shadow map into a texture resource, and then the later color pass would then +sample that texture to attenuate the final output color. That creates a +dependency on the shadow map from the color pass. Filament uses FrameGraph to +declare that dependency.

+

Details

+

Dependency Graph

+

The core of this framework is +a class that defines a dependency graph — that is, the class +defines nodes and connections between nodes. This class makes assumptions about +the types of its nodes. Like many other classes within Filament, this class is +without virtual function declaration to avoid paying the cost of virtual calls.

+

This class has additional functions to detect whether there is a cycle in the +graph, and it is able to cull unreachable nodes.

+

FrameGraph

+

A frame graph consists of two types of nodes

+
    +
  • Resource +
      +
    • This represents a generic resource such as a texture
    • +
    • 90% of the time, this is a texture.
    • +
    +
  • +
  • Pass +
      +
    • This represents a "computation/rendering process"
    • +
    • It takes a set of resources
    • +
    • It outputs a set of resources
    • +
    +
  • +
+

Edges can be created in the following three directions:

+
    +
  • Resource → Pass = A read
  • +
  • Pass → Resource = A write
  • +
  • Resource → Resource = A resource/subresource relationship.
  • +
+

An example

+

To better understand FrameGraph, we consider the following graphical +representation of a real graph. In this graph, blue nodes denote "Resources" and +orange nodes denote "Passes."

+

Sample frame graph

+

In this graph, we see that the "Color Pass" takes as input the "Shadowmap", +which has edges going into it, meaning that it's a texture array. The output of +the "Color Pass" are "viewRenderTarget" and "Depth Buffer."

+

Note that there is an outgoing edge from "viewRenderTarget", where the color +buffer will be used as input in the post-processing passes. But since "Depth +Buffer" is not relevant to the rest of the rendering, it does not have an +outgoing edge.

+

Since the graph is guaranteed to be acyclic, we can produce a +dependency-respecting ordering of the nodes by traversal of the graph (e.g. +topological sort).

+

Example code

+

We take a snippet of in production code to look through the details of building +a graph.

+
struct StructurePassData {
+    FrameGraphId<FrameGraphTexture> depth;
+    FrameGraphId<FrameGraphTexture> picking;
+};
+
+...
+
+// generate depth pass at the requested resolution
+auto& structurePass = fg.addPass<StructurePassData>("Structure Pass",
+        [&](FrameGraph::Builder& builder, auto& data) {
+            bool const isES2 = mEngine.getDriverApi().getFeatureLevel() == FeatureLevel::FEATURE_LEVEL_0;
+            data.depth = builder.createTexture("Structure Buffer", {
+                    .width = width, .height = height,
+                    .levels = uint8_t(levelCount),
+                    .format = isES2 ? TextureFormat::DEPTH24 : TextureFormat::DEPTH32F });
+
+            // workaround: since we have levels, this implies SAMPLEABLE (because of the gl
+            // backend, which implements non-sampleables with renderbuffers, which don't have levels).
+            // (should the gl driver revert to textures, in that case?)
+            data.depth = builder.write(data.depth,
+                    FrameGraphTexture::Usage::DEPTH_ATTACHMENT | FrameGraphTexture::Usage::SAMPLEABLE);
+
+            if (config.picking) {
+                data.picking = builder.createTexture("Picking Buffer", {
+                        .width = width, .height = height,
+                        .format = isES2 ? TextureFormat::RGBA8 : TextureFormat::RG32F });
+
+                data.picking = builder.write(data.picking,
+                        FrameGraphTexture::Usage::COLOR_ATTACHMENT);
+            }
+
+            builder.declareRenderPass("Structure Target", {
+                    .attachments = { .color = { data.picking }, .depth = data.depth },
+                    .clearFlags = TargetBufferFlags::COLOR0 | TargetBufferFlags::DEPTH
+            });
+        },
+        [=, renderPass = pass](FrameGraphResources const& resources,
+                auto const&, DriverApi&) mutable {
+            Variant structureVariant(Variant::DEPTH_VARIANT);
+            structureVariant.setPicking(config.picking);
+
+            auto out = resources.getRenderPassInfo();
+            renderPass.setRenderFlags(structureRenderFlags);
+            renderPass.setVariant(structureVariant);
+            renderPass.appendCommands(mEngine, RenderPass::CommandTypeFlags::SSAO);
+            renderPass.sortCommands(mEngine);
+            renderPass.execute(mEngine, resources.getPassName(), out.target, out.params);
+        }
+);
+
+
+

The addPass method creates a node and it take in two lambda functions as its +parameter. The first lambda sets up the resources that will be used in the +execution of the Pass. This lambda is executed immediately and synchronously when +addPass is called. The second lambda is the actual execution of the pass; it is +executed when the graph has been completed and is traversed.

+

What does it do?

+

In the above, we see through a graph and code what a frame graph looks like and +how to build it. We provide here a more detailed description of what it does:

+
    +
  • Manages the lifetime of the resources +
      +
    • Know how the resources are allocated, when it is used, and when it can +be freed
    • +
    +
  • +
  • Calculates the usage bit of the texture resource +
      +
    • The usage bit is used to indicate what the resources are used for: for +example, will it be blitted to or sampled from?
    • +
    +
  • +
  • Calculates the load/store bits of the rendertargets within a renderpass. +
      +
    • For example, if we are rendering into a texture, we would want to mark +it with the bit "keep" as oppose to "discard".
    • +
    +
  • +
+

Additional details

+
    +
  • In a previous version of FrameGraph, there were only edges between Resource +and Pass. For example, a Pass and Pass edge would not make logical sense. +The following iteration, allowed for edges between two Resource nodes to +indicate that one is a subresource of another (i.e. a layer in a mip-mapped +texture).
  • +
  • There are two extra features of FrameGraph that are important but has a lot +subtlety, and, incidentally, their inclusion added great complexity to the +implementation +
      +
    • Importing/exporting resources outside of the graph +
        +
      • In most cases, the graph and its resources are "alive" for only for +a frame.
      • +
      • For techniques like TAA (Temporal Anti-aliasing), we need to be able +to import past output into the current FrameGraph
      • +
      +
    • +
    +
  • +
  • Future Work +
      +
    • For CPU only passes, explore multi-threading and re-ordering of the Pass +nodes
    • +
    • A graphical debugger for online debugging session in the spirit of +matdbg.
    • +
    +
  • +
  • "RenderGraph" might be a more fitting name for this framework.
  • +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/notes/index.html b/docs/notes/index.html new file mode 100644 index 0000000000..06e27ae387 --- /dev/null +++ b/docs/notes/index.html @@ -0,0 +1,214 @@ + + + + + + Technical Notes - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Technical Notes

+

Documents that pertain to components and use cases of the project.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/notes/instruments.html b/docs/notes/instruments.html new file mode 100644 index 0000000000..1a9be89e76 --- /dev/null +++ b/docs/notes/instruments.html @@ -0,0 +1,240 @@ + + + + + + Using Instruments on macOS - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Using Instruments on macOS

+

When running a binary under Instruments on macOS, you may run into the following issue when +launching or attaching to an executable:

+
Failed to gain authorization
+Recovery Suggestion: Target binary needs to be debuggable and signed with 'get-task-allow'
+
+

This is a security precaution; the solution is to code sign the binary with the +com.apple.security.get-task-allow entitlement.

+
    +
  1. Create an entitlements.plist file with the following contents:
  2. +
+
<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+    <key>com.apple.security.get-task-allow</key>
+    <true/>
+</dict>
+</plist>
+
+
    +
  1. Run the following command:
  2. +
+
codesign -s - --entitlements entitlements.plist <binary>
+
+

Replace <binary> with the name of the binary, for example: out/cmake-debug/samples/gltf_viewer.

+

Afterwards, you should be able to successfully launch and attach to the executable using +Instruments.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/notes/libs.html b/docs/notes/libs.html new file mode 100644 index 0000000000..774fc18de0 --- /dev/null +++ b/docs/notes/libs.html @@ -0,0 +1,214 @@ + + + + + + Libraries - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Libraries

+

Collection of README.md from the /libs folder.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/notes/material_properties.html b/docs/notes/material_properties.html new file mode 100644 index 0000000000..21ba0171f6 --- /dev/null +++ b/docs/notes/material_properties.html @@ -0,0 +1,713 @@ + + + + + + Material Properties - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

CRAFTING PHYSICALLY-BASED MATERIALS

+

BASE COLOR/sRGB

+

Defines the perceived color of an object (sometimes called albedo). More precisely:

+

→ the diffuse color of a non-metallic object
+→ the specular color of a metallic object

+

BASE COLOR LUMINOSITY

+
+
+
+
 
+
+
+
+
Non-metal range
+
10 - 240
+
+
+
+
 
+
+
+
 
+
+
+
+
Metal range
+
170 - 255
+
+
+
+
+
+

METALLIC SAMPLES

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Silver Aluminum Platinum Iron Titanium Copper Gold Brass
250,249,245 244,245,245 214,209,200 192,189,186 206,200,194 251,216,184 255,220,157 244,228,173
#faf9f5 #faf5f5 #d6d1c8 #c0bdba #cec8c2 #fbd8b8 #fedc9d #f4e4ad
+
+

NON-METALLIC SAMPLES

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Coal Rubber Mud Wood Vegetation Brick Sand Concrete
50,50,50 53,53,53 85,61,49 135,92,60 123,130,78 148,125,117 177,168,132 192,191,187
#323232 #353535 #553d31 #875c3c #7b824e #947d75 #b1a884 #c0bfbb
+
+

METALLIC/GRAYSCALE

+

Defines whether a surface is dielectric (0.0, non-metal) or conductor (1.0, metal). +Pure, unweathered surfaces are rare and will be either 0.0 or 1.0. +Rust is not a conductor.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
0.00.10.20.30.40.50.60.70.80.91.0
NON-METAL/DIELECTRICMETAL/CONDUCTOR
+
+

ROUGHNESS/GRAYSCALE

+

Defines the perceived smoothness (0.0) or roughness (1.0). +It is sometimes called glossiness.

+

NON-METALLIC

+
+ + + + + + + + + + + + + + + + + + + + + +
0.00.10.20.30.40.50.60.70.80.91.0
+
+

METALLIC

+
+ + + + + + + + + + + + + + + + + + + + + +
0.00.10.20.30.40.50.60.70.80.91.0
+
+

REFLECTANCE/GRAYSCALE

+

Specular intensity for non-metals. The default is 0.5, or 4% reflectance.

+
+ + + + + + + + + + + + + + + + + + + + + +
0.00.10.20.30.40.50.60.70.80.91.0
+
+
+
+
+
+
+
No real-world material
+
+
+
+
+ 2% +
+
+
+
+
Common dielectrics
+
+
+
+
+ 5% +
+
+
+
+
+ 16% +
+
+
+ Gemstones +
+
+
 
+
+
+
+
All dielectrics
+
+
+
+
+
+

SAMPLES

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Water Glass Liquids Defaults Others Ruby Diamond Gemstones
90,90,90 119,119,119 127,127,127 180,180,180 255,255,255
2% 3.5% 2% to 4% 4% 2% to 5% 8% 16% 5% to 16%
+
+

CLEAR COAT/GRAYSCALE

+

Strength of the clear coat layer on top of a base dielectric or conductor layer. +The clear coat layer will commonly be set to 0.0 or 1.0. +This layer has a fixed index of refraction of 1.5.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
0.00.10.20.30.40.50.60.70.80.91.0
NO CLEAR COATFULL CLEAR COAT
+
+

CLEAR COAT ROUGHNESS/GRAYSCALE

+

Defines the perceived smoothness (0.0) or roughness (1.0) of the clear coat layer. +It is sometimes called glossiness. +This may affect the roughness of the base layer.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
0.00.10.20.30.40.50.60.70.80.91.0
GLOSSY CLEAR COATROUGH CLEAR COAT
+
+

ANISOTROPY/GRAYSCALE

+

Defines whether the material appearance is directionally dependent, that is isotropic (0.0) +or anisotropic (1.0). Brushed metals are anisotropic. +Values can be negative to change the orientation of the specular reflections.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
0.00.10.20.30.40.50.60.70.80.91.0
ISOTROPICANISOTROPIC
+
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/notes/metal_debugging.html b/docs/notes/metal_debugging.html new file mode 100644 index 0000000000..c530a4c3c3 --- /dev/null +++ b/docs/notes/metal_debugging.html @@ -0,0 +1,239 @@ + + + + + + Metal - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Debugging Metal

+

Enable Metal Validation

+

To enable the Metal validation layers when running a sample through the command-line, set the +following environment variable:

+
export METAL_DEVICE_WRAPPER_TYPE=1
+
+

You should then see the following output when running a sample with the Metal backend:

+
2020-10-13 18:01:44.101 gltf_viewer[73303:4946828] Metal API Validation Enabled
+
+

Metal Frame Capture from gltf_viewer

+

To capture Metal frames from within gltf_viewer:

+

1. Create an Info.plist file

+

Create an Info.plist file in the same directory as gltf_viewer (cmake/samples). Set its +contents to:

+
<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+    <key>MetalCaptureEnabled</key>
+    <true/>
+</dict>
+</plist>
+
+

2. Capture a frame

+

Run gltf_viewer as normal, and hit the "Capture frame" button under the Debug menu. The captured +frame will be saved to filament.gputrace in the current working directory. This file can then be +opened with Xcode for inspection.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/notes/performance_analysis.html b/docs/notes/performance_analysis.html new file mode 100644 index 0000000000..09cd9ffde7 --- /dev/null +++ b/docs/notes/performance_analysis.html @@ -0,0 +1,352 @@ + + + + + + Performance analysis - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Performance Analysis

+

Android

+

Prerequisites

+
    +
  • Download and install Android GPU Inspector (AGI). See https://developer.android.com/agi.
  • +
+
+

Profiling

+
    +
  1. +

    Before profiling the application or analyzing the performance in a consistent way, ideally +the GPU frequency on the target hardware should get locked. In order to do this, you need to do the following:

    +
      +
    1. Ensure your device is OEM unlocked. If your phone is carrier-locked, you may need +to wait a period, such as 60 or 90 days after activation to be eligible for unlocking. +Some phones don't support this at all. You will need to enable developer options +(e.g. Settings > About Phone and tap the Build number 7 times). You need to go to +Settings > System > Developer options and toggle on "OEM unlocking".
    2. +
    3. Next, you need to unlock the phone. You will need to install Android SDK Platform +Tools on you computer, enable "USB debugging" in your phone's Settings > System > Developer options.
    4. +
    5. Connect the phone to the computer via a USB cable and run from the command line: +
      adb reboot bootloader
      +# once the phone is in bootloader mode, run the following to begin the unlocking
      +# process:
      +fastboot flashing unlock
      +
      +A warning will appear on your phone's screen. Use the volume buttons to navigate and +the power button to select the "Unlock the bootloader" option. The phone will perform +a factory data reset and reboot with an unlocked bootloader.
    6. +
    7. You would need to flash an image to the phone with root permissions, such as a *-userdebug or *-eng +build. One way to do this is with the Android Flash Tool. Connect the +tool to your device and find a build to flash ending in -userdebug or -eng. Once you have that +selected run Install build.
    8. +
    9. Shell into your device as root and configure the gpu frequency to be locked, e.g.: +
      adb shell
      +su
      +# navigate to the system GPU directory. this varies on different phones. One phone might have
      +# it at /sys/class/kgsl/kgsl-3d0 and another might be in something similar, maybe with "mali" instead
      +# of "kgsl". At the time of writing this for the device at hand it was /sys/devices/platform/1f000000.mali
      +cd /sys/devices/platform/1f000000.mali
      +# get the current available GPU governors and frequencies.
      +# note that some systems may have these at gpu_available_governors and gpu_available_frequencies, but
      +# the system at the time of writing this had available_governors and available_frequences
      +cat available_governors
      +cat available_frequencies
      +# depending on the governors, you may want to set it prioritize performance over other things like
      +# battery. Some systems allow you to do this with something like (although, for the device used at the
      +# time of writing this did not have an equivalent option):
      +echo performance > gpu_governor
      +# finally, lock your frequency in, usually to something high like 897 MHz. Some systems may have you
      +# pipe the value to gpu_min_freq and gpu_max_freq, but the system used at the time of writing this had:
      +echo 940000 > hint_min_freq
      +echo 940000 > hint_max_freq
      +# you can typically verify the GPU is running at that frequency consistently by running something like
      +# the following a few times over time, which should show the frequency you want to lock the device to:
      +cat cur_freq
      +
      +
    10. +
    11. You may need to re-apply the hint_min_freq just before starting the profiling trace and check before and after +that the frequency remained at the value expected. Some systems may adjust the frequency on you, but you +may want to ensure the frequency remains the same through the analysis.
    12. +
    13. The GPU frequency settings should be undone after restarting the device, but after you have done your app profiling, +you can revert the state of the device, such as the OS build image, back to the way you had it +initially, as needed.
    14. +
    +
  2. +
  3. +

    Build a release build of Filament with the applicable backend(s) enabled +(+ any special flags for enabling sys strace. Nothing special is needed for Vulkan or WebGPU aside from +building a release build with no flags) (debug builds for this are useless)

    +
    # the following command assumes you are in the root filament directory
    +# and ANDROID_HOME is exported (and possibly also CC and CXX on linux as needed)
    +#
    +# NOTE: to build with WebGPU support you need to explicitly include the -W flag
    +# (it doesn't get compiled in by default), e.g.:
    +# ./build.sh -W -p android,desktop -i release
    +#
    +# Note that you can speed this up a bit (and reduce disk space usage) by limiting the target to just 
    +# the ABI you plan on testing with the -q flag, e.g. -q arm64-v8a. If you do this, you
    +# will need to update the android/gradle.properties file to specify the ABI(s) you are targeting
    +# with the com.google.android.filament.abis property.
    +# Thus, a build command that would target BOTH Vulkan AND WebGPU AND only target the ARM64 ABI would look something
    +# like:
    +# ./build.sh -W -q arm64-v8a -p android,desktop -i release
    +./build.sh -p android,desktop -i release
    +
    +
  4. +
  5. +

    Connect your Android device to your computer via a USB cord with USB debugging enabled and configure +the system property to default to the desired backend, e.g. +(to determine how these numbers map to the backends, see the enum class Backend +definition in filament/filament/backend/include/backend/DriverEnums.h):

    +
    # to set the backend to Vulkan: 
    +adb shell setprop debug.filament.backend 2
    +# to set the backend to WebGPU:
    +adb shell setprop debug.filament.backend 4
    +# to view the current property:
    +adb shell getprop debug.filament.backend
    +
    +
  6. +
  7. +

    Build and run a sample, e.g. sample-gltf-viewer, on your Android device +(Android Studio recommended).

    +
  8. +
  9. +

    Run AGI and follow instructions to profile the app/system +with trace capture(s). See https://developer.android.com/agi/start for more details to +get started.

    +
      +
    • We typically only run "Capture System Profiler trace" (not necessarily "Capture Frame Profiler trace")
    • +
    • When configuring the trace: +
        +
      • For both the WebGPU and Vulkan backends configure the profiler for the Vulkan API +(since WebGPU should be using Vulkan under the hood as well)
      • +
      • Running for ~1 seconds should suffice
      • +
      • Hit the "Configure" button in Trace objects, select "Switch to advanced mode" and add: +
        data_sources {
        +  config {
        +    name: "track_event"
        +    track_event_config {
        +      disabled_categories: "*"
        +      enabled_categories: "filament/filament"
        +      enabled_categories: "filament/jobsystem"
        +      enabled_categories: "filament/gltfio"
        +    }
        +  }
        +}
        +
        +
      • +
      +
    • +
    • One you open the trace, zoom into a series of frames to get a sense of generally how long they typically take +(use W, S, A and D keys and mouse wheel for navigation) and find a representative one. +We are most interested in the performance of the +FEngine::loop thread, how long it takes, overlap in activities/processes/commands, reduction in queue submissions, +etc. Similarly, we can view GPU timeline as it relates to that. We want to see overlapping +shader invocations and non-interrupted fragment shader runs.
    • +
    +
  10. +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/notes/release_guide.html b/docs/notes/release_guide.html new file mode 100644 index 0000000000..61e3e800e9 --- /dev/null +++ b/docs/notes/release_guide.html @@ -0,0 +1,299 @@ + + + + + + Release Guide - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Filament Release Guide

+

This guide makes use of some "environment variables":

+
    +
  • $RELEASE = the new version of Filament we are releasing today. (e.g., 1.9.3)
  • +
  • $NEXT_RELEASE = the version we plan to release next week (e.g., 1.9.4)
  • +
+

Before starting, ensure that each of these branches is up-to-date with origin:

+
    +
  • release
  • +
  • rc/$RELEASE
  • +
  • main
  • +
+

0. Check versions.

+

Make sure the rc/$RELEASE branch has the correct Filament version. It should have the version +corresponding to its name, $RELEASE.

+

Make sure MATERIAL_VERSION has been bumped to a new version if this is a MAJOR or MINOR release +(first two version numbers).

+

1. Bump Filament versions on main to $RELEASE.

+

Checkout main and run the following command to bump Filament's version to $RELEASE:

+
build/common/bump-version.sh $RELEASE
+
+

Commit changes to main with the title:

+
Release Filament $RELEASE
+
+

Do not push to origin yet.

+

2. Update RELEASE_NOTES.md on main.

+

Create a new header in RELEASE_NOTES.md for $NEXT_RELEASE. Copy the release notes in +NEW_RELEASE_NOTES.md to RELEASE_NOTES.md under the new header. Clear NEW_RELEASE_NOTES.md.

+

Amend these changes to the "Release Filament $RELEASE" commit.

+
git add -u
+git commit --amend --no-edit
+
+

3. Run release script.

+
build/common/release.sh rc/$RELEASE rc/$NEXT_RELEASE
+
+

This script will merge rc/$RELEASE into release, delete the rc branch, and create a new rc +branch called rc/$NEXT_RELEASE. Verify that everything looks okay locally.

+

4. Push the release branch.

+
git push origin release
+
+

5. Create the GitHub release.

+

Use the GitHub UI to create a GitHub release corresponding to $RELEASE version. +Make sure the target is set to the release branch.

+

6. Delete the old rc branch (optional).

+

This step is optional. The old rc branch may be left alive for a few weeks for posterity.

+
git push origin --delete rc/$RELEASE
+
+

7. Bump the version on the new rc branch to $NEXT_RELEASE.

+
git checkout rc/$NEXT_RELEASE
+build/common/bump-version.sh $NEXT_RELEASE
+
+

Commit the changes to rc/$NEXT_RELEASE with the title:

+
Bump version to $NEXT_RELEASE
+
+

8. Push main.

+
git push origin main
+
+

9. Push the new rc branch.

+
git push origin -u rc/$NEXT_RELEASE
+
+

10. Rebuild the GitHub release (if failed).

+

Sometimes the GitHub release job will fail. In this case, you can manually re-run the release job.

+

Remove any assets uploaded to the release (if needed).

+

For example, if rebuilding the Mac release, ensure that the filament-<version>-mac.tgz artifact +is removed from the release assets.

+

Update the release branch (if needed).

+

If you need to add one or more new commits to the release, perform the following:

+

First, push the new commit(s) to the release branch.

+

Then, with the release branch checked out with the new commit(s), run

+
git tag -f -a <release tagname>
+git push origin -f <release tagname>
+
+

This will update and force push the tag.

+

Re-run the GitHub release workflow

+

Navigate to Filament's release +workflow. Hit the Run workflow +dropdown. Modify Platform to build and Release tag to build, then hit Run workflow. This will +initiate a new release run.

+

11. Kick off the npm and CocoaPods release jobs

+

Navigate to Filament's npm deploy +workflow. +Hit the Run workflow dropdown. Modify Release tag to deploy to the tag corresponding to this +release (for example, v1.42.2).

+

Navigate to Filament's CocoaPods deploy +workflow. +Hit the Run workflow dropdown. Modify Release tag to deploy to the tag corresponding to this +release (for example, v1.42.2).

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/notes/spirv_debugging.html b/docs/notes/spirv_debugging.html new file mode 100644 index 0000000000..a3cbbff52c --- /dev/null +++ b/docs/notes/spirv_debugging.html @@ -0,0 +1,367 @@ + + + + + + SPIR-V - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Investigating SPIRV-Cross / SPIRV-Tools issues

+

There are 4 repositories at play here:

+ +

Typically, the bug is present either in spirv-tools or spirv-cross.

+

Build and install command-line tools on PATH

+

The goal is to replicate the bug outside of Filament, so we're going to use command-line versions of +the SPIRV tools.

+

Clone and build each repo

+

Note: Filament checks-out versions of these repositories inside third_party/; however, I've found +it easiser to check out fresh copies separately so I can simply git pull to get the latest +versions. Furthermore, Filament has modified some of these repositories locally for its own use +case. Checking them out separately "proves" that the issue isn't Filament-specific.

+
git clone git@github.com:KhronosGroup/SPIRV-Tools.git
+git clone git@github.com:KhronosGroup/SPIRV-Cross.git
+git clone git@github.com:KhronosGroup/glslang.git
+git clone git@github.com:KhronosGroup/SPIRV-Headers.git SPIRV-Tools/external/SPIRV-Headers
+
+cd SPIRV-Tools/
+mkdir build && cmake . -G Ninja -B build
+ninja -C build
+cd ..
+
+cd SPIRV-Cross/
+mkdir build && cmake . -G Ninja -B build
+ninja -C build
+cd ..
+
+cd glslang/
+mkdir build && cmake . -G Ninja -B build
+ninja -C build
+cd ..
+
+

Add directories to PATH

+
export PATH=`pwd`/SPIRV-Tools/build/tools:$PATH
+export PATH=`pwd`/glslang/build/StandAlone:$PATH
+export PATH=`pwd`/spirv-cross/build:$PATH
+
+

Ensure the following tools now exist on your PATH:

+
    +
  1. glslangValidator
  2. +
  3. spiv-opt
  4. +
  5. spirv-val
  6. +
  7. spirv-cross
  8. +
+

Isolate the problematic GLSL shader

+

First determine the Filament material and variant that causes the problem.

+

What we want is the "raw" GLSL version of the shader, before any optimizations / cross-compilation +happens.

+

We can use the --save-raw-variants debug flag in matc to export each GLSL +shader to a file. For example:

+
matc --save-raw-variants --optimize-size --variant-filter fog,ssr,vsm,stereo \
+        -a all -p all -o mymaterial.filamat mymaterial.mat
+
+

Files will be named like mymaterial_0x05.frag or mymaterial_0x05.vert.

+

Note that gltfio material "templates" first go through a build step. After building gltfio, the +gltfio Filament materials are output to:

+
out/cmake-release/libs/gltfio/*.mat
+
+

One of these materials can be compiled with the following command:

+
matc \
+    -TCUSTOM_PARAMS="// no custom params" \
+    -TCUSTOM_VERTEX="// no custom vertex" \
+    -TCUSTOM_FRAGMENT="// no custom fragment" \
+    -TDOUBLESIDED=false \
+    -TTRANSPARENCY=default \
+    -TSHADINGMODEL=unlit \
+    -TBLENDING=opaque \
+    --platform mobile --api metal -o temp.filamat \
+    unlit_opaque.mat
+
+

Reproduce the compilation error

+

The goal is to generate a .spv file that doesn't pass validation (through the spirv-val tool).

+

Reproducing the error usually involves a few steps:

+
    +
  1. Compile the raw GLSL shader into SPIR-V.
  2. +
+
glslangValidator -V -o unoptimized.spv in.frag
+
+
    +
  1. Optimize for performance.
  2. +
+
spirv-opt -Oconfig=optimizations.cfg unoptimized.spv -o optimized.spv
+
+

See optimizations.cfg for a template. This file should contain the same list of optimizations that +Filament employs. This should match the same optimizations specified in GLSLPostProcessor, for +example, GLSLPostProcessor::registerPerformancePasses or GLSLPostProcessor::registerSizePasses.

+
    +
  1. For shaders targeting Metal, convert relaxed ops to half.
  2. +
+
spirv-opt \
+    --convert-relaxed-to-half \
+    --simplify-instructions \
+    --redundancy-elimination \
+    --eliminate-dead-code-aggressive \
+    optimized.spv \
+    -o half.spv
+
+
    +
  1. Finally, validate the final SPIR-V.
  2. +
+
spirv-val half.spv
+
+
    +
  1. Sometimes validation will still pass, but still generate invalid shaders after cross-compiling. +In these cases, you'll need to cross compile to the target language and manually pick out errors +in the generated shader.
  2. +
+
# for OpenGL
+spirv-cross optimized.spv > optimized.frag
+
+# for OpenGL ES
+spirv-cross --es optimized.spv > optimized.frag
+
+# for MSL
+spirv-cross --msl optimized.spv > optimized.metal
+
+

To invoke Apple's compiler to compile MSL, you can run:

+
xcrun -sdk macosx metal -c optimized.metal -o /dev/null
+
+

Clean up the shader for a bug report

+

These commands will run the preprocessor only on in.frag, and remove any empty lines.

+
glslangValidator -E in.frag > preprocessed.frag
+sed '/^$/d' preprocessed.frag > preprocessed_small.frag
+
+

You can also run clang-format on the preprocessed shader to make it easier to read:

+
clang-format -i preprocessed_small.frag
+
+

I always try to "whittle down" the shader to a smaller version that still reproduces the error. This +might make it a bit easier on the Khronos team to diagnose the issue. I typically follow these steps +in a loop until I'm satisfied:

+
    +
  1. Delete an unnecessary part of the shader
  2. +
  3. Run the steps to reproduce the error
  4. +
  5. If the error still reproduces, repeat
  6. +
  7. Otherwise, undo the change and make a smaller change
  8. +
+

There's also a Reducer tool that's part of +SPIRV-Tools which can be used to automate these steps. I haven't experimented much with this, but it +seems promising.

+

Submit an Issue with the relevant Khronos repository

+

See some example issues that have been filed in the past:

+
    +
  • https://github.com/KhronosGroup/SPIRV-Cross/issues/1935
  • +
  • https://github.com/KhronosGroup/SPIRV-Cross/issues/1088
  • +
  • https://github.com/KhronosGroup/SPIRV-Cross/issues/1026
  • +
  • https://github.com/KhronosGroup/SPIRV-Tools/issues/4452
  • +
  • https://github.com/KhronosGroup/SPIRV-Tools/issues/3406
  • +
  • https://github.com/KhronosGroup/SPIRV-Tools/issues/3099
  • +
  • https://github.com/KhronosGroup/SPIRV-Tools/issues/5044
  • +
+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/notes/spirv_debugging_optimizations.cfg b/docs/notes/spirv_debugging_optimizations.cfg new file mode 100644 index 0000000000..b07a1d8e31 --- /dev/null +++ b/docs/notes/spirv_debugging_optimizations.cfg @@ -0,0 +1,33 @@ +--wrap-opkill +--eliminate-dead-branches +--merge-return +--inline-entry-points-exhaustive +--eliminate-dead-functions +--private-to-local +--scalar-replacement=0 +--ssa-rewrite +--ccp +--loop-unroll +--eliminate-dead-branches +--simplify-instructions +--scalar-replacement=0 +--eliminate-local-single-store +--if-conversion +--simplify-instructions +--eliminate-dead-code-aggressive +--eliminate-dead-branches +--merge-blocks +--convert-local-access-chains +--eliminate-local-single-block +--eliminate-dead-code-aggressive +--copy-propagate-arrays +--vector-dce +--eliminate-dead-inserts +--eliminate-dead-members +--eliminate-local-single-store +--merge-blocks +--ssa-rewrite +--redundancy-elimination +--simplify-instructions +--eliminate-dead-code-aggressive +--cfg-cleanup diff --git a/docs/notes/tools.html b/docs/notes/tools.html new file mode 100644 index 0000000000..44c112e1d5 --- /dev/null +++ b/docs/notes/tools.html @@ -0,0 +1,214 @@ + + + + + + Tools - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Tools

+

Collection of README.md from the /tools folder.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/notes/versioning.html b/docs/notes/versioning.html new file mode 100644 index 0000000000..b5b8a4b9ef --- /dev/null +++ b/docs/notes/versioning.html @@ -0,0 +1,235 @@ + + + + + + Versioning - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Versioning

+

Filament uses a 3-number versioning scheme that superficially resembles a semantic +version but is actually more interesting because of our material system. Here +are the guidelines:

+
    +
  • Increment the most significant number only when making a non-backwards compatible API change, +or when introducing a major new API.
  • +
  • Increment the middle number only when making a non-backwards compatible change to the material +system. When this number gets bumped, users need to rebuild their mat files. Reset the middle +number to zero when the most significant number has been incremented.
  • +
  • Increment the least significant number each time a new release is published. Reset this number +to zero if one of the other two numbers have been incremented.
  • +
+

Material Versioning

+

Additionally, the Filament renderer and material compiler internally contain a standalone integer +called MATERIAL_VERSION, defined in MaterialEnums.h. This should be incremented every time we +change the middle number in the public-facing version.

+

When a material version mismatch is detected at run time, a panic is triggered, even in release +builds. Therefore we should increment this only when making a serious breaking change to the +material system (e.g. changing the size of a uniform block). Cosmetic shader changes usually do +not merit a change to the material version number.

+

Currently our material archives have two version chunks, one for "normal" materials and one for +post-process materials. However for now these two numbers must be set to the same value.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/notes/vulkan_debugging.html b/docs/notes/vulkan_debugging.html new file mode 100644 index 0000000000..005e49b5dc --- /dev/null +++ b/docs/notes/vulkan_debugging.html @@ -0,0 +1,222 @@ + + + + + + Vulkan - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Debugging Vulkan

+

Enable Validation Logs

+

Simply install the LunarG SDK (it's fast and easy), then make sure you've got the following +environment variables set up in your bashrc file. For example:

+
export VULKAN_SDK='/path_to_home/VulkanSDK/1.3.216.0/x86_64'
+export VK_LAYER_PATH="$VULKAN_SDK/etc/explicit_layer.d"
+export PATH="$VULKAN_SDK/bin:$PATH"
+
+

As long as you're running a debug build of Filament, you should now see extra debugging spew in your +console if there are any errors or performance issues being caught by validation.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/remote/filament.js b/docs/remote/filament.js new file mode 100644 index 0000000000..8aab831460 --- /dev/null +++ b/docs/remote/filament.js @@ -0,0 +1,1403 @@ + +var Filament = (() => { + var _scriptName = typeof document != 'undefined' ? document.currentScript?.src : undefined; + if (typeof __filename != 'undefined') _scriptName ||= __filename; + return ( +function(moduleArg = {}) { + var moduleRtn; + +var Module=Object.assign({},moduleArg);var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";if(ENVIRONMENT_IS_NODE){}var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var read_,readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("fs");var nodePath=require("path");scriptDirectory=__dirname+"/";read_=(filename,binary)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);return fs.readFileSync(filename,binary?undefined:"utf8")};readBinary=filename=>{var ret=read_(filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}return ret};readAsync=(filename,onload,onerror,binary=true)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);fs.readFile(filename,binary?undefined:"utf8",(err,data)=>{if(err)onerror(err);else onload(binary?data.buffer:data)})};if(!Module["thisProgram"]&&process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{read_=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=(url,onload,onerror)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}onerror()};xhr.onerror=onerror;xhr.send(null)}}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["quit"])quit_=Module["quit"];var wasmBinary;if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];var wasmMemory;var ABORT=false;var EXITSTATUS;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.init.initialized)FS.init();FS.ignorePermissions=false;TTY.init();callRuntimeCallbacks(__ATINIT__)}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function getUniqueRunDependency(id){return id}function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;EXITSTATUS=1;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);var isFileURI=filename=>filename.startsWith("file://");function findWasmBinary(){var f="filament.wasm";if(!isDataURI(f)){return locateFile(f)}return f}var wasmBinaryFile;function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary&&(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER)){if(typeof fetch=="function"&&!isFileURI(binaryFile)){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{if(!response["ok"]){throw`failed to load wasm binary file at '${binaryFile}'`}return response["arrayBuffer"]()}).catch(()=>getBinarySync(binaryFile))}else if(readAsync){return new Promise((resolve,reject)=>{readAsync(binaryFile,response=>resolve(new Uint8Array(response)),reject)})}}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{a:wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["sc"];updateMemoryViews();wasmTable=wasmExports["vc"];addOnInit(wasmExports["tc"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}if(!wasmBinaryFile)wasmBinaryFile=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}var tempDouble;var tempI64;var ASM_CONSTS={1728572:()=>{const options=window.filament_glOptions;const context=window.filament_glContext;const handle=GL.registerContext(context,options);window.filament_contextHandle=handle;GL.makeContextCurrent(handle)},1728786:()=>{const handle=window.filament_contextHandle;GL.makeContextCurrent(handle)},1728867:($0,$1,$2,$3,$4,$5)=>{const fn=Emval.toValue($0);fn({renderable:Emval.toValue($1),depth:$2,fragCoords:[$3,$4,$5]})}};var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var noExitRuntime=Module["noExitRuntime"]||true;function syscallGetVarargI(){var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret}var syscallGetVarargP=syscallGetVarargI;var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.substr(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.substr(0,dir.length-1)}return root+dir},basename:path=>{if(path==="/")return"/";path=PATH.normalize(path);path=path.replace(/\/$/,"");var lastSlash=path.lastIndexOf("/");if(lastSlash===-1)return path;return path.substr(lastSlash+1)},join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(typeof crypto=="object"&&typeof crypto["getRandomValues"]=="function"){return view=>crypto.getRandomValues(view)}else if(ENVIRONMENT_IS_NODE){try{var crypto_module=require("crypto");var randomFillSync=crypto_module["randomFillSync"];if(randomFillSync){return view=>crypto_module["randomFillSync"](view)}var randomBytes=crypto_module["randomBytes"];return view=>(view.set(randomBytes(view.byteLength)),view)}catch(e){}}abort("initRandomDevice")};var randomFill=view=>(randomFill=initRandomFill())(view);var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).substr(1);to=PATH_FS.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};function intArrayFromString(stringy,dontAddNull,length){var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array}var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else if(typeof window!="undefined"&&typeof window.prompt=="function"){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops:ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output,0));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output,0));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output&&tty.output.length>0){err(UTF8ArrayToString(tty.output,0));tty.output=[]}}}};var mmapAlloc=size=>{abort()};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16384|511,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,allocate:MEMFS.stream_ops.allocate,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.timestamp=Date.now();if(parent){parent.contents[name]=node;parent.timestamp=node.timestamp}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.timestamp);attr.mtime=new Date(node.timestamp);attr.ctime=new Date(node.timestamp);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){if(attr.mode!==undefined){node.mode=attr.mode}if(attr.timestamp!==undefined){node.timestamp=attr.timestamp}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){throw FS.genericErrors[44]},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){if(FS.isDir(old_node.mode)){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}}delete old_node.parent.contents[old_node.name];old_node.parent.timestamp=Date.now();old_node.name=new_name;new_dir.contents[new_name]=old_node;new_dir.timestamp=old_node.parent.timestamp;old_node.parent=new_dir},unlink(parent,name){delete parent.contents[name];parent.timestamp=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.timestamp=Date.now()},readdir(node){var entries=[".",".."];for(var key of Object.keys(node.contents)){entries.push(key)}return entries},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var dep=!noRunDep?getUniqueRunDependency(`al ${url}`):"";readAsync(url,arrayBuffer=>{onload(new Uint8Array(arrayBuffer));if(dep)removeRunDependency(dep)},event=>{if(onerror){onerror()}else{throw`Loading data file "${url}" failed.`}});if(dep)addRunDependency(dep)};var FS_createDataFile=(parent,name,fileData,canRead,canWrite,canOwn)=>{FS.createDataFile(parent,name,fileData,canRead,canWrite,canOwn)};var preloadPlugins=Module["preloadPlugins"]||[];var FS_handledByPreloadPlugin=(byteArray,fullname,finish,onerror)=>{if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(plugin=>{if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}onload?.();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{onerror?.();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url,processData,onerror)}else{processData(url)}};var FS_modeStringToFlags=str=>{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,ErrnoError:class{constructor(errno){this.name="ErrnoError";this.errno=errno}},genericErrors:{},filesystems:null,syncFSRequests:0,FSStream:class{constructor(){this.shared={}}get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.mounted=null;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.node_ops={};this.stream_ops={};this.rdev=rdev;this.readMode=292|73;this.writeMode=146}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){path=PATH_FS.resolve(path);if(!path)return{path:"",node:null};var defaults={follow_mount:true,recurse_count:0};opts=Object.assign(defaults,opts);if(opts.recurse_count>8){throw new FS.ErrnoError(32)}var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i40){throw new FS.ErrnoError(32)}}}}return{path:current_path,node:current}},getPath(node){var path;while(true){if(FS.isRoot(node)){var mount=node.mount.mountpoint;if(!path)return mount;return mount[mount.length-1]!=="/"?`${mount}/${path}`:mount+path}path=path?`${node.name}/${path}`:node.name;node=node.parent}},hashName(parentid,name){var hash=0;for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&512){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type:type,opts:opts,mountpoint:mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name||name==="."||name===".."){throw new FS.ErrnoError(28)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},create(path,mode){mode=mode!==undefined?mode:438;mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode){mode=mode!==undefined?mode:511;mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var i=0;iFS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomLeft=randomFill(randomBuffer).byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16384|511,73);node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path}};ret.parent=ret;return ret}};return node}},{},"/proc/self/fd")},createStandardStreams(){if(Module["stdin"]){FS.createDevice("/dev","stdin",Module["stdin"])}else{FS.symlink("/dev/tty","/dev/stdin")}if(Module["stdout"]){FS.createDevice("/dev","stdout",null,Module["stdout"])}else{FS.symlink("/dev/tty","/dev/stdout")}if(Module["stderr"]){FS.createDevice("/dev","stderr",null,Module["stderr"])}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){[44].forEach(code=>{FS.genericErrors[code]=new FS.ErrnoError(code);FS.genericErrors[code].stack=""});FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS:MEMFS}},init(input,output,error){FS.init.initialized=true;Module["stdin"]=input||Module["stdin"];Module["stdout"]=output||Module["stdout"];Module["stderr"]=error||Module["stderr"];FS.createStandardStreams()},quit(){FS.init.initialized=false;for(var i=0;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url:url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr:ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return PATH.join2(dir,path)},doStat(func,path,buf){var stat=func(path);HEAP32[buf>>2]=stat.dev;HEAP32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAP32[buf+12>>2]=stat.uid;HEAP32[buf+16>>2]=stat.gid;HEAP32[buf+20>>2]=stat.rdev;tempI64=[stat.size>>>0,(tempDouble=stat.size,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+24>>2]=tempI64[0],HEAP32[buf+28>>2]=tempI64[1];HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();tempI64=[Math.floor(atime/1e3)>>>0,(tempDouble=Math.floor(atime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+40>>2]=tempI64[0],HEAP32[buf+44>>2]=tempI64[1];HEAPU32[buf+48>>2]=atime%1e3*1e3;tempI64=[Math.floor(mtime/1e3)>>>0,(tempDouble=Math.floor(mtime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+56>>2]=tempI64[0],HEAP32[buf+60>>2]=tempI64[1];HEAPU32[buf+64>>2]=mtime%1e3*1e3;tempI64=[Math.floor(ctime/1e3)>>>0,(tempDouble=Math.floor(ctime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+72>>2]=tempI64[0],HEAP32[buf+76>>2]=tempI64[1];HEAPU32[buf+80>>2]=ctime%1e3*1e3;tempI64=[stat.ino>>>0,(tempDouble=stat.ino,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+88>>2]=tempI64[0],HEAP32[buf+92>>2]=tempI64[1];return 0},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();HEAP32[argp>>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag:c_iflag,c_oflag:c_oflag,c_cflag:c_cflag,c_lflag:c_lflag,c_cc:c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.doStat(FS.stat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>{abort("")};var tupleRegistrations={};var runDestructors=destructors=>{while(destructors.length){var ptr=destructors.pop();var del=destructors.pop();del(ptr)}};function readPointer(pointer){return this["fromWireType"](HEAPU32[pointer>>2])}var awaitingDependencies={};var registeredTypes={};var typeDependencies={};var InternalError;var throwInternalError=message=>{throw new InternalError(message)};var whenDependentTypesAreResolved=(myTypes,dependentTypes,getTypeConverters)=>{myTypes.forEach(function(type){typeDependencies[type]=dependentTypes});function onComplete(typeConverters){var myTypeConverters=getTypeConverters(typeConverters);if(myTypeConverters.length!==myTypes.length){throwInternalError("Mismatched type converter count")}for(var i=0;i{if(registeredTypes.hasOwnProperty(dt)){typeConverters[i]=registeredTypes[dt]}else{unregisteredTypes.push(dt);if(!awaitingDependencies.hasOwnProperty(dt)){awaitingDependencies[dt]=[]}awaitingDependencies[dt].push(()=>{typeConverters[i]=registeredTypes[dt];++registered;if(registered===unregisteredTypes.length){onComplete(typeConverters)}})}});if(0===unregisteredTypes.length){onComplete(typeConverters)}};var __embind_finalize_value_array=rawTupleType=>{var reg=tupleRegistrations[rawTupleType];delete tupleRegistrations[rawTupleType];var elements=reg.elements;var elementsLength=elements.length;var elementTypes=elements.map(elt=>elt.getterReturnType).concat(elements.map(elt=>elt.setterArgumentType));var rawConstructor=reg.rawConstructor;var rawDestructor=reg.rawDestructor;whenDependentTypesAreResolved([rawTupleType],elementTypes,elementTypes=>{elements.forEach((elt,i)=>{var getterReturnType=elementTypes[i];var getter=elt.getter;var getterContext=elt.getterContext;var setterArgumentType=elementTypes[i+elementsLength];var setter=elt.setter;var setterContext=elt.setterContext;elt.read=ptr=>getterReturnType["fromWireType"](getter(getterContext,ptr));elt.write=(ptr,o)=>{var destructors=[];setter(setterContext,ptr,setterArgumentType["toWireType"](destructors,o));runDestructors(destructors)}});return[{name:reg.name,fromWireType:ptr=>{var rv=new Array(elementsLength);for(var i=0;i{if(elementsLength!==o.length){throw new TypeError(`Incorrect number of tuple elements for ${reg.name}: expected=${elementsLength}, actual=${o.length}`)}var ptr=rawConstructor();for(var i=0;i{var reg=structRegistrations[structType];delete structRegistrations[structType];var rawConstructor=reg.rawConstructor;var rawDestructor=reg.rawDestructor;var fieldRecords=reg.fields;var fieldTypes=fieldRecords.map(field=>field.getterReturnType).concat(fieldRecords.map(field=>field.setterArgumentType));whenDependentTypesAreResolved([structType],fieldTypes,fieldTypes=>{var fields={};fieldRecords.forEach((field,i)=>{var fieldName=field.fieldName;var getterReturnType=fieldTypes[i];var getter=field.getter;var getterContext=field.getterContext;var setterArgumentType=fieldTypes[i+fieldRecords.length];var setter=field.setter;var setterContext=field.setterContext;fields[fieldName]={read:ptr=>getterReturnType["fromWireType"](getter(getterContext,ptr)),write:(ptr,o)=>{var destructors=[];setter(setterContext,ptr,setterArgumentType["toWireType"](destructors,o));runDestructors(destructors)}}});return[{name:reg.name,fromWireType:ptr=>{var rv={};for(var i in fields){rv[i]=fields[i].read(ptr)}rawDestructor(ptr);return rv},toWireType:(destructors,o)=>{for(var fieldName in fields){if(!(fieldName in o)){throw new TypeError(`Missing field: "${fieldName}"`)}}var ptr=rawConstructor();for(fieldName in fields){fields[fieldName].write(ptr,o[fieldName])}if(destructors!==null){destructors.push(rawDestructor,ptr)}return ptr},argPackAdvance:GenericWireTypeSize,readValueFromPointer:readPointer,destructorFunction:rawDestructor}]})};var __embind_register_bigint=(primitiveType,name,size,minRange,maxRange)=>{};var embind_init_charCodes=()=>{var codes=new Array(256);for(var i=0;i<256;++i){codes[i]=String.fromCharCode(i)}embind_charCodes=codes};var embind_charCodes;var readLatin1String=ptr=>{var ret="";var c=ptr;while(HEAPU8[c]){ret+=embind_charCodes[HEAPU8[c++]]}return ret};var BindingError;var throwBindingError=message=>{throw new BindingError(message)};function sharedRegisterType(rawType,registeredInstance,options={}){var name=registeredInstance.name;if(!rawType){throwBindingError(`type "${name}" must have a positive integer typeid pointer`)}if(registeredTypes.hasOwnProperty(rawType)){if(options.ignoreDuplicateRegistrations){return}else{throwBindingError(`Cannot register type '${name}' twice`)}}registeredTypes[rawType]=registeredInstance;delete typeDependencies[rawType];if(awaitingDependencies.hasOwnProperty(rawType)){var callbacks=awaitingDependencies[rawType];delete awaitingDependencies[rawType];callbacks.forEach(cb=>cb())}}function registerType(rawType,registeredInstance,options={}){if(!("argPackAdvance"in registeredInstance)){throw new TypeError("registerType registeredInstance requires argPackAdvance")}return sharedRegisterType(rawType,registeredInstance,options)}var GenericWireTypeSize=8;var __embind_register_bool=(rawType,name,trueValue,falseValue)=>{name=readLatin1String(name);registerType(rawType,{name:name,fromWireType:function(wt){return!!wt},toWireType:function(destructors,o){return o?trueValue:falseValue},argPackAdvance:GenericWireTypeSize,readValueFromPointer:function(pointer){return this["fromWireType"](HEAPU8[pointer])},destructorFunction:null})};var shallowCopyInternalPointer=o=>({count:o.count,deleteScheduled:o.deleteScheduled,preservePointerOnDelete:o.preservePointerOnDelete,ptr:o.ptr,ptrType:o.ptrType,smartPtr:o.smartPtr,smartPtrType:o.smartPtrType});var throwInstanceAlreadyDeleted=obj=>{function getInstanceTypeName(handle){return handle.$$.ptrType.registeredClass.name}throwBindingError(getInstanceTypeName(obj)+" instance already deleted")};var finalizationRegistry=false;var detachFinalizer=handle=>{};var runDestructor=$$=>{if($$.smartPtr){$$.smartPtrType.rawDestructor($$.smartPtr)}else{$$.ptrType.registeredClass.rawDestructor($$.ptr)}};var releaseClassHandle=$$=>{$$.count.value-=1;var toDelete=0===$$.count.value;if(toDelete){runDestructor($$)}};var downcastPointer=(ptr,ptrClass,desiredClass)=>{if(ptrClass===desiredClass){return ptr}if(undefined===desiredClass.baseClass){return null}var rv=downcastPointer(ptr,ptrClass,desiredClass.baseClass);if(rv===null){return null}return desiredClass.downcast(rv)};var registeredPointers={};var getInheritedInstanceCount=()=>Object.keys(registeredInstances).length;var getLiveInheritedInstances=()=>{var rv=[];for(var k in registeredInstances){if(registeredInstances.hasOwnProperty(k)){rv.push(registeredInstances[k])}}return rv};var deletionQueue=[];var flushPendingDeletes=()=>{while(deletionQueue.length){var obj=deletionQueue.pop();obj.$$.deleteScheduled=false;obj["delete"]()}};var delayFunction;var setDelayFunction=fn=>{delayFunction=fn;if(deletionQueue.length&&delayFunction){delayFunction(flushPendingDeletes)}};var init_embind=()=>{Module["getInheritedInstanceCount"]=getInheritedInstanceCount;Module["getLiveInheritedInstances"]=getLiveInheritedInstances;Module["flushPendingDeletes"]=flushPendingDeletes;Module["setDelayFunction"]=setDelayFunction};var registeredInstances={};var getBasestPointer=(class_,ptr)=>{if(ptr===undefined){throwBindingError("ptr should not be undefined")}while(class_.baseClass){ptr=class_.upcast(ptr);class_=class_.baseClass}return ptr};var getInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);return registeredInstances[ptr]};var makeClassHandle=(prototype,record)=>{if(!record.ptrType||!record.ptr){throwInternalError("makeClassHandle requires ptr and ptrType")}var hasSmartPtrType=!!record.smartPtrType;var hasSmartPtr=!!record.smartPtr;if(hasSmartPtrType!==hasSmartPtr){throwInternalError("Both smartPtrType and smartPtr must be specified")}record.count={value:1};return attachFinalizer(Object.create(prototype,{$$:{value:record,writable:true}}))};function RegisteredPointer_fromWireType(ptr){var rawPointer=this.getPointee(ptr);if(!rawPointer){this.destructor(ptr);return null}var registeredInstance=getInheritedInstance(this.registeredClass,rawPointer);if(undefined!==registeredInstance){if(0===registeredInstance.$$.count.value){registeredInstance.$$.ptr=rawPointer;registeredInstance.$$.smartPtr=ptr;return registeredInstance["clone"]()}else{var rv=registeredInstance["clone"]();this.destructor(ptr);return rv}}function makeDefaultHandle(){if(this.isSmartPointer){return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this.pointeeType,ptr:rawPointer,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this,ptr:ptr})}}var actualType=this.registeredClass.getActualType(rawPointer);var registeredPointerRecord=registeredPointers[actualType];if(!registeredPointerRecord){return makeDefaultHandle.call(this)}var toType;if(this.isConst){toType=registeredPointerRecord.constPointerType}else{toType=registeredPointerRecord.pointerType}var dp=downcastPointer(rawPointer,this.registeredClass,toType.registeredClass);if(dp===null){return makeDefaultHandle.call(this)}if(this.isSmartPointer){return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp})}}var attachFinalizer=handle=>{if("undefined"===typeof FinalizationRegistry){attachFinalizer=handle=>handle;return handle}finalizationRegistry=new FinalizationRegistry(info=>{releaseClassHandle(info.$$)});attachFinalizer=handle=>{var $$=handle.$$;var hasSmartPtr=!!$$.smartPtr;if(hasSmartPtr){var info={$$:$$};finalizationRegistry.register(handle,info,handle)}return handle};detachFinalizer=handle=>finalizationRegistry.unregister(handle);return attachFinalizer(handle)};var init_ClassHandle=()=>{Object.assign(ClassHandle.prototype,{isAliasOf(other){if(!(this instanceof ClassHandle)){return false}if(!(other instanceof ClassHandle)){return false}var leftClass=this.$$.ptrType.registeredClass;var left=this.$$.ptr;other.$$=other.$$;var rightClass=other.$$.ptrType.registeredClass;var right=other.$$.ptr;while(leftClass.baseClass){left=leftClass.upcast(left);leftClass=leftClass.baseClass}while(rightClass.baseClass){right=rightClass.upcast(right);rightClass=rightClass.baseClass}return leftClass===rightClass&&left===right},clone(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.preservePointerOnDelete){this.$$.count.value+=1;return this}else{var clone=attachFinalizer(Object.create(Object.getPrototypeOf(this),{$$:{value:shallowCopyInternalPointer(this.$$)}}));clone.$$.count.value+=1;clone.$$.deleteScheduled=false;return clone}},delete(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}detachFinalizer(this);releaseClassHandle(this.$$);if(!this.$$.preservePointerOnDelete){this.$$.smartPtr=undefined;this.$$.ptr=undefined}},isDeleted(){return!this.$$.ptr},deleteLater(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}deletionQueue.push(this);if(deletionQueue.length===1&&delayFunction){delayFunction(flushPendingDeletes)}this.$$.deleteScheduled=true;return this}})};function ClassHandle(){}var createNamedFunction=(name,body)=>Object.defineProperty(body,"name",{value:name});var ensureOverloadTable=(proto,methodName,humanName)=>{if(undefined===proto[methodName].overloadTable){var prevFunc=proto[methodName];proto[methodName]=function(...args){if(!proto[methodName].overloadTable.hasOwnProperty(args.length)){throwBindingError(`Function '${humanName}' called with an invalid number of arguments (${args.length}) - expects one of (${proto[methodName].overloadTable})!`)}return proto[methodName].overloadTable[args.length].apply(this,args)};proto[methodName].overloadTable=[];proto[methodName].overloadTable[prevFunc.argCount]=prevFunc}};var exposePublicSymbol=(name,value,numArguments)=>{if(Module.hasOwnProperty(name)){if(undefined===numArguments||undefined!==Module[name].overloadTable&&undefined!==Module[name].overloadTable[numArguments]){throwBindingError(`Cannot register public name '${name}' twice`)}ensureOverloadTable(Module,name,name);if(Module.hasOwnProperty(numArguments)){throwBindingError(`Cannot register multiple overloads of a function with the same number of arguments (${numArguments})!`)}Module[name].overloadTable[numArguments]=value}else{Module[name]=value;if(undefined!==numArguments){Module[name].numArguments=numArguments}}};var char_0=48;var char_9=57;var makeLegalFunctionName=name=>{if(undefined===name){return"_unknown"}name=name.replace(/[^a-zA-Z0-9_]/g,"$");var f=name.charCodeAt(0);if(f>=char_0&&f<=char_9){return`_${name}`}return name};function RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast){this.name=name;this.constructor=constructor;this.instancePrototype=instancePrototype;this.rawDestructor=rawDestructor;this.baseClass=baseClass;this.getActualType=getActualType;this.upcast=upcast;this.downcast=downcast;this.pureVirtualFunctions=[]}var upcastPointer=(ptr,ptrClass,desiredClass)=>{while(ptrClass!==desiredClass){if(!ptrClass.upcast){throwBindingError(`Expected null or instance of ${desiredClass.name}, got an instance of ${ptrClass.name}`)}ptr=ptrClass.upcast(ptr);ptrClass=ptrClass.baseClass}return ptr};function constNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}function genericPointerToWireType(destructors,handle){var ptr;if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}if(this.isSmartPointer){ptr=this.rawConstructor();if(destructors!==null){destructors.push(this.rawDestructor,ptr)}return ptr}else{return 0}}if(!handle||!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(!this.isConst&&handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);if(this.isSmartPointer){if(undefined===handle.$$.smartPtr){throwBindingError("Passing raw pointer to smart pointer is illegal")}switch(this.sharingPolicy){case 0:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}break;case 1:ptr=handle.$$.smartPtr;break;case 2:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{var clonedHandle=handle["clone"]();ptr=this.rawShare(ptr,Emval.toHandle(()=>clonedHandle["delete"]()));if(destructors!==null){destructors.push(this.rawDestructor,ptr)}}break;default:throwBindingError("Unsupporting sharing policy")}}return ptr}function nonConstNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}var init_RegisteredPointer=()=>{Object.assign(RegisteredPointer.prototype,{getPointee(ptr){if(this.rawGetPointee){ptr=this.rawGetPointee(ptr)}return ptr},destructor(ptr){this.rawDestructor?.(ptr)},argPackAdvance:GenericWireTypeSize,readValueFromPointer:readPointer,fromWireType:RegisteredPointer_fromWireType})};function RegisteredPointer(name,registeredClass,isReference,isConst,isSmartPointer,pointeeType,sharingPolicy,rawGetPointee,rawConstructor,rawShare,rawDestructor){this.name=name;this.registeredClass=registeredClass;this.isReference=isReference;this.isConst=isConst;this.isSmartPointer=isSmartPointer;this.pointeeType=pointeeType;this.sharingPolicy=sharingPolicy;this.rawGetPointee=rawGetPointee;this.rawConstructor=rawConstructor;this.rawShare=rawShare;this.rawDestructor=rawDestructor;if(!isSmartPointer&®isteredClass.baseClass===undefined){if(isConst){this["toWireType"]=constNoSmartPtrRawPointerToWireType;this.destructorFunction=null}else{this["toWireType"]=nonConstNoSmartPtrRawPointerToWireType;this.destructorFunction=null}}else{this["toWireType"]=genericPointerToWireType}}var replacePublicSymbol=(name,value,numArguments)=>{if(!Module.hasOwnProperty(name)){throwInternalError("Replacing nonexistent public symbol")}if(undefined!==Module[name].overloadTable&&undefined!==numArguments){Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var dynCallLegacy=(sig,ptr,args)=>{sig=sig.replace(/p/g,"i");var f=Module["dynCall_"+sig];return f(ptr,...args)};var wasmTableMirror=[];var wasmTable;var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){if(funcPtr>=wasmTableMirror.length)wasmTableMirror.length=funcPtr+1;wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var dynCall=(sig,ptr,args=[])=>{if(sig.includes("j")){return dynCallLegacy(sig,ptr,args)}var rtn=getWasmTableEntry(ptr)(...args);return rtn};var getDynCaller=(sig,ptr)=>(...args)=>dynCall(sig,ptr,args);var embind__requireFunction=(signature,rawFunction)=>{signature=readLatin1String(signature);function makeDynCaller(){if(signature.includes("j")){return getDynCaller(signature,rawFunction)}return getWasmTableEntry(rawFunction)}var fp=makeDynCaller();if(typeof fp!="function"){throwBindingError(`unknown function pointer with signature ${signature}: ${rawFunction}`)}return fp};var extendError=(baseErrorType,errorName)=>{var errorClass=createNamedFunction(errorName,function(message){this.name=errorName;this.message=message;var stack=new Error(message).stack;if(stack!==undefined){this.stack=this.toString()+"\n"+stack.replace(/^Error(:[^\n]*)?\n/,"")}});errorClass.prototype=Object.create(baseErrorType.prototype);errorClass.prototype.constructor=errorClass;errorClass.prototype.toString=function(){if(this.message===undefined){return this.name}else{return`${this.name}: ${this.message}`}};return errorClass};var UnboundTypeError;var getTypeName=type=>{var ptr=___getTypeName(type);var rv=readLatin1String(ptr);_free(ptr);return rv};var throwUnboundTypeError=(message,types)=>{var unboundTypes=[];var seen={};function visit(type){if(seen[type]){return}if(registeredTypes[type]){return}if(typeDependencies[type]){typeDependencies[type].forEach(visit);return}unboundTypes.push(type);seen[type]=true}types.forEach(visit);throw new UnboundTypeError(`${message}: `+unboundTypes.map(getTypeName).join([", "]))};var __embind_register_class=(rawType,rawPointerType,rawConstPointerType,baseClassRawType,getActualTypeSignature,getActualType,upcastSignature,upcast,downcastSignature,downcast,name,destructorSignature,rawDestructor)=>{name=readLatin1String(name);getActualType=embind__requireFunction(getActualTypeSignature,getActualType);upcast&&=embind__requireFunction(upcastSignature,upcast);downcast&&=embind__requireFunction(downcastSignature,downcast);rawDestructor=embind__requireFunction(destructorSignature,rawDestructor);var legalFunctionName=makeLegalFunctionName(name);exposePublicSymbol(legalFunctionName,function(){throwUnboundTypeError(`Cannot construct ${name} due to unbound types`,[baseClassRawType])});whenDependentTypesAreResolved([rawType,rawPointerType,rawConstPointerType],baseClassRawType?[baseClassRawType]:[],base=>{base=base[0];var baseClass;var basePrototype;if(baseClassRawType){baseClass=base.registeredClass;basePrototype=baseClass.instancePrototype}else{basePrototype=ClassHandle.prototype}var constructor=createNamedFunction(name,function(...args){if(Object.getPrototypeOf(this)!==instancePrototype){throw new BindingError("Use 'new' to construct "+name)}if(undefined===registeredClass.constructor_body){throw new BindingError(name+" has no accessible constructor")}var body=registeredClass.constructor_body[args.length];if(undefined===body){throw new BindingError(`Tried to invoke ctor of ${name} with invalid number of parameters (${args.length}) - expected (${Object.keys(registeredClass.constructor_body).toString()}) parameters instead!`)}return body.apply(this,args)});var instancePrototype=Object.create(basePrototype,{constructor:{value:constructor}});constructor.prototype=instancePrototype;var registeredClass=new RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast);if(registeredClass.baseClass){registeredClass.baseClass.__derivedClasses??=[];registeredClass.baseClass.__derivedClasses.push(registeredClass)}var referenceConverter=new RegisteredPointer(name,registeredClass,true,false,false);var pointerConverter=new RegisteredPointer(name+"*",registeredClass,false,false,false);var constPointerConverter=new RegisteredPointer(name+" const*",registeredClass,false,true,false);registeredPointers[rawType]={pointerType:pointerConverter,constPointerType:constPointerConverter};replacePublicSymbol(legalFunctionName,constructor);return[referenceConverter,pointerConverter,constPointerConverter]})};function usesDestructorStack(argTypes){for(var i=1;i0?", ":"")+argsListWired}invokerFnBody+=(returns||isAsync?"var rv = ":"")+"invoker(fn"+(argsListWired.length>0?", ":"")+argsListWired+");\n";if(needsDestructorStack){invokerFnBody+="runDestructors(destructors);\n"}else{for(var i=isClassMethodFunc?1:2;i{var array=[];for(var i=0;i>2])}return array};var getFunctionName=signature=>{signature=signature.trim();const argsIndex=signature.indexOf("(");if(argsIndex!==-1){return signature.substr(0,argsIndex)}else{return signature}};var __embind_register_class_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,fn,isAsync)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=readLatin1String(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}var proto=classType.registeredClass.constructor;if(undefined===proto[methodName]){unboundTypesHandler.argCount=argCount-1;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-1]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));var func=craftInvokerFunction(humanName,invokerArgsArray,null,rawInvoker,fn,isAsync);if(undefined===proto[methodName].overloadTable){func.argCount=argCount-1;proto[methodName]=func}else{proto[methodName].overloadTable[argCount-1]=func}if(classType.registeredClass.__derivedClasses){for(const derivedClass of classType.registeredClass.__derivedClasses){if(!derivedClass.constructor.hasOwnProperty(methodName)){derivedClass.constructor[methodName]=func}}}return[]});return[]})};var __embind_register_class_constructor=(rawClassType,argCount,rawArgTypesAddr,invokerSignature,invoker,rawConstructor)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);invoker=embind__requireFunction(invokerSignature,invoker);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`constructor ${classType.name}`;if(undefined===classType.registeredClass.constructor_body){classType.registeredClass.constructor_body=[]}if(undefined!==classType.registeredClass.constructor_body[argCount-1]){throw new BindingError(`Cannot register multiple constructors with identical number of parameters (${argCount-1}) for class '${classType.name}'! Overload resolution is currently only performed using the parameter count, not actual type info!`)}classType.registeredClass.constructor_body[argCount-1]=()=>{throwUnboundTypeError(`Cannot construct ${classType.name} due to unbound types`,rawArgTypes)};whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{argTypes.splice(1,0,null);classType.registeredClass.constructor_body[argCount-1]=craftInvokerFunction(humanName,argTypes,null,invoker,rawConstructor);return[]});return[]})};var __embind_register_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,context,isPureVirtual,isAsync)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=readLatin1String(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}if(isPureVirtual){classType.registeredClass.pureVirtualFunctions.push(methodName)}function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}var proto=classType.registeredClass.instancePrototype;var method=proto[methodName];if(undefined===method||undefined===method.overloadTable&&method.className!==classType.name&&method.argCount===argCount-2){unboundTypesHandler.argCount=argCount-2;unboundTypesHandler.className=classType.name;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-2]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var memberFunction=craftInvokerFunction(humanName,argTypes,classType,rawInvoker,context,isAsync);if(undefined===proto[methodName].overloadTable){memberFunction.argCount=argCount-2;proto[methodName]=memberFunction}else{proto[methodName].overloadTable[argCount-2]=memberFunction}return[]});return[]})};var validateThis=(this_,classType,humanName)=>{if(!(this_ instanceof Object)){throwBindingError(`${humanName} with invalid "this": ${this_}`)}if(!(this_ instanceof classType.registeredClass.constructor)){throwBindingError(`${humanName} incompatible with "this" of type ${this_.constructor.name}`)}if(!this_.$$.ptr){throwBindingError(`cannot call emscripten binding method ${humanName} on deleted object`)}return upcastPointer(this_.$$.ptr,this_.$$.ptrType.registeredClass,classType.registeredClass)};var __embind_register_class_property=(classType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{fieldName=readLatin1String(fieldName);getter=embind__requireFunction(getterSignature,getter);whenDependentTypesAreResolved([],[classType],classType=>{classType=classType[0];var humanName=`${classType.name}.${fieldName}`;var desc={get(){throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])},enumerable:true,configurable:true};if(setter){desc.set=()=>throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])}else{desc.set=v=>throwBindingError(humanName+" is a read-only property")}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);whenDependentTypesAreResolved([],setter?[getterReturnType,setterArgumentType]:[getterReturnType],types=>{var getterReturnType=types[0];var desc={get(){var ptr=validateThis(this,classType,humanName+" getter");return getterReturnType["fromWireType"](getter(getterContext,ptr))},enumerable:true};if(setter){setter=embind__requireFunction(setterSignature,setter);var setterArgumentType=types[1];desc.set=function(v){var ptr=validateThis(this,classType,humanName+" setter");var destructors=[];setter(setterContext,ptr,setterArgumentType["toWireType"](destructors,v));runDestructors(destructors)}}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);return[]});return[]})};var emval_freelist=[];var emval_handles=[];var __emval_decref=handle=>{if(handle>9&&0===--emval_handles[handle+1]){emval_handles[handle]=undefined;emval_freelist.push(handle)}};var count_emval_handles=()=>emval_handles.length/2-5-emval_freelist.length;var init_emval=()=>{emval_handles.push(0,1,undefined,1,null,1,true,1,false,1);Module["count_emval_handles"]=count_emval_handles};var Emval={toValue:handle=>{if(!handle){throwBindingError("Cannot use deleted val. handle = "+handle)}return emval_handles[handle]},toHandle:value=>{switch(value){case undefined:return 2;case null:return 4;case true:return 6;case false:return 8;default:{const handle=emval_freelist.pop()||emval_handles.length;emval_handles[handle]=value;emval_handles[handle+1]=1;return handle}}}};var EmValType={name:"emscripten::val",fromWireType:handle=>{var rv=Emval.toValue(handle);__emval_decref(handle);return rv},toWireType:(destructors,value)=>Emval.toHandle(value),argPackAdvance:GenericWireTypeSize,readValueFromPointer:readPointer,destructorFunction:null};var __embind_register_emval=rawType=>registerType(rawType,EmValType);var enumReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?function(pointer){return this["fromWireType"](HEAP8[pointer])}:function(pointer){return this["fromWireType"](HEAPU8[pointer])};case 2:return signed?function(pointer){return this["fromWireType"](HEAP16[pointer>>1])}:function(pointer){return this["fromWireType"](HEAPU16[pointer>>1])};case 4:return signed?function(pointer){return this["fromWireType"](HEAP32[pointer>>2])}:function(pointer){return this["fromWireType"](HEAPU32[pointer>>2])};default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};var __embind_register_enum=(rawType,name,size,isSigned)=>{name=readLatin1String(name);function ctor(){}ctor.values={};registerType(rawType,{name:name,constructor:ctor,fromWireType:function(c){return this.constructor.values[c]},toWireType:(destructors,c)=>c.value,argPackAdvance:GenericWireTypeSize,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,ctor)};var requireRegisteredType=(rawType,humanName)=>{var impl=registeredTypes[rawType];if(undefined===impl){throwBindingError(`${humanName} has unknown type ${getTypeName(rawType)}`)}return impl};var __embind_register_enum_value=(rawEnumType,name,enumValue)=>{var enumType=requireRegisteredType(rawEnumType,"enum");name=readLatin1String(name);var Enum=enumType.constructor;var Value=Object.create(enumType.constructor.prototype,{value:{value:enumValue},constructor:{value:createNamedFunction(`${enumType.name}_${name}`,function(){})}});Enum.values[enumValue]=Value;Enum[name]=Value};var embindRepr=v=>{if(v===null){return"null"}var t=typeof v;if(t==="object"||t==="array"||t==="function"){return v.toString()}else{return""+v}};var floatReadValueFromPointer=(name,width)=>{switch(width){case 4:return function(pointer){return this["fromWireType"](HEAPF32[pointer>>2])};case 8:return function(pointer){return this["fromWireType"](HEAPF64[pointer>>3])};default:throw new TypeError(`invalid float width (${width}): ${name}`)}};var __embind_register_float=(rawType,name,size)=>{name=readLatin1String(name);registerType(rawType,{name:name,fromWireType:value=>value,toWireType:(destructors,value)=>value,argPackAdvance:GenericWireTypeSize,readValueFromPointer:floatReadValueFromPointer(name,size),destructorFunction:null})};var __embind_register_function=(name,argCount,rawArgTypesAddr,signature,rawInvoker,fn,isAsync)=>{var argTypes=heap32VectorToArray(argCount,rawArgTypesAddr);name=readLatin1String(name);name=getFunctionName(name);rawInvoker=embind__requireFunction(signature,rawInvoker);exposePublicSymbol(name,function(){throwUnboundTypeError(`Cannot call ${name} due to unbound types`,argTypes)},argCount-1);whenDependentTypesAreResolved([],argTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));replacePublicSymbol(name,craftInvokerFunction(name,invokerArgsArray,null,rawInvoker,fn,isAsync),argCount-1);return[]})};var integerReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?pointer=>HEAP8[pointer]:pointer=>HEAPU8[pointer];case 2:return signed?pointer=>HEAP16[pointer>>1]:pointer=>HEAPU16[pointer>>1];case 4:return signed?pointer=>HEAP32[pointer>>2]:pointer=>HEAPU32[pointer>>2];default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};var __embind_register_integer=(primitiveType,name,size,minRange,maxRange)=>{name=readLatin1String(name);if(maxRange===-1){maxRange=4294967295}var fromWireType=value=>value;if(minRange===0){var bitshift=32-8*size;fromWireType=value=>value<>>bitshift}var isUnsignedType=name.includes("unsigned");var checkAssertions=(value,toTypeName)=>{};var toWireType;if(isUnsignedType){toWireType=function(destructors,value){checkAssertions(value,this.name);return value>>>0}}else{toWireType=function(destructors,value){checkAssertions(value,this.name);return value}}registerType(primitiveType,{name:name,fromWireType:fromWireType,toWireType:toWireType,argPackAdvance:GenericWireTypeSize,readValueFromPointer:integerReadValueFromPointer(name,size,minRange!==0),destructorFunction:null})};var __embind_register_memory_view=(rawType,dataTypeIndex,name)=>{var typeMapping=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array];var TA=typeMapping[dataTypeIndex];function decodeMemoryView(handle){var size=HEAPU32[handle>>2];var data=HEAPU32[handle+4>>2];return new TA(HEAP8.buffer,data,size)}name=readLatin1String(name);registerType(rawType,{name:name,fromWireType:decodeMemoryView,argPackAdvance:GenericWireTypeSize,readValueFromPointer:decodeMemoryView},{ignoreDuplicateRegistrations:true})};var __embind_register_optional=(rawOptionalType,rawType)=>{__embind_register_emval(rawOptionalType)};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var __embind_register_std_string=(rawType,name)=>{name=readLatin1String(name);var stdStringIsUTF8=name==="std::string";registerType(rawType,{name:name,fromWireType(value){var length=HEAPU32[value>>2];var payload=value+4;var str;if(stdStringIsUTF8){var decodeStartPtr=payload;for(var i=0;i<=length;++i){var currentBytePtr=payload+i;if(i==length||HEAPU8[currentBytePtr]==0){var maxRead=currentBytePtr-decodeStartPtr;var stringSegment=UTF8ToString(decodeStartPtr,maxRead);if(str===undefined){str=stringSegment}else{str+=String.fromCharCode(0);str+=stringSegment}decodeStartPtr=currentBytePtr+1}}}else{var a=new Array(length);for(var i=0;i>2]=length;if(stdStringIsUTF8&&valueIsOfTypeString){stringToUTF8(value,ptr,length+1)}else{if(valueIsOfTypeString){for(var i=0;i255){_free(ptr);throwBindingError("String has UTF-16 code units that do not fit in 8 bits")}HEAPU8[ptr+i]=charCode}}else{for(var i=0;i{var endPtr=ptr;var idx=endPtr>>1;var maxIdx=idx+maxBytesToRead/2;while(!(idx>=maxIdx)&&HEAPU16[idx])++idx;endPtr=idx<<1;if(endPtr-ptr>32&&UTF16Decoder)return UTF16Decoder.decode(HEAPU8.subarray(ptr,endPtr));var str="";for(var i=0;!(i>=maxBytesToRead/2);++i){var codeUnit=HEAP16[ptr+i*2>>1];if(codeUnit==0)break;str+=String.fromCharCode(codeUnit)}return str};var stringToUTF16=(str,outPtr,maxBytesToWrite)=>{maxBytesToWrite??=2147483647;if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite>1]=codeUnit;outPtr+=2}HEAP16[outPtr>>1]=0;return outPtr-startPtr};var lengthBytesUTF16=str=>str.length*2;var UTF32ToString=(ptr,maxBytesToRead)=>{var i=0;var str="";while(!(i>=maxBytesToRead/4)){var utf32=HEAP32[ptr+i*4>>2];if(utf32==0)break;++i;if(utf32>=65536){var ch=utf32-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}else{str+=String.fromCharCode(utf32)}}return str};var stringToUTF32=(str,outPtr,maxBytesToWrite)=>{maxBytesToWrite??=2147483647;if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i=55296&&codeUnit<=57343){var trailSurrogate=str.charCodeAt(++i);codeUnit=65536+((codeUnit&1023)<<10)|trailSurrogate&1023}HEAP32[outPtr>>2]=codeUnit;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>2]=0;return outPtr-startPtr};var lengthBytesUTF32=str=>{var len=0;for(var i=0;i=55296&&codeUnit<=57343)++i;len+=4}return len};var __embind_register_std_wstring=(rawType,charSize,name)=>{name=readLatin1String(name);var decodeString,encodeString,readCharAt,lengthBytesUTF;if(charSize===2){decodeString=UTF16ToString;encodeString=stringToUTF16;lengthBytesUTF=lengthBytesUTF16;readCharAt=pointer=>HEAPU16[pointer>>1]}else if(charSize===4){decodeString=UTF32ToString;encodeString=stringToUTF32;lengthBytesUTF=lengthBytesUTF32;readCharAt=pointer=>HEAPU32[pointer>>2]}registerType(rawType,{name:name,fromWireType:value=>{var length=HEAPU32[value>>2];var str;var decodeStartPtr=value+4;for(var i=0;i<=length;++i){var currentBytePtr=value+4+i*charSize;if(i==length||readCharAt(currentBytePtr)==0){var maxReadBytes=currentBytePtr-decodeStartPtr;var stringSegment=decodeString(decodeStartPtr,maxReadBytes);if(str===undefined){str=stringSegment}else{str+=String.fromCharCode(0);str+=stringSegment}decodeStartPtr=currentBytePtr+charSize}}_free(value);return str},toWireType:(destructors,value)=>{if(!(typeof value=="string")){throwBindingError(`Cannot pass non-string to C++ string type ${name}`)}var length=lengthBytesUTF(value);var ptr=_malloc(4+length+charSize);HEAPU32[ptr>>2]=length/charSize;encodeString(value,ptr+4,length+charSize);if(destructors!==null){destructors.push(_free,ptr)}return ptr},argPackAdvance:GenericWireTypeSize,readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var __embind_register_value_array=(rawType,name,constructorSignature,rawConstructor,destructorSignature,rawDestructor)=>{tupleRegistrations[rawType]={name:readLatin1String(name),rawConstructor:embind__requireFunction(constructorSignature,rawConstructor),rawDestructor:embind__requireFunction(destructorSignature,rawDestructor),elements:[]}};var __embind_register_value_array_element=(rawTupleType,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{tupleRegistrations[rawTupleType].elements.push({getterReturnType:getterReturnType,getter:embind__requireFunction(getterSignature,getter),getterContext:getterContext,setterArgumentType:setterArgumentType,setter:embind__requireFunction(setterSignature,setter),setterContext:setterContext})};var __embind_register_value_object=(rawType,name,constructorSignature,rawConstructor,destructorSignature,rawDestructor)=>{structRegistrations[rawType]={name:readLatin1String(name),rawConstructor:embind__requireFunction(constructorSignature,rawConstructor),rawDestructor:embind__requireFunction(destructorSignature,rawDestructor),fields:[]}};var __embind_register_value_object_field=(structType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{structRegistrations[structType].fields.push({fieldName:readLatin1String(fieldName),getterReturnType:getterReturnType,getter:embind__requireFunction(getterSignature,getter),getterContext:getterContext,setterArgumentType:setterArgumentType,setter:embind__requireFunction(setterSignature,setter),setterContext:setterContext})};var __embind_register_void=(rawType,name)=>{name=readLatin1String(name);registerType(rawType,{isVoid:true,name:name,argPackAdvance:0,fromWireType:()=>undefined,toWireType:(destructors,o)=>undefined})};var nowIsMonotonic=1;var __emscripten_get_now_is_monotonic=()=>nowIsMonotonic;var __emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var emval_returnValue=(returnType,destructorsRef,handle)=>{var destructors=[];var result=returnType["toWireType"](destructors,handle);if(destructors.length){HEAPU32[destructorsRef>>2]=Emval.toHandle(destructors)}return result};var __emval_as=(handle,returnType,destructorsRef)=>{handle=Emval.toValue(handle);returnType=requireRegisteredType(returnType,"emval::as");return emval_returnValue(returnType,destructorsRef,handle)};var __emval_get_property=(handle,key)=>{handle=Emval.toValue(handle);key=Emval.toValue(key);return Emval.toHandle(handle[key])};var __emval_incref=handle=>{if(handle>9){emval_handles[handle+1]+=1}};var emval_symbols={};var getStringOrSymbol=address=>{var symbol=emval_symbols[address];if(symbol===undefined){return readLatin1String(address)}return symbol};var __emval_new_cstring=v=>Emval.toHandle(getStringOrSymbol(v));var __emval_run_destructors=handle=>{var destructors=Emval.toValue(handle);runDestructors(destructors);__emval_decref(handle)};var __emval_take_value=(type,arg)=>{type=requireRegisteredType(type,"_emval_take_value");var v=type["readValueFromPointer"](arg);return Emval.toHandle(v)};var readEmAsmArgsArray=[];var readEmAsmArgs=(sigPtr,buf)=>{readEmAsmArgsArray.length=0;var ch;while(ch=HEAPU8[sigPtr++]){var wide=ch!=105;wide&=ch!=112;buf+=wide&&buf%8?4:0;readEmAsmArgsArray.push(ch==112?HEAPU32[buf>>2]:ch==105?HEAP32[buf>>2]:HEAPF64[buf>>3]);buf+=wide?8:4}return readEmAsmArgsArray};var runEmAsmFunction=(code,sigPtr,argbuf)=>{var args=readEmAsmArgs(sigPtr,argbuf);return ASM_CONSTS[code](...args)};var _emscripten_asm_const_int=(code,sigPtr,argbuf)=>runEmAsmFunction(code,sigPtr,argbuf);var _emscripten_date_now=()=>Date.now();var _emscripten_err=str=>err(UTF8ToString(str));var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var _emscripten_get_now;_emscripten_get_now=()=>performance.now();var _emscripten_out=str=>out(UTF8ToString(str));var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}var alignUp=(x,multiple)=>x+(multiple-x%multiple)%multiple;for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignUp(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(typeof navigator=="object"&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var stringToAscii=(str,buffer)=>{for(var i=0;i{var bufSize=0;getEnvStrings().forEach((string,i)=>{var ptr=environ_buf+bufSize;HEAPU32[__environ+i*4>>2]=ptr;stringToAscii(string,ptr);bufSize+=string.length+1});return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;strings.forEach(string=>bufSize+=string.length+1);HEAPU32[penviron_buf_size>>2]=bufSize;return 0};function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var convertI32PairToI53Checked=(lo,hi)=>hi+2097152>>>0<4194305-!!lo?(lo>>>0)+hi*4294967296:NaN;function _fd_seek(fd,offset_low,offset_high,whence,newOffset){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);tempI64=[stream.position>>>0,(tempDouble=stream.position,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[newOffset>>2]=tempI64[0],HEAP32[newOffset+4>>2]=tempI64[1];if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(typeof offset!="undefined"){offset+=curr}}return ret};function _fd_write(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doWritev(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var _getentropy=(buffer,size)=>{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0};var webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.dibvbi=ctx.getExtension("WEBGL_draw_instanced_base_vertex_base_instance"));var webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.mdibvbi=ctx.getExtension("WEBGL_multi_draw_instanced_base_vertex_base_instance"));var webgl_enable_WEBGL_multi_draw=ctx=>!!(ctx.multiDrawWebgl=ctx.getExtension("WEBGL_multi_draw"));var getEmscriptenSupportedExtensions=ctx=>{var supportedExtensions=["EXT_color_buffer_float","EXT_conservative_depth","EXT_disjoint_timer_query_webgl2","EXT_texture_norm16","NV_shader_noperspective_interpolation","WEBGL_clip_cull_distance","EXT_color_buffer_half_float","EXT_depth_clamp","EXT_float_blend","EXT_texture_compression_bptc","EXT_texture_compression_rgtc","EXT_texture_filter_anisotropic","KHR_parallel_shader_compile","OES_texture_float_linear","WEBGL_blend_func_extended","WEBGL_compressed_texture_astc","WEBGL_compressed_texture_etc","WEBGL_compressed_texture_etc1","WEBGL_compressed_texture_s3tc","WEBGL_compressed_texture_s3tc_srgb","WEBGL_debug_renderer_info","WEBGL_debug_shaders","WEBGL_lose_context","WEBGL_multi_draw"];return(ctx.getSupportedExtensions()||[]).filter(ext=>supportedExtensions.includes(ext))};var GL={counter:1,buffers:[],mappedBuffers:{},programs:[],framebuffers:[],renderbuffers:[],textures:[],shaders:[],vaos:[],contexts:[],offscreenCanvases:{},queries:[],samplers:[],transformFeedbacks:[],syncs:[],byteSizeByTypeRoot:5120,byteSizeByType:[1,1,2,2,4,4,4,2,3,4,8],stringCache:{},stringiCache:{},unpackAlignment:4,recordError:errorCode=>{if(!GL.lastError){GL.lastError=errorCode}},getNewId:table=>{var ret=GL.counter++;for(var i=table.length;i{for(var i=0;i>2]=id}},MAX_TEMP_BUFFER_SIZE:2097152,numTempVertexBuffersPerSize:64,log2ceilLookup:i=>32-Math.clz32(i===0?0:i-1),generateTempBuffers:(quads,context)=>{var largestIndex=GL.log2ceilLookup(GL.MAX_TEMP_BUFFER_SIZE);context.tempVertexBufferCounters1=[];context.tempVertexBufferCounters2=[];context.tempVertexBufferCounters1.length=context.tempVertexBufferCounters2.length=largestIndex+1;context.tempVertexBuffers1=[];context.tempVertexBuffers2=[];context.tempVertexBuffers1.length=context.tempVertexBuffers2.length=largestIndex+1;context.tempIndexBuffers=[];context.tempIndexBuffers.length=largestIndex+1;for(var i=0;i<=largestIndex;++i){context.tempIndexBuffers[i]=null;context.tempVertexBufferCounters1[i]=context.tempVertexBufferCounters2[i]=0;var ringbufferLength=GL.numTempVertexBuffersPerSize;context.tempVertexBuffers1[i]=[];context.tempVertexBuffers2[i]=[];var ringbuffer1=context.tempVertexBuffers1[i];var ringbuffer2=context.tempVertexBuffers2[i];ringbuffer1.length=ringbuffer2.length=ringbufferLength;for(var j=0;j>1;var quadIndexes=new Uint16Array(numIndexes);var i=0,v=0;while(1){quadIndexes[i++]=v;if(i>=numIndexes)break;quadIndexes[i++]=v+1;if(i>=numIndexes)break;quadIndexes[i++]=v+2;if(i>=numIndexes)break;quadIndexes[i++]=v;if(i>=numIndexes)break;quadIndexes[i++]=v+2;if(i>=numIndexes)break;quadIndexes[i++]=v+3;if(i>=numIndexes)break;v+=4}context.GLctx.bufferData(34963,quadIndexes,35044);context.GLctx.bindBuffer(34963,null)}},getTempVertexBuffer:sizeBytes=>{var idx=GL.log2ceilLookup(sizeBytes);var ringbuffer=GL.currentContext.tempVertexBuffers1[idx];var nextFreeBufferIndex=GL.currentContext.tempVertexBufferCounters1[idx];GL.currentContext.tempVertexBufferCounters1[idx]=GL.currentContext.tempVertexBufferCounters1[idx]+1&GL.numTempVertexBuffersPerSize-1;var vbo=ringbuffer[nextFreeBufferIndex];if(vbo){return vbo}var prevVBO=GLctx.getParameter(34964);ringbuffer[nextFreeBufferIndex]=GLctx.createBuffer();GLctx.bindBuffer(34962,ringbuffer[nextFreeBufferIndex]);GLctx.bufferData(34962,1<{var idx=GL.log2ceilLookup(sizeBytes);var ibo=GL.currentContext.tempIndexBuffers[idx];if(ibo){return ibo}var prevIBO=GLctx.getParameter(34965);GL.currentContext.tempIndexBuffers[idx]=GLctx.createBuffer();GLctx.bindBuffer(34963,GL.currentContext.tempIndexBuffers[idx]);GLctx.bufferData(34963,1<{if(!GL.currentContext){return}var vb=GL.currentContext.tempVertexBuffers1;GL.currentContext.tempVertexBuffers1=GL.currentContext.tempVertexBuffers2;GL.currentContext.tempVertexBuffers2=vb;vb=GL.currentContext.tempVertexBufferCounters1;GL.currentContext.tempVertexBufferCounters1=GL.currentContext.tempVertexBufferCounters2;GL.currentContext.tempVertexBufferCounters2=vb;var largestIndex=GL.log2ceilLookup(GL.MAX_TEMP_BUFFER_SIZE);for(var i=0;i<=largestIndex;++i){GL.currentContext.tempVertexBufferCounters1[i]=0}},getSource:(shader,count,string,length)=>{var source="";for(var i=0;i>2]:undefined;source+=UTF8ToString(HEAPU32[string+i*4>>2],len)}return source},calcBufLength:(size,type,stride,count)=>{if(stride>0){return count*stride}var typeSize=GL.byteSizeByType[type-GL.byteSizeByTypeRoot];return size*typeSize*count},usedTempBuffers:[],preDrawHandleClientVertexAttribBindings:count=>{GL.resetBufferBinding=false;for(var i=0;i{if(GL.resetBufferBinding){GLctx.bindBuffer(34962,GL.buffers[GLctx.currentArrayBufferBinding])}},createContext:(canvas,webGLContextAttributes)=>{if(!canvas.getContextSafariWebGL2Fixed){canvas.getContextSafariWebGL2Fixed=canvas.getContext;function fixedGetContext(ver,attrs){var gl=canvas.getContextSafariWebGL2Fixed(ver,attrs);return ver=="webgl"==gl instanceof WebGLRenderingContext?gl:null}canvas.getContext=fixedGetContext}var ctx=canvas.getContext("webgl2",webGLContextAttributes);if(!ctx)return 0;var handle=GL.registerContext(ctx,webGLContextAttributes);return handle},registerContext:(ctx,webGLContextAttributes)=>{var handle=GL.getNewId(GL.contexts);var context={handle:handle,attributes:webGLContextAttributes,version:webGLContextAttributes.majorVersion,GLctx:ctx};if(ctx.canvas)ctx.canvas.GLctxObject=context;GL.contexts[handle]=context;if(typeof webGLContextAttributes.enableExtensionsByDefault=="undefined"||webGLContextAttributes.enableExtensionsByDefault){GL.initExtensions(context)}context.maxVertexAttribs=context.GLctx.getParameter(34921);context.clientBuffers=[];for(var i=0;i{GL.currentContext=GL.contexts[contextHandle];Module.ctx=GLctx=GL.currentContext?.GLctx;return!(contextHandle&&!GLctx)},getContext:contextHandle=>GL.contexts[contextHandle],deleteContext:contextHandle=>{if(GL.currentContext===GL.contexts[contextHandle]){GL.currentContext=null}if(typeof JSEvents=="object"){JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas)}if(GL.contexts[contextHandle]&&GL.contexts[contextHandle].GLctx.canvas){GL.contexts[contextHandle].GLctx.canvas.GLctxObject=undefined}GL.contexts[contextHandle]=null},initExtensions:context=>{context||=GL.currentContext;if(context.initExtensionsDone)return;context.initExtensionsDone=true;var GLctx=context.GLctx;webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx);webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx);if(context.version>=2){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query_webgl2")}if(context.version<2||!GLctx.disjointTimerQueryExt){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query")}webgl_enable_WEBGL_multi_draw(GLctx);getEmscriptenSupportedExtensions(GLctx).forEach(ext=>{if(!ext.includes("lose_context")&&!ext.includes("debug")){GLctx.getExtension(ext)}})}};var _glActiveTexture=x0=>GLctx.activeTexture(x0);var _glAttachShader=(program,shader)=>{GLctx.attachShader(GL.programs[program],GL.shaders[shader])};var _glBeginQuery=(target,id)=>{GLctx.beginQuery(target,GL.queries[id])};var _glBindAttribLocation=(program,index,name)=>{GLctx.bindAttribLocation(GL.programs[program],index,UTF8ToString(name))};var _glBindBuffer=(target,buffer)=>{if(target==34962){GLctx.currentArrayBufferBinding=buffer}else if(target==34963){GLctx.currentElementArrayBufferBinding=buffer}if(target==35051){GLctx.currentPixelPackBufferBinding=buffer}else if(target==35052){GLctx.currentPixelUnpackBufferBinding=buffer}GLctx.bindBuffer(target,GL.buffers[buffer])};var _glBindBufferBase=(target,index,buffer)=>{GLctx.bindBufferBase(target,index,GL.buffers[buffer])};var _glBindBufferRange=(target,index,buffer,offset,ptrsize)=>{GLctx.bindBufferRange(target,index,GL.buffers[buffer],offset,ptrsize)};var _glBindFramebuffer=(target,framebuffer)=>{GLctx.bindFramebuffer(target,GL.framebuffers[framebuffer])};var _glBindRenderbuffer=(target,renderbuffer)=>{GLctx.bindRenderbuffer(target,GL.renderbuffers[renderbuffer])};var _glBindSampler=(unit,sampler)=>{GLctx.bindSampler(unit,GL.samplers[sampler])};var _glBindTexture=(target,texture)=>{GLctx.bindTexture(target,GL.textures[texture])};var _glBindVertexArray=vao=>{GLctx.bindVertexArray(GL.vaos[vao]);var ibo=GLctx.getParameter(34965);GLctx.currentElementArrayBufferBinding=ibo?ibo.name|0:0};var _glBlendEquationSeparate=(x0,x1)=>GLctx.blendEquationSeparate(x0,x1);var _glBlendFuncSeparate=(x0,x1,x2,x3)=>GLctx.blendFuncSeparate(x0,x1,x2,x3);var _glBlitFramebuffer=(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9)=>GLctx.blitFramebuffer(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9);var _glBufferData=(target,size,data,usage)=>{if(true){if(data&&size){GLctx.bufferData(target,HEAPU8,usage,data,size)}else{GLctx.bufferData(target,size,usage)}return}GLctx.bufferData(target,data?HEAPU8.subarray(data,data+size):size,usage)};var _glBufferSubData=(target,offset,size,data)=>{if(true){size&&GLctx.bufferSubData(target,offset,HEAPU8,data,size);return}GLctx.bufferSubData(target,offset,HEAPU8.subarray(data,data+size))};var _glClear=x0=>GLctx.clear(x0);var _glClearBufferfi=(x0,x1,x2,x3)=>GLctx.clearBufferfi(x0,x1,x2,x3);var _glClearBufferfv=(buffer,drawbuffer,value)=>{GLctx.clearBufferfv(buffer,drawbuffer,HEAPF32,value>>2)};var _glClearBufferiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferiv(buffer,drawbuffer,HEAP32,value>>2)};var _glClearColor=(x0,x1,x2,x3)=>GLctx.clearColor(x0,x1,x2,x3);var _glClearDepthf=x0=>GLctx.clearDepth(x0);var _glClearStencil=x0=>GLctx.clearStencil(x0);var convertI32PairToI53=(lo,hi)=>(lo>>>0)+hi*4294967296;var _glClientWaitSync=(sync,flags,timeout_low,timeout_high)=>{var timeout=convertI32PairToI53(timeout_low,timeout_high);return GLctx.clientWaitSync(GL.syncs[sync],flags,timeout)};var _glColorMask=(red,green,blue,alpha)=>{GLctx.colorMask(!!red,!!green,!!blue,!!alpha)};var _glCompileShader=shader=>{GLctx.compileShader(GL.shaders[shader])};var _glCompressedTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,imageSize,data)=>{if(true){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,imageSize,data);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8,data,imageSize);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,data?HEAPU8.subarray(data,data+imageSize):null)};var _glCompressedTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)}else{GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,HEAPU8,data,imageSize)}};var _glCopyBufferSubData=(x0,x1,x2,x3,x4)=>GLctx.copyBufferSubData(x0,x1,x2,x3,x4);var _glCreateProgram=()=>{var id=GL.getNewId(GL.programs);var program=GLctx.createProgram();program.name=id;program.maxUniformLength=program.maxAttributeLength=program.maxUniformBlockNameLength=0;program.uniformIdCounter=1;GL.programs[id]=program;return id};var _glCreateShader=shaderType=>{var id=GL.getNewId(GL.shaders);GL.shaders[id]=GLctx.createShader(shaderType);return id};var _glCullFace=x0=>GLctx.cullFace(x0);var _glDeleteBuffers=(n,buffers)=>{for(var i=0;i>2];var buffer=GL.buffers[id];if(!buffer)continue;GLctx.deleteBuffer(buffer);buffer.name=0;GL.buffers[id]=null;if(id==GLctx.currentArrayBufferBinding)GLctx.currentArrayBufferBinding=0;if(id==GLctx.currentElementArrayBufferBinding)GLctx.currentElementArrayBufferBinding=0;if(id==GLctx.currentPixelPackBufferBinding)GLctx.currentPixelPackBufferBinding=0;if(id==GLctx.currentPixelUnpackBufferBinding)GLctx.currentPixelUnpackBufferBinding=0}};var _glDeleteFramebuffers=(n,framebuffers)=>{for(var i=0;i>2];var framebuffer=GL.framebuffers[id];if(!framebuffer)continue;GLctx.deleteFramebuffer(framebuffer);framebuffer.name=0;GL.framebuffers[id]=null}};var _glDeleteProgram=id=>{if(!id)return;var program=GL.programs[id];if(!program){GL.recordError(1281);return}GLctx.deleteProgram(program);program.name=0;GL.programs[id]=null};var _glDeleteQueries=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.deleteQuery(query);GL.queries[id]=null}};var _glDeleteRenderbuffers=(n,renderbuffers)=>{for(var i=0;i>2];var renderbuffer=GL.renderbuffers[id];if(!renderbuffer)continue;GLctx.deleteRenderbuffer(renderbuffer);renderbuffer.name=0;GL.renderbuffers[id]=null}};var _glDeleteSamplers=(n,samplers)=>{for(var i=0;i>2];var sampler=GL.samplers[id];if(!sampler)continue;GLctx.deleteSampler(sampler);sampler.name=0;GL.samplers[id]=null}};var _glDeleteShader=id=>{if(!id)return;var shader=GL.shaders[id];if(!shader){GL.recordError(1281);return}GLctx.deleteShader(shader);GL.shaders[id]=null};var _glDeleteSync=id=>{if(!id)return;var sync=GL.syncs[id];if(!sync){GL.recordError(1281);return}GLctx.deleteSync(sync);sync.name=0;GL.syncs[id]=null};var _glDeleteTextures=(n,textures)=>{for(var i=0;i>2];var texture=GL.textures[id];if(!texture)continue;GLctx.deleteTexture(texture);texture.name=0;GL.textures[id]=null}};var _glDeleteVertexArrays=(n,vaos)=>{for(var i=0;i>2];GLctx.deleteVertexArray(GL.vaos[id]);GL.vaos[id]=null}};var _glDepthFunc=x0=>GLctx.depthFunc(x0);var _glDepthMask=flag=>{GLctx.depthMask(!!flag)};var _glDepthRangef=(x0,x1)=>GLctx.depthRange(x0,x1);var _glDetachShader=(program,shader)=>{GLctx.detachShader(GL.programs[program],GL.shaders[shader])};var _glDisable=x0=>GLctx.disable(x0);var _glDisableVertexAttribArray=index=>{var cb=GL.currentContext.clientBuffers[index];cb.enabled=false;GLctx.disableVertexAttribArray(index)};var tempFixedLengthArray=[];var _glDrawBuffers=(n,bufs)=>{var bufArray=tempFixedLengthArray[n];for(var i=0;i>2]}GLctx.drawBuffers(bufArray)};var _glDrawElements=(mode,count,type,indices)=>{var buf;if(!GLctx.currentElementArrayBufferBinding){var size=GL.calcBufLength(1,type,0,count);buf=GL.getTempIndexBuffer(size);GLctx.bindBuffer(34963,buf);GLctx.bufferSubData(34963,0,HEAPU8.subarray(indices,indices+size));indices=0}GL.preDrawHandleClientVertexAttribBindings(count);GLctx.drawElements(mode,count,type,indices);GL.postDrawHandleClientVertexAttribBindings(count);if(!GLctx.currentElementArrayBufferBinding){GLctx.bindBuffer(34963,null)}};var _glDrawElementsInstanced=(mode,count,type,indices,primcount)=>{GLctx.drawElementsInstanced(mode,count,type,indices,primcount)};var _glEnable=x0=>GLctx.enable(x0);var _glEnableVertexAttribArray=index=>{var cb=GL.currentContext.clientBuffers[index];cb.enabled=true;GLctx.enableVertexAttribArray(index)};var _glEndQuery=x0=>GLctx.endQuery(x0);var _glFenceSync=(condition,flags)=>{var sync=GLctx.fenceSync(condition,flags);if(sync){var id=GL.getNewId(GL.syncs);sync.name=id;GL.syncs[id]=sync;return id}return 0};var _glFinish=()=>GLctx.finish();var _glFlush=()=>GLctx.flush();var _glFramebufferRenderbuffer=(target,attachment,renderbuffertarget,renderbuffer)=>{GLctx.framebufferRenderbuffer(target,attachment,renderbuffertarget,GL.renderbuffers[renderbuffer])};var _glFramebufferTexture2D=(target,attachment,textarget,texture,level)=>{GLctx.framebufferTexture2D(target,attachment,textarget,GL.textures[texture],level)};var _glFramebufferTextureLayer=(target,attachment,texture,level,layer)=>{GLctx.framebufferTextureLayer(target,attachment,GL.textures[texture],level,layer)};var _glFrontFace=x0=>GLctx.frontFace(x0);var _glGenBuffers=(n,buffers)=>{GL.genObject(n,buffers,"createBuffer",GL.buffers)};var _glGenFramebuffers=(n,ids)=>{GL.genObject(n,ids,"createFramebuffer",GL.framebuffers)};var _glGenQueries=(n,ids)=>{GL.genObject(n,ids,"createQuery",GL.queries)};var _glGenRenderbuffers=(n,renderbuffers)=>{GL.genObject(n,renderbuffers,"createRenderbuffer",GL.renderbuffers)};var _glGenSamplers=(n,samplers)=>{GL.genObject(n,samplers,"createSampler",GL.samplers)};var _glGenTextures=(n,textures)=>{GL.genObject(n,textures,"createTexture",GL.textures)};var _glGenVertexArrays=(n,arrays)=>{GL.genObject(n,arrays,"createVertexArray",GL.vaos)};var _glGenerateMipmap=x0=>GLctx.generateMipmap(x0);var _glGetBufferSubData=(target,offset,size,data)=>{if(!data){GL.recordError(1281);return}size&&GLctx.getBufferSubData(target,offset,HEAPU8,data,size)};var _glGetError=()=>{var error=GLctx.getError()||GL.lastError;GL.lastError=0;return error};var writeI53ToI64=(ptr,num)=>{HEAPU32[ptr>>2]=num;var lower=HEAPU32[ptr>>2];HEAPU32[ptr+4>>2]=(num-lower)/4294967296};var webglGetExtensions=function $webglGetExtensions(){var exts=getEmscriptenSupportedExtensions(GLctx);exts=exts.concat(exts.map(e=>"GL_"+e));return exts};var emscriptenWebGLGet=(name_,p,type)=>{if(!p){GL.recordError(1281);return}var ret=undefined;switch(name_){case 36346:ret=1;break;case 36344:if(type!=0&&type!=1){GL.recordError(1280)}return;case 34814:case 36345:ret=0;break;case 34466:var formats=GLctx.getParameter(34467);ret=formats?formats.length:0;break;case 33309:if(GL.currentContext.version<2){GL.recordError(1282);return}ret=webglGetExtensions().length;break;case 33307:case 33308:if(GL.currentContext.version<2){GL.recordError(1280);return}ret=name_==33307?3:0;break}if(ret===undefined){var result=GLctx.getParameter(name_);switch(typeof result){case"number":ret=result;break;case"boolean":ret=result?1:0;break;case"string":GL.recordError(1280);return;case"object":if(result===null){switch(name_){case 34964:case 35725:case 34965:case 36006:case 36007:case 32873:case 34229:case 36662:case 36663:case 35053:case 35055:case 36010:case 35097:case 35869:case 32874:case 36389:case 35983:case 35368:case 34068:{ret=0;break}default:{GL.recordError(1280);return}}}else if(result instanceof Float32Array||result instanceof Uint32Array||result instanceof Int32Array||result instanceof Array){for(var i=0;i>2]=result[i];break;case 2:HEAPF32[p+i*4>>2]=result[i];break;case 4:HEAP8[p+i]=result[i]?1:0;break}}return}else{try{ret=result.name|0}catch(e){GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`);return}}break;default:GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof result}!`);return}}switch(type){case 1:writeI53ToI64(p,ret);break;case 0:HEAP32[p>>2]=ret;break;case 2:HEAPF32[p>>2]=ret;break;case 4:HEAP8[p]=ret?1:0;break}};var _glGetFloatv=(name_,p)=>emscriptenWebGLGet(name_,p,2);var _glGetIntegerv=(name_,p)=>emscriptenWebGLGet(name_,p,0);var _glGetProgramBinary=(program,bufSize,length,binaryFormat,binary)=>{GL.recordError(1282)};var _glGetProgramInfoLog=(program,maxLength,length,infoLog)=>{var log=GLctx.getProgramInfoLog(GL.programs[program]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _glGetProgramiv=(program,pname,p)=>{if(!p){GL.recordError(1281);return}if(program>=GL.counter){GL.recordError(1281);return}program=GL.programs[program];if(pname==35716){var log=GLctx.getProgramInfoLog(program);if(log===null)log="(unknown error)";HEAP32[p>>2]=log.length+1}else if(pname==35719){if(!program.maxUniformLength){for(var i=0;i>2]=program.maxUniformLength}else if(pname==35722){if(!program.maxAttributeLength){for(var i=0;i>2]=program.maxAttributeLength}else if(pname==35381){if(!program.maxUniformBlockNameLength){for(var i=0;i>2]=program.maxUniformBlockNameLength}else{HEAP32[p>>2]=GLctx.getProgramParameter(program,pname)}};var _glGetQueryObjectuiv=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.getQueryParameter(query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetShaderInfoLog=(shader,maxLength,length,infoLog)=>{var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _glGetShaderiv=(shader,pname,p)=>{if(!p){GL.recordError(1281);return}if(pname==35716){var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var logLength=log?log.length+1:0;HEAP32[p>>2]=logLength}else if(pname==35720){var source=GLctx.getShaderSource(GL.shaders[shader]);var sourceLength=source?source.length+1:0;HEAP32[p>>2]=sourceLength}else{HEAP32[p>>2]=GLctx.getShaderParameter(GL.shaders[shader],pname)}};var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var _glGetString=name_=>{var ret=GL.stringCache[name_];if(!ret){switch(name_){case 7939:ret=stringToNewUTF8(webglGetExtensions().join(" "));break;case 7936:case 7937:case 37445:case 37446:var s=GLctx.getParameter(name_);if(!s){GL.recordError(1280)}ret=s?stringToNewUTF8(s):0;break;case 7938:var glVersion=GLctx.getParameter(7938);if(true)glVersion=`OpenGL ES 3.0 (${glVersion})`;else{glVersion=`OpenGL ES 2.0 (${glVersion})`}ret=stringToNewUTF8(glVersion);break;case 35724:var glslVersion=GLctx.getParameter(35724);var ver_re=/^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/;var ver_num=glslVersion.match(ver_re);if(ver_num!==null){if(ver_num[1].length==3)ver_num[1]=ver_num[1]+"0";glslVersion=`OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`}ret=stringToNewUTF8(glslVersion);break;default:GL.recordError(1280)}GL.stringCache[name_]=ret}return ret};var _glGetUniformBlockIndex=(program,uniformBlockName)=>GLctx.getUniformBlockIndex(GL.programs[program],UTF8ToString(uniformBlockName));var jstoi_q=str=>parseInt(str);var webglGetLeftBracePos=name=>name.slice(-1)=="]"&&name.lastIndexOf("[");var webglPrepareUniformLocationsBeforeFirstUse=program=>{var uniformLocsById=program.uniformLocsById,uniformSizeAndIdsByName=program.uniformSizeAndIdsByName,i,j;if(!uniformLocsById){program.uniformLocsById=uniformLocsById={};program.uniformArrayNamesById={};for(i=0;i0?nm.slice(0,lb):nm;var id=program.uniformIdCounter;program.uniformIdCounter+=sz;uniformSizeAndIdsByName[arrayName]=[sz,id];for(j=0;j{name=UTF8ToString(name);if(program=GL.programs[program]){webglPrepareUniformLocationsBeforeFirstUse(program);var uniformLocsById=program.uniformLocsById;var arrayIndex=0;var uniformBaseName=name;var leftBrace=webglGetLeftBracePos(name);if(leftBrace>0){arrayIndex=jstoi_q(name.slice(leftBrace+1))>>>0;uniformBaseName=name.slice(0,leftBrace)}var sizeAndId=program.uniformSizeAndIdsByName[uniformBaseName];if(sizeAndId&&arrayIndexGLctx.hint(x0,x1);var _glInvalidateFramebuffer=(target,numAttachments,attachments)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateFramebuffer(target,list)};var _glLinkProgram=program=>{program=GL.programs[program];GLctx.linkProgram(program);program.uniformLocsById=0;program.uniformSizeAndIdsByName={}};var emscriptenWebGLGetBufferBinding=target=>{switch(target){case 34962:target=34964;break;case 34963:target=34965;break;case 35051:target=35053;break;case 35052:target=35055;break;case 35982:target=35983;break;case 36662:target=36662;break;case 36663:target=36663;break;case 35345:target=35368;break}var buffer=GLctx.getParameter(target);if(buffer)return buffer.name|0;else return 0};var emscriptenWebGLValidateMapBufferTarget=target=>{switch(target){case 34962:case 34963:case 36662:case 36663:case 35051:case 35052:case 35882:case 35982:case 35345:return true;default:return false}};var _glMapBufferRange=(target,offset,length,access)=>{if((access&(1|32))!=0){err("glMapBufferRange access does not support MAP_READ or MAP_UNSYNCHRONIZED");return 0}if((access&2)==0){err("glMapBufferRange access must include MAP_WRITE");return 0}if((access&(4|8))==0){err("glMapBufferRange access must include INVALIDATE_BUFFER or INVALIDATE_RANGE");return 0}if(!emscriptenWebGLValidateMapBufferTarget(target)){GL.recordError(1280);err("GL_INVALID_ENUM in glMapBufferRange");return 0}var mem=_malloc(length),binding=emscriptenWebGLGetBufferBinding(target);if(!mem)return 0;if(!GL.mappedBuffers[binding])GL.mappedBuffers[binding]={};binding=GL.mappedBuffers[binding];binding.offset=offset;binding.length=length;binding.mem=mem;binding.access=access;return mem};var _glPixelStorei=(pname,param)=>{if(pname==3317){GL.unpackAlignment=param}GLctx.pixelStorei(pname,param)};var _glPolygonOffset=(x0,x1)=>GLctx.polygonOffset(x0,x1);var _glProgramBinary=(program,binaryFormat,binary,length)=>{GL.recordError(1280)};var computeUnpackAlignedImageSize=(width,height,sizePerPixel,alignment)=>{function roundedToNextMultipleOf(x,y){return x+y-1&-y}var plainRowSize=width*sizePerPixel;var alignedRowSize=roundedToNextMultipleOf(plainRowSize,alignment);return height*alignedRowSize};var colorChannelsInGlTextureFormat=format=>{var colorChannels={5:3,6:4,8:2,29502:3,29504:4,26917:2,26918:2,29846:3,29847:4};return colorChannels[format-6402]||1};var heapObjectForWebGLType=type=>{type-=5120;if(type==0)return HEAP8;if(type==1)return HEAPU8;if(type==2)return HEAP16;if(type==4)return HEAP32;if(type==6)return HEAPF32;if(type==5||type==28922||type==28520||type==30779||type==30782)return HEAPU32;return HEAPU16};var toTypedArrayIndex=(pointer,heap)=>pointer>>>31-Math.clz32(heap.BYTES_PER_ELEMENT);var emscriptenWebGLGetTexPixelData=(type,format,width,height,pixels,internalFormat)=>{var heap=heapObjectForWebGLType(type);var sizePerPixel=colorChannelsInGlTextureFormat(format)*heap.BYTES_PER_ELEMENT;var bytes=computeUnpackAlignedImageSize(width,height,sizePerPixel,GL.unpackAlignment);return heap.subarray(toTypedArrayIndex(pixels,heap),toTypedArrayIndex(pixels+bytes,heap))};var _glReadPixels=(x,y,width,height,format,type,pixels)=>{if(true){if(GLctx.currentPixelPackBufferBinding){GLctx.readPixels(x,y,width,height,format,type,pixels);return}var heap=heapObjectForWebGLType(type);var target=toTypedArrayIndex(pixels,heap);GLctx.readPixels(x,y,width,height,format,type,heap,target);return}var pixelData=emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,format);if(!pixelData){GL.recordError(1280);return}GLctx.readPixels(x,y,width,height,format,type,pixelData)};var _glRenderbufferStorage=(x0,x1,x2,x3)=>GLctx.renderbufferStorage(x0,x1,x2,x3);var _glRenderbufferStorageMultisample=(x0,x1,x2,x3,x4)=>GLctx.renderbufferStorageMultisample(x0,x1,x2,x3,x4);var _glSamplerParameterf=(sampler,pname,param)=>{GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _glSamplerParameteri=(sampler,pname,param)=>{GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _glScissor=(x0,x1,x2,x3)=>GLctx.scissor(x0,x1,x2,x3);var _glShaderSource=(shader,count,string,length)=>{var source=GL.getSource(shader,count,string,length);GLctx.shaderSource(GL.shaders[shader],source)};var _glStencilFuncSeparate=(x0,x1,x2,x3)=>GLctx.stencilFuncSeparate(x0,x1,x2,x3);var _glStencilMaskSeparate=(x0,x1)=>GLctx.stencilMaskSeparate(x0,x1);var _glStencilOpSeparate=(x0,x1,x2,x3)=>GLctx.stencilOpSeparate(x0,x1,x2,x3);var _glTexImage2D=(target,level,internalFormat,width,height,border,format,type,pixels)=>{if(true){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);var index=toTypedArrayIndex(pixels,heap);GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,heap,index);return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,internalFormat):null;GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixelData)};var _glTexParameterf=(x0,x1,x2)=>GLctx.texParameterf(x0,x1,x2);var _glTexParameteri=(x0,x1,x2)=>GLctx.texParameteri(x0,x1,x2);var _glTexStorage2D=(x0,x1,x2,x3,x4)=>GLctx.texStorage2D(x0,x1,x2,x3,x4);var _glTexStorage3D=(x0,x1,x2,x3,x4,x5)=>GLctx.texStorage3D(x0,x1,x2,x3,x4,x5);var _glTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,type,pixels)=>{if(true){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,heap,toTypedArrayIndex(pixels,heap));return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,0):null;GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixelData)};var _glTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,null)}};var webglGetUniformLocation=location=>{var p=GLctx.currentProgram;if(p){var webglLoc=p.uniformLocsById[location];if(typeof webglLoc=="number"){p.uniformLocsById[location]=webglLoc=GLctx.getUniformLocation(p,p.uniformArrayNamesById[location]+(webglLoc>0?`[${webglLoc}]`:""))}return webglLoc}else{GL.recordError(1282)}};var _glUniform1f=(location,v0)=>{GLctx.uniform1f(webglGetUniformLocation(location),v0)};var _glUniform1fv=(location,count,value)=>{count&&GLctx.uniform1fv(webglGetUniformLocation(location),HEAPF32,value>>2,count)};var _glUniform1i=(location,v0)=>{GLctx.uniform1i(webglGetUniformLocation(location),v0)};var _glUniform1iv=(location,count,value)=>{count&&GLctx.uniform1iv(webglGetUniformLocation(location),HEAP32,value>>2,count)};var _glUniform2fv=(location,count,value)=>{count&&GLctx.uniform2fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*2)};var _glUniform2iv=(location,count,value)=>{count&&GLctx.uniform2iv(webglGetUniformLocation(location),HEAP32,value>>2,count*2)};var _glUniform3fv=(location,count,value)=>{count&&GLctx.uniform3fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*3)};var _glUniform3iv=(location,count,value)=>{count&&GLctx.uniform3iv(webglGetUniformLocation(location),HEAP32,value>>2,count*3)};var _glUniform4fv=(location,count,value)=>{count&&GLctx.uniform4fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*4)};var _glUniform4iv=(location,count,value)=>{count&&GLctx.uniform4iv(webglGetUniformLocation(location),HEAP32,value>>2,count*4)};var _glUniformBlockBinding=(program,uniformBlockIndex,uniformBlockBinding)=>{program=GL.programs[program];GLctx.uniformBlockBinding(program,uniformBlockIndex,uniformBlockBinding)};var _glUniformMatrix3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*9)};var _glUniformMatrix4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*16)};var _glUnmapBuffer=target=>{if(!emscriptenWebGLValidateMapBufferTarget(target)){GL.recordError(1280);err("GL_INVALID_ENUM in glUnmapBuffer");return 0}var buffer=emscriptenWebGLGetBufferBinding(target);var mapping=GL.mappedBuffers[buffer];if(!mapping||!mapping.mem){GL.recordError(1282);err("buffer was never mapped in glUnmapBuffer");return 0}if(!(mapping.access&16)){if(true){GLctx.bufferSubData(target,mapping.offset,HEAPU8,mapping.mem,mapping.length)}else GLctx.bufferSubData(target,mapping.offset,HEAPU8.subarray(mapping.mem,mapping.mem+mapping.length))}_free(mapping.mem);mapping.mem=0;return 1};var _glUseProgram=program=>{program=GL.programs[program];GLctx.useProgram(program);GLctx.currentProgram=program};var _glVertexAttrib4f=(x0,x1,x2,x3,x4)=>GLctx.vertexAttrib4f(x0,x1,x2,x3,x4);var _glVertexAttribI4ui=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4ui(x0,x1,x2,x3,x4);var _glVertexAttribIPointer=(index,size,type,stride,ptr)=>{var cb=GL.currentContext.clientBuffers[index];if(!GLctx.currentArrayBufferBinding){cb.size=size;cb.type=type;cb.normalized=false;cb.stride=stride;cb.ptr=ptr;cb.clientside=true;cb.vertexAttribPointerAdaptor=function(index,size,type,normalized,stride,ptr){this.vertexAttribIPointer(index,size,type,stride,ptr)};return}cb.clientside=false;GLctx.vertexAttribIPointer(index,size,type,stride,ptr)};var _glVertexAttribPointer=(index,size,type,normalized,stride,ptr)=>{var cb=GL.currentContext.clientBuffers[index];if(!GLctx.currentArrayBufferBinding){cb.size=size;cb.type=type;cb.normalized=normalized;cb.stride=stride;cb.ptr=ptr;cb.clientside=true;cb.vertexAttribPointerAdaptor=function(index,size,type,normalized,stride,ptr){this.vertexAttribPointer(index,size,type,normalized,stride,ptr)};return}cb.clientside=false;GLctx.vertexAttribPointer(index,size,type,!!normalized,stride,ptr)};var _glViewport=(x0,x1,x2,x3)=>GLctx.viewport(x0,x1,x2,x3);var isLeapYear=year=>year%4===0&&(year%100!==0||year%400===0);var arraySum=(array,index)=>{var sum=0;for(var i=0;i<=index;sum+=array[i++]){}return sum};var MONTH_DAYS_LEAP=[31,29,31,30,31,30,31,31,30,31,30,31];var MONTH_DAYS_REGULAR=[31,28,31,30,31,30,31,31,30,31,30,31];var addDays=(date,days)=>{var newDate=new Date(date.getTime());while(days>0){var leap=isLeapYear(newDate.getFullYear());var currentMonth=newDate.getMonth();var daysInCurrentMonth=(leap?MONTH_DAYS_LEAP:MONTH_DAYS_REGULAR)[currentMonth];if(days>daysInCurrentMonth-newDate.getDate()){days-=daysInCurrentMonth-newDate.getDate()+1;newDate.setDate(1);if(currentMonth<11){newDate.setMonth(currentMonth+1)}else{newDate.setMonth(0);newDate.setFullYear(newDate.getFullYear()+1)}}else{newDate.setDate(newDate.getDate()+days);return newDate}}return newDate};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var _strftime=(s,maxsize,format,tm)=>{var tm_zone=HEAPU32[tm+40>>2];var date={tm_sec:HEAP32[tm>>2],tm_min:HEAP32[tm+4>>2],tm_hour:HEAP32[tm+8>>2],tm_mday:HEAP32[tm+12>>2],tm_mon:HEAP32[tm+16>>2],tm_year:HEAP32[tm+20>>2],tm_wday:HEAP32[tm+24>>2],tm_yday:HEAP32[tm+28>>2],tm_isdst:HEAP32[tm+32>>2],tm_gmtoff:HEAP32[tm+36>>2],tm_zone:tm_zone?UTF8ToString(tm_zone):""};var pattern=UTF8ToString(format);var EXPANSION_RULES_1={"%c":"%a %b %d %H:%M:%S %Y","%D":"%m/%d/%y","%F":"%Y-%m-%d","%h":"%b","%r":"%I:%M:%S %p","%R":"%H:%M","%T":"%H:%M:%S","%x":"%m/%d/%y","%X":"%H:%M:%S","%Ec":"%c","%EC":"%C","%Ex":"%m/%d/%y","%EX":"%H:%M:%S","%Ey":"%y","%EY":"%Y","%Od":"%d","%Oe":"%e","%OH":"%H","%OI":"%I","%Om":"%m","%OM":"%M","%OS":"%S","%Ou":"%u","%OU":"%U","%OV":"%V","%Ow":"%w","%OW":"%W","%Oy":"%y"};for(var rule in EXPANSION_RULES_1){pattern=pattern.replace(new RegExp(rule,"g"),EXPANSION_RULES_1[rule])}var WEEKDAYS=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];var MONTHS=["January","February","March","April","May","June","July","August","September","October","November","December"];function leadingSomething(value,digits,character){var str=typeof value=="number"?value.toString():value||"";while(str.length0?1:0}var compare;if((compare=sgn(date1.getFullYear()-date2.getFullYear()))===0){if((compare=sgn(date1.getMonth()-date2.getMonth()))===0){compare=sgn(date1.getDate()-date2.getDate())}}return compare}function getFirstWeekStartDate(janFourth){switch(janFourth.getDay()){case 0:return new Date(janFourth.getFullYear()-1,11,29);case 1:return janFourth;case 2:return new Date(janFourth.getFullYear(),0,3);case 3:return new Date(janFourth.getFullYear(),0,2);case 4:return new Date(janFourth.getFullYear(),0,1);case 5:return new Date(janFourth.getFullYear()-1,11,31);case 6:return new Date(janFourth.getFullYear()-1,11,30)}}function getWeekBasedYear(date){var thisDate=addDays(new Date(date.tm_year+1900,0,1),date.tm_yday);var janFourthThisYear=new Date(thisDate.getFullYear(),0,4);var janFourthNextYear=new Date(thisDate.getFullYear()+1,0,4);var firstWeekStartThisYear=getFirstWeekStartDate(janFourthThisYear);var firstWeekStartNextYear=getFirstWeekStartDate(janFourthNextYear);if(compareByDay(firstWeekStartThisYear,thisDate)<=0){if(compareByDay(firstWeekStartNextYear,thisDate)<=0){return thisDate.getFullYear()+1}return thisDate.getFullYear()}return thisDate.getFullYear()-1}var EXPANSION_RULES_2={"%a":date=>WEEKDAYS[date.tm_wday].substring(0,3),"%A":date=>WEEKDAYS[date.tm_wday],"%b":date=>MONTHS[date.tm_mon].substring(0,3),"%B":date=>MONTHS[date.tm_mon],"%C":date=>{var year=date.tm_year+1900;return leadingNulls(year/100|0,2)},"%d":date=>leadingNulls(date.tm_mday,2),"%e":date=>leadingSomething(date.tm_mday,2," "),"%g":date=>getWeekBasedYear(date).toString().substring(2),"%G":getWeekBasedYear,"%H":date=>leadingNulls(date.tm_hour,2),"%I":date=>{var twelveHour=date.tm_hour;if(twelveHour==0)twelveHour=12;else if(twelveHour>12)twelveHour-=12;return leadingNulls(twelveHour,2)},"%j":date=>leadingNulls(date.tm_mday+arraySum(isLeapYear(date.tm_year+1900)?MONTH_DAYS_LEAP:MONTH_DAYS_REGULAR,date.tm_mon-1),3),"%m":date=>leadingNulls(date.tm_mon+1,2),"%M":date=>leadingNulls(date.tm_min,2),"%n":()=>"\n","%p":date=>{if(date.tm_hour>=0&&date.tm_hour<12){return"AM"}return"PM"},"%S":date=>leadingNulls(date.tm_sec,2),"%t":()=>"\t","%u":date=>date.tm_wday||7,"%U":date=>{var days=date.tm_yday+7-date.tm_wday;return leadingNulls(Math.floor(days/7),2)},"%V":date=>{var val=Math.floor((date.tm_yday+7-(date.tm_wday+6)%7)/7);if((date.tm_wday+371-date.tm_yday-2)%7<=2){val++}if(!val){val=52;var dec31=(date.tm_wday+7-date.tm_yday-1)%7;if(dec31==4||dec31==5&&isLeapYear(date.tm_year%400-1)){val++}}else if(val==53){var jan1=(date.tm_wday+371-date.tm_yday)%7;if(jan1!=4&&(jan1!=3||!isLeapYear(date.tm_year)))val=1}return leadingNulls(val,2)},"%w":date=>date.tm_wday,"%W":date=>{var days=date.tm_yday+7-(date.tm_wday+6)%7;return leadingNulls(Math.floor(days/7),2)},"%y":date=>(date.tm_year+1900).toString().substring(2),"%Y":date=>date.tm_year+1900,"%z":date=>{var off=date.tm_gmtoff;var ahead=off>=0;off=Math.abs(off)/60;off=off/60*100+off%60;return(ahead?"+":"-")+String("0000"+off).slice(-4)},"%Z":date=>date.tm_zone,"%%":()=>"%"};pattern=pattern.replace(/%%/g,"\0\0");for(var rule in EXPANSION_RULES_2){if(pattern.includes(rule)){pattern=pattern.replace(new RegExp(rule,"g"),EXPANSION_RULES_2[rule](date))}}pattern=pattern.replace(/\0\0/g,"%");var bytes=intArrayFromString(pattern,false);if(bytes.length>maxsize){return 0}writeArrayToMemory(bytes,s);return bytes.length-1};var _strftime_l=(s,maxsize,format,tm,loc)=>_strftime(s,maxsize,format,tm);FS.createPreloadedFile=FS_createPreloadedFile;FS.staticInit();InternalError=Module["InternalError"]=class InternalError extends Error{constructor(message){super(message);this.name="InternalError"}};embind_init_charCodes();BindingError=Module["BindingError"]=class BindingError extends Error{constructor(message){super(message);this.name="BindingError"}};init_ClassHandle();init_embind();init_RegisteredPointer();UnboundTypeError=Module["UnboundTypeError"]=extendError(Error,"UnboundTypeError");init_emval();var GLctx;for(var i=0;i<32;++i)tempFixedLengthArray.push(new Array(i));var wasmImports={Ga:___syscall_fcntl64,Hb:___syscall_ioctl,Ib:___syscall_openat,Db:___syscall_stat64,Nb:__abort_js,v:__embind_finalize_value_array,i:__embind_finalize_value_object,yb:__embind_register_bigint,tb:__embind_register_bool,d:__embind_register_class,h:__embind_register_class_class_function,k:__embind_register_class_constructor,a:__embind_register_class_function,t:__embind_register_class_property,Xa:__embind_register_emval,f:__embind_register_enum,b:__embind_register_enum_value,Da:__embind_register_float,_:__embind_register_function,A:__embind_register_integer,l:__embind_register_memory_view,Z:__embind_register_optional,Ba:__embind_register_std_string,ia:__embind_register_std_wstring,w:__embind_register_value_array,e:__embind_register_value_array_element,j:__embind_register_value_object,c:__embind_register_value_object_field,vb:__embind_register_void,Kb:__emscripten_get_now_is_monotonic,Mb:__emscripten_memcpy_js,o:__emval_as,g:__emval_decref,p:__emval_get_property,ub:__emval_incref,F:__emval_new_cstring,n:__emval_run_destructors,q:__emval_take_value,ma:_emscripten_asm_const_int,Lb:_emscripten_date_now,sb:_emscripten_err,Cb:_emscripten_get_heap_max,Jb:_emscripten_get_now,rb:_emscripten_out,Bb:_emscripten_resize_heap,Eb:_environ_get,Fb:_environ_sizes_get,na:_fd_close,Gb:_fd_read,wb:_fd_seek,Fa:_fd_write,zb:_getentropy,r:_glActiveTexture,ka:_glAttachShader,db:_glBeginQuery,lb:_glBindAttribLocation,z:_glBindBuffer,sa:_glBindBufferBase,Ua:_glBindBufferRange,R:_glBindFramebuffer,Ma:_glBindRenderbuffer,W:_glBindSampler,x:_glBindTexture,$a:_glBindVertexArray,va:_glBlendEquationSeparate,ua:_glBlendFuncSeparate,aa:_glBlitFramebuffer,C:_glBufferData,ba:_glBufferSubData,Rb:_glClear,Wb:_glClearBufferfi,B:_glClearBufferfv,Vb:_glClearBufferiv,Ub:_glClearColor,Tb:_glClearDepthf,Sb:_glClearStencil,xb:_glClientWaitSync,ha:_glColorMask,mb:_glCompileShader,Ja:_glCompressedTexSubImage2D,Ia:_glCompressedTexSubImage3D,fc:_glCopyBufferSubData,Ea:_glCreateProgram,ob:_glCreateShader,wa:_glCullFace,fa:_glDeleteBuffers,S:_glDeleteFramebuffers,la:_glDeleteProgram,cb:_glDeleteQueries,Ra:_glDeleteRenderbuffers,ya:_glDeleteSamplers,X:_glDeleteShader,Oa:_glDeleteSync,Sa:_glDeleteTextures,ab:_glDeleteVertexArrays,ja:_glDepthFunc,ga:_glDepthMask,pa:_glDepthRangef,Y:_glDetachShader,m:_glDisable,Zb:_glDisableVertexAttribArray,ec:_glDrawBuffers,Ha:_glDrawElements,Pa:_glDrawElementsInstanced,u:_glEnable,_b:_glEnableVertexAttribArray,eb:_glEndQuery,ca:_glFenceSync,Ca:_glFinish,hb:_glFlush,G:_glFramebufferRenderbuffer,y:_glFramebufferTexture2D,O:_glFramebufferTextureLayer,xa:_glFrontFace,T:_glGenBuffers,$:_glGenFramebuffers,bb:_glGenQueries,oa:_glGenRenderbuffers,Aa:_glGenSamplers,H:_glGenTextures,_a:_glGenVertexArrays,hc:_glGenerateMipmap,Qb:_glGetBufferSubData,N:_glGetError,Za:_glGetFloatv,s:_glGetIntegerv,pb:_glGetProgramBinary,ib:_glGetProgramInfoLog,K:_glGetProgramiv,fb:_glGetQueryObjectuiv,jb:_glGetShaderInfoLog,E:_glGetShaderiv,J:_glGetString,Wa:_glGetUniformBlockIndex,ea:_glGetUniformLocation,Ya:_glHint,gb:_glInvalidateFramebuffer,kb:_glLinkProgram,Pb:_glMapBufferRange,P:_glPixelStorei,ta:_glPolygonOffset,qb:_glProgramBinary,Qa:_glReadPixels,Xb:_glRenderbufferStorage,Yb:_glRenderbufferStorageMultisample,za:_glSamplerParameterf,I:_glSamplerParameteri,ra:_glScissor,nb:_glShaderSource,V:_glStencilFuncSeparate,D:_glStencilMaskSeparate,U:_glStencilOpSeparate,L:_glTexImage2D,Ta:_glTexParameterf,M:_glTexParameteri,dc:_glTexStorage2D,Na:_glTexStorage3D,La:_glTexSubImage2D,Ka:_glTexSubImage3D,gc:_glUniform1f,rc:_glUniform1fv,da:_glUniform1i,nc:_glUniform1iv,qc:_glUniform2fv,mc:_glUniform2iv,pc:_glUniform3fv,lc:_glUniform3iv,oc:_glUniform4fv,kc:_glUniform4iv,Va:_glUniformBlockBinding,jc:_glUniformMatrix3fv,ic:_glUniformMatrix4fv,Ob:_glUnmapBuffer,Q:_glUseProgram,$b:_glVertexAttrib4f,ac:_glVertexAttribI4ui,cc:_glVertexAttribIPointer,bc:_glVertexAttribPointer,qa:_glViewport,Ab:_strftime_l};var wasmExports=createWasm();var ___wasm_call_ctors=()=>(___wasm_call_ctors=wasmExports["tc"])();var ___getTypeName=a0=>(___getTypeName=wasmExports["uc"])(a0);var _malloc=a0=>(_malloc=wasmExports["wc"])(a0);var _free=a0=>(_free=wasmExports["xc"])(a0);var dynCall_ji=Module["dynCall_ji"]=(a0,a1)=>(dynCall_ji=Module["dynCall_ji"]=wasmExports["yc"])(a0,a1);var dynCall_j=Module["dynCall_j"]=a0=>(dynCall_j=Module["dynCall_j"]=wasmExports["zc"])(a0);var dynCall_vij=Module["dynCall_vij"]=(a0,a1,a2,a3)=>(dynCall_vij=Module["dynCall_vij"]=wasmExports["Ac"])(a0,a1,a2,a3);var dynCall_viij=Module["dynCall_viij"]=(a0,a1,a2,a3,a4)=>(dynCall_viij=Module["dynCall_viij"]=wasmExports["Bc"])(a0,a1,a2,a3,a4);var dynCall_iiiiij=Module["dynCall_iiiiij"]=(a0,a1,a2,a3,a4,a5,a6)=>(dynCall_iiiiij=Module["dynCall_iiiiij"]=wasmExports["Cc"])(a0,a1,a2,a3,a4,a5,a6);var dynCall_jii=Module["dynCall_jii"]=(a0,a1,a2)=>(dynCall_jii=Module["dynCall_jii"]=wasmExports["Dc"])(a0,a1,a2);var dynCall_iiij=Module["dynCall_iiij"]=(a0,a1,a2,a3,a4)=>(dynCall_iiij=Module["dynCall_iiij"]=wasmExports["Ec"])(a0,a1,a2,a3,a4);var dynCall_iiiij=Module["dynCall_iiiij"]=(a0,a1,a2,a3,a4,a5)=>(dynCall_iiiij=Module["dynCall_iiiij"]=wasmExports["Fc"])(a0,a1,a2,a3,a4,a5);var dynCall_vijji=Module["dynCall_vijji"]=(a0,a1,a2,a3,a4,a5,a6)=>(dynCall_vijji=Module["dynCall_vijji"]=wasmExports["Gc"])(a0,a1,a2,a3,a4,a5,a6);var dynCall_jiji=Module["dynCall_jiji"]=(a0,a1,a2,a3,a4)=>(dynCall_jiji=Module["dynCall_jiji"]=wasmExports["Hc"])(a0,a1,a2,a3,a4);var dynCall_viijii=Module["dynCall_viijii"]=(a0,a1,a2,a3,a4,a5,a6)=>(dynCall_viijii=Module["dynCall_viijii"]=wasmExports["Ic"])(a0,a1,a2,a3,a4,a5,a6);var dynCall_iiiiijj=Module["dynCall_iiiiijj"]=(a0,a1,a2,a3,a4,a5,a6,a7,a8)=>(dynCall_iiiiijj=Module["dynCall_iiiiijj"]=wasmExports["Jc"])(a0,a1,a2,a3,a4,a5,a6,a7,a8);var dynCall_iiiiiijj=Module["dynCall_iiiiiijj"]=(a0,a1,a2,a3,a4,a5,a6,a7,a8,a9)=>(dynCall_iiiiiijj=Module["dynCall_iiiiiijj"]=wasmExports["Kc"])(a0,a1,a2,a3,a4,a5,a6,a7,a8,a9);var calledRun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(){if(runDependencies>0){return}preRun();if(runDependencies>0){return}function doRun(){if(calledRun)return;calledRun=true;Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve(Module);if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(function(){setTimeout(function(){Module["setStatus"]("")},1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise; + + + return moduleRtn; +} +); +})(); +if (typeof exports === 'object' && typeof module === 'object') + module.exports = Filament; +else if (typeof define === 'function' && define['amd']) + define([], () => Filament); +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Filament.onReadyListeners = []; + +Filament.isReady = false; + +/// init ::function:: Downloads assets, loads the Filament module, and invokes a callback when done. +/// +/// All JavaScript clients must call the init function, passing in a list of asset URL's and a +/// callback. This callback gets invoked only after all assets have been downloaded and the Filament +/// WebAssembly module has been loaded. Clients should only pass asset URL's that absolutely must +/// be ready at initialization time. +/// +/// When the callback is called, each downloaded asset is available in the `Filament.assets` global +/// object, which contains a mapping from URL's to Uint8Array objects. +/// +/// assets ::argument:: Array of strings containing URL's of required assets. +/// onready ::argument:: callback that gets invoked after all assets have been downloaded and the \ +/// Filament WebAssembly module has been loaded. +Filament.init = (assets, onready) => { + if (onready) { + Filament.onReadyListeners.push(onready); + } + if (Filament.initialized) { + console.assert(!assets || assets.length == 0, "Assets can be specified only with the first call to init."); + return; + }; + Filament.initialized = true; + + Filament.assets = {}; + + // Usage of glmatrix is optional. If it exists, then go ahead and augment it with some + // useful math functions. + if (typeof glMatrix !== 'undefined') { + Filament.loadMathExtensions(); + } + + // One task for compiling & loading the wasm file, plus one task for each asset. + let remainingTasks = 1 + assets.length; + const taskFinished = () => { + if (--remainingTasks == 0) { + for (const callback of Filament.onReadyListeners) { + callback(); + } + Filament.isReady = true; + } + }; + + // Issue a fetch for each asset. + Filament.fetch(assets, null, taskFinished); + + // Emscripten creates a global function called "Filament" that returns a promise that + // resolves to a module. Here we replace the function with the module. Note that our + // TypeScript bindings assume that Filament is a namespace, not a function. + Filament().then(module => { + + // Merge our extension functions into the emscripten module, not the other + // way around, because Emscripten potentially replaces the HEAPU8 views in + // the original module object (e.g. if it needs to grow the heap). + Filament = Object.assign(module, Filament); + + // At this point, emscripten has finished compiling and instancing the WebAssembly module. + // The JS classes that correspond to core Filament classes (e.g., Engine) are not guaranteed + // to exist until now. + + Filament.loadClassExtensions(); + taskFinished(); + }); +}; + +Filament.clearAssetCache = () => { + for (const key in Filament.assets) delete Filament.assets[key]; +}; + +/// fetch ::function:: Downloads assets and invokes a callback when done. +/// +/// This utility consumes an array of URI strings and invokes callbacks after each asset is +/// downloaded. Additionally, each downloaded asset becomes available in the `Filament.assets` +/// global object, which is a mapping from URI strings to `Uint8Array`. If desired, clients can +/// pre-populate entries in `Filament.assets` to circumvent HTTP requests (this should be done after +/// calling `Filament.init`). +/// +/// This function is used internally by `Filament.init` and `gltfio$FilamentAsset.loadResources`. +/// +/// assets ::argument:: Array of strings containing URL's of required assets. +/// onDone ::argument:: callback that gets invoked after all assets have been downloaded. +/// onFetched ::argument:: optional callback that's invoked after each asset is downloaded. +Filament.fetch = (assets, onDone, onFetched) => { + let remainingAssets = assets.length; + assets.forEach(name => { + + // Check if a buffer already exists in case the client wishes + // to provide its own data rather than using a HTTP request. + if (Filament.assets[name]) { + if (onFetched) { + onFetched(name); + } + if (--remainingAssets === 0 && onDone) { + onDone(); + } + } else { + fetch(name).then(response => { + if (!response.ok) { + throw new Error(name); + } + return response.arrayBuffer(); + }).then(arrayBuffer => { + Filament.assets[name] = new Uint8Array(arrayBuffer); + if (onFetched) { + onFetched(name); + } + if (--remainingAssets === 0 && onDone) { + onDone(); + } + }); + } + }); +}; + +// This file has been generated by beamsplitter + +Filament.loadGeneratedExtensions = function() { + + Filament.View.prototype.setDynamicResolutionOptionsDefaults = function(overrides) { + const options = { + minScale: [0.5, 0.5], + maxScale: [1.0, 1.0], + sharpness: 0.9, + enabled: false, + homogeneousScaling: false, + quality: Filament.View$QualityLevel.LOW, + }; + return Object.assign(options, overrides); + }; + + Filament.View.prototype.setBloomOptionsDefaults = function(overrides) { + const options = { + // JavaScript binding for dirt is not yet supported, must use default value. + // JavaScript binding for dirtStrength is not yet supported, must use default value. + strength: 0.10, + resolution: 384, + levels: 6, + blendMode: Filament.View$BloomOptions$BlendMode.ADD, + threshold: true, + enabled: false, + highlight: 1000.0, + quality: Filament.View$QualityLevel.LOW, + lensFlare: false, + starburst: true, + chromaticAberration: 0.005, + ghostCount: 4, + ghostSpacing: 0.6, + ghostThreshold: 10.0, + haloThickness: 0.1, + haloRadius: 0.4, + haloThreshold: 10.0, + }; + return Object.assign(options, overrides); + }; + + Filament.View.prototype.setFogOptionsDefaults = function(overrides) { + const options = { + distance: 0.0, + cutOffDistance: Infinity, + maximumOpacity: 1.0, + height: 0.0, + heightFalloff: 1.0, + color: [ 1.0, 1.0, 1.0 ], + density: 0.1, + inScatteringStart: 0.0, + inScatteringSize: -1.0, + fogColorFromIbl: false, + // JavaScript binding for skyColor is not yet supported, must use default value. + enabled: false, + }; + return Object.assign(options, overrides); + }; + + Filament.View.prototype.setDepthOfFieldOptionsDefaults = function(overrides) { + const options = { + cocScale: 1.0, + cocAspectRatio: 1.0, + maxApertureDiameter: 0.01, + enabled: false, + filter: Filament.View$DepthOfFieldOptions$Filter.MEDIAN, + nativeResolution: false, + foregroundRingCount: 0, + backgroundRingCount: 0, + fastGatherRingCount: 0, + maxForegroundCOC: 0, + maxBackgroundCOC: 0, + }; + return Object.assign(options, overrides); + }; + + Filament.View.prototype.setVignetteOptionsDefaults = function(overrides) { + const options = { + midPoint: 0.5, + roundness: 0.5, + feather: 0.5, + color: [0.0, 0.0, 0.0, 1.0], + enabled: false, + }; + return Object.assign(options, overrides); + }; + + Filament.View.prototype.setRenderQualityDefaults = function(overrides) { + const options = { + hdrColorBuffer: Filament.View$QualityLevel.HIGH, + }; + return Object.assign(options, overrides); + }; + + Filament.View.prototype.setSsctDefaults = function(overrides) { + const options = { + lightConeRad: 1.0, + shadowDistance: 0.3, + contactDistanceMax: 1.0, + intensity: 0.8, + lightDirection: [ 0, -1, 0 ], + depthBias: 0.01, + depthSlopeBias: 0.01, + sampleCount: 4, + rayCount: 1, + enabled: false, + }; + return Object.assign(options, overrides); + }; + + Filament.View.prototype.setGtaoDefaults = function(overrides) { + const options = { + sampleSliceCount: 4, + sampleStepsPerSlice: 3, + thicknessHeuristic: 0.004, + }; + return Object.assign(options, overrides); + }; + + Filament.View.prototype.setAmbientOcclusionOptionsDefaults = function(overrides) { + const options = { + aoType: Filament.View$AmbientOcclusionOptions$AmbientOcclusionType.SAO, + radius: 0.3, + power: 1.0, + bias: 0.0005, + resolution: 0.5, + intensity: 1.0, + bilateralThreshold: 0.05, + quality: Filament.View$QualityLevel.LOW, + lowPassFilter: Filament.View$QualityLevel.MEDIUM, + upsampling: Filament.View$QualityLevel.LOW, + enabled: false, + bentNormals: false, + minHorizonAngleRad: 0.0, + // JavaScript binding for ssct is not yet supported, must use default value. + // JavaScript binding for gtao is not yet supported, must use default value. + }; + return Object.assign(options, overrides); + }; + + Filament.View.prototype.setMultiSampleAntiAliasingOptionsDefaults = function(overrides) { + const options = { + enabled: false, + sampleCount: 4, + customResolve: false, + }; + return Object.assign(options, overrides); + }; + + Filament.View.prototype.setTemporalAntiAliasingOptionsDefaults = function(overrides) { + const options = { + filterWidth: 1.0, + feedback: 0.12, + lodBias: -1.0, + sharpness: 0.0, + enabled: false, + upscaling: false, + filterHistory: true, + filterInput: true, + useYCoCg: false, + boxType: Filament.View$TemporalAntiAliasingOptions$BoxType.AABB, + boxClipping: Filament.View$TemporalAntiAliasingOptions$BoxClipping.ACCURATE, + jitterPattern: Filament.View$TemporalAntiAliasingOptions$JitterPattern.HALTON_23_X16, + varianceGamma: 1.0, + preventFlickering: false, + historyReprojection: true, + }; + return Object.assign(options, overrides); + }; + + Filament.View.prototype.setScreenSpaceReflectionsOptionsDefaults = function(overrides) { + const options = { + thickness: 0.1, + bias: 0.01, + maxDistance: 3.0, + stride: 2.0, + enabled: false, + }; + return Object.assign(options, overrides); + }; + + Filament.View.prototype.setGuardBandOptionsDefaults = function(overrides) { + const options = { + enabled: false, + }; + return Object.assign(options, overrides); + }; + + Filament.View.prototype.setVsmShadowOptionsDefaults = function(overrides) { + const options = { + anisotropy: 0, + mipmapping: false, + msaaSamples: 1, + highPrecision: false, + minVarianceScale: 0.5, + lightBleedReduction: 0.15, + }; + return Object.assign(options, overrides); + }; + + Filament.View.prototype.setSoftShadowOptionsDefaults = function(overrides) { + const options = { + penumbraScale: 1.0, + penumbraRatioScale: 1.0, + }; + return Object.assign(options, overrides); + }; + + Filament.View.prototype.setStereoscopicOptionsDefaults = function(overrides) { + const options = { + enabled: false, + }; + return Object.assign(options, overrides); + }; + +}; + +/* +* Copyright (C) 2018 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +// Private utility that converts an asset string or Uint8Array into a low-level buffer descriptor. +// Note that the low-level buffer descriptor must be manually deleted. +function getBufferDescriptor(buffer) { + if ('string' == typeof buffer || buffer instanceof String) { + buffer = Filament.assets[buffer]; + } + if (buffer.buffer instanceof ArrayBuffer) { + buffer = Filament.Buffer(buffer); + } + return buffer; +} + +function isTexture(uri) { + // TODO: This is not a great way to determine if a resource is a texture, but we can + // remove it after gltfio gains support for concurrent downloading of vertex data: + // https://github.com/google/filament/issues/5909 + if (uri.endsWith(".png")) { + return true; + } + if (uri.endsWith(".ktx2")) { + return true; + } + if (uri.endsWith(".jpg") || uri.endsWith(".jpeg")) { + return true; + } + return false; +} + +Filament.vectorToArray = function(vector) { + const result = []; + for (let i = 0; i < vector.size(); i++) { + result.push(vector.get(i)); + } + return result; +}; + +Filament.shadowOptions = function(overrides) { + const options = { + mapSize: 1024, + shadowCascades: 1, + constantBias: 0.001, + normalBias: 1.0, + shadowFar: 0.0, + shadowNearHint: 1.0, + shadowFarHint: 100.0, + stable: false, + polygonOffsetConstant: 0.5, + polygonOffsetSlope: 2.0, + screenSpaceContactShadows: false, + stepCount: 8, + maxShadowDistance: 0.3 + }; + return Object.assign(options, overrides); +}; + +Filament.loadClassExtensions = function() { + + Filament.loadGeneratedExtensions(); + + /// Engine ::core class:: + + /// create ::static method:: Creates an Engine instance for the given canvas. + /// canvas ::argument:: the canvas DOM element + /// options ::argument:: optional WebGL 2.0 context configuration + /// ::retval:: an instance of [Engine] + Filament.Engine.create = function(canvas, options) { + const defaults = { + majorVersion: 2, + minorVersion: 0, + antialias: false, + depth: true, + alpha: false + }; + options = Object.assign(defaults, options); + + // Create the WebGL 2.0 context. + const ctx = canvas.getContext("webgl2", options); + + // Enable all desired extensions by calling getExtension on each one. + ctx.getExtension('WEBGL_compressed_texture_s3tc'); + ctx.getExtension('WEBGL_compressed_texture_s3tc_srgb'); + ctx.getExtension('WEBGL_compressed_texture_astc'); + ctx.getExtension('WEBGL_compressed_texture_etc'); + + // These transient globals are used temporarily during Engine construction. + window.filament_glOptions = options; + window.filament_glContext = ctx; + + // Register the GL context with emscripten and create the Engine. + const engine = Filament.Engine._create(); + + // Annotate the engine with the GL context to support multiple canvases. + engine.context = window.filament_glContext; + engine.handle = window.filament_contextHandle; + + // Ensure that we do not pollute the global namespace. + delete window.filament_glOptions; + delete window.filament_glContext; + delete window.filament_contextHandle; + + return engine; + }; + + Filament.Engine.prototype.execute = function() { + window.filament_contextHandle = this.handle; + this._execute(); + delete window.filament_contextHandle; + }; + + /// createMaterial ::method:: + /// package ::argument:: asset string, or Uint8Array, or [Buffer] with filamat contents + /// ::retval:: an instance of [createMaterial] + Filament.Engine.prototype.createMaterial = function(buffer) { + buffer = getBufferDescriptor(buffer); + const result = this._createMaterial(buffer); + buffer.delete(); + return result; + }; + + /// createTextureFromKtx1 ::method:: Utility function that creates a [Texture] from a KTX1 file. + /// buffer ::argument:: asset string, or Uint8Array, or [Buffer] with KTX1 file contents + /// options ::argument:: Options dictionary. + /// ::retval:: [Texture] + Filament.Engine.prototype.createTextureFromKtx1 = function(buffer, options) { + buffer = getBufferDescriptor(buffer); + const result = Filament._createTextureFromKtx1(buffer, this, options); + buffer.delete(); + return result; + }; + + /// createTextureFromKtx2 ::method:: Utility function that creates a [Texture] from a KTX2 file. + /// buffer ::argument:: asset string, or Uint8Array, or [Buffer] with KTX2 file contents + /// options ::argument:: Options dictionary. + /// ::retval:: [Texture] + Filament.Engine.prototype.createTextureFromKtx2 = function(buffer, options) { + options = options || {}; + buffer = getBufferDescriptor(buffer); + + const engine = this; + const quiet = false; + const reader = new Filament.Ktx2Reader(engine, quiet); + + reader.requestFormat(Filament.Texture$InternalFormat.RGBA8); + reader.requestFormat(Filament.Texture$InternalFormat.SRGB8_A8); + + const formats = options.formats || []; + for (const format of formats) { + reader.requestFormat(format); + } + + result = reader.load(buffer, options.srgb ? Filament.Ktx2Reader$TransferFunction.sRGB : + Filament.Ktx2Reader$TransferFunction.LINEAR); + + reader.delete(); + buffer.delete(); + return result; + }; + + /// createIblFromKtx1 ::method:: Utility that creates an [IndirectLight] from a KTX file. + /// NOTE: To prevent a leak, please be sure to destroy the associated reflections texture. + /// buffer ::argument:: asset string, or Uint8Array, or [Buffer] with KTX file contents + /// options ::argument:: Options dictionary. + /// ::retval:: [IndirectLight] + Filament.Engine.prototype.createIblFromKtx1 = function(buffer, options) { + buffer = getBufferDescriptor(buffer); + const result = Filament._createIblFromKtx1(buffer, this, options); + buffer.delete(); + return result; + }; + + /// createSkyFromKtx1 ::method:: Utility function that creates a [Skybox] from a KTX file. + /// NOTE: To prevent a leak, please be sure to destroy the associated texture. + /// buffer ::argument:: asset string, or Uint8Array, or [Buffer] with KTX file contents + /// options ::argument:: Options dictionary. + /// ::retval:: [Skybox] + Filament.Engine.prototype.createSkyFromKtx1 = function(buffer, options) { + const skytex = this.createTextureFromKtx1(buffer, options); + return Filament.Skybox.Builder().environment(skytex).build(this); + }; + + /// createTextureFromPng ::method:: Creates a 2D [Texture] from the raw contents of a PNG file. + /// buffer ::argument:: asset string, or Uint8Array, or [Buffer] with PNG file contents + /// options ::argument:: object with optional `srgb`, `noalpha`, and `nomips` keys. + /// ::retval:: [Texture] + Filament.Engine.prototype.createTextureFromPng = function(buffer, options) { + buffer = getBufferDescriptor(buffer); + const result = Filament._createTextureFromImageFile(buffer, this, options); + buffer.delete(); + return result; + }; + + /// createTextureFromJpeg ::method:: Creates a 2D [Texture] from the contents of a JPEG file. + /// buffer ::argument:: asset string, or Uint8Array, or [Buffer] with JPEG file contents + /// options ::argument:: JavaScript object with optional `srgb` and `nomips` keys. + /// ::retval:: [Texture] + Filament.Engine.prototype.createTextureFromJpeg = function(buffer, options) { + buffer = getBufferDescriptor(buffer); + const result = Filament._createTextureFromImageFile(buffer, this, options); + buffer.delete(); + return result; + }; + + /// loadFilamesh ::method:: Consumes the contents of a filamesh file and creates a renderable. + /// buffer ::argument:: asset string, or Uint8Array, or [Buffer] with filamesh contents + /// definstance ::argument:: Optional default [MaterialInstance] + /// matinstances ::argument:: Optional in-out object that gets populated with a \ + /// name-to-[MaterialInstance] mapping. Clients can also optionally provide individual \ + /// material instances using this argument. + /// ::retval:: JavaScript object with keys `renderable`, `vertexBuffer`, and `indexBuffer`. \ + /// These are of type [Entity], [VertexBuffer], and [IndexBuffer]. + Filament.Engine.prototype.loadFilamesh = function(buffer, definstance, matinstances) { + buffer = getBufferDescriptor(buffer); + const result = Filament._loadFilamesh(this, buffer, definstance, matinstances); + buffer.delete(); + return result; + }; + + /// createAssetLoader ::method:: + /// ::retval:: an instance of [AssetLoader] + /// Clients should create only one asset loader for the lifetime of their app, this prevents + /// memory leaks and duplication of Material objects. + Filament.Engine.prototype.createAssetLoader = function() { + const materials = new Filament.gltfio$UbershaderProvider(this); + return new Filament.gltfio$AssetLoader(this, materials); + }; + + /// addEntities ::method:: + /// entities ::argument:: array of entities + /// This method is equivalent to calling `addEntity` on each item in the array. + Filament.Scene.prototype.addEntities = function(entities) { + const vector = new Filament.EntityVector(); + for (const entity of entities) { + vector.push_back(entity); + } + this._addEntities(vector); + }; + + /// removeEntities ::method:: + /// entities ::argument:: array of entities + /// This method is equivalent to calling `remove` on each item in the array. + Filament.Scene.prototype.removeEntities = function(entities) { + const vector = new Filament.EntityVector(); + for (const entity of entities) { + vector.push_back(entity); + } + this._removeEntities(vector); + }; + + /// setShadowOptions ::method:: + /// instance ::argument:: Instance of a light component obtained from `getInstance`. + /// overrides ::argument:: Dictionary with one or more of the following properties: \ + /// mapSize, shadowCascades, constantBias, normalBias, shadowFar, shadowNearHint, \ + /// shadowFarHint, stable, polygonOffsetConstant, polygonOffsetSlope, \ + // screenSpaceContactShadows, stepCount, maxShadowDistance. + Filament.LightManager.prototype.setShadowOptions = function(instance, overrides) { + this._setShadowOptions(instance, Filament.shadowOptions(overrides)); + }; + + /// setClearOptions ::method:: + /// overrides ::argument:: Dictionary with one or more of the following properties: \ + /// clearColor, clear, discard. + Filament.Renderer.prototype.setClearOptions = function(overrides) { + const options = { + clearColor: [0, 0, 0, 0], + clear: false, + discard: true + }; + Object.assign(options, overrides); + this._setClearOptions(options); + }; + + /// setAmbientOcclusionOptions ::method:: + Filament.View.prototype.setAmbientOcclusionOptions = function(overrides) { + const options = this.setAmbientOcclusionOptionsDefaults(overrides); + this._setAmbientOcclusionOptions(options); + }; + + /// setDepthOfFieldOptions ::method:: + Filament.View.prototype.setDepthOfFieldOptions = function(overrides) { + const options = this.setDepthOfFieldOptionsDefaults(overrides); + this._setDepthOfFieldOptions(options); + }; + + /// setMultiSampleAntiAliasingOptions ::method:: + Filament.View.prototype.setMultiSampleAntiAliasingOptions = function(overrides) { + const options = this.setMultiSampleAntiAliasingOptionsDefaults(overrides); + this._setMultiSampleAntiAliasingOptions(options); + }; + + /// setTemporalAntiAliasingOptions ::method:: + Filament.View.prototype.setTemporalAntiAliasingOptions = function(overrides) { + const options = this.setTemporalAntiAliasingOptionsDefaults(overrides); + this._setTemporalAntiAliasingOptions(options); + }; + + /// setScreenSpaceReflectionsOptions ::method:: + Filament.View.prototype.setScreenSpaceReflectionsOptions = function(overrides) { + const options = this.setScreenSpaceReflectionsOptionsDefaults(overrides); + this._setScreenSpaceReflectionsOptions(options); + }; + + /// setBloomOptions ::method:: + Filament.View.prototype.setBloomOptions = function(overrides) { + const options = this.setBloomOptionsDefaults(overrides); + this._setBloomOptions(options); + }; + + /// setFogOptions ::method:: + Filament.View.prototype.setFogOptions = function(overrides) { + const options = this.setFogOptionsDefaults(overrides); + this._setFogOptions(options); + }; + + /// setVignetteOptions ::method:: + Filament.View.prototype.setVignetteOptions = function(overrides) { + const options = this.setVignetteOptionsDefaults(overrides); + this._setVignetteOptions(options); + }; + + /// setGuardBandOptions ::method:: + Filament.View.prototype.setGuardBandOptions = function(overrides) { + const options = this.setGuardBandOptionsDefaults(overrides); + this._setGuardBandOptions(options); + }; + + /// setStereoscopicOptions ::method:: + Filament.View.prototype.setStereoscopicOptions = function(overrides) { + const options = this.setStereoscopicOptionsDefaults(overrides); + this._setStereoscopicOptions(options); + } + + /// BufferObject ::core class:: + + /// setBuffer ::method:: + /// engine ::argument:: [Engine] + /// buffer ::argument:: asset string, or Uint8Array, or [Buffer] + /// byteOffset ::argument:: non-negative integer + Filament.BufferObject.prototype.setBuffer = function(engine, buffer, byteOffset = 0) { + buffer = getBufferDescriptor(buffer); + this._setBuffer(engine, buffer, byteOffset); + buffer.delete(); + }; + + /// VertexBuffer ::core class:: + + /// setBufferAt ::method:: + /// engine ::argument:: [Engine] + /// bufferIndex ::argument:: non-negative integer + /// buffer ::argument:: asset string, or Uint8Array, or [Buffer] + /// byteOffset ::argument:: non-negative integer + Filament.VertexBuffer.prototype.setBufferAt = function(engine, bufferIndex, buffer, byteOffset = 0) { + buffer = getBufferDescriptor(buffer); + this._setBufferAt(engine, bufferIndex, buffer, byteOffset); + buffer.delete(); + }; + + /// IndexBuffer ::core class:: + + /// setBuffer ::method:: + /// engine ::argument:: [Engine] + /// buffer ::argument:: asset string, or Uint8Array, or [Buffer] + /// byteOffset ::argument:: non-negative integer + Filament.IndexBuffer.prototype.setBuffer = function(engine, buffer, byteOffset = 0) { + buffer = getBufferDescriptor(buffer); + this._setBuffer(engine, buffer, byteOffset); + buffer.delete(); + }; + + Filament.LightManager$Builder.prototype.shadowOptions = function(overrides) { + return this._shadowOptions(Filament.shadowOptions(overrides)); + }; + + Filament.RenderableManager$Builder.prototype.build = + Filament.LightManager$Builder.prototype.build = + function(engine, entity) { + const result = this._build(engine, entity); + this.delete(); + return result; + }; + + Filament.ColorGrading$Builder.prototype.build = + Filament.RenderTarget$Builder.prototype.build = + Filament.VertexBuffer$Builder.prototype.build = + Filament.IndexBuffer$Builder.prototype.build = + Filament.Texture$Builder.prototype.build = + Filament.IndirectLight$Builder.prototype.build = + Filament.Skybox$Builder.prototype.build = + function(engine) { + const result = this._build(engine); + this.delete(); + return result; + }; + + Filament.Ktx1Bundle.prototype.getBlob = function(index) { + const blob = this._getBlob(index); + const result = blob.getBytes(); + blob.delete(); + return result; + } + + Filament.Ktx1Bundle.prototype.getCubeBlob = function(miplevel) { + const blob = this._getCubeBlob(miplevel); + const result = blob.getBytes(); + blob.delete(); + return result; + } + + Filament.Texture.prototype.setImage = function(engine, level, pbd) { + this._setImage(engine, level, pbd); + pbd.delete(); + } + + Filament.Texture.prototype.setImageCube = function(engine, level, pbd) { + this._setImageCube(engine, level, pbd); + pbd.delete(); + } + + Filament.Texture.prototype.getWidth = function(engine, level = 0) { + return this._getWidth(engine, level); + } + + Filament.Texture.prototype.getHeight = function(engine, level = 0) { + return this._getHeight(engine, level); + } + + Filament.Texture.prototype.getDepth = function(engine, level = 0) { + return this._getDepth(engine, level); + } + + Filament.Texture.prototype.getLevels = function(engine) { + return this._getLevels(engine); + } + + Filament.SurfaceOrientation$Builder.prototype.normals = function(buffer, stride = 0) { + buffer = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); + this.norPointer = Filament._malloc(buffer.byteLength); + Filament.HEAPU8.set(buffer, this.norPointer); + this._normals(this.norPointer, stride); + }; + + Filament.SurfaceOrientation$Builder.prototype.uvs = function(buffer, stride = 0) { + buffer = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); + this.uvsPointer = Filament._malloc(buffer.byteLength); + Filament.HEAPU8.set(buffer, this.uvsPointer); + this._uvs(this.uvsPointer, stride); + }; + + Filament.SurfaceOrientation$Builder.prototype.positions = function(buffer, stride = 0) { + buffer = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); + this.posPointer = Filament._malloc(buffer.byteLength); + Filament.HEAPU8.set(buffer, this.posPointer); + this._positions(this.posPointer, stride); + }; + + Filament.SurfaceOrientation$Builder.prototype.triangles16 = function(buffer) { + buffer = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); + this.t16Pointer = Filament._malloc(buffer.byteLength); + Filament.HEAPU8.set(buffer, this.t16Pointer); + this._triangles16(this.t16Pointer); + }; + + Filament.SurfaceOrientation$Builder.prototype.triangles32 = function(buffer) { + buffer = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); + this.t32Pointer = Filament._malloc(buffer.byteLength); + Filament.HEAPU8.set(buffer, this.t32Pointer); + this._triangles32(this.t32Pointer); + }; + + Filament.SurfaceOrientation$Builder.prototype.build = function() { + const result = this._build(); + this.delete(); + if ('norPointer' in this) Filament._free(this.norPointer); + if ('uvsPointer' in this) Filament._free(this.uvsPointer); + if ('posPointer' in this) Filament._free(this.posPointer); + if ('t16Pointer' in this) Filament._free(this.t16Pointer); + if ('t32Pointer' in this) Filament._free(this.t32Pointer); + return result; + }; + + Filament.SurfaceOrientation.prototype.getQuats = function(nverts) { + const attribType = Filament.VertexBuffer$AttributeType.SHORT4; + const quatsBufferSize = 8 * nverts; + const quatsBuffer = Filament._malloc(quatsBufferSize); + this._getQuats(quatsBuffer, nverts, attribType); + const arrayBuffer = Filament.HEAPU8.subarray(quatsBuffer, quatsBuffer + quatsBufferSize).slice().buffer; + Filament._free(quatsBuffer); + return new Int16Array(arrayBuffer); + }; + + Filament.SurfaceOrientation.prototype.getQuatsHalf4 = function (nverts) { + const attribType = Filament.VertexBuffer$AttributeType.HALF4; + const quatsBufferSize = 8 * nverts; + const quatsBuffer = Filament._malloc(quatsBufferSize); + this._getQuats(quatsBuffer, nverts, attribType); + const arrayBuffer = Filament.HEAPU8.subarray(quatsBuffer, quatsBuffer + quatsBufferSize).slice().buffer; + Filament._free(quatsBuffer); + return new Uint16Array(arrayBuffer); + }; + + Filament.SurfaceOrientation.prototype.getQuatsFloat4 = function (nverts) { + const attribType = Filament.VertexBuffer$AttributeType.FLOAT4; + const quatsBufferSize = 16 * nverts; + const quatsBuffer = Filament._malloc(quatsBufferSize); + this._getQuats(quatsBuffer, nverts, attribType); + const arrayBuffer = Filament.HEAPU8.subarray(quatsBuffer, quatsBuffer + quatsBufferSize).slice().buffer; + Filament._free(quatsBuffer); + return new Float32Array(arrayBuffer); + }; + + Filament.gltfio$AssetLoader.prototype.createAsset = function(buffer) { + buffer = getBufferDescriptor(buffer); + const result = this._createAsset(buffer); + buffer.delete(); + return result; + }; + + Filament.gltfio$AssetLoader.prototype.createInstancedAsset = function(buffer, instances) { + buffer = getBufferDescriptor(buffer); + const asset = this._createInstancedAsset(buffer, instances.length); + buffer.delete(); + const instancesVector = asset._getAssetInstances(); + for (let i = 0; i < instancesVector.size(); i++) { + instances[i] = instancesVector.get(i); + } + return asset; + }; + + // See the C++ documentation for ResourceLoader and AssetLoader. The JavaScript API differs in + // that it takes two optional callbacks: + // + // - onDone is called after all resources have been downloaded and decoded. + // - onFetched is called after each resource has finished downloading. + // + // Takes an optional base path for resolving the URI strings in the glTF file, which is + // typically the path to the parent glTF file. The given base path cannot itself be a relative + // URL, but clients can do the following to resolve a relative URL: + // const basePath = '' + new URL(myRelativeUrl, document.location); + // If the given base path is null, document.location is used as the base. + // + // The optional asyncInterval argument allows clients to control how decoding is amortized + // over time. It represents the number of milliseconds between each texture decoding task. + // + // The optional config argument is an object with boolean field `normalizeSkinningWeights`. + Filament.gltfio$FilamentAsset.prototype.loadResources = function(onDone, onFetched, basePath, + asyncInterval, config) { + const asset = this; + const engine = this.getEngine(); + const interval = asyncInterval || 30; + const defaults = { + normalizeSkinningWeights: true, + }; + config = Object.assign(defaults, config || {}); + + basePath = basePath || document.location; + onFetched = onFetched || ((name) => {}); + onDone = onDone || (() => {}); + + // Construct two lists of URI strings to fetch: textures and non-textures. + let textureUris = new Set(); + let bufferUris = new Set(); + const absoluteToRelativeUri = {}; + for (const relativeUri of this.getResourceUris()) { + const absoluteUri = '' + new URL(relativeUri, basePath); + absoluteToRelativeUri[absoluteUri] = relativeUri; + if (isTexture(relativeUri)) { + textureUris.add(absoluteUri); + continue; + } + bufferUris.add(absoluteUri); + } + textureUris = Array.from(textureUris); + bufferUris = Array.from(bufferUris); + + // Construct a resource loader and start decoding after all textures are fetched. + const resourceLoader = new Filament.gltfio$ResourceLoader(engine, + config.normalizeSkinningWeights); + + const stbProvider = new Filament.gltfio$StbProvider(engine); + const ktx2Provider = new Filament.gltfio$Ktx2Provider(engine); + + resourceLoader.addStbProvider("image/jpeg", stbProvider); + resourceLoader.addStbProvider("image/png", stbProvider); + resourceLoader.addKtx2Provider("image/ktx2", ktx2Provider); + + const onComplete = () => { + resourceLoader.asyncBeginLoad(asset); + + // NOTE: This decodes in the wasm layer instead of using Canvas2D, which allows Filament + // to have more control (handling of alpha, srgb, etc) and improves parity with native + // platforms. In the future we may wish to offload this to web workers. + + // Decode a single PNG or JPG every 30 milliseconds, or at the specified interval. + const timer = setInterval(() => { + resourceLoader.asyncUpdateLoad(); + const progress = resourceLoader.asyncGetLoadProgress(); + if (progress >= 1) { + clearInterval(timer); + resourceLoader.delete(); + stbProvider.delete(); + onDone(); + } + }, interval); + }; + + // Download all non-texture resources and invoke the callback when done. + if (bufferUris.length == 0) { + onComplete(); + } else { + Filament.fetch(bufferUris, onComplete, function(absoluteUri) { + const buffer = getBufferDescriptor(absoluteUri); + const relativeUri = absoluteToRelativeUri[absoluteUri]; + resourceLoader.addResourceData(relativeUri, buffer); + buffer.delete(); + onFetched(relativeUri); + }); + } + + // Begin downloading all texture resources, no completion callback necessary. + Filament.fetch(textureUris, null, function(absoluteUri) { + const buffer = getBufferDescriptor(absoluteUri); + const relativeUri = absoluteToRelativeUri[absoluteUri]; + resourceLoader.addResourceData(relativeUri, buffer); + buffer.delete(); + onFetched(relativeUri); + }); + }; + + Filament.gltfio$FilamentAsset.prototype.getEntities = function() { + return Filament.vectorToArray(this._getEntities()); + }; + + Filament.gltfio$FilamentAsset.prototype.getEntitiesByName = function(name) { + return Filament.vectorToArray(this._getEntitiesByName(name)); + }; + + Filament.gltfio$FilamentAsset.prototype.getEntitiesByPrefix = function(prefix) { + return Filament.vectorToArray(this._getEntitiesByPrefix(prefix)); + }; + + Filament.gltfio$FilamentAsset.prototype.getLightEntities = function() { + return Filament.vectorToArray(this._getLightEntities()); + }; + + Filament.gltfio$FilamentAsset.prototype.getRenderableEntities = function() { + return Filament.vectorToArray(this._getRenderableEntities()); + }; + + Filament.gltfio$FilamentAsset.prototype.getCameraEntities = function() { + return Filament.vectorToArray(this._getCameraEntities()); + }; + + Filament.gltfio$FilamentAsset.prototype.getResourceUris = function() { + return Filament.vectorToArray(this._getResourceUris()); + } + + Filament.gltfio$FilamentAsset.prototype.getAssetInstances = function() { + return Filament.vectorToArray(this._getAssetInstances()); + } + + Filament.gltfio$FilamentInstance.prototype.getMaterialVariantNames = function() { + return Filament.vectorToArray(this._getMaterialVariantNames()); + } +}; + +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// --------------- +// Buffer Wrappers +// --------------- + +// These wrappers make it easy for JavaScript clients to pass large swaths of data to Filament. They +// copy the contents of the given typed array into the WASM heap, then return a low-level buffer +// descriptor object. If the given array was taken from the WASM heap, then they create a temporary +// copy because the input pointer becomes invalidated after allocating heap memory for the buffer +// descriptor. + +/// Buffer ::function:: Constructs a [BufferDescriptor] by copying a typed array into the WASM heap. +/// typedarray ::argument:: Data to consume (e.g. Uint8Array, Uint16Array, Float32Array) +/// ::retval:: [BufferDescriptor] +Filament.Buffer = function(typedarray) { + console.assert(typedarray.buffer instanceof ArrayBuffer); + console.assert(typedarray.byteLength > 0); + + // The only reason we need to create a copy here is that emscripten might "grow" its entire heap + // (i.e. destroy and recreate) during the allocation of the BufferDescriptor, which would cause + // detachment if the source array happens to be view into the old emscripten heap. + const ta = typedarray.slice(); + + const bd = new Filament.driver$BufferDescriptor(ta.byteLength); + const uint8array = new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength); + + // getBytes() returns a view into the emscripten heap, this just does a memcpy into it. + bd.getBytes().set(uint8array); + + return bd; +}; + +/// PixelBuffer ::function:: Constructs a [PixelBufferDescriptor] by copying a typed array into \ +/// the WASM heap. +/// typedarray ::argument:: Data to consume (e.g. Uint8Array, Uint16Array, Float32Array) +/// format ::argument:: [PixelDataFormat] +/// datatype ::argument:: [PixelDataType] +/// ::retval:: [PixelBufferDescriptor] +Filament.PixelBuffer = function(typedarray, format, datatype) { + console.assert(typedarray.buffer instanceof ArrayBuffer); + console.assert(typedarray.byteLength > 0); + const ta = typedarray.slice(); + const bd = new Filament.driver$PixelBufferDescriptor(ta.byteLength, format, datatype); + const uint8array = new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength); + bd.getBytes().set(uint8array); + return bd; +}; + +/// CompressedPixelBuffer ::function:: Constructs a [PixelBufferDescriptor] for compressed texture +/// data by copying a typed array into the WASM heap. +/// typedarray ::argument:: Data to consume (e.g. Uint8Array, Uint16Array, Float32Array) +/// cdatatype ::argument:: [CompressedPixelDataType] +/// faceSize ::argument:: Number of bytes in each face (cubemaps only) +/// ::retval:: [PixelBufferDescriptor] +Filament.CompressedPixelBuffer = function(typedarray, cdatatype, faceSize) { + console.assert(typedarray.buffer instanceof ArrayBuffer); + console.assert(typedarray.byteLength > 0); + faceSize = faceSize || typedarray.byteLength; + const ta = typedarray.slice(); + const bd = new Filament.driver$PixelBufferDescriptor(ta.byteLength, cdatatype, faceSize, true); + const uint8array = new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength); + bd.getBytes().set(uint8array); + return bd; +}; + +Filament._loadFilamesh = function(engine, buffer, definstance, matinstances) { + matinstances = matinstances || {}; + const registry = new Filament.MeshReader$MaterialRegistry(); + for (let key in matinstances) { + registry.set(key, matinstances[key]); + } + if (definstance) { + registry.set("DefaultMaterial", definstance); + } + const mesh = Filament.MeshReader.loadMeshFromBuffer(engine, buffer, registry); + const keys = registry.keys(); + for (let i = 0; i < keys.size(); i++) { + const key = keys.get(i); + const minstance = registry.get(key); + matinstances[key] = minstance; + } + return { + "renderable": mesh.renderable(), + "vertexBuffer": mesh.vertexBuffer(), + "indexBuffer": mesh.indexBuffer(), + } +} + +// ------------------ +// Geometry Utilities +// ------------------ + +/// IcoSphere ::class:: Utility class for constructing spheres (requires glMatrix). +/// +/// The constructor takes an integer subdivision level, with 0 being an icosahedron. +/// +/// Exposes three arrays as properties: +/// +/// - `icosphere.vertices` Float32Array of XYZ coordinates. +/// - `icosphere.tangents` Uint16Array (interpreted as half-floats) encoding the surface orientation +/// as quaternions. +/// - `icosphere.triangles` Uint16Array with triangle indices. +/// +Filament.IcoSphere = function(nsubdivs) { + const X = .525731112119133606; + const Z = .850650808352039932; + const N = 0.; + this.vertices = new Float32Array([ + -X, +N, +Z, +X, +N, +Z, -X, +N, -Z, +X, +N, -Z , + +N, +Z, +X, +N, +Z, -X, +N, -Z, +X, +N, -Z, -X , + +Z, +X, +N, -Z, +X, +N, +Z, -X, +N, -Z, -X, +N , + ]); + this.triangles = new Uint16Array([ + 1, 4, 0, 4, 9, 0, 4, 5, 9, 8, 5, 4 , 1, 8, 4 , + 1, 10, 8, 10, 3, 8, 8, 3, 5, 3, 2, 5 , 3, 7, 2 , + 3, 10, 7, 10, 6, 7, 6, 11, 7, 6, 0, 11 , 6, 1, 0 , + 10, 1, 6, 11, 0, 9, 2, 11, 9, 5, 2, 9 , 11, 2, 7 , + ]); + + nsubdivs = nsubdivs || 0; + while (nsubdivs-- > 0) { + this.subdivide(); + } + + const nverts = this.vertices.length / 3; + + // This is a unit sphere, so normals = positions. + const normals = this.vertices; + + // Perform computations. + const sob = new Filament.SurfaceOrientation$Builder(); + sob.vertexCount(nverts); + sob.normals(normals, 0) + const orientation = sob.build(); + + // Copy the results out of the helper. + this.tangents = orientation.getQuats(nverts); + + // Free up the surface orientation helper now that we're done with it. + orientation.delete(); +} + +Filament.IcoSphere.prototype.subdivide = function() { + const srctris = this.triangles; + const srcverts = this.vertices; + const nsrctris = srctris.length / 3; + const ndsttris = nsrctris * 4; + const nsrcverts = srcverts.length / 3; + const ndstverts = nsrcverts + nsrctris * 3; + const dsttris = new Uint16Array(ndsttris * 3); + const dstverts = new Float32Array(ndstverts * 3); + dstverts.set(srcverts); + let srcind = 0, dstind = 0, i3 = nsrcverts * 3, i4 = i3 + 3, i5 = i4 + 3; + for (let tri = 0; tri < nsrctris; tri++, i3 += 9, i4 += 9, i5 += 9) { + const i0 = srctris[srcind++] * 3; + const i1 = srctris[srcind++] * 3; + const i2 = srctris[srcind++] * 3; + const v0 = srcverts.subarray(i0, i0 + 3); + const v1 = srcverts.subarray(i1, i1 + 3); + const v2 = srcverts.subarray(i2, i2 + 3); + const v3 = dstverts.subarray(i3, i3 + 3); + const v4 = dstverts.subarray(i4, i4 + 3); + const v5 = dstverts.subarray(i5, i5 + 3); + vec3.normalize(v3, vec3.add(v3, v0, v1)); + vec3.normalize(v4, vec3.add(v4, v1, v2)); + vec3.normalize(v5, vec3.add(v5, v2, v0)); + dsttris[dstind++] = i0 / 3; + dsttris[dstind++] = i3 / 3; + dsttris[dstind++] = i5 / 3; + dsttris[dstind++] = i3 / 3; + dsttris[dstind++] = i1 / 3; + dsttris[dstind++] = i4 / 3; + dsttris[dstind++] = i5 / 3; + dsttris[dstind++] = i3 / 3; + dsttris[dstind++] = i4 / 3; + dsttris[dstind++] = i2 / 3; + dsttris[dstind++] = i5 / 3; + dsttris[dstind++] = i4 / 3; + } + this.triangles = dsttris; + this.vertices = dstverts; +} + +// --------------- +// Math Extensions +// --------------- + +function clamp(v, least, most) { + return Math.max(Math.min(most, v), least); +} + +/// packSnorm16 ::function:: Converts a float in [-1, +1] into a half-float. +/// value ::argument:: float +/// ::retval:: half-float +Filament.packSnorm16 = function(value) { + return Math.round(clamp(value, -1.0, 1.0) * 32767.0); +} + +/// loadMathExtensions ::function:: Extends the [glMatrix](http://glmatrix.net/) math library. +/// Filament does not require its clients to use glMatrix, but if its usage is detected then +/// the [init] function will automatically call `loadMathExtensions`. +/// This defines the following functions: +/// - **vec4.packSnorm16** can be used to create half-floats (see [packSnorm16]) +/// - **mat3.fromRotation** now takes an arbitrary axis +Filament.loadMathExtensions = function() { + vec4.packSnorm16 = function(out, src) { + out[0] = Filament.packSnorm16(src[0]); + out[1] = Filament.packSnorm16(src[1]); + out[2] = Filament.packSnorm16(src[2]); + out[3] = Filament.packSnorm16(src[3]); + return out; + } + // In gl-matrix, mat3 rotation assumes rotation about the Z axis, so here we add a function + // to allow an arbitrary axis. + const fromRotationZ = mat3.fromRotation; + mat3.fromRotation = function(out, radians, axis) { + if (axis) { + return mat3.fromMat4(out, mat4.fromRotation(mat4.create(), radians, axis)); + } + return fromRotationZ(out, radians); + }; +}; + +// --------------- +// Texture helpers +// --------------- + +Filament._createTextureFromKtx1 = function(ktxdata, engine, options) { + options = options || {}; + const ktx = options['ktx'] || new Filament.Ktx1Bundle(ktxdata); + const srgb = !!options['srgb']; + return Filament.ktx1reader$createTexture(engine, ktx, srgb); +}; + +Filament._createIblFromKtx1 = function(ktxdata, engine, options) { + options = options || {}; + const iblktx = options['ktx'] = new Filament.Ktx1Bundle(ktxdata); + + const format = iblktx.info().glInternalFormat; + //if (format != this.ctx.R11F_G11F_B10F && format != this.ctx.RGB16F && format != this.ctx.RGB32F) { + if (format != 35898 && format != 33327 && format != 34837) { + console.warn('IBL texture format is 0x' + format.toString(16) + + ' which is not an expected floating-point format. Please use cmgen to generate IBL.'); + } + + const ibltex = Filament._createTextureFromKtx1(ktxdata, engine, options); + const shstring = iblktx.getMetadata("sh"); + const ibl = Filament.IndirectLight.Builder() + .reflections(ibltex) + .build(engine); + ibl.shfloats = shstring.split(/\s/, 9 * 3).map(parseFloat); + return ibl; +}; + +Filament._createTextureFromImageFile = function(fileContents, engine, options) { + const Sampler = Filament.Texture$Sampler; + const TextureFormat = Filament.Texture$InternalFormat; + const PixelDataFormat = Filament.PixelDataFormat; + + options = options || {}; + const srgb = !!options['srgb']; + const noalpha = !!options['noalpha']; + const nomips = !!options['nomips']; + + const decodedImage = Filament.decodeImage(fileContents, noalpha ? 3 : 4); + + let texformat, pbformat, pbtype; + if (noalpha) { + texformat = srgb ? TextureFormat.SRGB8 : TextureFormat.RGB8; + pbformat = PixelDataFormat.RGB; + pbtype = Filament.PixelDataType.UBYTE; + } else { + texformat = srgb ? TextureFormat.SRGB8_A8 : TextureFormat.RGBA8; + pbformat = PixelDataFormat.RGBA; + pbtype = Filament.PixelDataType.UBYTE; + } + + const tex = Filament.Texture.Builder() + .width(decodedImage.width) + .height(decodedImage.height) + .levels(nomips ? 1 : 0xff) + .sampler(Sampler.SAMPLER_2D) + .format(texformat) + .build(engine); + + const pixelbuffer = Filament.PixelBuffer(decodedImage.data.getBytes(), pbformat, pbtype); + tex.setImage(engine, 0, pixelbuffer); + if (!nomips) { + tex.generateMipmaps(engine); + } + return tex; +}; + +/// getSupportedFormats ::function:: Queries WebGL to check which compressed formats are supported. +/// ::retval:: object with boolean values and the following keys: s3tc, astc, etc +Filament.getSupportedFormats = function() { + if (Filament.supportedFormats) { + return Filament.supportedFormats; + } + const options = { majorVersion: 2, minorVersion: 0 }; + let ctx = document.createElement('canvas').getContext('webgl2', options); + const result = { + s3tc: false, + s3tc_srgb: false, + astc: false, + etc: false, + } + let exts = ctx.getSupportedExtensions(), nexts = exts.length, i; + for (i = 0; i < nexts; i++) { + let ext = exts[i]; + if (ext == "WEBGL_compressed_texture_s3tc") { + result.s3tc = true; + } else if (ext == "WEBGL_compressed_texture_s3tc_srgb") { + result.s3tc_srgb = true; + } else if (ext == "WEBGL_compressed_texture_astc") { + result.astc = true; + } else if (ext == "WEBGL_compressed_texture_etc") { + result.etc = true; + } + } + return Filament.supportedFormats = result; +} + +/// getSupportedFormatSuffix ::function:: Generate a file suffix according to the texture format. +/// Consumes a string describing desired formats and produces a file suffix depending on +/// which (if any) of the formats are actually supported by the WebGL implementation. This is +/// useful for compressed textures. For example, some platforms accept ETC and others accept S3TC. +/// desiredFormats ::argument:: space-delimited string of desired formats +/// ::retval:: empty string if there is no intersection of supported and desired formats. +Filament.getSupportedFormatSuffix = function(desiredFormats) { + desiredFormats = desiredFormats.split(' '); + let exts = Filament.getSupportedFormats(); + for (let key in exts) { + if (exts[key] && desiredFormats.includes(key)) { + return '_' + key; + } + } + return ''; +} diff --git a/docs/remote/filament.wasm b/docs/remote/filament.wasm new file mode 100755 index 0000000000..914b03e258 Binary files /dev/null and b/docs/remote/filament.wasm differ diff --git a/docs/remote/index.html b/docs/remote/index.html new file mode 100644 index 0000000000..615e43a9a2 --- /dev/null +++ b/docs/remote/index.html @@ -0,0 +1,472 @@ + + + +Filament Remote + + + + + + + + +
+
+ + + +
+ +

Disconnected.

+
+ +
+
+ + + + + + diff --git a/docs/remote/update.sh b/docs/remote/update.sh new file mode 100755 index 0000000000..f3c3cb9fad --- /dev/null +++ b/docs/remote/update.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +pushd "$(dirname "$0")" > /dev/null + +curl -OL https://nightly.link/google/filament/workflows/web-continuous/main/filament-web.zip +unzip -q filament-web.zip +tar -xvzf filament-release-web.tgz +rm filament-release-web.tgz +rm filament-web.zip +rm filament.d.ts +cp ../../web/samples/remote.html index.html + +popd + +git status +echo "" +echo "All done! Next, make a git commit that updates docs/remote." diff --git a/docs/samples/index.html b/docs/samples/index.html new file mode 100644 index 0000000000..2611b4d8c7 --- /dev/null +++ b/docs/samples/index.html @@ -0,0 +1,216 @@ + + + + + + Tutorials and Samples - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Tutorials and Samples

+

New users of Filament are encouraged to peruse through the samples to get a better +understanding of basic API usage. Additionally, you will find detailed tutorials +for iOS and web.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/samples/ios.html b/docs/samples/ios.html new file mode 100644 index 0000000000..c6248f3355 --- /dev/null +++ b/docs/samples/ios.html @@ -0,0 +1,562 @@ + + + + + + iOS Tutorial - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

CocoaPods Hello Triangle

+

As of release 1.8.0, you can install Filament in your iOS application using CocoaPods.

+

This guide will walk you through creating a basic "hello triangle" iOS application using Filament and the Metal backend.

+

a rotating triangle

+

The full source for this example is here. If you're just looking to get something up and running quickly, download the project, pod install, build, and run.

+

We'll be walking through 7 steps to get the rotating triangle up and running. All of the code we'll be writing will be in a single ViewController.mm file, and you can follow along here.

+ +

Creating a Boilerplate App with Filament

+

We'll start fresh by creating a new Single View App in Xcode.

+

create a single view app in Xcodde

+

Give your app a name, and use the default options.

+

use the default options in Xcode

+

If you haven't used CocoaPods before, I recommend watching this Route 85 video to help you get set up.

+

Create a Podfile in the Xcode project directory with the following:

+
platform :ios, '11.0'
+
+target 'HelloCocoaPods' do
+    pod 'Filament'
+end
+
+

Then run:

+
pod install
+
+

Close the project and then re-open the newly created HelloCocoaPods.xcworkspace file.

+

Instantiating the Filament Engine

+

Before we do anything with Filament, we first need to include the appropriate headers. Filament exposes a C++ API, so any files that include Filament headers need to be compiled in a variant of C++. We'll be using Objective-C++.

+

You should be able to simply change the extension of the default ViewController from .m to .mm, though I've found Xcode to be buggy with this on occasion. To make sure Xcode recognizes it as an Objective-C++ file, check that the type of file is "Objective-C++ Source".

+

change the type of ViewController.m to Objective-C++

+

Then, add the following to the top of ViewController.

+
#include <filament/Engine.h>
+
+using namespace filament;
+
+

We'll need to keep track of a few Filament objects, so let's add a section for private instance variables and add a pointer for our Engine instance.

+
@implementation Viewcontroller {
+     Engine* _engine;
+}
+
+

The Filament Engine is our main entrypoint into Filament. We start by instantiating it inside viewDidLoad.

+
- (void)viewDidLoad {
+    [super viewDidLoad];
+
+    _engine = Engine::create(Engine::Backend::METAL);
+}
+
+

We specify Engine::Backend::METAL to select the Metal backend. Filament also supports OpenGL on iOS, but we strongly recommend sticking to Metal.

+

Every Filament object we create must also be destroyed. Add the dealloc method and the following:

+
- (void)dealloc {
+    _engine->destroy(&_engine);
+}
+
+

If you compile and run the app now you should see output similar to the following:

+
FEngine (64 bits) created at 0x10ab94000 (threading is enabled)
+FEngine resolved backend: Metal
+
+

Creating a SwapChain

+

Before we can render anything, we'll first need to create a SwapChain. The SwapChain represents a platform-specific surface that can be rendered into. On iOS with Metal, it's a CAMetalLayer.

+

We could set up our own CAMetalLayer if we wanted to, but Apple provides a MTKView that is already backed by a CAMetalLayer. It also has a delegate protocol with some methods that will make things easier for us.

+

Inside Main.storyboard, change the type of ViewController's view to a MTKView.

+

ViewController view

+

change type of MTKView

+

Include the SwapChain.h and MTKView.h headers and make the ViewController conform to the MTKViewDelegate protocol.

+
#include <filament/SwapChain.h>
+
+#import <MetalKit/MTKView.h>
+
+@interface ViewController () <MTKViewDelegate>
+
+@end
+
+

Add a new private var:

+
SwapChain* _swapChain;
+
+

Inside viewDidLoad, we'll set our ViewController as the MTKView delegate and instantiate our SwapChain. To instantiate the SwapChain, we pass in view.layer which, because we set our View to a MTKView, will be a CAMetalLayer. Filament's API is platform-agnostic, which is why we need to cast the layer to a void*.

+
MTKView* mtkView = (MTKView*) self.view;
+mtkView.delegate = self;
+_swapChain = _engine->createSwapChain((__bridge void*) mtkView.layer);
+
+

The SwapChain needs to be destroyed in our dealloc function. We'll destroy the objects in the reverse order we created them; the Engine object should always be the the last object we destroy.

+
_engine->destroy(_swapChain);
+_engine->destroy(&_engine);
+
+

Finally, add stubs for some MTKViewDelegate methods, which we'll fill in later.

+
- (void)mtkView:(nonnull MTKView*)view drawableSizeWillChange:(CGSize)size {
+    // todo
+}
+
+- (void)drawInMTKView:(nonnull MTKView*)view {
+    // todo
+}
+
+

Clearing The Screen

+

We now have a Filament Engine and SwapChain set up. We'll need a few more objects before we can render anything.

+

A Filament Renderer gives us an API to render frames into the SwapChain. It takes a View, which defines a Viewport, Scene and Camera for rendering. The Camera represents a vantage point into a Scene, which contains references to all the entities we want to render.

+

Creating these are objects is straightforward. First, include the appropriate headers

+
#include <filament/Renderer.h>
+#include <filament/View.h>
+#include <filament/Camera.h>
+#include <filament/Scene.h>
+#include <filament/Viewport.h>
+
+#include <utils/Entity.h>
+#include <utils/EntityManager.h>
+
+using namespace utils;
+
+

add the following private vars

+
Renderer* _renderer;
+View* _view;
+Scene* _scene;
+Camera* _camera;
+Entity _cameraEntity;
+
+

and then instantiate them

+
_renderer = _engine->createRenderer();
+_view = _engine->createView();
+_scene = _engine->createScene();
+
+

The camera is a bit special. Filament uses an entity-component system, so we'll first need to create an Entity which we then attach a Camera component to.

+
_cameraEntity = EntityManager::get().create();
+_camera = _engine->createCamera(_cameraEntity);
+
+

Let's also inform our Renderer to clear to a light blue clear color, so we can know everything is working.

+
_renderer->setClearOptions({
+    .clearColor = {0.25f, 0.5f, 1.0f, 1.0f},
+    .clear = true
+});
+
+

The Camera and Scene need to be wired up to the View.

+
_view->setScene(_scene);
+_view->setCamera(_camera);
+
+

Our newly created objects get cleaned up inside dealloc.

+
_engine->destroyCameraComponent(_cameraEntity);
+EntityManager::get().destroy(_cameraEntity);
+_engine->destroy(_scene);
+_engine->destroy(_view);
+_engine->destroy(_renderer);
+
+

We need to set the Viewport on our View, which we want to do whenever the size of our SwapChain changes. We'll also update the projection matrix on our camera.

+

Let's create a new method, resize:, which will update the Viewport on our View to a given size. We'll call it in the mtkView:drawableSizeWillChange: delegate method, and at the end of viewDidLoad:

+
- (void)resize:(CGSize)size {
+    _view->setViewport({0, 0, (uint32_t) size.width, (uint32_t) size.height});
+
+    const double aspect = size.width / size.height;
+    const double left   = -2.0 * aspect;
+    const double right  =  2.0 * aspect;
+    const double bottom = -2.0;
+    const double top    =  2.0;
+    const double near   =  0.0;
+    const double far    =  1.0;
+    _camera->setProjection(Camera::Projection::ORTHO, left, right, bottom, top, near, far);
+}
+
+- (void)viewDidLoad {
+    ...
+
+    // Give our View a starting size based on the drawable size.
+    [self resize:mtkView.drawableSize];
+}
+
+- (void)mtkView(nonnull MTKView*)view drawableSizeWillChange:(CGSize)size {
+    [self resize:size];
+}
+
+

Lastly, in order to render, we'll call a few Filament API methods inside the drawInMTKView: method:

+
- (void)drawInMTKView:(nonnull MTKView*)view {
+    if (_renderer->beginFrame(_swapChain)) {
+        _renderer->render(_view);
+        _renderer->endFrame();
+    }
+}
+
+

The beginFrame method instructs Filament to start rendering to our specific SwapChain instance. It returns true if the engine is ready for another frame. It returns false to signal us to skip this frame, which could happen if we're sending frames down too quickly for the GPU to process.

+

At this point, you should be able to build and run the app, and you'll see a blue screen.

+

blue screen after clearing

+

Drawing a Triangle

+

In order to draw a triangle, we need to create vertex and index buffers to define its geometry. We'll then create a Renderable component.

+

We'll start by including some additional headers and adding a few new private vars:

+
#include <filament/VertexBuffer.h>
+#include <filament/IndexBuffer.h>
+#include <filament/RenderableManager.h>
+
+...
+
+VertexBuffer* _vertexBuffer;
+IndexBuffer* _indexBuffer;
+Entity _triangle;
+
+

First, we'll define the data for a single vertex.

+
struct Vertex {
+    math::float2 position;
+    math::float3 color;
+};
+
+

Creating a VertexBuffer and IndexBuffer is a matter of giving Filament a pointer to the data, along with information on its layout and size. Filament uses BufferDescriptors to accomplish this.

+

Inside viewDidLoad, we'll statically define some verticies and indices and create a BufferDescriptor for each.

+
static const Vertex TRIANGLE_VERTICES[3] = {
+    { { 0.867, -0.500}, {1.0, 0.0, 0.0} },
+    { { 0.000,  1.000}, {0.0, 1.0, 0.0} },
+    { {-0.867, -0.500}, {0.0, 0.0, 1.0} },
+};
+static const uint16_t TRIANGLE_INDICES[3] = { 0, 1, 2 };
+
+VertexBuffer::BufferDescriptor vertices(TRIANGLE_VERTICES, sizeof(Vertex) * 3, nullptr);
+IndexBuffer::BufferDescriptor indices(TRIANGLE_INDICES, sizeof(uint16_t) * 3, nullptr);
+
+

The last argument is an optional callback function, which will be called after Filament is done uploading the data to the GPU. Inside the callback, you'd typically release the memory of any buffers via a free or delete call. We pass nullptr because we don't need a callback as our vertex and index buffer memory is static.

+

Now we can instantiate our VertexBuffer and IndexBuffer.

+
using Type = VertexBuffer::AttributeType;
+
+const uint8_t stride = sizeof(Vertex);
+_vertexBuffer = VertexBuffer::Builder()
+    .vertexCount(3)
+    .bufferCount(1)
+    .attribute(VertexAttribute::POSITION, 0, Type::FLOAT2, offsetof(Vertex, position), stride)
+    .attribute(VertexAttribute::COLOR,    0, Type::FLOAT3, offsetof(Vertex, color),    stride)
+    .build(*_engine);
+
+_indexBuffer = IndexBuffer::Builder()
+    .indexCount(3)
+    .bufferType(IndexBuffer::IndexType::USHORT)
+    .build(*_engine);
+
+_vertexBuffer->setBufferAt(*_engine, 0, std::move(vertices));
+_indexBuffer->setBuffer(*_engine, std::move(indices));
+
+

We first create an Entity like we did for our camera. This time, we're attaching a Renderable component to the entity. The Renderable component takes geometry defined by our vertex and index buffers, and makes the entity visible in our scene.

+
_triangle = utils::EntityManager::get().create();
+
+using Primitive = RenderableManager::PrimitiveType;
+RenderableManager::Builder(1)
+    .geometry(0, Primitive::TRIANGLES, _vertexBuffer, _indexBuffer, 0, 3)
+    .culling(false)
+    .receiveShadows(false)
+    .castShadows(false)
+    .build(*_engine, _triangle);
+
+// Add the triangle to the scene.
+_scene->addEntity(_triangle);
+
+

Destroy the entity and buffers in dealloc.

+
_engine->destroy(_triangle);
+EntityManager::get().destroy(_triangle);
+_engine->destroy(_indexBuffer);
+_engine->destroy(_vertexBuffer);
+
+

If you build and run the app now, you should see a plain white triangle. When we created the renderable, we didn't specify any specific Material to use, so Filament used a default, white material. Let's create a custom material to color the triangle.

+

a white triangle

+

Compiling a Custom Material

+

For simplicity, we're going to compile a custom material at runtime. For production, we recommend using our matc tool to compile materials offline. You can download it as part of one of our releases.

+

First, add a few more headers. We'll be using Filament's filamat library to compile a custom material.

+
#include <filament/Material.h>
+#include <filament/MaterialInstance.h>
+
+#include <filamat/MaterialBuilder.h>
+
+

We'll store our material in a new private var. We'll also need one to store a material instance. You can think of a material as a "template", where a material instance is an instantiation of the template (similar to OOP classes and instances). For more information on Filament materials, read the Filament Materials Guide.

+
Material* _material;
+MaterialInstance* _materialInstance;
+
+

We'll use the filamat library to compile a material into a package, which we can then load into Filament. The material will be simple; it will load the interpolated color attribute and set it as the baseColor.

+

Make sure to insert this code into viewDidLoad before we create our Renderable.

+
// init must be called before we can build any materials.
+filamat::MaterialBuilder::init();
+
+// Compile a custom material to use on the triangle.
+filamat::Package pkg = filamat::MaterialBuilder()
+    // The material name, only used for debugging purposes.
+    .name("Triangle material")
+    // Use the unlit shading mode, because we don't have any lights in our scene.
+    .shading(filamat::MaterialBuilder::Shading::UNLIT)
+    // Expose the COLOR attribute visible to our shader code.
+    .require(VertexAttribute::COLOR)
+    // Custom GLSL fragment shader
+    .material("void material (inout MaterialInputs material) {"
+              "  prepareMaterial(material);"
+              "  material.baseColor = getColor();"
+              "}")
+    // Compile for Metal on mobile platforms.
+    .targetApi(filamat::MaterialBuilder::TargetApi::METAL)
+    .platform(filamat::MaterialBuilder::Platform::MOBILE)
+    .build();
+assert(pkg.isValid());
+
+// shutdown should be called after all materials are built.
+filamat::MaterialBuilder::shutdown();
+
+

Now that we have a filamat::Package representing the material, we can use it to instantiate a Filament Material. Note that again, we recommend using the matc command-line tool to compile material packages during your app's compilation phase if possible, instead of at run-time.

+
// Create a Filament material from the Package.
+_material = Material::Builder()
+    .package(pkg.getData(), pkg.getSize())
+    .build(*_engine);
+_materialInstance = _material->getDefaultInstance();
+
+

Now we can use the MaterialInstance when creating our Renderable.

+
// Create a renderable using our geometry and material.
+using Primitive = RenderableManager::PrimitiveType;
+RenderableManager::Builder(1)
+    .geometry(0, Primitive::TRIANGLES, _vertexBuffer, _indexBuffer, 0, 3)
+    // Use the MaterialInstance we just created.
+    .material(0, _materialInstance)
+    .culling(false)
+    .receiveShadows(false)
+    .castShadows(false)
+    .build(*_engine, _triangle);
+
+

Lastly, we make sure to destroy everything inside dealloc.

+
_engine->destroy(_materialInstance);
+_engine->destroy(_material);
+
+

Build and run. You should see the same triangle, but with colors.

+

the triangle with our custom material

+

Animating the Triangle

+

We'll do this by animating a transform on our triangle entity. First, include a new header.

+
#include <filament/TransformManager.h>
+
+

When we create our triangle entity, we'll also attach a transform component. We've already seen two other components: Renderable and Camera. The Transform component allows us to set world-space transformations on entities.

+

Inside viewDidLoad, after we create the triangle entity's Renderable component, we'll also attach a Transform component.

+
// Add a Transform component to the triangle, so we can animate it.
+_engine->getTransformManager().create(_triangle);
+
+

Create a new function, update, and add call it inside the drawInMTKView: method.

+
- (void)update {
+    auto& tm = _engine->getTransformManager();
+    auto i = tm.getInstance(_triangle);
+    const auto time = CACurrentMediaTime();
+    tm.setTransform(i, math::mat4f::rotation(time, math::float3 {0.0, 0.0, 1.0}));
+}
+
+- (void)drawInMTKView:(nonnull MTKView*)view {
+    [self update];
+    if (_renderer->beginFrame(_swapChain)) {
+        _renderer->render(_view);
+        _renderer->endFrame();
+    }
+}
+
+

Now we should see the triangle rotate around its z axis.

+

a rotating triangle

+

Next Steps

+

In this guide we've covered how to install Filament with CocoaPods and get rendering using the Metal backend. We also compiled a custom material. Again, here's the complete sample code for the app. If you're interesting in learning more, check out Filament's additional iOS samples. If you have any problems, feel free to open an issue.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/samples/web.html b/docs/samples/web.html new file mode 100644 index 0000000000..c02358bcd5 --- /dev/null +++ b/docs/samples/web.html @@ -0,0 +1,231 @@ + + + + + + Web Tutorial - Filament + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/docs/searcher.js b/docs/searcher.js new file mode 100644 index 0000000000..dc03e0a02d --- /dev/null +++ b/docs/searcher.js @@ -0,0 +1,483 @@ +"use strict"; +window.search = window.search || {}; +(function search(search) { + // Search functionality + // + // You can use !hasFocus() to prevent keyhandling in your key + // event handlers while the user is typing their search. + + if (!Mark || !elasticlunr) { + return; + } + + //IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith + if (!String.prototype.startsWith) { + String.prototype.startsWith = function(search, pos) { + return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search; + }; + } + + var search_wrap = document.getElementById('search-wrapper'), + searchbar = document.getElementById('searchbar'), + searchbar_outer = document.getElementById('searchbar-outer'), + searchresults = document.getElementById('searchresults'), + searchresults_outer = document.getElementById('searchresults-outer'), + searchresults_header = document.getElementById('searchresults-header'), + searchicon = document.getElementById('search-toggle'), + content = document.getElementById('content'), + + searchindex = null, + doc_urls = [], + results_options = { + teaser_word_count: 30, + limit_results: 30, + }, + search_options = { + bool: "AND", + expand: true, + fields: { + title: {boost: 1}, + body: {boost: 1}, + breadcrumbs: {boost: 0} + } + }, + mark_exclude = [], + marker = new Mark(content), + current_searchterm = "", + URL_SEARCH_PARAM = 'search', + URL_MARK_PARAM = 'highlight', + teaser_count = 0, + + SEARCH_HOTKEY_KEYCODE = 83, + ESCAPE_KEYCODE = 27, + DOWN_KEYCODE = 40, + UP_KEYCODE = 38, + SELECT_KEYCODE = 13; + + function hasFocus() { + return searchbar === document.activeElement; + } + + function removeChildren(elem) { + while (elem.firstChild) { + elem.removeChild(elem.firstChild); + } + } + + // Helper to parse a url into its building blocks. + function parseURL(url) { + var a = document.createElement('a'); + a.href = url; + return { + source: url, + protocol: a.protocol.replace(':',''), + host: a.hostname, + port: a.port, + params: (function(){ + var ret = {}; + var seg = a.search.replace(/^\?/,'').split('&'); + var len = seg.length, i = 0, s; + for (;i': '>', + '"': '"', + "'": ''' + }; + var repl = function(c) { return MAP[c]; }; + return function(s) { + return s.replace(/[&<>'"]/g, repl); + }; + })(); + + function formatSearchMetric(count, searchterm) { + if (count == 1) { + return count + " search result for '" + searchterm + "':"; + } else if (count == 0) { + return "No search results for '" + searchterm + "'."; + } else { + return count + " search results for '" + searchterm + "':"; + } + } + + function formatSearchResult(result, searchterms) { + var teaser = makeTeaser(escapeHTML(result.doc.body), searchterms); + teaser_count++; + + // The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor + var url = doc_urls[result.ref].split("#"); + if (url.length == 1) { // no anchor found + url.push(""); + } + + // encodeURIComponent escapes all chars that could allow an XSS except + // for '. Due to that we also manually replace ' with its url-encoded + // representation (%27). + var searchterms = encodeURIComponent(searchterms.join(" ")).replace(/\'/g, "%27"); + + return '' + result.doc.breadcrumbs + '' + + '' + + teaser + ''; + } + + function makeTeaser(body, searchterms) { + // The strategy is as follows: + // First, assign a value to each word in the document: + // Words that correspond to search terms (stemmer aware): 40 + // Normal words: 2 + // First word in a sentence: 8 + // Then use a sliding window with a constant number of words and count the + // sum of the values of the words within the window. Then use the window that got the + // maximum sum. If there are multiple maximas, then get the last one. + // Enclose the terms in . + var stemmed_searchterms = searchterms.map(function(w) { + return elasticlunr.stemmer(w.toLowerCase()); + }); + var searchterm_weight = 40; + var weighted = []; // contains elements of ["word", weight, index_in_document] + // split in sentences, then words + var sentences = body.toLowerCase().split('. '); + var index = 0; + var value = 0; + var searchterm_found = false; + for (var sentenceindex in sentences) { + var words = sentences[sentenceindex].split(' '); + value = 8; + for (var wordindex in words) { + var word = words[wordindex]; + if (word.length > 0) { + for (var searchtermindex in stemmed_searchterms) { + if (elasticlunr.stemmer(word).startsWith(stemmed_searchterms[searchtermindex])) { + value = searchterm_weight; + searchterm_found = true; + } + }; + weighted.push([word, value, index]); + value = 2; + } + index += word.length; + index += 1; // ' ' or '.' if last word in sentence + }; + index += 1; // because we split at a two-char boundary '. ' + }; + + if (weighted.length == 0) { + return body; + } + + var window_weight = []; + var window_size = Math.min(weighted.length, results_options.teaser_word_count); + + var cur_sum = 0; + for (var wordindex = 0; wordindex < window_size; wordindex++) { + cur_sum += weighted[wordindex][1]; + }; + window_weight.push(cur_sum); + for (var wordindex = 0; wordindex < weighted.length - window_size; wordindex++) { + cur_sum -= weighted[wordindex][1]; + cur_sum += weighted[wordindex + window_size][1]; + window_weight.push(cur_sum); + }; + + if (searchterm_found) { + var max_sum = 0; + var max_sum_window_index = 0; + // backwards + for (var i = window_weight.length - 1; i >= 0; i--) { + if (window_weight[i] > max_sum) { + max_sum = window_weight[i]; + max_sum_window_index = i; + } + }; + } else { + max_sum_window_index = 0; + } + + // add around searchterms + var teaser_split = []; + var index = weighted[max_sum_window_index][2]; + for (var i = max_sum_window_index; i < max_sum_window_index+window_size; i++) { + var word = weighted[i]; + if (index < word[2]) { + // missing text from index to start of `word` + teaser_split.push(body.substring(index, word[2])); + index = word[2]; + } + if (word[1] == searchterm_weight) { + teaser_split.push("") + } + index = word[2] + word[0].length; + teaser_split.push(body.substring(word[2], index)); + if (word[1] == searchterm_weight) { + teaser_split.push("") + } + }; + + return teaser_split.join(''); + } + + function init(config) { + results_options = config.results_options; + search_options = config.search_options; + searchbar_outer = config.searchbar_outer; + doc_urls = config.doc_urls; + searchindex = elasticlunr.Index.load(config.index); + + // Set up events + searchicon.addEventListener('click', function(e) { searchIconClickHandler(); }, false); + searchbar.addEventListener('keyup', function(e) { searchbarKeyUpHandler(); }, false); + document.addEventListener('keydown', function(e) { globalKeyHandler(e); }, false); + // If the user uses the browser buttons, do the same as if a reload happened + window.onpopstate = function(e) { doSearchOrMarkFromUrl(); }; + // Suppress "submit" events so the page doesn't reload when the user presses Enter + document.addEventListener('submit', function(e) { e.preventDefault(); }, false); + + // If reloaded, do the search or mark again, depending on the current url parameters + doSearchOrMarkFromUrl(); + } + + function unfocusSearchbar() { + // hacky, but just focusing a div only works once + var tmp = document.createElement('input'); + tmp.setAttribute('style', 'position: absolute; opacity: 0;'); + searchicon.appendChild(tmp); + tmp.focus(); + tmp.remove(); + } + + // On reload or browser history backwards/forwards events, parse the url and do search or mark + function doSearchOrMarkFromUrl() { + // Check current URL for search request + var url = parseURL(window.location.href); + if (url.params.hasOwnProperty(URL_SEARCH_PARAM) + && url.params[URL_SEARCH_PARAM] != "") { + showSearch(true); + searchbar.value = decodeURIComponent( + (url.params[URL_SEARCH_PARAM]+'').replace(/\+/g, '%20')); + searchbarKeyUpHandler(); // -> doSearch() + } else { + showSearch(false); + } + + if (url.params.hasOwnProperty(URL_MARK_PARAM)) { + var words = decodeURIComponent(url.params[URL_MARK_PARAM]).split(' '); + marker.mark(words, { + exclude: mark_exclude + }); + + var markers = document.querySelectorAll("mark"); + function hide() { + for (var i = 0; i < markers.length; i++) { + markers[i].classList.add("fade-out"); + window.setTimeout(function(e) { marker.unmark(); }, 300); + } + } + for (var i = 0; i < markers.length; i++) { + markers[i].addEventListener('click', hide); + } + } + } + + // Eventhandler for keyevents on `document` + function globalKeyHandler(e) { + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text' || !hasFocus() && /^(?:input|select|textarea)$/i.test(e.target.nodeName)) { return; } + + if (e.keyCode === ESCAPE_KEYCODE) { + e.preventDefault(); + searchbar.classList.remove("active"); + setSearchUrlParameters("", + (searchbar.value.trim() !== "") ? "push" : "replace"); + if (hasFocus()) { + unfocusSearchbar(); + } + showSearch(false); + marker.unmark(); + } else if (!hasFocus() && e.keyCode === SEARCH_HOTKEY_KEYCODE) { + e.preventDefault(); + showSearch(true); + window.scrollTo(0, 0); + searchbar.select(); + } else if (hasFocus() && e.keyCode === DOWN_KEYCODE) { + e.preventDefault(); + unfocusSearchbar(); + searchresults.firstElementChild.classList.add("focus"); + } else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE + || e.keyCode === UP_KEYCODE + || e.keyCode === SELECT_KEYCODE)) { + // not `:focus` because browser does annoying scrolling + var focused = searchresults.querySelector("li.focus"); + if (!focused) return; + e.preventDefault(); + if (e.keyCode === DOWN_KEYCODE) { + var next = focused.nextElementSibling; + if (next) { + focused.classList.remove("focus"); + next.classList.add("focus"); + } + } else if (e.keyCode === UP_KEYCODE) { + focused.classList.remove("focus"); + var prev = focused.previousElementSibling; + if (prev) { + prev.classList.add("focus"); + } else { + searchbar.select(); + } + } else { // SELECT_KEYCODE + window.location.assign(focused.querySelector('a')); + } + } + } + + function showSearch(yes) { + if (yes) { + search_wrap.classList.remove('hidden'); + searchicon.setAttribute('aria-expanded', 'true'); + } else { + search_wrap.classList.add('hidden'); + searchicon.setAttribute('aria-expanded', 'false'); + var results = searchresults.children; + for (var i = 0; i < results.length; i++) { + results[i].classList.remove("focus"); + } + } + } + + function showResults(yes) { + if (yes) { + searchresults_outer.classList.remove('hidden'); + } else { + searchresults_outer.classList.add('hidden'); + } + } + + // Eventhandler for search icon + function searchIconClickHandler() { + if (search_wrap.classList.contains('hidden')) { + showSearch(true); + window.scrollTo(0, 0); + searchbar.select(); + } else { + showSearch(false); + } + } + + // Eventhandler for keyevents while the searchbar is focused + function searchbarKeyUpHandler() { + var searchterm = searchbar.value.trim(); + if (searchterm != "") { + searchbar.classList.add("active"); + doSearch(searchterm); + } else { + searchbar.classList.remove("active"); + showResults(false); + removeChildren(searchresults); + } + + setSearchUrlParameters(searchterm, "push_if_new_search_else_replace"); + + // Remove marks + marker.unmark(); + } + + // Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and #heading-anchor . + // `action` can be one of "push", "replace", "push_if_new_search_else_replace" + // and replaces or pushes a new browser history item. + // "push_if_new_search_else_replace" pushes if there is no `?URL_SEARCH_PARAM=abc` yet. + function setSearchUrlParameters(searchterm, action) { + var url = parseURL(window.location.href); + var first_search = ! url.params.hasOwnProperty(URL_SEARCH_PARAM); + if (searchterm != "" || action == "push_if_new_search_else_replace") { + url.params[URL_SEARCH_PARAM] = searchterm; + delete url.params[URL_MARK_PARAM]; + url.hash = ""; + } else { + delete url.params[URL_MARK_PARAM]; + delete url.params[URL_SEARCH_PARAM]; + } + // A new search will also add a new history item, so the user can go back + // to the page prior to searching. A updated search term will only replace + // the url. + if (action == "push" || (action == "push_if_new_search_else_replace" && first_search) ) { + history.pushState({}, document.title, renderURL(url)); + } else if (action == "replace" || (action == "push_if_new_search_else_replace" && !first_search) ) { + history.replaceState({}, document.title, renderURL(url)); + } + } + + function doSearch(searchterm) { + + // Don't search the same twice + if (current_searchterm == searchterm) { return; } + else { current_searchterm = searchterm; } + + if (searchindex == null) { return; } + + // Do the actual search + var results = searchindex.search(searchterm, search_options); + var resultcount = Math.min(results.length, results_options.limit_results); + + // Display search metrics + searchresults_header.innerText = formatSearchMetric(resultcount, searchterm); + + // Clear and insert results + var searchterms = searchterm.split(' '); + removeChildren(searchresults); + for(var i = 0; i < resultcount ; i++){ + var resultElem = document.createElement('li'); + resultElem.innerHTML = formatSearchResult(results[i], searchterms); + searchresults.appendChild(resultElem); + } + + // Display results + showResults(true); + } + + fetch(path_to_root + 'searchindex.json') + .then(response => response.json()) + .then(json => init(json)) + .catch(error => { // Try to load searchindex.js if fetch failed + var script = document.createElement('script'); + script.src = path_to_root + 'searchindex.js'; + script.onload = () => init(window.search); + document.head.appendChild(script); + }); + + // Exported functions + search.hasFocus = hasFocus; +})(window.search); diff --git a/docs/searchindex.js b/docs/searchindex.js new file mode 100644 index 0000000000..8c7494bee3 --- /dev/null +++ b/docs/searchindex.js @@ -0,0 +1 @@ +Object.assign(window.search, {"doc_urls":["dup/intro.html#filament","dup/intro.html#download","dup/intro.html#android","dup/intro.html#ios","dup/intro.html#documentation","dup/intro.html#examples","dup/intro.html#features","dup/intro.html#apis","dup/intro.html#backends","dup/intro.html#rendering","dup/intro.html#post-processing","dup/intro.html#gltf-20","dup/intro.html#rendering-with-filament","dup/intro.html#native-linux-macos-and-windows","dup/intro.html#android-1","dup/intro.html#ios-1","dup/intro.html#assets","dup/intro.html#how-to-make-contributions","dup/intro.html#directory-structure","dup/intro.html#license","dup/intro.html#disclaimer","dup/building.html#building-filament","dup/building.html#prerequisites","dup/building.html#environment-variables","dup/building.html#ide","dup/building.html#easy-build","dup/building.html#filament-specific-cmake-options","dup/building.html#linux","dup/building.html#macos","dup/building.html#ios","dup/building.html#windows","dup/building.html#android","dup/building.html#webassembly","dup/building.html#running-the-native-samples","dup/building.html#generating-c-documentation","dup/building.html#software-rasterization","dup/building.html#swiftshader-vulkan-tested-on-macos-and-linux","dup/building.html#mesas-llvmpipe-gl-and-lavapipe-vulkan-tested-on-linux","build/windows_android.html#building-filament-for-android-on-windows","build/windows_android.html#prerequisites","build/windows_android.html#a-note-about-python-3","build/windows_android.html#desktop-tools","build/windows_android.html#build","build/windows_android.html#generate-aar","build/maven_release.html#maven-release","build/maven_release.html#register-for-a-sonatype-account","build/maven_release.html#signing-key","build/maven_release.html#build-the-android-release","build/maven_release.html#publish-to-sonatype","build/maven_release.html#a-note-on-the-legacy-staging-service","build/maven_release.html#1-upload-to-the-staging-api-compatibility-service","build/maven_release.html#2-move-the-repository-to-the-central-publisher-portal","build/maven_release.html#3-publish-the-release-on-sonatype","dup/contributing.html#how-to-become-a-contributor-and-submit-your-own-code","dup/contributing.html#contributor-license-agreement","dup/contributing.html#contributing-a-patch","dup/contributing.html#code-style","dup/contributing.html#code-reviews","dup/contributing.html#community-guidelines","dup/contributing.html#dependencies","dup/code_style.html#filament-code-style-and-formatting","dup/code_style.html#formatting","dup/code_style.html#naming-conventions","dup/code_style.html#files","dup/code_style.html#code","dup/code_style.html#code-style","dup/code_style.html#files-1","dup/code_style.html#headers","dup/code_style.html#strings","dup/code_style.html#misc","main/index.html#core-concepts","main/filament.html","main/materials.html","samples/index.html#tutorials-and-samples","samples/ios.html#cocoapods-hello-triangle","samples/ios.html#creating-a-boilerplate-app-with-filament","samples/ios.html#instantiating-the-filament-engine","samples/ios.html#creating-a-swapchain","samples/ios.html#clearing-the-screen","samples/ios.html#drawing-a-triangle","samples/ios.html#compiling-a-custom-material","samples/ios.html#animating-the-triangle","samples/ios.html#next-steps","samples/web.html#web-docs","samples/web.html#this-page-is-under-construction-links-are-not-working-at-the-moment","samples/web.html#tutorials","samples/web.html#demos","samples/web.html#other-documentation","notes/index.html#technical-notes","notes/material_properties.html#crafting-physically-based-materials","notes/material_properties.html#base-colorsrgb","notes/material_properties.html#base-color-luminosity","notes/material_properties.html#metallic-samples","notes/material_properties.html#non-metallic-samples","notes/material_properties.html#metallicgrayscale","notes/material_properties.html#roughnessgrayscale","notes/material_properties.html#non-metallic","notes/material_properties.html#metallic","notes/material_properties.html#reflectancegrayscale","notes/material_properties.html#samples","notes/material_properties.html#clear-coatgrayscale","notes/material_properties.html#clear-coat-roughnessgrayscale","notes/material_properties.html#anisotropygrayscale","notes/versioning.html#versioning","notes/versioning.html#material-versioning","notes/branching.html#branching","notes/branching.html#which-branch-do-i-open-my-pr-against","notes/branching.html#what-consitutes-a-bug","notes/release_guide.html#filament-release-guide","notes/release_guide.html#0-check-versions","notes/release_guide.html#1-bump-filament-versions-on-main-to-release","notes/release_guide.html#2-update-release_notesmd-on-main","notes/release_guide.html#3-run-release-script","notes/release_guide.html#4-push-the-release-branch","notes/release_guide.html#5-create-the-github-release","notes/release_guide.html#6-delete-the-old-rc-branch-optional","notes/release_guide.html#7-bump-the-version-on-the-new-rc-branch-to-next_release","notes/release_guide.html#8-push-main","notes/release_guide.html#9-push-the-new-rc-branch","notes/release_guide.html#10-rebuild-the-github-release-if-failed","notes/release_guide.html#remove-any-assets-uploaded-to-the-release-if-needed","notes/release_guide.html#update-the-release-branch-if-needed","notes/release_guide.html#re-run-the-github-release-workflow","notes/release_guide.html#11-kick-off-the-npm-and-cocoapods-release-jobs","dup/docs.html#documentation","dup/docs.html#how-to-create","dup/docs.html#prerequisites","dup/docs.html#how-to-generate","dup/docs.html#copy-to-docs","dup/docs.html#document-sources","dup/docs.html#introductory-doc","dup/docs.html#core-concept-docs","dup/docs.html#readmes","dup/docs.html#other-technical-notes","dup/docs.html#raw-source-files","dup/docs.html#adding-more-documents","notes/debugging.html#debugging","notes/metal_debugging.html#debugging-metal","notes/metal_debugging.html#enable-metal-validation","notes/metal_debugging.html#metal-frame-capture-from-gltf_viewer","notes/metal_debugging.html#1-create-an-infoplist-file","notes/metal_debugging.html#2-capture-a-frame","notes/vulkan_debugging.html#debugging-vulkan","notes/vulkan_debugging.html#enable-validation-logs","notes/spirv_debugging.html#investigating-spirv-cross--spirv-tools-issues","notes/spirv_debugging.html#build-and-install-command-line-tools-on-path","notes/spirv_debugging.html#clone-and-build-each-repo","notes/spirv_debugging.html#add-directories-to-path","notes/spirv_debugging.html#isolate-the-problematic-glsl-shader","notes/spirv_debugging.html#reproduce-the-compilation-error","notes/spirv_debugging.html#clean-up-the-shader-for-a-bug-report","notes/spirv_debugging.html#submit-an-issue-with-the-relevant-khronos-repository","notes/asan_ubsan.html#running-with-asanubsan","notes/asan_ubsan.html#enabling","notes/asan_ubsan.html#getting-memory-leak-detection-on-mac","notes/asan_ubsan.html#getting-memory-leak-output-in-clion","notes/asan_ubsan.html#setting-variables","notes/asan_ubsan.html#avoiding-losing-output","notes/instruments.html#using-instruments-on-macos","notes/coverage.html#generating-backend-code-coverage","notes/coverage.html#1-prerequisites-install-clang-and-llvm-tools","notes/coverage.html#using-homebrew","notes/coverage.html#using-macports","notes/coverage.html#required-tools","notes/coverage.html#2-build-filament-with-coverage-enabled","notes/coverage.html#3-run-the-backend-tests","notes/coverage.html#4-generate-the-coverage-report","notes/performance_analysis.html#performance-analysis","notes/performance_analysis.html#android","notes/performance_analysis.html#prerequisites","notes/framegraph.html#framegraph","notes/framegraph.html#details","notes/framegraph.html#dependency-graph","notes/framegraph.html#framegraph-1","notes/framegraph.html#an-example","notes/framegraph.html#example-code","notes/framegraph.html#what-does-it-do","notes/framegraph.html#additional-details","notes/libs.html#libraries","dup/bluegl.html#bluegl-mechanics","dup/bluegl.html#step-0-run-bluegl-genpy","dup/bluegl.html#step-1-include-the-bluegl-defines-header","dup/bluegl.html#step-2-include-the-bluegl-header-after-the-defines-header","dup/bluegl.html#step-3-call-blueglbind","dup/bluegl.html#step-4-call-glclear","dup/bluevk.html#updating-vulkan-headers","dup/filamat.html#filamat","dup/filamat.html#libraries","dup/filamat.html#linking-against-filamat","dup/filamat.html#linux","dup/filamat.html#macos","dup/filamat.html#windows","dup/filamat.html#compiling","dup/filamat.html#using-the-material-with-filament","dup/filamat.html#filamat-lite","dup/gltfio.html#description","dup/gltfio.html#ubershaderprovider","dup/iblprefilter.html#ibl-prefilter","dup/iblprefilter.html#library-and-headers","dup/iblprefilter.html#performance","dup/iblprefilter.html#example","dup/matdbg.html#matdbg","dup/matdbg.html#capabilities","dup/matdbg.html#setup-for-desktop","dup/matdbg.html#setup-for-android","dup/matdbg.html#debugger-usage","dup/matdbg.html#keyboard-shortcuts","dup/matdbg.html#architecture-overview","dup/matdbg.html#c-server","dup/matdbg.html#javascript-client","dup/matdbg.html#http-requests","dup/matdbg.html#wish-list","dup/matdbg.html#screenshot","dup/uberz.html#ubershader-archive-files","dup/uberz.html#ubershader-spec-files","notes/tools.html#tools","dup/beamsplitter.html#beamsplitter","dup/beamsplitter.html#description","dup/beamsplitter.html#instructions","dup/beamsplitter.html#emitter-flags","dup/beamsplitter.html#source-files","dup/beamsplitter.html#output-files","dup/beamsplitter.html#input-format","dup/beamsplitter.html#grammar","dup/beamsplitter.html#references","dup/cmgen.html#cmgen","dup/cmgen.html#usage","dup/cmgen.html#supported-input-formats","dup/cmgen.html#options","dup/cso_lut.html#conesphere-occlusion-lut-generator","dup/filamesh.html#filamesh","dup/filamesh.html#usage","dup/filamesh.html#format","dup/filamesh.html#header","dup/filamesh.html#vertex-data","dup/filamesh.html#index-data","dup/filamesh.html#parts","dup/filamesh.html#materials","dup/filamesh.html#example","dup/normal_blending.html#normal-blending","dup/mipgen.html#mipgen","dup/mipgen.html#usage","dup/matinfo.html#matinfo","dup/matinfo.html#usage","dup/roughness_prefilter.html#roughness-prefilter","dup/specular_color.html#specular-color","dup/specular_color.html#usage","dup/zbloat.html#zbloat","dup/zbloat.html#linux-macos-and-docker"],"index":{"documentStore":{"docInfo":{"0":{"body":37,"breadcrumbs":2,"title":1},"1":{"body":38,"breadcrumbs":2,"title":1},"10":{"body":43,"breadcrumbs":3,"title":2},"100":{"body":27,"breadcrumbs":6,"title":2},"101":{"body":23,"breadcrumbs":7,"title":3},"102":{"body":22,"breadcrumbs":5,"title":1},"103":{"body":70,"breadcrumbs":4,"title":1},"104":{"body":73,"breadcrumbs":5,"title":2},"105":{"body":3,"breadcrumbs":5,"title":1},"106":{"body":99,"breadcrumbs":8,"title":4},"107":{"body":36,"breadcrumbs":6,"title":2},"108":{"body":32,"breadcrumbs":8,"title":3},"109":{"body":24,"breadcrumbs":8,"title":3},"11":{"body":46,"breadcrumbs":3,"title":2},"110":{"body":21,"breadcrumbs":11,"title":6},"111":{"body":28,"breadcrumbs":9,"title":4},"112":{"body":21,"breadcrumbs":9,"title":4},"113":{"body":4,"breadcrumbs":9,"title":4},"114":{"body":15,"breadcrumbs":9,"title":4},"115":{"body":15,"breadcrumbs":11,"title":6},"116":{"body":13,"breadcrumbs":12,"title":7},"117":{"body":4,"breadcrumbs":8,"title":3},"118":{"body":5,"breadcrumbs":10,"title":5},"119":{"body":11,"breadcrumbs":10,"title":5},"12":{"body":0,"breadcrumbs":3,"title":2},"120":{"body":12,"breadcrumbs":10,"title":5},"121":{"body":37,"breadcrumbs":9,"title":4},"122":{"body":21,"breadcrumbs":10,"title":5},"123":{"body":36,"breadcrumbs":11,"title":6},"124":{"body":7,"breadcrumbs":4,"title":1},"125":{"body":0,"breadcrumbs":6,"title":3},"126":{"body":35,"breadcrumbs":4,"title":1},"127":{"body":37,"breadcrumbs":4,"title":1},"128":{"body":11,"breadcrumbs":5,"title":2},"129":{"body":11,"breadcrumbs":5,"title":2},"13":{"body":161,"breadcrumbs":5,"title":4},"130":{"body":39,"breadcrumbs":5,"title":2},"131":{"body":104,"breadcrumbs":6,"title":3},"132":{"body":39,"breadcrumbs":4,"title":1},"133":{"body":16,"breadcrumbs":5,"title":2},"134":{"body":19,"breadcrumbs":6,"title":3},"135":{"body":41,"breadcrumbs":6,"title":3},"136":{"body":5,"breadcrumbs":4,"title":1},"137":{"body":0,"breadcrumbs":6,"title":2},"138":{"body":31,"breadcrumbs":7,"title":3},"139":{"body":5,"breadcrumbs":8,"title":4},"14":{"body":60,"breadcrumbs":2,"title":1},"140":{"body":28,"breadcrumbs":8,"title":4},"141":{"body":21,"breadcrumbs":7,"title":3},"142":{"body":0,"breadcrumbs":6,"title":2},"143":{"body":42,"breadcrumbs":7,"title":3},"144":{"body":18,"breadcrumbs":11,"title":6},"145":{"body":13,"breadcrumbs":11,"title":6},"146":{"body":95,"breadcrumbs":9,"title":4},"147":{"body":21,"breadcrumbs":8,"title":3},"148":{"body":90,"breadcrumbs":9,"title":4},"149":{"body":140,"breadcrumbs":8,"title":3},"15":{"body":31,"breadcrumbs":2,"title":1},"150":{"body":83,"breadcrumbs":10,"title":5},"151":{"body":19,"breadcrumbs":10,"title":5},"152":{"body":0,"breadcrumbs":8,"title":2},"153":{"body":33,"breadcrumbs":7,"title":1},"154":{"body":62,"breadcrumbs":11,"title":5},"155":{"body":0,"breadcrumbs":11,"title":5},"156":{"body":35,"breadcrumbs":8,"title":2},"157":{"body":49,"breadcrumbs":9,"title":3},"158":{"body":81,"breadcrumbs":9,"title":3},"159":{"body":20,"breadcrumbs":10,"title":4},"16":{"body":30,"breadcrumbs":2,"title":1},"160":{"body":14,"breadcrumbs":12,"title":6},"161":{"body":16,"breadcrumbs":8,"title":2},"162":{"body":23,"breadcrumbs":8,"title":2},"163":{"body":30,"breadcrumbs":8,"title":2},"164":{"body":40,"breadcrumbs":11,"title":5},"165":{"body":36,"breadcrumbs":10,"title":4},"166":{"body":95,"breadcrumbs":10,"title":4},"167":{"body":0,"breadcrumbs":7,"title":2},"168":{"body":0,"breadcrumbs":6,"title":1},"169":{"body":608,"breadcrumbs":6,"title":1},"17":{"body":10,"breadcrumbs":3,"title":2},"170":{"body":46,"breadcrumbs":4,"title":1},"171":{"body":0,"breadcrumbs":4,"title":1},"172":{"body":41,"breadcrumbs":5,"title":2},"173":{"body":40,"breadcrumbs":4,"title":1},"174":{"body":71,"breadcrumbs":4,"title":1},"175":{"body":147,"breadcrumbs":5,"title":2},"176":{"body":51,"breadcrumbs":3,"title":0},"177":{"body":87,"breadcrumbs":5,"title":2},"178":{"body":4,"breadcrumbs":4,"title":1},"179":{"body":3,"breadcrumbs":6,"title":2},"18":{"body":310,"breadcrumbs":3,"title":2},"180":{"body":31,"breadcrumbs":9,"title":5},"181":{"body":9,"breadcrumbs":10,"title":6},"182":{"body":8,"breadcrumbs":11,"title":7},"183":{"body":27,"breadcrumbs":8,"title":4},"184":{"body":34,"breadcrumbs":8,"title":4},"185":{"body":52,"breadcrumbs":7,"title":3},"186":{"body":41,"breadcrumbs":5,"title":1},"187":{"body":51,"breadcrumbs":5,"title":1},"188":{"body":114,"breadcrumbs":7,"title":3},"189":{"body":35,"breadcrumbs":5,"title":1},"19":{"body":3,"breadcrumbs":2,"title":1},"190":{"body":30,"breadcrumbs":5,"title":1},"191":{"body":68,"breadcrumbs":5,"title":1},"192":{"body":23,"breadcrumbs":5,"title":1},"193":{"body":35,"breadcrumbs":7,"title":3},"194":{"body":77,"breadcrumbs":6,"title":2},"195":{"body":71,"breadcrumbs":5,"title":1},"196":{"body":57,"breadcrumbs":5,"title":1},"197":{"body":33,"breadcrumbs":6,"title":2},"198":{"body":9,"breadcrumbs":6,"title":2},"199":{"body":14,"breadcrumbs":5,"title":1},"2":{"body":84,"breadcrumbs":2,"title":1},"20":{"body":4,"breadcrumbs":2,"title":1},"200":{"body":51,"breadcrumbs":5,"title":1},"201":{"body":22,"breadcrumbs":5,"title":1},"202":{"body":61,"breadcrumbs":5,"title":1},"203":{"body":54,"breadcrumbs":6,"title":2},"204":{"body":115,"breadcrumbs":6,"title":2},"205":{"body":83,"breadcrumbs":6,"title":2},"206":{"body":30,"breadcrumbs":6,"title":2},"207":{"body":90,"breadcrumbs":6,"title":2},"208":{"body":42,"breadcrumbs":6,"title":2},"209":{"body":93,"breadcrumbs":6,"title":2},"21":{"body":0,"breadcrumbs":4,"title":2},"210":{"body":209,"breadcrumbs":6,"title":2},"211":{"body":59,"breadcrumbs":6,"title":2},"212":{"body":0,"breadcrumbs":5,"title":1},"213":{"body":151,"breadcrumbs":7,"title":3},"214":{"body":168,"breadcrumbs":7,"title":3},"215":{"body":4,"breadcrumbs":4,"title":1},"216":{"body":10,"breadcrumbs":5,"title":1},"217":{"body":16,"breadcrumbs":5,"title":1},"218":{"body":15,"breadcrumbs":5,"title":1},"219":{"body":42,"breadcrumbs":6,"title":2},"22":{"body":46,"breadcrumbs":3,"title":1},"220":{"body":1,"breadcrumbs":6,"title":2},"221":{"body":21,"breadcrumbs":6,"title":2},"222":{"body":118,"breadcrumbs":6,"title":2},"223":{"body":158,"breadcrumbs":5,"title":1},"224":{"body":51,"breadcrumbs":5,"title":1},"225":{"body":47,"breadcrumbs":5,"title":1},"226":{"body":7,"breadcrumbs":5,"title":1},"227":{"body":13,"breadcrumbs":7,"title":3},"228":{"body":151,"breadcrumbs":5,"title":1},"229":{"body":8,"breadcrumbs":9,"title":4},"23":{"body":20,"breadcrumbs":4,"title":2},"230":{"body":63,"breadcrumbs":5,"title":1},"231":{"body":3,"breadcrumbs":5,"title":1},"232":{"body":21,"breadcrumbs":5,"title":1},"233":{"body":135,"breadcrumbs":5,"title":1},"234":{"body":62,"breadcrumbs":6,"title":2},"235":{"body":7,"breadcrumbs":6,"title":2},"236":{"body":44,"breadcrumbs":5,"title":1},"237":{"body":19,"breadcrumbs":5,"title":1},"238":{"body":324,"breadcrumbs":5,"title":1},"239":{"body":29,"breadcrumbs":7,"title":2},"24":{"body":14,"breadcrumbs":3,"title":1},"240":{"body":7,"breadcrumbs":5,"title":1},"241":{"body":11,"breadcrumbs":5,"title":1},"242":{"body":12,"breadcrumbs":5,"title":1},"243":{"body":4,"breadcrumbs":5,"title":1},"244":{"body":21,"breadcrumbs":7,"title":2},"245":{"body":56,"breadcrumbs":7,"title":2},"246":{"body":30,"breadcrumbs":6,"title":1},"247":{"body":70,"breadcrumbs":5,"title":1},"248":{"body":49,"breadcrumbs":7,"title":3},"25":{"body":80,"breadcrumbs":4,"title":2},"26":{"body":68,"breadcrumbs":6,"title":4},"27":{"body":193,"breadcrumbs":3,"title":1},"28":{"body":51,"breadcrumbs":3,"title":1},"29":{"body":22,"breadcrumbs":3,"title":1},"3":{"body":10,"breadcrumbs":2,"title":1},"30":{"body":126,"breadcrumbs":3,"title":1},"31":{"body":301,"breadcrumbs":3,"title":1},"32":{"body":155,"breadcrumbs":3,"title":1},"33":{"body":158,"breadcrumbs":5,"title":3},"34":{"body":19,"breadcrumbs":5,"title":3},"35":{"body":35,"breadcrumbs":4,"title":2},"36":{"body":29,"breadcrumbs":7,"title":5},"37":{"body":93,"breadcrumbs":9,"title":7},"38":{"body":0,"breadcrumbs":9,"title":4},"39":{"body":57,"breadcrumbs":6,"title":1},"4":{"body":49,"breadcrumbs":2,"title":1},"40":{"body":22,"breadcrumbs":8,"title":3},"41":{"body":57,"breadcrumbs":7,"title":2},"42":{"body":95,"breadcrumbs":6,"title":1},"43":{"body":68,"breadcrumbs":7,"title":2},"44":{"body":0,"breadcrumbs":6,"title":2},"45":{"body":51,"breadcrumbs":7,"title":3},"46":{"body":28,"breadcrumbs":6,"title":2},"47":{"body":14,"breadcrumbs":7,"title":3},"48":{"body":0,"breadcrumbs":6,"title":2},"49":{"body":37,"breadcrumbs":8,"title":4},"5":{"body":10,"breadcrumbs":2,"title":1},"50":{"body":4,"breadcrumbs":10,"title":6},"51":{"body":12,"breadcrumbs":10,"title":6},"52":{"body":28,"breadcrumbs":8,"title":4},"53":{"body":0,"breadcrumbs":6,"title":4},"54":{"body":44,"breadcrumbs":5,"title":3},"55":{"body":55,"breadcrumbs":4,"title":2},"56":{"body":2,"breadcrumbs":4,"title":2},"57":{"body":20,"breadcrumbs":4,"title":2},"58":{"body":7,"breadcrumbs":4,"title":2},"59":{"body":39,"breadcrumbs":3,"title":1},"6":{"body":0,"breadcrumbs":2,"title":1},"60":{"body":43,"breadcrumbs":7,"title":4},"61":{"body":33,"breadcrumbs":4,"title":1},"62":{"body":0,"breadcrumbs":5,"title":2},"63":{"body":84,"breadcrumbs":4,"title":1},"64":{"body":62,"breadcrumbs":4,"title":1},"65":{"body":0,"breadcrumbs":5,"title":2},"66":{"body":61,"breadcrumbs":4,"title":1},"67":{"body":152,"breadcrumbs":4,"title":1},"68":{"body":19,"breadcrumbs":4,"title":1},"69":{"body":14,"breadcrumbs":4,"title":1},"7":{"body":13,"breadcrumbs":2,"title":1},"70":{"body":14,"breadcrumbs":4,"title":2},"71":{"body":21782,"breadcrumbs":3,"title":2},"72":{"body":10005,"breadcrumbs":3,"title":2},"73":{"body":18,"breadcrumbs":4,"title":2},"74":{"body":80,"breadcrumbs":7,"title":3},"75":{"body":61,"breadcrumbs":8,"title":4},"76":{"body":142,"breadcrumbs":7,"title":3},"77":{"body":141,"breadcrumbs":6,"title":2},"78":{"body":305,"breadcrumbs":6,"title":2},"79":{"body":265,"breadcrumbs":6,"title":2},"8":{"body":22,"breadcrumbs":2,"title":1},"80":{"body":248,"breadcrumbs":7,"title":3},"81":{"body":96,"breadcrumbs":6,"title":2},"82":{"body":34,"breadcrumbs":6,"title":2},"83":{"body":0,"breadcrumbs":6,"title":2},"84":{"body":0,"breadcrumbs":10,"title":6},"85":{"body":6,"breadcrumbs":5,"title":1},"86":{"body":4,"breadcrumbs":5,"title":1},"87":{"body":4,"breadcrumbs":5,"title":1},"88":{"body":6,"breadcrumbs":4,"title":2},"89":{"body":0,"breadcrumbs":8,"title":4},"9":{"body":89,"breadcrumbs":2,"title":1},"90":{"body":18,"breadcrumbs":6,"title":2},"91":{"body":12,"breadcrumbs":7,"title":3},"92":{"body":24,"breadcrumbs":6,"title":2},"93":{"body":24,"breadcrumbs":7,"title":3},"94":{"body":22,"breadcrumbs":5,"title":1},"95":{"body":9,"breadcrumbs":5,"title":1},"96":{"body":1,"breadcrumbs":6,"title":2},"97":{"body":1,"breadcrumbs":5,"title":1},"98":{"body":20,"breadcrumbs":5,"title":1},"99":{"body":24,"breadcrumbs":5,"title":1}},"docs":{"0":{"body":"Android Build Status iOS Build Status Linux Build Status macOS Build Status Windows Build Status Web Build Status Filament is a real-time physically based rendering engine for Android, iOS, Linux, macOS, Windows, and WebGL. It is designed to be as small as possible and as efficient as possible on Android.","breadcrumbs":"Introduction » Filament","id":"0","title":"Filament"},"1":{"body":"Download Filament releases to access stable builds. Filament release archives contains host-side tools that are required to generate assets. Make sure you always use tools from the same release as the runtime library. This is particularly important for matc (material compiler). If you'd rather build Filament yourself, please refer to our build manual .","breadcrumbs":"Introduction » Download","id":"1","title":"Download"},"10":{"body":"HDR bloom Depth of field bokeh Multiple tone mappers: generic (customizable), ACES, filmic, etc. Color and tone management: luminance scaling, gamut mapping Color grading: exposure, night adaptation, white balance, channel mixer, shadows/mid-tones/highlights, ASC CDL, contrast, saturation, etc. TAA, FXAA, MSAA Screen-space lens flares","breadcrumbs":"Introduction » Post processing","id":"10","title":"Post processing"},"100":{"body":"Strength of the clear coat layer on top of a base dielectric or conductor layer. The clear coat layer will commonly be set to 0.0 or 1.0 . This layer has a fixed index of refraction of 1.5. 0.00.10.20.30.40.50.60.70.80.91.0 NO CLEAR COAT FULL CLEAR COAT","breadcrumbs":"Technical Notes » Material Properties » CLEAR COAT/GRAYSCALE","id":"100","title":"CLEAR COAT/GRAYSCALE"},"101":{"body":"Defines the perceived smoothness (0.0) or roughness (1.0) of the clear coat layer. It is sometimes called glossiness . This may affect the roughness of the base layer. 0.00.10.20.30.40.50.60.70.80.91.0 GLOSSY CLEAR COAT ROUGH CLEAR COAT","breadcrumbs":"Technical Notes » Material Properties » CLEAR COAT ROUGHNESS/GRAYSCALE","id":"101","title":"CLEAR COAT ROUGHNESS/GRAYSCALE"},"102":{"body":"Defines whether the material appearance is directionally dependent , that is isotropic (0.0) or anisotropic (1.0). Brushed metals are anisotropic . Values can be negative to change the orientation of the specular reflections. 0.00.10.20.30.40.50.60.70.80.91.0 ISOTROPIC ANISOTROPIC","breadcrumbs":"Technical Notes » Material Properties » ANISOTROPY/GRAYSCALE","id":"102","title":"ANISOTROPY/GRAYSCALE"},"103":{"body":"Filament uses a 3-number versioning scheme that superficially resembles a semantic version but is actually more interesting because of our material system. Here are the guidelines: Increment the most significant number only when making a non-backwards compatible API change, or when introducing a major new API. Increment the middle number only when making a non-backwards compatible change to the material system. When this number gets bumped, users need to rebuild their mat files. Reset the middle number to zero when the most significant number has been incremented. Increment the least significant number each time a new release is published. Reset this number to zero if one of the other two numbers have been incremented.","breadcrumbs":"Technical Notes » Versioning » Versioning","id":"103","title":"Versioning"},"104":{"body":"Additionally, the Filament renderer and material compiler internally contain a standalone integer called MATERIAL_VERSION, defined in MaterialEnums.h. This should be incremented every time we change the middle number in the public-facing version. When a material version mismatch is detected at run time, a panic is triggered, even in release builds. Therefore we should increment this only when making a serious breaking change to the material system (e.g. changing the size of a uniform block). Cosmetic shader changes usually do not merit a change to the material version number. Currently our material archives have two version chunks, one for \"normal\" materials and one for post-process materials. However for now these two numbers must be set to the same value.","breadcrumbs":"Technical Notes » Versioning » Material Versioning","id":"104","title":"Material Versioning"},"105":{"body":"Filament branching strategy","breadcrumbs":"Technical Notes » Versioning » Branching » Branching","id":"105","title":"Branching"},"106":{"body":"For normal development, open PRs against main. Once they're merged, no further action is necessary. If you discover a bug with the latest release candidate , open a bug fix PR against the release candidate branch (rc/1.9.0, for example). Once the PR is merged, decide whether it makes sense for the fix to also go into main. If it was a temporary fix, simply make the correct fix in main as you would any other change. If the fix is good for main as well, use git cherry-pick to cherry-pick it into main. If an immediate hotfix is needed on the release branch, open a PR against the release branch. Once the PR is merged, decide whether the fix is temporary or permanent. If the fix was temporary, make the correct fix in both the next release candidate branch and main. If the fix is good, use git cherry-pick to cherry-pick it into the relase candidate branch and main.","breadcrumbs":"Technical Notes » Versioning » Branching » Which branch do I open my PR against?","id":"106","title":"Which branch do I open my PR against?"},"107":{"body":"Only bug fix PRs should be opened against the release candidate branch. Bugs are defined as one of the following introduced since the prior release : crashes rendering issues unintentional binary size increases unintentional public API changes For example, a long-standing crash just recently discovered would not necessitate a bug fix PR.","breadcrumbs":"Technical Notes » Versioning » Branching » What consitutes a bug?","id":"107","title":"What consitutes a bug?"},"108":{"body":"This guide makes use of some \"environment variables\": $RELEASE = the new version of Filament we are releasing today. (e.g., 1.9.3) $NEXT_RELEASE = the version we plan to release next week (e.g., 1.9.4) Before starting, ensure that each of these branches is up-to-date with origin: release rc/$RELEASE main","breadcrumbs":"Technical Notes » Versioning » Release Guide » Filament Release Guide","id":"108","title":"Filament Release Guide"},"109":{"body":"Make sure the rc/$RELEASE branch has the correct Filament version. It should have the version corresponding to its name, $RELEASE. Make sure MATERIAL_VERSION has been bumped to a new version if this is a MAJOR or MINOR release (first two version numbers).","breadcrumbs":"Technical Notes » Versioning » Release Guide » 0. Check versions.","id":"109","title":"0. Check versions."},"11":{"body":"Encodings Embeded Binary Primitive Types Points Lines Line Loop Line Strip Triangles Triangle Strip Triangle Fan Animation Transform animation Linear interpolation Morph animation Sparse accessor Skin animation Joint animation Extensions KHR_draco_mesh_compression KHR_lights_punctual KHR_materials_clearcoat KHR_materials_emissive_strength KHR_materials_ior KHR_materials_pbrSpecularGlossiness KHR_materials_sheen KHR_materials_transmission KHR_materials_unlit KHR_materials_variants KHR_materials_volume KHR_materials_specular KHR_mesh_quantization KHR_texture_basisu KHR_texture_transform EXT_meshopt_compression","breadcrumbs":"Introduction » glTF 2.0","id":"11","title":"glTF 2.0"},"110":{"body":"Checkout main and run the following command to bump Filament's version to $RELEASE: build/common/bump-version.sh $RELEASE Commit changes to main with the title: Release Filament $RELEASE Do not push to origin yet.","breadcrumbs":"Technical Notes » Versioning » Release Guide » 1. Bump Filament versions on main to $RELEASE.","id":"110","title":"1. Bump Filament versions on main to $RELEASE."},"111":{"body":"Create a new header in RELEASE_NOTES.md for $NEXT_RELEASE. Copy the release notes in NEW_RELEASE_NOTES.md to RELEASE_NOTES.md under the new header. Clear NEW_RELEASE_NOTES.md. Amend these changes to the \"Release Filament $RELEASE\" commit. git add -u\ngit commit --amend --no-edit","breadcrumbs":"Technical Notes » Versioning » Release Guide » 2. Update RELEASE_NOTES.md on main.","id":"111","title":"2. Update RELEASE_NOTES.md on main."},"112":{"body":"build/common/release.sh rc/$RELEASE rc/$NEXT_RELEASE This script will merge rc/$RELEASE into release, delete the rc branch, and create a new rc branch called rc/$NEXT_RELEASE. Verify that everything looks okay locally.","breadcrumbs":"Technical Notes » Versioning » Release Guide » 3. Run release script.","id":"112","title":"3. Run release script."},"113":{"body":"git push origin release","breadcrumbs":"Technical Notes » Versioning » Release Guide » 4. Push the release branch.","id":"113","title":"4. Push the release branch."},"114":{"body":"Use the GitHub UI to create a GitHub release corresponding to $RELEASE version. Make sure the target is set to the release branch.","breadcrumbs":"Technical Notes » Versioning » Release Guide » 5. Create the GitHub release.","id":"114","title":"5. Create the GitHub release."},"115":{"body":"This step is optional. The old rc branch may be left alive for a few weeks for posterity. git push origin --delete rc/$RELEASE","breadcrumbs":"Technical Notes » Versioning » Release Guide » 6. Delete the old rc branch (optional).","id":"115","title":"6. Delete the old rc branch (optional)."},"116":{"body":"git checkout rc/$NEXT_RELEASE\nbuild/common/bump-version.sh $NEXT_RELEASE Commit the changes to rc/$NEXT_RELEASE with the title: Bump version to $NEXT_RELEASE","breadcrumbs":"Technical Notes » Versioning » Release Guide » 7. Bump the version on the new rc branch to $NEXT_RELEASE.","id":"116","title":"7. Bump the version on the new rc branch to $NEXT_RELEASE."},"117":{"body":"git push origin main","breadcrumbs":"Technical Notes » Versioning » Release Guide » 8. Push main.","id":"117","title":"8. Push main."},"118":{"body":"git push origin -u rc/$NEXT_RELEASE","breadcrumbs":"Technical Notes » Versioning » Release Guide » 9. Push the new rc branch.","id":"118","title":"9. Push the new rc branch."},"119":{"body":"Sometimes the GitHub release job will fail. In this case, you can manually re-run the release job.","breadcrumbs":"Technical Notes » Versioning » Release Guide » 10. Rebuild the GitHub release (if failed).","id":"119","title":"10. Rebuild the GitHub release (if failed)."},"12":{"body":"","breadcrumbs":"Introduction » Rendering with Filament","id":"12","title":"Rendering with Filament"},"120":{"body":"For example, if rebuilding the Mac release, ensure that the filament--mac.tgz artifact is removed from the release assets.","breadcrumbs":"Technical Notes » Versioning » Release Guide » Remove any assets uploaded to the release (if needed).","id":"120","title":"Remove any assets uploaded to the release (if needed)."},"121":{"body":"If you need to add one or more new commits to the release, perform the following: First, push the new commit(s) to the release branch. Then, with the release branch checked out with the new commit(s), run git tag -f -a \ngit push origin -f This will update and force push the tag.","breadcrumbs":"Technical Notes » Versioning » Release Guide » Update the release branch (if needed).","id":"121","title":"Update the release branch (if needed)."},"122":{"body":"Navigate to Filament's release workflow . Hit the Run workflow dropdown. Modify Platform to build and Release tag to build , then hit Run workflow . This will initiate a new release run.","breadcrumbs":"Technical Notes » Versioning » Release Guide » Re-run the GitHub release workflow","id":"122","title":"Re-run the GitHub release workflow"},"123":{"body":"Navigate to Filament's npm deploy workflow . Hit the Run workflow dropdown. Modify Release tag to deploy to the tag corresponding to this release (for example, v1.42.2). Navigate to Filament's CocoaPods deploy workflow . Hit the Run workflow dropdown. Modify Release tag to deploy to the tag corresponding to this release (for example, v1.42.2).","breadcrumbs":"Technical Notes » Versioning » Release Guide » 11. Kick off the npm and CocoaPods release jobs","id":"123","title":"11. Kick off the npm and CocoaPods release jobs"},"124":{"body":"Filament's documentation (which you are reading) is a collection of pages created with mdBook .","breadcrumbs":"Technical Notes » Documentation » Documentation","id":"124","title":"Documentation"},"125":{"body":"","breadcrumbs":"Technical Notes » Documentation » How the book is created and updated","id":"125","title":"How the book is created and updated"},"126":{"body":"Install mdBook for your platform There is a script docs_src/build/install_mdbook.sh that might help. It is best practice to install the python dependencies in a virtual python environment. You can start such an environment by python3 -m venv venv\n. venv/bin/activate After that you may install deps and run the build script. selenium package for python python3 -m pip install selenium","breadcrumbs":"Technical Notes » Documentation » Prerequisites","id":"126","title":"Prerequisites"},"127":{"body":"We wrote a python script to gather and transform the different documents in the project tree into a single book. This script can be found in docs_src/build/run.py . In addition, docs_src/build/duplicates.json is used to describe the markdown files that are copied and transformed from the source tree. These copies are placed into docs_src/src_mdbook/src/dup. To collect the pages and generate the book, run the following cd docs_src\npython3 build/run.py","breadcrumbs":"Technical Notes » Documentation » Generate","id":"127","title":"Generate"},"128":{"body":"docs is the github-specfic directory for producing a web frontend (i.e. documentation) for a project. (To be completed)","breadcrumbs":"Technical Notes » Documentation » Copy to docs","id":"128","title":"Copy to docs"},"129":{"body":"We list the different document sources and how they are copied and processed into the collection of markdown files that are then processed with mdBook.","breadcrumbs":"Technical Notes » Documentation » Document sources","id":"129","title":"Document sources"},"13":{"body":"You must create an Engine, a Renderer and a SwapChain. The SwapChain is created from a native window pointer (an NSView on macOS or a HWND on Windows for instance): Engine* engine = Engine::create();\nSwapChain* swapChain = engine->createSwapChain(nativeWindow);\nRenderer* renderer = engine->createRenderer(); To render a frame you must then create a View, a Scene and a Camera: Camera* camera = engine->createCamera(EntityManager::get().create());\nView* view = engine->createView();\nScene* scene = engine->createScene(); view->setCamera(camera);\nview->setScene(scene); Renderables are added to the scene: Entity renderable = EntityManager::get().create();\n// build a quad\nRenderableManager::Builder(1) .boundingBox({{ -1, -1, -1 }, { 1, 1, 1 }}) .material(0, materialInstance) .geometry(0, RenderableManager::PrimitiveType::TRIANGLES, vertexBuffer, indexBuffer, 0, 6) .culling(false) .build(*engine, renderable);\nscene->addEntity(renderable); The material instance is obtained from a material, itself loaded from a binary blob generated by matc: Material* material = Material::Builder() .package((void*) BAKED_MATERIAL_PACKAGE, sizeof(BAKED_MATERIAL_PACKAGE)) .build(*engine);\nMaterialInstance* materialInstance = material->createInstance(); To learn more about materials and matc, please refer to the materials documentation . To render, simply pass the View to the Renderer: // beginFrame() returns false if we need to skip a frame\nif (renderer->beginFrame(swapChain)) { // for each View renderer->render(view); renderer->endFrame();\n} For complete examples of Linux, macOS and Windows Filament applications, look at the source files in the samples/ directory. These samples are all based on libs/filamentapp/ which contains the code that creates a native window with SDL2 and initializes the Filament engine, renderer and views. For more information on how to prepare environment maps for image-based lighting please refer to BUILDING.md .","breadcrumbs":"Introduction » Native Linux, macOS and Windows","id":"13","title":"Native Linux, macOS and Windows"},"130":{"body":"The github landing page for Filament displays an extensive introduction to Filament. It links to BUILDING.md and CONTRIBUTING.md, which are conventional pages for building or contributing to the project. We copy these pages from their respective locations in the project tree into docs_src/src_mdbook/src/dup. Moreover, to restore valid linkage between the pages, we need to perform a number of URL replacements in addition to the copy. These replacements are described in docs_src/build/duplicates.json .","breadcrumbs":"Technical Notes » Documentation » Introductory docs","id":"130","title":"Introductory docs"},"131":{"body":"The primary design of Filament as a phyiscally-based renderer and details of its materials system are described in Filament.md.html and Materials.md.html, respectively. These two documents are written in markdeep . To embed them into our book, we Convert the markdeep into html Embed the html output in a markdown file Place the markdown file in docs_src/src_mdbook/src/main We describe step 1 in detail for the sake of record: Start a local-only server to serve the markdeep file (e.g. Filament.md.html) Start a selenium driver (essentially run chromium in headless mode) Visit the local page through the driver (i.e. open url http://localhost:xx/Filament.md.html?export) Parse out the exported output in the retrieved html (note that the output of the markdeep export is an html with the output captured in a
 tag). Replace css styling in the exported output as needed (so they don't interfere with the book's css. Replace resource urls to refer to locations relative to the mdbook structure. Any markdeep doc can be placed in docs_src/src_markdeep/ and they will be parsed to html and included in the book as above.","breadcrumbs":"Technical Notes » Documentation » Core concept docs","id":"131","title":"Core concept docs"},"132":{"body":"Filament depends on a number of libraries, which reside in the directory libs. These individual libaries often have README.md in their root to describe itself. We collect these descriptions into our book. In addition, client usage of Filament also requires using a set of binary tools, which are located in tools. Some of tools also have README.md as description. We also collect them into the book. The process for copying and processing these READMEs is outlined in Introductory docs .","breadcrumbs":"Technical Notes » Documentation » READMEs","id":"132","title":"READMEs"},"133":{"body":"These are technical documents that do not fit into a library, tool, or directory of the Filament source tree. We collect them into the docs_src/src_mdbook/src/notes directory. No additional processing is needed for these documents.","breadcrumbs":"Technical Notes » Documentation » Other technical notes","id":"133","title":"Other technical notes"},"134":{"body":"These are files that are not part of the mdbook generation, but should be included output in /docs to point to standalone pages or components (for example, the remote page for Android's gltf_viewer). These files are stored in docs_src/src_raw.","breadcrumbs":"Technical Notes » Documentation » Raw source files","id":"134","title":"Raw source files"},"135":{"body":"To add any documentation, first consider the type of the document you like to add. If it belongs to any of the above sources, then simply place the document in the appropriate place, add a link in SUMMARY.md, and perform the steps outlined in how-to create section . For example, if you are adding a general technical note, then you would Place the document (file with extension .md) in docs_src/src_mdbook/src/notes Add a link in docs_src/src_mdbook/src/SUMMARY.md Run the commands in the Generate section","breadcrumbs":"Technical Notes » Documentation » Adding more documents","id":"135","title":"Adding more documents"},"136":{"body":"Helpful documents for specific debugging needs.","breadcrumbs":"Technical Notes » Debugging » Debugging","id":"136","title":"Debugging"},"137":{"body":"","breadcrumbs":"Technical Notes » Debugging » Metal » Debugging Metal","id":"137","title":"Debugging Metal"},"138":{"body":"To enable the Metal validation layers when running a sample through the command-line, set the following environment variable: export METAL_DEVICE_WRAPPER_TYPE=1 You should then see the following output when running a sample with the Metal backend: 2020-10-13 18:01:44.101 gltf_viewer[73303:4946828] Metal API Validation Enabled","breadcrumbs":"Technical Notes » Debugging » Metal » Enable Metal Validation","id":"138","title":"Enable Metal Validation"},"139":{"body":"To capture Metal frames from within gltf_viewer:","breadcrumbs":"Technical Notes » Debugging » Metal » Metal Frame Capture from gltf_viewer","id":"139","title":"Metal Frame Capture from gltf_viewer"},"14":{"body":"See android/samples for examples of how to use Filament on Android. You must always first initialize Filament by calling Filament.init(). Rendering with Filament on Android is similar to rendering from native code (the APIs are largely the same across languages). You can render into a Surface by passing a Surface to the createSwapChain method. This allows you to render to a SurfaceTexture, a TextureView or a SurfaceView. To make things easier we provide an Android specific API called UiHelper in the package com.google.android.filament.android. All you need to do is set a render callback on the helper and attach your SurfaceView or TextureView to it. You are still responsible for creating the swap chain in the onNativeWindowChanged() callback.","breadcrumbs":"Introduction » Android","id":"14","title":"Android"},"140":{"body":"Create an Info.plist file in the same directory as gltf_viewer (cmake/samples). Set its contents to: \n\n\n MetalCaptureEnabled \n\n","breadcrumbs":"Technical Notes » Debugging » Metal » 1. Create an Info.plist file","id":"140","title":"1. Create an Info.plist file"},"141":{"body":"Run gltf_viewer as normal, and hit the \"Capture frame\" button under the Debug menu. The captured frame will be saved to filament.gputrace in the current working directory. This file can then be opened with Xcode for inspection.","breadcrumbs":"Technical Notes » Debugging » Metal » 2. Capture a frame","id":"141","title":"2. Capture a frame"},"142":{"body":"","breadcrumbs":"Technical Notes » Debugging » Vulkan » Debugging Vulkan","id":"142","title":"Debugging Vulkan"},"143":{"body":"Simply install the LunarG SDK (it's fast and easy), then make sure you've got the following environment variables set up in your bashrc file. For example: export VULKAN_SDK='/path_to_home/VulkanSDK/1.3.216.0/x86_64'\nexport VK_LAYER_PATH=\"$VULKAN_SDK/etc/explicit_layer.d\"\nexport PATH=\"$VULKAN_SDK/bin:$PATH\" As long as you're running a debug build of Filament, you should now see extra debugging spew in your console if there are any errors or performance issues being caught by validation.","breadcrumbs":"Technical Notes » Debugging » Vulkan » Enable Validation Logs","id":"143","title":"Enable Validation Logs"},"144":{"body":"There are 4 repositories at play here: KhronosGroup/glslang KhronosGroup/spirv-tools KhronosGroup/spirv-cross KhronosGroup/SPIRV-Headers Typically, the bug is present either in spirv-tools or spirv-cross.","breadcrumbs":"Technical Notes » Debugging » SPIR-V » Investigating SPIRV-Cross / SPIRV-Tools issues","id":"144","title":"Investigating SPIRV-Cross / SPIRV-Tools issues"},"145":{"body":"The goal is to replicate the bug outside of Filament, so we're going to use command-line versions of the SPIRV tools.","breadcrumbs":"Technical Notes » Debugging » SPIR-V » Build and install command-line tools on PATH","id":"145","title":"Build and install command-line tools on PATH"},"146":{"body":"Note: Filament checks-out versions of these repositories inside third_party/; however, I've found it easiser to check out fresh copies separately so I can simply git pull to get the latest versions. Furthermore, Filament has modified some of these repositories locally for its own use case. Checking them out separately \"proves\" that the issue isn't Filament-specific. git clone git@github.com:KhronosGroup/SPIRV-Tools.git\ngit clone git@github.com:KhronosGroup/SPIRV-Cross.git\ngit clone git@github.com:KhronosGroup/glslang.git\ngit clone git@github.com:KhronosGroup/SPIRV-Headers.git SPIRV-Tools/external/SPIRV-Headers cd SPIRV-Tools/\nmkdir build && cmake . -G Ninja -B build\nninja -C build\ncd .. cd SPIRV-Cross/\nmkdir build && cmake . -G Ninja -B build\nninja -C build\ncd .. cd glslang/\nmkdir build && cmake . -G Ninja -B build\nninja -C build\ncd ..","breadcrumbs":"Technical Notes » Debugging » SPIR-V » Clone and build each repo","id":"146","title":"Clone and build each repo"},"147":{"body":"export PATH=`pwd`/SPIRV-Tools/build/tools:$PATH\nexport PATH=`pwd`/glslang/build/StandAlone:$PATH\nexport PATH=`pwd`/spirv-cross/build:$PATH Ensure the following tools now exist on your PATH: glslangValidator spiv-opt spirv-val spirv-cross","breadcrumbs":"Technical Notes » Debugging » SPIR-V » Add directories to PATH","id":"147","title":"Add directories to PATH"},"148":{"body":"First determine the Filament material and variant that causes the problem. What we want is the \"raw\" GLSL version of the shader, before any optimizations / cross-compilation happens. We can use the --save-raw-variants debug flag in matc to export each GLSL shader to a file. For example: matc --save-raw-variants --optimize-size --variant-filter fog,ssr,vsm,stereo \\ -a all -p all -o mymaterial.filamat mymaterial.mat Files will be named like mymaterial_0x05.frag or mymaterial_0x05.vert. Note that gltfio material \"templates\" first go through a build step. After building gltfio, the gltfio Filament materials are output to: out/cmake-release/libs/gltfio/*.mat One of these materials can be compiled with the following command: matc \\ -TCUSTOM_PARAMS=\"// no custom params\" \\ -TCUSTOM_VERTEX=\"// no custom vertex\" \\ -TCUSTOM_FRAGMENT=\"// no custom fragment\" \\ -TDOUBLESIDED=false \\ -TTRANSPARENCY=default \\ -TSHADINGMODEL=unlit \\ -TBLENDING=opaque \\ --platform mobile --api metal -o temp.filamat \\ unlit_opaque.mat","breadcrumbs":"Technical Notes » Debugging » SPIR-V » Isolate the problematic GLSL shader","id":"148","title":"Isolate the problematic GLSL shader"},"149":{"body":"The goal is to generate a .spv file that doesn't pass validation (through the spirv-val tool). Reproducing the error usually involves a few steps: Compile the raw GLSL shader into SPIR-V. glslangValidator -V -o unoptimized.spv in.frag Optimize for performance. spirv-opt -Oconfig=optimizations.cfg unoptimized.spv -o optimized.spv See optimizations.cfg for a template. This file should contain the same list of optimizations that Filament employs. This should match the same optimizations specified in GLSLPostProcessor, for example, GLSLPostProcessor::registerPerformancePasses or GLSLPostProcessor::registerSizePasses. For shaders targeting Metal, convert relaxed ops to half. spirv-opt \\ --convert-relaxed-to-half \\ --simplify-instructions \\ --redundancy-elimination \\ --eliminate-dead-code-aggressive \\ optimized.spv \\ -o half.spv Finally, validate the final SPIR-V. spirv-val half.spv Sometimes validation will still pass, but still generate invalid shaders after cross-compiling. In these cases, you'll need to cross compile to the target language and manually pick out errors in the generated shader. # for OpenGL\nspirv-cross optimized.spv > optimized.frag # for OpenGL ES\nspirv-cross --es optimized.spv > optimized.frag # for MSL\nspirv-cross --msl optimized.spv > optimized.metal To invoke Apple's compiler to compile MSL, you can run: xcrun -sdk macosx metal -c optimized.metal -o /dev/null","breadcrumbs":"Technical Notes » Debugging » SPIR-V » Reproduce the compilation error","id":"149","title":"Reproduce the compilation error"},"15":{"body":"Filament is supported on iOS 11.0 and above. See ios/samples for examples of using Filament on iOS. Filament on iOS is largely the same as native rendering with C++. A CAEAGLLayer or CAMetalLayer is passed to the createSwapChain method. Filament for iOS supports both Metal (preferred) and OpenGL ES.","breadcrumbs":"Introduction » iOS","id":"15","title":"iOS"},"150":{"body":"These commands will run the preprocessor only on in.frag, and remove any empty lines. glslangValidator -E in.frag > preprocessed.frag\nsed '/^$/d' preprocessed.frag > preprocessed_small.frag You can also run clang-format on the preprocessed shader to make it easier to read: clang-format -i preprocessed_small.frag I always try to \"whittle down\" the shader to a smaller version that still reproduces the error. This might make it a bit easier on the Khronos team to diagnose the issue. I typically follow these steps in a loop until I'm satisfied: Delete an unnecessary part of the shader Run the steps to reproduce the error If the error still reproduces, repeat Otherwise, undo the change and make a smaller change There's also a Reducer tool that's part of SPIRV-Tools which can be used to automate these steps. I haven't experimented much with this, but it seems promising.","breadcrumbs":"Technical Notes » Debugging » SPIR-V » Clean up the shader for a bug report","id":"150","title":"Clean up the shader for a bug report"},"151":{"body":"See some example issues that have been filed in the past: https://github.com/KhronosGroup/SPIRV-Cross/issues/1935 https://github.com/KhronosGroup/SPIRV-Cross/issues/1088 https://github.com/KhronosGroup/SPIRV-Cross/issues/1026 https://github.com/KhronosGroup/SPIRV-Tools/issues/4452 https://github.com/KhronosGroup/SPIRV-Tools/issues/3406 https://github.com/KhronosGroup/SPIRV-Tools/issues/3099 https://github.com/KhronosGroup/SPIRV-Tools/issues/5044","breadcrumbs":"Technical Notes » Debugging » SPIR-V » Submit an Issue with the relevant Khronos repository","id":"151","title":"Submit an Issue with the relevant Khronos repository"},"152":{"body":"","breadcrumbs":"Technical Notes » Debugging » Running with ASAN and UBSAN » Running with ASAN/UBSAN","id":"152","title":"Running with ASAN/UBSAN"},"153":{"body":"When building though build.sh, pass the -b flag. This sets the cmake variable FILAMENT_ENABLE_ASAN_UBSAN=ON which eventually passes \"-fsanitize=address -fsanitize=undefined\" to all compile and link operations. If building through CMake directly, or an IDE like CLion that doesn't use build.sh, instead pass -DFILAMENT_ENABLE_ASAN_UBSAN=ON to cmake in order to get the same result.","breadcrumbs":"Technical Notes » Debugging » Running with ASAN and UBSAN » Enabling","id":"153","title":"Enabling"},"154":{"body":"Memory leak detection isn't enabled by default on MacOS. There are two issues to address, first is using a version of clang that supports memory leak detection and second is enabling it at runtime. The version of clang distributed by Apple (with a version like \"Apple clang version 16.0.0\") doesn't currently support leak detection at all. Instead you will need to get or build a different LLVM, such as the one distributed through homebrew and get CMake to use that instead. Then during runtime you'll need to have the environment variable ASAN_OPTIONS include the option detect_leaks=1. Multiple ASAN_OPTIONS values are concatenated with :.","breadcrumbs":"Technical Notes » Debugging » Running with ASAN and UBSAN » Getting memory leak detection on Mac","id":"154","title":"Getting memory leak detection on Mac"},"155":{"body":"","breadcrumbs":"Technical Notes » Debugging » Running with ASAN and UBSAN » Getting memory leak output in CLion","id":"155","title":"Getting memory leak output in CLion"},"156":{"body":"Under Settings | Build, Execution, Deployment | Dynamic Analysis Tools | Sanitizers there is an ASAN Settings field that overrides whatever other ASAN_OPTIONS you might set elsewhere, so you must use that instead of setting it through your Run/Debug Configuration. To pass -DFILAMENT_ENABLE_ASAN_UBSAN=ON to CMake you'll want to create a new CMake Profile and pass it as a CMake argument.","breadcrumbs":"Technical Notes » Debugging » Running with ASAN and UBSAN » Setting variables","id":"156","title":"Setting variables"},"157":{"body":"CMake will consume ASAN output and display it through a separate \"Sanitizers\" tab. Unfortunately certain leak detection errors that interrupt the executable seem to not show up in this tab, but are still removed from the user-visible console output. If this is happening and you need to see the unfiltered console output you'll need to go to Settings | Build, Execution, Deployment | Dynamic Analysis Tools | Sanitizers and uncheck \"Use visual representation for Sanitizer's output\".","breadcrumbs":"Technical Notes » Debugging » Running with ASAN and UBSAN » Avoiding losing output","id":"157","title":"Avoiding losing output"},"158":{"body":"When running a binary under Instruments on macOS, you may run into the following issue when launching or attaching to an executable: Failed to gain authorization\nRecovery Suggestion: Target binary needs to be debuggable and signed with 'get-task-allow' This is a security precaution; the solution is to code sign the binary with the com.apple.security.get-task-allow entitlement. Create an entitlements.plist file with the following contents: \n\n\n com.apple.security.get-task-allow \n\n Run the following command: codesign -s - --entitlements entitlements.plist  Replace  with the name of the binary, for example: out/cmake-debug/samples/gltf_viewer. Afterwards, you should be able to successfully launch and attach to the executable using Instruments.","breadcrumbs":"Technical Notes » Debugging » Using Instruments on macOS » Using Instruments on macOS","id":"158","title":"Using Instruments on macOS"},"159":{"body":"Code coverage analysis helps visualize which parts of the backend are exercised by backend tests. This guide outlines the process for generating an HTML coverage report for Filament's backend on macOS.","breadcrumbs":"Technical Notes » Debugging » Code coverage analysis » Generating Backend Code Coverage","id":"159","title":"Generating Backend Code Coverage"},"16":{"body":"To get started you can use the textures and environment maps found respectively in third_party/textures and third_party/environments. These assets are under CC0 license. Please refer to their respective URL.txt files to know more about the original authors. Environments must be pre-processed using cmgen or using the libiblprefilter library.","breadcrumbs":"Introduction » Assets","id":"16","title":"Assets"},"160":{"body":"You'll need a recent version of Clang and its corresponding LLVM tools for code coverage. You can install these using Homebrew or MacPorts.","breadcrumbs":"Technical Notes » Debugging » Code coverage analysis » 1. Prerequisites: Install Clang and LLVM tools","id":"160","title":"1. Prerequisites: Install Clang and LLVM tools"},"161":{"body":"Install the llvm package: brew install llvm This typically installs the tools in a location like /usr/local/opt/llvm/bin. You may need to add this to your PATH environment variable.","breadcrumbs":"Technical Notes » Debugging » Code coverage analysis » Using Homebrew","id":"161","title":"Using Homebrew"},"162":{"body":"Install a specific version of Clang (e.g., version 18): sudo port install clang-18 MacPorts often adds version suffixes to the tool names (e.g., llvm-cov-mp-18).","breadcrumbs":"Technical Notes » Debugging » Code coverage analysis » Using MacPorts","id":"162","title":"Using MacPorts"},"163":{"body":"Ensure you can locate the following tools from your installation: clang and clang++ (The C/C++ compilers) llvm-profdata (For merging coverage data) llvm-cov (For generating reports) The rest of this guide assumes your tools are in your PATH. If not, you'll need to use the full path to each executable.","breadcrumbs":"Technical Notes » Debugging » Code coverage analysis » Required Tools","id":"163","title":"Required Tools"},"164":{"body":"Compile the backend_test_mac target with coverage instrumentation. This is done by setting the CC and CXX environment variables to point to your Clang compiler and using the -V flag in the build script. CC=clang CXX=clang++ ./build.sh -V -p desktop debug backend_test_mac If your Clang executables aren't in your PATH or have version suffixes, provide the full name or path (e.g., CC=/opt/local/bin/clang CXX=/opt/local/bin/clang++).","breadcrumbs":"Technical Notes » Debugging » Code coverage analysis » 2. Build Filament with Coverage Enabled","id":"164","title":"2. Build Filament with Coverage Enabled"},"165":{"body":"Running the test suite will generate the raw coverage data needed for the report. Navigate to the build output directory: cd out/cmake-debug/filament/backend Run the tests for a specific backend (e.g., Metal): ./backend_test_mac --api metal This command creates a default.profraw file in the current directory, which contains the raw execution profile data.","breadcrumbs":"Technical Notes » Debugging » Code coverage analysis » 3. Run the Backend Tests","id":"165","title":"3. Run the Backend Tests"},"166":{"body":"Finally, process the raw data and generate an HTML report. Merge the raw profile data into a single file using llvm-profdata. llvm-profdata merge -sparse default.profraw -o filament.profdata Remember to use the version-specific tool name if required (e.g., llvm-profdata-mp-18). Generate the HTML report using llvm-cov. This command creates a report for the entire backend_test_mac executable. llvm-cov show ./backend_test_mac \\ -instr-profile=filament.profdata \\ -format=html \\ -show-line-counts-or-regions > coverage.html To view coverage for a specific source file , add its path at the end of the command: llvm-cov show ./backend_test_mac \\ -instr-profile=filament.profdata \\ -format=html \\ -show-line-counts-or-regions \\ -- ../../../../filament/backend/src/metal/MetalDriver.mm > coverage.html Open the report in your browser: open coverage.html In the report, code paths that were not executed during the test run will be highlighted in red.","breadcrumbs":"Technical Notes » Debugging » Code coverage analysis » 4. Generate the Coverage Report","id":"166","title":"4. Generate the Coverage Report"},"167":{"body":"","breadcrumbs":"Technical Notes » Debugging » Performance analysis » Performance Analysis","id":"167","title":"Performance Analysis"},"168":{"body":"","breadcrumbs":"Technical Notes » Debugging » Performance analysis » Android","id":"168","title":"Android"},"169":{"body":"Download and install Android GPU Inspector (AGI). See https://developer.android.com/agi. Profiling Before profiling the application or analyzing the performance in a consistent way, ideally the GPU frequency on the target hardware should get locked . In order to do this, you need to do the following: Ensure your device is OEM unlocked. If your phone is carrier-locked, you may need to wait a period, such as 60 or 90 days after activation to be eligible for unlocking. Some phones don't support this at all. You will need to enable developer options (e.g. Settings > About Phone and tap the Build number 7 times). You need to go to Settings > System > Developer options and toggle on \"OEM unlocking\". Next, you need to unlock the phone. You will need to install Android SDK Platform Tools on you computer, enable \"USB debugging\" in your phone's Settings > System > Developer options. Connect the phone to the computer via a USB cable and run from the command line: adb reboot bootloader\n# once the phone is in bootloader mode, run the following to begin the unlocking\n# process:\nfastboot flashing unlock A warning will appear on your phone's screen. Use the volume buttons to navigate and the power button to select the \"Unlock the bootloader\" option. The phone will perform a factory data reset and reboot with an unlocked bootloader. You would need to flash an image to the phone with root permissions, such as a *-userdebug or *-eng build. One way to do this is with the Android Flash Tool . Connect the tool to your device and find a build to flash ending in -userdebug or -eng. Once you have that selected run Install build. Shell into your device as root and configure the gpu frequency to be locked, e.g.: adb shell\nsu\n# navigate to the system GPU directory. this varies on different phones. One phone might have\n# it at /sys/class/kgsl/kgsl-3d0 and another might be in something similar, maybe with \"mali\" instead\n# of \"kgsl\". At the time of writing this for the device at hand it was /sys/devices/platform/1f000000.mali\ncd /sys/devices/platform/1f000000.mali\n# get the current available GPU governors and frequencies.\n# note that some systems may have these at gpu_available_governors and gpu_available_frequencies, but\n# the system at the time of writing this had available_governors and available_frequences\ncat available_governors\ncat available_frequencies\n# depending on the governors, you may want to set it prioritize performance over other things like\n# battery. Some systems allow you to do this with something like (although, for the device used at the\n# time of writing this did not have an equivalent option):\necho performance > gpu_governor\n# finally, lock your frequency in, usually to something high like 897 MHz. Some systems may have you\n# pipe the value to gpu_min_freq and gpu_max_freq, but the system used at the time of writing this had:\necho 940000 > hint_min_freq\necho 940000 > hint_max_freq\n# you can typically verify the GPU is running at that frequency consistently by running something like\n# the following a few times over time, which should show the frequency you want to lock the device to:\ncat cur_freq You may need to re-apply the hint_min_freq just before starting the profiling trace and check before and after that the frequency remained at the value expected. Some systems may adjust the frequency on you, but you may want to ensure the frequency remains the same through the analysis. The GPU frequency settings should be undone after restarting the device, but after you have done your app profiling, you can revert the state of the device, such as the OS build image, back to the way you had it initially, as needed. Build a release build of Filament with the applicable backend(s) enabled (+ any special flags for enabling sys strace. Nothing special is needed for Vulkan or WebGPU aside from building a release build with no flags) (debug builds for this are useless) # the following command assumes you are in the root filament directory\n# and ANDROID_HOME is exported (and possibly also CC and CXX on linux as needed)\n#\n# NOTE: to build with WebGPU support you need to explicitly include the -W flag\n# (it doesn't get compiled in by default), e.g.:\n# ./build.sh -W -p android,desktop -i release\n#\n# Note that you can speed this up a bit (and reduce disk space usage) by limiting the target to just # the ABI you plan on testing with the -q flag, e.g. -q arm64-v8a. If you do this, you\n# will need to update the android/gradle.properties file to specify the ABI(s) you are targeting\n# with the com.google.android.filament.abis property.\n# Thus, a build command that would target BOTH Vulkan AND WebGPU AND only target the ARM64 ABI would look something\n# like:\n# ./build.sh -W -q arm64-v8a -p android,desktop -i release\n./build.sh -p android,desktop -i release Connect your Android device to your computer via a USB cord with USB debugging enabled and configure the system property to default to the desired backend, e.g. (to determine how these numbers map to the backends, see the enum class Backend definition in filament/filament/backend/include/backend/DriverEnums.h ) : # to set the backend to Vulkan: adb shell setprop debug.filament.backend 2\n# to set the backend to WebGPU:\nadb shell setprop debug.filament.backend 4\n# to view the current property:\nadb shell getprop debug.filament.backend Build and run a sample, e.g. sample-gltf-viewer, on your Android device ( Android Studio recommended) . Run AGI and follow instructions to profile the app/system with trace capture(s). See https://developer.android.com/agi/start for more details to get started. We typically only run \"Capture System Profiler trace\" (not necessarily \"Capture Frame Profiler trace\") When configuring the trace: For both the WebGPU and Vulkan backends configure the profiler for the Vulkan API (since WebGPU should be using Vulkan under the hood as well) Running for ~1 seconds should suffice Hit the \"Configure\" button in Trace objects, select \"Switch to advanced mode\" and add: data_sources { config { name: \"track_event\" track_event_config { disabled_categories: \"*\" enabled_categories: \"filament/filament\" enabled_categories: \"filament/jobsystem\" enabled_categories: \"filament/gltfio\" } }\n} One you open the trace, zoom into a series of frames to get a sense of generally how long they typically take (use W, S, A and D keys and mouse wheel for navigation) and find a representative one. We are most interested in the performance of the FEngine::loop thread, how long it takes, overlap in activities/processes/commands, reduction in queue submissions, etc. Similarly, we can view GPU timeline as it relates to that. We want to see overlapping shader invocations and non-interrupted fragment shader runs.","breadcrumbs":"Technical Notes » Debugging » Performance analysis » Prerequisites","id":"169","title":"Prerequisites"},"17":{"body":"Please read and follow the steps in CONTRIBUTING.md . Make sure you are familiar with the code style .","breadcrumbs":"Introduction » How to make contributions","id":"17","title":"How to make contributions"},"170":{"body":"FrameGraph is a framework within Filament for computing resources needed to render a frame. The framework enables declaring dependencies between resources. For example, when rendering shadows, we would need to first compute and store the shadow map into a texture resource, and then the later color pass would then sample that texture to attenuate the final output color. That creates a dependency on the shadow map from the color pass. Filament uses FrameGraph to declare that dependency.","breadcrumbs":"Technical Notes » Framegraph » FrameGraph","id":"170","title":"FrameGraph"},"171":{"body":"","breadcrumbs":"Technical Notes » Framegraph » Details","id":"171","title":"Details"},"172":{"body":"The core of this framework is a class that defines a dependency graph — that is, the class defines nodes and connections between nodes. This class makes assumptions about the types of its nodes. Like many other classes within Filament, this class is without virtual function declaration to avoid paying the cost of virtual calls. This class has additional functions to detect whether there is a cycle in the graph, and it is able to cull unreachable nodes.","breadcrumbs":"Technical Notes » Framegraph » Dependency Graph","id":"172","title":"Dependency Graph"},"173":{"body":"A frame graph consists of two types of nodes Resource This represents a generic resource such as a texture 90% of the time, this is a texture. Pass This represents a \"computation/rendering process\" It takes a set of resources It outputs a set of resources Edges can be created in the following three directions: Resource → Pass = A read Pass → Resource = A write Resource → Resource = A resource/subresource relationship.","breadcrumbs":"Technical Notes » Framegraph » FrameGraph","id":"173","title":"FrameGraph"},"174":{"body":"To better understand FrameGraph, we consider the following graphical representation of a real graph. In this graph, blue nodes denote \"Resources\" and orange nodes denote \"Passes.\" Sample frame graph In this graph, we see that the \"Color Pass\" takes as input the \"Shadowmap\", which has edges going into it, meaning that it's a texture array. The output of the \"Color Pass\" are \"viewRenderTarget\" and \"Depth Buffer.\" Note that there is an outgoing edge from \"viewRenderTarget\", where the color buffer will be used as input in the post-processing passes. But since \"Depth Buffer\" is not relevant to the rest of the rendering, it does not have an outgoing edge. Since the graph is guaranteed to be acyclic, we can produce a dependency-respecting ordering of the nodes by traversal of the graph (e.g. topological sort).","breadcrumbs":"Technical Notes » Framegraph » An example","id":"174","title":"An example"},"175":{"body":"We take a snippet of in production code to look through the details of building a graph. struct StructurePassData { FrameGraphId depth; FrameGraphId picking;\n}; ... // generate depth pass at the requested resolution\nauto& structurePass = fg.addPass(\"Structure Pass\", [&](FrameGraph::Builder& builder, auto& data) { bool const isES2 = mEngine.getDriverApi().getFeatureLevel() == FeatureLevel::FEATURE_LEVEL_0; data.depth = builder.createTexture(\"Structure Buffer\", { .width = width, .height = height, .levels = uint8_t(levelCount), .format = isES2 ? TextureFormat::DEPTH24 : TextureFormat::DEPTH32F }); // workaround: since we have levels, this implies SAMPLEABLE (because of the gl // backend, which implements non-sampleables with renderbuffers, which don't have levels). // (should the gl driver revert to textures, in that case?) data.depth = builder.write(data.depth, FrameGraphTexture::Usage::DEPTH_ATTACHMENT | FrameGraphTexture::Usage::SAMPLEABLE); if (config.picking) { data.picking = builder.createTexture(\"Picking Buffer\", { .width = width, .height = height, .format = isES2 ? TextureFormat::RGBA8 : TextureFormat::RG32F }); data.picking = builder.write(data.picking, FrameGraphTexture::Usage::COLOR_ATTACHMENT); } builder.declareRenderPass(\"Structure Target\", { .attachments = { .color = { data.picking }, .depth = data.depth }, .clearFlags = TargetBufferFlags::COLOR0 | TargetBufferFlags::DEPTH }); }, [=, renderPass = pass](FrameGraphResources const& resources, auto const&, DriverApi&) mutable { Variant structureVariant(Variant::DEPTH_VARIANT); structureVariant.setPicking(config.picking); auto out = resources.getRenderPassInfo(); renderPass.setRenderFlags(structureRenderFlags); renderPass.setVariant(structureVariant); renderPass.appendCommands(mEngine, RenderPass::CommandTypeFlags::SSAO); renderPass.sortCommands(mEngine); renderPass.execute(mEngine, resources.getPassName(), out.target, out.params); }\n); The addPass method creates a node and it take in two lambda functions as its parameter. The first lambda sets up the resources that will be used in the execution of the Pass. This lambda is executed immediately and synchronously when addPass is called. The second lambda is the actual execution of the pass; it is executed when the graph has been completed and is traversed.","breadcrumbs":"Technical Notes » Framegraph » Example code","id":"175","title":"Example code"},"176":{"body":"In the above, we see through a graph and code what a frame graph looks like and how to build it. We provide here a more detailed description of what it does: Manages the lifetime of the resources Know how the resources are allocated, when it is used, and when it can be freed Calculates the usage bit of the texture resource The usage bit is used to indicate what the resources are used for: for example, will it be blitted to or sampled from? Calculates the load/store bits of the rendertargets within a renderpass. For example, if we are rendering into a texture, we would want to mark it with the bit \"keep\" as oppose to \"discard\".","breadcrumbs":"Technical Notes » Framegraph » What does it do?","id":"176","title":"What does it do?"},"177":{"body":"In a previous version of FrameGraph, there were only edges between Resource and Pass. For example, a Pass and Pass edge would not make logical sense. The following iteration, allowed for edges between two Resource nodes to indicate that one is a subresource of another (i.e. a layer in a mip-mapped texture). There are two extra features of FrameGraph that are important but has a lot subtlety, and, incidentally, their inclusion added great complexity to the implementation Importing/exporting resources outside of the graph In most cases, the graph and its resources are \"alive\" for only for a frame. For techniques like TAA (Temporal Anti-aliasing), we need to be able to import past output into the current FrameGraph Future Work For CPU only passes, explore multi-threading and re-ordering of the Pass nodes A graphical debugger for online debugging session in the spirit of matdbg. \"RenderGraph\" might be a more fitting name for this framework.","breadcrumbs":"Technical Notes » Framegraph » Additional details","id":"177","title":"Additional details"},"178":{"body":"Collection of README.md from the /libs folder.","breadcrumbs":"Technical Notes » Libraries » Libraries","id":"178","title":"Libraries"},"179":{"body":"So you want to call glClear()?","breadcrumbs":"Technical Notes » Libraries » bluegl » BlueGL Mechanics","id":"179","title":"BlueGL Mechanics"},"18":{"body":"This repository not only contains the core Filament engine, but also its supporting libraries and tools. android: Android libraries and projects filamat-android: Filament material generation library (AAR) for Android filament-android: Filament library (AAR) for Android filament-utils-android: Extra utilities (KTX loader, math types, etc.) gltfio-android: Filament glTF loading library (AAR) for Android samples: Android-specific Filament samples art: Source for various artworks (logos, PDF manuals, etc.) assets: 3D assets to use with sample applications build: CMake build scripts docs: Documentation math: Mathematica notebooks used to explore BRDFs, equations, etc. filament: Filament rendering engine (minimal dependencies) backend: Rendering backends/drivers (Vulkan, Metal, OpenGL/ES) ide: Configuration files for IDEs (CLion, etc.) ios: Sample projects for iOS libs: Libraries bluegl: OpenGL bindings for macOS, Linux and Windows bluevk: Vulkan bindings for macOS, Linux, Windows and Android camutils: Camera manipulation utilities filabridge: Library shared by the Filament engine and host tools filaflat: Serialization/deserialization library used for materials filagui: Helper library for Dear ImGui filamat: Material generation library filamentapp: SDL2 skeleton to build sample apps filameshio: Tiny filamesh parsing library (see also tools/filamesh) geometry: Mesh-related utilities gltfio: Loader for glTF 2.0 ibl: IBL generation tools image: Image filtering and simple transforms imageio: Image file reading / writing, only intended for internal use matdbg: DebugServer for inspecting shaders at run-time (debug builds only) math: Math library mathio: Math types support for output streams utils: Utility library (threads, memory, data structures, etc.) viewer: glTF viewer library (requires gltfio) samples: Sample desktop applications shaders: Shaders used by filamat and matc third_party: External libraries and assets environments: Environment maps under CC0 license that can be used with cmgen models: Models under permissive licenses textures: Textures under CC0 license tools: Host tools cmgen: Image-based lighting asset generator filamesh: Mesh converter glslminifier: Minifies GLSL source code matc: Material compiler filament-matp: Material parser matinfo Displays information about materials compiled with matc mipgen Generates a series of miplevels from a source image normal-blending: Tool to blend normal maps resgen Aggregates binary blobs into embeddable resources roughness-prefilter: Pre-filters a roughness map from a normal map to reduce aliasing specular-color: Computes the specular color of conductors based on spectral data web: JavaScript bindings, documentation, and samples","breadcrumbs":"Introduction » Directory structure","id":"18","title":"Directory structure"},"180":{"body":"This step is only required if updating or modifying BlueGL. These artifacts should already be checked into the Filament repository. From the libs/bluegl folder, run: ./bluegl-gen.py The bluegl-gen.py script generates a set of files: assembly (proxy) files: BlueGLCore*.S header files: include/BlueGLDefines.h and include/bluegl/BlueGL.h a private header:include/private_BlueGL.h","breadcrumbs":"Technical Notes » Libraries » bluegl » Step 0: Run bluegl-gen.py","id":"180","title":"Step 0: Run bluegl-gen.py"},"181":{"body":"#include  This headers adds a bunch of defines: ...\n#define glClear bluegl_glClear\n...","breadcrumbs":"Technical Notes » Libraries » bluegl » Step 1: Include the BlueGL defines header:","id":"181","title":"Step 1: Include the BlueGL defines header:"},"182":{"body":"#include \n#include  This also includes the GL headers, like  for you.","breadcrumbs":"Technical Notes » Libraries » bluegl » Step 2: Include the BlueGL header after the defines header:","id":"182","title":"Step 2: Include the BlueGL header after the defines header:"},"183":{"body":"Internally, the BlueGL library maintains a list of function pointers: void* __blue_glCore_glClear; During bluegl::bind(), each function gets assigned to the appropriate symbol loaded from the OS-specific GL shared library via dlopen, dlsym, and equivalents.","breadcrumbs":"Technical Notes » Libraries » bluegl » Step 3: Call bluegl::bind()","id":"183","title":"Step 3: Call bluegl::bind()"},"184":{"body":"Because of the prior #define, you'll actually be calling bluegl_glClear(). This is a trampoline function, defined in the BlueGLCore*.S assembly file (the exact implementation varies slightly on each platform): .private_extern _bluegl_glClear\n_bluegl_glClear: mov ___blue_glCore_glClear@GOTPCREL(%rip), %r11 jmp *(%r11) The invokes the __blue_glCore_glClear function, which was previously assigned to the actual GL function.","breadcrumbs":"Technical Notes » Libraries » bluegl » Step 4: Call glClear()","id":"184","title":"Step 4: Call glClear()"},"185":{"body":"To update the Vulkan headers, perform the following steps. First, find the latest version of the Vulkan headers here: https://github.com/KhronosGroup/Vulkan-Headers/tags Replace v1.3.232 with the latest version of the headers in the following commands. cd libs/bluevk\ncurl -OL https://github.com/KhronosGroup/Vulkan-Headers/archive/refs/tags/v1.3.232.zip\nunzip v1.3.232.zip\nrsync -r Vulkan-Headers-1.3.232/include/vulkan/ include/vulkan --delete\nrsync -r Vulkan-Headers-1.3.232/include/vk_video/ include/vk_video --delete\nrm include/vulkan/*.hpp\nrm -r Vulkan-Headers-1.3.232 v1.3.232.zip","breadcrumbs":"Technical Notes » Libraries » bluevk » Updating Vulkan headers","id":"185","title":"Updating Vulkan headers"},"186":{"body":"Filamat allows for generating materials programatically on the device as opposed to with the matc tool on the host machine. The cost is a binary size increase of your app due to the relatively larger size of the filamat library. For a smaller-sized library, see filamat_lite . It has no dependencies on glslang, but can only compile materials for OpenGL and does no shader code optimization. The filamat package is included in the releases available on GitHub .","breadcrumbs":"Technical Notes » Libraries » filamat » Filamat","id":"186","title":"Filamat"},"187":{"body":"Filamat is distributed as a set of static libraries you must link against: filamat, Filamat main library filabridge, Support library for Filament / Filamat shaders, Shader text for material generation utils, Support library for Filament / Filamat smol-v, SPIR-V compression library To use Filamat from Java you must use the following two libraries instead: filamat-java.jar, Contains Filamat's Java classes filamat-jni, Filamat's JNI bindings","breadcrumbs":"Technical Notes » Libraries » filamat » Libraries","id":"187","title":"Libraries"},"188":{"body":"This walkthrough will get you successfully compiling and linking native code against Filamat with minimum dependencies. To start, download Filament's latest binary release and extract into a directory of your choosing. Binary releases are suffixed with the platform name, for example, filament-20181009-linux.tgz. Create a file, main.cpp, in the same directory with the following contents: #include  #include  using namespace filamat; int main(int argc, char** argv)\n{ // Must be called before any materials can be built. MaterialBuilder::init(); MaterialBuilder builder; builder .name(\"My material\") .material(\"void material (inout MaterialInputs material) {\" \" prepareMaterial(material);\" \" material.baseColor.rgb = float3(1.0, 0.0, 0.0);\" \"}\") .shading(MaterialBuilder::Shading::LIT) .targetApi(MaterialBuilder::TargetApi::ALL) .platform(MaterialBuilder::Platform::ALL); Package package = builder.build(); if (package.isValid()) { std::cout << \"Success!\" << std::endl; } // Call when finished building all materials to release internal MaterialBuilder resources. MaterialBuilder::shutdown(); return 0;\n} The directory should look like: |-- README.md\n|-- bin\n|-- docs\n|-- include\n|-- lib\n|-- main.cpp We'll use a platform-specific Makefile to compile and link main.cpp with Filamat's libraries. Copy your platform's Makefile below into a Makefile inside the same directory.","breadcrumbs":"Technical Notes » Libraries » filamat » Linking against Filamat","id":"188","title":"Linking against Filamat"},"189":{"body":"FILAMENT_LIBS=-lfilamat -lfilabridge -lshaders -lutils -lsmol-v\nCC=clang++ main: main.o $(CC) -Llib/x86_64/ -stdlib=libc++ main.o $(FILAMENT_LIBS) -lpthread -ldl -o main main.o: main.cpp $(CC) -Iinclude/ -std=c++20 -stdlib=libc++ -pthread -c main.cpp clean: rm -f main main.o .PHONY: clean","breadcrumbs":"Technical Notes » Libraries » filamat » Linux","id":"189","title":"Linux"},"19":{"body":"Please see LICENSE .","breadcrumbs":"Introduction » License","id":"19","title":"License"},"190":{"body":"FILAMENT_LIBS=-lfilamat -lfilabridge -lshaders -lutils -lsmol-v\nCC=clang++ main: main.o $(CC) -Llib/x86_64/ main.o $(FILAMENT_LIBS) -o main main.o: main.cpp $(CC) -Iinclude/ -std=c++20 -c main.cpp clean: rm -f main main.o .PHONY: clean","breadcrumbs":"Technical Notes » Libraries » filamat » macOS","id":"190","title":"macOS"},"191":{"body":"Note that the static libraries distributed for Windows include several variants: mt, md, mtd, mdd. These correspond to the run-time library flags /MT, /MD, /MTd, and /MDd, respectively. Here we use the mt variant. When building Filamat from source, the USE_STATIC_CRT CMake option can be used to change the run-time library version. FILAMENT_LIBS=lib/x86_64/mt/filamat.lib lib/x86_64/mt/filabridge.lib lib/x86_64/mt/shaders.lib \\ lib/x86_64/mt/utils.lib lib/x86_64/mt/smol-v.lib\nCC=clang-cl.exe main.exe: main.obj $(CC) main.obj $(FILAMENT_LIBS) gdi32.lib user32.lib opengl32.lib main.obj: main.cpp $(CC) /MT /Iinclude/ /std:c++20 /c main.cpp clean: del main.exe main.obj .PHONY: clean","breadcrumbs":"Technical Notes » Libraries » filamat » Windows","id":"191","title":"Windows"},"192":{"body":"You should be able to invoke make and run the executable successfully: $ make\n$ ./main\nSuccess! On Windows, you'll need to open up a Visual Studio Native Tools Command Prompt and invoke nmake instead of make.","breadcrumbs":"Technical Notes » Libraries » filamat » Compiling","id":"192","title":"Compiling"},"193":{"body":"For simplicity, this demo doesn't do anything useful with the built material package. To use the material with Filament, pass the material package's data into a Filament Material builder: Package package = builder.build(); filament::Material* myMaterial = Material::Builder() .package(package.getData(), package.getSize()) .build(*engine); Note that this will require linking against Filament's libraries in addition to Filamat's.","breadcrumbs":"Technical Notes » Libraries » filamat » Using the Material with Filament","id":"193","title":"Using the Material with Filament"},"194":{"body":"The filamat_lite library is interchangeable with filamat, with a few caveats: Material compilation is only supported for the OpenGL backend. No shader-level optimization is performed. GLSL correctness is not checked. In addition, filamat_lite only performs a simple text match to determine which properties on the MaterialInputs structure are set. The material input variable must also always be refered to by the name material. void anotherFunction(inout MaterialInputs m) { // Incorrect! The MaterialInputs is being referred to by the name \"m\". m.metallic = 0.0;\n} void aFunction(inout MaterialInputs material) { // Works, but only because the variable name \"material\" is used. material.reflectance = 0.5;\n} // The MaterialInputs variable must be named material.\nvoid material(inout MaterialInputs material) { prepareMaterial(material); // Good. material.roughness = materialParams.roughness; material.baseColor.rgb = vec3(1.0, 0.0, 1.0); aFunction(material); anotherFunction(material);\n}","breadcrumbs":"Technical Notes » Libraries » filamat » Filamat Lite","id":"194","title":"Filamat Lite"},"195":{"body":"gltfio is a loader library that consumes gltf or glb content and produces Filament objects. For usage details, see the docstring for AssetLoader. gltfio has two plug-in interfaces, TextureProvider and MaterialProvider. Filament ships with several ready-to-go implementations described below. MaterialProvider creates Filament materials in response to certain glTF requirements. UbershaderProvider loads pre-built materials. JitShaderProvider builds materials at run time using the filamat library. TextureProvider creates and populates Filament Texture objects. StbProvider uses the STB library to read PNG and JPEG files. Ktx2Provider uses the BasisU library to read KTX2 files.","breadcrumbs":"Technical Notes » Libraries » gltfio » Description","id":"195","title":"Description"},"196":{"body":"UbershaderProvider is a ready-to-go implementation of the MaterialProvider interface that should be used in applications that need fast startup times. There is no material compilation that occurs at run time, but the shaders might be relatively large and complex. At load time, the ubershader loader consumes an ubershader archive which is a precompiled set of materials bundled with formal descriptions of the glTF features that they support. The uberz command line tool consumes a list of .spec and .filamat files and produces a single .uberz file. For details on these two file formats, see the README in libs/uberz.","breadcrumbs":"Technical Notes » Libraries » gltfio » UbershaderProvider","id":"196","title":"UbershaderProvider"},"197":{"body":"This library can be used to generate the reflections texture used by filament's IndirectLight class. It is similar to the cmgen tool except that all computations are performed on the GPU and are therefore significantly faster. cmgen however offers more functionalities. IBL Prefilter is designed entirely as a client of filament, that is, it only uses filament public APIs.","breadcrumbs":"Technical Notes » Libraries » iblprefilter » IBL Prefilter","id":"197","title":"IBL Prefilter"},"198":{"body":"The library is called libfilament-iblprefilter.a and its public headers can be found in .","breadcrumbs":"Technical Notes » Libraries » iblprefilter » Library and headers","id":"198","title":"Library and headers"},"199":{"body":"Expect a total processing time of about 100ms to 300ms for a 5-levels 256 x 256 cubemap with 1024 samples.","breadcrumbs":"Technical Notes » Libraries » iblprefilter » Performance","id":"199","title":"Performance"},"2":{"body":"Android projects can simply declare Filament libraries as Maven dependencies: repositories { // ... mavenCentral()\n} dependencies { implementation 'com.google.android.filament:filament-android:1.65.0'\n} Here are all the libraries available in the group com.google.android.filament: Artifact Description filament-android The Filament rendering engine itself. filament-android-debug Debug version of filament-android. gltfio-android A glTF 2.0 loader for Filament, depends on filament-android. filament-utils-android KTX loading, Kotlin math, and camera utilities, depends on gltfio-android. filamat-android A runtime material builder/compiler. This library is large but contains a full shader compiler/validator/optimizer and supports both OpenGL and Vulkan. filamat-android-lite A much smaller alternative to filamat-android that can only generate OpenGL shaders. It does not provide validation or optimizations.","breadcrumbs":"Introduction » Android","id":"2","title":"Android"},"20":{"body":"This is not an officially supported Google product.","breadcrumbs":"Introduction » Disclaimer","id":"20","title":"Disclaimer"},"200":{"body":"#include \n#include  using namespace filament; Engine* engine = Engine::create(); // create an IBLPrefilterContext, keep it around if several cubemap will be processed.\nIBLPrefilterContext context(engine); // create the specular (reflections) filter. This operation generates the kernel, so it's important\n// to keep it around if it will be reused for several cubemaps.\nIBLPrefilterContext::SpecularFilter filter(context); // launch the heaver computation. Expect 100-100ms on the GPU.\nTexture* texture = filter(environment_cubemap); IndirectLight* indirectLight = IndirectLight::Builder() .reflections(texture) .build(engine);","breadcrumbs":"Technical Notes » Libraries » iblprefilter » Example","id":"200","title":"Example"},"201":{"body":"Capabilities Setup for Desktop Setup for Android Debugger Usage Architecture Overview C++ Server JavaScript Client HTTP Requests WebSocket Messages Wish List Screenshot Material Chunks","breadcrumbs":"Technical Notes » Libraries » matdbg » matdbg","id":"201","title":"matdbg"},"202":{"body":"matdbg is a library and web application that enables debugging and live-editing of Filament shaders. At the time of this writing, the following capabilities are supported. OpenGL: Editing GLSL Metal: Editing MSL Vulkan: Editing transpiled GLSL, displaying disassembled SPIR-V WebGPU: Editing WGSL Note that a given material can be built with multiple backends, even though only one backend is active in a particular session. For example, if the current app is using Vulkan, it is still possible to inspect the Metal shaders, as long as the material has been built with Metal support included.","breadcrumbs":"Technical Notes » Libraries » matdbg » Capabilities","id":"202","title":"Capabilities"},"203":{"body":"When using the easy build script, include the -d argument. For example: ./build.sh -fd debug gltf_viewer The d enables a CMake option called FILAMENT_ENABLE_MATDBG and the f ensures that CMake gets re-run so that the option is honored. Next, set an environment variable as follows. In Windows, use set instead of export. export FILAMENT_MATDBG_PORT=8080 Next, launch any app that links against a debug build of a Filament and point your web browser to http://localhost:8080. Skip ahead to Debugger Usage .","breadcrumbs":"Technical Notes » Libraries » matdbg » Setup for Desktop","id":"203","title":"Setup for Desktop"},"204":{"body":"Rebuild Filament for Android after enabling a CMake option called FILAMENT_ENABLE_MATDBG. Note that CMake is invoked from several places for Android (both gradle and our easy build script), so one pragmatic and reliable way of doing this is to simply hack CMakeLists.txt and filament-android/CMakeLists.txt by unconditionally setting FILAMENT_ENABLE_MATDBG to ON. After rebuilding Filament with the option enabled, ensure that internet permissions are enabled in your app by adding the following into your manifest as a child of the  element.  Now launch your app as usual. The Filament Engine sets up a server that is hardcoded to listen to port 8081. Next, you will need to forward your device's TCP port 8081 to your host port of choice. For example, to forward the matdbg server on your device to port 8081 on your host machine, do the following: adb forward tcp:8081 tcp:8081 This lets you go to http://localhost:8081 in Chrome on your host machine. Note that we generally use a release build of Filament when running on Android, so the shaders are optimized and very unreadable. This can be avoided by modifying the build such that -g is passed to matc even in release builds.","breadcrumbs":"Technical Notes » Libraries » matdbg » Setup for Android","id":"204","title":"Setup for Android"},"205":{"body":"After opening the matdbg page in your browser, the usual first step is to select a material in the upper-left pane. Sometimes you might need force your app to redraw (e.g. by resizing the window) in order make the materials selectable. The next step is to select an active (boldface) shader variant in the lower-left pane. This allows you to view the GLSL, MSL, and SPIR-V code that was generated by matc or filamat. In the sidebar, inactive shader variants have a disabled appearance, but they can still be examined in the shader editor. The active status of each shader program is refreshed every second. You can also make modifications to GLSL or MSL, so long as the shader inputs and uniforms remain intact. After making an edit, click the [rebuild] button in the header. Note that your edits will be lost after closing the web page.","breadcrumbs":"Technical Notes » Libraries » matdbg » Debugger Usage","id":"205","title":"Debugger Usage"},"206":{"body":"To save an edit, press Cmd+S ( Ctrl+S on Linux/Windows) as an alternative to clicking [rebuild]. If the editor has focus, you can navigate between materials by holding Shift+Ctrl while pressing the up or down arrow. Navigation between variants is similar, just use left / right instead of up / down.","breadcrumbs":"Technical Notes » Libraries » matdbg » Keyboard Shortcuts","id":"206","title":"Keyboard Shortcuts"},"207":{"body":"The matdbg library has two parts: a C++ server and a JavaScript client. The C++ server is responsible for instancing a civetweb context that handles HTTP and WebSocket requests. The JavaScript client is a small web app that contains a view into an in-browser database of materials. The WebSocket server receives push-style notifications from the client (such as edits) while the HTTP server responds to material queries using simple JSON messages. When a new WebSocket connection is established, the client asks the server for a list of materials in order to populate its in-browser database. If the connection is lost (e.g. if the app crashes), then the database stays intact and the web app is still functional. If a new Filament app is launched, the client inserts entries into its database rather than replacing the existing set. The material database is cleared only when the web page is manually refreshed by the user.","breadcrumbs":"Technical Notes » Libraries » matdbg » Architecture Overview","id":"207","title":"Architecture Overview"},"208":{"body":"The civetweb server is wrapped by our DebugServer class, whose public interface is comprised of a couple methods that are called from the Filament engine: addMaterial Notifies the debugger that the given material package is being loaded into the engine. setEditCallback Sets up a callback that allows the Filament engine to listen for shader edits. setQueryCallback Sets up a callback that allows the debugger to ask for current information.","breadcrumbs":"Technical Notes » Libraries » matdbg » C++ Server","id":"208","title":"C++ Server"},"209":{"body":"The web app is written in simple, modern JavaScript. It uses third-party libraries which are fetched from a CDN using 
+
+
+
+