Explorar el Código

Add a 3D waypoints demo

Co-authored-by: Aaron Franke <[email protected]>
Hugo Locurcio hace 6 años
padre
commit
54a8d37e6c

+ 19 - 0
3d/waypoints/README.md

@@ -0,0 +1,19 @@
+# 3D Waypoints
+
+This is an example of displaying GUI elements such as Labels in a 3D world,
+by projecting the 3D position onto the screen and displaying the GUI elements
+directly, instead of relying on viewports. This results in better readability
+and performance for use cases such as showing player names.
+
+Some waypoints showcased in the demo will also snap to the window borders when
+outside the player's view.
+
+No Viewport or Sprite3D nodes are used in this demo.
+
+Language: GDScript
+
+Renderer: GLES 2
+
+## Screenshots
+
+![Screenshot](screenshots/waypoints.png)

+ 43 - 0
3d/waypoints/camera.gd

@@ -0,0 +1,43 @@
+extends Camera
+
+const MOUSE_SENSITIVITY = 0.002
+const MOVE_SPEED = 0.6
+
+var rot = Vector3()
+var velocity = Vector3()
+
+
+func _ready():
+	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
+
+
+func _input(event):
+	# Mouse look (only if the mouse is captured).
+	if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
+		# Horizontal mouse look.
+		rot.y -= event.relative.x * MOUSE_SENSITIVITY
+		# Vertical mouse look.
+		rot.x = clamp(rot.x - event.relative.y * MOUSE_SENSITIVITY, -1.57, 1.57)
+		transform.basis = Basis(rot)
+
+	if event.is_action_pressed("toggle_mouse_capture"):
+		if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
+			Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
+		else:
+			Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
+
+
+func _process(delta):
+	var motion = Vector3(
+			Input.get_action_strength("move_right") - Input.get_action_strength("move_left"),
+			0,
+			Input.get_action_strength("move_back") - Input.get_action_strength("move_forward")
+	)
+
+	# Normalize motion to prevent diagonal movement from being
+	# `sqrt(2)` times faster than straight movement.
+	motion = motion.normalized()
+
+	velocity += MOVE_SPEED * delta * transform.basis.xform(motion)
+	velocity *= 0.85
+	translation += velocity

+ 9 - 0
3d/waypoints/default_env.tres

@@ -0,0 +1,9 @@
+[gd_resource type="Environment" load_steps=2 format=2]
+
+[sub_resource type="ProceduralSky" id=1]
+sun_latitude = 30.0
+sun_longitude = 40.0
+
+[resource]
+background_mode = 2
+background_sky = SubResource( 1 )

BIN
3d/waypoints/icon.png


+ 34 - 0
3d/waypoints/icon.png.import

@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.png"
+dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0

+ 139 - 0
3d/waypoints/main.tscn

@@ -0,0 +1,139 @@
+[gd_scene load_steps=17 format=2]
+
+[ext_resource path="res://camera.gd" type="Script" id=1]
+[ext_resource path="res://waypoint.tscn" type="PackedScene" id=2]
+[ext_resource path="res://noto_sans_regular.ttf" type="DynamicFontData" id=3]
+
+[sub_resource type="SpatialMaterial" id=1]
+albedo_color = Color( 0.6, 0.564706, 0.423529, 1 )
+
+[sub_resource type="CubeMesh" id=2]
+material = SubResource( 1 )
+size = Vector3( 16, 2, 16 )
+
+[sub_resource type="SpatialMaterial" id=3]
+albedo_color = Color( 0.788235, 0.788235, 0.788235, 1 )
+
+[sub_resource type="CubeMesh" id=4]
+material = SubResource( 3 )
+size = Vector3( 4, 1.5, 4 )
+
+[sub_resource type="SpatialMaterial" id=5]
+albedo_color = Color( 0.25098, 0.470588, 0.996078, 1 )
+
+[sub_resource type="CubeMesh" id=6]
+material = SubResource( 5 )
+size = Vector3( 1, 1, 1 )
+
+[sub_resource type="SpatialMaterial" id=7]
+albedo_color = Color( 0.435294, 0.917647, 0.380392, 1 )
+
+[sub_resource type="CubeMesh" id=8]
+material = SubResource( 7 )
+size = Vector3( 1, 1, 1 )
+
+[sub_resource type="SpatialMaterial" id=9]
+albedo_color = Color( 0.862745, 0.764706, 0.12549, 1 )
+
+[sub_resource type="CubeMesh" id=10]
+material = SubResource( 9 )
+size = Vector3( 1, 1, 1 )
+
+[sub_resource type="SpatialMaterial" id=11]
+albedo_color = Color( 0.996078, 0.266667, 0.25098, 1 )
+
+[sub_resource type="CubeMesh" id=12]
+material = SubResource( 11 )
+size = Vector3( 1, 1, 1 )
+
+[sub_resource type="DynamicFont" id=13]
+font_data = ExtResource( 3 )
+
+[node name="Main" type="Spatial"]
+
+[node name="Camera" type="Camera" parent="."]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 8 )
+fov = 75.0
+script = ExtResource( 1 )
+
+[node name="Ground" type="MeshInstance" parent="."]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1.5, 0 )
+mesh = SubResource( 2 )
+material/0 = null
+
+[node name="WhiteCube" type="MeshInstance" parent="."]
+mesh = SubResource( 4 )
+material/0 = null
+
+[node name="BlueCube" type="MeshInstance" parent="."]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0, -5 )
+mesh = SubResource( 6 )
+material/0 = null
+
+[node name="WaypointAnchor" type="Position3D" parent="BlueCube"]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0 )
+
+[node name="Waypoint" parent="BlueCube/WaypointAnchor" instance=ExtResource( 2 )]
+modulate = Color( 0.501961, 0.764706, 1, 1 )
+text = "Blue Waypoint"
+
+[node name="GreenCube" type="MeshInstance" parent="."]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0, 5 )
+mesh = SubResource( 8 )
+material/0 = null
+
+[node name="WaypointAnchor" type="Position3D" parent="GreenCube"]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0 )
+
+[node name="Waypoint" parent="GreenCube/WaypointAnchor" instance=ExtResource( 2 )]
+modulate = Color( 0.419608, 1, 0.427451, 1 )
+text = "Green Waypoint"
+
+[node name="YellowCube" type="MeshInstance" parent="."]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 0, 5 )
+mesh = SubResource( 10 )
+material/0 = null
+
+[node name="WaypointAnchor" type="Position3D" parent="YellowCube"]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0 )
+
+[node name="Waypoint" parent="YellowCube/WaypointAnchor" instance=ExtResource( 2 )]
+modulate = Color( 1, 0.992157, 0.419608, 1 )
+text = "Yellow Waypoint (non-sticky)"
+sticky = false
+
+[node name="RedCube" type="MeshInstance" parent="."]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 0, -5 )
+mesh = SubResource( 12 )
+material/0 = null
+
+[node name="WaypointAnchor" type="Position3D" parent="RedCube"]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0 )
+
+[node name="Waypoint" parent="RedCube/WaypointAnchor" instance=ExtResource( 2 )]
+modulate = Color( 1, 0.466667, 0.427451, 1 )
+text = "Red Waypoint"
+
+[node name="DirectionalLight" type="DirectionalLight" parent="."]
+transform = Transform( -0.642788, -0.383022, 0.663414, 0, 0.866025, 0.5, -0.766044, 0.321394, -0.556671, 0, 6, -9 )
+light_energy = 0.9
+shadow_enabled = true
+shadow_bias = 0.06
+directional_shadow_blend_splits = true
+directional_shadow_normal_bias = 0.0
+directional_shadow_bias_split_scale = 0.7
+directional_shadow_max_distance = 60.0
+
+[node name="Label" type="Label" parent="."]
+margin_left = 10.0
+margin_top = 10.0
+margin_right = 50.0
+margin_bottom = 24.0
+custom_fonts/font = SubResource( 13 )
+custom_colors/font_color_shadow = Color( 0, 0, 0, 0.501961 )
+custom_constants/shadow_offset_x = 1
+custom_constants/shadow_offset_y = 1
+text = "Press Esc or F10 to toggle mouse capture"
+__meta__ = {
+"_edit_use_anchors_": false
+}

BIN
3d/waypoints/noto_sans_regular.ttf


+ 77 - 0
3d/waypoints/project.godot

@@ -0,0 +1,77 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+;   [section] ; section goes between []
+;   param=value ; assign values to parameters
+
+config_version=4
+
+_global_script_classes=[  ]
+_global_script_class_icons={
+
+}
+
+[application]
+
+config/name="3D Waypoints"
+config/description="This is an example of displaying GUI elements such as Labels in a 3D world, without relying on viewports. This results in better readability and performance for use cases such as showing player names."
+run/main_scene="res://main.tscn"
+config/icon="res://icon.png"
+
+[display]
+
+window/stretch/mode="2d"
+window/stretch/aspect="expand"
+
+[input]
+
+move_forward={
+"deadzone": 0.5,
+"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":87,"unicode":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":90,"unicode":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777232,"unicode":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":12,"pressure":0.0,"pressed":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":-1.0,"script":null)
+ ]
+}
+move_back={
+"deadzone": 0.5,
+"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":83,"unicode":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777234,"unicode":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":13,"pressure":0.0,"pressed":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":1.0,"script":null)
+ ]
+}
+move_left={
+"deadzone": 0.51,
+"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":65,"unicode":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":81,"unicode":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777231,"unicode":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":14,"pressure":0.0,"pressed":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":-1.0,"script":null)
+ ]
+}
+move_right={
+"deadzone": 0.5,
+"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":68,"unicode":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777233,"unicode":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":15,"pressure":0.0,"pressed":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":1.0,"script":null)
+ ]
+}
+toggle_mouse_capture={
+"deadzone": 0.5,
+"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777253,"unicode":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777217,"unicode":0,"echo":false,"script":null)
+ ]
+}
+
+[rendering]
+
+quality/driver/driver_name="GLES2"
+vram_compression/import_etc=true
+vram_compression/import_etc2=false
+quality/filters/msaa=2
+environment/default_environment="res://default_env.tres"

+ 0 - 0
3d/waypoints/screenshots/.gdignore


BIN
3d/waypoints/screenshots/waypoints.png


+ 116 - 0
3d/waypoints/waypoint.gd

@@ -0,0 +1,116 @@
+extends Control
+
+# Some margin to keep the marker away from the screen's corners.
+const MARGIN = 8
+
+onready var camera = get_viewport().get_camera()
+onready var parent = get_parent()
+onready var label = $Label
+onready var marker = $Marker
+
+# The waypoint's text.
+export var text = "Waypoint" setget set_text
+
+# If `true`, the waypoint sticks to the viewport's edges when moving off-screen.
+export var sticky = true
+
+
+func _ready() -> void:
+	self.text = text
+
+	if not parent is Spatial:
+		push_error("The waypoint's parent node must inherit from Spatial.")
+
+
+func _process(_delta):
+	var parent_translation = parent.global_transform.origin
+	var camera_transform = camera.global_transform
+	var camera_translation = camera_transform.origin
+
+	# We would use "camera.is_position_behind(parent_translation)", except
+	# that it also accounts for the near clip plane, which we don't want.
+	var is_behind = camera_transform.basis.z.dot(parent_translation - camera_translation) > 0
+
+	# Fade the waypoint when the camera gets close.
+	var distance = camera_translation.distance_to(parent_translation)
+	modulate.a = clamp(range_lerp(distance, 0, 2, 0, 1), 0, 1 )
+
+	var unprojected_position = camera.unproject_position(parent_translation)
+	# `get_size_override()` will return a valid size only if the stretch mode is `2d`.
+	# Otherwise, the viewport size is used directly.
+	var viewport_base_size = (
+			get_viewport().get_size_override() if get_viewport().get_size_override() > Vector2(0, 0)
+			else get_viewport().size
+	)
+
+	if not sticky:
+		# For non-sticky waypoints, we don't need to clamp and calculate
+		# the position if the waypoint goes off screen.
+		rect_position = unprojected_position
+		visible = not is_behind
+		return
+
+	# We need to handle the axes differently.
+	# For the screen's X axis, the projected position is useful to us,
+	# but we need to force it to the side if it's also behind.
+	if is_behind:
+		if unprojected_position.x < viewport_base_size.x / 2:
+			unprojected_position.x = viewport_base_size.x - MARGIN
+		else:
+			unprojected_position.x = MARGIN
+
+	# For the screen's Y axis, the projected position is NOT useful to us
+	# because we don't want to indicate to the user that they need to look
+	# up or down to see something behind them. Instead, here we approximate
+	# the correct position using difference of the X axis Euler angles
+	# (up/down rotation) and the ratio of that with the camera's FOV.
+	# This will be slightly off from the theoretical "ideal" position.
+	if is_behind or unprojected_position.x < MARGIN or \
+			unprojected_position.x > viewport_base_size.x - MARGIN:
+		var look = camera_transform.looking_at(parent_translation, Vector3.UP)
+		var diff = angle_diff(look.basis.get_euler().x, camera_transform.basis.get_euler().x)
+		unprojected_position.y = viewport_base_size.y * (0.5 + (diff / deg2rad(camera.fov)))
+
+	rect_position = Vector2(
+			clamp(unprojected_position.x, MARGIN, viewport_base_size.x - MARGIN),
+			clamp(unprojected_position.y, MARGIN, viewport_base_size.y - MARGIN)
+	)
+
+	label.visible = true
+	rect_rotation = 0
+	# Used to display a diagonal arrow when the waypoint is displayed in
+	# one of the screen corners.
+	var overflow = 0
+
+	if rect_position.x <= MARGIN:
+		# Left overflow.
+		overflow = -45
+		label.visible = false
+		rect_rotation = 90
+	elif rect_position.x >= viewport_base_size.x - MARGIN:
+		# Right overflow.
+		overflow = 45
+		label.visible = false
+		rect_rotation = 270
+
+	if rect_position.y <= MARGIN:
+		# Top overflow.
+		label.visible = false
+		rect_rotation = 180 + overflow
+	elif rect_position.y >= viewport_base_size.y - MARGIN:
+		# Bottom overflow.
+		label.visible = false
+		rect_rotation = -overflow
+
+
+func set_text(p_text):
+	text = p_text
+
+	# The label's text can only be set once the node is ready.
+	if is_inside_tree():
+		label.text = p_text
+
+
+static func angle_diff(from, to):
+	var diff = fmod(to - from, TAU)
+	return fmod(2.0 * diff, TAU) - diff

+ 1 - 0
3d/waypoints/waypoint.svg

@@ -0,0 +1 @@
+<svg height="128" viewBox="0 0 33.866666 33.866666" width="128" xmlns="http://www.w3.org/2000/svg"><path d="m2.5 3.6166886h28.8667346l-14.433367 27.0625224z" fill="#fff" stroke="#000" stroke-width="3"/></svg>

+ 34 - 0
3d/waypoints/waypoint.svg.import

@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/waypoint.svg-de0ee5a99654d0ef48e907a9ea3ae741.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://waypoint.svg"
+dest_files=[ "res://.import/waypoint.svg-de0ee5a99654d0ef48e907a9ea3ae741.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=true
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0

+ 41 - 0
3d/waypoints/waypoint.tscn

@@ -0,0 +1,41 @@
+[gd_scene load_steps=5 format=2]
+
+[ext_resource path="res://waypoint.gd" type="Script" id=1]
+[ext_resource path="res://waypoint.svg" type="Texture" id=2]
+[ext_resource path="res://noto_sans_regular.ttf" type="DynamicFontData" id=3]
+
+[sub_resource type="DynamicFont" id=1]
+font_data = ExtResource( 3 )
+
+[node name="Waypoint" type="Control"]
+script = ExtResource( 1 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Label" type="Label" parent="."]
+margin_left = -200.0
+margin_top = -40.0
+margin_right = 200.0
+margin_bottom = -17.0
+custom_fonts/font = SubResource( 1 )
+custom_colors/font_color_shadow = Color( 0, 0, 0, 0.501961 )
+custom_constants/shadow_offset_x = 1
+custom_constants/shadow_offset_y = 1
+text = "Waypoint"
+align = 1
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Marker" type="TextureRect" parent="."]
+margin_left = -8.0
+margin_top = -16.0
+margin_right = 120.0
+margin_bottom = 112.0
+rect_scale = Vector2( 0.125, 0.125 )
+texture = ExtResource( 2 )
+__meta__ = {
+"_edit_use_anchors_": false,
+"_editor_description_": "An high-resolution texture is used and scaled down so the demo looks good at higher resolutions."
+}