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
72 changes: 66 additions & 6 deletions Editor/Sources/EditorApp/Panels/ViewportPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ struct ViewportPanel: View {
let gizmoSpace = store.gizmoSpace
let shadingMode = store.viewportShadingMode
let shadowsEnabled = store.viewportShadowsEnabled
let renderScalePercent = store.viewportRenderScalePercent
let interactionDownscaleEnabled = store.viewportInteractionDownscaleEnabled
let playbackState = store.playbackState

// 推送 gizmo 控制器所需的快照(摄像机 / 视口矩形 / 实体世界坐标)。
let _: Void = updateGizmoSnapshot(selectedID: selectedEntityID,
gizmoMode: gizmoMode,
gizmoSpace: gizmoSpace,
surface: surface)
gizmoSpace: gizmoSpace)

ViewportHost(surface: surface,
onInputEvent: { event in
Expand Down Expand Up @@ -71,6 +72,8 @@ struct ViewportPanel: View {
gizmoSpace: gizmoSpace,
shadingMode: shadingMode,
shadowsEnabled: shadowsEnabled,
renderScalePercent: renderScalePercent,
interactionDownscaleEnabled: interactionDownscaleEnabled,
playbackState: playbackState,
onSelectGizmoMode: { mode in
if gizmoMode != mode {
Expand All @@ -88,6 +91,12 @@ struct ViewportPanel: View {
onToggleShadows: {
app.setViewportShadowsEnabled(!shadowsEnabled)
},
onSelectRenderScale: { percent in
app.setViewportRenderScalePercent(percent)
},
onToggleInteractionDownscale: {
app.setViewportInteractionDownscaleEnabled(!interactionDownscaleEnabled)
},
onPlay: { app.applyPlaybackState(.playing) },
onPause: { app.applyPlaybackState(.paused) },
onStop: { app.applyPlaybackState(.stopped) })
Expand Down Expand Up @@ -381,8 +390,7 @@ struct ViewportPanel: View {

private func updateGizmoSnapshot(selectedID: UInt64?,
gizmoMode: EditorGizmoMode,
gizmoSpace: EditorGizmoSpace,
surface: ViewportSurfaceState) {
gizmoSpace: EditorGizmoSpace) {
guard let mode = controllerMode(for: gizmoMode),
let id = selectedID,
let world = scene.entityWorldPosition(id),
Expand All @@ -405,8 +413,6 @@ struct ViewportPanel: View {
space: gizmoSpace == .local ? .local : .world,
camera: camera,
frame: frame,
drawableWidth: Float(surface.width),
drawableHeight: Float(surface.height),
entityID: id,
entityWorldPosition: world,
entityWorldMatrix: worldMatrix,
Expand Down Expand Up @@ -1202,11 +1208,15 @@ private struct ViewportInfoBar: View {
let gizmoSpace: EditorGizmoSpace
let shadingMode: EditorViewportShadingMode
let shadowsEnabled: Bool
let renderScalePercent: Int
let interactionDownscaleEnabled: Bool
let playbackState: PlaybackState
let onSelectGizmoMode: (EditorGizmoMode) -> Void
let onSelectGizmoSpace: (EditorGizmoSpace) -> Void
let onSelectShadingMode: (EditorViewportShadingMode) -> Void
let onToggleShadows: () -> Void
let onSelectRenderScale: (Int) -> Void
let onToggleInteractionDownscale: () -> Void
let onPlay: () -> Void
let onPause: () -> Void
let onStop: () -> Void
Expand Down Expand Up @@ -1260,6 +1270,11 @@ private struct ViewportInfoBar: View {
}
.buttonStyle(.toggle)

RenderScaleSelector(percent: renderScalePercent,
interactionDownscaleEnabled: interactionDownscaleEnabled,
onSelect: onSelectRenderScale,
onToggleInteractionDownscale: onToggleInteractionDownscale)

Divider()
.frame(width: 1, height: 16)
.foregroundColor(Color(r: 0, g: 0, b: 0, a: 0.4))
Expand Down Expand Up @@ -1430,6 +1445,51 @@ private struct GizmoAxisChip: View {
}
}

/// Unreal-style screen percentage for the 3D viewport: presentation size is
/// fixed, the engine renders `percent` of it and the composite upscales.
private struct RenderScaleSelector: View {
let percent: Int
let interactionDownscaleEnabled: Bool
let onSelect: (Int) -> Void
let onToggleInteractionDownscale: () -> Void
@State private var isPresented: Bool = false

private static let presets = [50, 75, 100, 150, 200]

var body: some View {
Popover(isPresented: $isPresented, width: 168) {
Row(alignment: .center, spacing: 5) {
Text("\(percent)%", lineLimit: 1)
.font(.caption)
.foregroundColor(.onSurface)
Icon(UICommonIcons.chevronDown, size: 8, color: .onSurfaceMuted)
}
.padding(horizontal: 8, vertical: 4)
.background(.surfaceSunken)
.cornerRadius(3)
} content: {
Menu(menuEntries, width: 168, maxVisibleRows: 8, onItemActivated: {
isPresented = false
})
}
}

private var menuEntries: [MenuEntry] {
var entries: [MenuEntry] = Self.presets.map { preset in
.item(MenuItem(id: "renderscale-\(preset)",
title: "\(preset)%",
isSelected: preset == percent,
action: { onSelect(preset) }))
}
entries.append(.separator(id: "renderscale-sep"))
entries.append(.item(MenuItem(id: "renderscale-interaction",
title: L("Downscale while navigating"),
isSelected: interactionDownscaleEnabled,
action: { onToggleInteractionDownscale() })))
return entries
}
}

private struct ViewModeSelector: View {
let shadingMode: EditorViewportShadingMode
let onSelect: (EditorViewportShadingMode) -> Void
Expand Down
10 changes: 7 additions & 3 deletions Editor/Sources/EditorApp/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,13 @@ private func runLegacyEditor(launchOptions: EditorAppLaunchOptions) throws {
events: events,
onTick: { dt in
context.tick(deltaTime: dt)
if let bundle = context.bundle {
let size = bundle.app.viewportDrawableSize
inGameUIHost.tick(width: Int(size.width), height: Int(size.height))
if context.bundle != nil {
// HUD 布局用视口的逻辑尺寸;光栅化按窗口 content scale。
let scale = max(1, ContentScaleHolder.current)
let frame = EditorViewportDropTarget.frame
let logicalW = Int((frame?.width ?? 1280).rounded())
let logicalH = Int((frame?.height ?? 720).rounded())
inGameUIHost.tick(width: logicalW, height: logicalH, contentScale: scale)
}
},
onDisplayReady: { display in
Expand Down
8 changes: 2 additions & 6 deletions Editor/Sources/EditorCore/Editing/EditorGizmoController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@ public final class EditorGizmoController: @unchecked Sendable {
}
}

/// All screen-space math runs in the logical `frame` coordinates; the
/// engine's pixel resolution (render scale, HiDPI) never enters here.
public struct Snapshot {
public var mode: Mode
public var space: GizmoSpace
public var camera: RenderCamera
public var frame: ViewportScreenFrame
public var drawableWidth: Float
public var drawableHeight: Float
public var entityID: UInt64
public var entityWorldPosition: SIMD3<Float>
public var entityWorldMatrix: simd_float4x4
Expand All @@ -109,8 +109,6 @@ public final class EditorGizmoController: @unchecked Sendable {
space: GizmoSpace = .local,
camera: RenderCamera,
frame: ViewportScreenFrame,
drawableWidth: Float,
drawableHeight: Float,
entityID: UInt64,
entityWorldPosition: SIMD3<Float>,
entityWorldMatrix: simd_float4x4,
Expand All @@ -121,8 +119,6 @@ public final class EditorGizmoController: @unchecked Sendable {
self.space = space
self.camera = camera
self.frame = frame
self.drawableWidth = drawableWidth
self.drawableHeight = drawableHeight
self.entityID = entityID
self.entityWorldPosition = entityWorldPosition
self.entityWorldMatrix = entityWorldMatrix
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ public final class EditorViewportInputController: @unchecked Sendable {
activeInteraction != nil
}

/// True while the scene re-renders continuously (camera or gizmo drags) —
/// the window where interaction downscale pays off. Clicks and marquee
/// selection leave the camera static, so they stay full-res.
public var isContinuousSceneInteractionActive: Bool {
switch activeInteraction {
case .camera, .gizmo: return true
case .pendingClick, .marquee, nil: return false
}
}

public func begin(_ interaction: ActiveInteraction,
at point: (x: Float, y: Float),
modifiers: KeyModifiers) {
Expand Down
30 changes: 30 additions & 0 deletions Editor/Sources/EditorCore/Editing/EditorViewportResolution.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import RenderBackend

/// Maps the viewport's presentation size (physical pixels of the on-screen
/// quad) to the engine's render resolution: presentation × renderScale ×
/// optional interaction downscale. Pure math, kept separate for tests.
public enum EditorViewportResolution {
public static let maxDimension: UInt32 = 16_384
/// Extra factor applied while a camera / gizmo drag is active and the
/// interaction-downscale toggle is on.
public static let interactionFactor: Float = 0.5

public static func effectiveSize(presentation: RenderDrawableSize,
renderScalePercent: Int,
interactionDownscaleActive: Bool) -> RenderDrawableSize {
var scale = Float(EditorState.sanitizedRenderScalePercent(renderScalePercent)) / 100
if interactionDownscaleActive {
scale *= interactionFactor
}
return RenderDrawableSize(
width: scaled(presentation.width, by: scale),
height: scaled(presentation.height, by: scale)
)
}

private static func scaled(_ value: UInt32, by scale: Float) -> UInt32 {
let raw = (Float(value) * scale).rounded()
guard raw >= 1 else { return 1 }
return min(UInt32(raw), maxDimension)
}
}
30 changes: 29 additions & 1 deletion Editor/Sources/EditorCore/EditorCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ public final class EditorApplication: @unchecked Sendable {
engine.tick(
deltaTime: deltaTime,
inputEvents: inputEvents,
drawableSize: _viewportDrawableSize,
drawableSize: effectiveViewportDrawableSize(),
shouldRender: store.state.shouldRender,
renderSceneOverride: scene.currentRenderScene(),
jointPaletteOverride: scene.currentJointPaletteMap()
Expand Down Expand Up @@ -213,13 +213,41 @@ public final class EditorApplication: @unchecked Sendable {
pendingViewportEvents.append(event)
}

/// Presentation size of the viewport in physical pixels (reported by
/// `ViewportHost`). The engine renders this scaled by the render-scale
/// settings — see `effectiveViewportDrawableSize()`.
public var viewportDrawableSize: RenderDrawableSize { _viewportDrawableSize }

public func setViewportDrawableSize(_ size: RenderDrawableSize) {
guard _viewportDrawableSize != size else { return }
_viewportDrawableSize = size
}

private func effectiveViewportDrawableSize() -> RenderDrawableSize {
let state = store.state
let interacting = state.viewportInteractionDownscaleEnabled
&& EditorViewportInputController.shared.isContinuousSceneInteractionActive
return EditorViewportResolution.effectiveSize(
presentation: _viewportDrawableSize,
renderScalePercent: state.viewportRenderScalePercent,
interactionDownscaleActive: interacting
)
}

public func setViewportRenderScalePercent(_ percent: Int) {
let sanitized = EditorState.sanitizedRenderScalePercent(percent)
guard store.state.viewportRenderScalePercent != sanitized else { return }
store.dispatch(.setViewportRenderScalePercent(sanitized))
logConsole("Viewport render scale \(sanitized)%")
}

public func setViewportInteractionDownscaleEnabled(_ enabled: Bool) {
guard store.state.viewportInteractionDownscaleEnabled != enabled else { return }
store.dispatch(.setViewportInteractionDownscale(enabled))
logConsole(enabled ? "Viewport interaction downscale enabled"
: "Viewport interaction downscale disabled")
}

public func setViewportRenderCompletionHandler(_ handler: (@Sendable (ViewportSurfaceState) -> Void)?) {
engine.setRenderCompletionHandler { completion in
handler?(completion.viewportSurfaceState)
Expand Down
8 changes: 8 additions & 0 deletions Editor/Sources/EditorCore/State/EditorReducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public enum EditorAction: Sendable {
case setGizmoSpace(EditorGizmoSpace)
case setViewportShadingMode(EditorViewportShadingMode)
case setViewportShadowsEnabled(Bool)
case setViewportRenderScalePercent(Int)
case setViewportInteractionDownscale(Bool)
case setTranslateSnapEnabled(Bool)
case setRotateSnapEnabled(Bool)
case setScaleSnapEnabled(Bool)
Expand Down Expand Up @@ -118,6 +120,12 @@ public enum EditorReducer {
case let .setViewportShadowsEnabled(enabled):
state.viewportShadowsEnabled = enabled

case let .setViewportRenderScalePercent(percent):
state.viewportRenderScalePercent = EditorState.sanitizedRenderScalePercent(percent)

case let .setViewportInteractionDownscale(enabled):
state.viewportInteractionDownscaleEnabled = enabled

case let .setTranslateSnapEnabled(enabled):
state.translateSnapEnabled = enabled

Expand Down
20 changes: 20 additions & 0 deletions Editor/Sources/EditorCore/State/EditorState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,12 @@ public struct EditorState: Codable, Sendable {
public var viewportDirectionalCascadeCount: Int
public var viewportDirectionalCascadeSplitLambda: Float
public var viewportShadowDebugMode: EditorViewportShadowDebugMode
/// Screen-percentage style render scale for the 3D viewport (100 = native
/// pixels). The presentation size stays fixed; the engine renders
/// `presentation × percent/100` and the composite quad rescales.
public var viewportRenderScalePercent: Int
/// Temporarily halve the render resolution during camera / gizmo drags.
public var viewportInteractionDownscaleEnabled: Bool
public var translateSnapEnabled: Bool
public var rotateSnapEnabled: Bool
public var scaleSnapEnabled: Bool
Expand Down Expand Up @@ -293,6 +299,8 @@ public struct EditorState: Codable, Sendable {
viewportDirectionalCascadeCount: Int = 1,
viewportDirectionalCascadeSplitLambda: Float = 0.55,
viewportShadowDebugMode: EditorViewportShadowDebugMode = .off,
viewportRenderScalePercent: Int = 100,
viewportInteractionDownscaleEnabled: Bool = false,
translateSnapEnabled: Bool = false,
rotateSnapEnabled: Bool = false,
scaleSnapEnabled: Bool = false,
Expand Down Expand Up @@ -336,6 +344,8 @@ public struct EditorState: Codable, Sendable {
self.viewportDirectionalCascadeCount = Self.sanitizedDirectionalCascadeCount(viewportDirectionalCascadeCount)
self.viewportDirectionalCascadeSplitLambda = Self.sanitizedDirectionalCascadeSplitLambda(viewportDirectionalCascadeSplitLambda)
self.viewportShadowDebugMode = viewportShadowDebugMode
self.viewportRenderScalePercent = Self.sanitizedRenderScalePercent(viewportRenderScalePercent)
self.viewportInteractionDownscaleEnabled = viewportInteractionDownscaleEnabled
self.translateSnapEnabled = translateSnapEnabled
self.rotateSnapEnabled = rotateSnapEnabled
self.scaleSnapEnabled = scaleSnapEnabled
Expand Down Expand Up @@ -397,6 +407,8 @@ public struct EditorState: Codable, Sendable {
case viewportDirectionalCascadeCount
case viewportDirectionalCascadeSplitLambda
case viewportShadowDebugMode
case viewportRenderScalePercent
case viewportInteractionDownscaleEnabled
case translateSnapEnabled
case rotateSnapEnabled
case scaleSnapEnabled
Expand Down Expand Up @@ -459,6 +471,8 @@ public struct EditorState: Codable, Sendable {
viewportDirectionalCascadeCount: try c.decodeIfPresent(Int.self, forKey: .viewportDirectionalCascadeCount) ?? 1,
viewportDirectionalCascadeSplitLambda: try c.decodeIfPresent(Float.self, forKey: .viewportDirectionalCascadeSplitLambda) ?? 0.55,
viewportShadowDebugMode: try c.decodeIfPresent(EditorViewportShadowDebugMode.self, forKey: .viewportShadowDebugMode) ?? .off,
viewportRenderScalePercent: try c.decodeIfPresent(Int.self, forKey: .viewportRenderScalePercent) ?? 100,
viewportInteractionDownscaleEnabled: try c.decodeIfPresent(Bool.self, forKey: .viewportInteractionDownscaleEnabled) ?? false,
translateSnapEnabled: try c.decodeIfPresent(Bool.self, forKey: .translateSnapEnabled) ?? false,
rotateSnapEnabled: try c.decodeIfPresent(Bool.self, forKey: .rotateSnapEnabled) ?? false,
scaleSnapEnabled: try c.decodeIfPresent(Bool.self, forKey: .scaleSnapEnabled) ?? false,
Expand Down Expand Up @@ -503,6 +517,8 @@ public struct EditorState: Codable, Sendable {
try c.encode(viewportDirectionalCascadeCount, forKey: .viewportDirectionalCascadeCount)
try c.encode(viewportDirectionalCascadeSplitLambda, forKey: .viewportDirectionalCascadeSplitLambda)
try c.encode(viewportShadowDebugMode, forKey: .viewportShadowDebugMode)
try c.encode(viewportRenderScalePercent, forKey: .viewportRenderScalePercent)
try c.encode(viewportInteractionDownscaleEnabled, forKey: .viewportInteractionDownscaleEnabled)
try c.encode(translateSnapEnabled, forKey: .translateSnapEnabled)
try c.encode(rotateSnapEnabled, forKey: .rotateSnapEnabled)
try c.encode(scaleSnapEnabled, forKey: .scaleSnapEnabled)
Expand Down Expand Up @@ -537,6 +553,10 @@ public struct EditorState: Codable, Sendable {
public static func sanitizedDirectionalCascadeSplitLambda(_ value: Float) -> Float {
min(max(value, 0), 1)
}

public static func sanitizedRenderScalePercent(_ value: Int) -> Int {
min(max(value, 25), 200)
}
}

public struct EditorPendingCloseRequest: Equatable, Sendable {
Expand Down
Loading
Loading