Shader(着色器)是运行在GPU上的小程序,它负责计算屏幕上每一个像素的颜色。正因为它天然并行、逐像素运算的特性,用它做出来的视觉效果往往比CSS动画流畅得多,也更细腻。在React项目中接入GLSL着色器并不复杂,下面我们从原理讲起,一步步实现一个可交互的动态渐变背景效果。

一、先搞懂Shader的工作原理
WebGL的渲染管线中有两种着色器是我们能动手写的:顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)。顶点着色器对每个顶点执行一次,主要任务是确定几何体在屏幕上的位置;片元着色器则对每个像素执行一次,决定这个像素最终显示什么颜色。我们要做的大部分视觉效果,比如渐变、噪声、扭曲,核心逻辑都写在片元着色器里。
GLSL是Shader的编程语言,语法接近C语言,但有几个特殊之处:一是所有变量都有精度限定符(比如highp、mediump),在片元着色器中必须声明浮点数精度;二是它没有复杂的内置库,很多数学函数需要自己写;三是变量分attribute、uniform、varying几类,其中uniform是我们从JavaScript端往GPU传数据的主要通道,比如时间、鼠标位置,都是通过uniform传进去的。
理解一个关键点:Shader是逐像素执行的,这意味着你不能在里面写循环遍历所有像素,也不存在上一帧的概念(除非你自己把上一帧渲染到纹理里)。所有像素的颜色都是基于输入坐标和uniform参数独立计算出来的。这种纯函数式的思维模式,是从普通JavaScript过渡到Shader编程最大的思维转变。
二、React接入Shader的两种主流方式
第一种方式是使用原生WebGL API。你需要自己获取canvas上下文、编译着色器、链接程序、创建缓冲区。代码量较大但没有任何依赖,适合追求极致体积或者需要深度定制渲染管线的场景。写一个能跑起来的原生WebGL至少要几十行样板代码,对于快速实现视觉效果来说效率偏低。
第二种方式是使用react-three-fiber,这是Three.js的React封装,配合@react-three/drei中的shaderMaterial工具,可以非常优雅地把自定义Shader封装成一个React组件。安装依赖:
npm install three @react-three/fiber @react-three/drei
封装自定义材质的写法如下,注意shaderMaterial接收uniform定义、顶点着色器、片元着色器三个参数:
import { shaderMaterial } from '@react-three/drei'
import { extend } from '@react-three/fiber'
import * as THREE from 'three'
// 定义自定义材质,包含时间与鼠标两个uniform
const GradientMaterial = shaderMaterial(
{ uTime: 0, uMouse: new THREE.Vector2(0, 0) },
// 顶点着色器
`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
// 片元着色器
`
uniform float uTime;
uniform vec2 uMouse;
varying vec2 vUv;
void main() {
// 根据uv坐标和uTime计算动态渐变颜色
vec2 p = vUv - uMouse;
float d = length(p);
vec3 color = 0.5 + 0.5 * cos(uTime + vUv.xyx + vec3(0.0, 2.0, 4.0));
color += 0.15 / (d * 8.0 + 0.1); // 鼠标位置发出光晕
gl_FragColor = vec4(color, 1.0);
}
`
)
// 注册为JSX可用的元素,注意首字母必须大写
extend({ GradientMaterial })封装好之后,就可以像使用普通Three.js材质一样在JSX中使用<gradientMaterial />了。这种方式的优点是声明式、可组合,uniform的更新交给useFrame处理,代码结构清晰。缺点是引入了Three.js整个渲染体系,打包体积会增加几百KB,如果你的需求只是一个背景特效,可以考虑用原生WebGL或者更轻量的方案。
三、完整实战:随鼠标流动的渐变背景
下面我们把上面的材质用起来,实现一个铺满全屏、随鼠标移动产生光晕、随时间流动变化的渐变背景。整个组件的结构分为三层:Canvas负责创建渲染上下文,OrthographicCamera保证平面铺满屏幕,PlaneGeometry作为Shader的画布。
import { Canvas, useFrame, useThree } from '@react-three/fiber'
import { useRef } from 'react'
import * as THREE from 'three'
function GradientPlane() {
const materialRef = useRef()
const { viewport } = useThree()
const mouse = useRef(new THREE.Vector2(0, 0))
// 每帧更新uniform,驱动动画
useFrame((state) => {
if (!materialRef.current) return
materialRef.current.uTime = state.clock.elapsedTime
// state.pointer的坐标范围是-1到1,转换到0到1
materialRef.current.uMouse.set(
state.pointer.x * 0.5 + 0.5,
state.pointer.y * 0.5 + 0.5
)
})
return (
<mesh scale={[viewport.width, viewport.height, 1]}>
<planeGeometry args={[1, 1]} />
<gradientMaterial ref={materialRef} />
</mesh>
)
}
export default function GradientBackground() {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Canvas orthographic camera={{ zoom: 1, position: [0, 0, 1] }}>
<GradientPlane />
</Canvas>
</div>
)
}这段代码里有几个细节值得注意。useFrame会在每一帧渲染前执行,我们在里面更新uTime让渐变持续流动;state.pointer是react-three-fiber提供的归一化鼠标坐标,除以视口尺寸换算后映射到uv坐标系;平面通过viewport的宽高做缩放,保证无论窗口多大都能铺满。片元着色器中的cos配色公式是社区里非常经典的技巧,三个颜色通道相位错开,就能产生彩虹般的平滑过渡。
如果想要更丰富的效果,可以在片元着色器里引入噪声函数。经典的选择有Simplex Noise和FBM(分形布朗运动),后者通过多层噪声叠加能模拟出云、火焰、水面这类自然纹理。把噪声值加到渐变的相位上,画面立刻就有了流动的质感,这也是很多国外知名落地页背景的实现思路。
四、性能优化与常见坑
虽然Shader跑在GPU上,但不代表可以毫无顾忌。首先要注意片元着色器的计算量,尤其是FBM这种多层循环的噪声,在低端手机上可能掉帧。优化手段包括:降低canvas的分辨率(通过设置dpr为1甚至更低,用像素密度换性能)、减少噪声叠加层数、用mediump精度代替highp。另外,当组件不可见时记得暂停渲染,react-three-fiber的frameloop="demand"模式只在uniform变化时重绘,对静态或低频更新的效果能省下大量GPU开销。
开发过程中最常遇到的报错是着色器编译失败,浏览器控制台会输出类似ERROR: 0:1: 'uTime' : syntax error的信息,前面的数字表示出错的行号。排查时优先检查:浮点数是否漏写小数点(GLSL里1和1.0是两种类型,不能混用)、分号是否齐全、varying变量在两个着色器中是否都声明了、精度声明precision highp float;是否加在了片元着色器开头。另一个高频问题是uniform更新不生效,多半是材质实例没有被正确引用,用ref拿到实例后直接赋值即可,react-three-fiber会自动同步到GPU。
还有一个容易踩的坑是颜色空间。Three.js从r152版本开始默认使用线性色彩空间输出,如果你发现渲染出来的颜色和设计稿对不上、整体偏亮或偏暗,可以在Canvas上配置gl={{ outputColorSpace: THREE.SRGBColorSpace }},或者在片元着色器里手动做伽马校正:color = pow(color, vec3(0.4545));。理解这一点对还原设计稿的视觉效果非常重要。
总的来说,在React中使用GLSL着色器并不需要啃完整本图形学教材。掌握顶点和片元两个着色器的分工、学会用uniform传参、理解uv坐标和时间的配合,就能做出绝大多数常见的动态视觉效果。建议从本文的渐变背景开始,逐步加入噪声、扭曲、混合模式,你很快就能构建出属于自己风格的炫酷页面。