babylon.blurPostProcess.ts 7.0 KB

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