import Cartesian2 from "../Core/Cartesian2.js";
import Color from "../Core/Color.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import ClearCommand from "../Renderer/ClearCommand.js";
import FramebufferManager from "../Renderer/FramebufferManager.js";
import PixelDatatype from "../Renderer/PixelDatatype.js";
/**
* A post process stage that will get the luminance value at each pixel and
* uses parallel reduction to compute the average luminance in a 1x1 texture.
* This texture can be used as input for tone mapping.
*
* @constructor
* @private
*/
function AutoExposure() {
this._uniformMap = undefined;
this._command = undefined;
this._colorTexture = undefined;
this._depthTexture = undefined;
this._ready = false;
this._name = "czm_autoexposure";
this._logDepthChanged = undefined;
this._useLogDepth = undefined;
this._framebuffers = undefined;
this._previousLuminance = new FramebufferManager();
this._commands = undefined;
this._clearCommand = undefined;
this._minMaxLuminance = new Cartesian2();
/**
* Whether or not to execute this post-process stage when ready.
*
* @type {Boolean}
*/
this.enabled = true;
this._enabled = true;
/**
* The minimum value used to clamp the luminance.
*
* @type {Number}
* @default 0.1
*/
this.minimumLuminance = 0.1;
/**
* The maximum value used to clamp the luminance.
*
* @type {Number}
* @default 10.0
*/
this.maximumLuminance = 10.0;
}
Object.defineProperties(AutoExposure.prototype, {
/**
* Determines if this post-process stage is ready to be executed. A stage is only executed when both ready
* and {@link AutoExposure#enabled} are true
. A stage will not be ready while it is waiting on textures
* to load.
*
* @memberof AutoExposure.prototype
* @type {Boolean}
* @readonly
*/
ready: {
get: function () {
return this._ready;
},
},
/**
* The unique name of this post-process stage for reference by other stages.
*
* @memberof AutoExposure.prototype
* @type {String}
* @readonly
*/
name: {
get: function () {
return this._name;
},
},
/**
* A reference to the texture written to when executing this post process stage.
*
* @memberof AutoExposure.prototype
* @type {Texture}
* @readonly
* @private
*/
outputTexture: {
get: function () {
const framebuffers = this._framebuffers;
if (!defined(framebuffers)) {
return undefined;
}
return framebuffers[framebuffers.length - 1].getColorTexture(0);
},
},
});
function destroyFramebuffers(autoexposure) {
const framebuffers = autoexposure._framebuffers;
if (!defined(framebuffers)) {
return;
}
const length = framebuffers.length;
for (let i = 0; i < length; ++i) {
framebuffers[i].destroy();
}
autoexposure._framebuffers = undefined;
autoexposure._previousLuminance.destroy();
autoexposure._previousLuminance = undefined;
}
function createFramebuffers(autoexposure, context) {
destroyFramebuffers(autoexposure);
let width = autoexposure._width;
let height = autoexposure._height;
const pixelDatatype = context.halfFloatingPointTexture
? PixelDatatype.HALF_FLOAT
: PixelDatatype.FLOAT;
const length = Math.ceil(Math.log(Math.max(width, height)) / Math.log(3.0));
const framebuffers = new Array(length);
for (let i = 0; i < length; ++i) {
width = Math.max(Math.ceil(width / 3.0), 1.0);
height = Math.max(Math.ceil(height / 3.0), 1.0);
framebuffers[i] = new FramebufferManager();
framebuffers[i].update(context, width, height, 1, pixelDatatype);
}
const lastTexture = framebuffers[length - 1].getColorTexture(0);
autoexposure._previousLuminance.update(
context,
lastTexture.width,
lastTexture.height,
1,
pixelDatatype
);
autoexposure._framebuffers = framebuffers;
}
function destroyCommands(autoexposure) {
const commands = autoexposure._commands;
if (!defined(commands)) {
return;
}
const length = commands.length;
for (let i = 0; i < length; ++i) {
commands[i].shaderProgram.destroy();
}
autoexposure._commands = undefined;
}
function createUniformMap(autoexposure, index) {
let uniforms;
if (index === 0) {
uniforms = {
colorTexture: function () {
return autoexposure._colorTexture;
},
colorTextureDimensions: function () {
return autoexposure._colorTexture.dimensions;
},
};
} else {
const texture = autoexposure._framebuffers[index - 1].getColorTexture(0);
uniforms = {
colorTexture: function () {
return texture;
},
colorTextureDimensions: function () {
return texture.dimensions;
},
};
}
uniforms.minMaxLuminance = function () {
return autoexposure._minMaxLuminance;
};
uniforms.previousLuminance = function () {
return autoexposure._previousLuminance.getColorTexture(0);
};
return uniforms;
}
function getShaderSource(index, length) {
let source =
"uniform sampler2D colorTexture; \n" +
"varying vec2 v_textureCoordinates; \n" +
"float sampleTexture(vec2 offset) { \n";
if (index === 0) {
source +=
" vec4 color = texture2D(colorTexture, v_textureCoordinates + offset); \n" +
" return czm_luminance(color.rgb); \n";
} else {
source +=
" return texture2D(colorTexture, v_textureCoordinates + offset).r; \n";
}
source += "}\n\n";
source +=
"uniform vec2 colorTextureDimensions; \n" +
"uniform vec2 minMaxLuminance; \n" +
"uniform sampler2D previousLuminance; \n" +
"void main() { \n" +
" float color = 0.0; \n" +
" float xStep = 1.0 / colorTextureDimensions.x; \n" +
" float yStep = 1.0 / colorTextureDimensions.y; \n" +
" int count = 0; \n" +
" for (int i = 0; i < 3; ++i) { \n" +
" for (int j = 0; j < 3; ++j) { \n" +
" vec2 offset; \n" +
" offset.x = -xStep + float(i) * xStep; \n" +
" offset.y = -yStep + float(j) * yStep; \n" +
" if (offset.x < 0.0 || offset.x > 1.0 || offset.y < 0.0 || offset.y > 1.0) { \n" +
" continue; \n" +
" } \n" +
" color += sampleTexture(offset); \n" +
" ++count; \n" +
" } \n" +
" } \n" +
" if (count > 0) { \n" +
" color /= float(count); \n" +
" } \n";
if (index === length - 1) {
source +=
" float previous = texture2D(previousLuminance, vec2(0.5)).r; \n" +
" color = clamp(color, minMaxLuminance.x, minMaxLuminance.y); \n" +
" color = previous + (color - previous) / (60.0 * 1.5); \n" +
" color = clamp(color, minMaxLuminance.x, minMaxLuminance.y); \n";
}
source += " gl_FragColor = vec4(color); \n" + "} \n";
return source;
}
function createCommands(autoexposure, context) {
destroyCommands(autoexposure);
const framebuffers = autoexposure._framebuffers;
const length = framebuffers.length;
const commands = new Array(length);
for (let i = 0; i < length; ++i) {
commands[i] = context.createViewportQuadCommand(
getShaderSource(i, length),
{
framebuffer: framebuffers[i].framebuffer,
uniformMap: createUniformMap(autoexposure, i),
}
);
}
autoexposure._commands = commands;
}
/**
* A function that will be called before execute. Used to clear any textures attached to framebuffers.
* @param {Context} context The context.
* @private
*/
AutoExposure.prototype.clear = function (context) {
const framebuffers = this._framebuffers;
if (!defined(framebuffers)) {
return;
}
let clearCommand = this._clearCommand;
if (!defined(clearCommand)) {
clearCommand = this._clearCommand = new ClearCommand({
color: new Color(0.0, 0.0, 0.0, 0.0),
framebuffer: undefined,
});
}
const length = framebuffers.length;
for (let i = 0; i < length; ++i) {
framebuffers[i].clear(context, clearCommand);
}
};
/**
* A function that will be called before execute. Used to create WebGL resources and load any textures.
* @param {Context} context The context.
* @private
*/
AutoExposure.prototype.update = function (context) {
const width = context.drawingBufferWidth;
const height = context.drawingBufferHeight;
if (width !== this._width || height !== this._height) {
this._width = width;
this._height = height;
createFramebuffers(this, context);
createCommands(this, context);
if (!this._ready) {
this._ready = true;
}
}
this._minMaxLuminance.x = this.minimumLuminance;
this._minMaxLuminance.y = this.maximumLuminance;
const framebuffers = this._framebuffers;
const temp = framebuffers[framebuffers.length - 1];
framebuffers[framebuffers.length - 1] = this._previousLuminance;
this._commands[
this._commands.length - 1
].framebuffer = this._previousLuminance.framebuffer;
this._previousLuminance = temp;
};
/**
* Executes the post-process stage. The color texture is the texture rendered to by the scene or from the previous stage.
* @param {Context} context The context.
* @param {Texture} colorTexture The input color texture.
* @private
*/
AutoExposure.prototype.execute = function (context, colorTexture) {
this._colorTexture = colorTexture;
const commands = this._commands;
if (!defined(commands)) {
return;
}
const length = commands.length;
for (let i = 0; i < length; ++i) {
commands[i].execute(context);
}
};
/**
* Returns true if this object was destroyed; otherwise, false.
*
* If this object was destroyed, it should not be used; calling any function other than
* isDestroyed
will result in a {@link DeveloperError} exception.
*
true
if this object was destroyed; otherwise, false
.
*
* @see AutoExposure#destroy
*/
AutoExposure.prototype.isDestroyed = function () {
return false;
};
/**
* Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
* release of WebGL resources, instead of relying on the garbage collector to destroy this object.
*
* Once an object is destroyed, it should not be used; calling any function other than
* isDestroyed
will result in a {@link DeveloperError} exception. Therefore,
* assign the return value (undefined
) to the object as done in the example.
*