threepipe
Version:
A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.
336 lines • 14.8 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Matrix4, UnsignedByteType, Vector2, Vector3, Vector4 } from 'three';
import { ExtendedShaderPass } from '../../postprocessing';
import { PipelinePassPlugin } from '../base/PipelinePassPlugin';
import { uiConfig, uiFolderContainer, uiImage, uiSlider } from 'uiconfig.js';
import { getOrCall, glsl, onChange2, serialize, updateBit } from 'ts-browser-helpers';
import { shaderReplaceString, shaderUtils } from '../../utils';
import { getTexelDecoding, matDefine, matDefineBool } from '../../three';
import ssaoPass from './shaders/SSAOPlugin.pass.glsl';
import ssaoPatch from './shaders/SSAOPlugin.patch.glsl';
import { uiConfigMaterialExtension } from '../../materials/MaterialExtender';
import { GBufferPlugin } from './GBufferPlugin';
/**
* SSAO Plugin
*
* Adds Screen Space Ambient Occlusion (SSAO) to the scene.
* Adds a pass to calculate AO, which is then read by materials in the render pass.
* @category Plugins
*/
let SSAOPlugin = class SSAOPlugin extends PipelinePassPlugin {
constructor(bufferType = UnsignedByteType, sizeMultiplier = 1, enabled = true) {
super();
this.passId = 'ssao';
this.dependencies = [GBufferPlugin];
this._gbufferUnpackExtension = undefined;
this._gbufferUnpackExtensionChanged = () => {
if (!this._pass || !this._viewer)
throw new Error('SSAOPlugin: pass/viewer not created yet');
const newExtension = this._viewer.renderManager.gbufferUnpackExtension;
if (this._gbufferUnpackExtension === newExtension)
return;
if (this._gbufferUnpackExtension)
this._pass.material.unregisterMaterialExtensions([this._gbufferUnpackExtension]);
this._gbufferUnpackExtension = newExtension;
if (this._gbufferUnpackExtension)
this._pass.material.registerMaterialExtensions([this._gbufferUnpackExtension]);
else
this._viewer.console.warn('SSAOPlugin: GBuffer unpack extension removed');
};
this.enabled = enabled;
this.bufferType = bufferType;
this.sizeMultiplier = sizeMultiplier;
}
_createTarget(recreate = true) {
if (!this._viewer)
return;
if (recreate)
this._disposeTarget();
if (!this.target)
this.target = this._viewer.renderManager.createTarget({
depthBuffer: false,
type: this.bufferType,
sizeMultiplier: this.sizeMultiplier,
// magFilter: NearestFilter,
// minFilter: NearestFilter,
// generateMipmaps: false,
// encoding: LinearEncoding,
});
this.texture = this.target.texture;
this.texture.name = 'ssaoBuffer';
// if (this._pass) this._pass.target = this.target
}
_disposeTarget() {
if (!this._viewer)
return;
if (this.target) {
this._viewer.renderManager.disposeTarget(this.target);
this.target = undefined;
}
this.texture = undefined;
}
_createPass() {
if (!this._viewer)
throw new Error('SSAOPlugin: viewer not set');
if (!this._viewer.renderManager.gbufferTarget || !this._viewer.renderManager.gbufferUnpackExtension)
throw new Error('SSAOPlugin: GBuffer target not created. GBufferPlugin or DepthBufferPlugin is required.');
this._createTarget(true);
return new SSAOPluginPass(this.passId, () => this.target);
}
onAdded(viewer) {
super.onAdded(viewer);
viewer.forPlugin(GBufferPlugin, (gbuffer) => {
gbuffer.registerGBufferUpdater(this.constructor.PluginType, this.updateGBufferFlags.bind(this));
}, (gbuffer) => {
gbuffer.unregisterGBufferUpdater(this.constructor.PluginType);
});
this._gbufferUnpackExtensionChanged();
viewer.renderManager.addEventListener('gbufferUnpackExtensionChanged', this._gbufferUnpackExtensionChanged);
}
onRemove(viewer) {
this._disposeTarget();
return super.onRemove(viewer);
}
updateGBufferFlags(data, c) {
if (!c.material || !c.material.userData)
return;
const disabled = c.material.userData.ssaoCastDisabled || c.material.userData.pluginsDisabled;
const x = disabled ? 0 : 1;
data.w = updateBit(data.w, 3, x);
if (disabled && this._pass)
this._pass.checkGBufferFlag = true;
}
/**
* @deprecated use {@link target} instead
*/
get aoTarget() {
console.warn('SSAOPlugin: aoTarget is deprecated, use target instead');
return this.target;
}
};
SSAOPlugin.PluginType = 'SSAOPlugin';
__decorate([
uiImage('SSAO Buffer' /* {readOnly: true}*/)
], SSAOPlugin.prototype, "texture", void 0);
__decorate([
uiConfig()
], SSAOPlugin.prototype, "_pass", void 0);
SSAOPlugin = __decorate([
uiFolderContainer('SSAO Plugin')
], SSAOPlugin);
export { SSAOPlugin };
let SSAOPluginPass = class SSAOPluginPass extends ExtendedShaderPass {
// todo after bilateralPass is implemented
// @bindToValue({obj: 'bilateralPass', key: 'enabled', onChange: 'setDirty'})
// smoothEnabled = true
// todo after bilateralPass is implemented
// @bindToValue({obj: 'bilateralPass', key: 'enabled', onChange: 'setDirty'})
// smoothEdgeSharpness = true
constructor(passId, target) {
super({
defines: {
['LINEAR_DEPTH']: 1, // todo set from unpack extension
['NUM_SAMPLES']: 11,
['NUM_SPIRAL_TURNS']: 3,
['SSAO_PACKING']: 1, // 1 is (r: ssao, gba: depth), 2 is (rgb: ssao, a: 1), 3 is (rgba: packed_ssao), 4 is (rgb: packed_ssao, a: 1)
['PERSPECTIVE_CAMERA']: 1, // set in PerspectiveCamera2
['CHECK_GBUFFER_FLAG']: 0,
},
uniforms: {
tLastThis: { value: null },
screenSize: { value: new Vector2(0, 0) }, // set in ExtendedRenderMaterial
saoData: { value: new Vector4() },
frameCount: { value: 0 }, // set in RenderManager
cameraNearFar: { value: new Vector2(0.1, 1000) }, // set in PerspectiveCamera2
projection: { value: new Matrix4() }, // set in PerspectiveCamera2
saoBiasEpsilon: { value: new Vector3(1, 1, 1) },
},
vertexShader: shaderUtils.defaultVertex,
fragmentShader: ssaoPass,
}, 'tDiffuse'); // why is tLastThis not here. because encoding and size doesnt matter?
this.passId = passId;
this.target = target;
this.before = ['render'];
this.after = ['gbuffer', 'depth'];
this.required = ['render']; // gbuffer required check done in plugin.
// todo bilateralPass
// @serialize() readonly bilateralPass: BilateralFilterPass
// todo old deserialize
// @serialize() readonly parameters: SSAOParams = {
// intensity: 0.25,
// occlusionWorldRadius: 1,
// bias: 0.001,
// falloff: 1.3,
// }
this.intensity = 0.25;
this.occlusionWorldRadius = 1;
this.bias = 0.001;
this.falloff = 1.3;
this.numSamples = 8;
/**
* Whether to check for gbuffer flag or not. This is used to disable SSAO casting by some objects. its enabled automatically by the SSAOPlugin when required.
* This is disabled by default so that we dont read texture for no reason.
*/
this.checkGBufferFlag = false;
this.materialExtension = {
extraUniforms: {
tSSAOMap: () => ({ value: getOrCall(this.target)?.texture ?? null }),
},
shaderExtender: (shader, _material, _renderer) => {
if (!shader.defines.SSAO_ENABLED)
return;
// todo: only SSAO_PACKING = 1 and 2 is supported. Not 3 and 4 right now.
shader.fragmentShader = shaderReplaceString(shader.fragmentShader, '#include <aomap_fragment>', ssaoPatch);
},
onObjectRender: (_object, material, renderer) => {
// const opaque = !material.transparent && (!material.transmission || material.transmission < 0.001)
const x = this.enabled && // opaque &&
renderer.userData.screenSpaceRendering !== false &&
!material.userData?.pluginsDisabled &&
!material.userData?.ssaoDisabled ? 1 : 0;
if (material.defines.SSAO_ENABLED !== x) {
material.defines.SSAO_ENABLED = x;
material.needsUpdate = true;
}
},
parsFragmentSnippet: (renderer) => glsl `
uniform sampler2D tSSAOMap;
${getTexelDecoding('tSSAOMap', getOrCall(this.target)?.texture, renderer.capabilities.isWebGL2)}
#include <simpleCameraHelpers>
`,
computeCacheKey: () => {
return (this.enabled ? '1' : '0') + getOrCall(this.target)?.texture?.colorSpace;
},
uuid: SSAOPlugin.PluginType,
...uiConfigMaterialExtension(this._getUiConfig.bind(this), SSAOPlugin.PluginType),
isCompatible: material => {
return material.isPhysicalMaterial;
},
};
this.needsSwap = false;
this.clear = true;
// this.bilateralPass = new BilateralFilterPass(this._target as any, gBufferUnpack, 'rrrr')
// this._multiplyPass = new GenericBlendTexturePass(this._target.texture as any, 'c = vec4((1.0-b.r) * a.xyz, a.a);')
// this._getUiConfig = this._getUiConfig.bind(this)
}
render(renderer, writeBuffer, readBuffer, deltaTime, maskActive) {
if (!this.enabled)
return;
const target = getOrCall(this.target);
if (!target) {
console.warn('SSAOPluginPass: target not defined');
return;
}
this._updateParameters();
// if (!this.material.defines.HAS_GBUFFER) {
// console.warn('SSAOPluginPass: DepthNormalBuffer required for ssao')
// }
renderer.renderManager.blit(writeBuffer, {
source: target.texture,
});
this.uniforms.tLastThis.value = writeBuffer.texture;
super.render(renderer, target, readBuffer, deltaTime, maskActive);
// todo
// if (this.smoothEnabled) {
// this.bilateralPass.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive)
// }
}
_updateParameters() {
// const projectionScale = 1 / (Math.tan(DEG2RAD * (camera as any).fov / 2) * 2);
const saoData = this.material.uniforms.saoData.value;
// saoData.x = projectionScale;
saoData.y = this.intensity;
saoData.z = this.occlusionWorldRadius;
// saoData.w = this.accIndex_++;
const saoBiasEpsilon = this.material.uniforms.saoBiasEpsilon.value;
saoBiasEpsilon.x = this.bias;
saoBiasEpsilon.y = 0.001;
saoBiasEpsilon.z = this.falloff;
// this.material.uniforms.size.value.set(this._target.texture.image?.width, this._target.texture.image?.height)
}
beforeRender(_, camera, renderManager) {
if (!this.enabled)
return;
this.updateShaderProperties([camera, renderManager]);
}
/**
* Returns a uiConfig to toggle SSAO on a material.
* This uiConfig is added to each material by extension
* @param material
* @private
*/
_getUiConfig(material) {
return {
type: 'folder',
label: 'SSAO',
children: [
{
type: 'checkbox',
label: 'Enabled',
get value() {
return !(material.userData.ssaoDisabled ?? false);
},
set value(v) {
if (v === !(material.userData.ssaoDisabled ?? false))
return;
material.userData.ssaoDisabled = !v;
material.setDirty();
},
onChange: this.setDirty,
},
{
type: 'checkbox',
label: 'Cast SSAO',
get value() {
return !(material.userData.ssaoCastDisabled ?? false);
},
set value(v) {
if (v === !(material.userData.ssaoCastDisabled ?? false))
return;
material.userData.ssaoCastDisabled = !v;
material.setDirty();
},
onChange: this.setDirty,
},
],
};
}
};
__decorate([
serialize(),
uiSlider('Intensity', [0, 4], 0.01),
onChange2(SSAOPluginPass.prototype.setDirty)
], SSAOPluginPass.prototype, "intensity", void 0);
__decorate([
serialize(),
uiSlider('Occlusion World Radius', [0.1, 8], 0.01),
onChange2(SSAOPluginPass.prototype.setDirty)
], SSAOPluginPass.prototype, "occlusionWorldRadius", void 0);
__decorate([
serialize(),
uiSlider('Bias', [0.00001, 0.01], 0.00001),
onChange2(SSAOPluginPass.prototype.setDirty)
], SSAOPluginPass.prototype, "bias", void 0);
__decorate([
serialize(),
uiSlider('Falloff', [0.01, 3], 0.01),
onChange2(SSAOPluginPass.prototype.setDirty)
], SSAOPluginPass.prototype, "falloff", void 0);
__decorate([
serialize(),
uiSlider('Num Samples', [1, 11], 1),
matDefine('NUM_SAMPLES', undefined, undefined, SSAOPluginPass.prototype.setDirty)
], SSAOPluginPass.prototype, "numSamples", void 0);
__decorate([
matDefineBool('CHECK_GBUFFER_FLAG')
], SSAOPluginPass.prototype, "checkGBufferFlag", void 0);
SSAOPluginPass = __decorate([
uiFolderContainer('SSAO Pass')
], SSAOPluginPass);
export { SSAOPluginPass };
//# sourceMappingURL=SSAOPlugin.js.map