blurPostProcess.ts 13 KB

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