diff --git a/addons/phantom_camera/assets/PhantomCameraBtnPrimaryDefault.png b/addons/phantom_camera/assets/PhantomCameraBtnPrimaryDefault.png new file mode 100644 index 0000000..d0f80bc --- /dev/null +++ b/addons/phantom_camera/assets/PhantomCameraBtnPrimaryDefault.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ea8a0d7a1d820b1d69746665c425cc155f351aedd00441e9adc9e8d20977a1a +size 10296 diff --git a/addons/phantom_camera/assets/PhantomCameraBtnPrimaryDefault.png.import b/addons/phantom_camera/assets/PhantomCameraBtnPrimaryDefault.png.import new file mode 100644 index 0000000..6582ece --- /dev/null +++ b/addons/phantom_camera/assets/PhantomCameraBtnPrimaryDefault.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://censw3w53gldn" +path="res://.godot/imported/PhantomCameraBtnPrimaryDefault.png-fcf3696b583a82b1078609a5bfd648f5.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/assets/PhantomCameraBtnPrimaryDefault.png" +dest_files=["res://.godot/imported/PhantomCameraBtnPrimaryDefault.png-fcf3696b583a82b1078609a5bfd648f5.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/addons/phantom_camera/assets/PhantomCameraBtnPrimaryHover.png b/addons/phantom_camera/assets/PhantomCameraBtnPrimaryHover.png new file mode 100644 index 0000000..68c080b --- /dev/null +++ b/addons/phantom_camera/assets/PhantomCameraBtnPrimaryHover.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d84d9f2a9c2c11b0bb79e9ebb455d217853f75cf653fc5cc21c80a02a28ed2c +size 9931 diff --git a/addons/phantom_camera/assets/PhantomCameraBtnPrimaryHover.png.import b/addons/phantom_camera/assets/PhantomCameraBtnPrimaryHover.png.import new file mode 100644 index 0000000..a9b1fa8 --- /dev/null +++ b/addons/phantom_camera/assets/PhantomCameraBtnPrimaryHover.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://pvr8mbvl1onm" +path="res://.godot/imported/PhantomCameraBtnPrimaryHover.png-3d2e4d225f6a86ce8a9c981ee7926a16.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/assets/PhantomCameraBtnPrimaryHover.png" +dest_files=["res://.godot/imported/PhantomCameraBtnPrimaryHover.png-3d2e4d225f6a86ce8a9c981ee7926a16.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/addons/phantom_camera/fonts/Nunito-Black.ttf b/addons/phantom_camera/fonts/Nunito-Black.ttf new file mode 100644 index 0000000..1081731 Binary files /dev/null and b/addons/phantom_camera/fonts/Nunito-Black.ttf differ diff --git a/addons/phantom_camera/fonts/Nunito-Black.ttf.import b/addons/phantom_camera/fonts/Nunito-Black.ttf.import new file mode 100644 index 0000000..ac0fdb7 --- /dev/null +++ b/addons/phantom_camera/fonts/Nunito-Black.ttf.import @@ -0,0 +1,36 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://c4mm3of2mc8o5" +path="res://.godot/imported/Nunito-Black.ttf-2a374efbc207a97a99b8c70bdc4b7cbb.fontdata" + +[deps] + +source_file="res://addons/phantom_camera/fonts/Nunito-Black.ttf" +dest_files=["res://.godot/imported/Nunito-Black.ttf-2a374efbc207a97a99b8c70bdc4b7cbb.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +disable_embedded_bitmaps=true +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +modulate_color_glyphs=false +hinting=1 +subpixel_positioning=1 +keep_rounding_remainders=true +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/phantom_camera/fonts/Nunito-Regular.ttf b/addons/phantom_camera/fonts/Nunito-Regular.ttf new file mode 100644 index 0000000..dfd0fcb Binary files /dev/null and b/addons/phantom_camera/fonts/Nunito-Regular.ttf differ diff --git a/addons/phantom_camera/fonts/Nunito-Regular.ttf.import b/addons/phantom_camera/fonts/Nunito-Regular.ttf.import new file mode 100644 index 0000000..a2ee2e8 --- /dev/null +++ b/addons/phantom_camera/fonts/Nunito-Regular.ttf.import @@ -0,0 +1,36 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://dve7mgsjik4dg" +path="res://.godot/imported/Nunito-Regular.ttf-b6054d499efa1a10921004862b1e217a.fontdata" + +[deps] + +source_file="res://addons/phantom_camera/fonts/Nunito-Regular.ttf" +dest_files=["res://.godot/imported/Nunito-Regular.ttf-b6054d499efa1a10921004862b1e217a.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +disable_embedded_bitmaps=true +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +modulate_color_glyphs=false +hinting=1 +subpixel_positioning=1 +keep_rounding_remainders=true +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/phantom_camera/icons/misc/PriorityOverride.svg b/addons/phantom_camera/icons/misc/PriorityOverride.svg new file mode 100644 index 0000000..af6d8f2 --- /dev/null +++ b/addons/phantom_camera/icons/misc/PriorityOverride.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8573cfbd9173019b0f55912f687508f95aa7b6cdbba8ecd1b5f8b77eef1530b6 +size 1193 diff --git a/addons/phantom_camera/icons/misc/PriorityOverride.svg.import b/addons/phantom_camera/icons/misc/PriorityOverride.svg.import new file mode 100644 index 0000000..79ad2a1 --- /dev/null +++ b/addons/phantom_camera/icons/misc/PriorityOverride.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dy8eifa6aw2en" +path="res://.godot/imported/PriorityOverride.svg-e76e07f4bbd98169f119e17fe5f2f03f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/icons/misc/PriorityOverride.svg" +dest_files=["res://.godot/imported/PriorityOverride.svg-e76e07f4bbd98169f119e17fe5f2f03f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/addons/phantom_camera/icons/phantom_camera_2d.svg b/addons/phantom_camera/icons/phantom_camera_2d.svg new file mode 100644 index 0000000..6e0e31f --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_2d.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b102678393872faf668ac83b9bd66d0b3e07373ae319deb2331c7e999d77a7df +size 1340 diff --git a/addons/phantom_camera/icons/phantom_camera_2d.svg.import b/addons/phantom_camera/icons/phantom_camera_2d.svg.import new file mode 100644 index 0000000..9fe97c8 --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_2d.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dchkkx4v3ikpw" +path="res://.godot/imported/phantom_camera_2d.svg-e5483cbc858fc5f95f7210b1649dff0d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/icons/phantom_camera_2d.svg" +dest_files=["res://.godot/imported/phantom_camera_2d.svg-e5483cbc858fc5f95f7210b1649dff0d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/addons/phantom_camera/icons/phantom_camera_3d.svg b/addons/phantom_camera/icons/phantom_camera_3d.svg new file mode 100644 index 0000000..a966638 --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_3d.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7dfdfee0464f5417de1bc3f7e48daa9f4b4907789fc47c0e02474b7a88d93e35 +size 1340 diff --git a/addons/phantom_camera/icons/phantom_camera_3d.svg.import b/addons/phantom_camera/icons/phantom_camera_3d.svg.import new file mode 100644 index 0000000..8d1154b --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_3d.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c71drpb8o4prn" +path="res://.godot/imported/phantom_camera_3d.svg-41ed612e834470377fb56eebffa083fe.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/icons/phantom_camera_3d.svg" +dest_files=["res://.godot/imported/phantom_camera_3d.svg-41ed612e834470377fb56eebffa083fe.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/addons/phantom_camera/icons/phantom_camera_camera_3d_resource.svg b/addons/phantom_camera/icons/phantom_camera_camera_3d_resource.svg new file mode 100644 index 0000000..28b792a --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_camera_3d_resource.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f09be586bb6c3065d22d782e9741c3bcedb754499913692eee468b32772b1ca +size 1091 diff --git a/addons/phantom_camera/icons/phantom_camera_camera_3d_resource.svg.import b/addons/phantom_camera/icons/phantom_camera_camera_3d_resource.svg.import new file mode 100644 index 0000000..a2a2fa6 --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_camera_3d_resource.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dnaykbu6ue5lo" +path="res://.godot/imported/phantom_camera_camera_3d_resource.svg-f8bf8d1a5b7442fd6933bfbed999d57d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/icons/phantom_camera_camera_3d_resource.svg" +dest_files=["res://.godot/imported/phantom_camera_camera_3d_resource.svg-f8bf8d1a5b7442fd6933bfbed999d57d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/addons/phantom_camera/icons/phantom_camera_gizmo.svg b/addons/phantom_camera/icons/phantom_camera_gizmo.svg new file mode 100644 index 0000000..1ca416b --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_gizmo.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77170750906a09b5544cacf85d23d763df38a6f45d4aabce77e3ad744b87ca90 +size 2821 diff --git a/addons/phantom_camera/icons/phantom_camera_gizmo.svg.import b/addons/phantom_camera/icons/phantom_camera_gizmo.svg.import new file mode 100644 index 0000000..e46b332 --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_gizmo.svg.import @@ -0,0 +1,44 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://e36npe2rbxyg" +path.s3tc="res://.godot/imported/phantom_camera_gizmo.svg-ba1aacb9b1c5f4ef401d3bd3697a542b.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} + +[deps] + +source_file="res://addons/phantom_camera/icons/phantom_camera_gizmo.svg" +dest_files=["res://.godot/imported/phantom_camera_gizmo.svg-ba1aacb9b1c5f4ef401d3bd3697a542b.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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=0 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/phantom_camera/icons/phantom_camera_glow_logo.png b/addons/phantom_camera/icons/phantom_camera_glow_logo.png new file mode 100644 index 0000000..835af80 --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_glow_logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec9e08eff52b5aae81c303b5c1ff64cd2bb34aa312824e007da2ede51bab0b37 +size 25499 diff --git a/addons/phantom_camera/icons/phantom_camera_glow_logo.png.import b/addons/phantom_camera/icons/phantom_camera_glow_logo.png.import new file mode 100644 index 0000000..8cc3625 --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_glow_logo.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cjli3p2b8mfyh" +path="res://.godot/imported/phantom_camera_glow_logo.png-078f944973b55b32029ba02980211fe0.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/icons/phantom_camera_glow_logo.png" +dest_files=["res://.godot/imported/phantom_camera_glow_logo.png-078f944973b55b32029ba02980211fe0.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/addons/phantom_camera/icons/phantom_camera_host.svg b/addons/phantom_camera/icons/phantom_camera_host.svg new file mode 100644 index 0000000..e513d8d --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_host.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06226797a38b503367f65de1d7b16ec9946e57f25a69fdd41428c15a5a4b0ce4 +size 1976 diff --git a/addons/phantom_camera/icons/phantom_camera_host.svg.import b/addons/phantom_camera/icons/phantom_camera_host.svg.import new file mode 100644 index 0000000..bd05ffc --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_host.svg.import @@ -0,0 +1,44 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://5fatldiu7dd5" +path="res://.godot/imported/phantom_camera_host.svg-3150f8f2d82ca9ecab9a3a415da21c5b.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/icons/phantom_camera_host.svg" +dest_files=["res://.godot/imported/phantom_camera_host.svg-3150f8f2d82ca9ecab9a3a415da21c5b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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=true diff --git a/addons/phantom_camera/icons/phantom_camera_logo.png b/addons/phantom_camera/icons/phantom_camera_logo.png new file mode 100644 index 0000000..66550b4 --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb20d01e23b464efb54856c3d91e9787960a7cec488f236ffc2744b589ad31fd +size 70905 diff --git a/addons/phantom_camera/icons/phantom_camera_logo.png.import b/addons/phantom_camera/icons/phantom_camera_logo.png.import new file mode 100644 index 0000000..12e3ccd --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_logo.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cc0wmici0eic8" +path="res://.godot/imported/phantom_camera_logo.png-8b8d347b5e4800c86cd8095d030a3e5a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/icons/phantom_camera_logo.png" +dest_files=["res://.godot/imported/phantom_camera_logo.png-8b8d347b5e4800c86cd8095d030a3e5a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/addons/phantom_camera/icons/phantom_camera_noise_emitter_2d.svg b/addons/phantom_camera/icons/phantom_camera_noise_emitter_2d.svg new file mode 100644 index 0000000..e7eaa4d --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_noise_emitter_2d.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64532102798caca4b33c956183c44f29293e77271aa49458167214894974729a +size 2569 diff --git a/addons/phantom_camera/icons/phantom_camera_noise_emitter_2d.svg.import b/addons/phantom_camera/icons/phantom_camera_noise_emitter_2d.svg.import new file mode 100644 index 0000000..f25e7c0 --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_noise_emitter_2d.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b2r7mhd780y8d" +path="res://.godot/imported/phantom_camera_noise_emitter_2d.svg-1b3d37fe36964dc86a6ea6681d0772bb.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/icons/phantom_camera_noise_emitter_2d.svg" +dest_files=["res://.godot/imported/phantom_camera_noise_emitter_2d.svg-1b3d37fe36964dc86a6ea6681d0772bb.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/addons/phantom_camera/icons/phantom_camera_noise_emitter_3d.svg b/addons/phantom_camera/icons/phantom_camera_noise_emitter_3d.svg new file mode 100644 index 0000000..aad4766 --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_noise_emitter_3d.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e53743d2670f7c80f8028fdb3d8d1f991fc1b3db2296ac7b1a25050c0f81094 +size 2569 diff --git a/addons/phantom_camera/icons/phantom_camera_noise_emitter_3d.svg.import b/addons/phantom_camera/icons/phantom_camera_noise_emitter_3d.svg.import new file mode 100644 index 0000000..fd4775c --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_noise_emitter_3d.svg.import @@ -0,0 +1,44 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cby76y7m6xn4f" +path.s3tc="res://.godot/imported/phantom_camera_noise_emitter_3d.svg-9b90fe54aa618f65d52ac94515d41ea4.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} + +[deps] + +source_file="res://addons/phantom_camera/icons/phantom_camera_noise_emitter_3d.svg" +dest_files=["res://.godot/imported/phantom_camera_noise_emitter_3d.svg-9b90fe54aa618f65d52ac94515d41ea4.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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=0 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/phantom_camera/icons/phantom_camera_noise_emitter_gizmo.svg b/addons/phantom_camera/icons/phantom_camera_noise_emitter_gizmo.svg new file mode 100644 index 0000000..053af22 --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_noise_emitter_gizmo.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3cb881d696f9c3bcd261edae330a601fa11a6caf55cb16a127af8d428b2fd870 +size 2517 diff --git a/addons/phantom_camera/icons/phantom_camera_noise_emitter_gizmo.svg.import b/addons/phantom_camera/icons/phantom_camera_noise_emitter_gizmo.svg.import new file mode 100644 index 0000000..f94b1f9 --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_noise_emitter_gizmo.svg.import @@ -0,0 +1,44 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dw4iy855s0atm" +path.s3tc="res://.godot/imported/phantom_camera_noise_emitter_gizmo.svg-9a593802655a8d5038c7f55deab3882d.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} + +[deps] + +source_file="res://addons/phantom_camera/icons/phantom_camera_noise_emitter_gizmo.svg" +dest_files=["res://.godot/imported/phantom_camera_noise_emitter_gizmo.svg-9a593802655a8d5038c7f55deab3882d.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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=0 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/phantom_camera/icons/phantom_camera_noise_resource.svg b/addons/phantom_camera/icons/phantom_camera_noise_resource.svg new file mode 100644 index 0000000..9bc0a20 --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_noise_resource.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e74524ffd5939ae9352410dee68ef3fa35670d44d1539845a575a06ff9402c7a +size 3135 diff --git a/addons/phantom_camera/icons/phantom_camera_noise_resource.svg.import b/addons/phantom_camera/icons/phantom_camera_noise_resource.svg.import new file mode 100644 index 0000000..92c7e3d --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_noise_resource.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://fudwitkewe70" +path="res://.godot/imported/phantom_camera_noise_resource.svg-a81ed223714edd2c0d9cfa00be0c3f58.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/icons/phantom_camera_noise_resource.svg" +dest_files=["res://.godot/imported/phantom_camera_noise_resource.svg-a81ed223714edd2c0d9cfa00be0c3f58.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/addons/phantom_camera/icons/phantom_camera_tween.svg b/addons/phantom_camera/icons/phantom_camera_tween.svg new file mode 100644 index 0000000..aa11481 --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_tween.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92140ea47f48be25f49c5b5a3d977f32f2b726204d618fad663b0a300f2f7056 +size 1580 diff --git a/addons/phantom_camera/icons/phantom_camera_tween.svg.import b/addons/phantom_camera/icons/phantom_camera_tween.svg.import new file mode 100644 index 0000000..b9e024b --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_tween.svg.import @@ -0,0 +1,44 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dphl04mdf3220" +path="res://.godot/imported/phantom_camera_tween.svg-16faced08ef4a5f3458264d894230dbd.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/icons/phantom_camera_tween.svg" +dest_files=["res://.godot/imported/phantom_camera_tween.svg-16faced08ef4a5f3458264d894230dbd.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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=true diff --git a/addons/phantom_camera/icons/phantom_camera_updater_panel_icon.svg b/addons/phantom_camera/icons/phantom_camera_updater_panel_icon.svg new file mode 100644 index 0000000..66dbfb6 --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_updater_panel_icon.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:692963561a4d7f8eb617ca227ee20c36b53ede287294ff9ee23fca684dda8cf3 +size 231 diff --git a/addons/phantom_camera/icons/phantom_camera_updater_panel_icon.svg.import b/addons/phantom_camera/icons/phantom_camera_updater_panel_icon.svg.import new file mode 100644 index 0000000..d1ace76 --- /dev/null +++ b/addons/phantom_camera/icons/phantom_camera_updater_panel_icon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d4j4hrb7yusyq" +path="res://.godot/imported/phantom_camera_updater_panel_icon.svg-19823e6cbee8115f8b2554d0ee6e79db.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/icons/phantom_camera_updater_panel_icon.svg" +dest_files=["res://.godot/imported/phantom_camera_updater_panel_icon.svg-19823e6cbee8115f8b2554d0ee6e79db.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/addons/phantom_camera/icons/viewfinder/Camera2DIcon.svg b/addons/phantom_camera/icons/viewfinder/Camera2DIcon.svg new file mode 100644 index 0000000..09ca9bb --- /dev/null +++ b/addons/phantom_camera/icons/viewfinder/Camera2DIcon.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb5589767f6d3037a43a7ec09c37a94b120896a31c7d9b42878fc908ec9d8c69 +size 681 diff --git a/addons/phantom_camera/icons/viewfinder/Camera2DIcon.svg.import b/addons/phantom_camera/icons/viewfinder/Camera2DIcon.svg.import new file mode 100644 index 0000000..714d685 --- /dev/null +++ b/addons/phantom_camera/icons/viewfinder/Camera2DIcon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ccnsrg8hq74p2" +path="res://.godot/imported/Camera2DIcon.svg-300e6f57281180711c5ecf391104d4ba.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/icons/viewfinder/Camera2DIcon.svg" +dest_files=["res://.godot/imported/Camera2DIcon.svg-300e6f57281180711c5ecf391104d4ba.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/addons/phantom_camera/icons/viewfinder/Camera3DIcon.svg b/addons/phantom_camera/icons/viewfinder/Camera3DIcon.svg new file mode 100644 index 0000000..f34f234 --- /dev/null +++ b/addons/phantom_camera/icons/viewfinder/Camera3DIcon.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d23217cb7eed77e8c671ab3473e5ac9861d9dd01a7839fb637a9cdb136419c7 +size 681 diff --git a/addons/phantom_camera/icons/viewfinder/Camera3DIcon.svg.import b/addons/phantom_camera/icons/viewfinder/Camera3DIcon.svg.import new file mode 100644 index 0000000..be171ac --- /dev/null +++ b/addons/phantom_camera/icons/viewfinder/Camera3DIcon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dkiefpjsrj37n" +path="res://.godot/imported/Camera3DIcon.svg-4805c46004db1c89cc9443dd740693f5.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/icons/viewfinder/Camera3DIcon.svg" +dest_files=["res://.godot/imported/Camera3DIcon.svg-4805c46004db1c89cc9443dd740693f5.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/addons/phantom_camera/icons/viewfinder/SceneTypesIcon.svg b/addons/phantom_camera/icons/viewfinder/SceneTypesIcon.svg new file mode 100644 index 0000000..7c544d8 --- /dev/null +++ b/addons/phantom_camera/icons/viewfinder/SceneTypesIcon.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13d99e3cddf8a7ac182232c559f69224eefe433b16d8a525148fa61e0bb09ce4 +size 1245 diff --git a/addons/phantom_camera/icons/viewfinder/SceneTypesIcon.svg.import b/addons/phantom_camera/icons/viewfinder/SceneTypesIcon.svg.import new file mode 100644 index 0000000..e97d921 --- /dev/null +++ b/addons/phantom_camera/icons/viewfinder/SceneTypesIcon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dk7omm0x44suj" +path="res://.godot/imported/SceneTypesIcon.svg-66e2255bd3398007bec03a5cbfa4d0aa.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/icons/viewfinder/SceneTypesIcon.svg" +dest_files=["res://.godot/imported/SceneTypesIcon.svg-66e2255bd3398007bec03a5cbfa4d0aa.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/addons/phantom_camera/icons/viewfinder/Select.svg b/addons/phantom_camera/icons/viewfinder/Select.svg new file mode 100644 index 0000000..f776d73 --- /dev/null +++ b/addons/phantom_camera/icons/viewfinder/Select.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b07e65b7bfcc0b1e0da74219ab8113a56bc05ac3e19cc8945afc8724f1266df +size 647 diff --git a/addons/phantom_camera/icons/viewfinder/Select.svg.import b/addons/phantom_camera/icons/viewfinder/Select.svg.import new file mode 100644 index 0000000..b1cb20c --- /dev/null +++ b/addons/phantom_camera/icons/viewfinder/Select.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://rghrkoqrm2ig" +path="res://.godot/imported/Select.svg-cdf90b8b400d3b91a023b70c6a823894.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/icons/viewfinder/Select.svg" +dest_files=["res://.godot/imported/Select.svg-cdf90b8b400d3b91a023b70c6a823894.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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=2.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/phantom_camera/icons/warning.svg b/addons/phantom_camera/icons/warning.svg new file mode 100644 index 0000000..301c208 --- /dev/null +++ b/addons/phantom_camera/icons/warning.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:705784d6a5b117ac68801e193cead82eead05c98b52526d082dd291d9d12e56a +size 1077 diff --git a/addons/phantom_camera/icons/warning.svg.import b/addons/phantom_camera/icons/warning.svg.import new file mode 100644 index 0000000..07b7909 --- /dev/null +++ b/addons/phantom_camera/icons/warning.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cjlv0bg7byjx0" +path="res://.godot/imported/warning.svg-c1b21c265e0842bbdc9ed10735995366.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/icons/warning.svg" +dest_files=["res://.godot/imported/warning.svg-c1b21c265e0842bbdc9ed10735995366.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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=2.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/phantom_camera/inspector/phantom_camera_inspector_plugin.gd b/addons/phantom_camera/inspector/phantom_camera_inspector_plugin.gd new file mode 100644 index 0000000..d5a0d22 --- /dev/null +++ b/addons/phantom_camera/inspector/phantom_camera_inspector_plugin.gd @@ -0,0 +1,46 @@ +@tool +extends EditorInspectorPlugin + +#var _phantom_camera_script: Script = preload("res://addons/phantom_camera/scripts/phantom_camera.gd") + + +# TODO - Enable again once work is resumed for inspector based tasks + +#func _can_handle(object) -> bool: +# return object is _phantom_camera_script + + +func _parse_category(object: Object, category: String) -> void: + + var _margin_container: MarginContainer = MarginContainer.new() + var _margin_v: float = 20 + _margin_container.add_theme_constant_override("margin_left", 10) + _margin_container.add_theme_constant_override("margin_top", _margin_v) + _margin_container.add_theme_constant_override("margin_right", 10) + _margin_container.add_theme_constant_override("margin_bottom", _margin_v) + add_custom_control(_margin_container) + + var _vbox_container: VBoxContainer = VBoxContainer.new() + _margin_container.add_child(_vbox_container) + + var align_with_view_button = Button.new() + align_with_view_button.connect("pressed", _align_camera_with_view.bind(object)) + align_with_view_button.set_custom_minimum_size(Vector2(0, 60)) + align_with_view_button.set_text("Align with view") + _vbox_container.add_child(align_with_view_button) + + var preview_camera_button = Button.new() + preview_camera_button.connect("pressed", _preview_camera.bind(object)) + preview_camera_button.set_custom_minimum_size(Vector2(0, 60)) + preview_camera_button.set_text("Preview Camera") + _vbox_container.add_child(preview_camera_button) + + + +func _align_camera_with_view(object: Object) -> void: + print("Aligning camera with view") + print(object) + +func _preview_camera(object: Object) -> void: + print("Previewing camera") + print(object) diff --git a/addons/phantom_camera/inspector/phantom_camera_inspector_plugin.gd.uid b/addons/phantom_camera/inspector/phantom_camera_inspector_plugin.gd.uid new file mode 100644 index 0000000..325b3c0 --- /dev/null +++ b/addons/phantom_camera/inspector/phantom_camera_inspector_plugin.gd.uid @@ -0,0 +1 @@ +uid://dhwxgngr7sn6d diff --git a/addons/phantom_camera/panel/editor.gd.uid b/addons/phantom_camera/panel/editor.gd.uid new file mode 100644 index 0000000..52a27c4 --- /dev/null +++ b/addons/phantom_camera/panel/editor.gd.uid @@ -0,0 +1 @@ +uid://ppret7j0jle7 diff --git a/addons/phantom_camera/panel/editor.tscn b/addons/phantom_camera/panel/editor.tscn new file mode 100644 index 0000000..0c75c6a --- /dev/null +++ b/addons/phantom_camera/panel/editor.tscn @@ -0,0 +1,23 @@ +[gd_scene load_steps=4 format=3 uid="uid://cfdoaceoosi1w"] + +[ext_resource type="Script" uid="uid://cgfwg3paxkj2x" path="res://addons/phantom_camera/scripts/panel/editor.gd" id="1_86hp7"] +[ext_resource type="PackedScene" uid="uid://cuqkqsp3ikv5u" path="res://addons/phantom_camera/panel/updater/update_button.tscn" id="1_oowcd"] +[ext_resource type="PackedScene" uid="uid://dbkr3d716wtx0" path="res://addons/phantom_camera/panel/viewfinder/viewfinder_panel.tscn" id="2_xecnk"] + +[node name="EditorPanel" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_86hp7") + +[node name="UpdateButton" parent="." instance=ExtResource("1_oowcd")] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 8 +size_flags_vertical = 1 + +[node name="ViewfinderPanel" parent="." instance=ExtResource("2_xecnk")] +unique_name_in_owner = true +layout_mode = 2 diff --git a/addons/phantom_camera/panel/updater/download_update_panel.tscn b/addons/phantom_camera/panel/updater/download_update_panel.tscn new file mode 100644 index 0000000..5fa49e3 --- /dev/null +++ b/addons/phantom_camera/panel/updater/download_update_panel.tscn @@ -0,0 +1,253 @@ +[gd_scene load_steps=15 format=3 uid="uid://b25fl4usw0nlp"] + +[ext_resource type="Script" uid="uid://cjblcocen12r3" path="res://addons/phantom_camera/scripts/panel/updater/download_update_panel.gd" id="1_sx5xq"] +[ext_resource type="Texture2D" uid="uid://cc0wmici0eic8" path="res://addons/phantom_camera/icons/phantom_camera_logo.png" id="2_f3yo7"] +[ext_resource type="FontFile" uid="uid://c4mm3of2mc8o5" path="res://addons/phantom_camera/fonts/Nunito-Black.ttf" id="3_h8uk3"] +[ext_resource type="FontFile" uid="uid://dve7mgsjik4dg" path="res://addons/phantom_camera/fonts/Nunito-Regular.ttf" id="4_gwh4i"] +[ext_resource type="Texture2D" uid="uid://censw3w53gldn" path="res://addons/phantom_camera/assets/PhantomCameraBtnPrimaryDefault.png" id="5_bonti"] + +[sub_resource type="ImageTexture" id="ImageTexture_sjwi2"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_insma"] +bg_color = Color(0.0190018, 0.21903, 0.16997, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.0980392, 0.572549, 0.458824, 1) +border_blend = true +corner_radius_bottom_right = 12 +corner_radius_bottom_left = 12 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_8m63d"] +bg_color = Color(0.0784314, 0.109804, 0.129412, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.105882, 0.619608, 0.498039, 1) +corner_radius_bottom_right = 12 +corner_radius_bottom_left = 12 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_yn22d"] +bg_color = Color(0.0117647, 0.164706, 0.12549, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.0980392, 0.572549, 0.458824, 1) +border_blend = true +corner_radius_bottom_right = 12 +corner_radius_bottom_left = 12 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_djsbc"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_xtrn6"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_o12j0"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_buptb"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_g3tf0"] + +[node name="DownloadUpdatePanel" type="Control"] +custom_minimum_size = Vector2(300, 350) +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_bottom = -61.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_sx5xq") + +[node name="DownloadHTTPRequest" type="HTTPRequest" parent="."] +unique_name_in_owner = true + +[node name="Timer" type="Timer" parent="DownloadHTTPRequest"] +one_shot = true + +[node name="VBox" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/separation = 2 + +[node name="VBoxContainer2" type="VBoxContainer" parent="VBox"] +layout_mode = 2 +theme_override_constants/separation = -20 + +[node name="MarginContainer" type="MarginContainer" parent="VBox/VBoxContainer2"] +layout_mode = 2 +theme_override_constants/margin_top = 12 + +[node name="Logo" type="TextureRect" parent="VBox/VBoxContainer2/MarginContainer"] +unique_name_in_owner = true +clip_contents = true +custom_minimum_size = Vector2(300, 160) +layout_mode = 2 +texture = ExtResource("2_f3yo7") +expand_mode = 3 +stretch_mode = 5 + +[node name="VBoxContainer" type="VBoxContainer" parent="VBox/VBoxContainer2"] +layout_mode = 2 +theme_override_constants/separation = -5 + +[node name="NameLabel" type="Label" parent="VBox/VBoxContainer2/VBoxContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0.960784, 0.960784, 0.960784, 1) +theme_override_fonts/font = ExtResource("3_h8uk3") +theme_override_font_sizes/font_size = 32 +text = "Phantom Camera" +horizontal_alignment = 1 + +[node name="DownloadVersionLabel" type="Label" parent="VBox/VBoxContainer2/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_colors/font_color = Color(0.960784, 0.960784, 0.960784, 1) +theme_override_fonts/font = ExtResource("4_gwh4i") +theme_override_font_sizes/font_size = 18 +text = "v1.2.3 is available for download." +horizontal_alignment = 1 + +[node name="CurrentVersionLabel" type="Label" parent="VBox"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +theme_override_fonts/font = ExtResource("4_gwh4i") +text = "Current version: 0.0.0" +horizontal_alignment = 1 + +[node name="Center2" type="CenterContainer" parent="VBox"] +layout_mode = 2 + +[node name="NotesButton" type="LinkButton" parent="VBox/Center2"] +layout_mode = 2 +theme_override_colors/font_color = Color(0.917647, 0.631373, 0.368627, 1) +theme_override_colors/font_hover_color = Color(0.721569, 0.454902, 0.192157, 1) +theme_override_fonts/font = ExtResource("3_h8uk3") +theme_override_font_sizes/font_size = 18 +text = "Release Notes" + +[node name="Center" type="CenterContainer" parent="VBox"] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="VBox/Center"] +layout_mode = 2 + +[node name="BreakingLabel" type="Label" parent="VBox/Center/VBoxContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(0.72549, 0.227451, 0.34902, 1) +theme_override_fonts/font = ExtResource("3_h8uk3") +theme_override_font_sizes/font_size = 18 +text = "Potential Breaking Changes +in new release" +horizontal_alignment = 1 +uppercase = true + +[node name="BreakingMarginContainer" type="MarginContainer" parent="VBox/Center/VBoxContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="VBox/Center/VBoxContainer/BreakingMarginContainer"] +layout_mode = 2 + +[node name="RichTextLabel2" type="RichTextLabel" parent="VBox/Center/VBoxContainer/BreakingMarginContainer/VBoxContainer"] +layout_mode = 2 +theme_override_fonts/normal_font = ExtResource("4_gwh4i") +theme_override_fonts/bold_font = ExtResource("3_h8uk3") +theme_override_fonts/mono_font = ExtResource("3_h8uk3") +theme_override_font_sizes/normal_font_size = 18 +theme_override_font_sizes/bold_font_size = 14 +theme_override_font_sizes/mono_font_size = 12 +bbcode_enabled = true +text = "[center][b]I am prepared for any breaking +changes that may occur from this update[/b][/center]" +fit_content = true + +[node name="BreakingOptionButton" type="OptionButton" parent="VBox/Center/VBoxContainer/BreakingMarginContainer/VBoxContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +mouse_default_cursor_shape = 2 +theme_override_fonts/font = ExtResource("3_h8uk3") +theme_override_font_sizes/font_size = 18 +theme_override_icons/arrow = SubResource("ImageTexture_sjwi2") +theme_override_styles/normal = SubResource("StyleBoxFlat_insma") +theme_override_styles/hover = SubResource("StyleBoxFlat_8m63d") +theme_override_styles/pressed = SubResource("StyleBoxFlat_yn22d") +alignment = 1 +item_count = 2 +selected = 0 +popup/item_0/text = "Confirm choice" +popup/item_0/id = 0 +popup/item_1/text = "Yes, I am prepared" +popup/item_1/id = 1 + +[node name="DownloadButton" type="Button" parent="VBox/Center/VBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(240, 90) +layout_mode = 2 +mouse_default_cursor_shape = 2 +theme_override_styles/normal = SubResource("StyleBoxEmpty_djsbc") +theme_override_styles/hover = SubResource("StyleBoxEmpty_xtrn6") +theme_override_styles/pressed = SubResource("StyleBoxEmpty_o12j0") +theme_override_styles/disabled = SubResource("StyleBoxEmpty_buptb") +theme_override_styles/focus = SubResource("StyleBoxEmpty_g3tf0") + +[node name="DownloadButtonBG" type="NinePatchRect" parent="VBox/Center/VBoxContainer/DownloadButton"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("5_bonti") +patch_margin_left = 38 +patch_margin_top = 37 +patch_margin_right = 38 +patch_margin_bottom = 39 + +[node name="UpdateLabel" type="Label" parent="VBox/Center/VBoxContainer/DownloadButton"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 14 +anchor_top = 0.5 +anchor_right = 1.0 +anchor_bottom = 0.5 +offset_top = -14.5 +offset_bottom = 14.5 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_colors/font_color = Color(0.960784, 0.960784, 0.960784, 1) +theme_override_colors/font_shadow_color = Color(0, 0, 0, 1) +theme_override_fonts/font = ExtResource("3_h8uk3") +theme_override_font_sizes/font_size = 20 +text = "Update" +horizontal_alignment = 1 +uppercase = true + +[node name="MarginContainer" type="MarginContainer" parent="VBox"] +layout_mode = 2 +theme_override_constants/margin_top = 10 + +[node name="RichTextLabel" type="RichTextLabel" parent="VBox/MarginContainer"] +layout_mode = 2 +theme_override_fonts/normal_font = ExtResource("4_gwh4i") +theme_override_fonts/mono_font = ExtResource("3_h8uk3") +theme_override_font_sizes/normal_font_size = 12 +theme_override_font_sizes/mono_font_size = 12 +bbcode_enabled = true +text = "[center]The updater can be disabled within: +[code]Project Settings / Phantom Camera / Updater / Enable Updater[/code][/center]" +fit_content = true + +[connection signal="pressed" from="VBox/Center2/NotesButton" to="." method="_on_notes_button_pressed"] diff --git a/addons/phantom_camera/panel/updater/update_button.tscn b/addons/phantom_camera/panel/updater/update_button.tscn new file mode 100644 index 0000000..a6efba1 --- /dev/null +++ b/addons/phantom_camera/panel/updater/update_button.tscn @@ -0,0 +1,101 @@ +[gd_scene load_steps=10 format=3 uid="uid://cuqkqsp3ikv5u"] + +[ext_resource type="FontFile" uid="uid://c4mm3of2mc8o5" path="res://addons/phantom_camera/fonts/Nunito-Black.ttf" id="1_5e5k4"] +[ext_resource type="Script" uid="uid://bwc42i46603qn" path="res://addons/phantom_camera/scripts/panel/updater/update_button.gd" id="1_xtaw5"] +[ext_resource type="Texture2D" uid="uid://d4j4hrb7yusyq" path="res://addons/phantom_camera/icons/phantom_camera_updater_panel_icon.svg" id="2_c4d83"] +[ext_resource type="PackedScene" uid="uid://b25fl4usw0nlp" path="res://addons/phantom_camera/panel/updater/download_update_panel.tscn" id="2_vtgcx"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_c7fd1"] +content_margin_left = 10.0 +content_margin_top = 4.0 +content_margin_right = 10.0 +content_margin_bottom = 4.0 +bg_color = Color(0.0980392, 0.415686, 0.341176, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_y83dj"] +content_margin_left = 10.0 +content_margin_top = 4.0 +content_margin_right = 10.0 +content_margin_bottom = 4.0 +bg_color = Color(0.0784314, 0.109804, 0.129412, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.960784, 0.960784, 0.960784, 1) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_slf6e"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_lekqh"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dr4n4"] +content_margin_bottom = 20.0 +bg_color = Color(0.0784314, 0.109804, 0.129412, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.227451, 0.72549, 0.603922, 1) + +[node name="UpdateButton" type="Button"] +visible = false +offset_left = 1.0 +offset_right = 149.0 +offset_bottom = 28.0 +size_flags_vertical = 3 +theme_override_colors/font_color = Color(0.960784, 0.960784, 0.960784, 1) +theme_override_colors/font_hover_color = Color(0.939288, 0.917743, 0.892615, 1) +theme_override_colors/icon_normal_color = Color(0.960784, 0.960784, 0.960784, 1) +theme_override_fonts/font = ExtResource("1_5e5k4") +theme_override_font_sizes/font_size = 14 +theme_override_styles/normal = SubResource("StyleBoxFlat_c7fd1") +theme_override_styles/hover = SubResource("StyleBoxFlat_y83dj") +theme_override_styles/pressed = SubResource("StyleBoxEmpty_slf6e") +theme_override_styles/focus = SubResource("StyleBoxEmpty_lekqh") +text = "Update available" +icon = ExtResource("2_c4d83") +script = ExtResource("1_xtaw5") + +[node name="HTTPRequest" type="HTTPRequest" parent="."] +unique_name_in_owner = true + +[node name="DownloadDialog" type="AcceptDialog" parent="."] +unique_name_in_owner = true +transparent_bg = true +title = "New Update" +initial_position = 2 +size = Vector2i(450, 480) +transient = false +unresizable = true +borderless = true +keep_title_visible = false +content_scale_mode = 1 +theme_override_constants/buttons_separation = 30 +theme_override_styles/panel = SubResource("StyleBoxFlat_dr4n4") +ok_button_text = "Close" + +[node name="DownloadUpdatePanel" parent="DownloadDialog" instance=ExtResource("2_vtgcx")] +unique_name_in_owner = true +offset_left = 2.0 +offset_top = 2.0 +offset_right = -2.0 +offset_bottom = -80.0 + +[node name="NeedsReloadDialog" type="ConfirmationDialog" parent="."] +unique_name_in_owner = true + +[node name="UpdateFailedDialog" type="AcceptDialog" parent="."] +unique_name_in_owner = true diff --git a/addons/phantom_camera/panel/viewfinder/deadzone_style_box.tres b/addons/phantom_camera/panel/viewfinder/deadzone_style_box.tres new file mode 100644 index 0000000..7353299 --- /dev/null +++ b/addons/phantom_camera/panel/viewfinder/deadzone_style_box.tres @@ -0,0 +1,14 @@ +[gd_resource type="StyleBoxFlat" format=3 uid="uid://dpa7yvxlq043a"] + +[resource] +bg_color = Color(0.227451, 0.72549, 0.603922, 0.2) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_detail = 1 +expand_margin_left = 1.0 +expand_margin_top = 1.0 +expand_margin_right = 1.0 +expand_margin_bottom = 1.0 diff --git a/addons/phantom_camera/panel/viewfinder/host_list/host_list.tscn b/addons/phantom_camera/panel/viewfinder/host_list/host_list.tscn new file mode 100644 index 0000000..332415d --- /dev/null +++ b/addons/phantom_camera/panel/viewfinder/host_list/host_list.tscn @@ -0,0 +1,87 @@ +[gd_scene load_steps=8 format=3 uid="uid://mbjdav5oqves"] + +[ext_resource type="Script" uid="uid://c84cxry3t35ny" path="res://addons/phantom_camera/scripts/panel/viewfinder/host_list.gd" id="1_h6ayt"] +[ext_resource type="Texture2D" uid="uid://5fatldiu7dd5" path="res://addons/phantom_camera/icons/phantom_camera_host.svg" id="1_xlgqb"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_w002y"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_kq7gm"] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.0784314, 0.109804, 0.129412, 1) +border_width_top = 2 +border_width_right = 2 +border_color = Color(0.960784, 0.960784, 0.960784, 1) +corner_radius_top_right = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ynag5"] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.960784, 0.960784, 0.960784, 1) +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +corner_radius_top_right = 6 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q2svd"] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.0784314, 0.109804, 0.129412, 1) +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_right = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_e0jvt"] +content_margin_left = 0.0 +content_margin_top = 8.0 +content_margin_right = 0.0 +content_margin_bottom = 8.0 +bg_color = Color(0.0784314, 0.109804, 0.129412, 1) +border_width_top = 2 +border_width_right = 2 +border_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_right = 10 + +[node name="PCamHostList" type="VBoxContainer"] +anchors_preset = 9 +anchor_bottom = 1.0 +size_flags_horizontal = 0 +size_flags_vertical = 0 +theme_override_constants/separation = -2 +alignment = 2 +script = ExtResource("1_h6ayt") + +[node name="HostListButton" type="Button" parent="."] +unique_name_in_owner = true +custom_minimum_size = Vector2(40, 40) +layout_mode = 2 +size_flags_horizontal = 0 +theme_override_colors/icon_hover_color = Color(0.0784314, 0.109804, 0.129412, 1) +theme_override_colors/icon_hover_pressed_color = Color(0.0784314, 0.109804, 0.129412, 1) +theme_override_styles/focus = SubResource("StyleBoxEmpty_w002y") +theme_override_styles/hover_pressed = SubResource("StyleBoxFlat_kq7gm") +theme_override_styles/hover = SubResource("StyleBoxFlat_ynag5") +theme_override_styles/pressed = SubResource("StyleBoxFlat_kq7gm") +theme_override_styles/normal = SubResource("StyleBoxFlat_q2svd") +icon = ExtResource("1_xlgqb") +expand_icon = true + +[node name="ScrollContainer" type="ScrollContainer" parent="."] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 +theme_override_styles/panel = SubResource("StyleBoxFlat_e0jvt") +horizontal_scroll_mode = 0 + +[node name="HostListContainer" type="VBoxContainer" parent="ScrollContainer"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_constants/separation = 8 diff --git a/addons/phantom_camera/panel/viewfinder/host_list/host_list_item.tscn b/addons/phantom_camera/panel/viewfinder/host_list/host_list_item.tscn new file mode 100644 index 0000000..9ce67e5 --- /dev/null +++ b/addons/phantom_camera/panel/viewfinder/host_list/host_list_item.tscn @@ -0,0 +1,68 @@ +[gd_scene load_steps=10 format=3 uid="uid://btn6jgv0vix7"] + +[ext_resource type="FontFile" uid="uid://dve7mgsjik4dg" path="res://addons/phantom_camera/fonts/Nunito-Regular.ttf" id="1_anjxo"] +[ext_resource type="Theme" uid="uid://bhppejri5dbsf" path="res://addons/phantom_camera/themes/theme.tres" id="1_wql5t"] +[ext_resource type="Texture2D" uid="uid://rghrkoqrm2ig" path="res://addons/phantom_camera/icons/viewfinder/Select.svg" id="2_71b6g"] +[ext_resource type="ButtonGroup" uid="uid://dfu0b8jbtyr1n" path="res://addons/phantom_camera/panel/viewfinder/host_list/host_list_item_group.tres" id="3_06a0y"] +[ext_resource type="Script" uid="uid://bv24ubx8mutw7" path="res://addons/phantom_camera/scripts/panel/viewfinder/host_list_item.gd" id="3_a5o8b"] +[ext_resource type="Texture2D" uid="uid://cjlv0bg7byjx0" path="res://addons/phantom_camera/icons/warning.svg" id="3_qgpy7"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_0rxfi"] +content_margin_right = 6.0 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_llqnh"] + +[sub_resource type="Theme" id="Theme_7h15c"] +Button/colors/icon_hover_color = Color(0.960784, 0.960784, 0.960784, 1) +Button/colors/icon_hover_pressed_color = Color(0.227451, 0.72549, 0.603922, 1) +Button/colors/icon_normal_color = Color(0.227451, 0.72549, 0.603922, 1) +Button/colors/icon_pressed_color = Color(0.227451, 0.72549, 0.603922, 1) +Button/constants/icon_max_width = 20 +Button/styles/focus = SubResource("StyleBoxEmpty_llqnh") + +[node name="HostListItem" type="PanelContainer"] +offset_right = 229.0 +offset_bottom = 34.0 +theme_override_styles/panel = SubResource("StyleBoxEmpty_0rxfi") +script = ExtResource("3_a5o8b") + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +layout_mode = 2 +theme_override_constants/separation = 2 + +[node name="SelectPCamHost" type="Button" parent="HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 4 +tooltip_text = "Select the Phantom Camera Host node from the scene hierarchy" +focus_mode = 0 +theme = SubResource("Theme_7h15c") +icon = ExtResource("2_71b6g") +flat = true + +[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="ErrorPCamHost" type="TextureRect" parent="HBoxContainer/HBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(18, 18) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 4 +tooltip_text = "This Phantom Camera Host node will not affect a Camera node. +See the warning in the Scene Tree for more information." +texture = ExtResource("3_qgpy7") +expand_mode = 1 + +[node name="SwitchPCamHost" type="Button" parent="HBoxContainer/HBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(40, 0) +layout_mode = 2 +tooltip_text = "Change the viewfinder's camera to the camera of this Phantom Camera Host" +theme = ExtResource("1_wql5t") +theme_override_fonts/font = ExtResource("1_anjxo") +theme_override_font_sizes/font_size = 18 +toggle_mode = true +button_group = ExtResource("3_06a0y") +text = "{ PCamHostName }" diff --git a/addons/phantom_camera/panel/viewfinder/host_list/host_list_item_group.tres b/addons/phantom_camera/panel/viewfinder/host_list/host_list_item_group.tres new file mode 100644 index 0000000..64c4600 --- /dev/null +++ b/addons/phantom_camera/panel/viewfinder/host_list/host_list_item_group.tres @@ -0,0 +1,3 @@ +[gd_resource type="ButtonGroup" format=3 uid="uid://dfu0b8jbtyr1n"] + +[resource] diff --git a/addons/phantom_camera/panel/viewfinder/viewfinder_panel.tscn b/addons/phantom_camera/panel/viewfinder/viewfinder_panel.tscn new file mode 100644 index 0000000..b6caa0d --- /dev/null +++ b/addons/phantom_camera/panel/viewfinder/viewfinder_panel.tscn @@ -0,0 +1,563 @@ +[gd_scene load_steps=28 format=3 uid="uid://dbkr3d716wtx0"] + +[ext_resource type="Script" uid="uid://drmv3363t8amc" path="res://addons/phantom_camera/scripts/panel/viewfinder/viewfinder.gd" id="1_utvi8"] +[ext_resource type="StyleBox" uid="uid://dpa7yvxlq043a" path="res://addons/phantom_camera/panel/viewfinder/deadzone_style_box.tres" id="2_uabt4"] +[ext_resource type="FontFile" uid="uid://c4mm3of2mc8o5" path="res://addons/phantom_camera/fonts/Nunito-Black.ttf" id="3_li3i3"] +[ext_resource type="Texture2D" uid="uid://5fatldiu7dd5" path="res://addons/phantom_camera/icons/phantom_camera_host.svg" id="4_lcg1p"] +[ext_resource type="FontFile" uid="uid://dve7mgsjik4dg" path="res://addons/phantom_camera/fonts/Nunito-Regular.ttf" id="5_4jhax"] +[ext_resource type="Texture2D" uid="uid://dy8eifa6aw2en" path="res://addons/phantom_camera/icons/misc/PriorityOverride.svg" id="6_ptuth"] +[ext_resource type="Script" uid="uid://c84cxry3t35ny" path="res://addons/phantom_camera/scripts/panel/viewfinder/host_list.gd" id="7_kpij0"] +[ext_resource type="Theme" uid="uid://bhppejri5dbsf" path="res://addons/phantom_camera/themes/theme.tres" id="8_b4akn"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fle8t"] +bg_color = Color(0.227451, 0.72549, 0.603922, 0.2) +draw_center = false +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_detail = 1 +expand_margin_left = 1.0 +expand_margin_top = 1.0 +expand_margin_right = 1.0 +expand_margin_bottom = 1.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xmo1t"] +draw_center = false +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.745098, 0.858824, 0.380392, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q7vs4"] +bg_color = Color(0.929412, 0.87451, 0.619608, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0, 0, 0, 1) +corner_radius_top_left = 10 +corner_radius_top_right = 10 +corner_radius_bottom_right = 10 +corner_radius_bottom_left = 10 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_iho1a"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_obaj6"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_4b76l"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_yh38y"] +content_margin_left = 10.0 +content_margin_top = 10.0 +content_margin_right = 10.0 +content_margin_bottom = 10.0 +bg_color = Color(0.129412, 0.407843, 0.337255, 1) +border_width_left = 4 +border_width_top = 4 +border_width_right = 4 +border_width_bottom = 4 +border_color = Color(0.988235, 0.498039, 0.498039, 1) +corner_radius_top_left = 10 +corner_radius_top_right = 10 +corner_radius_bottom_right = 10 +corner_radius_bottom_left = 10 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_gci88"] +content_margin_left = 10.0 +content_margin_top = 10.0 +content_margin_right = 10.0 +content_margin_bottom = 10.0 +bg_color = Color(0.180392, 0.576471, 0.482353, 1) +corner_radius_top_left = 10 +corner_radius_top_right = 10 +corner_radius_bottom_right = 10 +corner_radius_bottom_left = 10 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fsxik"] +content_margin_left = 10.0 +content_margin_top = 10.0 +content_margin_right = 10.0 +content_margin_bottom = 10.0 +bg_color = Color(0.129412, 0.407843, 0.337255, 1) +border_width_left = 4 +border_width_top = 4 +border_width_right = 4 +border_width_bottom = 4 +border_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_left = 10 +corner_radius_top_right = 10 +corner_radius_bottom_right = 10 +corner_radius_bottom_left = 10 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_g5wua"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_x4bx8"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_840sd"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ja3vm"] +bg_color = Color(0.53, 0.1643, 0.255725, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_blend = true +corner_radius_top_left = 10 +corner_radius_top_right = 10 +corner_radius_bottom_right = 10 +corner_radius_bottom_left = 10 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_mk273"] +bg_color = Color(0.43, 0.1333, 0.207475, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_blend = true +corner_radius_top_left = 10 +corner_radius_top_right = 10 +corner_radius_bottom_right = 10 +corner_radius_bottom_left = 10 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_agqdu"] +bg_color = Color(0.72549, 0.227451, 0.34902, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_blend = true +corner_radius_top_left = 10 +corner_radius_top_right = 10 +corner_radius_bottom_right = 10 +corner_radius_bottom_left = 10 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_w002y"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_kq7gm"] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.0784314, 0.109804, 0.129412, 1) +border_width_top = 2 +border_width_right = 2 +border_color = Color(0.960784, 0.960784, 0.960784, 1) +corner_radius_top_right = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ynag5"] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.960784, 0.960784, 0.960784, 1) +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +corner_radius_top_right = 6 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q2svd"] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.0784314, 0.109804, 0.129412, 1) +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_right = 8 + +[node name="ViewfinderPanel" type="Control"] +clip_contents = true +custom_minimum_size = Vector2(0, 300) +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +mouse_filter = 2 +script = ExtResource("1_utvi8") + +[node name="Viewfinder" type="Control" parent="."] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +metadata/_edit_lock_ = true + +[node name="SubViewportContainer" type="SubViewportContainer" parent="Viewfinder"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +stretch = true + +[node name="SubViewport" type="SubViewport" parent="Viewfinder/SubViewportContainer"] +unique_name_in_owner = true +handle_input_locally = false +canvas_item_default_texture_filter = 0 +gui_disable_input = true +size = Vector2i(1152, 648) +size_2d_override_stretch = true +render_target_update_mode = 4 + +[node name="Camera2D" type="Camera2D" parent="Viewfinder/SubViewportContainer/SubViewport"] +unique_name_in_owner = true +editor_draw_screen = false + +[node name="DeadZoneHBoxContainer" type="HBoxContainer" parent="Viewfinder"] +unique_name_in_owner = true +clip_contents = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +theme_override_constants/separation = 0 + +[node name="DeadZoneLeftHBoxContainer" type="VBoxContainer" parent="Viewfinder/DeadZoneHBoxContainer"] +clip_contents = true +layout_mode = 2 +size_flags_horizontal = 3 +mouse_filter = 2 +theme_override_constants/separation = 0 + +[node name="DeadZoneLeftTopPanel" type="Panel" parent="Viewfinder/DeadZoneHBoxContainer/DeadZoneLeftHBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +mouse_filter = 2 +theme_override_styles/panel = ExtResource("2_uabt4") + +[node name="DeadZoneLeftCenterPanel" type="Panel" parent="Viewfinder/DeadZoneHBoxContainer/DeadZoneLeftHBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +mouse_filter = 2 +theme_override_styles/panel = ExtResource("2_uabt4") + +[node name="DeadZoneLeftBottomPanel" type="Panel" parent="Viewfinder/DeadZoneHBoxContainer/DeadZoneLeftHBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +mouse_filter = 2 +theme_override_styles/panel = ExtResource("2_uabt4") + +[node name="DeadZoneCenterHBoxContainer" type="VBoxContainer" parent="Viewfinder/DeadZoneHBoxContainer"] +unique_name_in_owner = true +clip_contents = true +layout_mode = 2 +size_flags_horizontal = 4 +mouse_filter = 2 +theme_override_constants/separation = 0 + +[node name="DeadZoneCenterTopPanel" type="Panel" parent="Viewfinder/DeadZoneHBoxContainer/DeadZoneCenterHBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +mouse_filter = 2 +theme_override_styles/panel = ExtResource("2_uabt4") + +[node name="DeadZoneCenterCenterPanel" type="Panel" parent="Viewfinder/DeadZoneHBoxContainer/DeadZoneCenterHBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 4 +mouse_filter = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_fle8t") + +[node name="DeadZoneCenterBottomPanel" type="Panel" parent="Viewfinder/DeadZoneHBoxContainer/DeadZoneCenterHBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +mouse_filter = 2 +theme_override_styles/panel = ExtResource("2_uabt4") + +[node name="DeadZoneRightHBoxContainer" type="VBoxContainer" parent="Viewfinder/DeadZoneHBoxContainer"] +clip_contents = true +layout_mode = 2 +size_flags_horizontal = 3 +mouse_filter = 2 +theme_override_constants/separation = 0 + +[node name="DeadZoneRightTopPanel" type="Panel" parent="Viewfinder/DeadZoneHBoxContainer/DeadZoneRightHBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +mouse_filter = 2 +theme_override_styles/panel = ExtResource("2_uabt4") + +[node name="DeadZoneRightCenterPanel" type="Panel" parent="Viewfinder/DeadZoneHBoxContainer/DeadZoneRightHBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +mouse_filter = 2 +theme_override_styles/panel = ExtResource("2_uabt4") + +[node name="DeadZoneRightBottomPanel" type="Panel" parent="Viewfinder/DeadZoneHBoxContainer/DeadZoneRightHBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +mouse_filter = 2 +theme_override_styles/panel = ExtResource("2_uabt4") + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="Viewfinder"] +unique_name_in_owner = true +clip_contents = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +ratio = 1.77778 + +[node name="CameraViewportPanel" type="Panel" parent="Viewfinder/AspectRatioContainer"] +layout_mode = 2 +mouse_filter = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_xmo1t") + +[node name="TargetPoint" type="Panel" parent="Viewfinder/AspectRatioContainer/CameraViewportPanel"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -3.0 +offset_top = -3.0 +offset_right = 3.0 +offset_bottom = 3.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_q7vs4") + +[node name="NoSupportMsg" type="Label" parent="."] +unique_name_in_owner = true +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -8.0 +offset_bottom = -8.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_fonts/font = ExtResource("3_li3i3") +theme_override_font_sizes/font_size = 24 +theme_override_styles/normal = SubResource("StyleBoxEmpty_iho1a") +text = "Control scenes are not supported" +horizontal_alignment = 1 +vertical_alignment = 1 +metadata/_edit_lock_ = true + +[node name="EmptyStateControl" type="Control" parent="."] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +metadata/_edit_use_anchors_ = true +metadata/_edit_lock_ = true + +[node name="BGColorRect" type="ColorRect" parent="EmptyStateControl"] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0, 0, 0, 1) +metadata/_edit_lock_ = true + +[node name="VBoxContainer" type="VBoxContainer" parent="EmptyStateControl"] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -200.0 +offset_top = -112.0 +offset_right = 200.0 +offset_bottom = 112.0 +grow_horizontal = 2 +grow_vertical = 2 +alignment = 1 + +[node name="EmptyStateIcon" type="TextureRect" parent="EmptyStateControl/VBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 64) +layout_mode = 2 +texture = ExtResource("4_lcg1p") +expand_mode = 1 +stretch_mode = 5 + +[node name="EmptyStateText" type="RichTextLabel" parent="EmptyStateControl/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_colors/default_color = Color(1, 1, 1, 1) +theme_override_fonts/normal_font = ExtResource("5_4jhax") +theme_override_fonts/bold_font = ExtResource("3_li3i3") +theme_override_font_sizes/normal_font_size = 24 +theme_override_font_sizes/bold_font_size = 24 +theme_override_styles/focus = SubResource("StyleBoxEmpty_obaj6") +theme_override_styles/normal = SubResource("StyleBoxEmpty_iho1a") +bbcode_enabled = true +text = "[center][b]NodeType[/b] Description [/center]" +fit_content = true + +[node name="AddNodeButton" type="Button" parent="EmptyStateControl/VBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(400, 54) +layout_mode = 2 +size_flags_horizontal = 4 +focus_mode = 0 +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_fonts/font = ExtResource("3_li3i3") +theme_override_font_sizes/font_size = 24 +theme_override_styles/focus = SubResource("StyleBoxEmpty_4b76l") +theme_override_styles/hover = SubResource("StyleBoxFlat_yh38y") +theme_override_styles/pressed = SubResource("StyleBoxFlat_gci88") +theme_override_styles/normal = SubResource("StyleBoxFlat_fsxik") + +[node name="AddNodeTypeText" type="RichTextLabel" parent="EmptyStateControl/VBoxContainer/AddNodeButton"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = 9.0 +offset_bottom = -11.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +theme_override_colors/default_color = Color(1, 1, 1, 1) +theme_override_fonts/normal_font = ExtResource("5_4jhax") +theme_override_fonts/bold_font = ExtResource("3_li3i3") +theme_override_font_sizes/normal_font_size = 24 +theme_override_font_sizes/bold_font_size = 24 +theme_override_styles/focus = SubResource("StyleBoxEmpty_g5wua") +theme_override_styles/normal = SubResource("StyleBoxEmpty_x4bx8") +bbcode_enabled = true +text = "[center]Add [img=32]res://addons/phantom_camera/icons/viewfinder/Camera3DIcon.svg[/img] [b]{NodeType}[/b][/center]" +scroll_active = false + +[node name="PriorityOverrideButton" type="Button" parent="."] +unique_name_in_owner = true +visible = false +layout_mode = 1 +offset_left = 5.0 +offset_top = 5.0 +offset_right = 165.0 +offset_bottom = 57.0 +mouse_default_cursor_shape = 2 +theme_override_styles/focus = SubResource("StyleBoxEmpty_840sd") +theme_override_styles/hover = SubResource("StyleBoxFlat_ja3vm") +theme_override_styles/pressed = SubResource("StyleBoxFlat_mk273") +theme_override_styles/normal = SubResource("StyleBoxFlat_agqdu") +metadata/_edit_lock_ = true + +[node name="PriorityOverrideIcon" type="TextureRect" parent="PriorityOverrideButton"] +layout_mode = 1 +offset_left = 8.0 +offset_top = 4.0 +offset_right = 32.0 +offset_bottom = 28.0 +texture = ExtResource("6_ptuth") +expand_mode = 1 + +[node name="PriorityOverrideByLabel" type="Label" parent="PriorityOverrideButton"] +layout_mode = 0 +offset_left = 30.0 +offset_top = 1.0 +offset_right = 140.0 +offset_bottom = 24.0 +theme_override_fonts/font = ExtResource("3_li3i3") +theme_override_font_sizes/font_size = 14 +text = "OVERRIDDEN BY" +vertical_alignment = 1 + +[node name="PriorityOverrideNameLabel" type="Label" parent="PriorityOverrideButton"] +unique_name_in_owner = true +layout_mode = 0 +offset_left = 6.0 +offset_top = 21.0 +offset_right = 153.0 +offset_bottom = 44.0 +theme_override_fonts/font = ExtResource("5_4jhax") +theme_override_font_sizes/font_size = 14 +text = "PCam_Name +" +vertical_alignment = 1 +text_overrun_behavior = 3 + +[node name="SizeLabel" type="Label" parent="."] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 4 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_top = -11.5 +offset_right = 40.0 +offset_bottom = 11.5 +grow_vertical = 2 + +[node name="PCamHostList" type="VBoxContainer" parent="."] +unique_name_in_owner = true +visible = false +layout_mode = 1 +anchors_preset = -1 +anchor_bottom = 1.0 +offset_top = 588.0 +grow_vertical = 2 +size_flags_horizontal = 0 +size_flags_vertical = 0 +theme_override_constants/separation = -2 +alignment = 2 +script = ExtResource("7_kpij0") + +[node name="HostListButton" type="Button" parent="PCamHostList"] +unique_name_in_owner = true +custom_minimum_size = Vector2(40, 40) +layout_mode = 2 +size_flags_horizontal = 0 +theme_override_colors/icon_hover_pressed_color = Color(0.0784314, 0.109804, 0.129412, 1) +theme_override_colors/icon_hover_color = Color(0.0784314, 0.109804, 0.129412, 1) +theme_override_styles/focus = SubResource("StyleBoxEmpty_w002y") +theme_override_styles/hover_pressed = SubResource("StyleBoxFlat_kq7gm") +theme_override_styles/hover = SubResource("StyleBoxFlat_ynag5") +theme_override_styles/pressed = SubResource("StyleBoxFlat_kq7gm") +theme_override_styles/normal = SubResource("StyleBoxFlat_q2svd") +icon = ExtResource("4_lcg1p") +expand_icon = true + +[node name="PanelContainer" type="PanelContainer" parent="PCamHostList"] +layout_mode = 2 +size_flags_vertical = 3 +theme = ExtResource("8_b4akn") + +[node name="ScrollContainer" type="ScrollContainer" parent="PCamHostList/PanelContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 +theme = ExtResource("8_b4akn") +horizontal_scroll_mode = 0 + +[node name="HostListContainer" type="VBoxContainer" parent="PCamHostList/PanelContainer/ScrollContainer"] +unique_name_in_owner = true +layout_mode = 2 +theme = ExtResource("8_b4akn") diff --git a/addons/phantom_camera/plugin.cfg b/addons/phantom_camera/plugin.cfg new file mode 100644 index 0000000..6325827 --- /dev/null +++ b/addons/phantom_camera/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Phantom Camera" +description="Control the movement and dynamically tween 2D & 3D cameras positions. Built for Godot 4. Inspired by Cinemachine." +author="Marcus Skov" +version="0.9.3.1" +script="plugin.gd" diff --git a/addons/phantom_camera/plugin.gd b/addons/phantom_camera/plugin.gd new file mode 100644 index 0000000..0421330 --- /dev/null +++ b/addons/phantom_camera/plugin.gd @@ -0,0 +1,181 @@ +@tool +extends EditorPlugin + +#region Constants + +const PCAM_HOST: String = "PhantomCameraHost" +const PCAM_2D: String = "PhantomCamera2D" +const PCAM_3D: String = "PhantomCamera3D" +const PCAM_NOISE_EMITTER_2D: String = "PhantomCameraNoiseEmitter2D" +const PCAM_NOISE_EMITTER_3D: String = "PhantomCameraNoiseEmitter3D" + +const PCam3DPlugin: Script = preload("res://addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo_plugin.gd") +const PCam3DNoiseEmitterPlugin: Script = preload("res://addons/phantom_camera/scripts/gizmos/phantom_camera_noise_emitter_gizmo_plugin_3d.gd") +const EditorPanel: PackedScene = preload("res://addons/phantom_camera/panel/editor.tscn") +const updater_constants: Script = preload("res://addons/phantom_camera/scripts/panel/updater/updater_constants.gd") +const PHANTOM_CAMERA_MANAGER: StringName = "PhantomCameraManager" + +#endregion + +#region Private Variables + +var _settings_show_jitter_tips: String = "phantom_camera/tips/show_jitter_tips" +var _settings_enable_editor_shortcut: String = "phantom_camera/general/enable_editor_shortcut" +var _settings_editor_shortcut: String = "phantom_camera/general/editor_shortcut" + +# TODO - Pending merge of https://github.com/godotengine/godot/pull/102889 - Should only support Godot version after the release that is featured in +#var _editor_shortcut: Shortcut = Shortcut.new() +#var _editor_shortcut_input: InputEventKey +#endregion + +#region Public Variables + +var pcam_3d_gizmo_plugin = PCam3DPlugin.new() +var pcam_3d_noise_emitter_gizmo_plugin = PCam3DNoiseEmitterPlugin.new() + +var editor_panel_instance: Control +var panel_button: Button +#var viewfinder_panel_instance + + +#endregion + +#region Private Functions + +func _enable_plugin() -> void: + print_rich("Phantom Camera documentation can be found at: [url=https://phantom-camera.dev]https://phantom-camera.dev[/url]") + if not Engine.has_singleton(PHANTOM_CAMERA_MANAGER): + add_autoload_singleton(PHANTOM_CAMERA_MANAGER, "res://addons/phantom_camera/scripts/managers/phantom_camera_manager.gd") + + +func _disable_plugin() -> void: + remove_autoload_singleton(PHANTOM_CAMERA_MANAGER) + + +func _enter_tree() -> void: + add_autoload_singleton(PHANTOM_CAMERA_MANAGER, "res://addons/phantom_camera/scripts/managers/phantom_camera_manager.gd") + + # Phantom Camera Nodes + add_custom_type(PCAM_2D, "Node2D", preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_2d.gd"), preload("res://addons/phantom_camera/icons/phantom_camera_2d.svg")) + add_custom_type(PCAM_3D, "Node3D", preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_3d.gd"), preload("res://addons/phantom_camera/icons/phantom_camera_2d.svg")) + add_custom_type(PCAM_HOST, "Node", preload("res://addons/phantom_camera/scripts/phantom_camera_host/phantom_camera_host.gd"), preload("res://addons/phantom_camera/icons/phantom_camera_2d.svg")) + add_custom_type(PCAM_NOISE_EMITTER_2D, "Node2D", preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_noise_emitter_2d.gd"), preload("res://addons/phantom_camera/icons/phantom_camera_noise_emitter_2d.svg")) + add_custom_type(PCAM_NOISE_EMITTER_3D, "Node3D", preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_noise_emitter_3d.gd"), preload("res://addons/phantom_camera/icons/phantom_camera_noise_emitter_3d.svg")) + + # Phantom Camera 3D Gizmo + add_node_3d_gizmo_plugin(pcam_3d_gizmo_plugin) + add_node_3d_gizmo_plugin(pcam_3d_noise_emitter_gizmo_plugin) + + var setting_updater_mode: String + var setting_updater_mode_default: int + if FileAccess.file_exists("res://dev_scenes/3d/dev_scene_3d.tscn"): # For forks + setting_updater_mode = "Off, Console Output" + setting_updater_mode_default = 1 + else: # For end-users + setting_updater_mode = "Off, Console Output, Updater Window" + setting_updater_mode_default = 2 + + if not ProjectSettings.has_setting(updater_constants.setting_updater_mode): + ProjectSettings.set_setting(updater_constants.setting_updater_mode, setting_updater_mode_default) + ProjectSettings.add_property_info({ + "name": updater_constants.setting_updater_mode, + "type": TYPE_INT, + "hint": PROPERTY_HINT_ENUM, + "hint_string": setting_updater_mode, + }) + ProjectSettings.set_initial_value(updater_constants.setting_updater_mode, setting_updater_mode_default) + ProjectSettings.set_as_basic(updater_constants.setting_updater_mode, true) + + + ## Setting for enabling / disabling Jitter tips in the Output + if not ProjectSettings.has_setting(_settings_show_jitter_tips): + ProjectSettings.set_setting(_settings_show_jitter_tips, true) + ProjectSettings.add_property_info({ + "name": _settings_show_jitter_tips, + "type": TYPE_BOOL, + }) + ProjectSettings.set_initial_value(_settings_show_jitter_tips, true) + ProjectSettings.set_as_basic(_settings_show_jitter_tips, true) + + +# TODO - Pending merge of https://github.com/godotengine/godot/pull/102889 - Should only support Godot version after this release +# if not ProjectSettings.has_setting(_settings_enable_editor_shortcut): +# ProjectSettings.set_setting(_settings_enable_editor_shortcut, false) +# ProjectSettings.set_initial_value(_settings_enable_editor_shortcut, false) + +# TODO - Pending merge of https://github.com/godotengine/godot/pull/102889 - Should only support Godot version after this release +# _viewfinder_shortcut_default.events = [editor_shortcut] +# if ProjectSettings.get_setting(_settings_enable_editor_shortcut): +# if not ProjectSettings.has_setting(_settings_editor_shortcut): +# ProjectSettings.set_setting(_settings_editor_shortcut, _editor_shortcut) +# ProjectSettings.set_initial_value(_settings_editor_shortcut, _editor_shortcut) + + + # TODO - Should be disabled unless in editor + # Viewfinder + editor_panel_instance = EditorPanel.instantiate() + editor_panel_instance.editor_plugin = self + panel_button = add_control_to_bottom_panel(editor_panel_instance, "Phantom Camera") + panel_button.toggled.connect(_btn_toggled) + if panel_button.toggle_mode: _btn_toggled(true) + + scene_changed.connect(editor_panel_instance.viewfinder.scene_changed) + scene_changed.connect(_scene_changed) + + +func _exit_tree() -> void: + panel_button.toggled.disconnect(_btn_toggled) + scene_changed.disconnect(editor_panel_instance.viewfinder.scene_changed) + scene_changed.disconnect(_scene_changed) + + remove_control_from_bottom_panel(editor_panel_instance) + editor_panel_instance.queue_free() + + remove_node_3d_gizmo_plugin(pcam_3d_gizmo_plugin) + remove_node_3d_gizmo_plugin(pcam_3d_noise_emitter_gizmo_plugin) + + remove_custom_type(PCAM_2D) + remove_custom_type(PCAM_3D) + remove_custom_type(PCAM_HOST) + remove_custom_type(PCAM_NOISE_EMITTER_2D) + remove_custom_type(PCAM_NOISE_EMITTER_3D) + + remove_autoload_singleton(PHANTOM_CAMERA_MANAGER) +# if get_tree().root.get_node_or_null(String(PHANTOM_CAMERA_MANAGER)): +# remove_autoload_singleton(PHANTOM_CAMERA_MANAGER) + + +func _btn_toggled(toggled_on: bool): + editor_panel_instance.viewfinder.set_visibility(toggled_on) +# if toggled_on: +# editor_panel_instance.viewfinder.viewfinder_visible = true +# editor_panel_instance.viewfinder.visibility_check() +# else: +# editor_panel_instance.viewfinder.viewfinder_visible = false + +func _make_visible(visible): + if editor_panel_instance: + editor_panel_instance.set_visible(visible) + +## TODO - Signal can be added directly to the editor_panel with the changes in Godot 4.5 (https://github.com/godotengine/godot/pull/102986) +func _scene_changed(scene_root: Node) -> void: + editor_panel_instance.viewfinder.scene_changed(scene_root) + +# TODO - Pending merge of https://github.com/godotengine/godot/pull/102889 - Should only support Godot version after this release +#func _set_editor_shortcut() -> InputEventKey: +# var shortcut: InputEventKey = InputEventKey.new() +# shortcut.keycode = 67 # Key = C +# shortcut.alt_pressed = true +# return shortcut + +#endregion + + +#region Public Functions + +func get_version() -> String: + var config: ConfigFile = ConfigFile.new() + config.load(get_script().resource_path.get_base_dir() + "/plugin.cfg") + return config.get_value("plugin", "version") + +#endregion diff --git a/addons/phantom_camera/plugin.gd.uid b/addons/phantom_camera/plugin.gd.uid new file mode 100644 index 0000000..1c23477 --- /dev/null +++ b/addons/phantom_camera/plugin.gd.uid @@ -0,0 +1 @@ +uid://c7g41in1osxi6 diff --git a/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo.gd b/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo.gd new file mode 100644 index 0000000..27e7eed --- /dev/null +++ b/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo.gd @@ -0,0 +1,84 @@ +@tool +extends EditorNode3DGizmo + +#var pcam_3d: PhantomCamera3D + +func _redraw() -> void: + clear() + + var icon: Material = get_plugin().get_material(get_plugin().get_name(), self) + add_unscaled_billboard(icon, 0.035) + + var pcam_3d: PhantomCamera3D = get_node_3d() + +# if pcam_3d.is_following(): +# _draw_target(pcam_3d, pcam_3d.get_follow_target_position(), "follow_target") +# if pcam_3d.is_looking_at(): +# _draw_target(pcam_3d, pcam_3d.get_look_at_target_position(), "look_at_target") + + if pcam_3d.is_active(): return + + var frustum_lines: PackedVector3Array = PackedVector3Array() + var height: float = 0.25 + var width: float = height * 1.25 + var forward: float = height * -1.5 + + # Trapezoid + frustum_lines.push_back(Vector3.ZERO) + frustum_lines.push_back(Vector3(-width, height, forward)) + + frustum_lines.push_back(Vector3.ZERO) + frustum_lines.push_back(Vector3(width, height, forward)) + + frustum_lines.push_back(Vector3.ZERO) + frustum_lines.push_back(Vector3(-width, -height, forward)) + + frustum_lines.push_back(Vector3.ZERO) + frustum_lines.push_back(Vector3(width, -height, forward)) + + ####### + # Frame + ####### + ## Left + frustum_lines.push_back(Vector3(-width, height, forward)) + frustum_lines.push_back(Vector3(-width, -height, forward)) + + ## Bottom + frustum_lines.push_back(Vector3(-width, -height, forward)) + frustum_lines.push_back(Vector3(width, -height, forward)) + + ## Right + frustum_lines.push_back(Vector3(width, -height, forward)) + frustum_lines.push_back(Vector3(width, height, forward)) + + ## Top + frustum_lines.push_back(Vector3(width, height, forward)) + frustum_lines.push_back(Vector3(-width, height, forward)) + + ############## + # Up Direction + ############## + var up_height: float = height + 0.15 + var up_width: float = width / 3 + + ## Left + frustum_lines.push_back(Vector3(0, up_height, forward)) + frustum_lines.push_back(Vector3(-up_width, height, forward)) + + ## Right + frustum_lines.push_back(Vector3(0, up_height, forward)) + frustum_lines.push_back(Vector3(up_width, height, forward)) + + var frustum_material: StandardMaterial3D = get_plugin().get_material("frustum", self) + add_lines(frustum_lines, frustum_material, false) + + +func _draw_target(pcam_3d: Node3D, target_position: Vector3, type: String) -> void: + var target_lines: PackedVector3Array = PackedVector3Array() + var direction: Vector3 = target_position - pcam_3d.global_position + var end_position: Vector3 = pcam_3d.global_basis.z * direction + + target_lines.push_back(Vector3.ZERO) + target_lines.push_back(end_position) + var target_material: StandardMaterial3D = get_plugin().get_material(type, self) + add_lines(target_lines, target_material, false) diff --git a/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo.gd.uid b/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo.gd.uid new file mode 100644 index 0000000..564e0cf --- /dev/null +++ b/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo.gd.uid @@ -0,0 +1 @@ +uid://cyr6fgximfw6q diff --git a/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo_plugin.gd b/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo_plugin.gd new file mode 100644 index 0000000..2caf713 --- /dev/null +++ b/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo_plugin.gd @@ -0,0 +1,37 @@ +@tool +extends EditorNode3DGizmoPlugin + +const PhantomCamera3DNode: Script = preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_3d.gd") +const PhantomCamera3DGizmo: Script = preload("res://addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo.gd") +const _icon_texture: Texture2D = preload("res://addons/phantom_camera/icons/phantom_camera_gizmo.svg") +var _gizmo_name: String = "PhantomCamera3D" + +var gizmo_name: String: set = set_gizmo_name +var _gizmo_icon: Texture2D +var _gizmo_spatial_script: Script = PhantomCamera3DNode + + +func set_gizmo_name(name: String) -> void: + _gizmo_name = name + + +func _get_gizmo_name() -> String: + return _gizmo_name + + +func _has_gizmo(spatial: Node3D) -> bool: + return spatial is PhantomCamera3D + + +func _init() -> void: + create_icon_material(gizmo_name, _icon_texture, false, Color.WHITE) + create_material("frustum", Color8(252, 127, 127, 255)) + create_material("follow_target", Color8(185, 58, 89)) + create_material("look_at_target", Color8(61, 207, 225)) + + +func _create_gizmo(for_node_3d: Node3D) -> EditorNode3DGizmo: + if for_node_3d is PhantomCamera3DNode: + return PhantomCamera3DGizmo.new() + else: + return null diff --git a/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo_plugin.gd.uid b/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo_plugin.gd.uid new file mode 100644 index 0000000..ce528d7 --- /dev/null +++ b/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo_plugin.gd.uid @@ -0,0 +1 @@ +uid://bkevga3bx4rfj diff --git a/addons/phantom_camera/scripts/gizmos/phantom_camera_noise_emitter_gizmo_plugin_3d.gd b/addons/phantom_camera/scripts/gizmos/phantom_camera_noise_emitter_gizmo_plugin_3d.gd new file mode 100644 index 0000000..3dd4d3e --- /dev/null +++ b/addons/phantom_camera/scripts/gizmos/phantom_camera_noise_emitter_gizmo_plugin_3d.gd @@ -0,0 +1,29 @@ +extends EditorNode3DGizmoPlugin + +var _spatial_script: Script = preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_noise_emitter_3d.gd") +var _gizmo_icon: Texture2D = preload("res://addons/phantom_camera/icons/phantom_camera_noise_emitter_gizmo.svg") + +var _gizmo_name: StringName = "PhantomCameraNoiseEmitter" + +func _init() -> void: + create_material("main", Color8(252, 127, 127, 255)) + create_handle_material("handles") + create_icon_material(_gizmo_name, _gizmo_icon, false, Color.WHITE) + + +func _has_gizmo(node: Node3D): + return node.get_script() == _spatial_script + + +func _get_gizmo_name() -> String: + return _gizmo_name + + +func _redraw(gizmo: EditorNode3DGizmo): + gizmo.clear() + + var icon: Material = get_material(_gizmo_name, gizmo) + gizmo.add_unscaled_billboard(icon, 0.035) + + #var material = get_material("main", gizmo) + #gizmo.add_lines(_draw_frustum(), material) diff --git a/addons/phantom_camera/scripts/gizmos/phantom_camera_noise_emitter_gizmo_plugin_3d.gd.uid b/addons/phantom_camera/scripts/gizmos/phantom_camera_noise_emitter_gizmo_plugin_3d.gd.uid new file mode 100644 index 0000000..2b93b6c --- /dev/null +++ b/addons/phantom_camera/scripts/gizmos/phantom_camera_noise_emitter_gizmo_plugin_3d.gd.uid @@ -0,0 +1 @@ +uid://dddokcd2ug05i diff --git a/addons/phantom_camera/scripts/managers/PhantomCameraManager.cs b/addons/phantom_camera/scripts/managers/PhantomCameraManager.cs new file mode 100644 index 0000000..5005230 --- /dev/null +++ b/addons/phantom_camera/scripts/managers/PhantomCameraManager.cs @@ -0,0 +1,36 @@ +using System.Linq; +using Godot; + +#nullable enable + +namespace PhantomCamera.Manager; + +public static class PhantomCameraManager +{ + private static GodotObject? _instance; + + public static GodotObject Instance => _instance ??= Engine.GetSingleton("PhantomCameraManager"); + + public static PhantomCamera2D[] PhantomCamera2Ds => + Instance.Call(MethodName.GetPhantomCamera2Ds).AsGodotArray() + .Select(node => new PhantomCamera2D(node)).ToArray(); + + public static PhantomCamera3D[] PhantomCamera3Ds => + Instance.Call(MethodName.GetPhantomCamera3Ds).AsGodotArray() + .Select(node => new PhantomCamera3D(node)).ToArray(); + + public static PhantomCameraHost[] PhantomCameraHosts => + Instance.Call(MethodName.GetPhantomCameraHosts).AsGodotArray() + .Select(node => new PhantomCameraHost(node)).ToArray(); + + public static PhantomCamera2D[] GetPhantomCamera2Ds() => PhantomCamera2Ds; + public static PhantomCamera3D[] GetPhantomCamera3Ds() => PhantomCamera3Ds; + public static PhantomCameraHost[] GetPhantomCameraHosts() => PhantomCameraHosts; + + public static class MethodName + { + public const string GetPhantomCamera2Ds = "get_phantom_camera_2ds"; + public const string GetPhantomCamera3Ds = "get_phantom_camera_3ds"; + public const string GetPhantomCameraHosts = "get_phantom_camera_hosts"; + } +} diff --git a/addons/phantom_camera/scripts/managers/PhantomCameraManager.cs.uid b/addons/phantom_camera/scripts/managers/PhantomCameraManager.cs.uid new file mode 100644 index 0000000..cdf7f4e --- /dev/null +++ b/addons/phantom_camera/scripts/managers/PhantomCameraManager.cs.uid @@ -0,0 +1 @@ +uid://vtj8iqx4bp43 diff --git a/addons/phantom_camera/scripts/managers/phantom_camera_manager.gd b/addons/phantom_camera/scripts/managers/phantom_camera_manager.gd new file mode 100644 index 0000000..6e13d9a --- /dev/null +++ b/addons/phantom_camera/scripts/managers/phantom_camera_manager.gd @@ -0,0 +1,149 @@ +@tool +extends Node + +const _CONSTANTS = preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd") + +#region Signals + +# Noise +signal noise_2d_emitted(noise_output: Transform2D, emitter_layer: int) +signal noise_3d_emitted(noise_output: Transform3D, emitter_layer: int) + +# PCam Host +signal pcam_host_added_to_scene(pcam_host: PhantomCameraHost) +signal pcam_host_removed_from_scene(pcam_host: PhantomCameraHost) + +# PCam +signal pcam_added_to_scene(pcam: Node) +signal pcam_removed_from_scene(pcam: Node) + +# Priority +signal pcam_priority_changed(pcam: Node) +signal pcam_visibility_changed(pcam: Node) + +signal pcam_teleport(pcam: Node) + +# Limit (2D) +signal limit_2d_changed(side: int, limit: int) +signal draw_limit_2d(enabled: bool) + +# Camera3DResource (3D) +signal camera_3d_resource_changed(property: String, value: Variant) + +# Viewfinder Signals +signal viewfinder_pcam_host_switch(pcam_host: PhantomCameraHost) +signal pcam_priority_override(pcam: Node, shouldOverride: bool) +signal pcam_dead_zone_changed(pcam: Node) +signal pcam_host_layer_changed(pcam: Node) + +#endregion + +#region Private Variables + +var _phantom_camera_host_list: Array[PhantomCameraHost] +var _phantom_camera_2d_list: Array[PhantomCamera2D] +var _phantom_camera_3d_list: Array[Node] ## Note: To support disable_3d export templates for 2D projects, this is purposely not strongly typed. + +#endregion + +#region Public Variables + +var phantom_camera_hosts: Array[PhantomCameraHost]: + get: + return _phantom_camera_host_list + +var phantom_camera_2ds: Array[PhantomCamera2D]: + get: + return _phantom_camera_2d_list + +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 screen_size: Vector2i + +#endregion + +#region Private Functions + +func _enter_tree() -> void: + if not Engine.has_singleton(_CONSTANTS.PCAM_MANAGER_NODE_NAME): + Engine.register_singleton(_CONSTANTS.PCAM_MANAGER_NODE_NAME, self) + Engine.physics_jitter_fix = 0 + + +func _ready() -> void: + # Setting default screensize + screen_size = Vector2i( + ProjectSettings.get_setting("display/window/size/viewport_width"), + ProjectSettings.get_setting("display/window/size/viewport_height") + ) + + # For editor + if Engine.is_editor_hint(): + ProjectSettings.settings_changed.connect(func(): + screen_size = Vector2i( + ProjectSettings.get_setting("display/window/size/viewport_width"), + ProjectSettings.get_setting("display/window/size/viewport_height") + ) + ) + # For runtime + else: + get_tree().get_root().size_changed.connect(func(): + screen_size = get_viewport().get_visible_rect().size + ) + +#endregion + +#region Public Functions + +func pcam_host_added(caller: Node) -> void: + if is_instance_of(caller, PhantomCameraHost): + _phantom_camera_host_list.append(caller) + pcam_host_added_to_scene.emit(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) + pcam_host_removed_from_scene.emit(caller) + else: + printerr("This method can only be called from a PhantomCameraHost node") + + +func pcam_added(caller) -> void: + if is_instance_of(caller, PhantomCamera2D): + _phantom_camera_2d_list.append(caller) + pcam_added_to_scene.emit(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) + pcam_added_to_scene.emit(caller) + +func pcam_removed(caller) -> void: + if is_instance_of(caller, PhantomCamera2D): + _phantom_camera_2d_list.erase(caller) + pcam_removed_from_scene.emit(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) + pcam_removed_from_scene.emit(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() + +#endregion diff --git a/addons/phantom_camera/scripts/managers/phantom_camera_manager.gd.uid b/addons/phantom_camera/scripts/managers/phantom_camera_manager.gd.uid new file mode 100644 index 0000000..ea18023 --- /dev/null +++ b/addons/phantom_camera/scripts/managers/phantom_camera_manager.gd.uid @@ -0,0 +1 @@ +uid://duq6jhf6unyis diff --git a/addons/phantom_camera/scripts/panel/editor.gd b/addons/phantom_camera/scripts/panel/editor.gd new file mode 100644 index 0000000..a10018c --- /dev/null +++ b/addons/phantom_camera/scripts/panel/editor.gd @@ -0,0 +1,23 @@ +@tool +extends VBoxContainer + +#region Onready + +@onready var updater: Control = %UpdateButton +@onready var viewfinder: Control = %ViewfinderPanel + +#endregion + +#region Public Variables + +var editor_plugin: EditorPlugin + +#endregion + + +#region Private Functions + +func _ready(): + updater.editor_plugin = editor_plugin + +#endregion diff --git a/addons/phantom_camera/scripts/panel/editor.gd.uid b/addons/phantom_camera/scripts/panel/editor.gd.uid new file mode 100644 index 0000000..6be368c --- /dev/null +++ b/addons/phantom_camera/scripts/panel/editor.gd.uid @@ -0,0 +1 @@ +uid://cgfwg3paxkj2x diff --git a/addons/phantom_camera/scripts/panel/updater/download_update_panel.gd b/addons/phantom_camera/scripts/panel/updater/download_update_panel.gd new file mode 100644 index 0000000..b19beb9 --- /dev/null +++ b/addons/phantom_camera/scripts/panel/updater/download_update_panel.gd @@ -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 diff --git a/addons/phantom_camera/scripts/panel/updater/download_update_panel.gd.uid b/addons/phantom_camera/scripts/panel/updater/download_update_panel.gd.uid new file mode 100644 index 0000000..ff50946 --- /dev/null +++ b/addons/phantom_camera/scripts/panel/updater/download_update_panel.gd.uid @@ -0,0 +1 @@ +uid://cjblcocen12r3 diff --git a/addons/phantom_camera/scripts/panel/updater/update_button.gd b/addons/phantom_camera/scripts/panel/updater/update_button.gd new file mode 100644 index 0000000..686535c --- /dev/null +++ b/addons/phantom_camera/scripts/panel/updater/update_button.gd @@ -0,0 +1,177 @@ +####################################################################### +# 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: + if ProjectSettings.get_setting(UPDATER_CONSTANTS.setting_updater_mode) == 1: ## For console output mode + + 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: + if ProjectSettings.get_setting(UPDATER_CONSTANTS.setting_updater_mode) == 0: return + + http_request.request(REMOTE_RELEASE_URL) + +#endregion diff --git a/addons/phantom_camera/scripts/panel/updater/update_button.gd.uid b/addons/phantom_camera/scripts/panel/updater/update_button.gd.uid new file mode 100644 index 0000000..cb88ddc --- /dev/null +++ b/addons/phantom_camera/scripts/panel/updater/update_button.gd.uid @@ -0,0 +1 @@ +uid://bwc42i46603qn diff --git a/addons/phantom_camera/scripts/panel/updater/updater_constants.gd b/addons/phantom_camera/scripts/panel/updater/updater_constants.gd new file mode 100644 index 0000000..94ac2ad --- /dev/null +++ b/addons/phantom_camera/scripts/panel/updater/updater_constants.gd @@ -0,0 +1,8 @@ +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_mode: StringName = setting_updater_name + "updater_mode" diff --git a/addons/phantom_camera/scripts/panel/updater/updater_constants.gd.uid b/addons/phantom_camera/scripts/panel/updater/updater_constants.gd.uid new file mode 100644 index 0000000..1f86170 --- /dev/null +++ b/addons/phantom_camera/scripts/panel/updater/updater_constants.gd.uid @@ -0,0 +1 @@ +uid://c8qkbc38waor2 diff --git a/addons/phantom_camera/scripts/panel/viewfinder/host_list.gd b/addons/phantom_camera/scripts/panel/viewfinder/host_list.gd new file mode 100644 index 0000000..662e598 --- /dev/null +++ b/addons/phantom_camera/scripts/panel/viewfinder/host_list.gd @@ -0,0 +1,112 @@ +@tool +extends VBoxContainer + +#region Constants + +const _constants := preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd") +const _host_list_item: PackedScene = preload("res://addons/phantom_camera/panel/viewfinder/host_list/host_list_item.tscn") + +#endregion + +signal pcam_host_removed(pcam_host: PhantomCameraHost) + +@onready var _host_list_button: Button = %HostListButton +@onready var _host_list_scroll_container: ScrollContainer = %ScrollContainer +@onready var _host_list_item_container: VBoxContainer = %HostListContainer + +var _host_list_open: bool = false + +var _bottom_offset_value: float + +var _pcam_host_list: Array[PhantomCameraHost] +var _pcam_manager: Node + +var _viewfinder_panel: Control + +#region Private Functions + +func _ready() -> void: + _host_list_button.pressed.connect(_host_list_button_pressed) + if Engine.has_singleton(_constants.PCAM_MANAGER_NODE_NAME): + _pcam_manager = Engine.get_singleton(_constants.PCAM_MANAGER_NODE_NAME) + _pcam_manager.pcam_host_removed_from_scene.connect(_remove_pcam_host) + + if not get_parent() is Control: return # To prevent errors when opening the scene on its own + _viewfinder_panel = get_parent() + _viewfinder_panel.resized.connect(_set_offset_top) + + _host_list_item_container.resized.connect(_set_offset_top) + + +func _set_offset_top() -> void: + offset_top = _set_host_list_size() + + +func _host_list_button_pressed() -> void: + _host_list_open = !_host_list_open + + var tween: Tween = create_tween() + var max_duration: float = 0.6 + + # 300 being the minimum size of the viewfinder's height + var duration: float = clampf( + max_duration / (300 / _host_list_item_container.size.y), + 0.3, + max_duration) + + tween.tween_property(self, "offset_top", _set_host_list_size(), duration)\ + .set_ease(Tween.EASE_OUT)\ + .set_trans(Tween.TRANS_QUINT) + + +func _set_host_list_size() -> float: + if not _host_list_open: + return clampf( + _viewfinder_panel.size.y - \ + _host_list_item_container.size.y - \ + _host_list_button.size.y - 20, + 0, + INF + ) + else: + return (_viewfinder_panel.size.y - _host_list_button.size.y / 2) + + +func _remove_pcam_host(pcam_host: PhantomCameraHost) -> void: + if _pcam_host_list.has(pcam_host): + _pcam_host_list.erase(pcam_host) + + var freed_pcam_host: Control + for host_list_item_instance in _host_list_item_container.get_children(): + if not host_list_item_instance.pcam_host == pcam_host: continue + freed_pcam_host = host_list_item_instance + host_list_item_instance.queue_free() + +#endregion + +#region Public Functions + +func add_pcam_host(pcam_host: PhantomCameraHost, is_default: bool) -> void: + if _pcam_host_list.has(pcam_host): return + + _pcam_host_list.append(pcam_host) + + var host_list_item_instance: PanelContainer = _host_list_item.instantiate() + var switch_pcam_host_button: Button = host_list_item_instance.get_node("%SwitchPCamHost") + if is_default: switch_pcam_host_button.button_pressed = true + + if not pcam_host.tree_exiting.is_connected(_remove_pcam_host): + pcam_host.tree_exiting.connect(_remove_pcam_host.bind(pcam_host)) + + host_list_item_instance.pcam_host = pcam_host + + _host_list_item_container.add_child(host_list_item_instance) + + +func clear_pcam_host_list() -> void: + _pcam_host_list.clear() + + for host_list_item_instance in _host_list_item_container.get_children(): + host_list_item_instance.queue_free() + +#endregion diff --git a/addons/phantom_camera/scripts/panel/viewfinder/host_list.gd.uid b/addons/phantom_camera/scripts/panel/viewfinder/host_list.gd.uid new file mode 100644 index 0000000..6923d3e --- /dev/null +++ b/addons/phantom_camera/scripts/panel/viewfinder/host_list.gd.uid @@ -0,0 +1 @@ +uid://c84cxry3t35ny diff --git a/addons/phantom_camera/scripts/panel/viewfinder/host_list_item.gd b/addons/phantom_camera/scripts/panel/viewfinder/host_list_item.gd new file mode 100644 index 0000000..5707974 --- /dev/null +++ b/addons/phantom_camera/scripts/panel/viewfinder/host_list_item.gd @@ -0,0 +1,58 @@ +@tool +extends Control + +const button_group_resource: ButtonGroup = preload("res://addons/phantom_camera/panel/viewfinder/host_list/host_list_item_group.tres") +const _constants = preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd") + +@onready var select_pcam_host: Button = %SelectPCamHost +@onready var switch_pcam_host: Button = %SwitchPCamHost + +var pcam_host: PhantomCameraHost: + set(value): + pcam_host = value + if not is_instance_valid(value): return + if not pcam_host.renamed.is_connected(_rename_pcam_host): + pcam_host.renamed.connect(_rename_pcam_host) + pcam_host.has_error.connect(_pcam_host_has_error) + get: + return pcam_host + +var _pcam_manager: Node + +#region Private fucntions + +func _ready() -> void: + switch_pcam_host.button_group = button_group_resource + select_pcam_host.pressed.connect(_select_pcam) + switch_pcam_host.pressed.connect(_switch_pcam_host) + + if not is_instance_valid(pcam_host): return + switch_pcam_host.text = pcam_host.name + + _pcam_host_has_error() + + +func _pcam_host_has_error() -> void: + if pcam_host.show_warning: + %ErrorPCamHost.visible = true + else: + %ErrorPCamHost.visible = false + + +func _rename_pcam_host() -> void: + switch_pcam_host.text = pcam_host.name + + +func _select_pcam() -> void: + EditorInterface.get_selection().clear() + EditorInterface.get_selection().add_node(pcam_host) + + +func _switch_pcam_host() -> void: + if not Engine.has_singleton(_constants.PCAM_MANAGER_NODE_NAME): return + if not is_instance_valid(_pcam_manager): + _pcam_manager = Engine.get_singleton(_constants.PCAM_MANAGER_NODE_NAME) + + _pcam_manager.viewfinder_pcam_host_switch.emit(pcam_host) + +#endregion diff --git a/addons/phantom_camera/scripts/panel/viewfinder/host_list_item.gd.uid b/addons/phantom_camera/scripts/panel/viewfinder/host_list_item.gd.uid new file mode 100644 index 0000000..9df2919 --- /dev/null +++ b/addons/phantom_camera/scripts/panel/viewfinder/host_list_item.gd.uid @@ -0,0 +1 @@ +uid://bv24ubx8mutw7 diff --git a/addons/phantom_camera/scripts/panel/viewfinder/viewfinder.gd b/addons/phantom_camera/scripts/panel/viewfinder/viewfinder.gd new file mode 100644 index 0000000..fe163a7 --- /dev/null +++ b/addons/phantom_camera/scripts/panel/viewfinder/viewfinder.gd @@ -0,0 +1,605 @@ +@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 _viewfinder: Control = %Viewfinder +@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: TextureRect = %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 + +@onready var _pcam_host_list: VBoxContainer = %PCamHostList + +#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 _pcam_manager: Node + +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.visible = false + if _root_node is Node2D: + _is_2d = true + else: + _is_2d = false + + _set_viewfinder(_root_node, false) + + if not Engine.is_editor_hint(): + _empty_state_control.visible = false + + _priority_override_button.visible = false + + # Triggered when viewport size is changed in Project Settings + ProjectSettings.settings_changed.connect(_settings_changed) + + # PCam Host List + _pcam_host_list.visible = false + _assign_manager() + _visibility_check() + + +func _pcam_host_switch(new_pcam_host: PhantomCameraHost) -> void: + _set_viewfinder_camera(new_pcam_host, true) + + +func _exit_tree() -> void: + 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 not _is_2d: return + 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 _visibility_check() -> void: + if not viewfinder_visible: return + + var pcam_host: PhantomCameraHost + var has_camera: bool = false + if not Engine.has_singleton(_constants.PCAM_MANAGER_NODE_NAME): return + + if not Engine.get_singleton(_constants.PCAM_MANAGER_NODE_NAME).get_phantom_camera_hosts().is_empty(): + has_camera = true + pcam_host = Engine.get_singleton(_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 = pcam_host.camera_2d + else: + camera_2d = _get_camera_2d() + + _is_2d = true + is_scene = true + _add_node_button.visible = true + _check_camera(root, camera_2d) + elif root is Node3D: + var camera_3d: Camera3D + if has_camera: + camera_3d = pcam_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.visible = true + _check_camera(root, camera_3d) + else: + # Is not a 2D or 3D scene + is_scene = false + _set_empty_viewfinder_state(_no_open_scene_string, _no_open_scene_icon) + _add_node_button.visible = false + + # Checks if a new scene is created and changes viewfinder accordingly + if not get_tree().node_added.is_connected(_node_added_to_scene): + get_tree().node_added.connect(_node_added_to_scene) + + if not _priority_override_button.pressed.is_connected(_select_override_pcam): + _priority_override_button.pressed.connect(_select_override_pcam) + + +func _node_added_to_scene(node: Node) -> void: + if node is Node2D or node is Node3D: + get_tree().node_added.disconnect(_node_added_to_scene) + _visibility_check() + + +func _get_camera_2d() -> Camera2D: + var edited_scene_root: Node = EditorInterface.get_edited_scene_root() + + if edited_scene_root == null: return null + + var viewport: Viewport = edited_scene_root.get_viewport() + if viewport == null: return null + + var viewport_rid: RID = viewport.get_viewport_rid() + if viewport_rid == null: return null + + var camerasGroupName: String = "__cameras_%d" % viewport_rid.get_id() + var cameras: Array[Node] = 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) -> void: + var camera_string: String + var pcam_string: String + var color: 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) + _set_viewfinder_state() + %NoSupportMsg.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.visible = false + _viewfinder.visible = true + + if is_instance_valid(_active_pcam): + if _active_pcam.get_follow_mode() == _active_pcam.FollowMode.FRAMED: + _dead_zone_h_box_container.visible = true + target_point.visible = true + else: + _dead_zone_h_box_container.visible = false + target_point.visible = false + + +func _set_empty_viewfinder_state(text: String, icon: CompressedTexture2D) -> void: + _viewfinder.visible = false + _framed_view_visible(false) + + _empty_state_control.visible = true + _empty_state_icon.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 scene_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(scene_root, camera, _constants.CAMERA_2D_NODE_NAME) + _constants.CAMERA_3D_NODE_NAME: + var camera: Camera3D = Camera3D.new() + _instantiate_node(scene_root, camera, _constants.CAMERA_3D_NODE_NAME) + _constants.PCAM_HOST_NODE_NAME: + var pcam_host: PhantomCameraHost = PhantomCameraHost.new() + var camera_owner: Node + if _is_2d: + camera_owner = _get_camera_2d() + else: + camera_owner = get_tree().get_edited_scene_root().get_viewport().get_camera_3d() + _instantiate_node( + scene_root, + pcam_host, + _constants.PCAM_HOST_NODE_NAME, + camera_owner + ) + _constants.PCAM_2D_NODE_NAME: + var pcam_2D: PhantomCamera2D = PhantomCamera2D.new() + _instantiate_node(scene_root, pcam_2D, _constants.PCAM_2D_NODE_NAME) + _constants.PCAM_3D_NODE_NAME: + var pcam_3D: PhantomCamera3D = PhantomCamera3D.new() + _instantiate_node(scene_root, pcam_3D, _constants.PCAM_3D_NODE_NAME) + + _visibility_check() + + +func _instantiate_node(scene_root: Node, node: Node, name: String, parent: Node = scene_root) -> void: + node.set_name(name) + parent.add_child(node) + node.owner = 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: + _pcam_host_list.visible = false + _set_viewfinder_camera(pcam_host_group[0], editor) + else: + _pcam_host_list.visible = true + _set_viewfinder_camera(pcam_host_group[0], editor) + for i in pcam_host_group.size(): + var is_default: bool = false + if i == 0: + is_default = true + _pcam_host_list.add_pcam_host(pcam_host_group[i], is_default) + + +func _set_viewfinder_camera(new_pcam_host: PhantomCameraHost, editor: bool) -> void: + pcam_host = new_pcam_host + + if _is_2d: + _selected_camera = pcam_host.camera_2d + + if editor: + sub_viewport.disable_3d = true + pcam_host = pcam_host + _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 + 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) + + set_process(true) + + if not pcam_host.viewfinder_update.is_connected(_on_update_editor_viewfinder): + pcam_host.viewfinder_update.connect(_on_update_editor_viewfinder) + + if not pcam_host.viewfinder_disable_dead_zone.is_connected(_disconnect_dead_zone): + pcam_host.viewfinder_disable_dead_zone.connect(_disconnect_dead_zone) + + if not aspect_ratio_container.resized.is_connected(_resized): + aspect_ratio_container.resized.connect(_resized) + + if is_instance_valid(pcam_host.get_active_pcam()): + _active_pcam = pcam_host.get_active_pcam() + else: + _framed_view_visible(false) + _active_pcam = null + return + + if not _active_pcam.follow_mode == PhantomCamera2D.FollowMode.FRAMED: return + + _framed_view_visible(true) + _on_dead_zone_changed() + _connect_dead_zone() + + +func _connect_dead_zone() -> void: + if not _active_pcam and is_instance_valid(pcam_host.get_active_pcam()): + _active_pcam = pcam_host.get_active_pcam() + + if not _active_pcam.dead_zone_changed.is_connected(_on_dead_zone_changed): + _active_pcam.dead_zone_changed.connect(_on_dead_zone_changed) + + _framed_view_visible(true) + _viewfinder.visible = true + _on_dead_zone_changed() + +func _disconnect_dead_zone() -> void: + if not is_instance_valid(_active_pcam): return + _framed_view_visible(_is_framed_pcam()) + + if _active_pcam.follow_mode_changed.is_connected(_check_follow_mode): + _active_pcam.follow_mode_changed.disconnect(_check_follow_mode) + + if _active_pcam.dead_zone_changed.is_connected(_on_dead_zone_changed): + _active_pcam.dead_zone_changed.disconnect(_on_dead_zone_changed) + + +func _resized() -> void: + _on_dead_zone_changed() + + +func _is_framed_pcam() -> bool: + if not is_instance_valid(pcam_host): return false + _active_pcam = pcam_host.get_active_pcam() + if not is_instance_valid(_active_pcam): return false + if not _active_pcam.follow_mode == PhantomCamera2D.FollowMode.FRAMED: return false + + return true + + +func _framed_view_visible(should_show: bool) -> void: + if should_show: + target_point.visible = true + _dead_zone_h_box_container.visible = true + else: + target_point.visible = false + _dead_zone_h_box_container.visible = false + + +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 + + if not _active_pcam == pcam_host.get_active_pcam(): + _active_pcam == pcam_host.get_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 + + +func _check_follow_mode() -> void: + _framed_view_visible(_is_framed_pcam()) + + +func _on_update_editor_viewfinder(check_framed_view: bool = false) -> void: + _active_pcam = pcam_host.get_active_pcam() + + if not is_instance_valid(_active_pcam): return + + if not _active_pcam.follow_mode_changed.is_connected(_check_follow_mode): + _active_pcam.follow_mode_changed.connect(_check_follow_mode) + + if _active_pcam.priority_override: + _priority_override_button.visible = true + _priority_override_name_label.set_text(_active_pcam.name) + _priority_override_button.set_tooltip_text(_active_pcam.name) + else: + _priority_override_button.visible = false + + _framed_view_visible(false) + if not check_framed_view: return + if _is_framed_pcam(): _connect_dead_zone() + + +func _select_override_pcam() -> void: + EditorInterface.get_selection().clear() + EditorInterface.get_selection().add_node(_active_pcam) + + +func _assign_manager() -> void: + if not is_instance_valid(_pcam_manager): + if Engine.has_singleton(_constants.PCAM_MANAGER_NODE_NAME): + _pcam_manager = Engine.get_singleton(_constants.PCAM_MANAGER_NODE_NAME) + _pcam_manager.pcam_host_added_to_scene.connect(_pcam_changed) + _pcam_manager.pcam_host_removed_from_scene.connect(_pcam_host_removed_from_scene) + + _pcam_manager.pcam_added_to_scene.connect(_pcam_changed) + _pcam_manager.pcam_removed_from_scene.connect(_pcam_changed) + + _pcam_manager.viewfinder_pcam_host_switch.connect(_pcam_host_switch) + + +func _pcam_host_removed_from_scene(pcam_host: PhantomCameraHost) -> void: + if _pcam_manager.phantom_camera_hosts.size() < 2: + _pcam_host_list.visible = false + + _visibility_check() + + +func _pcam_changed(pcam: Node) -> void: + _visibility_check() + +#endregion + + +#region Public Functions + +func set_visibility(visible: bool) -> void: + if visible: + viewfinder_visible = true + _visibility_check() + else: + viewfinder_visible = false + + +func update_dead_zone() -> void: + _set_viewfinder(_root_node, true) + + +## TODO - Signal can be added directly to this file with the changes in Godot 4.5 (https://github.com/godotengine/godot/pull/102986) +func scene_changed(scene_root: Node) -> void: + _assign_manager() + _priority_override_button.visible = false + _pcam_host_list.clear_pcam_host_list() + + if not scene_root is Node2D and not scene_root is Node3D: + is_scene = false + _pcam_host_list.visible = false + _set_empty_viewfinder_state(_no_open_scene_string, _no_open_scene_icon) + _add_node_button.visible = false + else: + _visibility_check() + +#endregion diff --git a/addons/phantom_camera/scripts/panel/viewfinder/viewfinder.gd.uid b/addons/phantom_camera/scripts/panel/viewfinder/viewfinder.gd.uid new file mode 100644 index 0000000..f115630 --- /dev/null +++ b/addons/phantom_camera/scripts/panel/viewfinder/viewfinder.gd.uid @@ -0,0 +1 @@ +uid://drmv3363t8amc diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCamera.cs b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera.cs new file mode 100644 index 0000000..267adf0 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera.cs @@ -0,0 +1,253 @@ +using Godot; +using PhantomCamera.Noise; + +#nullable enable + +namespace PhantomCamera; + +public enum InactiveUpdateMode +{ + Always, + Never +} + +public abstract class PhantomCamera +{ + protected readonly GodotObject Node; + + public delegate void BecameActiveEventHandler(); + public delegate void BecameInactiveEventHandler(); + public delegate void FollowTargetChangedEventHandler(); + public delegate void DeadZoneChangedEventHandler(); + public delegate void TweenStartedEventHandler(); + public delegate void IsTweeningEventHandler(); + public delegate void TweenCompletedEventHandler(); + + public event BecameActiveEventHandler? BecameActive; + public event BecameInactiveEventHandler? BecameInactive; + public event FollowTargetChangedEventHandler? FollowTargetChanged; + public event DeadZoneChangedEventHandler? DeadZoneChanged; + public event TweenStartedEventHandler? TweenStarted; + public event IsTweeningEventHandler? IsTweening; + public event TweenCompletedEventHandler? TweenCompleted; + + private readonly Callable _callableBecameActive; + private readonly Callable _callableBecameInactive; + private readonly Callable _callableFollowTargetChanged; + private readonly Callable _callableDeadZoneChanged; + private readonly Callable _callableTweenStarted; + private readonly Callable _callableIsTweening; + private readonly Callable _callableTweenCompleted; + + public int Priority + { + get => (int)Node.Call(MethodName.GetPriority); + set => Node.Call(MethodName.SetPriority, value); + } + + public bool IsActive => (bool)Node.Call(MethodName.IsActive); + + public bool FollowDamping + { + get => (bool)Node.Call(MethodName.GetFollowDamping); + set => Node.Call(MethodName.SetFollowDamping, value); + } + + public bool IsFollowing => (bool)Node.Call(PhantomCamera.MethodName.IsFollowing); + + public float DeadZoneWidth + { + get => (float)Node.Get(PropertyName.DeadZoneWidth); + set => Node.Set(PropertyName.DeadZoneWidth, value); + } + + public float DeadZoneHeight + { + get => (float)Node.Get(PropertyName.DeadZoneHeight); + set => Node.Set(PropertyName.DeadZoneHeight, value); + } + + public PhantomCameraTween TweenResource + { + get => new((Resource)Node.Call(MethodName.GetTweenResource)); + set => Node.Call(MethodName.SetTweenResource, (GodotObject)value.Resource); + } + + public bool TweenSkip + { + get => (bool)Node.Call(MethodName.GetTweenSkip); + set => Node.Call(MethodName.SetTweenSkip, value); + } + + public float TweenDuration + { + get => (float)Node.Call(MethodName.GetTweenDuration); + set => Node.Call(MethodName.SetTweenDuration, value); + } + + public TransitionType TweenTransition + { + get => (TransitionType)(int)Node.Call(MethodName.GetTweenTransition); + set => Node.Call(MethodName.SetTweenTransition, (int)value); + } + + public EaseType TweenEase + { + get => (EaseType)(int)Node.Call(MethodName.GetTweenEase); + set => Node.Call(MethodName.SetTweenEase, (int)value); + } + + public bool TweenOnLoad + { + get => (bool)Node.Call(MethodName.GetTweenOnLoad); + set => Node.Call(MethodName.SetTweenOnLoad, value); + } + + public InactiveUpdateMode InactiveUpdateMode + { + get => (InactiveUpdateMode)(int)Node.Call(MethodName.GetInactiveUpdateMode); + set => Node.Call(MethodName.SetInactiveUpdateMode, (int)value); + } + + public int HostLayers + { + get => (int)Node.Call(MethodName.GetHostLayers); + set => Node.Call(MethodName.SetHostLayers, value); + } + + public int NoiseEmitterLayer + { + get => (int)Node.Call(MethodName.GetNoiseEmitterLayer); + set => Node.Call(MethodName.SetNoiseEmitterLayer, value); + } + + public void TeleportPosition() + { + Node.Call(MethodName.TeleportPosition); + } + + public void SetHostLayersValue(int layer, bool enabled) + { + Node.Call(MethodName.SetHostLayersValue, layer, enabled); + } + + protected PhantomCamera(GodotObject phantomCameraNode) + { + Node = phantomCameraNode; + + _callableBecameActive = Callable.From(() => BecameActive?.Invoke()); + _callableBecameInactive = Callable.From(() => BecameInactive?.Invoke()); + _callableFollowTargetChanged = Callable.From(() => FollowTargetChanged?.Invoke()); + _callableDeadZoneChanged = Callable.From(() => DeadZoneChanged?.Invoke()); + _callableTweenStarted = Callable.From(() => TweenStarted?.Invoke()); + _callableIsTweening = Callable.From(() => IsTweening?.Invoke()); + _callableTweenCompleted = Callable.From(() => TweenCompleted?.Invoke()); + + Node.Connect(SignalName.BecameActive, _callableBecameActive); + Node.Connect(SignalName.BecameInactive, _callableBecameInactive); + Node.Connect(SignalName.FollowTargetChanged, _callableFollowTargetChanged); + Node.Connect(SignalName.DeadZoneChanged, _callableDeadZoneChanged); + Node.Connect(SignalName.TweenStarted, _callableTweenStarted); + Node.Connect(SignalName.IsTweening, _callableIsTweening); + Node.Connect(SignalName.TweenCompleted, _callableTweenCompleted); + } + + ~PhantomCamera() + { + Node.Disconnect(SignalName.BecameActive, _callableBecameActive); + Node.Disconnect(SignalName.BecameInactive, _callableBecameInactive); + Node.Disconnect(SignalName.FollowTargetChanged, _callableFollowTargetChanged); + Node.Disconnect(SignalName.DeadZoneChanged, _callableDeadZoneChanged); + Node.Disconnect(SignalName.TweenStarted, _callableTweenStarted); + Node.Disconnect(SignalName.IsTweening, _callableIsTweening); + Node.Disconnect(SignalName.TweenCompleted, _callableTweenCompleted); + } + + public static class MethodName + { + public const string GetFollowMode = "get_follow_mode"; + public const string IsActive = "is_active"; + + public const string GetPriority = "get_priority"; + public const string SetPriority = "set_priority"; + + public const string IsFollowing = "is_following"; + + public const string GetFollowTarget = "get_follow_target"; + public const string SetFollowTarget = "set_follow_target"; + + public const string GetFollowTargets = "get_follow_targets"; + public const string SetFollowTargets = "set_follow_targets"; + + public const string TeleportPosition = "teleport_position"; + + public const string AppendFollowTargets = "append_follow_targets"; + public const string AppendFollowTargetsArray = "append_follow_targets_array"; + public const string EraseFollowTargets = "erase_follow_targets"; + + public const string GetFollowPath = "get_follow_path"; + public const string SetFollowPath = "set_follow_path"; + + public const string GetFollowOffset = "get_follow_offset"; + public const string SetFollowOffset = "set_follow_offset"; + + public const string GetFollowDamping = "get_follow_damping"; + public const string SetFollowDamping = "set_follow_damping"; + + public const string GetFollowDampingValue = "get_follow_damping_value"; + public const string SetFollowDampingValue = "set_follow_damping_value"; + + public const string GetFollowAxisLock = "get_follow_axis_lock"; + public const string SetFollowAxisLock = "set_follow_axis_lock"; + + public const string GetTweenResource = "get_tween_resource"; + public const string SetTweenResource = "set_tween_resource"; + + public const string GetTweenSkip = "get_tween_skip"; + public const string SetTweenSkip = "set_tween_skip"; + + public const string GetTweenDuration = "get_tween_duration"; + public const string SetTweenDuration = "set_tween_duration"; + + public const string GetTweenTransition = "get_tween_transition"; + public const string SetTweenTransition = "set_tween_transition"; + + public const string GetTweenEase = "get_tween_ease"; + public const string SetTweenEase = "set_tween_ease"; + + public const string GetTweenOnLoad = "get_tween_on_load"; + public const string SetTweenOnLoad = "set_tween_on_load"; + + public const string GetInactiveUpdateMode = "get_inactive_update_mode"; + public const string SetInactiveUpdateMode = "set_inactive_update_mode"; + + public const string GetHostLayers = "get_host_layers"; + public const string SetHostLayers = "set_host_layers"; + public const string SetHostLayersValue = "set_host_layers_value"; + + public const string GetNoiseEmitterLayer = "get_noise_emitter_layer"; + public const string SetNoiseEmitterLayer = "set_noise_emitter_layer"; + + public const string EmitNoise = "emit_noise"; + } + + public static class PropertyName + { + public const string DeadZoneWidth = "dead_zone_width"; + public const string DeadZoneHeight = "dead_zone_height"; + } + + public static class SignalName + { + public const string BecameActive = "became_active"; + public const string BecameInactive = "became_inactive"; + public const string FollowTargetChanged = "follow_target_changed"; + public const string DeadZoneChanged = "dead_zone_changed"; + public const string DeadZoneReached = "dead_zone_reached"; + public const string TweenStarted = "tween_started"; + public const string IsTweening = "is_tweening"; + public const string TweenCompleted = "tween_completed"; + public const string TweenInterrupted = "tween_interrupted"; + public const string NoiseEmitted = "noise_emitted"; + } +} diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCamera.cs.uid b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera.cs.uid new file mode 100644 index 0000000..856fbdb --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera.cs.uid @@ -0,0 +1 @@ +uid://d3wh0457i0i3 diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCamera2D.cs b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera2D.cs new file mode 100644 index 0000000..20b3ba5 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera2D.cs @@ -0,0 +1,351 @@ +using System.Linq; +using Godot; +using Godot.Collections; +using PhantomCamera.Noise; + +#nullable enable + +namespace PhantomCamera; + +public enum FollowMode2D +{ + None, + Glued, + Simple, + Group, + Path, + Framed +} + +public enum FollowLockAxis2D +{ + None, + X, + Y, + XY +} + +public static class PhantomCamera2DExtensions +{ + public static PhantomCamera2D AsPhantomCamera2D(this Node2D node2D) + { + return new PhantomCamera2D(node2D); + } + + public static PhantomCameraNoiseEmitter2D AsPhantomCameraNoiseEmitter2D(this Node2D node2D) + { + return new PhantomCameraNoiseEmitter2D(node2D); + } + + public static PhantomCameraNoise2D AsPhantomCameraNoise2D(this Resource resource) + { + return new PhantomCameraNoise2D(resource); + } +} + +public class PhantomCamera2D : PhantomCamera +{ + public Node2D Node2D => (Node2D)Node; + + public delegate void TweenInterruptedEventHandler(Node2D pCam); + public delegate void DeadZoneReachedEventHandler(Vector2 side); + public delegate void NoiseEmittedEventHandler(Transform2D output); + + public event TweenInterruptedEventHandler? TweenInterrupted; + public event DeadZoneReachedEventHandler? DeadZoneReached; + public event NoiseEmittedEventHandler? NoiseEmitted; + + private readonly Callable _callableTweenInterrupted; + private readonly Callable _callableDeadZoneReached; + private readonly Callable _callableNoiseEmitted; + + public Node2D FollowTarget + { + get => (Node2D)Node2D.Call(PhantomCamera.MethodName.GetFollowTarget); + set => Node2D.Call(PhantomCamera.MethodName.SetFollowTarget, value); + } + + public Node2D[] FollowTargets + { + get => Node2D.Call(PhantomCamera.MethodName.GetFollowTargets).AsGodotArray().ToArray(); + set => Node2D.Call(PhantomCamera.MethodName.SetFollowTargets, new Array(value)); + } + + public void AppendFollowTargets(Node2D target) => Node2D.Call(PhantomCamera.MethodName.AppendFollowTargets, target); + public void AppendFollowTargetsArray(Node2D[] targets) => Node2D.Call(PhantomCamera.MethodName.AppendFollowTargetsArray, targets); + public void EraseFollowTargets(Node2D target) => Node2D.Call(PhantomCamera.MethodName.EraseFollowTargets, target); + + public FollowMode2D FollowMode => (FollowMode2D)(int)Node.Call(PhantomCamera.MethodName.GetFollowMode); + + public Path2D FollowPath + { + get => (Path2D)Node2D.Call(PhantomCamera.MethodName.GetFollowPath); + set => Node2D.Call(PhantomCamera.MethodName.SetFollowPath, value); + } + + public Vector2 FollowOffset + { + get => (Vector2)Node2D.Call(PhantomCamera.MethodName.GetFollowOffset); + set => Node2D.Call(PhantomCamera.MethodName.SetFollowOffset, value); + } + + public Vector2 FollowDampingValue + { + get => (Vector2)Node2D.Call(PhantomCamera.MethodName.GetFollowDampingValue); + set => Node2D.Call(PhantomCamera.MethodName.SetFollowDampingValue, value); + } + + public FollowLockAxis2D FollowAxisLock + { + get => (FollowLockAxis2D)(int)Node2D.Call(PhantomCamera.MethodName.GetFollowAxisLock); + set => Node2D.Call(PhantomCamera.MethodName.SetFollowAxisLock, (int)value); + } + + public Vector2 Zoom + { + get => (Vector2)Node2D.Call(MethodName.GetZoom); + set => Node2D.Call(MethodName.SetZoom, value); + } + + public bool SnapToPixel + { + get => (bool)Node2D.Call(MethodName.GetSnapToPixel); + set => Node2D.Call(MethodName.SetSnapToPixel, value); + } + + public bool RotateWithTarget + { + get => (bool)Node2D.Call(MethodName.GetRotateWithTarget); + set => Node2D.Call(MethodName.SetRotateWithTarget, value); + } + + public float RotationOffset + { + get => (float)Node2D.Call(MethodName.GetRotationOffset); + set => Node2D.Call(MethodName.SetRotationOffset, value); + } + + public bool RotationDamping + { + get => (bool)Node2D.Call(MethodName.GetRotationDamping); + set => Node2D.Call(MethodName.SetRotationDamping, value); + } + + public float RotationDampingValue + { + get => (float)Node2D.Call(MethodName.GetRotationDampingValue); + set => Node2D.Call(MethodName.SetRotationDampingValue, value); + } + + public int LimitLeft + { + get => (int)Node2D.Call(MethodName.GetLimitLeft); + set => Node2D.Call(MethodName.SetLimitLeft, value); + } + + public int LimitTop + { + get => (int)Node2D.Call(MethodName.GetLimitTop); + set => Node2D.Call(MethodName.SetLimitTop, value); + } + + public int LimitRight + { + get => (int)Node2D.Call(MethodName.GetLimitRight); + set => Node2D.Call(MethodName.SetLimitRight, value); + } + + public int LimitBottom + { + get => (int)Node2D.Call(MethodName.GetLimitBottom); + set => Node2D.Call(MethodName.SetLimitBottom, value); + } + + public Vector4I LimitMargin + { + get => (Vector4I)Node2D.Call(MethodName.GetLimitMargin); + set => Node2D.Call(MethodName.SetLimitMargin, value); + } + + public bool AutoZoom + { + get => (bool)Node2D.Call(MethodName.GetAutoZoom); + set => Node2D.Call(MethodName.SetAutoZoom, value); + } + + public float AutoZoomMin + { + get => (float)Node2D.Call(MethodName.GetAutoZoomMin); + set => Node2D.Call(MethodName.SetAutoZoomMin, value); + } + + public float AutoZoomMax + { + get => (float)Node2D.Call(MethodName.GetAutoZoomMax); + set => Node2D.Call(MethodName.SetAutoZoomMax, value); + } + + public Vector4 AutoZoomMargin + { + get => (Vector4)Node2D.Call(MethodName.GetAutoZoomMargin); + set => Node2D.Call(MethodName.SetAutoZoomMargin, value); + } + + public bool DrawLimits + { + get => (bool)Node2D.Get(PropertyName.DrawLimits); + set => Node2D.Set(PropertyName.DrawLimits, value); + } + + public PhantomCameraNoise2D Noise + { + get => new((Resource)Node2D.Call(MethodName.GetNoise)); + set => Node2D.Call(MethodName.SetNoise, (GodotObject)value.Resource); + } + + public void EmitNoise(Transform2D transform) => Node2D.Call(PhantomCamera.MethodName.EmitNoise, transform); + + public NodePath LimitTarget + { + get => (NodePath)Node2D.Call(MethodName.GetLimitTarget); + set => Node2D.Call(MethodName.SetLimitTarget, value); + } + + public static PhantomCamera2D FromScript(string path) => new(GD.Load(path).New().AsGodotObject()); + public static PhantomCamera2D FromScript(GDScript script) => new(script.New().AsGodotObject()); + + public PhantomCamera2D(GodotObject phantomCameraNode) : base(phantomCameraNode) + { + _callableTweenInterrupted = Callable.From(pCam => TweenInterrupted?.Invoke(pCam)); + _callableDeadZoneReached = Callable.From((Vector2 side) => DeadZoneReached?.Invoke(side)); + _callableNoiseEmitted = Callable.From((Transform2D output) => NoiseEmitted?.Invoke(output)); + + Node2D.Connect(SignalName.TweenInterrupted, _callableTweenInterrupted); + Node2D.Connect(SignalName.DeadZoneReached, _callableDeadZoneReached); + Node2D.Connect(SignalName.NoiseEmitted, _callableNoiseEmitted); + } + + ~PhantomCamera2D() + { + Node2D.Disconnect(SignalName.TweenInterrupted, _callableTweenInterrupted); + Node2D.Disconnect(SignalName.DeadZoneReached, _callableDeadZoneReached); + Node2D.Disconnect(SignalName.NoiseEmitted, _callableNoiseEmitted); + } + + public void SetLimitTarget(TileMap tileMap) + { + Node2D.Call(MethodName.SetLimitTarget, tileMap.GetPath()); + } + + public void SetLimitTarget(TileMapLayer tileMapLayer) + { + Node2D.Call(MethodName.SetLimitTarget, tileMapLayer.GetPath()); + } + + public void SetLimitTarget(CollisionShape2D shape2D) + { + Node2D.Call(MethodName.SetLimitTarget, shape2D.GetPath()); + } + + public LimitTargetQueryResult? GetLimitTarget() + { + var result = (NodePath)Node2D.Call(MethodName.GetLimitTarget); + return result.IsEmpty ? null : new LimitTargetQueryResult(Node2D.GetNode(result)); + } + + public void SetLimit(Side side, int value) + { + Node2D.Call(MethodName.SetLimit, (int)side, value); + } + + public int GetLimit(Side side) + { + return (int)Node2D.Call(MethodName.GetLimit, (int)side); + } + + public new static class MethodName + { + public const string GetZoom = "get_zoom"; + public const string SetZoom = "set_zoom"; + + public const string GetSnapToPixel = "get_snap_to_pixel"; + public const string SetSnapToPixel = "set_snap_to_pixel"; + + public const string GetRotateWithTarget = "get_rotate_with_target"; + public const string SetRotateWithTarget = "set_rotate_with_target"; + + public const string GetRotationOffset = "get_rotation_offset"; + public const string SetRotationOffset = "set_rotation_offset"; + + public const string GetRotationDamping = "get_rotation_damping"; + public const string SetRotationDamping = "set_rotation_damping"; + + public const string GetRotationDampingValue = "get_rotation_damping_value"; + public const string SetRotationDampingValue = "set_rotation_damping_value"; + + public const string GetLimit = "get_limit"; + public const string SetLimit = "set_limit"; + + public const string GetLimitLeft = "get_limit_left"; + public const string SetLimitLeft = "set_limit_left"; + + public const string GetLimitTop = "get_limit_top"; + public const string SetLimitTop = "set_limit_top"; + + public const string GetLimitRight = "get_limit_right"; + public const string SetLimitRight = "set_limit_right"; + + public const string GetLimitBottom = "get_limit_bottom"; + public const string SetLimitBottom = "set_limit_bottom"; + + public const string GetLimitTarget = "get_limit_target"; + public const string SetLimitTarget = "set_limit_target"; + + public const string GetLimitMargin = "get_limit_margin"; + public const string SetLimitMargin = "set_limit_margin"; + + public const string GetAutoZoom = "get_auto_zoom"; + public const string SetAutoZoom = "set_auto_zoom"; + + public const string GetAutoZoomMin = "get_auto_zoom_min"; + public const string SetAutoZoomMin = "set_auto_zoom_min"; + + public const string GetAutoZoomMax = "get_auto_zoom_max"; + public const string SetAutoZoomMax = "set_auto_zoom_max"; + + public const string GetAutoZoomMargin = "get_auto_zoom_margin"; + public const string SetAutoZoomMargin = "set_auto_zoom_margin"; + + public const string GetNoise = "get_noise"; + public const string SetNoise = "set_noise"; + } + + public new static class PropertyName + { + public const string DrawLimits = "draw_limits"; + } +} + +public class LimitTargetQueryResult(GodotObject godotObject) +{ + public bool IsTileMap => godotObject.IsClass("TileMap"); + + public bool IsTileMapLayer => godotObject.IsClass("TileMapLayer"); + + public bool IsCollisionShape2D => godotObject.IsClass("CollisionShape2D"); + + public TileMap? AsTileMap() + { + return IsTileMap ? (TileMap)godotObject : null; + } + + public TileMapLayer? AsTileMapLayer() + { + return IsTileMapLayer ? (TileMapLayer)godotObject : null; + } + + public CollisionShape2D? AsCollisionShape2D() + { + return IsCollisionShape2D ? (CollisionShape2D)godotObject : null; + } +} diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCamera2D.cs.uid b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera2D.cs.uid new file mode 100644 index 0000000..8cc93f0 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera2D.cs.uid @@ -0,0 +1 @@ +uid://c38e5qhsf3fk3 diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCamera3D.cs b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera3D.cs new file mode 100644 index 0000000..a3081d9 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera3D.cs @@ -0,0 +1,493 @@ +using System.Linq; +using Godot; +using Godot.Collections; +using PhantomCamera.Noise; + +#nullable enable + +namespace PhantomCamera; + +public enum LookAtMode +{ + None, + Mimic, + Simple, + Group +} + +public enum FollowMode3D +{ + None, + Glued, + Simple, + Group, + Path, + Framed, + ThirdPerson +} + +public enum FollowLockAxis3D +{ + None, + X, + Y, + Z, + XY, + XZ, + YZ, + XYZ +} + +public static class PhantomCamera3DExtensions +{ + public static PhantomCamera3D AsPhantomCamera3D(this Node3D node3D) + { + return new PhantomCamera3D(node3D); + } + + public static PhantomCameraNoiseEmitter3D AsPhantomCameraNoiseEmitter3D(this Node3D node3D) + { + return new PhantomCameraNoiseEmitter3D(node3D); + } + + public static PhantomCameraNoise3D AsPhantomCameraNoise3D(this Resource resource) + { + return new PhantomCameraNoise3D(resource); + } + + public static Camera3DResource AsCamera3DResource(this Resource resource) + { + return new Camera3DResource(resource); + } + + public static Vector3 GetThirdPersonRotation(this PhantomCamera3D pCam3D) => + (Vector3)pCam3D.Node3D.Call(PhantomCamera3D.MethodName.GetThirdPersonRotation); + + public static void SetThirdPersonRotation(this PhantomCamera3D pCam3D, Vector3 rotation) => + pCam3D.Node3D.Call(PhantomCamera3D.MethodName.SetThirdPersonRotation, rotation); + + public static Vector3 GetThirdPersonRotationDegrees(this PhantomCamera3D pCam3D) => + (Vector3)pCam3D.Node3D.Call(PhantomCamera3D.MethodName.GetThirdPersonRotationDegrees); + + public static void SetThirdPersonDegrees(this PhantomCamera3D pCam3D, Vector3 rotation) => + pCam3D.Node3D.Call(PhantomCamera3D.MethodName.SetThirdPersonRotationDegrees, rotation); + + public static Quaternion GetThirdPersonQuaternion(this PhantomCamera3D pCam3D) => + (Quaternion)pCam3D.Node3D.Call(PhantomCamera3D.MethodName.GetThirdPersonQuaternion); + + public static void SetThirdPersonQuaternion(this PhantomCamera3D pCam3D, Quaternion quaternion) => + pCam3D.Node3D.Call(PhantomCamera3D.MethodName.SetThirdPersonQuaternion, quaternion); + +} + +public class PhantomCamera3D : PhantomCamera +{ + public Node3D Node3D => (Node3D)Node; + + public delegate void LookAtTargetChangedEventHandler(); + public delegate void DeadZoneReachedEventHandler(); + public delegate void Camera3DResourceChangedEventHandler(); + public delegate void Camera3DResourcePropertyChangedEventHandler(StringName property, Variant value); + public delegate void TweenInterruptedEventHandler(Node3D pCam); + public delegate void NoiseEmittedEventHandler(Transform3D output); + + public event LookAtTargetChangedEventHandler? LookAtTargetChanged; + public event DeadZoneReachedEventHandler? DeadZoneReached; + public event Camera3DResourceChangedEventHandler? Camera3DResourceChanged; + public event Camera3DResourcePropertyChangedEventHandler? Camera3DResourcePropertyChanged; + public event TweenInterruptedEventHandler? TweenInterrupted; + public event NoiseEmittedEventHandler? NoiseEmitted; + + private readonly Callable _callableLookAtTargetChanged; + private readonly Callable _callableDeadZoneReached; + private readonly Callable _callableCamera3DResourceChanged; + private readonly Callable _callableCamera3DResourcePropertyChanged; + private readonly Callable _callableTweenInterrupted; + private readonly Callable _callableNoiseEmitted; + + public Node3D FollowTarget + { + get => (Node3D)Node3D.Call(PhantomCamera.MethodName.GetFollowTarget); + set => Node3D.Call(PhantomCamera.MethodName.SetFollowTarget, value); + } + + public Node3D[] FollowTargets + { + get => Node3D.Call(PhantomCamera.MethodName.GetFollowTargets).AsGodotArray().ToArray(); + set => Node3D.Call(PhantomCamera.MethodName.SetFollowTargets, new Array(value)); + } + + public void AppendFollowTarget(Node3D target) => Node3D.Call(PhantomCamera.MethodName.AppendFollowTargets, target); + public void AppendFollowTargetArray(Node3D[] targets) => Node3D.Call(PhantomCamera.MethodName.AppendFollowTargetsArray, targets); + public void EraseFollowTarget(Node3D target) => Node3D.Call(PhantomCamera.MethodName.EraseFollowTargets, target); + + public FollowMode3D FollowMode => (FollowMode3D)(int)Node.Call(PhantomCamera.MethodName.GetFollowMode); + + public Path3D FollowPath + { + get => (Path3D)Node3D.Call(PhantomCamera.MethodName.GetFollowPath); + set => Node3D.Call(PhantomCamera.MethodName.SetFollowPath, value); + } + + public Vector3 FollowOffset + { + get => (Vector3)Node3D.Call(PhantomCamera.MethodName.GetFollowOffset); + set => Node3D.Call(PhantomCamera.MethodName.SetFollowOffset, value); + } + + public Vector3 FollowDampingValue + { + get => (Vector3)Node3D.Call(PhantomCamera.MethodName.GetFollowDampingValue); + set => Node3D.Call(PhantomCamera.MethodName.SetFollowDampingValue, value); + } + + public FollowLockAxis3D FollowAxisLock + { + get => (FollowLockAxis3D)(int)Node3D.Call(PhantomCamera.MethodName.GetFollowAxisLock); + set => Node3D.Call(PhantomCamera.MethodName.SetFollowAxisLock, (int)value); + } + + public LookAtMode LookAtMode => (LookAtMode)(int)Node3D.Call(MethodName.GetLookAtMode); + + public Camera3DResource Camera3DResource + { + get => new((Resource)Node3D.Call(MethodName.GetCamera3DResource)); + set => Node3D.Call(MethodName.SetCamera3DResource, value.Resource); + } + + public float SpringLength + { + get => (float)Node3D.Call(MethodName.GetSpringLength); + set => Node3D.Call(MethodName.SetSpringLength, value); + } + + public float VerticalRotationOffset + { + get => (float)Node3D.Call(MethodName.GetVerticalRotationOffset); + set => Node3D.Call(MethodName.SetVerticalRotationOffset, value); + } + + public float HorizontalRotationOffset + { + get => (float)Node3D.Call(MethodName.GetHorizontalRotationOffset); + set => Node3D.Call(MethodName.SetHorizontalRotationOffset, value); + } + + public float FollowDistance + { + get => (float)Node3D.Call(MethodName.GetFollowDistance); + set => Node3D.Call(MethodName.SetFollowDistance, value); + } + + public bool AutoFollowDistance + { + get => (bool)Node3D.Call(MethodName.GetAutoFollowDistance); + set => Node3D.Call(MethodName.SetAutoFollowDistance, value); + } + + public float AutoFollowDistanceMin + { + get => (float)Node3D.Call(MethodName.GetAutoFollowDistanceMin); + set => Node3D.Call(MethodName.SetAutoFollowDistanceMin, value); + } + + public float AutoFollowDistanceMax + { + get => (float)Node3D.Call(MethodName.GetAutoFollowDistanceMax); + set => Node3D.Call(MethodName.SetAutoFollowDistanceMax, value); + } + + public float AutoFollowDistanceDivisor + { + get => (float)Node3D.Call(MethodName.GetAutoFollowDistanceDivisor); + set => Node3D.Call(MethodName.SetAutoFollowDistanceDivisor, value); + } + + public Node3D LookAtTarget + { + get => (Node3D)Node3D.Call(MethodName.GetLookAtTarget); + set => Node3D.Call(MethodName.SetLookAtTarget, value); + } + + public Node3D[] LookAtTargets + { + get => Node3D.Call(MethodName.GetLookAtTargets).AsGodotArray().ToArray(); + set => Node3D.Call(MethodName.SetLookAtTargets, new Array(value)); + } + + public bool IsLooking => (bool)Node3D.Call(MethodName.IsLooking); + + public int CollisionMask + { + get => (int)Node3D.Call(MethodName.GetCollisionMask); + set => Node3D.Call(MethodName.SetCollisionMask, value); + } + + public void SetCollisionMaskValue(int maskLayer, bool enable) => + Node3D.Call(MethodName.SetCollisionMaskValue, maskLayer, enable); + + public Shape3D Shape + { + get => (Shape3D)Node3D.Call(MethodName.GetShape); + set => Node3D.Call(MethodName.SetShape, value); + } + + public float Margin + { + get => (float)Node3D.Call(MethodName.GetMargin); + set => Node3D.Call(MethodName.SetMargin, value); + } + + public Vector3 LookAtOffset + { + get => (Vector3)Node3D.Call(MethodName.GetLookAtOffset); + set => Node3D.Call(MethodName.SetLookAtOffset, value); + } + + public bool LookAtDamping + { + get => (bool)Node3D.Call(MethodName.GetLookAtDamping); + set => Node3D.Call(MethodName.SetLookAtDamping, value); + } + + public float LookAtDampingValue + { + get => (float)Node3D.Call(MethodName.GetLookAtDampingValue); + set => Node3D.Call(MethodName.SetLookAtDampingValue, value); + } + + public Node3D Up + { + get => (Node3D)Node3D.Call(MethodName.GetUp); + set => Node3D.Call(MethodName.SetUp, value); + } + + public Vector3 UpTarget + { + get => (Vector3)Node3D.Call(MethodName.GetUpTarget); + set => Node3D.Call(MethodName.SetUpTarget, value); + } + + public int CullMask + { + get => (int)Node3D.Call(MethodName.GetCullMask); + set => Node3D.Call(MethodName.SetCullMask, value); + } + + public float HOffset + { + get => (float)Node3D.Call(MethodName.GetHOffset); + set => Node3D.Call(MethodName.SetHOffset, value); + } + + public float VOffset + { + get => (float)Node3D.Call(MethodName.GetVOffset); + set => Node3D.Call(MethodName.SetVOffset, value); + } + + public ProjectionType Projection + { + get => (ProjectionType)(int)Node3D.Call(MethodName.GetProjection); + set => Node3D.Call(MethodName.SetProjection, (int)value); + } + + public float Fov + { + get => (float)Node3D.Call(MethodName.GetFov); + set => Node3D.Call(MethodName.SetFov, value); + } + + public float Size + { + get => (float)Node3D.Call(MethodName.GetSize); + set => Node3D.Call(MethodName.SetSize, value); + } + + public Vector2 FrustumOffset + { + get => (Vector2)Node3D.Call(MethodName.GetFrustumOffset); + set => Node3D.Call(MethodName.SetFrustumOffset, value); + } + + public float Far + { + get => (float)Node3D.Call(MethodName.GetFar); + set => Node3D.Call(MethodName.SetFar, value); + } + + public float Near + { + get => (float)Node3D.Call(MethodName.GetNear); + set => Node3D.Call(MethodName.SetNear, value); + } + + public Environment Environment + { + get => (Environment)Node3D.Call(MethodName.GetEnvironment); + set => Node3D.Call(MethodName.SetEnvironment, value); + } + + public CameraAttributes Attributes + { + get => (CameraAttributes)Node3D.Call(MethodName.GetAttributes); + set => Node3D.Call(MethodName.SetAttributes, value); + } + + public PhantomCameraNoise3D Noise + { + get => new((Resource)Node3D.Call(MethodName.GetNoise)); + set => Node3D.Call(MethodName.SetNoise, (GodotObject)value.Resource); + } + + public void EmitNoise(Transform3D transform) => Node3D.Call(PhantomCamera.MethodName.EmitNoise, transform); + + public static PhantomCamera3D FromScript(string path) => new(GD.Load(path).New().AsGodotObject()); + public static PhantomCamera3D FromScript(GDScript script) => new(script.New().AsGodotObject()); + + public PhantomCamera3D(GodotObject phantomCamera3DNode) : base(phantomCamera3DNode) + { + _callableLookAtTargetChanged = Callable.From(() => LookAtTargetChanged?.Invoke()); + _callableDeadZoneReached = Callable.From(() => DeadZoneReached?.Invoke()); + _callableCamera3DResourceChanged = Callable.From(() => Camera3DResourceChanged?.Invoke()); + _callableCamera3DResourcePropertyChanged = Callable.From((StringName property, Variant value) => + Camera3DResourcePropertyChanged?.Invoke(property, value)); + _callableTweenInterrupted = Callable.From(pCam => TweenInterrupted?.Invoke(pCam)); + _callableNoiseEmitted = Callable.From((Transform3D output) => NoiseEmitted?.Invoke(output)); + + Node3D.Connect(SignalName.LookAtTargetChanged, _callableLookAtTargetChanged); + Node3D.Connect(PhantomCamera.SignalName.DeadZoneReached, _callableDeadZoneReached); + Node3D.Connect(SignalName.Camera3DResourceChanged, _callableCamera3DResourceChanged); + Node3D.Connect(SignalName.Camera3DResourcePropertyChanged, _callableCamera3DResourcePropertyChanged); + Node3D.Connect(PhantomCamera.SignalName.TweenInterrupted, _callableTweenInterrupted); + Node3D.Connect(PhantomCamera.SignalName.NoiseEmitted, _callableNoiseEmitted); + } + + ~PhantomCamera3D() + { + Node3D.Disconnect(SignalName.LookAtTargetChanged, _callableLookAtTargetChanged); + Node3D.Disconnect(PhantomCamera.SignalName.DeadZoneReached, _callableDeadZoneReached); + Node3D.Disconnect(SignalName.Camera3DResourceChanged, _callableCamera3DResourceChanged); + Node3D.Disconnect(SignalName.Camera3DResourcePropertyChanged, _callableCamera3DResourcePropertyChanged); + Node3D.Disconnect(PhantomCamera.SignalName.TweenInterrupted, _callableTweenInterrupted); + Node3D.Disconnect(PhantomCamera.SignalName.NoiseEmitted, _callableNoiseEmitted); + } + + public new static class MethodName + { + public const string GetLookAtMode = "get_look_at_mode"; + + public const string GetCamera3DResource = "get_camera_3d_resource"; + public const string SetCamera3DResource = "set_camera_3d_resource"; + + public const string GetThirdPersonRotation = "get_third_person_rotation"; + public const string SetThirdPersonRotation = "set_third_person_rotation"; + + public const string GetThirdPersonRotationDegrees = "get_third_person_rotation_degrees"; + public const string SetThirdPersonRotationDegrees = "set_third_person_rotation_degrees"; + + public const string GetThirdPersonQuaternion = "get_third_person_quaternion"; + public const string SetThirdPersonQuaternion = "set_third_person_quaternion"; + + public const string GetVerticalRotationOffset = "get_vertical_rotation_offset"; + public const string SetVerticalRotationOffset = "set_vertical_rotation_offset"; + + public const string GetHorizontalRotationOffset = "get_horizontal_rotation_offset"; + public const string SetHorizontalRotationOffset = "set_horizontal_rotation_offset"; + + public const string GetSpringLength = "get_spring_length"; + public const string SetSpringLength = "set_spring_length"; + + public const string GetFollowDistance = "get_follow_distance"; + public const string SetFollowDistance = "set_follow_distance"; + + public const string GetAutoFollowDistance = "get_auto_follow_distance"; + public const string SetAutoFollowDistance = "set_auto_follow_distance"; + + public const string GetAutoFollowDistanceMin = "get_auto_follow_distance_min"; + public const string SetAutoFollowDistanceMin = "set_auto_follow_distance_min"; + + public const string GetAutoFollowDistanceMax = "get_auto_follow_distance_max"; + public const string SetAutoFollowDistanceMax = "set_auto_follow_distance_max"; + + public const string GetAutoFollowDistanceDivisor = "get_auto_follow_distance_divisor"; + public const string SetAutoFollowDistanceDivisor = "set_auto_follow_distance_divisor"; + + public const string GetLookAtTarget = "get_look_at_target"; + public const string SetLookAtTarget = "set_look_at_target"; + + public const string GetLookAtTargets = "get_look_at_targets"; + public const string SetLookAtTargets = "set_look_at_targets"; + + public const string IsLooking = "is_looking"; + + public const string GetUp = "get_up"; + public const string SetUp = "set_up"; + + public const string GetUpTarget = "get_up_target"; + public const string SetUpTarget = "set_up_target"; + + public const string GetCollisionMask = "get_collision_mask"; + public const string SetCollisionMask = "set_collision_mask"; + + public const string SetCollisionMaskValue = "set_collision_mask_value"; + + public const string GetShape = "get_shape"; + public const string SetShape = "set_shape"; + + public const string GetMargin = "get_margin"; + public const string SetMargin = "set_margin"; + + public const string GetLookAtOffset = "get_look_at_offset"; + public const string SetLookAtOffset = "set_look_at_offset"; + + public const string GetLookAtDamping = "get_look_at_damping"; + public const string SetLookAtDamping = "set_look_at_damping"; + + public const string GetLookAtDampingValue = "get_look_at_damping_value"; + public const string SetLookAtDampingValue = "set_look_at_damping_value"; + + public const string GetCullMask = "get_cull_mask"; + public const string SetCullMask = "set_cull_mask"; + + public const string GetHOffset = "get_h_offset"; + public const string SetHOffset = "set_h_offset"; + + public const string GetVOffset = "get_v_offset"; + public const string SetVOffset = "set_v_offset"; + + public const string GetProjection = "get_projection"; + public const string SetProjection = "set_projection"; + + public const string GetFov = "get_fov"; + public const string SetFov = "set_fov"; + + public const string GetSize = "get_size"; + public const string SetSize = "set_size"; + + public const string GetFrustumOffset = "get_frustum_offset"; + public const string SetFrustumOffset = "set_frustum_offset"; + + public const string GetFar = "get_far"; + public const string SetFar = "set_far"; + + public const string GetNear = "get_near"; + public const string SetNear = "set_near"; + + public const string GetEnvironment = "get_environment"; + public const string SetEnvironment = "set_environment"; + + public const string GetAttributes = "get_attributes"; + public const string SetAttributes = "set_attributes"; + + public const string GetNoise = "get_noise"; + public const string SetNoise = "set_noise"; + } + + public new static class SignalName + { + public const string LookAtTargetChanged = "look_at_target_changed"; + public const string Camera3DResourceChanged = "camera_3d_resource_changed"; + public const string Camera3DResourcePropertyChanged = "camera_3d_resource_property_changed"; + } +} diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCamera3D.cs.uid b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera3D.cs.uid new file mode 100644 index 0000000..c1f0801 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera3D.cs.uid @@ -0,0 +1 @@ +uid://bx3g7jxtwhi04 diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter2D.cs b/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter2D.cs new file mode 100644 index 0000000..1e73b57 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter2D.cs @@ -0,0 +1,83 @@ +using Godot; + +namespace PhantomCamera.Noise; + +public class PhantomCameraNoiseEmitter2D(GodotObject node) +{ + public Node2D Node2D = (Node2D)node; + + public PhantomCameraNoise2D Noise + { + get => new((Resource)Node2D.Call(MethodName.GetNoise)); + set => Node2D.Call(MethodName.SetNoise, (GodotObject)value.Resource); + } + + public bool Continuous + { + get => (bool)Node2D.Call(MethodName.GetContinuous); + set => Node2D.Call(MethodName.SetContinuous, value); + } + + public float GrowthTime + { + get => (float)Node2D.Call(MethodName.GetGrowthTime); + set => Node2D.Call(MethodName.SetGrowthTime, value); + } + + public float Duration + { + get => (float)Node2D.Call(MethodName.GetDuration); + set => Node2D.Call(MethodName.SetDuration, value); + } + + public float DecayTime + { + get => (float)Node2D.Call(MethodName.GetDecayTime); + set => Node2D.Call(MethodName.SetDecayTime, value); + } + + public int NoiseEmitterLayer + { + get => (int)Node2D.Call(MethodName.GetNoiseEmitterLayer); + set => Node2D.Call(MethodName.SetNoiseEmitterLayer, value); + } + + public void SetNoiseEmitterLayerValue(int layer, bool value) => + Node2D.Call(MethodName.SetNoiseEmitterLayerValue, layer, value); + + public void Emit() => Node2D.Call(MethodName.Emit); + + public bool IsEmitting() => (bool)Node2D.Call(MethodName.IsEmitting); + + public void Stop() => Node2D.Call(MethodName.Stop); + + public void Toggle() => Node2D.Call(MethodName.Toggle); + + public static class MethodName + { + public const string GetNoise = "get_noise"; + public const string SetNoise = "set_noise"; + + public const string GetContinuous = "get_continuous"; + public const string SetContinuous = "set_continuous"; + + public const string GetGrowthTime = "get_growth_time"; + public const string SetGrowthTime = "set_growth_time"; + + public const string GetDuration = "get_duration"; + public const string SetDuration = "set_duration"; + + public const string GetDecayTime = "get_decay_time"; + public const string SetDecayTime = "set_decay_time"; + + public const string GetNoiseEmitterLayer = "get_noise_emitter_layer"; + public const string SetNoiseEmitterLayer = "set_noise_emitter_layer"; + + public const string SetNoiseEmitterLayerValue = "set_noise_emitter_layer_value"; + + public const string Emit = "emit"; + public const string IsEmitting = "is_emitting"; + public const string Stop = "stop"; + public const string Toggle = "toggle"; + } +} diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter2D.cs.uid b/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter2D.cs.uid new file mode 100644 index 0000000..4f35145 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter2D.cs.uid @@ -0,0 +1 @@ +uid://btom8l3wlkn2j diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter3D.cs b/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter3D.cs new file mode 100644 index 0000000..18620b2 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter3D.cs @@ -0,0 +1,83 @@ +using Godot; + +namespace PhantomCamera.Noise; + +public class PhantomCameraNoiseEmitter3D(GodotObject node) +{ + public Node3D Node3D = (Node3D)node; + + public PhantomCameraNoise3D Noise + { + get => new((Resource)Node3D.Call(MethodName.GetNoise)); + set => Node3D.Call(MethodName.SetNoise, (GodotObject)value.Resource); + } + + public bool Continuous + { + get => (bool)Node3D.Call(MethodName.GetContinuous); + set => Node3D.Call(MethodName.SetContinuous, value); + } + + public float GrowthTime + { + get => (float)Node3D.Call(MethodName.GetGrowthTime); + set => Node3D.Call(MethodName.SetGrowthTime, value); + } + + public float Duration + { + get => (float)Node3D.Call(MethodName.GetDuration); + set => Node3D.Call(MethodName.SetDuration, value); + } + + public float DecayTime + { + get => (float)Node3D.Call(MethodName.GetDecayTime); + set => Node3D.Call(MethodName.SetDecayTime, value); + } + + public int NoiseEmitterLayer + { + get => (int)Node3D.Call(MethodName.GetNoiseEmitterLayer); + set => Node3D.Call(MethodName.SetNoiseEmitterLayer, value); + } + + public void SetNoiseEmitterLayerValue(int layer, bool value) => + Node3D.Call(MethodName.SetNoiseEmitterLayerValue, layer, value); + + public void Emit() => Node3D.Call(MethodName.Emit); + + public bool IsEmitting() => (bool)Node3D.Call(MethodName.IsEmitting); + + public void Stop() => Node3D.Call(MethodName.Stop); + + public void Toggle() => Node3D.Call(MethodName.Toggle); + + public static class MethodName + { + public const string GetNoise = "get_noise"; + public const string SetNoise = "set_noise"; + + public const string GetContinuous = "get_continuous"; + public const string SetContinuous = "set_continuous"; + + public const string GetGrowthTime = "get_growth_time"; + public const string SetGrowthTime = "set_growth_time"; + + public const string GetDuration = "get_duration"; + public const string SetDuration = "set_duration"; + + public const string GetDecayTime = "get_decay_time"; + public const string SetDecayTime = "set_decay_time"; + + public const string GetNoiseEmitterLayer = "get_noise_emitter_layer"; + public const string SetNoiseEmitterLayer = "set_noise_emitter_layer"; + + public const string SetNoiseEmitterLayerValue = "set_noise_emitter_layer_value"; + + public const string Emit = "emit"; + public const string IsEmitting = "is_emitting"; + public const string Stop = "stop"; + public const string Toggle = "toggle"; + } +} diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter3D.cs.uid b/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter3D.cs.uid new file mode 100644 index 0000000..bf32a5b --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter3D.cs.uid @@ -0,0 +1 @@ +uid://buvda14filkjx diff --git a/addons/phantom_camera/scripts/phantom_camera/phantom_camera_2d.gd b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_2d.gd new file mode 100644 index 0000000..966c580 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_2d.gd @@ -0,0 +1,1726 @@ +@tool +@icon("res://addons/phantom_camera/icons/phantom_camera_2d.svg") +class_name PhantomCamera2D +extends Node2D + +## Controls a scene's [Camera2D] and applies logic to it. +## +## The scene's [param Camera2D] will follow the position of the +## [param PhantomCamera2D] with the highest priority. +## Each instance can have different positional and rotational logic applied +## to them. + +#region Constants + +const _constants := preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd") + +#endregion + +#region Signals + +## Emitted when the [param PhantomCamera2D] becomes active. +signal became_active + +## Emitted when the [param PhantomCamera2D] becomes inactive. +signal became_inactive + +## Emitted when the follow_mode changes. +## Note: This is for internal use only +signal follow_mode_changed + +## Emitted when [member follow_target] changes. +signal follow_target_changed + +## Emitted when dead zones changes.[br] +## [b]Note:[/b] Only applicable in [param Framed] [enum FollowMode]. +signal dead_zone_changed + +## Emitted when a target touches the edge of the dead zone in [param Framed] [enum FollowMode]. +signal dead_zone_reached(side: Vector2) + +## Emitted when the [param Camera2D] starts to tween to another [param PhantomCamera2D]. +signal tween_started + +## Emitted when the [param Camera2D] is to tweening towards another [param PhantomCamera2D]. +signal is_tweening + +## Emitted when the tween is interrupted due to another [param PhantomCamera2D] +## becoming active. The argument is the [param PhantomCamera2D] that interrupted +## the tween. +signal tween_interrupted(pcam_2d: PhantomCamera2D) + +## Emitted when the [param Camera2D] completes its tween to the +## [param PhantomCamera2D]. +signal tween_completed + +## Emitted when Noise should be applied to the Camera2D. +signal noise_emitted(noise_output: Transform2D) + +signal physics_target_changed + +#endregion + +#region Enums + +## Determines the positional logic for a given [param PhantomCamera2D] +## [br][br] +## The different modes have different functionalities and purposes, so choosing +## the correct one depends on what each [param PhantomCamera2D] is meant to do. +enum FollowMode { + NONE = 0, ## Default - No follow logic is applied. + GLUED = 1, ## Sticks to its target. + SIMPLE = 2, ## Follows its target with an optional offset. + GROUP = 3, ## Follows multiple targets with option to dynamically reframe itself. + PATH = 4, ## Follows a target while being positionally confined to a [Path2D] node. + FRAMED = 5, ## Applies a dead zone on the frame and only follows its target when it tries to leave it. +} + +## Determines how often an inactive [param PhantomCamera2D] should update +## its positional and rotational values. This is meant to reduce the amount +## of calculations inactive [param PhantomCamera2D] are doing when idling to +## improve performance. +enum InactiveUpdateMode { + ALWAYS, ## Always updates the [param PhantomCamera2D], even when it's inactive. + NEVER, ## Never updates the [param PhantomCamera2D] when it's inactive. Reduces the amount of computational resources when inactive. +# EXPONENTIALLY, +} + +enum FollowLockAxis { + NONE = 0, + X = 1, + Y = 2, + XY = 3, +} + +#endregion + +#region Exported Properties + +## To quickly preview a [param PhantomCamera2D] without adjusting its +## [member priority], this property allows the selected PCam to ignore the +## Priority system altogether and forcefully become the active one. It's +## partly designed to work within the Viewfinder, and will be disabled when +## running a build export of the game. +@export var priority_override: bool = false: + set(value): + priority_override = value + if Engine.is_editor_hint(): + if value: + if not Engine.has_singleton(_constants.PCAM_MANAGER_NODE_NAME): return + Engine.get_singleton(_constants.PCAM_MANAGER_NODE_NAME).pcam_priority_override.emit(self, true) + else: + if not Engine.has_singleton(_constants.PCAM_MANAGER_NODE_NAME): return + Engine.get_singleton(_constants.PCAM_MANAGER_NODE_NAME).pcam_priority_override.emit(self, false) + get: + return priority_override + + +## It defines which [param PhantomCamera2D] a scene's [param Camera2D] should +## be corresponding with and be attached to. This is decided by the PCam with +## the highest [param Priority]. +## [br][br] +## Changing [param Priority] will send an event to the scene's +## [PhantomCameraHost], which will then determine whether if the +## [param Priority] value is greater than or equal to the currently +## highest [param PhantomCamera2D]'s in the scene. The [param PhantomCamera2D] +## with the highest value will then reattach the [param Camera2D] accordingly. +@export var priority: int = 0: + set = set_priority, + get = get_priority + + +## Determines the positional logic for a given [param PhantomCamera2D]. +## The different modes have different functionalities and purposes, so +## choosing the correct one depends on what each [param PhantomCamera2D] +## is meant to do. +@export var follow_mode: FollowMode = FollowMode.NONE: + set(value): + follow_mode = value + + if follow_mode == FollowMode.NONE: + _should_follow = false + top_level = false + _is_parents_physics() + notify_property_list_changed() + return + + match follow_mode: + FollowMode.PATH: + if is_instance_valid(follow_path): + _should_follow_checker() + else: + _should_follow = false + FollowMode.GROUP: + _follow_targets_size_check() + _: + _should_follow_checker() + + if follow_mode == FollowMode.FRAMED: + if _follow_framed_initial_set and follow_target: + _follow_framed_initial_set = false + dead_zone_changed.connect(_on_dead_zone_changed) + else: + if dead_zone_changed.is_connected(_on_dead_zone_changed): + dead_zone_changed.disconnect(_on_dead_zone_changed) + + top_level = true + follow_mode_changed.emit() + notify_property_list_changed() + get: + return follow_mode + +## Determines which target should be followed. +## The [param Camera2D] will follow the position of the Follow Target +## based on the [member follow_mode] type and its parameters. +@export var follow_target: Node2D = null: + set = set_follow_target, + get = get_follow_target + +### Defines the targets that the [param PhantomCamera2D] should be following. +@export var follow_targets: Array[Node2D] = []: + set = set_follow_targets, + get = get_follow_targets + +## Determines the [Path2D] the [param PhantomCamera2D] +## should be bound to. +## The [param PhantomCamera2D] will follow the position of the +## [member follow_target] while sticking to the closest point on this path. +@export var follow_path: Path2D = null: + set = set_follow_path, + get = get_follow_path + + +## Applies a zoom level to the [param PhantomCamera2D], which effectively +## overrides the [param zoom] property of the [param Camera2D] node. +@export_custom(PROPERTY_HINT_LINK, "") var zoom: Vector2 = Vector2.ONE: + set = set_zoom, + get = get_zoom + + +## If enabled, will snap the [param Camera2D] to whole pixels as it moves. +## [br][br] +## This should be particularly useful in pixel art projects, +## where assets should always be aligned to the monitor's pixels to avoid +## unintended stretching. +@export var snap_to_pixel: bool = false: + set = set_snap_to_pixel, + get = get_snap_to_pixel + + +## Enables a preview of what the [PhantomCamera2D] will see in the +## scene. It works identically to how a [param Camera2D] shows which area +## will be visible during runtime. Likewise, this too will be affected by the +## [member zoom] property and the [param viewport_width] and +## [param Viewport Height] defined in the [param Project Settings]. +@export var frame_preview: bool = true: + set(value): + frame_preview = value + queue_redraw() + get: + return frame_preview + + +## Defines how the [param PhantomCamera2D] transition between one another. +## Changing the tween values for a given [param PhantomCamera2D] +## determines how transitioning to that instance will look like. +## This is a resource type that can be either used for one +## [param PhantomCamera] or reused across multiple - both 2D and 3D. +## By default, all [param PhantomCameras] will use a [param linear] +## transition, [param easeInOut] ease with a [param 1s] duration. +@export var tween_resource: PhantomCameraTween = PhantomCameraTween.new(): + set = set_tween_resource, + get = get_tween_resource + +## If enabled, the moment a [param PhantomCamera2D] is instantiated into +## a scene, and has the highest priority, it will perform its tween transition. +## This is most obvious if a [param PhantomCamera2D] has a long duration and +## is attached to a playable character that can be moved the moment a scene +## is loaded. Disabling the [param tween_on_load] property will +## disable this behaviour and skip the tweening entirely when instantiated. +@export var tween_on_load: bool = true: + set = set_tween_on_load, + get = get_tween_on_load + + +## Determines how often an inactive [param PhantomCamera2D] should update +## its positional and rotational values. This is meant to reduce the amount +## of calculations inactive [param PhantomCamera2Ds] are doing when idling +## to improve performance. +@export var inactive_update_mode: InactiveUpdateMode = InactiveUpdateMode.ALWAYS + + +## Determines which layers this [param PhantomCamera2D] should be able to communicate with [PhantomCameraHost] nodes.[br] +## A corresponding layer needs to be set on the [PhantomCameraHost] node. +@export_flags_2d_render var host_layers: int = 1: + set = set_host_layers, + get = get_host_layers + + +@export_group("Follow Parameters") +## Offsets the [member follow_target] position. +@export var follow_offset: Vector2 = Vector2.ZERO: + set = set_follow_offset, + get = get_follow_offset + +## Applies a damping effect on the [param Camera2D]'s movement. +## Leading to heavier / slower camera movement as the targeted node moves around. +## This is useful to avoid sharp and rapid camera movement. +@export var follow_damping: bool = false: + set = set_follow_damping, + get = get_follow_damping + +## Defines the damping amount. The ideal range should be somewhere between 0-1.[br][br] +## The damping amount can be specified in the individual axis.[br][br] +## [b]Lower value[/b] = faster / sharper camera movement.[br] +## [b]Higher value[/b] = slower / heavier camera movement. +@export_custom(PROPERTY_HINT_LINK, "") +var follow_damping_value: Vector2 = Vector2(0.1, 0.1): + set = set_follow_damping_value, + get = get_follow_damping_value + +## Prevents the [param PhantomCamera2D] from moving in a designated axis. +## This can be enabled or disabled at runtime or from the editor directly. +@export var follow_axis_lock: FollowLockAxis = FollowLockAxis.NONE: + set = set_lock_axis, + get = get_lock_axis +var _follow_axis_is_locked: bool = false +var _follow_axis_lock_value: Vector2 = Vector2.ZERO + +## Makes the [param PhantomCamera2D] copy the rotation of its [member follow_target][br] +## This behavior is only available when [member follow_mode] is set and only has one [member follow_target].[br][br] +## [b]Important:[/b] Be sure to disable [member Camera2D.ignore_rotation] on the [Camera2D] node to enable this feature. +@export var rotate_with_target: bool = false: + set = set_rotate_with_target, + get = get_rotate_with_target +var _should_rotate_with_target: bool = false + +## Offsets the rotation when [member rotate_with_target] is enabled. +@export_range(-360, 360, 0.001, "radians_as_degrees") var rotation_offset: float = 0: + set = set_rotation_offset, + get = get_rotation_offset + +## Enables rotational damping when [member rotate_with_target] is enabled. +@export var rotation_damping: bool = false: + set = set_rotation_damping, + get = get_rotation_damping + +## Defines the damping amount for the [member rotate_with_target]. +@export_range(0, 1) var rotation_damping_value: float = 0.1: + set = set_rotation_damping_value, + get = get_rotation_damping_value + + +@export_subgroup("Follow Group") +## Enables the [param PhantomCamera2D] to dynamically zoom in and out based on +## the targets' distances between each other. +## Once enabled, the [param Camera2D] will stay as zoomed in as possible, +## limited by the [member auto_zoom_max] and start zooming out as the targets +## move further apart, limited by the [member auto_zoom_min]. +## Note: Enabling this property hides and disables the [member zoom] property +## as this effectively overrides that value. +@export var auto_zoom: bool = false: + set = set_auto_zoom, + get = get_auto_zoom + +## Sets the param minimum zoom amount, in other words how far away the +## [param Camera2D] can be from scene.[br][br] +## This only works when [member auto_zoom] is enabled. +@export var auto_zoom_min: float = 1: + set = set_auto_zoom_min, + get = get_auto_zoom_min + +## Sets the maximum zoom amount, in other words how close the [param Camera2D] +## can move towards the scene.[br][br] +## This only works when [member auto_zoom] is enabled. +@export var auto_zoom_max: float = 5: + set = set_auto_zoom_max, + get = get_auto_zoom_max + +## Determines how close to the edges the targets are allowed to be. +## This is useful to avoid targets being cut off at the edges of the screen. +## [br][br] +## The Vector4 parameter order goes: [param Left] - [param Top] - [param Right] +## - [param Bottom]. +@export var auto_zoom_margin: Vector4 = Vector4.ZERO: + set = set_auto_zoom_margin, + get = get_auto_zoom_margin + + +@export_subgroup("Dead Zones") +## Defines the horizontal dead zone area. While the target is within it, the +## [param PhantomCamera2D] will not move in the horizontal axis. +## If the targeted node leaves the horizontal bounds, the +## [param PhantomCamera2D] will follow the target horizontally to keep +## it within bounds. +@export_range(0, 1) var dead_zone_width: float = 0: + set(value): + dead_zone_width = value + dead_zone_changed.emit() + get: + return dead_zone_width + +## Defines the vertical dead zone area. While the target is within it, the +## [param PhantomCamera2D] will not move in the vertical axis. +## If the targeted node leaves the vertical bounds, the +## [param PhantomCamera2D] will follow the target horizontally to keep +## it within bounds. +@export_range(0, 1) var dead_zone_height: float = 0: + set(value): + dead_zone_height = value + dead_zone_changed.emit() + get: + return dead_zone_height + +## Enables the [param dead zones] to be visible when running the game from the editor. +## [br] +## [param dead zones] will never be visible in build exports. +@export var show_viewfinder_in_play: bool = false + + +@export_group("Limit") + +## Shows the [param Camera2D]'s built-in limit border.[br] +## The [param PhantomCamera2D] and [param Camera2D] can move around anywhere within it. +@export var draw_limits: bool = false: + set(value): + _draw_limits = value + if Engine.is_editor_hint(): + _draw_camera_2d_limit() + get: + return _draw_limits + +## Defines the left side of the [param Camera2D] limit. +## The camera will not be able to move past this point. +@export var limit_left: int = -10000000: + set = set_limit_left, + get = get_limit_left +## Defines the top side of the [param Camera2D] limit. +## The camera will not be able to move past this point. +@export var limit_top: int = -10000000: + set = set_limit_top, + get = get_limit_top +## Defines the right side of the [param Camera2D] limit. +## The camera will not be able to move past this point. +@export var limit_right: int = 10000000: + set = set_limit_right, + get = get_limit_right +## Defines the bottom side of the [param Camera2D] limit. +## The camera will not be able to move past this point. +@export var limit_bottom: int = 10000000: + set = set_limit_bottom, + get = get_limit_bottom + +## Allows for setting either a [TileMap], [TileMapLayer] or [CollisionShape2D] node to +## automatically apply a limit size instead of manually adjusting the Left, +## Top, Right and Left properties.[br][br] +## [b]TileMap / TileMapLayer[/b][br] +## The Limit will update after the [TileSet] of the [TileMap] / [TileMapLayer] has changed.[br] +## [b]Note:[/b] The limit size will only update after closing the TileMap editor +## bottom panel. +## [br][br] +## [b]CollisionShape2D[/b][br] +## The limit will update in realtime as the Shape2D changes its size. +## Note: For performance reasons, resizing the [Shape2D] during runtime will not change the Limits sides. +@export_node_path("TileMap", "Node2D", "CollisionShape2D") var limit_target: NodePath = NodePath(""): + set = set_limit_target, + get = get_limit_target + +## Applies an offset to the [TileMap]/[TileMapLayer] Limit or [Shape2D] Limit. +## The values goes from [param Left], [param Top], [param Right] +## and [param Bottom]. +@export var limit_margin: Vector4i = Vector4.ZERO: + set = set_limit_margin, + get = get_limit_margin +#@export var limit_smoothed: bool = false: # TODO - Needs proper support + #set = set_limit_smoothing, + #get = get_limit_smoothing + +@export_group("Noise") +## Applies a noise, or shake, to a [Camera2D].[br] +## Once set, the noise will run continuously after the tween to the [PhantomCamera2D] is complete. +@export var noise: PhantomCameraNoise2D = null: + set = set_noise, + get = get_noise + +## If true, will trigger the noise while in the editor.[br] +## Useful in cases where you want to temporarily disable the noise in the editor without removing +## the resource.[br][br] +## [b]Note:[/b] This property has no effect on runtime behaviour. +@export var _preview_noise: bool = true: + set(value): + _preview_noise = value + if not value: + _transform_noise = Transform2D() + +## Enable a corresponding layer for a [member PhantomCameraNoiseEmitter2D.noise_emitter_layer] +## to make this [PhantomCamera2D] be affect by it. +@export_flags_2d_render var noise_emitter_layer: int = 0: + set = set_noise_emitter_layer, + get = get_noise_emitter_layer + +#region Private Variables + +var _is_active: bool = false + +var _should_follow: bool = false +var _follow_framed_offset: Vector2 = Vector2.ZERO +var _follow_target_physics_based: bool = false +var _physics_interpolation_enabled: bool = false # NOTE - Enable for Godot 4.3 and when PhysicsInterpolationMode bug is resolved + +var _has_multiple_follow_targets: bool = false +var _follow_targets_single_target_index: int = 0 +var _follow_targets: Array[Node2D] = [] + +var _follow_velocity_ref: Vector2 = Vector2.ZERO # Stores and applies the velocity of the follow movement +var _rotation_velocity_ref: float = 0 # Stores and applies the velocity of the rotation movement + +var _has_follow_path: bool = false + +var _tween_skip: bool = false + +## Defines the position of the [member follow_target] within the viewport.[br] +## This is only used for when [member follow_mode] is set to [param Framed]. +var _follow_framed_initial_set: bool = false + +static var _draw_limits: bool = false + +var _limit_sides: Vector4i = _limit_sides_default +var _limit_sides_default: Vector4i = Vector4i(-10000000, -10000000, 10000000, 10000000) + +var _limit_node: Node2D = null +var _tile_size_perspective_scaler: Vector2 = Vector2.ONE + +var _limit_inactive_pcam: bool = false + +var _follow_target_position: Vector2 = Vector2.ZERO + +var _transform_output: Transform2D = Transform2D() +var _transform_noise: Transform2D = Transform2D() + +var _has_noise_resource: bool = false + +# NOTE - Temp solution until Godot has better plugin autoload recognition out-of-the-box. +var _phantom_camera_manager: Node = null + +#endregion + +#region Public Variables + +var tween_duration: float: + set = set_tween_duration, + get = get_tween_duration +var tween_transition: PhantomCameraTween.TransitionType: + set = set_tween_transition, + get = get_tween_transition +var tween_ease: PhantomCameraTween.EaseType: + set = set_tween_ease, + get = get_tween_ease + +var viewport_position: Vector2 + +#endregion + +#region Private Functions + +func _validate_property(property: Dictionary) -> void: + ################ + ## Follow Target + ################ + if property.name == "follow_target": + if follow_mode == FollowMode.NONE or \ + follow_mode == FollowMode.GROUP: + property.usage = PROPERTY_USAGE_NO_EDITOR + + elif property.name == "follow_path" and \ + follow_mode != FollowMode.PATH: + property.usage = PROPERTY_USAGE_NO_EDITOR + + + #################### + ## Follow Parameters + #################### + if follow_mode == FollowMode.NONE: + match property.name: + "follow_offset", \ + "follow_damping", \ + "follow_damping_value", \ + "follow_axis_lock", \ + "rotate_with_target": + property.usage = PROPERTY_USAGE_NO_EDITOR + + if property.name == "follow_offset": + if follow_mode == FollowMode.PATH or \ + follow_mode == FollowMode.GLUED: + property.usage = PROPERTY_USAGE_NO_EDITOR + + + ############### + ## Follow Group + ############### + if follow_mode != FollowMode.GROUP: + match property.name: + "follow_targets", \ + "auto_zoom": + property.usage = PROPERTY_USAGE_NO_EDITOR + + if not auto_zoom or follow_mode != FollowMode.GROUP: + match property.name: + "auto_zoom_min", \ + "auto_zoom_max", \ + "auto_zoom_margin": + property.usage = PROPERTY_USAGE_NO_EDITOR + + ################ + ## Follow Framed + ################ + if not follow_mode == FollowMode.FRAMED: + match property.name: + "dead_zone_width", \ + "dead_zone_height", \ + "show_viewfinder_in_play": + property.usage = PROPERTY_USAGE_NO_EDITOR + + + ##################### + ## Rotate With Target + ##################### + if property.name == "rotate_with_target" and follow_mode == FollowMode.GROUP: + property.usage = PROPERTY_USAGE_NO_EDITOR + + + if not rotate_with_target or follow_mode == FollowMode.GROUP: + match property.name: + "rotation_damping", \ + "rotation_offset", \ + "rotation_damping_value": + property.usage = PROPERTY_USAGE_NO_EDITOR + + if property.name == "rotation_damping_value": + if not rotation_damping: + property.usage = PROPERTY_USAGE_NO_EDITOR + + + ####### + ## Zoom + ####### + if property.name == "zoom" and follow_mode == FollowMode.GROUP and auto_zoom: + property.usage = PROPERTY_USAGE_NO_EDITOR + + ######## + ## Limit + ######## + if is_instance_valid(_limit_node): + match property.name: + "limit_left", \ + "limit_top", \ + "limit_right", \ + "limit_bottom": + property.usage = PROPERTY_USAGE_NO_EDITOR + + if property.name == "limit_margin" and not _limit_node: + property.usage = PROPERTY_USAGE_NO_EDITOR + + +func _enter_tree() -> void: + _phantom_camera_manager = Engine.get_singleton(_constants.PCAM_MANAGER_NODE_NAME) + _tween_skip = !tween_on_load + + _phantom_camera_manager.pcam_added(self) + + priority_override = false + + match follow_mode: + FollowMode.NONE: + _is_parents_physics() + FollowMode.PATH: + if is_instance_valid(follow_path): + _should_follow_checker() + else: + _should_follow = false + FollowMode.GROUP: + _follow_targets_size_check() + _should_follow_checker() + _: + _should_follow_checker() + + if not visibility_changed.is_connected(_check_visibility): + visibility_changed.connect(_check_visibility) + + update_limit_all_sides() + + + +func _exit_tree() -> void: + if not follow_mode == FollowMode.GROUP: + follow_targets = [] + + if not is_instance_valid(_phantom_camera_manager): return + _phantom_camera_manager.pcam_removed(self) + + +func _ready() -> void: + if is_instance_valid(follow_target): + _transform_output.origin = _get_target_position_offset() + else: + _transform_output = global_transform + + _phantom_camera_manager.noise_2d_emitted.connect(_noise_emitted) + + if not Engine.is_editor_hint(): + _preview_noise = true + + if follow_mode == FollowMode.GROUP: + _follow_targets_size_check() + + +func _process(delta: float) -> void: + if _follow_target_physics_based or _is_active: return + process_logic(delta) + + +func _physics_process(delta: float) -> void: + if not _follow_target_physics_based or _is_active: return + process_logic(delta) + + +func process_logic(delta: float) -> void: + if _is_active: + if _has_noise_resource and _preview_noise: + _transform_noise = noise.get_noise_transform(delta) + else: + match inactive_update_mode: + InactiveUpdateMode.NEVER: return + InactiveUpdateMode.ALWAYS: + # Only triggers if limit isn't default + if _limit_inactive_pcam: + global_position = _set_limit_clamp_position(global_position) + # InactiveUpdateMode.EXPONENTIALLY: + # TODO - Trigger positional updates less frequently as more PCams gets added + + _limit_checker() + + if _should_follow: + _follow(delta) + else: + _transform_output = global_transform + + if _follow_axis_is_locked: + match follow_axis_lock: + FollowLockAxis.X: + _transform_output.origin.x = _follow_axis_lock_value.x + FollowLockAxis.Y: + _transform_output.origin.y = _follow_axis_lock_value.y + FollowLockAxis.XY: + _transform_output.origin.x = _follow_axis_lock_value.x + _transform_output.origin.y = _follow_axis_lock_value.y + + +func _limit_checker() -> void: + ## TODO - Needs to see if this can be triggerd only from CollisionShape2D Transform changes + if not Engine.is_editor_hint(): return + if draw_limits: + update_limit_all_sides() + + +func _follow(delta: float) -> void: + _set_follow_position() + _interpolate_position(_follow_target_position, delta) + + +func _set_follow_position() -> void: + match follow_mode: + FollowMode.GLUED: + _follow_target_position = follow_target.global_position + + FollowMode.SIMPLE: + _follow_target_position = _get_target_position_offset() + + FollowMode.GROUP: + if _has_multiple_follow_targets: + var rect: Rect2 = Rect2(_follow_targets[0].global_position, Vector2.ZERO) + for target in _follow_targets: + rect = rect.expand(target.global_position) + if auto_zoom: + rect = rect.grow_individual( + auto_zoom_margin.x, + auto_zoom_margin.y, + auto_zoom_margin.z, + auto_zoom_margin.w + ) + + if rect.size.x > rect.size.y * _phantom_camera_manager.screen_size.aspect(): + zoom = clamp(_phantom_camera_manager.screen_size.x / rect.size.x, auto_zoom_min, auto_zoom_max) * Vector2.ONE + else: + zoom = clamp(_phantom_camera_manager.screen_size.y / rect.size.y, auto_zoom_min, auto_zoom_max) * Vector2.ONE + _follow_target_position = rect.get_center() + follow_offset + else: + _follow_target_position = follow_targets[_follow_targets_single_target_index].global_position + follow_offset + + FollowMode.PATH: + var path_position: Vector2 = follow_path.global_position + + _follow_target_position = \ + follow_path.curve.get_closest_point( + _get_target_position_offset() - path_position + ) + path_position + + FollowMode.FRAMED: + if not Engine.is_editor_hint(): + if not _is_active: + _follow_target_position = _get_target_position_offset() + else: + viewport_position = (get_follow_target().get_global_transform_with_canvas().get_origin() + follow_offset) / get_viewport_rect().size + var framed_side_offset: Vector2 = _get_framed_side_offset() + + if framed_side_offset != Vector2.ZERO: + var glo_pos: Vector2 + var target_position: Vector2 = _get_target_position_offset() + _follow_framed_offset + + if dead_zone_width == 0 || dead_zone_height == 0: + if dead_zone_width == 0 && dead_zone_height != 0: + _follow_target_position = _get_target_position_offset() + elif dead_zone_width != 0 && dead_zone_height == 0: + glo_pos = _get_target_position_offset() + glo_pos.x += target_position.x - global_position.x + _follow_target_position = glo_pos + else: + _follow_target_position = _get_target_position_offset() + + # If a horizontal dead zone is reached + if framed_side_offset.x != 0 and framed_side_offset.y == 0: + _follow_target_position.y = _transform_output.origin.y + _follow_target_position.x = target_position.x + _follow_framed_offset.y = global_position.y - _get_target_position_offset().y + dead_zone_reached.emit(Vector2(framed_side_offset.x, 0)) + # If a vertical dead zone is reached + elif framed_side_offset.x == 0 and framed_side_offset.y != 0: + _follow_target_position.x = _transform_output.origin.x + _follow_target_position.y = target_position.y + _follow_framed_offset.x = global_position.x - _get_target_position_offset().x + dead_zone_reached.emit(Vector2(0, framed_side_offset.y)) + # If a deadzone corner is reached + else: + _follow_target_position = target_position + dead_zone_reached.emit(Vector2(framed_side_offset.x, framed_side_offset.y)) + else: + _follow_framed_offset = _transform_output.origin - _get_target_position_offset() + return + else: + _follow_target_position = _get_target_position_offset() + + +func _set_follow_velocity(index: int, value: float): + _follow_velocity_ref[index] = value + +func _set_rotation_velocity(index: int, value: float): + _rotation_velocity_ref = value + +func _interpolate_position(target_position: Vector2, delta: float) -> void: + var output_rotation: float = global_transform.get_rotation() + if rotate_with_target: + if rotation_damping and not Engine.is_editor_hint(): + output_rotation = _smooth_damp( + follow_target.get_rotation() + rotation_offset, + _transform_output.get_rotation(), + 0, + _rotation_velocity_ref, + _set_rotation_velocity, + rotation_damping_value, + delta + ) + else: + output_rotation = follow_target.get_rotation() + rotation_offset + + if _limit_inactive_pcam and not _tween_skip: + target_position = _set_limit_clamp_position(target_position) + + global_position = target_position + + if follow_damping and not Engine.is_editor_hint(): + var output_position: Vector2 + for i in 2: + output_position[i] = _smooth_damp( + global_position[i], + _transform_output.origin[i], + i, + _follow_velocity_ref[i], + _set_follow_velocity, + follow_damping_value[i], + delta + ) + _transform_output = Transform2D(output_rotation, output_position) + else: + _transform_output = Transform2D(output_rotation, target_position) + + +func _smooth_damp(target_axis: float, self_axis: float, index: int, current_velocity: float, set_velocity: Callable, damping_time: float, delta: float) -> float: + damping_time = maxf(0.0001, damping_time) + var omega: float = 2 / damping_time + var x: float = omega * delta + var exponential: float = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x) + var diff: float = self_axis - target_axis + var _target_axis: float = target_axis + + var max_change: float = INF * damping_time + diff = clampf(diff, -max_change, max_change) + target_axis = self_axis - diff + + var temp: float = (current_velocity + omega * diff) * delta + set_velocity.call(index, (current_velocity - omega * temp) * exponential) + var output: float = target_axis + (diff + temp) * exponential + + ## To prevent overshooting + if (_target_axis - self_axis > 0.0) == (output > _target_axis): + output = _target_axis + set_velocity.call(index, (output - _target_axis) / delta) + + return output + + +func _set_limit_clamp_position(value: Vector2) -> Vector2: + var camera_frame_rect_size: Vector2 = _camera_frame_rect().size + value.x = clampf(value.x, _limit_sides.x + camera_frame_rect_size.x / 2, _limit_sides.z - camera_frame_rect_size.x / 2) + value.y = clampf(value.y, _limit_sides.y + camera_frame_rect_size.y / 2, _limit_sides.w - camera_frame_rect_size.y / 2) + return value + + +func _draw() -> void: + if not Engine.is_editor_hint(): return + + if frame_preview and not _is_active: + draw_rect(_camera_frame_rect(), Color("3ab99a"), false, 2) + + +func _camera_frame_rect() -> Rect2: + var screen_size_zoom: Vector2 = Vector2(_phantom_camera_manager.screen_size.x / get_zoom().x, _phantom_camera_manager.screen_size.y / get_zoom().y) + + return Rect2(-screen_size_zoom / 2, screen_size_zoom) + + +func _on_tile_map_changed() -> void: + update_limit_all_sides() + + +func _get_target_position_offset() -> Vector2: + return follow_target.global_position + follow_offset + + +func _on_dead_zone_changed() -> void: + global_position = _get_target_position_offset() + + +func _get_framed_side_offset() -> Vector2: + var frame_out_bounds: Vector2 + + if viewport_position.x < 0.5 - dead_zone_width / 2: + # Is outside left edge + frame_out_bounds.x = -1 + + if viewport_position.y < 0.5 - dead_zone_height / 2: + # Is outside top edge + frame_out_bounds.y = 1 + + if viewport_position.x > 0.5 + dead_zone_width / 2: + # Is outside right edge + frame_out_bounds.x = 1 + + if viewport_position.y > 0.5001 + dead_zone_height / 2: # 0.501 to resolve an issue where the bottom vertical Dead Zone never becoming 0 when the Dead Zone Vertical parameter is set to 0 + # Is outside bottom edge + frame_out_bounds.y = -1 + + return frame_out_bounds + + +func _draw_camera_2d_limit() -> void: + if not is_instance_valid(_phantom_camera_manager): return + _phantom_camera_manager.draw_limit_2d.emit(draw_limits) + + +func _check_limit_is_not_default() -> void: + if _limit_sides == _limit_sides_default: + _limit_inactive_pcam = false + else: + _limit_inactive_pcam = true + + +func _check_visibility() -> void: + _phantom_camera_manager.pcam_visibility_changed.emit(self) + + +func _follow_target_tree_exiting(target: Node) -> void: + if target == follow_target: + _should_follow = false + if _follow_targets.has(target): + _follow_targets.erase(target) + + +func _should_follow_checker() -> void: + if follow_mode == FollowMode.NONE: + _should_follow = false + return + + if not follow_mode == FollowMode.GROUP: + if is_instance_valid(follow_target): + _should_follow = true + else: + _should_follow = false + + +func _follow_targets_size_check() -> void: + var targets_size: int = 0 + _follow_target_physics_based = false + _follow_targets = [] + for i in follow_targets.size(): + if follow_targets[i] == null: continue + if follow_targets[i].is_inside_tree(): + _follow_targets.append(follow_targets[i]) + targets_size += 1 + _follow_targets_single_target_index = i + _check_physics_body(follow_targets[i]) + if not follow_targets[i].tree_exiting.is_connected(_follow_target_tree_exiting): + follow_targets[i].tree_exiting.connect(_follow_target_tree_exiting.bind(follow_targets[i])) + + match targets_size: + 0: + _should_follow = false + _has_multiple_follow_targets = false + 1: + _should_follow = true + _has_multiple_follow_targets = false + _: + _should_follow = true + _has_multiple_follow_targets = true + + +func _noise_emitted(emitter_noise_output: Transform2D, emitter_layer: int) -> void: + if noise_emitter_layer & emitter_layer != 0: + noise_emitted.emit(emitter_noise_output) + + +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("Render layer must be between 1 and 20.") + else: + if value: + mask |= 1 << (layer_number - 1) + else: + mask &= ~(1 << (layer_number - 1)) + + return mask + + +func _check_physics_body(target: Node2D) -> void: + if target is PhysicsBody2D: + var show_jitter_tips := ProjectSettings.get_setting("phantom_camera/tips/show_jitter_tips") + var physics_interpolation_enabled := ProjectSettings.get_setting("physics/common/physics_interpolation") + + ## NOTE - Feature Toggle + if Engine.get_version_info().major == 4 and \ + Engine.get_version_info().minor < 3: + if show_jitter_tips == null: # Default value is null when referencing custom Project Setting + print_rich("Following a [b]PhysicsBody2D[/b] node will likely result in jitter - on lower physics ticks in particular.") + print_rich("If possible, will recommend upgrading to Godot 4.3, as it has built-in support for 2D Physics Interpolation, which will mitigate this issue.") + print_rich("Otherwise, try following the guide on the [url=https://phantom-camera.dev/support/faq#i-m-seeing-jitter-what-can-i-do]documentation site[/url] for better results.") + print_rich("This tip can be disabled from within [code]Project Settings / Phantom Camera / Tips / Show Jitter Tips[/code]") + return + ## NOTE - Only supported in Godot 4.3 or above + elif not physics_interpolation_enabled and show_jitter_tips == null: # Default value is null when referencing custom Project Setting + printerr("Physics Interpolation is disabled in the Project Settings, recommend enabling it to smooth out physics-based camera movement") + print_rich("This tip can be disabled from within [code]Project Settings / Phantom Camera / Tips / Show Jitter Tips[/code]") + _follow_target_physics_based = true + else: + _is_parents_physics(target) + physics_target_changed.emit() + + +func _is_parents_physics(target: Node = self) -> void: + var current_node: Node = target + while current_node: + current_node = current_node.get_parent() + if not current_node is PhysicsBody2D: continue + _follow_target_physics_based = true + +#endregion + + +#region Public Functions + +## Updates the limit sides based what has been set to define it +## This should be automatic, but can be called manully if need be. +func update_limit_all_sides() -> void: + var limit_rect: Rect2 + + if not is_instance_valid(_limit_node): + _limit_sides.x = limit_left + _limit_sides.y = limit_top + _limit_sides.z = limit_right + _limit_sides.w = limit_bottom + elif _limit_node is TileMap or _limit_node.is_class("TileMapLayer"): + var tile_map := _limit_node + + if not tile_map.tile_set: return # TODO: This should be removed once https://github.com/godotengine/godot/issues/96898 is resolved + + var tile_map_size: Vector2 = Vector2(tile_map.get_used_rect().size) * Vector2(tile_map.tile_set.tile_size) * tile_map.get_scale() + var tile_map_position: Vector2 = tile_map.global_position + Vector2(tile_map.get_used_rect().position) * Vector2(tile_map.tile_set.tile_size) * tile_map.get_scale() + + ## Calculates the Rect2 based on the Tile Map position and size + margin + limit_rect = Rect2( + tile_map_position + Vector2(limit_margin.x, limit_margin.y), + tile_map_size - Vector2(limit_margin.x, limit_margin.y) - Vector2(limit_margin.z, limit_margin.w) + ) + + # Left + _limit_sides.x = roundi(limit_rect.position.x) + # Top + _limit_sides.y = roundi(limit_rect.position.y) + # Right + _limit_sides.z = roundi(limit_rect.position.x + limit_rect.size.x) + # Bottom + _limit_sides.w = roundi(limit_rect.position.y + limit_rect.size.y) + elif _limit_node is CollisionShape2D: + var collision_shape_2d: CollisionShape2D = _limit_node as CollisionShape2D + + if not collision_shape_2d.get_shape(): return + + var shape_2d: Shape2D = collision_shape_2d.get_shape() + var shape_2d_size: Vector2 = shape_2d.get_rect().size + var shape_2d_position: Vector2 = collision_shape_2d.global_position + Vector2(shape_2d.get_rect().position) + + ## Calculates the Rect2 based on the Tile Map position and size + limit_rect = Rect2(shape_2d_position, shape_2d_size) + + ## Calculates the Rect2 based on the Tile Map position and size + margin + limit_rect = Rect2( + limit_rect.position + Vector2(limit_margin.x, limit_margin.y), + limit_rect.size - Vector2(limit_margin.x, limit_margin.y) - Vector2(limit_margin.z, limit_margin.w) + ) + + # Left + _limit_sides.x = roundi(limit_rect.position.x) + # Top + _limit_sides.y = roundi(limit_rect.position.y) + # Right + _limit_sides.z = roundi(limit_rect.position.x + limit_rect.size.x) + # Bottom + _limit_sides.w = roundi(limit_rect.position.y + limit_rect.size.y) + + _check_limit_is_not_default() + if not _is_active: return + if not is_instance_valid(_phantom_camera_manager): return + _phantom_camera_manager.limit_2d_changed.emit(SIDE_LEFT, _limit_sides.x) + _phantom_camera_manager.limit_2d_changed.emit(SIDE_TOP, _limit_sides.y) + _phantom_camera_manager.limit_2d_changed.emit(SIDE_RIGHT, _limit_sides.z) + _phantom_camera_manager.limit_2d_changed.emit(SIDE_BOTTOM, _limit_sides.w) + _phantom_camera_manager.draw_limit_2d.emit(draw_limits) + + +func reset_limit() -> void: + if not is_instance_valid(_phantom_camera_manager): return + _phantom_camera_manager.limit_2d_changed.emit(SIDE_LEFT, _limit_sides_default.x) + _phantom_camera_manager.limit_2d_changed.emit(SIDE_TOP, _limit_sides_default.y) + _phantom_camera_manager.limit_2d_changed.emit(SIDE_RIGHT, _limit_sides_default.z) + _phantom_camera_manager.limit_2d_changed.emit(SIDE_BOTTOM, _limit_sides_default.w) + _phantom_camera_manager.draw_limit_2d.emit(draw_limits) + + +## Assigns the value of the [param has_tweened] property. +## [b][color=yellow]Important:[/color][/b] This value can only be changed +## from the [PhantomCameraHost] script. +func set_tween_skip(caller: Node, value: bool) -> void: + if is_instance_of(caller, PhantomCameraHost): + _tween_skip = value + else: + printerr("Can only be called PhantomCameraHost class") +## Returns the current [param has_tweened] value. +func get_tween_skip() -> bool: + return _tween_skip + +## Returns the [Transform3D] value based on the [member follow_mode] / [member look_at_mode] target value. +func get_transform_output() -> Transform2D: + return _transform_output + + +## Returns the noise [Transform3D] value. +func get_noise_transform() -> Transform2D: + return _transform_noise + + +## Emits a noise based on a custom [Transform2D] value.[br] +## Use this function if you wish to make use of external noise patterns from, for example, other addons. +func emit_noise(value: Transform2D) -> void: + noise_emitted.emit(value) + + +## Teleports the [param PhantomCamera2D] and [Camera2D] to their designated position, +## bypassing the damping process. +func teleport_position() -> void: + _follow_velocity_ref = Vector2.ZERO + _set_follow_position() + _transform_output.origin = _follow_target_position + _phantom_camera_manager.pcam_teleport.emit(self) + + +# TODO: Enum link does link to anywhere is being tracked in: https://github.com/godotengine/godot/issues/106828 +## Returns true if this [param PhantomCamera2D]'s [member follow_mode] is not set to [enum FollowMode] +## and has a valid [member follow_target]. +func is_following() -> bool: + return _should_follow + +#endregion + + +#region Setter & Getter Functions + +## Assigns new [member zoom] value. +func set_zoom(value: Vector2) -> void: + zoom = value + queue_redraw() + +## Gets current [member zoom] value. +func get_zoom() -> Vector2: + return zoom + + +## Assigns new [member priority] value. +func set_priority(value: int) -> void: + priority = maxi(0, value) + if not is_node_ready(): return + if not Engine.has_singleton(_constants.PCAM_MANAGER_NODE_NAME): return + Engine.get_singleton(_constants.PCAM_MANAGER_NODE_NAME).pcam_priority_changed.emit(self) + +## Gets current [member priority] value. +func get_priority() -> int: + return priority + + +## Assigns a new PhantomCameraTween resource to the PhantomCamera2D +func set_tween_resource(value: PhantomCameraTween) -> void: + tween_resource = value + +## Gets the PhantomCameraTween resource assigned to the PhantomCamera2D +## Returns null if there's nothing assigned to it. +func get_tween_resource() -> PhantomCameraTween: + return tween_resource + + +## Assigns a new [param Tween Duration] to the [member tween_resource] value.[br] +## The duration value is in seconds. +func set_tween_duration(value: float) -> void: + tween_resource.duration = value + +## Gets the current [param Tween Duration] value inside the +## [member tween_resource].[br] +## The duration value is in seconds. +func get_tween_duration() -> float: + return tween_resource.duration + + +## Assigns a new [param Tween Transition] value inside the +## [member tween_resource]. +func set_tween_transition(value: int) -> void: + tween_resource.transition = value + +## Gets the current [param Tween Transition] value inside the +## [member tween_resource]. +func get_tween_transition() -> int: + return tween_resource.transition + + +## Assigns a new [param Tween Ease] value inside the [member tween_resource]. +func set_tween_ease(value: int) -> void: + tween_resource.ease = value + +## Gets the current [param Tween Ease] value inside the [member tween_resource]. +func get_tween_ease() -> int: + return tween_resource.ease + + +## Sets the [param PhantomCamera2D] active state.[br] +## [b][color=yellow]Important:[/color][/b] This value can only be changed +## from the [PhantomCameraHost] script. +func set_is_active(node, value) -> void: + if node is PhantomCameraHost: + _is_active = value + if value: + _should_follow_checker() + queue_redraw() + else: + printerr("PCams can only be set from the PhantomCameraHost") + +## Gets current active state of the [param PhantomCamera2D]. +## If it returns true, it means the [param PhantomCamera2D] is what the +## [param Camera2D] is currently following. +func is_active() -> bool: + return _is_active + + +## Enables or disables the [member tween_on_load]. +func set_tween_on_load(value: bool) -> void: + tween_on_load = value + +## Gets the current [member tween_on_load] value. +func get_tween_on_load() -> bool: + return tween_on_load + +## Sets the [member host_layers] value. +func set_host_layers(value: int) -> void: + host_layers = value + if is_instance_valid(_phantom_camera_manager): + _phantom_camera_manager.pcam_host_layer_changed.emit(self) + +## Enables or disables a given layer of [member host_layers]. +func set_host_layers_value(layer: int, value: bool) -> void: + host_layers = _set_layer(host_layers, layer, value) + +## Gets the current [member host_layers]. +func get_host_layers() -> int: + return host_layers + + +## Gets the current follow mode as an enum int based on [enum FollowMode].[br] +## [b]Note:[/b] Setting [enum FollowMode] purposely not added. +## A separate PCam should be used instead. +func get_follow_mode() -> int: + return follow_mode + + +## Assigns a new [Node2D] as the [member follow_target]. +func set_follow_target(value: Node2D) -> void: + if follow_mode == FollowMode.NONE or follow_mode == FollowMode.GROUP: return + if follow_target == value: return + follow_target = value + _follow_target_physics_based = false + if is_instance_valid(value): + if follow_mode == FollowMode.PATH: + if is_instance_valid(follow_path): + _should_follow = true + else: + _should_follow = false + else: + _should_follow = true + _check_physics_body(value) + if not follow_target.tree_exiting.is_connected(_follow_target_tree_exiting): + follow_target.tree_exiting.connect(_follow_target_tree_exiting.bind(follow_target)) + else: + _should_follow = false + follow_target_changed.emit() + notify_property_list_changed() + +## Erases the current [member follow_target]. +func erase_follow_target() -> void: + if follow_target == null: return + _should_follow = false + follow_target = null + _follow_target_physics_based = false + follow_target_changed.emit() + +## Gets the current [member follow_target]. +func get_follow_target() -> Node2D: + return follow_target + + +## Assigns a new [Path2D] to the [member follow_path]. +func set_follow_path(value: Path2D) -> void: + follow_path = value + if is_instance_valid(follow_path): + _should_follow_checker() + else: + _should_follow = false + +## Erases the current [Path2D] from the [member follow_path] property. +func erase_follow_path() -> void: + follow_path = null + +## Gets the current [Path2D] from the [member follow_path]. +func get_follow_path() -> Path2D: + return follow_path + + +## Assigns a new [param follow_targets] array value. +func set_follow_targets(value: Array[Node2D]) -> void: + if follow_mode != FollowMode.GROUP: return + if follow_targets == value: return + follow_targets = value + _follow_targets_size_check() + +## Appends a single [Node2D] to [member follow_targets]. +func append_follow_targets(value: Node2D) -> void: + if not is_instance_valid(value): + printerr(value, " is not a valid Node2D instance") + return + + if not follow_targets.has(value): + follow_targets.append(value) + _follow_targets_size_check() + else: + printerr(value, " is already part of Follow Group") + +## Adds an Array of type [Node2D] to [member follow_targets]. +func append_follow_targets_array(value: Array[Node2D]) -> void: + for target in value: + if not is_instance_valid(target): continue + if not follow_targets.has(target): + follow_targets.append(target) + _follow_targets_size_check() + else: + printerr(value, " is already part of Follow Group") + +## Removes a [Node2D] from [member follow_targets] array. +func erase_follow_targets(value: Node2D) -> void: + follow_targets.erase(value) + _follow_targets_size_check() + +## Gets all [Node2D] from [member follow_targets] array. +func get_follow_targets() -> Array[Node2D]: + return follow_targets + + +## Assigns a new Vector2 for the Follow Target Offset property. +func set_follow_offset(value: Vector2) -> void: + var temp_offset: Vector2 = follow_offset + + follow_offset = value + + if follow_axis_lock != FollowLockAxis.NONE: + temp_offset = temp_offset - value + match value: + FollowLockAxis.X: + _follow_axis_lock_value.x = _transform_output.origin.x + temp_offset.x + FollowLockAxis.Y: + _follow_axis_lock_value.y = _transform_output.origin.y + temp_offset.y + FollowLockAxis.XY: + _follow_axis_lock_value.x = _transform_output.origin.x + temp_offset.x + _follow_axis_lock_value.y = _transform_output.origin.y + temp_offset.y + + +## Gets the current Vector2 for the Follow Target Offset property. +func get_follow_offset() -> Vector2: + return follow_offset + + +## Enables or disables Follow Damping. +func set_follow_damping(value: bool) -> void: + follow_damping = value + notify_property_list_changed() + +## Gets the current Follow Damping property. +func get_follow_damping() -> bool: + return follow_damping + + +## Assigns new Damping value. +func set_follow_damping_value(value: Vector2) -> void: + ## TODO - Should be using @export_range once minimum version support is Godot 4.3 + if value.x < 0: value.x = 0 + elif value.y < 0: value.y = 0 + follow_damping_value = value + +## Gets the current Follow Damping value. +func get_follow_damping_value() -> Vector2: + return follow_damping_value + + +## Assigns a new [member follow_axis] member. Value is based on [enum FollowLockAxis] enum. +func set_lock_axis(value: FollowLockAxis) -> void: + follow_axis_lock = value + + # Wait for the node to be ready before setting lock + if not is_node_ready(): await ready + + # Prevent axis lock from working in the editor + if value != FollowLockAxis.NONE and not Engine.is_editor_hint(): + _follow_axis_is_locked = true + match value: + FollowLockAxis.X: + _follow_axis_lock_value.x = _transform_output.origin.x + FollowLockAxis.Y: + _follow_axis_lock_value.y = _transform_output.origin.y + FollowLockAxis.XY: + _follow_axis_lock_value.x = _transform_output.origin.x + _follow_axis_lock_value.y = _transform_output.origin.y + else: + _follow_axis_is_locked = false + +## Gets the current [member follow_axis_lock] value. Value is based on [enum FollowLockAxis] enum. +func get_lock_axis() -> FollowLockAxis: + return follow_axis_lock + + +## Enables or disables [member rotate_with_target]. +func set_rotate_with_target(value: bool) -> void: + rotate_with_target = value + notify_property_list_changed() + +## Gets the current [member rotate_with_target] value. +func get_rotate_with_target() -> bool: + return rotate_with_target + + +## Sets the [member rotation_offset]. +func set_rotation_offset(value: float) -> void: + rotation_offset = value + +## Gets the current [member rotation_offset] value. +func get_rotation_offset() -> float: + return rotation_offset + + +## Enables or disables [member rotation_damping]. +func set_rotation_damping(value: bool) -> void: + rotation_damping = value + notify_property_list_changed() + +## Gets the [member rotation_damping] value. +func get_rotation_damping() -> bool: + return rotation_damping + + +## Set the [member rotation_damping_value]. +func set_rotation_damping_value(value: float) -> void: + rotation_damping_value = value + +## Gets the [member rotation_damping_value] value. +func get_rotation_damping_value() -> float: + return rotation_damping_value + + +## Enables or disables [member snap_to_pixel]. +func set_snap_to_pixel(value: bool) -> void: + snap_to_pixel = value + +## Gets the current [member snap_to_pixel] value. +func get_snap_to_pixel() -> bool: + return snap_to_pixel + + +## Enables or disables Auto zoom when using Group Follow. +func set_auto_zoom(value: bool) -> void: + auto_zoom = value + notify_property_list_changed() + +## Gets Auto Zoom state. +func get_auto_zoom() -> bool: + return auto_zoom + + +## Assigns new Min Auto Zoom value. +func set_auto_zoom_min(value: float) -> void: + auto_zoom_min = value + +## Gets Min Auto Zoom value. +func get_auto_zoom_min() -> float: + return auto_zoom_min + + +## Assigns new Max Auto Zoom value. +func set_auto_zoom_max(value: float) -> void: + auto_zoom_max = value + +## Gets Max Auto Zoom value. +func get_auto_zoom_max() -> float: + return auto_zoom_max + + +## Assigns new Zoom Auto Margin value. +func set_auto_zoom_margin(value: Vector4) -> void: + auto_zoom_margin = value + +## Gets Zoom Auto Margin value. +func get_auto_zoom_margin() -> Vector4: + return auto_zoom_margin + + +## Sets a limit side based on the side parameter.[br] +## It's recommended to pass the [enum Side] enum as the sid parameter. +func set_limit(side: int, value: int) -> void: + match side: + SIDE_LEFT: limit_left = value + SIDE_TOP: limit_top = value + SIDE_RIGHT: limit_right = value + SIDE_BOTTOM: limit_bottom = value + _: printerr("Not a valid Side.") + +## Gets the limit side +func get_limit(value: int) -> int: + match value: + SIDE_LEFT: return limit_left + SIDE_TOP: return limit_top + SIDE_RIGHT: return limit_right + SIDE_BOTTOM: return limit_bottom + _: + printerr("Not a valid Side.") + return -1 + + +## Assign a the Camera2D Left Limit Side value. +func set_limit_left(value: int) -> void: + _limit_target_exist_error() + limit_left = value + update_limit_all_sides() + +## Gets the Camera2D Left Limit value. +func get_limit_left() -> int: + return limit_left + + +## Assign a the Camera2D Top Limit Side value. +func set_limit_top(value: int) -> void: + _limit_target_exist_error() + limit_top = value + update_limit_all_sides() + +## Gets the Camera2D Top Limit value. +func get_limit_top() -> int: + return limit_top + + +## Assign a the Camera2D Right Limit Side value. +func set_limit_right(value: int) -> void: + _limit_target_exist_error() + limit_right = value + update_limit_all_sides() + +## Gets the Camera2D Right Limit value. +func get_limit_right() -> int: + return limit_right + + +## Assign a the Camera2D Bottom Limit Side value. +func set_limit_bottom(value: int) -> void: + _limit_target_exist_error() + limit_bottom = value + update_limit_all_sides() + +## Gets the Camera2D Bottom Limit value. +func get_limit_bottom() -> int: + return limit_bottom + + +func _limit_target_exist_error() -> void: + if not limit_target.is_empty(): + printerr("Unable to set Limit Side due to Limit Target ", _limit_node.name, " being assigned") + + +# Sets a [memeber limit_target] node. +func set_limit_target(value: NodePath) -> void: + limit_target = value + + # Waits for PCam2d's _ready() before trying to validate limit_node_path + if not is_node_ready(): await ready + + # Removes signal from existing TileMap node + if is_instance_valid(get_node_or_null(value)): + var prev_limit_node: Node2D = _limit_node + var new_limit_node: Node2D = get_node(value) + + if prev_limit_node: + if prev_limit_node is TileMap or prev_limit_node.is_class("TileMapLayer"): + if prev_limit_node.changed.is_connected(_on_tile_map_changed): + prev_limit_node.changed.disconnect(_on_tile_map_changed) + + if new_limit_node is TileMap or new_limit_node.is_class("TileMapLayer"): + if not new_limit_node.changed.is_connected(_on_tile_map_changed): + new_limit_node.changed.connect(_on_tile_map_changed) + elif new_limit_node is CollisionShape2D: + var col_shape: CollisionShape2D = get_node(value) + + if col_shape.shape == null: + printerr("No Shape2D in: ", col_shape.name) + reset_limit() + limit_target = "" + return + else: + printerr("Limit Target is not a TileMap, TileMapLayer or CollisionShape2D node") + return + elif value == NodePath(""): + reset_limit() + limit_target = "" + else: + printerr("Limit Target cannot be found") + return + + _limit_node = get_node_or_null(value) + + notify_property_list_changed() + update_limit_all_sides() + +## Get [member limit_target] node. +func get_limit_target() -> NodePath: + if not limit_target: # TODO - Fixes an spam error if if limit_taret is empty + return NodePath("") + else: + return limit_target + + +## Set Tile Map Limit Margin. +func set_limit_margin(value: Vector4i) -> void: + limit_margin = value + update_limit_all_sides() +## Get Tile Map Limit Margin. +func get_limit_margin() -> Vector4i: + return limit_margin + + +### Enables or disables the Limit Smoothing beaviour. +#func set_limit_smoothing(value: bool) -> void: + #limit_smoothed = value +### Returns the Limit Smoothing beaviour. +#func get_limit_smoothing() -> bool: + #return limit_smoothed + + +## Sets a [PhantomCameraNoise2D] resource. +func set_noise(value: PhantomCameraNoise2D) -> void: + noise = value + if value != null: + _has_noise_resource = true + noise.set_trauma(1) + else: + _has_noise_resource = false + _transform_noise = Transform2D() + +## Returns the [PhantomCameraNoise2D] resource. +func get_noise() -> PhantomCameraNoise2D: + return noise + +func has_noise_resource() -> bool: + return _has_noise_resource + + +## 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 [member noise_emitter_layer]. +func set_noise_emitter_layer_value(value: int, enabled: bool) -> void: + noise_emitter_layer = _set_layer(noise_emitter_layer, value, enabled) + +## Returns the [member noise_emitter_layer] +func get_noise_emitter_layer() -> int: + return noise_emitter_layer + + +## Sets [member inactive_update_mode] property. +func set_inactive_update_mode(value: int) -> void: + inactive_update_mode = value + +## Gets [enum InactiveUpdateMode] value. +func get_inactive_update_mode() -> int: + return inactive_update_mode + + +func get_follow_target_physics_based() -> bool: + return _follow_target_physics_based + + +func get_class() -> String: + return "PhantomCamera2D" + + +func is_class(value) -> bool: + return value == "PhantomCamera2D" + +#endregion diff --git a/addons/phantom_camera/scripts/phantom_camera/phantom_camera_2d.gd.uid b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_2d.gd.uid new file mode 100644 index 0000000..66fa7de --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_2d.gd.uid @@ -0,0 +1 @@ +uid://bhexx6mj1xv3q diff --git a/addons/phantom_camera/scripts/phantom_camera/phantom_camera_3d.gd b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_3d.gd new file mode 100644 index 0000000..2b4480c --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_3d.gd @@ -0,0 +1,2279 @@ +@tool +@icon("res://addons/phantom_camera/icons/phantom_camera_3d.svg") +class_name PhantomCamera3D +extends Node3D + +## Controls a scene's [Camera3D] and applies logic to it. +## +## The scene's [Camera3D] will follow the position of the +## [param PhantomCamera3D] with the highest priority. +## Each instance can have different positional and rotational logic applied +## to them. + +#region Constants + +const _constants = preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd") + +#endregion + +#region Signals + +## Emitted when the [param PhantomCamera3D] becomes active. +signal became_active + +## Emitted when the [param PhantomCamera3D] becomes inactive. +signal became_inactive + +## Emitted when the follow_mode changes. +## Note: This is for internal use only +signal follow_mode_changed + +## Emitted when [member follow_target] changes. +signal follow_target_changed + +## Emitted when [member look_at_target] changes. +signal look_at_target_changed + +## Emitted when dead zones changes.[br] +## [b]Note:[/b] Only applicable in [param Framed] [member FollowMode]. +signal dead_zone_changed + +## Emitted when a target touches the edge of the dead zone in [param Framed] [enum FollowMode]. +signal dead_zone_reached + +## Emitted when the [param Camera3D] starts to tween to another +## [param PhantomCamera3D]. +signal tween_started + +## Emitted when the [param Camera3D] is to tweening towards another +## [param PhantomCamera3D]. +signal is_tweening + +## Emitted when the tween is interrupted due to another [param PhantomCamera3D] +## becoming active. The argument is the [param PhantomCamera3D] that +## interrupted the tween. +signal tween_interrupted(pcam_3d: PhantomCamera3D) + +## Emitted when the [param Camera3D] completes its tween to the +## [param PhantomCamera3D]. +signal tween_completed + +## Emitted when Noise should be applied to the [param Camera3D]. +signal noise_emitted(noise_output: Transform3D) + +signal physics_target_changed + +signal camera_3d_resource_property_changed(property: StringName, value: Variant) +signal camera_3d_resource_changed + +#endregion + + +#region Enums + +## Determines the positional logic for a given [param PhantomCamera3D] +## [br][br] +## The different modes have different functionalities and purposes, so choosing +## the correct one depends on what each [param PhantomCamera3D] is meant to do. +enum FollowMode { + NONE = 0, ## Default - No follow logic is applied. + GLUED = 1, ## Sticks to its target. + SIMPLE = 2, ## Follows its target with an optional offset. + GROUP = 3, ## Follows multiple targets with option to dynamically reframe itself. + PATH = 4, ## Follows a target while being positionally confined to a [Path3D] node. + FRAMED = 5, ## Applies a dead zone on the frame and only follows its target when it tries to leave it. + THIRD_PERSON = 6, ## Applies a [SpringArm3D] node to the target's position and allows for rotating around it. +} + +## Determines the rotational logic for a given [param PhantomCamera3D].[br][br] +## The different modes has different functionalities and purposes, so +## choosing the correct mode depends on what each [param PhantomCamera3D] +## is meant to do. +enum LookAtMode { + NONE = 0, ## Default - No Look At logic is applied. + MIMIC = 1, ## Copies its target's rotational value. + SIMPLE = 2, ## Looks at its target in a straight line. + GROUP = 3, ## Looks at the centre of its targets. +} + +## Determines how often an inactive [param PhantomCamera3D] should update +## its positional and rotational values. This is meant to reduce the amount +## of calculations inactive [param PhantomCamera3D] are doing when idling +## to improve performance. +enum InactiveUpdateMode { + ALWAYS, ## Always updates the [param PhantomCamera3D], even when it's inactive. + NEVER, ## Never updates the [param PhantomCamera3D] when it's inactive. Reduces the amount of computational resources when inactive. +# EXPONENTIALLY, +} + +enum FollowLockAxis { + NONE = 0, + X = 1, + Y = 2, + Z = 3, + XY = 4, + XZ = 5, + YZ = 6, + XYZ = 7, +} + +#endregion + + +#region Exported Properties + +## To quickly preview a [param PhantomCamera3D] without adjusting its +## [member Priority], this property allows the selected [param PhantomCamera3D] +## to ignore the Priority system altogether and forcefully become the active +## one. It's partly designed to work within the [param viewfinder], and will be +## disabled when running a build export of the game. +@export var priority_override: bool = false: + set(value): + priority_override = value + if Engine.is_editor_hint(): + if value: + if not Engine.has_singleton(_constants.PCAM_MANAGER_NODE_NAME): return + Engine.get_singleton(_constants.PCAM_MANAGER_NODE_NAME).pcam_priority_override.emit(self, priority_override) + else: + if not Engine.has_singleton(_constants.PCAM_MANAGER_NODE_NAME): return + Engine.get_singleton(_constants.PCAM_MANAGER_NODE_NAME).pcam_priority_override.emit(self, priority_override) + get: + return priority_override + + +## It defines which [param PhantomCamera3D] a scene's [param Camera3D] should +## be corresponding with and be attached to. This is decided by the +## [param PhantomCamera3D] with the highest [param priority]. +## [br][br] +## Changing [param priority] will send an event to the scene's +## [PhantomCameraHost], which will then determine whether if the +## [param priority] value is greater than or equal to the currently +## highest [param PhantomCamera3D]'s in the scene. The +## [param PhantomCamera3D] with the highest value will then reattach the +## Camera accordingly. +@export var priority: int = 0: + set = set_priority, + get = get_priority + + +## Determines the positional logic for a given [param PhantomCamera3D]. +## The different modes have different functionalities and purposes, so +## choosing the correct one depends on what each [param PhantomCamera3D] +## is meant to do. +@export var follow_mode: FollowMode = FollowMode.NONE: + set(value): + follow_mode = value + + if follow_mode == FollowMode.NONE: + _should_follow = false + top_level = false + _is_parents_physics() + notify_property_list_changed() + return + + match follow_mode: + FollowMode.PATH: + if is_instance_valid(follow_path): + _should_follow_checker() + else: + _should_follow = false + FollowMode.GROUP: + _follow_targets_size_check() + _: + _should_follow_checker() + + if follow_mode == FollowMode.FRAMED: + if _follow_framed_initial_set and follow_target: + _follow_framed_initial_set = false + dead_zone_changed.connect(_on_dead_zone_changed) + else: + if dead_zone_changed.is_connected(_on_dead_zone_changed): + dead_zone_changed.disconnect(_on_dead_zone_changed) + + if follow_mode == FollowMode.THIRD_PERSON: + top_level = false + _is_third_person_follow = true + else: + top_level = true + _is_third_person_follow = false + + follow_mode_changed.emit() + notify_property_list_changed() + + ## NOTE - Warning that Look At + Follow Mode hasn't been fully tested together yet + if look_at_mode != LookAtMode.NONE: + print_rich("[color=#EAA15E]Warning: Using both Look At and Follow Mode on the same PCam3D has not been fully tested yet, proceed with caution![/color]") + get: + return follow_mode + +## Determines which target should be followed. +## The [param Camera3D] will follow the position of the Follow Target based on +## the [member follow_mode] type and its parameters. +@export var follow_target: Node3D = null: + set = set_follow_target, + get = get_follow_target + +## Defines the targets that the [param PhantomCamera3D] should be following. +@export var follow_targets: Array[Node3D] = []: + set = set_follow_targets, + get = get_follow_targets + +## Determines the [Path3D] node the [param PhantomCamera3D] +## should be bound to. +## The [param PhantomCamera3D] will follow the position of the +## [member follow_target] while sticking to the closest point on this path. +@export var follow_path: Path3D = null: + set = set_follow_path, + get = get_follow_path + + +## Determines the rotational logic for a given [param PhantomCamera3D]. +## The different modes has different functionalities and purposes, +## so choosing the correct mode depends on what each +## [param PhantomCamera3D] is meant to do. +@export var look_at_mode: LookAtMode = LookAtMode.NONE: + set(value): + look_at_mode = value + + if look_at_mode == LookAtMode.NONE: + _should_look_at = false + notify_property_list_changed() + return + + if not look_at_mode == LookAtMode.GROUP: + if look_at_target is Node3D: + _should_look_at = true + else: # If Look At Group + _look_at_targets_size_check() + notify_property_list_changed() + + ## NOTE - Warning that Look At + Follow Mode hasn't been fully tested together yet + if follow_mode != FollowMode.NONE: + print_rich("[color=#EAA15E]Warning: Using both Look At and Follow Mode on the same PCam3D has not been fully tested yet, proceed with caution![/color]") + get: + return look_at_mode + +## Determines which target should be looked at. +## The [param PhantomCamera3D] will update its rotational value as the +## target changes its position. +@export var look_at_target: Node3D = null: + set = set_look_at_target, + get = get_look_at_target + +## Defines the targets that the camera should looking at. +## It will be looking at the centre of all the assigned targets. +@export var look_at_targets: Array[Node3D] = []: + set = set_look_at_targets, + get = get_look_at_targets + + +## Defines how [param ]PhantomCamera3Ds] transition between one another. +## Changing the tween values for a given [param PhantomCamera3D] +## determines how transitioning to that instance will look like. +## This is a resource type that can be either used for one +## [param PhantomCamera] or reused across multiple - both 2D and 3D. +## By default, all [param PhantomCameras] will use a [param linear] +## transition, [param easeInOut] ease with a [param 1s] duration. +@export var tween_resource: PhantomCameraTween = PhantomCameraTween.new(): + set = set_tween_resource, + get = get_tween_resource + +## If enabled, the moment a [param PhantomCamera3D] is instantiated into +## a scene, and has the highest priority, it will perform its tween transition. +## This is most obvious if a [param PhantomCamera3D] has a long duration and +## is attached to a playable character that can be moved the moment a scene +## is loaded. Disabling the [param tween_on_load] property will +## disable this behaviour and skip the tweening entirely when instantiated. +@export var tween_on_load: bool = true: + set = set_tween_on_load, + get = get_tween_on_load + + +## Determines how often an inactive [param PhantomCamera3D] should update +## its positional and rotational values. This is meant to reduce the amount +## of calculations inactive [param PhantomCamera3Ds] are doing when idling +## to improve performance. +@export var inactive_update_mode: InactiveUpdateMode = InactiveUpdateMode.ALWAYS: + set = set_inactive_update_mode, + get = get_inactive_update_mode + + +## Determines which layers this [param PhantomCamera3D] should be able to communicate with [PhantomCameraHost] nodes.[br] +## A corresponding layer needs to be set on the [PhantomCameraHost] node. +@export_flags_3d_render var host_layers: int = 1: + set = set_host_layers, + get = get_host_layers + + +## A resource type that allows for overriding the [param Camera3D] node's +## properties. +@export var camera_3d_resource: Camera3DResource = null: + set = set_camera_3d_resource, + get = get_camera_3d_resource + + +## Overrides the [member Camera3D.attribuets] resource property. +@export var attributes: CameraAttributes = null: + set = set_attributes, + get = get_attributes + + +## Overrides the [member Camera3D.environment] resource property. +@export var environment: Environment = null: + set = set_environment, + get = get_environment + + +@export_group("Follow Parameters") +## Offsets the [member follow_target] position. +@export var follow_offset: Vector3 = Vector3.ZERO: + set = set_follow_offset, + get = get_follow_offset + +## Applies a damping effect on the camera's movement. +## Leading to heavier / slower camera movement as the targeted node moves around. +## This is useful to avoid sharp and rapid camera movement. +@export var follow_damping: bool = false: + set = set_follow_damping, + get = get_follow_damping + +## Defines the damping amount. The ideal range should be somewhere between 0-1.[br][br] +## The damping amount can be specified in the individual axis.[br][br] +## [b]Lower value[/b] = faster / sharper camera movement.[br] +## [b]Higher value[/b] = slower / heavier camera movement. +@export_custom(PROPERTY_HINT_LINK, "") +var follow_damping_value: Vector3 = Vector3(0.1, 0.1, 0.1): + set = set_follow_damping_value, + get = get_follow_damping_value + + +## Prevents the [param PhantomCamera2D] from moving in a designated axis. +## This can be enabled or disabled at runtime or from the editor directly. +@export var follow_axis_lock: FollowLockAxis = FollowLockAxis.NONE: + set = set_follow_axis_lock, + get = get_follow_axis_lock +var _follow_axis_is_locked: bool = false +var _follow_axis_lock_value: Vector3 = Vector3.ZERO + + +## Sets a distance offset from the centre of the target's position. +## The distance is applied to the [param PhantomCamera3D]'s local z axis. +@export var follow_distance: float = 1: + set = set_follow_distance, + get = get_follow_distance + +## Enables the [param PhantomCamera3D] to automatically distance +## itself as the [param follow targets] move further apart.[br] +## It looks at the longest axis between the different targets and interpolates +## the distance length between the [member auto_follow_distance_min] and +## [member follow_group_distance] properties.[br][br] +## Note: Enabling this property hides and disables the [member follow_distance] +## property as this effectively overrides that property. +@export var auto_follow_distance: bool = false: + set = set_auto_follow_distance, + get = get_auto_follow_distance + +## Sets the minimum distance between the Camera and centre of [AABB]. +## [br][br] +## Note: This distance will only ever be reached when all the targets are in +## the exact same [param Vector3] coordinate, which will very unlikely +## happen, so adjust the value here accordingly. +## [br][br] +## If only one follow target is assigned to [member follow_targets], this value will be used as the `follow_distance`. +@export var auto_follow_distance_min: float = 1: + set = set_auto_follow_distance_min, + get = get_auto_follow_distance_min + +## Sets the maximum distance between the Camera and centre of [AABB]. +@export var auto_follow_distance_max: float = 5: + set = set_auto_follow_distance_max, + get = get_auto_follow_distance_max + +## Determines how fast the [member auto_follow_distance] moves between the +## maximum and minimum distance. The higher the value, the sooner the +## maximum distance is reached.[br][br] +## This value should be based on the sizes of the [member auto_follow_distance_min] +## and [member auto_follow_distance_max].[br] +## E.g. if the value between the [member auto_follow_distance_min] and +## [member auto_follow_distance_max] is small, consider keeping the number low +## and vice versa. +@export var auto_follow_distance_divisor: float = 10: + set = set_auto_follow_distance_divisor, + get = get_auto_follow_distance_divisor + + +@export_subgroup("Dead Zones") +## Defines the horizontal dead zone area. While the target is within it, the +## [param PhantomCamera3D] will not move in the horizontal axis. +## If the targeted node leaves the horizontal bounds, the +## [param PhantomCamera3D] will follow the target horizontally to keep +## it within bounds. +@export_range(0, 1) var dead_zone_width: float = 0: + set(value): + dead_zone_width = value + dead_zone_changed.emit() + get: + return dead_zone_width + +## Defines the vertical dead zone area. While the target is within it, the +## [param PhantomCamera3D] will not move in the vertical axis. +## If the targeted node leaves the vertical bounds, the +## [param PhantomCamera3D] will follow the target horizontally to keep +## it within bounds. +@export_range(0, 1) var dead_zone_height: float = 0: + set(value): + dead_zone_height = value + dead_zone_changed.emit() + get: + return dead_zone_height + +## Enables the dead zones to be visible when running the game from the editor. +## Dead zones will never be visible in build exports. +@export var show_viewfinder_in_play: bool = false + +## Defines the position of the [member follow_target] within the viewport.[br] +## This is only used for when [member follow_mode] is set to [param Framed]. +@export_subgroup("Spring Arm") + +## Applies a rotational offset to the Third Person [member follow_mode] in the [code]X[/code] axis. +@export_range(-360, 360, 0.1,"or_greater", "or_less", "radians_as_degrees") +var vertical_rotation_offset: float = 0: + set = set_vertical_rotation_offset, + get = get_vertical_rotation_offset + +## Applies a rotational offset to the Third Person [member follow_mode] in the [code]Y[/code] axis. +@export_range(-360, 360, 0.1, "or_greater", "or_less", "radians_as_degrees") +var horizontal_rotation_offset: float = 0: + set = set_horizontal_rotation_offset, + get = get_horizontal_rotation_offset + +## Defines the [member SpringArm3D.spring_length]. +@export var spring_length: float = 1: + set = set_spring_length, + get = get_spring_length + +## Defines the [member SpringArm3D.collision_mask] node's Collision Mask. +@export_flags_3d_physics var collision_mask: int = 1: + set = set_collision_mask, + get = get_collision_mask + +## Defines the [member SpringArm3D.shape] node's Shape3D. +@export var shape: Shape3D = null: + set = set_shape, + get = get_shape + +## Defines the [member SpringArm3D.margin] node's Margin. +@export var margin: float = 0.01: + set = set_margin, + get = get_margin + + +@export_group("Look At Parameters") +## Offsets the target's [param Vector3] position that the +## [param PhantomCamera3D] is looking at. +@export var look_at_offset: Vector3 = Vector3.ZERO: + set = set_look_at_offset, + get = get_look_at_offset + +## Applies a damping effect on the camera's rotation. +## Leading to heavier / slower camera movement as the targeted node moves around. +## This is useful to avoid sharp and rapid camera rotation. +@export var look_at_damping: bool = false: + set = set_look_at_damping, + get = get_look_at_damping + +## Defines the Rotational damping amount. The ideal range is typically somewhere between 0-1.[br][br] +## The damping amount can be specified in the individual axis.[br][br] +## [b]Lower value[/b] = faster / sharper camera rotation.[br] +## [b]Higher value[/b] = slower / heavier camera rotation. +@export_range(0.0, 1.0, 0.001, "or_greater") var look_at_damping_value: float = 0.25: + set = set_look_at_damping_value, + get = get_look_at_damping_value + +@export_subgroup("Up Direction") + +## Defines the upward direction of the [param PhantomCamera3D] when [member look_at_mode] is set. [br] +## This value will be overriden if [member up_target] is defined. +@export var up: Vector3 = Vector3.UP: + set = set_up, + get = get_up + +## Applies and continuously updates the [param up] direction of the [param PhantomCamera3D] based on this target when [member look_at_mode] is set.[br] +## Setting a value here will override the [member up] value. +@export var up_target: Node3D = null: + set = set_up_target, + get = get_up_target + + +@export_group("Noise") +## Applies a noise, or shake, to a [Camera3D].[br] +## Once set, the noise will run continuously after the tween to the [PhantomCamera3D] instance is complete. +@export var noise: PhantomCameraNoise3D = null: + set = set_noise, + get = get_noise + +## If true, will trigger the noise while in the editor.[br] +## Useful in cases where you want to temporarily disalbe the noise in the editor without removing +## the resource.[br][br] +## [b]Note:[/b] This property has no effect on runtime behaviour. +@export var _preview_noise: bool = true: + set(value): + _preview_noise = value + if not value: + _transform_noise = Transform3D() + +## Enable a corresponding layer for a [member PhantomCameraNoiseEmitter3D.noise_emitter_layer] +## to make this [PhantomCamera3D] be affect by it. +@export_flags_3d_render var noise_emitter_layer: int = 0: + set = set_noise_emitter_layer, + get = get_noise_emitter_layer + +#endregion + +#region Private Variables + +var _is_active: bool = false + +var _is_third_person_follow: bool = false +var _camera_target: Node3D = self # Calculates the position of the camera in the editor, uses instantiated SpringArm3D node when running the scene + +var _should_follow: bool = false +var _follow_target_physics_based: bool = false +var _physics_interpolation_enabled: bool = false ## TOOD - Should be enbled once toggling physics_interpolation_mode ON, when previously OFF, works in 3D + +var _has_multiple_follow_targets: bool = false +var _follow_targets_single_target_index: int = 0 +var _follow_targets: Array[Node3D] = [] + +var _should_look_at: bool = false +var _look_at_target_physics_based: bool = false + +var _has_multiple_look_at_targets: bool = false +var _look_at_targets_single_target_index: int = 0 + +var _current_rotation: Vector3 = Vector3.ZERO + +var _up: Vector3 = Vector3.UP +var _has_up_target: bool = false + +var _follow_target_position: Vector3 = Vector3.ZERO +var _look_at_target_position: Vector3 = Vector3.ZERO + +var _transform_output: Transform3D = Transform3D() +var _transform_noise: Transform3D = Transform3D() + +var _tween_skip: bool = false + +var _follow_velocity_ref: Vector3 = Vector3.ZERO # Stores and applies the velocity of the movement + +var _follow_framed_initial_set: bool = false +var _follow_framed_offset: Vector3 = Vector3.ZERO + +var _follow_spring_arm: SpringArm3D = null +var _has_follow_spring_arm: bool = false + +var _has_noise_resource: bool = false + + +# NOTE - Temp solution until Godot has better plugin autoload recognition out-of-the-box. +var _phantom_camera_manager: Node = null + +#endregion + +#region Public Variable + +var tween_duration: float: + set = set_tween_duration, + get = get_tween_duration +var tween_transition: PhantomCameraTween.TransitionType: + set = set_tween_transition, + get = get_tween_transition +var tween_ease: PhantomCameraTween.EaseType: + set = set_tween_ease, + get = get_tween_ease + +var keep_aspect: int: + set = set_keep_aspect, + get = get_keep_aspect +var cull_mask: int: + set = set_cull_mask, + get = get_cull_mask +var h_offset: float: + set = set_h_offset, + get = get_h_offset +var v_offset: float: + set = set_v_offset, + get = get_v_offset +var projection: Camera3DResource.ProjectionType: + set = set_projection, + get = get_projection +var fov: float: + set = set_fov, + get = get_fov +var size: float: + set = set_size, + get = get_size +var frustum_offset: Vector2: + set = set_frustum_offset, + get = get_frustum_offset +var far: float: + set = set_far, + get = get_far +var near: float: + set = set_near, + get = get_near + +var viewport_position: Vector2 + +#endregion + + +#region Private Functions + +func _validate_property(property: Dictionary) -> void: + ################ + ## Follow Target + ################ + if property.name == "follow_target": + if follow_mode == FollowMode.NONE or \ + follow_mode == FollowMode.GROUP: + property.usage = PROPERTY_USAGE_NO_EDITOR + + if property.name == "follow_path" and \ + follow_mode != FollowMode.PATH: + property.usage = PROPERTY_USAGE_NO_EDITOR + + #################### + ## Follow Parameters + #################### + if follow_mode == FollowMode.NONE: + match property.name: + "follow_offset", \ + "follow_damping", \ + "follow_damping_value", \ + "follow_axis_lock": + property.usage = PROPERTY_USAGE_NO_EDITOR + + if property.name == "follow_offset": + if follow_mode == FollowMode.PATH or \ + follow_mode == FollowMode.GLUED: + property.usage = PROPERTY_USAGE_NO_EDITOR + + if property.name == "follow_damping_value" and not follow_damping: + property.usage = PROPERTY_USAGE_NO_EDITOR + + if property.name == "follow_offset": + if follow_mode == FollowMode.PATH: + property.usage = PROPERTY_USAGE_NO_EDITOR + + if property.name == "follow_distance": + if not follow_mode == FollowMode.FRAMED: + if not follow_mode == FollowMode.GROUP or \ + auto_follow_distance: \ + property.usage = PROPERTY_USAGE_NO_EDITOR + + ############### + ## Group Follow + ############### + if property.name == "follow_targets" and \ + not follow_mode == FollowMode.GROUP: + property.usage = PROPERTY_USAGE_NO_EDITOR + + if property.name == "auto_follow_distance" and \ + not follow_mode == FollowMode.GROUP: + property.usage = PROPERTY_USAGE_NO_EDITOR + + if not auto_follow_distance or not follow_mode == FollowMode.GROUP: + match property.name: + "auto_follow_distance_min", \ + "auto_follow_distance_max", \ + "auto_follow_distance_divisor": + property.usage = PROPERTY_USAGE_NO_EDITOR + + ############### + ## Framed Follow + ############### + if not follow_mode == FollowMode.FRAMED: + match property.name: + "dead_zone_width", \ + "dead_zone_height", \ + "show_viewfinder_in_play": + property.usage = PROPERTY_USAGE_NO_EDITOR + + ###################### + ## Third Person Follow + ###################### + if not follow_mode == FollowMode.THIRD_PERSON: + match property.name: + "vertical_rotation_offset", \ + "horizontal_rotation_offset", \ + "spring_length", \ + "collision_mask", \ + "shape", \ + "margin": + property.usage = PROPERTY_USAGE_NO_EDITOR + + ########## + ## Look At + ########## + if look_at_mode == LookAtMode.NONE: + match property.name: + "look_at_target", \ + "look_at_offset" , \ + "look_at_damping", \ + "look_at_damping_value", \ + "up", \ + "up_target": + property.usage = PROPERTY_USAGE_NO_EDITOR + elif look_at_mode == LookAtMode.GROUP: + match property.name: + "look_at_target": + property.usage = PROPERTY_USAGE_NO_EDITOR + + if property.name == "look_at_target": + if look_at_mode == LookAtMode.NONE or \ + look_at_mode == LookAtMode.GROUP: + property.usage = PROPERTY_USAGE_NO_EDITOR + + if property.name == "look_at_targets" and \ + not look_at_mode == LookAtMode.GROUP: + property.usage = PROPERTY_USAGE_NO_EDITOR + + if property.name == "look_at_damping_value" and \ + not look_at_damping: + property.usage = PROPERTY_USAGE_NO_EDITOR + + if property.name == "up" and _has_up_target: + property.usage = PROPERTY_USAGE_NO_EDITOR + + +func _enter_tree() -> void: + _phantom_camera_manager = Engine.get_singleton(_constants.PCAM_MANAGER_NODE_NAME) + _tween_skip = !tween_on_load + + _phantom_camera_manager.pcam_added(self) + + priority_override = false + + if not visibility_changed.is_connected(_check_visibility): + visibility_changed.connect(_check_visibility) + + match follow_mode: + FollowMode.NONE: + _is_parents_physics() + FollowMode.PATH: + if is_instance_valid(follow_path): + _should_follow_checker() + else: + _should_follow = false + FollowMode.GROUP: + _follow_targets_size_check() + _should_follow_checker() + _: + _should_follow_checker() + + _should_look_at_checker() + if look_at_mode == LookAtMode.GROUP: + _look_at_targets_size_check() + + #if not get_parent() is SpringArm3D: + #if look_at_target: + #_look_at_target_node = look_at_target + #elif look_at_targets: + #_look_at_group_nodes.clear() + #for path in look_at_targets: + #if not path.is_empty() and path: + #_should_look_at = true + #_has_look_at_targets = true + #_look_at_group_nodes.append(path) + + +func _exit_tree() -> void: + if not follow_mode == FollowMode.GROUP: + follow_targets = [] + + if not is_instance_valid(_phantom_camera_manager): return + _phantom_camera_manager.pcam_removed(self) + + +func _ready(): + match follow_mode: + FollowMode.THIRD_PERSON: + _is_third_person_follow = true + if _should_follow: _transform_output.origin = _get_target_position_offset_distance() + if not Engine.is_editor_hint(): + if not is_instance_valid(_follow_spring_arm): + _follow_spring_arm = SpringArm3D.new() + _follow_spring_arm.top_level = true + _follow_spring_arm.spring_length = spring_length + _follow_spring_arm.collision_mask = collision_mask + _follow_spring_arm.shape = shape + _follow_spring_arm.margin = margin + # Stores the rotation value as the rotation gets skewed after + # the SpringArm3D is instantiated for some reason... + var initial_rotation: Vector3 = global_rotation + if _should_follow: _follow_spring_arm.add_excluded_object(follow_target) + get_parent().add_child.call_deferred(_follow_spring_arm) + + # Waits for the SpringArm3D to be ready and then apply rotation + # Resolves an issue most prominent in Godot 4.4 + await _follow_spring_arm.ready + reparent.call_deferred(_follow_spring_arm) + _camera_target = _follow_spring_arm + _follow_spring_arm.global_position = _get_target_position_offset() if is_instance_valid(follow_target) else global_position + _follow_spring_arm.global_rotation = initial_rotation + _has_follow_spring_arm = true + top_level = false + FollowMode.FRAMED: + if not Engine.is_editor_hint(): + if is_instance_valid(follow_target): + _follow_framed_offset = global_position - _get_target_position_offset() + _current_rotation = global_rotation + FollowMode.GROUP: + _follow_targets_size_check() + _: + if is_instance_valid(follow_target): + _transform_output.origin = _get_target_position_offset() + else: + _transform_output.origin = global_position + + if not Engine.is_editor_hint(): + _preview_noise = true + + ## NOTE - Only here to set position for Framed View on startup. + ## Should be removed once https://github.com/ramokz/phantom-camera/issues/161 is complete + _transform_output = global_transform + + _phantom_camera_manager.noise_3d_emitted.connect(_noise_emitted) + + +func _process(delta: float) -> void: + if _follow_target_physics_based or _is_active: return + process_logic(delta) + + +func _physics_process(delta: float) -> void: + if not _follow_target_physics_based or _is_active: return + process_logic(delta) + + +func process_logic(delta: float) -> void: + if _is_active: + if _has_noise_resource and _preview_noise: + _transform_noise = noise.get_noise_transform(delta) + else: + match inactive_update_mode: + InactiveUpdateMode.NEVER: return + # InactiveUpdateMode.EXPONENTIALLY: + # TODO - Trigger positional updates less frequently as more PCams gets added + + if _should_follow: + _follow(delta) + else: + _transform_output.origin = global_position + + if _should_look_at: + _look_at(delta) + elif not _is_third_person_follow: + _transform_output.basis = global_basis + + if _follow_axis_is_locked: + match follow_axis_lock: + FollowLockAxis.X: + _transform_output.origin.x = _follow_axis_lock_value.x + FollowLockAxis.Y: + _transform_output.origin.y = _follow_axis_lock_value.y + FollowLockAxis.Z: + _transform_output.origin.z = _follow_axis_lock_value.z + FollowLockAxis.XY: + _transform_output.origin.x = _follow_axis_lock_value.x + _transform_output.origin.y = _follow_axis_lock_value.y + FollowLockAxis.XZ: + _transform_output.origin.x = _follow_axis_lock_value.x + _transform_output.origin.z = _follow_axis_lock_value.z + FollowLockAxis.YZ: + _transform_output.origin.y = _follow_axis_lock_value.y + _transform_output.origin.z = _follow_axis_lock_value.z + FollowLockAxis.XYZ: + _transform_output.origin.x = _follow_axis_lock_value.x + _transform_output.origin.y = _follow_axis_lock_value.y + _transform_output.origin.z = _follow_axis_lock_value.z + + +func _follow(delta: float) -> void: + _set_follow_position() + _interpolate_position(delta) + +func _look_at(delta: float) -> void: + _set_look_at_position() + _interpolate_rotation(delta) + + +func _set_follow_position() -> void: + match follow_mode: + FollowMode.GLUED: + _follow_target_position = follow_target.global_position + + FollowMode.SIMPLE: + _follow_target_position = _get_target_position_offset() + + FollowMode.GROUP: + if _has_multiple_follow_targets: + var bounds: AABB = AABB(_follow_targets[0].global_position, Vector3.ZERO) + for target in _follow_targets: + bounds = bounds.expand(target.global_position) + var distance: float + if auto_follow_distance: + distance = lerpf(auto_follow_distance_min, auto_follow_distance_max, bounds.get_longest_axis_size() / auto_follow_distance_divisor) + distance = clampf(distance, auto_follow_distance_min, auto_follow_distance_max) + else: + distance = follow_distance + + _follow_target_position = \ + bounds.get_center() + \ + follow_offset + \ + global_basis.z * \ + distance + else: + _follow_target_position = \ + follow_targets[_follow_targets_single_target_index].global_position + \ + follow_offset + \ + global_basis.z * \ + auto_follow_distance_min + + FollowMode.PATH: + var path_position: Vector3 = follow_path.global_position + _follow_target_position = \ + follow_path.curve.get_closest_point( + follow_target.global_position - path_position + ) + path_position + + FollowMode.FRAMED: + if not Engine.is_editor_hint(): + if not _is_active: + _follow_target_position = _get_target_position_offset_distance() + else: + viewport_position = get_viewport().get_camera_3d().unproject_position(_get_target_position_offset()) + var visible_rect_size: Vector2 = get_viewport().get_visible_rect().size + viewport_position = viewport_position / visible_rect_size + _current_rotation = global_rotation + + if _current_rotation != global_rotation: + _follow_target_position = _get_target_position_offset_distance() + + if _get_framed_side_offset() != Vector2.ZERO: + var target_position: Vector3 = _get_target_position_offset() + _follow_framed_offset + var glo_pos: Vector3 + + if dead_zone_width == 0 || dead_zone_height == 0: + if dead_zone_width == 0 && dead_zone_height != 0: + glo_pos = _get_target_position_offset_distance() + glo_pos.z = target_position.z + _follow_target_position = glo_pos + elif dead_zone_width != 0 && dead_zone_height == 0: + glo_pos = _get_target_position_offset_distance() + glo_pos.x = target_position.x + _follow_target_position = glo_pos + else: + _follow_target_position = _get_target_position_offset_distance() + else: + if _current_rotation != global_rotation: + var opposite: float = sin(-global_rotation.x) * follow_distance + _get_target_position_offset().y + glo_pos.y = _get_target_position_offset().y + opposite + glo_pos.z = sqrt(pow(follow_distance, 2) - pow(opposite, 2)) + _get_target_position_offset().z + glo_pos.x = global_position.x + + _follow_target_position = glo_pos + _current_rotation = global_rotation + else: + dead_zone_reached.emit() + _follow_target_position = target_position + else: + _follow_framed_offset = global_position - _get_target_position_offset() + _current_rotation = global_rotation + return + else: + _follow_target_position = _get_target_position_offset_distance() + var unprojected_position: Vector2 = _get_raw_unprojected_position() + var viewport_width: float = get_viewport().size.x + var viewport_height: float = get_viewport().size.y + var camera_aspect: int = get_viewport().get_camera_3d().keep_aspect + var visible_rect_size: Vector2 = get_viewport().get_visible_rect().size + + unprojected_position = unprojected_position - visible_rect_size / 2 + if camera_aspect == Camera3D.KEEP_HEIGHT: + # Landscape View + var aspect_ratio_scale: float = viewport_width / viewport_height + unprojected_position.x = (unprojected_position.x / aspect_ratio_scale + 1) / 2 + unprojected_position.y = (unprojected_position.y + 1) / 2 + else: + # Portrait View + var aspect_ratio_scale: float = viewport_height / viewport_width + unprojected_position.x = (unprojected_position.x + 1) / 2 + unprojected_position.y = (unprojected_position.y / aspect_ratio_scale + 1) / 2 + + viewport_position = unprojected_position + + FollowMode.THIRD_PERSON: + if not Engine.is_editor_hint(): + if not _has_follow_spring_arm: return + _follow_target_position = _get_target_position_offset() + else: + _follow_target_position = _get_target_position_offset_distance_direction() + + +func _set_look_at_position() -> void: + match look_at_mode: + LookAtMode.MIMIC: + _look_at_target_position = global_position - look_at_target.global_basis.z + + LookAtMode.SIMPLE: + _look_at_target_position =look_at_target.global_position + + LookAtMode.GROUP: + if not _has_multiple_look_at_targets: + _look_at_target_position =look_at_targets[_look_at_targets_single_target_index].global_position + else: + var bounds: AABB = AABB(look_at_targets[0].global_position, Vector3.ZERO) + for node in look_at_targets: + bounds = bounds.expand(node.global_position) + _look_at_target_position =bounds.get_center() + + +func _get_target_position_offset() -> Vector3: + return follow_target.global_position + follow_offset + + +func _get_target_position_offset_distance() -> Vector3: + return _get_target_position_offset() + \ + transform.basis.z * follow_distance + +# Used in the editor for setting initial Third Person position and angle +func _get_target_position_offset_distance_direction() -> Vector3: + return _get_target_position_offset() + \ + follow_target.global_basis.z * \ + follow_distance * \ + Quaternion(follow_target.global_basis.x, vertical_rotation_offset) * \ + Quaternion(follow_target.global_basis.y, horizontal_rotation_offset) + + +func _set_follow_velocity(index: int, value: float) -> void: + _follow_velocity_ref[index] = value + +func _interpolate_position(delta: float) -> void: + if follow_damping and not Engine.is_editor_hint(): + if not _is_third_person_follow: + global_position = _follow_target_position + for i in 3: + _transform_output.origin[i] = _smooth_damp( + global_position[i], + _transform_output.origin[i], + i, + _follow_velocity_ref[i], + _set_follow_velocity, + follow_damping_value[i], + delta + ) + else: + for i in 3: + _camera_target.global_position[i] = _smooth_damp( + _follow_target_position[i], + _camera_target.global_position[i], + i, + _follow_velocity_ref[i], + _set_follow_velocity, + follow_damping_value[i], + delta + ) + _transform_output.origin = global_position + else: + _camera_target.global_position = _follow_target_position + _transform_output.origin = global_position + + if _is_third_person_follow: + var target_quat: Quaternion = _look_at_target_quat(_get_target_position_offset(), follow_target.global_basis.y) + var target_basis: Basis = Basis(target_quat) + _transform_output.basis = target_basis + global_basis = target_basis + + +func _look_at_target_quat(target_position: Vector3, up_direction: Vector3 = Vector3.UP) -> Quaternion: + var direction: Vector3 = -(target_position - global_position + look_at_offset).normalized() + + var basis_z: Vector3 = direction.normalized() + var basis_x: Vector3 = up_direction.cross(basis_z) + var basis_y: Vector3 = basis_z.cross(basis_x.normalized()) + + var target_basis: Basis = Basis(basis_x, basis_y, basis_z) + + if target_basis.determinant() == 0: + if target_basis.z == Vector3.UP: + global_rotation_degrees.x = -90 + else: + global_rotation_degrees.x = 90 + + _transform_output.basis = global_basis + return quaternion + + return target_basis.get_rotation_quaternion().normalized() + +func _interpolate_rotation(delta: float) -> void: + if _has_up_target: + _up = up_target.global_basis.y + + var target_quat: Quaternion = _look_at_target_quat(_look_at_target_position, _up) + + if look_at_damping: + var current_quat: Quaternion = quaternion.normalized() + var damping_time: float = max(0.0001, look_at_damping_value) + var t: float = min(1.0, delta / damping_time) + + var dot: float = current_quat.dot(target_quat) + + if dot < 0.0: + target_quat = -target_quat + dot = -dot + + dot = clampf(dot, -1.0, 1.0) + + var theta: float = acos(dot) * t + var sin_theta: float = sin(theta) + var sin_theta_total: float = sin(acos(dot)) + + if is_zero_approx(sin_theta_total): return + + var ratio_a: float = cos(theta) - dot * sin_theta / sin_theta_total + var ratio_b: float = sin_theta / sin_theta_total + var output: Quaternion = current_quat * ratio_a + target_quat * ratio_b + + _transform_output.basis = Basis(output) + quaternion = output + else: + _transform_output.basis = Basis(target_quat) + quaternion = target_quat + + +func _smooth_damp(target_axis: float, self_axis: float, index: int, current_velocity: float, set_velocity: Callable, damping_time: float, delta: float) -> float: + damping_time = maxf(0.0001, damping_time) + var omega: float = 2 / damping_time + var x: float = omega * delta + var exponential: float = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x) + var diff: float = self_axis - target_axis + var _target_axis: float = target_axis + + var max_change: float = INF * damping_time + diff = clampf(diff, -max_change, max_change) + target_axis = self_axis - diff + + var temp: float = (current_velocity + omega * diff) * delta + set_velocity.call(index, (current_velocity - omega * temp) * exponential) + var output: float = target_axis + (diff + temp) * exponential + + ## To prevent overshooting + if (_target_axis - self_axis > 0.0) == (output > _target_axis): + output = _target_axis + set_velocity.call(index, (output - _target_axis) / delta) + + return output + + +func _get_raw_unprojected_position() -> Vector2: + return get_viewport().get_camera_3d().unproject_position(follow_target.global_position + follow_offset) + + +func _on_dead_zone_changed() -> void: + global_position = _get_target_position_offset_distance() + + +func _get_framed_side_offset() -> Vector2: + var frame_out_bounds: Vector2 + + if viewport_position.x < 0.5 - dead_zone_width / 2: + # Is outside left edge + frame_out_bounds.x = -1 + + if viewport_position.y < 0.5 - dead_zone_height / 2: + # Is outside top edge + frame_out_bounds.y = 1 + + if viewport_position.x > 0.5 + dead_zone_width / 2: + # Is outside right edge + frame_out_bounds.x = 1 + + if viewport_position.y > 0.5001 + dead_zone_height / 2: # 0.501 to resolve an issue where the bottom vertical Dead Zone never becoming 0 when the Dead Zone Vertical parameter is set to 0 + # Is outside bottom edge + frame_out_bounds.y = -1 + + return frame_out_bounds + + +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("Render layer must be between 1 and 20.") + else: + if value: + mask |= 1 << (layer_number - 1) + else: + mask &= ~(1 << (layer_number - 1)) + + return mask + + +func _check_visibility() -> void: + _phantom_camera_manager.pcam_visibility_changed.emit(self) + + +func _follow_target_tree_exiting(target: Node) -> void: + if target == follow_target: + _should_follow = false + if _follow_targets.has(target): + _follow_targets.erase(target) + + +func _should_follow_checker() -> void: + if follow_mode == FollowMode.NONE: + _should_follow = false + return + + if not follow_mode == FollowMode.GROUP: + if is_instance_valid(follow_target): + _should_follow = true + else: + _should_follow = false + + +func _follow_targets_size_check() -> void: + var targets_size: int = 0 + _follow_target_physics_based = false + _follow_targets = [] + for i in follow_targets.size(): + if follow_targets[i] == null: continue + if follow_targets[i].is_inside_tree(): + _follow_targets.append(follow_targets[i]) + targets_size += 1 + _follow_targets_single_target_index = i + _check_physics_body(follow_targets[i]) + if not follow_targets[i].tree_exiting.is_connected(_follow_target_tree_exiting): + follow_targets[i].tree_exiting.connect(_follow_target_tree_exiting.bind(follow_targets[i])) + + match targets_size: + 0: + _should_follow = false + _has_multiple_follow_targets = false + 1: + _should_follow = true + _has_multiple_follow_targets = false + _: + _should_follow = true + _has_multiple_follow_targets = true + + +func _look_at_target_tree_exiting(target: Node) -> void: + if target == look_at_target: + _should_look_at = false + if look_at_targets.has(target): + erase_look_at_targets(target) + +func _up_target_tree_exiting() -> void: + up_target = null + + +func _should_look_at_checker() -> void: + if look_at_mode == LookAtMode.NONE: + _should_look_at = false + return + + if not look_at_mode == LookAtMode.GROUP: + if is_instance_valid(look_at_target): + _should_look_at = true + else: + _should_look_at = false + + +func _look_at_targets_size_check() -> void: + var targets_size: int = 0 + _look_at_target_physics_based = false + + for i in look_at_targets.size(): + if is_instance_valid(look_at_targets[i]): + targets_size += 1 + _look_at_targets_single_target_index = i + _check_physics_body(look_at_targets[i]) + if not look_at_targets[i].tree_exiting.is_connected(_look_at_target_tree_exiting): + look_at_targets[i].tree_exiting.connect(_look_at_target_tree_exiting.bind(look_at_targets[i])) + + match targets_size: + 0: + _should_look_at = false + _has_multiple_look_at_targets = false + 1: + _should_look_at = true + _has_multiple_look_at_targets = false + _: + _should_look_at = true + _has_multiple_look_at_targets = true + + +func _noise_emitted(emitter_noise_output: Transform3D, emitter_layer: int) -> void: + if noise_emitter_layer & emitter_layer != 0: + noise_emitted.emit(emitter_noise_output) + + +func _check_physics_body(target: Node3D) -> void: + if target is PhysicsBody3D: + var show_jitter_tips := ProjectSettings.get_setting("phantom_camera/tips/show_jitter_tips") + var physics_interpolation_enabled := ProjectSettings.get_setting("physics/common/physics_interpolation") + + ## NOTE - Feature Toggle + if Engine.get_version_info().major == 4 and \ + Engine.get_version_info().minor < 4: + if show_jitter_tips == null: # Default value is null when referencing custom Project Setting + print_rich("Following or Looking at a [b]PhysicsBody3D[/b] node will likely result in jitter - on lower physics ticks in particular.") + print_rich("If possible, will recommend upgrading to Godot 4.4, as it has built-in support for 3D Physics Interpolation, which will mitigate this issue.") + print_rich("Until then, try following the guide on the [url=https://phantom-camera.dev/support/faq#i-m-seeing-jitter-what-can-i-do]documentation site[/url] for better results.") + print_rich("This tip can be disabled from within [code]Project Settings / Phantom Camera / Tips / Show Jitter Tips[/code]") + return + ## NOTE - Only supported in Godot 4.4 or above + elif not physics_interpolation_enabled and show_jitter_tips == null: # Default value is null when referencing custom Project Setting + printerr("Physics Interpolation is disabled in the Project Settings, recommend enabling it to smooth out physics-based camera movement") + print_rich("This tip can be disabled from within [code]Project Settings / Phantom Camera / Tips / Show Jitter Tips[/code]") + _follow_target_physics_based = true + else: + _is_parents_physics(target) + physics_target_changed.emit() + + +func _is_parents_physics(target: Node = self) -> void: + var current_node: Node = target + while current_node: + current_node = current_node.get_parent() + if not current_node is PhysicsBody3D: continue + _follow_target_physics_based = true + + +func _camera_resource_changed() -> void: + camera_3d_resource_changed.emit() + +#endregion + +#region Public Functions + +# TBD +#func get_unprojected_position() -> Vector2: + #var unprojected_position: Vector2 = _get_raw_unprojected_position() + #var viewport_width: float = get_viewport().size.x + #var viewport_height: float = get_viewport().size.y + #var camera_aspect: Camera3D.KeepAspect = get_viewport().get_camera_3d().keep_aspect + #var visible_rect_size: Vector2 = get_viewport().size +# + #unprojected_position = unprojected_position - visible_rect_size / 2 + #if camera_aspect == Camera3D.KeepAspect.KEEP_HEIGHT: +## print("Landscape View") + #var aspect_ratio_scale: float = viewport_width / viewport_height + #unprojected_position.x = (unprojected_position.x / aspect_ratio_scale + 1) / 2 + #unprojected_position.y = (unprojected_position.y + 1) / 2 + #else: +## print("Portrait View") + #var aspect_ratio_scale: float = viewport_height / viewport_width + #unprojected_position.x = (unprojected_position.x + 1) / 2 + #unprojected_position.y = (unprojected_position.y / aspect_ratio_scale + 1) / 2 +# + #return unprojected_position + + +## Returns the [Transform3D] value based on the [member follow_mode] / [member look_at_mode] target value. +func get_transform_output() -> Transform3D: + return _transform_output + + +## Returns the noise [Transform3D] value. +func get_noise_transform() -> Transform3D: + return _transform_noise + + +## Emits a noise based on a custom [Transform3D] value.[br] +## Use this function if you wish to make use of external noise patterns from, for example, other addons. +func emit_noise(value: Transform3D) -> void: + noise_emitted.emit(value) + + +## Teleports the [param PhantomCamera3D] and [Camera3D] to their designated position, +## bypassing the damping process. +func teleport_position() -> void: + _follow_velocity_ref = Vector3.ZERO + _set_follow_position() + _transform_output.origin = _follow_target_position + _phantom_camera_manager.pcam_teleport.emit(self) + + +# TODO: Enum link does link to anywhere is being tracked in: https://github.com/godotengine/godot/issues/106828 +## Returns [code]true[/code] if this [param PhantomCamera3D]'s [member follow_mode] is not set to [constant FollowMode.NONE] +## and has a valid [member follow_target]. +func is_following() -> bool: + return _should_follow + +# TODO: Enum link does link to anywhere is being tracked in: https://github.com/godotengine/godot/issues/106828 +## Returns [code]true[/code] if this [param PhantomCamera3D]'s [member look_at_mode] is not set to [constant LookAtMode.NONE] +## and has a valid [member look_at_target]. +func is_looking() -> bool: + return _should_look_at + +#endregion + + +#region Setter & Getter Functions + +## Assigns the value of the [param has_tweened] property.[br] +## [b][color=yellow]Important:[/color][/b] This value can only be changed +## from the [PhantomCameraHost] script. +func set_tween_skip(caller: Node, value: bool) -> void: + if is_instance_of(caller, PhantomCameraHost): + _tween_skip = value + else: + printerr("Can only be called PhantomCameraHost class") +## Returns the current [param has_tweened] value. +func get_tween_skip() -> bool: + return _tween_skip + + +## Assigns new [member priority] value. +func set_priority(value: int) -> void: + priority = maxi(0, value) + if not is_node_ready(): return + if not Engine.has_singleton(_constants.PCAM_MANAGER_NODE_NAME): return + Engine.get_singleton(_constants.PCAM_MANAGER_NODE_NAME).pcam_priority_changed.emit(self) +## Gets current [param Priority] value. +func get_priority() -> int: + return priority + + +## Assigns a new [PhantomCameraTween] resource to the [param PhantomCamera3D]. +func set_tween_resource(value: PhantomCameraTween) -> void: + tween_resource = value +## Gets the [param PhantomCameraTween] resource assigned to the [param PhantomCamera3D]. +## Returns null if there's nothing assigned to it. +func get_tween_resource() -> PhantomCameraTween: + return tween_resource + +## Assigns a new [param Tween Duration] to the [member tween_resource] value.[br] +## The duration value is in seconds. +func set_tween_duration(value: float) -> void: + tween_resource.duration = value +## Gets the current [param Tween] Duration value. The duration value is in +## [param seconds]. +func get_tween_duration() -> float: + return tween_resource.duration + +## Assigns a new [param Tween Transition] to the [member tween_resource] value.[br] +## The duration value is in seconds. +func set_tween_transition(value: int) -> void: + tween_resource.transition = value +## Gets the current [param Tween Transition] value. +func get_tween_transition() -> int: + return tween_resource.transition + +## Assigns a new [param Tween Ease] to the [member tween_resource] value.[br] +## The duration value is in seconds. +func set_tween_ease(value: int) -> void: + tween_resource.ease = value +## Gets the current [param Tween Ease] value. +func get_tween_ease() -> int: + return tween_resource.ease + +## Sets the [param PhantomCamera3D] active state[br] +## [b][color=yellow]Important:[/color][/b] This value can only be changed +## from the [PhantomCameraHost] script. +func set_is_active(node: Node, value: bool) -> void: + if node is PhantomCameraHost: + _is_active = value + if value: + _should_follow_checker() + else: + printerr("PCams can only be set from the PhantomCameraHost") +## Gets current active state of the [param PhantomCamera3D]. +## If it returns true, it means the [param PhantomCamera3D] is what the +## [param Camera3D] is currently following. +func is_active() -> bool: + return _is_active + + +## Enables or disables the [member tween_on_load]. +func set_tween_on_load(value: bool) -> void: + tween_on_load = value +## Gets the current [member tween_on_load] value. +func get_tween_on_load() -> bool: + return tween_on_load + + +## Sets the [member host_layers] value. +func set_host_layers(value: int) -> void: + host_layers = value + if is_instance_valid(_phantom_camera_manager): + _phantom_camera_manager.pcam_host_layer_changed.emit(self) + +## Enables or disables a given layer of [member host_layers]. +func set_host_layers_value(layer: int, value: bool) -> void: + host_layers = _set_layer(host_layers, layer, value) + +## Gets the current [member host_layers]. +func get_host_layers() -> int: + return host_layers + + +## Gets the current follow mode as an enum int based on [member FollowMode] enum.[br] +## [b]Note:[/b] Setting [member follow_mode] has purposely not been added. +## A separate [param PhantomCamera3D] instance should be used instead. +func get_follow_mode() -> int: + return follow_mode + + +## Assigns a new [Node3D] as the [member follow_target]. +func set_follow_target(value: Node3D) -> void: + if follow_mode == FollowMode.NONE or follow_mode == FollowMode.GROUP: return + if follow_target == value: return + follow_target = value + _follow_target_physics_based = false + if is_instance_valid(value): + if follow_mode == FollowMode.PATH: + if is_instance_valid(follow_path): + _should_follow = true + else: + _should_follow = false + else: + _should_follow = true + _check_physics_body(value) + if follow_mode == FollowMode.THIRD_PERSON and is_instance_valid(_follow_spring_arm): + _follow_spring_arm.add_excluded_object(follow_target) + if not follow_target.tree_exiting.is_connected(_follow_target_tree_exiting): + follow_target.tree_exiting.connect(_follow_target_tree_exiting.bind(follow_target)) + else: + if not follow_mode == FollowMode.GROUP: + _should_follow = false + follow_target_changed.emit() + notify_property_list_changed() +## Removes the current [Node3D] [member follow_target]. +func erase_follow_target() -> void: + follow_target = null +## Gets the current Node3D target. +func get_follow_target() -> Node3D: + return follow_target + + +## Assigns a new [Path3D] to the [member follow_path] property. +func set_follow_path(value: Path3D) -> void: + follow_path = value + if is_instance_valid(follow_path): + _should_follow_checker() + else: + _should_follow = false + +## Erases the current [Path3D] from [member follow_path] property. +func erase_follow_path() -> void: + follow_path = null + +## Gets the current [Path3D] from the [member follow_path] property. +func get_follow_path() -> Path3D: + return follow_path + + +## Assigns a new [param follow_targets] array value. +func set_follow_targets(value: Array[Node3D]) -> void: + if not follow_mode == FollowMode.GROUP: return + if follow_targets == value: return + follow_targets = value + _follow_targets_size_check() + + +## Adds a single [Node3D] to [member follow_targets] array. +func append_follow_targets(value: Node3D) -> void: + if not is_instance_valid(value): + printerr(value, " is not a valid Node3D instance") + return + + if not follow_targets.has(value): + follow_targets.append(value) + _follow_targets_size_check() + else: + printerr(value, " is already part of Follow Group") + +## Adds an Array of type [Node3D] to [member follow_targets] array. +func append_follow_targets_array(value: Array[Node3D]) -> void: + for target in value: + if not is_instance_valid(target): continue + if not follow_targets.has(target): + follow_targets.append(target) + _follow_targets_size_check() + else: + printerr(value, " is already part of Follow Group") + +## Removes [Node3D] from [member follow_targets]. +func erase_follow_targets(value: Node3D) -> void: + follow_targets.erase(value) + _follow_targets_size_check() + + +## Gets all [Node3D] from [follow_targets]. +func get_follow_targets() -> Array[Node3D]: + return follow_targets + + +## Assigns a new [param Vector3] for the [param follow_offset] property. +func set_follow_offset(value: Vector3) -> void: + var temp_offset: Vector3 = follow_offset + follow_offset = value + + if follow_axis_lock != FollowLockAxis.NONE: + temp_offset = temp_offset - value + match value: + FollowLockAxis.X: + _follow_axis_lock_value.x = _transform_output.origin.x + temp_offset.x + FollowLockAxis.Y: + _follow_axis_lock_value.y = _transform_output.origin.y + temp_offset.y + FollowLockAxis.Z: + _follow_axis_lock_value.z = _transform_output.origin.z + temp_offset.z + FollowLockAxis.XY: + _follow_axis_lock_value.x = _transform_output.origin.x + temp_offset.x + _follow_axis_lock_value.y = _transform_output.origin.y + temp_offset.y + FollowLockAxis.XZ: + _follow_axis_lock_value.x = _transform_output.origin.x + temp_offset.x + _follow_axis_lock_value.z = _transform_output.origin.z + temp_offset.z + FollowLockAxis.YZ: + _follow_axis_lock_value.y = _transform_output.origin.y + temp_offset.y + _follow_axis_lock_value.z = _transform_output.origin.z + temp_offset.z + FollowLockAxis.XYZ: + _follow_axis_lock_value.x = _transform_output.origin.x + temp_offset.x + _follow_axis_lock_value.y = _transform_output.origin.y + temp_offset.y + _follow_axis_lock_value.z = _transform_output.origin.z + temp_offset.z + +## Gets the current [param Vector3] for the [param follow_offset] property. +func get_follow_offset() -> Vector3: + return follow_offset + + +## Enables or disables [member follow_damping]. +func set_follow_damping(value: bool) -> void: + follow_damping = value + notify_property_list_changed() + +## Gets the currents [member follow_damping] property. +func get_follow_damping() -> bool: + return follow_damping + + +## Assigns new [member follow_damping_value] value. +func set_follow_damping_value(value: Vector3) -> void: + ## TODO - Should be using @export_range once minimum version support is Godot 4.3 + if value.x < 0: value.x = 0 + elif value.y < 0: value.y = 0 + elif value.z < 0: value.z = 0 + follow_damping_value = value + +## Gets the currents [member follow_damping_value] value. +func get_follow_damping_value() -> Vector3: + return follow_damping_value + + +## Assigns a new [member follow_distance] value. +func set_follow_distance(value: float) -> void: + follow_distance = value + +## Gets [member follow_distance] value. +func get_follow_distance() -> float: + return follow_distance + + +## Enables or disables [member auto_follow_distance] when using Group Follow. +func set_auto_follow_distance(value: bool) -> void: + auto_follow_distance = value + notify_property_list_changed() + +## Gets [member auto_follow_distance] state. +func get_auto_follow_distance() -> bool: + return auto_follow_distance + + +## Assigns new [member auto_follow_distance_min] value. +func set_auto_follow_distance_min(value: float) -> void: + auto_follow_distance_min = value + +## Gets [member auto_follow_distance_min] value. +func get_auto_follow_distance_min() -> float: + return auto_follow_distance_min + + +## Assigns new [member auto_follow_distance_max] value. +func set_auto_follow_distance_max(value: float) -> void: + auto_follow_distance_max = value +## Gets [member auto_follow_distance_max] value. +func get_auto_follow_distance_max() -> float: + return auto_follow_distance_max + + +## Assigns new [member auto_follow_distance_divisor] value. +func set_auto_follow_distance_divisor(value: float) -> void: + auto_follow_distance_divisor = value + +## Gets [member auto_follow_distance_divisor] value. +func get_auto_follow_distance_divisor() -> float: + return auto_follow_distance_divisor + + +## Assigns new rotation (in radians) value to [SpringArm3D] for +## [param ThirdPerson] [enum FollowMode]. +func set_third_person_rotation(value: Vector3) -> void: + if not _is_third_person_follow: + printerr("Follow Mode is not set to Third Person") + return + _follow_spring_arm.rotation = value + +## Gets the rotation value (in radians) from the [SpringArm3D] for +## [param ThirdPerson] [enum FollowMode]. +func get_third_person_rotation() -> Vector3: + if not _is_third_person_follow: + printerr("Follow Mode is not set to Third Person") + return Vector3.ZERO + return _follow_spring_arm.rotation + + +## Assigns new rotation (in degrees) value to [SpringArm3D] for +## [param ThirdPerson] [enum FollowMode]. +func set_third_person_rotation_degrees(value: Vector3) -> void: + if not _is_third_person_follow: + printerr("Follow Mode is not set to Third Person") + return + _follow_spring_arm.rotation_degrees = value + +## Gets the rotation value (in degrees) from the [SpringArm3D] for +## [param ThirdPerson] [enum FollowMode]. +func get_third_person_rotation_degrees() -> Vector3: + if not _is_third_person_follow: + printerr("Follow Mode is not set to Third Person") + return Vector3.ZERO + return _follow_spring_arm.rotation_degrees + + +## Assigns new [Quaternion] value to [SpringArm3D] for [param ThirdPerson] +## [enum FollowMode]. +func set_third_person_quaternion(value: Quaternion) -> void: + if not _is_third_person_follow: + printerr("Follow Mode is not set to Third Person") + return + _follow_spring_arm.quaternion = value + +## Gets the [Quaternion] value of the [SpringArm3D] for [param ThirdPerson] +## [enum Follow mode]. +func get_third_person_quaternion() -> Quaternion: + if not _is_third_person_follow: + printerr("Follow Mode is not set to Third Person") + return Quaternion.IDENTITY + return _follow_spring_arm.quaternion + + +## Assigns a new [member set_vertical_rotation_offset] value. +func set_vertical_rotation_offset(value: float) -> void: + vertical_rotation_offset = value + +## Gets the [member vertical_rotation] value. +func get_vertical_rotation_offset() -> float: + return vertical_rotation_offset + + +func set_horizontal_rotation_offset(value: float) -> void: + horizontal_rotation_offset = value + +## Gets the [member horizontal_rotation] value. +func get_horizontal_rotation_offset() -> float: + return horizontal_rotation_offset + + +## Assigns a new ThirdPerson [member SpringArm3D.length] value. +func set_spring_length(value: float) -> void: + follow_distance = value + if not is_instance_valid(_follow_spring_arm): return + _follow_spring_arm.spring_length = value + +## Gets the [member SpringArm3D.length] +## from a [param ThirdPerson] [enum follow_mode] instance. +func get_spring_length() -> float: + return follow_distance + + +## Assigns a new [member collision_mask] to the [SpringArm3D] when [enum FollowMode] +## is set to [param ThirdPerson]. +func set_collision_mask(value: int) -> void: + collision_mask = value + if not is_instance_valid(_follow_spring_arm): return + _follow_spring_arm.collision_mask = collision_mask + +## Enables or disables a specific [member collision_mask] layer for the +## [SpringArm3D] when [enum FollowMode] is set to [param ThirdPerson]. +func set_collision_mask_value(value: int, enabled: bool) -> void: + collision_mask = _set_layer(collision_mask, value, enabled) + if not is_instance_valid(_follow_spring_arm): return + _follow_spring_arm.collision_mask = collision_mask + +## Gets [member collision_mask] from the [SpringArm3D] when [enum FollowMode] +## is set to [param ThirdPerson]. +func get_collision_mask() -> int: + return collision_mask + + +## Assigns a new [SpringArm3D.shape] when [enum FollowMode] +## is set to [param ThirdPerson]. +func set_shape(value: Shape3D) -> void: + shape = value + if not is_instance_valid(_follow_spring_arm): return + _follow_spring_arm.shape = shape + +## Gets [param ThirdPerson] [member SpringArm3D.shape] value. +func get_shape() -> Shape3D: + return shape + + +## Assigns a new [member SpringArm3D.margin] value when [enum FollowMode] +## is set to [param ThirdPerson]. +func set_margin(value: float) -> void: + margin = value + if not is_instance_valid(_follow_spring_arm): return + _follow_spring_arm.margin = margin + +## Gets the [SpringArm3D.margin] when [enum FollowMode] is set to +## [param ThirdPerson]. +func get_margin() -> float: + return margin + + +## Gets the current [member look_at_mode]. Value is based on [enum LookAtMode] +## enum.[br] +## Note: To set a new [member look_at_mode], a separate [param PhantomCamera3D] should be used. +func get_look_at_mode() -> int: + return look_at_mode + + +## Assigns new [Node3D] as [member look_at_target]. +func set_look_at_target(value: Node3D) -> void: + if look_at_mode == LookAtMode.NONE: return + if look_at_target == value: return + look_at_target = value + if not look_at_mode == LookAtMode.GROUP: + if is_instance_valid(look_at_target): + _should_look_at = true + _check_physics_body(value) + if not look_at_target.tree_exiting.is_connected(_look_at_target_tree_exiting): + look_at_target.tree_exiting.connect(_look_at_target_tree_exiting.bind(look_at_target)) + else: + _should_look_at = false + elif look_at_targets.size() == 0: + _should_look_at = false + + look_at_target_changed.emit() + notify_property_list_changed() + +## Gets current [Node3D] from [member look_at_target] property. +func get_look_at_target() -> Node3D: + return look_at_target + + +## Sets an array of type [Node3D] to [member set_look_at_targets]. +func set_look_at_targets(value: Array[Node3D]) -> void: + if not look_at_mode == LookAtMode.GROUP: return + if look_at_targets == value: return + look_at_targets = value + + _look_at_targets_size_check() + notify_property_list_changed() + +## Appends a [Node3D] to [member look_at_targets] array. +func append_look_at_target(value: Node3D) -> void: + if not is_instance_valid(value): + printerr(value, "is an invalid Node3D instance") + return + + if not look_at_targets.has(value): + look_at_targets.append(value) + _look_at_targets_size_check() + else: + printerr(value, " is already part of Look At Group") + + +## Appends an array of type [Node3D] to [member look_at_targets] array. +func append_look_at_targets_array(value: Array[Node3D]) -> void: + for val in value: + if not is_instance_valid(val): continue + if not look_at_targets.has(val): + look_at_targets.append(val) + _look_at_targets_size_check() + else: + printerr(val, " is already part of Look At Group") + +## Removes [Node3D] from [member look_at_targets] array. +func erase_look_at_targets(value: Node3D) -> void: + if look_at_targets.has(value): + look_at_targets.erase(value) + _look_at_targets_size_check() + else: + printerr(value, " is not part of Look At Group") + + +## Removes [Node3D] from [member look_at_targets] array. [br] +## @deprecated: Use [member erase_look_at_targets] instead. +func erase_look_at_targets_member(value: Node3D) -> void: + printerr("erase_look_at_targets_member is deprecated, use erase_look_at_targets instead") + erase_look_at_targets(value) + +## Gets all the [Node3D] instances in [member look_at_targets]. +func get_look_at_targets() -> Array[Node3D]: + return look_at_targets + + +## Assigns a new [Vector3] to the [member look_at_offset] value. +func set_look_at_offset(value: Vector3) -> void: + look_at_offset = value + +## Gets the current [member look_at_offset] value. +func get_look_at_offset() -> Vector3: + return look_at_offset + + +## Enables or disables [member look_at_damping]. +func set_look_at_damping(value: bool) -> void: + look_at_damping = value + notify_property_list_changed() + +## Gets the currents [member look_at_damping] property. +func get_look_at_damping() -> bool: + return look_at_damping + + +## Assigns new [member look_at_damping_value] value. +func set_look_at_damping_value(value: float) -> void: + look_at_damping_value = value + +## Gets the currents [member look_at_damping_value] value. +func get_look_at_damping_value() -> float: + return look_at_damping_value + +## Assigns the Follow Axis. +func set_follow_axis_lock(value: FollowLockAxis) -> void: + follow_axis_lock = value + + # Wait for the node to be ready before setting lock + if not is_node_ready(): await ready + + # Prevent axis lock from working in the editor + if value != FollowLockAxis.NONE and not Engine.is_editor_hint(): + _follow_axis_is_locked = true + match value: + FollowLockAxis.X: + _follow_axis_lock_value.x = _transform_output.origin.x + FollowLockAxis.Y: + _follow_axis_lock_value.y = _transform_output.origin.y + FollowLockAxis.Z: + _follow_axis_lock_value.z = _transform_output.origin.z + FollowLockAxis.XY: + _follow_axis_lock_value.x = _transform_output.origin.x + _follow_axis_lock_value.y = _transform_output.origin.y + FollowLockAxis.XZ: + _follow_axis_lock_value.x = _transform_output.origin.x + _follow_axis_lock_value.z = _transform_output.origin.z + FollowLockAxis.YZ: + _follow_axis_lock_value.y = _transform_output.origin.y + _follow_axis_lock_value.z = _transform_output.origin.z + FollowLockAxis.XYZ: + _follow_axis_lock_value.x = _transform_output.origin.x + _follow_axis_lock_value.y = _transform_output.origin.y + _follow_axis_lock_value.z = _transform_output.origin.z + else: + _follow_axis_is_locked = false + +## Gets the current [member follow_axis_lock] property. Value is based on [enum FollowLockAxis] enum. +func get_follow_axis_lock() -> FollowLockAxis: + return follow_axis_lock + + +## Sets the [member up] value. +func set_up(value: Vector3) -> void: + if value == Vector3.ZERO: + value = Vector3.UP + push_warning("Up value cannot be (0, 0, 0), resetting to (0, 1, 0).") + + up = value + if not _has_up_target: + _up = value + +## Gets the [member up] value. +func get_up() -> Vector3: + return up + + +## Sets the [member up_target]. +func set_up_target(value: Node3D) -> void: + up_target = value + if is_instance_valid(value): + _has_up_target = true + if not value.tree_exiting.is_connected(_up_target_tree_exiting): + value.tree_exiting.connect(_up_target_tree_exiting) + else: + _has_up_target = false + _up = up + notify_property_list_changed() + +## Gets the [member up_target]. +func get_up_target() -> Node3D: + return up_target + + +## Sets a [PhantomCameraNoise3D] resource. +func set_noise(value: PhantomCameraNoise3D) -> void: + noise = value + if value != null: + _has_noise_resource = true + noise.set_trauma(1) + else: + _has_noise_resource = false + _transform_noise = Transform3D() + +func get_noise() -> PhantomCameraNoise3D: + return noise + +func has_noise_resource() -> bool: + return _has_noise_resource + + +## 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 [member noise_emitter_layer]. +func set_noise_emitter_layer_value(value: int, enabled: bool) -> void: + noise_emitter_layer = _set_layer(noise_emitter_layer, value, enabled) + +## Returns the [member noise_emitter_layer]. +func get_noise_emitter_layer() -> int: + return noise_emitter_layer + + +## Sets [member inactive_update_mode] property. +func set_inactive_update_mode(value: int) -> void: + inactive_update_mode = value + +## Gets [member inactive_update_mode] property. +func get_inactive_update_mode() -> int: + return inactive_update_mode + + +## Assigns a [Camera3DResource]. +func set_camera_3d_resource(value: Camera3DResource) -> void: + camera_3d_resource = value + camera_3d_resource_changed.emit() + if value: + if not camera_3d_resource.changed.is_connected(_camera_resource_changed): + camera_3d_resource.changed.connect(_camera_resource_changed) + +## Gets the [Camera3DResource]. +func get_camera_3d_resource() -> Camera3DResource: + return camera_3d_resource + + +func set_keep_aspect(value: int) -> void: + if not camera_3d_resource: + printerr("Can't assign a keep_aspect value. No Camera3DResource assigned to ", name) + return + keep_aspect = value + camera_3d_resource_property_changed.emit("keep_aspect", value) + +func get_keep_aspect() -> Variant: + if not camera_3d_resource: return null + return camera_3d_resource.keep_aspect + + +## Assigns a new [member Camera3D.cull_mask] value.[br] +## [b]Note:[/b] This will override and make the [param Camera3DResource] unique to +## this [param PhantomCamera3D]. +func set_cull_mask(value: int) -> void: + if not camera_3d_resource: + printerr("Can't assign a cull_mask value. No Camera3DResource assigned to ", name) + return + camera_3d_resource.cull_mask = value + camera_3d_resource_property_changed.emit("cull_mask", value) + +## Enables or disables a specific [member Camera3D.cull_mask] layer.[br] +## [b]Note:[/b] This will override and make the [param Camera3DResource] unique to +## this [param PhantomCamera3D]. +func set_cull_mask_value(layer_number: int, value: bool) -> void: + if not camera_3d_resource: + printerr("Can't assign a cull_mask value. No Camera3DResource assigned to ", name) + return + var mask: int = _set_layer(get_cull_mask(), layer_number, value) + camera_3d_resource.cull_mask = mask + camera_3d_resource_property_changed.emit("cull_mask", mask) + +## Gets the [member Camera3D.cull_mask] value assigned to the [Camera3DResource]. +func get_cull_mask() -> Variant: + if not camera_3d_resource: return null + return camera_3d_resource.cull_mask + + +## Assigns a new [Environment] resource to the [Camera3DResource]. +func set_environment(value: Environment) -> void: + environment = value + camera_3d_resource_property_changed.emit("environment", value) + +## Gets the [Camera3D.environment] value assigned to the [Camera3DResource]. +func get_environment() -> Environment: + return environment + + +## Assigns a new [CameraAttributes] resource to the [Camera3DResource]. +func set_attributes(value: CameraAttributes) -> void: + attributes = value + camera_3d_resource_property_changed.emit("attributes", value) + +## Gets the [Camera3D.attributes] value assigned to the [Camera3DResource]. +func get_attributes() -> CameraAttributes: + return attributes + + +## Assigns a new [member Camera3D.h_offset] value.[br] +## [b]Note:[/b] This will override and make the [param Camera3DResource] unique to +## this [param PhantomCamera3D]. +func set_h_offset(value: float) -> void: + if not camera_3d_resource: + printerr("Can't assign a h_offset value. No Camera3DResource assigned to ", name) + return + camera_3d_resource.h_offset = value + camera_3d_resource_property_changed.emit("h_offset", value) + +## Gets the [member Camera3D.h_offset] value assigned to the [param Camera3DResource]. +func get_h_offset() -> Variant: + if not camera_3d_resource: return null + return camera_3d_resource.h_offset + + +## Assigns a new [Camera3D.v_offset] value.[br] +## [b]Note:[/b] This will override and make the [param Camera3DResource] unique to +## this [param PhantomCamera3D]. +func set_v_offset(value: float) -> void: + if not camera_3d_resource: + printerr("Can't assign a v_offset value. No Camera3DResource assigned to ", name) + return + camera_3d_resource.v_offset = value + camera_3d_resource_property_changed.emit("v_offset", value) + +## Gets the [member Camera3D.v_offset] value assigned to the [param Camera3DResource]. +func get_v_offset() -> Variant: + if not camera_3d_resource: return null + return camera_3d_resource.v_offset + + +## Assigns a new [Camera3D.projection] value.[br] +## [b]Note:[/b] This will override and make the [param Camera3DResource] unique to +## this [param PhantomCamera3D]. +func set_projection(value: int) -> void: + if not camera_3d_resource: + printerr("Can't assign a projection value. No Camera3DResource assigned to ", name) + return + camera_3d_resource.projection = value + camera_3d_resource_property_changed.emit("projection", value) + +## Gets the [member Camera3D.projection] value assigned to the [param Camera3DResource]. +func get_projection() -> Variant: + if not camera_3d_resource: return null + return camera_3d_resource.projection + + +## Assigns a new [member Camera3D.fov] value.[br] +## [b]Note:[/b] This will override and make the [param Camera3DResource] unique to +## this [param PhantomCamera3D]. +func set_fov(value: float) -> void: + if not camera_3d_resource: + printerr("Can't assign a fov value. No Camera3DResource assigned to ", name) + return + camera_3d_resource.fov = value + camera_3d_resource_property_changed.emit("fov", value) + +## Gets the [member Camera3D.fov] value assigned to the [param Camera3DResource]. +func get_fov() -> Variant: + if not camera_3d_resource: return null + return camera_3d_resource.fov + + +## Assigns a new [member Camera3D.size] value.[br] +## [b]Note:[/b] This will override and make the [param Camera3DResource] unique to +## this [param PhantomCamera3D]. +func set_size(value: float) -> void: + if not camera_3d_resource: + printerr("Can't assign a size value. No Camera3DResource assigned to ", name) + return + camera_3d_resource.size = value + camera_3d_resource_property_changed.emit("size", value) + +## Gets the [member Camera3D.size] value assigned to the [param Camera3DResource]. +func get_size() -> Variant: + if not camera_3d_resource: return null + return camera_3d_resource.size + + +## Assigns a new [member Camera3D.frustum_offset] value.[br] +## [b]Note:[/b] This will override and make the [param Camera3DResource] unique to +## this [param PhantomCamera3D]. +func set_frustum_offset(value: Vector2) -> void: + if not camera_3d_resource: + printerr("Can't assign a frustum_offset value. No Camera3DResource assigned to ", name) + return + camera_3d_resource.frustum_offset = value + camera_3d_resource_property_changed.emit("frustum_offset", value) + +## Gets the [member Camera3D.frustum_offset] value assigned to the [param Camera3DResource]. +func get_frustum_offset() -> Variant: + if not camera_3d_resource: return null + return camera_3d_resource.frustum_offset + + +## Assigns a new [member Camera3D.near] value.[br] +## [b]Note:[/b] This will override and make the [param Camera3DResource] unique to +## this [param PhantomCamera3D]. +func set_near(value: float) -> void: + if not camera_3d_resource: + printerr("Can't assign a near value. No Camera3DResource assigned to ", name) + return + camera_3d_resource.near = value + camera_3d_resource_property_changed.emit("near", value) + +## Gets the [member Camera3D.near] value assigned to the [param Camera3DResource]. +func get_near() -> Variant: + if not camera_3d_resource: return null + return camera_3d_resource.near + + +## Assigns a new [member Camera3D.far] value.[br] +## [b]Note:[/b] This will override and make the [param Camera3DResource] unique to +## this [param PhantomCamera3D]. +func set_far(value: float) -> void: + if not camera_3d_resource: + printerr("Can't assign a far value. No Camera3DResource assigned to ", name) + return + camera_3d_resource.far = value + camera_3d_resource_property_changed.emit("far", value) + +## Gets the [member Camera3D.far] value assigned to the [param Camera3DResource]. +func get_far() -> Variant: + if not camera_3d_resource: return null + return camera_3d_resource.far + + +func get_follow_target_physics_based() -> bool: + return _follow_target_physics_based + + +func get_class() -> String: + return "PhantomCamera3D" + + +func is_class(value) -> bool: + return value == "PhantomCamera3D" + +#endregion diff --git a/addons/phantom_camera/scripts/phantom_camera/phantom_camera_3d.gd.uid b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_3d.gd.uid new file mode 100644 index 0000000..31a2fc9 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_3d.gd.uid @@ -0,0 +1 @@ +uid://csjccrhj5wnx7 diff --git a/addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd new file mode 100644 index 0000000..43f6cf4 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd @@ -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 diff --git a/addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd.uid b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd.uid new file mode 100644 index 0000000..b1e3789 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd.uid @@ -0,0 +1 @@ +uid://dn74j5b5hdxu diff --git a/addons/phantom_camera/scripts/phantom_camera/phantom_camera_noise_emitter_2d.gd b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_noise_emitter_2d.gd new file mode 100644 index 0000000..7306810 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_noise_emitter_2d.gd @@ -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 [member noise_emitter_layer]. +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 diff --git a/addons/phantom_camera/scripts/phantom_camera/phantom_camera_noise_emitter_2d.gd.uid b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_noise_emitter_2d.gd.uid new file mode 100644 index 0000000..19dbc96 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_noise_emitter_2d.gd.uid @@ -0,0 +1 @@ +uid://bhd4nuiu23e7l diff --git a/addons/phantom_camera/scripts/phantom_camera/phantom_camera_noise_emitter_3d.gd b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_noise_emitter_3d.gd new file mode 100644 index 0000000..cd6c634 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_noise_emitter_3d.gd @@ -0,0 +1,265 @@ +@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 [member noise_emitter_layer]. +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 diff --git a/addons/phantom_camera/scripts/phantom_camera/phantom_camera_noise_emitter_3d.gd.uid b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_noise_emitter_3d.gd.uid new file mode 100644 index 0000000..aa7e880 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/phantom_camera_noise_emitter_3d.gd.uid @@ -0,0 +1 @@ +uid://ccmiitq0sdh7j diff --git a/addons/phantom_camera/scripts/phantom_camera_host/PhantomCameraHost.cs b/addons/phantom_camera/scripts/phantom_camera_host/PhantomCameraHost.cs new file mode 100644 index 0000000..be211bd --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera_host/PhantomCameraHost.cs @@ -0,0 +1,128 @@ +using Godot; + +#nullable enable + +namespace PhantomCamera; + +public enum InterpolationMode +{ + Auto, + Idle, + Physics +} + +public static class PhantomCameraHostExtensions +{ + public static PhantomCameraHost AsPhantomCameraHost(this Node node) + { + return new PhantomCameraHost(node); + } +} + +public class PhantomCameraHost() +{ + public Node Node { get; } = null!; + + public PhantomCameraHost(Node node) : this() + { + Node = node; + + _callablePCamBecameActive = Callable.From(pCam => PCamBecameActive?.Invoke(pCam)); + _callablePCamBecameInactive = Callable.From(pCam => PCamBecameInactive?.Invoke(pCam)); + + Node.Connect(SignalName.PCamBecameActive, _callablePCamBecameActive); + Node.Connect(SignalName.PCamBecameInactive, _callablePCamBecameInactive); + } + + ~PhantomCameraHost() + { + Node.Disconnect(SignalName.PCamBecameActive, _callablePCamBecameActive); + Node.Disconnect(SignalName.PCamBecameInactive, _callablePCamBecameInactive); + } + + public delegate void PCamBecameActiveEventHandler(Node pCam); + public delegate void PCamBecameInactiveEventHandler(Node pCam); + + public event PCamBecameActiveEventHandler? PCamBecameActive; + public event PCamBecameInactiveEventHandler? PCamBecameInactive; + + + private readonly Callable _callablePCamBecameActive; + private readonly Callable _callablePCamBecameInactive; + // For when Godot becomes the minimum version + // public InterpolationMode InterpolationMode + // { + // get => (InterpolationMode)(int)Node.Call(MethodName.GetInterpolationMode); + // set => Node.Call(MethodName.SetInterpolationMode, (int)value); + // } + + public int HostLayers + { + get => (int)Node.Call(PhantomCamera.MethodName.GetHostLayers); + set => Node.Call(PhantomCamera.MethodName.SetHostLayers, value); + } + + public void SetHostLayersValue(int layer, bool value) => Node.Call(MethodName.SetHostLayersValue, layer, value); + + public Camera2D? Camera2D => (Camera2D?)Node.Get(PropertyName.Camera2D); + + public Camera3D? Camera3D => (Camera3D?)Node.Get(PropertyName.Camera3D); + + public InterpolationMode InterpolationMode + { + get => (InterpolationMode)(int)Node.Call(MethodName.GetInterpolationMode); + set => Node.Call(MethodName.SetInterpolationMode, (int)value); + } + + public bool TriggerPhantomCameraTween => (bool)Node.Call(MethodName.GetTriggerPhantomCameraTween); + + public ActivePhantomCameraQueryResult? GetActivePhantomCamera() + { + var result = Node.Call(MethodName.GetActivePhantomCamera); + return result.VariantType == Variant.Type.Nil ? null : new ActivePhantomCameraQueryResult(result.AsGodotObject()); + } + + public static class PropertyName + { + public const string Camera2D = "camera_2d"; + public const string Camera3D = "camera_3d"; + } + + public static class MethodName + { + public const string GetActivePhantomCamera = "get_active_pcam"; + public const string GetTriggerPhantomCameraTween = "get_trigger_pcam_tween"; + + public const string GetInterpolationMode = "get_interpolation_mode"; + public const string SetInterpolationMode = "set_interpolation_mode"; + + public const string SetHostLayersValue = "set_host_layers_value"; + } + + public static class SignalName + { + public const string PCamBecameActive = "pcam_became_active"; + public const string PCamBecameInactive = "pcam_became_inactive"; + } +} + +public class ActivePhantomCameraQueryResult(GodotObject godotObject) +{ + public bool Is2D => godotObject.IsClass("Node2D") || ((Node)godotObject).Name.ToString().Contains("PhantomCamera2D") + || ((Node)godotObject).Name.ToString().Contains("PCam2D") + || ((Node)godotObject).Name.ToString().Contains("2D"); + + public bool Is3D => godotObject.IsClass("Node3D") || ((Node)godotObject).Name.ToString().Contains("PhantomCamera3D") + || ((Node)godotObject).Name.ToString().Contains("PCam3D") + || ((Node)godotObject).Name.ToString().Contains("3D"); + + public PhantomCamera2D? AsPhantomCamera2D() + { + return Is2D ? new PhantomCamera2D(godotObject) : null; + } + + public PhantomCamera3D? AsPhantomCamera3D() + { + return Is3D ? new PhantomCamera3D(godotObject) : null; + } +} diff --git a/addons/phantom_camera/scripts/phantom_camera_host/PhantomCameraHost.cs.uid b/addons/phantom_camera/scripts/phantom_camera_host/PhantomCameraHost.cs.uid new file mode 100644 index 0000000..ad4b4f5 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera_host/PhantomCameraHost.cs.uid @@ -0,0 +1 @@ +uid://cr8brwrls2nn3 diff --git a/addons/phantom_camera/scripts/phantom_camera_host/phantom_camera_host.gd b/addons/phantom_camera/scripts/phantom_camera_host/phantom_camera_host.gd new file mode 100644 index 0000000..8c6308c --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera_host/phantom_camera_host.gd @@ -0,0 +1,1416 @@ +@tool +@icon("res://addons/phantom_camera/icons/phantom_camera_host.svg") +class_name PhantomCameraHost +extends Node + +## Controls a scene's [Camera2D] (2D scenes) and [Camera3D] (3D scenes). +## +## All instantiated [param PhantomCameras] in a scene are assigned to a specific layer, where a +## PhantomCameraHost will react to those that corresponds. It is what determines which [param PhantomCamera] should +## be active. + +#region Constants + +const _constants := preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd") + +#endregion + + +#region Signals + +## Updates the viewfinder [param dead zones] sizes.[br] +## [b]Note:[/b] This is only being used in the editor viewfinder UI. +#signal update_editor_viewfinder +signal viewfinder_update(check_framed_view: bool) +signal viewfinder_disable_dead_zone + +## Used internally to check if the [param PhantomCameraHost] is valid. +## The result will be visible in the viewfinder when multiple instances are present. +signal has_error() + +## Emitted when a new [param PhantomCamera] becomes active and assigned to this [param PhantomCameraHost]. +signal pcam_became_active(pcam: Node) + +## Emitted when the currently active [param PhantomCamera] goes from being active to inactive. +signal pcam_became_inactive(pcam: Node) + +#endregion + + +#region Enums + +## Dictates whether if [param PhantomCameraHost]'s logic should be called in the physics or idle (process) frames. +enum InterpolationMode { + AUTO = 0, ## Automatically sets the [param Camera]'s logic to run in either physics or idle (process) frames depending on its active [param PhantomCamera]'s [param Follow] / [param Look At] Target + IDLE = 1, ## Always run the [param Camera] logic in idle (process) frames + PHYSICS = 2, ## Always run the [param Camera] logic in physics frames +} + +#endregion + + +#region Public Variables + +## Determines which [PhantomCamera2D] / [PhantomCamera3D] nodes this [param PhantomCameraHost] should recognise. +## At least one corresponding layer needs to be set on the [param PhantomCamera] for the [param PhantomCameraHost] node to work. +@export_flags_2d_render var host_layers: int = 1: + set = set_host_layers, + get = get_host_layers + +## Determines whether the [PhantomCamera2D] / [PhantomCamera3D] nodes this [param PhantomCameraHost] controls should use physics interpolation or not. +@export var interpolation_mode: InterpolationMode = InterpolationMode.AUTO: + set = set_interpolation_mode, + get = get_interpolation_mode + +#endregion + + +#region Private Variables + +var _active_pcam_2d: PhantomCamera2D = null +var _active_pcam_3d: Node = null ## Note: To support disable_3d export templates for 2D projects, this is purposely not strongly typed. +var _active_pcam_priority: int = -1 +var _active_pcam_missing: bool = true +var _active_pcam_has_damping: bool = false +var _follow_target_physics_based: bool = false + +var _prev_active_pcam_2d_transform: Transform2D = Transform2D() +var _prev_active_pcam_3d_transform: Transform3D = Transform3D() + +var _trigger_pcam_tween: bool = false +var _tween_elapsed_time: float = 0 +var _tween_duration: float = 0 +var _tween_is_instant: bool = false + +var _multiple_pcam_hosts: bool = false + +var _is_child_of_camera: bool = false +var _is_2d: bool = false + +var _viewfinder_node: Control = null +var _viewfinder_needed_check: bool = true + +var _camera_zoom: Vector2 = Vector2.ONE + +#region Camera3DResource + +var _prev_cam_attributes: CameraAttributes = null +var _cam_attribute_type: int = 0 # 0 = CameraAttributesPractical, 1 = CameraAttributesPhysical +var _cam_attribute_changed: bool = false +var _cam_attribute_assigned: bool = false + +#region CameraAttributes +var _prev_cam_auto_exposure_scale: float = 0.4 +var _cam_auto_exposure_scale_changed: bool = false + +var _prev_cam_auto_exposure_speed: float = 0.5 +var _cam_auto_exposure_speed_changed: bool = false + +var _prev_cam_exposure_multiplier: float = 1.0 +var _cam_exposure_multiplier_changed: bool = false + +var _prev_cam_exposure_sensitivity: float = 100.0 +var _cam_exposure_sensitivity_changed: bool = false + +#region CameraAttributesPractical +var _prev_cam_exposure_min_sensitivity: float = 0.0 +var _cam_exposure_min_sensitivity_changed: bool = false + +var _prev_cam_exposure_max_sensitivity: float = 800.0 +var _cam_exposure_max_sensitivity_changed: bool = false + +var _prev_cam_dof_blur_amount: float = 0.1 +var _cam_dof_blur_amount_changed: bool = false + +var _cam_dof_blur_far_distance_default: float = 10 +var _prev_cam_dof_blur_far_distance: float = _cam_dof_blur_far_distance_default +var _cam_dof_blur_far_distance_changed: bool = false + +var _cam_dof_blur_far_transition_default: float = 5 +var _prev_cam_dof_blur_far_transition: float = _cam_dof_blur_far_transition_default +var _cam_dof_blur_far_transition_changed: bool = false + +var _cam_dof_blur_near_distance_default: float = 2 +var _prev_cam_dof_blur_near_distance: float = _cam_dof_blur_near_distance_default +var _cam_dof_blur_near_distance_changed: bool = false + +var _cam_dof_blur_near_transition_default: float = 1 +var _prev_cam_dof_blur_near_transition: float = _cam_dof_blur_near_transition_default +var _cam_dof_blur_near_transition_changed: bool = false +#endregion + +#region CameraAttributesPhysical +var _prev_cam_exposure_min_exposure_value: float = 10.0 +var _cam_exposure_min_exposure_value_changed: bool = false + +var _prev_cam_exposure_max_exposure_value: float = -8.0 +var _cam_exposure_max_exposure_value_changed: bool = false + +var _prev_cam_exposure_aperture: float = 16.0 +var _cam_exposure_aperture_changed: bool = false + +var _prev_cam_exposure_shutter_speed: float = 100.0 +var _cam_exposure_shutter_speed_changed: bool = false + +var _prev_cam_frustum_far: float = 4000.0 +var _cam_frustum_far_changed: bool = false + +var _prev_cam_frustum_focal_length: float = 35.0 +var _cam_frustum_focal_length_changed: bool = false + +var _prev_cam_frustum_near: float = 0.05 +var _cam_frustum_near_changed: bool = false + +var _prev_cam_frustum_focus_distance: float = 10.0 +var _cam_frustum_focus_distance_changed: bool = false + +#endregion + +var _prev_cam_h_offset: float = 0 +var _cam_h_offset_changed: bool = false + +var _prev_cam_v_offset: float = 0 +var _cam_v_offset_changed: bool = false + +var _prev_cam_fov: float = 75 +var _cam_fov_changed: bool = false + +var _prev_cam_size: float = 1 +var _cam_size_changed: bool = false + +var _prev_cam_frustum_offset: Vector2 = Vector2.ZERO +var _cam_frustum_offset_changed: bool = false + +var _prev_cam_near: float = 0.05 +var _cam_near_changed: bool = false + +var _prev_cam_far: float = 4000 +var _cam_far_changed: bool = false + +#endregion + +var _active_pcam_2d_glob_transform: Transform2D = Transform2D() +var _active_pcam_3d_glob_transform: Transform3D = Transform3D() + +var _has_noise_emitted: bool = false +var _reset_noise_offset_2d: bool = false +var _noise_emitted_output_2d: Transform2D = Transform2D() +var _noise_emitted_output_3d: Transform3D = Transform3D() + +#endregion + +# NOTE - Temp solution until Godot has better plugin autoload recognition out-of-the-box. +var _phantom_camera_manager: Node = null + +#region Public Variables + +var show_warning: bool = false + +## For 2D scenes, is the [Camera2D] instance the [param PhantomCameraHost] controls. +var camera_2d: Camera2D = null + +## For 3D scenes, is the [Camera3D] instance the [param PhantomCameraHost] controls. +var camera_3d: Node = null ## Note: To support disable_3d export templates for 2D projects, this is purposely not strongly typed. + +#endregion + +#region Private Functions + +## TBD - For when Godot 4.3 becomes a minimum version +#func _validate_property(property: Dictionary) -> void: + #if property.name == "interpolation_mode" and get_parent() is Node3D: + #property.usage = PROPERTY_USAGE_NO_EDITOR + + +func _get_configuration_warnings() -> PackedStringArray: + var parent: Node = get_parent() + var first_pcam_host_child: PhantomCameraHost + + if _is_2d: + if not parent is Camera2D: + show_warning = true + has_error.emit() + return["Needs to be a child of a Camera2D in order to work."] + else: + if not parent.is_class("Camera3D"): ## Note: To support disable_3d export templates for 2D projects, this is purposely not strongly typed. + show_warning = true + has_error.emit() + return["Needs to be a child of a Camera3D in order to work."] + + for child in parent.get_children(): + if not child is PhantomCameraHost: continue + if not is_instance_valid(first_pcam_host_child): + first_pcam_host_child = child + continue + elif not first_pcam_host_child == self: + show_warning = true + has_error.emit() + return["Only the first PhantomCameraHost child will be used."] + child.update_configuration_warnings() + + show_warning = false + has_error.emit() + return[] + + +func _enter_tree() -> void: + var parent: Node = get_parent() + if parent is Camera2D or parent.is_class("Camera3D"): ## Note: To support disable_3d export templates for 2D projects, this is purposely not strongly typed. + _phantom_camera_manager = get_tree().root.get_node(_constants.PCAM_MANAGER_NODE_NAME) + _phantom_camera_manager.pcam_host_added(self) + + _is_child_of_camera = true + if parent is Camera2D: + _is_2d = true + camera_2d = parent + ## Force applies position smoothing to be disabled + ## This is to prevent overlap with the interpolation of the PCam2D. + camera_2d.set_position_smoothing_enabled(false) + else: + _is_2d = false + camera_3d = parent + + if _is_2d: + if not _phantom_camera_manager.get_phantom_camera_2ds().is_empty(): + for pcam in _phantom_camera_manager.get_phantom_camera_2ds(): + _pcam_added_to_scene(pcam) + + if not _phantom_camera_manager.limit_2d_changed.is_connected(_update_limit_2d): + _phantom_camera_manager.limit_2d_changed.connect(_update_limit_2d) + if not _phantom_camera_manager.draw_limit_2d.is_connected(_draw_limit_2d): + _phantom_camera_manager.draw_limit_2d.connect(_draw_limit_2d) + + else: + if not _phantom_camera_manager.get_phantom_camera_3ds().is_empty(): + for pcam in _phantom_camera_manager.get_phantom_camera_3ds(): + _pcam_added_to_scene(pcam) + + +func _exit_tree() -> void: + if is_instance_valid(_phantom_camera_manager): + _phantom_camera_manager.pcam_host_removed(self) + + +func _ready() -> void: + # Waits for the first process tick to finish before initializing any logic + # This should help with avoiding ocassional erratic camera movement upon running a scene + await get_tree().process_frame + + process_priority = 300 + process_physics_priority = 300 + + # PCam Host Signals + if Engine.has_singleton(_constants.PCAM_MANAGER_NODE_NAME): + _phantom_camera_manager = Engine.get_singleton(_constants.PCAM_MANAGER_NODE_NAME) + _phantom_camera_manager.pcam_host_layer_changed.connect(_pcam_host_layer_changed) + + # PCam Signals + _phantom_camera_manager.pcam_added_to_scene.connect(_pcam_added_to_scene) + _phantom_camera_manager.pcam_removed_from_scene.connect(_pcam_removed_from_scene) + + _phantom_camera_manager.pcam_priority_changed.connect(pcam_priority_updated) + _phantom_camera_manager.pcam_priority_override.connect(_pcam_priority_override) + + _phantom_camera_manager.pcam_visibility_changed.connect(_pcam_visibility_changed) + + _phantom_camera_manager.pcam_teleport.connect(_pcam_teleported) + + if _is_2d: + if not _phantom_camera_manager.limit_2d_changed.is_connected(_update_limit_2d): + _phantom_camera_manager.limit_2d_changed.connect(_update_limit_2d) + if not _phantom_camera_manager.draw_limit_2d.is_connected(_draw_limit_2d): + _phantom_camera_manager.draw_limit_2d.connect(_draw_limit_2d) + else: + printerr("Could not find Phantom Camera Manager singleton") + printerr("Make sure the addon is enable or that the singleton hasn't been disabled inside Project Settings / Globals") + + _find_pcam_with_highest_priority() + + if _is_2d: + camera_2d.offset = Vector2.ZERO + if not is_instance_valid(_active_pcam_2d): return + _active_pcam_2d_glob_transform = _active_pcam_2d.get_transform_output() + else: + if not is_instance_valid(_active_pcam_3d): return + _active_pcam_3d_glob_transform = _active_pcam_3d.get_transform_output() + + +func _pcam_host_layer_changed(pcam: Node) -> void: + if _pcam_is_in_host_layer(pcam): + _check_pcam_priority(pcam) + else: + if _is_2d: + if _active_pcam_2d == pcam: + _active_pcam_missing = true + _active_pcam_2d = null + _active_pcam_priority = -1 + pcam.set_is_active(self, false) + else: + if _active_pcam_3d == pcam: + _active_pcam_missing = true + _active_pcam_3d = null + _active_pcam_priority = -1 + pcam.set_is_active(self, false) + _find_pcam_with_highest_priority() + + +func _pcam_is_in_host_layer(pcam: Node) -> bool: + if pcam.host_layers & host_layers != 0: return true + return false + + +func _find_pcam_with_highest_priority() -> void: + var pcam_list: Array + if _is_2d: + pcam_list = _phantom_camera_manager.phantom_camera_2ds + else: + pcam_list = _phantom_camera_manager.phantom_camera_3ds + + for pcam in pcam_list: + _check_pcam_priority(pcam) + + +func _check_pcam_priority(pcam: Node) -> void: + if not _pcam_is_in_host_layer(pcam): return + if not pcam.visible: return # Prevents hidden PCams from becoming active + if pcam.get_priority() > _active_pcam_priority: + _assign_new_active_pcam(pcam) + _active_pcam_missing = false + else: + pcam.set_tween_skip(self, false) + + +func _assign_new_active_pcam(pcam: Node) -> void: + # Only checks if the scene tree is still present. + # Prevents a few errors and checks from happening if the scene is exited. + if not is_inside_tree(): return + var no_previous_pcam: bool + if is_instance_valid(_active_pcam_2d) or is_instance_valid(_active_pcam_3d): + if OS.has_feature("debug"): + viewfinder_disable_dead_zone.emit() + + if _is_2d: + _prev_active_pcam_2d_transform = camera_2d.global_transform + _active_pcam_2d.queue_redraw() + _active_pcam_2d.set_is_active(self, false) + _active_pcam_2d.became_inactive.emit() + pcam_became_inactive.emit(_active_pcam_2d) + + if _active_pcam_2d.physics_target_changed.is_connected(_check_pcam_physics): + _active_pcam_2d.physics_target_changed.disconnect(_check_pcam_physics) + + if _active_pcam_2d.noise_emitted.is_connected(_noise_emitted_2d): + _active_pcam_2d.noise_emitted.disconnect(_noise_emitted_2d) + + if _trigger_pcam_tween: + _active_pcam_2d.tween_interrupted.emit(pcam) + else: + _prev_active_pcam_3d_transform = camera_3d.global_transform + _active_pcam_3d.set_is_active(self, false) + _active_pcam_3d.became_inactive.emit() + pcam_became_inactive.emit(_active_pcam_3d) + + if _active_pcam_3d.physics_target_changed.is_connected(_check_pcam_physics): + _active_pcam_3d.physics_target_changed.disconnect(_check_pcam_physics) + + if _active_pcam_3d.noise_emitted.is_connected(_noise_emitted_3d): + _active_pcam_3d.noise_emitted.disconnect(_noise_emitted_3d) + + if _active_pcam_3d.camera_3d_resource_changed.is_connected(_camera_3d_resource_changed): + _active_pcam_3d.camera_3d_resource_changed.disconnect(_camera_3d_resource_changed) + + if _active_pcam_3d.camera_3d_resource_property_changed.is_connected(_camera_3d_resource_property_changed): + _active_pcam_3d.camera_3d_resource_property_changed.disconnect(_camera_3d_resource_property_changed) + + if _trigger_pcam_tween: + _active_pcam_3d.tween_interrupted.emit(pcam) + + if camera_3d.attributes != null: + var _attributes: CameraAttributes = camera_3d.attributes + + _prev_cam_exposure_multiplier = _attributes.exposure_multiplier + _prev_cam_auto_exposure_scale = _attributes.auto_exposure_scale + _prev_cam_auto_exposure_speed = _attributes.auto_exposure_speed + + if camera_3d.attributes is CameraAttributesPractical: + _attributes = _attributes as CameraAttributesPractical + + _prev_cam_dof_blur_amount = _attributes.dof_blur_amount + + if _attributes.dof_blur_far_enabled: + _prev_cam_dof_blur_far_distance = _attributes.dof_blur_far_distance + _prev_cam_dof_blur_far_transition = _attributes.dof_blur_far_transition + else: + _prev_cam_dof_blur_far_distance = _cam_dof_blur_far_distance_default + _prev_cam_dof_blur_far_transition = _cam_dof_blur_far_transition_default + + if _attributes.dof_blur_near_enabled: + _prev_cam_dof_blur_near_distance = _attributes.dof_blur_near_distance + _prev_cam_dof_blur_near_transition = _attributes.dof_blur_near_transition + else: + _prev_cam_dof_blur_near_distance = _cam_dof_blur_near_distance_default + _prev_cam_dof_blur_near_transition = _cam_dof_blur_near_transition_default + + if _attributes.auto_exposure_enabled: + _prev_cam_exposure_max_sensitivity = _attributes.auto_exposure_max_sensitivity + _prev_cam_exposure_min_sensitivity = _attributes.auto_exposure_min_sensitivity + + elif camera_3d.attributes is CameraAttributesPhysical: + _attributes = _attributes as CameraAttributesPhysical + + _prev_cam_frustum_focus_distance = _attributes.frustum_focus_distance + _prev_cam_frustum_focal_length = _attributes.frustum_focal_length + _prev_cam_frustum_far = _attributes.frustum_far + _prev_cam_frustum_near = _attributes.frustum_near + _prev_cam_exposure_aperture = _attributes.exposure_aperture + _prev_cam_exposure_shutter_speed = _attributes.exposure_shutter_speed + + if _attributes.auto_exposure_enabled: + _prev_cam_exposure_min_exposure_value = _attributes.auto_exposure_min_exposure_value + _prev_cam_exposure_max_exposure_value = _attributes.auto_exposure_max_exposure_value + + _prev_cam_h_offset = camera_3d.h_offset + _prev_cam_v_offset = camera_3d.v_offset + _prev_cam_fov = camera_3d.fov + _prev_cam_size = camera_3d.size + _prev_cam_frustum_offset = camera_3d.frustum_offset + _prev_cam_near = camera_3d.near + _prev_cam_far = camera_3d.far + + else: + no_previous_pcam = true + + ## Assign newly active pcam + if _is_2d: + _active_pcam_2d = pcam + _active_pcam_priority = _active_pcam_2d.priority + _active_pcam_has_damping = _active_pcam_2d.follow_damping + _tween_duration = _active_pcam_2d.tween_duration + + if not _active_pcam_2d.physics_target_changed.is_connected(_check_pcam_physics): + _active_pcam_2d.physics_target_changed.connect(_check_pcam_physics) + + if not _active_pcam_2d.noise_emitted.is_connected(_noise_emitted_2d): + _active_pcam_2d.noise_emitted.connect(_noise_emitted_2d) + else: + _active_pcam_3d = pcam + _active_pcam_priority = _active_pcam_3d.priority + _active_pcam_has_damping = _active_pcam_3d.follow_damping + _tween_duration = _active_pcam_3d.tween_duration + + if not Engine.is_editor_hint(): + # Assigns a default shape to SpringArm3D node is none is supplied + if _active_pcam_3d.follow_mode == _active_pcam_3d.FollowMode.THIRD_PERSON: + if not _active_pcam_3d.shape: + + var pyramid_shape_data = Engine.get_singleton("PhysicsServer3D").call("shape_get_data", + camera_3d.get_pyramid_shape_rid() + ) + var shape = ClassDB.instantiate("ConvexPolygonShape3D") + shape.points = pyramid_shape_data + _active_pcam_3d.shape = shape + + if not _active_pcam_3d.physics_target_changed.is_connected(_check_pcam_physics): + _active_pcam_3d.physics_target_changed.connect(_check_pcam_physics) + + if not _active_pcam_3d.noise_emitted.is_connected(_noise_emitted_3d): + _active_pcam_3d.noise_emitted.connect(_noise_emitted_3d) + + if not _active_pcam_3d.camera_3d_resource_changed.is_connected(_camera_3d_resource_changed): + _active_pcam_3d.camera_3d_resource_changed.connect(_camera_3d_resource_changed) + + if not _active_pcam_3d.camera_3d_resource_property_changed.is_connected(_camera_3d_resource_property_changed): + _active_pcam_3d.camera_3d_resource_property_changed.connect(_camera_3d_resource_property_changed) + + # Checks if the Camera3DResource has changed from the previous active PCam3D + if _active_pcam_3d.camera_3d_resource: + # Signal to detect if the Camera3D properties are being changed in the inspector + # This is to prevent accidential misalignment between the Camera3D and Camera3DResource + if Engine.is_editor_hint(): + if not Engine.get_singleton(&"EditorInterface").get_inspector().property_edited.is_connected(_camera_3d_edited): + Engine.get_singleton(&"EditorInterface").get_inspector().property_edited.connect(_camera_3d_edited) + if _prev_cam_h_offset != _active_pcam_3d.h_offset: + _cam_h_offset_changed = true + if _prev_cam_v_offset != _active_pcam_3d.v_offset: + _cam_v_offset_changed = true + if _prev_cam_fov != _active_pcam_3d.fov: + _cam_fov_changed = true + if _prev_cam_size != _active_pcam_3d.size: + _cam_size_changed = true + if _prev_cam_frustum_offset != _active_pcam_3d.frustum_offset: + _cam_frustum_offset_changed = true + if _prev_cam_near != _active_pcam_3d.near: + _cam_near_changed = true + if _prev_cam_far != _active_pcam_3d.far: + _cam_far_changed = true + else: + _cam_h_offset_changed = false + _cam_v_offset_changed = false + _cam_fov_changed = false + _cam_size_changed = false + _cam_frustum_offset_changed = false + _cam_near_changed = false + _cam_far_changed = false + _cam_attribute_changed = false + if Engine.is_editor_hint(): + if Engine.get_singleton(&"EditorInterface").get_inspector().property_edited.is_connected(_camera_3d_edited): + Engine.get_singleton(&"EditorInterface").get_inspector().property_edited.disconnect(_camera_3d_edited) + + if _active_pcam_3d.attributes == null: + _cam_attribute_changed = false + else: + if _prev_cam_attributes != _active_pcam_3d.attributes: + _prev_cam_attributes = _active_pcam_3d.attributes + _cam_attribute_changed = true + var _attributes: CameraAttributes = _active_pcam_3d.attributes + + if _prev_cam_auto_exposure_scale != _attributes.auto_exposure_scale: + _cam_auto_exposure_scale_changed = true + if _prev_cam_auto_exposure_speed != _attributes.auto_exposure_speed: + _cam_auto_exposure_speed_changed = true + if _prev_cam_exposure_multiplier != _attributes.exposure_multiplier: + _cam_exposure_multiplier_changed = true + if _prev_cam_exposure_sensitivity != _attributes.exposure_sensitivity: + _cam_exposure_sensitivity_changed = true + + if _attributes is CameraAttributesPractical: + _cam_attribute_type = 0 + + if camera_3d.attributes == null: + camera_3d.attributes = CameraAttributesPractical.new() + camera_3d.attributes = _active_pcam_3d.attributes.duplicate() + _cam_attribute_assigned = true + + if _prev_cam_exposure_min_sensitivity != _attributes.auto_exposure_min_sensitivity: + _cam_exposure_min_sensitivity_changed = true + if _prev_cam_exposure_max_sensitivity != _attributes.auto_exposure_max_sensitivity: + _cam_exposure_max_sensitivity_changed = true + + if _prev_cam_dof_blur_amount != _attributes.dof_blur_amount: + _cam_dof_blur_amount_changed = true + + if _prev_cam_dof_blur_far_distance != _attributes.dof_blur_far_distance: + _cam_dof_blur_far_distance_changed = true + camera_3d.attributes.dof_blur_far_enabled = true + if _prev_cam_dof_blur_far_transition != _attributes.dof_blur_far_transition: + _cam_dof_blur_far_transition_changed = true + camera_3d.attributes.dof_blur_far_enabled = true + + if _prev_cam_dof_blur_near_distance != _attributes.dof_blur_near_distance: + _cam_dof_blur_near_distance_changed = true + camera_3d.attributes.dof_blur_near_enabled = true + if _prev_cam_dof_blur_near_transition != _attributes.dof_blur_near_transition: + _cam_dof_blur_near_transition_changed = true + camera_3d.attributes.dof_blur_near_enabled = true + elif _attributes is CameraAttributesPhysical: + _cam_attribute_type = 1 + + if camera_3d.attributes == null: + camera_3d.attributes = CameraAttributesPhysical.new() + camera_3d.attributes = _active_pcam_3d.attributes.duplicate() + + if _prev_cam_exposure_min_exposure_value != _attributes.auto_exposure_min_exposure_value: + _cam_exposure_min_exposure_value_changed = true + if _prev_cam_exposure_max_exposure_value != _attributes.auto_exposure_max_exposure_value: + _cam_exposure_max_exposure_value_changed = true + + if _prev_cam_exposure_aperture != _attributes.exposure_aperture: + _cam_exposure_aperture_changed = true + if _prev_cam_exposure_shutter_speed != _attributes.exposure_shutter_speed: + _cam_exposure_shutter_speed_changed = true + + if _prev_cam_frustum_far != _attributes.frustum_far: + _cam_frustum_far_changed = true + + if _prev_cam_frustum_focal_length != _attributes.frustum_focal_length: + _cam_frustum_focal_length_changed = true + + if _prev_cam_frustum_focus_distance != _attributes.frustum_focus_distance: + _cam_frustum_focus_distance_changed = true + + if _prev_cam_frustum_near != _attributes.frustum_near: + _cam_frustum_near_changed = true + + if OS.has_feature("debug"): + viewfinder_update.emit(false) + + if _is_2d: + if _active_pcam_2d.show_viewfinder_in_play: + _viewfinder_needed_check = true + + _active_pcam_2d.set_is_active(self, true) + _active_pcam_2d.became_active.emit() + pcam_became_active.emit(_active_pcam_2d) + _camera_zoom = camera_2d.zoom + else: + if _active_pcam_3d.show_viewfinder_in_play: + _viewfinder_needed_check = true + + _active_pcam_3d.set_is_active(self, true) + _active_pcam_3d.became_active.emit() + pcam_became_active.emit(_active_pcam_3d) + if _active_pcam_3d.camera_3d_resource: + camera_3d.keep_aspect = _active_pcam_3d.keep_aspect + camera_3d.cull_mask = _active_pcam_3d.cull_mask + camera_3d.projection = _active_pcam_3d.projection + + if no_previous_pcam: + if _is_2d: + _prev_active_pcam_2d_transform = _active_pcam_2d.get_transform_output() + else: + _prev_active_pcam_3d_transform = _active_pcam_3d.get_transform_output() + + if pcam.get_tween_skip() or pcam.tween_duration == 0: + _tween_elapsed_time = pcam.tween_duration + if Engine.get_version_info().major == 4 and \ + Engine.get_version_info().minor >= 3: + _tween_is_instant = true + else: + _tween_elapsed_time = 0 + + _check_pcam_physics() + + _trigger_pcam_tween = true + + +func _check_pcam_physics() -> void: + if _is_2d: + if _active_pcam_2d.get_follow_target_physics_based() and interpolation_mode != InterpolationMode.IDLE: + _follow_target_physics_based = true + camera_2d.reset_physics_interpolation() + camera_2d.physics_interpolation_mode = Node.PHYSICS_INTERPOLATION_MODE_ON + if ProjectSettings.get_setting("physics/common/physics_interpolation"): + camera_2d.process_callback = Camera2D.CAMERA2D_PROCESS_PHYSICS # Prevents a warning + else: + camera_2d.process_callback = Camera2D.CAMERA2D_PROCESS_IDLE + else: + _follow_target_physics_based = false + camera_2d.physics_interpolation_mode = Node.PHYSICS_INTERPOLATION_MODE_INHERIT + if get_tree().physics_interpolation: + camera_2d.process_callback = Camera2D.CAMERA2D_PROCESS_PHYSICS # Prevents a warning + else: + camera_2d.process_callback = Camera2D.CAMERA2D_PROCESS_IDLE + else: + ## NOTE - Only supported in Godot 4.4 or later + if Engine.get_version_info().major == 4 and \ + Engine.get_version_info().minor >= 4: + if (get_tree().physics_interpolation or _active_pcam_3d.get_follow_target_physics_based()) and interpolation_mode != InterpolationMode.IDLE: + #if get_tree().physics_interpolation or _active_pcam_3d.get_follow_target_physics_based(): + _follow_target_physics_based = true + camera_3d.reset_physics_interpolation() + camera_3d.physics_interpolation_mode = Node.PHYSICS_INTERPOLATION_MODE_ON + else: + _follow_target_physics_based = false + camera_3d.physics_interpolation_mode = Node.PHYSICS_INTERPOLATION_MODE_INHERIT + + +## TODO - For 0.8 release +#func _find_pcam_with_highest_priority() -> void: + #var highest_priority_pcam: Node + #for pcam in _pcam_list: + #if not pcam.visible: continue # Prevents hidden PCams from becoming active + #if pcam.priority > _active_pcam_priority: + #_active_pcam_priority = pcam.priority + #highest_priority_pcam = pcam + #pcam.set_has_tweened(self, false) +# + #_active_pcam_missing = false +# + #if is_instance_valid(highest_priority_pcam): + #_assign_new_active_pcam(highest_priority_pcam) + #else: + #_active_pcam_missing = true + + +func _process(delta: float) -> void: + if _active_pcam_missing: return + + if not _follow_target_physics_based: _tween_follow_checker(delta) + + +func _physics_process(delta: float) -> void: + if _active_pcam_missing or not _follow_target_physics_based: return + _tween_follow_checker(delta) + + +func _tween_follow_checker(delta: float) -> void: + if _is_2d: + if not is_instance_valid(_active_pcam_2d): + _active_pcam_missing = true + return + + _active_pcam_2d.process_logic(delta) + _active_pcam_2d_glob_transform = _active_pcam_2d.get_transform_output() + + if _reset_noise_offset_2d: + camera_2d.offset = Vector2.ZERO # Resets noise position + _reset_noise_offset_2d = false + else: + if not is_instance_valid(_active_pcam_3d): + _active_pcam_missing = true + return + + _active_pcam_3d.process_logic(delta) + _active_pcam_3d_glob_transform = _active_pcam_3d.get_transform_output() + + if not _trigger_pcam_tween: + # Rechecks physics target if PCam transitioned with an instant tween + if _tween_is_instant: + _check_pcam_physics() + _tween_is_instant = false + _pcam_follow(delta) + else: + _pcam_tween(delta) + + # Camera Noise + if _is_2d: + if not _has_noise_emitted and not _active_pcam_2d.has_noise_resource(): return + camera_2d.offset += _active_pcam_2d.get_noise_transform().origin + _noise_emitted_output_2d.origin + if camera_2d.ignore_rotation and _noise_emitted_output_2d.get_rotation() != 0: + push_warning(camera_2d.name, " has ignore_rotation enabled. Uncheck the property if you want to apply rotational noise.") + else: + camera_2d.rotation += _active_pcam_2d.get_noise_transform().get_rotation() + _noise_emitted_output_2d.get_rotation() + _has_noise_emitted = false + _reset_noise_offset_2d = true + else: + if not _has_noise_emitted and not _active_pcam_3d.has_noise_resource(): return + camera_3d.global_transform *= _active_pcam_3d.get_noise_transform() * _noise_emitted_output_3d + _has_noise_emitted = false + + +func _pcam_follow(_delta: float) -> void: + if _active_pcam_missing or not _is_child_of_camera: return + + if _is_2d: + if _active_pcam_2d.snap_to_pixel: + var snap_to_pixel_glob_transform: Transform2D = _active_pcam_2d_glob_transform + snap_to_pixel_glob_transform.origin = snap_to_pixel_glob_transform.origin.round() + camera_2d.global_transform = snap_to_pixel_glob_transform + else: + camera_2d.global_transform = _active_pcam_2d_glob_transform + camera_2d.zoom = _active_pcam_2d.zoom + else: + camera_3d.global_transform = _active_pcam_3d_glob_transform + + if _viewfinder_needed_check: + _show_viewfinder_in_play() + _viewfinder_needed_check = false + + if Engine.is_editor_hint(): + if not _is_2d: + # TODO - Signal-based solution pending merge of: https://github.com/godotengine/godot/pull/99729 + if _active_pcam_3d.attributes != null: + camera_3d.attributes = _active_pcam_3d.attributes.duplicate() + + # TODO - Signal-based solution pending merge of: https://github.com/godotengine/godot/pull/99873 + if _active_pcam_3d.environment != null: + camera_3d.environment = _active_pcam_3d.environment.duplicate() + + +func _noise_emitted_2d(noise_output: Transform2D) -> void: + _noise_emitted_output_2d = noise_output + _has_noise_emitted = true + + +func _noise_emitted_3d(noise_output: Transform3D) -> void: + _noise_emitted_output_3d = noise_output + _has_noise_emitted = true + + +func _camera_3d_resource_changed() -> void: + if _active_pcam_3d.camera_3d_resource: + if Engine.is_editor_hint(): + if not Engine.get_singleton(&"EditorInterface").get_inspector().property_edited.is_connected(_camera_3d_edited): + Engine.get_singleton(&"EditorInterface").get_inspector().property_edited.connect(_camera_3d_edited) + camera_3d.keep_aspect = _active_pcam_3d.keep_aspect + camera_3d.cull_mask = _active_pcam_3d.cull_mask + camera_3d.h_offset = _active_pcam_3d.h_offset + camera_3d.v_offset = _active_pcam_3d.v_offset + camera_3d.projection = _active_pcam_3d.projection + camera_3d.fov = _active_pcam_3d.fov + camera_3d.size = _active_pcam_3d.size + camera_3d.frustum_offset = _active_pcam_3d.frustum_offset + camera_3d.near = _active_pcam_3d.near + camera_3d.far = _active_pcam_3d.far + else: + if Engine.is_editor_hint(): + if Engine.get_singleton(&"EditorInterface").get_inspector().property_edited.is_connected(_camera_3d_edited): + Engine.get_singleton(&"EditorInterface").get_inspector().property_edited.disconnect(_camera_3d_edited) + +func _camera_3d_edited(value: String) -> void: + if not Engine.get_singleton(&"EditorInterface").get_inspector().get_edited_object() == camera_3d: return + camera_3d.set(value, _active_pcam_3d.camera_3d_resource.get(value)) + push_warning("Camera3D properties are being overridden by ", _active_pcam_3d.name, "'s Camera3DResource") + +func _camera_3d_resource_property_changed(property: StringName, value: Variant) -> void: + camera_3d.set(property, value) + + +func _pcam_tween(delta: float) -> void: + # TODO - Should be optimised + # Run at the first tween frame + if _tween_elapsed_time == 0: + if _is_2d: + _active_pcam_2d.tween_started.emit() + _active_pcam_2d.reset_limit() + else: + _active_pcam_3d.tween_started.emit() + + _tween_elapsed_time = min(_tween_duration, _tween_elapsed_time + delta) + + if _is_2d: + _active_pcam_2d.is_tweening.emit() + var interpolation_destination: Vector2 = _tween_interpolate_value( + _prev_active_pcam_2d_transform.origin, + _active_pcam_2d_glob_transform.origin, + _active_pcam_2d.tween_duration, + _active_pcam_2d.tween_transition, + _active_pcam_2d.tween_ease + ) + + if _active_pcam_2d.snap_to_pixel: + camera_2d.global_position = interpolation_destination.round() + else: + camera_2d.global_position = interpolation_destination + + camera_2d.rotation = _tween_interpolate_value( + _prev_active_pcam_2d_transform.get_rotation(), + _active_pcam_2d_glob_transform.get_rotation(), + _active_pcam_2d.tween_duration, + _active_pcam_2d.tween_transition, + _active_pcam_2d.tween_ease + ) + camera_2d.zoom = _tween_interpolate_value( + _camera_zoom, + _active_pcam_2d.zoom, + _active_pcam_2d.tween_duration, + _active_pcam_2d.tween_transition, + _active_pcam_2d.tween_ease + ) + else: + _active_pcam_3d.is_tweening.emit() + camera_3d.global_position = _tween_interpolate_value( + _prev_active_pcam_3d_transform.origin, + _active_pcam_3d_glob_transform.origin, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + + var prev_active_pcam_3d_quat: Quaternion = Quaternion(_prev_active_pcam_3d_transform.basis.orthonormalized()) + camera_3d.quaternion = \ + Tween.interpolate_value( + prev_active_pcam_3d_quat, \ + prev_active_pcam_3d_quat.inverse() * Quaternion(_active_pcam_3d_glob_transform.basis.orthonormalized()), + _tween_elapsed_time, \ + _active_pcam_3d.tween_duration, \ + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + + if _cam_attribute_changed: + if _active_pcam_3d.attributes.auto_exposure_enabled: + if _cam_auto_exposure_scale_changed: + camera_3d.attributes.auto_exposure_scale = \ + _tween_interpolate_value( + _prev_cam_auto_exposure_scale, + _active_pcam_3d.attributes.auto_exposure_scale, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + if _cam_auto_exposure_speed_changed: + camera_3d.attributes.auto_exposure_speed = \ + _tween_interpolate_value( + _prev_cam_auto_exposure_scale, + _active_pcam_3d.attributes.auto_exposure_scale, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + + if _cam_attribute_type == 0: # CameraAttributePractical + if _active_pcam_3d.attributes.auto_exposure_enabled: + if _cam_exposure_min_sensitivity_changed: + camera_3d.attributes.auto_exposure_min_sensitivity = \ + _tween_interpolate_value( + _prev_cam_exposure_min_sensitivity, + _active_pcam_3d.attributes.auto_exposure_min_sensitivity, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + if _cam_exposure_max_sensitivity_changed: + camera_3d.attributes.auto_exposure_max_sensitivity = \ + _tween_interpolate_value( + _prev_cam_exposure_max_sensitivity, + _active_pcam_3d.attributes.auto_exposure_max_sensitivity, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + if _cam_dof_blur_amount_changed: + camera_3d.attributes.dof_blur_amount = \ + _tween_interpolate_value( + _prev_cam_dof_blur_amount, + _active_pcam_3d.attributes.dof_blur_amount, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + if _cam_dof_blur_far_distance_changed: + camera_3d.attributes.dof_blur_far_distance = \ + _tween_interpolate_value( + _prev_cam_dof_blur_far_distance, + _active_pcam_3d.attributes.dof_blur_far_distance, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + if _cam_dof_blur_far_transition_changed: + camera_3d.attributes.dof_blur_far_transition = \ + _tween_interpolate_value( + _prev_cam_dof_blur_far_transition, + _active_pcam_3d.attributes.dof_blur_far_transition, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + if _cam_dof_blur_near_distance_changed: + camera_3d.attributes.dof_blur_near_distance = \ + _tween_interpolate_value( + _prev_cam_dof_blur_near_distance, + _active_pcam_3d.attributes.dof_blur_near_distance, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + if _cam_dof_blur_near_transition_changed: + camera_3d.attributes.dof_blur_near_transition = \ + _tween_interpolate_value( + _prev_cam_dof_blur_near_transition, + _active_pcam_3d.attributes.dof_blur_near_transition, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + elif _cam_attribute_type == 1: # CameraAttributePhysical + if _cam_dof_blur_near_transition_changed: + camera_3d.attributes.auto_exposure_max_exposure_value = \ + _tween_interpolate_value( + _prev_cam_exposure_max_exposure_value, + _active_pcam_3d.attributes.auto_exposure_max_exposure_value, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + if _cam_exposure_min_exposure_value_changed: + camera_3d.attributes.auto_exposure_min_exposure_value = \ + _tween_interpolate_value( + _prev_cam_exposure_min_exposure_value, + _active_pcam_3d.attributes.auto_exposure_min_exposure_value, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + if _cam_exposure_aperture_changed: + camera_3d.attributes.exposure_aperture = \ + _tween_interpolate_value( + _prev_cam_exposure_aperture, + _active_pcam_3d.attributes.exposure_aperture, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + if _cam_exposure_shutter_speed_changed: + camera_3d.attributes.exposure_shutter_speed = \ + _tween_interpolate_value( + _prev_cam_exposure_shutter_speed, + _active_pcam_3d.attributes.exposure_shutter_speed, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + if _cam_frustum_far_changed: + camera_3d.attributes.frustum_far = \ + _tween_interpolate_value( + _prev_cam_frustum_far, + _active_pcam_3d.attributes.frustum_far, + _active_pcam_3d.tween_duration(), + _active_pcam_3d.tween_transition(), + _active_pcam_3d.tween_ease + ) + if _cam_frustum_near_changed: + camera_3d.attributes.frustum_near = \ + _tween_interpolate_value( + _prev_cam_frustum_far, + _active_pcam_3d.attributes.frustum_near, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + if _cam_frustum_focal_length_changed: + camera_3d.attributes.frustum_focal_length = \ + _tween_interpolate_value( + _prev_cam_frustum_focal_length, + _active_pcam_3d.attributes.frustum_focal_length, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + if _cam_frustum_focus_distance_changed: + camera_3d.attributes.frustum_focus_distance = \ + _tween_interpolate_value( + _prev_cam_frustum_focus_distance, + _active_pcam_3d.attributes.frustum_focus_distance, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + + if _cam_h_offset_changed: + camera_3d.h_offset = \ + _tween_interpolate_value( + _prev_cam_h_offset, + _active_pcam_3d.h_offset, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + + if _cam_v_offset_changed: + camera_3d.v_offset = \ + _tween_interpolate_value( + _prev_cam_v_offset, + _active_pcam_3d.v_offset, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + + if _cam_fov_changed: + camera_3d.fov = \ + _tween_interpolate_value( + _prev_cam_fov, + _active_pcam_3d.fov, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + + if _cam_size_changed: + camera_3d.size = \ + _tween_interpolate_value( + _prev_cam_size, + _active_pcam_3d.size, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + + if _cam_frustum_offset_changed: + camera_3d.frustum_offset = \ + _tween_interpolate_value( + _prev_cam_frustum_offset, + _active_pcam_3d.frustum_offset, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + + if _cam_near_changed: + camera_3d.near = \ + _tween_interpolate_value( + _prev_cam_near, + _active_pcam_3d.near, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + + if _cam_far_changed: + camera_3d.far = \ + _tween_interpolate_value( + _prev_cam_far, + _active_pcam_3d.far, + _active_pcam_3d.tween_duration, + _active_pcam_3d.tween_transition, + _active_pcam_3d.tween_ease + ) + + # Forcefully disables physics interpolation when tweens are instant + if _tween_is_instant: + if _is_2d: + camera_2d.physics_interpolation_mode = Node.PHYSICS_INTERPOLATION_MODE_OFF + camera_2d.reset_physics_interpolation() + else: + if Engine.get_version_info().major == 4 and \ + Engine.get_version_info().minor >= 4: + camera_3d.physics_interpolation_mode = Node.PHYSICS_INTERPOLATION_MODE_OFF + camera_3d.reset_physics_interpolation() + + if _tween_elapsed_time < _tween_duration: return + + _trigger_pcam_tween = false + _tween_elapsed_time = 0 + viewfinder_update.emit(true) + + if _is_2d: + _active_pcam_2d.update_limit_all_sides() + _active_pcam_2d.tween_completed.emit() + _active_pcam_2d.set_tween_skip(self, false) + if Engine.is_editor_hint(): + _active_pcam_2d.queue_redraw() + else: + if _active_pcam_3d.camera_3d_resource and _active_pcam_3d.attributes != null: + if _cam_attribute_type == 0: + if not _active_pcam_3d.attributes.dof_blur_far_enabled: + camera_3d.attributes.dof_blur_far_enabled = false + if not _active_pcam_3d.attributes.dof_blur_near_enabled: + camera_3d.attributes.dof_blur_near_enabled = false + _cam_h_offset_changed = false + _cam_v_offset_changed = false + _cam_fov_changed = false + _cam_size_changed = false + _cam_frustum_offset_changed = false + _cam_near_changed = false + _cam_far_changed = false + _cam_attribute_changed = false + + _active_pcam_3d.set_tween_skip(self, false) + _active_pcam_3d.tween_completed.emit() + + +func _tween_interpolate_value(from: Variant, to: Variant, duration: float, transition_type: int, ease_type: int) -> Variant: + return Tween.interpolate_value( + from, \ + to - from, + _tween_elapsed_time, \ + duration, \ + transition_type, + ease_type, + ) + + +func _show_viewfinder_in_play() -> void: + # Don't show the viewfinder in the actual editor or project builds + if Engine.is_editor_hint() or !OS.has_feature("editor"): return + + # Default the viewfinder node to be hidden + if is_instance_valid(_viewfinder_node): + _viewfinder_node.visible = false + + if _is_2d: + if not _active_pcam_2d.show_viewfinder_in_play: return + if _active_pcam_2d.follow_mode != _active_pcam_2d.FollowMode.FRAMED: return + else: + if not _active_pcam_3d.show_viewfinder_in_play: return + if _active_pcam_3d.follow_mode != _active_pcam_2d.FollowMode.FRAMED: return + + var canvas_layer: CanvasLayer = CanvasLayer.new() + get_tree().get_root().add_child(canvas_layer) + + # Instantiate the viewfinder scene if it isn't already + if not is_instance_valid(_viewfinder_node): + var _viewfinder_scene := load("res://addons/phantom_camera/panel/viewfinder/viewfinder_panel.tscn") + _viewfinder_node = _viewfinder_scene.instantiate() + canvas_layer.add_child(_viewfinder_node) + + _viewfinder_node.visible = true + _viewfinder_node.update_dead_zone() + + +func _update_limit_2d(side: int, limit: int) -> void: + if is_instance_valid(camera_2d): + camera_2d.set_limit(side, limit) + +func _draw_limit_2d(enabled: bool) -> void: + camera_2d.set_limit_drawing_enabled(enabled) + + +## Called when a [param PhantomCamera] is added to the scene.[br] +## [b]Note:[/b] This can only be called internally from a [param PhantomCamera] node. +func _pcam_added_to_scene(pcam: Node) -> void: + if not pcam.is_node_ready(): await pcam.ready + _check_pcam_priority(pcam) + + +## Called when a [param PhantomCamera] is removed from the scene.[br] +## [b]Note:[/b] This can only be called internally from a +## [param PhantomCamera] node. +func _pcam_removed_from_scene(pcam: Node) -> void: + if _is_2d: + if pcam == _active_pcam_2d: + _active_pcam_2d = null + _active_pcam_missing = true + _active_pcam_priority = -1 + _find_pcam_with_highest_priority() + else: + if pcam == _active_pcam_3d: + _active_pcam_3d = null + _active_pcam_missing = true + _active_pcam_priority = -1 + _find_pcam_with_highest_priority() + + +func _pcam_visibility_changed(pcam: Node) -> void: + if pcam == _active_pcam_2d or pcam == _active_pcam_3d: + _active_pcam_priority = -1 + _find_pcam_with_highest_priority() + return + _check_pcam_priority(pcam) + + +func _pcam_teleported(pcam: Node) -> void: + if _is_2d: + if not pcam == _active_pcam_2d: return + if not is_instance_valid(camera_2d): return + camera_2d.global_position = _active_pcam_2d.get_transform_output().origin + camera_2d.reset_physics_interpolation() + else: + if not pcam == _active_pcam_3d: return + if not is_instance_valid(camera_3d): return + camera_3d.global_position = _active_pcam_3d.get_transform_output().origin + camera_3d.reset_physics_interpolation() + + +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("Render 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 + +## Triggers a recalculation to determine which PhantomCamera has the highest priority. +func pcam_priority_updated(pcam: Node) -> void: + if not is_instance_valid(pcam): return + if not _pcam_is_in_host_layer(pcam): return + + if pcam == _active_pcam_2d or pcam == _active_pcam_3d: + if not pcam.visible: + refresh_pcam_list_priorty() + + if Engine.is_editor_hint(): + if _is_2d: + if not is_instance_valid(_active_pcam_2d): return + if _active_pcam_2d.priority_override: return + else: + if not is_instance_valid(_active_pcam_3d): return + if _active_pcam_3d.priority_override: return + + var current_pcam_priority: int = pcam.priority + + if current_pcam_priority >= _active_pcam_priority: + if _is_2d: + if pcam != _active_pcam_2d: + _assign_new_active_pcam(pcam) + else: + if pcam != _active_pcam_3d: + _assign_new_active_pcam(pcam) + pcam.set_tween_skip(self, false) + _active_pcam_missing = false + + if pcam == _active_pcam_2d or pcam == _active_pcam_3d: + if current_pcam_priority <= _active_pcam_priority: + _active_pcam_priority = current_pcam_priority + _find_pcam_with_highest_priority() + else: + _active_pcam_priority = current_pcam_priority + + +## Updates the viewfinder when a [param PhantomCamera] has its +## [param priority_ovrride] enabled.[br] +## [b]Note:[/b] This only affects the editor. +func _pcam_priority_override(pcam: Node, should_override: bool) -> void: + if not Engine.is_editor_hint(): return + if not _pcam_is_in_host_layer(pcam): return + if should_override: + if _is_2d: + if is_instance_valid(_active_pcam_2d): + if _active_pcam_2d.priority_override: + _active_pcam_2d.priority_override = false + else: + if is_instance_valid(_active_pcam_3d): + if _active_pcam_3d.priority_override: + _active_pcam_3d.priority_override = false + _assign_new_active_pcam(pcam) + else: + _find_pcam_with_highest_priority() + + viewfinder_update.emit(false) + + +## Updates the viewfinder when a [param PhantomCamera] has its +## [param priority_ovrride] disabled.[br] +## [b]Note:[/b] This only affects the editor. +func pcam_priority_override_disabled() -> void: + viewfinder_update.emit(false) + + +## Returns the currently active [param PhantomCamera] +func get_active_pcam() -> Node: + if _is_2d: + return _active_pcam_2d + else: + return _active_pcam_3d + + +## Returns whether if a [param PhantomCamera] should tween when it becomes +## active. If it's already active, the value will always be false. +## [b]Note:[/b] This can only be called internally from a +## [param PhantomCamera] node. +func get_trigger_pcam_tween() -> bool: + return _trigger_pcam_tween + + +## Refreshes the [param PhantomCamera] list and checks for the highest priority. [br] +## [b]Note:[/b] This should [b]not[/b] be necessary to call manually. +func refresh_pcam_list_priorty() -> void: + _active_pcam_priority = -1 + _find_pcam_with_highest_priority() + +#endregion + +#region Setters / Getters + +func set_interpolation_mode(value: int) -> void: + interpolation_mode = value + if is_inside_tree(): + _check_pcam_physics() +func get_interpolation_mode() -> int: + return interpolation_mode + +## Sets the [member host_layers] value. +func set_host_layers(value: int) -> void: + host_layers = value + + if not _is_child_of_camera: return + + if not _active_pcam_missing: + if _is_2d: + _pcam_host_layer_changed(_active_pcam_2d) + else: + _pcam_host_layer_changed(_active_pcam_3d) + else: + _find_pcam_with_highest_priority() + +## Enables or disables a given layer of [member host_layers]. +func set_host_layers_value(layer: int, value: bool) -> void: + host_layers = _set_layer(host_layers, layer, value) + +## Returns the [member host_layers] value. +func get_host_layers() -> int: + return host_layers + +#endregion diff --git a/addons/phantom_camera/scripts/phantom_camera_host/phantom_camera_host.gd.uid b/addons/phantom_camera/scripts/phantom_camera_host/phantom_camera_host.gd.uid new file mode 100644 index 0000000..e96433d --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera_host/phantom_camera_host.gd.uid @@ -0,0 +1 @@ +uid://bd046eokvcnu2 diff --git a/addons/phantom_camera/scripts/resources/Camera3DResource.cs b/addons/phantom_camera/scripts/resources/Camera3DResource.cs new file mode 100644 index 0000000..7bd00b5 --- /dev/null +++ b/addons/phantom_camera/scripts/resources/Camera3DResource.cs @@ -0,0 +1,117 @@ +using Godot; + +namespace PhantomCamera; + +public enum KeepAspect +{ + KeepWidth, + KeepHeight +} + +public enum ProjectionType +{ + Perspective, + Orthogonal, + Frustum +} + +public class Camera3DResource(Resource resource) +{ + public readonly Resource Resource = resource; + + public KeepAspect KeepAspect + { + get => (KeepAspect)(int)Resource.Call(MethodName.GetKeepAspect); + set => Resource.Call(MethodName.SetKeepAspect, (int)value); + } + + public int CullMask + { + get => (int)Resource.Call(MethodName.GetCullMask); + set => Resource.Call(MethodName.SetCullMask, value); + } + + public void SetCullMaskValue(int layer, bool value) => Resource.Call(MethodName.SetCullMaskValue, layer, value); + + public float HOffset + { + get => (float)Resource.Call(MethodName.GetHOffset); + set => Resource.Call(MethodName.SetHOffset, value); + } + + public float VOffset + { + get => (float)Resource.Call(MethodName.GetVOffset); + set => Resource.Call(MethodName.SetVOffset, value); + } + + public ProjectionType Projection + { + get => (ProjectionType)(int)Resource.Call(MethodName.GetProjection); + set => Resource.Call(MethodName.SetProjection, (int)value); + } + + public float Fov + { + get => (float)Resource.Call(MethodName.GetFov); + set => Resource.Call(MethodName.SetFov, Mathf.Clamp(value, 1, 179)); + } + + public float Size + { + get => (float)Resource.Call(MethodName.GetSize); + set => Resource.Call(MethodName.SetSize, Mathf.Clamp(value, 0.001f, float.PositiveInfinity)); + } + + public Vector2 FrustumOffset + { + get => (Vector2)Resource.Call(MethodName.GetFrustumOffset); + set => Resource.Call(MethodName.SetFrustumOffset, value); + } + + public float Near + { + get => (float)Resource.Call(MethodName.GetNear); + set => Resource.Call(MethodName.SetNear, Mathf.Clamp(value, 0.001f, float.PositiveInfinity)); + } + + public float Far + { + get => (float)Resource.Call(MethodName.GetFar); + set => Resource.Call(MethodName.SetFar, Mathf.Clamp(value, 0.01f, float.PositiveInfinity)); + } + + public static class MethodName + { + public const string GetKeepAspect = "get_keep_aspect"; + public const string SetKeepAspect = "set_keep_aspect"; + + public const string GetCullMask = "get_cull_mask"; + public const string SetCullMask = "set_cull_mask"; + public const string SetCullMaskValue = "set_cull_mask_value"; + + public const string GetHOffset = "get_h_offset"; + public const string SetHOffset = "set_h_offset"; + + public const string GetVOffset = "get_v_offset"; + public const string SetVOffset = "set_v_offset"; + + public const string GetProjection = "get_projection"; + public const string SetProjection = "set_projection"; + + public const string GetFov = "get_fov"; + public const string SetFov = "set_fov"; + + public const string GetSize = "get_size"; + public const string SetSize = "set_size"; + + public const string GetFrustumOffset = "get_frustum_offset"; + public const string SetFrustumOffset = "set_frustum_offset"; + + public const string GetNear = "get_near"; + public const string SetNear = "set_near"; + + public const string GetFar = "get_far"; + public const string SetFar = "set_far"; + } +} diff --git a/addons/phantom_camera/scripts/resources/Camera3DResource.cs.uid b/addons/phantom_camera/scripts/resources/Camera3DResource.cs.uid new file mode 100644 index 0000000..d66b43e --- /dev/null +++ b/addons/phantom_camera/scripts/resources/Camera3DResource.cs.uid @@ -0,0 +1 @@ +uid://jedyxlihuwbj diff --git a/addons/phantom_camera/scripts/resources/PhantomCameraNoise2D.cs b/addons/phantom_camera/scripts/resources/PhantomCameraNoise2D.cs new file mode 100644 index 0000000..16b7273 --- /dev/null +++ b/addons/phantom_camera/scripts/resources/PhantomCameraNoise2D.cs @@ -0,0 +1,92 @@ +using Godot; + +namespace PhantomCamera.Noise; + +public class PhantomCameraNoise2D(Resource resource) +{ + public readonly Resource Resource = resource; + + public float Amplitude + { + get => (float)Resource.Call(MethodName.GetAmplitude); + set => Resource.Call(MethodName.SetAmplitude, value); + } + + public float Frequency + { + get => (float)Resource.Call(MethodName.GetFrequency); + set => Resource.Call(MethodName.SetFrequency, value); + } + + public bool RandomizeNoiseSeed + { + get => (bool)Resource.Call(MethodName.GetRandomizeNoiseSeed); + set => Resource.Call(MethodName.SetRandomizeNoiseSeed, value); + } + + public int NoiseSeed + { + get => (int)Resource.Call(MethodName.GetNoiseSeed); + set => Resource.Call(MethodName.SetNoiseSeed, value); + } + + public bool RotationalNoise + { + get => (bool)Resource.Call(MethodName.GetRotationalNoise); + set => Resource.Call(MethodName.SetRotationalNoise, value); + } + + public bool PositionalNoise + { + get => (bool)Resource.Call(MethodName.GetPositionalNoise); + set => Resource.Call(MethodName.SetPositionalNoise, value); + } + + public float RotationalMultiplier + { + get => (float)Resource.Call(MethodName.GetRotationalMultiplier); + set => Resource.Call(MethodName.SetRotationalMultiplier, value); + } + + public float PositionalMultiplierX + { + get => (float)Resource.Call(MethodName.GetPositionalMultiplierX); + set => Resource.Call(MethodName.SetPositionalMultiplierX, value); + } + + public float PositionalMultiplierY + { + get => (float)Resource.Call(MethodName.GetPositionalMultiplierY); + set => Resource.Call(MethodName.SetPositionalMultiplierY, value); + } + + public static class MethodName + { + public const string GetAmplitude = "get_amplitude"; + public const string SetAmplitude = "set_amplitude"; + + public const string GetFrequency = "get_frequency"; + public const string SetFrequency = "set_frequency"; + + public const string GetRandomizeNoiseSeed = "get_randomize_noise_seed"; + public const string SetRandomizeNoiseSeed = "set_randomize_noise_seed"; + + public const string GetNoiseSeed = "get_noise_seed"; + public const string SetNoiseSeed = "set_noise_seed"; + + public const string GetRotationalNoise = "get_rotational_noise"; + public const string SetRotationalNoise = "set_rotational_noise"; + + public const string GetPositionalNoise = "get_positional_noise"; + public const string SetPositionalNoise = "set_positional_noise"; + + public const string GetRotationalMultiplier = "get_rotational_multiplier"; + public const string SetRotationalMultiplier = "set_rotational_multiplier"; + + public const string GetPositionalMultiplierX = "get_positional_multiplier_x"; + public const string SetPositionalMultiplierX = "set_positional_multiplier_x"; + + public const string GetPositionalMultiplierY = "get_positional_multiplier_y"; + public const string SetPositionalMultiplierY = "set_positional_multiplier_y"; + } +} diff --git a/addons/phantom_camera/scripts/resources/PhantomCameraNoise2D.cs.uid b/addons/phantom_camera/scripts/resources/PhantomCameraNoise2D.cs.uid new file mode 100644 index 0000000..16ddb84 --- /dev/null +++ b/addons/phantom_camera/scripts/resources/PhantomCameraNoise2D.cs.uid @@ -0,0 +1 @@ +uid://capjdoxs6gs6r diff --git a/addons/phantom_camera/scripts/resources/PhantomCameraNoise3D.cs b/addons/phantom_camera/scripts/resources/PhantomCameraNoise3D.cs new file mode 100644 index 0000000..175a427 --- /dev/null +++ b/addons/phantom_camera/scripts/resources/PhantomCameraNoise3D.cs @@ -0,0 +1,119 @@ +using Godot; + +namespace PhantomCamera.Noise; + +public class PhantomCameraNoise3D(Resource resource) +{ + public readonly Resource Resource = resource; + + public float Amplitude + { + get => (float)Resource.Call(MethodName.GetAmplitude); + set => Resource.Call(MethodName.SetAmplitude, value); + } + + public float Frequency + { + get => (float)Resource.Call(MethodName.GetFrequency); + set => Resource.Call(MethodName.SetFrequency, value); + } + + public bool RandomizeNoiseSeed + { + get => (bool)Resource.Call(MethodName.GetRandomizeNoiseSeed); + set => Resource.Call(MethodName.SetRandomizeNoiseSeed, value); + } + + public int NoiseSeed + { + get => (int)Resource.Call(MethodName.GetNoiseSeed); + set => Resource.Call(MethodName.SetNoiseSeed, value); + } + + public bool RotationalNoise + { + get => (bool)Resource.Call(MethodName.GetRotationalNoise); + set => Resource.Call(MethodName.SetRotationalNoise, value); + } + + public bool PositionalNoise + { + get => (bool)Resource.Call(MethodName.GetPositionalNoise); + set => Resource.Call(MethodName.SetPositionalNoise, value); + } + + public float RotationalMultiplierX + { + get => (float)Resource.Call(MethodName.GetRotationalMultiplierX); + set => Resource.Call(MethodName.SetRotationalMultiplierX, value); + } + + public float RotationalMultiplierY + { + get => (float)Resource.Call(MethodName.GetRotationalMultiplierY); + set => Resource.Call(MethodName.SetRotationalMultiplierY, value); + } + + public float RotationalMultiplierZ + { + get => (float)Resource.Call(MethodName.GetRotationalMultiplierZ); + set => Resource.Call(MethodName.SetRotationalMultiplierZ, value); + } + + public float PositionalMultiplierX + { + get => (float)Resource.Call(MethodName.GetPositionalMultiplierX); + set => Resource.Call(MethodName.SetPositionalMultiplierX, value); + } + + public float PositionalMultiplierY + { + get => (float)Resource.Call(MethodName.GetPositionalMultiplierY); + set => Resource.Call(MethodName.SetPositionalMultiplierY, value); + } + + public float PositionalMultiplierZ + { + get => (float)Resource.Call(MethodName.GetPositionalMultiplierZ); + set => Resource.Call(MethodName.SetPositionalMultiplierZ, value); + } + + public static class MethodName + { + public const string GetAmplitude = "get_amplitude"; + public const string SetAmplitude = "set_amplitude"; + + public const string GetFrequency = "get_frequency"; + public const string SetFrequency = "set_frequency"; + + public const string GetRandomizeNoiseSeed = "get_randomize_noise_seed"; + public const string SetRandomizeNoiseSeed = "set_randomize_noise_seed"; + + public const string GetNoiseSeed = "get_noise_seed"; + public const string SetNoiseSeed = "set_noise_seed"; + + public const string GetRotationalNoise = "get_rotational_noise"; + public const string SetRotationalNoise = "set_rotational_noise"; + + public const string GetPositionalNoise = "get_positional_noise"; + public const string SetPositionalNoise = "set_positional_noise"; + + public const string GetRotationalMultiplierX = "get_rotational_multiplier_x"; + public const string SetRotationalMultiplierX = "set_rotational_multiplier_x"; + + public const string GetRotationalMultiplierY = "get_rotational_multiplier_y"; + public const string SetRotationalMultiplierY = "set_rotational_multiplier_y"; + + public const string GetRotationalMultiplierZ = "get_rotational_multiplier_z"; + public const string SetRotationalMultiplierZ = "set_rotational_multiplier_z"; + + public const string GetPositionalMultiplierX = "get_positional_multiplier_x"; + public const string SetPositionalMultiplierX = "set_positional_multiplier_x"; + + public const string GetPositionalMultiplierY = "get_positional_multiplier_y"; + public const string SetPositionalMultiplierY = "set_positional_multiplier_y"; + + public const string GetPositionalMultiplierZ = "get_positional_multiplier_z"; + public const string SetPositionalMultiplierZ = "set_positional_multiplier_z"; + } +} diff --git a/addons/phantom_camera/scripts/resources/PhantomCameraNoise3D.cs.uid b/addons/phantom_camera/scripts/resources/PhantomCameraNoise3D.cs.uid new file mode 100644 index 0000000..53d184f --- /dev/null +++ b/addons/phantom_camera/scripts/resources/PhantomCameraNoise3D.cs.uid @@ -0,0 +1 @@ +uid://chk7643ynhe4f diff --git a/addons/phantom_camera/scripts/resources/PhantomCameraTween.cs b/addons/phantom_camera/scripts/resources/PhantomCameraTween.cs new file mode 100644 index 0000000..1c332b7 --- /dev/null +++ b/addons/phantom_camera/scripts/resources/PhantomCameraTween.cs @@ -0,0 +1,64 @@ +using Godot; + +namespace PhantomCamera; + +public enum TransitionType +{ + Linear, + Sine, + Quint, + Quart, + Quad, + Expo, + Elastic, + Cubic, + Circ, + Bounce, + Back +} + +public enum EaseType +{ + EaseIn, + EaseOut, + EaseInOut, + EaseOutIn +} + +public static class PhantomCameraTweenExtensions +{ + public static PhantomCameraTween AsPhantomCameraTween(this Resource resource) + { + return new PhantomCameraTween(resource); + } +} + +public class PhantomCameraTween(Resource tweenResource) +{ + public Resource Resource { get; } = tweenResource; + + public float Duration + { + get => (float)Resource.Get(PropertyName.Duration); + set => Resource.Set(PropertyName.Duration, value); + } + + public TransitionType Transition + { + get => (TransitionType)(int)Resource.Get(PropertyName.Transition); + set => Resource.Set(PropertyName.Transition, (int)value); + } + + public EaseType Ease + { + get => (EaseType)(int)Resource.Get(PropertyName.Ease); + set => Resource.Set(PropertyName.Ease, (int)value); + } + + public static class PropertyName + { + public const string Duration = "duration"; + public const string Transition = "transition"; + public const string Ease = "ease"; + } +} diff --git a/addons/phantom_camera/scripts/resources/PhantomCameraTween.cs.uid b/addons/phantom_camera/scripts/resources/PhantomCameraTween.cs.uid new file mode 100644 index 0000000..dd50ead --- /dev/null +++ b/addons/phantom_camera/scripts/resources/PhantomCameraTween.cs.uid @@ -0,0 +1 @@ +uid://ybr5c2s0tfvx diff --git a/addons/phantom_camera/scripts/resources/camera_3d_resource.gd b/addons/phantom_camera/scripts/resources/camera_3d_resource.gd new file mode 100644 index 0000000..c630e16 --- /dev/null +++ b/addons/phantom_camera/scripts/resources/camera_3d_resource.gd @@ -0,0 +1,110 @@ +@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. + +enum KeepAspect { + KEEP_WIDTH = 0, ## Preserves the horizontal aspect ratio; also known as Vert- scaling. This is usually the best option for projects running in portrait mode, as taller aspect ratios will benefit from a wider vertical FOV. + KEEP_HEIGHT = 1, ## Preserves the vertical aspect ratio; also known as Hor+ scaling. This is usually the best option for projects running in landscape mode, as wider aspect ratios will automatically benefit from a wider horizontal FOV. +} + +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.keep_aspect]. +@export var keep_aspect: KeepAspect = KeepAspect.KEEP_HEIGHT: + set(value): + keep_aspect = value + emit_changed() + get: + return keep_aspect + +## Overrides [member Camera3D.cull_mask]. +@export_flags_3d_render var cull_mask: int = 1048575: + set(value): + cull_mask = value + emit_changed() + get: + return cull_mask + +## Overrides [member Camera3D.h_offset]. +@export_range(0, 1, 0.001, "or_greater", "or_less", "hide_slider", "suffix:m") var h_offset: float = 0: + set(value): + h_offset = value + emit_changed() + get: + return h_offset + +## Overrides [member Camera3D.v_offset]. +@export_range(0, 1, 0.001, "or_greater", "or_less", "hide_slider", "suffix:m") var v_offset: float = 0: + set(value): + v_offset = value + emit_changed() + +## Overrides [member Camera3D.projection]. +@export var projection: ProjectionType = ProjectionType.PERSPECTIVE: + set(value): + projection = value + notify_property_list_changed() + emit_changed() + get: + return projection + +## Overrides [member Camera3D.fov]. +@export_range(1, 179, 0.1, "degrees") var fov: float = 75: + set(value): + fov = value + emit_changed() + get: + return fov + +## Overrides [member Camera3D.size]. +@export_range(0.001, 100, 0.001, "suffix:m", "or_greater") var size: float = 1: + set(value): + size = value + emit_changed() + get: + return size + +## Overrides [member Camera3d.frustum_offset]. +@export var frustum_offset: Vector2 = Vector2.ZERO: + set(value): + frustum_offset = value + emit_changed() + get: + return frustum_offset + +## Overrides [member Camera3D.near]. +@export_range(0.001, 10, 0.001, "suffix:m", "or_greater") var near: float = 0.05: + set(value): + near = value + emit_changed() + get: + return near + +## Overrides [member Camera3D.far]. +@export_range(0.01, 4000, 0.001, "suffix:m","or_greater") var far: float = 4000: + set(value): + far = value + emit_changed() + get: + return far + + +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 diff --git a/addons/phantom_camera/scripts/resources/camera_3d_resource.gd.uid b/addons/phantom_camera/scripts/resources/camera_3d_resource.gd.uid new file mode 100644 index 0000000..e8378eb --- /dev/null +++ b/addons/phantom_camera/scripts/resources/camera_3d_resource.gd.uid @@ -0,0 +1 @@ +uid://bc2tn187qiatpcheck diff --git a/addons/phantom_camera/scripts/resources/phantom_camera_noise_2d.gd b/addons/phantom_camera/scripts/resources/phantom_camera_noise_2d.gd new file mode 100644 index 0000000..cc87dba --- /dev/null +++ b/addons/phantom_camera/scripts/resources/phantom_camera_noise_2d.gd @@ -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 diff --git a/addons/phantom_camera/scripts/resources/phantom_camera_noise_2d.gd.uid b/addons/phantom_camera/scripts/resources/phantom_camera_noise_2d.gd.uid new file mode 100644 index 0000000..45ae480 --- /dev/null +++ b/addons/phantom_camera/scripts/resources/phantom_camera_noise_2d.gd.uid @@ -0,0 +1 @@ +uid://dimvdouy8g0sv diff --git a/addons/phantom_camera/scripts/resources/phantom_camera_noise_3d.gd b/addons/phantom_camera/scripts/resources/phantom_camera_noise_3d.gd new file mode 100644 index 0000000..6cf840f --- /dev/null +++ b/addons/phantom_camera/scripts/resources/phantom_camera_noise_3d.gd @@ -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 diff --git a/addons/phantom_camera/scripts/resources/phantom_camera_noise_3d.gd.uid b/addons/phantom_camera/scripts/resources/phantom_camera_noise_3d.gd.uid new file mode 100644 index 0000000..42a0694 --- /dev/null +++ b/addons/phantom_camera/scripts/resources/phantom_camera_noise_3d.gd.uid @@ -0,0 +1 @@ +uid://cuffvge5ad4aa diff --git a/addons/phantom_camera/scripts/resources/tween_resource.gd b/addons/phantom_camera/scripts/resources/tween_resource.gd new file mode 100644 index 0000000..0a3b46f --- /dev/null +++ b/addons/phantom_camera/scripts/resources/tween_resource.gd @@ -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.0 + +## 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 diff --git a/addons/phantom_camera/scripts/resources/tween_resource.gd.uid b/addons/phantom_camera/scripts/resources/tween_resource.gd.uid new file mode 100644 index 0000000..a0f2cf1 --- /dev/null +++ b/addons/phantom_camera/scripts/resources/tween_resource.gd.uid @@ -0,0 +1 @@ +uid://8umksf8e80fw diff --git a/addons/phantom_camera/themes/button_focus.tres b/addons/phantom_camera/themes/button_focus.tres new file mode 100644 index 0000000..e6fcc45 --- /dev/null +++ b/addons/phantom_camera/themes/button_focus.tres @@ -0,0 +1,17 @@ +[gd_resource type="StyleBoxFlat" format=3 uid="uid://p058hmj3uut0"] + +[resource] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.0784314, 0.109804, 0.129412, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 diff --git a/addons/phantom_camera/themes/button_hover.tres b/addons/phantom_camera/themes/button_hover.tres new file mode 100644 index 0000000..9d37a86 --- /dev/null +++ b/addons/phantom_camera/themes/button_hover.tres @@ -0,0 +1,13 @@ +[gd_resource type="StyleBoxFlat" format=3 uid="uid://5weqvkjsfso3"] + +[resource] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.960784, 0.960784, 0.960784, 1) +border_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 diff --git a/addons/phantom_camera/themes/button_normal.tres b/addons/phantom_camera/themes/button_normal.tres new file mode 100644 index 0000000..4eae33d --- /dev/null +++ b/addons/phantom_camera/themes/button_normal.tres @@ -0,0 +1,17 @@ +[gd_resource type="StyleBoxFlat" format=3 uid="uid://bclbwo3xrdat0"] + +[resource] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.0784314, 0.109804, 0.129412, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 diff --git a/addons/phantom_camera/themes/theme.tres b/addons/phantom_camera/themes/theme.tres new file mode 100644 index 0000000..7ce53d8 --- /dev/null +++ b/addons/phantom_camera/themes/theme.tres @@ -0,0 +1,102 @@ +[gd_resource type="Theme" load_steps=12 format=3 uid="uid://bhppejri5dbsf"] + +[ext_resource type="FontFile" uid="uid://dve7mgsjik4dg" path="res://addons/phantom_camera/fonts/Nunito-Regular.ttf" id="1_5rtjh"] +[ext_resource type="StyleBox" uid="uid://5weqvkjsfso3" path="res://addons/phantom_camera/themes/button_hover.tres" id="2_du6h5"] +[ext_resource type="StyleBox" uid="uid://bclbwo3xrdat0" path="res://addons/phantom_camera/themes/button_normal.tres" id="3_a8j1f"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ek0y3"] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rjkuq"] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_x7u0w"] +content_margin_top = 2.0 +content_margin_right = 8.0 +bg_color = Color(0.0784314, 0.109804, 0.129412, 1) +border_width_top = 2 +border_width_right = 2 +border_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_right = 10 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dln2q"] +content_margin_top = 8.0 +content_margin_bottom = 8.0 +draw_center = false + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wk7ot"] +bg_color = Color(0.227451, 0.72549, 0.603922, 1) +border_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jidrt"] +bg_color = Color(0.960784, 0.960784, 0.960784, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_o2xwc"] +bg_color = Color(0.960784, 0.960784, 0.960784, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ul127"] +draw_center = false +border_width_left = 4 +border_width_right = 4 +border_color = Color(0.8, 0.8, 0.8, 0) + +[resource] +default_font = ExtResource("1_5rtjh") +Button/colors/font_color = Color(0.227451, 0.72549, 0.603922, 1) +Button/colors/font_focus_color = Color(0.0784314, 0.109804, 0.129412, 1) +Button/colors/font_hover_color = Color(0.0784314, 0.109804, 0.129412, 1) +Button/colors/font_hover_pressed_color = Color(0.0784314, 0.109804, 0.129412, 1) +Button/colors/font_pressed_color = Color(0.0784314, 0.109804, 0.129412, 1) +Button/colors/icon_focus_color = Color(0.0784314, 0.109804, 0.129412, 1) +Button/colors/icon_hover_color = Color(0.0784314, 0.109804, 0.129412, 1) +Button/colors/icon_hover_pressed_color = Color(0.227451, 0.72549, 0.603922, 1) +Button/colors/icon_normal_color = Color(0.0784314, 0.109804, 0.129412, 1) +Button/colors/icon_pressed_color = Color(0.227451, 0.72549, 0.603922, 1) +Button/styles/focus = SubResource("StyleBoxFlat_ek0y3") +Button/styles/hover = ExtResource("2_du6h5") +Button/styles/hover_pressed = null +Button/styles/normal = ExtResource("3_a8j1f") +Button/styles/pressed = SubResource("StyleBoxFlat_rjkuq") +PanelContainer/styles/panel = SubResource("StyleBoxFlat_x7u0w") +ScrollContainer/styles/panel = SubResource("StyleBoxFlat_dln2q") +VBoxContainer/constants/separation = 8 +VScrollBar/styles/grabber = SubResource("StyleBoxFlat_wk7ot") +VScrollBar/styles/grabber_highlight = SubResource("StyleBoxFlat_jidrt") +VScrollBar/styles/grabber_pressed = SubResource("StyleBoxFlat_o2xwc") +VScrollBar/styles/scroll = SubResource("StyleBoxFlat_ul127") diff --git a/project.godot b/project.godot index 2eddb43..aeea555 100644 --- a/project.godot +++ b/project.godot @@ -15,6 +15,10 @@ run/main_scene="uid://daxngklaqlyba" config/features=PackedStringArray("4.5", "Forward Plus") run/max_fps=60 +[autoload] + +PhantomCameraManager="*res://addons/phantom_camera/scripts/managers/phantom_camera_manager.gd" + [debug] gdscript/warnings/untyped_declaration=2 @@ -38,7 +42,7 @@ movie_writer/movie_file="demos/demo.avi" [editor_plugins] -enabled=PackedStringArray("res://addons/format_on_save/plugin.cfg", "res://addons/gdlint_plugin/plugin.cfg") +enabled=PackedStringArray("res://addons/format_on_save/plugin.cfg", "res://addons/gdlint_plugin/plugin.cfg", "res://addons/phantom_camera/plugin.cfg") [file_customization]