Source: components/ShadowLightAdapterComponent.js

import { MathUtils } from '../math/MathUtils';
import { LightAdapterComponent } from './LightAdapterComponent';
import { Frustum, CameraFrustum, Box3, Plane, BoundingSphere } from '@uino/base-thing';

const __ = {
	private: Symbol('private'),
}

const _mat4_1 = MathUtils.createMat4();
const _mat4_2 = MathUtils.createMat4();
const _mat4_3 = MathUtils.createMat4();
const _mat4_4 = MathUtils.createMat4();

const _vec3_1 = MathUtils.createVec3();

const _cameraFrustum_1 = new CameraFrustum();
const _frustum_1 = new Frustum();

const _box3_1 = new Box3();
const _box3_2 = new Box3();
const _boundingSphere_1 = new BoundingSphere();

const _plane_1 = new Plane();

/**
 * @class ShadowLightAdapterComponent
 * The shadow light helper component.
 * @memberof THING
 * @extends THING.LightAdapterComponent
 * @public
 */
class ShadowLightAdapterComponent extends LightAdapterComponent {

	/**
	 * Auto update light position, target and shadow range by binding object's bounding.
	 */
	constructor() {
		super();

		this[__.private] = {};
		const _private = this[__.private];

		_private.lightSphere = null;
		_private.lightSphereRadius = null;
		_private.lightSphereShadowRadius = null;
		_private.bindingObject = null;
		_private.bindingObjectBoundingBox = null;
		_private.bindingCamera = null;
		_private.boundingBox = null;

		_private.distance = 500;

		_private.targetUp = { up: [0, 1, 0] };

		_private.horzAngle = 0;
		_private.vertAngle = 0;

		_private.farFactor = 2;

		_private.autoUpdateLightSphere = true;
		_private.needRefresh = false;

		_private.refreshElapsedTime = 0;
		_private.refreshInterval = 30 * 1000; // Refresh every 30 seconds
	}

	// #region Private

	_refreshLightSphereByBoundingBox() {
		const _private = this[__.private];

		if (!_private.bindingObject && !_private.boundingBox && !_private.bindingCamera) {
			return false;
		}

		if ((_private.bindingObject || _private.boundingBox) && (!_private.bindingCamera || _private.distance <= 0)) {
			this._getSphereByBox3();
			return true;
		}

		return false;
	}

	_refreshObjectBoundingBox() {
		const _private = this[__.private];

		if (!_private.bindingObject) {
			return false;
		}

		_private.bindingObjectBoundingBox = _private.bindingObject.bounding.boundingBox;

		return true;
	}

	_refreshLightSphereByCamera() {
		const _private = this[__.private];

		if (!_private.bindingObject && !_private.boundingBox && !_private.bindingCamera) {
			return false;
		}

		if (!_private.bindingObject && !_private.boundingBox && _private.bindingCamera) {
			this._getSphereByCamera();
			return true;
		}
		else if ((_private.bindingObject || _private.boundingBox) && _private.bindingCamera) {
			this._getSphereByBox3AndCamera();
			return true;
		}

		return false;
	}

	_refreshShadowRange() {
		const light = this.object;

		// Update shadow range
		if (light.castShadow) {
			const _private = this[__.private];

			const lightSphere = _private.lightSphere;
			if (!lightSphere) {
				return;
			}

			const lightRadius = this.onGetShadowRadius() || lightSphere.radius || 1;

			const shadowRange = light.shadowRange;
			shadowRange.width = lightRadius * 2;
			shadowRange.height = lightRadius * 2;
			shadowRange.far = lightRadius * _private.farFactor;
		}
	}

	_refresh() {
		const _private = this[__.private];

		const lightSphere = _private.lightSphere;
		if (!lightSphere) {
			return;
		}

		const light = this.object;
		const lightRadius = lightSphere.radius || 1;

		// Update light position
		const targetPosition = lightSphere.center;
		const position = MathUtils.getOffsetFromAngles(_private.horzAngle, _private.vertAngle, lightRadius);
		MathUtils.vec3.add(position, targetPosition, position);
		light.position = position;

		// Update look at position
		light.lookAt(targetPosition, _private.targetUp);

		this._refreshShadowRange();
	}

	_getSphereByBox3() {
		const _private = this[__.private];
		if (_private.boundingBox) {
			const box = _private.boundingBox;
			_box3_2.set(box.center, box.halfSize);
			_private.lightSphere = _box3_2.getBoundingSphere(_boundingSphere_1);
		}
		else if (_private.bindingObject) {
			_private.lightSphere = _private.bindingObject.bounding.getLightSphere(false);
		}
	}

	_getSphereByCamera() {
		const _private = this[__.private];
		const cameraNode = _private.bindingCamera.node;

		cameraNode.getMatrixWorld(_mat4_1);
		cameraNode.getProjectionMatrix(_mat4_2);

		_cameraFrustum_1.setFromProjectionMatrix(_mat4_2, _private.distance);
		_private.lightSphere = _cameraFrustum_1.getBoundingSphere(_mat4_1, _boundingSphere_1);
	}

	_getSphereByBox3AndCamera() {
		const _private = this[__.private];

		const cameraNode = _private.bindingCamera.node;

		let box3;
		if (_private.boundingBox) {
			const box = _private.boundingBox;
			_box3_2.set(box.center, box.halfSize);
			box3 = _box3_2;
		}
		else if (_private.bindingObject) {
			if (!_private.bindingObjectBoundingBox) {
				this._refreshObjectBoundingBox();
			}

			box3 = _private.bindingObjectBoundingBox;
		}

		cameraNode.getMatrixWorld(_mat4_1);
		cameraNode.getProjectionMatrix(_mat4_2);
		MathUtils.mat4.invert(_mat4_3, _mat4_1)
		MathUtils.mat4.multiply(_mat4_4, _mat4_2, _mat4_3);

		_frustum_1.setFromProjectionMatrix(_mat4_4);

		_cameraFrustum_1.setFromProjectionMatrix(_mat4_2, _private.distance);
		_frustum_1.setFrustumVertical(_cameraFrustum_1._vertices);

		if (!_frustum_1.intersectsBox(box3)) {
			return;
		}

		getBox3Polygons(box3);

		let curPolygons = boxPolygons;
		const frustumPlanes = _frustum_1.planes;
		for (let i = 0, l = frustumPlanes.length - 2; i < l; i++) {
			const plane = frustumPlanes[i];
			curPolygons = clipPolygons(plane, curPolygons);
		}

		let minZ = -Infinity;

		curPolygons.forEach(polygon => {
			for (let i = 0, l = polygon.verticesIndex; i < l; i++) {
				MathUtils.vec3.transformMat4(_vec3_1, polygon.vertices[i], _mat4_3);
				minZ = Math.max(minZ, _vec3_1[2]);
			}
		});

		_plane_1.constant = Math.max(Math.abs(minZ), 1) + _private.distance;
		_plane_1.normal = [0, 0, 1];
		_plane_1.applyMatrix4(_mat4_1);

		curPolygons = clipPolygons(_plane_1, curPolygons);

		_box3_1.makeEmpty();

		curPolygons.forEach(polygon => {
			for (let i = 0, l = polygon.verticesIndex; i < l; i++) {
				_box3_1.expandByPoint(polygon.vertices[i]);
			}
		})

		let maxRadiusSq = 0;
		curPolygons.forEach(polygon => {
			for (let i = 0, l = polygon.verticesIndex; i < l; i++) {
				maxRadiusSq = Math.max(maxRadiusSq, MathUtils.getDistanceToSquared(_box3_1.center, polygon.vertices[i]));
			}
		})
		const radius = Math.sqrt(maxRadiusSq);
		_private.lightSphere = _private.lightSphere || {
			center: [0, 0, 0],
			radius: 0,
			shadowRadius: 0
		};
		_private.lightSphere.center = _box3_1.center;
		_private.lightSphere.radius = radius;

		polygonIndex = 0;
	}

	// #endregion

	// #region Overrides

	onGetRadius() {
		const _private = this[__.private];

		return _private.lightSphereRadius;
	}

	onGetShadowRadius() {
		const _private = this[__.private];

		return _private.lightSphereShadowRadius;
	}

	onUpdateLightSphere(deltaTime) {
		const _private = this[__.private];

		if (_private.lightSphere) {
			if (_private.autoUpdateLightSphere) {
				_private.refreshElapsedTime += deltaTime * 1000;
				if (_private.needRefresh || _private.refreshElapsedTime >= _private.refreshInterval) {
					_private.refreshElapsedTime = 0;

					this._refreshObjectBoundingBox();
					this._refreshLightSphereByBoundingBox();
				}
			}
		}

		this._refreshLightSphereByCamera();

		var radius = this.onGetRadius();
		if (radius) {
			_private.lightSphere.radius = radius;
		}
	}

	onSelfUpdate(deltaTime) {
		const _private = this[__.private];

		// First we must bind object or boundingBox or camera.
		if (!_private.bindingObject && !_private.bindBoundingBox && !_private.bindingCamera) {
			return false;
		}

		// Upate bounding sphere
		this.onUpdateLightSphere(deltaTime);

		// Update light sphere
		const lightSphere = _private.lightSphere;
		const light = this.object;

		// Get the target position
		const targetPosition = lightSphere.center;
		const lightRadius = lightSphere.radius;
		if (!_private.needRefresh && MathUtils.exactEqualsVector3(light.target, targetPosition) && (_private.lightSphereRadius === null || MathUtils.equalsNumber(_private.lightSphereRadius, lightRadius))) {
			return false;
		}

		this._refresh();

		_private.needRefresh = false;

		return true;
	}

	onRefresh() {
		this._refreshLightSphereByBoundingBox();
		this._refreshLightSphereByCamera();
		this._refresh();
	}

	// #endregion

	/**
	 * Bind/Unbind object.
	 * @param {THING.BaseObject} object The object.
	 */
	bind(object) {
		const _private = this[__.private];

		_private.boundingBox = null;

		_private.bindingObject = object;

		this.onRefresh();
	}

	/**
	 * Bind/Unbind camera.
	 * @param {THING.Camera} camera The camera.
	 */
	bindCamera(camera) {
		const _private = this[__.private];

		_private.bindingCamera = camera;

		this.onRefresh();
	}

	/**
	 * @typedef {Object} BoxInfo
	 * @property {Array<Number>} center The center of box.
	 * @property {Array<Number>} halfSize The half size of box.
	 */

	/**
	 * Bind/Unbind boundingBox.
	 * @param {BoxInfo} box The box.
	 */
	bindBoundingBox(box) {
		const _private = this[__.private];

		_private.bindingObject = null;

		_private.boundingBox = box;

		this.onRefresh();
	}

	// #region Accessor

	/**
	 * Get the binding camera.
	 * @type {THING.Camera}
	 * @private
	 */
	get bindingCamera() {
		const _private = this[__.private];

		return _private.bindingCamera;
	}

	/**
	 * Get the binding object.
	 * @type {THING.BaseObject}
	 * @private
	 */
	get bindingObject() {
		const _private = this[__.private];

		return _private.bindingObject;
	}

	/**
	 * Check whetherh need to refresh.
	 * @type {Boolean}
	 * @private
	 */
	get needRefresh() {
		const _private = this[__.private];

		return _private.needRefresh;
	}
	set needRefresh(value) {
		const _private = this[__.private];

		_private.needRefresh = value;
	}

	/**
	 * Get/Set the light sphere radius of light position from target, default is null(indicates auto calculate).
	 * @type {Number}
	 */
	get lightSphereRadius() {
		const _private = this[__.private];

		return _private.lightSphereRadius;
	}
	set lightSphereRadius(value) {
		const _private = this[__.private];

		_private.lightSphereRadius = value;

		_private.needRefresh = true;
	}

	/**
	 * Get/Set the light sphere radius of shadow, default is null(indicates auto calculate).
	 * @type {Number}
	 */
	get lightSphereShadowRadius() {
		const _private = this[__.private];

		return _private.lightSphereShadowRadius;
	}
	set lightSphereShadowRadius(value) {
		const _private = this[__.private];

		_private.lightSphereShadowRadius = value;

		_private.needRefresh = true;
	}

	/**
	 * Get/Set the distance from light position to target.
	 * @type {Number}
	 */
	get distance() {
		const _private = this[__.private];

		return _private.distance;
	}
	set distance(value) {
		const _private = this[__.private];

		_private.distance = value;

		_private.needRefresh = true;
	}

	/**
	 * Get/Set the target up.
	 * @type {Array<Number>}
	 */
	get targetUp() {
		const _private = this[__.private];

		return _private.targetUp;
	}
	set targetUp(value) {
		const _private = this[__.private];

		_private.targetUp = value;

		_private.needRefresh = true;
	}

	/**
	 * Get/Set horz angles from object's bounding box center.
	 * @type {Number}
	 * @public
	 */
	get horzAngle() {
		const _private = this[__.private];

		return _private.horzAngle;
	}
	set horzAngle(value) {
		const _private = this[__.private];

		_private.horzAngle = value;

		_private.needRefresh = true;
	}

	/**
	 * Get/Set vert angles from object's bounding box center.
	 * @type {Number}
	 * @public
	 */
	get vertAngle() {
		const _private = this[__.private];

		return _private.vertAngle;
	}
	set vertAngle(value) {
		const _private = this[__.private];

		_private.vertAngle = value;

		_private.needRefresh = true;
	}

	/**
	 * Get/Set the far factor.
	 * @type {Number}
	 * @private
	 */
	get farFactor() {
		const _private = this[__.private];

		return _private.farFactor;
	}
	set farFactor(value) {
		const _private = this[__.private];

		_private.farFactor = value;
	}

	/**
	 * Enable/Disable auto update light sphere.
	 * @type {Boolean}
	 * @private
	 */
	get autoUpdateLightSphere() {
		const _private = this[__.private];

		return _private.autoUpdateLightSphere;
	}
	set autoUpdateLightSphere(value) {
		const _private = this[__.private];

		_private.autoUpdateLightSphere = value;
	}

	/**
	 * Get/Set refresh interval in milliseconds.
	 * @type {Number}
	 * @private
	 */
	get refreshInterval() {
		const _private = this[__.private];

		return _private.refreshInterval;
	}
	set refreshInterval(value) {
		const _private = this[__.private];

		_private.refreshInterval = value;
	}

	// #endregion

}

class Polygon {

	constructor() {
		this.plane = new Plane();
		this.vertices = [];
		this.verticesIndex = 0;
	}

	begin() {
		this.verticesIndex = 0;
		return this;
	}

	pushVertex(vector3) {
		this.vertices[this.verticesIndex] = vector3;

		this.verticesIndex++;

		return this;
	}

	end() {
		const plane = this.plane;
		const vertices = this.vertices;

		plane.normal = MathUtils.normalizeVector(MathUtils.crossVector(MathUtils.subVector(vertices[1], vertices[0]), MathUtils.subVector(vertices[2], vertices[0])));
		plane.constant = MathUtils.dotVector(plane.normal, vertices[0]);

		return this;
	}

}

const polygonPool = [];
let polygonIndex = 0;

function getPolygon() {
	let polygon = polygonPool[polygonIndex];

	if (!polygon) {
		polygon = new Polygon();
		polygonPool[polygonIndex] = polygon;
	}

	polygonIndex++;

	return polygon;
}

const EPSILON = 1e-5;
const COPLANAR = 0;
const FRONT = 1;
const BACK = 2;
const SPANNING = 3;

const types = [];

function splitPolygon(plane, polygon, outPolygons) {
	let polygonType = 0;
	types.length = 0;

	for (let i = 0; i < polygon.verticesIndex; i++) {
		const t = MathUtils.dotVector(plane.normal, polygon.vertices[i]) + plane.constant;
		const type = t < -EPSILON ? BACK : t > EPSILON ? FRONT : COPLANAR;
		polygonType |= type;
		types.push(type);
	}

	switch (polygonType) {
		case COPLANAR:
			if (MathUtils.dotVector(plane.normal, polygon.plane.normal) > 0) {
				outPolygons.push(polygon);
			}
			break;
		case FRONT:
			outPolygons.push(polygon);
			break;
		case SPANNING: {
			const _polygon = getPolygon();
			_polygon.begin();

			for (let i = 0; i < polygon.verticesIndex; i++) {
				const j = (i + 1) % polygon.verticesIndex;

				const ti = types[i],
					tj = types[j];
				const vi = polygon.vertices[i],
					vj = polygon.vertices[j];

				if (ti != BACK) _polygon.pushVertex(vi);
				if ((ti | tj) == SPANNING) {
					const t = -(plane.constant + MathUtils.dotVector(plane.normal, vi)) / MathUtils.dotVector(plane.normal, MathUtils.subVector(vj, vi));
					const v = MathUtils.lerpVector(vi, vj, t);

					_polygon.pushVertex(v);
				}
			}

			if (_polygon.verticesIndex >= 3) {
				_polygon.end();
				outPolygons.push(_polygon);
			}
			else {
				revertPolygon();
			}
			break;
		}
		default:
			break;
	}
	return outPolygons;
}

function revertPolygon() {
	polygonIndex--;
}

function clipPolygons(plane, polygons, outPolygons = []) {
	for (let i = 0, l = polygons.length; i < l; i++) {
		outPolygons = splitPolygon(plane, polygons[i], outPolygons);
	}

	return outPolygons;
}

const boxPoints_0 = MathUtils.createVec3();
const boxPoints_1 = MathUtils.createVec3();
const boxPoints_2 = MathUtils.createVec3();
const boxPoints_3 = MathUtils.createVec3();
const boxPoints_4 = MathUtils.createVec3();
const boxPoints_5 = MathUtils.createVec3();
const boxPoints_6 = MathUtils.createVec3();
const boxPoints_7 = MathUtils.createVec3();

const boxPolygons = [];

function getBox3Polygons(box3) {
	const minX = box3.min[0], minY = box3.min[1], minZ = box3.min[2];
	const maxX = box3.max[0], maxY = box3.max[1], maxZ = box3.max[2];

	boxPoints_0[0] = maxX; boxPoints_0[1] = maxY; boxPoints_0[2] = maxZ;
	boxPoints_1[0] = maxX; boxPoints_1[1] = minY; boxPoints_1[2] = maxZ;
	boxPoints_2[0] = maxX; boxPoints_2[1] = minY; boxPoints_2[2] = minZ;
	boxPoints_3[0] = maxX; boxPoints_3[1] = maxY; boxPoints_3[2] = minZ;
	boxPoints_4[0] = minX; boxPoints_4[1] = maxY; boxPoints_4[2] = maxZ;
	boxPoints_5[0] = minX; boxPoints_5[1] = minY; boxPoints_5[2] = maxZ;
	boxPoints_6[0] = minX; boxPoints_6[1] = minY; boxPoints_6[2] = minZ;
	boxPoints_7[0] = minX; boxPoints_7[1] = maxY; boxPoints_7[2] = minZ;

	boxPolygons[0] = getPolygon().begin().pushVertex(boxPoints_0).pushVertex(boxPoints_1).pushVertex(boxPoints_2).pushVertex(boxPoints_3).end();
	boxPolygons[1] = getPolygon().begin().pushVertex(boxPoints_4).pushVertex(boxPoints_7).pushVertex(boxPoints_6).pushVertex(boxPoints_5).end();
	boxPolygons[2] = getPolygon().begin().pushVertex(boxPoints_0).pushVertex(boxPoints_3).pushVertex(boxPoints_7).pushVertex(boxPoints_4).end();
	boxPolygons[3] = getPolygon().begin().pushVertex(boxPoints_1).pushVertex(boxPoints_5).pushVertex(boxPoints_6).pushVertex(boxPoints_2).end();
	boxPolygons[4] = getPolygon().begin().pushVertex(boxPoints_0).pushVertex(boxPoints_4).pushVertex(boxPoints_5).pushVertex(boxPoints_1).end();
	boxPolygons[5] = getPolygon().begin().pushVertex(boxPoints_3).pushVertex(boxPoints_2).pushVertex(boxPoints_6).pushVertex(boxPoints_7).end();
}

export { ShadowLightAdapterComponent }