强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 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.02003固定管线(类 OpenGL 1.5)
2.020071.00可编程管线(类 OpenGL 2.0)
3.020123.00MRT、纹理压缩、遮挡查询
3.120143.10计算着色器、间接绘制
3.220153.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.6OpenGL ES 3.2OpenGL ES 2.0
GLSL 版本4.603.20 ES1.00 ES
几何着色器
计算着色器
细分着色器
VAO必须必须可选扩展
纹理压缩BPTC/S3TC/ASTCETC2/ASTCETC1
帧缓冲FBOFBOFBO
浮点纹理✅(精度受限)扩展
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 纹理压缩格式

格式平台每像素说明
ETC1Android (通用)0.5 字节无 Alpha
ETC2OpenGL ES 3.0+0.5-1 字节带 Alpha
ASTCOpenGL ES 3.2+0.8-3.5 字节可变块大小,最高质量
PVRTCiOS (PowerVR)0.5-1 字节Apple 设备
S3TC/DXT桌面 + WebGL0.5-1 字节桌面标准

14.3 WebGL 概述

14.3.1 版本对照

版本OpenGL ES 对应浏览器支持关键特性
WebGL 1.0ES 2.0所有现代浏览器基础 3D
WebGL 2.0ES 3.0Chrome, Firefox, EdgeMRT、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 ESWebGL
上下文获取窗口系统 APIcanvas.getContext()
纹理上传glTexImage2Dgl.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
纹理分辨率4K1K-2K1K
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 FundamentalsWebGL 入门教程
Android OpenGL ESAndroid 官方文档

本章小结

  • 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 入门