[OpenGL] Hatching 素描画效果
參考:http://hhoppe.com/proj/hatching/
第一部分 : 論文概述
里面包含了論文和演講稿,接下來是對Real-Time hatching論文的一個概述:
? ? ? ? 主要的挑戰:
? ? ? ? (1) 有限的實時計算
? ? ? ? (2) 筆畫之間幀的相干性
? ? ? ? (3) 動態觀察角度下控制筆畫大小和密度
? ? ? ? 方法:
? ? ? ? TAMs圖像。每張圖像出現的筆劃,也會出現在它右邊或下邊的圖像。
? ? ? ? 將繪制的筆劃與處理成一系列不同筆觸大小的圖像(tonal art map, TAM),運行時使用TAM進行面片的紋理化。我們需要保證改變色調的同時保持筆劃空間和時間的相干性。
? ? ? ?(1) 離線創建圖像部分。我們需要保證:較亮圖像是較暗圖像筆劃的子集,用粗糙的mip-map級別筆畫,繪制更精細的筆畫,以保證分辨率一致性。
? ? ? (2)?實時渲染紋理部分。需要使用多紋理混合,根據頂點處的光照對紋理圖像進行加權。由于混合的紋理共享了多種筆劃,所以可以得到這么一個效果:隨著表面區域變得更暗,現有筆劃持續存在,而一些新的筆劃也會隨之出現。
? ? ? ?問題
? ? ? ?距離與尺度。我們希望在靠近對象時,看到更多的筆畫;而普通的紋理映射只會讓筆劃變得更大。這里使用類似硬件mip-map的方法,設計mip-map級別,使得筆劃在所有級別中具有相同(像素)寬度。對于更精細的層級,我們添加新的筆劃。
? ? 最左邊的球(無光照),顯示了mip-map隨著空間范圍(深度)逐漸在球的兩極處淡出;中間的圓柱(有光照)顯示了TAM平滑的過渡;最右邊的球是兩者的結合。
? ? ? ? TAMs的自動生成
? ? ? ? 從上到下,從左到右填充TAM圖像。
? ? ? ? 對每一列:先將列中的所有圖像復制到左側,保證所有筆劃都有較淺色調。
? ? ? ? 然后從上到下(也就是從粗到細)考慮每個圖像,重復向圖像添加筆劃,直到圖像的筆劃達到該列應該達到的值。每繪制一個筆劃,也會在同一列更精細的圖像繪制相同的筆劃,保證較粗糙圖像中筆劃總能出現在更精細圖像中。
? ? ? ? 通常會使得筆劃更均勻,方法是生成多個隨機放置的候選筆劃并選擇最合適的,合適的標準是:期望的色調值、陰影均勻度;該例中,對于淺色調圖像,候選者數量為1000,對于筆劃重疊的暗色調圖像逐漸減少到100。每個候選者筆劃長度隨機設置為寬度的0.3~1.0倍,并在方向上做一個隨機擾動。
? ? ? ? hatch圖像最佳大小為 : 256 x 256
? ? ? ??TAMs的渲染
? ? ? ? 渲染要考慮的問題:
? ? ? ? 如果每個面用一張紋理,渲染會出現跨邊緣的空間不連續性(如圖)、時間的不連續(避免突然出現)。
空間不連續性? ? ? ? 傳統著色模擬方法:對一個面的所有像素使用一個混合比例,在兩個連續tam紋理列之間進行插值。但空間上不夠連續,我們還需要混合每個面上的頂點貢獻。
? ? ? ? 另一種方案:為每個頂點選擇一個tam紋理,并在各個面上混合,可以保證空間上是平滑的,但是存在時間不連續性。(類似于Gourand著色頂點光照計算)
? ? ? ? ?為了獲取空間和時間的一致性,我們對每個網格頂點選擇合適的tam紋理;每個面最多混合6個紋理,每個頂點都會得到2種紋理的混合;然后對于每個面,在三個頂點處混合紋理,并在空間上使用頂點重心權重來組合它們。
? ? ? ? ?根據重心坐標,曲面內每個點都會受到3個頂點的線性組合,最多6個不同的紋理。
? ? ? ??
? ? ? ? ?在三角形面上混合6個TAM圖像。每列(a~c)對應一個頂點,并顯示該點的最大、最小圖像。所得的和如(d)所示。
? ? ? ? ?方法一: 僅用1個pass,6路混合
? ? ? ? ?一次pass中傳入6個紋理(由于是灰度圖,可以壓縮到2個紋理,存在顏色通道里)。提供的6張圖像,但在插值方法中提供了隱式白色。通過指定6個獨立系數,我們可以對著7個TAM圖像進行混合。根據著色器中漫反射和鏡面反射的顏色混合。
(該部分的示例代碼,來自演講稿)
!!VP1.0 #Vertex Program for Real-Time Hatching. //output vertex homogeneous coordinates DP4 R2.x, c[0], v[OPOS]; DP4 R2.y, c[1], v[OPOS]; DP4 R2.z, c[2], v[OPOS]; DP4 R2.w, c[3], v[OPOS]; MOV o[HPOS], R2; //stroke texture coordinates, transformed DP3 o[TEX0].x, c[4], v[TEX0]; DP3 o[TEX0].y, c[5], v[TEX0]; DP3 o[TEX1].x, c[4], v[TEX0]; DP3 o[TEX1].y, c[5], v[TEX0]; // splotch mask coordinates MOV o[TEX2], v[TEX0]; //get the Gouraud shade DP3 R1, c[8], v[NRML]; //apply clamp-linear tone transfer function MUL R1, R1, c[9].x; ADD R1, R1, c[9].y; MAX R1, R1, c[9].z; MIN R1, R1, c[9].w; //now look up the weights for the TAMs blending EXP R2.y, R1.x; //frac(tone) ARL A0.x, R1.x; MOV R3, c[A0.x + 10]; MAD R3, -R2.y, R3, R3; MAD o[COL1], R2.y, c[A0.x + 11], R3; MOV R4, c[A0.x + 20]; MAD R4, -R2.y, R4, R4; MAD o[COL0], R2.y, c[A0.x + 21], R4; END? ? ? ? ?方法二: 使用3個pass,2路混合
? ? ? ? 如果TAM圖像包含彩色筆畫,或者想要6個以上的TAM圖像,可以在一個面上3次累積紋理。首先使用黑色繪制整個網格,接下來,每個三角形渲染三次,將每個頂點的貢獻添加到幀緩沖區。對于每個頂點,我們加入兩個包含頂點色調的TAM圖像列。
? ? ? ? 設定閾值
? ? ? ? 對于6路混合,可以獲得一致性,但會導致一些灰色筆畫,對于鉛筆、木炭等藝術風格很自然,但對于類似墨水的筆法并不理想。對于單通道的方案,我們對寫入幀緩沖區的色調值設定閾值,可使得新筆畫顯示為逐漸延長的黑色筆畫,而不是逐漸變暗的灰色筆畫。但是它可能會引入鋸齒,我們可以使用超采樣來緩解。
第二部分:實現
? ? ? ??
? ? ? ? 開發框架:Qt + OpenGL
? ? ? ? 首先我沒有找到原分辨率的貼圖,所以我是直接從論文的圖上強行扣下來,分辨率非常低(最大的紋理分辨率還不到100x100),導致我不得不把紋理弄得密了一點來掩蓋貼圖的像素感,效果可能沒那么理想= =
? ? ? ? ?設定mipmap
? ? ? ? ??
? ? ? ? ? ? 首先按照如圖所示準備一系列mipmap貼圖。
? ? ? ? ? ? 我們平時設定紋理的時候,是使用glTexImage2D的,第二個參數即是mipmap層級,默認情況下我們只指定一張貼圖,其余由硬件生成。在這里,我們手動指定4張mipmap貼圖,如下:
GLuint p[6];glGenTextures(6, p);for(int i = 0;i < 6;i++){QImage img[4];for(int j = 0;j < 4;j++){img[j].load("./p" + QString::number(i+1) + "_" + QString::number(j) + ".png");img[j] = img[j].convertToFormat(QImage::Format_RGBA8888);}glBindTexture(GL_TEXTURE_2D, p[i]);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 3);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, img[0].width(), img[0].height(), 0,GL_RGBA, GL_UNSIGNED_BYTE, img[0].bits());glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, img[1].width(), img[1].height(), 0,GL_RGBA, GL_UNSIGNED_BYTE, img[1].bits());glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA, img[2].width(), img[2].height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, img[2].bits());glTexImage2D(GL_TEXTURE_2D, 3, GL_RGBA, img[3].width(), img[3].height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, img[3].bits());}glBindTexture(GL_TEXTURE_2D, 0);? ? ? ? ?混合紋理
? ? ? ? ?根據論文的描述,我們需要先根據頂點處的光照,為每個頂點對應2張TAM紋理。這里實際上是這么一個過程:
? ? ? ? (1) 首先根據光照方程求得光照因子(0~1之間,0代表最暗,1代表最亮)。
? ? ? ? (2) 然后把單位1等分為6份,看光照因子落在哪個區間內。具體計算方式就是光照因子乘以6后再取整。(等分為6份,意味著有7個邊界值,其中6個是TAM紋理,最后一個可以當做是一個純白色紋理,它也會參與到運算中)
? ? ? ? (3) 設TAM紋理為p1,p2..到p6, 最暗的為p6,最亮的為p1。那么光照因子為0.1時,它落在0 ~ 1/6 的區間,此時該頂點對應了p5,p6兩張紋理,權重是按照它到區間兩個邊界的距離來計算的。
? ? ? ? 原論文指出要每個頂點對應了2個紋理,需要對一個面,根據每個頂點的重心來計算權重,最多會混合6張紋理。我們用著色器來實現的話,這個重心是借由頂點到片元著色器的傳遞變量,會自動進行雙線性插值來完成的,所以我們無需自己進行重心相關計算。
? ? ? ? ?為了能夠更好地理解這一過程,我們來看一種比較極端的情況,也就是所有TAM紋理都參與了插值計算。算上白色紋理的話,相當于我們在頂點著色器填充7個參數,分別對應7張紋理的權重。假設一個面有三個頂點,分別為v1,v2,v3。每個頂點最多貢獻兩張紋理的權重,而且這兩張紋理是連續的,也就意味著其余的權重都為0。
? ? ? ? ?此時,v1對應p1,p2紋理; v2對應p3,p4紋理;v3對應p5,p6紋理,我們索性假設所有權重都為0.5。那么,在片元著色器,該三角形的重心位置,所有紋理都會貢獻相同的1/6權重。
?
頂點著色器
#version 450 coreuniform mat4 ModelMatrix; uniform mat4 IT_ModelMatrix; uniform mat4 ViewMatrix; uniform mat4 ProjectMatrix; uniform vec3 cameraPos; uniform vec3 LightLocation;attribute vec4 a_position; attribute vec3 a_normal; attribute vec3 a_tangent; attribute vec2 a_texcoord;varying vec2 v_texcoord;varying float param1,param2,param3,param4,param5,param6,param7; void main() {gl_Position = ModelMatrix * a_position;vec3 worldPos = vec3(gl_Position);gl_Position = ViewMatrix * gl_Position;gl_Position = ProjectMatrix * gl_Position;v_texcoord = a_texcoord;vec3 normal = mat3(IT_ModelMatrix) * a_normal;vec3 ViewDir = normalize(cameraPos - worldPos);vec3 lightDir = normalize(LightLocation - worldPos);vec3 reflectDir = normalize(reflect(-lightDir,normal));float diffuse = 0.9 * clamp(dot(normal, lightDir), 0, 1);float specular = 0.3 * pow(clamp(dot(reflectDir,ViewDir),0,1),15);float ambient = 0.1;float ratio = clamp(diffuse + specular + ambient, 0.0f, 1.0f) * 6;param1 = 0.0f;param2 = 0.0f;param3 = 0.0f;param4 = 0.0f;param5 = 0.0f;param6 = 0.0f;param7 = 0.0f;if(ratio <= 1){param2 = ratio;param1 = 1 - ratio;}else if(ratio <= 2){ratio -= 1;param3 = ratio;param2 = 1 - ratio;}else if(ratio <= 3){ratio -= 2;param4 = ratio;param3 = 1 - ratio;}else if(ratio <= 4){ratio -= 3;param5 = ratio;param4 = 1 - ratio;}else if(ratio <= 5){ratio -= 4;param6 = ratio;param5 = 1 - ratio;}else if(ratio <= 6){ratio -= 5;param7 = ratio;param6 = 1 - ratio;} }片元著色器
#version 450 core uniform sampler2D p1; uniform sampler2D p2; uniform sampler2D p3; uniform sampler2D p4; uniform sampler2D p5; uniform sampler2D p6;varying vec2 v_texcoord; varying float param1,param2,param3,param4,param5,param6,param7;void main() {vec2 uv = v_texcoord;gl_FragColor =param1 * texture2D(p6,uv) +param2 * texture2D(p5,uv) +param3 * texture2D(p4,uv) +param4 * texture2D(p3,uv) +param5 * texture2D(p2,uv) +param6 * texture2D(p1,uv) +param7 * vec4(1,1,1,1); }?
總結
以上是生活随笔為你收集整理的[OpenGL] Hatching 素描画效果的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 洛谷 1571 眼红的Medusa
- 下一篇: HTML 自定义实现emoji