@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
452 lines (418 loc) • 13.2 kB
text/typescript
import {
Line,
LineBasicMaterial,
Material,
Matrix4,
Mesh,
Object3D,
Vector2,
WebGLRenderer,
WebGLRenderTarget,
Object3DEventMap,
} from 'three';
import {WebGLCapabilities} from 'three/src/renderers/webgl/WebGLCapabilities';
import {Scene, Camera, MeshBasicMaterial} from 'three';
import {FullScreenQuad} from 'three/examples/jsm/postprocessing/Pass';
import {AbstractRenderer} from '../../../../viewers/Common';
import {isArray} from '../../../../../core/Type';
import {CoreSleep} from '../../../../../core/Sleep';
import {
PathTracingRenderer,
PathTracingSceneWorker,
PathTracingSceneGenerator,
BlurredEnvMapGenerator,
PhysicalCamera,
DenoiseMaterial,
GeneratorResult,
} from '../../../../../core/render/PBR/three-gpu-pathtracer';
import type {PathTracingRendererRopNode} from '../../PathTracingRenderer';
interface UpdateOptions {
//
resolutionScale: number;
displayDebug: boolean;
bounces: number;
transmissiveBounces: number;
stableNoise: boolean;
filterGlossyFactor: number;
backgroundBlur: number;
environmentIntensity: number;
tiles: Vector2;
multipleImportanceSampling: boolean;
//
denoise: boolean;
denoiseSigma: number;
denoiseThreshold: number;
denoiseKSigma: number;
//
maxSamplesCount: number;
samplesPerAnimationFrame: number;
f: Vector2;
//
useWorker: boolean;
}
interface PathtracingRendererContainerOptions {
node: PathTracingRendererRopNode;
webGLRenderer: WebGLRenderer;
pathTracingRenderer: PathTracingRenderer;
fsQuad: FullScreenQuad;
fsQuadMat: MeshBasicMaterial;
denoiseQuad: FullScreenQuad;
denoiseMat: DenoiseMaterial;
}
export class PathTracingRendererContainer implements AbstractRenderer {
public displayDebug = true;
public backgroundBlur = 0.1;
public maxSamplesCount = 1;
public samplesPerAnimationFrame = 1;
public frameRange = new Vector2();
public resolutionScale = 0.5;
private _useWorker: boolean = false;
public domElement: HTMLCanvasElement;
private _generated = false;
private _generating = false;
public readonly isPathTracingRendererContainer = true;
private _denoiseActive: boolean = false;
public readonly node: PathTracingRendererRopNode;
public readonly webGLRenderer: WebGLRenderer;
public readonly pathTracingRenderer: PathTracingRenderer;
public readonly fsQuad: FullScreenQuad;
public readonly fsQuadMat: MeshBasicMaterial;
public readonly denoiseQuad: FullScreenQuad;
public readonly denoiseMat: DenoiseMaterial;
constructor(options: PathtracingRendererContainerOptions) {
this.node = options.node;
this.webGLRenderer = options.webGLRenderer;
this.pathTracingRenderer = options.pathTracingRenderer;
this.fsQuad = options.fsQuad;
this.fsQuadMat = options.fsQuadMat;
this.denoiseQuad = options.denoiseQuad;
this.denoiseMat = options.denoiseMat;
this.domElement = this.webGLRenderer.domElement;
}
private _multipleImportanceSampling: boolean = true;
update(options: UpdateOptions) {
const {pathTracingRenderer} = this;
let resetRequired = false;
let generateRequired = false;
if (this.resolutionScale != options.resolutionScale) {
this.resolutionScale = options.resolutionScale;
resetRequired = true;
}
if (this.displayDebug != options.displayDebug) {
this.displayDebug = options.displayDebug;
resetRequired = true;
}
if (pathTracingRenderer.material.bounces != options.bounces) {
pathTracingRenderer.material.bounces = options.bounces;
resetRequired = true;
}
if (pathTracingRenderer.material.transmissiveBounces != options.transmissiveBounces) {
pathTracingRenderer.material.transmissiveBounces = options.transmissiveBounces;
resetRequired = true;
}
if (pathTracingRenderer.stableNoise != options.stableNoise) {
pathTracingRenderer.stableNoise = options.stableNoise;
resetRequired = true;
}
if (pathTracingRenderer.material.filterGlossyFactor != options.filterGlossyFactor) {
pathTracingRenderer.material.filterGlossyFactor = options.filterGlossyFactor;
resetRequired = true;
}
if (this.backgroundBlur != options.backgroundBlur) {
this.backgroundBlur = options.backgroundBlur;
generateRequired = true;
}
if (pathTracingRenderer.material.environmentIntensity != options.environmentIntensity) {
pathTracingRenderer.material.environmentIntensity = options.environmentIntensity;
resetRequired = true;
}
if (!pathTracingRenderer.tiles.equals(options.tiles)) {
pathTracingRenderer.tiles.set(options.tiles.x, options.tiles.y);
resetRequired = true;
}
if (this._multipleImportanceSampling != options.multipleImportanceSampling) {
this._multipleImportanceSampling = options.multipleImportanceSampling;
pathTracingRenderer.material.setDefine('FEATURE_MIS', Number(options.multipleImportanceSampling));
resetRequired = true;
}
//
this._denoiseActive = options.denoise;
this.denoiseMat.sigma = options.denoiseSigma;
this.denoiseMat.threshold = options.denoiseThreshold;
this.denoiseMat.kSigma = options.denoiseKSigma;
//
this._useWorker = options.useWorker;
// for progressive render only, no reset/generate required
if (this.maxSamplesCount > options.maxSamplesCount) {
// do a reset if the new max samples count is lower than the current one
resetRequired = true;
}
this.maxSamplesCount = options.maxSamplesCount;
// for sequence render only, no reset/generate required
this.samplesPerAnimationFrame = options.samplesPerAnimationFrame;
this.frameRange.copy(options.f);
// reset/generate if required
if (generateRequired) {
this.markAsNotGenerated();
} else if (resetRequired) {
this.reset();
}
}
render(scene: Scene, camera: PhysicalCamera) {
this.renderRealtime(scene, camera);
}
pbrRenderAllowed() {
return this._generating == false && this._generated == true;
}
markAsNotGenerated() {
this._generated = false;
}
renderRealtime(scene: Scene, camera: PhysicalCamera) {
if (!this.pbrRenderAllowed()) {
this.webGLRenderer.render(scene, camera);
return;
}
const maxSamplesCount = this.maxSamplesCount;
if (this.pathTracingRenderer.samples >= maxSamplesCount) {
return;
}
this.pathTracingRenderer.material.physicalCamera.updateFrom(camera);
this._preRender(camera);
this.pathTracingRenderer.update();
if (this.pathTracingRenderer.samples < 1) {
this.webGLRenderer.render(scene, camera);
}
this._postRender();
}
private _preRender(camera: Camera) {
this.pathTracingRenderer.camera = camera;
camera.updateMatrixWorld();
this._resetIfCameraUpdated(camera);
}
private _postRender() {
this.webGLRenderer.autoClear = false;
const quad = this._denoiseActive ? this.denoiseQuad : this.fsQuad;
const mat = this._denoiseActive ? this.denoiseMat : this.fsQuadMat;
mat.map = this.pathTracingRenderer.target.texture;
quad.render(this.webGLRenderer);
this.webGLRenderer.autoClear = true;
}
samplesCount() {
return Math.floor(this.pathTracingRenderer.samples);
}
getPixelRatio() {
return this.webGLRenderer.getPixelRatio();
}
compile(scene: Object3D<Object3DEventMap>, camera: Camera, targetScene: Scene | null | undefined) {
return this.webGLRenderer.compile(scene, camera, targetScene);
}
dispose(): void {
// Note:
// it's best NOT to dispose the renderers here,
// so that we can switch from one camera to the other in the editor.
// If we were to dispose, re-using the renderers would fail.
// this.webGLRenderer.dispose();
// this.pathTracingRenderer.dispose();
// this.fsQuadMat.dispose();
}
setSize(w: number, h: number, setStyle: boolean) {
this.webGLRenderer.setSize(w, h, setStyle);
this.pathTracingRenderer.setSize(
w * this.resolutionScale * globalThis.devicePixelRatio,
h * this.resolutionScale * globalThis.devicePixelRatio
);
this.pathTracingRenderer.reset();
}
setRenderTarget(
renderTarget: WebGLRenderTarget | null,
activeCubeFace?: number | undefined,
activeMipmapLevel?: number | undefined
) {
this.webGLRenderer.setRenderTarget(renderTarget, activeCubeFace, activeMipmapLevel);
}
readRenderTargetPixels(
renderTarget: WebGLRenderTarget,
x: number,
y: number,
width: number,
height: number,
buffer: Float32Array,
activeCubeFaceIndex?: number | undefined
) {
return this.webGLRenderer.readRenderTargetPixels(
renderTarget,
x,
y,
width,
height,
buffer,
activeCubeFaceIndex
);
}
get capabilities(): WebGLCapabilities {
return this.webGLRenderer.capabilities;
}
private _generator = new PathTracingSceneGenerator();
private _workerGenerator: PathTracingSceneWorker | undefined;
private _generateRequired = false;
async generate(scene: Scene) {
if (this._generating) {
this._generateRequired = true;
return;
}
console.warn('GENERATOR START');
this._generated = false;
this._generating = true;
const timeStart = performance.now();
const _restartIfRequired = () => {
if (this._generateRequired) {
this._generateRequired = false;
this._generating = false;
console.log('GENERATOR RESTART...');
const _this = this;
function generate() {
_this.generate(scene);
}
setTimeout(generate, 20);
return true;
}
};
const bgTexture = scene.environment;
if (bgTexture) {
const generator = new BlurredEnvMapGenerator(this.webGLRenderer);
const blurredTex = generator.generate(bgTexture, this.backgroundBlur);
this.pathTracingRenderer.material.envMapInfo.updateFrom(blurredTex);
await CoreSleep.sleep(20);
}
if (_restartIfRequired()) {
return;
}
// update matrices
scene.updateMatrixWorld(true);
prepareScene(scene);
const generateInThread = () => {
this._generator = this._generator || new PathTracingSceneGenerator();
return this._generator.generate(scene);
};
const generateInWorker = async () => {
this._workerGenerator = this._workerGenerator || new PathTracingSceneWorker();
const result = await this._workerGenerator.generate(scene, {
onProgress: (progress: number) => {},
});
return result;
};
let result: GeneratorResult | undefined;
try {
result = this._useWorker ? await generateInWorker() : generateInThread();
} catch (err) {
console.error(err);
this._generating = false;
return;
}
restoreScene(scene);
await CoreSleep.sleep(20);
if (_restartIfRequired()) {
return;
}
const {bvh, textures, materials, lights} = result;
const geometry = bvh.geometry;
const material = this.pathTracingRenderer.material;
material.bvh.updateFrom(bvh);
material.attributesArray.updateFrom(
geometry.attributes.normal,
geometry.attributes.tangent,
geometry.attributes.uv,
geometry.attributes.color
);
material.materialIndexAttribute.updateFrom(geometry.attributes.materialIndex);
material.textures.setTextures(this.webGLRenderer, 1024, 1024, textures);
material.materials.updateFrom(materials, textures);
// lights
const iesTextures = lights.map((light) => light.iesTexture);
material.iesProfiles.updateFrom(this.webGLRenderer, iesTextures);
material.lights.updateFrom(lights, iesTextures);
await CoreSleep.sleep(20);
if (_restartIfRequired()) {
return;
}
this._generating = false;
this._generated = true;
this.reset();
console.log('GENERATOR DONE', performance.now() - timeStart);
}
reset() {
if (!this._generated) {
return;
}
//
// in order to avoid too frequent .reset() calls,
// make sure to toggle damping off on the camera orbit controls
//
this.pathTracingRenderer.reset();
}
private _previousCameraProjectionMatrix: Matrix4 = new Matrix4();
private _previousCameraWorldMatrix: Matrix4 = new Matrix4();
private _resetIfCameraUpdated(camera: Camera) {
if (
this._previousCameraProjectionMatrix.equals(camera.projectionMatrix) &&
this._previousCameraWorldMatrix.equals(camera.matrixWorld)
) {
return;
}
this._previousCameraProjectionMatrix.copy(camera.projectionMatrix);
this._previousCameraWorldMatrix.copy(camera.matrixWorld);
this.reset();
}
}
const previousParent: Map<Object3D, Object3D> = new Map();
function prepareScene(scene: Scene) {
function _saveAndSetObjectState(object: Object3D) {
if (!object.parent) {
return;
}
previousParent.set(object, object.parent);
object.parent.remove(object);
}
function _isValidMaterial(material: Material) {
if (material instanceof LineBasicMaterial) {
return false;
}
if (material instanceof MeshBasicMaterial) {
return false;
}
return true;
}
function _isValidObject(object: Object3D) {
if (object instanceof Line) {
return false;
}
const material = (object as Mesh).material;
if (isArray(material)) {
for (const mat of material) {
if (!_isValidMaterial(mat)) {
return false;
}
}
} else {
if (!_isValidMaterial(material)) {
return false;
}
}
return true;
}
const objectsToRemoveFromHierarchy: Object3D[] = [];
scene.traverse((object) => {
if (!_isValidObject(object)) {
objectsToRemoveFromHierarchy.push(object);
}
});
for (const object of objectsToRemoveFromHierarchy) {
_saveAndSetObjectState(object);
}
}
function restoreScene(scene: Scene) {
previousParent.forEach((parent, object) => {
parent.add(object);
});
previousParent.clear();
}