Cocos 向量应用
参考
【分享】Cocos Creator 向量基础及其使用
Cocos Creator 向量基础及其使用
https://www.bilibili.com/video/BV1X7411F744?p=2
一、向量归一化
向量的归一化表示得到一个方向和向量相同的向量, 但是向量的模(向量的长度)为 1 。归一化后的向量,也被叫作单位向量。
二、向量点乘
向量点乘公式:
image.png
根据点乘公式,我们知道,向量点乘是一个数,那么这个数在图形学上的几何意义是什么呢?
1.计算两个向量之间的夹角
如果将向量a和向量b进行归一化,那么可以推导出
image.png
我们就可以知道两个向量之间的夹角(角度)
vec3 a;
vec3 b;
float c = dot(normalize(a), normalize(b));
2.判断两个向量前后(方向)
观察cosθ的曲线,可知在0到90度,270到360度时其值为正。
image.png
以上图为例,如果向量a为玩家的正方向。那么虚线上方的点乘值大于0,即为同方向,也可以理解为前方。
image.png以上图为例,如果AB在X轴上,那么点乘可以帮我们知道点C会落在Y轴的左侧还是右侧。这个也很好理解,之前的例子向量a是冲着y轴正方向,这个例子变成冲着x轴正方向而已。
这里顺便说一下叉积,因为是sinθ,此时根据sinθ曲线,0到180度是大于0,所以可以帮我们判断点C位于X轴的上方还是下方。
利用这个几何意义,可以实现:Creator3D:shader8_这种shader怎样配标题 3D内发光
3.计算向量投影
image.png得到投影后,还可以在进一步分解向量
image.png
投影的一个典型应用在对OBB包围盒进行碰撞检测的时候,经常会使用 分离轴定理SAT(Separating Axis Theorem) 进行检测
分离轴定理:通过判断任意两个矩形在任意角度下的投影是否均存在重叠,来判断是否发生碰撞。若在某一角度光源下,两物体的投影存在间隙,则为不碰撞,否则为发生碰撞。
计算投影就可以用到向量点乘了
详细可以参考 碰撞检测的向量实现
4.计算投影点,点到线段的距离
已知A,B,C三点坐标,求出C在AB上的投影坐标O。这样就能求点C到AB的距离CO。
AB点乘AC=|AB||AC|cosθ,很容易求得AC在AB方向 的投影长度|AO|。将向量AB归一化之后,再乘以距离|AO|,然后加上A点的坐标,即可得到O的坐标。
扩展一下,求C相对AB的镜面坐标,其实求到O的坐标后,得到向量CO,然后再加上O的坐标,即可得到结果。即2O-C。
三、向量叉乘
1.力矩与力偶的概念
从实践中知道,力除了能使物体移动外,还能使物体转动。例如用扳手拧螺母时,加力可使扳手绕螺母中心转动;拉门把手拽门,可把门打开,也是加力使门产生转动效应的实例。
那么力使物体产生转动效应与哪些因素有关呢?力F使扳手绕螺母中心O转动的效应,不仅与力的大小成正比,而且还与螺母中心到该力作用线的垂直距离d成正比。因此可用两者的乘积F·d来度量力F对扳手的转动效应。
image.png
转动中心O,称为矩心,矩心到力作用线的垂直距离d称为力臂,矩心和力的作用线所决定的平面称为力矩作用面,过矩心与此平面垂直的直线称为该力矩使物体转动的轴线。
因此,我们用力的大小与力臂的乘积F·d再加上正号或负号来表示力F使物体绕点转动的效应如图所示,称为力F对O点的矩,简称力矩,用符号Mo(F)或Mo表示。
一般规定:顺着转轴看力矩作用面使物体产生逆时针方向转动的力矩为正;反之,为负
2.知乎 力矩到底是一种什么样的矢量,为什么他的方向这么奇怪?
当物体在旋转时,旋转的方向一直在变,我们只能描述成是顺势针或者逆时针,而不能在旋转的平面里找到一个方向固定的向量来描述旋转的方向,所以物理上用垂直于旋转平面的方向来表示旋转方向。垂直于平面有两种可能的方向,以此分别表示逆时针和顺时针。这么奇葩大概是为了数据表达的简洁性和准确性吧
如果力和力臂都是在纸面上,那么使杠杆逆时针转动的,力矩的方向垂直于纸面指向外,如果力矩使杠杆顺时针转动,方向是垂直于纸面指向内
3.「力矩」是什么?
请注意我的措辞,力矩是来描述力对物体的转动作用,是描述作用的物理量,就是说转的厉不厉害
M=FrsinωF
力越大转的越厉害r离轴越远,转的越厉害。。。。。补充。。。。。。
叉积之后的方向应该是垂直F和L的,这怎么理解呢~为了判定转动方向。如果力矩方向在F,l所处平面上,你会发现转动一周力矩会指向平面上各个方向,没有办法选取正方向。如果垂直于F,l平面,就可以轻松的选定正方向,判定顺逆。
4.点积和叉乘的出现背景是什么?
image.pngimage.png
image.png
5.回归本源系列1.3-向量叉乘
image.png将右手四个手指指向u的方向,朝着v的方向弯曲。注意,谁在乘号前,四指先经过谁。
我们一般使用的是右手参照系。这种定义方法并不是一定的,比如在Unity中就是采用左手参照系。
6.计算法线向量
向量a和向量b的叉乘得到一个垂直于这个平面的向量,这个向量也叫法向量
7.判断向量的左右
image.png假设向量a ,向量 b 都在 xy 的二维平面上,并假设a x b = c 。那么
image.png
因为二维平面上,向量 \ a 和向量 b 的 z 肯定为 0,所以
image.png
根据右手螺旋定则, a x b 表示,法向量 c 是绕向量 a b 所在平面旋转得到的,这里可以定义
- c 的 z 值为正,则表示向量 a 在向量 b 的 右侧
- c 的 z 值为负,则表示向量 a 在向量 b 的 左侧
8.判断点在多边形内部还是外部
image.png以上图为例,在刚才左右的基础上,如果
- 向量{AP} 在向量{AB} 的左边
- 向量{BP} 在向量{BC} 的左边
- 向量{CP} 在向量{CA} 的左边
那么,点P 在三角线 ABC 内。这样子通过叉乘就可以知道点是否在三角形内/外,这也是光栅化的基础,判断点是否在三角形内。
更进一步,我们还可以通过向量叉乘来判断点是否在多边形内。比如:Cocos Creator 提供的 cc.Intersection.pointInPolygon 方法,其内部原理是通过向量叉乘来判断点是否在多边形内
image.png
SVG 的填充属性 fill-rule: evenodd(奇偶填充) 和 nonzero(非零填充) ,其内部实现 我猜 应该也是可以通过向量叉乘来解决
9.画多边形
既然知道了向量叉乘可以判断点是否在多边形内外,那么我们也可以根据这个几何意义去画任意多边形。以六边形为例:
image.png
/**
* 画六边形
* @param center 中心点
* @param side 六边形边长
* @param color 六边形颜色
*/
vec4 drawHex(vec2 center, float side, vec4 color) {
// 将uv往六边形中心点偏移,实现偏移后的坐标系原点在纹理中心,x 向右 y 向下
// 并转换为我们需要判断的点
vec2 uv = v_uv0.xy - center;
vec3 p = vec3(uv, 0.0);
// 计算六边形的六个顶点
float c = cos(radians(60.0));
float s = sin(radians(60.0));
vec3 p0 = vec3(side, 0.0, 0.0);
vec3 p1 = vec3(side * c, -side * s, 0.0);
vec3 p2 = vec3(-side * c, -side * s, 0.0);
vec3 p3 = vec3(-side, 0.0, 0.0);
vec3 p4 = vec3(-side * c, side * s, 0.0);
vec3 p5 = vec3(side * c, side * s, 0.0);
// 计算当前点是否在六边形内(通过向量叉乘)
float r0 = step(0.0, cross(p-p0, p1-p0).z);
float r1 = step(0.0, cross(p-p1, p2-p1).z);
float r2 = step(0.0, cross(p-p2, p3-p2).z);
float r3 = step(0.0, cross(p-p3, p4-p3).z);
float r4 = step(0.0, cross(p-p4, p5-p4).z);
float r5 = step(0.0, cross(p-p5, p0-p5).z);
// 如果在内部,inside = 1.0,否则 inside = 0.0
float inside = r0 * r1 * r2 * r3 * r4 * r5;
return vec4(color.rgb, color.a * inside);
}
void main() {
// ... 其他代码
gl_FragColor = drawHex(vec2(0.5, 0.5), 0.5, o);
}
四、cocos 向量转换为夹角signAngle
signAngle(other: Vec2): number
Defined in cocos/core/math/vec2.ts:774
获取当前向量和指定向量之间的有符号角度。
有符号角度的取值范围为 (-180, 180],当前向量可以通过逆时针旋转有符号角度与指定向量同向。
1.Cocos creator中需要掌握哪些数学物理知识
在游戏中,有时候我们需要计算物体旋转了多少度,比如我们要模拟一个KTV里面的游戏转盘,手指触摸的时候,要获取触摸的点的位置,触摸点的位置与转盘中心点之间构成一个向量,我们要获取转动了多少度,就需要有一个参考向量。
如下图所示,假如蓝色的圆就是转盘的边缘,我们最开始触摸A点,然后转动到B点,这时候转动的角度就是∠AOB,这个角度在Cocos中如何求呢?
代码pos就表示B点的坐标,pos和原点(0,0)相减,得到向量OB,也即代码中的dirVec,弧AB为radian,转换为角度degree。
image.png// 向量转换为角度
vectorsToDegree(dirVec) {
// 水平向右的对比向量
let comVec = cc.v2(1, 0);
// 求方向向量与对比向量间的弧度
let radian = dirVec.signAngle(comVec);
// 将弧度转换为角度
let degree = cc.misc.radiansToDegrees(radian);
return degree;
},
调用示例(具体可以参考我们的《KTV酒令转盘虚拟仿真实现》):
// 获取触摸点与原点连线的射线与X轴正向的角度
getTouchAngle(e){
var screen_pos = e.getLocation(); // 触摸点世界坐标
// 转换为相对于当前节点的锚点的坐标
var pos = this.node.convertToNodeSpaceAR(screen_pos);
// 获取触摸点距离轮盘中心点的向量
var dirVec = pos.sub(cc.v2(0,0));
// 将向量转换为基于参考方向(v2(0,1))的角度
return this.vectorsToDegree(dirVec);
},
2.CocosCreator计算夹角
export default class TestAngle extends cc.Component {
@property(cc.Node)
root: cc.Node;
@property(cc.Graphics)
graph: cc.Graphics;
@property(cc.Node)
target: cc.Node;
@property(cc.Label)
angleDes: cc.Label;
start() {
this.node.on(cc.Node.EventType.TOUCH_MOVE,this.onTouchMove,this);
}
onTouchMove(event: cc.Event.EventTouch): void {
let startPos: cc.Vec2 = this.target.convertToWorldSpaceAR(new cc.Vec2(0, 0));
let endPos: cc.Vec2 = event.getLocation();
let dirVec = endPos.sub(startPos);//获得从startPos指向endPos的方向向量
let comVec = new cc.Vec2(1, 0);//计算夹角的参考方向,这里选择x轴正方向
let radian = dirVec.signAngle(comVec);//获得带方向的夹角弧度值(参考方向顺时针为正值,逆时针为负值)
let degree = Math.floor(cc.misc.radiansToDegrees(radian));
console.log("x角度:" + degree)
this.angleDes.node.position = this.root.convertToNodeSpaceAR(endPos);
this.angleDes.string = degree + "°";
let comEnd = new cc.Vec2(startPos.x + 200, startPos.y);
this.drawAngle(startPos, endPos, comEnd);
}
/**
*
* @param startPos 起始点
* @param endPos 终点(对应鼠标点击的点)
* @param comEnd 参照向量的终点
*/
private drawAngle(startPos: cc.Vec2, endPos: cc.Vec2, comEnd: cc.Vec2) {
this.graph.clear();
this.graph.lineWidth = 1;
this.graph.strokeColor = cc.Color.WHITE;
this.graph.moveTo(startPos.x, startPos.y);
this.graph.lineTo(endPos.x, endPos.y);
this.graph.stroke();
this.graph.strokeColor = cc.Color.GREEN;
this.graph.moveTo(startPos.x, startPos.y);
this.graph.lineTo(comEnd.x, comEnd.y);
this.graph.stroke();
}
}
五、麒麟子Cocos Creator实用技巧七:方向与角度转换
本DEMO完整地址:https://gitee.com/qilinzi/qlz_ccc_tips 目录 base/assets/07_rotationtodir
1.让一个对象朝着他的方向移动
cc.Node节点的rotation是一个角度,因此,我们为了实现这个目标,首先要根据rotation求出他的dir方向,就好办了。示例代码
update(dt){
//由于Math函数接受的是孤度,所以我们先节节点的旋转转化为弧度
var angle = this.node.rotation / 180 * Math.PI;
//合成基于 X正方向的方向向量
var dir = cc.v2(Math.cos(angle),Math.sin(angle));
//单位化向量
dir.normalizeSelf();
//根据方向向量移动位置
var moveSpeed = 100;
this.node.x += dt * dir.x * moveSpeed;
this.node.y += dt * dir.y * moveSpeed;
}
2.让一个对象朝向另一个对象
通过高中数学我们可以知道, A到B的向量 = B点 - A点。 那我们只需要将目标对象的位置 - 自己的位置,即可得到方向向量。
方向向量转换为角度,需要认清一个隐含变量,就是这个角度的基准是 X 轴正方向。 使用向量来表示就是 (1,0)。
cc.Vec2提供了两个函数 cc.Vec2.angle和cc.Vec2.signAngle, 后者相比前者来说,后者产生的角度是有符号的,而前者会永远为正。 我们使用cc.Vec2.signAngle来将方向向量转换为弧度。 具体操作请看代码。
function lookAtObj(target){
//计算出朝向
var dx = target.x - this.node.x;
var dy = target.y - this.node.y;
var dir = cc.v2(dx,dy);
//根据朝向计算出夹角弧度
var angle = dir.signAngle(cc.v2(1,0));
//将弧度转换为欧拉角
var degree = angle / Math.PI * 180;
//赋值给节点
this.node.rotation = degree;
}
六、反复横跳的瞄准线! 从向量计算说起!基于射线检测的实现!
ba8f515c5cc87c25434717b508c1fe02f54a0a3d.gif已知入射向量(单位向量),法向量(单位向量),如何得出反射向量?
image.png
我们将反射向量平移至入射向量起点,延长法向量与其相交,这个延长线的长度,刚好是 入射向量在法向量上的投影的相反数的两倍 。再根据投影和向量加法可以推出反射向量的计算公式。
// author : lamyoung
const AIM_LINE_MAX_LENGTH = 1440;
const { ccclass, property } = cc._decorator;
@ccclass
export default class Main extends cc.Component {
@property({ type: cc.Graphics, tooltip: '瞄准线作图' })
graphic_line: cc.Graphics = null;
onLoad() {
cc.director.getPhysicsManager().enabled = true;
// cc.director.getPhysicsManager().debugDrawFlags = 1;
this.graphic_line.node.on(cc.Node.EventType.TOUCH_START, this.onTouchStart, this);
this.graphic_line.node.on(cc.Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
this.graphic_line.node.on(cc.Node.EventType.TOUCH_END, this.onTouchEnd, this);
this.graphic_line.node.on(cc.Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this);
}
private onTouchStart(touch: cc.Event.EventTouch) {
this.graphic_line.clear();
}
private _cur_length: number = 0;
private onTouchMove(touch: cc.Event.EventTouch) {
this.graphic_line.clear();
this._cur_length = 0;
const startLocation = touch.getStartLocation();
const location = touch.getLocation();
// 计算射线
this.drawRayCast(startLocation, location.subSelf(startLocation).normalizeSelf());
this.graphic_line.stroke();
}
private onTouchEnd(touch: cc.Event.EventTouch) {
this.graphic_line.clear();
}
/**
* @description 计算射线
* @param startLocation 起始位置 世界坐标系
* @param vector_dir 单位方向向量
*/
private drawRayCast(startLocation: cc.Vec2, vector_dir: cc.Vec2) {
// 剩余长度
const left_length = AIM_LINE_MAX_LENGTH - this._cur_length;
if (left_length <= 0) return;
// 计算线的终点位置
const endLocation = startLocation.add(vector_dir.mul(left_length));
// 射线测试
// 检测给定的线段穿过哪些碰撞体,可以获取到碰撞体在线段穿过碰撞体的那个点的法线向量和其他一些有用的信息。
const results = cc.director.getPhysicsManager().rayCast(startLocation, endLocation, cc.RayCastType.Closest);
if (results.length > 0) {
const result = results[0];
// 指定射线与穿过的碰撞体在哪一点相交。
const point = result.point;
// 画入射线段
this.drawAimLine(startLocation, point);
// 计算长度
const line_length = point.sub(startLocation).mag();
// 计算已画长度
this._cur_length += line_length;
// 指定碰撞体在相交点的表面的法线单位向量。
const vector_n = result.normal;
// 入射单位向量
const vector_i = vector_dir;
// 反射单位向量
const vector_r = vector_i.sub(vector_n.mul(2 * vector_i.dot(vector_n)));
// 接着计算下一段
this.drawRayCast(point, vector_r);
} else {
// 画剩余线段
this.drawAimLine(startLocation, endLocation);
}
}
/**
* @description 画瞄准线
* @param startLocation 起始位置 世界坐标系
* @param endLocation 结束位置 世界坐标系
*/
private drawAimLine(startLocation: cc.Vec2, endLocation: cc.Vec2) {
// 转换坐标
const graphic_startLocation = this.graphic_line.node.convertToNodeSpaceAR(startLocation);
this.graphic_line.moveTo(graphic_startLocation.x, graphic_startLocation.y);
// 画小圆圆
// 间隔
const delta = 20;
// 方向
const vector_dir = endLocation.sub(startLocation);
// 数量
const total_count = Math.round(vector_dir.mag() / delta);
// 每次间隔向量
vector_dir.normalizeSelf().mulSelf(delta);
for (let index = 0; index < total_count; index++) {
graphic_startLocation.addSelf(vector_dir)
this.graphic_line.circle(graphic_startLocation.x, graphic_startLocation.y, 2);
}
}
}
// 欢迎关注【白玉无冰】公众号
七、三维向量
在COCOS 3.x版本中,使用Vec3向量
1.向量长度
let t2 = v3(2, 2, 2).subtract(v3(0, 0, 0));
console.log("len:", t2.length(), "len2:", Vec3.len(t2));
上述代码打印出来的值是根号12,即三个维度的平方和,再开根号
let l = v3Points[1].clone().subtract(v3Points[0]).length();
let l2 = Vec3.len(v3Points[1].subtract(v3Points[0]));
console.log("p0:", v3Points[0], "p1:", v3Points[1], "len:", l, "l2:", l2);
这里展示的是clone方法的用处,如果不使用clone,第一次subtract改变了原始向量,导致两次取长度结果不一致。