Add tests for scenery surface caching and preview behavior
- Introduced `test_scenery_brick_preview.gd` to validate focused placement and CPU draw-cost for large brick walls. - Added `test_scenery_surface_cache.gd` to assess texture budget, visual equivalence, and cache invalidation. - Created `test_scenery_surface_preview.gd` to evaluate adaptive preview selection and placement behavior. - Implemented various helper functions to measure drawing performance and validate expected behaviors during tests. - Added unique identifiers for new test files to ensure proper tracking and management.
This commit is contained in:
@@ -2,14 +2,20 @@ class_name StageBackgroundGround
|
||||
extends "res://scripts/stage/scenery/stage_rect_surface.gd"
|
||||
## Non-walkable rectangular scenery surface behind the set.
|
||||
|
||||
const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||
const DRAW_ORDER := preload("res://scripts/stage/stage_draw_order.gd")
|
||||
|
||||
## Keeps procedural ground finishes aligned when this surface is split by the area eraser.
|
||||
var motif_origin_offset: Vector2 = Vector2.ZERO
|
||||
|
||||
|
||||
func configure(new_size: Vector2, finish: StringName, color: Color) -> void:
|
||||
super.configure(new_size, finish if SURFACE_RENDERER.is_background_finish(finish) else &"finish_background_grass", color)
|
||||
z_index = DRAW_ORDER.BACKGROUND_GROUND
|
||||
|
||||
|
||||
func get_surface_draw_origin_world() -> Vector2:
|
||||
return global_position + motif_origin_offset
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
SURFACE_RENDERER.draw_background(self, get_local_bounds(), finish_id, get_render_tint(), get_surface_draw_origin_world())
|
||||
_draw_surface(false)
|
||||
|
||||
@@ -2,17 +2,47 @@ class_name StageRectSurface
|
||||
extends Node2D
|
||||
## Shared rectangular geometry and transient tint preview for inert Stage scenery.
|
||||
|
||||
const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||
const SURFACE_CACHE := preload("res://scripts/stage/scenery/stage_surface_cache.gd")
|
||||
const PREVIEW_DRAW_BUDGET_USEC: int = 6000
|
||||
## Each rectangle needs one handle; the image itself is shared across rectangles.
|
||||
const MAX_RESIDENT_TILES: int = 1
|
||||
|
||||
var panel_size: Vector2 = Vector2(320.0, 400.0)
|
||||
var finish_id: StringName = &""
|
||||
var tint: Color = Color.WHITE
|
||||
var _preview_tint: Color = Color.WHITE
|
||||
var _has_preview_tint: bool = false
|
||||
var _preview_over_budget: bool = false
|
||||
var _surface_tiles: Dictionary = {}
|
||||
var _cache_style: Array = []
|
||||
var _cache_dirty: bool = true
|
||||
var _last_view_transform: Transform2D
|
||||
var _last_view_rect: Rect2
|
||||
var _was_covered: bool = false
|
||||
var _cover_regions_frame: int = -1
|
||||
var _cover_regions: Array[Dictionary] = []
|
||||
var _last_cover_regions: Array[Dictionary] = []
|
||||
## Placement uses adaptive vector detail; committed surfaces share material textures.
|
||||
var interactive_preview: bool = false:
|
||||
set(value):
|
||||
if interactive_preview == value:
|
||||
return
|
||||
interactive_preview = value
|
||||
_preview_over_budget = false
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func configure(new_size: Vector2, finish: StringName, color: Color) -> void:
|
||||
panel_size = Vector2(maxf(1.0, new_size.x), maxf(1.0, new_size.y))
|
||||
var next_size := Vector2(maxf(1.0, new_size.x), maxf(1.0, new_size.y))
|
||||
if panel_size == next_size and finish_id == finish and tint == color:
|
||||
return
|
||||
if finish_id != finish:
|
||||
_preview_over_budget = false
|
||||
panel_size = next_size
|
||||
finish_id = finish
|
||||
tint = color
|
||||
_cache_dirty = true
|
||||
queue_redraw()
|
||||
|
||||
|
||||
@@ -37,3 +67,154 @@ func get_render_tint() -> Color:
|
||||
|
||||
func get_surface_draw_origin_world() -> Vector2:
|
||||
return global_position
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
# Erase/Cancel retains detached geometry, never native drawing resources.
|
||||
for tile: SURFACE_CACHE in _surface_tiles.values():
|
||||
tile.dispose()
|
||||
_surface_tiles.clear()
|
||||
_cache_dirty = true
|
||||
_cover_regions_frame = -1
|
||||
_last_cover_regions.clear()
|
||||
RenderingServer.canvas_item_clear(get_canvas_item())
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not is_visible_in_tree():
|
||||
for tile: SURFACE_CACHE in _surface_tiles.values():
|
||||
tile.wanted = false
|
||||
SURFACE_CACHE.build_next()
|
||||
return
|
||||
var covered: bool = not interactive_preview and _covered_by_surface()
|
||||
var coverage_changed: bool = _get_cover_regions() != _last_cover_regions
|
||||
if coverage_changed:
|
||||
_last_cover_regions = _cover_regions.duplicate()
|
||||
if covered:
|
||||
for tile: SURFACE_CACHE in _surface_tiles.values():
|
||||
tile.wanted = false
|
||||
SURFACE_CACHE.build_next()
|
||||
# Camera pans, zooms, and window resizing can reveal previously culled motifs.
|
||||
if coverage_changed or covered != _was_covered or get_global_transform_with_canvas() != _last_view_transform or get_viewport_rect() != _last_view_rect:
|
||||
queue_redraw()
|
||||
_was_covered = covered
|
||||
|
||||
|
||||
func _draw_surface(wall: bool) -> void:
|
||||
_last_view_transform = get_global_transform_with_canvas()
|
||||
_last_view_rect = get_viewport_rect()
|
||||
if not interactive_preview and DisplayServer.get_name() != "headless" and finish_id not in [&"finish_wall_paint", &"finish_ground_paint"]:
|
||||
if _covered_by_surface():
|
||||
for tile: SURFACE_CACHE in _surface_tiles.values():
|
||||
tile.wanted = false
|
||||
return
|
||||
_draw_cached_surface(wall)
|
||||
return
|
||||
var started_usec: int = Time.get_ticks_usec() if interactive_preview else 0
|
||||
if wall:
|
||||
SURFACE_RENDERER.draw_wall(self, get_local_bounds(), finish_id, get_render_tint(), get_surface_draw_origin_world(), Vector2.ONE, true, interactive_preview, _preview_over_budget)
|
||||
else:
|
||||
SURFACE_RENDERER.draw_background(self, get_local_bounds(), finish_id, get_render_tint(), get_surface_draw_origin_world(), Vector2.ONE, true, interactive_preview, _preview_over_budget)
|
||||
# Latch for this placement session to avoid oscillating between detail levels.
|
||||
# The complexity estimate handles large first draws before timing is available.
|
||||
if interactive_preview and Time.get_ticks_usec() - started_usec > PREVIEW_DRAW_BUDGET_USEC:
|
||||
_preview_over_budget = true
|
||||
|
||||
|
||||
func _draw_cached_surface(wall: bool) -> void:
|
||||
var visible_rect: Rect2 = get_local_bounds().intersection(SURFACE_RENDERER._visible_local_rect(self))
|
||||
var color: Color = get_render_tint()
|
||||
var style: Array = [wall, finish_id, color]
|
||||
if _cache_style != style:
|
||||
for previous: SURFACE_CACHE in _surface_tiles.values():
|
||||
previous.dispose()
|
||||
_surface_tiles.clear()
|
||||
_cache_style = style
|
||||
_cache_dirty = false
|
||||
for previous: SURFACE_CACHE in _surface_tiles.values():
|
||||
previous.wanted = false
|
||||
if visible_rect.size.x <= 0.0 or visible_rect.size.y <= 0.0:
|
||||
return
|
||||
var period: Vector2 = SURFACE_RENDERER.pattern_size_for(finish_id)
|
||||
if period.x <= 0.0 or period.y <= 0.0:
|
||||
draw_rect(visible_rect, SURFACE_RENDERER.surface_base_color(finish_id, color, wall))
|
||||
return
|
||||
var pattern: SURFACE_CACHE = _surface_tiles.get(&"pattern") as SURFACE_CACHE
|
||||
if pattern == null:
|
||||
pattern = SURFACE_CACHE.new() as SURFACE_CACHE
|
||||
pattern.attach_to(self)
|
||||
pattern.request_pattern(wall, finish_id, color, period)
|
||||
pattern.updated.connect(queue_redraw)
|
||||
_surface_tiles[&"pattern"] = pattern
|
||||
pattern.wanted = true
|
||||
pattern.last_used_frame = Engine.get_process_frames()
|
||||
pattern.ensure_queued()
|
||||
var texture: Texture2D = pattern.get_texture()
|
||||
if texture == null:
|
||||
draw_rect(visible_rect, SURFACE_RENDERER.surface_base_color(finish_id, color, wall))
|
||||
return
|
||||
texture_repeat = CanvasItem.TEXTURE_REPEAT_ENABLED
|
||||
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
|
||||
# One quad samples a repeating material. Camera motion, panel resizing and
|
||||
# erase fragments change only these UVs, never the image or its mip levels.
|
||||
var sample_origin: Vector2 = visible_rect.position + get_surface_draw_origin_world()
|
||||
sample_origin = Vector2(fposmod(sample_origin.x, period.x), fposmod(sample_origin.y, period.y))
|
||||
var pixels_per_unit: Vector2 = Vector2(pattern.size) / period
|
||||
var source := Rect2(sample_origin * pixels_per_unit, visible_rect.size * pixels_per_unit)
|
||||
draw_texture_rect_region(texture, visible_rect, source, Color.WHITE, false, false)
|
||||
|
||||
|
||||
func _covered_by_surface(local_visible: Rect2 = Rect2()) -> bool:
|
||||
var visible: Rect2 = local_visible
|
||||
if visible.size.x <= 0.0 or visible.size.y <= 0.0:
|
||||
visible = get_local_bounds().intersection(SURFACE_RENDERER._visible_local_rect(self))
|
||||
if visible.size.x <= 0.0 or visible.size.y <= 0.0:
|
||||
return true
|
||||
var corners := PackedVector2Array([visible.position, Vector2(visible.end.x, visible.position.y), visible.end, Vector2(visible.position.x, visible.end.y)])
|
||||
for region: Dictionary in _get_cover_regions():
|
||||
var fully_covered: bool = true
|
||||
var cover_bounds: Rect2 = region["bounds"]
|
||||
var to_cover: Transform2D = region["transform"]
|
||||
for corner: Vector2 in corners:
|
||||
if not cover_bounds.has_point(to_cover * corner):
|
||||
fully_covered = false
|
||||
break
|
||||
if fully_covered:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _get_cover_regions() -> Array[Dictionary]:
|
||||
var frame: int = Engine.get_process_frames()
|
||||
if _cover_regions_frame == frame:
|
||||
return _cover_regions
|
||||
_cover_regions_frame = frame
|
||||
_cover_regions.clear()
|
||||
var parent: Node = get_parent()
|
||||
if parent == null or (parent is Node2D and (parent as Node2D).y_sort_enabled):
|
||||
return _cover_regions
|
||||
# Transparent ancestors and custom materials can reveal underlying layers.
|
||||
var ancestor: Node = self
|
||||
while ancestor is CanvasItem:
|
||||
if (ancestor as CanvasItem).modulate.a < 0.999 or (ancestor as CanvasItem).material != null:
|
||||
return _cover_regions
|
||||
ancestor = ancestor.get_parent()
|
||||
for sibling: Node in parent.get_children():
|
||||
if sibling == self or not (sibling is StageRectSurface):
|
||||
continue
|
||||
var cover: StageRectSurface = sibling as StageRectSurface
|
||||
if cover.z_as_relative != z_as_relative:
|
||||
continue
|
||||
if not cover.is_visible_in_tree() or cover.interactive_preview or cover.material != null or cover.modulate.a < 0.999 or cover.self_modulate.a < 0.999:
|
||||
continue
|
||||
if cover.z_index < z_index or (cover.z_index == z_index and cover.get_index() < get_index()):
|
||||
continue
|
||||
if SURFACE_RENDERER.surface_base_color(cover.finish_id, cover.get_render_tint(), SURFACE_RENDERER.is_wall_finish(cover.finish_id)).a < 0.999:
|
||||
continue
|
||||
if absf(cover.global_transform.determinant()) < 0.000001:
|
||||
continue
|
||||
_cover_regions.append({
|
||||
"bounds": cover.get_local_bounds().grow(-0.01),
|
||||
"transform": cover.global_transform.affine_inverse() * global_transform,
|
||||
})
|
||||
return _cover_regions
|
||||
|
||||
@@ -0,0 +1,929 @@
|
||||
class_name StageSurfaceCache
|
||||
extends RefCounted
|
||||
## Lightweight per-surface handle into a scene-scoped immutable bitmap cache.
|
||||
|
||||
const MAX_TEXTURE_EDGE: int = 4096
|
||||
const MAX_TEXTURE_PIXELS: float = 4194304.0
|
||||
const MAX_CACHE_BYTES: int = 64 * 1024 * 1024
|
||||
const MAX_CACHE_ENTRIES: int = 512
|
||||
const MAX_QUEUED_BUILDS: int = 512
|
||||
const MAX_QUEUE_SCAN_PER_FRAME: int = 8
|
||||
const RECENT_USE_PROTECTION_FRAMES: int = 3
|
||||
const MIN_DETAILED_SCALE: float = 0.5
|
||||
const PATTERN_CACHE_VERSION: int = 1
|
||||
const PATTERN_SOURCE_OVERSCAN: float = 4.0
|
||||
const PATTERN_PIXEL_SCALE: float = 2.0
|
||||
|
||||
static var _pool_refs: Dictionary = {}
|
||||
static var _last_service_frame: int = -1
|
||||
static var _active_pool_ref: WeakRef
|
||||
|
||||
signal updated
|
||||
|
||||
|
||||
class TileEntry extends RefCounted:
|
||||
var key: String
|
||||
var canonical_rect: Rect2
|
||||
var source_scale: Vector2
|
||||
var wall: bool
|
||||
var finish: StringName
|
||||
var tint: Color
|
||||
var detail_view_size: Vector2
|
||||
var size: Vector2i
|
||||
var byte_count: int = 0
|
||||
var texture: Texture2D
|
||||
var is_pattern: bool = false
|
||||
var period: Vector2 = Vector2.ZERO
|
||||
var watchers: Array[WeakRef] = []
|
||||
var canvas_owners: Array[WeakRef] = []
|
||||
var last_used_frame: int = 0
|
||||
var build_usec: int = 0
|
||||
var queued: bool = false
|
||||
var building: bool = false
|
||||
|
||||
|
||||
class SurfaceDrawing extends Node2D:
|
||||
const RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||
var source_rect: Rect2
|
||||
var wall: bool
|
||||
var finish: StringName
|
||||
var tint: Color
|
||||
var origin: Vector2 = Vector2.ZERO
|
||||
var detail_view_size: Vector2
|
||||
var low_detail: bool = false
|
||||
var active: bool = false
|
||||
var pool_ref: WeakRef
|
||||
|
||||
func _draw() -> void:
|
||||
var pool: Variant = pool_ref.get_ref() if pool_ref != null else null
|
||||
if not active or pool == null:
|
||||
return
|
||||
active = false
|
||||
var started_usec: int = Time.get_ticks_usec()
|
||||
if wall:
|
||||
RENDERER.draw_wall(self, source_rect, finish, tint, origin, Vector2.ONE, false, low_detail, low_detail, detail_view_size)
|
||||
else:
|
||||
RENDERER.draw_background(self, source_rect, finish, tint, origin, Vector2.ONE, false, low_detail, low_detail)
|
||||
pool._finish_vector_submission(Time.get_ticks_usec() - started_usec)
|
||||
|
||||
|
||||
class BakePool extends Node:
|
||||
const RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||
const BAKE_GUTTER_PIXELS: int = 4
|
||||
var scope_id: int = 0
|
||||
var entries: Dictionary = {}
|
||||
var queue: Array[String] = []
|
||||
var _waiting_handles: Array[WeakRef] = []
|
||||
var resident_bytes: int = 0
|
||||
var builds: int = 0
|
||||
var cache_hits: int = 0
|
||||
var evictions: int = 0
|
||||
var refusals: int = 0
|
||||
var readbacks: int = 0
|
||||
var pattern_builds: int = 0
|
||||
var pattern_readbacks: int = 0
|
||||
var pattern_resident_bytes: int = 0
|
||||
var pattern_retired_bytes: int = 0
|
||||
var last_build_usec: int = 0
|
||||
var vector_submission_usec: int = 0
|
||||
var retired_bytes: int = 0
|
||||
var budget_blocked_requests: int = 0
|
||||
var budget_pressure: bool = false
|
||||
var budget_pressure_frame: int = -1
|
||||
var _viewport: SubViewport
|
||||
var _drawing: SurfaceDrawing
|
||||
var _building_entry: TileEntry
|
||||
var _building_started_usec: int = 0
|
||||
var _reserved_bytes: int = 0
|
||||
var _retired_textures: Array[Texture2D] = []
|
||||
var _retire_callback_pending: bool = false
|
||||
var _capture_pending: bool = false
|
||||
var _disposed: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
set_process(true)
|
||||
_ensure_baker()
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
_prune_queue()
|
||||
StageSurfaceCache._service_pools()
|
||||
|
||||
func _ensure_baker() -> void:
|
||||
if is_instance_valid(_viewport):
|
||||
return
|
||||
_viewport = SubViewport.new()
|
||||
_viewport.name = "SurfaceBitmapBaker"
|
||||
_viewport.disable_3d = true
|
||||
_viewport.transparent_bg = true
|
||||
_viewport.render_target_clear_mode = SubViewport.CLEAR_MODE_ALWAYS
|
||||
_viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED
|
||||
_viewport.world_2d = World2D.new()
|
||||
add_child(_viewport)
|
||||
_drawing = SurfaceDrawing.new()
|
||||
_drawing.name = "SurfaceVectorSource"
|
||||
_drawing.pool_ref = weakref(self)
|
||||
_viewport.add_child(_drawing)
|
||||
|
||||
func queue_handle(handle: StageSurfaceCache) -> void:
|
||||
if _disposed or handle._disposed:
|
||||
return
|
||||
var entry: TileEntry = _find_or_create_entry(handle)
|
||||
if entry == null:
|
||||
refusals += 1
|
||||
_add_waiter(handle)
|
||||
return
|
||||
_bind_handle(handle, entry)
|
||||
if not _handle_is_interested(handle):
|
||||
return
|
||||
handle.last_used_frame = Engine.get_process_frames()
|
||||
entry.last_used_frame = Engine.get_process_frames()
|
||||
if entry.texture != null or entry.queued or entry.building:
|
||||
return
|
||||
if queue.size() >= MAX_QUEUED_BUILDS:
|
||||
refusals += 1
|
||||
_add_waiter(handle)
|
||||
return
|
||||
entry.queued = true
|
||||
queue.append(entry.key)
|
||||
handle._enqueued = true
|
||||
|
||||
func _find_or_create_entry(handle: StageSurfaceCache) -> TileEntry:
|
||||
var existing: TileEntry = entries.get(handle._cache_key) as TileEntry
|
||||
if existing != null:
|
||||
cache_hits += 1
|
||||
return existing
|
||||
while entries.size() + _retired_textures.size() >= MAX_CACHE_ENTRIES:
|
||||
if not _retired_textures.is_empty():
|
||||
return null
|
||||
if not _evict_one(null):
|
||||
return null
|
||||
if not _retired_textures.is_empty():
|
||||
return null
|
||||
var arguments: Array = handle._pending_arguments
|
||||
if arguments.size() < 6:
|
||||
return null
|
||||
var entry := TileEntry.new()
|
||||
entry.key = handle._cache_key
|
||||
entry.canonical_rect = arguments[0] as Rect2
|
||||
entry.source_scale = arguments[1] as Vector2
|
||||
entry.wall = bool(arguments[2])
|
||||
entry.finish = arguments[3] as StringName
|
||||
entry.tint = arguments[4] as Color
|
||||
entry.detail_view_size = arguments[5] as Vector2
|
||||
if arguments.size() >= 8:
|
||||
entry.is_pattern = bool(arguments[6])
|
||||
entry.period = arguments[7] as Vector2
|
||||
entries[entry.key] = entry
|
||||
return entry
|
||||
|
||||
func _bind_handle(handle: StageSurfaceCache, entry: TileEntry) -> void:
|
||||
_register_canvas_owner(entry, handle)
|
||||
var current: TileEntry = handle._entry_ref.get_ref() as TileEntry if handle._entry_ref != null else null
|
||||
if current == entry:
|
||||
handle.ready_for_display = entry.texture != null
|
||||
handle._building = entry.building
|
||||
handle._enqueued = entry.queued
|
||||
handle.build_usec = entry.build_usec
|
||||
return
|
||||
if current != null:
|
||||
_unwatch_handle(current, handle)
|
||||
handle._entry_ref = weakref(entry)
|
||||
handle.ready_for_display = entry.texture != null
|
||||
handle._building = entry.building
|
||||
handle._enqueued = entry.queued
|
||||
handle.build_usec = entry.build_usec
|
||||
var found: bool = false
|
||||
for watcher_ref: WeakRef in entry.watchers:
|
||||
if watcher_ref.get_ref() == handle:
|
||||
found = true
|
||||
break
|
||||
if not found:
|
||||
entry.watchers.append(weakref(handle))
|
||||
|
||||
func _unwatch_handle(entry: TileEntry, handle: StageSurfaceCache) -> void:
|
||||
for index: int in range(entry.watchers.size() - 1, -1, -1):
|
||||
var watcher: Variant = entry.watchers[index].get_ref()
|
||||
if watcher == null or watcher == handle:
|
||||
entry.watchers.remove_at(index)
|
||||
|
||||
func _register_canvas_owner(entry: TileEntry, handle: StageSurfaceCache) -> void:
|
||||
var owner: Variant = handle._owner_ref.get_ref() if handle._owner_ref != null else null
|
||||
if owner == null or not is_instance_valid(owner):
|
||||
return
|
||||
for index: int in range(entry.canvas_owners.size() - 1, -1, -1):
|
||||
var registered_owner: Variant = entry.canvas_owners[index].get_ref()
|
||||
if registered_owner == null or not is_instance_valid(registered_owner):
|
||||
entry.canvas_owners.remove_at(index)
|
||||
elif registered_owner == owner:
|
||||
return
|
||||
entry.canvas_owners.append(weakref(owner))
|
||||
|
||||
func _start_next_build() -> bool:
|
||||
if _disposed or _capture_pending:
|
||||
return false
|
||||
_prune_queue()
|
||||
var attempts: int = mini(queue.size(), MAX_QUEUE_SCAN_PER_FRAME)
|
||||
while attempts > 0 and not queue.is_empty():
|
||||
attempts -= 1
|
||||
var key: String = _pop_next_build_key()
|
||||
var entry: TileEntry = entries.get(key) as TileEntry
|
||||
if entry == null or not entry.queued or entry.texture != null:
|
||||
continue
|
||||
entry.queued = false
|
||||
if not _entry_has_live_watchers(entry):
|
||||
_evict_entry(entry)
|
||||
continue
|
||||
var size: Vector2i = _baker_size_for(entry)
|
||||
var required_bytes: int = _estimated_texture_bytes(entry)
|
||||
if not _make_room(required_bytes, entry):
|
||||
refusals += 1
|
||||
if queue.size() < MAX_QUEUED_BUILDS:
|
||||
entry.queued = true
|
||||
queue.append(entry.key)
|
||||
budget_blocked_requests += 1
|
||||
budget_pressure = true
|
||||
budget_pressure_frame = Engine.get_process_frames()
|
||||
continue
|
||||
_begin_build(entry, size)
|
||||
return true
|
||||
return false
|
||||
|
||||
func _baker_rect_for(entry: TileEntry) -> Rect2:
|
||||
return entry.canonical_rect.grow(PATTERN_SOURCE_OVERSCAN) if entry.is_pattern else entry.canonical_rect
|
||||
|
||||
func _baker_size_for(entry: TileEntry) -> Vector2i:
|
||||
var draw_rect: Rect2 = _baker_rect_for(entry)
|
||||
return StageSurfaceCache.texture_size_for(draw_rect.size, entry.source_scale)
|
||||
|
||||
func _pattern_output_size(entry: TileEntry) -> Vector2i:
|
||||
var draw_rect: Rect2 = _baker_rect_for(entry)
|
||||
var baker_size: Vector2i = StageSurfaceCache.texture_size_for(draw_rect.size, entry.source_scale)
|
||||
return Vector2i(
|
||||
maxi(1, roundi(float(baker_size.x) * entry.period.x / draw_rect.size.x)),
|
||||
maxi(1, roundi(float(baker_size.y) * entry.period.y / draw_rect.size.y))
|
||||
)
|
||||
|
||||
func _estimated_texture_bytes(entry: TileEntry) -> int:
|
||||
var base_size: Vector2i = _pattern_output_size(entry) if entry.is_pattern else _baker_size_for(entry)
|
||||
return _mip_chain_byte_count(base_size) if entry.is_pattern else base_size.x * base_size.y * 4
|
||||
|
||||
func _crop_pattern_image(image: Image, entry: TileEntry) -> Image:
|
||||
var draw_rect: Rect2 = _baker_rect_for(entry)
|
||||
var output_size: Vector2i = _pattern_output_size(entry)
|
||||
var scale: Vector2 = Vector2(image.get_size()) / draw_rect.size
|
||||
var crop_position := Vector2i(
|
||||
roundi((entry.canonical_rect.position.x - draw_rect.position.x) * scale.x),
|
||||
roundi((entry.canonical_rect.position.y - draw_rect.position.y) * scale.y)
|
||||
)
|
||||
crop_position.x = clampi(crop_position.x, 0, maxi(0, image.get_width() - output_size.x))
|
||||
crop_position.y = clampi(crop_position.y, 0, maxi(0, image.get_height() - output_size.y))
|
||||
var safe_size := Vector2i(
|
||||
mini(output_size.x, image.get_width() - crop_position.x),
|
||||
mini(output_size.y, image.get_height() - crop_position.y)
|
||||
)
|
||||
if safe_size.x <= 0 or safe_size.y <= 0:
|
||||
return Image.new()
|
||||
return image.get_region(Rect2i(crop_position, safe_size))
|
||||
|
||||
func _mip_chain_byte_count(size: Vector2i) -> int:
|
||||
var width: int = maxi(1, size.x)
|
||||
var height: int = maxi(1, size.y)
|
||||
var total: int = 0
|
||||
while true:
|
||||
total += width * height * 4
|
||||
if width == 1 and height == 1:
|
||||
break
|
||||
width = maxi(1, floori(float(width) * 0.5))
|
||||
height = maxi(1, floori(float(height) * 0.5))
|
||||
return total
|
||||
|
||||
func _pop_next_build_key() -> String:
|
||||
var selected_index: int = 0
|
||||
var selected_scale: float = INF
|
||||
for index: int in range(queue.size()):
|
||||
var entry: TileEntry = entries.get(queue[index]) as TileEntry
|
||||
if entry == null:
|
||||
continue
|
||||
var scale_rank: float = maxf(entry.source_scale.x, entry.source_scale.y)
|
||||
if scale_rank < selected_scale:
|
||||
selected_scale = scale_rank
|
||||
selected_index = index
|
||||
return queue.pop_at(selected_index)
|
||||
|
||||
func _begin_build(entry: TileEntry, texture_size: Vector2i) -> void:
|
||||
_ensure_baker()
|
||||
RenderingServer.canvas_item_clear(_drawing.get_canvas_item())
|
||||
_viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED
|
||||
_viewport.size = texture_size
|
||||
var draw_rect: Rect2 = _baker_rect_for(entry)
|
||||
_drawing.source_rect = draw_rect
|
||||
_drawing.wall = entry.wall
|
||||
_drawing.finish = entry.finish
|
||||
_drawing.tint = entry.tint
|
||||
_drawing.origin = Vector2.ZERO
|
||||
_drawing.detail_view_size = entry.period if entry.is_pattern else entry.detail_view_size
|
||||
_drawing.low_detail = not entry.is_pattern and minf(entry.source_scale.x, entry.source_scale.y) < MIN_DETAILED_SCALE
|
||||
var draw_scale := Vector2(texture_size) / draw_rect.size
|
||||
_drawing.scale = draw_scale
|
||||
_drawing.position = -draw_rect.position * draw_scale
|
||||
_drawing.active = true
|
||||
_building_entry = entry
|
||||
_building_started_usec = Time.get_ticks_usec()
|
||||
_reserved_bytes = _estimated_texture_bytes(entry)
|
||||
entry.building = true
|
||||
_capture_pending = true
|
||||
_drawing.queue_redraw()
|
||||
_viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
|
||||
if not RenderingServer.frame_post_draw.is_connected(_on_frame_post_draw):
|
||||
RenderingServer.frame_post_draw.connect(_on_frame_post_draw, CONNECT_ONE_SHOT)
|
||||
StageSurfaceCache._active_pool_ref = weakref(self)
|
||||
|
||||
func _finish_vector_submission(elapsed_usec: int) -> void:
|
||||
vector_submission_usec = elapsed_usec
|
||||
|
||||
func _on_frame_post_draw() -> void:
|
||||
if not _capture_pending or _building_entry == null:
|
||||
return
|
||||
var entry: TileEntry = _building_entry
|
||||
var image: Image
|
||||
if _entry_has_live_watchers(entry) and is_instance_valid(_viewport):
|
||||
image = _viewport.get_texture().get_image()
|
||||
if image != null and not image.is_empty():
|
||||
if entry.is_pattern:
|
||||
image = _crop_pattern_image(image, entry)
|
||||
if image != null and not image.is_empty():
|
||||
image.generate_mipmaps()
|
||||
if image != null and not image.is_empty():
|
||||
var texture := ImageTexture.create_from_image(image)
|
||||
entry.texture = texture
|
||||
entry.size = image.get_size()
|
||||
entry.byte_count = image.get_data_size()
|
||||
resident_bytes += entry.byte_count
|
||||
if entry.is_pattern:
|
||||
pattern_builds += 1
|
||||
pattern_readbacks += 1
|
||||
pattern_resident_bytes += entry.byte_count
|
||||
_reserved_bytes = 0
|
||||
builds += 1
|
||||
readbacks += 1
|
||||
budget_pressure = false
|
||||
else:
|
||||
_reserved_bytes = 0
|
||||
refusals += 1
|
||||
_notify_entry_failed(entry)
|
||||
last_build_usec = Time.get_ticks_usec() - _building_started_usec
|
||||
if entry.texture != null:
|
||||
entry.build_usec = last_build_usec
|
||||
_notify_entry_ready(entry)
|
||||
entry.building = false
|
||||
_building_entry = null
|
||||
_capture_pending = false
|
||||
if is_instance_valid(_viewport):
|
||||
_viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED
|
||||
if is_instance_valid(_drawing):
|
||||
_drawing.active = false
|
||||
RenderingServer.canvas_item_clear(_drawing.get_canvas_item())
|
||||
if StageSurfaceCache._active_pool_ref != null and StageSurfaceCache._active_pool_ref.get_ref() == self:
|
||||
StageSurfaceCache._active_pool_ref = null
|
||||
_wake_waiters()
|
||||
StageSurfaceCache._service_pools()
|
||||
|
||||
func _notify_entry_ready(entry: TileEntry) -> void:
|
||||
for index: int in range(entry.watchers.size() - 1, -1, -1):
|
||||
var handle: Variant = entry.watchers[index].get_ref()
|
||||
if handle == null:
|
||||
entry.watchers.remove_at(index)
|
||||
continue
|
||||
handle._on_entry_ready(entry)
|
||||
|
||||
func _notify_entry_failed(entry: TileEntry) -> void:
|
||||
for index: int in range(entry.watchers.size() - 1, -1, -1):
|
||||
var handle: Variant = entry.watchers[index].get_ref()
|
||||
if handle == null:
|
||||
entry.watchers.remove_at(index)
|
||||
continue
|
||||
handle._on_entry_failed(entry)
|
||||
|
||||
func _entry_has_live_watchers(entry: TileEntry) -> bool:
|
||||
var has_watcher: bool = false
|
||||
for index: int in range(entry.watchers.size() - 1, -1, -1):
|
||||
var handle: Variant = entry.watchers[index].get_ref()
|
||||
if handle == null or handle._disposed:
|
||||
entry.watchers.remove_at(index)
|
||||
continue
|
||||
if _handle_is_interested(handle):
|
||||
has_watcher = true
|
||||
return has_watcher
|
||||
|
||||
func _make_room(required_bytes: int, protected_entry: TileEntry) -> bool:
|
||||
if resident_bytes + retired_bytes + _reserved_bytes + required_bytes <= MAX_CACHE_BYTES:
|
||||
return true
|
||||
if retired_bytes > 0:
|
||||
return false
|
||||
if not _evict_one(protected_entry):
|
||||
return false
|
||||
# Keep the old texture alive until the frame that releases its CanvasItem RIDs.
|
||||
# The request will retry after the post-draw retirement callback.
|
||||
return resident_bytes + retired_bytes + _reserved_bytes + required_bytes <= MAX_CACHE_BYTES
|
||||
|
||||
func _evict_one(protected_entry: TileEntry) -> bool:
|
||||
var candidate: TileEntry
|
||||
for value: Variant in entries.values():
|
||||
var entry: TileEntry = value as TileEntry
|
||||
if entry == null or entry == protected_entry or entry.building:
|
||||
continue
|
||||
if _entry_is_protected(entry):
|
||||
continue
|
||||
if candidate == null or entry.last_used_frame < candidate.last_used_frame:
|
||||
candidate = entry
|
||||
if candidate == null:
|
||||
return false
|
||||
_evict_entry(candidate)
|
||||
return true
|
||||
|
||||
func _entry_is_protected(entry: TileEntry) -> bool:
|
||||
if entry.texture == null:
|
||||
return false
|
||||
var current_frame: int = Engine.get_process_frames()
|
||||
for index: int in range(entry.watchers.size() - 1, -1, -1):
|
||||
var handle: Variant = entry.watchers[index].get_ref()
|
||||
if handle == null or handle._disposed:
|
||||
entry.watchers.remove_at(index)
|
||||
continue
|
||||
if _handle_is_visible(handle) and (handle.wanted or current_frame - handle.last_used_frame <= RECENT_USE_PROTECTION_FRAMES):
|
||||
return true
|
||||
return false
|
||||
|
||||
func _evict_entry(entry: TileEntry) -> void:
|
||||
if entry == null or entry.building:
|
||||
return
|
||||
if entry.queued:
|
||||
queue.erase(entry.key)
|
||||
entry.queued = false
|
||||
_clear_entry_canvas_owners(entry)
|
||||
for index: int in range(entry.watchers.size() - 1, -1, -1):
|
||||
var handle: Variant = entry.watchers[index].get_ref()
|
||||
if handle == null:
|
||||
entry.watchers.remove_at(index)
|
||||
continue
|
||||
handle._on_entry_evicted(entry)
|
||||
var retired_texture: Texture2D = entry.texture
|
||||
var retired_size_bytes: int = entry.byte_count
|
||||
entry.texture = null
|
||||
resident_bytes = maxi(0, resident_bytes - entry.byte_count)
|
||||
if entry.is_pattern:
|
||||
pattern_resident_bytes = maxi(0, pattern_resident_bytes - entry.byte_count)
|
||||
entry.byte_count = 0
|
||||
if retired_texture != null:
|
||||
_retired_textures.append(retired_texture)
|
||||
retired_bytes += retired_size_bytes
|
||||
if entry.is_pattern:
|
||||
pattern_retired_bytes += retired_size_bytes
|
||||
if not _retire_callback_pending:
|
||||
_retire_callback_pending = true
|
||||
RenderingServer.frame_post_draw.connect(_on_retired_frame_post_draw, CONNECT_ONE_SHOT)
|
||||
if entries.get(entry.key) == entry:
|
||||
entries.erase(entry.key)
|
||||
evictions += 1
|
||||
|
||||
func _clear_owner_canvas(owner: Variant) -> void:
|
||||
if not (owner is CanvasItem) or not is_instance_valid(owner):
|
||||
return
|
||||
var canvas_item: CanvasItem = owner as CanvasItem
|
||||
RenderingServer.canvas_item_clear(canvas_item.get_canvas_item())
|
||||
canvas_item.queue_redraw()
|
||||
|
||||
func _clear_entry_canvas_owners(entry: TileEntry) -> void:
|
||||
var cleared_owners: Dictionary = {}
|
||||
for owner_ref: WeakRef in entry.canvas_owners:
|
||||
var owner: Variant = owner_ref.get_ref()
|
||||
if owner == null or not is_instance_valid(owner):
|
||||
continue
|
||||
var owner_id: int = owner.get_instance_id()
|
||||
if cleared_owners.has(owner_id):
|
||||
continue
|
||||
_clear_owner_canvas(owner)
|
||||
cleared_owners[owner_id] = true
|
||||
entry.canvas_owners.clear()
|
||||
|
||||
func _stats() -> Dictionary:
|
||||
var queued_count: int = 0
|
||||
for value: Variant in entries.values():
|
||||
var entry: TileEntry = value as TileEntry
|
||||
if entry != null and entry.queued:
|
||||
queued_count += 1
|
||||
return {
|
||||
"bytes": resident_bytes,
|
||||
"resident_bytes": resident_bytes,
|
||||
"retired_bytes": retired_bytes,
|
||||
"entries": entries.size(),
|
||||
"queued": queued_count,
|
||||
"bakers": 1 if is_instance_valid(_viewport) else 0,
|
||||
"live_vector_bakers": 1 if _capture_pending else 0,
|
||||
"live_vector_items": 1 if _capture_pending else 0,
|
||||
"builds": builds,
|
||||
"cache_hits": cache_hits,
|
||||
"evictions": evictions,
|
||||
"refusals": refusals,
|
||||
"budget_pressure": budget_pressure,
|
||||
"budget_pressure_frame": budget_pressure_frame,
|
||||
"budget_blocked_requests": budget_blocked_requests,
|
||||
"waiting_handles": _waiting_handles.size(),
|
||||
"readbacks": readbacks,
|
||||
"pattern_builds": pattern_builds,
|
||||
"pattern_readbacks": pattern_readbacks,
|
||||
"pattern_resident_bytes": pattern_resident_bytes,
|
||||
"pattern_retired_bytes": pattern_retired_bytes,
|
||||
"last_build_usec": last_build_usec,
|
||||
"vector_submission_usec": vector_submission_usec,
|
||||
"active_build": _building_entry.key if _building_entry != null else ""
|
||||
}
|
||||
|
||||
func _exit_tree() -> void:
|
||||
_disposed = true
|
||||
if RenderingServer.frame_post_draw.is_connected(_on_frame_post_draw):
|
||||
RenderingServer.frame_post_draw.disconnect(_on_frame_post_draw)
|
||||
if _capture_pending and _building_entry != null:
|
||||
_building_entry.building = false
|
||||
_notify_entry_failed(_building_entry)
|
||||
_building_entry = null
|
||||
_capture_pending = false
|
||||
if StageSurfaceCache._active_pool_ref != null and StageSurfaceCache._active_pool_ref.get_ref() == self:
|
||||
StageSurfaceCache._active_pool_ref = null
|
||||
for value: Variant in entries.values():
|
||||
var entry: TileEntry = value as TileEntry
|
||||
if entry == null:
|
||||
continue
|
||||
_clear_entry_canvas_owners(entry)
|
||||
for watcher_ref: WeakRef in entry.watchers:
|
||||
var handle: Variant = watcher_ref.get_ref()
|
||||
if handle != null:
|
||||
handle._on_entry_evicted(entry)
|
||||
entry.texture = null
|
||||
entries.clear()
|
||||
queue.clear()
|
||||
_waiting_handles.clear()
|
||||
_retired_textures.clear()
|
||||
resident_bytes = 0
|
||||
retired_bytes = 0
|
||||
pattern_resident_bytes = 0
|
||||
pattern_retired_bytes = 0
|
||||
if is_instance_valid(_viewport):
|
||||
_viewport.queue_free()
|
||||
_viewport = null
|
||||
_drawing = null
|
||||
StageSurfaceCache._forget_pool(scope_id, self)
|
||||
|
||||
func _prune_queue() -> void:
|
||||
if queue.is_empty():
|
||||
return
|
||||
var kept: Array[String] = []
|
||||
for key: String in queue:
|
||||
var entry: TileEntry = entries.get(key) as TileEntry
|
||||
if entry == null or not entry.queued or entry.texture != null or not _entry_has_live_watchers(entry):
|
||||
if entry != null:
|
||||
entry.queued = false
|
||||
_set_entry_handle_queue_state(entry, false)
|
||||
if entry.texture == null and not entry.building and not _entry_has_live_watchers(entry):
|
||||
_evict_entry(entry)
|
||||
continue
|
||||
kept.append(key)
|
||||
queue = kept
|
||||
if queue.size() < MAX_QUEUED_BUILDS and not _waiting_handles.is_empty():
|
||||
_wake_waiters()
|
||||
|
||||
func _entry_has_any_handles(entry: TileEntry) -> bool:
|
||||
for index: int in range(entry.watchers.size() - 1, -1, -1):
|
||||
var handle: Variant = entry.watchers[index].get_ref()
|
||||
if handle == null or handle._disposed:
|
||||
entry.watchers.remove_at(index)
|
||||
else:
|
||||
return true
|
||||
return false
|
||||
|
||||
func _set_entry_handle_queue_state(entry: TileEntry, is_queued: bool) -> void:
|
||||
for watcher_ref: WeakRef in entry.watchers:
|
||||
var handle: Variant = watcher_ref.get_ref()
|
||||
if handle != null:
|
||||
handle._enqueued = is_queued
|
||||
|
||||
func _handle_is_interested(handle: StageSurfaceCache) -> bool:
|
||||
return handle != null and not handle._disposed and handle.wanted and _handle_is_visible(handle)
|
||||
|
||||
func _handle_is_visible(handle: StageSurfaceCache) -> bool:
|
||||
if handle == null or handle._disposed:
|
||||
return false
|
||||
var owner: Variant = handle._owner_ref.get_ref() if handle._owner_ref != null else null
|
||||
if owner == null or not is_instance_valid(owner) or not (owner is Node2D):
|
||||
return false
|
||||
if not owner.is_inside_tree() or not owner.is_visible_in_tree():
|
||||
return false
|
||||
var visible_rect: Rect2 = RENDERER._visible_local_rect(owner)
|
||||
if handle.is_pattern:
|
||||
if not owner.has_method("get_local_bounds"):
|
||||
return false
|
||||
var local_bounds: Variant = owner.call("get_local_bounds")
|
||||
if not (local_bounds is Rect2):
|
||||
return false
|
||||
var bounds: Rect2 = local_bounds
|
||||
return bounds.intersects(visible_rect)
|
||||
return handle.source_rect.intersects(visible_rect) or handle.core_rect.intersects(visible_rect)
|
||||
|
||||
func _add_waiter(handle: StageSurfaceCache) -> void:
|
||||
for waiter_ref: WeakRef in _waiting_handles:
|
||||
if waiter_ref.get_ref() == handle:
|
||||
return
|
||||
_waiting_handles.append(weakref(handle))
|
||||
|
||||
func _wake_waiters() -> void:
|
||||
if _waiting_handles.is_empty() or _disposed:
|
||||
return
|
||||
var pending: Array[WeakRef] = _waiting_handles
|
||||
_waiting_handles = []
|
||||
for waiter_ref: WeakRef in pending:
|
||||
var handle: Variant = waiter_ref.get_ref()
|
||||
if handle == null or handle._disposed or not _handle_is_interested(handle):
|
||||
continue
|
||||
queue_handle(handle)
|
||||
|
||||
func _on_retired_frame_post_draw() -> void:
|
||||
_retired_textures.clear()
|
||||
retired_bytes = 0
|
||||
pattern_retired_bytes = 0
|
||||
_retire_callback_pending = false
|
||||
_wake_waiters()
|
||||
StageSurfaceCache._service_pools()
|
||||
|
||||
|
||||
var source_rect: Rect2
|
||||
var source_scale: Vector2
|
||||
var core_rect: Rect2
|
||||
var ready_for_display: bool = false
|
||||
var wanted: bool = false
|
||||
var last_used_frame: int = 0
|
||||
var build_usec: int = 0
|
||||
var _enqueued: bool = false
|
||||
var _building: bool = false
|
||||
var _pending_arguments: Array = []
|
||||
var _owner_ref: WeakRef
|
||||
var _pool_ref: WeakRef
|
||||
var _entry_ref: WeakRef
|
||||
var _cache_key: String = ""
|
||||
var _disposed: bool = false
|
||||
var _finish: StringName
|
||||
var _tint: Color
|
||||
var _origin: Vector2
|
||||
var is_pattern: bool = false
|
||||
|
||||
var size: Vector2i:
|
||||
get:
|
||||
var entry: TileEntry = _entry_ref.get_ref() as TileEntry if _entry_ref != null else null
|
||||
return entry.size if entry != null else Vector2i.ZERO
|
||||
|
||||
var render_target_update_mode: int:
|
||||
get:
|
||||
return SubViewport.UPDATE_DISABLED
|
||||
|
||||
|
||||
func attach_to(owner_node: Node) -> void:
|
||||
_owner_ref = weakref(owner_node)
|
||||
var pool: BakePool = _get_or_create_pool(owner_node)
|
||||
_pool_ref = weakref(pool) if pool != null else null
|
||||
|
||||
|
||||
func dispose() -> void:
|
||||
wanted = false
|
||||
_disposed = true
|
||||
var entry: TileEntry = _entry_ref.get_ref() as TileEntry if _entry_ref != null else null
|
||||
var pool: BakePool = _pool_ref.get_ref() as BakePool if _pool_ref != null else null
|
||||
if entry != null and pool != null:
|
||||
pool._unwatch_handle(entry, self)
|
||||
_entry_ref = null
|
||||
ready_for_display = false
|
||||
_building = false
|
||||
_enqueued = false
|
||||
|
||||
|
||||
func get_texture() -> Texture2D:
|
||||
var entry: TileEntry = _entry_ref.get_ref() as TileEntry if _entry_ref != null else null
|
||||
return entry.texture if entry != null else null
|
||||
|
||||
|
||||
func request_build(core: Rect2, rect: Rect2, pixel_scale: Vector2, wall: bool, finish: StringName, tint: Color, origin: Vector2, detail_view_size: Vector2) -> void:
|
||||
if _disposed:
|
||||
return
|
||||
is_pattern = false
|
||||
core_rect = core
|
||||
source_rect = rect
|
||||
source_scale = pixel_scale
|
||||
wanted = true
|
||||
_finish = finish
|
||||
_tint = tint
|
||||
_origin = origin
|
||||
var canonical_position: Vector2 = _snap_canonical_position(rect.position + origin, pixel_scale)
|
||||
var canonical_rect := Rect2(canonical_position, rect.size)
|
||||
_pending_arguments = [canonical_rect, pixel_scale, wall, finish, tint, detail_view_size]
|
||||
_cache_key = _make_cache_key(canonical_rect, pixel_scale, wall, finish, tint, detail_view_size)
|
||||
queue_if_needed()
|
||||
|
||||
|
||||
func request_pattern(wall: bool, finish: StringName, tint: Color, period: Vector2) -> void:
|
||||
if _disposed:
|
||||
return
|
||||
var safe_period := Vector2(maxf(1.0, period.x), maxf(1.0, period.y))
|
||||
is_pattern = true
|
||||
core_rect = Rect2(Vector2.ZERO, safe_period)
|
||||
source_rect = core_rect
|
||||
source_scale = Vector2.ONE * PATTERN_PIXEL_SCALE
|
||||
wanted = true
|
||||
_finish = finish
|
||||
_tint = tint
|
||||
_origin = Vector2.ZERO
|
||||
_pending_arguments = [core_rect, source_scale, wall, finish, tint, safe_period, true, safe_period]
|
||||
_cache_key = _make_pattern_cache_key(wall, finish, tint, safe_period)
|
||||
queue_if_needed()
|
||||
|
||||
|
||||
func ensure_queued() -> void:
|
||||
if _disposed:
|
||||
return
|
||||
queue_if_needed()
|
||||
|
||||
|
||||
func queue_if_needed() -> void:
|
||||
var pool: BakePool = _pool_ref.get_ref() as BakePool if _pool_ref != null else null
|
||||
if pool == null:
|
||||
var owner: Variant = _owner_ref.get_ref() if _owner_ref != null else null
|
||||
if owner != null and is_instance_valid(owner):
|
||||
pool = _get_or_create_pool(owner)
|
||||
_pool_ref = weakref(pool) if pool != null else null
|
||||
if pool != null:
|
||||
pool.queue_handle(self)
|
||||
|
||||
|
||||
func _on_entry_ready(entry: TileEntry) -> void:
|
||||
if _disposed:
|
||||
return
|
||||
ready_for_display = true
|
||||
_building = false
|
||||
_enqueued = false
|
||||
build_usec = entry.build_usec
|
||||
updated.emit()
|
||||
|
||||
|
||||
func _on_entry_failed(entry: TileEntry) -> void:
|
||||
if _disposed:
|
||||
return
|
||||
if _entry_ref != null and _entry_ref.get_ref() == entry:
|
||||
ready_for_display = false
|
||||
_building = false
|
||||
_enqueued = false
|
||||
updated.emit()
|
||||
|
||||
|
||||
func _on_entry_evicted(entry: TileEntry) -> void:
|
||||
if _entry_ref != null and _entry_ref.get_ref() == entry:
|
||||
_entry_ref = null
|
||||
ready_for_display = false
|
||||
_building = false
|
||||
_enqueued = false
|
||||
updated.emit()
|
||||
|
||||
|
||||
static func build_next() -> void:
|
||||
_service_pools()
|
||||
|
||||
|
||||
static func _service_pools() -> void:
|
||||
var frame: int = Engine.get_process_frames()
|
||||
if frame == _last_service_frame:
|
||||
return
|
||||
_last_service_frame = frame
|
||||
var active_pool: Variant = _active_pool_ref.get_ref() if _active_pool_ref != null else null
|
||||
if active_pool != null and is_instance_valid(active_pool):
|
||||
if not active_pool._capture_pending:
|
||||
active_pool._start_next_build()
|
||||
return
|
||||
_active_pool_ref = null
|
||||
for scope_id: Variant in _pool_refs.keys():
|
||||
var pool: Variant = _pool_refs[scope_id].get_ref()
|
||||
if pool == null or not is_instance_valid(pool):
|
||||
_pool_refs.erase(scope_id)
|
||||
continue
|
||||
if pool._start_next_build():
|
||||
return
|
||||
|
||||
|
||||
static func _get_or_create_pool(owner_node: Node) -> BakePool:
|
||||
if owner_node == null or not is_instance_valid(owner_node) or owner_node.get_tree() == null:
|
||||
return null
|
||||
var scope: Node = _get_scope_node(owner_node)
|
||||
if scope == null or not is_instance_valid(scope):
|
||||
return null
|
||||
var scope_id: int = scope.get_instance_id()
|
||||
var pool_ref: WeakRef = _pool_refs.get(scope_id) as WeakRef
|
||||
var existing: BakePool = pool_ref.get_ref() as BakePool if pool_ref != null else null
|
||||
if existing != null and is_instance_valid(existing):
|
||||
return existing
|
||||
var pool := BakePool.new()
|
||||
pool.name = "StageSurfaceBakePool"
|
||||
pool.scope_id = scope_id
|
||||
scope.add_child(pool)
|
||||
_pool_refs[scope_id] = weakref(pool)
|
||||
return pool
|
||||
|
||||
|
||||
static func _get_scope_node(owner_node: Node) -> Node:
|
||||
var tree: SceneTree = owner_node.get_tree()
|
||||
if tree == null:
|
||||
return null
|
||||
var current_scene: Node = tree.current_scene
|
||||
if current_scene != null and (current_scene == owner_node or current_scene.is_ancestor_of(owner_node)):
|
||||
return current_scene
|
||||
var root: Window = tree.root
|
||||
var scope: Node = owner_node
|
||||
while scope.get_parent() != null and scope.get_parent() != root:
|
||||
scope = scope.get_parent()
|
||||
return scope
|
||||
|
||||
|
||||
static func _forget_pool(scope_id: int, pool: BakePool) -> void:
|
||||
var pool_ref: WeakRef = _pool_refs.get(scope_id) as WeakRef
|
||||
if pool_ref != null and pool_ref.get_ref() == pool:
|
||||
_pool_refs.erase(scope_id)
|
||||
|
||||
|
||||
static func _make_cache_key(rect: Rect2, pixel_scale: Vector2, wall: bool, finish: StringName, tint: Color, detail_view_size: Vector2) -> String:
|
||||
return var_to_str([wall, finish, tint, rect.position.x, rect.position.y, rect.size.x, rect.size.y, pixel_scale.x, pixel_scale.y, detail_view_size.x, detail_view_size.y])
|
||||
|
||||
|
||||
static func _make_pattern_cache_key(wall: bool, finish: StringName, tint: Color, period: Vector2) -> String:
|
||||
return var_to_str([&"periodic_style", PATTERN_CACHE_VERSION, wall, finish, tint, period.x, period.y, PATTERN_PIXEL_SCALE, &"mipmaps"])
|
||||
|
||||
|
||||
static func _snap_canonical_position(position: Vector2, pixel_scale: Vector2) -> Vector2:
|
||||
var scale_x: float = maxf(0.001, pixel_scale.x)
|
||||
var scale_y: float = maxf(0.001, pixel_scale.y)
|
||||
return Vector2(roundf(position.x * scale_x) / scale_x, roundf(position.y * scale_y) / scale_y)
|
||||
|
||||
|
||||
static func get_stats(owner_node: Node = null) -> Dictionary:
|
||||
if owner_node != null and is_instance_valid(owner_node):
|
||||
var scope: Node = _get_scope_node(owner_node)
|
||||
if scope != null:
|
||||
var pool_ref: WeakRef = _pool_refs.get(scope.get_instance_id()) as WeakRef
|
||||
var pool: BakePool = pool_ref.get_ref() as BakePool if pool_ref != null else null
|
||||
return pool._stats() if pool != null else _empty_stats()
|
||||
var totals: Dictionary = _empty_stats()
|
||||
for scope_id: Variant in _pool_refs.keys():
|
||||
var pool: BakePool = _pool_refs[scope_id].get_ref() as BakePool
|
||||
if pool == null or not is_instance_valid(pool):
|
||||
continue
|
||||
var stats: Dictionary = pool._stats()
|
||||
for key: String in totals.keys():
|
||||
if key == "active_build":
|
||||
if not str(stats[key]).is_empty():
|
||||
totals[key] = stats[key]
|
||||
elif key == "last_build_usec" or key == "vector_submission_usec" or key == "budget_pressure_frame":
|
||||
totals[key] = maxi(int(totals[key]), int(stats[key]))
|
||||
else:
|
||||
totals[key] = int(totals[key]) + int(stats[key])
|
||||
return totals
|
||||
|
||||
|
||||
static func _empty_stats() -> Dictionary:
|
||||
return {
|
||||
"bytes": 0,
|
||||
"resident_bytes": 0,
|
||||
"retired_bytes": 0,
|
||||
"entries": 0,
|
||||
"queued": 0,
|
||||
"bakers": 0,
|
||||
"live_vector_bakers": 0,
|
||||
"live_vector_items": 0,
|
||||
"builds": 0,
|
||||
"cache_hits": 0,
|
||||
"evictions": 0,
|
||||
"refusals": 0,
|
||||
"budget_pressure": 0,
|
||||
"budget_pressure_frame": -1,
|
||||
"budget_blocked_requests": 0,
|
||||
"waiting_handles": 0,
|
||||
"readbacks": 0,
|
||||
"pattern_builds": 0,
|
||||
"pattern_readbacks": 0,
|
||||
"pattern_resident_bytes": 0,
|
||||
"pattern_retired_bytes": 0,
|
||||
"last_build_usec": 0,
|
||||
"vector_submission_usec": 0,
|
||||
"active_build": ""
|
||||
}
|
||||
|
||||
|
||||
static func texture_size_for(local_size: Vector2, pixel_scale: Vector2) -> Vector2i:
|
||||
var safe_size := Vector2(maxf(1.0, local_size.x), maxf(1.0, local_size.y))
|
||||
var safe_scale := Vector2(maxf(0.001, pixel_scale.x), maxf(0.001, pixel_scale.y))
|
||||
var pixels: Vector2 = safe_size * safe_scale
|
||||
var reduction: float = maxf(1.0, maxf(pixels.x, pixels.y) / float(MAX_TEXTURE_EDGE))
|
||||
reduction = maxf(reduction, sqrt(pixels.x * pixels.y / MAX_TEXTURE_PIXELS))
|
||||
return Vector2i(maxi(1, floori(pixels.x / reduction)), maxi(1, floori(pixels.y / reduction)))
|
||||
@@ -0,0 +1 @@
|
||||
uid://cwcf7h5yf3xi0
|
||||
@@ -7,6 +7,8 @@ const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_ren
|
||||
var surface_kind: StringName = &"wall"
|
||||
var finish_id: StringName = &"finish_wall_paint"
|
||||
var tint: Color = Color("#5A9D70")
|
||||
var corner_radius: float = 0.0
|
||||
var outside_color: Color = Color("#202633FA")
|
||||
|
||||
|
||||
func configure(kind: StringName, finish: StringName, color: Color) -> void:
|
||||
@@ -17,6 +19,12 @@ func configure(kind: StringName, finish: StringName, color: Color) -> void:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func set_rounded_frame(radius: float, background: Color) -> void:
|
||||
corner_radius = maxf(0.0, radius)
|
||||
outside_color = background
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var rect: Rect2 = Rect2(Vector2(3.0, 3.0), size - Vector2(6.0, 6.0))
|
||||
if rect.size.x <= 0.0 or rect.size.y <= 0.0:
|
||||
@@ -25,4 +33,49 @@ func _draw() -> void:
|
||||
SURFACE_RENDERER.draw_background(self, rect, finish_id, tint, Vector2.ZERO, Vector2.ONE, false)
|
||||
else:
|
||||
SURFACE_RENDERER.draw_wall(self, rect, finish_id, tint, Vector2.ZERO, Vector2.ONE, false)
|
||||
draw_rect(rect, Color("#F5F7FA"), false, 2.0)
|
||||
var radius: float = minf(corner_radius, minf(rect.size.x, rect.size.y) * 0.5)
|
||||
if radius > 0.0:
|
||||
var wall: bool = surface_kind != &"background_ground"
|
||||
var base_color: Color = SURFACE_RENDERER.surface_base_color(finish_id, tint, wall)
|
||||
_draw_rounded_corner(rect, radius, base_color, outside_color, 0)
|
||||
_draw_rounded_corner(rect, radius, base_color, outside_color, 1)
|
||||
_draw_rounded_corner(rect, radius, base_color, outside_color, 2)
|
||||
_draw_rounded_corner(rect, radius, base_color, outside_color, 3)
|
||||
var outline := StyleBoxFlat.new()
|
||||
outline.bg_color = Color.TRANSPARENT
|
||||
outline.draw_center = false
|
||||
outline.border_color = Color("#F5F7FA")
|
||||
outline.set_border_width_all(2)
|
||||
outline.set_corner_radius_all(roundi(radius))
|
||||
draw_style_box(outline, rect)
|
||||
else:
|
||||
draw_rect(rect, Color("#F5F7FA"), false, 2.0)
|
||||
|
||||
|
||||
func _draw_rounded_corner(rect: Rect2, radius: float, base_color: Color, background: Color, corner: int) -> void:
|
||||
var square := Rect2()
|
||||
var center := Vector2.ZERO
|
||||
var start_angle: float = 0.0
|
||||
match corner:
|
||||
0:
|
||||
square = Rect2(rect.position, Vector2.ONE * radius)
|
||||
center = rect.position + Vector2.ONE * radius
|
||||
start_angle = PI
|
||||
1:
|
||||
square = Rect2(Vector2(rect.end.x - radius, rect.position.y), Vector2.ONE * radius)
|
||||
center = Vector2(rect.end.x - radius, rect.position.y + radius)
|
||||
start_angle = -PI * 0.5
|
||||
2:
|
||||
square = Rect2(rect.end - Vector2.ONE * radius, Vector2.ONE * radius)
|
||||
center = rect.end - Vector2.ONE * radius
|
||||
start_angle = 0.0
|
||||
_:
|
||||
square = Rect2(Vector2(rect.position.x, rect.end.y - radius), Vector2.ONE * radius)
|
||||
center = Vector2(rect.position.x + radius, rect.end.y - radius)
|
||||
start_angle = PI * 0.5
|
||||
draw_rect(square, background, true)
|
||||
var arc := PackedVector2Array([center])
|
||||
for point_index: int in 13:
|
||||
var angle: float = start_angle + PI * 0.5 * float(point_index) / 12.0
|
||||
arc.append(center + Vector2(cos(angle), sin(angle)) * radius)
|
||||
draw_colored_polygon(arc, base_color)
|
||||
|
||||
@@ -13,12 +13,37 @@ const WALL_BRICK_WIDTH: float = 100.0
|
||||
const WALL_BRICK_HEIGHT: float = 38.0
|
||||
const MAX_WALL_BRICK_CELLS: int = 1200
|
||||
const MAX_BACKGROUND_CELLS: int = 12000
|
||||
## Finite material variations repeat independently of wall size and camera travel.
|
||||
## Even row periods preserve staggered brick/plank courses across the seam.
|
||||
const BRICK_PATTERN_CELLS := Vector2i(8, 8)
|
||||
const WOOD_PATTERN_CELLS := Vector2i(3, 6)
|
||||
const GRASS_PATTERN_CELLS := Vector2i(16, 16)
|
||||
const CONCRETE_PATTERN_CELLS := Vector2i(4, 4)
|
||||
const CARPET_PATTERN_CELLS := Vector2i(16, 16)
|
||||
## Conservative work estimate to avoid an expensive first placement redraw.
|
||||
## Runtime timing in StageRectSurface also catches slower machines.
|
||||
const PREVIEW_WORK_BUDGET: float = 6000.0
|
||||
const MAX_PREVIEW_TEXTURE_CELLS: int = 128
|
||||
|
||||
|
||||
static func is_wall_finish(id: StringName) -> bool:
|
||||
return WALL_FINISHES.has(id)
|
||||
|
||||
|
||||
static func pattern_size_for(finish: StringName) -> Vector2:
|
||||
match finish:
|
||||
&"finish_wall_brick": return Vector2(BRICK_PATTERN_CELLS) * Vector2(WALL_BRICK_WIDTH, WALL_BRICK_HEIGHT)
|
||||
&"finish_wall_wood": return Vector2(WOOD_PATTERN_CELLS) * Vector2(320.0, 64.0)
|
||||
&"finish_background_grass": return Vector2(GRASS_PATTERN_CELLS) * Vector2(44.0, 36.0)
|
||||
&"finish_background_concrete": return Vector2(CONCRETE_PATTERN_CELLS) * Vector2(280.0, 190.0)
|
||||
&"finish_background_carpet": return Vector2(CARPET_PATTERN_CELLS) * Vector2(34.0, 26.0)
|
||||
_: return Vector2.ZERO
|
||||
|
||||
|
||||
static func _pattern_seed(row: int, column: int, cells: Vector2i, row_factor: int, column_factor: int, offset: int = 0) -> int:
|
||||
return posmod(row, cells.y) * row_factor + posmod(column, cells.x) * column_factor + offset
|
||||
|
||||
|
||||
static func is_background_finish(id: StringName) -> bool:
|
||||
return BACKGROUND_FINISHES.has(id)
|
||||
|
||||
@@ -38,6 +63,28 @@ static func is_ground_finish(id: StringName) -> bool:
|
||||
return GROUND_FINISHES.has(id) or id == &"finish_ground_legacy"
|
||||
|
||||
|
||||
static func surface_base_color(finish: StringName, tint: Color, wall: bool) -> Color:
|
||||
if finish == &"finish_ground_legacy":
|
||||
return Color("#468B42")
|
||||
if finish.ends_with("brick"):
|
||||
return Color("#D2CBAF") if wall else Color("#994D35")
|
||||
if finish.ends_with("wood"):
|
||||
return Color("#9A673C") if wall else Color("#B88951")
|
||||
if finish == &"finish_ground_grass" or finish == &"finish_background_grass":
|
||||
return Color("#72A84C")
|
||||
if finish == &"finish_background_concrete":
|
||||
return Color("#A6AAA3")
|
||||
if finish == &"finish_background_carpet":
|
||||
return tint
|
||||
if finish == &"finish_ground_dirt":
|
||||
return Color("#8E633E")
|
||||
if finish == &"finish_ground_sidewalk":
|
||||
return Color("#B7B9B2")
|
||||
if finish == &"finish_ground_asphalt":
|
||||
return Color("#505A62")
|
||||
return tint
|
||||
|
||||
|
||||
static func draw_wall(
|
||||
canvas: CanvasItem,
|
||||
rect: Rect2,
|
||||
@@ -45,9 +92,12 @@ static func draw_wall(
|
||||
tint: Color,
|
||||
world_origin: Vector2,
|
||||
drawing_scale: Vector2 = Vector2.ONE,
|
||||
cull_to_viewport: bool = true
|
||||
cull_to_viewport: bool = true,
|
||||
interactive_preview: bool = false,
|
||||
force_simple_preview: bool = false,
|
||||
detail_view_size: Vector2 = Vector2.ZERO
|
||||
) -> void:
|
||||
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, true, false, cull_to_viewport)
|
||||
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, true, false, cull_to_viewport, interactive_preview, force_simple_preview, detail_view_size)
|
||||
|
||||
|
||||
static func draw_ground(
|
||||
@@ -68,9 +118,11 @@ static func draw_background(
|
||||
tint: Color,
|
||||
world_origin: Vector2,
|
||||
drawing_scale: Vector2 = Vector2.ONE,
|
||||
cull_to_viewport: bool = true
|
||||
cull_to_viewport: bool = true,
|
||||
interactive_preview: bool = false,
|
||||
force_simple_preview: bool = false
|
||||
) -> void:
|
||||
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, false, true, cull_to_viewport)
|
||||
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, false, true, cull_to_viewport, interactive_preview, force_simple_preview)
|
||||
|
||||
|
||||
static func _draw_surface(
|
||||
@@ -82,66 +134,47 @@ static func _draw_surface(
|
||||
drawing_scale: Vector2,
|
||||
wall: bool,
|
||||
background: bool,
|
||||
cull_to_viewport: bool
|
||||
cull_to_viewport: bool,
|
||||
interactive_preview: bool = false,
|
||||
force_simple_preview: bool = false,
|
||||
detail_view_size: Vector2 = Vector2.ZERO
|
||||
) -> void:
|
||||
var normalized := rect.abs()
|
||||
if normalized.size.x <= 0.0 or normalized.size.y <= 0.0:
|
||||
return
|
||||
var motif_scale: Vector2 = _normalized_scale(drawing_scale)
|
||||
var base: Color = tint
|
||||
if finish == &"finish_ground_legacy":
|
||||
base = Color("#468B42")
|
||||
elif finish.ends_with("brick"):
|
||||
base = Color("#D2CBAF") if wall else Color("#994D35")
|
||||
elif finish.ends_with("wood"):
|
||||
base = Color("#9A673C") if wall else Color("#B88951")
|
||||
elif finish == &"finish_ground_grass" or finish == &"finish_background_grass":
|
||||
base = Color("#72A84C")
|
||||
elif finish == &"finish_background_concrete":
|
||||
base = Color("#A6AAA3")
|
||||
elif finish == &"finish_background_carpet":
|
||||
base = tint
|
||||
elif finish == &"finish_ground_dirt":
|
||||
base = Color("#8E633E")
|
||||
elif finish == &"finish_ground_sidewalk":
|
||||
base = Color("#B7B9B2")
|
||||
elif finish == &"finish_ground_asphalt":
|
||||
base = Color("#505A62")
|
||||
var base: Color = surface_base_color(finish, tint, wall)
|
||||
canvas.draw_rect(normalized, base, true)
|
||||
var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized
|
||||
var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized
|
||||
var simplified: bool = interactive_preview and (force_simple_preview or needs_light_preview(finish, pattern_rect.size, motif_scale))
|
||||
if finish.ends_with("brick"):
|
||||
var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized
|
||||
var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized
|
||||
if pattern_rect.size.x > 0.0 and pattern_rect.size.y > 0.0:
|
||||
var detail_size: Vector2 = view_rect.size if not view_rect.size.is_zero_approx() else normalized.size
|
||||
if detail_view_size.x > 0.0 and detail_view_size.y > 0.0:
|
||||
detail_size = detail_view_size
|
||||
var detail_scale: float = brick_detail_scale_for_view(detail_size, motif_scale, wall)
|
||||
if wall:
|
||||
_draw_wall_bricks(canvas, pattern_rect, world_origin, motif_scale, detail_scale, tint)
|
||||
_draw_wall_bricks(canvas, pattern_rect, world_origin, motif_scale, detail_scale, tint, simplified)
|
||||
else:
|
||||
_draw_bricks(canvas, pattern_rect, world_origin, base, motif_scale, detail_scale)
|
||||
elif finish.ends_with("wood"):
|
||||
if wall:
|
||||
var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized
|
||||
var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized
|
||||
if pattern_rect.size.x > 0.0 and pattern_rect.size.y > 0.0:
|
||||
var detail_scale: float = wood_detail_scale_for_view(pattern_rect.size, motif_scale)
|
||||
_draw_wall_wood(canvas, pattern_rect, world_origin, motif_scale, detail_scale, tint)
|
||||
var detail_size: Vector2 = detail_view_size if detail_view_size.x > 0.0 and detail_view_size.y > 0.0 else pattern_rect.size
|
||||
var detail_scale: float = wood_detail_scale_for_view(detail_size, motif_scale)
|
||||
_draw_wall_wood(canvas, pattern_rect, world_origin, motif_scale, detail_scale, tint, simplified)
|
||||
else:
|
||||
_draw_wood(canvas, normalized, world_origin, base, motif_scale)
|
||||
elif finish == &"finish_background_grass":
|
||||
var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized
|
||||
var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized
|
||||
if pattern_rect.size.x > 0.0 and pattern_rect.size.y > 0.0:
|
||||
_draw_background_grass(canvas, pattern_rect, world_origin, motif_scale)
|
||||
_draw_background_grass(canvas, pattern_rect, world_origin, motif_scale, simplified)
|
||||
elif finish == &"finish_background_concrete":
|
||||
var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized
|
||||
var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized
|
||||
if pattern_rect.size.x > 0.0 and pattern_rect.size.y > 0.0:
|
||||
_draw_background_concrete(canvas, pattern_rect, world_origin, motif_scale)
|
||||
_draw_background_concrete(canvas, pattern_rect, world_origin, motif_scale, simplified)
|
||||
elif finish == &"finish_background_carpet":
|
||||
var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized
|
||||
var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized
|
||||
if pattern_rect.size.x > 0.0 and pattern_rect.size.y > 0.0:
|
||||
_draw_background_carpet(canvas, pattern_rect, world_origin, motif_scale, tint)
|
||||
_draw_background_carpet(canvas, pattern_rect, world_origin, motif_scale, tint, simplified)
|
||||
elif finish == &"finish_ground_grass":
|
||||
_draw_grass(canvas, normalized, world_origin, motif_scale)
|
||||
elif finish == &"finish_ground_dirt":
|
||||
@@ -154,6 +187,39 @@ static func _draw_surface(
|
||||
push_warning("StageSurfaceRenderer: unknown finish '%s'; drawing paint." % String(finish))
|
||||
|
||||
|
||||
static func needs_light_preview(finish: StringName, visible_size: Vector2, motif_scale: Vector2 = Vector2.ONE) -> bool:
|
||||
if visible_size.x <= 0.0 or visible_size.y <= 0.0:
|
||||
return false
|
||||
var cell_size: Vector2
|
||||
var work_per_cell: float
|
||||
match finish:
|
||||
&"finish_wall_brick":
|
||||
cell_size = Vector2(WALL_BRICK_WIDTH, WALL_BRICK_HEIGHT)
|
||||
work_per_cell = 240.0
|
||||
&"finish_wall_wood":
|
||||
cell_size = Vector2(320.0, 64.0)
|
||||
work_per_cell = 750.0
|
||||
&"finish_background_grass":
|
||||
cell_size = Vector2(44.0, 36.0)
|
||||
work_per_cell = 70.0
|
||||
&"finish_background_concrete":
|
||||
cell_size = Vector2(280.0, 190.0)
|
||||
work_per_cell = 20.0
|
||||
&"finish_background_carpet":
|
||||
cell_size = Vector2(34.0, 26.0)
|
||||
work_per_cell = 35.0
|
||||
_:
|
||||
return false
|
||||
var world_size: Vector2 = visible_size * _normalized_scale(motif_scale)
|
||||
var cells: float = (ceil(world_size.x / cell_size.x) + 1.0) * (ceil(world_size.y / cell_size.y) + 1.0)
|
||||
return cells * work_per_cell > PREVIEW_WORK_BUDGET
|
||||
|
||||
|
||||
static func _preview_texture_stride(size: Vector2, motif_scale: Vector2, cell_size: Vector2) -> int:
|
||||
var cells: Vector2 = size * motif_scale / cell_size + Vector2(3.0, 3.0)
|
||||
return maxi(1, ceili(sqrt(cells.x * cells.y / float(MAX_PREVIEW_TEXTURE_CELLS))))
|
||||
|
||||
|
||||
static func brick_detail_scale_for_view(view_size: Vector2, motif_scale: Vector2 = Vector2.ONE, wall: bool = false) -> float:
|
||||
var scale: float = 1.0
|
||||
var safe_size: Vector2 = Vector2(maxf(1.0, view_size.x), maxf(1.0, view_size.y))
|
||||
@@ -185,7 +251,7 @@ static func wood_detail_scale_for_view(view_size: Vector2, motif_scale: Vector2
|
||||
return scale
|
||||
|
||||
|
||||
static func _draw_wall_wood(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, detail_scale: float, tint: Color) -> void:
|
||||
static func _draw_wall_wood(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, detail_scale: float, tint: Color, simplified: bool = false) -> void:
|
||||
const BOARD_HEIGHT: float = 64.0
|
||||
const PLANK_WIDTH: float = 320.0
|
||||
var safe_scale: Vector2 = _normalized_scale(motif_scale)
|
||||
@@ -234,14 +300,15 @@ static func _draw_wall_wood(canvas: CanvasItem, rect: Rect2, origin: Vector2, mo
|
||||
Vector2(plank_local_left, board_local_top + board_world_height / safe_scale.y),
|
||||
_color_with_alpha(seam_color, 0.88), 2.0 / safe_scale.x
|
||||
)
|
||||
_draw_wall_wood_grain(
|
||||
canvas, rect, plank_world_left, board_world_top, plank_world_width, board_world_height,
|
||||
origin, safe_scale, row, column, grain_color, highlight_color, detail_scale, tint, natural
|
||||
)
|
||||
_draw_wall_wood_knot(
|
||||
canvas, rect, plank_world_left, board_world_top, plank_world_width, board_world_height,
|
||||
origin, safe_scale, row, column, detail_scale, tint, natural
|
||||
)
|
||||
if not simplified:
|
||||
_draw_wall_wood_grain(
|
||||
canvas, rect, plank_world_left, board_world_top, plank_world_width, board_world_height,
|
||||
origin, safe_scale, row, column, grain_color, highlight_color, detail_scale, tint, natural
|
||||
)
|
||||
_draw_wall_wood_knot(
|
||||
canvas, rect, plank_world_left, board_world_top, plank_world_width, board_world_height,
|
||||
origin, safe_scale, row, column, detail_scale, tint, natural
|
||||
)
|
||||
_draw_clipped_line(
|
||||
canvas, rect,
|
||||
Vector2(rect.position.x, board_local_top), Vector2(rect.end.x, board_local_top),
|
||||
@@ -274,7 +341,7 @@ static func _draw_wall_wood_grain(
|
||||
) -> void:
|
||||
const TRACK_COUNT: int = 5
|
||||
const SAMPLES: int = 32
|
||||
var seed: int = row * 73856093 + column * 19349663
|
||||
var seed: int = _pattern_seed(row, column, WOOD_PATTERN_CELLS, 73856093, 19349663)
|
||||
for track: int in TRACK_COUNT:
|
||||
var track_seed: int = seed + track * 83492791
|
||||
var lane: float = 0.16 + _wood_hash(track_seed) * 0.68
|
||||
@@ -312,7 +379,7 @@ static func _draw_wall_wood_knot(
|
||||
tint: Color,
|
||||
natural: Color
|
||||
) -> void:
|
||||
var seed: int = row * 92821 + column * 68917 + 31
|
||||
var seed: int = _pattern_seed(row, column, WOOD_PATTERN_CELLS, 92821, 68917, 31)
|
||||
if _wood_hash(seed) > 0.24:
|
||||
return
|
||||
var center_world := Vector2(
|
||||
@@ -376,7 +443,7 @@ static func _material_color(source: Color, selected: Color, natural: Color) -> C
|
||||
return Color.from_hsv(selected.h, saturation, value, source.a)
|
||||
|
||||
|
||||
static func _draw_background_grass(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2) -> void:
|
||||
static func _draw_background_grass(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, simplified: bool = false) -> void:
|
||||
const CELL_WIDTH: float = 44.0
|
||||
const CELL_HEIGHT: float = 36.0
|
||||
var world_left: float = rect.position.x * motif_scale.x + origin.x
|
||||
@@ -388,12 +455,16 @@ static func _draw_background_grass(canvas: CanvasItem, rect: Rect2, origin: Vect
|
||||
var first_row: int = floori(world_top / CELL_HEIGHT) - 1
|
||||
var last_row: int = floori(world_bottom / CELL_HEIGHT) + 1
|
||||
var remaining: int = MAX_BACKGROUND_CELLS
|
||||
for row: int in range(first_row, last_row + 1):
|
||||
for column: int in range(first_column, last_column + 1):
|
||||
var stride: int = _preview_texture_stride(rect.size, motif_scale, Vector2(CELL_WIDTH, CELL_HEIGHT)) if simplified else 1
|
||||
# Sample the same world cells at a lower density, including in negative coordinates.
|
||||
first_row = floori(float(first_row) / float(stride)) * stride
|
||||
first_column = floori(float(first_column) / float(stride)) * stride
|
||||
for row: int in range(first_row, last_row + 1, stride):
|
||||
for column: int in range(first_column, last_column + 1, stride):
|
||||
if remaining <= 0:
|
||||
return
|
||||
remaining -= 1
|
||||
var seed: int = row * 73856093 + column * 19349663 + 197
|
||||
var seed: int = _pattern_seed(row, column, GRASS_PATTERN_CELLS, 73856093, 19349663, 197)
|
||||
var center_world := Vector2((float(column) + 0.5) * CELL_WIDTH, (float(row) + 0.5) * CELL_HEIGHT)
|
||||
center_world += Vector2((_wood_hash(seed) - 0.5) * 34.0, (_wood_hash(seed + 1) - 0.5) * 26.0)
|
||||
var center := Vector2((center_world.x - origin.x) / motif_scale.x, (center_world.y - origin.y) / motif_scale.y)
|
||||
@@ -403,7 +474,7 @@ static func _draw_background_grass(canvas: CanvasItem, rect: Rect2, origin: Vect
|
||||
var highlight: Color = Color("#C3DA74").darkened(_wood_hash(seed + 3) * 0.24)
|
||||
var radius: float = 1.8 + _wood_hash(seed + 4) * 1.6
|
||||
canvas.draw_circle(center, radius, _color_with_alpha(tuft_color, 0.56))
|
||||
for blade: int in 4:
|
||||
for blade: int in (1 if simplified else 4):
|
||||
var blade_seed: int = seed + blade * 47 + 11
|
||||
var x_offset: float = (_wood_hash(blade_seed) - 0.5) * 9.0 / motif_scale.x
|
||||
var y_offset: float = (_wood_hash(blade_seed + 1) - 0.5) * 7.0 / motif_scale.y
|
||||
@@ -413,7 +484,7 @@ static func _draw_background_grass(canvas: CanvasItem, rect: Rect2, origin: Vect
|
||||
_draw_clipped_line(canvas, rect, start, tip, _color_with_alpha(highlight, 0.78), 1.25 / motif_scale.y)
|
||||
|
||||
|
||||
static func _draw_background_concrete(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2) -> void:
|
||||
static func _draw_background_concrete(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, simplified: bool = false) -> void:
|
||||
const SLAB_WIDTH: float = 280.0
|
||||
const SLAB_HEIGHT: float = 190.0
|
||||
var world_left: float = rect.position.x * motif_scale.x + origin.x
|
||||
@@ -431,11 +502,13 @@ static func _draw_background_concrete(canvas: CanvasItem, rect: Rect2, origin: V
|
||||
var x: float = float(column) * SLAB_WIDTH
|
||||
var local_x: float = (x - origin.x) / motif_scale.x
|
||||
_draw_clipped_line(canvas, rect, Vector2(local_x, rect.position.y), Vector2(local_x, rect.end.y), Color("#68757A", 0.32), column_gap)
|
||||
if simplified:
|
||||
continue
|
||||
for row: int in range(first_row, mini(last_row + 1, first_row + MAX_BACKGROUND_CELLS)):
|
||||
if remaining <= 0:
|
||||
return
|
||||
remaining -= 1
|
||||
var seed: int = column * 92821 + row * 68917 + 51
|
||||
var seed: int = _pattern_seed(row, column, CONCRETE_PATTERN_CELLS, 68917, 92821, 51)
|
||||
var world_point := Vector2(x + 16.0 + _wood_hash(seed) * (SLAB_WIDTH - 32.0), float(row) * SLAB_HEIGHT + 14.0 + _wood_hash(seed + 1) * (SLAB_HEIGHT - 28.0))
|
||||
var point := Vector2((world_point.x - origin.x) / motif_scale.x, (world_point.y - origin.y) / motif_scale.y)
|
||||
if rect.grow(3.0).has_point(point):
|
||||
@@ -447,25 +520,28 @@ static func _draw_background_concrete(canvas: CanvasItem, rect: Rect2, origin: V
|
||||
_draw_clipped_line(canvas, rect, Vector2(rect.position.x, local_y), Vector2(rect.end.x, local_y), Color("#68757A", 0.32), row_gap)
|
||||
|
||||
|
||||
static func _draw_background_carpet(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, tint: Color) -> void:
|
||||
static func _draw_background_carpet(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, tint: Color, simplified: bool = false) -> void:
|
||||
const CELL_WIDTH: float = 34.0
|
||||
const CELL_HEIGHT: float = 26.0
|
||||
var world_left: float = rect.position.x * motif_scale.x + origin.x
|
||||
var world_right: float = rect.end.x * motif_scale.x + origin.x
|
||||
var world_top: float = rect.position.y * motif_scale.y + origin.y
|
||||
var world_bottom: float = rect.end.y * motif_scale.y + origin.y
|
||||
var first_column: int = floori(world_left / CELL_WIDTH)
|
||||
var first_column: int = floori(world_left / CELL_WIDTH) - 1
|
||||
var last_column: int = floori(world_right / CELL_WIDTH)
|
||||
var first_row: int = floori(world_top / CELL_HEIGHT)
|
||||
var last_row: int = floori(world_bottom / CELL_HEIGHT)
|
||||
var remaining: int = MAX_BACKGROUND_CELLS
|
||||
var natural: Color = default_tint(&"finish_background_carpet")
|
||||
for row: int in range(first_row, last_row + 1):
|
||||
for column: int in range(first_column, last_column + 1):
|
||||
var stride: int = _preview_texture_stride(rect.size, motif_scale, Vector2(CELL_WIDTH, CELL_HEIGHT)) if simplified else 1
|
||||
first_row = floori(float(first_row) / float(stride)) * stride
|
||||
first_column = floori(float(first_column) / float(stride)) * stride
|
||||
for row: int in range(first_row, last_row + 1, stride):
|
||||
for column: int in range(first_column, last_column + 1, stride):
|
||||
if remaining <= 0:
|
||||
return
|
||||
remaining -= 1
|
||||
var seed: int = row * 92821 + column * 68917 + 71
|
||||
var seed: int = _pattern_seed(row, column, CARPET_PATTERN_CELLS, 92821, 68917, 71)
|
||||
var cell_left: float = float(column) * CELL_WIDTH
|
||||
var cell_top: float = float(row) * CELL_HEIGHT
|
||||
for fiber: int in 2:
|
||||
@@ -479,7 +555,7 @@ static func _draw_background_carpet(canvas: CanvasItem, rect: Rect2, origin: Vec
|
||||
_draw_clipped_line(canvas, rect, start, finish, _color_with_alpha(fiber_color, 0.42), 0.85 / motif_scale.y)
|
||||
|
||||
|
||||
static func _draw_wall_bricks(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, detail_scale: float, tint: Color) -> void:
|
||||
static func _draw_wall_bricks(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, detail_scale: float, tint: Color, interactive_preview: bool = false) -> void:
|
||||
var brick_world_w: float = WALL_BRICK_WIDTH * detail_scale
|
||||
var brick_world_h: float = WALL_BRICK_HEIGHT * detail_scale
|
||||
var brick_w: float = brick_world_w / motif_scale.x
|
||||
@@ -508,7 +584,7 @@ static func _draw_wall_bricks(canvas: CanvasItem, rect: Rect2, origin: Vector2,
|
||||
var column_count: int = int(ceil(rect.size.x * motif_scale.x / brick_world_w)) + 2
|
||||
for column_offset: int in column_count:
|
||||
var column: int = first_column + column_offset
|
||||
var seed: int = row * 92821 + column * 68917 + 31
|
||||
var seed: int = _pattern_seed(row, column, BRICK_PATTERN_CELLS, 92821, 68917, 31)
|
||||
var brick_world_left: float = float(column) * brick_world_w + stagger_world
|
||||
var brick_local_left: float = (brick_world_left - origin.x) / motif_scale.x
|
||||
var width_variation: float = 0.92 + _wood_hash(seed + 1) * 0.14
|
||||
@@ -526,6 +602,15 @@ static func _draw_wall_bricks(canvas: CanvasItem, rect: Rect2, origin: Vector2,
|
||||
continue
|
||||
if face_rect.end.y < rect.position.y or face_rect.position.y > rect.end.y:
|
||||
continue
|
||||
var color_index: int = int(_wood_hash(seed + 5) * float(palette.size()))
|
||||
var face_color: Color = palette[color_index]
|
||||
if interactive_preview:
|
||||
# Keep the committed pattern's world phase, dimensions, jitter, and palette.
|
||||
# Fine outlines and texture are expensive to regenerate on every drag frame.
|
||||
var visible_face: Rect2 = face_rect.intersection(rect)
|
||||
if visible_face.size.x > 0.0 and visible_face.size.y > 0.0:
|
||||
canvas.draw_rect(visible_face, face_color)
|
||||
continue
|
||||
var roughness := Vector2(1.7 * detail_scale / motif_scale.x, 1.2 * detail_scale / motif_scale.y)
|
||||
var outline: PackedVector2Array = _rough_brick_outline(face_rect, seed, roughness)
|
||||
var shadow_offset := Vector2(1.3 * detail_scale / motif_scale.x, 1.8 * detail_scale / motif_scale.y)
|
||||
@@ -535,8 +620,6 @@ static func _draw_wall_bricks(canvas: CanvasItem, rect: Rect2, origin: Vector2,
|
||||
var face_points: PackedVector2Array = _clip_polygon_to_rect(outline, rect)
|
||||
if face_points.size() < 3:
|
||||
continue
|
||||
var color_index: int = int(_wood_hash(seed + 5) * float(palette.size()))
|
||||
var face_color: Color = palette[color_index]
|
||||
canvas.draw_colored_polygon(face_points, face_color)
|
||||
var closed_outline := face_points.duplicate()
|
||||
closed_outline.append(face_points[0])
|
||||
@@ -628,12 +711,34 @@ static func _offset_polygon(points: PackedVector2Array, offset: Vector2) -> Pack
|
||||
|
||||
|
||||
static func _clip_polygon_to_rect(points: PackedVector2Array, rect: Rect2) -> PackedVector2Array:
|
||||
var fully_inside: bool = true
|
||||
for point: Vector2 in points:
|
||||
if not rect.has_point(point):
|
||||
fully_inside = false
|
||||
break
|
||||
if fully_inside:
|
||||
return points
|
||||
var clipped := points
|
||||
for edge: int in 4:
|
||||
clipped = _clip_polygon_to_edge(clipped, rect, edge)
|
||||
if clipped.is_empty():
|
||||
return clipped
|
||||
return clipped
|
||||
# Clipping at a corner can produce repeated vertices or a zero-area sliver.
|
||||
# These cannot be triangulated, even when the array still has three entries.
|
||||
var cleaned := PackedVector2Array()
|
||||
for point: Vector2 in clipped:
|
||||
if cleaned.is_empty() or cleaned[cleaned.size() - 1].distance_squared_to(point) > 0.000001:
|
||||
cleaned.append(point)
|
||||
if cleaned.size() > 1 and cleaned[0].distance_squared_to(cleaned[cleaned.size() - 1]) <= 0.000001:
|
||||
cleaned.remove_at(cleaned.size() - 1)
|
||||
if cleaned.size() < 3:
|
||||
return PackedVector2Array()
|
||||
var area: float = 0.0
|
||||
for index: int in range(1, cleaned.size() - 1):
|
||||
area += (cleaned[index] - cleaned[0]).cross(cleaned[index + 1] - cleaned[0])
|
||||
if absf(area) <= 0.001 or Geometry2D.triangulate_polygon(cleaned).is_empty():
|
||||
return PackedVector2Array()
|
||||
return cleaned
|
||||
|
||||
|
||||
static func _clip_polygon_to_edge(points: PackedVector2Array, rect: Rect2, edge: int) -> PackedVector2Array:
|
||||
|
||||
@@ -2,7 +2,6 @@ class_name StageWallPanel
|
||||
extends "res://scripts/stage/scenery/stage_rect_surface.gd"
|
||||
## Finite decorative wall panel with world-scale finish motifs.
|
||||
|
||||
const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||
const DRAW_ORDER := preload("res://scripts/stage/stage_draw_order.gd")
|
||||
## Saved source-wall anchor offset, retained to preserve erased-fragment metadata.
|
||||
## Motif drawing uses the fragment's current world origin so its brick courses stay aligned.
|
||||
@@ -23,4 +22,4 @@ func get_surface_draw_origin_world() -> Vector2:
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
SURFACE_RENDERER.draw_wall(self, get_local_bounds(), finish_id, get_render_tint(), get_surface_draw_origin_world())
|
||||
_draw_surface(true)
|
||||
|
||||
Reference in New Issue
Block a user