mirror of
https://gitlab.com/MaddoScientisto/cirnogodot.git
synced 2026-06-18 23:33:48 +00:00
3D Cameras with sweep and animation
This commit is contained in:
parent
4cc7a0c004
commit
7e76edc153
48 changed files with 3211 additions and 1511 deletions
19
addons/tattomoosa.vision_cone_3d/LICENSE.txt
Normal file
19
addons/tattomoosa.vision_cone_3d/LICENSE.txt
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2024 Matt O'Tousa
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
126
addons/tattomoosa.vision_cone_3d/README.md
Normal file
126
addons/tattomoosa.vision_cone_3d/README.md
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<div align="center">
|
||||
<br/>
|
||||
<br/>
|
||||
<img src="addons/tattomoosa.vision_cone_3d/icons/VisionCone3D.svg" width="100"/>
|
||||
<br/>
|
||||
<h1>
|
||||
VisionCone3D
|
||||
<br/>
|
||||
<sub>
|
||||
<sub>
|
||||
<sub>
|
||||
Simple but configurable 3D vision cone node for <a href="https://godotengine.org/">Godot</a>
|
||||
</sub>
|
||||
</sub>
|
||||
</sub>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
</h1>
|
||||
<br/>
|
||||
<br/>
|
||||
<img src="./readme_images/demo.png" height="140">
|
||||
<img src="./readme_images/stress_test.png" height="140">
|
||||
<img src="./readme_images/editor_view.png" height="140">
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
> Compatible with Godot 4.4 - see 4.3 branch for Godot 4.3 compatible version
|
||||
|
||||
Adds VisionCone3D, which tracks whether or not objects within its cone shape can be "seen".
|
||||
This can be used to let objects in your game "see" multiple objects efficiently.
|
||||
Default configuration should work for most use-cases out of the box.
|
||||
|
||||
## Features
|
||||
|
||||
* Edit range/angle of cone via 3D viewport editor gizmo
|
||||
* Debug visualization to easily diagnose any issues
|
||||
* Works with complex objects that have many collision shapes
|
||||
* Configurable vision probe settings allow tuning effectiveness and performance to your use-case
|
||||
* Ignore some physics bodies (eg the parent body)
|
||||
* Separate masks for bodies that can be seen and bodies that can only occlude other objects
|
||||
* Includes general-purpose ConeShape3D
|
||||
|
||||
## Installation
|
||||
|
||||
Install via the AssetLib tab within Godot by searching for VisionCone3D
|
||||
|
||||
## Usage
|
||||
|
||||
Add the VisionCone3D node to your scene. Turn on debug draw to see it working. Then you can...
|
||||
|
||||
### Connect to the body visible signals
|
||||
|
||||
These signals fire when a body is newly visible or newly hidden.
|
||||
|
||||
```python
|
||||
func _ready():
|
||||
vision_cone.body_sighted.connect(_on_body_sighted)
|
||||
vision_cone.body_hidden.connect(_on_body_hidden)
|
||||
|
||||
func _on_body_sighted(body: Node3D):
|
||||
print("body sighted: ", body.name)
|
||||
|
||||
func _on_body_hidden(body: Node3D):
|
||||
print("body hidden: ", body.name)
|
||||
```
|
||||
|
||||
### Poll the currently visible bodies
|
||||
|
||||
```python
|
||||
func _process(): # doesn't need to be during a physics frame
|
||||
print("bodies visible: ", vision_cone.get_visible_bodies())
|
||||
```
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
### Vision Test Mode
|
||||
|
||||
#### Center
|
||||
|
||||
Samples only the center point (position) of the CollisionShape. Most efficient, but least effective
|
||||
as if the center of a shape is obscured it won't be seen.
|
||||
|
||||
```python
|
||||
vision_cone.vision_test_mode = VisionCone3D.VisionTestMode.SAMPLE_CENTER
|
||||
```
|
||||
|
||||
#### Sample Random Vertices
|
||||
|
||||
Uses CollisionShape's `get_debug_mesh` to get a mesh representation of the CollisionShape,
|
||||
then samples random vertex points from that mesh.
|
||||
Effectiveness determined by the max body count and max probe per shape count
|
||||
|
||||
```python
|
||||
vision_cone.vision_test_mode = VisionCone3D.VisionTestMode.SAMPLE_RANDOM_VERTICES
|
||||
vision_cone.vision_test_max_body_count = 50 # Bodies probed, per-frame
|
||||
vision_cone.vision_test_shape_max_probe_count = 5 # Probes per hidden shape
|
||||
```
|
||||
|
||||
### Collision Masks
|
||||
|
||||
VisionCone3D has 2 collision masks, one used for bodies that can be seen by the cone and one for an environment,
|
||||
which can occlude seen bodies but is not itself probed for visibility.
|
||||
|
||||
For example, add the level collision layer to `collision_environment_mask` and the player/enemy/object collision layer to the `collision_mask`.
|
||||
The player/enemy/object can then hide behind the level, but no processing/probing will occur on the level collision geometry itself.
|
||||
|
||||
## The Future
|
||||
|
||||
This asset is still in development. I have some ideas for further performance tuning options, and I'm open to feedback on the usability and how to improve documentation or workflows.
|
||||
|
||||
### 2D Support?
|
||||
|
||||
I am open to adding a 2D version of this addon if there is sufficient interest.
|
||||
|
||||
See if [VisionCone2D](https://github.com/d-bucur/godot-vision-cone) meets your needs in the meantime. No relation.
|
||||
|
||||
## Upgrading
|
||||
|
||||
### 0.1.0 -> 0.2.0
|
||||
|
||||
v0.2.0 has significant performance improvements. Probably should have waited a few days before publishing. It probably doesn't have any users yet, but just in case...
|
||||
|
||||
* Use "Change Type..." on your VisionCone3Ds and select Area3D.
|
||||
* Use new ConeShape3D for all your cone-y collision needs
|
||||
246
addons/tattomoosa.vision_cone_3d/examples/demo.tscn
Normal file
246
addons/tattomoosa.vision_cone_3d/examples/demo.tscn
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,71 @@
|
|||
extends PanelContainer
|
||||
|
||||
@export var vision_cone : VisionCone3D
|
||||
@export var cameras : Array[Camera3D] = []
|
||||
|
||||
@onready var vision_test_center_checkbox : CheckBox = %VisionTestCenterCheckBox
|
||||
@onready var vision_test_scatter_checkbox : CheckBox = %VisionTestScatterCheckBox
|
||||
@onready var raycast_count_slider : Slider = %RaycastsPerFrameSlider
|
||||
@onready var angle_slider : Slider = %AngleSlider
|
||||
@onready var range_slider : Slider = %RangeSlider
|
||||
@onready var rotation_slider : Slider = %ObserverRotationSlider
|
||||
@onready var position_slider : Slider = %ObserverPositionSlider
|
||||
@onready var max_bodies_slider : Slider = %MaxBodiesSlider
|
||||
@onready var switch_camera_button : Button = %SwitchCameraButton
|
||||
|
||||
func _ready():
|
||||
vision_test_center_checkbox.toggled.connect(_set_center)
|
||||
vision_test_scatter_checkbox.toggled.connect(_set_scatter)
|
||||
raycast_count_slider.value_changed.connect(func(value: float): vision_cone.vision_test_shape_max_probe_count = value)
|
||||
angle_slider.value_changed.connect(func(value: float): vision_cone.angle = value)
|
||||
range_slider.value_changed.connect(func(value: float): vision_cone.range = value)
|
||||
rotation_slider.value_changed.connect(func(value: float): vision_cone.get_parent().rotation_degrees.y = -value)
|
||||
position_slider.value_changed.connect(func(value: float): vision_cone.get_parent().position.x = value)
|
||||
max_bodies_slider.value_changed.connect(func(value: float): vision_cone.vision_test_max_body_count = value)
|
||||
if cameras.is_empty():
|
||||
switch_camera_button.hide()
|
||||
else:
|
||||
cameras[0].current = true
|
||||
switch_camera_button.text = "Current Camera: " + cameras[0].name
|
||||
switch_camera_button.pressed.connect(
|
||||
func():
|
||||
for i in cameras.size():
|
||||
var cam := cameras[i]
|
||||
if cam.current:
|
||||
cam.current = false
|
||||
var next_cam : Camera3D
|
||||
if (i + 1) < cameras.size():
|
||||
next_cam = cameras[i + 1]
|
||||
else:
|
||||
next_cam = cameras[0]
|
||||
next_cam.current = true
|
||||
switch_camera_button.text = "Current Camera: " + next_cam.name
|
||||
return
|
||||
)
|
||||
|
||||
angle_slider.value = vision_cone.angle
|
||||
range_slider.value = vision_cone.range
|
||||
max_bodies_slider.value = vision_cone.vision_test_max_body_count
|
||||
raycast_count_slider.value = vision_cone.vision_test_shape_max_probe_count
|
||||
|
||||
if vision_cone.get_parent() is CharacterBody3D:
|
||||
vision_cone.get_parent().rotation_degrees.y = -rotation_slider.value
|
||||
vision_cone.get_parent().position.x = position_slider.value
|
||||
else:
|
||||
rotation_slider.get_parent().hide()
|
||||
position_slider.get_parent().hide()
|
||||
|
||||
_set_center(vision_test_center_checkbox.button_pressed)
|
||||
_set_scatter(vision_test_scatter_checkbox.button_pressed)
|
||||
|
||||
size.y = 0
|
||||
|
||||
func _set_center(value: bool):
|
||||
if !value:
|
||||
return
|
||||
vision_cone.vision_test_mode = VisionCone3D.VisionTestMode.SAMPLE_CENTER
|
||||
|
||||
func _set_scatter(value: bool):
|
||||
if !value:
|
||||
return
|
||||
vision_cone.vision_test_mode = VisionCone3D.VisionTestMode.SAMPLE_RANDOM_VERTICES
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://dof1xi7gcbq7s
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
[gd_scene load_steps=3 format=3 uid="uid://mqvpirrmppob"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_da376"]
|
||||
size = Vector3(1, 3, 1)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_mb6rt"]
|
||||
size = Vector3(1, 3, 1)
|
||||
|
||||
[node name="Blocker" type="StaticBody3D"]
|
||||
collision_layer = 2
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("BoxShape3D_da376")
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||
mesh = SubResource("BoxMesh_mb6rt")
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
[gd_scene load_steps=5 format=3 uid="uid://batptfh4cwpfb"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://cmgl8607thxgg" path="res://addons/tattomoosa.vision_cone_3d/examples/example_src/observable.tscn" id="1_p0bro"]
|
||||
|
||||
[sub_resource type="Animation" id="Animation_c3q4w"]
|
||||
length = 0.001
|
||||
tracks/0/type = "position_3d"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath(".")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = PackedFloat32Array(0, 1, -0.195181, 0, -1.77211)
|
||||
|
||||
[sub_resource type="Animation" id="Animation_jctf1"]
|
||||
resource_name = "back_and_forth"
|
||||
length = 8.0
|
||||
loop_mode = 1
|
||||
tracks/0/type = "position_3d"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath(".")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = PackedFloat32Array(0, 1, 0, 0, 0, 2.03333, 1, 4, 0, 0, 6.03333, 1, -4, 0, 0, 8, 1, 0, 0, 0)
|
||||
|
||||
[sub_resource type="AnimationLibrary" id="AnimationLibrary_al81f"]
|
||||
_data = {
|
||||
&"RESET": SubResource("Animation_c3q4w"),
|
||||
&"back_and_forth": SubResource("Animation_jctf1")
|
||||
}
|
||||
|
||||
[node name="MovingObservable" instance=ExtResource("1_p0bro")]
|
||||
transform = Transform3D(-1, 0, 8.74228e-08, 0, 1, 0, -8.74228e-08, 0, -1, -0.195181, 0, -1.77211)
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="." index="2"]
|
||||
libraries = {
|
||||
"": SubResource("AnimationLibrary_al81f")
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
[gd_scene load_steps=6 format=3 uid="uid://cmgl8607thxgg"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_dnr2f"]
|
||||
albedo_color = Color(0, 0, 0, 1)
|
||||
|
||||
[sub_resource type="CapsuleMesh" id="CapsuleMesh_nj3xo"]
|
||||
material = SubResource("StandardMaterial3D_dnr2f")
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ecabg"]
|
||||
albedo_color = Color(1, 0.635294, 1, 1)
|
||||
|
||||
[sub_resource type="CapsuleMesh" id="CapsuleMesh_5i4ah"]
|
||||
material = SubResource("StandardMaterial3D_ecabg")
|
||||
radius = 0.25
|
||||
height = 0.75
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_cf0b5"]
|
||||
|
||||
[node name="Observable" type="CharacterBody3D"]
|
||||
transform = Transform3D(-1, 0, 8.74228e-08, 0, 1, 0, -8.74228e-08, 0, -1, 0, 0, 0)
|
||||
metadata/_edit_group_ = true
|
||||
|
||||
[node name="Body" type="MeshInstance3D" parent="."]
|
||||
mesh = SubResource("CapsuleMesh_nj3xo")
|
||||
|
||||
[node name="Face" type="MeshInstance3D" parent="Body"]
|
||||
transform = Transform3D(-4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0, 1, 0, 0.386695, -0.364832)
|
||||
mesh = SubResource("CapsuleMesh_5i4ah")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("CapsuleShape3D_cf0b5")
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
[gd_scene load_steps=6 format=3 uid="uid://brqivkckug8uc"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cmgcelj1qxg8o" path="res://addons/tattomoosa.vision_cone_3d/src/VisionCone3D.gd" id="1_t4wly"]
|
||||
|
||||
[sub_resource type="CapsuleMesh" id="CapsuleMesh_td2ym"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ecabg"]
|
||||
albedo_color = Color(1, 0.635294, 1, 1)
|
||||
|
||||
[sub_resource type="CapsuleMesh" id="CapsuleMesh_5i4ah"]
|
||||
material = SubResource("StandardMaterial3D_ecabg")
|
||||
radius = 0.25
|
||||
height = 0.75
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_cf0b5"]
|
||||
|
||||
[node name="Observer" type="CharacterBody3D"]
|
||||
metadata/_edit_group_ = true
|
||||
|
||||
[node name="VisionCone3D" type="Area3D" parent="." node_paths=PackedStringArray("vision_test_ignore_bodies")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.41062, -0.023833)
|
||||
script = ExtResource("1_t4wly")
|
||||
debug_draw = true
|
||||
vision_test_ignore_bodies = [NodePath("..")]
|
||||
|
||||
[node name="Body" type="MeshInstance3D" parent="."]
|
||||
mesh = SubResource("CapsuleMesh_td2ym")
|
||||
|
||||
[node name="Face" type="MeshInstance3D" parent="Body"]
|
||||
transform = Transform3D(-4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0, 1, 0, 0.386695, -0.364832)
|
||||
mesh = SubResource("CapsuleMesh_5i4ah")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("CapsuleShape3D_cf0b5")
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
extends Label
|
||||
|
||||
@export var range_control : Range
|
||||
|
||||
func _ready():
|
||||
range_control.value_changed.connect(_set_displayed_value)
|
||||
_set_displayed_value(range_control.value)
|
||||
|
||||
func _set_displayed_value(_value: float):
|
||||
text = str(_value)
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://be67a8jv24c5o
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
[gd_scene load_steps=4 format=3 uid="uid://cdbsstpvtrvhd"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dof1xi7gcbq7s" path="res://addons/tattomoosa.vision_cone_3d/examples/example_src/VisionConeDemoControls.gd" id="1_vf8s6"]
|
||||
[ext_resource type="Script" uid="uid://be67a8jv24c5o" path="res://addons/tattomoosa.vision_cone_3d/examples/example_src/ui/ValueLabel.gd" id="2_8lgvy"]
|
||||
|
||||
[sub_resource type="ButtonGroup" id="ButtonGroup_dqtte"]
|
||||
|
||||
[node name="VisionConeControls" type="PanelContainer"]
|
||||
offset_left = 13.0
|
||||
offset_top = 14.0
|
||||
offset_right = 308.0
|
||||
offset_bottom = 24.0
|
||||
script = ExtResource("1_vf8s6")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 20
|
||||
|
||||
[node name="Range" type="VBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/Range"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/Range/HBoxContainer2"]
|
||||
layout_mode = 2
|
||||
text = "Range:"
|
||||
|
||||
[node name="RangeValueLabel" type="Label" parent="VBoxContainer/Range/HBoxContainer2" node_paths=PackedStringArray("range_control")]
|
||||
layout_mode = 2
|
||||
script = ExtResource("2_8lgvy")
|
||||
range_control = NodePath("../../RangeSlider")
|
||||
|
||||
[node name="RangeSlider" type="HSlider" parent="VBoxContainer/Range"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
min_value = 1.0
|
||||
max_value = 40.0
|
||||
step = 0.5
|
||||
value = 20.0
|
||||
|
||||
[node name="Angle" type="VBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/Angle"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/Angle/HBoxContainer2"]
|
||||
layout_mode = 2
|
||||
text = "Angle:"
|
||||
|
||||
[node name="AngleValueLabel" type="Label" parent="VBoxContainer/Angle/HBoxContainer2" node_paths=PackedStringArray("range_control")]
|
||||
layout_mode = 2
|
||||
script = ExtResource("2_8lgvy")
|
||||
range_control = NodePath("../../AngleSlider")
|
||||
|
||||
[node name="AngleSlider" type="HSlider" parent="VBoxContainer/Angle"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
min_value = 0.1
|
||||
max_value = 90.0
|
||||
step = 0.1
|
||||
value = 45.0
|
||||
|
||||
[node name="VisionTestModeControls" type="VBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label2" type="Label" parent="VBoxContainer/VisionTestModeControls"]
|
||||
layout_mode = 2
|
||||
text = "Vision Test Mode"
|
||||
|
||||
[node name="VisionTestMode" type="HBoxContainer" parent="VBoxContainer/VisionTestModeControls"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="VisionTestCenterCheckBox" type="CheckBox" parent="VBoxContainer/VisionTestModeControls/VisionTestMode"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
button_group = SubResource("ButtonGroup_dqtte")
|
||||
text = "Center"
|
||||
|
||||
[node name="VisionTestScatterCheckBox" type="CheckBox" parent="VBoxContainer/VisionTestModeControls/VisionTestMode"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
button_pressed = true
|
||||
button_group = SubResource("ButtonGroup_dqtte")
|
||||
text = "Scatter"
|
||||
|
||||
[node name="ProbesPerFrame" type="VBoxContainer" parent="VBoxContainer/VisionTestModeControls"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/VisionTestModeControls/ProbesPerFrame"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/VisionTestModeControls/ProbesPerFrame/HBoxContainer2"]
|
||||
layout_mode = 2
|
||||
text = "Max Probes Per Shape:"
|
||||
|
||||
[node name="RaycastPerFrameValueLabel" type="Label" parent="VBoxContainer/VisionTestModeControls/ProbesPerFrame/HBoxContainer2" node_paths=PackedStringArray("range_control")]
|
||||
layout_mode = 2
|
||||
script = ExtResource("2_8lgvy")
|
||||
range_control = NodePath("../../RaycastsPerFrameSlider")
|
||||
|
||||
[node name="RaycastsPerFrameSlider" type="HSlider" parent="VBoxContainer/VisionTestModeControls/ProbesPerFrame"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
min_value = 1.0
|
||||
value = 10.0
|
||||
|
||||
[node name="MaxBodies" type="VBoxContainer" parent="VBoxContainer/VisionTestModeControls"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/VisionTestModeControls/MaxBodies"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/VisionTestModeControls/MaxBodies/HBoxContainer2"]
|
||||
layout_mode = 2
|
||||
text = "Max Bodies Per Frame:"
|
||||
|
||||
[node name="MaxBodiesFrameValueLabel" type="Label" parent="VBoxContainer/VisionTestModeControls/MaxBodies/HBoxContainer2" node_paths=PackedStringArray("range_control")]
|
||||
layout_mode = 2
|
||||
script = ExtResource("2_8lgvy")
|
||||
range_control = NodePath("../../MaxBodiesSlider")
|
||||
|
||||
[node name="MaxBodiesSlider" type="HSlider" parent="VBoxContainer/VisionTestModeControls/MaxBodies"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
min_value = 1.0
|
||||
max_value = 500.0
|
||||
value = 11.0
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="SwitchCameraButton" type="Button" parent="VBoxContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Switch Camera"
|
||||
|
||||
[node name="ObserverRotation" type="VBoxContainer" parent="VBoxContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/VBoxContainer/ObserverRotation"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/VBoxContainer/ObserverRotation/HBoxContainer2"]
|
||||
layout_mode = 2
|
||||
text = "Observer Rotation:"
|
||||
|
||||
[node name="ObserverRotationValueLabel" type="Label" parent="VBoxContainer/VBoxContainer/ObserverRotation/HBoxContainer2" node_paths=PackedStringArray("range_control")]
|
||||
layout_mode = 2
|
||||
script = ExtResource("2_8lgvy")
|
||||
range_control = NodePath("../../ObserverRotationSlider")
|
||||
|
||||
[node name="ObserverRotationSlider" type="HSlider" parent="VBoxContainer/VBoxContainer/ObserverRotation"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
min_value = -90.0
|
||||
max_value = 90.0
|
||||
|
||||
[node name="ObserverPosition" type="VBoxContainer" parent="VBoxContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/VBoxContainer/ObserverPosition"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/VBoxContainer/ObserverPosition/HBoxContainer2"]
|
||||
layout_mode = 2
|
||||
text = "Observer Position:"
|
||||
|
||||
[node name="ObserverPositionValueLabel" type="Label" parent="VBoxContainer/VBoxContainer/ObserverPosition/HBoxContainer2" node_paths=PackedStringArray("range_control")]
|
||||
layout_mode = 2
|
||||
script = ExtResource("2_8lgvy")
|
||||
range_control = NodePath("../../ObserverPositionSlider")
|
||||
|
||||
[node name="ObserverPositionSlider" type="HSlider" parent="VBoxContainer/VBoxContainer/ObserverPosition"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
min_value = -8.0
|
||||
max_value = 8.0
|
||||
step = 0.1
|
||||
26
addons/tattomoosa.vision_cone_3d/examples/minimal.tscn
Normal file
26
addons/tattomoosa.vision_cone_3d/examples/minimal.tscn
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
[gd_scene load_steps=4 format=3 uid="uid://doqrupj5l86sr"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cmgcelj1qxg8o" path="res://addons/tattomoosa.vision_cone_3d/src/VisionCone3D.gd" id="1_78jke"]
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_si8ep"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_bebrf"]
|
||||
|
||||
[node name="Node3D" type="Node3D"]
|
||||
|
||||
[node name="VisionCone3D" type="Area3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 3.00271)
|
||||
script = ExtResource("1_78jke")
|
||||
debug_draw = true
|
||||
|
||||
[node name="StaticBody3D" type="StaticBody3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -2.66644)
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody3D"]
|
||||
mesh = SubResource("BoxMesh_si8ep")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D"]
|
||||
shape = SubResource("BoxShape3D_bebrf")
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, 0, 8.38814, 0)
|
||||
33
addons/tattomoosa.vision_cone_3d/icons/GizmoVisionCone.svg
Normal file
33
addons/tattomoosa.vision_cone_3d/icons/GizmoVisionCone.svg
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 28.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{display:none;}
|
||||
.st1{display:inline;fill-opacity:0.294;}
|
||||
.st2{display:inline;fill:#FFFFFF;}
|
||||
.st3{fill:#FFFFFF;}
|
||||
.st4{fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||
.st5{fill:none;stroke:#000000;stroke-width:3;stroke-miterlimit:10;}
|
||||
.st6{fill:#231F20;}
|
||||
.st7{fill:none;stroke:#000000;stroke-width:4;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_1_00000012455106065017520430000009640333213232038794_" class="st0">
|
||||
<path class="st1" d="M52,4c-6.6,0-12,5.4-12,12v26.6C27.7,49.2,20,62,20,76v4h28.6c2.3,8.5,11.1,13.5,19.7,11.2
|
||||
c5.4-1.5,9.7-5.7,11.2-11.2H108v-4c0-14-7.7-26.8-20-33.4V16c0-6.6-5.4-12-12-12H52z M40.3,82c-1.6-0.1-3.2,0.3-4.6,1.1l-10.4,6
|
||||
c-3.8,2.2-5.1,7.1-2.9,10.9s7.1,5.1,10.9,2.9l10.4-6c3.8-2.2,5.1-7.1,2.9-10.9C45.4,83.7,43,82.2,40.3,82z M87.7,82
|
||||
c-4.4,0.3-7.7,4.1-7.5,8.5c0.2,2.7,1.7,5.1,4,6.4l10.4,6c3.8,2.2,8.7,0.9,10.9-2.9s0.9-8.7-2.9-10.9l-10.4-6
|
||||
C90.9,82.3,89.3,81.9,87.7,82L87.7,82z M64,96c-4.4,0-8,3.6-8,8v12c0,4.4,3.6,8,8,8s8-3.6,8-8v-12C72,99.6,68.4,96,64,96z"/>
|
||||
<path class="st2" d="M52,8c-4.4,0-8,3.6-8,8v28.9C31.6,51.3,24,63.2,24,76h28c0,6.6,5.4,12,12,12s12-5.4,12-12h28
|
||||
c0-12.8-7.6-24.7-20-31.1V16c0-4.4-3.6-8-8-8H52z M40,86c-0.8-0.1-1.6,0.1-2.3,0.5l-10.4,6c-1.9,1.1-2.6,3.6-1.5,5.5
|
||||
s3.6,2.6,5.5,1.5l10.4-6c1.9-1.1,2.6-3.6,1.5-5.5C42.5,86.8,41.4,86.1,40,86L40,86z M88,86c-2.2,0.1-3.9,2-3.7,4.3
|
||||
c0.1,1.3,0.8,2.5,2,3.2l10.4,6c1.9,1.1,4.4,0.4,5.5-1.5s0.4-4.4-1.5-5.5l-10.4-6C89.6,86.1,88.8,86,88,86L88,86z M64,100
|
||||
c-2.2,0-4,1.8-4,4v12c0,2.2,1.8,4,4,4c2.2,0,4-1.8,4-4v-12C68,101.8,66.2,100,64,100z"/>
|
||||
</g>
|
||||
<path class="st3" d="M6.2,66.2l85.1,57.5L88,112.5c-8.8-30.2-8.8-62.4,0-92.6l3.3-11.2L6.2,66.2z"/>
|
||||
<path class="st4" d="M6.2,66.2l85.1,57.5L88,112.5c-8.8-30.2-8.8-62.4,0-92.6l3.3-11.2L6.2,66.2z"/>
|
||||
<ellipse class="st3" cx="107" cy="66.2" rx="15.7" ry="57.5"/>
|
||||
<ellipse class="st5" cx="107" cy="66.2" rx="15.7" ry="57.5"/>
|
||||
<ellipse class="st6" cx="98.3" cy="66.2" rx="3.6" ry="19.3"/>
|
||||
<ellipse class="st7" cx="99.3" cy="66.2" rx="3.6" ry="19.3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -0,0 +1,38 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://o1pkx5p4gmem"
|
||||
path.s3tc="res://.godot/imported/GizmoVisionCone.svg-ea8f76460bf35030690548bcee239273.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/tattomoosa.vision_cone_3d/icons/GizmoVisionCone.svg"
|
||||
dest_files=["res://.godot/imported/GizmoVisionCone.svg-ea8f76460bf35030690548bcee239273.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
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/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
|
||||
11
addons/tattomoosa.vision_cone_3d/icons/VisionCone3D.svg
Normal file
11
addons/tattomoosa.vision_cone_3d/icons/VisionCone3D.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 28.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:none;stroke:#FC7F7F;stroke-width:2;stroke-linecap:round;}
|
||||
.st1{fill:none;stroke:#FC7F7F;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;}
|
||||
</style>
|
||||
<polyline class="st0" points="10.6,1.7 2.7,8 10.6,14.3 "/>
|
||||
<ellipse class="st1" cx="11.6" cy="8" rx="1.4" ry="3.7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 662 B |
|
|
@ -0,0 +1,38 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://pgp4cqq8ov3o"
|
||||
path="res://.godot/imported/VisionCone3D.svg-2598d1ee3dd911c502082c26b2e835ec.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/tattomoosa.vision_cone_3d/icons/VisionCone3D.svg"
|
||||
dest_files=["res://.godot/imported/VisionCone3D.svg-2598d1ee3dd911c502082c26b2e835ec.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=true
|
||||
editor/convert_colors_with_editor_theme=true
|
||||
7
addons/tattomoosa.vision_cone_3d/plugin.cfg
Normal file
7
addons/tattomoosa.vision_cone_3d/plugin.cfg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[plugin]
|
||||
|
||||
name="VisionCone3D"
|
||||
description=""
|
||||
author="Tattomoosa"
|
||||
version="0.2.0"
|
||||
script="plugin.gd"
|
||||
21
addons/tattomoosa.vision_cone_3d/plugin.gd
Normal file
21
addons/tattomoosa.vision_cone_3d/plugin.gd
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
const DEBUG_DRAW_TOOL := "Set Vision Cone Debug Draw Visibility"
|
||||
const VisionCone3DGizmoPlugin := preload ("./src/editor/VisionCone3DGizmoPlugin.gd")
|
||||
var gizmo : VisionCone3DGizmoPlugin = VisionCone3DGizmoPlugin.new()
|
||||
|
||||
func _enter_tree() -> void:
|
||||
gizmo.undo_redo = get_undo_redo()
|
||||
add_node_3d_gizmo_plugin(gizmo)
|
||||
|
||||
# add_tool_menu_item(
|
||||
# DEBUG_DRAW_TOOL,
|
||||
# func():
|
||||
# VisionCone3D.debug_draw_all = !VisionCone3D.debug_draw_all
|
||||
# )
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
remove_node_3d_gizmo_plugin(gizmo)
|
||||
# remove_tool_menu_item(DEBUG_DRAW_TOOL)
|
||||
1
addons/tattomoosa.vision_cone_3d/plugin.gd.uid
Normal file
1
addons/tattomoosa.vision_cone_3d/plugin.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://yxw0yuks1mme
|
||||
50
addons/tattomoosa.vision_cone_3d/src/ConeShape3D.gd
Normal file
50
addons/tattomoosa.vision_cone_3d/src/ConeShape3D.gd
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
@tool
|
||||
class_name ConeShape3D
|
||||
extends ConvexPolygonShape3D
|
||||
|
||||
## The height of the cone
|
||||
@export var height : float = 2.0:
|
||||
set(value):
|
||||
height = value
|
||||
_request_resize()
|
||||
## The radius of the bottom of the cone
|
||||
@export var radius : float = 0.5:
|
||||
set(value):
|
||||
radius = value
|
||||
_request_resize()
|
||||
## The number of radial segments of the cone
|
||||
@export var resolution : int = 8:
|
||||
set(value):
|
||||
resolution = value
|
||||
_request_resize()
|
||||
|
||||
# Resize requested
|
||||
var pending_resize := false
|
||||
|
||||
# Update size to initial state
|
||||
func _init() -> void:
|
||||
_request_resize()
|
||||
|
||||
# Will only resize once per frame, during idle time
|
||||
func _request_resize() -> void:
|
||||
if !pending_resize:
|
||||
_update_size.call_deferred()
|
||||
pending_resize = true
|
||||
|
||||
# Updates shape size
|
||||
func _update_size() -> void:
|
||||
points = _make_cone_polygon_points()
|
||||
pending_resize = false
|
||||
|
||||
# Makes a cone polygon
|
||||
@warning_ignore("return_value_discarded")
|
||||
func _make_cone_polygon_points() -> PackedVector3Array:
|
||||
var pts : PackedVector3Array = []
|
||||
var top : Vector3 = Vector3(0, height / 2, 0)
|
||||
for i in resolution:
|
||||
var angle := float(i) * TAU / resolution
|
||||
var x := cos(angle) * radius
|
||||
var y := sin(angle) * radius
|
||||
pts.push_back(Vector3(x, -height/2, y))
|
||||
pts.push_back(top)
|
||||
return pts
|
||||
1
addons/tattomoosa.vision_cone_3d/src/ConeShape3D.gd.uid
Normal file
1
addons/tattomoosa.vision_cone_3d/src/ConeShape3D.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bc32kei4pauw1
|
||||
421
addons/tattomoosa.vision_cone_3d/src/VisionCone3D.gd
Normal file
421
addons/tattomoosa.vision_cone_3d/src/VisionCone3D.gd
Normal file
|
|
@ -0,0 +1,421 @@
|
|||
@tool
|
||||
@icon("../icons/VisionCone3D.svg")
|
||||
class_name VisionCone3D
|
||||
extends Area3D
|
||||
## Provides a "Vision Cone", a cone-shaped area where objects are then
|
||||
## probed for visibility via ray casts
|
||||
|
||||
## Emitted when a body is newly visible
|
||||
signal body_sighted(body: Node3D)
|
||||
|
||||
## Emitted when a body is newly not visible
|
||||
signal body_hidden(body: Node3D)
|
||||
|
||||
## Emitted when the cone shape changes
|
||||
signal shape_changed
|
||||
|
||||
#region VisionCone3D
|
||||
|
||||
#region members
|
||||
## Determines how visibility is probed for bodies within the cone area
|
||||
##
|
||||
enum VisionTestMode{
|
||||
## Samples the center of each CollisionShape. Maximum performance, least reliability
|
||||
SAMPLE_CENTER,
|
||||
## Samples random vertices of each CollisionShape, up to `vision_test_shape_max_probe_count`
|
||||
## for hidden objects
|
||||
## If shape was visible at last frame, tests last successful probe position first
|
||||
SAMPLE_RANDOM_VERTICES,
|
||||
## Gets a list of points where shapes collide from the physics engine
|
||||
# SAMPLE_COLLIDE_SHAPE,
|
||||
}
|
||||
|
||||
const VisionConeDebugVisualizer3D := preload("./debug/VisionConeDebugVisualizer3D.gd")
|
||||
|
||||
|
||||
## Distance that can be seen (the height of the vision cone)
|
||||
@warning_ignore("shadowed_global_identifier")
|
||||
@export var range := 20.0:
|
||||
set(v): range = v; _update_shape()
|
||||
|
||||
## Angle of the vision cone
|
||||
@export_range(0, 150) var angle := 45.0:
|
||||
set(v): angle = v; _update_shape()
|
||||
|
||||
## Whether or not to draw debug information
|
||||
@export var debug_draw := false:
|
||||
set(v):
|
||||
debug_draw = v
|
||||
if debug_draw and !_debug_visualizer:
|
||||
_debug_visualizer = VisionConeDebugVisualizer3D.new()
|
||||
add_child(_debug_visualizer)
|
||||
elif !debug_draw and _debug_visualizer:
|
||||
_debug_visualizer.queue_free()
|
||||
|
||||
@export_group("Vision Test", "vision_test_")
|
||||
|
||||
## Which VisionTestMode to use to determine if a shape is visible
|
||||
@export var vision_test_mode : VisionTestMode = VisionTestMode.SAMPLE_RANDOM_VERTICES
|
||||
## List of bodies to ignore in vision probing
|
||||
##
|
||||
## Useful for eg the VisionCone3D's parent body
|
||||
@export var vision_test_ignore_bodies : Array[PhysicsBody3D]
|
||||
|
||||
@export_subgroup("Per-frame probe settings")
|
||||
## Maximum amount of shape probes (per shape, per frame)
|
||||
@export var vision_test_shape_max_probe_count : int = 5
|
||||
## Maximum number of bodies to check, per-frame
|
||||
##
|
||||
## All bodies will still be evaluated as it will cycle through them
|
||||
## frame by frame, but `body_sighted` or `body_hidden` may lag behind
|
||||
## by some frames.
|
||||
@export var vision_test_max_body_count : int = 10
|
||||
|
||||
@export_group("Collision", "collision_")
|
||||
## Collision layer "hoisted" up from Area3D for convenience
|
||||
@export_flags_3d_physics var collision_layer_ : int = 1:
|
||||
get:
|
||||
return collision_layer
|
||||
set(value):
|
||||
collision_layer = value
|
||||
|
||||
## Collision mask "hoisted" up from Area3D for convenience
|
||||
##
|
||||
## This represents what can be "seen" and notified against. Generally useful for characters
|
||||
@export_flags_3d_physics var collision_mask_ : int = 1:
|
||||
get:
|
||||
return collision_mask
|
||||
set(value):
|
||||
collision_mask = value
|
||||
|
||||
## Collision mask of what objects can obscure visible objects (but don't need to
|
||||
## be tracked and probed to determine visibility)
|
||||
##
|
||||
## Generally useful for the environment but any node where you don't care
|
||||
## if its seen or not can be on this layer.
|
||||
## This layer only affects raycasts, which can collide with any layer
|
||||
## in either `collision_mask` or `collision_environment_mask`
|
||||
@export_flags_3d_physics var collision_environment_mask : int = 1
|
||||
|
||||
## Radius at the wide end of the vision cone
|
||||
var end_radius: float:
|
||||
get: return _get_end_radius()
|
||||
|
||||
# { Node3D "body" : Node3D "shape" }
|
||||
var _body_probe_data : Dictionary = {
|
||||
# Node3D "body" : [
|
||||
# "prober": VisionTestProber
|
||||
# ]
|
||||
}
|
||||
|
||||
var _last_probed_index : int = -1
|
||||
var _debug_visualizer : VisionConeDebugVisualizer3D
|
||||
var _collision_shape := CollisionShape3D.new()
|
||||
var _cone_shape := ConeShape3D.new()
|
||||
|
||||
#endregion members
|
||||
|
||||
## Returns a list of intersecting PhysicsBody3Ds. The overlapping body's
|
||||
## CollisionObject3D.collision_layer must be part of this area's CollisionObject3D.collision_mask
|
||||
## in order to be detected.
|
||||
func get_visible_bodies() -> Array[PhysicsBody3D]:
|
||||
var bodies : Array[PhysicsBody3D] = []
|
||||
for prober: VisionTestProber in _body_probe_data.values():
|
||||
bodies.push_back(prober.body)
|
||||
return bodies
|
||||
|
||||
## Whether or not a given point in global space is within the cone's
|
||||
## angle.
|
||||
func point_within_angle(global_point: Vector3) -> bool:
|
||||
var body_pos := -global_basis.z
|
||||
var pos := global_point - global_position
|
||||
var angle_to := pos.angle_to(body_pos)
|
||||
var angle_deg := rad_to_deg(angle_to)
|
||||
return angle_deg <= (angle / 2)
|
||||
|
||||
## Whether or not a given point in global space is within the cone
|
||||
func point_within_cone(global_point: Vector3) -> bool:
|
||||
var local_point := to_local(global_point)
|
||||
var z_distance : float = abs(local_point.z)
|
||||
if z_distance < 0 or z_distance > range:
|
||||
return false
|
||||
return point_within_angle(global_point)
|
||||
|
||||
func _init() -> void:
|
||||
add_child(_collision_shape)
|
||||
_collision_shape.shape = _cone_shape
|
||||
_collision_shape.rotation_degrees.x = 90
|
||||
_update_shape()
|
||||
# only true when copied
|
||||
if _debug_visualizer:
|
||||
_debug_visualizer.vision_cone = self
|
||||
|
||||
var err := body_shape_entered.connect(_on_body_shape_entered)
|
||||
if err != OK:
|
||||
push_warning("VisionCone3D body_shape_entered: ", error_string(err))
|
||||
err = body_shape_exited.connect(_on_body_shape_exited)
|
||||
if err != OK:
|
||||
push_warning("VisionCone3D body_shape_exited: ", error_string(err))
|
||||
|
||||
@warning_ignore("return_value_discarded")
|
||||
func _physics_process(_delta: float) -> void:
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
if !monitoring:
|
||||
return
|
||||
|
||||
var bodies_to_probe := _get_bodies_to_probe_this_frame()
|
||||
for body: CollisionObject3D in bodies_to_probe:
|
||||
if !is_instance_valid(body):
|
||||
push_warning("erasing invalid body")
|
||||
_body_probe_data.erase(body)
|
||||
continue
|
||||
_probe_body(body)
|
||||
|
||||
func _get_bodies_to_probe_this_frame() -> Array: # Array[CollisionObject3D]:
|
||||
var all_bodies := _body_probe_data.keys()
|
||||
if all_bodies.is_empty():
|
||||
return []
|
||||
if all_bodies.size() < vision_test_max_body_count:
|
||||
_last_probed_index = -1
|
||||
return all_bodies
|
||||
|
||||
var start_index := _last_probed_index + 1
|
||||
var to_end := all_bodies.slice(start_index, start_index + vision_test_max_body_count)
|
||||
var counted := to_end.size()
|
||||
var end_index : int = min(vision_test_max_body_count - counted, start_index)
|
||||
var from_start := all_bodies.slice(0, end_index)
|
||||
_last_probed_index = from_start.size() - 1 if from_start.size() > 0 else start_index + counted - 1
|
||||
return (to_end + from_start)
|
||||
|
||||
func _probe_body(body: CollisionObject3D) -> void:
|
||||
var body_was_visible_last_frame := false
|
||||
var body_is_visible := false
|
||||
var body_probes : Array[VisionTestProber]
|
||||
body_probes.assign(_body_probe_data[body])
|
||||
|
||||
for prober in body_probes:
|
||||
if prober.visible:
|
||||
body_was_visible_last_frame = true
|
||||
prober.probe()
|
||||
if prober.visible:
|
||||
body_is_visible = true
|
||||
|
||||
var body_visibility_changed := body_is_visible != body_was_visible_last_frame
|
||||
|
||||
if body_visibility_changed:
|
||||
if body_is_visible:
|
||||
body_sighted.emit(body)
|
||||
else:
|
||||
body_hidden.emit(body)
|
||||
|
||||
func _update_shape() -> void:
|
||||
_cone_shape.height = range
|
||||
_cone_shape.radius = end_radius
|
||||
_collision_shape.position.z = -range / 2
|
||||
update_gizmos()
|
||||
shape_changed.emit()
|
||||
|
||||
func _get_collision_shape_node_in_body(body: PhysicsBody3D, body_shape_index: int) -> Node3D:
|
||||
if !body:
|
||||
return null
|
||||
var body_shape_owner : int = body.shape_find_owner(body_shape_index)
|
||||
return body.shape_owner_get_owner(body_shape_owner)
|
||||
|
||||
func _get_end_radius() -> float:
|
||||
var angle_rad := deg_to_rad(angle / 2)
|
||||
return range * tan(angle_rad)
|
||||
|
||||
func _get_prober_for_shape(shape: CollisionShape3D, body: CollisionObject3D) -> VisionTestProber:
|
||||
for prober: VisionTestProber in _body_probe_data[body]:
|
||||
if prober.collision_shape == shape:
|
||||
return prober
|
||||
return null
|
||||
|
||||
@warning_ignore("return_value_discarded")
|
||||
func _on_body_shape_entered(
|
||||
_body_rid: RID,
|
||||
body: Node3D,
|
||||
body_shape_index: int,
|
||||
_local_shape_index: int,
|
||||
) -> void:
|
||||
# # weird!
|
||||
if !is_instance_valid(body):
|
||||
if _body_probe_data.has(body):
|
||||
_body_probe_data.erase(body)
|
||||
return
|
||||
var shape := _get_collision_shape_node_in_body(body, body_shape_index)
|
||||
var body_probes : Array[VisionTestProber] = _body_probe_data.get_or_add(
|
||||
body, [] as Array[VisionTestProber]
|
||||
)
|
||||
|
||||
var has_prober := _get_prober_for_shape(shape, body)
|
||||
if !has_prober:
|
||||
body_probes.push_back(VisionTestProber.new(self, shape, body))
|
||||
else:
|
||||
push_warning("Already has prober")
|
||||
|
||||
@warning_ignore("return_value_discarded")
|
||||
func _on_body_shape_exited(
|
||||
_body_rid: RID,
|
||||
body: Node3D,
|
||||
body_shape_index: int,
|
||||
_local_shape_index: int,
|
||||
) -> void:
|
||||
if !body:
|
||||
return
|
||||
var shape := _get_collision_shape_node_in_body(body, body_shape_index)
|
||||
var prober := _get_prober_for_shape(shape, body)
|
||||
var body_probers : Array[VisionTestProber]= _body_probe_data[body]
|
||||
body_probers.erase(prober)
|
||||
if body_probers.is_empty():
|
||||
_body_probe_data.erase(body)
|
||||
|
||||
|
||||
#endregion VisionCone3D
|
||||
|
||||
class VisionTestProber:
|
||||
## Useful for debugging probes
|
||||
const CONTINUE_PROBING_ON_SUCCESS := false
|
||||
|
||||
## Vision cone to probe for
|
||||
var vision_cone : VisionCone3D
|
||||
## Collision shape to probe
|
||||
var collision_shape: CollisionShape3D
|
||||
## Collision shape mesh representation
|
||||
var shape_probe_mesh: ArrayMesh
|
||||
## Collision shape's owning body
|
||||
var body : PhysicsBody3D
|
||||
|
||||
## Whether the probe found the shape to be visible
|
||||
var visible: bool = false
|
||||
## All probe results, for debugging
|
||||
var probe_results: Array[ProbeResult]
|
||||
|
||||
static var _rng := RandomNumberGenerator.new()
|
||||
|
||||
func _init(
|
||||
p_vision_cone: VisionCone3D,
|
||||
p_collision_shape: CollisionShape3D,
|
||||
p_body: PhysicsBody3D
|
||||
) -> void:
|
||||
vision_cone = p_vision_cone
|
||||
collision_shape = p_collision_shape
|
||||
body = p_body
|
||||
|
||||
func _probe_position(to: Vector3, shape_local_target: Vector3) -> ProbeResult:
|
||||
# Collide with bodies OR the environment
|
||||
var raycast_collision_mask := vision_cone.collision_mask | vision_cone.collision_environment_mask
|
||||
# can store reference to this?
|
||||
var space_state := vision_cone.get_world_3d().direct_space_state
|
||||
var from := vision_cone.global_position
|
||||
var exclude_bodies := vision_cone.vision_test_ignore_bodies\
|
||||
.filter(func(x: PhysicsBody3D) -> bool: return is_instance_valid(x))\
|
||||
.map(func(x: PhysicsBody3D) -> RID: return x.get_rid())
|
||||
var query := PhysicsRayQueryParameters3D.create(
|
||||
from,
|
||||
to,
|
||||
raycast_collision_mask,
|
||||
exclude_bodies
|
||||
)
|
||||
var result := space_state.intersect_ray(query)
|
||||
return ProbeResult.new(
|
||||
from,
|
||||
to,
|
||||
shape_local_target,
|
||||
result.collider if result.has("collider") else null
|
||||
)
|
||||
|
||||
func _get_last_visible_point_on_shape() -> Vector3:
|
||||
if probe_results.is_empty():
|
||||
push_warning("Shape was not determined to be visible during last _probe_position")
|
||||
|
||||
return Vector3.ZERO
|
||||
return probe_results[-1].shape_local_target
|
||||
|
||||
func _random_points_on_probe_mesh(count: int) -> PackedVector3Array:
|
||||
if !shape_probe_mesh:
|
||||
shape_probe_mesh = collision_shape.shape.get_debug_mesh()
|
||||
var vertices : PackedVector3Array = shape_probe_mesh.surface_get_arrays(0)[Mesh.ARRAY_VERTEX]
|
||||
var points : PackedVector3Array = []
|
||||
for point in count:
|
||||
points.push_back(vertices[_rng.randi_range(0, vertices.size() - 1)])
|
||||
return points
|
||||
|
||||
func _get_scatter_points(count: int) -> PackedVector3Array:
|
||||
var sample_points : PackedVector3Array = []
|
||||
# var random_point_count := vision_cone.vision_test_shape_max_probe_count
|
||||
sample_points.append_array(_random_points_on_probe_mesh(count))
|
||||
return sample_points
|
||||
|
||||
func _get_collide_points(count: int) -> PackedVector3Array:
|
||||
var sample_points : PackedVector3Array = []
|
||||
# var cone_shape := vision_cone._collision_shape.shape
|
||||
var observable_shape := collision_shape.shape
|
||||
var query := PhysicsShapeQueryParameters3D.new()
|
||||
query.collide_with_areas = true
|
||||
query.collide_with_bodies = false
|
||||
query.collision_mask = vision_cone.collision_layer
|
||||
query.shape_rid = observable_shape.get_rid()
|
||||
|
||||
var world_space := vision_cone.get_world_3d().direct_space_state
|
||||
var result := world_space.collide_shape(query)
|
||||
for i in result.size():
|
||||
if i % 2 == 1:
|
||||
continue
|
||||
sample_points.push_back(result[i])
|
||||
if sample_points.size() >= count:
|
||||
break
|
||||
return sample_points
|
||||
|
||||
func probe() -> void:
|
||||
var sample_points : PackedVector3Array = []
|
||||
var max_count := vision_cone.vision_test_shape_max_probe_count
|
||||
if visible:
|
||||
sample_points.append(_get_last_visible_point_on_shape())
|
||||
max_count -= 1
|
||||
match vision_cone.vision_test_mode:
|
||||
VisionTestMode.SAMPLE_CENTER:
|
||||
if !visible:
|
||||
sample_points.push_back(Vector3.ZERO)
|
||||
VisionTestMode.SAMPLE_RANDOM_VERTICES:
|
||||
sample_points.append_array(_get_scatter_points(max_count))
|
||||
|
||||
probe_results = []
|
||||
visible = false
|
||||
for shape_local_point in sample_points:
|
||||
var global_point := collision_shape.global_position +\
|
||||
(collision_shape.global_basis * shape_local_point)
|
||||
|
||||
if !vision_cone.point_within_cone(global_point):
|
||||
continue
|
||||
|
||||
var probe_result := _probe_position(global_point, shape_local_point)
|
||||
probe_results.push_back(probe_result)
|
||||
|
||||
# found body we were looking for
|
||||
if probe_result.collider == body:
|
||||
probe_result.visible = true
|
||||
visible = true
|
||||
if CONTINUE_PROBING_ON_SUCCESS:
|
||||
print_debug("visible - continuing to _probe_position")
|
||||
continue
|
||||
return
|
||||
|
||||
class ProbeResult:
|
||||
var start : Vector3
|
||||
var end : Vector3
|
||||
var shape_local_target : Vector3
|
||||
var collider : Node3D
|
||||
var visible : bool = false
|
||||
|
||||
func _init(
|
||||
p_start: Vector3,
|
||||
p_end: Vector3,
|
||||
p_shape_local_target: Vector3,
|
||||
p_collider: Node3D
|
||||
) -> void:
|
||||
start = p_start
|
||||
end = p_end
|
||||
shape_local_target = p_shape_local_target
|
||||
collider = p_collider
|
||||
1
addons/tattomoosa.vision_cone_3d/src/VisionCone3D.gd.uid
Normal file
1
addons/tattomoosa.vision_cone_3d/src/VisionCone3D.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cmgcelj1qxg8o
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
extends Node3D
|
||||
|
||||
const ProbeResult := VisionCone3D.VisionTestProber.ProbeResult
|
||||
|
||||
# TODO should be modifiable via EditorSettings
|
||||
const DEBUG_VISION_CONE_COLOR := Color(1, 1, 0, 0.02)
|
||||
# TODO should be modifiable via EditorSettings
|
||||
const DEBUG_RAY_COLOR_IS_VISIBLE := Color(Color.GREEN, 0.8)
|
||||
# TODO should be modifiable via EditorSettings
|
||||
const DEBUG_RAY_COLOR_IS_OBSTRUCTED := Color(Color.RED, 0.4)
|
||||
|
||||
@export var vision_cone : VisionCone3D
|
||||
|
||||
var debug_vision_cone_color := DEBUG_VISION_CONE_COLOR
|
||||
var debug_ray_color_is_visible := DEBUG_RAY_COLOR_IS_VISIBLE
|
||||
var debug_ray_color_in_cone := DEBUG_RAY_COLOR_IS_OBSTRUCTED
|
||||
|
||||
|
||||
var _bounds_renderer : MeshInstance3D
|
||||
var _probe_renderer : DebugProbeLineRenderer
|
||||
|
||||
func _init() -> void:
|
||||
# create cone renderer
|
||||
_bounds_renderer = MeshInstance3D.new()
|
||||
var mesh := CylinderMesh.new()
|
||||
mesh.material = make_visualizer_material()
|
||||
_bounds_renderer.mesh = mesh
|
||||
add_child(_bounds_renderer, false, INTERNAL_MODE_BACK)
|
||||
|
||||
_probe_renderer = DebugProbeLineRenderer.new()
|
||||
_probe_renderer.probe_success_material = make_visualizer_material(debug_ray_color_is_visible)
|
||||
_probe_renderer.probe_failure_material = make_visualizer_material(debug_ray_color_in_cone)
|
||||
add_child(_probe_renderer)
|
||||
|
||||
@warning_ignore("return_value_discarded")
|
||||
func _ready() -> void:
|
||||
vision_cone = get_parent()
|
||||
_probe_renderer.body_probe_data = vision_cone._body_probe_data
|
||||
vision_cone.shape_changed.connect(update_cone_shape)
|
||||
update_cone_shape()
|
||||
|
||||
func make_visualizer_material(albedo_color: Color = debug_vision_cone_color) -> StandardMaterial3D:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = albedo_color
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
return mat
|
||||
|
||||
func update_cone_shape() -> void:
|
||||
var m : CylinderMesh = _bounds_renderer.mesh
|
||||
m.top_radius = 0
|
||||
m.bottom_radius = vision_cone.end_radius
|
||||
m.height = vision_cone.range
|
||||
_bounds_renderer.rotation_degrees = Vector3(90, 0, 0)
|
||||
_bounds_renderer.position.z = -vision_cone.range / 2
|
||||
|
||||
class DebugProbeLineRenderer extends MeshInstance3D:
|
||||
var body_probe_data: Dictionary
|
||||
var probe_success_material : StandardMaterial3D
|
||||
var probe_failure_material : StandardMaterial3D
|
||||
|
||||
func _init() -> void:
|
||||
mesh = ImmediateMesh.new()
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
(mesh as ImmediateMesh).clear_surfaces()
|
||||
if body_probe_data.is_empty():
|
||||
return
|
||||
var successful : Array[ProbeResult] = []
|
||||
var failed : Array[ProbeResult] = []
|
||||
|
||||
for prober_list : Array[VisionCone3D.VisionTestProber] in body_probe_data.values():
|
||||
for prober in prober_list:
|
||||
for probe in prober.probe_results:
|
||||
if probe.visible:
|
||||
successful.push_back(probe)
|
||||
else:
|
||||
failed.push_back(probe)
|
||||
|
||||
var material_index := 0
|
||||
if !successful.is_empty():
|
||||
_add_probe_lines_surface(successful)
|
||||
mesh.surface_set_material(material_index, probe_success_material)
|
||||
material_index += 1
|
||||
if !failed.is_empty():
|
||||
_add_probe_lines_surface(failed)
|
||||
mesh.surface_set_material(material_index, probe_failure_material)
|
||||
material_index += 1
|
||||
|
||||
func _add_probe_lines_surface(probes: Array[ProbeResult]) -> void:
|
||||
var imesh := mesh as ImmediateMesh
|
||||
imesh.surface_begin(Mesh.PRIMITIVE_LINES)
|
||||
for probe in probes:
|
||||
imesh.surface_add_vertex(to_local(probe.start))
|
||||
imesh.surface_add_vertex(to_local(probe.end))
|
||||
imesh.surface_end()
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://8mi2so0m8lyx
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
extends EditorNode3DGizmoPlugin
|
||||
|
||||
var texture : Texture2D = preload("../../icons/GizmoVisionCone.svg")
|
||||
var undo_redo: EditorUndoRedoManager
|
||||
|
||||
var _start_drag_mouse_world_position : Vector3
|
||||
var _start_drag_range: float
|
||||
var _start_drag_angle : float
|
||||
|
||||
func _init() -> void:
|
||||
create_material("cone_preview", Color(1, 1, 0), false)
|
||||
create_handle_material("handles")
|
||||
create_icon_material(
|
||||
"icon",
|
||||
texture,
|
||||
)
|
||||
|
||||
func _get_gizmo_name() -> String:
|
||||
return "VisionCone3D"
|
||||
|
||||
func _get_handle_name(
|
||||
_gizmo: EditorNode3DGizmo,
|
||||
handle_id: int,
|
||||
_secondary: bool
|
||||
) -> String:
|
||||
match handle_id:
|
||||
0: return "Range"
|
||||
1: return "Angle"
|
||||
_: return ""
|
||||
|
||||
func _get_handle_value(
|
||||
gizmo: EditorNode3DGizmo,
|
||||
handle_id: int,
|
||||
_secondary: bool
|
||||
) -> Variant:
|
||||
var vc : VisionCone3D = gizmo.get_node_3d()
|
||||
match handle_id:
|
||||
0: return vc.range
|
||||
1: return vc.angle
|
||||
_: return null
|
||||
|
||||
func _begin_handle_action(
|
||||
gizmo: EditorNode3DGizmo,
|
||||
handle_id: int,
|
||||
_secondary: bool
|
||||
) -> void:
|
||||
var vc : VisionCone3D = gizmo.get_node_3d()
|
||||
_start_drag_mouse_world_position = vc.global_position + (-vc.global_basis.z * vc.range)
|
||||
match handle_id:
|
||||
0: # range
|
||||
_start_drag_range = vc.range
|
||||
1: # angle
|
||||
_start_drag_angle = vc.angle
|
||||
|
||||
func _commit_handle(
|
||||
gizmo: EditorNode3DGizmo,
|
||||
handle_id: int,
|
||||
_secondary: bool,
|
||||
_restore: Variant,
|
||||
# TODO use cancel
|
||||
_cancel: bool
|
||||
) -> void:
|
||||
var vc : VisionCone3D = gizmo.get_node_3d()
|
||||
match handle_id:
|
||||
0: # range
|
||||
undo_redo.create_action("Set range")
|
||||
undo_redo.add_do_property(vc, "range", vc.range)
|
||||
undo_redo.add_undo_property(vc, "range", _start_drag_range)
|
||||
1: # angle
|
||||
undo_redo.create_action("Set angle")
|
||||
undo_redo.add_do_property(vc, "angle", vc.angle)
|
||||
undo_redo.add_undo_property(vc, "angle", _start_drag_angle)
|
||||
undo_redo.commit_action()
|
||||
|
||||
func _set_handle(
|
||||
gizmo: EditorNode3DGizmo,
|
||||
handle_id: int,
|
||||
_secondary: bool,
|
||||
camera: Camera3D,
|
||||
_screen_pos: Vector2,
|
||||
) -> void:
|
||||
var vc : VisionCone3D = gizmo.get_node_3d()
|
||||
match handle_id:
|
||||
0: # range
|
||||
# TODO this mostly works but not if camera.y is near vc.y
|
||||
var world_pos := _calculate_mouse_world_position(
|
||||
camera,
|
||||
vc.global_position.y,
|
||||
Vector3.UP
|
||||
)
|
||||
var local_pos := vc.to_local(world_pos)
|
||||
var new_range := -local_pos.z
|
||||
if new_range > 0:
|
||||
vc.range = new_range
|
||||
|
||||
1: # angle
|
||||
# var local_end_range_pos := Vector3(0, 0, -vc.range)
|
||||
|
||||
# TODO this mostly works but not if camera.y is near vc.y
|
||||
var world_pos := _calculate_mouse_world_position(
|
||||
camera,
|
||||
vc.global_position.y,
|
||||
Vector3.UP
|
||||
)
|
||||
var local_pos := vc.to_local(world_pos)
|
||||
var radius := local_pos.x
|
||||
vc.angle = abs(rad_to_deg(atan(radius / vc.range))) * 2
|
||||
gizmo.get_node_3d().update_gizmos()
|
||||
|
||||
func _has_gizmo(node: Node3D) -> bool:
|
||||
return node is VisionCone3D
|
||||
|
||||
func _redraw(gizmo: EditorNode3DGizmo) -> void:
|
||||
gizmo.clear()
|
||||
var vc : VisionCone3D = gizmo.get_node_3d()
|
||||
|
||||
gizmo.add_unscaled_billboard(get_material("icon", gizmo), 0.04)
|
||||
|
||||
var lines := _make_cone_lines(360, 6, vc.end_radius, vc.range)
|
||||
# var cylinder_mesh := CylinderMesh.new()
|
||||
|
||||
var handles := PackedVector3Array([
|
||||
Vector3(0, 0, -vc.range),
|
||||
Vector3(vc.end_radius, 0, -vc.range)
|
||||
])
|
||||
|
||||
var cone_alpha := 1.0
|
||||
|
||||
if EditorInterface.get_selection().get_selected_nodes().has(vc):
|
||||
gizmo.add_lines(lines, get_material("cone_preview", gizmo), false, Color(1, 1, 1, cone_alpha))
|
||||
gizmo.add_handles(handles, get_material("handles", gizmo), [])
|
||||
|
||||
@warning_ignore("shadowed_global_identifier")
|
||||
@warning_ignore("return_value_discarded")
|
||||
@warning_ignore("integer_division")
|
||||
func _make_cone_lines(
|
||||
resolution: int,
|
||||
support_resolution: int,
|
||||
end_radius: float,
|
||||
range: float
|
||||
) -> PackedVector3Array:
|
||||
var points: PackedVector3Array = []
|
||||
var support_every := resolution / support_resolution
|
||||
var start : Vector3
|
||||
for i in resolution:
|
||||
# circle logic
|
||||
var angle := float(i) * TAU / resolution
|
||||
var x := cos(angle) * end_radius
|
||||
var y := sin(angle) * end_radius
|
||||
var point := Vector3(x, y, -range)
|
||||
points.append(point)
|
||||
|
||||
if i % support_every == 0:
|
||||
points.append(Vector3.ZERO)
|
||||
points.append(point)
|
||||
|
||||
if i == 0:
|
||||
start = point
|
||||
else:
|
||||
points.append(point)
|
||||
points.append(start)
|
||||
points.append(Vector3.ZERO)
|
||||
points.append(Vector3(0, 0, -range))
|
||||
return points
|
||||
|
||||
static func _calculate_mouse_world_position(
|
||||
camera: Camera3D,
|
||||
# world position along plane normal, could use a better name
|
||||
intersection_point: float,
|
||||
plane_normal: Vector3 = Vector3.UP
|
||||
) -> Vector3:
|
||||
var position := camera.get_viewport().get_mouse_position()
|
||||
var camera_from := camera.project_ray_origin(position)
|
||||
var camera_to := camera.project_ray_normal(position)
|
||||
|
||||
var n := plane_normal # plane normal
|
||||
var p := camera_from # ray origin
|
||||
var v := camera_to # ray direction
|
||||
var d := intersection_point # distance of the plane from origin
|
||||
var t := -(n.dot(p) - d) / n.dot(v) # solving for plain/ray intersection
|
||||
|
||||
return p + t * v
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://b3snj0jopsyp1
|
||||
Loading…
Add table
Add a link
Reference in a new issue