1
0
mirror of https://github.com/troisjs/trois.git synced 2024-11-24 04:12:02 +08:00
This commit is contained in:
Kevin Levron 2021-04-16 00:54:11 +02:00
parent 6bbb80710d
commit 154fd7439c
4 changed files with 536 additions and 439 deletions

View File

@ -1,175 +0,0 @@
import { InstancedMesh, Vector2, Vector3 } from 'three';
import useRaycaster from './useRaycaster';
export default function usePointer(options) {
const {
camera,
domElement,
intersectObjects,
touch = true,
resetOnEnd = false,
resetPosition = new Vector2(0, 0),
resetPositionV3 = new Vector3(0, 0, 0),
onEnter = () => {},
onMove = () => {},
onLeave = () => {},
onIntersectEnter = () => {},
onIntersectOver = () => {},
onIntersectMove = () => {},
onIntersectLeave = () => {},
onIntersectClick = () => {},
} = options;
const position = resetPosition.clone();
const positionN = new Vector2(0, 0);
const raycaster = useRaycaster({ camera });
const positionV3 = raycaster.position;
const obj = {
position,
positionN,
positionV3,
intersectObjects,
listeners: false,
addListeners,
removeListeners,
intersect,
};
return obj;
function reset() {
position.copy(resetPosition);
positionV3.copy(resetPositionV3);
};
function updatePosition(event) {
let x, y;
if (event.touches && event.touches.length > 0) {
x = event.touches[0].clientX;
y = event.touches[0].clientY;
} else {
x = event.clientX;
y = event.clientY;
}
const rect = domElement.getBoundingClientRect();
position.x = x - rect.left;
position.y = y - rect.top;
positionN.x = (position.x / rect.width) * 2 - 1;
positionN.y = -(position.y / rect.height) * 2 + 1;
raycaster.updatePosition(positionN);
};
function intersect() {
if (intersectObjects.length) {
const intersects = raycaster.intersect(positionN, intersectObjects);
const offObjects = [...intersectObjects];
const iMeshes = [];
intersects.forEach(intersect => {
const { object } = intersect;
const { component } = object;
// only once for InstancedMesh
if (object instanceof InstancedMesh) {
if (iMeshes.indexOf(object) !== -1) return;
iMeshes.push(object);
}
if (!object.over) {
object.over = true;
const overEvent = { type: 'pointerover', over: true, component, intersect };
const enterEvent = { ...overEvent, type: 'pointerenter' };
onIntersectOver(overEvent);
onIntersectEnter(enterEvent);
if (component.onPointerOver) component.onPointerOver(overEvent);
if (component.onPointerEnter) component.onPointerEnter(enterEvent);
}
const moveEvent = { type: 'pointermove', component, intersect };
onIntersectMove(moveEvent);
if (component.onPointerMove) component.onPointerMove(moveEvent);
offObjects.splice(offObjects.indexOf(object), 1);
});
offObjects.forEach(object => {
const { component } = object;
if (object.over) {
object.over = false;
const overEvent = { type: 'pointerover', over: false, component };
const leaveEvent = { ...overEvent, type: 'pointerleave' };
onIntersectOver(overEvent);
onIntersectLeave(leaveEvent);
if (component.onPointerOver) component.onPointerOver(overEvent);
if (component.onPointerLeave) component.onPointerLeave(leaveEvent);
}
});
}
};
function pointerEnter(event) {
updatePosition(event);
onEnter({ type: 'pointerenter', position, positionN, positionV3 });
};
function pointerMove(event) {
updatePosition(event);
onMove({ type: 'pointermove', position, positionN, positionV3 });
intersect();
};
function pointerClick(event) {
updatePosition(event);
if (intersectObjects.length) {
const intersects = raycaster.intersect(positionN, intersectObjects);
const iMeshes = [];
intersects.forEach(intersect => {
const { object } = intersect;
const { component } = object;
// only once for InstancedMesh
if (object instanceof InstancedMesh) {
if (iMeshes.indexOf(object) !== -1) return;
iMeshes.push(object);
}
const event = { type: 'click', component, intersect };
onIntersectClick(event);
if (component.onClick) component.onClick(event);
});
}
};
function pointerLeave() {
if (resetOnEnd) reset();
onLeave({ type: 'pointerleave' });
};
function addListeners() {
domElement.addEventListener('mouseenter', pointerEnter);
domElement.addEventListener('mousemove', pointerMove);
domElement.addEventListener('mouseleave', pointerLeave);
domElement.addEventListener('click', pointerClick);
if (touch) {
domElement.addEventListener('touchstart', pointerEnter);
domElement.addEventListener('touchmove', pointerMove);
domElement.addEventListener('touchend', pointerLeave);
}
obj.listeners = true;
};
function removeListeners() {
domElement.removeEventListener('mouseenter', pointerEnter);
domElement.removeEventListener('mousemove', pointerMove);
domElement.removeEventListener('mouseleave', pointerLeave);
domElement.removeEventListener('click', pointerClick);
domElement.removeEventListener('touchstart', pointerEnter);
domElement.removeEventListener('touchmove', pointerMove);
domElement.removeEventListener('touchend', pointerLeave);
obj.listeners = false;
};
};

225
src/core/usePointer.ts Normal file
View File

@ -0,0 +1,225 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { Camera, InstancedMesh, Intersection, Mesh, Vector2, Vector3 } from 'three'
import useRaycaster from './useRaycaster'
export interface PointerEventInterface {
type: 'pointerenter' | 'pointermove' | 'pointerleave' | 'click'
position?: Vector2
positionN?: Vector2
positionV3?: Vector3
}
export interface PointerIntersectEventInterface {
type: 'pointerenter' | 'pointerover' | 'pointermove' | 'pointerleave' | 'click'
component: any
over?: boolean
intersect?: Intersection
}
export type IntersectObject = Mesh | InstancedMesh
export interface PointerConfigInterface {
camera: Camera
domElement: HTMLCanvasElement
intersectObjects: IntersectObject[]
intersectMode?: 'frame'
touch?: boolean
resetOnEnd?: boolean
resetPosition?: Vector2
resetPositionV3?: Vector3
onEnter?(e: PointerEventInterface): void
onMove?(e: PointerEventInterface): void
onLeave?(e: PointerEventInterface): void
onClick?(e: PointerEventInterface): void
onIntersectEnter?(e: PointerIntersectEventInterface): void
onIntersectOver?(e: PointerIntersectEventInterface): void
onIntersectMove?(e: PointerIntersectEventInterface): void
onIntersectLeave?(e: PointerIntersectEventInterface): void
onIntersectClick?(e: PointerIntersectEventInterface): void
}
export interface PointerInterface {
position: Vector2
positionN: Vector2
positionV3: Vector3
intersectObjects: IntersectObject[]
listeners: boolean
addListeners(cb: void): void
removeListeners(cb: void): void
intersect(): void
}
export default function usePointer(options: PointerConfigInterface): PointerInterface {
const {
camera,
domElement,
intersectObjects,
touch = true,
resetOnEnd = false,
resetPosition = new Vector2(0, 0),
resetPositionV3 = new Vector3(0, 0, 0),
onEnter = () => {},
onMove = () => {},
onLeave = () => {},
onClick = () => {},
onIntersectEnter = () => {},
onIntersectOver = () => {},
onIntersectMove = () => {},
onIntersectLeave = () => {},
onIntersectClick = () => {},
} = options
const position = resetPosition.clone()
const positionN = new Vector2(0, 0)
const raycaster = useRaycaster({ camera })
const positionV3 = raycaster.position
const obj: PointerInterface = {
position,
positionN,
positionV3,
intersectObjects,
listeners: false,
addListeners,
removeListeners,
intersect,
}
return obj
function reset() {
position.copy(resetPosition)
positionV3.copy(resetPositionV3)
}
function updatePosition(event: TouchEvent | MouseEvent) {
let x, y
if (event instanceof TouchEvent && event.touches && event.touches.length > 0) {
x = (<TouchEvent>event).touches[0].clientX
y = (<TouchEvent>event).touches[0].clientY
} else {
x = (<MouseEvent>event).clientX
y = (<MouseEvent>event).clientY
}
const rect = domElement.getBoundingClientRect()
position.x = x - rect.left
position.y = y - rect.top
positionN.x = (position.x / rect.width) * 2 - 1
positionN.y = -(position.y / rect.height) * 2 + 1
raycaster.updatePosition(positionN)
}
function intersect() {
if (intersectObjects.length) {
const intersects = raycaster.intersect(positionN, intersectObjects)
const offObjects: IntersectObject[] = [...intersectObjects]
const iMeshes: InstancedMesh[] = []
intersects.forEach(intersect => {
const { object } = intersect
const { component } = object.userData
// only once for InstancedMesh
if (object instanceof InstancedMesh) {
if (iMeshes.indexOf(object) !== -1) return
iMeshes.push(object)
}
if (!object.userData.over) {
object.userData.over = true
const overEvent: PointerIntersectEventInterface = { type: 'pointerover', over: true, component, intersect }
const enterEvent: PointerIntersectEventInterface = { ...overEvent, type: 'pointerenter' }
onIntersectOver(overEvent)
onIntersectEnter(enterEvent)
component.onPointerOver?.(overEvent)
component.onPointerEnter?.(enterEvent)
}
const moveEvent: PointerIntersectEventInterface = { type: 'pointermove', component, intersect }
onIntersectMove(moveEvent)
component.onPointerMove?.(moveEvent)
offObjects.splice(offObjects.indexOf((<IntersectObject>object)), 1)
})
offObjects.forEach(object => {
const { component } = object.userData
if (object.userData.over) {
object.userData.over = false
const overEvent: PointerIntersectEventInterface = { type: 'pointerover', over: false, component }
const leaveEvent: PointerIntersectEventInterface = { ...overEvent, type: 'pointerleave' }
onIntersectOver(overEvent)
onIntersectLeave(leaveEvent)
component.onPointerOver?.(overEvent)
component.onPointerLeave?.(leaveEvent)
}
})
}
}
function pointerEnter(event: TouchEvent | MouseEvent) {
updatePosition(event)
onEnter({ type: 'pointerenter', position, positionN, positionV3 })
}
function pointerMove(event: TouchEvent | MouseEvent) {
updatePosition(event)
onMove({ type: 'pointermove', position, positionN, positionV3 })
intersect()
}
function pointerClick(event: TouchEvent | MouseEvent) {
updatePosition(event)
if (intersectObjects.length) {
const intersects = raycaster.intersect(positionN, intersectObjects)
const iMeshes: InstancedMesh[] = []
intersects.forEach(intersect => {
const { object } = intersect
const { component } = object.userData
// only once for InstancedMesh
if (object instanceof InstancedMesh) {
if (iMeshes.indexOf(object) !== -1) return
iMeshes.push(object)
}
const event: PointerIntersectEventInterface = { type: 'click', component, intersect }
onIntersectClick(event)
component.onClick?.(event)
})
}
onClick({ type: 'click', position, positionN, positionV3 })
}
function pointerLeave() {
if (resetOnEnd) reset()
onLeave({ type: 'pointerleave' })
}
function addListeners() {
domElement.addEventListener('mouseenter', pointerEnter)
domElement.addEventListener('mousemove', pointerMove)
domElement.addEventListener('mouseleave', pointerLeave)
domElement.addEventListener('click', pointerClick)
if (touch) {
domElement.addEventListener('touchstart', pointerEnter)
domElement.addEventListener('touchmove', pointerMove)
domElement.addEventListener('touchend', pointerLeave)
}
obj.listeners = true
}
function removeListeners() {
domElement.removeEventListener('mouseenter', pointerEnter)
domElement.removeEventListener('mousemove', pointerMove)
domElement.removeEventListener('mouseleave', pointerLeave)
domElement.removeEventListener('click', pointerClick)
domElement.removeEventListener('touchstart', pointerEnter)
domElement.removeEventListener('touchmove', pointerMove)
domElement.removeEventListener('touchend', pointerLeave)
obj.listeners = false
}
}

View File

@ -1,264 +0,0 @@
import { WebGLRenderer } from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import usePointer from './usePointer';
/**
* Three.js helper
*/
export default function useThree() {
// default conf
const conf = {
canvas: null,
antialias: true,
alpha: false,
autoClear: true,
orbit_ctrl: false,
pointer: false,
resize: false,
width: 300,
height: 150,
};
// size
const size = {
width: 1, height: 1,
wWidth: 1, wHeight: 1,
ratio: 1,
};
// handlers
const afterInitCallbacks = [];
let afterResizeCallbacks = [];
let beforeRenderCallbacks = [];
const intersectObjects = [];
// returned object
const obj = {
conf,
renderer: null,
camera: null,
cameraCtrl: null,
scene: null,
pointer: null,
size,
init,
dispose,
render,
renderC,
setSize,
onAfterInit,
onAfterResize, offAfterResize,
// onBeforeRender, offBeforeRender,
addIntersectObject, removeIntersectObject,
};
/**
* init three
*/
function init(params) {
if (params) {
Object.entries(params).forEach(([key, value]) => {
conf[key] = value;
});
}
if (!obj.scene) {
console.error('Missing Scene');
return;
}
if (!obj.camera) {
console.error('Missing Camera');
return;
}
obj.renderer = new WebGLRenderer({ canvas: conf.canvas, antialias: conf.antialias, alpha: conf.alpha });
obj.renderer.autoClear = conf.autoClear;
if (conf.resize) {
onResize();
window.addEventListener('resize', onResize);
} else {
setSize(conf.width, conf.height);
}
initPointer();
if (conf.orbit_ctrl) {
obj.orbitCtrl = new OrbitControls(obj.camera, obj.renderer.domElement);
if (conf.orbit_ctrl instanceof Object) {
Object.entries(conf.orbit_ctrl).forEach(([key, value]) => {
obj.orbitCtrl[key] = value;
});
}
}
afterInitCallbacks.forEach(c => c());
return true;
};
function initPointer() {
let pointerConf = {
camera: obj.camera,
domElement: obj.renderer.domElement,
intersectObjects,
};
if (conf.pointer && conf.pointer instanceof Object) {
pointerConf = { ...pointerConf, ...conf.pointer };
}
obj.pointer = usePointer(pointerConf);
if (conf.pointer || intersectObjects.length) {
obj.pointer.addListeners();
if (conf.pointer.intersectMode === 'frame') {
onBeforeRender(() => {
obj.pointer.intersect();
});
}
}
}
/**
* add after init callback
*/
function onAfterInit(callback) {
afterInitCallbacks.push(callback);
}
/**
* add after resize callback
*/
function onAfterResize(callback) {
afterResizeCallbacks.push(callback);
}
/**
* remove after resize callback
*/
function offAfterResize(callback) {
afterResizeCallbacks = afterResizeCallbacks.filter(c => c !== callback);
}
/**
* add before render callback
*/
function onBeforeRender(callback) {
beforeRenderCallbacks.push(callback);
}
/**
* remove before render callback
*/
function offBeforeRender(callback) {
beforeRenderCallbacks = beforeRenderCallbacks.filter(c => c !== callback);
}
/**
* default render
*/
function render() {
if (obj.orbitCtrl) obj.orbitCtrl.update();
beforeRenderCallbacks.forEach(c => c());
obj.renderer.render(obj.scene, obj.camera);
}
/**
* composer render
*/
function renderC() {
if (obj.orbitCtrl) obj.orbitCtrl.update();
beforeRenderCallbacks.forEach(c => c());
obj.composer.render();
}
/**
* add intersect object
*/
function addIntersectObject(o) {
if (intersectObjects.indexOf(o) === -1) {
intersectObjects.push(o);
}
// add listeners if needed
if (obj.pointer && !obj.pointer.listeners) {
obj.pointer.addListeners();
}
}
/**
* remove intersect object
*/
function removeIntersectObject(o) {
const i = intersectObjects.indexOf(o);
if (i !== -1) {
intersectObjects.splice(i, 1);
}
// remove listeners if needed
if (obj.pointer && !conf.pointer && intersectObjects.length === 0) {
obj.pointer.removeListeners();
}
}
/**
* remove listeners and dispose
*/
function dispose() {
beforeRenderCallbacks = [];
window.removeEventListener('resize', onResize);
if (obj.pointer) obj.pointer.removeListeners();
if (obj.orbitCtrl) obj.orbitCtrl.dispose();
if (obj.renderer) obj.renderer.dispose();
}
/**
* resize listener
*/
function onResize() {
if (conf.resize === 'window') {
setSize(window.innerWidth, window.innerHeight);
} else {
const elt = obj.renderer.domElement.parentNode;
setSize(elt.clientWidth, elt.clientHeight);
}
afterResizeCallbacks.forEach(c => c());
}
/**
* update renderer size and camera
*/
function setSize(width, height) {
size.width = width;
size.height = height;
size.ratio = width / height;
obj.renderer.setSize(width, height, false);
obj.camera.aspect = size.ratio;
obj.camera.updateProjectionMatrix();
if (obj.composer) {
obj.composer.setSize(width, height);
}
if (obj.camera.type === 'OrthographicCamera') {
size.wWidth = obj.camera.right - obj.camera.left;
size.wHeight = obj.camera.top - obj.camera.bottom;
} else {
const wsize = getCameraSize();
size.wWidth = wsize[0]; size.wHeight = wsize[1];
}
}
/**
* calculate camera visible area size
*/
function getCameraSize() {
const vFOV = (obj.camera.fov * Math.PI) / 180;
const h = 2 * Math.tan(vFOV / 2) * Math.abs(obj.camera.position.z);
const w = h * obj.camera.aspect;
return [w, h];
}
return obj;
}

311
src/core/useThree.ts Normal file
View File

@ -0,0 +1,311 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { Camera, OrthographicCamera, PerspectiveCamera, Scene, WebGLRenderer } from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import usePointer, { IntersectObject, PointerConfigInterface, PointerInterface } from './usePointer'
export interface ConfigInterface {
canvas?: HTMLCanvasElement
antialias: boolean
alpha: boolean
autoClear: boolean
orbitCtrl: boolean | Record<string, unknown>
pointer: boolean | PointerConfigInterface
resize: boolean | 'window'
width: number
height: number
[index:string]: any
}
export interface SizeInterface {
width: number
height: number
wWidth: number
wHeight: number
ratio: number
}
export interface ThreeInterface {
conf: ConfigInterface
renderer?: WebGLRenderer
camera?: Camera
cameraCtrl?: OrbitControls
scene?: Scene
pointer?: PointerInterface
size: SizeInterface
composer?: EffectComposer
init(config: ConfigInterface): boolean
dispose(): void
render(): void
renderC(): void
setSize(width: number, height: number): void
onAfterInit(callback: {(): void}): void
onAfterResize(callback: {(): void}): void
offAfterResize(callback: {(): void}): void
addIntersectObject(o: IntersectObject): void
removeIntersectObject(o: IntersectObject): void
}
/**
* Three.js helper
*/
export default function useThree(): ThreeInterface {
// default conf
const conf: ConfigInterface = {
antialias: true,
alpha: false,
autoClear: true,
orbitCtrl: false,
pointer: false,
resize: false,
width: 300,
height: 150,
}
// size
const size: SizeInterface = {
width: 1, height: 1,
wWidth: 1, wHeight: 1,
ratio: 1,
}
// handlers
// const afterInitCallbacks: void[] = []
// let afterResizeCallbacks: void[] = []
// let beforeRenderCallbacks: void[] = []
const afterInitCallbacks: {(): void;}[] = []
let afterResizeCallbacks: {(): void;}[] = []
let beforeRenderCallbacks: {(): void;}[] = []
const intersectObjects: IntersectObject[] = []
// returned object
const obj: ThreeInterface = {
conf,
size,
init,
dispose,
render,
renderC,
setSize,
onAfterInit,
onAfterResize, offAfterResize,
addIntersectObject, removeIntersectObject,
}
return obj
/**
* init three
*/
function init(params: ConfigInterface) {
if (params) {
Object.entries(params).forEach(([key, value]) => {
conf[key] = value
})
}
if (!obj.scene) {
console.error('Missing Scene')
return false
}
if (!obj.camera) {
console.error('Missing Camera')
return false
}
obj.renderer = new WebGLRenderer({ canvas: conf.canvas, antialias: conf.antialias, alpha: conf.alpha })
obj.renderer.autoClear = conf.autoClear
if (conf.resize) {
onResize()
window.addEventListener('resize', onResize)
} else {
setSize(conf.width, conf.height)
}
initPointer()
if (conf.orbitCtrl) {
obj.cameraCtrl = new OrbitControls(obj.camera, obj.renderer.domElement)
if (conf.orbitCtrl instanceof Object) {
Object.entries(conf.orbitCtrl).forEach(([key, value]) => {
// @ts-ignore
obj.cameraCtrl[key] = value
})
}
}
afterInitCallbacks.forEach(c => c())
return true
}
function initPointer() {
let pointerConf: PointerConfigInterface = {
camera: obj.camera!,
domElement: obj.renderer!.domElement,
intersectObjects,
}
if (conf.pointer && conf.pointer instanceof Object) {
pointerConf = { ...pointerConf, ...conf.pointer }
}
const pointer = obj.pointer = usePointer(pointerConf)
if (conf.pointer || intersectObjects.length) {
pointer.addListeners()
if (pointerConf.intersectMode === 'frame') {
onBeforeRender(pointer.intersect)
}
}
}
/**
* add after init callback
*/
function onAfterInit(callback: {(): void}) {
afterInitCallbacks.push(callback)
}
/**
* add after resize callback
*/
function onAfterResize(callback: {(): void}) {
afterResizeCallbacks.push(callback)
}
/**
* remove after resize callback
*/
function offAfterResize(callback: {(): void}) {
afterResizeCallbacks = afterResizeCallbacks.filter(c => c !== callback)
}
/**
* add before render callback
*/
function onBeforeRender(callback: {(): void}) {
beforeRenderCallbacks.push(callback)
}
/**
* remove before render callback
*/
// function offBeforeRender(callback: void) {
// beforeRenderCallbacks = beforeRenderCallbacks.filter(c => c !== callback)
// }
/**
* default render
*/
function render() {
if (obj.cameraCtrl) obj.cameraCtrl.update()
beforeRenderCallbacks.forEach(c => c())
obj.renderer!.render(obj.scene!, obj.camera!)
}
/**
* composer render
*/
function renderC() {
if (obj.cameraCtrl) obj.cameraCtrl.update()
beforeRenderCallbacks.forEach(c => c())
obj.composer!.render()
}
/**
* add intersect object
*/
function addIntersectObject(o: IntersectObject) {
if (intersectObjects.indexOf(o) === -1) {
intersectObjects.push(o)
}
// add listeners if needed
if (obj.pointer && !obj.pointer.listeners) {
obj.pointer.addListeners()
}
}
/**
* remove intersect object
*/
function removeIntersectObject(o: IntersectObject) {
const i = intersectObjects.indexOf(o)
if (i !== -1) {
intersectObjects.splice(i, 1)
}
// remove listeners if needed
if (obj.pointer && !conf.pointer && intersectObjects.length === 0) {
obj.pointer.removeListeners()
}
}
/**
* remove listeners and dispose
*/
function dispose() {
beforeRenderCallbacks = []
window.removeEventListener('resize', onResize)
if (obj.pointer) obj.pointer.removeListeners()
if (obj.cameraCtrl) obj.cameraCtrl.dispose()
if (obj.renderer) obj.renderer.dispose()
}
/**
* resize listener
*/
function onResize() {
if (conf.resize === 'window') {
setSize(window.innerWidth, window.innerHeight)
} else {
const elt = obj.renderer!.domElement.parentNode as Element
if (elt) setSize(elt.clientWidth, elt.clientHeight)
}
afterResizeCallbacks.forEach(c => c())
}
/**
* update renderer size and camera
*/
function setSize(width: number, height: number) {
size.width = width
size.height = height
size.ratio = width / height
obj.renderer!.setSize(width, height, false)
// already done in EffectComposer
// if (obj.composer) {
// obj.composer.setSize(width, height)
// }
const camera = (<Camera>obj.camera!)
if (camera.type === 'PerspectiveCamera') {
const pCamera = (<PerspectiveCamera>camera)
pCamera.aspect = size.ratio
pCamera.updateProjectionMatrix()
}
if (camera.type === 'OrthographicCamera') {
const oCamera = (<OrthographicCamera>camera)
size.wWidth = oCamera.right - oCamera.left
size.wHeight = oCamera.top - oCamera.bottom
} else {
const wsize = getCameraSize()
size.wWidth = wsize[0]; size.wHeight = wsize[1]
}
}
/**
* calculate camera visible area size
*/
function getCameraSize() {
const camera = (<PerspectiveCamera>obj.camera!)
const vFOV = (camera.fov * Math.PI) / 180
const h = 2 * Math.tan(vFOV / 2) * Math.abs(camera.position.z)
const w = h * camera.aspect
return [w, h]
}
}