想象你是一家网红火锅店的后厨。午市高峰,成千上万片毛肚、鸭血、土豆片从切配间传来,每位食客的锅底都在咕嘟作响。你不可能让一位师傅从头到尾做完所有菜,于是安排了流水线:切配师傅只负责切,摆盘师傅只负责摆,大家都照着同一本“标准菜谱卡”干活。GPU 就是这座后厨,而 GLSL(OpenGL Shading Language,着色器语言)就是写菜谱卡的语言。这张卡要被复印几百万份,每位师傅(GPU 的每个核心)同时执行同一份卡上的指令,各自处理自己手里的一枚顶点或一个像素。本文将带你从零读懂这份“菜谱卡”:认识食材分类(数据类型)、订单标签(变量限定符)、厨具用法(内置函数)、卡片版式(着色器结构)、前后工序(顶点与片段着色器),以及师傅培训上岗(编译链接)的全过程。

岗位关联

掌握 GLSL 语法是进入 GPU 世界的第一把钥匙:无论做游戏渲染、数据可视化还是 AI 视觉加速,你都需要用这门“厨房语言”让成千上万个处理器为你同时干活。

目录
  1. 可视化演示
  2. 1. 数据类型
  3. 2. 变量限定符
  4. 3. 内置函数
  5. 4. 着色器结构
  6. 5. 顶点与片段着色器
  7. 6. 编译链接流程
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示GLSL着色器从源码编译到GPU渲染的完整流程:编译链接管线、数据类型与限定符传递、顶点/片段着色器协同工作。切换模式查看不同阶段。

本节配有交互动效(进入视口后自动加载,约 3KB)。

GLSL编译链接管线 源码以字符串提交GPU驱动编译 顶点/片段着色器分别编译为着色器对象 链接为程序对象后绑定使用 Source→Compile→Link→UseProgram 运行时编译是GLSL核心特征

1. 数据类型

后厨备料间里,食材分门别类摆得整整齐齐:整颗蒜是单独的“标量”,一盘毛肚拼盘按两样、三样、四样装,对应“向量”;一张码放调料的小方桌对应“矩阵”;那台插着电源、伸手进去能取食材的冷藏库,则对应“纹理采样器”。GLSL 没有 Python 那种随意的动态类型,每一种食材必须明确标注品类,GPU 才知道该用多大的盘子(寄存器)、多快的火候(指令)来伺候它。

GLSL 的标量类型有 int、float、bool 三种。注意它和 C 语言明显不同:float f = 1; 是编译错误,你只能写 float f = 1.0;。GPU 为了让所有线程用完全一致的节奏执行,坚决不做“看情况自动换盘子”的隐式转换。想转换?请显式写 float(1) 或构造器。

向量是 GLSL 的主角。vec2 装两个浮点分量,vec3 三个,vec4 四个;整数向量有 ivec2/3/4,布尔向量有 bvec2/3/4。向量分量可以像拼盘方位一样用 .x/.y/.z/.w(位置)、.r/.g/.b/.a(颜色)、.s/.t/.p/.q(纹理坐标)三种别名读取,还能写 pos.xy 这样的 swizzle 截取操作。矩阵 mat2/mat3/mat4 用来做几何变换;sampler2D 是把纹理接进着色器的“钥匙”,不能参与算术,只能交给 texture() 采样函数使用。比如我们经常要算一个向量的长度:

把食材分好类,后厨的每个工位才能高效运转。写 GLSL 时也一样:开口先想清楚手里是标量、拼盘、调料桌还是冷藏库。类型对了,GPU 才不至于在高峰期手忙脚乱。

∥v⃗∥=vx2+vy2+vz2\displaystyle \|\vec{v}\| = \sqrt{v_x^2 + v_y^2 + v_z^2}
内置函数 length() 就是在算这个公式:把向量的每个分量平方、求和、再开根号,得到向量的“总长度”。
  1. 分量平方向量的三个分量分别平方,即 \(v_x^2\)、\(v_y^2\)、\(v_z^2\),对应 GLSL 里 v.x*v.x 这类运算。
    vx2+vy2+vz2v_x^2 + v_y^2 + v_z^2
  2. 平方求和把三个平方项累加,得到向量长度的平方 \(S\)。
    S=vx2+vy2+vz2\displaystyle S = v_x^2 + v_y^2 + v_z^2
  3. 开方还原对 \(S\) 开平方,得到欧氏长度,等价于一次 length(v) 调用。
    ∥v⃗∥=S\displaystyle \|\vec{v}\| = \sqrt{S}

2. 变量限定符

后厨里每份食材都贴着订单标签:写着“接自切配间”的,是后端传进来的;写着“送往摆盘间”的,是要交给下一岗的成品;墙上贴的“今日全场微辣”是老板开餐前统一定的告示,配菜途中谁也不能改动;菜谱角落写着“每锅 3 克盐”,这是打磨不动的铁律。GLSL 的变量限定符,正是这些标签。

  • in:从上一阶段接收输入。顶点着色器中来自顶点属性,片段着色器中来自顶点着色器输出的插值。
  • out:把结果输出给下一阶段。顶点着色器输出给光栅化,片段着色器输出为像素颜色。
  • uniform:CPU 在绘制前设定的全局只读变量,对本次绘制中的所有顶点、所有片段数值完全相同。
  • const:编译期就固定的常量,写入代码即不可变。

顶点着色器里最典型的操作,是把 CPU 传入的模型坐标乘上唯一的 MVP 矩阵(模型-视图-投影矩阵),得到变换后的位置。这正是一道“订单+主厨动作”的完美结合:

标签决定命运——uniform 像老板的“全场微辣”,谁都不能临时改口味;in 与 out 标清食材来路和去向;const 则是菜谱上写死的水盐比例。标签贴对,GPU 才能一眼看懂自己的分工。

p⃗′=M×p⃗\displaystyle \vec{p}' = M \times \vec{p}
\(M\) 是一个 uniform mat4,由 CPU 每帧传入;\(\vec{p}\) 是该顶点自身的坐标(in vec3)。矩阵乘法把该顶点的齐次坐标变换到新的位置,所有顶点共用同一份 \(M\),却各自得到不同的 \(\vec{p}'\)。
  1. 定义变换矩阵uniform mat4 M 保存模型-视图-投影组合矩阵,由 CPU 通过 glUniformMatrix4fv 传入,对所有顶点只读共享。
    M=[mij]4×4\displaystyle M = [m_{ij}]_{4\times 4}
  2. 表示顶点坐标着色器通过 in vec4 p 拿到每个顶点的齐次坐标,第四维通常为 1。
    p⃗=(x,y,z,1)⊤\vec{p} = (x, y, z, 1)^\top
  3. 矩阵乘向量GPU 将矩阵每一行与向量点积,得到新坐标,完成旋转、平移、缩放与投影的叠加。
    p⃗′=M×p⃗\vec{p}' = M \times \vec{p}

3. 内置函数

后厨墙上挂着一排标准厨具:量勺负责“按比例勾兑”的 mix,漏勺负责“兜住边界”的 clamp,榨汁机把味道拉回均衡浓度的 normalize,温度计负责“平滑过渡”的 smoothstep。GLSL 内置了一批被 GPU 硬件高度优化的函数,相当于官方免费提供的厨具,直接取用即可。

基础数学有 sin/cos/tan、pow/exp/log、abs/floor/ceil/fract;向量几何有 dot(点积)、cross(叉积)、normalize(归一化)、length(长度)、distance(距离);实用工具有 clamp(截断到区间)、mix(线性插值)、smoothstep(平滑阶梯);纹理采样有 texture(tex, uv)。一次内置函数调用往往就是一条硬件指令,比自己手写循环快几个数量级。

最常用的 mix,相当于“按比例勾兑两味料”:

有了这些“官方厨具”,你就不必亲手实现泰勒展开或插值循环。后厨之所以快,正是因为每个工位都用同一套千锤百炼的工具;GPU 上的着色器也因此能写得更短、跑得更稳。

mix(a,b,t)=a×(1−t)+b×t\displaystyle \mathrm{mix}(a, b, t) = a \times (1 - t) + b \times t
当 t=0 时返回 a,t=1 时返回 b,t=0.5 时返回二者正中间的值。颜色渐变、位置过渡、平滑切换,处处都是 mix 的身影。
  1. 确定两端取值a 与 b 是被插值的两端,类型可以是 float、vec2/3/4,每个分量各自独立插值。
    a,b∈Rna, b \in \mathbb{R}^n
  2. 计算混合比例t 是比例系数,通常落在 0 到 1 之间,控制结果偏向哪一端。
    t∈[0,1]t \in [0, 1]
  3. 加权求和a 按 \((1-t)\) 加权、b 按 \(t\) 加权后相加,得到最终的插值结果。
    a×(1−t)+b×t\displaystyle a \times (1 - t) + b \times t

4. 着色器结构

一张标准菜谱卡有自己的版式:抬头写菜谱版本号,正文是主厨步骤,最下方贴着“食材交接清单”。GLSL 着色器程序同样有一套固定骨架,初学者最忌天马行空地乱写——先记住框架,再填充内容。

骨架自上而下依次是:第一行 #version 300 es(GLSL ES 3.0 版本声明,必须是第一个非空行);随后在全局作用域声明 in / out / uniform 变量;然后定义唯一的入口函数 void main() { ... }。注意:main 没有返回值,一切结果通过 out 变量或内置输出变量(如片段着色器的 gl_FragColor 在旧版中、out vec4 fragColor 在新版中)交给 GPU;一个着色器只能有一个 main。

这种“一份程序、海量副本”的结构,本质是同一个函数被并行地应用到每个输入上:

菜谱卡写得好不好,直接决定后厨是否乱套:版本号像印刷版号,main 是不可省略的步骤区,接口声明则把“食材交接”画得清清楚楚。照着骨架写,你的第一个着色器就成功了一半。

yi=f(xi),i=1,2,…,n\displaystyle y_i = f(x_i), \quad i = 1, 2, \ldots, n
\(f\) 就是你写的 main 函数;\(x_i\) 是第 \(i\) 个顶点或片段的输入;\(y_i\) 是它的输出。GPU 不会写 for 循环一个个跑,而是让 \(n\) 个线程同时执行 \(f\)。
  1. 定义着色器函数main() 是每个着色器的执行体,相当于映射 \(f\),输入由 in 声明,输出由 out 声明。
    f:X→Yf: X \to Y
  2. 列出输入集合一次绘制有 \(n\) 个顶点需要在顶点阶段处理,或 \(n\) 个片段需要在片段阶段处理。
    {x1,x2,…,xn}\{x_1, x_2, \ldots, x_n\}
  3. 并行映射GPU 为每个输入分配一个线程同时执行 \(f\),因此总耗时趋近于单个输入的处理时间,而不是 \(n\) 倍。
    yi=f(xi)y_i = f(x_i)

5. 顶点与片段着色器

后厨流水线上,切配师傅和摆盘师傅是两种关键角色,各守一岗。GPU 渲染管线里同样有两个必须成对出现的着色器:顶点着色器(vertex shader)和片段着色器(fragment shader)。前者负责把每块食材(顶点)切好、摆正位置,后者负责给每份端出去的菜(像素)上色。

顶点着色器对每个顶点执行一次:拿到 in 顶点坐标(比如三维模型的 xyz),乘上 uniform 矩阵,算出 gl_Position(裁剪坐标),并把颜色、法线等数据通过 out 传下去。下面是一对最简着色器:

片段着色器对每个片段执行一次,输出最终颜色。顶点着色器的 out 变量不会原样到达片段阶段,而是被 GPU 按重心坐标在三角形内部插值。比如线段两端颜色分别是 \(c_0\) 和 \(c_1\),中间某点的颜色就是:

切配和摆盘通过一条插值滑轨无缝衔接,流水线节奏由 GPU 硬件自动编排。你只需管好两头:顶点阶段把几何摆对,片段阶段把颜色调好,剩下的交给光栅化。

c⃗frag=(1−t)⋅c⃗0+t⋅c⃗1\displaystyle \vec{c}_{\text{frag}} = (1 - t) \cdot \vec{c}_0 + t \cdot \vec{c}_1
这就是为什么一个三角形内部会出现自然的颜色渐变——片段拿到的是插值后的结果,而不是某个顶点的原始值。想禁用插值,可以用 flat 平坦限定符。
  1. 确定两端颜色out vec3 vColor 保存每个顶点的颜色,线段两端分别为 \(\vec{c}_0\) 与 \(\vec{c}_1\)。
    c⃗0,c⃗1\vec{c}_0, \vec{c}_1
  2. 计算插值比例光栅化阶段按片段在三角形内的重心坐标计算出比例 \(t\)。
    t=λ1(重心坐标分量)\displaystyle t = \lambda_1 \quad (\text{重心坐标分量})
  3. 线性混合颜色片段着色器收到的 in 颜色即加权混合结果,等价于 mix(c0, c1, t)。
    c⃗frag=(1−t) c⃗0+t c⃗1\displaystyle \vec{c}_{\text{frag}} = (1-t)\,\vec{c}_0 + t\,\vec{c}_1
glsl8 行
// 顶点着色器#version 300 eslayout(location = 0) in vec3 aPos;out vec3 vColor;void main() {    vColor = aPos * 0.5 + 0.5;   // 把位置映射成颜色    gl_Position = vec4(aPos, 1.0);}
glsl8 行
// 片段着色器#version 300 esprecision mediump float;in vec3 vColor;out vec4 fragColor;void main() {    fragColor = vec4(vColor, 1.0);}

6. 编译链接流程

厨师上岗前要过两道关:先读懂菜谱(编译),再和配菜间、摆盘间对好接口(链接)。GLSL 程序要跑在 GPU 上,同样要经历“编译 + 链接”两个阶段,只不过这两道关由 OpenGL 的 API 负责。

流程是这样的:先 glCreateShader 创建着色器对象,传入 GLSL 源码并 glCompileShader 编译;然后调用 glGetShaderiv 检查 GL_COMPILE_STATUS,失败则用 glGetShaderInfoLog 取回错误日志(比如“第 3 行缺分号”这类提示);接着把编译好的顶点着色器和片段着色器 glAttachShader 到 glCreateProgram 创建的程序对象上,调用 glLinkProgram 链接,再用 glGetProgramiv 检查链接状态;最后 glUseProgram 激活程序,更新 uniform,就可以开始绘制了。

链接之所以存在,是为了校验“接口契约”——顶点着色器的 out vec3 vColor; 必须与片段着色器的 in vec3 vColor; 名字、类型一致。这样一份链接好的程序,运行时的并行规模远大于你写的代码行数:

“培训”完成后,这些师傅就能在每帧渲染的高峰期同时开工。理解编译、链接、使用这三步,你才能在报错时一眼看出是“菜谱写错了”(编译失败)还是“前后工序对不上”(链接失败),从而快速修复。

Ninvocations=Nvertices+Nfragments\displaystyle N_{\text{invocations}} = N_{\text{vertices}} + N_{\text{fragments}}
一份几十行的着色器,可能被 GPU 同时展开成几十万份副本执行。编译期多花一点时间做检查,换来的是运行期海量并行的确定性。
  1. 统计顶点任务数一次绘制调用提交的所有顶点,每个顶点触发一次顶点着色器执行。
    Nvertices=顶点总数N_{\text{vertices}} = \text{顶点总数}
  2. 统计片段任务数光栅化产生的所有片段,每个片段触发一次片段着色器执行。
    Nfragments=覆盖像素总数(含多重采样)N_{\text{fragments}} = \text{覆盖像素总数(含多重采样)}
  3. 并行总开销两类任务在 GPU 的并行核心上同时执行,总调用次数为二者之和。
    Ninvocations=Nvertices+NfragmentsN_{\text{invocations}} = N_{\text{vertices}} + N_{\text{fragments}}

AI 应用场景

实时游戏渲染中的材质着色

阶段:图形渲染管线(AI 虚拟环境的视觉生成)

解决的问题:3D 游戏场景中成千上万个顶点需要每帧做坐标变换、每个像素要计算光照颜色。CPU 顺序循环只能处理少量模型;用顶点着色器并行变换顶点、片段着色器并行算颜色后,百万级三角形也能实时绘制。

具体案例:在自研引擎或 Unity Shader 中,写一个 GLSL 顶点着色器让树木顶点按风力值做正弦摆动;再用片段着色器中的 mix 与 smoothstep 把草地从枯黄渐变到嫩绿,模拟季节变化。

大规模数据热力图可视化

阶段:数据分析与可视化呈现

解决的问题:百万级数据点画热力图,CPU 逐格填色既慢又占内存。把数据写进纹理(sampler2D),在片段着色器里用 texture() 采样、用 mix() 映射颜色,一次 draw call 就能让 GPU 并行画出整幅图。

具体案例:城市人流平台把密度矩阵写入纹理,片段着色器读取采样值,按 mix 渐变把低密度区域画成冷色、高密度区域画成暖色,拖拽缩放手感依然流畅。

AI 视觉前处理的 GPU 加速

阶段:数据预处理(训练与推理流水线前端)

解决的问题:视频流逐帧缩放、归一化、灰度化在 CPU 上耗时严重;这些逐像素操作正是片段着色器的天然场景——每个像素独立计算,天然并行。

具体案例:在推理框架的自定义算子中,用 GLSL 把 4K 画面每个像素的 RGB 值映射到 [0,1],再做 mix 白平衡校正,帧率比 CPU 实现提升一个数量级。

Python 演示

python45 行
import math def mix(a, b, t):    return a * (1.0 - t) + b * t def clamp(x, lo, hi):    return max(lo, min(hi, x)) def fragment_shader(u, v, time):    # 模拟 GPU 片段着色器:输入像素坐标,输出 RGB 颜色    r = mix(0.14, 0.95, 0.5 + 0.5 * math.sin(u * 8.0 + time))    g = mix(0.51, 0.23, 0.5 + 0.5 * math.cos(v * 6.0 + time * 0.7))    b = mix(0.80, 0.10, 0.5 + 0.5 * math.sin((u + v) * 5.0 - time * 0.5))    return (clamp(r, 0.0, 1.0), clamp(g, 0.0, 1.0), clamp(b, 0.0, 1.0)) def render(width, height, time):    # 逐像素调用片段着色器,模拟 GPU 并行执行    rows = []    for v in range(height):        line = []        for u in range(width):            r, g, b = fragment_shader(u / width, v / height, time)            line.append("#" if r + g + b > 1.5 else ("+" if r + g + b > 0.9 else "."))        rows.append("".join(line))    return "\n".join(rows) def to_glsl_source():    # 生成真实的 GLSL 片段着色器源码    return """#version 300 esprecision mediump float;uniform float u_time;out vec4 fragColor;void main() {    vec2 uv = gl_FragCoord.xy / vec2(64.0, 48.0);    vec3 col = mix(vec3(0.14, 0.51, 0.80),                   vec3(0.95, 0.23, 0.10),                   0.5 + 0.5 * sin(uv.x * 8.0 + u_time));    fragColor = vec4(col, 1.0);}""" if __name__ == "__main__":    print("=== 生成的 GLSL 片段着色器源码 ===")    print(to_glsl_source())    print("\n=== 用 Python 模拟片段着色器渲染 64x24 帧(time=0.0)===")    print(render(64, 24, 0.0))
目的:用 Python 逐像素模拟 GPU 片段着色器的执行逻辑,同时输出一段可直接交给 OpenGL 编译的真实 GLSL 源码,帮你在没有 GPU 的环境下也能理解着色器的工作方式。
  1. def mix(a, b, t):复现 GLSL 内置函数 mix 的线性插值公式
  2. r = mix(0.14, 0.95, 0.5 + 0.5 * math.sin(u * 8.0 + time))用正弦函数生成随像素横坐标和时间变化的混合系数
  3. return (clamp(r, 0.0, 1.0), clamp(g, 0.0, 1.0), clamp(b, 0.0, 1.0))把颜色分量限制在 [0,1],确保输出合法
  4. def render(width, height, time):按像素坐标逐个调用片段着色器,模拟 GPU 的并行执行
  5. line.append("#" if r + g + b > 1.5 else ("+" if r + g + b > 0.9 else "."))把颜色亮度映射成字符,拼出 ASCII 画
  6. def to_glsl_source():生成与 Python 逻辑对应的真实 GLSL 源码
  7. vec2 uv = gl_FragCoord.xy / vec2(64.0, 48.0);计算当前片段的归一化坐标,相当于 Python 里的 u / width
  8. fragColor = vec4(col, 1.0);让片段输出最终颜色,即片段着色器的 out 接口
  9. print(to_glsl_source())打印 GLSL 源码,验证 Python 与 GPU 版本语义一致

常见误区

误区写 `float f = 1;` 或 `vec2 v = vec2(1, 2);` 报错时一头雾水,觉得 GPU 太死板——C 语言里 `int` 转 `float` 明明是自动的,凭什么 GLSL 不行?

正解GLSL 语法极为严格,禁止 int 与 float 之间的隐式转换,必须写 1.0、vec2(1.0, 2.0),或用 float() 显式转换。原因在于 GPU 的着色器会被编译成近似的硬件指令,指令与源码几乎一一对应;隐式转换会引入运行时开销和语义歧义,破坏所有线程的确定性。

为何常见:C、Java、JavaScript 中隐式转换太常见,形成了惯性思维;而且初学者通常还没经历过 GPU 调试,不理解“确定性”对并行执行的意义。

误区以为 `uniform` 就是普通全局变量,可以在 `main` 里直接改值,比如写 `uTime += 1.0;`,结果编译报错,还以为是编译器出了 bug。

正解uniform 是 CPU 到 GPU 的只读常量,着色器只能读不能写。想改变它,只能由 CPU 在绘制前用 glUniform* 系列接口更新。它就像老板开工前定下的“全场微辣”,开餐后任何人不得更改。

为何常见:大多数语言的全局变量默认可写,习惯性把 uniform 当成“便于传参的全局变量”,忽略了它底层是常量内存绑定这一事实。

误区初学者想实现模糊效果,以为在片段着色器里能像访问数组那样拿到 `(x-1, y)` 邻居像素的值,于是到处找“像素数组”的写法。

正解片段着色器一次只能看到当前片段,无法直接访问其他片段。要实现模糊,必须先把画面渲染到纹理(FBO),下一帧在该纹理上采样,用多个偏移的 texture() 调用模拟邻居读取。纹理是“跨片段通信”的唯一通道。

为何常见:顺序编程(CPU 的 for 循环)里数组随手可读,GPU 的“每个像素独立并行”模型彻底改变了这一前提,初学者很难立刻转换思维。

误区认为 `out` 到 `in` 只是简单数值拷贝,顶点是红色、片段就一定是红色,于是看到三角形内部出现渐变时感到莫名其妙。

正解顶点着色器的 out 数据会在光栅化阶段被 GPU 自动插值,片段着色器收到的是插值后的结果。所以一个三角形的颜色会在顶点之间平滑过渡。若想禁用插值,需使用 flat 平坦限定符(GLSL ES 3.0 可用)。

为何常见:把着色器想象成普通的 CPU 函数调用链,忽略了“光栅化”这个 GPU 特有的中间环节。

面试题

Q1. GLSL 中表示四维浮点向量的类型是?

单选题 · easy

  • A.vec3 三维浮点向量
  • B.vec4 四维浮点向量← 正确答案
  • C.mat4 四阶浮点矩阵
  • D.uvec2 二维无符号整型向量

答案:B。GLSL 用 vec 前缀加维度数字表示浮点向量,vec4 是四维浮点向量,常用于坐标 (x,y,z,w) 和颜色 (r,g,b,a)。矩阵用 mat 前缀;三维浮点向量是 vec3,不是 mat4;无符号整型向量是 uvec 系列,均不符合题意。

Q2. mix(0.0, 10.0, 0.5) 的计算结果是?

单选题 · easy

  • A.0.0,完全取起点值
  • B.10.0,完全取终点值
  • C.5.0,两端中点插值← 正确答案
  • D.2.0,按比例加权缩放

答案:C。mix(a, b, t) 等价于 a*(1-t)+b*t。当 t=0.5 时结果为 (0.0+10.0)/2=5.0。A 是 t=0 的结果,B 是 t=1 的结果,D 的系数没有对应依据。插值类函数是着色器中最常用的工具之一。

Q3. 由 CPU 每帧设置、在整个绘制过程中只读、对所有顶点和片段取值一致的变量限定符是?

单选题 · mid

  • A.in 输入限定符
  • B.out 输出限定符
  • C.uniform 全局统一变量← 正确答案
  • D.const 编译期常量

答案:C。uniform 由 CPU 通过 glUniform* 接口设置,一帧内对所有顶点、片段保持相同值且只读。in/out 是流水线接口,属于阶段间数据传递;const 是编译期固定的常量,不满足“CPU 每帧设置”这一条件。

Q4. 下列哪些属于 GLSL 内置函数?

多选题 · mid

  • A.sin 和 cos 三角函数← 正确答案
  • B.mix 和 clamp 实用函数← 正确答案
  • C.normalize 和 length 向量函数← 正确答案
  • D.printf 和 fopen 标准库函数

答案:ABC。GLSL 内置函数包括三角函数 sin/cos、插值截断 mix/clamp、向量工具 normalize/length 等,且都被 GPU 硬件优化过。printf 和 fopen 是 C 标准库函数,着色器里不存在终端输出和文件系统。

Q5. 片段着色器接收到的 in 变量值来自哪里?

单选题 · mid

  • A.顶点着色器输出的插值结果← 正确答案
  • B.顶点着色器输出的原始值
  • C.CPU 直接传入的顶点属性
  • D.显卡驱动随机生成的值

答案:A。顶点着色器的 out 变量经过光栅化阶段按重心坐标插值后,才作为片段着色器的 in 变量到达。B 忽略了插值环节;C 是顶点阶段的数据来源;D 不存在这种机制。理解“插值在中间”是掌握 GPU 流水线的关键。

Q6. 关于顶点着色器和片段着色器的协作,正确的说法有?

多选题 · mid

  • A.顶点着色器负责坐标变换← 正确答案
  • B.片段着色器负责输出像素颜色← 正确答案
  • C.光栅化阶段会进行插值← 正确答案
  • D.片段着色器在顶点着色器之前执行

答案:ABC。流水线顺序是顶点着色器 → 光栅化(插值)→ 片段着色器。顶点着色器做坐标变换,光栅化在三角形内插值,片段着色器输出最终颜色。D 把执行顺序完全说反了,片段着色器永远在顶点着色器之后运行。

Q7. 以下哪段 GLSL 代码在语法上合法?

单选题 · hard

  • A.float f = 1; 触发编译错误
  • B.vec2 v = vec2(1.0, 2.0); 合法← 正确答案
  • C.int i = 1.0; 触发编译错误
  • D.bool b = 1; 触发编译错误

答案:B。GLSL 禁止 int 与 float 隐式转换。vec2(1.0, 2.0) 用构造器显式创建向量,类型完全匹配,因此合法;A、C、D 都涉及标量类型混用,编译期就会报错。写 GLSL 时浮点字面量必须带小数点,这是最常见的语法陷阱。

Q8. 关于 GLSL 编译与链接流程,正确的说法有?

多选题 · hard

  • A.顶点与片段着色器需分别编译← 正确答案
  • B.链接阶段会校验 in/out 接口← 正确答案
  • C.编译日志可查看具体错误信息← 正确答案
  • D.编译失败时程序自动跳过绘制

答案:ABC。GLSL 程序先分别编译顶点和片段着色器,再链接成完整程序;链接时会校验两阶段 in/out 的接口匹配;glGetShaderInfoLog 可以取回编译日志做调试。编译失败时绘制确实不会执行,但程序不会“自动跳过绘制”,而是需要开发者处理错误状态。