ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Phaser 4 内部空间(Internal Space)完全指南:滤镜、遮罩与 0-1 坐标系实战解析

Phaser 4 内部空间(Internal Space)完全指南:滤镜、遮罩与 0-1 坐标系实战解析 Phaser 4 内部空间Internal Space完全指南滤镜、遮罩与 0-1 坐标系实战解析【免费下载链接】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 的滤镜Filter系统为背景聚焦内部空间Internal Space这一核心概念——它是理解遮罩Mask、模糊Blur等所有滤镜渲染行为的关键。文章将以一个真实的遮罩示例为线索逐帧拆解内部空间的渲染流程并结合仓库源码src/filters/Mask.js、src/gameobjects/components/Filters.js、src/gameobjects/components/FilterList.js讲解其底层实现原理。读完本文你将掌握内部空间与外部空间的区别、0-1 归一化坐标的使用规则、纹理遮罩与游戏对象遮罩的取舍以及如何避免遮罩对不上对象这一最常见的新手陷阱。引言Phaser 4 为什么需要内部空间Phaser 4 使用一套统一的Filter滤镜系统从最简单的模糊Blur到最复杂的遮罩Mask全部由它承载。滤镜在被应用时会工作在两种空间之一内部空间Internal Space聚焦于某个对象本身例如让一个滤镜或遮罩跟随、匹配某个具体的精灵或容器外部空间External Space聚焦于屏幕更准确地说是目标将要被绘制进去的那个帧缓冲例如让遮罩覆盖整个画面。两种空间各有用途但内部空间在初期最容易让人困惑——尤其是当你试图把某个对象摆成遮罩、期望它与被遮罩对象完美对齐时。因此官方文档专门编写了这份分步可视化指南本文在完整继承其内容的基础上补充了对应源码层面的解释。速查表内部空间的四条铁律先给出一份快速摘要方便你在实践中随时对照它就像在被遮罩对象的纹理上作画目标对象在渲染过程中被视为未变换untransformed状态——它的位移、缩放、旋转在内部空间阶段都被抵消请使用 0-1 空间所有坐标都是归一化的这里的1实际代表目标的宽度或高度像素如果目标对象无法提供尺寸信息则回退到外部空间。归一化坐标说明0-1 空间中的1并非 1 像素而是整个目标的宽/高。分步走可视化内部空间的渲染流程文档用一个完整示例演示了从加载资源到最终合成的全过程。下面我们逐步还原并对照每一步的渲染截图。示例场景代码先看官方示例场景的完整代码class Example extends Phaser.Scene { preload () { this.load.image(uv, assets/pics/uv-grid-diag.png); this.load.image(mask, assets/pics/splat1.png); } create () { const { width, height } this.scale; const bg this.add.gradient({ bands: { colorStart: 0x444488, colorEnd: 0x8866cc }, direction: Math.PI / 2, dither: true }, width / 2, height / 2, width, height); const maskSprite this.add.image(0, 0, mask) .setOrigin(0) .setScale(2); const uv this.add.image(width / 2, height / 2 256, uv) .enableFilters() .setScale(0.5) .setRotation(1); uv.filters.internal.addMask(maskSprite); } } const config { type: Phaser.AUTO, width: 1280, height: 720, backgroundColor: #304858, parent: phaser-example, scene: Example }; let game new Phaser.Game(config);这段代码的关键动作有三处值得单独拆解enableFilters()这是让一个游戏对象获得滤镜能力的开关。只有调用过它对象才拥有filters属性内含internal与external两个滤镜列表和专用的filterCamera。在源码中enableFilters 会为对象惰性创建一架isObjectInversion true的专用相机并把renderWebGLFilters注册进对象的渲染步骤列表——这正是内部空间能够抵消对象变换的机制入口。注意滤镜是WebGL 专属特性Canvas 渲染器下enableFilters会直接提前返回。uv.filters.internal.addMask(maskSprite)将maskSprite作为内部滤镜遮罩挂到uv对象上。addMask的完整签名见 FilterList.addMask是addMask(mask, invert, viewCamera, viewTransform, scaleFactor)其中mask既可以是纹理键名字符串也可以是一个 GameObject——两种方式的差异正是本文后半部分的核心。遮罩对象故意不放在 0-1 空间的中心maskSprite位于(0, 0)、origin 为(0, 0)、缩放为 2这是文档特意设置的对比参照用于暴露新手常见的对齐问题详见下文如果不应用变换会怎样一节。加载的纹理资源示例加载了两张纹理它们的尺寸并不相同uv-grid-diag.png1024×1024一张 16×16 的网格图颜色沿对角线渐变格子内循环显示 0-1A-F 的数字。它在游戏中被打到 0.5 倍缩放并被略微移出屏幕。![一张 16x16 的网格图颜色沿对角线渐变格子内循环显示数字 0-1A-F](https://raw.gitcode.com/gh_mirrors/ph/phaser/raw/55d1180c0ade93f82d4c14c12bcc88a1f0198c5c/docs/Phaser 4 Internal Space Guide/uv-grid-diag.png?utm_sourcegitcode_repo_files)splat1.png665×584一张带透明通道的泼溅墨迹图可作为遮罩使用。![一张带透明通道、可用于遮罩的泼溅墨迹图](https://raw.gitcode.com/gh_mirrors/ph/phaser/raw/55d1180c0ade93f82d4c14c12bcc88a1f0198c5c/docs/Phaser 4 Internal Space Guide/splat1.png?utm_sourcegitcode_repo_files)两张纹理尺寸不一致这一点非常重要——它是理解纹理遮罩会被拉伸这一现象的伏笔。游戏最终渲染效果如下![示例游戏最终的合成输出](https://raw.gitcode.com/gh_mirrors/ph/phaser/raw/55d1180c0ade93f82d4c14c12bcc88a1f0198c5c/docs/Phaser 4 Internal Space Guide/game-final.png?utm_sourcegitcode_repo_files)下面我们就来探索如何一步步得到这个结果。常规渲染不使用遮罩如果不使用任何遮罩Phaser 会把所有物体直接以其最终变换位置、缩放、旋转绘制到游戏画布上就像这样![未使用遮罩时的常规渲染效果](https://raw.gitcode.com/gh_mirrors/ph/phaser/raw/55d1180c0ade93f82d4c14c12bcc88a1f0198c5c/docs/Phaser 4 Internal Space Guide/game-regular.png?utm_sourcegitcode_repo_files)uv网格以 0.5 倍缩放、旋转 1 弧度出现在画面中maskSprite以 2 倍缩放出现在左上角其纹理大部分在屏幕外。而当我们使用滤镜如遮罩时渲染就必须换一条路径——帧缓冲。进入内部空间使用帧缓冲绘制滤镜时Phaser 会使用一个被称为帧缓冲framebuffer的独立画布这样就能在不动主画布的前提下完成中间处理。在绘制被遮罩对象之前渲染器已经完成了一系列步骤清空主画布、绘制背景色、绘制渐变背景、绘制maskSprite。此时主游戏画布是 1280×720![步骤 1清屏并绘制背景、渐变与遮罩精灵后的画面](https://raw.gitcode.com/gh_mirrors/ph/phaser/raw/55d1180c0ade93f82d4c14c12bcc88a1f0198c5c/docs/Phaser 4 Internal Space Guide/step-001-background.png?utm_sourcegitcode_repo_files)接下来是关键一步在绘制被遮罩的精灵时Phaser 会取消变换untransform它——移除全部缩放、旋转与位移把它还原到原始状态。同时无论对象的 origin 是什么它都会被居中放置让它完整出现在帧缓冲内。这就是内部滤镜工作所在的内部空间。结果看起来和原始纹理几乎一模一样尺寸是 1024×1024——尽管这个对象在游戏里实际上被缩放了![步骤 2内部空间基底——对象被取消变换并居中后的画面](https://raw.gitcode.com/gh_mirrors/ph/phaser/raw/55d1180c0ade93f82d4c14c12bcc88a1f0198c5c/docs/Phaser 4 Internal Space Guide/step-002-internal-base.png?utm_sourcegitcode_repo_files)说明一这张图看起来与原始纹理完全相同因为它直接取自运行截图。说明二帧缓冲在使用前其实会先被清空但空缓冲没有展示价值本文省略了所有清空步骤的图示——请默认每个新帧缓冲在使用前都是被清过的。如果渲染到这里就停止Phaser 会把这个纹理按精灵的原始变换重新变换回去并绘制效果将与上面的常规渲染一致只是多了一次帧缓冲中转。但这是滤镜的地盘流程不会就此打住。假如把纹理直接当作遮罩最简单的遮罩用法是直接使用一张纹理。此时流程是取上一步的结果把纹理作为遮罩套上去。但这里有个问题——两张纹理的分辨率不同1024×1024 对 665×584它们怎么对齐GL 渲染系统并不在意这个。它把纹理坐标一律当作 0-1 来处理无论源纹理是什么分辨率遮罩纹理会被拉伸去匹配目标![步骤 3A纹理遮罩——小尺寸遮罩纹理被拉伸铺满整个内部空间](https://raw.gitcode.com/gh_mirrors/ph/phaser/raw/55d1180c0ade93f82d4c14c12bcc88a1f0198c5c/docs/Phaser 4 Internal Space Guide/step-003A-texture-mask.png?utm_sourcegitcode_repo_files)可以看到较小的遮罩纹理被拉伸以适应更大的内部空间。这种自动适配有时正是你想要的但当你需要对齐更复杂的形状时往往希望所有东西保持原尺寸——这时就要改用游戏对象了。游戏对象遮罩当使用游戏对象GameObject作为遮罩时Phaser 会保持它们的变换不变——不取消变换、不重新定位让你能够精确控制遮罩的变换。实现方式遮罩对象通过一架由遮罩滤镜专属拥有的相机来绘制绘制目标是一个全新的帧缓冲。这样做的好处是如果遮罩对象本身很复杂例如一个包含大量子对象的 Container或自身也挂了滤镜的对象那么这些复杂度会在我们使用遮罩纹理之前就被烘焙解决掉。在本例中结果很简单遮罩精灵以它最初被赋予的变换绘制——位置(0, 0)、origin(0, 0)、缩放 2![步骤 3游戏对象遮罩——遮罩精灵以原始变换绘制不再被拉伸](https://raw.gitcode.com/gh_mirrors/ph/phaser/raw/55d1180c0ade93f82d4c14c12bcc88a1f0198c5c/docs/Phaser 4 Internal Space Guide/step-003-game-object-mask.png?utm_sourcegitcode_repo_files)注意此时对象已经不再恰好填满内部空间——它不再被拉伸。技术细节严格来说这个遮罩帧缓冲自身也会被拉伸只是它恰好与精灵帧缓冲同尺寸所以视觉上大小不变。在某些场景下你可能需要缩放遮罩帧缓冲——例如使用了非常大的对象、会产生巨大的帧缓冲时。请使用 Mask 滤镜的scaleFactor属性来控制这一点详见下文源码解析。遮罩纹理就绪后流程回到与普通纹理遮罩相同把这个帧缓冲作为遮罩应用。结果会被绘制到一个新的帧缓冲这是第三个临时帧缓冲——不过别担心它们会被回收复用![步骤 4游戏对象遮罩应用后的结果——遮罩超出边界基底纹理尺寸与位置不变](https://raw.gitcode.com/gh_mirrors/ph/phaser/raw/55d1180c0ade93f82d4c14c12bcc88a1f0198c5c/docs/Phaser 4 Internal Space Guide/step-004-mask-applied.png?utm_sourcegitcode_repo_files)注意一切保持原样遮罩纹理仍然超出边界基底纹理的大小和位置也没有变化。最后一步取被遮罩对象的原始变换把它应用到当前帧缓冲的纹理上。此时才真正改变大小与位置将纹理变换到它在游戏世界中的最终位置![步骤 5遮罩结果按对象原始变换进入游戏世界后的最终画面](https://raw.gitcode.com/gh_mirrors/ph/phaser/raw/55d1180c0ade93f82d4c14c12bcc88a1f0198c5c/docs/Phaser 4 Internal Space Guide/game-final.png?utm_sourcegitcode_repo_files)至此一次完整的内部遮罩渲染就完成了说明一整个输出作为一个整体被变换所以泼溅遮罩的手臂也跟着旋转了。说明二注意我们绘制了最终合成中并不在屏幕上的部分——右下角的紫色区域完全在屏幕之外。使用滤镜时请牢记整个内部空间都会被绘制哪怕部分最终会跑出屏幕。这既可能是优点例如模糊或置换滤镜需要读取帧缓冲那些区域的数据也可能是性能成本。假如遮罩对象不使用任何变换注意示例中的遮罩对象有一个特定变换位于(0,0)、缩放 2、origin(0,0)。如果完全不应用这些变换把对象留在(0,0)会发生什么![完全不应用遮罩对象变换时的游戏画面](https://raw.gitcode.com/gh_mirrors/ph/phaser/raw/55d1180c0ade93f82d4c14c12bcc88a1f0198c5c/docs/Phaser 4 Internal Space Guide/game-no-transform.png?utm_sourcegitcode_repo_files)结果完全不是我们想要的样子。发生了什么从背景里能找到线索遮罩对象不再完整地位于 0-1 空间内。它的中心在(0,0)大部分内容落入了 X 或 Y 的负区域因此屏幕上只能看到它的四分之一。遮罩帧缓冲同理——对象被画在角落里大部分内容在视野之外![步骤 3B未变换的遮罩对象——大部分内容落在负坐标区域](https://raw.gitcode.com/gh_mirrors/ph/phaser/raw/55d1180c0ade93f82d4c14c12bcc88a1f0198c5c/docs/Phaser 4 Internal Space Guide/step-003B-untransformed-mask.png?utm_sourcegitcode_repo_files)注意那不是一张小图它仍然是 1024×1024只是大部分区域是空的。核心经验法则遮罩对象应该待在 0-1 空间内其中 1 等于目标对象的完整尺寸。这条简单的规则能帮你避免大量令人头疼的对齐问题。内部滤镜之所以这样设计是为了让你能轻松、精确地在移动对象上控制复杂遮罩。你不需要去同步遮罩与对象的位置——只要使用内部空间你就能确切知道遮罩会落在哪里。实用的调试技巧通常你只需要把遮罩对象保留在主相机中可见就能直接看到它的内部位置渲染时把它的visible设为false即可隐藏——Mask 仍然会渲染一个不可见的对象。如果移动了主相机这种可视推断就会失效但规则不变让遮罩对象待在 0-1 空间里。外部空间External Space顺带把外部空间也讲清楚因为它与内部空间互为镜像。通常我们说外部空间就是屏幕或者说主相机。这通常是对的但严格来说外部空间是精灵原本要绘制进去的那个帧缓冲的空间——通常也就是屏幕但有时并不一样。例如如果在遮罩对象上挂一个滤镜它就会有两个不同的外部上下文第一个出现在它被绘制到游戏画布时尺寸是屏幕大小第二个出现在它被绘制到内部空间帧缓冲时尺寸是目标内部空间的大小。外部空间的特征不使用目标的变换——它保持原样并使用外部上下文的尺寸如果绘制一个简单纹理遮罩纹理会被拉伸到整个屏幕的尺寸完全忽略目标精灵的变换。这类用法适合镜子、屏幕等场景此时遮罩不属于被遮罩对象本身。下图中变换遮罩对象只是为了展示纹理被拉伸的方式该变换并不会用于遮罩。![使用外部纹理遮罩时的画面——纹理被拉伸到整个屏幕尺寸](https://raw.gitcode.com/gh_mirrors/ph/phaser/raw/55d1180c0ade93f82d4c14c12bcc88a1f0198c5c/docs/Phaser 4 Internal Space Guide/game-external-texture.png?utm_sourcegitcode_repo_files)如果绘制游戏对象遮罩它不会被变换。下例中虽然遮罩目标被缩放和变换了遮罩仍然使用遮罩对象当前在屏幕上的位置这种情况下变换遮罩对象确实会产生影响![使用外部游戏对象遮罩时的画面——遮罩使用对象在屏幕上的当前位置](https://raw.gitcode.com/gh_mirrors/ph/phaser/raw/55d1180c0ade93f82d4c14c12bcc88a1f0198c5c/docs/Phaser 4 Internal Space Guide/game-external-game-object.png?utm_sourcegitcode_repo_files)最后要记住某些对象的所有滤镜都必须使用外部空间它们仍然拥有 internal 滤镜对象只是内部滤镜使用外部坐标。原因是它们缺乏确定内部空间尺寸所需的信息——有时因为对象包含许多不可预测地移动的小对象有时因为边缘存在复杂的几何形状。源码视角内部空间的底层机制理解了渲染流程后再从源码层面验证一下这些机制是如何落地的。以下证据均来自当前仓库。滤镜挂在相机上filters.internal与filters.external每个相机包括每个启用滤镜的游戏对象专用的filterCamera都持有两个滤镜列表。见 Camera.jsthis.filters { internal: new Components.FilterList(this), external: new Components.FilterList(this) };FilterList 在文档注释中明确描述了完整的相机渲染步骤对象先渲染到与相机同尺寸的纹理内部滤镜对该纹理做处理通常同尺寸但可能为模糊而扩展纹理再按相机自身的变换绘制到上下文纹理外部滤镜继续处理最终结果绘制到上下文。并给出了一个直观例子一个旋转 45° 的对象如果应用水平模糊——内部滤镜下模糊会呈 45°因为发生在对象旋转之前外部滤镜下模糊保持水平因为发生在旋转之后。官方建议尽可能使用内部滤镜因为它们只作用于相机/游戏对象所在区域外部滤镜是全局的、开销更高。滤镜可以堆叠列表顺序即应用顺序。启用滤镜enableFilters()与filterCamera对游戏对象而言enableFilters()是入口。源码 Filters.js 显示它做了四件事检查 WebGL 渲染器this.scene.renderer.gl非 WebGL 环境直接返回惰性创建一架专用相机new Camera(0, 0, 1, 1)并设置isObjectInversion true——这正是取消对象变换的开关依据硬件最大纹理尺寸初始化maxFilterSizeWebGL 下至少 4096可调低以省内存把renderWebGLFilters作为渲染步骤插入到常规renderWebGL之前。此外focusFilters 展示了取消变换的实现它读取对象的x/y/rotation/scaleX/scaleY/origin算出中心后把滤镜相机centerOn到对象中心、setRotation(-rotation)反旋转、setZoom(1/scaleX, 1/scaleY)反缩放——于是对象在帧缓冲里变回未变换的原始模样也就是文档步骤 2 看到的效果。当filtersFocusContext为true对象没有可靠的getBounds/宽高时自动开启则改用外部上下文的大小与变换即回退到外部空间。Mask 滤镜纹理遮罩与游戏对象遮罩的实现src/filters/Mask.js 是内部/外部空间概念的集中体现纹理遮罩setTexture(key)L281-L292从纹理管理器查找帧并直接取其glTexture无任何更新逻辑——这与文档纹理遮罩被拉伸到 0-1 空间的描述一致游戏对象遮罩setGameObject(obj)L257-L268把对象渲染到一张动态纹理DynamicTexture上。updateDynamicTextureL208-L244在每次渲染时受autoUpdate/needsUpdate控制通过capture(gameObject, { transform, camera })把对象画进动态纹理——这正是文档通过专用相机绘制、复杂度被提前烘焙的源码依据scaleFactor构造参数之一默认 1L70-L73。它直接缩放动态纹理的宽高scaledWidth width * scaleFactor。官方注释特别提醒调整scaleFactor时必须同步调整遮罩对象的缩放以匹配并且组合 scale factor、对象变换与相机变换很容易把事情搞复杂请尽量精确——这正对应文档中遮罩帧缓冲自身也会被拉伸的提示invert是否反转遮罩交换隐藏与显示的区域viewCamera/viewTransform渲染游戏对象遮罩时使用的相机与变换模式local用对象自身属性world通过parentContainer计算世界位置autoUpdate默认true遮罩会随游戏对象变化自动更新置为false则可冻结遮罩。遮罩的本质见 Mask.js 类注释用纹理隐藏输入的一部分——将输入的每个 texel 的颜色与 alpha 乘以遮罩对应 texel 的 alpha。理解这一点有助于你设计遮罩纹理遮罩的透明区域会挖掉被遮罩对象。实践建议与常见陷阱综合文档与源码为你的实际开发总结以下建议做遮罩先想清楚用哪种空间要跟随/匹配某个具体对象 → 内部空间要覆盖整个画面如屏幕、镜子效果→ 外部空间。让遮罩对象待在 0-1 空间内1 目标对象的完整尺寸。对象中心最好放在目标内部空间的中心附近避免内容落入负坐标区域。纹理遮罩会被拉伸如果两张纹理分辨率不同纹理遮罩会铺满整个内部空间需要精确尺寸控制时改用游戏对象遮罩。善用visible调试让遮罩对象在主相机中可见即可实时观察其内部位置正式渲染时将其visible设为falseMask 仍会渲染这个隐形对象。警惕性能成本每个启用滤镜的对象、每个活动滤镜都会增加绘制调用外部滤镜是全局的、更昂贵。模糊/置换等滤镜会扩展帧缓冲注意maxFilterSize与scaleFactor的内存权衡。scaleFactor与对象缩放要同步调整改变遮罩纹理分辨率时必须同步缩放遮罩对象否则遮罩填充不准确。无尺寸信息的对象自动回退外部空间如果你的对象无法提供宽高如复杂的 Container 或几何边缘复杂的对象内部滤镜会退化为外部坐标——此时不要试图强行对齐内部空间。结论通过逐帧拆解一次内部遮罩的渲染我们完整看到了 Phaser 4 内部空间的工作方式对象被取消变换、居中绘制到帧缓冲遮罩在 0-1 空间内与之对齐最后整个结果再按对象原始变换进入游戏世界。嵌套的变换坐标确实复杂所以官方的建议很干脆——忽略嵌套变换把工作放在 0-1 空间里完成。配合源码Mask.js、FilterList.js、Filters.js你现在既能理解内部空间的渲染原理也能把握enableFilters、addMask、scaleFactor等关键 API 的真实行为。按照 0-1 空间法则设计你的遮罩对象大多数对齐问题都会迎刃而解。【免费下载链接】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),仅供参考
返回列表