diff --git a/doc/docs/non_destructive_layers_architecture.md b/doc/docs/non_destructive_layers_architecture.md
new file mode 100644
index 000000000..fd1899abe
--- /dev/null
+++ b/doc/docs/non_destructive_layers_architecture.md
@@ -0,0 +1,591 @@
+# Layer System Architecture
+
+This document describes the non-destructive layer system architecture introduced to Terrain3D, enabling artists to apply procedural and stamp-based modifications to terrain maps without permanently altering the base data.
+
+## Overview
+
+The layer system enables:
+- Non-destructive editing of height, control, and color maps
+- Multiple layer types with different generation strategies (stamps, curves, local nodes)
+- Real-time compositing with caching and dirty-tracking for performance
+- Blending modes (Add, Subtract, Replace) with intensity and feather controls
+- Coverage areas and alpha masks for precise control
+- Layer groups for synchronized multi-region editing
+
+## System Architecture with Layer System Changes
+
+This diagram shows the complete Terrain3D architecture with the **new layer system components highlighted in green**. Components and connections shown in standard colors were part of the original architecture.
+
+```mermaid
+graph TD
+ %% Styling
+ classDef main fill:#00558C,stroke:#333,stroke-width:2px,color:#fff;
+ classDef component fill:#444,stroke:#333,stroke-width:1px,color:#fff;
+ classDef plugin fill:#4B0082,stroke:#333,stroke-width:1px,color:#fff;
+ classDef layerNew fill:#2E7D32,stroke:#1B5E20,stroke-width:3px,color:#fff;
+ classDef resource fill:#666,stroke:#333,stroke-width:1px,color:#fff;
+ %% (Helper nodes removed; external tools now integrate through API only)
+
+ %% Main Nodes
+ T3D[Terrain3D
* Mesh generation
* Collision generation
* Camera snapping]:::main
+
+ T3DM[Terrain3DMaterial
* Saveable resource
* Combines shader snippets
* Exposes custom shader]:::resource
+
+ T3DI[Terrain3DInstancer
* Manages MMIs]:::component
+
+ T3DD[Terrain3DData
* Manages region data
* Creates TextureArrays for maps
* Layer group management]:::component
+
+ T3DA[Terrain3DAssets
* List of assets
* Creates TextureArrays for textures]:::component
+
+ GCM[GeoClipMap
* Creates mesh components]:::component
+
+ T3DR[Terrain3DRegion
* Stores height, control, color maps
* Layer compositing & caching
* Dirty-tracking optimization]:::component
+
+ GT[GeneratedTexture
* Creates TextureArrays
in RenderingServer]:::component
+
+ T3DTA[Terrain3DTextureAsset
* Albedo + Height tex
* Normal + Rough tex
* Texture settings]:::component
+
+ T3DMA[Terrain3DMeshAsset
* Scene File
* Mesh settings]:::component
+
+ T3DE[Terrain3DEditor
* C++ editing functions
* Operates on Data & Instancer
* Undo, redo
* Layer operations]:::component
+
+ EP[EditorPlugin
* GDScript EditorPlugin
* Interacts w/ C++
* Manages UI
* Layer stack UI]:::plugin
+
+ %% NEW Layer System Components
+ T3DL[Terrain3DLayer
* Base layer class
* Payload + alpha mask
* Blending modes
* Coverage areas
* Intensity & feather]:::layerNew
+
+ T3DSL[Terrain3DStampLayer
* Stamp-based layers
* Static payload data]:::layerNew
+
+ T3DCL[Terrain3DCurveLayer
* Curve-based layers
* Procedural generation
* Width & depth control
* Falloff curves]:::layerNew
+
+ T3DLNL[Terrain3DLocalNodeLayer
* Node-based layers
* Transform support]:::layerNew
+
+ MapType[MapType Enum
* TYPE_HEIGHT
* TYPE_CONTROL
* TYPE_COLOR
* Centralized in terrain_3d_map.h]:::layerNew
+
+ %% Original Connections
+ T3D --> T3DM
+ T3D --> T3DI
+ T3D --> T3DD
+ T3D --> T3DA
+ T3D --> GCM
+
+ T3DD --> GCM
+ T3DD --> T3DR
+ T3DD --> GT
+
+ T3DA --> GT
+ T3DA --> T3DTA
+ T3DA --> T3DMA
+
+ EP --> T3DE
+ T3DE --> T3DD
+ T3DE --> T3DI
+
+ %% NEW Layer System Connections (highlighted with thick arrows)
+ T3DR ==>|contains| T3DL
+ T3DL ==>|specializes to| T3DSL
+ T3DL ==>|specializes to| T3DCL
+ T3DL ==>|specializes to| T3DLNL
+
+ T3DE ==>|creates/modifies| T3DL
+ T3DD ==>|manages groups| T3DL
+ EP ==>|UI for| T3DL
+
+ T3DR ==>|composites layers| T3DM
+ T3DL -.->|uses| MapType
+ T3DR -.->|uses| MapType
+ T3DD -.->|uses| MapType
+```
+
+**Key Changes:**
+- **Terrain3DLayer hierarchy** (new): Base class with three specialized layer types
+- **MapType refactoring** (new): Standalone enum in `terrain_3d_map.h` for better organization
+- **Layer compositing** (new): Regions now composite base maps with layers on-demand
+- **External tool bridge** (new): `Terrain3DData.set_map_layer()` + `release_map_layer()` let procedural pipelines push layers safely
+- **Layer group management** (new): Terrain3DData manages group IDs for multi-region coordination
+- **Dirty-tracking optimization** (new): Incremental re-composition of modified areas
+
+## Layer Class Hierarchy
+
+```mermaid
+classDiagram
+ class Terrain3DLayer {
+ <>
+ #MapType _map_type
+ #Rect2i _coverage
+ #Ref~Image~ _payload
+ #Ref~Image~ _alpha
+ #real_t _intensity
+ #real_t _feather_radius
+ #bool _enabled
+ #bool _dirty
+ #BlendMode _blend_mode
+ #uint64_t _group_id
+ #bool _user_editable
+ +apply(Image target, real_t vertex_spacing)
+ +apply_rect(Image target, real_t vertex_spacing, Rect2i rect)
+ +mark_dirty()
+ +needs_rebuild(real_t vertex_spacing) bool
+ #_generate_payload(real_t vertex_spacing)
+ #_compute_feather_weight(Vector2i pixel) real_t
+ }
+
+ class Terrain3DStampLayer {
+ <>
+ +Inherits payload from Terrain3DLayer
+ }
+
+ class Terrain3DCurveLayer {
+ <>
+ -PackedVector3Array _points
+ -real_t _width
+ -real_t _depth
+ -bool _dual_groove
+ -Ref~Curve~ _falloff_curve
+ #_generate_payload(real_t vertex_spacing)
+ -_closest_point_on_polyline(Vector2 point, ...) bool
+ }
+
+ class Terrain3DLocalNodeLayer {
+ <>
+ -NodePath _source_path
+ -Transform3D _local_transform
+ #_generate_payload(real_t vertex_spacing)
+ }
+
+ class BlendMode {
+ <>
+ BLEND_ADD
+ BLEND_SUBTRACT
+ BLEND_REPLACE
+ }
+
+ Terrain3DLayer <|-- Terrain3DStampLayer
+ Terrain3DLayer <|-- Terrain3DCurveLayer
+ Terrain3DLayer <|-- Terrain3DLocalNodeLayer
+ Terrain3DLayer --> BlendMode
+```
+
+## Layer Compositing System
+
+```mermaid
+classDiagram
+ class Terrain3DRegion {
+ -Ref~Image~ _height_map
+ -Ref~Image~ _control_map
+ -Ref~Image~ _color_map
+ -TypedArray~Terrain3DLayer~ _height_layers
+ -TypedArray~Terrain3DLayer~ _control_layers
+ -TypedArray~Terrain3DLayer~ _color_layers
+ -Ref~Image~ _baked_height_map
+ -Ref~Image~ _baked_control_map
+ -Ref~Image~ _baked_color_map
+ -bool _height_layers_dirty
+ -bool _control_layers_dirty
+ -bool _color_layers_dirty
+ -Rect2i _height_dirty_rect
+ -Rect2i _control_dirty_rect
+ -Rect2i _color_dirty_rect
+ +get_composited_map(MapType) Ref~Image~
+ +add_layer(MapType, Terrain3DLayer, int) Terrain3DLayer
+ +remove_layer(MapType, int)
+ +mark_layers_dirty(MapType, bool)
+ +mark_layers_dirty_rect(MapType, Rect2i, bool)
+ -_apply_layers_to_rect(MapType, Image, Rect2i)
+ }
+
+ class Terrain3DData {
+ -Dictionary _regions
+ -TypedArray~Vector2i~ _region_locations
+ -TypedArray~Image~ _height_maps
+ -TypedArray~Image~ _control_maps
+ -TypedArray~Image~ _color_maps
+ -uint64_t _next_layer_group_id
+ +ensure_layer_group_id(Terrain3DLayer) uint64_t
+ +get_layer_groups(MapType) TypedArray~Dictionary~
+ +add_layer_to_coverage(MapType, Terrain3DLayer, Rect2i)
+ +move_layer_coverage(Terrain3DLayer, Vector2i)
+ -_allocate_layer_group_id() uint64_t
+ -_ensure_layer_group_id_internal(Terrain3DLayer) uint64_t
+ -_find_layer_owner(Terrain3DLayer, MapType, ...) bool
+ }
+
+ class Terrain3DEditor {
+ -Ref~Terrain3DLayer~ _active_layer
+ +set_active_layer(Terrain3DLayer)
+ +get_active_layer() Terrain3DLayer
+ +add_stamp_layer(Vector3, real_t, Image, Image, int)
+ +add_curve_layer(PackedVector3Array, real_t, real_t, int)
+ +finalize_stamp_layer(Terrain3DLayer)
+ -_operate_stamp_layer(regions, layer_only)
+ }
+
+ Terrain3DData --> Terrain3DRegion
+ Terrain3DRegion --> Terrain3DLayer
+ Terrain3DEditor --> Terrain3DData
+ Terrain3DEditor --> Terrain3DLayer
+```
+
+## Layer Compositing Workflow
+
+```mermaid
+sequenceDiagram
+ participant Renderer as RenderingServer
+ participant Data as Terrain3DData
+ participant Region as Terrain3DRegion
+ participant Layers as Layer Stack
+ participant Cache as Baked Map Cache
+
+ Note over Renderer: Needs texture update
+ Renderer->>Data: update_maps()
+ Data->>Region: get_composited_map(TYPE_HEIGHT)
+
+ alt Cache valid (not dirty)
+ Region-->>Data: Return cached baked map
+ else Full rebuild needed
+ Region->>Region: cache = base_map.duplicate()
+ loop For each layer
+ Region->>Layers: layer.apply(cache, vertex_spacing)
+ Layers->>Layers: _ensure_payload(vertex_spacing)
+ Layers->>Layers: Apply blend mode to cache
+ end
+ Region->>Cache: Store in _baked_height_map
+ Region->>Region: Mark clean (_height_layers_dirty = false)
+ Region-->>Data: Return baked map
+ else Incremental rebuild (dirty rect)
+ Region->>Region: cache.blit_rect(base_map, dirty_rect)
+ Region->>Region: _apply_layers_to_rect(TYPE_HEIGHT, cache, dirty_rect)
+ loop For each layer
+ Layers->>Layers: layer.apply_rect(cache, vertex_spacing, dirty_rect)
+ end
+ Region->>Region: Mark clean (_height_layers_dirty = false)
+ Region-->>Data: Return updated baked map
+ end
+
+ Data->>Renderer: Update TextureArray with composited map
+```
+
+## Layer Blending Algorithm
+
+```mermaid
+flowchart TD
+ Start([Apply Layer to Target]) --> GetPixel[Get source pixel from payload
Get destination pixel from target]
+ GetPixel --> CalcAlpha[Calculate alpha_weight
from alpha mask if present]
+ CalcAlpha --> CalcFeather[Calculate feather_weight
based on distance from edge]
+ CalcFeather --> CombineWeights[mask_weight = alpha_weight × feather_weight
replace_weight = min mask_weight × intensity, 1
additive_weight = mask_weight × intensity]
+
+ CombineWeights --> CheckBlendMode{Blend Mode?}
+
+ CheckBlendMode -->|BLEND_REPLACE| Replace[dst = lerp dst, payload, replace_weight]
+ CheckBlendMode -->|BLEND_ADD| Add[delta = payload × additive_weight
dst += delta]
+ CheckBlendMode -->|BLEND_SUBTRACT| Subtract[delta = payload × additive_weight
dst -= delta]
+
+ Replace --> WritePixel[Write pixel to target]
+ Add --> WritePixel
+ Subtract --> WritePixel
+ WritePixel --> NextPixel{More pixels
in coverage?}
+
+ NextPixel -->|Yes| GetPixel
+ NextPixel -->|No| End([Done])
+```
+
+## Layer Group System
+
+The layer group system enables multiple regions to share the same layer instance, allowing stamps and curves to span region boundaries seamlessly.
+
+```mermaid
+graph TB
+ subgraph "Terrain3DData"
+ NextID[_next_layer_group_id: 1, 2, 3...]
+ GroupAlloc[_allocate_layer_group_id]
+ end
+
+ subgraph "Region A (0, 0)"
+ LayerA1[StampLayer
group_id: 1
coverage: 100,100 to 200,200]
+ LayerA2[CurveLayer
group_id: 2
coverage: 50,150 to 300,180]
+ end
+
+ subgraph "Region B (1, 0)"
+ LayerB1[StampLayer
group_id: 1
coverage: -156,100 to -56,200]
+ LayerB2[CurveLayer
group_id: 2
coverage: -206,150 to 44,180]
+ end
+
+ subgraph "Region C (0, 1)"
+ LayerC1[StampLayer
group_id: 1
coverage: 100,-156 to 200,-56]
+ end
+
+ NextID --> GroupAlloc
+ GroupAlloc --> LayerA1
+ GroupAlloc --> LayerB1
+ GroupAlloc --> LayerC1
+ GroupAlloc --> LayerA2
+ GroupAlloc --> LayerB2
+
+ LayerA1 -.->|Same group| LayerB1
+ LayerB1 -.->|Same group| LayerC1
+ LayerA2 -.->|Same group| LayerB2
+
+ style LayerA1 fill:#4CAF50
+ style LayerB1 fill:#4CAF50
+ style LayerC1 fill:#4CAF50
+ style LayerA2 fill:#2196F3
+ style LayerB2 fill:#2196F3
+```
+
+## Dirty Tracking & Optimization
+
+```mermaid
+stateDiagram-v2
+ [*] --> Clean: Initial state
+
+ Clean --> FullDirty: Layer added/removed
Base map modified
Layer enabled/disabled
+ Clean --> RectDirty: Layer coverage moved
Layer payload updated
+
+ FullDirty --> Rebuilding: get_composited_map() called
+ RectDirty --> IncrementalRebuild: get_composited_map() called
+
+ Rebuilding --> Clean: Full re-composite complete
Cache updated
+ IncrementalRebuild --> Clean: Dirty rect re-composited
Cache updated
+
+ note right of FullDirty
+ _layers_dirty = true
+ _dirty_rect_valid = false
+ end note
+
+ note right of RectDirty
+ _layers_dirty = true
+ _dirty_rect_valid = true
+ _dirty_rect = affected area
+ end note
+
+ note right of Clean
+ _layers_dirty = false
+ Cache valid for rendering
+ end note
+```
+
+## Data Flow: Layer Application
+
+```mermaid
+flowchart TB
+ subgraph "Input"
+ LayerDef[Layer Definition
coverage, payload, alpha
intensity, feather, blend_mode]
+ end
+
+ subgraph "Payload Generation"
+ CheckCache{Payload
cached?}
+ Generate[_generate_payload
Curve: Rasterize polyline
Stamp: Use provided image
LocalNode: Query node data]
+ Store[Store in _payload]
+ end
+
+ subgraph "Blending"
+ LoadBase[Load base map
or cached composite]
+ IterPixels[For each pixel in coverage]
+ CalcWeights[Calculate alpha × feather weights]
+ BlendPixel[Apply blend mode
Add / Subtract / Replace]
+ WritePixel[Write to target]
+ end
+
+ subgraph "Caching"
+ UpdateCache[Update _baked_map cache]
+ MarkClean[_layers_dirty = false]
+ end
+
+ subgraph "Output"
+ ToRenderer[Send to RenderingServer
as TextureArray]
+ ToCollision[Update collision mesh]
+ ToInstancer[Update instance placement]
+ end
+
+ LayerDef --> CheckCache
+ CheckCache -->|No or dirty| Generate
+ CheckCache -->|Yes| LoadBase
+ Generate --> Store
+ Store --> LoadBase
+
+ LoadBase --> IterPixels
+ IterPixels --> CalcWeights
+ CalcWeights --> BlendPixel
+ BlendPixel --> WritePixel
+ WritePixel --> IterPixels
+
+ IterPixels --> UpdateCache
+ UpdateCache --> MarkClean
+
+ MarkClean --> ToRenderer
+ MarkClean --> ToCollision
+ MarkClean --> ToInstancer
+```
+
+## MapType Refactoring
+
+The `MapType` enum was extracted into a standalone header file for better code organization and to avoid circular dependencies.
+
+```mermaid
+graph LR
+ subgraph "Before: terrain_3d_region.h"
+ OldRegion[Terrain3DRegion class
+ MapType enum]
+ end
+
+ subgraph "After: Separate Files"
+ MapH[terrain_3d_map.h
MapType enum
Helper functions]
+ NewRegion[terrain_3d_region.h
Terrain3DRegion class]
+ Layer[terrain_3d_layer.h
Terrain3DLayer class]
+ Data[terrain_3d_data.h
Terrain3DData class]
+ end
+
+ OldRegion -.->|Refactored into| MapH
+ OldRegion -.->|Refactored into| NewRegion
+
+ NewRegion --> MapH
+ Layer --> MapH
+ Data --> MapH
+
+ style MapH fill:#4CAF50
+```
+
+**terrain_3d_map.h** now provides:
+```cpp
+enum MapType {
+ TYPE_HEIGHT,
+ TYPE_CONTROL,
+ TYPE_COLOR,
+ TYPE_MAX,
+};
+
+inline Image::Format map_type_get_format(MapType p_type);
+inline const char* map_type_get_string(MapType p_type);
+inline Color map_type_get_default_color(MapType p_type);
+```
+
+## Editor Integration
+
+```mermaid
+graph TB
+ subgraph "GDScript UI (ui.gd)"
+ LayerStack[Layer Stack Panel
List of layers per map type]
+ LayerSettings[Layer Settings
Intensity, feather, blend mode]
+ AddLayerBtn[Add Layer Button]
+ end
+
+ subgraph "C++ Editor (terrain_3d_editor.cpp)"
+ ActiveLayer[_active_layer
Currently selected layer]
+ StampOp[_operate_stamp_layer
Paint to active layer]
+ AddStamp[add_stamp_layer
Create new stamp]
+ AddCurve[add_curve_layer
Create new curve]
+ FinalizeStamp[finalize_stamp_layer
Commit to region]
+ end
+
+ subgraph "Data Layer"
+ RegionLayers[Region layer arrays
_height_layers
_control_layers
_color_layers]
+ end
+
+ LayerStack -->|Select layer| ActiveLayer
+ LayerSettings -->|Modify properties| ActiveLayer
+ AddLayerBtn -->|Create stamp| AddStamp
+ AddLayerBtn -->|Create curve| AddCurve
+
+ ActiveLayer -->|Paint stroke| StampOp
+ StampOp -->|Update payload| ActiveLayer
+
+ AddStamp -->|Finalize| FinalizeStamp
+ FinalizeStamp -->|Add to region| RegionLayers
+```
+
+## Key Features
+
+### 1. Non-Destructive Editing
+- Base maps remain unchanged
+- Layers composite on top at render time
+- Easy to enable/disable, reorder, or remove layers
+
+### 2. Layer Types
+- **Stamp Layers**: Static image data stamped onto terrain
+- **Curve Layers**: Procedurally generated from path data
+- **Local Node Layers**: Driven by scene node transforms
+
+### 3. Compositing Performance
+- Incremental dirty-rect updates minimize re-compositing cost
+- Full-map caching for clean layers (no redundant processing)
+- Per-map-type dirty tracking
+
+### 4. Multi-Region Support
+- Layers can span multiple regions via layer groups
+- Group IDs synchronize layers across region boundaries
+- Automatic region creation for stamps/curves that extend beyond loaded tiles
+
+### 5. Blending Controls
+- **Blend Modes**: Add, Subtract, Replace
+- **Intensity**: Scales layer effect strength
+- **Alpha Mask**: Per-pixel layer opacity
+- **Feather**: Smooth edge falloff
+
+### 6. External Tool Hooks
+- `Terrain3DData.set_map_layer()` lets procedural pipelines inject baked images into the layer stack without touching base maps
+- `Terrain3DData.release_map_layer()` recycles IDs or removes stale layers when plugins re-slice data
+- Layer stack UI in the editor still exposes everything for manual editing after external updates
+
+## Performance Characteristics
+
+| Operation | Without Layers | With Layers (Clean) | With Layers (Dirty) |
+|-----------|----------------|---------------------|---------------------|
+| Map access | Direct Image lookup | Cached composite lookup | Re-composite + cache |
+| Incremental edit | Modify base map | Update layer payload | Update layer + dirty rect |
+| Render update | Upload base map | Upload cached composite | Re-composite then upload |
+| Memory overhead | 3 maps × region_size² | + 3 cached composites | Same |
+| Best for | Simple terrain | Complex multi-layer setups | Interactive layer editing |
+
+## Usage Examples
+
+### Creating a Stamp Layer (C++)
+```cpp
+// In Terrain3DEditor
+Ref stamp_image = ...; // Load or generate
+Ref alpha_mask = ...; // Optional
+Vector3 world_pos = Vector3(100, 0, 100);
+real_t radius = 50.0;
+
+add_stamp_layer(world_pos, radius, stamp_image, alpha_mask, Terrain3DRegion::TYPE_HEIGHT);
+// Returns a Terrain3DStampLayer that can be further edited
+```
+
+## External Tool Workflow
+
+Procedural plugins can push their baked results straight into the non-destructive stack without touching the base maps by pairing `set_map_layer()` with the new `release_map_layer()` helper. Each external write should use a deterministic ID per `(region_location, map_type)` so subsequent updates overwrite the exact layer instead of creating new `BLEND_REPLACE` entries.
+
+```gdscript
+var data := terrain.get_data()
+var region := Vector2i(0, 0)
+var payload := Image.create(...)
+var id := hash([region, Terrain3DRegion.TYPE_HEIGHT])
+
+# Create or update a hidden layer owned by the plugin
+data.set_map_layer(region, Terrain3DRegion.TYPE_HEIGHT, payload, id)
+
+# When migrating that ID to a new region or removing its effect
+data.release_map_layer(id, true, true)
+
+# Omit the second argument to keep the layer in-place but free the ID:
+data.release_map_layer(id, false)
+```
+
+`release_map_layer(external_id, remove_layer := true, update := true)` performs two jobs:
+- Drops the cached lookup so the same `external_id` can be reused safely.
+- Optionally removes the stored layer and refreshes the affected region, which prevents abandoned `BLEND_REPLACE` layers from persisting in the editor.
+
+This mirrors how version-control–friendly plugins (for example OldGnuts) can create deterministic slices, update them as inputs change, and explicitly clean up any temporary slices when the generation pass completes.
+
+## Implementation Files
+
+### C++ Core
+- **src/terrain_3d_layer.h/cpp**: Base layer class and specializations
+- **src/terrain_3d_region.h/cpp**: Layer compositing and caching logic
+- **src/terrain_3d_data.h/cpp**: Layer group management
+- **src/terrain_3d_editor.h/cpp**: Layer editing operations
+- **src/terrain_3d_map.h**: MapType enum and utilities
+
+### GDScript Helpers
+- **project/addons/terrain_3d/src/ui.gd**: Layer stack UI integration
+
+### Demo
+- **project/demo/src/LayerDemo.gd**: Example layer usage
\ No newline at end of file
diff --git a/doc/docs/tips_technical.md b/doc/docs/tips_technical.md
index 32c12e560..366646ad9 100644
--- a/doc/docs/tips_technical.md
+++ b/doc/docs/tips_technical.md
@@ -11,7 +11,7 @@ This list are for items that don't already have dedicated pages in the documenta
| GPU Sculpting| [Pending](https://github.com/TokisanGames/Terrain3D/issues/174). Currently painting occurs on the CPU in C++. It's reasonably fast, but we have a soft limit of 200 on the brush size, as larger sizes lag.
| Holes | Holes work for both visual and collision.
| Jolt | [Godot-Jolt](https://github.com/godot-jolt/godot-jolt) was merged into Godot. Terrain3D works with both Godot and Jolt physics. Collision is generated where regions are defined.
-| Non-destructive layers | Used for things like river beds, roads or paths that follow a curve and tweak the terrain. It's [possible](https://github.com/TokisanGames/Terrain3D/issues/129) in the future.
+| Non-destructive layers | Supported via `Terrain3DData.set_map_layer()` (with optional `release_map_layer()` cleanup) so external generators can push fully baked textures into the layer stack without touching the base maps. Use deterministic `external_id` values per `(region, map_type)` to update an existing layer instead of spawning duplicates.
| Object placement | The [instancer](instancer.md) supports placing foliage. Placing objects that shouldn't be in a MultiMeshInstance node is [out of scope](https://github.com/TokisanGames/Terrain3D/issues/47). See 3rd party tools below.
| Streaming | There is no streaming built in to Godot. Region Streaming is [in progress](https://github.com/TokisanGames/Terrain3D/pull/675).
| Roads | Look at [Godot Road Generator](https://github.com/TheDuckCow/godot-road-generator/).
diff --git a/project/addons/terrain_3d/csharp/Terrain3D.cs b/project/addons/terrain_3d/csharp/Terrain3D.cs
index 10418fb3d..ef6dbabb7 100644
--- a/project/addons/terrain_3d/csharp/Terrain3D.cs
+++ b/project/addons/terrain_3d/csharp/Terrain3D.cs
@@ -8,6 +8,7 @@
namespace TokisanGames;
+[Tool]
public partial class Terrain3D : Node3D
{
@@ -28,10 +29,9 @@ protected Terrain3D() { }
/// The existing or a new instance of the wrapper script attached to the supplied .
public new static Terrain3D Bind(GodotObject godotObject)
{
-#if DEBUG
if (!IsInstanceValid(godotObject))
- throw new InvalidOperationException("The supplied GodotObject instance is not valid.");
-#endif
+ return null;
+
if (godotObject is Terrain3D wrapperScriptInstance)
return wrapperScriptInstance;
@@ -150,15 +150,20 @@ public event AssetsChangedSignalHandler AssetsChangedSignal
public new static readonly StringName CollisionMode = "collision_mode";
public new static readonly StringName CollisionShapeSize = "collision_shape_size";
public new static readonly StringName CollisionRadius = "collision_radius";
+ public new static readonly StringName CollisionTarget = "collision_target";
public new static readonly StringName CollisionLayer = "collision_layer";
public new static readonly StringName CollisionMask = "collision_mask";
public new static readonly StringName CollisionPriority = "collision_priority";
public new static readonly StringName PhysicsMaterial = "physics_material";
- public new static readonly StringName CollisionTarget = "collision_target";
+ public new static readonly StringName ClipmapTarget = "clipmap_target";
public new static readonly StringName MeshLods = "mesh_lods";
public new static readonly StringName MeshSize = "mesh_size";
public new static readonly StringName VertexSpacing = "vertex_spacing";
- public new static readonly StringName ClipmapTarget = "clipmap_target";
+ public new static readonly StringName TessellationLevel = "tessellation_level";
+ public new static readonly StringName DisplacementScale = "displacement_scale";
+ public new static readonly StringName DisplacementSharpness = "displacement_sharpness";
+ public new static readonly StringName BufferShaderOverrideEnabled = "buffer_shader_override_enabled";
+ public new static readonly StringName BufferShaderOverride = "buffer_shader_override";
public new static readonly StringName RenderLayers = "render_layers";
public new static readonly StringName MouseLayer = "mouse_layer";
public new static readonly StringName CastShadows = "cast_shadows";
@@ -182,9 +187,12 @@ public event AssetsChangedSignalHandler AssetsChangedSignal
public new static readonly StringName ShowControlScale = "show_control_scale";
public new static readonly StringName ShowColormap = "show_colormap";
public new static readonly StringName ShowRoughmap = "show_roughmap";
+ public new static readonly StringName ShowDisplacementBuffer = "show_displacement_buffer";
+ public new static readonly StringName ShowTextureAlbedo = "show_texture_albedo";
public new static readonly StringName ShowTextureHeight = "show_texture_height";
public new static readonly StringName ShowTextureNormal = "show_texture_normal";
public new static readonly StringName ShowTextureRough = "show_texture_rough";
+ public new static readonly StringName ShowTextureAo = "show_texture_ao";
}
public new string Version
@@ -279,6 +287,12 @@ public event AssetsChangedSignalHandler AssetsChangedSignal
set => Set(GDExtensionPropertyName.CollisionRadius, value);
}
+ public new Node3D CollisionTarget
+ {
+ get => Get(GDExtensionPropertyName.CollisionTarget).As();
+ set => Set(GDExtensionPropertyName.CollisionTarget, value);
+ }
+
public new long CollisionLayer
{
get => Get(GDExtensionPropertyName.CollisionLayer).As();
@@ -303,10 +317,10 @@ public event AssetsChangedSignalHandler AssetsChangedSignal
set => Set(GDExtensionPropertyName.PhysicsMaterial, value);
}
- public new Node3D CollisionTarget
+ public new Node3D ClipmapTarget
{
- get => Get(GDExtensionPropertyName.CollisionTarget).As();
- set => Set(GDExtensionPropertyName.CollisionTarget, value);
+ get => Get(GDExtensionPropertyName.ClipmapTarget).As();
+ set => Set(GDExtensionPropertyName.ClipmapTarget, value);
}
public new long MeshLods
@@ -327,10 +341,34 @@ public event AssetsChangedSignalHandler AssetsChangedSignal
set => Set(GDExtensionPropertyName.VertexSpacing, value);
}
- public new Node3D ClipmapTarget
+ public new long TessellationLevel
{
- get => Get(GDExtensionPropertyName.ClipmapTarget).As();
- set => Set(GDExtensionPropertyName.ClipmapTarget, value);
+ get => Get(GDExtensionPropertyName.TessellationLevel).As();
+ set => Set(GDExtensionPropertyName.TessellationLevel, value);
+ }
+
+ public new double DisplacementScale
+ {
+ get => Get(GDExtensionPropertyName.DisplacementScale).As();
+ set => Set(GDExtensionPropertyName.DisplacementScale, value);
+ }
+
+ public new double DisplacementSharpness
+ {
+ get => Get(GDExtensionPropertyName.DisplacementSharpness).As();
+ set => Set(GDExtensionPropertyName.DisplacementSharpness, value);
+ }
+
+ public new bool BufferShaderOverrideEnabled
+ {
+ get => Get(GDExtensionPropertyName.BufferShaderOverrideEnabled).As();
+ set => Set(GDExtensionPropertyName.BufferShaderOverrideEnabled, value);
+ }
+
+ public new Shader BufferShaderOverride
+ {
+ get => Get(GDExtensionPropertyName.BufferShaderOverride).As();
+ set => Set(GDExtensionPropertyName.BufferShaderOverride, value);
}
public new long RenderLayers
@@ -345,9 +383,9 @@ public event AssetsChangedSignalHandler AssetsChangedSignal
set => Set(GDExtensionPropertyName.MouseLayer, value);
}
- public new Variant CastShadows
+ public new long CastShadows
{
- get => Get(GDExtensionPropertyName.CastShadows).As();
+ get => Get(GDExtensionPropertyName.CastShadows).As();
set => Set(GDExtensionPropertyName.CastShadows, value);
}
@@ -471,6 +509,18 @@ public event AssetsChangedSignalHandler AssetsChangedSignal
set => Set(GDExtensionPropertyName.ShowRoughmap, value);
}
+ public new bool ShowDisplacementBuffer
+ {
+ get => Get(GDExtensionPropertyName.ShowDisplacementBuffer).As();
+ set => Set(GDExtensionPropertyName.ShowDisplacementBuffer, value);
+ }
+
+ public new bool ShowTextureAlbedo
+ {
+ get => Get(GDExtensionPropertyName.ShowTextureAlbedo).As();
+ set => Set(GDExtensionPropertyName.ShowTextureAlbedo, value);
+ }
+
public new bool ShowTextureHeight
{
get => Get(GDExtensionPropertyName.ShowTextureHeight).As();
@@ -489,6 +539,12 @@ public event AssetsChangedSignalHandler AssetsChangedSignal
set => Set(GDExtensionPropertyName.ShowTextureRough, value);
}
+ public new bool ShowTextureAo
+ {
+ get => Get(GDExtensionPropertyName.ShowTextureAo).As();
+ set => Set(GDExtensionPropertyName.ShowTextureAo, value);
+ }
+
public new static class GDExtensionMethodName
{
public new static readonly StringName SetEditor = "set_editor";
diff --git a/project/addons/terrain_3d/csharp/Terrain3DAssets.cs b/project/addons/terrain_3d/csharp/Terrain3DAssets.cs
index 67e98f98d..f3e52c68a 100644
--- a/project/addons/terrain_3d/csharp/Terrain3DAssets.cs
+++ b/project/addons/terrain_3d/csharp/Terrain3DAssets.cs
@@ -8,6 +8,7 @@
namespace TokisanGames;
+[Tool]
public partial class Terrain3DAssets : Resource
{
@@ -28,10 +29,9 @@ protected Terrain3DAssets() { }
/// The existing or a new instance of the wrapper script attached to the supplied .
public new static Terrain3DAssets Bind(GodotObject godotObject)
{
-#if DEBUG
if (!IsInstanceValid(godotObject))
- throw new InvalidOperationException("The supplied GodotObject instance is not valid.");
-#endif
+ return null;
+
if (godotObject is Terrain3DAssets wrapperScriptInstance)
return wrapperScriptInstance;
@@ -148,10 +148,12 @@ public event TexturesChangedSignalHandler TexturesChangedSignal
public new static readonly StringName GetTextureColors = "get_texture_colors";
public new static readonly StringName GetTextureNormalDepths = "get_texture_normal_depths";
public new static readonly StringName GetTextureAoStrengths = "get_texture_ao_strengths";
+ public new static readonly StringName GetTextureAoLightAffects = "get_texture_ao_light_affects";
public new static readonly StringName GetTextureRoughnessMods = "get_texture_roughness_mods";
public new static readonly StringName GetTextureUvScales = "get_texture_uv_scales";
public new static readonly StringName GetTextureVerticalProjections = "get_texture_vertical_projections";
public new static readonly StringName GetTextureDetiles = "get_texture_detiles";
+ public new static readonly StringName GetTextureDisplacements = "get_texture_displacements";
public new static readonly StringName ClearTextures = "clear_textures";
public new static readonly StringName UpdateTextureList = "update_texture_list";
public new static readonly StringName SetMeshAsset = "set_mesh_asset";
@@ -186,6 +188,9 @@ public event TexturesChangedSignalHandler TexturesChangedSignal
public new float[] GetTextureAoStrengths() =>
Call(GDExtensionMethodName.GetTextureAoStrengths, []).As();
+ public new float[] GetTextureAoLightAffects() =>
+ Call(GDExtensionMethodName.GetTextureAoLightAffects, []).As();
+
public new float[] GetTextureRoughnessMods() =>
Call(GDExtensionMethodName.GetTextureRoughnessMods, []).As();
@@ -198,6 +203,9 @@ public event TexturesChangedSignalHandler TexturesChangedSignal
public new Vector2[] GetTextureDetiles() =>
Call(GDExtensionMethodName.GetTextureDetiles, []).As();
+ public new Vector2[] GetTextureDisplacements() =>
+ Call(GDExtensionMethodName.GetTextureDisplacements, []).As();
+
public new void ClearTextures(bool update = false) =>
Call(GDExtensionMethodName.ClearTextures, [update]);
diff --git a/project/addons/terrain_3d/csharp/Terrain3DCollision.cs b/project/addons/terrain_3d/csharp/Terrain3DCollision.cs
index 1aca6d98a..66a196fa4 100644
--- a/project/addons/terrain_3d/csharp/Terrain3DCollision.cs
+++ b/project/addons/terrain_3d/csharp/Terrain3DCollision.cs
@@ -8,6 +8,7 @@
namespace TokisanGames;
+[Tool]
public partial class Terrain3DCollision : GodotObject
{
@@ -28,10 +29,9 @@ protected Terrain3DCollision() { }
/// The existing or a new instance of the wrapper script attached to the supplied .
public new static Terrain3DCollision Bind(GodotObject godotObject)
{
-#if DEBUG
if (!IsInstanceValid(godotObject))
- throw new InvalidOperationException("The supplied GodotObject instance is not valid.");
-#endif
+ return null;
+
if (godotObject is Terrain3DCollision wrapperScriptInstance)
return wrapperScriptInstance;
diff --git a/project/addons/terrain_3d/csharp/Terrain3DData.cs b/project/addons/terrain_3d/csharp/Terrain3DData.cs
index e45e00564..cb84288f4 100644
--- a/project/addons/terrain_3d/csharp/Terrain3DData.cs
+++ b/project/addons/terrain_3d/csharp/Terrain3DData.cs
@@ -8,6 +8,7 @@
namespace TokisanGames;
+[Tool]
public partial class Terrain3DData : GodotObject
{
@@ -28,10 +29,9 @@ protected Terrain3DData() { }
/// The existing or a new instance of the wrapper script attached to the supplied .
public new static Terrain3DData Bind(GodotObject godotObject)
{
-#if DEBUG
if (!IsInstanceValid(godotObject))
- throw new InvalidOperationException("The supplied GodotObject instance is not valid.");
-#endif
+ return null;
+
if (godotObject is Terrain3DData wrapperScriptInstance)
return wrapperScriptInstance;
@@ -280,6 +280,20 @@ public event MapsEditedSignalHandler MapsEditedSignal
public new static readonly StringName LoadDirectory = "load_directory";
public new static readonly StringName LoadRegion = "load_region";
public new static readonly StringName GetMaps = "get_maps";
+ public new static readonly StringName GetLayers = "get_layers";
+ public new static readonly StringName GetLayerGroups = "get_layer_groups";
+ public new static readonly StringName EnsureLayerGroupId = "ensure_layer_group_id";
+ public new static readonly StringName AddLayer = "add_layer";
+ public new static readonly StringName AddStampLayer = "add_stamp_layer";
+ public new static readonly StringName AddStampLayerGlobal = "add_stamp_layer_global";
+ public new static readonly StringName SplitLayerPayloadGlobalData = "split_layer_payload_global_data";
+ public new static readonly StringName GetLayerOwnerInfo = "get_layer_owner_info";
+ public new static readonly StringName SetLayerCoverage = "set_layer_coverage";
+ public new static readonly StringName MoveStampLayer = "move_stamp_layer";
+ public new static readonly StringName SetLayerEnabled = "set_layer_enabled";
+ public new static readonly StringName RemoveLayer = "remove_layer";
+ public new static readonly StringName SetMapLayer = "set_map_layer";
+ public new static readonly StringName ReleaseMapLayer = "release_map_layer";
public new static readonly StringName UpdateMaps = "update_maps";
public new static readonly StringName GetHeightMapsRid = "get_height_maps_rid";
public new static readonly StringName GetControlMapsRid = "get_control_maps_rid";
@@ -406,11 +420,53 @@ public event MapsEditedSignalHandler MapsEditedSignal
public new void LoadRegion(Vector2I regionLocation, string directory, bool update = true) =>
Call(GDExtensionMethodName.LoadRegion, [regionLocation, directory, update]);
- public new Godot.Collections.Array GetMaps(Terrain3DRegion.MapType mapType) =>
- Call(GDExtensionMethodName.GetMaps, [Variant.From(mapType)]).As();
+ public new Godot.Collections.Array GetMaps(long/* "Empty Enum Constant String" */ mapType) =>
+ Call(GDExtensionMethodName.GetMaps, [mapType]).As();
+
+ public new Godot.Collections.Array GetLayers(Vector2I regionLocation, long/* "Empty Enum Constant String" */ mapType) =>
+ Call(GDExtensionMethodName.GetLayers, [regionLocation, mapType]).As();
+
+ public new Godot.Collections.Array GetLayerGroups(long/* "Empty Enum Constant String" */ mapType) =>
+ Call(GDExtensionMethodName.GetLayerGroups, [mapType]).As();
+
+ public new long EnsureLayerGroupId(Terrain3DLayer layer) =>
+ Call(GDExtensionMethodName.EnsureLayerGroupId, [layer]).As();
+
+ public new Terrain3DLayer AddLayer(Vector2I regionLocation, long/* "Empty Enum Constant String" */ mapType, Terrain3DLayer layer, long index = -1, bool update = true) =>
+ Terrain3DLayer.Bind(Call(GDExtensionMethodName.AddLayer, [regionLocation, mapType, layer, index, update]).As());
+
+ public new Terrain3DStampLayer AddStampLayer(Vector2I regionLocation, long/* "Empty Enum Constant String" */ mapType, Image payload, Rect2I coverage, Image alpha = null, double intensity = 1, double featherRadius = 0, Terrain3DLayer.BlendModeEnum blendMode = Terrain3DLayer.BlendModeEnum.Add, long index = -1, bool update = true) =>
+ Terrain3DStampLayer.Bind(Call(GDExtensionMethodName.AddStampLayer, [regionLocation, mapType, payload, coverage, alpha, intensity, featherRadius, Variant.From(blendMode), index, update]).As());
+
+ public new Godot.Collections.Array AddStampLayerGlobal(Rect2I globalCoverage, long/* "Empty Enum Constant String" */ mapType, Image payload, Image alpha = null, double intensity = 1, double featherRadius = 0, Terrain3DLayer.BlendModeEnum blendMode = Terrain3DLayer.BlendModeEnum.Add, bool autoCreateRegions = true, bool update = true) =>
+ Call(GDExtensionMethodName.AddStampLayerGlobal, [globalCoverage, mapType, payload, alpha, intensity, featherRadius, Variant.From(blendMode), autoCreateRegions, update]).As();
+
+ public new Godot.Collections.Array SplitLayerPayloadGlobalData(Rect2I globalCoverage, Image payload, Image alpha = null) =>
+ Call(GDExtensionMethodName.SplitLayerPayloadGlobalData, [globalCoverage, payload, alpha]).As();
+
+ public new Godot.Collections.Dictionary GetLayerOwnerInfo(Terrain3DLayer layer, long/* "Empty Enum Constant String" */ mapType) =>
+ Call(GDExtensionMethodName.GetLayerOwnerInfo, [layer, mapType]).As();
+
+ public new bool SetLayerCoverage(Vector2I regionLocation, long/* "Empty Enum Constant String" */ mapType, long index, Rect2I coverage, bool update = true) =>
+ Call(GDExtensionMethodName.SetLayerCoverage, [regionLocation, mapType, index, coverage, update]).As();
+
+ public new bool MoveStampLayer(Terrain3DStampLayer layer, Vector3 worldPosition, bool update = true) =>
+ Call(GDExtensionMethodName.MoveStampLayer, [layer, worldPosition, update]).As();
+
+ public new void SetLayerEnabled(Vector2I regionLocation, long/* "Empty Enum Constant String" */ mapType, long index, bool enabled, bool update = true) =>
+ Call(GDExtensionMethodName.SetLayerEnabled, [regionLocation, mapType, index, enabled, update]);
+
+ public new void RemoveLayer(Vector2I regionLocation, long/* "Empty Enum Constant String" */ mapType, long index, bool update = true) =>
+ Call(GDExtensionMethodName.RemoveLayer, [regionLocation, mapType, index, update]);
+
+ public new Terrain3DStampLayer SetMapLayer(Vector2I regionLocation, long/* "Empty Enum Constant String" */ mapType, Image image, long externalId = 0, bool update = true) =>
+ Terrain3DStampLayer.Bind(Call(GDExtensionMethodName.SetMapLayer, [regionLocation, mapType, image, externalId, update]).As());
+
+ public new bool ReleaseMapLayer(long externalId, bool removeLayer = true, bool update = true) =>
+ Call(GDExtensionMethodName.ReleaseMapLayer, [externalId, removeLayer, update]).As();
- public new void UpdateMaps(Terrain3DRegion.MapType mapType = Terrain3DRegion.MapType.Max, bool allRegions = true, bool generateMipmaps = false) =>
- Call(GDExtensionMethodName.UpdateMaps, [Variant.From(mapType), allRegions, generateMipmaps]);
+ public new void UpdateMaps(long/* "Empty Enum Constant String" */ mapType = 3, bool allRegions = true, bool generateMipmaps = false) =>
+ Call(GDExtensionMethodName.UpdateMaps, [mapType, allRegions, generateMipmaps]);
public new Rid GetHeightMapsRid() =>
Call(GDExtensionMethodName.GetHeightMapsRid, []).As();
@@ -421,11 +477,11 @@ public event MapsEditedSignalHandler MapsEditedSignal
public new Rid GetColorMapsRid() =>
Call(GDExtensionMethodName.GetColorMapsRid, []).As();
- public new void SetPixel(Terrain3DRegion.MapType mapType, Vector3 globalPosition, Color pixel) =>
- Call(GDExtensionMethodName.SetPixel, [Variant.From(mapType), globalPosition, pixel]);
+ public new void SetPixel(long/* "Empty Enum Constant String" */ mapType, Vector3 globalPosition, Color pixel) =>
+ Call(GDExtensionMethodName.SetPixel, [mapType, globalPosition, pixel]);
- public new Color GetPixel(Terrain3DRegion.MapType mapType, Vector3 globalPosition) =>
- Call(GDExtensionMethodName.GetPixel, [Variant.From(mapType), globalPosition]).As();
+ public new Color GetPixel(long/* "Empty Enum Constant String" */ mapType, Vector3 globalPosition) =>
+ Call(GDExtensionMethodName.GetPixel, [mapType, globalPosition]).As();
public new void SetHeight(Vector3 globalPosition, double height) =>
Call(GDExtensionMethodName.SetHeight, [globalPosition, height]);
@@ -520,11 +576,11 @@ public event MapsEditedSignalHandler MapsEditedSignal
public new void ImportImages(Godot.Collections.Array images, Vector3 globalPosition = default, double offset = 0, double scale = 1) =>
Call(GDExtensionMethodName.ImportImages, [images, globalPosition, offset, scale]);
- public new Error ExportImage(string fileName, Terrain3DRegion.MapType mapType) =>
- Call(GDExtensionMethodName.ExportImage, [fileName, Variant.From(mapType)]).As();
+ public new Error ExportImage(string fileName, long/* "Empty Enum Constant String" */ mapType) =>
+ Call(GDExtensionMethodName.ExportImage, [fileName, mapType]).As();
- public new Image LayeredToImage(Terrain3DRegion.MapType mapType) =>
- Call(GDExtensionMethodName.LayeredToImage, [Variant.From(mapType)]).As();
+ public new Image LayeredToImage(long/* "Empty Enum Constant String" */ mapType) =>
+ Call(GDExtensionMethodName.LayeredToImage, [mapType]).As();
public new void Dump(bool verbose = false) =>
Call(GDExtensionMethodName.Dump, [verbose]);
diff --git a/project/addons/terrain_3d/csharp/Terrain3DEditor.cs b/project/addons/terrain_3d/csharp/Terrain3DEditor.cs
index 20547134e..8796a8df6 100644
--- a/project/addons/terrain_3d/csharp/Terrain3DEditor.cs
+++ b/project/addons/terrain_3d/csharp/Terrain3DEditor.cs
@@ -8,6 +8,7 @@
namespace TokisanGames;
+[Tool]
public partial class Terrain3DEditor : GodotObject
{
@@ -28,10 +29,9 @@ protected Terrain3DEditor() { }
/// The existing or a new instance of the wrapper script attached to the supplied .
public new static Terrain3DEditor Bind(GodotObject godotObject)
{
-#if DEBUG
if (!IsInstanceValid(godotObject))
- throw new InvalidOperationException("The supplied GodotObject instance is not valid.");
-#endif
+ return null;
+
if (godotObject is Terrain3DEditor wrapperScriptInstance)
return wrapperScriptInstance;
@@ -87,6 +87,17 @@ public enum Tool
Max = 12,
}
+ public new static class GDExtensionPropertyName
+ {
+ public new static readonly StringName ActiveLayerIndex = "active_layer_index";
+ }
+
+ public new long ActiveLayerIndex
+ {
+ get => Get(GDExtensionPropertyName.ActiveLayerIndex).As();
+ set => Set(GDExtensionPropertyName.ActiveLayerIndex, value);
+ }
+
public new static class GDExtensionMethodName
{
public new static readonly StringName SetTerrain = "set_terrain";
@@ -101,6 +112,9 @@ public enum Tool
public new static readonly StringName Operate = "operate";
public new static readonly StringName BackupRegion = "backup_region";
public new static readonly StringName StopOperation = "stop_operation";
+ public new static readonly StringName CreateLayer = "create_layer";
+ public new static readonly StringName SetActiveLayerReference = "set_active_layer_reference";
+ public new static readonly StringName GetActiveLayerGroupId = "get_active_layer_group_id";
public new static readonly StringName ApplyUndo = "apply_undo";
}
@@ -140,6 +154,15 @@ public enum Tool
public new void StopOperation() =>
Call(GDExtensionMethodName.StopOperation, []);
+ public new long CreateLayer(Vector2I regionLocation, long/* "Empty Enum Constant String" */ mapType, bool select = true) =>
+ Call(GDExtensionMethodName.CreateLayer, [regionLocation, mapType, select]).As();
+
+ public new void SetActiveLayerReference(Terrain3DLayer layer, long/* "Empty Enum Constant String" */ mapType) =>
+ Call(GDExtensionMethodName.SetActiveLayerReference, [layer, mapType]);
+
+ public new long GetActiveLayerGroupId() =>
+ Call(GDExtensionMethodName.GetActiveLayerGroupId, []).As();
+
public new void ApplyUndo(Godot.Collections.Dictionary data) =>
Call(GDExtensionMethodName.ApplyUndo, [data]);
diff --git a/project/addons/terrain_3d/csharp/Terrain3DInstancer.cs b/project/addons/terrain_3d/csharp/Terrain3DInstancer.cs
index abd7ccda1..87232484f 100644
--- a/project/addons/terrain_3d/csharp/Terrain3DInstancer.cs
+++ b/project/addons/terrain_3d/csharp/Terrain3DInstancer.cs
@@ -8,6 +8,7 @@
namespace TokisanGames;
+[Tool]
public partial class Terrain3DInstancer : GodotObject
{
@@ -28,10 +29,9 @@ protected Terrain3DInstancer() { }
/// The existing or a new instance of the wrapper script attached to the supplied .
public new static Terrain3DInstancer Bind(GodotObject godotObject)
{
-#if DEBUG
if (!IsInstanceValid(godotObject))
- throw new InvalidOperationException("The supplied GodotObject instance is not valid.");
-#endif
+ return null;
+
if (godotObject is Terrain3DInstancer wrapperScriptInstance)
return wrapperScriptInstance;
diff --git a/project/addons/terrain_3d/csharp/Terrain3DLayer.cs b/project/addons/terrain_3d/csharp/Terrain3DLayer.cs
new file mode 100644
index 000000000..3eaf32868
--- /dev/null
+++ b/project/addons/terrain_3d/csharp/Terrain3DLayer.cs
@@ -0,0 +1,152 @@
+#pragma warning disable CS0109
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using Godot;
+using Godot.Collections;
+
+namespace TokisanGames;
+
+[Tool]
+public partial class Terrain3DLayer : Resource
+{
+
+ private new static readonly StringName NativeName = new StringName("Terrain3DLayer");
+
+ [Obsolete("Wrapper types cannot be constructed with constructors (it only instantiate the underlying Terrain3DLayer object), please use the Instantiate() method instead.")]
+ protected Terrain3DLayer() { }
+
+ private static CSharpScript _wrapperScriptAsset;
+
+ ///
+ /// Try to cast the script on the supplied to the wrapper type,
+ /// if no script has attached to the type, or the script attached to the type does not inherit the wrapper type,
+ /// a new instance of the wrapper script will get attaches to the .
+ ///
+ /// The developer should only supply the that represents the correct underlying GDExtension type.
+ /// The that represents the correct underlying GDExtension type.
+ /// The existing or a new instance of the wrapper script attached to the supplied .
+ public new static Terrain3DLayer Bind(GodotObject godotObject)
+ {
+ if (!IsInstanceValid(godotObject))
+ return null;
+
+ if (godotObject is Terrain3DLayer wrapperScriptInstance)
+ return wrapperScriptInstance;
+
+#if DEBUG
+ var expectedType = typeof(Terrain3DLayer);
+ var currentObjectClassName = godotObject.GetClass();
+ if (!ClassDB.IsParentClass(expectedType.Name, currentObjectClassName))
+ throw new InvalidOperationException($"The supplied GodotObject ({currentObjectClassName}) is not the {expectedType.Name} type.");
+#endif
+
+ if (_wrapperScriptAsset is null)
+ {
+ var scriptPathAttribute = typeof(Terrain3DLayer).GetCustomAttributes().FirstOrDefault();
+ if (scriptPathAttribute is null) throw new UnreachableException();
+ _wrapperScriptAsset = ResourceLoader.Load(scriptPathAttribute.Path);
+ }
+
+ var instanceId = godotObject.GetInstanceId();
+ godotObject.SetScript(_wrapperScriptAsset);
+ return (Terrain3DLayer)InstanceFromId(instanceId);
+ }
+
+ ///
+ /// Creates an instance of the GDExtension type, and attaches a wrapper script instance to it.
+ ///
+ /// The wrapper instance linked to the underlying GDExtension "Terrain3DLayer" type.
+ public new static Terrain3DLayer Instantiate() => Bind(ClassDB.Instantiate(NativeName).As());
+
+ public enum BlendModeEnum
+ {
+ Add = 0,
+ Subtract = 1,
+ Replace = 2,
+ }
+
+ public new static class GDExtensionPropertyName
+ {
+ public new static readonly StringName MapType = "map_type";
+ public new static readonly StringName Enabled = "enabled";
+ public new static readonly StringName Intensity = "intensity";
+ public new static readonly StringName FeatherRadius = "feather_radius";
+ public new static readonly StringName BlendMode = "blend_mode";
+ public new static readonly StringName Coverage = "coverage";
+ public new static readonly StringName Payload = "payload";
+ public new static readonly StringName Alpha = "alpha";
+ public new static readonly StringName GroupId = "group_id";
+ public new static readonly StringName UserEditable = "user_editable";
+ }
+
+ public new Variant MapType
+ {
+ get => Get(GDExtensionPropertyName.MapType).As();
+ set => Set(GDExtensionPropertyName.MapType, value);
+ }
+
+ public new bool Enabled
+ {
+ get => Get(GDExtensionPropertyName.Enabled).As();
+ set => Set(GDExtensionPropertyName.Enabled, value);
+ }
+
+ public new double Intensity
+ {
+ get => Get(GDExtensionPropertyName.Intensity).As();
+ set => Set(GDExtensionPropertyName.Intensity, value);
+ }
+
+ public new double FeatherRadius
+ {
+ get => Get(GDExtensionPropertyName.FeatherRadius).As();
+ set => Set(GDExtensionPropertyName.FeatherRadius, value);
+ }
+
+ public new Terrain3DLayer.BlendModeEnum BlendMode
+ {
+ get => Get(GDExtensionPropertyName.BlendMode).As();
+ set => Set(GDExtensionPropertyName.BlendMode, Variant.From(value));
+ }
+
+ public new Rect2I Coverage
+ {
+ get => Get(GDExtensionPropertyName.Coverage).As();
+ set => Set(GDExtensionPropertyName.Coverage, value);
+ }
+
+ public new Image Payload
+ {
+ get => Get(GDExtensionPropertyName.Payload).As();
+ set => Set(GDExtensionPropertyName.Payload, value);
+ }
+
+ public new Image Alpha
+ {
+ get => Get(GDExtensionPropertyName.Alpha).As();
+ set => Set(GDExtensionPropertyName.Alpha, value);
+ }
+
+ public new long GroupId
+ {
+ get => Get(GDExtensionPropertyName.GroupId).As();
+ set => Set(GDExtensionPropertyName.GroupId, value);
+ }
+
+ public new bool UserEditable
+ {
+ get => Get(GDExtensionPropertyName.UserEditable).As();
+ set => Set(GDExtensionPropertyName.UserEditable, value);
+ }
+
+ public new static class GDExtensionMethodName
+ {
+ public new static readonly StringName MarkDirty = "mark_dirty";
+ }
+
+ public new void MarkDirty() =>
+ Call(GDExtensionMethodName.MarkDirty, []);
+
+}
diff --git a/project/addons/terrain_3d/csharp/Terrain3DMaterial.cs b/project/addons/terrain_3d/csharp/Terrain3DMaterial.cs
index e4fa45c44..892afa650 100644
--- a/project/addons/terrain_3d/csharp/Terrain3DMaterial.cs
+++ b/project/addons/terrain_3d/csharp/Terrain3DMaterial.cs
@@ -8,6 +8,7 @@
namespace TokisanGames;
+[Tool]
public partial class Terrain3DMaterial : Resource
{
@@ -28,10 +29,9 @@ protected Terrain3DMaterial() { }
/// The existing or a new instance of the wrapper script attached to the supplied .
public new static Terrain3DMaterial Bind(GodotObject godotObject)
{
-#if DEBUG
if (!IsInstanceValid(godotObject))
- throw new InvalidOperationException("The supplied GodotObject instance is not valid.");
-#endif
+ return null;
+
if (godotObject is Terrain3DMaterial wrapperScriptInstance)
return wrapperScriptInstance;
@@ -75,7 +75,6 @@ public enum TextureFilteringEnum
public new static class GDExtensionPropertyName
{
- public new static readonly StringName ShaderParameters = "_shader_parameters";
public new static readonly StringName WorldBackground = "world_background";
public new static readonly StringName TextureFiltering = "texture_filtering";
public new static readonly StringName AutoShaderEnabled = "auto_shader_enabled";
@@ -87,6 +86,10 @@ public enum TextureFilteringEnum
public new static readonly StringName ShowVertexGrid = "show_vertex_grid";
public new static readonly StringName ShowContours = "show_contours";
public new static readonly StringName ShowNavigation = "show_navigation";
+ public new static readonly StringName DisplacementScale = "displacement_scale";
+ public new static readonly StringName DisplacementSharpness = "displacement_sharpness";
+ public new static readonly StringName BufferShaderOverrideEnabled = "buffer_shader_override_enabled";
+ public new static readonly StringName BufferShaderOverride = "buffer_shader_override";
public new static readonly StringName ShowCheckered = "show_checkered";
public new static readonly StringName ShowGrey = "show_grey";
public new static readonly StringName ShowHeightmap = "show_heightmap";
@@ -98,15 +101,12 @@ public enum TextureFilteringEnum
public new static readonly StringName ShowControlScale = "show_control_scale";
public new static readonly StringName ShowColormap = "show_colormap";
public new static readonly StringName ShowRoughmap = "show_roughmap";
+ public new static readonly StringName ShowTextureAlbedo = "show_texture_albedo";
public new static readonly StringName ShowTextureHeight = "show_texture_height";
public new static readonly StringName ShowTextureNormal = "show_texture_normal";
+ public new static readonly StringName ShowTextureAo = "show_texture_ao";
public new static readonly StringName ShowTextureRough = "show_texture_rough";
- }
-
- public new Godot.Collections.Dictionary ShaderParameters
- {
- get => Get(GDExtensionPropertyName.ShaderParameters).As();
- set => Set(GDExtensionPropertyName.ShaderParameters, value);
+ public new static readonly StringName ShowDisplacementBuffer = "show_displacement_buffer";
}
public new Terrain3DMaterial.WorldBackgroundEnum WorldBackground
@@ -175,6 +175,30 @@ public enum TextureFilteringEnum
set => Set(GDExtensionPropertyName.ShowNavigation, value);
}
+ public new double DisplacementScale
+ {
+ get => Get(GDExtensionPropertyName.DisplacementScale).As();
+ set => Set(GDExtensionPropertyName.DisplacementScale, value);
+ }
+
+ public new double DisplacementSharpness
+ {
+ get => Get(GDExtensionPropertyName.DisplacementSharpness).As();
+ set => Set(GDExtensionPropertyName.DisplacementSharpness, value);
+ }
+
+ public new bool BufferShaderOverrideEnabled
+ {
+ get => Get(GDExtensionPropertyName.BufferShaderOverrideEnabled).As();
+ set => Set(GDExtensionPropertyName.BufferShaderOverrideEnabled, value);
+ }
+
+ public new Variant BufferShaderOverride
+ {
+ get => Get(GDExtensionPropertyName.BufferShaderOverride).As();
+ set => Set(GDExtensionPropertyName.BufferShaderOverride, value);
+ }
+
public new bool ShowCheckered
{
get => Get(GDExtensionPropertyName.ShowCheckered).As();
@@ -241,6 +265,12 @@ public enum TextureFilteringEnum
set => Set(GDExtensionPropertyName.ShowRoughmap, value);
}
+ public new bool ShowTextureAlbedo
+ {
+ get => Get(GDExtensionPropertyName.ShowTextureAlbedo).As();
+ set => Set(GDExtensionPropertyName.ShowTextureAlbedo, value);
+ }
+
public new bool ShowTextureHeight
{
get => Get(GDExtensionPropertyName.ShowTextureHeight).As();
@@ -253,24 +283,38 @@ public enum TextureFilteringEnum
set => Set(GDExtensionPropertyName.ShowTextureNormal, value);
}
+ public new bool ShowTextureAo
+ {
+ get => Get(GDExtensionPropertyName.ShowTextureAo).As();
+ set => Set(GDExtensionPropertyName.ShowTextureAo, value);
+ }
+
public new bool ShowTextureRough
{
get => Get(GDExtensionPropertyName.ShowTextureRough).As();
set => Set(GDExtensionPropertyName.ShowTextureRough, value);
}
+ public new bool ShowDisplacementBuffer
+ {
+ get => Get(GDExtensionPropertyName.ShowDisplacementBuffer).As();
+ set => Set(GDExtensionPropertyName.ShowDisplacementBuffer, value);
+ }
+
public new static class GDExtensionMethodName
{
public new static readonly StringName Update = "update";
public new static readonly StringName GetMaterialRid = "get_material_rid";
public new static readonly StringName GetShaderRid = "get_shader_rid";
+ public new static readonly StringName GetBufferMaterialRid = "get_buffer_material_rid";
+ public new static readonly StringName GetBufferShaderRid = "get_buffer_shader_rid";
public new static readonly StringName SetShaderParam = "set_shader_param";
public new static readonly StringName GetShaderParam = "get_shader_param";
public new static readonly StringName Save = "save";
}
- public new void Update() =>
- Call(GDExtensionMethodName.Update, []);
+ public new void Update(bool full = false) =>
+ Call(GDExtensionMethodName.Update, [full]);
public new Rid GetMaterialRid() =>
Call(GDExtensionMethodName.GetMaterialRid, []).As();
@@ -278,6 +322,12 @@ public enum TextureFilteringEnum
public new Rid GetShaderRid() =>
Call(GDExtensionMethodName.GetShaderRid, []).As();
+ public new Rid GetBufferMaterialRid() =>
+ Call(GDExtensionMethodName.GetBufferMaterialRid, []).As();
+
+ public new Rid GetBufferShaderRid() =>
+ Call(GDExtensionMethodName.GetBufferShaderRid, []).As();
+
public new void SetShaderParam(StringName name, Variant value) =>
Call(GDExtensionMethodName.SetShaderParam, [name, value]);
diff --git a/project/addons/terrain_3d/csharp/Terrain3DMeshAsset.cs b/project/addons/terrain_3d/csharp/Terrain3DMeshAsset.cs
index 989b5ea58..02dd9d61a 100644
--- a/project/addons/terrain_3d/csharp/Terrain3DMeshAsset.cs
+++ b/project/addons/terrain_3d/csharp/Terrain3DMeshAsset.cs
@@ -8,6 +8,7 @@
namespace TokisanGames;
+[Tool]
public partial class Terrain3DMeshAsset : Resource
{
@@ -28,10 +29,9 @@ protected Terrain3DMeshAsset() { }
/// The existing or a new instance of the wrapper script attached to the supplied .
public new static Terrain3DMeshAsset Bind(GodotObject godotObject)
{
-#if DEBUG
if (!IsInstanceValid(godotObject))
- throw new InvalidOperationException("The supplied GodotObject instance is not valid.");
-#endif
+ return null;
+
if (godotObject is Terrain3DMeshAsset wrapperScriptInstance)
return wrapperScriptInstance;
@@ -245,9 +245,9 @@ public event InstanceCountChangedSignalHandler InstanceCountChangedSignal
set => Set(GDExtensionPropertyName.Density, value);
}
- public new Variant CastShadows
+ public new long CastShadows
{
- get => Get(GDExtensionPropertyName.CastShadows).As();
+ get => Get(GDExtensionPropertyName.CastShadows).As();
set => Set(GDExtensionPropertyName.CastShadows, value);
}
diff --git a/project/addons/terrain_3d/csharp/Terrain3DRegion.cs b/project/addons/terrain_3d/csharp/Terrain3DRegion.cs
index 56d26f73e..a67b8333d 100644
--- a/project/addons/terrain_3d/csharp/Terrain3DRegion.cs
+++ b/project/addons/terrain_3d/csharp/Terrain3DRegion.cs
@@ -8,6 +8,7 @@
namespace TokisanGames;
+[Tool]
public partial class Terrain3DRegion : Resource
{
@@ -28,10 +29,9 @@ protected Terrain3DRegion() { }
/// The existing or a new instance of the wrapper script attached to the supplied .
public new static Terrain3DRegion Bind(GodotObject godotObject)
{
-#if DEBUG
if (!IsInstanceValid(godotObject))
- throw new InvalidOperationException("The supplied GodotObject instance is not valid.");
-#endif
+ return null;
+
if (godotObject is Terrain3DRegion wrapperScriptInstance)
return wrapperScriptInstance;
@@ -77,6 +77,9 @@ public enum MapType
public new static readonly StringName HeightMap = "height_map";
public new static readonly StringName ControlMap = "control_map";
public new static readonly StringName ColorMap = "color_map";
+ public new static readonly StringName HeightLayers = "height_layers";
+ public new static readonly StringName ControlLayers = "control_layers";
+ public new static readonly StringName ColorLayers = "color_layers";
public new static readonly StringName Instances = "instances";
public new static readonly StringName Edited = "edited";
public new static readonly StringName Deleted = "deleted";
@@ -126,6 +129,24 @@ public enum MapType
set => Set(GDExtensionPropertyName.ColorMap, value);
}
+ public new Godot.Collections.Array HeightLayers
+ {
+ get => Get(GDExtensionPropertyName.HeightLayers).As();
+ set => Set(GDExtensionPropertyName.HeightLayers, value);
+ }
+
+ public new Godot.Collections.Array ControlLayers
+ {
+ get => Get(GDExtensionPropertyName.ControlLayers).As();
+ set => Set(GDExtensionPropertyName.ControlLayers, value);
+ }
+
+ public new Godot.Collections.Array ColorLayers
+ {
+ get => Get(GDExtensionPropertyName.ColorLayers).As();
+ set => Set(GDExtensionPropertyName.ColorLayers, value);
+ }
+
public new Godot.Collections.Dictionary Instances
{
get => Get(GDExtensionPropertyName.Instances).As();
@@ -162,6 +183,13 @@ public enum MapType
public new static readonly StringName GetMap = "get_map";
public new static readonly StringName SetMaps = "set_maps";
public new static readonly StringName GetMaps = "get_maps";
+ public new static readonly StringName GetLayers = "get_layers";
+ public new static readonly StringName SetLayers = "set_layers";
+ public new static readonly StringName MarkLayersDirty = "mark_layers_dirty";
+ public new static readonly StringName AddLayer = "add_layer";
+ public new static readonly StringName RemoveLayer = "remove_layer";
+ public new static readonly StringName ClearLayers = "clear_layers";
+ public new static readonly StringName GetCompositedMap = "get_composited_map";
public new static readonly StringName SanitizeMaps = "sanitize_maps";
public new static readonly StringName SanitizeMap = "sanitize_map";
public new static readonly StringName ValidateMapSize = "validate_map_size";
@@ -175,11 +203,11 @@ public enum MapType
public new static readonly StringName Dump = "dump";
}
- public new void SetMap(Terrain3DRegion.MapType mapType, Image map) =>
- Call(GDExtensionMethodName.SetMap, [Variant.From(mapType), map]);
+ public new void SetMap(long/* "Empty Enum Constant String" */ mapType, Image map) =>
+ Call(GDExtensionMethodName.SetMap, [mapType, map]);
- public new Image GetMap(Terrain3DRegion.MapType mapType) =>
- Call(GDExtensionMethodName.GetMap, [Variant.From(mapType)]).As();
+ public new Image GetMap(long/* "Empty Enum Constant String" */ mapType) =>
+ Call(GDExtensionMethodName.GetMap, [mapType]).As();
public new void SetMaps(Godot.Collections.Array maps) =>
Call(GDExtensionMethodName.SetMaps, [maps]);
@@ -187,11 +215,32 @@ public enum MapType
public new Godot.Collections.Array GetMaps() =>
Call(GDExtensionMethodName.GetMaps, []).As();
+ public new Godot.Collections.Array GetLayers(long/* "Empty Enum Constant String" */ mapType) =>
+ Call(GDExtensionMethodName.GetLayers, [mapType]).As();
+
+ public new void SetLayers(long/* "Empty Enum Constant String" */ mapType, Godot.Collections.Array layers) =>
+ Call(GDExtensionMethodName.SetLayers, [mapType, layers]);
+
+ public new void MarkLayersDirty(long/* "Empty Enum Constant String" */ mapType, bool markModified = true) =>
+ Call(GDExtensionMethodName.MarkLayersDirty, [mapType, markModified]);
+
+ public new Terrain3DLayer AddLayer(long/* "Empty Enum Constant String" */ mapType, Terrain3DLayer layer, long index = -1) =>
+ Terrain3DLayer.Bind(Call(GDExtensionMethodName.AddLayer, [mapType, layer, index]).As());
+
+ public new void RemoveLayer(long/* "Empty Enum Constant String" */ mapType, long index) =>
+ Call(GDExtensionMethodName.RemoveLayer, [mapType, index]);
+
+ public new void ClearLayers(long/* "Empty Enum Constant String" */ mapType) =>
+ Call(GDExtensionMethodName.ClearLayers, [mapType]);
+
+ public new Image GetCompositedMap(long/* "Empty Enum Constant String" */ mapType) =>
+ Call(GDExtensionMethodName.GetCompositedMap, [mapType]).As();
+
public new void SanitizeMaps() =>
Call(GDExtensionMethodName.SanitizeMaps, []);
- public new Image SanitizeMap(Terrain3DRegion.MapType mapType, Image map) =>
- Call(GDExtensionMethodName.SanitizeMap, [Variant.From(mapType), map]).As();
+ public new Image SanitizeMap(long/* "Empty Enum Constant String" */ mapType, Image map) =>
+ Call(GDExtensionMethodName.SanitizeMap, [mapType, map]).As();
public new bool ValidateMapSize(Image map) =>
Call(GDExtensionMethodName.ValidateMapSize, [map]).As();
diff --git a/project/addons/terrain_3d/csharp/Terrain3DStampLayer.cs b/project/addons/terrain_3d/csharp/Terrain3DStampLayer.cs
new file mode 100644
index 000000000..b12a4f590
--- /dev/null
+++ b/project/addons/terrain_3d/csharp/Terrain3DStampLayer.cs
@@ -0,0 +1,63 @@
+#pragma warning disable CS0109
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using Godot;
+using Godot.Collections;
+
+namespace TokisanGames;
+
+[Tool]
+public partial class Terrain3DStampLayer : Terrain3DLayer
+{
+
+ private new static readonly StringName NativeName = new StringName("Terrain3DStampLayer");
+
+ [Obsolete("Wrapper types cannot be constructed with constructors (it only instantiate the underlying Terrain3DStampLayer object), please use the Instantiate() method instead.")]
+ protected Terrain3DStampLayer() { }
+
+ private static CSharpScript _wrapperScriptAsset;
+
+ ///
+ /// Try to cast the script on the supplied to the wrapper type,
+ /// if no script has attached to the type, or the script attached to the type does not inherit the wrapper type,
+ /// a new instance of the wrapper script will get attaches to the .
+ ///
+ /// The developer should only supply the that represents the correct underlying GDExtension type.
+ /// The that represents the correct underlying GDExtension type.
+ /// The existing or a new instance of the wrapper script attached to the supplied .
+ public new static Terrain3DStampLayer Bind(GodotObject godotObject)
+ {
+ if (!IsInstanceValid(godotObject))
+ return null;
+
+ if (godotObject is Terrain3DStampLayer wrapperScriptInstance)
+ return wrapperScriptInstance;
+
+#if DEBUG
+ var expectedType = typeof(Terrain3DStampLayer);
+ var currentObjectClassName = godotObject.GetClass();
+ if (!ClassDB.IsParentClass(expectedType.Name, currentObjectClassName))
+ throw new InvalidOperationException($"The supplied GodotObject ({currentObjectClassName}) is not the {expectedType.Name} type.");
+#endif
+
+ if (_wrapperScriptAsset is null)
+ {
+ var scriptPathAttribute = typeof(Terrain3DStampLayer).GetCustomAttributes().FirstOrDefault();
+ if (scriptPathAttribute is null) throw new UnreachableException();
+ _wrapperScriptAsset = ResourceLoader.Load(scriptPathAttribute.Path);
+ }
+
+ var instanceId = godotObject.GetInstanceId();
+ godotObject.SetScript(_wrapperScriptAsset);
+ return (Terrain3DStampLayer)InstanceFromId(instanceId);
+ }
+
+ ///
+ /// Creates an instance of the GDExtension type, and attaches a wrapper script instance to it.
+ ///
+ /// The wrapper instance linked to the underlying GDExtension "Terrain3DStampLayer" type.
+ public new static Terrain3DStampLayer Instantiate() => Bind(ClassDB.Instantiate(NativeName).As());
+
+}
diff --git a/project/addons/terrain_3d/csharp/Terrain3DTextureAsset.cs b/project/addons/terrain_3d/csharp/Terrain3DTextureAsset.cs
index c90a17895..7dfe3039a 100644
--- a/project/addons/terrain_3d/csharp/Terrain3DTextureAsset.cs
+++ b/project/addons/terrain_3d/csharp/Terrain3DTextureAsset.cs
@@ -8,6 +8,7 @@
namespace TokisanGames;
+[Tool]
public partial class Terrain3DTextureAsset : Resource
{
@@ -28,10 +29,9 @@ protected Terrain3DTextureAsset() { }
/// The existing or a new instance of the wrapper script attached to the supplied .
public new static Terrain3DTextureAsset Bind(GodotObject godotObject)
{
-#if DEBUG
if (!IsInstanceValid(godotObject))
- throw new InvalidOperationException("The supplied GodotObject instance is not valid.");
-#endif
+ return null;
+
if (godotObject is Terrain3DTextureAsset wrapperScriptInstance)
return wrapperScriptInstance;
@@ -148,7 +148,10 @@ public event SettingChangedSignalHandler SettingChangedSignal
public new static readonly StringName NormalTexture = "normal_texture";
public new static readonly StringName NormalDepth = "normal_depth";
public new static readonly StringName AoStrength = "ao_strength";
+ public new static readonly StringName AoLightAffect = "ao_light_affect";
public new static readonly StringName Roughness = "roughness";
+ public new static readonly StringName DisplacementScale = "displacement_scale";
+ public new static readonly StringName DisplacementOffset = "displacement_offset";
public new static readonly StringName UvScale = "uv_scale";
public new static readonly StringName VerticalProjection = "vertical_projection";
public new static readonly StringName DetilingRotation = "detiling_rotation";
@@ -197,12 +200,30 @@ public event SettingChangedSignalHandler SettingChangedSignal
set => Set(GDExtensionPropertyName.AoStrength, value);
}
+ public new double AoLightAffect
+ {
+ get => Get(GDExtensionPropertyName.AoLightAffect).As();
+ set => Set(GDExtensionPropertyName.AoLightAffect, value);
+ }
+
public new double Roughness
{
get => Get(GDExtensionPropertyName.Roughness).As();
set => Set(GDExtensionPropertyName.Roughness, value);
}
+ public new double DisplacementScale
+ {
+ get => Get(GDExtensionPropertyName.DisplacementScale).As();
+ set => Set(GDExtensionPropertyName.DisplacementScale, value);
+ }
+
+ public new double DisplacementOffset
+ {
+ get => Get(GDExtensionPropertyName.DisplacementOffset).As();
+ set => Set(GDExtensionPropertyName.DisplacementOffset, value);
+ }
+
public new double UvScale
{
get => Get(GDExtensionPropertyName.UvScale).As();
diff --git a/project/addons/terrain_3d/csharp/Terrain3DUtil.cs b/project/addons/terrain_3d/csharp/Terrain3DUtil.cs
index b43340100..b68305be2 100644
--- a/project/addons/terrain_3d/csharp/Terrain3DUtil.cs
+++ b/project/addons/terrain_3d/csharp/Terrain3DUtil.cs
@@ -8,6 +8,7 @@
namespace TokisanGames;
+[Tool]
public partial class Terrain3DUtil : GodotObject
{
@@ -28,10 +29,9 @@ protected Terrain3DUtil() { }
/// The existing or a new instance of the wrapper script attached to the supplied .
public new static Terrain3DUtil Bind(GodotObject godotObject)
{
-#if DEBUG
if (!IsInstanceValid(godotObject))
- throw new InvalidOperationException("The supplied GodotObject instance is not valid.");
-#endif
+ return null;
+
if (godotObject is Terrain3DUtil wrapperScriptInstance)
return wrapperScriptInstance;
@@ -166,8 +166,8 @@ protected Terrain3DUtil() { }
public new static Image LoadImage(string fileName, long cacheMode = 0, Vector2 r16HeightRange = default, Vector2I r16Size = default) =>
ClassDB.ClassCallStatic(NativeName, GDExtensionMethodName.LoadImage, [fileName, cacheMode, r16HeightRange, r16Size]).As();
- public new static Image PackImage(Image srcRgb, Image srcA, bool invertGreen = false, bool invertAlpha = false, bool normalizeAlpha = false, long alphaChannel = 0) =>
- ClassDB.ClassCallStatic(NativeName, GDExtensionMethodName.PackImage, [srcRgb, srcA, invertGreen, invertAlpha, normalizeAlpha, alphaChannel]).As();
+ public new static Image PackImage(Image srcRgb, Image srcA, Image srcAo, bool invertGreen = false, bool invertAlpha = false, bool normalizeAlpha = false, long alphaChannel = 0, long aoChannel = 0) =>
+ ClassDB.ClassCallStatic(NativeName, GDExtensionMethodName.PackImage, [srcRgb, srcA, srcAo, invertGreen, invertAlpha, normalizeAlpha, alphaChannel, aoChannel]).As();
public new static Image LuminanceToHeight(Image srcRgb) =>
ClassDB.ClassCallStatic(NativeName, GDExtensionMethodName.LuminanceToHeight, [srcRgb]).As();
diff --git a/project/addons/terrain_3d/src/editor_plugin.gd b/project/addons/terrain_3d/src/editor_plugin.gd
index 51196ca7b..5e8e93233 100644
--- a/project/addons/terrain_3d/src/editor_plugin.gd
+++ b/project/addons/terrain_3d/src/editor_plugin.gd
@@ -221,6 +221,7 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) ->
if current_region_position != region_position:
current_region_position = region_position
update_region_grid()
+ ui.update_layer_panel()
if _input_mode > 0 and editor.is_operating():
# Inject pressure - Relies on C++ set_brush_data() using same dictionary instance
@@ -266,6 +267,7 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) ->
# _input_apply released, save undo data
elif editor.is_operating():
editor.stop_operation()
+ ui.update_layer_panel()
return AFTER_GUI_INPUT_STOP
return AFTER_GUI_INPUT_PASS
@@ -398,6 +400,7 @@ func update_region_grid() -> void:
region_gizmo.grid = terrain.get_data().get_region_locations()
terrain.update_gizmos()
+ ui.update_layer_panel()
return
region_gizmo.show_rect = false
diff --git a/project/addons/terrain_3d/src/tool_settings.gd b/project/addons/terrain_3d/src/tool_settings.gd
index 0daf68a09..2800afd56 100644
--- a/project/addons/terrain_3d/src/tool_settings.gd
+++ b/project/addons/terrain_3d/src/tool_settings.gd
@@ -27,6 +27,12 @@ const MultiPicker: Script = preload("res://addons/terrain_3d/src/multi_picker.gd
const DEFAULT_BRUSH: String = "circle0.exr"
const BRUSH_PATH: String = "res://addons/terrain_3d/brushes"
const ES_TOOL_SETTINGS: String = "terrain3d/tool_settings/"
+const LAYER_BLEND_LABELS := ["Add", "Subtract", "Replace"]
+const MAP_TYPE_LABELS := {
+ Terrain3DRegion.TYPE_HEIGHT: "Height",
+ Terrain3DRegion.TYPE_CONTROL: "Control",
+ Terrain3DRegion.TYPE_COLOR: "Color"
+}
# Add settings flags
const NONE: int = 0x0
@@ -50,6 +56,20 @@ var rotation_list: VBoxContainer
var color_list: VBoxContainer
var collision_list: VBoxContainer
var settings: Dictionary = {}
+var layer_section: VBoxContainer
+var layers_list: VBoxContainer
+var layer_header_label: Label
+var layer_refresh_button: Button
+var layer_add_button: Button
+var layer_locked_filter_button: CheckBox
+var current_layer_region: Vector2i = Vector2i.ZERO
+var current_layer_map_type: int = Terrain3DRegion.TYPE_MAX
+var layer_selection_group: ButtonGroup
+var _layer_entries: Array = [] ## Array[Dictionary] storing group_id, representative layer, and slice metadata
+var _show_locked_layers: bool = false
+var _selected_layer_groups: Dictionary = {}
+var _layer_row_panels: Dictionary = {}
+var _selected_row_style: StyleBoxFlat
func _ready() -> void:
@@ -187,6 +207,45 @@ func _ready() -> void:
add_setting({ "name":"gamma", "type":SettingType.SLIDER, "list":advanced_list, "default":1.0,
"unit":"γ", "range":Vector3(0.1, 2.0, 0.01) })
+ advanced_list.add_child(HSeparator.new(), true)
+ layer_section = VBoxContainer.new()
+ layer_section.visible = false
+ advanced_list.add_child(layer_section, true)
+
+ var header := HBoxContainer.new()
+ header.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ layer_section.add_child(header, true)
+
+ layer_header_label = Label.new()
+ layer_header_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ layer_header_label.text = "Layers"
+ header.add_child(layer_header_label, true)
+
+ layer_locked_filter_button = CheckBox.new()
+ layer_locked_filter_button.focus_mode = Control.FOCUS_NONE
+ layer_locked_filter_button.text = "Show locked"
+ layer_locked_filter_button.tooltip_text = "Toggle visibility of locked layers"
+ layer_locked_filter_button.button_pressed = _show_locked_layers
+ layer_locked_filter_button.toggled.connect(_on_locked_filter_toggled)
+ header.add_child(layer_locked_filter_button, true)
+
+ layer_refresh_button = Button.new()
+ layer_refresh_button.icon = get_theme_icon("Reload", "EditorIcons")
+ layer_refresh_button.tooltip_text = "Refresh the layer list using the current brush position"
+ layer_refresh_button.pressed.connect(_on_refresh_layers)
+ header.add_child(layer_refresh_button, true)
+
+ layer_add_button = Button.new()
+ layer_add_button.icon = get_theme_icon("Add", "EditorIcons")
+ layer_add_button.tooltip_text = "Create a new layer at the current brush position"
+ layer_add_button.focus_mode = Control.FOCUS_NONE
+ layer_add_button.pressed.connect(_on_add_layer)
+ header.add_child(layer_add_button, true)
+
+ layers_list = VBoxContainer.new()
+ layers_list.add_theme_constant_override("separation", 4)
+ layer_section.add_child(layers_list, true)
+
func create_submenu(p_parent: Control, p_button_name: String, p_layout: Layout, p_hover_pop: bool = true) -> Container:
var menu_button: Button = Button.new()
@@ -610,6 +669,444 @@ func get_setting(p_setting: String) -> Variant:
value = 0
return value
+func update_layer_stack(region_loc: Vector2i, map_type: int, layer_entries: Array) -> void:
+ if not layer_section or not layers_list or not layer_header_label or not layer_add_button:
+ return
+ var previous_map_type := current_layer_map_type
+ current_layer_region = region_loc
+ current_layer_map_type = map_type
+ if previous_map_type != map_type:
+ _selected_layer_groups.clear()
+ _layer_entries = layer_entries.duplicate(true)
+ _layer_row_panels.clear()
+ for child in layers_list.get_children():
+ child.queue_free()
+ if map_type == Terrain3DRegion.TYPE_MAX:
+ layer_section.visible = false
+ layer_header_label.text = "Layers"
+ layer_add_button.disabled = true
+ _selected_layer_groups.clear()
+ return
+ layer_section.visible = true
+ layer_selection_group = ButtonGroup.new()
+ if layer_locked_filter_button:
+ layer_locked_filter_button.button_pressed = _show_locked_layers
+ var active_index: int = 0
+ if plugin and plugin.editor and plugin.editor.has_method("get_active_layer_index"):
+ active_index = plugin.editor.get_active_layer_index()
+ if active_index > 0:
+ var preview_entry_index := active_index - 1
+ if preview_entry_index >= 0 and preview_entry_index < _layer_entries.size():
+ var preview_entry: Dictionary = _layer_entries[preview_entry_index]
+ if not bool(preview_entry.get("user_editable", true)):
+ active_index = 0
+ if plugin and plugin.editor and plugin.editor.has_method("set_active_layer_index"):
+ plugin.editor.set_active_layer_index(0)
+ var group_count := _layer_entries.size()
+ var region_count := _count_unique_regions()
+ var slice_count := _count_total_slices()
+ var locked_group_count := 0
+ for entry in _layer_entries:
+ if not bool(entry.get("user_editable", true)):
+ locked_group_count += 1
+ var label_prefix: String = MAP_TYPE_LABELS.get(map_type, "Map")
+ var group_suffix := "" if group_count == 1 else "s"
+ var region_suffix := "" if region_count == 1 else "s"
+ var slice_suffix := "" if slice_count == 1 else "s"
+ var header_text := "%s Layers (%d group%s, %d region%s, %d slice%s)" % [label_prefix, group_count, group_suffix, region_count, region_suffix, slice_count, slice_suffix]
+ if locked_group_count > 0 and not _show_locked_layers:
+ header_text += " • %d locked hidden" % locked_group_count
+ layer_header_label.text = header_text
+ _add_base_layer_entry(active_index)
+ var visible_entry_count := 0
+ for entry_id in range(_layer_entries.size()):
+ var entry: Dictionary = _layer_entries[entry_id]
+ var layer: Terrain3DLayer = entry.get("layer")
+ if layer == null:
+ continue
+ var ui_index := entry_id + 1
+ entry["ui_index"] = ui_index
+ var group_id := int(entry.get("group_id", 0))
+ var locked := not bool(entry.get("user_editable", true))
+ if locked and not _show_locked_layers:
+ _selected_layer_groups.erase(group_id)
+ continue
+ visible_entry_count += 1
+ var row_panel := PanelContainer.new()
+ row_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ var row := HBoxContainer.new()
+ row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ row_panel.add_child(row, true)
+ layers_list.add_child(row_panel, true)
+ _layer_row_panels[entry_id] = row_panel
+ var selection_toggle := CheckBox.new()
+ selection_toggle.focus_mode = Control.FOCUS_NONE
+ selection_toggle.tooltip_text = "Select this layer for batch actions"
+ selection_toggle.button_pressed = _is_group_selected(group_id)
+ selection_toggle.toggled.connect(_on_layer_row_selection_toggled.bind(entry_id))
+ row.add_child(selection_toggle)
+ var label_text := _describe_layer(entry)
+ var tooltip := _describe_layer_regions(entry)
+ var can_select := bool(entry.get("user_editable", true))
+ var select_button := _create_layer_select_button(ui_index, label_text, active_index, entry_id, tooltip, can_select)
+ row.add_child(select_button)
+ var toggle := CheckBox.new()
+ toggle.focus_mode = Control.FOCUS_NONE
+ toggle.button_pressed = layer.is_enabled()
+ toggle.tooltip_text = "Enable or disable this layer"
+ toggle.toggled.connect(_on_layer_toggle.bind(entry_id))
+ row.add_child(toggle)
+ var remove_button := Button.new()
+ remove_button.focus_mode = Control.FOCUS_NONE
+ remove_button.icon = get_theme_icon("Remove", "EditorIcons")
+ remove_button.tooltip_text = "Remove this layer"
+ remove_button.pressed.connect(_on_layer_remove.bind(entry_id))
+ row.add_child(remove_button)
+ _apply_row_selection_style(entry_id)
+ if visible_entry_count == 0:
+ var empty_label := Label.new()
+ empty_label.autowrap_mode = TextServer.AUTOWRAP_WORD
+ if group_count == 0:
+ empty_label.text = "No additional layers yet"
+ elif not _show_locked_layers and locked_group_count > 0:
+ empty_label.text = "All additional layers are locked (enable \"Show locked\" to view them)"
+ else:
+ empty_label.text = "No visible layers"
+ empty_label.modulate = Color(0.75, 0.75, 0.75)
+ layers_list.add_child(empty_label, true)
+ _update_layer_action_state()
+ _sync_active_layer_row()
+
+func clear_layer_stack() -> void:
+ _layer_entries.clear()
+ _selected_layer_groups.clear()
+ _layer_row_panels.clear()
+ update_layer_stack(Vector2i.ZERO, Terrain3DRegion.TYPE_MAX, [])
+
+func _describe_layer(entry: Dictionary) -> String:
+ var parts: Array[String] = []
+ var layer: Terrain3DLayer = entry.get("layer")
+ var ui_index := int(entry.get("ui_index", -1))
+ if ui_index >= 0:
+ parts.append("#%d" % (ui_index))
+ if layer:
+ parts.append(layer.get_class().replace("Terrain3D", ""))
+ var blend := layer.get_blend_mode()
+ if blend >= 0 and blend < LAYER_BLEND_LABELS.size():
+ parts.append(LAYER_BLEND_LABELS[blend])
+ var intensity := layer.get_intensity()
+ if not is_equal_approx(intensity, 1.0):
+ parts.append("x%.2f" % intensity)
+ var coverage: Rect2i = layer.get_coverage()
+ if coverage.size.x > 0 and coverage.size.y > 0:
+ parts.append("%dx%d @ %d,%d" % [coverage.size.x, coverage.size.y, coverage.position.x, coverage.position.y])
+ if not bool(entry.get("user_editable", true)):
+ parts.append("locked")
+ var slice_count := _slice_count(entry)
+ var slice_suffix := "" if slice_count == 1 else "s"
+ parts.append("%d slice%s" % [slice_count, slice_suffix])
+ var region_count := int(entry.get("region_count", slice_count))
+ var region_suffix := "" if region_count == 1 else "s"
+ parts.append("%d region%s" % [region_count, region_suffix])
+ var region_loc: Vector2i = entry.get("region_location", Vector2i.ZERO)
+ parts.append("primary @%d,%d" % [region_loc.x, region_loc.y])
+ return " • ".join(parts)
+
+func _describe_layer_regions(entry: Dictionary) -> String:
+ var slices: Array = entry.get("layers", [])
+ if slices.is_empty():
+ return "No regions"
+ var labels: PackedStringArray = []
+ for slice in slices:
+ var loc: Vector2i = slice.get("region_location", Vector2i.ZERO)
+ labels.append("%d,%d" % [loc.x, loc.y])
+ return "Regions: %s" % ", ".join(labels)
+
+func _slice_count(entry: Dictionary) -> int:
+ var slices: Array = entry.get("layers", [])
+ return slices.size()
+
+func _describe_base_layer(map_type: int) -> String:
+ match map_type:
+ Terrain3DRegion.TYPE_HEIGHT:
+ return "Base Heightmap"
+ Terrain3DRegion.TYPE_CONTROL:
+ return "Base Control Map"
+ Terrain3DRegion.TYPE_COLOR:
+ return "Base Color Map"
+ _:
+ return "Base Layer"
+
+func _create_layer_select_button(index: int, label: String, active_index: int, entry_id: int = -1, tooltip: String = "", can_select: bool = true) -> Button:
+ var button := Button.new()
+ button.toggle_mode = true
+ button.focus_mode = Control.FOCUS_NONE
+ button.button_group = layer_selection_group
+ button.text = label
+ button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ var base_tooltip := tooltip if not tooltip.is_empty() else "Make this the active layer for painting"
+ if can_select:
+ button.tooltip_text = base_tooltip
+ else:
+ var lock_tooltip := "Layer is locked and cannot be the active paint target"
+ button.tooltip_text = "%s\n%s" % [base_tooltip, lock_tooltip] if not base_tooltip.is_empty() else lock_tooltip
+ button.disabled = true
+ if index == active_index:
+ button.set_pressed_no_signal(true)
+ button.toggled.connect(_on_layer_selected.bind(index, entry_id))
+ return button
+
+func _add_base_layer_entry(active_index: int) -> void:
+ var base_row := HBoxContainer.new()
+ base_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ var select := _create_layer_select_button(0, _describe_base_layer(current_layer_map_type), active_index)
+ base_row.add_child(select)
+ var spacer := Control.new()
+ spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ base_row.add_child(spacer)
+ layers_list.add_child(base_row, true)
+
+func _get_layer_entry(entry_id: int) -> Dictionary:
+ if entry_id < 0 or entry_id >= _layer_entries.size():
+ return {}
+ return _layer_entries[entry_id]
+
+func _count_unique_regions() -> int:
+ var unique := {}
+ for entry in _layer_entries:
+ var slices: Array = entry.get("layers", [])
+ for slice in slices:
+ var region_loc: Vector2i = slice.get("region_location", Vector2i.ZERO)
+ unique[str(region_loc)] = true
+ return unique.size()
+
+func _count_total_slices() -> int:
+ var total := 0
+ for entry in _layer_entries:
+ total += _slice_count(entry)
+ return total
+
+func _region_exists(region_loc: Vector2i) -> bool:
+ if not plugin or not plugin.terrain:
+ return false
+ var data: Terrain3DData = plugin.terrain.data
+ if data == null:
+ return false
+ return data.has_region(region_loc)
+
+func _update_layer_action_state() -> void:
+ if not layer_add_button:
+ return
+ layer_add_button.disabled = not _region_exists(current_layer_region)
+
+func _sync_active_layer_row() -> void:
+ if not plugin or not plugin.editor:
+ return
+ if not plugin.editor.has_method("get_active_layer_group_id"):
+ return
+ var group_id: int = plugin.editor.get_active_layer_group_id()
+ if group_id == 0:
+ return
+ for entry_id in range(_layer_entries.size()):
+ var entry: Dictionary = _layer_entries[entry_id]
+ if int(entry.get("group_id", 0)) == group_id and bool(entry.get("user_editable", true)):
+ if plugin.editor.has_method("set_active_layer_index"):
+ plugin.editor.set_active_layer_index(entry_id + 1)
+ return
+
+func _on_layer_selected(pressed: bool, ui_index: int, entry_id: int = -1) -> void:
+ if not pressed:
+ return
+ if entry_id >= 0:
+ var entry := _get_layer_entry(entry_id)
+ if not entry.is_empty():
+ if not bool(entry.get("user_editable", true)):
+ return
+ current_layer_region = entry.get("region_location", current_layer_region)
+ _update_layer_action_state()
+ if not plugin or not plugin.editor:
+ return
+ if not plugin.editor.has_method("set_active_layer_index"):
+ return
+ plugin.editor.set_active_layer_index(ui_index)
+ if entry_id >= 0 and plugin.editor.has_method("set_active_layer_reference"):
+ var entry := _get_layer_entry(entry_id)
+ if not entry.is_empty():
+ plugin.editor.set_active_layer_reference(entry.get("layer"), current_layer_map_type)
+ # Ensure the editor picks up the change for pending stamp data
+ _on_refresh_layers()
+
+func _has_layer_context() -> bool:
+ return plugin and plugin.terrain and plugin.terrain.data
+
+func _on_layer_toggle(pressed: bool, entry_id: int) -> void:
+ if not _has_layer_context() or current_layer_map_type == Terrain3DRegion.TYPE_MAX:
+ return
+ var target_ids := _get_batch_entry_ids(entry_id)
+ var any_changed := false
+ for target_id in target_ids:
+ var entry := _get_layer_entry(target_id)
+ if entry.is_empty():
+ continue
+ if _toggle_entry_layers(entry, pressed):
+ any_changed = true
+ if any_changed:
+ _on_refresh_layers()
+
+func _on_layer_remove(entry_id: int) -> void:
+ if not _has_layer_context() or current_layer_map_type == Terrain3DRegion.TYPE_MAX:
+ return
+ var target_ids := _get_batch_entry_ids(entry_id)
+ var removed_groups: Array[int] = []
+ var any_removed := false
+ for target_id in target_ids:
+ var entry := _get_layer_entry(target_id)
+ if entry.is_empty():
+ continue
+ removed_groups.append(int(entry.get("group_id", 0)))
+ if _remove_entry_layers(entry):
+ any_removed = true
+ for group_id in removed_groups:
+ _selected_layer_groups.erase(group_id)
+ if any_removed:
+ if plugin and plugin.editor and plugin.editor.has_method("set_active_layer_index"):
+ plugin.editor.set_active_layer_index(0)
+ if plugin and plugin.editor and plugin.editor.has_method("set_active_layer_reference"):
+ plugin.editor.set_active_layer_reference(null, Terrain3DRegion.TYPE_MAX)
+ _on_refresh_layers()
+
+
+func _on_refresh_layers() -> void:
+ if plugin and plugin.ui and plugin.ui.has_method("update_layer_panel"):
+ plugin.ui.update_layer_panel()
+ else:
+ clear_layer_stack()
+
+func _on_locked_filter_toggled(pressed: bool) -> void:
+ _show_locked_layers = pressed
+ if not _show_locked_layers:
+ _clear_hidden_locked_selection()
+ _on_refresh_layers()
+
+func _on_layer_row_selection_toggled(pressed: bool, entry_id: int) -> void:
+ var entry := _get_layer_entry(entry_id)
+ if entry.is_empty():
+ return
+ var group_id := int(entry.get("group_id", 0))
+ if group_id == 0:
+ return
+ _set_group_selected(group_id, pressed)
+ _apply_row_selection_style(entry_id)
+
+func _is_group_selected(group_id: int) -> bool:
+ if group_id == 0:
+ return false
+ return _selected_layer_groups.has(group_id)
+
+func _set_group_selected(group_id: int, selected: bool) -> void:
+ if group_id == 0:
+ return
+ if selected:
+ _selected_layer_groups[group_id] = true
+ else:
+ _selected_layer_groups.erase(group_id)
+
+func _get_selected_entry_ids() -> Array:
+ var ids: Array = []
+ if _selected_layer_groups.is_empty():
+ return ids
+ for entry_id in range(_layer_entries.size()):
+ var entry: Dictionary = _layer_entries[entry_id]
+ if _is_group_selected(int(entry.get("group_id", 0))):
+ ids.append(entry_id)
+ return ids
+
+func _get_batch_entry_ids(primary_entry_id: int) -> Array:
+ var selected_ids := _get_selected_entry_ids()
+ if selected_ids.is_empty():
+ return [primary_entry_id]
+ if selected_ids.has(primary_entry_id):
+ return selected_ids
+ return [primary_entry_id]
+
+func _apply_row_selection_style(entry_id: int) -> void:
+ if not _layer_row_panels.has(entry_id):
+ return
+ var panel: PanelContainer = _layer_row_panels[entry_id]
+ if panel == null:
+ return
+ var entry := _get_layer_entry(entry_id)
+ if entry.is_empty():
+ panel.remove_theme_stylebox_override("panel")
+ return
+ var group_id := int(entry.get("group_id", 0))
+ if _is_group_selected(group_id):
+ panel.add_theme_stylebox_override("panel", _get_selected_row_style())
+ else:
+ panel.remove_theme_stylebox_override("panel")
+
+func _get_selected_row_style() -> StyleBoxFlat:
+ if _selected_row_style == null:
+ _selected_row_style = StyleBoxFlat.new()
+ _selected_row_style.bg_color = Color(0.35, 0.6, 1.0, 0.15)
+ _selected_row_style.border_color = Color(0.35, 0.6, 1.0, 0.6)
+ _selected_row_style.set_border_width_all(1)
+ _selected_row_style.set_corner_radius_all(3)
+ return _selected_row_style
+
+func _toggle_entry_layers(entry: Dictionary, pressed: bool) -> bool:
+ var slices: Array = entry.get("layers", [])
+ if slices.is_empty():
+ return false
+ var changed := false
+ for i in range(slices.size()):
+ var slice: Dictionary = slices[i]
+ var region_loc: Vector2i = slice.get("region_location", current_layer_region)
+ var layer_index := int(slice.get("layer_index", -1))
+ if layer_index < 0:
+ continue
+ var update := (i == slices.size() - 1)
+ plugin.terrain.data.set_layer_enabled(region_loc, current_layer_map_type, layer_index, pressed, update)
+ changed = true
+ return changed
+
+func _remove_entry_layers(entry: Dictionary) -> bool:
+ var slices: Array = entry.get("layers", [])
+ if slices.is_empty():
+ return false
+ var removed := false
+ for i in range(slices.size()):
+ var slice: Dictionary = slices[i]
+ var region_loc: Vector2i = slice.get("region_location", current_layer_region)
+ var layer_index := int(slice.get("layer_index", -1))
+ if layer_index < 0:
+ continue
+ var update := (i == slices.size() - 1)
+ plugin.terrain.data.remove_layer(region_loc, current_layer_map_type, layer_index, update)
+ removed = true
+ return removed
+
+func _clear_hidden_locked_selection() -> void:
+ if _selected_layer_groups.is_empty():
+ return
+ for entry in _layer_entries:
+ if not bool(entry.get("user_editable", true)):
+ var group_id := int(entry.get("group_id", 0))
+ _selected_layer_groups.erase(group_id)
+
+func _on_add_layer() -> void:
+ if not _has_layer_context() or current_layer_map_type == Terrain3DRegion.TYPE_MAX:
+ return
+ if not _region_exists(current_layer_region):
+ return
+ if not plugin or not plugin.editor:
+ return
+ if not plugin.editor.has_method("create_layer"):
+ return
+ var new_index: int = plugin.editor.create_layer(current_layer_region, current_layer_map_type, true)
+ if new_index >= 0:
+ plugin.editor.set_active_layer_index(new_index)
+ _on_refresh_layers()
func set_setting(p_setting: String, p_value: Variant) -> void:
var object: Object = settings.get(p_setting)
diff --git a/project/addons/terrain_3d/src/ui.gd b/project/addons/terrain_3d/src/ui.gd
index e11437b50..da23d9d51 100644
--- a/project/addons/terrain_3d/src/ui.gd
+++ b/project/addons/terrain_3d/src/ui.gd
@@ -136,6 +136,10 @@ func set_visible(p_visible: bool, p_menu_only: bool = false) -> void:
visible = p_visible
toolbar.set_visible(p_visible)
tool_settings.set_visible(p_visible)
+ if p_visible:
+ update_layer_panel()
+ else:
+ tool_settings.clear_layer_stack()
if plugin.editor:
if p_visible:
@@ -182,7 +186,7 @@ func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor
to_show.push_back("size")
to_show.push_back("strength")
if _selected_operation in [Terrain3DEditor.ADD, Terrain3DEditor.SUBTRACT]:
- to_show.push_back("invert")
+ to_show.push_back("invert")
elif _selected_operation == Terrain3DEditor.GRADIENT:
to_show.push_back("gradient_points")
to_show.push_back("drawable")
@@ -280,6 +284,69 @@ func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor
print("Terrain3DUI: _on_tool_changed: calling _on_setting_changed()")
_on_setting_changed()
plugin.update_region_grid()
+ update_layer_panel()
+
+
+func update_layer_panel() -> void:
+ if not plugin or not plugin.terrain or not tool_settings:
+ return
+ var data: Terrain3DData = plugin.terrain.data
+ if data == null:
+ tool_settings.clear_layer_stack()
+ return
+ var map_type: int = Terrain3DRegion.TYPE_MAX
+ if plugin.editor:
+ map_type = _map_type_for_tool(plugin.editor.get_tool())
+ if map_type == Terrain3DRegion.TYPE_MAX:
+ tool_settings.clear_layer_stack()
+ return
+ var default_region := data.get_region_location(plugin.mouse_global_position)
+ var layer_entries := _collect_global_layers(data, map_type, default_region)
+ tool_settings.update_layer_stack(default_region, map_type, layer_entries)
+
+func _collect_global_layers(data: Terrain3DData, map_type: int, default_region: Vector2i) -> Array:
+ var entries: Array = []
+ if data == null:
+ return entries
+ var groups: Array = data.get_layer_groups(map_type) if data.has_method("get_layer_groups") else []
+ if groups.is_empty():
+ return entries
+ for group_dict in groups:
+ var slices: Array = group_dict.get("layers", [])
+ if slices.is_empty():
+ continue
+ var primary_slice: Dictionary = {}
+ var layer_ref: Terrain3DLayer = null
+ for slice in slices:
+ var layer_candidate: Terrain3DLayer = slice.get("layer")
+ if layer_candidate and layer_ref == null:
+ layer_ref = layer_candidate
+ if primary_slice.is_empty():
+ primary_slice = slice
+ if slice.get("region_location", Vector2i.ZERO) == default_region:
+ primary_slice = slice
+ break
+ if primary_slice.is_empty():
+ continue
+ var unique_regions := {}
+ for slice in slices:
+ var loc: Vector2i = slice.get("region_location", Vector2i.ZERO)
+ unique_regions[str(loc)] = loc
+ var editable := true
+ if layer_ref and layer_ref.has_method("is_user_editable"):
+ editable = layer_ref.is_user_editable()
+ var entry := {
+ "group_id": group_dict.get("group_id", 0),
+ "map_type": map_type,
+ "layer": primary_slice.get("layer"),
+ "layers": slices,
+ "region_location": primary_slice.get("region_location", Vector2i.ZERO),
+ "layer_index": primary_slice.get("layer_index", -1),
+ "region_count": unique_regions.size(),
+ "user_editable": editable
+ }
+ entries.append(entry)
+ return entries
func _on_setting_changed(p_setting: Variant = null) -> void:
@@ -649,3 +716,15 @@ func pick(p_global_position: Vector3) -> void:
func set_button_editor_icon(p_button: Button, p_icon_name: String) -> void:
p_button.icon = EditorInterface.get_base_control().get_theme_icon(p_icon_name, "EditorIcons")
+
+
+func _map_type_for_tool(p_tool: int) -> int:
+ match p_tool:
+ Terrain3DEditor.SCULPT, Terrain3DEditor.HEIGHT, Terrain3DEditor.INSTANCER:
+ return Terrain3DRegion.TYPE_HEIGHT
+ Terrain3DEditor.TEXTURE, Terrain3DEditor.AUTOSHADER, Terrain3DEditor.HOLES, Terrain3DEditor.NAVIGATION, Terrain3DEditor.ANGLE, Terrain3DEditor.SCALE:
+ return Terrain3DRegion.TYPE_CONTROL
+ Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS:
+ return Terrain3DRegion.TYPE_COLOR
+ _:
+ return Terrain3DRegion.TYPE_MAX
diff --git a/src/constants.h b/src/constants.h
index b1cc474a3..dbdd0cede 100644
--- a/src/constants.h
+++ b/src/constants.h
@@ -3,6 +3,8 @@
#ifndef CONSTANTS_CLASS_H
#define CONSTANTS_CLASS_H
+#include
+
// GDExtension uses the godot namespace, custom modules do not.
#if defined(GDEXTENSION) && !defined(GODOT_MODULE)
using namespace godot;
diff --git a/src/register_types.cpp b/src/register_types.cpp
index 41cb61910..f7110e8f7 100644
--- a/src/register_types.cpp
+++ b/src/register_types.cpp
@@ -9,6 +9,7 @@
#include "register_types.h"
#include "terrain_3d.h"
#include "terrain_3d_editor.h"
+#include "terrain_3d_layer.h"
void initialize_terrain_3d_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
@@ -25,6 +26,8 @@ void initialize_terrain_3d_module(ModuleInitializationLevel p_level) {
ClassDB::register_class();
ClassDB::register_class();
ClassDB::register_class();
+ ClassDB::register_class();
+ ClassDB::register_class();
}
void uninitialize_terrain_3d_module(ModuleInitializationLevel p_level) {
diff --git a/src/terrain_3d.cpp b/src/terrain_3d.cpp
index 164935eab..4842203c5 100644
--- a/src/terrain_3d.cpp
+++ b/src/terrain_3d.cpp
@@ -13,6 +13,7 @@
#include
#include
#include
+#include
#include "logger.h"
#include "terrain_3d.h"
@@ -75,11 +76,11 @@ void Terrain3D::_initialize() {
LOG(DEBUG, "Connecting _data::height_maps_changed signal to update_aabbs()");
_data->connect("height_maps_changed", callable_mp(this, &Terrain3D::_update_mesher_aabbs));
}
- // Texture assets changed, update material uniforms without rebuilding shaders
- if (!_assets->is_connected("textures_changed", callable_mp(_material.ptr(), &Terrain3DMaterial::update).bind(false))) {
- LOG(DEBUG, "Connecting _assets.textures_changed to _material->update()");
- _assets->connect("textures_changed", callable_mp(_material.ptr(), &Terrain3DMaterial::update).bind(false));
- }
+ // // Texture assets changed, update material
+ // if (!_assets->is_connected("textures_changed", callable_mp(_material.ptr(), &Terrain3DMaterial::_update_texture_arrays))) {
+ // LOG(DEBUG, "Connecting _assets.textures_changed to _material->_update_texture_arrays()");
+ // _assets->connect("textures_changed", callable_mp(_material.ptr(), &Terrain3DMaterial::_update_texture_arrays));
+ // }
// Initialize the system
if (!_initialized && _is_inside_world && is_inside_tree()) {
LOG(INFO, "Initializing main subsystems");
@@ -107,6 +108,7 @@ void Terrain3D::__physics_process(const double p_delta) {
LOG(DEBUG, "Camera is null, getting the current one");
_grab_camera();
}
+ _process_mesh_snap_request();
if (_tessellation_level > 0) {
if (_mesher && _d_buffer_vp && _material.is_valid()) {
// If clipmap target has moved enough, re-center buffer on the target.
@@ -129,6 +131,29 @@ void Terrain3D::__physics_process(const double p_delta) {
}
}
+void Terrain3D::_process_mesh_snap_request() {
+ if (!_mesh_snap_requested) {
+ return;
+ }
+ _mesh_snap_requested = false;
+ snap_mesh();
+}
+
+void Terrain3D::_update_mesher_aabbs() {
+ if (!_mesher || !_data) {
+ return;
+ }
+ Vector2 height_range = _data->get_height_range();
+ bool has_cached = !Math::is_nan(_last_height_range.x) && !Math::is_nan(_last_height_range.y);
+ if (has_cached &&
+ Math::is_equal_approx(height_range.x, _last_height_range.x) &&
+ Math::is_equal_approx(height_range.y, _last_height_range.y)) {
+ return;
+ }
+ _last_height_range = height_range;
+ _mesher->update_aabbs();
+}
+
/**
* If running in the editor, grab the first editor viewport camera.
* The edited_scene_root is excluded in case the user already has a Camera3D in their scene.
@@ -573,6 +598,14 @@ Vector3 Terrain3D::get_collision_target_position() const {
}
void Terrain3D::snap() {
+ snap_mesh();
+ if (_collision) {
+ LOG(DEBUG, "Terrain3D::snap resetting collision target");
+ _collision->reset_target_position();
+ }
+}
+
+void Terrain3D::snap_mesh() {
if (_mesher) {
_mesher->reset_target_position();
}
@@ -584,6 +617,10 @@ void Terrain3D::snap() {
}
}
+void Terrain3D::request_mesh_snap() {
+ _mesh_snap_requested = true;
+}
+
void Terrain3D::set_region_size(const RegionSize p_size) {
if (!is_valid_region_size(p_size)) {
LOG(ERROR, "Invalid region size: ", p_size, ". Must be power of 2, 64-2048");
diff --git a/src/terrain_3d.h b/src/terrain_3d.h
index 664468377..d80ca1359 100644
--- a/src/terrain_3d.h
+++ b/src/terrain_3d.h
@@ -5,12 +5,16 @@
#include
#include
+#include
+#include
#include
#include
#include
#include
#include
#include
+#include
+#include
#include "constants.h"
#include "target_node_3d.h"
@@ -50,7 +54,7 @@ class Terrain3D : public Node3D {
String _data_directory;
bool _is_inside_world = false;
bool _initialized = false;
- uint8_t _warnings = 0u;
+ uint8_t _warnings = 0;
// Object references
Terrain3DData *_data = nullptr;
@@ -85,6 +89,8 @@ class Terrain3D : public Node3D {
GeometryInstance3D::GIMode _gi_mode = GeometryInstance3D::GI_MODE_STATIC;
real_t _cull_margin = 0.0f;
bool _free_editor_textures = true;
+ bool _mesh_snap_requested = false;
+ Vector2 _last_height_range = Vector2(NAN, NAN);
// Mouse cursor
SubViewport *_mouse_vp = nullptr;
@@ -111,7 +117,8 @@ class Terrain3D : public Node3D {
void _destroy_instancer();
void _destroy_collision(const bool p_final = false);
void _destroy_mesher(const bool p_final = false);
- void _update_mesher_aabbs() { _mesher ? _mesher->update_aabbs() : void(); }
+ void _update_mesher_aabbs();
+ void _process_mesh_snap_request();
void _setup_mouse_picking();
void _destroy_mouse_picking();
@@ -162,6 +169,8 @@ class Terrain3D : public Node3D {
void set_collision_target(Node3D *p_node);
Vector3 get_collision_target_position() const;
void snap();
+ void snap_mesh();
+ void request_mesh_snap();
// Regions
void set_region_size(const RegionSize p_size);
diff --git a/src/terrain_3d_collision.cpp b/src/terrain_3d_collision.cpp
index 2d2bb2093..45af113d8 100644
--- a/src/terrain_3d_collision.cpp
+++ b/src/terrain_3d_collision.cpp
@@ -40,24 +40,24 @@ Dictionary Terrain3DCollision::_get_shape_data(const Vector2i &p_position, const
LOG(EXTREME, "Region not found at: ", region_loc, ". Returning blank");
return Dictionary();
}
- map = region->get_map(TYPE_HEIGHT);
- cmap = region->get_map(TYPE_CONTROL);
+ map = region->get_composited_map(TYPE_HEIGHT);
+ cmap = region->get_composited_map(TYPE_CONTROL);
// Get +X, +Z adjacent regions in case we run over
region = data->get_region_ptr(region_loc + Vector2i(1, 0));
if (region && !region->is_deleted()) {
- map_x = region->get_map(TYPE_HEIGHT);
- cmap_x = region->get_map(TYPE_CONTROL);
+ map_x = region->get_composited_map(TYPE_HEIGHT);
+ cmap_x = region->get_composited_map(TYPE_CONTROL);
}
region = data->get_region_ptr(region_loc + Vector2i(0, 1));
if (region && !region->is_deleted()) {
- map_z = region->get_map(TYPE_HEIGHT);
- cmap_z = region->get_map(TYPE_CONTROL);
+ map_z = region->get_composited_map(TYPE_HEIGHT);
+ cmap_z = region->get_composited_map(TYPE_CONTROL);
}
region = data->get_region_ptr(region_loc + Vector2i(1, 1));
if (region && !region->is_deleted()) {
- map_xz = region->get_map(TYPE_HEIGHT);
- cmap_xz = region->get_map(TYPE_CONTROL);
+ map_xz = region->get_composited_map(TYPE_HEIGHT);
+ cmap_xz = region->get_composited_map(TYPE_CONTROL);
}
for (int z = 0; z < hshape_size; z++) {
diff --git a/src/terrain_3d_data.cpp b/src/terrain_3d_data.cpp
index fbf793ca1..1c95bf403 100644
--- a/src/terrain_3d_data.cpp
+++ b/src/terrain_3d_data.cpp
@@ -6,6 +6,14 @@
#include
#include
#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
#include "logger.h"
#include "terrain_3d_data.h"
@@ -25,6 +33,8 @@ void Terrain3DData::_clear() {
_generated_height_maps.clear();
_generated_control_maps.clear();
_generated_color_maps.clear();
+ _next_layer_group_id = 1;
+ _external_layers.clear();
}
// Structured to work with do_for_regions. Should be renamed when copy_paste is expanded
@@ -43,6 +53,93 @@ void Terrain3DData::_copy_paste_dfr(const Terrain3DRegion *p_src_region, const R
_terrain->get_instancer()->copy_paste_dfr(p_src_region, p_src_rect, p_dst_region);
}
+bool Terrain3DData::_find_layer_owner(const Ref &p_layer, const MapType p_map_type, Terrain3DRegion **r_region, Vector2i *r_region_loc, int *r_index) const {
+ if (p_layer.is_null()) {
+ return false;
+ }
+ Array keys = _regions.keys();
+ for (int i = 0; i < keys.size(); i++) {
+ Vector2i region_loc = keys[i];
+ Terrain3DRegion *region = get_region_ptr(region_loc);
+ if (!region) {
+ continue;
+ }
+ TypedArray layers = region->get_layers(p_map_type);
+ for (int j = 0; j < layers.size(); j++) {
+ Ref candidate = layers[j];
+ if (candidate == p_layer) {
+ if (r_region) {
+ *r_region = region;
+ }
+ if (r_region_loc) {
+ *r_region_loc = region_loc;
+ }
+ if (r_index) {
+ *r_index = j;
+ }
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+uint64_t Terrain3DData::_allocate_layer_group_id() {
+ return _next_layer_group_id++;
+}
+
+uint64_t Terrain3DData::_ensure_layer_group_id_internal(const Ref &p_layer) {
+ if (p_layer.is_null()) {
+ return 0;
+ }
+ uint64_t group_id = p_layer->get_group_id();
+ if (group_id == 0) {
+ group_id = _allocate_layer_group_id();
+ p_layer->set_group_id(group_id);
+ } else if (group_id >= _next_layer_group_id) {
+ _next_layer_group_id = group_id + 1;
+ }
+ return group_id;
+}
+
+void Terrain3DData::_ensure_region_layer_groups(const Ref &p_region) {
+ if (p_region.is_null()) {
+ return;
+ }
+ for (int map_type = TYPE_HEIGHT; map_type < TYPE_MAX; map_type++) {
+ TypedArray layers = p_region->get_layers(MapType(map_type));
+ for (int i = 0; i < layers.size(); i++) {
+ _ensure_layer_group_id_internal(layers[i]);
+ }
+ }
+}
+
+uint64_t Terrain3DData::ensure_layer_group_id(const Ref &p_layer) {
+ return _ensure_layer_group_id_internal(p_layer);
+}
+
+Ref Terrain3DData::_duplicate_layer_template(const Ref &p_template) const {
+ if (p_template.is_null()) {
+ return Ref();
+ }
+ Ref clone = p_template->duplicate(true);
+ if (clone.is_null()) {
+ return Ref();
+ }
+ clone->set_payload(Ref());
+ clone->set_alpha(Ref());
+ clone->set_coverage(Rect2i());
+ clone->mark_dirty();
+ clone->set_map_type(p_template->get_map_type());
+ clone->set_group_id(p_template->get_group_id());
+ clone->set_enabled(p_template->is_enabled());
+ clone->set_intensity(p_template->get_intensity());
+ clone->set_feather_radius(p_template->get_feather_radius());
+ clone->set_blend_mode(p_template->get_blend_mode());
+ clone->set_user_editable(p_template->is_user_editable());
+ return clone;
+}
+
///////////////////////////
// Public Functions
///////////////////////////
@@ -248,6 +345,7 @@ Error Terrain3DData::add_region(const Ref &p_region, const bool
return FAILED;
}
p_region->sanitize_maps();
+ _ensure_region_layer_groups(p_region);
p_region->set_deleted(false);
if (!_region_locations.has(region_loc)) {
_region_locations.push_back(region_loc);
@@ -441,6 +539,683 @@ TypedArray Terrain3DData::get_maps(const MapType p_map_type) const {
return TypedArray();
}
+TypedArray Terrain3DData::get_layers(const Vector2i &p_region_loc, const MapType p_map_type) const {
+ TypedArray layers;
+ const Terrain3DRegion *region = get_region_ptr(p_region_loc);
+ if (!region) {
+ LOG(ERROR, "Cannot retrieve layers. Region not found at ", p_region_loc);
+ return layers;
+ }
+ layers = region->get_layers(p_map_type);
+ return layers;
+}
+
+TypedArray Terrain3DData::get_layer_groups(const MapType p_map_type) {
+ TypedArray result;
+ if (p_map_type < 0 || p_map_type >= TYPE_MAX) {
+ return result;
+ }
+ Dictionary grouped;
+ Array region_keys = _regions.keys();
+ for (int i = 0; i < region_keys.size(); i++) {
+ Vector2i region_loc = region_keys[i];
+ const Terrain3DRegion *region = get_region_ptr(region_loc);
+ if (!region || region->is_deleted()) {
+ continue;
+ }
+ TypedArray layers = region->get_layers(p_map_type);
+ for (int layer_index = 0; layer_index < layers.size(); layer_index++) {
+ Ref layer = layers[layer_index];
+ if (layer.is_null()) {
+ continue;
+ }
+ uint64_t group_id = _ensure_layer_group_id_internal(layer);
+ if (group_id == 0) {
+ continue;
+ }
+ if (!grouped.has(group_id)) {
+ Dictionary group_info;
+ group_info["group_id"] = group_id;
+ group_info["map_type"] = p_map_type;
+ group_info["layers"] = Array();
+ grouped[group_id] = group_info;
+ }
+ Dictionary group_info = grouped[group_id];
+ Array slices = group_info["layers"];
+ Dictionary slice_info;
+ slice_info["region_location"] = region_loc;
+ slice_info["layer_index"] = layer_index;
+ slice_info["layer"] = layer;
+ slices.push_back(slice_info);
+ group_info["layers"] = slices;
+ grouped[group_id] = group_info;
+ }
+ }
+ Array grouped_keys = grouped.keys();
+ grouped_keys.sort();
+ for (int i = 0; i < grouped_keys.size(); i++) {
+ Variant key = grouped_keys[i];
+ result.push_back(grouped[key]);
+ }
+ return result;
+}
+
+Ref Terrain3DData::add_layer(const Vector2i &p_region_loc, const MapType p_map_type, const Ref &p_layer, const int p_index, const bool p_update) {
+ Terrain3DRegion *region = get_region_ptr(p_region_loc);
+ if (!region) {
+ LOG(ERROR, "Cannot add layer. Region not found at ", p_region_loc);
+ return Ref();
+ }
+ if (p_layer.is_null()) {
+ LOG(ERROR, "Cannot add layer. Provided layer is null");
+ return Ref();
+ }
+ _ensure_layer_group_id_internal(p_layer);
+ Ref layer = region->add_layer(p_map_type, p_layer, p_index);
+ if (layer.is_valid()) {
+ region->set_modified(true);
+ region->set_edited(true);
+ if (p_update) {
+ update_maps(p_map_type, false, false);
+ }
+ }
+ return layer;
+}
+
+Ref Terrain3DData::add_stamp_layer(const Vector2i &p_region_loc, const MapType p_map_type, const Ref &p_payload, const Rect2i &p_coverage, const Ref &p_alpha, const real_t p_intensity, const real_t p_feather_radius, const Terrain3DLayer::BlendMode p_blend_mode, const int p_index, const bool p_update) {
+ Ref result;
+ Terrain3DRegion *region = get_region_ptr(p_region_loc);
+ if (!region) {
+ LOG(ERROR, "Cannot add stamp layer. Region not found at ", p_region_loc);
+ return result;
+ }
+ if (p_payload.is_null()) {
+ LOG(ERROR, "Stamp layer requires a valid payload image");
+ return result;
+ }
+ Rect2i coverage = p_coverage;
+ int region_size = region->get_region_size();
+ Vector2i max_bounds(region_size, region_size);
+ coverage.position.x = CLAMP(coverage.position.x, 0, max_bounds.x - 1);
+ coverage.position.y = CLAMP(coverage.position.y, 0, max_bounds.y - 1);
+ Vector2i coverage_end = coverage.position + coverage.size;
+ coverage_end.x = CLAMP(coverage_end.x, coverage.position.x + 1, max_bounds.x);
+ coverage_end.y = CLAMP(coverage_end.y, coverage.position.y + 1, max_bounds.y);
+ coverage.size = coverage_end - coverage.position;
+ if (coverage.size.x <= 0 || coverage.size.y <= 0) {
+ LOG(WARN, "Stamp coverage lies outside the region bounds; skipping layer creation");
+ return result;
+ }
+ Ref payload = p_payload;
+ Image::Format expected_format = map_type_get_format(p_map_type);
+ if (payload->get_format() != expected_format) {
+ payload = payload->duplicate();
+ if (payload.is_valid()) {
+ payload->convert(expected_format);
+ }
+ }
+ if (payload.is_null()) {
+ LOG(ERROR, "Failed to prepare stamp payload image");
+ return result;
+ }
+ Ref layer;
+ layer.instantiate();
+ layer->set_map_type(p_map_type);
+ layer->set_coverage(coverage);
+ layer->set_payload(payload);
+ if (p_alpha.is_valid()) {
+ layer->set_alpha(p_alpha);
+ }
+ layer->set_intensity(p_intensity);
+ layer->set_feather_radius(p_feather_radius);
+ layer->set_blend_mode(p_blend_mode);
+ layer->set_enabled(true);
+ _ensure_layer_group_id_internal(layer);
+ Ref stored = region->add_layer(p_map_type, layer, p_index);
+ result = stored;
+ if (result.is_null()) {
+ result = layer;
+ }
+ region->set_modified(true);
+ region->set_edited(true);
+ if (p_update) {
+ update_maps(p_map_type, false, false);
+ }
+ return result;
+}
+
+Ref Terrain3DData::get_layer_in_group(const Vector2i &p_region_loc, const MapType p_map_type, const uint64_t p_group_id, int *r_index) {
+ if (p_group_id == 0) {
+ return Ref();
+ }
+ const Terrain3DRegion *region = get_region_ptr(p_region_loc);
+ if (!region) {
+ return Ref();
+ }
+ TypedArray layers = region->get_layers(p_map_type);
+ for (int i = 0; i < layers.size(); i++) {
+ Ref layer = layers[i];
+ if (layer.is_null()) {
+ continue;
+ }
+ uint64_t group_id = _ensure_layer_group_id_internal(layer);
+ if (group_id == p_group_id) {
+ if (r_index) {
+ *r_index = i;
+ }
+ return layer;
+ }
+ }
+ return Ref();
+}
+
+Ref Terrain3DData::create_layer_group_slice(const Vector2i &p_region_loc, const MapType p_map_type, const uint64_t p_group_id, const Ref &p_template_layer, const bool p_update) {
+ if (p_group_id == 0 || p_template_layer.is_null()) {
+ return Ref();
+ }
+ Ref clone = _duplicate_layer_template(p_template_layer);
+ if (clone.is_null()) {
+ return Ref();
+ }
+ clone->set_group_id(p_group_id);
+ clone->set_map_type(p_map_type);
+ return add_layer(p_region_loc, p_map_type, clone, -1, p_update);
+}
+
+TypedArray Terrain3DData::add_stamp_layer_global(const Rect2i &p_global_coverage, const MapType p_map_type, const Ref &p_payload, const Ref &p_alpha, const real_t p_intensity, const real_t p_feather_radius, const Terrain3DLayer::BlendMode p_blend_mode, const bool p_auto_create_regions, const bool p_update) {
+ TypedArray created_layers;
+ if (!p_global_coverage.has_area()) {
+ LOG(WARN, "add_stamp_layer_global: coverage has no area");
+ return created_layers;
+ }
+ LayerSplitResults splits = split_layer_payload_global(p_global_coverage, p_payload, p_alpha);
+ if (splits.empty()) {
+ return created_layers;
+ }
+ bool any_added = false;
+ uint64_t shared_group_id = 0;
+ for (const LayerSplitResult &slice : splits) {
+ Terrain3DRegion *region = get_region_ptr(slice.region_location);
+ if (!region && p_auto_create_regions) {
+ Ref new_region = add_region_blank(slice.region_location, false);
+ region = new_region.is_valid() ? new_region.ptr() : nullptr;
+ }
+ if (!region) {
+ LOG(WARN, "add_stamp_layer_global: region ", slice.region_location, " unavailable; skipping slice");
+ continue;
+ }
+ Ref added = add_stamp_layer(slice.region_location, p_map_type, slice.payload, slice.coverage, slice.alpha, p_intensity, p_feather_radius, p_blend_mode, -1, false);
+ if (added.is_valid()) {
+ if (shared_group_id == 0) {
+ shared_group_id = ensure_layer_group_id(added);
+ } else {
+ added->set_group_id(shared_group_id);
+ }
+ created_layers.push_back(added);
+ any_added = true;
+ }
+ }
+ if (any_added && p_update) {
+ update_maps(p_map_type, false, false);
+ }
+ return created_layers;
+}
+
+Terrain3DData::LayerSplitResults Terrain3DData::split_layer_payload_global(const Rect2i &p_global_coverage, const Ref &p_payload, const Ref &p_alpha) const {
+ LayerSplitResults slices;
+ if (!p_global_coverage.has_area()) {
+ LOG(DEBUG, "split_layer_payload_global: requested coverage has no area");
+ return slices;
+ }
+ if (p_payload.is_null()) {
+ LOG(WARN, "split_layer_payload_global: payload image is null");
+ return slices;
+ }
+ if (_region_size <= 0) {
+ LOG(ERROR, "split_layer_payload_global: invalid region size ", _region_size);
+ return slices;
+ }
+ Vector2i payload_size(p_payload->get_width(), p_payload->get_height());
+ if (payload_size.x <= 0 || payload_size.y <= 0) {
+ LOG(ERROR, "split_layer_payload_global: payload has invalid dimensions ", payload_size);
+ return slices;
+ }
+ if (payload_size != p_global_coverage.size) {
+ LOG(ERROR, "split_layer_payload_global: payload size ", payload_size, " does not match coverage size ", p_global_coverage.size);
+ return slices;
+ }
+ if (p_alpha.is_valid()) {
+ Vector2i alpha_size(p_alpha->get_width(), p_alpha->get_height());
+ if (alpha_size != payload_size) {
+ LOG(WARN, "split_layer_payload_global: alpha size ", alpha_size, " does not match payload size ", payload_size, ". Alpha will be ignored");
+ }
+ }
+ auto floor_div = [](int value, int divisor) -> int {
+ if (divisor == 0) {
+ return 0;
+ }
+ int result = value / divisor;
+ int remainder = value % divisor;
+ if (remainder != 0 && ((remainder < 0) != (divisor < 0))) {
+ result -= 1;
+ }
+ return result;
+ };
+ Vector2i coverage_end = p_global_coverage.get_end();
+ coverage_end -= Vector2i(1, 1); // Make inclusive for region range calculation
+ int min_region_x = floor_div(p_global_coverage.position.x, _region_size);
+ int min_region_y = floor_div(p_global_coverage.position.y, _region_size);
+ int max_region_x = floor_div(coverage_end.x, _region_size);
+ int max_region_y = floor_div(coverage_end.y, _region_size);
+ for (int region_y = min_region_y; region_y <= max_region_y; region_y++) {
+ for (int region_x = min_region_x; region_x <= max_region_x; region_x++) {
+ Vector2i region_loc(region_x, region_y);
+ Rect2i region_bounds(region_loc * _region_size, Vector2i(_region_size, _region_size));
+ Rect2i slice_global = region_bounds.intersection(p_global_coverage);
+ if (!slice_global.has_area()) {
+ continue;
+ }
+ Rect2i payload_rect(slice_global.position - p_global_coverage.position, slice_global.size);
+ if (payload_rect.position.x < 0 || payload_rect.position.y < 0 ||
+ payload_rect.get_end().x > payload_size.x || payload_rect.get_end().y > payload_size.y) {
+ LOG(ERROR, "split_layer_payload_global: computed payload rect ", payload_rect,
+ " falls outside payload bounds ", Rect2i(Vector2i(), payload_size));
+ return slices;
+ }
+ Ref region_payload;
+ region_payload.instantiate();
+ region_payload->create(slice_global.size.x, slice_global.size.y, false, p_payload->get_format());
+ region_payload->blit_rect(p_payload, payload_rect, Vector2i());
+ Ref region_alpha;
+ if (p_alpha.is_valid() && p_alpha->get_width() == payload_size.x && p_alpha->get_height() == payload_size.y) {
+ region_alpha.instantiate();
+ region_alpha->create(slice_global.size.x, slice_global.size.y, false, p_alpha->get_format());
+ region_alpha->blit_rect(p_alpha, payload_rect, Vector2i());
+ }
+ LayerSplitResult slice;
+ slice.region_location = region_loc;
+ slice.coverage = Rect2i(slice_global.position - region_bounds.position, slice_global.size);
+ slice.payload = region_payload;
+ slice.alpha = region_alpha;
+ slices.push_back(slice);
+ }
+ }
+ return slices;
+}
+
+TypedArray Terrain3DData::split_layer_payload_global_data(const Rect2i &p_global_coverage, const Ref &p_payload, const Ref &p_alpha) const {
+ TypedArray result;
+ LayerSplitResults slices = split_layer_payload_global(p_global_coverage, p_payload, p_alpha);
+ for (const LayerSplitResult &slice : slices) {
+ Dictionary entry;
+ entry["region_location"] = slice.region_location;
+ entry["coverage"] = slice.coverage;
+ if (slice.payload.is_valid()) {
+ entry["payload"] = slice.payload;
+ }
+ if (slice.alpha.is_valid()) {
+ entry["alpha"] = slice.alpha;
+ }
+ result.push_back(entry);
+ }
+ return result;
+}
+
+Dictionary Terrain3DData::get_layer_owner_info(const Ref &p_layer, const MapType p_map_type) const {
+ Dictionary result;
+ if (p_layer.is_null()) {
+ return result;
+ }
+ Terrain3DRegion *region = nullptr;
+ Vector2i region_loc;
+ int index = -1;
+ if (!_find_layer_owner(p_layer, p_map_type, ®ion, ®ion_loc, &index)) {
+ return result;
+ }
+ result["region_location"] = region_loc;
+ result["index"] = index;
+ result["map_type"] = p_map_type;
+ return result;
+}
+
+bool Terrain3DData::set_layer_coverage(const Vector2i &p_region_loc, const MapType p_map_type, const int p_index, const Rect2i &p_coverage, const bool p_update) {
+ Terrain3DRegion *region = get_region_ptr(p_region_loc);
+ if (!region) {
+ LOG(WARN, "Cannot set layer coverage. Region not found at ", p_region_loc);
+ return false;
+ }
+ int region_size = region->get_region_size();
+ if (!is_valid_region_size(region_size)) {
+ Ref map = region->get_map(p_map_type);
+ if (map.is_valid()) {
+ region_size = MAX(map->get_width(), map->get_height());
+ }
+ }
+ if (!is_valid_region_size(region_size)) {
+ region_size = _region_size;
+ }
+ if (!is_valid_region_size(region_size)) {
+ LOG(ERROR, "Cannot set layer coverage. Region size unresolved for ", p_region_loc);
+ return false;
+ }
+ TypedArray layers = region->get_layers(p_map_type);
+ if (p_index < 0 || p_index >= layers.size()) {
+ LOG(WARN, "Layer index ", p_index, " out of bounds for region ", p_region_loc);
+ return false;
+ }
+ Ref layer = layers[p_index];
+ if (layer.is_null()) {
+ LOG(WARN, "Layer at index ", p_index, " is null in region ", p_region_loc);
+ return false;
+ }
+ Rect2i coverage = p_coverage;
+ Vector2i coverage_size = coverage.size;
+ if (coverage_size.x <= 0 || coverage_size.y <= 0) {
+ Ref payload = layer->get_payload();
+ if (payload.is_valid()) {
+ coverage_size = payload->get_size();
+ } else {
+ coverage_size = Vector2i(_region_size, _region_size);
+ }
+ coverage.size = coverage_size;
+ }
+ coverage_size.x = CLAMP(coverage_size.x, 1, region_size);
+ coverage_size.y = CLAMP(coverage_size.y, 1, region_size);
+ int max_x = MAX(0, region_size - coverage_size.x);
+ int max_y = MAX(0, region_size - coverage_size.y);
+ Vector2i clamped_pos(
+ CLAMP(coverage.position.x, 0, max_x),
+ CLAMP(coverage.position.y, 0, max_y));
+ coverage.position = clamped_pos;
+ coverage.size = coverage_size;
+ layer->set_coverage(coverage);
+ layer->mark_dirty();
+ region->mark_layers_dirty(p_map_type);
+ region->set_modified(true);
+ region->set_edited(true);
+ if (p_update) {
+ update_maps(p_map_type, false, false);
+ }
+ return true;
+}
+
+bool Terrain3DData::move_stamp_layer(const Ref &p_layer, const Vector3 &p_world_position, const bool p_update) {
+ if (p_layer.is_null()) {
+ LOG(WARN, "Cannot move stamp layer: layer reference is null");
+ return false;
+ }
+ MapType map_type = p_layer->get_map_type();
+ Terrain3DRegion *current_region = nullptr;
+ Vector2i current_loc;
+ int current_index = -1;
+ if (!_find_layer_owner(p_layer, map_type, ¤t_region, ¤t_loc, ¤t_index)) {
+ LOG(WARN, "Cannot move stamp layer: owning region not located");
+ return false;
+ }
+ LOG(DEBUG, "move_stamp_layer: request from region ", current_loc, " index ", current_index,
+ " map_type ", map_type, " world ", p_world_position);
+ Vector2i target_loc = get_region_location(p_world_position);
+ Terrain3DRegion *target_region = get_region_ptr(target_loc);
+ if (!target_region) {
+ LOG(WARN, "Cannot move stamp layer: target region ", target_loc, " not available for world position ", p_world_position);
+ return false;
+ }
+ Rect2i coverage = p_layer->get_coverage();
+ Vector2i coverage_size = coverage.size;
+ if (coverage_size.x <= 0 || coverage_size.y <= 0) {
+ Ref payload = p_layer->get_payload();
+ if (payload.is_valid()) {
+ coverage_size = payload->get_size();
+ } else {
+ coverage_size = Vector2i(_region_size, _region_size);
+ }
+ }
+ if (coverage_size.x <= 0 || coverage_size.y <= 0) {
+ LOG(WARN, "Cannot move stamp layer: invalid coverage dimensions");
+ return false;
+ }
+ coverage.size = coverage_size;
+ Vector2 region_origin = Vector2(target_loc) * real_t(_region_size) * _vertex_spacing;
+ Vector2 local = (Vector2(p_world_position.x, p_world_position.z) - region_origin) / _vertex_spacing;
+ Vector2 offset = Vector2(coverage_size) * 0.5f;
+ Vector2 target_pos_f = local - offset;
+ Vector2i target_pos(
+ int(Math::round(target_pos_f.x)),
+ int(Math::round(target_pos_f.y)));
+ int max_x = MAX(0, _region_size - coverage_size.x);
+ int max_y = MAX(0, _region_size - coverage_size.y);
+ target_pos.x = CLAMP(target_pos.x, 0, max_x);
+ target_pos.y = CLAMP(target_pos.y, 0, max_y);
+ Rect2i target_coverage(target_pos, coverage_size);
+ LOG(DEBUG, "move_stamp_layer: target region ", target_loc, " coverage size ", coverage_size,
+ " target_pos ", target_pos, " region_origin ", region_origin);
+ Ref stamp_layer = p_layer;
+ if (current_region != target_region) {
+ LOG(DEBUG, "move_stamp_layer: transferring layer from region ", current_loc, " to ", target_loc);
+ current_region->remove_layer(map_type, current_index);
+ current_region->mark_layers_dirty(map_type);
+ current_region->set_modified(true);
+ current_region->set_edited(true);
+ Ref stored = target_region->add_layer(map_type, stamp_layer);
+ Ref stored_stamp = stored;
+ if (stored_stamp.is_valid()) {
+ stamp_layer = stored_stamp;
+ }
+ }
+ stamp_layer->set_coverage(target_coverage);
+ stamp_layer->mark_dirty();
+ target_region->mark_layers_dirty(map_type);
+ target_region->set_modified(true);
+ target_region->set_edited(true);
+ LOG(DEBUG, "move_stamp_layer: coverage updated to ", target_coverage, " (map_type ", map_type, ")");
+ if (p_update) {
+ update_maps(map_type, false, false);
+ }
+ return true;
+}
+
+void Terrain3DData::set_layer_enabled(const Vector2i &p_region_loc, const MapType p_map_type, const int p_index, const bool p_enabled, const bool p_update) {
+ Terrain3DRegion *region = get_region_ptr(p_region_loc);
+ if (!region) {
+ LOG(ERROR, "Cannot set layer enabled state. Region not found at ", p_region_loc);
+ return;
+ }
+ TypedArray layers = region->get_layers(p_map_type);
+ if (p_index < 0 || p_index >= layers.size()) {
+ LOG(WARN, "Layer index ", p_index, " out of bounds for region ", p_region_loc);
+ return;
+ }
+ Ref layer = layers[p_index];
+ if (layer.is_null()) {
+ LOG(WARN, "Layer at index ", p_index, " is null in region ", p_region_loc);
+ return;
+ }
+ if (layer->is_enabled() == p_enabled) {
+ return;
+ }
+ layer->set_enabled(p_enabled);
+ region->mark_layers_dirty(p_map_type);
+ region->set_modified(true);
+ region->set_edited(true);
+ if (p_update) {
+ update_maps(p_map_type, false, false);
+ }
+}
+
+void Terrain3DData::remove_layer(const Vector2i &p_region_loc, const MapType p_map_type, const int p_index, const bool p_update) {
+ Terrain3DRegion *region = get_region_ptr(p_region_loc);
+ if (!region) {
+ LOG(ERROR, "Cannot remove layer. Region not found at ", p_region_loc);
+ return;
+ }
+ region->remove_layer(p_map_type, p_index);
+ region->set_modified(true);
+ region->set_edited(true);
+ if (p_update) {
+ update_maps(p_map_type, false, false);
+ }
+}
+
+Ref Terrain3DData::set_map_layer(const Vector2i &p_region_loc, const MapType p_map_type, const Ref &p_image, const uint64_t p_external_id, const bool p_update) {
+ // Validate input data
+ if (p_image.is_null()) {
+ LOG(ERROR, "set_map_layer: Input image is null");
+ return Ref();
+ }
+
+ if (p_image->get_width() <= 0 || p_image->get_height() <= 0) {
+ LOG(ERROR, "set_map_layer: Input image has invalid dimensions: ",
+ Vector2i(p_image->get_width(), p_image->get_height()));
+ return Ref();
+ }
+
+ // Validate image format matches the MapType
+ Image::Format expected_format = map_type_get_format(p_map_type);
+ Image::Format actual_format = p_image->get_format();
+
+ if (actual_format != expected_format) {
+ LOG(ERROR, "set_map_layer: Image format mismatch. Expected ",
+ expected_format, " for ", map_type_get_string(p_map_type),
+ " but got ", actual_format);
+ return Ref();
+ }
+
+ Terrain3DRegion *region = get_region_ptr(p_region_loc);
+ if (!region) {
+ LOG(ERROR, "set_map_layer: Region not found at ", p_region_loc);
+ return Ref();
+ }
+
+ Ref layer;
+
+ // Check if we have an existing external layer for this external_id
+ if (p_external_id != 0 && _external_layers.has(p_external_id)) {
+ Dictionary layer_info = _external_layers[p_external_id];
+ Vector2i stored_loc = layer_info.get("region_loc", Vector2i());
+ MapType stored_type = MapType(int(layer_info.get("map_type", TYPE_HEIGHT)));
+
+ // Verify the region and map type match
+ if (stored_loc == p_region_loc && stored_type == p_map_type) {
+ layer = layer_info.get("layer", Ref());
+ if (layer.is_valid()) {
+ // Update existing layer
+ layer->set_payload(p_image);
+ layer->set_coverage(Rect2i(Vector2i(), Vector2i(p_image->get_width(), p_image->get_height())));
+
+ // Mark dirty for the entire coverage area
+ // Thread-safe: mark_layers_dirty uses atomic-safe operations for dirty flags
+ region->mark_layers_dirty(p_map_type, true);
+ region->set_modified(true);
+ region->set_edited(true);
+
+ if (p_update) {
+ update_maps(p_map_type, false, false);
+ }
+
+ LOG(DEBUG, "set_map_layer: Updated existing external layer ", p_external_id,
+ " in region ", p_region_loc, " for ", map_type_get_string(p_map_type));
+
+ return layer;
+ }
+ } else {
+ LOG(WARN, "set_map_layer: External ID ", p_external_id,
+ " registered for different region/type. Creating new layer.");
+ }
+ }
+
+ // Create new layer
+ layer.instantiate();
+ if (layer.is_null()) {
+ LOG(ERROR, "set_map_layer: Failed to instantiate Terrain3DStampLayer");
+ return Ref();
+ }
+
+ // Configure the layer as non-user-editable (can only be modified by external tools)
+ layer->set_user_editable(false);
+ layer->set_map_type(p_map_type);
+ layer->set_payload(p_image);
+ layer->set_coverage(Rect2i(Vector2i(), Vector2i(p_image->get_width(), p_image->get_height())));
+ layer->set_blend_mode(Terrain3DLayer::BLEND_REPLACE); // Default to replace mode like set_map()
+ layer->set_intensity(1.0f);
+ layer->set_enabled(true);
+
+ // Add the layer to the region
+ Ref added_layer = region->add_layer(p_map_type, layer, -1);
+ if (added_layer.is_null()) {
+ LOG(ERROR, "set_map_layer: Failed to add layer to region");
+ return Ref();
+ }
+
+ // Track this external layer if external_id is provided
+ if (p_external_id != 0) {
+ Dictionary layer_info;
+ layer_info["region_loc"] = p_region_loc;
+ layer_info["layer"] = layer;
+ layer_info["map_type"] = int(p_map_type);
+ _external_layers[p_external_id] = layer_info;
+ }
+
+ region->set_modified(true);
+ region->set_edited(true);
+
+ if (p_update) {
+ update_maps(p_map_type, false, false);
+ }
+
+ LOG(DEBUG, "set_map_layer: Created new external layer ",
+ (p_external_id != 0 ? String::num_uint64(p_external_id) : String("(no ID)")),
+ " in region ", p_region_loc, " for ", map_type_get_string(p_map_type));
+
+ return layer;
+}
+
+bool Terrain3DData::release_map_layer(const uint64_t p_external_id, const bool p_remove_layer, const bool p_update) {
+ if (p_external_id == 0) {
+ LOG(WARN, "release_map_layer: external_id must be non-zero");
+ return false;
+ }
+ if (!_external_layers.has(p_external_id)) {
+ LOG(WARN, "release_map_layer: External ID ", p_external_id, " is not registered");
+ return false;
+ }
+ Dictionary layer_info = _external_layers[p_external_id];
+ _external_layers.erase(p_external_id);
+
+ if (!p_remove_layer) {
+ LOG(DEBUG, "release_map_layer: Detached tracking for external layer ", p_external_id);
+ return true;
+ }
+
+ Ref layer = layer_info.get("layer", Ref());
+ if (layer.is_null()) {
+ LOG(WARN, "release_map_layer: Layer reference missing for external ID ", p_external_id);
+ return false;
+ }
+ Vector2i region_loc = layer_info.get("region_loc", Vector2i());
+ MapType map_type = MapType(int(layer_info.get("map_type", TYPE_HEIGHT)));
+ Terrain3DRegion *region = get_region_ptr(region_loc);
+ if (!region) {
+ LOG(WARN, "release_map_layer: Region not found at ", region_loc, " for external ID ", p_external_id);
+ return false;
+ }
+ TypedArray layers = region->get_layers(map_type);
+ for (int i = 0; i < layers.size(); i++) {
+ if (layers[i] == layer) {
+ region->remove_layer(map_type, i);
+ region->set_modified(true);
+ region->set_edited(true);
+ if (p_update) {
+ update_maps(map_type, false, false);
+ }
+ LOG(DEBUG, "release_map_layer: Removed external layer ", p_external_id, " from region ", region_loc, " (", map_type_get_string(map_type), ")");
+ return true;
+ }
+ }
+
+ LOG(WARN, "release_map_layer: Layer not found in region stack for external ID ", p_external_id);
+ return false;
+}
+
void Terrain3DData::update_maps(const MapType p_map_type, const bool p_all_regions, const bool p_generate_mipmaps) {
// Generate region color mipmaps
if (p_generate_mipmaps && (p_map_type == TYPE_COLOR || p_map_type == TYPE_MAX)) {
@@ -487,8 +1262,9 @@ void Terrain3DData::update_maps(const MapType p_map_type, const bool p_all_regio
_region_locations = TypedArray(); // enforce new pointer
Array locs = _regions.keys();
int region_id = 0;
- for (const Vector2i ®ion_loc : locs) {
- const Terrain3DRegion *region = get_region_ptr(region_loc);
+ for (int i = 0; i < locs.size(); i++) {
+ Vector2i region_loc = locs[i];
+ Terrain3DRegion *region = get_region_ptr(region_loc);
if (region && !region->is_deleted()) {
region_id += 1; // Begin at 1 since 0 = no region
int map_index = get_region_map_index(region_loc);
@@ -507,10 +1283,11 @@ void Terrain3DData::update_maps(const MapType p_map_type, const bool p_all_regio
if (_generated_height_maps.is_dirty()) {
LOG(EXTREME, "Regenerating height texture array from regions");
_height_maps.clear();
- for (const Vector2i ®ion_loc : _region_locations) {
- const Terrain3DRegion *region = get_region_ptr(region_loc);
+ for (int i = 0; i < _region_locations.size(); i++) {
+ Vector2i region_loc = _region_locations[i];
+ Terrain3DRegion *region = get_region_ptr(region_loc);
if (region) {
- _height_maps.push_back(region->get_height_map());
+ _height_maps.push_back(region->get_composited_map(TYPE_HEIGHT));
} else {
LOG(ERROR, "Can't find region ", region_loc, ", _regions: ", _regions,
", locations: ", _region_locations, ". Please report this error.");
@@ -520,7 +1297,6 @@ void Terrain3DData::update_maps(const MapType p_map_type, const bool p_all_regio
_generated_height_maps.create(_height_maps);
calc_height_range();
any_changed = true;
- LOG(DEBUG, "Emitting height_maps_changed");
emit_signal("height_maps_changed");
}
@@ -528,15 +1304,15 @@ void Terrain3DData::update_maps(const MapType p_map_type, const bool p_all_regio
if (_generated_control_maps.is_dirty()) {
LOG(EXTREME, "Regenerating control texture array from regions");
_control_maps.clear();
- for (const Vector2i ®ion_loc : _region_locations) {
- const Terrain3DRegion *region = get_region_ptr(region_loc);
+ for (int i = 0; i < _region_locations.size(); i++) {
+ Vector2i region_loc = _region_locations[i];
+ Terrain3DRegion *region = get_region_ptr(region_loc);
if (region) {
- _control_maps.push_back(region->get_control_map());
+ _control_maps.push_back(region->get_composited_map(TYPE_CONTROL));
}
}
_generated_control_maps.create(_control_maps);
any_changed = true;
- LOG(DEBUG, "Emitting control_maps_changed");
emit_signal("control_maps_changed");
}
@@ -544,60 +1320,65 @@ void Terrain3DData::update_maps(const MapType p_map_type, const bool p_all_regio
if (_generated_color_maps.is_dirty()) {
LOG(EXTREME, "Regenerating color texture array from regions");
_color_maps.clear();
- for (const Vector2i ®ion_loc : _region_locations) {
- const Terrain3DRegion *region = get_region_ptr(region_loc);
+ for (int i = 0; i < _region_locations.size(); i++) {
+ Vector2i region_loc = _region_locations[i];
+ Terrain3DRegion *region = get_region_ptr(region_loc);
if (region) {
- _color_maps.push_back(region->get_color_map());
+ _color_maps.push_back(region->get_composited_map(TYPE_COLOR));
}
}
_generated_color_maps.create(_color_maps);
any_changed = true;
- LOG(DEBUG, "Emitting color_maps_changed");
emit_signal("color_maps_changed");
}
// If no maps have been rebuilt, update only individual regions in the array.
// Regions marked Edited have been changed by Terrain3DEditor::_operate_map or undo / redo processing.
if (!any_changed) {
- for (const Vector2i ®ion_loc : _region_locations) {
- const Terrain3DRegion *region = get_region_ptr(region_loc);
+ for (int i = 0; i < _region_locations.size(); i++) {
+ Vector2i region_loc = _region_locations[i];
+ Terrain3DRegion *region = get_region_ptr(region_loc);
if (region && region->is_edited()) {
int region_id = get_region_id(region_loc);
switch (p_map_type) {
case TYPE_HEIGHT:
- _generated_height_maps.update(region->get_height_map(), region_id);
- LOG(DEBUG, "Emitting height_maps_changed");
+ {
+ Ref height_map = region->get_composited_map(TYPE_HEIGHT);
+ _generated_height_maps.update(height_map, region_id);
+ region->update_height_range_from_image(height_map);
+ update_master_heights(region->get_height_range());
+ }
emit_signal("height_maps_changed");
break;
case TYPE_CONTROL:
- _generated_control_maps.update(region->get_control_map(), region_id);
- LOG(DEBUG, "Emitting control_maps_changed");
+ _generated_control_maps.update(region->get_composited_map(TYPE_CONTROL), region_id);
emit_signal("control_maps_changed");
break;
case TYPE_COLOR:
- _generated_color_maps.update(region->get_color_map(), region_id);
- LOG(DEBUG, "Emitting color_maps_changed");
+ _generated_color_maps.update(region->get_composited_map(TYPE_COLOR), region_id);
emit_signal("color_maps_changed");
break;
default:
- _generated_height_maps.update(region->get_height_map(), region_id);
- _generated_control_maps.update(region->get_control_map(), region_id);
- _generated_color_maps.update(region->get_color_map(), region_id);
- LOG(DEBUG, "Emitting height_maps_changed");
+ {
+ Ref height_map = region->get_composited_map(TYPE_HEIGHT);
+ _generated_height_maps.update(height_map, region_id);
+ region->update_height_range_from_image(height_map);
+ update_master_heights(region->get_height_range());
+ }
+ _generated_control_maps.update(region->get_composited_map(TYPE_CONTROL), region_id);
+ _generated_color_maps.update(region->get_composited_map(TYPE_COLOR), region_id);
emit_signal("height_maps_changed");
- LOG(DEBUG, "Emitting control_maps_changed");
emit_signal("control_maps_changed");
- LOG(DEBUG, "Emitting color_maps_changed");
emit_signal("color_maps_changed");
break;
}
+ region->set_edited(false);
}
}
}
if (any_changed) {
- LOG(DEBUG, "Emitting maps_changed");
emit_signal("maps_changed");
- _terrain->snap();
+ _terrain->request_mesh_snap();
}
}
@@ -644,12 +1425,12 @@ Color Terrain3DData::get_pixel(const MapType p_map_type, const Vector3 &p_global
Vector3 descaled_pos = p_global_position / _vertex_spacing;
Vector2i img_pos = Vector2i(descaled_pos.x - global_offset.x, descaled_pos.z - global_offset.y);
img_pos = img_pos.clamp(V2I_ZERO, V2I(_region_size - 1));
- Image *map = region->get_map_ptr(p_map_type);
- if (map) {
- return map->get_pixelv(img_pos);
- } else {
- return COLOR_NAN;
+ Ref composite = region->get_composited_map(p_map_type);
+ if (composite.is_valid()) {
+ return composite->get_pixelv(img_pos);
}
+ Image *map = region->get_map_ptr(p_map_type);
+ return map ? map->get_pixelv(img_pos) : COLOR_NAN;
}
real_t Terrain3DData::get_height(const Vector3 &p_global_position) const {
@@ -823,7 +1604,6 @@ void Terrain3DData::add_edited_area(const AABB &p_area) {
} else {
_edited_area = p_area;
}
- LOG(DEBUG, "Emitting maps_edited");
emit_signal("maps_edited", p_area);
}
@@ -1101,9 +1881,18 @@ Ref Terrain3DData::layered_to_image(const MapType p_map_type) const {
Vector2i img_location = (region_loc - top_left) * _region_size;
LOG(DEBUG, "Region to blit: ", region_loc, " Export image coords: ", img_location);
const Terrain3DRegion *region = get_region_ptr(region_loc);
- if (region) {
- img->blit_rect(region->get_map(map_type), Rect2i(V2I_ZERO, _region_sizev), img_location);
+ if (!region) {
+ continue;
+ }
+ Ref source = region->get_composited_map(map_type);
+ if (source.is_null()) {
+ source = region->get_map(map_type);
}
+ if (source.is_null()) {
+ continue;
+ }
+ Rect2i src_rect = Rect2i(V2I_ZERO, source->get_size());
+ img->blit_rect(source, src_rect, img_location);
}
return img;
}
@@ -1186,6 +1975,20 @@ void Terrain3DData::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_control_maps"), &Terrain3DData::get_control_maps);
ClassDB::bind_method(D_METHOD("get_color_maps"), &Terrain3DData::get_color_maps);
ClassDB::bind_method(D_METHOD("get_maps", "map_type"), &Terrain3DData::get_maps);
+ ClassDB::bind_method(D_METHOD("get_layers", "region_location", "map_type"), &Terrain3DData::get_layers);
+ ClassDB::bind_method(D_METHOD("get_layer_groups", "map_type"), &Terrain3DData::get_layer_groups);
+ ClassDB::bind_method(D_METHOD("ensure_layer_group_id", "layer"), &Terrain3DData::ensure_layer_group_id);
+ ClassDB::bind_method(D_METHOD("add_layer", "region_location", "map_type", "layer", "index", "update"), &Terrain3DData::add_layer, DEFVAL(-1), DEFVAL(true));
+ ClassDB::bind_method(D_METHOD("add_stamp_layer", "region_location", "map_type", "payload", "coverage", "alpha", "intensity", "feather_radius", "blend_mode", "index", "update"), &Terrain3DData::add_stamp_layer, DEFVAL(Ref()), DEFVAL(1.0f), DEFVAL(0.0f), DEFVAL(Terrain3DLayer::BLEND_ADD), DEFVAL(-1), DEFVAL(true));
+ ClassDB::bind_method(D_METHOD("add_stamp_layer_global", "global_coverage", "map_type", "payload", "alpha", "intensity", "feather_radius", "blend_mode", "auto_create_regions", "update"), &Terrain3DData::add_stamp_layer_global, DEFVAL(Ref()), DEFVAL(1.0f), DEFVAL(0.0f), DEFVAL(Terrain3DLayer::BLEND_ADD), DEFVAL(true), DEFVAL(true));
+ ClassDB::bind_method(D_METHOD("split_layer_payload_global_data", "global_coverage", "payload", "alpha"), &Terrain3DData::split_layer_payload_global_data, DEFVAL(Ref()));
+ ClassDB::bind_method(D_METHOD("get_layer_owner_info", "layer", "map_type"), &Terrain3DData::get_layer_owner_info);
+ ClassDB::bind_method(D_METHOD("set_layer_coverage", "region_location", "map_type", "index", "coverage", "update"), &Terrain3DData::set_layer_coverage, DEFVAL(true));
+ ClassDB::bind_method(D_METHOD("move_stamp_layer", "layer", "world_position", "update"), &Terrain3DData::move_stamp_layer, DEFVAL(true));
+ ClassDB::bind_method(D_METHOD("set_layer_enabled", "region_location", "map_type", "index", "enabled", "update"), &Terrain3DData::set_layer_enabled, DEFVAL(true));
+ ClassDB::bind_method(D_METHOD("remove_layer", "region_location", "map_type", "index", "update"), &Terrain3DData::remove_layer, DEFVAL(true));
+ ClassDB::bind_method(D_METHOD("set_map_layer", "region_location", "map_type", "image", "external_id", "update"), &Terrain3DData::set_map_layer, DEFVAL(0), DEFVAL(true));
+ ClassDB::bind_method(D_METHOD("release_map_layer", "external_id", "remove_layer", "update"), &Terrain3DData::release_map_layer, DEFVAL(true), DEFVAL(true));
ClassDB::bind_method(D_METHOD("update_maps", "map_type", "all_regions", "generate_mipmaps"), &Terrain3DData::update_maps, DEFVAL(TYPE_MAX), DEFVAL(true), DEFVAL(false));
ClassDB::bind_method(D_METHOD("get_height_maps_rid"), &Terrain3DData::get_height_maps_rid);
ClassDB::bind_method(D_METHOD("get_control_maps_rid"), &Terrain3DData::get_control_maps_rid);
@@ -1244,4 +2047,4 @@ void Terrain3DData::_bind_methods() {
ADD_SIGNAL(MethodInfo("control_maps_changed"));
ADD_SIGNAL(MethodInfo("color_maps_changed"));
ADD_SIGNAL(MethodInfo("maps_edited", PropertyInfo(Variant::AABB, "edited_area")));
-}
+}
\ No newline at end of file
diff --git a/src/terrain_3d_data.h b/src/terrain_3d_data.h
index c9347b9df..44370a732 100644
--- a/src/terrain_3d_data.h
+++ b/src/terrain_3d_data.h
@@ -6,8 +6,14 @@
#include "constants.h"
#include "generated_texture.h"
#include "terrain_3d.h"
+#include "terrain_3d_layer.h"
#include "terrain_3d_region.h"
+#include
+
+#include
+#include
+
class Terrain3D;
class Terrain3DData : public Object {
@@ -74,11 +80,33 @@ class Terrain3DData : public Object {
GeneratedTexture _generated_control_maps;
GeneratedTexture _generated_color_maps;
+ uint64_t _next_layer_group_id = 1;
+
+ // Track external tool layers by external_id -> {region_loc, layer_ref}
+ // Used to find and update existing layers created by external tools
+ Dictionary _external_layers; // Dict[uint64_t external_id] -> Dictionary{"region_loc": Vector2i, "layer": Ref, "map_type": MapType}
+
// Functions
void _clear();
void _copy_paste_dfr(const Terrain3DRegion *p_src_region, const Rect2i &p_src_rect, const Rect2i &p_dst_rect, const Terrain3DRegion *p_dst_region);
+ bool _find_layer_owner(const Ref &p_layer, const MapType p_map_type, Terrain3DRegion **r_region = nullptr, Vector2i *r_region_loc = nullptr, int *r_index = nullptr) const;
+ uint64_t _allocate_layer_group_id();
+ uint64_t _ensure_layer_group_id_internal(const Ref &p_layer);
+ void _ensure_region_layer_groups(const Ref &p_region);
+ Ref _duplicate_layer_template(const Ref &p_template) const;
public:
+ uint64_t ensure_layer_group_id(const Ref &p_layer);
+ TypedArray get_layer_groups(const MapType p_map_type);
+
+ struct LayerSplitResult {
+ Vector2i region_location;
+ Rect2i coverage;
+ Ref payload;
+ Ref alpha;
+ };
+ typedef std::vector LayerSplitResults;
+
Terrain3DData() {}
void initialize(Terrain3D *p_terrain);
~Terrain3DData() { _clear(); }
@@ -132,6 +160,30 @@ class Terrain3DData : public Object {
TypedArray get_color_maps() const { return _color_maps; }
TypedArray get_maps(const MapType p_map_type) const;
void update_maps(const MapType p_map_type = TYPE_MAX, const bool p_all_regions = true, const bool p_generate_mipmaps = false);
+ TypedArray get_layers(const Vector2i &p_region_loc, const MapType p_map_type) const;
+ Ref add_layer(const Vector2i &p_region_loc, const MapType p_map_type, const Ref