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图形学的关键概念:着色器编程、矩阵变换、深度测试、帧缓冲和纹理处理。
延伸挑战
- 尝试为场景添加多个光源并生成混合阴影
- 实现点光源的360°阴影(使用立方体贴图)