Chapter 9: Geometry, Textures & Lighting

大綱

上一章學到如何利用SceneKit在Scene中增加一個3D物件,接下來要做個比較複雜的3D物件。

要做個房間,這個房間由下列東西組成

  • 房間有天花板,牆壁,地板,當然還要有個入口。

  • 利用不同的textures讓物件更加真實。

  • 在scene中增加光源。

摘要

The SceneKit coordinate system

在之前番外篇就有提到關於座標系統,這一章最困難部分就是要清楚了解座標系統,才有辦法順利組成這個房間。

番外篇

Textures

在SceneKit asset catalog已經有提供不同的textures可以使用。

Building the portal

準備完成這章最主要的app, 透過SceneKit建立一個栩栩如生的房間,會先分下列步驟

  • 將texture image覆蓋到物件上。

// input是個geometry並且還有對應的scaleX, scaleY
func repeatTextures(geometry: SCNGeometry, scaleX: Float, scaleY: Float) {
  // wrapS的"S"表示是對應X的座標系統
  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

  // wrapT的"T"表示是對應Y的座標系統
  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

  // contentsTransform用來設定scale transform, 單位是SCNMatrix4
  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)
}
  • 只有在進到房間內,才會看到天花板,地板,其餘時候要隱藏。

func makeOuterSurfaceNode(width: CGFloat, height: CGFloat, length: CGFloat) -> SCNNode {
  // 建立房間的外層
  let outerSurface = SCNBox(width: width, height: height, length: length, chamferRadius: 0)

  // 利用很小的transparency來隱藏這個外層
  outerSurface.firstMaterial?.diffuse.contents = UIColor.white
  outerSurface.firstMaterial?.transparency = 0.000001

  // renderingOrder越大,表示越晚被render
  // portal外面時,要讓天花板跟牆是看不到,此時要將天花板跟牆的renderingOrder的值大於10
  let outerSurfaceNode = SCNNode(geometry: outerSurface)
  outerSurfaceNode.renderingOrder = 10

  return outerSurfaceNode
}
  • 建立地板。

// 建立地板
func makeFloorNode() -> SCNNode {
  // 建立地板的外層
  let outerFloorNode = makeOuterSurfaceNode(width: SURFACE_WIDTH, height: SURFACE_HEIGHT, length: SURFACE_LENGTH)

  // 設立地板外層位置
  outerFloorNode.position = SCNVector3(SURFACE_HEIGHT * 0.5, -SURFACE_HEIGHT, 0)
  let floorNode = SCNNode()
  floorNode.addChildNode(outerFloorNode)

  // 建立地板的內層
  let innerFloor = SCNBox(width: SURFACE_WIDTH, height: SURFACE_HEIGHT, length: SURFACE_LENGTH, chamferRadius: 0)

  // physicallyBased的燈光,可以讓Material的看起來更真實
  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")

  // 將Textures重複採樣貼到地板內層上
  repeatTextures(geometry: innerFloor, scaleX: SCALEX, scaleY: SCALEY)

  // renderingOrder的數值比outerFloorNode高,確保當使用者在portal外面,這個FloorNode是不會被看到
  let innerFloorNode = SCNNode(geometry: innerFloor)
  innerFloorNode.renderingOrder = 100

  // 設立地板內層位置
  innerFloorNode.position = SCNVector3(SURFACE_HEIGHT * 0.5, 0, 0)
  floorNode.addChildNode(innerFloorNode)

  // 此時floorNode有兩個child nodes: outerFloorNode, innerFloorNode
  // 座標系統是以floorNode為中心的座標
  return floorNode
}

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


  let floorNode = makeFloorNode()
  floorNode.position = SCNVector3(0, POSITION_Y, POSITION_Z)
  portal.addChildNode(floorNode)
  • 建立天花板。

// 建立天花板
func makeCeilingNode() -> SCNNode {
  // 建立天花板的外層
  let outerCeilingNode = makeOuterSurfaceNode(width: SURFACE_WIDTH, height: SURFACE_HEIGHT, length: SURFACE_LENGTH)

  // 設立天花板外層位置
  outerCeilingNode.position = SCNVector3(SURFACE_HEIGHT * 0.5, SURFACE_HEIGHT, 0)
  let ceilingNode = SCNNode()
  ceilingNode.addChildNode(outerCeilingNode)

  // 建立天花板的內層
  let innerCeiling = SCNBox(width: SURFACE_WIDTH, height: SURFACE_HEIGHT, length: SURFACE_LENGTH, chamferRadius: 0)

  // physicallyBased的燈光,可以讓Material的看起來更真實
  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")

  // 將Textures重複採樣貼到天花板內層上
  repeatTextures(geometry: innerCeiling, scaleX:
    SCALEX, scaleY: SCALEY)

  let innerCeilingNode = SCNNode(geometry: innerCeiling)
  innerCeilingNode.renderingOrder = 100

  innerCeilingNode.position = SCNVector3(SURFACE_HEIGHT * 0.5, 0, 0)
  ceilingNode.addChildNode(innerCeilingNode)

  // 此時ceilingNode有兩個child nodes: outerCeilingNode, innerCeilingNode
  // 座標系統是以ceilingNode為中心的座標
  return ceilingNode
}


 let ceilingNode = makeCeilingNode()
 ceilingNode.position = SCNVector3(0, POSITION_Y+WALL_HEIGHT, POSITION_Z)
 portal.addChildNode(ceilingNode)
  • 建立3面牆面。

// 建立牆壁
func makeWallNode(length: CGFloat = WALL_LENGTH, height: CGFloat = WALL_HEIGHT, maskLowerSide:Bool = false) -> SCNNode {

  // 建立外牆的geometry
  let outerWall = SCNBox(width: WALL_WIDTH, height: height, length: length, chamferRadius: 0)
  outerWall.firstMaterial?.diffuse.contents = UIColor.white
  outerWall.firstMaterial?.transparency = 0.000001

  // 建立外牆node
  let outerWallNode = SCNNode(geometry: outerWall)
  // If maskLowerSide is set to true, the outer wall is placed below the inner wall in the wall node’s coordinate system; otherwise, it’s placed above
  let multiplier: CGFloat = maskLowerSide ? -1 : 1
  outerWallNode.position = SCNVector3(WALL_WIDTH*multiplier,0,0)
  outerWallNode.renderingOrder = 10

  //
  let wallNode = SCNNode()
  wallNode.addChildNode(outerWallNode)

  // 建立內牆
  let innerWall = SCNBox(width: WALL_WIDTH, height: height, length: length, chamferRadius: 0)

  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")

  let innerWallNode = SCNNode(geometry: innerWall)
  wallNode.addChildNode(innerWallNode)

  return wallNode
}

    let farWallNode = makeWallNode()
    farWallNode.eulerAngles = SCNVector3(0, 90.0.degreesToRadians, 0)
    farWallNode.position = SCNVector3(0, POSITION_Y+WALL_HEIGHT*0.5, POSITION_Z-SURFACE_LENGTH*0.5)
    portal.addChildNode(farWallNode)

    let rightSideWallNode = makeWallNode(maskLowerSide: true)
    rightSideWallNode.eulerAngles = SCNVector3(0, 180.0.degreesToRadians, 0)
    rightSideWallNode.position = SCNVector3(WALL_LENGTH*0.5, POSITION_Y+WALL_HEIGHT*0.5, POSITION_Z)
    portal.addChildNode(rightSideWallNode)

    let leftSideWallNode = makeWallNode(maskLowerSide: true)
    leftSideWallNode.position = SCNVector3(-WALL_LENGTH*0.5, POSITION_Y+WALL_HEIGHT*0.5, POSITION_Z)
    portal.addChildNode(leftSideWallNode)

Adding the doorway

接下來,是比較複雜的是建立房間的入口處,現在房間已經有三面牆,還差一面牆,但這面牆要有入口,要像個"ㄇ"字型的入口,但目前沒辦法做到將一個box的立方體挖個洞變成"ㄇ"字型,所以只好將"ㄇ"拆成"|","-","|"三段,然後組合起來。

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


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

    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)

    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)

    let aboveDoorNode = makeWallNode(length: DOOR_WIDTH,
                                     height: WALL_HEIGHT - DOOR_HEIGHT)

    aboveDoorNode.eulerAngles = SCNVector3(0, 270.0.degreesToRadians, 0)
    aboveDoorNode.position =
      SCNVector3(0,
                 POSITION_Y+(WALL_HEIGHT-DOOR_HEIGHT)*0.5+DOOR_HEIGHT,
                 POSITION_Z+SURFACE_LENGTH*0.5)
    node.addChildNode(aboveDoorNode)
  }

Placing lights

最後一步,就是置放光源,讓整個場景更加的真實。

  func placeLightSource(rootNode: SCNNode) {
    // 建立SCNLight並設定強度,預設強度1000
    let light = SCNLight()
    light.intensity = 10
    // omnidirectional 無向性光源, 光源向四面八方發射
    light.type = .omni
    // 建立node, 並指定其光源
    let lightNode = SCNNode()
    lightNode.light = light
    // 指定位置
    lightNode.position = SCNVector3(0,
                                    POSITION_Y+WALL_HEIGHT,
                                    POSITION_Z)
    rootNode.addChildNode(lightNode)
  }

Last updated

Was this helpful?