diff --git a/Sources/CShaderTypes/ShaderTypes.h b/Sources/CShaderTypes/ShaderTypes.h index cab98082..cccf9bc4 100644 --- a/Sources/CShaderTypes/ShaderTypes.h +++ b/Sources/CShaderTypes/ShaderTypes.h @@ -121,7 +121,8 @@ typedef enum{ typedef enum{ debugPassModeIndex, - debugPassFrustumPlanesIndex + debugPassFrustumPlanesIndex, + debugPassReverseZIndex }DebugPassBufferIndices; typedef enum{ diff --git a/Sources/DemoGame/AppDelegate.swift b/Sources/DemoGame/AppDelegate.swift index a1dcdd69..9ef5cfac 100644 --- a/Sources/DemoGame/AppDelegate.swift +++ b/Sources/DemoGame/AppDelegate.swift @@ -12,7 +12,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate { private enum Constants { static let appVersion = "0.12.10" - static let windowSize = NSSize(width: 1920, height: 1080) + static let defaultWindowSize = NSSize(width: 1920, height: 1080) + static let minimumWindowSize = NSSize(width: 640, height: 480) + } + + private struct LaunchOptions { + var windowSize = Constants.defaultWindowSize } private enum ExportError: LocalizedError { @@ -37,7 +42,11 @@ func applicationDidFinishLaunching(_: Notification) { print("Launching Untold Engine v\(Constants.appVersion)") - setupWindow() + guard let launchOptions = parseLaunchOptions(arguments: CommandLine.arguments) else { + NSApp.terminate(nil) + return + } + setupWindow(size: launchOptions.windowSize) setupRendererAndScene() wireDemoStateCallbacks() presentHUD() @@ -47,17 +56,90 @@ true } - private func setupWindow() { + private func setupWindow(size: NSSize) { window = NSWindow( - contentRect: NSRect(origin: .zero, size: Constants.windowSize), + contentRect: NSRect(origin: .zero, size: size), styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false ) window.title = "Untold Engine v\(Constants.appVersion)" + window.minSize = Constants.minimumWindowSize window.center() } + private func parseLaunchOptions(arguments: [String]) -> LaunchOptions? { + var options = LaunchOptions() + var index = 1 + + while index < arguments.count { + let argument = arguments[index] + + if argument == "--help" || argument == "-h" { + printUsage() + return nil + } + + if argument == "--resolution" { + guard index + 1 < arguments.count else { + print("Missing value for --resolution. Expected WIDTHxHEIGHT, for example 800x600.") + printUsage() + return nil + } + + if let size = parseResolution(arguments[index + 1]) { + options.windowSize = size + } else { + print("Invalid resolution '\(arguments[index + 1])'. Expected WIDTHxHEIGHT with minimum 640x480.") + printUsage() + return nil + } + index += 2 + continue + } + + if argument.hasPrefix("--resolution=") { + let value = String(argument.dropFirst("--resolution=".count)) + if let size = parseResolution(value) { + options.windowSize = size + } else { + print("Invalid resolution '\(value)'. Expected WIDTHxHEIGHT with minimum 640x480.") + printUsage() + return nil + } + } + + index += 1 + } + + return options + } + + private func parseResolution(_ value: String) -> NSSize? { + let normalized = value.lowercased() + let parts = normalized.split(separator: "x", omittingEmptySubsequences: false) + guard parts.count == 2, + let width = Double(parts[0]), + let height = Double(parts[1]), + width >= Constants.minimumWindowSize.width, + height >= Constants.minimumWindowSize.height + else { + return nil + } + + return NSSize(width: width, height: height) + } + + private func printUsage() { + print(""" + Usage: swift run untolddemo [--resolution WIDTHxHEIGHT] + + Options: + --resolution WIDTHxHEIGHT Start the demo at a specific window size, for example 800x600. + -h, --help Show this help. + """) + } + private func setupRendererAndScene() { guard let renderer = UntoldRenderer.create() else { print("Failed to initialize the renderer.") @@ -162,6 +244,9 @@ demoState.onTileBoundsChanged = { enabled in setTileBoundsDebug(enabled: enabled) } + demoState.onMouseOverControlPanelChanged = { [weak self] isOver in + self?.gameScene.suppressCameraInput = isOver + } } private func presentHUD() { diff --git a/Sources/DemoGame/DemoHUD.swift b/Sources/DemoGame/DemoHUD.swift index 8597b9b9..e5a92557 100644 --- a/Sources/DemoGame/DemoHUD.swift +++ b/Sources/DemoGame/DemoHUD.swift @@ -59,9 +59,31 @@ } struct DemoHUD: View { + private struct ResolutionPreset: Identifiable { + let label: String + let size: NSSize + + var id: String { + label + } + } + private enum Constants { static let statsRefreshInterval: TimeInterval = 0.1 static let usdzExtension = "usdz" + static let compactWidth: CGFloat = 980 + static let panelCornerRadius: CGFloat = 8 + static let panelPadding: CGFloat = 12 + static let edgePadding: CGFloat = 16 + static let compactMinimumPanelHeight: CGFloat = 220 + static let controlsPanelWidth: CGFloat = 320 + static let sidePanelWidth: CGFloat = 260 + static let resolutionPresets: [ResolutionPreset] = [ + .init(label: "800x600", size: NSSize(width: 800, height: 600)), + .init(label: "1280x720", size: NSSize(width: 1280, height: 720)), + .init(label: "1600x900", size: NSSize(width: 1600, height: 900)), + .init(label: "1920x1080", size: NSSize(width: 1920, height: 1080)), + ] } private enum LocalImportMode { @@ -82,240 +104,335 @@ @Bindable var state: DemoState @State private var showFilePicker = false @State private var localImportMode: LocalImportMode = .asset + @State private var areControlsVisible = false + @State private var areStatsVisibleInCompact = false var body: some View { - ZStack(alignment: .topLeading) { - SceneView(renderer: renderer) + GeometryReader { proxy in + let isCompact = proxy.size.width < Constants.compactWidth + let controlsWidth = max( + 260, + min(Constants.controlsPanelWidth, proxy.size.width - Constants.edgePadding * 2) + ) + + ZStack(alignment: .topLeading) { + SceneView(renderer: renderer) + + if isCompact { + if areControlsVisible { + ScrollView { + controlsPanel + .padding(Constants.panelPadding) + } + .frame(width: controlsWidth, alignment: .topLeading) + .frame( + maxHeight: max(Constants.compactMinimumPanelHeight, proxy.size.height - Constants.edgePadding * 2), + alignment: .topLeading + ) + .background(.regularMaterial, in: RoundedRectangle(cornerRadius: Constants.panelCornerRadius)) + .padding(Constants.edgePadding) + .onHover { state.isMouseOverControlPanel = $0 } + } + } else { + controlsPanel + .padding(Constants.panelPadding) + .frame(width: Constants.controlsPanelWidth, alignment: .topLeading) + .background(.regularMaterial, in: RoundedRectangle(cornerRadius: Constants.panelCornerRadius)) + .padding(Constants.edgePadding) + .onHover { state.isMouseOverControlPanel = $0 } + } + + if isCompact { + compactTopBar + .padding(Constants.edgePadding) + .frame(maxWidth: .infinity, alignment: .topTrailing) + } else { + sidePanel + .padding(Constants.edgePadding) + .frame(maxWidth: .infinity, alignment: .topTrailing) + } + } + } + .onReceive(Timer.publish(every: Constants.statsRefreshInterval, on: .main, in: .common).autoconnect()) { _ in + if state.showStats { + state.stats = getEngineStatsSnapshot() + } + } + .fileImporter( + isPresented: $showFilePicker, + allowedContentTypes: localImportMode.allowedContentTypes + ) { result in + handleLocalImport(result) + } + .sheet(isPresented: $state.showExportPanel) { + DemoExportSheet(state: state) + } + } - VStack(alignment: .leading, spacing: 8) { + private var controlsPanel: some View { + VStack(alignment: .leading, spacing: 8) { + HStack(alignment: .center, spacing: 8) { sectionLabel("SCENES") + Spacer() + resolutionMenu + } - HStack(alignment: .center, spacing: 8) { - Picker("Remote Scene", selection: $state.selectedRemoteSceneID) { - ForEach(state.remoteScenes) { scene in - Text(scene.title).tag(scene.id) - } - } - .pickerStyle(.menu) - .frame(maxWidth: .infinity, alignment: .leading) - .disabled(state.isLoading || state.remoteScenes.isEmpty) - Button("Load", action: loadSelectedRemoteScene) - .buttonStyle(.borderedProminent) - .tint(.blue) - .disabled(state.isLoading || state.selectedRemoteScene?.manifestURL == nil) - if state.isLoading { - ProgressView() - .scaleEffect(0.6) - .frame(width: 16, height: 16) + HStack(alignment: .center, spacing: 8) { + Picker("Remote Scene", selection: $state.selectedRemoteSceneID) { + ForEach(state.remoteScenes) { scene in + Text(scene.title).tag(scene.id) } } + .pickerStyle(.menu) + .frame(maxWidth: .infinity, alignment: .leading) + .disabled(state.isLoading || state.remoteScenes.isEmpty) + Button("Load", action: loadSelectedRemoteScene) + .buttonStyle(.borderedProminent) + .tint(.blue) + .disabled(state.isLoading || state.selectedRemoteScene?.manifestURL == nil) + if state.isLoading { + ProgressView() + .scaleEffect(0.6) + .frame(width: 16, height: 16) + } + } - HStack(alignment: .center, spacing: 8) { - Text("Local Scene") - .foregroundStyle(.secondary) - .frame(width: 92, alignment: .leading) - Menu("Browse") { - Button("Asset (.untold)", action: openLocalAssetPicker) - Button("Tiled Scene (.json)", action: openLocalTiledScenePicker) - } - .disabled(state.isLoading) - Spacer(minLength: 0) + HStack(alignment: .center, spacing: 8) { + Text("Local Scene") + .foregroundStyle(.secondary) + .frame(width: 92, alignment: .leading) + Menu("Browse") { + Button("Asset (.untold)", action: openLocalAssetPicker) + Button("Tiled Scene (.json)", action: openLocalTiledScenePicker) } + .disabled(state.isLoading) + Spacer(minLength: 0) + } - Divider() + Divider() - sectionLabel("CONTROLS") - controlHint("WASD / QE", "Translate") - controlHint("Right-drag (+ Shift)", "Orbit / Yaw") - controlHint("Two-finger swipe (+ Shift)", "Orbit / Yaw") + sectionLabel("CONTROLS") + controlHint("WASD / QE", "Translate") + controlHint("Right-drag (+ Shift)", "Orbit / Yaw") + controlHint("Two-finger swipe (+ Shift)", "Orbit / Yaw") - Divider() + Divider() - sectionLabel("FEATURES") + sectionLabel("FEATURES") - Toggle("Static Batching", isOn: $state.batchingEnabled) - .toggleStyle(.checkbox) - .disabled(!state.hasLoadedEntity || state.isLoading) + Toggle("Static Batching", isOn: $state.batchingEnabled) + .toggleStyle(.checkbox) + .disabled(!state.hasLoadedEntity || state.isLoading) - Toggle("Geometry Streaming", isOn: $state.streamingEnabled) - .toggleStyle(.checkbox) - .disabled(!state.hasLoadedEntity || state.isLoading) - - HStack { - Text("Stream Radius").foregroundStyle(.secondary) - Spacer() - TextField("", value: $state.streamingRadius, format: .number) - .textFieldStyle(.roundedBorder) - .frame(width: 70) - } - .padding(.leading, 12) - .opacity(state.streamingEnabled ? 1.0 : 0.35) - .disabled(!state.streamingEnabled) - - HStack { - Text("Unload Radius").foregroundStyle(.secondary) - Spacer() - TextField("", value: $state.unloadRadius, format: .number) - .textFieldStyle(.roundedBorder) - .frame(width: 70) - } - .padding(.leading, 12) - .opacity(state.streamingEnabled ? 1.0 : 0.35) - .disabled(!state.streamingEnabled) + Toggle("Geometry Streaming", isOn: $state.streamingEnabled) + .toggleStyle(.checkbox) + .disabled(!state.hasLoadedEntity || state.isLoading) + + HStack { + Text("Stream Radius").foregroundStyle(.secondary) + Spacer() + TextField("", value: $state.streamingRadius, format: .number) + .textFieldStyle(.roundedBorder) + .frame(width: 70) + } + .padding(.leading, 12) + .opacity(state.streamingEnabled ? 1.0 : 0.35) + .disabled(!state.streamingEnabled) - Divider() + HStack { + Text("Unload Radius").foregroundStyle(.secondary) + Spacer() + TextField("", value: $state.unloadRadius, format: .number) + .textFieldStyle(.roundedBorder) + .frame(width: 70) + } + .padding(.leading, 12) + .opacity(state.streamingEnabled ? 1.0 : 0.35) + .disabled(!state.streamingEnabled) - sectionLabel("POST FX") + Divider() - HStack(alignment: .center, spacing: 8) { - Picker("Preset", selection: $state.selectedPostFXPreset) { - ForEach(DemoState.PostFXPreset.allCases) { preset in - Text(preset.rawValue).tag(preset) - } - } - .pickerStyle(.menu) + sectionLabel("POST FX") - Button("Apply") { - state.applySelectedPostFXPreset() + HStack(alignment: .center, spacing: 8) { + Picker("Preset", selection: $state.selectedPostFXPreset) { + ForEach(DemoState.PostFXPreset.allCases) { preset in + Text(preset.rawValue).tag(preset) } - .buttonStyle(.bordered) } + .pickerStyle(.menu) - Toggle("Color Grading", isOn: $state.colorGradingEnabled) - .toggleStyle(.checkbox) - - sliderRow( - title: "Exposure", - value: $state.exposure, - range: -4.0 ... 4.0, - enabled: state.colorGradingEnabled - ) + Button("Apply") { + state.applySelectedPostFXPreset() + } + .buttonStyle(.bordered) + } - sliderRow( - title: "Brightness", - value: $state.brightness, - range: -1.0 ... 1.0, - enabled: state.colorGradingEnabled - ) + Toggle("Color Grading", isOn: $state.colorGradingEnabled) + .toggleStyle(.checkbox) + + sliderRow( + title: "Exposure", + value: $state.exposure, + range: -4.0 ... 4.0, + enabled: state.colorGradingEnabled + ) + + sliderRow( + title: "Brightness", + value: $state.brightness, + range: -1.0 ... 1.0, + enabled: state.colorGradingEnabled + ) + + sliderRow( + title: "Contrast", + value: $state.contrast, + range: 0.0 ... 2.0, + enabled: state.colorGradingEnabled + ) + + sliderRow( + title: "Saturation", + value: $state.saturation, + range: 0.0 ... 2.0, + enabled: state.colorGradingEnabled + ) + + Toggle("SSAO", isOn: $state.ssaoEnabled) + .toggleStyle(.checkbox) + + sliderRow( + title: "SSAO Radius", + value: $state.ssaoRadius, + range: 0.1 ... 2.0, + enabled: state.ssaoEnabled + ) + + sliderRow( + title: "SSAO Bias", + value: $state.ssaoBias, + range: 0.0 ... 0.1, + enabled: state.ssaoEnabled + ) + + sliderRow( + title: "SSAO Intensity", + value: $state.ssaoIntensity, + range: 0.0 ... 2.0, + enabled: state.ssaoEnabled + ) - sliderRow( - title: "Contrast", - value: $state.contrast, - range: 0.0 ... 2.0, - enabled: state.colorGradingEnabled - ) + Divider() - sliderRow( - title: "Saturation", - value: $state.saturation, - range: 0.0 ... 2.0, - enabled: state.colorGradingEnabled - ) + sectionLabel("DEBUG") - Toggle("SSAO", isOn: $state.ssaoEnabled) - .toggleStyle(.checkbox) + Toggle("LOD Debug", isOn: $state.lodDebugEnabled) + .toggleStyle(.checkbox) - sliderRow( - title: "SSAO Radius", - value: $state.ssaoRadius, - range: 0.1 ... 2.0, - enabled: state.ssaoEnabled - ) + Toggle("Texture Streaming Debug", isOn: $state.textureStreamingTierDebugEnabled) + .toggleStyle(.checkbox) - sliderRow( - title: "SSAO Bias", - value: $state.ssaoBias, - range: 0.0 ... 0.1, - enabled: state.ssaoEnabled - ) + Picker("G-Buffer View", selection: $state.renderDebugView) { + Text("Lit").tag(RenderDebugViewMode.lit) + Text("Albedo").tag(RenderDebugViewMode.albedo) + Text("Normal").tag(RenderDebugViewMode.normal) + Text("Depth").tag(RenderDebugViewMode.depth) + Text("SSAO (Blurred)").tag(RenderDebugViewMode.ssaoBlurred) + } + .pickerStyle(.menu) - sliderRow( - title: "SSAO Intensity", - value: $state.ssaoIntensity, - range: 0.0 ... 2.0, - enabled: state.ssaoEnabled - ) + Toggle("Spatial Debug", isOn: $state.spatialDebugEnabled) + .toggleStyle(.checkbox) + if state.spatialDebugEnabled { + Toggle("Occupied Only", isOn: $state.spatialOccupiedOnly) + .toggleStyle(.checkbox) + .padding(.leading, 12) + Picker("Mode", selection: $state.spatialColorMode) { + Text("Plain").tag(SpatialDebugLeafColorMode.plain) + Text("Residency").tag(SpatialDebugLeafColorMode.residency) + Text("Culling").tag(SpatialDebugLeafColorMode.culling) + } + .pickerStyle(.segmented) + .frame(minWidth: 180) + Toggle("Tile Bounds", isOn: $state.tileBoundsEnabled) + .toggleStyle(.checkbox) + .padding(.leading, 12) + } - Divider() + Divider() - sectionLabel("DEBUG") + Toggle("Engine Stats", isOn: $state.showStats) + .toggleStyle(.checkbox) + } + } - Toggle("LOD Debug", isOn: $state.lodDebugEnabled) - .toggleStyle(.checkbox) + private var sidePanel: some View { + HStack { + Spacer() + VStack(alignment: .trailing, spacing: 12) { + if state.showStats { + StatsPanel(stats: state.stats) + .frame(width: Constants.sidePanelWidth) + } - Toggle("Texture Streaming Debug", isOn: $state.textureStreamingTierDebugEnabled) - .toggleStyle(.checkbox) + DemoToolsPanel( + isBusy: state.isLoading || state.isExporting, + isExporting: state.isExporting, + openExportSheet: { state.showExportPanel = true } + ) + .frame(width: Constants.sidePanelWidth) + } + } + } - Picker("G-Buffer View", selection: $state.renderDebugView) { - Text("Lit").tag(RenderDebugViewMode.lit) - Text("Albedo").tag(RenderDebugViewMode.albedo) - Text("Normal").tag(RenderDebugViewMode.normal) - Text("Depth").tag(RenderDebugViewMode.depth) - Text("SSAO (Blurred)").tag(RenderDebugViewMode.ssaoBlurred) + private var compactTopBar: some View { + VStack(alignment: .trailing, spacing: 8) { + HStack(spacing: 8) { + Button(areControlsVisible ? "Hide Controls" : "Controls") { + areControlsVisible.toggle() } - .pickerStyle(.menu) + .buttonStyle(.borderedProminent) - Toggle("Spatial Debug", isOn: $state.spatialDebugEnabled) - .toggleStyle(.checkbox) - if state.spatialDebugEnabled { - Toggle("Occupied Only", isOn: $state.spatialOccupiedOnly) - .toggleStyle(.checkbox) - .padding(.leading, 12) - Picker("Mode", selection: $state.spatialColorMode) { - Text("Plain").tag(SpatialDebugLeafColorMode.plain) - Text("Residency").tag(SpatialDebugLeafColorMode.residency) - Text("Culling").tag(SpatialDebugLeafColorMode.culling) - } - .pickerStyle(.segmented) - .frame(minWidth: 180) - Toggle("Tile Bounds", isOn: $state.tileBoundsEnabled) - .toggleStyle(.checkbox) - .padding(.leading, 12) - } + resolutionMenu - Divider() + Button(areStatsVisibleInCompact ? "Hide Stats" : "Stats") { + areStatsVisibleInCompact.toggle() + } + .buttonStyle(.bordered) + .disabled(!state.showStats) - Toggle("Engine Stats", isOn: $state.showStats) - .toggleStyle(.checkbox) + Button("Export") { + state.showExportPanel = true + } + .buttonStyle(.bordered) + .disabled(state.isLoading || state.isExporting) } - .padding(12) - .fixedSize(horizontal: true, vertical: false) - .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 8)) - .padding() - - HStack { - Spacer() - VStack(alignment: .trailing, spacing: 12) { - if state.showStats { - StatsPanel(stats: state.stats) - .frame(width: 260) - } + .padding(8) + .background(.regularMaterial, in: RoundedRectangle(cornerRadius: Constants.panelCornerRadius)) - DemoToolsPanel( - isBusy: state.isLoading || state.isExporting, - isExporting: state.isExporting, - openExportSheet: { state.showExportPanel = true } - ) - .frame(width: 260) - } - .padding() + if areStatsVisibleInCompact, state.showStats { + StatsPanel(stats: state.stats) + .frame(width: Constants.sidePanelWidth) } - .frame(maxWidth: .infinity, alignment: .topTrailing) } - .onReceive(Timer.publish(every: Constants.statsRefreshInterval, on: .main, in: .common).autoconnect()) { _ in - if state.showStats { - state.stats = getEngineStatsSnapshot() + } + + private var resolutionMenu: some View { + Menu("Resolution") { + ForEach(Constants.resolutionPresets) { preset in + Button(preset.label) { + resizeWindow(to: preset.size) + } } } - .fileImporter( - isPresented: $showFilePicker, - allowedContentTypes: localImportMode.allowedContentTypes - ) { result in - handleLocalImport(result) - } - .sheet(isPresented: $state.showExportPanel) { - DemoExportSheet(state: state) - } + .menuStyle(.button) + } + + private func resizeWindow(to size: NSSize) { + guard let window = NSApp.keyWindow ?? NSApp.windows.first else { return } + window.setContentSize(size) + window.center() } private func sectionLabel(_ text: String) -> some View { diff --git a/Sources/DemoGame/DemoState.swift b/Sources/DemoGame/DemoState.swift index 94c343a6..127a27de 100644 --- a/Sources/DemoGame/DemoState.swift +++ b/Sources/DemoGame/DemoState.swift @@ -223,6 +223,10 @@ var showStats: Bool = true var stats: EngineStatsSnapshot = .init() + var isMouseOverControlPanel: Bool = false { + didSet { onMouseOverControlPanelChanged?(isMouseOverControlPanel) } + } + // MARK: - Callbacks (wired by AppDelegate) var onLoadFile: ((String, @escaping @Sendable (Bool) -> Void) -> Void)? @@ -238,6 +242,7 @@ var onRenderDebugViewChanged: ((RenderDebugViewMode) -> Void)? var onSpatialDebugChanged: ((Bool, Bool, SpatialDebugLeafColorMode) -> Void)? var onTileBoundsChanged: ((Bool) -> Void)? + var onMouseOverControlPanelChanged: ((Bool) -> Void)? func applySelectedPostFXPreset() { let preset = selectedPostFXPreset.enginePreset diff --git a/Sources/DemoGame/GameScene.swift b/Sources/DemoGame/GameScene.swift index cb01cd87..e32771cf 100644 --- a/Sources/DemoGame/GameScene.swift +++ b/Sources/DemoGame/GameScene.swift @@ -50,6 +50,7 @@ private var cameraBehavior: CameraBehavior = .flyOrbit private var wasRightMousePressed: Bool = false private var wasScrolling: Bool = false + var suppressCameraInput: Bool = false init() { InputSystem.shared.registerKeyboardEvents() @@ -315,7 +316,7 @@ ) // } - if input.keyState.rightMousePressed { + if input.keyState.rightMousePressed, !suppressCameraInput { if !wasRightMousePressed { resetOrbitTarget(entityId: camera) } @@ -340,7 +341,7 @@ // right-click drag. resetOrbitTarget fires only on the first scroll // frame, matching the right-mouse-press behaviour above. let scroll = input.scrollDelta - let isScrolling = (scroll.x != 0 || scroll.y != 0) && !input.keyState.rightMousePressed + let isScrolling = (scroll.x != 0 || scroll.y != 0) && !input.keyState.rightMousePressed && !suppressCameraInput if isScrolling { if !wasScrolling { resetOrbitTarget(entityId: camera) diff --git a/Sources/UntoldEngine/Renderer/Pipelines/RenderPipeLines.swift b/Sources/UntoldEngine/Renderer/Pipelines/RenderPipeLines.swift index 62aadd44..70850d3b 100644 --- a/Sources/UntoldEngine/Renderer/Pipelines/RenderPipeLines.swift +++ b/Sources/UntoldEngine/Renderer/Pipelines/RenderPipeLines.swift @@ -154,7 +154,7 @@ public func InitGridPipeline() -> RenderPipeline? { vertexDescriptor: createGridVertexDescriptor(), colorFormats: [wf.environment], depthFormat: renderInfo.depthPixelFormat, - depthCompareFunction: MTLCompareFunction.less, + depthCompareFunction: MTLCompareFunction.lessEqual, depthEnabled: false, blendMode: .alphaStraight, name: "Grid Pipeline" diff --git a/Sources/UntoldEngine/Renderer/RenderPasses.swift b/Sources/UntoldEngine/Renderer/RenderPasses.swift index e7ff1cc4..d8a9f553 100644 --- a/Sources/UntoldEngine/Renderer/RenderPasses.swift +++ b/Sources/UntoldEngine/Renderer/RenderPasses.swift @@ -489,23 +489,21 @@ public enum RenderPasses { // update uniforms var gridUniforms = Uniforms() - let modelMatrix = simd_float4x4.init(1.0) - guard let camera = CameraSystem.shared.activeCamera, let cameraComponent = scene.get(component: CameraComponent.self, for: camera) else { handleError(.noActiveCamera) return } - var viewMatrix: simd_float4x4 = cameraComponent.viewSpace - - viewMatrix = viewMatrix.inverse - let modelViewMatrix = simd_mul(viewMatrix, modelMatrix) - gridUniforms.modelViewMatrix = modelViewMatrix - gridUniforms.viewMatrix = viewMatrix - - // Note, the perspective projection space has to be inverted to create the infinite grid + // viewMatrix (inverted) and projectionMatrix (inverted) are used by the vertex shader + // to unproject NDC corners into world-space rays for the infinite grid. + let invView = cameraComponent.viewSpace.inverse + gridUniforms.viewMatrix = invView gridUniforms.projectionMatrix = renderInfo.perspectiveSpace.inverse + // modelViewMatrix repurposed: stores the forward P*V matrix so the fragment shader + // can compute correct clip-space depth regardless of Z convention. + gridUniforms.modelViewMatrix = simd_mul(renderInfo.perspectiveSpace, cameraComponent.viewSpace) + if let gridUniformBuffer = bufferResources.gridUniforms { gridUniformBuffer.contents().copyMemory( from: &gridUniforms, byteCount: MemoryLayout.stride diff --git a/Sources/UntoldEngine/Renderer/UntoldEngine.swift b/Sources/UntoldEngine/Renderer/UntoldEngine.swift index d31a8f89..b8de3cb0 100644 --- a/Sources/UntoldEngine/Renderer/UntoldEngine.swift +++ b/Sources/UntoldEngine/Renderer/UntoldEngine.swift @@ -66,7 +66,7 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { } renderInfo.device = device renderInfo.commandQueue = commandQueue - renderInfo.reverseZEnabled = false + renderInfo.reverseZEnabled = true renderInfo.colorPixelFormat = .rgba16Float renderInfo.depthPixelFormat = renderer.metalView.depthStencilPixelFormat renderInfo.viewPort = simd_float2( @@ -107,7 +107,7 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { } renderInfo.commandQueue = commandQueue - renderInfo.reverseZEnabled = false + renderInfo.reverseZEnabled = true renderInfo.colorPixelFormat = .rgba16Float renderInfo.depthPixelFormat = view.depthStencilPixelFormat renderInfo.viewPort = simd_float2( @@ -525,9 +525,9 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { } let aspect = Float(size.width) / Float(size.height) - let projectionMatrix = matrixPerspectiveRightHand( - fovyRadians: degreesToRadians(degrees: fov), aspectRatio: aspect, nearZ: near, farZ: far - ) + let projectionMatrix = renderInfo.reverseZEnabled + ? matrixPerspectiveRightHandReverseZ(fovyRadians: degreesToRadians(degrees: fov), aspectRatio: aspect, nearZ: near, farZ: far) + : matrixPerspectiveRightHand(fovyRadians: degreesToRadians(degrees: fov), aspectRatio: aspect, nearZ: near, farZ: far) renderInfo.perspectiveSpace = projectionMatrix @@ -668,7 +668,7 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { } renderInfo.commandQueue = commandQueue - renderInfo.reverseZEnabled = false + renderInfo.reverseZEnabled = true renderInfo.colorPixelFormat = view.colorPixelFormat renderInfo.depthPixelFormat = view.depthStencilPixelFormat renderInfo.viewPort = simd_float2(Float(view.bounds.size.width), Float(view.bounds.size.height)) diff --git a/Sources/UntoldEngine/Shaders/GridShader.metal b/Sources/UntoldEngine/Shaders/GridShader.metal index c91d7126..c3a2ffd3 100644 --- a/Sources/UntoldEngine/Shaders/GridShader.metal +++ b/Sources/UntoldEngine/Shaders/GridShader.metal @@ -49,11 +49,10 @@ float3 unprojectPoint(float x, float y, float z, float4x4 uView, float4x4 uProje return unprojPoint.xyz/unprojPoint.w; } -float computeDepth(float3 pos, float4x4 uView, float4x4 uProjection){ - - float4 clipSpacePos=uProjection*uView*float4(pos.xyz,1.0); - return (clipSpacePos.z/clipSpacePos.w); - +// vpMatrix must be the forward P*V transform (stored in uniformSpace.modelViewMatrix). +float computeDepth(float3 pos, float4x4 vpMatrix){ + float4 clipSpacePos = vpMatrix * float4(pos.xyz, 1.0); + return clipSpacePos.z / clipSpacePos.w; } float4 computeGrid(float3 uFragPos,float uScale){ @@ -81,16 +80,11 @@ float4 computeGrid(float3 uFragPos,float uScale){ return color; } -float computeLinearDepth(float3 pos,float near,float far, float4x4 uView, float4x4 uProjection){ - - float4 clipSpacePos=uProjection*uView*float4(pos.xyz,1.0); - - float clipSpaceDepth=(clipSpacePos.z/clipSpacePos.w)*2.0-1.0; //put back between -1 and 1 - - float linearDepth=(2.0*near*far)/(far+near-clipSpaceDepth*(far-near)); //get linear value between 0.01 and 100 - - return linearDepth/far; - +// Computes normalized [0,1] distance from the camera to pos, independent of Z convention. +// uInvView must be V^{-1} (stored in uniformSpace.viewMatrix); its translation column is the camera world position. +float computeLinearDepth(float3 pos, float far, float4x4 uInvView){ + float3 cameraPos = (uInvView * float4(0.0, 0.0, 0.0, 1.0)).xyz; + return length(pos - cameraPos) / far; } @@ -117,16 +111,16 @@ fragment FragmentOut fragmentGridShader(VertexOutput vertexOut [[stage_in]], con FragmentOut finalColor; - float near=0.01; float far=100.0; float t=-vertexOut.nearPoint.y/(vertexOut.farPoint.y-vertexOut.nearPoint.y); float3 fragPosition=vertexOut.nearPoint+t*(vertexOut.farPoint-vertexOut.nearPoint); - finalColor.depth=clamp(computeDepth(fragPosition, uniformSpace.viewMatrix, uniformSpace.projectionMatrix),0.0,0.9); + // modelViewMatrix holds the forward P*V matrix for correct depth in any Z convention. + finalColor.depth=clamp(computeDepth(fragPosition, uniformSpace.modelViewMatrix),0.0,1.0); - float linearDepth=computeLinearDepth(fragPosition, near, far, uniformSpace.viewMatrix, uniformSpace.projectionMatrix); + float linearDepth=computeLinearDepth(fragPosition, far, uniformSpace.viewMatrix); float fading=max(0.0, (0.5-linearDepth)); diff --git a/Sources/UntoldEngine/Shaders/ShadersUtils.metal b/Sources/UntoldEngine/Shaders/ShadersUtils.metal index 23c3199f..b850d2bc 100644 --- a/Sources/UntoldEngine/Shaders/ShadersUtils.metal +++ b/Sources/UntoldEngine/Shaders/ShadersUtils.metal @@ -532,17 +532,19 @@ float computeLuma(float3 color) { return 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b; } -float linearizeDepthForViewing(float depth, float near, float far){ - +float linearizeDepthForViewing(float depth, float near, float far, bool reverseZ){ + // Custom values for the scene float sceneNear = 0.1; float sceneFar = 50.0; - - float linear = near * far / (far + depth * (near - far)); // standard Z - // Normalize to 0–1 for visualization + float linear = reverseZ + ? near * far / (near + depth * (far - near)) + : near * far / (far + depth * (near - far)); + + // Normalize to 0–1 for visualization return saturate((linear - sceneNear) / (sceneFar - sceneNear)); - + } float linearizeDepth(float depth, float near, float far){ diff --git a/Sources/UntoldEngine/Shaders/debugShader.metal b/Sources/UntoldEngine/Shaders/debugShader.metal index 8d7e3a11..305ae2f5 100644 --- a/Sources/UntoldEngine/Shaders/debugShader.metal +++ b/Sources/UntoldEngine/Shaders/debugShader.metal @@ -26,7 +26,8 @@ fragment float4 fragmentDebugShader(VertexDebugOutput vertexOut [[stage_in]], texture2d finalTexture[[texture(0)]], depth2d depthTexture [[texture(1)]], constant int &debugMode [[buffer(debugPassModeIndex)]], - constant simd_float2 &frustumPlanes [[buffer(debugPassFrustumPlanesIndex)]]) { + constant simd_float2 &frustumPlanes [[buffer(debugPassFrustumPlanesIndex)]], + constant bool &reverseZ [[buffer(debugPassReverseZIndex)]]) { constexpr sampler s(min_filter::linear,mag_filter::linear); @@ -39,7 +40,7 @@ fragment float4 fragmentDebugShader(VertexDebugOutput vertexOut [[stage_in]], float near = frustumPlanes.x; float far = frustumPlanes.y; float rawDepth = depthTexture.sample(s, vertexOut.uvCoords); - float normalized = linearizeDepthForViewing(rawDepth, near, far); + float normalized = linearizeDepthForViewing(rawDepth, near, far, reverseZ); return float4(normalized, normalized, normalized, 1.0); } diff --git a/Sources/UntoldEngine/Systems/RenderingSystem.swift b/Sources/UntoldEngine/Systems/RenderingSystem.swift index 0cec39f0..bae9c850 100644 --- a/Sources/UntoldEngine/Systems/RenderingSystem.swift +++ b/Sources/UntoldEngine/Systems/RenderingSystem.swift @@ -932,6 +932,13 @@ public let lookRenderPass: RenderPasses.RenderPassExecution = { commandBuffer in index: Int(debugPassFrustumPlanesIndex.rawValue) ) + var reverseZ = renderInfo.reverseZEnabled + encoder.setFragmentBytes( + &reverseZ, + length: MemoryLayout.stride, + index: Int(debugPassReverseZIndex.rawValue) + ) + encoder.setFragmentTexture(debugDepth, index: 1) } )(commandBuffer) diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.air index 0b1bdcee..ec5b91c1 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.metallib index 09022fd9..c002a453 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.air index d7aa3513..2afa82cb 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.metallib index a0f55022..5b78201e 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.air index d5fe7ca0..12d15d01 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.metallib index 4d3a8b62..bfb72575 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.air index 43df5222..a156a634 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.metallib index c703845a..b806f984 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.air index 4a2d7c21..ea64bc8a 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.metallib index 6084df93..9568f0fd 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metallib index aad6b539..085da0b3 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metallib differ diff --git a/Sources/UntoldEngine/Utils/Globals.swift b/Sources/UntoldEngine/Utils/Globals.swift index bdb08f83..56f1e0ea 100644 --- a/Sources/UntoldEngine/Utils/Globals.swift +++ b/Sources/UntoldEngine/Utils/Globals.swift @@ -248,7 +248,7 @@ var timePassedSinceLastFrame: Float { // Frustum info public let far: Float = 500 -public let near: Float = 0.01 +public let near: Float = 0.1 public let fov: Float = 65.0 // Shadow max parameters (legacy single-cascade — kept for reference) diff --git a/Sources/UntoldEngine/Utils/MathUtils.swift b/Sources/UntoldEngine/Utils/MathUtils.swift index c997bf62..b896113f 100644 --- a/Sources/UntoldEngine/Utils/MathUtils.swift +++ b/Sources/UntoldEngine/Utils/MathUtils.swift @@ -274,6 +274,23 @@ public func matrixPerspectiveRightHand( ) } +/// Reverse-Z variant: maps near→1, far→0, giving float32 precision to distant geometry. +public func matrixPerspectiveRightHandReverseZ( + fovyRadians fovy: Float, aspectRatio: Float, nearZ: Float, farZ: Float +) -> matrix_float4x4 { + let ys = 1 / tanf(fovy * 0.5) + let xs = ys / aspectRatio + let zs = nearZ / (farZ - nearZ) + return matrix_float4x4.init( + columns: ( + vector_float4(xs, 0, 0, 0), + vector_float4(0, ys, 0, 0), + vector_float4(0, 0, zs, -1), + vector_float4(0, 0, zs * farZ, 0) + ) + ) +} + public func matrix_look_at_right_hand(_ eye: simd_float3, _ target: simd_float3, _ up: simd_float3) -> simd_float4x4 { diff --git a/Tests/UntoldEngineRenderTests/BaseRenderSetup.swift b/Tests/UntoldEngineRenderTests/BaseRenderSetup.swift index 9ce0b23b..1e493be7 100644 --- a/Tests/UntoldEngineRenderTests/BaseRenderSetup.swift +++ b/Tests/UntoldEngineRenderTests/BaseRenderSetup.swift @@ -114,10 +114,9 @@ class BaseRenderSetup: XCTestCase { renderer.mtkView(renderer.metalView, drawableSizeWillChange: size) renderer.pendingResize = true let aspect = Float(windowWidth) / Float(windowHeight) - renderInfo.perspectiveSpace = matrixPerspectiveRightHand( - fovyRadians: degreesToRadians(degrees: fov), - aspectRatio: aspect, nearZ: near, farZ: far - ) + renderInfo.perspectiveSpace = renderInfo.reverseZEnabled + ? matrixPerspectiveRightHandReverseZ(fovyRadians: degreesToRadians(degrees: fov), aspectRatio: aspect, nearZ: near, farZ: far) + : matrixPerspectiveRightHand(fovyRadians: degreesToRadians(degrees: fov), aspectRatio: aspect, nearZ: near, farZ: far) renderInfo.viewPort = simd_float2(Float(windowWidth), Float(windowHeight)) diff --git a/Tests/UntoldEngineRenderTests/CullingTest.swift b/Tests/UntoldEngineRenderTests/CullingTest.swift index e049e7de..7bb85ee8 100644 --- a/Tests/UntoldEngineRenderTests/CullingTest.swift +++ b/Tests/UntoldEngineRenderTests/CullingTest.swift @@ -122,14 +122,11 @@ final class CullingTest: BaseRenderSetup { let windowWidth = 1280 let windowHeight = 720 - // Initialize projection + // Initialize projection matching the renderer's active Z convention. let aspect = Float(windowWidth) / Float(windowHeight) - let projectionMatrix = matrixPerspectiveRightHand( - fovyRadians: degreesToRadians(degrees: fov), - aspectRatio: aspect, - nearZ: near, - farZ: far - ) + let projectionMatrix = renderInfo.reverseZEnabled + ? matrixPerspectiveRightHandReverseZ(fovyRadians: degreesToRadians(degrees: fov), aspectRatio: aspect, nearZ: near, farZ: far) + : matrixPerspectiveRightHand(fovyRadians: degreesToRadians(degrees: fov), aspectRatio: aspect, nearZ: near, farZ: far) renderInfo.perspectiveSpace = projectionMatrix @@ -140,10 +137,13 @@ final class CullingTest: BaseRenderSetup { let viewProjection: simd_float4x4 = simd_mul(renderInfo.perspectiveSpace, cameraComponent.viewSpace) - let F = buildFrustum(from: viewProjection, ndcNear: 0, ndcFar: 1) + let ndcNear: Float = renderInfo.reverseZEnabled ? 1.0 : 0.0 + let ndcFar: Float = renderInfo.reverseZEnabled ? 0.0 : 1.0 + + let F = buildFrustum(from: viewProjection, ndcNear: ndcNear, ndcFar: ndcFar) // Frustum center should evaluate >= 0 for all planes - let corners = unprojectCorners(viewProj: viewProjection) + let corners = unprojectCorners(viewProj: viewProjection, ndcNear: ndcNear, ndcFar: ndcFar) let center = corners.reduce(SIMD3(repeating: 0), +) / 8 for p in F.planes { XCTAssertGreaterThanOrEqual(pointPlaneDistance(p, center), -1e-5, "Frustum center should be inside (plane inward)") @@ -342,7 +342,10 @@ final class CullingTest: BaseRenderSetup { renderInfo.viewPort = originalViewport } - textureResources.hzbDepthPyramid = makeHZBTestTexture(depthValue: 1.0) + // "Clear" HZB — nothing occluding, camera sees to the far plane. + // Standard-Z: far = 1.0. Reverse-Z: far = 0.0. + let clearDepth: Float = renderInfo.reverseZEnabled ? 0.0 : 1.0 + textureResources.hzbDepthPyramid = makeHZBTestTexture(depthValue: clearDepth) renderInfo.hzbMipCount = 1 renderInfo.hzbIsValid = true renderInfo.viewPort = simd_float2(1920, 1080) @@ -389,7 +392,10 @@ final class CullingTest: BaseRenderSetup { renderInfo.viewPort = originalViewport } - textureResources.hzbDepthPyramid = makeHZBTestTexture(depthValue: 0.2) + // "Solid" HZB — an occluder close to the camera sits in front of the candidate (z=0.8±0.1). + // Standard-Z: close = small value (0.2). Reverse-Z: close = large value (0.95). + let occluderDepth: Float = renderInfo.reverseZEnabled ? 0.95 : 0.2 + textureResources.hzbDepthPyramid = makeHZBTestTexture(depthValue: occluderDepth) renderInfo.hzbMipCount = 1 renderInfo.hzbIsValid = true renderInfo.viewPort = simd_float2(1920, 1080) diff --git a/Tests/UntoldEngineRenderTests/RendererTest.swift b/Tests/UntoldEngineRenderTests/RendererTest.swift index cede0092..3306f29d 100644 --- a/Tests/UntoldEngineRenderTests/RendererTest.swift +++ b/Tests/UntoldEngineRenderTests/RendererTest.swift @@ -36,13 +36,10 @@ final class RendererTests: BaseRenderSetup { // Aspect ratio let aspect = Float(windowWidth) / Float(windowHeight) - // Compute the expected projection matrix - let expectedProjectionMatrix = matrixPerspectiveRightHand( - fovyRadians: degreesToRadians(degrees: fov), - aspectRatio: aspect, - nearZ: near, - farZ: far - ) + // Compute the expected projection matrix, matching the renderer's active Z convention. + let expectedProjectionMatrix = renderInfo.reverseZEnabled + ? matrixPerspectiveRightHandReverseZ(fovyRadians: degreesToRadians(degrees: fov), aspectRatio: aspect, nearZ: near, farZ: far) + : matrixPerspectiveRightHand(fovyRadians: degreesToRadians(degrees: fov), aspectRatio: aspect, nearZ: near, farZ: far) // Compare with the initialized projection matrix in the renderer let actualProjectionMatrix = renderInfo.perspectiveSpace diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/BloomReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/BloomReference.png index 97927da1..a2af98bf 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/BloomReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/BloomReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/ChromaticAberrationReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/ChromaticAberrationReference.png index 24c00189..744e5743 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/ChromaticAberrationReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/ChromaticAberrationReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/ColorGradingReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/ColorGradingReference.png index 398c4086..479abaa8 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/ColorGradingReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/ColorGradingReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/CompositeColorTargetReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/CompositeColorTargetReference.png index b9dc57e4..85e6ea6e 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/CompositeColorTargetReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/CompositeColorTargetReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/DepthOfFieldReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/DepthOfFieldReference.png index 98466690..d5d42e94 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/DepthOfFieldReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/DepthOfFieldReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/DepthTargetReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/DepthTargetReference.png index 83155b03..7e5d456e 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/DepthTargetReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/DepthTargetReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/FXAAReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/FXAAReference.png index 61e99009..a2ae98bd 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/FXAAReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/FXAAReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/FlythroughWaypoint1Reference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/FlythroughWaypoint1Reference.png index 490a5b86..e65b8361 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/FlythroughWaypoint1Reference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/FlythroughWaypoint1Reference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/FlythroughWaypoint2Reference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/FlythroughWaypoint2Reference.png index 2afea537..402dc1d4 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/FlythroughWaypoint2Reference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/FlythroughWaypoint2Reference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/FlythroughWaypoint3Reference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/FlythroughWaypoint3Reference.png index 7be988ee..5452d997 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/FlythroughWaypoint3Reference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/FlythroughWaypoint3Reference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/LightPassColorReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/LightPassColorReference.png index a965d76b..9f24b2f1 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/LightPassColorReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/LightPassColorReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/NormalTargetReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/NormalTargetReference.png index a667b722..874931a7 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/NormalTargetReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/NormalTargetReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/SSAOReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/SSAOReference.png index 22f7ab6c..49c4948f 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/SSAOReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/SSAOReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/TransparencyTargetReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/TransparencyTargetReference.png index a965d76b..9f24b2f1 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/TransparencyTargetReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/TransparencyTargetReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/VignetteReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/VignetteReference.png index 79675d87..4109f142 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/VignetteReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/VignetteReference.png differ diff --git a/docs/API/UsingSpatialInput.md b/docs/API/UsingSpatialInput.md index dbcc04d6..7f7a41b0 100644 --- a/docs/API/UsingSpatialInput.md +++ b/docs/API/UsingSpatialInput.md @@ -523,12 +523,19 @@ if state.spatialTapActive, let entityId = state.pickedEntityId { ## Get Ground/Plane Hit Position -To retrieve the exact world-space position where the user taps on the ground, use `pickRealSurfacePosition`. This is useful for calibration workflows where you need to anchor a point on the ground and scale a model relative to it. +To retrieve the exact world-space position where the user taps on a real-world surface, use `pickRealSurfacePosition`. This raycasts against ARKit-detected physical planes in the user's environment. This is useful for calibration workflows where you need to anchor a point on the ground and scale a model relative to it. + +The `filter` parameter controls which plane alignments are considered: + +- `.horizontalAny` — horizontal planes only (floor, ceiling, table, seat) +- `.verticalAny` — vertical planes only (wall, door, window) +- `.any` — all detected planes regardless of alignment ```swift let state = InputSystem.shared.xrSpatialInputState if state.spatialTapActive{ + // Hit a horizontal surface (e.g. floor or table) if let hit = pickRealSurfacePosition( rayOrigin: state.rayOriginWorld, rayDirection: state.rayDirectionWorld, @@ -536,6 +543,24 @@ if state.spatialTapActive{ ) { Logger.log(message: "Surface type: \(hit.surfaceKind)", vector: hit.worldPosition) } + + // Hit a vertical surface (e.g. wall or door) + if let hit = pickRealSurfacePosition( + rayOrigin: state.rayOriginWorld, + rayDirection: state.rayDirectionWorld, + filter: .verticalAny + ) { + Logger.log(message: "Surface type: \(hit.surfaceKind)", vector: hit.worldPosition) + } + + // Hit any detected surface + if let hit = pickRealSurfacePosition( + rayOrigin: state.rayOriginWorld, + rayDirection: state.rayDirectionWorld, + filter: .any + ) { + Logger.log(message: "Surface type: \(hit.surfaceKind)", vector: hit.worldPosition) + } } ```