前言
Laya目前为止还没有支持LineRenderer这个组件,在做相关特效的时候不是很方便,本文主要讲解如何仿照Unity在Laya上实现一个基础版的LineRenderer。
提示:以下是本篇文章正文内容,下面案例可供参考
一、目标
Unity场景中包含的LineRenderer导出到Laya后,Laya可自动创建LineSprite3D对象(需要修改引擎),就像MeshSprite3D一样,再使用LineSprite3D的api设置起点和终点,产生连线特效。
二、原理
简单来说,就是把一个带uv动画的Plane拉伸成连接两点的一个平面,通过位移、拉伸和旋转达到想要的效果。我们只需要构建一个新的矩阵,然后使用新的矩阵替换原来的世界矩阵。我们给出两个点 P 1 P_{1} P1 和 P 2 P_{2} P2 ,即为要连接的两个点。
1.构建Plane
构建一个1x1的Plane,并使用uv动画播放特效(uv动画本文不做解释)
2.构建转换矩阵
这个步骤又可详细分为以下几步:
-
1、把Plane移动到 P 1 P_{1} P1和 P 1 P_{1} P1的中点,即Plane的位移为:
d x = ( P 1 . x + P 2 . x ) / 2 dx=(P_{1}.x+P_{2}.x)/2 dx=(P1.x+P2.x)/2 d y = ( P 1 . y + P 2 . y ) / 2 dy=(P_{1}.y+P_{2}.y)/2 dy=(P1.y+P2.y)/2 d z = ( P 1 . z + P 2 . z ) / 2 dz=(P_{1}.z+P_{2}.z)/2 dz=(P1.z+P2.z)/2 -
2、 跟据上图,可知需要拉伸的是x轴,Plane的x轴拉伸的长度 = 两个点的距离。
s c a l e X = d i s t a n c e ( P 1 , P 2 ) scaleX=distance(P_{1},P_{2}) scaleX=distance(P1,P2) -
3、接下来求新的xyz轴基向量。
Plane的x轴经过转换后必须指向 P 2 P_{2} P2,即和 P 1 P_{1} P1到 P 2 P_{2} P2的方向一致,我们称该方向为 r i g h t right right,于是
r i g h t = n o r m a l i z e ( P 2 − P 1 ) right=normalize(P_{2}-P_{1}) right=normalize(P2−P1)
z轴转换后的向量起个名字叫 f o r w a r d forward forward, f o r w a r d forward forward必须垂直于 r i g h t right right,同时为了使Plane尽可能的面向摄像机方向, f o r w a r d forward forward就必须要垂直于摄像机位置到Plane的向量(给个名字就叫 c a m e r a D i r cameraDir cameraDir吧),因此, r i g h t right right叉乘 c a m e r a D i r cameraDir cameraDir就得到 f o r w a r d forward forward
c a m e r a D i r = 摄 像 机 坐 标 − P 1 P 2 中 点 cameraDir=摄像机坐标-P_{1}P_{2}中点 cameraDir=摄像机坐标−P1P2中点 f o r w a r d = n o r m a l i z e ( r i g h t × c a m e r a D i r ) forward=normalize(right×cameraDir) forward=normalize(right×cameraDir)
y轴转换后的向量也起个名字叫 u p up up, u p up up必须同时垂直于 f o r w a r d forward forward和 r i g h t right right,所以:
u p = r i g h t × f o r w a r d up=right×forward up=right×forward
可得矩阵
[ d i s t a n c e ⋅ r i g h t d x u p d y f o r w a r d d z 0 1 ] \left[ \begin{matrix} distance·right & dx\\ up & dy \\ forward & dz \\ 0 & 1 \end{matrix} \right] ⎣⎢⎢⎡distance⋅rightupforward0dxdydz1⎦⎥⎥⎤
三、实现
- LayaAir版本:2.10.0_beta(因为需要修改引擎,所以文中使用了ts实验版,本文最下方有经过修改的2.7.0版本,可直接使用)
- Unity2018.4.7f1
- LayaAirUnityPlugin为LayaAir引擎对应的版本。
虽然Laya还未实现LineRenderer,但如果Unity场景中包含了LineRenderer,并使用LayaAirUnityPlugin插件导出资源时,资源文件就会包含LineRenderer的数据,我们只需读取这些数据就可以在Laya中生成与Unity中一样的LineRenderer。
先看在Unity中LineRenderer的设置:
我们先分析导出的数据,以下红框是.ls文件中的LineRenderer:
Laya对包含了LinerRender的物体(在Unity中该物体名字是“Line”)识别为LineSprite3D类型,我们就使用LineSprite3D这个名字实现一个Sprite类。上图中,LineSprite3D的props属性还包含了许多信息,但接下来我只使用widthMultiplier,即宽度,其它的暂不实现。
LineSprite3D.ts
import { Vector3 } from "../math/Vector3";
import { Laya } from "Laya";
import { BaseCamera } from "./BaseCamera";
import { Transform3D } from "./Transform3D";
import { Node } from "../../display/Node";
import LineRenderer from "./LineRenderer";
import { MeshSprite3D } from "./MeshSprite3D";
import { PrimitiveMesh } from "../resource/models/PrimitiveMesh";
export default class LineSprite3D extends MeshSprite3D {
private _delta: number;
private _interval: number;
private _widthMultiplier: number = 1;
private _target1: Transform3D;
private _target2: Transform3D;
private _startPoint = new Vector3();
private _endPoint = new Vector3();
private _middlePos = new Vector3();
private _right = new Vector3();
private _forward = new Vector3();
private _up = new Vector3();
private _cameraDir = new Vector3();
private _distance = 0;
private _wordMatrixElement: Float32Array;
constructor() {
super(PrimitiveMesh.createPlane(1, 1));
this._delta = 0;
this._interval = 50;
this._render = new LineRenderer(this);
/* 为_render赋值之后,需要重新设置meshFilter.sharedMesh,这样才能触发为新的render生成RenderElement
具体才考meshFilter.sharedMesh的定义*/
let mesh = this.meshFilter.sharedMesh;
this.meshFilter.sharedMesh = null;
this.meshFilter.sharedMesh = mesh;
}
_parse(data: any, spriteMap: any): void {
super._parse(data, spriteMap);
this._widthMultiplier = data.widthMultiplier;
this._startPoint = new Vector3(data.positions.values[0][0], data.positions.values[0][1], data.positions.values[0][2]);
this._endPoint = new Vector3(data.positions.values[1][0], data.positions.values[1][1], data.positions.values[1][2]);
}
_cloneTo(destObject: any, rootSprite: Node, dstSprite: Node): void {
destObject._widthMultiplier = this._widthMultiplier;
super._cloneTo(destObject, rootSprite, dstSprite);
}
/**
* 设置起点位置
*/
setStartPoint(startPoint: Vector3): void {
if (Vector3.distanceSquared(startPoint, this._startPoint) > 1e-6) {
startPoint.cloneTo(this._startPoint);
this._delta = this._interval;
}
};
/**
* 设置终点位置
*/
setEndPoint(endPoint: Vector3): void {
if (Vector3.distanceSquared(endPoint, this._endPoint) > 1e-6) {
endPoint.cloneTo(this._endPoint);
this._delta = this._interval;
}
};
/**
* 连线自动跟随两个目标移动
*/
setTargets(target1: Transform3D, target2: Transform3D): void {
this._target1 = target1;
this._target2 = target2;
}
/**
* 根据起点,终点,宽,计算正方形的位置,缩放,旋转
*/
_updateShape(camera: BaseCamera): void {
this._delta += Laya.timer.delta;
if (this._delta >= this._interval) {
this._delta -= this._interval;
var m = this._render.material as any;
m.tilingOffsetW += 0.25;
if (m.tilingOffsetW >= 1) m.tilingOffsetW = 0;
}
if (this._target1 && this._target2) {
this._startPoint = this._target1.position;
this._endPoint = this._target2.position;
}
//计算两个物体间的距离,即LineSprite3D x轴的缩放大小
this._distance = Vector3.distance(this._endPoint, this._startPoint);
if (this._distance == 0) {
this.transform.localScale.setValue(0, 0, 0);
this.transform.localScale = this.transform.localScale;
return;
}
//计算两个物体的中点位置,即LineSprite3D的位置
Vector3.lerp(this._startPoint, this._endPoint, 0.5, this._middlePos);
//计算right向量,即新的x轴基向量
Vector3.subtract(this._endPoint, this._startPoint, this._right);
Vector3.normalize(this._right, this._right);
//计算forward方向,即新的z轴基向量
Vector3.subtract(camera.transform.position, this._middlePos, this._cameraDir); //从LineSprite3D指向摄像机的向量
// camera.transform.getForward(this._cameraDir);
// Vector3.subtract(Vector3._ZERO, this._cameraDir, this._cameraDir);
Vector3.cross(this._right, this._cameraDir, this._forward); //使用叉乘得到forward方向
Vector3.normalize(this._forward, this._forward);
//计算up向量,即新的y轴基向量
Vector3.cross(this._forward, this._right, this._up);
//开始构建矩阵
this._wordMatrixElement = this.transform.worldMatrix.elements;
//位移
this._wordMatrixElement[12] = this._middlePos.x;
this._wordMatrixElement[13] = this._middlePos.y;
this._wordMatrixElement[14] = this._middlePos.z;
this._wordMatrixElement[15] = 1;
//新的x基向量
this._wordMatrixElement[0] = this._right.x * this._distance;
this._wordMatrixElement[1] = this._right.y * this._distance;
this._wordMatrixElement[2] = this._right.z * this._distance;
this._wordMatrixElement[3] = 0;
//新的y基向量
this._wordMatrixElement[4] = this._up.x;
this._wordMatrixElement[5] = this._up.y;
this._wordMatrixElement[6] = this._up.z;
this._wordMatrixElement[7] = 0;
//新的z基向量
this._wordMatrixElement[8] = this._forward.x * this._widthMultiplier;
this._wordMatrixElement[9] = this._forward.y * this._widthMultiplier;
this._wordMatrixElement[10] = this._forward.z * this._widthMultiplier;
this._wordMatrixElement[11] = 0;
};
get widthMultiplier(): number {
return this._widthMultiplier;
}
set widthMultiplier(val: number) {
this._widthMultiplier = val;
}
/**
* @internal
*/
protected _create(): Node {
return new LineSprite3D();
}
}
LineSprite3D继承MeshSprite3D,这样继承的好处是,只需要传一个1x1大小的Plane,MeshSprite3D原有的方法就会帮我们做渲染,而我们只需要在LineSprite3D中计算新的矩阵即可。
LineRenderer.ts
import { Transform3D } from "./Transform3D";
import { RenderContext3D } from "./render/RenderContext3D";
import { MeshRenderer } from "./MeshRenderer";
import LineSprite3D from "./LineSprite3D";
export default class LineRenderer extends MeshRenderer {
_renderUpdate(context: RenderContext3D, transform: Transform3D): void {
super._renderUpdate(context, transform);
(this._owner as LineSprite3D)._updateShape(context.camera);
}
}
LineRenderer继承MeshRenderer,作用是为计算矩阵提供渲染状态(这里只用到了摄像机状态)
为Laya引擎增加LineSprite3D
- 在Laya3D.ts的_getSprite3DHierarchyInnerUrls方法中加入LineSprite3D
- 在Scene3DUtils.ts的_createSprite3DInstance方法中加入
- 在Scene3DUtils.ts的_createNodeByJson方法中加入
使用
var g1 = this.view.getChildByName("Sphere 1") as Sprite3D;
var g2 = this.view.getChildByName("Sphere 2") as Sprite3D;
var line = this.view.getChildByName("Line") as LineSprite3D;
//使用方法1:固定起点和终点
line.setStartPoint(g1.transform.position);
line.setEndPoint(g2.transform.position);
//使用方法2:设定物体的transform,连线会跟随物体移动
line.setTargets(g1.transform, g2.transform);
//设置宽度
line.widthMultiplier = 2;
Demo
Demo使用的是2.7.0 ts版编写,有需要的同学可以直接替换 bin/libs/laya.d3.js 和 libs/LayaAir.d.ts 两个文件即可。
链接:https://pan.baidu.com/s/1yHhgzuZAuqTPFWonw3d7hg
提取码:5dj3