Home Reference Source Repository

js/SceneKit/SCNLight.js

'use strict'

import NSObject from '../ObjectiveC/NSObject'
//import SCNAnimatable from './SCNAnimatable'
//import SCNTechniqueSupport from './SCNTechniqueSupport'
import SCNMaterialProperty from './SCNMaterialProperty'
import SCNMatrix4 from './SCNMatrix4'
import CGSize from '../CoreGraphics/CGSize'
import SKColor from '../SpriteKit/SKColor'
//import SCNShadowMode from './SCNShadowMode'

const _LightType = {
  IES: 'ies',
  ambient: 'ambient',
  directional: 'directional',
  omni: 'omni',
  probe: 'probe',
  spot: 'spot'
}

/**
 * A light source that can be attached to a node to illuminate the scene.
 * @access public
 * @extends {NSObject}
 * @implements {SCNAnimatable}
 * @implements {SCNTechniqueSupport}
 * @see https://developer.apple.com/documentation/scenekit/scnlight
 */
export default class SCNLight extends NSObject {
  static get _propTypes() {
    return {
      type: 'string',
      color: 'plist',
      temperature: 'float',
      intensity: 'float',
      name: 'string',
      attenuationStartDistance: 'float',
      attenuationEndDistance: 'float',
      attenuationFalloffExponent: 'float',
      spotInnerAngle: 'float',
      spotOuterAngle: 'float',
      gobo: ['SCNMaterialProperty', '_gobo'],
      castsShadow: 'boolean',
      shadowRadius: 'float',
      shadowColor: 'plist',
      shadowMapSize: 'CGSize',
      shadowSampleCount: 'integer',
      shadowMode: 'integer',
      shadowBias: 'float',
      orthographicScale: 'float',
      zFar: 'float',
      zNear: 'float',
      lightCategoryBitMask: ['integer', 'categoryBitMask'],
      automaticallyAdjustsShadowProjection: 'boolean',
      forcesBackFaceCasters: 'boolean',
      maximumShadowDistance: 'float',
      sampleDistributedShadowMaps: 'boolean',
      shadowCascadeCount: 'integer',
      shadowCascadeSplittingFactor: 'float',

      entityID: ['string', '_entityID'],
      version: ['float', null],
      spotFallOffExponent: ['float', null],
      usesDeferredShadows: ['boolean', null],
      usesModulatedMode: ['boolean', null],
      shouldBakeIndirectLighting: ['boolean', null],
      shouldBakeDirectLighting: ['boolean', null],
      baked: ['boolean', null],
      goboProjectShadows: ['boolean', null],
      shadowSampleCount2: ['integer', null],
      sphericalHarmonics: ['NSMutableData', null],
      autoShadowProjection: ['boolean', null]
    }
  }

  // Creating a Light

  /**
   * Creates a light from the specified Model I/O light object.
   * @access public
   * @constructor
   * @param {?MDLLight} [mdlLight = null] - A Model I/O light object.
   * @desc The Model I/O framework provides universal support for import, export, description, and processing of several 3D asset file formats and related resources. (For details, see Model I/O.) The MDLLight class is a generic description of a light source in a scene, supporting a superset of the attributes described by the SCNLight class.
   * @see https://developer.apple.com/documentation/scenekit/scnlight/1419849-init
   */
  constructor(mdlLight = null) {
    super()

    // Modifying a Light’s Appearance

    /**
     * A constant identifying the general behavior of the light.
     * @type {SCNLight.LightType}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1522919-type
     */
    this.type = _LightType.omni

    /**
     * The color of the light. Animatable.
     * @type {SKColor}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1523627-color
     */
    this.color = new SKColor(1, 1, 1, 1)

    /**
     * The color temperature, in degrees Kelvin, of the light source. Animatable.
     * @type {number}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1640545-temperature
     */
    this.temperature = 6500.0

    /**
     * The luminous flux, in lumens, or total brightness of the light. Animatable.
     * @type {number}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1640548-intensity
     */
    this.intensity = 1000.0


    // Managing Light Attributes

    /**
     * A name associated with the light.
     * @type {?string}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1522839-name
     */
    this.name = null


    // Managing Light Attenuation

    /**
     * The distance from the light at which its intensity begins to diminish. Animatable.
     * @type {number}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1524223-attenuationstartdistance
     */
    this.attenuationStartDistance = 0

    /**
     * The distance from the light at which its intensity is completely diminished. Animatable.
     * @type {number}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1524140-attenuationenddistance
     */
    this.attenuationEndDistance = 0

    /**
     * The transition curve for the light’s intensity between its attenuation start and end distances. Animatable.
     * @type {number}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1522879-attenuationfalloffexponent
     */
    this.attenuationFalloffExponent = 0


    // Managing Spotlight Extent

    /**
     * The angle, in degrees, of the area fully lit by a spotlight. Animatable.
     * @type {number}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1522797-spotinnerangle
     */
    this.spotInnerAngle = 0

    /**
     * The angle, in degrees, of the area partially lit by a spotlight. Animatable.
     * @type {number}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1523382-spotouterangle
     */
    this.spotOuterAngle = 45.0

    this._gobo = new SCNMaterialProperty()

    // Managing Shadows Cast by the Light

    /**
     * A Boolean value that determines whether the light casts shadows.
     * @type {boolean}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1523816-castsshadow
     */
    this.castsShadow = false

    /**
     * A number that specifies the amount of blurring around the edges of shadows cast by the light. Animatable.
     * @type {number}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1523724-shadowradius
     */
    this.shadowRadius = 3.0

    /**
     * The color of shadows cast by the light. Animatable.
     * @type {SKColor}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1522864-shadowcolor
     */
    this.shadowColor = new SKColor(0, 0, 0, 1)

    /**
     * The size of the shadow map image that SceneKit renders when creating shadows.
     * @type {CGSize}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1524127-shadowmapsize
     */
    this.shadowMapSize = new CGSize(0, 0)

    /**
     * The number of samples from the shadow map that SceneKit uses to render each pixel.
     * @type {number}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1523300-shadowsamplecount
     */
    this.shadowSampleCount = 0

    /**
     * The mode SceneKit uses to render shadows.
     * @type {SCNShadowMode}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1522847-shadowmode
     */
    this.shadowMode = null

    /**
     * The amount of correction to apply to the shadow to prevent rendering artifacts.
     * @type {number}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1522849-shadowbias
     */
    this.shadowBias = 1.0

    /**
     * The orthographic scale SceneKit uses when rendering the shadow map for a directional light.
     * @type {number}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1523951-orthographicscale
     */
    this.orthographicScale = 1.0

    /**
     * The maximum distance between the light and a visible surface for casting shadows.
     * @type {number}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1522845-zfar
     */
    this.zFar = 100.0

    /**
     * The minimum distance between the light and a visible surface for casting shadows. Animatable.
     * @type {number}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1522630-znear
     */
    this.zNear = 1.0


    // Choosing Nodes to be Illuminated by the Light

    /**
     * A mask that defines which categories this light belongs to.
     * @type {number}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1523669-categorybitmask
     */
    this.categoryBitMask = -1


    // Managing Photometric Lights

    /**
     * The URL for a file that contains photometry data describing the intended appearance of the light.
     * @type {?string}
     * @see https://developer.apple.com/documentation/scenekit/scnlight/1640546-iesprofileurl
     */
    this.iesProfileURL = null

    /**
     * @access private
     * @type {?string}
     */
    this._entityID = null

    this._context = null
    this._shadowFrameBuffer = null
    this._shadowDepthBuffer = null
    this._shadowDepthTexture = null
    this._projectionTransform = null


    /**
     * 
     * @type {boolean}
     * @see
     */
    this.automaticallyAdjustsShadowProjection = false

    /**
     * 
     * @type {boolean}
     * @see
     */
    this.forcesBackFaceCasters = false

    /**
     *
     * @type {number}
     * @see
     */
    this.maximumShadowDistance = 0.0

    /**
     *
     * @type {boolean}
     * @see
     */
    this.sampleDistributedShadowMaps = false

    /**
     *
     * @type {number}
     * @see
     */
    this.shadowCascadeCount = 1

    /**
     *
     * @type {number}
     * @see
     */
    this.shadowCascadeSplittingFactor = 1.0
  }

  // Managing Light Attributes

  /**
   * Returns the value of a lighting attribute.
   * @deprecated
   * @access public
   * @param {string} key - A constant specifying a lighting attribute. See Lighting Attribute Keys for available keys and their possible values.
   * @returns {?Object} - 
   * @desc A light’s type property determines its set of available attributes.You can also get the values of lighting attributes using Key-value coding. The key path for each lighting attribute is listed in Lighting Attribute Keys.
   * @see https://developer.apple.com/documentation/scenekit/scnlight/1523345-attribute
   */
  attributeForKey(key) {
    return null
  }

  /**
   * Sets the value for a lighting attribute.
   * @deprecated
   * @access public
   * @param {?Object} attribute - The value for the lighting attribute.
   * @param {string} key - A constant specifying a lighting attribute. See Lighting Attribute Keys for available keys and their possible values.
   * @returns {void}
   * @desc A light’s type property determines its set of available attributes.You can also set or animate changes to the values of lighting attributes using Key-value coding. The key path for each lighting attribute is listed in Lighting Attribute Keys.
   * @see https://developer.apple.com/documentation/scenekit/scnlight/1523148-setattribute
   */
  setAttributeForKey(attribute, key) {
  }

  // Managing Spotlight Extent

  /**
   * An image or other visual content affecting the shape and color of a light’s illuminated area.
   * @type {?SCNMaterialProperty}
   * @desc In photographic and stage lighting terminology, a gobo (also known as a flag or cookie) is a stencil, gel, or other object placed just in front of a light source, shaping or coloring the beam of light.You alter the appearance of a spotlight by changing the contents property of the object permanently assigned to this property. As with other material properties, you can use a color or image, or a Core Animation layer containing animated content, as a lighting gobo.This property applies only to lights whose type property is spot.
   * @see https://developer.apple.com/documentation/scenekit/scnlight/1523524-gobo
   */
  get gobo() {
    return this._gobo
  }

  // Structures

  /**
   * @type {Object} LightType
   * @property {string} IES A light source whose shape, direction, and intensity of illumination is determined by a photometric profile.
   * @property {string} ambient A light that illuminates all objects in the scene from all directions.
   * @property {string} directional A light source with a uniform direction and constant intensity.
   * @property {string} omni An omnidirectional light, also known as a point light. 
   * @property {string} probe A sample of the environment around a point in a scene to be used in environment-based lighting.
   * @property {string} spot A light source that illuminates a cone-shaped area.
   * @see https://developer.apple.com/documentation/scenekit/scnlight.lighttype
   */
  static get LightType() {
    return _LightType
  }

  get _shadowMapWidth() {
    if(this.shadowMapSize.width > 0){
      return this.shadowMapSize.width
    }
    // FIXME: adjust shadowMapSize
    return 2048
  }

  get _shadowMapHeight() {
    if(this.shadowMapSize.height > 0){
      return this.shadowMapSize.height
    }
    // FIXME: adjust shadowMapSize
    return 2048
  }


  _getDepthBufferForContext(context){
    if(this._shadowFrameBuffer && this._context === context){
      return this._shadowFrameBuffer
    }
    this._context = context

    const gl = context
    const width = this._shadowMapWidth
    const height = this._shadowMapHeight
    this._shadowFrameBuffer = gl.createFramebuffer()
    this._shadowDepthBuffer = gl.createRenderbuffer()
    gl.bindFramebuffer(gl.FRAMEBUFFER, this._shadowFrameBuffer)

    gl.bindRenderbuffer(gl.RENDERBUFFER, this._shadowDepthBuffer)
    gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height)
    gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, this._shadowDepthBuffer)

    this._shadowDepthTexture = gl.createTexture()
    gl.bindTexture(gl.TEXTURE_2D, this._shadowDepthTexture)
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
    gl.generateMipmap(gl.TEXTURE_2D)

    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this._shadowDepthTexture, 0)
    gl.drawBuffers([gl.COLOR_ATTACHMENT0])

    gl.bindRenderbuffer(gl.RENDERBUFFER, null)
    gl.bindFramebuffer(gl.FRAMEBUFFER, null)

    return this._shadowFrameBuffer
  }

  /**
   * @access private
   * @param {CGRect} viewRect -
   * @returns {void}
   */
  _updateProjectionTransform() {
    const m = new SCNMatrix4()
    const left = 0
    const right = this._shadowMapWidth
    const top = 0
    const bottom = this._shadowMapHeight
    const aspect = this._shadowMapWidth / this._shadowMapHeight

    if(this.type === _LightType.directional){
      // orthographic
      // FIXME: use orthographicScale, adjust x/y scale automatically
      //m.m11 = 2 / (right - left)
      m.m11 = 0.1
      //m.m11 = 2 / (right - left)
      m.m12 = 0
      m.m13 = 0
      m.m14 = 0
      m.m21 = 0
      //m.m22 = 2 / (top - bottom)
      m.m22 = 0.1
      m.m23 = 0
      m.m24 = 0
      m.m31 = 0
      m.m32 = 0
      m.m33 = -2 / (this.zFar - this.zNear)
      //m.m33 = -1 / (this.zFar - this.zNear)
      m.m34 = 0
      m.m41 = 0
      m.m42 = 0
      m.m43 = -(this.zFar + this.zNear) / (this.zFar - this.zNear)
      //m.m43 = -this.zFar / (this.zFar - this.zNear)
      m.m44 = 1
    }else{
      // perspective
      let m11 = 1
      let m22 = 1
      if(this.yFov <= 0 && this.xFov <= 0){
        const cot = 1.0 / Math.tan(Math.PI / 6.0)
        m11 = cot / aspect
        m22 = cot
      }else if(this.yFov <= 0){
        const cot = 1.0 / Math.tan(this.xFov * Math.PI / 360.0)
        m11 = cot
        m22 = cot * aspect
      }else if(this.xFov <= 0){
        const cot = 1.0 / Math.tan(this.yFov * Math.PI / 360.0)
        m11 = cot / aspect
        m22 = cot
      }else{
        // FIXME: compare xFov to yFov
        const cot = 1.0 / Math.tan(this.yFov * Math.PI / 360.0)
        m11 = cot / aspect
        m22 = cot
      }

      m.m11 = m11
      m.m12 = 0
      m.m13 = 0
      m.m14 = 0
      m.m21 = 0
      m.m22 = m22
      m.m23 = 0
      m.m24 = 0
      m.m31 = 0
      m.m32 = 0
      m.m33 = -(this.zFar + this.zNear) / (this.zFar - this.zNear)
      m.m34 = -1
      m.m41 = 0
      m.m42 = 0
      m.m43 = -2 * this.zFar * this.zNear / (this.zFar - this.zNear)
      m.m44 = 0
    } 
    this._projectionTransform = m
  }
}