将 SpriteKit SKScene overlaySKScene 的 2D 点映射到 SceneKit SCNScene 中的 3D 点
Map 2D point from SpriteKit SKScene overlaySKScene to 3D point in SceneKit SCNScene
我正在尝试将触摸从 SpriteKit 转换为 SceneKit,但遇到了一些困难。我的意图是移动一个 3D "monster",这样它就好像在同一个位置(虽然在后面)一个 2D crystal。 2D crystal 添加到 SpriteKit 提供的 overlaySKScene
并定位。我们的 "monster" 是坐在 SCNScene
中的全 3D SCNNode
,如下所示。
我已经尝试了多种方法,其中最接近的方法我已经包含在下面的 makeMonsterEatCrystalAtLocation
方法中,尽管我知道它是错误的,因为它只在 SpriteKit 世界中起作用 - 它不会从 SpriteKit 转换为 SceneKit。我还研究了 SCNSceneRenderer
协议中的 unprojectPoint
,但该方法仅适用于 SceneKit 本身。任何帮助将不胜感激!我不偏爱 Objective-C 或 Swift,即使下面是 Obj-C。
- (void)setupSceneKit {
_sceneView = [[SCNView alloc] initWithFrame:[[UIScreen mainScreen] bounds] options:@{@"SCNPreferredRenderingAPIKey": @(SCNRenderingAPIOpenGLES2)}];
[self.view insertSubview:_sceneView aboveSubview:backgroundView];
_sceneView.scene = [SCNScene scene];
_sceneView.allowsCameraControl = YES;
_sceneView.autoenablesDefaultLighting = NO;
_sceneView.backgroundColor = [UIColor clearColor];
camera = [SCNCamera camera];
[camera setXFov:20];
[camera setYFov:20];
camera.zFar = 10000.0f;
cameraNode = [SCNNode node];
cameraNode.camera = camera;
cameraNode.position = SCNVector3Make(0, 3, 700);
[_sceneView.scene.rootNode addChildNode:cameraNode];
}
- (void)createMonster {
_monsterNode = [SCNNode node];
[self.sceneView.scene.rootNode addChildNode:_monsterNode];
[SCNTransaction begin];
[SCNTransaction setAnimationDuration:0.5f];
_monsterNode.position = SCNVector3Zero;
[SCNTransaction commit];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
CGPoint crystalLocation = [[touches anyObject] locationInNode:self.sceneView.overlaySKScene];
SKNode *touchedCrystalNode = [self.sceneView.overlaySKScene nodeAtPoint:crystalLocation];
// determine that this is the SKNode touchedCrystalNode we're looking for...
[self makeMonsterEatCrystalAtLocation:crystalLocation];
}
- (void)makeMonsterEatCrystalAtLocation:(CGPoint)location {
// Attempt to move the "monster" (SCNNode in SceneKit) to appear as if its in the same
// location as the "crystal" (which is presented in the SpriteKit overlay: self.sceneView.overlaySKScene) and eventually
// play an animation of the monster eating the crystal.
CGPoint scnPoint = [self.sceneView.overlaySKScene convertPointToView:location];
[SCNTransaction begin];
[SCNTransaction setAnimationDuration:0.5f];
self.monsterNode.position = SCNVector3Make(scnPoint.x, scnPoint.y, 0);
[SCNTransaction commit];
// ...
}
一种方法是将怪物放在 SK3DNode
中。那么你就可以在SpriteKit中度过你一生的大部分时间,而不必在坐标系之间进行转换。
投影时需要处理几个不同的坐标系:SpriteKit场景(原点在左下角),SceneKit视图的屏幕坐标(原点在左上角或左下角,取决于OS) ,视图的相机局部坐标系,以及 SceneKit 场景的世界坐标系。
让我们从 SceneKit 方面开始。您的相机有自己的 3D 坐标系,表示 view frustum 的内容。
屏幕上的单个点对应于平截头体内从近平面到远平面的一条线段;相机 space 中的 Z 轴对应于点在近平面和远平面之间的相对位置。您的 SCNView
,由于遵守协议 SCNSceneRenderer
,可以在屏幕坐标和 3D 世界之间转换 space。
要移动您的怪物,您将使用 projectPoint
获取其当前相机 x/y/z 坐标,找出您要移动到的屏幕位置,然后使用 unprojectPoint
计算该屏幕位置的世界位置。像这样:
let screenDestinationX = 100.0
let screenDestinationY = 200.0
let monsterCoordinates = sceneView.projectPoint(MonsterNode.position)
let monsterDestinationCoordinates = SCNVector3D(x: screenDestinationX,
y: screenDestinationY,
z: monsterCoordinates.z)
let monsterDestinationPosition = sceneView.unprojectPoint(monsterDestinationCoordinates)
通过为怪物开始和结束保持相同的相机 Z 坐标,您将使怪物与相机保持相同的距离。
现在是 SpriteKit 方面。如果您的 sceneView.overlaySKScene
的 scaleMode
设置为 SKSceneScaleMode.ResizeFill
,您的 SKScene
将与您的屏幕大小相同。当然,Y 轴的方向可能不同。所以向你的 crystal 询问它的 SpriteKit 坐标,必要时翻转 Y(高度减去 Y,而不仅仅是 Y),并且你有目的地 X 和 Y,你可以传递给 SceneKit 进行投影。
我有一个示例项目,它显示了 https://github.com/halmueller/ImmersiveInterfaces/tree/master/Tracking%20Overlay 处 SceneKit 和叠加 SpriteKit 坐标之间的转换。它并不完全符合您的要求,但确实显示了交互。
最后,下面是一个显示世界坐标和相机坐标之间转换的游乐场:
import SceneKit
import SpriteKit
import XCPlayground
let sceneView = SCNView(frame: CGRect(x: 0, y: 0, width: 800, height: 600))
XCPlaygroundPage.currentPage.liveView = sceneView
var scene = SCNScene()
sceneView.scene = scene
sceneView.backgroundColor = SKColor.greenColor()
sceneView.debugOptions = .ShowWireframe
// default lighting
sceneView.autoenablesDefaultLighting = true
// a camera
var cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
print (cameraNode.camera?.zFar, cameraNode.camera?.zNear)
cameraNode.camera?.automaticallyAdjustsZRange = false
cameraNode.position = SCNVector3(x: 5, y: 5, z: 50)
scene.rootNode.addChildNode(cameraNode)
let center = SCNNode()
center.position = SCNVector3(x:0, y:0, z:0)
scene.rootNode.addChildNode(center)
let centerConstraint = SCNLookAtConstraint(target: center)
cameraNode.constraints = [centerConstraint]
let sphere = SCNSphere(radius: 2)
sphere.geodesic = true
let sphereNode1 = SCNNode(geometry: sphere)
sphereNode1.position = SCNVector3(x:-8, y:5, z:10)
scene.rootNode.addChildNode(sphereNode1)
let sphereNode2 = SCNNode(geometry: sphere)
sphereNode2.position = SCNVector3(x:8, y:-5, z:-30)
scene.rootNode.addChildNode(sphereNode2)
let sphere1Coordinates = sceneView.projectPoint(sphereNode1.position)
print ("sphere1 position", sphereNode1.position, "screen coordinates",sphere1Coordinates)
let sphere2Coordinates = sceneView.projectPoint(sphereNode2.position)
print ("sphere2 position", sphereNode2.position, "screen coordinates",sphere2Coordinates)
let sphere2NearCoordinates = SCNVector3(x: sphere2Coordinates.x, y: sphere2Coordinates.y, z: sphere1Coordinates.z)
let sphere2FarPosition = sphereNode2.position
let sphere2NearPosition = sceneView.unprojectPoint(sphere2NearCoordinates)
let sphere1FarCoordinates = SCNVector3(x: sphere1Coordinates.x, y: sphere1Coordinates.y, z: sphere2Coordinates.z)
let sphere1NearPosition = sphereNode1.position
let sphere1FarPosition = sceneView.unprojectPoint(sphere1FarCoordinates)
let sphere1LowerCoordinates = SCNVector3(x: sphere1Coordinates.x, y: sphere1Coordinates.y - 100, z: sphere2Coordinates.z)
let sphere1LowerPosition = sceneView.unprojectPoint(sphere1LowerCoordinates)
let forwardAction = SCNAction.moveTo(sphere2NearPosition, duration: 3)
let backAction = SCNAction.moveTo(sphere2FarPosition, duration: 3)
let shiftAction = SCNAction.moveTo(sphere1LowerPosition, duration: 3)
let forwardBackShift = SCNAction.sequence([forwardAction, backAction, forwardAction, shiftAction])
sphereNode2.runAction(forwardBackShift) {
print ("sphere1 position", sphereNode1.position, "screen coordinates",sceneView.projectPoint(sphereNode1.position))
print ("sphere2 position", sphereNode2.position, "screen coordinates",sceneView.projectPoint(sphereNode2.position))
}
我正在尝试将触摸从 SpriteKit 转换为 SceneKit,但遇到了一些困难。我的意图是移动一个 3D "monster",这样它就好像在同一个位置(虽然在后面)一个 2D crystal。 2D crystal 添加到 SpriteKit 提供的 overlaySKScene
并定位。我们的 "monster" 是坐在 SCNScene
中的全 3D SCNNode
,如下所示。
我已经尝试了多种方法,其中最接近的方法我已经包含在下面的 makeMonsterEatCrystalAtLocation
方法中,尽管我知道它是错误的,因为它只在 SpriteKit 世界中起作用 - 它不会从 SpriteKit 转换为 SceneKit。我还研究了 SCNSceneRenderer
协议中的 unprojectPoint
,但该方法仅适用于 SceneKit 本身。任何帮助将不胜感激!我不偏爱 Objective-C 或 Swift,即使下面是 Obj-C。
- (void)setupSceneKit {
_sceneView = [[SCNView alloc] initWithFrame:[[UIScreen mainScreen] bounds] options:@{@"SCNPreferredRenderingAPIKey": @(SCNRenderingAPIOpenGLES2)}];
[self.view insertSubview:_sceneView aboveSubview:backgroundView];
_sceneView.scene = [SCNScene scene];
_sceneView.allowsCameraControl = YES;
_sceneView.autoenablesDefaultLighting = NO;
_sceneView.backgroundColor = [UIColor clearColor];
camera = [SCNCamera camera];
[camera setXFov:20];
[camera setYFov:20];
camera.zFar = 10000.0f;
cameraNode = [SCNNode node];
cameraNode.camera = camera;
cameraNode.position = SCNVector3Make(0, 3, 700);
[_sceneView.scene.rootNode addChildNode:cameraNode];
}
- (void)createMonster {
_monsterNode = [SCNNode node];
[self.sceneView.scene.rootNode addChildNode:_monsterNode];
[SCNTransaction begin];
[SCNTransaction setAnimationDuration:0.5f];
_monsterNode.position = SCNVector3Zero;
[SCNTransaction commit];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
CGPoint crystalLocation = [[touches anyObject] locationInNode:self.sceneView.overlaySKScene];
SKNode *touchedCrystalNode = [self.sceneView.overlaySKScene nodeAtPoint:crystalLocation];
// determine that this is the SKNode touchedCrystalNode we're looking for...
[self makeMonsterEatCrystalAtLocation:crystalLocation];
}
- (void)makeMonsterEatCrystalAtLocation:(CGPoint)location {
// Attempt to move the "monster" (SCNNode in SceneKit) to appear as if its in the same
// location as the "crystal" (which is presented in the SpriteKit overlay: self.sceneView.overlaySKScene) and eventually
// play an animation of the monster eating the crystal.
CGPoint scnPoint = [self.sceneView.overlaySKScene convertPointToView:location];
[SCNTransaction begin];
[SCNTransaction setAnimationDuration:0.5f];
self.monsterNode.position = SCNVector3Make(scnPoint.x, scnPoint.y, 0);
[SCNTransaction commit];
// ...
}
一种方法是将怪物放在 SK3DNode
中。那么你就可以在SpriteKit中度过你一生的大部分时间,而不必在坐标系之间进行转换。
投影时需要处理几个不同的坐标系:SpriteKit场景(原点在左下角),SceneKit视图的屏幕坐标(原点在左上角或左下角,取决于OS) ,视图的相机局部坐标系,以及 SceneKit 场景的世界坐标系。
让我们从 SceneKit 方面开始。您的相机有自己的 3D 坐标系,表示 view frustum 的内容。
屏幕上的单个点对应于平截头体内从近平面到远平面的一条线段;相机 space 中的 Z 轴对应于点在近平面和远平面之间的相对位置。您的 SCNView
,由于遵守协议 SCNSceneRenderer
,可以在屏幕坐标和 3D 世界之间转换 space。
要移动您的怪物,您将使用 projectPoint
获取其当前相机 x/y/z 坐标,找出您要移动到的屏幕位置,然后使用 unprojectPoint
计算该屏幕位置的世界位置。像这样:
let screenDestinationX = 100.0
let screenDestinationY = 200.0
let monsterCoordinates = sceneView.projectPoint(MonsterNode.position)
let monsterDestinationCoordinates = SCNVector3D(x: screenDestinationX,
y: screenDestinationY,
z: monsterCoordinates.z)
let monsterDestinationPosition = sceneView.unprojectPoint(monsterDestinationCoordinates)
通过为怪物开始和结束保持相同的相机 Z 坐标,您将使怪物与相机保持相同的距离。
现在是 SpriteKit 方面。如果您的 sceneView.overlaySKScene
的 scaleMode
设置为 SKSceneScaleMode.ResizeFill
,您的 SKScene
将与您的屏幕大小相同。当然,Y 轴的方向可能不同。所以向你的 crystal 询问它的 SpriteKit 坐标,必要时翻转 Y(高度减去 Y,而不仅仅是 Y),并且你有目的地 X 和 Y,你可以传递给 SceneKit 进行投影。
我有一个示例项目,它显示了 https://github.com/halmueller/ImmersiveInterfaces/tree/master/Tracking%20Overlay 处 SceneKit 和叠加 SpriteKit 坐标之间的转换。它并不完全符合您的要求,但确实显示了交互。
最后,下面是一个显示世界坐标和相机坐标之间转换的游乐场:
import SceneKit
import SpriteKit
import XCPlayground
let sceneView = SCNView(frame: CGRect(x: 0, y: 0, width: 800, height: 600))
XCPlaygroundPage.currentPage.liveView = sceneView
var scene = SCNScene()
sceneView.scene = scene
sceneView.backgroundColor = SKColor.greenColor()
sceneView.debugOptions = .ShowWireframe
// default lighting
sceneView.autoenablesDefaultLighting = true
// a camera
var cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
print (cameraNode.camera?.zFar, cameraNode.camera?.zNear)
cameraNode.camera?.automaticallyAdjustsZRange = false
cameraNode.position = SCNVector3(x: 5, y: 5, z: 50)
scene.rootNode.addChildNode(cameraNode)
let center = SCNNode()
center.position = SCNVector3(x:0, y:0, z:0)
scene.rootNode.addChildNode(center)
let centerConstraint = SCNLookAtConstraint(target: center)
cameraNode.constraints = [centerConstraint]
let sphere = SCNSphere(radius: 2)
sphere.geodesic = true
let sphereNode1 = SCNNode(geometry: sphere)
sphereNode1.position = SCNVector3(x:-8, y:5, z:10)
scene.rootNode.addChildNode(sphereNode1)
let sphereNode2 = SCNNode(geometry: sphere)
sphereNode2.position = SCNVector3(x:8, y:-5, z:-30)
scene.rootNode.addChildNode(sphereNode2)
let sphere1Coordinates = sceneView.projectPoint(sphereNode1.position)
print ("sphere1 position", sphereNode1.position, "screen coordinates",sphere1Coordinates)
let sphere2Coordinates = sceneView.projectPoint(sphereNode2.position)
print ("sphere2 position", sphereNode2.position, "screen coordinates",sphere2Coordinates)
let sphere2NearCoordinates = SCNVector3(x: sphere2Coordinates.x, y: sphere2Coordinates.y, z: sphere1Coordinates.z)
let sphere2FarPosition = sphereNode2.position
let sphere2NearPosition = sceneView.unprojectPoint(sphere2NearCoordinates)
let sphere1FarCoordinates = SCNVector3(x: sphere1Coordinates.x, y: sphere1Coordinates.y, z: sphere2Coordinates.z)
let sphere1NearPosition = sphereNode1.position
let sphere1FarPosition = sceneView.unprojectPoint(sphere1FarCoordinates)
let sphere1LowerCoordinates = SCNVector3(x: sphere1Coordinates.x, y: sphere1Coordinates.y - 100, z: sphere2Coordinates.z)
let sphere1LowerPosition = sceneView.unprojectPoint(sphere1LowerCoordinates)
let forwardAction = SCNAction.moveTo(sphere2NearPosition, duration: 3)
let backAction = SCNAction.moveTo(sphere2FarPosition, duration: 3)
let shiftAction = SCNAction.moveTo(sphere1LowerPosition, duration: 3)
let forwardBackShift = SCNAction.sequence([forwardAction, backAction, forwardAction, shiftAction])
sphereNode2.runAction(forwardBackShift) {
print ("sphere1 position", sphereNode1.position, "screen coordinates",sceneView.projectPoint(sphereNode1.position))
print ("sphere2 position", sphereNode2.position, "screen coordinates",sceneView.projectPoint(sphereNode2.position))
}