three.js 實(shí)現(xiàn)露珠滴落動(dòng)畫效果的示例代碼
前言
大家好,這里是 CSS 魔法使——alphardex。
本文我們將用three.js來(lái)實(shí)現(xiàn)一種很酷的光學(xué)效果——露珠滴落。我們知道,在露珠從一個(gè)物體表面滴落的時(shí)候,會(huì)產(chǎn)生一種粘著的效果。2D平面中,這種粘著效果其實(shí)用css濾鏡就可以輕松實(shí)現(xiàn)。但是到了3D世界,就沒那么簡(jiǎn)單了,這時(shí)我們就得依靠光照來(lái)實(shí)現(xiàn),其中涉及到了一個(gè)關(guān)鍵算法——光線步進(jìn)(Ray Marching)。以下是最終實(shí)現(xiàn)的效果圖
撒,哈吉馬路由!
準(zhǔn)備工作
筆者的 three.js模板 :點(diǎn)擊右下角的fork即可復(fù)制一份
正片
全屏相機(jī)
首先將相機(jī)換成正交相機(jī),再將平面的長(zhǎng)度調(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
,這個(gè)只要用模板現(xiàn)成的就可以了
重點(diǎn)是片元著色器 rayMarchingFragmentShader
片元著色器
背景
作為熱身運(yùn)動(dòng),先創(chuàng)建一個(gè)輻射狀的背景吧
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的意思是符號(hào)距離函數(shù):若傳遞給函數(shù)空間中的某個(gè)坐標(biāo),則返回那個(gè)點(diǎn)與某些平面之間的最短距離,返回值的符號(hào)表示點(diǎn)在平面的內(nèi)部還是外部,故稱符號(hào)距離函數(shù)。
如果我們要?jiǎng)?chuàng)建一個(gè)球,就得用球的sdf來(lái)創(chuàng)建。球體方程可以用如下的glsl代碼來(lái)表示
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)系,國(guó)外已經(jīng)有大牛把 常用的sdf公式 都整理出來(lái)了
在sdf里先創(chuàng)建一個(gè)方塊
float sdf(vec3 p){ float box=sdBox(p,vec3(.3)); return box; }
畫面上仍舊一片空白,因?yàn)槲覀兊募钨e——光線還尚未入場(chǎng)。
光線步進(jìn)
接下來(lái)就是本文的頭號(hào)人物——光線步進(jìn)了。在介紹她之前,我們先來(lái)看看她的好姬友光線追蹤吧。
首先,我們需要知道光線追蹤是如何進(jìn)行的:給相機(jī)一個(gè)位置 eye
,在前面放一個(gè)網(wǎng)格,從相機(jī)的位置發(fā)射一束射線 ray
,穿過網(wǎng)格打在物體上,所成的像的每一個(gè)像素對(duì)應(yīng)著網(wǎng)格上的每一個(gè)點(diǎn)。
而在光線步進(jìn)中,整個(gè)場(chǎng)景會(huì)由一系列的sdf的角度定義。為了找到場(chǎng)景和視線之間的邊界,我們會(huì)從相機(jī)的位置開始,沿著射線,一點(diǎn)一點(diǎn)地移動(dòng)每個(gè)點(diǎn),每一步都會(huì)判斷這個(gè)點(diǎn)在不在場(chǎng)景的某個(gè)表面內(nèi)部,如果在則完成,表示光線擊中了某東西,如果不在則光線繼續(xù)步進(jìn)。
上圖中,p0是相機(jī)位置,藍(lán)色的線代表射線??梢钥闯龉饩€的第一步p0p1就邁的非常大,它也恰好是此時(shí)光線到表面的最短距離。表面上的點(diǎn)盡管是最短距離,但并沒有沿著視線的方向,因此要繼續(xù)檢測(cè)到p4這個(gè)點(diǎn)
shadertoy上有一個(gè) 可交互的例子
以下是光線步進(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<maxIter;i++){ vec3 pos=eye+depth*ray; float dist=sdf(pos); depth+=dist; if(dist<EPSILON||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<end){ vec3 pos=eye+depth*ray; color=pos; } ... }
在光線步進(jìn)的引誘下,野生的方塊出現(xiàn)了!
居中材質(zhì)
目前的方塊有2個(gè)問題: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)); ... }
方塊瞬間飄到了畫面的正中央,但此時(shí)的她還沒有顏色
計(jì)算表面法線
在光照模型中,我們需要 計(jì)算出表面法線 ,才能給材質(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<end){ vec3 pos=eye+depth*ray; vec3 normal=calcNormal(pos); color=normal; } ... }
此時(shí)方塊被賦予了藍(lán)色,但我們還看不出她是個(gè)立體圖形
動(dòng)起來(lái)
讓方塊360°旋轉(zhuǎn)起來(lái)吧,3D旋轉(zhuǎn)函數(shù)直接在 gist 上搜一下就有了
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; }
融合效果
單單一個(gè)方塊太孤單了,創(chuàng)建一個(gè)球來(lái)陪陪她吧
如何讓球和方塊貼在一起呢,你需要 smin 這個(gè)函數(shù)
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,她們又分開了
動(dòng)態(tài)融合
接下來(lái)就是露珠滴落的動(dòng)畫實(shí)現(xiàn)了,其實(shí)就是對(duì)融合圖形應(yīng)用了一個(gè)位移變換
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貼圖來(lái)助陣
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<end){ 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)目地址
到此這篇關(guān)于three.js 實(shí)現(xiàn)露珠滴落動(dòng)畫效果的示例代碼的文章就介紹到這了,更多相關(guān)three.js 實(shí)現(xiàn)露珠滴落動(dòng)畫內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript array常用方法代碼實(shí)例詳解
這篇文章主要介紹了JavaScript array常用方法代碼實(shí)例詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09js實(shí)現(xiàn)模擬計(jì)算器退格鍵刪除文字效果的方法
這篇文章主要介紹了js實(shí)現(xiàn)模擬計(jì)算器退格鍵刪除文字效果的方法,涉及javascript字符串截取操作的相關(guān)技巧,需要的朋友可以參考下2015-05-05JavaScript中的toString()和toLocaleString()方法的區(qū)別
本文給大家介紹JavaScript中的toString()和toLocaleString()方法的區(qū)別,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-02-02JavaScript字符串處理之trim()和replace()詳解
這篇文章主要給大家介紹了關(guān)于JavaScript字符串處理之trim()和replace()的相關(guān)資料,文中介紹的所有這些方法都不會(huì)修改原始字符串,而是返回一個(gè)新的字符串,需要的朋友可以參考下2024-10-10利用js判斷瀏覽器類型(是否為IE,Firefox,Opera瀏覽器)
我們開發(fā)的人來(lái)說經(jīng)常要加個(gè)判斷,要不可能某些功能沒法正常使用。要是沒加個(gè)判斷就會(huì)給大家?guī)?lái)些麻煩2013-11-11JavaScript?ES6中class定義類實(shí)例方法
ES6提供了更接近面向?qū)ο?注意:javascript本質(zhì)上是基于對(duì)象的語(yǔ)言)語(yǔ)言的寫法,引入了Class(類)這個(gè)概念,作為對(duì)象的模板,下面這篇文章主要給大家介紹了關(guān)于JavaScript?ES6中class定義類的相關(guān)資料,需要的朋友可以參考下2022-07-07原生js實(shí)現(xiàn)class的添加和刪除簡(jiǎn)單代碼
下面小編就為大家?guī)?lái)一篇原生js實(shí)現(xiàn)class的添加和刪除簡(jiǎn)單代碼。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2016-07-07uniapp開發(fā)小程序?qū)崿F(xiàn)全局懸浮按鈕的代碼
這篇文章主要介紹了uniapp開發(fā)小程序如何實(shí)現(xiàn)全局懸浮按鈕,但是在uniapp中式?jīng)]有window對(duì)象,和dom元素的,需要獲取頁(yè)面上節(jié)點(diǎn)的幾何信息,具體實(shí)例代碼詳細(xì)跟隨小編一起看看吧2022-03-03