Add phantom camera

This commit is contained in:
2025-03-01 19:52:43 +01:00
parent 57930f1c50
commit a9f8fb717e
149 changed files with 19276 additions and 2 deletions

View File

@@ -0,0 +1,75 @@
@tool
extends Node
const PHANTOM_CAMERA_CONSTS = preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd")
signal noise_2d_emitted(noise_output: Transform2D, emitter_layer: int)
signal noise_3d_emitted(noise_output: Transform3D, emitter_layer: int)
var phantom_camera_hosts: Array[PhantomCameraHost]:
get:
return _phantom_camera_host_list
var _phantom_camera_host_list: Array[PhantomCameraHost]
var phantom_camera_2ds: Array[PhantomCamera2D]:
get:
return _phantom_camera_2d_list
var _phantom_camera_2d_list: Array[PhantomCamera2D]
var phantom_camera_3ds: Array[Node]: ## Note: To support disable_3d export templates for 2D projects, this is purposely not strongly typed.
get:
return _phantom_camera_3d_list
var _phantom_camera_3d_list: Array[Node] ## Note: To support disable_3d export templates for 2D projects, this is purposely not strongly typed.
func _enter_tree():
Engine.physics_jitter_fix = 0
func pcam_host_added(caller: Node) -> void:
if is_instance_of(caller, PhantomCameraHost):
_phantom_camera_host_list.append(caller)
else:
printerr("This method can only be called from a PhantomCameraHost node")
func pcam_host_removed(caller: Node) -> void:
if is_instance_of(caller, PhantomCameraHost):
_phantom_camera_host_list.erase(caller)
else:
printerr("This method can only be called from a PhantomCameraHost node")
func pcam_added(caller, host_slot: int = 0) -> void:
if is_instance_of(caller, PhantomCamera2D):
_phantom_camera_2d_list.append(caller)
elif caller.is_class("PhantomCamera3D"): ## Note: To support disable_3d export templates for 2D projects, this is purposely not strongly typed.
_phantom_camera_3d_list.append(caller)
if not _phantom_camera_host_list.is_empty():
_phantom_camera_host_list[host_slot].pcam_added_to_scene(caller)
func pcam_removed(caller) -> void:
if is_instance_of(caller, PhantomCamera2D):
_phantom_camera_2d_list.erase(caller)
elif caller.is_class("PhantomCamera3D"): ## Note: To support disable_3d export templates for 2D projects, this is purposely not strongly typed.
_phantom_camera_3d_list.erase(caller)
else:
printerr("This method can only be called from a PhantomCamera node")
func get_phantom_camera_hosts() -> Array[PhantomCameraHost]:
return _phantom_camera_host_list
func get_phantom_camera_2ds() -> Array[PhantomCamera2D]:
return _phantom_camera_2d_list
func get_phantom_camera_3ds() -> Array: ## Note: To support disable_3d export templates for 2D projects, this is purposely not strongly typed.
return _phantom_camera_3d_list
func scene_changed() -> void:
_phantom_camera_2d_list.clear()
_phantom_camera_3d_list.clear()
_phantom_camera_host_list.clear()

View File

@@ -0,0 +1,162 @@
#######################################################################
# Credit goes to the Dialogue Manager plugin for this script
# Check it out at: https://github.com/nathanhoad/godot_dialogue_manager
#######################################################################
@tool
extends Control
#region Constants
const TEMP_FILE_NAME = "user://temp.zip"
#endregion
#region Signals
signal failed()
signal updated(updated_to_version: String)
#endregion
#region @onready
#@onready var logo: TextureRect = %Logo
@onready var _download_verion: Label = %DownloadVersionLabel
@onready var _download_http_request: HTTPRequest = %DownloadHTTPRequest
@onready var _download_button: Button = %DownloadButton
@onready var _download_button_bg: NinePatchRect = %DownloadButtonBG
@onready var _download_label: Label = %UpdateLabel
@onready var _breaking_label: Label = %BreakingLabel
@onready var _breaking_margin_container: MarginContainer = %BreakingMarginContainer
@onready var _breaking_options_button: OptionButton = %BreakingOptionButton
#@onready var current_version_label: Label = %CurrentVersionLabel
#endregion
#region Variables
# Todo - For 4.2 upgrade - Shows current version
var _download_dialogue: AcceptDialog
var _button_texture_default: Texture2D = load("res://addons/phantom_camera/assets/PhantomCameraBtnPrimaryDefault.png")
var _button_texture_hover: Texture2D = load("res://addons/phantom_camera/assets/PhantomCameraBtnPrimaryHover.png")
var next_version_release: Dictionary:
set(value):
next_version_release = value
_download_verion.text = "%s update is available for download" % value.tag_name.substr(1)
# Todo - For 4.2 upgrade
#current_version_label.text = "Current version is " + editor_plugin.get_version()
get:
return next_version_release
var _breaking_window_height: float = 520
var _breaking_window_height_update: float = 600
#endregion
#region Private Functions
func _ready() -> void:
_download_http_request.request_completed.connect(_on_http_request_request_completed)
_download_button.pressed.connect(_on_download_button_pressed)
_download_button.mouse_entered.connect(_on_mouse_entered)
_download_button.mouse_exited.connect(_on_mouse_exited)
_breaking_label.hide()
_breaking_margin_container.hide()
_breaking_options_button.hide()
_breaking_options_button.item_selected.connect(_on_item_selected)
func _on_item_selected(index: int) -> void:
if index == 1:
_download_button.show()
_download_dialogue.size = Vector2(_download_dialogue.size.x, _breaking_window_height_update)
else:
_download_button.hide()
_download_dialogue.size = Vector2(_download_dialogue.size.x, _breaking_window_height)
func _on_download_button_pressed() -> void:
_download_http_request.request(next_version_release.zipball_url)
_download_button.disabled = true
_download_label.text = "Downloading..."
_download_button_bg.hide()
func _on_mouse_entered() -> void:
_download_button_bg.set_texture(_button_texture_hover)
func _on_mouse_exited() -> void:
_download_button_bg.set_texture(_button_texture_default)
func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
if result != HTTPRequest.RESULT_SUCCESS:
failed.emit()
return
# Save the downloaded zip
var zip_file: FileAccess = FileAccess.open(TEMP_FILE_NAME, FileAccess.WRITE)
zip_file.store_buffer(body)
zip_file.close()
OS.move_to_trash(ProjectSettings.globalize_path("res://addons/phantom_camera"))
var zip_reader: ZIPReader = ZIPReader.new()
zip_reader.open(TEMP_FILE_NAME)
var files: PackedStringArray = zip_reader.get_files()
var base_path = files[1]
# Remove archive folder
files.remove_at(0)
# Remove assets folder
files.remove_at(0)
for path in files:
var new_file_path: String = path.replace(base_path, "")
if path.ends_with("/"):
DirAccess.make_dir_recursive_absolute("res://addons/%s" % new_file_path)
else:
var file: FileAccess = FileAccess.open("res://addons/%s" % new_file_path, FileAccess.WRITE)
file.store_buffer(zip_reader.read_file(path))
zip_reader.close()
DirAccess.remove_absolute(TEMP_FILE_NAME)
updated.emit(next_version_release.tag_name.substr(1))
func _on_notes_button_pressed() -> void:
OS.shell_open(next_version_release.html_url)
#endregion
#region Public Functions
func show_updater_warning(next_version_number: Array, current_version_number: Array) -> void:
var current_version_number_0: int = current_version_number[0] as int
var current_version_number_1: int = current_version_number[1] as int
var next_version_number_0: int = next_version_number[0] as int # Major release number in the new release
var next_version_number_1: int = next_version_number[1] as int # Minor release number in the new release
if next_version_number_0 > current_version_number_0 or \
next_version_number_1 > current_version_number_1:
_breaking_label.show()
_breaking_margin_container.show()
_breaking_options_button.show()
_download_button.hide()
_download_dialogue = get_parent()
_download_dialogue.size = Vector2(_download_dialogue.size.x, _breaking_window_height)
#endregion

View File

@@ -0,0 +1,179 @@
#######################################################################
# Credit goes to the Dialogue Manager plugin for this script
# Check it out at: https://github.com/nathanhoad/godot_dialogue_manager
#######################################################################
@tool
extends Button
#region Constants
const REMOTE_RELEASE_URL: StringName = "https://api.github.com/repos/ramokz/phantom-camera/releases"
const UPDATER_CONSTANTS := preload("res://addons/phantom_camera/scripts/panel/updater/updater_constants.gd")
#endregion
#region @onready
@onready var http_request: HTTPRequest = %HTTPRequest
@onready var download_dialog: AcceptDialog = %DownloadDialog
@onready var download_update_panel: Control = %DownloadUpdatePanel
@onready var needs_reload_dialog: AcceptDialog = %NeedsReloadDialog
@onready var update_failed_dialog: AcceptDialog = %UpdateFailedDialog
#endregion
#region Variables
# The main editor plugin
var editor_plugin: EditorPlugin
var needs_reload: bool = false
# A lambda that gets called just before refreshing the plugin. Return false to stop the reload.
var on_before_refresh: Callable = func(): return true
#endregion
#region Private Functions
func _ready() -> void:
hide()
# Check for updates on GitHub Releases
check_for_update()
pressed.connect(_on_update_button_pressed)
http_request.request_completed.connect(_request_request_completed)
download_update_panel.updated.connect(_on_download_update_panel_updated)
needs_reload_dialog.confirmed.connect(_on_needs_reload_dialog_confirmed)
func _request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
if result != HTTPRequest.RESULT_SUCCESS: return
if not editor_plugin: return
var current_version: String = editor_plugin.get_version()
# Work out the next version from the releases information on GitHub
var response: Array = JSON.parse_string(body.get_string_from_utf8())
if typeof(response) != TYPE_ARRAY: return
# GitHub releases are in order of creation, not order of version
var versions: Array = response.filter(func(release):
var version: String = release.tag_name.substr(1)
return version_to_number(version) > version_to_number(current_version)
)
if versions.size() > 0:
# Safeguard forks from being updated itself
if FileAccess.file_exists("res://dev_scenes/3d/dev_scene_3d.tscn") or \
not ProjectSettings.get_setting(UPDATER_CONSTANTS.setting_updater_enabled):
if not ProjectSettings.get_setting(UPDATER_CONSTANTS.setting_updater_notify_release): return
print_rich("
[color=#3AB99A] ********[/color]
[color=#3AB99A] ************[/color]
[color=#3AB99A]**************[/color]
[color=#3AB99A]****** *** *[/color]
[color=#3AB99A]****** ***[/color]
[color=#3AB99A]********** *****[/color]
[color=#3AB99A]******** ***********[/color]
[color=#3AB99A]******** *********** **[/color]
[color=#3AB99A]********* **************[/color]
[color=#3AB99A]********** *************[/color]
[color=#3AB99A]** ** ** ******* **[/color]
[font_size=18][b]New Phantom Camera version is available[/b][/font_size]")
if FileAccess.file_exists("res://dev_scenes/3d/dev_scene_3d.tscn"):
print_rich("[font_size=14][color=#EAA15E][b]As you're using a fork of the project, you will need to update it manually[/b][/color][/font_size]")
print_rich("[font_size=12]If you don't want to see this message, then it can be disabled inside:\n[code]Project Settings/Phantom Camera/Updater/Show New Release Info on Editor Launch in Output[/code]")
return
download_update_panel.next_version_release = versions[0]
download_update_panel.show_updater_warning(
versions[0].tag_name.substr(1).split("."),
current_version.split(".")
)
_set_scale()
editor_plugin.panel_button.add_theme_color_override("font_color", Color("#3AB99A"))
editor_plugin.panel_button.icon = load("res://addons/phantom_camera/icons/phantom_camera_updater_panel_icon.svg")
editor_plugin.panel_button.add_theme_color_override("icon_normal_color", Color("#3AB99A"))
show()
func _on_update_button_pressed() -> void:
if needs_reload:
var will_refresh = on_before_refresh.call()
if will_refresh:
EditorInterface.restart_editor(true)
else:
_set_scale()
download_dialog.popup_centered()
func _set_scale() -> void:
var scale: float = EditorInterface.get_editor_scale()
download_dialog.min_size = Vector2(300, 250) * scale
func _on_download_dialog_close_requested() -> void:
download_dialog.hide()
func _on_download_update_panel_updated(updated_to_version: String) -> void:
download_dialog.hide()
needs_reload_dialog.dialog_text = "Reload to finish update"
needs_reload_dialog.ok_button_text = "Reload"
needs_reload_dialog.cancel_button_text = "Cancel"
needs_reload_dialog.popup_centered()
needs_reload = true
text = "Reload Project"
func _on_download_update_panel_failed() -> void:
download_dialog.hide()
update_failed_dialog.dialog_text = "Updated Failed"
update_failed_dialog.popup_centered()
func _on_needs_reload_dialog_confirmed() -> void:
EditorInterface.restart_editor(true)
func _on_timer_timeout() -> void:
if not needs_reload:
check_for_update()
#endregion
#region Public Functions
# Convert a version number to an actually comparable number
func version_to_number(version: String) -> int:
var regex = RegEx.new()
regex.compile("[a-zA-Z]+")
if regex.search(str(version)): return 0
var bits = version.split(".")
var version_bit: int
var multiplier: int = 10000
for i in bits.size():
version_bit += bits[i].to_int() * multiplier / (10 ** (i))
return version_bit
func check_for_update() -> void:
http_request.request(REMOTE_RELEASE_URL)
#endregion

View File

@@ -0,0 +1,9 @@
extends RefCounted
# Plugin Project Settings Sections
const setting_phantom_camera: StringName = "phantom_camera/"
const setting_updater_name: StringName = setting_phantom_camera + "updater/"
# Updater Settings
const setting_updater_enabled: StringName = setting_updater_name + "enable_updater"
const setting_updater_notify_release: StringName = setting_updater_name + "show_new_release_info_on_editor_launch_in_output"

View File

@@ -0,0 +1,484 @@
@tool
extends Control
#region Constants
const _constants = preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd")
# TODO - Should be in a central location
const _camera_2d_icon: CompressedTexture2D = preload("res://addons/phantom_camera/icons/viewfinder/Camera2DIcon.svg")
const _camera_3d_icon: CompressedTexture2D = preload("res://addons/phantom_camera/icons/viewfinder/Camera3DIcon.svg")
const _pcam_host_icon: CompressedTexture2D = preload("res://addons/phantom_camera/icons/phantom_camera_host.svg")
const _pcam_2D_icon: CompressedTexture2D = preload("res://addons/phantom_camera/icons/phantom_camera_2d.svg")
const _pcam_3D_icon: CompressedTexture2D = preload("res://addons/phantom_camera/icons/phantom_camera_3d.svg")
const _overlay_color_alpha: float = 0.3
#endregion
#region @onready
@onready var dead_zone_center_hbox: VBoxContainer = %DeadZoneCenterHBoxContainer
@onready var dead_zone_center_center_panel: Panel = %DeadZoneCenterCenterPanel
@onready var dead_zone_left_center_panel: Panel = %DeadZoneLeftCenterPanel
@onready var dead_zone_right_center_panel: Panel = %DeadZoneRightCenterPanel
@onready var target_point: Panel = %TargetPoint
@onready var aspect_ratio_container: AspectRatioContainer = %AspectRatioContainer
@onready var camera_viewport_panel: Panel = aspect_ratio_container.get_child(0)
@onready var _framed_viewfinder: Control = %FramedViewfinder
@onready var _dead_zone_h_box_container: Control = %DeadZoneHBoxContainer
@onready var sub_viewport: SubViewport = %SubViewport
@onready var _empty_state_control: Control = %EmptyStateControl
@onready var _empty_state_icon: Control = %EmptyStateIcon
@onready var _empty_state_text: RichTextLabel = %EmptyStateText
@onready var _add_node_button: Button = %AddNodeButton
@onready var _add_node_button_text: RichTextLabel = %AddNodeTypeText
@onready var _priority_override_button: Button = %PriorityOverrideButton
@onready var _priority_override_name_label: Label = %PriorityOverrideNameLabel
@onready var _camera_2d: Camera2D = %Camera2D
#endregion
#region Private Variables
var _no_open_scene_icon: CompressedTexture2D = preload("res://addons/phantom_camera/icons/viewfinder/SceneTypesIcon.svg")
var _no_open_scene_string: String = "[b]2D[/b] or [b]3D[/b] scene open"
var _selected_camera: Node
var _active_pcam: Node
var _is_2d: bool
var root_node: Node
#endregion
#region Public variables
var pcam_host_group: Array[PhantomCameraHost]
var is_scene: bool
var viewfinder_visible: bool
var min_horizontal: float
var max_horizontal: float
var min_vertical: float
var max_vertical: float
var pcam_host: PhantomCameraHost
#endregion
#region Private Functions
func _ready() -> void:
if not Engine.is_editor_hint():
set_process(true)
camera_viewport_panel.self_modulate.a = 0
root_node = get_tree().current_scene
if root_node is Node2D || root_node is Node3D:
%SubViewportContainer.set_visible(false)
if root_node is Node2D:
_is_2d = true
else:
_is_2d = false
_set_viewfinder(root_node, false)
if Engine.is_editor_hint():
# BUG - Both signals below are called whenever a noe is selected in the scenetree
# Should only be triggered whenever a node is added or removed.
get_tree().node_added.connect(_node_added_or_removed)
get_tree().node_removed.connect(_node_added_or_removed)
else:
_empty_state_control.set_visible(false)
_priority_override_button.set_visible(false)
# Triggered when viewport size is changed in Project Settings
ProjectSettings.settings_changed.connect(_settings_changed)
func _exit_tree() -> void:
if Engine.is_editor_hint():
if get_tree().node_added.is_connected(_node_added_or_removed):
get_tree().node_added.disconnect(_node_added_or_removed)
get_tree().node_removed.disconnect(_node_added_or_removed)
if aspect_ratio_container.resized.is_connected(_resized):
aspect_ratio_container.resized.disconnect(_resized)
if _add_node_button.pressed.is_connected(visibility_check):
_add_node_button.pressed.disconnect(visibility_check)
if is_instance_valid(_active_pcam):
if _active_pcam.dead_zone_changed.is_connected(_on_dead_zone_changed):
_active_pcam.dead_zone_changed.disconnect(_on_dead_zone_changed)
if _priority_override_button.pressed.is_connected(_select_override_pcam):
_priority_override_button.pressed.disconnect(_select_override_pcam)
func _process(_delta: float) -> void:
if Engine.is_editor_hint() and not viewfinder_visible: return
if not is_instance_valid(_active_pcam): return
var unprojected_position_clamped: Vector2 = Vector2(
clamp(_active_pcam.viewport_position.x, min_horizontal, max_horizontal),
clamp(_active_pcam.viewport_position.y, min_vertical, max_vertical)
)
if not Engine.is_editor_hint():
target_point.position = camera_viewport_panel.size * unprojected_position_clamped - target_point.size / 2
if _is_2d:
if not is_instance_valid(pcam_host): return
if not is_instance_valid(pcam_host.camera_2d): return
var window_size_height: float = ProjectSettings.get_setting("display/window/size/viewport_height")
sub_viewport.size_2d_override = sub_viewport.size * (window_size_height / sub_viewport.size.y)
_camera_2d.global_transform = pcam_host.camera_2d.global_transform
_camera_2d.offset = pcam_host.camera_2d.offset
_camera_2d.zoom = pcam_host.camera_2d.zoom
_camera_2d.ignore_rotation = pcam_host.camera_2d.ignore_rotation
_camera_2d.anchor_mode = pcam_host.camera_2d.anchor_mode
_camera_2d.limit_left = pcam_host.camera_2d.limit_left
_camera_2d.limit_top = pcam_host.camera_2d.limit_top
_camera_2d.limit_right = pcam_host.camera_2d.limit_right
_camera_2d.limit_bottom = pcam_host.camera_2d.limit_bottom
func _settings_changed() -> void:
var viewport_width: float = ProjectSettings.get_setting("display/window/size/viewport_width")
var viewport_height: float = ProjectSettings.get_setting("display/window/size/viewport_height")
var ratio: float = viewport_width / viewport_height
aspect_ratio_container.set_ratio(ratio)
camera_viewport_panel.size.x = viewport_width / (viewport_height / sub_viewport.size.y)
## Applies Project Settings to Viewport
sub_viewport.canvas_item_default_texture_filter = ProjectSettings.get_setting("rendering/textures/canvas_textures/default_texture_filter")
# TODO - Add resizer for Framed Viewfinder
func _node_added_or_removed(_node: Node) -> void:
visibility_check()
func visibility_check() -> void:
if not viewfinder_visible: return
var phantom_camera_host: PhantomCameraHost
var has_camera: bool = false
if not get_tree().root.get_node(_constants.PCAM_MANAGER_NODE_NAME).get_phantom_camera_hosts().is_empty():
has_camera = true
phantom_camera_host = get_tree().root.get_node(_constants.PCAM_MANAGER_NODE_NAME).get_phantom_camera_hosts()[0]
var root: Node = EditorInterface.get_edited_scene_root()
if root is Node2D:
var camera_2d: Camera2D
if has_camera:
camera_2d = phantom_camera_host.camera_2d
else:
camera_2d = _get_camera_2d()
_is_2d = true
is_scene = true
_add_node_button.set_visible(true)
_check_camera(root, camera_2d, true)
elif root is Node3D:
var camera_3d: Camera3D
if has_camera:
camera_3d = phantom_camera_host.camera_3d
elif root.get_viewport() != null:
if root.get_viewport().get_camera_3d() != null:
camera_3d = root.get_viewport().get_camera_3d()
_is_2d = false
is_scene = true
_add_node_button.set_visible(true)
_check_camera(root, camera_3d, false)
else:
is_scene = false
# Is not a 2D or 3D scene
_set_empty_viewfinder_state(_no_open_scene_string, _no_open_scene_icon)
_add_node_button.set_visible(false)
if not _priority_override_button.pressed.is_connected(_select_override_pcam):
_priority_override_button.pressed.connect(_select_override_pcam)
func _get_camera_2d() -> Camera2D:
var edited_scene_root = EditorInterface.get_edited_scene_root()
if edited_scene_root == null: return null
var viewport = edited_scene_root.get_viewport()
if viewport == null: return null
var viewport_rid = viewport.get_viewport_rid()
if viewport_rid == null: return null
var camerasGroupName = "__cameras_%d" % viewport_rid.get_id()
var cameras = get_tree().get_nodes_in_group(camerasGroupName)
for camera in cameras:
if camera is Camera2D and camera.is_current:
return camera
return null
func _check_camera(root: Node, camera: Node, is_2D: bool) -> void:
var camera_string: String
var pcam_string: String
var color: Color
var color_alpha: Color
var camera_icon: CompressedTexture2D
var pcam_icon: CompressedTexture2D
if is_2D:
camera_string = _constants.CAMERA_2D_NODE_NAME
pcam_string = _constants.PCAM_2D_NODE_NAME
color = _constants.COLOR_2D
camera_icon = _camera_2d_icon
pcam_icon = _pcam_2D_icon
else:
camera_string = _constants.CAMERA_3D_NODE_NAME
pcam_string = _constants.PCAM_3D_NODE_NAME
color = _constants.COLOR_3D
camera_icon = _camera_3d_icon
pcam_icon = _pcam_3D_icon
if camera:
# Has Camera
if camera.get_children().size() > 0:
for cam_child in camera.get_children():
if cam_child is PhantomCameraHost:
pcam_host = cam_child
if pcam_host:
if get_tree().root.get_node(_constants.PCAM_MANAGER_NODE_NAME).get_phantom_camera_2ds() or \
get_tree().root.get_node(_constants.PCAM_MANAGER_NODE_NAME).get_phantom_camera_3ds():
# Pcam exists in tree
_set_viewfinder(root, true)
# if pcam_host.get_active_pcam().get_get_follow_mode():
# _on_dead_zone_changed()
_set_viewfinder_state()
%NoSupportMsg.set_visible(false)
else:
# No PCam in scene
_update_button(pcam_string, pcam_icon, color)
_set_empty_viewfinder_state(pcam_string, pcam_icon)
else:
# No PCamHost in scene
_update_button(_constants.PCAM_HOST_NODE_NAME, _pcam_host_icon, _constants.PCAM_HOST_COLOR)
_set_empty_viewfinder_state(_constants.PCAM_HOST_NODE_NAME, _pcam_host_icon)
else:
# No PCamHost in scene
_update_button(_constants.PCAM_HOST_NODE_NAME, _pcam_host_icon, _constants.PCAM_HOST_COLOR)
_set_empty_viewfinder_state(_constants.PCAM_HOST_NODE_NAME, _pcam_host_icon)
else:
# No Camera
_update_button(camera_string, camera_icon, color)
_set_empty_viewfinder_state(camera_string, camera_icon)
func _update_button(text: String, icon: CompressedTexture2D, color: Color) -> void:
_add_node_button_text.set_text("[center]Add [img=32]" + icon.resource_path + "[/img] [b]"+ text + "[/b][/center]");
var button_theme_hover: StyleBoxFlat = _add_node_button.get_theme_stylebox("hover")
button_theme_hover.border_color = color
_add_node_button.add_theme_stylebox_override("hover", button_theme_hover)
func _set_viewfinder_state() -> void:
_empty_state_control.set_visible(false)
_framed_viewfinder.set_visible(true)
if is_instance_valid(_active_pcam):
if _active_pcam.get_follow_mode() == _active_pcam.FollowMode.FRAMED:
_dead_zone_h_box_container.set_visible(true)
target_point.set_visible(true)
else:
_dead_zone_h_box_container.set_visible(false)
target_point.set_visible(false)
func _set_empty_viewfinder_state(text: String, icon: CompressedTexture2D) -> void:
_framed_viewfinder.set_visible(false)
target_point.set_visible(false)
_empty_state_control.set_visible(true)
_empty_state_icon.set_texture(icon)
if icon == _no_open_scene_icon:
_empty_state_text.set_text("[center]No " + text + "[/center]")
else:
_empty_state_text.set_text("[center]No [b]" + text + "[/b] in scene[/center]")
if _add_node_button.pressed.is_connected(_add_node):
_add_node_button.pressed.disconnect(_add_node)
_add_node_button.pressed.connect(_add_node.bind(text))
func _add_node(node_type: String) -> void:
var root: Node = EditorInterface.get_edited_scene_root()
match node_type:
_no_open_scene_string:
pass
_constants.CAMERA_2D_NODE_NAME:
var camera: Camera2D = Camera2D.new()
_instantiate_node(root, camera, _constants.CAMERA_2D_NODE_NAME)
_constants.CAMERA_3D_NODE_NAME:
var camera: Camera3D = Camera3D.new()
_instantiate_node(root, camera, _constants.CAMERA_3D_NODE_NAME)
_constants.PCAM_HOST_NODE_NAME:
var pcam_host: PhantomCameraHost = PhantomCameraHost.new()
pcam_host.set_name(_constants.PCAM_HOST_NODE_NAME)
if _is_2d:
# get_tree().get_edited_scene_root().get_viewport().get_camera_2d().add_child(pcam_host)
_get_camera_2d().add_child(pcam_host)
pcam_host.set_owner(get_tree().get_edited_scene_root())
else:
# var pcam_3D := get_tree().get_edited_scene_root().get_viewport().get_camera_3d()
get_tree().get_edited_scene_root().get_viewport().get_camera_3d().add_child(pcam_host)
pcam_host.set_owner(get_tree().get_edited_scene_root())
_constants.PCAM_2D_NODE_NAME:
var pcam_2D: PhantomCamera2D = PhantomCamera2D.new()
_instantiate_node(root, pcam_2D, _constants.PCAM_2D_NODE_NAME)
_constants.PCAM_3D_NODE_NAME:
var pcam_3D: PhantomCamera3D = PhantomCamera3D.new()
_instantiate_node(root, pcam_3D, _constants.PCAM_3D_NODE_NAME)
func _instantiate_node(root: Node, node: Node, name: String) -> void:
node.set_name(name)
root.add_child(node)
node.set_owner(get_tree().get_edited_scene_root())
func _set_viewfinder(root: Node, editor: bool) -> void:
pcam_host_group = get_tree().root.get_node(_constants.PCAM_MANAGER_NODE_NAME).get_phantom_camera_hosts()
if pcam_host_group.size() != 0:
if pcam_host_group.size() == 1:
var pcam_host: PhantomCameraHost = pcam_host_group[0]
if _is_2d:
_selected_camera = pcam_host.camera_2d
_active_pcam = pcam_host.get_active_pcam() as PhantomCamera2D
if editor:
var camera_2d_rid: RID = _selected_camera.get_canvas()
sub_viewport.disable_3d = true
_camera_2d.zoom = pcam_host.camera_2d.zoom
_camera_2d.offset = pcam_host.camera_2d.offset
_camera_2d.ignore_rotation = pcam_host.camera_2d.ignore_rotation
sub_viewport.world_2d = pcam_host.camera_2d.get_world_2d()
sub_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
sub_viewport.render_target_clear_mode = SubViewport.CLEAR_MODE_ALWAYS
sub_viewport.size_2d_override_stretch = true
else:
_selected_camera = pcam_host.camera_3d
_active_pcam = pcam_host.get_active_pcam() as PhantomCamera3D
if editor:
var camera_3d_rid: RID = _selected_camera.get_camera_rid()
sub_viewport.disable_3d = false
sub_viewport.world_3d = pcam_host.camera_3d.get_world_3d()
RenderingServer.viewport_attach_camera(sub_viewport.get_viewport_rid(), camera_3d_rid)
if _selected_camera.keep_aspect == Camera3D.KeepAspect.KEEP_HEIGHT:
aspect_ratio_container.set_stretch_mode(AspectRatioContainer.STRETCH_HEIGHT_CONTROLS_WIDTH)
else:
aspect_ratio_container.set_stretch_mode(AspectRatioContainer.STRETCH_WIDTH_CONTROLS_HEIGHT)
_on_dead_zone_changed()
set_process(true)
if not pcam_host.update_editor_viewfinder.is_connected(_on_update_editor_viewfinder):
pcam_host.update_editor_viewfinder.connect(_on_update_editor_viewfinder.bind(pcam_host))
if not aspect_ratio_container.resized.is_connected(_resized):
aspect_ratio_container.resized.connect(_resized)
if not _active_pcam.dead_zone_changed.is_connected(_on_dead_zone_changed):
_active_pcam.dead_zone_changed.connect(_on_dead_zone_changed)
func _resized() -> void:
_on_dead_zone_changed()
func _on_dead_zone_changed() -> void:
if not is_instance_valid(_active_pcam): return
if not _active_pcam.follow_mode == _active_pcam.FollowMode.FRAMED: return
# Waits until the camera_viewport_panel has been resized when launching the game
if camera_viewport_panel.size.x == 0:
await camera_viewport_panel.resized
#print(_active_pcam.get_pcam_host_owner())
if is_instance_valid(_active_pcam.get_pcam_host_owner()):
pcam_host = _active_pcam.get_pcam_host_owner()
if not _active_pcam == pcam_host.get_active_pcam():
_active_pcam == pcam_host.get_active_pcam()
print("Active pcam in viewfinder: ", _active_pcam)
var dead_zone_width: float = _active_pcam.dead_zone_width * camera_viewport_panel.size.x
var dead_zone_height: float = _active_pcam.dead_zone_height * camera_viewport_panel.size.y
dead_zone_center_hbox.set_custom_minimum_size(Vector2(dead_zone_width, 0))
dead_zone_center_center_panel.set_custom_minimum_size(Vector2(0, dead_zone_height))
dead_zone_left_center_panel.set_custom_minimum_size(Vector2(0, dead_zone_height))
dead_zone_right_center_panel.set_custom_minimum_size(Vector2(0, dead_zone_height))
min_horizontal = 0.5 - _active_pcam.dead_zone_width / 2
max_horizontal = 0.5 + _active_pcam.dead_zone_width / 2
min_vertical = 0.5 - _active_pcam.dead_zone_height / 2
max_vertical = 0.5 + _active_pcam.dead_zone_height / 2
####################
## Priority Override
####################
func _on_update_editor_viewfinder(pcam_host: PhantomCameraHost) -> void:
if pcam_host.get_active_pcam().priority_override:
_active_pcam = pcam_host.get_active_pcam()
_priority_override_button.set_visible(true)
_priority_override_name_label.set_text(_active_pcam.name)
_priority_override_button.set_tooltip_text(_active_pcam.name)
else:
_priority_override_button.set_visible(false)
func _select_override_pcam() -> void:
EditorInterface.get_selection().clear()
EditorInterface.get_selection().add_node(_active_pcam)
#endregion
#region Public Functions
func update_dead_zone() -> void:
_set_viewfinder(root_node, true)
func scene_changed(scene_root: Node) -> void:
if not scene_root is Node2D and not scene_root is Node3D:
is_scene = false
_set_empty_viewfinder_state(_no_open_scene_string, _no_open_scene_icon)
_add_node_button.set_visible(false)
#endregion

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
@tool
extends RefCounted
#region Constants
#const PhantomCameraHost: Script = preload("res://addons/phantom_camera/scripts/phantom_camera_host/phantom_camera_host.gd")
const CAMERA_2D_NODE_NAME: StringName = "Camera2D"
const CAMERA_3D_NODE_NAME: StringName = "Camera3D"
const PCAM_HOST_NODE_NAME: StringName = "PhantomCameraHost"
const PCAM_MANAGER_NODE_NAME: String = "PhantomCameraManager" # TODO - Convert to StringName once https://github.com/godotengine/godot/pull/72702 is merged
const PCAM_2D_NODE_NAME: StringName = "PhantomCamera2D"
const PCAM_3D_NODE_NAME: StringName = "PhantomCamera3D"
const PCAM_HOST: StringName = "phantom_camera_host"
const COLOR_2D: Color = Color("8DA5F3")
const COLOR_3D: Color = Color("FC7F7F")
const COLOR_PCAM: Color = Color("3AB99A")
const COLOR_PCAM_33: Color = Color("3ab99a33")
const PCAM_HOST_COLOR: Color = Color("E0E0E0")
#endregion
#region Group Names
const PCAM_GROUP_NAME: StringName = "phantom_camera_group"
const PCAM_HOST_GROUP_NAME: StringName = "phantom_camera_host_group"
#endregion

View File

@@ -0,0 +1,264 @@
@tool
@icon("res://addons/phantom_camera/icons/phantom_camera_noise_emitter_2d.svg")
class_name PhantomCameraNoiseEmitter2D
extends Node2D
## Emits positional and rotational noise to active [PhantomCamera2D]s and its corresponding [Camera2D].
##
## Is a node meant to apply positional and rotational noise, also referred to as shake, to the [Camera2D].
## It is designed for use cases such as when hitting or when being hit, earthquakes or to add a
## bit of slight movement to the camera to make it feel less static.
## The emitter can affect multiple [PhantomCamera2D] in a given scene based on which [member noise_emitter_layer]
## are enabled by calling its [method emit] function. At least one corresponding layer has to be
## set on the [PhantomCamera2D] and the emitter node.
const _constants = preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd")
#region Exported Proerpties
## The [PhantomCameraNoise2D] resource that defines the noise pattern.
@export var noise: PhantomCameraNoise2D = null:
set = set_noise,
get = get_noise
## If true, previews the noise in the editor - can be seen in the viewfinder.
@export var preview: bool = false:
set(value):
preview = value
_play = value
get:
return preview
## If true, repeats the noise indefinitely once started. Otherwise, it will only be triggered once. [br]
@export var continuous: bool = false:
set = set_continuous,
get = get_continuous
## Determines how long the noise should take to reach full [member intensity] once started.[br]
## The value is set in [b]seconds[/b].
@export_exp_easing("positive_only", "suffix: s") var growth_time: float = 0:
set = set_growth_time,
get = get_growth_time
## Sets the duration for the camera noise if [member continuous] is set to [b]false[/b].[br][br]
## The value is set in [b]seconds[/b].
@export_range(0, 10, 0.001, "or_greater", "suffix: s") var duration: float = 1.0:
set = set_duration,
get = get_duration
## Determines how long the noise should take to come to a full stop.[br]
## The value is set in [b]seconds[/b].
@export_exp_easing("attenuation", "positive_only", "suffix: s") var decay_time: float = 0:
set = set_decay_time,
get = get_decay_time
## Enabled layers will affect [PhantomCamera2D] nodes with at least one corresponding layer enabled.[br]
## Enabling multiple corresponding layers on the same [PhantomCamera2D] causes no additional effect.
@export_flags_2d_render var noise_emitter_layer: int = 1:
set = set_noise_emitter_layer,
get = get_noise_emitter_layer
#endregion
#region Private Variables
var _play: bool = false:
set(value):
_play = value
if value:
_elasped_play_time = 0
_decay_countdown = 0
_play = true
_should_grow = true
_start_duration_countdown = false
_should_decay = false
else:
_should_decay = true
if noise.randomize_noise_seed:
noise.noise_seed = randi() & 1000
else:
noise.reset_noise_time()
get:
return _play
var _start_duration_countdown: bool = false
var _decay_countdown: float = 0
var _should_grow: bool = false
var _should_decay: bool = false
var _elasped_play_time: float = 0
var _noise_output: Transform2D = Transform2D()
# NOTE - Temp solution until Godot has better plugin autoload recognition out-of-the-box.
var _phantom_camera_manager: Node
#endregion
#region Private Functions
func _get_configuration_warnings() -> PackedStringArray:
if noise == null:
return ["Noise resource is required in order to trigger emitter."]
else:
return []
func _validate_property(property) -> void:
if property.name == "duration" and continuous:
property.usage = PROPERTY_USAGE_NO_EDITOR
func _enter_tree() -> void:
_phantom_camera_manager = get_tree().root.get_node(_constants.PCAM_MANAGER_NODE_NAME)
func _process(delta: float) -> void:
if not _play and not _should_decay: return
if noise == null:
printerr("Noise resource missing in ", name)
_play = false
return
_elasped_play_time += delta
if _should_grow:
noise.set_trauma(minf(_elasped_play_time / growth_time, 1))
if _elasped_play_time >= growth_time:
_should_grow = false
_start_duration_countdown = true
noise.set_trauma(1)
else:
noise.set_trauma(1)
if not continuous:
if _start_duration_countdown:
if _elasped_play_time >= duration + growth_time:
_should_decay = true
_start_duration_countdown = false
if _should_decay:
_decay_countdown += delta
noise.set_trauma(maxf(1 - (_decay_countdown / decay_time), 0))
if _decay_countdown >= decay_time:
noise.set_trauma(0)
_play = false
preview = false
_should_decay = false
_elasped_play_time = 0
_decay_countdown = 0
_noise_output = noise.get_noise_transform(delta)
_phantom_camera_manager.noise_2d_emitted.emit(_noise_output, noise_emitter_layer)
func _set_layer(current_layers: int, layer_number: int, value: bool) -> int:
var mask: int = current_layers
# From https://github.com/godotengine/godot/blob/51991e20143a39e9ef0107163eaf283ca0a761ea/scene/3d/camera_3d.cpp#L638
if layer_number < 1 or layer_number > 20:
printerr("Layer must be between 1 and 20.")
else:
if value:
mask |= 1 << (layer_number - 1)
else:
mask &= ~(1 << (layer_number - 1))
return mask
#endregion
#region Public Functions
## Emits noise to the [PhantomCamera2D]s that has at least one matching layers.
func emit() -> void:
if _play: _play = false
_play = true
## Returns the state for the emitter. If true, the emitter is currently emitting.
func is_emitting() -> bool:
return _play
## Stops the emitter from emitting noise.
func stop(should_decay: bool = true) -> void:
if should_decay:
_should_decay = true
else:
_play = false
## Toggles the emitter on and off.
func toggle() -> void:
_play = !_play
#endregion
#region Setter & Getter Functions
## Sets the [member noise] resource.
func set_noise(value: PhantomCameraNoise2D) -> void:
noise = value
update_configuration_warnings()
## Returns the [member noise] resource.
func get_noise() -> PhantomCameraNoise2D:
return noise
## Sets the [member continous] value.
func set_continuous(value: bool) -> void:
continuous = value
notify_property_list_changed()
## Gets the [member continous] value.
func get_continuous() -> bool:
return continuous
## Sets the [member growth_time] value.
func set_growth_time(value: float) -> void:
growth_time = value
## Returns the [member growth_time] value.
func get_growth_time() -> float:
return growth_time
## Sets the [member duration] value.
func set_duration(value: float) -> void:
duration = value
if duration == 0:
duration = 0.001
## Returns the [member duration] value.
func get_duration() -> float:
return duration
## Sets the [member decay_time] value.
func set_decay_time(value: float) -> void:
decay_time = value
## Returns the [member decay_time] value.
func get_decay_time() -> float:
return decay_time
## Sets the [member noise_emitter_layer] value.
func set_noise_emitter_layer(value: int) -> void:
noise_emitter_layer = value
## Enables or disables a given layer of the [member noise_emitter_layer] value.
func set_noise_emitter_value(value: int, enabled: bool) -> void:
noise_emitter_layer = _set_layer(noise_emitter_layer, value, enabled)
## Returns the [member noise_emitter_layer] value.
func get_noise_emitter_layer() -> int:
return noise_emitter_layer
#endregion

View File

@@ -0,0 +1,266 @@
@tool
@icon("res://addons/phantom_camera/icons/phantom_camera_noise_emitter_3d.svg")
class_name PhantomCameraNoiseEmitter3D
extends Node3D
## Emits positional and rotational noise to active [PhantomCamera3D]s and its corresponding [Camera3D].
##
## Is a node meant to apply positional and rotational noise, also referred to as shake, to the [Camera3D].
## It is designed for use cases such as when hitting or when being hit, earthquakes or to add a
## bit of slight movement to the camera to make it feel less static.
## The emitter can affect multiple [PhantomCamera3D] in a given scene based on which [member noise_emitter_layer]
## are enabled by calling its [method emit] function. At least one corresponding layer has to be
## set on the [PhantomCamera3D] and the emitter node.
const _constants = preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd")
#region Exported Properties
## The [PhantomCameraNoise3D] resource that defines the noise pattern.
@export var noise: PhantomCameraNoise3D = null:
set = set_noise,
get = get_noise
## If true, previews the noise in the Viewfinder.
@export var preview: bool = false:
set(value):
preview = value
_play = value
get:
return preview
## If true, repeats the noise indefinitely once started.Otherwise, it will only be triggered once. [br]
## [b]Note:[/b] This will always be enabled if the resource is assigned the the [PhantomCamera3D]'s
## [member PhantomCamera3D.noise] property.
@export var continuous: bool = false:
set = set_continuous,
get = get_continuous
## Determines how long the noise should take to reach full [member intensity] once started.[br]
## The value is set in [b]seconds[/b].
@export_exp_easing("positive_only", "suffix: s") var growth_time: float = 0:
set = set_growth_time,
get = get_growth_time
## Sets the duration for the camera noise if [member loop] is set to false.[br]
## If the duration is [param 0] then [member continous] becomes enabled.[br]
## The value is set in [b]seconds[/b].
@export_range(0, 10, 0.001, "or_greater", "suffix: s") var duration: float = 1.0:
set = set_duration,
get = get_duration
## Determines how long the noise should take to come to a full stop.[br]
## The value is set in [b]seconds[/b].
@export_exp_easing("attenuation", "positive_only", "suffix: s") var decay_time: float = 0:
set = set_decay_time,
get = get_decay_time
## Enabled layers will affect [PhantomCamera3D] nodes with at least one corresponding layer enabled.[br]
## Enabling multiple corresponding layers on the same [PhantomCamera3D] causes no additional effect.
@export_flags_3d_render var noise_emitter_layer: int = 1:
set = set_noise_emitter_layer,
get = get_noise_emitter_layer
#endregion
#region Private Variables
var _play: bool = false:
set(value):
_play = value
if value:
_elasped_play_time = 0
_decay_countdown = 0
_play = true
_should_grow = true
_start_duration_countdown = false
_should_decay = false
else:
_should_decay = true
if noise.randomize_noise_seed:
noise.noise_seed = randi() & 1000
else:
noise.reset_noise_time()
get:
return _play
var _start_duration_countdown: bool = false
var _decay_countdown: float = 0
var _should_grow: bool = false
var _should_decay: bool = false
var _elasped_play_time: float = 0
var _noise_output: Transform3D = Transform3D()
# NOTE - Temp solution until Godot has better plugin autoload recognition out-of-the-box.
var _phantom_camera_manager: Node
#endregion
#region Private Functions
func _get_configuration_warnings() -> PackedStringArray:
if noise == null:
return ["Noise resource is required in order to trigger emitter."]
else:
return []
func _validate_property(property) -> void:
if property.name == "duration" and continuous:
property.usage = PROPERTY_USAGE_NO_EDITOR
func _enter_tree() -> void:
_phantom_camera_manager = get_tree().root.get_node(_constants.PCAM_MANAGER_NODE_NAME)
func _process(delta: float) -> void:
if not _play and not _should_decay: return
if noise == null:
printerr("Noise resource missing in ", name)
_play = false
return
_elasped_play_time += delta
if _should_grow:
noise.set_trauma(minf(_elasped_play_time / growth_time, 1))
if _elasped_play_time >= growth_time:
_should_grow = false
_start_duration_countdown = true
noise.set_trauma(1)
if not continuous:
if _start_duration_countdown:
if _elasped_play_time >= duration + growth_time:
_should_decay = true
_start_duration_countdown = false
if _should_decay:
_decay_countdown += delta
noise.set_trauma(maxf(1 - (_decay_countdown / decay_time), 0))
if _decay_countdown >= decay_time:
noise.set_trauma(0)
_play = false
preview = false
_should_decay = false
_elasped_play_time = 0
_decay_countdown = 0
_noise_output = noise.get_noise_transform(delta)
_phantom_camera_manager.noise_3d_emitted.emit(_noise_output, noise_emitter_layer)
func _set_layer(current_layers: int, layer_number: int, value: bool) -> int:
var mask: int = current_layers
# From https://github.com/godotengine/godot/blob/51991e20143a39e9ef0107163eaf283ca0a761ea/scene/3d/camera_3d.cpp#L638
if layer_number < 1 or layer_number > 20:
printerr("Layer must be between 1 and 20.")
else:
if value:
mask |= 1 << (layer_number - 1)
else:
mask &= ~(1 << (layer_number - 1))
return mask
#endregion
#region Public Functions
## Emits noise to the [PhantomCamera3D]s that has at least one matching layers.
func emit() -> void:
if _play: _play = false
_play = true
## Returns the state for the emitter. If true, the emitter is currently emitting.
func is_emitting() -> bool:
return _play
## Stops the emitter from emitting noise.
func stop(should_decay: bool = true) -> void:
if should_decay:
_should_decay = true
else:
_play = false
## Toggles the emitter on and off.[br]
func toggle() -> void:
_play = !_play
#endregion
#region Setter & Getter Functions
## Sets the [member noise] resource.
func set_noise(value: PhantomCameraNoise3D) -> void:
noise = value
update_configuration_warnings()
## Returns the [member noise] resource.
func get_noise() -> PhantomCameraNoise3D:
return noise
## Sets the [member continous] value.
func set_continuous(value: bool) -> void:
continuous = value
notify_property_list_changed()
## Gets the [member continous] value.
func get_continuous() -> bool:
return continuous
## Sets the [member growth_time] value.
func set_growth_time(value: float) -> void:
growth_time = value
## Returns the [member growth_time] value.
func get_growth_time() -> float:
return growth_time
## Sets the [member duration] value.
func set_duration(value: float) -> void:
duration = value
if duration == 0:
duration = 0.001
## Returns the [member duration] value.
func get_duration() -> float:
return duration
## Sets the [member decay_time] value.
func set_decay_time(value: float) -> void:
decay_time = value
## Returns the [member decay_time] value.
func get_decay_time() -> float:
return decay_time
## Sets the [member noise_emitter_layer] value.
func set_noise_emitter_layer(value: int) -> void:
noise_emitter_layer = value
## Enables or disables a given layer of the [member noise_emitter_layer] value.
func set_noise_emitter_value(value: int, enabled: bool) -> void:
noise_emitter_layer = _set_layer(noise_emitter_layer, value, enabled)
## Returns the [member noise_emitter_layer] value.
func get_noise_emitter_layer() -> int:
return noise_emitter_layer
#endregion

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,59 @@
@tool
@icon("res://addons/phantom_camera/icons/phantom_camera_camera_3d_resource.svg")
class_name Camera3DResource
extends Resource
## Resource for [PhantomCamera3D] to override various [Camera3D] properties.
##
## The overrides defined here will be applied to the [Camera3D] upon the
## [PhantomCamera3D] becoming active.
## Overrides [member Camera3D.cull_mask].
@export_flags_3d_render var cull_mask: int = 1048575
## Overrides [member Camera3D.h_offset].
@export_range(0, 1, 0.001, "hide_slider", "suffix:m") var h_offset: float = 0
## Overrides [member Camera3D.v_offset].
@export_range(0, 1, 0.001, "hide_slider", "suffix:m") var v_offset: float = 0
enum ProjectionType {
PERSPECTIVE = 0, ## Perspective projection. Objects on the screen becomes smaller when they are far away.
ORTHOGONAL = 1, ## Orthogonal projection, also known as orthographic projection. Objects remain the same size on the screen no matter how far away they are.
FRUSTUM = 2, ## Frustum projection. This mode allows adjusting frustum_offset to create "tilted frustum" effects.
}
## Overrides [member Camera3D.projection].
@export var projection: ProjectionType = ProjectionType.PERSPECTIVE:
set(value):
projection = value
notify_property_list_changed()
get:
return projection
## Overrides [member Camera3D.fov].
@export_range(1, 179, 0.1, "degrees") var fov: float = 75
## Overrides [member Camera3D.size].
@export_range(0.001, 100, 0.001, "suffix:m", "or_greater") var size: float = 1
## Overrides [member Camera3d.frustum_offset].
@export var frustum_offset: Vector2 = Vector2.ZERO
## Overrides [member Camera3D.near].
@export_range(0.001, 10, 0.001, "suffix:m", "or_greater") var near: float = 0.05
## Overrides [member Camera3D.far].
@export_range(0.01, 4000, 0.001, "suffix:m","or_greater") var far: float = 4000
func _validate_property(property: Dictionary) -> void:
if property.name == "fov" and not projection == ProjectionType.PERSPECTIVE:
property.usage = PROPERTY_USAGE_NO_EDITOR
if property.name == "size" and projection == ProjectionType.PERSPECTIVE:
property.usage = PROPERTY_USAGE_NO_EDITOR
if property.name == "frustum_offset" and not projection == ProjectionType.FRUSTUM:
property.usage = PROPERTY_USAGE_NO_EDITOR

View File

@@ -0,0 +1,228 @@
@tool
@icon("res://addons/phantom_camera/icons/phantom_camera_noise_resource.svg")
class_name PhantomCameraNoise2D
extends Resource
## A resource type used to apply noise, or shake, to [Camera2D]s that have a [PhantomCameraHost] as a child.
##
## Is a resource type that defines, calculates and outputs the noise values to a [Camera2D] through active
## [PhantomCamera3D].[br]
## It can be applied to either [PhantomCameraNoiseEmitter2D] or a [PhantomCamera2D] noise property directly
#region Exported Properties
## Defines the size of the noise pattern.[br]
## Higher values will increase the range the noise can reach.
@export_range(0, 1000, 0.001, "or_greater") var amplitude: float = 10:
set = set_amplitude,
get = get_amplitude
## Sets the density of the noise pattern.[br]
## Higher values will result in more erratic noise.
@export_range(0, 10, 0.001, "or_greater") var frequency: float = 0.5:
set = set_frequency,
get = get_frequency
## If true, randomizes the noise pattern every time the noise is run.[br]
## If disabled, [member seed] can be used to define a fixed noise pattern.
@export var randomize_noise_seed: bool = true:
set = set_randomize_noise_seed,
get = get_randomize_noise_seed
## Sets a predetermined seed noise value.[br]
## Useful if wanting to achieve a persistent noise pattern every time the noise is emitted.
@export var noise_seed: int = 0:
set = set_noise_seed,
get = get_noise_seed
## Enables noise changes to the [member Camera2D.offset] position.
@export var positional_noise: bool = true:
set = set_positional_noise,
get = get_positional_noise
## Enables noise changes to the [Camera2D]'s rotation.
@export var rotational_noise: bool = false:
set = set_rotational_noise,
get = get_rotational_noise
@export_group("Positional Multiplier")
## Multiplies positional noise amount in the X-axis.[br]
## Set the value to [param 0] to disable noise in the axis.
@export_range(0, 1, 0.001, "or_greater") var positional_multiplier_x: float = 1:
set = set_positional_multiplier_x,
get = get_positional_multiplier_x
## Multiplies positional noise amount in the Y-axis.[br]
## Set the value to [param 0] to disable noise in the axis.
@export_range(0, 1, 0.001, "or_greater") var positional_multiplier_y: float = 1:
set = set_positional_multiplier_y,
get = get_positional_multiplier_y
@export_group("Rotational Multiplier")
## Multiplies rotational noise amount.
@export_range(0, 1, 0.001, "or_greater") var rotational_multiplier: float = 1:
set = set_rotational_multiplier,
get = get_rotational_multiplier
#endregion
#region Private Variables
var _noise_algorithm: FastNoiseLite = FastNoiseLite.new()
var _noise_positional_multiplier: Vector2 = Vector2(
positional_multiplier_x,
positional_multiplier_y
)
var _trauma: float = 0.0:
set(value):
_trauma = value
var _noise_time: float = 0.0
#endregion
#region Private Functions
func _init():
_noise_algorithm.noise_type = FastNoiseLite.TYPE_PERLIN
if randomize_noise_seed: _noise_algorithm.seed = randi()
_noise_algorithm.frequency = frequency
func _validate_property(property: Dictionary) -> void:
if randomize_noise_seed and property.name == "noise_seed":
property.usage = PROPERTY_USAGE_NO_EDITOR
if not rotational_noise and property.name == "rotational_multiplier":
property.usage = PROPERTY_USAGE_NO_EDITOR
if not positional_noise:
match property.name:
"positional_multiplier_x", \
"positional_multiplier_y":
property.usage = PROPERTY_USAGE_NO_EDITOR
func _get_noise_from_seed(noise_seed: int) -> float:
return _noise_algorithm.get_noise_2d(noise_seed, _noise_time) * amplitude
func set_trauma(value: float) -> void:
_trauma = value
#endregion
#region Public Functions
func get_noise_transform(delta: float) -> Transform2D:
var output_position: Vector2 = Vector2.ZERO
var output_rotation: float = 0.0
_noise_time += delta
_trauma = maxf(_trauma, 0.0)
if positional_noise:
for i in 2:
output_position[i] = _noise_positional_multiplier[i] * pow(_trauma, 2) * _get_noise_from_seed(i + noise_seed)
if rotational_noise:
output_rotation = rotational_multiplier / 100 * pow(_trauma, 2) * _get_noise_from_seed(noise_seed)
return Transform2D(output_rotation, output_position)
func reset_noise_time() -> void:
_noise_time = 0
#endregion
#region Setters & Getters
## Sets the [member amplitude] value.
func set_amplitude(value: float) -> void:
amplitude =value
## Returns the [member amplitude] value.
func get_amplitude() -> float:
return amplitude
## Sets the [member frequency] value.
func set_frequency(value: float) -> void:
frequency = value
_noise_algorithm.frequency = value
## Returns the [member frequency] value.
func get_frequency() -> float:
return frequency
## Sets the [member randomize_seed] value.
func set_randomize_noise_seed(value: int) -> void:
randomize_noise_seed = value
if value: _noise_algorithm.seed = randi()
notify_property_list_changed()
## Returns the [member randomize_seed] value.
func get_randomize_noise_seed() -> int:
return randomize_noise_seed
## Sets the [member randomize_seed] value.
func set_noise_seed(value: int) -> void:
noise_seed = value
## Returns the [member seed] value.
func get_noise_seed() -> int:
return noise_seed
## Sets the [member positional_noise] value.
func set_positional_noise(value: bool) -> void:
positional_noise = value
notify_property_list_changed()
## Returns the [member positional_noise] value.
func get_positional_noise() -> bool:
return positional_noise
## Sets the [member rotational_noise] value.
func set_rotational_noise(value: bool) -> void:
rotational_noise = value
notify_property_list_changed()
## Returns the [member rotational_noise] value.
func get_rotational_noise() -> bool:
return rotational_noise
## Sets the [member positional_multiplier_x] value.
func set_positional_multiplier_x(value: float) -> void:
positional_multiplier_x = value
_noise_positional_multiplier.x = value
## Returns the [member positional_multiplier_x] value.
func get_positional_multiplier_x() -> float:
return positional_multiplier_x
## Sets the [member positional_multiplier_y] value.
func set_positional_multiplier_y(value: float) -> void:
positional_multiplier_y = value
_noise_positional_multiplier.y = value
## Returns the [member positional_multiplier_y] value.
func get_positional_multiplier_y() -> float:
return positional_multiplier_y
## Sets the [member rotational_multiplier] value.
func set_rotational_multiplier(value: float) -> void:
rotational_multiplier = value
## Returns the [member rotational_multiplier] value.
func get_rotational_multiplier() -> float:
return rotational_multiplier
#endregion

View File

@@ -0,0 +1,301 @@
@tool
@icon("res://addons/phantom_camera/icons/phantom_camera_noise_resource.svg")
class_name PhantomCameraNoise3D
extends Resource
## A resource type used to apply noise, or shake, to [Camera3D]s that have a [PhantomCameraHost] as a child.
##
## Is a resource type that defines, calculates and outputs the noise values to a [Camera3D] through active
## [PhantomCamera3D].[br]
## It can be applied to either [PhantomCameraNoiseEmitter3D] or a [PhantomCamera3D] noise property directly
#region Exported Properties
## Defines the size of the noise pattern.[br]
## Higher values will increase the range the noise can reach.
@export_range(0, 100, 0.001, "or_greater") var amplitude: float = 10:
set = set_amplitude,
get = get_amplitude
## Sets the density of the noise pattern.[br]
## Higher values will result in more erratic noise.
@export_range(0, 10, 0.001, "or_greater") var frequency: float = 0.2:
set = set_frequency,
get = get_frequency
## If true, randomizes the noise pattern every time the noise is run.[br]
## If disabled, [member seed] can be used to define a fixed noise pattern.
@export var randomize_noise_seed: bool = true:
set = set_randomize_noise_seed,
get = get_randomize_noise_seed
## Sets a predetermined seed noise value.[br]
## Useful if wanting to achieve a persistent noise pattern every time the noise is emitted.
@export var noise_seed: int = 0:
set = set_noise_seed,
get = get_noise_seed
## Enables noise changes to the [Camera3D]'s rotation.
@export var rotational_noise: bool = true:
set = set_rotational_noise,
get = get_rotational_noise
## Enables noise changes to the camera's position.[br][br]
## [b]Important[/b][br]This can cause geometry clipping if the camera gets too close while this is active.
@export var positional_noise: bool = false:
set = set_positional_noise,
get = get_positional_noise
@export_group("Rotational Multiplier")
## Multiplies rotational noise amount in the X-axis.[br]
## Set the value to [param 0] to disable noise in the axis.
@export_range(0, 1, 0.001, "or_greater") var rotational_multiplier_x: float = 1:
set = set_rotational_multiplier_x,
get = get_rotational_multiplier_x
## Multiplies rotational noise amount in the Y-axis.[br]
## Set the value to [param 0] to disable noise in the axis.
@export_range(0, 1, 0.001, "or_greater") var rotational_multiplier_y: float = 1:
set = set_rotational_multiplier_y,
get = get_rotational_multiplier_y
## Multiplies rotational noise amount in the Z-axis.[br]
## Set the value to [param 0] to disable noise in the axis.
@export_range(0, 1, 0.001, "or_greater") var rotational_multiplier_z: float = 1:
set = set_rotational_multiplier_z,
get = get_rotational_multiplier_z
@export_group("Positional Multiplier")
## Multiplies positional noise amount in the X-axis.[br]
## Set the value to [param 0] to disable noise in the axis.[br]
## [b]Note:[/b] Rotational Offset is recommended to avoid potential camera clipping with the environment.
@export_range(0, 1, 0.001, "or_greater") var positional_multiplier_x: float = 1:
set = set_positional_multiplier_x,
get = get_positional_multiplier_x
## Multiplies positional noise amount in the Y-axis.[br]
## Set the value to [param 0] to disable noise in the axis.[br]
## [b]Note:[/b] Rotational Offset is recommended to avoid potential camera clipping with the environment.
@export_range(0, 1, 0.001, "or_greater") var positional_multiplier_y: float = 1:
set = set_positional_multiplier_y,
get = get_positional_multiplier_y
## Multiplies positional noise amount in the Z-axis.[br]
## Set the value to [param 0] to disable noise in the axis.[br]
## [b]Note:[/b] Rotational Offset is recommended to avoid potential camera clipping with the environment.
@export_range(0, 1, 0.001, "or_greater") var positional_multiplier_z: float = 1:
set = set_positional_multiplier_z,
get = get_positional_multiplier_z
#endregion
#region Private Variables
var _noise_algorithm: FastNoiseLite = FastNoiseLite.new()
var _noise_rotational_multiplier: Vector3 = Vector3(
rotational_multiplier_x,
rotational_multiplier_y,
rotational_multiplier_z,
)
var _noise_positional_multiplier: Vector3 = Vector3(
positional_multiplier_x,
positional_multiplier_y,
positional_multiplier_z,
)
var _trauma: float = 0.0:
set(value):
_trauma = value
if _trauma == 0.0:
_noise_time = 0.0
var _noise_time: float = 0.0
#endregion
#region Private Functions
func _init():
_noise_algorithm.noise_type = FastNoiseLite.TYPE_PERLIN
if randomize_noise_seed: _noise_algorithm.seed = randi()
_noise_algorithm.frequency = frequency
func _validate_property(property: Dictionary) -> void:
if randomize_noise_seed and property.name == "noise_seed":
property.usage = PROPERTY_USAGE_NO_EDITOR
if not rotational_noise:
match property.name:
"rotational_multiplier_x", \
"rotational_multiplier_y", \
"rotational_multiplier_z":
property.usage = PROPERTY_USAGE_NO_EDITOR
if not positional_noise:
match property.name:
"positional_multiplier_x", \
"positional_multiplier_y", \
"positional_multiplier_z":
property.usage = PROPERTY_USAGE_NO_EDITOR
func _get_noise_from_seed(noise_seed: int) -> float:
return _noise_algorithm.get_noise_2d(noise_seed, _noise_time) * amplitude
func set_trauma(value: float) -> void:
_trauma = value
#endregion
#region Public Functions
func get_noise_transform(delta: float) -> Transform3D:
var output_rotation: Vector3 = Vector3.ZERO
var output_position: Vector3 = Vector3.ZERO
_noise_time += delta
_trauma = maxf(_trauma, 0.0)
for i in 3:
if rotational_noise:
output_rotation[i] = deg_to_rad(
_noise_rotational_multiplier[i] * pow(_trauma, 2) * _get_noise_from_seed(i + noise_seed)
)
if positional_noise:
output_position[i] += _noise_positional_multiplier[i] / 10 * \
pow(_trauma, 2) * _get_noise_from_seed(i + noise_seed)
return Transform3D(Quaternion.from_euler(output_rotation), output_position)
func reset_noise_time() -> void:
_noise_time = 0
#endregion
#region Setters & Getters
## Sets the [member amplitude] value.
func set_amplitude(value: float) -> void:
amplitude =value
## Returns the [member amplitude] value.
func get_amplitude() -> float:
return amplitude
## Sets the [member frequency] value.
func set_frequency(value: float) -> void:
frequency = value
_noise_algorithm.frequency = value
## Returns the [member frequency] value.
func get_frequency() -> float:
return frequency
## Sets the [member randomize_seed] value.
func set_randomize_noise_seed(value: int) -> void:
randomize_noise_seed = value
if value: _noise_algorithm.seed = randi()
notify_property_list_changed()
## Returns the [member randomize_seed] value.
func get_randomize_noise_seed() -> int:
return randomize_noise_seed
## Sets the [member randomize_seed] value.
func set_noise_seed(value: int) -> void:
noise_seed = value
## Returns the [member seed] value.
func get_noise_seed() -> int:
return noise_seed
## Sets the [member positional_noise] value.
func set_positional_noise(value: bool) -> void:
positional_noise = value
notify_property_list_changed()
## Returns the [member positional_noise] value.
func get_positional_noise() -> bool:
return positional_noise
## Sets the [member rotational_noise] value.
func set_rotational_noise(value: bool) -> void:
rotational_noise = value
notify_property_list_changed()
## Returns the [member rotational_noise] value.
func get_rotational_noise() -> bool:
return rotational_noise
## Sets the [member positional_multiplier_x] value.
func set_positional_multiplier_x(value: float) -> void:
positional_multiplier_x = value
_noise_positional_multiplier.x = value
## Returns the [member positional_multiplier_x] value.
func get_positional_multiplier_x() -> float:
return positional_multiplier_x
## Sets the [member positional_multiplier_y] value.
func set_positional_multiplier_y(value: float) -> void:
positional_multiplier_y = value
_noise_positional_multiplier.y = value
## Returns the [member positional_multiplier_y] value.
func get_positional_multiplier_y() -> float:
return positional_multiplier_y
## Sets the [member positional_multiplier_z] value.
func set_positional_multiplier_z(value: float) -> void:
positional_multiplier_z = value
_noise_positional_multiplier.z = value
## Returns the [member positional_multiplier_z] value.
func get_positional_multiplier_z() -> float:
return positional_multiplier_z
## Sets the [member rotational_multiplier_x] value.
func set_rotational_multiplier_x(value: float) -> void:
rotational_multiplier_x = value
_noise_rotational_multiplier.x = value
## Returns the [member rotational_multiplier_x] value.
func get_rotational_multiplier_x() -> float:
return rotational_multiplier_x
## Sets the [member rotational_multiplier_y] value.
func set_rotational_multiplier_y(value: float) -> void:
rotational_multiplier_y = value
_noise_rotational_multiplier.y = value
## Returns the [member rotational_multiplier_y] value.
func get_rotational_multiplier_y() -> float:
return rotational_multiplier_y
## Sets the [member rotational_multiplier_z] value.
func set_rotational_multiplier_z(value: float) -> void:
rotational_multiplier_z = value
_noise_rotational_multiplier.z = value
## Returns the [member rotational_multiplier_z] value.
func get_rotational_multiplier_z() -> float:
return rotational_multiplier_z
#endregion

View File

@@ -0,0 +1,41 @@
@icon("res://addons/phantom_camera/icons/phantom_camera_tween.svg")
class_name PhantomCameraTween
extends Resource
## Tweening resource for [PhantomCamera2D] and [PhantomCamera3D].
##
## Defines how [param PhantomCameras] transition between one another.
## Changing the tween values for a given [param PhantomCamera] determines how
## transitioning to that instance will look like.
enum TransitionType {
LINEAR = 0, ## The animation is interpolated linearly.
SINE = 1, ## The animation is interpolated using a sine function.
QUINT = 2, ## The animation is interpolated with a quintic (to the power of 5) function.
QUART = 3, ## The animation is interpolated with a quartic (to the power of 4) function.
QUAD = 4, ## The animation is interpolated with a quadratic (to the power of 2) function.
EXPO = 5, ## The animation is interpolated with an exponential (to the power of x) function.
ELASTIC = 6, ## The animation is interpolated with elasticity, wiggling around the edges.
CUBIC = 7, ## The animation is interpolated with a cubic (to the power of 3) function.
CIRC = 8, ## The animation is interpolated with a function using square roots.
BOUNCE = 9, ## The animation is interpolated by bouncing at the end.
BACK = 10, ## The animation is interpolated backing out at ends.
# CUSTOM = 11,
# NONE = 12,
}
enum EaseType {
EASE_IN = 0, ## The interpolation starts slowly and speeds up towards the end.
EASE_OUT = 1, ## The interpolation starts quickly and slows down towards the end.
EASE_IN_OUT = 2, ## A combination of EASE_IN and EASE_OUT. The interpolation is slowest at both ends.
EASE_OUT_IN = 3, ## A combination of EASE_IN and EASE_OUT. The interpolation is fastest at both ends.
}
## The time it takes to tween to this PhantomCamera in [param seconds].
@export var duration: float = 1
## The transition bezier type for the tween. The options are defined in the [enum TransitionType].
@export var transition: TransitionType = TransitionType.LINEAR
## The ease type for the tween. The options are defined in the [enum EaseType].
@export var ease: EaseType = EaseType.EASE_IN_OUT