extends SceneTree const EffectPlayer = preload("res://fx/effect_player.gd") var failures := 0 func check(ok: bool, label: String) -> void: if not ok: failures += 1 printerr("FAIL: " + label) func _init() -> void: call_deferred("run") func run() -> void: var fx := EffectPlayer.new() fx.build({"particles": [{"emitter": {"TimeEventLifeTime": [[0, 1.0]]}, "particle": { "BillboardType": 1, "RotationType": 4, "RotationSpeed": 90, "ColorOperationType": 4}}]}) var emitter: GPUParticles3D = fx._emitters[0] var pm: ParticleProcessMaterial = emitter.process_material var material: ShaderMaterial = emitter.material_override check(pm.angular_velocity_min == 0 and pm.angular_velocity_max == 0, "no second angular integration") check(pm.anim_offset_min == 0 and pm.anim_offset_max == 1 and pm.anim_speed_max == 0, "stable per-birth sign seed") check(material.get_shader_parameter("random_rotation") == true, "random rotation enabled") check(is_equal_approx(material.get_shader_parameter("rotation_speed"), PI / 2), "full magnitude in radians") if DisplayServer.get_name() != "headless": await pixels(material) fx.free() print("effect_random_rotation_test: failures=%d" % failures) quit(1 if failures else 0) func pixels(material: ShaderMaterial) -> void: var viewport := SubViewport.new() viewport.size = Vector2i(128, 128) viewport.own_world_3d = true viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS root.add_child(viewport) var environment := WorldEnvironment.new() environment.environment = Environment.new() environment.environment.background_mode = Environment.BG_COLOR environment.environment.background_color = Color.BLACK viewport.add_child(environment) var image := Image.create(2, 2, false, Image.FORMAT_RGBA8) image.fill(Color.WHITE) material.set_shader_parameter("albedo_texture", ImageTexture.create_from_image(image)) material.set_shader_parameter("rotation_lifetime", 1.0) var node := MultiMeshInstance3D.new() node.multimesh = MultiMesh.new() node.multimesh.transform_format = MultiMesh.TRANSFORM_3D node.multimesh.use_custom_data = true node.multimesh.mesh = QuadMesh.new() node.multimesh.mesh.size = Vector2(1.2, 0.16) node.multimesh.instance_count = 1 node.multimesh.set_instance_transform(0, Transform3D.IDENTITY) node.material_override = material viewport.add_child(node) var camera := Camera3D.new() camera.projection = Camera3D.PROJECTION_ORTHOGONAL camera.size = 2.0 camera.position.z = 3 viewport.add_child(camera) var results: Array[Image] = [] for seed in [0.25, 0.75]: node.multimesh.set_instance_custom_data(0, Color(0, 0.5, seed, 1)) await process_frame await RenderingServer.frame_post_draw results.append(viewport.get_texture().get_image()) var positive := 0 var negative := 0 var mismatch := 0 var lit := 0 for y in 128: for x in 128: var a := results[0].get_pixel(x, y).r > 0.5 var b := results[1].get_pixel(x, y).r > 0.5 if a: lit += 1 positive += (2 * x - 127) * (2 * y - 127) if b: negative += (2 * x - 127) * (2 * y - 127) if a != (results[1].get_pixel(127 - x, y).r > 0.5): mismatch += 1 check(lit > 300, "rendered visible particles") check(positive * float(negative) < 0, "opposite rotation directions") check(mismatch < 60, "equal magnitude produces mirrored GPU images") viewport.free() await process_frame