Phaser 4.0 Beta 7 相机矩阵系统重写与渲染管线变更深度解析
【免费下载链接】phaserPhaser is a fun, free and fast 2D game framework for making HTML5 games for desktop and mobile web browsers, supporting Canvas and WebGL rendering.项目地址: https://gitcode.com/gh_mirrors/ph/phaser
导读
本文围绕 Phaser 4.0 beta 7 的核心变更展开:相机系统矩阵计算被整体重写,从单矩阵模型演进为"视图矩阵 + 外部矩阵"双矩阵模型,同时引入matrixExternal、matrixCombined与copyWithScrollFactorFrom等新 API。文章将结合仓库源码(Camera.js、GetCalcMatrix.js、TransformMatrix.js)与单元测试(GetCalcMatrix.test.js)逐项拆解这次重写的设计动机、实现细节与迁移影响,并同步梳理SpriteGPULayer批量渲染增强、roundPixels默认值调整等一系列修复与优化,帮助你准确评估升级风险并适配新 API。
一、为什么重写相机矩阵系统
Phaser 3 时代,相机将位置(position)、旋转(rotation)与缩放(zoom)统一合并进Camera#matrix一个矩阵中,滚动偏移(scroll)则在渲染流程的后续阶段被"追加"进去。这种单矩阵方案在常规场景下工作良好,但一旦涉及嵌套变换(nested transforms)、滤镜(filters)以及其他依赖变换矩阵的内部系统,就会出现难以排查的错位问题——因为"相机在哪"和"相机看到什么"被耦合在同一个矩阵里,无法干净地分离。
beta 7 的核心思路是把这两种语义拆开:
- 视图(View):相机的旋转、缩放与滚动 —— 决定"世界被如何观察";
- 位置(Position):相机在屏幕上的平移 —— 决定"相机本身在哪里"。
从源码可以看到这一拆分的落点。在 Camera.js 的preRender流程中,matrix只承载视图变换(ITRS 顺序应用 origin、rotation、zoom 与滚动平移),而matrixExternal通过applyITRS(this.x, this.y, 0, 1, 1)单独记录相机位置,最后用matrixExternal.multiply(matrix, this.matrixCombined)合成出组合矩阵:
// src/cameras/2d/Camera.js(节选) matrix.applyITRS(originX, originY, this.rotation, zoomX, zoomY); matrix.translate(-sx - originX, -sy - originY); matrixExternal.applyITRS(this.x, this.y, 0, 1, 1); this.shakeEffect.preRender(); matrixExternal.multiply(matrix, this.matrixCombined);这个重写不改变开发者设置相机属性的方式(setScroll、setZoom、setRotation等用法不变),它主要影响内部渲染系统。只有当你直接读取相机矩阵时,才会感知到差异。
二、双矩阵模型:matrix / matrixExternal / matrixCombined
beta 7 之后,相机持有三份矩阵,语义截然不同:
| 矩阵属性 | 包含内容 | 典型用途 |
|---|---|---|
Camera#matrix | 旋转 + 缩放 + 滚动(不含位置) | 渲染到 framebuffer / 滤镜合成场景下的视图矩阵 |
Camera#matrixExternal | 相机位置(新增) | 将相机"摆放"到屏幕上的外部变换 |
Camera#matrixCombined | matrix × matrixExternal(两者相乘) | 常规渲染时使用的最终视图矩阵 |
选择逻辑集中体现在新增的Camera#getViewMatrix(forceComposite)方法(Camera.js):
- 当相机需要渲染到 framebuffer,或挂载了内部/外部滤镜(
filters.external.length > 0 || filters.internal.length > 0),或forceComposite为true时,返回不包含位置的matrix; - 否则返回组合矩阵
matrixCombined。
getViewMatrix: function (forceComposite) { if ( forceComposite || this.forceComposite || this.filters.external.length > 0 || this.filters.internal.length > 0 ) { return this.matrix; } else { return this.matrixCombined; } }之所以在"渲染到 framebuffer"场景下要剔除位置,是因为 framebuffer 是一个离屏目标,相机位置此时属于"屏幕空间"的概念,不应被带入纹理坐标空间。这正是ignoreCameraPosition参数的用途(详见下一节)。
三、GetCalcMatrix 的变化与 GetCalcMatrixResults 新成员
GetCalcMatrix(src, camera, parentMatrix, ignoreCameraPosition)是 Phaser 渲染管线中计算游戏对象最终变换矩阵的核心入口,几乎所有 WebGL/Canvas 渲染器都会调用它。beta 7 给它增加了一个新参数:
ignoreCameraPosition(布尔值,默认false):为true时,返回结果中的"外部矩阵"使用单位矩阵而不是相机的matrixExternal,从而让对象变换完全脱离相机位置影响。这个标志在相机渲染到 framebuffer(滤镜、RenderTexture 内部)时非常关键。
对应实现见 GetCalcMatrix.js:
var GetCalcMatrix = function (src, camera, parentMatrix, ignoreCameraPosition) { if (ignoreCameraPosition) { camExternalMatrix.loadIdentity(); } else { camExternalMatrix.copyFrom(camera.matrixExternal); } camMatrix.copyWithScrollFactorFrom( ignoreCameraPosition ? camera.matrix : camera.matrixCombined, camera.scrollX, camera.scrollY, src.scrollFactorX, src.scrollFactorY ); calcMatrix.copyFrom(camMatrix); if (parentMatrix) { calcMatrix.multiply(parentMatrix); } spriteMatrix.applyITRS(src.x, src.y, src.rotation, src.scaleX, src.scaleY); calcMatrix.multiply(spriteMatrix); return result; };返回值result在 GetCalcMatrixResults.js 中定义,beta 7 新增了cameraExternal属性:
| 结果属性 | 类型 | 含义 |
|---|---|---|
cameraExternal | TransformMatrix | 相机外部矩阵(相机在屏幕上的位置);ignoreCameraPosition时为单位矩阵 |
camera | TransformMatrix | 相机视图矩阵,已按对象scrollFactor修正滚动偏移 |
sprite | TransformMatrix | 游戏对象自身的世界变换矩阵 |
calc | TransformMatrix | 最终合成矩阵(相机 × 父级 × 对象) |
配套的单元测试(GetCalcMatrix.test.js)精确验证了这一行为:
ignoreCameraPosition = true时,cameraExternal的a/b/c/d/tx/ty全部回到单位矩阵值(a=1, d=1, tx=0, ty=0);ignoreCameraPosition = false时,cameraExternal完整复制相机的matrixExternal(测试中验证了缩放4与平移(10, 20))。
同一测试还覆盖了滚动偏移与scrollFactor的组合([L204-L228]):scrollFactor = 1时滚动不产生位移,scrollFactor = 0时完整应用滚动偏移。此外GetCalcMatrix返回的是同一个结果对象引用(每次调用复用内部矩阵实例,见 [L230-L239]),这意味着拿到结果后必须立即使用或自行复制,否则下一次渲染会覆盖这些值——这是从 Phaser 3.50 起就一直存在的约定。
四、新方法 copyWithScrollFactorFrom:替代手工滚动运算
在 Phaser 3 时代,渲染代码里经常出现这类手工修正:
spriteMatrix.e -= camera.scrollX * src.scrollFactorX; spriteMatrix.f -= camera.scrollY * src.scrollFactorY;beta 7 提供了标准替代方案TransformMatrix#copyWithScrollFactorFrom(src, scrollX, scrollY, scrollFactorX, scrollFactorY)(TransformMatrix.js):
copyWithScrollFactorFrom: function (src, scrollX, scrollY, scrollFactorX, scrollFactorY) { var matrix = this.matrix; matrix[0] = src.a; matrix[1] = src.b; matrix[2] = src.c; matrix[3] = src.d; var sx = scrollX * (1.0 - scrollFactorX); var sy = scrollY * (1.0 - scrollFactorY); matrix[4] = src.a * sx + src.c * sy + src.e; matrix[5] = src.b * sx + src.d * sy + src.f; return this; }它从源矩阵复制旋转/缩放部分(a、b、c、d),并把滚动偏移按(1 - scrollFactor)的比例折算进平移分量(e、f)。注意一个易混淆点:scrollFactor越大,滚动对矩阵的影响越小——scrollFactor = 1表示对象跟随世界(不受相机滚动影响),scrollFactor = 0表示对象完全锁定在屏幕上(如 HUD)。上面的公式中(1.0 - scrollFactor)恰好体现了这一语义。
TransformMatrix.test.js 对该方法做了系统验证:滚动为(0, 0)时结果与源矩阵一致;scrollFactor = 1时不产生滚动位移;scrollFactor = 0时完整应用(100, 200)的滚动偏移;scrollFactor = 0.5时平移分量取半。
对内部系统的实际影响
- CanvasRenderer(src/renderer/canvas/CanvasRenderer.js)与TilemapLayerCanvasRenderer(src/tilemaps/TilemapLayerCanvasRenderer.js)等渲染器均改为消费新的矩阵体系;
GetCalcMatrix调用方在ignoreCameraPosition为true时改用camera.matrix而非camera.matrixCombined,确保 framebuffer 渲染不再被相机屏幕位置污染;- 新系统修复了大量嵌套变换、滤镜与变换矩阵组合使用时的历史问题(changelog 原文:"fixes many issues with nested transforms, filters, and other uses of transforms")。
五、SpriteGPULayer 批量渲染增强
SpriteGPULayer是 Phaser 4 引入的 GPU 批量精灵层,beta 7 为其补齐了一批面向性能调优的 API 与文档:
5.1 新增成员操作方法
SpriteGPULayer#insertMembers(index, members)(SpriteGPULayer.js):在指定索引处批量插入成员;SpriteGPULayer#insertMembersData(index, data)(SpriteGPULayer.js):直接以底层数据块形式插入成员,用于零拷贝的高效写入路径;SpriteGPULayer#getDataByteSize()(SpriteGPULayer.js):返回单个成员的字节大小,内部在分配成员缓冲区(this.nextMember = new ArrayBuffer(this.getDataByteSize()))时使用,外部在按字节写入成员数据时同样需要它来推算偏移。
5.2 非循环动画与动态粒子
beta 7 允许SpriteGPULayer成员动画设置loop: false,一次性播放后即终止。这一能力特别适合一次性粒子特效与动态来源(如程序生成的临时成员)——不需要为了单次播放创建并回收整个层。
从源码看,循环标志被编码进 delay 的符号位(SpriteGPULayer.js):value.loop !== undefined ? value.loop : true为默认值,if (!loop)时以负 delay 写入,解码端通过var loop = delay > 0还原。同时,Gravity 模式动画现在支持负加速度(即向上的"喷发"效果),该模式使用SpriteGPULayer#gravity属性,默认值为1024像素/秒²(SpriteGPULayer.js)。
5.3 分段(segment)处理修正
一个值得注意的底层修复:SpriteGPULayer的段数从 32 改为 24。原因是 32 段会与32 位数字处理发生冲突(segment 编码与位移位运算在 32 位整数边界上产生错误),24 段避开了这一陷阱。若你的代码直接操作 segment 索引或依赖原始编码布局,请注意:
- 段数上限已改为 24(
this._segments = 24,见 SpriteGPULayer.js); - 成员数据的位编码被重新排列(changelog:"Rearrange SpriteGPULayer data encoding"),因此自定义 shader 或直接读写成员数据的代码需要同步更新;
- 修复了从配置对象(config object)生成帧动画失败的问题。
六、其他小改动与新增能力
- Extern#render 文档:新增了编写
Extern#render函数的说明文档(Extern是外部渲染对象,用于把自定义渲染逻辑挂进 Phaser 渲染管线)。 - Tilemap 父矩阵支持:
TilemapLayer与TilemapGPULayer渲染时支持父矩阵(parent matrix),可以正确嵌套在Container等带变换的父级之下。 - Shape 默认
filtersFocusContext = true(Shape.js):防止滤镜上下文聚焦时把描边(stroke)裁剪到边缘之外。若你在Shape上叠加滤镜,将不再需要手动设置该标志。
七、修复与行为调整清单
7.1roundPixels默认改为false
这是 beta 7 中最可能影响现有项目观感的行为变化。在 Config.js 中:
roundPixels的默认值由true调整为false(GetValue(renderConfig, 'roundPixels', false, config));- 开启
pixelArt时仍会自动把roundPixels置为true("When enabled, this also setsantialiasandantialiasGLtofalseandroundPixelstotrue")。
官方注释给出了理由:该选项极易产生"messy results"(脏乱像素边缘),因此默认关闭;但它仍保留给确实需要整数像素对齐的场景。迁移建议:
- 追求像素风、需要纹理对齐像素网格的项目:显式配置
roundPixels: true或直接开启pixelArt: true; - 其他项目保持默认即可,无需改动。
7.2 DOMElement 无容器时报错
DOMElement依赖一个 DOM 容器来挂载元素。beta 7 起,若游戏配置中没有设置 DOM 容器,构造时会直接抛出明确错误:
throw new Error('No DOM Container set in game config');(见 DOMElement.js。)这要求使用DOMElement前必须在 Game 配置中提供dom.createContainer(或等价配置),错误信息比静默失败更容易定位问题。
7.3 TextureSource#setFlipY 全局生效
TextureSource#setFlipY(value)(TextureSource.js)现在能影响所有纹理——除了压缩纹理(compressed textures)——因为压缩纹理的方向是固定的。该方法控制 WebGL 纹理上传时的UNPACK_FLIP_Y_WEBGL标志;值为undefined时默认为true,且会触发this.update()刷新纹理。
7.4 WebGLProgramWrapper 对 undefined uniform 的识别
WebGLProgramWrapper现在能正确识别值为undefined的 uniform,并判断其是否真的发生了变化:如果 uniform 仍是undefined且没有更新,则跳过无意义的 GPU uniform 上传,减少不必要的状态更新开销。
7.5 TileSprite 与 smoothPixelArt
修复了TileSprite错误应用smoothPixelArt游戏选项的问题——此前该选项在TileSprite上可能被错误地套用,导致平铺精灵纹理出现意外的线性/最近邻过滤行为。
7.6 BatchHandler 事件引用
修复BatchHandler中缺少对 Renderer 事件(Renderer events)的引用问题(感谢 @mikuso 的反馈),避免在特定事件分发路径上出现引用错误。
八、升级与迁移清单
针对 beta 7 的变更,升级时建议逐项核对:
- 不要直接读取/改写
Camera#matrix来叠加滚动:改用camera.matrixExternal、camera.matrixCombined或通过GetCalcMatrix的结果消费矩阵;需要带滚动因子的副本时使用copyWithScrollFactorFrom。 - 自定义渲染器 / 自定义 shader:确认渲染管线传入的矩阵语义。渲染到 framebuffer 时传
camera.matrix(不含位置),常规渲染时传camera.matrixCombined;GetCalcMatrixResults的新属性cameraExternal对应相机屏幕位置。 SpriteGPULayer段数:若依赖 32 段布局,需调整为 24 段并适配新的位编码;一次性粒子特效请使用loop: false。roundPixels:若此前依赖默认开启的像素对齐,需在配置中显式声明。DOMElement:确保 Game 配置中创建了 DOM 容器,否则直接抛错。Shape+ 滤镜:无需再手动开启filtersFocusContext,默认已开启以避免描边被裁剪。
结语
Phaser 4.0 beta 7 的相机矩阵重写是一次"向内"的架构级变更:对外 API 保持稳定,对内则将视图与位置彻底解耦,并通过GetCalcMatrix(ignoreCameraPosition)为 framebuffer 渲染提供了一条干净路径,从根上解决了嵌套变换与滤镜场景下的矩阵污染问题。配合SpriteGPULayer的批量能力扩展与一系列渲染修复,这一版本为 Phaser 4 的正式发布奠定了更稳固的变换与渲染基础。后续版本中这些 API 已持续演进,建议以仓库内 changelog/v4/4.0-rc 目录下的相邻版本记录(如 beta-8、rc.1~rc.7)对照阅读,掌握完整演进脉络。
【免费下载链接】phaserPhaser is a fun, free and fast 2D game framework for making HTML5 games for desktop and mobile web browsers, supporting Canvas and WebGL rendering.项目地址: https://gitcode.com/gh_mirrors/ph/phaser
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考