第 14 章:OpenGL ES 与 WebGL
第 14 章:OpenGL ES 与 WebGL
当 3D 渲染需要运行在手机、平板或浏览器中时,就进入了 OpenGL ES 和 WebGL 的世界。本章讲解它们与桌面 OpenGL 的关键差异以及实战适配策略。
14.1 OpenGL ES 概述
14.1.1 版本演进
OpenGL ES(Embedded Systems)是 OpenGL 的嵌入式子集,由 Khronos 维护:
| 版本 | 年份 | GLSL ES | 关键特性 |
|---|---|---|---|
| 1.0 | 2003 | — | 固定管线(类 OpenGL 1.5) |
| 2.0 | 2007 | 1.00 | 可编程管线(类 OpenGL 2.0) |
| 3.0 | 2012 | 3.00 | MRT、纹理压缩、遮挡查询 |
| 3.1 | 2014 | 3.10 | 计算着色器、间接绘制 |
| 3.2 | 2015 | 3.20 | 几何/细分着色器、模板索引 |
14.1.2 平台分布
Android → OpenGL ES 2.0(最低)/ 3.0(主流)/ 3.2(旗舰)
iOS → OpenGL ES 2.0(旧设备)/ 3.0+(推荐,但 Apple 推荐 Metal)
嵌入式 → OpenGL ES 2.0 / 3.0
Web → WebGL 1.0(≈ ES 2.0)/ WebGL 2.0(≈ ES 3.0)
14.2 OpenGL ES 与桌面版的关键差异
14.2.1 API 差异对照
| 特性 | 桌面 OpenGL 4.6 | OpenGL ES 3.2 | OpenGL ES 2.0 |
|---|---|---|---|
| GLSL 版本 | 4.60 | 3.20 ES | 1.00 ES |
| 几何着色器 | ✅ | ✅ | ❌ |
| 计算着色器 | ✅ | ✅ | ❌ |
| 细分着色器 | ✅ | ✅ | ❌ |
| VAO | 必须 | 必须 | 可选扩展 |
| 纹理压缩 | BPTC/S3TC/ASTC | ETC2/ASTC | ETC1 |
| 帧缓冲 | FBO | FBO | FBO |
| 浮点纹理 | ✅ | ✅(精度受限) | 扩展 |
| instancing | ✅ | ✅ | 扩展 |
| 3D 纹理 | ✅ | ✅ | ❌ |
| 双精度 | ✅ | ❌ | ❌ |
| glPolygonMode | ✅ | ❌ | ❌ |
| 默认帧缓冲 | 系统管理 | 系统管理 | 系统管理 |
14.2.2 GLSL ES 与 GLSL 差异
// GLSL ES 3.00 着色器
#version 300 es
precision mediump float; // 必须指定默认精度
in vec3 aPos;
in vec2 aTexCoord;
out vec2 vTexCoord;
uniform mat4 mvp;
void main() {
gl_Position = mvp * vec4(aPos, 1.0);
vTexCoord = aTexCoord;
}
// 片段着色器(必须声明精度)
#version 300 es
precision mediump float; // mediump = 中精度, highp = 高精度, lowp = 低精度
in vec2 vTexCoord;
out vec4 FragColor;
uniform sampler2D tex;
void main() {
FragColor = texture(tex, vTexCoord);
}
| 精度 | float 范围 | 整数范围 | 典型用途 |
|---|---|---|---|
lowp | ±2, 精度 1/256 | ±256 | 颜色值 |
mediump | ±16384, 精度 1/1024 | ±2^15 | 纹理坐标、法线 |
highp | 完整 float | ±2^31 | 位置计算、深度 |
⚠️ 精度问题:移动端 GPU 的 mediump 精度远低于桌面 float。如果着色器结果在移动端出现色带或抖动,切换到 highp。
14.2.3 纹理压缩格式
| 格式 | 平台 | 每像素 | 说明 |
|---|---|---|---|
| ETC1 | Android (通用) | 0.5 字节 | 无 Alpha |
| ETC2 | OpenGL ES 3.0+ | 0.5-1 字节 | 带 Alpha |
| ASTC | OpenGL ES 3.2+ | 0.8-3.5 字节 | 可变块大小,最高质量 |
| PVRTC | iOS (PowerVR) | 0.5-1 字节 | Apple 设备 |
| S3TC/DXT | 桌面 + WebGL | 0.5-1 字节 | 桌面标准 |
14.3 WebGL 概述
14.3.1 版本对照
| 版本 | OpenGL ES 对应 | 浏览器支持 | 关键特性 |
|---|---|---|---|
| WebGL 1.0 | ES 2.0 | 所有现代浏览器 | 基础 3D |
| WebGL 2.0 | ES 3.0 | Chrome, Firefox, Edge | MRT、3D 纹理 |
14.3.2 WebGL 代码示例
// WebGL 2.0 初始化
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl2');
if (!gl) {
alert('WebGL 2.0 not supported');
throw new Error('WebGL 2 not supported');
}
// 着色器源码
const vertexShaderSource = `#version 300 es
precision mediump float;
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aTexCoord;
out vec2 vTexCoord;
uniform mat4 mvp;
void main() {
gl_Position = mvp * vec4(aPos, 1.0);
vTexCoord = aTexCoord;
}`;
const fragmentShaderSource = `#version 300 es
precision mediump float;
in vec2 vTexCoord;
out vec4 FragColor;
uniform sampler2D tex;
void main() {
FragColor = texture(tex, vTexCoord);
}`;
// 编译着色器
function compileShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('Shader error:', gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
const vs = compileShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fs = compileShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Link error:', gl.getProgramInfoLog(program));
}
// 创建 VAO
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
// 顶点数据
const vertices = new Float32Array([
// 位置 // UV
-0.5, -0.5, 0.0, 0.0, 0.0,
0.5, -0.5, 0.0, 1.0, 0.0,
0.5, 0.5, 0.0, 1.0, 1.0,
-0.5, 0.5, 0.0, 0.0, 1.0,
]);
const vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 20, 0);
gl.enableVertexAttribArray(1);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 20, 12);
// 索引数据
const indices = new Uint16Array([0, 1, 2, 0, 2, 3]);
const ebo = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
// 加载纹理
function loadTexture(url) {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
new Uint8Array([128, 128, 128, 255])); // 占位
const image = new Image();
image.onload = function() {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.generateMipmap(gl.TEXTURE_2D);
};
image.src = url;
return texture;
}
// 渲染循环
function render() {
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0.1, 0.1, 0.2, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.enable(gl.DEPTH_TEST);
gl.useProgram(program);
gl.bindVertexArray(vao);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
requestAnimationFrame(render);
}
render();
14.3.3 WebGL 与 OpenGL ES 差异
| 特性 | OpenGL ES | WebGL |
|---|---|---|
| 上下文获取 | 窗口系统 API | canvas.getContext() |
| 纹理上传 | glTexImage2D | gl.texImage2D(image/video/canvas) |
| 异步纹理 | 手动同步 | image.onload 回调 |
| 深度范围 | [-1, 1] (NDC) | [0, 1](WebGL 2) |
| Y 轴方向 | 向上 | 向下(需要翻转) |
| 调试 | RenderDoc 等 | 浏览器 DevTools |
| 安全限制 | 无 | 跨域纹理限制 |
14.4 跨平台适配策略
14.4.1 条件编译
// 桌面版
#ifdef GL_ES
precision mediump float;
#else
#version 460 core
#endif
// 功能检测
#if defined(GL_ES_VERSION_3_0) || __VERSION__ >= 300
// ES 3.0+ / GL 3.3+ 功能
#else
// ES 2.0 回退
#endif
14.4.2 抽象层设计
// 渲染后端抽象
class RenderBackend {
public:
virtual void init() = 0;
virtual void createBuffer(BufferType type, size_t size, void* data) = 0;
virtual void drawIndexed(int count) = 0;
virtual void setShader(const char* vertSrc, const char* fragSrc) = 0;
};
class OpenGLBackend : public RenderBackend { /* ... */ };
class OpenGLESBackend : public RenderBackend { /* ... */ };
class WebGLBackend : public RenderBackend { /* JavaScript bindings */ };
14.4.3 性能适配策略
| 策略 | 桌面 | 移动端 | Web |
|---|---|---|---|
| 纹理分辨率 | 4K | 1K-2K | 1K |
| Mipmap | 三线性 | 双线性 | 双线性 |
| 阴影 | PCF 5×5 | 硬阴影 | 无/简单 |
| 后处理 | 多级 | 最少 | 无 |
| 绘制调用 | 无限制 | <100/帧 | <100/帧 |
| 三角形数 | 无限制 | <100K/帧 | <50K/帧 |
14.5 Android OpenGL ES 开发
14.5.1 GLSurfaceView 配置
// Android OpenGL ES 基本配置
public class MyGLSurfaceView extends GLSurfaceView {
public MyGLSurfaceView(Context context) {
super(context);
setEGLContextClientVersion(3); // OpenGL ES 3.0
setRenderer(new MyRenderer());
setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
}
}
public class MyRenderer implements GLSurfaceView.Renderer {
@Override
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
GLES30.glClearColor(0.1f, 0.1f, 0.2f, 1.0f);
// 初始化资源
}
@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES30.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 unused) {
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT | GLES30.GL_DEPTH_BUFFER_BIT);
// 渲染
}
}
14.5.2 Android 纹理加载
// 使用 BitmapFactory 加载纹理
public static int loadTexture(Context context, int resourceId) {
int[] textureIds = new int[1];
GLES30.glGenTextures(1, textureIds, 0);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = false; // 不缩放
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resourceId, options);
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureIds[0]);
GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, bitmap, 0);
GLES30.glGenerateMipmap(GLES30.GL_TEXTURE_2D);
bitmap.recycle();
return textureIds[0];
}
14.6 注意事项
⚠️ 精度问题:mediump 的 float 精度在不同设备上可能不同。关键计算(如深度值)使用 highp。
⚠️ 纹理尺寸:OpenGL ES 要求纹理尺寸为 2 的幂次(ES 2.0 严格要求,ES 3.0 放宽)。
⚠️ WebGL 跨域限制:从其他域加载纹理需要设置 CORS 头,否则
texImage2D会失败并污染画布。
⚠️ 移动端功耗:持续渲染会快速消耗电池。使用
RENDERMODE_WHEN_DIRTY并在不需要时停止渲染。
⚠️ Apple 的方向:Apple 已在 iOS 上弃用 OpenGL ES,推荐使用 Metal。iOS 开发应考虑 Metal 或使用跨平台引擎(Unity/Unreal)。
14.7 业务场景
场景 1:Web 3D 产品展示
Three.js + WebGL 2.0 实现在线产品 360° 浏览器预览。
场景 2:移动游戏
OpenGL ES 3.0 实现移动端实时渲染,使用 ETC2 压缩纹理减少内存占用。
场景 3:地图引擎
WebGL 实现矢量地图渲染,大量 POI 使用实例化绘制。
14.8 扩展阅读
| 资源 | 说明 |
|---|---|
| OpenGL ES 3.2 规范 | 官方规范 |
| WebGL 2.0 规范 | 官方规范 |
| Three.js | 最流行的 WebGL 框架 |
| WebGL Fundamentals | WebGL 入门教程 |
| Android OpenGL ES | Android 官方文档 |
本章小结
- OpenGL ES 是桌面 OpenGL 的嵌入式子集,ES 3.0+ 对应桌面版的大部分功能
- GLSL ES 必须声明精度(lowp/mediump/highp),精度在不同设备上可能不同
- WebGL 1.0 ≈ ES 2.0,WebGL 2.0 ≈ ES 3.0,通过 canvas.getContext 获取上下文
- 跨平台适配需要关注纹理压缩格式、着色器精度和渲染负载
- 移动端优化重点:减少绘制调用、使用压缩纹理、控制分辨率
上一章:第 13 章:OpenCL 进阶 下一章:第 15 章:Vulkan 入门