WebGL魔法:从立方体到逼真阴影的奇妙之旅

WebGL魔法:从立方体到逼真阴影的奇妙之旅

你是否曾被3D游戏中逼真的阴影效果所震撼?那种物体在光照下投射出自然阴影的效果,让虚拟世界瞬间拥有了真实的深度和立体感。今天,我们将一起揭开这份魔法的面纱------使用原生WebGL绘制一个立方体,并为它添加专业级的阴影效果!无需任何框架,只用纯粹的WebGL API,一步步带你从零实现。准备好了吗?让我们开启这场视觉魔法的旅程!

🔍 一、WebGL绘制立方体:3D世界的基石

1.1 WebGL基础设置

一切从创建画布开始。我们在HTML中放置一个canvas元素,并获取WebGL上下文:

html 复制代码
<canvas id="glCanvas" width="800" height="600"></canvas>
<script>
  const canvas = document.getElementById('glCanvas');
  const gl = canvas.getContext('webgl');
  
  if (!gl) {
    alert('你的浏览器不支持WebGL!');
  }
  
  // 设置清屏颜色与深度测试
  gl.clearColor(0.1, 0.1, 0.1, 1.0);
  gl.enable(gl.DEPTH_TEST);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
</script>

深度测试(gl.DEPTH_TEST)是3D绘制的关键,它确保靠近相机的物体正确地遮挡后面的物体。

1.2 创建立方体顶点数据

立方体有6个面,每个面由2个三角形(即6个顶点)组成。为了高效管理,我们使用顶点数组和索引数组:

javascript 复制代码
// 立方体顶点位置(8个顶点)
const positions = [
  // 前面
  -0.5, -0.5,  0.5,
   0.5, -0.5,  0.5,
   0.5,  0.5,  0.5,
  -0.5,  0.5,  0.5,
  // 后面...(其他面数据类似)
];

// 顶点颜色(RGBA)
const colors = [
  // 前面 - 蓝色
  0.0, 0.0, 1.0, 1.0,
  0.0, 0.0, 1.0, 1.0,
  // ...其他面颜色
];

// 索引数据(定义三角形连接顺序)
const indices = [
  // 前面
  0, 1, 2,  0, 2, 3,
  // 右面
  1, 5, 6,  1, 6, 2,
  // ...其他面索引
];

1.3 着色器:WebGL的灵魂

顶点着色器处理位置变换,片段着色器决定像素颜色:

glsl 复制代码
// 顶点着色器
attribute vec3 aPosition;
attribute vec4 aColor;

uniform mat4 uModelMatrix;
uniform mat4 uViewMatrix;
uniform mat4 uProjectionMatrix;

varying vec4 vColor;

void main() {
  gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * vec4(aPosition, 1.0);
  vColor = aColor;
}

// 片段着色器
precision mediump float;
varying vec4 vColor;

void main() {
  gl_FragColor = vColor;
}

1.4 数据传递与渲染

创建缓冲区并将数据传入GPU:

javascript 复制代码
// 创建位置缓冲区
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

// 设置属性指针
const aPosition = gl.getAttribLocation(program, 'aPosition');
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);

// 同样设置颜色缓冲区...

// 索引缓冲区
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);

最后设置模型-视图-投影矩阵并绘制:

javascript 复制代码
// 计算矩阵
const modelMatrix = mat4.create();
mat4.rotate(modelMatrix, modelMatrix, rotationAngle, [0, 1, 0]);

const viewMatrix = mat4.create();
mat4.lookAt(viewMatrix, [0, 0, 5], [0, 0, 0], [0, 1, 0]);

const projectionMatrix = mat4.create();
mat4.perspective(projectionMatrix, Math.PI/4, canvas.width/canvas.height, 0.1, 100.0);

// 传递矩阵到着色器
gl.uniformMatrix4fv(uModelMatrix, false, modelMatrix);
// ...其他矩阵传递

// 绘制立方体
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);

至此,一个旋转的彩色立方体已跃然屏上!但这只是开始,真正的魔法在阴影效果✨


🌑 二、添加阴影:让立方体"落地生根"

2.1 阴影原理揭秘

阴影是光线被物体阻挡后形成的暗区。在WebGL中,常用两种技术实现:

  • 平面投影阴影:将物体顶点投影到指定平面(如地面)。计算简单但只能投影到平面
  • 阴影贴图(Shadow Mapping):通过从光源视角渲染深度图,再与实际深度比较确定阴影。效果真实,支持复杂场景

我们将聚焦于更逼真的阴影贴图技术。

2.2 阴影贴图四步法

步骤1:创建深度纹理(阴影贴图)

javascript 复制代码
// 创建深度纹理
const depthTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, depthTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT, 
              1024, 1024, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_INT, null);

// 设置纹理参数
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

// 创建帧缓冲并附加纹理
const framebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, 
                        gl.TEXTURE_2D, depthTexture, 0);

深度纹理将存储从光源视角看到的场景深度信息。

步骤2:从光源视角渲染深度

使用专门用于生成深度图的着色器:

glsl 复制代码
// 深度着色器 - 顶点部分
uniform mat4 uLightMatrix;  // 光源的MVP矩阵

void main() {
  gl_Position = uLightMatrix * vec4(aPosition, 1.0);
}

// 深度着色器 - 片段部分
void main() {
  // 不需要输出颜色,深度会自动写入
}

渲染过程:

javascript 复制代码
gl.viewport(0, 0, 1024, 1024);  // 使用纹理尺寸
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.clear(gl.DEPTH_BUFFER_BIT);

// 计算光源矩阵(光源位置和投影)
const lightProjection = mat4.create();
mat4.ortho(lightProjection, -10, 10, -10, 10, 0.1, 100);

const lightView = mat4.create();
mat4.lookAt(lightView, [5, 5, 5], [0, 0, 0], [0, 1, 0]);

const lightMatrix = mat4.create();
mat4.multiply(lightMatrix, lightProjection, lightView);

// 激活深度着色器并绘制场景
gl.useProgram(depthProgram);
gl.uniformMatrix4fv(uLightMatrix, false, lightMatrix);
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);

步骤3:正常渲染场景并应用阴影

在主着色器中添加阴影计算逻辑:

glsl 复制代码
// 主着色器新增代码
uniform mat4 uLightMatrix;  // 同样的光源矩阵
uniform sampler2D uShadowMap;  // 深度纹理

varying vec4 vPositionFromLight;  // 顶点在光源空间的位置

void main() {
  // 转换到[0,1]范围
  vec3 shadowCoord = vPositionFromLight.xyz / vPositionFromLight.w * 0.5 + 0.5;
  
  // 从深度纹理获取最近表面深度
  float closestDepth = texture2D(uShadowMap, shadowCoord.xy).r;
  
  // 当前片段的深度
  float currentDepth = shadowCoord.z;
  
  // 检查当前片段是否在阴影中
  float shadow = (currentDepth > closestDepth + 0.005) ? 0.5 : 1.0;
  
  // 应用阴影因子
  vec3 color = ... // 正常光照计算的颜色
  gl_FragColor = vec4(color * shadow, 1.0);
}

步骤4:传递光源空间位置

顶点着色器需要计算顶点在光源空间的位置:

glsl 复制代码
// 顶点着色器新增
varying vec4 vPositionFromLight;

void main() {
  // ...原有代码
  vPositionFromLight = uLightMatrix * vec4(aPosition, 1.0);
}

⚡ 三、高级优化与调试技巧

3.1 解决阴影锯齿(锯齿问题)

阴影边缘常出现锯齿。通过PCF(百分比渐进过滤) 柔化边缘:

glsl 复制代码
float pcfShadow(vec3 shadowCoord) {
  float shadow = 0.0;
  vec2 texelSize = 1.0 / textureSize(uShadowMap, 0);
  for(int x = -1; x <= 1; ++x) {
    for(int y = -1; y <= 1; ++y) {
      float closestDepth = texture(uShadowMap, 
                                  shadowCoord.xy + vec2(x, y) * texelSize).r;
      shadow += (currentDepth > closestDepth + 0.005) ? 0.0 : 1.0;
    }
  }
  return shadow / 9.0;
}

3.2 深度精度优化

将深度值编码到RGBA通道,提升精度:

glsl 复制代码
// 深度编码(写入阶段)
const vec4 bitShift = vec4(1.0, 256.0, 256.0*256.0, 256.0*256.0*256.0);
const vec4 bitMask = vec4(1.0/256.0, 1.0/256.0, 1.0/256.0, 0.0);
vec4 rgbaDepth = fract(gl_FragCoord.z * bitShift);
rgbaDepth -= rgbaDepth.gbaa * bitMask;
gl_FragColor = rgbaDepth;

// 深度解码(读取阶段)
float unpackDepth(vec4 rgba) {
  const vec4 bitShift = vec4(1.0, 1.0/256.0, 1.0/(256.0*256.0), 1.0/(256.0*256.0*256.0));
  return dot(rgba, bitShift);
}

这种方法有效避免了深度值精度不足导致的条纹问题(称为"马赫带")。

3.3 常见问题排查

  • 阴影缺失 :检查帧缓冲状态 gl.checkFramebufferStatus(),确保深度附件设置正确
  • 阴影位置偏移:调整光源投影矩阵的视锥体和偏置值
  • 全黑/全白:检查深度纹理是否正确绑定和采样

💎 结语:掌握光影,创造世界

至此,你已经掌握了WebGL中立方体绘制与阴影生成的核心技术!从基础立方体到逼真的动态阴影,这条路径涵盖了现代3D图形学的关键概念:着色器编程、矩阵变换、深度测试、帧缓冲和纹理处理。

延伸挑战

  1. 尝试为场景添加多个光源并生成混合阴影
  2. 实现点光源的360°阴影(使用立方体贴图)
相关推荐
又又呢2 小时前
前端面试题总结——webpack篇
前端·webpack·node.js
dog shit3 小时前
web第十次课后作业--Mybatis的增删改查
android·前端·mybatis
我有一只臭臭3 小时前
el-tabs 切换时数据不更新的问题
前端·vue.js
七灵微3 小时前
【前端】工具链一本通
前端
Nueuis4 小时前
微信小程序前端面经
前端·微信小程序·小程序
_r0bin_6 小时前
前端面试准备-7
开发语言·前端·javascript·fetch·跨域·class
IT瘾君6 小时前
JavaWeb:前端工程化-Vue
前端·javascript·vue.js
potender6 小时前
前端框架Vue
前端·vue.js·前端框架
站在风口的猪11087 小时前
《前端面试题:CSS预处理器(Sass、Less等)》
前端·css·html·less·css3·sass·html5
程序员的世界你不懂7 小时前
(9)-Fiddler抓包-Fiddler如何设置捕获Https会话
前端·https·fiddler