PlayKit.ai

3D Model Generation

Generate AI-powered 3D models in Unreal Engine using natural language prompts

3D Model Generation

The PlayKit SDK enables AI-powered 3D model generation using natural language prompts. Generate game-ready 3D models with textures and materials directly from text descriptions.

Overview

The UPlayKit3DClient component provides:

  • Text-to-3D Generation: Create 3D models from natural language descriptions
  • Automatic Polling: Task status is automatically monitored with progress updates
  • Progress Tracking: Real-time progress percentages and status changes
  • Quality Control: Configurable texture and geometry quality
  • PBR Materials: Optional physically-based rendering material generation

Important: Generated model URLs expire after 5 minutes. Download them immediately upon completion.

Adding the Component

Blueprint

  1. Select your Actor in the editor
  2. Click "Add Component"
  3. Search for "PlayKit 3D Client"
  4. Add to your Actor

C++

#include "Client/PlayKit3DClient.h"

UCLASS()
class AMyActor : public AActor
{
    GENERATED_BODY()

public:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "PlayKit")
    UPlayKit3DClient* ThreeDClient;

    AMyActor()
    {
        ThreeDClient = CreateDefaultSubobject<UPlayKit3DClient>(TEXT("3DClient"));
    }
};

Configuration Properties

Configure the component's default settings in the Details panel:

PropertyTypeDefaultDescription
Model NameString"tripo-3d"AI model to use for generation
Model VersionString"v2.5-20250123"Specific model version
Default Texture QualityEnumStandardTexture detail level (Standard/Detailed)
Default Geometry QualityEnumStandardMesh detail level (Standard/Detailed)
bDefault PBRBooleantrueEnable PBR materials by default
Default Face LimitInteger50000Maximum polygon count (0 = server default)
bDefault Auto SizeBooleanfalseAutomatically normalize model size

Basic Generation

Simple Generation

void AMyActor::GenerateSimpleModel()
{
    // Bind event handlers
    ThreeDClient->OnCompleted.AddDynamic(this, &AMyActor::OnModelGenerated);
    ThreeDClient->OnProgress.AddDynamic(this, &AMyActor::OnProgressUpdate);
    ThreeDClient->OnError.AddDynamic(this, &AMyActor::OnGenerationError);

    // Start generation
    ThreeDClient->Generate3D(TEXT("A medieval castle with stone walls and towers"));
}

void AMyActor::OnModelGenerated(FPlayKit3DResponse Response)
{
    if (Response.bSuccess)
    {
        FString ModelURL = Response.Task.Output.ModelUrl;
        UE_LOG(LogTemp, Log, TEXT("Model URL: %s"), *ModelURL);

        // CRITICAL: Download immediately (5-minute expiration)
        DownloadModelFromURL(ModelURL);
    }
}

void AMyActor::OnProgressUpdate(const FString& TaskId, int32 Progress)
{
    UE_LOG(LogTemp, Log, TEXT("Generation progress: %d%%"), Progress);
    // Update progress bar UI
}

void AMyActor::OnGenerationError(const FString& ErrorCode, const FString& ErrorMessage)
{
    UE_LOG(LogTemp, Error, TEXT("Generation failed [%s]: %s"), *ErrorCode, *ErrorMessage);
}

Blueprint

Event BeginPlay
    �?3D Client �?Generate 3D
    ├── Prompt: "A fantasy sword with glowing runes"
    └── On Completed �?Download Model (Model URL)

Advanced Generation

With Negative Prompt

ThreeDClient->Generate3DWithNegative(
    TEXT("A futuristic spaceship"),
    TEXT("low quality, blurry, broken geometry")  // Things to avoid
);

Full Configuration

void AMyActor::GenerateAdvancedModel()
{
    FPlayKit3DConfig Config;
    Config.Prompt = TEXT("A detailed dragon statue");
    Config.NegativePrompt = TEXT("low poly, simple, cartoonish");
    Config.bTexture = true;
    Config.bPBR = true;
    Config.TextureQuality = EPlayKit3DQuality::Detailed;
    Config.GeometryQuality = EPlayKit3DQuality::Detailed;
    Config.FaceLimit = 100000;  // Higher polygon count
    Config.bAutoSize = true;    // Normalize size
    Config.TextureSeed = 12345; // For reproducible results

    ThreeDClient->Generate3DAdvanced(Config);
}

Event Callbacks

The component provides four event types:

OnProgress

Fired during polling with current progress percentage:

UFUNCTION()
void OnProgressUpdate(const FString& TaskId, int32 Progress)
{
    // Update loading UI
    ProgressBar->SetPercent(Progress / 100.0f);
    StatusText->SetText(FText::Format(
        LOCTEXT("GeneratingProgress", "Generating: {0}%"),
        FText::AsNumber(Progress)
    ));
}

OnStatusChanged

Fired when task status changes (Queued �?Running �?Success):

UFUNCTION()
void OnStatusChanged(const FString& TaskId,
                     EPlayKit3DTaskStatus OldStatus,
                     EPlayKit3DTaskStatus NewStatus)
{
    switch (NewStatus)
    {
        case EPlayKit3DTaskStatus::Queued:
            StatusText->SetText(LOCTEXT("StatusQueued", "Queued..."));
            break;
        case EPlayKit3DTaskStatus::Running:
            StatusText->SetText(LOCTEXT("StatusRunning", "Generating..."));
            break;
        case EPlayKit3DTaskStatus::Success:
            StatusText->SetText(LOCTEXT("StatusSuccess", "Complete!"));
            break;
    }
}

OnCompleted

Fired when generation succeeds:

UFUNCTION()
void OnModelGenerated(FPlayKit3DResponse Response)
{
    if (Response.bSuccess)
    {
        // Main model file (GLB format)
        FString ModelURL = Response.Task.Output.ModelUrl;

        // Optional PBR model (if enabled)
        FString PBRModelURL = Response.Task.Output.PBRModelUrl;

        // Preview image
        FString PreviewImageURL = Response.Task.Output.RenderedImageUrl;

        // Timestamps
        FDateTime GeneratedAt = Response.Task.Output.GeneratedAt;
        int64 CreatedTimestamp = Response.Task.CreatedAt;
        int64 CompletedTimestamp = Response.Task.CompletedAt;

        // Download before URLs expire!
        DownloadModel(ModelURL);
    }
}

OnError

Fired when generation fails:

UFUNCTION()
void OnGenerationError(const FString& ErrorCode, const FString& ErrorMessage)
{
    if (ErrorCode == TEXT("REQUEST_IN_PROGRESS"))
    {
        // Task already running
        ShowMessage(TEXT("Please wait for current generation to complete"));
    }
    else if (ErrorCode == TEXT("PLAYER_INSUFFICIENT_CREDIT"))
    {
        // Not enough credits
        ShowRechargeDialog();
    }
    else
    {
        // Other errors
        UE_LOG(LogTemp, Error, TEXT("Error [%s]: %s"), *ErrorCode, *ErrorMessage);
    }
}

Task Management

Check Status

// Check if generation is running
if (ThreeDClient->IsProcessing())
{
    UE_LOG(LogTemp, Log, TEXT("Generation in progress"));
}

// Get current task info
FString TaskId = ThreeDClient->GetCurrentTaskId();
EPlayKit3DTaskStatus Status = ThreeDClient->GetCurrentStatus();
int32 Progress = ThreeDClient->GetCurrentProgress();

Cancel Task

void AMyActor::CancelGeneration()
{
    ThreeDClient->CancelTask();
    // Polling stops, events cleared
}

Manual Status Query

// Query specific task by ID (normally automatic)
ThreeDClient->QueryTaskStatus(TEXT("task-id-here"));

Quality Settings

Texture Quality

  • Standard: Faster generation, lower detail
  • Detailed: Slower generation, higher detail
Config.TextureQuality = EPlayKit3DQuality::Detailed;

Geometry Quality

Controls mesh complexity:

Config.GeometryQuality = EPlayKit3DQuality::Detailed;
Config.FaceLimit = 100000;  // Maximum polygon count

PBR Materials

Enable physically-based rendering materials:

Config.bPBR = true;  // Generates separate PBR model URL

Downloading Models

Critical: Model URLs expire after 5 minutes. Download immediately upon receiving OnCompleted event.

HTTP Download Example

void AMyActor::DownloadModelFromURL(const FString& URL)
{
    TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
    Request->SetURL(URL);
    Request->SetVerb(TEXT("GET"));
    Request->OnProcessRequestComplete().BindUObject(
        this, &AMyActor::HandleModelDownload);
    Request->ProcessRequest();
}

void AMyActor::HandleModelDownload(
    FHttpRequestPtr Request,
    FHttpResponsePtr Response,
    bool bWasSuccessful)
{
    if (bWasSuccessful && Response.IsValid())
    {
        TArray<uint8> ModelData = Response->GetContent();

        // Save to disk
        FString SavePath = FPaths::ProjectSavedDir() / TEXT("Models") / TEXT("generated.glb");
        FFileHelper::SaveArrayToFile(ModelData, *SavePath);

        // Import to Unreal (requires GLB importer plugin)
        ImportGLBModel(SavePath);
    }
}

Save to Content Browser

void AMyActor::SaveToContentBrowser(const TArray<uint8>& ModelData, const FString& AssetName)
{
    FString PackagePath = TEXT("/Game/GeneratedModels/") + AssetName;
    UPackage* Package = CreatePackage(*PackagePath);

    // Create asset from GLB data
    // (Implementation depends on your GLB import pipeline)
}

Prompt Engineering

Effective Prompts

// Good: Specific and detailed
TEXT("A low-poly treasure chest with metal hinges and lock, game-ready asset")

// Good: Style specification
TEXT("Stylized cartoon tree with rounded shapes, mobile game aesthetic")

// Good: Technical requirements
TEXT("Modular brick wall section, tileable, PBR textures, game asset")

Best Practices

  1. Be Specific: Include material types, style, purpose
  2. Set Expectations: "low-poly", "high-detail", "game-ready"
  3. Define Style: "realistic", "stylized", "cartoon", "sci-fi"
  4. Mention Use Case: "character prop", "environment asset", "weapon"

Negative Prompts

Use negative prompts to avoid unwanted features:

Config.NegativePrompt = TEXT("high poly, photorealistic, complex geometry");

Error Handling

Common Errors

Error CodeCauseSolution
REQUEST_IN_PROGRESSTask already runningWait for completion or cancel
INVALID_PROMPTEmpty promptProvide valid description
PLAYER_INSUFFICIENT_CREDITNot enough creditsPurchase credits
GENERATION_FAILEDServer errorRetry with different prompt
CONTENT_BANNEDViolates content policyModify prompt
NETWORK_ERRORConnection failedCheck internet connection

Retry Logic

void AMyActor::GenerateWithRetry(const FString& Prompt, int32 MaxRetries)
{
    ThreeDClient->OnError.AddDynamic(this, &AMyActor::HandleErrorWithRetry);
    ThreeDClient->Generate3D(Prompt);
}

void AMyActor::HandleErrorWithRetry(const FString& ErrorCode, const FString& ErrorMessage)
{
    if (RetryCount < MaxRetries && ErrorCode == TEXT("NETWORK_ERROR"))
    {
        RetryCount++;
        FTimerHandle RetryTimer;
        GetWorld()->GetTimerManager().SetTimer(
            RetryTimer,
            [this]() { ThreeDClient->Generate3D(LastPrompt); },
            2.0f,  // Wait 2 seconds
            false
        );
    }
}

Performance Considerations

Memory Management

// Component automatically cleans up on EndPlay
// Timers and HTTP requests are automatically cancelled

void AMyActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    Super::EndPlay(EndPlayReason);
    // No manual cleanup needed
}

Concurrent Requests

Only one generation task per component instance:

// Check before starting
if (!ThreeDClient->IsProcessing())
{
    ThreeDClient->Generate3D(Prompt);
}
else
{
    UE_LOG(LogTemp, Warning, TEXT("Task already in progress"));
}

Polling Efficiency

The component automatically:

  • Adjusts polling interval based on server recommendations
  • Retries on network failures
  • Stops polling on terminal states

Complete Example

UCLASS()
class AModelGeneratorActor : public AActor
{
    GENERATED_BODY()

public:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
    UPlayKit3DClient* ThreeDClient;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FString PromptText;

    AModelGeneratorActor()
    {
        ThreeDClient = CreateDefaultSubobject<UPlayKit3DClient>(TEXT("3DClient"));

        // Configure defaults
        ThreeDClient->ModelName = TEXT("tripo-3d");
        ThreeDClient->DefaultTextureQuality = EPlayKit3DQuality::Detailed;
        ThreeDClient->DefaultFaceLimit = 75000;
    }

protected:
    virtual void BeginPlay() override
    {
        Super::BeginPlay();

        // Bind events
        ThreeDClient->OnCompleted.AddDynamic(this, &AModelGeneratorActor::OnCompleted);
        ThreeDClient->OnProgress.AddDynamic(this, &AModelGeneratorActor::OnProgress);
        ThreeDClient->OnStatusChanged.AddDynamic(this, &AModelGeneratorActor::OnStatusChanged);
        ThreeDClient->OnError.AddDynamic(this, &AModelGeneratorActor::OnError);
    }

public:
    UFUNCTION(BlueprintCallable, Category = "PlayKit")
    void StartGeneration()
    {
        if (PromptText.IsEmpty())
        {
            UE_LOG(LogTemp, Warning, TEXT("Prompt is empty"));
            return;
        }

        ThreeDClient->Generate3D(PromptText);
    }

private:
    UFUNCTION()
    void OnCompleted(FPlayKit3DResponse Response)
    {
        if (Response.bSuccess)
        {
            DownloadAndImport(Response.Task.Output.ModelUrl);
        }
    }

    UFUNCTION()
    void OnProgress(const FString& TaskId, int32 Progress)
    {
        UE_LOG(LogTemp, Log, TEXT("Progress: %d%%"), Progress);
    }

    UFUNCTION()
    void OnStatusChanged(const FString& TaskId,
                        EPlayKit3DTaskStatus OldStatus,
                        EPlayKit3DTaskStatus NewStatus)
    {
        // Update UI
    }

    UFUNCTION()
    void OnError(const FString& ErrorCode, const FString& ErrorMessage)
    {
        UE_LOG(LogTemp, Error, TEXT("Error: %s"), *ErrorMessage);
    }

    void DownloadAndImport(const FString& URL)
    {
        // Download implementation
    }
};

Blueprint Nodes Reference

NodeCategoryDescription
Generate 3DPlayKit|3DStart generation with simple prompt
Generate 3D (With Negative)PlayKit|3DGenerate with negative prompt
Generate 3D (Advanced)PlayKit|3DGenerate with full configuration
Cancel TaskPlayKit|3DStop current generation
Is ProcessingPlayKit|3DCheck if task is running
Get Current Task IDPlayKit|3DGet active task identifier
Get Current StatusPlayKit|3DGet current task status
Get Current ProgressPlayKit|3DGet progress percentage

Next Steps