前端页面实现面料的模拟

实现面料模拟一直是一个比较难的问题,因此今天说一下我的具体实现方案,和代码

先展示结果,这是没有加物理效果的衣服

这是加了物理效果的衣服

是不是效果很明显

首先,先说原理,通过修改每个顶点的位置来实现衣服的移动和变形,最简单的实现方式就是,设置一个重力,每秒向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方法即可

相关推荐
ZhangTao_zata4 分钟前
前端知识点
前端·javascript·css
GDAL9 分钟前
HTML5中`<ul>`标签深入全面解析
前端·html·html5
桃子叔叔13 分钟前
前端工程化3:使用lerna管理多包
前端·前端工程化·lerna
下雪天的夏风29 分钟前
Vant 按需引入导致 Typescript,eslint 报错问题
前端·typescript·eslint
流浪的大萝卜36 分钟前
开发一个电商API接口的步骤!!!
java·大数据·前端·数据仓库·后端·爬虫·python
2301_7969821443 分钟前
requests-html的具体使用方法有哪些?
前端·python·html
运维Z叔44 分钟前
利用shuji还原webpack打包源码
服务器·前端·webpack·node.js·postman·xss·csrf
不cong明的亚子44 分钟前
webpack5-手撸RemoveConsolePlugin插件
前端·webpack·react
Lee_Yu_Fan1 小时前
vue项目如何在js文件中导入assets文件夹下图片
前端·vue.js
秋沐1 小时前
Vue3流程图插件-Vue Flow
前端·vue.js·流程图