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 }