Created
April 15, 2011 10:38
-
-
Save tyage/921505 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
window.Boku2D = (function() { | |
/* | |
Recursive Dimensional Clustering | |
http://lab.polygonal.de/articles/recursive-dimensional-clustering/ | |
*/ | |
var RDC = function (collide) { | |
this.collide = collide; | |
}; | |
// RDC.SUBDIVISION_THRESHOLD = 4; | |
// RDC.CONTACT_THRESHOLD = 0.1; | |
RDC.prototype.bruteForce = function (group) { | |
var collde = this.collide; | |
for (var i=0,l=group.length; i<l; i++) { | |
for (var j=i+1; j<l; j++) { | |
collde(group[i], group[j]); | |
} | |
} | |
}; | |
RDC.prototype.recursiveClustering = function (group, axis1, axis2) { | |
// if (axis1 === -1 || group.length < RDC.SUBDIVISION_THRESHOLD) { | |
if (axis1 === -1 || group.length < 4) { | |
this.bruteForce(group); | |
} else { | |
var boundaries = this.getOpenCloseBounds(group, axis1); | |
boundaries.sort(function (a, b) { | |
return a.pos - b.pos; | |
}); | |
var newAxis1 = axis2, | |
newAxis2 = -1, | |
groupSubdivided = false, | |
subgroup = [], | |
count = 0, | |
l = boundaries.length; | |
for (var i=0; i<l; i++) | |
{ | |
var b = boundaries[i]; | |
if (b.type === 1) { | |
count++; | |
subgroup.push(b.obj); | |
} else { | |
count--; | |
if (count === 0) { | |
if (i !== (l - 1)) { | |
groupSubdivided = true; | |
} | |
if (groupSubdivided) { | |
if (axis1 === 0) { | |
newAxis1 = 1; | |
} else if (axis1 === 1) { | |
newAxis1 = 0; | |
} | |
} | |
this.recursiveClustering(subgroup, newAxis1, newAxis2); | |
subgroup = []; | |
} | |
} | |
} | |
} | |
}; | |
RDC.prototype.getOpenCloseBounds = function (group, axis) { | |
var l = group.length; | |
var boundaries = []; | |
switch(axis) | |
{ | |
case 0: | |
for (var i=0; i<l; i++) { | |
var o = group[i]; | |
var center = o.center.x; | |
var size = o.size.x; | |
boundaries.push({ | |
type: 1, | |
// pos: o.center.x - o.size.x + RDC.CONTACT_THRESHOLD, | |
pos: center - size + 0.1, | |
obj: o | |
}); | |
boundaries.push({ | |
type: 0, | |
// pos: o.center.x + o.size.x - RDC.CONTACT_THRESHOLD, | |
pos: center + size - 0.1, | |
obj: o | |
}); | |
} | |
break; | |
case 1: | |
for (var i=0; i<l; i++) { | |
var o = group[i]; | |
var center = o.center.y; | |
var size = o.size.y; | |
boundaries.push({ | |
type: 1, | |
// pos: o.center.y - o.size.y + RDC.CONTACT_THRESHOLD, | |
pos: center - size + 0.1, | |
obj: o | |
}); | |
boundaries.push({ | |
type: 0, | |
// pos: o.center.y + o.size.y - RDC.CONTACT_THRESHOLD, | |
pos: center + size - 0.1, | |
obj: o | |
}); | |
} | |
} | |
return boundaries; | |
}; | |
var IsNumber = function(num) { | |
return typeof num === "number"; | |
}; | |
var Extend = function(obj, option) { | |
if (!option) { | |
return; | |
} | |
var key; | |
for (key in option) { | |
if (option.hasOwnProperty(key)) { | |
obj[key] = option[key]; | |
} | |
} | |
}; | |
var Vec = function(x, y) { | |
this.x = x || 0; | |
this.y = y || 0; | |
}; | |
Vec.prototype = { | |
copy: function() { | |
return new Vec(this.x, this.y); | |
}, | |
add: function(v, y) { | |
if (y !== undefined) { | |
v = new Vec(v, y); | |
} | |
var x = this.x + v.x, | |
y = this.y + v.y; | |
return new Vec(x, y); | |
}, | |
subtract: function(v, y) { | |
if (y !== undefined) { | |
v = new Vec(v, y); | |
} | |
var x = this.x - v.x, | |
y = this.y - v.y; | |
return new Vec(x, y); | |
}, | |
multiply: function(i) { | |
var x = this.x * i, | |
y = this.y * i; | |
return new Vec(x, y); | |
}, | |
divide: function(i) { | |
var x = this.x / i, | |
y = this.y / i; | |
return new Vec(x, y); | |
}, | |
dot: function(v) { | |
return this.x * v.x + this.y * v.y; | |
}, | |
length: function() { | |
return Math.sqrt(this.x*this.x + this.y*this.y); | |
}, | |
normalize: function() { | |
var length = this.length(); | |
return this.divide(length); | |
} | |
}; | |
var Manifold = function(object, opponent) { | |
this.object = object; | |
this.opponent = opponent; | |
this.normal = new Vec(); | |
}; | |
Manifold.prototype = { | |
solve: function() { | |
var move = this.object, | |
fixed = this.opponent; | |
if (move.fixed) { | |
return false; | |
} | |
var friction = Math.sqrt(move.friction * fixed.friction); | |
var spring = Math.sqrt(move.elastic * fixed.elastic); | |
var damp = Math.sqrt(move.viscosity * fixed.viscosity); | |
// var relSpeed = move.speed.subtract(fixed.speed); | |
var relSpeed = { | |
x: move.speed.x - fixed.speed.x, | |
y: move.speed.y - fixed.speed.y | |
}; | |
// 衝突ソルバー | |
// this.normal = new Vec(); | |
this.normal.x = 0; | |
this.normal.y = 0; | |
if (this.direction.x < 0) { | |
var diff = move._maxPos.x - fixed._minPos.x; | |
var adj = spring * diff + damp * relSpeed.x; | |
// this.normal = this.normal.add(-adj, 0); | |
this.normal.x += -adj; | |
} else if (this.direction.x > 0) { | |
var diff = fixed._maxPos.x - move._minPos.x; | |
var adj = spring * diff - damp * relSpeed.x; | |
// this.normal = this.normal.add(adj, 0); | |
this.normal.x += adj; | |
} | |
if (this.direction.y < 0) { | |
var diff = move._maxPos.y - fixed._minPos.y; | |
var adj = spring * diff + damp * relSpeed.y; | |
// this.normal = this.normal.add(0, -adj); | |
this.normal.y += -adj; | |
} else if (this.direction.y > 0) { | |
var diff = fixed._maxPos.y - move._minPos.y; | |
var adj = spring * diff - damp * relSpeed.y; | |
// this.normal = this.normal.add(0, adj); | |
this.normal.y += adj; | |
} | |
var l = move.contacts.length; | |
move.applyForce({ | |
x: this.normal.x / l, | |
y: this.normal.y / l | |
}); | |
// 摩擦 | |
var n = this.normal.length(), | |
force = new Vec(n, n).multiply(friction); | |
if (force.x > Math.abs(move.force.x)) { | |
force.x = Math.abs(move.force.x); | |
} | |
if (force.y > Math.abs(move.force.y)) { | |
force.y = Math.abs(move.force.y); | |
} | |
if (this.direction.y !== 0) { | |
if (relSpeed.x > 0) { | |
force.x = -force.x; | |
} | |
} else { | |
force.x = 0; | |
} | |
if (this.direction.x !== 0) { | |
if (relSpeed.y > 0) { | |
force.y = -force.y; | |
} | |
} else { | |
force.y = 0; | |
} | |
move.applyForce(force); | |
// !fixed.fixed && fixed.applyForce(force.multiply(-1)); | |
!fixed.fixed && fixed.applyForce({ | |
x: force.x * -1, | |
y: force.y * -1 | |
}); | |
} | |
}; | |
var Contact = function(object1, object2) { | |
var direction1 = new Vec(), | |
direction2 = new Vec(); | |
// 前回の位置から衝突方向を調べる | |
if (object1._maxTmpPos.x < object2._minTmpPos.x && | |
object1._maxPos.x > object2._minPos.x) { | |
// direction1 = direction1.add(-1, 0); | |
direction1.x += -1; | |
} | |
if (object1._minTmpPos.x > object2._maxTmpPos.x && | |
object1._minPos.x < object2._maxPos.x) { | |
// direction1 = direction1.add(1, 0); | |
direction1.x += 1; | |
} | |
if (object1._maxTmpPos.y < object2._minTmpPos.y && | |
object1._maxPos.y > object2._minPos.y) { | |
// direction1 = direction1.add(0, -1); | |
direction1.y += -1; | |
} | |
if (object1._minTmpPos.y > object2._maxTmpPos.y && | |
object1._minPos.y < object2._maxPos.y) { | |
// direction1 = direction1.add(0, 1); | |
direction1.y += 1; | |
} | |
if (direction1.x === 0 && direction1.y === 0) { | |
// 最小分離距離を計る | |
var normals = []; | |
if ((object2._minPos.x <= object1._minPos.x && | |
object1._minPos.x <= object2._maxPos.x) || | |
(object1._minPos.x <= object2._maxPos.x && | |
object2._maxPos.x <= object1._maxPos.x)) { | |
// 左衝突 | |
var d = object2._maxPos.x - object1._minPos.x; | |
// normal = new Vec(d, 0); | |
// normal.direction = new Vec(1, 0); | |
var normal = { | |
length: d, | |
direction: { | |
x: 1, | |
y: 0 | |
} | |
}; | |
normals.push(normal); | |
} | |
if ((object2._minPos.x <= object1._maxPos.x && | |
object1._maxPos.x <= object2._maxPos.x) || | |
(object1._minPos.x <= object2._minPos.x && | |
object2._minPos.x <= object1._maxPos.x)) { | |
// 右衝突 | |
var d = object1._maxPos.x - object2._minPos.x; | |
// normal = new Vec(d, 0); | |
// normal.direction = new Vec(-1, 0); | |
var normal = { | |
length: d, | |
direction: { | |
x: -1, | |
y: 0 | |
} | |
}; | |
normals.push(normal); | |
} | |
if ((object2._minPos.y <= object1._minPos.y && | |
object1._minPos.y <= object2._maxPos.y) || | |
(object1._minPos.y <= object2._maxPos.y && | |
object2._maxPos.y <= object1._maxPos.y)) { | |
// 上衝突 | |
var d = object2._maxPos.y - object1._minPos.y; | |
// normal = new Vec(0, d); | |
// normal.direction = new Vec(0, 1); | |
var normal = { | |
length: d, | |
direction: { | |
x: 0, | |
y: 1 | |
} | |
}; | |
normals.push(normal); | |
} | |
if ((object2._minPos.y <= object1._maxPos.y && | |
object1._maxPos.y <= object2._maxPos.y) || | |
(object1._minPos.y <= object2._minPos.y && | |
object2._minPos.y <= object1._maxPos.y)) { | |
// 下衝突 | |
var d = object1._maxPos.y - object2._minPos.y; | |
// normal = new Vec(0, d); | |
// normal.direction = new Vec(0, -1); | |
var normal = { | |
length: d, | |
direction: { | |
x: 0, | |
y: -1 | |
} | |
}; | |
normals.push(normal); | |
} | |
var normal = normals[0]; | |
for (var i=0,l=normals.length;i<l;i++) { | |
// normal = normal.length() > normals[i].length() ? | |
normal = normal.length > normals[i].length ? | |
normals[i] : | |
normal; | |
} | |
if (normal) { | |
// direction1 = normal.direction; | |
direction1.x = normal.direction.x; | |
direction1.y = normal.direction.y; | |
} | |
} | |
// direction2 = direction1.multiply(-1); | |
direction2.x = direction1.x * -1; | |
direction2.y = direction1.y * -1; | |
var manifold1 = new Manifold(object1, object2), | |
manifold2 = new Manifold(object2, object1); | |
manifold1.direction = direction1; | |
manifold2.direction = direction2; | |
this.manifolds = [manifold1, manifold2]; | |
this.stepCount = 0; | |
this.world = object1.world; | |
this.object1 = object1; | |
this.object2 = object2; | |
this.update(); | |
var manifolds = this.manifolds; | |
for (var i=0,l=manifolds.length;i<l;i++) { | |
var manifold = manifolds[i]; | |
manifold.object.createContact(this, manifold); | |
} | |
}; | |
Contact.prototype = { | |
update: function() { | |
this.stepCount = this.world.stepCount; | |
var manifolds = this.manifolds; | |
for (var i=0,l=manifolds.length;i<l;i++) { | |
var manifold = manifolds[i]; | |
manifold.object.updateContact(this, manifold); | |
} | |
}, | |
solve: function() { | |
var manifolds = this.manifolds; | |
for (var i=0,l=manifolds.length;i<l;i++) { | |
var manifold = manifolds[i]; | |
manifold.object.beforeSolve(this, manifold); | |
manifold.solve(); | |
manifold.object.afterSolve(this, manifold); | |
} | |
}, | |
destroy: function() { | |
this._removeFromList(this.world.contacts); | |
this._removeFromList(this.object1.contacts); | |
this._removeFromList(this.object2.contacts); | |
var manifolds = this.manifolds; | |
for (var i=0,l=manifolds.length;i<l;i++) { | |
var manifold = manifolds[i]; | |
manifold.object.destroyContact(this, manifold); | |
} | |
}, | |
_removeFromList: function(contacts) { | |
for (var i=0,l=contacts.length;i<l;i++) { | |
if (contacts[i] == this) { | |
delete contacts[i]; | |
contacts.splice(i, 1); | |
} | |
} | |
} | |
}; | |
var Collide = function(object1, object2) { | |
if (!CheckContact(object1, object2)) { | |
return; | |
} | |
var list = object1.contacts, | |
contact = null; | |
for (var i=0,l=list.length;i<l;i++) { | |
var c = list[i]; | |
if (c.manifolds[0].opponent === object2 || | |
c.manifolds[1].opponent === object2) { | |
contact = c; | |
break; | |
} | |
} | |
if (contact) { | |
contact.update(); | |
} else { | |
contact = new Contact(object1, object2); | |
object1.contacts.push(contact); | |
object2.contacts.push(contact); | |
object1.world.contacts.push(contact); | |
} | |
}; | |
var CheckContact = function(object1, object2) { | |
return object1._maxPos.x >= object2._minPos.x && | |
object1._minPos.x <= object2._maxPos.x && | |
object1._maxPos.y >= object2._minPos.y && | |
object1._minPos.y <= object2._maxPos.y; | |
}; | |
var Model = function() { | |
var newObject = function(option) { | |
this._init(option); | |
}; | |
/* | |
object <- object.prototype | |
<- object.prototype.prototype( = Model.prototype) | |
*/ | |
var newModel = function() {}; | |
newModel.prototype = Model.defaults; | |
newObject.prototype = new newModel(); | |
return newObject; | |
}; | |
Model.defaults = { | |
speed: new Vec(), | |
force: new Vec(), | |
accel: new Vec(), | |
center: new Vec(), | |
centerTmp: new Vec(), | |
size: new Vec(0, 0), | |
gravity: new Vec(0, 10), | |
weight: 1, // 質量 | |
elastic: 7, // 反発係数 → バネ定数 (侵入量が減る) | |
viscosity: 3, // 粘性係数 → ダンパ係数 (速度が落ちる) | |
friction: 0.1, // 摩擦係数 | |
fixed: false, | |
contacts: [], | |
init: function(option) {}, | |
beforeStep: function(time) {}, | |
afterStep: function(time) {}, | |
createContact: function(contact, manifold) {}, | |
destroyContact: function(contact, manifold) {}, | |
updateContact: function(contact, manifold) {}, | |
beforeSolve: function(contact, manifold) {}, | |
afterSolve: function(contact, manifold) {}, | |
_init: function(option) { | |
this.speed = this.speed.copy(); | |
this.force = this.force.copy(); | |
this.accel = this.accel.copy(); | |
this.center = this.center.copy(); | |
this.centerTmp = this.center.copy(); | |
this.size = this.size.copy(); | |
this.gravity = this.gravity.copy(); | |
this.contacts = []; | |
Extend(this, option); | |
this.init(option); | |
}, | |
_makeCache: function() { | |
this._maxTmpPos = this.maxTmpPos(); | |
this._minTmpPos = this.minTmpPos(); | |
this._maxPos = this.maxPos(); | |
this._minPos = this.minPos(); | |
}, | |
_step: function(time) { | |
this.beforeStep(time); | |
// this.applyForce(this.gravity.multiply(this.weight)); | |
this.applyForce({ | |
x: this.gravity.x * this.weight, | |
y: this.gravity.y * this.weight | |
}); | |
// this.speed = this.speed.add(this.accel.multiply(time)); | |
this.speed.x += this.accel.x * time; | |
this.speed.y += this.accel.y * time; | |
//this.centerTmp = this.center.copy(); | |
this.centerTmp.x = this.center.x; | |
this.centerTmp.y = this.center.y; | |
//this.center = this.center.add(this.speed.multiply(time)); | |
this.center.x += this.speed.x * time; | |
this.center.y += this.speed.y * time; | |
this._makeCache(); | |
this.afterStep(time); | |
this.resetForce(); | |
}, | |
maxTmpPos: function() { | |
// return this.centerTmp.add(this.size.divide(2)); | |
return { | |
x: this.centerTmp.x + this.size.x/2, | |
y: this.centerTmp.y + this.size.y/2 | |
}; | |
}, | |
minTmpPos: function() { | |
// return this.centerTmp.subtract(this.size.divide(2)); | |
return { | |
x: this.centerTmp.x - this.size.x/2, | |
y: this.centerTmp.y - this.size.y/2 | |
}; | |
}, | |
maxPos: function() { | |
// return this.center.add(this.size.divide(2)); | |
return { | |
x: this.center.x + this.size.x/2, | |
y: this.center.y + this.size.y/2 | |
}; | |
}, | |
minPos: function() { | |
// return this.center.subtract(this.size.divide(2)); | |
return { | |
x: this.center.x - this.size.x/2, | |
y: this.center.y - this.size.y/2 | |
}; | |
}, | |
applyForce: function(force) { | |
// this.force = this.force.add(force); | |
this.force.x += force.x; | |
this.force.y += force.y; | |
// this.accel = this.accel.add(force.divide(this.weight)); | |
this.accel.x += force.x/this.weight; | |
this.accel.y += force.y/this.weight; | |
}, | |
resetForce: function() { | |
// this.accel = new Vec(); | |
this.accel.x = 0; | |
this.accel.y = 0; | |
} | |
}; | |
var Rdc = new RDC(Collide); | |
var World = function(option) { | |
this.objects = []; | |
this.size = new Vec(0, 0); | |
this.stepCount = 0; | |
this.contacts = []; | |
Extend(this, option); | |
this.init(option); | |
}; | |
World.prototype = { | |
step: function(time) { | |
this._createObject(); | |
this.beforeStep(time); | |
this.stepCount++; | |
// 衝突情報取得 | |
if (this.objects < 100) { | |
Rdc.bruteForce(this.objects); | |
} else { | |
Rdc.recursiveClustering(this.objects, 0, 1); | |
} | |
// 衝突処理 | |
var contacts = this.contacts; | |
for (var i=0;i<contacts.length;i++) { | |
var contact = contacts[i]; | |
if (contact.stepCount !== this.stepCount) { | |
contact.destroy(); | |
i--; | |
} else { | |
contact.solve(); | |
} | |
} | |
// 各アイテムの進行 | |
for (var i=0,l=this.objects.length;i<l;i++) { | |
var object = this.objects[i]; | |
if (object) { | |
object._step(time); | |
} | |
} | |
this.afterStep(time); | |
return this; | |
}, | |
_createObject: function() { | |
var objects = this._stackObjects; | |
for (var i=0,l=objects.length;i<l;i++) { | |
var object = objects[i]; | |
object.world = this; | |
this.objects.push(object); | |
object.centerTmp.x = object.center.x; | |
object.centerTmp.y = object.center.y; | |
object._makeCache(); | |
} | |
this._stackObjects = []; | |
}, | |
createObject: function(object) { | |
this._stackObjects.push(object); | |
}, | |
destroyObject: function(object) { | |
if (object) { | |
for (var i=0,l=object.contacts.length;i<l;i++) { | |
var contact = object.contacts[i]; | |
if (contact) { | |
contact.destroy(); | |
} | |
} | |
for (i=0,l=this.objects.length;i<l;i++) { | |
if (this.objects[i] === object) { | |
delete this.objects[i]; | |
this.objects.splice(i, 1); | |
} | |
} | |
} | |
}, | |
destroy: function() { | |
for (var i=0,l=this.objects.length;i<l;i++) { | |
this.destroyObject(this.objects[i]); | |
} | |
}, | |
init: function(option) {}, | |
beforeStep: function(time) {}, | |
afterStep: function(time) {}, | |
_stackObjects: [] | |
}; | |
return { | |
Vec: Vec, | |
World: World, | |
Model: Model | |
}; | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment