three.js 实现露珠滴落动画效果的示例代码
前言
大家好,这里是CSS魔法使——alphardex。
本文我们将用three.js来实现一种很酷的光学效果——露珠滴落。我们知道,在露珠从一个物体表面滴落的时候,会产生一种粘着的效果。2D平面中,这种粘着效果其实用css滤镜就可以轻松实现。但是到了3D世界,就没那么简单了,这时我们就得依靠光照来实现,其中涉及到了一个关键算法——光线步进(RayMarching)。以下是最终实现的效果图
撒,哈吉马路由!
准备工作
笔者的three.js模板:点击右下角的fork即可复制一份
正片
全屏相机
首先将相机换成正交相机,再将平面的长度调整为2,使其填满屏幕
classRayMarchingextendsBase{ constructor(sel:string,debug:boolean){ super(sel,debug); this.clock=newTHREE.Clock(); this.cameraPosition=newTHREE.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(); } //创建平面 createPlane(){ constgeometry=newTHREE.PlaneBufferGeometry(2,2,100,100); constmaterial=this.rayMarchingMaterial; this.createMesh({ geometry, material }); } }
创建材质
创建好着色器材质,里面定义好所有要传递给着色器的参数
constmatcapTextureUrl="https://i.loli.net/2021/02/27/7zhBySIYxEqUFW3.png"; classRayMarchingextendsBase{ //创建光线追踪材质 createRayMarchingMaterial(){ constloader=newTHREE.TextureLoader(); consttexture=loader.load(matcapTextureUrl); constrayMarchingMaterial=newTHREE.ShaderMaterial({ vertexShader:rayMarchingVertexShader, fragmentShader:rayMarchingFragmentShader, side:THREE.DoubleSide, uniforms:{ uTime:{ value:0 }, uMouse:{ value:newTHREE.Vector2(0,0) }, uResolution:{ value:newTHREE.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; } }
顶点着色器rayMarchingVertexShader,这个只要用模板现成的就可以了
重点是片元着色器rayMarchingFragmentShader
片元着色器
背景
作为热身运动,先创建一个辐射状的背景吧
varyingvec2vUv; vec3background(vec2uv){ floatdist=length(uv-vec2(.5)); vec3bg=mix(vec3(.3),vec3(.0),dist); returnbg; } voidmain(){ vec3bg=background(vUv); vec3color=bg; gl_FragColor=vec4(color,1.); }
sdf
如何在光照模型中创建物体呢?我们需要sdf。
sdf的意思是符号距离函数:若传递给函数空间中的某个坐标,则返回那个点与某些平面之间的最短距离,返回值的符号表示点在平面的内部还是外部,故称符号距离函数。
如果我们要创建一个球,就得用球的sdf来创建。球体方程可以用如下的glsl代码来表示
floatsdSphere(vec3p,floatr) { returnlength(p)-r; }
方块的代码如下
floatsdBox(vec3p,vec3b) { vec3q=abs(p)-b; returnlength(max(q,0.))+min(max(q.x,max(q.y,q.z)),0.); }
看不懂怎么办?没关系,国外已经有大牛把常用的sdf公式都整理出来了
在sdf里先创建一个方块
floatsdf(vec3p){ floatbox=sdBox(p,vec3(.3)); returnbox; }
画面上仍旧一片空白,因为我们的嘉宾——光线还尚未入场。
光线步进
接下来就是本文的头号人物——光线步进了。在介绍她之前,我们先来看看她的好姬友光线追踪吧。
首先,我们需要知道光线追踪是如何进行的:给相机一个位置eye,在前面放一个网格,从相机的位置发射一束射线ray,穿过网格打在物体上,所成的像的每一个像素对应着网格上的每一个点。
而在光线步进中,整个场景会由一系列的sdf的角度定义。为了找到场景和视线之间的边界,我们会从相机的位置开始,沿着射线,一点一点地移动每个点,每一步都会判断这个点在不在场景的某个表面内部,如果在则完成,表示光线击中了某东西,如果不在则光线继续步进。
上图中,p0是相机位置,蓝色的线代表射线。可以看出光线的第一步p0p1就迈的非常大,它也恰好是此时光线到表面的最短距离。表面上的点尽管是最短距离,但并没有沿着视线的方向,因此要继续检测到p4这个点
shadertoy上有一个可交互的例子
以下是光线步进的glsl代码实现
constfloatEPSILON=.0001; floatrayMarch(vec3eye,vec3ray,floatend,intmaxIter){ floatdepth=0.; for(inti=0;i=end){ break; } } returndepth; }
在主函数中创建一条射线,将其投喂给光线步进算法,即可获得光线到表面的最短距离
voidmain(){ ... vec3eye=vec3(0.,0.,2.5); vec3ray=normalize(vec3(vUv,-eye.z)); floatend=5.; intmaxIter=256; floatdepth=rayMarch(eye,ray,end,maxIter); if(depth在光线步进的引诱下,野生的方块出现了!
居中材质
目前的方块有2个问题:1.没有居中2.x轴方向上被拉伸
居中+拉伸素质2连走起
vec2centerUv(vec2uv){ uv=2.*uv-1.; floataspect=uResolution.x/uResolution.y; uv.x*=aspect; returnuv; } voidmain(){ ... vec2cUv=centerUv(vUv); vec3ray=normalize(vec3(cUv,-eye.z)); ... }方块瞬间飘到了画面的正中央,但此时的她还没有颜色
计算表面法线
在光照模型中,我们需要计算出表面法线,才能给材质赋予颜色
vec3calcNormal(invec3p) { constfloateps=.0001; constvec2h=vec2(eps,0); returnnormalize(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))); } voidmain(){ ... if(depth此时方块被赋予了蓝色,但我们还看不出她是个立体图形
动起来
让方块360°旋转起来吧,3D旋转函数直接在gist上搜一下就有了
uniformfloatuVelocityBox; mat4rotationMatrix(vec3axis,floatangle){ axis=normalize(axis); floats=sin(angle); floatc=cos(angle); floatoc=1.-c; returnmat4(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.); } vec3rotate(vec3v,vec3axis,floatangle){ mat4m=rotationMatrix(axis,angle); return(m*vec4(v,1.)).xyz; } floatsdf(vec3p){ vec3p1=rotate(p,vec3(1.),uTime*uVelocityBox); floatbox=sdBox(p1,vec3(.3)); returnbox; }融合效果
单单一个方块太孤单了,创建一个球来陪陪她吧
如何让球和方块贴在一起呢,你需要smin这个函数
uniformfloatuProgress; floatsmin(floata,floatb,floatk) { floath=clamp(.5+.5*(b-a)/k,0.,1.); returnmix(b,a,h)-k*h*(1.-h); } floatsdf(vec3p){ vec3p1=rotate(p,vec3(1.),uTime*uVelocityBox); floatbox=sdBox(p1,vec3(.3)); floatsphere=sdSphere(p,.3); floatsBox=smin(box,sphere,.3); floatmixedBox=mix(sBox,box,uProgress); returnmixedBox; }把uProgress的值设为0,她们成功地贴在了一起
把uProgress的值调回1,她们又分开了
动态融合
接下来就是露珠滴落的动画实现了,其实就是对融合图形应用了一个位移变换
uniformfloatuAngle; uniformfloatuDistance; uniformfloatuVelocitySphere; constfloatPI=3.14159265359; floatmovingSphere(vec3p,floatshape){ floatrad=uAngle*PI; vec3pos=vec3(cos(rad),sin(rad),0.)*uDistance; vec3displacement=pos*fract(uTime*uVelocitySphere); floatgotoCenter=sdSphere(p-displacement,.1); returnsmin(shape,gotoCenter,.3); } floatsdf(vec3p){ vec3p1=rotate(p,vec3(1.),uTime*uVelocityBox); floatbox=sdBox(p1,vec3(.3)); floatsphere=sdSphere(p,.3); floatsBox=smin(box,sphere,.3); floatmixedBox=mix(sBox,box,uProgress); mixedBox=movingSphere(p,mixedBox); returnmixedBox; }matcap贴图
默认的材质太土了?我们有帅气的matcap贴图来助阵
uniformsampler2DuTexture; vec2matcap(vec3eye,vec3normal){ vec3reflected=reflect(eye,normal); floatm=2.8284271247461903*sqrt(reflected.z+1.); returnreflected.xy/m+.5; } floatfresnel(floatbias,floatscale,floatpower,vec3I,vec3N) { returnbias+scale*pow(1.+dot(I,N),power); } voidmain(){ ... if(depth安排上了matcap和菲涅尔公式后,瞬间cool了有没有?!
项目地址
RayMarchingGooeyEffect
到此这篇关于three.js实现露珠滴落动画效果的示例代码的文章就介绍到这了,更多相关three.js实现露珠滴落动画内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。