10. 进阶实战:三大常见 WebGL 效果
掌握了基础语法和渲染管线后,我们来看三个最常见的“花哨”效果是如何实现的。 这些效果是面试和实际项目中出现频率最高的。
1. 菲涅尔效果 (Fresnel Effect)
现象:物体边缘发光,或者边缘反光更强(像肥皂泡、水珠、丝绸)。 原理:根据“视线方向”和“法线方向”的夹角来改变颜色。视线越垂直于表面,颜色越深;视线越平行于表面(边缘),颜色越亮。
需要同时配合 Vertex Shader 和 Fragment Shader。
// --- 顶点着色器 (Vertex Shader) ---
attribute vec3 position;
attribute vec3 normal;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform mat3 normalMatrix; // 专门用于变换法线的矩阵
varying vec3 vNormal;
varying vec3 vViewPosition;
void main() {
// 1. 计算变换后的法线
vNormal = normalize(normalMatrix * normal);
// 2. 计算视图空间中的位置 (View Position)
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
// 3. 视线向量 = 眼睛位置(0,0,0) - 物体位置
// 在视图空间中,相机永远在原点 (0,0,0)
// 所以 vector(Camera - Position) 等价于 -mvPosition
vViewPosition = -mvPosition.xyz;
gl_Position = projectionMatrix * mvPosition;
}
// --- 片段着色器 (Fragment Shader) ---
precision mediump float;
varying vec3 vNormal;
varying vec3 vViewPosition;
void main() {
// 1. 计算视线方向 (View Direction)
// 必须重新归一化,因为插值过程中长度会变
vec3 viewDir = normalize(vViewPosition);
// 2. 计算法线方向 (Normal Direction)
vec3 normal = normalize(vNormal);
// 3. 计算点积 (Dot Product)
// dot = 1.0 (正对) -> 边缘强度 = 0.0
// dot = 0.0 (边缘) -> 边缘强度 = 1.0
float dotProduct = dot(viewDir, normal);
// 4. 反转并增强效果
// clamp 确保值在 0.0 到 1.0 之间,abs 防止背面算出负数
float fresnel = 1.0 - clamp(abs(dotProduct), 0.0, 1.0);
// 用指数函数 (pow) 让边缘更锐利
// 指数越高,边缘光越细
fresnel = pow(fresnel, 3.0);
// 5. 混合颜色
vec3 baseColor = vec3(0.0, 0.0, 1.0); // 蓝色中心
vec3 glowColor = vec3(0.0, 1.0, 1.0); // 青色边缘
gl_FragColor = vec4(mix(baseColor, glowColor, fresnel), 1.0);
}
2. 纹理流动 (Texture Scrolling)
现象:火焰燃烧、水流流动、传送带。 原理:在采样纹理时,动态修改 UV 坐标。
uniform float uTime;
uniform sampler2D uTexture;
varying vec2 vUv;
void main() {
// 1. 修改 UV
vec2 flowUv = vUv;
// 让纹理沿着 X 轴移动
flowUv.x -= uTime * 0.5;
// 让纹理产生波浪扭曲 (高级技巧)
flowUv.y += sin(flowUv.x * 10.0 + uTime) * 0.1;
// 2. 采样
vec4 color = texture2D(uTexture, flowUv);
gl_FragColor = color;
}
3. 噪声 (Noise)
现象:云彩、地形、大理石纹路、溶解效果。 原理:GLSL 没有内置随机函数。我们需要一个“伪随机”函数,输入相同的坐标,永远返回相同的(看起来随机的)值。
最简单的 2D 伪随机函数(直接背诵即可):
// 输入 uv,返回 0.0 ~ 1.0 的随机数
float random (vec2 st) {
return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
}
// 简单的噪声溶解效果
void main() {
float noiseVal = random(vUv); // 生成噪点
// 随时间变化的阈值
float threshold = sin(uTime) * 0.5 + 0.5;
// 如果噪点值小于阈值,丢弃像素 (透明)
if (noiseVal < threshold) {
discard;
}
gl_FragColor = vec4(1.0);
}
💡 总结
这三个效果代表了 Shader 编程的三个方向:
- 光照方向 (菲涅尔):利用
dot(view, normal)。 - UV 方向 (流动):利用
uv + time。 - 算法方向 (噪声):利用数学公式生成图案。
📝 编程作业
难度:⭐⭐⭐ 困难 | 预计时间:120 分钟(每个效果 40 分钟)
任务要求
实现三个经典的 WebGL 效果,这是面试和实际项目中最常见的。
效果 1:菲涅尔效果 ⭐⭐
要求:
- 创建一个 3D 物体(球体或复杂模型)
- 实现边缘发光效果
- 边缘颜色和中心颜色可配置
代码提示:
// 片段着色器
varying vec3 vNormal;
varying vec3 vViewPosition;
void main() {
vec3 viewDir = normalize(vViewPosition);
vec3 normal = normalize(vNormal);
float fresnel = 1.0 - clamp(abs(dot(viewDir, normal)), 0.0, 1.0);
fresnel = pow(fresnel, 3.0);
vec3 baseColor = vec3(0.0, 0.0, 1.0);
vec3 glowColor = vec3(0.0, 1.0, 1.0);
gl_FragColor = vec4(mix(baseColor, glowColor, fresnel), 1.0);
}
效果 2:纹理流动 ⭐⭐
要求:
- 加载一张纹理图片
- 实现纹理的滚动动画
- 可选:添加波浪扭曲效果
代码提示:
uniform float uTime;
uniform sampler2D uTexture;
varying vec2 vUv;
void main() {
vec2 flowUv = vUv;
flowUv.x -= uTime * 0.5; // 水平滚动
// 可选:波浪扭曲
flowUv.y += sin(flowUv.x * 10.0 + uTime) * 0.1;
gl_FragColor = texture2D(uTexture, flowUv);
}
效果 3:噪声溶解 ⭐⭐⭐
要求:
- 实现基础的噪声函数
- 创建溶解效果(物体逐渐消失)
- 添加边缘发光(溶解边缘)
代码提示:
// 噪声函数
float random(vec2 st) {
return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
}
uniform float uTime;
varying vec2 vUv;
void main() {
float noiseVal = random(vUv);
float threshold = sin(uTime) * 0.5 + 0.5;
if (noiseVal < threshold) {
discard; // 丢弃像素
}
// 边缘发光
float edge = smoothstep(threshold - 0.1, threshold, noiseVal);
vec3 glowColor = vec3(1.0, 0.5, 0.0);
gl_FragColor = vec4(glowColor * edge, 1.0);
}
检查清单
- 菲涅尔效果边缘清晰可见
- 纹理流动平滑自然
- 溶解效果随时间变化
- 所有效果参数可通过 uniform 调整
- 代码结构清晰,有注释
扩展挑战
- 🌟 将三个效果组合在一起
- 🌟 添加 GUI 控制面板(如 dat.GUI)
- 🌟 实现更复杂的噪声函数(如 Perlin Noise)
参考资源
- 完整作业清单:编程作业清单.md