• Playcanvas后处理-辉光bloom


    (一)Bloom介绍

    Bloom(辉光、光晕、泛光)是一种常见的摄像机后处理(PostProcessing)效果,用于再现真实世界相机的成像伪影。这种效果会产生从图像中明亮区域边界延伸的光条纹(或羽毛),从而产生一种极其明亮的光线压倒捕捉场景的相机的错觉。

    辉光简单的说法,就是有些地方足够亮,看上去是溢出到周围的区域,下面是playcanvas官方提供的辉光效果对比图

    图1 未使用辉光效果

    图2 使用辉光效果

    (二)Bloom实现原理

    bloom的实现原理很简单:就是取camera获取图像的高亮部分,进行高斯模糊,并和原图进行合并就可以实现。

    1. 提取较亮区域

    通过阈值提取较亮区域的像素点

    1. varying vec2 vUv0;
    2. uniform sampler2D uBaseTexture
    3. uniform float uBloomThreshold;
    4. float luminance(vec4 color)
    5. {
    6. return 0.2125 * color[0] + 0.7154 * color[1] + 0.0721 * color[2];
    7. }
    8. void main(void)
    9. {
    10. vec4 color = texture2D(uBaseTexture, vUv0);
    11. // 只保留亮度超过阈值亮度的像素点
    12. float val = clamp(luminance(color) - uBloomThreshold, 0.0, 1.0);
    13. gl_FragColor = color * val;
    14. //这是playcanvas官网提供提取亮度高于某个阈值的算法,但还不知道它的原理
    15. //gl_FragColor = clamp((color - uBloomThreshold) / (1.0 - uBloomThreshold), 0.0, 1.0);
    16. }

    2. 高斯模糊

    接下来,就是如何得到模糊图uBloomTexture

    一般模糊图像的算法,我们可以选择常见的高斯模糊,它可以减少图像噪声、降低细节层次

    高斯模糊的实现原理,这里不做多赘述,这里提供2篇文章供参考。

    原理参考:2D Shader学习——高斯模糊

    shader实现参考:基于线性采样的高效高斯模糊实现(译)

    参考第二篇文章,我们在JavaScript中,计算我们高斯卷积核的权重和位移

    1. var SAMPLE_COUNT = 15;
    2. //高斯曲线
    3. function computeGaussian(n, theta) {
    4. return ((1.0 / Math.sqrt(2 * Math.PI * theta)) * Math.exp(-(n * n) / (2 * theta * theta)));
    5. }
    6. function calculateBlurValues(sampleWeights, sampleOffsets, dx, dy, blurAmount) {
    7. // Create temporary arrays for computing our filter settings.
    8. // The first sample always has a zero offset.
    9. sampleWeights[0] = computeGaussian(0, blurAmount);
    10. sampleOffsets[0] = 0;
    11. sampleOffsets[1] = 0;
    12. // Maintain a sum of all the weighting values.
    13. var totalWeights = sampleWeights[0];
    14. // Add pairs of additional sample taps, positioned
    15. // along a line in both directions from the center.
    16. var i, len;
    17. for (i = 0, len = Math.floor(SAMPLE_COUNT / 2); i < len; i++) {
    18. // Store weights for the positive and negative taps.
    19. var weight = computeGaussian(i + 1, blurAmount);
    20. sampleWeights[i * 2] = weight;
    21. sampleWeights[i * 2 + 1] = weight;
    22. totalWeights += weight * 2;
    23. var sampleOffset = i * 2 + 1.5;
    24. sampleOffsets[i * 4] = dx * sampleOffset;
    25. sampleOffsets[i * 4 + 1] = dy * sampleOffset;
    26. sampleOffsets[i * 4 + 2] = -dx * sampleOffset;
    27. sampleOffsets[i * 4 + 3] = -dy * sampleOffset;
    28. }
    29. // Normalize the list of sample weightings, so they will always sum to one.
    30. for (i = 0, len = sampleWeights.length; i < len; i++) {
    31. sampleWeights[i] /= totalWeights;
    32. }
    33. }

    在fragment shader中,对图像进行卷积模糊(注意:这里的shader只对水平或垂直一个方向卷积

    1. #define SAMPLE_COUNT 15
    2. varying vec2 vUv0;
    3. uniform sampler2D uBloomTexture;
    4. uniform vec2 uBlurOffsets[15];
    5. uniform float uBlurWeights[15];
    6. void main(void)
    7. {
    8. vec4 color = vec4(0.0);
    9. for (int i = 0; i < SAMPLE_COUNT; i++)
    10. {
    11. color += texture2D(uBloomTexture, vUv0 + uBlurOffsets[i]) * uBlurWeights[i];
    12. }
    13. gl_FragColor = color;
    14. }

    最后,我们需要进行2次方向的滤波处理

    原理具体参考文章二中,如何将高斯滤波器分为水平方向和垂直方向的滤波器的原理

    1. // Pass 2: draw from rendertarget 1 into rendertarget 2(垂直方向)
    2. calculateBlurValues(this.sampleWeights, this.sampleOffsets, 1.0 / this.targets[1].width, 0, this.blurAmount);
    3. scope.resolve("uBlurWeights[0]").setValue(this.sampleWeights);
    4. scope.resolve("uBlurOffsets[0]").setValue(this.sampleOffsets);
    5. scope.resolve("uBloomTexture").setValue(this.targets[0].colorBuffer);
    6. this.drawQuad(this.targets[1], this.blurShader);
    7. // Pass 3: draw from rendertarget 2 back into rendertarget 1(水平方向)
    8. calculateBlurValues(this.sampleWeights, this.sampleOffsets, 0, 1.0 / this.targets[0].height, this.blurAmount);
    9. scope.resolve("uBlurWeights[0]").setValue(this.sampleWeights);
    10. scope.resolve("uBlurOffsets[0]").setValue(this.sampleOffsets);
    11. scope.resolve("uBloomTexture").setValue(this.targets[1].colorBuffer);
    12. this.drawQuad(this.targets[0], this.blurShader);

    3. 混合原图和模糊图

    那我们最后的fragment shader就可以这样实现,原图+模糊图进行混合

    1. varying vec2 vUv0;
    2. //bloom 强度
    3. uniform float uBloomEffectIntensity;
    4. uniform sampler2D uBaseTexture;
    5. uniform sampler2D uBloomTexture;
    6. void main(void)
    7. {
    8. vec4 bloom = texture2D(uBloomTexture, vUv0) * uBloomEffectIntensity;
    9. vec4 base = texture2D(uBaseTexture, vUv0);
    10. //将原图变暗,防止两图叠加后,像素溢出1
    11. base *= (1.0 - clamp(bloom, 0.0, 1.0));
    12. //合并原图和模糊图,得到最终的bloom效果
    13. gl_FragColor = base + bloom;
    14. }

    (三)在playcanvas编辑器中使用

    1. 创建脚本bloom.js

    1. // --------------- POST EFFECT DEFINITION --------------- //
    2. var SAMPLE_COUNT = 15;
    3. function computeGaussian(n, theta) {
    4. return ((1.0 / Math.sqrt(2 * Math.PI * theta)) * Math.exp(-(n * n) / (2 * theta * theta)));
    5. }
    6. function calculateBlurValues(sampleWeights, sampleOffsets, dx, dy, blurAmount) {
    7. // Look up how many samples our gaussian blur effect supports.
    8. // Create temporary arrays for computing our filter settings.
    9. // The first sample always has a zero offset.
    10. sampleWeights[0] = computeGaussian(0, blurAmount);
    11. sampleOffsets[0] = 0;
    12. sampleOffsets[1] = 0;
    13. // Maintain a sum of all the weighting values.
    14. var totalWeights = sampleWeights[0];
    15. // Add pairs of additional sample taps, positioned
    16. // along a line in both directions from the center.
    17. var i, len;
    18. for (i = 0, len = Math.floor(SAMPLE_COUNT / 2); i < len; i++) {
    19. // Store weights for the positive and negative taps.
    20. var weight = computeGaussian(i + 1, blurAmount);
    21. sampleWeights[i * 2] = weight;
    22. sampleWeights[i * 2 + 1] = weight;
    23. totalWeights += weight * 2;
    24. // To get the maximum amount of blurring from a limited number of
    25. // pixel shader samples, we take advantage of the bilinear filtering
    26. // hardware inside the texture fetch unit. If we position our texture
    27. // coordinates exactly halfway between two texels, the filtering unit
    28. // will average them for us, giving two samples for the price of one.
    29. // This allows us to step in units of two texels per sample, rather
    30. // than just one at a time. The 1.5 offset kicks things off by
    31. // positioning us nicely in between two texels.
    32. var sampleOffset = i * 2 + 1.5;
    33. // Store texture coordinate offsets for the positive and negative taps.
    34. sampleOffsets[i * 4] = dx * sampleOffset;
    35. sampleOffsets[i * 4 + 1] = dy * sampleOffset;
    36. sampleOffsets[i * 4 + 2] = -dx * sampleOffset;
    37. sampleOffsets[i * 4 + 3] = -dy * sampleOffset;
    38. }
    39. // Normalize the list of sample weightings, so they will always sum to one.
    40. for (i = 0, len = sampleWeights.length; i < len; i++) {
    41. sampleWeights[i] /= totalWeights;
    42. }
    43. }
    44. /**
    45. * @class
    46. * @name BloomEffect
    47. * @classdesc Implements the BloomEffect post processing effect.
    48. * @description Creates new instance of the post effect.
    49. * @augments PostEffect
    50. * @param {GraphicsDevice} graphicsDevice - The graphics device of the application.
    51. * @property {number} bloomThreshold Only pixels brighter then this threshold will be processed. Ranges from 0 to 1.
    52. * @property {number} blurAmount Controls the amount of blurring.
    53. * @property {number} bloomIntensity The intensity of the effect.
    54. */
    55. function BloomEffect(graphicsDevice) {
    56. pc.PostEffect.call(this, graphicsDevice);
    57. // Shaders
    58. var attributes = {
    59. aPosition: pc.SEMANTIC_POSITION
    60. };
    61. // Pixel shader extracts the brighter areas of an image.
    62. // This is the first step in applying a bloom postprocess.
    63. var extractFrag = [
    64. "varying vec2 vUv0;",
    65. "",
    66. "uniform sampler2D uBaseTexture;",
    67. "uniform float uBloomThreshold;",
    68. "",
    69. "float luminance(vec4 color)",
    70. "{",
    71. " return 0.2125 * color[0] + 0.7154 * color[1] + 0.0721 * color[2]; ",
    72. "}",
    73. "",
    74. "void main(void)",
    75. "{",
    76. // Look up the original image color.
    77. " vec4 color = texture2D(uBaseTexture, vUv0);",
    78. "",
    79. // Adjust it to keep only values brighter than the specified threshold.
    80. " float val = clamp(luminance(color) - uBloomThreshold, 0.0, 1.0);",
    81. " gl_FragColor = color * val;",
    82. "}"
    83. ].join("\n");
    84. // Pixel shader applies a one dimensional gaussian blur filter.
    85. // This is used twice by the bloom postprocess, first to
    86. // blur horizontally, and then again to blur vertically.
    87. var gaussianBlurFrag = [
    88. "#define SAMPLE_COUNT " + SAMPLE_COUNT,
    89. "",
    90. "varying vec2 vUv0;",
    91. "",
    92. "uniform sampler2D uBloomTexture;",
    93. "uniform vec2 uBlurOffsets[" + SAMPLE_COUNT + "];",
    94. "uniform float uBlurWeights[" + SAMPLE_COUNT + "];",
    95. "",
    96. "void main(void)",
    97. "{",
    98. " vec4 color = vec4(0.0);",
    99. // Combine a number of weighted image filter taps.
    100. " for (int i = 0; i < SAMPLE_COUNT; i++)",
    101. " {",
    102. " color += texture2D(uBloomTexture, vUv0 + uBlurOffsets[i]) * uBlurWeights[i];",
    103. " }",
    104. "",
    105. " gl_FragColor = color;",
    106. "}"
    107. ].join("\n");
    108. // Pixel shader combines the bloom image with the original
    109. // scene, using tweakable intensity levels.
    110. // This is the final step in applying a bloom postprocess.
    111. var combineFrag = [
    112. "varying vec2 vUv0;",
    113. "",
    114. "uniform float uBloomEffectIntensity;",
    115. "uniform sampler2D uBaseTexture;",
    116. "uniform sampler2D uBloomTexture;",
    117. "",
    118. "void main(void)",
    119. "{",
    120. // Look up the bloom and original base image colors.
    121. " vec4 bloom = texture2D(uBloomTexture, vUv0) * uBloomEffectIntensity;",
    122. " vec4 base = texture2D(uBaseTexture, vUv0);",
    123. "",
    124. // Darken down the base image in areas where there is a lot of bloom,
    125. // to prevent things looking excessively burned-out.
    126. " base *= (1.0 - clamp(bloom, 0.0, 1.0));",
    127. "",
    128. // Combine the two images.
    129. " gl_FragColor = base + bloom;",
    130. "}"
    131. ].join("\n");
    132. this.extractShader = pc.createShaderFromCode(graphicsDevice, pc.PostEffect.quadVertexShader, extractFrag, 'BloomExtractShader', attributes);
    133. this.blurShader = pc.createShaderFromCode(graphicsDevice, pc.PostEffect.quadVertexShader, gaussianBlurFrag, 'BloomBlurShader', attributes);
    134. this.combineShader = pc.createShaderFromCode(graphicsDevice, pc.PostEffect.quadVertexShader, combineFrag, 'BloomCombineShader', attributes);
    135. this.targets = [];
    136. // Effect defaults
    137. this.bloomThreshold = 0.25;
    138. this.blurAmount = 4;
    139. this.bloomIntensity = 1.25;
    140. // Uniforms
    141. this.sampleWeights = new Float32Array(SAMPLE_COUNT);
    142. this.sampleOffsets = new Float32Array(SAMPLE_COUNT * 2);
    143. }
    144. BloomEffect.prototype = Object.create(pc.PostEffect.prototype);
    145. BloomEffect.prototype.constructor = BloomEffect;
    146. BloomEffect.prototype._destroy = function () {
    147. if (this.targets) {
    148. var i;
    149. for (i = 0; i < this.targets.length; i++) {
    150. this.targets[i].destroyTextureBuffers();
    151. this.targets[i].destroy();
    152. }
    153. }
    154. this.targets.length = 0;
    155. };
    156. BloomEffect.prototype._resize = function (target) {
    157. var width = target.colorBuffer.width;
    158. var height = target.colorBuffer.height;
    159. if (width === this.width && height === this.height)
    160. return;
    161. this.width = width;
    162. this.height = height;
    163. this._destroy();
    164. // Render targets
    165. var i;
    166. for (i = 0; i < 2; i++) {
    167. var colorBuffer = new pc.Texture(this.device, {
    168. name: "Bloom Texture" + i,
    169. format: pc.PIXELFORMAT_RGBA8,
    170. width: width >> 1,
    171. height: height >> 1,
    172. mipmaps: false
    173. });
    174. colorBuffer.minFilter = pc.FILTER_LINEAR;
    175. colorBuffer.magFilter = pc.FILTER_LINEAR;
    176. colorBuffer.addressU = pc.ADDRESS_CLAMP_TO_EDGE;
    177. colorBuffer.addressV = pc.ADDRESS_CLAMP_TO_EDGE;
    178. colorBuffer.name = 'pe-bloom-' + i;
    179. var bloomTarget = new pc.RenderTarget({
    180. name: "Bloom Render Target " + i,
    181. colorBuffer: colorBuffer,
    182. depth: false
    183. });
    184. this.targets.push(bloomTarget);
    185. }
    186. };
    187. Object.assign(BloomEffect.prototype, {
    188. render: function (inputTarget, outputTarget, rect) {
    189. this._resize(inputTarget);
    190. var device = this.device;
    191. var scope = device.scope;
    192. // Pass 1: draw the scene into rendertarget 1, using a
    193. // shader that extracts only the brightest parts of the image.
    194. scope.resolve("uBloomThreshold").setValue(this.bloomThreshold);
    195. scope.resolve("uBaseTexture").setValue(inputTarget.colorBuffer);
    196. this.drawQuad(this.targets[0], this.extractShader);
    197. // Pass 2: draw from rendertarget 1 into rendertarget 2,
    198. // using a shader to apply a horizontal gaussian blur filter.
    199. calculateBlurValues(this.sampleWeights, this.sampleOffsets, 1.0 / this.targets[1].width, 0, this.blurAmount);
    200. scope.resolve("uBlurWeights[0]").setValue(this.sampleWeights);
    201. scope.resolve("uBlurOffsets[0]").setValue(this.sampleOffsets);
    202. scope.resolve("uBloomTexture").setValue(this.targets[0].colorBuffer);
    203. this.drawQuad(this.targets[1], this.blurShader);
    204. // Pass 3: draw from rendertarget 2 back into rendertarget 1,
    205. // using a shader to apply a vertical gaussian blur filter.
    206. calculateBlurValues(this.sampleWeights, this.sampleOffsets, 0, 1.0 / this.targets[0].height, this.blurAmount);
    207. scope.resolve("uBlurWeights[0]").setValue(this.sampleWeights);
    208. scope.resolve("uBlurOffsets[0]").setValue(this.sampleOffsets);
    209. scope.resolve("uBloomTexture").setValue(this.targets[1].colorBuffer);
    210. this.drawQuad(this.targets[0], this.blurShader);
    211. // Pass 4: draw both rendertarget 1 and the original scene
    212. // image back into the main backbuffer, using a shader that
    213. // combines them to produce the final bloomed result.
    214. scope.resolve("uBloomEffectIntensity").setValue(this.bloomIntensity);
    215. scope.resolve("uBloomTexture").setValue(this.targets[0].colorBuffer);
    216. scope.resolve("uBaseTexture").setValue(inputTarget.colorBuffer);
    217. this.drawQuad(outputTarget, this.combineShader, rect);
    218. }
    219. });
    220. // ----------------- SCRIPT DEFINITION ------------------ //
    221. var Bloom = pc.createScript('bloom');
    222. Bloom.attributes.add('bloomIntensity', {
    223. type: 'number',
    224. default: 1,
    225. min: 0,
    226. title: 'Intensity'
    227. });
    228. Bloom.attributes.add('bloomThreshold', {
    229. type: 'number',
    230. default: 0.25,
    231. min: 0,
    232. max: 1,
    233. title: 'Threshold'
    234. });
    235. Bloom.attributes.add('blurAmount', {
    236. type: 'number',
    237. default: 4,
    238. min: 1,
    239. 'title': 'Blur amount'
    240. });
    241. Bloom.prototype.initialize = function () {
    242. this.effect = new BloomEffect(this.app.graphicsDevice);
    243. this.effect.bloomThreshold = this.bloomThreshold;
    244. this.effect.blurAmount = this.blurAmount;
    245. this.effect.bloomIntensity = this.bloomIntensity;
    246. var queue = this.entity.camera.postEffects;
    247. queue.addEffect(this.effect);
    248. this.on('attr', function (name, value) {
    249. this.effect[name] = value;
    250. }, this);
    251. this.on('state', function (enabled) {
    252. if (enabled) {
    253. queue.addEffect(this.effect);
    254. } else {
    255. queue.removeEffect(this.effect);
    256. }
    257. });
    258. this.on('destroy', function () {
    259. queue.removeEffect(this.effect);
    260. this.effect._destroy();
    261. });
    262. };

    2. 将脚本挂载在相机

  • 相关阅读:
    Codeforces Round #818 (Div. 2)
    stm32f4单片机强制类型转换为float程序跑飞问题
    XML - XML学习/XML文件解析器(C++)实现
    智能合约经典漏洞案例,xSurge 重入漏洞+套利 综合运用
    axios升级依赖版本后报错SyntaxError: Cannot use import statement outside a module
    基于阶梯碳交易的含P2G-CCS耦合和燃气掺氢的虚拟电厂优化调度matlab程序
    有效的字母异位词 C
    Python | Leetcode Python题解之第47题全排列II
    德鲁克《卓有成效的管理者》学习笔记-掌握时间的学习和实践
    防御式编程之断言assert的使用
  • 原文地址:https://blog.csdn.net/u014494705/article/details/134512109