Last active
May 23, 2024 03:42
-
-
Save drewolbrich/ca4802c43e6e226e9cdd95a9e52118b3 to your computer and use it in GitHub Desktop.
An example of how to make visionOS volumes work correctly with Settings > Display > Appearance > Window Zoom
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ScaledVolumeContentView.swift | |
// VolumeScaleExample | |
// | |
// Created by Drew Olbrich on 11/6/23. | |
// Copyright © 2023 Lunar Skydiving LLC. All rights reserved. | |
// | |
// MIT License | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in all | |
// copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
// SOFTWARE. | |
// | |
import SwiftUI | |
import RealityKit | |
/// As of visionOS 1.0, if a `RealityView` is presented in a volume and the | |
/// user changes their display size preference using **Settings > Display > | |
/// Appearance > Window Zoom**, the volume's content will either appear too small or | |
/// may be clipped to the edges of the volume for small window zoom sizes. | |
/// | |
/// Apple's volumetric window group example code from | |
/// https://developer.apple.com/documentation/swiftui/windowstyle/volumetric/ | |
/// is insufficient, and may unexpectedly result in clipped geometry when the user | |
/// changes their display size preference: | |
/// | |
/// WindowGroup(id: Module.globe.name) { | |
/// Globe() | |
/// .environment(model) | |
/// } | |
/// .windowStyle(.volumetric) | |
/// .defaultSize(width: 0.6, height: 0.6, depth: 0.6, in: .meters) | |
/// | |
/// To address this issue, `ScaledVolumeContentView`, defined below, uses | |
/// `GeometryReader3D` to present a `RealityView` with content that is automatically | |
/// scaled to reflect the size of its enclosing volume. | |
/// | |
/// The view's `RealityView` has a single entity named `scaledContentEntity` added | |
/// to it. For the scaling to work correctly, all other entities that appear in the | |
/// volume must be parented to `scaledContentEntity`. | |
/// | |
/// `scaledContentEntity` is scaled automatically to account for the display size | |
/// preference chosen by the user in the visionOS Settings app using | |
/// **Settings > Display > Appearance > Window Zoom**. | |
/// | |
/// The simple scaling used here may not be appropriate for every use case. For | |
/// example, you may hypothetically want to replace the | |
/// `scaleEntity(_:with:for:defaultVolumeSize:)` method below with more complex code | |
/// that repositions some entities and scales others. | |
/// | |
/// However, if you are simply using `RealityView` with a volume, the code below | |
/// most likely models the behavior you may expect `RealityView` to have, and not | |
/// the behavior it actually does have. | |
/// | |
/// Example usage: | |
/// | |
/// let defaultVolumeSize = Size3D(width: 0.4, height: 0.4, depth: 0.4) | |
/// ... | |
/// | |
/// WindowGroup { | |
/// ScaledVolumeContentView(defaultVolumeSize: defaultVolumeSize) | |
/// } | |
/// .windowStyle(.volumetric) | |
/// .defaultSize(defaultVolumeSize, in: .meters) | |
/// | |
/// Modify `createVolumeContentEntity` to create your own content for the volume. | |
/// | |
/// Add attachments and placeholders to `RealityView` in this code as necessary. | |
struct ScaledVolumeContentView: View { | |
/// The default size of the volume, in meters, matching the value passed to | |
/// `defaultSize` when the volume was created. | |
/// | |
/// This is not the actual size of the volume if the user has selected a display | |
/// size other than Large in **Settings > Display > Appearance > Window Zoom**. | |
let defaultVolumeSize: Size3D | |
/// The entity that the volume's content is parented to. | |
/// | |
/// As the volume changes size, this entity is scaled to account for the size of the | |
/// volume, ensuring that the volume's content is not clipped to the edges of the | |
/// volume. | |
@State private var scaledContentEntity = Entity() | |
var body: some View { | |
GeometryReader3D { proxy in | |
RealityView { content in | |
// For this approach to work correctly, all entities in the volume must be parented | |
// to this entity instead of being added to the `RealityView` directly using | |
// `RealityViewContent/add`. | |
content.add(scaledContentEntity) | |
// Create the volume's content. | |
let contentEntity = createVolumeContentEntity(forVolumeSize: defaultVolumeSize) | |
scaledContentEntity.addChild(contentEntity) | |
// Scale the RealityView's content to compensate for the initial size of the volume. | |
scaleEntity(scaledContentEntity, with: content, for: proxy, defaultVolumeSize: defaultVolumeSize) | |
} update: { content in | |
// This closure is called whenever the size of the volume changes, which will | |
// happen if the user selects a new value for **Settings > Display > Appearance**. | |
// Set the scale of the RealityView's content to reflect the updated volume size. | |
scaleEntity(scaledContentEntity, with: content, for: proxy, defaultVolumeSize: defaultVolumeSize) | |
} | |
} | |
} | |
/// Returns an entity representing the volume's content. | |
/// | |
/// In this example, we return a yellow sphere, scaled to fit the size of the volume. | |
private func createVolumeContentEntity(forVolumeSize volumeSize: Size3D) -> Entity { | |
let sphereRadius = Float(min(volumeSize.width, volumeSize.height, volumeSize.depth))/2 | |
return Entity.generateYellowSphere(radius: sphereRadius) | |
} | |
/// Sets the scale of `entity` to reflect the user's preferred display size, | |
/// selected with **Settings > Display > Appearance**. | |
/// | |
/// `defaultVolumeSize` must be set to the default size assigned when the enclosing | |
/// volume was created. | |
private func scaleEntity(_ entity: Entity, with realityViewContent: RealityViewContent, for geometryProxy3D: GeometryProxy3D, defaultVolumeSize: Size3D) { | |
/// The bounding box of the volume's contents, in meters. | |
/// | |
/// This value will only equal `defaultVolumeSize` if the preferred display size | |
/// selected by the user with **Settings > Display > Appearance** happens to be | |
/// Large, which is the default value for visionOS 1.0. | |
let scaledVolumeContentBoundingBox = realityViewContent.convert(geometryProxy3D.frame(in: .local), from: .local, to: .scene) | |
let scale = scaledVolumeContentBoundingBox.extents.x/Float(defaultVolumeSize.width) | |
entity.scale = [1, 1, 1]*scale | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment