3.1 光照和材质 (Lighting and Materials)


文档摘要

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 光照和材质 (Lighting and Materials)

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) 的准备

在顶点着色器中,我们需要做以下准备工作:

  1. 传递顶点位置 (Position):这是最基本的操作,用于确定顶点在世界空间中的位置。

  2. 传递顶点法线 (Normal):法线向量垂直于顶点所在的表面,是计算光照的关键信息。我们需要确保法线向量在世界空间中也是正确的。

  3. 计算世界空间中的顶点位置和法线:如果模型变换(例如模型矩阵)不是统一缩放,直接使用模型空间的法线向量进行光照计算可能会产生错误的结果。因此,通常需要在顶点着色器中将顶点位置和法线转换到世界空间。对于法线向量,需要使用 法线矩阵 (Normal Matrix) 进行变换,以正确处理非均匀缩放和旋转。法线矩阵是模型矩阵的逆转置矩阵的子矩阵 (通常是 3x3)。

将顶点位置和法线传递到片元着色器,以便在片元着色器中进行光照计算。

3.1.3.2 片元着色器 (Fragment Shader) 的光照计算

在片元着色器中,我们需要执行以下步骤来计算每个片元的光照颜色:

  1. 接收顶点着色器传递的数据:包括世界空间中的顶点位置和法线。

  2. 定义光照参数:包括光源的位置、颜色、类型(例如,点光源、方向光、聚光灯)等。

  3. 定义材质属性:包括环境光颜色、漫反射颜色、镜面反射颜色、镜面反射指数等。

  4. 计算环境光成分:环境光的计算非常简单,直接将环境光颜色与材质的环境光颜色相乘即可。

  5. 计算漫反射成分

    • 计算从片元指向光源的 光线方向向量 (Light Direction)

    • 计算光线方向向量和表面法线的 点积 (Dot Product)。点积的结果表示光线方向和法线方向的夹角的余弦值。

    • 如果点积为负数,表示光线从表面背面照射,漫反射为 0。否则,漫反射强度与点积成正比。

    • 将漫反射强度与光源的颜色和材质的漫反射颜色相乘,得到漫反射颜色成分。

  6. 计算镜面反射成分

    • 计算 反射向量 (Reflection Vector):反射向量是光线方向向量关于表面法线的反射方向。可以使用 reflect() 函数计算。

    • 计算从片元指向观察者的 观察方向向量 (View Direction)

    • 计算反射向量和观察方向向量的 点积 (Dot Product)

    • 如果点积为负数,表示观察方向与反射方向相反,镜面反射为 0。否则,将点积值提高到 镜面反射指数 (Shininess) 次幂,模拟高光的锐利度。

    • 将镜面反射强度与光源的颜色和材质的镜面反射颜色相乘,得到镜面反射颜色成分。

  7. 将环境光、漫反射和镜面反射成分相加:得到最终的光照颜色。

  8. 应用材质的颜色 (可选):可以将计算出的光照颜色与材质的颜色进行某种混合(例如相乘),以实现更丰富的材质效果。

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; }

代码详解:

  1. 顶点着色器 (Vertex Shader):

    • aVertexPositionaVertexNormal:接收顶点位置和法线属性。

    • uModelMatrix, uViewMatrix, uProjectionMatrix:接收模型、视图和投影矩阵,用于坐标变换。

    • uNormalMatrix:接收法线矩阵,用于正确变换法线。

    • vPositionWorldvNormalWorld:将顶点位置和法线变换到世界空间,并传递给片元着色器。

  2. 片元着色器 (Fragment Shader):

    • vPositionWorldvNormalWorld:接收顶点着色器传递的世界空间位置和法线。

    • uAmbientColor, uDiffuseColor, uSpecularColor, uShininess:接收材质属性。

    • uLightDirection, uLightColor:接收定向光的光照参数。

    • main() 函数中,根据 Phong 光照模型计算环境光、漫反射和镜面反射成分,并将它们相加得到最终颜色。

  3. JavaScript 代码:

    • 初始化 WebGL 上下文和着色器程序。

    • 定义顶点数据(立方体顶点位置和法线)和索引数据。

    • 创建缓冲区并绑定顶点数据和索引数据。

    • render() 函数中:

      • 设置视图矩阵、投影矩阵和模型矩阵。

      • 计算法线矩阵。

      • 设置 Uniform 变量,传递矩阵、材质属性和光照参数到着色器。

      • 绘制立方体。

      • 使用 requestAnimationFrame() 创建动画循环。

运行代码:

index.htmlmain.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) 材质。


发布者: 作者: 转发
评论区 (0)
U