• 3D
  • Making Doom 3 Style Computers and Terminals

In Doom 3, the intractable terminals are all 3D objects that the player interacts with them naturally similar to real-life. A video is worth sixty thousand words per second. Here is a short demonstration of it:

I really love recreating such thing in Godot. But my question is how can I do it? The first idea that comes to mind is to create the UI instance and render it to the world using a 3D sprite. But how would I pass the mouse events to the UI instance? I'd love to hear your thoughts and opinions on this.

That is achieved through render targets or realtime texures or as we know them in godot: Viewports. You should be able to raycast into the virtual screen to interact with whats within it.

The Gui in 3D demo project on the Godot Demo repository might be a good reference on how to use Viewports this way with normal Control-based UI nodes :smile:

That's exactly what I was looking for. Thank you that helps a lot.

I've been experimenting with this and I've hit a roadblock. I'm using the code used in the 3D GUI demo and I have encountered two problems. 1. If there are any active UI nodes casting to the main camera, it won't work. I'm not quite sure why is this happening. Could it be that the UI is blocking the raycast? 2. On my player character I use Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED). I assumed what this is doing is centering the cursor and hiding it. But it stops this method from working. I tried a trick by setting the mouse mode to VISIBLE and centering the mouse manually by executing this snippet every frame: var center_coordinates = get_viewport().size/2 get_viewport().warp_mouse(center_coordinates) But now I can't look around as my cursor is being strong armed. I'm at a loss. How can I overcome this issue?

@Leakbang said: I've been experimenting with this and I've hit a roadblock. I'm using the code used in the 3D GUI demo and I have encountered two problems. 1. If there are any active UI nodes casting to the main camera, it won't work. I'm not quite sure why is this happening. Could it be that the UI is blocking the raycast?

Hmm, not sure. I think it is probably because of the use of _unhandled_input and so the UI nodes are catching and processing the input before it gets to the 3D GUI, but that's just a guess on my part. You could quickly check by changing _unhandled_input to just _input and see if it works.

  1. On my player character I use Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED). I assumed what this is doing is centering the cursor and hiding it. But it stops this method from working. I tried a trick by setting the mouse mode to VISIBLE and centering the mouse manually by executing this snippet every frame: ... But now I can't look around as my cursor is being strong armed. I'm at a loss. How can I overcome this issue?

What I would try doing is changing the code that passes the event to the Viewport so that instead of reading the mouse position, it always just sends the center of the screen.

This code is where I think you would need to make the change: Line 62 in gui_3d.gd. I think instead of event.global_position you would instead want to use get_viewport().size/2. I think then it should work with the cursor being captured.

Ok so I managed to it make fully work. I encountered a lot of other issues along the way, but here is my final setup: This is main code for the terminal screens. Any terminal with a screen will simply have this code loaded in as the first thing using entends and then other functionalities can be easily added on top.

extends Spatial

export (NodePath) var cursor_sprite

var quad_mesh_size
var is_mouse_inside = false
var is_mouse_held = false
var last_mouse_pos3D = null
var last_mouse_pos2D = null
var virutal_cursor = null

onready var node_viewport = $Viewport
onready var node_quad = $Quad
onready var node_area = $Quad/Area

func _ready():
	pass


func _process(_delta):
	pass


func _input(event):
	var is_mouse_event = false
	for mouse_event in [InputEventMouseButton, InputEventMouseMotion, InputEventScreenDrag, InputEventScreenTouch]:
		if event is mouse_event:
			is_mouse_event = true
			break
			
	if is_mouse_event and (is_mouse_inside or is_mouse_held):
		handle_mouse(event)
	elif not is_mouse_event:
		node_viewport.input(event)


func handle_mouse(event):
	quad_mesh_size = node_quad.mesh.size

	if event is InputEventMouseButton or event is InputEventScreenTouch:
		is_mouse_held = event.pressed
	var mouse_pos3D = find_mouse()

	if is_mouse_inside:
		mouse_pos3D = node_area.global_transform.affine_inverse() * mouse_pos3D
		last_mouse_pos3D = mouse_pos3D
	else:
		mouse_pos3D = last_mouse_pos3D
		if mouse_pos3D == null:
			mouse_pos3D = Vector3.ZERO


	var mouse_pos2D = Vector2(mouse_pos3D.x, -mouse_pos3D.y)
	
	
	mouse_pos2D.x += quad_mesh_size.x / 2
	mouse_pos2D.y += quad_mesh_size.y / 2

	mouse_pos2D.x = mouse_pos2D.x / quad_mesh_size.x
	mouse_pos2D.y = mouse_pos2D.y / quad_mesh_size.y


	mouse_pos2D.x = mouse_pos2D.x * node_viewport.size.x
	mouse_pos2D.y = mouse_pos2D.y * node_viewport.size.y

	event.position = mouse_pos2D
	event.global_position = mouse_pos2D

	last_mouse_pos2D = mouse_pos2D
	get_node(cursor_sprite).rect_position = last_mouse_pos2D 
	
	node_viewport.input(event)


func find_mouse():
	var result = virutal_cursor
	if result:
		return result
	else:
		return null

The player has a raycast casting to where it's looking at. This code is running every frame to check if the player is looking at a screen or not.

func _physics_process(delta):
	Target = get_collider();
	if get_collider() and Target.owner.is_in_group("Terminal"):
		Target.owner.is_mouse_inside = true
		var cursor = get_collision_point ()
		Target.owner.virutal_cursor = cursor

This will essentially let the terminal know that the player is looking at it. I did some stress testing and I couldn't get it to break, so I'd reckon that this is a fairly robust system. The only problem that I found is the responsiveness of the screens. The cursor lags a tiny bit. But since it's updating it's position every frame I don't know how to fix the jittery motion. It would be great if the movement of the cursor can be somehow smoothed out (Line 70). Thank you a lot for helping me with this! I hope this could help others too.

Awesome, I'm glad you got it working! Thanks for sharing the solution so others can benefit to :smile:

Thanks for following up with the solution. I think when you use viewports for mouse movement, there is always a 1 frame delay. At least this is what I noticed when I tried to make a "fake" mouse cursor (by setting a sprite to the mouse position). Because it is getting the position from the last frame.

Something kept bugging me and I realized a huge issue with this current setup. It's aliasing, Here's an example: I'm only standing about a meter away from this terminal and it looks horrible. The viewport node has the option to toggle FXAA and MSAA on, but I didn't feel a difference even at 16x MSAA. I also tried increasing the MSAA value globally however, it only started to look better at 16x.... which is horrible. Even at point blank, there is noticeable aliasing. So how can I overcome this terrible aliasing? Is there something wrong with the setup or is this a problem with the engine?

My guess is that it's the lack of mip-maps that are causing the issue. You might be able to enable them by setting the flags of the texture to use mip maps, but it might not be supported with ViewportTextures, at least from what I can find on GitHub.

Viewports also have a size. You can make the size bigger, that may help, but you'll still need filtering and mipmaps enabled or it may look worse.

Personally I'd utilize a proximity solution: Don't display the screen at all if the player is too far away from the screen. Also allows you to actively update a viewport only when its about to be interacted with.

Yes, you can also save the viewport to a texture (when not interacting with it) and that can definitely have filtering and mipmaps.

Yeah Godot's not generating mipmaps for Viewport Textures. I tried some tricks such as getting a snapshot of the viewport and manually generating mipmaps for that but none of them worked. These were all mentioned by other people in the Github issue mentioned by TwistedTwigleg.

@cybereality said: Viewports also have a size. You can make the size bigger, that may help, but you'll still need filtering and mipmaps enabled or it may look worse.

I tried that but that doesn't make a difference.

@Megalomaniak said: Personally I'd utilize a proximity solution: Don't display the screen at all if the player is too far away from the screen. Also allows you to actively update a viewport only when its about to be interacted with.

That makes a lot of sense as it also helps with optimzation, however the issue persists and is visible at close range.

The good news is that the dev team has noticed this issue and they just added the Github issue to the 3.5 milestone, which is great! So hopefully, they can finally fix this issue.

So I did find about this addon that adds planar reflections to godot (https://github.com/SIsilicon/Godot-Planar-Reflection-Plugin). I noticed it uses viewports and it seems that it, somehow generates mipmaps. The reflections look pretty good smooth (or blurry) and don't look pixelated. Sadly I couldn't get it to work for my viewport. I tried copying and integrating some parts of the code but I couldn't replicate the effect. Maybe, the camera makes a difference.

The viewport node has the option to toggle FXAA and MSAA on, but I didn't feel a difference even at 16x MSAA. I also tried increasing the MSAA value globally however, it only started to look better at 16x.... which is horrible.

For future reference, MSAA only affects polygon edges, not texture or shader-induced aliasing. 16× MSAA incidentally makes it less noticeable because most graphics drivers implement it as a mixture of 2× SSAA and 8× MSAA. However, it's very slow and causes various bugs that are difficult to fix on the application side.

FXAA can hide some shader-induced aliasing given its post-processing-based nature, but it's not intended to be a high-end antialiasing solution.