PixiJS v8 PerspectiveMesh 实战指南:用四角透视网格实现 2.5D 卡片、地板与伪 3D 场景
【免费下载链接】pixijsThe HTML5 Creation Engine: Create beautiful digital content with the fastest, most flexible 2D WebGL renderer.项目地址: https://gitcode.com/gh_mirrors/pi/pixijs
PerspectiveMesh是 PixiJS v8 场景系统中专门用于"假透视"渲染的网格类:你只需指定纹理平面四个角在屏幕上的坐标,它就会在细分网格上逐顶点计算透视正确的 UV 插值,把一张 2D 纹理"斜放"进场景。本文以 skills/pixijs-scene-mesh/references/mesh-perspective.md 为骨架,结合仓库内 PerspectiveMesh 源码、PerspectivePlaneGeometry 源码 与官方示例深入讲解。读完本文,你将掌握PerspectiveMesh的完整配置项、四角顺序约定、逐帧动画与纹理热替换技巧,并能用它在 2D 渲染器中实现倾斜卡片、伪 3D 地板等 2.5D 效果。
PerspectiveMesh 是什么:UV 层面的透视投影
PerspectiveMesh是一个"渲染带透视投影纹理平面"的网格。与真实的 3D 渲染不同,它没有 Z 轴、没有深度缓冲、没有相机——透视效果完全是在 UV 坐标层面"算"出来的:
- 内部把平面细分成
verticesX × verticesY的网格(网格顶点位置与 UV 一一对应); - 每次设置四角坐标时,根据四个目标角点解出一个 2D 单应(homography)投影矩阵;
- 每个网格顶点都经过该矩阵变换并做齐次除法(
x/w、y/w),从而在二维空间内模拟出"近大远小"的透视形变。
正因为透视校正是逐顶点完成的,网格越密,投影越平滑;网格太稀,纹理就会被明显拉扯成三角形。PixiJS 把质量与性能的权衡完全交给了开发者(源码注释原文:"It is a balance between performance and quality! We leave that to you to decide.",见 PerspectiveMesh.ts)。
从类层次看,PerspectiveMesh extends Mesh<PerspectivePlaneGeometry>,而PerspectivePlaneGeometry extends PlaneGeometry(PlaneGeometry.ts)。也就是说它同时继承了MeshPlane的细分平面拓扑和Mesh的通用渲染能力。它的适用场景包括:
- 2D 广告牌(billboard)斜放效果;
- 延伸向远方的假地板;
- 倾斜的卡牌、菜单面板;
- 布局中的"伪 3D"视觉欺骗;
- 把 2D 纹理映射进 3D 场景贴图(源码 JSDoc 原话:"Great for mapping a 2D mesh into a 3D scene.")。
快速上手
最基础的用法如下(与文档 Quick Start 一致):
const texture = await Assets.load("card.png"); const mesh = new PerspectiveMesh({ texture, verticesX: 20, verticesY: 20, x0: 0, y0: 0, // top-left x1: 300, y1: 30, // top-right (raised) x2: 280, y2: 300, // bottom-right x3: 20, y3: 280, // bottom-left }); app.stage.addChild(mesh);四个角坐标在局部坐标系中定义了一个四边形,网格会把纹理"扭曲"进这个四边形,并自动计算透视正确的 UV。注意四角必须从左上角开始按顺时针列出,否则会产生视觉瑕疵(详见下文"四角顺序约定")。
构造函数选项详解
new PerspectiveMesh(options: PerspectivePlaneOptions)
注意:接口名是
PerspectivePlaneOptions而不是PerspectiveMeshOptions,它继承自MeshPlaneOptions(源码见 PerspectiveMesh.ts)。
| Option | Type | Default | Description |
|---|---|---|---|
texture | Texture | Texture.WHITE | 贴到四边形上的纹理。继承自MeshPlaneOptions;同时决定几何体的初始width/height。 |
verticesX | number | 10 | 网格列数。顶点越多透视越平滑,绘制开销越高。 |
verticesY | number | 10 | 网格行数。顶点越多透视越平滑,绘制开销越高。 |
x0 | number | 0 | 左上角 x。 |
y0 | number | 0 | 左上角 y。 |
x1 | number | 100 | 右上角 x。 |
y1 | number | 0 | 右上角 y。 |
x2 | number | 100 | 右下角 x。 |
y2 | number | 100 | 右下角 y。 |
x3 | number | 0 | 左下角 x。 |
y3 | number | 100 | 左下角 y。 |
几个关键点:
- 默认值来自
PerspectiveMesh.defaultOptions。默认配置是一块 100×100 像素、使用Texture.WHITE、10×10 顶点网格的正方形(PerspectiveMesh.ts)。它是一个静态类属性,可以全局覆盖,例如让所有新实例默认采用 15×15 网格并自带一点倾斜:
PerspectiveMesh.defaultOptions = { ...PerspectiveMesh.defaultOptions, verticesX: 15, verticesY: 15, // Move top edge up for default skew y0: -20, y1: -20, };- 构造函数不接受
geometry参数:PerspectiveMesh会自己构建内部的PerspectivePlaneGeometry(用纹理宽高作为平面宽高,见 PerspectiveMesh.ts)。这也是它与基类Mesh(必须显式传入geometry)最重要的区别。 - 其余
MeshOptions字段(shader、state、roundPixels)同样有效,完整字段清单见 mesh.md。 - 所有
Container选项也都合法(position、scale、tint、label、filters、zIndex等),完整说明见 constructor-options.md。官方示例 mesh_perspective_3d.ts 就同时使用了pivot、x、y、scale等容器属性把网格居中并放大。 - 运行时想改变透视,直接调用
mesh.setCorners(x0, y0, x1, y1, x2, y2, x3, y3)即可(见 PerspectiveMesh.ts)。
四角顺序约定:顺时针,从左上开始
四个角必须按top-left → top-right → bottom-right → bottom-left的顺时针顺序给出:
mesh.setCorners( 0, 0, // top-left (x0, y0) 200, 0, // top-right (x1, y1) 200, 200, // bottom-right(x2, y2) 0, 200, // bottom-left (x3, y3) );setCorners会原地更新几何体并立即重新计算投影,因此它天然适合放进每帧动画回调里。如果你违反顺序或四个角构成自交(蝴蝶结形)的非凸四边形,UV 插值会产生明显视觉瑕疵——这一点在"常见误区"一节还会详细展开。
核心使用模式
静态倾斜卡片
只需把上边两个角稍微抬高/压低,就能得到一张"斜插"进画面的卡牌:
const card = new PerspectiveMesh({ texture, verticesX: 20, verticesY: 20, x0: 0, y0: 0, // top-left x1: 300, y1: 30, // top-right (raised) x2: 280, y2: 300, // bottom-right x3: 20, y3: 280, // bottom-left });这也是 PerspectiveMesh.ts JSDoc 示例中"top-right raised"的用法。视觉回归测试 perspective-mesh.scene.ts 中同样用y1: 0抬高右上角、压低左上角来验证倾斜渲染(注意该测试排除了 canvas 渲染器,因为透视网格依赖 GPU 顶点变换)。
逐帧动画透视
在app.ticker里每帧调用setCorners,几何体会自动重算透视 UV:
const mesh = new PerspectiveMesh({ texture, verticesX: 20, verticesY: 20, }); app.ticker.add(() => { const t = performance.now() / 1000; const wave = Math.sin(t) * 30; mesh.setCorners(0, wave, 200, -wave, 200, 200, 0, 200); });上下两个角反向摆动,卡牌就像在绕水平轴轻微翻转。由于setCorners只是原地改写顶点缓冲并调用buffer.update()上传 GPU,逐帧调用开销很小(见 applyProjectiveTransformationToPlane.ts)。
更进一步,官方示例 mesh_perspective_3d.ts 展示了真正的进阶玩法:对四个角点做 3D 旋转矩阵 + 透视除法(scale = perspective / (perspective - z)),把旋转结果喂给setCorners,再用鼠标位置驱动angleX/angleY,就能在纯 2D 渲染器里模拟出"跟随鼠标旋转的 3D 卡牌"交互效果。这正是文档所说"no Z axis"限制下最实用的补偿手段。
顶点密度与质量权衡
const coarse = new PerspectiveMesh({ texture, verticesX: 5, verticesY: 5 }); const smooth = new PerspectiveMesh({ texture, verticesX: 30, verticesY: 30 });- 5×5 网格:透视形变会呈现明显的三角形拉伸;
- 20×20:文档建议的"不错默认值";
- 30×30 及以上:平滑,但顶点/索引数量与每次
setCorners的计算量同步上升。
两个轴默认都是10。透视校正发生在每个顶点上,所以"倾斜越明显,网格就要越密"。
伪 3D 地板(Fake Floor)
把"远"的两个角放在屏幕更高处、并且比"近"的两个角更靠拢,就能制造出地面向远处延伸的效果:
const floor = new PerspectiveMesh({ texture: floorTex, verticesX: 20, verticesY: 20, x0: 200, y0: 300, // near left on screen x1: 600, y1: 300, // near right x2: 800, y2: 200, // far right x3: 0, y3: 200, // far left }); app.stage.addChild(floor);搭配上方滚动播放的TilingSprite(如天空或路面贴图),即可做出风格化的 2D 驾驶/跑酷游戏背景。这是PerspectiveMesh在 2D 游戏中最经典的落地场景之一。
运行时热替换纹理
mesh.texture = await Assets.load("new-card.png");PerspectiveMesh覆写了texturesetter(PerspectiveMesh.ts):换纹理时会自动调用textureUpdated(),把几何体的width/height更新为新纹理尺寸,并重新执行一次updateProjection()(PerspectiveMesh.ts)。结果是:四角位置保持不变,透视投影在换图后依然成立,只是平面尺寸跟随新纹理同步。
源码级原理:从四角坐标到透视网格
理解底层实现能帮你更准确地预估质量与性能,也能避免踩坑。
1. 网格生成:PlaneGeometry.build
PerspectivePlaneGeometry继承自PlaneGeometry,构造时按verticesX × verticesY生成规则网格:位置缓冲按行列铺满width × height,UV 缓冲按[0,1]归一化,索引缓冲按每格两个三角形组织(PlaneGeometry.ts)。初始四角corners就是[0, 0, width, 0, width, height, 0, height]——一个未变形的矩形(PerspectivePlaneGeometry.ts)。
2. 单应矩阵求解:compute2DProjection
updateProjection()把纹理的四个源角(0,0)、(width,0)、(width,height)、(0,height)映射到你给定的四个目标角,交给compute2DProjection解出一个 3×3 投影矩阵(PerspectivePlaneGeometry.ts)。数学内核在 compute2DProjections.ts:它通过"basis-to-points"矩阵、伴随矩阵(adjugate)与 3×3 矩阵乘法,求出把源四边形射影变换到目标四边形的单应矩阵——这正是"透视校正"的数学本质,能正确处理平行线汇聚、近大远小等仿射变换做不到的效果。
3. 逐顶点应用:applyProjectiveTransformationToPlane
拿到投影矩阵后,applyProjectiveTransformationToPlane.ts 遍历每个网格顶点:
const newX = (a00 * x) + (a01 * y) + a02; const newY = (a10 * x) + (a11 * y) + a12; const w = (a20 * x) + (a21 * y) + a22; vertices[i] = newX / w; // 齐次除法 vertices[i + 1] = newY / w;每个顶点都要做一次齐次坐标除法(x/w、y/w),这也是"透视"的由来:距离越"远"的顶点w越大,位置被压缩得越厉害。最后调用buffer.update()把新位置推给 GPU。网格越密,这一步的顶点数与矩阵运算量越大——这就是verticesX/verticesY直接影响性能的底层原因。
从 MeshPlane.ts 的对比可以看出,普通MeshPlane只做网格细分和可选autoResize,而PerspectiveMesh在此之上追加了"角点 → 单应 → 顶点重投影"整条链路,这是两者能力上的分水岭。
常见误区
[HIGH] 期望真正的 3D
PerspectiveMesh只是带 UV 校正的 2D 网格:没有 Z 轴、没有深度缓冲、没有相机。真正的 3D 需要借助完整的 WebGL/WebGPU 3D 库,或者在外部手动计算好顶点位置后喂给setCorners(官方示例 mesh_perspective_3d.ts 就是"手动驱动"路线的参考实现)。文档明确警告:"This is not a full 3D mesh, it is a 2D mesh with a perspective projection applied to it."
[MEDIUM] 顶点太少,倾斜时明显拉伸
错误写法(2×2 网格只有两个三角形,纹理被线性拉伸、毫无透视校正):
const mesh = new PerspectiveMesh({ texture, verticesX: 2, verticesY: 2, x0: 0, y0: 0, x1: 300, y1: 50, x2: 280, y2: 250, x3: 20, y3: 200, });正确写法(20×20 网格,透视平滑):
const mesh = new PerspectiveMesh({ texture, verticesX: 20, verticesY: 20, x0: 0, y0: 0, x1: 300, y1: 50, x2: 280, y2: 250, x3: 20, y3: 200, });经验法则:只要能看到明显的倾斜,就把密度提到 10×10 以上。
[MEDIUM] 非凸四角(自交/蝴蝶结形)
如果四个角构成自交或非凸四边形,UV 插值必然产生视觉伪影。始终按一致的顺时针顺序列角,并确认四边形是凸的。这与compute2DProjection解单应矩阵的前提直接相关——对退化四边形的映射在数学上不稳定,会表现为纹理撕裂或扭曲。
相关资源与进一步阅读
- 技能总览:pixijs-scene-mesh/SKILL.md —— 了解
Mesh、MeshSimple、MeshPlane、MeshRope、PerspectiveMesh五个变体如何选型("需要倾斜 2D 卡片或地板 →PerspectiveMesh")。 - 基类参考:mesh.md ——
MeshOptions的shader、state、roundPixels等继承字段,以及MeshGeometry的 batching 规则。 - 源码:PerspectiveMesh.ts、PerspectivePlaneGeometry.ts、compute2DProjections.ts、applyProjectiveTransformationToPlane.ts。
- 官方示例:examples/mesh_perspective_3d.ts(鼠标驱动的伪 3D 旋转卡牌)。
- 视觉回归测试:tests/visual/scenes/mesh/perspective-mesh.scene.ts。
- 通用构造函数选项:constructor-options.md。
【免费下载链接】pixijsThe HTML5 Creation Engine: Create beautiful digital content with the fastest, most flexible 2D WebGL renderer.项目地址: https://gitcode.com/gh_mirrors/pi/pixijs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考