实现面料模拟一直是一个比较难的问题,因此今天说一下我的具体实现方案,和代码
先展示结果,这是没有加物理效果的衣服
这是加了物理效果的衣服
是不是效果很明显
首先,先说原理,通过修改每个顶点的位置来实现衣服的移动和变形,最简单的实现方式就是,设置一个重力,每秒向y轴的负方向移动固定或递增的距离,来实现基本的物理效果
其次,如何实现面料效果呢?需要让面料的各个点之间实现拉力的影响,相互约束,从而使得衣服的粒子不会到处乱跑
然后,就是衣服传到人模上面,具体的实现方案是:人模使用八叉树,判断衣服粒子能否进入到八叉树的各个正方体中,一旦进入,则开启该粒子的碰撞检测,threejs的八叉树有自己的射线功能,性能还是可以的
上代码吧
let neighbors;
import * as THREE from "three";
function vecScale(a,anr, scale) {
anr *= 3;
a[anr++] *= scale;
a[anr++] *= scale;
a[anr] *= scale;
}
function vecCopy(a,anr, b,bnr) {
anr *= 3; bnr *= 3;
a[anr++] = b[bnr++];
a[anr++] = b[bnr++];
a[anr] = b[bnr];
}
// 向量相加
function vecAdd(a,anr, b,bnr, scale = 1.0) {
anr *= 3; bnr *= 3;
a[anr++] += b[bnr++] * scale;
a[anr++] += b[bnr++] * scale;
a[anr] += b[bnr] * scale;
}
function vecSetDiff(dst,dnr, a,anr, b,bnr, scale = 1.0) {
dnr *= 3; anr *= 3; bnr *= 3;
dst[dnr++] = (a[anr++] - b[bnr++]) * scale;
dst[dnr++] = (a[anr++] - b[bnr++]) * scale;
dst[dnr] = (a[anr] - b[bnr]) * scale;
}
function vecLengthSquared(a,anr) {
anr *= 3;
let a0 = a[anr], a1 = a[anr + 1], a2 = a[anr + 2];
return a0 * a0 + a1 * a1 + a2 * a2;
}
function vecDistSquared(a,anr, b,bnr) {
anr *= 3; bnr *= 3;
let a0 = a[anr] - b[bnr], a1 = a[anr + 1] - b[bnr + 1], a2 = a[anr + 2] - b[bnr + 2];
return a0 * a0 + a1 * a1 + a2 * a2;
}
function vecSetCross(a,anr, b,bnr, c,cnr) {
anr *= 3; bnr *= 3; cnr *= 3;
a[anr++] = b[bnr + 1] * c[cnr + 2] - b[bnr + 2] * c[cnr + 1];
a[anr++] = b[bnr + 2] * c[cnr + 0] - b[bnr + 0] * c[cnr + 2];
a[anr] = b[bnr + 0] * c[cnr + 1] - b[bnr + 1] * c[cnr + 0];
}
function findTriNeighbors(triIds)
{
// create common edges
// 将点的关联放入到edges当中
let edges = [];
let numTris = triIds.length / 3;
for (let i = 0; i < numTris; i++) {
for (let j = 0; j < 3; j++) {
let id0 = triIds[3 * i + j];
let id1 = triIds[3 * i + (j + 1) % 3];
edges.push({
id0 : Math.min(id0, id1),
id1 : Math.max(id0, id1),
edgeNr : 3 * i + j
});
}
}
// sort so common edges are next to each other
edges.sort((a, b) => ((a.id0 < b.id0) || (a.id0 == b.id0 && a.id1 < b.id1)) ? -1 : 1);
// find matchign edges
neighbors = new Float32Array(3 * numTris);
neighbors.fill(-1); // open edge
let nr = 0;
while (nr < edges.length) {
let e0 = edges[nr];
nr++;
if (nr < edges.length) {
let e1 = edges[nr];
if (e0.id0 == e1.id0 && e0.id1 == e1.id1) {
neighbors[e0.edgeNr] = e1.edgeNr;
neighbors[e1.edgeNr] = e0.edgeNr;
}
nr++;
}
}
return neighbors;
}
class Cloth {
constructor(mesh, scene, bendingCompliance = 1.0)
{
this.numParticles = mesh.vertices.length / 3; // 顶点的数量
this.pos = new Float32Array(mesh.vertices); // 实际的顶点位置
this.prevPos = new Float32Array(mesh.vertices); // 储存上一时刻顶点的位置
this.restPos = new Float32Array(mesh.vertices); // 储存初始顶点的位置
this.vel = new Float32Array(3 * this.numParticles); // 储存点的速度
this.invMass = new Float32Array(this.numParticles);
this.uv = new Float32Array(mesh.uv);
this.flag = false;
this.Force = new Float32Array(3 * this.numParticles); // 储存点的力
// stretching and bending constraints
// 根据面来获取点之间的关联
neighbors = findTriNeighbors(mesh.faceTriIds);
let numTris = mesh.faceTriIds.length / 3;
let edgeIds = [];
let triPairIds = [];
for (let i = 0; i < numTris; i++) {
for (let j = 0; j < 3; j++) {
let id0 = mesh.faceTriIds[3 * i + j];
let id1 = mesh.faceTriIds[3 * i + (j + 1) % 3];
// each edge only once
let n = neighbors[3 * i + j];
if (n < 0 || id0 < id1) {
edgeIds.push(id0);
edgeIds.push(id1);
}
// tri pair
if (n >= 0) {
// opposite ids
let ni = Math.floor(n / 3);
let nj = n % 3;
let id2 = mesh.faceTriIds[3 * i + (j + 2) % 3];
let id3 = mesh.faceTriIds[3 * ni + (nj + 2) % 3];
triPairIds.push(id0);
triPairIds.push(id1);
triPairIds.push(id2);
triPairIds.push(id3);
}
}
}
this.stretchingIds = new Int32Array(edgeIds);
this.bendingIds = new Int32Array(triPairIds);
this.stretchingLengths = new Float32Array(this.stretchingIds.length / 2);
this.bendingLengths = new Float32Array(this.bendingIds.length / 4);
this.stretchingCompliance = 0;
this.bendingCompliance = bendingCompliance;
this.temp = new Float32Array(4 * 3);
this.grads = new Float32Array(4 * 3);
this.grabId = -1;
this.grabInvMass = 0.0;
this.initPhysics(mesh.faceTriIds);
// visual edge mesh
let geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(this.pos, 3));
geometry.setIndex(edgeIds);
let lineMaterial = new THREE.LineBasicMaterial({color: 0xfff000, linewidth: 2});
this.edgeMesh = new THREE.LineSegments(geometry, lineMaterial);
this.edgeMesh.visible = false;
scene.add(this.edgeMesh);
// visual tri mesh
geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(this.pos, 3));
geometry.setIndex(mesh.faceTriIds);
geometry.setAttribute('uv',new THREE.BufferAttribute(this.uv, 2))
let visMaterial = new THREE.MeshPhongMaterial({color: 0xff0000, side: THREE.DoubleSide});
this.triMesh = new THREE.Mesh(geometry, visMaterial);
this.triMesh.castShadow = true;
this.triMesh.userData = this; // for raycasting
this.triMesh.layers.enable(1);
scene.add(this.triMesh);
geometry.computeVertexNormals();
this.UpdateMeshes();
this.volIdOrder = [[1,3,2], [0,2,3], [0,3,1], [0,1,2]];
}
initPhysics(triIds)
{
this.invMass.fill(0.0);
let numTris = triIds.length / 3;
let e0 = [0.0, 0.0, 0.0];
let e1 = [0.0, 0.0, 0.0];
let c = [0.0, 0.0, 0.0];
// 计算各个点的质量
for (let i = 0; i < numTris; i++) {
let id0 = triIds[3 * i];
let id1 = triIds[3 * i + 1];
let id2 = triIds[3 * i + 2];
vecSetDiff(e0,0, this.pos,id1, this.pos,id0);
vecSetDiff(e1,0, this.pos,id2, this.pos,id0);
vecSetCross(c,0, e0,0, e1,0);
let A = 0.5 * Math.sqrt(vecLengthSquared(c,0));
let pInvMass = A > 0.0 ? 1.0 / A / 3.0 : 0.0;
this.invMass[id0] += pInvMass;
this.invMass[id1] += pInvMass;
this.invMass[id2] += pInvMass;
}
// 获取点与点之间的关联
for (let i = 0; i < this.stretchingLengths.length; i++) {
let id0 = this.stretchingIds[2 * i];
let id1 = this.stretchingIds[2 * i + 1];
this.stretchingLengths[i] = Math.sqrt(vecDistSquared(this.pos,id0, this.pos,id1));
}
for (let i = 0; i < this.bendingLengths.length; i++) {
let id0 = this.bendingIds[4 * i + 2];
let id1 = this.bendingIds[4 * i + 3];
this.bendingLengths[i] = Math.sqrt(vecDistSquared(this.pos,id0, this.pos,id1));
}
// attach
let minX = Number.MAX_VALUE;
let maxX = -Number.MAX_VALUE;
let maxY = -Number.MAX_VALUE;
for (let i = 0; i < this.numParticles; i++) {
minX = Math.min(minX, this.pos[3 * i]);
maxX = Math.max(maxX, this.pos[3 * i]);
maxY = Math.max(maxY, this.pos[3 * i + 1]);
}
// 设置定点
// for (let i = 0; i < this.numParticles; i++) {
// // let x = this.pos[3 * i];
// let y = this.pos[3 * i + 1];
// if (y === maxY )
// this.invMass[i] = 0.0;
// }
}
Solve(dt,mesh, gravity)
{
// 1、在开始时获取每个点的力,根据质量得到加速度,再计算得到速度,速度*时间=距离变化,最后修改position,然后计算每个点的力,保存起来
for (let i = 0; i < this.numParticles; i++) {
if (this.invMass[i] == 0.0)
continue;
const point = new THREE.Vector3(this.pos[i*3],this.pos[i*3+1],this.pos[i*3+2])
// 判断点是否碰撞
const flag = this.checkInBox(point,mesh.worldOctree,mesh)
if (flag) {
// this.vel[3*i] = 0
// this.vel[3*i+1] = 0
// this.vel[3*i+2] = 0
this.invMass[i] = 0
}else{
vecAdd(this.vel,i, gravity,0, dt);
}
vecCopy(this.prevPos,i, this.pos,i);
vecAdd(this.pos,i, this.vel,i, dt);
// 设置地面
var y = this.pos[3 * i + 1];
if (y < 0.0) {
vecCopy(this.pos,i, this.prevPos,i);
this.pos[3 * i + 1] = this.pos[3*i+1] < 0 ? 0:this.pos[3*i+1];
}
}
this.solveStretching(this.stretchingCompliance, dt);
this.solveBending(this.bendingCompliance, dt);
for (let i = 0; i < this.numParticles; i++) {
if (this.invMass[i] == 0.0)
continue;
vecSetDiff(this.vel,i, this.pos,i, this.prevPos,i, 1.0 / dt);
}
}
// 判断点是否在当前树内
checkInBox(point,octree,mesh){
if (!this.pointInsideBox(point,octree.box)) {
return false
}
if (octree.subTrees.length === 0 && octree.triangles.length === 0) {
return false
}
if (octree.subTrees.length) {
for (let i = 0; i < octree.subTrees.length; i++) {
const octree_x = octree.subTrees[i];
if(this.checkInBox(point,octree_x,mesh)){
return true
}
}
return false;
}
if (octree.triangles.length) {
// TODO 检测点和内部是否发生碰撞
// return true
return this.pointInsideMesh(point,mesh)
}
}
solveStretching(compliance,dt) {
let alpha = compliance / dt /dt;
for (let i = 0; i < this.stretchingLengths.length; i++) {
let id0 = this.stretchingIds[2 * i];
let id1 = this.stretchingIds[2 * i + 1];
let w0 = this.invMass[id0];
let w1 = this.invMass[id1];
let w = w0 + w1;
if (w == 0.0)
continue;
vecSetDiff(this.grads,0, this.pos,id0, this.pos,id1);
let len = Math.sqrt(vecLengthSquared(this.grads,0));
if (len == 0.0)
continue;
vecScale(this.grads,0, 1.0 / len);
let restLen = this.stretchingLengths[i];
let C = len - restLen;
let s = -C / (w + alpha);
vecAdd(this.pos,id0, this.grads,0, s * w0);
vecAdd(this.pos,id1, this.grads,0, -s * w1);
}
}
solveBending(compliance, dt) {
let alpha = compliance / dt /dt;
for (let i = 0; i < this.bendingLengths.length; i++) {
let id0 = this.bendingIds[4 * i + 2];
let id1 = this.bendingIds[4 * i + 3];
let w0 = this.invMass[id0];
let w1 = this.invMass[id1];
let w = w0 + w1;
if (w == 0.0)
continue;
vecSetDiff(this.grads,0, this.pos,id0, this.pos,id1);
let len = Math.sqrt(vecLengthSquared(this.grads,0));
if (len == 0.0)
continue;
vecScale(this.grads,0, 1.0 / len);
let restLen = this.bendingLengths[i];
let C = len - restLen;
let s = -C / (w + alpha);
vecAdd(this.pos,id0, this.grads,0, s * w0);
vecAdd(this.pos,id1, this.grads,0, -s * w1);
}
}
// 判断点是否在盒子内
pointInsideBox(point,box){
return point.x > box.min.x && point.x < box.max.x && point.y > box.min.y && point.y < box.max.y && point.z > box.min.z && point.z < box.max.z
}
// 判断点是否在模型内
pointInsideMesh(point,mesh){
const ray = new THREE.Ray(point, new THREE.Vector3(0,-1,0));
const y = mesh.worldOctree.rayIntersect(ray)
const ray05 = new THREE.Ray(point, new THREE.Vector3(0,1,0));
const y02 = mesh.worldOctree.rayIntersect(ray05)
const ray01 = new THREE.Ray(point, new THREE.Vector3(-1,0,0));
const x01 = mesh.worldOctree.rayIntersect(ray01)
const ray02 = new THREE.Ray(point, new THREE.Vector3(1,0,0));
const x02 = mesh.worldOctree.rayIntersect(ray02)
const ray03 = new THREE.Ray(point, new THREE.Vector3(0,0,1));
const z01 = mesh.worldOctree.rayIntersect(ray03)
const ray04 = new THREE.Ray(point, new THREE.Vector3(0,0,-1));
const z02 = mesh.worldOctree.rayIntersect(ray04)
if ((y || y02) && (x01 || x02) && (z01 || z02)) {
return true
}
}
UpdateMeshes() {
this.triMesh.geometry.computeVertexNormals();
this.triMesh.geometry.attributes.position.needsUpdate = true;
this.triMesh.geometry.computeBoundingSphere();
this.edgeMesh.geometry.attributes.position.needsUpdate = true;
}
}
export default Cloth
新建一个cloth的对象,放入模型数据mesh和场景scene,mesh的数据格式{ vertices, faceTriIds, uv },三个数组
然后每次渲染的时候调一次,cloth对象的Solve方法即可