summaryrefslogtreecommitdiff
path: root/data/shaders/pbr-frag.glsl
blob: e2841fea5372acfceb4c1a222efed43f854eca56 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
// -*- mode: c -*-

struct Material {
  vec3 baseColorFactor;
  bool baseColorTextureEnabled;
  int baseColorTexcoord;
  float metallicFactor;
  float roughnessFactor;
  bool metallicRoughnessTextureEnabled;
  int metallicRoughnessTexcoord;
  vec3 normalFactor;
  bool normalTextureEnabled;
  int normalTexcoord;
  bool occlusionTextureEnabled;
  int occlusionTexcoord;
  vec3 emissiveFactor;
  bool emissiveTextureEnabled;
  int emissiveTexcoord;
  int alphaMode;
  float alphaCutoff;
};

struct Light {
  bool enabled;
  int type;
  vec3 position;
  vec3 direction;
  vec4 color;
  float cutOff;
};

#ifdef GLSL120
attribute vec3 fragWorldPos;
attribute vec3 fragNormal;
attribute vec2 fragTexcoord0;
attribute vec2 fragTexcoord1;
attribute vec4 fragColor0;
#else
in vec3 fragWorldPos;
in vec3 fragNormal;
in vec2 fragTexcoord0;
in vec2 fragTexcoord1;
in vec4 fragColor0;
#endif

#ifdef GLSL330
out vec4 fragColor;
#endif

#define MAX_LIGHTS 4

uniform Material material;
uniform Light lights[MAX_LIGHTS];
uniform vec4 ambientLightColor;
uniform bool vertexColored;
uniform vec3 cameraPosition;
uniform sampler2D baseColorTexture;
uniform sampler2D metallicRoughnessTexture;
uniform sampler2D normalTexture;
uniform sampler2D occlusionTexture;
uniform sampler2D emissiveTexture;

const float PI = 3.14159265358979323846;
const float GAMMA = 2.2;

#ifndef GLSL330
// Compatibility shim for older GLSL versions.
vec2 texture(sampler2D tex, vec2 coord) {
  return texture2D(tex, coord);
}
#endif

float posDot(vec3 v1, vec3 v2) {
  return max(dot(v1, v2), 0.0);
}

vec3 fresnelSchlick(float cosTheta, vec3 baseColor)
{
    return baseColor + (1.0 - baseColor) * pow(max(1.0 - cosTheta, 0.0), 5.0);
}

float distributionGGX(vec3 normal, vec3 halfAngle, float roughness)
{
    float a = roughness * roughness * roughness * roughness;
    float ndoth = posDot(normal, halfAngle);
    float denominator = ndoth * ndoth * (a - 1.0) + 1.0;

    return a / (PI * denominator * denominator);
}

float geometrySchlickGGX(float ndotv, float roughness)
{
    float r = roughness + 1.0;
    float k = (r * r) / 8.0;

    return ndotv / (ndotv * (1.0 - k) + k);
}

float geometrySmith(vec3 normal, vec3 viewDirection, vec3 lightDirection,
                    float roughness)
{
    return geometrySchlickGGX(posDot(normal, viewDirection), roughness) *
      geometrySchlickGGX(posDot(normal, lightDirection), roughness);
}

vec4 applyAlpha(vec4 color) {
  // Apply alpha mode.
  if(material.alphaMode == 0) { // opaque
    return vec4(color.rgb, 1.0);
  } else if(material.alphaMode == 1) { // mask
    if(color.a >= material.alphaCutoff) {
      return vec4(color.rgb, 1.0);
    } else {
      discard;
    }
  } else if(material.alphaMode == 2) { // blend
    if(color.a <= 0.005) {
      discard;
    } else {
      return color;
    }
  }
}

vec2 texcoord(int i) {
  return i == 0 ? fragTexcoord0: fragTexcoord1;
}

vec4 sRGBtoLinear(vec4 srgb) {
  return vec4(pow(srgb.rgb, vec3(GAMMA)), srgb.a);
}

vec3 gammaCorrect(vec3 color) {
  return pow(color, vec3(1.0 / GAMMA));
}

vec3 toneMap(vec3 color) {
  return color / (color + vec3(1.0));
}

float materialMetallic() {
  float m = material.metallicFactor;

  if(material.metallicRoughnessTextureEnabled) {
    m *= texture(metallicRoughnessTexture,
                 texcoord(material.metallicRoughnessTexcoord)).b;
  }

  return m;
}

float materialRoughness() {
  float r = material.roughnessFactor;

  if(material.metallicRoughnessTextureEnabled) {
    r *= texture(metallicRoughnessTexture,
                 texcoord(material.metallicRoughnessTexcoord)).g;
  }

  return r;
}

vec4 materialAlbedo() {
  vec4 color = vec4(0.0, 0.0, 1.0, 1.0);

  if(material.baseColorTextureEnabled) {
    vec4 texColor = texture(baseColorTexture,
                            texcoord(material.baseColorTexcoord));
    color = sRGBtoLinear(texColor);
  }

  color *= vec4(material.baseColorFactor, 1.0);

  if(vertexColored) {
    color *= fragColor0;
  }

  return color;
}

vec3 materialEmissive() {
  vec3 color = vec3(0.0);

  if(material.emissiveTextureEnabled) {
    vec4 texColor = texture(emissiveTexture,
                            texcoord(material.emissiveTexcoord));
    color = sRGBtoLinear(texColor).rgb;
  }

  return color * material.emissiveFactor;
}

vec3 materialOcclusion() {
  if(material.occlusionTextureEnabled) {
    return vec3(texture(occlusionTexture,
                        texcoord(material.occlusionTexcoord)).r);
  } else {
    return vec3(1.0);
  }
}

vec3 materialNormal() {
  if(material.normalTextureEnabled) {
    // See: http://www.thetenthplanet.de/archives/1180
    vec2 t = texcoord(material.normalTexcoord);
    vec3 tangentNormal = texture(normalTexture, t).xyz * 2.0 - 1.0;

    vec3 q1 = dFdx(fragWorldPos);
    vec3 q2 = dFdy(fragWorldPos);
    vec2 st1 = dFdx(fragTexcoord0);
    vec2 st2 = dFdy(fragTexcoord0);

    vec3 N = normalize(fragNormal);
    vec3 T = normalize(q1 * st2.t - q2 * st1.t);
    vec3 B = -normalize(cross(N, T));
    mat3 TBN = mat3(T, B, N);

    return normalize(TBN * tangentNormal);
  } else {
    return fragNormal;
  }
}

vec3 lightDirection(Light light) {
  if(light.type == 0 || light.type == 2) { // point and spot lights
    return normalize(light.position - fragWorldPos);
  } else if(light.type == 1) { // directional light
    return normalize(-light.direction);
  }

  return vec3(0.0); // should never be reached.
}

vec3 lightAttenuate(Light light) {
  float distance = length(light.position - fragWorldPos);
  float attenuation = 1.0 / (distance * distance);
  return light.color.rgb * attenuation;
}

vec3 lightRadiance(Light light, vec3 direction) {
  if(light.type == 0) { // point light
    return lightAttenuate(light);
  } else if(light.type == 1) { // directional light
    return light.color.rgb;
  } else if(light.type == 2) { // spot light
    float theta = dot(direction, normalize(-light.direction));
    // Spot lights only shine light in a specific conical area.
    // They have no effect outside of that area.
    if(theta > light.cutOff) {
      // Feather out the light as it approaches the edge of its cone.
      float intensity = (theta - light.cutOff) / (1.0 - light.cutOff);
      return lightAttenuate(light) * intensity;
    } else {
      return vec3(0.0);
    }
  }

  return vec3(0.0); // should never be reached.
}

// Useful resources I learned a lot from:
// https://learnopengl.com/PBR/Theory
// http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.html
void main(void) {
  // The unit vector pointing from the fragment position to the viewer
  // position.
  vec3 viewDirection = normalize(cameraPosition - fragWorldPos);
  float metallic = materialMetallic();
  float roughness = materialRoughness();
  vec3 normal = materialNormal();
  // The "raw" albedo has an alpha channel which we need to preserve
  // so that we can apply the desired alpha blending method at the
  // end, but it is completely ignored for lighting calculations.
  vec4 rawAlbedo = materialAlbedo();
  vec3 albedo = rawAlbedo.rgb;
  // The ambient occlusion factor affects the degree to which ambient
  // lighting has an affect on the fragment.  Each element of the
  // vector is in the range [0, 1].
  vec3 ambientOcclusion = materialOcclusion();
  // The color that the fragment relects at zero incidence.  In other
  // words, the color reflected when looking directly at the fragment.
  // A material that is fully metallic will fully express its albedo
  // color.  A material that isn't metallic at all will have a gray
  // initial color.  The grayscale value of 0.04 apparently achieves a
  // good result so that's what we do.  Any other metallic value is
  // somewhere between both colors, as calculated by a linear
  // interpolation.
  vec3 baseColor = mix(vec3(0.04), albedo, metallic);
  // We will accumulate the results of all lights (ambient, emissive,
  // and user-specified direct lights) into this color vector.
  vec3 color = vec3(0.0);

  // Apply direct lighting.
  for(int i = 0; i < MAX_LIGHTS; ++i)
  {
    Light light = lights[i];

    if(!light.enabled) {
      continue;
    }

    // The unit vector pointing from the fragment position to the
    // light position.
    vec3 direction = lightDirection(light);
    // The dot product of the surface normal and the light direction
    // gives a value in the range [0, 1] that tells us how directly
    // light is hitting the fragment. 0 means the normal and light
    // direction form a >= 90 degree angle and thus the light should
    // have no effect.  1 means the light is directly hitting the
    // surface and the light should be applied with full intensity.
    float incidenceFactor = posDot(normal, direction);
    // The intensity of the light.
    vec3 radiance = lightRadiance(light, direction);

    // Skip all the expensive math below if the light will have no
    // effect on the result.
    if(incidenceFactor == 0.0 || radiance == vec3(0.0)) {
      continue;
    }

    // The halfway vector is named as such because it is halfway
    // between the view direction and the light direction.  It is a
    // significant vector for microfacet lighting calculations.
    vec3 halfwayVector = normalize(viewDirection + direction);
    // Apply the Trowbridge-Reitz GGX normal distribution function.
    // This function approximates the percentage of microfacets that
    // are aligned with the halfway vector.  Smooth objects have a lot
    // of aligned microfacets over a small area, thus the viewer sees
    // a bright spot.  Rough objects have few aligned microfacets over
    // a larger area, thus it appears dull.
    float normalDistribution = distributionGGX(normal, halfwayVector, roughness);
    // Apply the geometry distribution function using Smith's method.
    // This function approximates how much light is reflected based on
    // the fragment's roughness.  Rougher objects reflect less light
    // because the microfacets of the surface obstruct or overshadow
    // the reflected light rays.
    float geoFactor = geometrySmith(normal, viewDirection, direction, roughness);
    // Compute the fresnel factor via Schlick's approximation to get
    // the specular reflection coeffecient, i.e. the percentage of
    // light that is reflected.  Since this is a microfacet lighting
    // model, we need to use the dot product of the halfway vector and
    // the view direction to get the cos(theta) value, according to:
    // https://en.wikipedia.org/wiki/Schlick%27s_approximation
    //
    // I also found this article useful:
    // http://psgraphics.blogspot.com/2020/03/fresnel-equations-schlick-approximation.html
    vec3 reflectionFactor = fresnelSchlick(posDot(halfwayVector, direction), baseColor);
    // Refracted light is the leftover light that hasn't been
    // reflected.  One additional complicating factor is that metallic
    // surfaces don't refract light, so we must scale the refraction
    // factor based on how metallic the fragment is.
    vec3 refractionFactor = (vec3(1.0) - reflectionFactor) * (1.0 - metallic);
    // The dot product of the surface normal and the view direction
    // gives a value in the range [0, 1] that tells us how directly
    // the viewer is looking at the fragment.  The effect of specular
    // lighting changes based on the angle from the viewer to the
    // surface.
    float viewFactor = posDot(normal, viewDirection);
    // Specular and diffuse lighting are processed together using the
    // Cook-Torrance bidirectional reflectance distribution function
    // (BRDF.)
    //
    // http://www.codinglabs.net/article_physically_based_rendering_cook_torrance.aspx
    vec3 cookTorranceNumerator = normalDistribution * reflectionFactor * geoFactor;
    float cookTorranceDenominator = 4.0 * viewFactor * incidenceFactor;
    // We need to be careful to avoid a division by zero error, such
    // as when the specular factor is 0, so we clamp the denominator
    // to some very small value to safeguard ourselves.
    vec3 specular = cookTorranceNumerator / max(cookTorranceDenominator, 0.0001);
    // Apply Lambertian reflectance to get the diffuse lighting
    // factor.  https://en.wikipedia.org/wiki/Lambertian_reflectance
    vec3 diffuse = refractionFactor * albedo / PI;
    // The final light value is the combination of diffuse and
    // specular light scaled by the intensity of the light source and
    // the angle of incidence (how directly the light hit the
    // fragment.)
    color += (diffuse + specular) * radiance * incidenceFactor;
  }

  // The emissive texture says which fragments emit light.  We simply
  // add this light value to the color accumulator.
  color += materialEmissive();
  // Apply simple ambient lighting.  The affect of the ambient light
  // is dampened by the ambient occlusion factor.
  //
  // TODO: Use image based lighting.
  color += ambientLightColor.rgb * albedo * ambientOcclusion;
  // Apply Reinhard tone mapping to convert our high dynamic range
  // color value to low dynamic range.  All of the lighting
  // calculations stacked on top of each other is likely to create
  // color channel values that exceed the [0, 1] range of OpenGL's
  // linear color space, i.e. high dynamic range.  If we did nothing,
  // the resulting color values would be clamped to that range and the
  // final image would be mostly white and washed out.
  color = toneMap(color);
  // Apply gamma correction.  The power of 2.2 is the rough average
  // gamma value of most monitors.  So, scaling the color by the
  // inverse, the power of 1/2.2, the color that people see on the
  // monitor is closer to what we intend to present with our original
  // linear color value.
  color = gammaCorrect(color);
  // Add alpha channel back in and apply the appropriate blend mode.
  // Yay, we're done!
  vec4 finalColor = applyAlpha(vec4(color, rawAlbedo.a));

#ifdef GLSL330
  fragColor = finalColor;
#else
  gl_FragColor = finalColor;
#endif
}