降り注ぐボール / Falling balls
ライブラリ無しで剛体物理演算をしてみました。
参考までにどうぞ。
(一通りコメントに目を通すと色々分かると思います)
このシミュレーションでは三つ以上の物体が同時に
衝突した時のことを考えていません(=APEみたいな感じ)。
そのため積み上げ処理には極端に弱いです。
安定して積み上げたい場合は接触点を保持しておいて、
力積を連立方程式として解いてやると安定します(=Box2Dとか)。
図形を追加したい場合はその図形の質量属性と描画関数を作って、
衝突検出の部分にその図形を追加してやると動きます。
/**
* Copyright saharan ( http://wonderfl.net/user/saharan )
* MIT License ( http://www.opensource.org/licenses/mit-license.php )
* Downloaded from: http://wonderfl.net/c/sQhB
*/
/*
* 降り注ぐボール / Falling balls
*
* ライブラリ無しで剛体物理演算をしてみました。
* 参考までにどうぞ。
* (一通りコメントに目を通すと色々分かると思います)
*
* このシミュレーションでは三つ以上の物体が
* 同時に衝突した時のことを考えていません(=APEみたいな感じ)。
* そのため積み上げ処理には極端に弱いです。
* 安定して積み上げたい場合は接触点を保持しておいて、
* 力積を連立方程式として解いてやると安定します(=Box2Dとか)。
*
* 図形を追加したい場合はその図形の質量属性と描画関数を作って、
* 衝突検出の部分にその図形を追加してやると動きます。
*/
package {
import flash.events.MouseEvent;
import flash.events.Event;
import flash.display.Sprite;
import net.hires.debug.Stats;
[SWF(frameRate="60")]
public class Physics extends Sprite {
private var bodies:Vector.<Body>;
private var gravity:Vector2D;
private var dt:Number;
private const WALL_BODY:Circle = new Circle(0, 0, 1, 0, 0, 0.5, 0.5);
public function Physics() {
initialize();
}
private function initialize():void {
bodies = new Vector.<Body>();
// Circle(x座標, y座標, 半径, 角度, 密度, 反発係数, 摩擦係数)
for(var i:int = 0; i < 3; i++) {
bodies.push(new Circle(2.5 + i * 3.5, 18, 0.5, 0, 0, 0.5, 0.5));
bodies.push(new Circle(20.35 - i * 3.5, 18, 0.5, 0, 0, 0.5, 0.5));
}
bodies.push(new Circle(11.625, 12, 2, 0, 0, 0.5, 0.5));
gravity = new Vector2D(0, 9.80665);
dt = 1 / 60; // フレームレートにあわせて変更
addEventListener(Event.ENTER_FRAME, frame);
var s:Stats = new Stats();
s.alpha = 0.8;
addChild(s);
}
private function frame(e:Event):void {
graphics.clear();
graphics.beginFill(0x282828);
graphics.drawRect(0, 0, 465, 465);
var b:Body;
for each(b in bodies) {
b.draw(graphics, 20); // 20px = 1m
}
if(Math.random() > 0.5) {
b = new Circle(11.625 + Math.random() * 20 - 10, -3,
Math.random() * 0.5 + 0.25, 0, 1, Math.random(), Math.random());
b.linearVelocity.setVector(Math.random() * 10 - 5, Math.random() * 2);
b.angularVelocity = Math.random() * 4 - 2;
bodies.push(b);
}
for each(b in bodies) {
b.preMove(gravity, dt);
}
broadPhase();
for each(b in bodies) {
b.postMove(dt);
}
}
private function broadPhase():void { // 広域衝突判定(総当り = O(n^2))
for(var i:int = 0; i < bodies.length; i++) {
const b:Body = bodies[i];
for(var j:int = 0; j < i; j++) {
narrowPhase(b, bodies[j]);
}
if(b is Circle) {
const c:Circle = b as Circle;
// 壁境界
if(c.position.x - c.radius < 0) {
solve(c, WALL_BODY, new Vector2D(0, c.position.y),
new Vector2D(1, 0));
}
if(c.position.x + c.radius > 23.25) {
solve(c, WALL_BODY, new Vector2D(23.25, c.position.y),
new Vector2D(-1, 0));
}
if(c.position.y - c.radius > 23.25) {
bodies.splice(i, 1);
i--;
}
}
}
}
private function narrowPhase(b1:Body, b2:Body):void { // 詳細衝突判定
// 図形を追加したならここを弄る
if(b1 is Circle && b2 is Circle) { // 両方丸だった
const c1:Circle = b1 as Circle;
const c2:Circle = b2 as Circle;
if(c1.position.x + c1.radius < c2.position.x - c2.radius)
return;
if(c1.position.y + c1.radius < c2.position.y - c2.radius)
return; // AABBもどき
var d:Vector2D = c1.position.sub(c2.position);
if(d.length() < c1.radius + c2.radius) { // 接触あり
d.normalize();
solve(c1, c2, c1.position.add(d.mul(-c1.radius)), d);
}
}
}
private function solve(b1:Body, b2:Body, p:Vector2D,
n:Vector2D):void { // 重要! いわゆる物理の公式で2物体間の衝突を計算
// p = 衝突座標, n = 法線ベクトル
// 相乗平均で反発係数を算出
const restitution:Number = Math.sqrt(b1.restitution * b2.restitution);
// 相乗平均で摩擦係数を算出
const friction:Number = Math.sqrt(b1.friction * b2.friction);
// 接線ベクトル
const t:Vector2D = new Vector2D(n.y, -n.x);
// 相対速度
const relVel:Vector2D = calcRelativeVelocity(b1, b2, p);
// 法線方向の相対速度(相対速度N)
const dotN:Number = n.dot(relVel);
// 接線方向の相対速度(相対速度T)
const dotT:Number = t.dot(relVel);
// 法線方向の適正質量(適正質量N)
const massN:Number = calcEffectiveMass(b1, b2, p, n);
// 接線方向の適正質量(適正質量T)
const massT:Number = calcEffectiveMass(b1, b2, p, t);
// 衝突後の理想相対速度N
var idealN:Number = 0;
if(dotN < -0.5) idealN = -dotN * restitution;
// ((理想相対速度N - 現在の相対速度N) * 適正質量N)を力積Nとする
var impulseN:Number = (idealN - dotN) * massN;
if(impulseN < 0) impulseN = 0;
b1.applyImpulse(n.mul(impulseN), // 法線方向の力積適応
p.sub(b1.position));
b2.applyImpulse(n.mul(-impulseN), // 作用・反作用の法則
p.sub(b2.position));
// (-現在の相対速度T * 適正質量T)で完全にずれを打ち消す力積T
// しかし摩擦力には上限があるので(力積N * 摩擦係数)でクランプ
var impulseT:Number = clamp(massT * -dotT,
-impulseN * friction, impulseN * friction);
b1.applyImpulse(t.mul(impulseT), // 接線方向の力積適応
p.sub(b1.position));
b2.applyImpulse(t.mul(-impulseT), // 作用・反作用の法則
p.sub(b2.position));
// Debug
// graphics.lineStyle(1, 0);
// graphics.moveTo(p.x * 20, p.y * 20);
// graphics.lineTo(p.x * 20 + n.x * 5, p.y * 20 + n.y * 5);
// graphics.lineStyle();
// graphics.beginFill(0xff0000);
// graphics.drawCircle(p.x * 20, p.y * 20, 2);
// graphics.endFill();
}
private function clamp(v:Number, min:Number, max:Number):Number {
return v < min ? min : v > max ? max: v;
}
private function calcRelativeVelocity(b1:Body, b2:Body,
p:Vector2D):Vector2D { // 相対速度を計算
var rel:Vector2D = new Vector2D();
rel.addEqual(b1.linearVelocity);
rel.addEqual(p.sub(b1.position).crossReverse(b1.angularVelocity));
rel.subEqual(b2.linearVelocity);
rel.subEqual(p.sub(b2.position).crossReverse(b2.angularVelocity));
return rel;
}
private function calcEffectiveMass(b1:Body, b2:Body,
p:Vector2D, n:Vector2D):Number { // 適正な質量を計算
// 物体に与えた力は速度に変換される際質量(= M)で割られるので、
// 与えた力より実際は小さく(M > 1)あるいは大きく(M < 1)反映されてしまう。
// しかし適当な値(この場合はM)を力に掛けてやることで元の力をそのまま速度に反映できる。
// (実際は質量だけでなく慣性モーメントも影響してきます)
const r1:Vector2D = p.sub(b1.position); // それぞれに対する相対位置
const r2:Vector2D = p.sub(b2.position);
// 相対位置と法線の内積(= 法線方向に力を与えたときの回りにくさ)を計算
const dot1:Number = n.dot(r1);
const dot2:Number = n.dot(r2);
// 1 / ...になっているのは逆数を元に戻すため。
// 逆数を使えば無限大の質量も表現できる(1 / ∞ = 0)
return 1 / (
// それぞれの質量の逆数を加算
b1.invMass +
b2.invMass +
// 慣性モーメント・相対位置・法線から
// 物体の角速度に与える影響を計算
b1.invInertia * (r1.x * r1.x + r1.y * r1.y - dot1 * dot1) +
b2.invInertia * (r2.x * r2.x + r2.y * r2.y - dot2 * dot2)
);
}
}
}
import flash.display.Graphics;
class Body { // 剛体
public var mass:Number; // 質量(kg)
public var invMass:Number; // 質量の逆数
public var inertia:Number; // 慣性モーメント(kg・m^2)
public var invInertia:Number; // 慣性モーメントの逆数
public var position:Vector2D; // 位置
public var linearVelocity:Vector2D; // 並進速度
public var rotation:Number; // 角度
public var angularVelocity:Number; // 角速度
public var restitution:Number; // 反発係数
public var friction:Number; // 摩擦係数
public var color:uint;
public function applyImpulse(force:Vector2D,
relativePosition:Vector2D):void { // 剛体に力を与える
// 並進加速度は位置に関係なく加算される
linearVelocity.x += force.x * invMass;
linearVelocity.y += force.y * invMass;
// 角加速度は相対位置と力のベクトルとの外積で求まる
angularVelocity += relativePosition.cross3D(force) * invInertia;
}
public function draw(g:Graphics, scale:Number):void {} // For override.
public function preMove(gravity:Vector2D, dt:Number):void {
if(mass > 0) {
linearVelocity.addEqual(gravity.mul(dt));
} else { // 固定物体
linearVelocity.setVector(0, 0);
angularVelocity = 0;
}
}
public function postMove(dt:Number):void {
if(mass > 0) {
position.addEqual(linearVelocity.mul(dt));
rotation += angularVelocity * dt;
}
}
}
class Circle extends Body { // 円(2Dの球体)
public var radius:Number; // 半径
public function Circle(x:Number, y:Number, radius:Number, rotation:Number,
density:Number, restitution:Number, friction:Number) {
position = new Vector2D(x, y);
this.radius = radius;
this.rotation = rotation;
this.restitution = restitution;
this.friction = friction;
linearVelocity = new Vector2D();
angularVelocity = 0;
if(density > 0) {
mass = radius * radius * Math.PI * density; // 密度 * 面積 = 質量
invMass = 1 / mass;
inertia = 2 / 5 * radius * radius * mass; // 慣性モーメント(円ではなく球体とする)
invInertia = 1 / inertia;
} else { // 密度が0以下なら固定物体に
mass = invMass = 0;
inertia = invInertia = 0;
}
color = (Math.random() * 128 + 96) << 16 | (Math.random() * 128 + 96) << 8 | (Math.random() * 128 + 96);
}
public override function draw(g:Graphics, scale:Number):void {
// Standard
// g.lineStyle(1, 0x000000);
// g.beginFill(color);
// g.drawCircle(position.x * scale, position.y * scale, radius * scale);
// g.endFill();
// g.moveTo(position.x * scale, position.y * scale);
// g.lineTo((position.x + Math.cos(rotation) * radius) * scale,
// (position.y + Math.sin(rotation) * radius) * scale);
// QB (Colored from http://wonderfl.net/c/qoTQ)
// 僕と契約して、物理演算少女に(ry
// Hi, let's contract to me and become physics g...(abbreviated)
g.beginFill(0xff0060);
g.drawCircle(position.x * scale, position.y * scale,radius * scale);
g.endFill();
g.beginFill(0xc00000);
g.drawCircle(position.x * scale, position.y * scale, radius * scale * 0.625);
g.endFill();
g.beginFill(0xffffff);
g.drawCircle((position.x + Math.cos(rotation - 0.5) * radius * 0.5) * scale,
(position.y + Math.sin(rotation - 0.5) * radius * 0.5) * scale, radius * scale * 0.25);
g.endFill();
}
}
class Vector2D { // 2Dベクトル
public var x:Number; // x要素
public var y:Number; // y要素
public function Vector2D(x:Number = 0, y:Number = 0) {
this.x = x;
this.y = y;
}
public function setVector(x:Number, y:Number):void {
this.x = x;
this.y = y;
}
public function addEqual(v:Vector2D):void { // 加算
x += v.x;
y += v.y;
}
public function subEqual(v:Vector2D):void { // 減算
x -= v.x;
y -= v.y;
}
public function mulEqual(s:Number):void { // 乗算
x *= s;
y *= s;
}
public function add(v:Vector2D):Vector2D { // 加算
return new Vector2D(x + v.x, y + v.y);
}
public function sub(v:Vector2D):Vector2D { // 減算
return new Vector2D(x - v.x, y - v.y);
}
public function mul(s:Number):Vector2D { // 乗算
return new Vector2D(x * s, y * s);
}
public function dot(v:Vector2D):Number { // 内積
return x * v.x + y * v.y;
}
public function cross(z:Number):Vector2D { // 外積
return new Vector2D(y * z, -x * z);
}
public function crossReverse(z:Number):Vector2D { // 外積
return new Vector2D(-y * z, x * z);
}
public function cross3D(v:Vector2D):Number { // 外積(z要素を取り出す)
return x * v.y - y * v.x;
}
public function length():Number { // ベクトルの大きさ
return Math.sqrt(x * x + y * y);
}
public function normalize():void { // 正規化する
if(x == y && y == 0)
return; // 正規化できない
const invLength:Number = 1 / length();
x *= invLength;
y *= invLength;
}
}