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:
Fr0zkaandClaude Opus 5.5 committed 2026-10-02 12:14:04 +02:00
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);