GPUParticles2D Conversion Failings and Usage Oddities in 4.X #8400
Replies: 2 comments 1 reply
-
This is because the Godot 4 project converter can only rename properties, not compute new values for them. To compute new values for old properties that were renamed, a This can't be done for properties that weren't renamed though, so fixing emission shape after conversion is likely not possible.
3.x-style particle trails can be created using subemitters with the Constant subemitter mode. This is also generally more flexible.
My recommendation when working with GPU-based particles in 4.x is to set Fixed FPS to 0 and disable Interpolation, because it often breaks effects in various ways. This essentially restores the behavior of 3.x GPU-based particles. If this still occurs with fixed FPS set to 0 and interpolation disabled, then this may be due to a mismatch between the end of the particle's lifetime and what the subemitter code considers as "end of the particle's lifetime" (maybe a off-by-one or rounding error). Interpolation allows for more reliable particle behavior at varying framerates so it should ideally be fixed, but it's a lot of work to resolve all the bugs mentioned in that PR. |
Beta Was this translation helpful? Give feedback.
-
This post details the steps I took to help with the batch conversion of Particles2D in 3.5 to GPUParticles2D in 4.2. EDIT: There are currently a handful of bugs that affect porting particle effects from 3.5 to 4.2. When these bugs are fixed I'll edit this post to include the full process I took which clears up a handful of issues that exist in the scripts below, but also takes a bit more effort apply because of full project conversion shortcomings. For now here are the methods you can use to more fully convert Particles2D to GPUParticles2D. The methods that need to be calledfunc _get_conversion_data():
if child.conversion_data.is_empty() and Engine.get_version_info().major < 4:
var new_data := {}
if child.process_material.get("emission_shape") > 0: new_data["push_emission_shape"] = true
if child.process_material.get("flag_align_y"): new_data["flag_align_y"] = true
if child.process_material.get("flag_disable_z"): new_data["flag_disable_z"] = true
if child.process_material.get("color_ramp"): new_data["convert_color_ramp"] = true
new_data["convert_color_space"] = true
new_data["disable_fixed_fps"] = true
new_data["disable_interpolation"] = true
for parameter in ["initial_velocity", "angular_velocity","orbit_velocity","linear_accel","radial_accel","tangential_accel","damping","angle","scale","anim_speed","anim_offset"]:
var parameter_random = child.process_material.get( parameter + "_random" )
var conversion_max = child.process_material.get( parameter )
var conversion_min = conversion_max * (1.0 - parameter_random)
if parameter == "scale" and conversion_max < 1:
conversion_min = conversion_max
conversion_max = 1.0
elif conversion_max < 0:
var swap = conversion_min
conversion_min = conversion_max
conversion_max = swap
new_data[parameter + "_min"] = conversion_min
new_data[parameter + "_max"] = conversion_max
for parameter in ["hue_variation"]:
var parameter_random = child.process_material.get( parameter + "_random" )
var conversion_max = child.process_material.get( parameter )
var conversion_min = lerp( conversion_max, -conversion_max, child.process_material.get( parameter_random ) )
if conversion_max < 0:
var swap = conversion_min
conversion_min = conversion_max
conversion_max = swap
new_data[parameter + "_min"] = conversion_min
new_data[parameter + "_max"] = conversion_max
for parameter in ["angle_curve", "linear_accel_curve", "tangential_accel_curve", "radial_accel_curve", "damping_curve"]:
if not child.process_material.get( parameter ):
continue
var curve:Curve = child.process_material.get( parameter ).curve
if curve:
var parameter_base = parameter.trim_suffix("_curve")
var parameter_random = parameter_base + "_random"
var parameter_value = child.process_material.get( parameter_base )
var parameter_min = parameter_base + "_min"
var parameter_max = parameter_base + "_max"
new_data[parameter_min] = 1.0 - child.process_material.get( parameter_random )
new_data[parameter_max] = 1.0
if abs(parameter_value) <= 1:
continue
child.process_material.set( parameter_base, 0 )
for i in curve.get_point_count():
curve.set_point_value( i, curve.get_point_position( i ).y + parameter_value )
child.conversion_data = new_data
func _conversion() -> void:
if Engine.get_version_info().major < 4:
return
for child in get_children():
if child is DynamicParticles2D:
if child.conversion_data.get("convert_color_ramp",false):
var new_gradient := GradientTexture1D.new()
new_gradient.gradient = child.process_material.get("color_ramp").gradient
child.process_material.set("color_ramp", new_gradient)
if child.conversion_data.get("convert_color_space",false):
child.process_material.set("color", child.process_material.get("color").linear_to_srgb() )
child.conversion_data.erase("convert_color_space")
if child.conversion_data.get("push_emission_shape",false):
child.process_material.set( "emission_shape", child.process_material.get("emission_shape") + 1 )
child.conversion_data.erase("push_emission_shape")
if child.conversion_data.get("flag_align_y",false):
child.process_material.set("particle_flag_align_y",true)
child.conversion_data.erase("flag_align_y")
if child.conversion_data.get("flag_disable_z",false):
child.process_material.set("particle_flag_disable_z",true)
child.conversion_data.erase("flag_disable_z")
if child.conversion_data.get("disable_fixed_fps",false):
child.fixed_fps = 0
child.conversion_data.erase("disable_fixed_fps")
if child.conversion_data.get("disable_interpolation",false):
child.interpolate = false
child.conversion_data.erase("disable_interpolation")
for meta_variable in child.conversion_data.keys():
var meta_value = child.conversion_data[meta_variable]
if child.conversion_data.has( meta_variable ):
child.process_material.set( meta_variable, meta_value )
child.conversion_data.erase(meta_variable) Original PostStep 1) Use a custom class ( tool
class_name Particles2DToGPUParticles2D extends Particles2D
export var conversion_data := {} This code should be run on all your NOTE: I use var meta_min_max_fix_variables := ["initial_velocity", "angular_velocity","orbit_velocity","linear_accel","radial_accel","tangential_accel","damping","angle","hue_variation","scale"]
var new_data := {}
if process_material.get("emission_shape") > 0:
new_data["push_emission_shape"] = true
if process_material.get("flag_align_y"):
new_data["flag_align_y"] = true
if process_material.get("flag_disable_z"):
new_data["flag_disable_z"] = true
new_data["disable_fixed_fps"] = true
new_data["disable_interpolation"] = true
for parameter in meta_min_max_fix_variables:
var parameter_random = process_material.get( parameter + "_random" )
var conversion_max = process_material.get( parameter )
var conversion_min = conversion_max * (1.0 - parameter_random)
if sign(conversion_max) == -1:
var swap = conversion_min
conversion_min = conversion_max
conversion_max = swap
new_data[parameter + "_min"] = conversion_min
new_data[parameter + "_max"] = conversion_max
conversion_data = new_data Step 2) Open all of the Step 3) Copy the project and perform the full project conversion. Step 4) Run this code on all of your if conversion_data.is_empty():
continue
if conversion_data.get("push_emission_shape",false):
process_material.set( "emission_shape", process_material.get("emission_shape") + 1 )
conversion_data.erase("push_emission_shape")
if conversion_data.get("flag_align_y",false):
process_material.set("particle_flag_align_y",true)
conversion_data.erase("flag_align_y")
if conversion_data.get("flag_disable_z",false):
process_material.set("particle_flag_disable_z",true)
conversion_data.erase("flag_disable_z")
if conversion_data.get("disable_fixed_fps",false):
fixed_fps = 0
conversion_data.erase("disable_fixed_fps")
if conversion_data.get("disable_interpolation",false):
interpolate = false
conversion_data.erase("disable_interpolation")
for meta_variable in conversion_data.keys():
var meta_value = conversion_data[meta_variable]
if conversion_data.has( meta_variable ):
process_material.set( meta_variable, meta_value )
conversion_data.erase(meta_variable) NOTE: This is not a complete conversion. See Further Considerations below. ResultsHere's a comparison of running just the full project conversion vs running with this additional conversion step. Original 3.5.2 Particle Effects Godot_v3.5.2-stable_win64_2023-11-11_06-59-58.mp44.2 Full Conversion Godot_v4.2-beta5_win64_2023-11-12_12-23-49.mp44.2 Full Conversion + Conversion Class Godot_v4.2-beta5_win64_2023-11-12_12-24-30.mp4Further Considerations
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
I've started on the daunting task of porting somewhat complicated particle effects to 4.2 and it's a big pain point. At some point in the development process it seems these nodes lost a lot of polish. This is by no means an exhaustive list, this is just the issues I found going through THREE of my particle effects.
EDIT: Check #8400 (comment) for tips on batch conversion.
Conversion
I have to basically redo all of the particle effects out the gate because what particle effect doesn't use those parameters?
Even lower hanging fruit, the particle flags themselves weren't kept.
flag_align_y
becomesparticle_flag_align_y
, etc.Emission shapes weren't converted: Sphere needs to become Sphere Radius, Ring for some reason becomes Directed Points (I assume this is an enum thing and the old values were just numbers that no longer line up.)
Some previously additive curves are now used as multipliers against the parameters instead. If you used those together before this makes the conversion more difficult since now you have to think about the math works out to get a similar effect since -50 + a curve that goes from 100 to 0 over time has to become -50 + a curve that goes from -1 to 1 over time. Not sure if this could have been added to the converter but a math wizard might be able to answer that.
Previously additive but now multiplicative curves still have additive sensible default values. If I've got -50 I likely don't want to multiply that by 200. -1 to 1 seems like a more sensible default. Some curves don't even get a default range.
Trails
Godot_v3.5.2-stable_win64_2023-11-11_07-04-12.mp4
3.5 vs 4.2 (attempted recreation)
Godot_v3.5.2-stable_win64_2023-11-11_07-13-52.mp4
Godot_v4.2-beta5_win64_2023-11-11_07-16-00.mp4
Sub Emitters
Godot_v4.2-beta5_win64_2023-11-11_07-27-23.mp4
If you try to fix their offset by tinkering with the FPS / interpolation they just don't spawn sometimes.
They ignore their emission shape offset. Even if only Point + Offset was supported this would help a lot when it comes to visual consistency.
It's also possible that the lifetime of particles just doesn't work properly and the sub emitter is the ground truth for where a particle actually is and otherwise particles persist for too long, I really just can't tell.
In Conclusion
All in all this has just been a particularly negative part of the porting experience that could be smoothed out.
Here's what a bunch of my particle effects looked like in 3.5 vs post-conversion 4.2 (I have already fixed a few) for reference:
Godot_v3.5.2-stable_win64_2023-11-11_06-59-58.mp4
Godot_v4.2-beta5_win64_2023-11-11_07-01-29.mp4
Beta Was this translation helpful? Give feedback.
All reactions