OpenGL 可编程性发展史
OpenGL 可编程性(Programmability)的发展跨越了众多硬件版本和 OpenGL 扩展,最终汇聚成现代的 OpenGL 着色语言(OpenGL Shading Language,简称 GLSL)。本文介绍可编程性如何在 OpenGL 中诞生并发展。
基础可配置硬件
着色器(Shader)的概念虽然早已存在,但将其应用于硬件渲染的想法却是后来才出现的。早期的着色器系统(如 Pixar 的 RenderMan 标准)都是基于软件的。
早期的消费级图形硬件本质上只是一个基础光栅化器,能够将单个纹理映射到三角形上。CPU 必须完成顶点数据的初始变换。多重纹理通过多通道技术实现:简单地使用混合函数(blend functions)再次渲染多边形。这类图形芯片能执行的唯一逐片段操作是将单个插值颜色与单个插值纹理坐标的纹理颜色相乘或相加。
消费级图形芯片真正可编程性的开端是多重纹理(multitexture):将两个或更多纹理映射到同一三角形的能力。ARB_multitexture 是第一个 ARB 扩展,旨在提供对此硬件功能的访问。
纹理环境(Texture Environment)
OpenGL 1.0 有一个称为纹理环境的设置,控制在单纹理情况下逐顶点颜色如何与纹理颜色组合。ARB_multitexture 扩展了这一基本概念:每个纹理都有独立的环境函数,在每个阶段,前一阶段的颜色根据该阶段的环境操作符应用到纹理颜色上。
随着两个或更多纹理的出现,如何将它们组合成单个片段输出成为关键问题。EXT_texture_env_combine 扩展后来被引入,为各种纹理环境阶段添加了新函数和操作符。这定义了一种流水线架构,代表了逐片段操作的基础配置。
寄存器组合器的出现
NVIDIA 对此并不满足——这没有充分暴露 TNT 硬件的能力。于是他们添加了 NV_texture_env_combine4,不仅增加了更多参数和操作符,还引入了一个关键特性:重排序。
在标准组合器模型中,特定阶段只能访问该阶段的纹理颜色;一旦处理过第一阶段,逐顶点颜色就不再可用,只能使用前一阶段的颜色。NVIDIA 扩展暴露了访问任何阶段纹理值的能力,以及在任何阶段获取逐顶点颜色的能力。
虽然可能只有 2 个阶段,但这标志着图形卡真正可编程性的开始。每个阶段实际上是汇编语言中的一个操作码,程序只有 2 个操作码。它有一个由逐顶点颜色和两个纹理颜色组成的寄存器文件,以及一个存储第一个操作码输出的临时寄存器。
GeForce 256(NVIDIA 创造了"GPU"——几何处理单元——这个术语)是第一款提供硬件顶点变换和光照的消费级硬件。这种 T&L 是非常固定功能的,本质上完全(或几乎完全)暴露了 OpenGL 的固定功能 T&L 管线。但真正的可编程能力体现在片段处理上。
简单的环境组合器被取代,取而代之的是在两个硬件世代中基本保持不变的硬件:寄存器组合器(Register Combiners)。
NV_register_combiners 扩展建立了一个新范式。这些寄存器组合器是 NV_texture_env_combine4 的增强版,明确地像汇编语言一样运作:寄存器文件是显式构造,临时寄存器等也是如此。
- 系统硬编码了 8 个寄存器组合器阶段的限制,但有可查询的枚举值
- NV10 系列 GPU(GeForce 1 和 2)仅提供 2 个阶段
- 每个寄存器组合器阶段可执行 4 个独立操作:2 个向量操作(乘法或点积)和 2 个对颜色 alpha 或蓝色分量的标量操作
- 输出必须组合成 2 个 RGBA 颜色,存储到两个可读寄存器供下一阶段使用
- 还有一个功能更有限的最终组合器阶段,主要用于雾混合等固定功能操作
真正的可编程性
GeForce 3(第一款 NV20 芯片)包含了真正可编程性的首个实例。尽管 NVIDIA 是高度可配置片段处理的先驱,但其可编程性体现在顶点处理上。GeForce 3 是第一款为消费级硬件带来可编程性的 GPU。
通过 NV_vertex_program 在 OpenGL 中暴露的顶点处理器能够执行多达 128 个操作码的程序。它有一个相当大的寄存器文件(256 个 4 向量 uniform)和相当多的临时寄存器。虽然不能循环或纹理采样,但与之前微弱的固定功能相比非常强大。
尽管顶点处理器实现了巨大飞跃,片段阶段却停滞不前。NVIDIA 按照寄存器组合器规范承诺的那样,将组合器数量增加到 8 个并添加了两个额外的纹理访问。他们的纹理管线可配置性不高。
NVIDIA 没有为片段处理带来真正的编程能力,而是选择了称为"纹理着色器"的东西。通过 NV_texture_shader 扩展暴露,纹理着色器是将纹理单元用作计算单元的一种方式。每个纹理着色器将结果馈送到下一个,因此可以有最多 4 条指令的有效程序(NV20 硬件只有 4 个纹理单元)。
限制
- 4 单元限制比 register_combiner 的 8 操作限制小得多
- 操作码的灵活性从来不是特别好
- 每个操作都消耗一个纹理:每个用于纹理坐标数学运算的操作都会移除访问纹理的能力
当时片段处理的圣杯是纹理采样的通用性:能够从顶点管线获取任意输入、访问纹理、对其执行任意计算、将这些值作为纹理坐标馈送、并使用结果访问更多纹理。
令人惊讶的是,ATI 是第一个真正做对这一点的厂商。Radeon 8500 附带了多个 OpenGL 扩展,其中包括 ATI_fragment_shader。这些片段着色器可以:
- 执行最多 6 次纹理访问
- 对这些访问执行最多 8 次数学运算
- 使用这些运算结果执行最多 6 次额外的纹理访问
- 然后是另外 8 次数学运算
虽然不能完美通用(只能获得一次依赖纹理访问),但比 NVIDIA 的硬件好得多,而且足够通用,可以称为真正可编程。
整理混乱
此时,OpenGL 的可编程管线是一团糟——因为它根本没有一个统一的管线。核心 OpenGL 采纳了 ARB_multitexture 和 EXT_texture_env_combine,但真正的可编程工作是在不断增加的厂商特定扩展套件中完成的,每个集合都彼此不兼容。
当 NVIDIA 主导图形行业时,这种状态可能还行。但随着 ATI 真正竞争性地进入市场,这已不可接受。
第一次修复尝试:EXT_texture_crossbar 扩展试图将纹理环境通用化,有效地将片段处理暴露为类似寄存器组合器的形式。然而,规范本身太差,NVIDIA 无法实现,没有他们的支持,它就夭折了。而且,新一代 NVIDIA 和 ATI 片段硬件(纹理着色器和 ATI 片段着色器)可以进行纹理坐标计算,crossbar 扩展无法提供访问。整个纹理环境模型对将来不再可行。
第二次尝试:ARB_vertex_program 和 ARB_fragment_program。这些是可编程管线阶段的汇编级语言,是 NV_vertex_program 和 ATI_fragment_shader 的略微通用化组合。它们是一对合理的扩展,完成了工作。语言的设计允许根据需要用新功能扩展。
Direct3D 的类似情况
Direct3D 也远未免疫于此问题。Shader Model 1.0 的"像素"阶段更准确地说是"NVIDIA 的寄存器组合器和纹理着色器"。它不是通用的,当然也不是跨平台的。Shader Model 1.1 的"像素"阶段同样可以说是"ATI 的片段处理器"。
因此,没有 SM2 能力的 NVIDIA 硬件无法支持 SM1.1,因为它根本做不到。D3D 有单一、统一的方式指定着色器,但着色器的片段部分必须是平台特定的。由于硬件差异,基本算法必须根据使用 SM1.0 还是 SM1.1 来改变。随着 SM2.0,微软下了决心,就像 ARB_*_program 扩展一样,强制 NVIDIA 和 ATI 接受相同的标准。
OpenGL 着色语言
随着 Radeon 9700 和 GeForce FX,图形开发者获得了比以往更强大的能力。片段着色器可以更长、更复杂:ATI 提供最多 4 次依赖纹理获取,而 NVIDIA 以完全通用的片段着色器领先。
此时,OpenGL ARB 成员之一开始了重新设计 OpenGL API 的努力,使其更具前瞻性。3D Labs(当时是专业图形硬件供应商)开始了他们称为 OpenGL 2.0 的努力。他们的计划包括一个简称为 OpenGL 着色语言的类 C 风格着色语言。
历史注记
这个语言是 3D Labs 重塑 OpenGL 尝试中唯一幸存下来的东西。
该语言经过打磨改进,最终以 GLSL 1.0 发布。四个扩展控制其初始使用:
ARB_shader_objects:定义创建着色器和程序的方式及其关系ARB_vertex_shader:定义如何使用对象覆盖固定功能顶点管线ARB_fragment_shader:定义如何使用对象覆盖固定功能片段管线ARB_shading_language_100:定义实际使用的语言
最终,OpenGL 确实达到了 2.0 版本,但没有 API 重写。GL 2.0 正式将 GLSL 纳入核心,这是唯一被采纳的着色语言。
其他发展
在整个 GLSL 时代,NVIDIA 一直不懈地为他们的硬件保持 ARB_vertex_program 和 ARB_fragment_program 的更新。他们发布了一系列扩展来扩展语言,使其达到现代功能水平。
NVIDIA 还推广了一种称为 Cg 的语言。与 GLSL 不同,该语言是独立运行时的一部分。它编译成其他着色语言;甚至可以编译成 GLSL。NVIDIA 最大的卖点是 Cg 几乎与 Direct3D 的 HLSL 着色语言完全相同。