@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
297 lines (236 loc) • 9.56 kB
text/typescript
// 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;
}
}