PIXI v3源码解析 其二
之前我们做做热身看了看工具类,现在正式进入游戏引擎核心。
DisplayObject类需要一个我们之前没有看的类, RenderTexture类。
他在DiaplayObject类上是这么引入的,
RenderTexture = require('../textures/RenderTexture'),
这个类用来辅助创建渲染材质。在我们要渲染复杂内容的时候会用到。
下面先我们插播材质类的解析,按照他的引用一步步分析。
BaseTexture = require(‘./BaseTexture’),
材质类依赖的第一个类是基础材质类BaseTexture。
构造时接受三个参数, 分别是材质源, 拉伸模式, 分辨率。
构造时如果材质参数存在则开始构造材质。
他自身需要关注的属性如下几个:
- resolution分辨率, 默认为1
- 基础宽高
- 真实宽高(假设材质需要拉伸的话)
- scaleMode拉伸模式, 线性或插值
- hasLoaded是否已载入成功
- isLoading是否正在载入中
- premultipliedAlpha是否需要乘以透明度参数, 只webGL有效
需要注意到,
在该js的16行:EventEmitter.call(this);
,
163行, BaseTexture.prototype = Object.create(EventEmitter.prototype);
该类的继承自node的事件模块。从基础材质类开始, PIXI开始引入了事件机制, 往后的源码中将有很多的hook钩子函数。
我们学习下这个EventEmitter的源码,我们可以通过查看pixi.js浏览器版本来看node里定义的这个是个什么样的模块:
var prefix = typeof Object.create !== 'function' ? '~' : false; //是否需要前缀
function EE(fn, context, once) { //监听器,含有需要执行的方法,执行源,以及是否单次这三个属性
this.fn = fn;
this.context = context;
this.once = once || false;
}
function EventEmitter() { /* Nothing to set */ }
EventEmitter.prototype._events = undefined;
EventEmitter.prototype.listeners = function listeners(event, exists) {
var evt = prefix ? prefix + event : event //加前缀
, available = this._events && this._events[evt]; //获取指定名称的事件下绑定的回调函数集合
if (exists) return !!available; //只是想知道这个事件有没有注册
if (!available) return []; //如果可用事件为空
if (available.fn) return [available.fn]; //如果不是数组,是单个含fn属性的
for (var i = 0, l = available.length, ee = new Array(l); i < l; i++) {
ee[i] = available[i].fn; //将数组内容赋给ee
}
return ee;
};
EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { //事件触发器
var evt = prefix ? prefix + event : event;
if (!this._events || !this._events[evt]) return false; //获取存储的事件
var listeners = this._events[evt] //监听的函数列表
, len = arguments.length
, args
, i;
if ('function' === typeof listeners.fn) {
if (listeners.once) this.removeListener(event, listeners.fn, undefined, true);
switch (len) { //将listeners.context作为触发对象,执行回调
case 1: return listeners.fn.call(listeners.context), true;
case 2: return listeners.fn.call(listeners.context, a1), true;
case 3: return listeners.fn.call(listeners.context, a1, a2), true;
case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true;
case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true;
case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true;
}
for (i = 1, args = new Array(len -1); i < len; i++) { //多于6个参数时
args[i - 1] = arguments[i];
}
listeners.fn.apply(listeners.context, args);
} else {
var length = listeners.length
, j;
for (i = 0; i < length; i++) {
if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true);
switch (len) {
case 1: listeners[i].fn.call(listeners[i].context); break;
case 2: listeners[i].fn.call(listeners[i].context, a1); break;
case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break;
default:
if (!args) for (j = 1, args = new Array(len -1); j < len; j++) {
args[j - 1] = arguments[j];
}
listeners[i].fn.apply(listeners[i].context, args);
}
}
}
return true;
};
EventEmitter.prototype.on = function on(event, fn, context) { //事件注册器
var listener = new EE(fn, context || this) //新的监听器
, evt = prefix ? prefix + event : event;
if (!this._events) this._events = prefix ? {} : Object.create(null);
if (!this._events[evt]) this._events[evt] = listener;
else {
if (!this._events[evt].fn) this._events[evt].push(listener); //放进指定事件的监听列表
else this._events[evt] = [
this._events[evt], listener
];
}
return this;
};
EventEmitter.prototype.once = function once(event, fn, context) {
var listener = new EE(fn, context || this, true) //和on唯一不一样的地方就是他是单次的
, evt = prefix ? prefix + event : event;
if (!this._events) this._events = prefix ? {} : Object.create(null);
if (!this._events[evt]) this._events[evt] = listener;
else {
if (!this._events[evt].fn) this._events[evt].push(listener);
else this._events[evt] = [
this._events[evt], listener
];
}
return this;
};
看完了就可以理解pixi里面的事件机制了。
基本套路就是on或once对指定事件注册回调方法,通过emit来触发这些已存在的回调。本质上是一种发布订阅的观察者模式。
BaseTexture.prototype.update = function ()
{
....
this.emit('update', this); //update最后, 触发他的update事件
};
...
source.onload = function ()
{
'''
scope.emit('loaded', scope); //资源载入完毕时, 会触发loaded事件
};
下面我们节选局部的一个方法来窥探下pixi里对于一个类是如何统一管理的:
BaseTexture.prototype.destroy = function ()
{ //材质的销毁方法
if (this.imageUrl) //source是image转换来的
{
delete utils.BaseTextureCache[this.imageUrl]; //删除这个图像url在基础材质中的缓存
delete utils.TextureCache[this.imageUrl]; //删除这个图像url在材质中的缓存
this.imageUrl = null; //将这个材质对象url置空
if (!navigator.isCocoonJS)
{
this.source.src = ''; //image对象src置空
}
}
else if (this.source && this.source._pixiId) //source并非图像转换来
{
delete utils.BaseTextureCache[this.source._pixiId]; //靠_pixiId删除映射
}
this.source = null; //删除
this.dispose(); //触发删除该材质后的事件
};
从这个类的销毁方法我们可以看到:
一个健壮的类库对于其中的每个类都是精心管理着的, 比如这里,
一个材质对象新建时我们会把他的信息用全局数据结构tils.BaseTextureCache
缓存起来, 方便管理还能确保不重复创建。
同理, 销毁材质时需要把相关的内容都删除干净,去除引用,方便js的gc对其回收。最后还可能有销毁完毕后的处置方法,让某些用到这个材质的对象都会做出相应的响应操作。
我们在阅读这部分源码的时候会心生一个疑惑,就是基础材质的第一个参数source究竟是一个什么样的对象呢?
在基础材质里对于这个对象我们操作了很多次,不仅和他的src属性打交道,还有getContext方法。这个source究竟是image还是canvas对象啊?
这时候我们可以翻看官方API文档或看源码方法注释:
@param source {Image|Canvas} the source object of the texture.
可见,这个source可以是image对象或canvas对象, PIXI会根据对象检测自动处理的。
js中函数天生多态,因为没有参数模式的硬性设置,所以我们可以根据参数的不同自动为其做检测。
之前源码中许多让人疑惑的部分,都是因为分情况处理image或canvas导致的。
在这个模块源码的最后,有两个方法完美解决了刚才的疑惑:
下面就是PIXI对于image和canvas对象的分别处理。
这两个方法没有挂载在原型链上。可以直接作为方法调用,并返回一个new BaseTexture()
对象。简简单单就实现了基础材质的无new式创建!
BaseTexture.fromImage = function (imageUrl, crossorigin, scaleMode)
{
var baseTexture = utils.BaseTextureCache[imageUrl];
if (crossorigin === undefined && imageUrl.indexOf('data:') !== 0)
{
crossorigin = true;
}
if (!baseTexture)
{
var image = new Image();//document.createElement('img');
if (crossorigin)
{
image.crossOrigin = '';
}
baseTexture = new BaseTexture(image, scaleMode); //在外面看起来是个无new式创建
baseTexture.imageUrl = imageUrl;
image.src = imageUrl; //重要,这才是从image对象链接到基础材质类的关键起始一步。
utils.BaseTextureCache[imageUrl] = baseTexture; //材质注册
baseTexture.resolution = utils.getResolutionOfUrl(imageUrl);
}
return baseTexture;
};
BaseTexture.fromCanvas = function (canvas, scaleMode)
{
if (!canvas._pixiId)
{
canvas._pixiId = 'canvas_' + utils.uid(); //canvas通过_pixiId标识
}
var baseTexture = utils.BaseTextureCache[canvas._pixiId];
if (!baseTexture)
{
baseTexture = new BaseTexture(canvas, scaleMode);
utils.BaseTextureCache[canvas._pixiId] = baseTexture;
}
return baseTexture;
};
下一个材质是普通材质Texture类, 然而他又依赖一个VideoBaseTexture类,
我们先看这个类:
VideoBaseTexture = require(‘./VideoBaseTexture’),
“音视频材质”类, PIXI中音视频作为资源的一种来对待。用于加载音频或视频资源。
核心方法PIXI.VideoBaseTexture.fromUrl()
有四种加载url资源的方式。
有了基础材质我们看的一头雾水的经验,这次我们从入口方法开始看:
VideoBaseTexture.fromUrl = function (videoSrc, scaleMode)
{
var video = document.createElement('video'); //这里是dom和canvas交互的最初始步骤
if (Array.isArray(videoSrc))
{
for (var i = 0; i < videoSrc.length; ++i)
{ //构建多层dom
video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime));
}
}
else
{ //单层dom
video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime));
}
video.load();
video.play();
return VideoBaseTexture.fromVideo(video, scaleMode);
};
VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl;
function createSource(path, type) //在video中创建source,是video特性。
{
if (!type)
{
type = 'video/' + path.substr(path.lastIndexOf('.') + 1);
}
var source = document.createElement('source');
source.src = path;
source.type = type;
return source;
}
在这里主要是资源在dom部分的处理,然后我们进fromVideo看下:
VideoBaseTexture.fromVideo = function (video, scaleMode)
{
if (!video._pixiId)
{
video._pixiId = 'video_' + utils.uid();
}
var baseTexture = utils.BaseTextureCache[video._pixiId];
if (!baseTexture)
{
baseTexture = new VideoBaseTexture(video, scaleMode); //桥接至PIXI框架
utils.BaseTextureCache[ video._pixiId ] = baseTexture;
}
return baseTexture;
};
fromVideo将dom资源顺利的引入到PIXI的VideoBaseTexture类里,并携带了video标签对象。这个类便不再难以理解了。
VideoBaseTexture类继承自BaseTexture类。所以onload这些过程便不需要再写了。
只需要对音视频资源做一些额外的处理。
多出了一些原型方法,他们会在video触发相应的事件时触发,并修改音频材质对象的状态。
source.addEventListener('canplay', this._onCanPlay);
source.addEventListener('canplaythrough', this._onCanPlay);
source.addEventListener('play', this._onPlayStart.bind(this));
source.addEventListener('pause', this._onPlayStop.bind(this));
下面是Texture依赖的另一个类TextureUvs
TextureUvs = require('./TextureUvs'),
这个类内容较少, 用来存储Uvs of a texture
(猜测是预览图像,实现对象展示时的快照功能)。
我们接着看材质类Texture
Texture = require(‘./Texture’),
最终的这个材质类集合了之前的所有的辅助材质类。
从他所暴露出的接口就可以看出,无论是来自image还是canvas还是video,我们调用这个类都会用对应的辅助类来差异化处理。
Texture.fromImage = function (imageUrl, crossorigin, scaleMode){...}
Texture.fromFrame = function (frameId){...}
Texture.fromCanvas = function (canvas, scaleMode){...}
Texture.fromVideo = function (video, scaleMode){...}
Texture.fromVideoUrl = function (videoUrl, scaleMode){...}
Texture.addTextureToCache = function (texture, id){...}
Texture.removeTextureFromCache = function (id){...}
上述方法里都会将生成的BaseTexture作为参数来初始化Texture, 例如这样:
Texture.fromImage = function (imageUrl, crossorigin, scaleMode)
{
var texture = utils.TextureCache[imageUrl];
if (!texture)
{
texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode));
utils.TextureCache[imageUrl] = texture;
}
return texture;
};
拿到的BaseTexture对象会作为new Texture对象的BaseTexture属性。
所以我们就能理清这两个类之间的关系了,BaseTexture是Texture的辅助类,是用来单纯展现或表示其材质的。
Texture在他们之上添加了frame属性(frame是PIXI的矩形类),制定了材质的显示范围。他是用框架中常用的Object.defineProperties
方法来巧妙地定义的。
Object.defineProperties(Texture.prototype, {
frame: {
get: function ()
{
return this._frame; //Texture.frame 时获取到 _frame
},
set: function (frame) //Texture.frame = Frame 时会执行,封装了setter操作
{
this._frame = frame; //对frame的读写其实都是对内部变量_frame的操作,这两个属性值等价,setter操作不等价。
this.noFrame = false; //还要额外处理的属性之一
this.width = frame.width; //额外处理的属性之二
this.height = frame.height; //额外处理的属性之三
if (!this.trim && !this.rotate && (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height))
{
throw new Error('Texture Error: frame does not fit inside the base Texture dimensions ' + this);
}
this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded;
if (this.trim)
{
this.width = this.trim.width;
this.height = this.trim.height;
this._frame.width = this.trim.width;
this._frame.height = this.trim.height;
}
else
{
this.crop = frame;
}
if (this.valid)
{
this._updateUvs(); //额外执行的方法
}
}
}
});
我们什么时候需要这样来定义属性呢?
我们先想一个问题,当前这个Texture类有很多属性,但是很多是使用者不需要关注的,他们可能只是用来记录状态或缓存用的。
但是我们对一些关键属性的操作会影响到他们,如果不对关键属性做自定义setter处理,那么就需要开发者在执行赋值操作后手动修改那些辅助属性。这显然是不可思议的。
于是Object.defineProperties
封装关键属性的setter操作,让赋值操作能带上许多其他属性的额外处理。
下面我们回到本节最初我们要看的RenderTexture类, 在我们对BaseTexture和Texture了解完毕之后,
终于到了渲染器部分。
RenderTarget = require(‘../renderers/webgl/utils/RenderTarget’),
这个类是用来webGL模式下处理最基础的绘制对象的,接受的参数中最重要的就是他的第一个参数:从canvas标签上通过getContext(‘webgl’)方法获取到的webGL绘制对象。
他依赖一个StencilMaskStack = require('./StencilMaskStack');
遮罩相关的数据结构。
所以,这个类中很多方法因为涉及webgl的api,所以对前端工程师来说相对比较生僻。
FilterManager = require(‘../renderers/webgl/managers/FilterManager’),
这个类管理整个过滤器, 继承自WebGlManage。
他是webgl特有的。
CanvasBuffer = require(‘../renderers/canvas/utils/CanvasBuffer’),
我们知道pixi在浏览器不支持webgl的时候会切换成canvas渲染,这里封装好了一个canvas渲染器。
内容比较少,我们可以一窥这个封装的过程。
function CanvasBuffer(width, height)
{
this.canvas = document.createElement('canvas'); //新建一个canvas
this.context = this.canvas.getContext('2d'); //获取其渲染对象
this.canvas.width = width;
this.canvas.height = height;
}
CanvasBuffer.prototype.constructor = CanvasBuffer;
module.exports = CanvasBuffer;
Object.defineProperties(CanvasBuffer.prototype, {
width: {
get: function ()
{
return this.canvas.width;
},
set: function (val)
{
this.canvas.width = val;
}
},
height: {
get: function ()
{
return this.canvas.height;
},
set: function (val)
{
this.canvas.height = val;
}
}
});
CanvasBuffer.prototype.clear = function () //封装器的清除方法本质是对其内置的canvas内容的清除
{
this.context.setTransform(1, 0, 0, 1, 0, 0);
this.context.clearRect(0,0, this.canvas.width, this.canvas.height);
};
CanvasBuffer.prototype.resize = function (width, height)
{
this.canvas.width = width;
this.canvas.height = height;
};
CanvasBuffer.prototype.destroy = function () //封装器销毁方法只需把原来的canvas元素的引用丢失就行了,这些丢失引用的dom对象和普通的js对象之后都会被javascript的垃圾回收机制给处理掉,这就是封装器销毁的原理,即引用丢失。
{
this.context = null;
this.canvas = null;
};
可见,pixi里面的构造函数,也就是类,是非常多的。
不过有很多都是装饰者模式,把其他对象封装作为自己的一个属性,然后赋予其更多的属性和方法。
我们在研习框架源码的时候,一定要知道某个类的用法,确定我们操作的对象到底是谁。
比如上面这个canvasBuffer类。
使用这个类,本质还是在操作一个canvas的dom对象,但是却为你方便的封装了一系列简单操作。
比如context已经为你制定好了就是getContext(‘2d’),比如调用clear就会直接清空canvas这样的。
canvas为我们暴露出的api其实都是相当底层和原始的,pixi的这一层封装就像是研磨不平滑的平面,是很有必要的。
我们说完了RenderTexture的依赖,终于可以看看他本身了。
RenderTexture = require(‘../textures/RenderTexture’),
A RenderTexture is a special texture that allows any Pixi display object to be rendered to it.
注释是这样介绍他的,RenderTexture是一个特殊的材质,允许任何Pixi的displayObject用它来进行渲染。
所有的材质都要被预加载,否则就会呈现出一个黑色矩形。
同时RenderTexture还会为displayobject提供一个无关位置和旋转角度的快照。我们有理由相信这个功能一定来自TextureUvs类。
function RenderTexture(renderer, width, height, scaleMode, resolution){...}
其接受五个参数:
-
renderer {PIXI.CanvasRenderer PIXI.WebGLRenderer} 渲染器 - [width=100] {number} 要渲染的宽度
- [height=100] {number} 要渲染的高度
- [scaleMode] {number} 要渲染的拉伸模式
- [resolution=1] {number} 分辨率
对于不同渲染模式下会有不同的材质渲染方式。
if (this.renderer.type === CONST.RENDERER_TYPE.WEBGL)
{
var gl = this.renderer.gl; //webgl模式下我们textureBuffer是renderTarget实例。
this.textureBuffer = new RenderTarget(gl, this.width, this.height, baseTexture.scaleMode, this.resolution);//, this.baseTexture.scaleMode);
this.baseTexture._glTextures[gl.id] = this.textureBuffer.texture;
this.filterManager = new FilterManager(this.renderer); //当然webgl下才有过滤模式
this.filterManager.onContextChange();
this.filterManager.resize(width, height);
this.render = this.renderWebGL;
this.renderer.currentRenderer.start();
this.renderer.currentRenderTarget.activate();
}
else //canvas模式下没有filterManager,textureBuffer源自canvasBuffer。
{
this.render = this.renderCanvas;
this.textureBuffer = new CanvasBuffer(this.width* this.resolution, this.height* this.resolution);
this.baseTexture.source = this.textureBuffer.canvas;
}
毫无疑问,webGl模式的渲染会更强大,相对的,源码内部的实现方式也复杂点,不过用户并不要关心webgl与canvas的切换。他们的切换是自动且封装完毕的。除非有指定渲染模式的特殊需求。
RenderTexture提供了一个获取渲染结果的方法。
他是由该类连续三个方法实现的。
RenderTexture.prototype.getImage = function ()
{
var image = new Image();
image.src = this.getBase64();
return image; //返回一个可用于直接展示的image对象。
};
RenderTexture.prototype.getBase64 = function ()
{
return this.getCanvas().toDataURL(); //返回canvas图像转换为DataURL的结果
};
RenderTexture.prototype.getCanvas = function ()
{
if (this.renderer.type === CONST.RENDERER_TYPE.WEBGL)
{
var gl = this.renderer.gl;
var width = this.textureBuffer.size.width;
var height = this.textureBuffer.size.height;
var webGLPixels = new Uint8Array(4 * width * height);
gl.bindFramebuffer(gl.FRAMEBUFFER, this.textureBuffer.frameBuffer);
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, webGLPixels);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
var tempCanvas = new CanvasBuffer(width, height);
var canvasData = tempCanvas.context.getImageData(0, 0, width, height);
canvasData.data.set(webGLPixels);
tempCanvas.context.putImageData(canvasData, 0, 0);
return tempCanvas.canvas; //将webGL内容用一个临时canvas保存来返回
}
else
{
return this.textureBuffer.canvas; //返回canvas对象
}
};
这个例子可以很好的界定面向对象编程中方法“职责”的边界。
从一个只想获取iamge对象的开发者的角度,getImage还要依次执行getBase64和getCanvas方法,一个getImage方法拆分成三段简直蠢得可以。
但是假如其他开发者只想获取这个canvas的base64信息或只是canvas元素本身呢?
按照那个“聪明”的开发者想法,我们还得重写个包含getCanvas的getBase64方法和一个裸的getCanvas方法。
牺牲了一部分可读性和连贯性,A+B+C式的定义方法,换来了结构的清晰和三个耦合度极低的API。
如果变成ABC+BC+C的模式,只为了每个方法的流畅,却增加了大量的冗余代码。得不偿失。
而且,后者还完全没有考虑框架的扩展性。每次在调用链上增加一个方法,都是N(N是之前调用链长度)倍于前者的代码冗余!
所以说,这三个方法的定义很规范也很巧妙。
他们之前分工明确,功能清晰,每个都可以独立成一个API。从任何一个方法切入都不会有功能差错。
这就是一个健壮的库提供的API的标准。也是面向对象编程中界定类的方法的依据。
RenderTexture类还有获取全部像素和单个像素的两个原型方法。我们已经知道PIXI会有两套渲染模式,如果对webGL模式不熟悉,我们直接看稍微熟悉点的canvas方法吧,这也是种快速理解源码的办法。
RenderTexture.prototype.getPixels = function ()
{
var width, height;
if (this.renderer.type === CONST.RENDERER_TYPE.WEBGL)
{
...
}
else
{
width = this.textureBuffer.canvas.width;
height = this.textureBuffer.canvas.height;
return this.textureBuffer.canvas.getContext('2d').getImageData(0, 0, width, height).data; //获取全部像素数组的数据
}
};
RenderTexture.prototype.getPixel = function (x, y)
{
if (this.renderer.type === CONST.RENDERER_TYPE.WEBGL)
{
...
}
else //否则获取指定X,Y位置的单个像素的数据
{
return this.textureBuffer.canvas.getContext('2d').getImageData(x, y, 1, 1).data;
}
};
至此,我们用很长的篇幅看完了材质类。关于真正用到它的DsiplayObject,sprite等重要方法只能留到下一章啦。