229 lines
6.2 KiB
GDScript3
229 lines
6.2 KiB
GDScript3
|
|
class_name LocalStormCloudLayer
|
||
|
|
extends Node3D
|
||
|
|
|
||
|
|
const CLOUD_SHADER: Shader = preload(
|
||
|
|
"res://world/environment/local_storm_cloud.gdshader"
|
||
|
|
)
|
||
|
|
const MIN_CLOUD_ALTITUDE: float = 15.0
|
||
|
|
const MAX_CLOUD_ALTITUDE: float = 23.0
|
||
|
|
const GRID_AXIS_COUNT: int = 9
|
||
|
|
const GRID_SPACING: float = 70.0
|
||
|
|
const FIELD_HALF_EXTENT: float = GRID_AXIS_COUNT * GRID_SPACING * 0.5
|
||
|
|
const DISTANCE_FADE_START: float = 240.0
|
||
|
|
const DISTANCE_FADE_END: float = 300.0
|
||
|
|
const WRAP_RADIUS: float = FIELD_HALF_EXTENT
|
||
|
|
const MAXIMUM_OPACITY: float = 1.0
|
||
|
|
const BASE_VELOCITY := Vector2(0.825, -0.45)
|
||
|
|
const BODY_OUTLINE: Array[Vector2] = [
|
||
|
|
Vector2(-6.9, -0.8),
|
||
|
|
Vector2(-6.6, -2.0),
|
||
|
|
Vector2(-5.7, -3.0),
|
||
|
|
Vector2(-4.5, -3.5),
|
||
|
|
Vector2(-3.0, -3.7),
|
||
|
|
Vector2(-1.8, -4.2),
|
||
|
|
Vector2(-0.5, -4.3),
|
||
|
|
Vector2(0.7, -4.0),
|
||
|
|
Vector2(2.0, -4.1),
|
||
|
|
Vector2(3.0, -3.5),
|
||
|
|
Vector2(4.6, -3.3),
|
||
|
|
Vector2(5.8, -2.5),
|
||
|
|
Vector2(6.6, -1.5),
|
||
|
|
Vector2(7.0, -0.2),
|
||
|
|
Vector2(6.8, 1.0),
|
||
|
|
Vector2(6.1, 1.9),
|
||
|
|
Vector2(6.0, 2.9),
|
||
|
|
Vector2(5.1, 3.7),
|
||
|
|
Vector2(3.8, 4.0),
|
||
|
|
Vector2(2.7, 4.7),
|
||
|
|
Vector2(1.4, 4.9),
|
||
|
|
Vector2(0.2, 5.3),
|
||
|
|
Vector2(-1.2, 5.0),
|
||
|
|
Vector2(-2.1, 4.4),
|
||
|
|
Vector2(-3.6, 4.2),
|
||
|
|
Vector2(-4.9, 3.5),
|
||
|
|
Vector2(-5.4, 2.5),
|
||
|
|
Vector2(-6.3, 1.7),
|
||
|
|
Vector2(-6.8, 0.6),
|
||
|
|
]
|
||
|
|
|
||
|
|
var _target: Node3D
|
||
|
|
var _material: ShaderMaterial
|
||
|
|
var _multimesh: MultiMesh
|
||
|
|
var _cloud_mesh: MultiMeshInstance3D
|
||
|
|
var _drift_offset := Vector2.ZERO
|
||
|
|
var _storm_amount: float = 0.0
|
||
|
|
|
||
|
|
|
||
|
|
func setup(target: Node3D) -> void:
|
||
|
|
_target = target
|
||
|
|
_build_cloud_field()
|
||
|
|
_update_cloud_transforms()
|
||
|
|
set_process(true)
|
||
|
|
|
||
|
|
|
||
|
|
func set_storm_amount(amount: float, color: Color) -> void:
|
||
|
|
_storm_amount = clampf(amount, 0.0, 1.0)
|
||
|
|
var should_be_visible: bool = _storm_amount > 0.001
|
||
|
|
if should_be_visible and not visible:
|
||
|
|
_update_cloud_transforms()
|
||
|
|
visible = should_be_visible
|
||
|
|
if _material == null:
|
||
|
|
return
|
||
|
|
_material.set_shader_parameter("weather_opacity", _storm_amount)
|
||
|
|
_material.set_shader_parameter("cloud_color", color)
|
||
|
|
|
||
|
|
|
||
|
|
func get_storm_amount() -> float:
|
||
|
|
return _storm_amount
|
||
|
|
|
||
|
|
|
||
|
|
func get_patch_count() -> int:
|
||
|
|
return GRID_AXIS_COUNT * GRID_AXIS_COUNT
|
||
|
|
|
||
|
|
|
||
|
|
func _process(delta: float) -> void:
|
||
|
|
if not visible or _target == null or not is_instance_valid(_target):
|
||
|
|
return
|
||
|
|
_drift_offset += BASE_VELOCITY * delta
|
||
|
|
_drift_offset = Vector2(
|
||
|
|
_wrap_axis(_drift_offset.x),
|
||
|
|
_wrap_axis(_drift_offset.y),
|
||
|
|
)
|
||
|
|
_update_cloud_transforms()
|
||
|
|
|
||
|
|
|
||
|
|
func _build_cloud_field() -> void:
|
||
|
|
_material = ShaderMaterial.new()
|
||
|
|
_material.shader = CLOUD_SHADER
|
||
|
|
_material.set_shader_parameter("weather_opacity", 0.0)
|
||
|
|
_material.set_shader_parameter("maximum_opacity", MAXIMUM_OPACITY)
|
||
|
|
_material.set_shader_parameter("distance_fade_start", DISTANCE_FADE_START)
|
||
|
|
_material.set_shader_parameter("distance_fade_end", DISTANCE_FADE_END)
|
||
|
|
|
||
|
|
var cloud_body_mesh: ArrayMesh = _build_cloud_body_mesh()
|
||
|
|
cloud_body_mesh.surface_set_material(0, _material)
|
||
|
|
|
||
|
|
_multimesh = MultiMesh.new()
|
||
|
|
_multimesh.transform_format = MultiMesh.TRANSFORM_3D
|
||
|
|
_multimesh.mesh = cloud_body_mesh
|
||
|
|
_multimesh.instance_count = get_patch_count()
|
||
|
|
_multimesh.custom_aabb = AABB(
|
||
|
|
Vector3(
|
||
|
|
-FIELD_HALF_EXTENT - 60.0,
|
||
|
|
MIN_CLOUD_ALTITUDE - 5.0,
|
||
|
|
-FIELD_HALF_EXTENT - 60.0,
|
||
|
|
),
|
||
|
|
Vector3(
|
||
|
|
(FIELD_HALF_EXTENT + 60.0) * 2.0,
|
||
|
|
MAX_CLOUD_ALTITUDE - MIN_CLOUD_ALTITUDE + 10.0,
|
||
|
|
(FIELD_HALF_EXTENT + 60.0) * 2.0,
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
_cloud_mesh = MultiMeshInstance3D.new()
|
||
|
|
_cloud_mesh.name = "CloudField"
|
||
|
|
_cloud_mesh.multimesh = _multimesh
|
||
|
|
_cloud_mesh.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||
|
|
add_child(_cloud_mesh)
|
||
|
|
visible = false
|
||
|
|
|
||
|
|
|
||
|
|
func _build_cloud_body_mesh() -> ArrayMesh:
|
||
|
|
var outline := PackedVector2Array(BODY_OUTLINE)
|
||
|
|
var surface_tool := SurfaceTool.new()
|
||
|
|
surface_tool.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||
|
|
var face_indices: PackedInt32Array = Geometry2D.triangulate_polygon(
|
||
|
|
outline
|
||
|
|
)
|
||
|
|
for triangle_offset: int in range(0, face_indices.size(), 3):
|
||
|
|
var point_a: Vector2 = outline[face_indices[triangle_offset]]
|
||
|
|
var point_b: Vector2 = outline[face_indices[triangle_offset + 1]]
|
||
|
|
var point_c: Vector2 = outline[face_indices[triangle_offset + 2]]
|
||
|
|
_add_oriented_triangle(
|
||
|
|
surface_tool,
|
||
|
|
_to_cloud_vertex(point_a),
|
||
|
|
_to_cloud_vertex(point_b),
|
||
|
|
_to_cloud_vertex(point_c),
|
||
|
|
Vector3.DOWN,
|
||
|
|
)
|
||
|
|
surface_tool.index()
|
||
|
|
return surface_tool.commit()
|
||
|
|
|
||
|
|
|
||
|
|
func _to_cloud_vertex(point: Vector2) -> Vector3:
|
||
|
|
return Vector3(point.x, 0.0, point.y)
|
||
|
|
|
||
|
|
|
||
|
|
func _add_oriented_triangle(
|
||
|
|
surface_tool: SurfaceTool,
|
||
|
|
point_a: Vector3,
|
||
|
|
point_b: Vector3,
|
||
|
|
point_c: Vector3,
|
||
|
|
expected_normal: Vector3,
|
||
|
|
) -> void:
|
||
|
|
if (point_b - point_a).cross(point_c - point_a).dot(expected_normal) < 0.0:
|
||
|
|
var swap: Vector3 = point_b
|
||
|
|
point_b = point_c
|
||
|
|
point_c = swap
|
||
|
|
for point: Vector3 in [point_a, point_b, point_c]:
|
||
|
|
surface_tool.set_normal(expected_normal)
|
||
|
|
surface_tool.add_vertex(point)
|
||
|
|
|
||
|
|
|
||
|
|
func _update_cloud_transforms() -> void:
|
||
|
|
if _target == null or _multimesh == null:
|
||
|
|
return
|
||
|
|
global_position = _target.global_position
|
||
|
|
var target_xz := Vector2(
|
||
|
|
_target.global_position.x,
|
||
|
|
_target.global_position.z,
|
||
|
|
)
|
||
|
|
var grid_center: float = float(GRID_AXIS_COUNT - 1) * 0.5
|
||
|
|
var instance_index: int = 0
|
||
|
|
for grid_z: int in GRID_AXIS_COUNT:
|
||
|
|
for grid_x: int in GRID_AXIS_COUNT:
|
||
|
|
var patch_index: int = grid_z * GRID_AXIS_COUNT + grid_x
|
||
|
|
var world_offset := Vector2(
|
||
|
|
(float(grid_x) - grid_center) * GRID_SPACING,
|
||
|
|
(float(grid_z) - grid_center) * GRID_SPACING,
|
||
|
|
) + _drift_offset
|
||
|
|
var relative := Vector2(
|
||
|
|
_wrap_axis(world_offset.x - target_xz.x),
|
||
|
|
_wrap_axis(world_offset.y - target_xz.y),
|
||
|
|
)
|
||
|
|
var altitude: float = lerpf(
|
||
|
|
MIN_CLOUD_ALTITUDE,
|
||
|
|
MAX_CLOUD_ALTITUDE,
|
||
|
|
float(patch_index % 4) / 3.0,
|
||
|
|
)
|
||
|
|
var patch_scale: float = 2.20 + float(
|
||
|
|
(patch_index * 7) % 10
|
||
|
|
) * 0.10
|
||
|
|
var width_variation: float = 0.90 + float(
|
||
|
|
(patch_index * 5) % 7
|
||
|
|
) * 0.04
|
||
|
|
var depth_variation: float = 0.90 + float(
|
||
|
|
(patch_index * 3 + 2) % 7
|
||
|
|
) * 0.04
|
||
|
|
var patch_basis := Basis(
|
||
|
|
Vector3.UP,
|
||
|
|
deg_to_rad(float(patch_index * 17 % 41) - 20.0),
|
||
|
|
).scaled(Vector3(
|
||
|
|
patch_scale * 2.35 * width_variation,
|
||
|
|
patch_scale * 0.34,
|
||
|
|
patch_scale * 2.65 * depth_variation,
|
||
|
|
))
|
||
|
|
_multimesh.set_instance_transform(
|
||
|
|
instance_index,
|
||
|
|
Transform3D(
|
||
|
|
patch_basis,
|
||
|
|
Vector3(relative.x, altitude, relative.y),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
instance_index += 1
|
||
|
|
|
||
|
|
|
||
|
|
func _wrap_axis(value: float) -> float:
|
||
|
|
return fposmod(value + FIELD_HALF_EXTENT, FIELD_HALF_EXTENT * 2.0) \
|
||
|
|
- FIELD_HALF_EXTENT
|