大家好,我是前端西瓜哥。
之前写了一篇 PixiJS 绘制矩形,简单说了一下 PixiJS 是怎么绘制矩形的。
《PixiJS 源码解读:绘制矩形,底层都做了什么?》
它更多的讲解上层的东西,没花太多笔墨描绘底层渲染的流程。所以我写了这篇文章,对渲染流程进行补充讲解。
PixiJS 版本为 7.2.4。
要求读者熟悉 WebGL 的基础知识。
本文会 以绘制设置了填充和描边的矩形为例子,看底层 WebGL 的调用执行。
业务层代码:
const app = new PIXI.Application({ width: 500, height: 300, background: "#cc0", //(土黄色)});document.body.appendChild(app.view);const graph = new PIXI.Graphics();graph.beginFill(0xff0044); // 红色填充色graph.lineStyle({ color: "blue", width: 4 }); // 蓝色描边graph.drawRect(90, 70, 300, 100);app.stage.addChild(graph);
绘制结果为:
第一步是创建 gl 对象,上下文类型优先使用 "webgl2"。
如果不支持,会降级为 "webgl"、"experimental-webgl"。
gl = canvas.getContext("webgl2", options);
gl 在 renderer 渲染器初始化的时候构建的,可通过 app.renderer.gl 拿到。
定义 顶点着色器 和 片元着色器。
着色器(Shader)是一种类 C 语言 GLSL,用于描述需要绘制的 顶点信息和颜色信息。
首先是 字符串模板,等着根据配置填充成一个完整的着色器代码片段。
顶点着色器的模板(后面会基于它生成真正可用的着色器)位于 packages/core/src/batch/texture.vert 中。
batch 文件夹都是和 批量绘制 有关的逻辑,批量、减少 draw call 正是 PixiJS 高效绘制的秘诀。
precision highp float;attribute vec2 aVertexPosition;attribute vec2 aTextureCoord;attribute vec4 aColor;attribute float aTextureId;uniform mat3 projectionMatrix;uniform mat3 translationMatrix;uniform vec4 tint;varying vec2 vTextureCoord;varying vec4 vColor;varying float vTextureId;void main(void){ gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); vTextureCoord = aTextureCoord; vTextureId = aTextureId; vColor = aColor * tint;}
片元着色器和颜色有关。
varying vec2 vTextureCoord;varying vec4 vColor;varying float vTextureId;uniform sampler2D uSamplers[%count%];void main(void){ vec4 color; %forloop% gl_FragColor = color * vColor;}
这里的 %count% 和%forloop% 是占位符,会在之后进行替换。
在 renderer 初始化时,上面的模板会进行一系列的改造,两个着色器最终转换为下面的样子。
顶点着色器(Vertex Shader)和顶点的位置、大小有关。
补充一些简单注释说明。
precision highp float; // 浮点数使用高精度#define SHADER_NAME pixi-shader-2precision highp float;attribute vec2 aVertexPosition; // 顶点位置 x 和 yattribute vec2 aTextureCoord; // 纹理坐标,会传给片元着色器attribute vec4 aColor; // 颜色,rgba,会传给片元着色器attribute float aTextureId; // 纹理单元 ID,会传给片元着色器uniform mat3 projectionMatrix; // 投影矩阵uniform mat3 translationMatrix; // 平移变换矩阵uniform vec4 tint; // 改变颜色,实现滤镜效果,会和 aColor 相乘传给片元着色器varying vec2 vTextureCoord; // varing 都是用来传递的varying vec4 vColor;varying float vTextureId;void main(void){ // 进行一系列矩阵乘法运算,将最后的点传给内置的着色器变量,设置点的位置 gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); // 下面都是要传给片元着色器的变量 vTextureCoord = aTextureCoord; vTextureId = aTextureId; vColor = aColor * tint;}
片元着色器(Fragment Shader)用于描述顶点围成区域的像素颜色。
下面是片元着色器的最终代码,同样我会加一些注释说明
precision mediump float;#define SHADER_NAME pixi-shader-2varying vec2 vTextureCoord; // 纹理坐标,varying vec4 vColor; // 颜色varying float vTextureId; // 使用哪一个纹理采样器uniform sampler2D uSamplers[16]; // 16 个纹理采样器void main(void){ vec4 color; if(vTextureId < 0.5) { // 从纹理采样器(比如图片转换过来的像素点集合)中,提取特定位置的像素点 color = texture2D(uSamplers[0], vTextureCoord); }else if(vTextureId < 1.5) { color = texture2D(uSamplers[1], vTextureCoord); } // ... } else { color = texture2D(uSamplers[15], vTextureCoord); } // 叠加颜色值,和纹理采样器取得的颜色值,赋值给片元着色器内置变量 gl_FragColor = color * vColor;}
如果没有设置纹理,PixiJS 会给一个默认的兜底用纹理对象,一个 16x16 的白色方形。
这两个着色器片段会保存到 Shader 实例中,放到 app.render.shader 下。
第一次调用 renderer 渲染器 render 方法时,PixiJS 会 创建顶点着色器对象和片元着色器对象。
这些逻辑是在 generateProgram 方法中实现的。该方法的核心代码:
function generateProgram(gl, program) { //(1)创建顶点着色器对象、片元着色器对象等 const glVertShader = compileShader(gl, gl.VERTEX_SHADER, program.vertexSrc); const glFragShader = compileShader( gl, gl.FRAGMENT_SHADER, program.fragmentSrc ); // 创建程序对象 const webGLProgram = gl.createProgram(); //(2)绑定 attribute // keys 为 ['aColor', 'aTextureCoord', 'aTextureId', 'aVertexPosition'] for (let i = 0; i < keys.length; i++) { program.attributeData[keys[i]].location = i; // 将属性绑定到顶点着色器的制定位置 // 如:gl.bindAttribLocation(gl.program, 0, "aColor"); gl.bindAttribLocation(webGLProgram, i, keys[i]); } // 删除着色器对象,释放内存 gl.deleteShader(glVertShader); gl.deleteShader(glFragShader); //(3)绑定 uniformLocation(准确来说是拿地址,还没正式绑定) // 属性(对应 i 变量)有:projectionMatrix、tint、translationMatrix、uSamplers for (const i in program.uniformData) { const data = program.uniformData[i]; uniformData[i] = { location: gl.getUniformLocation(webGLProgram, i), value: defaultValue(data.type, data.size), }; } const glProgram = new GLProgram(webGLProgram, uniformData); return glProgram;}
分成三个主要步骤。
创建着色器对象、程序对象。
compileShader 实现:
function compileShader(gl, type, src) { const shader = gl.createShader(type); gl.shaderSource(shader, src); gl.compileShader(shader); gl.attachShader(webGLProgram, glVertShader); gl.attachShader(webGLProgram, glFragShader); // ... gl.linkProgram(webGLProgram); return shader;}
绑定 attribute 类型的变量 (但此时还没传入 Buffer 数据,只是设置了如何访问等操作);
绑定 uniform 类型的变量。
之后在 app.renderer.shader.bind 方法内执行下面代码,应用刚刚创建的程序对象。
this.gl.useProgram(glProgram.program);
前面做的是准备工作,编译着色器。
接下来就是渲染阶段。
PIXI.Ticker 定时器会在渲染下一帧前调用 renderer.render 方法,进入 WebGL 的渲染流程。
首先是清空画布。
// 入口方法:renderer.renderTexture.clearclass ObjectRendererSystem { render(displayObject, options) { // ... // (1) 清空画布,并指定颜色 renderer.renderTexture.clear(); // ... }}
它会执行 clear 方法
class FramebufferSystem { clear(r, g, b, a, mask = BUFFER_BITS.COLOR | BUFFER_BITS.DEPTH) { const { gl } = this; // 背景色 #cc0 转换为 rbga 格式: // (0.800000011920929, 0.800000011920929, 0, 1) gl.clearColor(r, g, b, a); // 清空颜色和深度缓存 gl.clear(mask); }}
递归图形树(app.stage),调用它们(继承了 IRenderableObject 接口类型)的 render 方法,它们会拿到 renderer 对象,然后执行自己的渲染逻辑。
// app.stage 是 Container 实例class Container extends DisplayObject { render(renderer) { // ... this._render(renderer); // 真正的渲染逻辑 for (let i = 0, j = this.children.length; i < j; ++i) { this.children[i].render(renderer); } }}
对于前文的示例代码,会分析矩形属性,构建顶点和片元数据,然后执行 WebGL 的绘制 API。
先基于 x、y、width、height 计算出矩形的 4 个顶点放到 points。
然后进行三角化。三角化就是将图形转换为对应的三角形的组合。
所谓图形的渲染,其实就是绘制一个个小的三角形,组成特定的形状。这些三角形的点,根据不同图形(比如矩形和圆形),需要用不同算法去计算出来,然后把数据通过 WebGL 命令交给 GPU,让它帮我们绘制出来。
首先是填充的三角化(对应 buildRectangle.triangulate() )。
基于前面的 4 个点得到填充块的 4 个点,并设置对应的索引值 indices,之后调用 gl.drawElements() 需要用到。
接着是描边的三角化(对应 buildLine())。
下面是绘制描边的代码片段:
PixiJS 的计算逻辑很复杂,这是因为涉及到连接方式、末端样式的情况。
同样,也要计算它的顶点、索引、纹理坐标。
西瓜哥我将最终的填充和描边产生的点,做了一下可视化。
用的是 desmos 可视化工具,这里给一下这个可视化链接:
https://www.desmos.com/calculator/r3dwqeweu2?lang=zh-CN。
最后计算好的三角化数据会保存到 graph 对象的 batches 数组下(batches 表示要批量处理的意思)。
batch 对象包括顶点坐标(vertexData)、颜色(_batchRGB)、索引(indices)和纹理坐标(uvs)。
下面是填充色对应的数据:
这里产生了两个 batch 对象(对应填充和描边),然后遍历传给 BatchRender 类的 render 方法。说是 render 方法,其实并不立即 render,而是将 batch 对象的数据解读和保存起来,之后 flush 时才正式将数据加到 WebGL 里。
这些属性会组合拼装在一个类型数组里。6 个一组,逐顶点绘制。
传完后,会调用 BatchRender 类的 flush 方法,将顶点数据和索引数组通过 gl.bufferData() 进行绑定。
在 ShaderSystem 类的 syncUniforms 中,会依次设置好各个 uniform 变量:tint、translationMatrix、uSamplers、projectionMatrix。
class ShaderSystem { syncUniforms(group, glProgram, syncData) { // 生成同步 uniform 的函数(不同 uniform 的函数不同) const syncFunc = group.syncUniforms[this.shader.program.id] || this.createSyncGroups(group); // 同步! syncFunc(glProgram.uniformData, group.uniforms, this.renderer, syncData); } createSyncGroups(group) { const id = this.getSignature(group, this.shader.program.uniformData, "u"); if (!this.cache[id]) { this.cache[id] = generateUniformsSync(group, this.shader.program.uniformData); } group.syncUniforms[this.shader.program.id] = this.cache[id]; return group.syncUniforms[this.shader.program.id]; } }
下面是设置 tint 的方法:
绑定纹理。
class TextureSystem { bind(texture, location = 0) { const { gl } = this; // 开启 gl.activeTexture(gl.TEXTURE0 + location); // ... gl.bindTexture(texture.target, glTexture.texture); // ... }}
因为示例并不绘制图片,PixiJS 会提供默认的的白色纹理对象(所有值都是 1),这样颜色值和其相乘,结果还是原来的颜色值。
最后调用 drawBatches 进行绘制。
drawBatches() { const dcCount = this._dcIndex; const { gl, state: stateSystem } = this.renderer; const drawCalls = _BatchRenderer._drawCallPool; let curTexArray = null; for (let i = 0; i < dcCount; i++) { const { texArray, type, size, start, blend } = drawCalls[i]; if (curTexArray !== texArray) { curTexArray = texArray; // 刚刚提到的纹理绑定逻辑 this.bindAndClearTexArray(texArray); } this.state.blendMode = blend; stateSystem.set(this.state); // 绘制 API gl.drawElements(type, size, gl.UNSIGNED_SHORT, start * 2); }}
最后我们就绘制出一个有填充和描边的矩形了。
之后 Ticker 会不断地在绘制下一帧时调用 renderer 的 render 方法进行渲染,如果图形没改变(比如通过 dirtyId 和 cacheDirty 是否相同判断),我们会跳过三角化的环节,使用缓存好的数据去绘制渲染。
PixiJS 绘制图形使用了 WebGL,为了利用 GPU 的并行能力,需要给着色器一次性提供尽可能多的顶点和颜色信息。
PixiJS 提供了一些基础图形,比如矩形。绘制时会根据图形属性信息进行三角化,最后将所有的信息组合起来,一次性提供给 WebGL。
这篇文章其实断断续续写了好久,PixiJS 里的弯弯道道挺多的,经常调试了半天就是找不着北了,一度搁置。最后还是硬着头皮不断地调试和思考,总算把这篇文章结束掉了。
本文链接:http://www.28at.com/showinfo-26-12747-0.htmlPixiJS 源码解读:绘制矩形的渲染过程讲解
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: Java 集合框架超详细!