Я думал реализовать это путем манипулирования костями, но обнаружил, что RealityKit (iOS 16) не предоставляет такой возможности.
Если я загружаю модель как Entity, то у меня есть граф объектов типа Entity — геометрия модели, но нет костей.
Если Я загружаю модель как ModelEntity, тогда у меня вообще нет прямого доступа к дереву дочерних элементов, но есть 2 списка: JointNames и JointTransforms. Насколько я понимаю, это списки названий и трансформаций костей соответственно.
Ниже приведен код базовой загрузки объекта:
Код: Выделить всё
import SwiftUI
import RealityKit
import ARKit
import Combine
struct ARViewContainer: UIViewRepresentable {
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = [.horizontal]
arView.session.run(configuration)
addCouchingOverlay(arView: arView)
addModelToARView(arView: arView, context: context)
return arView
}
func updateUIView(_ uiView: ARView, context: Context) { }
func makeCoordinator() -> ARCoordinator {
ARCoordinator()
}
private func addModelToARView(arView: ARView, context: Context) {
Entity.loadModelAsync(named: "model.usdz").sink(
receiveCompletion: { completion in
if case let .failure(error) = completion {
print("Error loading model: \(error)")
}
},
receiveValue: { modelEntity in
configureModel(context: context, arView: arView, modelEntity: modelEntity)
}
).store(in: &context.coordinator.cancellables)
}
}
extension ARViewContainer {
private func addCouchingOverlay(arView: ARView) {
let coachingOverlay = ARCoachingOverlayView()
coachingOverlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
coachingOverlay.session = arView.session
coachingOverlay.goal = .horizontalPlane
arView.addSubview(coachingOverlay)
}
private func configureModel(context: Context, arView: ARView, modelEntity: ModelEntity) {
let anchorEntity = AnchorEntity(plane: .horizontal)
anchorEntity.addChild(modelEntity)
arView.scene.addAnchor(anchorEntity)
let minScale: Float = 0.001
modelEntity.scale = [minScale, minScale, minScale]
context.coordinator.modelEntity = modelEntity
arView.scene.subscribe(to: SceneEvents.Update.self) { _ in
let currentScale = modelEntity.scale.x
if currentScale < minScale {
modelEntity.scale = [minScale, minScale, minScale]
}
}.store(in: &context.coordinator.cancellables)
}
}
class ARCoordinator {
var cancellables: Set = []
var modelEntity: ModelEntity?
}
В ходе эксперимента я обнаружил, что при изменении трансформаций в списке JointTransforms визуально меняется сама модель. Поэтому я даже пытался собрать из Entity собственное дерево, задавая имена и преобразования из списков JointNames и JointTransforms. Затем я сохраняю их в объекте-координаторе для быстрого доступа к ним, а также индексы необходимых суставов для последующих обновлений значений в JointTransforms.
Я запустите функцию по таймеру, чтобы изменить трансформацию кости согласно следующей логике:
- берём объекты головы, шеи и самой модели
получите положение головы относительно шеи - выполните преобразование головы, используя вид (at:, from:, upVector:relativeTo:) функция
- перезаписать значение в JointTransforms по индексу
Код: Выделить всё
import Foundation
import RealityKit
import Combine
class ARCoordinator {
var cancellables: Set = []
var modelEntity: ModelEntity?
var entity: Entity?
var neck: Entity?
var head: Entity?
var lEye: Entity?
var rEye: Entity?
var headTransformIdx = 0
var lEyeTransformIdx = 0
var rEyeTransformIdx = 0
private var trackingTimer: Timer?
private var skeletoneIsBuilded = false
private var entities: [String: Entity] = [:]
func startCameraTrackingTimer(arView: ARView) {
trackingTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
if let isBuilded = self?.skeletoneIsBuilded, isBuilded {
self?.updateModelOrientation(arView: arView)
}
}
}
private func prepareEntities() {
let neckName = "root/body/neck"
let headName = "root/body/neck/head"
let eyeLName = "root/body/neck/head/eye_left"
let eyeRName = "root/body/neck/head/eye_right"
if let entity = entities[neckName],
let idx = modelEntity?.jointNames.firstIndex(where: { $0.hasSuffix("/neck") }) {
neck = entity
}
if let entity = entities[headName],
let idx = modelEntity?.jointNames.firstIndex(where: { $0.hasSuffix("/head") }) {
head = entity
headTransformIdx = idx
}
if let entity = entities[eyeLName],
let idx = modelEntity?.jointNames.firstIndex(where: { $0.hasSuffix("/eye_left") }) {
lEye = entity
lEyeTransformIdx = idx
}
if let entity = entities[eyeRName],
let idx = modelEntity?.jointNames.firstIndex(where: { $0.hasSuffix("/eye_right") }) {
rEye = entity
rEyeTransformIdx = idx
}
}
func stopCameraTrackingTimer() {
trackingTimer?.invalidate()
}
private func updateModelOrientation(arView: ARView) {
if let modelEntity = modelEntity,
let neck = neck,
let head = head {
let position = head.position(relativeTo: neck)
head.look(
at: arView.cameraTransform.translation,
from: position,
upVector: [0, 1, 0],
relativeTo: neck
)
modelEntity.jointTransforms[headTransformIdx] = head.transform
}
}
func buildGraphAsync(jointNames: [String], jointTransforms: [Transform]) async -> Entity? {
let graphRoot: Entity? = await withTaskGroup(of: Entity?.self) { group in
group.addTask { [weak self] in
return self?.buildGraph(jointNames: jointNames, jointTransforms: jointTransforms)
}
return await group.first(where: { $0 != nil }) ?? nil
}
skeletoneIsBuilded = true
prepareEntities()
return graphRoot
}
private func buildGraph(jointNames: [String], jointTransforms: [Transform]) -> Entity? {
guard jointNames.count == jointTransforms.count else {
print("Error: the number of names and transformations does not match.")
return nil
}
var idx = 0
let root = Entity.create(name: jointNames[idx], transform: jointTransforms[idx])
entities = [jointNames[idx]: root]
idx += 1
while idx < jointNames.count {
let name = jointNames[idx]
let transform = jointTransforms[idx]
let parentPathComponents = name.split(separator: "/").dropLast()
let parentName = parentPathComponents.joined(separator: "/")
let entity = Entity.create(name: name, transform: transform)
entities[name] = entity
if let parent = entities[parentName] {
parent.addChild(entity)
} else {
print("Parent not fount for: \(name)")
}
idx += 1
}
return root
}
}
extension Entity {
static func create(name: String, transform: Transform) -> Entity {
let entity = Entity()
entity.name = name
entity.transform = transform
return entity
}
}
struct ARViewContainer: UIViewRepresentable {
func makeUIView(context: Context) -> ARView {
// ...
context.coordinator.startCameraTrackingTimer(arView: arView)
return arView
}
private func configureModel(context: Context, arView: ARView, modelEntity: ModelEntity) {
// ...
Task.detached {
let entity = await context.coordinator.buildGraphAsync(
jointNames: modelEntity.jointNames,
jointTransforms: modelEntity.jointTransforms
)
await MainActor.run {
context.coordinator.entity = entity
}
}
}
}
- изменил значения в upVector
- установил объект шеи и nil как относительный
- если правильно выбрать вектор, то голова вращается, но только горизонтально. Движения по вертикальной оси нет.
- Я заметил, что голова модели резко меняет направление в противоположную сторону еще до того, как камера окажется на другой стороне. То есть создается впечатление, будто точка, от которой рассчитывается направление на камеру, находится заметно ближе к наблюдателю, чем сам объект. то есть эта точка находится между объектом и камерой. Возможно надо как-то правильно настроить привязку...
Заранее спасибо за любую помощь)
Подробнее здесь: https://stackoverflow.com/questions/791 ... the-camera
Мобильная версия