babylon.blurPostProcess.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. module BABYLON {
  2. /**
  3. * The Blur Post Process which blurs an image based on a kernel and direction.
  4. * Can be used twice in x and y directions to perform a guassian blur in two passes.
  5. */
  6. export class BlurPostProcess extends PostProcess {
  7. protected _kernel: number;
  8. protected _idealKernel: number;
  9. protected _packedFloat: boolean = false;
  10. private _staticDefines:string = ""
  11. /**
  12. * Sets the length in pixels of the blur sample region
  13. */
  14. public set kernel(v: number) {
  15. if (this._idealKernel === v) {
  16. return;
  17. }
  18. v = Math.max(v, 1);
  19. this._idealKernel = v;
  20. this._kernel = this._nearestBestKernel(v);
  21. if(!this.blockCompilation){
  22. this._updateParameters();
  23. }
  24. }
  25. /**
  26. * Gets the length in pixels of the blur sample region
  27. */
  28. public get kernel(): number {
  29. return this._idealKernel;
  30. }
  31. /**
  32. * Sets wether or not the blur needs to unpack/repack floats
  33. */
  34. public set packedFloat(v: boolean) {
  35. if (this._packedFloat === v) {
  36. return;
  37. }
  38. this._packedFloat = v;
  39. if(!this.blockCompilation){
  40. this._updateParameters();
  41. }
  42. }
  43. /**
  44. * Gets wether or not the blur is unpacking/repacking floats
  45. */
  46. public get packedFloat(): boolean {
  47. return this._packedFloat;
  48. }
  49. /**
  50. * Creates a new instance BlurPostProcess
  51. * @param name The name of the effect.
  52. * @param direction The direction in which to blur the image.
  53. * @param kernel The size of the kernel to be used when computing the blur. eg. Size of 3 will blur the center pixel by 2 pixels surrounding it.
  54. * @param options The required width/height ratio to downsize to before computing the render pass. (Use 1.0 for full size)
  55. * @param camera The camera to apply the render pass to.
  56. * @param samplingMode The sampling mode to be used when computing the pass. (default: 0)
  57. * @param engine The engine which the post process will be applied. (default: current engine)
  58. * @param reusable If the post process can be reused on the same frame. (default: false)
  59. * @param textureType Type of textures used when performing the post process. (default: 0)
  60. * @param blockCompilation If compilation of the shader should not be done in the constructor. The updateEffect method can be used to compile the shader at a later time. (default: false)
  61. */
  62. constructor(name: string,
  63. /** The direction in which to blur the image. */
  64. public direction: Vector2,
  65. kernel: number, options: number | PostProcessOptions, camera: Nullable<Camera>, samplingMode: number = Texture.BILINEAR_SAMPLINGMODE, engine?: Engine, reusable?: boolean, textureType: number = Engine.TEXTURETYPE_UNSIGNED_INT, defines = "", private blockCompilation = false) {
  66. super(name, "kernelBlur", ["delta", "direction", "cameraMinMaxZ"], ["circleOfConfusionSampler"], options, camera, samplingMode, engine, reusable, null, textureType, "kernelBlur", {varyingCount: 0, depCount: 0}, true);
  67. this._staticDefines = defines;
  68. this.onApplyObservable.add((effect: Effect) => {
  69. if(this._outputTexture){
  70. effect.setFloat2('delta', (1 / this._outputTexture.width) * this.direction.x, (1 / this._outputTexture.height) * this.direction.y);
  71. }else{
  72. effect.setFloat2('delta', (1 / this.width) * this.direction.x, (1 / this.height) * this.direction.y);
  73. }
  74. });
  75. this.kernel = kernel;
  76. }
  77. /**
  78. * Updates the effect with the current post process compile time values and recompiles the shader.
  79. * @param defines Define statements that should be added at the beginning of the shader. (default: null)
  80. * @param uniforms Set of uniform variables that will be passed to the shader. (default: null)
  81. * @param samplers Set of Texture2D variables that will be passed to the shader. (default: null)
  82. * @param indexParameters The index parameters to be used for babylons include syntax "#include<kernelBlurVaryingDeclaration>[0..varyingCount]". (default: undefined) See usage in babylon.blurPostProcess.ts and kernelBlur.vertex.fx
  83. * @param onCompiled Called when the shader has been compiled.
  84. * @param onError Called if there is an error when compiling a shader.
  85. */
  86. public updateEffect(defines: Nullable<string> = null, uniforms: Nullable<string[]> = null, samplers: Nullable<string[]> = null, indexParameters?: any,
  87. onCompiled?: (effect: Effect) => void, onError?: (effect: Effect, errors: string) => void) {
  88. this._updateParameters(onCompiled, onError);
  89. }
  90. protected _updateParameters(onCompiled?: (effect: Effect) => void, onError?: (effect: Effect, errors: string) => void): void {
  91. // Generate sampling offsets and weights
  92. let N = this._kernel;
  93. let centerIndex = (N - 1) / 2;
  94. // Generate Gaussian sampling weights over kernel
  95. let offsets = [];
  96. let weights = [];
  97. let totalWeight = 0;
  98. for (let i = 0; i < N; i++) {
  99. let u = i / (N - 1);
  100. let w = this._gaussianWeight(u * 2.0 - 1);
  101. offsets[i] = (i - centerIndex);
  102. weights[i] = w;
  103. totalWeight += w;
  104. }
  105. // Normalize weights
  106. for (let i = 0; i < weights.length; i++) {
  107. weights[i] /= totalWeight;
  108. }
  109. // Optimize: combine samples to take advantage of hardware linear sampling
  110. // Walk from left to center, combining pairs (symmetrically)
  111. let linearSamplingWeights = [];
  112. let linearSamplingOffsets = [];
  113. let linearSamplingMap = [];
  114. for (let i = 0; i <= centerIndex; i += 2) {
  115. let j = Math.min(i + 1, Math.floor(centerIndex));
  116. let singleCenterSample = i === j;
  117. if (singleCenterSample) {
  118. linearSamplingMap.push({ o: offsets[i], w: weights[i] });
  119. } else {
  120. let sharedCell = j === centerIndex;
  121. let weightLinear = (weights[i] + weights[j] * (sharedCell ? .5 : 1.));
  122. let offsetLinear = offsets[i] + 1 / (1 + weights[i] / weights[j]);
  123. if (offsetLinear === 0) {
  124. linearSamplingMap.push({ o: offsets[i], w: weights[i] });
  125. linearSamplingMap.push({ o: offsets[i + 1], w: weights[i + 1] });
  126. } else {
  127. linearSamplingMap.push({ o: offsetLinear, w: weightLinear });
  128. linearSamplingMap.push({ o: -offsetLinear, w: weightLinear });
  129. }
  130. }
  131. }
  132. for (let i = 0; i < linearSamplingMap.length; i++) {
  133. linearSamplingOffsets[i] = linearSamplingMap[i].o;
  134. linearSamplingWeights[i] = linearSamplingMap[i].w;
  135. }
  136. // Replace with optimized
  137. offsets = linearSamplingOffsets;
  138. weights = linearSamplingWeights;
  139. // Generate shaders
  140. let maxVaryingRows = this.getEngine().getCaps().maxVaryingVectors;
  141. let freeVaryingVec2 = Math.max(maxVaryingRows, 0.) - 1; // Because of sampleCenter
  142. let varyingCount = Math.min(offsets.length, freeVaryingVec2);
  143. let defines = "";
  144. defines+=this._staticDefines;
  145. // The DOF fragment should ignore the center pixel when looping as it is handled manualy in the fragment shader.
  146. if(this._staticDefines.indexOf("DOF") != -1){
  147. defines += `#define CENTER_WEIGHT ${this._glslFloat(weights[varyingCount-1])}\r\n`;
  148. varyingCount--;
  149. }
  150. for (let i = 0; i < varyingCount; i++) {
  151. defines += `#define KERNEL_OFFSET${i} ${this._glslFloat(offsets[i])}\r\n`;
  152. defines += `#define KERNEL_WEIGHT${i} ${this._glslFloat(weights[i])}\r\n`;
  153. }
  154. let depCount = 0;
  155. for (let i = freeVaryingVec2; i < offsets.length; i++) {
  156. defines += `#define KERNEL_DEP_OFFSET${depCount} ${this._glslFloat(offsets[i])}\r\n`;
  157. defines += `#define KERNEL_DEP_WEIGHT${depCount} ${this._glslFloat(weights[i])}\r\n`;
  158. depCount++;
  159. }
  160. if (this.packedFloat) {
  161. defines += `#define PACKEDFLOAT 1`;
  162. }
  163. this.blockCompilation = false;
  164. super.updateEffect(defines, null, null, {
  165. varyingCount: varyingCount,
  166. depCount: depCount
  167. }, onCompiled, onError);
  168. }
  169. /**
  170. * Best kernels are odd numbers that when divided by 2, their integer part is even, so 5, 9 or 13.
  171. * Other odd kernels optimize correctly but require proportionally more samples, even kernels are
  172. * possible but will produce minor visual artifacts. Since each new kernel requires a new shader we
  173. * want to minimize kernel changes, having gaps between physical kernels is helpful in that regard.
  174. * The gaps between physical kernels are compensated for in the weighting of the samples
  175. * @param idealKernel Ideal blur kernel.
  176. * @return Nearest best kernel.
  177. */
  178. protected _nearestBestKernel(idealKernel: number): number {
  179. let v = Math.round(idealKernel);
  180. for (let k of [v, v - 1, v + 1, v - 2, v + 2]) {
  181. if (((k % 2) !== 0) && ((Math.floor(k / 2) % 2) === 0) && k > 0) {
  182. return Math.max(k, 3);
  183. }
  184. }
  185. return Math.max(v, 3);
  186. }
  187. /**
  188. * Calculates the value of a Gaussian distribution with sigma 3 at a given point.
  189. * @param x The point on the Gaussian distribution to sample.
  190. * @return the value of the Gaussian function at x.
  191. */
  192. protected _gaussianWeight(x: number): number {
  193. //reference: Engine/ImageProcessingBlur.cpp #dcc760
  194. // We are evaluating the Gaussian (normal) distribution over a kernel parameter space of [-1,1],
  195. // so we truncate at three standard deviations by setting stddev (sigma) to 1/3.
  196. // The choice of 3-sigma truncation is common but arbitrary, and means that the signal is
  197. // truncated at around 1.3% of peak strength.
  198. //the distribution is scaled to account for the difference between the actual kernel size and the requested kernel size
  199. let sigma = (1 / 3);
  200. let denominator = Math.sqrt(2.0 * Math.PI) * sigma;
  201. let exponent = -((x * x) / (2.0 * sigma * sigma));
  202. let weight = (1.0 / denominator) * Math.exp(exponent);
  203. return weight;
  204. }
  205. /**
  206. * Generates a string that can be used as a floating point number in GLSL.
  207. * @param x Value to print.
  208. * @param decimalFigures Number of decimal places to print the number to (excluding trailing 0s).
  209. * @return GLSL float string.
  210. */
  211. protected _glslFloat(x: number, decimalFigures = 8) {
  212. return x.toFixed(decimalFigures).replace(/0+$/, '');
  213. }
  214. }
  215. }