js/SceneKit/SCNRenderer.js
'use strict'
import CGPoint from '../CoreGraphics/CGPoint'
import CGRect from '../CoreGraphics/CGRect'
import CGSize from '../CoreGraphics/CGSize'
import NSObject from '../ObjectiveC/NSObject'
//import SCNSceneRenderer from './SCNSceneRenderer'
//import SCNTechniqueSupport from './SCNTechniqueSupport'
//import SCNScene from './SCNScene'
//import SCNAntialiasingMode from './SCNAntialiasingMode'
import SCNMaterial from './SCNMaterial'
import SCNMaterialProperty from './SCNMaterialProperty'
import SCNMatrix4 from './SCNMatrix4'
import SCNMatrix4MakeTranslation from './SCNMatrix4MakeTranslation'
import SCNNode from './SCNNode'
import SCNProgram from './SCNProgram'
import SCNPhysicsWorld from './SCNPhysicsWorld'
import SCNCamera from './SCNCamera'
import SCNLight from './SCNLight'
import SCNVector3 from './SCNVector3'
import SCNVector4 from './SCNVector4'
import SCNGeometryPrimitiveType from './SCNGeometryPrimitiveType'
import SCNGeometrySource from './SCNGeometrySource'
import SCNHitTestOption from './SCNHitTestOption'
import SCNHitTestResult from './SCNHitTestResult'
import SKColor from '../SpriteKit/SKColor'
import SKSpriteNode from '../SpriteKit/SKSpriteNode'
import SKTexture from '../SpriteKit/SKTexture'
import _SCNDefaultVertexShader from './_SCNDefaultVertexShader'
import _SCNDefaultFragmentShader from './_SCNDefaultFragmentShader'
import _SCNDefaultPBRFragmentShader from './_SCNDefaultPBRFragmentShader'
import _SCNDefaultShadowVertexShader from './_SCNDefaultShadowVertexShader'
import _SCNDefaultShadowFragmentShader from './_SCNDefaultShadowFragmentShader'
import _SCNDefaultParticleVertexShader from './_SCNDefaultParticleVertexShader'
import _SCNDefaultParticleFragmentShader from './_SCNDefaultParticleFragmentShader'
import _SCNDefaultHitTestVertexShader from './_SCNDefaultHitTestVertexShader'
import _SCNDefaultHitTestFragmentShader from './_SCNDefaultHitTestFragmentShader'
import _InstanceOf from '../util/_InstanceOf'
const _cameraLoc = 0
const _materialLoc = 1
const _lightLoc = 2
const _scnLightsLoc = 3
const _fogLoc = 4
const _shadowTextureBaseIndex = 8
const _fsDirectionalShadow = `
//float shadow = convDepth(texture(u_shadowTexture__I__, v_directionalShadowTexcoord[__I__].xy / v_directionalShadowTexcoord[__I__].w));
//if(v_directionalShadowDepth[__I__].z / v_directionalShadowDepth[__I__].w - 0.0001 > shadow){
// _output.color.rgb += material.diffuse.rgb * light.directionalShadow[__I__].shadowColor.rgb;
//}else{
// // diffuse
// vec3 lightVec = normalize(v_light[numLights]);
// float diffuse = clamp(dot(lightVec, _surface.normal), 0.0f, 1.0f);
// _output.color.rgb += light.directionalShadow[__I__].color.rgb * material.diffuse.rgb * diffuse;
// // specular
// if(diffuse > 0.0f){
// vec3 halfVec = normalize(lightVec + _surface.view);
// float specular = pow(dot(halfVec, _surface.normal), material.shininess);
// _output.color.rgb += specularColor.rgb * specular;
// }
//}
{
float shadow = 0.0;
for(int i=0; i<4; i++){
float d = convDepth(texture(u_shadowTexture__I__, (v_directionalShadowTexcoord[__I__].xy + poissonDisk[i]/700.0) / v_directionalShadowTexcoord[__I__].w));
if(v_directionalShadowDepth[__I__].z / v_directionalShadowDepth[__I__].w - 0.0001 > d){
shadow += 0.25;
}
}
//vec3 shadowColor = material.diffuse.rgb * light.directionalShadow[__I__].shadowColor.rgb;
vec3 shadowColor = light.directionalShadow[__I__].shadowColor.rgb;
// diffuse
vec3 lightVec = normalize(v_light[numLights]);
float diffuse = clamp(dot(lightVec, _surface.normal), 0.0f, 1.0f);
vec3 lightDiffuse = light.directionalShadow[__I__].color.rgb * diffuse;
_lightingContribution.diffuse += shadowColor * shadow + lightDiffuse * (1.0 - shadow);
// specular
if(diffuse > 0.0f){
vec3 halfVec = normalize(lightVec + _surface.view);
float specular = pow(dot(halfVec, _surface.normal), _surface.shininess);
// TODO: use intensity
_lightingContribution.specular += vec3(specular);
}
//_output.color.rgb += shadowColor * shadow + lightColor * (1.0 - shadow);
}
numLights += 1;
`
const _defaultCameraDistance = 15
/**
* A renderer for displaying SceneKit scene in an an existing Metal workflow or OpenGL context.
* @access public
* @extends {NSObject}
* @implements {SCNSceneRenderer}
* @implements {SCNTechniqueSupport}
* @see https://developer.apple.com/documentation/scenekit/scnrenderer
*/
export default class SCNRenderer extends NSObject {
// Creating a Renderer
/**
* Creates a renderer with the specified Metal device.
* @access public
* @constructor
* @param {?MTLDevice} device - A Metal device.
* @param {?Map<AnyHashable, Object>} [options = null] - An optional dictionary for future extensions.
* @desc Use this initializer to create a SceneKit renderer that draws into the rendering targets your app already uses to draw other content. For the device parameter, pass the MTLDevice object your app uses for drawing. Then, to tell SceneKit to render your content, call the SCNRenderer method, providing a command buffer and render pass descriptor for SceneKit to use in its rendering.
* @see https://developer.apple.com/documentation/scenekit/scnrenderer/1518404-init
*/
constructor(device, options = null) {
super()
// Specifying a Scene
/**
* The scene to be rendered.
* @type {?SCNScene}
* @see https://developer.apple.com/documentation/scenekit/scnrenderer/1518400-scene
*/
this.scene = null
// Managing Animation Timing
this._nextFrameTime = 0
/**
* context to draw frame
* @type {WebGLRenderingContext}
*/
this._context = null
/**
*
* @access private
* @type {SKColor}
*/
this._backgroundColor = null
//////////////////////
// SCNSceneRenderer //
//////////////////////
// Managing Scene Display
/**
* Required. The node from which the scene’s contents are viewed for rendering.
* @access private
* @type {?SCNNode}
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1523982-pointofview
*/
this._pointOfView = null
/**
* Required. A Boolean value that determines whether SceneKit automatically adds lights to a scene.
* @type {boolean}
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1523812-autoenablesdefaultlighting
*/
this.autoenablesDefaultLighting = false
/**
* Required. A Boolean value that determines whether SceneKit applies jittering to reduce aliasing artifacts.
* @type {boolean}
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1524026-isjitteringenabled
*/
this.isJitteringEnabled = false
/**
* Required. A Boolean value that determines whether SceneKit displays rendering performance statistics in an accessory view.
* @type {boolean}
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522763-showsstatistics
*/
this.showsStatistics = false
/**
* Required. Options for drawing overlay content in a scene that can aid debugging.
* @type {SCNDebugOptions}
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1523281-debugoptions
*/
this.debugOptions = null
this._renderingAPI = null
// Managing Scene Animation Timing
/**
* Required. The current scene time.
* @type {number}
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522680-scenetime
*/
this.sceneTime = 0
/**
* current time in seconds
* @access private
* @type {number}
*/
this._time = 0
/**
* Required. A Boolean value that determines whether the scene is playing.
* @type {boolean}
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1523401-isplaying
*/
this.isPlaying = false
/**
* Required. A Boolean value that determines whether SceneKit restarts the scene time after all animations in the scene have played.
* @type {boolean}
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522878-loops
*/
this.loops = false
// Participating in the Scene Rendering Process
/**
* Required. A delegate object that receives messages about SceneKit’s rendering process.
* @type {?SCNSceneRendererDelegate}
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522671-delegate
*/
this.delegate = null
// Customizing Scene Rendering with Metal
this._currentRenderCommandEncoder = null
this._device = null
this._commandQueue = null
this._colorPixelFormat = null
this._depthPixelFormat = null
this._stencilPixelFormat = null
// Rendering Sprite Kit Content over a Scene
/**
* Required. A Sprite Kit scene to be rendered on top of the SceneKit content.
* @type {?SKScene}
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1524051-overlayskscene
*/
this.overlaySKScene = null
// Working With Positional Audio
/**
* Required. The node representing the listener’s position in the scene for use with positional audio effects.
* @type {?SCNNode}
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1523747-audiolistener
*/
//this.audioListener = null
//this._audioEnvironmentNode = null
//this._audioEngine = null
// Instance Properties
/**
* Required.
* @type {number}
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522854-currenttime
*/
this.currentTime = 0
/**
* @access private
* @type {SCNProgram}
*/
this.__defaultProgram = null
/**
* @access private
* @type {SCNProgram}
*/
this.__defaultPBRProgram = null
/**
* @access private
* @type {SCNProgram}
*/
this.__defaultParticleProgram = null
/**
* @access private
* @type {SCNProgram}
*/
this.__defaultHitTestProgram = null
/**
* @access private
* @type {SCNProgram}
*/
this.__defaultShadowProgram = null
this._location = new Map()
this._defaultCameraPosNode = new SCNNode()
this._defaultCameraRotNode = new SCNNode()
this._defaultCameraNode = new SCNNode()
this._defaultCameraNode.name = 'kSCNFreeViewCameraName'
const camera = new SCNCamera()
camera.name = 'kSCNFreeViewCameraNameCamera'
this._defaultCameraNode.camera = camera
this._defaultCameraNode.position = new SCNVector3(0, 0, _defaultCameraDistance)
this._defaultCameraNode._presentation = this._defaultCameraNode.copy()
this._defaultCameraPosNode.addChildNode(this._defaultCameraRotNode)
this._defaultCameraPosNode._presentation = this._defaultCameraPosNode.copy()
this._defaultCameraRotNode.addChildNode(this._defaultCameraNode)
this._defaultCameraRotNode._presentation = this._defaultCameraRotNode.copy()
this._defaultLightNode = new SCNNode()
const light = new SCNLight()
light.color = SKColor.white
light.type = SCNLight.LightType.omni
light.position = new SCNVector3(0, 10, 10)
this._defaultLightNode.light = light
this._defaultLightNode._presentation = this._defaultLightNode.copy()
/**
* @access private
* @type {CGRect}
*/
this._viewRect = new CGRect(new CGPoint(0, 0), new CGSize(0, 0))
/**
* The background color of the view.
* @type {SKColor}
*/
this._backgroundColor = SKColor.white
/**
* @access private
* @type {WebGLTexture}
*/
this.__dummyTexture = null
/**
* @access private
* @type {Object}
*/
this._lightNodes = {}
/**
* @access private
* @type {Object}
*/
this._numLights = {}
/**
* @access private
* @type {WebGLBuffer}
*/
this._cameraBuffer = null
/**
* @access private
* @type {WebGLBuffer}
*/
this._lightBuffer = null
/**
* @access private
* @type {WebGLBuffer}
*/
this._scnLightsBuffer = null
/**
* @access private
* @type {WebGLBuffer}
*/
this._fogBuffer = null
////////////////////////////
// Hit Test
////////////////////////////
/**
* @access private
* @type {WebGLFramebuffer}
*/
this._hitFrameBuffer = null
/**
* @access private
* @type {WebGLRenderbuffer}
*/
this._hitDepthBuffer = null
/**
* @access private
* @type {WebGLTexture}
*/
this._hitObjectIDTexture = null
/**
* @access private
* @type {WebGLTexture}
*/
this._hitFaceIDTexture = null
/**
* @access private
* @type {WebGLTexture}
*/
this._hitPositionTexture = null
/**
* @access private
* @type {WebGLTexture}
*/
this._hitNormalTexture = null
/**
* @access private
* @type {SCNProgram}
*/
this._currentProgram = null
}
// Managing Animation Timing
/**
* The timestamp for the next frame to be rendered.
* @type {number}
* @desc If the renderer’s scene has any attached actions or animations, use this property to determine how long your app should wait before telling the renderer to draw another frame. If this property’s value matches that of the renderer’s currentTime property, the scene contains a continuous animation—schedule your next render at whatever time best maintains your app’s performance. If the value is infinite, the scene has no running actions or animations.
* @see https://developer.apple.com/documentation/scenekit/scnrenderer/1518410-nextframetime
*/
get nextFrameTime() {
return this._nextFrameTime
}
// Rendering a Scene Using Metal
/**
* Renders the scene’s contents at the specified system time in the specified Metal command buffer.
* @access public
* @param {number} time - The timestamp, in seconds, at which to render the scene.
* @param {CGRect} viewport - The pixel dimensions in which to render.
* @param {MTLCommandBuffer} commandBuffer - The Metal command buffer in which SceneKit should schedule rendering commands.
* @param {MTLRenderPassDescriptor} renderPassDescriptor - The Metal render pass descriptor describing the rendering target.
* @returns {void}
* @desc This method can be used only with an SCNRenderer object created with the SCNRenderer initializer. Call this method to tell SceneKit to draw the renderer’s scene into the render target described by the renderPassDescriptor parameter, by encoding render commands into the commandBuffer parameter.When you call this method, SceneKit updates its hierarchy of presentation nodes based on the specified timestamp, and then draws the scene using the specified Metal objects. NoteBy default, the playback timing of actions and animations in a scene is based on the system time, not the scene time. Before using this method to control the playback of animations, set the usesSceneTimeBase property of each animation to true, or specify the playUsingSceneTimeBase option when loading a scene file that contains animations.
* @see https://developer.apple.com/documentation/scenekit/scnrenderer/1518401-render
*/
renderAtTimePassDescriptor(time, viewport, commandBuffer, renderPassDescriptor) {
}
// Rendering a Scene Using OpenGL
/**
* Renders the scene’s contents in the renderer’s OpenGL context.
* @deprecated
* @access public
* @returns {void}
* @desc This method can be used only with an SCNRenderer object created with the SCNRenderer initializer. Call this method to tell SceneKit to draw the renderer’s scene into the OpenGL context you created the renderer with.When you call this method, SceneKit updates its hierarchy of presentation nodes based on the current system time, and then draws the scene.
* @see https://developer.apple.com/documentation/scenekit/scnrenderer/1518403-render
*/
render() {
if(this.context === null){
console.error('SCNRenderer.render(): context is null')
return
}
const gl = this.context
if(this.scene === null){
if(this.overlaySKScene){
const sk = this.overlaySKScene
gl.clearColor(sk.backgroundColor.red, sk.backgroundColor.green, sk.backgroundColor.blue, sk.backgroundColor.alpha)
gl.clear(gl.COLOR_BUFFER_BIT)
this._renderOverlaySKScene()
}
return
}
this._lightNodes = this._createLightNodeArray() // createLightNodeArray must be called before getting program
const p = this._defaultProgram
const glProgram = p._getGLProgramForContext(gl)
gl.clearColor(this._backgroundColor.red, this._backgroundColor.green, this._backgroundColor.blue, this._backgroundColor.alpha)
gl.clearDepth(1.0)
gl.clearStencil(0)
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
//gl.useProgram(glProgram)
this._useProgram(p)
gl.depthFunc(gl.LEQUAL)
gl.depthMask(true)
gl.enable(gl.BLEND)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
//////////////////////////
// Camera
//////////////////////////
if(this._cameraBuffer === null){
this._initializeCameraBuffer(glProgram)
}
const cameraData = []
const cameraNode = this._getCameraNode()
cameraNode._updateWorldTransform()
const cameraPNode = cameraNode.presentation || cameraNode
const camera = cameraPNode.camera
camera._updateProjectionTransform(this._viewRect)
cameraData.push(...cameraPNode.worldTransform.getTranslation().floatArray(), 0)
cameraData.push(...cameraPNode.viewTransform.floatArray())
cameraData.push(...cameraPNode.inverseViewTransform.floatArray())
cameraData.push(...cameraPNode.viewProjectionTransform.floatArray())
gl.bindBuffer(gl.UNIFORM_BUFFER, this._cameraBuffer)
gl.bufferData(gl.UNIFORM_BUFFER, new Float32Array(cameraData), gl.DYNAMIC_DRAW)
gl.bindBuffer(gl.UNIFORM_BUFFER, null)
//console.log('cameraNode.worldPosition: ' + cameraPNode.worldTransform.getTranslation().float32Array())
//console.log('viewTransform: ' + cameraPNode.viewTransform.float32Array())
//console.log('projectionTransform: ' + cameraNode.camera.projectionTransform.float32Array())
//console.log('viewProjectionTransform: ' + cameraNode.viewProjectionTransform.float32Array())
//////////////////////////
// Fog
//////////////////////////
if(this._fogBuffer === null){
this._initializeFogBuffer(glProgram)
}
const fogData = []
if(this.scene.fogColor !== null && this.scene.fogEndDistance !== 0){
fogData.push(
...this.scene.fogColor.floatArray(),
this.scene.fogStartDistance,
this.scene.fogEndDistance,
this.scene.fogDensityExponent,
0
)
}else{
fogData.push(0, 0, 0, 0, camera.zFar * 2, camera.zFar * 2 + 1, 1, 0)
}
gl.bindBuffer(gl.UNIFORM_BUFFER, this._fogBuffer)
gl.bufferData(gl.UNIFORM_BUFFER, new Float32Array(fogData), gl.DYNAMIC_DRAW)
gl.bindBuffer(gl.UNIFORM_BUFFER, null)
//////////////////////////
// Lights
//////////////////////////
if(this._lightBuffer === null){
this._initializeLightBuffer(glProgram)
}
const lights = this._lightNodes
const lightData = []
lights.ambient.forEach((node) => {
lightData.push(...node.presentation.light.color.float32Array())
})
lights.directional.forEach((node) => {
const direction = (new SCNVector3(0, 0, -1)).rotateWithQuaternion(node.presentation._worldOrientation)
lightData.push(
...node.presentation.light.color.float32Array(),
...direction.float32Array(), 0
)
})
lights.directionalShadow.forEach((node) => {
const direction = (new SCNVector3(0, 0, -1)).rotateWithQuaternion(node.presentation._worldOrientation)
node.presentation.light._updateProjectionTransform()
lightData.push(
...node.presentation.light.color.float32Array(),
...direction.float32Array(), 0,
...node.presentation.light.shadowColor.float32Array(),
...node.presentation.lightViewProjectionTransform.float32Array(),
...node.presentation.shadowProjectionTransform.float32Array()
)
})
lights.omni.forEach((node) => {
lightData.push(
...node.presentation.light.color.float32Array(),
...node.presentation._worldTranslation.float32Array(), 0
)
})
lights.probe.forEach((node) => {
lightData.push(...node.presentation.light.color.float32Array())
})
lights.spot.forEach((node) => {
lightData.push(...node.presentation.light.color.float32Array())
})
gl.bindBuffer(gl.UNIFORM_BUFFER, this._lightBuffer)
gl.bufferData(gl.UNIFORM_BUFFER, new Float32Array(lightData), gl.DYNAMIC_DRAW)
gl.bindBuffer(gl.UNIFORM_BUFFER, null)
// FIXME: set params for each light
const scnLightsData = []
if(lights.directionalShadow.length > 0){
const l = lights.directionalShadow[0].presentation
const direction = (new SCNVector3(0, 0, -1)).rotateWithQuaternion(l._worldOrientation)
scnLightsData.push(...direction.float32Array(), 0)
scnLightsData.push(...l.shadowProjectionTransform.float32Array())
}else{
// direction
scnLightsData.push(0, 0, 0, 0)
// identity matrix
scnLightsData.push(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
gl.bindBuffer(gl.UNIFORM_BUFFER, this._scnLightsBuffer)
gl.bufferData(gl.UNIFORM_BUFFER, new Float32Array(scnLightsData), gl.DYNAMIC_DRAW)
gl.bindBuffer(gl.UNIFORM_BUFFER, null)
//////////////////////////
// Background (SkyBox)
//////////////////////////
if(this.scene.background._contents !== null){
const skyBox = this.scene._skyBox
skyBox.position = cameraPNode._worldTranslation
const scale = camera.zFar * 1.154
skyBox.scale = new SCNVector3(scale, scale, scale)
skyBox._updateWorldTransform()
// disable fog
const disabledFogData = fogData.slice(0)
disabledFogData[4] = camera.zFar * 2.0 // startDistance
disabledFogData[5] = camera.zFar * 2.1 // endDistance
disabledFogData[6] = 1.0 // densityExponent
gl.bindBuffer(gl.UNIFORM_BUFFER, this._fogBuffer)
gl.bufferData(gl.UNIFORM_BUFFER, new Float32Array(disabledFogData), gl.DYNAMIC_DRAW)
gl.bindBuffer(gl.UNIFORM_BUFFER, null)
this._renderNode(skyBox)
// enable fog
gl.bindBuffer(gl.UNIFORM_BUFFER, this._fogBuffer)
gl.bufferData(gl.UNIFORM_BUFFER, new Float32Array(fogData), gl.DYNAMIC_DRAW)
gl.bindBuffer(gl.UNIFORM_BUFFER, null)
}
//////////////////////////
// Shadow
//////////////////////////
//gl.useProgram(this._defaultShadowProgram._glProgram)
this._useProgram(this._defaultShadowProgram)
gl.enable(gl.DEPTH_TEST)
gl.depthMask(true)
gl.depthFunc(gl.LEQUAL)
gl.clearDepth(1.0)
gl.clearColor(1.0, 1.0, 1.0, 1.0)
gl.disable(gl.BLEND)
const shadowRenderingArray = this._createShadowNodeArray()
for(const key of Object.keys(lights)){
for(const lightNode of lights[key]){
this._renderNodesShadowOfLight(shadowRenderingArray, lightNode)
}
}
this._setViewPort() // reset viewport size
this._useProgram(p)
for(let i=0; i<lights.directionalShadow.length; i++){
const node = lights.directionalShadow[i]
const symbol = `TEXTURE${i+_shadowTextureBaseIndex}`
gl.activeTexture(gl[symbol])
gl.bindTexture(gl.TEXTURE_2D, node.presentation.light._shadowDepthTexture)
}
gl.enable(gl.BLEND)
//////////////////////////
// Nodes
//////////////////////////
const renderingArray = this._createRenderingNodeArray()
renderingArray.forEach((node) => {
this._renderNode(node)
})
const particleProgram = this._defaultParticleProgram._glProgram
//gl.useProgram(particleProgram)
this._useProgram(this._defaultParticleProgram)
gl.depthMask(false)
gl.enable(gl.BLEND)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE)
gl.uniformMatrix4fv(gl.getUniformLocation(particleProgram, 'viewTransform'), false, cameraPNode.viewTransform.float32Array())
gl.uniformMatrix4fv(gl.getUniformLocation(particleProgram, 'projectionTransform'), false, cameraPNode.projectionTransform.float32Array())
//////////////////////////
// Particles
//////////////////////////
if(this.scene._particleSystems !== null){
for(const system of this.scene._particleSystems){
this._renderParticleSystem(system)
}
}
const particleArray = this._createParticleNodeArray()
particleArray.forEach((node) => {
this._renderParticle(node)
})
//////////////////////////
// 2D Overlay
//////////////////////////
this._renderOverlaySKScene()
// DEBUG: show shadow map
//this._showShadowMapOfLight(lights.directionalShadow[0])
gl.flush()
}
_renderOverlaySKScene() {
if(this.overlaySKScene === null){
return
}
const gl = this.context
gl.disable(gl.CULL_FACE)
gl.enable(gl.DEPTH_TEST)
gl.depthFunc(gl.GEQUAL)
gl.enable(gl.BLEND)
gl.depthMask(true)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.clearStencil(0)
gl.clearDepth(-1)
gl.clear(gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
const skNodes = this._createSKNodeArray()
for(const node of skNodes){
this._renderSKNode(node)
}
}
/**
* @access private
* @returns {SCNNode} -
*/
_getCameraNode() {
let cameraNode = this._pointOfView
if(cameraNode === null){
cameraNode = this._searchCameraNode()
this._pointOfView = cameraNode
if(cameraNode === null){
cameraNode = this._defaultCameraNode
}
}
if(cameraNode === this._defaultCameraNode){
this._defaultCameraPosNode._updateWorldTransform()
}
return cameraNode
}
/**
*
* @access private
* @returns {SCNNode[]} -
*/
_createShadowNodeArray() {
const arr = [this.scene._rootNode]
const targetNodes = []
while(arr.length > 0){
const node = arr.shift()
if(node.presentation !== null
&& node.presentation.geometry !== null
&& node.presentation.castsShadow
&& node.presentation._worldOpacity > 0
&& !node.presentation.isHidden){
targetNodes.push(node)
}
arr.push(...node.childNodes)
}
return targetNodes
}
/**
*
* @access private
* @returns {SCNNode[]} -
*/
_createRenderingNodeArray() {
const arr = [this.scene._rootNode]
const targetNodes = []
while(arr.length > 0){
const node = arr.shift()
if(node.presentation !== null && node.presentation.geometry !== null){
targetNodes.push(node)
}
arr.push(...node.childNodes)
}
targetNodes.sort((a, b) => {
return (a.presentation.renderingOrder - b.presentation.renderingOrder) + (b.presentation._worldOpacity - a.presentation._worldOpacity) * 0.5
})
return targetNodes
}
/**
*
* @access private
* @returns {SCNNode[]} -
*/
_createParticleNodeArray() {
const arr = [this.scene._rootNode]
const targetNodes = []
while(arr.length > 0){
const node = arr.shift()
if(node.presentation !== null && node.presentation.particleSystems !== null){
targetNodes.push(node)
}
arr.push(...node.childNodes)
}
targetNodes.sort((a, b) => { return (a.renderingOrder - b.renderingOrder) + (b.opacity - a.opacity) * 0.5 })
return targetNodes
}
/**
*
* @access private
* @returns {SCNNode[]} -
*/
_createLightNodeArray() {
const targetNodes = {
ies: [],
ambient: [],
directional: [],
omni: [],
probe: [],
spot: [],
directionalShadow: []
}
const arr = [this.scene.rootNode]
let numLights = 0
while(arr.length > 0){
const node = arr.shift()
if(node.presentation !== null && node.presentation.light !== null){
if(node.presentation.light.type === 'directional' && node.presentation.light.castsShadow){
targetNodes.directionalShadow.push(node)
}else{
targetNodes[node.presentation.light.type].push(node)
}
if(node.presentation.light.type !== SCNLight.LightType.ambient){
numLights += 1
}
}
arr.push(...node.childNodes)
}
if(this.autoenablesDefaultLighting && numLights === 0){
targetNodes[this._defaultLightNode.light.type].push(this._defaultLightNode)
}
return targetNodes
}
/**
*
* @access private
* @returns {SCNNode[]} -
*/
_createRenderingPhysicsNodeArray() {
const arr = [this.scene._rootNode]
const targetNodes = []
while(arr.length > 0){
const node = arr.shift()
if(node.presentation !== null
&& node.presentation.physicsBody !== null
&& node.presentation.physicsBody.physicsShape !== null){
targetNodes.push(node)
}
arr.push(...node.childNodes)
}
targetNodes.sort((a, b) => { return a.renderingOrder - b.renderingOrder })
return targetNodes
}
/**
*
* @access private
* @param {SCNNode[]} nodes -
* @param {SCNNode} lightNode -
* @returns {void}
*/
_renderNodesShadowOfLight(nodes, lightNode) {
const lp = lightNode.presentation
const light = lp.light
if(!lp.castsShadow){
return
}
this._setViewPort(light._shadowMapWidth, light._shadowMapHeight)
const gl = this.context
const glProgram = this._defaultShadowProgram._getGLProgramForContext(gl)
gl.bindFramebuffer(gl.FRAMEBUFFER, light._getDepthBufferForContext(gl))
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
gl.uniformMatrix4fv(gl.getUniformLocation(glProgram, 'viewProjectionTransform'), false, lp.lightViewProjectionTransform.float32Array())
for(const node of nodes){
const geometry = node.presentation.geometry
const geometryCount = geometry.geometryElements.length
if(geometryCount === 0){
// nothing to draw...
continue
}
if(geometry._shadowVAO === null){
this._initializeShadowVAO(node, glProgram)
}
if(node.morpher !== null){
//this._updateVAO(node)
}
if(node.presentation.skinner !== null){
if(node.presentation.skinner._useGPU){
gl.uniform1i(gl.getUniformLocation(glProgram, 'numSkinningJoints'), node.presentation.skinner.numSkinningJoints)
gl.uniform4fv(gl.getUniformLocation(glProgram, 'skinningJoints'), node.presentation.skinner.float32Array())
}else{
gl.uniform1i(gl.getUniformLocation(glProgram, 'numSkinningJoints'), 0)
gl.uniform4fv(gl.getUniformLocation(glProgram, 'skinningJoints'), SCNMatrix4MakeTranslation(0, 0, 0).float32Array3x4f())
}
}else{
gl.uniform1i(gl.getUniformLocation(glProgram, 'numSkinningJoints'), 0)
gl.uniform4fv(gl.getUniformLocation(glProgram, 'skinningJoints'), node.presentation._worldTransform.float32Array3x4f())
}
for(let i=0; i<geometryCount; i++){
const vao = geometry._shadowVAO[i]
const element = geometry.geometryElements[i]
gl.bindVertexArray(vao)
// FIXME: use bufferData instead of bindBufferBase
let shape = null
switch(element.primitiveType){
case SCNGeometryPrimitiveType.triangles:
shape = gl.TRIANGLES
break
case SCNGeometryPrimitiveType.triangleStrip:
shape = gl.TRIANGLE_STRIP
break
case SCNGeometryPrimitiveType.line:
shape = gl.LINES
break
case SCNGeometryPrimitiveType.point:
shape = gl.POINTS
break
case SCNGeometryPrimitiveType.polygon:
shape = gl.TRIANGLE_FAN
break
default:
throw new Error(`unsupported primitiveType: ${element.primitiveType}`)
}
let size = null
switch(element.bytesPerIndex){
case 1:
size = gl.UNSIGNED_BYTE
break
case 2:
size = gl.UNSIGNED_SHORT
break
case 4:
size = gl.UNSIGNED_INT
break
default:
throw new Error(`unsupported index size: ${element.bytesPerIndex}`)
}
gl.drawElements(shape, element._glData.length, size, 0)
}
}
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
}
/**
*
* @access private
* @param {SCNNode} node -
* @returns {void}
*/
_renderNode(node) {
if(node.presentation.isHidden || node.presentation._worldOpacity <= 0){
return
}
const gl = this.context
const geometry = node.presentation.geometry
const geometryCount = geometry.geometryElements.length
if(geometryCount === 0){
// nothing to draw...
return
}
const scnProgram = this._getProgramForGeometry(geometry)
const glProgram = scnProgram._getGLProgramForContext(gl)
this._switchProgram(scnProgram)
if(geometry._vertexArrayObjects === null){
this._initializeVAO(node, glProgram)
this._initializeUBO(node, glProgram) // FIXME: program should have UBO, not node.
}
if(node.morpher !== null || (node.skinner && !node.skinner._useGPU)){
this._updateVAO(node)
}
gl.uniformMatrix4fv(gl.getUniformLocation(glProgram, 'modelTransform'), false, node._worldTransform.float32Array())
if(node.presentation.skinner !== null){
if(node.presentation.skinner._useGPU){
gl.uniform1i(gl.getUniformLocation(glProgram, 'numSkinningJoints'), node.presentation.skinner.numSkinningJoints)
gl.uniform4fv(gl.getUniformLocation(glProgram, 'skinningJoints'), node.presentation.skinner.float32Array())
}else{
gl.uniform1i(gl.getUniformLocation(glProgram, 'numSkinningJoints'), 0)
gl.uniform4fv(gl.getUniformLocation(glProgram, 'skinningJoints'), SCNMatrix4MakeTranslation(0, 0, 0).float32Array3x4f())
}
}else{
gl.uniform1i(gl.getUniformLocation(glProgram, 'numSkinningJoints'), 0)
gl.uniform4fv(gl.getUniformLocation(glProgram, 'skinningJoints'), node.presentation._worldTransform.float32Array3x4f())
}
for(let i=0; i<geometryCount; i++){
const materialCount = geometry.materials.length
const material = geometry.materials[i % materialCount]
let p = glProgram
if(material && (material.program || material.lightingModel === SCNMaterial.LightingModel.physicallyBased)){
const _scnProgram = material.program ? material.program : this._defaultPBRProgram
this._switchProgram(_scnProgram)
// TODO: refactoring
p = _scnProgram._getGLProgramForContext(gl)
if(node.presentation.skinner !== null){
if(node.presentation.skinner._useGPU){
gl.uniform1i(gl.getUniformLocation(p, 'numSkinningJoints'), node.presentation.skinner.numSkinningJoints)
gl.uniform4fv(gl.getUniformLocation(p, 'skinningJoints'), node.presentation.skinner.float32Array())
}else{
gl.uniform1i(gl.getUniformLocation(p, 'numSkinningJoints'), 0)
gl.uniform4fv(gl.getUniformLocation(p, 'skinningJoints'), SCNMatrix4MakeTranslation(0, 0, 0).float32Array3x4f())
}
}else{
gl.uniform1i(gl.getUniformLocation(p, 'numSkinningJoints'), 0)
gl.uniform4fv(gl.getUniformLocation(p, 'skinningJoints'), node.presentation._worldTransform.float32Array3x4f())
}
const materialIndex = gl.getUniformBlockIndex(p, 'materialUniform')
gl.uniformBlockBinding(p, materialIndex, _materialLoc)
gl.bindBufferBase(gl.UNIFORM_BUFFER, _materialLoc, geometry._materialBuffer)
material._callBindingHandlerForNodeProgramContextRenderer(node, p, gl, this)
}else{
this._switchProgram(scnProgram)
geometry._callBindingHandlerForNodeProgramContextRenderer(node, glProgram, gl, this)
}
const vao = geometry._vertexArrayObjects[i]
const element = geometry.geometryElements[i]
gl.bindVertexArray(vao)
// FIXME: use bufferData instead of bindBufferBase
gl.bindBufferBase(gl.UNIFORM_BUFFER, _materialLoc, geometry._materialBuffer)
geometry._bufferMaterialData(gl, p, i, node.presentation._worldOpacity)
let shape = null
switch(element.primitiveType){
case SCNGeometryPrimitiveType.triangles:
shape = gl.TRIANGLES
break
case SCNGeometryPrimitiveType.triangleStrip:
shape = gl.TRIANGLE_STRIP
break
case SCNGeometryPrimitiveType.line:
shape = gl.LINES
break
case SCNGeometryPrimitiveType.point:
shape = gl.POINTS
break
case SCNGeometryPrimitiveType.polygon:
shape = gl.TRIANGLE_FAN
break
default:
throw new Error(`unsupported primitiveType: ${element.primitiveType}`)
}
let size = null
switch(element.bytesPerIndex){
case 1:
size = gl.UNSIGNED_BYTE
break
case 2:
size = gl.UNSIGNED_SHORT
break
case 4:
size = gl.UNSIGNED_INT
break
default:
throw new Error(`unsupported index size: ${element.bytesPerIndex}`)
}
gl.drawElements(shape, element._glData.length, size, 0)
}
}
/**
*
* @access private
* @param {SCNNode} node -
* @returns {void}
*/
_renderParticle(node) {
if(node.presentation.isHidden){
return
}
//const systems = node.presentation.particleSystems
const systems = node.particleSystems
systems.forEach((system) => {
this._renderParticleSystem(system, node)
})
}
/**
*
* @access private
* @param {SCNParticleSystem} system -
* @param {?SCNNode} [node = null] -
* @returns {void}
*/
_renderParticleSystem(system, node = null) {
//this.currentTime
const gl = this.context
//let program = this._defaultParticleProgram._glProgram
//if(system._program !== null){
// program = system._program._glProgram
//}
let p = this._defaultParticleProgram
if(system._program !== null){
p = system._program
}
const glProgram = p._getGLProgramForContext(gl)
this._useProgram(p)
//this._switchProgram(p)
gl.disable(gl.CULL_FACE)
if(system._vertexBuffer === null){
system._initializeVAO(gl, glProgram)
}
gl.bindVertexArray(system._vertexArray)
system._bufferMaterialData(gl, glProgram)
if(node){
gl.uniformMatrix4fv(gl.getUniformLocation(glProgram, 'modelTransform'), false, node._worldTransform.float32Array())
}else{
let m = SCNMatrix4MakeTranslation(0, 0, 0)
gl.uniformMatrix4fv(gl.getUniformLocation(glProgram, 'modelTransform'), false, m.float32Array())
}
gl.drawElements(gl.TRIANGLES, system._particles.length * 6, system._glIndexSize, 0)
}
/**
*
* @access private
* @param {SCNNode} node -
* @param {number} objectID -
* @param {Map} options -
* @returns {void}
*/
_renderNodeForHitTest(node, objectID, options) {
const gl = this.context
const geometry = node.presentation.geometry
const glProgram = this._defaultHitTestProgram._getGLProgramForContext(gl)
const geometryCount = geometry.geometryElements.length
if(geometryCount === 0){
// nothing to draw...
return
}
if(geometry._vertexArrayObjects === null){
// geometry is not ready
return
}
if(geometry._hitTestVAO === null){
this._initializeHitTestVAO(node, glProgram)
}
gl.uniform1i(gl.getUniformLocation(glProgram, 'objectID'), objectID)
if(node.presentation.skinner !== null && node.presentation.skinner._useGPU){
if(node.presentation.skinner._useGPU){
gl.uniform1i(gl.getUniformLocation(glProgram, 'numSkinningJoints'), node.presentation.skinner.numSkinningJoints)
gl.uniform4fv(gl.getUniformLocation(glProgram, 'skinningJoints'), node.presentation.skinner.float32Array())
}else{
gl.uniform1i(gl.getUniformLocation(glProgram, 'numSkinningJoints'), 0)
gl.uniform4fv(gl.getUniformLocation(glProgram, 'skinningJoints'), SCNMatrix4MakeTranslation(0, 0, 0).float32Array3x4f())
}
}else{
gl.uniform1i(gl.getUniformLocation(glProgram, 'numSkinningJoints'), 0)
gl.uniform4fv(gl.getUniformLocation(glProgram, 'skinningJoints'), node.presentation._worldTransform.float32Array3x4f())
}
for(let i=0; i<geometryCount; i++){
const vao = geometry._hitTestVAO[i]
const element = geometry.geometryElements[i]
gl.bindVertexArray(vao)
gl.uniform1i(gl.getUniformLocation(glProgram, 'geometryID'), i)
let shape = null
switch(element.primitiveType){
case SCNGeometryPrimitiveType.triangles:
shape = gl.TRIANGLES
break
case SCNGeometryPrimitiveType.triangleStrip:
shape = gl.TRIANGLE_STRIP
break
case SCNGeometryPrimitiveType.line:
shape = gl.LINES
break
case SCNGeometryPrimitiveType.point:
shape = gl.POINTS
break
case SCNGeometryPrimitiveType.polygon:
shape = gl.TRIANGLE_FAN
break
default:
throw new Error(`unsupported primitiveType: ${element.primitiveType}`)
}
let size = null
switch(element.bytesPerIndex){
case 1:
size = gl.UNSIGNED_BYTE
break
case 2:
size = gl.UNSIGNED_SHORT
break
case 4:
size = gl.UNSIGNED_INT
break
default:
throw new Error(`unsupported index size: ${element.bytesPerIndex}`)
}
//console.log(`hitTest drawElements: length: ${element._glData.length}`)
gl.drawElements(shape, element._glData.length, size, 0)
}
}
/**
*
* @access private
* @param {SCNNode} node -
* @param {number} objectID -
* @param {Map} options -
* @returns {void}
*/
_renderPhysicsNodeForHitTest(node, objectID, options) {
const gl = this.context
const p = node.presentation
const body = p.physicsBody
const geometry = body.physicsShape._sourceGeometry
const geometryCount = geometry.geometryElements.length
if(geometryCount === 0){
// nothing to draw...
return
}
const glProgram = this._defaultHitTestProgram._getGLProgramForContext(gl)
if(geometry._vertexBuffer === null){
// should I copy the geometry?
geometry._createVertexBuffer(gl, node, false, geometry)
}
if(geometry._hitTestVAO === null){
this._initializeHitTestVAO(node, glProgram, true)
}
gl.uniform1i(gl.getUniformLocation(glProgram, 'objectID'), objectID)
if(node.presentation.skinner !== null && node.presentation.skinner._useGPU){
if(node.presentation.skinner._useGPU){
gl.uniform1i(gl.getUniformLocation(glProgram, 'numSkinningJoints'), node.presentation.skinner.numSkinningJoints)
gl.uniform4fv(gl.getUniformLocation(glProgram, 'skinningJoints'), node.presentation.skinner.float32Array())
}else{
gl.uniform1i(gl.getUniformLocation(glProgram, 'numSkinningJoints'), 0)
gl.uniform4fv(gl.getUniformLocation(glProgram, 'skinningJoints'), SCNMatrix4MakeTranslation(0, 0, 0).float32Array3x4f())
}
}else{
gl.uniform1i(gl.getUniformLocation(glProgram, 'numSkinningJoints'), 0)
gl.uniform4fv(gl.getUniformLocation(glProgram, 'skinningJoints'), node.presentation._worldTransform.float32Array3x4f())
}
for(let i=0; i<geometryCount; i++){
const vao = geometry._hitTestVAO[i]
const element = geometry.geometryElements[i]
gl.bindVertexArray(vao)
gl.uniform1i(gl.getUniformLocation(glProgram, 'geometryID'), i)
let shape = null
switch(element.primitiveType){
case SCNGeometryPrimitiveType.triangles:
shape = gl.TRIANGLES
break
case SCNGeometryPrimitiveType.triangleStrip:
shape = gl.TRIANGLE_STRIP
break
case SCNGeometryPrimitiveType.line:
shape = gl.LINES
break
case SCNGeometryPrimitiveType.point:
shape = gl.POINTS
break
case SCNGeometryPrimitiveType.polygon:
shape = gl.TRIANGLE_FAN
break
default:
throw new Error(`unsupported primitiveType: ${element.primitiveType}`)
}
let size = null
switch(element.bytesPerIndex){
case 1:
size = gl.UNSIGNED_BYTE
break
case 2:
size = gl.UNSIGNED_SHORT
break
case 4:
size = gl.UNSIGNED_INT
break
default:
throw new Error(`unsupported index size: ${element.bytesPerIndex}`)
}
gl.drawElements(shape, element._glData.length, size, 0)
}
}
/**
*
* @access private
* @returns {SKNode[]} -
*/
_createSKNodeArray() {
if(this.overlaySKScene === null){
return []
}
const arr = [this.overlaySKScene]
const targetNodes = []
while(arr.length > 0){
const node = arr.shift()
targetNodes.push(node)
arr.push(...node.children)
}
//targetNodes.sort((a, b) => { return a.renderingOrder - b.renderingOrder })
return targetNodes
}
/**
*
* @access private
* @param {SKNode} node -
* @returns {void}
*/
_renderSKNode(node) {
node._render(this.context, this._viewRect)
}
/**
* Renders the scene’s contents at the specified system time in the renderer’s OpenGL context.
* @access public
* @param {number} time - The timestamp, in seconds, at which to render the scene.
* @returns {void}
* @desc This method can be used only with an SCNRenderer object created with the SCNRenderer initializer. Call this method to tell SceneKit to draw the renderer’s scene into the OpenGL context you created the renderer with.When you call this method, SceneKit updates its hierarchy of presentation nodes based on the specified timestamp, and then draws the scene.NoteBy default, the playback timing of actions and animations in a scene is based on the system time, not the scene time. Before using this method to control the playback of animations, set the usesSceneTimeBase property of each animation to true, or specify the playUsingSceneTimeBase option when loading a scene file that contains animations.
* @see https://developer.apple.com/documentation/scenekit/scnrenderer/1518402-render
*/
renderAtTime(time) {
}
// Capturing a Snapshot
/**
* Creates an image by drawing the renderer’s content at the specified system time.
* @access public
* @param {number} time - The timestamp, in seconds, at which to render the scene.
* @param {CGSize} size - The size, in pixels, of the image to create.
* @param {SCNAntialiasingMode} antialiasingMode - The antialiasing mode to use for the image output.
* @returns {Image} -
* @desc When you call this method, SceneKit updates its hierarchy of presentation nodes based on the specified timestamp, and then draws the scene into a new image object of the specified size.
* @see https://developer.apple.com/documentation/scenekit/scnrenderer/1641767-snapshot
*/
snapshotAtTimeWith(time, size, antialiasingMode) {
return null
}
// Instance Methods
/**
*
* @access public
* @param {SCNNode[]} lightProbes -
* @param {number} time -
* @returns {void}
* @see https://developer.apple.com/documentation/scenekit/scnrenderer/2097153-updateprobes
*/
updateProbesAtTime(lightProbes, time) {
}
//////////////////////
// SCNSceneRenderer //
//////////////////////
// Presenting a Scene
/**
* Required. Displays the specified scene with an animated transition.
* @access public
* @param {SCNScene} scene - The new scene to be displayed.
* @param {SKTransition} transition - An object that specifies the duration and style of the animated transition.
* @param {?SCNNode} pointOfView - The node to use as the pointOfView property when displaying the new scene.
* @param {?function(): void} [completionHandler = null] - A block that SceneKit calls after the transition animation has completed.This block takes no parameters and has no return value.
* @returns {void}
* @desc Use this method to change the scene displayed in a SceneKit view (or other renderer) with an animated transition. For details on transition styles, see SKTransition.
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1523028-present
*/
presentWithIncomingPointOfView(scene, transition, pointOfView, completionHandler = null) {
}
// Managing Scene Display
/**
* Required. The node from which the scene’s contents are viewed for rendering.
* @type {?SCNNode}
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1523982-pointofview
*/
get pointOfView() {
return this._getCameraNode()
}
/**
* Required. The node from which the scene’s contents are viewed for rendering.
* @type {?SCNNode}
* @param {?SCNNode} newValue -
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1523982-pointofview
*/
set pointOfView(newValue) {
this._pointOfView = newValue
}
/**
* Required. The graphics technology SceneKit uses to render the scene.
* @type {SCNRenderingAPI}
* @desc You choose a graphics technology when initializing a scene renderer:When initializing a SCNView object, use the init(frame:options:) initializer and the preferredRenderingAPI key. Alternatively, create a view in Interface Builder and use the Rendering API control in the inspector. During initialization, the view will attempt to use the preferred API, but will fall back to a different API if the preferred one is not supported on the current hardware.To create a SCNRenderer object that renders into your own OpenGL contect, use the init(context:options:) initializer. To create a renderer for use in your own Metal workflow, use the init(device:options:) initializer.The rendering technology used by a SCNLayer object is determined by Core Animation.After initializing a renderer, this property reflects the rendering technology in use.
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522616-renderingapi
*/
get renderingAPI() {
return this._renderingAPI
}
// Preloading Renderer Resources
/**
* Required. Prepares a SceneKit object for rendering.
* @access public
* @param {Object} object - An SCNScene, SCNNode, SCNGeometry, or SCNMaterial instance.
* @param {?function(): boolean} [block = null] - A block that SceneKit calls periodically while preparing the object. The block takes no parameters.Your block should return false to tell SceneKit to continue preparing the object, or true to cancel preparation.Pass nil for this parameter if you do not need an opportunity to cancel preparing the object.
* @returns {boolean} -
* @desc By default, SceneKit lazily loads resources onto the GPU for rendering. This approach uses memory and GPU bandwidth efficiently, but can lead to stutters in an otherwise smooth frame rate when you add large amounts of new content to an animated scene. To avoid such issues, use this method to prepare content for drawing before adding it to the scene. You can call this method on a secondary thread to prepare content asynchronously. SceneKit prepares all content associated with the object parameter you provide. If you provide an SCNMaterial object, SceneKit loads any texture images assigned to its material properties. If you provide an SCNGeometry object, SceneKit loads all materials attached to the geometry, as well as its vertex data. If you provide an SCNNode or SCNScene object, SceneKit loads all geometries and materials associated with the node and all its child nodes, or with the entire node hierarchy of the scene.You can use the block parameter to cancel preparation if content is no longer needed. For example, in a game you might use this method to preload areas of the game world the player is soon to enter, but if the player character dies before entering those areas, you can return true from the block to cancel preloading.You can observe the progress of this operation with the Progress class. For details, see Progress.
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522798-prepare
*/
prepareShouldAbortBlock(object, block = null) {
return false
}
/**
* Required. Prepares the specified SceneKit objects for rendering, using a background thread.
* @access public
* @param {Object[]} objects - An array of containing one or more SCNScene, SCNNode, SCNGeometry, or SCNMaterial instances.
* @param {?function(arg1: boolean): void} [completionHandler = null] - A block that SceneKit calls when object preparation fails or completes.The block takes the following parameter:successtrue if all content was successfully prepared for rendering; otherwise, false.
* @returns {void}
* @desc By default, SceneKit lazily loads resources onto the GPU for rendering. This approach uses memory and GPU bandwidth efficiently, but can lead to stutters in an otherwise smooth frame rate when you add large amounts of new content to an animated scene. To avoid such issues, use this method to prepare content for drawing before adding it to the scene. SceneKit uses a secondary thread to prepare content asynchronously.SceneKit prepares all content associated with the objects you provide. If you provide an SCNMaterial object, SceneKit loads any texture images assigned to its material properties. If you provide an SCNGeometry object, SceneKit loads all materials attached to the geometry, as well as its vertex data. If you provide an SCNNode or SCNScene object, SceneKit loads all geometries and materials associated with the node and all its child nodes, or with the entire node hierarchy of the scene.You can observe the progress of this operation with the Progress class. For details, see Progress.
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1523375-prepare
*/
prepare(objects, completionHandler = null) {
}
// Working With Projected Scene Contents
/**
* Required. Searches the renderer’s scene for objects corresponding to a point in the rendered image.
* @access public
* @param {CGPoint} point -
* @param {?Map<SCNHitTestOption, Object>} [options = null] - A dictionary of options affecting the search. See Hit Testing Options Keys for acceptable values.
* @returns {SCNHitTestResult[]} -
* @desc A 2D point in the rendered screen coordinate space can refer to any point along a line segment in the 3D scene coordinate space. Hit-testing is the process of finding elements of a scene located along this line segment. For example, you can use this method to find the geometry corresponding to a click event in a SceneKit view.
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522929-hittest
*/
hitTest(point, options = null) {
if(this.scene === null){
return []
}
let _options = new Map()
if(options instanceof Map){
_options = options
}else if(Array.isArray(options)){
_options = new Map(options)
}
const cameraNode = this._getCameraNode()
cameraNode.camera._updateProjectionTransform(this._viewRect)
const from = new SCNVector3(point.x, point.y, 0)
const to = new SCNVector3(point.x, point.y, 1.0)
const useGPU = false
if(!useGPU){
return this._hitTestByCPU(cameraNode.viewProjectionTransform, from, to, _options)
}
return this._hitTestByGPU(cameraNode.viewProjectionTransform, from, to, _options)
}
_initializeHitFrameBuffer() {
const gl = this.context
const width = this._viewRect.size.width
const height = this._viewRect.size.height
this._hitFrameBuffer = gl.createFramebuffer()
this._hitDepthBuffer = gl.createRenderbuffer()
gl.bindFramebuffer(gl.FRAMEBUFFER, this._hitFrameBuffer)
gl.bindRenderbuffer(gl.RENDERBUFFER, this._hitDepthBuffer)
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height)
this._hitObjectIDTexture = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, this._hitObjectIDTexture)
// texImage2D(target, level, internalformat, width, height, border, format, type, source)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null)
this._hitFaceIDTexture = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, this._hitFaceIDTexture)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null)
this._hitPositionTexture = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, this._hitPositionTexture)
//gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, width, height, 0, gl.RGB, gl.UNSIGNED_BYTE, null)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null)
this._hitNormalTexture = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, this._hitNormalTexture)
//gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, width, height, 0, gl.RGB, gl.UNSIGNED_BYTE, null)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null)
//gl.framebufferRenderbuffer(target, attachment, renderbuffertarget, renderbuffer)
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, this._hitDepthBuffer)
//gl.framebufferTexture2D(target, attachment, textarget, texture, level)
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this._hitObjectIDTexture, 0)
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, this._hitFaceIDTexture, 0)
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT2, gl.TEXTURE_2D, this._hitPositionTexture, 0)
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT3, gl.TEXTURE_2D, this._hitNormalTexture, 0)
gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2, gl.COLOR_ATTACHMENT3])
gl.bindRenderbuffer(gl.RENDERBUFFER, null)
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
}
/**
* @access private
* @param {SCNMatrix4} viewProjectionMatrix -
* @param {SCNVector3} from -
* @param {SCNVector3} to -
* @param {Object} options -
* @returns {SCNHitTestResult[]} -
*/
_hitTestByCPU(viewProjectionMatrix, from, to, options) {
const result = []
const invVp = viewProjectionMatrix.invert()
const rayFrom = from.transform(invVp)
const rayTo = to.transform(invVp)
//console.log(`rayFrom: ${rayFrom.float32Array()}`)
//console.log(`rayTo : ${rayTo.float32Array()}`)
//const rayVec = rayTo.sub(rayFrom)
const renderingArray = this._createRenderingNodeArray()
//console.log(`renderingArray.length: ${renderingArray.length}`)
let categoryBitMask = options.get(SCNHitTestOption.categoryBitMask)
if(typeof categoryBitMask === 'undefined'){
categoryBitMask = -1
}
for(const node of renderingArray){
if(node.categoryBitMask & categoryBitMask){
//result.push(...this._nodeHitTestByCPU(node, rayFrom, rayVec))
const hits = SCNPhysicsWorld._hitTestWithSegmentNode(rayFrom, rayTo, node)
if(hits.length > 0){
// convert from the child's coordinate to this node's coordinate
for(const h of hits){
h._node = node
h._worldCoordinates = node.convertPositionTo(h._localCoordinates, null)
h._worldNormal = node.convertPositionTo(h._localNormal, null)
}
result.push(...hits)
}
}
}
return result
}
/**
* @access private
* @param {SCNMatrix4} viewProjectionTransform -
* @param {SCNVector3} from -
* @param {SCNVector3} to -
* @param {Map} options -
* @returns {SCNHitTestResult[]} -
*/
_hitTestByGPU(viewProjectionTransform, from, to, options) {
const result = []
const gl = this._context
if(this._hitFrameBuffer === null){
this._initializeHitFrameBuffer()
}
const prg = this._defaultHitTestProgram
const hitTestProgram = prg._glProgram
this._useProgram(prg)
//gl.useProgram(hitTestProgram)
gl.bindFramebuffer(gl.FRAMEBUFFER, this._hitFrameBuffer)
gl.depthMask(true)
gl.depthFunc(gl.LEQUAL)
gl.enable(gl.SCISSOR_TEST)
gl.disable(gl.BLEND)
gl.clearColor(0, 0, 0, 0)
gl.clearDepth(1.0)
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
const x = (from.x + 1.0) * 0.5 * this._viewRect.size.width
const y = (from.y + 1.0) * 0.5 * this._viewRect.size.height
let sx = x - 1
let sy = y - 1
if(sx < 0){
sx = 0
}else if(sx + 3 > this._viewRect.size.width){
sx = this._viewRect.size.width - 3
}
if(sy < 0){
sy = 0
}else if(sy + 3 > this._viewRect.size.height){
sy = this._viewRect.size.width - 3
}
gl.scissor(sx, sy, 3, 3)
gl.uniformMatrix4fv(gl.getUniformLocation(hitTestProgram, 'viewProjectionTransform'), false, viewProjectionTransform.float32Array())
let backFaceCulling = options.get(SCNHitTestOption.backFaceCulling)
if(typeof backFaceCulling === 'undefined'){
backFaceCulling = true
}
if(backFaceCulling){
gl.enable(gl.CULL_FACE)
gl.cullFace(gl.BACK)
}else{
gl.disable(gl.CULL_FACE)
}
let categoryBitMask = options.get(SCNHitTestOption.categoryBitMask)
if(typeof categoryBitMask === 'undefined'){
categoryBitMask = -1
}
let ignoreHiddenNodes = options.get(SCNHitTestOption.ignoreHiddenNodes)
if(typeof ignoreHiddenNodes === 'undefined'){
ignoreHiddenNodes = true
}
const renderingArray = this._createRenderingNodeArray()
const len = renderingArray.length
for(let i=0; i<len; i++){
const node = renderingArray[i]
if((node.categoryBitMask & categoryBitMask) === 0){
continue
}
if(ignoreHiddenNodes && node.isHidden){
continue
}
this._renderNodeForHitTest(node, i + 100, options)
}
const objectIDBuf = new Uint8Array(4)
gl.readBuffer(gl.COLOR_ATTACHMENT0)
gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, objectIDBuf, 0)
const objectID = objectIDBuf[0] * 256 + objectIDBuf[1]
const geometryIndex = objectIDBuf[2] * 256 + objectIDBuf[3]
const faceIDBuf = new Uint8Array(4)
gl.readBuffer(gl.COLOR_ATTACHMENT1)
gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, faceIDBuf, 0)
const faceIndex = faceIDBuf[0] * 16777216 + faceIDBuf[1] * 65536 + faceIDBuf[2] * 256 + faceIDBuf[3]
const positionBuf = new Uint8Array(4)
gl.readBuffer(gl.COLOR_ATTACHMENT2)
gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, positionBuf, 0)
//const screenPos = new SCNVector3(positionBuf[0] / 127.5 - 1.0, positionBuf[1] / 127.5 - 1.0, positionBuf[2] / 127.5 - 1.0)
//const position = screenPos.transform(viewProjectionTransform.invert())
const p = (((positionBuf[3] / 255.0 + positionBuf[2]) / 255.0 + positionBuf[1] / 255.0) + positionBuf[0]) / 255.0
const position = from.lerp(to, p)
const normalBuf = new Uint8Array(4)
gl.readBuffer(gl.COLOR_ATTACHMENT3)
gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, normalBuf, 0)
const normal = new SCNVector3(normalBuf[0] / 127.5 - 1.0, normalBuf[1] / 127.5 - 1.0, normalBuf[2] / 127.5 - 1.0)
//console.log('***** Hit Result *****')
//console.log(`objectID: ${objectID}`)
//console.log(`geometryIndex: ${geometryIndex}`)
//console.log(`faceIndex: ${faceIndex}`)
//console.log(`position: ${position.floatArray()}`)
//console.log(`normal: ${normal.floatArray()}`)
//console.log('**********************')
if(objectID >= 100){
const r = new SCNHitTestResult()
const node = renderingArray[objectID - 100]
const worldInv = node.presentation._worldTransform.invert()
r._node = node
r._geometryIndex = geometryIndex
r._faceIndex = faceIndex
r._worldCoordinates = position
r._worldNormal = normal
r._modelTransform = node.presentation._worldTransform
r._localCoordinates = position.transform(worldInv)
r._localNormal = normal.transform(worldInv)
result.push(r)
}
gl.disable(gl.SCISSOR_TEST)
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
return result
}
/**
* @access private
* @param {SCNVector3} from -
* @param {SCNVector3} to -
* @param {Map} options -
* @param {Object} _options -
* @returns {SCNHitTestResult[]} -
*/
_physicsHitTestByGPU(from, to, options, _options) {
const result = []
const gl = this._context
const viewProjectionTransform = this._createViewProjectionTransformForRay(from, to)
const _from = from.transform(viewProjectionTransform)
const _to = to.transform(viewProjectionTransform)
if(this._hitFrameBuffer === null){
this._initializeHitFrameBuffer()
}
const prg = this._defaultHitTestProgram
const hitTestProgram = prg._glProgram
//gl.useProgram(hitTestProgram)
this._useProgram(prg)
gl.bindFramebuffer(gl.FRAMEBUFFER, this._hitFrameBuffer)
gl.depthMask(true)
gl.depthFunc(gl.LEQUAL)
gl.enable(gl.SCISSOR_TEST)
gl.disable(gl.BLEND)
gl.clearColor(0, 0, 0, 0)
gl.clearDepth(1.0)
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
// screen position
const x = (_from.x + 1.0) * 0.5 * this._viewRect.size.width
const y = (_from.y + 1.0) * 0.5 * this._viewRect.size.height
// left top of the scissor area
const areaSize = 3
let sx = x - 1
let sy = y - 1
if(sx < 0){
sx = 0
}else if(sx + areaSize > this._viewRect.size.width){
sx = this._viewRect.size.width - areaSize
}
if(sy < 0){
sy = 0
}else if(sy + areaSize > this._viewRect.size.height){
sy = this._viewRect.size.width - areaSize
}
gl.scissor(sx, sy, areaSize, areaSize)
gl.uniformMatrix4fv(gl.getUniformLocation(hitTestProgram, 'viewProjectionTransform'), false, viewProjectionTransform.float32Array())
let backFaceCulling = options.get(SCNPhysicsWorld.TestOption.backfaceCulling)
if(typeof backFaceCulling === 'undefined'){
backFaceCulling = true
}
if(backFaceCulling){
gl.enable(gl.CULL_FACE)
gl.cullFace(gl.BACK)
}else{
gl.disable(gl.CULL_FACE)
}
let collisionBitMask = options.get(SCNPhysicsWorld.TestOption.collisionBitMask)
if(typeof collisionBitMask === 'undefined'){
collisionBitMask = -1
}
let searchMode = options.get(SCNPhysicsWorld.TestOption.searchMode)
if(typeof searchMode === 'undefined'){
searchMode = SCNPhysicsWorld.TestSearchMode.closest
}
let renderingArray = null
if(_options && _options.targets){
renderingArray = _options.targets
collisionBitMask = -1
}else{
renderingArray = this._createRenderingPhysicsNodeArray()
}
const len = renderingArray.length
for(let i=0; i<len; i++){
const node = renderingArray[i]
const body = node.physicsBody
if((body.categoryBitMask & collisionBitMask) === 0){
continue
}
this._renderPhysicsNodeForHitTest(node, i + 100, options)
}
const objectIDBuf = new Uint8Array(4)
gl.readBuffer(gl.COLOR_ATTACHMENT0)
gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, objectIDBuf, 0)
const objectID = objectIDBuf[0] * 256 + objectIDBuf[1]
const geometryIndex = objectIDBuf[2] * 256 + objectIDBuf[3]
const faceIDBuf = new Uint8Array(4)
gl.readBuffer(gl.COLOR_ATTACHMENT1)
gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, faceIDBuf, 0)
const faceIndex = faceIDBuf[0] * 16777216 + faceIDBuf[1] * 65536 + faceIDBuf[2] * 256 + faceIDBuf[3]
const positionBuf = new Uint8Array(4)
gl.readBuffer(gl.COLOR_ATTACHMENT2)
gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, positionBuf, 0)
//const screenPos = new SCNVector3(positionBuf[0] / 127.5 - 1.0, positionBuf[1] / 127.5 - 1.0, positionBuf[2] / 127.5 - 1.0)
//const position = screenPos.transform(viewProjectionTransform.invert())
const p = (((positionBuf[3] / 255.0 + positionBuf[2]) / 255.0 + positionBuf[1] / 255.0) + positionBuf[0]) / 255.0
const screenPos = _from.lerp(_to, p)
const position = screenPos.transform(viewProjectionTransform.invert())
const normalBuf = new Uint8Array(4)
gl.readBuffer(gl.COLOR_ATTACHMENT3)
gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, normalBuf, 0)
const normal = new SCNVector3(normalBuf[0] / 127.5 - 1.0, normalBuf[1] / 127.5 - 1.0, normalBuf[2] / 127.5 - 1.0)
//console.log('***** Hit Result *****')
//console.log(`objectID: ${objectID}`)
//console.log(`geometryIndex: ${geometryIndex}`)
//console.log(`faceIndex: ${faceIndex}`)
//console.log(`from: ${from.floatArray()}`)
//console.log(`to: ${to.floatArray()}`)
//console.log(`positionBuf: ${positionBuf[0]}, ${positionBuf[1]}, ${positionBuf[2]}`)
//console.log(`sPos: ${screenPos.floatArray()}`)
//console.log(`position: ${position.floatArray()}`)
//console.log(`normal: ${normal.floatArray()}`)
//console.log('**********************')
if(objectID >= 100){
const r = new SCNHitTestResult()
const node = renderingArray[objectID - 100]
const worldInv = node.presentation._worldTransform.invert()
r._node = node
r._geometryIndex = geometryIndex
r._faceIndex = faceIndex
r._worldCoordinates = position
r._worldNormal = normal
r._modelTransform = node.presentation._worldTransform
r._localCoordinates = position.transform(worldInv)
r._localNormal = normal.transform(worldInv)
result.push(r)
}
gl.disable(gl.SCISSOR_TEST)
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
return result
}
/**
*
* @access private
* @param {SCNVector3} from -
* @param {SCNVector3} to -
* @returns {SCNMatrix4} -
*/
_createViewProjectionTransformForRay(from, to) {
const vec = to.sub(from)
const len = vec.length()
const zNear = 1
const zFar = zNear + len
const proj = new SCNMatrix4()
proj.m11 = 1
proj.m22 = 1
proj.m33 = -(zFar + zNear) / len
proj.m34 = -1
proj.m43 = -2 * zFar * zNear / len
// TODO: use an orthographic projection
//proj.m33 = -2 / len
//proj.m43 = -(zFar + zNear) / len
//proj.m44 = 1
const view = new SCNMatrix4()
const up = new SCNVector3(0, 1, 0)
if(vec.x === 0 && vec.z === 0){
up.y = 0
up.z = 1
}
const f = vec.normalize()
const s = f.cross(up).normalize()
const u = s.cross(f).normalize()
view.m11 = s.x
view.m21 = s.y
view.m31 = s.z
view.m12 = u.x
view.m22 = u.y
view.m32 = u.z
view.m13 = -f.x
view.m23 = -f.y
view.m33 = -f.z
view.m44 = 1
const eye = from.sub(f.mul(zNear))
const t = eye.transform(view)
view.m41 = -t.x
view.m42 = -t.y
view.m43 = -t.z
return view.mult(proj)
}
/**
* Required. Returns a Boolean value indicating whether a node might be visible from a specified point of view.
* @access public
* @param {SCNNode} node - The node whose visibility is to be tested.
* @param {SCNNode} pointOfView - A node defining a point of view, as used by the pointOfView property.
* @returns {boolean} -
* @desc Any node containing a camera or spotlight may serve as a point of view (see the pointOfView property for details). Such a node defines a viewing frustum—a portion of the scene’s coordinate space, shaped like a truncated pyramid, that encloses all points visible from that point of view.Use this method to test whether a node lies within the viewing frustum defined by another node (which may or may not be the scene renderer’s current pointOfView node). For example, in a game scene containing multiple camera nodes, you could use this method to determine which camera is currently best for viewing a moving player character.Note that this method does not perform occlusion testing. That is, it returns true if the tested node lies within the specified viewing frustum regardless of whether that node’s contents are obscured by other geometry.
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522647-isnode
*/
isNodeInsideFrustumOf(node, pointOfView) {
return false
}
/**
* Required. Returns all nodes that might be visible from a specified point of view.
* @access public
* @param {SCNNode} pointOfView - A node defining a point of view, as used by the pointOfView property.
* @returns {SCNNode[]} -
* @desc Any node containing a camera or spotlight may serve as a point of view (see the pointOfView property for details). Such a node defines a viewing frustum—a portion of the scene’s coordinate space, shaped like a truncated pyramid, that encloses all points visible from that point of view.Use this method find all nodes whose content lies within the viewing frustum defined by another node (which may or may not be the scene renderer’s current pointOfView node).Note that this method does not perform occlusion testing. That is, the returned array includes any node that lies within the specified viewing frustum regardless of whether that node’s contents are obscured by other geometry.
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522942-nodesinsidefrustum
*/
nodesInsideFrustumOf(pointOfView) {
return null
}
/**
* Required. Projects a point from the 3D world coordinate system of the scene to the 2D pixel coordinate system of the renderer.
* @access public
* @param {SCNVector3} point - A point in the world coordinate system of the renderer’s scene.
* @returns {SCNVector3} -
* @desc The z-coordinate of the returned point describes the depth of the projected point relative to the near and far clipping planes of the renderer’s viewing frustum (defined by its pointOfView node). Projecting a point on the near clipping plane returns a point whose z-coordinate is 0.0; projecting a point on the far clipping plane returns a point whose z-coordinate is 1.0.
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1524089-projectpoint
*/
projectPoint(point) {
return null
}
/**
* Required. Unprojects a point from the 2D pixel coordinate system of the renderer to the 3D world coordinate system of the scene.
* @access public
* @param {SCNVector3} point - A point in the screen-space (view, layer, or GPU viewport) coordinate system of the scene renderer.
* @returns {SCNVector3} -
* @desc The z-coordinate of the point parameter describes the depth at which to unproject the point relative to the near and far clipping planes of the renderer’s viewing frustum (defined by its pointOfView node). Unprojecting a point whose z-coordinate is 0.0 returns a point on the near clipping plane; unprojecting a point whose z-coordinate is 1.0 returns a point on the far clipping plane.A 2D point in the rendered screen coordinate space can refer to any point along a line segment in the 3D scene coordinate space. To test for scene contents along this line—for example, to find the geometry corresponding to the location of a click event in a view—use the hitTest(_:options:) method.
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522631-unprojectpoint
*/
unprojectPoint(point) {
return null
}
// Customizing Scene Rendering with Metal
/**
* Required. The Metal render command encoder in use for the current SceneKit rendering pass.
* @type {?MTLRenderCommandEncoder}
* @desc Use this render command encoder to encode additional rendering commands before or after SceneKit draws its own content.This property is valid only during the SceneKit rendering loop—that is, within one of the methods defined in the SCNSceneRendererDelegate protocol. Accessing this property at any other time returns nil.
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522609-currentrendercommandencoder
*/
get currentRenderCommandEncoder() {
return this._currentRenderCommandEncoder
}
/**
* Required. The Metal device this renderer uses for rendering.
* @type {?MTLDevice}
* @desc Use this property to create or look up other Metal resources that use the same device as your SceneKit renderer.NoteThis property is valid only for scene renderers whose renderingAPI value is metal. You create a SceneKit view that renders using Metal with the preferredRenderingAPI initialization option or in Interface Builder, or an SCNRenderer that uses Metal with the init(device:options:) method. For OpenGL-based scene renderers, this property’s value is always nil.
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1523935-device
*/
get device() {
return this._device
}
/**
* Required. The Metal command queue this renderer uses for rendering.
* @type {?MTLCommandQueue}
* @desc Use this property to schedule additional command buffers for the Metal device to execute as part of the render cycle. For example, you can use a compute command encoder to modify the vertex data in a Metal buffer for use by a SCNGeometrySource object.NoteThis property is valid only for scene renderers whose renderingAPI value is metal. You create a SceneKit view that renders using Metal with the preferredRenderingAPI initialization option or in Interface Builder, or an SCNRenderer that uses Metal with the init(device:options:) method. For OpenGL-based scene renderers, this property’s value is always nil.
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1523974-commandqueue
*/
get commandQueue() {
return this._commandQueue
}
/**
* Required. The Metal pixel format for the renderer’s color output.
* @type {MTLPixelFormat}
* @desc Use this property, along with the depthPixelFormat and stencilPixelFormat properties, if you perform custom drawing with Metal (see the SCNSceneRendererDelegate and SCNNodeRendererDelegate classes) and need to create a new MTLRenderPipelineState object to change the GPU state as part of your rendering.NoteThis property is valid only for scene renderers whose renderingAPI value is metal. You create a SceneKit view that renders using Metal with the preferredRenderingAPI initialization option or in Interface Builder, or an SCNRenderer that uses Metal with the init(device:options:) method. For OpenGL-based scene renderers, this property’s value is always nil.
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1523701-colorpixelformat
*/
get colorPixelFormat() {
return this._colorPixelFormat
}
/**
* Required. The Metal pixel format for the renderer’s depth buffer.
* @type {MTLPixelFormat}
* @desc Use this property, along with the colorPixelFormat and stencilPixelFormat properties, if you perform custom drawing with Metal (see the SCNSceneRendererDelegate and SCNNodeRendererDelegate classes) and need to create a new MTLRenderPipelineState object to change the GPU state as part of your rendering.NoteThis property is valid only for scene renderers whose renderingAPI value is metal. You create a SceneKit view that renders using Metal with the preferredRenderingAPI initialization option or in Interface Builder, or an SCNRenderer that uses Metal with the init(device:options:) method. For OpenGL-based scene renderers, this property’s value is always nil.
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1523780-depthpixelformat
*/
get depthPixelFormat() {
return this._depthPixelFormat
}
/**
* Required. The Metal pixel format for the renderer’s stencil buffer.
* @type {MTLPixelFormat}
* @desc Use this property, along with the depthPixelFormat and colorPixelFormat properties, if you perform custom drawing with Metal (see the SCNSceneRendererDelegate and SCNNodeRendererDelegate classes) and need to create a new MTLRenderPipelineState object to change the GPU state as part of your rendering.NoteThis property is valid only for scene renderers whose renderingAPI value is metal. You create a SceneKit view that renders using Metal with the preferredRenderingAPI initialization option or in Interface Builder, or an SCNRenderer that uses Metal with the init(device:options:) method. For OpenGL-based scene renderers, this property’s value is always nil.
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1523315-stencilpixelformat
*/
get stencilPixelFormat() {
return this._stencilPixelFormat
}
// Customizing Scene Rendering with OpenGL
/**
* Required. The OpenGL rendering context that SceneKit uses for rendering the scene.
* @type {?Object}
* @desc In macOS, the value of this property is a Core OpenGL cglContextObj object.In iOS, the value of this property is an EAGLContext object.
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522840-context
*/
get context() {
return this._context
}
_setContext(context) {
this._context = context
this._createDummyTexture()
}
// Working With Positional Audio
/**
* Required. The 3D audio mixing node SceneKit uses for positional audio effects.
* @type {AVAudioEnvironmentNode}
* @desc SceneKit uses this audio node to spatialize sounds from SCNAudioPlayer objects attached to nodes in the scene. You can use this object in conjunction with the audioEngine property to rearrange the audio graph to add other, non-spatialized audio sources or mix in audio processing effects.
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1523582-audioenvironmentnode
*/
get audioEnvironmentNode() {
return this._audioEnvironmentNode
}
/**
* Required. The audio engine SceneKit uses for playing scene sounds.
* @type {AVAudioEngine}
* @desc SceneKit uses this audio engine to play sounds from SCNAudioPlayer objects attached to nodes in the scene. You can use this object directly to add other sound sources not related to scene contents, or to add other sound processing nodes or mixing nodes to the audio engine. To identify the node SceneKit uses for spatializing scene sounds when connecting other nodes, use the audioEnvironmentNode property.
* @see https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522686-audioengine
*/
get audioEngine() {
return this._audioEngine
}
/**
* @access private
* @param {SCNGeometry} geometry -
* @returns {SCNProgram} -
*/
_getProgramForGeometry(geometry) {
if(geometry.program !== null && geometry.program._programCompiled){
return geometry.program
}
if(geometry.program || geometry.shaderModifiers !== null || geometry._shadableHelper !== null){
this._compileProgramForObject(geometry)
}
for(const material of geometry.materials){
if(material.program || material.shaderModifiers !== null || material._shadableHelper !== null){
this._compileProgramForObject(material)
}
}
if(geometry.program){
return geometry.program
}
return this._defaultProgram
}
/**
* @access private
* @param {SCNShadable} obj -
* @returns {void}
*/
_compileProgramForObject(obj) {
const gl = this.context
let p = obj.program
if(!p){
p = new SCNProgram()
obj.program = p
}else if(p._programCompiled){
return p
}
p._parentObject = obj
const glProgram = p._getGLProgramForContext(gl)
const vsText = this._vertexShaderForObject(obj)
const fsText = this._fragmentShaderForObject(obj)
// initialize vertex shader
const vertexShader = gl.createShader(gl.VERTEX_SHADER)
gl.shaderSource(vertexShader, vsText)
gl.compileShader(vertexShader)
if(!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)){
const info = gl.getShaderInfoLog(vertexShader)
throw new Error(`vertex shader compile error: ${info}`)
}
p._glVertexShader = vertexShader
// initialize fragment shader
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
gl.shaderSource(fragmentShader, fsText)
gl.compileShader(fragmentShader)
if(!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)){
const info = gl.getShaderInfoLog(fragmentShader)
throw new Error(`fragment shader compile error: ${info}`)
}
p._glFragmentShader = fragmentShader
gl.attachShader(glProgram, vertexShader)
gl.attachShader(glProgram, fragmentShader)
// link program object
gl.linkProgram(glProgram)
if(!gl.getProgramParameter(glProgram, gl.LINK_STATUS)){
const info = gl.getProgramInfoLog(glProgram)
throw new Error(`program link error: ${info}`)
}
p._programCompiled = true
return p
}
/**
* @access private
* @param {SCNProgram} program -
* @returns {void}
*/
_useProgram(program) {
if(this._currentProgram === program){
return
}
const gl = this.context
const glProgram = program._getGLProgramForContext(gl)
gl.useProgram(glProgram)
program._setDummyTextureForContext(gl)
this._currentProgram = program
}
/**
* @access private
* @param {SCNProgram} program -
* @returns {void}
*/
_switchProgram(program) {
if(this._currentProgram === program){
return
}
const gl = this.context
const glProgram = program._getGLProgramForContext(gl)
gl.useProgram(glProgram)
// set dummy textures
program._setDummyTextureForContext(gl)
// set shadow textures
const lights = this._lightNodes
for(let i=0; i<lights.directionalShadow.length; i++){
const node = lights.directionalShadow[i]
const symbol = `TEXTURE${i+8}`
gl.activeTexture(gl[symbol])
gl.bindTexture(gl.TEXTURE_2D, node.presentation.light._shadowDepthTexture)
}
// bind buffers
const cameraIndex = gl.getUniformBlockIndex(glProgram, 'cameraUniform')
gl.uniformBlockBinding(glProgram, cameraIndex, _cameraLoc)
gl.bindBufferBase(gl.UNIFORM_BUFFER, _cameraLoc, this._cameraBuffer)
const fogIndex = gl.getUniformBlockIndex(glProgram, 'fogUniform')
gl.uniformBlockBinding(glProgram, fogIndex, _fogLoc)
gl.bindBufferBase(gl.UNIFORM_BUFFER, _fogLoc, this._fogBuffer)
const lightIndex = gl.getUniformBlockIndex(glProgram, 'lightUniform')
gl.uniformBlockBinding(glProgram, lightIndex, _lightLoc)
gl.bindBufferBase(gl.UNIFORM_BUFFER, _lightLoc, this._lightBuffer)
const scnLightsIndex = gl.getUniformBlockIndex(glProgram, 'SCNLightsUniform')
gl.uniformBlockBinding(glProgram, scnLightsIndex, _scnLightsLoc)
gl.bindBufferBase(gl.UNIFORM_BUFFER, _scnLightsLoc, this._scnLightsBuffer)
// set uniform variables
const uniformTime = gl.getUniformLocation(glProgram, 'u_time')
if(uniformTime){
// this._time might be too large.
const time = this._time % 100000.0
gl.uniform1f(uniformTime, time)
}
const obj = program._parentObject
if(obj){
// bind custom uniforms
let textureNo = 16 // TEXTURE0-15 is reserved for the default renderer (0-11: material, 12-15: shadow)
for(const key of Object.keys(obj._valuesForUndefinedKeys)){
const loc = gl.getUniformLocation(glProgram, key)
if(loc !== null){
const val = obj._valuesForUndefinedKeys[key]
if(typeof val === 'number'){
gl.uniform1f(loc, val)
}else if(_InstanceOf(val, SCNVector3)){
gl.uniform3fv(loc, val.float32Array())
}else if(_InstanceOf(val, SCNVector4) || _InstanceOf(val, SKColor)){
gl.uniform4fv(loc, val.float32Array())
}else if(_InstanceOf(val, SCNMaterialProperty)){
// TODO: refactoring: SCNGeometry has the same function
if(val._contents instanceof Image){
//val._contents = this._createTexture(gl, val._contents)
const image = val._contents
const texture = gl.createTexture()
const canvas = document.createElement('canvas')
canvas.width = image.naturalWidth
canvas.height = 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)
val._contents = texture
}
if(val._contents instanceof WebGLTexture){
gl.uniform1i(loc, textureNo)
gl.activeTexture(gl[`TEXTURE${textureNo}`])
// TODO: check texture type
gl.bindTexture(gl.TEXTURE_2D, val._contents)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, val._magnificationFilterFor(gl))
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, val._minificationFilterFor(gl))
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, val._wrapSFor(gl))
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, val._wrapTFor(gl))
}
textureNo += 1
}
// TODO: implement for other types
}
}
}
this._currentProgram = program
}
/**
* @access private
* @type {SCNProgram}
*/
get _defaultProgram() {
const numLightsChanged = this._numLightsChanged()
if(this.__defaultProgram !== null && !numLightsChanged){
return this.__defaultProgram
}
const gl = this.context
if(this.__defaultProgram === null){
this.__defaultProgram = new SCNProgram()
}
const p = this.__defaultProgram
const glProgram = p._getGLProgramForContext(gl)
const vsText = this._defaultVertexShader
const fsText = this._defaultFragmentShader
// initialize vertex shader
const vertexShader = gl.createShader(gl.VERTEX_SHADER)
gl.shaderSource(vertexShader, vsText)
gl.compileShader(vertexShader)
if(!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)){
const info = gl.getShaderInfoLog(vertexShader)
throw new Error(`vertex shader compile error: ${info}`)
}
this.__defaultProgram._glVertexShader = vertexShader
// initialize fragment shader
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
gl.shaderSource(fragmentShader, fsText)
gl.compileShader(fragmentShader)
if(!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)){
const info = gl.getShaderInfoLog(fragmentShader)
throw new Error(`fragment shader compile error: ${info}`)
}
this.__defaultProgram._glFragmentShader = fragmentShader
gl.attachShader(glProgram, vertexShader)
gl.attachShader(glProgram, fragmentShader)
// link program object
gl.linkProgram(glProgram)
if(!gl.getProgramParameter(glProgram, gl.LINK_STATUS)){
const info = gl.getProgramInfoLog(glProgram)
throw new Error(`program link error: ${info}`)
}
this._switchProgram(p)
gl.enable(gl.DEPTH_TEST)
gl.depthFunc(gl.LEQUAL)
gl.enable(gl.BLEND)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.enable(gl.CULL_FACE)
gl.cullFace(gl.BACK)
// set default textures to prevent warnings
//this._setDummyTextureAsDefault(p)
return this.__defaultProgram
}
/**
* @access private
* @type {SCNProgram}
*/
get _defaultPBRProgram() {
const numLightsChanged = this._numLightsChanged()
if(this.__defaultPBRProgram !== null && !numLightsChanged){
return this.__defaultPBRProgram
}
const gl = this.context
if(this.__defaultPBRProgram === null){
this.__defaultPBRProgram = new SCNProgram()
}
const p = this.__defaultPBRProgram
const glProgram = p._getGLProgramForContext(gl)
const vsText = this._defaultVertexShader
const fsText = this._defaultPBRFragmentShader
// initialize vertex shader
const vertexShader = gl.createShader(gl.VERTEX_SHADER)
gl.shaderSource(vertexShader, vsText)
gl.compileShader(vertexShader)
if(!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)){
const info = gl.getShaderInfoLog(vertexShader)
throw new Error(`vertex shader compile error: ${info}`)
}
this.__defaultPBRProgram._glVertexShader = vertexShader
// initialize fragment shader
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
gl.shaderSource(fragmentShader, fsText)
gl.compileShader(fragmentShader)
if(!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)){
const info = gl.getShaderInfoLog(fragmentShader)
throw new Error(`fragment shader compile error: ${info}`)
}
this.__defaultPBRProgram._glFragmentShader = fragmentShader
gl.attachShader(glProgram, vertexShader)
gl.attachShader(glProgram, fragmentShader)
// link program object
gl.linkProgram(glProgram)
if(!gl.getProgramParameter(glProgram, gl.LINK_STATUS)){
const info = gl.getProgramInfoLog(glProgram)
throw new Error(`program link error: ${info}`)
}
this._switchProgram(p)
gl.enable(gl.DEPTH_TEST)
gl.depthFunc(gl.LEQUAL)
gl.enable(gl.BLEND)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.enable(gl.CULL_FACE)
gl.cullFace(gl.BACK)
// set default textures to prevent warnings
//this._setDummyTextureAsDefault(p)
return this.__defaultPBRProgram
}
/**
* @access private
* @param {SCNGeometry} geometry -
* @returns {SCNProgram} -
*/
_programForGeometry(geometry) {
}
/**
* @access private
* @returns {string} -
*/
get _defaultVertexShader() {
return this._replaceTexts(_SCNDefaultVertexShader)
}
/**
* @access private
* @param {SCNShadable} obj -
* @returns {string} -
*/
_vertexShaderForObject(obj) {
let txt = obj.program.vertexShader
if(!txt){
txt = _SCNDefaultVertexShader
}
return this._replaceTexts(txt, obj)
}
/**
* @access private
* @returns {string} -
*/
get _defaultFragmentShader() {
return this._replaceTexts(_SCNDefaultFragmentShader)
}
/**
* @access private
* @returns {string} -
*/
get _defaultPBRFragmentShader() {
return this._replaceTexts(_SCNDefaultPBRFragmentShader)
}
/**
* @access private
* @param {SCNShadable} obj -
* @returns {string} -
*/
_fragmentShaderForObject(obj) {
let txt = obj.program.fragmentShader
if(!txt){
txt = _SCNDefaultFragmentShader
}
return this._replaceTexts(txt, obj)
}
/**
* @access private
* @param {string} text -
* @param {?SCNShadable} [shadable = null] -
* @returns {string} -
*/
_replaceTexts(text, shadable = null) {
const vars = new Map()
const numAmbient = this._numLights[SCNLight.LightType.ambient]
const numDirectional = this._numLights[SCNLight.LightType.directional]
const numDirectionalShadow = this._numLights.directionalShadow
const numOmni = this._numLights[SCNLight.LightType.omni]
const numSpot = this._numLights[SCNLight.LightType.spot]
const numIES = this._numLights[SCNLight.LightType.IES]
const numProbe = this._numLights[SCNLight.LightType.probe]
const shadableHelper = shadable ? shadable._shadableHelper : null
const customProperties = shadable ? shadable._valuesForUndefinedKeys : {}
const shaderModifiers = shadable ? shadable.shaderModifiers : null
vars.set('__NUM_AMBIENT_LIGHTS__', numAmbient)
vars.set('__NUM_DIRECTIONAL_LIGHTS__', numDirectional)
vars.set('__NUM_DIRECTIONAL_SHADOW_LIGHTS__', numDirectionalShadow)
vars.set('__NUM_OMNI_LIGHTS__', numOmni)
vars.set('__NUM_SPOT_LIGHTS__', numSpot)
vars.set('__NUM_IES_LIGHTS__', numIES)
vars.set('__NUM_PROBE_LIGHTS__', numProbe)
vars.set('__USE_SHADER_MODIFIER_GEOMETRY__', 0)
vars.set('__SHADER_MODIFIER_GEOMETRY__', '')
vars.set('__USE_SHADER_MODIFIER_SURFACE__', 0)
vars.set('__SHADER_MODIFIER_SURFACE__', '')
vars.set('__USE_SHADER_MODIFIER_FRAGMENT__', 0)
vars.set('__SHADER_MODIFIER_FRAGMENT__', '')
let customUniform = ''
let customSurface = ''
let customTexcoord = ''
if(shaderModifiers){
for(const key of Object.keys(shaderModifiers)){
const mod = shaderModifiers[key]
const _texts = mod.split(/^\s*#pragma\s+body\s*$/m)
if(_texts.length === 1){
// nothing to do
}else if(_texts.length === 2){
customUniform += _texts[0].replace(/^\s*#pragma\s+.*$/mg, '')
}else{
throw new Error('found multiple "#pragma body" in the shaderModifier')
}
}
}else{
for(const key of Object.keys(customProperties)){
const val = customProperties[key]
if(typeof val === 'number'){
customUniform += `uniform float ${key};`
}else if(_InstanceOf(val, SCNMaterialProperty)){
customUniform += `uniform sampler2D ${key};`
customTexcoord += `_surface.${key}Texcoord = v_texcoord${val.mappingChannel};`
customSurface += `vec2 ${key}Texcoord;`
}else{
// TODO: implement for other types
throw new Error(`custom property for ${key} is not implemented`)
}
}
}
vars.set('__USER_CUSTOM_UNIFORM__', customUniform)
vars.set('__USER_CUSTOM_SURFACE__', customSurface)
vars.set('__USER_CUSTOM_TEXCOORD__', customTexcoord)
let modifiers = null
if(shaderModifiers){
modifiers = shaderModifiers
}else if(shadableHelper){
modifiers = shadableHelper._shaderModifiers
}
if(modifiers){
if(modifiers.SCNShaderModifierEntryPointGeometry){
const _text = this._processShaderText(modifiers.SCNShaderModifierEntryPointGeometry)
vars.set('__USE_SHADER_MODIFIER_GEOMETRY__', 1)
vars.set('__SHADER_MODIFIER_GEOMETRY__', _text)
}
if(modifiers.SCNShaderModifierEntryPointSurface){
const _text = this._processShaderText(modifiers.SCNShaderModifierEntryPointSurface)
vars.set('__USE_SHADER_MODIFIER_SURFACE__', 1)
vars.set('__SHADER_MODIFIER_SURFACE__', _text)
}
if(modifiers.SCNShaderModifierEntryPointFragment){
const _text = this._processShaderText(modifiers.SCNShaderModifierEntryPointFragment)
vars.set('__USE_SHADER_MODIFIER_FRAGMENT__', 1)
vars.set('__SHADER_MODIFIER_FRAGMENT__', _text)
}
}
let vsLighting = ''
let fsLighting = ''
if(numDirectionalShadow > 0){
for(let i=0; i<numDirectionalShadow; i++){
const fsDSText = _fsDirectionalShadow.replace(new RegExp('__I__', 'g'), i)
fsLighting += fsDSText
}
}
vars.set('__FS_LIGHTING__', fsLighting)
let result = text
vars.forEach((value, key) => {
const rex = new RegExp(key, 'g')
result = result.replace(rex, value)
})
return result
}
_processShaderText(text) {
let _text = text
const _texts = text.split(/^\s*#pragma\s+body\s*$/m)
if(_texts.length == 2){
_text = _texts[1].replace(/^\s*#pragma\s+.*$/mg, '')
}
_text = _text.replace(/texture2D/g, 'texture')
_text = _text.replace(/float2/g, 'vec2')
_text = _text.replace(/float3/g, 'vec3')
_text = _text.replace(/float4/g, 'vec4')
_text = _text.replace(/scn_frame\.time/g, 'u_time')
//_text = _text.replace(/#pragma alpha/g, '')
_text = _text.replace(/half /g, 'float ') // FIXME: check semicolon before half
_text = _text.replace(/u_modelTransform/g, 'modelTransform') // TODO: use u_modelTransform
_text = _text.replace(/u_viewTransform/g, 'camera.viewTransform') // TODO: use u_viewTransform
_text = _text.replace(/u_inverseViewTransform/g, 'camera.inverseViewTransform') // TODO: use u_inverseViewTransform
_text = _text.replace(/u_viewProjectionTransform/g, 'caemra.viewProjectionTransform') // TODO: use u_viewTransform
_text = _text.replace(/\s*uniform[^;]*;/g, '')
// workaround for Badger...
_text = _text.replace('uvs.x *= 2', 'uvs.x *= 2.0')
_text = _text.replace('tn * 2 - 1', 'tn * 2.0 - vec3(1)')
_text = _text.replace('tn2 * 2 - 1', 'tn2 * 2.0 - vec3(1)')
// workaround for Fox2...
_text = _text.replace('pow(_surface.ambientOcclusion,3)', 'pow(_surface.ambientOcclusion,3.0)')
_text = _text.replace('pow(AO,5)', 'pow(AO,5.0)')
_text = _text.replace('pow(1.-fresnelBasis , 6)', 'pow(1.-fresnelBasis , 6.0)')
_text = _text.replace('pow(1.-fresnelBasis , 4)', 'pow(1.-fresnelBasis , 4.0)')
_text = _text.replace('vec3(1,0.4,0.0) * 1;', 'vec3(1,0.4,0.0);')
_text = _text.replace('vec3(0.6,0.3,0.2) * 1;', 'vec3(0.6,0.3,0.2);')
_text = _text.replace('vec4 WorldPos', 'vec3 WorldPos')
_text = _text.replace('mult * 5;', 'mult * 5.0;')
_text = _text.replace('mask * (1 - feather) + feather / 2', 'mask * (1.0 - feather) + feather / 2.0')
_text = _text.replace(
'vec4 pos = modelTransform * _geometry.position;',
'vec4 pos = modelTransform * vec4(_geometry.position, 1);'
)
_text = _text.replace('cos((u_time * 0.5 + pos.x) * 2)', 'cos((u_time * 0.5 + pos.x) * 2.0)')
_text = _text.replace('(WorldPos.x * 10)', '(WorldPos.x * 10.0)')
_text = _text.replace('(WorldPos.z + WorldPos.x) * 3)', '(WorldPos.z + WorldPos.x) * 3.0)')
_text = _text.replace('pow(flowmap, 1.0/2.2)', 'pow(flowmap, vec2(1.0/2.2))')
_text = _text.replace(/\(flowmap\/2\)/g, '(flowmap/2.0)')
return _text
}
_initializeVAO(node, glProgram) {
const gl = this.context
const geometry = node.presentation.geometry
const baseGeometry = node.geometry
// prepare vertex array data
const vertexBuffer = geometry._createVertexBuffer(gl, node)
// TODO: retain attribute locations
const positionLoc = gl.getAttribLocation(glProgram, 'position')
const normalLoc = gl.getAttribLocation(glProgram, 'normal')
const tangentLoc = gl.getAttribLocation(glProgram, 'tangent')
const colorLoc = gl.getAttribLocation(glProgram, 'color')
const texcoord0Loc = gl.getAttribLocation(glProgram, 'texcoord0')
const texcoord1Loc = gl.getAttribLocation(glProgram, 'texcoord1')
const boneIndicesLoc = gl.getAttribLocation(glProgram, 'boneIndices')
const boneWeightsLoc = gl.getAttribLocation(glProgram, 'boneWeights')
geometry._vertexArrayObjects = []
const elementCount = node.presentation.geometry.geometryElements.length
for(let i=0; i<elementCount; i++){
const element = node.presentation.geometry.geometryElements[i]
const material = node.presentation.geometry.materials[i]
const vao = gl.createVertexArray()
gl.bindVertexArray(vao)
// initialize vertex buffer
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
gl.bindAttribLocation(glProgram, positionLoc, 'position')
gl.bindAttribLocation(glProgram, normalLoc, 'normal')
gl.bindAttribLocation(glProgram, tangentLoc, 'tangent')
gl.bindAttribLocation(glProgram, colorLoc, 'color')
gl.bindAttribLocation(glProgram, texcoord0Loc, 'texcoord0')
gl.bindAttribLocation(glProgram, texcoord1Loc, 'texcoord1')
gl.bindAttribLocation(glProgram, boneIndicesLoc, 'boneIndices')
gl.bindAttribLocation(glProgram, boneWeightsLoc, 'boneWeights')
// vertexAttribPointer(ulong idx, long size, ulong type, bool norm, long stride, ulong offset)
// position
const posSrc = geometry.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.vertex)[0]
if(posSrc){
gl.enableVertexAttribArray(positionLoc)
gl.vertexAttribPointer(positionLoc, posSrc.componentsPerVector, gl.FLOAT, false, posSrc.dataStride, posSrc.dataOffset)
}else{
gl.disableVertexAttribArray(positionLoc)
}
// normal
const nrmSrc = geometry.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.normal)[0]
if(nrmSrc){
gl.enableVertexAttribArray(normalLoc)
gl.vertexAttribPointer(normalLoc, nrmSrc.componentsPerVector, gl.FLOAT, false, nrmSrc.dataStride, nrmSrc.dataOffset)
}else{
gl.disableVertexAttribArray(normalLoc)
}
// tangent
const tanSrc = geometry.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.tangent)[0]
if(tanSrc){
gl.enableVertexAttribArray(tangentLoc)
gl.vertexAttribPointer(tangentLoc, tanSrc.componentsPerVector, gl.FLOAT, false, tanSrc.dataStride, tanSrc.dataOffset)
}else{
gl.disableVertexAttribArray(tangentLoc)
}
// color
const colorSrc = geometry.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.color)[0]
if(colorSrc){
gl.enableVertexAttribArray(colorLoc)
gl.vertexAttribPointer(colorLoc, colorSrc.componentsPerVector, gl.FLOAT, false, colorSrc.dataStride, colorSrc.dataOffset)
}else{
gl.disableVertexAttribArray(colorLoc)
}
// texcoord0
const tex0Src = geometry.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.texcoord)[0]
if(tex0Src){
//console.log(`texSrc: ${texcoordLoc}, ${texSrc.componentsPerVector}, ${texSrc.dataStride}, ${texSrc.dataOffset}`)
gl.enableVertexAttribArray(texcoord0Loc)
gl.vertexAttribPointer(texcoord0Loc, tex0Src.componentsPerVector, gl.FLOAT, false, tex0Src.dataStride, tex0Src.dataOffset)
}else{
gl.disableVertexAttribArray(texcoord0Loc)
}
// texcoord1
const tex1Src = geometry.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.texcoord)[1]
if(tex1Src){
gl.enableVertexAttribArray(texcoord1Loc)
gl.vertexAttribPointer(texcoord1Loc, tex1Src.componentsPerVector, gl.FLOAT, false, tex1Src.dataStride, tex1Src.dataOffset)
}else{
gl.disableVertexAttribArray(texcoord1Loc)
}
// boneIndices
const indSrc = (node.skinner && node.skinner._useGPU) ? node.skinner._boneIndices : null
if(indSrc){
//console.log(`indSrc: ${boneIndicesLoc}, ${indSrc.componentsPerVector}, ${indSrc.dataStride}, ${indSrc.dataOffset}`)
gl.enableVertexAttribArray(boneIndicesLoc)
gl.vertexAttribPointer(boneIndicesLoc, indSrc.componentsPerVector, gl.FLOAT, false, indSrc.dataStride, indSrc.dataOffset)
}else{
gl.disableVertexAttribArray(boneIndicesLoc)
}
// boneWeights
const wgtSrc = (node.skinner && node.skinner._useGPU) ? node.skinner._boneWeights : null
if(wgtSrc){
//console.log(`wgtSrc: ${boneWeightsLoc}, ${wgtSrc.componentsPerVector}, ${wgtSrc.dataStride}, ${wgtSrc.dataOffset}`)
gl.enableVertexAttribArray(boneWeightsLoc)
gl.vertexAttribPointer(boneWeightsLoc, wgtSrc.componentsPerVector, gl.FLOAT, false, wgtSrc.dataStride, wgtSrc.dataOffset)
}else{
gl.disableVertexAttribArray(boneWeightsLoc)
}
// FIXME: use setting
gl.disable(gl.CULL_FACE)
// initialize index buffer
// FIXME: check geometrySource semantic
const indexBuffer = element._createBuffer(gl)
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
geometry._vertexArrayObjects.push(vao)
}
}
_initializeShadowVAO(node, glProgram) {
const gl = this.context
const geometry = node.presentation.geometry
const baseGeometry = node.geometry
// prepare vertex array data
const vertexBuffer = geometry._createVertexBuffer(gl, node)
// TODO: retain attribute locations
const positionLoc = gl.getAttribLocation(glProgram, 'position')
const boneIndicesLoc = gl.getAttribLocation(glProgram, 'boneIndices')
const boneWeightsLoc = gl.getAttribLocation(glProgram, 'boneWeights')
geometry._shadowVAO = []
const elementCount = node.presentation.geometry.geometryElements.length
for(let i=0; i<elementCount; i++){
const element = node.presentation.geometry.geometryElements[i]
const material = node.presentation.geometry.materials[i]
const shadowVAO = gl.createVertexArray()
gl.bindVertexArray(shadowVAO)
// initialize vertex buffer
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
gl.bindAttribLocation(glProgram, positionLoc, 'position')
gl.bindAttribLocation(glProgram, boneIndicesLoc, 'boneIndices')
gl.bindAttribLocation(glProgram, boneWeightsLoc, 'boneWeights')
// vertexAttribPointer(ulong idx, long size, ulong type, bool norm, long stride, ulong offset)
// position
const posSrc = geometry.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.vertex)[0]
if(posSrc){
gl.enableVertexAttribArray(positionLoc)
gl.vertexAttribPointer(positionLoc, posSrc.componentsPerVector, gl.FLOAT, false, posSrc.dataStride, posSrc.dataOffset)
}else{
gl.disableVertexAttribArray(positionLoc)
}
// boneIndices
const indSrc = (node.skinner && node.skinner._useGPU) ? node.skinner._boneIndices : null
if(indSrc){
gl.enableVertexAttribArray(boneIndicesLoc)
gl.vertexAttribPointer(boneIndicesLoc, indSrc.componentsPerVector, gl.FLOAT, false, indSrc.dataStride, indSrc.dataOffset)
}else{
gl.disableVertexAttribArray(boneIndicesLoc)
}
// boneWeights
const wgtSrc = (node.skinner && node.skinner._useGPU) ? node.skinner._boneWeights : null
if(wgtSrc){
gl.enableVertexAttribArray(boneWeightsLoc)
gl.vertexAttribPointer(boneWeightsLoc, wgtSrc.componentsPerVector, gl.FLOAT, false, wgtSrc.dataStride, wgtSrc.dataOffset)
}else{
gl.disableVertexAttribArray(boneWeightsLoc)
}
// FIXME: use setting
gl.disable(gl.CULL_FACE)
// initialize index buffer
// FIXME: check geometrySource semantic
const indexBuffer = element._createBuffer(gl)
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
geometry._shadowVAO.push(shadowVAO)
}
}
_initializeHitTestVAO(node, glProgram, physics = false) {
const gl = this.context
const geometry = physics ? node.physicsBody.physicsShape._sourceGeometry : node.presentation.geometry
const baseGeometry = physics ? geometry : node.geometry
// TODO: retain attribute locations
const positionLoc = gl.getAttribLocation(glProgram, 'position')
const normalLoc = gl.getAttribLocation(glProgram, 'normal')
const boneIndicesLoc = gl.getAttribLocation(glProgram, 'boneIndices')
const boneWeightsLoc = gl.getAttribLocation(glProgram, 'boneWeights')
geometry._hitTestVAO = []
const elementCount = geometry.geometryElements.length
for(let i=0; i<elementCount; i++){
const element = geometry.geometryElements[i]
const vao = gl.createVertexArray()
gl.bindVertexArray(vao)
gl.bindBuffer(gl.ARRAY_BUFFER, geometry._vertexBuffer)
gl.bindAttribLocation(glProgram, positionLoc, 'position')
gl.bindAttribLocation(glProgram, normalLoc, 'normal')
gl.bindAttribLocation(glProgram, boneIndicesLoc, 'boneIndices')
gl.bindAttribLocation(glProgram, boneWeightsLoc, 'boneWeights')
// vertexAttribPointer(ulong idx, long size, ulong type, bool norm, long stride, ulong offset)
// position
const posSrc = geometry.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.vertex)[0]
if(posSrc){
gl.enableVertexAttribArray(positionLoc)
gl.vertexAttribPointer(positionLoc, posSrc.componentsPerVector, gl.FLOAT, false, posSrc.dataStride, posSrc.dataOffset)
}else{
gl.disableVertexAttribArray(positionLoc)
}
// normal
const nrmSrc = geometry.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.normal)[0]
if(nrmSrc){
gl.enableVertexAttribArray(normalLoc)
gl.vertexAttribPointer(normalLoc, nrmSrc.componentsPerVector, gl.FLOAT, false, nrmSrc.dataStride, nrmSrc.dataOffset)
}else{
gl.disableVertexAttribArray(normalLoc)
}
// boneIndices
const indSrc = node.skinner ? node.skinner._boneIndices : null
if(indSrc){
gl.enableVertexAttribArray(boneIndicesLoc)
gl.vertexAttribPointer(boneIndicesLoc, indSrc.componentsPerVector, gl.FLOAT, false, indSrc.dataStride, indSrc.dataOffset)
}else{
gl.disableVertexAttribArray(boneIndicesLoc)
}
// boneWeights
const wgtSrc = node.skinner ? node.skinner._boneWeights : null
if(wgtSrc){
gl.enableVertexAttribArray(boneWeightsLoc)
gl.vertexAttribPointer(boneWeightsLoc, wgtSrc.componentsPerVector, gl.FLOAT, false, wgtSrc.dataStride, wgtSrc.dataOffset)
}else{
gl.disableVertexAttribArray(boneWeightsLoc)
}
// initialize index buffer
// FIXME: check geometrySource semantic
const indexBuffer = element._createBuffer(gl)
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, element._buffer)
geometry._hitTestVAO.push(vao)
}
}
_initializeCameraBuffer(glProgram) {
const gl = this.context
const cameraIndex = gl.getUniformBlockIndex(glProgram, 'cameraUniform')
this._cameraBuffer = gl.createBuffer()
gl.uniformBlockBinding(glProgram, cameraIndex, _cameraLoc)
gl.bindBufferBase(gl.UNIFORM_BUFFER, _cameraLoc, this._cameraBuffer)
}
_initializeFogBuffer(glProgram) {
const gl = this.context
const fogIndex = gl.getUniformBlockIndex(glProgram, 'fogUniform')
this._fogBuffer = gl.createBuffer()
gl.uniformBlockBinding(glProgram, fogIndex, _fogLoc)
gl.bindBufferBase(gl.UNIFORM_BUFFER, _fogLoc, this._fogBuffer)
}
_initializeLightBuffer(glProgram) {
const gl = this.context
// TODO: replace lightUniform to SCNLightsUniform
const lightIndex = gl.getUniformBlockIndex(glProgram, 'lightUniform')
this._lightBuffer = gl.createBuffer()
gl.uniformBlockBinding(glProgram, lightIndex, _lightLoc)
gl.bindBufferBase(gl.UNIFORM_BUFFER, _lightLoc, this._lightBuffer)
const scnLightsIndex = gl.getUniformBlockIndex(glProgram, 'SCNLightsUniform')
this._scnLightsBuffer = gl.createBuffer()
gl.uniformBlockBinding(glProgram, scnLightsIndex, _scnLightsLoc)
gl.bindBufferBase(gl.UNIFORM_BUFFER, _scnLightsLoc, this._scnLightsBuffer)
for(let i=0; i<this._lightNodes.directionalShadow.length; i++){
const node = this._lightNodes.directionalShadow[i]
const symbol = `TEXTURE${i+_shadowTextureBaseIndex}`
const name = `u_shadowTexture${i}`
gl.uniform1i(gl.getUniformLocation(glProgram, name), i+_shadowTextureBaseIndex)
gl.activeTexture(gl[symbol])
gl.bindTexture(gl.TEXTURE_2D, node.presentation.light._shadowDepthTexture)
}
}
_initializeUBO(node, glProgram) {
const gl = this.context
const geometry = node.presentation.geometry
const materialIndex = gl.getUniformBlockIndex(glProgram, 'materialUniform')
geometry._materialBuffer = gl.createBuffer()
gl.uniformBlockBinding(glProgram, materialIndex, _materialLoc)
gl.bindBufferBase(gl.UNIFORM_BUFFER, _materialLoc, geometry._materialBuffer)
}
_updateVAO(node) {
const gl = this.context
const geometry = node.presentation.geometry
const baseGeometry = node.geometry
geometry._updateVertexBuffer(gl, baseGeometry)
}
get _dummyTexture() {
return this.__dummyTexture
}
_createDummyTexture() {
const gl = this.context
const canvas = document.createElement('canvas')
canvas.width = 1
canvas.height = 1
const context = canvas.getContext('2d')
context.fillStyle = 'rgba(255, 255, 255, 1.0)'
context.fillRect(0, 0, 1, 1)
this.__dummyTexture = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, this.__dummyTexture)
// 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, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, canvas)
gl.bindTexture(gl.TEXTURE_2D, null)
}
/**
* @access private
* @param {SCNProgram} program -
* @returns {void}
*/
//_setDummyTextureAsDefault(program) {
// const gl = this.context
// const p = program
// const texNames = [
// gl.TEXTURE0,
// gl.TEXTURE1,
// gl.TEXTURE2,
// gl.TEXTURE3,
// gl.TEXTURE4,
// gl.TEXTURE5,
// gl.TEXTURE6,
// gl.TEXTURE7
// ]
// const texSymbols = [
// 'u_emissionTexture',
// 'u_ambientTexture',
// 'u_diffuseTexture',
// 'u_specularTexture',
// 'u_reflectiveTexture',
// 'u_transparentTexture',
// 'u_multiplyTexture',
// 'u_normalTexture'
// ]
// for(let i=0; i<texNames.length; i++){
// const texName = texNames[i]
// const symbol = texSymbols[i]
// gl.uniform1i(gl.getUniformLocation(p._glProgram, symbol), i)
// gl.activeTexture(texName)
// gl.bindTexture(gl.TEXTURE_2D, this.__dummyTexture)
// }
//}
_switchToDefaultCamera() {
if(this._pointOfView === null){
this._defaultCameraPosNode.position = new SCNVector3(0, 0, 0)
this._defaultCameraRotNode.rotation = new SCNVector4(0, 0, 0, 0)
this._defaultCameraNode.position = new SCNVector3(0, 0, _defaultCameraDistance)
}else if(this._pointOfView !== this._defaultCameraNode){
const rot = this.pointOfView.presentation._worldRotation
const rotMat = SCNMatrix4.matrixWithRotation(rot)
const pos = this.pointOfView.presentation._worldTranslation
this._defaultCameraPosNode.position = (new SCNVector3(0, 0, -_defaultCameraDistance)).rotate(rotMat).add(pos)
this._defaultCameraRotNode.rotation = rot
this._defaultCameraNode.position = new SCNVector3(0, 0, _defaultCameraDistance)
//console.log(`pov defined: pov.pos: ${this._pointOfView._worldTranslation.float32Array()}`)
//console.log(`pov defined: node.pos: ${this._defaultCameraNode._worldTranslation.float32Array()}`)
}
this._pointOfView = this._defaultCameraNode
}
_setDefaultCameraOrientation(orientation) {
this._defaultCameraRotNode.orientation = orientation
}
_searchCameraNode() {
const nodes = [this.scene._rootNode]
let node = nodes.shift()
while(node){
if(node.camera !== null){
return node
}
nodes.push(...node._childNodes)
node = nodes.shift()
}
return null
}
/**
* @access private
* @returns {SCNVector3} -
*/
_getCameraPosition() {
if(this._pointOfView === this._defaultCameraNode){
return this._defaultCameraPosNode.position
}else if(this._pointOfView === null){
return new SCNVector3(0, 0, 0)
}
const rot = this._getCameraOrientation()
const rotMat = SCNMatrix4.matrixWithRotation(rot)
const pos = this._pointOfView.presentation._worldTranslation
return pos.add((new SCNVector3(0, 0, -_defaultCameraDistance)).rotate(rotMat))
}
/**
* @access private
* @returns {SCNVector4} -
*/
_getCameraOrientation() {
if(this._pointOfView === this._defaultCameraNode){
return this._defaultCameraRotNode.presentation.orientation
}else if(this._pointOfView === null){
return new SCNVector4(0, 0, 0, 0)
}
return this._pointOfView.presentation._worldOrientation
}
/**
* @access private
* @returns {number} -
*/
_getCameraDistance() {
if(this._pointOfView === this._defaultCameraNode){
return this._defaultCameraNode.presentation.position.z
}
return _defaultCameraDistance
}
/**
* @access private
* @returns {boolean} - true if the number of lights is changed.
*/
_numLightsChanged() {
let changed = false
const types = [...Object.values(SCNLight.LightType), 'directionalShadow']
for(const type of types){
const num = this._lightNodes[type].length
if(num !== this._numLights[type]){
changed = true
this._numLights[type] = num
}
}
return changed
}
/**
* @access private
* @param {SCNNode} node -
* @param {SCNVector3} rayPoint -
* @param {SCNVector3} rayVec -
* @returns {SCNHitTestResult[]} -
*/
_nodeHitTestByCPU(node, rayPoint, rayVec) {
const result = []
const geometry = node.presentation.geometry
const geometryCount = geometry.geometryElements.length
if(geometryCount === 0){
// nothing to draw...
return result
}
const invRay = rayVec.mul(-1)
//console.log(`rayPoint: ${rayPoint.float32Array()}`)
//console.log(`rayVec: ${rayVec.float32Array()}`)
//if(node.morpher !== null){
// this._updateVAO(node)
//}
// TODO: test the bounding box/sphere first for performance
const source = geometry.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.vertex)[0]
const sourceLen = source.vectorCount
const sourceData = []
const modelTransform = node.presentation._worldTransform
const skinningJoints = []
if(node.presentation.skinner){
const skinner = node.presentation.skinner
const numBones = skinner._bones.length
for(let i=0; i<numBones; i++){
const bone = skinner._bones[i]
const mat = skinner._boneInverseBindTransforms[i].mult(bone._presentation._worldTransform)
skinningJoints.push(mat)
}
for(let i=0; i<sourceLen; i++){
const weights = skinner._boneWeights._vectorAt(i)
const indices = skinner._boneIndices._vectorAt(i)
const mat = new SCNMatrix4()
for(let j=0; j<skinner.numSkinningJoints; j++){
mat.add(skinningJoints[indices[j]].mul(weights[j]))
}
sourceData.push(source._scnVectorAt(i).transform(mat))
}
}else{
for(let i=0; i<sourceLen; i++){
sourceData.push(source._scnVectorAt(i).transform(modelTransform))
}
}
for(let i=0; i<geometryCount; i++){
//console.log(`geometry element ${i}`)
const element = geometry.geometryElements[i]
switch(element.primitiveType){
case SCNGeometryPrimitiveType.line:
console.warn('hitTest for line is not implemented')
continue
case SCNGeometryPrimitiveType.point:
console.warn('hitTest for point is not implemented')
continue
}
const elementData = element._glData
const len = element.primitiveCount
//console.log(`primitiveCount: ${len}`)
// TODO: check cull settings
for(let pi=0; pi<len; pi++){
const indices = element._indexAt(pi)
const v0 = sourceData[indices[0]]
const v1 = sourceData[indices[1]]
const v2 = sourceData[indices[2]]
const e1 = v1.sub(v0)
const e2 = v2.sub(v0)
let denom = this._det(e1, e2, invRay)
if(denom <= 0){
continue
}
denom = 1.0 / denom
const d = rayPoint.sub(v0)
const u = this._det(d, e2, invRay) * denom
if(u < 0 || u > 1){
continue
}
const v = this._det(e1, d, invRay) * denom
if(v < 0 || v > 1){
continue
}
const t = this._det(e1, e2, d) * denom
if(t < 0){
continue
}
// Hit!
//console.log(`Hit! ${i}: ${pi}`)
const hitPoint = rayPoint.add(rayVec.mul(t))
const invModel = modelTransform.invert()
const res = new SCNHitTestResult()
res._node = node
res._geometryIndex = i
res._faceIndex = pi
res._worldCoordinates = hitPoint
res._localCoordinates = hitPoint.transform(invModel)
const nom = e1.cross(e2)
res._worldNormal = nom.normalize()
res._localNormal = nom.transform(invModel)
res._modelTransform = modelTransform
res._boneNode = null // it should be array... what should I put here?
result.push(res)
}
}
return result
}
/**
* @access private
* @type {SCNProgram}
*/
get _defaultParticleProgram() {
if(this.__defaultParticleProgram !== null){
return this.__defaultParticleProgram
}
const gl = this.context
if(this.__defaultParticleProgram === null){
this.__defaultParticleProgram = new SCNProgram()
}
const p = this.__defaultParticleProgram
const glProgram = p._getGLProgramForContext(gl)
const vsText = _SCNDefaultParticleVertexShader
const fsText = _SCNDefaultParticleFragmentShader
// initialize vertex shader
const vertexShader = gl.createShader(gl.VERTEX_SHADER)
gl.shaderSource(vertexShader, vsText)
gl.compileShader(vertexShader)
if(!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)){
const info = gl.getShaderInfoLog(vertexShader)
throw new Error(`particle vertex shader compile error: ${info}`)
}
// initialize fragment shader
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
gl.shaderSource(fragmentShader, fsText)
gl.compileShader(fragmentShader)
if(!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)){
const info = gl.getShaderInfoLog(fragmentShader)
throw new Error(`particle fragment shader compile error: ${info}`)
}
gl.attachShader(glProgram, vertexShader)
gl.attachShader(glProgram, fragmentShader)
// link program object
gl.linkProgram(glProgram)
if(!gl.getProgramParameter(glProgram, gl.LINK_STATUS)){
const info = gl.getProgramInfoLog(glProgram)
throw new Error(`program link error: ${info}`)
}
//gl.useProgram(glProgram)
this._useProgram(p)
//gl.clearColor(1, 1, 1, 1)
//gl.clearDepth(1.0)
//gl.clearStencil(0)
gl.enable(gl.DEPTH_TEST)
gl.depthFunc(gl.LEQUAL)
gl.enable(gl.BLEND)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.enable(gl.CULL_FACE)
gl.cullFace(gl.BACK)
// set default textures to prevent warnings
this._setDummyParticleTextureAsDefault()
return this.__defaultParticleProgram
}
/**
* @access private
* @type {SCNProgram}
*/
get _defaultShadowProgram() {
if(this.__defaultShadowProgram !== null){
return this.__defaultShadowProgram
}
const gl = this.context
if(this.__defaultShadowProgram === null){
this.__defaultShadowProgram = new SCNProgram()
}
const p = this.__defaultShadowProgram
const glProgram = p._getGLProgramForContext(gl)
const vsText = _SCNDefaultShadowVertexShader
const fsText = _SCNDefaultShadowFragmentShader
// initialize vertex shader
const vertexShader = gl.createShader(gl.VERTEX_SHADER)
gl.shaderSource(vertexShader, vsText)
gl.compileShader(vertexShader)
if(!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)){
const info = gl.getShaderInfoLog(vertexShader)
throw new Error(`particle vertex shader compile error: ${info}`)
}
// initialize fragment shader
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
gl.shaderSource(fragmentShader, fsText)
gl.compileShader(fragmentShader)
if(!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)){
const info = gl.getShaderInfoLog(fragmentShader)
throw new Error(`particle fragment shader compile error: ${info}`)
}
gl.attachShader(glProgram, vertexShader)
gl.attachShader(glProgram, fragmentShader)
// link program object
gl.linkProgram(glProgram)
if(!gl.getProgramParameter(glProgram, gl.LINK_STATUS)){
const info = gl.getProgramInfoLog(glProgram)
throw new Error(`program link error: ${info}`)
}
//gl.useProgram(glProgram)
this._useProgram(p)
//gl.clearColor(1, 1, 1, 1)
//gl.clearDepth(1.0)
//gl.clearStencil(0)
gl.enable(gl.DEPTH_TEST)
gl.depthFunc(gl.LEQUAL)
//gl.enable(gl.BLEND)
gl.disable(gl.BLEND)
//gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.enable(gl.CULL_FACE)
gl.cullFace(gl.BACK)
return this.__defaultShadowProgram
}
_setDummyParticleTextureAsDefault() {
const gl = this.context
const p = this._defaultParticleProgram
const glProgram = p._getGLProgramForContext(gl)
const texNames = [
gl.TEXTURE0
//gl.TEXTURE1
]
const texSymbols = [
'particleTexture'
//'colorTexture'
]
for(let i=0; i<texNames.length; i++){
const texName = texNames[i]
const symbol = texSymbols[i]
gl.uniform1i(gl.getUniformLocation(glProgram, symbol), i)
gl.activeTexture(texName)
gl.bindTexture(gl.TEXTURE_2D, this.__dummyTexture)
}
}
/**
* @access private
* @type {SCNProgram}
*/
get _defaultHitTestProgram() {
if(this.__defaultHitTestProgram !== null){
return this.__defaultHitTestProgram
}
const gl = this.context
if(this.__defaultHitTestProgram === null){
this.__defaultHitTestProgram = new SCNProgram()
}
const p = this.__defaultHitTestProgram
const glProgram = p._getGLProgramForContext(gl)
const vsText = _SCNDefaultHitTestVertexShader
const fsText = _SCNDefaultHitTestFragmentShader
// initialize vertex shader
const vertexShader = gl.createShader(gl.VERTEX_SHADER)
gl.shaderSource(vertexShader, vsText)
gl.compileShader(vertexShader)
if(!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)){
const info = gl.getShaderInfoLog(vertexShader)
throw new Error(`hitTest vertex shader compile error: ${info}`)
}
// initialize fragment shader
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
gl.shaderSource(fragmentShader, fsText)
gl.compileShader(fragmentShader)
if(!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)){
const info = gl.getShaderInfoLog(fragmentShader)
throw new Error(`hitTest fragment shader compile error: ${info}`)
}
gl.attachShader(glProgram, vertexShader)
gl.attachShader(glProgram, fragmentShader)
// link program object
gl.linkProgram(glProgram)
if(!gl.getProgramParameter(glProgram, gl.LINK_STATUS)){
const info = gl.getProgramInfoLog(glProgram)
throw new Error(`program link error: ${info}`)
}
//gl.useProgram(glProgram)
this._useProgram(p)
//gl.clearColor(1, 1, 1, 1)
//gl.clearDepth(1.0)
//gl.clearStencil(0)
gl.enable(gl.DEPTH_TEST)
gl.depthFunc(gl.LEQUAL)
gl.enable(gl.BLEND)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.enable(gl.CULL_FACE)
gl.cullFace(gl.BACK)
//this._setDummyHitTestTextureAsDefault()
return this.__defaultHitTestProgram
}
// for debug
_showShadowMapOfLight(lightNode) {
const gl = this.context
const p = lightNode.presentation
const light = p.light
if(!this.__debugShadowMapSprite){
const node = new SKSpriteNode()
node.size = new CGSize(100, 100)
node.anchorPoint = new CGPoint(0.0, 0.0)
const texture = new SKTexture()
texture._glTexture = light._shadowDepthTexture
texture._image = {
naturalWidth: 100,
naturalHeight: 100
}
node._texture = texture
node.__presentation = node.copy()
node.__presentation._isPresentationInstance = true
node.position = new CGPoint(100, 100)
node._updateWorldTransform()
this.__debugShadowMapSprite = node
}
gl.clearDepth(-1)
gl.clearStencil(0)
gl.depthMask(true)
gl.enable(gl.DEPTH_TEST)
gl.disable(gl.CULL_FACE)
gl.depthFunc(gl.GEQUAL)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.clear(gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
this._renderSKNode(this.__debugShadowMapSprite)
}
_setViewPort(width = null, height = null) {
let w = width
let h = height
if(w === null || h === null){
w = this._viewRect.size.width
h = this._viewRect.size.height
}
this.context.viewport(0, 0, w, h)
}
/**
* calculate a determinant of 3x3 matrix from 3 vectors.
* @access private
* @param {SCNVector3} v1 -
* @param {SCNVector3} v2 -
* @param {SCNVector3} v3 -
* @returns {number} -
*/
_det(v1, v2, v3) {
return (
v1.x * v2.y * v3.z
+ v1.y * v2.z * v3.x
+ v1.z * v2.x * v3.y
- v1.x * v2.z * v3.y
- v1.y * v2.x * v3.z
- v1.z * v2.y * v3.x
)
}
}