iOS Human Interface Guidelines の Augmented Reality (ARKit) をざっくり訳す
以下、ざっくり訳。間違っていたらごめんなさい。
あと、軟弱なので訳せなかったところは飛ばしている。
一応、ベータなので内容は変更される可能性あり。
Augmented Reality - Technologies - iOS Human Interface Guidelines
注記
- phone と原文で書かれているところは iPhone(iPad) にしている
- () の部分は自分が追記したもの。ポエム。
Augmented Reality (AR)
ARKit を使用して仮想オブジェクトを現実の世界にシームレスに融合させる臨場感あふれる魅力的な体験を提供する。
AR アプリでは、カメラを使用して現実の世界を表示し、3次元の仮想オブジェクトが重ね合わされ、 実際に存在するという錯覚を作り出す。
ユーザーは、さまざまな角度からオブジェクトを鑑賞するためデバイスの方向を変えることができ、 ジェスチャーや動きを使ってオブジェクトを操作などインタラクションを起こすことができる。
エクスペリエンスの設計
ディスプレイ全体を使用する。
現実世界でアプリの仮想オブジェクト設置し、操作するためにできるだけ多くの画面を使用する。
没入体験を減らす操作系の UI や情報で画面が乱雑にならないようする。
現実的なオブジェクトを配置するときには頑張ってつくりこめ
すべての AR 体験でリアルなフォルムの仮想オブジェクトを必要とするわけではないが、使用する場合はその環境にあたかもあるような見栄えのオブジェクトを使うべき。
検出された現実世界のサーフェス上にオブジェクトを配置し、より良いテクスチャやジオメトリを作成し、適切なスケーリング、仮想オブジェクトに環境照明を反映させる。
ユーザーに不快感を与えないようにする
一定の距離や角度でデバイスを長時間保持させるとユーザーは疲労する可能性あり。 アプリを使用した際、デバイスをどのようにもって使用されるか考えて不快感を与えさせないように。
例えば、オブジェクトを遠くに配置することで、近くに移動する必要がなくなる。
アプリでユーザーが動くことを推奨する場合は、徐々に動きを促す。
最初にゲームなど経験に適応する時間を与えること。
その後、徐々に動きを促すべき。
ユーザーの安全に注意する
あまりにも長距離の移動をさせたり、近くに人や物が存在すると、危険な状態になる可能性がある。
なので、アプリを安全に運用する方法を検討する。
例えば、ゲームで大きな動きや突然の動きを避けるなど。
臨場感あふれる体験を高めるために、オーディオと触覚フィードバックを使用する
音響効果またはバイブレーション/触覚フィードバックは、仮想物体が現実世界表面、または他の仮想物体と接触したことを確認させることができる。
没入型のゲームでは、BGM はユーザーを仮想世界にのめり込ませるのに役立つ。
可能な限り、コンテキスト(前後関係, 流れ)でヒントを提供する
オブジェクトの周りに3次元回転インジケータを配置すると、オーバーレイで表示されたテキストベースの命令よりも直感的になる。
平面検出前やユーザーがコンテンツのヒントなどに応答していない場合など。
状況によっては、テキストオーバーレイヒントが必要。
何らかの指示をするテキストを表示する場合は、わかりやすい用語を使用せよ
ユーザー全員がテクノロジーに詳しいわけではないので、
アプローチが容易になるように、ARKit、ワールドディテクション、トラッキングなどの開発者用語を使用しないようにする。
代わりに、ほとんどの人が理解できるフレンドリーな会話言葉を使用せよ。 (トラッキングとか大丈夫そうだけどドキュメントには書いてある)
○ | × |
---|---|
平面を見つけることができません。 側面に移動するか、iPhone(iPad)の位置を変えてみてください。 | 平面を見つけることができません。トラッキングを調整します。 |
場所をタップして@配置するオブジェクトの名前」を配置します。 | 平面をタップしてオブジェクトを固定します。 |
多くの照明を点けて動してみてください。 | 機能が不十分です。 |
iPhone(iPad)をゆっくり動かしてみてください。 | 過度の動きが検出されました。 |
AR 体験への不必要な中断を避ける
現実環境を分析後、ユーザーが AR アプリを終了して AR アプリに再び入るたびに平面が検出される。
デバイスと仮想空間のカメラの位置が変更されている可能性があり、以前に配置されたオブジェクトは再配置される可能性が高くなる。
AR を非表示にしオブジェクト設定の変更をできるようにすることも解決策の一つ。
(状況によってはフルリセットも必要かも?)
拡張現実に入り込む
初期設定が行われている状態を示し、ユーザーに伝える。
現実環境を分析する初期化プロセスは、アプリが AR に動作する度に発生し数秒かかる。
起こりうる混乱を減らし、プロセスをスピードアップするために、初期化中の状況を示し、周囲を探索させたり、積極的に平面を探すよう促す。
仮想オブジェクトの配置
平面にインターフェースを配置して、オブジェクトを配置できるタイミングを理解させる
ビジュアルインジケータは、サーフェスターゲティングモードがアクティブであることを伝えるうえで最適な方法で 人が水平で平らな面をだと推測するのに役立つ。
平面がターゲットに設定されると、インジケータの外観が変更され、オブジェクトの配置が可能になることが見た目でわかるようになる。
アプリに合わせたビジュアルインジケータをデザインしても良い。
ユーザーがオブジェクトを配置するときに適切に応答させる
平面検出の間、(非常に短時間で)精度が向上する。
ユーザーが画面をタップしてオブジェクトを配置する場合は、現在使用可能な情報を使用して配置し、位置の設定、サーフェスの範囲を超えて配置されてしまった場合は戻す。
検出されたサーフェスのエッジにオブジェクトを正確置こうとしない
AR では、平面境界は環境分析毎に変化する可能性のあるため。
仮想オブジェクトとのユーザインタラクション
画面上のコントロールは直接操作を優先させる
画面上のオブジェクトなど、別のコントロールする UI を使って操作するのではなく、直接オブジェクトを触り操作できれば、より没入感があり直感的。
ただし、ユーザーが動き回っているときなど、直接の操作が混乱を招いたり操作困難になることがあるので注意。
標準的で使い慣れたジェスチャーを使用して、仮想オブジェクトと直接操作する
たとえば、オブジェクトを移動するための1本指でのドラッグジェスチャ、回転するオブジェクトの2本指で操作するジェスチャをサポートするなどを検討する。
詳しい内容は ジェスチャ(英語) を参照。
インタラクションを簡素に保つ。
タッチジェスチャーの処理は 2 次元であるが、AR の現実世界は 3 次元。
仮想オブジェクトとのユーザーのやり取りを簡素化するために、以下のアプローチを検討する。
- オブジェクトが置かれている2次元表面への移動を制限する
- オブジェクトの回転を単一の軸に制限する
操作可能な仮想オブジェクトの適切な位置でジェスチャーに応答させる
小さい、薄い、または遠くに置かれたオブジェクトの特定のポイントに人の指が正確に触れることは難しい。
アプリでインタラクションを起こすオブジェクトの近くでジェスチャを検出する場合は、ユーザーがオブジェクトに影響を与えることを想定することを勧める。
ユーザーが設置するオブジェクトの拡大縮小(スケーリング)が必要かどうかを検討する
通常、スケーリングはおもちゃやゲームのキャラクターなど、オブジェクトに固有のサイズがなく、ユーザーがそれを大きくしたり小さくしたりする場合に適している。
実際の世界に比べて大きさが決まっているサイズのオブジェクトの場合、家具のようなアイテムが正確なサイズに配置されている場合、スケーリングはさせない。
競合するジェスチャーに注意する
2本指のピンチジェスチャーは、2本指の回転ジェスチャーと非常によく似ている。
もし、2つのジェスチャーを実装する場合は、アプリをテストして、正しく操作するできることを確認しておく。
仮想オブジェクトの動きがスムーズであることを確認する
オブジェクトのサイズを変更したり、オブジェクトを回転させたり、新しい場所に移動したりするときに、オブジェクトが突然消えて現れたりなど、スムーズに表示されるよう注意する。
より魅力的なインタラクションの方法を探求する。
ジェスチャーが AR 内の仮想オブジェクトとインタラクションを起こす唯一の方法ではない。
モーション(ジャイロ、コンパス、加速度センサー)や近接(近接センサー)など、他の要素を使用してコンテンツを息吹を吹き込むことができる。
例えば、ユーザーが歩いているときに、ゲームキャラクターがユーザーを見るように頭を動かすなど。
エラーなど問題が起こった時の処理
使用できる環境の条件を満たしていない場合、ユーザー側からリセットできるようにする
オブジェクト配置の改善や条件の改善を待つように強制しない。
もう一度やり直してより良い結果が得られるかどうかを確認する方法を教える。
(多分、光が足りないとうことを伝えたいのだと思われる)
問題が発生した場合には可能な修正を提案する
ユーザーの環境と平面の検出の分析は、さまざまな理由で失敗することがある。
十分な光がない、平面が反射する、平面が充分なディテールを持っていない、カメラが大きく動くなど。
アプリ得る詳細情報が不十分か、動きが多すぎるか、平面の検出に時間がかかりすぎる場合は、問題を解決するための提案を提示する。
問題 | 考えられる提案 |
---|---|
不十分な機能が検出されました | 多くの照明を点けて移動してみてください |
過度の動きが検出されました | iPhone(iPad)を遅く動かしてみてください。 |
表面の検出に時間がかかりすぎる | 移動して、より多くのライトをオンにして、iPhone(iPad)が模様など質感のある平面をカメラがとらえている事を確認してください。 |
可能なデバイスでのみ AR 機能を提供する
アプリの主な使用目的が AR の場合は、ARKit をサポートする端末での使用できるようにする。
製品写真を含む家具カタログのようなアプリで、AR を第2の機能として提供している場合、サポートされていないデバイスで AR 使用時は、エラーを表示しないようにする。
(AR の機能を塞ぐか、違う方法で似たような体験をさせるということだと思われる)
また、デバイスが ARKit をサポートしていない場合は、最初の画面にオプションである AR 機能を提示しないこと。
開発については、「 Information Property List Key Reference 」の UIRequiredDeviceCapabilities セクション の ARKit 用のキーと、ARConfiguration の isSupported プロパティを参照。
もっと詳しく知る
開発者向けのガイダンスについては ARKit を参照。
iOS 11 / Xcode 9 の SceneKit プロパティ、メソッド等の追加・変更・廃止一覧
自分のメモ用。
Beta なので変更される可能性あり。
Type Color
- 変更 / Modified
- 追加 / Added
- 廃止 / Deprecated
変更内容
- SCNScene
- SCNScene.Attribute
- init(rawValue: String)
- SCNScene.Attribute
- SCNView
- SCNView.Option
- init(rawValue: String)
- cameraControlConfiguration:SCNCameraControlConfiguration
- defaultCameraController: SCNCameraController
- rendersContinuously: Bool
- SCNView.Option
- SCNNode
- SCNBoundingVolume
- boundingBox: (min: SCNVector3, max: SCNVector3)
- boundingSphere: (center: SCNVector3, radius: Float)
- entity: GKEntity?
- focusBehavior: SCNNodeFocusBehavior
- simdEulerAngles: simd_float3
- simdOrientation: simd_quatf
- simdPivot: simd_float4x4
- simdPosition: simd_float3
- simdRotation: simd_float4
- simdScale: simd_float3
- simdTransform: simd_float4x4
- simdWorldFront: simd_float3
- simdWorldOrientation: simd_quatf
- simdWorldPosition: simd_float3
- simdWorldRight: simd_float3
- simdWorldTransform: simd_float4x4
- simdWorldUp: simd_float3
- worldFront: SCNVector3
- worldOrientation: SCNQuaternion
- worldPosition: SCNVector3
- worldRight: SCNVector3
- worldUp: SCNVector3
- localFront: SCNVector3
- localRight: SCNVector3
- localUp: SCNVector3
- simdLocalFront: simd_float3
- simdLocalRight: simd_float3
- simdLocalUp: simd_float3
- convertVector(SCNVector3, from: SCNNode?)
- convertVector(SCNVector3, to: SCNNode?)
- localRotate(by: SCNQuaternion)
- localTranslate(by: SCNVector3)
- look(at: SCNVector3)
- look(at: SCNVector3, up: SCNVector3, localFront: SCNVector3)
- rotate(by: SCNQuaternion, aroundTarget: SCNVector3)
- setWorldTransform(SCNMatrix4)
- simdConvertPosition(simd_float3, from: SCNNode?)
- simdConvertPosition(simd_float3, to: SCNNode?)
- simdConvertTransform(simd_float4x4, from: SCNNode?)
- simdConvertTransform(simd_float4x4, to: SCNNode?)
- simdConvertVector(simd_float3, from: SCNNode?)
- simdConvertVector(simd_float3, to: SCNNode?)
- simdLocalRotate(by: simd_quatf)
- simdLocalTranslate(by: simd_float3)
- simdLook(at: vector_float3)
- simdLook(at: vector_float3, up: vector_float3, localFront: simd_float3)
- simdRotate(by: simd_quatf, aroundTarget: simd_float3)
- SCNBoundingVolume
- Basic 3D Data Types
- SCNVector3
- init()
- init(float3)
- init(double3)
- init(Int, Int, Int)
- init(Float, Float, Float)
- init(Double, Double, Double)
- init(CGFloat, CGFloat, CGFloat)
- init(x: CGFloat, y: CGFloat, z: CGFloat)
- init(x: Float, y: Float, z: Float)
- SCNVector4
- init()
- init(float4)
- init(double4)
- init(Double, Double, Double, Double)
- init(CGFloat, CGFloat, CGFloat, CGFloat)
- init(Float, Float, Float, Float)
- init(Int, Int, Int, Int)
- init(x: CGFloat, y: CGFloat, z: CGFloat, w: CGFloat)
- init(x: Float, y: Float, z: Float, w: Float)
- SCNMatrix4
- init()
- init(double4x4)
- init(double4x4)
- init(float4x4)
- init(float4x4)
- init(m11: Float, m12: Float, m13: Float, m14: Float, m21: Float, m22: Float, m23: Float, m24: Float, m31: Float, m32: Float, m33: Float, m34: Float, m41: Float, m42: Float, m43: Float, m44: Float)
- SCNFloat
- SCNVector3
- SCNSceneRenderer
- SCNHitTestOption
- searchMode: SCNHitTestOption
- SCNHitTestSearchMode
- all
- any
- closest
- SCNDebugOptions
- init(rawValue: String)
- renderAsWireframe: SCNDebugOptions
- showCameras: SCNDebugOptions
- showConstraints: SCNDebugOptions
- showCreases: SCNDebugOptions
- showSkeletons: SCNDebugOptions
- SCNHitTestOption
- SCNSceneRendererDelegate
- renderer(SCNSceneRenderer, didApplyConstraintsAtTime: TimeInterval)
- SCNRenderer
- render(withViewport: CGRect, commandBuffer: MTLCommandBuffer, passDescriptor: MTLRenderPassDescriptor)
- update(atTime: CFTimeInterval)
- SCNHitTestResult
- boneNode: SCNNode
- SCNCamera
- yFov: Double
- xFov: Double
- projectionDirection: SCNCameraProjectionDirection
- SCNCameraProjectionDirection
- horizontal
- vertical
- focalDistance: CGFloat
- focalSize: CGFloat
- focalBlurRadius: CGFloat
- aperture: CGFloat
- apertureBladeCount: Int
- fStop: CGFloat
- fieldOfView: CGFloat
- focalBlurSampleCount: Int
- focalLength: CGFloat
- focusDistance: CGFloat
- screenSpaceAmbientOcclusionBias: CGFloat
- screenSpaceAmbientOcclusionDepthThreshold: CGFloat
- screenSpaceAmbientOcclusionIntensity: CGFloat
- screenSpaceAmbientOcclusionNormalThreshold: CGFloat
- screenSpaceAmbientOcclusionRadius: CGFloat
- sensorHeight: CGFloat
- wantsDepthOfField: Bool
- SCNCameraController
- SCNInteractionMode
- automaticTarget: Bool
- delegate: SCNCameraControllerDelegate?
- inertiaEnabled: Bool
- inertiaFriction: Float
- interactionMode: SCNInteractionMode
- isInertiaRunning: Bool
- maximumHorizontalAngle: Float
- maximumVerticalAngle: Float
- minimumHorizontalAngle: Float
- minimumVerticalAngle: Float
- pointOfView: SCNNode?
- target: SCNVector3
- worldUp: SCNVector3
- beginInteraction(CGPoint, withViewport: CGSize)
- clearRoll()
- continueInteraction(CGPoint, withViewport: CGSize, sensitivity: CGFloat)
- dolly(by: Float, onScreenPoint: CGPoint, viewport: CGSize)
- dolly(toTarget: Float)
- endInteraction(CGPoint, withViewport: CGSize, velocity: CGPoint)
- frameNodes([SCNNode])
- roll(by: Float, aroundScreenPoint: CGPoint, viewport: CGSize)
- rollAroundTarget(Float)
- rotateBy(x: Float, y: Float)
- stopInertia()
- translateInCameraSpaceBy(x: Float, y: Float, z: Float)
SCNCameraControlConfiguration
- allowsTranslation: Bool
- autoSwitchToFreeCamera: Bool
- flyModeVelocity: CGFloat
- panSensitivity: CGFloat
- rotationSensitivity: CGFloat
- truckSensitivity: CGFloat
SCNCameraControllerDelegate
- cameraInertiaDidEnd(for: SCNCameraController)
- cameraInertiaWillStart(for: SCNCameraController)
- SCNLight
- SCNLight.LightType
- init(rawValue: String)
- automaticallyAdjustsShadowProjection: Bool
- forcesBackFaceCasters: Bool
- maximumShadowDistance: CGFloat
- sampleDistributedShadowMaps: Bool
- shadowCascadeCount: Int
- shadowCascadeSplittingFactor: CGFloat
- sphericalHarmonicsCoefficients: Data
- SCNLight.LightType
- SCNMaterial
- SCNMaterial.LightingModel
- init(rawValue: String)
- displacement
- SCNTransparencyMode
- `default`
- dualLayer
- singleLayer
- SCNBlendMode
- max
- colorBufferWriteMask: SCNColorMask
- SCNColorMask
- fillMode: SCNFillMode
- SCNFillMode
- SCNMaterial.LightingModel
- SCNMaterialProperty
- textureComponents: SCNColorMask
- SCNGeometry
- SCNBoundingVolume
- boundingBox: (min: SCNVector3, max: SCNVector3)
- boundingSphere: (center: SCNVector3, radius: Float)
- elements: [SCNGeometryElement]
- sources: [SCNGeometrySource]
- elementCount: Int
- element(at: Int)
- sources(for: SCNGeometrySource.Semantic)
- tessellator: SCNGeometryTessellator?
- SCNGeometryTessellator
- wantsAdaptiveSubdivision: Bool
- SCNBoundingVolume
- SCNGeometrySource
- init(vertices: [SCNVector3])
- init(normals: [SCNVector3])
- init(textureCoordinates: [CGPoint])
- SCNGeometrySource.Semantic
- init(String)
- init(rawValue: String)
- SCNAnimatable
- addAnimation(SCNAnimationProtocol, forKey: String?)
- func animation(forKey: String)
- func removeAnimation(forKey: String, fadeOutDuration: CGFloat)
- func pauseAnimation(forKey: String)
- func resumeAnimation(forKey: String)
- func isAnimationPaused(forKey: String)
- addAnimationPlayer(SCNAnimationPlayer, forKey: String?)
- animationPlayer(forKey: String)
- removeAnimation(forKey: String, blendOutDuration: CGFloat)
- func setAnimationSpeed(CGFloat, forKey: String)
- SCNAnimationEvent
- SCNAnimationEventBlock
- SCNAnimation
- SCNAnimationDidStartBlock
- SCNAnimationDidStopBlock
- init(caAnimation: CAAnimation)
- init(contentsOf: URL)
- init(named: String)
- animationDidStart: SCNAnimationDidStartBlock?
- animationDidStop: SCNAnimationDidStopBlock?
- animationEvents: [SCNAnimationEvent]?
- autoreverses: Bool
- blendInDuration: TimeInterval
- blendOutDuration: TimeInterval
- duration: TimeInterval
- fillsBackward: Bool
- fillsForward: Bool
- isAdditive: Bool
- isAppliedOnCompletion: Bool
- isCumulative: Bool
- isRemovedOnCompletion: Bool
- keyPath: String?
- repeatCount: CGFloat
- startDelay: TimeInterval
- timeOffset: TimeInterval
- timingFunction: SCNTimingFunction
- usesSceneTimeBase: Bool
- SCNAnimationPlayer
- init(animation: SCNAnimation)
- animation: SCNAnimation
- blendFactor: CGFloat
- paused: Bool
- speed: CGFloat
- play()
- stop()
- stop(withBlendOutDuration: TimeInterval)
- SCNTimingFunction
- init(caMediaTimingFunction: CAMediaTimingFunction)
- init(timingMode: SCNActionTimingMode)
- SCNAnimationProtocol
- NSObjectProtocol
- SCNConstraint
- isEnabled: Bool
- isIncremental: Bool
- SCNBillboardConstraint
- SCNBillboardAxis
- init(rawValue: UInt)
- SCNBillboardAxis
- SCNLookAtConstraint
- localFront: SCNVector3
- var targetOffset: SCNVector3
- var worldUp: SCNVector3
- SCNDistanceConstraint
- init(target: SCNNode?)
- maximumDistance: CGFloat
- minimumDistance: CGFloat
- target: SCNNode?
- SCNAvoidOccluderConstraint
- init(target: SCNNode?)
- bias: CGFloat
- occluderCategoryBitMask: Int
- target: SCNNode?
- SCNAvoidOccluderConstraintDelegate
- SCNAvoidOccluderConstraintDelegate
- SCNAccelerationConstraint
- damping: CGFloat
- decelerationDistance: CGFloat
- maximumLinearAcceleration: CGFloat
- maximumLinearVelocity: CGFloat
- SCNSliderConstraint
- collisionCategoryBitMask: Int
- offset: SCNVector3
- radius: CGFloat
- SCNReplicatorConstraint
- init(target: SCNNode?)
- orientationOffset: SCNQuaternion
- positionOffset: SCNVector3
- replicatesOrientation: Bool
- replicatesPosition: Bool
- replicatesScale: Bool
- scaleOffset: SCNVector3
- target: SCNNode?
- SCNTransformConstraint
- orientationConstraint(inWorldSpace: Bool, with: (SCNNode, SCNQuaternion) -> SCNQuaternion)
- positionConstraint(inWorldSpace: Bool, with: (SCNNode, SCNVector3) -> SCNVector3)
- SCNSkinner
- skeleton
- SCNMorpher
- unifiesNormals: Bool
- weights: [NSNumber]
- setWeight(CGFloat, forTargetNamed: String)
- weight(forTargetNamed: String)
- SCNPhysicsBody
- SCNPhysicsCollisionCategory
- init(rawValue: UInt)
- SCNPhysicsCollisionCategory
SCNPhysicsShape
- SCNPhysicsShape.Option
- init(rawValue: String)
- SCNPhysicsShape.ShapeType
- init(rawValue: String)
- SCNPhysicsShape.Option
SCNPhysicsContact
- sweepTestFraction
- SCNPhysicsWorld
- SCNPhysicsWorld.TestOption
- SCNPhysicsWorld.TestSearchMode
- init(rawValue: String)
- init(rawValue: String)
- SCNPhysicsWorld.TestSearchMode
- SCNPhysicsWorld.TestOption
- SCNPhysicsConeTwistJoint
- init(body: SCNPhysicsBody, frame: SCNMatrix4)
- init(bodyA: SCNPhysicsBody, frameA: SCNMatrix4, bodyB: SCNPhysicsBody, frameB: SCNMatrix4)
- bodyA: SCNPhysicsBody
- bodyB: SCNPhysicsBody?
- frameA: SCNMatrix4
- frameB: SCNMatrix4
- maximumAngularLimit1: CGFloat
- maximumAngularLimit2: CGFloat
- maximumTwistAngle: CGFloat
- SCNParticleSystem
- SCNParticleSystem.ParticleProperty
- init(rawValue: String)
- orientationDirection: SCNVector3
- particleIntensity: CGFloat
- particleIntensityVariation: CGFloat
- SCNParticleSystem.ParticleProperty
- SCNShadable
- SCNShaderModifierEntryPoint
- init(rawValue: String)
- SCNShaderModifierEntryPoint
- SCNSceneSource
- SCNSceneSource.LoadingOption
- SCNSceneSource.AnimationImportPolicy
- init(rawValue: String)
- convertToYUp
- convertUnitsToMeters
- init(rawValue: String)
- SCNSceneSource.AnimationImportPolicy
- entryWithIdentifier
(String, withClass: T.Type)
- SCNSceneSource.LoadingOption
- SCNNodeFocusBehavior
- focusable
- none
- occluding
- SCNTessellationSmoothingMode
- none
- phong
- pnTriangles
iOS で SceneKit を試す(Swift 3) その90 - ここで一区切り
Swift 3 (Xcode 8) の SceneKit の解説に関してはここで一区切り。
カスタムの Action、ParticleSystem、PhysicsField、 シーンを管理する SCNSceneRenderer、 scn ファイルを読み書きする SCNSceneSource、JS で SceneKit を設定する SCNExportJavaScriptModule
などなど、紹介していない機能もあるが、ある程度は紹介できたと思う。
とりあえず、技術書典3 で Swift Playground 2.0 for iPad + SceneKit で薄い厚い本を出す予定なので Xcode 9 で追加したものは、そちらでご紹介という形になるかもというところ。
iOS で SceneKit を試す(Swift 3) その89 - SCNNode をフラット化する。
もう1つ忘れていたものノードのフラット化について。
Scene Graph でチルドノードが増えすぎたりするとドローコールが増えるため、パフォーマンスが落ちる可能性があり、 SceneKit の機能でノードのジオメトリをひとまとめにする Flatten Node の処理を行うことで緩和できる可能性がある。
フラット化できるもの
基本、ジオメトリとマテリアルのみで他は無視されたはず。
Scene Editor での使用
今回、ピンク、青と青のクローン、緑と緑のクローンの球を設定。
Scene Graph View でチルドノードのあるノードを選択。
右クリックから Flatten Node を押すとひとまとめになる。
Attributes Inspector の Geometory Elements のエレメントが3つになっており、 最適化がされている。
コードでの使用
SCNNode に flattenedClone() メソッドがあり、実行するとフラット化された SCNNode が返る。
注意点
たまに中身がおかしくなる場合があるので、必ず確認しよう。
特にコードで行う場合。
今回はここまで。
iOS で SceneKit を試す(Swift 3) その88 - SceneKit で SpriteKit の SKVideoNode を使用して動画のテクスチャを適応してみる
書き忘れていたが、SceneKit では SpriteKit の SKScene シーンを SCNMaterialProperty の各 contents に渡すことができる。
やり方としては作成した SKScene をジオメトリの firstMaterial.diffuse.contents を渡すだけ。 2D描画処理のコストはかかるが、複雑なテクスチャを作成できる。
ちなみに、SpriteKit 側も SK3DNode でイニシャライズして scnScene に設定した SCNScene を放り込むと、SceneKit の表示を 2D のテクスチャにしてくれる。
今回やること
AVFoundation の AVPlayer で動画を読み込み、それを SpriteKit でシーンを作り SKVideoNode を作成。 SpriteKit でシーンを SceneKit のテクスチャに割り当てる。
コードを書いてみる
いつも通り、Xcode の Game テンプレートで SceneKit を選択し作成する。
動画をアップロードできないため、iOS で読み込める mov や m4v 動画ファイルをプロジェクトに自前で追加してほしい。
必要のないコードを変更する
GameViewController.swift を開き、宇宙船は今回使用しないので 19 行目を以下に変更。
let scene = SCNScene(named: "art.scnassets/ship.scn")!
let scene = SCNScene()
アニメーションも必要ないので 44 行目と 47 行目の以下のコードを削除
let ship = scene.rootNode.childNode(withName: "ship", recursively: true)! ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1)))
インポート部分の変更
今回、SpriteKit と動画再生用の AVFoundation を使用するため、 GameViewController.swift の「import SceneKit」の下に以下のコードを追加
import SpriteKit import AVFoundation
viewDidLoad を編集
以下のコードを追加。
流れとしては AVPlayerItem で動画ファイルを指定して、AVPlayer で先ほどの AVPlayerItem を設定し再生。 SKScene 作成後、SKVideoNode で作成した AVPlayer を設定し SKScene へ設定。 SCNBox の Diffuse の contents に設定した SKScene を適応するだけ。
let item = AVPlayerItem(url: URL(fileURLWithPath: Bundle.main.path(forResource: "ファイル名", ofType: "m4v")!)) let videoPlayer = AVPlayer(playerItem: item) videoPlayer.play() let skScene = SKScene() skScene.backgroundColor = UIColor.black skScene.size = CGSize(width: 1024, height: 1024) let skVideoNode = SKVideoNode(avPlayer: videoPlayer) skVideoNode.size = CGSize(width: 1024, height: 1024) skVideoNode.position = CGPoint(x: 512, y: 512) skScene.addChild(skVideoNode) let boxNode = SCNNode(geometry: SCNBox(width: 4, height: 4, length: 4, chamferRadius: 0)) boxNode.geometry?.firstMaterial?.diffuse.contents = skScene scene.rootNode.addChildNode(boxNode)
1回しか再生されないのでループしたい
このままの実装だと1回しか再生されないので、AVPlayer ループの処理をする。
終了時に何もしないようにして、AVPlayerItemDidPlayToEndTimeNotification で通知を飛ばす。 通知を受けたら再生位置を最初に戻し再生する。
AVPlayer の初期化の後に以下を追加。
videoPlayer.actionAtItemEnd = AVPlayerActionAtItemEnd.none; NotificationCenter.default.addObserver(self, selector: #selector(self.stateEnd), name: NSNotification.Name("AVPlayerItemDidPlayToEndTimeNotification"), object: videoPlayer.currentItem)
GameViewController 内のどこかに以下の関数を追加
func stateEnd(notification: NSNotification) { let avPlayerItem = notification.object as? AVPlayerItem avPlayerItem?.seek(to: kCMTimeZero) }
もっと簡単に書く
SKVideoNode は再生と一時停止ぐらいしか機能がないため、今回は AVPlayer を使用したが、 1回のみの再生で使用するのなら、AVPlayer の記述を消して init(fileNamed: String) や init(url: URL) で直接ファイルを参照できる。
パフォーマンスについて
それなりに 2D の描画処理が必要になる模様。
GameViewController.swift の全コード
import UIKit import QuartzCore import SceneKit import SpriteKit import AVFoundation class GameViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let scene = SCNScene() let cameraNode = SCNNode() cameraNode.camera = SCNCamera() cameraNode.position = SCNVector3(x: 0, y: 0, z: 15) scene.rootNode.addChildNode(cameraNode) let lightNode = SCNNode() lightNode.light = SCNLight() lightNode.light!.type = .omni lightNode.position = SCNVector3(x: 0, y: 10, z: 10) scene.rootNode.addChildNode(lightNode) let ambientLightNode = SCNNode() ambientLightNode.light = SCNLight() ambientLightNode.light!.type = .ambient ambientLightNode.light!.color = UIColor.darkGray scene.rootNode.addChildNode(ambientLightNode) // AVPlayer の設定 let item = AVPlayerItem(url: URL(fileURLWithPath: Bundle.main.path(forResource: "ファイル名", ofType: "m4v")!)) let videoPlayer = AVPlayer(playerItem: item) videoPlayer.actionAtItemEnd = AVPlayerActionAtItemEnd.none; NotificationCenter.default.addObserver(self, selector: #selector(self.stateEnd), name: NSNotification.Name("AVPlayerItemDidPlayToEndTimeNotification"), object: videoPlayer.currentItem) videoPlayer.play() // SKScene の設定 let skScene = SKScene() skScene.backgroundColor = UIColor.black skScene.size = CGSize(width: 1024, height: 1024) let skVideoNode = SKVideoNode(avPlayer: videoPlayer) skVideoNode.size = CGSize(width: 1024, height: 1024) skVideoNode.position = CGPoint(x: 512, y: 512) skScene.addChild(skVideoNode) // SCNBox の設定 let boxNode = SCNNode(geometry: SCNBox(width: 4, height: 4, length: 4, chamferRadius: 0)) boxNode.geometry?.firstMaterial?.diffuse.contents = skScene scene.rootNode.addChildNode(boxNode) let scnView = self.view as! SCNView scnView.scene = scene scnView.allowsCameraControl = true scnView.showsStatistics = true scnView.backgroundColor = UIColor.black } func stateEnd(notification: NSNotification) { let avPlayerItem = notification.object as? AVPlayerItem avPlayerItem?.seek(to: kCMTimeZero) } override var shouldAutorotate: Bool { return true } override var prefersStatusBarHidden: Bool { return true } override var supportedInterfaceOrientations: UIInterfaceOrientationMask { if UIDevice.current.userInterfaceIdiom == .phone { return .allButUpsideDown } else { return .all } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Release any cached data, images, etc that aren't in use. } }
今回はここまで。
iOS で SceneKit を試す(Swift 3) その87 - SceneKit でマルチパスレンダリングを行う SCNTechnique を試す
SceneKit では他のゲームエンジンと同様にシーンを複数回レンダリングし画像を合成するマルチパスレンダリングを行うことができる。
内容的には結構複雑なので、こんなことができるよというのがわかってもらえれば良いかと。
マルチパスレンダリングの使用用途は?
主に2タイプある。
レンダリングされたカラーバッファを入力として使用し、それをフラグメントシェーダーで処理するポストプロセッシング
カラーグレーディングやディスプレースメントマッピングなどに使用。
もう一つは、最初にレンダリングしパスとして仲介するバッファにキャプチャ、そのバッファを使用して追加の描画パスを実行して最終出力イメージを作成する遅延シェーディング
モーションブラーやスクリーンスペースアンビエントオクルージョン(SSAO)などに使用。
公式ドキュメントでの遅延シェーディングを使用した SSAO の例。
- 最初のレンダリング 0 パス目で色情報と深度情報を作成
- 1 パス で 0 パス で作成した深度情報とシーンの法線情報を設定
- 2 パスで 1 パスから作成した情報から SSAO の影の処理とノイズのテクスチャを作成
- 3 パス目で 2 パスと 0 パスの色情報とを合成し最終画像を作る
SCNTechnique - SceneKit | Apple Developer Documentation
実装するには?
Metal か GLSL のシェーダーを作成、plist などでそれらのシェーダーとともに初期設定をして、その plist を呼び出す。
そして、その plist を SCNTechnique に設定するとマルチパスレンダリング実行される。
今回 plist を使用しているが、SCNTechnique 初期化の際 NSDictionary 渡すだけなので、JSON から変換してもよいし、直接 NSDictionary に設定値を書き込んでもよい。
コードを書いてみる
かなり複雑なことが可能なのだが、今回は簡単な画面全体をドット絵っぽくするサンプルをつくる。
内容としては 0 パス目で画像を取得し、1 パス目で縮小された画像を等倍で表示するだけのもの。
てことで、いつも通り、Xcode の Game テンプレートで SceneKit を選択し作成する。
Metal シェーダーを設定する
メニューバーの File > New > File… か Project Navigator (Command + 1) 右クリックからの New File… もしくは Command + N で新規ファイルを作成を行い、
Metal File を選ぶか、テキストファイルとして作成して .metal ファイルを変更する。
今回は Pixelate.metal というファイル名にした。
作成したら以下のコードを書く。
#include <metal_stdlib> using namespace metal; #include <SceneKit/scn_metal> struct custom_vertex_t { float4 position [[attribute(SCNVertexSemanticPosition)]]; }; struct out_vertex_t { float4 position [[position]]; float2 uv; }; constexpr sampler s = sampler(coord::normalized, address::repeat, filter::nearest); vertex out_vertex_t pixelate_pass_through_vertex(custom_vertex_t in [[stage_in]], constant SCNSceneBuffer& scn_frame [[buffer(0)]]) { out_vertex_t out; out.position = in.position; out.uv = float2((in.position.x + 1.0) * 0.5 , (in.position.y + 1.0) * -0.5); return out; }; fragment half4 pixelate_pass_through_fragment(out_vertex_t vert [[stage_in]], texture2d<float, access::sample> colorSampler [[texture(0)]]) { float4 fragment_color = colorSampler.sample( s, vert.uv); return half4(fragment_color); };
ヴァーテックスの構造体とフラグメントの構造体を作成し、レンダー画像の再サンプルの再ニアレストネイバーを使用するように指定。
pixelate_pass_through_vertex を情報を取得し、pixelate_pass_through_fragment で色の情報を返している。
plist を設定する
新規ファイルで plist を選択するか、テキストファイルを .plist にして作成。
今回は Pixelate.plist として保存した。
plist をそのまま設定するのが面倒なので、
Project Navigator から Pixelate.plist 右クリックして Open As > Source Code で表示を XML にして、
以下のコードを書く
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>passes</key> <dict> <key>pixelate_scene</key> <dict> <key>draw</key> <string>DRAW_SCENE</string> <key>inputs</key> <dict/> <key>outputs</key> <dict> <key>color</key> <string>color_scene</string> </dict> <key>colorStates</key> <dict> <key>clear</key> <true/> <key>clearColor</key> <string>sceneBackground</string> </dict> </dict> <key>resample_pixelation</key> <dict> <key>draw</key> <string>DRAW_QUAD</string> <key>program</key> <string>doesntexist</string> <key>metalVertexShader</key> <string>pixelate_pass_through_vertex</string> <key>metalFragmentShader</key> <string>pixelate_pass_through_fragment</string> <key>inputs</key> <dict> <key>colorSampler</key> <string>color_scene</string> </dict> <key>outputs</key> <dict> <key>color</key> <string>COLOR</string> </dict> </dict> </dict> <key>sequence</key> <array> <string>pixelate_scene</string> <string>resample_pixelation</string> </array> <key>targets</key> <dict> <key>color_scene</key> <dict> <key>type</key> <string>color</string> <key>size</key> <string>64x114</string> </dict> </dict> <key>symbols</key> <dict/> </dict> </plist>
target の color_space で画像を変更するサイズを指定。
passes で pixelate_scene でレンダリング情報を取得し、resample_pixelation で Metal シェーダーの処理を設定。
sequence で pixelate_scene を 0 パス目で処理、その後の 1パス目で resample_pixelation の処理を行うよう指定している。
SCNView に SCNTechnique を適応する
GameViewController.swift を開き、viewDidLoad へ以下のコードを書く。
plist から NSDictionary に変換、SCNTechnique へ初期化、SCNView に SCNTechnique を渡す。
if let path = Bundle.main.path(forResource: "Pixelate", ofType: "plist") { if let dict = NSDictionary(contentsOfFile: path) { let dict2 = dict as! [String : AnyObject] let technique = SCNTechnique(dictionary:dict2) scnView.technique = technique } }
ガウシアンブラーを試してみる
4パスで処理する必要があるのため Metal の処理の部分で若干厳しさはある。
まとめ
公式ドキュメントの PDF (英語) で Metal Shader を覚えて、SCNTechnique の使用方法を覚える必要があるため大変だが、ここら辺がひと通りできるようになると、SceneKit や Metal での表現の幅が広がるので試してみると良いかもしれない。
SCNTechnique - SceneKit | Apple Developer Documentation
今回はここまで。
iOS で SceneKit を試す(Swift 3) その86 - SceneKit のカスタムシェーダーについて
SceneKit でのカスタムシェーダーは主に3つ
- SCNProgram でプリコンパイルしたものを使う
- SCNShadable の shaderModifiers に Metal / GLSL のスニペットであるテキストデータを使う
- SCNTechnique で設定し主にポストプロセスのように画面全体の変更する際に使う
基本、シェーダーは導入の説明を書くだけで、 何個か記事を書くことになりそうだから shaderModifiers を今回、SCNTechnique を次回軽く紹介していく。
ちなみに SCNProgram は面倒なので割愛。
SCNShadable の shaderModifiers とは?
ノードのマテリアルに対して個別でカスタムシェーダーを適応することができ、
カスタムシェーダーを適応する際は、定義されたエントリポイントを使用することとなる。
エントリポイントは以下の4つとなり、上から実行される。
エントリーポイント名 | 説明 |
---|---|
geometry | ジオメトリ形状を変更する |
surface | サーフェスプロパティ(Diffuse とか Material で設定しているもの)を変更する |
lightingModel | ライト情報の適応する情報を変更する |
fragment | 全ての情報を計算後に色を変更する |
使い方としては SCNMaterial を初期化して、初期化したものの shaderModifiers に SCNShaderModifierEntryPoint のエントリポイントに設定する。
シェーダーの記述は String 型なので、テキストファイルを読み込む形にしても使用できる。
ちなみに、マテリアルの情報が書き換えられるため、ジオメトリが持つ firstMaterial は無視される。
また、WWDC 2017 の SceneKit のセッションで言っていたように Xcode 9 からは Scene Editor でのシェーダーの実行テストができるようになった。
コードを書いてみる
いつも通り、Xcode の Game テンプレートで SceneKit を選択し作成。
GameViewController.swift を開く。
以下のコードを viewDidLoad() 内に書いてビルドすると宇宙船がトゥーンシェーディングされる
let shipMesh = scene.rootNode.childNode(withName: "shipMesh", recursively: true)! let toonMaterial = SCNMaterial() toonMaterial.shaderModifiers = [ SCNShaderModifierEntryPoint.lightingModel: "vec3 lDir = normalize(vec3(0.1, 1.0, 1.0));" + "float dotProduct = dot(_surface.normal, lDir);" + "_lightingContribution.diffuse += (dotProduct * dotProduct * _light.intensity.rgb);" + "_lightingContribution.diffuse = floor(_lightingContribution.diffuse * 3.0) / 3.0;" ] shipMesh.geometry?.materials = [toonMaterial] shipMesh.geometry?.firstMaterial?.diffuse.contents = UIImage(named: "art.scnassets/texture.png")
コード的には、宇宙船のジオメトリを指定し、SCNMaterial を初期化。
初期化したものの shaderModifiers に SCNShaderModifierEntryPoint の lightingModel をエントリーポイントとし、法線情報から外積を使用しライトの光の色ともに色を平滑化させている。
例えば、shaderModifiers を以下のものに変更すると色が反転する。
SCNShaderModifierEntryPoint.fragment: "_output.color.rgb = vec3(1.0) - _output.color.rgb;"
色を反転させているが、宇宙船の部分だけが色の反転が行われており、背景は反転していない。
と、このような感じでカスタムシェーダーを作成できる。
基本、一連の SceneKit の記事では数学の話を省いているが、 シェーダーに関してはそれなりに数学の知識が必要なのでいつかじっくり説明したい気はする。
今回はここまで。