提前預警下,能看到文末的都是合格的關注者。
這兩天在 Shadertoy 上新了一個小作品 —— [ 水母生物 ]。期間獲得了一些新的知識,於是決定好好整理一篇推送,來分享下這個小創作的細節。廢話不多說先看一下這個 [ 水母生物 ] 的實時錄屏(無後期,只加了音效)。
https://www.shadertoy.com/view/ttdSWN
這個水母小作品有幾個有意思的技術細節,下面會分別分享如何解決:
1. 上百個觸手如何實時計算以節省性能?
2. 水母頂部傘狀物隨著呼吸節奏的折皺變化。
3. 模擬次表面散射的假的透光著色效果。
4. 玻璃盒子與光線的相交計算。
5. 玻璃表面的汙漬著色和 HUD 掃描效果。
1. 上百個觸手如何計算以節省性能?
在 Raymarching 裡,對空間進行取餘操作是常見的克隆物體的手段,舉個最簡單的例子,我們在世界坐標原點 (0, 0, 0) 位置放置一個球體, Raymarching 裡的代碼將是這樣的:
float sdSphere( vec3 p, float r ) {
return length(p) - r;
}
float map( vec3 p ) {
float radius = 0.5;
return sdSphere( p, radius );
}
如果我們想對這個球體進行克隆,只需要在 map() 函數的一開始加入一行代碼,對 p 位置參數進行一個取餘操作。
float map( vec3 p ) {
// Modify p to clone spheres
p = mod( p, vec3(2.0) ) - 1.0;
float radius = 0.5;
return sdSphere( p, radius );
}
基於這個克隆思路,我們觀察水母的觸手,是呈環形分布在一個圓周上,我們是否可以對這個圓周進行分割呢?假設現在有 10 個分布在圓周上的觸角,我們可以基於角度 ( TWO_PI / 10.0 ) 從俯視圖角度將空間等分成 10 份。這 10 個子空間的觸角是完全相同的。
我們編寫一個名為 circleClone() 的函數,來實現環狀分布克隆的功能。circleClone() 代碼如下,其中 num 變量決定將空間等分成多少份;id 變量記錄著當前位置所屬區域的索引,後面我們可以用這個 id 來為不同的觸角設置不同的波動偏移、長度、顏色等等。
/*
* @param p: 位置
* @param num: 切分的數量
* @param id: 返回當前位置所屬的切分區域序號
*/
vec3 circleClone( vec3 p, float num, out float id ) {
float angleArea = PI * 2.0 / num;
float len = length( p.xz );
float originangle = atan( p.x, p.z );
float angle = mod( originangle, angleArea ) - angleArea * 0.5;
// id: 區分不同的區域
id = floor( originangle / angleArea );
vec3 modPos = vec3( cos( angle ) * len, p.y, sin( angle ) * len );
return modPos;
}
利用這個函數,我們就可以實現,無論多少個觸手,即使成百上千,都只需要進行 1 次觸手的 SDF 計算就可以。下面兩個動圖展示了用不同的 num 參數去切割空間的效果。
2. 水母頂部傘狀物隨著呼吸節奏的折皺變化
這其實是一個很細節的處理,水母頂部的傘狀物在舒展和收縮的時候,表面的皺褶會有不同程度的變化,舒展時皺褶展開變少,收縮時皺褶變多。
怎麼給水母的傘狀物表面添加皺褶,並控制這種變化呢?其實就是通過控制 noise 來實現的。注意下面 noise() 函數的參數,我們使用的不是位置 p,而是 normalize(p),這樣保證相同方向(以原點為出發點)得到的噪聲值是一樣的。
// _NoiseScale 保證皺褶在豎直方向上拉伸
vec3 _NoiseScale = vec3( 8.0, 0.2, 8.0 );
// _NoiseAmp 控制皺褶的強度。
// 第1個 smoothstep 控制強度從上到下遞增
// 第2個 smoothstep 控制強度隨舒展收縮程度變化
float _NoiseAmp = 0.4 * smoothstep( radius, -radius, p.y ) * smoothstep(-0.4, 0.2, shake);
float n = noise( normalize( p ) * _NoiseScale + time * 0.5 );
float d = sdSphere( p, radius );
// Offset d by noise
d += n * _NoiseAmp;
3. 模擬SSS的假的透光著色效果
沒有加SSS的效果
加了SSS的效果
對比上面兩張效果圖,不難看出加了 SSS之後 ,更能體現水母表面透光的感覺。在代碼裡,我實現 Fake SSS 的方式很簡單,通過計算當前表面的厚度 thickness,除以光源之間的距離的平方,得到一個係數。最後利用該係數混合顏色的時候,使用光源和表面顏色的乘積來作為 SSS 的顏色。
這種計算明顯不是基於物理的,但可以大概模擬出我們想要的透光效果,如果你有更好的方法,可以在評論區評論交流。
// thickness:當前表面指向光源位置的厚度
// hitdata: 包含了sss等表面材質信息的變量
// distToLight: 當前表面點和光源之間的距離
// lCol: 光源的顏色
float sss = (1.0 - thickness) * hitdata.sss / (0.0001 + pow(distToLight, 2.0));
col = mix(col, lCol*hitdata.col, sss);
4. 玻璃盒子與光線的相交計算
因為水母被困在一個玻璃盒子裡,所以我們的光線首先要和玻璃盒計算相交,然後再計算出折射後的新的光線方向,才能好對水母進行 Raymarching 計算。
我們當然可以先對盒子進行一個 Raymarching 的計算,求出其和光線交點位置,再進行水母的計算。但是 Raymarching 畢竟是一個吃顯卡的算法,所以我選擇用 Ray-Box Intersection 的相關算法,直接一次求出光線和盒子的交點、以及交點位置的法線。
這裡有一個小插曲,本來我使用的是 scratchapixel 網站上《A Minimal Ray-Tracer: Rendering Simple Shapes (Sphere, Cube, Disk, Plane, etc.)》裡的方法,但是這篇文章畢竟介紹的是在 CPU 的代碼,移植到 GPU 後雖然可以正常運行,但卻顯得有點複雜。
很幸運上傳到 Shadertoy 之後, iq 留言推薦了一篇他的博文,裡面有在 shader 裡直接求解光線和盒子相交的算法,簡直不要太簡潔優雅。
// Ray-Box intersection
// http://iquilezles.org/www/articles/boxfunctions/boxfunctions.htm
/*
* @param ro: 攝像機位置
* @param rd: 攝像機光線
* @param txx: 世界空間轉到盒子空間的矩陣
* @param txi: 盒子空間轉到世界空間的矩陣
* @param nearInfo: 最近相交點的信息
* @param farInfo : 最遠相交點的信息
*/
void iBox( in vec3 ro, in vec3 rd, in mat4 txx, in mat4 txi, in vec3 rad, out vec4 nearInfo, out vec4 farInfo )
{
// convert from ray to box space
vec3 rdd = (txx*vec4(rd,0.0)).xyz;
vec3 roo = (txx*vec4(ro,1.0)).xyz;
// ray-box intersection in box space
vec3 m = 1.0/rdd;
vec3 n = m*roo;
vec3 k = abs(m)*rad;
vec3 t1 = -n - k;
vec3 t2 = -n + k;
float tN = max( max( t1.x, t1.y ), t1.z );
float tF = min( min( t2.x, t2.y ), t2.z );
if( tN > tF || tF < 0.0) {
nearInfo = vec4(9999.0);
farInfo = vec4(9999.0);
return;
}
vec3 norN = -sign(rdd)*step(t1.yzx,t1.xyz)*step(t1.zxy,t1.xyz);
vec3 norF = -sign(rdd)*step(t2.xyz,t2.yzx)*step(t2.xyz,t2.zxy);
// convert to ray space
norN = (txi * vec4(norN,0.0)).xyz;
norF = (txi * vec4(norF,0.0)).xyz;
nearInfo = vec4(tN, norN);
farInfo = vec4(tF, norF);
}
5. 玻璃表面的汙漬著色和 HUD 掃描效果
Shadertoy 不允許自己上傳自定義貼圖,所以我從它給的貼圖裡選了一張比較合適的,來製作玻璃表面的汙漬感。
選用的Shadertoy上的貼圖
原理很簡單,我們考慮最簡單的情況,當光線和玻璃表面相交的時候,一部分光線反射,一部分折射。其中反射光線的顏色值可以從環境貼圖裡取;而折射光線的顏色值則是水母的著色結果。我們利用上面貼圖的 R 通道值控制 fresnel 的值,即反射和折射光線的混合比,從而實現玻璃汙漬的感覺。
// iChannel1:環境貼圖
// iChannel2:玻璃紋理貼圖
// nearPoint: 光線和盒子相交位置
// nor:法線
// reflectionCol: 反射顏色
// refractionCol: 折射顏色
vec3 sc = tex3D( iChannel2, nearPoint, nor ).rgb;
float baseF = 0.1 + sc.r * 0.9;
float fresnel = baseF + ( 1.0 - baseF ) * pow( 1.0 - max( dot( -rd, nor ), 0.0), 2.2);
col = mix( reflectionCol, refractionCol, fresnel );
玻璃表面的 HUD 掃描效果也很有意思的,根據當前交點的位置,計算它的 uv 值,然後根據 uv 判斷當前位置在不在方格線上和中心圓點上,如果在,當前點就設置為 HUD 亮色。同樣的,每個方格子都有自己獨一無二(可能有少部分重複的不過不重要嘻嘻)的 id,通過這些 id 我們可以偽造出隨機閃爍的效果。
// Simple HUD
// nearPoint:盒子和光線相交點位置
// nor:盒子和光線相交點法線
vec2 uv;
if (nor.x != 0.0) uv = nearPoint.yz;
if (nor.y != 0.0) uv = nearPoint.xz;
if (nor.z != 0.0) uv = nearPoint.xy;
vec2 uv1 = uv * 5.0;
// id:每個方格子都有自己的「唯一」索引
float id = sin( floor( uv1.x ) * 10.0 ) + cos( floor( uv1.y ) * 10.0 );
id = sin( id * 10.0 + time * 3.6 );
// 取餘操作將 uv 限制在 0.0 - 1.0 之間
uv1 = mod( uv1, vec2( 1.0 ) );
// grid 變量存儲著當前位置在不在方格線上,0為不在,1為在
float grid = smoothstep( 0.97 - lineWidth , 1.0 - lineWidth , max(uv1.x, uv1.y) ) * 0.5;
// 隨機的方格中心圓點
float centerDot = smoothstep( dotRadius, dotRadius - 0.01, length(uv1 - vec2(0.5)));
centerDot *= smoothstep( 0.5, 1.0, id );
// 合併方格和中心圓點
grid = max( grid, centerDot );
// 掃描效果
grid = max( 0.0, min( 1.0, grid * sin( time * 1.8 + nearPoint.y * 1.0 ) ) );
加了HUD效果的玻璃盒子
Breakdown 分解:
下面的 Breakdown 視頻展示了從 盒子->水母->SSS和反射效果->自發光->HUD 掃描->氣泡 的遞進合成過程。
結語:
完整的原始碼在 Shadertoy 網站上,歡迎交流指正。
https://www.shadertoy.com/view/ttdSWN
最後我將 Shadertoy 上的場景移植到 Unity裡,可以更加方便地操控攝像機、調整參數、自定義更豐富的貼圖、保存序列幀、和傳統的 Mesh 混合渲染等等。有興趣的同學推薦一個 Youtube 上 Peer Play 的教程,他講解了如何在 Unity 裡面玩轉 Raymaching。
https://www.youtube.com/watch?v=oPnft4z9iJs&t=371s
將Shadertoy移植到Unity
這篇推送的內容已經太多了,差不多該結束了,如果你能看到了這裡,我花了一天時間整理這些東西就不虧了。未來一年計劃性保持失業狀態,打算扎進 Unity 和 WebGL 的世界裡,再加上最近疫情嚴重只能宅家,可能會比去年更新頻率上一個臺階。
再見,玩狼人殺去了。
- Twitter: @SenZh4
- Instagram: @sen_zhengys
- Behance: @Sen Zheng(鄭越升)
- 站酷: @鄭越升