Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix reference BRDF implementation (#2386) #2392

Merged
merged 6 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions extensions/2.0/Khronos/KHR_materials_clearcoat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,35 +117,33 @@ The `fresnel_coat` function is computed using the Schlick Fresnel term from the
```
function fresnel_coat(normal, ior, weight, base, layer) {
emackey marked this conversation as resolved.
Show resolved Hide resolved
f0 = ((1-ior)/(1+ior))^2
fr = f0 + (1 - f0)*(1 - abs(NdotV))^5 // N = normal
fr = f0 + (1 - f0)*(1 - abs(dot(V, normal)))^5
return mix(base, layer, weight * fr)
}
```

Applying the functions we arrive at the coated material

```
coated_material = mix(material, clearcoat_brdf(clearcoatRughness^2), clearcoat * (0.04 + (1 - 0.04) * (1 - NdotV)^5))
coated_material = mix(material, clearcoat_brdf(clearcoatRoughness^2), clearcoat * (0.04 + (1 - 0.04) * (1 - VdotNc)^5))
```

and finally, substituting and simplifying, using some symbols from [Appendix B](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#appendix-b-brdf-implementation) and `Nc` for the clearcoat normal:

```
clearcoatFresnel = 0.04 + (1 - 0.04) * (1 - abs(VdotNc))^5
clearcoatAlpha = clearcoatRoughness^2
clearcoat_fresnel = 0.04 + (1 - 0.04) * (1 - abs(VdotNc))^5
clearcoat_alpha = clearcoatRoughness^2
clearcoat_brdf = D(clearcoat_alpha) * G(clearcoat_alpha) / (4 * abs(VdotNc) * abs(LdotNc))

f_clearcoat = clearcoatFresnel * D(clearcoatAlpha) * G / (4 * abs(VdotNc) * abs(LdotNc))

coated_material = (f_diffuse + f_specular) * (1 - clearcoat * clearcoatFresnel) +
f_clearcoat * clearcoat
coated_material = mix(material, clearcoat_brdf, clearcoat * clearcoat_fresnel)
```

#### Emission

The clearcoat layer is on top of emission in the layering stack. Consequently, the emission is darkened by the Fresnel term.

```
coated_emission = emission * (0.04 + (1 - 0.04) * (1 - NdotV)^5)
coated_emission = emission * (1 - clearcoat * clearcoat_fresnel)
```

#### Discussion
Expand Down
4 changes: 2 additions & 2 deletions extensions/2.0/Khronos/KHR_materials_ior/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,10 @@ Valid values for `ior` are numbers greater than or equal to 1. In addition, a va
The extension changes the computation of the Fresnel term defined in [Appendix B](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#appendix-b-brdf-implementation) to the following:

```
const dielectricSpecular = ((ior - 1)/(ior + 1))^2
dielectric_f0 = ((ior - 1)/(ior + 1))^2
```

Note that for the default index of refraction `ior = 1.5` this term evaluates to `dielectricSpecular = 0.04`.
Note that for the default index of refraction `ior = 1.5` this term evaluates to `dielectric_f0 = 0.04`.

## Interaction with other extensions

Expand Down
62 changes: 31 additions & 31 deletions extensions/2.0/Khronos/KHR_materials_sheen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,82 +91,82 @@ Not all incoming light is reflected at a micro-fiber. Some of the light may hit

All implementations should use the same calculations for the BRDF inputs. See [Appendix B](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#appendix-b-brdf-implementation) for more details on the BRDF calculations.

The sheen formula `f_sheen` follows the common microfacet form:
The sheen formula follows the common microfacet form with visibility term $\mathcal{V}_s$:

$$
\text{SheenBRDF} = \frac{G_S D_S}{4 \, \left|N \cdot L \right| \, \left| N \cdot V \right|} = \mathcal{V}_S D_S
$$

*f*<sub>*sheen*</sub> = *sheenColor* * *sheenFresnel* * *sheenDistribution* * *sheenVisibility* = *sheenColor* * *F*<sub>*S*</sub> * *G*<sub>*S*</sub> * *D*<sub>*S*</sub> / (4 * abs(dot(*N*, *L*)) * abs(dot(*N*, *V*)))

### Sheen distribution

The sheen distribution follows the "Charlie" sheen definition from ImageWorks [Conty and Kulla (2017)](#ContyKulla2017):
The sheen distribution $D_s$ follows the "Charlie" sheen definition from ImageWorks [Conty and Kulla (2017)](#ContyKulla2017):

```glsl
alphaG = sheenRoughness * sheenRoughness
invR = 1 / alphaG
alpha_g = sheenRoughness * sheenRoughness
inv_r = 1 / alpha_g
cos2h = NdotH * NdotH
sin2h = 1 - cos2h
sheenDistribution = (2 + invR) * pow(sin2h, invR * 0.5) / (2 * PI);
sheen_distribution = (2 + inv_r) * pow(sin2h, inv_r * 0.5) / (2 * PI);
```

### Sheen visibility

The "Charlie" sheen visibility is also defined in the same document:
The "Charlie" sheen visibility $\mathcal{V}_s = \frac{G_s}{4 \, \left|N \cdot L \right| \, \left| N \cdot V \right|}$ is also defined in the same document:

```glsl
float l(float x, float alphaG)
float l(float x, float alpha_g)
{
float oneMinusAlphaSq = (1.0 - alphaG) * (1.0 - alphaG);
float a = mix(21.5473, 25.3245, oneMinusAlphaSq);
float b = mix(3.82987, 3.32435, oneMinusAlphaSq);
float c = mix(0.19823, 0.16801, oneMinusAlphaSq);
float d = mix(-1.97760, -1.27393, oneMinusAlphaSq);
float e = mix(-4.32054, -4.85967, oneMinusAlphaSq);
float one_minus_alpha_sq = (1.0 - alpha_g) * (1.0 - alpha_g);
float a = mix(21.5473, 25.3245, one_minus_alpha_sq);
float b = mix(3.82987, 3.32435, one_minus_alpha_sq);
float c = mix(0.19823, 0.16801, one_minus_alpha_sq);
float d = mix(-1.97760, -1.27393, one_minus_alpha_sq);
float e = mix(-4.32054, -4.85967, one_minus_alpha_sq);
return a / (1.0 + b * pow(x, c)) + d * x + e;
}

float lambdaSheen(float cosTheta, float alphaG)
float lambda_sheen(float cos_theta, float alpha_g)
{
return abs(cosTheta) < 0.5 ? exp(l(cosTheta, alphaG)) : exp(2.0 * l(0.5, alphaG) - l(1.0 - cosTheta, alphaG));
return abs(cos_theta) < 0.5 ? exp(l(cos_theta, alpha_g)) : exp(2.0 * l(0.5, alpha_g) - l(1.0 - cos_theta, alpha_g));
}

sheenVisibility = 1.0 / ((1.0 + lambdaSheen(NdotV, alphaG) + lambdaSheen(NdotL, alphaG)) * (4.0 * NdotV * NdotL));
sheen_visibility = 1.0 / ((1.0 + lambda_sheen(NdotV, alpha_g) + lambda_sheen(NdotL, alpha_g)) * (4.0 * NdotV * NdotL));
```

However, depending on device performance and resource constraints, one can use a simpler visibility term, like the one defined by [Ashikhmin and Premoze (2007)](#AshikhminPremoze2007) (but that will make the BRDF not energy conserving when using the albedo-scaling technique described below):

```glsl
sheenVisibility = 1 / (4 * (NdotL + NdotV - NdotL * NdotV))
sheen_visibility = 1 / (4 * (NdotL + NdotV - NdotL * NdotV))
```

### Sheen Fresnel

The Fresnel term may be omitted, i.e., *F* = 1.

### Sheen layering

#### Albedo-scaling technique

The sheen layer can be combined with the base layer with an albedo-scaling technique described in [Conty and Kulla (2017)](#ContyKulla2017). The base layer *f*<sub>*diffuse*</sub> + *f*<sub>*specular*</sub> from [Appendix B](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#appendix-b-brdf-implementation) is scaled with *sheenAlbedoScaling* to avoid energy gain.

*f* = *f*<sub>*sheen*</sub> + (*f*<sub>*diffuse*</sub> + *f*<sub>*specular*</sub>) * *sheenAlbedoScaling*
The sheen layer can be combined with the base layer with an albedo-scaling technique described in [Conty and Kulla (2017)](#ContyKulla2017). The base layer `material = mix(dielectric_brdf, metal_brdf, metallic)` from [Appendix B](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#appendix-b-brdf-implementation) is scaled with `sheen_albedo_scaling` to avoid energy gain.

```glsl
float max3(vec3 v) { return max(max(v.x, v.y), v.z); }
sheen_albedo_scaling = min(1.0 - max3(sheenColor) * E(VdotN), 1.0 - max3(sheenColor) * E(LdotN))

sheenAlbedoScaling = min(1.0 - max3(sheenColor) * E(VdotN), 1.0 - max3(sheenColor) * E(LdotN))
sheen_material = sheenColor * sheen_brdf + material * sheen_albedo_scaling
```

The values `E(x)` can be looked up in a table which can be found in section 6.2.3 of [Enterprise PBR Shading Model](#theory-documentation-and-implementations) if you use the "Charlie" visibility term. If you use Ashikhmin instead, you can get the lookup table by using the [cmgen tool from Filament](#theory-documentation-and-implementations), with the `--ibl-dfg` and `--ibl-dfg-cloth` flags: the table is in the blue channel of the generated picture. The lookup must be done with `x = VdotN` and `y = sheenRoughness`.

If you want to trade a bit of accuracy for more performance, you can use the `VdotN` term only and thus avoid doing multiple lookups for `LdotN`. The albedo scaling term is simplified to:
```glsl
sheenAlbedoScaling = 1.0 - max3(sheenColor) * E(VdotN)
sheen_albedo_scaling = 1.0 - max3(sheenColor) * E(VdotN)
```

In this simplified form, it can be used to scale the base layer for both direct and indirect lights:

```glsl
specular_direct *= sheenAlbedoScaling;
diffuse_direct *= sheenAlbedoScaling;
environmentIrradiance_indirect *= sheenAlbedoScaling
specularEnvironmentReflectance_indirect *= sheenAlbedoScaling
specular_direct *= sheen_albedo_scaling;
diffuse_direct *= sheen_albedo_scaling;
environment_irradiance_indirect *= sheen_albedo_scaling
specular_environment_reflectance_indirect *= sheen_albedo_scaling
```

## Schema
Expand Down
37 changes: 18 additions & 19 deletions extensions/2.0/Khronos/KHR_materials_specular/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ Factor and texture are combined by multiplication to describe a single value.
| **specularColorFactor** | `number[3]` | The F0 color of the specular reflection (linear RGB). | No, default: `[1.0, 1.0, 1.0]`|
| **specularColorTexture** | [`textureInfo`](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#reference-textureinfo) | A texture that defines the F0 color of the specular reflection, stored in the `RGB` channels and encoded in sRGB. This texture will be multiplied by specularColorFactor. | No |

If a texture is defined:

- The specular color is computed with : `specularColor = specularColorFactor * sampleLinear(specularColorTexture).rgb`.
- The specular strength is computed with : `specular = specularFactor * sample(specularTexture).a`.

The `specular` and `specularColor` parameters affect the `dielectric_brdf` of the glTF 2.0 metallic-roughness material.

```
Expand Down Expand Up @@ -134,41 +139,35 @@ function fresnel_mix(f0_color, ior, weight, base, layer) {
}
```

Therefore, the Fresnel term `F` in the final BRDF of the material changes to
Therefore, the Fresnel term `dielectric_fresnel` in the final BRDF of the material changes to

```
dielectricSpecularF0 = min(0.04 * specularColorFactor * specularColorTexture.rgb, float3(1.0)) *
specularFactor * specularTexture.a
dielectricSpecularF90 = specularFactor * specularTexture.a

F0 = lerp(dielectricSpecularF0, baseColor.rgb, metallic)
F90 = lerp(dielectricSpecularF90, 1, metallic)

F = F0 + (F90 - F0) * (1 - VdotH)^5
dielectric_f0 = min(0.04 * specularColor, float3(1.0)) * specular
dielectric_f90 = specular
dielectric_fresnel = mix(dielectric_f0, dielectric_f90, fresnel_w)
```

Note that in `dielectricSpecularF0` we clamp the product of specular color and f0 reflectance from IOR (`0.04`), before multiplying by specular.
Note that in `dielectric_f0` we clamp the product of specular color and f0 reflectance from IOR (`0.04`), before multiplying by `specular`.

In the diffuse component we have to account for the fact that `F` is now an RGB value.
In the diffuse component we have to account for the fact that `dielectric_fresnel` is now an RGB value. Thus we redefine `dielectric_brdf` as follows:

```
c_diff = lerp(baseColor.rgb, black, metallic)
diffuse = c_diff / PI
f_diffuse = (1 - max(F.r, F.g, F.b)) * diffuse
dielectric_fresnel_max = max_value(dielectric_fresnel)
dielectric_brdf = dielectric_fresnel * specular_brdf + (1 - dielectric_fresnel_max) * diffuse_brdf
```

## Interaction with other extensions

If `KHR_materials_ior` is used in combination with `KHR_materials_specular`, the constant `0.04` is replaced by the value computed from the IOR.

```
dielectricSpecularF0 = min(((ior - outside_ior) / (ior + outside_ior))^2 * specularColorFactor * specularColorTexture.rgb, float3(1.0)) * specularFactor * specularTexture.a
dielectricSpecularF90 = specularFactor * specularTexture.a
dielectric_f0 = min(((ior - outside_ior) / (ior + outside_ior))^2 * specularColor, float3(1.0)) * specular
dielectric_f90 = specular
```

`outside_ior` is typically set to 1.0, the index of refraction of air.

If `KHR_materials_transmission` is used in combination with `KHR_materials_specular`, the ratio of transmission and reflection computed from the Fresnel term also depends on `dielectricSpecularF0` and `dielectricSpecularF90`. The following images show a thin, transmissive material.
If `KHR_materials_transmission` is used in combination with `KHR_materials_specular`, the ratio of transmission and reflection computed from the Fresnel term also depends on `dielectric_f0` and `dielectric_f90`. The following images show a thin, transmissive material.

Specular from 0 to 1:

Expand All @@ -179,7 +178,7 @@ Specular color from [0,0,0] to [1,1,1] (top) and [0,0,0] to [1,0,0]:
![](figures/specular-color-thin.png)
![](figures/specular-color-thin-2.png)

If `KHR_materials_transmission` and `KHR_materials_volume` are used in combination with `KHR_materials_specular`, specular factor and specular color have no effect on the refraction angle. The direction of the refracted light ray is only based on the index of refraction defined in `KHR_materials_ior`. The ratio of transmission and reflection computed from the Fresnel term still depends on `dielectricSpecularF0` and `dielectricSpecularF90`. The following images show a refractive material.
If `KHR_materials_transmission` and `KHR_materials_volume` are used in combination with `KHR_materials_specular`, specular factor and specular color have no effect on the refraction angle. The direction of the refracted light ray is only based on the index of refraction defined in `KHR_materials_ior`. The ratio of transmission and reflection computed from the Fresnel term still depends on `dielectric_f0` and `dielectric_f90`. The following images show a refractive material.

Specular from 0 to 1:

Expand All @@ -197,7 +196,7 @@ Specular color from [0,0,0] to [1,1,1] (top) and [0,0,0] to [1,0,0]:
Material models that define F0 in terms of reflectance at normal incidence can be converted by encoding the reflectance in the specular color parameters. Typically, the reflectance ranges from 0% to 8%, given as a value in range [0,1], with 0.5 (=4%) being the default. F0 is computed from `reflectance` in the following way:

```
dielectricSpecularF0 = 0.08 * reflectance
dielectric_f0 = 0.08 * reflectance
```

In contrast, `KHR_materials_specular` defines a constant factor of 0.04 to compute F0, as this corresponds to glTF's default IOR of 1.5. Therefore, by encoding an additional constant factor of 2 in `specularColorFactor`, we can convert from reflectance to specular color without any loss.
Expand Down
8 changes: 4 additions & 4 deletions extensions/2.0/Khronos/KHR_materials_transmission/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,26 +207,26 @@ With the step function $\chi^+$ we ensure that the microsurface is only visible
Introducing the visibility function

$$
V_T = \frac{G_T}{4 \left| N \cdot L \right| \left| N \cdot V \right|}
\mathcal{V}_T = \frac{G_T}{4 \left| N \cdot L \right| \left| N \cdot V \right|}
$$

simplifies the original microfacet BTDF to

$$
\text{MicrofacetBTDF} = V_T D_T
\text{MicrofacetBTDF} = \mathcal{V}_T D_T
$$

with

$$
V_T = \frac{\chi^+\left(\frac{H_T \cdot L}{N \cdot L}\right)}{\left| N \cdot L\right| + \sqrt{\alpha^2 + (1 - \alpha^2) (N \cdot L)^2}} \frac{\chi^+\left(\frac{H_T \cdot V}{N \cdot V}\right)}{\left| N \cdot V \right| + \sqrt{\alpha^2 + (1 - \alpha^2) (N \cdot V)^2}}
\mathcal{V}_T = \frac{\chi^+\left(\frac{H_T \cdot L}{N \cdot L}\right)}{\left| N \cdot L\right| + \sqrt{\alpha^2 + (1 - \alpha^2) (N \cdot L)^2}} \frac{\chi^+\left(\frac{H_T \cdot V}{N \cdot V}\right)}{\left| N \cdot V \right| + \sqrt{\alpha^2 + (1 - \alpha^2) (N \cdot V)^2}}
$$

Thus we have the function

```
function specular_btdf(α) {
return V_T * D_T
return Vis_T * D_T
proog128 marked this conversation as resolved.
Show resolved Hide resolved
}
```

Expand Down
29 changes: 15 additions & 14 deletions specification/2.0/Specification.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3058,33 +3058,33 @@ G = \frac{2 \, \left| N \cdot L \right| \, \chi^{+}(H \cdot L)}{\left| N \cdot L

where χ^+^(*x*) denotes the Heaviside function: 1 if *x* > 0 and 0 if *x* <= 0. See <<Heitz2014,Heitz (2014)>> for a derivation of the formulas.

Introducing the visibility function
Introducing the visibility function $\mathcal{V}$

[latexmath]
++++
V = \frac{G}{4 \, \left| N \cdot L \right| \, \left| N \cdot V \right|}
\mathcal{V} = \frac{G}{4 \, \left| N \cdot L \right| \, \left| N \cdot V \right|}
++++

simplifies the original microfacet BRDF to

[latexmath]
++++
\text{MicrofacetBRDF} = V D
\text{MicrofacetBRDF} = \mathcal{V} D
++++

with

[latexmath]
++++
V = \frac{\, \chi^{+}(H \cdot L)}{\left| N \cdot L\right| + \sqrt{\alpha^2 + (1 - \alpha^2) (N \cdot L)^2}} \frac{\, \chi^{+}(H \cdot V)}{\left| N \cdot V \right| + \sqrt{\alpha^2 + (1 - \alpha^2) (N \cdot V)^2}}
\mathcal{V} = \frac{\, \chi^{+}(H \cdot L)}{\left| N \cdot L\right| + \sqrt{\alpha^2 + (1 - \alpha^2) (N \cdot L)^2}} \frac{\, \chi^{+}(H \cdot V)}{\left| N \cdot V \right| + \sqrt{\alpha^2 + (1 - \alpha^2) (N \cdot V)^2}}
++++

Thus, we have the function

[source,c]
----
function specular_brdf(α) {
return V * D
return Vis * D
proog128 marked this conversation as resolved.
Show resolved Hide resolved
}
----

Expand Down Expand Up @@ -3178,22 +3178,23 @@ Metal and dielectric are mixed according to the metalness:
material = mix(dielectric_brdf, metal_brdf, metallic)
----

Taking advantage of the fact that `roughness` is shared between metal and dielectric and that the Schlick Fresnel is used, we can simplify the mix and arrive at the final BRDF for the material:
The full code is given below:

[source,c]
----
const black = 0
fresnel_w = (1 - abs(VdotH))^5

c_diff = lerp(baseColor.rgb, black, metallic)
f0 = lerp(0.04, baseColor.rgb, metallic)
α = roughness^2
diffuse_brdf = (1 / π) * baseColor.rgb
specular_brdf = D(roughness^2) * G(roughness^2) / (4 * abs(VdotN) * abs(LdotN))

F = f0 + (1 - f0) * (1 - abs(VdotH))^5
dielectric_f0 = 0.04
dielectric_fresnel = dielectric_f0 + (1 - dielectric_f0) * fresnel_w
dielectric_brdf = mix(diffuse_brdf, specular_brdf, dielectric_fresnel)

f_diffuse = (1 - F) * (1 / π) * c_diff
f_specular = F * D(α) * G(α) / (4 * abs(VdotN) * abs(LdotN))
metal_fresnel = baseColor.rgb + (1 - baseColor.rgb) * fresnel_w
metal_brdf = metal_fresnel * specular_brdf

material = f_diffuse + f_specular
material = mix(dielectric_brdf, metal_brdf, metallic)
----


Expand Down
Loading