2D Scene in 3D World

Godot Version

4.3

Question

I have a 2D Character.
All it consists of is a Sprite2D and HUD elements that contain relevant data for the character.

Due to aesthetic reasons, I want to display this character in a 3D environment.

I figured out I could just display the 2D character, as is, on a Viewport Texture with the billboard flag enabled. It looks as I envisioned it to be.

However, any UI elements in the sub-viewport can’t be interacted with, and that’s a big no-no. I’ve tried viewport.push_input(...), but it doesn’t seem to be working. I can only assume that this is because the GUI elements are a child of a Node2D?

So, I was wondering if there’s a way to have dynamic, interactive GUI elements around a 3D object (if the character moves, so does the GUI).

Also, I have some concerns with how this might be implemented:

  1. In a 2D scene, I have a system in place that draws a line between two GUI elements by grabbing their position in respect to the screen. However, since this method involves get_viewport(), I’m afraid this might break functionality when using a SubViewport.

  2. It is… quite annoying positioning everything in a SubViewport.


    Anytime I add child to the Viewport and try to adjust it’s positioning, it takes me to the 2D renderer window but I can’t see anything. Not that big of a deal, just wondering if I’m missing something…

In my case, I made to where the keyboard is used to switch focus between the UI element:

I used push_input for this

func _input(event):
	if c_mode == 1:
		if subvterminal:
			subvterminal.push_input(event)

but you want to be able to click on the elements on the screen.

since the elements are in 2D and not perspective, you can put your 2D elements as children of the 3D objects (no need for viewports), and then position them using is_position_behind and unproject_position:

func _process(_delta):
	if cam.is_position_behind(unit.get_head_position()):
		visible = false
	else:
		visible = true
		var posish : Vector2 = cam.unproject_position(unit.get_head_position())
		posish.y -= y_shift
		posish.x -= hhwidth
		position = posish

edit: if you want the elements to be in perspective, you’ll have to create them using 3D nodes and Area3Ds for clicking. then:

func _ready() -> void:
	$Area3D.pressed.connect(unit_clicked_on)

func unit_clicked_on() -> void:
	print(unit_name, " clicked on ", name)

in my case I’m using a custom signal pressed:

class_name ClickInterface
extends Area3D

@onready var highlight : Sprite3D = $highlight

signal pressed

func _ready() -> void:
	input_event.connect(clicked_on)
	mouse_entered.connect(hover_over)
	mouse_exited.connect(hover_end)

func clicked_on(_cam : Node, event : InputEvent, _event_position : Vector3, _normal : Vector3, _shape_idx : int) -> void:
	if event is InputEventMouseButton:
		if event.is_pressed():
			if event.button_index == MOUSE_BUTTON_LEFT:
				pressed.emit()
	if event is InputEventScreenTouch:
		if event.is_pressed():
			pressed.emit()

func hover_over() -> void:
	highlight.visible = true
	if CombatManager.get_hover_mode() == 0:
		highlight.modulate = Color.RED

func hover_end() -> void:
	highlight.visible = false

You can make a 3D plane, then make the character a texture on the plane. Then you can add a regular collision shape to the plane to make it interactable.