3 931 Codepen

Летающие самолетики

Генератор летающих самолетиков на three.js. Эффект похож на стаи птиц в небе.


JS

Подключенные библиотеки:
https://cdnjs.cloudflare.com/ajax/libs/three.js/84/three.min.js
https://threejs.org/examples/js/controls/OrbitControls.js
https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5/dat.gui.min.js
сам скрипт
//Based on:
//http://www.red3d.com/cwr/boids/
//http://www.kfish.org/boids/pseudocode.html

var canvas = document.getElementById("canvas");
var TWO_PI = Math.PI*2;

  const mobile = ( navigator.userAgent.match(/Android/i)
      || navigator.userAgent.match(/webOS/i)
      || navigator.userAgent.match(/iPhone/i)
      //|| navigator.userAgent.match(/iPad/i)
      || navigator.userAgent.match(/iPod/i)
      || navigator.userAgent.match(/BlackBerry/i)
      || navigator.userAgent.match(/Windows Phone/i)
      );

//Spatial variables
var width = 2000;
var height = 2000;
var depth = 2000;
var centre = [width/2,height/2, depth/2];

//Agents
var boids = [];
var food = [];
var predators = [];

var boidCount;
if(mobile){
  boidCount = 200;
}else{
  boidCount = 600;
}

var foodCount = 10;
var predatorCount = 3;

var periodic = false;
var sphere = true;
var toggle_predators = false;

//Boid movement variables
var neighbourhood = width/6;
var proximity = width/40;
var minNeighbour = width/10;
var maxNeighbour = width/2;
var minProx = width/100;
var maxProx = 4*proximity;
var speed = 3;

var scene = new THREE.Scene();

var renderer = new THREE.WebGLRenderer({antialias: true, canvas: canvas});
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor( 0x66deff, 1);
document.body.appendChild( renderer.domElement );

distance = 400;

var FOV = 2 * Math.atan( window.innerHeight / ( 2 * distance ) ) * 90 / Math.PI;

var camera = new THREE.PerspectiveCamera(FOV, window.innerWidth / window.innerHeight, 1, 20000);

camera.position.set(centre[0], centre[1], -1000);

window.addEventListener( 'resize', onWindowResize, false );

function onWindowResize(){

  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
    renderer.setSize( window.innerWidth, window.innerHeight );
}

// --------------- //

//Lights
var light_1 = new THREE.AmbientLight( 0xffffff, 1.7); 
scene.add(light_1);

var light_2;
light_2 = new THREE.DirectionalLight(0xa0a0a0, 0.5);
light_2.position.set(0, -1, 0);
scene.add(light_2);

var light_3;
light_3 = new THREE.DirectionalLight(0xa0a0a0, 1);
light_3.position.set(0, 1, -1);
scene.add(light_3);

//Paper plane geometry
var geom = new THREE.Geometry();
var p_geom = new THREE.SphereGeometry( 5, 32, 32 );

//Brackets for purely aesthetic considerations
{
  geom.vertices.push(
      new THREE.Vector3(   0,  0.5,  0 ),
      new THREE.Vector3( -0.4, -0.6, 0 ),
      new THREE.Vector3( -0.1, -0.6, -0.1 ),
      new THREE.Vector3(   0, -0.6, 0.15 ),
      new THREE.Vector3(  0.1, -0.6, -0.1 ),
      new THREE.Vector3(  0.4, -0.6, 0 )
      );
}

geom.faces.push( new THREE.Face3( 0, 1, 2 ) );
geom.faces.push( new THREE.Face3( 0, 2, 3 ) );
geom.faces.push( new THREE.Face3( 0, 3, 4 ) );
geom.faces.push( new THREE.Face3( 0, 4, 5 ) );

geom.scale(30,30,30);
//Rotate planes to point forward with their noses
geom.rotateX( Math.PI / 2 );

var colour = 0xffffff;

var material = new THREE.MeshStandardMaterial( {color: colour, side: THREE.DoubleSide, shading: THREE.FlatShading} );
var p_material = new THREE.MeshStandardMaterial( {color: 0xff0000, side: THREE.DoubleSide, shading: THREE.FlatShading} );

//Generate random boids
for(i = 0; i < boidCount; i++){

  var g_ = new THREE.Mesh(geom, material);
  var x = 0.5 - Math.random();
  var y = 0.5 - Math.random();
  var z = 0.5 - Math.random();

  var boid = {
    vel_x: x,
    vel_y: y,
    vel_z: z,
    geo: g_
  };

  boid.geo.position.x = width/2 - Math.random() * width;
  boid.geo.position.y = height/2 - Math.random() * height;
  boid.geo.position.z = depth/2 - Math.random() * depth;
  boids.push(boid);
}

for(i = 0; i < boids.length; i++){
  scene.add(boids[i].geo);
}

//Generate food and predators
for(i = 0; i < predatorCount; i++){

  var g_ = new THREE.Mesh(geom, p_material);
  var x = 0.5 - Math.random();
  var y = 0.5 - Math.random();
  var z = 0.5 - Math.random();

  var predator = {
    vel_x: 0,
    vel_y: 0,
    vel_z: 0,
    geo: g_
  };

  predator.geo.position.x = width/2 - Math.random() * width;
  predator.geo.position.y = height/2 - Math.random() * height;
  predator.geo.position.z = depth/2 - Math.random() * depth;
  if(!toggle_predators){
    predator.geo.visible = false;
  }
  predators.push(predator);
};


for(i = 0; i < predators.length; i++){
  scene.add(predators[i].geo);
}

//-----------GUI-----------//
//dat.gui library controls
var reset_button = { reset:function(){ 
  toggle_predators = false;
  setTransparency();
  colour = 0xffffff;
  setColour(colour);
  periodic = false;
  sphere = true;
  speed = 3;
  neighbourhood = width/6;
  proximity = width/40;
  
  for(i = 0; i < boidCount; i++){

  var x = 0.5 - Math.random();
  var y = 0.5 - Math.random();
  var z = 0.5 - Math.random();

    boids[i].vel_x = x;
    boids[i].vel_y = y;
    boids[i].vel_z = z;

  boids[i].geo.position.x = width/2 - Math.random() * width;
  boids[i].geo.position.y = height/2 - Math.random() * height;
  boids[i].geo.position.z = depth/2 - Math.random() * depth;
}

}};

var gui = new dat.GUI();

gui.add(this, 'proximity').min(minProx).max(maxProx).step(10).listen();
gui.add(this, 'neighbourhood').min(minNeighbour).max(maxNeighbour).step(10).listen();
gui.add(this, 'speed').min(0).max(10).step(1).listen();
gui.add(this, 'sphere').listen().onchange(function(value) { periodic = false; sphere = true;} );
gui.add(this, 'periodic').listen().onchange(function(value) { periodic = true; sphere = false;} );
gui.addColor(this, 'colour').listen().onchange(function(value) { setColour();} );
gui.add(this, 'toggle_predators').listen().onchange(function(value){ setTransparency();});
gui.add(reset_button, 'reset');

gui.close();

function setColour(){
  material.color.setHex(colour);
}

function setTransparency(){
  if(toggle_predators){
    for(p = 0; p < predatorCount; p++){
      predators[p].geo.visible = true
      predators[p].geo.position.x = width/2 - Math.random() * width;
      predators[p].geo.position.y = height/2 - Math.random() * height;
      predators[p].geo.position.z = depth/2 - Math.random() * depth;;
    }
  }else{
    for(p = 0; p < predatorCount; p++){
      predators[p].geo.visible = false;
    }
  }
}


//----------RULES----------//
function rules(i){
  var direction = {
    x: 0,
    y: 0,
    z: 0
  } 
  var position = {
    x: 0,
    y: 0,
    z: 0
  };
  var spread = {
    x: 0,
    y: 0,
    z: 0
  };

  var neighbours = 0;
  var dist;
  var dot;
  for(j = 0; j < boidCount; j++){
    if(i != j){
      dist = Math.sqrt((boids[i].geo.position.x - boids[j].geo.position.x) * (boids[i].geo.position.x - boids[j].geo.position.x) + (boids[i].geo.position.y - boids[j].geo.position.y) * (boids[i].geo.position.y - boids[j].geo.position.y) +(boids[i].geo.position.z - boids[j].geo.position.z) * (boids[i].geo.position.z - boids[j].geo.position.z));
      dot = boids[i].vel_x * (boids[j].geo.position.x - boids[i].geo.position.x) + boids[i].vel_y * (boids[j].geo.position.y - boids[i].geo.position.y)  + boids[i].vel_z * (boids[j].geo.position.z - boids[i].geo.position.z);  
      if((dist < neighbourhood) && (dot > -0.75)){
        neighbours++;
        direction.x += boids[j].vel_x;
        direction.y += boids[j].vel_y;
        direction.z += boids[j].vel_z;

        position.x += boids[j].geo.position.x;
        position.y += boids[j].geo.position.y;
        position.z += boids[j].geo.position.z;
      }

      if(dist < proximity){
        spread.x = (boids[i].geo.position.x - boids[j].geo.position.x);
        spread.y = (boids[i].geo.position.y - boids[j].geo.position.y);
        spread.z = (boids[i].geo.position.z - boids[j].geo.position.z);

        boids[i].vel_x += (spread.x)/500;
        boids[i].vel_y += (spread.y)/500;
        boids[i].vel_z += (spread.z)/500;  
      }    
    }
  }

  if(toggle_predators){
    for(p = 0; p < predatorCount; p++){
      dist = Math.sqrt((boids[i].geo.position.x - predators[p].geo.position.x) * (boids[i].geo.position.x - predators[p].geo.position.x) + (boids[i].geo.position.y - predators[p].geo.position.y) * (boids[i].geo.position.y - predators[p].geo.position.y) +(boids[i].geo.position.z - predators[p].geo.position.z) * (boids[i].geo.position.z - predators[p].geo.position.z));
      if(dist < 0.5*neighbourhood){

        spread.x = (boids[i].geo.position.x - predators[p].geo.position.x);
        spread.y = (boids[i].geo.position.y - predators[p].geo.position.y);
        spread.z = (boids[i].geo.position.z - predators[p].geo.position.z);

        boids[i].vel_x += (spread.x)/150;
        boids[i].vel_y += (spread.y)/150;
        boids[i].vel_z += (spread.z)/150;  
      }
    }
  }
  direction.x /= neighbours;
  direction.y /= neighbours;
  direction.z /= neighbours;

  position.x /= neighbours;
  position.y /= neighbours;
  position.z /= neighbours;

  if(neighbours > 0){
    boids[i].vel_x += (direction.x - boids[i].vel_x)/200;
    boids[i].vel_y += (direction.y - boids[i].vel_y)/200;
    boids[i].vel_z += (direction.z - boids[i].vel_z)/200;

    boids[i].vel_x += (position.x - boids[i].geo.position.x)/500;
    boids[i].vel_y += (position.y - boids[i].geo.position.y)/500;
    boids[i].vel_z += (position.z - boids[i].geo.position.z)/500; 
  }
}

function selectPrey(p){
  var prey = 0;
  var minDist = width * 100;

  var position = {
    x: 0,
    y: 0,
    z: 0
  };

  for(i = 0; i < boidCount; i++){
    dist = Math.sqrt((boids[i].geo.position.x - predators[p].geo.position.x) * (boids[i].geo.position.x - predators[p].geo.position.x) + (boids[i].geo.position.y - predators[p].geo.position.y) * (boids[i].geo.position.y - predators[p].geo.position.y) +(boids[i].geo.position.z - predators[p].geo.position.z) * (boids[i].geo.position.z - predators[p].geo.position.z));
    dot = predators[p].vel_x * (boids[i].geo.position.x - predators[p].geo.position.x) + predators[p].vel_y * (boids[i].geo.position.y - predators[p].geo.position.y)  + predators[p].vel_z * (boids[i].geo.position.z - predators[p].geo.position.z);  
    if((Math.abs(dist) < Math.abs(minDist)) && (dot > 0.75)){
      minDist = dist;
      prey = i;  
    }
  }

  position.x = (predators[p].geo.position.x - boids[prey].geo.position.x);
  position.y = (predators[p].geo.position.y - boids[prey].geo.position.y);
  position.z = (predators[p].geo.position.z - boids[prey].geo.position.z);
 
  predators[p].vel_x -= position.x/150;
  predators[p].vel_y -= position.y/150;
  predators[p].vel_z -= position.z/150;
}

//----------MOVE----------//
function move(){
  for(i = 0; i < boidCount; i++){

    if(periodic){
      if(boids[i].geo.position.x > width/2){
        boids[i].geo.position.x = -width/2+10;
      }
      if(boids[i].geo.position.y > height/2){
        boids[i].geo.position.y = -height/2+10;
      }
      if(boids[i].geo.position.z > depth/2){
        boids[i].geo.position.z = -depth/2+10;
      }

      if(boids[i].geo.position.x < -width/2){
        boids[i].geo.position.x = width/2-10;
      }
      if(boids[i].geo.position.y < -height/2){
        boids[i].geo.position.y = height/2-10;
      }
      if(boids[i].geo.position.z < -depth/2){
        boids[i].geo.position.z = depth/2-10;
      }
    }

    if(sphere){
      var dist = Math.sqrt(boids[i].geo.position.x * boids[i].geo.position.x + boids[i].geo.position.y * boids[i].geo.position.y + boids[i].geo.position.z * boids[i].geo.position.z); 
      if(dist > width/2){
        boids[i].vel_x -= boids[i].geo.position.x/5000;
        boids[i].vel_y -= boids[i].geo.position.y/5000;
        boids[i].vel_z -= boids[i].geo.position.z/5000;
      }
    }

    var magnitude = Math.sqrt((boids[i].vel_x * boids[i].vel_x) + (boids[i].vel_y * boids[i].vel_y) + (boids[i].vel_z * boids[i].vel_z));

    if(magnitude > 0){
      boids[i].vel_x /= magnitude;
      boids[i].vel_y /= magnitude;
      boids[i].vel_z /= magnitude;
    }

    if(speed > 0){
      boids[i].vel_x *= speed;
      boids[i].vel_y *= speed;
      boids[i].vel_z *= speed;

      boids[i].geo.position.x += boids[i].vel_x;
      boids[i].geo.position.y += boids[i].vel_y;
      boids[i].geo.position.z += boids[i].vel_z;

      boids[i].geo.lookAt(new THREE.Vector3(boids[i].geo.position.x + boids[i].vel_x, boids[i].geo.position.y + boids[i].vel_y, boids[i].geo.position.z + boids[i].vel_z));
    }
  }

  if(toggle_predators){
    for(p = 0; p < predatorCount; p++){
      var dist = Math.sqrt(predators[p].geo.position.x * predators[p].geo.position.x + predators[p].geo.position.y * predators[p].geo.position.y + predators[p].geo.position.z * predators[p].geo.position.z); 
      if(dist > width/2){
        predators[p].vel_x -= predators[p].geo.position.x/50;
        predators[p].vel_y -= predators[p].geo.position.y/50;
        predators[p].vel_z -= predators[p].geo.position.z/50;
      }

      var magnitude = Math.sqrt((predators[p].vel_x * predators[p].vel_x) + (predators[p].vel_y * predators[p].vel_y) + (predators[p].vel_z * predators[p].vel_z));

      if(magnitude > 0){
        predators[p].vel_x /= magnitude;
        predators[p].vel_y /= magnitude;
        predators[p].vel_z /= magnitude;
      }

      if(speed > 0){
        predators[p].vel_x *= speed;
        predators[p].vel_y *= speed;
        predators[p].vel_z *= speed;

        predators[p].geo.position.x += predators[p].vel_x;
        predators[p].geo.position.y += predators[p].vel_y;
        predators[p].geo.position.z += predators[p].vel_z;

        predators[p].geo.lookAt(new THREE.Vector3(predators[p].geo.position.x + predators[p].vel_x, predators[p].geo.position.y + predators[p].vel_y, predators[p].geo.position.z + predators[p].vel_z));
      }
    }
  } 
}


//OrbitControls.js for camera manipulation
controls = new THREE.OrbitControls( camera, renderer.domElement );

//----------DRAW----------//
function draw(){

  if(toggle_predators){
  for(p = 0; p < predatorCount; p++){
    selectPrey(p);
  }
  }

  for(i = 0; i < boidCount; i++){
    rules(i);
  }

  move();
  renderer.render(scene, camera);
  requestAnimationFrame(draw);
}

requestAnimationFrame(draw);

CSS

body {
    background-color: #fff;    
    margin: 0;
        overflow: hidden;
}
.label {
    position: absolute;
    top: 0;
    left: 0;
    padding: 5px 15px;
    color: #fff;
    font-size: 13px;
    background-color: rgba(0, 0, 0, .15);
}
.instructions {
    position: absolute;
    bottom: 0%;
    left: 0;
    padding: 5px 15px;
    color: #fff;
    font-size: 13px;
    background-color: rgba(0, 0, 0, .15);
}
canvas { display:block; }

HTML

<head>
<canvas id="canvas"></canvas>
</head>
<body>
<div class="label">BOIDS</div>
<div class="instructions">DRAG TO MOVE CAMERA</div>
    </body>

Комментарии

  • Facebook
  • Вконтакте

Похожие статьи