How to use SCNAvoidOccluderConstraint (any example)

Viewed 286

Does anyone have example of using SCNAvoidOccluderConstraint?

The only description, which I found is:

@abstract A SCNAvoidOccluderConstraint constraints place the receiver at a position that prevent nodes with the specified category to occlude the target.

@discussion The target node and it's children are ignored as potential occluders.

UPDATE: Xcode 9 was officially released and still no one line in documentation.

2 Answers

Coming late, but here is a working example (in Python, but can easily be reproduced in Swift or ObjC). The ball with the SCNAvoidOccluderConstraint on it jumps back on its trajectory whenever the block in the middle obstructs the view to the other ball.

"""
avoid occluder demo
"""

from objc_util import *
import sceneKit as scn
import ui
import math

def dot(v1, v2):
  return sum(x*y for x,y in zip(list(v1),list(v2)))

def det2(v1, v2):
  return v1[0]*v2[1] - v1[1]*v2[0]

class Demo:

  @classmethod
  def run(cls):
    cls().main()

  @on_main_thread
  def main(self):
    main_view = ui.View()
    w, h = ui.get_screen_size()
    main_view.frame = (0,0,w,h)
    main_view.name = 'avoid occluder demo'

    scene_view = scn.View(main_view.frame, superView=main_view)
    scene_view.autoresizingMask = scn.ViewAutoresizing.FlexibleHeight | scn.ViewAutoresizing.FlexibleWidth
    scene_view.allowsCameraControl = True
    scene_view.delegate = self
    scene_view.backgroundColor = 'white'
    scene_view.rendersContinuously = True
    scene_view.scene = scn.Scene()

    root_node = scene_view.scene.rootNode

    floor_geometry = scn.Floor()
    floor_node = scn.Node.nodeWithGeometry(floor_geometry)
    root_node.addChildNode(floor_node)

    ball_radius = 0.2
    ball_geometry = scn.Sphere(radius=ball_radius)
    ball_geometry.firstMaterial.diffuse.contents = (.48, .48, .48)
    ball_geometry.firstMaterial.specular.contents = (.88, .88, .88)
    self.ball_node_1 = scn.Node.nodeWithGeometry(ball_geometry)
    self.ball_node_2 = scn.Node.nodeWithGeometry(ball_geometry)

    root_node.addChildNode(self.ball_node_1)
    root_node.addChildNode(self.ball_node_2)

    occluder_geometry = scn.Box(0.3, 2., 15., 0.2)
    occluder_geometry.firstMaterial.diffuse.contents = (.91, .91, .91)
    occluder_node = scn.Node.nodeWithGeometry(occluder_geometry)
    occluder_node.position = (0., 0.8, 0.)
    root_node.addChildNode(occluder_node)

    self.orbit_r = 10
    self.omega_speed_1 = math.pi/1500
    self.omega_speed_2 = 1.5*self.omega_speed_1
    self.ball_node_1.position = (self.orbit_r, 0.5, 0.)
    self.ball_node_2.position = (0., 0.5, self.orbit_r)

    constraint = scn.AvoidOccluderConstraint.avoidOccluderConstraintWithTarget(self.ball_node_1)
    self.ball_node_2.constraints = [constraint]

    camera_node = scn.Node()
    camera_node.camera = scn.Camera()
    camera_node.position = (0.5*self.orbit_r , 0.5*self.orbit_r, 1.5*self.orbit_r)
    camera_node.lookAt(root_node.position)
    root_node.addChildNode(camera_node)

    light_node = scn.Node()
    light_node.position = (self.orbit_r, self.orbit_r, self.orbit_r)
    light = scn.Light()
    light.type = scn.LightTypeDirectional
    light.castsShadow = True
    light.shadowSampleCount = 32
    light.color = (.99, 1.0, .86)
    light_node.light = light
    light_node.lookAt(root_node.position)
    root_node.addChildNode(light_node)

    main_view.present(hide_title_bar=False)

  def update(self, view, atTime):
    pos_1 = self.ball_node_1.presentationNode.position
    pos_2 = self.ball_node_2.presentationNode.position
    self.omega_1 = -math.atan2(det2((pos_1.x, pos_1.z), (1., 0.)), dot((pos_1.x, pos_1.z), (1., 0.)))
    self.omega_2 = -math.atan2(det2((pos_2.x, pos_2.z), (1., 0.)), dot((pos_2.x, pos_2.z), (1., 0.)))
    self.omega_1 += self.omega_speed_1
    self.omega_2 += self.omega_speed_2
    self.ball_node_1.position = (self.orbit_r*math.cos(self.omega_1), 0.5, self.orbit_r*math.sin(self.omega_1))
    self.ball_node_2.position = (self.orbit_r*math.cos(self.omega_2), 0.5, self.orbit_r*math.sin(self.omega_2))

Demo.run()

I came across this oldie recently hoping to get more insight if this constaint could be used to solve a particular problem. Unfortunately, Apple's documentation is still atrocious so I'm leaving some more notes here for posterity.

SCNAvoidOccluderConstraint takes an SCNNode as a target. If an object obstructs the line of sight between the target node and the node that you added this constraint to, the node with the constraint will jump to the nearest point between its original position and the target to re-establish its line of sight with the target node.

In most cases, this point will be immediately on the other side of the obstructing object, so your constrained object will not move all the way through to the other side of the obstructing object; its center point will be aligned with the occluder's visible edge with respect to the target.

Furthermore, you can use a SCNNode's categoryBitMask and the constraint's occluderCategoryBitMask to exclude certain nodes from being occluders.

Here's a loose adaptation of @pulbrich 's original answer in Swift which illustrates the usage. You can paste this in the default XCode SceneKit Game project's GameViewController.swift file:

import SceneKit
import QuartzCore

class GameViewController: NSViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let scene = SCNScene()
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        scene.rootNode.addChildNode(cameraNode)
        cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
        cameraNode.look(at: SCNVector3(0,0,0))
        let lightNode = SCNNode()
        lightNode.light = SCNLight()
        lightNode.light!.type = .omni
        lightNode.position = SCNVector3(x: 0, y: 10, z: 0)
        scene.rootNode.addChildNode(lightNode)
        let ambientLightNode = SCNNode()
        ambientLightNode.light = SCNLight()
        ambientLightNode.light!.type = .ambient
        ambientLightNode.light!.color = NSColor.darkGray
        scene.rootNode.addChildNode(ambientLightNode)
        let scnView = self.view as! SCNView
        scnView.scene = scene
        scnView.allowsCameraControl = true
        scnView.showsStatistics = true
        scnView.backgroundColor = NSColor.black
        
        //--------------------------

        let ball_radius = 0.2
        let ball_geometry = SCNSphere(radius: ball_radius)
        let ball_node_1 = SCNNode.init(geometry: ball_geometry)
        ball_node_1.name = "b1"
        let ball_node_2 = SCNNode.init(geometry:ball_geometry)
        ball_node_2.name = "b2"
        
        scene.rootNode.addChildNode(ball_node_1)
        scene.rootNode.addChildNode(ball_node_2)
        
        ball_node_1.worldPosition = SCNVector3(5, 5, 0)
        ball_node_2.worldPosition = SCNVector3(5, -5, 0)
        
        let occluder_geometry = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0)
        let occluder_node = SCNNode.init(geometry: occluder_geometry)
        scene.rootNode.addChildNode(occluder_node)

        let constraint = SCNAvoidOccluderConstraint(target: ball_node_1)
        ball_node_2.constraints = [constraint]
        
        let a1 = CABasicAnimation(keyPath: "position")
        a1.toValue = SCNVector3(-5,5,0)
        a1.duration = 5
        a1.autoreverses = true
        a1.repeatCount = .infinity
        ball_node_1.addAnimation(a1, forKey: "move1")
        
        //IMPORTANT NOTE: this constraint will hose CABasicAnimation,
        //but the presense of the animation will snap it back to the toValue
        //vector when the target is no longer occluded
        let a2 = CABasicAnimation(keyPath: "position")
        a2.toValue = SCNVector3(5,-5,0)
        //a2.toValue = SCNVector3(-5,-5,0)
        a2.duration = 5
        a2.autoreverses = true
        a2.repeatCount = .infinity
        ball_node_2.addAnimation(a2, forKey: "move2")
    }
}
Related