var bind = require('../utils/bind'); var diff = require('../utils').diff; var debug = require('../utils/debug'); var registerComponent = require('../core/component').registerComponent; var THREE = require('../lib/three'); var degToRad = THREE.Math.degToRad; var warn = debug('components:light:warn'); /** * Light component. */ module.exports.Component = registerComponent('light', { schema: { angle: {default: 60, if: {type: ['spot']}}, color: {type: 'color'}, groundColor: {type: 'color', if: {type: ['hemisphere']}}, decay: {default: 1, if: {type: ['point', 'spot']}}, distance: {default: 0.0, min: 0, if: {type: ['point', 'spot']}}, intensity: {default: 1.0, min: 0, if: {type: ['ambient', 'directional', 'hemisphere', 'point', 'spot']}}, penumbra: {default: 0, min: 0, max: 1, if: {type: ['spot']}}, type: { default: 'directional', oneOf: ['ambient', 'directional', 'hemisphere', 'point', 'spot'], schemaChange: true }, target: {type: 'selector', if: {type: ['spot', 'directional']}}, // Shadows. castShadow: {default: false, if: {type: ['point', 'spot', 'directional']}}, shadowBias: {default: 0, if: {castShadow: true}}, shadowCameraFar: {default: 500, if: {castShadow: true}}, shadowCameraFov: {default: 90, if: {castShadow: true}}, shadowCameraNear: {default: 0.5, if: {castShadow: true}}, shadowCameraTop: {default: 5, if: {castShadow: true}}, shadowCameraRight: {default: 5, if: {castShadow: true}}, shadowCameraBottom: {default: -5, if: {castShadow: true}}, shadowCameraLeft: {default: -5, if: {castShadow: true}}, shadowCameraVisible: {default: false, if: {castShadow: true}}, shadowMapHeight: {default: 512, if: {castShadow: true}}, shadowMapWidth: {default: 512, if: {castShadow: true}}, shadowRadius: {default: 1, if: {castShadow: true}} }, /** * Notifies scene a light has been added to remove default lighting. */ init: function () { var el = this.el; this.light = null; this.defaultTarget = null; this.rendererSystem = this.el.sceneEl.systems.renderer; this.system.registerLight(el); }, /** * (Re)create or update light. */ update: function (oldData) { var data = this.data; var diffData = diff(data, oldData); var light = this.light; var rendererSystem = this.rendererSystem; var self = this; // Existing light. if (light && !('type' in diffData)) { var shadowsLoaded = false; // Light type has not changed. Update light. Object.keys(diffData).forEach(function (key) { var value = data[key]; switch (key) { case 'color': { light.color.set(value); rendererSystem.applyColorCorrection(light.color); break; } case 'groundColor': { light.groundColor.set(value); rendererSystem.applyColorCorrection(light.groundColor); break; } case 'angle': { light.angle = degToRad(value); break; } case 'target': { // Reset target if selector is null. if (value === null) { if (data.type === 'spot' || data.type === 'directional') { light.target = self.defaultTarget; } } else { // Target specified, set target to entity's `object3D` when it is loaded. if (value.hasLoaded) { self.onSetTarget(value, light); } else { value.addEventListener('loaded', bind(self.onSetTarget, self, value, light)); } } break; } case 'castShadow': case 'shadowBias': case 'shadowCameraFar': case 'shadowCameraFov': case 'shadowCameraNear': case 'shadowCameraTop': case 'shadowCameraRight': case 'shadowCameraBottom': case 'shadowCameraLeft': case 'shadowCameraVisible': case 'shadowMapHeight': case 'shadowMapWidth': case 'shadowRadius': if (!shadowsLoaded) { self.updateShadow(); shadowsLoaded = true; } break; default: { light[key] = value; } } }); return; } // No light yet or light type has changed. Create and add light. this.setLight(this.data); this.updateShadow(); }, setLight: function (data) { var el = this.el; var newLight = this.getLight(data); if (newLight) { if (this.light) { el.removeObject3D('light'); } this.light = newLight; this.light.el = el; el.setObject3D('light', this.light); // HACK solution for issue #1624 if (data.type === 'spot' || data.type === 'directional' || data.type === 'hemisphere') { el.getObject3D('light').translateY(-1); } // set and position default lighttarget as a child to enable spotlight orientation if (data.type === 'spot') { el.setObject3D('light-target', this.defaultTarget); el.getObject3D('light-target').position.set(0, 0, -1); } } }, /** * Updates shadow-related properties on the current light. */ updateShadow: function () { var el = this.el; var data = this.data; var light = this.light; light.castShadow = data.castShadow; // Shadow camera helper. var cameraHelper = el.getObject3D('cameraHelper'); if (data.shadowCameraVisible && !cameraHelper) { el.setObject3D('cameraHelper', new THREE.CameraHelper(light.shadow.camera)); } else if (!data.shadowCameraVisible && cameraHelper) { el.removeObject3D('cameraHelper'); } if (!data.castShadow) { return light; } // Shadow appearance. light.shadow.bias = data.shadowBias; light.shadow.radius = data.shadowRadius; light.shadow.mapSize.height = data.shadowMapHeight; light.shadow.mapSize.width = data.shadowMapWidth; // Shadow camera. light.shadow.camera.near = data.shadowCameraNear; light.shadow.camera.far = data.shadowCameraFar; if (light.shadow.camera instanceof THREE.OrthographicCamera) { light.shadow.camera.top = data.shadowCameraTop; light.shadow.camera.right = data.shadowCameraRight; light.shadow.camera.bottom = data.shadowCameraBottom; light.shadow.camera.left = data.shadowCameraLeft; } else { light.shadow.camera.fov = data.shadowCameraFov; } light.shadow.camera.updateProjectionMatrix(); if (cameraHelper) { cameraHelper.update(); } }, /** * Creates a new three.js light object given data object defining the light. * * @param {object} data */ getLight: function (data) { var angle = data.angle; var color = new THREE.Color(data.color); this.rendererSystem.applyColorCorrection(color); color = color.getHex(); var decay = data.decay; var distance = data.distance; var groundColor = new THREE.Color(data.groundColor); this.rendererSystem.applyColorCorrection(groundColor); groundColor = groundColor.getHex(); var intensity = data.intensity; var type = data.type; var target = data.target; var light = null; switch (type.toLowerCase()) { case 'ambient': { return new THREE.AmbientLight(color, intensity); } case 'directional': { light = new THREE.DirectionalLight(color, intensity); this.defaultTarget = light.target; if (target) { if (target.hasLoaded) { this.onSetTarget(target, light); } else { target.addEventListener('loaded', bind(this.onSetTarget, this, target, light)); } } return light; } case 'hemisphere': { return new THREE.HemisphereLight(color, groundColor, intensity); } case 'point': { return new THREE.PointLight(color, intensity, distance, decay); } case 'spot': { light = new THREE.SpotLight(color, intensity, distance, degToRad(angle), data.penumbra, decay); this.defaultTarget = light.target; if (target) { if (target.hasLoaded) { this.onSetTarget(target, light); } else { target.addEventListener('loaded', bind(this.onSetTarget, this, target, light)); } } return light; } default: { warn('%s is not a valid light type. ' + 'Choose from ambient, directional, hemisphere, point, spot.', type); } } }, onSetTarget: function (targetEl, light) { light.target = targetEl.object3D; }, /** * Remove light on remove (callback). */ remove: function () { var el = this.el; el.removeObject3D('light'); if (el.getObject3D('cameraHelper')) { el.removeObject3D('cameraHelper'); } } });