Add Ground Zero foliage pass
This commit is contained in:
@@ -424,7 +424,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
|
|||||||
- [x] Verify terrain scale is 1 km x 1 km in Unreal.
|
- [x] Verify terrain scale is 1 km x 1 km in Unreal.
|
||||||
- [x] Verify terrain tile origin and centered Unreal bounds for the Ground Zero test map.
|
- [x] Verify terrain tile origin and centered Unreal bounds for the Ground Zero test map.
|
||||||
- [x] Verify neighboring tile edge coordinates against the registry before multi-tile stitching.
|
- [x] Verify neighboring tile edge coordinates against the registry before multi-tile stitching.
|
||||||
- [ ] Add foliage pass.
|
- [x] Add foliage pass.
|
||||||
- [~] Add resource nodes.
|
- [~] Add resource nodes.
|
||||||
- [ ] Add biome-appropriate natural resources based on Ground Zero.
|
- [ ] Add biome-appropriate natural resources based on Ground Zero.
|
||||||
- [ ] Add water source.
|
- [ ] Add water source.
|
||||||
@@ -1402,4 +1402,4 @@ Next version .01 priorities:
|
|||||||
|
|
||||||
Immediate next item:
|
Immediate next item:
|
||||||
|
|
||||||
- [ ] Add foliage pass.
|
- [ ] Add biome-appropriate natural resources based on Ground Zero.
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,40 @@
|
|||||||
|
# Ground Zero First-Pass Foliage
|
||||||
|
|
||||||
|
The Ground Zero demo map now has a first-pass foliage layer driven by terrain
|
||||||
|
height and simple biome placement rules.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
- Native actor: `AAgrarianFoliagePatch`
|
||||||
|
- Map setup script: `Scripts/setup_ground_zero_demo_map.py`
|
||||||
|
- Map actor label: `AGR_GroundZeroFoliage_FirstPass`
|
||||||
|
- Mesh source: Unreal level prototyping meshes for now.
|
||||||
|
|
||||||
|
The actor uses hierarchical instanced static mesh components for three visible
|
||||||
|
foliage groups:
|
||||||
|
|
||||||
|
- Trees: taller cylinder instances placed mostly on higher/hill terrain.
|
||||||
|
- Shrubs: low blocky scrub instances across valley and hillside terrain.
|
||||||
|
- Grass: small clump instances across the broader walkable tile.
|
||||||
|
|
||||||
|
## Placement Rules
|
||||||
|
|
||||||
|
The first pass is intentionally deterministic. It uses the Ground Zero Unreal
|
||||||
|
heightmap and a fixed random seed so repeated map setup runs produce the same
|
||||||
|
foliage distribution.
|
||||||
|
|
||||||
|
The placement avoids the demo player start, shelter, campfire, resource nodes,
|
||||||
|
and wildlife spawn area so the initial investor path remains readable.
|
||||||
|
|
||||||
|
## Counts
|
||||||
|
|
||||||
|
- Trees: `42`
|
||||||
|
- Shrubs: `96`
|
||||||
|
- Grass clumps: `180`
|
||||||
|
|
||||||
|
## Follow-Up
|
||||||
|
|
||||||
|
This is a visual and performance-safe placeholder pass, not final ecology. The
|
||||||
|
next biome/resource passes should replace prototype meshes with real coastal
|
||||||
|
California scrub, grassland, woodland, and resource-specific assets, then tie
|
||||||
|
spawn density to land-cover and hydrography data.
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import math
|
||||||
|
import random
|
||||||
import struct
|
import struct
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -13,6 +15,14 @@ XY_SCALE_CM = 100000.0 / (LANDSCAPE_SIZE - 1)
|
|||||||
Z_SCALE_CM = 100.0
|
Z_SCALE_CM = 100.0
|
||||||
LANDSCAPE_MIN_XY = -50000.0
|
LANDSCAPE_MIN_XY = -50000.0
|
||||||
|
|
||||||
|
FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass"
|
||||||
|
FOLIAGE_RANDOM_SEED = 4160544
|
||||||
|
FOLIAGE_MESHES = {
|
||||||
|
"tree": "/Game/LevelPrototyping/Meshes/SM_Cylinder",
|
||||||
|
"shrub": "/Game/LevelPrototyping/Meshes/SM_Cube",
|
||||||
|
"grass": "/Game/LevelPrototyping/Meshes/SM_Cylinder",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
DEMO_ACTORS = [
|
DEMO_ACTORS = [
|
||||||
{
|
{
|
||||||
@@ -88,6 +98,34 @@ DEMO_ACTORS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
FOLIAGE_ZONES = {
|
||||||
|
"trees": {
|
||||||
|
"count": 42,
|
||||||
|
"x_range": (-25000.0, 42000.0),
|
||||||
|
"y_range": (-12000.0, 42000.0),
|
||||||
|
"min_elevation_m": 18.0,
|
||||||
|
"avoid_radius_cm": 3600.0,
|
||||||
|
"scale_range": (0.75, 1.35),
|
||||||
|
},
|
||||||
|
"shrubs": {
|
||||||
|
"count": 96,
|
||||||
|
"x_range": (-42000.0, 45000.0),
|
||||||
|
"y_range": (-38000.0, 45000.0),
|
||||||
|
"min_elevation_m": 7.0,
|
||||||
|
"avoid_radius_cm": 2600.0,
|
||||||
|
"scale_range": (0.45, 1.05),
|
||||||
|
},
|
||||||
|
"grass": {
|
||||||
|
"count": 180,
|
||||||
|
"x_range": (-46000.0, 46000.0),
|
||||||
|
"y_range": (-46000.0, 46000.0),
|
||||||
|
"min_elevation_m": 4.0,
|
||||||
|
"avoid_radius_cm": 1800.0,
|
||||||
|
"scale_range": (0.35, 0.9),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_actor_label(actor):
|
def get_actor_label(actor):
|
||||||
try:
|
try:
|
||||||
return actor.get_actor_label()
|
return actor.get_actor_label()
|
||||||
@@ -109,6 +147,13 @@ def load_blueprint_class(path):
|
|||||||
return generated_class
|
return generated_class
|
||||||
|
|
||||||
|
|
||||||
|
def load_required_asset(path):
|
||||||
|
asset = unreal.EditorAssetLibrary.load_asset(path)
|
||||||
|
if not asset:
|
||||||
|
raise RuntimeError(f"Required asset not found: {path}")
|
||||||
|
return asset
|
||||||
|
|
||||||
|
|
||||||
def load_heightmap():
|
def load_heightmap():
|
||||||
raw = HEIGHTMAP_PATH.read_bytes()
|
raw = HEIGHTMAP_PATH.read_bytes()
|
||||||
expected_bytes = LANDSCAPE_SIZE * LANDSCAPE_SIZE * 2
|
expected_bytes = LANDSCAPE_SIZE * LANDSCAPE_SIZE * 2
|
||||||
@@ -125,12 +170,120 @@ def terrain_z_cm(height_values, x_cm, y_cm):
|
|||||||
return elevation_m * 100.0
|
return elevation_m * 100.0
|
||||||
|
|
||||||
|
|
||||||
|
def terrain_elevation_m(height_values, x_cm, y_cm):
|
||||||
|
return terrain_z_cm(height_values, x_cm, y_cm) / 100.0
|
||||||
|
|
||||||
|
|
||||||
def remove_existing_demo_actors(labels):
|
def remove_existing_demo_actors(labels):
|
||||||
for actor in unreal.EditorLevelLibrary.get_all_level_actors():
|
for actor in unreal.EditorLevelLibrary.get_all_level_actors():
|
||||||
if get_actor_label(actor) in labels:
|
if get_actor_label(actor) in labels:
|
||||||
unreal.EditorLevelLibrary.destroy_actor(actor)
|
unreal.EditorLevelLibrary.destroy_actor(actor)
|
||||||
|
|
||||||
|
|
||||||
|
def distance_2d(a, b):
|
||||||
|
return math.hypot(a.x - b.x, a.y - b.y)
|
||||||
|
|
||||||
|
|
||||||
|
def make_foliage_transform(height_values, x, y, yaw, z_offset, scale_xy, scale_z):
|
||||||
|
return unreal.Transform(
|
||||||
|
location=unreal.Vector(x, y, terrain_z_cm(height_values, x, y) + z_offset),
|
||||||
|
rotation=unreal.Rotator(0.0, yaw, 0.0),
|
||||||
|
scale=unreal.Vector(scale_xy, scale_xy, scale_z),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def configure_foliage_meshes(foliage_actor):
|
||||||
|
component_meshes = {
|
||||||
|
"tree_instances": load_required_asset(FOLIAGE_MESHES["tree"]),
|
||||||
|
"shrub_instances": load_required_asset(FOLIAGE_MESHES["shrub"]),
|
||||||
|
"grass_instances": load_required_asset(FOLIAGE_MESHES["grass"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
for property_name, mesh in component_meshes.items():
|
||||||
|
component = foliage_actor.get_editor_property(property_name)
|
||||||
|
component.set_editor_property("static_mesh", mesh)
|
||||||
|
|
||||||
|
|
||||||
|
def choose_foliage_points(height_values, zone, reserved_points, existing_points):
|
||||||
|
rng = random.Random(FOLIAGE_RANDOM_SEED + len(existing_points) + int(zone["count"]))
|
||||||
|
chosen = []
|
||||||
|
attempts = 0
|
||||||
|
max_attempts = zone["count"] * 80
|
||||||
|
|
||||||
|
while len(chosen) < zone["count"] and attempts < max_attempts:
|
||||||
|
attempts += 1
|
||||||
|
x = rng.uniform(zone["x_range"][0], zone["x_range"][1])
|
||||||
|
y = rng.uniform(zone["y_range"][0], zone["y_range"][1])
|
||||||
|
point = unreal.Vector(x, y, 0.0)
|
||||||
|
|
||||||
|
if terrain_elevation_m(height_values, x, y) < zone["min_elevation_m"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if any(distance_2d(point, reserved) < zone["avoid_radius_cm"] for reserved in reserved_points):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if any(distance_2d(point, existing) < zone["avoid_radius_cm"] * 0.55 for existing in existing_points):
|
||||||
|
continue
|
||||||
|
|
||||||
|
chosen.append(point)
|
||||||
|
existing_points.append(point)
|
||||||
|
|
||||||
|
if len(chosen) != zone["count"]:
|
||||||
|
unreal.log_warning(f"Placed {len(chosen)} of {zone['count']} requested foliage instances for zone.")
|
||||||
|
|
||||||
|
return chosen
|
||||||
|
|
||||||
|
|
||||||
|
def spawn_foliage_actor(height_values):
|
||||||
|
reserved_points = [
|
||||||
|
spec["location_xy"]
|
||||||
|
for spec in DEMO_ACTORS
|
||||||
|
if spec["label"] not in {"AGR_DemoSun", "AGR_DemoSkyLight", "AGR_DemoFog", "AGR_DemoNoticeActor"}
|
||||||
|
]
|
||||||
|
|
||||||
|
foliage_actor = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world(
|
||||||
|
unreal.AgrarianFoliagePatch,
|
||||||
|
unreal.Vector(0.0, 0.0, 0.0),
|
||||||
|
unreal.Rotator(0.0, 0.0, 0.0),
|
||||||
|
FOLIAGE_LABEL,
|
||||||
|
)
|
||||||
|
if not foliage_actor:
|
||||||
|
raise RuntimeError("Could not spawn first-pass foliage actor.")
|
||||||
|
|
||||||
|
set_actor_label(foliage_actor, FOLIAGE_LABEL)
|
||||||
|
configure_foliage_meshes(foliage_actor)
|
||||||
|
foliage_actor.clear_foliage()
|
||||||
|
|
||||||
|
existing_points = []
|
||||||
|
rng = random.Random(FOLIAGE_RANDOM_SEED)
|
||||||
|
|
||||||
|
for point in choose_foliage_points(height_values, FOLIAGE_ZONES["trees"], reserved_points, existing_points):
|
||||||
|
scale = rng.uniform(*FOLIAGE_ZONES["trees"]["scale_range"])
|
||||||
|
foliage_actor.add_tree_instance(
|
||||||
|
make_foliage_transform(height_values, point.x, point.y, rng.uniform(0.0, 360.0), 5.0, scale, scale * 5.5)
|
||||||
|
)
|
||||||
|
|
||||||
|
for point in choose_foliage_points(height_values, FOLIAGE_ZONES["shrubs"], reserved_points, existing_points):
|
||||||
|
scale = rng.uniform(*FOLIAGE_ZONES["shrubs"]["scale_range"])
|
||||||
|
foliage_actor.add_shrub_instance(
|
||||||
|
make_foliage_transform(height_values, point.x, point.y, rng.uniform(0.0, 360.0), 4.0, scale * 2.2, scale * 0.85)
|
||||||
|
)
|
||||||
|
|
||||||
|
for point in choose_foliage_points(height_values, FOLIAGE_ZONES["grass"], reserved_points, existing_points):
|
||||||
|
scale = rng.uniform(*FOLIAGE_ZONES["grass"]["scale_range"])
|
||||||
|
foliage_actor.add_grass_instance(
|
||||||
|
make_foliage_transform(height_values, point.x, point.y, rng.uniform(0.0, 360.0), 2.0, scale * 0.28, scale * 1.6)
|
||||||
|
)
|
||||||
|
|
||||||
|
unreal.log(
|
||||||
|
"Placed first-pass Ground Zero foliage: "
|
||||||
|
f"{foliage_actor.get_tree_instance_count()} trees, "
|
||||||
|
f"{foliage_actor.get_shrub_instance_count()} shrubs, "
|
||||||
|
f"{foliage_actor.get_grass_instance_count()} grass clumps."
|
||||||
|
)
|
||||||
|
return foliage_actor
|
||||||
|
|
||||||
|
|
||||||
def spawn_demo_actor(spec, height_values):
|
def spawn_demo_actor(spec, height_values):
|
||||||
location_xy = spec["location_xy"]
|
location_xy = spec["location_xy"]
|
||||||
z = spec.get("fixed_z")
|
z = spec.get("fixed_z")
|
||||||
@@ -160,9 +313,11 @@ def main():
|
|||||||
raise RuntimeError(f"Could not load map: {MAP_PATH}")
|
raise RuntimeError(f"Could not load map: {MAP_PATH}")
|
||||||
|
|
||||||
labels = {spec["label"] for spec in DEMO_ACTORS}
|
labels = {spec["label"] for spec in DEMO_ACTORS}
|
||||||
|
labels.add(FOLIAGE_LABEL)
|
||||||
remove_existing_demo_actors(labels)
|
remove_existing_demo_actors(labels)
|
||||||
|
|
||||||
height_values = load_heightmap()
|
height_values = load_heightmap()
|
||||||
|
spawn_foliage_actor(height_values)
|
||||||
for spec in DEMO_ACTORS:
|
for spec in DEMO_ACTORS:
|
||||||
spawn_demo_actor(spec, height_values)
|
spawn_demo_actor(spec, height_values)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import unreal
|
||||||
|
|
||||||
|
|
||||||
|
MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
|
||||||
|
FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass"
|
||||||
|
EXPECTED_COUNTS = {
|
||||||
|
"trees": 42,
|
||||||
|
"shrubs": 96,
|
||||||
|
"grass": 180,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_actor_label(actor):
|
||||||
|
try:
|
||||||
|
return actor.get_actor_label()
|
||||||
|
except Exception:
|
||||||
|
return actor.get_name()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not unreal.EditorLevelLibrary.load_level(MAP_PATH):
|
||||||
|
raise RuntimeError(f"Could not load map: {MAP_PATH}")
|
||||||
|
|
||||||
|
actors = unreal.EditorLevelLibrary.get_all_level_actors()
|
||||||
|
foliage_actors = [actor for actor in actors if get_actor_label(actor) == FOLIAGE_LABEL]
|
||||||
|
if len(foliage_actors) != 1:
|
||||||
|
raise RuntimeError(f"Expected exactly one {FOLIAGE_LABEL}, found {len(foliage_actors)}")
|
||||||
|
|
||||||
|
foliage = foliage_actors[0]
|
||||||
|
actual_counts = {
|
||||||
|
"trees": foliage.get_tree_instance_count(),
|
||||||
|
"shrubs": foliage.get_shrub_instance_count(),
|
||||||
|
"grass": foliage.get_grass_instance_count(),
|
||||||
|
}
|
||||||
|
|
||||||
|
failures = [
|
||||||
|
f"{kind} expected {expected}, got {actual_counts[kind]}"
|
||||||
|
for kind, expected in EXPECTED_COUNTS.items()
|
||||||
|
if actual_counts[kind] != expected
|
||||||
|
]
|
||||||
|
if failures:
|
||||||
|
raise RuntimeError("Ground Zero foliage verification failed: " + "; ".join(failures))
|
||||||
|
|
||||||
|
unreal.log(
|
||||||
|
"Ground Zero foliage verification complete: "
|
||||||
|
f"{actual_counts['trees']} trees, "
|
||||||
|
f"{actual_counts['shrubs']} shrubs, "
|
||||||
|
f"{actual_counts['grass']} grass clumps."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// Copyright Pacificao. All Rights Reserved.
|
||||||
|
|
||||||
|
#include "AgrarianFoliagePatch.h"
|
||||||
|
|
||||||
|
#include "Components/HierarchicalInstancedStaticMeshComponent.h"
|
||||||
|
#include "Components/SceneComponent.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
void ConfigureFoliageComponent(UHierarchicalInstancedStaticMeshComponent* Component, const FName CollisionProfileName)
|
||||||
|
{
|
||||||
|
if (!Component)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Component->SetMobility(EComponentMobility::Static);
|
||||||
|
Component->SetCollisionProfileName(CollisionProfileName);
|
||||||
|
Component->SetGenerateOverlapEvents(false);
|
||||||
|
Component->bCastDynamicShadow = true;
|
||||||
|
Component->bCastStaticShadow = true;
|
||||||
|
Component->InstanceStartCullDistance = 120000;
|
||||||
|
Component->InstanceEndCullDistance = 180000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AAgrarianFoliagePatch::AAgrarianFoliagePatch()
|
||||||
|
{
|
||||||
|
PrimaryActorTick.bCanEverTick = false;
|
||||||
|
|
||||||
|
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
|
||||||
|
RootComponent = SceneRoot;
|
||||||
|
SceneRoot->SetMobility(EComponentMobility::Static);
|
||||||
|
|
||||||
|
TreeInstances = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("TreeInstances"));
|
||||||
|
TreeInstances->SetupAttachment(SceneRoot);
|
||||||
|
ConfigureFoliageComponent(TreeInstances, TEXT("BlockAll"));
|
||||||
|
|
||||||
|
ShrubInstances = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("ShrubInstances"));
|
||||||
|
ShrubInstances->SetupAttachment(SceneRoot);
|
||||||
|
ConfigureFoliageComponent(ShrubInstances, TEXT("NoCollision"));
|
||||||
|
|
||||||
|
GrassInstances = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("GrassInstances"));
|
||||||
|
GrassInstances->SetupAttachment(SceneRoot);
|
||||||
|
ConfigureFoliageComponent(GrassInstances, TEXT("NoCollision"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AAgrarianFoliagePatch::ClearFoliage()
|
||||||
|
{
|
||||||
|
if (TreeInstances)
|
||||||
|
{
|
||||||
|
TreeInstances->ClearInstances();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ShrubInstances)
|
||||||
|
{
|
||||||
|
ShrubInstances->ClearInstances();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GrassInstances)
|
||||||
|
{
|
||||||
|
GrassInstances->ClearInstances();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 AAgrarianFoliagePatch::AddTreeInstance(const FTransform& InstanceTransform)
|
||||||
|
{
|
||||||
|
return TreeInstances ? TreeInstances->AddInstance(InstanceTransform, true) : INDEX_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 AAgrarianFoliagePatch::AddShrubInstance(const FTransform& InstanceTransform)
|
||||||
|
{
|
||||||
|
return ShrubInstances ? ShrubInstances->AddInstance(InstanceTransform, true) : INDEX_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 AAgrarianFoliagePatch::AddGrassInstance(const FTransform& InstanceTransform)
|
||||||
|
{
|
||||||
|
return GrassInstances ? GrassInstances->AddInstance(InstanceTransform, true) : INDEX_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 AAgrarianFoliagePatch::GetTreeInstanceCount() const
|
||||||
|
{
|
||||||
|
return TreeInstances ? TreeInstances->GetInstanceCount() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 AAgrarianFoliagePatch::GetShrubInstanceCount() const
|
||||||
|
{
|
||||||
|
return ShrubInstances ? ShrubInstances->GetInstanceCount() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 AAgrarianFoliagePatch::GetGrassInstanceCount() const
|
||||||
|
{
|
||||||
|
return GrassInstances ? GrassInstances->GetInstanceCount() : 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// Copyright Pacificao. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "AgrarianFoliagePatch.generated.h"
|
||||||
|
|
||||||
|
class UHierarchicalInstancedStaticMeshComponent;
|
||||||
|
class USceneComponent;
|
||||||
|
|
||||||
|
UCLASS(Blueprintable)
|
||||||
|
class AGRARIANGAME_API AAgrarianFoliagePatch : public AActor
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
AAgrarianFoliagePatch();
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Foliage")
|
||||||
|
TObjectPtr<USceneComponent> SceneRoot;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Foliage")
|
||||||
|
TObjectPtr<UHierarchicalInstancedStaticMeshComponent> TreeInstances;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Foliage")
|
||||||
|
TObjectPtr<UHierarchicalInstancedStaticMeshComponent> ShrubInstances;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Foliage")
|
||||||
|
TObjectPtr<UHierarchicalInstancedStaticMeshComponent> GrassInstances;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|Foliage")
|
||||||
|
void ClearFoliage();
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|Foliage")
|
||||||
|
int32 AddTreeInstance(const FTransform& InstanceTransform);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|Foliage")
|
||||||
|
int32 AddShrubInstance(const FTransform& InstanceTransform);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|Foliage")
|
||||||
|
int32 AddGrassInstance(const FTransform& InstanceTransform);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Agrarian|Foliage")
|
||||||
|
int32 GetTreeInstanceCount() const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Agrarian|Foliage")
|
||||||
|
int32 GetShrubInstanceCount() const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Agrarian|Foliage")
|
||||||
|
int32 GetGrassInstanceCount() const;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user