三维三体

显示所有步骤

之前的两体,三体,N体都是在同一平面空间里的,而我们真实的宇宙是三维空间的, 天体在空间中的定位有三个坐标。现在我们来要看看在三维宇宙空间里万有引力作用下的三体, 先看看运行效果, 可以触摸(鼠标左键)滑动,360°旋转:

绘制三维空间效果比较复杂,浏览器提供的API函数,不能简单实现,所以用了个第三方库来做基础: three.js, 同时为了实现交互操作 - 可在宇宙空间里旋转查看三体各个角度的运行动态,还使用里个three.js提供的控制代码 OrbitControls.js。

首先我们对万有引力在二维平面空间的实现代码做了升级- 在二维坐标x, y的基础上增加了代表第三维的z坐标, 原来相应的函数,计算代码只需做了简单的扩展:

  function distance3(A, B){
      var dX = A.x - B.x;
      var dY = A.y - B.y;
      var dZ = A.z - B.z;
      return Math.sqrt(dX*dX + dY*dY + dZ*dZ);
  }

  function force3(A, B, d){
      if ( d === undefined ) d = distance3(A,B);
      if ( !d ) return 0;

      return G * A.m * B.m / d / d;
  }

  function direction3(A, B, normalize, dist){
      if (normalize){
          if (!dist) dist = distance3(A,B);

          if(!dist)
              return {x: 0, y: 0, z: 0};
          else
              return { x: (B.x-A.x)/dist, y: (B.y-A.y)/dist, z: (B.z-A.z)/dist};
      }
      else{
          return {x: B.x - A.x, y: B.y - A.y, z: B.z - A.z};
      }
  }

  function updateObject3(obj, scalarF, normDirect, deltaT){
      obj.forced.x = scalarF * normDirect.x;
      obj.forced.y = scalarF * normDirect.y;
      obj.forced.z = scalarF * normDirect.z;

      obj.acc.x = obj.forced.x / obj.m;
      obj.acc.y = obj.forced.y / obj.m;
      obj.acc.z = obj.forced.z / obj.m;

      obj.dv.x = obj.acc.x * deltaT;
      obj.dv.y = obj.acc.y * deltaT;
      obj.dv.z = obj.acc.z * deltaT;

      obj.x = obj.x + (obj.v.x + obj.dv.x/2) * deltaT;
      obj.y = obj.y + (obj.v.y + obj.dv.y/2) * deltaT;
      obj.z = obj.z + (obj.v.z + obj.dv.z/2) * deltaT;

      obj.v.x = obj.v.x + obj.dv.x;
      obj.v.y = obj.v.y + obj.dv.y;
      obj.v.z = obj.v.z + obj.dv.z;

  }

  function calcUpdate2Object3(A, B, deltaT){
      var d = distance3(A, B);
      var f = force3(A, B, d);
      var fAB = direction3(A, B, true, d);
      var fBA = {x: -fAB.x, y: -fAB.y, z: -fAB.z};

      updateObject3(A, f, fAB, deltaT);
      updateObject3(B, f, fBA, deltaT);
  }

  function makeObject3(name, m, r, x, y, z, v, c){
       return {
          name: name,
        m: m,
        r:  r,
        x: x || 0,
        y: y || 0,
        z: z || 0,
        v: v || {x: 0, y: 0, z: 0},
        color: c || 'red',

        forced: {x: 0, y: 0, z: 0},
        acc: {x: 0, y: 0, z: 0},
        dv: {x: 0, y: 0, z: 0},
        path: [x, y, z],
    };
  }

然后增加了些三维空间图形显示相关的几个函数:

  function createNewLine(startingPoint, lineMat, scene){
    var geometry = new THREE.Geometry();
    for (var i=0; i< max_len; i++){
      geometry.vertices.push(startingPoint.clone());
    }
    var theLine = new THREE.Line(geometry, lineMat);
    theLine.geometry.dynamic = true;
    scene.add(theLine);
    return theLine
  }

  function addPoint(myPoint, theLine) {
    theLine.geometry.vertices.push(theLine.geometry.vertices.shift());
    theLine.geometry.vertices[max_len-1] = myPoint;
    theLine.geometry.verticesNeedUpdate = true;
  }

  function createSkySphere(radius, scene, camera, imgUrl) {
    var texture = new THREE.TextureLoader().load(imgUrl);
    var matSky = new THREE.MeshPhongMaterial({ map: texture, overdraw: 0.5 } );
    matSky.side = THREE.BackSide;
    var sphereSky = new THREE.SphereGeometry(radius, 128 , 128);
    var skyMesh = new THREE.Mesh( sphereSky, matSky );
    scene.add(skyMesh);
    skyMesh.position.set(camera.position.x, camera.position.y, camera.position.z);
    return skyMesh;
  }

  function render(renderer, camera, scene, target) {
    renderer.clear();
    camera.lookAt(target.position);
    renderer.render(scene, camera);
  }

最后是对上面几个方面代码对综合调用,完成计算,更新,交互,展示,代码稍复杂点:

  var canvas = document.getElementById('bodies-canvas');
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;

  var spaceUnit = canvas.width/ 1000;
  var posX = 0 * spaceUnit, posY = 0 * spaceUnit, posZ = 0 * spaceUnit;

  var mass = 1.0 * Math.pow(10, 14);
  var radius = spaceUnit * 40;
  var x = posX;
  var y = posY;
  var z = posZ;
  var velocity = {x:0.1,y:0, z:0};
  var color = 'red';
  var objA = makeObject('objA',    
                        mass, radius, x, y, z, velocity, color) 

  mass = 1.0 * Math.pow(10, 11);
  radius = spaceUnit * 20;
  x = posX + 200*spaceUnit;
  y = posY + 200*spaceUnit;
  z = posZ + 200*spaceUnit;
  velocity = {x:0.0, y:-1, z:2.5};
  color = 'blue';
  var objB = makeObject('objB',
                         mass, radius, x, y, z, velocity, color) 

  mass = 1.0 * Math.pow(10, 12);
  radius = spaceUnit * 15;
  x = posX;
  y = posY - 400*spaceUnit;
  z = posZ;
  velocity = {x:1, y:0.5, z:2.0};
  color = 'orange';
  var objC = makeObject('objC',
                         mass, radius, x, y, z, velocity, color)
  console.info(objA);
  console.info(objB);
  console.info(objC);

  var camera, scene, renderer, controls;
  var fov = 60, near = 1, far = 10000 * spaceUnit;
  var aspect = canvas.width / canvas.height;

  var meshA, meshB, meshC;
  var lineA, lineB, lineC;

  var matA = new THREE.LineBasicMaterial({linewidth: 1, color: objA.color});
  var pointA = new THREE.Vector3(objA.x, objA.y, objA.z);

  var matB = new THREE.LineBasicMaterial({linewidth: 1, color: objB.color});
  var pointB = new THREE.Vector3(objB.x, objB.y, objB.z);

  var matC = new THREE.LineBasicMaterial({linewidth: 1, color: objC.color});
  var pointC = new THREE.Vector3(objC.x, objC.y, objC.z);

  function init() {
    camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
    scene = new THREE.Scene();

    scene.add(new THREE.AmbientLight(0x999999));

    controls = new THREE.OrbitControls( camera );

    lineA = createNewLine(pointA, matA, scene);
    lineB = createNewLine(pointB, matB, scene);
    lineC = createNewLine(pointC, matC, scene);

    meshA = new THREE.Mesh(
        new THREE.SphereBufferGeometry( objA.r, 16, 8 ),
        new THREE.MeshBasicMaterial( { color: objA.color, wireframe: true } )
    );
    scene.add( meshA );
    meshA.position.set(objA.x, objA.y, objA.z);

    meshB = new THREE.Mesh(
        new THREE.SphereBufferGeometry( objB.r, 16, 8 ),
        new THREE.MeshBasicMaterial( { color: objB.color, wireframe: true } )
    );
    scene.add( meshB );
    meshB.position.set(objB.x, objB.y, objB.z);

    meshC = new THREE.Mesh(
        new THREE.SphereBufferGeometry( objC.r, 16, 8 ),
        new THREE.MeshBasicMaterial( { color: objC.color, wireframe: true } )
    );
    scene.add( meshC );
    meshC.position.set(objC.x, objC.y, objC.z);

    var maxX = Math.max(meshA.position.x + objA.r,
                                            meshB.position.x + objB.r,
                                            meshC.position.x + objC.r);
    var maxY = Math.max(meshA.position.y + objA.r,
                                            meshB.position.y + objB.r,
                                            meshC.position.y + objC.r);
    var maxZ = Math.max(meshA.position.z + objA.r,
                                            meshB.position.z + objB.r,
                                            meshC.position.z + objC.r);

    camera.position.x = maxX;
    camera.position.z = maxZ + 1 + 1500*spaceUnit;
    console.log('camera.position:', camera.position);

    var r = 2 * Math.max(maxX, maxY, maxZ, camera.position.z);
    createSkySphere(r, scene, camera, '/images/newton-law/space.jpg');

    renderer = new THREE.WebGLRenderer({canvas: canvas, antialias: true});
    renderer.setSize(canvas.width, canvas.height);
    window.addEventListener('resize', onWindowResized, false);
    onWindowResized(null);
  }

  function onWindowResized(event) {
    if (canvas && renderer && camera){
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      renderer.setSize(canvas.width, canvas.height);
      renderer.setViewport(0, 0, canvas.width, canvas.height);
      camera.aspect = canvas.width / canvas.height;
      camera.updateProjectionMatrix();
    }
  }

  var limitDraw = 1000/30, limitCalc = 1000/30, calcTimes = 10;
  var lastDrawTime = Date.now() - limitDraw;
  var lastCalcTime = Date.now() - limitCalc;
  function update() {
    var now = Date.now();
    var deltaCalc = now - lastCalcTime;
    if (deltaCalc >= limitCalc) {
      lastCalcTime = now;
      for(var i = 0; i < calcTimes; i++){
        calcUpdate2Object(objA, objB, limitCalc/1000);

        calcUpdate2Object(objA, objC, limitCalc/1000);
        calcUpdate2Object(objB, objC, limitCalc/1000);
      }
    }

    var deltaDraw = now - lastDrawTime;
    if (deltaDraw >= limitDraw) {
      meshA.position.set(objA.x, objA.y, objA.z);
      meshB.position.set(objB.x, objB.y, objB.z);
      meshC.position.set(objC.x, objC.y, objC.z);

      meshA.rotation.y += 0.005;
      meshB.rotation.y += 0.01;
      meshC.rotation.y += 0.01;

      pointA = new THREE.Vector3(objA.x, objA.y, objA.z);
      addPoint(pointA, lineA);

      pointB = new THREE.Vector3(objB.x, objB.y, objB.z);
      addPoint(pointB, lineB);

      pointC = new THREE.Vector3(objC.x, objC.y, objC.z);
      addPoint(pointC, lineC);

      controls.update(deltaDraw);

      render(renderer, camera, scene, meshA);
    }

    requestAnimationFrame(update);
  }

  init();
  update();

最后(全屏体验):