Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 96 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,41 @@ let package = Package(

.library(name: "UntoldEngineAR", targets: ["UntoldEngineAR"]),

// Executable for the demo app (primary name)
// Executable for the showcase demo app (primary name)
.executable(
name: "showcasedemo",
targets: ["ShowcaseDemo"]
),

// Backward-compatible executable alias
.executable(
name: "untolddemo",
targets: ["DemoGame"]
targets: ["ShowcaseDemo"]
),

.executable(
name: "starterdemo",
targets: ["StarterDemo"]
),

.executable(
name: "largescenestreamingdemo",
targets: ["LargeSceneStreamingDemo"]
),

.executable(
name: "interactiongameplaydemo",
targets: ["InteractionGameplayDemo"]
),

.executable(
name: "renderingqualitydemo",
targets: ["RenderingQualityDemo"]
),

.executable(
name: "exporterpipelinedemo",
targets: ["ExporterPipelineDemo"]
),

.executable(
Expand All @@ -69,7 +100,7 @@ let package = Package(
// Backward-compatible executable alias
.executable(
name: "DemoGame",
targets: ["DemoGame"]
targets: ["ShowcaseDemo"]
),
],
dependencies: [],
Expand Down Expand Up @@ -148,9 +179,69 @@ let package = Package(
),
// These executables are macOS-only
.executableTarget(
name: "DemoGame",
name: "ShowcaseDemo",
dependencies: ["UntoldEngine"],
path: "Sources/Demos/ShowcaseDemo",
swiftSettings: [.swiftLanguageMode(.v6)],
linkerSettings: [
.linkedFramework("Metal"),
.linkedFramework("QuartzCore", .when(platforms: [.macOS, .iOS])),
.linkedFramework("AppKit", .when(platforms: [.macOS])),
]
),
.executableTarget(
name: "StarterDemo",
dependencies: ["UntoldEngine"],
path: "Sources/Demos/StarterDemo",
exclude: ["README.md"],
swiftSettings: [.swiftLanguageMode(.v6)],
linkerSettings: [
.linkedFramework("Metal"),
.linkedFramework("QuartzCore", .when(platforms: [.macOS, .iOS])),
.linkedFramework("AppKit", .when(platforms: [.macOS])),
]
),
.executableTarget(
name: "LargeSceneStreamingDemo",
dependencies: ["UntoldEngine"],
path: "Sources/Demos/LargeSceneStreamingDemo",
exclude: ["README.md"],
swiftSettings: [.swiftLanguageMode(.v6)],
linkerSettings: [
.linkedFramework("Metal"),
.linkedFramework("QuartzCore", .when(platforms: [.macOS, .iOS])),
.linkedFramework("AppKit", .when(platforms: [.macOS])),
]
),
.executableTarget(
name: "InteractionGameplayDemo",
dependencies: ["UntoldEngine"],
path: "Sources/Demos/InteractionGameplayDemo",
exclude: ["README.md"],
swiftSettings: [.swiftLanguageMode(.v6)],
linkerSettings: [
.linkedFramework("Metal"),
.linkedFramework("QuartzCore", .when(platforms: [.macOS, .iOS])),
.linkedFramework("AppKit", .when(platforms: [.macOS])),
]
),
.executableTarget(
name: "RenderingQualityDemo",
dependencies: ["UntoldEngine"],
path: "Sources/Demos/RenderingQualityDemo",
exclude: ["README.md"],
swiftSettings: [.swiftLanguageMode(.v6)],
linkerSettings: [
.linkedFramework("Metal"),
.linkedFramework("QuartzCore", .when(platforms: [.macOS, .iOS])),
.linkedFramework("AppKit", .when(platforms: [.macOS])),
]
),
.executableTarget(
name: "ExporterPipelineDemo",
dependencies: ["UntoldEngine"],
path: "Sources/DemoGame",
path: "Sources/Demos/ExporterPipelineDemo",
exclude: ["README.md"],
swiftSettings: [.swiftLanguageMode(.v6)],
linkerSettings: [
.linkedFramework("Metal"),
Expand Down
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,36 @@ http://www.haroldserrano.com

## 🚀 Try the Engine Right Now

The fastest way to experience Untold Engine is to run the demo project.
The best first step is to run the Starter Demo. It is intentionally small and
shows the basic shape of an Untold Engine app without the extra systems used by
the larger showcase.

> **Recommendation:** Use the latest stable release instead of the `develop`
> branch. The `develop` branch is the bleeding-edge version of Untold Engine and
> is updated frequently, so it may contain unstable changes or regressions.

Clone the repository and launch the demo:
Clone the repository and launch the Starter Demo:

```bash
git clone https://github.com/untoldengine/UntoldEngine.git
cd UntoldEngine
git checkout v0.13.3
swift run untolddemo
swift run starterdemo
```

The demo UI lets you see the engine in action right away. Using the `Remote Scene` drop-down menu, you can choose a scene to stream directly into the demo through the engine's **Asset Remote Streaming** support.
After that, run the focused demos based on what you want to learn:

| Demo | Command | Start here when you want to learn |
| --- | --- | --- |
| Starter Demo | `swift run starterdemo` | The minimal app structure: renderer setup, camera, light, input, and a simple scene. |
| Interaction / Gameplay Demo | `swift run interactiongameplaydemo` | Gameplay-style movement, input handling, animation switching, physics pause/resume, and parented entities. |
| Rendering Quality Demo | `swift run renderingqualitydemo` | Post-processing controls such as color grading, SSAO, bloom, vignette, depth of field, anti-aliasing, and debug views. |
| Large Scene Streaming Demo | `swift run largescenestreamingdemo` | Manifest-driven tiled scene streaming, LOD, batching, streaming stats, and large-world traversal. |
| Exporter Pipeline Demo | `swift run exporterpipelinedemo` | Loading exported `.untold` assets, applying exported animation clips, and checking validation metadata. |
| Showcase Demo | `swift run showcasedemo` | A broader engine showcase that combines many systems in one app. Use this after the focused demos. |

The demos live under `Sources/Demos`. The older `swift run untolddemo` command
still works as a compatibility alias for the Showcase Demo.

![untoldengine-image-2](/docs/images/engine-highlight-2.png)

Expand Down
200 changes: 200 additions & 0 deletions Sources/Demos/ExporterPipelineDemo/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
//
// AppDelegate.swift
// ExporterPipelineDemo
//

#if os(macOS)
import AppKit
import Observation
import SwiftUI
import UntoldEngine

@MainActor
@Observable
final class ExporterPipelineState {
var selectedAsset: ExportedAssetOption = .redplayer
var selectedAnimation: ExportedAnimationOption = .idle
var status = PipelineStatus()
}

@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
private enum Constants {
static let windowSize = NSSize(width: 1280, height: 780)
static let minimumWindowSize = NSSize(width: 920, height: 620)
}

private var window: NSWindow!
private var renderer: UntoldRenderer!
private var gameScene: GameScene!
private let state = ExporterPipelineState()

func applicationDidFinishLaunching(_: Notification) {
setupWindow()
setupRendererAndScene()
presentSceneView()
}

func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool {
true
}

private func setupWindow() {
window = NSWindow(
contentRect: NSRect(origin: .zero, size: Constants.windowSize),
styleMask: [.titled, .closable, .resizable],
backing: .buffered,
defer: false
)
window.title = "Untold Engine Exporter Pipeline Demo"
window.minSize = Constants.minimumWindowSize
window.center()
}

private func setupRendererAndScene() {
guard let renderer = UntoldRenderer.create() else {
print("Failed to initialize UntoldRenderer.")
NSApp.terminate(nil)
return
}

self.renderer = renderer
gameScene = GameScene()
gameScene.onStatusChanged = { [weak state] status in
Task { @MainActor in
state?.status = status
}
}

renderer.setupCallbacks(
gameUpdate: { [weak self] deltaTime in
self?.gameScene.update(deltaTime: deltaTime)
},
handleInput: { [weak self] in
self?.gameScene.handleInput()
}
)
}

private func presentSceneView() {
guard let renderer else { return }

let view = ExporterPipelineDemoView(
renderer: renderer,
state: state,
actions: .init(
loadAsset: { [weak self] asset in self?.gameScene.loadAsset(asset) },
loadAnimation: { [weak self] animation in self?.gameScene.loadAnimation(animation) },
reset: { [weak self] in self?.gameScene.resetScene() }
)
)

window.contentView = NSHostingView(rootView: view)
window.makeKeyAndOrderFront(nil)
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
}
}

private struct ExporterPipelineActions {
let loadAsset: (ExportedAssetOption) -> Void
let loadAnimation: (ExportedAnimationOption) -> Void
let reset: () -> Void
}

private struct ExporterPipelineDemoView: View {
let renderer: UntoldRenderer
@Bindable var state: ExporterPipelineState
let actions: ExporterPipelineActions

var body: some View {
ZStack(alignment: .topLeading) {
SceneView(renderer: renderer)
panel
.padding(16)
}
}

private var panel: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Exporter Pipeline")
.font(.headline)

Picker("Asset", selection: $state.selectedAsset) {
ForEach(ExportedAssetOption.allCases) { option in
Text(option.title).tag(option)
}
}

HStack {
Button("Load Model") {
actions.loadAsset(state.selectedAsset)
}
Button("Reset") {
actions.reset()
}
}

Divider()

Picker("Animation", selection: $state.selectedAnimation) {
ForEach(ExportedAnimationOption.allCases) { option in
Text(option.title).tag(option)
}
}
.disabled(!state.selectedAsset.supportsAnimation)

Button("Load Animation") {
actions.loadAnimation(state.selectedAnimation)
}
.disabled(!state.selectedAsset.supportsAnimation)

Divider()

statusRows

Text("Right-drag to orbit")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(12)
.frame(width: 390, alignment: .leading)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 8))
}

private var statusRows: some View {
VStack(alignment: .leading, spacing: 6) {
Text(state.status.message)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(2)

row("Entity", state.status.loadedEntity)
row("Asset exists", state.status.assetExists ? "Yes" : "No")
row("Validation", state.status.validation.found ? "Found" : "Missing")
row("Asset name", state.status.validation.assetName)
row("Meshes", "\(state.status.validation.meshCount)")
row("Vertices", "\(state.status.validation.totalVertices)")
row("Indices", "\(state.status.validation.totalIndices)")
row("Clips", state.status.animationClips)

Text(state.status.assetPath)
.font(.caption2)
.foregroundStyle(.secondary)
.lineLimit(3)
.textSelection(.enabled)
}
}

private func row(_ label: String, _ value: String) -> some View {
HStack {
Text(label)
.foregroundStyle(.secondary)
Spacer()
Text(value)
.monospacedDigit()
}
.font(.caption)
}
}
#endif
Loading
Loading