babylon.blurPostProcess.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. module BABYLON {
  2. export class BlurPostProcess extends PostProcess {
  3. protected _kernel: number;
  4. protected _idealKernel: number;
  5. protected _packedFloat: boolean = false;
  6. /**
  7. * Sets the length in pixels of the blur sample region
  8. */
  9. public set kernel(v: number) {
  10. if (this._idealKernel === v) {
  11. return;
  12. }
  13. v = Math.max(v, 1);
  14. this._idealKernel = v;
  15. this._kernel = this._nearestBestKernel(v);
  16. this._updateParameters();
  17. }
  18. /**
  19. * Gets the length in pixels of the blur sample region
  20. */
  21. public get kernel(): number {
  22. return this._idealKernel;
  23. }
  24. /**
  25. * Sets wether or not the blur needs to unpack/repack floats
  26. */
  27. public set packedFloat(v: boolean) {
  28. if (this._packedFloat === v) {
  29. return;
  30. }
  31. this._packedFloat = v;
  32. this._updateParameters();
  33. }
  34. /**
  35. * Gets wether or not the blur is unpacking/repacking floats
  36. */
  37. public get packedFloat(): boolean {
  38. return this._packedFloat;
  39. }
  40. constructor(name: string, public direction: Vector2, kernel: number, options: number | PostProcessOptions, camera: Nullable<Camera>, samplingMode: number = Texture.BILINEAR_SAMPLINGMODE, engine?: Engine, reusable?: boolean, textureType: number = Engine.TEXTURETYPE_UNSIGNED_INT) {
  41. super(name, "kernelBlur", ["delta", "direction"], null, options, camera, samplingMode, engine, reusable, null, textureType, "kernelBlur", {varyingCount: 0, depCount: 0}, true);
  42. this.onApplyObservable.add((effect: Effect) => {
  43. effect.setFloat2('delta', (1 / this.width) * this.direction.x, (1 / this.height) * this.direction.y);
  44. });
  45. this.kernel = kernel;
  46. }
  47. protected _updateParameters(): void {
  48. // Generate sampling offsets and weights
  49. let N = this._kernel;
  50. let centerIndex = (N - 1) / 2;
  51. // Generate Gaussian sampling weights over kernel
  52. let offsets = [];
  53. let weights = [];
  54. let totalWeight = 0;
  55. for (let i = 0; i < N; i++) {
  56. let u = i / (N - 1);
  57. let w = this._gaussianWeight(u * 2.0 - 1);
  58. offsets[i] = (i - centerIndex);
  59. weights[i] = w;
  60. totalWeight += w;
  61. }
  62. // Normalize weights
  63. for (let i = 0; i < weights.length; i++) {
  64. weights[i] /= totalWeight;
  65. }
  66. // Optimize: combine samples to take advantage of hardware linear sampling
  67. // Walk from left to center, combining pairs (symmetrically)
  68. let linearSamplingWeights = [];
  69. let linearSamplingOffsets = [];
  70. let linearSamplingMap = [];
  71. for (let i = 0; i <= centerIndex; i += 2) {
  72. let j = Math.min(i + 1, Math.floor(centerIndex));
  73. let singleCenterSample = i === j;
  74. if (singleCenterSample) {
  75. linearSamplingMap.push({ o: offsets[i], w: weights[i] });
  76. } else {
  77. let sharedCell = j === centerIndex;
  78. let weightLinear = (weights[i] + weights[j] * (sharedCell ? .5 : 1.));
  79. let offsetLinear = offsets[i] + 1 / (1 + weights[i] / weights[j]);
  80. if (offsetLinear === 0) {
  81. linearSamplingMap.push({ o: offsets[i], w: weights[i] });
  82. linearSamplingMap.push({ o: offsets[i + 1], w: weights[i + 1] });
  83. } else {
  84. linearSamplingMap.push({ o: offsetLinear, w: weightLinear });
  85. linearSamplingMap.push({ o: -offsetLinear, w: weightLinear });
  86. }
  87. }
  88. }
  89. for (let i = 0; i < linearSamplingMap.length; i++) {
  90. linearSamplingOffsets[i] = linearSamplingMap[i].o;
  91. linearSamplingWeights[i] = linearSamplingMap[i].w;
  92. }
  93. // Replace with optimized
  94. offsets = linearSamplingOffsets;
  95. weights = linearSamplingWeights;
  96. // Generate shaders
  97. let maxVaryingRows = this.getEngine().getCaps().maxVaryingVectors;
  98. let freeVaryingVec2 = Math.max(maxVaryingRows, 0.) - 1; // Because of sampleCenter
  99. let varyingCount = Math.min(offsets.length, freeVaryingVec2);
  100. let defines = "";
  101. for (let i = 0; i < varyingCount; i++) {
  102. defines += `#define KERNEL_OFFSET${i} ${this._glslFloat(offsets[i])}\r\n`;
  103. defines += `#define KERNEL_WEIGHT${i} ${this._glslFloat(weights[i])}\r\n`;
  104. }
  105. let depCount = 0;
  106. for (let i = freeVaryingVec2; i < offsets.length; i++) {
  107. defines += `#define KERNEL_DEP_OFFSET${depCount} ${this._glslFloat(offsets[i])}\r\n`;
  108. defines += `#define KERNEL_DEP_WEIGHT${depCount} ${this._glslFloat(weights[i])}\r\n`;
  109. depCount++;
  110. }
  111. if (this.packedFloat) {
  112. defines += `#define PACKEDFLOAT 1`;
  113. }
  114. this.updateEffect(defines, null, null, {
  115. varyingCount: varyingCount,
  116. depCount: depCount
  117. });
  118. }
  119. /**
  120. * Best kernels are odd numbers that when divided by 2, their integer part is even, so 5, 9 or 13.
  121. * Other odd kernels optimize correctly but require proportionally more samples, even kernels are
  122. * possible but will produce minor visual artifacts. Since each new kernel requires a new shader we
  123. * want to minimize kernel changes, having gaps between physical kernels is helpful in that regard.
  124. * The gaps between physical kernels are compensated for in the weighting of the samples
  125. * @param idealKernel Ideal blur kernel.
  126. * @return Nearest best kernel.
  127. */
  128. protected _nearestBestKernel(idealKernel: number): number {
  129. let v = Math.round(idealKernel);
  130. for (let k of [v, v - 1, v + 1, v - 2, v + 2]) {
  131. if (((k % 2) !== 0) && ((Math.floor(k / 2) % 2) === 0) && k > 0) {
  132. return Math.max(k, 3);
  133. }
  134. }
  135. return Math.max(v, 3);
  136. }
  137. /**
  138. * Calculates the value of a Gaussian distribution with sigma 3 at a given point.
  139. * @param x The point on the Gaussian distribution to sample.
  140. * @return the value of the Gaussian function at x.
  141. */
  142. protected _gaussianWeight(x: number): number {
  143. //reference: Engine/ImageProcessingBlur.cpp #dcc760
  144. // We are evaluating the Gaussian (normal) distribution over a kernel parameter space of [-1,1],
  145. // so we truncate at three standard deviations by setting stddev (sigma) to 1/3.
  146. // The choice of 3-sigma truncation is common but arbitrary, and means that the signal is
  147. // truncated at around 1.3% of peak strength.
  148. //the distribution is scaled to account for the difference between the actual kernel size and the requested kernel size
  149. let sigma = (1 / 3);
  150. let denominator = Math.sqrt(2.0 * Math.PI) * sigma;
  151. let exponent = -((x * x) / (2.0 * sigma * sigma));
  152. let weight = (1.0 / denominator) * Math.exp(exponent);
  153. return weight;
  154. }
  155. /**
  156. * Generates a string that can be used as a floating point number in GLSL.
  157. * @param x Value to print.
  158. * @param decimalFigures Number of decimal places to print the number to (excluding trailing 0s).
  159. * @return GLSL float string.
  160. */
  161. protected _glslFloat(x: number, decimalFigures = 8) {
  162. return x.toFixed(decimalFigures).replace(/0+$/, '');
  163. }
  164. }
  165. }