- 反照率:反照率(Albedo)纹理为每一个金属的纹素(Texel)(纹理像素)指定表面颜色或者基础反射率。这和我们之前使用过的漫反射纹理相当类似,不同的是所有光照信息都是由一个纹理中提取的。漫反射纹理的图像当中常常包含一些细小的阴影或者深色的裂纹,而反照率纹理中是不会有这些东西的。它应该只包含表面的颜色(或者折射吸收系数)。
- 法线:法线贴图使我们可以逐片段的指定独特的法线,来为表面制造出起伏不平的假象。
- 金属度:金属(Metallic)贴图逐个纹素的指定该纹素是不是金属质地的。
- 粗糙度:粗糙度(Roughness)贴图可以以纹素为单位指定某个表面有多粗糙。采样得来的粗糙度数值会影响一个表面的微平面统计学上的取向度。一个比较粗糙的表面会得到更宽阔更模糊的镜面反射(高光),而一个比较光滑的表面则会得到集中而清晰的镜面反射。某些PBR引擎预设采用的是对某些美术师来说更加直观的光滑度(Smoothness)贴图而非粗糙度贴图,不过这些数值在采样之时就马上用(1.0 – 光滑度)转换成了粗糙度。
- AO:环境光遮蔽(Ambient Occlusion)贴图或者说AO贴图为表面和周围潜在的几何图形指定了一个额外的阴影因子。
- 顶点着色器
export var vs_pbr =
`#version 300 es
precision mediump float;
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoords;
layout (location = 2) in vec3 aNormal;
out vec2 TexCoords;
out vec3 WorldPos;
out vec3 Normal;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
void main()
{
TexCoords = aTexCoords;
WorldPos = vec3(model * vec4(aPos, 1.0));
Normal = mat3(model) * aNormal;
gl_Position = projection * view * vec4(WorldPos, 1.0);
}`
- 片元着色器
export var fs_pbr =
`#version 300 es
precision mediump float;
out vec4 FragColor;
in vec2 TexCoords;
in vec3 WorldPos;
in vec3 Normal;
// material parameters
uniform sampler2D albedoMap;
uniform sampler2D normalMap;
uniform sampler2D metallicMap;
uniform sampler2D roughnessMap;
uniform sampler2D aoMap;
// lights
uniform vec3 lightPositions[4];
uniform vec3 lightColors[4];
uniform vec3 camPos;
const float PI = 3.14159265359;
// ----------------------------------------------------------------------------
// Easy trick to get tangent-normals to world-space to keep PBR code simplified.
// Don't worry if you don't get what's going on; you generally want to do normal
// mapping the usual way for performance anways; I do plan make a note of this
// technique somewhere later in the normal mapping tutorial.
vec3 getNormalFromMap()
{
vec3 tangentNormal = texture(normalMap, TexCoords).xyz * 2.0 - 1.0;
vec3 Q1 = dFdx(WorldPos);
vec3 Q2 = dFdy(WorldPos);
vec2 st1 = dFdx(TexCoords);
vec2 st2 = dFdy(TexCoords);
vec3 N = normalize(Normal);
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);
}
// ----------------------------------------------------------------------------
float DistributionGGX(vec3 N, vec3 H, float roughness)
{
float a = roughness*roughness;
float a2 = a*a;
float NdotH = max(dot(N, H), 0.0);
float NdotH2 = NdotH*NdotH;
float nom = a2;
float denom = (NdotH2 * (a2 - 1.0) + 1.0);
denom = PI * denom * denom;
return nom / denom;
}
// ----------------------------------------------------------------------------
float GeometrySchlickGGX(float NdotV, float roughness)
{
float r = (roughness + 1.0);
float k = (r*r) / 8.0;
float nom = NdotV;
float denom = NdotV * (1.0 - k) + k;
return nom / denom;
}
// ----------------------------------------------------------------------------
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
float ggx2 = GeometrySchlickGGX(NdotV, roughness);
float ggx1 = GeometrySchlickGGX(NdotL, roughness);
return ggx1 * ggx2;
}
// ----------------------------------------------------------------------------
vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
// ----------------------------------------------------------------------------
void main()
{
vec3 albedo = pow(texture(albedoMap, TexCoords).rgb, vec3(2.2));
float metallic = texture(metallicMap, TexCoords).r;
float roughness = texture(roughnessMap, TexCoords).r;
float ao = texture(aoMap, TexCoords).r;
vec3 N = getNormalFromMap();
vec3 V = normalize(camPos - WorldPos);
// calculate reflectance at normal incidence; if dia-electric (like plastic) use F0
// of 0.04 and if it's a metal, use the albedo color as F0 (metallic workflow)
vec3 F0 = vec3(0.04);
F0 = mix(F0, albedo, metallic);
// reflectance equation
vec3 Lo = vec3(0.0);
for(int i = 0; i < 4; ++i)
{
// calculate per-light radiance
vec3 L = normalize(lightPositions[i] - WorldPos);
vec3 H = normalize(V + L);
float distance = length(lightPositions[i] - WorldPos);
float attenuation = 1.0 / (distance * distance);
vec3 radiance = lightColors[i] * attenuation;
// Cook-Torrance BRDF
float NDF = DistributionGGX(N, H, roughness);
float G = GeometrySmith(N, V, L, roughness);
vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);
vec3 nominator = NDF * G * F;
float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001; // 0.001 to prevent divide by zero.
vec3 specular = nominator / denominator;
// kS is equal to Fresnel
vec3 kS = F;
// for energy conservation, the diffuse and specular light can't
// be above 1.0 (unless the surface emits light); to preserve this
// relationship the diffuse component (kD) should equal 1.0 - kS.
vec3 kD = vec3(1.0) - kS;
// multiply kD by the inverse metalness such that only non-metals
// have diffuse lighting, or a linear blend if partly metal (pure metals
// have no diffuse light).
kD *= 1.0 - metallic;
// scale light by NdotL
float NdotL = max(dot(N, L), 0.0);
// add to outgoing radiance Lo
Lo += (kD * albedo / PI + specular) * radiance * NdotL; // note that we already multiplied the BRDF by the Fresnel (kS) so we won't multiply by kS again
}
// ambient lighting (note that the next IBL tutorial will replace
// this ambient lighting with environment lighting).
vec3 ambient = vec3(0.03) * albedo * ao;
vec3 color = ambient + Lo;
// HDR tonemapping
color = color / (color + vec3(1.0));
// gamma correct
color = pow(color, vec3(1.0/2.2));
FragColor = vec4(color, 1.0);
}`
// This code is a javascript translation of code originally written by Joey de Vries under the CC BY-NC 4.0 licence.
// For more information please visit https://learnopengl.com/About
import { vec3, mat4 } from '../../../math/glmatrix/index.js';
import { vs_pbr, fs_pbr } from '../../js/Ch44/shaders/2/index.js';
import { Shader } from '../../js/common/Shader.js';
import { Mouse } from '../../js/common/Mouse.js';
import { KeyInput } from '../../js/common/KeyInput.js';
import { Camera, CameraMovement } from '../../js/common/Camera.js';
import { Sphere } from '../../js/geometry/VertexObjects.js';
const sizeFloat = 4;
let canvas = document.createElement('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.body.appendChild(canvas);
var gl = canvas.getContext('webgl2', { antialias: true });
if (!gl) {
console.log("WebGL 2 needed");
}
const GLFW_KEY_W = 'w', GLFW_KEY_S = 's', GLFW_KEY_A = 'a', GLFW_KEY_D = 'd', GLFW_KEY_1 = '1', GLFW_KEY_2 = '2', GLFW_KEY_Q = 'q', GLFW_KEY_E = 'e', GLFW_KEY_SPACE = ' ';
let keyInput = new KeyInput({
GLFW_KEY_W, GLFW_KEY_S, GLFW_KEY_A, GLFW_KEY_D,
GLFW_KEY_1, GLFW_KEY_2, GLFW_KEY_Q, GLFW_KEY_E,
GLFW_KEY_SPACE
});
let shader = null;
let projection = mat4.create(), view = mat4.create();
let model = mat4.create();
let lightPositions = null;
let lightColors = null;
const nrRows = 7;
const nrColumns = 7;
const spacing = 2.5;
let sphereVAO = null;
let sphere;
let albedo = null;
let normal = null;
let metallic = null;
let roughness = null;
let ao = null;
let camera = new Camera(vec3.fromValues(0.0, 0.0, 5.0), vec3.fromValues(0.0, 1.0, 0.0));
mat4.perspective(projection, (camera.Zoom) * Math.PI / 180, canvas.width / canvas.height, 0.1, 100.0);
window.addEventListener("resize", () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
gl.viewport(0, 0, window.innerWidth, window.innerHeight);
mat4.perspective(projection, (camera.Zoom) * Math.PI / 180, window.innerWidth / window.innerHeight, 0.1, 100.0);
});
let deltaTime = 0.0;
let lastFrame = 0.0;
let mouse = new Mouse();
mouse.moveCallback = mouse_move_callback;
mouse.scrollCallback = mouse_scroll_callback;
let main = function () {
gl.enable(gl.DEPTH_TEST);
lightPositions = new Float32Array([
-10.0, 10.0, 10.0,
10.0, 10.0, 10.0,
-10.0, -10.0, 10.0,
10.0, -10.0, 10.0
]);
lightColors = new Float32Array([
300.0, 300.0, 300.0,
300.0, 300.0, 300.0,
300.0, 300.0, 300.0,
300.0, 300.0, 300.0
]);
shader = new Shader(gl, vs_pbr, fs_pbr);
shader.use(gl);
shader.setInt(gl, "albedoMap", 0);
shader.setInt(gl, "normalMap", 1);
shader.setInt(gl, "metallicMap", 2);
shader.setInt(gl, "roughnessMap", 3);
shader.setInt(gl, "aoMap", 4);
albedo = loadTexture("../../textures/rustedIron/albedo.png", 4, false);
normal = loadTexture("../../textures/rustedIron/normal.png", 3, false);
metallic = loadTexture("../../textures/rustedIron/metallic.png", 1, false);
roughness = loadTexture("../../textures/rustedIron/roughness.png", 1, false);
ao = loadTexture("../../textures/rustedIron/ao.png", 3, false);
animate();
}();
function animate() {
render();
requestAnimationFrame(animate);
}
function render() {
let currentFrame = performance.now();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
processInput();
gl.clearColor(0.5, 0.5, 0.5, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
shader.use(gl);
gl.uniformMatrix4fv(gl.getUniformLocation(shader.programId, "projection"), false, projection);
view = camera.GetViewMatrix();
gl.uniformMatrix4fv(gl.getUniformLocation(shader.programId, "view"), false, view);
gl.uniform3f(gl.getUniformLocation(shader.programId, "camPos"), camera.Position[0], camera.Position[1], camera.Position[2]);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, albedo);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, normal);
gl.activeTexture(gl.TEXTURE2);
gl.bindTexture(gl.TEXTURE_2D, metallic);
gl.activeTexture(gl.TEXTURE3);
gl.bindTexture(gl.TEXTURE_2D, roughness);
gl.activeTexture(gl.TEXTURE4);
gl.bindTexture(gl.TEXTURE_2D, ao);
gl.uniform3fv(gl.getUniformLocation(shader.programId, "lightPositions"), lightPositions);
gl.uniform3fv(gl.getUniformLocation(shader.programId, "lightColors"), lightColors);
mat4.identity(model);
for (let row = 0; row < nrRows; ++row) {
for (let col = 0; col < nrColumns; ++col) {
mat4.identity(model);
mat4.translate(model, model, vec3.fromValues((col - (nrColumns / 2)) * spacing, (row - (nrRows / 2)) * spacing, 0.0));
gl.uniformMatrix4fv(gl.getUniformLocation(shader.programId, "model"), false, model);
renderSphere();
}
}
for (let i = 0; i < 4; ++i) {
let newPos = vec3.fromValues(lightPositions[3 * i], lightPositions[3 * i + 1], lightPositions[3 * i + 2]);
mat4.identity(model);
mat4.translate(model, model, newPos);
mat4.scale(model, model, vec3.fromValues(0.5, 0.5, 0.5));
gl.uniformMatrix4fv(gl.getUniformLocation(shader.programId, "model"), false, model);
renderSphere();
}
}
function processInput() {
const GLFW_PRESS = true;
const GLFW_RELEASE = false;
if (keyInput.isDown(GLFW_KEY_W) == GLFW_PRESS)
camera.ProcessKeyboard(CameraMovement.FORWARD, deltaTime);
if (keyInput.isDown(GLFW_KEY_S) == GLFW_PRESS)
camera.ProcessKeyboard(CameraMovement.BACKWARD, deltaTime);
if (keyInput.isDown(GLFW_KEY_A) == GLFW_PRESS)
camera.ProcessKeyboard(CameraMovement.LEFT, deltaTime);
if (keyInput.isDown(GLFW_KEY_D) == GLFW_PRESS)
camera.ProcessKeyboard(CameraMovement.RIGHT, deltaTime);
}
function mouse_move_callback(xoffset, yoffset, buttonID) {
if (buttonID == 1)
camera.ProcessMouseMovement(xoffset, yoffset);
}
function mouse_scroll_callback(yoffset) {
camera.ProcessMouseScroll(yoffset);
}
function renderSphere() {
if (sphereVAO == null) {
sphere = new Sphere(64, 64);
sphereVAO = gl.createVertexArray();
let vbo = gl.createBuffer();
let ebo = gl.createBuffer();
gl.bindVertexArray(sphereVAO);
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, sphere.vertices, gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, sphere.indices, gl.STATIC_DRAW);
let stride = (3 + 2 + 3) * sizeFloat;
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, stride, 0);
gl.enableVertexAttribArray(1);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, stride, (6 * sizeFloat));
gl.enableVertexAttribArray(2);
gl.vertexAttribPointer(2, 3, gl.FLOAT, false, stride, (3 * sizeFloat));
}
gl.bindVertexArray(sphereVAO);
gl.drawElements(gl.TRIANGLES, sphere.indices.length, gl.UNSIGNED_SHORT, 0);
}
function loadTexture(url, nrComponents, gammaCorrection) {
const textureID = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, textureID);
const level = 0;
const srcType = gl.UNSIGNED_BYTE;
const pixel = new Uint8Array([0, 0, 255, 255]);
gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA, 1, 1, 0, gl.RGBA, srcType, pixel);
const image = new Image();
image.onload = function () {
let internalFormat;
let dataFormat;
if (nrComponents == 1) {
dataFormat = gl.RED;
internalFormat = gl.R8;
}
else if (nrComponents == 3) {
internalFormat = gammaCorrection ? gl.SRGB8 : gl.RGB;
dataFormat = gl.RGB;
}
else if (nrComponents == 4) {
internalFormat = gammaCorrection ? gl.SRGB8_ALPHA8 : gl.RGBA;
dataFormat = gl.RGBA;
}
gl.bindTexture(gl.TEXTURE_2D, textureID);
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, image.naturalWidth, image.naturalHeight, 0, dataFormat, gl.UNSIGNED_BYTE, image);
gl.generateMipmap(gl.TEXTURE_2D);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
};
image.src = url;
return textureID;
}
//# sourceMappingURL=lightingTextured.js.map