module BABYLON { export class HDRRenderingPipeline extends PostProcessRenderPipeline { /** * Public members */ // Gaussian Blur /** * Gaussian blur coefficient * @type {number} */ public gaussCoeff: number = 0.3; /** * Gaussian blur mean * @type {number} */ public gaussMean: number = 1.0; /** * Gaussian blur standard derivation * @type {number} */ public gaussStandDev: number = 0.8; // HDR /** * Exposure, controls the overall intensity of the pipeline * @type {number} */ public exposure: number = 1.0; /** * Minimum luminance that the post-process can output. Luminance is >= 0 * @type {number} */ public minimumLuminance: number = 1.0; /** * Maximum luminance that the post-process can output. Must be suprerior to minimumLuminance * @type {number} */ public maximumLuminance: number = 1e20; /** * Increase rate for luminance: eye adaptation speed to bright * @type {number} */ public luminanceIncreaserate: number = 0.5; /** * Decrease rate for luminance: eye adaptation speed to dark * @type {number} */ public luminanceDecreaseRate: number = 0.5; // Bright pass /** * Minimum luminance needed to compute HDR * @type {number} */ public brightThreshold: number = 0.8; /** * Private members */ // Gaussian blur private _guassianBlurHPostProcess: PostProcess; private _guassianBlurVPostProcess: PostProcess; // Bright pass private _brightPassPostProcess: PostProcess; // Texture adder private _textureAdderPostProcess: PostProcess; // Down Sampling private _downSampleX4PostProcess: PostProcess; // Original Post-process private _originalPostProcess: PostProcess; // HDR private _hdrPostProcess: PostProcess; private _hdrCurrentLuminance: number; private _hdrOutputLuminance: number; // Luminance generator public static LUM_STEPS: number = 6; private _downSamplePostProcesses: Array; // Global private _needUpdate: boolean = true; /** * @constructor * @param {string} name - The rendering pipeline name * @param {BABYLON.Scene} scene - The scene linked to this pipeline * @param {any} ratio - The size of the postprocesses (0.5 means that your postprocess will have a width = canvas.width 0.5 and a height = canvas.height 0.5) * @param {BABYLON.PostProcess} originalPostProcess - the custom original color post-process. Must be "reusable". Can be null. * @param {BABYLON.Camera[]} cameras - The array of cameras that the rendering pipeline will be attached to */ constructor(name: string, scene: Scene, ratio: number, originalPostProcess: PostProcess = null, cameras?: Camera[]) { super(scene.getEngine(), name); // Bright pass this._createBrightPassPostProcess(scene, ratio); // Down sample X4 this._createDownSampleX4PostProcess(scene, ratio); // Create gaussian blur post-processes this._createGaussianBlurPostProcess(scene, ratio); // Texture adder this._createTextureAdderPostProcess(scene, ratio); // Luminance generator this._createLuminanceGeneratorPostProcess(scene); // HDR this._createHDRPostProcess(scene, ratio); // Pass postprocess if (originalPostProcess === null) { this._originalPostProcess = new PassPostProcess("hdr", ratio, null, Texture.BILINEAR_SAMPLINGMODE, scene.getEngine(), false); } else { this._originalPostProcess = originalPostProcess; } // Configure pipeline this.addEffect(new PostProcessRenderEffect(scene.getEngine(), "HDRPassPostProcess", () => { return this._originalPostProcess; }, true)); this.addEffect(new PostProcessRenderEffect(scene.getEngine(), "HDRBrightPass", () => { return this._brightPassPostProcess; }, true)); this.addEffect(new PostProcessRenderEffect(scene.getEngine(), "HDRDownSampleX4", () => { return this._downSampleX4PostProcess; }, true)); this.addEffect(new PostProcessRenderEffect(scene.getEngine(), "HDRGaussianBlurH", () => { return this._guassianBlurHPostProcess; }, true)); this.addEffect(new PostProcessRenderEffect(scene.getEngine(), "HDRGaussianBlurV", () => { return this._guassianBlurVPostProcess; }, true)); this.addEffect(new PostProcessRenderEffect(scene.getEngine(), "HDRTextureAdder", () => { return this._textureAdderPostProcess; }, true)); var addDownSamplerPostProcess = (id: number) => { this.addEffect(new PostProcessRenderEffect(scene.getEngine(), "HDRDownSampler" + id, () => { return this._downSamplePostProcesses[id]; }, true)); }; for (var i = HDRRenderingPipeline.LUM_STEPS - 1; i >= 0; i--) { addDownSamplerPostProcess(i); } this.addEffect(new PostProcessRenderEffect(scene.getEngine(), "HDR", () => { return this._hdrPostProcess; }, true)); // Finish scene.postProcessRenderPipelineManager.addPipeline(this); this.update(); } /** * Tells the pipeline to update its post-processes */ public update(): void { this._needUpdate = true; } /** * Returns the current calculated luminance */ public getCurrentLuminance(): number { return this._hdrCurrentLuminance; } /** * Returns the currently drawn luminance */ public getOutputLuminance(): number { return this._hdrOutputLuminance; } /** * Creates the HDR post-process and computes the luminance adaptation */ private _createHDRPostProcess(scene: Scene, ratio: number): void { var hdrLastLuminance = 0.0; this._hdrOutputLuminance = -1.0; this._hdrCurrentLuminance = 1.0; this._hdrPostProcess = new PostProcess("hdr", "hdr", ["exposure", "avgLuminance"], ["otherSampler"], ratio, null, Texture.BILINEAR_SAMPLINGMODE, scene.getEngine(), false, "#define HDR"); this._hdrPostProcess.onApply = (effect: Effect) => { if (this._hdrOutputLuminance < 0.0) { this._hdrOutputLuminance = this._hdrCurrentLuminance; } else { var dt = (hdrLastLuminance - (hdrLastLuminance + scene.getEngine().getDeltaTime())) / 1000.0; if (this._hdrCurrentLuminance < this._hdrOutputLuminance + this.luminanceDecreaseRate * dt) { this._hdrOutputLuminance += this.luminanceDecreaseRate * dt; } else if (this._hdrCurrentLuminance > this._hdrOutputLuminance - this.luminanceIncreaserate * dt) { this._hdrOutputLuminance -= this.luminanceIncreaserate * dt; } else { this._hdrOutputLuminance = this._hdrCurrentLuminance; } } this._hdrOutputLuminance = Tools.Clamp(this._hdrOutputLuminance, this.minimumLuminance, this.maximumLuminance); hdrLastLuminance += scene.getEngine().getDeltaTime(); effect.setTextureFromPostProcess("textureSampler", this._originalPostProcess); effect.setTextureFromPostProcess("otherSampler", this._textureAdderPostProcess); effect.setFloat("exposure", this.exposure); effect.setFloat("avgLuminance", this._hdrOutputLuminance); this._needUpdate = false; }; } /** * Texture Adder post-process */ private _createTextureAdderPostProcess(scene: Scene, ratio: number): void { this._textureAdderPostProcess = new PostProcess("hdr", "hdr", [], ["otherSampler"], ratio, null, Texture.BILINEAR_SAMPLINGMODE, scene.getEngine(), false, "#define TEXTURE_ADDER"); this._textureAdderPostProcess.onApply = (effect: Effect) => { effect.setTextureFromPostProcess("otherSampler", this._originalPostProcess); }; } /** * Down sample X4 post-process */ private _createDownSampleX4PostProcess(scene: Scene, ratio: number): void { var downSampleX4Offsets = new Array(32); this._downSampleX4PostProcess = new PostProcess("hdr", "hdr", ["dsOffsets"], [], ratio / 4, null, Texture.BILINEAR_SAMPLINGMODE, scene.getEngine(), false, "#define DOWN_SAMPLE_X4"); this._downSampleX4PostProcess.onApply = (effect: Effect) => { if (this._needUpdate) { var id = 0; for (var i = -2; i < 2; i++) { for (var j = -2; j < 2; j++) { downSampleX4Offsets[id] = (i + 0.5) * (1.0 / this._downSampleX4PostProcess.width); downSampleX4Offsets[id + 1] = (j + 0.5) * (1.0 / this._downSampleX4PostProcess.height); id += 2; } } } effect.setArray2("dsOffsets", downSampleX4Offsets); }; } /** * Bright pass post-process */ private _createBrightPassPostProcess(scene: Scene, ratio: number): void { var brightOffsets = new Array(8); var brightPassCallback = (effect: Effect) => { if (this._needUpdate) { var sU = (1.0 / this._brightPassPostProcess.width); var sV = (1.0 / this._brightPassPostProcess.height); brightOffsets[0] = -0.5 * sU; brightOffsets[1] = 0.5 * sV; brightOffsets[2] = 0.5 * sU; brightOffsets[3] = 0.5 * sV; brightOffsets[4] = -0.5 * sU; brightOffsets[5] = -0.5 * sV; brightOffsets[6] = 0.5 * sU; brightOffsets[7] = -0.5 * sV; } effect.setArray2("dsOffsets", brightOffsets); effect.setFloat("brightThreshold", this.brightThreshold); }; this._brightPassPostProcess = new PostProcess("hdr", "hdr", ["dsOffsets", "brightThreshold"], [], ratio, null, Texture.BILINEAR_SAMPLINGMODE, scene.getEngine(), false, "#define BRIGHT_PASS"); this._brightPassPostProcess.onApply = brightPassCallback; } /** * Luminance generator. Creates the luminance post-process and down sample post-processes */ private _createLuminanceGeneratorPostProcess(scene: Scene): void { var lumSteps: number = HDRRenderingPipeline.LUM_STEPS; var luminanceOffsets = new Array(8); var downSampleOffsets = new Array(18); var halfDestPixelSize: number; this._downSamplePostProcesses = new Array(lumSteps); // Utils for luminance var luminanceUpdateSourceOffsets = (width: number, height: number) => { var sU = (1.0 / width); var sV = (1.0 / height); luminanceOffsets[0] = -0.5 * sU; luminanceOffsets[1] = 0.5 * sV; luminanceOffsets[2] = 0.5 * sU; luminanceOffsets[3] = 0.5 * sV; luminanceOffsets[4] = -0.5 * sU; luminanceOffsets[5] = -0.5 * sV; luminanceOffsets[6] = 0.5 * sU; luminanceOffsets[7] = -0.5 * sV; }; var luminanceUpdateDestOffsets = (width: number, height: number) => { var id = 0; for (var x = -1; x < 2; x++) { for (var y = -1; y < 2; y++) { downSampleOffsets[id] = (x) / width; downSampleOffsets[id + 1] = (y) / height; id += 2; } } }; // Luminance callback var luminanceCallback = (effect: Effect) => { if (this._needUpdate) { luminanceUpdateSourceOffsets(this._textureAdderPostProcess.width, this._textureAdderPostProcess.height); } effect.setTextureFromPostProcess("textureSampler", this._textureAdderPostProcess); effect.setArray2("lumOffsets", luminanceOffsets); } // Down sample callbacks var downSampleCallback = (indice: number) => { var i = indice; return (effect: Effect) => { luminanceUpdateSourceOffsets(this._downSamplePostProcesses[i].width, this._downSamplePostProcesses[i].height); luminanceUpdateDestOffsets(this._downSamplePostProcesses[i].width, this._downSamplePostProcesses[i].height); halfDestPixelSize = 0.5 / this._downSamplePostProcesses[i].width; effect.setTextureFromPostProcess("textureSampler", this._downSamplePostProcesses[i + 1]); effect.setFloat("halfDestPixelSize", halfDestPixelSize); effect.setArray2("dsOffsets", downSampleOffsets); } }; var downSampleAfterRenderCallback = (effect: Effect) => { // Unpack result var pixel = scene.getEngine().readPixels(0, 0, 1, 1); var bit_shift = new Vector4(1.0 / (255.0 * 255.0 * 255.0), 1.0 / (255.0 * 255.0), 1.0 / 255.0, 1.0); this._hdrCurrentLuminance = (pixel[0] * bit_shift.x + pixel[1] * bit_shift.y + pixel[2] * bit_shift.z + pixel[3] * bit_shift.w) / 100.0; }; // Create luminance post-process var ratio = { width: Math.pow(3, lumSteps - 1), height: Math.pow(3, lumSteps - 1) }; this._downSamplePostProcesses[lumSteps - 1] = new PostProcess("hdr", "hdr", ["lumOffsets"], [], ratio, null, Texture.NEAREST_SAMPLINGMODE, scene.getEngine(), false, "#define LUMINANCE_GENERATOR", Engine.TEXTURETYPE_FLOAT); this._downSamplePostProcesses[lumSteps - 1].onApply = luminanceCallback; // Create down sample post-processes for (var i = lumSteps - 2; i >= 0; i--) { var length = Math.pow(3, i); ratio = { width: length, height: length }; var defines = "#define DOWN_SAMPLE\n"; if (i === 0) { defines += "#define FINAL_DOWN_SAMPLE\n"; // To pack the result } this._downSamplePostProcesses[i] = new PostProcess("hdr", "hdr", ["dsOffsets", "halfDestPixelSize"], [], ratio, null, Texture.NEAREST_SAMPLINGMODE, scene.getEngine(), false, defines, Engine.TEXTURETYPE_FLOAT); this._downSamplePostProcesses[i].onApply = downSampleCallback(i); if (i === 0) { this._downSamplePostProcesses[i].onAfterRender = downSampleAfterRenderCallback; } } } /** * Gaussian blur post-processes. Horizontal and Vertical */ private _createGaussianBlurPostProcess(scene: Scene, ratio: number): void { var blurOffsetsW = new Array(9); var blurOffsetsH = new Array(9); var blurWeights = new Array(9); var uniforms: string[] = ["blurOffsets", "blurWeights"]; // Utils for gaussian blur var calculateBlurOffsets = (height: boolean) => { var lastOutputDimensions: any = { width: scene.getEngine().getRenderWidth() * (ratio / 4), height: scene.getEngine().getRenderHeight() * (ratio / 4) }; for (var i = 0; i < 9; i++) { var value = (i - 4.0) * (1.0 / (height === true ? lastOutputDimensions.height : lastOutputDimensions.width)); if (height) { blurOffsetsH[i] = value; } else { blurOffsetsW[i] = value; } } }; var calculateWeights = () => { var x: number = 0.0; for (var i = 0; i < 9; i++) { x = (i - 4.0) / 4.0; blurWeights[i] = this.gaussCoeff * (1.0 / Math.sqrt(2.0 * Math.PI * this.gaussStandDev * this.gaussStandDev)) * Math.exp((-((x - this.gaussMean) * (x - this.gaussMean))) / (2.0 * this.gaussStandDev * this.gaussStandDev)); } } // Callback var gaussianBlurCallback = (height: boolean) => { return (effect: Effect) => { if (this._needUpdate) { calculateWeights(); calculateBlurOffsets(height); } effect.setArray("blurOffsets", height ? blurOffsetsH : blurOffsetsW); effect.setArray("blurWeights", blurWeights); }; }; // Create horizontal gaussian blur post-processes this._guassianBlurHPostProcess = new PostProcess("hdr", "hdr", uniforms, [], ratio / 4, null, Texture.BILINEAR_SAMPLINGMODE, scene.getEngine(), false, "#define GAUSSIAN_BLUR_H"); this._guassianBlurHPostProcess.onApply = gaussianBlurCallback(false); // Create vertical gaussian blur post-process this._guassianBlurVPostProcess = new PostProcess("hdr", "hdr", uniforms, [], ratio / 4, null, Texture.BILINEAR_SAMPLINGMODE, scene.getEngine(), false, "#define GAUSSIAN_BLUR_V"); this._guassianBlurVPostProcess.onApply = gaussianBlurCallback(true); } } }