import { Frustum, CameraFrustum } from '@uino/base-thing';
import { MathUtils, Utils, BaseTickableObject3D } from "@uino/thing";
import { ViewProbeInnerPlane } from "./ViewProbeInnerPlane";
import { ViewProbeOutline } from "./ViewProbeOutline";
const mat4_1 = MathUtils.createMat4();
const _ray = {
origin: undefined,
direction: undefined,
}
/**
* @class ViewProbe
* @summary 视图探针对象,用于模拟摄像机视锥体并在三维场景中进行视锥体裁剪与遮挡检测。
* 支持自定义视锥体参数(FOV、近远平面、宽高比),可检测对象在视锥体内的可见性状态,
* 以及对象间的相互遮挡关系(遮挡剔除)。同时提供可视化辅助功能,
* 以轮廓线、扫描面和扫描锥体形式呈现视锥体与被检测对象的相交情况。
* 注意:这里视椎的方向是自身z轴反向,因此如果lookAt的话,结果和Projector正好相反
* @memberof THING
* @extends THING.BaseTickableObject3D
* @public
* @example
* // 创建视图探针并检测对象可见性
const probe = new THING.EXTEND.ViewProbe({
fov: 60,
aspect: 16 / 9,
near: 1,
far: 100,
outerColor: [1, 0, 0],
castShadow:false,
innerScanning: true
});
probe.position = [20, 15, 0];
probe.rotateX(-90);
const result = probe.intersectObjects(app.query('.Entity'), true);
console.log(result)
*/
class ViewProbe extends BaseTickableObject3D {
static defaultTagArray = ['Dummy'];
/**
* 创建一个视图探针对象
* @param {object} param 选项
* @param {number} [param.fov] 视锥体的视场角值
* @param {number} [param.aspect] 视锥体的宽高比值
* @param {number} [param.near] 视锥体的近平面值
* @param {number} [param.far] 视锥体的远平面值
* @param {number|string|Array} [param.outerColor] 轮廓线颜色
* @param {boolean} [param.innerScanning] 是否生成扫描平面
* @param {boolean} [param.coneScanning] 是否生成扫描锥体
* @param {Array} [param.scanningNum] 扫描区域的射线数量,扫描区域的数量
* @param {number|string|Array} [param.innerPlaneColor] 扫描面颜色
* @param {number} [param.innerPlaneOpacity] 扫描面透明度
*/
constructor(param = {}) {
super(param);
this._cameraFrustum = new Frustum();
this._cameraFrustum.setProjectionMatrix(30, 16 / 9, 1, 20);
this._helpFrustum = new CameraFrustum();
// outerline mesh
this._outMesh = new ViewProbeOutline(this._cameraFrustum);
this._outMesh.style.color = 0xffffff;
this._outMesh.visible = false;
this._outMesh.castShadow = false;
this.add(this._outMesh, { attachMode: false });
// scanning num = horizontalScanningNum * verticalScanningNum
this._horizontalScanningNum = 15;
this._verticalScanningNum = 20;
this._innerPlanes = [];
this._innerScanning = true;
this._coneScanning = false;
this._innerCounter = 0;
this._scanning = false;
this._scanningObject = null;
this._innerPlaneColor = undefined;
this._innerPlaneOpacity = undefined;
this._cache = {
outer: [],
inner: []
};
this._useCache = false;
this._isIntersect = true;
this.onSetup(param);
}
_updateProjectionMatrixAndFrustum() {
const cameraFrustum = this._cameraFrustum;
const matrix = [];
this.getMatrixWorld(matrix, true);
cameraFrustum.matrixWorld = matrix;
if (cameraFrustum.near === 0) {
cameraFrustum.near += Number.EPSILON;
}
if (cameraFrustum.far === 0 || cameraFrustum.far === cameraFrustum.near) {
cameraFrustum.far = cameraFrustum.near + Number.EPSILON;
}
cameraFrustum.updateProjectionMatrix();
const matrixWorldInverse = MathUtils.createMat4();
MathUtils.mat4.invert(matrixWorldInverse, this.matrixWorld);
MathUtils.mat4.multiply(mat4_1, cameraFrustum.projectionMatrix, matrixWorldInverse);
cameraFrustum.setFromProjectionMatrix(mat4_1);
const vertices = this._helpFrustum.setWorldFromProjectionMatrix(cameraFrustum.projectionMatrix, cameraFrustum.far, this.transform);
cameraFrustum.setFrustumVertical(vertices);
}
_updateInnerPlane(idx, probeObjects) {
let y = 2 * idx / this._verticalScanningNum - 1;
let scanningCoords = [];
for (let i = 0; i <= this._horizontalScanningNum; i++) {
let x = 2 * i / this._horizontalScanningNum - 1;
scanningCoords.push([x, y]);
}
let distArray;
if (this._useCache && this._cache.inner[idx]) {
distArray = this._cache.inner[idx];
}
else {
distArray = scanningCoords.map(coord => {
const ray = this._cameraFrustum.getRayFromCamera(coord);
const intersects = Frustum.intersectObjects(ray, probeObjects);
return intersects[0] ? intersects[0].distance : Infinity;
});
this._cache.inner[idx] = distArray;
}
if (this._innerPlanes[idx] && this._innerPlanes[idx].isViewProbeInnerPlane) {
this._innerPlanes[idx].destroy();
this._innerPlanes[idx] = null;
}
this._innerPlanes[idx] = new ViewProbeInnerPlane({
frustum: this._cameraFrustum,
vertexNum: this._horizontalScanningNum + 2,
scanningCoords,
distArray,
color: this._innerPlaneColor,
opacity: this._innerPlaneOpacity
});
this._innerPlanes[idx].visible = true;
this._innerPlanes[idx].castShadow = this.castShadow;
this.add(this._innerPlanes[idx], { attachMode: false });
this._innerPlanes[idx].waitForComplete().then(() => {
this._innerPlanes[idx].node.setRenderLayer(this.node.getRenderLayer());
});
}
_updateConePlane(idx, probeObjects) {
const isSide = idx % 2 === 0;
let scanningCoords = [];
if (isSide) {
const x = idx - 1;
for (let i = 0; i <= this._verticalScanningNum; i++) {
const y = 2 * i / this._verticalScanningNum - 1;
scanningCoords.push([x, y]);
}
}
else {
const y = idx - 2;
for (let i = 0; i <= this._horizontalScanningNum; i++) {
const x = 2 * i / this._horizontalScanningNum - 1;
scanningCoords.push([x, y]);
}
}
let distArray;
if (this._useCache && this._cache.inner[idx]) {
distArray = this._cache.inner[idx];
}
else {
distArray = scanningCoords.map(coord => {
const ray = this._cameraFrustum.getRayFromCamera(coord);
const intersects = Frustum.intersectObjects(ray, probeObjects);
return intersects[0] ? intersects[0].distance : Infinity;
});
this._cache.inner[idx] = distArray;
}
if (this._innerPlanes[idx] && this._innerPlanes[idx].isViewProbeInnerPlane) {
this._innerPlanes[idx].destroy();
this._innerPlanes[idx] = null;
}
this._innerPlanes[idx] = new ViewProbeInnerPlane({
frustum: this._cameraFrustum,
vertexNum: isSide ? this._verticalScanningNum + 2 : this._horizontalScanningNum + 2,
scanningCoords,
distArray,
color: this._innerPlaneColor,
opacity: this._innerPlaneOpacity
});
this._innerPlanes[idx].visible = true;
this.add(this._innerPlanes[idx], { attachMode: false });
this._innerPlanes[idx].waitForComplete().then(() => {
this._innerPlanes[idx].node.setRenderLayer(this.node.getRenderLayer());
});
}
onSetup(param) {
if (param.fov !== undefined) {
this._cameraFrustum.fov = param.fov;
}
if (param.aspect !== undefined) {
this._cameraFrustum.aspect = param.aspect;
}
if (param.near !== undefined) {
this._cameraFrustum.near = param.near;
}
if (param.far !== undefined) {
this._cameraFrustum.far = param.far;
}
if (param.outerColor !== undefined) {
this._outMesh.style.color = Utils.parseColor(param.outerColor);
}
if (param.innerScanning !== undefined) {
this._innerScanning = !!param.innerScanning;
}
if (param.coneScanning !== undefined) {
this._coneScanning = !!param.coneScanning;
}
if (param.scanningNum !== undefined) {
this._horizontalScanningNum = param.scanningNum[0];
this._verticalScanningNum = param.scanningNum[1];
}
this._innerPlaneColor = Utils.parseColor(param.innerPlaneColor);
this._innerPlaneOpacity = param.innerPlaneOpacity;
this._updateProjectionMatrixAndFrustum();
}
onUpdate(deltaTime) {
super.onUpdate(deltaTime);
if (!this.visible || !this._isIntersect) return true;
this._updateProjectionMatrixAndFrustum();
if (this._scanning) {
if (this._innerCounter > this._verticalScanningNum) {
this._scanning = false;
this._innerCounter = 0;
this._scanningObject = null;
if (this._saveCache) {
localStorage.setItem(this.name + '_scanning_data', JSON.stringify(this._cache));
}
}
else {
this._updateInnerPlane(this._innerCounter, this._scanningObject);
this._innerCounter++;
}
}
else if (this._scanningConePlane) {
if (this._innerCounter > 3) {
this._scanningConePlane = false;
this._innerCounter = 0;
this._scanningObject = null;
if (this._saveCache) {
localStorage.setItem(this.name + '_scanning_data', JSON.stringify(this._cache));
}
}
else {
this._updateConePlane(this._innerCounter, this._scanningObject);
this._innerCounter++;
}
}
return true;
}
/**
* 开始生成扫描区域,每帧扫描一条水平线
* @param {Array<Object3D>} scanningObject 要扫描的对象
* @param {boolean} [update=true] 场景矩阵是否需要更新
* @param {boolean} [useCache=false] 是否尝试使用存储在浏览器缓存(localStorage)中的扫描值,可以提高性能
* @param {boolean} [saveCache=false] 扫描结束后是否自动将扫描结果存储在浏览器缓存(localStorage)中
* 浏览器缓存的键为 name + '_scanning_data',所以如果场景中有多个需要缓存的视锥体扫描结果,
* 注意设置不同的名称
* @public
*/
start(scanningObject = [], update = true, useCache = false, saveCache = false) {
if (!this.visible) {
// Utils.warn('ViewProbe: .start() must accept an array of Object3D parameter!');
return;
}
this._isIntersect = true;
if (update) {
this._updateProjectionMatrixAndFrustum();
}
this._useCache = useCache;
this._saveCache = saveCache;
if (useCache) {
let cache = localStorage.getItem(this.name + '_scanning_data');
if (cache) {
this._cache = JSON.parse(cache, function (key, value) {
return value === null ? Infinity : value;
});
}
}
const scanningCoords = [
[-1, 1],
[-1, -1],
[1, -1],
[1, 1]
];
let distArray;
if (this._useCache && this._cache.outer.length > 0) {
distArray = this._cache.outer;
}
else {
distArray = scanningCoords.map(coord => {
const ray = this._cameraFrustum.getRayFromCamera(coord);
const intersects = Frustum.intersectObjects(ray, scanningObject);
return intersects[0] ? intersects[0].distance : Infinity;
});
this._cache.outer = distArray;
if (this._saveCache) {
localStorage.setItem(this.name + '_scanning_data', JSON.stringify(this._cache));
}
}
this._outMesh.update(scanningCoords, distArray);
this._outMesh.visible = true;
if (this._innerScanning) {
this._innerCounter = 0;
this._scanning = true;
this._scanningObject = scanningObject || this.app.query('.Ground');
}
else if (this._coneScanning) {
this._innerCounter = 0;
this._scanningConePlane = true;
this._scanningObject = scanningObject || this.app.query('.Ground');
}
return distArray;
}
/**
* 暂停任务
* @public
*/
pause() {
if (this._scanning) {
this._scanning = false;
this._innerCounter = 0;
this._scanningObject = null;
}
this._isIntersect = false;
}
/**
* 停止计时
* @public
*/
stop() {
this._outMesh.visible = false;
for (let i = 0; i <= this._verticalScanningNum; i++) {
if (this._innerPlanes[i]) {
this._innerPlanes[i].visible = false;
}
}
this._scanning = false;
this._scanningConePlane = false;
this._innerCounter = 0;
this._scanningObject = null;
this._isIntersect = false;
}
/**
* 检测一组对象与视锥体的关系,
* 返回包围盒在视锥体内的对象。
* 如果 occlusionCulling = true,则返回视锥体内未被遮挡的对象。
* (注意:设置 occlusionCulling = true 后,
* 将使用对象的几何体而不是包围盒进行精确检测。)
* @param {Array<Object3D>} testObjects - 检测对象范围
* @param {boolean} [occlusionCulling=false] - 遮挡剔除
* @return {Array<Object3D>}
* @public
*/
intersectObjects(testObjects, occlusionCulling = false) {
if (!this.visible || !this._isIntersect || !testObjects) {
return [];
}
const that = this;
let result = testObjects.filter(object => this._cameraFrustum.intersectsBox(object.boundingBox));
if (occlusionCulling) {
const objects = result;
result = objects.filter(object => {
const cameraPosition = MathUtils.getVec3FromMatrixColumn(this._cameraFrustum.matrixWorld, 3);
const objectCenter = object.boundingBox.center;
_ray.origin = cameraPosition;
_ray.direction = MathUtils.normalizeVector(MathUtils.subVector(objectCenter, cameraPosition));
const intersects = Frustum.intersectObjects(_ray, objects);
if (intersects.length > 0) {
if (that.app.objectManager.getBaseObjectFromNode(intersects[0].node) === object) {
return true;
}
}
return false;
});
}
return result;
}
/**
* 检测单个对象与视锥体的关系,
* 返回当前对象包围盒与视锥体的空间状态(OUT | VISIBLE | OBSCURED)。
* 如果设置 occlusionCulling = true,并设置了 testObjects,
* 则会考虑对象与视锥体内其他对象(testObjects)的遮挡关系。
* (注意:设置 occlusionCulling = true 后,
* 将使用对象的几何体而不是包围盒进行精确检测。)
* @param {Object3D} object - 要检测的对象
* @param {Array<Object3D>} [testObjects] - 检测对象范围
* @param {boolean} [occlusionCulling=false] - 遮挡剔除
* @return {number} - 0 表示 OUT,1 表示 VISIBLE,2 表示 OBSCURED,-1 表示检查错误
* @public
*/
intersectObject(object, testObjects, occlusionCulling = false) {
if (!this.visible || !this._isIntersect || !Utils.isObject(object) || (occlusionCulling && !testObjects)) {
return -1;
}
const that = this;
const intersectState = this._cameraFrustum.intersectsBox(object.boundingBox);
if (intersectState && occlusionCulling) {
const objects = testObjects.filter(object => that._cameraFrustum.intersectsBox(object.boundingBox));
const cameraPosition = MathUtils.getVec3FromMatrixColumn(that._cameraFrustum.matrixWorld, 3);
const objectCenter = object.boundingBox.center;
_ray.origin = cameraPosition;
_ray.direction = MathUtils.normalizeVector(MathUtils.subVector(objectCenter, cameraPosition));
const intersects = Frustum.intersectObjects(_ray, objects);
if (intersects.length > 0) {
if (that.app.objectManager.getBaseObjectFromNode(intersects[0].node) === object) {
return ViewProbe.VISIBLE;
}
else {
return ViewProbe.OBSCURED;
}
}
}
else if (intersectState) {
return ViewProbe.VISIBLE;
}
return ViewProbe.OUT;
}
/**
* 获取/设置视场角
* @type {number}
* @public
*/
get fov() {
return this._cameraFrustum.fov;
}
set fov(val) {
this._cameraFrustum.fov = val;
this._updateProjectionMatrixAndFrustum();
}
/**
* 获取/设置宽高比
* @type {number}
* @public
*/
set aspect(val) {
this._cameraFrustum.aspect = val;
this._updateProjectionMatrixAndFrustum();
}
get aspect() {
return this._cameraFrustum.aspect;
}
/**
* 获取/设置近平面距离
* @type {number}
* @public
*/
get near() {
return this._cameraFrustum.near;
}
set near(val) {
this._cameraFrustum.near = val;
this._updateProjectionMatrixAndFrustum();
}
/**
* 获取/设置远平面距离
* @type {number}
* @public
*/
get far() {
return this._cameraFrustum.far;
}
set far(val) {
this._cameraFrustum.far = val;
this._updateProjectionMatrixAndFrustum();
}
get isViewProbe() {
return true;
}
}
ViewProbe.OUT = 0; // outside the frustum
ViewProbe.VISIBLE = 1; // Inside the frustum, visible
ViewProbe.OBSCURED = 2; // Inside the frustum, invisible
export {
ViewProbe
}