目录
前言
第一章 片元着色器基础认知:渲染管线中的 “像素画家”
1.1 什么是片元着色器?
1.2 片元着色器在渲染管线中的位置
1.3 片元着色器的核心特性
第二章 片元着色器的核心输入:数据来源与使用规则
2.1 核心输入 1:varying 变量(顶点插值数据)
2.1.1 varying 变量的使用规则
2.1.2 实战案例:用 varying 变量实现颜色渐变
2.2 核心输入 2:uniform 变量(全局共享数据)
2.2.1 常用 uniform 变量类型与作用
2.2.2 实战案例:用 uniform 变量实现随时间变色
第三章 片元着色器的核心输出:gl_FragColor 与颜色规则
3.1 gl_FragColor 的基础规则
3.1.1 变量格式与颜色通道
3.1.2 必选声明:浮点数精度(precision)
3.2 颜色计算的核心逻辑
3.2.1 基础纯色:固定通道值
3.2.2 渐变颜色:基于坐标 / 时间的动态计算
3.2.3 纹理颜色:采样外部图片 / 视频
3.2.4 混合颜色:多源颜色叠加
第四章 片元着色器进阶:特效实现与性能优化
4.1 进阶应用 1:鼠标交互特效(跟随鼠标的颜色变化)
4.2 进阶应用 2:内置函数组合实现复杂特效
4.3 片元着色器的性能优化技巧
4.3.1 减少复杂计算的调用次数
4.3.2 合理使用纹理过滤与压缩
4.3.3 避免动态分支(if/else、switch)
4.3.4 控制片元数量
4.3.5 选择合适的浮点数精度
第五章 综合实战:带纹理与交互的动态波纹效果
5.1 完整代码
5.2 案例核心逻辑解析
5.3 运行与修改建议
第六章 总结与后续学习方向
6.1 本章核心知识点回顾
6.2 常见问题与排查技巧
6.3 后续学习方向
前言
在 WEBGL 渲染管线中,片元着色器(Fragment Shader)是决定 “最终像素外观” 的关键环节 —— 它接收顶点着色器传递的插值数据(如颜色、纹理坐标),结合 CPU 传递的全局参数(如纹理、时间),通过逐像素计算,最终输出每个像素的 RGBA 颜色。对零基础开发者而言,片元着色器的核心难点并非语法(已在 GLSL ES 基础教程中覆盖),而是 “逐像素并行计算思维”“纹理采样逻辑” 与 “颜色混合规则” 的结合。
本文作为 WEBGL Shader 系列的第四篇,将聚焦片元着色器的核心逻辑:从 “输入数据(varying/uniform)” 到 “颜色计算(基础色、纹理色、特效色)”,再到 “最终输出(gl_FragColor)”,通过 “理论 + 可运行案例” 的形式,帮你建立片元着色器的完整工作框架。所有案例仍基于 Three.js 框架(跳过 WEBGL 底层纹理配置),你可直接复制代码运行,边改边学,直观感受像素级渲染的原理。
掌握本文内容后,你将能独立实现基础纹理渲染、颜色渐变、动态特效(如随时间变色、鼠标交互),为后续学习光照模型、后期处理打下坚实基础。
第一章 片元着色器基础认知:渲染管线中的 “像素画家”
在深入逻辑前,需先明确片元着色器在 WEBGL 渲染管线中的角色 —— 它不是孤立的 “颜色生成器”,而是与顶点着色器、光栅化阶段紧密联动的 “逐像素处理单元”。
1.1 什么是片元着色器?
片元着色器是运行在 GPU 片元处理器上的小型程序,其核心职责是:
接收输入数据:包括顶点着色器传递的
插值数据(如颜色、纹理坐标)、CPU 传递的
varying
全局数据(如纹理、时间、分辨率);逐像素计算颜色:根据输入数据,通过数学运算、纹理采样、逻辑判断,计算当前像素的最终颜色(RGBA);输出像素颜色:通过内置变量
uniform
将颜色传递给后续的 “颜色混合” 阶段,最终显示在屏幕上。
gl_FragColor
需注意:“片元(Fragment)” 并非完全等同于 “像素(Pixel)”—— 片元是光栅化阶段生成的 “像素候选者”,包含颜色、深度等信息,经过深度测试、模板测试后,才会最终成为屏幕上的像素。
1.2 片元着色器在渲染管线中的位置
WEBGL 渲染管线的核心流程可简化为:
CPU准备数据→顶点着色器(逐顶点)→光栅化(生成片元)→片元着色器(逐片元)→颜色混合/深度测试→屏幕显示
其中,片元着色器处于 “光栅化” 与 “屏幕显示” 之间,是GPU 渲染的最后一个可编程阶段—— 其输出直接决定了每个像素的最终外观,任何颜色错误、纹理异常都可定位到片元着色器逻辑。
1.3 片元着色器的核心特性
与顶点着色器相比,片元着色器有以下 3 个关键特性,需在开发中重点关注:
逐像素并行执行:GPU 会为每个片元分配独立的计算核心,同时执行片元着色器逻辑(如 1000×1000 像素的画面会并行执行 100 万次片元着色器),无 “顺序执行” 概念,每个片元的计算无法访问其他片元的数据;依赖插值数据:片元着色器的
变量值由顶点着色器 “插值生成”—— 若顶点 A 的颜色为红色、顶点 B 为蓝色,那么 A 与 B 之间的片元颜色会从红平滑渐变到蓝,插值精度由 GPU 硬件保证;计算成本敏感:片元数量远大于顶点数量(如一个三角形可能包含数千个片元),片元着色器的计算复杂度直接影响渲染性能(如复杂的噪声计算、多层纹理混合会导致帧率下降)。
varying
第二章 片元着色器的核心输入:数据来源与使用规则
片元着色器的所有颜色计算都基于 “输入数据” 展开,核心输入包括varying 变量(顶点插值数据) 和uniform 变量(全局共享数据),二者共同构成片元着色器的 “数据基础”。
2.1 核心输入 1:varying 变量(顶点插值数据)
变量是片元着色器接收顶点着色器数据的唯一通道,其核心作用是 “传递逐顶点数据并自动插值”,是实现渐变、纹理映射的基础。
varying
2.1.1 varying 变量的使用规则
声明一致性:必须在顶点着色器和片元着色器中 “同名、同类型” 声明(如顶点着色器写
,片元着色器也需写
varying vec2 vUv;
),否则数据传递失败;只读属性:片元着色器中只能读取
varying vec2 vUv;
变量的值,不能修改(值由顶点着色器赋值 + GPU 插值生成);类型限制:支持基础类型(
varying
/
float
/
int
)和向量类型(
bool
/
vec2
/
vec3
),不支持矩阵类型;插值精度:插值过程由 GPU 自动完成,精度极高,开发者无需关心具体插值算法(默认线性插值)。
vec4
2.1.2 实战案例:用 varying 变量实现颜色渐变
通过顶点着色器传递顶点颜色,片元着色器接收插值后的
变量,实现平滑的颜色渐变效果:
varying
步骤 1:顶点着色器传递顶点颜色
glsl
// 顶点着色器代码
attribute vec3 position;
attribute vec3 color; // Three.js自动传递的顶点颜色(需CPU为几何体设置)
varying vec3 vColor; // 传递颜色给片元着色器
void main() {
vColor = color; // 为varying变量赋值(逐顶点)
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
步骤 2:CPU 为几何体设置顶点颜色
javascript
// 创建平面几何体(10x10,32x32分段)
const planeGeometry = new THREE.PlaneGeometry(10, 10, 32, 32);
// 生成顶点颜色数据:4个角分别为红、绿、蓝、黄
const vertexColors = [];
const uv = planeGeometry.attributes.uv.array; // 获取uv坐标(用于判断顶点位置)
for (let i = 0; i < planeGeometry.attributes.position.count; i++) {
const u = uv[i * 2]; // 第i个顶点的u坐标(0~1)
const v = uv[i * 2 + 1]; // 第i个顶点的v坐标(0~1)
// 左上角(u=0,v=1):红色
if (u < 0.5 && v > 0.5) vertexColors.push(1.0, 0.0, 0.0);
// 右上角(u=1,v=1):绿色
else if (u > 0.5 && v > 0.5) vertexColors.push(0.0, 1.0, 0.0);
// 左下角(u=0,v=0):蓝色
else if (u < 0.5 && v < 0.5) vertexColors.push(0.0, 0.0, 1.0);
// 右下角(u=1,v=0):黄色
else vertexColors.push(1.0, 1.0, 0.0);
}
// 将颜色数据添加为几何体的attribute属性
planeGeometry.setAttribute(
'color',
new THREE.Float32BufferAttribute(vertexColors, 3)
);
步骤 3:片元着色器使用插值后的颜色
glsl
// 片元着色器代码
precision mediump float; // 必选:声明浮点数精度
varying vec3 vColor; // 接收插值后的颜色
void main() {
// 直接使用插值后的颜色输出像素
gl_FragColor = vec4(vColor, 1.0);
}
运行效果:平面被分为四个象限,从四个角的红、绿、蓝、黄向中心平滑渐变,形成色彩过渡带 —— 这直观体现了
变量的 “插值特性”,也是所有渐变效果的底层逻辑。
varying
2.2 核心输入 2:uniform 变量(全局共享数据)
变量是片元着色器与顶点着色器共享的 “全局数据”,用于传递不随像素变化的参数(如纹理、时间、分辨率、全局颜色)。在片元着色器中,
uniform
变量是实现 “动态特效”“交互控制” 的核心,以下是最常用的 4 类
uniform
变量:
uniform
2.2.1 常用 uniform 变量类型与作用
uniform 变量类型 | 示例变量名 | 作用 | 典型应用场景 |
---|---|---|---|
|
(时间)、 (透明度) |
传递单值参数,控制动态效果 | 随时间变色、透明度调节 |
|
(屏幕分辨率)、 (鼠标位置) |
传递二维参数,关联屏幕 / 交互位置 | 屏幕中心渐变、鼠标跟随特效 |
|
(全局颜色)、 (光源颜色) |
传递颜色或三维参数 | 全局纯色渲染、光源颜色控制 |
|
(2D 纹理) |
传递图片 / 视频纹理数据 | 纹理映射、图片渲染 |
2.2.2 实战案例:用 uniform 变量实现随时间变色
通过
(时间)控制颜色通道的变化,实现像素颜色随时间循环的动态效果:
uTime
片元着色器代码
glsl
precision mediump float;
uniform float uTime; // 时间(CPU每帧更新)
void main() {
// 1. 用sin/cos函数生成0~1范围的颜色值(循环变化)
float red = sin(uTime) * 0.5 + 0.5; // sin值范围-1~1 → 转为0~1
float green = cos(uTime * 1.2) * 0.5 + 0.5; // 1.2控制绿色变化节奏
float blue = sin(uTime * 0.8) * 0.5 + 0.5; // 0.8控制蓝色变化节奏
float alpha = 1.0; // 固定透明度
// 2. 输出动态颜色
gl_FragColor = vec4(red, green, blue, alpha);
}
CPU 端传递并更新 uTime
javascript
// 创建Shader材质时声明uTime
const shaderMaterial = new THREE.ShaderMaterial({
vertexShader: `
attribute vec3 position;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: fragmentShaderCode,
uniforms: {
uTime: { value: 0.0 }
}
});
// 渲染循环中更新uTime
function animate() {
requestAnimationFrame(animate);
shaderMaterial.uniforms.uTime.value += 0.01; // 每帧增加0.01,控制动画速度
renderer.render(scene, camera);
}
animate();
运行效果:平面颜色随时间循环变化,红、绿、蓝三色通道按不同节奏交替明暗,呈现出动态的色彩流动效果 —— 这是
变量在片元着色器中实现动态特效的基础用法。
uniform
第三章 片元着色器的核心输出:gl_FragColor 与颜色规则
片元着色器的唯一核心输出是内置变量
(类型为
gl_FragColor
),用于指定当前片元的最终颜色(RGBA)。正确理解
vec4
的赋值规则、颜色通道范围,是避免 “颜色异常” 的关键。
gl_FragColor
3.1 gl_FragColor 的基础规则
3.1.1 变量格式与颜色通道
是
gl_FragColor
类型的内置变量,格式为
vec4
,四个通道的含义与范围如下:
gl_FragColor = vec4(red, green, blue, alpha);
通道 | 含义 | 取值范围 | 说明 |
---|---|---|---|
(R) |
红色分量 | 0.0 ~ 1.0 | 0.0 表示无红色,1.0 表示最饱和红色 |
(G) |
绿色分量 | 0.0 ~ 1.0 | 0.0 表示无绿色,1.0 表示最饱和绿色 |
(B) |
蓝色分量 | 0.0 ~ 1.0 | 0.0 表示无蓝色,1.0 表示最饱和蓝色 |
(A) |
透明度分量 | 0.0 ~ 1.0 | 0.0 表示完全透明,1.0 表示完全不透明 |
注意:若通道值超出 0.0~1.0 范围(如
),GPU 会自动 “钳位” 到 0.0~1.0(即 1.5→1.0,-0.2→0.0),导致颜色失真,需在代码中确保通道值在有效范围。
vec4(1.5, -0.2, 0.8, 1.0)
3.1.2 必选声明:浮点数精度(precision)
片元着色器中必须显式声明浮点数精度,否则浏览器会报 “precision highp float; not supported” 错误 —— 这是因为不同 GPU 对浮点数精度的支持不同,需开发者明确指定。
常用的精度声明有 3 种:
glsl
// 1. 高精度(highp):适合需要高精度的场景(如纹理坐标计算),但部分低端GPU不支持
precision highp float;
// 2. 中精度(mediump):默认推荐,平衡精度与性能,所有GPU都支持
precision mediump float;
// 3. 低精度(lowp):精度最低,性能最高,适合颜色等对精度要求不高的场景
precision lowp float;
最佳实践:片元着色器默认使用
,既能满足绝大多数场景的精度需求,又能保证跨设备兼容性。
precision mediump float;
3.2 颜色计算的核心逻辑
片元着色器的核心工作是 “计算
的四个通道值”,常见的颜色计算逻辑包括 “基础纯色”“渐变颜色”“纹理颜色”“混合颜色” 四类,以下逐一讲解并提供案例。
gl_FragColor
3.2.1 基础纯色:固定通道值
最简单的颜色计算方式,直接为 RGBA 通道赋值固定值,适合纯色物体渲染:
glsl
precision mediump float;
void main() {
// 红色(R=1.0, G=0.0, B=0.0, A=1.0)
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
// 半透明蓝色(R=0.0, G=0.0, B=1.0, A=0.5)
// gl_FragColor = vec4(0.0, 0.0, 1.0, 0.5);
// 灰色(R=G=B=0.5,灰度值0.5)
// gl_FragColor = vec4(0.5, 0.5, 0.5, 1.0);
}
3.2.2 渐变颜色:基于坐标 / 时间的动态计算
通过片元的坐标(如
、屏幕坐标)或时间(
vUv
)计算颜色通道值,实现渐变效果,常见的有 “线性渐变”“径向渐变”“环形渐变”。
uTime
案例 1:基于 uv 的线性渐变(水平方向)
glsl
precision mediump float;
varying vec2 vUv; // 顶点着色器传递的uv坐标(0~1)
void main() {
// uv.x从0→1,红色从0→1,绿色从1→0,实现红绿线性渐变
float red = vUv.x; // 左→右:红色从0变1
float green = 1.0 - vUv.x; // 左→右:绿色从1变0
float blue = 0.2; // 固定蓝色分量
gl_FragColor = vec4(red, green, blue, 1.0);
}
案例 2:基于屏幕中心的径向渐变
glsl
precision mediump float;
uniform vec2 uResolution; // 屏幕分辨率(如1920x1080)
varying vec2 vUv;
void main() {
// 1. 将uv坐标转为屏幕像素坐标(0~uResolution)
vec2 screenPos = vUv * uResolution;
// 2. 计算当前像素到屏幕中心的距离
vec2 center = uResolution / 2.0; // 屏幕中心坐标
float distance = distance(screenPos, center); // 内置函数:计算两点距离
// 3. 基于距离计算颜色(距离越远,颜色越暗)
float brightness = 1.0 - smoothstep(0.0, 500.0, distance); // 0~500像素内渐变
gl_FragColor = vec4(brightness, brightness * 0.8, brightness * 0.5, 1.0);
}
运行效果:屏幕中心为最亮的橙黄色,向边缘逐渐变暗,形成径向渐变 ——
函数用于实现 “平滑过渡”,避免颜色突变。
smoothstep
3.2.3 纹理颜色:采样外部图片 / 视频
纹理(Texture)是片元着色器中最常用的资源,通过
类型的
sampler2D
变量传递图片 / 视频数据,再用
uniform
内置函数采样颜色,实现图片渲染。
texture2D
完整案例:加载并渲染一张图片纹理
glsl
// 片元着色器代码
precision mediump float;
uniform sampler2D uTexture; // 纹理采样器(CPU传递图片)
varying vec2 vUv; // 纹理坐标(0~1)
void main() {
// 1. 纹理采样:从uTexture中读取vUv坐标对应的颜色
vec3 texColor = texture2D(uTexture, vUv).rgb;
// 2. (可选)调整纹理颜色(如转为灰度)
// float gray = (texColor.r + texColor.g + texColor.b) / 3.0;
// texColor = vec3(gray);
// 3. 输出纹理颜色
gl_FragColor = vec4(texColor, 1.0);
}
CPU 端加载纹理并传递给 uniform
javascript
// 1. 创建纹理加载器
const textureLoader = new THREE.TextureLoader();
// 2. 加载图片(替换为你的图片URL,支持本地/网络图片)
const texture = textureLoader.load('https://threejs.org/examples/textures/land_ocean_ice_cloud_2048.jpg',
// 加载成功回调
() => {
console.log('纹理加载成功');
},
// 加载进度回调
(xhr) => {
console.log(`纹理加载进度:${(xhr.loaded / xhr.total) * 100}%`);
},
// 加载失败回调
(err) => {
console.error('纹理加载失败:', err);
}
);
// 3. 设置纹理参数(可选,优化纹理显示)
texture.wrapS = THREE.RepeatWrapping; // 水平方向重复
texture.wrapT = THREE.RepeatWrapping; // 垂直方向重复
texture.magFilter = THREE.LinearFilter; // 放大时线性过滤(更平滑)
texture.minFilter = THREE.LinearMipmapLinearFilter; // 缩小时线性过滤
// 4. 创建Shader材质,传递纹理uniform
const shaderMaterial = new THREE.ShaderMaterial({
vertexShader: `
attribute vec3 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = uv; // 传递纹理坐标
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: fragmentShaderCode,
uniforms: {
uTexture: { value: texture }
}
});
运行效果:平面上会显示加载的图片,
坐标(0~1)对应图片的左上角(0,0)到右下角(1,1),实现图片的完整渲染 —— 若修改
vUv
(如
vUv
),图片会在平面上重复显示两次。
vUv = uv * 2.0
3.2.4 混合颜色:多源颜色叠加
将多个颜色源(如基础色、纹理色、特效色)通过数学运算叠加,实现更丰富的视觉效果,常见的混合方式有 “加法混合”“乘法混合”“线性插值混合”。
案例:纹理色 + 动态特效色混合
glsl
precision mediump float;
uniform sampler2D uTexture; // 纹理
uniform float uTime; // 时间
varying vec2 vUv;
void main() {
// 1. 采样纹理颜色
vec3 texColor = texture2D(uTexture, vUv).rgb;
// 2. 生成动态特效色(随时间和uv变化的色偏)
vec3 effectColor = vec3(
sin(uTime + vUv.x * 10.0) * 0.2 + 0.8, // 红色通道偏移
cos(uTime + vUv.y * 10.0) * 0.2 + 0.8, // 绿色通道偏移
1.0 // 蓝色通道固定
);
// 3. 颜色混合:纹理色 * 特效色(乘法混合,增强色偏效果)
vec3 finalColor = texColor * effectColor;
// 4. 输出混合后的颜色
gl_FragColor = vec4(finalColor, 1.0);
}
运行效果:图片纹理会随时间呈现动态的色偏效果,红色和绿色通道按不同节奏明暗变化 —— 乘法混合的特点是 “暗部更暗,亮部保留”,适合添加色偏、光影等特效。
第四章 片元着色器进阶:特效实现与性能优化
掌握基础颜色计算后,本节将讲解片元着色器的进阶应用:基于鼠标交互的特效、常用内置函数的组合使用,以及关键性能优化技巧(避免帧率下降)。
4.1 进阶应用 1:鼠标交互特效(跟随鼠标的颜色变化)
通过
(鼠标位置)
uMouse
变量,实现片元颜色随鼠标位置动态变化的交互效果:
uniform
片元着色器代码
glsl
precision mediump float;
uniform vec2 uMouse; // 鼠标位置(0~1,归一化坐标)
uniform vec2 uResolution; // 屏幕分辨率
varying vec2 vUv;
void main() {
// 1. 将鼠标位置和片元位置转为屏幕像素坐标
vec2 mousePos = uMouse * uResolution;
vec2 fragPos = vUv * uResolution;
// 2. 计算片元到鼠标的距离
float distance = distance(fragPos, mousePos);
// 3. 基于距离计算颜色:鼠标附近为红色,远处为蓝色
float red = smoothstep(200.0, 50.0, distance); // 50~200像素内,红色从1→0
float blue = smoothstep(50.0, 200.0, distance); // 50~200像素内,蓝色从0→1
float green = 0.2;
gl_FragColor = vec4(red, green, blue, 1.0);
}
CPU 端跟踪鼠标位置并传递 uMouse
javascript
// 初始化鼠标位置(默认屏幕中心)
const mouse = new THREE.Vector2(0.5, 0.5);
// 监听鼠标移动事件
window.addEventListener('mousemove', (e) => {
// 将鼠标像素坐标(e.clientX/e.clientY)归一化到0~1
mouse.x = e.clientX / window.innerWidth;
mouse.y = 1.0 - e.clientY / window.innerHeight; // 翻转Y轴(Three.js Y轴向上)
});
// 创建Shader材质时声明uMouse和uResolution
const shaderMaterial = new THREE.ShaderMaterial({
// 顶点着色器代码(传递vUv)
vertexShader: `
attribute vec3 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: fragmentShaderCode,
uniforms: {
uMouse: { value: mouse },
uResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }
}
});
// 渲染循环中更新uResolution(窗口大小变化时)
function animate() {
requestAnimationFrame(animate);
// 窗口大小适配
if (window.innerWidth !== shaderMaterial.uniforms.uResolution.value.x ||
window.innerHeight !== shaderMaterial.uniforms.uResolution.value.y) {
shaderMaterial.uniforms.uResolution.value.set(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
renderer.render(scene, camera);
}
animate();
运行效果:鼠标移动时,屏幕上会出现一个红色 “热点”,随鼠标位置移动,热点周围从红平滑过渡到蓝,形成跟随鼠标的颜色交互效果 —— 这是网页端 Shader 交互的典型实现思路。
4.2 进阶应用 2:内置函数组合实现复杂特效
GLSL ES 提供了大量内置函数,组合使用这些函数可实现复杂特效(如噪声、边缘检测、模糊),以下以 “环形波纹特效” 为例,讲解内置函数的组合应用。
案例:随时间变化的环形波纹
glsl
precision mediump float;
uniform float uTime; // 时间
uniform vec2 uResolution; // 屏幕分辨率
varying vec2 vUv;
void main() {
// 1. 归一化坐标并居中(将(0,0)移到屏幕中心,范围-1~1)
vec2 uv = (vUv * 2.0 - 1.0) * vec2(uResolution.x / uResolution.y, 1.0);
// (注:乘以宽高比是为了避免波纹在宽屏上拉伸)
// 2. 计算极坐标(距离+角度)
float distance = length(uv); // 到中心的距离(半径)
float angle = atan(uv.y, uv.x); // 与X轴的夹角(弧度)
// 3. 生成波纹效果:随时间和距离变化的亮度
float wave = sin(distance * 10.0 - uTime * 3.0 + angle * 2.0) * 0.5 + 0.5;
// 解释:
// distance*10.0:控制波纹密度(值越大,波纹越多)
// uTime*3.0:控制波纹移动速度(值越大,速度越快)
// angle*2.0:添加角度变化,让波纹更有层次感
// 4. 增强波纹边缘(用step函数生成明暗对比)
float edge = step(0.45, wave); // 波纹亮度>0.45的部分显示为1.0,否则0.0
// 5. 输出最终颜色(黑色背景+白色波纹)
gl_FragColor = vec4(vec3(edge), 1.0);
}
运行效果:屏幕中心向外扩散白色环形波纹,波纹随时间移动,且带有角度方向的明暗变化 —— 案例中组合使用了
(计算距离)、
length
(计算角度)、
atan
(生成波动)、
sin
(生成边缘)四类内置函数,实现了复杂的动态特效。
step
4.3 片元着色器的性能优化技巧
片元着色器是性能消耗的 “重灾区”(片元数量远大于顶点),以下 5 个优化技巧可有效避免帧率下降,确保动画流畅:
4.3.1 减少复杂计算的调用次数
避免在循环中调用复杂函数:GLSL 不支持动态循环次数,且循环会打破 GPU 并行优化,优先用内置函数替代循环(如用
生成周期性效果,而非循环累加);复用计算结果:若多个通道需要相同的计算结果(如
sin
),将结果存储在局部变量中,避免重复计算:
distance
glsl
// 优化前:重复计算distance
float red = sin(distance(uv, center) * 10.0);
float green = cos(distance(uv, center) * 8.0);
// 优化后:复用distance结果
float dist = distance(uv, center);
float red = sin(dist * 10.0);
float green = cos(dist * 8.0);
4.3.2 合理使用纹理过滤与压缩
纹理过滤选择:远处纹理用
(平滑),近处纹理用
THREE.LinearMipmapLinearFilter
,避免使用
THREE.LinearFilter
(像素化);纹理压缩:使用 WebP、ETC1 等压缩纹理格式,减少纹理加载时间和 GPU 内存占用(Three.js 支持
THREE.NearestFilter
加载压缩纹理)。
THREE.TextureLoader
4.3.3 避免动态分支(if/else、switch)
GPU 并行执行时,动态分支会导致 “部分核心等待”(如一半片元走 if 分支,一半走 else 分支),降低执行效率。可用
、
step
、
mix
等内置函数替代动态分支:
smoothstep
glsl
// 优化前:使用if/else
float color;
if (distance < 100.0) {
color = 1.0;
} else {
color = 0.0;
}
// 优化后:用step替代
float color = step(100.0, distance); // distance<100.0时返回0.0,否则1.0(需注意逻辑反转)
// 或用smoothstep实现平滑过渡
float color = smoothstep(90.0, 100.0, distance); // 90~100之间平滑从0→1
4.3.4 控制片元数量
减少屏幕分辨率:若特效对分辨率要求不高,可将渲染器尺寸设为屏幕尺寸的 0.5 倍(
),再通过 CSS 拉伸到全屏,减少 50% 的片元数量;避免全屏渲染:若特效仅需在局部区域显示(如一个圆形区域),可通过
renderer.setSize(width*0.5, height*0.5)
语句丢弃区域外的片元,减少无效计算:
discard
glsl
if (distance(uv, center) > 200.0) {
discard; // 丢弃距离中心超过200像素的片元,不进行后续颜色计算
}
4.3.5 选择合适的浮点数精度
优先使用 mediump:片元着色器默认用
,高精度(highp)仅在需要精确计算时使用(如纹理坐标、光照),低精度(lowp)可用于颜色等对精度要求低的场景;避免高精度纹理采样:纹理采样默认使用 mediump,无需强制指定 highp,减少 GPU 计算压力。
precision mediump float;
第五章 综合实战:带纹理与交互的动态波纹效果
本节整合前文知识点,实现一个 “带图片纹理 + 鼠标交互 + 环形波纹” 的综合案例,涵盖纹理采样、鼠标交互、内置函数组合、颜色混合,帮助你巩固片元着色器核心逻辑。
5.1 完整代码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>片元着色器综合案例:纹理+交互波纹</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.min.js"></script>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<script>
// 1. 基础环境搭建(Three.js三要素)
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x111111); // 背景色:深灰色
document.body.appendChild(renderer.domElement);
camera.position.z = 5;
// 2. 交互数据:鼠标位置跟踪
const mouse = new THREE.Vector2(0.5, 0.5);
window.addEventListener('mousemove', (e) => {
// 鼠标坐标归一化(0~1),并翻转Y轴
mouse.x = e.clientX / window.innerWidth;
mouse.y = 1.0 - e.clientY / window.innerHeight;
});
// 3. 加载纹理图片
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load(
'https://threejs.org/examples/textures/land_ocean_ice_cloud_2048.jpg',
() => console.log('纹理加载成功'),
(xhr) => console.log(`加载进度:${(xhr.loaded/xhr.total)*100}%`),
(err) => console.error('纹理加载失败:', err)
);
// 设置纹理参数
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.LinearFilter;
// 4. Shader代码(核心:片元着色器实现纹理+波纹+交互)
const vertexShaderCode = `
attribute vec3 position;
attribute vec2 uv;
uniform vec2 uResolution;
varying vec2 vUv; // 传递纹理坐标
varying vec2 vScreenPos; // 传递屏幕像素坐标
void main() {
vUv = uv;
// 计算屏幕像素坐标(传递给片元着色器,用于鼠标交互)
vScreenPos = uv * uResolution;
// 顶点最终位置
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShaderCode = `
precision mediump float;
// 输入:全局数据
uniform sampler2D uTexture; // 纹理
uniform float uTime; // 时间
uniform vec2 uMouse; // 鼠标位置(0~1)
uniform vec2 uResolution; // 屏幕分辨率
// 输入:顶点着色器传递的插值数据
varying vec2 vUv; // 纹理坐标
varying vec2 vScreenPos; // 屏幕像素坐标
void main() {
// 步骤1:计算鼠标在屏幕上的像素坐标
vec2 mousePos = uMouse * uResolution;
// 步骤2:计算片元到鼠标的距离,生成波纹强度
float distToMouse = distance(vScreenPos, mousePos);
// 波纹效果:随距离和时间变化的偏移量(仅影响纹理坐标)
float wave = sin(distToMouse * 0.02 - uTime * 5.0) * 0.01;
// 步骤3:将波纹偏移应用到纹理坐标(实现纹理波纹)
vec2 wavedUv = vUv + wave * vec2(sin(uTime), cos(uTime));
// (注:vec2(sin(uTime), cos(uTime))让波纹向任意方向扩散)
// 步骤4:采样纹理颜色(带波纹偏移)
vec3 texColor = texture2D(uTexture, wavedUv).rgb;
// 步骤5:添加鼠标附近的高亮效果(距离越近,亮度越高)
float highlight = smoothstep(300.0, 100.0, distToMouse); // 100~300像素内高亮
texColor *= (1.0 + highlight * 0.5); // 高亮区域亮度提升50%
// 步骤6:输出最终颜色
gl_FragColor = vec4(texColor, 1.0);
}
`;
// 5. 创建Shader材质并传递uniform
const shaderMaterial = new THREE.ShaderMaterial({
vertexShader: vertexShaderCode,
fragmentShader: fragmentShaderCode,
uniforms: {
uTexture: { value: texture },
uTime: { value: 0.0 },
uMouse: { value: mouse },
uResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }
}
});
// 6. 创建平面几何体并添加到场景
const planeGeometry = new THREE.PlaneGeometry(10, 10, 32, 32);
const plane = new THREE.Mesh(planeGeometry, shaderMaterial);
scene.add(plane);
// 7. 渲染循环(更新时间与分辨率)
function animate() {
requestAnimationFrame(animate);
// 更新时间变量(控制波纹速度)
shaderMaterial.uniforms.uTime.value += 0.01;
// 窗口大小适配
if (window.innerWidth !== shaderMaterial.uniforms.uResolution.value.x ||
window.innerHeight !== shaderMaterial.uniforms.uResolution.value.y) {
const width = window.innerWidth;
const height = window.innerHeight;
shaderMaterial.uniforms.uResolution.value.set(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
}
renderer.render(scene, camera);
}
// 启动动画
animate();
</script>
</body>
</html>
5.2 案例核心逻辑解析
纹理与波纹结合:通过
变量计算纹理坐标的偏移量,将偏移后的
wave
传入
wavedUv
,实现纹理的波纹效果;鼠标交互:计算片元到鼠标的距离,生成
texture2D
高亮因子,让鼠标附近的纹理亮度提升,增强交互感;动态效果:
highlight
控制波纹的移动速度,
uTime
让波纹向任意方向扩散,避免单一方向的单调;性能优化:复用
vec2(sin(uTime), cos(uTime))
计算结果,避免重复调用
distToMouse
函数;使用
distance
精度,平衡性能与效果。
mediump
5.3 运行与修改建议
运行效果:深灰色背景下,一张地球纹理图片随鼠标移动呈现环形波纹,鼠标附近区域高亮,波纹随时间向外扩散;修改尝试 1:调整
计算中的
wave
(波纹密度)和
0.02
(波纹强度),观察波纹效果变化;修改尝试 2:将
0.01
改为
wavedUv = vUv + wave * ...
,让波纹从鼠标位置向四周扩散;修改尝试 3:添加
wavedUv = vUv + wave * (vScreenPos - mousePos)
语句,让波纹仅在圆形区域内显示(如
discard
)。
if (distToMouse > 500.0) discard;
第六章 总结与后续学习方向
6.1 本章核心知识点回顾
片元着色器定位:渲染管线中 “逐像素计算颜色” 的关键阶段,输入为
插值数据和
varying
全局数据,输出为
uniform
;核心输入逻辑:
gl_FragColor
变量:接收顶点着色器数据,自动插值实现平滑过渡(如颜色、纹理坐标);
varying
变量:传递全局数据(时间、纹理、鼠标位置),是动态特效与交互的核心; 颜色计算方式:
uniform
基础纯色:固定 RGBA 通道值;渐变颜色:基于坐标 / 时间的动态计算(线性、径向、环形);纹理颜色:通过
和
sampler2D
采样外部图片;混合颜色:多源颜色叠加(加法、乘法、线性插值); 进阶与优化:组合内置函数实现复杂特效(波纹、交互),通过减少计算、避免分支、控制分辨率优化性能。
texture2D
6.2 常见问题与排查技巧
片元着色器编译报错:
忘记声明
(必选);
precision mediump float;
/
uniform
变量在顶点 / 片元着色器中名称 / 类型不一致;颜色通道值使用整数(如
varying
),需改为 0.0~1.0 范围(
vec4(255, 0, 0, 1)
); 纹理显示异常:
vec4(1.0, 0, 0, 1)
纹理路径错误(检查控制台是否有 404 错误);纹理坐标
未正确传递(顶点着色器需赋值
vUv
);纹理未加载完成就渲染(可在
vUv = uv
的加载成功回调中启动渲染); 交互效果无响应:
textureLoader
鼠标位置未正确归一化(需翻转 Y 轴,
);
mouse.y = 1.0 - e.clientY / window.innerHeight
变量未在渲染循环中实时更新(Three.js 中
uMouse
是引用类型,无需重新赋值,只需修改
Vector2
/
x
); 帧率下降:
y
片元数量过多(减少渲染分辨率或使用
丢弃无效片元);存在复杂循环或动态分支(用内置函数替代)。
discard
6.3 后续学习方向
掌握片元着色器核心逻辑后,可按以下路径深入:
光照模型:学习 Phong、Blinn-Phong、PBR(基于物理的渲染)等光照模型,结合顶点法线(
),实现物体的漫反射、高光、金属质感;后期处理:学习 Three.js 的
attribute vec3 normal
,用片元着色器实现模糊、泛光、色调映射、边缘检测等后期特效(如电影级画面调色);高级纹理技术:学习立方体贴图(天空盒)、法线纹理(凹凸映射)、视差纹理(深度感),增强物体的细节表现;噪声与分形:学习 Perlin 噪声、Simplex 噪声的 GLSL 实现,生成自然效果(如地形、云层、火焰、水流)。
EffectComposer
暂无评论内容