Shader(着色器)——是一种在GPU上运行的程序,用于处理图形数据:顶点、片段或计算任务。与CPU上的普通代码不同,着色器在数百个内核上并行工作,独立处理每个元素。根据Khronos Group(2025)的数据,100%的现代3D应用程序都使用着色器——从移动游戏到系统应用程序中的UI效果。开发人员使用专门的着色语言编写着色器:GLSL、HLSL、Metal Shading Language或SPIR-V。
要点
Shader——是用专用语言编写并在GPU上执行的小程序。每个着色器独立处理一个数据元素——顶点、片段(像素)或工作元素——彼此互不影响。GPU的并行性允许同时运行数千个着色器实例。
该术语由Pixar在20世纪80年代引入,用于描述控制RenderMan中表面外观的程序。实时着色器随NVIDIA GeForce 3(2001)——第一张具有可编程管线的显卡——出现。在着色器出现之前,图形通过固定参数配置:颜色、纹理、混合模式。
根据Unity Technologies(2025)的数据,平均每个移动游戏使用50–200个着色器。每个着色器针对不同平台、质量级别和光照配置都有变体(variants)。变体数量每个项目可达10,000个。
着色器架构分为输入数据(属性、uniform变量、纹理)、程序代码和输出数据(颜色、位置)。Uniform变量在着色器启动前由CPU设置,并在帧的所有元素中保持不变。属性是每个元素的数据(顶点坐标、法线、UV坐标)。
SIMT(单指令多线程)——着色器在GPU上的执行模型。一条指令加载一次,但由数百个线程在不同数据上执行。例如,为1000个像素的Fragment Shader启动1000个具有相同代码的线程,但每个线程接收自己的UV坐标和内插属性。
SIMT的重要后果:着色器中的分支代价高昂。如果在if块内一半线程走一条分支,另一半走另一条分支,则GPU会顺序执行所有线程的两个分支。性能下降到50%。避免着色器中的动态分支,或通过在CPU上预先计算来最小化它们。
现代GPU支持多种着色器类型,每种类型针对图形或计算管线的不同阶段。让我们来看看主要类型:顶点着色器、片段着色器、计算着色器和现代网格着色器。
| 着色器类型 | 管线阶段 | 处理 | 语言 |
|---|---|---|---|
| Vertex Shader | 顶点 | 每个顶点 | GLSL, HLSL |
| Fragment Shader | 片段 | 每个像素 | GLSL, HLSL |
| Compute Shader | 计算 | 任意数据 | GLSL, HLSL |
| Geometry Shader | 几何 | 图元 | GLSL, HLSL |
| Tessellation Shader | 曲面细分 | 补丁 | GLSL, HLSL |
| Mesh Shader | 网格管线 | Task + Mesh | HLSL, MSL |
Vertex Shader处理几何体的每个顶点:转换坐标、计算光照、将数据传递到片段着色器。这是图形管线的强制阶段——没有它,顶点将无法显示在屏幕上。Vertex Shader在每帧的每个顶点上调用。
Fragment Shader(或Pixel Shader)——资源最密集的阶段。它使用纹理、光照和材质计算每个片段的最终颜色。Fragment Shader为几何体覆盖的每个像素执行,考虑所有应用的纹理和效果。优化此着色器可带来最大的性能提升。
Compute Shader——用于在GPU上进行任意计算的通用着色器。与图形着色器不同,它不绑定到几何体或像素。Compute Shader通过工作组使用任意数据缓冲区。用于物理、粒子模拟、后效果和神经网络推理。
Mesh Shader——最新类型的着色器,出现在NVIDIA Turing(2018)和Metal 3中。Mesh Shader用一个网格管线取代了顶点、几何和曲面细分着色器。与Task Shader配合使用:Task Shader决定渲染哪些网格组,Mesh Shader即时生成几何体。
着色器使用专门的语言编写,这些语言编译为GPU机器码。语言的选择取决于平台和API。让我们来看看主要语言:GLSL、HLSL、Metal Shading Language和中间格式SPIR-V。
GLSL——用于OpenGL、OpenGL ES和WebGL的着色语言。语法基于C,增加了向量类型(vec2、vec4、mat4)和内置函数(texture、normalize、reflect)。GLSL ES——用于移动设备的版本,精度有限(lowp、mediump、highp)。根据Khronos(2025)的数据,GLSL凭借其跨平台能力仍然是最广泛使用的着色语言。
#version 300 es
precision mediump float;
in vec2 v_texCoord;
uniform sampler2D u_texture;
out vec4 fragColor;
void main() {
fragColor = texture(u_texture, v_texCoord);
}
一个简单的片段着色器根据UV坐标从纹理读取颜色。precision mediump指示GPU对float使用半精度——在移动设备上加速执行25–40%。
HLSL——微软为DirectX开发的着色语言。语法更接近C++,支持类、结构和模板。HLSL用于Windows应用程序,并通过Shader Model 6.7+支持光线追踪、网格着色器和采样器反馈。对于移动开发,HLSL不直接使用,但可以编译为SPIR-V用于Vulkan。
MSL(Metal着色语言)——苹果的着色语言,基于C++14。与GLSL和HLSL不同,MSL与应用程序一起编译,从而消除了设备上的JIT编译。MSL支持指针、模板和C++标准库。用于所有苹果设备:iPhone、iPad、Mac、Apple TV。
#include <metal_stdlib>
using namespace metal;
struct VertexOut {
float4 position [[position]];
float2 texCoord;
};
fragment float4
myFragment(VertexOut in [[stage_in]],
texture2d<float> tex [[texture(0)]]) {
constexpr sampler s = sampler(filter::linear);
return tex.sample(s, in.texCoord);
}
MSL使用[[position]]、[[stage_in]]和[[texture(N)]]属性来绑定资源。苹果编译器为当前GPU生成优化代码,考虑寄存器和缓存数量。
SPIR-V——着色器的中间二进制格式,Vulkan的标准。着色器用GLSL或HLSL编写,编译为SPIR-V并加载到Vulkan应用程序中。SPIR-V不绑定到特定语言——存在从Rust、Python、OpenCL C到SPIR-V的编译器。
移动平台上的着色器与桌面相比有限制:更少的寄存器、有限的精度和缺乏对某些指令的支持。让我们看看Android(Vulkan/OpenGL ES)和iOS(Metal)上着色器的特点。
Android使用GLSL ES用于OpenGL,使用SPIR-V用于Vulkan。OpenGL ES 3.2支持默认精度为mediump的着色器。Vulkan需要通过glslangValidator将GLSL显式编译为SPIR-V。在Android上,着色器从文本资源或编译的SPIR-V文件加载。
Qualcomm Adreno GPU在驱动级别优化着色器。建议:颜色和UV使用mediump,位置仅使用highp;避免在顶点着色器中进行纹理采样;将计算分组为向量(vec4而不是4x float)。根据Qualcomm(2025)的数据,这些优化可提升30–50%。
iOS专门使用Metal Shading Language。着色器通过Xcode与应用程序一起编译为机器码。Metal提供Shader Debugger和GPU Capture用于分析着色器。苹果GPU(TBDR)具有特定的优化:早期片段测试、无内存渲染目标和可编程混合。
根据Apple WWDC 2024的数据,对于iOS着色器,关键是要使用half而不是float,限制寄存器压力(最多64个寄存器)并避免依赖纹理采样。iOS着色器应保持紧凑——最佳大小为50–100条ALU指令。
Unity和Unreal Engine游戏引擎提供可视化着色器编辑器(Shader Graph、Material Editor)和抽象语言(ShaderLab、USF)。开发人员用高级语言编写着色器,引擎为目标平台编译。Unity使用HLSL作为中间语言,Unreal使用USF(Unreal着色器格式)。
着色器优化——移动设备图形开发的关键阶段。编写不当的着色器可能将FPS从60降低到20。让我们来看看主要的优化规则和技术。
使用最小足够的精度:lowp用于颜色和UV,mediump用于法线和光照,highp仅用于位置和矩阵。根据ARM(2025)的数据,在Mali GPU上,mediump运算比highp快2倍。在GLSL ES中,这通过精度限定符设置。
纹理采样——着色器中最昂贵的操作(10–30个周期,而ALU指令只需1–2个周期)。减少采样次数:将纹理合并到图集中,使用uniform数组存储代替纹理,通过导数指令缓存相邻像素的结果。
动态分支(带有uniform变量的if)会降低SIMT管线的性能。用数学函数替换分支:mix()、step()、smoothstep()和clamp()。这些函数在1–2条指令中执行,而分支可能因线程分歧而花费8–16条指令。
// 差:着色器中的动态分支
if (u_enableLight) {
color *= computeLighting(normal);
}
// 好:分支的数学替换
color *= mix(1.0, computeLighting(normal),
float(u_enableLight));
mix()在两个值之间执行线性插值。当u_enableLight=0时,返回1.0——颜色不变。当u_enableLight=1时,返回光照结果。没有分支——所有线程执行相同代码。
让我们看看移动开发的实用着色器示例——从简单着色到程序化纹理生成。每个示例演示一种特定技术。
冯氏模型——具有漫反射和镜面反射分量的经典光照模型。Vertex Shader为每个顶点计算光照,Fragment Shader仅插值结果(高洛德着色)。由于成本低,适合移动设备。
#version 300 es
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec3 a_normal;
uniform mat4 u_mvp;
uniform mat4 u_modelView;
out vec3 v_color;
void main() {
vec3 normal = normalize(mat3(u_modelView) * a_normal);
vec3 lightDir = normalize(vec3(0.0, 1.0, 1.0));
float diff = max(dot(normal, lightDir), 0.0);
v_color = vec3(0.8, 0.2, 0.2) * (0.3 + diff * 0.7);
gl_Position = u_mvp * a_position;
}
着色器计算漫反射光照:法线和光照方向的点积。结果与环境光(0.3)和漫反射(0.7)分量混合。每个顶点执行10–15条ALU指令——在移动GPU上以60 FPS处理100,000个多边形是可以接受的。
程序化纹理在Fragment Shader中生成,无需从文件加载。节省视频内存并简化动画。示例——具有可变单元格大小的棋盘。仅需几条ALU指令——对GPU负载最小。
#version 300 es
precision mediump float;
in vec2 v_uv;
uniform float u_cells;
out vec4 fragColor;
void main() {
vec2 cell = floor(v_uv * u_cells);
float isWhite = mod(cell.x + cell.y, 2.0);
fragColor = mix(vec4(0.1, 0.1, 0.1, 1.0),
vec4(0.9, 0.9, 0.9, 1.0),
isWhite);
}
8条ALU指令——移动设备的理想着色器。u_cells设置每个轴上的单元格数量。floor和mod——廉价操作,在所有移动GPU上1个周期执行。
常见问题
Shader在GPU上以SIMT模式运行——一条指令用于数百个线程。普通C++程序在CPU上以MIMD模式运行。着色器无法访问文件系统、输入/输出和动态内存。它通过缓冲区和纹理接收数据,并返回位置或颜色。
GLSL——用于OpenGL ES和WebGL,跨平台。Metal Shading Language——用于iOS。HLSL——Unity和Unreal Engine的基础。从GLSL开始——理解基础可以迁移到任何着色语言。
主要原因:高精度(使用highp而非mediump)、过度纹理采样、动态分支和超过寄存器限制。使用RenderDoc或Xcode GPU Frame Debugger分析着色器。
Compute Shader——用于GPU上任意计算的着色器:粒子物理、图像处理、布料模拟。与Fragment Shader不同,它不绑定到像素,可以写入任意缓冲区。
可以,通过可视化编辑器:Unity中的Shader Graph,Unreal Engine中的Material Editor或Metal Shader Converter。它们基于节点图生成着色器代码。深度优化需要对着色器语言的理解。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。