Shader 魔法的學習之路(3):笑臉威力加強版


3樓貓 發佈時間:2022-05-17 09:20:57 作者:mnikn Language

上篇文章中我們畫出來了一個笑臉,但是看起來很粗糙,這次我們玩大一點,畫一個能夠讓別人看不出是用代碼畫出來的笑臉吧!
一張臉簡化為臉、眼睛、嘴巴和眉毛,我們就依次去畫吧!
本次文章內容可能看上去有點複雜,不懂的同學可以自己模擬一下,調一下參數慢慢思考。

先畫臉

首先我們先按照之前的方式設置一下基礎的代碼:
vec4 face(vec2 uv) { vec4 color = vec4(1.0, 0.6, 0.3, 1.0); float d = length(uv); color.a = smoothstep(0.5, 0.495, d); // 邊緣漸變 float gradient = smoothstep(0.45, 0.5, d); // 多次相乘讓 gradient 能夠讓顏色從中心到邊緣的衰弱感加深, 參考 y=x^2 函數圖形 gradient *= gradient; color.rgb = mix(color.rgb, color.rgb * 0.6, gradient); return color; } ​ void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec4 color = vec4(0.0, 0.0, 0.0, 1.0); ​ vec2 uv = fragCoord/iResolution.xy; uv -= 0.5; // 讓畫布變為正方形 uv.x *= iResolution.x / iResolution.y; vec4 face_c = face(uv); color.rgb = mix(color.rgb, face_c.rgb, face_c.a); fragColor = color; }
代碼和之前相似,不過不同點是:
  • 我們添加了 uv.x *= iResolution.x / iResolution.y 讓我們整個畫布能夠標準化成正方形,而上次文章因為沒有這步,畫布是長方形,所以畫出來的是一個橢圓。
  • 使用了 mix 函數而不是直接相乘來混色,mix
    函數的作用是接受前兩個參數,第三個參數作為比例,根據比例返回處於前兩個參數之間的值。使用 mix 函數而不是直接相乘的原因,是因為接下來我們要畫很多個圓,直接相乘的話會讓這些圓之間的顏色相互干擾,而 mix 函數用 alpha 做比例保證之間不會相互影響。
  • 我們多添加了臉部邊緣的漸變,其中我們處理完後利用 y=x^2 的函數圖形讓這個值快速上升,從而讓邊緣顏色衰退不是線性的,衰退感更強。
接下來我們逐步添加細節:
float sat(float t) { return clamp(t, 0.0, 1.0); } ​ float remap01(float a, float b, float t) { return sat((t - a) / (b - a)); } ​ float remap(float a, float b, float c, float d, float t) { return remap01(a, b, t) * (d - c) + c; } ​ vec4 face(vec2 uv) { vec4 color = vec4(1.0, 0.6, 0.3, 1.0); float d = length(uv); color.a = smoothstep(0.5, 0.495, d); // 邊緣漸變 float gradient = smoothstep(0.45, 0.5, d); // 多次相乘讓 gradient 能夠讓顏色從中心到邊緣的衰弱感加深, 參考 y=x^2 函數圖形 gradient *= gradient; color.rgb = mix(color.rgb, color.rgb * 0.6, gradient); // 臉部高光 float highlight = smoothstep(0.45, 0.447, d); // 使用 remap 讓高光只畫在臉的上半部分 highlight *= remap(0.45, 0.05, 0.8, 0.0, uv.y); color.rgb = mix(color.rgb, vec3(1.0), highlight); // 描邊 color.rgb = mix(color.rgb, vec3(0.5, 0.1, 0.1), smoothstep(0.487, 0.495, d)); // 臉頰 // 讓臉頰能夠鏡像 uv.x = abs(uv.x); vec2 cheek_uv = uv + vec2(-0.2, 0.2); float cheek_d = length(cheek_uv); float cheek = smoothstep(0.2, 0.05, cheek_d) * 0.3; color.rgb = mix(color.rgb, vec3(0.9, 0.2, 0.1), cheek); return color; }
這裡我們添加了幾個幫助函數:
  • sat:clamp 的進一步封裝,返回值限制在 [0,1],超過或者小於這個範圍就限制為 1 或者 0
  • remap01:mix 的相反版本,根據前兩個參數的相差比例,把第三個參數固定在 [0,1] 滑動
  • remap:區域映射,前兩個參數和後兩個參數表示兩個不同區域,最好一個參數作為比例,把第一個區域按照比例映射到第二個區域上,所以返回值範圍 [c, d],詳細說明:https://zhuanlan.zhihu.com/p/158039963
同時我們添加了臉部的上邊高光,描邊和臉頰,思路和之前類似,一些特殊操作在代碼上有相關說明。

畫個眼睛

vec2 within(vec2 uv, vec4 rect) { return (uv - rect.xy) / (rect.zw - rect.xy); } ​ vec4 eye(vec2 uv, float dir) { uv -= 0.5; uv.x *= dir; // 基礎的圓 vec4 color = vec4(1.0); float d = length(uv); color.a = smoothstep(0.4, 0.39, d); vec3 iris_color = vec3(0.2, 0.6, 0.9); // 眼眶 float iris = smoothstep(0.27, 0.4 , d) * 0.7; iris *= iris; color.rgb = mix(color.rgb, iris_color, iris); // 眼眶下左邊緣描邊 color.rgb *= 1.0 - smoothstep(0.38, 0.4, d) * sat(-uv.x-uv.y); // 眼球描邊 color.rgb = mix(color.rgb, vec3(0.0), smoothstep(0.27, 0.26, d)); // 眼球 iris_color *= 1.0 + smoothstep(0.26, 0.05, d); color.rgb = mix(color.rgb, iris_color, smoothstep(0.26, 0.24, d)); // 瞳孔 color.rgb = mix(color.rgb, vec3(0.0), smoothstep(0.15, 0.14, d)); // 瞳孔高光 float highligh1 = smoothstep(0.12, 0.11, length(uv+vec2(-0.2,-0.1))); color.rgb = mix(color.rgb, vec3(1.0), highligh1); float highlight2 = smoothstep(0.07, 0.06, length(uv+vec2(0.15,0.08))); color.rgb = mix(color.rgb, vec3(1.0), highlight2); return color; } ​ void mainImage( out vec4 fragColor, in vec2 fragCoord ) { // ...之前的代碼 // 畫眼球 vec4 eye_c = eye(within(vec2(abs(uv.x), uv.y), vec4(0.04, -0.12, 0.35, 0.22)), dir); color.rgb = mix(color.rgb, eye_c.rgb, eye_c.a); }
大體的思路和畫臉的時候差不多,有一些特殊的是我們添加了一個 within 函數,該函數的作用是把畫布限制在對應區域,然後我們就可以在 eye 函數里面認為 uv 整個範圍是 [0,1],不用再仔細調座標。
注意一點是,在調用 within 時,我們先讓 uv 做了水平鏡像然後再傳進函數,而不是在函數里面做鏡像,因為 eye
函數里面畫布已經被限制區域了,如果在函數里面做鏡像,就只會在限制區域內做鏡像而不是基於臉的中心做鏡像。
同時我們使用了個 sign 函數來獲得 uv.x 當前是負數還是正數,然後傳進函數改變眼睛繪製方向。因為兩隻眼睛是做了水平鏡像,這樣眼睛高光的方向是向著臉中心,而我們想要眼睛高光統一向著右邊,所以乘上 uv.x 的方向來修正。

畫嘴

vec4 mouth(vec2 uv) { uv -= 0.5; vec4 color = vec4(0.8, 0.3, 0.2, 1.0); // 嘴 uv.y += uv.x*uv.x; float d = length(uv); color.a = smoothstep(0.4, 0.39, d); // 牙齒 float td = remap(-0.25, 0.25, 0.0, 1.0, uv.y); vec3 teeth_col = vec3(smoothstep(0.5, 0.3, d)); color.rgb = mix(color.rgb, teeth_col, smoothstep(0.05, 0.02, td)); // 嘴部漸變 float mtd = 1.0 - remap(-0.25, 0.3, 0.0, 1.0, uv.y); color.rgb *= 1.0 - smoothstep(0.4, 0.0, mtd) * 0.2; return color; } ​ void mainImage( out vec4 fragColor, in vec2 fragCoord ) { // ...之前的代碼 // 畫嘴 vec4 mouth_c = mouth(within(uv, vec4(-0.3, -0.12, 0.3, -0.45))); color.rgb = mix(color.rgb, mouth_c.rgb, mouth_c.a); }

畫眉毛

vec4 brow(vec2 uv) { uv -= 0.5; // 調整眉毛位置 uv.y -= uv.x * uv.x - uv.x; vec4 color = vec4(0.8, 0.4, 0.2, 1.0) * 0.8; float d = length(uv); color *= 1.0 + smoothstep(0.38, 0.05, d); color.a = smoothstep(0.4, 0.39, d); // 描邊 color.rgb *= 1.0 - smoothstep(0.37, 0.4, d) * 0.5; return color; } ​ void mainImage( out vec4 fragColor, in vec2 fragCoord ) { // ...之前的代碼 // 畫眉毛 vec4 brow_c = brow(within(vec2(abs(uv.x), uv.y), vec4(0.04, 0.33, 0.36, 0.24))); color.rgb = mix(color.rgb, brow_c.rgb, brow_c.a); }

總結

這次文章裡面的內容較多,但是所有的畫法主要是繪製位置的計算和顏色間的各種混合,大家最好自己調一下代碼消化一下。
完整代碼在:點擊跳轉

© 2022 3樓貓 下載APP 站點地圖 廣告合作:asmrly666@gmail.com