import { Utils, Object3D, Thing, ImageTexture, VideoTexture } from '@uino/thing';
import { FisheyeType, FisheyeType_Reverse } from '../../const';
/**
* @class Projector
* @summary 视频投影对象,用于将图像或视频纹理投射到三维场景中的指定物体表面。
* 支持透视投影和正交投影两种模式,并可配置鱼眼效果、桶形畸变校正、遮挡处理等高级功能。
* 投射纹理支持动态视频或静态图片,适用于监控画面投射、投影映射、AR 辅助透视等场景。
* @memberof THING
* @extends THING.Object3D
* @public
* @example
* // 创建视频投影,将监控画面投射到指定物体
const projector = new THING.Projector({
debug:true,
image: {
type: 'VideoTexture',
url: 'https://uinnova-pano.oss-cn-beijing.aliyuncs.com/projector/resource/50.mp4'
},
receiveObjects: [obj],
fov: 60,
near: 0.5,
far: 50,
color: [1, 1, 1],
opacity: 0.5
});
projector.position = [0, 10, 0];
projector.lookAt([0, 0, 0]);
*/
class Projector extends Object3D {
static defaultTagArray = ['Dummy'];
/**
* 构造函数,用于初始化 Projector 对象。
* @param {object} param - 初始化参数对象,包含 Projector 的配置选项。
* @param {object} param.camera - 相机对象,用于渲染。
* @param {Array} param.receiveObjects - 接收物体的数组。数组内元素支持uuid和Object对象。
* @param {string} param.color - Projector 的颜色。
* @param {number} param.opacity - Projector 的不透明度。
* @param {boolean} param.debug - 是否启用调试模式。
* @param {number} param.fov - 视场角。
* @param {number} param.far - 远裁剪面。
* @param {number} param.near - 近裁剪面。
* @param {number} param.aspect - 纵横比。
* @param {number} param.orthoSize - 正交投影大小。
* @param {boolean} param.occlusion - 是否启用遮挡效果。
* @param {number} param.occlusionBias - 遮挡偏差。
* @param {number} param.occlusionQuality - 遮挡质量。
* @param {string} param.fisheye - 鱼眼效果类型。
* @param {boolean} param.barrelCorrection - 是否启用桶形畸变校正。
* @param {number} param.barrelFx - 桶形畸变的 X 方向参数。
* @param {number} param.barrelFy - 桶形畸变的 Y 方向参数。
* @param {number} param.barrelS - 桶形畸变的缩放参数。
* @param {string} param.projectionType - 投影类型。
* @param {object} param.image - 图像参数,支持THING.ImageTexture和THING.VideoTexture两种THINGJS对象;同时支持结构为{type,url}的数据对象,数据对象参数如下。
* @param {string} param.image.type - 图像类型<string>,支持'ImageTexture' 和 'VideoTexture'。
* @param {string} param.image.url - 图像URL<string>。
*/
constructor(param = {}) {
super(param);
const camera = this.app.camera;
param.camera = camera;
const node = Utils.createObject('DecalNode', { external: { uScene: this.node._uScene, param: param }});
this._node = node;
this.body.setNode(node);
this._receiveObjects = [];
this._instanceState = [];
this._debug = false;
this._map = null;
if ('color' in param) {
this.color = param['color'];
}
if ('opacity' in param) {
this.opacity = param['opacity'];
}
if ('debug' in param) {
this.debug = param['debug'];
}
if ('fov' in param) {
this.fov = param['fov'];
}
if ('far' in param) {
this.far = param['far'];
}
if ('near' in param) {
this.near = param['near'];
}
if ('aspect' in param) {
this.aspect = param['aspect'];
}
if ('orthoSize' in param) {
this.orthoSize = param['orthoSize'];
}
if ('occlusion' in param) {
this.occlusion = param['occlusion'];
}
if ('occlusionBias' in param) {
this.occlusionBias = param['occlusionBias'];
}
if ('occlusionQuality' in param) {
this.occlusionQuality = param['occlusionQuality'];
}
if ('fisheye' in param) {
this.fisheye = param['fisheye'];
}
if ('barrelCorrection' in param) {
this.barrelCorrection = param['barrelCorrection'];
}
if ('barrelFx' in param) {
this.barrelFx = param['barrelFx'];
}
if ('barrelFy' in param) {
this.barrelFy = param['barrelFy'];
}
if ('barrelS' in param) {
this.barrelS = param['barrelS'];
}
if ('projectionType' in param) {
this.projectionType = param['projectionType'];
}
// set time out make sure the receive objects be created
if ('receiveObjects' in param) {
let array = param['receiveObjects'];
if (typeof array === 'string') {
array = array.split(',');
}
Utils.setTimeout(() => {
let receiveObjects = [];
array.forEach(item => {
if (item.isObject3D) {
receiveObjects.push(item);
}
else if (typeof item === 'string') {
let obj = this.app.objectManager.getBaseObjectFromUUID(item);
if (obj) {
receiveObjects.push(obj);
}
}
})
this.setReceiveObjects(receiveObjects);
});
}
if ('image' in param) {
let image = param['image'];
if (image.isBaseTexture) {
this.image = image;
}
else if (typeof image.type === 'string') {
if (image.type == 'VideoTexture') {
this.image = new VideoTexture({ url: image.url });
}
else {
this.image = new ImageTexture(image.url);
}
}
}
}
_clearCurrentObject() {
this._node.clearCurrentObject(); // clear
for (let i = 0; i < this._receiveObjects.length; i++) {
if (!this._receiveObjects[i].destroyed) {
this._receiveObjects[i].render.callbacks.beforeMakeInstancedDrawing = null;
if (this._instanceState[i]) {
this._receiveObjects[i].makeInstancedDrawing(true);
}
}
}
this._receiveObjects.length = 0;
this._instanceState.length = 0;
}
/**
* @public
* 设置可以接收投影的物体列表。传入的物体必须是 Object3D 的实例,或者是它们的 UUID 字符串。
* @param {String[]|Object3D[]} value - 接收投影的物体列表,可以是 Object3D 实例或它们的 UUID 字符串。
*/
async setReceiveObjects(value) {
this._clearCurrentObject();
if (!value || value.length <= 0) {
return;
}
this._receiveObjects = value;
this._instanceState = [];
const temp = [];
for (let i = 0; i < value.length; i++) {
await value[i].waitForComplete();
if (value[i].destroyed) {
continue;
}
this._instanceState[i] = value[i].isInstancedDrawing;
value[i].makeInstancedDrawing(false);
value[i].render.callbacks.beforeMakeInstancedDrawing = function () {
console.warn('Cannot change instanced state when using projector!');
return false;
}
temp.push(value[i].node);
}
this._node.setReceiveNodes(temp);
}
/**
* Get array of nodes which receive this projector.
* @returns {Array}
*/
getReceiveObjects() {
return this._receiveObjects;
}
/**
* 设置/获取投影类型。默认值为 'Perspective'。
* 支持 'Perspective'(透视投影)和 'Orthographic'(正交投影)。
* @type {string}
* @example
* projector.projectionType = 'Perspective';
* @public
* @example
* projector.projectionType = 'Orthographic';
* projector.orthoSize = 10;
*/
set projectionType(value) {
this._node.setProjectionType(value);
}
get projectionType() {
return this._node.getProjectionType();
}
/**
* 设置/获取贴花相机近平面。
* @type {number}
* @example
* projector.near = 0.1;
* @public
*/
set near(value) {
this._node.setNear(value);
}
get near() {
return this._node.getNear();
}
/**
* 设置/获取贴花相机远平面。
* @type {number}
* @example
* projector.far = 1000;
* @public
*/
set far(value) {
this._node.setFar(value);
}
get far() {
return this._node.getFar();
}
/**
* 设置/获取贴花相机视角(度)。使用此选项时相机类型为 Perspective。
* @type {number}
* @example
* projector.fov = 60;
* @public
*/
set fov(value) {
this._node.setFov(value);
}
get fov() {
return this._node.getFov();
}
/**
* 设置/获取贴花相机纵横比。
* @type {number}
* @example
* projector.aspect = 1.5;
* @public
*/
set aspect(value) {
this._node.setAspect(value);
}
get aspect() {
return this._node.getAspect();
}
/**
* 设置/获取贴花相机正交尺寸。使用此选项时相机类型为 Orthographic。
* @type {number}
* @example
* projector.orthoSize = 10;
* @public
*/
set orthoSize(value) {
this._node.setOrthoSize(value);
}
get orthoSize() {
return this._node.getOrthoSize();
}
/**
* 设置/获取遮挡质量,默认值为 512。可选值包括 256、512、1024、2048 和 4096。
* @type {number}
* @example
* projector.occlusionQuality = 1024;
* @public
*/
set occlusionQuality(value) {
this._node.setOcclusionQuality(value);
}
get occlusionQuality() {
return this._node.getOcclusionQuality();
}
/**
* 设置/获取贴图。
* 贴图对象,包含图像的纹理资源。
* @type {THING.ImageTexture}
* @example
* projector.image = new THING.ImageTexture('./path/to/image.png');
* @public
*/
set image(value) {
this._map = value;
this._node.setMap(value.getTextureResource());
}
get image() {
return this._map;
}
/**
* 设置/获取是否使用球面空间。默认值为 false。
* @type {boolean}
* @example
* projector.sphereSpace = true;
* @public
*/
set sphereSpace(value) {
this._node.setSphereSpace(value);
this.debug = this._debug;
}
get sphereSpace() {
return this._node.getSphereSpace();
}
/**
* 设置/获取不透明度。
* @type {number}
* @example
* projector.opacity = 0.5;
* @public
*/
set opacity(value) {
this._node.setOpacity(value);
}
get opacity() {
return this._node.getOpacity();
}
/**
* 设置/获取贴花颜色。默认值为 [1, 1, 1](白色)。
* @type {Array<number>}
* @example
* projector.color = [1, 0, 0];
* @public
*/
set color(value) {
this._node.setColor(Utils.parseColor(value));
}
get color() {
return this._node.getColor();
}
/**
* 设置/获取是否使用遮挡。默认值为 true。
* @type {boolean}
* @example
* projector.occlusion = true;
* @public
*/
set occlusion(value) {
this._node.setOcclusion(value);
}
get occlusion() {
return this._node.getOcclusion();
}
/**
* 设置/获取遮挡偏移。默认值为 -0。
* @type {number}
* @example
* projector.occlusionBias = -0.001;
* @public
*/
set occlusionBias(value) {
this._node.setOcclusionBias(value);
}
get occlusionBias() {
return this._node.getOcclusionBias();
}
/**
* 设置鱼眼类型。
* 鱼眼类型值:
* 1 - 前
* 2 - 后
* 3 - 左
* 4 - 右
* 5 - 底部
* 默认值为 false。
* @type {FisheyeType}
* @example
* projector.fisheye = 'Front';
* @public
*/
set fisheye(value) {
if (value == false) {
this._node.setFisheye(value);
}
else {
this._node.setFisheye(FisheyeType[value]);
}
}
get fisheye() {
const fisheye = this._node.getFisheye()
if (fisheye == false) {
return fisheye;
}
else {
return FisheyeType_Reverse[fisheye];
}
}
/**
* 设置/获取是否使用桶形畸变矫正。默认值为 false。
* @type {boolean}
* @example
* projector.barrelCorrection = true;
* @public
*/
set barrelCorrection(value) {
this._node.setBarrelCorrection(value);
}
get barrelCorrection() {
return this._node.getBarrelCorrection();
}
/**
* 设置/获取桶形畸变 X 方向值。
* @type {number}
* @example
* projector.barrelFx = 1.0;
* @public
*/
set barrelFx(value) {
this._node.setBarrelFx(value);
}
get barrelFx() {
return this._node.getBarrelFx();
}
/**
* 设置/获取桶形畸变 Y 方向值。
* @type {number}
* @example
* projector.barrelFy = 1.0;
* @public
*/
set barrelFy(value) {
this._node.setBarrelFy(value);
}
get barrelFy() {
return this._node.getBarrelFy();
}
/**
* 设置/获取桶形畸变缩放因子。
* @type {number}
* @example
* projector.barrelS = 1.0;
* @public
*/
set barrelS(value) {
this._node.setBarrelS(value);
}
get barrelS() {
return this._node.getBarrelS();
}
/**
* 设置/获取是否使用调试。
* @type {boolean}
* @example
* projector.debug = true;
* @public
*/
set debug(value) {
// if (value === this._debug) {
// return;
// }
this._debug = value;
if (value) {
this._node.setDebug();
}
else {
this.node.closeDebug();
}
this.updateMatrixWorld();
}
get debug() {
return this._debug;
}
/**
* onDestroy callback
*/
onDestroy() {
this._receiveObjects = null;
this._map = null;
super.onDestroy();
}
onExportExternalData() {
let external = Object.assign({}, super.onExportExternalData());
// receiveObjects
let receiveObjectsURL = '';
this.getReceiveObjects().forEach(obj => {
receiveObjectsURL = receiveObjectsURL + obj.uuid + ',';
});
if (receiveObjectsURL) {
external.receiveObjects = receiveObjectsURL;
}
// image
if (this.image) {
let imageType = 'ImageTexture';
if (this.image.isVideoTexture) {
imageType = 'VideoTexture';
}
external.image = {
type: imageType,
url: this.image.url
}
}
Utils.setAttributeIfExist(external, 'projectionType', this, (v) => { v != 'Perspective' });
Utils.setAttributeIfExist(external, 'fisheye', this, (v) => { v != false });
Utils.setAttributeIfExist(external, 'debug', this, (v) => { v != false });
Utils.setAttributeIfExist(external, 'color', this);
Utils.setAttributeIfExist(external, 'opacity', this);
Utils.setAttributeIfExist(external, 'fov', this);
Utils.setAttributeIfExist(external, 'far', this);
Utils.setAttributeIfExist(external, 'near', this);
Utils.setAttributeIfExist(external, 'aspect', this);
Utils.setAttributeIfExist(external, 'orthoSize', this);
Utils.setAttributeIfExist(external, 'occlusion', this, (v) => { v != false });
Utils.setAttributeIfExist(external, 'occlusionBias', this, (v) => { v != -0.0004 });
Utils.setAttributeIfExist(external, 'occlusionQuality', this, (v) => { v != 512 });
Utils.setAttributeIfExist(external, 'barrelCorrection', this, (v) => { v != false });
Utils.setAttributeIfExist(external, 'barrelFx', this);
Utils.setAttributeIfExist(external, 'barrelFy', this);
Utils.setAttributeIfExist(external, 'barrelS', this);
return external;
}
}
export { Projector };