Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node3D gizmo improvements #50748

Merged
merged 1 commit into from
Jul 23, 2021
Merged

Node3D gizmo improvements #50748

merged 1 commit into from
Jul 23, 2021

Conversation

JFonS
Copy link
Contributor

@JFonS JFonS commented Jul 22, 2021

  • Clean-up of node_3d_editor_plugin.{h,cpp}: removed unused code, fixed some bugs. I think I kept the selection behavior (with locked/grouped nodes, and shift pressed/unpressed) the same as before, but it wouldn't hurt if @foxydevloper and @EricEzaM could double check, since I had to rebase on their commits.

  • Moved node_3d_editor_gizmos.{h,cpp} to editor/plugins.

  • Added support for multiple gizmos per node. This means custom gizmos will no longer override the built-in ones and that multiple gizmos can be used in more complex nodes.

  • Added support for handle IDs. When adding handles to a gizmo, an ID can be specified for each one, making it easier to work with gizmos that have a variable number of handles.

  • Added support for subgizmos, selectable elements that can be transformed without needing a node of their own. By overriding _subgizmo_intersect_frustum() and/or _subgizmo_intersect_ray() gizmos can define which subgizmos should be selected on a region or click selection. Subgizmo transformations are applied usingget/set/commit virtual methods, similar to how handles work.

With this changes we should be good to start implementing more advanced gizmos for Skeleton3D, Path3D and MultimeshInstance3D amongst others.

Simple example implementation of subgizmos

In order to test the new subgizmo system I implemented a simple node that holds a list of transforms. The gizmo exposes a subgizmo for each transform and just displays them as boxes.

Peek.2021-07-22.19-27.mp4
Example source code
class Testing3DGizmoPlugin : public EditorNode3DGizmoPlugin {
	GDCLASS(Testing3DGizmoPlugin, EditorNode3DGizmoPlugin);

public:
	bool has_gizmo(Node3D *p_spatial) override;
	String get_gizmo_name() const override;

	int subgizmos_intersect_ray(const EditorNode3DGizmo *p_gizmo, Camera3D *p_camera, const Vector2 &p_point) const override;
	Vector<int> subgizmos_intersect_frustum(const EditorNode3DGizmo *p_gizmo, const Camera3D *p_camera, const Vector<Plane> &p_frustum) const override;
	Transform3D get_subgizmo_transform(const EditorNode3DGizmo *p_gizmo, int p_id) const override;
	void set_subgizmo_transform(const EditorNode3DGizmo *p_gizmo, int p_id, Transform3D p_transform) const override;
	void commit_subgizmos(const EditorNode3DGizmo *p_gizmo, const Vector<int> &p_ids, const Vector<Transform3D> &p_restore, bool p_cancel = false) const override;

	void redraw(EditorNode3DGizmo *p_gizmo) override;

	Testing3DGizmoPlugin();
};

//////////////

Testing3DGizmoPlugin::Testing3DGizmoPlugin() {
	create_material("solid", Color(0, 0, 1));
	create_material("sel_solid", Color(0, 1, 0));
}

bool Testing3DGizmoPlugin::has_gizmo(Node3D *p_spatial) {
	return Object::cast_to<MyNode>(p_spatial) != nullptr;
}

String Testing3DGizmoPlugin::get_gizmo_name() const {
	return "MyNode";
}

int Testing3DGizmoPlugin::subgizmos_intersect_ray(const EditorNode3DGizmo *p_gizmo, Camera3D *p_camera, const Vector2 &p_point) const {
	MyNode *mn = Object::cast_to<MyNode>(p_gizmo->get_spatial_node());
	ERR_FAIL_COND_V(!mn, -1);

	Vector<Transform3D> xforms = mn->get_xforms();

	for (int i = 0; i < xforms.size(); i++) {
		Transform3D t = mn->get_global_transform() * xforms[i];
		Vector2 p = p_camera->unproject_position(t.origin);
		if (p.distance_to(p_point) < 20) {
			return i;
		}
	}
	return -1;
}

Vector<int> Testing3DGizmoPlugin::subgizmos_intersect_frustum(const EditorNode3DGizmo *p_gizmo, const Camera3D *p_camera, const Vector<Plane> &p_frustum) const {
	MyNode *mn = Object::cast_to<MyNode>(p_gizmo->get_spatial_node());
	ERR_FAIL_COND_V(!mn, Vector<int>());

	const Plane *frustum = p_frustum.ptr();
	int fc = p_frustum.size();

	Vector<Transform3D> xforms = mn->get_xforms();
	Vector<int> ret;
	for (int i = 0; i < xforms.size(); i++) {
		Vector3 p = (mn->get_global_transform() * xforms[i]).origin;

		bool any_out = false;

		for (int j = 0; j < fc; j++) {
			if (frustum[j].is_point_over(p)) {
				any_out = true;
				break;
			}
		}

		if (!any_out) {
			ret.push_back(i);
		}
	}
	return ret;
}

Transform3D Testing3DGizmoPlugin::get_subgizmo_transform(const EditorNode3DGizmo *p_gizmo, int p_id) const {
	MyNode *mn = Object::cast_to<MyNode>(p_gizmo->get_spatial_node());
	ERR_FAIL_COND_V(!mn, Transform3D());
	ERR_FAIL_INDEX_V(p_id, mn->get_xforms().size(), Transform3D());
	return mn->get_xforms()[p_id];
}

void Testing3DGizmoPlugin::set_subgizmo_transform(const EditorNode3DGizmo *p_gizmo, int p_id, Transform3D p_transform) const {
	MyNode *mn = Object::cast_to<MyNode>(p_gizmo->get_spatial_node());
	ERR_FAIL_COND(!mn);

	Vector<Transform3D> xforms = mn->get_xforms();
	ERR_FAIL_INDEX(p_id, xforms.size());
	xforms.write[p_id] = p_transform;
	mn->set_xforms(xforms);
}

void Testing3DGizmoPlugin::commit_subgizmos(const EditorNode3DGizmo *p_gizmo, const Vector<int> &p_ids, const Vector<Transform3D> &p_restore, bool p_cancel) const {
	MyNode *mn = Object::cast_to<MyNode>(p_gizmo->get_spatial_node());
	ERR_FAIL_COND(!mn);

	Vector<Transform3D> xforms = mn->get_xforms();

	Array xforms_a;
	xforms_a.resize(xforms.size());
	for (int i = 0; i < xforms.size(); i++) {
		xforms_a[i] = xforms[i];
	}

	Array restore_a = xforms_a.duplicate();
	for (int i = 0; i < p_ids.size(); i++) {
		int id = p_ids[i];
		ERR_FAIL_INDEX(id, xforms.size());
		restore_a[id] = p_restore[i];
	}

	if (p_cancel) {
		mn->set_xforms(p_restore);
	} else {
		UndoRedo *ur = Node3DEditor::get_singleton()->get_undo_redo();
		ur->create_action(TTR("Change Testing Transform"));
		ur->add_do_method(mn, "set_xforms", xforms_a);
		ur->add_undo_method(mn, "set_xforms", restore_a);
		ur->commit_action();
	}
}

void Testing3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) {
	MyNode *mn = Object::cast_to<MyNode>(p_gizmo->get_spatial_node());
	ERR_FAIL_COND(!mn);
	Vector<Transform3D> xforms = mn->get_xforms();

	p_gizmo->clear();

	Ref<Material> solid_material = get_material("solid", p_gizmo);
	Ref<Material> sel_solid_material = get_material("sel_solid", p_gizmo);

	for (int i = 0; i < xforms.size(); i++) {
		p_gizmo->add_solid_box(p_gizmo->is_subgizmo_selected(i) ? sel_solid_material : solid_material, Vector3(0.5,0.5,0.5), Vector3(), xforms[i]);
	}
}

@JFonS JFonS added this to the 4.0 milestone Jul 22, 2021
@JFonS JFonS requested review from a team as code owners July 22, 2021 19:18
Copy link
Member

@akien-mga akien-mga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only had a cursory look through the code but it looks good to me.

* Clean-up of node_3d_editor_plugin.{h,cpp}: removed unused code, fixed some bugs.
* Moved node_3d_editor_gizmos.{h,cpp} to editor/plugins.
* Added support for multiple gizmos per node. This means custom gizmos will no longer override the built-in ones and that multiple gizmos can be used in more complex nodes.
* Added support for handle IDs. When adding handles to a gizmo, an ID can be specified for each one, making it easier to work with gizmos that have a variable number of handles.
* Added support for subgizmos, selectable elements that can be transformed without needing a node of their own. By overriding _subgizmo_intersect_frustum() and/or _subgizmo_intersect_ray() gizmos can define which subgizmos should be selected on a region or click selection. Subgizmo transformations are applied using get/set/commit virtual methods, similar to how handles work.
@akien-mga akien-mga merged commit 4c3d585 into godotengine:master Jul 23, 2021
@akien-mga
Copy link
Member

Thanks!

@TokageItLab
Copy link
Member

@JFonS I'm working on applying it to SkeletonGizmo, but I'm a little confused about the id handling. Can you give me the MyNode sample code above?

@TokageItLab
Copy link
Member

TokageItLab commented Jul 25, 2021

@JFonS And, there is a problem with the subgizmos_intersect_ray() firing before grabbing the arrow and gyro Gizmo for Transform. I think the correct behavior at 0:14 in the above your video should be to grab the Gizmo, not to select the object behind it.

2021-07-25.9.16.21.mov

@JFonS
Copy link
Contributor Author

JFonS commented Jul 25, 2021

I will take a look at the sorting, you are right it probably makes more sense to select after checking the transform gizmo.

The example code is already in the PR description. You have to open the "Example source code" section under the video :)

@TokageItLab
Copy link
Member

TokageItLab commented Jul 25, 2021

I saw the sample code about GizmoPlugin, but I don't know what's about MyNode class. Does it simply have an array of xforms?
So, is it right that p_id is needed to display multiple Gizmo's at the same time, and if there is always one Gizmo, there is no need to use it?

@JFonS
Copy link
Contributor Author

JFonS commented Jul 25, 2021

I don't have the code for MyNode at hand right now, I can post it later today, but it just stores a Vector<Transform3D>.

p_id is the subgizmo ID, it can be any integer you want, as long as you know which ID corresponds to which subgizmo. In the case of Skeleton3D you should have one ID for each bone. Then get_subgizmo_transform() should return the local transform for the given bone. Similarly for set_subgizmo_transform() and commit_subgizmos().

@TokageItLab
Copy link
Member

TokageItLab commented Jul 25, 2021

Thanks, I understand about MyNode.
I'm changing the Skeleton's selected_bone_id in a raycast. In this case, do I need the id of SubGizmo?

In this case, the transform can be retrieved in get_subgizmo_transform() using the seleced_id of the Skeleton instead of the id of the SubGizmo.

Transform3D Skeleton3DGizmoPlugin::get_subgizmo_transform(const EditorNode3DGizmo *p_gizmo, int p_id) const {
	Skeleton3D *skeleton = Object::cast_to<Skeleton3D>(p_gizmo->get_spatial_node());
	ERR_FAIL_COND_V(!skeleton, Transform3D());

	Transform3D tr;
	if (skeleton->get_selected_bone() == -1) {
		tr = skeleton->get_global_transform();
	} else {
		tr = skeleton->get_bone_global_pose(skeleton->get_selected_bone());
	}

	return tr;
}

@TokageItLab
Copy link
Member

TokageItLab commented Jul 26, 2021

@JFonS Does the above your example work when changing the Scale of the MyNode itself, and when changing the Scale of the MyNode's parent? With the current value of p_transform, I'm thinking it might require a complicated transformation to get it to work correctly.

When the parent Node's scale is not Vector3(1,1,1) and Node3DEditor::are_local_coords_enabled() is true, the value of p_transform as the argument of set_subgizmo_transform() is quite different between TRANSLATE and ROTATE or SCALE.

When Node3DEditor::are_local_coords_enabled() is false, this difference does not occur, so I consider this to be inconsistent behavior and maybe a bug. I may post issue or PR about it later.

Addition:
Fixed #50933 by JFonS

@AndreaCatania
Copy link
Contributor

This PR is very very great and will simplify my Character gizmos, where I was using many handles to move the head position and direction, etc.. This will simplify things a lot, thanks!

As other thing you can consider to improve this this mechanism even more is to couple 2D and 3D gizmos. For example, I've some nodes that inherit directly from a Node, but aren't neither Node2D and Node3D. The simplest one is the Entity, as you can imagine, sometimes it needs to spawn the transform gizmos, and sometimes it doesn't.
If you would like supporting that, and you need more info about it, I would be glad to explain it further with some examples.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants