[ARKit]13-[译]在ARKit中创建一个时空门App:材质和光照

1,927 阅读16分钟

说明

ARKit系列文章目录

译者注:本文是Raywenderlich上《ARKit by Tutorials》免费章节的翻译,是原书第9章.原书7~9章完成了一个时空门app.
官网原文地址www.raywenderlich.com/195419/buil…


本文是我们书籍ARKit by Tutorials中的第9章,“材质和光照”.这本书向你展示了如何用苹果的增强现实框架ARKit,来构建五个沉浸式的,好看的AR应用.开始吧!

在本app三部教程的前两部,你已经学会了如何用SceneKit向场景中添加3D物体.现在是时候将这些知识用起来,构建整个时空门了.在本教程中,你将会学到:

  • 创建墙壁,天花板和屋顶,并调整他们的位置和朝向.
  • 用不同的纹理将时空门内部做的更真实.
  • 添加灯光到你的场景.

开始

点击这里下载本文资料,然后打开starter文件夹中的starter项目.在你开始前,你需要知道一点关于SceneKit的知识.

SceneKit坐标系统

正如前一章你看到的那样,SceneKit可能用来添加虚拟物体到你的视图中.SceneKit内容视图包含了一个树状层级结构的节点,也就是scene graph(场景图).场景拥有一个root node(根节点),它定义了场景中的世界坐标空间,其它节点则构成了这个世界中的可见内容.你在屏幕上渲染出的每一个node或3D物体都是一个SCNNode类型的对象.一个SCNNode对象定义了自身坐标系相对于父节点的变换(位置,朝向和缩放).它本身并没有任何可见内容.

场景中的rootNode对象,定义了SceneKit渲染出的世界坐标系.你添加到根节点上的每一个子节点都会创建一个自己的坐标系统,同样这个坐标系也会被自己的子节点继承.

SceneKit使用了一个右手系的坐标系统(默认),;视图的朝向是z轴的负方向,如下所示.

SCNNode对象的位置是使用一个SCNVector3来定义的,它代表了自己在父节点坐标系中的位置.默认位置是零向量,表示当前节点是位于父节点坐标系的原点上.在本例中,SCNVector3是一个三元向量,每个元素代表每个坐标轴的Float数值.

SCNNode对象的朝向,也就是pitch(俯仰), yaw(偏航), roll(滚转)角度是由eulerAngles属性定义的.它同样也是一个SCNVector3结构体表示的,每个分量是一个弧度制表示的角度.

纹理

SCNNode对象自身是不包含任何可见内容的.你需要将2D和3D物体添加到场景上时,只要将SCNGeometry对象添加到节点上就可以了.几何体中拥有SCNmaterial对象,可以决定它的外观.

一个SCNMaterial拥有若干个可见属性.每一个可见属性都是一个SCNMaterialProperty类型的实例对象,它提供了一个实体颜色,纹理或其他2D内容.其中的很多可见属性用来完成基础着色,基于物理着色和特殊效果,可以让材质看起来更真实.

SceneKit asset catalog是专门设计出来,帮助你无需代码就能管理项目中的素材的.在你的starter项目中,打开Assets.scnassets文件夹.会看到已经有一些图片,用来表示天花板,地板和墙壁中的各种不同可见属性.

使用SceneKit,你还可以使用添加了SCNLight对象的节点,来给场景中的几何体添加光照和阴影效果.

创建时空门

让我们进入创建时空门地板的环节.打开SCNNodeHelpers.swift,并在文件顶部import SceneKit语句下方添加下列代码.

/ 1
let SURFACE_LENGTH: CGFloat = 3.0
let SURFACE_HEIGHT: CGFloat = 0.2
let SURFACE_WIDTH: CGFloat = 3.0

// 2
let SCALEX: Float = 2.0
let SCALEY: Float = 2.0

// 3
let WALL_WIDTH:CGFloat = 0.2
let WALL_HEIGHT:CGFloat = 3.0
let WALL_LENGTH:CGFloat = 3.0

代码含义:

  1. 定义常量,用来表示时空门中地板和天花板的尺寸.地板和天花板的高度也就是它们的厚度.
  2. 这些常量表示表面纹理的缩放和重复.
  3. 这些定义墙壁节点的宽度,高度和长度.

接着,给SCNNodeHelpers添加下面的方法:

func repeatTextures(geometry: SCNGeometry, scaleX: Float, scaleY: Float) {
  // 1
  geometry.firstMaterial?.diffuse.wrapS = SCNWrapMode.repeat
  geometry.firstMaterial?.selfIllumination.wrapS = SCNWrapMode.repeat
  geometry.firstMaterial?.normal.wrapS = SCNWrapMode.repeat
  geometry.firstMaterial?.specular.wrapS = SCNWrapMode.repeat
  geometry.firstMaterial?.emission.wrapS = SCNWrapMode.repeat
  geometry.firstMaterial?.roughness.wrapS = SCNWrapMode.repeat

  // 2
  geometry.firstMaterial?.diffuse.wrapT = SCNWrapMode.repeat
  geometry.firstMaterial?.selfIllumination.wrapT = SCNWrapMode.repeat
  geometry.firstMaterial?.normal.wrapT = SCNWrapMode.repeat
  geometry.firstMaterial?.specular.wrapT = SCNWrapMode.repeat
  geometry.firstMaterial?.emission.wrapT = SCNWrapMode.repeat
  geometry.firstMaterial?.roughness.wrapT = SCNWrapMode.repeat

  // 3
  geometry.firstMaterial?.diffuse.contentsTransform =
    SCNMatrix4MakeScale(scaleX, scaleY, 0)
  geometry.firstMaterial?.selfIllumination.contentsTransform =
    SCNMatrix4MakeScale(scaleX, scaleY, 0)
  geometry.firstMaterial?.normal.contentsTransform =
    SCNMatrix4MakeScale(scaleX, scaleY, 0)
  geometry.firstMaterial?.specular.contentsTransform =
    SCNMatrix4MakeScale(scaleX, scaleY, 0)
  geometry.firstMaterial?.emission.contentsTransform =
    SCNMatrix4MakeScale(scaleX, scaleY, 0)
  geometry.firstMaterial?.roughness.contentsTransform =
    SCNMatrix4MakeScale(scaleX, scaleY, 0)
}

这里定义一个方法,来使纹理沿X和Y方向重复.

代码解释;

  1. 这个方法接收一个SCNGeometry对象和X,Y缩放因子作为输入.纹理贴图使用ST坐标系统:S对应X,T对应于Y.这里你为所有可见属性都定义了S方向的wrapping mode(包裹模式)为SCNWrapMode.repeat.
  2. 为所有可见属性都定义了T方向的wrapping mode(包裹模式)为SCNWrapMode.repeat.在repeat模式下,纹理采样只使用了纹理坐标的一小部分.
  3. 这里,每一个可见属性contentsTransform都设置为用SCNMatrix4结构体表示的缩放变换矩阵.设置X和Y缩放因子为scaleXscaleY.

你只让用户进入时空门时,地板和天花板节点显示;其他情况下,隐藏起来.要实现这个效果,在SCNNodeHelpers中添加下列代码:

func makeOuterSurfaceNode(width: CGFloat,
                          height: CGFloat,
                          length: CGFloat) -> SCNNode {
  // 1
  let outerSurface = SCNBox(width: width,
                            height: height,
                            length: length,
                            chamferRadius: 0)
  
  // 2
  outerSurface.firstMaterial?.diffuse.contents = UIColor.white
  outerSurface.firstMaterial?.transparency = 0.000001
  
  // 3
  let outerSurfaceNode = SCNNode(geometry: outerSurface)
  outerSurfaceNode.renderingOrder = 10
  return outerSurfaceNode
}

代码解释:

  1. 创建一个outerSurface场景立方体几何体对象,尺寸和地板与天花板相同.
  2. 添加可见内容到立方体对象的漫反射属性,使其渲染出来.设置transparency(透明度) 为非常低的数值,这样这个物体就从视图中隐藏起来.
  3. outerSurface几何体创建一个SCNNode对象.设置节点的renderingOrder(渲染顺序) 为10.节点的渲染顺序值越大就渲染得越晚.为了让地板和天花板从时空门外面不可见,你将需要使内部的天花板和地板节点的渲染顺序远大于10.

现在向SCNNodeHelpers中添加下列代码来创建时空门的地板:

func makeFloorNode() -> SCNNode {
  // 1
  let outerFloorNode = makeOuterSurfaceNode(
                       width: SURFACE_WIDTH,
                       height: SURFACE_HEIGHT,
                       length: SURFACE_LENGTH)
  
  // 2
  outerFloorNode.position = SCNVector3(SURFACE_HEIGHT * 0.5,
                                       -SURFACE_HEIGHT, 0)
  let floorNode = SCNNode()
  floorNode.addChildNode(outerFloorNode)

  // 3
  let innerFloor = SCNBox(width: SURFACE_WIDTH,
                          height: SURFACE_HEIGHT,
                          length: SURFACE_LENGTH,
                          chamferRadius: 0)
  
  // 4
  innerFloor.firstMaterial?.lightingModel = .physicallyBased
  innerFloor.firstMaterial?.diffuse.contents =
    UIImage(named: 
    "Assets.scnassets/floor/textures/Floor_Diffuse.png")
  innerFloor.firstMaterial?.normal.contents =
    UIImage(named: 
    "Assets.scnassets/floor/textures/Floor_Normal.png")
  innerFloor.firstMaterial?.roughness.contents =
    UIImage(named: 
    "Assets.scnassets/floor/textures/Floor_Roughness.png")
  innerFloor.firstMaterial?.specular.contents =
    UIImage(named: 
    "Assets.scnassets/floor/textures/Floor_Specular.png")
  innerFloor.firstMaterial?.selfIllumination.contents =
    UIImage(named: 
    "Assets.scnassets/floor/textures/Floor_Gloss.png")
  
  // 5  
  repeatTextures(geometry: innerFloor, 
                 scaleX: SCALEX, scaleY: SCALEY)
  
  // 6
  let innerFloorNode = SCNNode(geometry: innerFloor)
  innerFloorNode.renderingOrder = 100
  
  // 7
  innerFloorNode.position = SCNVector3(SURFACE_HEIGHT * 0.5, 
                                       0, 0)
  floorNode.addChildNode(innerFloorNode)
  return floorNode
}

代码解释:

  1. 使用定义好的地板的尺寸创建一个外层的地板节点.
  2. 放置outerFloorNode,使其位于地板节点的底面下方.将其添加到floorNode上,这个节点将会同时持有地板的内层和外层表面.
  3. 使用SCNBox对象创建地板几何体,尺寸使用预先定义的尺寸.
  4. 地板材质的lightingModel(光照模型) 设置为physicallyBased.这种类型的阴影包含了对现实中灯光和材质物理效果的抽象.材质的可见属性设置为scnassets素材集中的纹理图片.
  5. 材质的纹理沿X和Y方向重复,使用的是先前定义的repeatTextures().
  6. 给地板创建一个节点,使用innerFloor几何体对象,并设置渲染顺序高于outerFloorNode.这样确保了当用户在时空门外面时,地板节点是不可见的.
  7. 最后,设置innerFloorNode的位置,使其位于outerFloorNode上方,并将其添加到floorNode作为子节点.返回地板节点对象给函数调用者.

打开PortalViewController.swift并添加下列常量:

let POSITION_Y: CGFloat = -WALL_HEIGHT*0.5
let POSITION_Z: CGFloat = -SURFACE_LENGTH*0.5

这些常量代表节点在Y和Z方向上的位置偏移.

通过替换makePortal() 来将地板节点添加到时空门中.

func makePortal() -> SCNNode {
  // 1
  let portal = SCNNode()
  
  // 2
  let floorNode = makeFloorNode()
  floorNode.position = SCNVector3(0, POSITION_Y, POSITION_Z)
  
  // 3
  portal.addChildNode(floorNode)
  return portal
}

代码很简单:

  1. 创建一个SCNNode对象来持有时空门.
  2. 用在SCNNodeHelpers中定义的makeFloorNode() 方法来创建地板节点.使用前面的常量偏移值来设定floorNode的位置.SCNGeometry的中心会对准父节点坐标系中的坐标位置.<比如设定为(1,1,1),则几何体中心会对准(1,1,1)>
  3. 添加floorNode到时空门节点并返回时空门节点.注意,时空门节点是在用户点击视图时,在被添加到renderer(_ :, didAdd:, for:) 中的锚点位置上的.

运行app.你会看到地板节点有些黑暗,那是因为你还没有添加光源而已!

现在添加天花板节点.打开SCNNodeHelpers.swift并添加下列方法:

func makeCeilingNode() -> SCNNode {
  // 1
  let outerCeilingNode = makeOuterSurfaceNode(
                          width: SURFACE_WIDTH,
                          height: SURFACE_HEIGHT,
                          length: SURFACE_LENGTH)
  
  // 2                                            
  outerCeilingNode.position = SCNVector3(SURFACE_HEIGHT * 0.5,
                                         SURFACE_HEIGHT, 0)
  let ceilingNode = SCNNode()
  ceilingNode.addChildNode(outerCeilingNode)

  // 3
  let innerCeiling = SCNBox(width: SURFACE_WIDTH,
                            height: SURFACE_HEIGHT,
                            length: SURFACE_LENGTH,
                            chamferRadius: 0)
  
  // 4                            
  innerCeiling.firstMaterial?.lightingModel = .physicallyBased
  innerCeiling.firstMaterial?.diffuse.contents =
    UIImage(named: 
    "Assets.scnassets/ceiling/textures/Ceiling_Diffuse.png")
  innerCeiling.firstMaterial?.emission.contents =
    UIImage(named: 
    "Assets.scnassets/ceiling/textures/Ceiling_Emis.png")
  innerCeiling.firstMaterial?.normal.contents =
    UIImage(named: 
    "Assets.scnassets/ceiling/textures/Ceiling_Normal.png")
  innerCeiling.firstMaterial?.specular.contents =
    UIImage(named: 
    "Assets.scnassets/ceiling/textures/Ceiling_Specular.png")
  innerCeiling.firstMaterial?.selfIllumination.contents =
    UIImage(named: 
    "Assets.scnassets/ceiling/textures/Ceiling_Gloss.png")
  
  // 5
  repeatTextures(geometry: innerCeiling, scaleX: 
                 SCALEX, scaleY: SCALEY)
  
  // 6
  let innerCeilingNode = SCNNode(geometry: innerCeiling)
  innerCeilingNode.renderingOrder = 100
  
  // 7
  innerCeilingNode.position = SCNVector3(SURFACE_HEIGHT * 0.5, 
                                         0, 0)
  ceilingNode.addChildNode(innerCeilingNode)  
  return ceilingNode
}

代码解释:

  1. 类似于地板,创建一个outerCeilingNode.
  2. 设置外层天花板节点的位置,使其在天花板的上方.创建一个节点来持有内层和外层天花板.将outerCeilingNode添加为ceilingNode的子节点.
  3. innerCeiling创建一个有合适尺寸的SCNBox对象.
  4. 设置lightModelphysicallyBased,材质的可见属性设置为scnassets素材集中的纹理图片.
  5. 材质的纹理沿X和Y方向重复,使用的是先前定义的repeatTextures().
  6. innerCeilingNode创建一个节点,使用innerCeiling几何体对象,并设置渲染顺序高于outerCeilingNode,这样它的渲染顺序就在外层之后.
  7. 最后,设置innerCeilingNode的位置,并将其添加到ceilingNode作为子节点.返回ceilingNode给函数调用者.

现在需要在其他地方调用这个方法了.打开PortalViewController.swift添加下面的代码到makePortal() 中,放在return语句前面.

/ 1
let ceilingNode = makeCeilingNode()
ceilingNode.position = SCNVector3(0,
                                  POSITION_Y+WALL_HEIGHT,
                                  POSITION_Z)
// 2
portal.addChildNode(ceilingNode)
  1. 使用刚才定义的makeCeilingNode() 方法创建天花板节点.设置ceilingNode的位置为SCNVector3结构体.中心点的Y坐标,偏移了地板厚度加墙壁高度的位置.
    你也可以减掉SURFACE_HEIGHT来得到天花板的厚度.类似于地板,Z坐标偏移也设置为POSITION_Z.这就是天花板中心点到摄像机在Z轴上的距离.
  2. 添加ceilingNode作为时空门的子节点.

运行一下app,你会看到:

是时候添加墙壁了!

打开SCNNodeHelpers.swift并添加下列方法.

func makeWallNode(length: CGFloat = WALL_LENGTH,
                  height: CGFloat = WALL_HEIGHT,
                  maskLowerSide:Bool = false) -> SCNNode {
    
  // 1                      
  let outerWall = SCNBox(width: WALL_WIDTH,
                         height: height,
                         length: length,
                         chamferRadius: 0)
  // 2                        
  outerWall.firstMaterial?.diffuse.contents = UIColor.white
  outerWall.firstMaterial?.transparency = 0.000001

  // 3
  let outerWallNode = SCNNode(geometry: outerWall)
  let multiplier: CGFloat = maskLowerSide ? -1 : 1
  outerWallNode.position = SCNVector3(WALL_WIDTH*multiplier,0,0)
  outerWallNode.renderingOrder = 10
  
  // 4
  let wallNode = SCNNode()
  wallNode.addChildNode(outerWallNode)

  // 5
  let innerWall = SCNBox(width: WALL_WIDTH,
                         height: height,
                         length: length,
                         chamferRadius: 0)
  
  // 6                       
  innerWall.firstMaterial?.lightingModel = .physicallyBased
  innerWall.firstMaterial?.diffuse.contents =
    UIImage(named: 
    "Assets.scnassets/wall/textures/Walls_Diffuse.png")
  innerWall.firstMaterial?.metalness.contents =
    UIImage(named: 
    "Assets.scnassets/wall/textures/Walls_Metalness.png")
  innerWall.firstMaterial?.roughness.contents =
    UIImage(named: 
    "Assets.scnassets/wall/textures/Walls_Roughness.png")
  innerWall.firstMaterial?.normal.contents =
    UIImage(named: 
    "Assets.scnassets/wall/textures/Walls_Normal.png")
  innerWall.firstMaterial?.specular.contents =
    UIImage(named: 
    "Assets.scnassets/wall/textures/Walls_Spec.png")
  innerWall.firstMaterial?.selfIllumination.contents =
    UIImage(named: 
    "Assets.scnassets/wall/textures/Walls_Gloss.png")

  // 7
  let innerWallNode = SCNNode(geometry: innerWall)
  wallNode.addChildNode(innerWallNode)  
  return wallNode
}

代码解释:

  1. 创建一个outerWall节点,并放在墙壁的外侧,确保它从外面看是透明的.创建一个个SCNBox对象来匹配墙壁的尺寸.
  2. 设置材质的diffuse内容为纯白色并且透明值为一个极低的数值.这帮助我们达到外部透视的效果.
  3. outerWall几何体创建一个节点.multiplier是根据外层墙壁的哪一面需要被渲染来设定.如果maskLowerSide设置为true,那么外层墙壁在墙壁节点的坐标系统中会被放置在内层墙壁的下面;否则,它就被放置在内层的上面.
    设置节点的位置,这样外层墙壁在X方向上偏移了墙壁的宽度.设置外层墙壁的渲染顺序为一个较低的数值,这样它就会被优先渲染.这样会使墙壁从外面不可见.
  4. 创建一个节点来持有墙壁,并将outerWallNode添加为其子节点.
  5. innerWall创建一个SCNBox对象,尺寸同墙壁的尺寸.
  6. 设置lightingModelphysicallyBased.类似于天花板和地板节点,设置可见属性的内容为各种墙壁纹理图片.
  7. 最后,使用innerWall几何体创建一个innerWallNode对象.添加这个节点到父节点wallNode对象上.默认情况下,innerWallNode会被放置在wallNode的原点上.返回节点给函数调用者.

现在添加时空门远处的墙壁.打开PortalViewController.swift并添加下列方法,在makePortal() 的末尾return语句前:

// 1
let farWallNode = makeWallNode()

// 2
farWallNode.eulerAngles = SCNVector3(0, 
                                     90.0.degreesToRadians, 0)

// 3
farWallNode.position = SCNVector3(0,
                                  POSITION_Y+WALL_HEIGHT*0.5,
                                  POSITION_Z-SURFACE_LENGTH*0.5)
portal.addChildNode(farWallNode)

代码很直白:

  1. 创建远处墙壁的节点.farWallNode需要遮蔽低一侧.所以使用maskLowerSide的默认值false就可以了.
  2. 给节点设置eulerAngles.因为墙壁是沿Y轴旋转的并垂直于摄像机,所以第二个分量旋转为90度.在X和Z轴方向不旋转.
  3. 设置farWallNode的中心位置,使其高度偏移为POSITION_Y.它的深度计算是:天花板中心点的深度加上从天花板中心点到远端的距离.

创建并运行app,你会看到远处的墙壁上方与天花板相接,下方与地板相接.

接下来你将添加左边和右边的墙壁.在makePortal() 中,在return语句前添加下列代码:

// 1
let rightSideWallNode = makeWallNode(maskLowerSide: true)

// 2
rightSideWallNode.eulerAngles = SCNVector3(0, 180.0.degreesToRadians, 0)

// 3
rightSideWallNode.position = SCNVector3(WALL_LENGTH*0.5,
                              POSITION_Y+WALL_HEIGHT*0.5,
                              POSITION_Z)
portal.addChildNode(rightSideWallNode)

// 4
let leftSideWallNode = makeWallNode(maskLowerSide: true)

// 5
leftSideWallNode.position = SCNVector3(-WALL_LENGTH*0.5,
                            POSITION_Y+WALL_HEIGHT*0.5,
                            POSITION_Z)
portal.addChildNode(leftSideWallNode)

代码解释:

  1. 创建右侧墙壁的节点.你想要将外层墙壁在节点中的低一级,所以你设置maskLowerSidetrue.
  2. 设置墙壁沿Y轴旋转180度.这样确保了墙壁的内侧对着右边.
  3. 设置墙壁的位置,使其与远处墙壁的右侧,天花板,还有地板平齐.将rightSideWallNode添加为protal的子节点.
  4. 类似于右侧墙壁节点,创建一个节点代表左侧墙壁,并设置maskLowerSidetrue.
  5. 左侧墙壁就不需要再旋转了,但你还是需要调整其位置,让它和远处墙壁的左侧,天花板,地板接缝对齐.将左侧墙壁添加为时空门节点的子节点.

编译运行app,你的时空门现在有了三面墙壁了.如果你走出时空门,所有的墙壁都是不可见的.

添加门框通道

还有一件事需要完成:一个入口!目前,时空门还没有第四面墙.其实我们需要的不是第四面墙,还是需要一个能进入和离开的门框通道.

打开PortalViewController.swift并添加下列常量:

let DOOR_WIDTH:CGFloat = 1.0
let DOOR_HEIGHT:CGFloat = 2.4

正如它们的名字含义,它们定义了门框的宽和高.

PortalViewController中添加下列代码:

func addDoorway(node: SCNNode) {
  // 1
  let halfWallLength: CGFloat = WALL_LENGTH * 0.5
  let frontHalfWallLength: CGFloat = 
                   (WALL_LENGTH - DOOR_WIDTH) * 0.5

  // 2
  let rightDoorSideNode = makeWallNode(length: frontHalfWallLength)
  rightDoorSideNode.eulerAngles = SCNVector3(0,270.0.degreesToRadians, 0)
  rightDoorSideNode.position = SCNVector3(halfWallLength - 0.5 * DOOR_WIDTH,
                                          POSITION_Y+WALL_HEIGHT*0.5,
                                          POSITION_Z+SURFACE_LENGTH*0.5)
  node.addChildNode(rightDoorSideNode)

  // 3
  let leftDoorSideNode = makeWallNode(length: frontHalfWallLength)
  leftDoorSideNode.eulerAngles = SCNVector3(0, 270.0.degreesToRadians, 0)
  leftDoorSideNode.position = SCNVector3(-halfWallLength + 0.5 * frontHalfWallLength,
                                         POSITION_Y+WALL_HEIGHT*0.5,
                                         POSITION_Z+SURFACE_LENGTH*0.5)
  node.addChildNode(leftDoorSideNode)
}

addDoorway(node:) 这个方法向指定的node添加一个带有入口的墙壁.
代码解释:

  1. 定义常量来储存墙壁长度的一半,还有门两侧墙壁的长度.
  2. 用上一步声明的常量,创建一个节点来代表入门右侧的墙壁.你还需要调整一下节点的位置和旋转,以使它对准到右侧墙壁,天花板与地板的接缝处.然后将rightDoorSideNode添加到指定node上,成为其子节点.
  3. 同第2步,创建门框通道的左侧节点,设置leftDoorSideNode位置和旋转.最后用addChildNode() 将其添加到node上作为子节点.

makePortalNode() 方法中,return portal前添加下面语句:

addDoorway(node: portal)

这里添加门框通道到时空门节点上.

运行app.你将看到时空门上的门框,但是目前门的上方直通到天花板.我们需要再加一块墙壁来让门框高度达到预告定义的DOOR_HEIGHT.

addDoorway(node:) 方法的末尾添加下面代码:

// 1
let aboveDoorNode = makeWallNode(length: DOOR_WIDTH,
                                 height: WALL_HEIGHT - DOOR_HEIGHT)
// 2                                 
aboveDoorNode.eulerAngles = SCNVector3(0, 270.0.degreesToRadians, 0)
// 3
aboveDoorNode.position =
  SCNVector3(0,
              POSITION_Y+(WALL_HEIGHT-DOOR_HEIGHT)*0.5+DOOR_HEIGHT,
              POSITION_Z+SURFACE_LENGTH*0.5)                                    
node.addChildNode(aboveDoorNode)
  1. 创建一个墙壁节点,尺寸参照上面的入口.
  2. 调整aboveDoorNode的旋转,使它在时空门的前面.掩蔽的面朝外.
  3. 设置节点的位置,使其正好放置在门框通道的上方.将其添加为node的子节点.

运行app.这次你会看到门框通道现在有了合适的墙壁了.

放置灯光

这个时空门看起来并不是太诱人.事实上,它相当暗淡和阴郁.你可以添加一个光源来照亮它们! 添加下列方法到PortalViewController中:

func placeLightSource(rootNode: SCNNode) {
  // 1
  let light = SCNLight()
  light.intensity = 10
  // 2
  light.type = .omni
  // 3
  let lightNode = SCNNode()
  lightNode.light = light
  // 4
  lightNode.position = SCNVector3(0,
                                 POSITION_Y+WALL_HEIGHT,
                                 POSITION_Z)
  rootNode.addChildNode(lightNode)
}

代码解释:

  1. 创建一个SCNLight对象并设置它的intensity(强度).因为我们使用的是physicallyBased(基于物理的) 灯光模型,这个值就是光源的光通量.默认值是1000流明,但你想要一个较低的强度,让它看起来稍暗些.
  2. 灯光类型决定了灯光的形状和方向,同时还有一系列的属性来修改灯光的行为表现.这里,你设置灯光类型为omnidirectional(全方向),也就是点光源灯光.一个全方向灯光强度和方向是固定的.灯光相对于场景中其它物体的位置决定了光的方向.
  3. 创建一个节点来持有灯光,并将light对象附加到节点的light属性上.
  4. 用Y和Z偏移值,将灯放在天花板的中央,然后将lightNode添加为rootNode的子节点. 在makePortal() 中,在return portal之前添加下列代码.
placeLightSource(rootNode: portal)

这样就在时空门里面放置了一个光源. 运行一下app,你将会看到一个更明亮,更吸引人的通道,通往你的虚拟世界!

下一步做什么?

到这里我们的时空门app就完成了!你已经通过创作这个科幻时空门学到了很多.让我们回顾一下这个app涉及到的内容.

  • 你已经对SceneKit的坐标系统和材质有了一个基本了解.
  • 你已经学会如何用不同几何体来创建SCNNode对象,并给他们附加纹理.
  • 你还在场景中放置了光源,让时空门看起来更真实.

如果想更进一步,你还可以做很多:

  • 制作一个门,当用户点击屏幕时打开或关闭.
  • 探索使用更多不同几何体来创建一个房间,这样能无限进入.
  • 试着将通道改为不同形状. 不要止步于此.让你的科幻想像力充分发挥出来!

如果你喜欢本系列教程,请购买本书的完整版,ARKit by Tutorials, available on our online store.

本章资料下载