Add a graphics tablet input demo (#1162)

This commit is contained in:
Hugo Locurcio
2025-02-10 18:17:34 +01:00
committed by GitHub
parent 0d46333c4e
commit fdb2f50a20
8 changed files with 558 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
# Graphics Tablet Input
A demo showing how to use graphics tablet input in Godot. Godot has full support
for pressure sensitivity, tilt and pen inversion (i.e. checking whether the
"eraser" is being used on the pen). Note that some platforms and tablets
may not support reporting tilt.
Input accumulation and V-Sync are disabled by default in this demo to minimize
input lag and get crisp lines (even at low FPS). This makes for the most
responsive drawing experience possible. You can toggle them in the sidebar to
see the difference it makes. Note that on Android, iOS and Web platforms, V-Sync
is forced at a system level and cannot be disabled.
Lines are drawn using the Line2D node. Every time you lift off the open and start a new
line, a new Line2D node is created. Line antialiasing is provided by enabling 2D MSAA
in the Project Settings.
Mouse input can also be used to draw in this demo, but using a tablet is recommended.
> [!NOTE]
>
> If you experience issues on Windows, try changing the tablet driver in the Project
> Settings to **wintab** instead of the default **winink**. Also, try changing your
> tablet's input mode from relative to absolute mode.
Language: GDScript
Renderer: Mobile
## Screenshots
![Screenshot](screenshots/graphics_tablet_input.webp)

View File

@@ -0,0 +1,189 @@
extends Control
# Automatically split lines at regular intervals to avoid performance issues
# while drawing. This is especially due to the width curve which has to be recreated
# on every new point.
const SPLIT_POINT_COUNT = 1024
var stroke: Line2D
var width_curve: Curve
var pressures := PackedFloat32Array()
var event_position: Vector2
var event_tilt: Vector2
var line_color := Color.BLACK
var line_width: float = 3.0
# If `true`, modulate line width accordding to pen pressure.
# This is done using a width curve that is continuously recreated to match the line's actual profile
# as the line is being drawn by the user.
var pressure_sensitive: bool = true
var show_tilt_vector: bool = true
@onready var tablet_info: Label = %TabletInfo
func _ready() -> void:
# This makes tablet and mouse input reported as often as possible regardless of framerate.
# When accumulated input is disabled, we can query the pen/mouse position at every input event
# seen by the operating system, without being limited to the framerate the application runs at.
# The downside is that this uses more CPU resources, so input accumulation should only be
# disabled when you need to have access to precise input coordinates.
Input.use_accumulated_input = false
start_stroke()
%TabletDriver.text = "Tablet driver: %s" % DisplayServer.tablet_get_current_driver()
func _input(event: InputEvent) -> void:
if event is InputEventKey:
if Input.is_action_pressed(&"increase_line_width"):
$CanvasLayer/PanelContainer/Options/LineWidth/HSlider.value += 0.5
#_on_line_width_value_changed(line_width)
if Input.is_action_pressed(&"decrease_line_width"):
$CanvasLayer/PanelContainer/Options/LineWidth/HSlider.value -= 0.5
#_on_line_width_value_changed(line_width)
if not stroke:
return
if event is InputEventMouseMotion:
var event_mouse_motion := event as InputEventMouseMotion
tablet_info.text = "Pressure: %.3f\nTilt: %.3v\nInverted pen: %s" % [
event_mouse_motion.pressure,
event_mouse_motion.tilt,
"Yes" if event_mouse_motion.pen_inverted else "No",
]
if event_mouse_motion.pressure <= 0 and stroke.points.size() > 1:
# Initial part of a stroke; create a new line.
start_stroke()
# Enable the buttons if they were previously disabled.
%ClearAllLines.disabled = false
%UndoLastLine.disabled = false
if event_mouse_motion.pressure > 0:
# Continue existing line.
stroke.add_point(event_mouse_motion.position)
pressures.push_back(event_mouse_motion.pressure)
# Only compute the width curve if it's present, as it's not even created
# if pressure sensitivity is disabled.
if width_curve:
width_curve.clear_points()
for pressure_idx in range(pressures.size()):
width_curve.add_point(Vector2(
float(pressure_idx) / pressures.size(),
pressures[pressure_idx]
))
# Split into a new line if it gets too long to avoid performance issues.
# This is mostly reached when input accumulation is disabled, as enabling
# input accumulation will naturally reduce point count by a lot.
if stroke.get_point_count() >= SPLIT_POINT_COUNT:
start_stroke()
event_position = event_mouse_motion.position
event_tilt = event_mouse_motion.tilt
queue_redraw()
func _draw() -> void:
if show_tilt_vector:
# Draw tilt vector.
draw_line(event_position, event_position + event_tilt * 50, Color(1, 0, 0, 0.5), 2, true)
func start_stroke() -> void:
var new_stroke := Line2D.new()
new_stroke.begin_cap_mode = Line2D.LINE_CAP_ROUND
new_stroke.end_cap_mode = Line2D.LINE_CAP_ROUND
new_stroke.joint_mode = Line2D.LINE_JOINT_ROUND
# Adjust round precision depending on line width to improve performance
# and ensure it doesn't go above the default.
new_stroke.round_precision = mini(line_width, 8)
new_stroke.default_color = line_color
new_stroke.width = line_width
if pressure_sensitive:
new_stroke.width_curve = Curve.new()
add_child(new_stroke)
new_stroke.owner = self
stroke = new_stroke
if pressure_sensitive:
width_curve = new_stroke.width_curve
else:
width_curve = null
pressures.clear()
func _on_undo_last_line_pressed() -> void:
# Remove last node of type Line2D in the scene.
var last_line_2d: Line2D = find_children("", "Line2D")[-1]
if last_line_2d:
# Remove stray empty line present at the end due to mouse motion.
# Note that doing it once doesn't always suffice, as multiple empty lines
# may exist at the end of the list (e.g. after changing line width/color settings).
# In this case, the user will have to use undo multiple times.
if last_line_2d.get_point_count() == 0:
last_line_2d.queue_free()
var other_last_line_2d: Line2D = find_children("", "Line2D")[-2]
if other_last_line_2d:
other_last_line_2d.queue_free()
else:
last_line_2d.queue_free()
# Since a new line is created as soon as mouse motion occurs (even if nothing is visible yet),
# we consider the list of lines to be empty with up to 2 items in it here.
%UndoLastLine.disabled = find_children("", "Line2D").size() <= 2
start_stroke()
func _on_clear_all_lines_pressed() -> void:
# Remove all nodes of type Line2D in the scene.
for node in find_children("", "Line2D"):
node.queue_free()
%ClearAllLines.disabled = true
start_stroke()
func _on_line_color_changed(color: Color) -> void:
line_color = color
# Required to make the setting change apply immediately.
start_stroke()
func _on_line_width_value_changed(value: float) -> void:
line_width = value
$CanvasLayer/PanelContainer/Options/LineWidth/Value.text = "%.1f" % value
# Required to make the setting change apply immediately.
start_stroke()
func _on_pressure_sensitive_toggled(toggled_on: bool) -> void:
pressure_sensitive = toggled_on
# Required to make the setting change apply immediately.
start_stroke()
func _on_show_tilt_vector_toggled(toggled_on: bool) -> void:
show_tilt_vector = toggled_on
func _on_msaa_item_selected(index: int) -> void:
get_viewport().msaa_2d = index as Viewport.MSAA
func _on_max_fps_value_changed(value: float) -> void:
# Since the project has low-processor usage mode enabled, we change its sleep interval instead.
# Since this is a value in microseconds between frames, we have to convert it from a FPS value.
@warning_ignore("narrowing_conversion")
OS.low_processor_usage_mode_sleep_usec = 1_000_000.0 / value
$CanvasLayer/PanelContainer/Options/MaxFPS/Value.text = str(roundi(value))
func _on_v_sync_toggled(toggled_on: bool) -> void:
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED if toggled_on else DisplayServer.VSYNC_DISABLED)
func _on_input_accumulation_toggled(toggled_on: bool) -> void:
Input.use_accumulated_input = toggled_on

View File

@@ -0,0 +1,228 @@
[gd_scene load_steps=13 format=3 uid="uid://dxpettbof8pr8"]
[ext_resource type="Script" path="res://graphics_tablet_input.gd" id="1_fhuxi"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0pp2f"]
content_margin_left = 12.0
content_margin_top = 12.0
content_margin_right = 12.0
content_margin_bottom = 12.0
bg_color = Color(0.223529, 0.223529, 0.223529, 1)
[sub_resource type="InputEventAction" id="InputEventAction_spbge"]
action = &"undo_last_line"
[sub_resource type="Shortcut" id="Shortcut_3mcds"]
events = [SubResource("InputEventAction_spbge")]
[sub_resource type="InputEventAction" id="InputEventAction_7n722"]
action = &"clear_all_lines"
[sub_resource type="Shortcut" id="Shortcut_5wv4g"]
events = [SubResource("InputEventAction_7n722")]
[sub_resource type="InputEventAction" id="InputEventAction_y8lr1"]
action = &"change_line_color"
[sub_resource type="Shortcut" id="Shortcut_1nmmy"]
events = [SubResource("InputEventAction_y8lr1")]
[sub_resource type="InputEventAction" id="InputEventAction_0l3by"]
action = &"toggle_pressure_sensitive"
[sub_resource type="Shortcut" id="Shortcut_mnr5q"]
events = [SubResource("InputEventAction_0l3by")]
[sub_resource type="InputEventAction" id="InputEventAction_4p65y"]
action = &"toggle_tilt_vector"
[sub_resource type="Shortcut" id="Shortcut_231fk"]
events = [SubResource("InputEventAction_4p65y")]
[node name="ColorRect" type="ColorRect"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_default_cursor_shape = 3
script = ExtResource("1_fhuxi")
[node name="CanvasLayer" type="CanvasLayer" parent="."]
[node name="PanelContainer" type="PanelContainer" parent="CanvasLayer"]
offset_right = 264.0
offset_bottom = 648.0
size_flags_horizontal = 0
theme_override_styles/panel = SubResource("StyleBoxFlat_0pp2f")
[node name="Options" type="VBoxContainer" parent="CanvasLayer/PanelContainer"]
custom_minimum_size = Vector2(240, 0)
layout_mode = 2
size_flags_horizontal = 0
theme_override_constants/separation = 10
[node name="UndoLastLine" type="Button" parent="CanvasLayer/PanelContainer/Options"]
unique_name_in_owner = true
layout_mode = 2
disabled = true
shortcut = SubResource("Shortcut_3mcds")
text = "Undo Last Line"
[node name="ClearAllLines" type="Button" parent="CanvasLayer/PanelContainer/Options"]
unique_name_in_owner = true
layout_mode = 2
disabled = true
shortcut = SubResource("Shortcut_5wv4g")
text = "Clear All Lines"
[node name="HSeparator" type="HSeparator" parent="CanvasLayer/PanelContainer/Options"]
layout_mode = 2
[node name="LineColor" type="HBoxContainer" parent="CanvasLayer/PanelContainer/Options"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="Label" type="Label" parent="CanvasLayer/PanelContainer/Options/LineColor"]
layout_mode = 2
text = "Line Color"
[node name="ColorPickerButton" type="ColorPickerButton" parent="CanvasLayer/PanelContainer/Options/LineColor"]
custom_minimum_size = Vector2(0, 30)
layout_mode = 2
size_flags_horizontal = 3
shortcut = SubResource("Shortcut_1nmmy")
[node name="LineWidth" type="HBoxContainer" parent="CanvasLayer/PanelContainer/Options"]
layout_mode = 2
tooltip_text = "(-: Decrease, +: Increase)"
theme_override_constants/separation = 10
[node name="Label" type="Label" parent="CanvasLayer/PanelContainer/Options/LineWidth"]
layout_mode = 2
text = "Line Width"
[node name="HSlider" type="HSlider" parent="CanvasLayer/PanelContainer/Options/LineWidth"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 4
min_value = 0.5
max_value = 20.0
step = 0.5
value = 3.0
[node name="Value" type="Label" parent="CanvasLayer/PanelContainer/Options/LineWidth"]
custom_minimum_size = Vector2(35, 0)
layout_mode = 2
text = "3.0"
horizontal_alignment = 1
[node name="PressureSensitive" type="CheckButton" parent="CanvasLayer/PanelContainer/Options"]
layout_mode = 2
tooltip_text = "If enabled, modulates line width according to pen pressure.
This has no effect if drawing with a mouse or a tablet
without pen pressure support."
button_pressed = true
shortcut = SubResource("Shortcut_mnr5q")
text = "Pressure-Sensitive"
[node name="ShowTiltVector" type="CheckButton" parent="CanvasLayer/PanelContainer/Options"]
layout_mode = 2
tooltip_text = "If enabled, shows a visual representation of the tilt vector
reported by the graphics tablet. Tilt is not supported by all
graphics tablets."
button_pressed = true
shortcut = SubResource("Shortcut_231fk")
text = "Show Tilt Vector"
[node name="HSeparator2" type="HSeparator" parent="CanvasLayer/PanelContainer/Options"]
layout_mode = 2
[node name="MSAA" type="HBoxContainer" parent="CanvasLayer/PanelContainer/Options"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="Label" type="Label" parent="CanvasLayer/PanelContainer/Options/MSAA"]
layout_mode = 2
text = "MSAA 2D"
[node name="OptionButton" type="OptionButton" parent="CanvasLayer/PanelContainer/Options/MSAA"]
layout_mode = 2
selected = 3
item_count = 4
popup/item_0/text = "Disabled"
popup/item_1/text = "2×"
popup/item_1/id = 1
popup/item_2/text = "4×"
popup/item_2/id = 2
popup/item_3/text = "8×"
popup/item_3/id = 3
[node name="MaxFPS" type="HBoxContainer" parent="CanvasLayer/PanelContainer/Options"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="Label" type="Label" parent="CanvasLayer/PanelContainer/Options/MaxFPS"]
layout_mode = 2
text = "Max FPS"
[node name="HSlider" type="HSlider" parent="CanvasLayer/PanelContainer/Options/MaxFPS"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 4
min_value = 10.0
max_value = 240.0
step = 5.0
value = 145.0
[node name="Value" type="Label" parent="CanvasLayer/PanelContainer/Options/MaxFPS"]
custom_minimum_size = Vector2(35, 0)
layout_mode = 2
text = "145"
horizontal_alignment = 1
[node name="VSync" type="CheckButton" parent="CanvasLayer/PanelContainer/Options"]
layout_mode = 2
tooltip_text = "Disable V-Sync to achieve lower input latency.
Note that Android, iOS and Web platforms enforce
V-Sync at a system level with no reliable way to disable it."
text = "V-Sync"
[node name="InputAccumulation" type="CheckButton" parent="CanvasLayer/PanelContainer/Options"]
layout_mode = 2
tooltip_text = "If enabled, inputs are collected and merged into a single input event on every rendered frame.
By default, this is enabled in Godot, but this project disables it by default.
This should be left disabled for drawing apps that expect precise input,
as lines can become visibly jagged otherwise."
text = "Input Accumulation"
[node name="Spacer" type="Control" parent="CanvasLayer/PanelContainer/Options"]
layout_mode = 2
size_flags_vertical = 3
[node name="TabletInfo" type="Label" parent="CanvasLayer/PanelContainer/Options"]
unique_name_in_owner = true
modulate = Color(1, 1, 1, 0.67451)
layout_mode = 2
text = "Pressure: 0.000
Tilt: (0.000, 0.000)
Inverted pen: No"
[node name="TabletDriver" type="Label" parent="CanvasLayer/PanelContainer/Options"]
unique_name_in_owner = true
modulate = Color(1, 1, 1, 0.501961)
layout_mode = 2
text = "Tablet driver: <driver>"
[connection signal="pressed" from="CanvasLayer/PanelContainer/Options/UndoLastLine" to="." method="_on_undo_last_line_pressed"]
[connection signal="pressed" from="CanvasLayer/PanelContainer/Options/ClearAllLines" to="." method="_on_clear_all_lines_pressed"]
[connection signal="color_changed" from="CanvasLayer/PanelContainer/Options/LineColor/ColorPickerButton" to="." method="_on_line_color_changed"]
[connection signal="value_changed" from="CanvasLayer/PanelContainer/Options/LineWidth/HSlider" to="." method="_on_line_width_value_changed"]
[connection signal="toggled" from="CanvasLayer/PanelContainer/Options/PressureSensitive" to="." method="_on_pressure_sensitive_toggled"]
[connection signal="toggled" from="CanvasLayer/PanelContainer/Options/ShowTiltVector" to="." method="_on_show_tilt_vector_toggled"]
[connection signal="item_selected" from="CanvasLayer/PanelContainer/Options/MSAA/OptionButton" to="." method="_on_msaa_item_selected"]
[connection signal="value_changed" from="CanvasLayer/PanelContainer/Options/MaxFPS/HSlider" to="." method="_on_max_fps_value_changed"]
[connection signal="toggled" from="CanvasLayer/PanelContainer/Options/VSync" to="." method="_on_v_sync_toggled"]
[connection signal="toggled" from="CanvasLayer/PanelContainer/Options/InputAccumulation" to="." method="_on_input_accumulation_toggled"]

View File

@@ -0,0 +1 @@
<svg height="128" width="128" xmlns="http://www.w3.org/2000/svg"><path d="m23.361485 80.005717c-2.104041 2.400048-3.360066 5.840116-3.360066 9.904197 0 13.024266-25.1044898-1.424028-2.696053 21.360426 7.072139 7.20014 21.232415 5.36011 28.304553-1.82404a18.640364 18.640374 0 0 0 0-26.048516c-8.800171-8.952179-17.600343-8.672174-22.240434-3.392067zm18.400358-13.120264 19.200375 19.200385 54.401062-54.40109a13.718869 13.718876 0 0 0 -19.200375-19.600392z" fill="#808080"/></svg>

After

Width:  |  Height:  |  Size: 481 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://besl3a0sbq1v0"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,71 @@
; 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=5
[application]
config/name="Graphics Tablet Input"
config/description="A demo showing how to use graphics tablet input in Godot."
config/tags=PackedStringArray("2d", "demo", "input", "official", "porting")
run/main_scene="res://graphics_tablet_input.tscn"
config/features=PackedStringArray("4.3")
run/low_processor_mode=true
config/icon="res://icon.svg"
[display]
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"
window/vsync/vsync_mode=0
[input]
undo_last_line={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194308,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":90,"physical_keycode":0,"key_label":0,"unicode":122,"location":0,"echo":false,"script":null)
]
}
clear_all_lines={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":4194308,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
change_line_color={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":67,"physical_keycode":0,"key_label":0,"unicode":99,"location":0,"echo":false,"script":null)
]
}
increase_line_width={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194437,"physical_keycode":0,"key_label":0,"unicode":43,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":61,"key_label":0,"unicode":61,"location":0,"echo":false,"script":null)
]
}
decrease_line_width={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194435,"physical_keycode":0,"key_label":0,"unicode":45,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":45,"key_label":0,"unicode":41,"location":0,"echo":false,"script":null)
]
}
toggle_pressure_sensitive={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":80,"physical_keycode":0,"key_label":0,"unicode":112,"location":0,"echo":false,"script":null)
]
}
toggle_tilt_vector={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":84,"physical_keycode":0,"key_label":0,"unicode":116,"location":0,"echo":false,"script":null)
]
}
[rendering]
renderer/rendering_method="mobile"
anti_aliasing/quality/msaa_2d=3

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB