Godot Runtime 3D Gizmo Tutorial Part 2
Pre-tutorial note
This tutorial uses Markdeep! Markdeep renders to HTML locally on your web browser and this may take a second, so please be patient! 🙂
**Godot Runtime 3D Gizmo Tutorial Part 2** # Part overviewIn this part, we will be starting to make the 3D gizmos work in Godot, at runtime. Let's jump right in! # Making Gizmos ## Setup Before we start working on the gizmos, we need to do a few things so we are ready and know how everything is working. First let's look at how Editor_Viewport
and its children nodes are setup.* Editor_Viewport
: The Viewport node that holds all of the gizmos. The plan is that anything we render in this viewport will be placed on top of what is rendered in the scene. See below for more details. *Gizmo_Camera
: The Camera node that will render what it sees to theEditor_Viewport
. *Gizmos
: A Spatial node to hold all of the gizmos our editor has. This node is primarily used for organization purposes, and we'll also use it to position the gizmo at the selected object. *Translate
: The translate/translation gizmo. This gizmo will move the selected object through 3D space when interacted with. *Handle_X
: A StaticBody node that defines the X axis handle for the translation gizmo. The reason we are using StaticBody nodes is because we need something for the raycast to collide with. *Mesh
: A MeshInstance node that will render the cube we are using for the handle. *CollisionShape
: A CollisionShape node that defines the collision area for the handle. *Handle_Y
: A StaticBody node that defines the Y axis handle for the translation gizmo. This has the same child node structure asHandle_X
, just with a different material (green instead of red) and position. *Handle_Z
: A StaticBody node that defines the Z axis handle for the translation gizmo. This has the same child node structure asHandle_X
, just with a different material (blue instead of red) and position. *Body_Center
: A StaticBody node that defines the center handle for the translation gizmo. The center handle will move the object relative to the camera, instead of on a single axis. This node has the same child layout asHandle_X
, just with a different material (yellow instead of red) and position. *Rotate
: The rotation gizmo. This gizmo will rotate the selected object when interacted with. It has a child node layout very similar to theTranslate
node. Feel free to take a look if you want, though there shouldn't be much, if any, difference in comparison toTranslate
. *Scale
: The scale gizmo. This gizmo will scale the selected object when interacted with. It has a child node layout very similar to theTranslate
node. Feel free to take a look if you want, though there shouldn't be much, if any, difference in comparison toTranslate
. *Select
: The select gizmo. This gizmo is purely visual and is just there to show which object is selected. *Mesh
: A MeshInstance node to display the select cube. This uses the same yellow material as the center gizmo meshes. As you can see, this is quite a few nodes! There are a few things we should go over before we start writing the code we'll need to make the gizmos work. ### Viewport setup First I want to quickly mention some points about the Viewport node that tripped me up when working on this project. First let's take a look at theEditor_Viewport
node's properties. Please ignore theScript Variables
section, it is only there because I took the picture using the completed project, and will not be there until we write the script.There are a few things we'll need to set for the viewport to work like we want it to, some of them obvious and some of them not. First, notice how we have defined the size of the viewport to be 1280x720
. We will have the viewport change its size to always be the same size as the root viewport, but when the project is first initializing and we get try to get the render texture, if we do not set a viewport size by default there may be some shuttering as Godot tires to render into a0x0
sized image. Just a bit lower in the properties list, you will find that theTransparent Bg
property is enabled. TheTransparent Bg
property will make the viewport render the background with transparent pixels. If this property is not enabled, then the background will render solid black instead of transparent, which is not what we want. Another thing to note is the properties we have setup in theRender Target
category. The only thing that is changed from the default Viewport settings is that we have enabled theV Flip
property. In Godot, by default Viewports render upside down (for some reason) and so by enabling this property we can skip a step where we have to flip the viewport ourselves. This means that when we are using input events with a delta property, like mouse motion events, we need to flip the Y axis so the event acts like we would expect. Finally, and this one caught me by surprise, is how theWorld
property works and how Godot physics work with Viewports. _____ First, let me quickly explain what a World does in Godot. A World object is responsible for controlling all of the physics and 3D environment settings for a Viewport. For example, the root viewport has its own world and when we use theGet_World
function, we can access things like the direct physics space. Placing a WorldEnvironment world will override the default world environment of the viewport above it. So, I thought it would be a good idea to use a new World in theEditor_Viewport
so we could use raycasting to detect which part of the gizmo we have selected without having to worry about colliding with other objects in the scene. Maybe it is a bug, or maybe I'm missing something, but I thought that by enabling theOwn World
property and supplying a new World object, that the Viewport would be using its own isolated world and then we could raycast into it without worrying about colliding with other physics objects in the scene. This is what the [documentation](https://docs.godotengine.org/en/3.0/classes/class_viewport.html) seems to imply as well. However, enabling theOwn World
property with a custom World object seems to make the physics not work at all for that Viewport. However, if we disable theOwn World
property but still have a custom World object, then physics within the World work as expected and will not interact with objects outside of the Viewport. Why/How this happens, I have no idea, but it took me a good couple hours of trying to figure out why theOwn World
property was not working to get to this point. So, because of this we now have a custom World, but we do not haveOwn World
enabled, because with it enabled all physics within the viewport ceases to function. ______ Finally, the last thing thing I want to mention about Viewport nodes is something I actually briefly mentioned in part 1. Viewport nodes do not receive input events on their own by default, and have to have input events passed to them in order for them to work. This makes sense, because for the majority of Viewport uses, the Viewport will likely be of a different size and position in comparison to the root viewport. However, in our case it means we have to pass input events to the viewport if we want any of the child nodes in the viewport to receive any input events. We did this inEditor_Controller
in part 1, but it is worth noting again. I spend quite a bit of time trying to figure out why the Viewport was not receiving input events before I hit upon why it was not working. As I said before, it makes sense, but because our viewport is the same size and position as the root viewport, we can just pass on input events without needing to do any additional processing. !!! NOTE: Note If you would like to see a tutorial on how to use Viewport nodes in multiple different ways, like how the Gui in 3D demo on the Godot demo repository works, let me know in the comments below! There is a lot of cool things you can do with Viewports once you know how they work and how to manipulate them to do what you want, and I'd love to share what I know! ### Gizmo setup That's all I have to say about the Viewport. Now let's look at the gizmos and go over how they are setup in more detail. We'll take a in depth look at the translate gizmo, and then a brief look at the other gizmos, as they more or less are the same. Because you cannot see nodes that are a child of a Viewport node that has its own world, in order to see the nodes we'll have to move them outside of theEditor_Viewport
node temporarily so we can take a look. Go ahead and select theGizmos
node and re-parent it by dragging and dropping it on top of theEditor_Controller
node. This will re-parent theGizmos
node and since it will no longer be a child ofEditor_Viewport
, we can see what we are working with. Now that we can actually see what we are working with, let's take a closer look! __________As you can see, the translate gizmo is just a few cube shaped StaticBody nodes with MeshInstance and CollisionShape child nodes. Feel free to take a closer look if you want, but I am going to assume you can figure out, more or less, how these nodes are basically setup. If you need help, feel free to ask in the comments below! Let's quickly go over what each of the cube shaped nodes represent: * The big yellow cube in the center is for translating/moving the selected object relative to the view of the camera. This is invaluable for quickly moving a object to roughly where you want. * The red cube shape is for translating/moving the selected object on the global X axis. This means that when this handle is clicked and dragged, the selected object will only be moving on the X axis relative to the world. This is very useful for getting very precise positioning on the X axis. * The green cube shape is for translating/moving the selected object on the global Y axis. This means that when this handle is clicked and dragged, the selected object will only be moving on the Y axis relative to the world. * The blue cube shape is for translating/moving the selected object on the global Z axis. This means that when this handle is clicked and dragged, the selected object will only be moving on the Z axis relative to the world. Using these four elements together, we can position a object almost exactly where we want in 3D space. One thing to note specifically to the translate gizmo is that the three handles (the red, green, and blue cube shapes) are pointing towards the axis they move the object on, and they are pointing towards the positive side of said axis. While we are here, let's quickly setup the cube shaped nodes in the gizmo with a simple script that will allow us to know which axis the mouse is over. The code is really simple and is entirely self contained. Select the Handle_X
child node in theTranslate
node and make a new script calledEditor_Gizmo_Collider.gd
. Save it somewhere and then add the following code:
1 2 3 4 5 6 7 8 9 10 11 12 |
extends StaticBody export (String, "ALL", "X", "Y", "Z") var gizmo_axis = "ALL"; var collision_shape = null; func _ready(): collision_shape = get_node("CollisionShape"); func activate(): collision_shape.disabled = false; func deactivate(): collision_shape.disabled = true; |
This code is super simple and really is only there to make things a tad easier later. Let's quickly go through how this script works, starting with its class variables: *gizmo_axis
: A variable to hold the axis the StaticBody node represents. There are four choices: TheX
,Y
, andZ
axis, as well asALL
, which will move the object relative to the camera. *collision_shape
: A variable to hold the CollisionShape node for this StaticBody. We need this so we can disable and enable the collider as we activate/deactivate the gizmo itself. !!! TIP: Tip In Godot, collision objects like StaticBody nodes are active in the physics world, even when they are not visible. This is different than some game engines that disable collision shapes when the node/component is disabled. All we have to do to disable a collision object is either move it to a collision layer that is empty (or zero), or we can simply disable all of the collision shapes, which will give the collision object no way to collide with anything. ### Going Through_ready
: All we're doing here is getting theCollisionShape
node and assigning it to thecollision_shape
variable. ### Going Throughactivate
All we're doing is enablingcollision_shape
by setting itsdisabled
property tofalse
. ### Going Throughdeactivate
Inversely, all we're doing is disablingcollision_shape
by setting itsdisabled
property totrue
. _______ And that is all we need to do code wise! Now, select theHandle_X
node and in it's script properties, setGizmo Axis
toX
.This will make that when we collide with that StaticBody, we will know it is for the X
axis. Now all we need to do is setup the other three nodes. SelectHandle_Y
and set theGizmo Axis
property toY
, forHandle_Z
set theGizmo Axis
property toZ
, and forBody_Center
set theGizmo Axis
property toALL
. That is all we need to do for the translation gizmo right now, so let's turn our attention next to the rotation gizmo! ______As you can see, the rotation ( Rotate
) gizmo is almost exactly the same as the translation gizmo, with the only major difference being that the yellow cube in the center has been replaced with a yellow sphere. Let's quickly go over what each of the different shaped nodes represent: * The big yellow sphere in the center is for rotating the selected object relative to the view of the camera. This is invaluable for quickly getting the rough rotation you want. * The red cube shape is for rotating the selected object on the global X axis. This means that when this handle is clicked and dragged, the selected object will only be rotating on the X axis relative to the world. This is very useful for getting precise rotation on the X axis. * The green cube shape is for rotating the selected object on the global Y axis. This means that when this handle is clicked and dragged, the selected object will only be rotating on the Y axis relative to the world. * The blue cube shape is for rotating the selected object on the global Z axis. This means that when this handle is clicked and dragged, the selected object will only be rotating on the Z axis relative to the world. As you can see, functionally the rotation gizmo is very similar to the translation gizmo, just instead of moving the object through 3D space, the rotation gizmo instead rotates the object. !!! TIP: Tip I made a minor mistake in the setup of the rotation gizmo in the starter assets included in part 1. You'll find that theCollisionShape
node underHandle_Y
is not positioned correctly in relation to the gizmo handle. All you need to do to position it correctly is change itsTranslation
to(0, 1, 0)
and itsRotation Degrees
to(-90, 0, 0)
. Sorry about that! Like with theTranslate
Gizmo, we'll need to apply theEditor_Gizmo_Collider.gd
script to all of the StaticBody nodes underRotate
. Select all of the nodes (Handle_X
,Handle_Y
,Handle_Z
, andBody_Center
) and the in the inspector scroll down and assign theEditor_Gizmo_Collider.gd
. Once all of the nodes have the script attached, configure theGizmo Axis
variable for each of the nodes to the following: *Gizmo Axis
inHandle_X
->X
*Gizmo Axis
inHandle_Y
->Y
*Gizmo Axis
inHandle_Z
->Z
*Gizmo Axis
inBody_Center
->All
And then we're done with the rotation gizmo and can move on to the scale gizmo next! _______As with the other two gizmos we've looked at so far, the Scale
gizmo is almost exactly the same. This time the only thing that has really changed is the shape of the handles coming out of the scale gizmo, as they have squares on the ends instead of just being cube shaped. Let's quickly go over what each of the different shaped nodes represent: * The big yellow cube in the center is for scale the selected object evenly across all axes. This is invaluable for quickly getting the rough scale you want. * The red cube handle is for scaling the selected object on the X axis. This means that when this handle is clicked and dragged, the selected object will only be scaled on the X axis. This is very useful for getting precise scaling on the X axis. * The green cube handle is for scaling the selected object on the global Y axis. This means that when this handle is clicked and dragged, the selected object will only be scaled on the Y axis. * The blue cube handle is for scaling the selected object on the global Z axis. This means that when this handle is clicked and dragged, the selected object will only be scaled on the Z axis. Like with theTranslate
Gizmo, we'll need to apply theEditor_Gizmo_Collider.gd
script to all of the StaticBody nodes underScale
. Select all of the nodes (Handle_X
,Handle_Y
,Handle_Z
, andBody_Center
) and the in the inspector scroll down and assign theEditor_Gizmo_Collider.gd
. Once all of the nodes have the script attached, configure theGizmo Axis
variable for each of the nodes to the following: *Gizmo Axis
inHandle_X
->X
*Gizmo Axis
inHandle_Y
->Y
*Gizmo Axis
inHandle_Z
->Z
*Gizmo Axis
inBody_Center
->All
And then we're done with the scale gizmo and can move on to the last gizmo, which is the simplest of the bunch! _______The last gizmo is the Select
gizmo, which is simply just a yellow cube. Unlike the other gizmos, the select gizmo is just a MeshInstance node. This gizmo is only used to visually show what is currently selected and serves no other purpose. # Programming the Gizmos Now that we have looked at all of the gizmos, let's move on to programming them next. First, move theGizmos
node back so that it is a child of theEditor_Viewport
node again. ## Programming the Gizmo Viewport. First, let's work on the script that will oversee everything that happens within the editor viewport. Select theEditor_Viewport
node and make a new script calledEditor_Viewport_Controller.gd
. Save it where you want, and then add the following code:
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 |
extends Viewport var editor_controller; const GIZMO_COLLISION_LAYER = 2; export (NodePath) var path_to_texture; export (NodePath) var path_to_editor_camera; var editor_camera; var gizmo_camera; var left_mouse_button_down = false; var selected_physics_object; var selected_object_physics_layer; var gizmos_holder; var gizmo_translate; var gizmo_rotate; var gizmo_scale; var gizmo_select; var current_gizmo; func _ready(): editor_controller = get_parent(); self.size = get_tree().root.size yield(get_tree(), "idle_frame") yield(get_tree(), "idle_frame") get_node(path_to_texture).texture = get_texture(); editor_camera = get_node(path_to_editor_camera); editor_camera.connect("physics_object_selected", self, "physics_object_selected"); gizmo_camera = get_node("Gizmo_Camera"); editor_controller.connect("on_editor_mode_change", self, "on_editor_mode_change"); gizmos_holder = get_node("Gizmos"); gizmo_translate = gizmos_holder.get_node("Translate"); gizmo_rotate = gizmos_holder.get_node("Rotate"); gizmo_scale = gizmos_holder.get_node("Scale"); gizmo_select = gizmos_holder.get_node("Select"); current_gizmo = gizmo_select; update_gizmos(); func on_editor_mode_change(): update_gizmos(); func update_gizmos(): gizmo_translate.update(false); gizmo_rotate.update(false); gizmo_scale.update(false); gizmo_select.update(false); if (editor_controller.editor_mode == "TRANSLATE"): current_gizmo = gizmo_translate; elif (editor_controller.editor_mode == "ROTATE"): current_gizmo = gizmo_rotate; elif (editor_controller.editor_mode == "SCALE"): current_gizmo = gizmo_scale; elif (editor_controller.editor_mode == "SELECT"): current_gizmo = gizmo_select; if (selected_physics_object != null): current_gizmo.update(true); func _process(delta): if (gizmo_camera != null): gizmo_camera.global_transform = editor_camera.view_camera.global_transform; if (get_tree().root.size != self.size): self.size = get_tree().root.size; if (selected_physics_object != null): gizmos_holder.global_transform.origin = selected_physics_object.global_transform.origin; func _physics_process(delta): if (Input.is_mouse_button_pressed(BUTTON_LEFT)): if (left_mouse_button_down == false): left_mouse_button_down = true; if (editor_controller.editor_mode != "SELECT"): send_editor_raycast(); else: left_mouse_button_down = false; func send_editor_raycast(): var space_state = world.direct_space_state; var raycast_from = gizmo_camera.project_ray_origin(get_mouse_position()) var raycast_to = raycast_from + gizmo_camera.project_ray_normal(get_mouse_position()) * 100; var result = space_state.intersect_ray(raycast_from, raycast_to, [self], GIZMO_COLLISION_LAYER); if (result.size() > 0): current_gizmo.axis_set(result.collider.gizmo_axis); else: current_gizmo.axis_set("NONE"); func physics_object_selected(new_object): if (selected_physics_object != null): selected_physics_object.collision_layer = selected_object_physics_layer; if (selected_physics_object is RigidBody): selected_physics_object.apply_impulse(Vector3(0,0,0), Vector3(0, 0.01, 0)); selected_physics_object = new_object; if (selected_physics_object != null): selected_object_physics_layer = selected_physics_object.collision_layer; selected_physics_object.collision_layer = 0; if (selected_physics_object is RigidBody): selected_physics_object.linear_velocity = Vector3(0,0,0); selected_physics_object.angular_velocity = Vector3(0,0,0); selected_physics_object.sleeping = true; current_gizmo.update(true); else: current_gizmo.update(false); |
This is quite a bit of code to go through, but we'll take it step by step! First, let's start with the class variables: *editor_controller
: A variable to hold a reference to theEditor_Controller
node. We need this so we can tell when the editor mode has changed. *GIZMO_COLLISOIN_LAYER
: The collision layer that all of the gizmo StaticBody nodes are on. Check out this handy [Godot QA post](https://godotengine.org/qa/17896/collision-layer-and-masks-in-gdscript) for details on how to convert a collision layer into a integer! *path_to_texture
: A exported NodePath to the TextureRect node that we will use to display the contents of the Viewport on. *path_to_editor_camera
: A exported NodePath to theEditor_Camera_Controller
node. We need this so we can get the currently selected object within the scene. *editor_camera
: A variable to hold theEditor_Camera_Controller
node. *gizmo_camera
A variable to hold the camera used to render everything withinEditor_Viewport
. *left_mouse_button_down
: A variable to hold whether the left mouse button is down or not. *selected_physics_object
: A variable to hold the currently selected physics object, which can be any of the following node types: RigidBody, StaticBody, or KinematicBody. *selected_object_physics_layer
: A variable to hold the physics layer(s) the selected physics object was on prior to selection. *gizmos_holder
: A variable to hold the node that is holding all of the gizmos. *gizmo_translate
: A variable to hold the translate gizmo (translation -> translation -> movement on the X, Y, and/or Z axes). *gizmo_rotate
: A variable to hold the rotation gizmo. *gizmo_scale
: A variable to hold the scale gizmo. *gizmo_select
: A variable to hold the selection gizmo. *current_gizmo
: A variable to hold the currently active gizmo. Phew! That is quite a few variables. Hopefully I explained what each one is going to be used for, though if you have any questions after you have completed the tutorial, feel free to ask in the comments below. Now let's go through each of the functions and what they do! ### Going Through_ready
First we get theEditor_Controller
node and assign it to theeditor_controller
variable. !!! NOTE: Note using get_parent assumes that the parent of this node is Editor_Controller. Depending on your project, this may or may not be a safe assumption. Next we make the editor viewport have the same size as the root viewport. We do this by getting the scene tree using theget_tree
function, and then we set the editor viewport'ssize
to theroot
viewport'ssize
. This will make the editor viewport the same size as the viewport used to render the game window! Next we let two render frames pass usingyeild(get_tree(), "idle_frame)
. We do this because we need to make sure the Viewport has rendered and captured a image at least once. Then we assign thetexture
property in the TextureRect node atpath_to_texture
to the texture stored within theEditor_Viewport
. This will make the TextureRect display the viewport's contents. Next we get theEditor_Camera_Controller
node usingpath_to_editor_camera
and assign it to theeditor_camera
variable. We then connect thephysics_object_selected
signal to thephysics_object_selected
function. After that we get theGizmo_Camera
node and assign it to thegizmo_camera
variable. We'll need the gizmo camera so we can move it to the same position as the view camera, and we'll also use it for raycasting with the gizmos themselves. Next we connect theon_editor_mode_change
signal ineditor_controller
to theon_editor_mode_change
function so we can change the active gizmo when the editor mode has changed. Then we get theGizmos
node and assign it to thegizmos_holder
node. After that we get the four gizmos and assign them to the class variables intended to store them. Finally, we setcurrent_gizmo
to the select gizmo by setting it togizmo_select
. This will make the select gizmo the default gizmo. We then call theupdate_gizmos
function so the gizmo are setup correctly based on which gizmo is the current gizmo. ### Going Throughon_editor_mode_change
All we're doing here is callingupdate_gizmos
so when the editor mode changes, the proper gizmo is active and ready. ### Going Throughupdate_gizmos
First we call a function calledupdate
on all four of the gizmos and pass infalse
. This will disable the gizmos and make them invisible. !!! NOTE: Note Don't worry, we're going to make the gizmo scripts after this! For now just trust that it will work as intended by the time we reach the end. If you have questions once the gizmo scripts are written, let me know in the comments below and I'll do my best to answer! Then based on the current editor mode,editor_controller.editor_mode
, we setcurrent_gizmo
to the gizmo that corresponds to the to the editor mode. For example, when the editor mode isTRANSLATE
, we setcurrent_gizmo
togizmo_translate
. Finally, we check to see ifcurrent_gizmo
is notnull
. Ifcurrent_gizmo
is notnull
, we call theupdate
function and pass intrue
, which will make the gizmo active and ready. ### Going Through_process
First we check to see ifgizmo_camera
is notnull
. If it is not, then we set theglobal_transform
ofgizmo_camera
to theglobal_transform
of theview_camera
ineditor_camera
. This will place the gizmo camera at the exact same position as the view camera, which will make it where the content we render in theEditor_Viewport
will be positioned correctly. !!! NOTE: Note If you are wondering why we are not just using a RemoteTransform node, it is because I couldn't the RemoteTransform node to work correctly withGizmo_Camera
and still render to theEditor_Viewport
. Maybe it was something on my end, or maybe it is a bug with the RemoteTransform node, but I just couldn't find a way to get it to stay in sync withView_Camera
while still rendering toEditor_Viewport
. Next we check to see if thesize
of the editor viewport is different than thesize
of theroot
viewport. If it is, then we set thesize
inEditor_Viewport
to the samesize
as theroot
viewport. Finally, we check to see if there is a selected physics object by checking to see ifselected_physics_object
is notnull
. If there is a selected physics object, then we set the position (global_transform.origin
) of thegizmos_holder
node to the position of the selected physics object. ### Going Through_physics_process
First we check to see if the left mouse button is pressed or held usingis_mouse_button_pressed
and passing inBUTTON_LEFT
. If the left mouse button is pressed/held, we then check to see if the left mouse button was just pressed or not by checking to see ifleft_mouse_button_down
isfalse
. Ifleft_mouse_button_down
is false, then we setleft_mouse_button_down
to true so the code below it is only called once. We then check to see if theeditor_mode
ineditor_controller
is NOTSELECT
. If it is notSELECT
, then we call thesend_editor_raycast
function so we send out a raycast that will interact with the current gizmo. Ifis_mouse_button_pressed
returnsfalse
instead oftrue
, then we know the left mouse button is no longer pressed/held anymore. In that case, all we do is setleft_mouse_button_down
tofalse
. ### Going Throughsend_editor_raycast
First we get the space state for the world within THIS viewport, withinEditor_Viewport
. Then we get the starting and ending position of the raycast we want to create and assign the results toraycast_from
andraycast_to
. See [Ray-casting in Godot](https://docs.godotengine.org/en/3.0/tutorials/physics/ray-casting.html) on the Godot documentation for more information on how to raycast using a Camera node. Then we send out the raycast usingintersect_ray
and store the results within a new variable calledresult
. Note that we are passingGIZMO_COLLISION_LAYER
so the raycast will ONLY collide with physics objects on that layer. Then we check to see if there is something stored withinresult
by checking how many elements are stored within it using thesize
function. Ifresults
does have something stored within it, then the raycast collided with something. In that case, we call a function calledaxis_set
incurrent_gizmo
and pass in thegizmo_axis
variable from thecollider
the raycast collided with. If you recall, we set thegizmo_axis
variables on all of the StaticBody nodes for the gizmos usingEditor_Gizmo_Collider.gd
, which we made earlier. Ifresults
does not have anything stored within it, then we callaxis_set
incurrent_gizmo
, but we pass in"NONE"
since the raycast did not collide with the gizmo. ### Going Throughphysics_object_selected
First we check to see if there is already a physics object selected by checking to see ifselected_physics_object
is notnull
. If there is already a physics object selected, we first set the collision/physics layer of the object stored inselected_physics_object
to the collision/physics layer we stored in theselected_object_physics_layer
variable. Then we check to see if theselected_physics_object
stored is a RigidBody node. If it is, then we apply a very, very small impulse to it. This will make it where it will not be sleeping and will make it interact with the physics world again. If we did not do this, it would just stay floating in the air until another physics object collided with it. Regardless of whether there was a stored physics object already inselected_physics_object
or not, we then setselected_physics_object
to the passed innew_object
. !!! NOTE: Note Remember, this is passed in from the editor camera, and when no object is selected it will passnull
. This meansnew_object
will benull
when no physics object was selected. Ifselected_physics_object
is notnull
, then we first store thecollision_layer
the physics object is on in theselected_object_physics_layer
variable so we can later reassign it when the object is no longer selected. Then set thecollision_layer
inselected_physics_object
to0
, so it is not on any collision layers and cannot interact/collide with anything. If the newlyselected_physics_object
is a RigidBody node, then we set both thelinear_velocity
andangular_velocity
to zero so the RigidBody will not move when selected. Then we put it to sleep by setting thesleeping
variable totrue
, so it will not be effected by forces like gravity. Finally, regardless of whether the newlyselected_physics_object
is a RigidBody or not, we tell the current gizmo to update by calling theupdate
function. _____ If the newlyselected_physics_object
isnull
, then we callupdate
on the current gizmo, but we pass infalse
so the gizmo is disabled and inactive. ## Programming the gizmos themselves Okay, now we're about halfway there. All that is left is to make each of the gizmos themselves. The gizmos will be responsible for changing the position, rotation, and scale of the selected object when they are active. Let's start with theTranslate
gizmo first. ### Programming theTranslate
gizmo. Select theTranslate
gizmo (Example_Scene
->Editor_Controller
->Editor_Viewport
->Gizmos
->Translate
) and make a new script calledGizmo_Translate.gd
. Save it where you want, and then add the following code:
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 |
extends Spatial export (NodePath) var path_to_editor_viewport; var editor_viewport; export (String, "ALL", "X", "Y", "Z", "NONE") var active_gizmo_axis; # NOTE: Translate -> Translation -> Movement on the X, Y, and/or Z axes. const TRANSLATE_SPEED = 0.1; var left_button_down = false; func _ready(): editor_viewport = get_node(path_to_editor_viewport); update(false); active_gizmo_axis = "NONE"; func _input(event): if (active_gizmo_axis != "NONE"): if (event is InputEventMouseButton): if (event.button_index == BUTTON_LEFT): left_button_down = event.is_pressed(); elif (event is InputEventMouseMotion): if (left_button_down == true): if (editor_viewport.selected_physics_object != null): var prior_position = editor_viewport.selected_physics_object.global_transform.origin var current_position = prior_position; var mouse_delta = event.relative * TRANSLATE_SPEED; current_position += editor_viewport.gizmo_camera.global_transform.basis.y * -mouse_delta.y; current_position += editor_viewport.gizmo_camera.global_transform.basis.x * mouse_delta.x; if (active_gizmo_axis == "X"): current_position.y = prior_position.y; current_position.z = prior_position.z; elif (active_gizmo_axis == "Y"): current_position.x = prior_position.x; current_position.z = prior_position.z; elif (active_gizmo_axis == "Z"): current_position.x = prior_position.x; current_position.y = prior_position.y; editor_viewport.selected_physics_object.global_transform.origin = current_position; func update(var is_active): for child in get_children(): if (is_active == true): child.activate(); else: child.deactivate(); visible = is_active; func axis_set(new_axis): active_gizmo_axis = new_axis; |
Let's go through how this script works, starting with its class variables: *path_to_editor_viewport
: A exported NodePath to theEditor_Viewport
node. *editor_viewport
: A variable to hold theEditor_Viewport
node. *active_gizmo_axis
: A variable to hold the currently active axis. *NOTE: We are making this a exported variable so we can check its value in the debugger. *TRANSLATE_SPEED
: A variable to store the speed the translate gizmo moves the selected object at. You may need to adjust this depending on the sensitivity of your mouse, and how fast you want the gizmo to move the object. *left_button_down
: A variable to store whether the left mouse button is down or not. Next lets go through all of the functions, starting with_ready
. ### Going Through_ready
First we get theEditor_Viewport
node usingpath_to_editor_viewport
and we assign it toeditor_viewport
. Next we call theupdate
function and pass infalse
so it is disabled by default. Then we set the default gizmo axis toNONE
by settingactive_gizmo_axis
to"NONE"
. ### Going Through_input
!!! NOTE: Note This is where the majority of the code unique to each gizmo is, and where the gizmo actually does something. First, we check to make sureactive_gizmo_axis
is NOT set toNONE
. Then we check to see if the inputevent
is a InputEventMouseButton event, a event created when a mouse button is pressed/held/released. We then check to see if thebutton_index
in the event is equal toBUTTON_LEFT
, meaning the left button was pressed/held/released. If it is the left mouse button, then we setleft_button_down
toevent.is_pressed()
. Theis_pressed
function will return true if the button is pressed *and* when the button is held, which is exactly what we want. ____ If the inputevent
is not a InputEventMouseButton event, but is rather a InputEventMouseMotion event, then we check to see ifleft_button_down
istrue
. If it is, then we check to make sure there is a selected physics object by checking to see ifselected_physics_object
ineditor_viewport
is notnull
. If there is a select physics object, we then store the position of said object in a new variable calledprior_position
. We then make another new variable calledcurrent_position
and we assign it toprior_position
. Then we get the relative position of the mouse event (event.relative
) multiplied byTRANSLATE_SPEED
to a new variable calledmouse_delta
. Next we add the gizmo camera's relative Y axis (global_transform.basis.y
) multiplied bymouse_delta
on the negativey
axis. This will move the object up/down relative to the gizmo camera. We then do the same thing for the gizmo camera's relative X axis (global_transform.basis.x
) and we multiply it bymouse_delta
on thex
axis instead of the Y. This will move the object right/left relative to the gizmo camera. Then we check to see ifactive_gizmo_axis
is equal toX
. If it is, we setcurrent_position
on they
andz
axes to the stored position inprior_position
. This will make it wherecurrent_position
only has changed on thex
axis. If insteadactive_gizmo_axis
is equal toY
. If it is we setcurrent_position
on thex
andz
axes to the stored position inprior_position
. Ifactive_gizmo_axis
is set toZ
, then we setcurrent_position
on thex
andy
axes to the stored position inprior_position
. Finally, we change the selected physics object's position tocurrent_position
, applying the changes we made through the gizmo. ### Going Throughupdate
First we go through all of the children in the gizmo. !!! NOTE: Note We are going to assume that all children of the gizmos have theEditor_Gizmo_Collider
script attached to them. If the gizmo is supposed to be active,is_active
is equal totrue
, then we tell the gizmo collider to become active by calling theactivate
function on thechild
node. If the gizmo is not supposed to be active,is_active
is equal tofalse
, then we call thedeactivate
function on thechild
node. Finally, based on the value passed inis_active
, we change the visibility (visible
) of the gizmo. ### Going Throughaxis_set
All we are doing here is settingactive_gizmo_axis
to the passed in axis,new_axis
. ### Programming theRotate
gizmo. That's all we need to do for theTranslate
gizmo, so let's move on to theRotate
gizmo next! Select theRotate
node and make a new script calledGizmo_Rotate.gd
. Save it where you want, and then add the following code:
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 |
extends Spatial export (NodePath) var path_to_editor_viewport; var editor_viewport; export (String, "ALL", "X", "Y", "Z", "NONE") var active_gizmo_axis; var left_button_down = false; const LOOK_AT_ROTATION_SPEED = 4; const ROTATION_SPEED = 1; func _ready(): editor_viewport = get_node(path_to_editor_viewport); update(false); active_gizmo_axis = "NONE"; func _input(event): if (active_gizmo_axis != "NONE"): if (event is InputEventMouseButton): if (event.button_index == BUTTON_LEFT): left_button_down = event.is_pressed(); elif (event is InputEventMouseMotion): if (left_button_down == true): if (editor_viewport.selected_physics_object != null): var prior_rotation = editor_viewport.selected_physics_object.rotation_degrees; var current_rotation = prior_rotation; if (active_gizmo_axis == "ALL"): var mouse_delta = event.relative * LOOK_AT_ROTATION_SPEED; var raycast_from = editor_viewport.gizmo_camera.project_ray_origin(editor_viewport.get_mouse_position()) var obj_distance = (editor_viewport.gizmo_camera.global_transform.origin - editor_viewport.selected_physics_object.global_transform.origin).length() var raycast_to = raycast_from + editor_viewport.gizmo_camera.project_ray_normal(editor_viewport.get_mouse_position()) * obj_distance; editor_viewport.selected_physics_object.look_at(raycast_to, Vector3(0,1,0)); else: var mouse_delta = event.relative * ROTATION_SPEED; current_rotation += editor_viewport.selected_physics_object.global_transform.basis.y * mouse_delta.x; current_rotation += editor_viewport.selected_physics_object.global_transform.basis.x * -mouse_delta.y; if (active_gizmo_axis == "X"): current_rotation.y = prior_rotation.y; current_rotation.z = prior_rotation.z; elif (active_gizmo_axis == "Y"): current_rotation.x = prior_rotation.x; current_rotation.z = prior_rotation.z; elif (active_gizmo_axis == "Z"): current_rotation.x = prior_rotation.x; current_rotation.y = prior_rotation.y; editor_viewport.selected_physics_object.rotation_degrees = current_rotation; self.rotation_degrees = editor_viewport.selected_physics_object.rotation_degrees; func update(var is_active): for child in get_children(): if (is_active == true): child.activate(); else: child.deactivate(); visible = is_active; func axis_set(new_axis): active_gizmo_axis = new_axis; |
As you can see, all that has really changed is the class variables and the code within the_input
function, so **that is what we are going to be focusing on**. First, let's start with the new class variables: *LOOK_AT_ROTATION_SPEED
: The speed the rotation gizmo changes the rotation of the selected object at when using thelook_at
function. *ROTATION_SPEED
: The speed the rotation gizmo changes the rotation of the selected object at when constrained to a single axis. You may need to change this depending on the sensitivity of your mouse and how fast you want the gizmo to rotate the object. ### Going Through_input
As before, we check to see if the input event is a InputEventMouseButton, and updateleft_button_down
accordingly. Likewise, if the event is a InputEventMouseMotion event, we check to see if the left mouse button is down and if there is a physics object selected. This is exactly the same as theTranslate
gizmo. First we store the current rotation of the selected object in a new variable calledprior_rotation
. We then make a new variable calledcurrent_rotation
and assign it toprior_rotation
. _______ Next we check to see ifactive_gizmo_axis
is set toALL
. !!! NOTE: Note I have to admit, the rotation gizmo does not work like a 'normal' free rotation gizmo. This is because I could not find a way to get it working like the rotation gizmo in programs like Blender, mainly because of a lack of resources I could find (I tried!), so instead we're going to make a workable approximation usinglook_at
instead! First we assign the relative position of the mouse (event.relative
) multiplied byLOOK_AT_ROTATION_SPEED
to a new variable calledmouse_delta
. Then we get the origin of a raycast using the mouse position, just like in the Godot documentation on ray-casting. Next we calculate the distance from the gizmo camera to the selected physics object by subtracting the position of the selected physics object from the position of the camera and getting thelength
of the resulting vector. We store this distance in a variable calledobj_distance
. Then we calculate the end point of a 'raycast' using the same method we used for ray-casting prior, but this time we multiply it byobj_distance
so it is on the same relative plan as the selected physics object. Finally, we make the selected physics object look at the position stored withinraycast_to
using thelook_at
function. This will make the object rotate to look at the mouse. _____ If theactive_gizmo_axis
is NOT set toALL
, then we need to handle rotation differently. First, we assign the relative position of the mouse (event.relative
) multiplied byROTATION_SPEED
to a new variable calledmouse_delta
. Then we changecurrent_rotation
by adding the gizmo camera's relativey
axis (global_transform.basis.y
) multiplied bymouse_delta
on thex
axis. This will (sort of) rotate the object around the Y axis, but relative to the camera. Next we do the same thing but on the X axis. We add the gizmo camera's relativex
axis (global_transform.x
) multiplied bymouse_delta
on the negativey
axis tocurrent_rotation
. This will (sort of) rotate the object around the X axis, but relative to the camera. After that we check to see ifactive_gizmo_axis
is eitherX
,Y
, orZ
in separateif
checks. Based on whetheractive_gizmo_axis
is set to one of those values, we assign some of the axes incurrent_rotation
to the axes stored inprior_rotation
so the rotation only happens on one axis. Finally, we set the rotation on the selected physics object tocurrent_rotation
. _____ Regardless of which method we used to rotate the object, we then set the rotation gizmo's rotation to the same rotation as the selected object by setting therotation_degrees
in the rotation gizmo to therotation_degrees
ofselected_physics_object
. ### Programming theScale
gizmo That is all we need to do for the rotation gizmo, so let's move on to the scale gizmo next! Select theScale
node and make a new script calledGizmo_Scale.gd
. Save it where you want, and then add the following code:
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 |
extends Spatial export (NodePath) var path_to_editor_viewport; var editor_viewport; export (String, "ALL", "X", "Y", "Z", "NONE") var active_gizmo_axis; const SCALE_SPEED = 0.1; var left_button_down = false; func _ready(): editor_viewport = get_node(path_to_editor_viewport); update(false); active_gizmo_axis = "NONE"; func _input(event): if (active_gizmo_axis != "NONE"): if (event is InputEventMouseButton): if (event.button_index == BUTTON_LEFT): left_button_down = event.is_pressed(); elif (event is InputEventMouseMotion): if (left_button_down == true): if (editor_viewport.selected_physics_object != null): var prior_scale = editor_viewport.selected_physics_object.scale; var current_scale = prior_scale; var mouse_delta = event.relative * SCALE_SPEED; if (active_gizmo_axis == "ALL"): if (event.relative.x < 0 or -event.relative.y < 0): current_scale -= Vector3(1,1,1) * mouse_delta.length(); else: current_scale += Vector3(1,1,1) * mouse_delta.length(); else: current_scale += editor_viewport.gizmo_camera.global_transform.basis.y * -mouse_delta.y; current_scale += editor_viewport.gizmo_camera.global_transform.basis.x * mouse_delta.x; # If the active gizmo axis is the X axis... if (active_gizmo_axis == "X"): # Then reset current_scale on the Y and Z axis to the scale stored in prior_scale. current_scale.y = prior_scale.y; current_scale.z = prior_scale.z; # If the active gizmo axis is the Y axis... elif (active_gizmo_axis == "Y"): # Then reset current_scale on the X and Z axis to the scale stored in prior_scale. current_scale.x = prior_scale.x; current_scale.z = prior_scale.z; # If the active gizmo axis is the Z axis... elif (active_gizmo_axis == "Z"): # Then reset current_scale on the X and Y axis to the scale stored in prior_scale. current_scale.x = prior_scale.x; current_scale.y = prior_scale.y; # Set the select object's scale to current_scale. editor_viewport.selected_physics_object.scale = current_scale; func update(var is_active): for child in get_children(): if (is_active == true): child.activate(); else: child.deactivate(); visible = is_active; func axis_set(new_axis): active_gizmo_axis = new_axis; |
As you can see, all that has really changed is the class variables and the code within the_input
function, so **that is what we are going to be focusing on**. First, let's start with the new class variables: *SCALE_SPEED
: The speed the scale gizmo changes the scale of the selected object at. Depending on the sensitivity of your mouse and/or how fast you want the object to be scaled, you may need to change this. ### Going Through_input
As before, we check to see if the input event is a InputEventMouseButton, and updateleft_button_down
accordingly. Likewise, if the event is a InputEventMouseMotion event, we check to see if the left mouse button is down and if there is a physics object selected. This is exactly the same as theTranslate
gizmo. First we store the current scale of the selected object in a new variable calledprior_scale
. We then make a new variable calledcurrent_scale
and assign it toprior_scale
. _______ Next we check to see ifactive_gizmo_axis
is set toALL
. If it is, we then check to see if the relative mouse motion is less than zero on either axis by checking to see ifevent.relative.x
is less than zero orevent.relative.y
is less than zero. If the relative mouse motion is less than zero, then we subtractVector3(1,1,1)
multiplied by thelength
ofmouse_delta
. This will scale the object down evenly on all axes. If the relative mouse motion is more than zero, then we addVector3(1,1,1)
multiplied by thelength
ofmouse_delta
, which will scale the object up evenly on all axes. _____ If theactive_gizmo_axis
is NOT set toALL
, we instead add the gizmo camera's relative Y axis (global_transform.basis.y
) multiplied bymouse_delta
on the negativey
axis. This will scale the object up/down relative to the gizmo camera. Likewise, we then add the gizmo camera's relative X axis (global_transform.basis.x
) multiplied bymouse_delta
on thex
axis. This will scale the object left/right relative to the gizmo camera. Regardless of how we changedcurrent_scale
, we then check to see ifactive_gizmo_axis
is eitherX
,Y
, orZ
in separateif
checks. Based on whetheractive_gizmo_axis
is set to one of those values, we assign some of the axes incurrent_scale
to the axes stored inprior_scale
so the scaling only happens on one axis, if theactive_gizmo_axis
isX
,Y
, orZ
. Finally, we set the selected object'sscale
tocurrent_scale
. ### Programming TheSelect
Gizmo Alright, last one! Select theSelect
node and make a new script calledGizmo_Select
. Save it somewhere and then add the following code:
1 2 3 4 5 6 7 8 9 10 |
extends Spatial func _ready(): update(false); func update(var is_active): visible = is_active; func axis_set(new_axis): pass |
As you can see, because theSelect
gizmo does not do anything, it's code is really tiny! All we're doing in the_ready
function is making the gizmo invisible by default by calling theupdate
function and passingfalse
. In theupdate
function all we are doing is settingvisible
to the passed inis_active
variable, and for theaxis_set
function we just callpass
because there is nothing to do so we can ignore the function! Phew! Only one minor thing left to do, and then we're done! ## Setting everything up All that is left to do now is setup the NodePath variables we've exported in our scripts. For theTranslate
,Rotate
, andScale
nodes, set thePath To Editor Viewport
properties so they point to theEditor_Viewport
node. Then select theEditor_Viewport
node and assignPath To Texture
property to theGizmo_Viewport
TextRect node stored in theEditor_UI
node. For thePath To Editor Camera
property inEditor_Viewport
, assign it to theEditor_Camera
node. # Final NotesPhew! With all that done, go ahead and give the project a test! Now when you are in select mode, try selecting one of the objects. A yellow cube should appear. Then change to another mode to change it's position, rotation, or scale. You can limit the axis affected by the gizmo by dragging one of the colored handles instead of the yellow center. That concludes it for this tutorial! There are still loads of improvements that could be made. For example: * Add snapping to keep the position/rotation/scale on a grid. * Redo the rotation gizmo so it functions akin to the rotation gizmo in other programs. * Add a save/load system so changes made with the gizmos are saved. * Add support for more nodes! Using the AABB system, you maybe be able to select MeshInstance nodes and other nodes that have a AABB box. Hopefully this two part tutorial series will be helpful for you guys. I am using a similar system to this for my own game, and while I have changed several things, the gist of the gizmo system is almost exactly the same as what the tutorial here shows. If you have any questions, comments, or other feedback, let me know in the comments below! !!! WARNING: Warning If you ever get lost, be sure to read over the code again! If you have any questions, feel free to ask in the comments below! You can access the finished project for this tutorial series [right here](https://drive.google.com/open?id=1dtGVHF54I-Np7R-s40K_D-boFZ5CIMz0)! Please read LICENSE.html
orLICENSE.pdf
for details on attribution. **You must include attributions to use some/all of the assets provided**. See theLICENSE
files for details. !!! TIP: ThanksThanks for going through this tutorial! If you like what we do, **please consider buying one of [our paid games](https://randommomentania.com/paid-games/) or [products](https://randommomentania.com/other-products/) to help support RandomMomentania!** Have a suggestion for a future tutorial? Let us know either in the comments section below, or by following the instructions on our [Contact and FAQ](https://randommomentania.com/contact/) page! © RandomMomentania 2019
Amazing tutorial, thank you!
The rotation and scaling is really janky and doesn’t make any sense, but this gives a really great starting point for someone to start refining that.
Thank you for all the research and experimentation you have done to get this far, I don’t think I would have gotten to this point without these tutorials!