-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathproton_trail.gd
256 lines (201 loc) · 6.05 KB
/
proton_trail.gd
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
@tool
extends Node3D
# --
# ProtonTrail
# --
# This node generates a 3D trail at runtime
# Two nodes defines the width of the the trail. At each tick, the process
# function update the trail geometry, add new points if the latest ones
# are too far away from the previous ones, and remove the oldest points
# if they expired.
#
# --
# material : The material applied to the trail. UV coordinates are
# defined as follow :
# + The most recent points are at the left of the texture (0,0) and (0,1)
# + The oldest points are at the right (1,0) and (1,1)
# + That means the texture is stretch accross the entire trail
#
# invert_uv_x / y : Flip the UV coordinates on their respective axis
#
# smooth: 0 to disable. Smooth the previous geometry, useful if your emitter
# moves too fast and produce a jagged trail
#
# resolution : The higher the value, the more vertices will be generated
#
# life_time : How long a point can exist in the trail.
#
# emit : True means the trail will keep creating new points. False means it
# will only draw points still in memory and delete them, but wont create
# new geometry.
# --
@export var material: Material
@export var resolution := 4.0
@export var life_time := 0.1
@export_range(0.0, 1.0) var smooth := 0.5
@export var invert_uv_x := false
@export var invert_uv_y := false
@export var cast_shadow := true
@export var emit := true : set = set_emit
var _meshInstance := MeshInstance3D.new()
var _geometry := ImmediateMesh.new()
var _data := []
var _previous_data := []
var _max_dist: float
@onready var _top: Node3D = get_node_or_null("Top")
@onready var _bottom: Node3D = get_node_or_null("Bottom")
class Point:
var ttl: float
var p1: Vector3
var p2: Vector3
var n: Vector3
func _ready():
if Engine.is_editor_hint():
if not _top or not _bottom:
_create_required_nodes()
_meshInstance.set_name(get_name() + "Geometry")
_meshInstance.set_material_override(material)
_meshInstance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON if cast_shadow else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
_meshInstance.mesh = _geometry
_max_dist = 1.0 / resolution
func _enter_tree() -> void:
if _meshInstance and not _meshInstance.get_parent():
get_tree().get_root().call_deferred("add_child", _meshInstance)
func _exit_tree() -> void:
if is_instance_valid(_meshInstance):
_meshInstance.queue_free()
func _process(delta : float):
if not _top or not _bottom:
if has_node("Top") and has_node("Bottom"):
_top = get_node("Top")
_bottom = get_node("Bottom")
else:
set_process(false)
return
_update_all_ttl(delta)
_update_geometry()
_draw_all_geometry()
func _update_all_ttl(delta: float) -> void:
_update_ttl(_data, delta)
var size := _previous_data.size()
var index := 0
for i in size:
index = size - 1.0 - i
_update_ttl(_previous_data[index], delta)
if _previous_data[index].is_empty():
_previous_data.pop_back()
func _update_ttl(data: Array, delta: float) -> void:
var size = data.size()
var index = 0
for i in size:
index = size - 1.0 - i
data[index].ttl -= delta
if data[index].ttl <= 0:
data.pop_back()
func _update_geometry():
if not emit:
return
if _data.size() <= 1:
_add_single_point()
return
var top_pos := _top.get_global_transform().origin
var bottom_pos := _bottom.get_global_transform().origin
var dist_top: float = _data[1].p1.distance_to(top_pos)
var dist_bottom: float = _data[1].p2.distance_to(bottom_pos)
var dist = max(dist_top, dist_bottom)
# Always keep the last point on the emitter position
# if there's no need for new points
if dist <= _max_dist:
_data[0].p1 = top_pos
_data[0].p2 = bottom_pos
else:
_add_points_to_trail(ceil(dist / _max_dist))
_smooth_trail(_data)
for data in _previous_data:
_smooth_trail(data)
func _smooth_trail(data: Array) -> void:
var a1: Vector3
var a2: Vector3
var b1: Vector3
var b2: Vector3
var c1: Vector3
var c2: Vector3
var mean1: Vector3
var mean2: Vector3
for i in data.size() - 1:
if i == 0:
continue
a1 = data[i - 1].p1
a2 = data[i - 1].p2
b1 = data[i].p1
b2 = data[i].p2
c1 = data[i + 1].p1
c2 = data[i + 1].p2
mean1 = (a1 + c1) / 2.0
mean2 = (a2 + c2) / 2.0
data[i].p1 = b1.lerp(mean1, smooth)
data[i].p2 = b2.lerp(mean2, smooth)
data[i].n = (b1 - mean1).normalized()
func _add_points_to_trail(count: int):
var top_start = _data[0].p1
var top_end = _top.get_global_transform().origin
var bottom_start = _data[0].p2
var bottom_end = _bottom.get_global_transform().origin
for i in count:
var f: float = (i + 1.0) / (count)
var p = Point.new()
p.ttl = life_time
p.p1 = top_start.lerp(top_end, f)
p.p2 = bottom_start.lerp(bottom_end, f)
_data.push_front(p)
func _add_single_point():
var p = Point.new()
p.ttl = life_time
p.p1 = _top.get_global_transform().origin
p.p2 = _bottom.get_global_transform().origin
_data.push_front(p)
func _draw_all_geometry() -> void:
_geometry.clear_surfaces()
_draw_geometry(_data)
for d in _previous_data:
_draw_geometry(d)
func _draw_geometry(data: Array):
if data.size() <= 1:
return
var count := data.size()
var uv_x := 0.0
var uv_y_top := 1.0 if invert_uv_y else 0.0
var uv_y_bottom := 0.0 if invert_uv_y else 1.0
_geometry.surface_begin(Mesh.PRIMITIVE_TRIANGLE_STRIP, null)
var a: Vector3
var b: Vector3
var c: Vector3
for i in count:
uv_x = i / (count - 1.0)
if invert_uv_x:
uv_x = 1.0 - uv_x
_geometry.surface_set_normal(data[i].n)
_geometry.surface_set_uv(Vector2(uv_x, uv_y_top))
_geometry.surface_add_vertex(data[i].p1)
_geometry.surface_set_uv(Vector2(uv_x , uv_y_bottom))
_geometry.surface_add_vertex(data[i].p2)
_geometry.surface_end()
func _create_required_nodes():
var owner = get_tree().get_edited_scene_root()
_top = Marker3D.new()
_top.set_name("Top")
add_child(_top)
_top.translate(Vector3.UP)
_top.set_owner(owner)
_bottom = Marker3D.new()
_bottom.set_name("Bottom")
add_child(_bottom)
_bottom.translate(Vector3.DOWN)
_bottom.set_owner(owner)
func set_emit(val: bool) -> void:
if emit == val:
return
emit = val
if not emit:
_previous_data.push_front(_data.duplicate())
_data.clear()