二喵今天分享下2d坐标系和3d坐标之间的转换!
昨天是疯狂星期四!很多同学问老菜喵3D游戏中,Canvas的坐标和3D世界中的坐标如何通过屏幕空间坐标相互转换。

老菜喵请出了我们的老演员坤坤,让我们用一个吃金币的小游戏(小游戏结尾获取下载链接,模型来自真ikun,与老菜喵无关,商用后果自付~),一文搞懂空间坐标转换!
坐标的基本概念
在 Cocos 引擎中,屏幕空间坐标、世界坐标和本地坐标是三种不同的坐标系,它们各自有不同的用途和特点:
-
屏幕空间坐标(Screen Space Coordinates): 这是一个二维坐标系,用于确定元素在屏幕上的位置。它的原点(0,0)通常位于屏幕的左下角,最大值为屏幕的分辨率(例如,如果屏幕分辨率是1920x1080,那么右上角的屏幕空间坐标就是(1920,1080))。这个坐标系主要在处理用户输入(如点击或拖动)或在屏幕上显示UI元素时使用。
-
世界坐标(World Coordinates): 这是一个三维坐标系,用于确定游戏世界中物体的位置。世界坐标系统中的每个点都有一个唯一的位置,无论摄像机在何处,无论相机如何移动或旋转,其在世界空间中的坐标都是固定的。
-
本地坐标(Local Coordinates): 这也是一个三维坐标系,但它是相对于特定游戏对象的。在本地坐标系中,(0,0,0)总是表示对象的原点,其他坐标则表示相对于这个原点的位置。当你移动或旋转一个物体时,它的本地坐标将不会改变,但其世界坐标将会改变。编辑器中默认的坐标系就是本地坐标,有个特殊情况当本地坐标的父节点坐标(包括父节点的父节点)也都是(0,0,0),且不存在缩放和旋转,我们也可以使用本地坐标替代世界坐标,这时候本地坐标和世界坐标的值是一样的。

总的来说,屏幕空间坐标、世界坐标和本地坐标都是描述物体位置的方式,但它们是在不同的上下文和用途中使用的。
2D UI坐标转 3D 节点坐标
由于2D UI和3D 节点的相机不一样,这里的转换其实指的是视觉上,通过坐标转换,让3D 节点的位置在视觉上看起来一致。
这个Demo中比较有代表性的粒子就是 3D的金币飞到了2D 节点的下,并增加金币数量。

我们来简单看下这段代码,先使用2D UI的相机把2D物体坐标转换了了屏幕空间坐标,再把屏幕空间坐标转换了3D相机下的世界坐标,随着3D相机的位移和缩放,这个转换过的世界坐标不是一个绝对坐标,会随着3D坐标的位移和缩放而发生改变。
/* 2D UI Node's wordposition to screen space pos*/
aa.cameras[1].worldToScreen(this._coinView.worldPosition, temp_V3_3);
const flyingPos = new Vec3();
/* screen space pos to 3d world position under this camera*/
aa.cameras[0].screenToWorld(temp_V3_3, flyingPos);
所以当我们进行金币飞行时候,为了节省性能,最好在金币飞行时候暂停镜头的跟随与移动。(这里给角色添加了一个举手的动画,并做了暂停)

接下来再飞行中,我们希望3D金币能飞到2D的金币精灵上面,这时候需要改变下观察3D金币的相机,我们额外创建一个金币相机,这个相机只能观察3D_UI场景,且优先级是2,模式是depth_only.

(2D相机的优先级是1,这个相机渲染的画面会在2D之上)
最后,由于我们使用的3D主相机是透视相机,和正常人眼观察的情况一样,符合近大远小的规律,我们2D节点在屏幕边缘,转换的屏幕坐标再到3D世界坐标,所处的位置也会比较边缘,为了让视觉上一致,我们给金币添加了一个tween动画,让金币的scale也慢慢变大。
代码如下:
for (var i = bl - 1; i >= 0; i--) {
const coin = backCoins[i];
/* change layer to be visible to 3D coins' camera in order to be viewed upon the 2D camera*/
coin.parent = aa.layer3D[4];
coin.layer = Layers.Enum.UI_3D;
coin.setRotationFromEuler(90, 0, 0)
const delay = i * dt;
/* move and scale the coin */
tween(coin).delay(delay).to(0.5 + delay, { position: flyingPos, scale: coinScaled }, { easing: 'fade' }).call(() => {
aa.res.putNode(coin);
}).start();
}
整个2D UI坐标转 3D 节点坐标的流程就完成了。
重点如下:
- 2D相机负责把2D物体的世界坐标转换成屏幕空间坐标
- 3D相机负责把屏幕空间坐标转换成3D相机观察中的世界坐标
- 3D相机是透视相机时候需要放大3D节点,是正交相机时候则无需放大
3D节点 坐标转 2D 节点坐标
这个需要也是很常用的,常用于2D节点跟随3D角色,如
- 角色血条
- 伤害文字
- 角色状态
这里我们给坤坤添加一个金币数量的状态,并切可以跟随金币的数量而改变跟随高度。
updateCoinBar(backCoins: Node[]) {
if (this._coinBarViewComp) {
/* backcoins length */
const bl = backCoins.length;
Vec3.copy(temp_V3_1, this._playerPos);
temp_V3_1.y += 1.5 + (bl + 1) * 0.075;
aa.cameras[0].convertToUINode(temp_V3_1, aa.layer2D[4], temp_V3_1);
this._coinBarView.position = temp_V3_1;
if (this._currentCoins != bl) {
this._currentCoins = bl;
this._coinBarViewComp.coin = "Coins " + bl;
}
}
}
在上述代码中我们首先获取角色的位置和金币的数量,并动态计算出状态栏在3D空间内相对应的位置,然后通过3D相机的convertToUINode 转换成2D节点下的本地坐标。
引擎convertToUINode的方法也是先把3D坐标转换成改相机相对于的屏幕空间坐标,再把屏幕空间坐标转换成2D节点下的本地坐标。
public convertToUINode (wpos: Vec3 | Readonly<Vec3>, uiNode: Node, out?: Vec3): Vec3 {
if (!out) {
out = new Vec3();
}
if (!this._camera) { return out; }
this.worldToScreen(wpos, _temp_vec3_1);
const cmp = uiNode.getComponent('cc.UITransform') as UITransform;
const designSize = cclegacy.view.getVisibleSize();
const xoffset = _temp_vec3_1.x - this._camera.width * 0.5;
const yoffset = _temp_vec3_1.y - this._camera.height * 0.5;
_temp_vec3_1.x = xoffset / cclegacy.view.getScaleX() + designSize.width * 0.5;
_temp_vec3_1.y = yoffset / cclegacy.view.getScaleY() + designSize.height * 0.5;
if (cmp) {
cmp.convertToNodeSpaceAR(_temp_vec3_1, out);
}
return out;
}
当角色移动时候,金币状态也会跟随角色移动。

获取Demo
关注公众号老菜喵,回复 金币 即可获得



