immersive-home/addons/godot-xr-tools/objects/viewport_2d_in_3d.gd
2023-12-14 00:03:03 +01:00

513 lines
14 KiB
GDScript

@tool
extends Node3D
## XR ToolsViewport 2D in 3D
##
## This script manages a 2D scene rendered as a texture on a 3D quad.
##
## Pointer and keyboard input are mapped into the 2D scene.
## Signal for pointer events
signal pointer_event(event)
## Transparent property
enum TransparancyMode {
OPAQUE, ## Render opaque
TRANSPARENT, ## Render transparent
SCISSOR, ## Render using alpha-scissor
}
## Viewport Update Mode
enum UpdateMode {
UPDATE_ONCE, ## Update once (redraw triggered if set again to UPDATE_ONCE)
UPDATE_ALWAYS, ## Update on every frame
UPDATE_THROTTLED, ## Update at throttled rate
}
# The following dirty flags are private (leading _) to suppress them in the
# generated documentation. Unfortunately gdlint complaints on private constants
# (see https://github.com/Scony/godot-gdscript-toolkit/issues/223). Until this
# is fixed we suppress the rule.
# gdlint: disable=constant-name
# State dirty flags
const _DIRTY_NONE := 0x0000 # Everything up to date
const _DIRTY_MATERIAL := 0x0001 # Material needs update
const _DIRTY_SCENE := 0x0002 # Scene needs update
const _DIRTY_SIZE := 0x0004 # Viewport size needs update
const _DIRTY_ALBEDO := 0x0008 # Albedo texture needs update
const _DIRTY_UPDATE := 0x0010 # Update mode needs update
const _DIRTY_TRANSPARENCY := 0x0020 # Transparency needs update
const _DIRTY_ALPHA_SCISSOR := 0x0040 # Alpha scissor needs update
const _DIRTY_UNSHADED := 0x0080 # Shade mode needs update
const _DIRTY_FILTERED := 0x0100 # Filter mode needs update
const _DIRTY_SURFACE := 0x0200 # Surface material needs update
const _DIRTY_REDRAW := 0x0400 # Redraw required
const _DIRTY_ALL := 0x07FF # All dirty
# Default layer of 1:static-world, 21:pointable, 23:ui-objects
const DEFAULT_LAYER := 0b0000_0000_0101_0000_0000_0000_0000_0001
# Physics property group
@export_group("Physics")
## Physical screen size property
@export var screen_size : Vector2 = Vector2(3.0, 2.0): set = set_screen_size
## Viewport collision enabled property
@export var enabled : bool = true: set = set_enabled
## Collision layer
@export_flags_3d_physics var collision_layer : int = DEFAULT_LAYER: set = set_collision_layer
# Content property group
@export_group("Content")
## Scene property
@export var scene : PackedScene: set = set_scene
## Viewport size property
@export var viewport_size : Vector2 = Vector2(300.0, 200.0): set = set_viewport_size
## Update Mode property
@export var update_mode : UpdateMode = UpdateMode.UPDATE_ALWAYS: set = set_update_mode
## Update throttle property
@export var throttle_fps : float = 30.0
# Input property group
@export_group("Input")
## Allow physical keyboard input to viewport
@export var input_keyboard : bool = true
## Allow gamepad input to viewport
@export var input_gamepad : bool = false
# Rendering property group
@export_group("Rendering")
## Custom material template
@export var material : StandardMaterial3D = null: set = set_material
## Transparent property
@export var transparent : TransparancyMode = TransparancyMode.TRANSPARENT: set = set_transparent
## Alpha Scissor Threshold property (ignored when custom material provided)
var alpha_scissor_threshold : float = 0.25: set = set_alpha_scissor_threshold
## Unshaded flag (ignored when custom material provided)
var unshaded : bool = false: set = set_unshaded
## Filtering flag (ignored when custom material provided)
var filter : bool = true: set = set_filter
var is_ready : bool = false
var scene_node : Node
var viewport_texture : ViewportTexture
var time_since_last_update : float = 0.0
var _screen_material : StandardMaterial3D
var _dirty := _DIRTY_ALL
# Called when the node enters the scene tree for the first time.
func _ready():
is_ready = true
# Listen for pointer events on the screen body
$StaticBody3D.connect("pointer_event", _on_pointer_event)
# Apply physics properties
_update_screen_size()
_update_enabled()
_update_collision_layer()
# Update the render objects
_update_render()
# Provide custom property information
func _get_property_list() -> Array[Dictionary]:
# Select visibility of properties
var show_alpha_scissor := not material and transparent == TransparancyMode.SCISSOR
var show_unshaded := not material
var show_filter := not material
# Return extra properties
return [
{
name = "Rendering",
type = TYPE_NIL,
usage = PROPERTY_USAGE_GROUP
},
{
name = "alpha_scissor_threshold",
type = TYPE_FLOAT,
usage = PROPERTY_USAGE_DEFAULT if show_alpha_scissor else PROPERTY_USAGE_NO_EDITOR,
hint = PROPERTY_HINT_RANGE,
hint_string = "0.0,1.0"
},
{
name = "unshaded",
type = TYPE_BOOL,
usage = PROPERTY_USAGE_DEFAULT if show_unshaded else PROPERTY_USAGE_NO_EDITOR
},
{
name = "filter",
type = TYPE_BOOL,
usage = PROPERTY_USAGE_DEFAULT if show_filter else PROPERTY_USAGE_NO_EDITOR
}
]
# Allow revert of custom properties
func _property_can_revert(property : StringName) -> bool:
match property:
"alpha_scissor_threshold":
return true
"unshaded":
return true
"filter":
return true
_:
return false
# Provide revert values for custom properties
func _property_get_revert(property : StringName): # Variant
match property:
"alpha_scissor_threshold":
return 0.25
"unshaded":
return false
"filter":
return true
## Get the 2D scene instance
func get_scene_instance() -> Node:
return scene_node
## Connect a 2D scene signal
func connect_scene_signal(which : String, callback : Callable, flags : int = 0):
if scene_node:
scene_node.connect(which, callback, flags)
# Handle pointer event from screen-body
func _on_pointer_event(event : XRToolsPointerEvent) -> void:
pointer_event.emit(event)
# Handler for input events
func _input(event):
# Map keyboard events to the viewport if enabled
if input_keyboard and (event is InputEventKey or event is InputEventShortcut):
$Viewport.push_input(event)
return
# Map gamepad events to the viewport if enable
if input_gamepad and (event is InputEventJoypadButton or event is InputEventJoypadMotion):
$Viewport.push_input(event)
return
# Process event
func _process(delta):
# Process screen refreshing
if Engine.is_editor_hint():
# Perform periodic material refreshes to handle the user modifying the
# material properties in the editor
time_since_last_update += delta
if time_since_last_update > 1.0:
time_since_last_update = 0.0
# Trigger material refresh
_dirty = _DIRTY_MATERIAL
_update_render()
elif update_mode == UpdateMode.UPDATE_THROTTLED:
# Perform throttled updates of the viewport
var frame_time = 1.0 / throttle_fps
time_since_last_update += delta
if time_since_last_update > frame_time:
time_since_last_update = 0.0
# Trigger update
$Viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
else:
# This is no longer needed
set_process(false)
## Set screen size property
func set_screen_size(new_size: Vector2) -> void:
screen_size = new_size
if is_ready:
_update_screen_size()
## Set enabled property
func set_enabled(is_enabled: bool) -> void:
enabled = is_enabled
if is_ready:
_update_enabled()
## Set collision layer property
func set_collision_layer(new_layer: int) -> void:
collision_layer = new_layer
if is_ready:
_update_collision_layer()
## Set scene property
func set_scene(new_scene: PackedScene) -> void:
scene = new_scene
_dirty |= _DIRTY_SCENE
if is_ready:
_update_render()
## Set viewport size property
func set_viewport_size(new_size: Vector2) -> void:
viewport_size = new_size
_dirty |= _DIRTY_SIZE
if is_ready:
_update_render()
## Set update mode property
func set_update_mode(new_update_mode: UpdateMode) -> void:
update_mode = new_update_mode
_dirty |= _DIRTY_UPDATE
if is_ready:
_update_render()
## Set material property
func set_material(new_material: StandardMaterial3D) -> void:
material = new_material
notify_property_list_changed()
_dirty |= _DIRTY_MATERIAL
if is_ready:
_update_render()
## Set transparent property
func set_transparent(new_transparent: TransparancyMode) -> void:
transparent = new_transparent
notify_property_list_changed()
_dirty |= _DIRTY_TRANSPARENCY
if is_ready:
_update_render()
## Set the alpha scisser threshold
func set_alpha_scissor_threshold(new_threshold: float) -> void:
alpha_scissor_threshold = new_threshold
_dirty |= _DIRTY_ALPHA_SCISSOR
if is_ready:
_update_render()
## Set the unshaded property
func set_unshaded(new_unshaded : bool) -> void:
unshaded = new_unshaded
_dirty |= _DIRTY_UNSHADED
if is_ready:
_update_render()
## Set filter property
func set_filter(new_filter: bool) -> void:
filter = new_filter
_dirty |= _DIRTY_FILTERED
if is_ready:
_update_render()
# Screen size update handler
func _update_screen_size() -> void:
$Screen.mesh.size = screen_size
$StaticBody3D.screen_size = screen_size
$StaticBody3D/CollisionShape3D.shape.size = Vector3(
screen_size.x,
screen_size.y,
0.02)
# Enabled update handler
func _update_enabled() -> void:
if Engine.is_editor_hint():
return
$StaticBody3D/CollisionShape3D.disabled = !enabled
# Collision layer update handler
func _update_collision_layer() -> void:
$StaticBody3D.collision_layer = collision_layer
# This complex function processes the render dirty flags and performs the
# minimal number of updates to get the render objects into the correct state.
func _update_render() -> void:
# Handle material change
if _dirty & _DIRTY_MATERIAL:
_dirty &= ~_DIRTY_MATERIAL
# Construct the new screen material
if material:
# Copy custom material
_screen_material = material.duplicate()
else:
# Create new local material
_screen_material = StandardMaterial3D.new()
# Disable culling
_screen_material.params_cull_mode = StandardMaterial3D.CULL_DISABLED
# Ensure local material is configured
_dirty |= _DIRTY_TRANSPARENCY | \
_DIRTY_ALPHA_SCISSOR | \
_DIRTY_UNSHADED | \
_DIRTY_FILTERED
# Ensure new material renders viewport onto surface
_dirty |= _DIRTY_ALBEDO | _DIRTY_SURFACE
# If we have no screen material then skip everything else
if not _screen_material:
return
# Handle scene change
if _dirty & _DIRTY_SCENE:
_dirty &= ~_DIRTY_SCENE
# Out with the old
if is_instance_valid(scene_node):
$Viewport.remove_child(scene_node)
scene_node.queue_free()
# In with the new
if scene:
# Instantiate provided scene
scene_node = scene.instantiate()
$Viewport.add_child(scene_node)
elif $Viewport.get_child_count() == 1:
# Use already-provided scene
scene_node = $Viewport.get_child(0)
# Ensure the new scene is rendered at least once
_dirty |= _DIRTY_REDRAW
# Handle viewport size change
if _dirty & _DIRTY_SIZE:
_dirty &= ~_DIRTY_SIZE
# Set the viewport size
$Viewport.size = viewport_size
$StaticBody3D.viewport_size = viewport_size
# Update our viewport texture, it will have changed
_dirty |= _DIRTY_ALBEDO
# Handle albedo change:
if _dirty & _DIRTY_ALBEDO:
_dirty &= ~_DIRTY_ALBEDO
# Set the screen material to use the viewport for the albedo channel
viewport_texture = $Viewport.get_texture()
_screen_material.albedo_texture = viewport_texture
# Handle update mode change
if _dirty & _DIRTY_UPDATE:
_dirty &= ~_DIRTY_UPDATE
# Apply update rules
if Engine.is_editor_hint():
# Update once. Process function used for editor refreshes
$Viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
set_process(true)
elif update_mode == UpdateMode.UPDATE_ONCE:
# Update once. Process function not used
$Viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
set_process(false)
elif update_mode == UpdateMode.UPDATE_ALWAYS:
# Update always. Process function not used
$Viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
set_process(false)
elif update_mode == UpdateMode.UPDATE_THROTTLED:
# Update once. Process function triggers periodic refresh
$Viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
set_process(true)
# Handle transparency update
if _dirty & _DIRTY_TRANSPARENCY:
_dirty &= ~_DIRTY_TRANSPARENCY
# If using a temporary material then update transparency
if _screen_material and not material:
# Set the transparancy mode
match transparent:
TransparancyMode.OPAQUE:
_screen_material.transparency = BaseMaterial3D.TRANSPARENCY_DISABLED
TransparancyMode.TRANSPARENT:
_screen_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
TransparancyMode.SCISSOR:
_screen_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR
# Set the viewport background transparency mode and force a redraw
$Viewport.transparent_bg = transparent != TransparancyMode.OPAQUE
_dirty |= _DIRTY_REDRAW
# Handle alpha scissor update
if _dirty & _DIRTY_ALPHA_SCISSOR:
_dirty &= ~_DIRTY_ALPHA_SCISSOR
# If using a temporary material with alpha-scissor then update
if _screen_material and not material and transparent == TransparancyMode.SCISSOR:
_screen_material.params_alpha_scissor_threshold = alpha_scissor_threshold
# Handle unshaded update
if _dirty & _DIRTY_UNSHADED:
_dirty &= ~_DIRTY_UNSHADED
# If using a temporary material then update the shading mode and force a redraw
if _screen_material and not material:
_screen_material.shading_mode = (
BaseMaterial3D.SHADING_MODE_UNSHADED if unshaded else
BaseMaterial3D.SHADING_MODE_PER_PIXEL)
#_dirty |= _DIRTY_REDRAW
# Handle filter update
if _dirty & _DIRTY_FILTERED:
_dirty &= ~_DIRTY_FILTERED
# If using a temporary material then update the filter mode and force a redraw
if _screen_material and not material:
_screen_material.texture_filter = (
BaseMaterial3D.TEXTURE_FILTER_LINEAR if filter else
BaseMaterial3D.TEXTURE_FILTER_NEAREST)
#_dirty |= _DIRTY_REDRAW
# Handle surface material update
if _dirty & _DIRTY_SURFACE:
_dirty &= ~_DIRTY_SURFACE
# Set the screen to render using the new screen material
$Screen.set_surface_override_material(0, _screen_material)
# Handle forced redraw of the viewport
if _dirty & _DIRTY_REDRAW:
_dirty &= ~_DIRTY_REDRAW
# Force a redraw of the viewport
if Engine.is_editor_hint() or update_mode == UpdateMode.UPDATE_ONCE:
$Viewport.render_target_update_mode = SubViewport.UPDATE_ONCE