本例选择了《天空之城》的25张照片,组成5×5的照片墙)。首先我们在setupContentEntity
方法中构建了一个纹理数组,将这25张照片添加到数组images
中。其中封装了setup
方法,借助于visionOS对沉浸式空间的支持,我们创建了三个平面,组成具有立体感的照片墙。
在setup
方法中调用了addChildEntities
,对images
随机打散,通过quotientAndRemainder
方法对5求商取余来设置x
和y
的值,从而生成5×5的照片,z
轴上仅以平面为基准做了小小的调整。将准备好的位置和纹理,传入makePlane
方法进行配置返回实体再分别添加到3个平面中。
为增加趣味性,这里还定义了toggleSorted()
方法,在沉浸式空间内点击时会打散(randomSetChildPositions()
方法),再次点击又会重置收起(resetChildPositions()
)。完整的ViewModel.swift
文件内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
import SwiftUI import RealityKit @Observable class ViewModel { private let planeSize = CGSize(width: 0.32, height: 0.18) private let maxPlaneSize = CGSize(width: 3.0, height: 2.0) private var contentEntity = Entity() private var boardPlanes: [ModelEntity] = [] private var images: [MaterialParameters.Texture] = [] private var sorted = true func setupContentEntity() -> Entity { for i in 1..<26 { let name = "laputa\(String(format: "%03d", i))" if let texture = try? TextureResource.load(named: name) { images.append(MaterialParameters.Texture(texture)) } } setup() return contentEntity } func toggleSorted() { if sorted { sorted.toggle() randomSetChildPositions() } else { sorted.toggle() resetChildPositions() } } // MARK: - Private private func setup() { for i in 0..<3 { let boardPlane = ModelEntity( mesh: .generatePlane(width: 3, height: 2), materials: [SimpleMaterial(color: .clear, isMetallic: false)] ) boardPlane.position = SIMD3<Float>(x: 0, y : 2, z: -0.5 - 0.1 * Float(i + 1)) contentEntity.addChild(boardPlane) boardPlanes.append(boardPlane) addChildEntities(boardPlane: boardPlane) } } private func addChildEntities(boardPlane: ModelEntity) { var i: Int = 0 for image in images.shuffled().prefix(30) { let divisionResult = i.quotientAndRemainder(dividingBy: 5) let x: Float = Float(divisionResult.remainder) * 0.4 - 0.75 let y: Float = Float(divisionResult.quotient) * 0.25 - 0.5 let z: Float = boardPlane.position.z + Float(i) * 0.0001 let entity = makePlane(name: "", position: SIMD3<Float>(x: x, y: y, z: z), texture: image) boardPlane.addChild(entity) i += 1 } } private func makePlane(name: String, position: SIMD3<Float>, texture: MaterialParameters.Texture) -> ModelEntity { var material = SimpleMaterial() material.color = .init(texture: texture) let entity = ModelEntity( mesh: .generatePlane(width: 0.32, height: 0.18, cornerRadius: 0.0), materials: [material], collisionShape: .generateBox(width: 0.32, height: 0.18, depth: 0.1), mass: 0.0 ) entity.name = name entity.position = position entity.components.set(InputTargetComponent(allowedInputTypes: .indirect)) return entity } private func move(entity: Entity, position: SIMD2<Float>) { let move = FromToByAnimation<Transform>( name: "move", from: .init(scale: .init(repeating: 1), translation: entity.position), to: .init(scale: .init(repeating: 1), translation: .init(x: position.x, y: position.y, z: entity.position.z)), duration: 2.0, timing: .linear, bindTarget: .transform ) let animation = try! AnimationResource.generate(with: move) entity.playAnimation(animation, transitionDuration: 2.0) } private func randomSetChildPositions() { let size = CGSize(width: planeSize.width * 1.2, height: planeSize.height * 1.2) for boardPlane in boardPlanes { let newPoints = randomPoints(count: boardPlane.children.count, size: size) for i in 0..<boardPlane.children.count { let entity = boardPlane.children[i] move(entity: entity, position: newPoints[i]) } } } private func resetChildPositions() { for boardPlane in boardPlanes { var i: Int = 0 for entity in boardPlane.children { let divisionResult = i.quotientAndRemainder(dividingBy: 5) let x: Float = Float(divisionResult.remainder) * 0.4 - 0.75 let y: Float = Float(divisionResult.quotient) * 0.25 - 0.5 move(entity: entity, position: SIMD2<Float>(x, y)) i += 1 } } } private func randomPoints(count: Int, size: CGSize) -> [SIMD2<Float>] { var ret: [SIMD2<Float>] = [] while ret.count < count { if let point = randomPoint(size: size, positions: ret) { ret.append(point) } } return ret } private func randomPoint(size: CGSize, positions: [SIMD2<Float>]) -> SIMD2<Float>? { for _ in 0..<5000 { let x = CGFloat.random(in: -maxPlaneSize.width...(maxPlaneSize.width / 2)) let y = CGFloat.random(in: -maxPlaneSize.height...(maxPlaneSize.height / 2)) let frame = CGRect(x: CGFloat(x), y: CGFloat(y), width: size.width, height: size.height) if positions.isEmpty { return SIMD2<Float>(Float(x), Float(y)) } else { var intersects = false for position in positions { let f = CGRect(x: CGFloat(position.x), y: CGFloat(position.y), width: size.width, height: size.height) if f.intersects(frame) { intersects = true } } if !intersects { return SIMD2<Float>(Float(frame.minX), Float(frame.minY)) } } } return nil } } |
在ImmersiveView
中发生了Tap事件后会调用其中的toggleSorted()
方法,其它代码与此前的示例并没什么不同。
1 2 3 4 5 6 7 8 9 10 11 12 |
struct ImmersiveView: View { @State var model = ViewModel() var body: some View { RealityView { content in content.add(model.setupContentEntity()) } .onTapGesture { model.toggleSorted() } } } |
示例代码:GitHub仓库
其它相关内容请见虚拟现实(VR)/增强现实(AR)&visionOS开发学习笔记