How do I scale 3D nodes along one direction with the position not staying in the center?

Godot Version

4.5.1

Question

When you scale nodes in Godot, the position of the node stays in the center, and the scale on the axis scales outwards. In other engines and software, there are options to be able to scale on one axis in one direction, for example, scaling a node only on +Z, etc. It’s hard to describe, thus I am asking here because my research efforts haven’t gone right. This kind of functionality I am looking for is present in CSGBox nodes, however, for my purposes, I cannot use a CSGBox. I need all Node3Ds to be scalable not from the center, but from the origin point of the axis you choose. The best way I can describe it is stretching a Node3D or pulling a Node3D towards a certain direction.

I tried to create an Editor Plugin, however everything I tried and every code I tried for an EditorNode3DGizmoPlugin that I could find seemingly did not work and had a multitude of issues, furthermore it was just really really really complicated, not much info or tutorials about it. I searched for github projects for examples, but nothing worked, there were always errors like “Condition !material.has(p.name) is true Returning ” or something vaguely along those lines, spamming my console.

I also tried to use _notification in a tool script to run code everytime I scaled an object. I made a var for the last scale variable and detected if the new scale was not the same as the last scale, then position -= (last_scale - scale) * 0.5 to set position based on the offset, however this did not work as the position kept snapping back to the center even though I was changing it. A lot of bugs and unintended behaviour from the looks of it, for such a simple feature to want.

I was a bit frustrated so I don’t have any code examples, I’m just asking for help as a last resort. It seems as if the answer is just around the corner, but everything I try unfortunately fails. It would actually be cool to have a working custom gizmo that didn’t scream errors at me or spam lines everywhere for some reason, or say that it cannot find a material even though it clearly has it. If anyone is reading this, please let me know if you have a solution to this, thanks.

What’s the origin point of an axis? Isn’t that the same as the center.

Allow me to attempt to demonstrate using screenshots. In Godot when you scale things, it scales keeping the position in the centre of the object.

I want to scale from where you start scaling the object. Not extruding a face, but scaling the object whilst maintaining it’s position from where you scale it, similarly to how you’d scale something in Roblox, or something like that.

Just make it to a child of another node3D, place the object relative to it, with the node3D at the location you want to scale from. Than scale the node 3D.

But scaling is a critical operation, espec. non uniform scale. It can mess with all kind of calculations, like physics, AutoLOD etc.

You want to move one side of the bounding box? From strictly linear transformation stand point this is compound of scaling and moving. What types of objects do you need to manipulate like that? Meshes?

I want to manipulate and scale any Node3D object, in the editor, whilst also moving the object so that the position of the object stays where it was in relation to where I started scaling it, as opposed to keeping the object in the centre as it scales which is the default behaviour. It should be simple to do, however it seems quite hard. Certain programs however like Roblox have this functionality and I want to see if Godot can do the same, and I’m willing to try and implement it, but I do not understand the EditorNode3DGizmoPlugin at all. Everything I have looked for in regards to it makes no sense and spits out errors.

I don’t have any experience with Roblox. Are we talking about something like Blender’s “Scale Cage”? Afaik, Godot still doesn’t have this type of scaling tool.

You can’t scale any Node3D like this because Node3D doesn’t have a bounding box. Node3D only implements a standard transformation matrix. Without a bounding box, this type of interaction doesn’t make much sense. The first class downstream Node3D hierarchy that implements a bounding box is VisualInstance3D.

In general, you’ll have to implement a gizmo and then calculate and apply scale and translation depending on gizmo interaction.

Have you searched if there already is a plugin that does it or if someone made a feature request?

Scale Cage? Looking it up yeah that seems to be what I need. I didn’t even know that existed in Blender because of how you access it. My use case behind asking this is for my own custom level editing plugin of which I’ve easily been able to make custom docks and stuff like that, but when it comes to level editing, I want the ability to scale specific objects in that manner, as it’s annoying to scale from the center, and then have to manually re-adjust the position, if that makes sense.

The reason why I’m scaling a Node3D is because I have a custom node that extends Node3D, I then put a MeshInstance3D inside that node to render a mesh, which can be a model, a cube, a sphere, anything really, and give it properties. This is just for personal use, making a makeshift level editor using Godot, I know there are alternatives like Blender or Trenchbroom but this is annoying to have to switch from Godot to another program, so I want to add custom functionality directly into Godot myself. So far, so good, but when it comes to GIzmos, I’m struggling. Specifically because I’ve never coded anything gizmo related before, it’s so arduous.

Not knowing much about this, I have to go searching for examples, there exists an example Gizmo plugin on Github that seems to sort of work for my needs, GitHub - arnemileswinter/godot-cube-gizmo-demo: This project serves as an example for creating and using Gizmo Plugins in Godot 4.1. however there are bugs with this. Earlier it wasn’t working at all, now this example is working for some reason, so I can learn from it and see how I can edit it to suit my needs, but currently there are bugs to this demo.

The Gizmo stays visible even when you haven’t clicked on it, and it seems to stretch off into infinity when you click on it for some reason.

But, it’s a start. I’ll likely figure something out with this.

Looks like the infinity stretch bug was already fixed in a pull request.
Maybe now this is easier than I thought, all I need to do is make the gizmo adjust the scale of the object and move it according to it’s position.

Then your custom node will have to maintain a custom bounding box.

So there are parts of Godot that you can do this with. The one that comes to mind is the BoxShape3D shape for a CollisionShape3D. You can alter just one side. But what it’s doing under the hood is actually scaling and moving the position of the object. So if you wanted to make an editor plugin, I’d take a look at those controls then copy that implementation.

I have made some progress, albeit this is a shoddy prototype. The raycast to the world code made it glitch out and freak the hell out so I’ll fix that later. I’m new to the site so I can’t show a video though, so I guess I’ll just make a text post about it.

I used the code I found on github to create a custom gizmo with 6 sides, the scale and position updates exactly how I want it to, however it’s currently in a prototype state where it’s using my mouse directly and not a raycasted position in the world because the code for that in the repo seems to be buggy as hell with it, so I will fix it later.

1 Like

I have solved my issue. The object now scales in the way I want it to, here’s my code:

@tool

extends EditorNode3DGizmoPlugin

func _has_gizmo(node):
	return node is MyCustomNode

func _get_gizmo_name():
	return "SampleCube"

func _init():
	create_material("main", Color(1.0, 1.0, 1.0, 1.0))
	create_handle_material("handles")

func _redraw(gizmo):
	gizmo.clear()
	var n = gizmo.get_node_3d()
	
	var handles = PackedVector3Array()
	handles.push_back(Vector3(0.5, 0, 0)) # x-handle, handle_id 0
	handles.push_back(Vector3(0, 0.5, 0)) # y-handle, handle_id 1
	handles.push_back(Vector3(0, 0, 0.5)) # z-handle, handle_id 2
	handles.push_back(Vector3(-0.5, 0, 0)) # -x-handle, handle_id 3
	handles.push_back(Vector3(0, 0, -0.5)) # -z-handle, handle_id 4
	handles.push_back(Vector3(0, -0.5, 0)) # y-handle, handle_id 5
	gizmo.add_handles(handles, get_material("handles", gizmo), [])

func _get_handle_name(gizmo, handle_id, secondary):
	match handle_id:
		0: return "x"
		1: return "y"
		2: return "z"
		3: return "-x"
		4: return "-z"
		5: return "-y"

func _get_handle_value(gizmo, handle_id, secondary):
	var n = gizmo.get_node_3d()
	match handle_id:
		0: return n.scale.x
		1: return n.scale.y
		2: return n.scale.z
		3: return -n.scale.x
		4: return -n.scale.z
		5: return -n.scale.y

func _set_handle(gizmo: EditorNode3DGizmo, handle_id: int, secondary: bool, camera: Camera3D, screen_pos: Vector2):
	var n = gizmo.get_node_3d()
	
	var plane : Plane;
	match handle_id:
		0: plane = Plane.PLANE_XY
		1: plane = Plane.PLANE_XY
		2: plane = Plane.PLANE_YZ
		3: plane = Plane.PLANE_XY
		4: plane = Plane.PLANE_YZ
		5: plane = Plane.PLANE_XY
	plane = n.global_transform * plane
	
	var ray_from = camera.project_ray_origin(screen_pos)
	var ray_to = camera.project_ray_normal(screen_pos)
	var intersection = plane.intersects_ray(ray_from, ray_to)
	var drag_to = n.global_transform.affine_inverse()
	if intersection != null:
		drag_to *= intersection
	var last_scale = n.scale
	match handle_id:
		0: 
			n.scale.x *= drag_to.x + 0.5
		1: 
			n.scale.y *= drag_to.y + 0.5
		2: 
			n.scale.z *= drag_to.z + 0.5
		3: 
			n.scale.x *= drag_to.x - 0.5
		4: 
			n.scale.z *= drag_to.z - 0.5
		5: 
			n.scale.y *= drag_to.y - 0.5
	n.scale = abs(n.scale)
	match handle_id:
		0:
			n.position -= (last_scale - n.scale) * 0.5
		1:
			n.position -= (last_scale - n.scale) * 0.5
		2:
			n.position -= (last_scale - n.scale) * 0.5
		3:
			n.position += (last_scale - n.scale) * 0.5
		4:
			n.position += (last_scale - n.scale) * 0.5
		5:
			n.position += (last_scale - n.scale) * 0.5
	n.update_gizmos()

It seems a bit messy, maybe unpolished, I screwed up the ordering which should be minus X, Y, Z but is instead minus x, z, y, but it works, and that’s amazing. There does appear to be a bug with this still and thats when you rotate the object the scaling messes up but I’ll figure that out somehow.

1 Like

What if pivot is not in the center of the bounding box?