中EN
精修课概念页

片元着色器:让每个像素同时开工

Fragment Shaders & GLSL Basics

更新于 2026-10-15 约 15 分钟GLSL片元着色器WebGLuniform程序化

换一种画画的方式

平时用程序画画,像手抄一本书:先画一个圆,再画一个矩形,再画一条线,一步接一步。

片元着色器不是这样工作的。它更像一台印刷机:同一段代码在屏幕的每个像素上同时运行。你写的不是「先画什么、后画什么」,而是一个函数——输入「我在屏幕上的哪个位置」,输出「我该是什么颜色」。《着色器之书》(The Book of Shaders) 把它比作图形领域的古登堡印刷术:从手机滤镜到 3D 游戏,处处都有它。

一笔一笔画123先画圆,再画矩形,再画线所有像素一起算同一个 main(),每个像素各算各的
左边是串行的思路,按顺序一件件画;右边是片元着色器的思路,每一格都跑同一段代码,只因为位置不同,算出来的颜色才不同。

为什么非得这么干:数一数像素

一帧画面要把每一个像素都算一遍,而且一秒要算几十帧。书里算过一笔账:

分辨率帧率每秒要算的像素
800 × 60030 fps约 1440 万
2880 × 1800(视网膜屏)60 fps约 3.11 亿

CPU 像几根很粗的管道,任务得排着队一个个过;普通电脑上这样的「线程」往往只有几个。GPU 则是成千上万根细管子,每个像素一个小任务,一起往下流。三角函数、矩阵运算这些常用数学,GPU 还有硬件加速,不必全靠软件一步步算。

第一个着色器:只做一件事

最小的片元着色器只有一个 main 函数,最后把颜色交给保留变量 gl_FragColor。颜色是 vec4,四个分量对应 RGBA,每个分量都归一化在 0 到 1 之间。

Hello World:把整块画布涂成一个颜色
改一改

也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。

每个像素都执行同一行赋值,所以整块画布是同一个颜色。改改 vec4 里的数字看看。

这几行里藏着几个之后天天要用的东西:

  • 类型:float、int、bool,以及 vec2、vec3、vec4 这些向量。
  • 预处理:#ifdef GL_ES 只在手机和浏览器这类 OpenGL ES 环境里成立,这时要声明浮点精度:precision mediump float;。可选 lowp、mediump、highp,精度越低通常越快,画质也越差。
  • 构造:向量可以拆开拼,比如 vec4(vec3(1.0, 0.0, 1.0), 1.0)。也可以写一个返回 vec4 的辅助函数,再在 main 里调用。

gl_FragColor 是旧式 GLSL 的写法。现代的核心配置 (core profile) 改成自己声明一个 out vec4 变量来输出颜色,道理一样。

uniform:CPU 发给所有像素的同一份广播

成千上万个线程都需要从 CPU 拿一些相同的输入,比如现在几点了、画布有多大。这类输入叫 uniform:对所有线程都一样,而且只读。它可以是 float、vec2~vec4、mat2~mat4,也可以是纹理 sampler2D、samplerCube。

CPU每帧写一次uniform(人人相同)u_resolutionu_mouseu_time像素 A · 位置 (3, 120)像素 B · 位置 (240, 66)像素 C · 位置 (517, 9)
uniform 由 CPU 每帧写一次,所有像素读到的都是同一份;每个像素唯一不同的输入,是它自己的位置 gl_FragCoord。

书里约定 uniform 名字前加 u_,最常用的是这三个。ShaderToy 上同样的东西换了名字,类型也不一样,移植代码时要留意:

含义本书写法ShaderToy 写法
画布宽高(像素)vec2 u_resolutionvec3 iResolution
鼠标位置(像素)vec2 u_mousevec4 iMouse
启动以来的秒数float u_timefloat iTime

本站的编辑器两种写法都认。先让画面随时间动起来:

用 u_time 做一盏呼吸灯0.0s
改一改

也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。

sin 在 −1 和 1 之间往返,乘 0.5 再加 0.5 就挪到了 0~1,正好拿来当混合比例。

gl_FragCoord:每个像素唯一不同的输入

uniform 人人一样,那像素之间靠什么区分?靠 gl_FragCoord:当前这个线程负责的像素,在屏幕上的坐标。它每个线程都不一样,所以不是 uniform;书里把它叫作 varying。

它的单位是像素,画布换个尺寸数值就全变了。通常先除以画布尺寸,把坐标归一化到 0~1:

vec2 st = gl_FragCoord.xy / u_resolution.xy;

书里接着出了一道题:(0,0)、(1,0)、(0,1)、(0.5,0.5)、(1,1) 分别在画布的哪里?把 st.x 放进红色通道、st.y 放进绿色通道,答案就直接画出来了:

st.xst.y(0, 0)(1, 0)(0, 1)(1, 1)(0.5, 0.5)红色通道 = st.x绿色通道 = st.y
st 的原点在左下角,x 向右、y 向上。把 st.x 当红色、st.y 当绿色:左下几乎是黑的,右下偏红,左上偏绿,右上红绿相加成黄色。

下面就是这段代码。另外,u_mouse 也是像素单位,同样除以 u_resolution 就能和 st 放在一起比较。把鼠标移进画布,白点会跟着你走:

归一化坐标,加上一个跟着鼠标的点
改一改

也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。

画布通常不是正方形,st 在横向上被拉长了。算圆点距离时把 x 乘上宽高比,圆才不会变成椭圆。

没有 print,就用颜色调试

着色器几乎没有调试手段:没有断点,也打印不出变量。最常用的办法是把想检查的值涂成显眼的颜色,看画面去猜它是多少。编译失败时,很多编辑器只会显示一片白屏。本站的编辑器会把出错的那一行标出来,方便不少。

下面这个小工具把一个值分成三种情况来显示:落在 0~1 之间显示成灰度,超过 1 显示成粉色,跌破 0 显示成蓝色。

把数值画出来
改一改

也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。

单独标出越界的部分,算式哪里出了问题一眼就能看到。

再往前一步:插值从哪来

前面一直在画全屏的片元,每个像素只有一个位置可用。在完整的光栅化管线里,片元着色器还有别的输入:顶点着色器用 out 输出值,片元着色器用同名、同类型的 in 接收,程序链接时这两端会被接上。

三角形只有三个顶点,覆盖到的片元却多得多。中间那些片元的输入从哪来?光栅器按每个片元在三角形里的位置,把三个顶点的值插值出来。LearnOpenGL 里有个经典例子:三个顶点分别是红、绿、蓝,插出来就是整面的渐变。下面用一个片元着色器模拟这件事:

模拟光栅器:按重心坐标混合三个顶点的颜色0.0s
改一改

也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。

重心坐标就是「离哪个顶点更近,就听谁的更多」。三个权重加起来等于 1,点在三角形外面时会有权重变成负数。

在游戏里怎么用

取舍与常见的坑

小结与练习

  • 片元着色器是一个「位置 → 颜色」的函数,在每个像素上并行执行;线程彼此看不见,也不记得上一帧。
  • 输出写进 gl_FragColor,所有像素共享的输入用 uniform,像素自己的位置用 gl_FragCoord。
  • 先把坐标除以 u_resolution 归一化到 0~1,后面所有课程都从这一步开始。

练习:在「归一化坐标」那段代码里,用 u_time 让背景的蓝色通道随时间缓慢变化;再试着让白点的大小也随着 sin(u_time) 一呼一吸。做完就可以进入下一课塑形函数了。

  1. The Book of Shaders: about this book
  2. The Book of Shaders: What is a shader?
  3. The Book of Shaders: Hello world!
  4. The Book of Shaders: uniforms
  5. The Book of Shaders: Running your shader
  6. thebookofshaders.com
  7. Getting Started — Shaders
  8. learnopengl.com/Getting-started/Shaders

相关页面

图形学可编程着色器与 GPU 管线Programmable Shaders & GPU PipelineGPU 把第 2 章的概念管线实现为一组硬件阶段,分为完全可编程、可配置但不可编程、完全固定三类。现代 GPU 采用统一着色器架构:各类着色器共用同一套编程模型和核心,由 GPU 自行分配负载。着色器用 HLSL、GLSL 等类 C 语言编写,输入分为 uniform 和 varying 两类。图形学像素着色器Pixel Shader像素着色器(OpenGL 里叫片元着色器)对光栅化产生的每个片元运行,用插值后的顶点数据算出颜色,也可以输出不透明度、修改深度或直接丢弃片元。多渲染目标让它一次输出多组数据,催生了延迟着色。它通常只能写自己所在的像素,但能通过 2×2 quad 拿到相邻像素的导数。图形学GPU 并行架构GPU Parallel ArchitectureGPU 为吞吐量而设计:芯片大部分面积是成千上万个着色器核心,而不是缓存和控制逻辑。它靠在大量线程组 (warp/wavefront) 之间快速切换来隐藏访存延迟,用 SIMD 让一组线程锁步执行同一条指令。理解占用率和线程分歧,是写出高效着色器的关键。图形学塑形函数Shaping Functions塑形函数把归一化到 [0,1] 的一维输入弯成可控制的曲线,是片元着色器里混合颜色、画边、做动画缓动的「基本功」。GLSL 的 pow/step/smoothstep/sin 等多为硬件加速;掌握一维再扩展到二维空间。图形学着色器中的颜色Color in Shaders在 GLSL 里颜色与空间坐标共用 vec2/vec3/vec4:可用 .rgba / .xyzw / .stpq 或下标访问,并 swizzle 任意重排。mix 在 [0,1] 上混合;HSB(HSV)比 RGB 更直观,还可映到极坐标色轮。图形学着色器纹理采样Shader Texture SamplingGPU 把图像存成 sampler2D:一张浮点向量的「表」,坐标连续时硬件做插值。片元里用 texture2D(sampler, uv)(现代 GLSL 为 texture)按归一化 UV 取色。BoS 强调:通道是 [0,1] 连续域、可越界重复、可用噪声扭曲 UV——图像变成可拉伸的「数码软垫」,为后处理与光学玩具式特效铺路。图形学图像处理与后处理Image Processing & Post-Processing后处理是在渲染出图像后再用 GPU 对其做图像处理:把离屏缓冲当纹理,用全屏 pass 让像素着色器(或计算着色器)对每个像素采样邻域并加权求和。核心技巧包括可分离滤波、利用双线性插值减少采样、降采样、以及借助深度/法线保持边缘的(联合)双边滤波(RTR4 12.1 节)。图形学着色方程与经典光照模型Shading Equation & Classic Lighting ModelsGEA 把渲染方程在两条简化假设下化成的求和式称为着色方程:只有理想化的点光源和方向光,间接光只用一个环境光项粗略近似。配上 Lambert、Phong、Blinn-Phong 这类简单 BRDF,它给不出照片级真实感,但能产生可信的画面,是早期游戏的全部光照,也是理解 BRDF 用法的最好入门(GEA4 卷 II 12.4、12.5.1)。图形学着色频率与材质系统Shading Frequency & Material Systems实现着色时要按"计算频率"划分工作:整个 draw call 不变的放到应用端作为 uniform,其余放在相应的可编程阶段。实践中大多数着色在像素级完成,顶点着色器负责变换和变形。材质系统把面向美术的材质与底层着色器组织起来,处理海量着色器变体的组合问题。游戏开发 · UE5材质系统Material SystemUE 材质 (Material) 是用节点图描述「表面如何与光交互」的着色器生成器。它有三个正交维度:着色模型 (Shading Model) 决定光照模型(Default Lit、次表面、清漆、毛发等),混合模式 (Blend Mode) 决定与背景的合成公式(Opaque/Masked/Translucent…),参数 + 材质实例 (Material Instance) 让美术在不重编译着色器的前提下做变体。PBR 输入(Base Color/Roughness/Metallic/Specular)直接对应 RTR4 的金属度-粗糙度工作流;最大的工程坑是静态参数导致的着色器排列爆炸。游戏开发 · UE5材质图到 HLSL 的翻译Material Graph to HLSL Translation材质编辑器点 Apply 之后,引擎先把节点图翻译成 HLSL,再把 HLSL 编译成 GPU 字节码。翻译以 MaterialTemplate.ush 为模板、由 FHLSLMaterialTranslator 做文本替换与常量折叠;内置节点能被折叠,Custom 材质表达式 则不能。每个材质还会按特性/平台/用途生成多份着色器排列。游戏开发 · UE5Custom 材质表达式Custom Material ExpressionCustom 节点允许在材质图里直接写 HLSL:任意数量输入、指定输出类型,可选额外输出、define 与外部 .ush/.usf 包含路径。它是「内置节点做不到」时的逃生口;代价是无法常量折叠、输入仅函数局部可见、且必须是合法 HLSL。优先用内置节点与材质函数,只在必要时上 Custom。图形学二维 SDF 形状2D SDF Shapes在片元着色器里画形,不是发三角形图元,而是对每个像素问「我在形的里面还是外面」。矩形用 step/smoothstep 做轴对齐边界;圆与更复杂形用距离场 (distance field)——到特征点的距离再阈值。这是 2D SDF 程序化造型的入门路径。图形学着色器伪随机Shader Pseudorandom片元着色器里没有真正的「每次调用都不同」的随机:rand(x) 必须是确定性伪随机——同一输入永远同一输出。常用手法是 fract(sin(x)大数);二维则用 dot 把 vec2 压成标量再喂给同一套路。把空间 floor 成格子后,格子共享一个随机值,就能在秩序与混沌之间做生成设计。

被这些页面引用3 篇

输入关键词开始搜索

滚轮或双指缩放 · 拖动平移 · 双击放大 / 还原