diff --git a/3d/waypoints/README.md b/3d/waypoints/README.md new file mode 100644 index 00000000..e14f41f2 --- /dev/null +++ b/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) diff --git a/3d/waypoints/camera.gd b/3d/waypoints/camera.gd new file mode 100644 index 00000000..859de512 --- /dev/null +++ b/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 diff --git a/3d/waypoints/default_env.tres b/3d/waypoints/default_env.tres new file mode 100644 index 00000000..bb1b81d4 --- /dev/null +++ b/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 ) diff --git a/3d/waypoints/icon.png b/3d/waypoints/icon.png new file mode 100644 index 00000000..a980ee0f Binary files /dev/null and b/3d/waypoints/icon.png differ diff --git a/3d/waypoints/icon.png.import b/3d/waypoints/icon.png.import new file mode 100644 index 00000000..96cbf462 --- /dev/null +++ b/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 diff --git a/3d/waypoints/main.tscn b/3d/waypoints/main.tscn new file mode 100644 index 00000000..c4890b38 --- /dev/null +++ b/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 +} diff --git a/3d/waypoints/noto_sans_regular.ttf b/3d/waypoints/noto_sans_regular.ttf new file mode 100644 index 00000000..694273d5 Binary files /dev/null and b/3d/waypoints/noto_sans_regular.ttf differ diff --git a/3d/waypoints/project.godot b/3d/waypoints/project.godot new file mode 100644 index 00000000..f62f7cdf --- /dev/null +++ b/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" diff --git a/3d/waypoints/screenshots/.gdignore b/3d/waypoints/screenshots/.gdignore new file mode 100644 index 00000000..e69de29b diff --git a/3d/waypoints/screenshots/waypoints.png b/3d/waypoints/screenshots/waypoints.png new file mode 100644 index 00000000..1c0dbaa2 Binary files /dev/null and b/3d/waypoints/screenshots/waypoints.png differ diff --git a/3d/waypoints/waypoint.gd b/3d/waypoints/waypoint.gd new file mode 100644 index 00000000..335eb8a7 --- /dev/null +++ b/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 diff --git a/3d/waypoints/waypoint.svg b/3d/waypoints/waypoint.svg new file mode 100644 index 00000000..6a5a1b0a --- /dev/null +++ b/3d/waypoints/waypoint.svg @@ -0,0 +1 @@ + diff --git a/3d/waypoints/waypoint.svg.import b/3d/waypoints/waypoint.svg.import new file mode 100644 index 00000000..01515739 --- /dev/null +++ b/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 diff --git a/3d/waypoints/waypoint.tscn b/3d/waypoints/waypoint.tscn new file mode 100644 index 00000000..d57817d8 --- /dev/null +++ b/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." +}