PixiJS 源码解读:绘制矩形的渲染过程讲解

PixiJS 源码解读:绘制矩形的渲染过程讲解大家好 我是前端西瓜哥 之前写了一篇 PixiJS 绘制矩形 简单说了一下 PixiJS 是怎么绘制矩形的 PixiJS 源码解读 绘制矩形 底层都做了什么 它更多的讲解上层的东西 没花太多笔墨描绘底层渲染的流程 所以我写了这篇文章 对

欢迎大家来到IT世界,在知识的湖畔探索吧!

大家好,我是前端西瓜哥。

之前写了一篇 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); 

欢迎大家来到IT世界,在知识的湖畔探索吧!

绘制结果为:

PixiJS 源码解读:绘制矩形的渲染过程讲解

欢迎大家来到IT世界,在知识的湖畔探索吧!

创建 gl

第一步是创建 gl 对象,上下文类型优先使用 “webgl2″。

如果不支持,会降级为 “webgl”、”experimental-webgl”。

欢迎大家来到IT世界,在知识的湖畔探索吧!gl = canvas.getContext("webgl2", options); 

gl 在 renderer 渲染器初始化的时候构建的,可通过 app.renderer.gl 拿到。

构建着色器代码片段

定义 顶点着色器片元着色器

着色器(Shader)是一种类 C 语言 GLSL,用于描述需要绘制的 顶点信息和颜色信息

着色器模板

首先是 字符串模板,等着根据配置填充成一个完整的着色器代码片段。

顶点着色器的模板(后面会基于它生成真正可用的着色器)位于 packages/core/src/batch/texture.vert 中。

PixiJS 源码解读:绘制矩形的渲染过程讲解

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; } 

片元着色器和颜色有关。

欢迎大家来到IT世界,在知识的湖畔探索吧!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)和顶点的位置、大小有关。

PixiJS 源码解读:绘制矩形的渲染过程讲解

补充一些简单注释说明。

顶点着色器

precision highp float; // 浮点数使用高精度 #define SHADER_NAME pixi-shader-2 precision highp float; attribute vec2 aVertexPosition; // 顶点位置 x 和 y attribute 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)用于描述顶点围成区域的像素颜色。

下面是片元着色器的最终代码,同样我会加一些注释说明

欢迎大家来到IT世界,在知识的湖畔探索吧!precision mediump float; #define SHADER_NAME pixi-shader-2 varying 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 下。

PixiJS 源码解读:绘制矩形的渲染过程讲解

编译着色器程序

第一次调用 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; } 

分成三个主要步骤。

(1)创建着色器对象、程序对象。

compileShader 实现:

欢迎大家来到IT世界,在知识的湖畔探索吧!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; } 

(2)绑定 attribute 类型的变量 (但此时还没传入 Buffer 数据,只是设置了如何访问等操作);

(3)绑定 uniform 类型的变量。

之后在 app.renderer.shader.bind 方法内执行下面代码,应用刚刚创建的程序对象

this.gl.useProgram(glProgram.program); 

渲染阶段

前面做的是准备工作,编译着色器。

接下来就是渲染阶段

PIXI.Ticker 定时器会在渲染下一帧前调用 renderer.render 方法,进入 WebGL 的渲染流程。

清空画布填充背景色

首先是清空画布。

欢迎大家来到IT世界,在知识的湖畔探索吧!// 入口方法:renderer.renderTexture.clear class 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.0929, 0.0929, 0, 1)     gl.clearColor(r, g, b, a);     // 清空颜色和深度缓存     gl.clear(mask);   } } 

递归调用 render

递归图形树(app.stage),调用它们(继承了 IRenderableObject 接口类型)的 render 方法,它们会拿到 renderer 对象,然后执行自己的渲染逻辑。

欢迎大家来到IT世界,在知识的湖畔探索吧!// 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。

PixiJS 源码解读:绘制矩形的渲染过程讲解

然后进行三角化。三角化就是将图形转换为对应的三角形的组合。

所谓图形的渲染,其实就是绘制一个个小的三角形,组成特定的形状。这些三角形的点,根据不同图形(比如矩形和圆形),需要用不同算法去计算出来,然后把数据通过 WebGL 命令交给 GPU,让它帮我们绘制出来。

首先是填充的三角化(对应 buildRectangle.triangulate() )。

基于前面的 4 个点得到填充块的 4 个点,并设置对应的索引值 indices,之后调用 gl.drawElements() 需要用到。

PixiJS 源码解读:绘制矩形的渲染过程讲解

接着是描边的三角化(对应 buildLine())。

下面是绘制描边的代码片段:

PixiJS 源码解读:绘制矩形的渲染过程讲解

PixiJS 的计算逻辑很复杂,这是因为涉及到连接方式、末端样式的情况。

同样,也要计算它的顶点、索引、纹理坐标。

西瓜哥我将最终的填充和描边产生的点,做了一下可视化。

PixiJS 源码解读:绘制矩形的渲染过程讲解

用的是 desmos 可视化工具,这里给一下这个可视化链接:

https://www.desmos.com/calculator/r3dwqeweu2?lang=zh-CN

最后计算好的三角化数据会保存到 graph 对象的 batches 数组下(batches 表示要批量处理的意思)。

batch 对象包括顶点坐标(vertexData)、颜色(_batchRGB)、索引(indices)和纹理坐标(uvs)。

下面是填充色对应的数据:

PixiJS 源码解读:绘制矩形的渲染过程讲解

批量渲染

这里产生了两个 batch 对象(对应填充和描边),然后遍历传给 BatchRender 类的 render 方法。说是 render 方法,其实并不立即 render,而是将 batch 对象的数据解读和保存起来,之后 flush 时才正式将数据加到 WebGL 里。

PixiJS 源码解读:绘制矩形的渲染过程讲解

这些属性会组合拼装在一个类型数组里。6 个一组,逐顶点绘制。

PixiJS 源码解读:绘制矩形的渲染过程讲解

传完后,会调用 BatchRender 类的 flush 方法,将顶点数据和索引数组通过 gl.bufferData() 进行绑定。

PixiJS 源码解读:绘制矩形的渲染过程讲解

image-

绑定 uniform 值

在 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 的方法:

PixiJS 源码解读:绘制矩形的渲染过程讲解

绑定纹理

绑定纹理。

欢迎大家来到IT世界,在知识的湖畔探索吧!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);   } } 

最后我们就绘制出一个有填充和描边的矩形了。

PixiJS 源码解读:绘制矩形的渲染过程讲解

之后 Ticker 会不断地在绘制下一帧时调用 renderer 的 render 方法进行渲染,如果图形没改变(比如通过 dirtyId 和 cacheDirty 是否相同判断),我们会跳过三角化的环节,使用缓存好的数据去绘制渲染。

结尾

PixiJS 绘制图形使用了 WebGL,为了利用 GPU 的并行能力,需要给着色器一次性提供尽可能多的顶点和颜色信息。

PixiJS 提供了一些基础图形,比如矩形。绘制时会根据图形属性信息进行三角化,最后将所有的信息组合起来,一次性提供给 WebGL。

这篇文章其实断断续续写了好久,PixiJS 里的弯弯道道挺多的,经常调试了半天就是找不着北了,一度搁置。最后还是硬着头皮不断地调试和思考,总算把这篇文章结束掉了。

我是前端西瓜哥,欢迎关注我,学习更多前端图形知识。


免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/85066.html

(0)

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

联系我们YX

mu99908888

在线咨询: 微信交谈

邮件:itzsgw@126.com

工作时间:时刻准备着!

关注微信