上篇文章说道,审核被拒了,这两天就忙着把AR的功能加进去,再提交一版。中间还尝试了一下Facebook的Spark AR Studio,还有抖音的像塑,快手的Beyond Effect。三者都是社交媒体的特效制作工具,回头我再把其他几个的使用体验也都写成文章,敬请期待。
0. ARKit vs RealityKit
ARKit还是RealityKit 其实描述的不准确,其实应该在SceneKit 还是 RealityKit之间进行选择,当然我们会选择未来的版本即RealityKit。就因为是未来的版本,所以现在RealityKit的文章很少,文档也不是很健全,中间不免会踩不少的坑。没关系,早就踩坑踩习惯了!当然这里同时又激发了写RealityKit文档翻译的欲望。。
1. 通过UIViewRepresentable自定义View
首先创建一个文件PosterARView.swift
struct PosterARView: UIViewRepresentable { //1
var arView: ARView! // 2
var imageUrl: String = ""
init(imageUrl url: String) {
self.arView = ARView() // 3
self.arView.addCoaching()
self.imageUrl = url
}
func makeCoordinator() -> ARViewCoordinator {
return ARViewCoordinator(arView: self.arView, imageUrl: self.imageUrl)
} //4
func makeUIView(context: Context) -> ARView { //5
let arConfiguration = ARWorldTrackingConfiguration()
arConfiguration.planeDetection = .vertical
arView.session.run(arConfiguration) // 6
arView.session.delegate = context.coordinator // 7
let tapGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(sender:)))
arView.addGestureRecognizer(tapGesture) // 8
return arView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
-
UIViewRepresentable是一个UIKit视图的包装器,用于将该视图集成到SwiftUI的视图层次中。(参考链接1)
-
我们在这里定义了一个ARView
-
初始化 ARView,并给这个ARView添加引导层
-
coordinator比较关键,是ARView和包裹其的SwiftUI中View通讯的桥梁。我们在这里初始化了自定义的ARViewCoordinator,这个对象我会在后面介绍
-
makeUIView 是 UIViewRepresentable协议必须要实现的的方法,我们在这个方法中初始化ARView,并返回ARView。
-
我们可以看到在这个项目中我初始化的AR配置是世界追踪,并设置了追踪的屏幕是垂直的,因为我只希望用户将海报贴在垂直的墙面上。
-
将ARView 中 session的delegate设置到context的coodinator,也就是 4 中返回的对象。
-
这里给ARView添加一个 点击手势识别的方法。 (参考链接2)
实现ARCoachingOverlayView
extension ARView: ARCoachingOverlayViewDelegate {
func addCoaching() {
let coachingOverlay = ARCoachingOverlayView()
coachingOverlay.delegate = self
coachingOverlay.session = self.session
coachingOverlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
coachingOverlay.goal = .anyPlane
self.addSubview(coachingOverlay)
}
public func coachingOverlayViewDidDeactivate(_ coachingOverlayView: ARCoachingOverlayView) {
//Ready to add entities next?
}
}
这里通过 实现 addCoaching 方法来完成ARView的引导层设置,这里我们设置引导层结束并退出的目标是 识别到任意屏幕。
2. 定义一个Poster对象
class Poster: Entity, HasModel, HasAnchoring, HasCollision { // 1
required init() {
super.init()
}
convenience init(_ imageUrl: String) {
self.init()
let cache = Environment(\.imageCache).wrappedValue // 2
var image: UIImage?
// Create a temporary file URL to store the image at the remote URL.
let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
image = cache[URL(string: imageUrl)!]
let data = image?.pngData()
if data == nil {
return
}
// Write the image Data to the file URL.
try! data!.write(to: fileURL) // 3
let mesh = MeshResource.generatePlane(width: 0.4, height: 0.6) // 4
// 不受AR环境中光的影响
var material = UnlitMaterial()
material.tintColor = UIColor.white.withAlphaComponent(1) // 5
do {
// Create a TextureResource by loading the contents of the file URL.
let texture = try TextureResource.load(contentsOf: fileURL)
material.baseColor = MaterialColorParameter.texture(texture)
self.components[ModelComponent] = ModelComponent(
mesh: mesh,
materials: [material]
) // 6
} catch {
print(error.localizedDescription)
}
}
}
- 主要继承了Entity 和 HasAnchoring对象,用于后续将entity添加到session中,这里参考一下官方文档的图
-
这里我使用了前面文章提到的 AsyncImage 中定义的图片缓存,也就是图片下载过之后,在这里就不需要重复下载了,只需要根据URL去缓存中找图片对象就可以了。
-
这里需要将内存中的数据保存在磁盘上,才能供后续texture调用。
-
因为我们这里要将海报贴在墙上,所以要创建一个 plane类型的mesh,并设置mesh的宽和高为0.4米和0.6米。这里参数的单位就是“米”,因为这个单位,我还找了半天,最终在文档中找到了。(参考链接3)
-
如果使用参考链接中的方法使用 SimpleMaterial 会使得图片很暗,所以这里使用 UnlitMaterial 使得texture不受AR环境光影响
-
最终生成海报图片的Entity。参考链接4
3. 协调器-实现ARViewCoordinator
class ARViewCoordinator: NSObject, ARSessionDelegate {
var arView: ARView
var imageUrl: String = ""
init(arView: ARView, imageUrl url: String) {
self.arView = arView
self.imageUrl = url
super.init()
}
@objc func handleTap(sender: UITapGestureRecognizer) { // 1
let tapLocation: CGPoint = sender.location(in: arView) // 2
let estimatedPlane: ARRaycastQuery.Target = .existingPlaneInfinite
let alignment: ARRaycastQuery.TargetAlignment = .vertical
let result: [ARRaycastResult] = arView.raycast(from: tapLocation, allowing: estimatedPlane, alignment: alignment) // 3
guard let rayCast: ARRaycastResult = result.first
else {
print("no result")
return
} // 4
guard let planeAnchor = rayCast.anchor as? ARPlaneAnchor
else {
print("not a plane")
return
}
if planeAnchor.alignment != .vertical {
print("not a vertical plane")
return
}
let poster = Poster(imageUrl) // 7
let radians = -90.0 * Float.pi / 180.0
// 根据raycast 设置位置
poster.setTransformMatrix(rayCast.worldTransform, relativeTo: nil)
// 根据X轴旋转 90°
poster.transform.rotation *= simd_quatf(angle: radians, axis: SIMD3<Float>(1,0,0)) // 8
// 在场景中添加该entity
arView.scene.addAnchor(poster) // 9
}
func session(_ session: ARSession, didUpdate frame: ARFrame) {
}
}
-
在上文我们提到了,为ARView添加手势,这里是手势action的处理函数
-
获取到点击手势的屏幕位置点
-
通过Reality Kit的raycast方法,获取屏幕点击位置发送射线所到达的真实世界的点。(参考链接5)
-
判断如果射线没有返回任何值,则返回。
-
判断如果射线到达的结果不是一个平面,则返回。
-
判断如果射线锁到达的平面不是垂直的,则返回。
-
通过从资源详情页带过来的imageUrl,创建海报对象,这个对象是一个Entity
-
当我们添加Entity到ARView的Scene中的时候,平面默认是面向下的,因此我们需要对屏幕进行翻转。这里同时可以看一下ARView中坐标系,如下图。(参考链接6)
- 最终将海报Entity添加到ARView的Scene中。
4. 创建SwiftUI View包裹 ARView
因为ARView是一个 UIViewRepresentable 的实现,无法直接继承到 SwiftUI 的 View中,因此我们需要先创建一个 View将其包裹起来,再供其他View使用
//
// ResourceDetailPosterView.swift
// FoloPro
//
// Created by GUNNER on 2021/8/31.
//
import SwiftUI
struct ResourceDetailPosterView: View {
var imageUrl: String = ""
var body: some View {
PosterARView(imageUrl: imageUrl).navigationTitle("AR海报")
}
}
struct ResourceDetailPosterView_Previews: PreviewProvider {
static var previews: some View {
ResourceDetailPosterView(imageUrl: "https://cdn1.paranoidsqd.com/2021-08-11-151936.png")
}
}
5. 在原有详情页跳转
我们在原有详情页的海报图片上包裹一层 NavigationLink 来实现 页面跳转
VStack (alignment:.center) {
HStack(alignment:.top) {
NavigationLink(
destination: ResourceDetailPosterView(imageUrl: resource.poster)) {
AsyncImage(url: URL(string: resource.poster)!,
placeholder: { Text("Loading ...") },
image: {
Image(uiImage: $0).resizable()
})
.scaledToFit()
.frame(width: 156, height: 240)
}
....
6. 再次被拒
心想这次增加了这个屌炸天的功能之后,不会再说我功能少了吧?结果还是被拒了,被拒的原因如下:
imageGuideLine 2.3 - Performance - Accurate Metadata
意思大概就是,在详情页里面提到了可以追剧,但是实际功能并不能追剧。所以要么增加追剧的功能,要么把详情页的介绍去掉。
OK,那我先简单点做,把详情页改一下吧。
imageGuideline 4.0 - Design
主要是提到了页面的UI的问题,有的页面太拥挤了,一些文字看不全。
我大概知道一些地方了,主要是类目页和详情页。那我就先改一版UI再提交吧。 等我的好消息!
参考链接:
-
https://betterprogramming.pub/how-to-use-uiviewrepresentable-in-swiftui-1b9a0a7c1358
-
https://developer.apple.com/documentation/realitykit/meshresource/3244420-generateplane
-
https://stackoverflow.com/questions/61854324/add-uiimage-as-texture-to-a-plane-in-realitykit
-
https://developer.apple.com/documentation/realitykit/arview/3282007-raycast
网友评论