PixiJS源码分析系列:第三章 使用 canvas 作为渲染器
使用 canvasRenderer 渲染
上一章分析了一下 Sprite 在默认 webgl 渲染器上的渲染,这章让我们把目光聚集到 canvasRenderer 上
使用 canvas 渲染器渲染图片的 demo
要使用 canvas 作为渲染器,我们需要引用 pixi-legacy.js
/bundles/pixi.js-legacy/dist/pixi-legacy.js
像下面这样先建一个简单的 demo 用于测试:
<script src="/bundles/pixi.js-legacy/dist/pixi-legacy.js"></script>
<script type="text/javascript">
const app = new PIXI.Application({ width: 800, height: 600 , forceCanvas: true});
document.body.appendChild(app.view);
const rectangle = PIXI.Sprite.from('logo.png');
rectangle.x = 100;
rectangle.y = 100;
rectangle.anchor.set(0.5);
rectangle.rotation = Math.PI / 4;
app.stage.addChild(rectangle);
</script>
同样创建一个简单的加载显示 logo 的 demo
运行它应该可以看到在第一章 simple.html
中一模一样的一张 logo 图被渲染在了网页上
在 Application.ts 的 constructor 函数内,即 78 行添加 console.log(this.renderer);
输出当前的渲染器看看
constructor(options?: Partial<IApplicationOptions>)
{
// The default options
options = Object.assign({
forceCanvas: false,
}, options);
this.renderer = autoDetectRenderer<VIEW>(options);
console.log(this.renderer);
// install plugins here
Application._plugins.forEach((plugin) =>
{
plugin.init.call(this, options);
});
}
图 3-1
图 3-1 中可以发现输出了一个 _CanvasRenderer2 的而不是 CanvasRenderer 实例,是因为其实在 demo 中加载的 pixi.js 是经过 rollup 编译后的。
demo https://github.com/willian12345/blogpost/tree/main/analysis/PixiJS/pixijs-dev/examples/sprite-canvas.html
Sprite 类
Sprite 是 webgl 渲染器和 canvas 渲染器共用的
注意 Sprite.ts 类本身并不做渲染,保存了 Sprite 的基本信息
在此处最终渲染到 canvas 上用的是 CanvasSpriteRenderer
渲染类
我们在直接使用 html 的 canvas 绘制图像时,是直接调用 context.drawImage
方法,并传递一个“图像源”
但在 pixi.js 内,这个图像源并不是直接的图像或 canvas,而是先封装成了一个 texture 即纹理对象,统一管理
找到 /packages/CanvasSpriteRenderer.ts 的第 37 - 40 行
static extension: ExtensionMetadata = {
name: 'sprite',
type: ExtensionType.CanvasRendererPlugin,
};
可以看到 CanvasSpriteRenderer 是一个渲染器的插件,当需要渲染一个 sprite 的时候调用的是此插件
最终被调用的 sprite 渲染方法, 即绘制图片或路径等到 canvas 上
render(sprite: Sprite): void
{
const texture = sprite._texture;
const renderer = this.renderer;
const context = renderer.canvasContext.activeContext;
const activeResolution = renderer.canvasContext.activeResolution;
if (!texture.valid)
{
return;
}
const sourceWidth = texture._frame.width;
const sourceHeight = texture._frame.height;
let destWidth = texture._frame.width;
let destHeight = texture._frame.height;
if (texture.trim)
{
destWidth = texture.trim.width;
destHeight = texture.trim.height;
}
let wt = sprite.transform.worldTransform;
let dx = 0;
let dy = 0;
const source = texture.baseTexture.getDrawableSource();
if (texture.orig.width <= 0 || texture.orig.height <= 0 || !texture.valid || !source)
{
return;
}
renderer.canvasContext.setBlendMode(sprite.blendMode, true);
context.globalAlpha = sprite.worldAlpha;
// If smoothingEnabled is supported and we need to change the smoothing property for sprite texture
const smoothingEnabled = texture.baseTexture.scaleMode === SCALE_MODES.LINEAR;
const smoothProperty = renderer.canvasContext.smoothProperty;
if (smoothProperty
&& context[smoothProperty] !== smoothingEnabled)
{
context[smoothProperty] = smoothingEnabled;
}
if (texture.trim)
{
dx = (texture.trim.width / 2) + texture.trim.x - (sprite.anchor.x * texture.orig.width);
dy = (texture.trim.height / 2) + texture.trim.y - (sprite.anchor.y * texture.orig.height);
}
else
{
dx = (0.5 - sprite.anchor.x) * texture.orig.width;
dy = (0.5 - sprite.anchor.y) * texture.orig.height;
}
if (texture.rotate)
{
wt.copyTo(canvasRenderWorldTransform);
wt = canvasRenderWorldTransform;
groupD8.matrixAppendRotationInv(wt, texture.rotate, dx, dy);
// the anchor has already been applied above, so lets set it to zero
dx = 0;
dy = 0;
}
dx -= destWidth / 2;
dy -= destHeight / 2;
renderer.canvasContext.setContextTransform(wt, sprite.roundPixels, 1);
// Allow for pixel rounding
if (sprite.roundPixels)
{
dx = dx | 0;
dy = dy | 0;
}
const resolution = texture.baseTexture.resolution;
const outerBlend = renderer.canvasContext._outerBlend;
if (outerBlend)
{
context.save();
context.beginPath();
context.rect(
dx * activeResolution,
dy * activeResolution,
destWidth * activeResolution,
destHeight * activeResolution
);
context.clip();
}
if (sprite.tint !== 0xFFFFFF)
{
if (sprite._cachedTint !== sprite.tintValue || sprite._tintedCanvas.tintId !== sprite._texture._updateID)
{
sprite._cachedTint = sprite.tintValue;
// TODO clean up caching - how to clean up the caches?
sprite._tintedCanvas = canvasUtils.getTintedCanvas(sprite, sprite.tintValue);
}
context.drawImage(
sprite._tintedCanvas,
0,
0,
Math.floor(sourceWidth * resolution),
Math.floor(sourceHeight * resolution),
Math.floor(dx * activeResolution),
Math.floor(dy * activeResolution),
Math.floor(destWidth * activeResolution),
Math.floor(destHeight * activeResolution)
);
}
else
{
context.drawImage(
source,
texture._frame.x * resolution,
texture._frame.y * resolution,
Math.floor(sourceWidth * resolution),
Math.floor(sourceHeight * resolution),
Math.floor(dx * activeResolution),
Math.floor(dy * activeResolution),
Math.floor(destWidth * activeResolution),
Math.floor(destHeight * activeResolution)
);
}
if (outerBlend)
{
context.restore();
}
// just in case, leaking outer blend here will be catastrophic!
renderer.canvasContext.setBlendMode(BLEND_MODES.NORMAL);
}
我想 sprite render 方法估计是在使用 pixi 时用的最多的方法
在此 render 方法内 打一个 debugger 后:
图 3-2
看一下方法的调用栈 从 图 3-2 中的红色向上箭头可以看到 _tick 函数一级一级往 render 方法内调用
render 函数做了什么
render 方法大致做了以下几步:
接受一个 sprite 对象实例,获取到这个 sprite 的当前 "激活的canvas2d上下文" activeContext
当前激活的上下文不是固定的“根上下文” rootContext 而是可变的,因为可以并允许创建多个 canvas 的情况存在比如 “离屏渲染,用新的canvas缓存图片” 等
在
/packages/canvas-render/CanvasContextSystem.ts
文件的第 79 行init()
初始化方法内可以看到this.activeContext = this.rootContext;
默认就是“根上下文”接下来是确定当前 canvas context 的渲染模式
renderer.canvasContext.setBlendMode(sprite.blendMode, true);
即根据传递进来的 sprite 的 blendMode 确定当前 canvas context 的渲染模式, blendMode 是一个枚举值
blendMode 对应的是 可以查看
/packages/canvas-render/src/utils/mapCanvasBlendModesToPixi.ts
中的生成并存储的 CanvasRenderingContext2D.globalCompositeOperation 值具体值所对应的效果可查看 https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
除了做特效外,碰到过最多blendMode 的应用场景是在一些 html5 做那种刮奖效果
生成当前 context 上下文的变幻矩阵(transform)
根据传递进来的 sprite 的 texture 确定绘制“图形”的尺寸,旋转信息,转换成当前上下文的变幻矩阵(transform)
render 方法内的 'wt' 变量(word transform) , 就是这一句
renderer.canvasContext.setContextTransform(wt, sprite.roundPixels, 1);
根据 outerBlend 确定是否需要上下文进行 clip 裁剪
其实就是是否要用就遮罩效果 相关信息可查看 https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Compositing
调用上下文的 canvas 原生方法 drawImage 开始真正的绘制工作, 这里的 tint 值比较有意思,后面再详细介绍它
绘制前先判断之前是否有缓存过图形,如果之前绘制过就直接绘制缓存的图形以提高性能
此处作者还注释了一句,
// TODO clean up caching - how to clean up the caches?
, 充分说明了写程序肯定不是一蹴而就的 _!
用 tint 给显示对象(DisplayObject)上色
tint 属性用于改变显示对象的颜色。
它通过混合原始颜色与指定的颜色来给显示对象
这里有几个关键点来理解 tint 属性的工作方式:
颜色混合:tint接受一个十六进制颜色值,这个值用来与对象原有的颜色进行混合。混合操作不是简单地替换颜色,而是基于色彩理论进行的,因此可以得到不同的视觉效果。
透明度影响:tint操作同时影响颜色和alpha(透明度)。这意味着,即使不直接改变对象的透明度,颜色的变化也可能影响其视觉上的透明程度。
全白或全透明不受影响:如果一个像素是完全白色(#FFFFFF)或完全透明,那么tint不会改变它。这是因为全白像素可以吸收任何颜色的混合,而全透明像素则不显示颜色变化。
多边形和纹理:对于包含纹理的显示对象(如Sprite),tint会影响整个纹理的颜色。而对于矢量图形(如通过Graphics绘制的形状),颜色混合会直接应用于线条或填充颜色。
性能考虑:与直接更换纹理或颜色相比使用 tint 在很多情况下更高效,因为它避免了重新加载或创建新的纹理资源。
在这里判断是否需要处理 tint 缓存比较有意思,如果你将 render 内的 sprite.tint 用 console.log 输出会得到值 16777215
而用 console.log(0xFFFFFF)
输出的也是 16777215
, 都会转换成十进制
以我们这个 sprite-canvas.html
为例,它是不缓存的,所以会直接走最下面的直接绘制逻辑
如果把我们的 sprite-canvas.html 代码修改一下,加一句 rectangle.tint = 'red';
如下:
<script type="text/javascript">
const app = new PIXI.Application({ width: 800, height: 600 , forceCanvas: true});
document.body.appendChild(app.view);
const rectangle = PIXI.Sprite.from('logo.png');
rectangle.x = 100;
rectangle.y = 100;
rectangle.anchor.set(0.5);
rectangle.rotation = Math.PI / 4;
rectangle.tint = 'red';
app.stage.addChild(rectangle);
</script>
你可以看到,整个 PixiJS 的 logo 变成了绝色
图 3-3
更准确的说应该说是混合成了红色
在 render 函数内的 sprite._tintedCanvas = canvasUtils.getTintedCanvas(sprite, sprite.tintValue);
在 /packages/canvas-render/src/canvasUtils.ts
源文件 50 行找到 的 getTintedCanvas
方法
此方法内会调用 canvasUtils.tintMethod(texture, color, canvas);
canvasUtils.tintMethod = canvasUtils.canUseMultiply ? canvasUtils.tintWithMultiply : canvasUtils.tintWithPerPixel;
最后根据能不能使用 multiply 确定使用哪种 tint 方法,优先使用 tintWithMultiply 方法
/packages/canvas-render/src/canvasUtils.ts
源文件 110 - 159 行:
tintWithMultiply: (texture: Texture, color: number, canvas: ICanvas): void =>
{
const context = canvas.getContext('2d');
const crop = texture._frame.clone();
const resolution = texture.baseTexture.resolution;
crop.x *= resolution;
crop.y *= resolution;
crop.width *= resolution;
crop.height *= resolution;
canvas.width = Math.ceil(crop.width);
canvas.height = Math.ceil(crop.height);
context.save();
context.fillStyle = Color.shared.setValue(color).toHex();
context.fillRect(0, 0, crop.width, crop.height);
context.globalCompositeOperation = 'multiply';
const source = texture.baseTexture.getDrawableSource();
context.drawImage(
source,
crop.x,
crop.y,
crop.width,
crop.height,
0,
0,
crop.width,
crop.height
);
context.globalCompositeOperation = 'destination-atop';
context.drawImage(
source,
crop.x,
crop.y,
crop.width,
crop.height,
0,
0,
crop.width,
crop.height
);
context.restore();
},
在 tintWithMultiply 这个方法通过设置 context 上下文的 fillStyle 结合 globalCompositeOperation 绘制一个矩形框叠加到图像源(source)上 实现变色,当然这里使用的肯定是独立于 rootContext 的 canvas
context.fillStyle = Color.shared.setValue(color).toHex();
context.fillRect(0, 0, crop.width, crop.height);
context.globalCompositeOperation = 'multiply';
如果不支持 Multiply 则调用性能消耗更高的 tintWithPerPixel 方法
tintWithPerPixel: (texture: Texture, color: number, canvas: ICanvas): void =>
{
const context = canvas.getContext('2d');
const crop = texture._frame.clone();
const resolution = texture.baseTexture.resolution;
crop.x *= resolution;
crop.y *= resolution;
crop.width *= resolution;
crop.height *= resolution;
canvas.width = Math.ceil(crop.width);
canvas.height = Math.ceil(crop.height);
context.save();
context.globalCompositeOperation = 'copy';
context.drawImage(
texture.baseTexture.getDrawableSource(),
crop.x,
crop.y,
crop.width,
crop.height,
0,
0,
crop.width,
crop.height
);
context.restore();
const [r, g, b] = Color.shared.setValue(color).toArray();
const pixelData = context.getImageData(0, 0, crop.width, crop.height);
const pixels = pixelData.data;
for (let i = 0; i < pixels.length; i += 4)
{
pixels[i + 0] *= r;
pixels[i + 1] *= g;
pixels[i + 2] *= b;
}
context.putImageData(pixelData, 0, 0);
},
注意 tintWithPerPixel 这个方法内是先绘制源图像,再利用 getImageData 和 putImageData 像素级操作实现的变色效果,所以传统比较消耗性能
在 render 方法的最后一句 renderer.canvasContext.setBlendMode(BLEND_MODES.NORMAL);
将上下文的渲染模式恢复为普通值,以免影响全局的渲染
至上 canvas-sprite 渲染流程算是走完了
本章小节
果然 canvas 的渲染比起 webgl 的渲染容易理解一些,虽然都是顺序执行的命令行,但是 webgl 的渲染模式需要绘制到 GPU 之前需要收集的命令比 canvas 渲染要多出许多步骤
下一章让我们聚焦到最重要的事件交互上,PixiJS 是如何在 canvas 上实现交互事件的,如何处理最典型的鼠标点击事件并响应点击
还有,如果你到现在还是没能在你本地把调式项目跑起来,那么首先参考这个系列文章的第一章,然后直接下载我这个 https://github.com/willian12345/blogpost/tree/main/analysis/PixiJS/pixijs-dev 调式项目
注:转载请注明出处博客园:王二狗Sheldon池中物 (willian12345@126.com)
PixiJS源码分析系列:第三章 使用 canvas 作为渲染器的更多相关文章
- jQuery-1.9.1源码分析系列(三) Sizzle选择器引擎——词法解析
jQuery源码9600多行,而Sizzle引擎就独占近2000行,占了1/5.Sizzle引擎.jQuery事件机制.ajax是整个jQuery的核心,也是jQuery技术精华的体现.里面的有些策略 ...
- jQuery-1.9.1源码分析系列(三) Sizzle选择器引擎——编译原理
这一节要分析的东东比较复杂,篇幅会比较大,也不知道我描述后能不能让人看明白.这部分的源码我第一次看的时候也比较吃力,现在重头看一遍,再分析一遍,看能否查缺补漏. 看这一部分的源码需要有一个完整的概念后 ...
- jQuery-1.9.1源码分析系列(三) Sizzle选择器引擎——总结与性能分析
Sizzle引擎的主体部分已经分析完毕了,今天为这部分划一个句号. a. Sizzle解析流程总结 是时候该做一个总结了.Sizzle解析的流程已经一目了然了. 1.选择器进入Sizzle( sele ...
- spring5源码分析系列(三)——IOC容器的初始化(一)
前言: IOC容器的初始化包括BeanDefinition的Resource定位.载入.注册三个基本过程. 本文以ApplicationContext为例讲解,XmlWebApplicationCon ...
- jQuery-1.9.1源码分析系列(三) Sizzle选择器引擎——一些有用的Sizzle API
说一下Sizzle中零碎的API.这些API有的被jQuery接管,直接使用jQuery.xxx就可以使用,有的没有被接管,如果要在jQuery中使用,使用方法是jQuery.find.xxx. 具体 ...
- jQuery源码分析系列(三)Sizzle选择器引擎-下
选择函数:select() 看到select()函数,if(match.length === 1){}存在的意义是尽量简化执行步骤,避免compile()函数的调用. 简化操作同样根据tokenize ...
- Thinkphp源码分析系列(三)– App应用程序类
// +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO ...
- jQuery源码分析系列
声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://git ...
- jQuery-1.9.1源码分析系列完毕目录整理
jQuery 1.9.1源码分析已经完毕.目录如下 jQuery-1.9.1源码分析系列(一)整体架构 jQuery-1.9.1源码分析系列(一)整体架构续 jQuery-1.9.1源码分析系列(二) ...
- [转]jQuery源码分析系列
文章转自:jQuery源码分析系列-Aaron 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://github.com/JsAaro ...
随机推荐
- CSS3 超实用属性:pointer-events (可穿透图层的鼠标事件)
1.是什么 pointer-events 直译为指针事件,该属性指定在什么情况下某个DOM可以成为鼠标事件的 target. 简而言之,就是允许/禁止DOM的鼠标事件(click事件.hover事件. ...
- Java异常中throw 与throws的区别
throw 与 throws区别 在Java中,throws和throw是两个不同的关键字,它们在异常处理中起着不同的作用. throws关键字: throws用于声明一个方法可能会抛出的异常.当一个 ...
- 解决:Maven PKIX path building failed: sun.security.provider.certpath
在构建SpringBoot项目时,maven下载依赖会报 PKIX path building failed: sun.security.provider.certpath的错误. 使用https:/ ...
- abc356
D1.5h没做出,E0.5h做出来啦? E 有两个做法,第一个是枚举分子来计算分母对答案的贡献,另一种是枚举分母来求分子对答案的贡献. 枚举分子来计算分母对答案的贡献需要用到数论分块,所以我们讲枚举分 ...
- 如何排查常规软件问题 - 面向 Linux 初级用户的教程
笔者从 14 年做开源软件以来,接触了众多 Linux 新手用户,这里我为这类用户总结了一些常见的问题排查方法,希望能帮助到大家.如果你已经工作多年,对于下面提到的思路和方法应该非常熟悉,如果对某一条 ...
- XTuner大模型单卡低成本微调实战
Smiling & Weeping ---- 有趣是片难寻的土,灵魂是朵难养的花 一些关于的模型训练的小tips: 1.ctrl+c中断 2.tmux new -s 名称 3.ctrl+ ...
- PowerBI_一分钟了解POWERBI计算组功能及作用(第一部分)
1: X-mind&计算组(CALCULATION GROUP)介绍 1.1 什么是计算组 PowerBI的计算组功能可以显著减少必须创建的冗余度量值数,通过允许将DAX表达式定义为应用于模型 ...
- FreeRTOS简单内核实现6 优先级
0.思考与回答 0.1.思考一 如何实现 RTOS 内核支持多优先级? 因为不支持优先级,所以所有的任务都插入了一个名为 pxReadyTasksLists 的就绪链表中,相当于所有任务的优先级都是一 ...
- 未能加载文件或程序集“netstandard,Version=2.0.0.0, Culture=neutral,PublicKeyToken=cc7b13ffcd2ddd51”或它的某一个依赖项 解决
未能加载文件或程序集"netstandard,Version=2.0.0.0, Culture=neutral,PublicKeyToken=cc7b13ffcd2ddd51"或它 ...
- python 二次封装logging,打印日志文件名正确,且正确写入/结合pytest执行,日志不输出的问题
基于之前日志问题,二次封装日志后,导致日志输出的文件名不对,取到的文件一直都是当前二次封装的log的文件名,基于这个问题,做了优化,详细看 https://www.cnblogs.com/cuitan ...