package alternativa.engine3d.physics { import alternativa.engine3d.*; import alternativa.engine3d.core.BSPNode; import alternativa.engine3d.core.PolyPrimitive; import alternativa.engine3d.core.Scene3D; import alternativa.types.Point3D; import alternativa.types.Set; import alternativa.utils.ObjectUtils; use namespace alternativa3d; /** * Класс реализует алгоритм непрерывного определения столкновений эллипсоида с плоскими выпуклыми многоугольниками. */ public class EllipsoidCollider { // Максимальное количество попыток найти свободное от столкновения со сценой направление private static const MAX_COLLISIONS:uint = 50; // Радиус наибольшей сферы private var _radius:Number = 100; private var _radius2:Number = _radius * _radius; private var _radiusX:Number = _radius; private var _radiusY:Number = _radius; private var _radiusZ:Number = _radius; private var _radiusX2:Number = _radiusX * _radiusX; private var _radiusY2:Number = _radiusY * _radiusY; private var _radiusZ2:Number = _radiusZ * _radiusZ; // Коэффициенты масштабирования осей private var _scaleX:Number = 1; private var _scaleY:Number = 1; private var _scaleZ:Number = 1; // Квадраты коэффициентов масштабирования осей private var _scaleX2:Number = 1; private var _scaleY2:Number = 1; private var _scaleZ2:Number = 1; private var collisionSource:Point3D; private var currentDisplacement:Point3D = new Point3D(); private var collisionDestination:Point3D = new Point3D(); private var collisionPlanes:Array = new Array(); private var collisionPrimitive:PolyPrimitive; private var collisionPrimitiveNearest:PolyPrimitive; private var collisionPlanePoint:Point3D = new Point3D(); private var collisionPrimitiveNearestLengthSqr:Number; private var collisionPrimitivePoint:Point3D = new Point3D(); private var collisionNormal:Point3D = new Point3D(); private var collisionPoint:Point3D = new Point3D(); private var collisionOffset:Number; private var currentCoords:Point3D = new Point3D(); private var collision:Collision = new Collision(); private var collisionRadius:Number; private var radiusVector:Point3D = new Point3D(); private var p1:Point3D = new Point3D(); private var p2:Point3D = new Point3D();; private var localCollisionPlanePoint:Point3D = new Point3D(); // Флаг использования упорщённого алгоритма. Включается когда эллипсоид представляет собой сферу. private var useSimpleAlgorithm:Boolean = true; /** * Сцена, в которой определяются столкновения. */ public var scene:Scene3D; /** * Погрешность определения расстояний и координат. Две точки совпадают, если модуль разности любых соответствующих * координат меньше указанной погрешности. */ public var offsetThreshold:Number = 0.01; /** * Множество объектов, учитываемых в процессе определения столкновений. В качестве объектов могут выступать экземпляры * классов Mesh и Surface. Каким образом учитываются перечисленные в множестве объекты зависит * от значения поля collisionSetMode. Значение null эквивалентно заданию пустого множества. * * @see #collisionSetMode * @see alternativa.engine3d.core.Mesh * @see alternativa.engine3d.core.Surface */ public var collisionSet:Set; /** * Параметр определяет, каким образом учитываются объекты, перечисленные в множестве collisionSet. Если * значение параметра равно true, то грани объектов из множества игнорируются при определении столкновений. * При значении параметра false учитываются только столкновения с гранями, принадлежащим перечисленным * в множестве объектам. * * @default true * @see #collisionSet */ private var _collisionSetMode:int = CollisionSetMode.EXCLUDE; /** * Создаёт новый экземпляр класса. * * @param scene сцена, в которой определяются столкновения * @param scaleX радиус эллипсоида по оси X * @param scaleY радиус эллипсоида по оси Y * @param scaleZ радиус эллипсоида по оси Z */ public function EllipsoidCollider(scene:Scene3D = null, radiusX:Number = 100, radiusY:Number = 100, radiusZ:Number = 100) { this.scene = scene; this.radiusX = radiusX; this.radiusY = radiusY; this.radiusZ = radiusZ; } /** * @private */ public function get collisionSetMode():int { return _collisionSetMode; } /** * Параметр определяет, каким образом учитываются объекты, перечисленные в множестве collisionSet. * * @default CollisionSetMode.EXCLUDE * @see #collisionSet * @see CollisionSetMode */ public function set collisionSetMode(value:int):void { if (value != CollisionSetMode.EXCLUDE && value != CollisionSetMode.INCLUDE) { throw ArgumentError(ObjectUtils.getClassName(this) + ".collisionSetMode invalid value"); } _collisionSetMode = value; } /** * Величина радиуса (полуоси) эллипсоида по оси X. При установке отрицательного значения берётся модуль. * * @default 100 */ public function get radiusX():Number { return _radiusX; } /** * @private */ public function set radiusX(value:Number):void { _radiusX = value >= 0 ? value : -value; _radiusX2 = _radiusX * _radiusX; calculateScales(); } /** * Величина радиуса (полуоси) эллипсоида по оси Y. При установке отрицательного значения берётся модуль. * * @default 100 */ public function get radiusY():Number { return _radiusY; } /** * @private */ public function set radiusY(value:Number):void { _radiusY = value >= 0 ? value : -value; _radiusY2 = _radiusY * _radiusY; calculateScales(); } /** * Величина радиуса (полуоси) эллипсоида по оси Z. При установке отрицательного значения берётся модуль. * * @default 100 */ public function get radiusZ():Number { return _radiusZ; } /** * @private */ public function set radiusZ(value:Number):void { _radiusZ = value >= 0 ? value : -value; _radiusZ2 = _radiusZ * _radiusZ; calculateScales(); } /** * Расчёт коэффициентов масштабирования осей. */ private function calculateScales():void { _radius = _radiusX; if (_radiusY > _radius) { _radius = _radiusY; } if (_radiusZ > _radius) { _radius = _radiusZ; } _radius2 = _radius * _radius; _scaleX = _radiusX / _radius; _scaleY = _radiusY / _radius; _scaleZ = _radiusZ / _radius; _scaleX2 = _scaleX * _scaleX; _scaleY2 = _scaleY * _scaleY; _scaleZ2 = _scaleZ * _scaleZ; useSimpleAlgorithm = (_radiusX == _radiusY) && (_radiusX == _radiusZ); } /** * Расчёт конечного положения эллипсоида по заданному начальному положению и вектору смещения. Если задано значение * поля scene, то при вычислении конечного положения учитываются столкновения с объектами сцены, * принимая во внимание множество collisionSet и режим работы collisionSetMode. Если * значение поля scene равно null, то результат работы метода будет простой суммой двух * входных векторов. * * @param sourcePoint начальное положение центра эллипсоида в системе координат корневого объекта сцены * @param displacementVector вектор перемещения эллипсоида в системе координат корневого объекта сцены. Если модуль * каждого компонента вектора не превышает значения offsetThreshold, эллипсоид остаётся в начальной точке. * @param destinationPoint в эту переменную записывается расчётное положение центра эллипсоида в системе координат * корневого объекта сцены * * @see #scene * @see #collisionSet * @see #collisionSetMode * @see #offsetThreshold */ public function calculateDestination(sourcePoint:Point3D, displacementVector:Point3D, destinationPoint:Point3D):void { // Расчеты не производятся, если перемещение мало if (displacementVector.x < offsetThreshold && displacementVector.x > -offsetThreshold && displacementVector.y < offsetThreshold && displacementVector.y > -offsetThreshold && displacementVector.z < offsetThreshold && displacementVector.z > -offsetThreshold) { destinationPoint.x = sourcePoint.x; destinationPoint.y = sourcePoint.y; destinationPoint.z = sourcePoint.z; return; } // Начальные координаты currentCoords.x = sourcePoint.x; currentCoords.y = sourcePoint.y; currentCoords.z = sourcePoint.z; // Начальный вектор перемещения currentDisplacement.x = displacementVector.x; currentDisplacement.y = displacementVector.y; currentDisplacement.z = displacementVector.z; // Начальная точка назначения destinationPoint.x = sourcePoint.x + currentDisplacement.x; destinationPoint.y = sourcePoint.y + currentDisplacement.y; destinationPoint.z = sourcePoint.z + currentDisplacement.z; if (useSimpleAlgorithm) { calculateDestinationS(sourcePoint, destinationPoint); } else { calculateDestinationE(sourcePoint, destinationPoint); } } /** * Вычисление точки назначения для сферы. * @param sourcePoint * @param destinationPoint */ private function calculateDestinationS(sourcePoint:Point3D, destinationPoint:Point3D):void { var collisionCount:uint = 0; var hasCollision:Boolean; do { hasCollision = getCollision(currentCoords, currentDisplacement, collision); if (hasCollision ) { // Вынос точки назначения из-за плоскости столкновения на высоту радиуса сферы над плоскостью по направлению нормали var offset:Number = _radius + offsetThreshold + collision.offset - destinationPoint.x*collision.normal.x - destinationPoint.y*collision.normal.y - destinationPoint.z*collision.normal.z; destinationPoint.x += collision.normal.x * offset; destinationPoint.y += collision.normal.y * offset; destinationPoint.z += collision.normal.z * offset; // Коррекция текущих кординат центра сферы для следующей итерации currentCoords.x = collision.point.x + collision.normal.x * (_radius + offsetThreshold); currentCoords.y = collision.point.y + collision.normal.y * (_radius + offsetThreshold); currentCoords.z = collision.point.z + collision.normal.z * (_radius + offsetThreshold); // Коррекция вектора скорости. Результирующий вектор направлен вдоль плоскости столкновения. currentDisplacement.x = destinationPoint.x - currentCoords.x; currentDisplacement.y = destinationPoint.y - currentCoords.y; currentDisplacement.z = destinationPoint.z - currentCoords.z; // Если смещение слишком мало, останавливаемся if (currentDisplacement.x < offsetThreshold && currentDisplacement.x > -offsetThreshold && currentDisplacement.y < offsetThreshold && currentDisplacement.y > -offsetThreshold && currentDisplacement.z < offsetThreshold && currentDisplacement.z > -offsetThreshold) { break; } } } while (hasCollision && (++collisionCount < MAX_COLLISIONS)); // Если количество итераций достигло максимально возможного значения, то остаемся на старом месте if (collisionCount == MAX_COLLISIONS) { destinationPoint.x = sourcePoint.x; destinationPoint.y = sourcePoint.y; destinationPoint.z = sourcePoint.z; } } /** * Вычисление точки назначения для эллипсоида. * @param destinationPoint * @return */ private function calculateDestinationE(sourcePoint:Point3D, destinationPoint:Point3D):void { var collisionCount:uint = 0; var hasCollision:Boolean; // Цикл выполняется до тех пор, пока не будет найдено ни одного столкновения на очередной итерации или пока не // будет достигнуто максимально допустимое количество столкновений, что означает зацикливание алгоритма и // необходимость принудительного выхода. do { hasCollision = getCollision(currentCoords, currentDisplacement, collision); if (hasCollision) { // Вынос точки назначения из-за плоскости столкновения на высоту эффективного радиуса эллипсоида над плоскостью по направлению нормали var offset:Number = collisionRadius + offsetThreshold + collision.offset - destinationPoint.x * collision.normal.x - destinationPoint.y * collision.normal.y - destinationPoint.z * collision.normal.z; destinationPoint.x += collision.normal.x * offset; destinationPoint.y += collision.normal.y * offset; destinationPoint.z += collision.normal.z * offset; // Коррекция текущих кординат центра эллипсоида для следующей итерации collisionRadius = (collisionRadius + offsetThreshold) / collisionRadius; currentCoords.x = collision.point.x - collisionRadius * radiusVector.x; currentCoords.y = collision.point.y - collisionRadius * radiusVector.y; currentCoords.z = collision.point.z - collisionRadius * radiusVector.z; // Коррекция вектора смещения. Результирующий вектор направлен параллельно плоскости столкновения. currentDisplacement.x = destinationPoint.x - currentCoords.x; currentDisplacement.y = destinationPoint.y - currentCoords.y; currentDisplacement.z = destinationPoint.z - currentCoords.z; // Если смещение слишком мало, останавливаемся if (currentDisplacement.x < offsetThreshold && currentDisplacement.x > -offsetThreshold && currentDisplacement.y < offsetThreshold && currentDisplacement.y > -offsetThreshold && currentDisplacement.z < offsetThreshold && currentDisplacement.z > -offsetThreshold) { destinationPoint.x = currentCoords.x; destinationPoint.y = currentCoords.y; destinationPoint.z = currentCoords.z; break; } } } while (hasCollision && (++collisionCount < MAX_COLLISIONS)); // Если количество итераций достигло максимально возможного значения, то остаемся на старом месте if (collisionCount == MAX_COLLISIONS) { destinationPoint.x = sourcePoint.x; destinationPoint.y = sourcePoint.y; destinationPoint.z = sourcePoint.z; } } /** * Метод определяет наличие столкновения при смещении эллипсоида из заданной точки на величину указанного вектора * перемещения, принимая во внимание множество collisionSet и режим работы collisionSetMode. * * @param sourcePoint начальное положение центра эллипсоида в системе координат корневого объекта сцены * @param displacementVector вектор перемещения эллипсоида в системе координат корневого объекта сцены * @param collision в эту переменную будут записаны данные о плоскости и точке столкновения в системе координат * корневого объекта сцены * * @return true, если эллипсоид при заданном перемещении столкнётся с каким-либо полигоном сцены, * false если столкновений нет или не задано значение поля scene. * * @see #scene * @see #collisionSet * @see #collisionSetMode */ public function getCollision(sourcePoint:Point3D, displacementVector:Point3D, collision:Collision):Boolean { if (scene == null) { return false; } collisionSource = sourcePoint; currentDisplacement.x = displacementVector.x; currentDisplacement.y = displacementVector.y; currentDisplacement.z = displacementVector.z; collisionDestination.x = collisionSource.x + currentDisplacement.x; collisionDestination.y = collisionSource.y + currentDisplacement.y; collisionDestination.z = collisionSource.z + currentDisplacement.z; collectPotentialCollisionPlanes(scene.bsp); collisionPlanes.sortOn("sourceOffset", Array.NUMERIC | Array.DESCENDING); var plane:CollisionPlane; // Пока не найдём столкновение с примитивом или плоскости не кончатся if (useSimpleAlgorithm) { while ((plane = collisionPlanes.pop()) != null) { if (collisionPrimitive == null) { calculateCollisionWithPlaneS(plane); } CollisionPlane.destroyCollisionPlane(plane); } } else { while ((plane = collisionPlanes.pop()) != null) { if (collisionPrimitive == null) { calculateCollisionWithPlaneE(plane); } CollisionPlane.destroyCollisionPlane(plane); } } var collisionFound:Boolean = collisionPrimitive != null; if (collisionFound) { collision.face = collisionPrimitive.face; collision.normal = collisionNormal; collision.offset = collisionOffset; collision.point = collisionPoint; } collisionPrimitive = null; collisionSource = null; return collisionFound; } /** * Сбор потенциальных плоскостей столкновения. * * @param node текущий узел BSP-дерева */ private function collectPotentialCollisionPlanes(node:BSPNode):void { if (node == null) { return; } var sourceOffset:Number = collisionSource.x * node.normal.x + collisionSource.y * node.normal.y + collisionSource.z * node.normal.z - node.offset; var destinationOffset:Number = collisionDestination.x * node.normal.x + collisionDestination.y * node.normal.y + collisionDestination.z * node.normal.z - node.offset; var plane:CollisionPlane; if (sourceOffset >= 0) { // Исходное положение центра перед плоскостью ноды // Проверяем передние ноды collectPotentialCollisionPlanes(node.front); // Грубая оценка пересечения с плоскостью по радиусу ограничивающей сферы эллипсоида if (destinationOffset < _radius) { // Нашли потенциальное пересечение с плоскостью plane = CollisionPlane.createCollisionPlane(node, true, sourceOffset, destinationOffset); collisionPlanes.push(plane); // Проверяем задние ноды collectPotentialCollisionPlanes(node.back); } } else { // Исходное положение центра за плоскостью ноды // Проверяем задние ноды collectPotentialCollisionPlanes(node.back); // Грубая оценка пересечения с плоскостью по радиусу ограничивающей сферы эллипсоида if (destinationOffset > -_radius) { // Столкновение возможно только в случае если в ноде есть примитивы, направленные назад if (node.backPrimitives != null) { // Нашли потенциальное пересечение с плоскостью plane = CollisionPlane.createCollisionPlane(node, false, -sourceOffset, -destinationOffset); collisionPlanes.push(plane); } // Проверяем передние ноды collectPotentialCollisionPlanes(node.front); } } } /** * @private * Определение пересечения сферы с примитивами, лежащими в заданной плоскости. * * @param plane плоскость, содержащая примитивы для проверки */ private function calculateCollisionWithPlaneS(plane:CollisionPlane):void { collisionPlanePoint.copy(collisionSource); var normal:Point3D = plane.node.normal; // Если сфера врезана в плоскость if (plane.sourceOffset <= _radius) { if (plane.infront) { collisionPlanePoint.x -= normal.x * plane.sourceOffset; collisionPlanePoint.y -= normal.y * plane.sourceOffset; collisionPlanePoint.z -= normal.z * plane.sourceOffset; } else { collisionPlanePoint.x += normal.x * plane.sourceOffset; collisionPlanePoint.y += normal.y * plane.sourceOffset; collisionPlanePoint.z += normal.z * plane.sourceOffset; } } else { // Находим центр сферы во время столкновения с плоскостью var time:Number = (plane.sourceOffset - _radius) / (plane.sourceOffset - plane.destinationOffset); collisionPlanePoint.x = collisionSource.x + currentDisplacement.x * time; collisionPlanePoint.y = collisionSource.y + currentDisplacement.y * time; collisionPlanePoint.z = collisionSource.z + currentDisplacement.z * time; // Устанавливаем точку пересечения cферы с плоскостью if (plane.infront) { collisionPlanePoint.x -= normal.x * _radius; collisionPlanePoint.y -= normal.y * _radius; collisionPlanePoint.z -= normal.z * _radius; } else { collisionPlanePoint.x += normal.x * _radius; collisionPlanePoint.y += normal.y * _radius; collisionPlanePoint.z += normal.z * _radius; } } // Проверяем примитивы плоскости var primitive:*; collisionPrimitiveNearestLengthSqr = Number.MAX_VALUE; collisionPrimitiveNearest = null; if (plane.infront) { if ((primitive = plane.node.primitive) != null) { if (((_collisionSetMode == CollisionSetMode.EXCLUDE) && (collisionSet == null || !(collisionSet[primitive.face._mesh] || collisionSet[primitive.face._surface]))) || ((_collisionSetMode == CollisionSetMode.INCLUDE) && (collisionSet != null) && (collisionSet[primitive.face._mesh] || collisionSet[primitive.face._surface]))) { calculateCollisionWithPrimitiveS(plane.node.primitive); } } else { for (primitive in plane.node.frontPrimitives) { if (((_collisionSetMode == CollisionSetMode.EXCLUDE) && (collisionSet == null || !(collisionSet[primitive.face._mesh] || collisionSet[primitive.face._surface]))) || ((_collisionSetMode == CollisionSetMode.INCLUDE) && (collisionSet != null) && (collisionSet[primitive.face._mesh] || collisionSet[primitive.face._surface]))) { calculateCollisionWithPrimitiveS(primitive); if (collisionPrimitive != null) break; } } } } else { for (primitive in plane.node.backPrimitives) { if (((_collisionSetMode == CollisionSetMode.EXCLUDE) && (collisionSet == null || !(collisionSet[primitive.face._mesh] || collisionSet[primitive.face._surface]))) || ((_collisionSetMode == CollisionSetMode.INCLUDE) && (collisionSet != null) && (collisionSet[primitive.face._mesh] || collisionSet[primitive.face._surface]))) { calculateCollisionWithPrimitiveS(primitive); if (collisionPrimitive != null) break; } } } if (collisionPrimitive != null) { // Если точка пересечения попала в примитив // Нормаль плоскости при столкновении - нормаль плоскости if (plane.infront) { collisionNormal.x = normal.x; collisionNormal.y = normal.y; collisionNormal.z = normal.z; collisionOffset = plane.node.offset; } else { collisionNormal.x = -normal.x; collisionNormal.y = -normal.y; collisionNormal.z = -normal.z; collisionOffset = -plane.node.offset; } // Точка столкновения в точке столкновения с плоскостью collisionPoint.x = collisionPlanePoint.x; collisionPoint.y = collisionPlanePoint.y; collisionPoint.z = collisionPlanePoint.z; } else { // Если точка пересечения не попала ни в один примитив, проверяем столкновение с ближайшей // Вектор из ближайшей точки в центр сферы var nearestPointToSourceX:Number = collisionSource.x - collisionPrimitivePoint.x; var nearestPointToSourceY:Number = collisionSource.y - collisionPrimitivePoint.y; var nearestPointToSourceZ:Number = collisionSource.z - collisionPrimitivePoint.z; // Если движение в сторону точки if (nearestPointToSourceX * currentDisplacement.x + nearestPointToSourceY * currentDisplacement.y + nearestPointToSourceZ * currentDisplacement.z <= 0) { // Ищем нормализованный вектор обратного направления var vectorLength:Number = Math.sqrt(currentDisplacement.x * currentDisplacement.x + currentDisplacement.y * currentDisplacement.y + currentDisplacement.z * currentDisplacement.z); var vectorX:Number = -currentDisplacement.x / vectorLength; var vectorY:Number = -currentDisplacement.y / vectorLength; var vectorZ:Number = -currentDisplacement.z / vectorLength; // Длина вектора из ближайшей точки в центр сферы var nearestPointToSourceLengthSqr:Number = nearestPointToSourceX * nearestPointToSourceX + nearestPointToSourceY * nearestPointToSourceY + nearestPointToSourceZ * nearestPointToSourceZ; // Проекция вектора из ближайшей точки в центр сферы на нормализованный вектор обратного направления var projectionLength:Number = nearestPointToSourceX * vectorX + nearestPointToSourceY * vectorY + nearestPointToSourceZ * vectorZ; var projectionInsideSphereLengthSqr:Number = _radius2 - nearestPointToSourceLengthSqr + projectionLength * projectionLength; if (projectionInsideSphereLengthSqr > 0) { // Находим расстояние из ближайшей точки до сферы var distance:Number = projectionLength - Math.sqrt(projectionInsideSphereLengthSqr); if (distance < vectorLength) { // Столкновение сферы с ближайшей точкой произошло // Точка столкновения в ближайшей точке collisionPoint.x = collisionPrimitivePoint.x; collisionPoint.y = collisionPrimitivePoint.y; collisionPoint.z = collisionPrimitivePoint.z; // Находим нормаль плоскости столкновения var nearestPointToSourceLength:Number = Math.sqrt(nearestPointToSourceLengthSqr); collisionNormal.x = nearestPointToSourceX / nearestPointToSourceLength; collisionNormal.y = nearestPointToSourceY / nearestPointToSourceLength; collisionNormal.z = nearestPointToSourceZ / nearestPointToSourceLength; // Смещение плоскости столкновения collisionOffset = collisionPoint.x * collisionNormal.x + collisionPoint.y * collisionNormal.y + collisionPoint.z * collisionNormal.z; collisionPrimitive = collisionPrimitiveNearest; } } } } } /** * @private * Определение столкновения сферы с примитивом. * * @param primitive примитив, столкновение с которым проверяется */ private function calculateCollisionWithPrimitiveS(primitive:PolyPrimitive):void { var length:uint = primitive.num; var points:Array = primitive.points; var normal:Point3D = primitive.face.globalNormal; var inside:Boolean = true; for (var i:uint = 0; i < length; i++) { var p1:Point3D = points[i]; var p2:Point3D = points[(i < length - 1) ? (i + 1) : 0]; var edgeX:Number = p2.x - p1.x; var edgeY:Number = p2.y - p1.y; var edgeZ:Number = p2.z - p1.z; var vectorX:Number = collisionPlanePoint.x - p1.x; var vectorY:Number = collisionPlanePoint.y - p1.y; var vectorZ:Number = collisionPlanePoint.z - p1.z; var crossX:Number = vectorY * edgeZ - vectorZ * edgeY; var crossY:Number = vectorZ * edgeX - vectorX * edgeZ; var crossZ:Number = vectorX * edgeY - vectorY * edgeX; if (crossX * normal.x + crossY * normal.y + crossZ * normal.z > 0) { // Точка за пределами полигона inside = false; var edgeLengthSqr:Number = edgeX * edgeX + edgeY * edgeY + edgeZ * edgeZ; var edgeDistanceSqr:Number = (crossX * crossX + crossY * crossY + crossZ * crossZ) / edgeLengthSqr; // Если расстояние до прямой меньше текущего ближайшего if (edgeDistanceSqr < collisionPrimitiveNearestLengthSqr) { // Ищем нормализованный вектор ребра var edgeLength:Number = Math.sqrt(edgeLengthSqr); var edgeNormX:Number = edgeX / edgeLength; var edgeNormY:Number = edgeY / edgeLength; var edgeNormZ:Number = edgeZ / edgeLength; // Находим расстояние до точки перпендикуляра вдоль ребра var t:Number = edgeNormX * vectorX + edgeNormY * vectorY + edgeNormZ * vectorZ; var vectorLengthSqr:Number; if (t < 0) { // Ближайшая точка - первая vectorLengthSqr = vectorX * vectorX + vectorY * vectorY + vectorZ * vectorZ; if (vectorLengthSqr < collisionPrimitiveNearestLengthSqr) { collisionPrimitiveNearestLengthSqr = vectorLengthSqr; collisionPrimitivePoint.x = p1.x; collisionPrimitivePoint.y = p1.y; collisionPrimitivePoint.z = p1.z; collisionPrimitiveNearest = primitive; } } else { if (t > edgeLength) { // Ближайшая точка - вторая vectorX = collisionPlanePoint.x - p2.x; vectorY = collisionPlanePoint.y - p2.y; vectorZ = collisionPlanePoint.z - p2.z; vectorLengthSqr = vectorX * vectorX + vectorY * vectorY + vectorZ * vectorZ; if (vectorLengthSqr < collisionPrimitiveNearestLengthSqr) { collisionPrimitiveNearestLengthSqr = vectorLengthSqr; collisionPrimitivePoint.x = p2.x; collisionPrimitivePoint.y = p2.y; collisionPrimitivePoint.z = p2.z; collisionPrimitiveNearest = primitive; } } else { // Ближайшая точка на ребре collisionPrimitiveNearestLengthSqr = edgeDistanceSqr; collisionPrimitivePoint.x = p1.x + edgeNormX * t; collisionPrimitivePoint.y = p1.y + edgeNormY * t; collisionPrimitivePoint.z = p1.z + edgeNormZ * t; collisionPrimitiveNearest = primitive; } } } } } // Если попали в примитив if (inside) { collisionPrimitive = primitive; } } /** * Проверка на действительное столкновение эллипсоида с плоскостью. */ private function calculateCollisionWithPlaneE(plane:CollisionPlane):void { var normalX:Number = plane.node.normal.x; var normalY:Number = plane.node.normal.y; var normalZ:Number = plane.node.normal.z; // Смещение по направлению к плоскости вдоль нормали. Положительное смещение означает приближение к плоскости, отрицательное -- удаление // от плоскости, в этом случае столкновения не происходит. var displacementAlongNormal:Number = currentDisplacement.x * normalX + currentDisplacement.y * normalY + currentDisplacement.z * normalZ; if (plane.infront) { displacementAlongNormal = -displacementAlongNormal; } // Выходим из функции в случае удаления от плоскости if (displacementAlongNormal < 0) { return; } // Определение ближайшей к плоскости точки эллипсоида var k:Number = _radius / Math.sqrt(normalX * normalX * _scaleX2 + normalY * normalY * _scaleY2 + normalZ * normalZ * _scaleZ2); // Положение точки в локальной системе координат эллипсоида var localClosestX:Number = k * normalX * _scaleX2; var localClosestY:Number = k * normalY * _scaleY2; var localClosestZ:Number = k * normalZ * _scaleZ2; // Глобальные координаты точки var px:Number = collisionSource.x + localClosestX; var py:Number = collisionSource.y + localClosestY; var pz:Number = collisionSource.z + localClosestZ; // Растояние от найденной точки эллипсоида до плоскости var closestPointDistance:Number = px * normalX + py * normalY + pz * normalZ - plane.node.offset; if (!plane.infront) { closestPointDistance = -closestPointDistance; } if (closestPointDistance > plane.sourceOffset) { // Найдена наиболее удалённая точка, расчитываем вторую px = collisionSource.x - localClosestX; py = collisionSource.y - localClosestY; pz = collisionSource.z - localClosestZ; closestPointDistance = px * normalX + py * normalY + pz * normalZ - plane.node.offset; if (!plane.infront) { closestPointDistance = -closestPointDistance; } } // Если расстояние от ближайшей точки эллипсоида до плоскости больше, чем смещение эллипсоида вдоль нормали плоскости, // то столкновения не произошло и нужно завершить выполнение функции if (closestPointDistance > displacementAlongNormal) { return; } // Если добрались до этого места, значит произошло столкновение с плоскостью. Требуется определить точку столкновения // с ближайшим полигоном, лежащим в этой плоскости if (closestPointDistance <= 0 ) { // Эллипсоид пересекается с плоскостью, ищем проекцию ближайшей точки эллипсоида на плоскость if (plane.infront) { collisionPlanePoint.x = px - normalX * closestPointDistance; collisionPlanePoint.y = py - normalY * closestPointDistance; collisionPlanePoint.z = pz - normalZ * closestPointDistance; } else { collisionPlanePoint.x = px + normalX * closestPointDistance; collisionPlanePoint.y = py + normalY * closestPointDistance; collisionPlanePoint.z = pz + normalZ * closestPointDistance; } } else { // Эллипсоид не пересекается с плоскостью, ищем точку контакта var t:Number = closestPointDistance / displacementAlongNormal; collisionPlanePoint.x = px + currentDisplacement.x * t; collisionPlanePoint.y = py + currentDisplacement.y * t; collisionPlanePoint.z = pz + currentDisplacement.z * t; } // Проверяем примитивы плоскости var primitive:*; collisionPrimitiveNearestLengthSqr = Number.MAX_VALUE; collisionPrimitiveNearest = null; if (plane.infront) { if ((primitive = plane.node.primitive) != null) { if (((_collisionSetMode == CollisionSetMode.EXCLUDE) && (collisionSet == null || !(collisionSet[primitive.face._mesh] || collisionSet[primitive.face._surface]))) || ((_collisionSetMode == CollisionSetMode.INCLUDE) && (collisionSet != null) && (collisionSet[primitive.face._mesh] || collisionSet[primitive.face._surface]))) { calculateCollisionWithPrimitiveE(primitive); } } else { for (primitive in plane.node.frontPrimitives) { if (((_collisionSetMode == CollisionSetMode.EXCLUDE) && (collisionSet == null || !(collisionSet[primitive.face._mesh] || collisionSet[primitive.face._surface]))) || ((_collisionSetMode == CollisionSetMode.INCLUDE) && (collisionSet != null) && (collisionSet[primitive.face._mesh] || collisionSet[primitive.face._surface]))) { calculateCollisionWithPrimitiveE(primitive); if (collisionPrimitive != null) { break; } } } } } else { for (primitive in plane.node.backPrimitives) { if (((_collisionSetMode == CollisionSetMode.EXCLUDE) && (collisionSet == null || !(collisionSet[primitive.face._mesh] || collisionSet[primitive.face._surface]))) || ((_collisionSetMode == CollisionSetMode.INCLUDE) && (collisionSet != null) && (collisionSet[primitive.face._mesh] || collisionSet[primitive.face._surface]))) { calculateCollisionWithPrimitiveE(primitive); if (collisionPrimitive != null) { break; } } } } if (collisionPrimitive != null) { // Если точка пересечения попала в примитив // Нормаль плоскости при столкновении - нормаль плоскости примитива if (plane.infront) { collisionNormal.x = normalX; collisionNormal.y = normalY; collisionNormal.z = normalZ; collisionOffset = plane.node.offset; } else { collisionNormal.x = -normalX; collisionNormal.y = -normalY; collisionNormal.z = -normalZ; collisionOffset = -plane.node.offset; } // Радиус эллипсоида в точке столкновения collisionRadius = localClosestX * collisionNormal.x + localClosestY * collisionNormal.y + localClosestZ * collisionNormal.z; if (collisionRadius < 0) { collisionRadius = -collisionRadius; } radiusVector.x = px - collisionSource.x; radiusVector.y = py - collisionSource.y; radiusVector.z = pz - collisionSource.z; // Точка столкновения совпадает с точкой столкновения с плоскостью примитива collisionPoint.x = collisionPlanePoint.x; collisionPoint.y = collisionPlanePoint.y; collisionPoint.z = collisionPlanePoint.z; } else { // Если точка пересечения не попала внутрь примитива, находим пересечение с ближайшей точкой ближайшего примитива // Трансформированная в пространство эллипсоида ближайшая точка на примитиве px = collisionPrimitivePoint.x; py = collisionPrimitivePoint.y; pz = collisionPrimitivePoint.z; var collisionExists:Boolean; // Квадрат расстояния из центра эллипсоида до точки примитива var r2:Number = px*px + py*py + pz*pz; if (r2 < _radius2) { // Точка оказалась внутри эллипсоида, находим точку на поверхности эллипсоида, лежащую на том же радиусе k = _radius / Math.sqrt(r2); px *= k * _scaleX; py *= k * _scaleY; pz *= k * _scaleZ; collisionExists = true; } else { // Точка вне эллипсоида, находим пересечение луча, направленного противоположно скорости эллипсоида из точки // примитива, с поверхностью эллипсоида // Трансформированный в пространство эллипсоида противоположный вектор скорости var vx:Number = - currentDisplacement.x / _scaleX; var vy:Number = - currentDisplacement.y / _scaleY; var vz:Number = - currentDisplacement.z / _scaleZ; // Нахождение точки пересечения сферы и луча, направленного вдоль вектора скорости var a:Number = vx*vx + vy*vy + vz*vz; var b:Number = 2 * (px*vx + py*vy + pz*vz); var c:Number = r2 - _radius2; var d:Number = b*b - 4*a*c; // Решение есть только при действительном дискриминанте квадратного уравнения if (d >=0) { // Выбирается минимальное время, т.к. нужна первая точка пересечения t = -0.5 * (b + Math.sqrt(d)) / a; // Точка лежит на луче только если время положительное if (t >= 0 && t <= 1) { // Координаты точки пересечения луча с эллипсоидом, переведённые обратно в нормальное пространство px = (px + t * vx) * _scaleX; py = (py + t * vy) * _scaleY; pz = (pz + t * vz) * _scaleZ; collisionExists = true; } } } if (collisionExists) { // Противоположная нормаль к эллипсоиду в точке пересечения collisionNormal.x = - px / _scaleX2; collisionNormal.y = - py / _scaleY2; collisionNormal.z = - pz / _scaleZ2; collisionNormal.normalize(); // Радиус эллипсоида в точке столкновения collisionRadius = px * collisionNormal.x + py * collisionNormal.y + pz * collisionNormal.z; if (collisionRadius < 0) { collisionRadius = -collisionRadius; } radiusVector.x = px; radiusVector.y = py; radiusVector.z = pz; // Точка столкновения в ближайшей точке collisionPoint.x = collisionPrimitivePoint.x * _scaleX + currentCoords.x; collisionPoint.y = collisionPrimitivePoint.y * _scaleY + currentCoords.y; collisionPoint.z = collisionPrimitivePoint.z * _scaleZ + currentCoords.z; // Смещение плоскости столкновения collisionOffset = collisionPoint.x * collisionNormal.x + collisionPoint.y * collisionNormal.y + collisionPoint.z * collisionNormal.z; collisionPrimitive = collisionPrimitiveNearest; } } } /** * @private * Определение наличия столкновения эллипсоида с примитивом. Все расчёты выполняются в пространстве эллипсоида, где он выглядит * как сфера. По окончании работы может быть установлена переменная collisionPrimitive в случае попадания точки * столкновения внутрь примитива или collisionPrimitiveNearest в случае столкновения с ребром примитива через * минимальное время. * * @param primitive примитив, столкновение с которым проверяется */ private function calculateCollisionWithPrimitiveE(primitive:PolyPrimitive):void { var length:uint = primitive.num; var points:Array = primitive.points; var normal:Point3D = primitive.face.globalNormal; var inside:Boolean = true; var point1:Point3D; var point2:Point3D = points[length - 1]; p2.x = (point2.x - currentCoords.x) / _scaleX; p2.y = (point2.y - currentCoords.y) / _scaleY; p2.z = (point2.z - currentCoords.z) / _scaleZ; localCollisionPlanePoint.x = (collisionPlanePoint.x - currentCoords.x) / _scaleX; localCollisionPlanePoint.y = (collisionPlanePoint.y - currentCoords.y) / _scaleY; localCollisionPlanePoint.z = (collisionPlanePoint.z - currentCoords.z) / _scaleZ; // Обход всех рёбер примитива for (var i:uint = 0; i < length; i++) { point1 = point2; point2 = points[i]; p1.x = p2.x; p1.y = p2.y; p1.z = p2.z; p2.x = (point2.x - currentCoords.x) / _scaleX; p2.y = (point2.y - currentCoords.y) / _scaleY; p2.z = (point2.z - currentCoords.z) / _scaleZ; // Расчёт векторного произведения вектора ребра на радиус-вектор точки столкновения относительно начала ребра // с целью определения положения точки столкновения относительно полигона var edgeX:Number = p2.x - p1.x; var edgeY:Number = p2.y - p1.y; var edgeZ:Number = p2.z - p1.z; var vectorX:Number = localCollisionPlanePoint.x - p1.x; var vectorY:Number = localCollisionPlanePoint.y - p1.y; var vectorZ:Number = localCollisionPlanePoint.z - p1.z; var crossX:Number = vectorY * edgeZ - vectorZ * edgeY; var crossY:Number = vectorZ * edgeX - vectorX * edgeZ; var crossZ:Number = vectorX * edgeY - vectorY * edgeX; if (crossX * normal.x + crossY * normal.y + crossZ * normal.z > 0) { // Точка за пределами полигона inside = false; var edgeLengthSqr:Number = edgeX * edgeX + edgeY * edgeY + edgeZ * edgeZ; var edgeDistanceSqr:Number = (crossX * crossX + crossY * crossY + crossZ * crossZ) / edgeLengthSqr; // Если расстояние до прямой меньше текущего ближайшего if (edgeDistanceSqr < collisionPrimitiveNearestLengthSqr) { // Ищем нормализованный вектор ребра var edgeLength:Number = Math.sqrt(edgeLengthSqr); var edgeNormX:Number = edgeX / edgeLength; var edgeNormY:Number = edgeY / edgeLength; var edgeNormZ:Number = edgeZ / edgeLength; // Находим расстояние до точки перпендикуляра вдоль ребра var t:Number = edgeNormX * vectorX + edgeNormY * vectorY + edgeNormZ * vectorZ; var vectorLengthSqr:Number; if (t < 0) { // Ближайшая точка - первая vectorLengthSqr = vectorX * vectorX + vectorY * vectorY + vectorZ * vectorZ; if (vectorLengthSqr < collisionPrimitiveNearestLengthSqr) { collisionPrimitiveNearestLengthSqr = vectorLengthSqr; collisionPrimitivePoint.x = p1.x; collisionPrimitivePoint.y = p1.y; collisionPrimitivePoint.z = p1.z; collisionPrimitiveNearest = primitive; } } else { if (t > edgeLength) { // Ближайшая точка - вторая vectorX = localCollisionPlanePoint.x - p2.x; vectorY = localCollisionPlanePoint.y - p2.y; vectorZ = localCollisionPlanePoint.z - p2.z; vectorLengthSqr = vectorX * vectorX + vectorY * vectorY + vectorZ * vectorZ; if (vectorLengthSqr < collisionPrimitiveNearestLengthSqr) { collisionPrimitiveNearestLengthSqr = vectorLengthSqr; collisionPrimitivePoint.x = p2.x; collisionPrimitivePoint.y = p2.y; collisionPrimitivePoint.z = p2.z; collisionPrimitiveNearest = primitive; } } else { // Ближайшая точка на ребре collisionPrimitiveNearestLengthSqr = edgeDistanceSqr; collisionPrimitivePoint.x = p1.x + edgeNormX * t; collisionPrimitivePoint.y = p1.y + edgeNormY * t; collisionPrimitivePoint.z = p1.z + edgeNormZ * t; collisionPrimitiveNearest = primitive; } } } } } // Если попали в примитив if (inside) { collisionPrimitive = primitive; } } } }