standalone: the owner's world outside Unreal -- bit-identical density, D3D11 fly-through viewer
Standalone/ (C++20, MSVC /O2 /arch:AVX2 /fp:precise, Build.ps1): copied density pipeline, op stack, cave morphology, passages, strate manager/composer, noise, DetSinCos, mesher, clipmap selection, with a UE compatibility layer (Compat/). Owner data exported once to Standalone/data/owner_world.json by a default-off explore-commandlet exporter (the only plugin-source change). 25 cm voxels. Parity: seed 0, 3,436 samples (strate edges, negative/far coords, rooms, tunnels, passages, landings): bit-identical. Viewer: full 7-level clipmap, per-level toggles 1-7, T = round winding tunnels (standalone option), N/P seed, F wireframe. Timing NOT yet a baseline: 515 s for 12,054 tiles (L0 70 s, L6 296 s); the compat TMap/TSet are vector-backed with linear lookup, which the plugin's caches do not expect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
a443eb8546
commit
05f62cff36
114 files changed
+68206
-3
No files matched your search
@@ -25,6 +25,8 @@
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "UObject/Package.h"
|
||||
#include "UObject/StrongObjectPtr.h"
|
||||
#include "UObject/UnrealType.h"
|
||||
#include "UObject/TextProperty.h"
|
||||
#include "UObject/UObjectGlobals.h"
|
||||
#include "VoxelCaveMorphology.h"
|
||||
#include "VoxelDiffLayer.h"
|
||||
@@ -38,11 +40,13 @@
|
||||
#include "VoxelNoise.h"
|
||||
#include "VoxelPassageGeometry.h"
|
||||
#include "VoxelSettings.h"
|
||||
#include "VoxelSeasonAsset.h"
|
||||
#include "VoxelStackSampler.h"
|
||||
#include "VoxelStrateDefinition.h"
|
||||
#include "VoxelStrateManager.h"
|
||||
#include "VoxelStrateMeasure.h"
|
||||
#include "VoxelStrateComposer.h"
|
||||
#include "VoxelTerrainOpDefinition.h"
|
||||
#include "VoxelTypes.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -379,6 +383,7 @@ struct FExploreArguments
|
||||
float RoomMouthRiseBlend = 0.0f;
|
||||
bool bPassageFloorWritesOverride = false;
|
||||
int32 PassageFloorWrites = 1;
|
||||
bool bStandaloneWorldExport = false;
|
||||
bool bRenderCameraOverride = false;
|
||||
FVector RenderCamera = FVector::ZeroVector;
|
||||
bool bRenderTargetOverride = false;
|
||||
@@ -418,6 +423,9 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
FString OutText;
|
||||
|
||||
OutArguments.bSeedOverride = FParse::Value(*Params, TEXT("seed="), OutArguments.Seed);
|
||||
int32 StandaloneWorldExport = 0;
|
||||
FParse::Value(*Params, TEXT("standaloneworldexport="), StandaloneWorldExport);
|
||||
OutArguments.bStandaloneWorldExport = StandaloneWorldExport != 0;
|
||||
FParse::Value(*Params, TEXT("archetype="), ArchetypeText);
|
||||
FParse::Value(*Params, TEXT("slot="), OutArguments.Slot);
|
||||
FParse::Value(*Params, TEXT("strateref="), OutArguments.StrateReference);
|
||||
@@ -939,6 +947,13 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
"densityslice must be origin x,y,z, direction x,y,z, sample count, and positive step.");
|
||||
return false;
|
||||
}
|
||||
if (OutArguments.bStandaloneWorldExport
|
||||
&& (!OutArguments.bExport || OutArguments.SettingsReference.IsEmpty()
|
||||
|| !OutArguments.StrateReference.IsEmpty()))
|
||||
{
|
||||
OutError = TEXT("standaloneworldexport requires modes=export and settingsref, and must omit strateref.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (OutText.IsEmpty())
|
||||
{
|
||||
@@ -1572,6 +1587,7 @@ bool PrepareRegionEvalWorld(
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!World.Definitions.IsValidIndex(Arguments.Slot))
|
||||
{
|
||||
OutError = TEXT("Region evaluator target definition is missing.");
|
||||
@@ -12196,6 +12212,525 @@ bool RunOpBounds(
|
||||
return true;
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonValue> StandaloneSerializeStruct(
|
||||
const UStruct* Type, const void* Data, TSet<const UObject*>& ActiveObjects,
|
||||
const UObject* Object = nullptr);
|
||||
|
||||
TSharedPtr<FJsonValue> StandaloneSerializeProperty(
|
||||
FProperty* Property, const void* Data, TSet<const UObject*>& ActiveObjects)
|
||||
{
|
||||
if (Property == nullptr || Data == nullptr) return MakeShared<FJsonValueNull>();
|
||||
if (const FBoolProperty* Bool = CastField<FBoolProperty>(Property))
|
||||
{
|
||||
return MakeShared<FJsonValueBoolean>(Bool->GetPropertyValue(Data));
|
||||
}
|
||||
if (const FEnumProperty* Enum = CastField<FEnumProperty>(Property))
|
||||
{
|
||||
return MakeShared<FJsonValueNumber>(static_cast<double>(
|
||||
Enum->GetUnderlyingProperty()->GetSignedIntPropertyValue(Data)));
|
||||
}
|
||||
if (const FNumericProperty* Numeric = CastField<FNumericProperty>(Property))
|
||||
{
|
||||
const double Value = Numeric->IsFloatingPoint()
|
||||
? Numeric->GetFloatingPointPropertyValue(Data)
|
||||
: static_cast<double>(Numeric->GetSignedIntPropertyValue(Data));
|
||||
return MakeShared<FJsonValueNumber>(Value);
|
||||
}
|
||||
if (const FStrProperty* String = CastField<FStrProperty>(Property))
|
||||
{
|
||||
return MakeShared<FJsonValueString>(String->GetPropertyValue(Data));
|
||||
}
|
||||
if (const FNameProperty* Name = CastField<FNameProperty>(Property))
|
||||
{
|
||||
return MakeShared<FJsonValueString>(Name->GetPropertyValue(Data).ToString());
|
||||
}
|
||||
if (const FTextProperty* Text = CastField<FTextProperty>(Property))
|
||||
{
|
||||
return MakeShared<FJsonValueString>(Text->GetPropertyValue(Data).ToString());
|
||||
}
|
||||
if (const FSoftObjectProperty* Soft = CastField<FSoftObjectProperty>(Property))
|
||||
{
|
||||
return MakeShared<FJsonValueString>(
|
||||
Soft->GetPropertyValue(Data).ToSoftObjectPath().ToString());
|
||||
}
|
||||
if (const FArrayProperty* Array = CastField<FArrayProperty>(Property))
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> Values;
|
||||
FScriptArrayHelper Helper(Array, Data);
|
||||
Values.Reserve(Helper.Num());
|
||||
for (int32 Index = 0; Index < Helper.Num(); ++Index)
|
||||
{
|
||||
Values.Add(StandaloneSerializeProperty(
|
||||
Array->Inner, Helper.GetRawPtr(Index), ActiveObjects));
|
||||
}
|
||||
return MakeShared<FJsonValueArray>(MoveTemp(Values));
|
||||
}
|
||||
if (const FSetProperty* Set = CastField<FSetProperty>(Property))
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> Values;
|
||||
FScriptSetHelper Helper(Set, Data);
|
||||
for (int32 Index = 0; Index < Helper.GetMaxIndex(); ++Index)
|
||||
{
|
||||
if (Helper.IsValidIndex(Index))
|
||||
{
|
||||
Values.Add(StandaloneSerializeProperty(
|
||||
Set->ElementProp, Helper.GetElementPtr(Index), ActiveObjects));
|
||||
}
|
||||
}
|
||||
return MakeShared<FJsonValueArray>(MoveTemp(Values));
|
||||
}
|
||||
if (const FMapProperty* Map = CastField<FMapProperty>(Property))
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> Values;
|
||||
FScriptMapHelper Helper(Map, Data);
|
||||
for (int32 Index = 0; Index < Helper.GetMaxIndex(); ++Index)
|
||||
{
|
||||
if (!Helper.IsValidIndex(Index)) continue;
|
||||
TSharedRef<FJsonObject> Pair = MakeShared<FJsonObject>();
|
||||
Pair->SetField(TEXT("key"), StandaloneSerializeProperty(
|
||||
Map->KeyProp, Helper.GetKeyPtr(Index), ActiveObjects));
|
||||
Pair->SetField(TEXT("value"), StandaloneSerializeProperty(
|
||||
Map->ValueProp, Helper.GetValuePtr(Index), ActiveObjects));
|
||||
Values.Add(MakeShared<FJsonValueObject>(Pair));
|
||||
}
|
||||
return MakeShared<FJsonValueArray>(MoveTemp(Values));
|
||||
}
|
||||
if (const FStructProperty* Struct = CastField<FStructProperty>(Property))
|
||||
{
|
||||
return StandaloneSerializeStruct(Struct->Struct, Data, ActiveObjects);
|
||||
}
|
||||
if (const FObjectPropertyBase* ObjectProperty = CastField<FObjectPropertyBase>(Property))
|
||||
{
|
||||
const UObject* Object = ObjectProperty->GetObjectPropertyValue(Data);
|
||||
if (!Object) return MakeShared<FJsonValueNull>();
|
||||
if (Object->GetClass()->GetName().StartsWith(TEXT("Voxel"), ESearchCase::CaseSensitive))
|
||||
{
|
||||
return StandaloneSerializeStruct(Object->GetClass(), Object, ActiveObjects, Object);
|
||||
}
|
||||
return MakeShared<FJsonValueString>(Object->GetPathName());
|
||||
}
|
||||
return MakeShared<FJsonValueNull>();
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonValue> StandaloneSerializeStruct(
|
||||
const UStruct* Type, const void* Data, TSet<const UObject*>& ActiveObjects,
|
||||
const UObject* Object)
|
||||
{
|
||||
if (Type == nullptr || Data == nullptr) return MakeShared<FJsonValueNull>();
|
||||
if (Object && ActiveObjects.Contains(Object))
|
||||
{
|
||||
return MakeShared<FJsonValueString>(Object->GetPathName());
|
||||
}
|
||||
if (Object) ActiveObjects.Add(Object);
|
||||
|
||||
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
||||
for (TFieldIterator<FProperty> It(Type); It; ++It)
|
||||
{
|
||||
FProperty* Property = *It;
|
||||
Result->SetField(Property->GetName(), StandaloneSerializeProperty(
|
||||
Property, Property->ContainerPtrToValuePtr<void>(const_cast<void*>(Data)),
|
||||
ActiveObjects));
|
||||
}
|
||||
if (Object) ActiveObjects.Remove(Object);
|
||||
return MakeShared<FJsonValueObject>(Result);
|
||||
}
|
||||
|
||||
template<class TWriter>
|
||||
void StandaloneWriteJsonValue(
|
||||
TWriter& Writer, const TCHAR* Identifier, const TSharedPtr<FJsonValue>& Value)
|
||||
{
|
||||
if (Identifier) Writer.WriteIdentifierPrefix(Identifier);
|
||||
if (!Value.IsValid() || Value->Type == EJson::Null)
|
||||
{
|
||||
Writer.WriteNull();
|
||||
}
|
||||
else if (Value->Type == EJson::Boolean)
|
||||
{
|
||||
Writer.WriteValue(Value->AsBool());
|
||||
}
|
||||
else if (Value->Type == EJson::Number)
|
||||
{
|
||||
Writer.WriteValue(Value->AsNumber());
|
||||
}
|
||||
else if (Value->Type == EJson::String)
|
||||
{
|
||||
Writer.WriteValue(Value->AsString());
|
||||
}
|
||||
else if (Value->Type == EJson::Array)
|
||||
{
|
||||
Writer.WriteArrayStart();
|
||||
for (const TSharedPtr<FJsonValue>& Element : Value->AsArray())
|
||||
{
|
||||
StandaloneWriteJsonValue(Writer, nullptr, Element);
|
||||
}
|
||||
Writer.WriteArrayEnd();
|
||||
}
|
||||
else if (Value->Type == EJson::Object)
|
||||
{
|
||||
Writer.WriteObjectStart();
|
||||
for (const TPair<FString, TSharedPtr<FJsonValue>>& Field : Value->AsObject()->Values)
|
||||
{
|
||||
StandaloneWriteJsonValue(Writer, *Field.Key, Field.Value);
|
||||
}
|
||||
Writer.WriteObjectEnd();
|
||||
}
|
||||
else
|
||||
{
|
||||
Writer.WriteNull();
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> StandaloneVectorJson(const FVector& Value)
|
||||
{
|
||||
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
||||
Result->SetNumberField(TEXT("x"), Value.X);
|
||||
Result->SetNumberField(TEXT("y"), Value.Y);
|
||||
Result->SetNumberField(TEXT("z"), Value.Z);
|
||||
return Result;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> StandaloneLandingJson(const FVoxelPassageLanding& Landing)
|
||||
{
|
||||
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
||||
Result->SetObjectField(TEXT("standing_point"), StandaloneVectorJson(Landing.StandingPoint));
|
||||
Result->SetObjectField(TEXT("door_point"), StandaloneVectorJson(Landing.DoorPoint));
|
||||
Result->SetObjectField(TEXT("door_direction"), StandaloneVectorJson(Landing.DoorDirection));
|
||||
Result->SetNumberField(TEXT("floor_z"), Landing.FloorZ);
|
||||
Result->SetNumberField(TEXT("ceiling_z"), Landing.CeilingZ);
|
||||
Result->SetNumberField(TEXT("half_width"), Landing.HalfWidth);
|
||||
Result->SetNumberField(TEXT("floor_thickness"), Landing.FloorThickness);
|
||||
Result->SetBoolField(TEXT("source_player_fit"), Landing.bSourcePlayerFit);
|
||||
return Result;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> StandalonePassageJson(const FVoxelPassage& Passage)
|
||||
{
|
||||
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
||||
Result->SetNumberField(TEXT("upper_strate_index"), Passage.UpperStrateIndex);
|
||||
Result->SetNumberField(TEXT("lower_strate_index"), Passage.LowerStrateIndex);
|
||||
Result->SetObjectField(TEXT("upper_point"), StandaloneVectorJson(Passage.UpperPoint));
|
||||
Result->SetObjectField(TEXT("lower_point"), StandaloneVectorJson(Passage.LowerPoint));
|
||||
Result->SetObjectField(TEXT("requested_upper_point"), StandaloneVectorJson(Passage.RequestedUpperPoint));
|
||||
Result->SetObjectField(TEXT("requested_lower_point"), StandaloneVectorJson(Passage.RequestedLowerPoint));
|
||||
Result->SetNumberField(TEXT("radius"), Passage.Radius);
|
||||
Result->SetNumberField(TEXT("passage_type"), static_cast<int32>(Passage.PassageType));
|
||||
auto WriteVectors = [](const TCHAR* Key, const TArray<FVector>& Source,
|
||||
TSharedRef<FJsonObject> Out)
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> Values;
|
||||
Values.Reserve(Source.Num());
|
||||
for (const FVector& Value : Source)
|
||||
{
|
||||
Values.Add(MakeShared<FJsonValueObject>(StandaloneVectorJson(Value)));
|
||||
}
|
||||
Out->SetArrayField(Key, MoveTemp(Values));
|
||||
};
|
||||
auto WriteFloats = [](const TCHAR* Key, const TArray<float>& Source,
|
||||
TSharedRef<FJsonObject> Out)
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> Values;
|
||||
Values.Reserve(Source.Num());
|
||||
for (float Value : Source) Values.Add(MakeShared<FJsonValueNumber>(Value));
|
||||
Out->SetArrayField(Key, MoveTemp(Values));
|
||||
};
|
||||
WriteVectors(TEXT("control_points"), Passage.ControlPoints, Result);
|
||||
WriteFloats(TEXT("control_radii"), Passage.ControlRadii, Result);
|
||||
Result->SetObjectField(TEXT("upper_landing"), StandaloneLandingJson(Passage.UpperLanding));
|
||||
Result->SetObjectField(TEXT("lower_landing"), StandaloneLandingJson(Passage.LowerLanding));
|
||||
Result->SetBoolField(TEXT("walkable_tunnel_contract"), Passage.bWalkableTunnelContract);
|
||||
Result->SetBoolField(TEXT("native_floor_enabled"), Passage.bNativeFloorEnabled);
|
||||
WriteFloats(TEXT("native_floor_profile_z"), Passage.NativeFloorProfileZ, Result);
|
||||
WriteFloats(TEXT("native_floor_relief_scales"), Passage.NativeFloorReliefScales, Result);
|
||||
Result->SetNumberField(TEXT("native_floor_relief_strength"), Passage.NativeFloorReliefStrength);
|
||||
Result->SetNumberField(TEXT("native_floor_relief_frequency"), Passage.NativeFloorReliefFrequency);
|
||||
Result->SetNumberField(TEXT("native_floor_seed"),
|
||||
static_cast<int64>(Passage.NativeFloorSeed));
|
||||
Result->SetNumberField(TEXT("tunnel_max_gradient_degrees"), Passage.TunnelMaxGradientDegrees);
|
||||
Result->SetNumberField(TEXT("tunnel_vertical_drop_voxels"), Passage.TunnelVerticalDropVoxels);
|
||||
Result->SetNumberField(TEXT("tunnel_required_horizontal_run_voxels"), Passage.TunnelRequiredHorizontalRunVoxels);
|
||||
Result->SetNumberField(TEXT("tunnel_horizontal_path_length_voxels"), Passage.TunnelHorizontalPathLengthVoxels);
|
||||
Result->SetNumberField(TEXT("tunnel_minimum_clear_width_voxels"), Passage.TunnelMinimumClearWidthVoxels);
|
||||
Result->SetNumberField(TEXT("tunnel_clear_height_voxels"), Passage.TunnelClearHeightVoxels);
|
||||
Result->SetObjectField(TEXT("bound_center"), StandaloneVectorJson(Passage.BoundCenter));
|
||||
Result->SetNumberField(TEXT("bound_radius"), Passage.BoundRadius);
|
||||
Result->SetNumberField(TEXT("bound_radius_sq"), Passage.BoundRadiusSq);
|
||||
return Result;
|
||||
}
|
||||
|
||||
struct FStandaloneExportSample
|
||||
{
|
||||
FVector Position = FVector::ZeroVector;
|
||||
FString Kind;
|
||||
int32 StrateIndex = INDEX_NONE;
|
||||
float Density = 0.0f;
|
||||
};
|
||||
|
||||
bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutError, FString* OutJson)
|
||||
{
|
||||
UVoxelSettings* AuthoredSettings = LoadObject<UVoxelSettings>(
|
||||
nullptr, *Arguments.SettingsReference);
|
||||
if (!AuthoredSettings)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Could not load owner settings asset '%s'."),
|
||||
*Arguments.SettingsReference);
|
||||
return false;
|
||||
}
|
||||
UVoxelStrateManager* Manager = NewObject<UVoxelStrateManager>(
|
||||
GetTransientPackage(), NAME_None, RF_Transient);
|
||||
int32 Seed = AuthoredSettings->Seed;
|
||||
float OriginSpineRadius = AuthoredSettings->OriginSpineRadius;
|
||||
float WorldRadiusVoxels = AuthoredSettings->WorldRadiusVoxels;
|
||||
if (const UVoxelSeasonAsset* Season = AuthoredSettings->Season.LoadSynchronous())
|
||||
{
|
||||
Seed = Season->GetSeasonSeed();
|
||||
OriginSpineRadius = Season->GetOriginSpineRadius();
|
||||
WorldRadiusVoxels = Season->GetWorldRadiusVoxels();
|
||||
}
|
||||
if (!Manager->Initialize(AuthoredSettings, Seed))
|
||||
{
|
||||
OutError = TEXT("Owner settings did not produce a valid resolved strate layout.");
|
||||
return false;
|
||||
}
|
||||
Seed = Manager->GetWorldSeed();
|
||||
UVoxelGenerator* Generator = NewObject<UVoxelGenerator>(
|
||||
GetTransientPackage(), NAME_None, RF_Transient);
|
||||
Generator->InitializeSettings(AuthoredSettings);
|
||||
Generator->SetStrateManager(Manager);
|
||||
|
||||
TArray<FStandaloneExportSample> Samples;
|
||||
auto AddSample = [&Samples](const FVector& Position, const TCHAR* Kind, int32 StrateIndex)
|
||||
{
|
||||
if (!VoxelMath::IsFinite(Position.X) || !VoxelMath::IsFinite(Position.Y)
|
||||
|| !VoxelMath::IsFinite(Position.Z)) return;
|
||||
FStandaloneExportSample Sample;
|
||||
Sample.Position = Position;
|
||||
Sample.Kind = Kind;
|
||||
Sample.StrateIndex = StrateIndex;
|
||||
Samples.Add(MoveTemp(Sample));
|
||||
};
|
||||
|
||||
const int32 XYAnchors[] = { 0, -1, 1, -128, 128, -512, 512, -2048, 2048, -8192, 8192 };
|
||||
FRandomStream SampleRandom(Seed ^ 0x13579BDF);
|
||||
FVector Camera = FVector::ZeroVector;
|
||||
bool bCameraFound = false;
|
||||
for (const FStrateSlot& Slot : Manager->GetLayout())
|
||||
{
|
||||
const float TopZ = static_cast<float>(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||
const float MidZ = (TopZ + BottomZ) * 0.5f;
|
||||
const int32 MidChunkZ = FMath::FloorToInt(MidZ / CHUNK_SIZE);
|
||||
for (int32 ZOffset : { -2, -1, 0, 1, 2 })
|
||||
{
|
||||
for (int32 Y : XYAnchors)
|
||||
for (int32 X : XYAnchors)
|
||||
{
|
||||
AddSample(FVector(X, Y, TopZ + ZOffset), TEXT("strate_boundary"), Slot.StrateIndex);
|
||||
AddSample(FVector(X, Y, BottomZ + ZOffset), TEXT("strate_boundary"), Slot.StrateIndex);
|
||||
}
|
||||
}
|
||||
for (int32 Index = 0; Index < 384; ++Index)
|
||||
{
|
||||
const float X = static_cast<float>(SampleRandom.RandRange(-4096, 4096));
|
||||
const float Y = static_cast<float>(SampleRandom.RandRange(-4096, 4096));
|
||||
const float Z = BottomZ + SampleRandom.RandRange(0, FMath::Max(1, FMath::FloorToInt(TopZ - BottomZ)));
|
||||
AddSample(FVector(X, Y, Z), TEXT("varied_world"), Slot.StrateIndex);
|
||||
}
|
||||
|
||||
if (Slot.Definition)
|
||||
{
|
||||
const FStrateGenerationParams Params = Manager->GetGenerationParams(
|
||||
FIntVector(0, 0, MidChunkZ));
|
||||
FChunkSDFCache Cache;
|
||||
VoxelCaveMorphology::BuildChunkCache(
|
||||
Cache, -384.0f, -384.0f, 384.0f, 384.0f, Params,
|
||||
static_cast<uint32>(Seed), Slot.StrateIndex,
|
||||
&Slot.Definition->TerrainOperations);
|
||||
for (const FCachedRoom& Room : Cache.Rooms)
|
||||
{
|
||||
const FVector Candidate = Room.Center;
|
||||
const float Density = Generator->GetDensityAt(
|
||||
Candidate.X, Candidate.Y, Candidate.Z);
|
||||
if (Density >= 0.0f)
|
||||
{
|
||||
AddSample(Candidate, TEXT("room_interior"), Slot.StrateIndex);
|
||||
if (Slot.StrateIndex == 0 && !bCameraFound
|
||||
&& Generator->GetDensityAt(Candidate.X, Candidate.Y, Candidate.Z + 8.0f) >= 0.0f
|
||||
&& Generator->GetDensityAt(Candidate.X, Candidate.Y, Candidate.Z + 16.0f) >= 0.0f)
|
||||
{
|
||||
Camera = Candidate;
|
||||
bCameraFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const FCachedTunnel& Tunnel : Cache.Tunnels)
|
||||
{
|
||||
for (int32 PointIndex = 0; PointIndex < Tunnel.WorldControlPoints.Num(); ++PointIndex)
|
||||
{
|
||||
const FVector Candidate = Tunnel.WorldControlPoints[PointIndex];
|
||||
const float Density = Generator->GetDensityAt(
|
||||
Candidate.X, Candidate.Y, Candidate.Z);
|
||||
if (Density >= 0.0f)
|
||||
{
|
||||
AddSample(Candidate, TEXT("tunnel_interior"), Slot.StrateIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Slot.StrateIndex == 0)
|
||||
{
|
||||
// Prefer an open, body-height column in the first authored strate. This seed pose is
|
||||
// exported to the standalone viewer; it uses only the owner's live base field.
|
||||
for (int32 Radius = 0; Radius <= 128 && !bCameraFound; Radius += 8)
|
||||
for (int32 Y = -Radius; Y <= Radius && !bCameraFound; Y += 8)
|
||||
for (int32 X = -Radius; X <= Radius && !bCameraFound; X += 8)
|
||||
{
|
||||
const float Z = MidZ;
|
||||
if (Generator->GetDensityAt(static_cast<float>(X), static_cast<float>(Y), Z) >= 0.0f
|
||||
&& Generator->GetDensityAt(static_cast<float>(X), static_cast<float>(Y), Z + 8.0f) >= 0.0f
|
||||
&& Generator->GetDensityAt(static_cast<float>(X), static_cast<float>(Y), Z + 16.0f) >= 0.0f)
|
||||
{
|
||||
Camera = FVector(X, Y, Z + 2.0f);
|
||||
bCameraFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const FVoxelPassage& Passage : Manager->GetPassages())
|
||||
{
|
||||
for (const FVector& Point : Passage.ControlPoints)
|
||||
{
|
||||
AddSample(Point, TEXT("inter_strate_passage"), Passage.UpperStrateIndex);
|
||||
}
|
||||
AddSample(Passage.UpperLanding.StandingPoint, TEXT("passage_landing"), Passage.UpperStrateIndex);
|
||||
AddSample(Passage.LowerLanding.StandingPoint, TEXT("passage_landing"), Passage.LowerStrateIndex);
|
||||
}
|
||||
|
||||
for (FStandaloneExportSample& Sample : Samples)
|
||||
{
|
||||
Sample.Density = Generator->GetDensityAt(
|
||||
static_cast<float>(Sample.Position.X), static_cast<float>(Sample.Position.Y),
|
||||
static_cast<float>(Sample.Position.Z));
|
||||
}
|
||||
|
||||
if (!bCameraFound)
|
||||
{
|
||||
OutError = TEXT("Could not locate open camera space in the owner's first strate.");
|
||||
return false;
|
||||
}
|
||||
|
||||
FString Json;
|
||||
TSharedRef<FExploreJsonWriter> Writer =
|
||||
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json);
|
||||
Writer->WriteObjectStart();
|
||||
Writer->WriteValue(TEXT("schema_version"), 1);
|
||||
Writer->WriteValue(TEXT("source"), TEXT("VoxelForgeExplore standalone base-world export"));
|
||||
Writer->WriteValue(TEXT("voxel_size_cm"), VOXEL_SIZE);
|
||||
Writer->WriteValue(TEXT("seed"), Seed);
|
||||
Writer->WriteValue(TEXT("open_surface_entry"), AuthoredSettings->bOpenSurfaceEntry);
|
||||
Writer->WriteValue(TEXT("origin_spine_radius"), OriginSpineRadius);
|
||||
Writer->WriteValue(TEXT("world_radius_voxels"), WorldRadiusVoxels);
|
||||
Writer->WriteValue(TEXT("edge_seal_thickness"), AuthoredSettings->EdgeSealThickness);
|
||||
Writer->WriteValue(TEXT("total_strates"), Manager->GetNumStrates());
|
||||
Writer->WriteValue(TEXT("inter_strate_gap_chunks"), AuthoredSettings->InterStrateGapChunks);
|
||||
Writer->WriteValue(TEXT("clip_radius"), AuthoredSettings->ClipRadius);
|
||||
Writer->WriteValue(TEXT("max_clip_level"), AuthoredSettings->MaxClipLevel);
|
||||
Writer->WriteValue(TEXT("full_res_clip_levels"), AuthoredSettings->FullResClipLevels);
|
||||
Writer->WriteValue(TEXT("coarse_tile_cells"), AuthoredSettings->CoarseTileCells);
|
||||
Writer->WriteValue(TEXT("render_distance_chunks"), AuthoredSettings->RenderDistanceChunks);
|
||||
Writer->WriteValue(TEXT("generate_skirts"), AuthoredSettings->bGenerateSkirts);
|
||||
Writer->WriteValue(TEXT("skirt_cells"), AuthoredSettings->SkirtCells);
|
||||
Writer->WriteValue(TEXT("lod_octave_drop"), AuthoredSettings->LODOctaveDrop);
|
||||
Writer->WriteValue(TEXT("strate_content_cut_min_level"), AuthoredSettings->GetEffectiveStrateContentCutMinLevel());
|
||||
TSet<const UObject*> ActiveObjects;
|
||||
StandaloneWriteJsonValue(*Writer, TEXT("settings_asset"),
|
||||
StandaloneSerializeStruct(AuthoredSettings->GetClass(), AuthoredSettings,
|
||||
ActiveObjects, AuthoredSettings));
|
||||
|
||||
Writer->WriteArrayStart(TEXT("strates"));
|
||||
for (const FStrateSlot& Slot : Manager->GetLayout())
|
||||
{
|
||||
Writer->WriteObjectStart();
|
||||
Writer->WriteValue(TEXT("index"), Slot.StrateIndex);
|
||||
Writer->WriteValue(TEXT("top_chunk_z"), Slot.TopChunkZ);
|
||||
Writer->WriteValue(TEXT("bottom_chunk_z"), Slot.BottomChunkZ);
|
||||
Writer->WriteValue(TEXT("height_chunks"), Slot.HeightInChunks);
|
||||
StandaloneWriteJsonValue(*Writer, TEXT("asset"),
|
||||
StandaloneSerializeStruct(Slot.Definition->GetClass(), Slot.Definition,
|
||||
ActiveObjects, Slot.Definition));
|
||||
StandaloneWriteJsonValue(*Writer, TEXT("generation_params"),
|
||||
StandaloneSerializeStruct(FStrateGenerationParams::StaticStruct(),
|
||||
&Slot.Definition->GenerationParams, ActiveObjects));
|
||||
StandaloneWriteJsonValue(*Writer, TEXT("disturbances"),
|
||||
StandaloneSerializeStruct(FStrateDisturbanceParams::StaticStruct(),
|
||||
&Slot.Definition->Disturbances, ActiveObjects));
|
||||
StandaloneWriteJsonValue(*Writer, TEXT("passage_config"),
|
||||
StandaloneSerializeStruct(FStratePassageConfig::StaticStruct(),
|
||||
&Slot.Definition->PassageConfig, ActiveObjects));
|
||||
Writer->WriteArrayStart(TEXT("terrain_operation_assets"));
|
||||
for (const FStrateTerrainOpEntry& Entry : Slot.Definition->TerrainOperations)
|
||||
{
|
||||
Writer->WriteObjectStart();
|
||||
Writer->WriteValue(TEXT("weight"), Entry.Weight);
|
||||
Writer->WriteValue(TEXT("probability"), Entry.Probability);
|
||||
Writer->WriteValue(TEXT("asset_path"), Entry.Operation.ToString());
|
||||
if (const UVoxelTerrainOpDefinition* Op = Entry.Operation.LoadSynchronous())
|
||||
{
|
||||
StandaloneWriteJsonValue(*Writer, TEXT("asset"),
|
||||
StandaloneSerializeStruct(Op->GetClass(), Op, ActiveObjects, Op));
|
||||
}
|
||||
Writer->WriteObjectEnd();
|
||||
}
|
||||
Writer->WriteArrayEnd();
|
||||
Writer->WriteObjectEnd();
|
||||
}
|
||||
Writer->WriteArrayEnd();
|
||||
|
||||
Writer->WriteArrayStart(TEXT("passages"));
|
||||
for (const FVoxelPassage& Passage : Manager->GetPassages())
|
||||
{
|
||||
StandaloneWriteJsonValue(*Writer, nullptr,
|
||||
MakeShared<FJsonValueObject>(StandalonePassageJson(Passage)));
|
||||
}
|
||||
Writer->WriteArrayEnd();
|
||||
Writer->WriteArrayStart(TEXT("density_samples"));
|
||||
for (const FStandaloneExportSample& Sample : Samples)
|
||||
{
|
||||
Writer->WriteObjectStart();
|
||||
Writer->WriteValue(TEXT("x"), static_cast<double>(Sample.Position.X));
|
||||
Writer->WriteValue(TEXT("y"), static_cast<double>(Sample.Position.Y));
|
||||
Writer->WriteValue(TEXT("z"), static_cast<double>(Sample.Position.Z));
|
||||
Writer->WriteValue(TEXT("density"), static_cast<double>(Sample.Density));
|
||||
Writer->WriteValue(TEXT("kind"), Sample.Kind);
|
||||
Writer->WriteValue(TEXT("strate_index"), Sample.StrateIndex);
|
||||
Writer->WriteObjectEnd();
|
||||
}
|
||||
Writer->WriteArrayEnd();
|
||||
Writer->WriteObjectStart(TEXT("viewer_start"));
|
||||
Writer->WriteValue(TEXT("found_open_space"), bCameraFound);
|
||||
Writer->WriteValue(TEXT("x"), static_cast<double>(Camera.X));
|
||||
Writer->WriteValue(TEXT("y"), static_cast<double>(Camera.Y));
|
||||
Writer->WriteValue(TEXT("z"), static_cast<double>(Camera.Z));
|
||||
Writer->WriteObjectEnd();
|
||||
Writer->WriteObjectEnd();
|
||||
Writer->Close();
|
||||
|
||||
const FString Path = FPaths::Combine(Arguments.OutDirectory, TEXT("owner_world.json"));
|
||||
if (!FFileHelper::SaveStringToFile(
|
||||
Json, *Path, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Could not write standalone owner data to '%s'."), *Path);
|
||||
return false;
|
||||
}
|
||||
if (OutJson) *OutJson = Json;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeStandaloneExport] seed=%d voxel_size_cm=%.3f strates=%d passages=%d samples=%d camera_found=%d file=%s"),
|
||||
Seed, VOXEL_SIZE, Manager->GetNumStrates(), Manager->GetPassages().Num(),
|
||||
Samples.Num(), bCameraFound ? 1 : 0, *Path);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UVoxelForgeExploreCommandlet::UVoxelForgeExploreCommandlet()
|
||||
@@ -12239,6 +12774,16 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (Arguments.bStandaloneWorldExport)
|
||||
{
|
||||
if (!RunStandaloneWorldExport(Arguments, Error, OutJson))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] %s"), *Error);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
FExploreBudget Budget(
|
||||
MainStartSeconds,
|
||||
static_cast<double>(Arguments.BudgetMinutes) * 60.0);
|
||||
|
||||
Reference in new issue
Block a user