原文地址:WebGL學習之HDR與Bloomjavascript
HDR (High Dynamic Range,高動態範圍),在攝影領域,指的是能夠提供更多的動態範圍和圖像細節的一種技術手段。簡單講就是將不一樣曝光拍攝出的最佳細節的LDR (低動態範圍) 圖像合成後,就叫HDR,它能同時反映出場景最暗和最亮部分的細節。爲何須要多張圖片?由於目前的單反相機的寬容度仍是有限的,一張照片不能反映出高動態場景的全部細節。一張圖片拍攝就必需要在暗光和高光之間作出取捨,只能亮部暗部二者取其一。可是經過HDR合成多張圖片,卻能達到咱們想要的效果。
html
那麼在WebGL中,HDR具體指的是什麼。它指的是讓咱們能用超過1.0的數據表示顏色值。到目前爲止,咱們用的都是LDR(低動態範圍),全部的顏色值都被限制在了 [0,1] 範圍。在現實當中,太陽,燈光這類光源它們的顏色值確定是遠遠超出1.0的範圍的。java
本節實現的效果請看hdr & bloom
git
當幀緩衝使用標準化的定點格式(像gl.RGB)爲其顏色緩衝的內部格式,WebGL會在將這些值存入幀緩衝前自動將其約束到0.0到1.0之間。這一操做對大部分幀緩衝格式都是成立的,除了專門用來存放被拓展範圍值的浮點格式。github
WebGL擴大顏色值範圍的方法就是:把顏色的格式設置成16位浮點數或者32位浮點數,即把幀緩衝的顏色緩衝的內部格式設定成 gl.RGB16F, gl.RGBA16F, gl.RGB32F 或者 gl.RGBA32F,這些幀緩衝被叫作浮點幀緩衝(Floating Point Framebuffer),浮點幀緩衝能夠存儲超過0.0到1.0範圍的浮點值,因此很是適合HDR渲染。web
建立浮點幀緩衝,咱們只須要改變顏色緩衝的內部格式參數就好了(注意 gl.FLOAT參數):算法
gl.bindTexture(gl.TEXTURE_2D, colorBuffer); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, gl.RGB, gl.FLOAT, NULL);
幀緩衝默認一個顏色份量只佔用8位(bits)。當使用一個使用32位每顏色份量時(使用gl.RGB32F 或者 gl.RGBA32F),咱們須要四倍的內存來存儲這些顏色。因此除非你須要一個很是高的精確度,32位不是必須的,使用 gl.RGB16F就足夠了。緩存
色調映射(Tone Mapping)是一個損失很小的轉換浮點顏色值至咱們所需的LDR[0.0, 1.0]範圍內的過程,一般會伴有特定的風格的色平衡(Stylistic Color Balance)。app
最簡單的色調映射算法是Reinhard色調映射,它涉及到分散整個HDR顏色值到LDR顏色值上,全部的值都有對應。Reinhard色調映射算法平均地將全部亮度值分散到LDR上。將Reinhard色調映射應用到以前的片斷着色器上,而且加上一個Gamma校訂過濾:函數
void main() { const float gamma = 2.2; vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb; // Reinhard色調映射 vec3 mapped = hdrColor / (hdrColor + vec3(1.0)); // Gamma校訂 mapped = pow(mapped, vec3(1.0 / gamma)); color = vec4(mapped, 1.0); }
有了Reinhard色調映射的應用,咱們再也不會在場景明亮的地方損失細節。固然,這個算法是傾向明亮的區域的,暗的區域會不那麼精細也不那麼有區分度。
另外一個色調映射應用是曝光(Exposure)參數的使用。HDR圖片包含在不一樣曝光等級的細節。若是咱們有一個場景要展示日夜交替,咱們固然會在白天使用低曝光,在夜間使用高曝光,就像人眼調節方式同樣。有了這個曝光參數,咱們能夠去設置能夠同時在白天和夜晚不一樣光照條件工做的光照參數,咱們只須要調整曝光參數就好了。
一個簡單的曝光色調映射算法會像這樣:
uniform float exposure; void main() { const float gamma = 2.2; vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb; // 曝光色調映射 vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure); // Gamma校訂 mapped = pow(mapped, vec3(1.0 / gamma)); color = vec4(mapped, 1.0); }
Bloom 泛光 (或者眩光),是用來模擬光源那種發光或發熱的技術。區分明亮光源的方式是使它們發出光芒,光源的光芒向四周發散,這樣觀察者就會產生光源或亮區的確是強光區。Bloom使咱們感受到一個明亮的物體真的有種明亮的感受。而Bloom和HDR的結合使用能很是完美地展現光源效果。
泛光的品質很大程度上取決於所用的模糊過濾器的質量和類型。下面這幾步就是泛光後處理特效的過程,它總結了實現泛光所需的步驟。
首先咱們要從渲染出來的場景中提取兩張圖片。能夠渲染場景兩次,每次使用一個不一樣的不一樣的着色器渲染到不一樣的幀緩衝中,但可使用一個叫作MRT(Multiple Render Targets多渲染目標)的小技巧,有了它咱們可以在一個單獨渲染處理中提取兩個圖片。在片元着色器的輸出前,咱們指定一個佈局location標識符,這樣咱們即可控制一個片元着色器寫入到哪一個顏色緩衝:
layout (location = 0) out vec4 FragColor; layout (location = 1) out vec4 BrightColor;
使用多個片元着色器輸出的必要條件是,有多個顏色緩衝附加到了當前綁定的幀緩衝對象上。直到如今,咱們一直使用着 gl.COLOR_ATTACHMENT0,但經過使用 gl.COLOR_ATTACHMENT1,能夠獲得一個附加了兩個顏色緩衝的幀緩衝對象。
但首先咱們仍是將建立幀緩衝的功能進行封裝:
function createFramebuffer(gl,opt,width,height){ const fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fb); const framebufferInfo = { framebuffer: fb, textures: [] }; const texs = opt.texs || 1;//顏色緩衝數量 const depth = !!opt.depth; // SECTION 建立紋理 for(let i=0;i< texs;i++){ const tex = initTexture(gl,opt, width, height); framebufferInfo.textures.push(tex); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, tex, 0); } // SECTION 建立用於保存深度的渲染緩衝區 if(depth) { const depthBuffer = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer); } // 檢查幀緩衝區對象 const e = gl.checkFramebufferStatus(gl.FRAMEBUFFER); if (gl.FRAMEBUFFER_COMPLETE !== e) { throw new Error('Frame buffer object is incomplete: ' + e.toString()); } // 解綁幀緩衝區對象 gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindTexture(gl.TEXTURE_2D, null); if(depth) gl.bindRenderbuffer(gl.RENDERBUFFER, null); return framebufferInfo; }
接着調用上面的函數建立包含兩個顏色附件和一個深度附件的幀緩衝區。
//場景幀緩存(2顏色附件 包含正常顏色 和 hdr高光顏色,1深度附件) const fbo = createFramebuffer(gl,{informat:gl.RGBA16F, type:gl.FLOAT, texs:2, depth:true});
在渲染的時候還須要顯式告知WebGL咱們正在經過gl.drawBuffers渲染到多個顏色緩衝,不然WebGL只會渲染到幀緩衝的第一個顏色附件,而忽略全部其餘的。
//採樣到2個顏色附件 gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1]);
當渲染到這個幀緩衝的時候,一個着色器使用一個佈局location修飾符,而後把不一樣顏色值渲染到相應的顏色緩衝。這樣就省去了爲提取高光區域的額外渲染步驟。
#version 300 es precision highp float; layout (location = 0) out vec4 FragColor; layout (location = 1) out vec4 BrightColor; //... void main() { vec3 normal = normalize(vNormal); vec3 viewDirection = normalize(u_viewPosition - vposition); //... vec3 result = ambient + lighting; // 檢查結果值是否高於某個門檻,若是高於就渲染到高光顏色緩存中 float brightness = dot(result, vec3(0.2126, 0.7152, 0.0722)); if(brightness > 1.0){ BrightColor = vec4(result, 1.0); } else { BrightColor = vec4(0.0, 0.0, 0.0, 1.0); } FragColor = vec4(result, 1.0); }
這裏先正常計算光照,將其傳遞給第一個片元着色器的輸出變量FragColor。而後咱們使用當前儲存在FragColor的東西來決定它的亮度是否超過了必定閾限。咱們經過恰當地將其轉爲灰度的方式計算一個fragment的亮度,若是它超過了必定閾限,咱們就把顏色輸出到第二個顏色緩衝,那裏保存着全部亮部。
這也說明了爲何泛光在HDR基礎上可以運行得很好。由於HDR中,咱們能夠將顏色值指定超過1.0這個默認的範圍,咱們可以獲得對一個圖像中的亮度的更好的控制權。沒有HDR咱們必須將閾限設置爲小於1.0的數,雖然可行,可是亮部很容易變得不少,這就致使光暈效果太重。
有了一個提取出的亮區圖像,咱們如今就要把這個圖像進行模糊處理。
要實現高斯模糊過濾須要一個二維四方形做爲權重,從這個二維高斯曲線方程中去獲取它。然而這個過程有個問題,就是很快會消耗極大的性能。以一個32×32的模糊kernel爲例,咱們必須對每一個fragment從一個紋理中採樣1024次!
幸運的是,高斯方程有個很是巧妙的特性,它容許咱們把二維方程分解爲兩個更小的方程:一個描述水平權重,另外一個描述垂直權重。咱們首先用水平權重在整個紋理上進行水平模糊,而後在經改變的紋理上進行垂直模糊。利用這個特性,結果是同樣的,可是能夠節省難以置信的性能,由於咱們如今只需作32+32次採樣,再也不是1024了!這叫作兩步高斯模糊。
這意味着咱們若是對一個圖像進行模糊處理,至少須要兩步,最好使用幀緩衝對象作這件事。具體來講,咱們將實現像乒乓球同樣的幀緩衝來實現高斯模糊。意思是使用一對幀緩衝,咱們把另外一個幀緩衝的顏色緩衝放進當前的幀緩衝的顏色緩衝中,使用不一樣的着色效果渲染指定的次數。基本上就是不斷地切換幀緩衝和紋理去繪製。這樣咱們先在場景紋理的第一個緩衝中進行模糊,而後在把第一個幀緩衝的顏色緩衝放進第二個幀緩衝進行模糊,接着將第二個幀緩衝的顏色緩衝放進第一個,循環往復。
在咱們研究幀緩衝以前,先來實現高斯模糊的片元着色器:
#version 300 es precision highp float; uniform sampler2D image; uniform bool horizontal; in vec2 texcoord; out vec4 FragColor; const float weight[5] = float[](0.2270270270, 0.1945945946, 0.1216216216, 0.0540540541, 0.0162162162); void main() { vec2 tex_offset = vec2(1.0 / float(textureSize(image, 0)));//每一個像素的尺寸 vec3 result = texture(image, texcoord).rgb * weight[0]; if (horizontal) { for (int i = 0; i < 5; ++i) { result += texture(image, texcoord + vec2(tex_offset.x * float(i), 0.0)).rgb * weight[i]; result += texture(image, texcoord - vec2(tex_offset.x * float(i), 0.0)).rgb * weight[i]; } } else { for (int i = 0; i < 5; ++i) { result += texture(image, texcoord + vec2(0.0, tex_offset.y * float(i))).rgb * weight[i]; result += texture(image, texcoord - vec2(0.0, tex_offset.y * float(i))).rgb * weight[i]; } } FragColor = vec4 (result, 1.0); }
這裏使用一個比較小的高斯權重作例子,每次咱們用它來指定當前fragment的水平或垂直樣本的特定權重。你會發現咱們基本上是將模糊過濾器根據咱們在uniform變量horizontal設置的值分割爲一個水平和一個垂直部分。經過用1.0除以紋理的大小(從textureSize獲得一個vec2)獲得一個紋理像素的實際大小,以此做爲偏移距離的根據。
接着爲圖像的模糊處理建立兩個基本的幀緩衝,每一個只有一個顏色緩衝紋理,調用上面封裝好的createFramebuffer函數便可。
//2乒乓幀緩存(都只包含1顏色附件) const hFbo = createFramebuffer(gl,{informat:gl.RGBA16F, type:gl.FLOAT}); const vFbo = createFramebuffer(gl,{informat:gl.RGBA16F, type:gl.FLOAT});
獲得一個HDR紋理後,咱們用提取出來的亮區紋理填充一個幀緩衝,而後對其模糊處理6次(3次垂直3次水平):
/** * 乒乓幀緩存 */ gl.useProgram(pProgram.program); for(let i=0; i < 6; i++){ bindFramebufferInfo(gl, i%2 ? hFbo:vFbo); setBuffersAndAttributes(gl, pProgram, pVao); setUniforms(pProgram,{ horizontal: i%2? true:false, image: i == 0 ? fbo.textures[1]: i%2 ? vFbo.textures[0]: hFbo.textures[0], //第1次兩個乒乓幀緩存都爲空,所以第一次要將燈光紋理傳入 }); drawBufferInfo(gl, pVao); }
每次循環根據渲染的是水平仍是垂直來綁定兩個緩衝其中之一,而將另外一個綁定爲紋理進行模糊。第一次迭代,由於兩個顏色緩衝都是空的因此咱們隨意綁定一個去進行模糊處理。重複這個步驟6次,亮區圖像就進行一個重複3次的高斯模糊了。這樣咱們能夠對任意圖像進行任意次模糊處理;高斯模糊循環次數越多,模糊的強度越大。
有了場景的HDR紋理和模糊處理的亮區紋理,只需把它們結合起來就能實現泛光或稱光暈效果了。最終的片元着色器要把兩個紋理混合:
#version 300 es precision highp float; in vec2 texcoord; uniform sampler2D image; uniform sampler2D imageBlur; uniform bool bloom; out vec4 FragColor; const float exposure = 1.0; const float gamma = 2.2; void main() { vec3 hdrColor = texture(image, texcoord).rgb; vec3 bloomColor = texture(imageBlur, texcoord).rgb; if (bloom) hdrColor += bloomColor; //添加融合 //色調映射 // vec3 result = hdrColor / (hdrColor + vec3(1.0)); vec3 result = vec3 (1.0) - exp(-hdrColor * exposure); //進行gamma校訂 result = pow(result, vec3 (1.0 / gamma)); FragColor = vec4(result, 1.0); }
注意要在應用色調映射以前添加泛光效果。這樣添加的亮區的泛光,也會柔和轉換爲LDR,光照效果相對會更好。把兩個紋理結合之後,場景亮區便有了合適的光暈特效:
這裏只用了一個相對簡單的高斯模糊過濾器,它在每一個方向上只有5個樣本。經過沿着更大的半徑或重複更屢次數的模糊,進行採樣咱們就能夠提高模糊的效果。由於模糊的質量與泛光效果的質量正相關,提高模糊效果就可以提高泛光效果。
這個HDR + Bloom的是目前爲止渲染流程最複雜的一個特效了,使用了3個着色器program和3個幀緩衝區,繪製的時候要不斷切換program 和 幀緩衝區。目前有個問題是,從幀緩衝渲染到正常緩衝後場景的鋸齒感挺嚴重的,後續還得深刻學習下抗鋸齒(anti-aliasing)。