ARKit教程12_第九章:几何,纹理和灯光

前言

ARKit感兴趣的同学,可以订阅ARKit教程专题
源代码地址在这里

正文

这一章,我们会想办法创建一个逼真的房子:有墙友地板还有天花板的那种。

相关概念介绍

SceneKit坐标系统

SceneKit可用于向视图添加虚拟3D对象。 SceneKit内容视图由节点的分层树结构组成,也称为场景图。一个场景由根节点和其他节点组成,根节点定义场景世界的坐标空间,其他节点用可见内容填充世界。在屏幕上呈现的每个节点或3D对象都是SCNNode类型的对象。 SCNNode对象定义了相对于其父节点的坐标空间变换(位置,方向和比例)。其实它本身没有任何可见内容。

场景中的rootNode对象定义了SceneKit渲染的世界的坐标系。添加到此根节点的每个子节点都会创建自己的坐标系,而该坐标系又由其自己的子节点继承。

SceneKit使用右手坐标系,其中(默认情况下)视图方向沿负Z轴,如下图所示:

ARKit教程12_第九章:几何,纹理和灯光_第1张图片

SCNNode对象的位置是使用SCNVector3定义的,SCNVector3将其定位在其父级的坐标系内。默认位置是零向量,表示该节点位于父节点坐标系的原点。在这种情况下,SCNVector3是一个三分量向量,其中每个分量都是一个Float类型的数值,表示每个轴上的坐标。

SCNNode对象的方向 - 表示为俯仰,偏航和滚转角度 - 由其eulerAngles属性定义。这也由SCNVector3结构表示,其中每个矢量分量是以弧度表示的角度。

纹理

SCNNode对象本身没有任何可见内容。通过将SCNGeometry对象附加到节点,可以将2D3D对象添加到场景中。几何图形附加了确定其外观的SCNMaterial对象。

SCNMaterial具有多个可视属性。每个可视属性都是SCNMaterialProperty类的一个实例,它提供纯色,纹理或其他2D内容。基本着色,基于物理的着色以及可用于使材质看起来更逼真的特殊效果有多种视觉属性。

使用SceneKit,还可以使用附加了SCNLight对象的节点来遮挡场景中具有光照和阴影效果的几何体。

创建门户

让我们直接创建门户的内容。打开SCNNodeHelpers.swift文件并添加以下内容。

let surfaceLength: CGFloat = 3.0
let surfaceHeight: CGFloat = 0.1
let surfaceWidth: CGFloat = 3.0

let scaleX: Float = 2.0
let scaleY: Float = 2.0

let wallWidth: CGFloat = 0.1
let wallHeight: CGFloat = 3.0
let wallLength: CGFloat = 3.0

上面的代码作用如下:

  • 1: 我们可以定义门户的楼层和天花板的尺寸。屋顶和天花板的高度对应于厚度。
  • 2: 这些是在表面上缩放和重复纹理的常量。
  • 3:这些定义了墙壁节点的宽度,高度和长度。

之后我们再添加如下代码:

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

firstMaterial?.diffuse.wrapT = modeRepeat
firstMaterial?.selfIllumination.wrapT = modeRepeat
firstMaterial?.normal.wrapT = modeRepeat
firstMaterial?.specular.wrapT = modeRepeat
firstMaterial?.emission.wrapT = modeRepeat
firstMaterial?.roughness.wrapT = modeRepeat

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

这定义了一种在XY维度上在表面上重复纹理图像的方法。

上面的代码作用如下:

  • 1: 该方法采用SCNGeometry对象以及XY缩放因子作为输入参数。纹理映射使用ST坐标系统,这种坐标系统只是一种命名协议而已:S对应于XT对应于Y。在这里,可以将S维度的包装模式定义为SCNWrapMode.repeat,用于材料的所有可视属性。
  • 2: 我们可以将T维度的包装模式定义为SCNWrapMode.repeat以及所有可视属性。对于重复模式,纹理采样仅使用纹理坐标的小数部分。
  • 3: 这里,每个视觉属性contentsTransform被设置为由SIGMatrix4结构描述。我们可以将XY缩放系数分别设置为scaleXscaleY

我们只想在用户进入门户时显示floor(地板)和ceiling(天花板)节点;任何其他时间,你需要隐藏它们。要实现此功能,需要在SCNNodeHelpers文件中添加如下代码:

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

上面的代码作用如下:

  • 1: 使用floor(地板)和ceiling(天花板)的尺寸创建outerSurface场景框几何对象。
  • 2: 将可见内容添加到框对象的漫反射属性中,这样方便渲染。将透明度设置为较低的值,因此对象将从视图中隐藏。
  • 3: 从outerSurface几何体创建SCNNode对象。将节点的renderingOrder设置为10。最后渲染具有较大渲染顺序的节点。为了使天花板和窗户从门户外部看不见,需要将使内部天花板和地面节点的渲染顺序远大于10

创建地板节点的代码如下:

func makeFloorNode() -> SCNNode{
    let outerFloorNode = makeOuterSurfaceNode(width: surfaceWidth, height: surfaceHeight, length: surfaceLength)
    outerFloorNode.position = SCNVector3(surfaceHeight * 0.5, -surfaceHeight, 0)

    let floorNode = SCNNode()
    floorNode.addChildNode(outerFloorNode)

    let innerFloor = SCNBox(width: surfaceWidth, height: surfaceHeight, length: surfaceLength, chamferRadius: 0)
    innerFloor.firstMaterial?.lightingModel = .physicallyBased
    innerFloor.firstMaterial?.diffuse.contents = UIImage(named: "ARResource.scnassets/floor/textures/FloorDiffuse.png")
    innerFloor.firstMaterial?.normal.contents = UIImage(named: "ARResource.scnassets/floor/textures/FloorNormal.png")
    innerFloor.firstMaterial?.roughness.contents = UIImage(named: "ARResource.scnassets/floor/textures/FloorRoughness.png")
    innerFloor.firstMaterial?.specular.contents = UIImage(named: "ARResource.scnassets/floor/textures/FloorSpecular.png")
    innerFloor.firstMaterial?.selfIllumination.contents = UIImage(named: "ARResource.scnassets/floor/textures/FloorGloss.png")

    repeatTextures(geometry: innerFloor, scaleX: scaleX, scaleY: scaleY)

    let innerFloorNode = SCNNode(geometry: innerFloor)
    innerFloorNode.renderingOrder = 100
    innerFloorNode.position = SCNVector3(surfaceHeight * 0.5, 0, 0)
    floorNode.addChildNode(innerFloorNode)
    return floorNode
   }

上面的代码作用如下:

  • 1:使用floor的尺寸创建floor节点的下侧。
  • 2: 定位outerFloorNode,使其布置在floor节点的底部。将节点添加到floorNode,该节点包含floor的内表面和外表面。
  • 3: 使用先前为每个维度声明的常量初始化的SCNBox对象来创建面板的几何体。
  • 4: 地板材料的lightingModel设置为physicalBased。这种类型的阴影包含了物理灯光和材料的逼真抽象。使用ARResource.scnassets目录中的纹理图像设置材质的各种视觉属性的内容。
  • 5: 使用之前定义的repeatTextures()XY维度上重复材质的纹理。
  • 6: 使用innerFloor几何对象为floor创建节点,并将渲染顺序设置为高于outerFloorNode的渲染顺序。这确保了当用户在门户外部时,该节点将是不可见的。
  • 7: 最后,将innerFloorNode的位置设置为位于outerFloorNode上方,并将其作为子项添加到floorNode。将floor节点对象返回给调用者。

打开ViewController.swift并且添加如下代码:

let positionY: CGFloat = -WALL_HEIGHT*0.5 
let positionZ: CGFloat = -SURFACE_LENGTH*0.5

这些常数表示YZ维度中节点的位置偏移。通过替换makePortal()floor节点添加到门户中。

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()创建floor节点。我们可以使用常量偏移设置floorNode的位置。 SCNGeometry的中心在节点的父坐标系中设置为此位置。
  • 3: 将floorNode添加到门户节点并返回门户节点。请注意,当用户在renderer(_ :, didAdd:, for:)中点击视图时,门户节点将添加到在锚点位置创建的节点。

运行程序效果如下:

你会注意到floor节点是黑暗的。那是因为我们还没有添加光源。

打开SCNNodeHelpers.swift,添加如下代码:

func makeCeilingNode() -> SCNNode{
    let outerCeilingNode = makeOuterSurfaceNode(width: surfaceWidth, height: surfaceHeight, length: surfaceLength)
    outerCeilingNode.position = SCNVector3(surfaceWidth * 0.5, surfaceHeight, 0)
    let ceilingNode = SCNNode()
    ceilingNode.addChildNode(outerCeilingNode)
    let innerCeiling = SCNBox(width: surfaceWidth, height: surfaceHeight, length: surfaceLength, chamferRadius: 0)
    innerCeiling.firstMaterial?.lightingModel = .physicallyBased
    innerCeiling.firstMaterial?.diffuse.contents = UIImage(named: "ARResource.scnassets/ceiling/textures/CeilingDiffuse.png")
    innerCeiling.firstMaterial?.emission.contents = UIImage(named: "ARResource.scnassets/ceiling/textures/CeilingEmis.png")
   innerCeiling.firstMaterial?.normal.contents = UIImage(named: "ARResource.scnassets/ceiling/textures/CeilingNormal.png")
    innerCeiling.firstMaterial?.specular.contents = UIImage(named: "ARResource.scnassets/ceiling/textures/CeilingSpecular.png")
    innerCeiling.firstMaterial?.selfIllumination.contents = UIImage(named: "ARResource.scnassets/ceiling/textures/CeilingGloss.png")
    repeatTextures(geometry: innerCeiling, scaleX: scaleX, scaleY: scaleY)

    let innerCeilingNode = SCNNode(geometry: innerCeiling)
    innerCeilingNode.renderingOrder = 100
    innerCeilingNode.position = SCNVector3(surfaceHeight * 0.5, 0, 0)
    ceilingNode.addChildNode(innerCeilingNode)
    return ceilingNode
}

上面的代码作用如下:

  • 1: 与floor类似,我们创建一个带有天花板尺寸的outerCeilingNode
  • 2: 设置外部天花板节点的位置,使其位于天花板顶部。创建一个节点来固定天花板的内侧和外侧。添加outerCeilingNode作为ceilingNode的子节点。
  • 3: 使innerCeiling成为具有相应尺寸的SCNBox对象。
  • 4: 将lightingModel设置为physicalBased。此外,设置ARResource.scnassets目录中的各种纹理图像定义的可视属性的内容。
  • 5: repeatTextures()XY维度中包裹纹理图像,以创建天花板的重复图案。
  • 6: 使用innerCeiling几何创建innerCeilingNode,并将其renderingOrder属性设置为一个比较大的值,以便在outerCeilingNode之后呈现它。
  • 7: 将innerCeilingNode放在其父节点中,并将其添加为ceilingNode的子节点。返回ceilingNode给调用者。

现在从某个地方调用它。打开PortalViewController.swift并在return语句之前将以下代码块添加到makePortal():

// 1 
let ceilingNode = makeCeilingNode() 
ceilingNode.position = SCNVector3(0, POSITION_Y+WALL_HEIGHT, POSITION_Z) 
// 2 
portal.addChildNode(ceilingNode)

上面的代码作用如下:

  • 1:使用刚刚定义的makeCeilingNode()创建天花板节点。将ceilingNode中心的位置设置为SCNVector3结构。中心的Y坐标被添加到墙壁高度的地板的Y位置偏移。
  • 2:添加ceilingNode作为门户的子节点。

运行程序,显示效果如下:

往下看是地板,上面是天花板,就差墙了。接下来,砌墙!

打开SCNNodeHelpers.swift文件,添加以下代码:

func makeWallNode(length: CGFloat = wallLength, height: CGFloat = wallHeight, maskLowerSide: Bool = false) -> SCNNode{
    let outerWall = SCNBox(width: wallWidth, height: height, length: length, chamferRadius: 0)
    outerWall.firstMaterial?.diffuse.contents = UIColor.white
    outerWall.firstMaterial?.transparency = 0.000001
    let outerWallNode = SCNNode(geometry: outerWall)
    let multiplier: CGFloat = maskLowerSide ? -1 : 1
outerWallNode.position = SCNVector3(wallWidth * multiplier, 0, 0)
    outerWallNode.renderingOrder = 10
    let wallNode = SCNNode()
    wallNode.addChildNode(outerWallNode)
    let innerWall = SCNBox(width: wallWidth, height: height, length: length, chamferRadius: 0)
    innerWall.firstMaterial?.lightingModel = .physicallyBased
    innerWall.firstMaterial?.diffuse.contents = UIImage(named: "ARResource.scnassets/wall/textures/WallsDiffuse.png")
    innerWall.firstMaterial?.metalness.contents = UIImage(named: "ARResource.scnassets/wall/textures/WallsMetalness.png")
    innerWall.firstMaterial?.roughness.contents = UIImage(named: "ARResource.scnassets/wall/textures/WallsRoughness.png")
    innerWall.firstMaterial?.normal.contents = UIImage(named: "ARResource.scnassets/wall/textures/WallsNormal.png")
    innerWall.firstMaterial?.specular.contents = UIImage(named: "ARResource.scnassets/wall/textures/WallsSpec.png")
    innerWall.firstMaterial?.selfIllumination.contents = UIImage(named: "ARResource.scnassets/wall/textures/WallsGloss.png")
    let innerWallNode = SCNNode(geometry: innerWall)
    innerWallNode.renderingOrder = 100
    wallNode.addChildNode(innerWallNode)
    return wallNode
}

上面的代码作用如下:

  • 1: 创建一个外墙节点,该节点将位于墙的外侧,使其从外部看起来是透明的。创建一个与墙的尺寸匹配的SCNBox对象。
  • 2: 可以将材质的漫反射内容设置为单色白色,将透明度设置为较低的数字。如果从房间外面看墙,这有助于实现透视效果。
  • 3: 使用outerWall几何创建节点。根据需要渲染外墙的墙的哪一侧设置乘数。如果maskLowerSide设置为true,则外墙位于墙节点坐标系中的内壁下方;否则,它将放在上面。

    我们可以设置节点的位置,使外墙偏移X维度中的墙宽度。将外墙的渲染顺序设置为较小的数字,以便首先渲染它。这使得墙壁从外面看不见。
  • 4:还可以创建一个节点来保存墙,并将outerWallNode添加为其子节点。
  • 5:使innerWall成为具有相应墙尺寸的SCNBox对象。
  • 6:将lightingModel设置为physicalBased。与天花板和地面节点类似,我们可以设置由墙壁的各种纹理图像定义的视觉属性的内容。
  • 7: 最后,使用innerWall几何创建innerWallNode对象。将此节点添加到父wallNode对象。默认情况下,innerWallNode位于wallNode的原点。将节点返回给调用者。

现在,打开ViewController.swift,之后在makePortal()函数中添加如下代码:

  • 1: 为远墙创建一个节点。 farWallNode需要下方的掩码。所以maskLowerSide的默认值为false
  • 2: 将eulerAngles添加到节点。由于壁沿Y轴旋转并垂直于摄像机,因此第二部件的旋转角度为90度。墙壁没有X轴和Z轴的旋转角度。
  • 3: 设置farWallNode中心的位置,使其高度偏移positionY。它的深度是通过将天花板中心的深度加到从天花板中心到远端的距离来计算的。

现在只有一面墙,我们需要把其他几面墙也添加上:

    let rightSideWallNode = makeWallNode(maskLowerSide: true)
    rightSideWallNode.eulerAngles = SCNVector3(0, 180.0.degreeToRadians, 0)
    rightSideWallNode.position = SCNVector3(wallLength*0.5,
                                            positionY+wallHeight*0.5,
                                            positionZ)
    portal.addChildNode(rightSideWallNode)
    
    let leftSideWallNode = makeWallNode(maskLowerSide: true)
    leftSideWallNode.position = SCNVector3(-wallLength*0.5,
                                           positionY+wallHeight*0.5,
                                           positionZ)
    portal.addChildNode(leftSideWallNode)

上面代码作用如下:

  • 1: 为右墙创建一个节点。我们希望将外墙放在节点的下边,因此将maskLowerSide设置为true
  • 2:我们可以将墙的旋转沿Y轴设置为180度。这确保了墙壁的内侧朝向正确的方向。
  • 3: 设置墙壁的位置,使其与远墙,天花板和地板的右边缘相连。将rightSideWallNode添加为门户的子节点。
  • 4: 与右墙节点类似,创建一个节点来表示左墙,并将maskLowerSide设置为true
  • 5: 左墙没有任何旋转,但调整它的位置,以便它与远墙,天花板和天花板的左边缘相连。我们将左墙节点添加为门户节点的子节点。

运行程序,效果如下:

添加门口

现在三面墙都有了,最后一面墙应该留出来点空间,充当一个门。我们需要先定义一个门的宽度和高度:

let doorWidth: CGFloat = 1.0
let doorHeight: CGFloat = 2.4

然后添加一个addDoorway(node: SCNNode)方法:

func addDoorway(node: SCNNode){
    let halfWalllength: CGFloat = wallLength * 0.5
    let frontHalfWallLength: CGFloat = (wallLength - doorWidth) * 0.5
    let rightDoorSideNode = makeWallNode(length: frontHalfWallLength)
    rightDoorSideNode.eulerAngles = SCNVector3(0, 270.0.degreeToRadians, 0)
    rightDoorSideNode.position = SCNVector3(halfWalllength - 0.5 * doorWidth, positionY + wallLength * 0.5, positionZ + surfaceLength * 0.5)
    node.addChildNode(rightDoorSideNode)
    
    let leftDoorSideNode = makeWallNode(length: frontHalfWallLength)
    leftDoorSideNode.eulerAngles = SCNVector3(0, 270.0.degreeToRadians, 0)
    leftDoorSideNode.position = SCNVector3(-halfWalllength + 0.5 * frontHalfWallLength, positionY + wallHeight * 0.5, positionZ + surfaceLength * 0.5)
    node.addChildNode(leftDoorSideNode)
    
    let aboveDoorNode = makeWallNode(length: doorWidth, height: wallHeight - doorHeight)
    aboveDoorNode.eulerAngles = SCNVector3(0, 270.0.degreeToRadians, 0)
    aboveDoorNode.position = SCNVector3(0, positionY + (wallHeight - doorHeight) * 0.5 + doorHeight, positionZ + surfaceLength * 0.5)
    node.addChildNode(aboveDoorNode)
}

上面的代码作用如下:

  • 1: 定义用于存储门两侧的半壁长度和前壁长度的常数。
  • 2: 使用上一步中声明的常量创建一个节点来表示入口右侧的墙。我们还可以调整节点的旋转和位置,使其连接到右墙,天花板和地板的前边缘。然后,将rightDoorSideNode添加为给定节点的子节点。
  • 3: 与步骤2类似,我们可以为门口左侧创建一个节点,并设置leftDoorSideNode的旋转和位置,使其与左侧墙壁,天花板和地面节点的前边缘相连。最后,使用addChildNode()将其作为子节点添加到节点。

之后在makePortal()中添加下面的代码:

addDoorway(node: portal)

构建并运行应用程序。你会看到门口的门口,但门的顶部正在接触天花板。我们需要添加另一块墙,以使门口跨越预先设定的doorHeight

我们在addDoorway(node:)函数中添加如下代码:

let aboveDoorNode = makeWallNode(length: doorWidth, height: wallHeight - doorHeight)
aboveDoorNode.eulerAngles = SCNVector3(0, 270.0.degreeToRadians, 0)
aboveDoorNode.position = SCNVector3(0, positionY + (wallHeight - doorHeight) * 0.5 + doorHeight, positionZ + surfaceLength * 0.5)
node.addChildNode(aboveDoorNode)

上面的代码作用如下:

  • 1: 创建一个具有相应尺寸的墙节点,使其位于门户入口上方。
  • 2: 调整aboveDoorNode的旋转,使其位于门户的前面。蒙面侧放在外面。
  • 3: 设置节点的位置,使其位于我们刚刚构建的门口的顶部。将其添加为节点的子节点。

运行程序效果如下:

添加灯光效果

添加灯光代码如下:

func placeLightSource(rootNode: SCNNode){
    let light = SCNLight()
    light.intensity = 10
    light.type = .omni
    let lightNode = SCNNode()
    lightNode.light = light
    lightNode.position = SCNVector3(0, positionY + wallHeight, positionZ)
    rootNode.addChildNode(lightNode)
}

上面的代码作用如下:

  • 1: 创建一个SCNLight对象并设置其强度。由于我们使用的是基于物理的照明模型,因此该值是光源的发光流量。默认值为1000流明,但是我们希望强度更低,使其外观稍暗。
  • 2: 灯光类型决定灯光提供的照明形状和方向,以及可用于修改灯光行为的属性集。在这里,我们可以将光的类型设置为全向,也称为点光源。全向光具有恒定的强度和方向。灯光相对于场景中其他物体的位置决定了它的方向。
  • 3: 我们可以创建一个节点来保持灯光,并使用灯光属性将灯光对象附加到节点。
  • 4: 使用YZ偏移将光放在天花板的中心,然后添加lightNode作为rootNode的子节点。

makePortal()方法中添加如下代码:

placeLightSource(rootNode: portal)

运行程序,效果如下:

这下灯光也有了。

上一章 目录 下一章

你可能感兴趣的:(ARKit教程12_第九章:几何,纹理和灯光)