第三章:WebGL 高级技术


文档摘要

第三章:WebGL 高级技术 第三章:WebGL 高级技术 引言 在掌握了 WebGL 的基础知识,例如顶点着色器、片段着色器、缓冲区对象和纹理之后,我们已经具备了渲染基本 3D 场景的能力。然而,为了创建更具沉浸感、更逼真、性能更优的 WebGL 应用,我们需要深入学习和应用一些高级技术。本章将带领您探索 WebGL 的高级领域,涵盖光照、纹理、几何体操作、后期处理以及性能优化等方面。通过学习本章内容,您将能够突破 WebGL 基础的限制,构建更加复杂和精美的 3D 世界。 3.1 高级光照技术 光照是 3D 渲染中至关重要的组成部分,它赋予场景深度、形状和真实感。基础光照模型,如漫反射和镜面反射,在某些情况下可能显得过于简单。高级光照技术旨在模拟更真实的光照效果,提升视觉体验。 3.1.

第三章:WebGL 高级技术

第三章:WebGL 高级技术

引言

在掌握了 WebGL 的基础知识,例如顶点着色器、片段着色器、缓冲区对象和纹理之后,我们已经具备了渲染基本 3D 场景的能力。然而,为了创建更具沉浸感、更逼真、性能更优的 WebGL 应用,我们需要深入学习和应用一些高级技术。本章将带领您探索 WebGL 的高级领域,涵盖光照、纹理、几何体操作、后期处理以及性能优化等方面。通过学习本章内容,您将能够突破 WebGL 基础的限制,构建更加复杂和精美的 3D 世界。

3.1 高级光照技术

光照是 3D 渲染中至关重要的组成部分,它赋予场景深度、形状和真实感。基础光照模型,如漫反射和镜面反射,在某些情况下可能显得过于简单。高级光照技术旨在模拟更真实的光照效果,提升视觉体验。

3.1.1 阴影

阴影是增强场景真实感的重要因素。在 WebGL 中实现阴影效果,最常用的技术是阴影贴图 (Shadow Mapping)

阴影贴图原理:

阴影贴图的核心思想是从光源的角度渲染场景的深度信息,生成一张深度纹理(阴影贴图)。在实际渲染场景时,对于每个片段,我们将其转换到光源空间,并与阴影贴图中的深度值进行比较。如果片段的深度值大于阴影贴图中的深度值,则说明该片段被遮挡,处于阴影中。

代码实践 (阴影贴图):

以下是一个简化的阴影贴图实现流程,我们将使用两个渲染通道:

  1. 阴影贴图渲染通道 (Shadow Pass):

    • 设置一个帧缓冲对象 (Framebuffer Object, FBO) 和一个深度纹理作为阴影贴图。

    • 从光源的角度设置相机 (Light Space Transform)。

    • 渲染场景,仅输出深度信息到阴影贴图。

    // 创建阴影贴图纹理 const shadowMapTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, shadowMapTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT32F, shadowMapSize, shadowMapSize, 0, gl.DEPTH_COMPONENT, gl.FLOAT, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // 创建帧缓冲对象 const shadowMapFBO = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, shadowMapFBO); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, shadowMapTexture, 0); gl.drawBuffers([gl.NONE]); // 不需要颜色附件 gl.readBuffer(gl.NONE); // 不需要读取颜色 // ... (设置光源相机矩阵 - lightSpaceMatrix) ... // 阴影通道渲染 gl.bindFramebuffer(gl.FRAMEBUFFER, shadowMapFBO); gl.viewport(0, 0, shadowMapSize, shadowMapSize); gl.clear(gl.DEPTH_BUFFER_BIT); // 使用阴影着色器渲染场景,只渲染深度 renderScene(shadowShader, lightSpaceMatrix); // renderScene 函数需要根据实际场景实现 gl.bindFramebuffer(gl.FRAMEBUFFER, null); // 恢复默认帧缓冲

    阴影着色器 (顶点着色器 - shadow.vert):

    #version 300 es in vec3 aVertexPosition; uniform mat4 uLightSpaceMatrix; uniform mat4 uModelMatrix; void main() { gl_Position = uLightSpaceMatrix * uModelMatrix * vec4(aVertexPosition, 1.0); }

    阴影着色器 (片段着色器 - shadow.frag):

    #version 300 es precision highp float; void main() { // 片段着色器在阴影通道中可以为空,因为我们只关心深度缓冲区 }
  2. 主渲染通道 (Main Pass):

    • 正常渲染场景。

    • 在片段着色器中,将片段的世界坐标转换到光源空间。

    • 使用转换后的坐标采样阴影贴图,获取阴影信息。

    • 根据阴影信息调整片段的颜色。

    // ... (主渲染通道设置,相机、场景渲染等) ... // 绑定阴影贴图纹理到纹理单元 gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, shadowMapTexture); // ... (主渲染通道渲染场景,使用主着色器) ... renderScene(mainShader, viewMatrix, projectionMatrix); // renderScene 函数需要根据实际场景实现

    主着色器 (顶点着色器 - main.vert):

    #version 300 es in vec3 aVertexPosition; in vec3 aVertexNormal; uniform mat4 uModelMatrix; uniform mat4 uViewMatrix; uniform mat4 uProjectionMatrix; uniform mat4 uLightSpaceMatrix; out vec3 vNormal; out vec4 vShadowCoord; out vec3 vWorldPosition; void main() { vWorldPosition = vec3(uModelMatrix * vec4(aVertexPosition, 1.0)); gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * vec4(aVertexPosition, 1.0); vNormal = normalize(mat3(transpose(inverse(uModelMatrix))) * aVertexNormal); vShadowCoord = uLightSpaceMatrix * uModelMatrix * vec4(aVertexPosition, 1.0); }

    主着色器 (片段着色器 - main.frag):

    #version 300 es precision highp float; in vec3 vNormal; in vec4 vShadowCoord; in vec3 vWorldPosition; uniform vec3 uLightDirection; uniform vec3 uViewPosition; uniform sampler2D shadowMap; out vec4 fragColor; float calculateShadow(vec4 shadowCoord) { vec3 projCoords = shadowCoord.xyz / shadowCoord.w; projCoords = projCoords * 0.5 + 0.5; // 归一化到 [0, 1] if(projCoords.z > 1.0) return 0.0; // 超出阴影贴图范围 float shadowMapDepth = texture(shadowMap, projCoords.xy).r; float fragmentDepth = projCoords.z; float shadowFactor = 0.0; if(fragmentDepth > shadowMapDepth) { shadowFactor = 0.7; // 阴影强度 } return shadowFactor; } void main() { vec3 normal = normalize(vNormal); vec3 lightDir = normalize(-uLightDirection); vec3 viewDir = normalize(uViewPosition - vWorldPosition); vec3 diffuseColor = vec3(0.8, 0.8, 0.8); // 漫反射颜色 // 漫反射光照 float diffuseStrength = max(dot(normal, lightDir), 0.0); vec3 diffuse = diffuseStrength * diffuseColor; // 环境光照 vec3 ambient = vec3(0.2, 0.2, 0.2); // 计算阴影 float shadow = calculateShadow(vShadowCoord); vec3 finalColor = (ambient + (1.0 - shadow) * diffuse); fragColor = vec4(finalColor, 1.0); }

    Graph TD 图 (阴影贴图流程):

graph TD
A[光源视角渲染通道 Shadow Pass] --> B{生成阴影贴图 Depth Texture};
B --> C[主渲染通道 Main Pass];
C --> D{采样阴影贴图};
D --> E{计算阴影因子};
E --> F[应用阴影效果];
F --> G[最终渲染结果];

**3.1.2 基于物理的渲染 (Physically Based Rendering, PBR)** 传统的 Blinn-Phong 光照模型在模拟真实材质的光照交互方面存在局限性。PBR 是一种更先进的光照模型,它基于物理原理,能够更准确地模拟光线与物体表面的交互,从而产生更逼真的渲染效果。 **PBR 核心概念:** * **能量守恒 (Energy Conservation):** 物体反射的光的能量不能超过入射光的能量。 * **微表面模型 (Microfacet Model):** 假设物体表面由无数微小的镜面组成,这些微表面的法线分布影响了光线的反射方向。 * **BRDF (Bidirectional Reflectance Distribution Function):** 双向反射分布函数,描述了光线从某个入射方向照射到表面后,在各个出射方向上的反射能量分布。PBR 中常用的 BRDF 模型包括 Cook-Torrance BRDF。 **PBR 材质属性:** * **Albedo (反照率/基础颜色):** 物体表面反射的颜色,通常指漫反射颜色。 * **Metallic (金属度):** 描述材质是金属还是非金属,取值范围 [0, 1]。金属材质具有反射特性,而非金属材质主要表现为漫反射。 * **Roughness (粗糙度):** 描述表面微表面的粗糙程度,取值范围 [0, 1]。粗糙度越高,表面越粗糙,镜面反射越模糊。 * **Normal Map (法线贴图):** 用于模拟物体表面的细节凹凸,无需增加实际几何体。 * **Ambient Occlusion (环境光遮蔽):** 模拟物体自身遮挡环境光照的效果,增强场景的深度感和阴影细节。 **代码实践 (PBR 基础着色器):** 以下是一个简化的 PBR 片段着色器示例,使用了 Cook-Torrance BRDF 模型: ```glsl #version 300 es precision highp float; in vec3 vNormal; in vec3 vWorldPosition; in vec2 vTexCoord; uniform vec3 uViewPosition; uniform vec3 uLightPosition; uniform vec3 uLightColor; uniform sampler2D albedoMap; uniform sampler2D normalMap; uniform sampler2D metallicMap; uniform sampler2D roughnessMap; uniform sampler2D aoMap; out vec4 fragColor; const float PI = 3.14159265359; // 法线贴图采样 vec3 getNormalFromMap() { vec3 normalMapValue = texture(normalMap, vTexCoord).rgb; vec3 tangentNormal = normalize(normalMapValue * 2.0 - 1.0); vec3 Q1 = dFdx(vWorldPosition); vec3 Q2 = dFdy(vWorldPosition); vec2 st1 = dFdx(vTexCoord); vec2 st2 = dFdy(vTexCoord); vec3 N = normalize(vNormal); 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); } // Fresnel 菲涅尔方程 float fresnelSchlick(float cosTheta, float F0) { return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0); } // 几何函数 G (几何遮蔽) float geometrySchlickGGX(float NdotV, float roughness) { float r = roughness; float a = r * r; float k = (a * a) / 2.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; } // 分布函数 D (法线分布) float distributionGGX(float NdotH, float roughness) { float a = roughness*roughness; float a2 = a*a; float NdotH2 = NdotH*NdotH; float denom = (NdotH2 * (a2 - 1.0) + 1.0); denom = PI * denom*denom; return a2 / denom; } void main() { vec3 albedo = texture(albedoMap, vTexCoord).rgb; float metallic = texture(metallicMap, vTexCoord).r; float roughness = texture(roughnessMap, vTexCoord).r; float ao = texture(aoMap, vTexCoord).r; vec3 normal = normalize(getNormalFromMap()); // 使用法线贴图 vec3 viewDir = normalize(uViewPosition - vWorldPosition); vec3 lightDir = normalize(uLightPosition - vWorldPosition); vec3 halfDir = normalize(viewDir + lightDir); vec3 F0 = mix(vec3(0.04), albedo, metallic); // 金属材质的 F0 不同 float NDF = distributionGGX(max(dot(normal, halfDir), 0.0), roughness); float G = geometrySmith(normal, viewDir, lightDir, roughness); float F = fresnelSchlick(max(dot(viewDir, halfDir), 0.0), 0.04); // 非金属 F0 固定为 0.04 vec3 numerator = NDF * G * F; float denominator = 4.0 * max(dot(normal, viewDir), 0.0) * max(dot(normal, lightDir), 0.0); vec3 specular = numerator / max(denominator, 0.0001); // 避免除以 0 vec3 kS = F; vec3 kD = vec3(1.0) - kS; kD *= (1.0 - metallic); // 金属没有漫反射 float NdotL = max(dot(normal, lightDir), 0.0); vec3 diffuse = kD * albedo / PI; // 漫反射部分 vec3 ambient = vec3(0.03) * albedo * ao; // 环境光照 vec3 color = (ambient + NdotL * (diffuse + specular)) * uLightColor; fragColor = vec4(color, 1.0); }

3.2 高级纹理技术

纹理是 WebGL 中为物体表面添加细节和视觉丰富度的关键手段。高级纹理技术旨在提升纹理的质量、效率和灵活性。

3.2.1 纹理压缩

纹理通常占据 WebGL 应用的大部分资源,尤其是在高分辨率场景中。纹理压缩技术可以显著减小纹理文件的大小,从而减少加载时间、内存占用和带宽消耗,提升性能。

常用的纹理压缩格式 (WebGL Extensions):

  • ASTC (Adaptive Scalable Texture Compression): 一种高效且灵活的压缩格式,支持多种压缩比率和质量级别,适用于各种平台。WebGL 中通过 WEBGL_compressed_texture_astc 扩展支持。

  • ETC (Ericsson Texture Compression): 一种广泛应用于移动平台的压缩格式,包括 ETC1、ETC2 等变体。WebGL 中通过 WEBGL_compressed_texture_etcWEBGL_compressed_texture_etc1 扩展支持。

  • S3TC (S3 Texture Compression): 一种较早的压缩格式,包括 DXT1、DXT5 等变体。WebGL 中通过 WEBGL_compressed_texture_s3tc 扩展支持。

  • PVRTC (PowerVR Texture Compression): PowerVR GPU 专用的压缩格式,WebGL 中通过 WEBGL_compressed_texture_pvrtc 扩展支持。

代码实践 (纹理压缩加载):

function loadCompressedTexture(gl, url, format, callback) { const extension = gl.getExtension(format); if (!extension) { console.error("不支持纹理压缩格式: " + format); callback(null); return; } const image = new Image(); image.onload = function() { gl.bindTexture(gl.TEXTURE_2D, texture); gl.compressedTexImage2D(gl.TEXTURE_2D, 0, extension.COMPRESSED_RGBA_S3TC_DXT5_EXT, gl.RGBA, gl.UNSIGNED_BYTE, image); // 以 S3TC DXT5 为例 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); gl.generateMipmap(gl.TEXTURE_2D); gl.bindTexture(gl.TEXTURE_2D, null); callback(texture); }; image.onerror = function() { console.error("加载纹理失败: " + url); callback(null); }; image.src = url; } // ... (使用纹理压缩) ... const texture = gl.createTexture(); loadCompressedTexture(gl, "compressed_texture.ktx", "WEBGL_compressed_texture_s3tc_srgb", function(compressedTexture) { if (compressedTexture) { // 使用压缩纹理 } else { // 加载普通纹理作为备选项 loadTexture(gl, "fallback_texture.png", function(fallbackTexture) { // 使用普通纹理 }); } });

3.2.2 程序化纹理 (Procedural Texture)

程序化纹理 是一种通过算法在运行时生成纹理的技术,而不是预先加载图像文件。程序化纹理具有以下优点:

  • 节省存储空间: 无需存储大量的纹理图像文件。

  • 无限分辨率: 可以根据需要生成任意分辨率的纹理。

  • 动态性: 可以根据时间、参数等动态改变纹理。

  • 可重复性: 每次运行程序生成的纹理都是一致的。

常见的程序化纹理生成方法:

  • 噪声函数 (Noise Functions): 例如 Perlin 噪声、Simplex 噪声等,可以生成各种自然纹理,如云、木纹、大理石等。

  • 数学函数: 例如正弦波、余弦波、分形等,可以生成规则或抽象的纹理。

  • 细胞自动机 (Cellular Automata): 例如 Conway's Game of Life,可以生成复杂的图案和动画纹理。

代码实践 (程序化噪声纹理 - Simplex 噪声):

需要引入 Simplex 噪声算法的 GLSL 代码 (例如从第三方库获取)。

#version 300 es precision highp float; uniform vec2 uResolution; out vec4 fragColor; // ... (Simplex 噪声算法 GLSL 代码) ... void main() { vec2 uv = gl_FragCoord.xy / uResolution.xy; float noiseValue = snoise(uv * 5.0); // 调整频率 vec3 color = vec3(noiseValue * 0.5 + 0.5); // 映射到 [0, 1] 范围 fragColor = vec4(color, 1.0); }

JavaScript 代码 (生成程序化纹理并应用):

// 创建程序化纹理 function createProceduralTexture(gl, shaderProgram, width, height) { const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); // 创建空纹理 const framebuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); gl.viewport(0, 0, width, height); gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(shaderProgram); gl.uniform2f(gl.getUniformLocation(shaderProgram, "uResolution"), width, height); // ... (设置顶点属性和绘制) ... // 绘制一个覆盖整个纹理的 quad gl.drawArrays(gl.TRIANGLES, 0, 6); // 假设已经设置了 quad 的顶点数据 gl.bindFramebuffer(gl.FRAMEBUFFER, null); // 恢复默认帧缓冲 gl.bindTexture(gl.TEXTURE_2D, null); return texture; } // ... (加载程序化纹理着色器) ... const proceduralTextureShader = createShaderProgram(gl, proceduralTextureVS, proceduralTextureFS); const proceduralTexture = createProceduralTexture(gl, proceduralTextureShader, 256, 256); // ... (在渲染中使用程序化纹理) ... gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, proceduralTexture);

3.3 高级几何体操作

除了加载预先制作的 3D 模型,WebGL 还允许我们动态地操作几何体,实现更复杂的效果和交互。

3.3.1 几何体实例化 (Geometry Instancing)

几何体实例化 是一种高效渲染大量相同几何体的方法。它允许我们只提交一次几何数据,然后通过实例数组 (Instance Array) 指定每个实例的位置、旋转、缩放等属性,从而大幅减少 Draw Call 和顶点数据传输,提升渲染性能。

代码实践 (几何体实例化 - 使用 ANGLE_instanced_arrays 扩展):

需要检查 ANGLE_instanced_arrays 扩展是否可用。

const ext = gl.getExtension('ANGLE_instanced_arrays'); if (!ext) { console.error("不支持 ANGLE_instanced_arrays 扩展"); return; } // ... (创建几何体缓冲区 - 顶点位置、法线等) ... const positionBuffer = createBuffer(gl, positions); const normalBuffer = createBuffer(gl, normals); // 创建实例属性缓冲区 - 实例位置 const instancePositionBuffer = createBuffer(gl, instancePositions); // ... (设置顶点属性指针) ... gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(positionLocation); gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer); gl.vertexAttribPointer(normalLocation, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(normalLocation); // 设置实例属性指针 gl.bindBuffer(gl.ARRAY_BUFFER, instancePositionBuffer); gl.vertexAttribPointer(instancePositionLocation, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(instancePositionLocation); ext.vertexAttribDivisorANGLE(instancePositionLocation, 1); // 关键:设置为实例属性 // ... (绘制实例化几何体) ... ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, vertexCount, instanceCount);

顶点着色器 (支持实例属性):

#version 300 es in vec3 aVertexPosition; in vec3 aVertexNormal; in vec3 aInstancePosition; // 实例位置属性 uniform mat4 uModelMatrix; uniform mat4 uViewMatrix; uniform mat4 uProjectionMatrix; out vec3 vNormal; out vec3 vWorldPosition; void main() { mat4 modelMatrix = uModelMatrix; modelMatrix[3].xyz += aInstancePosition; // 应用实例位置偏移 vWorldPosition = vec3(modelMatrix * vec4(aVertexPosition, 1.0)); gl_Position = uProjectionMatrix * uViewMatrix * modelMatrix * vec4(aVertexPosition, 1.0); vNormal = normalize(mat3(transpose(inverse(modelMatrix))) * aVertexNormal); }

Graph TD 图 (几何体实例化流程):

3.3.2 顶点纹理拾取 (Vertex Texture Fetch, VTF)

顶点纹理拾取 允许顶点着色器读取纹理数据。这项技术在早期 WebGL 版本中可能受限,但在 WebGL2 和部分扩展支持下,可以实现更强大的效果。VTF 的应用场景包括:

  • 地形渲染: 使用高度图纹理驱动顶点高度,生成复杂的地形表面。

  • 粒子系统: 使用纹理存储粒子属性,顶点着色器根据纹理数据更新粒子位置和状态。

  • 置换贴图 (Displacement Mapping): 根据纹理数据在顶点着色器中动态调整顶点位置,实现更精细的表面细节。

代码实践 (顶点纹理拾取 - 地形渲染):

需要确保 WebGL 环境支持顶点纹理拾取 (WebGL2 或 OES_texture_float 扩展)。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U