经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » JavaScript » 查看文章
three.js 实现露珠滴落动画效果的示例代码
来源:jb51  时间:2021/3/1 12:56:42  对本文有异议

前言

大家好,这里是 CSS 魔法使——alphardex。

本文我们将用three.js来实现一种很酷的光学效果——露珠滴落。我们知道,在露珠从一个物体表面滴落的时候,会产生一种粘着的效果。2D平面中,这种粘着效果其实用css滤镜就可以轻松实现。但是到了3D世界,就没那么简单了,这时我们就得依靠光照来实现,其中涉及到了一个关键算法——光线步进(Ray Marching)。以下是最终实现的效果图

撒,哈吉马路由!

准备工作

笔者的 three.js模板 :点击右下角的fork即可复制一份

正片

全屏相机

首先将相机换成正交相机,再将平面的长度调整为2,使其填满屏幕

  1. class RayMarching extends Base {
  2. constructor(sel: string, debug: boolean) {
  3. super(sel, debug);
  4. this.clock = new THREE.Clock();
  5. this.cameraPosition = new THREE.Vector3(0, 0, 0);
  6. this.orthographicCameraParams = {
  7. left: -1,
  8. right: 1,
  9. top: 1,
  10. bottom: -1,
  11. near: 0,
  12. far: 1,
  13. zoom: 1
  14. };
  15. }
  16. // 初始化
  17. init() {
  18. this.createScene();
  19. this.createOrthographicCamera();
  20. this.createRenderer();
  21. this.createRayMarchingMaterial();
  22. this.createPlane();
  23. this.createLight();
  24. this.trackMousePos();
  25. this.addListeners();
  26. this.setLoop();
  27. }
  28. // 创建平面
  29. createPlane() {
  30. const geometry = new THREE.PlaneBufferGeometry(2, 2, 100, 100);
  31. const material = this.rayMarchingMaterial;
  32. this.createMesh({
  33. geometry,
  34. material
  35. });
  36. }
  37. }

创建材质

创建好着色器材质,里面定义好所有要传递给着色器的参数

  1. const matcapTextureUrl = "https://i.loli.net/2021/02/27/7zhBySIYxEqUFW3.png";
  2.  
  3. class RayMarching extends Base {
  4. // 创建光线追踪材质
  5. createRayMarchingMaterial() {
  6. const loader = new THREE.TextureLoader();
  7. const texture = loader.load(matcapTextureUrl);
  8. const rayMarchingMaterial = new THREE.ShaderMaterial({
  9. vertexShader: rayMarchingVertexShader,
  10. fragmentShader: rayMarchingFragmentShader,
  11. side: THREE.DoubleSide,
  12. uniforms: {
  13. uTime: {
  14. value: 0
  15. },
  16. uMouse: {
  17. value: new THREE.Vector2(0, 0)
  18. },
  19. uResolution: {
  20. value: new THREE.Vector2(window.innerWidth, window.innerHeight)
  21. },
  22. uTexture: {
  23. value: texture
  24. },
  25. uProgress: {
  26. value: 1
  27. },
  28. uVelocityBox: {
  29. value: 0.25
  30. },
  31. uVelocitySphere: {
  32. value: 0.5
  33. },
  34. uAngle: {
  35. value: 1.5
  36. },
  37. uDistance: {
  38. value: 1.2
  39. }
  40. }
  41. });
  42. this.rayMarchingMaterial = rayMarchingMaterial;
  43. }
  44. }

顶点着色器 rayMarchingVertexShader ,这个只要用模板现成的就可以了

重点是片元着色器 rayMarchingFragmentShader

片元着色器

背景

作为热身运动,先创建一个辐射状的背景吧

  1. varying vec2 vUv;
  2.  
  3. vec3 background(vec2 uv){
  4. float dist=length(uv-vec2(.5));
  5. vec3 bg=mix(vec3(.3),vec3(.0),dist);
  6. return bg;
  7. }
  8.  
  9. void main(){
  10. vec3 bg=background(vUv);
  11. vec3 color=bg;
  12. gl_FragColor=vec4(color,1.);
  13. }

sdf

如何在光照模型中创建物体呢?我们需要sdf。

sdf的意思是符号距离函数:若传递给函数空间中的某个坐标,则返回那个点与某些平面之间的最短距离,返回值的符号表示点在平面的内部还是外部,故称符号距离函数。

如果我们要创建一个球,就得用球的sdf来创建。球体方程可以用如下的glsl代码来表示

  1. float sdSphere(vec3 p,float r)
  2. {
  3. return length(p)-r;
  4. }

方块的代码如下

  1. float sdBox(vec3 p,vec3 b)
  2. {
  3. vec3 q=abs(p)-b;
  4. return length(max(q,0.))+min(max(q.x,max(q.y,q.z)),0.);
  5. }

看不懂怎么办?没关系,国外已经有大牛把 常用的sdf公式 都整理出来了

在sdf里先创建一个方块

  1. float sdf(vec3 p){
  2. float box=sdBox(p,vec3(.3));
  3. return box;
  4. }

画面上仍旧一片空白,因为我们的嘉宾——光线还尚未入场。

光线步进

接下来就是本文的头号人物——光线步进了。在介绍她之前,我们先来看看她的好姬友光线追踪吧。

首先,我们需要知道光线追踪是如何进行的:给相机一个位置 eye ,在前面放一个网格,从相机的位置发射一束射线 ray ,穿过网格打在物体上,所成的像的每一个像素对应着网格上的每一个点。

而在光线步进中,整个场景会由一系列的sdf的角度定义。为了找到场景和视线之间的边界,我们会从相机的位置开始,沿着射线,一点一点地移动每个点,每一步都会判断这个点在不在场景的某个表面内部,如果在则完成,表示光线击中了某东西,如果不在则光线继续步进。

上图中,p0是相机位置,蓝色的线代表射线。可以看出光线的第一步p0p1就迈的非常大,它也恰好是此时光线到表面的最短距离。表面上的点尽管是最短距离,但并没有沿着视线的方向,因此要继续检测到p4这个点

shadertoy上有一个 可交互的例子

以下是光线步进的glsl代码实现

  1. const float EPSILON=.0001;
  2.  
  3. float rayMarch(vec3 eye,vec3 ray,float end,int maxIter){
  4. float depth=0.;
  5. for(int i=0;i<maxIter;i++){
  6. vec3 pos=eye+depth*ray;
  7. float dist=sdf(pos);
  8. depth+=dist;
  9. if(dist<EPSILON||dist>=end){
  10. break;
  11. }
  12. }
  13. return depth;
  14. }

在主函数中创建一条射线,将其投喂给光线步进算法,即可获得光线到表面的最短距离

  1. void main(){
  2. ...
  3. vec3 eye=vec3(0.,0.,2.5);
  4. vec3 ray=normalize(vec3(vUv,-eye.z));
  5. float end=5.;
  6. int maxIter=256;
  7. float depth=rayMarch(eye,ray,end,maxIter);
  8. if(depth<end){
  9. vec3 pos=eye+depth*ray;
  10. color=pos;
  11. }
  12. ...
  13. }

在光线步进的引诱下,野生的方块出现了!

居中材质

目前的方块有2个问题:1. 没有居中 2. x轴方向上被拉伸

居中+拉伸素质2连走起

  1. vec2 centerUv(vec2 uv){
  2. uv=2.*uv-1.;
  3. float aspect=uResolution.x/uResolution.y;
  4. uv.x*=aspect;
  5. return uv;
  6. }
  7.  
  8. void main(){
  9. ...
  10. vec2 cUv=centerUv(vUv);
  11. vec3 ray=normalize(vec3(cUv,-eye.z));
  12. ...
  13. }

方块瞬间飘到了画面的正中央,但此时的她还没有颜色

计算表面法线

在光照模型中,我们需要 计算出表面法线 ,才能给材质赋予颜色

  1. vec3 calcNormal(in vec3 p)
  2. {
  3. const float eps=.0001;
  4. const vec2 h=vec2(eps,0);
  5. return normalize(vec3(sdf(p+h.xyy)-sdf(p-h.xyy),
  6. sdf(p+h.yxy)-sdf(p-h.yxy),
  7. sdf(p+h.yyx)-sdf(p-h.yyx)));
  8. }
  9.  
  10. void main(){
  11. ...
  12. if(depth<end){
  13. vec3 pos=eye+depth*ray;
  14. vec3 normal=calcNormal(pos);
  15. color=normal;
  16. }
  17. ...
  18. }

此时方块被赋予了蓝色,但我们还看不出她是个立体图形

动起来

让方块360°旋转起来吧,3D旋转函数直接在 gist 上搜一下就有了

  1. uniform float uVelocityBox;
  2.  
  3. mat4 rotationMatrix(vec3 axis,float angle){
  4. axis=normalize(axis);
  5. float s=sin(angle);
  6. float c=cos(angle);
  7. float oc=1.-c;
  8. 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.,
  9. oc*axis.x*axis.y+axis.z*s,oc*axis.y*axis.y+c,oc*axis.y*axis.z-axis.x*s,0.,
  10. oc*axis.z*axis.x-axis.y*s,oc*axis.y*axis.z+axis.x*s,oc*axis.z*axis.z+c,0.,
  11. 0.,0.,0.,1.);
  12. }
  13.  
  14. vec3 rotate(vec3 v,vec3 axis,float angle){
  15. mat4 m=rotationMatrix(axis,angle);
  16. return(m*vec4(v,1.)).xyz;
  17. }
  18.  
  19. float sdf(vec3 p){
  20. vec3 p1=rotate(p,vec3(1.),uTime*uVelocityBox);
  21. float box=sdBox(p1,vec3(.3));
  22. return box;
  23. }

融合效果

单单一个方块太孤单了,创建一个球来陪陪她吧

如何让球和方块贴在一起呢,你需要 smin 这个函数

  1. uniform float uProgress;
  2.  
  3. float smin(float a,float b,float k)
  4. {
  5. float h=clamp(.5+.5*(b-a)/k,0.,1.);
  6. return mix(b,a,h)-k*h*(1.-h);
  7. }
  8.  
  9. float sdf(vec3 p){
  10. vec3 p1=rotate(p,vec3(1.),uTime*uVelocityBox);
  11. float box=sdBox(p1,vec3(.3));
  12. float sphere=sdSphere(p,.3);
  13. float sBox=smin(box,sphere,.3);
  14. float mixedBox=mix(sBox,box,uProgress);
  15. return mixedBox;
  16. }

uProgress 的值设为0,她们成功地贴在了一起

uProgress 的值调回1,她们又分开了

动态融合

接下来就是露珠滴落的动画实现了,其实就是对融合图形应用了一个位移变换

  1. uniform float uAngle;
  2. uniform float uDistance;
  3. uniform float uVelocitySphere;
  4.  
  5. const float PI=3.14159265359;
  6.  
  7. float movingSphere(vec3 p,float shape){
  8. float rad=uAngle*PI;
  9. vec3 pos=vec3(cos(rad),sin(rad),0.)*uDistance;
  10. vec3 displacement=pos*fract(uTime*uVelocitySphere);
  11. float gotoCenter=sdSphere(p-displacement,.1);
  12. return smin(shape,gotoCenter,.3);
  13. }
  14.  
  15. float sdf(vec3 p){
  16. vec3 p1=rotate(p,vec3(1.),uTime*uVelocityBox);
  17. float box=sdBox(p1,vec3(.3));
  18. float sphere=sdSphere(p,.3);
  19. float sBox=smin(box,sphere,.3);
  20. float mixedBox=mix(sBox,box,uProgress);
  21. mixedBox=movingSphere(p,mixedBox);
  22. return mixedBox;
  23. }

matcap贴图

默认的材质太土了?我们有帅气的matcap贴图来助阵

  1. uniform sampler2D uTexture;
  2.  
  3. vec2 matcap(vec3 eye,vec3 normal){
  4. vec3 reflected=reflect(eye,normal);
  5. float m=2.8284271247461903*sqrt(reflected.z+1.);
  6. return reflected.xy/m+.5;
  7. }
  8.  
  9. float fresnel(float bias,float scale,float power,vec3 I,vec3 N)
  10. {
  11. return bias+scale*pow(1.+dot(I,N),power);
  12. }
  13.  
  14. void main(){
  15. ...
  16. if(depth<end){
  17. vec3 pos=eye+depth*ray;
  18. vec3 normal=calcNormal(pos);
  19. vec2 matcapUv=matcap(ray,normal);
  20. color=texture2D(uTexture,matcapUv).rgb;
  21. float F=fresnel(0.,.4,3.2,ray,normal);
  22. color=mix(color,bg,F);
  23. }
  24. ...
  25. }

安排上了matcap和菲涅尔公式后,瞬间cool了有没有?!

项目地址

Ray Marching Gooey Effect

到此这篇关于three.js 实现露珠滴落动画效果的示例代码的文章就介绍到这了,更多相关three.js 实现露珠滴落动画内容请搜索w3xue以前的文章或继续浏览下面的相关文章希望大家以后多多支持w3xue!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号