|
为了给大家更直观的印象,我分享一个用 JavaScript 写的 Demo,大家可以在浏览器里看(简陋的)效果。
https://cs275-try.glitch.me源码在这里:Glitch : ,是很简单的 mass-spring model:
为节省时间,我用 A-Frame 框架来处理非物理模拟的部分,首先介绍一下这一部分:- <a-entity id=&#34;head&#34; gltf-model=&#34;#head-gltf&#34; position=&#34;0 1 -5&#34; scale=&#34;0.75 0.75 0.75&#34;>
- <a-animation attribute=&#34;rotation&#34;
- dur=&#34;1000&#34;
- fill=&#34;forwards&#34;
- direction=&#34;alternate&#34;
- from=&#34;0 0 -60&#34;
- to=&#34;0 0 60&#34;
- repeat=&#34;indefinite&#34;></a-animation>
- <a-entity append-hair id=&#34;hair-1&#34; line=&#34;start: 0 2.5 0; end: 0 4 0&#34;></a-entity>
- <a-entity append-hair id=&#34;hair-2&#34; line=&#34;start: 0 2.5 0; end: 0.3 4 0&#34;></a-entity>
- <a-entity append-hair id=&#34;hair-3&#34; line=&#34;start: 0 2.5 0; end: -0.3 4 0&#34;></a-entity>
- </a-entity>
复制代码 <a-entity id=&#34;head&#34; 是人头的模型,为节省时间我找了个球当头。
<a-animation attribute=&#34;rotation&#34; 则让它摇头晃脑,这样可以带动简陋的头发朴素地飘扬。
<a-entity append-hair 是带上了 append-hair component 的一个 entity,我在这个 component 里面写了个循环来添加头发,并进行物理模拟更新头发的位置。然后这个 entity 还带有 line component,这个 component 会画出头发根部的一小段,也就是毛囊了,模拟开始后的一瞬间我们会在毛囊上接发,接上十根 line 当真正的头发。
接下来来看 append-hair 是怎么实现的: - var { AFRAME, THREE } = window;
- AFRAME.registerComponent(&#39;append-hair&#39;, {
- schema: {
- initialized: { default: false },
- length: { type: &#39;int&#39;, default: 10 },
- size: { type: &#39;number&#39;, default: 2.5 },
- penalty: { type: &#39;number&#39;, default: 980 },
- initialLength: { type: &#39;number&#39;, default: 0.01 },
- stiffness: { type: &#39;number&#39;, default: 300 },
- damping: { type: &#39;number&#39;, default: 0.01 },
- graverty: { type: &#39;number&#39;, default: 9.8 },
- },
复制代码 可以看到这部分代码就是在 A-Frame 框架里注册了一个叫 append-hair 的 component,注册之后就可以在上面的 HTML 模板里使用这个 component 了。
Schema 描述了这个 component 可以传入的参数,以及参数的默认值,从这些参数就可以看出这个模拟头发的简单模型由哪些变量控制:
length 表示头发的段数,如下图,mass-spring model 中,头发其实是由很多段弹簧组成的,length 越长模拟越精细,相当于对头发无限细分。生产中会用 shader 对它们插值,从而得到亮丽而连续的发丝,这样得到的发丝叫 guide strand(导缕),对应于下图中的第三步,接着要把它复制几次,从用较少的模拟计算量而得到比较密的头发。
size是用来做碰撞检测的,理论上说应该给头做个包围盒,然后在每个 tick 检测头发的每个质点是否和包围盒相交,相交了就给一个惩罚力,让头发远离头。不过既然我的头,是个球,那直接用球的半径来检测碰撞就好了,比较简单。
penalty 就是惩罚力的加速度
initialLength 是弹簧的初始长度,胡克说过,弹力 = 弹性系数 x (当前长度 - initialLength)
stiffness 就是弹性系数
damping 是阻尼,用它给出与速度方向相反的力,来防止头发动得太快,看起来会很假(当然它不是这个 Demo 显得很假的关键因素)
graverty 头发会自然下落,这个加速度设为经典的 9.8
以上的参数建模了每根头发的简单物理行为。
接着看看每个 tick 会发生什么:- tock(time, delta) {
- let positionOfHairRoot = null;
- // get local position
- {
- const { x, y, z } = this.el.components.line.data.end;
- positionOfHairRoot = new THREE.Vector3(x, y, z);
- }
- // get global position
- positionOfHairRoot = this.el.object3D.localToWorld(positionOfHairRoot)
- if (this.data.initialized) {
- // select consequent hairs
- const consequentHairs = document.querySelectorAll(`a-scene > a-entity.consequent-hairs-of-${this.el.id} > a-entity[mass]`);
- this.updateConsequentHair(consequentHairs, positionOfHairRoot, delta);
- } else {
- // add consequent hair entities
- const head = document.querySelector(&#39;a-scene&#39;);
- const consequentHairGroup = document.createElement(&#39;a-entity&#39;);
- consequentHairGroup.setAttribute(&#39;class&#39;, `consequent-hairs-of-${this.el.id}`);
- head.appendChild(consequentHairGroup);
- this.initConsequentHair(consequentHairGroup, positionOfHairRoot);
- this.data.initialized = true;
- }
- },
复制代码 由于第一个 tick 时很多 entity 还没完成初始化,所以我选择在 tock 里进行物理模拟,tock 指每个 tick 完成计算后的那些瞬间。
这段代码大意就是取得毛囊的位置,然后如果头发还没初始化就进行接发,用 document.createElement 创建一个实体组,把这个实体组放到 head 实体上,然后用 this.initConsequentHair 往里面塞进 length 根头发。这都是很基本的 DOM 操作,让我想到了用 jQuery 的黑暗日子(虽然我没在生产中用过 jQuery)。
那如果已经初始化完了,就进行物理模拟,this.updateConsequentHair 就是更新毛囊上接的一根接一根的头发。
我首先把每个 mass 的位置放到数组里- /** calculate consequent hairs&#39; position */
- updateConsequentHair(consequentHairs, positionOfHairRoot, delta) {
- // collect positions
- const positions = [positionOfHairRoot];
- for (let i = 0; i < this.data.length; i += 1) {
- const hairPart = consequentHairs[i];
- positions.push(hairPart.components.line.data.end);
- }
复制代码 然后取出弹簧的参数 initialLength, stiffness 对于每一段弹簧进行模拟:- const { initialLength, stiffness, damping } = this.data;
- // update components and position
- for (let i = 0; i < this.data.length; i += 1) {
复制代码 用胡克定律可以算出其弹力:- const distance = (new THREE.Vector3()).copy(positions[i]).sub(positions[i + 1]);
- // f_s = k(x - x_0)
- const kx = Math.max(0, distance.length() - initialLength) * stiffness;
- const springForce = distance.normalize().multiplyScalar(kx);
复制代码 然后取出每个质点的速度,用阻尼系数算出其阻力:- // only consequentHairs have force and velocity, root hair is moved by animation
- // f_d = -k_d * v
- const hairPart = consequentHairs[i];
- let { x, y, z } = hairPart.components.velocity.data;
- const dampingForce = new THREE.Vector3(x, y, z).multiplyScalar(damping);
复制代码 从而得到合力:- // f = f_s + f_d + g + penalty
- const force = (new THREE.Vector3()).copy(springForce)
- .add(dampingForce)
- .add(new THREE.Vector3(0, -this.data.graverty, 0))
- .add(this.getCollidingWithHeadPenaltyForce(positions[i + 1])); // note that threejs is not immutable!
- hairPart.setAttribute(&#39;force&#39;, force); // make it visible in dev tool
复制代码 合力除以质点的质量就得到加速度,加速度乘以每帧的时间的平方就得到了质点运动的距离:- // a = f / m
- const acceleator = (new THREE.Vector3()).copy(force).divideScalar(hairPart.components.mass.data);
- // v = a * dt
- const velocity = (new THREE.Vector3()).copy(acceleator).multiplyScalar(delta / 1000);
- hairPart.setAttribute(&#39;velocity&#39;, velocity); // make it visible in dev tool
- // s = v * dt
- const positionDelta = (new THREE.Vector3()).copy(velocity).multiplyScalar(delta / 1000);
- ({ x, y, z } = positions[i + 1]);
复制代码 最后更新一下质点的位置就完事了:- positions[i + 1] = new THREE.Vector3(x, y, z).add(positionDelta);
- }
- // set positions
- for (let i = 0; i < this.data.length; i += 1) {
- const { x, y, z } = positions[i];
- const start = `${x}, ${y}, ${z}`;
- {
- const { x, y, z } = positions[i + 1];
- const end = `${x}, ${y}, ${z}`;
- const hairPart = consequentHairs[i];
- hairPart.setAttribute(&#39;line&#39;, { start, end });
- }
- }
复制代码 可以发现 mass-spring 的代码很简单,实际效果可以看到头上的弹簧就像秀发一样舞动,但好像还缺了点什么……
没有做的:
限制头发伸长的长度,头发段虽然是弹簧,但应该只能在有限范围内伸缩,防止出现 Demo 里变成魔发公主的情况遍历脑袋表面随机生成更多毛囊,目前是手动放了三个毛囊,所以只有三毛对惩罚力进行精细的调参,让头发不至于在头表面蹦跳,也不会栽进头里,我在 Demo 里就随便给了个值shader,插值得到更密更顺的头发,还有对光的反映头发之间的碰撞检测,做了就可以模拟头发的缠绕等行为,而不是任由它们互相穿过与空气的结合,需要用 Navier-Stokes 方程模拟空气,并在与头发接触时传递冲量,用 JS 做效果极差,就删了
模拟头发还有布料这样柔软的物体,除了离散的模型外还有用连续体力学来解的。
那 Mass-Spring 这么好懂为啥不都用它呢?目前仍未得到完美解决的问题在于,如何通过头发布料的变形来反过来得到力。
Continue Model 可以以更大的计算量为代价解决这些问题。主要是描述每一个点处的应力(形变的情况),头发里就是个 1D 的张量,乘以一个方向就能得到这个方向上的力。
当然这些就不是我能弄懂的了…… |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|