three.js物理引擎ammo入门

2024-02-21 10:46 孙水迪 509

本文是入门文章,为了方便理解,部分机制可能描述得不那么准确

1.工作机制

threejs由5个部分组成,分别是

  • Scene,场景,存放Mesh
  • Camera,相机,从某个角度拍摄Scene
  • Mesh,3D对象
  • Renderer,渲染器,用相机拍摄场景
  • Canvas,画布,html里显示渲染结果的节点

具体流程可以认为是首先将渲染的3D对象添加到场景里,然后渲染器调用相机拍摄场景并将结果呈现在画布里

ammo在此基础上增加了一个物理世界physicsWorld

将3D对象添加到场景的同时再添加到物理世界里,在物理世界里进行运动并获取运动后的3D对象的状态,将状态同步到场景里,重新用相机拍摄渲染到画布里即为每一帧的更新

2.使用方式

步骤如下

Ammo().then(function(Ammo) {
        //初始化
        init();
        //刷新每帧的运行
        animate();
        function init() {
            //创建threejs的相机,场景,渲染器并绑定画布
            initGraphics();
            //创建ammo的物理世界
            initPhysics();
            //创建3D对象并添加到场景和物理世界中
            createObjects();
            //绑定外部设备比如键盘的事件
            initInput();
        }
        function animate() {
            //设置每帧刷新时的回调
            requestAnimationFrame( animate );
            //更新物理世界并同步到场景,之后重新渲染最新结果
            render();
        }
    }
)

3.初始化threejs,initGraphics

创建摄像头,渲染器,场景并添加环境光和光源,设置画布

function initGraphics() {
    //创建相机
    camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.2, 2000 );
    camera.position.x = -7;
    camera.position.y = 5;
    camera.position.z =  8;
    //设置用鼠标移动相机
    controls = new THREE.OrbitControls( camera );
    //移动时旋转中心点的坐标
    controls.target.y = 2;

    //设置渲染器为webgl
    renderer = new THREE.WebGLRenderer();
    renderer.setClearColor( 0xbfd1e5 );
    //设置像素密度和设备相同
    renderer.setPixelRatio( window.devicePixelRatio );
    renderer.setSize( window.innerWidth, window.innerHeight );
    //启用阴影映射
    renderer.shadowMap.enabled = true;

    //纹理加载器
    textureLoader = new THREE.TextureLoader();

    //创建场景
    scene = new THREE.Scene();

    //环境光
    var ambientLight = new THREE.AmbientLight( 0x404040 );
    scene.add( ambientLight );

    //定向光源
    var light = new THREE.DirectionalLight( 0xffffff, 1 );
    light.position.set( -10, 10, 5 );
    //光源阴影
    light.castShadow = true;
    var d = 10;
    //光照范围,近裁剪面,远裁剪面
    light.shadow.camera.left = -d;
    light.shadow.camera.right = d;
    light.shadow.camera.top = d;
    light.shadow.camera.bottom = -d;
    light.shadow.camera.near = 2;
    light.shadow.camera.far = 50;
    //阴影贴图分辨率
    light.shadow.mapSize.x = 1024;
    light.shadow.mapSize.y = 1024;
    scene.add( light );


    //在画布上绑定渲染结果
    container = document.getElementById( 'container' );
    container.innerHTML = "";
    container.appendChild( renderer.domElement );

    //自适应大小
    window.addEventListener( 'resize', onWindowResize, false );

}

4.初始化ammo,initPhysics

function initPhysics() {
    //创建软体和刚体碰撞的配置
    collisionConfiguration = new Ammo.btSoftBodyRigidBodyCollisionConfiguration();
    //创建一个碰撞分发器,它将根据碰撞配置处理碰撞事件
    dispatcher = new Ammo.btCollisionDispatcher( collisionConfiguration );
    //创建一个广相位(broadphase)算法,用于检测潜在的碰撞对
    broadphase = new Ammo.btDbvtBroadphase();
    //创建一个约束求解器,用于解决物体之间的约束和碰撞响应
    solver = new Ammo.btSequentialImpulseConstraintSolver();
    //创建一个软体(soft body)求解器,用于模拟柔软的物体
    softBodySolver = new Ammo.btDefaultSoftBodySolver();
    //创建一个软体和刚体混合的物理世界,将前面创建的碰撞分发器、广相位算法、约束求解器、碰撞配置和软体求解器传递给它
    physicsWorld = new Ammo.btSoftRigidDynamicsWorld( dispatcher, broadphase, solver, collisionConfiguration, softBodySolver);
    //设置全局重力
    physicsWorld.setGravity( new Ammo.btVector3( 0, gravityConstant, 0 ) );
    physicsWorld.getWorldInfo().set_m_gravity( new Ammo.btVector3( 0,gravityConstant, 0 ) );
}

5.创建3D对象,createObjects

这一步根据实际需要的场景创建对应的对象

下面代码创建了一个地板设置质量为0,这样子地板就不会因为重力掉落了

在地板上创建了方块堆叠成的墙和一个可以旋转的机械臂,机械臂和球体通过绳子链接

function createObjects() {

    //位置信息
    var pos = new THREE.Vector3();
    //旋转信息
    var quat = new THREE.Quaternion();

    // 地板
    pos.set( 0, - 0.5, 0 );
    quat.set( 0, 0, 0, 1 );
    //质量为0,所以不会掉下去
    var ground = createParalellepiped( 40, 1, 40, 0, pos, quat, new THREE.MeshPhongMaterial( { color: 0xFFFFFF } ) );
    //产生阴影
    ground.castShadow = true;
    //显示其他物理产生的阴影
    ground.receiveShadow = true;
    textureLoader.load( "../textures/grid.png", function( texture ) {
        //设置为重复平铺
        texture.wrapS = THREE.RepeatWrapping;
        texture.wrapT = THREE.RepeatWrapping;
        //每行每列重复40次
        texture.repeat.set( 40, 40 );
        //设置材质纹理
        ground.material.map = texture;
        //修改后通知刷新材质
        ground.material.needsUpdate = true;
    } );


    // 球
    var ballMass = 0.2;
    var ballRadius = 0.6;

    var ball = new THREE.Mesh( new THREE.SphereGeometry( ballRadius, 20, 20 ), new THREE.MeshPhongMaterial( { color: 0x202020 } ) );
    ball.castShadow = true;
    ball.receiveShadow = true;
    var ballShape = new Ammo.btSphereShape( ballRadius );
    ballShape.setMargin( margin );
    pos.set( -3, 2, 0 );
    quat.set( 0, 0, 0, 1 );
    createRigidBody( ball, ballShape, ballMass, pos, quat );
    //设置摩擦系数
    ball.userData.physicsBody.setFriction( 0.5 );

    // 墙
    var brickMass = 0.5;
    var brickLength = 1.2;
    var brickDepth = 0.6;
    var brickHeight = brickLength * 0.5;
    var numBricksLength = 6;
    var numBricksHeight = 8;
    var z0 = - numBricksLength * brickLength * 0.5;
    pos.set( 0, brickHeight * 0.5, z0 );
    quat.set( 0, 0, 0, 1 );
    //创建长方体组成的墙壁
    for ( var j = 0; j < numBricksHeight; j ++ ) {

        var oddRow = ( j % 2 ) == 1;

        pos.z = z0;

        if ( oddRow ) {
            pos.z -= 0.25 * brickLength;
        }

        var nRow = oddRow? numBricksLength + 1 : numBricksLength;
        for ( var i = 0; i < nRow; i ++ ) {

            var brickLengthCurrent = brickLength;
            var brickMassCurrent = brickMass;
            if ( oddRow && ( i == 0 || i == nRow - 1 ) ) {
                brickLengthCurrent *= 0.5;
                brickMassCurrent *= 0.5;
            }

            var brick = createParalellepiped( brickDepth, brickHeight, brickLengthCurrent, brickMassCurrent, pos, quat, createMaterial() );
            brick.castShadow = true;
            brick.receiveShadow = true;

            if ( oddRow && ( i == 0 || i == nRow - 2 ) ) {
                pos.z += 0.75 * brickLength;
            }
            else {
                pos.z += brickLength;
            }

        }
        pos.y += brickHeight;
    }

    // 绳
    var ropeNumSegments = 10;
    var ropeLength = 4;
    var ropeMass = 3;
    var ropePos = ball.position.clone();
    ropePos.y += ballRadius;

    var segmentLength = ropeLength / ropeNumSegments;
    var ropeGeometry = new THREE.BufferGeometry();
    var ropeMaterial = new THREE.LineBasicMaterial( { color: 0x000000 } );
    var ropePositions = [];
    var ropeIndices = [];

    for ( var i = 0; i < ropeNumSegments + 1; i++ ) {
        ropePositions.push( ropePos.x, ropePos.y + i * segmentLength, ropePos.z );
    }

    for ( var i = 0; i < ropeNumSegments; i++ ) {
        ropeIndices.push( i, i + 1 );
    }

    ropeGeometry.setIndex( new THREE.BufferAttribute( new Uint16Array( ropeIndices ), 1 ) );
    ropeGeometry.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( ropePositions ), 3 ) );
    ropeGeometry.computeBoundingSphere();
    rope = new THREE.LineSegments( ropeGeometry, ropeMaterial );
    rope.castShadow = true;
    rope.receiveShadow = true;
    scene.add( rope );

    // 绳
    var softBodyHelpers = new Ammo.btSoftBodyHelpers();
    var ropeStart = new Ammo.btVector3( ropePos.x, ropePos.y, ropePos.z );
    var ropeEnd = new Ammo.btVector3( ropePos.x, ropePos.y + ropeLength, ropePos.z );
    var ropeSoftBody = softBodyHelpers.CreateRope( physicsWorld.getWorldInfo(), ropeStart, ropeEnd, ropeNumSegments - 1, 0 );
    var sbConfig = ropeSoftBody.get_m_cfg();
    sbConfig.set_viterations( 10 );
    sbConfig.set_piterations( 10 );
    ropeSoftBody.setTotalMass( ropeMass, false )
    Ammo.castObject( ropeSoftBody, Ammo.btCollisionObject ).getCollisionShape().setMargin( margin * 3 );
    //1是碰撞组,同一组的不碰撞,-1是其与其他组都碰撞
    physicsWorld.addSoftBody( ropeSoftBody, 1, -1 );
    rope.userData.physicsBody = ropeSoftBody;
    ropeSoftBody.setActivationState( 4 );

    // 基座和旋转臂
    var armMass = 2;
    var armLength = 3;
    var pylonHeight = ropePos.y + ropeLength;
    var baseMaterial = new THREE.MeshPhongMaterial( { color: 0x606060 } );
    pos.set( ropePos.x, 0.1, ropePos.z - armLength );
    quat.set( 0, 0, 0, 1 );
    var base = createParalellepiped( 1, 0.2, 1, 0, pos, quat, baseMaterial );
    base.castShadow = true;
    base.receiveShadow = true;
    pos.set( ropePos.x, 0.5 * pylonHeight, ropePos.z - armLength );
    var pylon = createParalellepiped( 0.4, pylonHeight, 0.4, 0, pos, quat, baseMaterial );
    pylon.castShadow = true;
    pylon.receiveShadow = true;
    pos.set( ropePos.x, pylonHeight + 0.2, ropePos.z - 0.5 * armLength );
    var arm = createParalellepiped( 0.4, 0.4, armLength + 0.4, armMass, pos, quat, baseMaterial );
    arm.castShadow = true;
    arm.receiveShadow = true;

    // 绳子两端绑定
    var influence = 1;
    ropeSoftBody.appendAnchor( 0, ball.userData.physicsBody, true, influence );
    ropeSoftBody.appendAnchor( ropeNumSegments, arm.userData.physicsBody, true, influence );

    //创建铰链约束可以旋转
    var pivotA = new Ammo.btVector3( 0, pylonHeight * 0.5, 0 );
    var pivotB = new Ammo.btVector3( 0, -0.2, - armLength * 0.5 );
    var axis = new Ammo.btVector3( 0, 1, 0 );
    hinge = new Ammo.btHingeConstraint( pylon.userData.physicsBody, arm.userData.physicsBody, pivotA, pivotB, axis, axis, true );
    physicsWorld.addConstraint( hinge, true );


}

//创建长方体 入参为长宽高,质量,位置,旋转,材质
function createParalellepiped( sx, sy, sz, mass, pos, quat, material ) {

    var threeObject = new THREE.Mesh( new THREE.BoxGeometry( sx, sy, sz, 1, 1, 1 ), material );
    var shape = new Ammo.btBoxShape( new Ammo.btVector3( sx * 0.5, sy * 0.5, sz * 0.5 ) );
    //设置碰撞检测误差范围,在距离多少时算碰到
    shape.setMargin( margin );

    createRigidBody( threeObject, shape, mass, pos, quat );

    return threeObject;

}

function createRigidBody( threeObject, physicsShape, mass, pos, quat ) {

    threeObject.position.copy( pos );
    threeObject.quaternion.copy( quat );

    //创建运动对象
    var transform = new Ammo.btTransform();
    //重置为单位对象后再设置
    transform.setIdentity();
    transform.setOrigin( new Ammo.btVector3( pos.x, pos.y, pos.z ) );
    transform.setRotation( new Ammo.btQuaternion( quat.x, quat.y, quat.z, quat.w ) );
    var motionState = new Ammo.btDefaultMotionState( transform );

    //通过质量计算局部惯性
    var localInertia = new Ammo.btVector3(0, 0, 0 );
    physicsShape.calculateLocalInertia( mass, localInertia );
    //创建刚体
    var rbInfo = new Ammo.btRigidBodyConstructionInfo( mass, motionState, physicsShape, localInertia );
    var body = new Ammo.btRigidBody( rbInfo );

    threeObject.userData.physicsBody = body;

    scene.add( threeObject );

    if ( mass > 0 ) {
        rigidBodies.push( threeObject );
        //有质量就响应外部的力或碰撞
        body.setActivationState( 4 );
    }

    physicsWorld.addRigidBody( body );

}

function createRandomColor() {
    return Math.floor( Math.random() * ( 1 << 24 ) );
}

function createMaterial() {
    return new THREE.MeshPhongMaterial( { color: createRandomColor() } );
}

6.绑定外部时间,initInput

绑定键盘的qa键设置机械臂的角速度方向

function initInput() {

    window.addEventListener( 'keydown', function( event ) {

        switch ( event.keyCode ) {
            // Q
            case 81:
                armMovement = 1;
                break;

            // A
            case 65:
                armMovement = - 1;
                break;
        }

    }, false );

    window.addEventListener( 'keyup', function( event ) {

        armMovement = 0;

    }, false );

}

7.渲染结果,render

按帧渲染

function render() {
    //获取2次更新的时间差
    var deltaTime = clock.getDelta();
    //更新物理世界和场景
    updatePhysics( deltaTime );
    //更新相机
    controls.update( deltaTime );
    //渲染到画布
    renderer.render( scene, camera );

    time += deltaTime;

}

function updatePhysics( deltaTime ) {

    //根据输入设置铰链的角加速度和最大速度 控制器会自动更新,无需自己更新物理世界的位置
    hinge.enableAngularMotor( true, 1.50 * armMovement, 50 );

    //仿真步进,步进时间和最多分割为多少个子步进
    physicsWorld.stepSimulation( deltaTime, 10 );

    //更新绳子的渲染位置
    var softBody = rope.userData.physicsBody;
    var ropePositions = rope.geometry.attributes.position.array;
    //存了xyz,所以要/3
    var numVerts = ropePositions.length / 3;
    var nodes = softBody.get_m_nodes();
    var indexFloat = 0;
    for ( var i = 0; i < numVerts; i ++ ) {

        var node = nodes.at( i );
        var nodePos = node.get_m_x();
        ropePositions[ indexFloat++ ] = nodePos.x();
        ropePositions[ indexFloat++ ] = nodePos.y();
        ropePositions[ indexFloat++ ] = nodePos.z();

    }
    rope.geometry.attributes.position.needsUpdate = true;

    //更新刚体,从物理世界获取最新位置设置到3D对象的属性里
    for ( var i = 0, il = rigidBodies.length; i < il; i++ ) {
        var objThree = rigidBodies[ i ];
        var objPhys = objThree.userData.physicsBody;
        var ms = objPhys.getMotionState();
        if ( ms ) {

            ms.getWorldTransform( transformAux1 );
            var p = transformAux1.getOrigin();
            var q = transformAux1.getRotation();
            objThree.position.set( p.x(), p.y(), p.z() );
            objThree.quaternion.set( q.x(), q.y(), q.z(), q.w() );

        }
    }

}

8.完整代码

官网示例demo,增加了部分注释,threejs和ammo的库代码自行引入。

<html lang="en">
    <head>
        <title>Ammo.js softbody rope demo</title>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
        <style>
            body {
                color: #61443e;
                font-family:Monospace;
                font-size:13px;
                text-align:center;


                background-color: #bfd1e5;
                margin: 0px;
                overflow: hidden;
            }


            #info {
                position: absolute;
                top: 0px; width: 100%;
                padding: 5px;
            }


            a {


                color: #a06851;
            }
</style>
    </head>
    <body>
<div id="info">Ammo.js soft body rope demo<br>Press Q or A to move the arm.</div>
        <div id="container"><br /><br /><br /><br /><br />Loading...</div>


<script src="builds/ammo.js"></script>


        <script src="js/three/three.min.js"></script>
        <script src="js/three/OrbitControls.js"></script>
        <script src="js/three/Detector.js"></script>
        <script src="js/three/stats.min.js"></script>


        <script>


Ammo().then(function(Ammo) {


// Detects webgl
            if ( ! Detector.webgl ) {
                Detector.addGetWebGLMessage();
                document.getElementById( 'container' ).innerHTML = "";
            }


            // - Global variables -


// Graphics variables
            var container, stats;
            var camera, controls, scene, renderer;
            var textureLoader;
            var clock = new THREE.Clock();


            // Physics variables
            var gravityConstant = -9.8;
var collisionConfiguration;
var dispatcher;
var broadphase;
var solver;
var physicsWorld;
var rigidBodies = [];
var margin = 0.05;
var hinge;
var rope;
var transformAux1 = new Ammo.btTransform();


var time = 0;
var armMovement = 0;


// - Main code -


            init();
            animate();




            // - Functions -


            function init() {


initGraphics();


 initPhysics();
                //
 createObjects();
                //
 initInput();


            }


            function initGraphics() {
                //创建相机
                camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.2, 2000 );
camera.position.x = -7;
camera.position.y = 5;
                camera.position.z =  8;
                //设置用鼠标移动相机
                controls = new THREE.OrbitControls( camera );
                //移动时旋转中心点的坐标
                controls.target.y = 2;


                //设置渲染器为webgl
                renderer = new THREE.WebGLRenderer();
renderer.setClearColor( 0xbfd1e5 );
                //设置像素密度和设备相同
                renderer.setPixelRatio( window.devicePixelRatio );
                renderer.setSize( window.innerWidth, window.innerHeight );
                //启用阴影映射
                renderer.shadowMap.enabled = true;


                //纹理加载器
                textureLoader = new THREE.TextureLoader();


                //创建场景
                scene = new THREE.Scene();


                //环境光
var ambientLight = new THREE.AmbientLight( 0x404040 );
scene.add( ambientLight );


                //定向光源
                var light = new THREE.DirectionalLight( 0xffffff, 1 );
                light.position.set( -10, 10, 5 );
                //光源阴影
light.castShadow = true;
var d = 10;
                //光照范围,近裁剪面,远裁剪面
    light.shadow.camera.left = -d;
    light.shadow.camera.right = d;
    light.shadow.camera.top = d;
    light.shadow.camera.bottom = -d;
    light.shadow.camera.near = 2;
    light.shadow.camera.far = 50;
                //阴影贴图分辨率
    light.shadow.mapSize.x = 1024;
    light.shadow.mapSize.y = 1024;
                scene.add( light );




                //在画布上绑定渲染结果
                container = document.getElementById( 'container' );
                container.innerHTML = "";
                container.appendChild( renderer.domElement );


                //左上角监事面板
                stats = new Stats();
                stats.domElement.style.position = 'absolute';
                stats.domElement.style.top = '0px';
                container.appendChild( stats.domElement );


                //自适应大小
                window.addEventListener( 'resize', onWindowResize, false );


            }


function initPhysics() {
                //创建软体和刚体碰撞的配置
collisionConfiguration = new Ammo.btSoftBodyRigidBodyCollisionConfiguration();
                //创建一个碰撞分发器,它将根据碰撞配置处理碰撞事件
dispatcher = new Ammo.btCollisionDispatcher( collisionConfiguration );
                //创建一个广相位(broadphase)算法,用于检测潜在的碰撞对
broadphase = new Ammo.btDbvtBroadphase();
                //创建一个约束求解器,用于解决物体之间的约束和碰撞响应
solver = new Ammo.btSequentialImpulseConstraintSolver();
                //创建一个软体(soft body)求解器,用于模拟柔软的物体
softBodySolver = new Ammo.btDefaultSoftBodySolver();
                //创建一个软体和刚体混合的物理世界,将前面创建的碰撞分发器、广相位算法、约束求解器、碰撞配置和软体求解器传递给它
physicsWorld = new Ammo.btSoftRigidDynamicsWorld( dispatcher, broadphase, solver, collisionConfiguration, softBodySolver);
                //设置全局重力
                physicsWorld.setGravity( new Ammo.btVector3( 0, gravityConstant, 0 ) );
                physicsWorld.getWorldInfo().set_m_gravity( new Ammo.btVector3( 0,gravityConstant, 0 ) );
            }


            function createObjects() {


                //位置信息
var pos = new THREE.Vector3();
                //旋转信息
var quat = new THREE.Quaternion();


// 地板
pos.set( 0, - 0.5, 0 );
quat.set( 0, 0, 0, 1 );
                //质量为0,所以不会掉下去
var ground = createParalellepiped( 40, 1, 40, 0, pos, quat, new THREE.MeshPhongMaterial( { color: 0xFFFFFF } ) );
                //产生阴影
ground.castShadow = true;
                //显示其他物理产生的阴影
ground.receiveShadow = true;
textureLoader.load( "../textures/grid.png", function( texture ) {
                    //设置为重复平铺
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
                    //每行每列重复40次
texture.repeat.set( 40, 40 );
                    //设置材质纹理
ground.material.map = texture;
                    //修改后通知刷新材质
ground.material.needsUpdate = true;
} );




// 球
var ballMass = 0.2;
var ballRadius = 0.6;


var ball = new THREE.Mesh( new THREE.SphereGeometry( ballRadius, 20, 20 ), new THREE.MeshPhongMaterial( { color: 0x202020 } ) );
ball.castShadow = true;
ball.receiveShadow = true;
var ballShape = new Ammo.btSphereShape( ballRadius );
ballShape.setMargin( margin );
pos.set( -3, 2, 0 );
quat.set( 0, 0, 0, 1 );
createRigidBody( ball, ballShape, ballMass, pos, quat );
                //设置摩擦系数
ball.userData.physicsBody.setFriction( 0.5 );


// 墙
var brickMass = 0.5;
var brickLength = 1.2;
var brickDepth = 0.6;
var brickHeight = brickLength * 0.5;
var numBricksLength = 6;
var numBricksHeight = 8;
var z0 = - numBricksLength * brickLength * 0.5;
pos.set( 0, brickHeight * 0.5, z0 );
quat.set( 0, 0, 0, 1 );
                //创建长方体组成的墙壁
for ( var j = 0; j < numBricksHeight; j ++ ) {


var oddRow = ( j % 2 ) == 1;


pos.z = z0;


if ( oddRow ) {
pos.z -= 0.25 * brickLength;
}


var nRow = oddRow? numBricksLength + 1 : numBricksLength;
for ( var i = 0; i < nRow; i ++ ) {


var brickLengthCurrent = brickLength;
var brickMassCurrent = brickMass;
if ( oddRow && ( i == 0 || i == nRow - 1 ) ) {
brickLengthCurrent *= 0.5;
brickMassCurrent *= 0.5;
}


var brick = createParalellepiped( brickDepth, brickHeight, brickLengthCurrent, brickMassCurrent, pos, quat, createMaterial() );
brick.castShadow = true;
brick.receiveShadow = true;


if ( oddRow && ( i == 0 || i == nRow - 2 ) ) {
pos.z += 0.75 * brickLength;
}
else {
pos.z += brickLength;
}


}
pos.y += brickHeight;
}


// 绳
var ropeNumSegments = 10;
var ropeLength = 4;
var ropeMass = 3;
var ropePos = ball.position.clone();
ropePos.y += ballRadius;


var segmentLength = ropeLength / ropeNumSegments;
var ropeGeometry = new THREE.BufferGeometry();
var ropeMaterial = new THREE.LineBasicMaterial( { color: 0x000000 } );
var ropePositions = [];
var ropeIndices = [];


for ( var i = 0; i < ropeNumSegments + 1; i++ ) {
ropePositions.push( ropePos.x, ropePos.y + i * segmentLength, ropePos.z );
}


for ( var i = 0; i < ropeNumSegments; i++ ) {
ropeIndices.push( i, i + 1 );
}


ropeGeometry.setIndex( new THREE.BufferAttribute( new Uint16Array( ropeIndices ), 1 ) );
ropeGeometry.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( ropePositions ), 3 ) );
ropeGeometry.computeBoundingSphere();
rope = new THREE.LineSegments( ropeGeometry, ropeMaterial );
rope.castShadow = true;
rope.receiveShadow = true;
scene.add( rope );


// 绳
var softBodyHelpers = new Ammo.btSoftBodyHelpers();
var ropeStart = new Ammo.btVector3( ropePos.x, ropePos.y, ropePos.z );
var ropeEnd = new Ammo.btVector3( ropePos.x, ropePos.y + ropeLength, ropePos.z );
var ropeSoftBody = softBodyHelpers.CreateRope( physicsWorld.getWorldInfo(), ropeStart, ropeEnd, ropeNumSegments - 1, 0 );
var sbConfig = ropeSoftBody.get_m_cfg();
sbConfig.set_viterations( 10 );
sbConfig.set_piterations( 10 );
ropeSoftBody.setTotalMass( ropeMass, false )
Ammo.castObject( ropeSoftBody, Ammo.btCollisionObject ).getCollisionShape().setMargin( margin * 3 );
                //1是碰撞组,同一组的不碰撞,-1是其与其他组都碰撞
physicsWorld.addSoftBody( ropeSoftBody, 1, -1 );
rope.userData.physicsBody = ropeSoftBody;
ropeSoftBody.setActivationState( 4 );


// 基座和旋转臂
var armMass = 2;
var armLength = 3;
var pylonHeight = ropePos.y + ropeLength;
var baseMaterial = new THREE.MeshPhongMaterial( { color: 0x606060 } );
pos.set( ropePos.x, 0.1, ropePos.z - armLength );
quat.set( 0, 0, 0, 1 );
var base = createParalellepiped( 1, 0.2, 1, 0, pos, quat, baseMaterial );
base.castShadow = true;
base.receiveShadow = true;
pos.set( ropePos.x, 0.5 * pylonHeight, ropePos.z - armLength );
var pylon = createParalellepiped( 0.4, pylonHeight, 0.4, 0, pos, quat, baseMaterial );
pylon.castShadow = true;
pylon.receiveShadow = true;
pos.set( ropePos.x, pylonHeight + 0.2, ropePos.z - 0.5 * armLength );
var arm = createParalellepiped( 0.4, 0.4, armLength + 0.4, armMass, pos, quat, baseMaterial );
arm.castShadow = true;
arm.receiveShadow = true;


// 绳子两端绑定
var influence = 1;
ropeSoftBody.appendAnchor( 0, ball.userData.physicsBody, true, influence );
ropeSoftBody.appendAnchor( ropeNumSegments, arm.userData.physicsBody, true, influence );


//创建铰链约束可以旋转
var pivotA = new Ammo.btVector3( 0, pylonHeight * 0.5, 0 );
var pivotB = new Ammo.btVector3( 0, -0.2, - armLength * 0.5 );
var axis = new Ammo.btVector3( 0, 1, 0 );
hinge = new Ammo.btHingeConstraint( pylon.userData.physicsBody, arm.userData.physicsBody, pivotA, pivotB, axis, axis, true );
physicsWorld.addConstraint( hinge, true );




            }


            //创建长方体 入参为长宽高,质量,位置,旋转,材质
            function createParalellepiped( sx, sy, sz, mass, pos, quat, material ) {


var threeObject = new THREE.Mesh( new THREE.BoxGeometry( sx, sy, sz, 1, 1, 1 ), material );
var shape = new Ammo.btBoxShape( new Ammo.btVector3( sx * 0.5, sy * 0.5, sz * 0.5 ) );
                //设置碰撞检测误差范围,在距离多少时算碰到
shape.setMargin( margin );


createRigidBody( threeObject, shape, mass, pos, quat );


return threeObject;


            }


            function createRigidBody( threeObject, physicsShape, mass, pos, quat ) {


              threeObject.position.copy( pos );
              threeObject.quaternion.copy( quat );


                //创建运动对象
var transform = new Ammo.btTransform();
                //重置为单位对象后再设置
          transform.setIdentity();
          transform.setOrigin( new Ammo.btVector3( pos.x, pos.y, pos.z ) );
          transform.setRotation( new Ammo.btQuaternion( quat.x, quat.y, quat.z, quat.w ) );
var motionState = new Ammo.btDefaultMotionState( transform );


                //通过质量计算局部惯性
var localInertia = new Ammo.btVector3(0, 0, 0 );
      physicsShape.calculateLocalInertia( mass, localInertia );
                //创建刚体
      var rbInfo = new Ammo.btRigidBodyConstructionInfo( mass, motionState, physicsShape, localInertia );
      var body = new Ammo.btRigidBody( rbInfo );


threeObject.userData.physicsBody = body;


scene.add( threeObject );


if ( mass > 0 ) {
rigidBodies.push( threeObject );
                    //有质量就响应外部的力或碰撞
body.setActivationState( 4 );
}


physicsWorld.addRigidBody( body );


            }


function createRandomColor() {
return Math.floor( Math.random() * ( 1 << 24 ) );
}


            function createMaterial() {
              return new THREE.MeshPhongMaterial( { color: createRandomColor() } );
            }


            function initInput() {


              window.addEventListener( 'keydown', function( event ) {


switch ( event.keyCode ) {
// Q
case 81:
armMovement = 1;
break;


// A
case 65:
armMovement = - 1;
break;
}


              }, false );


window.addEventListener( 'keyup', function( event ) {


armMovement = 0;


              }, false );


            }


            function onWindowResize() {


                camera.aspect = window.innerWidth / window.innerHeight;
                camera.updateProjectionMatrix();


                renderer.setSize( window.innerWidth, window.innerHeight );


            }


            function animate() {


                requestAnimationFrame( animate );


                render();
                stats.update();


            }


            function render() {
                //获取2次更新的时间差
              var deltaTime = clock.getDelta();
                //更新物理世界和场景
              updatePhysics( deltaTime );
                //更新相机
                controls.update( deltaTime );
                //渲染到画布
                renderer.render( scene, camera );


                time += deltaTime;


            }


            function updatePhysics( deltaTime ) {


              //根据输入设置铰链的角加速度和最大速度 控制器会自动更新,无需自己更新物理世界的位置
              hinge.enableAngularMotor( true, 1.50 * armMovement, 50 );


//仿真步进,步进时间和最多分割为多少个子步进
physicsWorld.stepSimulation( deltaTime, 10 );


//更新绳子的渲染位置
var softBody = rope.userData.physicsBody;
var ropePositions = rope.geometry.attributes.position.array;
                //存了xyz,所以要/3
var numVerts = ropePositions.length / 3;
var nodes = softBody.get_m_nodes();
var indexFloat = 0;
for ( var i = 0; i < numVerts; i ++ ) {


var node = nodes.at( i );
var nodePos = node.get_m_x();
ropePositions[ indexFloat++ ] = nodePos.x();
ropePositions[ indexFloat++ ] = nodePos.y();
ropePositions[ indexFloat++ ] = nodePos.z();


}
rope.geometry.attributes.position.needsUpdate = true;


    //更新刚体,从物理世界获取最新位置设置到3D对象的属性里
    for ( var i = 0, il = rigidBodies.length; i < il; i++ ) {
      var objThree = rigidBodies[ i ];
      var objPhys = objThree.userData.physicsBody;
var ms = objPhys.getMotionState();
if ( ms ) {


          ms.getWorldTransform( transformAux1 );
var p = transformAux1.getOrigin();
var q = transformAux1.getRotation();
objThree.position.set( p.x(), p.y(), p.z() );
objThree.quaternion.set( q.x(), q.y(), q.z(), q.w() );


        }
    }


}


});
</script>


    </body>
</html>