Three.js中归一化坐标的通用计算公式

3,368 阅读6分钟

写作初衷

        这其实是我很久之前学习three.js时的一篇笔记,当时被【归一化坐标】这个概念搞得头晕脑胀,想着干脆在网上找个通用公式直接拿来用算了,但是网上找到的公式都有一个隐式条件:假设three.js对应的canvas元素是在屏幕左上角的。大概大佬们对于一些简单的概念不屑于深入讲解吧,这就苦了我这种菜鸟,被这个折腾了好久。后来真正理解了这是个什么东西,也就有了本文提到的【通用计算公式】,有多通用不好说,但是大多数时候应该是管用的。

归一化坐标

        归一化坐标,是一个二维坐标,仅有X/Y两个维度,且X和Y的取值范围均为[-1, 1],坐标原点位于three.js所创建的canvas的中心处。
        关于【归一化坐标】的具体概念,可以去百度搜索,我这里说一下我自己的一些见解,主要是作为一个使用者,应该如何去看待这个东西。疏漏之处,还请指正。
        要了解归一化坐标,有这么两个前置概念:屏幕坐标三维坐标
        其中,屏幕坐标指的就是物理屏幕上是坐标,三维坐标指的则是three.js所创建的这个逻辑上的三维世界中的坐标。屏幕坐标以【像素(px)】为单位,三维坐标以三维世界中的【单位1】为单位。屏幕坐标的取值范围是屏幕是像素数(X对应横向长度,y对应纵向长度),三维坐标的取值范围的无穷(z/y/z三个轴均如此)。屏幕坐标的坐标系原点在屏幕的左上角(页面的左上角),三维坐标的坐标系原点在三维世界的中心。
        现在,我有一个屏幕坐标,使用起来很简单;我也可以创建一个三维坐标,使用起来也很简单。但是,如果我想要把一个屏幕坐标转换为三维坐标使用呢?这时,就要使用一个中间值了,这个中间值,也就是这个【归一化坐标】。也就是说,作为一个使用者,可以将归一化坐标当做是:屏幕坐标三维坐标进行相互转换时的一个中间坐标。在使用上来说,归一化坐标实际使用中的意义,就在这里了。此外,归一化坐标在图形学中还有个正式的名字:NDC空间坐标,也叫标准化设备坐标,有兴趣可以再额外进行了解。

公式:

首先,先把公式摆出来,以点击事件触发的屏幕坐标为演示。

/**
 * @mouse 归一化坐标,一个二维坐标
 * @event 点击事件中的事件对象
 * @container 渲染器的包裹父元素
 */
var mouse = new Three.Vector2();
// 则有公式如下:
mouse.x = ((event.clientX - container.getBoundingClientRect().left) / container.getBoundingClientRect().width) * 2 - 1;
mouse.y = - ((event.clientY - container.getBoundingClientRect().top) / container.getBoundingClientRect().height) * 2 + 1;

附加:如何从归一化坐标转化为屏幕坐标(x, y)?

反转上述过程即可(这里没有再附加当canvas不是全屏时的转化,可以根据上面的公式按需推倒):

/**
 * @coord 要转换为屏幕坐标的归一化坐标
 */
 屏幕坐标x = (window.innerWidth / 2) * coord.x + window.innerWidth / 2;
 屏幕坐标y = (-window.innerHeight / 2) * coord.y + window.innerHeight / 2;

推导过程:

设定:屏幕宽度为w,高度为h
已知:A点的屏幕坐标为(x, y),归一化坐标轴所在矩形(也就是three.js所创建的canvas元素)距离屏幕上方距离为t,距离屏幕左侧距离为l
求得:A点的归一化坐标 (a, b)

        所谓归一化坐标,可以理解为在屏幕中的某个矩形中建立一个平面直角坐标系(可以称为归一化坐标系),右为此坐标系x轴正方向,上为此坐标系y轴正方向,且x轴取值范围为[-1,1],y轴取值范围为[-1,1],那么矩形内任意一点的坐标都可以用一个屏幕坐标的百分比来表示,百分比的计算为:
坐标x = (x-l)/w - 0.5
坐标y = (y-t)/h - 0.5
        分析:此式中x-l表示矩形内点距离矩形左侧边缘的距离,则(x-l)/w可表示所点击的点距离屏幕左边缘的距离所占矩形宽度的百分比,此时得到一个值域在[0, 1]的数字,再减去0.5则可将值域限制在[-0.5, 0.5],同理推得坐标y = (y-t)/h - 0.5。此时可以得到一个坐标的x,y值域均在[-0.5, 0.5]的坐标,这时将值*2则可将值域拓展到[-1, 1],则此时的计算公式也变为:坐标x = (x-l)/w * 2 - 1 坐标y = (y-t)/h * 2 - 1
        最后,考虑y轴,屏幕坐标轴的y轴轴方向与矩形内归一化坐标系y轴的轴方向相反(无需考虑x轴就是因为屏幕坐标轴的x轴与归一化坐标轴的x轴方向相同),而此前所有计算中y值均为屏幕坐标轴的y值,是故将点从屏幕坐标轴转到归一化坐标轴时,应当对y值进行取反,即坐标y = -[(y-t)/h * 2 - 1],去括号,将负号参与计算,得坐标y = 1 - (y-t)/h * 2
        至此,得出公式:

坐标x = (x-l)/w * 2 - 1
坐标y = 1 - (y-t)/h * 2

x、y对应为event.clientXevent.clientY
w、h对应为 container.getBoundingClientRect().widthcontainer.getBoundingClientRect().height
l、t 对应为 container.getBoundingClientRect().leftcontainer.getBoundingClientRect().top

        至此,得出上面所说的通用公式。
        其实这个所谓的【通用公式】,其实也就是比网上的那些公式多了一步减法而已,x/y分别减去了canvas元素到屏幕(网页)左边缘的距离和到屏幕(网页)上边缘的距离。但是不理解【归一化坐标】的具体含义,想要想到这一步减去还是很难的。

结尾

文章虽然不长,但是新手写作,难免有疏漏的地方,如果有哪里考虑的不周到了,或者表述的不清楚了,希望诸位读者不吝斧正。
还有,掘金的这个Markdown编辑器最让我觉得难受的一点是:作为一个文本编辑器,居然没有对ctrl+s这个快捷键进行捕捉!也就是说每次按到这个快捷键都会执行【保存网页】的操作,弹出【保存网页】的弹窗!作为一个写笔记和写代码都很喜欢随手ctrl+s的快捷键爱好者,表示很难受!啊,敲完这句话我又点了一下crtl+s,又是一个弹窗...你说你都自动保存了,捕获一下常用的快捷键不就是随手的事么ヽ(ー_ー)ノ