如何在 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的理解还不够透彻,不知道怎么把上一帧的世界坐标放到贴图里让下一帧读取,所以这个方法没有成功。
总结我的问题
如何在 THREE.js GPGpu 中读取最后一帧世界位置?步骤是什么?
有没有更好的方法来做这个模拟?即使这与我的想法无关。例如,模拟真实世界的力。
我的想法是有几个 等距点的同心球层 像这样:
- 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
,重新编码矩阵环形缓冲区,这样它就不需要重新排序,等等......
最后是新预览:
创意来自
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的理解还不够透彻,不知道怎么把上一帧的世界坐标放到贴图里让下一帧读取,所以这个方法没有成功。
总结我的问题
如何在 THREE.js GPGpu 中读取最后一帧世界位置?步骤是什么?
有没有更好的方法来做这个模拟?即使这与我的想法无关。例如,模拟真实世界的力。
我的想法是有几个 等距点的同心球层 像这样:
- 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
,重新编码矩阵环形缓冲区,这样它就不需要重新排序,等等......
最后是新预览: