PixiJS 源碼解讀:繪制矩形的渲染過程講解
大家好,我是前端西瓜哥。
之前寫了一篇 PixiJS 繪制矩形,簡單說了一下 PixiJS 是怎么繪制矩形的。
《PixiJS 源碼解讀:繪制矩形,底層都做了什么?》
它更多的講解上層的東西,沒花太多筆墨描繪底層渲染的流程。所以我寫了這篇文章,對渲染流程進行補充講解。
PixiJS 版本為 7.2.4。
要求讀者熟悉 WebGL 的基礎(chǔ)知識。
本文會 以繪制設(shè)置了填充和描邊的矩形為例子,看底層 WebGL 的調(diào)用執(zhí)行。
業(yè)務(wù)層代碼:
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);
繪制結(jié)果為:
創(chuàng)建 gl
第一步是創(chuàng)建 gl 對象,上下文類型優(yōu)先使用 "webgl2"。
如果不支持,會降級為 "webgl"、"experimental-webgl"。
gl = canvas.getContext("webgl2", options);
gl 在 renderer 渲染器初始化的時候構(gòu)建的,可通過 app.renderer.gl 拿到。
構(gòu)建著色器代碼片段
定義 頂點著色器 和 片元著色器。
著色器(Shader)是一種類 C 語言 GLSL,用于描述需要繪制的 頂點信息和顏色信息。
著色器模板
首先是 字符串模板,等著根據(jù)配置填充成一個完整的著色器代碼片段。
頂點著色器的模板(后面會基于它生成真正可用的著色器)位于 packages/core/src/batch/texture.vert 中。
batch 文件夾都是和 批量繪制 有關(guān)的邏輯,批量、減少 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;
}
片元著色器和顏色有關(guān)。
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 初始化時,上面的模板會進行一系列的改造,兩個著色器最終轉(zhuǎn)換為下面的樣子。
頂點著色器(Vertex Shader)和頂點的位置、大小有關(guān)。
補充一些簡單注釋說明。
頂點著色器
precision highp float; // 浮點數(shù)使用高精度
#define SHADER_NAME pixi-shader-2
precision highp float;
attribute vec2 aVertexPosition; // 頂點位置 x 和 y
attribute vec2 aTextureCoord; // 紋理坐標(biāo),會傳給片元著色器
attribute vec4 aColor; // 顏色,rgba,會傳給片元著色器
attribute float aTextureId; // 紋理單元 ID,會傳給片元著色器
uniform mat3 projectionMatrix; // 投影矩陣
uniform mat3 translationMatrix; // 平移變換矩陣
uniform vec4 tint; // 改變顏色,實現(xiàn)濾鏡效果,會和 aColor 相乘傳給片元著色器
varying vec2 vTextureCoord; // varing 都是用來傳遞的
varying vec4 vColor;
varying float vTextureId;
void main(void){
// 進行一系列矩陣乘法運算,將最后的點傳給內(nèi)置的著色器變量,設(shè)置點的位置
gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0);
// 下面都是要傳給片元著色器的變量
vTextureCoord = aTextureCoord;
vTextureId = aTextureId;
vColor = aColor * tint;
}
片元著色器
片元著色器(Fragment Shader)用于描述頂點圍成區(qū)域的像素顏色。
下面是片元著色器的最終代碼,同樣我會加一些注釋說明
precision mediump float;
#define SHADER_NAME pixi-shader-2
varying vec2 vTextureCoord; // 紋理坐標(biāo),
varying vec4 vColor; // 顏色
varying float vTextureId; // 使用哪一個紋理采樣器
uniform sampler2D uSamplers[16]; // 16 個紋理采樣器
void main(void){
vec4 color;
if(vTextureId < 0.5) {
// 從紋理采樣器(比如圖片轉(zhuǎn)換過來的像素點集合)中,提取特定位置的像素點
color = texture2D(uSamplers[0], vTextureCoord);
}else if(vTextureId < 1.5) {
color = texture2D(uSamplers[1], vTextureCoord);
}
// ...
} else {
color = texture2D(uSamplers[15], vTextureCoord);
}
// 疊加顏色值,和紋理采樣器取得的顏色值,賦值給片元著色器內(nèi)置變量
gl_FragColor = color * vColor;
}
如果沒有設(shè)置紋理,PixiJS 會給一個默認(rèn)的兜底用紋理對象,一個 16x16 的白色方形。
這兩個著色器片段會保存到 Shader 實例中,放到 app.render.shader 下。
編譯著色器程序
第一次調(diào)用 renderer 渲染器 render 方法時,PixiJS 會 創(chuàng)建頂點著色器對象和片元著色器對象。
這些邏輯是在 generateProgram 方法中實現(xiàn)的。該方法的核心代碼:
function generateProgram(gl, program) {
//(1)創(chuàng)建頂點著色器對象、片元著色器對象等
const glVertShader = compileShader(gl, gl.VERTEX_SHADER, program.vertexSrc);
const glFragShader = compileShader(
gl,
gl.FRAGMENT_SHADER,
program.fragmentSrc
);
// 創(chuàng)建程序?qū)ο? 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]);
}
// 刪除著色器對象,釋放內(nèi)存
gl.deleteShader(glVertShader);
gl.deleteShader(glFragShader);
//(3)綁定 uniformLocation(準(zhǔn)確來說是拿地址,還沒正式綁定)
// 屬性(對應(yīng) 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;
}
分成三個主要步驟。
創(chuàng)建著色器對象、程序?qū)ο蟆?/p>
compileShader 實現(xiàn):
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 數(shù)據(jù),只是設(shè)置了如何訪問等操作);
綁定 uniform 類型的變量。
之后在 app.renderer.shader.bind 方法內(nèi)執(zhí)行下面代碼,應(yīng)用剛剛創(chuàng)建的程序?qū)ο蟆?/p>
this.gl.useProgram(glProgram.program);
渲染階段
前面做的是準(zhǔn)備工作,編譯著色器。
接下來就是渲染階段。
PIXI.Ticker 定時器會在渲染下一幀前調(diào)用 renderer.render 方法,進入 WebGL 的渲染流程。
清空畫布填充背景色
首先是清空畫布。
// 入口方法:renderer.renderTexture.clear
class ObjectRendererSystem {
render(displayObject, options) {
// ...
// (1) 清空畫布,并指定顏色
renderer.renderTexture.clear();
// ...
}
}
它會執(zhí)行 clear 方法
class FramebufferSystem {
clear(r, g, b, a, mask = BUFFER_BITS.COLOR | BUFFER_BITS.DEPTH) {
const { gl } = this;
// 背景色 #cc0 轉(zhuǎn)換為 rbga 格式:
// (0.800000011920929, 0.800000011920929, 0, 1)
gl.clearColor(r, g, b, a);
// 清空顏色和深度緩存
gl.clear(mask);
}
}
遞歸調(diào)用 render
遞歸圖形樹(app.stage),調(diào)用它們(繼承了 IRenderableObject 接口類型)的 render 方法,它們會拿到 renderer 對象,然后執(zhí)行自己的渲染邏輯。
// 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);
}
}
}
對于前文的示例代碼,會分析矩形屬性,構(gòu)建頂點和片元數(shù)據(jù),然后執(zhí)行 WebGL 的繪制 API。
對矩形三角化,構(gòu)建頂點和片元數(shù)據(jù)
先基于 x、y、width、height 計算出矩形的 4 個頂點放到 points。
然后進行三角化。三角化就是將圖形轉(zhuǎn)換為對應(yīng)的三角形的組合。
所謂圖形的渲染,其實就是繪制一個個小的三角形,組成特定的形狀。這些三角形的點,根據(jù)不同圖形(比如矩形和圓形),需要用不同算法去計算出來,然后把數(shù)據(jù)通過 WebGL 命令交給 GPU,讓它幫我們繪制出來。
首先是填充的三角化(對應(yīng) buildRectangle.triangulate() )。
基于前面的 4 個點得到填充塊的 4 個點,并設(shè)置對應(yīng)的索引值 indices,之后調(diào)用 gl.drawElements() 需要用到。
接著是描邊的三角化(對應(yīng) buildLine())。
下面是繪制描邊的代碼片段:
PixiJS 的計算邏輯很復(fù)雜,這是因為涉及到連接方式、末端樣式的情況。
同樣,也要計算它的頂點、索引、紋理坐標(biāo)。
西瓜哥我將最終的填充和描邊產(chǎn)生的點,做了一下可視化。
用的是 desmos 可視化工具,這里給一下這個可視化鏈接:
https://www.desmos.com/calculator/r3dwqeweu2?lang=zh-CN。
最后計算好的三角化數(shù)據(jù)會保存到 graph 對象的 batches 數(shù)組下(batches 表示要批量處理的意思)。
batch 對象包括頂點坐標(biāo)(vertexData)、顏色(_batchRGB)、索引(indices)和紋理坐標(biāo)(uvs)。
下面是填充色對應(yīng)的數(shù)據(jù):
批量渲染
這里產(chǎn)生了兩個 batch 對象(對應(yīng)填充和描邊),然后遍歷傳給 BatchRender 類的 render 方法。說是 render 方法,其實并不立即 render,而是將 batch 對象的數(shù)據(jù)解讀和保存起來,之后 flush 時才正式將數(shù)據(jù)加到 WebGL 里。
這些屬性會組合拼裝在一個類型數(shù)組里。6 個一組,逐頂點繪制。
傳完后,會調(diào)用 BatchRender 類的 flush 方法,將頂點數(shù)據(jù)和索引數(shù)組通過 gl.bufferData() 進行綁定。
綁定 uniform 值
在 ShaderSystem 類的 syncUniforms 中,會依次設(shè)置好各個 uniform 變量:tint、translationMatrix、uSamplers、projectionMatrix。
class ShaderSystem {
syncUniforms(group, glProgram, syncData) {
// 生成同步 uniform 的函數(shù)(不同 uniform 的函數(shù)不同)
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];
}
}
下面是設(shè)置 tint 的方法:
綁定紋理
綁定紋理。
class TextureSystem {
bind(texture, location = 0) {
const { gl } = this;
// 開啟
gl.activeTexture(gl.TEXTURE0 + location);
// ...
gl.bindTexture(texture.target, glTexture.texture);
// ...
}
}
因為示例并不繪制圖片,PixiJS 會提供默認(rèn)的的白色紋理對象(所有值都是 1),這樣顏色值和其相乘,結(jié)果還是原來的顏色值。
渲染
最后調(diào)用 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 會不斷地在繪制下一幀時調(diào)用 renderer 的 render 方法進行渲染,如果圖形沒改變(比如通過 dirtyId 和 cacheDirty 是否相同判斷),我們會跳過三角化的環(huán)節(jié),使用緩存好的數(shù)據(jù)去繪制渲染。
結(jié)尾
PixiJS 繪制圖形使用了 WebGL,為了利用 GPU 的并行能力,需要給著色器一次性提供盡可能多的頂點和顏色信息。
PixiJS 提供了一些基礎(chǔ)圖形,比如矩形。繪制時會根據(jù)圖形屬性信息進行三角化,最后將所有的信息組合起來,一次性提供給 WebGL。
這篇文章其實斷斷續(xù)續(xù)寫了好久,PixiJS 里的彎彎道道挺多的,經(jīng)常調(diào)試了半天就是找不著北了,一度擱置。最后還是硬著頭皮不斷地調(diào)試和思考,總算把這篇文章結(jié)束掉了。