Cesium渲染模块之Shader
1. 引言
Cesium是一款三维地球和地图可视化开源JavaScript库,使用WebGL来进行硬件加速图形,使用时不需要任何插件支持,基于Apache2.0许可的开源程序,可以免费用于商业和非商业用途
Cesium官网:Cesium: The Platform for 3D Geospatial
Cesium GitHub站点:CesiumGS/cesium: An open-source JavaScript library for world-class 3D globes and maps (github.com)
API文档:Index - Cesium Documentation
通过阅读源码,理清代码逻辑,有助于扩展与开发,笔者主要参考了以下两个系列的文章
渲染是前端可视化的核心,本文描述Cesium渲染模块的Shader
2. WebGL中的Shader
以下大致是一个最简的WebGL绘制代码:
<canvas id="canvas"></canvas> <script> const vertexSource = ` attribute vec3 aPos; void main() { gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); } ` const fragmentSource = ` void main() { gl_FragColor = vec4(1.0, 0.5, 0.2, 1.0); } ` const canvas = document.getElementById('canvas'); canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight; const gl = canvas.getContext('webgl2'); if (!gl) { alert('WebGL not supported'); } const vertices = new Float32Array([ -0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.0, 0.5, 0.0, ]); const vbo = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vbo); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); const vao = gl.createVertexArray(); gl.bindVertexArray(vao); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(0) const vertexShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertexShader, vertexSource); gl.compileShader(vertexShader); const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragmentShader, fragmentSource); gl.compileShader(fragmentShader); const shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); gl.clearColor(0.2, 0.3, 0.3, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(shaderProgram); gl.drawArrays(gl.TRIANGLES, 0, 3); </script>
其中,着色器(Shader)是运行在GPU上的小程序,这些小程序为图形渲染管线的某个特定部分而运行,从基本意义上来说,着色器只是一种把输入转化为输出的程序。着色器可以是一个顶点着色器(vertex shader)或片元着色器(fragment shader),每个ShaderProgram都需要这两种类型的着色器。上述代码中创建着色器和着色器程序的代码:
const vertexShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertexShader, vertexSource); gl.compileShader(vertexShader); const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragmentShader, fragmentSource); gl.compileShader(fragmentShader); const shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram);
创建着色器的步骤大致为:
- 使用
WebGLRenderingContext.createShader()
初始化着色器 - 通过
WebGLRenderingContext.shaderSource()
挂接 GLSL 源代码 - 最后调用
WebGLRenderingContext.compileShader()
完成着色器(shader)的编译
此时 WebGLShader 仍不是可用的形式,它需要被添加到一个 WebGLProgram
里
创建着色器程序的步骤大致为:
- 使用
WebGLRenderingContext.createProgram()
初始化着色器程序 - 通过
WebGLRenderingContext.attachShader()
然后附着顶点着色器和片段着色器 - 最后调用
WebGLRenderingContext.linkProgram()
完成着色器(shader)的连接
使用着色器程序(上述代码中):
// Use the program gl.useProgram(shaderProgram); // Draw a single triangle gl.drawArrays(gl.TRIANGLES, 0, 3);
3. Cesium中的Shader
Cesium渲染模块中的Shader对象包含从创建GLSL到创建Shader Program整个流程
流程大致为:
- Cesium中支持分段编写GLSL代码,包括ShaderStruct、ShaderFunction、ShaderDestination
- 将分段的代码组合成GLSL,即ShaderSource
- ShaderBuilder使用ShaderSource创建的ShaderProgram会缓存起来,即ShaderCache
- 需要新的ShaderProgram时,先查询缓存中是否有,有就复用,无则创建
在Cesium源码中创建ShaderProgram大多也是这个流程,例如PolylineCollection.js
:
const fs = new ShaderSource({ defines: defines, sources: ["in vec4 v_pickColor;\n", this.material.shaderSource, PolylineFS], }); const vsSource = batchTable.getVertexShaderCallback()(PolylineVS); const vs = new ShaderSource({ defines: defines, sources: [PolylineCommon, vsSource], }); this.shaderProgram = ShaderProgram.fromCache({ context: context, vertexShaderSource: vs, fragmentShaderSource: fs, attributeLocations: attributeLocations, });
ShaderProgram.fromCache
只是简单的指向ShaderCache.getShaderProgram
ShaderProgram.fromCache = function (options) { // ... return options.context.shaderCache.getShaderProgram(options); };
ShaderCache.getShaderProgram
逻辑就是先查询缓存中是否有Shader,有就复用,无则创建:
ShaderCache.prototype.getShaderProgram = function (options) { // ... let cachedShader; if (defined(this._shaders[keyword])) { cachedShader = this._shaders[keyword]; } else { const shaderProgram = new ShaderProgram(); cachedShader = { cache: this, shaderProgram: shaderProgram, keyword: keyword, derivedKeywords: [], count: 0, }; } return cachedShader.shaderProgram; };
注意,此时的ShaderProgram只是个空壳,它并没有真正的创建WebGLProgram对象,但是它具备了创建WebGLProgram对象所需要的条件
可以参考源码中PointCloud.js
:
function createShaders(){ // ... drawCommand.shaderProgram = ShaderProgram.fromCache({ context: context, vertexShaderSource: vs, fragmentShaderSource: fs, attributeLocations: attributeLocations, }); drawCommand.shaderProgram._bind(); }
所以shaderProgram._bind()
才创建了WebGLProgram对象
ShaderProgram.prototype._bind = function () { initialize(this); this._gl.useProgram(this._program); }; function initialize(shader) { // ... reinitialize(shader); } function reinitialize(shader) { // ... const program = createAndLinkProgram(gl, shader, shader._debugShaders); }
通过多处调用,最后createAndLinkProgram(gl, shader)
实现了创建:
function createAndLinkProgram(gl, shader) { const vsSource = shader._vertexShaderText; const fsSource = shader._fragmentShaderText; const vertexShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertexShader, vsSource); gl.compileShader(vertexShader); const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragmentShader, fsSource); gl.compileShader(fragmentShader); const program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); const attributeLocations = shader._attributeLocations; if (defined(attributeLocations)) { for (const attribute in attributeLocations) { if (attributeLocations.hasOwnProperty(attribute)) { gl.bindAttribLocation( program, attributeLocations[attribute], attribute ); } } } gl.linkProgram(program); gl.deleteShader(vertexShader); gl.deleteShader(fragmentShader); // ... return program; }
值得一说的是,真正的WebGLProgram对象是直到需要绘制时才创建,不需要绘制的就不会创建,这样有效节省了资源:
function beginDraw(context, framebuffer, passState, shaderProgram, renderState) { // ... bindFramebuffer(context, framebuffer); applyRenderState(context, renderState, passState, false); shaderProgram._bind(); }
4. 参考资料
[1]WebGLProgram - Web API 接口参考 | MDN (mozilla.org)
[2]WebGLShader - Web API 接口参考 | MDN (mozilla.org)
[3]Cesium原理篇:6 Render模块(3: Shader) - fu*k - 博客园 (cnblogs.com)
[4]Cesium渲染模块之概述 - 当时明月在曾照彩云归 - 博客园 (cnblogs.com)
[5]Cesium DrawCommand 1 不谈地球 画个三角形 - 岭南灯火 - 博客园 (cnblogs.com)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律