Cocos Creator 3.x 如何将世界坐标转屏幕坐标?Creator 3D 怎么将 3D 坐标转化到 Canvas 上面?触摸的屏幕坐标如何转世界坐标?怎么把 Canvas 下的节点坐标转换为 3D 场景坐标?

在 Cocos 社区中,不时能看到和“坐标转换”有关的讨论。不论是 Cocos Creator 3D 版本,还是 2D 和 3D 产品融合后的 3.x 版本,都有大量开发者对这方面的操作存在困扰。今天给大家分享一篇Cocos布道师『放空』老师的一篇文章,让大家轻松搞懂坐标转换原理


大纲总览

PART 1. 屏幕坐标

PART 2. UI 触点坐标

PART 3. 不同坐标之间的转换

PART 4. 射线检测

PART 1

屏幕坐标

在 Cocos Creator 3.x 游戏运行时显示的画布大小就是屏幕区域,屏幕坐标是从画布的左下角为原点开始计算,可以通过 view.getCanvasSize 接口获取屏幕尺寸。

知道屏幕起点和屏幕尺寸,此时心里就可以大概估算出点击屏幕时候的触点位置值。触摸事件刚好可以帮助验证,通过 systemEvent 来监听:

1    // 监听系统触摸移动事件
2    onEnable () {
3        systemEvent.on(SystemEventType.TOUCH_MOVE, this._touchMove, this);
4    }
5
6    _touchMove(touch: Touch, event: EventTouch){
7        // 打印获取到的屏幕触点位置
8        console.log(touch.getLocation());
9    }

点击并移动后就能看出坐标值的走向,触点越往右,x 值越大,越往上,y 值越大。

在 Cocos Creator 3.x 中,3D 相机在不设置 viewport 的情况下,尺寸始终跟屏幕尺寸一样大小这一点很重要,因为后面的内容都离不开相机。

PART 2

UI 触点坐标 

UI 最大的特点就是适配与交互。

在看 UI 文档时,可能会获取到几个跟适配最直接的有关联的内容,比如设计分辨率、布局组件(Layout),对齐组件(Widget),多分辨率适配方案等。

其中,与坐标直接有关联的就是设计分辨率与多分辨率适配方案,这在官方文档多分辨率适配方案上都有提及,我在此处根据这个知识快速带大家了解一下适配规则是如何影响 UI 内容呈现

1

UI 多分辨率适配方案

在 UI 制作时,通常会先确定 UI 设计的大小,称之为设计分辨率。设计分辨率和适配模式决定了运行时 UI 内容尺寸(UI 根节点 Canvas 的尺寸)。

在这里必须明确知道的条件就是设计分辨率和设备分辨率(屏幕分辨率)肯定是固定不变的。在这个条件下,了解一下适合大多数游戏开发的两种模式,分别是适配宽度(fit width)适配高度(fit height)

在这里用一个例子做说明,假设设计分辨率是 960x640,UI 上有一个和设计分辨率一样大小的背景(没有添加 Widget)外加四个始终紧贴 UI 内容区域四角的精灵(四个精灵都带有 Widget 组件,分别是上、下、左、右紧贴 UI 内容区域四角)。

当选用的适配方案是适配高度,屏幕分辨率是 1334x750,此时引擎就会将设计分辨率的高度自动撑满屏幕高度,也就是将场景图像放大到 750/640=1.1718 倍,但是放大后的宽度 960*1.1718=1125 依然小于屏幕宽度 1334,这样就容易出现黑边(如下图背景没有铺满屏幕)。

这种方式肯定是大多数游戏开发者不想看到的情况,因此,引擎会根据 Widget 组件计算将宽度自动“铺满”屏幕(如下图四个精灵)。

当然,这样的方式也有弊端,就是背景如果完全适应设计分辨率大小(添加了适配组件 Widget,并且上下左右同时适配 UI 内容区域)会出现一定的拉伸。此处,由于拉伸比例较小,所以看的不是很明显,感兴趣的朋友可自行验证。

反过来,如果采用的屏幕分辨率是 960x720,场景图像放大到 720/640=1.125,放大后的宽度为 1.125*960=1080,大于屏幕宽度 960,这样就会出现内容裁切的现象(如下图背景)。因此,引擎会根据 Widget 组件计算将宽度自动“限制”在屏幕内(如下图四个精灵)。这种方式的弊端就是背景如果完全适应设计分辨率大小会出现挤压。

适配宽度也是同样原理。明白了适配原理,就可以根据不同平台选择合适的适配方案。

2

UI 触点获取

在 UI 上,大家应该最关心的是,如何根据触点位置设置 UI 元素。

对于 Creator 2D 用户来说,获取触点信息的方法是在事件监听回调里,通过 event.getLocation() 获取触点信息,这里的触点信息是屏幕触点根据 UI 内容区域计算出来的。

而在 Creator 3.x 里,屏幕和 UI 是完全区分开的,用户可以在没有 UI 的情况下点击屏幕获取触点信息。因此,获取屏幕触点,是通过 event.getLocation()。而希望同样按照 Creator 2D 的方式获取“屏幕触点”坐标,则可以通过 event.getUILocation() 的方式获取。

event.getUILocation() 获取到的触点信息,也可以用来直接设置 UI 元素的世界坐标(因为 UI 上的每像素等价于 3D 上的每单位,所以可以根据这个坐标点直接设置 UI 元素的世界坐标)。

1const location = touch.getLocation();
2console.log(`screen touch pos: ${location}`);
3const locationUI = touch.getUILocation();
4console.log(`UI touch pos: ${locationUI}`);

1const pos = new Vec3(locationUI.x, locationUI.y, 0);
2// 此处的 sprite 就是屏幕上的白色图片的引用
3this.sprite.setWorldPosition(pos);

当然,对于渲染原理比较清楚的同学,也可以直接通过屏幕坐标与 UI 相机的方式来处理,具体原理请参考下方的屏幕坐标与 3D 节点世界坐标互转

1// 此脚本挂载在 Canvas 节点身上23// 获取屏幕坐标4const location = touch.getLocation();5const screenPos = new Vec3(location.x, location.y, 0);6const pos = new Vec3();7const canvas = this.getComponent(Canvas)!;8// 获取 Canvas 关联相机的相机数据9const uiCamera= canvas.cameraComponent?.camera!;
10// 利用相机数据对象将屏幕点转换成世界坐标下的值
11uiCamera.screenToWorld(pos, screenPos);
12this.sprite.setWorldPosition(pos);
13
14注意:此处是因为 UI 没有深度信息,因此可以直接设置世界坐标,将 z 值固定为 0。如果是 3D 节点,还要考虑深度。
15在预计 3.4 以及之后的版本会统一用相机来转换,大家就不需要理解 UI 触点这个概念,之后也会提供相对应的转换方式使用文章,望悉知。

PART 3

不同坐标之间的转换

了解了屏幕坐标、UI  坐标以及相机,接下来就可以做更多坐标之间的转换。首先再说明一下本地坐标世界坐标之间的关系

世界坐标的中心点(也称之为原点)是节点在场景下创建的,位于节点树最外层并且它的 positon 属性值全部都是 0 的节点所处的位置,就是世界坐标的中心点。所有节点通过 node.getWorldPosition 接口获取到的值,都是对于该位置的偏移值,也可以理解为绝对差值。

本地坐标是相对于父节点的偏移。假设节点 A 的世界坐标为 (0,10,0),节点 B 是节点 A 的直接子节点,节点 B 的本地坐标是 (0,10,0),节点 A 和 B 都没有旋转和缩放,那么节点 B 的世界坐标是多少?

答案显而易见是(0, 20, 0)。节点 B 的坐标是本地坐标,本地坐标是相对于父节点的偏移,也就是 B 相对于 A 在 y 值上偏移 10,A 的世界坐标 y 值为 10,也就是相对于世界原点偏 y 值偏移了 10,那么可以计算出,B 相对于世界原点 y 值偏移了 10+10=20。

1

屏幕坐标与 3D 节点世界坐标互转

在进行 3D 节点位置转换到屏幕坐标这项工作之前,先要了解一下 3D 物体最终是如何渲染到屏幕上的。

首先,所有的物体都需要被相机照射才能显示,因此,物体需要在相机的视距框内,在这里需要做一次世界空间转相机空间的操作。其次,在相机空间下还需要做深度检测等操作,最后,将空间内容投影到屏幕。这部分怎么理解呢?看下图:

在这里用透视相机举例,这是透视相机的俯视图,透视相机近平面值是 1,远平面值是 1000。相机的可视范围是从近平面到远平面,从图上可以看出,越接近近平面,也就是深度值越小的横切面面积越小,反之越大。不同面积的横切面最终铺满绘制到同一个画布上,就会出现近大远小的效果,同时,不同切面之间也会经过深度检测实现视野近的物体遮挡视野远的物体。最后的呈现就有点类似于用相机拍照片这么一个过程,将所有立体的东西都“拍扁”。知道了物体如何绘制到屏幕上,反过来是不是也很容易呢?

其实并没有那么容易。因为平面的东西变得立体需要经过很多细节化处理。在这里通常需要处理的最直接的一点就是深度还原。根据画面的呈现,决定它原来应该处于的深度,因此,在做屏幕触点转 3D 节点世界坐标的时候,可以指定深度。深度的范围如上图是近平面到远平面之间,也就是 1 - 1000,在数值上采用的是归一化的值 0 - 1。在默认情况下,如果不指定深度,那么物体转换后的位置是在近平面位置,如果是 0.5,那么则是距离相机 500 左右的位置。

接下来,通过下方代码,就能轻松实现屏幕坐标与 3D 节点世界坐标的互相转换:

1// 3D 相机引用2@property(Camera)3camera: Camera = null!;45@property(MeshRenderer)6meshRenderer: MeshRenderer= null!;78onEnable () {9    systemEvent.on(SystemEventType.TOUCH_START, this._touchStart, this);
10}
11
12_touchStart (touch: Touch, event: EventTouch) {
13     // 获取 3D 相机里的相机数据
14    const camera = this.camera.camera;
15    const pos = new Vec3();
16
17    // 1. 3D 节点世界坐标转屏幕坐标
18    const wpos = this.meshRenderer.node.worldPosition;
19    // 将 3D 节点的世界坐标转换到屏幕坐标
20    camera.worldToScreen(pos, wpos);
21
22    // 2. 屏幕坐标转 3D 节点世界坐标
23    // 获取当前触点在屏幕上的坐标
24    const location = touch.getLocation();
25    // 注意此处的 z 值。将近平面到远平面之间的距离归一化到 0-1 之间的值。
26    // 如果值是 0.5,那么转换后的世界坐标值则是在相机近平面到远平面的中心切面的位置
27    const screenPos = new Vec3(location.x, location.y, 0.5);
28    camera.screenToWorld(pos, screenPos);
29}

为了便于观察,此处将相机的远平面调整为 20。

2

3D 节点之间的坐标转换

1// 3D 节点 nodeB 本地坐标转换到 3D 节点 nodeA 本地坐标
2const out = new Vec3();
3const tempMat4 = new Mat4();
4const nodeAWorldMat4 = nodeA.getWorldMatrix();
5Mat4.invert(tempMat4, nodeAWorldMat4);
6Vec3.transformMat4(out, nodeB.worldPosition, tempMat4);

3

屏幕坐标转 UI 触点坐标

在这里还是要明确一下,UI 触点坐标就是屏幕触点根据 UI 内容区域计算出来的值,也就是 UI 世界坐标值。这个概念本身不太好理解,也显得繁杂,因此,引擎组也会在之后的版本里改善这种转换方式,直接通过相机来处理。但是在 3.4 之前的版本还需要用,所以此处直接做一个简单的总结:

在监听节点事件触发的回调里执行 event.getUILocation 获得到的值是触点信息是屏幕触点根据 UI 内容区域计算出来的值,可以直接用于设置 UI 节点的世界坐标。如果有设计需求是需要点击屏幕设置 UI 节点的,就直接用该方法。

1const locationUI = touch.getUILocation();
2const pos = new Vec3(locationUI.x, locationUI.y, 0);
3this.sprite.setWorldPosition(pos);

4

UI 不同节点之间的坐标转换

与 Creator 2D 不同,UI 节点的尺寸和锚点信息不再在节点身上,而是每一个 UI 节点都会持有一个 UITransform 组件,因此,有关 UI 节点之间的变换 API 都在该组件身上。当获取到屏幕触点转换后的 UI 节点世界坐标之后,就可以根据该坐标转换到不同 UI 节点的本地坐标。

● 屏幕触点坐标转换到 UI 节点本地坐标

1// 假设此处有两个节点,nodeA 和它的字节的 nodeB,点击屏幕设置 nodeB 的坐标
2
3// 屏幕触点根据 UI 内容区域计算出来的值
4const locationUI = touch.getUILocation();
5const uiSpaceWorldPos = new Vec3(locationUI.x, locationUI.y, 0);
6const nodeAUITrans = nodeA.getComponent(UITransform)!;
7// 转换到 nodeA 节点下的本地坐标(该值也就是相对于 nodeA 的值)
8nodeAUITrans.convertToNodeSpaceAR(uiSpaceWorldPos, pos);
9nodeB.position = pos;

● UI 节点之间坐标互转

1// 假设此处有两个节点,nodeA 和 nodeB,它们既不为兄弟也不存在父子关系2const nodeBUITrans = nodeB.getComponent(UITransform)!;3// 最终偏移值存储在 pos 上4const pos = new Vec3();5// 获取 nodeA 相对于 nodeB 的偏移6nodeBUITrans.convertToNodeSpaceAR(nodeA.worldPosition, pos);7nodeA.parent = nodeB;8nodeA.position = pos;9
10// 如果此处希望取与自身带有一定偏移的点做转换,可以采用如下方法:
11// 相对于 nodeA x 轴偏移 10 个单位
12const offset = new Vec3(10, 0, 0);
13const nodeAUITrans = nodeA.getComponent(UITransform)!;
14// 将偏移后的值转换到世界坐标
15nodeAUITrans.convertToWorldSpaceAR(offset, pos);
16// 获取相对于 nodeA 在 x 轴上偏移 10 个单位的点,转换到相对于 nodeB 的偏移
17nodeBUITrans.convertToNodeSpaceAR(pos, pos);
18nodeA.parent = nodeB;
19nodeA.position = pos;

5

3D 节点世界坐标转 UI 节点本地坐标

上面的内容已经铺垫很多了,这里直接上代码,原理就是将 3D 节点的世界坐标转换到屏幕坐标再转换到 UI 节点的本地坐标。

1// 假设 3D 节点 cube,UI 节点 spriteA,2const out = new Vec3();3const wpos = cube.worldPosition;4// 直接调用相机的转换接口完成整个操作5// 此处 out 获取的值是“相对于”spriteA偏移的值6camera.convertToUINode(wpos, spriteA, out);7const node = new Node();8// 本地坐标才是相对于父节点的偏移,因此,需要明确好转换后的父节点,否则就会出现转换一直不正确的现象9node.parent = spriteA;
10node.position = out;

PART 4

射线检测

在开始说射线检测怎么用之前,先要说说为什么要有射线检测。很多 Creator 2D 同学开发游戏的时候,都知道节点身上有一个 “Size” 属性,代表这个节点的尺寸。点击屏幕的时候,在大多数情况下引擎通过触点与节点的位置和 “Size” 计算是否该节点被点击到。监听方法采用节点事件,在节点被点击到时触发回调,代码如下:

1this.node.on(Node.EventType.TOUCH_MOVE, this._touchMove, this);

但是在 Creator 3.x 3D 节点上使用该方法是行不通的,因为 3D 节点没有所谓的 “Size” 属性,在不同的深度下,物体所呈现的大小是不一样的。因此,物体是否被点击到是无法简单通过位置和 “Size” 计算。在这里就需要使用到射线检测功能。

射线检测简单来说就是可以指定一个起点 A 和一个终点 B,引擎会发射一条由 A 到 B 的射线,收集并返回与射线碰撞到的所有对象各自的位置、法线等信息,这样就能知道点击到的对象是谁,以及它们分别如何处理。Cocos Creator 3.x 提供了 3 种创建射线的方法:

1import { geometry } from 'cc';2const { ray } = geometry;34// 方法 1. 通过起点 + 方向的方式56// 构造一条从(0,-1,0)出发,指向 Y 轴的射线7// 前三个参数是起点,后三个参数是方向8const outRay = new ray(0, -1, 0, 0, 1, 0);9// 或者
10const outRay2 = ray.create(0, -1, 0, 0, 1, 0);
11
12// 方法 2. 通过起点 + 射线上的另一点
13
14// 构造一条从原点出发,指向 Z 轴的射线
15const outRay = new ray();
16geometry.ray.fromPoints(outRay, Vec3.ZERO, Vec3.UNIT_Z);
17
18// 方法 3. 用相机构造一条从相机原点到屏幕某点发射出的射线
19
20// 假设此处已经关联上一个相机
21const cameraCom: Camera;
22const outRay = new ray();
23// 获得一条途径屏幕坐标(0,0)发射出的一条射线
24// 前两个参数是屏幕坐标
25cameraCom.screenPointToRay(0, 0, outRay);

从上述射线创建的方法来看,如果要点击屏幕判断是否点击到某个对象,采用的是方法 3。细心的朋友可能想问,为啥判断点击屏幕是否点击到某个 3D 对象需要用到相机?接下来,我在场景里用两个相机照射同一个胶囊体,大概就能解释清楚这里面的关系。

此时,你就会清楚的发现,即使游戏场景里只有一个胶囊体,但是如果用两个不同拍摄角度的相机照射,是会在画布上看到两个物体 A 的。此时,点击区域 1,胶囊体究竟是否被点击到了?点击区域 2,胶囊体究竟是否被点击到了?根本无法判定。

如果指定相机判断,是不是就明确了许多。用相机 A 的视角判断,点击区域 1,没有点击到胶囊体;用相机 B 的视角判断,点击区域 1,点击到了胶囊体。可以通过下方代码验证:

1import { _decorator, Component, systemEvent, SystemEventType, Touch, EventTouch, Camera, geometry, MeshRenderer } from 'cc';2const { ccclass, property } = _decorator;3const { Ray, intersect } = geometry;4const { rayModel } = intersect;56@ccclass('TestPrint')7export class TestPrint extends Component {8    // 指定相机9    @property(Camera)
10    camera: Camera = null!;
11
12    // 指定模型的渲染组件
13    @property(MeshRenderer)
14    meshRenderer: MeshRenderer= null!;
15
16    onEnable () {
17       systemEvent.on(SystemEventType.TOUCH_START, this._touchStart, this)
18    }
19
20    _touchStart(touch: Touch, event: EventTouch){
21        const location = touch.getLocation();
22        const ray = new Ray();
23        // 创建一条连接触点位置与相机位置的射线
24        this.camera.screenPointToRay(location.x, location.y, ray);
25        // 获取到渲染组件身上用于存储渲染数据的 model 做射线检测
26        const raycast = rayModel(ray, this.meshRenderer.model!);
27        // 返回值只有 0 与 !0,0 就是没检测到
28        if(raycast > 0){
29            console.log('capsule clicked');
30        }
31    }
32}

注:两个相机照射的内容如果都需要在画布上呈现,那么其中一个相机的 ClearFlags 需要为 DEPTH_ONLY。

以上内容就是关于Cocos Creator 3.x 坐标转换有关的全部内容,希望对你有所帮助!

  • Creator H5游戏开发PDF免费下载(800+页)

  • Creator零基础修仙实战

  • 2021年他开发19款插件工具!款款口碑爆棚

  • 为什么能持续成交,我究竟是做对了什么?

  • 这样学Shader可以少走几年弯路,看完我信了……

一文搞懂 Cocos Creator 3.x 坐标转换!建议收藏相关推荐

  1. 一文搞懂 Cocos Creator 3.0 坐标转换原理

    一文搞懂 Cocos Creator 3.0 坐标转换原理 屏幕坐标 UI 触点坐标 UI 多分辨率适配方案 UI 触点获取 不同坐标之间的转换 屏幕坐标与 3D 节点世界坐标互转 3D 节点之间的坐 ...

  2. ❤『知识集锦』一文搞懂mysql索引!!(建议收藏)

    作者:不吃西红柿 简介:CSDN博客专家.蓝桥签约作者.大数据领域优质创作者. 以我的资历和文凭,将来这个城市的大街,都归我扫.   [系列课程介绍] 『面试知识集锦』系列课程包括以下20个系列,超过 ...

  3. 一文读懂联邦学习的前世今生(建议收藏)

    前言 联邦学习(Federated Learning)作为人工智能的一个新分支,为机器学习的新时代打开了大门.如果投票问人工智能和大数据应用领域有什么好玩又好用的新技术,"联邦学习" ...

  4. 案例+图解带你一文读懂Canvas【2W字,建议收藏】

    前言 在早期web端的动画.广告.游戏等基本上都是使用Flash来实现的,要在网页上播放Flash需要一堆代码和插件,因此Flash的使用上比较复杂,还会给开发者带来一堆麻烦. 自从HTML5提供 C ...

  5. 3 万字 + 100 张图带你彻底搞懂 TCP 面试题(强烈建议收藏)

    大家好,我是小林,一个专为大家图解的工具人. 不管面试 Java .C/C++.Python 等开发岗位, TCP 的知识点可以说是必问的了. 任 TCP 虐我千百遍,我仍待 TCP 如初恋. 过去不 ...

  6. 一篇文章搞懂FastDfs(全是干货,建议收藏)

    这里写目录标题 1. 什么是分布式文件系统 2. 为什么要使用分布式文件系统 3. FastDFS 与 HDFS比较 4. 什么是FastDFS 5. 常见术语 6. FastDFS架构 7. Fas ...

  7. 一文搞懂RNN(循环神经网络)

    基础篇|一文搞懂RNN(循环神经网络) https://mp.weixin.qq.com/s/va1gmavl2ZESgnM7biORQg 神经网络基础 神经网络可以当做是能够拟合任意函数的黑盒子,只 ...

  8. 一文搞懂 Python 的 import 机制

    一.前言 希望能够让读者一文搞懂 Python 的 import 机制 1.什么是 import 机制? 通常来讲,在一段 Python 代码中去执行引用另一个模块中的代码,就需要使用 Python ...

  9. python语言语句快的标记是什么_一文搞懂Python程序语句

    原标题:一文搞懂Python程序语句 程序流 Python 程序中常用的基本数据类型,包括: 内置的数值数据类型 Tuple 容器类型 String 容器类型 List 容器类型 自然的顺序是从页面或 ...

最新文章

  1. ActionDescriptor 的认识
  2. 车道线检测--End-to-end Lane Detection through Differentiable Least-Squares Fitting
  3. python数组和列表_Python-01矩阵、数组和列表等的总结
  4. Leetcode 剑指 Offer 40. 最小的k个数 (每日一题 20210825)
  5. Hive SQL基础
  6. 手握价值70万录用书的程序员提离职,领导:你已升职成功,还走?
  7. Lync Server的环境搭建(五):Lync-Server的安装部署
  8. java递归统计一个文件夹含子文件夹里文件不同后缀的出现次数
  9. php ci框架 实例化类,php框架CI(codeigniter)自动加载与自主创建对象操作实例分析...
  10. 谈谈软件工程设计的艺术
  11. squid via检测转发循环
  12. 【前沿方案】华为自动驾驶网络解决方案.pdf(附80页pdf下载链接)
  13. python textrank_TextRank算法提取文本摘要
  14. [zt]OpenCV2.1.0的安装
  15. C/C++链接过程相关
  16. 跑路了,在国外当程序员有多爽?
  17. 七年级计算机上教学计划,新人教版七年级数学下册教学计划(精选5篇)
  18. 基于java 网页的宠物店管理系统
  19. 【CityHunter】服务器端设计思路
  20. 解决1 error and 0 warnings potentially fixable with the `--fix` option.

热门文章

  1. 记录机器学习练习中不懂的函数
  2. VTK笔记-图形相关-多边形数据转换图像数据-vtkPolyData转换为vtkImageData
  3. 狄利克雷卷积学习记录
  4. Python爬虫学习笔记:概念、知识和简单应用
  5. STM32设置USB HID模式
  6. UR5双臂Gazebo仿真(Python)
  7. 小程序 uni canvas绘制圆角图片 圆角矩形
  8. 在cmd命令下imp oracle dmp文件
  9. The superclass javax.servlet.http.HttpServlet was not found on the Java Build Path类似问题简单解决方案
  10. 蜗牛星际安装winserver 2012的网卡驱动