Home Reference Source Repository

js/SceneKit/SCNGeometry.js

'use strict'

import NSObject from '../ObjectiveC/NSObject'
//import SCNAnimatable from './SCNAnimatable'
import SCNCullMode from './SCNCullMode'
//import SCNBoundingVolume from './SCNBoundingVolume'
//import SCNShadable from './SCNShadable'
import SCNGeometrySource from './SCNGeometrySource'
//import SCNGeometryElement from './SCNGeometryElement'
//import SCNLevelOfDetail from './SCNLevelOfDetail'
import SCNMaterial from './SCNMaterial'
//import SCNMatrix4MakeTranslation from './SCNMatrix4MakeTranslation'
import SCNOrderedDictionary from './SCNOrderedDictionary'
import SCNVector3 from './SCNVector3'
import SKColor from '../SpriteKit/SKColor'
import _InstanceOf from '../util/_InstanceOf'

/**
 * A three-dimensional shape (also called a model or mesh) that can be displayed in a scene, with attached materials that define its appearance.
 * @access public
 * @extends {NSObject}
 * @implements {SCNAnimatable}
 * @implements {SCNBoundingVolume}
 * @implements {SCNShadable}
 * @see https://developer.apple.com/documentation/scenekit/scngeometry
 */
export default class SCNGeometry extends NSObject {
  static get _propTypes() {
    const addSources = (obj, sources, key, coder) => { 
      //console.log(`addSources source.length: ${sources.length}, key: ${key}`)
      obj._geometrySources.push(...sources) 
    }
    return {
      name: 'string',
      levelsOfDetail: 'NSArray',
      materials: 'NSArray',
      tessellator: 'SCNGeometryTessellator',
      subdivisionLevel: 'integer',
      // program
      // shaderModifiers
      elements: ['NSArray', '_geometryElements'],
      kGeometrySourceSemanticColor: ['NSArray', addSources],
      kGeometrySourceSemanticEdgeCrease: ['NSArray', addSources],
      kGeometrySourceSemanticNormal: ['NSArray', addSources],
      kGeometrySourceSemanticTangent: ['NSArray', addSources],
      kGeometrySourceSemanticTexcoord: ['NSArray', addSources],
      kGeometrySourceSemanticVertex: ['NSArray', (obj, sources) => {
        addSources(obj, sources)
        obj._updateBoundingBox()
      }],
      kGeometrySourceSemanticVertexCrease: ['NSArray', addSources],
      wantsAdaptiveSubdivision: 'boolean',
      adaptiveSubdivision: ['boolean', null],

      entityID: ['string', '_entityID'],
      subdivisionSettings: ['bytes', null],
      shadableHelper: ['SCNShadableHelper', '_shadableHelper']
    }
  }

  // Creating a Geometry Object

  /**
   * Creates a new geometry built from the specified geometry sources and elements.
   * @access public
   * @constructor
   * @param {SCNGeometrySource[]} sources - An array of SCNGeometrySource objects describing vertices in the geometry and their attributes.
   * @param {?SCNGeometryElement[]} elements - An array of SCNGeometryElement objects describing how to connect the geometry’s vertices.
   * @desc A geometry's visible content comes from the combination of geometry sources, which contain data describing its vertices, with geometry elements, which contain data describing how the vertices connect to form a surface. Each SCNGeometrySource object describes an attribute of all vertices in the geometry (vertex position, surface normal vector, color, or texture mapping coordinates) identified by the source's semantic property. To create a custom geometry you must provide at least one source, for the vertex semantic. Typically, you also provide sources for normals and texture coordinates for use in lighting and shading.Sources for the vertex, normal, and color semantics must be unique-if multiple objects in the sources array have the same semantic, SceneKit uses only the first. A geometry may have multiple sources for the texcoord semantic-the order of texture coordinate sources in the sources array determines the value to use for the mappingChannel property when attaching materials.Each SCNGeometryElement object describes how vertices from the geometry sources are combined into polygons to create the geometry's shape. Creating a custom geometry requires at least one element. If the elements array contains multiple objects, their order determines the arrangement of the geometry's materials-for details, see the discussion of the materials property.
   * @see https://developer.apple.com/documentation/scenekit/scngeometry/1522803-init
   */
  constructor(sources = [], elements = []) {
    super()

    if(!Array.isArray(sources)){
      throw new Error('SCNGeometry(sources, elements): sources must be Array')
    }
    if(!Array.isArray(elements)){
      throw new Error('SCNGeometry(sources, elements): elements must be Array')
    }

    // Managing Geometry Attributes

    /**
     * A name associated with the geometry object.
     * @type {?string}
     * @see https://developer.apple.com/documentation/scenekit/scngeometry/1522953-name
     */
    this.name = null

    /**
     * An array of SCNLevelOfDetail objects for managing the geometry’s appearance when viewed from far away.
     * @type {?SCNLevelOfDetail[]}
     * @see https://developer.apple.com/documentation/scenekit/scngeometry/1523745-levelsofdetail
     */
    this.levelsOfDetail = null


    // Managing a Geometry’s Materials

    /**
     * An array of SCNMaterial objects that determine the geometry’s appearance when rendered.
     * @type {SCNMaterial[]}
     * @see https://developer.apple.com/documentation/scenekit/scngeometry/1523472-materials
     */
    this.materials = []

    
    // Managing Geometry Data

    this._geometryElements = elements
    this._geometrySources = sources
    this._vertexArrayObjects = null
    this._materialBuffer = null
    //this._textureFlagBuffer = null
    this._shadowVAO = null
    this._hitTestVAO = null

    // Working with Subdivision Surfaces

    /**
     * The number of subdivisions SceneKit uses to smooth the geometry’s surface at render time.
     * @type {number}
     * @see https://developer.apple.com/documentation/scenekit/scngeometry/1524177-subdivisionlevel
     */
    this.subdivisionLevel = 0

    /**
     * The geometry element identifying which edges of the geometry’s surface should remain sharp after subdivision.
     * @type {?SCNGeometryElement}
     * @see https://developer.apple.com/documentation/scenekit/scngeometry/1523246-edgecreaseselement
     */
    this.edgeCreasesElement = null

    /**
     * The geometry source specifying the smoothness or sharpness of edges after surface subdivision.
     * @type {?SCNGeometrySource}
     * @see https://developer.apple.com/documentation/scenekit/scngeometry/1523479-edgecreasessource
     */
    this.edgeCreasesSource = null


    /////////////////
    // SCNShadable //
    /////////////////
    
    // Assigning a Custom Shader Program

    /**
     * A program used when rendering the object.
     * @type {?SCNProgram}
     * @see https://developer.apple.com/documentation/scenekit/scnshadable/1523689-program
     */
    this.program = null

    // Customizing SceneKit’s Shader Programs

    /**
     * A dictionary of GLSL source code snippets for customizing the shader programs provided by SceneKit.
     * @type {?Map<SCNShaderModifierEntryPoint, string>}
     * @see https://developer.apple.com/documentation/scenekit/scnshadable/1523348-shadermodifiers
     */
    this.shaderModifiers = null

    /**
     * @access private
     * @type {Map<string, SCNBindingBlock>}
     */
    this._bindingHandler = {}

    /**
     * @access private
     * @type {Map<string, SCNBindingBlock>}
     */
    this._unbindingHandler = {}

    /**
     * @access private
     * @type {Object}
     */
    this._valuesForUndefinedKeys = {}


    ///////////////////
    // SCNAnimatable //
    ///////////////////

    /**
     * @access private
     * @type {SCNOrderedDictionary}
     */
    this._animations = new SCNOrderedDictionary()

    ///////////////////////
    // SCNBoundingVolume //
    ///////////////////////

    // Working with Bounding Volumes

    /**
     * The minimum and maximum corner points of the object’s bounding box.
     * @type {{min: SCNVector3, max: SCNVector3}}
     * @see https://developer.apple.com/documentation/scenekit/scnboundingvolume/2034705-boundingbox
     */
    this.boundingBox = null

    /**
     * The center point and radius of the object’s bounding sphere.
     * @access private
     * @type {Object}
     * @parameter {SCNVector3} _boundingSphere.center
     * @parameter {number} _boundingSphere.radius
     */
    //this._boundingSphere = null

    /**
     * 
     * @type {SCNGeometryTessellator}
     * @see https://developer.apple.com/documentation/scenekit/scngeometry/2867472-tessellator
     */
    this.tessellator = null

    /**
     *
     * @type {boolean}
     * @see https://developer.apple.com/documentation/scenekit/scngeometry/2888353-wantsadaptivesubdivision
     */
    this.wantsAdaptiveSubdivision = false // TODO: check the default value

    this._vertexBuffer = null
    this._indexBuffer = null

    this._isPresentationInstance = false
    this._presentation = null

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

    /**
     * @access private
     * @type {?SCNShadableHelper}
     */
    this._shadableHelper = null

    this._btVertices = null
    this._btMesh = null
    this._btShape = null

    /**
     * @access private
     * @type {Promise}
     */
    this._loadedPromise = null
  }

  // Managing a Geometry’s Materials

  /**
   * The first material attached to the geometry.
   * @type {?SCNMaterial}
   * @see https://developer.apple.com/documentation/scenekit/scngeometry/1523485-firstmaterial
   */
  get firstMaterial() {
    return this.materials[0]
  }

  set firstMaterial(newValue) {
    this.materials[0] = newValue
  }

  /**
   * Returns the first material attached to the geometry with the specified name.
   * @access public
   * @param {string} name - The name of the material to be retrieved.
   * @returns {?SCNMaterial} - 
   * @desc You can use the name property of each SCNMaterial object to make managing your scene graph easier. Materials loaded from a scene file may have names assigned by an artist using a 3D authoring tool.If a geometry has multiple materials attached with the same name, this method returns the first according to the order of the materials array.
   * @see https://developer.apple.com/documentation/scenekit/scngeometry/1523789-material
   */
  materialNamed(name) {
    return null
  }

  /**
   * Attaches a material to the geometry at the specified index.
   * @access public
   * @param {SCNMaterial} material - The material to attach.
   * @param {number} index - The location in the geometry’s materials array at which to add the new material.ImportantRaises an exception (rangeException) if index is greater than the number of elements in the materials array.
   * @returns {void}
   * @see https://developer.apple.com/documentation/scenekit/scngeometry/1522876-insertmaterial
   */
  insertMaterialAt(material, index) {
  }

  /**
   * Removes a material attached to the geometry.
   * @access public
   * @param {number} index - The index of the attached material to be removed.ImportantRaises an exception (rangeException) if index is beyond the bounds of the materials array.
   * @returns {void}
   * @see https://developer.apple.com/documentation/scenekit/scngeometry/1522646-removematerial
   */
  removeMaterialAt(index) {
  }

  /**
   * Replaces a material attached to the geometry with another.
   * @access public
   * @param {number} index - The index of the attached material to be replaced.ImportantRaises an exception (rangeException) if index is beyond the bounds of the materials array.
   * @param {SCNMaterial} material - The material with which to replace the attached material.
   * @returns {void}
   * @see https://developer.apple.com/documentation/scenekit/scngeometry/1522714-replacematerial
   */
  replaceMaterialAtIndexWith(index, material) {
  }

  // Managing Geometry Data

  /**
   * Returns the geometry element at a specified index.
   * @access public
   * @param {number} elementIndex - The index of the geometry element.
   * @returns {SCNGeometryElement} - 
   * @desc Each SCNGeometryElement object describes how vertices from the geometry’s sources are combined into polygons to create the geometry’s shape. Visible geometries contain at least one element.
   * @see https://developer.apple.com/documentation/scenekit/scngeometry/1523266-geometryelement
   */
  geometryElementAtIndex(elementIndex) {
    return this._geometryElements[elementIndex]
  }

  /**
   * Returns the geometry sources for a specified semantic.
   * @access public
   * @param {SCNGeometrySource.Semantic} semantic - A constant identifying a semantic for which to return geometry sources. See Geometry Semantic Identifiers for possible values.
   * @returns {SCNGeometrySource[]} - 
   * @desc Each SCNGeometrySource object describes an attribute of all vertices in the geometry (such as vertex position, surface normal vector, color, or texture mapping coordinates) identified by the source’s semantic property. A geometry always has at least one source, for the vertex semantic, typically has additional sources for use in lighting and shading, and may have other sources for skeletal animation or surface subdivision information.The vertex, normal, and color semantics each refer to at most one source. A geometry may have multiple sources for the texcoord semantic—in this case, indices in the returned array correspond to values for the mappingChannel property used when attaching textures to materials.
   * @see https://developer.apple.com/documentation/scenekit/scngeometry/1522926-getgeometrysources
   */
  getGeometrySourcesForSemantic(semantic) {
    return this._geometrySources.filter((source) => source.semantic === semantic)
  }

  /**
   * An array of geometry elements that describe the geometry’s shape.
   * @type {SCNGeometryElement[]}
   * @desc Each SCNGeometryElement object describes how vertices from the geometry’s sources are combined into polygons to create the geometry’s shape. Visible geometries contain at least one element.For geometries with multiple elements, you can use the materials property to attach different materials to each element.
   * @see https://developer.apple.com/documentation/scenekit/scngeometry/1523046-geometryelements
   */
  get geometryElements() {
    return this._geometryElements.slice(0)
  }

  /**
   * An array of geometry sources that provide vertex data for the geometry.
   * @type {SCNGeometrySource[]}
   * @desc Each SCNGeometrySource object describes an attribute of all vertices in the geometry (such as vertex position, surface normal vector, color, or texture mapping coordinates) identified by the source’s semantic property. A geometry always has at least one source (for the vertex semantic), typically has additional sources for use in lighting and shading, and may have other sources for skeletal animation or surface subdivision information.
   * @see https://developer.apple.com/documentation/scenekit/scngeometry/1523662-geometrysources
   */
  get geometrySources() {
    return this._geometrySources.slice(0)
  }

  get geometryGLSource() {
    return new Float32Array(this._geometrySources[0])
  }

  /**
   * The number of geometry elements in the geometry.
   * @type {number}
   * @desc Each SCNGeometryElement object describes how vertices from the geometry’s sources are combined into polygons to create the geometry’s shape. Visible geometries contain at least one element.For geometries with multiple elements, you can use the materials property to attach different materials to each element.
   * @see https://developer.apple.com/documentation/scenekit/scngeometry/1523800-geometryelementcount
   */
  get geometryElementCount() {
    return this._geometryElements.length
  }

  ///////////////////////
  // SCNBoundingVolume //
  ///////////////////////

  // Working with Bounding Volumes

  /**
   * The center point and radius of the object’s bounding sphere.
   * @type {Object}
   * @parameter {SCNVector3} _boundingSphere.center -
   * @parameter {number} _boundingSphere.radius -
   * @returns {Object} -
   * @desc Scene Kit defines a bounding sphere in the local coordinate space using a center point and a radius. For example, if a node’s bounding sphere has the center point {3, 1, 4} and radius 2.0, all points in the vertex data of node’s geometry (and any geometry attached to its child nodes) lie within 2.0 units of the center point.The coordinates provided when reading this property are valid only if the object has a volume to be measured. For a geometry containing no vertex data or a node containing no geometry (and whose child nodes, if any, contain no geometry), the values center and radius are both zero.
   * @see https://developer.apple.com/documentation/scenekit/scnboundingvolume/2034707-boundingsphere
   */
  getBoundingSphere() {
    if(this.boundingBox === null){
      return { center: new SCNVector3(0, 0, 0), radius: 0 }
    }
    const max = this.boundingBox.max
    const min = this.boundingBox.min
    const w = (max.x - min.x) * 0.5
    const h = (max.y - min.y) * 0.5
    const l = (max.z - min.z) * 0.5
    const r = Math.sqrt(w * w + h * h + l * l)
    const c = new SCNVector3(min.x + w, min.y + h, min.z + l)

    return { center: c, radius: r }
  }

  /////////////////
  // SCNShadable //
  /////////////////

  // Handling Parameters in Custom OpenGL Shader Programs

  /**
   * Specifies a block to be called before rendering with programs with the specified GLSL uniform variable or attribute name.
   * @access public
   * @param {string} symbol - A GLSL uniform variable or attribute name.
   * @param {?SCNBindingBlock} [block = null] - A block to be called by SceneKit.
   * @returns {void}
   * @desc Use this method to associate a block with a SceneKit object (geometry or material) to handle setup of an attribute or uniform variable in a custom SCNProgram shader associated with that object. SceneKit calls your block before rendering the object. In the block, you can execute any OpenGL commands or other code necessary for preparing your custom shader. For example, the following block updates the time uniform variable in a custom fragment shader for producing animated effects:CFTimeInterval startTime = CFAbsoluteTimeGetCurrent();
[myNode.geometry.firstMaterial handleBindingOfSymbol:@"time" usingBlock:
    ^(unsigned int programID, unsigned int location, SCNNode *renderedNode, SCNRenderer *renderer) {
        glUniform1f(location, CFAbsoluteTimeGetCurrent() - startTime);
    }];
This method is for OpenGL shader programs only. To bind custom variable data for Metal shader programs, use the handleBinding(ofBufferNamed:frequency:handler:) method.CFTimeInterval startTime = CFAbsoluteTimeGetCurrent();
[myNode.geometry.firstMaterial handleBindingOfSymbol:@"time" usingBlock:
    ^(unsigned int programID, unsigned int location, SCNNode *renderedNode, SCNRenderer *renderer) {
        glUniform1f(location, CFAbsoluteTimeGetCurrent() - startTime);
    }];

   * @see https://developer.apple.com/documentation/scenekit/scnshadable/1523063-handlebinding
   */
  handleBindingOfSymbolHandler(symbol, block = null) {
    this._bindingHandler[symbol] = block
  }

  /**
   * Specifies a block to be called after rendering with programs with the specified GLSL uniform variable or attribute name.
   * @access public
   * @param {string} symbol - A GLSL uniform variable or attribute name.
   * @param {?SCNBindingBlock} [block = null] - A block to be called by SceneKit.
   * @returns {void}
   * @desc Use this method to associate a block with a SceneKit object (geometry or material) to handle cleanup related to an attribute or uniform variable in a custom SCNProgram shader associated with that object. SceneKit will call your block after rendering the object. In the block, you can execute any OpenGL commands or other code necessary for post-rendering tasks.This method is for OpenGL shader programs only. To bind custom variable data for Metal shader programs, use the handleBinding(ofBufferNamed:frequency:handler:) method.
   * @see https://developer.apple.com/documentation/scenekit/scnshadable/1522783-handleunbinding
   */
  handleUnbindingOfSymbolHandler(symbol, block = null) {
    this._unbindingHandler[symbol] = block
  }

  /**
   * @access private
   * @param {SCNNode} node -
   * @param {WebGLProgram} glProgram -
   * @param {WebGLRenderingContext} gl -
   * @param {SCNRenderer} renderer -
   * @returns {void}
   */
  _callBindingHandlerForNodeProgramContextRenderer(node, glProgram, gl, renderer) {
    const bindingKeys = Object.keys(this._bindingHandler)
    for(const key of bindingKeys){
      const handler = this._bindingHandler[key]
      const loc = gl.getUniformBlockIndex(glProgram, key)
      handler(glProgram, loc, node, renderer)
    }
  }
  
  /**
   * @access private
   * @param {SCNNode} node -
   * @param {WebGLProgram} glProgram -
   * @param {WebGLRenderingContext} gl -
   * @param {SCNRenderer} renderer -
   * @returns {void}
   */
  _callUnindingHandlerForNodeProgramContextRenderer(node, glProgram, gl, renderer) {
    const bindingKeys = Object.keys(this._unbindingHandler)
    for(const key of bindingKeys){
      const handler = this._unbindingHandler[key]
      const loc = gl.getUniformBlockIndex(glProgram, key)
      handler(glProgram, loc, node, renderer)
    }
  }

  ///////////////////
  // SCNAnimatable //
  ///////////////////

  // Managing Animations

  /**
   * Required. Adds an animation object for the specified key.
   * @access public
   * @param {CAAnimation} animation - The animation object to be added.
   * @param {?string} key - An string identifying the animation for later retrieval. You may pass nil if you don’t need to reference the animation later.
   * @returns {void}
   * @desc Newly added animations begin executing after the current run loop cycle ends.SceneKit does not define any requirements for the contents of the key parameter—it need only be unique among the keys for other animations you add. If you add an animation with an existing key, this method overwrites the existing animation.
   * @see https://developer.apple.com/documentation/scenekit/scnanimatable/1523386-addanimation
   */
  addAnimationForKey(animation, key) {
    if(typeof key === 'undefined' || key === null){
      key = Symbol()
    }
    const anim = animation.copy()
    // FIXME: use current frame time
    anim._animationStartTime = Date.now() * 0.001
    anim._prevTime = anim._animationStartTime - 0.0000001

    this._animations.set(key, anim)
  }

  /**
   * Required. Returns the animation with the specified key.
   * @access public
   * @param {string} key - A string identifying a previously added animation.
   * @returns {?CAAnimation} - 
   * @desc Attempting to modify any properties of the returned object results in undefined behavior.
   * @see https://developer.apple.com/documentation/scenekit/scnanimatable/1524020-animation
   */
  animationForKey(key) {
    return this._animations.get(key)
  }

  /**
   * Required. Removes all the animations currently attached to the object.
   * @access public
   * @returns {void}
   * @see https://developer.apple.com/documentation/scenekit/scnanimatable/1522762-removeallanimations
   */
  removeAllAnimations() {
    this._animations.clear()
  }

  /**
   * Required. Removes the animation attached to the object with the specified key.
   * @access public
   * @param {string} key - A string identifying an attached animation to remove.
   * @returns {void}
   * @see https://developer.apple.com/documentation/scenekit/scnanimatable/1522880-removeanimation
   */
  removeAnimationForKey(key) {
    this._animations.delete(key)
    // TODO: reset values
  }

  /**
   * Required. Removes the animation attached to the object with the specified key, smoothly transitioning out of the animation’s effect.
   * @access public
   * @param {string} key - A string identifying an attached animation to remove.
   * @param {number} duration - The duration for transitioning out of the animation’s effect before it is removed.
   * @returns {void}
   * @desc Use this method to create smooth transitions between the effects of multiple animations. For example, the geometry loaded from a scene file for a game character may have associated animations for player actions such as walking and jumping. When the player lands from a jump, you remove the jump animation so the character continues walking. If you use the removeAnimation(forKey:) method to remove the jump animation, SceneKit abruptly switches from the current frame of the jump animation to the current frame of the walk animation. If you use the removeAnimation(forKey:fadeOutDuration:) method instead, SceneKit plays both animations at once during that duration and interpolates vertex positions from one animation to the other, creating a smooth transition.
   * @see https://developer.apple.com/documentation/scenekit/scnanimatable/1522841-removeanimation
   */
  removeAnimationForKeyFadeOutDuration(key, duration) {
  }

  /**
   * Required. An array containing the keys of all animations currently attached to the object.
   * @type {string[]}
   * @desc This array contains all keys for which animations are attached to the object, or is empty if there are no attached animations. The ordering of animation keys in the array is arbitrary.
   * @see https://developer.apple.com/documentation/scenekit/scnanimatable/1523610-animationkeys
   */
  get animationKeys() {
    const keys = []
    for(const key of this._animations.keys()){
      keys.push(key)
    }
    return keys
  }

  // Pausing and Resuming Animations

  /**
   * Required. Pauses the animation attached to the object with the specified key.
   * @access public
   * @param {string} key - A string identifying an attached animation.
   * @returns {void}
   * @desc This method has no effect if no animation is attached to the object with the specified key.
   * @see https://developer.apple.com/documentation/scenekit/scnanimatable/1523592-pauseanimation
   */
  pauseAnimationForKey(key) {
  }

  /**
   * Required. Resumes a previously paused animation attached to the object with the specified key.
   * @access public
   * @param {string} key - A string identifying an attached animation.
   * @returns {void}
   * @desc This method has no effect if no animation is attached to the object with the specified key or if the specified animation is not currently paused.
   * @see https://developer.apple.com/documentation/scenekit/scnanimatable/1523332-resumeanimation
   */
  resumeAnimationForKey(key) {
  }

  /**
   * Required. Returns a Boolean value indicating whether the animation attached to the object with the specified key is paused.
   * @access public
   * @param {string} key - A string identifying an attached animation.
   * @returns {boolean} - 
   * @see https://developer.apple.com/documentation/scenekit/scnanimatable/1523703-isanimationpaused
   */
  isAnimationPausedForKey(key) {
    return false
  }

  // Instance Methods

  /**
   * Required. 
   * @access public
   * @param {number} speed - 
   * @param {string} key - 
   * @returns {void}
   * @see https://developer.apple.com/documentation/scenekit/scnanimatable/1778343-setanimationspeed
   */
  setAnimationSpeedForKey(speed, key) {
  }

  /**
   * @access private
   * @param {WebGLContext} gl -
   * @param {SCNGeometry} geometry - 
   * @param {boolean} update -
   * @returns {WebGLBuffer} -
   */
  //_createVertexBuffer(gl, baseGeometry, update = false) {
  _createVertexBuffer(gl, node, update = false, _base = null) {
    const baseGeometry = (_base === null ? node.geometry : _base)
    const baseSkinner = node.skinner
    const skinner = node.presentation.skinner
    if(this._vertexBuffer === null){
      this._vertexBuffer = gl.createBuffer()
    }else if(!update){
      return this._vertexBuffer
    }

    gl.bindBuffer(gl.ARRAY_BUFFER, this._vertexBuffer)
    const arr = []
    const vertexSource = baseGeometry.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.vertex)[0]
    const normalSource = baseGeometry.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.normal)[0]
    let tangentSource = baseGeometry.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.tangent)[0]
    const colorSource = baseGeometry.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.color)[0]
    const texcoordSource0 = baseGeometry.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.texcoord)[0]
    const texcoordSource1 = baseGeometry.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.texcoord)[1]
    const indexSource = baseSkinner ? baseSkinner._boneIndices : null
    const weightSource = baseSkinner ? baseSkinner._boneWeights: null
    const vectorCount = vertexSource.vectorCount

    const pVertexSource = this.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.vertex)[0]
    const pNormalSource = this.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.normal)[0]
    let pTangentSource = this.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.tangent)[0]
    const pColorSource = this.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.color)[0]
    const pTexcoordSource0 = this.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.texcoord)[0]
    const pTexcoordSource1 = this.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.texcoord)[1]
    //const pIndexSource = this.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.boneIndices)[0]
    const pIndexSource = skinner ? skinner._boneIndices : null
    //const pWeightSource = this.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.boneWeights)[0]
    const pWeightSource = skinner ? skinner._boneWeights : null

    if(typeof vertexSource === 'undefined'){
      throw new Error('vertexSource is undefined')
    }
    if(typeof normalSource !== 'undefined' && normalSource.vectorCount !== vectorCount){
      throw new Error('normalSource.vectorCount !== vertexSource.vectorCount')
    }
    if(typeof tangentSource !== 'undefined' && tangentSource.vectorCount !== vectorCount){
      throw new Error('tangentSource.vectorCount !== vertexSource.vectorCount')
    }
    if(typeof colorSource !== 'undefined' && colorSource.vectorCount !== vectorCount){
      throw new Error('colorSource.vectorCount !== vertexSource.vectorCount')
    }
    if(typeof texcoordSource0 !== 'undefined' && texcoordSource0.vectorCount !== vectorCount){
      throw new Error('texcoordSource0.vectorCount !== vertexSource.vectorCount')
    }
    if(typeof texcoordSource1 !== 'undefined' && texcoordSource1.vectorCount !== vectorCount){
      throw new Error('texcoordSource1.vectorCount !== vertexSource.vectorCount')
    }
    if(typeof tangentSource === 'undefined' && this.materials.find((m) => !_InstanceOf(m._normal._contents, SKColor))){
      tangentSource = this._createTangentSource()
      pTangentSource = tangentSource
      this._geometrySources.push(tangentSource)
      if(baseGeometry !== this){
        baseGeometry._geometrySources.push(tangentSource)
      }
    }

    //const vertexArray = vertexSource ? vertexSource.data : null
    const vertexComponents = vertexSource ? vertexSource.componentsPerVector : 0
    //const normalArray = normalSource ? normalSource.data : null
    const normalComponents = normalSource ? normalSource.componentsPerVector : 0
    const tangentComponents = tangentSource ? tangentSource.componentsPerVector : 0
    const colorComponents = colorSource ? colorSource.componentsPerVector : 0
    //const texcoordArray = texcoordSource ? texcoordSource.data : null
    const texcoord0Components = texcoordSource0 ? texcoordSource0.componentsPerVector : 0
    const texcoord1Components = texcoordSource1 ? texcoordSource1.componentsPerVector : 0

    for(let i=0; i<vectorCount; i++){
      if(vertexSource){
        arr.push(...vertexSource._vectorAt(i))
      }
      if(normalSource){
        arr.push(...normalSource._vectorAt(i))
      }
      if(tangentSource){
        arr.push(...tangentSource._vectorAt(i))
      }
      if(colorSource){
        arr.push(...colorSource._vectorAt(i))
      }
      if(texcoordSource0){
        arr.push(...texcoordSource0._vectorAt(i))
      }
      if(texcoordSource1){
        arr.push(...texcoordSource1._vectorAt(i))
      }
    }

    //console.log(`vertex(0): ${vertexSource._vectorAt(0)}`)
    //console.log(`normal(0): ${normalSource._vectorAt(0)}`)
    //console.log(`texcoord(0): ${texcoordSource._vectorAt(0)}`)

    // update geometry sources
    // FIXME: Don't change geometry sources. Use other variables
    const bytesPerComponent = 4
    let offset = 0
    const stride = (
      vertexComponents
      + normalComponents
      + tangentComponents
      + colorComponents
      + texcoord0Components
      + texcoord1Components
    ) * bytesPerComponent

    pVertexSource._bytesPerComponent = bytesPerComponent
    pVertexSource._dataOffset = offset
    pVertexSource._dataStride = stride
    offset += vertexComponents * bytesPerComponent

    if(pNormalSource){
      pNormalSource._bytesPerComponent = bytesPerComponent
      pNormalSource._dataOffset = offset
      pNormalSource._dataStride = stride
      offset += normalComponents * bytesPerComponent
    }
    if(pTangentSource){
      pTangentSource._bytesPerComponent = bytesPerComponent
      pTangentSource._dataOffset = offset
      pTangentSource._dataStride = stride
      offset += tangentComponents * bytesPerComponent
    }
    if(pColorSource){
      pColorSource._bytesPerComponent = bytesPerComponent
      pColorSource._dataOffset = offset
      pColorSource._dataStride = stride
      offset += colorComponents * bytesPerComponent
    }
    if(pTexcoordSource0){
      pTexcoordSource0._bytesPerComponent = bytesPerComponent
      pTexcoordSource0._dataOffset = offset
      pTexcoordSource0._dataStride = stride
      offset += texcoord0Components * bytesPerComponent
    }
    if(pTexcoordSource1){
      pTexcoordSource1._bytesPerComponent = bytesPerComponent
      pTexcoordSource1._dataOffset = offset
      pTexcoordSource1._dataStride = stride
      offset += texcoord1Components * bytesPerComponent
    }

    //console.log(`offset: ${offset}, vectorCount: ${vectorCount}`)
    offset *= vectorCount

    const indexArray = indexSource ? indexSource.data : null
    const indexComponents = indexSource ? indexSource.componentsPerVector : 0
    const weightArray = weightSource ? weightSource.data : null
    const weightComponents = weightSource ? weightSource.componentsPerVector : 0
    const boneStride = (indexComponents + weightComponents) * bytesPerComponent

    for(let i=0; i<vectorCount; i++){
      if(indexSource){
        arr.push(...indexSource._vectorAt(i))
      }
      if(weightSource){
        arr.push(...weightSource._vectorAt(i))
      }
    }

    if(pIndexSource){
      pIndexSource._bytesPerComponent = bytesPerComponent
      pIndexSource._dataOffset = offset
      pIndexSource._dataStride = boneStride
      offset += indexComponents * bytesPerComponent
    }
    if(pWeightSource){
      pWeightSource._bytesPerComponent = bytesPerComponent
      pWeightSource._dataOffset = offset
      pWeightSource._dataStride = boneStride
      offset += weightComponents * bytesPerComponent
    }

    const vertexData = new Float32Array(arr)
    //console.log(`vertexData length: ${arr.length}`)
    gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.DYNAMIC_DRAW)

    // set new data
    pVertexSource._data = arr
    if(pNormalSource){
      pNormalSource._data = arr
    }
    if(pTangentSource){
      pTangentSource._data = arr
    }
    if(pColorSource){
      pColorSource._data = arr
    }
    if(pTexcoordSource0){
      pTexcoordSource0._data = arr
    }
    if(pTexcoordSource1){
      pTexcoordSource1._data = arr
    }
    if(pIndexSource){
      pIndexSource._data = arr
    }
    if(pWeightSource){
      pWeightSource._data = arr
    }

    return this._vertexBuffer
  }

  /**
   * @access private
   * @param {WebGLContext} gl -
   * @param {boolean} update -
   * @returns {WebGLBuffer} -
   */
  _createIndexBuffer(gl, update = false) {
    if(this._indexBuffer && !update){
      return this._indexBuffer
    }
    this._indexBuffer = this._geometryElements[0]._createBuffer(gl)
    return this._indexBuffer
  }

  /**
   * @access private
   * @param {WebGLRenderingContext} gl -
   * @param {SCNGeometry} baseGeometry - 
   * @returns {void}
   */
  _updateVertexBuffer(gl, baseGeometry) {
    const pVertexSource = this.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.vertex)[0]
    const vertexData = new Float32Array(pVertexSource._data)
    gl.bindBuffer(gl.ARRAY_BUFFER, this._vertexBuffer)
    gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.DYNAMIC_DRAW)
  }

  /**
   * @access private
   * @param {WebGLRenderingContext} gl -
   * @param {WebGLProgram} program -
   * @param {number} index - material index
   * @param {number} opacity -
   * @returns {void}
   */
  _bufferMaterialData(gl, program, index, opacity) {
    // TODO: move this function to SCNProgram
    const materialCount = this.materials.length
    let material = this.materials[index % materialCount]
    if(!material){
      // FIXME: What should I do if there's no material? 
      material = new SCNMaterial()
    }

    if(material.lightingModel === SCNMaterial.LightingModel.constant){
      this._bindMaterialDataForConstantLighting(material, gl, program, opacity)
      return
    }

    let diffuse = material.diffuse.float32Array()
    diffuse[3] *= opacity
    let ambient = null
    if(material.locksAmbientWithDiffuse){
      ambient = diffuse
    }else{
      ambient = material.ambient.float32Array()
      ambient[3] *= opacity
    }

    const materialData = new Float32Array([
      ...ambient,
      ...diffuse,
      ...material.specular.float32Array(),
      ...material.normal.float32Array(),
      ...material.reflective.float32Array(),
      ...material.emission.float32Array(),
      ...material.transparent.float32Array(),
      ...material.multiply.float32Array(),
      ...material.ambientOcclusion.float32Array(),
      ...material.selfIllumination.float32Array(),
      ...material.metalness.float32Array(),
      ...material.roughness.float32Array(),
      material.shininess * 100.0, 
      material.fresnelExponent,
      0, 0 // needs padding for 16-byte alignment
    ])
    
    gl.bindBuffer(gl.UNIFORM_BUFFER, this._materialBuffer)
    gl.bufferData(gl.UNIFORM_BUFFER, materialData, gl.DYNAMIC_DRAW)
    gl.bindBuffer(gl.UNIFORM_BUFFER, null)

    const textureFlags = []

    // emission
    let selfIllumination = 0
    if(material._selfIllumination._contents instanceof Image || material._selfIllumination._contents instanceof WebGLTexture){
      this._setTextureToName(gl, material._selfIllumination, 'TEXTURE0', textureFlags)
      selfIllumination = 1
    }else if(material._emission._contents instanceof Image || material._emission._contents instanceof WebGLTexture){
      this._setTextureToName(gl, material._emission, 'TEXTURE0', textureFlags)
    }else{
      textureFlags.push(0)
    }
    gl.uniform1i(gl.getUniformLocation(program, 'selfIllumination'), selfIllumination)

    // ambient
    this._setTextureToName(gl, material._ambient, 'TEXTURE1', textureFlags)

    // diffuse
    this._setTextureToName(gl, material._diffuse, 'TEXTURE2', textureFlags)

    // specular
    this._setTextureToName(gl, material._specular, 'TEXTURE3', textureFlags)

    // reflective
    this._setCubeTextureToName(gl, material._reflective, 'TEXTURE4', textureFlags)

    // transparent
    this._setTextureToName(gl, material._transparent, 'TEXTURE5', textureFlags)

    // multiply
    this._setTextureToName(gl, material._multiply, 'TEXTURE6', textureFlags)

    // normal
    this._setTextureToName(gl, material._normal, 'TEXTURE7', textureFlags)

    // ambientOcclusion
    this._setTextureToName(gl, material._ambientOcclusion, 'TEXTURE8', textureFlags)

    // selfIllumination
    this._setTextureToName(gl, material._selfIllumination, 'TEXTURE9', textureFlags)

    // metalness
    this._setTextureToName(gl, material._metalness, 'TEXTURE10', textureFlags)

    // roughness
    this._setTextureToName(gl, material._roughness, 'TEXTURE11', textureFlags)

    // TODO: cache uniform location
    gl.uniform1iv(gl.getUniformLocation(program, 'textureFlags'), new Int32Array(textureFlags))

    if(material.isDoubleSided){
      gl.disable(gl.CULL_FACE)
    }else{
      gl.enable(gl.CULL_FACE)
      if(material.cullMode === SCNCullMode.back){
        gl.cullFace(gl.BACK)
      }else{
        gl.cullFace(gl.FRONT)
      }
    }

    const blendFuncSrc = [
      gl.SRC_ALPHA, // alpha
      gl.ONE, // add
      gl.ZERO, // subtract
      gl.ZERO, // multiply
      gl.SRC_ALPHA, // screen
      gl.ONE // replace
    ]
    const blendFuncDst = [
      gl.ONE_MINUS_SRC_ALPHA, // alpha
      gl.ONE, // add
      gl.ONE_MINUS_SRC_COLOR, // subtract
      gl.SRC_COLOR, // multiply
      gl.ONE, // screen
      gl.ZERO // replace
    ]
    gl.blendFunc(blendFuncSrc[material.blendMode], blendFuncDst[material.blendMode])
  }

  /**
   * @access private
   * @param {SCNMaterial} material -
   * @param {WebGLRenderingContext} gl -
   * @param {WebGLProgram} program -
   * @param {number} opacity -
   */
  _bindMaterialDataForConstantLighting(material, gl, program, opacity) {
    const ambient = material.ambient.float32Array()
    ambient[3] *= opacity
    const materialData = new Float32Array([
      ...ambient,
      0, 0, 0, 1, // diffuse
      0, 0, 0, 1, // specular
      0, 0, 0, 1, // normal
      0, 0, 0, 1, // reflective
      0, 0, 0, 1, // emission
      0, 0, 0, 1, // transparent
      1, 1, 1, 1, // multiply
      0, 0, 0, 1, // ambientOcclusion
      0, 0, 0, 1, // selfIllumination
      0, 0, 0, 1, // metalness
      0, 0, 0, 1, // roughness
      0, // shininess
      0, // fresnelExponent
      0, 0 // needs padding for 16-byte alignment
    ])

    gl.bindBuffer(gl.UNIFORM_BUFFER, this._materialBuffer)
    gl.bufferData(gl.UNIFORM_BUFFER, materialData, gl.DYNAMIC_DRAW)
    gl.bindBuffer(gl.UNIFORM_BUFFER, null)

    const textureFlags = []

    // emission
    textureFlags.push(0)

    gl.uniform1i(gl.getUniformLocation(program, 'selfIllumination'), 0)

    // ambient
    this._setTextureToName(gl, material._ambient, 'TEXTURE1', textureFlags)

    textureFlags.push(0) // diffuse
    textureFlags.push(0) // specular
    textureFlags.push(0) // reflective
    textureFlags.push(0) // transparent
    textureFlags.push(0) // multiply
    textureFlags.push(0) // normal
    textureFlags.push(0) // ambientOcclusion
    textureFlags.push(0) // selfIllumination
    textureFlags.push(0) // metalness
    textureFlags.push(0) // roughness

    // TODO: cache uniform location
    gl.uniform1iv(gl.getUniformLocation(program, 'textureFlags'), new Int32Array(textureFlags))

    if(material.isDoubleSided){
      gl.disable(gl.CULL_FACE)
    }else{
      gl.enable(gl.CULL_FACE)
      if(material.cullMode === SCNCullMode.back){
        gl.cullFace(gl.BACK)
      }else{
        gl.cullFace(gl.FRONT)
      }
    }

    const blendFuncSrc = [
      gl.SRC_ALPHA, // alpha
      gl.ONE, // add
      gl.ZERO, // subtract
      gl.ZERO, // multiply
      gl.SRC_ALPHA, // screen
      gl.ONE // replace
    ]
    const blendFuncDst = [
      gl.ONE_MINUS_SRC_ALPHA, // alpha
      gl.ONE, // add
      gl.ONE_MINUS_SRC_COLOR, // subtract
      gl.SRC_COLOR, // multiply
      gl.ONE, // screen
      gl.ZERO // replace
    ]
    gl.blendFunc(blendFuncSrc[material.blendMode], blendFuncDst[material.blendMode])
  }

  /**
   * @access private
   * @param {WebGLRenderingContext} gl -
   * @param {SCNMaterialProperty} m -
   * @param {string} name -
   * @param {boolean[]} textureFlags -
   * @returns {void}
   */
  _setCubeTextureToName(gl, m, name, textureFlags) {
    if(m._contents instanceof Image){
      m._contents = this._createCubeTexture(gl, m._contents)
    }
    if(m._contents instanceof WebGLTexture){
      textureFlags.push(1)
      gl.activeTexture(gl[name])
      gl.bindTexture(gl.TEXTURE_CUBE_MAP, m._contents)
      gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, m._magnificationFilterFor(gl))
      gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, m._minificationFilterFor(gl))
      gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, m._wrapSFor(gl))
      gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, m._wrapTFor(gl))
    }else{
      textureFlags.push(0)
    }
  }

  /**
   * @access private
   * @param {WebGLRenderingContext} gl -
   * @param {SCNMaterialProperty} m -
   * @param {string} name -
   * @param {boolean[]} textureFlags -
   * @returns {void}
   */
  _setTextureToName(gl, m, name, textureFlags) {
    if(m._contents instanceof Image){
      m._contents = this._createTexture(gl, m._contents)
    }
    if(m._contents instanceof WebGLTexture){
      textureFlags.push(1)
      gl.activeTexture(gl[name])
      gl.bindTexture(gl.TEXTURE_2D, m._contents)
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, m._magnificationFilterFor(gl))
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, m._minificationFilterFor(gl))
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, m._wrapSFor(gl))
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, m._wrapTFor(gl))
    }else{
      textureFlags.push(0)
    }
  }

  _createTangentSource() {
    const elements = this._geometryElements
    const vertex = this.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.vertex)[0]
    const texcoord = this.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.texcoord)[0]

    const data = []
    const semantic = SCNGeometrySource.Semantic.tangent
    const vectorCount = vertex.vectorCount
    const floatComponents = true
    const componentsPerVector = 3
    const bytesPerComponent = 4
    const dataOffset = 0
    const dataStride = 12

    const tangent = []
    for(let i=0; i<vectorCount; i++){
      tangent.push(new SCNVector3(0, 0, 0))
    }

    for(const element of elements){
      const len = element.primitiveCount
      for(let i=0; i<len; i++){
        const index = element._indexAt(i)
        const pos0 = vertex._scnVectorAt(index[0])
        const pos1 = vertex._scnVectorAt(index[1])
        const pos2 = vertex._scnVectorAt(index[2])
        const tex0 = texcoord._scnVectorAt(index[0])
        const tex1 = texcoord._scnVectorAt(index[1])
        const tex2 = texcoord._scnVectorAt(index[2])

        const p1 = pos1.sub(pos0)
        const p2 = pos2.sub(pos0)
        const t1 = tex1.sub(tex0)
        const t2 = tex2.sub(tex0)

        const t = p1.mul(t2.y).sub(p2.mul(t1.y))
        tangent[index[0]] = tangent[index[0]].add(t)
        tangent[index[1]] = tangent[index[1]].add(t)
        tangent[index[2]] = tangent[index[2]].add(t)
      }
    }

    for(let i=0; i<vectorCount; i++){
      data.push(...tangent[i].normalize().floatArray())
    }

    return new SCNGeometrySource(data, semantic, vectorCount, floatComponents, componentsPerVector, bytesPerComponent, dataOffset, dataStride)
  }

  copy() {
    const geometry = new SCNGeometry()
    geometry.name = this.name
    geometry.levelsOfDetail = this.levelsOfDetail
    geometry.materials = this.materials
    geometry._geometryElements = this._geometryElements.slice(0)
    geometry._geometrySources = this._geometrySources.slice(0)
    geometry._vertexArrayObjects = this._vertexArrayObjects ? this._vertexArrayObjects.slice(0) : null
    geometry.subdivisonLevel = this.subdivisionLevel
    geometry.edgeCreasesElement = this.edgeCreasesElement
    geometry.edgeCreasesSource = this.edgeCreasesSource
    geometry.program = this.program
    geometry.shaderModifiers = this.shaderModifiers
    //geometry._animationKeys = this._animationKeys
    geometry.boundingBox = this.boundingBox
    //geometry._boundingSphere = this._boundingSphere
    geometry._vertexBuffer = this._vertexBuffer
    geometry._indexBuffer = this._indexBuffer
    geometry._animations = this._animations.copy()
    geometry._shadableHelper = this._shadableHelper

    return geometry
  }

  /**
   * @access private
   * @param {WebGLRenderingContext} gl -
   * @param {Image} image -
   * @returns {WebGLTexture} -
   */
  _createCubeTexture(gl, image) {
    const texture = gl.createTexture()

    gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture)
    // texImage2D(target, level, internalformat, width, height, border, format, type, source)
    // Safari complains that 'source' is not ArrayBufferView type, but WebGL2 should accept HTMLCanvasElement.
    const targets = [
      gl.TEXTURE_CUBE_MAP_POSITIVE_Z,
      gl.TEXTURE_CUBE_MAP_POSITIVE_X,
      gl.TEXTURE_CUBE_MAP_NEGATIVE_Z,
      gl.TEXTURE_CUBE_MAP_NEGATIVE_X,
      gl.TEXTURE_CUBE_MAP_NEGATIVE_Y,
      gl.TEXTURE_CUBE_MAP_POSITIVE_Y
    ]
    //const tx = [0, 1.0/6.0, 2.0/6.0, 3.0/6.0, 4.0/6.0, 5.0/6.0, 1]
    //const itx = [4, 1, 5, 0, 2, 3]
    const margin = 0.001
    const sx = [4.0/6.0 + margin, 1.0/6.0 + margin, 5.0/6.0 + margin, 0 + margin, 2.0/6.0 + margin, 3.0/6.0 + margin]
    const imageWidth = image.naturalWidth
    const imageHeight = image.naturalHeight
    const srcWidth = imageHeight - margin * 2

    for(let i=0; i<6; i++){
      const canvas = document.createElement('canvas')
      canvas.width = imageHeight
      canvas.height = imageHeight
      canvas.getContext('2d').drawImage(image, sx[i], 0, srcWidth, imageHeight, 0, 0, imageHeight, imageHeight)

      gl.texImage2D(targets[i], 0, gl.RGBA, imageHeight, imageHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, canvas)
    }

    gl.generateMipmap(gl.TEXTURE_CUBE_MAP)
    return texture
  }

  /**
   * @access private
   * @param {WebGLRenderingContext} gl -
   * @param {Image} image -
   * @returns {WebGLTexture} -
   */
  _createTexture(gl, image) {
    const texture = gl.createTexture()

    const canvas = document.createElement('canvas')
    canvas.width = image.naturalWidth
    canvas.height = image.naturalHeight
    //console.warn(`image size: ${image.naturalWidth} ${image.naturalHeight}`)
    canvas.getContext('2d').drawImage(image, 0, 0)

    gl.bindTexture(gl.TEXTURE_2D, texture)
    // texImage2D(target, level, internalformat, width, height, border, format, type, source)
    // Safari complains that 'source' is not ArrayBufferView type, but WebGL2 should accept HTMLCanvasElement.
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, image.width, image.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, canvas)
    gl.generateMipmap(gl.TEXTURE_2D)
    //gl.bindTexture(gl.TEXTURE_2D, null)
    return texture
  }

  /**
   * @access private
   * @returns {Ammo.btCollisionShape} -
   * @desc call Ammo.destroy(shape) after using it.
   */
  _createBtCollisionShape() {
    return this._createBtConvexTriangleMeshShape()
  }

  _createBtConvexTriangleMeshShape() {
    //this._destoryShape()
    //this._btVertices = []

    //const vertexSource = this.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.vertex)[0]
    //const vertexCount = vertexSource.vectorCount
    //for(let i=0; i<vertexCount; i++){
    //  this._btVertices.push(vertexSource._scnVectorAt(i)._createBtVector3())
    //}

    //this._btMesh = new Ammo.btTriangleMesh(false, false) // 16bit indices, 3 component vertices
    //for(const element of this._geometryElements){
    //  const indexCount = element._primitiveCount
    //  for(let i=0; i<indexCount; i++){
    //    // TODO: check primitiveType
    //    const indices = element._indexAt(i)
    //    this._btMesh.addTriangle(
    //      this._btVertices[indices[0]],
    //      this._btVertices[indices[1]],
    //      this._btVertices[indices[2]],
    //      true
    //    )
    //  }
    //}

    //const calcAabb = true
    //this._btShape = new Ammo.btTriangleMeshShape(this._btMesh, calcAabb)

    //return this._btShape
  }

  _destroyShape() {
    //if(this._btShape === null){
    //  return
    //}
    //Ammo.destroy(this._btShape)
    //this._btShape = null

    //Ammo.destroy(this._btMesh)
    //this._btMesh = null

    //for(const v of this._btVertices){
    //  Ammo.destroy(v)
    //}
    //this._btVerices = null
  }

  _execDestory() {
    // TODO: delete indexBuffer, vertexBuffer
    this._destroyShape()
  }

  _updateBoundingBox() {
    return this._updateBoundingBoxForSkinner()
  }

  _updateBoundingBoxForSkinner(skinner = null){
    let transform = null
    if(skinner){
      transform = skinner.baseGeometryBindTransform
    }

    const sources = this.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.vertex)
    const min = new SCNVector3(Infinity, Infinity, Infinity)
    const max = new SCNVector3(-Infinity, -Infinity, -Infinity)
    for(const src of sources){
      const result = src._createBoundingBox(transform)
      if(result.min.x < min.x){
        min.x = result.min.x
      }
      if(result.max.x > max.x){
        max.x = result.max.x
      }
      if(result.min.y < min.y){
        min.y = result.min.y
      }
      if(result.max.y > max.y){
        max.y = result.max.y
      }
      if(result.min.z < min.z){
        min.z = result.min.z
      }
      if(result.max.z > max.z){
        max.z = result.max.z
      }
    }
    this.boundingBox = { min: min, max: max }
    return this.boundingBox
  }

  /**
   * @access private
   * @param {SCNGeometry} geometry -
   * @returns {boolean} -
   */
  _intersectsBoundingBox(geometry) {
    const b1 = this.boundingBox
    const b2 = geometry.boundingBox

    if(b1.min.x > b2.max.x || b1.max.x < b2.min.x){
      return false
    }
    if(b1.min.y > b2.max.y || b1.max.y < b2.min.y){
      return false
    }
    if(b1.min.z > b2.max.z || b1.max.z < b2.min.z){
      return false
    }
    return true
  }

  /**
   * @access private
   * @returns {Promise} -
   */
  _getLoadedPromise() {
    if(this._loadedPromise){
      return this._loadedPromise
    }

    const promises = []
    for(const m of this.materials){
      promises.push(m.didLoad)
    }
    //this._loadedPromise = Promise.all(promises)
    //return this._loadedPromise
    return Promise.all(promises)
  }

  /**
   * @access public
   * @type {Promise} -
   */
  get didLoad() {
    return this._getLoadedPromise()
  }

  /**
   * Invoked by setValue(_:forKey:) when it finds no property for a given key.
   * @access public
   * @param {?Object} value - The value for the key identified by key.
   * @param {string} key - A string that is not equal to the name of any of the receiver's properties.
   * @returns {void}
   * @desc Subclasses can override this method to handle the request in some other way. The default implementation raises an NSUndefinedKeyException.
   * @see https://developer.apple.com/documentation/objectivec/nsobject/1413490-setvalue
   */
  setValueForUndefinedKey(value, key) {
    this._valuesForUndefinedKeys[key] = value
  }

  /**
   * Invoked by value(forKey:) when it finds no property corresponding to a given key.
   * @access public
   * @param {string} key - A string that is not equal to the name of any of the receiver's properties.
   * @returns {?Object} - 
   * @desc Subclasses can override this method to return an alternate value for undefined keys. The default implementation raises an NSUndefinedKeyException.
   * @see https://developer.apple.com/documentation/objectivec/nsobject/1413457-value
   */
  valueForUndefinedKey(key) {
    if(typeof this._valuesForUndefinedKeys[key] !== 'undefined'){
      return this._valuesForUndefinedKeys[key]
    }
    return super.valueForUndefinedKey(key)
  }

}