Skip to content

Latest commit

 

History

History
449 lines (352 loc) · 13.9 KB

UsingEngineAPI.md

File metadata and controls

449 lines (352 loc) · 13.9 KB

Engine Settings API

The Untold Engine uses a consistent style for its API:

setDomain(.property(value))
setDomain(.group(.property(value)))

This keeps user-facing setup code predictable and avoids requiring developers to remember which singleton or global variable owns each value.

Existing direct APIs such as LODConfig.shared, SSAOParams.shared, antiAliasingMode, and assetBasePath are still available for compatibility and advanced tuning.

Rendering

setRendering(.antiAliasing(.fxaa))
setRendering(.antiAliasing(.smaa))
setRendering(.antiAliasing(.msaa))
setRendering(.antiAliasing(.taa))      // temporal: jittered frames blended with their reprojected history
setRendering(.antiAliasing(.msaaTaa))  // 4× MSAA on the G-buffer plus the temporal resolve
setRendering(.antiAliasing(.none))
TAAParams.shared.historyWeight = 0.9   // share of the history in each resolved pixel (0…0.98)
TAAParams.shared.clipGamma = 1.25      // clip box = 3×3 mean ± γσ: lower rejects ghosts sooner, higher keeps pixel-thin features

setRendering(.debugView(.lit))
setRendering(.debugView(.depth))
setRendering(.debugView(.position))
setRendering(.debugView(.ssaoBlurred))

setRendering(.postProcessing(.enabled))
setRendering(.postProcessing(.disabled))

Renderer extensions use the same rendering domain:

setRendering(.extensions(.register(WaterRenderExtension())))
setRendering(.extensions(.unregister("water")))
setRendering(.extensions(.removeAll))

Wireframe parameters can also be configured through the same domain:

setRendering(.wireframe(.params(
    color: simd_float4(0.2, 0.85, 1.0, 0.65),
    fadeEnabled: true,
    fadeStart: 8.0,
    fadeEnd: 40.0,
    minimumAlpha: 0.08
)))

Directional-light (CSM) shadows cover a fixed real-world distance from the camera regardless of SceneRootTransform.shared.scale — a scene placed at a small scale (e.g. AR tabletop placement) packs its geometry into a much smaller slice of that distance, so the default spends most of the shadow map's resolution on empty space. Tighten it to roughly the placed content's physical size to sharpen shadow edges:

setRendering(.maxShadowCastingDistance(2.0))
getMaxShadowCastingDistance()

Objects too small on screen to be seen are left out of the frame: by default, any object whose bounds are under one pixel tall is not drawn and casts no shadow. A scene with tens of thousands of small parts (a building model with every clip and bolt) shows most of them at a pixel or less from a distance, and each would still cost a draw. The size is that of the sphere around the object's bounds, so a long thin object goes only once its whole length is that small. Raise the size to drop more, or set it to 0 to draw everything:

setRendering(.smallObjectCulling(pixels: 2.0))
getSmallObjectCullingPixels()

Occlusion culling tests each frame's objects against the depth of the frame before, seen from that frame's camera, so a moving camera costs nothing by itself. After a jump, or a flight faster than this many scene units a frame, the previous frame says too little and the test is skipped for that frame:

setRendering(.occlusionCullingMaxCameraStep(0.5))
getOcclusionCullingMaxCameraStep()

On visionOS, a layer configured with isFoveationEnabled hands the engine a rasterization rate map per eye, and the engine draws through it by default: dense where the eyes look, coarse in the periphery, and the only configuration in which the compositor accepts a maxRenderQuality above the platform default. Disabling it in such a layer is a diagnostic (the frame is drawn uniformly into textures the compositor still unwarps):

setRendering(.foveatedRendering(.disabled))
getFoveatedRenderingEnabled()

Entities that draw the same meshes with the same materials are encoded as one instanced draw, in the opaque pass and in the shadow cascades: a scene that places a model many times costs a draw per model, not per placement. Nothing has to be set up for it. An entity whose material is edited, or that is fading, skinned or deformed, is drawn on its own. The picture is the same either way; turn it off to compare, or to rule it out while tracking down a problem:

setRendering(.instancedDraws(.disabled))
getInstancedDrawsEnabled()

PostFX

Use setPostFX for individual post-processing and SSAO settings:

setPostFX(.preset(.cinematic))

setPostFX(.ssao(.enabled(true)))
setPostFX(.ssao(.radius(0.8)))
setPostFX(.ssao(.bias(0.025)))
setPostFX(.ssao(.intensity(0.75)))
setPostFX(.ssao(.quality(.balanced)))

setPostFX(.colorGrading(.enabled(true)))
setPostFX(.colorGrading(.exposure(-0.2)))
setPostFX(.colorGrading(.saturation(0.9)))

setPostFX(.vignette(.enabled(true)))
setPostFX(.vignette(.intensity(0.5)))
setPostFX(.vignette(.radius(0.8)))

setPostFX(.bloomThreshold(.enabled(true)))
setPostFX(.bloomThreshold(.threshold(0.6)))
setPostFX(.bloomThreshold(.intensity(0.8)))
setPostFX(.bloomComposite(.enabled(true)))
setPostFX(.bloomComposite(.intensity(1.0)))

setPostFX(.chromaticAberration(.enabled(true)))
setPostFX(.chromaticAberration(.intensity(0.02)))

setPostFX(.depthOfField(.enabled(true)))
setPostFX(.depthOfField(.focusDistance(4.7)))
setPostFX(.depthOfField(.focusRange(1.5)))
setPostFX(.depthOfField(.maxBlur(10.0)))

The nested property shape is intentional: the compiler keeps effect-specific settings grouped with the effect they belong to.

Engine Globals

setEngine(.assetBasePath(gameDataURL))
setEngine(.metrics(.enabled))
setEngine(.metrics(.disabled))

Geometry Streaming

setGeometryStreaming(.enabled(true))
setGeometryStreaming(.tileConcurrency(2))
setGeometryStreaming(.meshConcurrency(3))
setGeometryStreaming(.lodConcurrency(4))
setGeometryStreaming(.hlodConcurrency(4))
setGeometryStreaming(.queryRadius(500.0))
setGeometryStreaming(.frustumGate(.enabled(meshPadding: 5.0, tilePadding: 20.0)))
setGeometryStreaming(.velocityLookAhead(time: 0.5, minSpeed: 1.5))
setGeometryStreaming(.candidateSorting(importance: true, occlusion: true))
setGeometryStreaming(.minimumParsedTileResidentSeconds(8.0))
setGeometryStreaming(.timeouts(tileParse: 60.0, meshLoad: 60.0))

Keep one-shot streaming actions as commands:

GeometryStreamingSystem.shared.forceUnloadAllParsedTiles()

Static Batching

setBatching(.enabled(true))
setBatching(.cellSize(32.0))
setBatching(.maxDirtyCellsPerTick(8))
setBatching(.visibilityGatedBuild(true))
setBatching(.backgroundArtifactBuild(true))
setBatching(.runtimeTuning(.visionOSBalanced))
setBatching(.packSinglePlacements(true))   // a pack's parts placed once are marked static (default)

Entity tagging and rebuild commands remain explicit:

setEntityStaticBatchComponent(entityId: entity)
generateBatches()
clearSceneBatches()

LOD

setLOD(.fadeTransitions(.enabled(duration: 0.25)))
setLOD(.fadeTransitions(.disabled))
setLOD(.distanceBias(1.0))
setLOD(.hysteresis(5.0))
setLOD(.updateFrameInterval(4))
setLOD(.minimumCameraDisplacement(0.5))
setLOD(.distanceThresholds([50, 100, 200, 500]))

setLOD(.fadeTransitions(.enabled(duration:))) replaces the older direct configuration:

LODConfig.shared.enableFadeTransitions = true
LODConfig.shared.fadeTransitionTime = 0.25

Spatial Debug

setSpatialDebug(.octreeLeafBounds(.enabled(
    maxLeafNodeCount: 0,
    occupiedOnly: true,
    colorMode: .culling
)))
setSpatialDebug(.tileBounds(enabled: true, maxTileNodeCount: 500))
setSpatialDebug(.staticBatchCellBounds(enabled: true, maxCellCount: 2000, colorMode: .lod))
setSpatialDebug(.lodLevels(true))
setSpatialDebug(.textureStreamingTiers(true))
setSpatialDebug(.disabled)

Logger

setLogger(.level(.debug))
setLogger(.category(.tileStreaming, true))
setLogger(.categories([.streamingHeartbeat, .oocTiming], true))
setLogger(.resetCategories)

Logging itself stays message-oriented:

Logger.log(message: "Scene loaded", category: LogCategory.general.rawValue)

Camera

let camera = findGameCamera()
setCamera(.active(camera))
setCamera(.defaultFOV(70.0))
setCamera(.clipPlanes(near: 0.1, far: 1000.0))

Input (macOS)

// Registration
registerKeyboardEvents()
unregisterKeyboardEvents()
registerMouseEvents()

// Query
let keys = getKeyboardState()
let controller = getGameControllerState()

Input (iOS)

// Registration
registerTouchEvents(view: view)
unregisterTouchEvents()

// Query
let touch = getIOSTouchState()
let controller = getGameControllerState()   // game controller detection is automatic

Input (XR)

// Register / unregister XR spatial event handling
registerXREvents()
unregisterXREvents()

// Config
setInput(.xr(.pickingBackend(.octreeGPUPreferred)))
setInput(.xr(.twoHandRotateAxisMode(.dynamicSnapped)))
setInput(.xr(.sceneReady(true)))

// Query
let state = getXRSpatialInputState()
let ready = isXRSceneReady()

Input (PSVR2 Sense)

// Query
let state = getPSVR2SenseState()
let connected = isPSVR2SenseConnected()

if state.left.isTracked {
    let leftPosition = state.left.position
    let leftOrientation = state.left.orientation
}

PSVR2 Sense spatial poses are query-only — there is no setInput(.psvr2(...)) configuration API or trigger-haptics support. Button/trigger input still comes through getGameControllerState(). See UsingInputSystem.md for the full PSVR2ControllerPose field reference.

Spatial Manipulation (visionOS)

Use setSpatialManipulation for tuning thresholds and behaviour:

setSpatialManipulation(.intentTranslationThreshold(0.01))
setSpatialManipulation(.intentRotationThreshold(0.08))
setSpatialManipulation(.intentDominanceRatio(1.15))
setSpatialManipulation(.zoomScale(min: 0.05, max: 20.0))
setSpatialManipulation(.rotationDeltaLimit(perFrame: 0.12, twoHand: 0.35))
setSpatialManipulation(.twoHandRotationDeadzone(0.001))
setSpatialManipulation(.rotationSmoothing(factor: 0.25, deadzone: 0.002))
setSpatialManipulation(.classificationFrames(3))
setSpatialManipulation(.inputEpsilon(0.0001))

Per-frame lifecycle calls use free functions so callers do not need the shared instance:

let state = getXRSpatialInputState()

// Full pinch-drag + rotate arbitration for a single entity
processPinchTransformLifecycle(from: state)

// Simpler per-delta drag (call each frame while pinch is active)
applyPinchDragIfNeeded(from: state, entityId: myEntity, sensitivity: 1.0)

// Anchored drag / rotate for individual entities
// dragPlane filters world-axis displacement; it is not ray-plane picking.
processAnchoredPinchDragLifecycle(from: state, entityId: myEntity, dragPlane: .xz)
processAnchoredPinchDragLifecycle(
    from: state,
    entityId: myEntity,
    dragPlane: .xz,
    positionTransform: { worldPosition in
        simd_float3(worldPosition.x.rounded(), worldPosition.y, worldPosition.z.rounded())
    }
)

// Anchored drag / rotate for the entire scene root
processAnchoredSceneDragLifecycle(from: state)
processAnchoredSceneRotateLifecycle(from: state)

// Unified scene-root manipulation with drag/rotate arbitration
processAnchoredSceneManipulationLifecycle(from: state, dragSensitivity: 1.0, rotateSensitivity: 1.0)

// Two-hand zoom and rotate for a single entity
applyTwoHandZoomIfNeeded(from: state, entityId: myEntity)
applyTwoHandRotateIfNeeded(from: state, entityId: myEntity)

Session control:

resetSpatialManipulation()
endSpatialManipulation()
endAnchoredPinchDrag()
endAnchoredSceneDrag()
endAnchoredSceneManipulation()
endAnchoredSceneRotate()

Lighting

Use setLight(entityId:, _:) for all per-entity light configuration. Light-type-specific properties are grouped under sub-domains that follow the setDomain(.group(.property(value))) shape:

// Shared across all light types
setLight(entityId: light, .color(simd_float3(1.0, 0.85, 0.7)))
setLight(entityId: light, .intensity(2.5))
setLight(entityId: light, .power(300.0)) // radiometric watts for local lights

// Directional light
setLight(entityId: light, .directional(.active))
setLight(entityId: light, .directional(.castsShadow(true)))

// Point light
setLight(entityId: light, .point(.range(5.0)))
setLight(entityId: light, .point(.radius(0.1)))
setLight(entityId: light, .point(.falloff(0.7)))
setLight(entityId: light, .point(.attenuation(simd_float3(1, 0.5, 0.1))))
setLight(entityId: light, .point(.castsShadow(true)))

// Spot light
setLight(entityId: light, .spot(.range(8.0)))
setLight(entityId: light, .spot(.radius(0.05)))
setLight(entityId: light, .spot(.coneAngle(22.5))) // engine half-angle
setLight(entityId: light, .spot(.falloff(0.5)))
setLight(entityId: light, .spot(.attenuation(simd_float3(1, 0.5, 0.1))))
setLight(entityId: light, .spot(.castsShadow(true)))

// Area light
setLight(entityId: light, .area(.range(6.0)))
setLight(entityId: light, .area(.twoSided(true)))

Light creation and query functions remain explicit:

createDirLight(entityId: entity)
createPointLight(entityId: entity)
createSpotLight(entityId: entity)
createAreaLight(entityId: entity)

getLightColor(entityId: entity)
getLightIntensity(entityId: entity)
getLightRadius(entityId: entity)
getLightFalloff(entityId: entity)
getLightConeAngle(entityId: entity)

Style Rule

For Contributors, when adding new public settings, prefer one of these forms:

setLOD(.newProperty(value))
setRendering(.newProperty(value))
setPostFX(.effect(.newProperty(value)))
setEngine(.newProperty(value))
setGeometryStreaming(.newProperty(value))
setBatching(.newProperty(value))
setSpatialDebug(.newProperty(value))
setLogger(.newProperty(value))
setCamera(.newProperty(value))
registerKeyboardEvents()
registerMouseEvents()
registerTouchEvents(view: view)
setInput(.xr(.newProperty(value)))
setSpatialManipulation(.newProperty(value))
setLight(entityId: entity, .lightType(.newProperty(value)))
setSceneChannel(.contextGeometry, .renderMode(.wireframe))

Avoid adding new public examples that require direct mutation of shared singletons unless the setting is intentionally advanced/internal.