3.1 光照和材质 (Lighting and Materials) 3.1 WebGL 光照和材质 (Lighting and Materials) 3.1.1 光照 (Lighting) 的基本概念 光照模拟了现实世界中光线与物体表面的相互作用,使我们能够感知物体的形状、深度和空间关系。在 WebGL 中,我们通常使用 光照模型 (Lighting Model) 来近似这种物理过程。最常用的光照模型是基于 Phong 反射模型 (Phong Reflection Model) 的变体。 Phong 反射模型将光线与表面的交互分解为三个主要成分: 环境光 (Ambient Light):模拟来自环境的间接光照,它均匀地照亮场景中的所有物体,没有明确的方向。
3.1 WebGL 光照和材质 (Lighting and Materials)
3.1.1 光照 (Lighting) 的基本概念
光照模拟了现实世界中光线与物体表面的相互作用,使我们能够感知物体的形状、深度和空间关系。在 WebGL 中,我们通常使用 光照模型 (Lighting Model) 来近似这种物理过程。最常用的光照模型是基于 Phong 反射模型 (Phong Reflection Model) 的变体。
Phong 反射模型将光线与表面的交互分解为三个主要成分:
环境光 (Ambient Light):模拟来自环境的间接光照,它均匀地照亮场景中的所有物体,没有明确的方向。环境光确保即使在阴影区域也能看到物体的基本颜色,避免完全的黑暗。
漫反射光 (Diffuse Light):模拟来自光源的直接光照,并被表面均匀地散射到各个方向。漫反射光的强度取决于光线方向和表面法线的夹角。当光线垂直于表面时,漫反射最强;当光线平行于表面时,漫反射为零。漫反射光是物体主要颜色的来源。
镜面反射光 (Specular Light):模拟光滑表面(如金属或抛光塑料)反射光线时产生的高光。镜面反射光的方向性很强,只有当观察方向接近反射方向时才能看到高光。镜面反射光的强度取决于光线方向、表面法线和观察方向,以及表面的 镜面反射指数 (Shininess),该指数控制高光的大小和锐利度。
为了更清晰地理解这三个光照成分,我们可以使用 Mermaid 的 graph TD 图来可视化它们之间的关系:
3.1.2 材质 (Materials) 的概念
材质描述了物体表面与光线交互的方式。不同的材质会反射、吸收和散射不同比例的光线,从而产生不同的视觉效果。在 WebGL 中,我们通过定义材质的属性来模拟不同的材质,例如:
环境光颜色 (Ambient Color):材质对环境光的反射率。
漫反射颜色 (Diffuse Color):材质对漫反射光的反射率,通常决定了物体的基本颜色。
镜面反射颜色 (Specular Color):材质对镜面反射光的反射率,决定了高光的颜色和强度。
镜面反射指数 (Shininess):材质的表面光滑度,影响高光的大小和锐利度。
通过调整这些材质属性,我们可以模拟各种不同的表面,例如金属、塑料、木材、石头等等。
3.1.3 WebGL 中光照和材质的实现
在 WebGL 中,光照和材质的计算通常在 片元着色器 (Fragment Shader) 中完成。我们需要将光照信息(如光源位置、颜色、类型)和材质属性 (如颜色、反射率、光滑度) 传递给着色器,然后在着色器中根据光照模型计算每个片元的最终颜色。
3.1.3.1 顶点着色器 (Vertex Shader) 的准备
在顶点着色器中,我们需要做以下准备工作:
传递顶点位置 (Position):这是最基本的操作,用于确定顶点在世界空间中的位置。
传递顶点法线 (Normal):法线向量垂直于顶点所在的表面,是计算光照的关键信息。我们需要确保法线向量在世界空间中也是正确的。
计算世界空间中的顶点位置和法线:如果模型变换(例如模型矩阵)不是统一缩放,直接使用模型空间的法线向量进行光照计算可能会产生错误的结果。因此,通常需要在顶点着色器中将顶点位置和法线转换到世界空间。对于法线向量,需要使用 法线矩阵 (Normal Matrix) 进行变换,以正确处理非均匀缩放和旋转。法线矩阵是模型矩阵的逆转置矩阵的子矩阵 (通常是 3x3)。
将顶点位置和法线传递到片元着色器,以便在片元着色器中进行光照计算。
3.1.3.2 片元着色器 (Fragment Shader) 的光照计算
在片元着色器中,我们需要执行以下步骤来计算每个片元的光照颜色:
接收顶点着色器传递的数据:包括世界空间中的顶点位置和法线。
定义光照参数:包括光源的位置、颜色、类型(例如,点光源、方向光、聚光灯)等。
定义材质属性:包括环境光颜色、漫反射颜色、镜面反射颜色、镜面反射指数等。
计算环境光成分:环境光的计算非常简单,直接将环境光颜色与材质的环境光颜色相乘即可。
计算漫反射成分:
计算从片元指向光源的 光线方向向量 (Light Direction)。
计算光线方向向量和表面法线的 点积 (Dot Product)。点积的结果表示光线方向和法线方向的夹角的余弦值。
如果点积为负数,表示光线从表面背面照射,漫反射为 0。否则,漫反射强度与点积成正比。
将漫反射强度与光源的颜色和材质的漫反射颜色相乘,得到漫反射颜色成分。
计算镜面反射成分:
计算 反射向量 (Reflection Vector):反射向量是光线方向向量关于表面法线的反射方向。可以使用 reflect() 函数计算。
计算从片元指向观察者的 观察方向向量 (View Direction)。
计算反射向量和观察方向向量的 点积 (Dot Product)。
如果点积为负数,表示观察方向与反射方向相反,镜面反射为 0。否则,将点积值提高到 镜面反射指数 (Shininess) 次幂,模拟高光的锐利度。
将镜面反射强度与光源的颜色和材质的镜面反射颜色相乘,得到镜面反射颜色成分。
将环境光、漫反射和镜面反射成分相加:得到最终的光照颜色。
应用材质的颜色 (可选):可以将计算出的光照颜色与材质的颜色进行某种混合(例如相乘),以实现更丰富的材质效果。
3.1.4 代码实践:基础 Phong 光照模型
下面是一个简单的 WebGL 代码示例,演示如何实现基础的 Phong 光照模型。
HTML 文件 (index.html):
<!DOCTYPE html> <html> <head> <title>WebGL Lighting</title> <style> body { margin: 0; } canvas { display: block; } </style> </head> <body> <canvas id="myCanvas"></canvas> <script src="main.js"></script> </body> </html>
JavaScript 文件 (main.js):
const canvas = document.getElementById('myCanvas'); const gl = canvas.getContext('webgl'); if (!gl) { alert('WebGL not supported!'); } // 顶点着色器代码 const vsSource = ` attribute vec4 aVertexPosition; attribute vec3 aVertexNormal; uniform mat4 uModelMatrix; uniform mat4 uViewMatrix; uniform mat4 uProjectionMatrix; uniform mat4 uNormalMatrix; // 法线矩阵 varying vec3 vPositionWorld; varying vec3 vNormalWorld; void main() { vPositionWorld = (uModelMatrix * aVertexPosition).xyz; vNormalWorld = normalize((uNormalMatrix * vec4(aVertexNormal, 0.0)).xyz); // 使用法线矩阵变换法线并归一化 gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * aVertexPosition; } `; // 片元着色器代码 const fsSource = ` precision mediump float; varying vec3 vPositionWorld; varying vec3 vNormalWorld; uniform vec3 uAmbientColor; uniform vec3 uDiffuseColor; uniform vec3 uSpecularColor; uniform float uShininess; uniform vec3 uLightDirection; // 定向光方向 uniform vec3 uLightColor; void main() { vec3 normal = normalize(vNormalWorld); vec3 lightDir = normalize(uLightDirection); vec3 viewDir = normalize(-vPositionWorld); // 假设相机在原点 // 环境光 vec3 ambient = uAmbientColor * uLightColor; // 漫反射 float diff = max(dot(normal, lightDir), 0.0); vec3 diffuse = diff * uDiffuseColor * uLightColor; // 镜面反射 vec3 reflectDir = reflect(-lightDir, normal); float spec = pow(max(dot(viewDir, reflectDir), 0.0), uShininess); vec3 specular = spec * uSpecularColor * uLightColor; vec3 finalColor = ambient + diffuse + specular; gl_FragColor = vec4(finalColor, 1.0); } `; // 初始化着色器程序 function initShaderProgram(gl, vsSource, fsSource) { const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource); const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource); const shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { alert('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram)); return null; } return shaderProgram; } // 创建着色器 function createShader(gl, type, source) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } return shader; } const shaderProgram = initShaderProgram(gl, vsSource, fsSource); const programInfo = { program: shaderProgram, attribLocations: { vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'), vertexNormal: gl.getAttribLocation(shaderProgram, 'aVertexNormal'), }, uniformLocations: { modelMatrix: gl.getUniformLocation(shaderProgram, 'uModelMatrix'), viewMatrix: gl.getUniformLocation(shaderProgram, 'uViewMatrix'), projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'), normalMatrix: gl.getUniformLocation(shaderProgram, 'uNormalMatrix'), // 法线矩阵 uniform ambientColor: gl.getUniformLocation(shaderProgram, 'uAmbientColor'), diffuseColor: gl.getUniformLocation(shaderProgram, 'uDiffuseColor'), specularColor: gl.getUniformLocation(shaderProgram, 'uSpecularColor'), shininess: gl.getUniformLocation(shaderProgram, 'uShininess'), lightDirection: gl.getUniformLocation(shaderProgram, 'uLightDirection'), lightColor: gl.getUniformLocation(shaderProgram, 'uLightColor'), }, }; // 几何数据 (立方体) const cubeVertices = [ // Front face -1.0, -1.0, 1.0, 0.0, 0.0, 1.0, // Vertex 0: bottom left front 1.0, -1.0, 1.0, 0.0, 0.0, 1.0, // Vertex 1: bottom right front 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, // Vertex 2: top right front -1.0, 1.0, 1.0, 0.0, 0.0, 1.0, // Vertex 3: top left front // Back face -1.0, -1.0, -1.0, 0.0, 0.0, -1.0, // Vertex 4: bottom left back -1.0, 1.0, -1.0, 0.0, 0.0, -1.0, // Vertex 5: top left back 1.0, 1.0, -1.0, 0.0, 0.0, -1.0, // Vertex 6: top right back 1.0, -1.0, -1.0, 0.0, 0.0, -1.0, // Vertex 7: bottom right back // Top face -1.0, 1.0, -1.0, 0.0, 1.0, 0.0, // Vertex 8: top left back -1.0, 1.0, 1.0, 0.0, 1.0, 0.0, // Vertex 9: top left front 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, // Vertex 10: top right front 1.0, 1.0, -1.0, 0.0, 1.0, 0.0, // Vertex 11: top right back // Bottom face -1.0, -1.0, -1.0, 0.0, -1.0, 0.0, // Vertex 12: bottom left back 1.0, -1.0, -1.0, 0.0, -1.0, 0.0, // Vertex 13: bottom right back 1.0, -1.0, 1.0, 0.0, -1.0, 0.0, // Vertex 14: bottom right front -1.0, -1.0, 1.0, 0.0, -1.0, 0.0, // Vertex 15: bottom left front // Right face 1.0, -1.0, -1.0, 1.0, 0.0, 0.0, // Vertex 16: bottom right back 1.0, 1.0, -1.0, 1.0, 0.0, 0.0, // Vertex 17: top right back 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, // Vertex 18: top right front 1.0, -1.0, 1.0, 1.0, 0.0, 0.0, // Vertex 19: bottom right front // Left face -1.0, -1.0, -1.0, -1.0, 0.0, 0.0, // Vertex 20: bottom left back -1.0, -1.0, 1.0, -1.0, 0.0, 0.0, // Vertex 21: bottom left front -1.0, 1.0, 1.0, -1.0, 0.0, 0.0, // Vertex 22: top left front -1.0, 1.0, -1.0, -1.0, 0.0, 0.0 // Vertex 23: top left back ]; const cubeIndices = [ 0, 1, 2, 0, 2, 3, // front 4, 5, 6, 4, 6, 7, // back 8, 9, 10, 8, 10, 11, // top 12, 13, 14, 12, 14, 15, // bottom 16, 17, 18, 16, 18, 19, // right 20, 21, 22, 20, 22, 23 // left ]; // 初始化缓冲区 function initBuffers(gl) { // 创建顶点位置缓冲区 const positionBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); const positions = cubeVertices.map( (v, index) => index % 6 < 3 ? v : null).filter(v => v !== null); // 提取顶点位置 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); // 创建顶点法线缓冲区 const normalBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer); const vertexNormals = cubeVertices.map( (v, index) => index % 6 >= 3 ? v : null).filter(v => v !== null); // 提取顶点法线 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexNormals), gl.STATIC_DRAW); // 创建索引缓冲区 const indexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeIndices), gl.STATIC_DRAW); return { position: positionBuffer, normal: normalBuffer, indices: indexBuffer, }; } const buffers = initBuffers(gl); // 渲染函数 function render(now) { now *= 0.001; // convert to seconds resizeCanvasToDisplaySize(gl.canvas); gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clearDepth(1.0); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); const fieldOfView = 45 * Math.PI / 180; // in radians const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; const zNear = 0.1; const zFar = 100.0; const projectionMatrix = mat4.create(); mat4.perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar); const viewMatrix = mat4.create(); mat4.translate(viewMatrix, // destination matrix viewMatrix, // matrix to translate [0, 0, -6]); // amount to translate mat4.rotate(viewMatrix, // destination matrix viewMatrix, // matrix to rotate now, // amount to rotate in radians [0, 1, 0]); // axis to rotate around const modelMatrix = mat4.create(); mat4.rotate(modelMatrix, // destination matrix modelMatrix, // matrix to rotate now * 0.5, // amount to rotate in radians [0, 1, 0]); // axis to rotate around // 计算法线矩阵 (模型矩阵的逆转置矩阵的子矩阵) const normalMatrix = mat4.create(); mat4.invert(normalMatrix, modelMatrix); mat4.transpose(normalMatrix, normalMatrix); // 设置着色器程序 gl.useProgram(programInfo.program); // 设置顶点位置属性 { const numComponents = 3; const type = gl.FLOAT; const normalize = false; const stride = 6 * Float32Array.BYTES_PER_ELEMENT; // 步长,每个顶点属性组的大小 const offset = 0; // 偏移量 gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position); gl.vertexAttribPointer( programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); } // 设置顶点法线属性 { const numComponents = 3; const type = gl.FLOAT; const normalize = false; const stride = 6 * Float32Array.BYTES_PER_ELEMENT; const offset = 3 * Float32Array.BYTES_PER_ELEMENT; // 法线数据偏移3个float gl.bindBuffer(gl.ARRAY_BUFFER, buffers.normal); gl.vertexAttribPointer( programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); } // 设置 Uniform 变量 gl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix, false, projectionMatrix); gl.uniformMatrix4fv(programInfo.uniformLocations.viewMatrix, false, viewMatrix); gl.uniformMatrix4fv(programInfo.uniformLocations.modelMatrix, false, modelMatrix); gl.uniformMatrix4fv(programInfo.uniformLocations.normalMatrix, false, normalMatrix); // 传递法线矩阵 gl.uniform3fv(programInfo.uniformLocations.ambientColor, [0.2, 0.2, 0.2]); // 环境光颜色 gl.uniform3fv(programInfo.uniformLocations.diffuseColor, [0.8, 0.8, 0.8]); // 漫反射颜色 gl.uniform3fv(programInfo.uniformLocations.specularColor, [1.0, 1.0, 1.0]); // 镜面反射颜色 gl.uniform1f(programInfo.uniformLocations.shininess, 32.0); // 镜面反射指数 gl.uniform3fv(programInfo.uniformLocations.lightDirection, [0.5, 0.5, 1.0]); // 定向光方向 gl.uniform3fv(programInfo.uniformLocations.lightColor, [1.0, 1.0, 1.0]); // 光源颜色 // 绘制 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.indices); const vertexCount = cubeIndices.length; const type = gl.UNSIGNED_SHORT; const offset = 0; gl.drawElements(gl.TRIANGLES, vertexCount, type, offset); requestAnimationFrame(render); } requestAnimationFrame(render); // 辅助函数:调整画布大小以适应显示 function resizeCanvasToDisplaySize(canvas) { const displayWidth = canvas.clientWidth; const displayHeight = canvas.clientHeight; const needResize = canvas.width !== displayWidth || canvas.height !== displayHeight; if (needResize) { canvas.width = displayWidth; canvas.height = displayHeight; } return needResize; }
代码详解:
顶点着色器 (Vertex Shader):
aVertexPosition 和 aVertexNormal:接收顶点位置和法线属性。
uModelMatrix, uViewMatrix, uProjectionMatrix:接收模型、视图和投影矩阵,用于坐标变换。
uNormalMatrix:接收法线矩阵,用于正确变换法线。
vPositionWorld 和 vNormalWorld:将顶点位置和法线变换到世界空间,并传递给片元着色器。
片元着色器 (Fragment Shader):
vPositionWorld 和 vNormalWorld:接收顶点着色器传递的世界空间位置和法线。
uAmbientColor, uDiffuseColor, uSpecularColor, uShininess:接收材质属性。
uLightDirection, uLightColor:接收定向光的光照参数。
在 main() 函数中,根据 Phong 光照模型计算环境光、漫反射和镜面反射成分,并将它们相加得到最终颜色。
JavaScript 代码:
初始化 WebGL 上下文和着色器程序。
定义顶点数据(立方体顶点位置和法线)和索引数据。
创建缓冲区并绑定顶点数据和索引数据。
在 render() 函数中:
设置视图矩阵、投影矩阵和模型矩阵。
计算法线矩阵。
设置 Uniform 变量,传递矩阵、材质属性和光照参数到着色器。
绘制立方体。
使用 requestAnimationFrame() 创建动画循环。
运行代码:
将 index.html 和 main.js 保存到同一个目录下,然后在浏览器中打开 index.html 文件,你将看到一个旋转的、带有 Phong 光照的彩色立方体。
3.1.5 光源类型
除了示例中使用的 定向光 (Directional Light) 之外,WebGL 中常用的光源类型还包括:
点光源 (Point Light):从一个点向所有方向发射光线。点光源需要指定光源的位置。光照强度会随着距离衰减。
聚光灯 (Spot Light):类似于手电筒,从一个点向特定方向的锥形区域发射光线。聚光灯需要指定光源的位置、方向、锥角和衰减参数。
要实现不同类型的光源,需要在片元着色器中修改光照计算部分,根据光源类型计算光线方向、距离衰减等。
例如,对于点光源,光线方向向量需要从片元位置指向光源位置,并且需要根据片元到光源的距离计算光照衰减。
3.1.6 材质的更多属性
除了示例中使用的基本材质属性外,还可以添加更多材质属性来模拟更复杂的表面,例如:
自发光颜色 (Emissive Color):模拟物体自身发光的效果。
反射率贴图 (Reflection Map) 和 折射率 (Refraction Index):用于模拟镜面反射和折射效果。
粗糙度 (Roughness) 和 金属度 (Metalness):用于实现基于物理的渲染 (PBR) 材质。