-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathTrail3D.cs
259 lines (210 loc) · 7.36 KB
/
Trail3D.cs
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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
using Godot;
using System;
using System.Collections.Generic;
[Tool]
public partial class Trail3D : Node3D
{
[ExportGroup("Trail Properties")]
[Export] private bool emitting;
[Export] private float duration = 0.5f;
[Export] private float snapshotInterval = 0.02f;
[Export] private float width = 1;
[Export] private Curve widthCurve;
[Export] private Vector2 UVScale = new Vector2(1,1);
[Export] private Material material;
private List<TargetSnapshot> snapshotBuffer;
private MeshInstance3D trailMesh;
private float t;
private float snapshotT;
public void SetWidth(float value) => width = value;
public void SetDuration(float value) => duration = value;
public void SetSnapshotInterval(float value) => snapshotInterval = value;
public void SetMaterial(Material value) => material = value;
public bool IsEmitting() => emitting;
public void StartEmitting() => emitting = true;
public void StopEmitting()
{
Init();
emitting = false;
}
private void Init()
{
foreach (var child in GetChildren())
{
child.Free();
}
trailMesh = new MeshInstance3D();
AddChild(trailMesh);
trailMesh.Mesh = new ImmediateMesh();
if (trailMesh.Mesh is ImmediateMesh mesh)
{
mesh.ClearSurfaces();
}
GD.Print("initializing");
trailMesh.TopLevel = true;
t = 0;
snapshotT = 0;
snapshotBuffer = new List<TargetSnapshot>();
}
public override void _EnterTree()
{
Init();
}
public override void _Process(double delta)
{
if (!emitting)
{
return;
}
if (snapshotBuffer == null)
{
Init();
}
if (trailMesh == null)
{
GD.Print("[TRAIL3D] TRAIL MESH COULD NOT BE CREATED.");
emitting = false;
return;
}
float dt = (float)delta;
//Push a snapshot and draw a trail if a snapshotInterval has elapsed since the last snapshot.
if (snapshotT > snapshotInterval)
{
int count = snapshotBuffer.Count;
if (count > 0)
{
//Only push a snapshot if the target has moved.
if (snapshotBuffer[count-1].Position != GlobalPosition)
{
PushSnapshot();
}
//Remove a snapshot once it's been "alive" for duration.
if (t - snapshotBuffer[0].Time > duration)
{
snapshotBuffer.RemoveAt(0);
}
}
else //If there's nothing in the snapshotBuffer append the current position.
{
PushSnapshot();
}
DrawTrail();
snapshotT = 0;
}
t += dt;
snapshotT += dt;
}
//Add a snapshot to the buffer.
private void PushSnapshot()
{
snapshotBuffer.Add(new TargetSnapshot(GlobalPosition, GlobalTransform.Basis, t));
}
private void DrawTrail()
{
if (trailMesh.Mesh is ImmediateMesh mesh)
{
mesh.ClearSurfaces();
if(snapshotBuffer.Count < 2) return; //Only draw a face if there's two snapshots to draw between
//Iterate through the snapshot buffer and draw all the faces.
mesh.SurfaceBegin(Mesh.PrimitiveType.Triangles, material);
for(int i = 1; i < snapshotBuffer.Count; i++)
{
DrawFace(mesh, i);
}
GD.Print("---");
mesh.SurfaceEnd();
}
}
/*DRAW FACE METHOD:
- This function is responsible for drawing two triangles between two TargetSnapshots, resulting in a quad face.
- The Trail's are drawn using Immediate Mode geometry, the entire trail is redrawn every frame as it's insanely
cheap.
*/
private void DrawFace(ImmediateMesh mesh, int index)
{
TargetSnapshot snapshot = snapshotBuffer[index];
TargetSnapshot previousSnapshot = snapshotBuffer[index - 1];
float snapX = index / (float)snapshotBuffer.Count;
float snapWidth = widthCurve.Sample(snapX);
float prevSnapX = (index - 1) / (float)snapshotBuffer.Count;
float prevSnapWidth = widthCurve.Sample(prevSnapX);
Vector3 vert1 = previousSnapshot.Position + previousSnapshot.Basis.Y.Normalized() * prevSnapWidth * width;
Vector3 vert2 = snapshot.Position + snapshot.Basis.Y.Normalized() * snapWidth * width;
Vector3 vert3 = previousSnapshot.Position - previousSnapshot.Basis.Y.Normalized() * prevSnapWidth * width;
Vector3 vert4 = snapshot.Position - snapshot.Basis.Y.Normalized() * snapWidth * width;
/*NORMALS:
- all vertices on a face point in the same direction. Afaik, the only case where you *might* want to change
this is if you have a really low snapshot rate, and you want to do some blending on sharp angles. However with a higher
snapshot rate that blending shouldn't be necessary.
*/
Vector3 normal = snapshot.Basis.Z.Normalized();
/*UVs:
- UV's are seemless however, I need to implement some sort of triplanar mapping/similar to ensure that the UV's don't
stretch when the acceleration of the target isn't constant, and actually fit the faces that they are on.
- If all the faces were perfect squares the current implementation would work fine, which I might add as an option so you can
enable a min distance approach rather than a time based one. Even still, this would be based on thickness which means a lot of
fine tuning would be necessary to achieve a desired result.
*/
float snapUVx = Mathf.Lerp(0, 1, snapX) * UVScale.X;
float prevSnapUVx = Mathf.Lerp(0, 1, prevSnapX) * UVScale.X;
float snapUVy = Mathf.Lerp(0, 1, snapWidth) * UVScale.Y;
float prevSnapUVy = Mathf.Lerp(0, 1, prevSnapWidth) * UVScale.Y;
Vector2 vert1UV = new Vector2(prevSnapUVx, 0.5f + prevSnapUVy / 2);
Vector2 vert2UV = new Vector2(snapUVx, 0.5f + snapUVy / 2);
Vector2 vert3UV = new Vector2(prevSnapUVx, 0.5f - prevSnapUVy / 2);
Vector2 vert4UV = new Vector2(snapUVx, 0.5f - snapUVy / 2);
Vector2[] tri1UVs = { vert1UV, vert2UV, vert3UV};
Triangle tri1 = new Triangle(new[] {vert1, vert2, vert3}, new[] {normal, normal, normal}, tri1UVs, t);
//This triangles vertices must start with vert4 to ensure the tri points in the same direction as tri1.
Vector2[] tri2UVs = {vert4UV, vert3UV, vert2UV};
Triangle tri2 = new Triangle(new[] {vert4, vert3, vert2}, new[] {normal, normal, normal}, tri2UVs, t);
//It's important to load the triangles in this order to ensure that the trail doesn't self-intersect.
for (int i = 0; i < tri1.Vertices.Length; i++)
{
mesh.SurfaceSetUV(tri1.UVs[i]);
mesh.SurfaceSetNormal(tri1.Normals[i]);
mesh.SurfaceAddVertex(tri1.Vertices[i]);
}
for (int i = 0; i < tri2.Vertices.Length; i++)
{
mesh.SurfaceSetUV(tri2.UVs[i]);
mesh.SurfaceSetNormal(tri2.Normals[i]);
mesh.SurfaceAddVertex(tri2.Vertices[i]);
}
}
/*TRIANGLE STRUCT
- Basic triangle class that represents half of a "face" on the mesh.
- 2 Target Snapshots are required to draw a face, the DrawFace method draws a triangle between the currently
sampled position and the previous one, to ensure a continous row of triangles.
*/
public struct Triangle
{
public Vector3[] Vertices;
public Vector3[] Normals;
public Vector2[] UVs;
public float Time { get; private set; }
public Triangle(Vector3[] vertices, Vector3[] normals, Vector2[] uvs, float time)
{
Vertices = vertices;
Normals = normals;
UVs = uvs;
Time = time;
}
}
/*TARGET SNAPSHOT CLASS
- Stores the Position and Basis at a given point in time.
- The time is stored so we can remove this snapshot from the buffer after it's been alive for duration.
*/
public class TargetSnapshot
{
public Vector3 Position { get; private set; }
public Basis Basis { get; private set; }
public float Time { get; private set; }
public TargetSnapshot(Vector3 position, Basis basis, float time)
{
Position = position;
Basis = basis;
Time = time;
}
}
}