Que fait PropMotion ?
Faites performer un objet 3D dans votre application SwiftUI. Patterns de production pour une tâche spécifique et courante : un acteur produit sur une scène SceneKit transparente – entrées, sorties, lancers, ombres, retours haptiques et les pièges silencieux qui coûtent des jours.
Quand utiliser SceneKit
Soyez honnête sur la position du framework :
- SceneKit est en mode maintenance. Pour les nouvelles applications avec des besoins 3D lourds (pipelines de ressources, USD/USDZ, AR, grands mondes), préférez RealityKit.
- SceneKit reste le chemin le plus rapide vers un acteur 3D décoratif dans une application SwiftUI : une
SCNViewtransparente se compose par-dessus n'importe quelle mise en page SwiftUI, les modificateurs de shader de géométrie sont une simple chaîne MSL, l'interopérabilité avec CoreAnimation est mature, et tout ceci fonctionne sur des vues UIKit simples sans configuration de session. - Si l'élément 3D est un objet héros unique avec un mouvement chorégraphié, les recettes de ce skill s'appliquent directement. S'il s'agit d'un monde interactif complet, arrêtez-vous et considérez d'abord RealityKit.
La recette de base de la scène
Une scène est : une SCNView transparente, un coordinateur @MainActor qui possède le graphe de scène, une caméra à hauteur d'œil debout, un rig d'éclairage à trois lumières plus une lumière d'ombre dédiée, un blob de contact et un attrape-ombre. L'acteur performe via des animations d'images clés pré-calculées.
struct ProductStage: UIViewRepresentable {
let item: Item
// La clé doit changer UNIQUEMENT lorsque la scène doit changer visiblement.
private var stateKey: String { "\(item.id)" }
func makeCoordinator() -> Coordinator { Coordinator() }
func makeUIView(context: Context) -> SCNView {
let view = SCNView()
view.backgroundColor = .clear // la scène se compose par-dessus SwiftUI
view.isOpaque = false
view.antialiasingMode = .multisampling4X
view.scene = context.coordinator.buildScene()
view.pointOfView = context.coordinator.cameraNode
context.coordinator.install(item)
context.coordinator.markState(stateKey)
// Échauffement de la première image, allumage à deux clés : compiler les shaders en
// dehors du chemin critique, garer l'acteur hors scène, et verrouiller la première
// entrée sur UN délai minimum ET la fin de la préparation.
context.coordinator.parkOffstage()
if let scene = view.scene {
view.prepare([scene]) { _ in
DispatchQueue.main.async { context.coordinator.markPipelinesWarm() }
}
}
context.coordinator.scheduleFirstEntrance()
return view
}
func updateUIView(_ view: SCNView, context: Context) {
// Différence de clé d'état : les re-rendus SwiftUI ne doivent jamais rejouer les entrées.
guard context.coordinator.stateKey != stateKey else { return }
context.coordinator.markState(stateKey)
context.coordinator.transition(to: item)
}
}
Les pièces, dans l'ordre de construction :
- Indicateurs de vue transparente (
backgroundColor = .clear,isOpaque = false), MSAA 4x. L'écran hôte fournit l'arrière-plan ; la scène n'a pas de boîte. - Le coordinateur possède la hiérarchie de nœuds, décomposée en un nœud par préoccupation de mouvement (déplacement, levage, lacet, rotation), de sorte que les animations indépendantes ne se disputent jamais une seule transformation.
- Caméra :
projectionDirection = .horizontalpour que la taille de l'acteur suive la largeur de la scène ; position surélevée avec une légère inclinaison vers le bas, ce qui donne l'impression d'un observateur debout. Gardez-la statique ; une caméra en mouvement donne un aspect synthétique. - Lumières : clé chaude, remplissage froid faible, contour dur, ambiante uniquement comme valeur de base, plus une lumière directionnelle séparée projetant des ombres, dirigée de derrière-dessus l'acteur pour ne jamais perturber la sculpture visible.
- Contact au sol : un plan de dégradé doux et sombre (le blob de contact) sous l'acteur plus un plan attrape-ombre invisible
.shadowOnlypour l'ombre réelle. - Les matériaux réfléchissants échantillonnent une petite carte d'environnement programmatique, pas une photo. Gardez la zone derrière la caméra sombre.
- Toute la chorégraphie est
CAKeyframeAnimationpré-calculée, protégée par des jetons de génération afin que toute nouvelle séquence remplace le travail en attente.
Détails complets avec code : references/stage-recipe.md
Les pièges qui coûtent le plus de temps
| Piège | Correction |
|---|---|
| Les ombres différées ne se rendent jamais lorsque MSAA est activé, sans aucune erreur dans la console | Utilisez shadowMode = .forward plus un attrape .shadowOnly. Déboguez toute ombre manquante en donnant d'abord à la scène un sol lambert gris visible. |
La première image d'une SCNView fraîche compile les pipelines Metal au milieu de votre animation d'entrée | prepare([scene]) en arrière-plan, garez l'acteur hors scène, retardez la première entrée. Ne démarrez jamais une animation sur la première image. |
fillMode = .forwards + isRemovedOnCompletion = false épingle la présentation, et la suppression est par objet | Un clearAnimations central qui balaie le nœud, chaque géométrie enfant, les lumières et les actions en cours, appelé depuis chaque point d'entrée. |
| Le métal réel vu de face rend comme un trou gris ou noir | Un miroir vu de face reflète la zone d'environnement derrière la caméra. Conservez la base peinte approuvée et ajoutez une fine couche additive avec sa propre carte de réflexion. |
Basculer castsShadow fait apparaître une pénombre floue en une image ; shadowBias et le categoryBitMask de la lumière sont ignorés pour les ombres directionnelles avant | Gardez castsShadow en permanence et animez l'alpha de shadowColor, synchronisé avec le mouvement. |
| Un délai d'échauffement fixe provoque encore des saccades sur les appareils froids et le simulateur ; la première image d'un effet de particules compile son propre pipeline et chute exactement au moment où il se déclenche | Allumage à deux clés : verrouillez l'entrée sur le délai minimum ET le gestionnaire d'achèvement de prepare. Échauffez les pipelines de particules avec une explosion d'opacité zéro correspondant aux indicateurs de l'effet réel. |
Le format par défaut de UIGraphicsImageRenderer hérite de l'échelle de l'écran, gonflant chaque texture générée 9 fois en pixels – mortel pour les textures rendues en direct (gravure par frappe) | Épinglez l'échelle du format du renderer à 1 et dimensionnez le canevas à la projection à l'écran de l'acteur ; coalescez les reciblages multi-entrées en un rendu par passage de mise à jour. |
| Reproduire le mouvement d'un objet réel à partir de photos donne des rigs confiants qui échouent de côté – chaque correction en révèle une nouvelle de fausse | Les photos fixes portent des poses, pas des chemins ou des mécanismes ; les adaptations de pose finale ne déterminent pas la trajectoire. Modélisez le chemin : un rig de calibration avec des contrôles de pose directs, l'auteur créant des images clés par rapport à l'objet physique. |
| Un mouvement animé par images clés hoquette rythmiquement à ses images clés et résiste à toutes les corrections de rendu et de timing | Les secousses sont cuites dans la courbe : la base Catmull-Rom uniforme sur des images clés inégalement espacées saute la vitesse à chaque nœud. Interpolez avec des tangentes Hermite pondérées par la durée (ou un cubique naturel) sur des phases basées sur la distance, et verrouillez avec une vérification de continuité numérique. |
| Un sous-maillage découpé d'un modèle plus grand se mesure comme s'il s'agissait de l'objet entier, sans aucune erreur nulle part | La découpe n'a réduit que le tampon d'index ; le tampon de sommets contient toujours tous les sommets de l'original. Mesurez uniquement les sommets référencés par les indices du sous-maillage. |
Une image carrée assignée à scene.lightingEnvironment est silencieusement ignorée – zéro réflexions, tous les matériaux miroirs rendent noir, aucune sortie console | Peignez la carte d'environnement dans une disposition de cube-map reconnue, le plus simple un canevas sphérique 2:1 (1024x512). Seul material.reflective accepte une sphère map carrée. |
Carte de référence
- references/stage-recipe.md : la scène complète, configuration de la vue, coordinateur, hiérarchie de nœuds, caméra, rig d'éclairage, blob de contact et attrape, cartes d'environnement programmatiques et textures, particules d'impact sur scène, modificateurs de shader sur les acteurs de scène (le piège de l'uniforme d'espace linéaire), maintien de l'identité SwiftUI de la scène (pièges de remontage et de tempête de mise à jour).
- references/first-frame-and-warmup.md : le hoquet de compilation du pipeline Metal au premier dessin et les deux remèdes, échauffement avec entrée retardée (amélioré en allumage à deux clés verrouillée sur l'achèvement de prepare), ou maintien de la scène montée à chaud et reciblage ; échauffement du pipeline de particules ; prouver le remède avec des balises et des traces de hoquet sur l'appareil.
- references/shadows-and-lights.md : tous les pièges silencieux des ombres, la combinaison fiable forward + shadowOnly, l'animation de la visibilité des ombres, le budget de lumière neutre, le métal vu de face.
- references/baked-animation.md : pourquoi les images clés pré-calculées battent les temporisateurs, la feuille de repère pour concevoir des séquences multi-phases avant de les cuire, la classification des coutures (C1 vs impulsions de contact), la conception du poids, l'échantillonnage dense, la comptabilité de nettoyage, les jetons de génération, le vol d'un nœud en pleine animation, le roulement sans glissement, le roulement le long de chemins au sol (direction, mise en scène dans l'espace écran, traces de débogage), les canaux au-delà des transformations (poids de morphing, valeurs d'objectif, uniforms de shader), les ressorts comme matériau d'écriture, une propriété un propriétaire.
- references/motion-from-reference.md : reproduire le mouvement d'un objet réel – ce que les photos et vidéos peuvent et ne peuvent pas vous dire, modéliser le chemin au lieu du mécanisme, le rig de calibration (le propriétaire pose l'acteur et enregistre les images clés), paramétrage par distance et interpolation pondérée par la durée des poses enregistrées manuellement, le piège Catmull-Rom uniforme, les portes de continuité numérique, les boucles d'état cosinusoïdales sans couture avec sorties de la phase en cours, mesure des sous-maillages réduits.
- references/physics-without-engine.md : l'intégrateur à pas fixe artisanal cuit en images clés, les murs et le sol comme nombres simples, les retours haptiques pilotés par contact, un basculement à pivot de bord, et pourquoi cela bat SCNPhysics pour les scènes chorégraphiées.
- references/gestures-and-haptics.md : glisser pour saisir sans test de hit, pinces douces en cours de maintien, vélocité de relâchement, retours haptiques d'impact, séquences scriptées survivant aux doigts en direct (tentatives limitées, sondes avec jeton, gardes d'acteur caché, le vol ferme le contrat de maintien), Réduire le mouvement comme taxonomie, accès VoiceOver à une scène invisible, coexistence avec les gestes SwiftUI.
- references/sequencing-stages.md : diriger plusieurs scènes comme un seul film – une horloge maîtresse avec des séquences comme données, les pièges de dérive du temps réel, les coupures sur le mouvement après que la sortie est terminée, le pré-montage de la scène suivante pour l'échauffement, une scène survivant à sa propre coupure (fumée persistante), le rebasage de la chronologie sur une interaction utilisateur, un lead d'enregistrement, la vérification des coupures image par image.
- references/modeling-actors.md : construction d'objets héros crédibles à partir de primitives – les ratios du monde réel avant l'estimation, les anneaux pour les faces encastrées, les silhouettes tube-plus-tore, la lisibilité des motifs radiaux, l'inclinaison par instance pour les faces concaves, les espaces ouverts, les caractéristiques de relief comme géométrie (jamais de peinture), l'albedo de métal satiné sur les scènes sombres, le verrouillage des acteurs qui arrivent sous forme de fichiers.
- references/camera-choreography.md : la caméra comme interprète – le rig orbital, les vols de révélation pré-calculés, la mise au point suivant le chariot, l'orbite par glissement avec inertie et retour à la maison, les réflexions SCNFloor comme ancrage, la mise en scène mate sous lumières descendantes, les contraintes pour les rigs de suivi (et pourquoi elles ne vont jamais sur l'acteur).
- references/instancing-and-swarms.md : des dizaines d'acteurs à la fois – une géométrie pour l'essaim, des piles basées sur des emplacements au lieu de la physique, des paraboles résolues à l'envers depuis le point d'atterrissage, la culbute fusionnée en pose de repos, l'aléatoire ensemencé, l'échelonnement par planification, notes de budget.
- references/multi-actor-physics.md : plusieurs acteurs dans une seule simulation pré-calculée – N vecteurs d'état sur une horloge, collisions par paires (séparer, puis échanger lorsqu'ils s'approchent), la saisie qui fige le monde, le test de hit pour savoir quel acteur le doigt a sélectionné, listes de contact par acteur pour l'écrasement et les retours haptiques.
Liste de vérification de révision
Avant de livrer une scène, vérifiez :
-
backgroundColor = .clearetisOpaque = falsesur la SCNView ; la scène se compose sans boîte ni couture visibles. - Aucune animation ne démarre sur la première image de la vue ;
prepares'exécute et la première entrée est retardée ou la scène est maintenue montée à chaud. - Les ombres utilisent le mode avant avec un attrape
.shadowOnly; rien ne repose sur les ombres différées,shadowBiasou le verrouillagecategoryBitMaskde la lumière. - Les plans avec texture alpha (étiquettes, décalcomanies) ont
castsShadow = false, sinon ils projettent des ombres carrées. - Les changements de visibilité des ombres atténuent l'alpha de
shadowColor;castsShadown'est jamais basculé à l'exécution. - Si la scène doit rendre une texture avec une fidélité des couleurs, l'ambiante plus la directionnelle multipliée par cos(incidence) s'additionne à neutre (1000) pour une surface plane ; aucun gris clair écrété.
- Toute image sur
lightingEnvironment(oubackground) utilise une disposition de cube-map reconnue – sphérique 2:1, bande 6:1 ou 1:6, ou six images ; une image carrée y est silencieusement ignorée et les matériaux réfléchissants rendent noir. - Chaque continuation asynchrone (entrées retardées, retours haptiques planifiés, transitions chaînées) capture un jeton de génération et le revérifie avant d'agir.
- Tout code qui vole un nœud en animation capture les valeurs
.presentationdans le modèle avant d'appelerremoveAllAnimations. - Les animations pré-calculées définissent le modèle à la pose finale avant
addAnimation; rien ne dépend defillMode = .forwardssans un balayage de nettoyage. - Les séquences multi-phases ont un commentaire de feuille de repère au-dessus de la fonction de pose correspondant à la structure de limite de phase ; les temps de phase et de repère vivent dans cette seule structure, jamais comme des littéraux dans la boucle d'échantillonnage ou dans les retours haptiques planifiés. Chaque couture est classée : les coutures de conception sont C1, les coutures d'impulsion se produisent uniquement aux événements de contact et chacune atterrit sur un repère.
- Les objets roulants asservissent la rotation à la distance divisée par le rayon ; rien ne glisse.
- Le mouvement copié d'un objet réel est créé sous forme d'images clés via un rig de calibration (contrôles de pose directs, poses enregistrées, l'œil du propriétaire),
…
[tronqué pour la traduction]