-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathtrail.ts
More file actions
191 lines (160 loc) · 6.18 KB
/
trail.ts
File metadata and controls
191 lines (160 loc) · 6.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import {
Component,
Mesh,
MeshIndexType,
MeshAttribute,
Material,
Object3D,
MeshComponent,
} from '@wonderlandengine/api';
import {property} from '@wonderlandengine/api/decorators.js';
import {vec3} from 'gl-matrix';
const direction = vec3.create();
const offset = vec3.create();
const normal = vec3.create();
const UP = vec3.fromValues(0, 1, 0);
/**
* Dynamic mesh-based trail
*
* This component keeps track of the world position of the object it's added to.
* At a fixed interval the world position is stored as start and end points of the trail segments.
*
* The trail tapers off along its length. UV texture coordinates are setup such that the
* U-axis covers the width of the trail and the V-axis covers the length of the trail.
* This allows the trail's appearance to be defined using a texture.
*/
export class Trail extends Component {
static TypeName = 'trail';
/** The material to apply to the trail mesh */
@property.material()
material: Material | null = null;
/** The number of segments in the trail mesh */
@property.int(50)
segments = 50;
/** The time interval before recording a new point */
@property.float(0.1)
interval = 0.1;
/** The width of the trail (in world space) */
@property.float(1.0)
width = 1.0;
/** Whether or not the trail should taper off */
@property.bool(true)
taper = true;
/**
* The maximum delta time in seconds, above which the trail resets.
* This prevents the trail from jumping around when updates happen
* infrequently (e.g. when the tab doesn't have focus).
*/
@property.float(1.0)
resetThreshold = 0.5;
private _currentPointIndex = 0;
private _timeTillNext = 0;
private _points: Array<vec3> = [];
private _trailContainer: Object3D | null = null;
private _meshComp: MeshComponent | null = null;
private _mesh: Mesh | null = null;
private _indexData: Uint32Array | null = null;
start() {
this._points = new Array(this.segments + 1);
for (let i = 0; i < this._points.length; ++i) {
this._points[i] = vec3.create();
}
/* The points array is circular, so keep track of its head */
this._timeTillNext = this.interval;
this._trailContainer = this.engine.scene.addObject();
/* Workaround for new objects sometimes not being set dirty before 1.4.6 */
this._trailContainer.setDirty();
this._meshComp = this._trailContainer.addComponent('mesh')!;
this._meshComp.material = this.material;
/* Each point will have two vertices; one on either side */
const vertexCount = 2 * this._points.length;
/* Each segment consists of two triangles */
this._indexData = new Uint32Array(6 * this.segments);
for (let i = 0, v = 0; i < vertexCount - 2; i += 2, v += 6) {
this._indexData
.subarray(v, v + 6)
.set([i + 1, i + 0, i + 2, i + 2, i + 3, i + 1]);
}
this._mesh = new Mesh(this.engine, {
vertexCount: vertexCount,
indexData: this._indexData,
indexType: MeshIndexType.UnsignedInt,
});
this._meshComp.mesh = this._mesh;
}
updateVertices() {
if (!this._mesh) return;
const positions = this._mesh.attribute(MeshAttribute.Position)!;
const texCoords = this._mesh.attribute(MeshAttribute.TextureCoordinate);
const normals = this._mesh.attribute(MeshAttribute.Normal);
vec3.set(direction, 0, 0, 0);
for (let i = 0; i < this._points.length; ++i) {
const curr =
this._points[(this._currentPointIndex + i + 1) % this._points.length];
const next =
this._points[(this._currentPointIndex + i + 2) % this._points.length];
/* The last point has no next, so re-use the direction of the previous segment */
if (i !== this._points.length - 1) {
vec3.sub(direction, next, curr);
}
vec3.cross(offset, UP, direction);
vec3.normalize(offset, offset);
const timeFraction = 1.0 - this._timeTillNext / this.interval;
const fraction = (i - timeFraction) / this.segments;
vec3.scale(offset, offset, ((this.taper ? fraction : 1.0) * this.width) / 2.0);
positions.set(i * 2, [
curr[0] - offset[0],
curr[1] - offset[1],
curr[2] - offset[2],
]);
positions.set(i * 2 + 1, [
curr[0] + offset[0],
curr[1] + offset[1],
curr[2] + offset[2],
]);
if (normals) {
vec3.cross(normal, direction, offset);
vec3.normalize(normal, normal);
normals.set(i * 2, normal);
normals.set(i * 2 + 1, normal);
}
if (texCoords) {
texCoords.set(i * 2, [0, fraction]);
texCoords.set(i * 2 + 1, [1, fraction]);
}
}
/* Notify WLE that the mesh has changed */
this._mesh.update();
}
resetTrail() {
this.object.getPositionWorld(this._points[0]);
for (let i = 1; i < this._points.length; ++i) {
vec3.copy(this._points[i], this._points[0]);
}
this._currentPointIndex = 0;
this._timeTillNext = this.interval;
}
update(dt: number) {
this._timeTillNext -= dt;
if (dt > this.resetThreshold) {
this.resetTrail();
}
if (this._timeTillNext < 0) {
this._currentPointIndex = (this._currentPointIndex + 1) % this._points.length;
this._timeTillNext = (this._timeTillNext % this.interval) + this.interval;
}
this.object.getPositionWorld(this._points[this._currentPointIndex]);
this.updateVertices();
}
onActivate() {
this.resetTrail();
if (this._meshComp) this._meshComp.active = true;
}
onDeactivate() {
if (this._meshComp) this._meshComp.active = false;
}
onDestroy() {
if (this._trailContainer) this._trailContainer.destroy();
if (this._mesh) this._mesh.destroy();
}
}