如何在 THREE.js 中为可旋转的毛茸茸的球制作动画?

How to animate a rotatable fluffy ball in THREE.js?

创意来自

Instagram @sennepldn

Fluffy ball demo (from Instagram)

Fluffy ball demo (If you can't access Instagram Link)

三天前看到这个例子的时候,非常惊讶,于是想挑战一下自己,尝试做出这个效果。

三天后,我尝试了很多方法,比如使用vertex shader或者GPUComputationRenderer,还是没有做出正确的模拟。所以最后还是决定上来问问大家


我的解决方案

第 1 步

首先用下面两个看起来有硬刺的球来模拟Demo中的Fluffy Ball

从第一帧到第二帧,小球顺时针旋转了大约30度(我随机假设了一个角度)

第 2 步

将上球简化成下图

图中的黑线代表上图中最长的尖峰。

A1,a2,b1,b2(Vector3)表示不同时刻a点和b点的位置

但是最后模拟出来的是软毛而不是硬刺,所以Frame2 b点的位置应该是b3。

第 3 步

所以打算用下面的方法模拟一下

伪造代码(在顶点着色器中)(b1、b2、b3 表示 vec3 中的位置)

b3 = mix(b1, b2, 0.9);

为了读取上一帧b点(b1)的位置,我把上一帧的modelMatrix保存下来,统一传给下一帧。

在下一帧,我们可以通过modelMatrix * vec4(position, 1.0)得到b1世界位置,但是这种方法是不正确的。

比如第3帧,如果我们要读取最后一帧的b点位置。这种方法会得到b2而不是b3,但是实际模拟需要b3。

有什么方法可以正确获取最后一帧b点的位置吗?

我也试过用GPUComputationRenderer保存上一帧的位置,放到一个Texture里

不过可能是我对GPGPU的理解还不够透彻,不知道怎么把上一帧的世界坐标放到贴图里让下一帧读取,所以这个方法没有成功。


总结我的问题

  1. 如何在 THREE.js GPGpu 中读取最后一帧世界位置?步骤是什么?

  2. 有没有更好的方法来做这个模拟?即使这与我的想法无关。例如,模拟真实世界的力。

我的想法是有几个 等距点的同心球层 像这样:

  • Make a sphere with equidistant vertices

其中每一层都有自己的变换矩阵。开始时所有层都具有相同的 并且在每一帧(或模拟计时器滴答)之后传递矩阵(如循环环形缓冲区)因此最低半径层具有实际矩阵,下一层具有前一帧的矩阵等等。它基本上是 运动模糊的几何版本...

然而,我基于这些尝试在片段着色器(作为合并粒子)中进行光线追踪:

  • ray and ellipsoid intersection accuracy improvement
  • Atmospheric scattering GLSL fragment shader
  • raytrace through 3D mesh

在精确度和其他数学边缘情况和舍入问题上碰壁,导致丑陋的工件,调试(如果可能的话)将永远花费时间,移植到 64 位将解决部分问题...

所以我决定尝试在每帧的基础上为此创建几何数据,这在几何着色器中可能是可行的(稍后)。因此,首先我尝试在 CPU 方面执行此操作(C++ 和旧 GL api 现在只是为了简单起见并作为概念证明)

所以我得到了用于渲染图层的单位球面上的等距点列表(静态点)。所以每个点都被转化为线带,其中点只是按图层半径缩放并按其所属图层的矩阵进行变换。这里的 C++ 代码:

//---------------------------------------------------------------------------
// just some vector and matrix math needed for this
void  vectorf_mul(float *c,float *a,float  b) { for(int i=0;i<3;i++) c[i]=a[i]*b; } // c[3] = a[3]*b
void  matrixf_mul_vector(float *c,float *a,float *b)    // c[3] = a[16]*b[3]
    {
    float q[3];
    q[0]=(a[ 0]*b[0])+(a[ 4]*b[1])+(a[ 8]*b[2])+(a[12]);
    q[1]=(a[ 1]*b[0])+(a[ 5]*b[1])+(a[ 9]*b[2])+(a[13]);
    q[2]=(a[ 2]*b[0])+(a[ 6]*b[1])+(a[10]*b[2])+(a[14]);
    for(int i=0;i<3;i++) c[i]=q[i];
    }
//---------------------------------------------------------------------------
// equidistant sphere points
const int sphere_na=20;                     // number of slices (latitude)
float sphere_pnt[sphere_na*sphere_na*6];    // equidistant sphere points
int sphere_n=0;                             // 3 * number of points
// create list of equidistant sphere points r=1 center=(0,0,0)
void sphere_init()
    {
    int ia,ib,na=sphere_na,nb;
    float x,y,z,r;
    float a,b,da,db;
    da=M_PI/float(na-1);                    // latitude angle step
    sphere_n=0;
    for (a=-0.5*M_PI,ia=0;ia<na;ia++,a+=da) // slice sphere to circles in xy planes
        {
        r=cos(a);                       // radius of actual circle in xy plane
        z=sin(a);                       // height of actual circle in xy plane
        nb=ceil(2.0*M_PI*r/da);
        if ((ia==0)||(ia==na-1)) { nb=1; db=0.0; }  // handle edge cases
        db=2.0*M_PI/float(nb);             // longitude angle step
        for (b=0.0,ib=0;ib<nb;ib++,b+=db)   // cut circle to vertexes
            {
            x=r*cos(b);                     // compute x,y of vertex
            y=r*sin(b);
            sphere_pnt[sphere_n]=x; sphere_n++;
            sphere_pnt[sphere_n]=y; sphere_n++;
            sphere_pnt[sphere_n]=z; sphere_n++;
            }
        }
    }
// render sphere as lines from center to surface
void sphere_draw()
    {
    int i;
    glBegin(GL_LINES);
    for (i=0;i<sphere_n;i+=3)
        {
        glColor3f(0.0,0.0,0.0); glVertex3f(0.0,0.0,0.0);
        glColor3f(0.5,0.6,0.7); glVertex3fv(sphere_pnt+i);
        }
    glEnd();
    }
//---------------------------------------------------------------------------
// puff ball
const int puff_n=8;                     // number of layers
float *puff_m[puff_n]={NULL};           // transform matrix for each layer
float puff_matrices[puff_n*16];         // transform matrix for each layer
float puff_col[puff_n][3];              // color for each layer
// render sphere as spicules dived to layers
void puff_draw(float r0,float r1)
    {
    int i,j;
    float p[3],*p0,r,dr=(r1-r0)/float(puff_n);
    glColor3f(0.5,0.6,0.7);
    for (i=0;i<sphere_n;i+=3)
        {
        p0=sphere_pnt+i;
        glBegin(GL_LINE_STRIP);
        for (r=r0,j=0;j<puff_n;j++,r+=dr)
            {
            vectorf_mul(p,p0,r);
            matrixf_mul_vector(p,puff_m[j],p);
            glColor3fv(puff_col[j]);
            glVertex3fv(p);
            }
        glEnd();
        }
    }
// update matrices
void puff_update()
    {
    int i;
    float *p,t;
    if (puff_m[0]==NULL)                    // in first pass all the matrices are actual
        {
        for (i=0;i<puff_n;i++)
            {
            puff_m[i]=puff_matrices+(i<<4);
            glGetFloatv(GL_MODELVIEW_MATRIX,puff_m[i]);
            t=1.0-(float(i)/float(puff_n-1));
            puff_col[i][0]=0.1+0.3*t;
            puff_col[i][1]=0.2+0.5*t;
            puff_col[i][2]=0.2+0.6*t;
            }
        return;
        }
    p=puff_m[puff_n-1];                     // insert actual matrix to ring FIFO buffer
    for (i=puff_n-1;i>0;i--) puff_m[i]=puff_m[i-1];
    puff_m[0]=p;
    glGetFloatv(GL_MODELVIEW_MATRIX,puff_m[0]);
    }
//---------------------------------------------------------------------------

和用法:

// init
sphere_init();

// render
glEnable(GL_DEPTH_TEST);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0.0,0.0,-5.0);  // this is just putting ball before my perspective camera
glRotatef(animt,0.0,0.3,0.5); // animt is just changing angle updated in timer
puff_update();  // this will update the matrices in layers (must be called before first draw)
glMatrixMode(GL_MODELVIEW); // however render itself is using unit matrix !!!
glLoadIdentity();
puff_draw(0.5,1.0); // render the hairs with radiuses from 0.5 up to 1.0

此处预览(头发数量足够少以实际看到几何图形):

如您所见,毛发在正确的方向上弯曲(它们也受速度影响,这也应该受到平移的影响,而不仅仅是旋转作为奖励)所以作为概念证明,这种方法是有效的。如果您希望它们弯曲得更多,只需降低 r0,r1 半径之间的差异即可。

如您所见,这应该可以轻松移植到 GLSL 几何着色器。 CPU 只需传递一次单位球体点,几何体将以相同的方式为每个点发出一条线带。您只需要将矩阵作为制服即可。

这里是另一个改变旋转和降低 r0,r1 差异的预览:

除了调整点数 sphere_na 和层数 puff_n 之外,还可以添加这些来改进:

  • 非线性分布r0,r1之间的层以实现不同的look/behavior
  • 添加球体表面(r0 这样你就看不到它后面的毛发了)
  • 纹理
  • 运动模糊
  • 可变头发宽度(因此在底部更宽)
  • 灯光模型(可能只是针对表面而不是毛发本身)

我认为这可能用于改进(曲率和可变宽度):

[Edit1] 一些调整和几何着色器

我成功地将它移植到着色器并添加了一些东西,比如颜色、线宽和内球表面。这里是完整的 VCL/C++/OpenGL/GLSL 代码:

VCL 应用程序 window(忽略 VCL 的东西)

//---------------------------------------------------------------------------
#include <vcl.h>
#include <math.h>
#pragma hdrstop
#include "Unit1.h"
#include "gl_simple.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
float animx=0.0;
float animy=0.0;
bool _glsl=true;
//---------------------------------------------------------------------------
// just some vector and matrix math needed for this
void  vectorf_mul(float *c,float *a,float  b) { for(int i=0;i<3;i++) c[i]=a[i]*b; } // c[3] = a[3]*b
void  matrixf_mul_vector(float *c,float *a,float *b)    // c[3] = a[16]*b[3]
    {
    float q[3];
    q[0]=(a[ 0]*b[0])+(a[ 4]*b[1])+(a[ 8]*b[2])+(a[12]);
    q[1]=(a[ 1]*b[0])+(a[ 5]*b[1])+(a[ 9]*b[2])+(a[13]);
    q[2]=(a[ 2]*b[0])+(a[ 6]*b[1])+(a[10]*b[2])+(a[14]);
    for(int i=0;i<3;i++) c[i]=q[i];
    }
//---------------------------------------------------------------------------
// equidistant sphere points
const int sphere_na=30;                     // number of slices (latitude)
float sphere_pnt[sphere_na*sphere_na*4];    // equidistant sphere points (approx size <= regular grid size / 1.5)
int sphere_n=0;                             // 3 * number of points
int sphere_slc[sphere_na+1];                // start index of each slice
// create list of equidistant sphere points r=1 center=(0,0,0)
void sphere_init()
    {
    int ia,ib,na=sphere_na,nb;
    float x,y,z,r;
    float a,b,da,db;
    da=M_PI/float(na-1);                    // latitude angle step
    sphere_n=0;
    for (a=-0.5*M_PI,ia=0;ia<na;ia++,a+=da) // slice sphere to circles in xy planes
        {
        sphere_slc[ia]=sphere_n;
        r=cos(a);                       // radius of actual circle in xy plane
        z=sin(a);                       // height of actual circle in xy plane
        nb=ceil(2.0*M_PI*r/da);
        if ((ia==0)||(ia==na-1)) { nb=1; db=0.0; }  // handle edge cases
        db=2.0*M_PI/float(nb);             // longitude angle step
        for (b=0.0,ib=0;ib<nb;ib++,b+=db)   // cut circle to vertexes
            {
            x=r*cos(b);                     // compute x,y of vertex
            y=r*sin(b);
            sphere_pnt[sphere_n]=x; sphere_n++;
            sphere_pnt[sphere_n]=y; sphere_n++;
            sphere_pnt[sphere_n]=z; sphere_n++;
            }
        }
    sphere_slc[na]=sphere_n;
    }
void sphere_gl_draw(float r)
    {
    int i,i0,i1,i2,n,n0,n1,j,k;
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glScalef(r,r,r);
    glBegin(GL_TRIANGLE_STRIP);
    for (k=1;k<sphere_na;k++)
        {
        i0=sphere_slc[k-1];
        i1=sphere_slc[k+0];
        i2=sphere_slc[k+1];
        n0=(i1-i0)/3;
        n1=(i2-i1)/3;
        n=n0; if (n<n1) n=n1;
        for (i=0;i<n;i++)
            {
            j=i0+(3*((i*n0)/n));
            glNormal3fv(sphere_pnt+j);
            glVertex3fv(sphere_pnt+j);
            j=i1+(3*((i*n1)/n));
            glNormal3fv(sphere_pnt+j);
            glVertex3fv(sphere_pnt+j);
            }
        }
    glEnd();
    glPopMatrix();
    }
//---------------------------------------------------------------------------
// puff ball
float r0=0.80,r1=1.0;                   // hair radiuses
const int puff_n=16;                        // number of layers (the more the more its bendy like)
float *puff_m[puff_n]={NULL};           // transform matrix for each layer
float puff_matrices[puff_n*16];         // transform matrix for each layer
float puff_col[puff_n*3];               // color for each layer
// render sphere as spicules dived to layers
void puff_gl_draw(float r0,float r1)
    {
    int i,j;
    float p[3],*p0,r,dr=r1-r0;
    float t,dt=1.0/float(puff_n);
    glColor3f(0.5,0.6,0.7);
    for (i=0;i<sphere_n;i+=3)
        {
        p0=sphere_pnt+i;
        glBegin(GL_LINE_STRIP);
        for (t=0.0,j=0;j<puff_n;j++,t+=dt)
            {
            r=r0+t*dr;
            vectorf_mul(p,p0,r);
            matrixf_mul_vector(p,puff_m[j],p);
            glColor3fv(puff_col+j+j+j);
            glVertex3fv(p);
            }
        glEnd();
        }
    }
void puff_glsl_draw(float r0,float r1)
    {
    int i;
    glBegin(GL_POINTS);
    for (i=0;i<sphere_n;i+=3)
        {
        glVertex3fv(sphere_pnt+i);
        }
    glEnd();
    }
// update matrices
void puff_update()
    {
    int i;
    float *p,t;
    if (puff_m[0]==NULL)                    // in first pass all the matrices are actual
        {
        for (i=0;i<puff_n;i++)
            {
            puff_m[i]=puff_matrices+(i<<4);
            glGetFloatv(GL_MODELVIEW_MATRIX,puff_m[i]);
            t=float(i)/float(puff_n-1);
            puff_col[i+i+i+0]=0.1+0.1*t;
            puff_col[i+i+i+1]=0.1+0.2*t;
            puff_col[i+i+i+2]=0.1+0.4*t;
            }
        return;
        }
    p=puff_m[puff_n-1];                     // insert actual matrix to ring FIFO buffer
    for (i=puff_n-1;i>0;i--) puff_m[i]=puff_m[i-1];
    puff_m[0]=p;
    glGetFloatv(GL_MODELVIEW_MATRIX,puff_m[0]);
    }
//---------------------------------------------------------------------------
void gl_draw()
    {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glEnable(GL_DEPTH_TEST);

    // animate matrix
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glTranslatef(0.0,0.0,-10.0*r1);
    glRotatef(animx,1.0,0.0,0.0);
    glRotatef(animy,0.0,1.0,0.0);
    puff_update();

    glEnable(GL_LIGHTING);
    glEnable(GL_LIGHT0);
    glEnable(GL_COLOR_MATERIAL);
    glColor3f(0.4,0.2,0.1);
    sphere_gl_draw(r0);
    glDisable(GL_LIGHTING);
    glDisable(GL_COLOR_MATERIAL);

    glLineWidth(3.0);
    if (_glsl)
        {
        GLint id;
        glUseProgram(prog_id);
        id=glGetUniformLocation(prog_id,"r0"); glUniform1f(id,r0);
        id=glGetUniformLocation(prog_id,"r1"); glUniform1f(id,r1);
        id=glGetUniformLocation(prog_id,"n" ); glUniform1i(id,puff_n);
        // pass ring FIFO of matrices in their order
        // can be improved by passing start index instead of reordering
        int i,j,k;
        float m[16*puff_n];
        for (k=0,i=0;i<puff_n;i++)
         for (j=0;j<16;j++,k++)
          m[k]=puff_m[i][j];
        id=glGetUniformLocation(prog_id,"mv"); glUniformMatrix4fv(id,puff_n,false,m);
        glGetFloatv(GL_PROJECTION_MATRIX,m);
        id=glGetUniformLocation(prog_id,"mp"); glUniformMatrix4fv(id,1,false,m);
        id=glGetUniformLocation(prog_id,"c" );  glUniform3fv(id,puff_n,puff_col);

        puff_glsl_draw(r0,r1);
        glUseProgram(0);
        }
    else{
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
        puff_gl_draw(r0,r1);
        }
    glLineWidth(1.0);

    glFlush();
    SwapBuffers(hdc);
    }
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner):TForm(Owner)
    {
    gl_init(Handle);

    int hnd,siz; char vertex[4096],geom[4096],fragment[4096];
    hnd=FileOpen("puff.glsl_vert",fmOpenRead); siz=FileSeek(hnd,0,2); FileSeek(hnd,0,0); FileRead(hnd,vertex  ,siz); vertex  [siz]=0; FileClose(hnd);
    hnd=FileOpen("puff.glsl_geom",fmOpenRead); siz=FileSeek(hnd,0,2); FileSeek(hnd,0,0); FileRead(hnd,geom    ,siz); geom    [siz]=0; FileClose(hnd);
    hnd=FileOpen("puff.glsl_frag",fmOpenRead); siz=FileSeek(hnd,0,2); FileSeek(hnd,0,0); FileRead(hnd,fragment,siz); fragment[siz]=0; FileClose(hnd);
    glsl_init(vertex,geom,fragment);
//  hnd=FileCreate("GLSL.txt"); FileWrite(hnd,glsl_log,glsl_logs); FileClose(hnd);

    int i0,i;
    mm_log->Lines->Clear();
    for (i=i0=0;i<glsl_logs;i++)
     if ((glsl_log[i]==13)||(glsl_log[i]==10))
        {
        glsl_log[i]=0;
        mm_log->Lines->Add(glsl_log+i0);
        glsl_log[i]=13;
        for (;((glsl_log[i]==13)||(glsl_log[i]==10))&&(i<glsl_logs);i++);
        i0=i;
        }
    if (i0<glsl_logs) mm_log->Lines->Add(glsl_log+i0);

    sphere_init();
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
    {
    gl_exit();
    glsl_exit();
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormResize(TObject *Sender)
    {
    gl_resize(ClientWidth,ClientHeight-mm_log->Height);
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormPaint(TObject *Sender)
    {
    gl_draw();
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::Timer1Timer(TObject *Sender)
    {
    gl_draw();
    animx+=1.5; if (animx>=360.0) animx=-360.0;
    animy+=2.5; if (animy>=360.0) animy=-360.0;
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormMouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y)
    {
    _glsl=!_glsl;
    }
//---------------------------------------------------------------------------

gl_simple.h可以在这里找到:

然而那里发布的版本是旧的(我使用的那个已经添加了几何着色器支持......)这里的着色器:

// Vertex
#version 400 core
in  vec3 pos;
void main()
    {
    gl_Position=vec4(pos,1.0);
    }
// Geometry
#version 400 core
layout(points) in;
layout(line_strip, max_vertices = 32) out;

uniform int n;          // hair layers
uniform float r0;       // min radius for hairs
uniform float r1;       // max radius for hairs
uniform mat4 mp;        // global projection
uniform mat4 mv[32];    // hair modelview per layer
uniform vec3 c[32];     // color per layer

out vec3 col;
void main()
    {
    int i;
    vec3 p;
    float r,dr=(r1-r0)/float(n-1);
    // input just single point
    p=gl_in[0].gl_Position.xyz;
    // emit line strip
    for (r=r0,i=0;i<n;i++,r+=dr)
        {
        col=c[i];
        gl_Position=mp*mv[i]*vec4(p.xyz*r,1.0);
        EmitVertex();
        }
    EndPrimitive();
    }
// Fragment
#version 400 core
in vec3 col;
out vec4 fcol;
void main()
    {
    fcol=vec4(col,1.0);
    }

VCL 应用程序非常简单 window,带有一个 40 毫秒更新计时器和 GLSL 日志备忘录,因此请忽略和/或模仿您环境中的 VCL 事件。 _glsl 仅确定是否使用着色器。这远未优化,有很多东西需要改进,比如使用 VAO 代替 glBegin/glEnd,重新编码矩阵环形缓冲区,这样它就不需要重新排序,等等......

最后是新预览: