UNPKG

@polygonjs/polygonjs

Version:

node-based WebGL 3D engine https://polygonjs.com

297 lines (236 loc) 9.56 kB
// converted to typescript from: // https://github.com/mrdoob/three.js/blob/dev/examples/jsm/objects/Reflector.js import { Material, Scene, Camera, Object3D, BufferGeometry, WebGLRenderer, Mesh, WebGLRenderTarget, RenderTargetOptions, Vector2, Vector3, Vector4, LinearFilter, Plane, Matrix4, PerspectiveCamera, } from 'three'; import {CoreRenderBlur} from '../../../core/render/Blur'; import {isBooleanTrue} from '../../../core/Type'; import {CoreTransform, rotateGeometry} from '../../../core/Transform'; import {Poly} from '../../../engine/Poly'; import {AbstractRenderer} from '../../../engine/viewers/Common'; export interface BaseReflectorOptions { // color: Color; // opacity: number; renderer?: AbstractRenderer; pixelRatio: number; multisamples: number; clipBias: number; active: boolean; tblur: boolean; blur: number; verticalBlurMult: number; tblur2: boolean; blur2: number; verticalBlur2Mult: number; scene: Scene; } export const renderTargetParams: RenderTargetOptions = { minFilter: LinearFilter, magFilter: LinearFilter, // format: RGBAFormat, // encoding: LinearEncoding, }; export abstract class BaseReflector<TGeometry extends BufferGeometry, TMaterial extends Material> extends Mesh< TGeometry, TMaterial > { static REFLECTOR_DEFAULT_UP = new Vector3(0, 0, 1); public override type = 'BaseReflector'; private reflectorPlane = new Plane(); private normal = new Vector3(); private reflectorWorldPosition = new Vector3(); private cameraWorldPosition = new Vector3(); private rotationMatrix = new Matrix4(); private lookAtPosition = new Vector3(0, 0, -1); private clipPlane = new Vector4(); private view = new Vector3(); private target = new Vector3(); private q = new Vector4(); protected textureMatrix = new Matrix4(); private virtualCamera = new PerspectiveCamera(); protected renderTarget: WebGLRenderTarget | undefined; public override material: TMaterial = this._createMaterial(); protected _coreRenderBlur: CoreRenderBlur | undefined; public override onBeforeRender = this._onBeforeRender.bind(this); protected _mirrorCameraMultipliedByMatrixWorld = true; constructor(public override geometry: TGeometry, protected _options: BaseReflectorOptions) { super(geometry); if (this._options.renderer) { this._createRenderTarget(this._options.renderer); } this._addWindowResizeEvent(); } private _createRenderTarget(renderer: AbstractRenderer) { const {width, height} = this._getRendererSize(renderer); this.renderTarget = Poly.renderersController.createRenderTarget(width, height, renderTargetParams); if (this._options.multisamples > 0) { this.renderTarget.samples = this._options.multisamples; } this._assignMaterialRenderTarget(); this._coreRenderBlur = new CoreRenderBlur(new Vector2(width, height)); } protected abstract _createMaterial(): TMaterial; protected abstract _assignMaterialRenderTarget(): void; dispose() { this.geometry.dispose(); this.renderTarget?.dispose(); this.material?.dispose(); this.onBeforeRender = () => {}; this._removeWindowResizeEvent(); } private _onWindowResizeBound = this._onWindowResize.bind(this); private _addWindowResizeEvent() { globalThis.addEventListener('resize', this._onWindowResizeBound.bind(this), false); } private _removeWindowResizeEvent() { globalThis.removeEventListener('resize', this._onWindowResizeBound.bind(this), false); } private _onWindowResize() { // if the object has been detached, we remove the event // TODO: it should be added back when the object is added to the hierarchy this.traverseAncestors((object) => { if (!object.parent) { if (object.uuid != this._options.scene.uuid) { this._removeWindowResizeEvent(); } } }); const renderer = this._options.renderer; if (renderer) { const {width, height} = this._getRendererSize(renderer); this.renderTarget?.setSize(width, height); this._coreRenderBlur?.setSize(width, height); } } private _getRendererSize(renderer: AbstractRenderer) { const canvas = renderer.domElement; const width = canvas.width * this._options.pixelRatio; const height = canvas.height * this._options.pixelRatio; return {width, height}; } static rotateGeometry(geometry: BufferGeometry, direction: Vector3) { rotateGeometry(geometry, direction, this.REFLECTOR_DEFAULT_UP); } static compensateGeometryRotation(object: Object3D, direction: Vector3) { CoreTransform.rotateObject(object, this.REFLECTOR_DEFAULT_UP, direction); } protected _onBeforeRender( renderer: WebGLRenderer, scene: Scene, anyCamera: Camera // geometry: BufferGeometry, // material: Material, // group: Group ) { if (!this._options.active) { return; } if (!this.renderTarget) { this._createRenderTarget(renderer); } if (!(this.renderTarget && this._coreRenderBlur)) { return; } const camera = anyCamera as PerspectiveCamera; this.reflectorWorldPosition.setFromMatrixPosition(this.matrixWorld); this.cameraWorldPosition.setFromMatrixPosition(camera.matrixWorld); this.rotationMatrix.extractRotation(this.matrixWorld); this.normal.set(0, 0, 1); this.normal.applyMatrix4(this.rotationMatrix); this.view.subVectors(this.reflectorWorldPosition, this.cameraWorldPosition); // Avoid rendering when reflector is facing away if (this.view.dot(this.normal) > 0) return; this.view.reflect(this.normal).negate(); this.view.add(this.reflectorWorldPosition); this.rotationMatrix.extractRotation(camera.matrixWorld); this.lookAtPosition.set(0, 0, -1); this.lookAtPosition.applyMatrix4(this.rotationMatrix); this.lookAtPosition.add(this.cameraWorldPosition); this.target.subVectors(this.reflectorWorldPosition, this.lookAtPosition); this.target.reflect(this.normal).negate(); this.target.add(this.reflectorWorldPosition); this.virtualCamera.position.copy(this.view); this.virtualCamera.up.set(0, 1, 0); this.virtualCamera.up.applyMatrix4(this.rotationMatrix); this.virtualCamera.up.reflect(this.normal); this.virtualCamera.lookAt(this.target); this.virtualCamera.far = camera.far; // Used in WebGLBackground this.virtualCamera.updateMatrixWorld(); this.virtualCamera.projectionMatrix.copy(camera.projectionMatrix); // Update the texture matrix this.textureMatrix.set(0.5, 0.0, 0.0, 0.5, 0.0, 0.5, 0.0, 0.5, 0.0, 0.0, 0.5, 0.5, 0.0, 0.0, 0.0, 1.0); this.textureMatrix.multiply(this.virtualCamera.projectionMatrix); this.textureMatrix.multiply(this.virtualCamera.matrixWorldInverse); if (this._mirrorCameraMultipliedByMatrixWorld) { this.textureMatrix.multiply(this.matrixWorld); } // Now update projection matrix with new clip plane, implementing code from: http://www.terathon.com/code/oblique.html // Paper explaining this technique: http://www.terathon.com/lengyel/Lengyel-Oblique.pdf this.reflectorPlane.setFromNormalAndCoplanarPoint(this.normal, this.reflectorWorldPosition); this.reflectorPlane.applyMatrix4(this.virtualCamera.matrixWorldInverse); this.clipPlane.set( this.reflectorPlane.normal.x, this.reflectorPlane.normal.y, this.reflectorPlane.normal.z, this.reflectorPlane.constant ); var projectionMatrix = this.virtualCamera.projectionMatrix; this.q.x = (Math.sign(this.clipPlane.x) + projectionMatrix.elements[8]) / projectionMatrix.elements[0]; this.q.y = (Math.sign(this.clipPlane.y) + projectionMatrix.elements[9]) / projectionMatrix.elements[5]; this.q.z = -1.0; this.q.w = (1.0 + projectionMatrix.elements[10]) / projectionMatrix.elements[14]; // Calculate the scaled plane vector this.clipPlane.multiplyScalar(2.0 / this.clipPlane.dot(this.q)); // Replacing the third row of the projection matrix projectionMatrix.elements[2] = this.clipPlane.x; projectionMatrix.elements[6] = this.clipPlane.y; projectionMatrix.elements[10] = this.clipPlane.z + 1.0 - this._options.clipBias; projectionMatrix.elements[14] = this.clipPlane.w; // Render this.renderTarget.texture.colorSpace = renderer.outputColorSpace; this.visible = false; var currentRenderTarget = renderer.getRenderTarget(); var currentXrEnabled = renderer.xr.enabled; var currentShadowAutoUpdate = renderer.shadowMap.autoUpdate; renderer.xr.enabled = false; // Avoid camera modification renderer.shadowMap.autoUpdate = false; // Avoid re-computing shadows renderer.setRenderTarget(this.renderTarget); renderer.state.buffers.depth.setMask(true); // make sure the depth buffer is writable so it can be properly cleared, see #18897 if (renderer.autoClear === false) renderer.clear(); renderer.render(scene, this.virtualCamera); if (isBooleanTrue(this._options.tblur)) { const blurAmount = this._options.blur * this._options.pixelRatio; const verticalBlurAmount = blurAmount * this._options.verticalBlurMult; this._coreRenderBlur.applyBlur(this.renderTarget, renderer, blurAmount, verticalBlurAmount); if (isBooleanTrue(this._options.tblur2)) { const blurAmount2 = this._options.blur2 * this._options.pixelRatio; const verticalBlurAmount2 = blurAmount2 * this._options.verticalBlur2Mult; this._coreRenderBlur.applyBlur(this.renderTarget, renderer, blurAmount2, verticalBlurAmount2); } } renderer.xr.enabled = currentXrEnabled; renderer.shadowMap.autoUpdate = currentShadowAutoUpdate; renderer.setRenderTarget(currentRenderTarget); // Restore viewport var viewport = (camera as any).viewport; if (viewport !== undefined) { renderer.state.viewport(viewport); } this.visible = true; } }