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
27 changes: 27 additions & 0 deletions Sources/UntoldEngine/Renderer/Pipelines/RenderPipeLines.swift
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,31 @@ public func InitDebugPipeline() -> RenderPipeline? {
)
}

public func InitTAAResolvePipeline() -> RenderPipeline? {
let wf = renderInfo.colorPipeline.working
return CreatePipeline(
vertexShader: "vertexTAAResolveShader",
fragmentShader: "fragmentTAAResolveShader",
vertexDescriptor: createPostProcessVertexDescriptor(),
colorFormats: [wf.lookOutput],
depthFormat: .invalid,
depthEnabled: false,
name: "TAA Resolve Pipeline"
)
}

public func InitVelocityPipeline() -> RenderPipeline? {
CreatePipeline(
vertexShader: "vertexVelocityShader",
fragmentShader: "fragmentVelocityShader",
vertexDescriptor: createPostProcessVertexDescriptor(),
colorFormats: [.rg16Float],
depthFormat: .invalid,
depthEnabled: false,
name: "Velocity Pipeline"
)
}

public func InitTransparencyPipeline() -> RenderPipeline? {
CreatePipeline(
vertexShader: "vertexModelShader",
Expand Down Expand Up @@ -677,6 +702,8 @@ public func DefaultPipeLines() -> [(RenderPipelineType, RenderPipelineInitBlock)
(.outputTransform, InitOutputTransformPipeline),
(.debug, InitDebugPipeline),
(.transparency, InitTransparencyPipeline),
(.velocity, InitVelocityPipeline),
(.taaResolve, InitTAAResolvePipeline),
]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,6 @@ public extension RenderPipelineType {
static let fxaa: RenderPipelineType = "fxaa"
static let transparency: RenderPipelineType = "transparency"
static let debug: RenderPipelineType = "debug"
static let velocity: RenderPipelineType = "velocity"
static let taaResolve: RenderPipelineType = "taaResolve"
}
66 changes: 66 additions & 0 deletions Sources/UntoldEngine/Renderer/RenderInitializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,16 @@ func initRenderPassDescriptors() {
],
depthAttachment: nil
)

// TAA: velocity pass writes rg16Float motion vectors each frame.
renderInfo.velocityRenderPassDescriptor = createRenderPassDescriptor(
width: Int(renderInfo.viewPort.x),
height: Int(renderInfo.viewPort.y),
colorAttachments: [
(textureResources.velocityTexture, .clear, .store, MTLClearColorMake(0.0, 0.0, 0.0, 0.0)),
],
depthAttachment: nil
)
}

func initTextureResources() {
Expand Down Expand Up @@ -635,6 +645,62 @@ func initTextureResources() {
storageMode: .shared
)

// TAA: screen-space motion vectors (rg16Float, camera-only velocity).
textureResources.velocityTexture = createTexture(
device: renderInfo.device,
label: "Velocity Texture",
pixelFormat: .rg16Float,
width: viewportWidth,
height: viewportHeight,
usage: [.shaderRead, .renderTarget],
storageMode: .shared
)

// TAA output, history, and position-history textures (one set per eye in stereo).
let eyeCount = renderInfo.isXRStereoMode ? 2 : 1
for eye in 0 ..< eyeCount {
// .shared so the CPU can read it back via getBytes (PSNR tests, debug captures).
// History and position-history remain .private — they are GPU-internal only.
let outTex = createTexture(
device: renderInfo.device,
label: "TAA Output Eye \(eye)",
pixelFormat: wf.lookOutput,
width: viewportWidth,
height: viewportHeight,
usage: [.shaderRead, .renderTarget],
storageMode: .shared
)
let histTex = createTexture(
device: renderInfo.device,
label: "TAA History Eye \(eye)",
pixelFormat: wf.lookOutput,
width: viewportWidth,
height: viewportHeight,
usage: [.shaderRead, .renderTarget],
storageMode: .private
)
let posHistTex = createTexture(
device: renderInfo.device,
label: "TAA Position History Eye \(eye)",
pixelFormat: wf.gBufferPosition,
width: viewportWidth,
height: viewportHeight,
usage: [.shaderRead, .renderTarget],
storageMode: .private
)

if eye == 0 {
textureResources.taaOutputTexture = outTex
textureResources.taaHistoryTexture = histTex
textureResources.taaPositionHistoryTexture = posHistTex
}
textureResources.taaOutputTextureEye[eye] = outTex
textureResources.taaHistoryTextureEye[eye] = histTex
textureResources.taaPositionHistoryTextureEye[eye] = posHistTex
}
TemporalAA.shared.markReady()
Logger.log(message: "TAA textures initialised (\(eyeCount) eye(s)).")

textureResources.fxaaTexture = createTexture(
device: renderInfo.device,
label: "FXAA Output Texture",
Expand Down
1 change: 1 addition & 0 deletions Sources/UntoldEngine/Renderer/RenderPasses.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1955,6 +1955,7 @@ public enum RenderPasses {
}
renderInfo.offscreenRenderPassDescriptor.depthAttachment.loadAction = .load
// set the states for the pipeline
renderPassDescriptor.colorAttachments[0].texture = textureResources.deferredColorMap
renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadAction.load
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(1.0, 1.0, 1.0, 1.0)
renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreAction.store
Expand Down
30 changes: 30 additions & 0 deletions Sources/UntoldEngine/Renderer/RenderResources.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ public struct RenderInfo {
public var isXRStereoMode: Bool = false
public var xrEye0ViewProjection: simd_float4x4 = matrix_identity_float4x4
public var xrEye1ViewProjection: simd_float4x4 = matrix_identity_float4x4

// TAA / MetalFX Temporal Scaler
// unjitteredPerspectiveSpace is the clean projection matrix used by culling, shadow cascades,
// and velocity computation. perspectiveSpace carries the per-frame Halton jitter for rasterisation.
public var unjitteredPerspectiveSpace: simd_float4x4 = matrix_identity_float4x4
public var taaJitterX: Float = 0 // current frame jitter offset in pixels (X)
public var taaJitterY: Float = 0 // current frame jitter offset in pixels (Y)

// Per-eye unjittered view-projection matrices for camera-only velocity computation.
// Index 0 = left / mono, index 1 = right (XR stereo).
public var prevViewProjectionEye: [simd_float4x4] = [matrix_identity_float4x4, matrix_identity_float4x4]
public var currentViewProjectionEye: [simd_float4x4] = [matrix_identity_float4x4, matrix_identity_float4x4]

/// Velocity pass render pass descriptor (writes rg16Float motion vectors)
public var velocityRenderPassDescriptor: MTLRenderPassDescriptor!
}

@inline(__always)
Expand Down Expand Up @@ -227,6 +242,21 @@ public struct TextureResources {
public var depthMapEye: [MTLTexture?] = [nil, nil]
public var hzbDepthPyramidEye: [MTLTexture?] = [nil, nil]
public var hzbMipViewsEye: [[MTLTexture]] = [[], []]

// TAA / MetalFX Temporal Scaler textures.
// velocityTexture: rg16Float screen-space motion vectors (camera-only, re-computed each frame).
// taaOutputTexture: resolved TAA output for mono / desktop.
// taaOutputTextureEye[]: per-eye resolved TAA output for XR stereo.
public var velocityTexture: MTLTexture?
public var taaOutputTexture: MTLTexture?
public var taaOutputTextureEye: [MTLTexture?] = [nil, nil]
// History textures: the previous frame's resolved TAA output, read by the resolve shader.
public var taaHistoryTexture: MTLTexture?
public var taaHistoryTextureEye: [MTLTexture?] = [nil, nil]
// Previous frame's world-position G-buffer, used to reject stale history on
// disocclusions and object motion that camera-only velocity cannot represent.
public var taaPositionHistoryTexture: MTLTexture?
public var taaPositionHistoryTextureEye: [MTLTexture?] = [nil, nil]
}

public struct AccelStructResources {
Expand Down
88 changes: 81 additions & 7 deletions Sources/UntoldEngine/Renderer/UntoldEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,40 @@ public class UntoldRenderer: NSObject, MTKViewDelegate {
initSizeableResources()
pendingResize = false
}

// TAA jitter (desktop / mono path).
// Apply a per-frame Halton sub-pixel offset to the projection matrix so the
// temporal scaler can accumulate multiple sample positions over time.
// Culling and shadow systems use unjitteredPerspectiveSpace to stay stable.
if TAAParams.shared.enabled, TemporalAA.shared.isSupported {
let jitter = TemporalAA.shared.currentJitter()
let vw = Float(renderInfo.viewPort.x)
let vh = Float(renderInfo.viewPort.y)
var jittered = renderInfo.unjitteredPerspectiveSpace
jittered[3][0] += jitter.x * 2.0 / vw
jittered[3][1] += jitter.y * 2.0 / vh
renderInfo.perspectiveSpace = jittered
renderInfo.taaJitterX = jitter.x
renderInfo.taaJitterY = jitter.y

// Track prev/current unjittered VP for the camera-only velocity pass.
if let cam = CameraSystem.shared.activeCamera,
let camComp = scene.get(component: CameraComponent.self, for: cam)
{
let effectiveView = SceneRootTransform.shared.effectiveViewMatrix(camComp.viewSpace)
let unjitteredVP = simd_mul(renderInfo.unjitteredPerspectiveSpace, effectiveView)
renderInfo.prevViewProjectionEye[0] = renderInfo.currentViewProjectionEye[0]
renderInfo.currentViewProjectionEye[0] = unjitteredVP

// Auto-reset TAA history after large camera jumps (teleport / scene cut).
TemporalAA.shared.checkAndResetIfNeeded(cameraPosition: camComp.localPosition)
}
} else {
renderInfo.perspectiveSpace = renderInfo.unjitteredPerspectiveSpace
renderInfo.taaJitterX = 0
renderInfo.taaJitterY = 0
}

// Tick the progressive loader here (main thread, before runFrame) so newly
// registered entities are picked up by BatchingSystem in the same frame.
// In XR, UntoldEngineXR.renderNewFrame() dispatches this to the main thread
Expand Down Expand Up @@ -529,6 +563,9 @@ public class UntoldRenderer: NSObject, MTKViewDelegate {
fovyRadians: degreesToRadians(degrees: fov), aspectRatio: aspect, nearZ: near, farZ: far
)

// Store the clean (unjittered) projection. The per-frame draw() applies Halton
// jitter on top of this into perspectiveSpace before each render.
renderInfo.unjitteredPerspectiveSpace = projectionMatrix
renderInfo.perspectiveSpace = projectionMatrix

let viewPortSize: simd_float2 = simd_make_float2(Float(size.width), Float(size.height))
Expand Down Expand Up @@ -576,6 +613,12 @@ public class UntoldRenderer: NSObject, MTKViewDelegate {

renderer.initResources()

// On Vision Pro the display resolution is high enough that spatial aliasing is
// barely visible, and TAA's temporal ghosting is more noticeable than aliasing
// on a precision head-tracked display. FXAA gives clean edges with zero lag.
TAAParams.shared.enabled = false
FXAAParams.shared.enabled = true

renderEnvironment = true

return renderer
Expand Down Expand Up @@ -632,21 +675,52 @@ public class UntoldRenderer: NSObject, MTKViewDelegate {
projectionMatrix: simd_float4x4,
eyeIndex: Int
) {
renderInfo.perspectiveSpace = projectionMatrix

guard let camera = CameraSystem.shared.activeCamera, let cameraComponent = scene.get(component: CameraComponent.self, for: camera) else {
handleError(.noActiveCamera)
return
}

cameraComponent.viewSpace = viewMatrix

// Save this eye's view-projection for next frame's per-eye HZB culling.
// --- Unjittered VP (used by culling, shadow cascades, and velocity) ---
let effectiveVM = SceneRootTransform.shared.effectiveViewMatrix(viewMatrix)
let unjitteredVP = simd_mul(projectionMatrix, effectiveVM)

// Save this eye's unjittered VP for next frame's per-eye HZB culling.
if renderInfo.isXRStereoMode {
let effectiveVM = SceneRootTransform.shared.effectiveViewMatrix(viewMatrix)
let eyeVP = simd_mul(projectionMatrix, effectiveVM)
if eyeIndex == 0 { renderInfo.xrEye0ViewProjection = eyeVP }
else { renderInfo.xrEye1ViewProjection = eyeVP }
if eyeIndex == 0 { renderInfo.xrEye0ViewProjection = unjitteredVP }
else { renderInfo.xrEye1ViewProjection = unjitteredVP }
}

// Track prev/current unjittered VP for the camera-only velocity pass.
let safeEye = min(eyeIndex, 1)
renderInfo.prevViewProjectionEye[safeEye] = renderInfo.currentViewProjectionEye[safeEye]
renderInfo.currentViewProjectionEye[safeEye] = unjitteredVP

// --- Jittered projection (rasterisation only) ---
// Both eyes share the same Halton sample for a given frame so the temporal
// accumulation in each per-eye scaler sees a consistent sample pattern.
if TAAParams.shared.enabled, TemporalAA.shared.isSupported {
let jitter = TemporalAA.shared.currentJitter()
let vw = Float(renderInfo.viewPort.x)
let vh = Float(renderInfo.viewPort.y)
var jittered = projectionMatrix
jittered[3][0] += jitter.x * 2.0 / vw
jittered[3][1] += jitter.y * 2.0 / vh
renderInfo.perspectiveSpace = jittered
renderInfo.unjitteredPerspectiveSpace = projectionMatrix
renderInfo.taaJitterX = jitter.x
renderInfo.taaJitterY = jitter.y

// Auto-reset TAA history after large head-position jumps (XR teleport).
if safeEye == 0 {
TemporalAA.shared.checkAndResetIfNeeded(cameraPosition: cameraComponent.localPosition)
}
} else {
renderInfo.perspectiveSpace = projectionMatrix
renderInfo.unjitteredPerspectiveSpace = projectionMatrix
renderInfo.taaJitterX = 0
renderInfo.taaJitterY = 0
}

configuration.updateXRRenderingSystemCallback!(.xr(commandBuffer: commandBuffer, passDescriptor: passDescriptor))
Expand Down
Loading
Loading