WEBGL Shader零基础教程(四):片元着色器核心逻辑

目录

前言

第一章 片元着色器基础认知:渲染管线中的 “像素画家”

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 片元处理器上的小型程序,其核心职责是:

接收输入数据:包括顶点着色器传递的
varying
插值数据(如颜色、纹理坐标)、CPU 传递的
uniform
全局数据(如纹理、时间、分辨率);逐像素计算颜色:根据输入数据,通过数学运算、纹理采样、逻辑判断,计算当前像素的最终颜色(RGBA);输出像素颜色:通过内置变量
gl_FragColor
将颜色传递给后续的 “颜色混合” 阶段,最终显示在屏幕上。

需注意:“片元(Fragment)” 并非完全等同于 “像素(Pixel)”—— 片元是光栅化阶段生成的 “像素候选者”,包含颜色、深度等信息,经过深度测试、模板测试后,才会最终成为屏幕上的像素。

1.2 片元着色器在渲染管线中的位置

WEBGL 渲染管线的核心流程可简化为:

CPU准备数据→顶点着色器(逐顶点)→光栅化(生成片元)→片元着色器(逐片元)→颜色混合/深度测试→屏幕显示

其中,片元着色器处于 “光栅化” 与 “屏幕显示” 之间,是GPU 渲染的最后一个可编程阶段—— 其输出直接决定了每个像素的最终外观,任何颜色错误、纹理异常都可定位到片元着色器逻辑。

1.3 片元着色器的核心特性

与顶点着色器相比,片元着色器有以下 3 个关键特性,需在开发中重点关注:

逐像素并行执行:GPU 会为每个片元分配独立的计算核心,同时执行片元着色器逻辑(如 1000×1000 像素的画面会并行执行 100 万次片元着色器),无 “顺序执行” 概念,每个片元的计算无法访问其他片元的数据;依赖插值数据:片元着色器的
varying
变量值由顶点着色器 “插值生成”—— 若顶点 A 的颜色为红色、顶点 B 为蓝色,那么 A 与 B 之间的片元颜色会从红平滑渐变到蓝,插值精度由 GPU 硬件保证;计算成本敏感:片元数量远大于顶点数量(如一个三角形可能包含数千个片元),片元着色器的计算复杂度直接影响渲染性能(如复杂的噪声计算、多层纹理混合会导致帧率下降)。

第二章 片元着色器的核心输入:数据来源与使用规则

片元着色器的所有颜色计算都基于 “输入数据” 展开,核心输入包括varying 变量(顶点插值数据) 和uniform 变量(全局共享数据),二者共同构成片元着色器的 “数据基础”。

2.1 核心输入 1:varying 变量(顶点插值数据)


varying
变量是片元着色器接收顶点着色器数据的唯一通道,其核心作用是 “传递逐顶点数据并自动插值”,是实现渐变、纹理映射的基础。

2.1.1 varying 变量的使用规则

声明一致性:必须在顶点着色器和片元着色器中 “同名、同类型” 声明(如顶点着色器写
varying vec2 vUv;
,片元着色器也需写
varying vec2 vUv;
),否则数据传递失败;只读属性:片元着色器中只能读取
varying
变量的值,不能修改(值由顶点着色器赋值 + GPU 插值生成);类型限制:支持基础类型(
float
/
int
/
bool
)和向量类型(
vec2
/
vec3
/
vec4
),不支持矩阵类型;插值精度:插值过程由 GPU 自动完成,精度极高,开发者无需关心具体插值算法(默认线性插值)。

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
变量是片元着色器与顶点着色器共享的 “全局数据”,用于传递不随像素变化的参数(如纹理、时间、分辨率、全局颜色)。在片元着色器中,
uniform
变量是实现 “动态特效”“交互控制” 的核心,以下是最常用的 4 类
uniform
变量:

2.2.1 常用 uniform 变量类型与作用
uniform 变量类型 示例变量名 作用 典型应用场景

float

uTime
(时间)、
uAlpha
(透明度)
传递单值参数,控制动态效果 随时间变色、透明度调节

vec2

uResolution
(屏幕分辨率)、
uMouse
(鼠标位置)
传递二维参数,关联屏幕 / 交互位置 屏幕中心渐变、鼠标跟随特效

vec3/vec4

uColor
(全局颜色)、
uLightColor
(光源颜色)
传递颜色或三维参数 全局纯色渲染、光源颜色控制

sampler2D

uTexture
(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
(类型为
vec4
),用于指定当前片元的最终颜色(RGBA)。正确理解
gl_FragColor
的赋值规则、颜色通道范围,是避免 “颜色异常” 的关键。

3.1 gl_FragColor 的基础规则

3.1.1 变量格式与颜色通道


gl_FragColor

vec4
类型的内置变量,格式为
gl_FragColor = vec4(red, green, blue, alpha);
,四个通道的含义与范围如下:

通道 含义 取值范围 说明

red
(R)
红色分量 0.0 ~ 1.0 0.0 表示无红色,1.0 表示最饱和红色

green
(G)
绿色分量 0.0 ~ 1.0 0.0 表示无绿色,1.0 表示最饱和绿色

blue
(B)
蓝色分量 0.0 ~ 1.0 0.0 表示无蓝色,1.0 表示最饱和蓝色

alpha
(A)
透明度分量 0.0 ~ 1.0 0.0 表示完全透明,1.0 表示完全不透明

注意:若通道值超出 0.0~1.0 范围(如
vec4(1.5, -0.2, 0.8, 1.0)
),GPU 会自动 “钳位” 到 0.0~1.0(即 1.5→1.0,-0.2→0.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 }
    }
});

运行效果:平面上会显示加载的图片,
vUv
坐标(0~1)对应图片的左上角(0,0)到右下角(1,1),实现图片的完整渲染 —— 若修改
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
,避免使用
THREE.NearestFilter
(像素化);纹理压缩:使用 WebP、ETC1 等压缩纹理格式,减少纹理加载时间和 GPU 内存占用(Three.js 支持
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 倍(
renderer.setSize(width*0.5, height*0.5)
),再通过 CSS 拉伸到全屏,减少 50% 的片元数量;避免全屏渲染:若特效仅需在局部区域显示(如一个圆形区域),可通过
discard
语句丢弃区域外的片元,减少无效计算:

glsl



if (distance(uv, center) > 200.0) {
    discard; // 丢弃距离中心超过200像素的片元,不进行后续颜色计算
}
4.3.5 选择合适的浮点数精度

优先使用 mediump:片元着色器默认用
precision mediump float;
,高精度(highp)仅在需要精确计算时使用(如纹理坐标、光照),低精度(lowp)可用于颜色等对精度要求低的场景;避免高精度纹理采样:纹理采样默认使用 mediump,无需强制指定 highp,减少 GPU 计算压力。

第五章 综合实战:带纹理与交互的动态波纹效果

本节整合前文知识点,实现一个 “带图片纹理 + 鼠标交互 + 环形波纹” 的综合案例,涵盖纹理采样、鼠标交互、内置函数组合、颜色混合,帮助你巩固片元着色器核心逻辑。

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
(波纹密度)和
0.01
(波纹强度),观察波纹效果变化;修改尝试 2:将
wavedUv = vUv + wave * ...
改为
wavedUv = vUv + wave * (vScreenPos - mousePos)
,让波纹从鼠标位置向四周扩散;修改尝试 3:添加
discard
语句,让波纹仅在圆形区域内显示(如
if (distToMouse > 500.0) discard;
)。

第六章 总结与后续学习方向

6.1 本章核心知识点回顾

片元着色器定位:渲染管线中 “逐像素计算颜色” 的关键阶段,输入为
varying
插值数据和
uniform
全局数据,输出为
gl_FragColor
核心输入逻辑

varying
变量:接收顶点着色器数据,自动插值实现平滑过渡(如颜色、纹理坐标);
uniform
变量:传递全局数据(时间、纹理、鼠标位置),是动态特效与交互的核心; 颜色计算方式
基础纯色:固定 RGBA 通道值;渐变颜色:基于坐标 / 时间的动态计算(线性、径向、环形);纹理颜色:通过
sampler2D

texture2D
采样外部图片;混合颜色:多源颜色叠加(加法、乘法、线性插值); 进阶与优化:组合内置函数实现复杂特效(波纹、交互),通过减少计算、避免分支、控制分辨率优化性能。

6.2 常见问题与排查技巧

片元着色器编译报错
忘记声明
precision mediump float;
(必选);
uniform
/
varying
变量在顶点 / 片元着色器中名称 / 类型不一致;颜色通道值使用整数(如
vec4(255, 0, 0, 1)
),需改为 0.0~1.0 范围(
vec4(1.0, 0, 0, 1)
); 纹理显示异常
纹理路径错误(检查控制台是否有 404 错误);纹理坐标
vUv
未正确传递(顶点着色器需赋值
vUv = uv
);纹理未加载完成就渲染(可在
textureLoader
的加载成功回调中启动渲染); 交互效果无响应
鼠标位置未正确归一化(需翻转 Y 轴,
mouse.y = 1.0 - e.clientY / window.innerHeight
);
uMouse
变量未在渲染循环中实时更新(Three.js 中
Vector2
是引用类型,无需重新赋值,只需修改
x
/
y
); 帧率下降
片元数量过多(减少渲染分辨率或使用
discard
丢弃无效片元);存在复杂循环或动态分支(用内置函数替代)。

6.3 后续学习方向

掌握片元着色器核心逻辑后,可按以下路径深入:

光照模型:学习 Phong、Blinn-Phong、PBR(基于物理的渲染)等光照模型,结合顶点法线(
attribute vec3 normal
),实现物体的漫反射、高光、金属质感;后期处理:学习 Three.js 的
EffectComposer
,用片元着色器实现模糊、泛光、色调映射、边缘检测等后期特效(如电影级画面调色);高级纹理技术:学习立方体贴图(天空盒)、法线纹理(凹凸映射)、视差纹理(深度感),增强物体的细节表现;噪声与分形:学习 Perlin 噪声、Simplex 噪声的 GLSL 实现,生成自然效果(如地形、云层、火焰、水流)。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
水母云的头像 - 鹿快
评论 抢沙发

请登录后发表评论

    暂无评论内容