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 &p_layer, const int p_index = -1, const bool p_update = true); + Ref 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 = Ref(), const real_t p_intensity = 1.0f, const real_t p_feather_radius = 0.0f, const Terrain3DLayer::BlendMode p_blend_mode = Terrain3DLayer::BLEND_ADD, const int p_index = -1, const bool p_update = true); + TypedArray add_stamp_layer_global(const Rect2i &p_global_coverage, const MapType p_map_type, const Ref &p_payload, const Ref &p_alpha = Ref(), const real_t p_intensity = 1.0f, const real_t p_feather_radius = 0.0f, const Terrain3DLayer::BlendMode p_blend_mode = Terrain3DLayer::BLEND_ADD, const bool p_auto_create_regions = true, const bool p_update = true); + LayerSplitResults split_layer_payload_global(const Rect2i &p_global_coverage, const Ref &p_payload, const Ref &p_alpha = Ref()) const; + TypedArray split_layer_payload_global_data(const Rect2i &p_global_coverage, const Ref &p_payload, const Ref &p_alpha = Ref()) const; + Dictionary get_layer_owner_info(const Ref &p_layer, const MapType p_map_type) const; + bool 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 = true); + bool move_stamp_layer(const Ref &p_layer, const Vector3 &p_world_position, const bool p_update = true); + void 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 = true); + void remove_layer(const Vector2i &p_region_loc, const MapType p_map_type, const int p_index, const bool p_update = true); + Ref get_layer_in_group(const Vector2i &p_region_loc, const MapType p_map_type, const uint64_t p_group_id, int *r_index = nullptr); + Ref 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 = true); + + // External tool integration - non-destructive layer writing + // Primary entry point for procedural tools (like funofabot's terrain3d-tools) to commit their final results + // to the non-destructive layer stack. Similar to set_map() but writes to a layer instead of + // absolute override. Creates or updates a non-user-editable layer that can only be modified + // by external tools, preventing accidental edits from the Terrain3D editor. + // Thread-safe: Uses atomic-safe dirty flag updates for background thread compatibility. + Ref set_map_layer(const Vector2i &p_region_loc, const MapType p_map_type, const Ref &p_image, const uint64_t p_external_id = 0, const bool p_update = true); + // Allows external tools to drop or recycle tracked layer IDs before creating new map layers. + bool release_map_layer(const uint64_t p_external_id, const bool p_remove_layer = true, const bool p_update = true); + RID get_height_maps_rid() const { return _generated_height_maps.get_rid(); } RID get_control_maps_rid() const { return _generated_control_maps.get_rid(); } RID get_color_maps_rid() const { return _generated_color_maps.get_rid(); } @@ -409,4 +461,4 @@ inline void Terrain3DData::update_master_heights(const Vector2 &p_low_high) { } } -#endif // TERRAIN3D_DATA_CLASS_H +#endif // TERRAIN3D_DATA_CLASS_H \ No newline at end of file diff --git a/src/terrain_3d_editor.cpp b/src/terrain_3d_editor.cpp index 4abf0e281..6f59e2a39 100644 --- a/src/terrain_3d_editor.cpp +++ b/src/terrain_3d_editor.cpp @@ -1,14 +1,30 @@ // Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +#include +#include +#include +#include + #include +#include #include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include #include "constants.h" #include "logger.h" #include "terrain_3d.h" #include "terrain_3d_data.h" #include "terrain_3d_editor.h" +#include "terrain_3d_layer.h" #include "terrain_3d_util.h" /////////////////////////// @@ -93,6 +109,8 @@ void Terrain3DEditor::_operate_map(const Vector3 &p_global_position, const real_ LOG(ERROR, "Invalid tool selected"); return; } + bool has_group_handle = (_active_layer_group_id != 0 && _active_layer_map_type == map_type); + bool paint_to_layer = has_group_handle || (map_type == TYPE_HEIGHT && _active_layer_index > 0); int region_size = _terrain->get_region_size(); Vector2i region_vsize = V2I(region_size); @@ -151,6 +169,15 @@ void Terrain3DEditor::_operate_map(const Vector3 &p_global_position, const real_ real_t gamma = _brush_data["gamma"]; PackedVector3Array gradient_points = _brush_data["gradient_points"]; + bool brush_auto_alpha_enabled = _brush_data.get("brush_auto_alpha_enabled", true); + bool brush_auto_alpha_from_border = _brush_data.get("brush_auto_alpha_from_border", true); + real_t brush_manual_neutral = real_t(_brush_data.get("brush_manual_neutral_value", 0.f)); + real_t brush_alpha_gain = CLAMP(real_t(_brush_data.get("brush_alpha_gain", 1.f)), 0.f, 10.f); + real_t brush_alpha_min_threshold = CLAMP(real_t(_brush_data.get("brush_alpha_min_threshold", 0.001f)), 0.f, 1.f); + real_t brush_alpha_neutral = brush_manual_neutral; + if (brush_auto_alpha_from_border) { + brush_alpha_neutral = real_t(_brush_data.get("brush_alpha_neutral_value", brush_manual_neutral)); + } real_t randf = UtilityFunctions::randf(); real_t rot = randf * Math_PI * real_t(_brush_data["brush_spin_speed"]); @@ -185,6 +212,34 @@ void Terrain3DEditor::_operate_map(const Vector3 &p_global_position, const real_ // need to track if _added_removed_locations has changed between now and the end of the loop int regions_added_removed = _added_removed_locations.size(); + struct LayerContext { + Ref region; + Ref layer; + Ref payload; + Ref composite; + Image *payload_ptr = nullptr; + Image *composite_ptr = nullptr; + Image::Format expected_format = Image::FORMAT_MAX; + int expected_width = 0; + int expected_height = 0; + bool initialized = false; + bool valid = false; + bool marked_dirty = false; + bool payload_rebuilt = false; + bool composite_is_cache = false; + bool force_base_fallback = false; + }; + std::unordered_map layer_contexts; + struct DirtyRegionInfo { + Ref region; + int min_x = INT_MAX; + int min_y = INT_MAX; + int max_x = INT_MIN; + int max_y = INT_MIN; + bool has_writes = false; + }; + std::unordered_map base_dirty_regions; + for (real_t x = 0.f; x < brush_size; x += vertex_spacing) { for (real_t y = 0.f; y < brush_size; y += vertex_spacing) { Vector2 brush_offset = Vector2(x, y) - (V2(brush_size) * .5f); @@ -205,6 +260,214 @@ void Terrain3DEditor::_operate_map(const Vector3 &p_global_position, const real_ if (!map) { continue; } + LayerContext *ctx_ptr = nullptr; + if (paint_to_layer) { + auto ctx_it = layer_contexts.find(region_loc); + if (ctx_it == layer_contexts.end()) { + LayerContext ctx; + ctx.region = region; + ctx.initialized = true; + ctx.expected_format = map->get_format(); + int map_width = map->get_width(); + int map_height = map->get_height(); + if (map_width <= 0) { + map_width = region_size; + } + if (map_height <= 0) { + map_height = region_size; + } + ctx.expected_width = map_width; + ctx.expected_height = map_height; + if (ctx.expected_format == Image::FORMAT_MAX) { + ctx.expected_format = map_type_get_format(map_type); + } + ctx.payload_rebuilt = false; + ctx.composite_is_cache = false; + + TypedArray target_layers = region->get_layers(map_type); + int layer_array_index = _active_layer_index - 1; + Ref candidate_layer; + if (has_group_handle) { + int group_layer_index = -1; + candidate_layer = data->get_layer_in_group(region_loc, map_type, _active_layer_group_id, &group_layer_index); + if (candidate_layer.is_null() && _active_layer_template.is_valid()) { + candidate_layer = data->create_layer_group_slice(region_loc, map_type, _active_layer_group_id, _active_layer_template, false); + if (candidate_layer.is_valid()) { + target_layers = region->get_layers(map_type); + layer_array_index = target_layers.size() - 1; + } + } else { + layer_array_index = group_layer_index; + } + if (candidate_layer.is_null()) { + static int missing_group_slice_log_count = 0; + if (missing_group_slice_log_count < 5) { + LOG(WARN, "Active layer group ", _active_layer_group_id, " unavailable in region ", region_loc, "; skipping slice"); + missing_group_slice_log_count++; + } + } + } else { + if (layer_array_index < 0 || layer_array_index >= target_layers.size()) { + static int missing_layer_log_count = 0; + if (missing_layer_log_count < 5) { + LOG(WARN, "Cannot edit layer index ", _active_layer_index, " (array index ", layer_array_index, + ") in region ", region_loc, ": available layers=", target_layers.size()); + missing_layer_log_count++; + } + } else { + candidate_layer = target_layers[layer_array_index]; + } + } + if (candidate_layer.is_null()) { + // Already logged above. + } else if (!candidate_layer->is_user_editable()) { + static int locked_layer_log_count = 0; + if (locked_layer_log_count < 5) { + LOG(WARN, "Layer ", _active_layer_index, " in region ", region_loc, " is locked for editing; falling back to base map"); + locked_layer_log_count++; + } + candidate_layer.unref(); + ctx.force_base_fallback = true; + } else if (!candidate_layer->is_enabled()) { + ctx.layer = candidate_layer; + } else { + int expected_width = map->get_width(); + int expected_height = map->get_height(); + if (expected_width <= 0) { + expected_width = region_size; + } + if (expected_height <= 0) { + expected_height = region_size; + } + if (expected_width <= 0 || expected_height <= 0) { + static int empty_map_log_count = 0; + if (empty_map_log_count < 5) { + LOG(WARN, "Region ", region_loc, " map dimensions invalid when preparing layer payload: ", expected_width, "x", expected_height); + empty_map_log_count++; + } + } else { + Ref payload = candidate_layer->get_payload(); + if (ctx.expected_width <= 0) { + ctx.expected_width = expected_width; + } + if (ctx.expected_height <= 0) { + ctx.expected_height = expected_height; + } + if (ctx.expected_format == Image::FORMAT_MAX) { + ctx.expected_format = map_type_get_format(map_type); + } + bool payload_ready = payload.is_valid() && payload->get_width() == ctx.expected_width && payload->get_height() == ctx.expected_height; + if (!payload_ready) { + Ref new_payload; + new_payload.instantiate(); + new_payload->create(ctx.expected_width, ctx.expected_height, false, ctx.expected_format); + new_payload->fill(Color(0.0f, 0.0f, 0.0f, 1.0f)); + candidate_layer->set_payload(new_payload); + candidate_layer->set_coverage(Rect2i(Vector2i(), Vector2i(ctx.expected_width, ctx.expected_height))); + payload = candidate_layer->get_payload(); + } + if (payload.is_valid()) { + ctx.layer = candidate_layer; + ctx.payload = payload; + ctx.payload_ptr = payload.ptr(); + ctx.payload_rebuilt = false; + ctx.composite = region->get_composited_map(map_type); + if (ctx.composite.is_valid() && ctx.composite->get_width() == ctx.expected_width && ctx.composite->get_height() == ctx.expected_height) { + ctx.composite_ptr = ctx.composite.ptr(); + ctx.valid = true; + ctx.composite_is_cache = true; + } else { + if (ctx.composite.is_valid()) { + static int mismatched_composite_log_count = 0; + if (mismatched_composite_log_count < 5) { + LOG(WARN, "Composited map dimensions ", ctx.composite->get_width(), "x", ctx.composite->get_height(), + " did not match expected ", ctx.expected_width, "x", ctx.expected_height, " for region ", region_loc); + mismatched_composite_log_count++; + } + } + ctx.composite_ptr = map; + ctx.valid = true; + ctx.composite_is_cache = false; + } + } + } + } + auto insert_result = layer_contexts.emplace(region_loc, ctx); + ctx_ptr = &insert_result.first->second; + } else { + ctx_ptr = &ctx_it->second; + if (!ctx_ptr->region.is_valid()) { + ctx_ptr->region = region; + } + if (ctx_ptr->expected_format == Image::FORMAT_MAX) { + ctx_ptr->expected_format = map_type_get_format(map_type); + } + if (ctx_ptr->expected_format == Image::FORMAT_MAX) { + Image::Format map_format = map->get_format(); + ctx_ptr->expected_format = map_format != Image::FORMAT_MAX ? map_format : map_type_get_format(map_type); + } + if (ctx_ptr->expected_width <= 0) { + int current_width = map->get_width(); + ctx_ptr->expected_width = current_width > 0 ? current_width : region_size; + } + if (ctx_ptr->expected_height <= 0) { + int current_height = map->get_height(); + ctx_ptr->expected_height = current_height > 0 ? current_height : region_size; + } + if (ctx_ptr->layer.is_valid()) { + Ref refreshed_payload = ctx_ptr->layer->get_payload(); + if (refreshed_payload.is_valid()) { + ctx_ptr->payload = refreshed_payload; + ctx_ptr->payload_ptr = refreshed_payload.ptr(); + } + } + if (ctx_ptr->region.is_valid()) { + Ref refreshed_composite = ctx_ptr->region->get_composited_map(map_type); + if (refreshed_composite.is_valid()) { + ctx_ptr->composite = refreshed_composite; + ctx_ptr->composite_ptr = refreshed_composite.ptr(); + ctx_ptr->composite_is_cache = true; + } + } + } + } + bool using_layer = paint_to_layer && ctx_ptr && ctx_ptr->valid && ctx_ptr->payload_ptr && ctx_ptr->composite_ptr; + Image *layer_payload_ptr = nullptr; + Image *composited_map_ptr = nullptr; + if (using_layer) { + int payload_width = ctx_ptr->payload_ptr->get_width(); + int payload_height = ctx_ptr->payload_ptr->get_height(); + if ((payload_width <= 0 || payload_height <= 0) && !ctx_ptr->payload_rebuilt && ctx_ptr->expected_width > 0 && ctx_ptr->expected_height > 0) { + Image::Format fmt = ctx_ptr->expected_format == Image::FORMAT_MAX ? map_type_get_format(map_type) : ctx_ptr->expected_format; + ctx_ptr->payload_ptr->create(ctx_ptr->expected_width, ctx_ptr->expected_height, false, fmt); + ctx_ptr->payload_ptr->fill(Color(0.0f, 0.0f, 0.0f, 1.0f)); + ctx_ptr->payload_rebuilt = true; + ctx_ptr->payload = ctx_ptr->layer.is_valid() ? ctx_ptr->layer->get_payload() : ctx_ptr->payload; + if (ctx_ptr->payload.is_valid()) { + ctx_ptr->payload_ptr = ctx_ptr->payload.ptr(); + } + payload_width = ctx_ptr->payload_ptr->get_width(); + payload_height = ctx_ptr->payload_ptr->get_height(); + if (ctx_ptr->layer.is_valid()) { + Rect2i coverage = ctx_ptr->layer->get_coverage(); + if (coverage.size.x <= 0 || coverage.size.y <= 0 || coverage.position != Vector2i()) { + ctx_ptr->layer->set_coverage(Rect2i(Vector2i(), Vector2i(ctx_ptr->expected_width, ctx_ptr->expected_height))); + } + } + } + if (payload_width <= 0 || payload_height <= 0) { + static int empty_payload_log_count = 0; + if (empty_payload_log_count < 5) { + LOG(WARN, "Layer payload dimensions invalid (", payload_width, "x", payload_height, + ") in region ", region_loc, ". Falling back to base layer"); + empty_payload_log_count++; + } + using_layer = false; + } else { + layer_payload_ptr = ctx_ptr->payload_ptr; + composited_map_ptr = ctx_ptr->composite_ptr; + } + } // Identify position on map image Vector2 uv_position = _get_uv_position(brush_global_position, region_size, vertex_spacing); @@ -224,14 +487,51 @@ void Terrain3DEditor::_operate_map(const Vector3 &p_global_position, const real_ edited_area = edited_area.expand(edited_position); // Start brushing on the map - real_t brush_alpha = brush_image->get_pixelv(brush_pixel_position).r; + real_t brush_value = brush_image->get_pixelv(brush_pixel_position).r; + brush_value = std::isnan(brush_value) ? 0.f : brush_value; + real_t brush_alpha = brush_value; + if (brush_auto_alpha_enabled) { + brush_alpha = Math::abs(brush_value - brush_alpha_neutral); + brush_alpha *= brush_alpha_gain; + if (brush_alpha < brush_alpha_min_threshold) { + brush_alpha = 0.f; + } else { + brush_alpha = MIN(brush_alpha, 1.f); + } + } brush_alpha = real_t(Math::pow(double(brush_alpha), double(gamma))); brush_alpha = std::isnan(brush_alpha) ? 0.f : brush_alpha; Color src = map->get_pixelv(map_pixel_position); Color dest = src; + bool layer_pixel_applied = false; + bool wrote_to_base_map = false; + bool skip_layer_paint = paint_to_layer && !using_layer; + if (skip_layer_paint && ctx_ptr && ctx_ptr->force_base_fallback) { + skip_layer_paint = false; + } + if (skip_layer_paint) { + continue; + } + Color composite_src = src; + Color payload_src; + real_t layer_value = 0.0f; + if (using_layer) { + if (layer_payload_ptr) { + payload_src = layer_payload_ptr->get_pixelv(map_pixel_position); + layer_value = payload_src.r; + } + if (composited_map_ptr) { + composite_src = composited_map_ptr->get_pixelv(map_pixel_position); + } else { + composite_src = src; + composite_src.r += layer_value; + } + } + Color working_src = using_layer ? composite_src : src; + dest = working_src; if (map_type == TYPE_HEIGHT) { - real_t srcf = src.r; + real_t srcf = working_src.r; // In case data in existing map has nan or inf saved, check, and reset to real number if required. srcf = std::isnan(srcf) ? 0.f : srcf; real_t destf = srcf; @@ -302,11 +602,26 @@ void Terrain3DEditor::_operate_map(const Vector3 &p_global_position, const real_ break; } dest = Color(destf, 0.f, 0.f, 1.f); + if (using_layer && layer_payload_ptr) { + real_t composite_value = composite_src.r; + real_t delta_total = destf - composite_value; + real_t updated_value = layer_value + delta_total; + layer_payload_ptr->set_pixelv(map_pixel_position, Color(updated_value, 0.0f, 0.0f, 1.0f)); + if (composited_map_ptr && ctx_ptr->composite_is_cache) { + composited_map_ptr->set_pixelv(map_pixel_position, Color(destf, 0.0f, 0.0f, 1.0f)); + } + layer_pixel_applied = true; + if (ctx_ptr) { + ctx_ptr->marked_dirty = true; + } + } else { + map->set_pixelv(map_pixel_position, dest); + wrote_to_base_map = true; + } region->update_height(destf); data->update_master_height(destf); edited_position.y = destf; edited_area = edited_area.expand(edited_position); - } else if (map_type == TYPE_CONTROL) { // Get current bit field from pixel uint32_t base_id = get_base(src.r); @@ -551,11 +866,61 @@ void Terrain3DEditor::_operate_map(const Vector3 &p_global_position, const real_ default: break; } + map->set_pixelv(map_pixel_position, dest); + wrote_to_base_map = true; } backup_region(region); - map->set_pixelv(map_pixel_position, dest); + if (!wrote_to_base_map && !layer_pixel_applied) { + map->set_pixelv(map_pixel_position, dest); + wrote_to_base_map = true; + } + if (wrote_to_base_map) { + DirtyRegionInfo &dirty_info = base_dirty_regions[region_loc]; + if (!dirty_info.region.is_valid()) { + dirty_info.region = region; + } + dirty_info.has_writes = true; + dirty_info.min_x = MIN(dirty_info.min_x, map_pixel_position.x); + dirty_info.min_y = MIN(dirty_info.min_y, map_pixel_position.y); + dirty_info.max_x = MAX(dirty_info.max_x, map_pixel_position.x); + dirty_info.max_y = MAX(dirty_info.max_y, map_pixel_position.y); + } } } + + if (!paint_to_layer) { + for (auto &entry : base_dirty_regions) { + const DirtyRegionInfo &dirty = entry.second; + if (!dirty.region.is_valid() || !dirty.has_writes) { + continue; + } + int width = dirty.max_x - dirty.min_x + 1; + int height = dirty.max_y - dirty.min_y + 1; + if (width <= 0 || height <= 0) { + continue; + } + Rect2i dirty_rect(Vector2i(dirty.min_x, dirty.min_y), Vector2i(width, height)); + dirty.region->mark_layers_dirty_rect(map_type, dirty_rect); + } + } + + if (paint_to_layer) { + for (auto &entry : layer_contexts) { + LayerContext &ctx = entry.second; + if (!ctx.valid || !ctx.marked_dirty) { + continue; + } + if (ctx.layer.is_valid()) { + ctx.layer->mark_dirty(); + } + if (ctx.region.is_valid()) { + ctx.region->mark_layers_dirty(map_type); + ctx.region->set_modified(true); + ctx.region->set_edited(true); + } + } + } + // Regenerate color mipmaps for edited regions if (map_type == TYPE_COLOR) { for (Ref region : _edited_regions) { @@ -578,6 +943,7 @@ void Terrain3DEditor::_operate_map(const Vector3 &p_global_position, const real_ } // Update Dynamic / Editor collision if (_terrain->get_collision_mode() == Terrain3DCollision::DYNAMIC_EDITOR) { + LOG(DEBUG, "Editor forcing collision update (operate_map)"); _terrain->get_collision()->update(true); } if (_tool == HEIGHT || _tool == SCULPT || _tool == TEXTURE || _tool == AUTOSHADER) { @@ -843,6 +1209,36 @@ void Terrain3DEditor::set_brush_data(const Dictionary &p_data) { if (img.is_valid() && !img->is_empty()) { _brush_data["brush_image"] = img; _brush_data["brush_image_size"] = img->get_size(); + real_t neutral_value = 0.0f; + Image *img_ptr = img.ptr(); + if (img_ptr) { + int width = img_ptr->get_width(); + int height = img_ptr->get_height(); + if (width > 0 && height > 0) { + double total = 0.0; + int64_t count = 0; + for (int x = 0; x < width; x++) { + total += img_ptr->get_pixel(x, 0).r; + count++; + if (height > 1) { + total += img_ptr->get_pixel(x, height - 1).r; + count++; + } + } + for (int y = 1; y < height - 1; y++) { + total += img_ptr->get_pixel(0, y).r; + count++; + if (width > 1) { + total += img_ptr->get_pixel(width - 1, y).r; + count++; + } + } + if (count > 0) { + neutral_value = real_t(total / double(count)); + } + } + } + _brush_data["brush_alpha_neutral_value"] = neutral_value; } else { LOG(ERROR, "Brush data doesn't contain a valid image"); } @@ -855,6 +1251,9 @@ void Terrain3DEditor::set_brush_data(const Dictionary &p_data) { } else { LOG(ERROR, "Brush data doesn't contain an image and texture"); } + if (!_brush_data.has("brush_alpha_neutral_value")) { + _brush_data["brush_alpha_neutral_value"] = 0.0f; + } // Santize settings // size is redundantly clamped differently in _operate_map and instancer::add_transforms @@ -891,6 +1290,11 @@ void Terrain3DEditor::set_brush_data(const Dictionary &p_data) { _brush_data["gamma"] = CLAMP(real_t(p_data.get("gamma", 1.f)), 0.1f, 2.f); _brush_data["brush_spin_speed"] = CLAMP(real_t(p_data.get("brush_spin_speed", 0.f)), 0.f, 1.f); _brush_data["gradient_points"] = p_data.get("gradient_points", PackedVector3Array()); + _brush_data["brush_auto_alpha_enabled"] = bool(p_data.get("brush_auto_alpha_enabled", true)); + _brush_data["brush_auto_alpha_from_border"] = bool(p_data.get("brush_auto_alpha_from_border", true)); + _brush_data["brush_manual_neutral_value"] = real_t(p_data.get("brush_manual_neutral_value", 0.f)); + _brush_data["brush_alpha_gain"] = CLAMP(real_t(p_data.get("brush_alpha_gain", 1.f)), 0.f, 10.f); + _brush_data["brush_alpha_min_threshold"] = CLAMP(real_t(p_data.get("brush_alpha_min_threshold", 0.001f)), 0.f, 1.f); Util::print_dict("set_brush_data() Santized brush data:", _brush_data, EXTREME); } @@ -1001,6 +1405,99 @@ void Terrain3DEditor::stop_operation() { _is_operating = false; } +void Terrain3DEditor::set_active_layer_index(const int p_index) { + int sanitized_index = p_index; + if (sanitized_index < 0) { + if (sanitized_index < -1) { + static int negative_index_log_count = 0; + if (negative_index_log_count < 5) { + LOG(WARN, "Received invalid active layer index ", sanitized_index, "; falling back to base layer (0)"); + negative_index_log_count++; + } + } + sanitized_index = 0; + } + if (_active_layer_index == sanitized_index) { + return; + } + _active_layer_index = sanitized_index; + if (_active_layer_index == 0) { + _active_layer_group_id = 0; + _active_layer_map_type = TYPE_MAX; + _active_layer_template = Ref(); + } +} + +void Terrain3DEditor::set_active_layer_reference(const Ref &p_layer, const MapType p_map_type) { + if (p_layer.is_null() || !_terrain) { + _active_layer_group_id = 0; + _active_layer_map_type = TYPE_MAX; + _active_layer_template = Ref(); + return; + } + Terrain3DData *data = _terrain->get_data(); + if (!data) { + return; + } + _active_layer_group_id = data->ensure_layer_group_id(p_layer); + _active_layer_template = p_layer; + _active_layer_map_type = p_map_type; +} + +int Terrain3DEditor::create_layer(const Vector2i &p_region_loc, const MapType p_map_type, const bool p_select) { + IS_INIT(-1); + Terrain3DData *data = _terrain->get_data(); + if (!data) { + return -1; + } + Ref region = data->get_region(p_region_loc); + if (region.is_null()) { + LOG(WARN, "Cannot create layer: region not found at ", p_region_loc); + return -1; + } + Ref new_layer; + if (p_map_type == TYPE_HEIGHT) { + Ref stamp_layer; + stamp_layer.instantiate(); + stamp_layer->set_map_type(p_map_type); + stamp_layer->set_blend_mode(Terrain3DLayer::BLEND_ADD); + stamp_layer->set_enabled(true); + new_layer = stamp_layer; + } else { + new_layer.instantiate(); + new_layer->set_map_type(p_map_type); + new_layer->set_enabled(true); + } + if (new_layer.is_null()) { + LOG(ERROR, "Failed to instantiate layer resource for map type ", p_map_type); + return -1; + } + TypedArray existing_layers = region->get_layers(p_map_type); + int insert_index = existing_layers.size(); + Ref added_layer = data->add_layer(p_region_loc, p_map_type, new_layer, insert_index, true); + if (added_layer.is_null()) { + LOG(ERROR, "Failed to add layer to region ", p_region_loc); + return -1; + } + set_active_layer_reference(added_layer, p_map_type); + TypedArray updated_layers = region->get_layers(p_map_type); + int result_index = -1; + for (int i = 0; i < updated_layers.size(); i++) { + if (updated_layers[i] == added_layer) { + result_index = i; + break; + } + } + if (result_index < 0) { + result_index = MAX(0, updated_layers.size() - 1); + } + int ui_index = result_index + 1; + if (p_select && ui_index > 0) { + set_active_layer_index(ui_index); + } + return ui_index; +} + /////////////////////////// // Protected Functions /////////////////////////// @@ -1035,11 +1532,17 @@ void Terrain3DEditor::_bind_methods() { ClassDB::bind_method(D_METHOD("get_tool"), &Terrain3DEditor::get_tool); ClassDB::bind_method(D_METHOD("set_operation", "operation"), &Terrain3DEditor::set_operation); ClassDB::bind_method(D_METHOD("get_operation"), &Terrain3DEditor::get_operation); + ClassDB::bind_method(D_METHOD("set_active_layer_index", "index"), &Terrain3DEditor::set_active_layer_index); + ClassDB::bind_method(D_METHOD("get_active_layer_index"), &Terrain3DEditor::get_active_layer_index); ClassDB::bind_method(D_METHOD("start_operation", "position"), &Terrain3DEditor::start_operation); ClassDB::bind_method(D_METHOD("is_operating"), &Terrain3DEditor::is_operating); ClassDB::bind_method(D_METHOD("operate", "position", "camera_direction"), &Terrain3DEditor::operate); ClassDB::bind_method(D_METHOD("backup_region", "region"), &Terrain3DEditor::backup_region); ClassDB::bind_method(D_METHOD("stop_operation"), &Terrain3DEditor::stop_operation); + ClassDB::bind_method(D_METHOD("create_layer", "region_location", "map_type", "select"), &Terrain3DEditor::create_layer, DEFVAL(true)); + ClassDB::bind_method(D_METHOD("set_active_layer_reference", "layer", "map_type"), &Terrain3DEditor::set_active_layer_reference); + ClassDB::bind_method(D_METHOD("get_active_layer_group_id"), &Terrain3DEditor::get_active_layer_group_id); ClassDB::bind_method(D_METHOD("apply_undo", "data"), &Terrain3DEditor::_apply_undo); + ADD_PROPERTY(PropertyInfo(Variant::INT, "active_layer_index"), "set_active_layer_index", "get_active_layer_index"); } diff --git a/src/terrain_3d_editor.h b/src/terrain_3d_editor.h index 89ec4a383..d52188f1c 100644 --- a/src/terrain_3d_editor.h +++ b/src/terrain_3d_editor.h @@ -3,10 +3,25 @@ #ifndef TERRAIN3D_EDITOR_CLASS_H #define TERRAIN3D_EDITOR_CLASS_H +#include + #include #include - +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "constants.h" #include "terrain_3d.h" +#include "terrain_3d_layer.h" #include "terrain_3d_region.h" class Terrain3DEditor : public Object { @@ -88,6 +103,10 @@ class Terrain3DEditor : public Object { AABB _modified_area; Dictionary _undo_data; // See _get_undo_data for definition uint64_t _last_pen_tick = 0; + int _active_layer_index = 0; + uint64_t _active_layer_group_id = 0; + MapType _active_layer_map_type = TYPE_MAX; + Ref _active_layer_template; void _send_region_aabb(const Vector2i &p_region_loc, const Vector2 &p_height_range = V2_ZERO); Ref _operate_region(const Vector2i &p_region_loc); @@ -120,6 +139,11 @@ class Terrain3DEditor : public Object { void operate(const Vector3 &p_global_position, const real_t p_camera_direction); void backup_region(const Ref &p_region); void stop_operation(); + void set_active_layer_index(const int p_index); + int get_active_layer_index() const { return _active_layer_index; } + int create_layer(const Vector2i &p_region_loc, const MapType p_map_type, const bool p_select = true); + void set_active_layer_reference(const Ref &p_layer, const MapType p_map_type); + uint64_t get_active_layer_group_id() const { return _active_layer_group_id; } protected: static void _bind_methods(); diff --git a/src/terrain_3d_layer.cpp b/src/terrain_3d_layer.cpp new file mode 100644 index 000000000..b86f4be61 --- /dev/null +++ b/src/terrain_3d_layer.cpp @@ -0,0 +1,280 @@ +// Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. + +#include +#include +#include +#include +#include +#include + +#include "logger.h" +#include "terrain_3d_layer.h" +#include "terrain_3d_util.h" + +namespace { +static inline real_t smooth_step(real_t edge0, real_t edge1, real_t x) { + x = CLAMP((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + return x * x * (3.0f - 2.0f * x); +} +} + +/////////////////////////// +// Terrain3DLayer +/////////////////////////// + +void Terrain3DLayer::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_map_type", "map_type"), &Terrain3DLayer::set_map_type); + ClassDB::bind_method(D_METHOD("get_map_type"), &Terrain3DLayer::get_map_type); + ClassDB::bind_method(D_METHOD("set_enabled", "enabled"), &Terrain3DLayer::set_enabled); + ClassDB::bind_method(D_METHOD("is_enabled"), &Terrain3DLayer::is_enabled); + ClassDB::bind_method(D_METHOD("set_intensity", "intensity"), &Terrain3DLayer::set_intensity); + ClassDB::bind_method(D_METHOD("get_intensity"), &Terrain3DLayer::get_intensity); + ClassDB::bind_method(D_METHOD("set_feather_radius", "radius"), &Terrain3DLayer::set_feather_radius); + ClassDB::bind_method(D_METHOD("get_feather_radius"), &Terrain3DLayer::get_feather_radius); + ClassDB::bind_method(D_METHOD("set_blend_mode", "blend_mode"), &Terrain3DLayer::set_blend_mode); + ClassDB::bind_method(D_METHOD("get_blend_mode"), &Terrain3DLayer::get_blend_mode); + ClassDB::bind_method(D_METHOD("set_coverage", "rect"), &Terrain3DLayer::set_coverage); + ClassDB::bind_method(D_METHOD("get_coverage"), &Terrain3DLayer::get_coverage); + ClassDB::bind_method(D_METHOD("set_payload", "image"), &Terrain3DLayer::set_payload); + ClassDB::bind_method(D_METHOD("get_payload"), &Terrain3DLayer::get_payload); + ClassDB::bind_method(D_METHOD("set_alpha", "image"), &Terrain3DLayer::set_alpha); + ClassDB::bind_method(D_METHOD("get_alpha"), &Terrain3DLayer::get_alpha); + ClassDB::bind_method(D_METHOD("set_group_id", "group_id"), &Terrain3DLayer::set_group_id); + ClassDB::bind_method(D_METHOD("get_group_id"), &Terrain3DLayer::get_group_id); + ClassDB::bind_method(D_METHOD("set_user_editable", "editable"), &Terrain3DLayer::set_user_editable); + ClassDB::bind_method(D_METHOD("is_user_editable"), &Terrain3DLayer::is_user_editable); + ClassDB::bind_method(D_METHOD("mark_dirty"), &Terrain3DLayer::mark_dirty); + + ADD_PROPERTY(PropertyInfo(Variant::INT, "map_type", PROPERTY_HINT_ENUM, "HEIGHT,CONTROL,COLOR"), "set_map_type", "get_map_type"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "enabled"), "set_enabled", "is_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "intensity", PROPERTY_HINT_RANGE, "0.0,10.0,0.01"), "set_intensity", "get_intensity"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "feather_radius", PROPERTY_HINT_RANGE, "0.0,64.0,0.01"), "set_feather_radius", "get_feather_radius"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "blend_mode", PROPERTY_HINT_ENUM, "Add,Subtract,Replace"), "set_blend_mode", "get_blend_mode"); + ADD_PROPERTY(PropertyInfo(Variant::RECT2I, "coverage"), "set_coverage", "get_coverage"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "payload", PROPERTY_HINT_RESOURCE_TYPE, "Image"), "set_payload", "get_payload"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "alpha", PROPERTY_HINT_RESOURCE_TYPE, "Image"), "set_alpha", "get_alpha"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "group_id", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_INTERNAL), "set_group_id", "get_group_id"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "user_editable", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_INTERNAL), "set_user_editable", "is_user_editable"); + + BIND_ENUM_CONSTANT(BLEND_ADD); + BIND_ENUM_CONSTANT(BLEND_SUBTRACT); + BIND_ENUM_CONSTANT(BLEND_REPLACE); +} + +void Terrain3DLayer::_generate_payload(const real_t p_vertex_spacing) { + if (_payload.is_null() && _coverage.size != Vector2i()) { + _payload = Util::get_filled_image(_coverage.size, COLOR_BLACK, false, map_type_get_format(_map_type)); + } + _cached_vertex_spacing = p_vertex_spacing; + _dirty = false; +} + +void Terrain3DLayer::_ensure_payload(const real_t p_vertex_spacing) { + if (_dirty || _payload.is_null() || needs_rebuild(p_vertex_spacing)) { + _generate_payload(p_vertex_spacing); + } +} + +real_t Terrain3DLayer::_compute_feather_weight(const Vector2i &p_pixel) const { + if (_feather_radius <= CMP_EPSILON) { + return 1.0f; + } + Vector2i size = _payload->get_size(); + Vector2 dist_to_edge = Vector2(MIN(real_t(p_pixel.x), real_t(size.x - 1 - p_pixel.x)), MIN(real_t(p_pixel.y), real_t(size.y - 1 - p_pixel.y))); + real_t shortest = MIN(dist_to_edge.x, dist_to_edge.y); + if (shortest >= _feather_radius) { + return 1.0f; + } + real_t t = CLAMP(shortest / _feather_radius, 0.0f, 1.0f); + return smooth_step(0.0f, 1.0f, t); +} + +void Terrain3DLayer::set_map_type(const MapType p_type) { + if (_map_type != p_type) { + _map_type = p_type; + mark_dirty(); + } +} + +void Terrain3DLayer::set_enabled(const bool p_enabled) { + _enabled = p_enabled; +} + +void Terrain3DLayer::set_intensity(const real_t p_intensity) { + if (!Math::is_equal_approx(_intensity, p_intensity)) { + _intensity = p_intensity; + } +} + +void Terrain3DLayer::set_feather_radius(const real_t p_radius) { + if (!Math::is_equal_approx(_feather_radius, p_radius)) { + _feather_radius = MAX(0.0f, p_radius); + } +} + +void Terrain3DLayer::set_blend_mode(const BlendMode p_mode) { + if (_blend_mode != p_mode) { + _blend_mode = p_mode; + } +} + +void Terrain3DLayer::set_coverage(const Rect2i &p_rect) { + if (_coverage != p_rect) { + _coverage = p_rect; + mark_dirty(); + } +} + +void Terrain3DLayer::set_payload(const Ref &p_image) { + _payload = p_image; + mark_dirty(); +} + +void Terrain3DLayer::set_alpha(const Ref &p_alpha) { + _alpha = p_alpha; +} + +void Terrain3DLayer::set_group_id(const uint64_t p_group_id) { + _group_id = p_group_id; +} + +void Terrain3DLayer::set_user_editable(bool p_editable) { + _user_editable = p_editable; +} + +bool Terrain3DLayer::needs_rebuild(const real_t p_vertex_spacing) const { + return !Math::is_equal_approx(_cached_vertex_spacing, p_vertex_spacing); +} + +namespace { +static inline Rect2i clamp_rect_to_size(const Rect2i &p_rect, const Vector2i &p_max_size) { + Rect2i clamped = p_rect; + Vector2i pos_clamped = p_rect.position.clamp(Vector2i(), p_max_size); + Vector2i coverage_end = p_rect.position + p_rect.size; + Vector2i end_clamped = Vector2i(MIN(coverage_end.x, p_max_size.x), MIN(coverage_end.y, p_max_size.y)); + clamped.position = pos_clamped; + clamped.size = end_clamped - clamped.position; + return clamped; +} +} + +void Terrain3DLayer::apply(Image &p_target, const real_t p_vertex_spacing) { + apply_rect(p_target, p_vertex_spacing, Rect2i()); +} + +void Terrain3DLayer::apply_rect(Image &p_target, const real_t p_vertex_spacing, const Rect2i &p_rect) { + if (!_enabled) { + return; + } + _ensure_payload(p_vertex_spacing); + if (_payload.is_null()) { + LOG(DEBUG, "Layer payload missing for map type ", _map_type, ", coverage ", _coverage); + return; + } + if (_payload->get_width() <= 0 || _payload->get_height() <= 0) { + LOG(ERROR, "Layer payload has invalid size ", Vector2i(_payload->get_width(), _payload->get_height()), " for coverage ", _coverage); + return; + } + Vector2i target_size = Vector2i(p_target.get_width(), p_target.get_height()); + if (target_size.x <= 0 || target_size.y <= 0) { + return; + } + Rect2i coverage = _coverage.has_area() ? _coverage : Rect2i(Vector2i(), Vector2i(_payload->get_width(), _payload->get_height())); + Rect2i coverage_clamped = clamp_rect_to_size(coverage, target_size); + if (coverage_clamped.size.x <= 0 || coverage_clamped.size.y <= 0) { + return; + } + Rect2i effective = coverage_clamped; + if (p_rect.has_area()) { + Rect2i limit = clamp_rect_to_size(p_rect, target_size); + effective = effective.intersection(limit); + if (!effective.has_area()) { + return; + } + } + int skipped_samples = 0; + Vector2i loop_end = effective.position + effective.size; + for (int dst_y = effective.position.y; dst_y < loop_end.y; dst_y++) { + for (int dst_x = effective.position.x; dst_x < loop_end.x; dst_x++) { + int src_x = dst_x - coverage.position.x; + int src_y = dst_y - coverage.position.y; + if (src_x < 0 || src_y < 0 || src_x >= _payload->get_width() || src_y >= _payload->get_height()) { + skipped_samples++; + continue; + } + Color src = _payload->get_pixel(src_x, src_y); + Color dst = p_target.get_pixel(dst_x, dst_y); + + real_t alpha_weight = 1.0f; + if (_alpha.is_valid()) { + int alpha_w = _alpha->get_width(); + int alpha_h = _alpha->get_height(); + if (src_x >= 0 && src_x < alpha_w && src_y >= 0 && src_y < alpha_h) { + alpha_weight = _alpha->get_pixel(src_x, src_y).r; + } + } + real_t feather_weight = _compute_feather_weight(Vector2i(src_x, src_y)); + real_t mask_weight = CLAMP(alpha_weight * feather_weight, 0.0f, 1.0f); + real_t intensity = MAX(_intensity, 0.0f); + real_t replace_weight = MIN(mask_weight * intensity, 1.0f); + real_t additive_weight = mask_weight * intensity; + + switch (_map_type) { + case TYPE_HEIGHT: { + real_t payload = src.r; + if (_blend_mode == BLEND_REPLACE) { + dst.r = Math::lerp(dst.r, payload, replace_weight); + } else { + real_t sign = (_blend_mode == BLEND_SUBTRACT) ? -1.0f : 1.0f; + real_t delta = payload * additive_weight; + dst.r += delta * sign; + } + dst.a = 1.0f; + } break; + case TYPE_CONTROL: { + real_t payload = src.r; + if (_blend_mode == BLEND_REPLACE) { + dst.r = Math::lerp(dst.r, payload, replace_weight); + } else { + real_t sign = (_blend_mode == BLEND_SUBTRACT) ? -1.0f : 1.0f; + real_t delta = payload * additive_weight; + dst.r += delta * sign; + } + dst.a = 1.0f; + } break; + case TYPE_COLOR: { + Color payload = src; + if (_blend_mode == BLEND_REPLACE) { + dst = dst.lerp(payload, replace_weight); + } else { + real_t sign = (_blend_mode == BLEND_SUBTRACT) ? -1.0f : 1.0f; + Color delta = payload * additive_weight; + dst.r = CLAMP(dst.r + delta.r * sign, 0.0f, 1.0f); + dst.g = CLAMP(dst.g + delta.g * sign, 0.0f, 1.0f); + dst.b = CLAMP(dst.b + delta.b * sign, 0.0f, 1.0f); + dst.a = CLAMP(dst.a + delta.a * sign, 0.0f, 1.0f); + } + } break; + default: + break; + } + + p_target.set_pixel(dst_x, dst_y, dst); + } + } + if (skipped_samples > 0) { + LOG(WARN, "Layer skipped ", skipped_samples, " samples due to payload bounds. coverage=", coverage, " clamped=", effective, " payload_size=", Vector2i(_payload->get_width(), _payload->get_height())); + } +} + +void Terrain3DLayer::mark_dirty() { + _dirty = true; +} + +/////////////////////////// +// Terrain3DStampLayer +/////////////////////////// + +void Terrain3DStampLayer::_bind_methods() { +} + diff --git a/src/terrain_3d_layer.h b/src/terrain_3d_layer.h new file mode 100644 index 000000000..88d6bb0f4 --- /dev/null +++ b/src/terrain_3d_layer.h @@ -0,0 +1,103 @@ +// Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. + +#ifndef TERRAIN3D_LAYER_CLASS_H +#define TERRAIN3D_LAYER_CLASS_H + +#include + +#include +#include + +#include "constants.h" +#include "terrain_3d_map.h" + +class Terrain3DLayer : public Resource { + GDCLASS(Terrain3DLayer, Resource); + CLASS_NAME(); + +public: + enum BlendMode { + BLEND_ADD = 0, + BLEND_SUBTRACT, + BLEND_REPLACE, + }; + +protected: + MapType _map_type = TYPE_HEIGHT; + Rect2i _coverage = Rect2i(); + Ref _payload; + Ref _alpha; + real_t _intensity = 1.0f; + real_t _feather_radius = 0.0f; + bool _enabled = true; + bool _dirty = true; + BlendMode _blend_mode = BLEND_ADD; + + real_t _cached_vertex_spacing = 0.0f; + uint64_t _group_id = 0; + bool _user_editable = true; + +protected: + static void _bind_methods(); + + virtual void _generate_payload(const real_t p_vertex_spacing); + void _ensure_payload(const real_t p_vertex_spacing); + real_t _compute_feather_weight(const Vector2i &p_pixel) const; + +public: + Terrain3DLayer() {} + ~Terrain3DLayer() {} + + void set_map_type(const MapType p_type); + MapType get_map_type() const { return _map_type; } + + void set_enabled(const bool p_enabled); + bool is_enabled() const { return _enabled; } + + void set_intensity(const real_t p_intensity); + real_t get_intensity() const { return _intensity; } + + void set_feather_radius(const real_t p_radius); + real_t get_feather_radius() const { return _feather_radius; } + + void set_blend_mode(const BlendMode p_mode); + BlendMode get_blend_mode() const { return _blend_mode; } + + void set_coverage(const Rect2i &p_rect); + Rect2i get_coverage() const { return _coverage; } + + void set_payload(const Ref &p_image); + Ref get_payload() const { return _payload; } + + void set_alpha(const Ref &p_alpha); + Ref get_alpha() const { return _alpha; } + + void set_group_id(const uint64_t p_group_id); + uint64_t get_group_id() const { return _group_id; } + + void set_user_editable(bool p_editable); + bool is_user_editable() const { return _user_editable; } + + bool needs_rebuild(const real_t p_vertex_spacing) const; + + void apply(Image &p_target, const real_t p_vertex_spacing); + void apply_rect(Image &p_target, const real_t p_vertex_spacing, const Rect2i &p_rect); + + void mark_dirty(); +}; + +VARIANT_ENUM_CAST(Terrain3DLayer::BlendMode); + +class Terrain3DStampLayer : public Terrain3DLayer { + GDCLASS(Terrain3DStampLayer, Terrain3DLayer); + CLASS_NAME(); + +protected: + static void _bind_methods(); + +public: + Terrain3DStampLayer() {} + ~Terrain3DStampLayer() {} +}; + +#endif // TERRAIN3D_LAYER_CLASS_H diff --git a/src/terrain_3d_map.h b/src/terrain_3d_map.h new file mode 100644 index 000000000..683187da6 --- /dev/null +++ b/src/terrain_3d_map.h @@ -0,0 +1,54 @@ +// Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. + +#ifndef TERRAIN3D_MAP_CLASS_H +#define TERRAIN3D_MAP_CLASS_H + +#include +#include + +#include "constants.h" + +enum MapType { + TYPE_HEIGHT, + TYPE_CONTROL, + TYPE_COLOR, + TYPE_MAX, +}; + +inline const Image::Format FORMAT[] = { + Image::FORMAT_RF, + Image::FORMAT_RF, + Image::FORMAT_RGBA8, + Image::Format(TYPE_MAX), +}; + +inline const char *TYPESTR[] = { + "TYPE_HEIGHT", + "TYPE_CONTROL", + "TYPE_COLOR", + "TYPE_MAX", +}; + +inline const Color COLOR[] = { + COLOR_BLACK, + COLOR_CONTROL, + COLOR_ROUGHNESS, + COLOR_NAN, +}; + +inline Image::Format map_type_get_format(MapType p_type) { + int idx = CLAMP(static_cast(p_type), 0, static_cast(TYPE_MAX) - 1); + return FORMAT[idx]; +} + +inline const char *map_type_get_string(MapType p_type) { + int idx = CLAMP(static_cast(p_type), 0, static_cast(TYPE_MAX) - 1); + return TYPESTR[idx]; +} + +inline Color map_type_get_default_color(MapType p_type) { + int idx = CLAMP(static_cast(p_type), 0, static_cast(TYPE_MAX) - 1); + return COLOR[idx]; +} + +#endif // TERRAIN3D_MAP_CLASS_H diff --git a/src/terrain_3d_region.cpp b/src/terrain_3d_region.cpp index 5d7ca8ff4..18896abc8 100644 --- a/src/terrain_3d_region.cpp +++ b/src/terrain_3d_region.cpp @@ -1,16 +1,320 @@ // Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +#include #include #include "logger.h" #include "terrain_3d_data.h" +#include "terrain_3d_layer.h" #include "terrain_3d_region.h" #include "terrain_3d_util.h" +namespace { +static inline Rect2i merge_rects(const Rect2i &a, const Rect2i &b) { + if (!a.has_area()) { + return b; + } + if (!b.has_area()) { + return a; + } + Vector2i min_pt(MIN(a.position.x, b.position.x), MIN(a.position.y, b.position.y)); + Vector2i max_pt(MAX(a.position.x + a.size.x, b.position.x + b.size.x), + MAX(a.position.y + a.size.y, b.position.y + b.size.y)); + return Rect2i(min_pt, max_pt - min_pt); +} +} + ///////////////////// // Public Functions ///////////////////// +TypedArray &Terrain3DRegion::_get_layers_ref(const MapType p_map_type) { + switch (p_map_type) { + case TYPE_HEIGHT: + return _height_layers; + case TYPE_CONTROL: + return _control_layers; + case TYPE_COLOR: + return _color_layers; + default: + return _height_layers; + } +} + +const TypedArray &Terrain3DRegion::_get_layers_ref(const MapType p_map_type) const { + switch (p_map_type) { + case TYPE_HEIGHT: + return _height_layers; + case TYPE_CONTROL: + return _control_layers; + case TYPE_COLOR: + return _color_layers; + default: + return _height_layers; + } +} + +bool &Terrain3DRegion::_get_layers_dirty(const MapType p_map_type) const { + switch (p_map_type) { + case TYPE_HEIGHT: + return const_cast(_height_layers_dirty); + case TYPE_CONTROL: + return const_cast(_control_layers_dirty); + case TYPE_COLOR: + return const_cast(_color_layers_dirty); + default: + return const_cast(_height_layers_dirty); + } +} + +bool &Terrain3DRegion::_get_dirty_rect_valid(const MapType p_map_type) const { + switch (p_map_type) { + case TYPE_HEIGHT: + return const_cast(_height_dirty_rect_valid); + case TYPE_CONTROL: + return const_cast(_control_dirty_rect_valid); + case TYPE_COLOR: + return const_cast(_color_dirty_rect_valid); + default: + return const_cast(_height_dirty_rect_valid); + } +} + +Rect2i &Terrain3DRegion::_get_dirty_rect(const MapType p_map_type) const { + switch (p_map_type) { + case TYPE_HEIGHT: + return const_cast(_height_dirty_rect); + case TYPE_CONTROL: + return const_cast(_control_dirty_rect); + case TYPE_COLOR: + return const_cast(_color_dirty_rect); + default: + return const_cast(_height_dirty_rect); + } +} + +Rect2i Terrain3DRegion::_clamp_rect_to_map(const MapType p_map_type, const Rect2i &p_rect) const { + if (!p_rect.has_area()) { + return Rect2i(); + } + Vector2i max_size(_region_size, _region_size); + Ref base = get_map(p_map_type); + if (base.is_valid()) { + max_size = Vector2i(base->get_width(), base->get_height()); + } + if (max_size.x <= 0 || max_size.y <= 0) { + return Rect2i(); + } + Vector2i pos = p_rect.position; + Vector2i size = p_rect.size; + Vector2i end = pos + size; + pos.x = CLAMP(pos.x, 0, max_size.x); + pos.y = CLAMP(pos.y, 0, max_size.y); + end.x = CLAMP(end.x, 0, max_size.x); + end.y = CLAMP(end.y, 0, max_size.y); + Vector2i clamped_size = end - pos; + if (clamped_size.x <= 0 || clamped_size.y <= 0) { + return Rect2i(); + } + return Rect2i(pos, clamped_size); +} + +Ref &Terrain3DRegion::_get_baked_map(const MapType p_map_type) const { + switch (p_map_type) { + case TYPE_HEIGHT: + return const_cast &>(_baked_height_map); + case TYPE_CONTROL: + return const_cast &>(_baked_control_map); + case TYPE_COLOR: + return const_cast &>(_baked_color_map); + default: + return const_cast &>(_baked_height_map); + } +} + +void Terrain3DRegion::mark_layers_dirty(const MapType p_map_type, const bool p_mark_modified) const { + mark_layers_dirty_rect(p_map_type, Rect2i(), p_mark_modified); +} + +void Terrain3DRegion::mark_layers_dirty_rect(const MapType p_map_type, const Rect2i &p_rect, const bool p_mark_modified) const { + bool &dirty = _get_layers_dirty(p_map_type); + bool &rect_valid = _get_dirty_rect_valid(p_map_type); + Rect2i &dirty_rect = _get_dirty_rect(p_map_type); + Ref &cache = _get_baked_map(p_map_type); + if (!p_rect.has_area() || cache.is_null()) { + dirty = true; + rect_valid = false; + dirty_rect = Rect2i(); + if (cache.is_valid()) { + cache.unref(); + } + } else { + Rect2i clamped = _clamp_rect_to_map(p_map_type, p_rect); + if (!clamped.has_area()) { + return; + } + dirty = true; + dirty_rect = rect_valid ? merge_rects(dirty_rect, clamped) : clamped; + rect_valid = true; + } + if (p_mark_modified) { + const_cast(this)->set_edited(true); + const_cast(this)->set_modified(true); + } +} + +TypedArray Terrain3DRegion::get_layers(const MapType p_map_type) const { + return _get_layers_ref(p_map_type); +} + +void Terrain3DRegion::set_layers(const MapType p_map_type, const TypedArray &p_layers) { + TypedArray &layers = _get_layers_ref(p_map_type); + layers = p_layers; + for (int i = 0; i < layers.size(); i++) { + Ref layer = layers[i]; + if (layer.is_valid()) { + layer->set_map_type(p_map_type); + } + } + mark_layers_dirty(p_map_type); +} + +Ref Terrain3DRegion::add_layer(const MapType p_map_type, const Ref &p_layer, const int p_index) { + TypedArray &layers = _get_layers_ref(p_map_type); + Ref layer = p_layer; + if (layer.is_null()) { + layer.instantiate(); + layer->set_map_type(p_map_type); + } + if (p_index >= 0 && p_index < layers.size()) { + layers.insert(p_index, layer); + } else { + layers.push_back(layer); + } + mark_layers_dirty(p_map_type); + if (layer.is_valid()) { + int expected_size = _region_size; + if (!is_valid_region_size(expected_size)) { + Ref base_map = get_map(p_map_type); + if (base_map.is_valid()) { + expected_size = MAX(base_map->get_width(), base_map->get_height()); + } + } + if (expected_size > 0) { + Vector2i expected_dims(expected_size, expected_size); + Rect2i coverage = layer->get_coverage(); + if (!coverage.has_area()) { + layer->set_coverage(Rect2i(Vector2i(), expected_dims)); + } + Ref payload = layer->get_payload(); + bool payload_invalid = payload.is_null() || payload->get_width() <= 0 || payload->get_height() <= 0; + if (payload_invalid) { + Ref init_payload = Util::get_filled_image(expected_dims, COLOR_BLACK, false, map_type_get_format(p_map_type)); + layer->set_payload(init_payload); + } + } else { + static int invalid_layer_dims_log_count = 0; + if (invalid_layer_dims_log_count < 5) { + LOG(WARN, "Unable to initialize layer payload; expected size unresolved for map type ", p_map_type, " in region ", _location); + invalid_layer_dims_log_count++; + } + } + } + return layer; +} + +void Terrain3DRegion::remove_layer(const MapType p_map_type, const int p_index) { + TypedArray &layers = _get_layers_ref(p_map_type); + if (p_index < 0 || p_index >= layers.size()) { + return; + } + layers.remove_at(p_index); + mark_layers_dirty(p_map_type); +} + +void Terrain3DRegion::clear_layers(const MapType p_map_type) { + TypedArray &layers = _get_layers_ref(p_map_type); + layers.clear(); + mark_layers_dirty(p_map_type); +} + +Ref Terrain3DRegion::get_composited_map(const MapType p_map_type) const { + const TypedArray &layers = _get_layers_ref(p_map_type); + Ref base = get_map(p_map_type); + if (layers.is_empty() || base.is_null()) { + return base; + } + Ref &cache = _get_baked_map(p_map_type); + bool &dirty = _get_layers_dirty(p_map_type); + bool &rect_valid = _get_dirty_rect_valid(p_map_type); + Rect2i &dirty_rect = _get_dirty_rect(p_map_type); + if (cache.is_null()) { + cache = base->duplicate(); + if (cache.is_null()) { + return base; + } + for (int i = 0; i < layers.size(); i++) { + Ref layer = layers[i]; + if (layer.is_null()) { + continue; + } + layer->set_map_type(p_map_type); + layer->apply(*cache.ptr(), _vertex_spacing); + } + dirty = false; + rect_valid = false; + dirty_rect = Rect2i(); + return cache; + } + if (!dirty) { + return cache; + } + if (!rect_valid || !dirty_rect.has_area()) { + cache->copy_from(base); + for (int i = 0; i < layers.size(); i++) { + Ref layer = layers[i]; + if (layer.is_null()) { + continue; + } + layer->set_map_type(p_map_type); + layer->apply(*cache.ptr(), _vertex_spacing); + } + dirty = false; + rect_valid = false; + dirty_rect = Rect2i(); + return cache; + } + Rect2i clamped = _clamp_rect_to_map(p_map_type, dirty_rect); + if (!clamped.has_area()) { + dirty = false; + rect_valid = false; + dirty_rect = Rect2i(); + return cache; + } + cache->blit_rect(base, clamped, clamped.position); + _apply_layers_to_rect(p_map_type, *cache.ptr(), clamped); + dirty = false; + rect_valid = false; + dirty_rect = Rect2i(); + return cache; +} + +void Terrain3DRegion::_apply_layers_to_rect(const MapType p_map_type, Image &p_target, const Rect2i &p_rect) const { + const TypedArray &layers = _get_layers_ref(p_map_type); + for (int i = 0; i < layers.size(); i++) { + Ref layer = layers[i]; + if (layer.is_null()) { + continue; + } + layer->set_map_type(p_map_type); + Rect2i overlap = p_rect.intersection(layer->get_coverage()); + if (!overlap.has_area()) { + continue; + } + layer->apply_rect(p_target, _vertex_spacing, overlap); + } +} + void Terrain3DRegion::set_version(const real_t p_version) { real_t version = CLAMP(p_version, 0.8f, 100.f); if (_version == version) { @@ -39,6 +343,9 @@ void Terrain3DRegion::set_region_size(const int p_region_size) { } SET_IF_DIFF(_region_size, p_region_size); LOG(INFO, "Setting region ", _location, " size: ", p_region_size); + mark_layers_dirty(TYPE_HEIGHT, false); + mark_layers_dirty(TYPE_CONTROL, false); + mark_layers_dirty(TYPE_COLOR, false); } void Terrain3DRegion::set_map(const MapType p_map_type, const Ref &p_image) { @@ -119,6 +426,7 @@ void Terrain3DRegion::set_height_map(const Ref &p_map) { } _height_map = map; calc_height_range(); + mark_layers_dirty(TYPE_HEIGHT); } void Terrain3DRegion::set_control_map(const Ref &p_map) { @@ -133,6 +441,7 @@ void Terrain3DRegion::set_control_map(const Ref &p_map) { _modified = true; } _control_map = map; + mark_layers_dirty(TYPE_CONTROL); } void Terrain3DRegion::set_color_map(const Ref &p_map) { @@ -147,6 +456,7 @@ void Terrain3DRegion::set_color_map(const Ref &p_map) { _modified = true; } _color_map = map; + mark_layers_dirty(TYPE_COLOR); } void Terrain3DRegion::sanitize_maps() { @@ -159,16 +469,19 @@ void Terrain3DRegion::sanitize_maps() { _modified = true; } _height_map = map; + mark_layers_dirty(TYPE_HEIGHT); map = sanitize_map(TYPE_CONTROL, _control_map); if (_control_map != map) { _modified = true; } _control_map = map; + mark_layers_dirty(TYPE_CONTROL); map = sanitize_map(TYPE_COLOR, _color_map); if (_color_map != map) { _modified = true; } _color_map = map; + mark_layers_dirty(TYPE_COLOR); } Ref Terrain3DRegion::sanitize_map(const MapType p_map_type, const Ref &p_map) const { @@ -245,12 +558,26 @@ void Terrain3DRegion::set_height_range(const Vector2 &p_range) { }; } -void Terrain3DRegion::calc_height_range() { - Vector2 range = Util::get_min_max(_height_map); +void Terrain3DRegion::update_height_range_from_image(const Ref &p_image) { + if (p_image.is_null() || p_image->is_empty()) { + return; + } + Vector2 range = Util::get_min_max(p_image); if (_height_range != range) { _height_range = range; _modified = true; - LOG(DEBUG, "Recalculated new height range: ", _height_range, " for region: ", (_location.x != INT32_MAX) ? String(_location) : "(new)", ". Marking modified"); + LOG(DEBUG, "Updated height range from composited image: ", _height_range, + " for region: ", (_location.x != INT32_MAX) ? String(_location) : "(new)"); + } +} + +void Terrain3DRegion::calc_height_range() { + Ref source = get_composited_map(TYPE_HEIGHT); + if (source.is_null()) { + source = _height_map; + } + if (source.is_valid()) { + update_height_range_from_image(source); } } @@ -336,6 +663,40 @@ void Terrain3DRegion::set_data(const Dictionary &p_data) { SET_IF_HAS(_control_map, "control_map"); SET_IF_HAS(_color_map, "color_map"); SET_IF_HAS(_instances, "instances"); +#undef SET_IF_HAS + if (p_data.has("height_layers")) { + Array height_layers = p_data["height_layers"]; + _height_layers = height_layers; + } + if (p_data.has("control_layers")) { + Array control_layers = p_data["control_layers"]; + _control_layers = control_layers; + } + if (p_data.has("color_layers")) { + Array color_layers = p_data["color_layers"]; + _color_layers = color_layers; + } + for (int i = 0; i < _height_layers.size(); i++) { + Ref layer = _height_layers[i]; + if (layer.is_valid()) { + layer->set_map_type(TYPE_HEIGHT); + } + } + for (int i = 0; i < _control_layers.size(); i++) { + Ref layer = _control_layers[i]; + if (layer.is_valid()) { + layer->set_map_type(TYPE_CONTROL); + } + } + for (int i = 0; i < _color_layers.size(); i++) { + Ref layer = _color_layers[i]; + if (layer.is_valid()) { + layer->set_map_type(TYPE_COLOR); + } + } + mark_layers_dirty(TYPE_HEIGHT); + mark_layers_dirty(TYPE_CONTROL); + mark_layers_dirty(TYPE_COLOR); } Dictionary Terrain3DRegion::get_data() const { @@ -352,6 +713,9 @@ Dictionary Terrain3DRegion::get_data() const { dict["control_map"] = _control_map; dict["color_map"] = _color_map; dict["instances"] = _instances; + dict["height_layers"] = _height_layers; + dict["control_layers"] = _control_layers; + dict["color_layers"] = _color_layers; return dict; } @@ -375,6 +739,54 @@ Ref Terrain3DRegion::duplicate(const bool p_deep) { dict["control_map"] = _control_map->duplicate(); dict["color_map"] = _color_map->duplicate(); dict["instances"] = _instances.duplicate(true); + { + TypedArray layers; + for (int i = 0; i < _height_layers.size(); i++) { + Ref layer = _height_layers[i]; + Ref copy = layer; + if (layer.is_valid()) { + Ref dup_res = layer->duplicate(); + Ref dup_layer = dup_res; + if (dup_layer.is_valid()) { + copy = dup_layer; + } + } + layers.push_back(copy); + } + dict["height_layers"] = layers; + } + { + TypedArray layers; + for (int i = 0; i < _control_layers.size(); i++) { + Ref layer = _control_layers[i]; + Ref copy = layer; + if (layer.is_valid()) { + Ref dup_res = layer->duplicate(); + Ref dup_layer = dup_res; + if (dup_layer.is_valid()) { + copy = dup_layer; + } + } + layers.push_back(copy); + } + dict["control_layers"] = layers; + } + { + TypedArray layers; + for (int i = 0; i < _color_layers.size(); i++) { + Ref layer = _color_layers[i]; + Ref copy = layer; + if (layer.is_valid()) { + Ref dup_res = layer->duplicate(); + Ref dup_layer = dup_res; + if (dup_layer.is_valid()) { + copy = dup_layer; + } + } + layers.push_back(copy); + } + dict["color_layers"] = layers; + } region->set_data(dict); } return region; @@ -434,6 +846,19 @@ void Terrain3DRegion::_bind_methods() { ClassDB::bind_method(D_METHOD("get_map", "map_type"), &Terrain3DRegion::get_map); ClassDB::bind_method(D_METHOD("set_maps", "maps"), &Terrain3DRegion::set_maps); ClassDB::bind_method(D_METHOD("get_maps"), &Terrain3DRegion::get_maps); + ClassDB::bind_method(D_METHOD("get_layers", "map_type"), &Terrain3DRegion::get_layers); + ClassDB::bind_method(D_METHOD("set_layers", "map_type", "layers"), &Terrain3DRegion::set_layers); + ClassDB::bind_method(D_METHOD("set_height_layers", "layers"), &Terrain3DRegion::set_height_layers); + ClassDB::bind_method(D_METHOD("get_height_layers"), &Terrain3DRegion::get_height_layers); + ClassDB::bind_method(D_METHOD("set_control_layers", "layers"), &Terrain3DRegion::set_control_layers); + ClassDB::bind_method(D_METHOD("get_control_layers"), &Terrain3DRegion::get_control_layers); + ClassDB::bind_method(D_METHOD("set_color_layers", "layers"), &Terrain3DRegion::set_color_layers); + ClassDB::bind_method(D_METHOD("get_color_layers"), &Terrain3DRegion::get_color_layers); + ClassDB::bind_method(D_METHOD("mark_layers_dirty", "map_type", "mark_modified"), &Terrain3DRegion::mark_layers_dirty, DEFVAL(true)); + ClassDB::bind_method(D_METHOD("add_layer", "map_type", "layer", "index"), &Terrain3DRegion::add_layer, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("remove_layer", "map_type", "index"), &Terrain3DRegion::remove_layer); + ClassDB::bind_method(D_METHOD("clear_layers", "map_type"), &Terrain3DRegion::clear_layers); + ClassDB::bind_method(D_METHOD("get_composited_map", "map_type"), &Terrain3DRegion::get_composited_map); ClassDB::bind_method(D_METHOD("set_height_map", "map"), &Terrain3DRegion::set_height_map); ClassDB::bind_method(D_METHOD("get_height_map"), &Terrain3DRegion::get_height_map); ClassDB::bind_method(D_METHOD("set_control_map", "map"), &Terrain3DRegion::set_control_map); @@ -477,6 +902,10 @@ void Terrain3DRegion::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "height_map", PROPERTY_HINT_RESOURCE_TYPE, "Image", ro_flags), "set_height_map", "get_height_map"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "control_map", PROPERTY_HINT_RESOURCE_TYPE, "Image", ro_flags), "set_control_map", "get_control_map"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "color_map", PROPERTY_HINT_RESOURCE_TYPE, "Image", ro_flags), "set_color_map", "get_color_map"); + int layer_flags = PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_INTERNAL; + ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "height_layers", PROPERTY_HINT_ARRAY_TYPE, "Terrain3DLayer", layer_flags), "set_height_layers", "get_height_layers"); + ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "control_layers", PROPERTY_HINT_ARRAY_TYPE, "Terrain3DLayer", layer_flags), "set_control_layers", "get_control_layers"); + ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "color_layers", PROPERTY_HINT_ARRAY_TYPE, "Terrain3DLayer", layer_flags), "set_color_layers", "get_color_layers"); ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "instances", PROPERTY_HINT_NONE, "", ro_flags), "set_instances", "get_instances"); // Double-clicking a region .res file shows what's on disk, the defaults, not in memory. So these are hidden diff --git a/src/terrain_3d_region.h b/src/terrain_3d_region.h index 2b233480f..7670c72d0 100644 --- a/src/terrain_3d_region.h +++ b/src/terrain_3d_region.h @@ -4,42 +4,25 @@ #define TERRAIN3D_REGION_CLASS_H #include "constants.h" +#include "terrain_3d_map.h" #include "terrain_3d_util.h" +class Terrain3DLayer; +class Terrain3DStampLayer; + class Terrain3DRegion : public Resource { GDCLASS(Terrain3DRegion, Resource); CLASS_NAME(); -public: // Constants - enum MapType { - TYPE_HEIGHT, - TYPE_CONTROL, - TYPE_COLOR, - TYPE_MAX, - }; - - static inline const Image::Format FORMAT[] = { - Image::FORMAT_RF, // TYPE_HEIGHT - Image::FORMAT_RF, // TYPE_CONTROL - Image::FORMAT_RGBA8, // TYPE_COLOR - Image::Format(TYPE_MAX), // Proper size of array instead of FORMAT_MAX - }; - - static inline const char *TYPESTR[] = { - "TYPE_HEIGHT", - "TYPE_CONTROL", - "TYPE_COLOR", - "TYPE_MAX", - }; - - static inline const Color COLOR[] = { - COLOR_BLACK, // TYPE_HEIGHT - COLOR_CONTROL, // TYPE_CONTROL - COLOR_ROUGHNESS, // TYPE_COLOR - COLOR_NAN, // TYPE_MAX, unused just in case someone indexes the array - }; - private: + TypedArray &_get_layers_ref(const MapType p_map_type); + const TypedArray &_get_layers_ref(const MapType p_map_type) const; + bool &_get_layers_dirty(const MapType p_map_type) const; + bool &_get_dirty_rect_valid(const MapType p_map_type) const; + Rect2i &_get_dirty_rect(const MapType p_map_type) const; + Ref &_get_baked_map(const MapType p_map_type) const; + Rect2i _clamp_rect_to_map(const MapType p_map_type, const Rect2i &p_rect) const; + void _apply_layers_to_rect(const MapType p_map_type, Image &p_target, const Rect2i &p_rect) const; // Saved data real_t _version = 0.8f; // Set to first version to ensure we always upgrades this int _region_size = 0; @@ -48,6 +31,21 @@ class Terrain3DRegion : public Resource { Ref _height_map; Ref _control_map; Ref _color_map; + TypedArray _height_layers; + TypedArray _control_layers; + TypedArray _color_layers; + mutable Ref _baked_height_map; + mutable Ref _baked_control_map; + mutable Ref _baked_color_map; + mutable bool _height_layers_dirty = true; + mutable bool _control_layers_dirty = true; + mutable bool _color_layers_dirty = true; + mutable Rect2i _height_dirty_rect; + mutable Rect2i _control_dirty_rect; + mutable Rect2i _color_dirty_rect; + mutable bool _height_dirty_rect_valid = false; + mutable bool _control_dirty_rect_valid = false; + mutable bool _color_dirty_rect_valid = false; // Instancer Dictionary _instances; // Meshes{int} -> Cells{v2i} -> [ Transform3D, Color, Modified ] real_t _vertex_spacing = 1.f; // Spacing that instancer transforms are currently scaled by. @@ -73,12 +71,26 @@ class Terrain3DRegion : public Resource { Image *get_map_ptr(const MapType p_map_type) const; void set_maps(const TypedArray &p_maps); TypedArray get_maps() const; + TypedArray get_layers(const MapType p_map_type) const; + void set_layers(const MapType p_map_type, const TypedArray &p_layers); + Ref add_layer(const MapType p_map_type, const Ref &p_layer, const int p_index = -1); + void remove_layer(const MapType p_map_type, const int p_index); + void clear_layers(const MapType p_map_type); + Ref get_composited_map(const MapType p_map_type) const; + void mark_layers_dirty(const MapType p_map_type, const bool p_mark_modified = true) const; + void mark_layers_dirty_rect(const MapType p_map_type, const Rect2i &p_rect, const bool p_mark_modified = true) const; void set_height_map(const Ref &p_map); Ref get_height_map() const { return _height_map; } void set_control_map(const Ref &p_map); Ref get_control_map() const { return _control_map; } void set_color_map(const Ref &p_map); Ref get_color_map() const { return _color_map; } + void set_height_layers(const TypedArray &p_layers) { set_layers(TYPE_HEIGHT, p_layers); } + TypedArray get_height_layers() const { return _height_layers; } + void set_control_layers(const TypedArray &p_layers) { set_layers(TYPE_CONTROL, p_layers); } + TypedArray get_control_layers() const { return _control_layers; } + void set_color_layers(const TypedArray &p_layers) { set_layers(TYPE_COLOR, p_layers); } + TypedArray get_color_layers() const { return _color_layers; } void sanitize_maps(); Ref sanitize_map(const MapType p_map_type, const Ref &p_map) const; bool validate_map_size(const Ref &p_map) const; @@ -87,12 +99,18 @@ class Terrain3DRegion : public Resource { Vector2 get_height_range() const { return _height_range; } void update_height(const real_t p_height); void update_heights(const Vector2 &p_low_high); + void update_height_range_from_image(const Ref &p_image); void calc_height_range(); // Instancer void set_instances(const Dictionary &p_instances); Dictionary get_instances() const { return _instances; } - void set_vertex_spacing(const real_t p_vertex_spacing) { _vertex_spacing = CLAMP(p_vertex_spacing, 0.25f, 100.f); } + void set_vertex_spacing(const real_t p_vertex_spacing) { + _vertex_spacing = CLAMP(p_vertex_spacing, 0.25f, 100.f); + mark_layers_dirty(TYPE_HEIGHT); + mark_layers_dirty(TYPE_CONTROL); + mark_layers_dirty(TYPE_COLOR); + } real_t get_vertex_spacing() const { return _vertex_spacing; } // Working Data @@ -118,15 +136,7 @@ class Terrain3DRegion : public Resource { static void _bind_methods(); }; -using MapType = Terrain3DRegion::MapType; -VARIANT_ENUM_CAST(Terrain3DRegion::MapType); -constexpr Terrain3DRegion::MapType TYPE_HEIGHT = Terrain3DRegion::MapType::TYPE_HEIGHT; -constexpr Terrain3DRegion::MapType TYPE_CONTROL = Terrain3DRegion::MapType::TYPE_CONTROL; -constexpr Terrain3DRegion::MapType TYPE_COLOR = Terrain3DRegion::MapType::TYPE_COLOR; -constexpr Terrain3DRegion::MapType TYPE_MAX = Terrain3DRegion::MapType::TYPE_MAX; -constexpr inline const Image::Format *FORMAT = Terrain3DRegion::FORMAT; -constexpr inline const char **TYPESTR = Terrain3DRegion::TYPESTR; -constexpr inline const Color *COLOR = Terrain3DRegion::COLOR; +VARIANT_ENUM_CAST(MapType); // Inline functions diff --git a/src/terrain_3d_util.cpp b/src/terrain_3d_util.cpp index 1345b25e9..d74ef126b 100644 --- a/src/terrain_3d_util.cpp +++ b/src/terrain_3d_util.cpp @@ -175,7 +175,7 @@ Vector2 Terrain3DUtil::get_min_max(const Ref &p_image) { } } - LOG(INFO, "Calculating minimum and maximum values of the image: ", min_max); + LOG(DEBUG, "Calculated height min/max: ", min_max); return min_max; }