three.js 實(shí)現(xiàn)露珠滴落動畫
前言
本文我們將用three.js來實(shí)現(xiàn)一種很酷的光學(xué)效果——露珠滴落。我們知道,在露珠從一個物體表面滴落的時候,會產(chǎn)生一種粘著的效果。2D平面中,這種粘著效果其實(shí)用css濾鏡就可以輕松實(shí)現(xiàn)。但是到了3D世界,就沒那么簡單了,這時我們就得依靠光照來實(shí)現(xiàn),其中涉及到了一個關(guān)鍵算法——光線步進(jìn)(Ray Marching)。以下是最終實(shí)現(xiàn)的效果圖

撒,哈吉馬路由!
準(zhǔn)備工作
筆者的three.js模板:點(diǎn)擊右下角的fork即可復(fù)制一份 https://codepen.io/alphardex/pen/yLaQdOq
正片
全屏相機(jī)
首先將相機(jī)換成正交相機(jī),再將平面的長度調(diào)整為2,使其填滿屏幕
class?RayMarching?extends?Base?{
??constructor(sel:?string,?debug:?boolean)?{
????super(sel,?debug);
????this.clock?=?new?THREE.Clock();
????this.cameraPosition?=?new?THREE.Vector3(0,?0,?0);
????this.orthographicCameraParams?=?{
??????left:?-1,
??????right:?1,
??????top:?1,
??????bottom:?-1,
??????near:?0,
??????far:?1,
??????zoom:?1
????};
??}
??//?初始化
??init()?{
????this.createScene();
????this.createOrthographicCamera();
????this.createRenderer();
????this.createRayMarchingMaterial();
????this.createPlane();
????this.createLight();
????this.trackMousePos();
????this.addListeners();
????this.setLoop();
??}
??//?創(chuàng)建平面
??createPlane()?{
????const?geometry?=?new?THREE.PlaneBufferGeometry(2,?2,?100,?100);
????const?material?=?this.rayMarchingMaterial;
????this.createMesh({
??????geometry,
??????material
????});
??}
}

創(chuàng)建材質(zhì)
創(chuàng)建好著色器材質(zhì),里面定義好所有要傳遞給著色器的參數(shù)
const?matcapTextureUrl?=?"https://i.loli.net/2021/02/27/7zhBySIYxEqUFW3.png";
class?RayMarching?extends?Base?{
??//?創(chuàng)建光線追蹤材質(zhì)
??createRayMarchingMaterial()?{
????const?loader?=?new?THREE.TextureLoader();
????const?texture?=?loader.load(matcapTextureUrl);
????const?rayMarchingMaterial?=?new?THREE.ShaderMaterial({
??????vertexShader:?rayMarchingVertexShader,
??????fragmentShader:?rayMarchingFragmentShader,
??????side:?THREE.DoubleSide,
??????uniforms:?{
????????uTime:?{
??????????value:?0
????????},
????????uMouse:?{
??????????value:?new?THREE.Vector2(0,?0)
????????},
????????uResolution:?{
??????????value:?new?THREE.Vector2(window.innerWidth,?window.innerHeight)
????????},
????????uTexture:?{
??????????value:?texture
????????},
????????uProgress:?{
??????????value:?1
????????},
????????uVelocityBox:?{
??????????value:?0.25
????????},
????????uVelocitySphere:?{
??????????value:?0.5
????????},
????????uAngle:?{
??????????value:?1.5
????????},
????????uDistance:?{
??????????value:?1.2
????????}
??????}
????});
????this.rayMarchingMaterial?=?rayMarchingMaterial;
??}
}
頂點(diǎn)著色器rayMarchingVertexShader,這個只要用模板現(xiàn)成的就可以了
重點(diǎn)是片元著色器rayMarchingFragmentShader
片元著色器
背景
作為熱身運(yùn)動,先創(chuàng)建一個輻射狀的背景吧
varying vec2 vUv;
vec3 background(vec2 uv){
float dist=length(uv-vec2(.5));
vec3 bg=mix(vec3(.3),vec3(.0),dist);
return bg;
}
void main(){
vec3 bg=background(vUv);
vec3 color=bg;
gl_FragColor=vec4(color,1.);
}

sdf
如何在光照模型中創(chuàng)建物體呢?我們需要sdf。
sdf的意思是符號距離函數(shù):若傳遞給函數(shù)空間中的某個坐標(biāo),則返回那個點(diǎn)與某些平面之間的最短距離,返回值的符號表示點(diǎn)在平面的內(nèi)部還是外部,故稱符號距離函數(shù)。
如果我們要創(chuàng)建一個球,就得用球的sdf來創(chuàng)建。球體方程可以用如下的glsl代碼來表示
float sdSphere(vec3 p,float r)
{
return length(p)-r;
}
方塊的代碼如下
float sdBox(vec3 p,vec3 b)
{
vec3 q=abs(p)-b;
return length(max(q,0.))+min(max(q.x,max(q.y,q.z)),0.);
}
看不懂怎么辦?沒關(guān)系,國外已經(jīng)有大牛把常用的sdf公式都整理出來了
公式:https://www.iquilezles.org/www/articles/distfunctions/distfunctions.htm
在sdf里先創(chuàng)建一個方塊
float sdf(vec3 p){
float box=sdBox(p,vec3(.3));
return box;
}
畫面上仍舊一片空白,因?yàn)槲覀兊募钨e——光線還尚未入場。
光線步進(jìn)
接下來就是本文的頭號人物——光線步進(jìn)了。在介紹她之前,我們先來看看她的好姬友光線追蹤吧。

首先,我們需要知道光線追蹤是如何進(jìn)行的:給相機(jī)一個位置eye,在前面放一個網(wǎng)格,從相機(jī)的位置發(fā)射一束射線ray,穿過網(wǎng)格打在物體上,所成的像的每一個像素對應(yīng)著網(wǎng)格上的每一個點(diǎn)。
而在光線步進(jìn)中,整個場景會由一系列的sdf的角度定義。為了找到場景和視線之間的邊界,我們會從相機(jī)的位置開始,沿著射線,一點(diǎn)一點(diǎn)地移動每個點(diǎn),每一步都會判斷這個點(diǎn)在不在場景的某個表面內(nèi)部,如果在則完成,表示光線擊中了某東西,如果不在則光線繼續(xù)步進(jìn)。

上圖中,p0是相機(jī)位置,藍(lán)色的線代表射線??梢钥闯龉饩€的第一步p0p1就邁的非常大,它也恰好是此時光線到表面的最短距離。表面上的點(diǎn)盡管是最短距離,但并沒有沿著視線的方向,因此要繼續(xù)檢測到p4這個點(diǎn)
shadertoy上有一個可交互的例子:
https://www.shadertoy.com/view/4dKyRz
以下是光線步進(jìn)的glsl代碼實(shí)現(xiàn)
const float EPSILON=.0001;
float rayMarch(vec3 eye,vec3 ray,float end,int maxIter){
float depth=0.;
for(int i=0;i vec3 pos=eye+depth*ray;
float dist=sdf(pos);
depth+=dist;
if(dist=end){
break;
}
}
return depth;
}
在主函數(shù)中創(chuàng)建一條射線,將其投喂給光線步進(jìn)算法,即可獲得光線到表面的最短距離
void main(){
...
vec3 eye=vec3(0.,0.,2.5);
vec3 ray=normalize(vec3(vUv,-eye.z));
float end=5.;
int maxIter=256;
float depth=rayMarch(eye,ray,end,maxIter);
if(depth vec3 pos=eye+depth*ray;
color=pos;
}
...
}

在光線步進(jìn)的引誘下,野生的方塊出現(xiàn)了!
居中材質(zhì)
目前的方塊有2個問題:1. 沒有居中 2. x軸方向上被拉伸
居中+拉伸素質(zhì)2連走起
vec2 centerUv(vec2 uv){
uv=2.*uv-1.;
float aspect=uResolution.x/uResolution.y;
uv.x*=aspect;
return uv;
}
void main(){
...
vec2 cUv=centerUv(vUv);
vec3 ray=normalize(vec3(cUv,-eye.z));
...
}

方塊瞬間飄到了畫面的正中央,但此時的她還沒有顏色
計算表面法線
在光照模型中,我們需要計算出表面法線
https://www.iquilezles.org/www/articles/normalsSDF/normalsSDF.htm
才能給材質(zhì)賦予顏色
vec3 calcNormal(in vec3 p)
{
const float eps=.0001;
const vec2 h=vec2(eps,0);
return normalize(vec3(sdf(p+h.xyy)-sdf(p-h.xyy),
sdf(p+h.yxy)-sdf(p-h.yxy),
sdf(p+h.yyx)-sdf(p-h.yyx)));
}
void main(){
...
if(depth vec3 pos=eye+depth*ray;
vec3 normal=calcNormal(pos);
color=normal;
}
...
}

此時方塊被賦予了藍(lán)色,但我們還看不出她是個立體圖形
動起來
讓方塊360°旋轉(zhuǎn)起來吧,3D旋轉(zhuǎn)函數(shù)直接在gist上搜一下就有了
https://gist.github.com/yiwenl/3f804e80d0930e34a0b33359259b556c
uniform float uVelocityBox;
mat4 rotationMatrix(vec3 axis,float angle){
axis=normalize(axis);
float s=sin(angle);
float c=cos(angle);
float oc=1.-c;
return mat4(oc*axis.x*axis.x+c,oc*axis.x*axis.y-axis.z*s,oc*axis.z*axis.x+axis.y*s,0.,
oc*axis.x*axis.y+axis.z*s,oc*axis.y*axis.y+c,oc*axis.y*axis.z-axis.x*s,0.,
oc*axis.z*axis.x-axis.y*s,oc*axis.y*axis.z+axis.x*s,oc*axis.z*axis.z+c,0.,
0.,0.,0.,1.);
}
vec3 rotate(vec3 v,vec3 axis,float angle){
mat4 m=rotationMatrix(axis,angle);
return(m*vec4(v,1.)).xyz;
}
float sdf(vec3 p){
vec3 p1=rotate(p,vec3(1.),uTime*uVelocityBox);
float box=sdBox(p1,vec3(.3));
return box;
}

融合效果
單單一個方塊太孤單了,創(chuàng)建一個球來陪陪她吧
如何讓球和方塊貼在一起呢,你需要smin這個函數(shù)
https://www.iquilezles.org/www/articles/smin/smin.htm
uniform float uProgress;
float smin(float a,float b,float k)
{
float h=clamp(.5+.5*(b-a)/k,0.,1.);
return mix(b,a,h)-k*h*(1.-h);
}
float sdf(vec3 p){
vec3 p1=rotate(p,vec3(1.),uTime*uVelocityBox);
float box=sdBox(p1,vec3(.3));
float sphere=sdSphere(p,.3);
float sBox=smin(box,sphere,.3);
float mixedBox=mix(sBox,box,uProgress);
return mixedBox;
}
把uProgress的值設(shè)為0,她們成功地貼在了一起

把uProgress的值調(diào)回1,她們又分開了
動態(tài)融合
接下來就是露珠滴落的動畫實(shí)現(xiàn)了,其實(shí)就是對融合圖形應(yīng)用了一個位移變換
uniform float uAngle;
uniform float uDistance;
uniform float uVelocitySphere;
const float PI=3.14159265359;
float movingSphere(vec3 p,float shape){
float rad=uAngle*PI;
vec3 pos=vec3(cos(rad),sin(rad),0.)*uDistance;
vec3 displacement=pos*fract(uTime*uVelocitySphere);
float gotoCenter=sdSphere(p-displacement,.1);
return smin(shape,gotoCenter,.3);
}
float sdf(vec3 p){
vec3 p1=rotate(p,vec3(1.),uTime*uVelocityBox);
float box=sdBox(p1,vec3(.3));
float sphere=sdSphere(p,.3);
float sBox=smin(box,sphere,.3);
float mixedBox=mix(sBox,box,uProgress);
mixedBox=movingSphere(p,mixedBox);
return mixedBox;
}

matcap貼圖
默認(rèn)的材質(zhì)太土了?我們有帥氣的matcap貼圖來助陣
uniform sampler2D uTexture;
vec2 matcap(vec3 eye,vec3 normal){
vec3 reflected=reflect(eye,normal);
float m=2.8284271247461903*sqrt(reflected.z+1.);
return reflected.xy/m+.5;
}
float fresnel(float bias,float scale,float power,vec3 I,vec3 N)
{
return bias+scale*pow(1.+dot(I,N),power);
}
void main(){
...
if(depth vec3 pos=eye+depth*ray;
vec3 normal=calcNormal(pos);
vec2 matcapUv=matcap(ray,normal);
color=texture2D(uTexture,matcapUv).rgb;
float F=fresnel(0.,.4,3.2,ray,normal);
color=mix(color,bg,F);
}
...
}

安排上了matcap和菲涅爾公式后,瞬間cool了有沒有?!
項(xiàng)目地址
Ray Marching Gooey Effect:
https://codepen.io/alphardex/pen/BaQYXvy
--- EOF ---
源自:https://juejin.cn/post/6934461126977519629
聲明:文章著作權(quán)歸作者所有,如有侵權(quán),請聯(lián)系小編刪除
