Threejs3D模型爆炸效果

先看下效果

请添加图片描述

一、原理说明

让各个子mesh沿着一个固定的方向移动,这个方向就是该子mesh的包围盒中心与爆炸中心(模型整体的包围盒中心)的连线。移动轨迹如下图
请添加图片描述

二、代码如下

import{
    Vector3,
    Box3,
    Mesh,
    LineBasicMaterial,
    Geometry,
    Line,
    Matrix4
} from 'three'

/**
 * 使用方法:
 * 1、调用setSplitModel函数将要拆分的模型传入预处理
 * 然后两种控制爆炸方式
 * (1)实时更新
 *      调用startSplit()/quitSplit()函数开始爆炸/恢复
 *      需要在three的animate函数中调用update函数
 * (2)滑动条
 *      调用setValue函数把滑动条的值传入。
 */
export class ModelSplit{
    constructor(){
        this.meshList = [];
        this.running = false;
        this.targetSplitValue = 0;
        this.currentSplitValue = 0;
        this.offset = 1;

        this.splitScale = 1; //影响拆分距离,就是mesh的包围盒中心与爆炸中心的距离的倍率
        this.splitSpeed = 100;//影响拆分速度,反比例
    }

    setSplitModel(model){
        if(!model || model instanceof Mesh) {
            console.warn("只能处理Scene、Object3D、Group")
            return;
        }
        this.quit();
        this.meshList = [];

        //计算模型整体包围盒中心作为爆炸中心
        model.updateMatrixWorld();
        var box = new Box3().expandByObject(model); 
        var center = box.getCenter(new Vector3());
        model.traverse(node=>{
            if(node.type == "Mesh"){
                //分别计算每个mesh的包围盒中心,其与爆炸中心连线作为爆炸方向
                let subBox = new Box3().expandByObject(node);
                let meshCenter = subBox.getCenter(new Vector3());
                node._splitSrcPos = node.getWorldPosition(new Vector3())

                //这里计算各个轴向分速度,这样可以使用滑动条控制进度
                let subSpeed = {
                    x:(meshCenter.x-center.x) * this.splitScale / this.splitSpeed,
                    y:(meshCenter.y-center.y) * this.splitScale / this.splitSpeed,
                    z:(meshCenter.z-center.z) * this.splitScale / this.splitSpeed,
                }
                node._splitSpeed = subSpeed;
                this.meshList.push(node);
            }            
        })
        this.currentSplitValue = 0;
        this.targetSplitValue = 0;
        this.running = false;
    }

    /**
     * 开始爆炸
     */
    startSplit(){
        this.targetSplitValue = this.splitSpeed;
        this.currentSplitValue = 0;
        this.offset = 1;
        this.running = true;
        this.isQuit = false;
    }

    /**
     * 开始反向爆炸(还原)
     */
    quitSplit(){
        this.targetSplitValue = 0;
        this.currentSplitValue = this.splitSpeed;
        this.offset = -1;
        this.running = true;
        this.isQuit = true;
    }

    /**
     * 退出拆分时还原
     */
    quit(){
        if(this.currentSplitValue != 0 && this.meshList.length > 0){
            for(var i=0;i<this.meshList.length;i++){
                let node = this.meshList[i];
                node.position.copy(node._splitSrcPos);
            }
            this.currentSplitValue = 0;
            this.targetSplitValue = 0;
        }
    }

    /**
     * 如果用滑动条控制时将滑动条的值传入这个函数
     * @param {*} value [0,1]的值,表示爆炸进度
     */
    setValue(value){
        if(value < 0) value = 0;
        if(value > 1) value = 1;
        this.currentSplitValue = value * this.splitSpeed;
        for(var i=0;i<this.meshList.length;i++){
            let node = this.meshList[i];
            let x = node._splitSpeed.x * this.currentSplitValue + node._splitSrcPos.x;
            let y = node._splitSpeed.y * this.currentSplitValue + node._splitSrcPos.y;
            let z = node._splitSpeed.z * this.currentSplitValue + node._splitSrcPos.z;
            node.parent.updateMatrixWorld();
            let invMat = node.parent.matrixWorld.clone().invert();
            //不同版本的threejs用不同的方法获取逆矩阵,如果用上面的报错就换下面的
            //let invMat = new Matrix4().getInverse(node.parent.matrixWorld.clone());
            let pos = new Vector3(x,y,z).applyMatrix4(invMat);
            node.position.copy(pos);
        }
    }

    /**
     * 更新
     * @returns 
     */
    update(){
        if(this.running && this.meshList.length > 0){
            
            if (this.currentSplitValue != this.targetSplitValue) {
                this.currentSplitValue += this.offset;
            }

            for(var i=0;i<this.meshList.length;i++){
                let node = this.meshList[i];
                let x = node._splitSpeed.x * this.currentSplitValue + node._splitSrcPos.x;
                let y = node._splitSpeed.y * this.currentSplitValue + node._splitSrcPos.y;
                let z = node._splitSpeed.z * this.currentSplitValue + node._splitSrcPos.z;
                node.parent.updateMatrixWorld();
                let invMat = node.parent.matrixWorld.clone().invert();
                //不同版本的threejs用不同的方法获取逆矩阵,如果用上面的报错就换下面的
            	//let invMat = new Matrix4().getInverse(node.parent.matrixWorld.clone());
                let pos = new Vector3(x,y,z).applyMatrix4(invMat);
                node.position.copy(pos);
            }

            if(this.currentSplitValue == this.targetSplitValue){
                this.running = false;

                if(this.isQuit == true){
                    this.quit();
                    this.isQuit = false;
                }
            }
        }
    }
}

三、其他效果

请添加图片描述
使用按键模型滑动条
请添加图片描述
其他模型效果

四、然而但是

目前来看拆分效果不错,直到遇到某些模型,比如下面的
请添加图片描述
简单分析下,这种情况就是子mesh拆分方向比较接近,导致都往一个方向跑。
为了解决这个问题,那是不是可以把爆炸中心放在所有子mesh的包围盒中心呢,试试效果
请添加图片描述
看起来方向是没问题了,不过有些小模型跑很远,有些大模型还重叠了,这是拆分距离问题。这里就把距离固定为模型整体包围盒大小好了,修改后效果如下
请添加图片描述
请添加图片描述
看起来好多了。修改后的代码如下

import{
    Vector3,
    Box3,
    Mesh,
    LineBasicMaterial,
    Geometry,
    Line
} from 'three'

/**
 * 使用方法:
 * 1、调用setSplitModel函数将要拆分的模型传入预处理
 * 然后两种控制爆炸方式
 * (1)实时更新
 *      调用startSplit()/quitSplit()函数开始爆炸/恢复
 *      需要在three的animate函数中调用update函数
 * (2)滑动条
 *      调用setValue函数把滑动条的值传入。
 */
export class ModelSplit{
    constructor(){
        this.meshList = [];
        this.running = false;
        this.targetSplitValue = 0;
        this.currentSplitValue = 0;
        this.offset = 1;

        this.splitScale = 5; //影响拆分距离,就是mesh的包围盒中心与爆炸中心的距离的倍率
        this.splitSpeed = 100;//影响拆分速度,反比例

        this.mode = 2;
        if(this.mode == 2){
            this.splitScale = 0.3; //影响拆分距离,就是mesh的包围盒中心与爆炸中心的距离的倍率
           this.splitSpeed = 50;//影响拆分速度,反比例
        }
    }

    setSplitModel(model){
        if(!model || model instanceof Mesh) {
            console.warn("只能处理Scene、Object3D、Group")
            return;
        }
        this.quit();
        this.meshList = [];

        //计算模型整体包围盒中心作为爆炸中心
        model.updateMatrixWorld();
        var box = new Box3().expandByObject(model); 
        var maxLength = box.max.clone().distanceTo(box.min);
        var center;
        if(this.mode == 1){
            center = box.getCenter(new Vector3());
        }
        else{  //爆炸中心由子mesh的包围盒中心决定
            center = new Vector3();
            var subBox,subCenter,count = 0;;
            model.traverse(node=>{
                if(node.type == "Mesh"){
                    //分别计算每个mesh的包围盒中心,其与爆炸中心连线作为爆炸方向
                    subBox = new Box3().expandByObject(node);
                    subCenter = subBox.getCenter(new Vector3());
                    center = center.clone().add(subCenter);
                    count++;
                }            
            })
            center = center.clone().multiplyScalar(1/count);
        }
        
        model.traverse(node=>{
            if(node.type == "Mesh"){
                //分别计算每个mesh的包围盒中心,其与爆炸中心连线作为爆炸方向
                let subBox = new Box3().expandByObject(node);
                let meshCenter = subBox.getCenter(new Vector3());
                node._splitSrcPos = node.getWorldPosition(new Vector3())

                let subSpeed;
                if(this.mode == 1){
                    subSpeed = {
                        x:(meshCenter.x-center.x) * this.splitScale / this.splitSpeed,
                        y:(meshCenter.y-center.y) * this.splitScale / this.splitSpeed,
                        z:(meshCenter.z-center.z) * this.splitScale / this.splitSpeed,
                    }
                }
                else{
                    let targetPos = meshCenter.clone().add(meshCenter.clone().sub(center).normalize().multiplyScalar(maxLength));
                    //这里计算各个轴向分速度,这样可以使用滑动条控制进度
                    subSpeed = {
                        x:(targetPos.x - center.x) * this.splitScale / this.splitSpeed,
                        y:(targetPos.y - center.y) * this.splitScale / this.splitSpeed,
                        z:(targetPos.z - center.z) * this.splitScale / this.splitSpeed,
                        // x:(meshCenter.x-center.x) * this.splitScale / this.splitSpeed,
                        // y:(meshCenter.y-center.y) * this.splitScale / this.splitSpeed,
                        // z:(meshCenter.z-center.z) * this.splitScale / this.splitSpeed,
                    }
                }
               
                node._splitSpeed = subSpeed;
                this.meshList.push(node);
            }            
        })
        this.currentSplitValue = 0;
        this.targetSplitValue = 0;
        this.running = false;
    }

    /**
     * 开始爆炸
     */
    startSplit(){
        this.targetSplitValue = this.splitSpeed;
        this.currentSplitValue = 0;
        this.offset = 1;
        this.running = true;
        this.isQuit = false;
    }

    /**
     * 开始反向爆炸(还原)
     */
    quitSplit(){
        this.targetSplitValue = 0;
        this.currentSplitValue = this.splitSpeed;
        this.offset = -1;
        this.running = true;
        this.isQuit = true;
    }

    /**
     * 退出拆分时还原
     */
    quit(){
        if(this.currentSplitValue != 0 && this.meshList.length > 0){
            for(var i=0;i<this.meshList.length;i++){
                let node = this.meshList[i];
                node.position.copy(node._splitSrcPos);
            }
            this.currentSplitValue = 0;
            this.targetSplitValue = 0;
        }
    }

    /**
     * 如果用滑动条控制时将滑动条的值传入这个函数
     * @param {*} value [0,1]的值,表示爆炸进度
     */
    setValue(value){
        if(value < 0) value = 0;
        if(value > 1) value = 1;
        this.currentSplitValue = value * this.splitSpeed;
        for(var i=0;i<this.meshList.length;i++){
            let node = this.meshList[i];
            let x = node._splitSpeed.x * this.currentSplitValue + node._splitSrcPos.x;
            let y = node._splitSpeed.y * this.currentSplitValue + node._splitSrcPos.y;
            let z = node._splitSpeed.z * this.currentSplitValue + node._splitSrcPos.z;
            node.parent.updateMatrixWorld();
            let invMat = node.parent.matrixWorld.clone().invert();
            let pos = new Vector3(x,y,z).applyMatrix4(invMat);
            node.position.copy(pos);
        }
    }

    /**
     * 更新
     * @returns 
     */
    update(){
        if(this.running && this.meshList.length > 0){
            
            if (this.currentSplitValue != this.targetSplitValue) {
                this.currentSplitValue += this.offset;
            }

            for(var i=0;i<this.meshList.length;i++){
                let node = this.meshList[i];
                let x = node._splitSpeed.x * this.currentSplitValue + node._splitSrcPos.x;
                let y = node._splitSpeed.y * this.currentSplitValue + node._splitSrcPos.y;
                let z = node._splitSpeed.z * this.currentSplitValue + node._splitSrcPos.z;
                node.parent.updateMatrixWorld();
                let invMat = node.parent.matrixWorld.clone().invert();
                let pos = new Vector3(x,y,z).applyMatrix4(invMat);
                node.position.copy(pos);
            }

            if(this.currentSplitValue == this.targetSplitValue){
                this.running = false;

                if(this.isQuit == true){
                    this.quit();
                    this.isQuit = false;
                }
            }
        }
    }
}

为了保留原始效果,在构造函数中添加了一个变量this.mode,将值赋为1时是原先的效果,赋为2时是新的效果

评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值