目的:
移植Inigo Quilez的可視化三維Julia的算法到ue4。
本篇僅考慮造型,忽略表面法線計算和光照模型等。
忽略分形相關細節數學原理。
參考:
最近牆越來越高,我也沒法放鏈接了。
1.Inigo Quilez在ShaderToy上的Julia,網址後綴MsfGRr
2.維度:數學漫步第一季的複數(下)
3.Ray Tracing Quaternion Julia Sets on the GPU——Keenan Crane,University of Illinois at Urbana-Champaign
pdf,非論文。
觀察:
我們意識到難點有二:
1.完整的JuliaSet沒有解析式,只有遞推式。
若要判斷某一點是否可由初始集合經過k次迭代能被包含,消耗也是巨大而不現實的。
因此我們似乎既沒辦法直接RayTrace,也沒辦法RayMarch出一個Julia雲。
(下面是a=0時的2D情況,演示了2次迭代,這個特殊情況我們能判斷點是否在Julia內,因爲Julia此時爲unit disk)
2.標準Julia集極其小,看不清。
由於是z^2+a,“可見部分”就跟單位圓差不多大小。我們希望搞一個actor,能隨意縮放,位移Julia。但又不改變Julia的那些公式(因爲理論公式現成,要改麻煩)。
分析:
1.問題一:根據距離場RayMarch到Julia表面
簡而言之,雖無Julia表面解析式,但有其距離場估計的表達式。
距離場是一個數值場S=F(x,y,z),空間中每點數值爲距離Julia表面最近的距離。
僞代碼
bool intersectJulia(...)
{
float h = 1.0;
float t = 0.0;
for( int i=0; i<最大March次數; i++ )
{
if( h<0.0001(碰到表面) ||
t>最遠距離 (沒有碰到表面) )
{
break;
}
h = MarchStep( ro+rd*t, ...);
t += h;
}
if( t<最遠距離) { return true; }
return false;
}
2.問題二:移動ro位置
Julia所有表達式不變,對傳入的視線射線的端點位置調整。
ro本來是相機位置camPos。
縮放:對camPos縮放,越小,說明我們越接近單位圓去ray這個Julia,Julia越大。
移動:camPos-JuliaPos
步驟
1.計算ro
注意到RayMarchFunc也被連了進去。它以*1的形式參與ro的計算,而主函數以ro爲參,保證了其usf內的函數先於主函數編譯。
//RayMarchFunc.usf
return 1;
}
float4 qsqr( float4 a ) // square a quaterion
{
return float4( a.x*a.x - a.y*a.y - a.z*a.z - a.w*a.w,
2.0*a.x*a.y,
2.0*a.x*a.z,
2.0*a.x*a.w );
}
float map(float3 p,float4 c )
{
float4 z = float4(p,0.0);
float md2 = 1.0;
float mz2 = dot(z,z);
float4 trap = float4(abs(z.xyz),dot(z,z));
float n = 1.0;
for( int i=0; i<11; i++ )
{
// dz -> 2·z·dz, meaning |dz| -> 2·|z|·|dz|
// Now we take thr 2.0 out of the loop and do it at the end with an exp2
md2 *= mz2;
// z -> z^2 + c
z = qsqr(z) + c;
trap = min( trap, float4(abs(z.xyz),dot(z,z)) );
mz2 = dot(z,z);
if(mz2>4.0) break;
n += 1.0;
}
return 0.25*sqrt(mz2/md2)*exp2(-n)*log(mz2); // d = 0.5·|z|·log|z| / |dz|
}
bool intersectJulia(float3 ro, float3 rd ,float4 c)
{
float4 res;
float resT = -1.0;
float maxd = 10.0;
float h = 1.0;
float t = 0.0;
for( int i=0; i<300; i++ )
{
if( h<0.0001||t>maxd ) break;
h = map( ro+rd*t, c );
t += h;
}
if( t<maxd ) { resT=t;return true; }
return false;
2.計算rd
和上篇一樣
3.主函數
其中c就是julia遞推式z^2+a的a。
4.結合藍圖Actor
控制材質參數,包括Julia位置等。
5.結果
在ue4默認相機下,正常工作。
我注意到,當我使用ue4影視鏡頭時,由於其FOV並非90度,所以移動起來Julia的位置和大小不正常。這和上面的推導一致。
我還把Inigo Quilez計算法線的部分移植進來。這部分代碼對我來說水平太高且不具普遍性,放棄理解了。先只寫了一個diffuse,看起來不是很好看。對比最近的分形宣傳視頻,果然分形好看還是在光照和材質上。
當然,IQ大神自己是能信手拈來好看的光照模型的,並且作了AA。這是shaderToy原圖:
但他的代碼太過硬核,不適合學習。所以我決定找找其他代碼清晰,而各方面都考慮得比較全面得Shader學習一下。
這是他的光照部分的代碼:
// sky
{
float co = clamp( dot(-rd,nor), 0.0, 1.0 );
vec3 ref = reflect( rd, nor );
//float sha = softshadow( pos+0.0005*nor, ref, 0.001, 4.0, c );
float sha = occ;
sha *= smoothstep( -0.1, 0.1, ref.y );
float fre = 0.1 + 0.9*pow(1.0-co,5.0);
col = mate*0.3*vec3(0.8,0.9,1.0)*(0.6+0.4*nor.y)*occ;
col += 2.0*0.3*vec3(0.8,0.9,1.0)*(0.6+0.4*nor.y)*sha*fre;
}
// sun
{
const vec3 lig = sun;
float dif = clamp( dot( lig, nor ), 0.0, 1.0 );
float sha = softshadow( pos, lig, 0.001, 64.0, c );
vec3 hal = normalize( -rd+lig );
float co = clamp( dot(hal,lig), 0.0, 1.0 );
float fre = 0.04 + 0.96*pow(1.0-co,5.0);
float spe = pow(clamp(dot(hal,nor), 0.0, 1.0 ), 32.0 );
col += mate*3.5*vec3(1.00,0.90,0.70)*dif*sha;
col += 7.0*3.5*vec3(1.00,0.90,0.70)*spe*dif*sha*fre;
}
// extra fill
{
const vec3 lig = vec3( -0.707, 0.000, -0.707 );
float dif = clamp(0.5+0.5*dot(lig,nor), 0.0, 1.0 );
col += mate* 1.5*vec3(0.14,0.14,0.14)*dif*occ;
}
// fake SSS
{
float fre = clamp( 1.+dot(rd,nor), 0.0, 1.0 );
col += mate* mate*0.6*fre*fre*(0.2+0.8*occ);
}
結語
之後參考比較規整的光照模型,將Julia Set表面整好看點...
1.技術差,就會醜。
2.在非學術的Shader實現裏,我經常看到Inigo Quilez。我最佩服的就是他獨立思考的能力。以參考中的那篇ShaderToy所附帶的技術博客爲例,他拋開常見的z=a+bi+cj+dk形式的複數,自創z=a+bi+cj形式複數,並規定基向量的運算規則,十分大膽。針對實際問題,更改思考框架的操作可以說是我的夢想。
3.在Ray Tracing Quaternion Julia Sets on the GPU中,作者十分風趣,各個細節都十分詳細(就像把你當傻瓜一樣),甚至充滿半頁紙的圖,結尾直接上代碼,你不理解都不行。與滿篇廢話又不着重點,吹上天又沒東西的gp論文比,我太喜歡了。
作者的伊利諾伊大學厄巴納-香檳分校似乎挺牛的。